diff --git a/lib/livekit_client.dart b/lib/livekit_client.dart index 27276733b..fda515d9f 100644 --- a/lib/livekit_client.dart +++ b/lib/livekit_client.dart @@ -46,6 +46,7 @@ export 'src/publication/local.dart'; export 'src/publication/remote.dart'; export 'src/publication/track_publication.dart'; export 'src/support/platform.dart'; +export 'src/support/value_or_absent.dart'; export 'src/track/audio_visualizer.dart'; export 'src/track/local/audio.dart'; export 'src/track/local/local.dart'; diff --git a/lib/src/core/engine.dart b/lib/src/core/engine.dart index 0a1ca490f..19ac84cf4 100644 --- a/lib/src/core/engine.dart +++ b/lib/src/core/engine.dart @@ -39,6 +39,7 @@ import '../publication/local.dart'; import '../support/disposable.dart'; import '../support/platform.dart' show lkPlatformIsTest, lkPlatformIs, PlatformType; import '../support/region_url_provider.dart'; +import '../support/value_or_absent.dart'; import '../support/websocket.dart'; import '../track/local/local.dart'; import '../track/local/video.dart'; @@ -562,18 +563,18 @@ class Engine extends Disposable with EventsEmittable { // The server provided iceServers are only used if // the client's iceServers are not set. if (rtcConfiguration.iceServers == null && serverProvidedIceServers.isNotEmpty) { - rtcConfiguration = connectOptions.rtcConfiguration.copyWith(iceServers: serverProvidedIceServers); + rtcConfiguration = connectOptions.rtcConfiguration.copyWith(iceServers: Value(serverProvidedIceServers)); } // set forceRelay if server response is enabled if (serverResponseForceRelay == lk_models.ClientConfigSetting.ENABLED) { rtcConfiguration = rtcConfiguration.copyWith( - iceTransportPolicy: RTCIceTransportPolicy.relay, + iceTransportPolicy: Value(RTCIceTransportPolicy.relay), ); } if (kIsWeb && (roomOptions.e2eeOptions != null || roomOptions.encryption != null)) { - rtcConfiguration = rtcConfiguration.copyWith(encodedInsertableStreams: true); + rtcConfiguration = rtcConfiguration.copyWith(encodedInsertableStreams: Value(true)); } return rtcConfiguration; diff --git a/lib/src/core/room.dart b/lib/src/core/room.dart index 2508063f8..8e864d96f 100644 --- a/lib/src/core/room.dart +++ b/lib/src/core/room.dart @@ -42,6 +42,7 @@ import '../proto/livekit_rtc.pb.dart' as lk_rtc; import '../support/disposable.dart'; import '../support/platform.dart'; import '../support/region_url_provider.dart'; +import '../support/value_or_absent.dart'; import '../support/websocket.dart' show WebSocketException; import '../track/audio_management.dart'; import '../track/local/audio.dart'; @@ -262,9 +263,9 @@ class Room extends DisposableChangeNotifier with EventsEmittable { if (_e2eeManager != null) { // Disable backup codec when e2ee is enabled roomOptions = roomOptions.copyWith( - defaultVideoPublishOptions: roomOptions.defaultVideoPublishOptions.copyWith( - backupVideoCodec: const BackupVideoCodec(enabled: false), - ), + defaultVideoPublishOptions: Value(roomOptions.defaultVideoPublishOptions.copyWith( + backupVideoCodec: Value(const BackupVideoCodec(enabled: false)), + )), ); } @@ -449,7 +450,7 @@ class Room extends DisposableChangeNotifier with EventsEmittable { logger.info('Publishing preconnect audio track'); await _localParticipant!.publishAudioTrack( preConnectAudioBuffer.localTrack!, - publishOptions: roomOptions.defaultAudioPublishOptions.copyWith(preConnect: true), + publishOptions: roomOptions.defaultAudioPublishOptions.copyWith(preConnect: Value(true)), ); } @@ -1095,9 +1096,9 @@ extension RoomHardwareManagementMethods on Room { await Hardware.instance.selectAudioOutput(device); } engine.roomOptions = engine.roomOptions.copyWith( - defaultAudioOutputOptions: roomOptions.defaultAudioOutputOptions.copyWith( - deviceId: device.deviceId, - ), + defaultAudioOutputOptions: Value(roomOptions.defaultAudioOutputOptions.copyWith( + deviceId: Value(device.deviceId), + )), ); } @@ -1112,9 +1113,9 @@ extension RoomHardwareManagementMethods on Room { await Hardware.instance.selectAudioInput(device); } engine.roomOptions = engine.roomOptions.copyWith( - defaultAudioCaptureOptions: roomOptions.defaultAudioCaptureOptions.copyWith( - deviceId: device.deviceId, - ), + defaultAudioCaptureOptions: Value(roomOptions.defaultAudioCaptureOptions.copyWith( + deviceId: Value(device.deviceId), + )), ); } @@ -1126,7 +1127,8 @@ extension RoomHardwareManagementMethods on Room { // Always update roomOptions so future tracks use the correct device engine.roomOptions = engine.roomOptions.copyWith( - defaultCameraCaptureOptions: roomOptions.defaultCameraCaptureOptions.copyWith(deviceId: device.deviceId), + defaultCameraCaptureOptions: + Value(roomOptions.defaultCameraCaptureOptions.copyWith(deviceId: Value(device.deviceId))), ); try { @@ -1137,7 +1139,8 @@ extension RoomHardwareManagementMethods on Room { } catch (e) { // if the switching actually fails, reset it to the previous deviceId engine.roomOptions = engine.roomOptions.copyWith( - defaultCameraCaptureOptions: roomOptions.defaultCameraCaptureOptions.copyWith(deviceId: currentDeviceId), + defaultCameraCaptureOptions: + Value(roomOptions.defaultCameraCaptureOptions.copyWith(deviceId: Value(currentDeviceId))), ); } } @@ -1150,9 +1153,9 @@ extension RoomHardwareManagementMethods on Room { if (lkPlatformIsMobile()) { await Hardware.instance.setSpeakerphoneOn(speakerOn, forceSpeakerOutput: forceSpeakerOutput); engine.roomOptions = engine.roomOptions.copyWith( - defaultAudioOutputOptions: roomOptions.defaultAudioOutputOptions.copyWith( - speakerOn: speakerOn, - ), + defaultAudioOutputOptions: Value(roomOptions.defaultAudioOutputOptions.copyWith( + speakerOn: Value(speakerOn), + )), ); } } diff --git a/lib/src/hardware/hardware.dart b/lib/src/hardware/hardware.dart index 505a4d3d8..133d6b514 100644 --- a/lib/src/hardware/hardware.dart +++ b/lib/src/hardware/hardware.dart @@ -21,6 +21,7 @@ import '../logger.dart'; import '../support/native.dart'; import '../support/native_audio.dart'; import '../support/platform.dart'; +import '../support/value_or_absent.dart'; import '../track/audio_management.dart'; class MediaDevice { @@ -151,9 +152,9 @@ class Hardware { config = await onConfigureNativeAudio.call(audioTrackState); if (_preferSpeakerOutput && _forceSpeakerOutput) { config = config.copyWith( - appleAudioCategoryOptions: { + appleAudioCategoryOptions: Value({ AppleAudioCategoryOption.defaultToSpeaker, - }, + }), ); } logger.fine('configuring for ${audioTrackState} using ${config}...'); diff --git a/lib/src/options.dart b/lib/src/options.dart index af31ce5fd..803b8da69 100644 --- a/lib/src/options.dart +++ b/lib/src/options.dart @@ -14,6 +14,7 @@ import 'constants.dart'; import 'e2ee/options.dart'; +import 'support/value_or_absent.dart'; import 'track/local/audio.dart'; import 'track/local/video.dart'; import 'track/options.dart'; @@ -143,33 +144,33 @@ class RoomOptions { }); RoomOptions copyWith({ - CameraCaptureOptions? defaultCameraCaptureOptions, - ScreenShareCaptureOptions? defaultScreenShareCaptureOptions, - AudioCaptureOptions? defaultAudioCaptureOptions, - VideoPublishOptions? defaultVideoPublishOptions, - AudioPublishOptions? defaultAudioPublishOptions, - AudioOutputOptions? defaultAudioOutputOptions, - bool? adaptiveStream, - bool? dynacast, - bool? stopLocalTrackOnUnpublish, - E2EEOptions? e2eeOptions, - E2EEOptions? encryption, - bool? fastPublish, + ValueOrAbsent defaultCameraCaptureOptions = const Absent(), + ValueOrAbsent defaultScreenShareCaptureOptions = const Absent(), + ValueOrAbsent defaultAudioCaptureOptions = const Absent(), + ValueOrAbsent defaultVideoPublishOptions = const Absent(), + ValueOrAbsent defaultAudioPublishOptions = const Absent(), + ValueOrAbsent defaultAudioOutputOptions = const Absent(), + ValueOrAbsent adaptiveStream = const Absent(), + ValueOrAbsent dynacast = const Absent(), + ValueOrAbsent stopLocalTrackOnUnpublish = const Absent(), + ValueOrAbsent e2eeOptions = const Absent(), + ValueOrAbsent encryption = const Absent(), + ValueOrAbsent fastPublish = const Absent(), }) { return RoomOptions( - defaultCameraCaptureOptions: defaultCameraCaptureOptions ?? this.defaultCameraCaptureOptions, - defaultScreenShareCaptureOptions: defaultScreenShareCaptureOptions ?? this.defaultScreenShareCaptureOptions, - defaultAudioCaptureOptions: defaultAudioCaptureOptions ?? this.defaultAudioCaptureOptions, - defaultVideoPublishOptions: defaultVideoPublishOptions ?? this.defaultVideoPublishOptions, - defaultAudioPublishOptions: defaultAudioPublishOptions ?? this.defaultAudioPublishOptions, - defaultAudioOutputOptions: defaultAudioOutputOptions ?? this.defaultAudioOutputOptions, - adaptiveStream: adaptiveStream ?? this.adaptiveStream, - dynacast: dynacast ?? this.dynacast, - stopLocalTrackOnUnpublish: stopLocalTrackOnUnpublish ?? this.stopLocalTrackOnUnpublish, + defaultCameraCaptureOptions: defaultCameraCaptureOptions.valueOr(this.defaultCameraCaptureOptions), + defaultScreenShareCaptureOptions: defaultScreenShareCaptureOptions.valueOr(this.defaultScreenShareCaptureOptions), + defaultAudioCaptureOptions: defaultAudioCaptureOptions.valueOr(this.defaultAudioCaptureOptions), + defaultVideoPublishOptions: defaultVideoPublishOptions.valueOr(this.defaultVideoPublishOptions), + defaultAudioPublishOptions: defaultAudioPublishOptions.valueOr(this.defaultAudioPublishOptions), + defaultAudioOutputOptions: defaultAudioOutputOptions.valueOr(this.defaultAudioOutputOptions), + adaptiveStream: adaptiveStream.valueOr(this.adaptiveStream), + dynacast: dynacast.valueOr(this.dynacast), + stopLocalTrackOnUnpublish: stopLocalTrackOnUnpublish.valueOr(this.stopLocalTrackOnUnpublish), // ignore: deprecated_member_use_from_same_package - e2eeOptions: e2eeOptions ?? this.e2eeOptions, - encryption: encryption ?? this.encryption, - fastPublish: fastPublish ?? this.fastPublish, + e2eeOptions: e2eeOptions.valueOr(this.e2eeOptions), + encryption: encryption.valueOr(this.encryption), + fastPublish: fastPublish.valueOr(this.fastPublish), ); } } @@ -194,16 +195,16 @@ class BackupVideoCodec { final VideoEncoding? encoding; final bool simulcast; BackupVideoCodec copyWith({ - bool? enabled, - String? codec, - VideoEncoding? encoding, - bool? simulcast, + ValueOrAbsent enabled = const Absent(), + ValueOrAbsent codec = const Absent(), + ValueOrAbsent encoding = const Absent(), + ValueOrAbsent simulcast = const Absent(), }) { return BackupVideoCodec( - enabled: enabled ?? this.enabled, - codec: codec ?? this.codec, - encoding: encoding ?? this.encoding, - simulcast: simulcast ?? this.simulcast, + enabled: enabled.valueOr(this.enabled), + codec: codec.valueOr(this.codec), + encoding: encoding.valueOr(this.encoding), + simulcast: simulcast.valueOr(this.simulcast), ); } } @@ -272,30 +273,30 @@ class VideoPublishOptions extends PublishOptions { this.degradationPreference}); VideoPublishOptions copyWith({ - VideoEncoding? videoEncoding, - VideoEncoding? screenShareEncoding, - bool? simulcast, - List? videoSimulcastLayers, - List? screenShareSimulcastLayers, - String? videoCodec, - BackupVideoCodec? backupVideoCodec, - DegradationPreference? degradationPreference, - String? scalabilityMode, - String? name, - String? stream, + ValueOrAbsent videoEncoding = const Absent(), + ValueOrAbsent screenShareEncoding = const Absent(), + ValueOrAbsent simulcast = const Absent(), + ValueOrAbsent> videoSimulcastLayers = const Absent(), + ValueOrAbsent> screenShareSimulcastLayers = const Absent(), + ValueOrAbsent videoCodec = const Absent(), + ValueOrAbsent backupVideoCodec = const Absent(), + ValueOrAbsent degradationPreference = const Absent(), + ValueOrAbsent scalabilityMode = const Absent(), + ValueOrAbsent name = const Absent(), + ValueOrAbsent stream = const Absent(), }) => VideoPublishOptions( - videoEncoding: videoEncoding ?? this.videoEncoding, - screenShareEncoding: screenShareEncoding ?? this.screenShareEncoding, - simulcast: simulcast ?? this.simulcast, - videoSimulcastLayers: videoSimulcastLayers ?? this.videoSimulcastLayers, - screenShareSimulcastLayers: screenShareSimulcastLayers ?? this.screenShareSimulcastLayers, - videoCodec: videoCodec ?? this.videoCodec, - backupVideoCodec: backupVideoCodec ?? this.backupVideoCodec, - degradationPreference: degradationPreference ?? this.degradationPreference, - scalabilityMode: scalabilityMode ?? this.scalabilityMode, - name: name ?? this.name, - stream: stream ?? this.stream, + videoEncoding: videoEncoding.valueOr(this.videoEncoding), + screenShareEncoding: screenShareEncoding.valueOr(this.screenShareEncoding), + simulcast: simulcast.valueOr(this.simulcast), + videoSimulcastLayers: videoSimulcastLayers.valueOr(this.videoSimulcastLayers), + screenShareSimulcastLayers: screenShareSimulcastLayers.valueOr(this.screenShareSimulcastLayers), + videoCodec: videoCodec.valueOr(this.videoCodec), + backupVideoCodec: backupVideoCodec.valueOr(this.backupVideoCodec), + degradationPreference: degradationPreference.valueOr(this.degradationPreference), + scalabilityMode: scalabilityMode.valueOr(this.scalabilityMode), + name: name.valueOr(this.name), + stream: stream.valueOr(this.stream), ); @override @@ -332,20 +333,20 @@ class AudioPublishOptions extends PublishOptions { }); AudioPublishOptions copyWith({ - AudioEncoding? encoding, - bool? dtx, - String? name, - String? stream, - bool? red, - bool? preConnect, + ValueOrAbsent encoding = const Absent(), + ValueOrAbsent dtx = const Absent(), + ValueOrAbsent name = const Absent(), + ValueOrAbsent stream = const Absent(), + ValueOrAbsent red = const Absent(), + ValueOrAbsent preConnect = const Absent(), }) => AudioPublishOptions( - encoding: encoding ?? this.encoding, - dtx: dtx ?? this.dtx, - name: name ?? this.name, - stream: stream ?? this.stream, - red: red ?? this.red, - preConnect: preConnect ?? this.preConnect, + encoding: encoding.valueOr(this.encoding), + dtx: dtx.valueOr(this.dtx), + name: name.valueOr(this.name), + stream: stream.valueOr(this.stream), + red: red.valueOr(this.red), + preConnect: preConnect.valueOr(this.preConnect), ); @override diff --git a/lib/src/participant/local.dart b/lib/src/participant/local.dart index dc0f39948..8b21de47f 100644 --- a/lib/src/participant/local.dart +++ b/lib/src/participant/local.dart @@ -44,6 +44,7 @@ import '../proto/livekit_models.pb.dart' as lk_models; import '../proto/livekit_rtc.pb.dart' as lk_rtc; import '../publication/local.dart'; import '../support/platform.dart'; +import '../support/value_or_absent.dart'; import '../track/local/audio.dart'; import '../track/local/local.dart'; import '../track/local/video.dart'; @@ -220,7 +221,7 @@ class LocalParticipant extends Participant { if (publishOptions.videoCodec.toLowerCase() != publishOptions.videoCodec) { publishOptions = publishOptions.copyWith( - videoCodec: publishOptions.videoCodec.toLowerCase(), + videoCodec: Value(publishOptions.videoCodec.toLowerCase()), ); } @@ -231,7 +232,7 @@ class LocalParticipant extends Participant { .where((c) => videoCodecs.any((v) => c.mime.toLowerCase().endsWith(v))) .any((c) => publishOptions?.videoCodec == mimeTypeToVideoCodecString(c.mime))) { publishOptions = publishOptions.copyWith( - videoCodec: mimeTypeToVideoCodecString(room.engine.enabledPublishCodecs![0].mime).toLowerCase(), + videoCodec: Value(mimeTypeToVideoCodecString(room.engine.enabledPublishCodecs![0].mime).toLowerCase()), ); } } @@ -240,19 +241,19 @@ class LocalParticipant extends Participant { final isSVC = isSVCCodec(publishOptions.videoCodec); if (isSVC) { if (!room.roomOptions.dynacast) { - room.engine.roomOptions = room.roomOptions.copyWith(dynacast: true); + room.engine.roomOptions = room.roomOptions.copyWith(dynacast: Value(true)); } if (publishOptions.scalabilityMode == null) { publishOptions = publishOptions.copyWith( - scalabilityMode: 'L3T3_KEY', + scalabilityMode: Value('L3T3_KEY'), ); } // vp9 svc with screenshare has problem to encode, always use L1T3 here if (track.source == TrackSource.screenShareVideo) { publishOptions = publishOptions.copyWith( - scalabilityMode: 'L1T3', + scalabilityMode: Value('L1T3'), ); } } @@ -267,8 +268,8 @@ class LocalParticipant extends Participant { final settings = track.mediaStreamTrack.getSettings(); if ((settings['width'] is int && settings['width'] as int > 0) && (settings['height'] is int && settings['height'] as int > 0)) { - dimensions = dimensions.copyWith(width: settings['width'] as int); - dimensions = dimensions.copyWith(height: settings['height'] as int); + dimensions = dimensions.copyWith(width: Value(settings['width'] as int)); + dimensions = dimensions.copyWith(height: Value(settings['height'] as int)); } } catch (_) { logger.warning('Failed to call `mediaStreamTrack.getSettings()`'); @@ -392,7 +393,7 @@ class LocalParticipant extends Participant { 'requested a different codec than specified by serverRequested: ${publishOptions.videoCodec}, server: ${updatedCodec}', ); publishOptions = publishOptions.copyWith( - videoCodec: updatedCodec, + videoCodec: Value(updatedCodec), ); // recompute encodings since bitrates/etc could have changed encodings = Utils.computeVideoEncodings( @@ -734,7 +735,7 @@ class LocalParticipant extends Participant { /// track separately, it has to be returned once in getDisplayMedia, /// so we publish it twice here, but only return videoTrack to user. if (captureScreenAudio ?? false) { - captureOptions = captureOptions.copyWith(captureScreenAudio: true); + captureOptions = captureOptions.copyWith(captureScreenAudio: Value(true)); final tracks = await LocalVideoTrack.createScreenShareTracksWithAudio(captureOptions); LocalTrackPublication? publication; for (final track in tracks) { @@ -828,7 +829,7 @@ class LocalParticipant extends Participant { } var options = room.roomOptions.defaultVideoPublishOptions; - options = options.copyWith(simulcast: backupCodecOpts.simulcast); + options = options.copyWith(simulcast: Value(backupCodecOpts.simulcast)); if (backupCodec.toLowerCase() == publication.track?.codec?.toLowerCase()) { // not needed, same codec already published diff --git a/lib/src/support/native_audio.dart b/lib/src/support/native_audio.dart index 70bc59dce..036707bf9 100644 --- a/lib/src/support/native_audio.dart +++ b/lib/src/support/native_audio.dart @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import 'value_or_absent.dart'; + // https://developer.apple.com/documentation/avfaudio/avaudiosession/category enum AppleAudioCategory { soloAmbient, @@ -139,15 +141,15 @@ class NativeAudioConfiguration { }; NativeAudioConfiguration copyWith({ - AppleAudioCategory? appleAudioCategory, - Set? appleAudioCategoryOptions, - AppleAudioMode? appleAudioMode, - bool? preferSpeakerOutput, + ValueOrAbsent appleAudioCategory = const Absent(), + ValueOrAbsent?> appleAudioCategoryOptions = const Absent(), + ValueOrAbsent appleAudioMode = const Absent(), + ValueOrAbsent preferSpeakerOutput = const Absent(), }) => NativeAudioConfiguration( - appleAudioCategory: appleAudioCategory ?? this.appleAudioCategory, - appleAudioCategoryOptions: appleAudioCategoryOptions ?? this.appleAudioCategoryOptions, - appleAudioMode: appleAudioMode ?? this.appleAudioMode, - preferSpeakerOutput: preferSpeakerOutput ?? this.preferSpeakerOutput, + appleAudioCategory: appleAudioCategory.valueOr(this.appleAudioCategory), + appleAudioCategoryOptions: appleAudioCategoryOptions.valueOr(this.appleAudioCategoryOptions), + appleAudioMode: appleAudioMode.valueOr(this.appleAudioMode), + preferSpeakerOutput: preferSpeakerOutput.valueOr(this.preferSpeakerOutput), ); } diff --git a/lib/src/support/value_or_absent.dart b/lib/src/support/value_or_absent.dart new file mode 100644 index 000000000..df9e0b14f --- /dev/null +++ b/lib/src/support/value_or_absent.dart @@ -0,0 +1,54 @@ +// Copyright 2024 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Allows distinguishing between setting null and no-op in copyWith operations. +/// +/// Example usage: +/// ```dart +/// class MyClass { +/// final String? name; +/// MyClass({this.name}); +/// +/// MyClass copyWith({ValueOrAbsent name = const Absent()}) => +/// MyClass(name: name.valueOr(this.name)); +/// } +/// +/// // Usage: +/// obj.copyWith(name: Value(null)); // explicitly set to null +/// obj.copyWith(name: Value('test')); // set to a value +/// obj.copyWith(); // keep existing (absent) +/// ``` +sealed class ValueOrAbsent { + const ValueOrAbsent(); + + /// Returns the contained value if present, otherwise returns [other]. + T valueOr(T other); +} + +/// Represents an explicitly provided value (including null). +class Value extends ValueOrAbsent { + final T value; + const Value(this.value); + + @override + T valueOr(T other) => value; +} + +/// Represents the absence of a value (use existing). +class Absent extends ValueOrAbsent { + const Absent(); + + @override + T valueOr(T other) => other; +} diff --git a/lib/src/track/audio_management.dart b/lib/src/track/audio_management.dart index 95e0fa499..4e691780b 100644 --- a/lib/src/track/audio_management.dart +++ b/lib/src/track/audio_management.dart @@ -20,6 +20,7 @@ import '../logger.dart'; import '../support/native.dart'; import '../support/native_audio.dart'; import '../support/platform.dart'; +import '../support/value_or_absent.dart'; import 'local/local.dart'; import 'remote/remote.dart'; @@ -115,9 +116,9 @@ Future _onAudioTrackCountDidChange() async { if (Hardware.instance.forceSpeakerOutput) { config = config.copyWith( - appleAudioCategoryOptions: { + appleAudioCategoryOptions: Value({ AppleAudioCategoryOption.defaultToSpeaker, - }, + }), ); } } diff --git a/lib/src/track/local/audio.dart b/lib/src/track/local/audio.dart index e3675a340..66369dc92 100644 --- a/lib/src/track/local/audio.dart +++ b/lib/src/track/local/audio.dart @@ -23,6 +23,7 @@ import '../../logger.dart'; import '../../options.dart'; import '../../stats/audio_source_stats.dart'; import '../../stats/stats.dart'; +import '../../support/value_or_absent.dart'; import '../../types/other.dart'; import '../audio_management.dart'; import '../options.dart'; @@ -39,7 +40,7 @@ class LocalAudioTrack extends LocalTrack with AudioTrack, LocalAudioManagementMi if (currentOptions.deviceId == deviceId) { return; } - currentOptions = currentOptions.copyWith(deviceId: deviceId); + currentOptions = currentOptions.copyWith(deviceId: Value(deviceId)); if (!muted) { await restartTrack(); } diff --git a/lib/src/track/local/video.dart b/lib/src/track/local/video.dart index 4c35205e5..f5ba04cda 100644 --- a/lib/src/track/local/video.dart +++ b/lib/src/track/local/video.dart @@ -26,6 +26,7 @@ import '../../proto/livekit_models.pb.dart' as lk_models; import '../../proto/livekit_rtc.pb.dart' as lk_rtc; import '../../stats/stats.dart'; import '../../support/platform.dart'; +import '../../support/value_or_absent.dart'; import '../../types/other.dart'; import '../options.dart'; import 'audio.dart'; @@ -242,7 +243,7 @@ class LocalVideoTrack extends LocalTrack with VideoTrack { if (options == null) { options = const ScreenShareCaptureOptions(captureScreenAudio: true); } else { - options = options.copyWith(captureScreenAudio: true); + options = options.copyWith(captureScreenAudio: Value(true)); } final stream = await LocalTrack.createStream(options); @@ -299,12 +300,12 @@ extension LocalVideoTrackExt on LocalVideoTrack { } if (fastSwitch) { - currentOptions = options.copyWith(deviceId: deviceId); + currentOptions = options.copyWith(deviceId: Value(deviceId)); await rtc.Helper.switchCamera(mediaStreamTrack, deviceId, mediaStream); return; } - await restartTrack(options.copyWith(deviceId: deviceId)); + await restartTrack(options.copyWith(deviceId: Value(deviceId))); await replaceTrackForMultiCodecSimulcast(mediaStreamTrack); } diff --git a/lib/src/track/options.dart b/lib/src/track/options.dart index e60d49270..edd27c48b 100644 --- a/lib/src/track/options.dart +++ b/lib/src/track/options.dart @@ -16,6 +16,7 @@ import 'package:flutter/foundation.dart' show kIsWeb; import '../support/native.dart'; import '../support/platform.dart'; +import '../support/value_or_absent.dart'; import '../track/local/audio.dart'; import '../track/local/video.dart'; import '../types/video_parameters.dart'; @@ -108,18 +109,18 @@ class CameraCaptureOptions extends VideoCaptureOptions { // Returns new options with updated properties CameraCaptureOptions copyWith({ - VideoParameters? params, - CameraPosition? cameraPosition, - String? deviceId, - double? maxFrameRate, - bool? stopCameraCaptureOnMute, + ValueOrAbsent params = const Absent(), + ValueOrAbsent cameraPosition = const Absent(), + ValueOrAbsent deviceId = const Absent(), + ValueOrAbsent maxFrameRate = const Absent(), + ValueOrAbsent stopCameraCaptureOnMute = const Absent(), }) => CameraCaptureOptions( - params: params ?? this.params, - cameraPosition: cameraPosition ?? this.cameraPosition, - deviceId: deviceId ?? this.deviceId, - maxFrameRate: maxFrameRate ?? this.maxFrameRate, - stopCameraCaptureOnMute: stopCameraCaptureOnMute ?? this.stopCameraCaptureOnMute, + params: params.valueOr(this.params), + cameraPosition: cameraPosition.valueOr(this.cameraPosition), + deviceId: deviceId.valueOr(this.deviceId), + maxFrameRate: maxFrameRate.valueOr(this.maxFrameRate), + stopCameraCaptureOnMute: stopCameraCaptureOnMute.valueOr(this.stopCameraCaptureOnMute), ); } @@ -158,22 +159,22 @@ class ScreenShareCaptureOptions extends VideoCaptureOptions { : super(params: captureOptions.params); ScreenShareCaptureOptions copyWith({ - bool? useiOSBroadcastExtension, - bool? captureScreenAudio, - VideoParameters? params, - String? sourceId, - double? maxFrameRate, - bool? preferCurrentTab, - String? selfBrowserSurface, + ValueOrAbsent useiOSBroadcastExtension = const Absent(), + ValueOrAbsent captureScreenAudio = const Absent(), + ValueOrAbsent params = const Absent(), + ValueOrAbsent sourceId = const Absent(), + ValueOrAbsent maxFrameRate = const Absent(), + ValueOrAbsent preferCurrentTab = const Absent(), + ValueOrAbsent selfBrowserSurface = const Absent(), }) => ScreenShareCaptureOptions( - useiOSBroadcastExtension: useiOSBroadcastExtension ?? this.useiOSBroadcastExtension, - captureScreenAudio: captureScreenAudio ?? this.captureScreenAudio, - params: params ?? this.params, - sourceId: sourceId ?? deviceId, - maxFrameRate: maxFrameRate ?? this.maxFrameRate, - preferCurrentTab: preferCurrentTab ?? this.preferCurrentTab, - selfBrowserSurface: selfBrowserSurface ?? this.selfBrowserSurface, + useiOSBroadcastExtension: useiOSBroadcastExtension.valueOr(this.useiOSBroadcastExtension), + captureScreenAudio: captureScreenAudio.valueOr(this.captureScreenAudio), + params: params.valueOr(this.params), + sourceId: sourceId.valueOr(deviceId), + maxFrameRate: maxFrameRate.valueOr(this.maxFrameRate), + preferCurrentTab: preferCurrentTab.valueOr(this.preferCurrentTab), + selfBrowserSurface: selfBrowserSurface.valueOr(this.selfBrowserSurface), ); @override @@ -346,20 +347,20 @@ class AudioCaptureOptions extends LocalTrackOptions { } AudioCaptureOptions copyWith({ - String? deviceId, - bool? noiseSuppression, - bool? echoCancellation, - bool? autoGainControl, - bool? highPassFilter, - bool? typingNoiseDetection, + ValueOrAbsent deviceId = const Absent(), + ValueOrAbsent noiseSuppression = const Absent(), + ValueOrAbsent echoCancellation = const Absent(), + ValueOrAbsent autoGainControl = const Absent(), + ValueOrAbsent highPassFilter = const Absent(), + ValueOrAbsent typingNoiseDetection = const Absent(), }) { return AudioCaptureOptions( - deviceId: deviceId ?? this.deviceId, - noiseSuppression: noiseSuppression ?? this.noiseSuppression, - echoCancellation: echoCancellation ?? this.echoCancellation, - autoGainControl: autoGainControl ?? this.autoGainControl, - highPassFilter: highPassFilter ?? this.highPassFilter, - typingNoiseDetection: typingNoiseDetection ?? this.typingNoiseDetection, + deviceId: deviceId.valueOr(this.deviceId), + noiseSuppression: noiseSuppression.valueOr(this.noiseSuppression), + echoCancellation: echoCancellation.valueOr(this.echoCancellation), + autoGainControl: autoGainControl.valueOr(this.autoGainControl), + highPassFilter: highPassFilter.valueOr(this.highPassFilter), + typingNoiseDetection: typingNoiseDetection.valueOr(this.typingNoiseDetection), ); } } @@ -374,10 +375,13 @@ class AudioOutputOptions { const AudioOutputOptions({this.deviceId, this.speakerOn}); - AudioOutputOptions copyWith({String? deviceId, bool? speakerOn}) { + AudioOutputOptions copyWith({ + ValueOrAbsent deviceId = const Absent(), + ValueOrAbsent speakerOn = const Absent(), + }) { return AudioOutputOptions( - deviceId: deviceId ?? this.deviceId, - speakerOn: speakerOn ?? this.speakerOn, + deviceId: deviceId.valueOr(this.deviceId), + speakerOn: speakerOn.valueOr(this.speakerOn), ); } } diff --git a/lib/src/types/audio_encoding.dart b/lib/src/types/audio_encoding.dart index ee277299a..122019be1 100644 --- a/lib/src/types/audio_encoding.dart +++ b/lib/src/types/audio_encoding.dart @@ -15,6 +15,7 @@ import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; import 'package:meta/meta.dart'; +import '../support/value_or_absent.dart'; import 'priority.dart'; /// A type that represents audio encoding information. @@ -36,14 +37,14 @@ class AudioEncoding { }); AudioEncoding copyWith({ - int? maxBitrate, - Priority? bitratePriority, - Priority? networkPriority, + ValueOrAbsent maxBitrate = const Absent(), + ValueOrAbsent bitratePriority = const Absent(), + ValueOrAbsent networkPriority = const Absent(), }) => AudioEncoding( - maxBitrate: maxBitrate ?? this.maxBitrate, - bitratePriority: bitratePriority ?? this.bitratePriority, - networkPriority: networkPriority ?? this.networkPriority, + maxBitrate: maxBitrate.valueOr(this.maxBitrate), + bitratePriority: bitratePriority.valueOr(this.bitratePriority), + networkPriority: networkPriority.valueOr(this.networkPriority), ); @override diff --git a/lib/src/types/other.dart b/lib/src/types/other.dart index 46f39b07b..887e0f2cc 100644 --- a/lib/src/types/other.dart +++ b/lib/src/types/other.dart @@ -18,6 +18,7 @@ import 'package:flutter/material.dart'; import '../extensions.dart'; import '../participant/participant.dart'; +import '../support/value_or_absent.dart'; typedef CancelListenFunc = Future Function(); @@ -177,18 +178,18 @@ class RTCConfiguration { // Returns new options with updated properties RTCConfiguration copyWith({ - int? iceCandidatePoolSize, - List? iceServers, - RTCIceTransportPolicy? iceTransportPolicy, - bool? encodedInsertableStreams, - bool? isDscpEnabled, + ValueOrAbsent iceCandidatePoolSize = const Absent(), + ValueOrAbsent?> iceServers = const Absent(), + ValueOrAbsent iceTransportPolicy = const Absent(), + ValueOrAbsent encodedInsertableStreams = const Absent(), + ValueOrAbsent isDscpEnabled = const Absent(), }) => RTCConfiguration( - iceCandidatePoolSize: iceCandidatePoolSize ?? this.iceCandidatePoolSize, - iceServers: iceServers ?? this.iceServers, - iceTransportPolicy: iceTransportPolicy ?? this.iceTransportPolicy, - encodedInsertableStreams: encodedInsertableStreams ?? this.encodedInsertableStreams, - isDscpEnabled: isDscpEnabled ?? this.isDscpEnabled, + iceCandidatePoolSize: iceCandidatePoolSize.valueOr(this.iceCandidatePoolSize), + iceServers: iceServers.valueOr(this.iceServers), + iceTransportPolicy: iceTransportPolicy.valueOr(this.iceTransportPolicy), + encodedInsertableStreams: encodedInsertableStreams.valueOr(this.encodedInsertableStreams), + isDscpEnabled: isDscpEnabled.valueOr(this.isDscpEnabled), ); } diff --git a/lib/src/types/video_dimensions.dart b/lib/src/types/video_dimensions.dart index c975e4b0d..adbec71a0 100644 --- a/lib/src/types/video_dimensions.dart +++ b/lib/src/types/video_dimensions.dart @@ -16,6 +16,8 @@ import 'dart:math' as math; import 'package:meta/meta.dart'; +import '../support/value_or_absent.dart'; + /// A simple class that represents dimensions of video. @immutable class VideoDimensions { @@ -31,12 +33,12 @@ class VideoDimensions { String toString() => '${runtimeType}(${width}x${height})'; VideoDimensions copyWith({ - int? width, - int? height, + ValueOrAbsent width = const Absent(), + ValueOrAbsent height = const Absent(), }) => VideoDimensions( - width ?? this.width, - height ?? this.height, + width.valueOr(this.width), + height.valueOr(this.height), ); // ---------------------------------------------------------------------- diff --git a/lib/src/types/video_encoding.dart b/lib/src/types/video_encoding.dart index 4f24694bb..a1af12229 100644 --- a/lib/src/types/video_encoding.dart +++ b/lib/src/types/video_encoding.dart @@ -15,6 +15,7 @@ import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; import 'package:meta/meta.dart'; +import '../support/value_or_absent.dart'; import 'priority.dart'; /// A type that represents video encoding information. @@ -40,16 +41,16 @@ class VideoEncoding implements Comparable { }); VideoEncoding copyWith({ - int? maxFramerate, - int? maxBitrate, - Priority? bitratePriority, - Priority? networkPriority, + ValueOrAbsent maxFramerate = const Absent(), + ValueOrAbsent maxBitrate = const Absent(), + ValueOrAbsent bitratePriority = const Absent(), + ValueOrAbsent networkPriority = const Absent(), }) => VideoEncoding( - maxFramerate: maxFramerate ?? this.maxFramerate, - maxBitrate: maxBitrate ?? this.maxBitrate, - bitratePriority: bitratePriority ?? this.bitratePriority, - networkPriority: networkPriority ?? this.networkPriority, + maxFramerate: maxFramerate.valueOr(this.maxFramerate), + maxBitrate: maxBitrate.valueOr(this.maxBitrate), + bitratePriority: bitratePriority.valueOr(this.bitratePriority), + networkPriority: networkPriority.valueOr(this.networkPriority), ); @override diff --git a/lib/src/utils.dart b/lib/src/utils.dart index d242e6d90..fad3bf5b1 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -26,6 +26,7 @@ import 'package:meta/meta.dart'; import './proto/livekit_models.pb.dart' as lk_models; import './support/native.dart'; +import './support/value_or_absent.dart'; import 'extensions.dart'; import 'livekit.dart'; import 'logger.dart'; @@ -285,10 +286,10 @@ class Utils { if (codec != null) { switch (codec) { case 'av1': - result = result.copyWith(maxBitrate: (result.maxBitrate * 0.7).toInt()); + result = result.copyWith(maxBitrate: Value((result.maxBitrate * 0.7).toInt())); break; case 'vp9': - result = result.copyWith(maxBitrate: (result.maxBitrate * 0.85).toInt()); + result = result.copyWith(maxBitrate: Value((result.maxBitrate * 0.85).toInt())); break; default: break;