Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion melos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ command:
device_info_plus: ^12.1.0
share_plus: ^11.0.0
stream_chat_flutter: ^9.17.0
stream_webrtc_flutter: ^2.2.3
stream_webrtc_flutter: ^2.2.4
stream_video: ^1.2.2
stream_video_flutter: ^1.2.2
stream_video_noise_cancellation: ^1.2.2
Expand Down
66 changes: 38 additions & 28 deletions packages/stream_video/lib/src/call/session/call_session.dart
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,25 @@ class CallSession extends Disposable {
});
}

Future<void> _ensureAndroidAudioConfiguration() async {
if (CurrentPlatform.isAndroid &&
_streamVideo.options.androidAudioConfiguration != null) {
try {
await rtc.Helper.setAndroidAudioConfiguration(
_streamVideo.options.androidAudioConfiguration!,
);
_logger.v(
() => '[_ensureAndroidAudioConfiguration] Configuration applied',
);
} catch (e) {
_logger.w(
() =>
'[_ensureAndroidAudioConfiguration] Failed to apply Android audio configuration: $e',
);
}
}
}

Future<sfu_events.ReconnectDetails> getReconnectDetails(
SfuReconnectionStrategy strategy, {
String? migratingFromSfuId,
Expand Down Expand Up @@ -295,6 +314,10 @@ class CallSession extends Disposable {

_logger.v(() => '[start] sfu joined: $event');

// Ensure WebRTC initialization completes before creating rtcManager
await _streamVideo.webrtcInitializationCompleter.future;
await _ensureAndroidAudioConfiguration();

if (isAnonymousUser) {
rtcManager =
await rtcManagerFactory.makeRtcManager(
Expand Down Expand Up @@ -339,20 +362,6 @@ class CallSession extends Disposable {
await onRtcManagerCreatedCallback?.call(rtcManager!);
_rtcManagerSubject!.add(rtcManager!);

// Set Android audio configuration right after creating rtcManager
if (CurrentPlatform.isAndroid &&
_streamVideo.options.androidAudioConfiguration != null) {
try {
await rtc.Helper.setAndroidAudioConfiguration(
_streamVideo.options.androidAudioConfiguration!,
);
} catch (e) {
_logger.w(
() => '[start] Failed to set Android audio configuration: $e',
);
}
}

stateManager.sfuPinsUpdated(event.callState.pins);

final environment = ClientEnvironment(
Expand Down Expand Up @@ -464,6 +473,8 @@ class CallSession extends Disposable {

stateManager.sfuPinsUpdated(event.callState.pins);

await _ensureAndroidAudioConfiguration();

result = Result.success(
(
callState: event.callState,
Expand Down Expand Up @@ -696,6 +707,10 @@ class CallSession extends Disposable {
// Only start remote tracks. Local tracks are started by the user.
if (track is! RtcRemoteTrack) return;

if (track.isAudioTrack) {
await _ensureAndroidAudioConfiguration();
}

await track.start();
}

Expand Down Expand Up @@ -900,19 +915,8 @@ class CallSession extends Disposable {
) async {
_logger.d(() => '[onRemoteTrackReceived] remoteTrack: $remoteTrack');

if (CurrentPlatform.isAndroid &&
remoteTrack.isAudioTrack &&
_streamVideo.options.androidAudioConfiguration != null) {
try {
await rtc.Helper.setAndroidAudioConfiguration(
_streamVideo.options.androidAudioConfiguration!,
);
} catch (e) {
_logger.w(
() =>
'[onRemoteTrackReceived] Failed to apply Android audio configuration: $e',
);
}
if (remoteTrack.isAudioTrack) {
await _ensureAndroidAudioConfiguration();
}

// Start the track.
Expand Down Expand Up @@ -968,7 +972,13 @@ class CallSession extends Disposable {
return Result.error('Unable to set speaker device, Call not connected');
}

return rtcManager.setAudioOutputDevice(device: device);
final result = await rtcManager.setAudioOutputDevice(device: device);

if (result.isSuccess && CurrentPlatform.isAndroid) {
await _ensureAndroidAudioConfiguration();
}

return result;
}

Future<Result<RtcLocalTrack>> setCameraEnabled(
Expand Down
23 changes: 13 additions & 10 deletions packages/stream_video/lib/src/stream_video.dart
Original file line number Diff line number Diff line change
Expand Up @@ -188,16 +188,16 @@ class StreamVideo extends Disposable {
_state.user.value = user;

if (CurrentPlatform.isAndroid || CurrentPlatform.isIos) {
unawaited(
rtc.WebRTC.initialize(
options: {
if (CurrentPlatform.isAndroid &&
options.androidAudioConfiguration != null)
'androidAudioConfiguration': options.androidAudioConfiguration!
.toMap(),
},
),
);
rtc.WebRTC.initialize(
options: {
if (CurrentPlatform.isAndroid &&
options.androidAudioConfiguration != null)
'androidAudioConfiguration': options.androidAudioConfiguration!
.toMap(),
},
).then((_) {
webrtcInitializationCompleter.complete();
});
}

final tokenProvider = switch (user.type) {
Expand Down Expand Up @@ -298,6 +298,9 @@ class StreamVideo extends Disposable {

StreamVideoOptions get options => _options;

@internal
Completer<void> webrtcInitializationCompleter = Completer();

final _tokenManager = TokenManager();
final _subscriptions = Subscriptions();

Expand Down
2 changes: 1 addition & 1 deletion packages/stream_video/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ dependencies:
rxdart: ^0.28.0
sdp_transform: ^0.3.2
state_notifier: ^1.0.0
stream_webrtc_flutter: ^2.2.3
stream_webrtc_flutter: ^2.2.4
synchronized: ^3.1.0
system_info2: ^4.0.0
tart: ^0.6.0
Expand Down
2 changes: 1 addition & 1 deletion packages/stream_video_filters/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ dependencies:
sdk: flutter
plugin_platform_interface: ^2.0.2
stream_video: ^1.2.2
stream_webrtc_flutter: ^2.2.3
stream_webrtc_flutter: ^2.2.4

dev_dependencies:
flutter_lints: ^6.0.0
Expand Down
1 change: 1 addition & 0 deletions packages/stream_video_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* Added `reconnectReason` to reconnect details for sfu logs.
* Fixed race condition where automatic ICE restart could interfere with fast reconnect, causing subscriber video to not recover.
* [iOS] Fixed CallKit event suppression to avoid repeated mute toggle loops.
* [Android] Fixed video flickering in Skia renderer by scoping renderer keys with prefix and using stable participant-based keys.

## 1.2.2

Expand Down
2 changes: 1 addition & 1 deletion packages/stream_video_flutter/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ dependencies:
stream_video: ^1.2.2
stream_video_flutter: ^1.2.2
stream_video_push_notification: ^1.2.2
stream_webrtc_flutter: ^2.2.3
stream_webrtc_flutter: ^2.2.4

dependency_overrides:
stream_video:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class StreamCallParticipant extends StatelessWidget {
super.key,
required this.call,
required this.participant,
this.rendererScopePrefix,
this.videoFit,
this.backgroundColor,
this.borderRadius,
Expand Down Expand Up @@ -121,6 +122,9 @@ class StreamCallParticipant extends StatelessWidget {
/// Callback that is called when the size of the participant widget changes.
final ValueSetter<Size>? onSizeChanged;

/// Optional prefix to scope renderer keys (e.g. PiP vs main view).
final String? rendererScopePrefix;

@override
Widget build(BuildContext context) {
final theme = StreamCallParticipantTheme.of(context);
Expand Down Expand Up @@ -194,6 +198,10 @@ class StreamCallParticipant extends StatelessWidget {
return Stack(
children: [
StreamVideoRenderer(
key: ValueKey(
'${rendererScopePrefix ?? ''}${participant.uniqueParticipantKey}-video',
),
rendererScopePrefix: rendererScopePrefix,
call: call,
participant: participant,
videoTrackType: SfuTrackType.video,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,10 @@ class StreamLivestreamHosts extends StatefulWidget {
CallParticipantState host,
) {
return StreamCallParticipant(
// We use the sessionId as the key to map the state to the participant.
key: Key(host.uniqueParticipantKey),
key: ValueKey(
'${host.uniqueParticipantKey}-livehost-video',
),
rendererScopePrefix: 'livehost',
call: call,
participant: host,
);
Expand Down Expand Up @@ -97,7 +99,7 @@ class _StreamLivestreamHostsState extends State<StreamLivestreamHosts>
Widget _buildScreenShareContent(CallParticipantState host) {
return widget.screenShareContentBuilder?.call(context, widget.call, host) ??
ScreenShareContent(
key: ValueKey('${host.uniqueParticipantKey} - screenShareContent'),
key: ValueKey('${host.uniqueParticipantKey}-livehost-screenshare'),
call: widget.call,
participant: host,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ class ScreenShareCallParticipantsContent extends StatelessWidget {
CallParticipantState participant,
) {
return StreamCallParticipant(
key: ValueKey(participant.uniqueParticipantKey),
key: ValueKey(
'${participant.uniqueParticipantKey}-screenshare-video',
),
rendererScopePrefix: 'screenshareContent',
call: call,
participant: participant,
);
Expand Down Expand Up @@ -74,8 +77,9 @@ class ScreenShareCallParticipantsContent extends StatelessWidget {
children: [
ScreenShareContent(
key: ValueKey(
'${participant.uniqueParticipantKey} - screenShareContent',
'${participant.uniqueParticipantKey}-screenShareContent',
),
rendererScopePrefix: 'screenshareContent',
call: call,
participant: participant,
),
Expand Down Expand Up @@ -118,6 +122,7 @@ class ScreenShareContent extends StatefulWidget {
super.key,
required this.call,
required this.participant,
this.rendererScopePrefix,
this.backgroundColor = const Color(0xFF272A30),
this.borderRadius = const BorderRadius.all(Radius.circular(8)),
});
Expand All @@ -128,6 +133,9 @@ class ScreenShareContent extends StatefulWidget {
/// The participant that shares their screen.
final CallParticipantState participant;

/// Optional prefix to scope renderer keys (e.g. PiP vs main view).
final String? rendererScopePrefix;

/// The background color for the video.
final Color backgroundColor;

Expand Down Expand Up @@ -171,6 +179,10 @@ class _ScreenShareContentState extends State<ScreenShareContent> {
}
},
child: StreamVideoRenderer(
key: ValueKey(
'${widget.rendererScopePrefix ?? ''}${widget.participant.uniqueParticipantKey}-screen-share',
),
rendererScopePrefix: widget.rendererScopePrefix,
call: widget.call,
participant: widget.participant,
videoFit: VideoFit.contain,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ class _StreamCallContentState extends State<StreamCallContent> {
late bool _isScreenShareEnabled;
late CallStatus _status;

final _androidPipManager = AndroidPipManager.instance();
late bool _isInPictureInPictureMode =
_androidPipManager.isInPictureInPictureMode;

StreamSubscription<({CallStatus status, bool isScreenShareEnabled})>?
_callStateSubscription;

Expand All @@ -115,6 +119,10 @@ class _StreamCallContentState extends State<StreamCallContent> {
void initState() {
super.initState();
_startListeningToCallState();

_androidPipManager.addOnPictureInPictureModeChangedListener(
_handlePictureInPictureModeChanged,
);
}

@override
Expand All @@ -130,9 +138,18 @@ class _StreamCallContentState extends State<StreamCallContent> {
@override
void dispose() {
_callStateSubscription?.cancel();
_androidPipManager.removeOnPictureInPictureModeChangedListener(
_handlePictureInPictureModeChanged,
);
super.dispose();
}

void _handlePictureInPictureModeChanged(bool isInPictureInPictureMode) {
setState(() {
_isInPictureInPictureMode = isInPictureInPictureMode;
});
}

void _startListeningToCallState() {
final callState = call.state.value;
_status = callState.status;
Expand Down Expand Up @@ -190,11 +207,12 @@ class _StreamCallContentState extends State<StreamCallContent> {
call: call,
configuration: widget.pictureInPictureConfiguration,
),
widget.callParticipantsWidgetBuilder?.call(context, call) ??
StreamCallParticipants(
call: call,
layoutMode: widget.layoutMode,
),
if (!_isInPictureInPictureMode)
widget.callParticipantsWidgetBuilder?.call(context, call) ??
StreamCallParticipants(
call: call,
layoutMode: widget.layoutMode,
),
],
);
} else {
Expand Down
Loading
Loading