Match muted behavior with JS SDK (#32)

* attempt 1

* implement

* clean up

* clean up

* cleaner code

* mute was opposite

* fix suggestion

* fix local pub muted

* update example

* update pubs

* format
This commit is contained in:
Hiroshi Horie
2021-11-20 02:39:52 +07:00
committed by GitHub
parent 4a59a1e6d5
commit 4f0973ce66
15 changed files with 199 additions and 130 deletions
+15 -25
View File
@@ -51,7 +51,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
if (result == true) await participant.unpublishAllTracks(); if (result == true) await participant.unpublishAllTracks();
} }
void _muteAudio() async { void _disableAudio() async {
await participant.setMicrophoneEnabled(false); await participant.setMicrophoneEnabled(false);
// The following code is an example how to mute a track // The following code is an example how to mute a track
// if (participant.hasAudio) { // if (participant.hasAudio) {
@@ -60,7 +60,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
// } // }
} }
Future<void> _unmuteAudio() async { Future<void> _enableAudio() async {
await participant.setMicrophoneEnabled(true); await participant.setMicrophoneEnabled(true);
// The following code is an example how to unmute / publish a audio track // The following code is an example how to unmute / publish a audio track
// if (participant.hasAudio) { // if (participant.hasAudio) {
@@ -73,7 +73,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
// } // }
} }
void _muteVideo() async { void _disableVideo() async {
await participant.setCameraEnabled(false); await participant.setCameraEnabled(false);
// The following code is an example how to mute a video track // The following code is an example how to mute a video track
// if (participant.hasVideo) { // if (participant.hasVideo) {
@@ -82,7 +82,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
// } // }
} }
void _unmuteVideo() async { void _enableVideo() async {
await participant.setCameraEnabled(true); await participant.setCameraEnabled(true);
// The following code is an example how to unmute / publish a video track // The following code is an example how to unmute / publish a video track
// if (participant.hasVideo) { // if (participant.hasVideo) {
@@ -114,7 +114,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
} }
} }
void _shareScreen() async { void _enableScreenShare() async {
final lp = widget.room.localParticipant; final lp = widget.room.localParticipant;
for (final track in lp.videoTracks) { for (final track in lp.videoTracks) {
@@ -143,7 +143,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
} }
} }
void _unshareScreen() async { void _disableScreenShare() async {
final lp = widget.room.localParticipant; final lp = widget.room.localParticipant;
try { try {
@@ -175,16 +175,6 @@ class _ControlsWidgetState extends State<ControlsWidget> {
@override @override
Widget build(BuildContext context) { 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( return Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 15, vertical: 15,
@@ -200,27 +190,27 @@ class _ControlsWidgetState extends State<ControlsWidget> {
icon: const Icon(EvaIcons.closeCircleOutline), icon: const Icon(EvaIcons.closeCircleOutline),
tooltip: 'Unpublish all', tooltip: 'Unpublish all',
), ),
if (canMute) if (participant.isMicrophoneEnabled())
IconButton( IconButton(
onPressed: _muteAudio, onPressed: _disableAudio,
icon: const Icon(EvaIcons.mic), icon: const Icon(EvaIcons.mic),
tooltip: 'mute audio', tooltip: 'mute audio',
) )
else else
IconButton( IconButton(
onPressed: _unmuteAudio, onPressed: _enableAudio,
icon: const Icon(EvaIcons.micOff), icon: const Icon(EvaIcons.micOff),
tooltip: 'un-mute audio', tooltip: 'un-mute audio',
), ),
if (videoEnabled) if (participant.isCameraEnabled())
IconButton( IconButton(
onPressed: _muteVideo, onPressed: _disableVideo,
icon: const Icon(EvaIcons.video), icon: const Icon(EvaIcons.video),
tooltip: 'mute video', tooltip: 'mute video',
) )
else else
IconButton( IconButton(
onPressed: _unmuteVideo, onPressed: _enableVideo,
icon: const Icon(EvaIcons.videoOff), icon: const Icon(EvaIcons.videoOff),
tooltip: 'un-mute video', tooltip: 'un-mute video',
), ),
@@ -231,16 +221,16 @@ class _ControlsWidgetState extends State<ControlsWidget> {
onPressed: () => _toggleCamera(), onPressed: () => _toggleCamera(),
tooltip: 'toggle camera', tooltip: 'toggle camera',
), ),
if (screenShareEnabled) if (participant.isScreenShareEnabled())
IconButton( IconButton(
icon: const Icon(EvaIcons.monitorOutline), icon: const Icon(EvaIcons.monitorOutline),
onPressed: () => _unshareScreen(), onPressed: () => _disableScreenShare(),
tooltip: 'unshare screen (experimental)', tooltip: 'unshare screen (experimental)',
) )
else else
IconButton( IconButton(
icon: const Icon(EvaIcons.monitor), icon: const Icon(EvaIcons.monitor),
onPressed: () => _shareScreen(), onPressed: () => _enableScreenShare(),
tooltip: 'share screen (experimental)', tooltip: 'share screen (experimental)',
), ),
IconButton( IconButton(
+4 -4
View File
@@ -232,28 +232,28 @@ packages:
name: path_provider_android name: path_provider_android
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.6" version: "2.0.7"
path_provider_ios: path_provider_ios:
dependency: transitive dependency: transitive
description: description:
name: path_provider_ios name: path_provider_ios
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.6" version: "2.0.7"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
name: path_provider_linux name: path_provider_linux
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.1" version: "2.1.2"
path_provider_macos: path_provider_macos:
dependency: transitive dependency: transitive
description: description:
name: path_provider_macos name: path_provider_macos
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.2" version: "2.0.3"
path_provider_platform_interface: path_provider_platform_interface:
dependency: transitive dependency: transitive
description: description:
+10
View File
@@ -61,3 +61,13 @@ class TrackVisibilityUpdatedEvent with TrackEvent, InternalEvent {
required this.info, required this.info,
}); });
} }
@internal
class TrackMuteUpdatedEvent with TrackEvent, InternalEvent {
final Track track;
final bool muted;
const TrackMuteUpdatedEvent({
required this.track,
required this.muted,
});
}
+4 -3
View File
@@ -247,15 +247,16 @@ extension LocalParticipantTrackSourceExt on LocalParticipant {
} }
Future<void> setSourceEnabled(TrackSource source, bool enabled) async { Future<void> 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 (pub != null) {
if (enabled) { if (enabled) {
pub.muted = false; await pub.unmute();
} else { } else {
if (source == TrackSource.screenShareVideo) { if (source == TrackSource.screenShareVideo) {
await unpublishTrack(pub.sid); await unpublishTrack(pub.sid);
} else { } else {
pub.muted = true; await pub.mute();
} }
} }
} else if (enabled) { } else if (enabled) {
+6 -1
View File
@@ -1,5 +1,4 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:livekit_client/src/track/track.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import '../events.dart'; import '../events.dart';
@@ -8,6 +7,7 @@ import '../logger.dart';
import '../managers/event.dart'; import '../managers/event.dart';
import '../proto/livekit_models.pb.dart' as lk_models; import '../proto/livekit_models.pb.dart' as lk_models;
import '../support/disposable.dart'; import '../support/disposable.dart';
import '../track/track.dart';
import '../track/track_publication.dart'; import '../track/track_publication.dart';
import '../types.dart'; import '../types.dart';
import 'remote_participant.dart'; import 'remote_participant.dart';
@@ -199,6 +199,11 @@ extension ParticipantTrackSourceExt on Participant {
true); true);
} }
bool isScreenShareEnabled() {
return !(getTrackPublicationBySource(TrackSource.screenShareVideo)?.muted ??
true);
}
/// Find a track publication by its [TrackSource] /// Find a track publication by its [TrackSource]
TrackPublication? getTrackPublicationBySource(TrackSource source) { TrackPublication? getTrackPublicationBySource(TrackSource source) {
if (source == TrackSource.unknown) return null; if (source == TrackSource.unknown) return null;
+1 -2
View File
@@ -98,8 +98,7 @@ class RemoteParticipant extends Participant {
} }
await track.start(); await track.start();
await pub.updateTrack(track);
pub.track = track;
addTrackPublication(pub); addTrackPublication(pub);
[events, roomEvents].emit(TrackSubscribedEvent( [events, roomEvents].emit(TrackSubscribedEvent(
+8 -2
View File
@@ -16,6 +16,7 @@ import 'proto/livekit_models.pb.dart' as lk_models;
import 'proto/livekit_rtc.pb.dart' as lk_rtc; import 'proto/livekit_rtc.pb.dart' as lk_rtc;
import 'rtc_engine.dart'; import 'rtc_engine.dart';
import 'support/disposable.dart'; import 'support/disposable.dart';
import 'track/local_track_publication.dart';
import 'track/track.dart'; import 'track/track.dart';
import 'types.dart'; import 'types.dart';
@@ -178,8 +179,13 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
(event) => _onSignalConnectionQualityUpdateEvent(event.updates)) (event) => _onSignalConnectionQualityUpdateEvent(event.updates))
..on<EngineDataPacketReceivedEvent>(_onDataMessageEvent) ..on<EngineDataPacketReceivedEvent>(_onDataMessageEvent)
..on<EngineRemoteMuteChangedEvent>((event) async { ..on<EngineRemoteMuteChangedEvent>((event) async {
final track = localParticipant.trackPublications[event.sid]; final publication = localParticipant.trackPublications[event.sid]
track?.muted = event.muted; as LocalTrackPublication?;
if (event.muted) {
await publication?.mute();
} else {
await publication?.unmute();
}
}) })
..on<EngineTrackAddedEvent>((event) async { ..on<EngineTrackAddedEvent>((event) async {
final idParts = event.stream.id.split('|'); final idParts = event.stream.id.split('|');
+2 -1
View File
@@ -6,9 +6,10 @@ import '../exceptions.dart';
import '../logger.dart'; import '../logger.dart';
import '../types.dart'; import '../types.dart';
import 'audio_track.dart'; import 'audio_track.dart';
import 'local_track.dart';
import 'options.dart'; import 'options.dart';
class LocalAudioTrack extends AudioTrack { class LocalAudioTrack extends AudioTrack with LocalTrack {
// private constructor // private constructor
LocalAudioTrack._( LocalAudioTrack._(
TrackSource source, TrackSource source,
+22
View File
@@ -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<void> mute() async {
logger.fine('LocalTrack.mute() muted: $muted');
if (muted) return;
await disable();
updateMuted(true);
events.emit(TrackMuteUpdatedEvent(track: this, muted: muted));
}
Future<void> unmute() async {
logger.fine('LocalTrack.unmute() muted: $muted');
if (!muted) return;
await enable();
updateMuted(false);
events.emit(TrackMuteUpdatedEvent(track: this, muted: muted));
}
}
+37 -22
View File
@@ -1,8 +1,9 @@
import '../events.dart'; import '../events.dart';
import '../extensions.dart'; import '../extensions.dart';
import '../logger.dart'; import '../internal/events.dart';
import '../participant/local_participant.dart'; import '../participant/local_participant.dart';
import '../proto/livekit_models.pb.dart' as lk_models; import '../proto/livekit_models.pb.dart' as lk_models;
import 'local_track.dart';
import 'track.dart'; import 'track.dart';
import 'track_publication.dart'; import 'track_publication.dart';
@@ -14,7 +15,7 @@ class LocalTrackPublication extends TrackPublication {
Track track, Track track,
this._participant, this._participant,
) : super.fromInfo(info) { ) : super.fromInfo(info) {
this.track = track; updateTrack(track);
// register dispose func // register dispose func
onDispose(() async { onDispose(() async {
// this object is responsible for disposing track // 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 @override
set muted(bool val) { Future<bool> updateTrack(Track? newValue) async {
if (val == muted) return; final didUpdate = await super.updateTrack(newValue);
logger.finer('setMute: ${val}');
super.muted = val; if (newValue != null) {
track?.mediaStreamTrack.enabled = !val; // attach listener to track
_participant.engine.signalClient.sendMuteTrack(sid, val); final listener = newValue.createListener()
// listen for track muted events
if (val) { ..on<TrackMuteUpdatedEvent>((event) {
// Track muted // send signal to server
[_participant.events, _participant.roomEvents].emit(TrackMutedEvent( _participant.engine.signalClient.sendMuteTrack(sid, event.muted);
participant: _participant, // emit events
track: this, final newEvent = event.muted
)); ? TrackMutedEvent(participant: _participant, track: this)
} else { : TrackUnmutedEvent(participant: _participant, track: this);
// Track un-muted [_participant.events, _participant.roomEvents].emit(newEvent);
[_participant.events, _participant.roomEvents].emit(TrackUnmutedEvent( });
participant: _participant, // dispose listener when the track is disposed
track: this, newValue.onDispose(() => listener.dispose());
));
} }
return didUpdate;
}
@override
bool get muted => track?.muted ?? super.muted;
Future<void> mute() async {
if (track is! LocalTrack) return;
// Mute the track associated with this publication
return (track as LocalTrack).mute();
}
Future<void> unmute() async {
if (track is! LocalTrack) return;
// Unmute the track associated with this publication
return (track as LocalTrack).unmute();
} }
} }
+2 -1
View File
@@ -3,13 +3,14 @@ import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
import '../exceptions.dart'; import '../exceptions.dart';
import '../logger.dart'; import '../logger.dart';
import '../types.dart'; import '../types.dart';
import 'local_track.dart';
import 'options.dart'; import 'options.dart';
import 'track.dart'; import 'track.dart';
import 'video_track.dart'; import 'video_track.dart';
/// A video track from the local device. Use static methods in this class to create /// A video track from the local device. Use static methods in this class to create
/// video tracks. /// video tracks.
class LocalVideoTrack extends VideoTrack { class LocalVideoTrack extends VideoTrack with LocalTrack {
// //
// Options used for this track // Options used for this track
// //
+34 -53
View File
@@ -60,7 +60,15 @@ class RemoteTrackPublication extends TrackPublication {
wait: const Duration(seconds: 2), 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 // called any time visibility info updates
@@ -133,33 +141,33 @@ class RemoteTrackPublication extends TrackPublication {
_participant.engine.signalClient.sendUpdateTrackSettings(settings); _participant.engine.signalClient.sendUpdateTrackSettings(settings);
} }
@internal
@override @override
set track(Track? newValue) { Future<bool> updateTrack(Track? newValue) async {
if (super.track != newValue) { final didUpdate = await super.updateTrack(track);
logger.fine('setTrack ${newValue} $sid ${objectId}');
// dispose previous track (if exists)
super.track?.dispose();
super.track = newValue;
// Only listen for visibility updates if video optimization is on // Only listen for visibility updates if video optimization is on
// and the attached track is a video track // and the attached track is a video track
if (_participant.engine.connectOptions.optimizeVideo && if (didUpdate &&
newValue != null && newValue != null &&
newValue.kind == lk_models.TrackType.VIDEO) { _participant.engine.connectOptions.optimizeVideo &&
// newValue.kind == lk_models.TrackType.VIDEO) {
// Attach visibility event listener (if video track) //
// // Attach visibility event listener (if video track)
final listener = newValue.createListener(); //
listener.on<TrackVisibilityUpdatedEvent>( final listener = newValue.createListener();
_onVideoRendererVisibilityUpdateEvent); listener.on<TrackVisibilityUpdatedEvent>(
newValue.onDispose(() async { _onVideoRendererVisibilityUpdateEvent);
await listener.dispose(); //
// consider all views are disposed when track is null newValue.onDispose(() async {
_visibilities.clear(); await listener.dispose();
if (!isDisposed) _visibilityDidUpdate?.call(null); // consider all views are disposed when track is null
}); _visibilities.clear();
} if (!isDisposed) _visibilityDidUpdate?.call(null);
});
} }
return didUpdate;
} }
set videoQuality(lk_rtc.VideoQuality val) { set videoQuality(lk_rtc.VideoQuality val) {
@@ -190,34 +198,7 @@ class RemoteTrackPublication extends TrackPublication {
publication: this, publication: this,
)); ));
// Simply set to null for now // Simply set to null for now
track = null; updateTrack(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;
} }
} }
+6 -3
View File
@@ -33,6 +33,9 @@ abstract class Track extends DisposableChangeNotifier
bool _active = false; bool _active = false;
bool get isActive => _active; bool get isActive => _active;
bool _muted = false;
bool get muted => _muted;
Track( Track(
this.kind, this.kind,
this.source, this.source,
@@ -52,9 +55,6 @@ abstract class Track extends DisposableChangeNotifier
}); });
} }
bool get muted =>
mediaStreamTrack.muted == null ? false : mediaStreamTrack.muted!;
rtc.RTCRtpMediaType get mediaType { rtc.RTCRtpMediaType get mediaType {
switch (kind) { switch (kind) {
case lk_models.TrackType.AUDIO: case lk_models.TrackType.AUDIO:
@@ -125,4 +125,7 @@ abstract class Track extends DisposableChangeNotifier
'[$objectId] set rtc.mediaStreamTrack.enabled did throw ${_}'); '[$objectId] set rtc.mediaStreamTrack.enabled did throw ${_}');
} }
} }
@internal
void updateMuted(bool muted) => _muted = muted;
} }
+28 -7
View File
@@ -1,7 +1,9 @@
import '../support/disposable.dart'; import 'package:meta/meta.dart';
import '../proto/livekit_models.pb.dart' as lk_models;
import '../types.dart';
import '../extensions.dart'; import '../extensions.dart';
import '../proto/livekit_models.pb.dart' as lk_models;
import '../support/disposable.dart';
import '../types.dart';
import 'track.dart'; import 'track.dart';
/// Represents a track that's published to the server. This class contains /// 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 lk_models.TrackType kind;
final TrackSource source; final TrackSource source;
Track? track; Track? _track;
bool muted = false; Track? get track => _track;
// metadata-muted
bool _muted = false;
bool get muted => _muted;
bool simulcasted = false; bool simulcasted = false;
TrackDimension? dimension; TrackDimension? dimension;
bool get subscribed => track != null; bool get subscribed => _track != null;
TrackPublication.fromInfo(lk_models.TrackInfo info) TrackPublication.fromInfo(lk_models.TrackInfo info)
: sid = info.sid, : sid = info.sid,
@@ -36,7 +43,6 @@ abstract class TrackPublication extends Disposable {
kind == lk_models.TrackType.VIDEO && name == Track.screenShareName; kind == lk_models.TrackType.VIDEO && name == Track.screenShareName;
void updateFromInfo(lk_models.TrackInfo info) { void updateFromInfo(lk_models.TrackInfo info) {
muted = info.muted;
simulcasted = info.simulcast; simulcasted = info.simulcast;
if (info.type == lk_models.TrackType.VIDEO) { if (info.type == lk_models.TrackType.VIDEO) {
dimension = TrackDimension(info.width, info.height); dimension = TrackDimension(info.width, info.height);
@@ -51,4 +57,19 @@ abstract class TrackPublication extends Disposable {
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
other is TrackPublication && sid == other.sid; 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<bool> updateTrack(Track? newValue) async {
if (_track == newValue) return false;
// dispose previous track (if exists)
await _track?.dispose();
_track = newValue;
return true;
}
} }
+20 -6
View File
@@ -169,21 +169,35 @@ packages:
name: path_provider name: path_provider
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted 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: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
name: path_provider_linux name: path_provider_linux
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0" version: "2.1.2"
path_provider_macos: path_provider_macos:
dependency: transitive dependency: transitive
description: description:
name: path_provider_macos name: path_provider_macos
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.2" version: "2.0.3"
path_provider_platform_interface: path_provider_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -197,7 +211,7 @@ packages:
name: path_provider_windows name: path_provider_windows
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.3" version: "2.0.4"
platform: platform:
dependency: transitive dependency: transitive
description: description:
@@ -218,7 +232,7 @@ packages:
name: process name: process
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.2.3" version: "4.2.4"
protobuf: protobuf:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -314,7 +328,7 @@ packages:
name: win32 name: win32
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.9" version: "2.3.0"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description: