diff --git a/example/lib/pages/connect.dart b/example/lib/pages/connect.dart index ff0695eef..03041db71 100644 --- a/example/lib/pages/connect.dart +++ b/example/lib/pages/connect.dart @@ -28,6 +28,7 @@ class _ConnectPageState extends State { static const _storeKeyE2EE = 'e2ee'; static const _storeKeySharedKey = 'shared-key'; static const _storeKeyMultiCodec = 'multi-codec'; + static const _storeKeyAutoSubscribe = 'auto-subscribe'; final _uriCtrl = TextEditingController(); final _tokenCtrl = TextEditingController(); @@ -38,6 +39,7 @@ class _ConnectPageState extends State { bool _busy = false; bool _e2ee = false; bool _multiCodec = false; + bool _autoSubscribe = false; String _preferredCodec = 'Preferred Codec'; @override @@ -96,6 +98,7 @@ class _ConnectPageState extends State { _dynacast = prefs.getBool(_storeKeyDynacast) ?? true; _e2ee = prefs.getBool(_storeKeyE2EE) ?? false; _multiCodec = prefs.getBool(_storeKeyMultiCodec) ?? false; + _autoSubscribe = prefs.getBool(_storeKeyAutoSubscribe) ?? true; }); } @@ -110,6 +113,7 @@ class _ConnectPageState extends State { await prefs.setBool(_storeKeyDynacast, _dynacast); await prefs.setBool(_storeKeyE2EE, _e2ee); await prefs.setBool(_storeKeyMultiCodec, _multiCodec); + await prefs.setBool(_storeKeyAutoSubscribe, _autoSubscribe); } Future _connect(BuildContext ctx) async { @@ -139,6 +143,7 @@ class _ConnectPageState extends State { e2ee: _e2ee, e2eeKey: e2eeKey, simulcast: _simulcast, + autoSubscribe: _autoSubscribe, adaptiveStream: _adaptiveStream, dynacast: _dynacast, preferredCodec: _preferredCodec, @@ -164,6 +169,13 @@ class _ConnectPageState extends State { }); } + void _setAutoSubscribe(bool? value) async { + if (value == null || _autoSubscribe == value) return; + setState(() { + _autoSubscribe = value; + }); + } + void _setE2EE(bool? value) async { if (value == null || _e2ee == value) return; setState(() { @@ -260,6 +272,19 @@ class _ConnectPageState extends State { ], ), ), + Padding( + padding: const EdgeInsets.only(bottom: 5), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Auto Subscribe'), + Switch( + value: _autoSubscribe, + onChanged: (value) => _setAutoSubscribe(value), + ), + ], + ), + ), Padding( padding: const EdgeInsets.only(bottom: 5), child: Row( diff --git a/example/lib/pages/prejoin.dart b/example/lib/pages/prejoin.dart index 2b02c44ae..b1dcf59fc 100644 --- a/example/lib/pages/prejoin.dart +++ b/example/lib/pages/prejoin.dart @@ -17,6 +17,7 @@ class JoinArgs { this.e2ee = false, this.e2eeKey, this.simulcast = true, + this.autoSubscribe = true, this.adaptiveStream = true, this.dynacast = true, this.preferredCodec = 'VP8', @@ -27,6 +28,7 @@ class JoinArgs { final bool e2ee; final String? e2eeKey; final bool simulcast; + final bool autoSubscribe; final bool adaptiveStream; final bool dynacast; final String preferredCodec; @@ -209,6 +211,9 @@ class _PreJoinPageState extends State { maxFrameRate: 30, params: _selectedVideoParameters), e2eeOptions: e2eeOptions, ), + connectOptions: ConnectOptions( + autoSubscribe: args.autoSubscribe, + ), fastConnectOptions: FastConnectOptions( microphone: TrackOption(track: _audioTrack), camera: TrackOption(track: _videoTrack), diff --git a/example/lib/pages/room.dart b/example/lib/pages/room.dart index c68094e21..604c10c84 100644 --- a/example/lib/pages/room.dart +++ b/example/lib/pages/room.dart @@ -2,15 +2,17 @@ import 'dart:async'; import 'dart:convert'; import 'dart:math' as math; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:livekit_client/livekit_client.dart'; +import 'package:visibility_detector/visibility_detector.dart'; import 'package:livekit_example/method_channels/replay_kit_channel.dart'; import '../exts.dart'; import '../utils.dart'; import '../widgets/controls.dart'; import '../widgets/participant.dart'; -import '../widgets/participant_info.dart'; +import '../widgets/participant_grid_tile.dart'; class RoomPage extends StatefulWidget { final Room room; @@ -26,14 +28,18 @@ class RoomPage extends StatefulWidget { State createState() => _RoomPageState(); } -class _RoomPageState extends State { - List participantTracks = []; +class _RoomPageState extends State with WidgetsBindingObserver { + List participantTracks = []; EventsListener get _listener => widget.listener; bool get fastConnection => widget.room.engine.fastConnectOptions != null; + bool get autoSubscribe => widget.room.connectOptions.autoSubscribe; + bool gridView = false; bool _flagStartedReplayKit = false; @override void initState() { super.initState(); + WidgetsBinding.instance.addObserver(this); + // add callback for a `RoomEvent` as opposed to a `ParticipantEvent` widget.room.addListener(_onRoomDidUpdate); // add callbacks for finer grained events @@ -73,10 +79,36 @@ class _RoomPageState extends State { await _listener.dispose(); await widget.room.dispose(); })(); + WidgetsBinding.instance.removeObserver(this); onWindowShouldClose = null; super.dispose(); } + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + setState(() {}); + if (autoSubscribe) return; + if (state == AppLifecycleState.resumed) { + for (var p in participantTracks) { + if (p.hasVideo && participantSubscriptions.containsKey(p.identity)) { + (p as RemoteParticipant) + .videoTrackPublications + .firstOrNull + ?.subscribe(); + } + } + } else if (state == AppLifecycleState.paused) { + for (var p in participantTracks) { + if (p.hasVideo && participantSubscriptions.containsKey(p.identity)) { + (p as RemoteParticipant) + .videoTrackPublications + .firstOrNull + ?.unsubscribe(); + } + } + } + } + /// for more information, see [event types](https://docs.livekit.io/client/events/#events) void _setUpListeners() => _listener ..on((event) async { @@ -103,6 +135,12 @@ class _RoomPageState extends State { ..on((_) => _sortParticipants()) ..on((_) => _sortParticipants()) ..on((_) => _sortParticipants()) + ..on((event) { + print('Participant connected: ${event.participant.identity}'); + }) + ..on((event) { + print('Participant disconnected: ${event.participant.identity}'); + }) ..on(_onE2EEStateEvent) ..on((event) { print( @@ -162,30 +200,15 @@ class _RoomPageState extends State { } void _sortParticipants() { - List userMediaTracks = []; - List screenTracks = []; + List sortedParticipants = []; for (var participant in widget.room.remoteParticipants.values) { - for (var t in participant.videoTrackPublications) { - if (t.isScreenShare) { - screenTracks.add(ParticipantTrack( - participant: participant, - videoTrack: t.track, - isScreenShare: true, - )); - } else { - userMediaTracks.add(ParticipantTrack( - participant: participant, - videoTrack: t.track, - isScreenShare: false, - )); - } - } + sortedParticipants.add(participant); } // sort speakers for the grid - userMediaTracks.sort((a, b) { + sortedParticipants.sort((a, b) { // loudest speaker first - if (a.participant.isSpeaking && b.participant.isSpeaking) { - if (a.participant.audioLevel > b.participant.audioLevel) { + if (a.isSpeaking && b.isSpeaking) { + if (a.audioLevel > b.audioLevel) { return -1; } else { return 1; @@ -193,98 +216,248 @@ class _RoomPageState extends State { } // last spoken at - final aSpokeAt = a.participant.lastSpokeAt?.millisecondsSinceEpoch ?? 0; - final bSpokeAt = b.participant.lastSpokeAt?.millisecondsSinceEpoch ?? 0; + final aSpokeAt = a.lastSpokeAt?.millisecondsSinceEpoch ?? 0; + final bSpokeAt = b.lastSpokeAt?.millisecondsSinceEpoch ?? 0; if (aSpokeAt != bSpokeAt) { return aSpokeAt > bSpokeAt ? -1 : 1; } // video on - if (a.participant.hasVideo != b.participant.hasVideo) { - return a.participant.hasVideo ? -1 : 1; + if (a.hasVideo != b.hasVideo) { + return a.hasVideo ? -1 : 1; } // joinedAt - return a.participant.joinedAt.millisecondsSinceEpoch - - b.participant.joinedAt.millisecondsSinceEpoch; + return a.joinedAt.millisecondsSinceEpoch - + b.joinedAt.millisecondsSinceEpoch; }); - final localParticipantTracks = - widget.room.localParticipant?.videoTrackPublications; - if (localParticipantTracks != null) { - for (var t in localParticipantTracks) { - if (t.isScreenShare) { - if (lkPlatformIs(PlatformType.iOS)) { - if (!_flagStartedReplayKit) { - _flagStartedReplayKit = true; + if (widget.room.localParticipant != null) { + sortedParticipants.add(widget.room.localParticipant!); + final localParticipantTracks = + widget.room.localParticipant?.videoTrackPublications; + if (localParticipantTracks != null) { + for (var t in localParticipantTracks) { + if (t.isScreenShare) { + if (lkPlatformIs(PlatformType.iOS)) { + if (!_flagStartedReplayKit) { + _flagStartedReplayKit = true; - ReplayKitChannel.startReplayKit(); - } - } - screenTracks.add(ParticipantTrack( - participant: widget.room.localParticipant!, - videoTrack: t.track, - isScreenShare: true, - )); - } else { - if (lkPlatformIs(PlatformType.iOS)) { - if (_flagStartedReplayKit) { - _flagStartedReplayKit = false; + ReplayKitChannel.startReplayKit(); + } + } else { + if (lkPlatformIs(PlatformType.iOS)) { + if (_flagStartedReplayKit) { + _flagStartedReplayKit = false; - ReplayKitChannel.closeReplayKit(); + ReplayKitChannel.closeReplayKit(); + } + } } } - - userMediaTracks.add(ParticipantTrack( - participant: widget.room.localParticipant!, - videoTrack: t.track, - isScreenShare: false, - )); } } } + setState(() { - participantTracks = [...screenTracks, ...userMediaTracks]; + participantTracks = [ + ...sortedParticipants, + ]; }); } + void subscribeToVideoTracks(RemoteParticipant participant) async { + if (participantSubscriptions[participant.identity] == true) { + return; + } + participantSubscriptions[participant.identity] = true; + await participant.videoTrackPublications.firstOrNull?.subscribe(); + } + + void unSubscribeToVideoTracks(RemoteParticipant participant) async { + if (participantSubscriptions[participant.identity] == false) { + return; + } + participantSubscriptions[participant.identity] = false; + await participant.videoTrackPublications.firstOrNull?.unsubscribe(); + } + + Map visibleParticipants = {}; + Map participantSubscriptions = {}; + + Widget _widgetForParticipant(int index) { + final participant = participantTracks[index]; + return VisibilityDetector( + key: Key(participant.identity), + onVisibilityChanged: (info) { + if (autoSubscribe) return; + final bool isVisible = info.visibleFraction > 0; + final bool isCompletelyGone = info.visibleFraction == 0; + final bool isSubscribed = + participantSubscriptions[participant.identity] ?? false; + final bool shouldSubscribe = + !isSubscribed && isVisible && participant is! LocalParticipant; + + if (shouldSubscribe) { + subscribeToVideoTracks( + participant as RemoteParticipant, + ); + visibleParticipants[participant.identity] = true; + } else if (participant is! LocalParticipant && isCompletelyGone) { + unSubscribeToVideoTracks( + participant as RemoteParticipant, + ); + visibleParticipants.remove( + participant.identity, + ); + } + }, + child: SizedBox( + width: 240, + height: 180, + child: ParticipantGridTile( + participantTracks[index], + participantSubscriptions, + ), + ), + ); + } + @override Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: Text( + 'Room: ${widget.room.name}', + style: const TextStyle(color: Colors.white), + ), + actions: [ + IconButton( + icon: Icon( + Icons.view_module, + color: gridView ? Colors.green : Colors.white, + ), + onPressed: () { + setState(() { + gridView = true; + }); + }, + ), + IconButton( + icon: Icon( + Icons.view_sidebar, + color: !gridView ? Colors.green : Colors.white, + ), + onPressed: () { + setState(() { + gridView = false; + }); + }, + ), + SizedBox.fromSize( + size: const Size(42, 10), + ) + ], + ), body: Stack( children: [ - Column( - children: [ - Expanded( - child: participantTracks.isNotEmpty - ? ParticipantWidget.widgetFor(participantTracks.first, - showStatsLayer: true) - : Container()), - if (widget.room.localParticipant != null) - SafeArea( - top: false, - child: ControlsWidget( - widget.room, widget.room.localParticipant!), - ) - ], - ), - Positioned( + if (gridView) + CustomScrollView( + slivers: [ + SliverGrid( + gridDelegate: + const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 300.0, + mainAxisSpacing: 10.0, + crossAxisSpacing: 10.0, + childAspectRatio: 1.5, + ), + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) => + _widgetForParticipant(index), + childCount: participantTracks.length, + ), + ), + ], + ), + if (widget.room.localParticipant != null && gridView) + Positioned( left: 0, right: 0, - top: 0, - child: SizedBox( - height: 120, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: math.max(0, participantTracks.length - 1), - itemBuilder: (BuildContext context, int index) => SizedBox( - width: 180, - height: 120, - child: ParticipantWidget.widgetFor( - participantTracks[index + 1]), + bottom: 20, + child: SafeArea( + top: false, + child: ControlsWidget( + widget.room, widget.room.localParticipant!), + ), + ), + if (!gridView) + Column( + children: [ + Expanded( + child: participantTracks.isNotEmpty + ? VisibilityDetector( + key: Key(participantTracks.first.identity), + onVisibilityChanged: (info) { + if (autoSubscribe) return; + final bool isVisible = info.visibleFraction > 0; + final bool isCompletelyGone = + info.visibleFraction == 0; + final bool isSubscribed = + participantSubscriptions[ + participantTracks.first.identity] ?? + false; + final bool shouldSubscribe = !isSubscribed && + isVisible && + participantTracks.first + is! LocalParticipant; + + if (shouldSubscribe) { + subscribeToVideoTracks( + participantTracks.first + as RemoteParticipant, + ); + visibleParticipants[ + participantTracks.first.identity] = true; + } else if (participantTracks.first + is! LocalParticipant && + isCompletelyGone) { + unSubscribeToVideoTracks( + participantTracks.first + as RemoteParticipant, + ); + visibleParticipants.remove( + participantTracks.first.identity, + ); + } + }, + child: ParticipantWidget.widgetFor( + participantTracks.first, + showStatsLayer: true), + ) + : Container()), + if (widget.room.localParticipant != null) + SafeArea( + top: false, + child: ControlsWidget( + widget.room, widget.room.localParticipant!), + ) + ], + ), + if (!gridView) + Positioned( + left: 0, + right: 0, + top: 0, + child: SizedBox( + height: 120, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: math.max(0, participantTracks.length - 1), + itemBuilder: (BuildContext context, int index) => + _widgetForParticipant(index + 1), ), - ), - )), + )), ], ), ); diff --git a/example/lib/widgets/participant.dart b/example/lib/widgets/participant.dart index bb41dba3d..6a49f7d6f 100644 --- a/example/lib/widgets/participant.dart +++ b/example/lib/widgets/participant.dart @@ -10,28 +10,20 @@ import 'participant_stats.dart'; abstract class ParticipantWidget extends StatefulWidget { // Convenience method to return relevant widget for participant - static ParticipantWidget widgetFor(ParticipantTrack participantTrack, + static ParticipantWidget widgetFor(Participant participant, {bool showStatsLayer = false}) { - if (participantTrack.participant is LocalParticipant) { - return LocalParticipantWidget( - participantTrack.participant as LocalParticipant, - participantTrack.videoTrack, - participantTrack.isScreenShare, - showStatsLayer); - } else if (participantTrack.participant is RemoteParticipant) { - return RemoteParticipantWidget( - participantTrack.participant as RemoteParticipant, - participantTrack.videoTrack, - participantTrack.isScreenShare, - showStatsLayer); + if (participant is LocalParticipant) { + return LocalParticipantWidget(participant, showStatsLayer); + } else if (participant is RemoteParticipant) { + return RemoteParticipantWidget(participant, showStatsLayer); } throw UnimplementedError('Unknown participant type'); } // Must be implemented by child class abstract final Participant participant; - abstract final VideoTrack? videoTrack; - abstract final bool isScreenShare; + VideoTrack? get videoTrack; + bool get isScreenShare; abstract final bool showStatsLayer; final VideoQuality quality; @@ -45,16 +37,16 @@ class LocalParticipantWidget extends ParticipantWidget { @override final LocalParticipant participant; @override - final VideoTrack? videoTrack; + VideoTrack? get videoTrack => + participant.videoTrackPublications.firstOrNull?.track; @override - final bool isScreenShare; + bool get isScreenShare => + participant.videoTrackPublications.firstOrNull?.isScreenShare ?? false; @override final bool showStatsLayer; const LocalParticipantWidget( this.participant, - this.videoTrack, - this.isScreenShare, this.showStatsLayer, { Key? key, }) : super(key: key); @@ -67,16 +59,16 @@ class RemoteParticipantWidget extends ParticipantWidget { @override final RemoteParticipant participant; @override - final VideoTrack? videoTrack; + VideoTrack? get videoTrack => + participant.videoTrackPublications.firstOrNull?.track; @override - final bool isScreenShare; + bool get isScreenShare => + participant.videoTrackPublications.firstOrNull?.isScreenShare ?? false; @override final bool showStatsLayer; const RemoteParticipantWidget( this.participant, - this.videoTrack, - this.isScreenShare, this.showStatsLayer, { Key? key, }) : super(key: key); @@ -139,7 +131,8 @@ abstract class _ParticipantWidgetState // Video InkWell( onTap: () => setState(() => _visible = !_visible), - child: activeVideoTrack != null && !activeVideoTrack!.muted + child: (activeVideoTrack != null && !activeVideoTrack!.muted) && + widget.participant.isCameraEnabled() ? VideoTrackRenderer( activeVideoTrack!, fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain, @@ -217,15 +210,17 @@ class _RemoteParticipantWidgetState mainAxisAlignment: MainAxisAlignment.end, children: [ // Menu for RemoteTrackPublication - if (firstAudioPublication != null && !isScreenShare) + if (widget.participant.audioTrackPublications.isNotEmpty) RemoteTrackPublicationMenuWidget( - pub: firstAudioPublication!, + remote: widget.participant, + source: TrackSource.microphone, icon: Icons.volume_up, ), // Menu for RemoteTrackPublication - if (videoPublication != null) + if (widget.participant.videoTrackPublications.isNotEmpty) RemoteTrackPublicationMenuWidget( - pub: videoPublication!, + remote: widget.participant, + source: TrackSource.camera, icon: isScreenShare ? Icons.monitor : Icons.videocam, ), if (videoPublication != null) @@ -245,13 +240,20 @@ class _RemoteParticipantWidgetState class RemoteTrackPublicationMenuWidget extends StatelessWidget { final IconData icon; - final RemoteTrackPublication pub; + final RemoteParticipant remote; + final TrackSource source; const RemoteTrackPublicationMenuWidget({ - required this.pub, + required this.remote, + required this.source, required this.icon, Key? key, }) : super(key: key); + List get publications => + source == TrackSource.microphone + ? remote.audioTrackPublications + : remote.videoTrackPublications; + @override Widget build(BuildContext context) => Material( color: Colors.black.withOpacity(0.3), @@ -259,22 +261,24 @@ class RemoteTrackPublicationMenuWidget extends StatelessWidget { tooltip: 'Subscribe menu', icon: Icon(icon, color: { - TrackSubscriptionState.notAllowed: Colors.red, - TrackSubscriptionState.unsubscribed: Colors.grey, - TrackSubscriptionState.subscribed: Colors.green, - }[pub.subscriptionState]), + TrackSubscriptionState.notAllowed: Colors.red, + TrackSubscriptionState.unsubscribed: Colors.grey, + TrackSubscriptionState.subscribed: Colors.green, + }[publications.firstOrNull?.subscriptionState] ?? + Colors.white), onSelected: (value) => value(), itemBuilder: (BuildContext context) => >[ // Subscribe/Unsubscribe - if (pub.subscribed == false) + if (publications.isNotEmpty && + publications.firstOrNull?.subscribed == false) PopupMenuItem( child: const Text('Subscribe'), - value: () => pub.subscribe(), + value: () => publications.firstOrNull?.subscribe(), ) - else if (pub.subscribed == true) + else if (publications.firstOrNull?.subscribed ?? false) PopupMenuItem( child: const Text('Un-subscribe'), - value: () => pub.unsubscribe(), + value: () => publications.firstOrNull?.unsubscribe(), ), ], ), diff --git a/example/lib/widgets/participant_grid_tile.dart b/example/lib/widgets/participant_grid_tile.dart new file mode 100644 index 000000000..63e83afb2 --- /dev/null +++ b/example/lib/widgets/participant_grid_tile.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:livekit_client/livekit_client.dart'; + +import 'participant.dart'; + +extension ParticipantExtension on Participant { + bool get isLocalParticipant => this is LocalParticipant; + bool get isRemoteParticipant => this is RemoteParticipant; +} + +class ParticipantGridTile extends StatefulWidget { + final Participant participant; + final Map participantSubscriptions; + const ParticipantGridTile( + this.participant, + this.participantSubscriptions, { + Key? key, + }) : super(key: key); + + @override + State createState() => _ParticipantGridTile(); +} + +class _ParticipantGridTile extends State { + String get name => isLocalParticipant + ? '${widget.participant.name} (you)' + : widget.participant.name; + + bool get isLocalParticipant => widget.participant.isLocalParticipant; + + @override + Widget build(BuildContext context) { + /* + bool hasVideo = false; + + if (!isLocalParticipant) { + hasVideo = + (widget.participantSubscriptions[widget.participant.identity] ?? + false) && + widget.participant.isCameraEnabled() && + widget.participant.videoTrackPublications.isNotEmpty && + widget.participant.videoTrackPublications[0].track != null; + } else if (isLocalParticipant && + widget.participant.isCameraEnabled() && + widget.participant.videoTrackPublications.isNotEmpty) { + hasVideo = true; + } + */ + return Stack( + children: [ + ClipRRect( + //borderRadius: BorderRadius.circular(AppSpacing.radiixSmall), + child: Stack( + children: [ + // Your video renderer + DecoratedBox( + decoration: BoxDecoration( + //borderRadius: BorderRadius.circular(AppSpacing.radiixSmall), + border: Border.all( + color: Colors.white, + width: 2, + )), + position: DecorationPosition.foreground, + child: ParticipantWidget.widgetFor(widget.participant), + ), + /*Material( + type: MaterialType.transparency, + child: InkWell( + splashColor: Colors.blueGrey, + onTap: () { + if (lkPlatformIsMobile()) { + Fluttertoast.showToast( + msg: + 'participant.isCameraEnabled() = ${widget.participant.isCameraEnabled()}, hasVideo = $hasVideo', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + timeInSecForIosWeb: 1, + backgroundColor: Colors.orange, + textColor: Colors.white, + fontSize: 16.0); + } else { + print( + 'participant.isCameraEnabled() = ${widget.participant.isCameraEnabled()}, hasVideo = $hasVideo'); + } + }, + child: Container( + width: double.infinity, + height: double.infinity, + color: const Color.fromARGB(0, 0, 0, 0), + ), + ), + ),*/ + ], + ), + ) + ], + ); + } +} diff --git a/example/lib/widgets/participant_info.dart b/example/lib/widgets/participant_info.dart index fc11603a7..c26d1d060 100644 --- a/example/lib/widgets/participant_info.dart +++ b/example/lib/widgets/participant_info.dart @@ -1,16 +1,6 @@ import 'package:flutter/material.dart'; import 'package:livekit_client/livekit_client.dart'; -class ParticipantTrack { - ParticipantTrack( - {required this.participant, - required this.videoTrack, - required this.isScreenShare}); - VideoTrack? videoTrack; - Participant participant; - final bool isScreenShare; -} - class ParticipantInfoWidget extends StatelessWidget { // final String? title; diff --git a/example/pubspec.yaml b/example/pubspec.yaml index c2e54f653..293721d78 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -21,6 +21,8 @@ dependencies: flutter_svg: ^2.0.5 dropdown_button2: ^2.3.6 flutter_window_close: ^0.2.2 + fluttertoast: ^8.2.4 + visibility_detector: ^0.4.0+2 collection: '>=1.16.0' livekit_client: diff --git a/lib/src/core/room.dart b/lib/src/core/room.dart index f159bd859..6439698c3 100644 --- a/lib/src/core/room.dart +++ b/lib/src/core/room.dart @@ -701,6 +701,8 @@ class Room extends DisposableChangeNotifier with EventsEmittable { return; } + _sidToIdentity.removeWhere((key, value) => value == identity); + await participant.removeAllPublishedTracks(notify: true); emitWhenConnected(ParticipantDisconnectedEvent(participant: participant));