diff --git a/lib/providers/chat_call.dart b/lib/providers/chat_call.dart index 93971fc..842912c 100644 --- a/lib/providers/chat_call.dart +++ b/lib/providers/chat_call.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:livekit_client/livekit_client.dart'; +import 'package:livekit_noise_filter/livekit_noise_filter.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; import 'package:surface/providers/sn_network.dart'; @@ -132,9 +133,12 @@ class ChatCallProvider extends ChangeNotifier { void initRoom() { initHardware(); _room = Room( - roomOptions: const RoomOptions( + roomOptions: RoomOptions( dynacast: true, adaptiveStream: true, + defaultAudioCaptureOptions: AudioCaptureOptions( + processor: LiveKitNoiseFilter(), + ), defaultAudioPublishOptions: AudioPublishOptions( name: 'call_voice', stream: 'call_stream', diff --git a/lib/screens/chat/call_room.dart b/lib/screens/chat/call_room.dart index f774cd9..6042557 100644 --- a/lib/screens/chat/call_room.dart +++ b/lib/screens/chat/call_room.dart @@ -32,7 +32,7 @@ class _CallRoomScreenState extends State { } } - Widget _buildListLayout() { + Widget _buildMeetLayout() { final call = context.read(); return Stack( children: [ @@ -41,9 +41,7 @@ class _CallRoomScreenState extends State { Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75), child: call.focusTrack != null ? InteractiveParticipantWidget( - isFixedAvatar: false, participant: call.focusTrack!, - onTap: () {}, ) : const SizedBox.shrink(), ), @@ -62,23 +60,18 @@ class _CallRoomScreenState extends State { return Container(); } - return Padding( - padding: const EdgeInsets.only(top: 8, left: 8), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(8)), - child: InteractiveParticipantWidget( - isFixedAvatar: true, - width: 120, - height: 120, - color: Theme.of(context).cardColor, - participant: track, - onTap: () { - if (track.participant.sid != - call.focusTrack?.participant.sid) { - call.setFocusTrack(track); - } - }, - ), + return SizedBox( + height: 128, + width: 128, + child: InteractiveParticipantWidget( + participant: track, + avatarSize: 32, + onTap: () { + if (track.participant.sid != + call.focusTrack?.participant.sid) { + call.setFocusTrack(track); + } + }, ), ); }, @@ -89,50 +82,26 @@ class _CallRoomScreenState extends State { ); } - Widget _buildGridLayout() { + Widget _buildListLayout() { final call = context.read(); - return LayoutBuilder(builder: (context, constraints) { - double screenWidth = constraints.maxWidth; - double screenHeight = constraints.maxHeight; - - int columns = (math.sqrt(call.participantTracks.length)).ceil(); - int rows = (call.participantTracks.length / columns).ceil(); - - double tileWidth = screenWidth / columns; - double tileHeight = screenHeight / rows; - - return StyledWidget(GridView.builder( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: columns, - childAspectRatio: tileWidth / tileHeight, - crossAxisSpacing: 8, - mainAxisSpacing: 8, - ), - itemCount: math.max(0, call.participantTracks.length), - itemBuilder: (BuildContext context, int index) { - final track = call.participantTracks[index]; - return Card( - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(8)), - child: InteractiveParticipantWidget( - color: Theme.of(context) - .colorScheme - .surfaceContainerHigh - .withOpacity(0.75), - participant: track, - onTap: () { - if (track.participant.sid != - call.focusTrack?.participant.sid) { - call.setFocusTrack(track); - } - }, - ), - ), - ); - }, - )).padding(all: 8); - }); + return LayoutBuilder( + builder: (context, constraints) { + return ListView.builder( + padding: EdgeInsets.zero, + itemCount: math.max(0, call.participantTracks.length), + itemBuilder: (BuildContext context, int index) { + final track = call.participantTracks[index]; + return InteractiveParticipantWidget( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + isList: true, + avatarSize: 24, + participant: track, + ); + }, + ); + }, + ); } @override @@ -176,131 +145,129 @@ class _CallRoomScreenState extends State { ]), ), ), - body: GestureDetector( - behavior: HitTestBehavior.translucent, - child: Column( - children: [ + body: Column( + children: [ + SizedBox( + width: MediaQuery.of(context).size.width, + height: 64, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Builder(builder: (context) { + final call = context.read(); + final connectionQuality = + call.room.localParticipant?.connectionQuality ?? + livekit.ConnectionQuality.unknown; + return Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + call.channel?.name ?? 'unknown'.tr(), + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + const Gap(6), + Text(call.lastDuration.toString()) + ], + ), + Row( + children: [ + Text( + { + livekit.ConnectionState.disconnected: + 'callStatusDisconnected'.tr(), + livekit.ConnectionState.connected: + 'callStatusConnected'.tr(), + livekit.ConnectionState.connecting: + 'callStatusConnecting'.tr(), + livekit.ConnectionState.reconnecting: + 'callStatusReconnecting'.tr(), + }[call.room.connectionState]!, + ), + const Gap(6), + if (connectionQuality != + livekit.ConnectionQuality.unknown) + Icon( + { + livekit.ConnectionQuality.excellent: + Icons.signal_cellular_alt, + livekit.ConnectionQuality.good: + Icons.signal_cellular_alt_2_bar, + livekit.ConnectionQuality.poor: + Icons.signal_cellular_alt_1_bar, + }[connectionQuality], + color: { + livekit.ConnectionQuality.excellent: + Colors.green, + livekit.ConnectionQuality.good: + Colors.orange, + livekit.ConnectionQuality.poor: + Colors.red, + }[connectionQuality], + size: 16, + ) + else + const SizedBox( + width: 12, + height: 12, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + padding: EdgeInsets.zero, + ), + ).padding(all: 3), + ], + ), + ], + ), + ); + }), + Row( + children: [ + IconButton( + icon: _layoutMode == 0 + ? const Icon(Icons.view_list) + : const Icon(Icons.grid_view), + onPressed: () { + _switchLayout(); + }, + ), + ], + ), + ], + ).padding(left: 20, right: 16), + ), + Expanded( + child: Material( + color: Theme.of(context).colorScheme.surfaceContainerLow, + child: Builder( + builder: (context) { + switch (_layoutMode) { + case 1: + return _buildListLayout(); + default: + return _buildMeetLayout(); + } + }, + ), + ), + ), + if (call.room.localParticipant != null) SizedBox( width: MediaQuery.of(context).size.width, - height: 64, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Builder(builder: (context) { - final call = context.read(); - final connectionQuality = - call.room.localParticipant?.connectionQuality ?? - livekit.ConnectionQuality.unknown; - return Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Text( - call.channel?.name ?? 'unknown'.tr(), - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - const Gap(6), - Text(call.lastDuration.toString()) - ], - ), - Row( - children: [ - Text( - { - livekit.ConnectionState.disconnected: - 'callStatusDisconnected'.tr(), - livekit.ConnectionState.connected: - 'callStatusConnected'.tr(), - livekit.ConnectionState.connecting: - 'callStatusConnecting'.tr(), - livekit.ConnectionState.reconnecting: - 'callStatusReconnecting'.tr(), - }[call.room.connectionState]!, - ), - const Gap(6), - if (connectionQuality != - livekit.ConnectionQuality.unknown) - Icon( - { - livekit.ConnectionQuality.excellent: - Icons.signal_cellular_alt, - livekit.ConnectionQuality.good: - Icons.signal_cellular_alt_2_bar, - livekit.ConnectionQuality.poor: - Icons.signal_cellular_alt_1_bar, - }[connectionQuality], - color: { - livekit.ConnectionQuality.excellent: - Colors.green, - livekit.ConnectionQuality.good: - Colors.orange, - livekit.ConnectionQuality.poor: - Colors.red, - }[connectionQuality], - size: 16, - ) - else - const SizedBox( - width: 12, - height: 12, - child: CircularProgressIndicator( - color: Colors.white, - strokeWidth: 2, - ), - ).padding(all: 3), - ], - ), - ], - ), - ); - }), - Row( - children: [ - IconButton( - icon: _layoutMode == 0 - ? const Icon(Icons.view_list) - : const Icon(Icons.grid_view), - onPressed: () { - _switchLayout(); - }, - ), - ], - ), - ], - ).padding(left: 20, right: 16), - ), - Expanded( - child: Material( - color: Theme.of(context).colorScheme.surfaceContainerLow, - child: Builder( - builder: (context) { - switch (_layoutMode) { - case 1: - return _buildGridLayout(); - default: - return _buildListLayout(); - } - }, - ), + child: ControlsWidget( + call.room, + call.room.localParticipant!, ), ), - if (call.room.localParticipant != null) - SizedBox( - width: MediaQuery.of(context).size.width, - child: ControlsWidget( - call.room, - call.room.localParticipant!, - ), - ), - ], - ), - onTap: () {}, + Gap(MediaQuery.of(context).padding.bottom), + ], ), ); }); diff --git a/lib/widgets/account/account_image.dart b/lib/widgets/account/account_image.dart index d6bad1d..bdc826f 100644 --- a/lib/widgets/account/account_image.dart +++ b/lib/widgets/account/account_image.dart @@ -54,11 +54,15 @@ class AccountImage extends StatelessWidget { )) .center(), ) - : AutoResizeUniversalImage( + : UniversalImage( sn.getAttachmentUrl(url), filterQuality: filterQuality, key: Key('attachment-${content.hashCode}'), fit: BoxFit.cover, + width: (radius != null ? radius! : 20) * 2, + height: (radius != null ? radius! : 20) * 2, + cacheWidth: (radius != null ? radius! : 20) * 2, + cacheHeight: (radius != null ? radius! : 20) * 2, ), ), ), diff --git a/lib/widgets/chat/call/call_no_content.dart b/lib/widgets/chat/call/call_no_content.dart index 1dd4e03..9b95756 100644 --- a/lib/widgets/chat/call/call_no_content.dart +++ b/lib/widgets/chat/call/call_no_content.dart @@ -8,12 +8,12 @@ import 'package:surface/widgets/account/account_image.dart'; class NoContentWidget extends StatefulWidget { final SnAccount? userinfo; final bool isSpeaking; - final bool isFixed; + final double? avatarSize; const NoContentWidget({ super.key, this.userinfo, - this.isFixed = false, + this.avatarSize, required this.isSpeaking, }); @@ -45,41 +45,35 @@ class _NoContentWidgetState extends State @override Widget build(BuildContext context) { - final double radius = widget.isFixed - ? 32 - : math.min( - MediaQuery.of(context).size.width * 0.1, - MediaQuery.of(context).size.height * 0.1, - ); + final double radius = widget.avatarSize ?? + math.min( + MediaQuery.of(context).size.width * 0.1, + MediaQuery.of(context).size.height * 0.1, + ); - return Container( - alignment: Alignment.center, - child: Center( - child: Animate( - autoPlay: false, - controller: _animationController, - effects: [ - CustomEffect( - begin: widget.isSpeaking ? 2 : 0, - end: 8, - curve: Curves.easeInOut, - duration: 1250.ms, - builder: (context, value, child) => Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(radius + 8)), - border: value > 0 - ? Border.all(color: Colors.green, width: value) - : null, - ), - child: child, - ), - ) - ], - child: AccountImage( - content: widget.userinfo?.avatar, - radius: radius, + return Animate( + autoPlay: false, + controller: _animationController, + effects: [ + CustomEffect( + begin: widget.isSpeaking ? 2 : 0, + end: 8, + curve: Curves.easeInOut, + duration: 1250.ms, + builder: (context, value, child) => Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(radius + 8)), + border: value > 0 + ? Border.all(color: Colors.green, width: value) + : null, + ), + child: child, ), - ), + ) + ], + child: AccountImage( + content: widget.userinfo?.avatar, + radius: radius, ), ); } diff --git a/lib/widgets/chat/call/call_participant.dart b/lib/widgets/chat/call/call_participant.dart index 56d851d..b15c3a7 100644 --- a/lib/widgets/chat/call/call_participant.dart +++ b/lib/widgets/chat/call/call_participant.dart @@ -2,7 +2,9 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart'; +import 'package:gap/gap.dart'; import 'package:livekit_client/livekit_client.dart'; +import 'package:styled_widget/styled_widget.dart'; import 'package:surface/types/account.dart'; import 'package:surface/types/chat.dart'; import 'package:surface/widgets/chat/call/call_no_content.dart'; @@ -11,23 +13,32 @@ import 'package:surface/widgets/chat/call/call_participant_menu.dart'; import 'package:surface/widgets/chat/call/call_participant_stats.dart'; abstract class ParticipantWidget extends StatefulWidget { - static ParticipantWidget widgetFor(ParticipantTrack participantTrack, - {bool isFixed = false, bool showStatsLayer = false}) { + static ParticipantWidget widgetFor( + ParticipantTrack participantTrack, { + double? avatarSize, + EdgeInsets? padding, + bool showStatsLayer = false, + bool isList = false, + }) { if (participantTrack.participant is LocalParticipant) { return LocalParticipantWidget( participantTrack.participant as LocalParticipant, participantTrack.videoTrack, - isFixed, + avatarSize, participantTrack.isScreenShare, showStatsLayer, + isList, + padding, ); } else if (participantTrack.participant is RemoteParticipant) { return RemoteParticipantWidget( participantTrack.participant as RemoteParticipant, participantTrack.videoTrack, - isFixed, + avatarSize, participantTrack.isScreenShare, showStatsLayer, + isList, + padding, ); } throw UnimplementedError('Unknown participant type'); @@ -36,8 +47,10 @@ abstract class ParticipantWidget extends StatefulWidget { abstract final Participant participant; abstract final VideoTrack? videoTrack; abstract final bool isScreenShare; - abstract final bool isFixed; + abstract final double? avatarSize; abstract final bool showStatsLayer; + abstract final bool isList; + abstract final EdgeInsets? padding; final VideoQuality quality; const ParticipantWidget({ @@ -52,18 +65,24 @@ class LocalParticipantWidget extends ParticipantWidget { @override final VideoTrack? videoTrack; @override - final bool isFixed; + final double? avatarSize; @override final bool isScreenShare; @override final bool showStatsLayer; + @override + final bool isList; + @override + final EdgeInsets? padding; const LocalParticipantWidget( this.participant, this.videoTrack, - this.isFixed, + this.avatarSize, this.isScreenShare, - this.showStatsLayer, { + this.showStatsLayer, + this.isList, + this.padding, { super.key, }); @@ -77,18 +96,24 @@ class RemoteParticipantWidget extends ParticipantWidget { @override final VideoTrack? videoTrack; @override - final bool isFixed; + final double? avatarSize; @override final bool isScreenShare; @override final bool showStatsLayer; + @override + final bool isList; + @override + final EdgeInsets? padding; const RemoteParticipantWidget( this.participant, this.videoTrack, - this.isFixed, + this.avatarSize, this.isScreenShare, - this.showStatsLayer, { + this.showStatsLayer, + this.isList, + this.padding, { super.key, }); @@ -136,19 +161,82 @@ abstract class _ParticipantWidgetState } @override - Widget build(BuildContext ctx) { + Widget build(BuildContext context) { + if (widget.isList) { + return Padding( + padding: widget.padding ?? EdgeInsets.zero, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + SizedBox( + width: (widget.avatarSize ?? 32) * 2, + height: (widget.avatarSize ?? 32) * 2, + child: Center( + child: NoContentWidget( + userinfo: _userinfoMetadata, + avatarSize: widget.avatarSize, + isSpeaking: widget.participant.isSpeaking, + ), + ), + ), + const Gap(8), + Expanded( + child: SizedBox( + height: (widget.avatarSize ?? 32) * 2, + child: ParticipantInfoWidget( + isList: true, + title: widget.participant.name.isNotEmpty + ? widget.participant.name + : widget.participant.identity, + audioAvailable: _firstAudioPublication?.muted == false && + _firstAudioPublication?.subscribed == true, + connectionQuality: widget.participant.connectionQuality, + isScreenShare: widget.isScreenShare, + ), + ), + ), + ], + ), + if (_activeVideoTrack != null && !_activeVideoTrack!.muted) + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(8)), + child: AspectRatio( + aspectRatio: 16 / 9, + child: Material( + borderRadius: const BorderRadius.all(Radius.circular(8)), + color: Theme.of(context) + .colorScheme + .surfaceContainer + .withOpacity(0.75), + child: VideoTrackRenderer( + _activeVideoTrack!, + fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain, + ), + ), + ).padding(top: 8), + ), + ], + ), + ); + } + return Stack( children: [ - _activeVideoTrack != null && !_activeVideoTrack!.muted - ? VideoTrackRenderer( - _activeVideoTrack!, - fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain, - ) - : NoContentWidget( - userinfo: _userinfoMetadata, - isFixed: widget.isFixed, - isSpeaking: widget.participant.isSpeaking, - ), + if (_activeVideoTrack != null && !_activeVideoTrack!.muted) + VideoTrackRenderer( + _activeVideoTrack!, + fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain, + ) + else + Center( + child: NoContentWidget( + userinfo: _userinfoMetadata, + avatarSize: widget.avatarSize, + isSpeaking: widget.participant.isSpeaking, + ), + ), if (widget.showStatsLayer) Positioned( top: 30, @@ -199,44 +287,51 @@ class _RemoteParticipantWidgetState } class InteractiveParticipantWidget extends StatelessWidget { - final double? width; - final double? height; - final Color? color; - final bool isFixedAvatar; + final double? avatarSize; + final bool isList; final ParticipantTrack participant; - final Function() onTap; + final Function? onTap; + final EdgeInsets? padding; const InteractiveParticipantWidget({ super.key, - this.width, - this.height, - this.color, - this.isFixedAvatar = false, + this.avatarSize, + this.isList = false, + this.padding, required this.participant, - required this.onTap, + this.onTap, }); @override Widget build(BuildContext context) { - return GestureDetector( - child: Container( - width: width, - height: height, - color: color, - child: ParticipantWidget.widgetFor(participant, isFixed: isFixedAvatar), - ), - onTap: () => onTap(), - onLongPress: () { - if (participant.participant is LocalParticipant) return; - showModalBottomSheet( - context: context, - builder: (context) => ParticipantMenu( - participant: participant.participant as RemoteParticipant, - videoTrack: participant.videoTrack, - isScreenShare: participant.isScreenShare, + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap != null + ? () { + onTap?.call(); + } + : null, + onLongPress: () { + if (participant.participant is LocalParticipant) return; + showModalBottomSheet( + context: context, + builder: (context) => ParticipantMenu( + participant: participant.participant as RemoteParticipant, + videoTrack: participant.videoTrack, + isScreenShare: participant.isScreenShare, + ), + ); + }, + child: Container( + child: ParticipantWidget.widgetFor( + participant, + avatarSize: avatarSize, + isList: isList, + padding: padding, ), - ); - }, + ), + ), ); } } diff --git a/lib/widgets/chat/call/call_participant_info.dart b/lib/widgets/chat/call/call_participant_info.dart index d81a75e..15dbead 100644 --- a/lib/widgets/chat/call/call_participant_info.dart +++ b/lib/widgets/chat/call/call_participant_info.dart @@ -9,6 +9,7 @@ class ParticipantInfoWidget extends StatelessWidget { final bool audioAvailable; final ConnectionQuality connectionQuality; final bool isScreenShare; + final bool isList; const ParticipantInfoWidget({ super.key, @@ -16,64 +17,124 @@ class ParticipantInfoWidget extends StatelessWidget { this.audioAvailable = true, this.connectionQuality = ConnectionQuality.unknown, this.isScreenShare = false, + this.isList = false, }); @override - Widget build(BuildContext context) => Container( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75), - padding: const EdgeInsets.symmetric( - vertical: 7, - horizontal: 10, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (title != null) - Flexible( - child: Text( - title!, - overflow: TextOverflow.ellipsis, - style: const TextStyle(color: Colors.white), - ), + Widget build(BuildContext context) { + if (isList) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (title != null) + Text( + title!, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, ), - const Gap(5), - isScreenShare - ? const Icon( - Symbols.monitor, + ).padding(left: 2), + Row( + children: [ + isScreenShare + ? const Icon( + Symbols.monitor, + color: Colors.white, + size: 16, + ) + : Icon( + audioAvailable ? Symbols.mic : Symbols.mic_off, + color: audioAvailable ? Colors.white : Colors.red, + size: 16, + ), + const Gap(3), + if (connectionQuality != ConnectionQuality.unknown) + Icon( + { + ConnectionQuality.excellent: Symbols.signal_cellular_alt, + ConnectionQuality.good: Symbols.signal_cellular_alt_2_bar, + ConnectionQuality.poor: Symbols.signal_cellular_alt_1_bar, + }[connectionQuality], + color: { + ConnectionQuality.excellent: Colors.green, + ConnectionQuality.good: Colors.orange, + ConnectionQuality.poor: Colors.red, + }[connectionQuality], + size: 16, + ) + else + const SizedBox( + width: 12, + height: 12, + child: CircularProgressIndicator( color: Colors.white, - size: 16, - ) - : Icon( - audioAvailable ? Symbols.mic : Symbols.mic_off, - color: audioAvailable ? Colors.white : Colors.red, - size: 16, + strokeWidth: 2, ), - const Gap(3), - if (connectionQuality != ConnectionQuality.unknown) - Icon( - { - ConnectionQuality.excellent: Symbols.signal_cellular_alt, - ConnectionQuality.good: Symbols.signal_cellular_alt_2_bar, - ConnectionQuality.poor: Symbols.signal_cellular_alt_1_bar, - }[connectionQuality], - color: { - ConnectionQuality.excellent: Colors.green, - ConnectionQuality.good: Colors.orange, - ConnectionQuality.poor: Colors.red, - }[connectionQuality], - size: 16, - ) - else - const SizedBox( - width: 12, - height: 12, - child: CircularProgressIndicator( - color: Colors.white, - strokeWidth: 2, - ), - ).padding(all: 3), - ], - ), + ).padding(all: 3), + ], + ) + ], ); + } + + return Container( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75), + padding: const EdgeInsets.symmetric( + vertical: 7, + horizontal: 10, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (title != null) + Flexible( + child: Text( + title!, + overflow: TextOverflow.ellipsis, + style: const TextStyle(color: Colors.white), + ), + ), + const Gap(5), + isScreenShare + ? const Icon( + Symbols.monitor, + color: Colors.white, + size: 16, + ) + : Icon( + audioAvailable ? Symbols.mic : Symbols.mic_off, + color: audioAvailable ? Colors.white : Colors.red, + size: 16, + ), + const Gap(3), + if (connectionQuality != ConnectionQuality.unknown) + Icon( + { + ConnectionQuality.excellent: Symbols.signal_cellular_alt, + ConnectionQuality.good: Symbols.signal_cellular_alt_2_bar, + ConnectionQuality.poor: Symbols.signal_cellular_alt_1_bar, + }[connectionQuality], + color: { + ConnectionQuality.excellent: Colors.green, + ConnectionQuality.good: Colors.orange, + ConnectionQuality.poor: Colors.red, + }[connectionQuality], + size: 16, + ) + else + const SizedBox( + width: 12, + height: 12, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ).padding(all: 3), + ], + ), + ); + } } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 76c0b56..ba801f9 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -24,6 +24,7 @@ import gal import hotkey_manager_macos import in_app_review import livekit_client +import livekit_noise_filter import local_notifier import media_kit_libs_macos_video import media_kit_video @@ -60,6 +61,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin")) InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin")) + LiveKitKrispNoiseFilterPlugin.register(with: registry.registrar(forPlugin: "LiveKitKrispNoiseFilterPlugin")) LocalNotifierPlugin.register(with: registry.registrar(forPlugin: "LocalNotifierPlugin")) MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index bbf6ff5..fc776d2 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -152,6 +152,11 @@ PODS: - flutter_webrtc - FlutterMacOS - WebRTC-SDK (= 125.6422.06) + - livekit_noise_filter (0.0.1): + - flutter_webrtc + - FlutterMacOS + - LiveKitKrispNoiseFilter (= 0.0.7) + - LiveKitKrispNoiseFilter (0.0.7) - local_notifier (0.1.0): - FlutterMacOS - media_kit_libs_macos_video (1.0.4): @@ -237,6 +242,7 @@ DEPENDENCIES: - hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`) - in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`) - livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`) + - livekit_noise_filter (from `Flutter/ephemeral/.symlinks/plugins/livekit_noise_filter/macos`) - local_notifier (from `Flutter/ephemeral/.symlinks/plugins/local_notifier/macos`) - media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`) - media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`) @@ -265,6 +271,7 @@ SPEC REPOS: - GoogleDataTransport - GoogleUtilities - HotKey + - LiveKitKrispNoiseFilter - nanopb - OrderedSet - PromisesObjC @@ -315,6 +322,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/in_app_review/macos livekit_client: :path: Flutter/ephemeral/.symlinks/plugins/livekit_client/macos + livekit_noise_filter: + :path: Flutter/ephemeral/.symlinks/plugins/livekit_noise_filter/macos local_notifier: :path: Flutter/ephemeral/.symlinks/plugins/local_notifier/macos media_kit_libs_macos_video: @@ -378,6 +387,8 @@ SPEC CHECKSUMS: hotkey_manager_macos: a4317849af96d2430fa89944d3c58977ca089fbe in_app_review: 0599bccaed5e02f6bed2b0d30d16f86b63ed8638 livekit_client: 35690bf9861be6325a6f7d11bb38d50c7c9fed80 + livekit_noise_filter: c5710c0871ef3621b48c0b44d3c3ff938ba414b2 + LiveKitKrispNoiseFilter: efe418ceca28163ace0ff222bd2cc02384645d84 local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65 media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758 diff --git a/pubspec.lock b/pubspec.lock index 91e6ffb..0474658 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1381,6 +1381,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.1" + livekit_noise_filter: + dependency: "direct main" + description: + name: livekit_noise_filter + sha256: "398bfd1cc63ada9dee9fd7ea415e2fc1e51e091a6d217aad3649b882c35c7fcb" + url: "https://pub.dev" + source: hosted + version: "0.1.0" local_notifier: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 3112c12..f2d484b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -144,6 +144,7 @@ dependencies: latlong2: ^0.9.1 crypto: ^3.0.6 audioplayers: ^6.4.0 + livekit_noise_filter: ^0.1.0 dev_dependencies: flutter_test: