Skip to content

Commit 1769457

Browse files
authored
fix(llc,ui): fixing screen share cancelation (#1085)
* fixing screen share cancelation * tweaks * fix * changelog * webrtc dependency bump * changed approach to subscribtion for ios broadcast starting * fix
1 parent 09cfc71 commit 1769457

File tree

17 files changed

+417
-47
lines changed

17 files changed

+417
-47
lines changed

melos.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ command:
2222
device_info_plus: ^12.1.0
2323
share_plus: ^11.0.0
2424
stream_chat_flutter: ^9.17.0
25-
stream_webrtc_flutter: ^1.0.12
25+
stream_webrtc_flutter: ^1.0.13
2626
stream_video: ^0.11.1
2727
stream_video_flutter: ^0.11.1
2828
stream_video_noise_cancellation: ^0.11.1

packages/stream_video/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
# Unreleased
2+
3+
🐞 Fixed
4+
5+
- [Android/iOS] Fixed an issue where screen sharing was not stopped correctly when canceled via the system UI on Android or iOS.
6+
- [iOS] Improved broadcast extension handling — the app now waits for the broadcast picker selection before actually starting screen sharing.
7+
18
## 0.11.1
29

310
🔄 Changed

packages/stream_video/lib/src/call/call.dart

Lines changed: 114 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:async/async.dart' show CancelableOperation;
88
import 'package:collection/collection.dart';
99
import 'package:internet_connection_checker_plus/internet_connection_checker_plus.dart';
1010
import 'package:meta/meta.dart';
11+
import 'package:rxdart/transformers.dart';
1112
import 'package:stream_webrtc_flutter/stream_webrtc_flutter.dart' as rtc;
1213
import 'package:stream_webrtc_flutter/stream_webrtc_flutter.dart';
1314
import 'package:synchronized/synchronized.dart';
@@ -87,6 +88,7 @@ const _idConnect = 6;
8788
const _idAwait = 7;
8889
const _idFastReconnectTimeout = 8;
8990
const _idReconnect = 9;
91+
const _idNativeWebRtc = 10;
9092

9193
const _tag = 'SV:Call';
9294
int _callSeq = 1;
@@ -381,12 +383,20 @@ class Call {
381383
_observeState();
382384
_observeReconnectEvents();
383385
_observeUserId();
386+
_observeNativeWebRtcEventStream();
384387

385388
_logger.v(() => '[_init] initialized');
386389
_initialized = true;
387390
});
388391
}
389392

393+
void _observeNativeWebRtcEventStream() {
394+
_subscriptions.add(
395+
_idNativeWebRtc,
396+
_onNativeWebRtcEvent(),
397+
);
398+
}
399+
390400
void _observeState() {
391401
_subscriptions.add(
392402
_idState,
@@ -458,6 +468,52 @@ class Call {
458468
state.settings.audio.redundantCodingEnabled;
459469
}
460470

471+
StreamSubscription<NativeWebRtcEvent> _onNativeWebRtcEvent() {
472+
return RtcMediaDeviceNotifier.instance.nativeWebRtcEventsStream().listen((
473+
event,
474+
) {
475+
_logger.d(
476+
() => '[_onNativeWebRtcEvent] screenSharingStopped: $event',
477+
);
478+
479+
switch (event) {
480+
case ScreenSharingStoppedEvent _:
481+
if (CurrentPlatform.isIos) {
482+
// On iOS only one broadcast extension can be active at a time
483+
setScreenShareEnabled(enabled: false);
484+
} else {
485+
final trackId = event.data?['trackId'] as String?;
486+
if (trackId != null && state.value.localParticipant != null) {
487+
final track = getTrack(
488+
state.value.localParticipant!.trackIdPrefix,
489+
SfuTrackType.screenShare,
490+
);
491+
492+
if (track?.mediaTrack.id == trackId) {
493+
setScreenShareEnabled(enabled: false);
494+
}
495+
}
496+
}
497+
break;
498+
case ScreenSharingStartedEvent _:
499+
_stateManager.participantSetScreenShareEnabled(
500+
enabled: true,
501+
);
502+
503+
_connectOptions = _connectOptions.copyWith(
504+
screenShare: TrackOption.enabled(
505+
constraints: const ScreenShareConstraints(
506+
useiOSBroadcastExtension: true,
507+
),
508+
),
509+
);
510+
break;
511+
default:
512+
return;
513+
}
514+
});
515+
}
516+
461517
Future<void> _onCoordinatorEvent(StreamCallEvent event) async {
462518
// Return if the event is not for this call.
463519
if (event.callCid != state.value.callCid) return;
@@ -559,7 +615,6 @@ class Call {
559615
return _stateManager.callMetadataChanged(event.metadata);
560616
case StreamCallSessionStartedEvent _:
561617
return _stateManager.callMetadataChanged(event.metadata);
562-
563618
default:
564619
break;
565620
}
@@ -1906,15 +1961,21 @@ class Call {
19061961
if (cameraOption is TrackProvided) {
19071962
return _setLocalTrack(cameraOption.track);
19081963
} else if (cameraOption is TrackEnabled) {
1964+
final constraints = cameraOption.constraints is CameraConstraints
1965+
? cameraOption.constraints as CameraConstraints?
1966+
: null;
1967+
19091968
return setCameraEnabled(
19101969
enabled: true,
1911-
constraints: CameraConstraints(
1912-
facingMode: facingMode,
1913-
deviceId: deviceId,
1914-
params:
1915-
targetResolution?.toVideoParams() ??
1916-
RtcVideoParametersPresets.h720_16x9,
1917-
),
1970+
constraints:
1971+
constraints ??
1972+
CameraConstraints(
1973+
facingMode: facingMode,
1974+
deviceId: deviceId,
1975+
params:
1976+
targetResolution?.toVideoParams() ??
1977+
RtcVideoParametersPresets.h720_16x9,
1978+
),
19181979
);
19191980
}
19201981

@@ -1925,7 +1986,10 @@ class Call {
19251986
if (microphoneOption is TrackProvided) {
19261987
await _setLocalTrack(microphoneOption.track);
19271988
} else if (microphoneOption is TrackEnabled) {
1928-
await setMicrophoneEnabled(enabled: true);
1989+
final constraints = microphoneOption.constraints is AudioConstraints
1990+
? microphoneOption.constraints as AudioConstraints?
1991+
: null;
1992+
await setMicrophoneEnabled(enabled: true, constraints: constraints);
19291993
}
19301994
}
19311995

@@ -1936,15 +2000,22 @@ class Call {
19362000
if (screenShareOption is TrackProvided) {
19372001
await _setLocalTrack(screenShareOption.track);
19382002
} else if (screenShareOption is TrackEnabled) {
2003+
final constraints =
2004+
screenShareOption.constraints is ScreenShareConstraints
2005+
? screenShareOption.constraints as ScreenShareConstraints?
2006+
: null;
2007+
19392008
await setScreenShareEnabled(
19402009
enabled: true,
1941-
constraints: ScreenShareConstraints(
1942-
params:
1943-
targetResolution?.toVideoParams(
1944-
defaultBitrate: RtcVideoParametersPresets.k1080pBitrate,
1945-
) ??
1946-
RtcVideoParametersPresets.h1080_16x9,
1947-
),
2010+
constraints:
2011+
constraints ??
2012+
ScreenShareConstraints(
2013+
params:
2014+
targetResolution?.toVideoParams(
2015+
defaultBitrate: RtcVideoParametersPresets.k1080pBitrate,
2016+
) ??
2017+
RtcVideoParametersPresets.h1080_16x9,
2018+
),
19482019
);
19492020
}
19502021
}
@@ -1962,13 +2033,19 @@ class Call {
19622033
final mediaConstraints = track.mediaConstraints;
19632034
if (mediaConstraints is AudioConstraints) {
19642035
_logger.v(() => '[setLocalTrack]: setMicrophoneEnabled true');
1965-
await setMicrophoneEnabled(enabled: true);
2036+
await setMicrophoneEnabled(
2037+
enabled: true,
2038+
constraints: mediaConstraints,
2039+
);
19662040
} else if (mediaConstraints is CameraConstraints) {
19672041
_logger.v(() => '[setLocalTrack]: setCameraEnabled true');
1968-
await setCameraEnabled(enabled: true);
2042+
await setCameraEnabled(enabled: true, constraints: mediaConstraints);
19692043
} else if (mediaConstraints is ScreenShareConstraints) {
19702044
_logger.v(() => '[setLocalTrack] setScreenShareEnabled true');
1971-
await setScreenShareEnabled(enabled: true);
2045+
await setScreenShareEnabled(
2046+
enabled: true,
2047+
constraints: mediaConstraints,
2048+
);
19722049
} else {
19732050
streamLog.e(
19742051
_tag,
@@ -2749,7 +2826,9 @@ class Call {
27492826
);
27502827

27512828
_connectOptions = _connectOptions.copyWith(
2752-
camera: enabled ? TrackOption.enabled() : TrackOption.disabled(),
2829+
camera: enabled
2830+
? TrackOption.enabled(constraints: constraints)
2831+
: TrackOption.disabled(),
27532832
cameraFacingMode: constraints?.facingMode ?? FacingMode.user,
27542833
);
27552834
}
@@ -2811,7 +2890,9 @@ class Call {
28112890
);
28122891

28132892
_connectOptions = _connectOptions.copyWith(
2814-
microphone: enabled ? TrackOption.enabled() : TrackOption.disabled(),
2893+
microphone: enabled
2894+
? TrackOption.enabled(constraints: constraints)
2895+
: TrackOption.disabled(),
28152896
);
28162897
}
28172898

@@ -2848,16 +2929,27 @@ class Call {
28482929
) ??
28492930
Result.error('Call session is null, cannot start screen share');
28502931

2932+
// In case of iOS Broadcast Extension, we don't update the state here
2933+
// We listen to the ScreenShareStarted event instead
2934+
if (CurrentPlatform.isIos &&
2935+
constraints is ScreenShareConstraints &&
2936+
constraints.useiOSBroadcastExtension) {
2937+
return result.map((_) => none);
2938+
}
2939+
28512940
if (result.isSuccess) {
28522941
_stateManager.participantSetScreenShareEnabled(
28532942
enabled: enabled,
28542943
);
28552944

28562945
_connectOptions = _connectOptions.copyWith(
2857-
screenShare: enabled ? TrackOption.enabled() : TrackOption.disabled(),
2946+
screenShare: enabled
2947+
? TrackOption.enabled(constraints: updatedConstraints)
2948+
: TrackOption.disabled(),
28582949
);
28592950

28602951
if (enabled) {
2952+
// [web only] Automatically stop screen share when the track ends
28612953
result.getDataOrNull()?.mediaTrack.onEnded = () {
28622954
setScreenShareEnabled(enabled: false);
28632955
};

packages/stream_video/lib/src/call/call_connect_options.dart

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@ abstract class TrackOption with EquatableMixin {
111111
factory TrackOption.fromSetting({required bool enabled}) =>
112112
enabled ? TrackOption.enabled() : TrackOption.disabled();
113113

114-
factory TrackOption.enabled() {
115-
return TrackEnabled._instance;
114+
factory TrackOption.enabled({MediaConstraints? constraints}) {
115+
return TrackEnabled._(constraints: constraints);
116116
}
117117

118118
factory TrackOption.disabled() {
@@ -140,12 +140,15 @@ class TrackDisabled extends TrackOption {
140140
}
141141

142142
class TrackEnabled extends TrackOption {
143-
const TrackEnabled._();
143+
const TrackEnabled._({this.constraints});
144144

145-
static const TrackEnabled _instance = TrackEnabled._();
145+
final MediaConstraints? constraints;
146146

147147
@override
148-
String toString() => 'enabled';
148+
List<Object?> get props => [constraints];
149+
150+
@override
151+
String toString() => 'enabled($constraints)';
149152
}
150153

151154
class TrackProvided<T extends MediaConstraints> extends TrackOption {

packages/stream_video/lib/src/call/session/call_session.dart

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -770,7 +770,18 @@ class CallSession extends Disposable {
770770
if (track == null) return;
771771

772772
// Only stop remote tracks. Local tracks are stopped by the user.
773-
if (track is! RtcRemoteTrack) return;
773+
if (track is! RtcRemoteTrack) {
774+
final localTrack = rtcManager?.getTrack(track.trackId);
775+
if (localTrack != null &&
776+
localTrack.isScreenShareTrack &&
777+
localTrack.mediaTrack.enabled) {
778+
// If the unpublished track is a local screen share track and it's still enabled,
779+
// disable screen sharing. It means the screen sharing was muted by the server.
780+
await setScreenShareEnabled(false);
781+
}
782+
783+
return;
784+
}
774785

775786
await track.stop();
776787
}

0 commit comments

Comments
 (0)