Skip to content
Draft
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
1 change: 1 addition & 0 deletions packages/stream_video/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
- A coordinator WebSocket error frame that says nothing about the credentials — a rate limit, or an error about a single request — no longer closes an otherwise healthy connection. Only an expired or rejected token, or a rejected API key, closes the socket now; the rest are logged.
- Fixed the participant sort reordering tiles that are visible on screen. One participant whose tile was not visible was enough to move the dominant speaker to the first tile.
- A viewport visibility is now recorded whether or not the session accepts it. A dropped report left a participant recorded as something they were not for the rest of the call, since a viewport only ever reports what changed.
- On iOS devices without multitasking camera access, the camera track is now muted while the app is in the background, so other participants see camera-off instead of a frozen frame.

## 1.6.0

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import 'package:stream_core/stream_core.dart';

/// Whether the camera track should be muted while the app is in the background.
///
/// iOS suspends camera capture in the background unless the capture session
/// supports multitasking camera access, which leaves the other participants
/// looking at a frozen frame. Muting the track shows them camera-off instead.
///
/// [multitaskingCameraAccessSupported] is `null` when it could not be read, in
/// which case the track is muted rather than risking the frozen frame.
bool shouldMuteCameraInBackground({
required bool isVideoEnabled,
required bool muteVideoWhenInBackground,
required bool? multitaskingCameraAccessSupported,
required PlatformType platform,
}) {
if (!isVideoEnabled) return false;
if (muteVideoWhenInBackground) return true;
if (platform != PlatformType.ios) return false;

return multitaskingCameraAccessSupported != true;
}
29 changes: 28 additions & 1 deletion packages/stream_video/lib/src/stream_video.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import 'core/connection_state.dart';
import 'core/internet_connection_network_state_provider.dart';
import 'errors/stream_video_exception.dart';
import 'errors/stream_video_exception_composer.dart';
import 'internal/_background_mute_policy.dart';
import 'internal/_instance_holder.dart';
import 'latency/latency_service.dart';
import 'latency/latency_settings.dart';
Expand Down Expand Up @@ -593,6 +594,19 @@ class StreamVideo extends Disposable {
);
}

/// Whether the capture session in use supports camera access while
/// multitasking, or `null` when it could not be read.
Future<bool?> _multitaskingCameraAccessSupported() async {
if (!CurrentPlatform.isIos) return null;

try {
return await rtc.Helper.isIOSMultitaskingCameraAccessSupported();
} catch (e) {
_logger.w(() => '[multitaskingCameraAccessSupported] failed: $e');
return null;
}
}

Future<void> _onAppState(LifecycleState state) async {
_logger.d(() => '[onAppState] state: $state');
try {
Expand All @@ -611,14 +625,23 @@ class StreamVideo extends Disposable {
_subscriptions.cancel(_idEvents);
await _client.closeConnection();
} else if (activeCalls.isNotEmpty) {
final multitaskingCameraAccessSupported =
await _multitaskingCameraAccessSupported();

for (final activeCall in activeCalls) {
final callState = activeCall.state.value;
final isVideoEnabled =
callState.localParticipant?.isVideoEnabled ?? false;
final isAudioEnabled =
callState.localParticipant?.isAudioEnabled ?? false;

if (_options.muteVideoWhenInBackground && isVideoEnabled) {
if (shouldMuteCameraInBackground(
isVideoEnabled: isVideoEnabled,
muteVideoWhenInBackground: _options.muteVideoWhenInBackground,
multitaskingCameraAccessSupported:
multitaskingCameraAccessSupported,
platform: CurrentPlatform.type,
)) {
await activeCall.setCameraEnabled(enabled: false);
_mutedCameraByStateChange[activeCall.callCid.value] = true;
_logger.v(() => 'Muted camera track since app was paused.');
Expand Down Expand Up @@ -1611,6 +1634,10 @@ class StreamVideoOptions {

final AudioProcessor? audioProcessor;

/// Mutes the camera track while the app is in the background.
///
/// On iOS devices without multitasking camera access the camera track is
/// muted in the background regardless of this option.
final bool muteVideoWhenInBackground;
final bool muteAudioWhenInBackground;
final bool autoConnect;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_core/stream_core.dart';
import 'package:stream_video/src/internal/_background_mute_policy.dart';

void main() {
group('shouldMuteCameraInBackground', () {
bool shouldMute({
bool isVideoEnabled = true,
bool muteVideoWhenInBackground = false,
bool? multitaskingCameraAccessSupported = false,
PlatformType platform = PlatformType.ios,
}) {
return shouldMuteCameraInBackground(
isVideoEnabled: isVideoEnabled,
muteVideoWhenInBackground: muteVideoWhenInBackground,
multitaskingCameraAccessSupported: multitaskingCameraAccessSupported,
platform: platform,
);
}

test('does not mute a camera that is already off', () {
expect(shouldMute(isVideoEnabled: false), isFalse);
});

test('mutes on iOS without multitasking camera access', () {
expect(shouldMute(), isTrue);
});

test('does not mute on iOS with multitasking camera access', () {
expect(shouldMute(multitaskingCameraAccessSupported: true), isFalse);
});

test('mutes on iOS when support could not be read', () {
expect(shouldMute(multitaskingCameraAccessSupported: null), isTrue);
});

test('does not mute on other platforms', () {
for (final platform in [
PlatformType.android,
PlatformType.web,
PlatformType.macOS,
PlatformType.windows,
PlatformType.linux,
]) {
expect(
shouldMute(platform: platform),
isFalse,
reason: 'should not mute on $platform',
);
}
});

test('mutes on any platform when the option is set', () {
expect(
shouldMute(
muteVideoWhenInBackground: true,
platform: PlatformType.android,
),
isTrue,
);
expect(
shouldMute(
muteVideoWhenInBackground: true,
multitaskingCameraAccessSupported: true,
),
isTrue,
);
});

test('the option does not mute a camera that is off', () {
expect(
shouldMute(isVideoEnabled: false, muteVideoWhenInBackground: true),
isFalse,
);
});
});
}
11 changes: 6 additions & 5 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1989,12 +1989,13 @@ packages:
source: hosted
version: "2.1.1"
stream_webrtc_flutter:
dependency: transitive
dependency: "direct overridden"
description:
name: stream_webrtc_flutter
sha256: "6938c87d6054e7ddcd5114641f7173e84f000f14aedfc57651c1115726dcf356"
url: "https://pub.dev"
source: hosted
path: "."
ref: "3bb87874d59bb9276969daef356f5fefa01639c5"
resolved-ref: "3bb87874d59bb9276969daef356f5fefa01639c5"
url: "https://github.com/GetStream/webrtc-flutter.git"
source: git
version: "3.2.0"
string_scanner:
dependency: transitive
Expand Down
6 changes: 6 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ dev_dependencies:
dependency_overrides:
cli_util: ^0.5.1
file_picker: ^12.0.0-beta.5
# TODO: drop once a release with the multitasking camera access fix is out.
# https://github.com/GetStream/webrtc-flutter/pull/88
stream_webrtc_flutter:
git:
url: https://github.com/GetStream/webrtc-flutter.git
ref: 3bb87874d59bb9276969daef356f5fefa01639c5
stream_core_flutter:
git:
url: https://github.com/GetStream/stream-core-flutter.git
Expand Down