diff --git a/example/pubspec.lock b/example/pubspec.lock index df901bc..2a98327 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -56,7 +56,7 @@ packages: name: dart_webrtc url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.3" eva_icons_flutter: dependency: "direct main" description: @@ -134,14 +134,14 @@ packages: name: flutter_webrtc url: "https://pub.dartlang.org" source: hosted - version: "0.8.0" + version: "0.8.1" google_fonts: dependency: "direct main" description: name: google_fonts url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.2.0" http: dependency: transitive description: @@ -239,7 +239,7 @@ packages: name: path_provider_android url: "https://pub.dartlang.org" source: hosted - version: "2.0.9" + version: "2.0.11" path_provider_ios: dependency: transitive description: @@ -316,7 +316,7 @@ packages: name: provider url: "https://pub.dartlang.org" source: hosted - version: "6.0.1" + version: "6.0.2" shared_preferences: dependency: "direct main" description: @@ -461,7 +461,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.3.1" + version: "2.3.3" xdg_directories: dependency: transitive description: diff --git a/lib/src/core/engine.dart b/lib/src/core/engine.dart index c4923ed..455ef77 100644 --- a/lib/src/core/engine.dart +++ b/lib/src/core/engine.dart @@ -600,6 +600,8 @@ class Engine extends Disposable with EventsEmittable { ..on((event) => events.emit(event)) // relay ..on((event) => events.emit(event)) + // relay to Room + ..on((event) => events.emit(event)) ..on((event) async { if (connectionState == ConnectionState.reconnecting) { logger.warning('Received leave signal while engine is reconnecting.'); diff --git a/lib/src/core/room.dart b/lib/src/core/room.dart index e1a5689..c4c4f69 100644 --- a/lib/src/core/room.dart +++ b/lib/src/core/room.dart @@ -56,6 +56,10 @@ class Room extends DisposableChangeNotifier with EventsEmittable { /// sid of the room String? sid; + /// Server version + String? get serverVersion => _serverVersion; + String? _serverVersion; + List _activeSpeakers = []; /// a list of participants that are actively speaking, including local participant. @@ -109,6 +113,7 @@ class Room extends DisposableChangeNotifier with EventsEmittable { sid = joinResponse.room.sid; name = joinResponse.room.name; + _serverVersion = joinResponse.serverVersion; logger.fine( 'Connected to LiveKit server, version: ${joinResponse.serverVersion}'); @@ -168,6 +173,20 @@ class Room extends DisposableChangeNotifier with EventsEmittable { await publication?.unmute(); } }) + ..on((event) { + // Signal for Dynacast + final options = roomOptions ?? const RoomOptions(); + // Dynacast is off or is unsupported + if (!options.dynacast || _serverVersion == '0.15.1') return; + // Find the publication + final publication = localParticipant?.trackPublications[event.trackSid]; + if (publication == null) { + logger.warning( + 'Received subscribed quality update for unknown track (${event.trackSid})'); + return; + } + publication.updatePublishingLayers(event.updates); + }) ..on((event) async { logger.fine('EngineTrackAddedEvent trackSid:${event.track.id}'); diff --git a/lib/src/core/signal_client.dart b/lib/src/core/signal_client.dart index 3060a97..ddb7758 100644 --- a/lib/src/core/signal_client.dart +++ b/lib/src/core/signal_client.dart @@ -180,6 +180,12 @@ class SignalClient extends Disposable with EventsEmittable { updates: msg.streamStateUpdate.streamStates, )); break; + case lk_rtc.SignalResponse_Message.subscribedQualityUpdate: + events.emit(SignalSubscribedQualityUpdatedEvent( + trackSid: msg.subscribedQualityUpdate.trackSid, + updates: msg.subscribedQualityUpdate.subscribedQualities, + )); + break; default: logger.warning('skipping unsupported signal message'); } diff --git a/lib/src/extensions.dart b/lib/src/extensions.dart index 8e6bb1f..978db48 100644 --- a/lib/src/extensions.dart +++ b/lib/src/extensions.dart @@ -132,3 +132,11 @@ extension PBStreamStateExt on lk_rtc.StreamState { }[this] ?? StreamState.paused; } + +extension VideoQualityExt on lk_models.VideoQuality { + String toRid() => { + lk_models.VideoQuality.HIGH: 'f', + lk_models.VideoQuality.MEDIUM: 'h', + lk_models.VideoQuality.LOW: 'q', + }[this]!; +} diff --git a/lib/src/internal/events.dart b/lib/src/internal/events.dart index f90e79b..4443a22 100644 --- a/lib/src/internal/events.dart +++ b/lib/src/internal/events.dart @@ -196,6 +196,17 @@ class SignalStreamStateUpdatedEvent }); } +@internal +class SignalSubscribedQualityUpdatedEvent + with SignalEvent, EngineEvent, InternalEvent { + final String trackSid; + final List updates; + const SignalSubscribedQualityUpdatedEvent({ + required this.trackSid, + required this.updates, + }); +} + // ---------------------------------------------------------------------- // Engine events // ---------------------------------------------------------------------- diff --git a/lib/src/options.dart b/lib/src/options.dart index a6d9375..3edbad3 100644 --- a/lib/src/options.dart +++ b/lib/src/options.dart @@ -52,6 +52,11 @@ class RoomOptions { /// Defaults to false. final bool adaptiveStream; + /// enable Dynacast, off by default. With Dynacast dynamically pauses + /// video layers that are not being consumed by any subscribers, significantly + /// reducing publishing CPU and bandwidth usage. + final bool dynacast; + /// Set this to false in case you would like to stop the track yourself. /// If you set this to false, make sure you call [Track.stop]. /// Defaults to true. @@ -64,6 +69,7 @@ class RoomOptions { this.defaultVideoPublishOptions = const VideoPublishOptions(), this.defaultAudioPublishOptions = const AudioPublishOptions(), this.adaptiveStream = false, + this.dynacast = false, this.stopLocalTrackOnUnpublish = true, }); } diff --git a/lib/src/publication/local.dart b/lib/src/publication/local.dart index 4ef7c34..9b1bbf3 100644 --- a/lib/src/publication/local.dart +++ b/lib/src/publication/local.dart @@ -1,5 +1,11 @@ +import 'package:collection/collection.dart'; +import 'package:meta/meta.dart'; + +import '../extensions.dart'; +import '../logger.dart'; import '../participant/local.dart'; import '../proto/livekit_models.pb.dart' as lk_models; +import '../proto/livekit_rtc.pb.dart' as lk_rtc; import '../track/local/local.dart'; import 'track_publication.dart'; @@ -27,4 +33,40 @@ class LocalTrackPublication extends TrackPublication { /// Unmute the track associated with this publication Future unmute() async => await track?.unmute(); + + @internal + void updatePublishingLayers(List layers) async { + // + final params = track?.sender?.parameters; + if (params == null) return; + + final encodings = params.encodings; + if (encodings == null) return; + + bool didChange = false; + + for (final encoding in encodings) { + final layer = layers.firstWhereOrNull((e) => + // If there is exact match, use it + (e.quality.toRid() == encoding.rid) || + // Use low layer if rid is null (not simulcast) + (encoding.rid == null && e.quality == lk_models.VideoQuality.LOW)); + if (layer != null && encoding.active != layer.enabled) { + encoding.active = layer.enabled; + logger.fine('Setting layer ${layer.quality} to ${layer.enabled}'); + // FireFox does not support setting encoding.active to false, so we + // have a workaround of lowering its bitrate and resolution to the min. + // TODO: Workaround for firefox + didChange = true; + } + } + + if (didChange) { + params.encodings = encodings; + final result = await track?.sender?.setParameters(params); + if (result == false) { + logger.warning('Failed to update sender parameters'); + } + } + } }