diff --git a/example/lib/widgets/controls.dart b/example/lib/widgets/controls.dart index e973563..3026b06 100644 --- a/example/lib/widgets/controls.dart +++ b/example/lib/widgets/controls.dart @@ -51,7 +51,7 @@ class _ControlsWidgetState extends State { if (result == true) await participant.unpublishAllTracks(); } - void _muteAudio() async { + void _disableAudio() async { await participant.setMicrophoneEnabled(false); // The following code is an example how to mute a track // if (participant.hasAudio) { @@ -60,7 +60,7 @@ class _ControlsWidgetState extends State { // } } - Future _unmuteAudio() async { + Future _enableAudio() async { await participant.setMicrophoneEnabled(true); // The following code is an example how to unmute / publish a audio track // if (participant.hasAudio) { @@ -73,7 +73,7 @@ class _ControlsWidgetState extends State { // } } - void _muteVideo() async { + void _disableVideo() async { await participant.setCameraEnabled(false); // The following code is an example how to mute a video track // if (participant.hasVideo) { @@ -82,7 +82,7 @@ class _ControlsWidgetState extends State { // } } - void _unmuteVideo() async { + void _enableVideo() async { await participant.setCameraEnabled(true); // The following code is an example how to unmute / publish a video track // if (participant.hasVideo) { @@ -114,7 +114,7 @@ class _ControlsWidgetState extends State { } } - void _shareScreen() async { + void _enableScreenShare() async { final lp = widget.room.localParticipant; for (final track in lp.videoTracks) { @@ -143,7 +143,7 @@ class _ControlsWidgetState extends State { } } - void _unshareScreen() async { + void _disableScreenShare() async { final lp = widget.room.localParticipant; try { @@ -175,16 +175,6 @@ class _ControlsWidgetState extends State { @override Widget build(BuildContext context) { - // mute audio - final canMute = participant.hasAudio && !participant.isMuted; - - final videoPub = - participant.getTrackPublicationBySource(TrackSource.camera); - final videoEnabled = videoPub != null && !videoPub.muted; - final screenSharePub = - participant.getTrackPublicationBySource(TrackSource.screenShareVideo); - final screenShareEnabled = screenSharePub != null && !screenSharePub.muted; - return Padding( padding: const EdgeInsets.symmetric( vertical: 15, @@ -200,27 +190,27 @@ class _ControlsWidgetState extends State { icon: const Icon(EvaIcons.closeCircleOutline), tooltip: 'Unpublish all', ), - if (canMute) + if (participant.isMicrophoneEnabled()) IconButton( - onPressed: _muteAudio, + onPressed: _disableAudio, icon: const Icon(EvaIcons.mic), tooltip: 'mute audio', ) else IconButton( - onPressed: _unmuteAudio, + onPressed: _enableAudio, icon: const Icon(EvaIcons.micOff), tooltip: 'un-mute audio', ), - if (videoEnabled) + if (participant.isCameraEnabled()) IconButton( - onPressed: _muteVideo, + onPressed: _disableVideo, icon: const Icon(EvaIcons.video), tooltip: 'mute video', ) else IconButton( - onPressed: _unmuteVideo, + onPressed: _enableVideo, icon: const Icon(EvaIcons.videoOff), tooltip: 'un-mute video', ), @@ -231,16 +221,16 @@ class _ControlsWidgetState extends State { onPressed: () => _toggleCamera(), tooltip: 'toggle camera', ), - if (screenShareEnabled) + if (participant.isScreenShareEnabled()) IconButton( icon: const Icon(EvaIcons.monitorOutline), - onPressed: () => _unshareScreen(), + onPressed: () => _disableScreenShare(), tooltip: 'unshare screen (experimental)', ) else IconButton( icon: const Icon(EvaIcons.monitor), - onPressed: () => _shareScreen(), + onPressed: () => _enableScreenShare(), tooltip: 'share screen (experimental)', ), IconButton( diff --git a/example/pubspec.lock b/example/pubspec.lock index afc2925..378e630 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -232,28 +232,28 @@ packages: name: path_provider_android url: "https://pub.dartlang.org" source: hosted - version: "2.0.6" + version: "2.0.7" path_provider_ios: dependency: transitive description: name: path_provider_ios url: "https://pub.dartlang.org" source: hosted - version: "2.0.6" + version: "2.0.7" path_provider_linux: dependency: transitive description: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_macos: dependency: transitive description: name: path_provider_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.3" path_provider_platform_interface: dependency: transitive description: diff --git a/lib/src/internal/events.dart b/lib/src/internal/events.dart index 7e6b9c6..49309a4 100644 --- a/lib/src/internal/events.dart +++ b/lib/src/internal/events.dart @@ -61,3 +61,13 @@ class TrackVisibilityUpdatedEvent with TrackEvent, InternalEvent { required this.info, }); } + +@internal +class TrackMuteUpdatedEvent with TrackEvent, InternalEvent { + final Track track; + final bool muted; + const TrackMuteUpdatedEvent({ + required this.track, + required this.muted, + }); +} diff --git a/lib/src/participant/local_participant.dart b/lib/src/participant/local_participant.dart index d78a9b2..8669eb5 100644 --- a/lib/src/participant/local_participant.dart +++ b/lib/src/participant/local_participant.dart @@ -247,15 +247,16 @@ extension LocalParticipantTrackSourceExt on LocalParticipant { } Future setSourceEnabled(TrackSource source, bool enabled) async { - final pub = getTrackPublicationBySource(source); + logger.fine('setSourceEnabled(source: $source, enabled: $enabled)'); + final pub = getTrackPublicationBySource(source) as LocalTrackPublication?; if (pub != null) { if (enabled) { - pub.muted = false; + await pub.unmute(); } else { if (source == TrackSource.screenShareVideo) { await unpublishTrack(pub.sid); } else { - pub.muted = true; + await pub.mute(); } } } else if (enabled) { diff --git a/lib/src/participant/participant.dart b/lib/src/participant/participant.dart index 132588f..2276bf9 100644 --- a/lib/src/participant/participant.dart +++ b/lib/src/participant/participant.dart @@ -1,5 +1,4 @@ import 'package:collection/collection.dart'; -import 'package:livekit_client/src/track/track.dart'; import 'package:meta/meta.dart'; import '../events.dart'; @@ -8,6 +7,7 @@ import '../logger.dart'; import '../managers/event.dart'; import '../proto/livekit_models.pb.dart' as lk_models; import '../support/disposable.dart'; +import '../track/track.dart'; import '../track/track_publication.dart'; import '../types.dart'; import 'remote_participant.dart'; @@ -199,6 +199,11 @@ extension ParticipantTrackSourceExt on Participant { true); } + bool isScreenShareEnabled() { + return !(getTrackPublicationBySource(TrackSource.screenShareVideo)?.muted ?? + true); + } + /// Find a track publication by its [TrackSource] TrackPublication? getTrackPublicationBySource(TrackSource source) { if (source == TrackSource.unknown) return null; diff --git a/lib/src/participant/remote_participant.dart b/lib/src/participant/remote_participant.dart index 2397697..244f796 100644 --- a/lib/src/participant/remote_participant.dart +++ b/lib/src/participant/remote_participant.dart @@ -98,8 +98,7 @@ class RemoteParticipant extends Participant { } await track.start(); - - pub.track = track; + await pub.updateTrack(track); addTrackPublication(pub); [events, roomEvents].emit(TrackSubscribedEvent( diff --git a/lib/src/room.dart b/lib/src/room.dart index 3c5a6f1..2bb2187 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -16,6 +16,7 @@ import 'proto/livekit_models.pb.dart' as lk_models; import 'proto/livekit_rtc.pb.dart' as lk_rtc; import 'rtc_engine.dart'; import 'support/disposable.dart'; +import 'track/local_track_publication.dart'; import 'track/track.dart'; import 'types.dart'; @@ -178,8 +179,13 @@ class Room extends DisposableChangeNotifier with EventsEmittable { (event) => _onSignalConnectionQualityUpdateEvent(event.updates)) ..on(_onDataMessageEvent) ..on((event) async { - final track = localParticipant.trackPublications[event.sid]; - track?.muted = event.muted; + final publication = localParticipant.trackPublications[event.sid] + as LocalTrackPublication?; + if (event.muted) { + await publication?.mute(); + } else { + await publication?.unmute(); + } }) ..on((event) async { final idParts = event.stream.id.split('|'); diff --git a/lib/src/track/local_audio_track.dart b/lib/src/track/local_audio_track.dart index e9fc258..d4d3c2c 100644 --- a/lib/src/track/local_audio_track.dart +++ b/lib/src/track/local_audio_track.dart @@ -6,9 +6,10 @@ import '../exceptions.dart'; import '../logger.dart'; import '../types.dart'; import 'audio_track.dart'; +import 'local_track.dart'; import 'options.dart'; -class LocalAudioTrack extends AudioTrack { +class LocalAudioTrack extends AudioTrack with LocalTrack { // private constructor LocalAudioTrack._( TrackSource source, diff --git a/lib/src/track/local_track.dart b/lib/src/track/local_track.dart new file mode 100644 index 0000000..48a71a9 --- /dev/null +++ b/lib/src/track/local_track.dart @@ -0,0 +1,22 @@ +import '../internal/events.dart'; +import '../logger.dart'; +import 'track.dart'; + +mixin LocalTrack on Track { + // only local tracks can set muted + Future mute() async { + logger.fine('LocalTrack.mute() muted: $muted'); + if (muted) return; + await disable(); + updateMuted(true); + events.emit(TrackMuteUpdatedEvent(track: this, muted: muted)); + } + + Future unmute() async { + logger.fine('LocalTrack.unmute() muted: $muted'); + if (!muted) return; + await enable(); + updateMuted(false); + events.emit(TrackMuteUpdatedEvent(track: this, muted: muted)); + } +} diff --git a/lib/src/track/local_track_publication.dart b/lib/src/track/local_track_publication.dart index 3f5bbfe..bdc0b0a 100644 --- a/lib/src/track/local_track_publication.dart +++ b/lib/src/track/local_track_publication.dart @@ -1,8 +1,9 @@ import '../events.dart'; import '../extensions.dart'; -import '../logger.dart'; +import '../internal/events.dart'; import '../participant/local_participant.dart'; import '../proto/livekit_models.pb.dart' as lk_models; +import 'local_track.dart'; import 'track.dart'; import 'track_publication.dart'; @@ -14,7 +15,7 @@ class LocalTrackPublication extends TrackPublication { Track track, this._participant, ) : super.fromInfo(info) { - this.track = track; + updateTrack(track); // register dispose func onDispose(() async { // this object is responsible for disposing track @@ -22,28 +23,42 @@ class LocalTrackPublication extends TrackPublication { }); } - /// Mute or unmute the current track. When muted, track will stop sending data @override - set muted(bool val) { - if (val == muted) return; - logger.finer('setMute: ${val}'); + Future updateTrack(Track? newValue) async { + final didUpdate = await super.updateTrack(newValue); - super.muted = val; - track?.mediaStreamTrack.enabled = !val; - _participant.engine.signalClient.sendMuteTrack(sid, val); - - if (val) { - // Track muted - [_participant.events, _participant.roomEvents].emit(TrackMutedEvent( - participant: _participant, - track: this, - )); - } else { - // Track un-muted - [_participant.events, _participant.roomEvents].emit(TrackUnmutedEvent( - participant: _participant, - track: this, - )); + if (newValue != null) { + // attach listener to track + final listener = newValue.createListener() + // listen for track muted events + ..on((event) { + // send signal to server + _participant.engine.signalClient.sendMuteTrack(sid, event.muted); + // emit events + final newEvent = event.muted + ? TrackMutedEvent(participant: _participant, track: this) + : TrackUnmutedEvent(participant: _participant, track: this); + [_participant.events, _participant.roomEvents].emit(newEvent); + }); + // dispose listener when the track is disposed + newValue.onDispose(() => listener.dispose()); } + + return didUpdate; + } + + @override + bool get muted => track?.muted ?? super.muted; + + Future mute() async { + if (track is! LocalTrack) return; + // Mute the track associated with this publication + return (track as LocalTrack).mute(); + } + + Future unmute() async { + if (track is! LocalTrack) return; + // Unmute the track associated with this publication + return (track as LocalTrack).unmute(); } } diff --git a/lib/src/track/local_video_track.dart b/lib/src/track/local_video_track.dart index 89fcba8..89f5ef8 100644 --- a/lib/src/track/local_video_track.dart +++ b/lib/src/track/local_video_track.dart @@ -3,13 +3,14 @@ import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; import '../exceptions.dart'; import '../logger.dart'; import '../types.dart'; +import 'local_track.dart'; import 'options.dart'; import 'track.dart'; import 'video_track.dart'; /// A video track from the local device. Use static methods in this class to create /// video tracks. -class LocalVideoTrack extends VideoTrack { +class LocalVideoTrack extends VideoTrack with LocalTrack { // // Options used for this track // diff --git a/lib/src/track/remote_track_publication.dart b/lib/src/track/remote_track_publication.dart index 7b725b0..c8804f6 100644 --- a/lib/src/track/remote_track_publication.dart +++ b/lib/src/track/remote_track_publication.dart @@ -60,7 +60,15 @@ class RemoteTrackPublication extends TrackPublication { wait: const Duration(seconds: 2), ); - this.track = track; + updateTrack(track); + } + + @internal + @override + void updateFromInfo(lk_models.TrackInfo info) { + super.updateFromInfo(info); + updateMuted(info.muted); + track?.updateMuted(info.muted); } // called any time visibility info updates @@ -133,33 +141,33 @@ class RemoteTrackPublication extends TrackPublication { _participant.engine.signalClient.sendUpdateTrackSettings(settings); } + @internal @override - set track(Track? newValue) { - if (super.track != newValue) { - logger.fine('setTrack ${newValue} $sid ${objectId}'); - // dispose previous track (if exists) - super.track?.dispose(); - super.track = newValue; + Future updateTrack(Track? newValue) async { + final didUpdate = await super.updateTrack(track); - // Only listen for visibility updates if video optimization is on - // and the attached track is a video track - if (_participant.engine.connectOptions.optimizeVideo && - newValue != null && - newValue.kind == lk_models.TrackType.VIDEO) { - // - // Attach visibility event listener (if video track) - // - final listener = newValue.createListener(); - listener.on( - _onVideoRendererVisibilityUpdateEvent); - newValue.onDispose(() async { - await listener.dispose(); - // consider all views are disposed when track is null - _visibilities.clear(); - if (!isDisposed) _visibilityDidUpdate?.call(null); - }); - } + // Only listen for visibility updates if video optimization is on + // and the attached track is a video track + if (didUpdate && + newValue != null && + _participant.engine.connectOptions.optimizeVideo && + newValue.kind == lk_models.TrackType.VIDEO) { + // + // Attach visibility event listener (if video track) + // + final listener = newValue.createListener(); + listener.on( + _onVideoRendererVisibilityUpdateEvent); + // + newValue.onDispose(() async { + await listener.dispose(); + // consider all views are disposed when track is null + _visibilities.clear(); + if (!isDisposed) _visibilityDidUpdate?.call(null); + }); } + + return didUpdate; } set videoQuality(lk_rtc.VideoQuality val) { @@ -190,34 +198,7 @@ class RemoteTrackPublication extends TrackPublication { publication: this, )); // Simply set to null for now - track = null; - } - } - - /// for internal use - /// {@nodoc} - @override - @internal - set muted(bool val) { - if (val == muted) { - return; - } - super.muted = val; - if (val) { - // Track muted - [_participant.events, _participant.roomEvents].emit(TrackMutedEvent( - participant: _participant, - track: this, - )); - } else { - // Track un-muted - [_participant.events, _participant.roomEvents].emit(TrackUnmutedEvent( - participant: _participant, - track: this, - )); - } - if (subscribed) { - track?.mediaStreamTrack.enabled = !val; + updateTrack(null); } } diff --git a/lib/src/track/track.dart b/lib/src/track/track.dart index 51dc33e..2475d7f 100644 --- a/lib/src/track/track.dart +++ b/lib/src/track/track.dart @@ -33,6 +33,9 @@ abstract class Track extends DisposableChangeNotifier bool _active = false; bool get isActive => _active; + bool _muted = false; + bool get muted => _muted; + Track( this.kind, this.source, @@ -52,9 +55,6 @@ abstract class Track extends DisposableChangeNotifier }); } - bool get muted => - mediaStreamTrack.muted == null ? false : mediaStreamTrack.muted!; - rtc.RTCRtpMediaType get mediaType { switch (kind) { case lk_models.TrackType.AUDIO: @@ -125,4 +125,7 @@ abstract class Track extends DisposableChangeNotifier '[$objectId] set rtc.mediaStreamTrack.enabled did throw ${_}'); } } + + @internal + void updateMuted(bool muted) => _muted = muted; } diff --git a/lib/src/track/track_publication.dart b/lib/src/track/track_publication.dart index bbf2b4a..a5945ee 100644 --- a/lib/src/track/track_publication.dart +++ b/lib/src/track/track_publication.dart @@ -1,7 +1,9 @@ -import '../support/disposable.dart'; -import '../proto/livekit_models.pb.dart' as lk_models; -import '../types.dart'; +import 'package:meta/meta.dart'; + import '../extensions.dart'; +import '../proto/livekit_models.pb.dart' as lk_models; +import '../support/disposable.dart'; +import '../types.dart'; import 'track.dart'; /// Represents a track that's published to the server. This class contains @@ -16,12 +18,17 @@ abstract class TrackPublication extends Disposable { final lk_models.TrackType kind; final TrackSource source; - Track? track; - bool muted = false; + Track? _track; + Track? get track => _track; + + // metadata-muted + bool _muted = false; + bool get muted => _muted; + bool simulcasted = false; TrackDimension? dimension; - bool get subscribed => track != null; + bool get subscribed => _track != null; TrackPublication.fromInfo(lk_models.TrackInfo info) : sid = info.sid, @@ -36,7 +43,6 @@ abstract class TrackPublication extends Disposable { kind == lk_models.TrackType.VIDEO && name == Track.screenShareName; void updateFromInfo(lk_models.TrackInfo info) { - muted = info.muted; simulcasted = info.simulcast; if (info.type == lk_models.TrackType.VIDEO) { dimension = TrackDimension(info.width, info.height); @@ -51,4 +57,19 @@ abstract class TrackPublication extends Disposable { @override bool operator ==(Object other) => other is TrackPublication && sid == other.sid; + + @internal + void updateMuted(bool muted) => _muted = muted; + + // Update track to new value, dispose previous if exists. + // Returns true if value has changed. + // Intended for internal use only. + @internal + Future updateTrack(Track? newValue) async { + if (_track == newValue) return false; + // dispose previous track (if exists) + await _track?.dispose(); + _track = newValue; + return true; + } } diff --git a/pubspec.lock b/pubspec.lock index da97412..baf7fdd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -169,21 +169,35 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.0.7" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.7" + path_provider_ios: + dependency: transitive + description: + name: path_provider_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.7" path_provider_linux: dependency: transitive description: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.2" path_provider_macos: dependency: transitive description: name: path_provider_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.3" path_provider_platform_interface: dependency: transitive description: @@ -197,7 +211,7 @@ packages: name: path_provider_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.0.4" platform: dependency: transitive description: @@ -218,7 +232,7 @@ packages: name: process url: "https://pub.dartlang.org" source: hosted - version: "4.2.3" + version: "4.2.4" protobuf: dependency: "direct main" description: @@ -314,7 +328,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.2.9" + version: "2.3.0" xdg_directories: dependency: transitive description: