Skip to content

Commit b41f09f

Browse files
authored
fix: Picture in Picture improvements (#857)
* pip improvements * tweaks
1 parent 3639417 commit b41f09f

File tree

17 files changed

+225
-100
lines changed

17 files changed

+225
-100
lines changed

dogfooding/lib/di/injector.dart

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// 📦 Package imports:
22
import 'package:flutter/foundation.dart';
3-
import 'package:flutter/material.dart';
43
// 🌎 Project imports:
54
import 'package:flutter_dogfooding/core/repos/app_preferences.dart';
65
import 'package:flutter_dogfooding/core/repos/user_chat_repository.dart';
@@ -155,8 +154,8 @@ StreamVideo _initStreamVideo(
155154
tokenLoader: tokenLoader,
156155
options: const StreamVideoOptions(
157156
logPriority: Priority.verbose,
158-
muteAudioWhenInBackground: true,
159-
muteVideoWhenInBackground: true,
157+
muteAudioWhenInBackground: false,
158+
muteVideoWhenInBackground: false,
160159
keepConnectionsAliveWhenInBackground: true,
161160
),
162161
pushNotificationManagerProvider: StreamVideoPushNotificationManager.create(

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

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'dart:typed_data';
66
import 'package:collection/collection.dart';
77
import 'package:internet_connection_checker_plus/internet_connection_checker_plus.dart';
88
import 'package:meta/meta.dart';
9+
import 'package:stream_webrtc_flutter/stream_webrtc_flutter.dart' as rtc;
910
import 'package:stream_webrtc_flutter/stream_webrtc_flutter.dart';
1011
import 'package:synchronized/synchronized.dart';
1112

@@ -1979,6 +1980,24 @@ class Call {
19791980
return result.map((_) => none);
19801981
}
19811982

1983+
Future<Result<bool>> setMultitaskingCameraAccessEnabled(bool enabled) async {
1984+
if (CurrentPlatform.isIos) {
1985+
try {
1986+
final result =
1987+
await rtc.Helper.enableIOSMultitaskingCameraAccess(enabled);
1988+
return Result.success(result);
1989+
} catch (error, stackTrace) {
1990+
_logger.e(() => 'Failed to set multitasking camera access: $error');
1991+
return Result.error(
1992+
'Failed to set multitasking camera access',
1993+
stackTrace,
1994+
);
1995+
}
1996+
}
1997+
1998+
return const Result.success(false);
1999+
}
2000+
19822001
Future<Result<None>> setVideoInputDevice(RtcMediaDevice device) async {
19832002
final result = await _session?.setVideoInputDevice(device) ??
19842003
Result.error('Session is null');
@@ -1998,14 +2017,19 @@ class Call {
19982017
if (enabled && !hasPermission(CallPermission.sendVideo)) {
19992018
return Result.error('Missing permission to send video');
20002019
}
2001-
20022020
final result =
20032021
await _session?.setCameraEnabled(enabled, constraints: constraints) ??
20042022
Result.error('Session is null');
20052023

20062024
if (result.isSuccess) {
2025+
// Set multitasking camera access for iOS
2026+
final multitaskingResult = await setMultitaskingCameraAccessEnabled(
2027+
enabled && !_streamVideo.muteVideoWhenInBackground,
2028+
);
2029+
20072030
_stateManager.participantSetCameraEnabled(
20082031
enabled: enabled,
2032+
iOSMultitaskingCameraAccessEnabled: multitaskingResult.getDataOrNull(),
20092033
);
20102034

20112035
_connectOptions = _connectOptions.copyWith(

packages/stream_video/lib/src/call/state/mixins/state_participant_mixin.dart

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -245,8 +245,13 @@ mixin StateParticipantMixin on StateNotifier<CallState> {
245245

246246
void participantSetCameraEnabled({
247247
required bool enabled,
248+
bool? iOSMultitaskingCameraAccessEnabled,
248249
}) {
249-
return _toggleTrackType(SfuTrackType.video, enabled);
250+
return _toggleTrackType(
251+
SfuTrackType.video,
252+
enabled,
253+
iOSMultitaskingCameraAccessEnabled: iOSMultitaskingCameraAccessEnabled,
254+
);
250255
}
251256

252257
void participantSetMicrophoneEnabled({
@@ -263,9 +268,11 @@ mixin StateParticipantMixin on StateNotifier<CallState> {
263268

264269
void _toggleTrackType(
265270
SfuTrackType trackType,
266-
bool enabled,
267-
) {
271+
bool enabled, {
272+
bool? iOSMultitaskingCameraAccessEnabled,
273+
}) {
268274
state = state.copyWith(
275+
iOSMultitaskingCameraAccessEnabled: iOSMultitaskingCameraAccessEnabled,
269276
callParticipants: state.callParticipants.map((participant) {
270277
if (participant.isLocal) {
271278
final publishedTracks = participant.publishedTracks;

packages/stream_video/lib/src/call_state.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class CallState extends Equatable {
4848
blockedUserIds: const [],
4949
participantCount: 0,
5050
anonymousParticipantCount: 0,
51+
iOSMultitaskingCameraAccessEnabled: false,
5152
custom: const {},
5253
);
5354
}
@@ -87,6 +88,7 @@ class CallState extends Equatable {
8788
required this.blockedUserIds,
8889
required this.participantCount,
8990
required this.anonymousParticipantCount,
91+
required this.iOSMultitaskingCameraAccessEnabled,
9092
required this.custom,
9193
});
9294

@@ -124,6 +126,7 @@ class CallState extends Equatable {
124126
final List<String> blockedUserIds;
125127
final int participantCount;
126128
final int anonymousParticipantCount;
129+
final bool iOSMultitaskingCameraAccessEnabled;
127130
final Map<String, Object> custom;
128131

129132
String get callId => callCid.id;
@@ -175,6 +178,7 @@ class CallState extends Equatable {
175178
List<String>? blockedUserIds,
176179
int? participantCount,
177180
int? anonymousParticipantCount,
181+
bool? iOSMultitaskingCameraAccessEnabled,
178182
Map<String, Object>? custom,
179183
}) {
180184
return CallState._(
@@ -213,6 +217,8 @@ class CallState extends Equatable {
213217
participantCount: participantCount ?? this.participantCount,
214218
anonymousParticipantCount:
215219
anonymousParticipantCount ?? this.anonymousParticipantCount,
220+
iOSMultitaskingCameraAccessEnabled: iOSMultitaskingCameraAccessEnabled ??
221+
this.iOSMultitaskingCameraAccessEnabled,
216222
custom: custom ?? this.custom,
217223
);
218224
}
@@ -282,6 +288,7 @@ class CallState extends Equatable {
282288
blockedUserIds,
283289
participantCount,
284290
anonymousParticipantCount,
291+
iOSMultitaskingCameraAccessEnabled,
285292
custom,
286293
];
287294

packages/stream_video/lib/src/stream_video.dart

Lines changed: 43 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,9 @@ class StreamVideo extends Disposable {
221221
final StreamVideoOptions _options;
222222
final MutableClientState _state;
223223

224+
bool get muteVideoWhenInBackground => _options.muteVideoWhenInBackground;
225+
bool get muteAudioWhenInBackground => _options.muteAudioWhenInBackground;
226+
224227
final _tokenManager = TokenManager();
225228
final _subscriptions = Subscriptions();
226229
late final CoordinatorClient _client;
@@ -429,30 +432,33 @@ class StreamVideo extends Disposable {
429432
try {
430433
final activeCallCid = _state.activeCall.valueOrNull?.callCid;
431434

432-
if (state.isPaused &&
433-
activeCallCid == null &&
434-
!_options.keepConnectionsAliveWhenInBackground) {
435-
_logger.i(() => '[onAppState] close connection');
436-
_subscriptions.cancel(_idEvents);
437-
await _client.closeConnection();
438-
} else if (state.isPaused && activeCallCid != null) {
439-
final callState = activeCall?.state.value;
440-
final isVideoEnabled =
441-
callState?.localParticipant?.isVideoEnabled ?? false;
442-
final isAudioEnabled =
443-
callState?.localParticipant?.isAudioEnabled ?? false;
444-
445-
if (_options.muteVideoWhenInBackground && isVideoEnabled) {
446-
await activeCall?.setCameraEnabled(enabled: false);
447-
_mutedCameraByStateChange = true;
448-
_logger.v(() => 'Muted camera track since app was paused.');
449-
}
450-
if (_options.muteAudioWhenInBackground && isAudioEnabled) {
451-
await activeCall?.setMicrophoneEnabled(enabled: false);
452-
_mutedAudioByStateChange = true;
453-
_logger.v(() => 'Muted audio track since app was paused.');
435+
if (state.isPaused) {
436+
// Handle app paused state
437+
if (activeCallCid == null &&
438+
!_options.keepConnectionsAliveWhenInBackground) {
439+
_logger.i(() => '[onAppState] close connection');
440+
_subscriptions.cancel(_idEvents);
441+
await _client.closeConnection();
442+
} else if (activeCallCid != null) {
443+
final callState = activeCall?.state.value;
444+
final isVideoEnabled =
445+
callState?.localParticipant?.isVideoEnabled ?? false;
446+
final isAudioEnabled =
447+
callState?.localParticipant?.isAudioEnabled ?? false;
448+
449+
if (_options.muteVideoWhenInBackground && isVideoEnabled) {
450+
await activeCall?.setCameraEnabled(enabled: false);
451+
_mutedCameraByStateChange = true;
452+
_logger.v(() => 'Muted camera track since app was paused.');
453+
}
454+
if (_options.muteAudioWhenInBackground && isAudioEnabled) {
455+
await activeCall?.setMicrophoneEnabled(enabled: false);
456+
_mutedAudioByStateChange = true;
457+
_logger.v(() => 'Muted audio track since app was paused.');
458+
}
454459
}
455460
} else if (state.isResumed) {
461+
// Handle app resumed state
456462
_logger.i(() => '[onAppState] open connection');
457463
await _client.openConnection();
458464
_subscriptions.add(_idEvents, _client.events.listen(_onEvent));
@@ -610,20 +616,23 @@ class StreamVideo extends Disposable {
610616
cid: calls.first.callCid!,
611617
);
612618

613-
callResult.fold(success: (result) async {
614-
final call = result.data;
615-
await call.accept();
619+
callResult.fold(
620+
success: (result) async {
621+
final call = result.data;
622+
await call.accept();
616623

617-
onCallAccepted?.call(call);
624+
onCallAccepted?.call(call);
618625

619-
return true;
620-
}, failure: (error) {
621-
_logger.d(
622-
() =>
623-
'[consumeAndAcceptActiveCall] error consuming incoming call: $error',
624-
);
625-
return false;
626-
});
626+
return true;
627+
},
628+
failure: (error) {
629+
_logger.d(
630+
() =>
631+
'[consumeAndAcceptActiveCall] error consuming incoming call: $error',
632+
);
633+
return false;
634+
},
635+
);
627636

628637
return false;
629638
}

packages/stream_video/lib/src/webrtc/rtc_track/rtc_local_track.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ class RtcLocalTrack<T extends MediaConstraints> extends RtcTrack {
158158
streamLog.i(_tag, () => 'Enabling track $trackId');
159159
try {
160160
mediaTrack.enabled = true;
161+
161162
for (final track in clonedTracks) {
162163
track.enabled = true;
163164
}

packages/stream_video/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ dependencies:
3030
rxdart: ^0.28.0
3131
sdp_transform: ^0.3.2
3232
state_notifier: ^1.0.0
33-
stream_webrtc_flutter: ^0.12.9
33+
stream_webrtc_flutter: ^0.12.9+1
3434
synchronized: ^3.1.0
3535
system_info2: ^4.0.0
3636
tart: ^0.5.1

packages/stream_video_flutter/CHANGELOG.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
## Upcoming
22

33
✅ Added
4-
- Introduced `disposeAfterResolvingRinging()` and `consumeAndAcceptActiveCall()` methods in `StreamVideo` to simplify the ringing flow implementation.
4+
* Introduced `disposeAfterResolvingRinging()` and `consumeAndAcceptActiveCall()` methods in `StreamVideo` to simplify the ringing flow implementation.
55
- Refer to the updated [Incoming Call Documentation](https://getstream.io/video/docs/flutter/incoming-calls/overview/) or the [Ringing Tutorial](https://getstream.io/video/sdk/flutter/tutorial/ringing/) for more details.
66

77
🔄 Changed
8-
- Deprecated the `backgroundVoipCallHandler` parameter in `StreamVideoPushNotificationManager`, as it is no longer required for iOS ringing to function in a terminated state.
8+
* Deprecated the `backgroundVoipCallHandler` parameter in `StreamVideoPushNotificationManager`, as it is no longer required for iOS ringing to function in a terminated state.
99

1010
🐞 Fixed
1111
* Center alignment of buttons in `StreamLobbyVideo` to support more screen sizes.
1212

13+
🚧 (Breaking) Picture-in-Picture (PiP) Improvements & Fixes
14+
* **Fixed:** PiP not working on Android 15.
15+
* **Fixed:** PiP not displaying other participants' screen sharing.
16+
* **Added support for iOS 18 Multitasking Camera Access changes.** From **iOS 18**, you can easily enable camera usage while the app is in the background (e.g., for PiP). Refer to [Picture in Picture documentation](https://getstream.io/video/docs/flutter/advanced/picture_in_picture/) for details.
17+
* Added `disablePictureInPictureWhenScreenSharing` configuration option to `PictureInPictureConfiguration`. When **true** (default), PiP is disabled if the local device is screen sharing.
18+
* ❗ Breaking Change: `ignoreLocalParticipantVideo` parameter in `IOSPictureInPictureConfiguration` is replaced by `includeLocalParticipantVideo`. By default, local video **is enabled** and will appear in PiP mode if the iOS device supports **Multitasking Camera Access**.
19+
* ❗ Breaking Change: `ignoreLocalParticipantVideo` parameter in `StreamPictureInPictureUiKitView` is also replaced by `includeLocalParticipantVideo`.
20+
1321
## 0.7.2
1422

1523
🐞 Fixed

packages/stream_video_flutter/android/src/main/kotlin/io/getstream/video/flutter/stream_video_flutter/service/PictureInPictureHelper.kt

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import android.content.pm.PackageManager
77
import android.os.Build
88
import android.util.Rational
99
import io.getstream.video.flutter.stream_video_flutter.service.utils.getBoolean
10+
import android.app.PictureInPictureUiState
1011

1112
class PictureInPictureHelper {
1213
companion object {
@@ -41,11 +42,7 @@ class PictureInPictureHelper {
4142

4243
val params = PictureInPictureParams.Builder()
4344
params.setAspectRatio(aspect).apply {
44-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
45-
setAutoEnterEnabled(true)
46-
}
4745
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
48-
setTitle("Video Player")
4946
setSeamlessResizeEnabled(true)
5047
}
5148
}

packages/stream_video_flutter/example/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ dependencies:
3030
stream_video: ^0.7.2
3131
stream_video_flutter: ^0.7.2
3232
stream_video_push_notification: ^0.7.2
33-
stream_webrtc_flutter: ^0.12.9
33+
stream_webrtc_flutter: ^0.12.9+1
3434

3535
dependency_overrides:
3636
stream_video:

0 commit comments

Comments
 (0)