11import 'package:collection/collection.dart' ;
2+ import 'package:eva_icons_flutter/eva_icons_flutter.dart' ;
23import 'package:flutter/material.dart' ;
34import 'package:flutter_webrtc/flutter_webrtc.dart' ;
45import 'package:livekit_client/livekit_client.dart' ;
@@ -23,8 +24,8 @@ class ParticipantWidget extends StatefulWidget {
2324
2425class _ParticipantWidgetState extends State <ParticipantWidget > {
2526 //
26- TrackPublication ? videoPub ;
27- TrackPublication ? audioPub ;
27+ TrackPublication ? firstVideoPub ;
28+ TrackPublication ? firstAudioPub ;
2829
2930 @override
3031 void initState () {
@@ -50,18 +51,14 @@ class _ParticipantWidgetState extends State<ParticipantWidget> {
5051 // register for change so Flutter will re-build the widget upon change
5152 void _onParticipantChanged () {
5253 //
53- final firstAudio = widget.participant.audioTracks
54- .firstWhereOrNull ((pub) => pub.subscribed);
55- final firstVideo = widget.participant.videoTracks
56- .firstWhereOrNull ((pub) => ! pub.isScreenShare && pub.subscribed);
57-
58- if (firstVideo is RemoteTrackPublication ) {
59- firstVideo.videoQuality = widget.quality;
60- }
61-
6254 setState (() {
63- audioPub = ! (firstAudio? .muted ?? true ) ? firstAudio : null ;
64- videoPub = ! (firstVideo? .muted ?? true ) ? firstVideo : null ;
55+ // For simplification, We are assuming here
56+ // there is only 1 video / audio tracks.
57+ firstAudioPub = widget.participant.audioTracks.firstOrNull;
58+ firstVideoPub = widget.participant.videoTracks.firstOrNull;
59+ if (firstVideoPub is RemoteTrackPublication ) {
60+ (firstVideoPub as RemoteTrackPublication ).videoQuality = widget.quality;
61+ }
6562 });
6663 }
6764
@@ -71,22 +68,105 @@ class _ParticipantWidgetState extends State<ParticipantWidget> {
7168 child: Stack (
7269 children: [
7370 // Video
74- if (videoPub != null )
71+ if (firstVideoPub? .subscribed == true &&
72+ firstVideoPub? .muted == false )
7573 VideoTrackRenderer (
76- videoPub ! .track as VideoTrack ,
74+ firstVideoPub ! .track as VideoTrack ,
7775 fit: RTCVideoViewObjectFit .RTCVideoViewObjectFitCover ,
7876 )
7977 else
8078 const NoVideoWidget (),
8179
8280 Align (
8381 alignment: Alignment .bottomCenter,
84- child: ParticipantInfoWidget (
85- title: widget.participant.identity,
86- muted: audioPub == null ,
82+ child: Column (
83+ crossAxisAlignment: CrossAxisAlignment .stretch,
84+ mainAxisSize: MainAxisSize .min,
85+ children: [
86+ //
87+ // Menu for Video RemoteTrackPublication
88+ //
89+ Row (
90+ mainAxisSize: MainAxisSize .max,
91+ mainAxisAlignment: MainAxisAlignment .end,
92+ children: [
93+ if (firstVideoPub is RemoteTrackPublication )
94+ RemoteTrackPublicationMenuWidget (
95+ pub: firstVideoPub as RemoteTrackPublication ,
96+ icon: EvaIcons .videoOutline,
97+ ),
98+ //
99+ // Menu for Audio RemoteTrackPublication
100+ //
101+ if (firstAudioPub is RemoteTrackPublication )
102+ RemoteTrackPublicationMenuWidget (
103+ pub: firstAudioPub as RemoteTrackPublication ,
104+ icon: EvaIcons .volumeUpOutline,
105+ ),
106+ ],
107+ ),
108+
109+ ParticipantInfoWidget (
110+ title: widget.participant.identity,
111+ audioAvailable: firstAudioPub? .muted == false &&
112+ firstAudioPub? .subscribed == true ,
113+ ),
114+ ],
87115 ),
88116 ),
89117 ],
90118 ),
91119 );
92120}
121+
122+ class RemoteTrackPublicationMenuWidget extends StatelessWidget {
123+ final IconData icon;
124+ final RemoteTrackPublication pub;
125+ const RemoteTrackPublicationMenuWidget ({
126+ required this .pub,
127+ required this .icon,
128+ Key ? key,
129+ }) : super (key: key);
130+
131+ @override
132+ Widget build (BuildContext context) => Material (
133+ // type: MaterialType.card,
134+ color: Colors .black.withOpacity (0.3 ),
135+ // shape: CircleBorder(),
136+ child: PopupMenuButton <Function >(
137+ // shape: CircleBorder(),
138+ icon: Icon (icon),
139+ onSelected: (value) => value (),
140+ itemBuilder: (BuildContext context) {
141+ return < PopupMenuEntry <Function >> [
142+ //
143+ // Mute/Unmute
144+ //
145+ if (pub.muted == false )
146+ PopupMenuItem (
147+ child: const Text ('Mute' ),
148+ value: () => pub.muted = true ,
149+ ),
150+ if (pub.muted == true )
151+ PopupMenuItem (
152+ child: const Text ('Un-mute' ),
153+ value: () => pub.muted = false ,
154+ ),
155+ //
156+ // Subscribe/Unsubscribe
157+ //
158+ if (pub.subscribed == false )
159+ PopupMenuItem (
160+ child: const Text ('Subscribe' ),
161+ value: () => pub.subscribed = true ,
162+ ),
163+ if (pub.subscribed == true )
164+ PopupMenuItem (
165+ child: const Text ('Un-subscribe' ),
166+ value: () => pub.subscribed = false ,
167+ ),
168+ ];
169+ },
170+ ),
171+ );
172+ }
0 commit comments