Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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: ^1.0.12
stream_webrtc_flutter: ^1.0.13
stream_video: ^0.11.1
stream_video_flutter: ^0.11.1
stream_video_noise_cancellation: ^0.11.1
Expand Down
7 changes: 7 additions & 0 deletions packages/stream_video/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# Unreleased

🐞 Fixed

- [Android/iOS] Fixed an issue where screen sharing was not stopped correctly when canceled via the system UI on Android or iOS.
- [iOS] Improved broadcast extension handling — the app now waits for the broadcast picker selection before actually starting screen sharing.

## 0.11.1

🔄 Changed
Expand Down
136 changes: 114 additions & 22 deletions packages/stream_video/lib/src/call/call.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:async/async.dart' show CancelableOperation;
import 'package:collection/collection.dart';
import 'package:internet_connection_checker_plus/internet_connection_checker_plus.dart';
import 'package:meta/meta.dart';
import 'package:rxdart/transformers.dart';
import 'package:stream_webrtc_flutter/stream_webrtc_flutter.dart' as rtc;
import 'package:stream_webrtc_flutter/stream_webrtc_flutter.dart';
import 'package:synchronized/synchronized.dart';
Expand Down Expand Up @@ -87,6 +88,7 @@ const _idConnect = 6;
const _idAwait = 7;
const _idFastReconnectTimeout = 8;
const _idReconnect = 9;
const _idNativeWebRtc = 10;

const _tag = 'SV:Call';
int _callSeq = 1;
Expand Down Expand Up @@ -381,12 +383,20 @@ class Call {
_observeState();
_observeReconnectEvents();
_observeUserId();
_observeNativeWebRtcEventStream();

_logger.v(() => '[_init] initialized');
_initialized = true;
});
}

void _observeNativeWebRtcEventStream() {
_subscriptions.add(
_idNativeWebRtc,
_onNativeWebRtcEvent(),
);
}

void _observeState() {
_subscriptions.add(
_idState,
Expand Down Expand Up @@ -458,6 +468,52 @@ class Call {
state.settings.audio.redundantCodingEnabled;
}

StreamSubscription<NativeWebRtcEvent> _onNativeWebRtcEvent() {
return RtcMediaDeviceNotifier.instance.nativeWebRtcEventsStream().listen((
event,
) {
_logger.d(
() => '[_onNativeWebRtcEvent] screenSharingStopped: $event',
);

switch (event) {
case ScreenSharingStoppedEvent _:
if (CurrentPlatform.isIos) {
// On iOS only one broadcast extension can be active at a time
setScreenShareEnabled(enabled: false);
} else {
final trackId = event.data?['trackId'] as String?;
if (trackId != null && state.value.localParticipant != null) {
final track = getTrack(
state.value.localParticipant!.trackIdPrefix,
SfuTrackType.screenShare,
);

if (track?.mediaTrack.id == trackId) {
setScreenShareEnabled(enabled: false);
}
}
}
break;
case ScreenSharingStartedEvent _:
_stateManager.participantSetScreenShareEnabled(
enabled: true,
);

_connectOptions = _connectOptions.copyWith(
screenShare: TrackOption.enabled(
constraints: const ScreenShareConstraints(
useiOSBroadcastExtension: true,
),
),
);
break;
default:
return;
}
});
}

Future<void> _onCoordinatorEvent(StreamCallEvent event) async {
// Return if the event is not for this call.
if (event.callCid != state.value.callCid) return;
Expand Down Expand Up @@ -559,7 +615,6 @@ class Call {
return _stateManager.callMetadataChanged(event.metadata);
case StreamCallSessionStartedEvent _:
return _stateManager.callMetadataChanged(event.metadata);

default:
break;
}
Expand Down Expand Up @@ -1906,15 +1961,21 @@ class Call {
if (cameraOption is TrackProvided) {
return _setLocalTrack(cameraOption.track);
} else if (cameraOption is TrackEnabled) {
final constraints = cameraOption.constraints is CameraConstraints
? cameraOption.constraints as CameraConstraints?
: null;

return setCameraEnabled(
enabled: true,
constraints: CameraConstraints(
facingMode: facingMode,
deviceId: deviceId,
params:
targetResolution?.toVideoParams() ??
RtcVideoParametersPresets.h720_16x9,
),
constraints:
constraints ??
CameraConstraints(
facingMode: facingMode,
deviceId: deviceId,
params:
targetResolution?.toVideoParams() ??
RtcVideoParametersPresets.h720_16x9,
),
);
}

Expand All @@ -1925,7 +1986,10 @@ class Call {
if (microphoneOption is TrackProvided) {
await _setLocalTrack(microphoneOption.track);
} else if (microphoneOption is TrackEnabled) {
await setMicrophoneEnabled(enabled: true);
final constraints = microphoneOption.constraints is AudioConstraints
? microphoneOption.constraints as AudioConstraints?
: null;
await setMicrophoneEnabled(enabled: true, constraints: constraints);
}
}

Expand All @@ -1936,15 +2000,22 @@ class Call {
if (screenShareOption is TrackProvided) {
await _setLocalTrack(screenShareOption.track);
} else if (screenShareOption is TrackEnabled) {
final constraints =
screenShareOption.constraints is ScreenShareConstraints
? screenShareOption.constraints as ScreenShareConstraints?
: null;

await setScreenShareEnabled(
enabled: true,
constraints: ScreenShareConstraints(
params:
targetResolution?.toVideoParams(
defaultBitrate: RtcVideoParametersPresets.k1080pBitrate,
) ??
RtcVideoParametersPresets.h1080_16x9,
),
constraints:
constraints ??
ScreenShareConstraints(
params:
targetResolution?.toVideoParams(
defaultBitrate: RtcVideoParametersPresets.k1080pBitrate,
) ??
RtcVideoParametersPresets.h1080_16x9,
),
);
}
}
Expand All @@ -1962,13 +2033,19 @@ class Call {
final mediaConstraints = track.mediaConstraints;
if (mediaConstraints is AudioConstraints) {
_logger.v(() => '[setLocalTrack]: setMicrophoneEnabled true');
await setMicrophoneEnabled(enabled: true);
await setMicrophoneEnabled(
enabled: true,
constraints: mediaConstraints,
);
} else if (mediaConstraints is CameraConstraints) {
_logger.v(() => '[setLocalTrack]: setCameraEnabled true');
await setCameraEnabled(enabled: true);
await setCameraEnabled(enabled: true, constraints: mediaConstraints);
} else if (mediaConstraints is ScreenShareConstraints) {
_logger.v(() => '[setLocalTrack] setScreenShareEnabled true');
await setScreenShareEnabled(enabled: true);
await setScreenShareEnabled(
enabled: true,
constraints: mediaConstraints,
);
} else {
streamLog.e(
_tag,
Expand Down Expand Up @@ -2749,7 +2826,9 @@ class Call {
);

_connectOptions = _connectOptions.copyWith(
camera: enabled ? TrackOption.enabled() : TrackOption.disabled(),
camera: enabled
? TrackOption.enabled(constraints: constraints)
: TrackOption.disabled(),
cameraFacingMode: constraints?.facingMode ?? FacingMode.user,
);
}
Expand Down Expand Up @@ -2811,7 +2890,9 @@ class Call {
);

_connectOptions = _connectOptions.copyWith(
microphone: enabled ? TrackOption.enabled() : TrackOption.disabled(),
microphone: enabled
? TrackOption.enabled(constraints: constraints)
: TrackOption.disabled(),
);
}

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

// In case of iOS Broadcast Extension, we don't update the state here
// We listen to the ScreenShareStarted event instead
if (CurrentPlatform.isIos &&
constraints is ScreenShareConstraints &&
constraints.useiOSBroadcastExtension) {
return result.map((_) => none);
}

if (result.isSuccess) {
_stateManager.participantSetScreenShareEnabled(
enabled: enabled,
);

_connectOptions = _connectOptions.copyWith(
screenShare: enabled ? TrackOption.enabled() : TrackOption.disabled(),
screenShare: enabled
? TrackOption.enabled(constraints: updatedConstraints)
: TrackOption.disabled(),
);

if (enabled) {
// [web only] Automatically stop screen share when the track ends
result.getDataOrNull()?.mediaTrack.onEnded = () {
setScreenShareEnabled(enabled: false);
};
Expand Down
13 changes: 8 additions & 5 deletions packages/stream_video/lib/src/call/call_connect_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ abstract class TrackOption with EquatableMixin {
factory TrackOption.fromSetting({required bool enabled}) =>
enabled ? TrackOption.enabled() : TrackOption.disabled();

factory TrackOption.enabled() {
return TrackEnabled._instance;
factory TrackOption.enabled({MediaConstraints? constraints}) {
return TrackEnabled._(constraints: constraints);
}

factory TrackOption.disabled() {
Expand Down Expand Up @@ -140,12 +140,15 @@ class TrackDisabled extends TrackOption {
}

class TrackEnabled extends TrackOption {
const TrackEnabled._();
const TrackEnabled._({this.constraints});

static const TrackEnabled _instance = TrackEnabled._();
final MediaConstraints? constraints;

@override
String toString() => 'enabled';
List<Object?> get props => [constraints];

@override
String toString() => 'enabled($constraints)';
}

class TrackProvided<T extends MediaConstraints> extends TrackOption {
Expand Down
13 changes: 12 additions & 1 deletion packages/stream_video/lib/src/call/session/call_session.dart
Original file line number Diff line number Diff line change
Expand Up @@ -770,7 +770,18 @@ class CallSession extends Disposable {
if (track == null) return;

// Only stop remote tracks. Local tracks are stopped by the user.
if (track is! RtcRemoteTrack) return;
if (track is! RtcRemoteTrack) {
final localTrack = rtcManager?.getTrack(track.trackId);
if (localTrack != null &&
localTrack.isScreenShareTrack &&
localTrack.mediaTrack.enabled) {
// If the unpublished track is a local screen share track and it's still enabled,
// disable screen sharing. It means the screen sharing was muted by the server.
await setScreenShareEnabled(false);
}

return;
}

await track.stop();
}
Expand Down
Loading
Loading