diff --git a/example/lib/pages/room.dart b/example/lib/pages/room.dart index 77f8d9d4c..6560d322a 100644 --- a/example/lib/pages/room.dart +++ b/example/lib/pages/room.dart @@ -71,13 +71,7 @@ class _RoomPageState extends State { // Create video track final localVideo = await LocalVideoTrack.createCameraTrack(); // Try to publish the video - await widget.room.localParticipant.publishVideoTrack( - localVideo, - // options: TrackPublishOptions( - // // simulcast: true, - // videoEncoding: VideoParameters.presetQVGA169.encoding, - // ), - ); + await widget.room.localParticipant.publishVideoTrack(localVideo); // Create mic track final localAudio = await LocalAudioTrack.create(); diff --git a/example/lib/widgets/participant.dart b/example/lib/widgets/participant.dart index cf93d5535..c0f216be1 100644 --- a/example/lib/widgets/participant.dart +++ b/example/lib/widgets/participant.dart @@ -1,4 +1,5 @@ import 'package:collection/collection.dart'; +import 'package:eva_icons_flutter/eva_icons_flutter.dart'; import 'package:flutter/material.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:livekit_client/livekit_client.dart'; @@ -23,8 +24,8 @@ class ParticipantWidget extends StatefulWidget { class _ParticipantWidgetState extends State { // - TrackPublication? videoPub; - TrackPublication? audioPub; + TrackPublication? firstVideoPub; + TrackPublication? firstAudioPub; @override void initState() { @@ -50,18 +51,14 @@ class _ParticipantWidgetState extends State { // register for change so Flutter will re-build the widget upon change void _onParticipantChanged() { // - final firstAudio = widget.participant.audioTracks - .firstWhereOrNull((pub) => pub.subscribed); - final firstVideo = widget.participant.videoTracks - .firstWhereOrNull((pub) => !pub.isScreenShare && pub.subscribed); - - if (firstVideo is RemoteTrackPublication) { - firstVideo.videoQuality = widget.quality; - } - setState(() { - audioPub = !(firstAudio?.muted ?? true) ? firstAudio : null; - videoPub = !(firstVideo?.muted ?? true) ? firstVideo : null; + // For simplification, We are assuming here + // there is only 1 video / audio tracks. + firstAudioPub = widget.participant.audioTracks.firstOrNull; + firstVideoPub = widget.participant.videoTracks.firstOrNull; + if (firstVideoPub is RemoteTrackPublication) { + (firstVideoPub as RemoteTrackPublication).videoQuality = widget.quality; + } }); } @@ -71,9 +68,10 @@ class _ParticipantWidgetState extends State { child: Stack( children: [ // Video - if (videoPub != null) + if (firstVideoPub?.subscribed == true && + firstVideoPub?.muted == false) VideoTrackRenderer( - videoPub!.track as VideoTrack, + firstVideoPub!.track as VideoTrack, fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, ) else @@ -81,12 +79,94 @@ class _ParticipantWidgetState extends State { Align( alignment: Alignment.bottomCenter, - child: ParticipantInfoWidget( - title: widget.participant.identity, - muted: audioPub == null, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + // + // Menu for Video RemoteTrackPublication + // + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (firstVideoPub is RemoteTrackPublication) + RemoteTrackPublicationMenuWidget( + pub: firstVideoPub as RemoteTrackPublication, + icon: EvaIcons.videoOutline, + ), + // + // Menu for Audio RemoteTrackPublication + // + if (firstAudioPub is RemoteTrackPublication) + RemoteTrackPublicationMenuWidget( + pub: firstAudioPub as RemoteTrackPublication, + icon: EvaIcons.volumeUpOutline, + ), + ], + ), + + ParticipantInfoWidget( + title: widget.participant.identity, + audioAvailable: firstAudioPub?.muted == false && + firstAudioPub?.subscribed == true, + ), + ], ), ), ], ), ); } + +class RemoteTrackPublicationMenuWidget extends StatelessWidget { + final IconData icon; + final RemoteTrackPublication pub; + const RemoteTrackPublicationMenuWidget({ + required this.pub, + required this.icon, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) => Material( + // type: MaterialType.card, + color: Colors.black.withOpacity(0.3), + // shape: CircleBorder(), + child: PopupMenuButton( + // shape: CircleBorder(), + icon: Icon(icon), + onSelected: (value) => value(), + itemBuilder: (BuildContext context) { + return >[ + // + // Mute/Unmute + // + if (pub.muted == false) + PopupMenuItem( + child: const Text('Mute'), + value: () => pub.muted = true, + ), + if (pub.muted == true) + PopupMenuItem( + child: const Text('Un-mute'), + value: () => pub.muted = false, + ), + // + // Subscribe/Unsubscribe + // + if (pub.subscribed == false) + PopupMenuItem( + child: const Text('Subscribe'), + value: () => pub.subscribed = true, + ), + if (pub.subscribed == true) + PopupMenuItem( + child: const Text('Un-subscribe'), + value: () => pub.subscribed = false, + ), + ]; + }, + ), + ); +} diff --git a/example/lib/widgets/participant_info.dart b/example/lib/widgets/participant_info.dart index d1354d3ec..403f11e11 100644 --- a/example/lib/widgets/participant_info.dart +++ b/example/lib/widgets/participant_info.dart @@ -4,11 +4,11 @@ import 'package:flutter/material.dart'; class ParticipantInfoWidget extends StatelessWidget { // final String? title; - final bool muted; + final bool audioAvailable; const ParticipantInfoWidget({ this.title, - this.muted = true, + this.audioAvailable = true, Key? key, }) : super(key: key); @@ -33,8 +33,8 @@ class ParticipantInfoWidget extends StatelessWidget { Padding( padding: const EdgeInsets.only(left: 5), child: Icon( - !muted ? EvaIcons.mic : EvaIcons.micOff, - color: !muted ? Colors.white : Colors.red, + audioAvailable ? EvaIcons.mic : EvaIcons.micOff, + color: audioAvailable ? Colors.white : Colors.red, size: 16, ), ),