From fd967662de398aaa7b2e5b6306a39b78194fc0d7 Mon Sep 17 00:00:00 2001 From: cloudwebrtc Date: Wed, 26 Mar 2025 14:40:16 +0800 Subject: [PATCH 01/13] release: 1.2.0. --- CHANGELOG.md | 6 ++++++ pubspec.yaml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04f2aba..27fd35d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 1.2.0 (2025-03-26) + +* Bump version for livekit sdk. +* Make AudioVisualizer configurable. +* Support AudioVisualizer for web. + ## 1.1.1 (2025-02-07) * Don't fail analyze on deprecation warnings (#13) diff --git a/pubspec.yaml b/pubspec.yaml index 299a62a..9ddee23 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: livekit_components description: "Out-of-the-box flutter widgets for livekit client." -version: 1.1.1 +version: 1.2.0 homepage: https://github.com/livekit/components-flutter environment: From 60adf746ec129c1830c2906ee35bda3affbdeb06 Mon Sep 17 00:00:00 2001 From: cloudwebrtc Date: Thu, 27 Mar 2025 10:56:12 +0800 Subject: [PATCH 02/13] fix layout for media select button. --- .../room/media_device_select_button.dart | 147 +++++++++--------- 1 file changed, 74 insertions(+), 73 deletions(-) diff --git a/lib/src/ui/widgets/room/media_device_select_button.dart b/lib/src/ui/widgets/room/media_device_select_button.dart index e69a20f..20197d9 100644 --- a/lib/src/ui/widgets/room/media_device_select_button.dart +++ b/lib/src/ui/widgets/room/media_device_select_button.dart @@ -51,84 +51,85 @@ class MediaDeviceSelectWidget extends StatelessWidget { Widget build(BuildContext context) { var deviceScreenType = getDeviceType(MediaQuery.of(context).size); return Row(mainAxisSize: MainAxisSize.min, children: [ - ElevatedButton( - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all( - deviceIsOpened ? LKColors.lkBlue : Colors.grey.withOpacity(0.9)), - foregroundColor: WidgetStateProperty.all(Colors.white), - overlayColor: WidgetStateProperty.all( - deviceIsOpened ? LKColors.lkLightBlue : Colors.grey), - shape: WidgetStateProperty.all(const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20.0), - bottomLeft: Radius.circular(20.0)))), - padding: WidgetStateProperty.all( - deviceScreenType == DeviceScreenType.desktop || - lkPlatformIsDesktop() - ? const EdgeInsets.fromLTRB(10, 20, 10, 20) - : const EdgeInsets.all(12), + SizedBox( + height: 40, + child: ElevatedButton( + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all(deviceIsOpened + ? LKColors.lkBlue + : Colors.grey.withOpacity(0.9)), + foregroundColor: WidgetStateProperty.all(Colors.white), + overlayColor: WidgetStateProperty.all( + deviceIsOpened ? LKColors.lkLightBlue : Colors.grey), + shape: WidgetStateProperty.all(const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20.0), + bottomLeft: Radius.circular(20.0)))), + padding: WidgetStateProperty.all( + const EdgeInsets.fromLTRB(12, 20, 12, 20)), + ), + onPressed: () => onToggle?.call(!deviceIsOpened), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(deviceIsOpened || defaultSelectable ? iconOn : iconOff), + const SizedBox(width: 2), + if (title != null && + (deviceScreenType != DeviceScreenType.mobile || showLabel)) + Text( + title!, + style: TextStyle(fontSize: 14), + ), + ], ), - ), - onPressed: () => onToggle?.call(!deviceIsOpened), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(deviceIsOpened || defaultSelectable ? iconOn : iconOff), - const SizedBox(width: 2), - if (title != null && - (deviceScreenType != DeviceScreenType.mobile || showLabel)) - Text( - title!, - style: TextStyle(fontSize: 14), - ), - ], ), ), const SizedBox(width: 0.2), - PopupMenuButton( - padding: const EdgeInsets.all(12), - icon: const Icon(Icons.arrow_drop_down), - offset: Offset( - 0, ((deviceList.isNotEmpty ? deviceList.length : 1) * -55.0)), - style: ButtonStyle( - backgroundColor: - WidgetStateProperty.all(Colors.grey.withOpacity(0.9)), - foregroundColor: WidgetStateProperty.all(Colors.white), - overlayColor: WidgetStateProperty.all(Colors.grey), - elevation: WidgetStateProperty.all(20), - shape: WidgetStateProperty.all(const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topRight: Radius.circular(20.0), - bottomRight: Radius.circular(20.0)))), + SizedBox( + height: 40, + child: PopupMenuButton( + icon: const Icon(Icons.arrow_drop_down), + offset: Offset( + 0, ((deviceList.isNotEmpty ? deviceList.length : 1) * -55.0)), + style: ButtonStyle( + backgroundColor: + WidgetStateProperty.all(Colors.grey.withOpacity(0.9)), + foregroundColor: WidgetStateProperty.all(Colors.white), + overlayColor: WidgetStateProperty.all(Colors.grey), + shape: WidgetStateProperty.all(const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topRight: Radius.circular(20.0), + bottomRight: Radius.circular(20.0)))), + ), + enabled: deviceIsOpened || defaultSelectable, + itemBuilder: (BuildContext context) { + return [ + if (deviceList.isNotEmpty) + ...deviceList.map((device) { + return PopupMenuItem( + value: device, + child: ListTile( + selected: (device.deviceId == selectedDeviceId), + selectedColor: LKColors.lkBlue, + leading: (device.deviceId == selectedDeviceId) + ? Icon( + Icons.check_box_outlined, + color: (device.deviceId == selectedDeviceId) + ? LKColors.lkBlue + : Colors.white, + ) + : const Icon( + Icons.check_box_outline_blank, + color: Colors.white, + ), + title: Text(device.label), + ), + onTap: () => onSelect?.call(device), + ); + }) + ]; + }, ), - enabled: deviceIsOpened || defaultSelectable, - itemBuilder: (BuildContext context) { - return [ - if (deviceList.isNotEmpty) - ...deviceList.map((device) { - return PopupMenuItem( - value: device, - child: ListTile( - selected: (device.deviceId == selectedDeviceId), - selectedColor: LKColors.lkBlue, - leading: (device.deviceId == selectedDeviceId) - ? Icon( - Icons.check_box_outlined, - color: (device.deviceId == selectedDeviceId) - ? LKColors.lkBlue - : Colors.white, - ) - : const Icon( - Icons.check_box_outline_blank, - color: Colors.white, - ), - title: Text(device.label), - ), - onTap: () => onSelect?.call(device), - ); - }) - ]; - }, ), ]); } From 752dd504bf9dcc44fbce1bfa085895e3769ca1dd Mon Sep 17 00:00:00 2001 From: cloudwebrtc Date: Thu, 27 Mar 2025 11:20:02 +0800 Subject: [PATCH 03/13] expose more fields for TrackIdentifier. --- lib/src/types/track_identifier.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/types/track_identifier.dart b/lib/src/types/track_identifier.dart index 7ec8988..91ab391 100644 --- a/lib/src/types/track_identifier.dart +++ b/lib/src/types/track_identifier.dart @@ -30,4 +30,8 @@ class TrackIdentifier { source == TrackSource.camera || source == TrackSource.camera; bool get hasTrack => track != null; + + ParticipantKind get kind => participant.kind; + + String get name => participant.name; } From aa10f7babdf6670088b8b35c47907e40df0fd11b Mon Sep 17 00:00:00 2001 From: cloudwebrtc Date: Thu, 27 Mar 2025 13:40:13 +0800 Subject: [PATCH 04/13] more changes. --- lib/livekit_components.dart | 1 + lib/src/types/track_identifier.dart | 5 ++ .../participant/participant_selector.dart | 80 +++++++++++++++++++ .../track/audio_visualizer_widget.dart | 33 ++++++-- 4 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 lib/src/ui/builder/participant/participant_selector.dart diff --git a/lib/livekit_components.dart b/lib/livekit_components.dart index c69ba57..e17946d 100644 --- a/lib/livekit_components.dart +++ b/lib/livekit_components.dart @@ -45,6 +45,7 @@ export 'src/ui/builder/participant/participant_name.dart'; export 'src/ui/builder/participant/participant_kind.dart'; export 'src/ui/builder/participant/participant_permissions.dart'; export 'src/ui/builder/participant/participant_transcription.dart'; +export 'src/ui/builder/participant/participant_selector.dart'; export 'src/ui/builder/track/connection_quality_indicator.dart'; export 'src/ui/builder/track/e2e_encryption_indicator.dart'; diff --git a/lib/src/types/track_identifier.dart b/lib/src/types/track_identifier.dart index 91ab391..3ff4103 100644 --- a/lib/src/types/track_identifier.dart +++ b/lib/src/types/track_identifier.dart @@ -26,11 +26,16 @@ class TrackIdentifier { bool get isAudio => source == TrackSource.microphone || source == TrackSource.screenShareAudio; + bool get isVideo => source == TrackSource.camera || source == TrackSource.camera; + bool get isLocal => participant is LocalParticipant; + bool get hasTrack => track != null; + bool get isAgent => kind == ParticipantKind.AGENT; + ParticipantKind get kind => participant.kind; String get name => participant.name; diff --git a/lib/src/ui/builder/participant/participant_selector.dart b/lib/src/ui/builder/participant/participant_selector.dart new file mode 100644 index 0000000..942f5fd --- /dev/null +++ b/lib/src/ui/builder/participant/participant_selector.dart @@ -0,0 +1,80 @@ +import 'package:flutter/widgets.dart'; +import 'package:livekit_client/livekit_client.dart'; +import 'package:livekit_components/src/types/track_identifier.dart'; +import 'package:provider/provider.dart'; +import 'package:collection/collection.dart'; + +import '../../../context/room_context.dart'; +import '../../../debug/logger.dart'; +import 'participant_track.dart'; + +class ParticipantSelector extends StatelessWidget { + final bool Function(TrackIdentifier identifier) filter; + final Widget Function(BuildContext context, TrackIdentifier identifier) + builder; + const ParticipantSelector({ + required this.filter, + required this.builder, + super.key, + }); + + List> buildTracksMap( + bool audio, bool video, List participants) { + final List> trackMap = []; + int index = 0; + for (Participant participant in participants) { + Debug.log('=> participant ${participant.identity}, index: $index'); + index++; + var tracks = participant.trackPublications.values; + for (var track in tracks) { + if (track.kind == TrackType.AUDIO && !audio) { + continue; + } + if (track.kind == TrackType.VIDEO && !video) { + continue; + } + trackMap.add(MapEntry(TrackIdentifier(participant, track), track)); + + Debug.log( + '=> ${track.source.toString()} track ${track.sid} for ${participant.identity}'); + } + + if (!audio && !tracks.any((t) => t.kind == TrackType.VIDEO) || + !video && tracks.any((t) => t.kind == TrackType.AUDIO) || + tracks.isEmpty) { + trackMap.add(MapEntry(TrackIdentifier(participant), null)); + Debug.log('=> no tracks for ${participant.identity}'); + } + } + + return trackMap; + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, roomCtx, child) { + Debug.log('> ParticipantSelector for ${roomCtx.roomName}'); + return Selector>( + selector: (context, participants) => roomCtx.participants, + shouldRebuild: (previous, next) => previous.length != next.length, + builder: (context, participants, child) { + var trackMap = buildTracksMap(true, true, participants); + var identifier = + trackMap.firstWhereOrNull((entry) => filter(entry.key))?.key; + + if (identifier == null) { + return const SizedBox(); + } + + return ParticipantTrack( + participant: identifier.participant, + track: identifier.track, + builder: (context) => builder(context, identifier), + ); + }, + ); + }, + ); + } +} diff --git a/lib/src/ui/widgets/track/audio_visualizer_widget.dart b/lib/src/ui/widgets/track/audio_visualizer_widget.dart index 2e61f00..5ab18ba 100644 --- a/lib/src/ui/widgets/track/audio_visualizer_widget.dart +++ b/lib/src/ui/widgets/track/audio_visualizer_widget.dart @@ -4,7 +4,6 @@ import 'package:livekit_client/livekit_client.dart'; import 'package:provider/provider.dart'; import '../../../context/track_reference_context.dart'; -import '../theme.dart'; import 'no_track_widget.dart'; class AudioVisualizerWidgetOptions { @@ -15,6 +14,7 @@ class AudioVisualizerWidgetOptions { final double maxHeight; final int durationInMilliseconds; final Color color; + final Color backgroundColor; final double spacing; final double cornerRadius; final double barMinOpacity; @@ -27,6 +27,7 @@ class AudioVisualizerWidgetOptions { this.maxHeight = 100, this.durationInMilliseconds = 500, this.color = Colors.white, + this.backgroundColor = Colors.transparent, this.spacing = 5, this.cornerRadius = 9999, this.barMinOpacity = 0.35, @@ -35,9 +36,11 @@ class AudioVisualizerWidgetOptions { class AudioVisualizerWidget extends StatelessWidget { final AudioVisualizerWidgetOptions options; + final Widget noTrackWidget; const AudioVisualizerWidget({ Key? key, + this.noTrackWidget = const NoTrackWidget(), this.options = const AudioVisualizerWidgetOptions(), }) : super(key: key); @@ -46,7 +49,7 @@ class AudioVisualizerWidget extends StatelessWidget { var trackCtx = Provider.of(context); if (trackCtx == null) { - return const NoTrackWidget(); + return noTrackWidget; } return Consumer( @@ -55,13 +58,14 @@ class AudioVisualizerWidget extends StatelessWidget { selector: (context, audioTrack) => trackCtx.audioTrack, builder: (BuildContext context, AudioTrack? audioTrack, Widget? child) { if (trackCtx.audioTrack == null) { - return const NoTrackWidget(); + return noTrackWidget; } return Container( - color: LKColors.lkDarkBlue, + color: options.backgroundColor, child: Center( child: SoundWaveformWidget( audioTrack: audioTrack, + participant: trackCtx.participant, options: options, ), ), @@ -75,10 +79,12 @@ class AudioVisualizerWidget extends StatelessWidget { class SoundWaveformWidget extends StatefulWidget { final AudioVisualizerWidgetOptions options; final AudioTrack? audioTrack; + final Participant participant; const SoundWaveformWidget({ super.key, this.audioTrack, + required this.participant, this.options = const AudioVisualizerWidgetOptions(), }); @@ -89,15 +95,28 @@ class SoundWaveformWidget extends StatefulWidget { class _SoundWaveformWidgetState extends State with TickerProviderStateMixin { late AnimationController controller; - List samples = [0, 0, 0, 0, 0, 0, 0]; + late List samples; AudioVisualizer? _visualizer; EventsListener? _listener; + EventsListener? _participantListener; void _startVisualizer(AudioTrack? track) async { if (track == null) { return; } - samples = List.filled(widget.options.barCount, 0); + + _participantListener ??= widget.participant.createListener(); + _participantListener?.on((e) { + if (mounted) { + setState(() { + samples = List.filled(widget.options.barCount, + widget.options.minHeight / widget.options.maxHeight); + }); + } + }); + + samples = List.filled(widget.options.barCount, + widget.options.minHeight / widget.options.maxHeight); _visualizer ??= createVisualizer(track, options: AudioVisualizerOptions( barCount: widget.options.barCount, @@ -120,6 +139,8 @@ class _SoundWaveformWidgetState extends State _visualizer = null; await _listener?.dispose(); _listener = null; + await _participantListener?.dispose(); + _participantListener = null; } @override From e96a9fd3bad9727cbea961d816f96916f5bfe573 Mon Sep 17 00:00:00 2001 From: cloudwebrtc Date: Thu, 27 Mar 2025 16:24:16 +0800 Subject: [PATCH 05/13] more changes. --- lib/livekit_components.dart | 2 + lib/src/context/media_device_context.dart | 6 +- lib/src/types/agent_state.dart | 15 ++++ lib/src/ui/prejoin/prejoin.dart | 6 +- .../ui/widgets/room/camera_select_button.dart | 30 ++++++-- .../ui/widgets/room/camera_switch_button.dart | 13 +++- lib/src/ui/widgets/room/chat_toggle.dart | 19 ++++-- lib/src/ui/widgets/room/control_bar.dart | 58 +++++++++++++--- .../ui/widgets/room/disconnect_button.dart | 19 ++++-- lib/src/ui/widgets/room/join_button.dart | 23 +++++-- .../room/media_device_select_button.dart | 68 +++++++++++-------- .../room/microphone_select_button.dart | 30 ++++++-- .../ui/widgets/room/screenshare_toggle.dart | 18 +++-- .../widgets/room/speaker_switch_button.dart | 13 +++- .../track/audio_visualizer_widget.dart | 7 +- 15 files changed, 248 insertions(+), 79 deletions(-) create mode 100644 lib/src/types/agent_state.dart diff --git a/lib/livekit_components.dart b/lib/livekit_components.dart index e17946d..e7e25ae 100644 --- a/lib/livekit_components.dart +++ b/lib/livekit_components.dart @@ -21,6 +21,8 @@ export 'src/context/track_reference_context.dart'; export 'src/debug/logger.dart'; +export 'src/types/agent_state.dart'; + export 'src/ui/builder/camera_preview.dart'; export 'src/ui/builder/room/chat_toggle.dart'; export 'src/ui/builder/room/chat.dart'; diff --git a/lib/src/context/media_device_context.dart b/lib/src/context/media_device_context.dart index 0bb4806..e8800ba 100644 --- a/lib/src/context/media_device_context.dart +++ b/lib/src/context/media_device_context.dart @@ -77,13 +77,13 @@ class MediaDeviceContext extends ChangeNotifier { selectedAudioInputDeviceId ??= Hardware.instance.selectedAudioInput?.deviceId ?? - _audioInputs?.first.deviceId; + _audioInputs?.firstOrNull?.deviceId; selectedVideoInputDeviceId ??= Hardware.instance.selectedVideoInput?.deviceId ?? - _videoInputs?.first.deviceId; + _videoInputs?.firstOrNull?.deviceId; selectedAudioOutputDeviceId ??= Hardware.instance.selectedAudioOutput?.deviceId ?? - _audioOutputs?.first.deviceId; + _audioOutputs?.firstOrNull?.deviceId; notifyListeners(); } diff --git a/lib/src/types/agent_state.dart b/lib/src/types/agent_state.dart new file mode 100644 index 0000000..ed3f857 --- /dev/null +++ b/lib/src/types/agent_state.dart @@ -0,0 +1,15 @@ +/// Possible states for the agent participant +/// These states are set by the agent and sent via LiveKit metadata +enum AgentState { + initializing, // Agent is starting up + speaking, // Agent is speaking to the user + thinking, // Agent is processing user input + listening; // Agent is listening to user audio + + static AgentState fromString(String value) { + return AgentState.values.firstWhere( + (state) => state.name == value, + orElse: () => AgentState.initializing, + ); + } +} diff --git a/lib/src/ui/prejoin/prejoin.dart b/lib/src/ui/prejoin/prejoin.dart index c60eaf0..ebd997b 100644 --- a/lib/src/ui/prejoin/prejoin.dart +++ b/lib/src/ui/prejoin/prejoin.dart @@ -89,11 +89,9 @@ class Prejoin extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - MicrophoneSelectButton( - showLabels: true, - ), + MicrophoneSelectButton(), CameraSelectButton( - showLabels: true, + showTitleWidget: true, ), ], )), diff --git a/lib/src/ui/widgets/room/camera_select_button.dart b/lib/src/ui/widgets/room/camera_select_button.dart index 6446b5d..8eff2f9 100644 --- a/lib/src/ui/widgets/room/camera_select_button.dart +++ b/lib/src/ui/widgets/room/camera_select_button.dart @@ -13,6 +13,7 @@ // limitations under the License. import 'package:flutter/material.dart'; +import 'package:livekit_components/src/ui/widgets/theme.dart'; import '../../builder/room/media_device_select_button.dart'; import 'media_device_select_button.dart'; @@ -20,16 +21,37 @@ import 'media_device_select_button.dart'; class CameraSelectButton extends StatelessWidget { const CameraSelectButton({ super.key, - this.showLabels = false, + this.showTitleWidget = true, + this.color = Colors.grey, + this.selectedColor = LKColors.lkBlue, + this.iconColor = Colors.white, + this.foregroundColor = Colors.white, + this.selectedOverlayColor = LKColors.lkLightBlue, + this.titleWidget, }); - final bool showLabels; + final bool showTitleWidget; + final Color color; + final Color selectedColor; + final Color iconColor; + final Color foregroundColor; + final Color selectedOverlayColor; + final Widget? titleWidget; @override Widget build(BuildContext context) { return MediaDeviceSelectButton( builder: (context, roomCtx, deviceCtx) => MediaDeviceSelectWidget( - title: 'Camera', + titleWidget: titleWidget ?? + Text( + 'Camera', + style: TextStyle(fontSize: 14, color: iconColor), + ), + backgroundColor: color, + selectedColor: selectedColor, + selectedOverlayColor: selectedOverlayColor, + iconColor: iconColor, + foregroundColor: foregroundColor, iconOn: Icons.videocam, iconOff: Icons.videocam_off, deviceList: deviceCtx.videoInputs ?? [], @@ -38,7 +60,7 @@ class CameraSelectButton extends StatelessWidget { onSelect: (device) => deviceCtx.selectVideoInput(device), onToggle: (enabled) => enabled ? deviceCtx.enableCamera() : deviceCtx.disableCamera(), - showLabel: showLabels, + showTitleWidget: showTitleWidget, ), ); } diff --git a/lib/src/ui/widgets/room/camera_switch_button.dart b/lib/src/ui/widgets/room/camera_switch_button.dart index b86528f..4bd57ee 100644 --- a/lib/src/ui/widgets/room/camera_switch_button.dart +++ b/lib/src/ui/widgets/room/camera_switch_button.dart @@ -22,19 +22,26 @@ class CameraSwitchButton extends StatelessWidget { this.currentPosition = CameraPosition.front, this.onToggle, this.disabled = false, + this.backgroundColor = Colors.grey, + this.foregroundColor = Colors.white, + this.overlayColor = Colors.grey, }); final CameraPosition? currentPosition; final Function(CameraPosition position)? onToggle; final bool disabled; + final Color backgroundColor; + final Color foregroundColor; + final Color overlayColor; @override Widget build(BuildContext context) { return ElevatedButton( style: ButtonStyle( - backgroundColor: WidgetStateProperty.all(Colors.grey.withOpacity(0.9)), - foregroundColor: WidgetStateProperty.all(Colors.white), - overlayColor: WidgetStateProperty.all(Colors.grey), + backgroundColor: + WidgetStateProperty.all(backgroundColor.withOpacity(0.9)), + foregroundColor: WidgetStateProperty.all(foregroundColor), + overlayColor: WidgetStateProperty.all(overlayColor), shape: WidgetStateProperty.all(const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(20.0)))), padding: WidgetStateProperty.all( diff --git a/lib/src/ui/widgets/room/chat_toggle.dart b/lib/src/ui/widgets/room/chat_toggle.dart index c0eb3c5..53ad380 100644 --- a/lib/src/ui/widgets/room/chat_toggle.dart +++ b/lib/src/ui/widgets/room/chat_toggle.dart @@ -24,22 +24,33 @@ class ChatToggleWidget extends StatelessWidget { required this.isChatOpen, required this.toggleChat, this.showLabel = true, + this.backgroundColor = Colors.grey, + this.foregroundColor = Colors.white, + this.overlayColor = Colors.grey, + this.selectedColor = LKColors.lkBlue, + this.selectedOverlayColor = LKColors.lkLightBlue, }); final bool isChatOpen; final Function(bool enabled) toggleChat; final bool showLabel; + final Color backgroundColor; + final Color foregroundColor; + final Color overlayColor; + final Color selectedColor; + final Color selectedOverlayColor; @override Widget build(BuildContext context) { var deviceScreenType = getDeviceType(MediaQuery.of(context).size); return ElevatedButton( style: ButtonStyle( - backgroundColor: WidgetStateProperty.all( - isChatOpen ? LKColors.lkBlue : Colors.grey.withOpacity(0.9)), - foregroundColor: WidgetStateProperty.all(Colors.white), + backgroundColor: WidgetStateProperty.all(isChatOpen + ? selectedColor + : backgroundColor.withValues(alpha: 0.9)), + foregroundColor: WidgetStateProperty.all(foregroundColor), overlayColor: WidgetStateProperty.all( - isChatOpen ? LKColors.lkLightBlue : Colors.grey), + isChatOpen ? selectedOverlayColor : backgroundColor), shape: WidgetStateProperty.all( const RoundedRectangleBorder( borderRadius: BorderRadius.all( diff --git a/lib/src/ui/widgets/room/control_bar.dart b/lib/src/ui/widgets/room/control_bar.dart index 22934c9..06f0e60 100644 --- a/lib/src/ui/widgets/room/control_bar.dart +++ b/lib/src/ui/widgets/room/control_bar.dart @@ -23,6 +23,7 @@ import '../../builder/room/disconnect_button.dart'; import '../../builder/room/media_device_select_button.dart'; import '../../builder/room/screenshare_toggle.dart'; import '../../builder/room/speaker_switch.dart'; +import '../theme.dart'; import 'camera_switch_button.dart'; import 'chat_toggle.dart'; import 'disconnect_button.dart'; @@ -39,7 +40,12 @@ class ControlBar extends StatelessWidget { this.screenShare = true, this.leave = true, this.settings = true, - this.showLabels = false, + this.showTitleWidget = false, + this.backgroundColor = Colors.grey, + this.selectedColor = LKColors.lkBlue, + this.selectedOverlayColor = LKColors.lkLightBlue, + this.foregroundColor = Colors.white, + this.iconColor = Colors.white, }); final bool microphone; @@ -49,7 +55,13 @@ class ControlBar extends StatelessWidget { final bool screenShare; final bool leave; final bool settings; - final bool showLabels; + final bool showTitleWidget; + + final Color backgroundColor; + final Color selectedColor; + final Color selectedOverlayColor; + final Color foregroundColor; + final Color iconColor; bool get isMobile => lkPlatformIsMobile(); @@ -67,7 +79,15 @@ class ControlBar extends StatelessWidget { if (microphone) MediaDeviceSelectButton( builder: (context, roomCtx, deviceCtx) => MediaDeviceSelectWidget( - title: 'Microphone', + titleWidget: Text( + 'Microphone', + style: TextStyle(fontSize: 14), + ), + backgroundColor: backgroundColor, + selectedColor: selectedColor, + selectedOverlayColor: selectedOverlayColor, + foregroundColor: foregroundColor, + iconColor: iconColor, iconOn: Icons.mic, iconOff: Icons.mic_off, deviceList: deviceCtx.audioInputs ?? [], @@ -77,14 +97,22 @@ class ControlBar extends StatelessWidget { onToggle: (enabled) => enabled ? deviceCtx.enableMicrophone() : deviceCtx.disableMicrophone(), - showLabel: showLabels, + showTitleWidget: showTitleWidget, ), ), if (audioOutput && (lkPlatformIsDesktop() || lkPlatformIs(PlatformType.web))) MediaDeviceSelectButton( builder: (context, roomCtx, deviceCtx) => MediaDeviceSelectWidget( - title: 'Audio Output', + titleWidget: Text( + 'Audio Output', + style: TextStyle(fontSize: 14), + ), + backgroundColor: backgroundColor, + selectedColor: selectedColor, + selectedOverlayColor: selectedOverlayColor, + foregroundColor: foregroundColor, + iconColor: iconColor, iconOn: Icons.volume_up, iconOff: Icons.volume_off, deviceList: deviceCtx.audioOutputs ?? [], @@ -93,13 +121,21 @@ class ControlBar extends StatelessWidget { toggleAvailable: false, defaultSelectable: true, onSelect: (device) => deviceCtx.selectAudioOutput(device), - showLabel: showLabels, + showTitleWidget: showTitleWidget, ), ), if (camera) MediaDeviceSelectButton( builder: (context, roomCtx, deviceCtx) => MediaDeviceSelectWidget( - title: 'Camera', + titleWidget: Text( + 'Camera', + style: TextStyle(fontSize: 14), + ), + backgroundColor: backgroundColor, + selectedColor: selectedColor, + selectedOverlayColor: selectedOverlayColor, + foregroundColor: foregroundColor, + iconColor: iconColor, iconOn: Icons.videocam, iconOff: Icons.videocam_off, deviceList: deviceCtx.videoInputs ?? [], @@ -109,7 +145,7 @@ class ControlBar extends StatelessWidget { onToggle: (enabled) => enabled ? deviceCtx.enableCamera() : deviceCtx.disableCamera(), - showLabel: showLabels, + showTitleWidget: showTitleWidget, ), ), if (isMobile && microphone) @@ -135,7 +171,7 @@ class ControlBar extends StatelessWidget { roomCtx: roomCtx, deviceCtx: deviceCtx, screenShareEnabled: screenShareEnabled, - showLabel: showLabels, + showLabel: showTitleWidget, ), ), if (chat) @@ -143,7 +179,7 @@ class ControlBar extends StatelessWidget { builder: (context, roomCtx, isChatEnabled) => ChatToggleWidget( isChatOpen: roomCtx.isChatEnabled, toggleChat: (enabled) => roomCtx.toggleChat(enabled), - showLabel: showLabels, + showLabel: showTitleWidget, ), ), if (leave) @@ -151,7 +187,7 @@ class ControlBar extends StatelessWidget { builder: (context, roomCtx, connected) => DisconnectButtonWidget( roomCtx: roomCtx, connected: connected, - showLabel: showLabels, + showLabel: showTitleWidget, ), ), ], diff --git a/lib/src/ui/widgets/room/disconnect_button.dart b/lib/src/ui/widgets/room/disconnect_button.dart index a178d6d..f964035 100644 --- a/lib/src/ui/widgets/room/disconnect_button.dart +++ b/lib/src/ui/widgets/room/disconnect_button.dart @@ -27,6 +27,11 @@ class DisconnectButtonWidget extends StatelessWidget { this.onPressed, this.title = 'Leave', this.showLabel = true, + this.backgroundColor = Colors.grey, + this.selectedColor = Colors.red, + this.foregroundColor = Colors.white, + this.overlayColor = Colors.grey, + this.selectedOverlayColor = Colors.redAccent, }); final bool connected; @@ -34,6 +39,12 @@ class DisconnectButtonWidget extends StatelessWidget { final String title; final bool showLabel; + final Color backgroundColor; + final Color foregroundColor; + final Color overlayColor; + final Color selectedColor; + final Color selectedOverlayColor; + final Function()? onPressed; @override @@ -42,10 +53,10 @@ class DisconnectButtonWidget extends StatelessWidget { return ElevatedButton( style: ButtonStyle( backgroundColor: WidgetStateProperty.all( - connected ? Colors.red : Colors.grey.withOpacity(0.9)), - foregroundColor: WidgetStateProperty.all(Colors.white), - overlayColor: - WidgetStateProperty.all(connected ? Colors.redAccent : Colors.grey), + connected ? selectedColor : backgroundColor.withOpacity(0.9)), + foregroundColor: WidgetStateProperty.all(foregroundColor), + overlayColor: WidgetStateProperty.all( + connected ? selectedOverlayColor : overlayColor), shape: WidgetStateProperty.all( const RoundedRectangleBorder( borderRadius: BorderRadius.all( diff --git a/lib/src/ui/widgets/room/join_button.dart b/lib/src/ui/widgets/room/join_button.dart index 17706a3..2f1baf0 100644 --- a/lib/src/ui/widgets/room/join_button.dart +++ b/lib/src/ui/widgets/room/join_button.dart @@ -23,12 +23,23 @@ class JoinButtonWidget extends StatelessWidget { required this.roomCtx, required this.connected, this.onPressed, + this.backgroundColor = LKColors.lkBlue, + this.overlayColor = Colors.grey, + this.foregroundColor = Colors.white, + this.disabledColor = Colors.grey, + this.disabledOverlayColor = LKColors.lkLightBlue, }) : super(key: key); RoomContext roomCtx; bool connected; final Function()? onPressed; + final Color backgroundColor; + final Color foregroundColor; + final Color overlayColor; + final Color disabledColor; + final Color disabledOverlayColor; + @override Widget build(BuildContext context) { return ElevatedButton( @@ -38,9 +49,11 @@ class JoinButtonWidget extends StatelessWidget { ? roomCtx.connect() : null, style: ButtonStyle( - backgroundColor: - WidgetStateProperty.all(connected ? Colors.grey : LKColors.lkBlue), - foregroundColor: WidgetStateProperty.all(Colors.white), + backgroundColor: WidgetStateProperty.all( + connected ? disabledColor : backgroundColor), + foregroundColor: WidgetStateProperty.all(foregroundColor), + overlayColor: WidgetStateProperty.all( + connected ? overlayColor : disabledOverlayColor), shape: WidgetStateProperty.all( const RoundedRectangleBorder( borderRadius: BorderRadius.all( @@ -50,9 +63,9 @@ class JoinButtonWidget extends StatelessWidget { ), padding: WidgetStateProperty.all(const EdgeInsets.all(12)), ), - child: const Text( + child: Text( 'Join Room', - style: TextStyle(color: Colors.white, fontSize: 16), + style: TextStyle(color: foregroundColor, fontSize: 16), ), ); } diff --git a/lib/src/ui/widgets/room/media_device_select_button.dart b/lib/src/ui/widgets/room/media_device_select_button.dart index 20197d9..53a7a6b 100644 --- a/lib/src/ui/widgets/room/media_device_select_button.dart +++ b/lib/src/ui/widgets/room/media_device_select_button.dart @@ -15,31 +15,40 @@ import 'package:flutter/material.dart'; import 'package:livekit_client/livekit_client.dart'; +import 'package:livekit_components/livekit_components.dart'; import 'package:responsive_builder/responsive_builder.dart'; -import '../theme.dart'; - class MediaDeviceSelectWidget extends StatelessWidget { const MediaDeviceSelectWidget({ super.key, required this.iconOn, required this.iconOff, required this.deviceList, - this.title, + required this.backgroundColor, + required this.foregroundColor, + required this.selectedColor, + required this.selectedOverlayColor, + required this.iconColor, + required this.titleWidget, this.onToggle, this.onSelect, required this.deviceIsOpened, this.selectedDeviceId, - this.showLabel = false, + this.showTitleWidget = false, this.defaultSelectable = false, this.toggleAvailable = true, }); final bool deviceIsOpened; - final String? title; + final Widget? titleWidget; final IconData iconOn; final IconData iconOff; - final bool showLabel; + final bool showTitleWidget; + final Color backgroundColor; + final Color iconColor; + final Color selectedColor; + final Color selectedOverlayColor; + final Color foregroundColor; final String? selectedDeviceId; final List deviceList; final Function(MediaDevice device)? onSelect; @@ -56,11 +65,12 @@ class MediaDeviceSelectWidget extends StatelessWidget { child: ElevatedButton( style: ButtonStyle( backgroundColor: WidgetStateProperty.all(deviceIsOpened - ? LKColors.lkBlue - : Colors.grey.withOpacity(0.9)), - foregroundColor: WidgetStateProperty.all(Colors.white), + ? selectedColor + : backgroundColor.withValues(alpha: 0.9)), + foregroundColor: WidgetStateProperty.all(foregroundColor), overlayColor: WidgetStateProperty.all( - deviceIsOpened ? LKColors.lkLightBlue : Colors.grey), + deviceIsOpened ? selectedOverlayColor : backgroundColor), + shadowColor: WidgetStateProperty.all(Colors.transparent), shape: WidgetStateProperty.all(const RoundedRectangleBorder( borderRadius: BorderRadius.only( topLeft: Radius.circular(20.0), @@ -72,14 +82,13 @@ class MediaDeviceSelectWidget extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(deviceIsOpened || defaultSelectable ? iconOn : iconOff), + Icon(deviceIsOpened || defaultSelectable ? iconOn : iconOff, + color: iconColor), const SizedBox(width: 2), - if (title != null && - (deviceScreenType != DeviceScreenType.mobile || showLabel)) - Text( - title!, - style: TextStyle(fontSize: 14), - ), + if (titleWidget != null && + (deviceScreenType != DeviceScreenType.mobile || + showTitleWidget)) + titleWidget!, ], ), ), @@ -88,14 +97,19 @@ class MediaDeviceSelectWidget extends StatelessWidget { SizedBox( height: 40, child: PopupMenuButton( - icon: const Icon(Icons.arrow_drop_down), + icon: Icon( + Icons.arrow_drop_down, + color: iconColor, + ), offset: Offset( 0, ((deviceList.isNotEmpty ? deviceList.length : 1) * -55.0)), style: ButtonStyle( - backgroundColor: - WidgetStateProperty.all(Colors.grey.withOpacity(0.9)), - foregroundColor: WidgetStateProperty.all(Colors.white), - overlayColor: WidgetStateProperty.all(Colors.grey), + backgroundColor: WidgetStateProperty.all(deviceIsOpened + ? selectedColor + : backgroundColor.withValues(alpha: 0.9)), + foregroundColor: WidgetStateProperty.all(foregroundColor), + overlayColor: WidgetStateProperty.all( + deviceIsOpened ? selectedOverlayColor : backgroundColor), shape: WidgetStateProperty.all(const RoundedRectangleBorder( borderRadius: BorderRadius.only( topRight: Radius.circular(20.0), @@ -110,17 +124,17 @@ class MediaDeviceSelectWidget extends StatelessWidget { value: device, child: ListTile( selected: (device.deviceId == selectedDeviceId), - selectedColor: LKColors.lkBlue, + selectedColor: selectedColor, leading: (device.deviceId == selectedDeviceId) ? Icon( Icons.check_box_outlined, color: (device.deviceId == selectedDeviceId) - ? LKColors.lkBlue - : Colors.white, + ? selectedColor + : backgroundColor, ) - : const Icon( + : Icon( Icons.check_box_outline_blank, - color: Colors.white, + color: backgroundColor, ), title: Text(device.label), ), diff --git a/lib/src/ui/widgets/room/microphone_select_button.dart b/lib/src/ui/widgets/room/microphone_select_button.dart index a03136d..f8458a6 100644 --- a/lib/src/ui/widgets/room/microphone_select_button.dart +++ b/lib/src/ui/widgets/room/microphone_select_button.dart @@ -15,21 +15,43 @@ import 'package:flutter/material.dart'; import '../../builder/room/media_device_select_button.dart'; +import '../theme.dart'; import 'media_device_select_button.dart'; class MicrophoneSelectButton extends StatelessWidget { const MicrophoneSelectButton({ super.key, - this.showLabels = false, + this.showtitleWidget = true, + this.titleWidget, + this.color = Colors.grey, + this.selectedColor = LKColors.lkBlue, + this.iconColor = Colors.white, + this.foregroundColor = Colors.white, + this.selectedOverlayColor = LKColors.lkLightBlue, }); - final bool showLabels; + final bool showtitleWidget; + final Widget? titleWidget; + final Color color; + final Color selectedColor; + final Color iconColor; + final Color foregroundColor; + final Color selectedOverlayColor; @override Widget build(BuildContext context) { return MediaDeviceSelectButton( builder: (context, roomCtx, deviceCtx) => MediaDeviceSelectWidget( - title: 'Microphone', + titleWidget: titleWidget ?? + Text( + 'Microphone', + style: TextStyle(fontSize: 14, color: iconColor), + ), + backgroundColor: color, + selectedColor: selectedColor, + foregroundColor: foregroundColor, + selectedOverlayColor: selectedOverlayColor, + iconColor: iconColor, iconOn: Icons.mic, iconOff: Icons.mic_off, deviceList: deviceCtx.audioInputs ?? [], @@ -39,7 +61,7 @@ class MicrophoneSelectButton extends StatelessWidget { onToggle: (enabled) => enabled ? deviceCtx.enableMicrophone() : deviceCtx.disableMicrophone(), - showLabel: showLabels, + showTitleWidget: showtitleWidget, ), ); } diff --git a/lib/src/ui/widgets/room/screenshare_toggle.dart b/lib/src/ui/widgets/room/screenshare_toggle.dart index 5c0cbc8..308d22a 100644 --- a/lib/src/ui/widgets/room/screenshare_toggle.dart +++ b/lib/src/ui/widgets/room/screenshare_toggle.dart @@ -28,12 +28,22 @@ class ScreenShareToggleWidget extends StatelessWidget { required this.deviceCtx, required this.screenShareEnabled, this.showLabel = false, + this.backgroundColor = Colors.grey, + this.foregroundColor = Colors.white, + this.selectedColor = LKColors.lkBlue, + this.overlayColor = Colors.grey, + this.selectedOverlayColor = LKColors.lkLightBlue, }); final bool screenShareEnabled; final bool showLabel; final RoomContext roomCtx; final MediaDeviceContext deviceCtx; + final Color backgroundColor; + final Color foregroundColor; + final Color overlayColor; + final Color selectedColor; + final Color selectedOverlayColor; @override Widget build(BuildContext context) { @@ -41,11 +51,11 @@ class ScreenShareToggleWidget extends StatelessWidget { return ElevatedButton( style: ButtonStyle( backgroundColor: WidgetStateProperty.all(screenShareEnabled - ? LKColors.lkBlue - : Colors.grey.withOpacity(0.9)), - foregroundColor: WidgetStateProperty.all(Colors.white), + ? selectedColor + : backgroundColor.withValues(alpha: 0.9)), + foregroundColor: WidgetStateProperty.all(foregroundColor), overlayColor: WidgetStateProperty.all( - screenShareEnabled ? LKColors.lkLightBlue : Colors.grey), + screenShareEnabled ? selectedOverlayColor : overlayColor), shape: WidgetStateProperty.all( const RoundedRectangleBorder( borderRadius: BorderRadius.all( diff --git a/lib/src/ui/widgets/room/speaker_switch_button.dart b/lib/src/ui/widgets/room/speaker_switch_button.dart index 92ef617..a35be40 100644 --- a/lib/src/ui/widgets/room/speaker_switch_button.dart +++ b/lib/src/ui/widgets/room/speaker_switch_button.dart @@ -20,19 +20,26 @@ class SpeakerSwitchButton extends StatelessWidget { this.isSpeakerOn = false, this.onToggle, this.disabled = false, + this.backgroundColor = Colors.grey, + this.foregroundColor = Colors.white, + this.overlayColor = Colors.grey, }); final bool isSpeakerOn; final Function(bool speakerOn)? onToggle; final bool disabled; + final Color backgroundColor; + final Color foregroundColor; + final Color overlayColor; @override Widget build(BuildContext context) { return ElevatedButton( style: ButtonStyle( - backgroundColor: WidgetStateProperty.all(Colors.grey.withOpacity(0.9)), - foregroundColor: WidgetStateProperty.all(Colors.white), - overlayColor: WidgetStateProperty.all(Colors.grey), + backgroundColor: + WidgetStateProperty.all(backgroundColor.withValues(alpha: 0.9)), + foregroundColor: WidgetStateProperty.all(foregroundColor), + overlayColor: WidgetStateProperty.all(overlayColor), shape: WidgetStateProperty.all(const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(20.0)))), padding: WidgetStateProperty.all(const EdgeInsets.all(12)), diff --git a/lib/src/ui/widgets/track/audio_visualizer_widget.dart b/lib/src/ui/widgets/track/audio_visualizer_widget.dart index 5ab18ba..008491f 100644 --- a/lib/src/ui/widgets/track/audio_visualizer_widget.dart +++ b/lib/src/ui/widgets/track/audio_visualizer_widget.dart @@ -4,6 +4,7 @@ import 'package:livekit_client/livekit_client.dart'; import 'package:provider/provider.dart'; import '../../../context/track_reference_context.dart'; +import '../theme.dart'; import 'no_track_widget.dart'; class AudioVisualizerWidgetOptions { @@ -14,7 +15,6 @@ class AudioVisualizerWidgetOptions { final double maxHeight; final int durationInMilliseconds; final Color color; - final Color backgroundColor; final double spacing; final double cornerRadius; final double barMinOpacity; @@ -27,7 +27,6 @@ class AudioVisualizerWidgetOptions { this.maxHeight = 100, this.durationInMilliseconds = 500, this.color = Colors.white, - this.backgroundColor = Colors.transparent, this.spacing = 5, this.cornerRadius = 9999, this.barMinOpacity = 0.35, @@ -37,10 +36,12 @@ class AudioVisualizerWidgetOptions { class AudioVisualizerWidget extends StatelessWidget { final AudioVisualizerWidgetOptions options; final Widget noTrackWidget; + final Color backgroundColor; const AudioVisualizerWidget({ Key? key, this.noTrackWidget = const NoTrackWidget(), + this.backgroundColor = Colors.transparent, this.options = const AudioVisualizerWidgetOptions(), }) : super(key: key); @@ -61,7 +62,7 @@ class AudioVisualizerWidget extends StatelessWidget { return noTrackWidget; } return Container( - color: options.backgroundColor, + color: backgroundColor, child: Center( child: SoundWaveformWidget( audioTrack: audioTrack, From 6f237b1543cba78e8aac84cda3b14a3b6e56a75b Mon Sep 17 00:00:00 2001 From: cloudwebrtc Date: Thu, 27 Mar 2025 19:51:19 +0800 Subject: [PATCH 06/13] update. --- example/lib/main.dart | 15 +++- lib/livekit_components.dart | 2 + lib/src/context/room_context.dart | 6 +- lib/src/context/transcription_context.dart | 58 +++++++++++++ lib/src/ui/builder/room/transcription.dart | 41 +++++++++ .../participant/transcription_widget.dart | 87 +++++++++++++++++++ .../track/audio_visualizer_widget.dart | 79 +++++++++-------- 7 files changed, 248 insertions(+), 40 deletions(-) create mode 100644 lib/src/context/transcription_context.dart create mode 100644 lib/src/ui/builder/room/transcription.dart create mode 100644 lib/src/ui/widgets/participant/transcription_widget.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index 2b7af20..0761df9 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -113,6 +113,16 @@ class MyHomePage extends StatelessWidget { flex: 5, child: Stack( children: [ + /* Expanded( + child: TranscriptionBuilder( + builder: + (context, roomCtx, transcriptions) { + return TranscriptionWidget( + transcriptions: transcriptions, + ); + }, + ), + ),*/ /// show participant loop ParticipantLoop( showAudioTracks: true, @@ -137,7 +147,10 @@ class MyHomePage extends StatelessWidget { identifier.isAudio && roomCtx .enableAudioVisulizer - ? const AudioVisualizerWidget() + ? const AudioVisualizerWidget( + backgroundColor: + LKColors.lkDarkBlue, + ) : IsSpeakingIndicator( builder: (context, isSpeaking) { diff --git a/lib/livekit_components.dart b/lib/livekit_components.dart index e7e25ae..380a805 100644 --- a/lib/livekit_components.dart +++ b/lib/livekit_components.dart @@ -37,6 +37,7 @@ export 'src/ui/builder/room/room_participants.dart'; export 'src/ui/builder/room/room.dart'; export 'src/ui/builder/room/screenshare_toggle.dart'; export 'src/ui/builder/room/room_active_recording_indicator.dart'; +export 'src/ui/builder/room/transcription.dart'; export 'src/ui/builder/participant/participant_track.dart'; export 'src/ui/builder/participant/participant_attributes.dart'; @@ -67,6 +68,7 @@ export 'src/ui/widgets/room/control_bar.dart'; export 'src/ui/widgets/room/disconnect_button.dart'; export 'src/ui/widgets/room/screenshare_toggle.dart'; export 'src/ui/widgets/room/clear_pin_button.dart'; +export 'src/ui/widgets/participant/transcription_widget.dart'; export 'src/ui/widgets/track/audio_visualizer_widget.dart'; export 'src/ui/widgets/track/focus_toggle.dart'; diff --git a/lib/src/context/room_context.dart b/lib/src/context/room_context.dart index a80bf5f..b87f49f 100644 --- a/lib/src/context/room_context.dart +++ b/lib/src/context/room_context.dart @@ -17,12 +17,14 @@ import 'dart:async'; import 'package:flutter/material.dart' hide ConnectionState; import 'package:livekit_client/livekit_client.dart'; +import 'package:livekit_components/src/context/transcription_context.dart'; import 'package:provider/provider.dart'; import '../debug/logger.dart'; import 'chat_context.dart'; -class RoomContext extends ChangeNotifier with ChatContextMixin { +class RoomContext extends ChangeNotifier + with ChatContextMixin, TranscriptionContextMixin { /// Get the [RoomContext] from the [context]. /// this method must be called under the [LivekitRoom] widget. static RoomContext? of(BuildContext context) { @@ -62,6 +64,7 @@ class RoomContext extends ChangeNotifier with ChatContextMixin { ..on((event) { Debug.event('RoomContext: RoomConnectedEvent $roomName'); chatContextSetup(_listener, _room.localParticipant!); + transcriptionContextSetup(_listener); _connectionState = _room.connectionState; _connected = true; _connecting = false; @@ -76,6 +79,7 @@ class RoomContext extends ChangeNotifier with ChatContextMixin { Debug.event('RoomContext: RoomDisconnectedEvent $roomName'); _connectionState = _room.connectionState; chatContextSetup(null, null); + transcriptionContextSetup(null); _connected = false; _participants.clear(); onDisconnected?.call(); diff --git a/lib/src/context/transcription_context.dart b/lib/src/context/transcription_context.dart new file mode 100644 index 0000000..3609f4b --- /dev/null +++ b/lib/src/context/transcription_context.dart @@ -0,0 +1,58 @@ +// Copyright 2024 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import 'package:collection/collection.dart'; + +import 'package:livekit_client/livekit_client.dart'; + +import '../debug/logger.dart'; + +class TranscriptionForParticipant { + TranscriptionForParticipant( + this.segment, + this.participant, + ); + TranscriptionSegment segment; + final Participant participant; +} + +mixin TranscriptionContextMixin on ChangeNotifier { + final List _transcriptions = []; + List get transcriptions => _transcriptions; + EventsListener? _listener; + + void transcriptionContextSetup(EventsListener? listener) { + _listener = listener; + if (listener != null) { + _listener!.on((event) { + Debug.event('TranscriptionContext: TranscriptionEvent'); + for (var segment in event.segments) { + var transcription = _transcriptions + .firstWhereOrNull((t) => t.segment.id == segment.id); + if (transcription != null) { + transcription.segment = segment; + } else { + _transcriptions + .add(TranscriptionForParticipant(segment, event.participant)); + } + } + notifyListeners(); + }); + } else { + _listener = null; + _transcriptions.clear(); + } + } +} diff --git a/lib/src/ui/builder/room/transcription.dart b/lib/src/ui/builder/room/transcription.dart new file mode 100644 index 0000000..c794653 --- /dev/null +++ b/lib/src/ui/builder/room/transcription.dart @@ -0,0 +1,41 @@ +// Copyright 2024 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import '../../../context/room_context.dart'; +import '../../../context/transcription_context.dart'; + +class TranscriptionBuilder extends StatelessWidget { + const TranscriptionBuilder({ + super.key, + required this.builder, + }); + + final Function(BuildContext context, RoomContext roomCtx, + List transcriptions) builder; + + @override + Widget build(BuildContext context) { + return Consumer(builder: (context, roomCtx, child) { + return builder( + context, + roomCtx, + roomCtx.transcriptions, + ); + }); + } +} diff --git a/lib/src/ui/widgets/participant/transcription_widget.dart b/lib/src/ui/widgets/participant/transcription_widget.dart new file mode 100644 index 0000000..dfa2316 --- /dev/null +++ b/lib/src/ui/widgets/participant/transcription_widget.dart @@ -0,0 +1,87 @@ +// Copyright 2024 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; + +import 'package:chat_bubbles/chat_bubbles.dart'; +import 'package:livekit_client/livekit_client.dart'; + +import '../../../context/transcription_context.dart'; +import '../theme.dart'; + +class TranscriptionWidget extends StatelessWidget { + TranscriptionWidget({ + super.key, + required this.transcriptions, + this.backgroundColor = LKColors.lkDarkBlue, + this.textColor = Colors.white, + }); + + final Color backgroundColor; + final Color textColor; + final List transcriptions; + final ScrollController _scrollController = ScrollController(); + + List _buildMessages( + List transcriptions) { + List msgWidgets = []; + var sortedTranscriptions = transcriptions + ..sort((a, b) => + a.segment.firstReceivedTime.compareTo(b.segment.firstReceivedTime)); + for (var transcription in sortedTranscriptions) { + var participant = transcription.participant; + var segment = transcription.segment; + /*msgWidgets.add( + CustomDateNameChip( + name: participant.identity, + date: segment.lastReceivedTime, + ), + );*/ + msgWidgets.add( + BubbleNormal( + text: segment.text + (segment.isFinal ? '' : '...'), + color: textColor, + tail: false, + isSender: participant is LocalParticipant, + ), + ); + } + return msgWidgets; + } + + void scrollToBottom() { + _scrollController.animateTo(_scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 300), curve: Curves.easeOut); + } + + @override + Widget build(BuildContext context) { + return Container( + color: backgroundColor, + padding: const EdgeInsets.all(1.0), + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + controller: _scrollController, + child: Column( + children: _buildMessages(transcriptions), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/ui/widgets/track/audio_visualizer_widget.dart b/lib/src/ui/widgets/track/audio_visualizer_widget.dart index 008491f..6a6b616 100644 --- a/lib/src/ui/widgets/track/audio_visualizer_widget.dart +++ b/lib/src/ui/widgets/track/audio_visualizer_widget.dart @@ -170,44 +170,47 @@ class _SoundWaveformWidgetState extends State final count = widget.options.barCount; final minHeight = widget.options.minHeight; final maxHeight = widget.options.maxHeight; - return AnimatedBuilder( - animation: controller, - builder: (c, child) { - return Row( - mainAxisSize: MainAxisSize.min, - children: List.generate( - count, - (i) { - final heightPercent = - ((samples[i] - minHeight) / (maxHeight - minHeight)) - .clamp(0.0, 1.0); - final barOpacity = - (1.0 - widget.options.barMinOpacity) * heightPercent + - widget.options.barMinOpacity; - - return AnimatedContainer( - duration: Duration( - milliseconds: - widget.options.durationInMilliseconds ~/ count), - margin: i == (samples.length - 1) - ? EdgeInsets.zero - : EdgeInsets.only(right: widget.options.spacing), - height: samples[i] < minHeight - ? minHeight - : samples[i] > maxHeight - ? maxHeight - : samples[i], - width: widget.options.width, - decoration: BoxDecoration( - color: widget.options.color.withOpacity(barOpacity), - borderRadius: - BorderRadius.circular(widget.options.cornerRadius), - ), - ); - }, - ), - ); - }, + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) => + AnimatedBuilder( + animation: controller, + builder: (c, child) { + return Row( + mainAxisSize: MainAxisSize.min, + children: List.generate( + count, + (i) { + final heightPercent = + ((samples[i] - minHeight) / (maxHeight - minHeight)) + .clamp(0.0, 1.0); + final barOpacity = + (1.0 - widget.options.barMinOpacity) * heightPercent + + widget.options.barMinOpacity; + + return AnimatedContainer( + duration: Duration( + milliseconds: + widget.options.durationInMilliseconds ~/ count), + margin: i == (samples.length - 1) + ? EdgeInsets.zero + : EdgeInsets.only(right: widget.options.spacing), + height: samples[i] < minHeight + ? minHeight + : samples[i] > maxHeight + ? maxHeight + : samples[i], + width: widget.options.width, + decoration: BoxDecoration( + color: widget.options.color.withOpacity(barOpacity), + borderRadius: + BorderRadius.circular(widget.options.cornerRadius), + ), + ); + }, + ), + ); + }, + ), ); } } From 9d6162ee4050bb3376978cc43e775baec019661d Mon Sep 17 00:00:00 2001 From: cloudwebrtc Date: Thu, 27 Mar 2025 20:06:19 +0800 Subject: [PATCH 07/13] fix. --- lib/src/ui/widgets/camera_preview.dart | 4 +++- .../ui/widgets/participant/is_speaking_indicator.dart | 4 +++- .../ui/widgets/participant/participant_status_bar.dart | 2 +- lib/src/ui/widgets/room/camera_switch_button.dart | 2 +- lib/src/ui/widgets/room/disconnect_button.dart | 2 +- lib/src/ui/widgets/theme.dart | 8 ++++---- lib/src/ui/widgets/track/audio_visualizer_widget.dart | 2 +- lib/src/ui/widgets/track/no_track_widget.dart | 9 +++++++-- lib/src/ui/widgets/track/track_stats_widget.dart | 2 +- 9 files changed, 22 insertions(+), 13 deletions(-) diff --git a/lib/src/ui/widgets/camera_preview.dart b/lib/src/ui/widgets/camera_preview.dart index ae8943b..f528fd3 100644 --- a/lib/src/ui/widgets/camera_preview.dart +++ b/lib/src/ui/widgets/camera_preview.dart @@ -24,8 +24,10 @@ class CameraPreviewWidget extends StatelessWidget { const CameraPreviewWidget({ super.key, required this.track, + this.iconColor = LKColors.lkBlue, }); final LocalVideoTrack? track; + final Color iconColor; @override Widget build(BuildContext context) { return Container( @@ -39,7 +41,7 @@ class CameraPreviewWidget extends StatelessWidget { child: LayoutBuilder( builder: (context, constraints) => Icon( Icons.videocam_off_outlined, - color: LKColors.lkBlue, + color: iconColor, size: math.min(constraints.maxHeight, constraints.maxWidth) * 0.33, diff --git a/lib/src/ui/widgets/participant/is_speaking_indicator.dart b/lib/src/ui/widgets/participant/is_speaking_indicator.dart index 00d7eb9..08769d9 100644 --- a/lib/src/ui/widgets/participant/is_speaking_indicator.dart +++ b/lib/src/ui/widgets/participant/is_speaking_indicator.dart @@ -21,10 +21,12 @@ class IsSpeakingIndicatorWidget extends StatelessWidget { Key? key, required this.isSpeaking, required this.child, + this.borderColor = LKColors.lkBlue, }) : super(key: key); final bool isSpeaking; final Widget child; + final Color borderColor; @override Widget build(BuildContext context) { @@ -33,7 +35,7 @@ class IsSpeakingIndicatorWidget extends StatelessWidget { border: isSpeaking ? Border.all( width: 3, - color: LKColors.lkBlue, + color: borderColor, ) : null, ), diff --git a/lib/src/ui/widgets/participant/participant_status_bar.dart b/lib/src/ui/widgets/participant/participant_status_bar.dart index 98bfda8..bd1afa6 100644 --- a/lib/src/ui/widgets/participant/participant_status_bar.dart +++ b/lib/src/ui/widgets/participant/participant_status_bar.dart @@ -49,7 +49,7 @@ class ParticipantStatusBar extends StatelessWidget { var isScreenShare = trackCtx?.isScreenShare ?? false; return Container( padding: const EdgeInsets.symmetric(horizontal: 6), - color: Colors.black.withOpacity(0.6), + color: Colors.black.withValues(alpha: 0.6), child: Row( mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.center, diff --git a/lib/src/ui/widgets/room/camera_switch_button.dart b/lib/src/ui/widgets/room/camera_switch_button.dart index 4bd57ee..139433a 100644 --- a/lib/src/ui/widgets/room/camera_switch_button.dart +++ b/lib/src/ui/widgets/room/camera_switch_button.dart @@ -39,7 +39,7 @@ class CameraSwitchButton extends StatelessWidget { return ElevatedButton( style: ButtonStyle( backgroundColor: - WidgetStateProperty.all(backgroundColor.withOpacity(0.9)), + WidgetStateProperty.all(backgroundColor.withValues(alpha: 0.9)), foregroundColor: WidgetStateProperty.all(foregroundColor), overlayColor: WidgetStateProperty.all(overlayColor), shape: WidgetStateProperty.all(const RoundedRectangleBorder( diff --git a/lib/src/ui/widgets/room/disconnect_button.dart b/lib/src/ui/widgets/room/disconnect_button.dart index f964035..0574455 100644 --- a/lib/src/ui/widgets/room/disconnect_button.dart +++ b/lib/src/ui/widgets/room/disconnect_button.dart @@ -53,7 +53,7 @@ class DisconnectButtonWidget extends StatelessWidget { return ElevatedButton( style: ButtonStyle( backgroundColor: WidgetStateProperty.all( - connected ? selectedColor : backgroundColor.withOpacity(0.9)), + connected ? selectedColor : backgroundColor.withValues(alpha: 0.9)), foregroundColor: WidgetStateProperty.all(foregroundColor), overlayColor: WidgetStateProperty.all( connected ? selectedOverlayColor : overlayColor), diff --git a/lib/src/ui/widgets/theme.dart b/lib/src/ui/widgets/theme.dart index 732dce7..4208ada 100644 --- a/lib/src/ui/widgets/theme.dart +++ b/lib/src/ui/widgets/theme.dart @@ -60,7 +60,7 @@ class LiveKitTheme { // backgroundColor: WidgetStateProperty.all(accentColor), backgroundColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.disabled)) { - return accentColor.withOpacity(0.5); + return accentColor.withValues(alpha: 0.5); } return accentColor; }), @@ -75,13 +75,13 @@ class LiveKitTheme { if (states.contains(WidgetState.selected)) { return accentColor; } - return accentColor.withOpacity(0.3); + return accentColor.withValues(alpha: 0.3); }), thumbColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.selected)) { return Colors.white; } - return Colors.white.withOpacity(0.3); + return Colors.white.withValues(alpha: 0.3); }), ), dialogTheme: DialogTheme( @@ -103,7 +103,7 @@ class LiveKitTheme { color: LKColors.lkBlue, ), hintStyle: TextStyle( - color: LKColors.lkBlue.withOpacity(0.5), + color: LKColors.lkBlue.withValues(alpha: 0.5), ), enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, diff --git a/lib/src/ui/widgets/track/audio_visualizer_widget.dart b/lib/src/ui/widgets/track/audio_visualizer_widget.dart index 6a6b616..35b7b39 100644 --- a/lib/src/ui/widgets/track/audio_visualizer_widget.dart +++ b/lib/src/ui/widgets/track/audio_visualizer_widget.dart @@ -201,7 +201,7 @@ class _SoundWaveformWidgetState extends State : samples[i], width: widget.options.width, decoration: BoxDecoration( - color: widget.options.color.withOpacity(barOpacity), + color: widget.options.color.withValues(alpha: barOpacity), borderRadius: BorderRadius.circular(widget.options.cornerRadius), ), diff --git a/lib/src/ui/widgets/track/no_track_widget.dart b/lib/src/ui/widgets/track/no_track_widget.dart index 097f5ed..79ff023 100644 --- a/lib/src/ui/widgets/track/no_track_widget.dart +++ b/lib/src/ui/widgets/track/no_track_widget.dart @@ -25,8 +25,13 @@ import '../theme.dart'; class NoTrackWidget extends StatelessWidget { const NoTrackWidget({ super.key, + this.iconColor = LKColors.lkBlue, + this.backgroundColor = LKColors.lkDarkBlue, }); + final Color iconColor; + final Color backgroundColor; + @override Widget build(BuildContext context) { var trackCtx = Provider.of(context); @@ -51,12 +56,12 @@ class NoTrackWidget extends StatelessWidget { } return Container( - color: LKColors.lkDarkBlue, + color: backgroundColor, child: Center( child: LayoutBuilder( builder: (context, constraints) => Icon( icon, - color: LKColors.lkBlue, + color: iconColor, size: math.min(constraints.maxHeight, constraints.maxWidth) * 0.33, ), ), diff --git a/lib/src/ui/widgets/track/track_stats_widget.dart b/lib/src/ui/widgets/track/track_stats_widget.dart index 59c8af5..815f46c 100644 --- a/lib/src/ui/widgets/track/track_stats_widget.dart +++ b/lib/src/ui/widgets/track/track_stats_widget.dart @@ -26,7 +26,7 @@ class TrackStatsWidget extends StatelessWidget { children: [ trackCtx.showStatistics && stats.isNotEmpty ? Container( - color: Colors.black.withOpacity(0.3), + color: Colors.black.withValues(alpha: 0.3), padding: const EdgeInsets.symmetric( vertical: 8, horizontal: 8, From 344487e7b355027a55dfa6926e8e548633f4ad7e Mon Sep 17 00:00:00 2001 From: cloudwebrtc Date: Thu, 27 Mar 2025 20:06:53 +0800 Subject: [PATCH 08/13] update. --- lib/src/ui/widgets/room/media_device_select_button.dart | 1 - lib/src/ui/widgets/track/audio_visualizer_widget.dart | 1 - 2 files changed, 2 deletions(-) diff --git a/lib/src/ui/widgets/room/media_device_select_button.dart b/lib/src/ui/widgets/room/media_device_select_button.dart index 53a7a6b..b67cd14 100644 --- a/lib/src/ui/widgets/room/media_device_select_button.dart +++ b/lib/src/ui/widgets/room/media_device_select_button.dart @@ -15,7 +15,6 @@ import 'package:flutter/material.dart'; import 'package:livekit_client/livekit_client.dart'; -import 'package:livekit_components/livekit_components.dart'; import 'package:responsive_builder/responsive_builder.dart'; class MediaDeviceSelectWidget extends StatelessWidget { diff --git a/lib/src/ui/widgets/track/audio_visualizer_widget.dart b/lib/src/ui/widgets/track/audio_visualizer_widget.dart index 35b7b39..05f25c3 100644 --- a/lib/src/ui/widgets/track/audio_visualizer_widget.dart +++ b/lib/src/ui/widgets/track/audio_visualizer_widget.dart @@ -4,7 +4,6 @@ import 'package:livekit_client/livekit_client.dart'; import 'package:provider/provider.dart'; import '../../../context/track_reference_context.dart'; -import '../theme.dart'; import 'no_track_widget.dart'; class AudioVisualizerWidgetOptions { From e64cde65af892ce0ae23586e960aaba003dadc44 Mon Sep 17 00:00:00 2001 From: cloudwebrtc Date: Wed, 2 Apr 2025 16:21:02 +0800 Subject: [PATCH 09/13] update. --- lib/livekit_components.dart | 1 + lib/src/context/transcription_context.dart | 10 +--------- lib/src/types/transcription.dart | 11 +++++++++++ lib/src/ui/builder/room/transcription.dart | 2 +- .../widgets/participant/transcription_widget.dart | 13 ++++--------- .../ui/widgets/room/media_device_select_button.dart | 3 ++- 6 files changed, 20 insertions(+), 20 deletions(-) create mode 100644 lib/src/types/transcription.dart diff --git a/lib/livekit_components.dart b/lib/livekit_components.dart index 380a805..5d7e64a 100644 --- a/lib/livekit_components.dart +++ b/lib/livekit_components.dart @@ -22,6 +22,7 @@ export 'src/context/track_reference_context.dart'; export 'src/debug/logger.dart'; export 'src/types/agent_state.dart'; +export 'src/types/transcription.dart'; export 'src/ui/builder/camera_preview.dart'; export 'src/ui/builder/room/chat_toggle.dart'; diff --git a/lib/src/context/transcription_context.dart b/lib/src/context/transcription_context.dart index 3609f4b..a6e6464 100644 --- a/lib/src/context/transcription_context.dart +++ b/lib/src/context/transcription_context.dart @@ -18,15 +18,7 @@ import 'package:collection/collection.dart'; import 'package:livekit_client/livekit_client.dart'; import '../debug/logger.dart'; - -class TranscriptionForParticipant { - TranscriptionForParticipant( - this.segment, - this.participant, - ); - TranscriptionSegment segment; - final Participant participant; -} +import '../types/transcription.dart'; mixin TranscriptionContextMixin on ChangeNotifier { final List _transcriptions = []; diff --git a/lib/src/types/transcription.dart b/lib/src/types/transcription.dart new file mode 100644 index 0000000..74e8be8 --- /dev/null +++ b/lib/src/types/transcription.dart @@ -0,0 +1,11 @@ +import 'package:livekit_client/livekit_client.dart' + show Participant, TranscriptionSegment; + +class TranscriptionForParticipant { + TranscriptionForParticipant( + this.segment, + this.participant, + ); + TranscriptionSegment segment; + final Participant participant; +} diff --git a/lib/src/ui/builder/room/transcription.dart b/lib/src/ui/builder/room/transcription.dart index c794653..7f20aa7 100644 --- a/lib/src/ui/builder/room/transcription.dart +++ b/lib/src/ui/builder/room/transcription.dart @@ -17,7 +17,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../../context/room_context.dart'; -import '../../../context/transcription_context.dart'; +import '../../../types/transcription.dart'; class TranscriptionBuilder extends StatelessWidget { const TranscriptionBuilder({ diff --git a/lib/src/ui/widgets/participant/transcription_widget.dart b/lib/src/ui/widgets/participant/transcription_widget.dart index dfa2316..0beb1cf 100644 --- a/lib/src/ui/widgets/participant/transcription_widget.dart +++ b/lib/src/ui/widgets/participant/transcription_widget.dart @@ -17,7 +17,7 @@ import 'package:flutter/material.dart'; import 'package:chat_bubbles/chat_bubbles.dart'; import 'package:livekit_client/livekit_client.dart'; -import '../../../context/transcription_context.dart'; +import '../../../types/transcription.dart'; import '../theme.dart'; class TranscriptionWidget extends StatelessWidget { @@ -42,18 +42,13 @@ class TranscriptionWidget extends StatelessWidget { for (var transcription in sortedTranscriptions) { var participant = transcription.participant; var segment = transcription.segment; - /*msgWidgets.add( - CustomDateNameChip( - name: participant.identity, - date: segment.lastReceivedTime, - ), - );*/ + var isLocal = participant is LocalParticipant; msgWidgets.add( BubbleNormal( text: segment.text + (segment.isFinal ? '' : '...'), color: textColor, - tail: false, - isSender: participant is LocalParticipant, + tail: isLocal, + isSender: isLocal, ), ); } diff --git a/lib/src/ui/widgets/room/media_device_select_button.dart b/lib/src/ui/widgets/room/media_device_select_button.dart index b67cd14..5c2cc3d 100644 --- a/lib/src/ui/widgets/room/media_device_select_button.dart +++ b/lib/src/ui/widgets/room/media_device_select_button.dart @@ -58,6 +58,7 @@ class MediaDeviceSelectWidget extends StatelessWidget { @override Widget build(BuildContext context) { var deviceScreenType = getDeviceType(MediaQuery.of(context).size); + double edge = deviceScreenType == DeviceScreenType.mobile ? 12 : 20; return Row(mainAxisSize: MainAxisSize.min, children: [ SizedBox( height: 40, @@ -75,7 +76,7 @@ class MediaDeviceSelectWidget extends StatelessWidget { topLeft: Radius.circular(20.0), bottomLeft: Radius.circular(20.0)))), padding: WidgetStateProperty.all( - const EdgeInsets.fromLTRB(12, 20, 12, 20)), + EdgeInsets.fromLTRB(12, edge, 12, edge)), ), onPressed: () => onToggle?.call(!deviceIsOpened), child: Row( From 216cbca94f70e47bfe4b70dda4a9e16121732606 Mon Sep 17 00:00:00 2001 From: cloudwebrtc Date: Wed, 2 Apr 2025 16:22:59 +0800 Subject: [PATCH 10/13] dart run import_sorter:main --no-comments. --- lib/src/context/room_context.dart | 2 +- lib/src/context/transcription_context.dart | 2 +- lib/src/ui/builder/participant/participant_selector.dart | 5 +++-- lib/src/ui/widgets/room/camera_select_button.dart | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/src/context/room_context.dart b/lib/src/context/room_context.dart index b87f49f..ebd714b 100644 --- a/lib/src/context/room_context.dart +++ b/lib/src/context/room_context.dart @@ -17,9 +17,9 @@ import 'dart:async'; import 'package:flutter/material.dart' hide ConnectionState; import 'package:livekit_client/livekit_client.dart'; -import 'package:livekit_components/src/context/transcription_context.dart'; import 'package:provider/provider.dart'; +import 'package:livekit_components/src/context/transcription_context.dart'; import '../debug/logger.dart'; import 'chat_context.dart'; diff --git a/lib/src/context/transcription_context.dart b/lib/src/context/transcription_context.dart index a6e6464..4d6b237 100644 --- a/lib/src/context/transcription_context.dart +++ b/lib/src/context/transcription_context.dart @@ -13,8 +13,8 @@ // limitations under the License. import 'package:flutter/material.dart'; -import 'package:collection/collection.dart'; +import 'package:collection/collection.dart'; import 'package:livekit_client/livekit_client.dart'; import '../debug/logger.dart'; diff --git a/lib/src/ui/builder/participant/participant_selector.dart b/lib/src/ui/builder/participant/participant_selector.dart index 942f5fd..4cd59f4 100644 --- a/lib/src/ui/builder/participant/participant_selector.dart +++ b/lib/src/ui/builder/participant/participant_selector.dart @@ -1,9 +1,10 @@ import 'package:flutter/widgets.dart'; + +import 'package:collection/collection.dart'; import 'package:livekit_client/livekit_client.dart'; -import 'package:livekit_components/src/types/track_identifier.dart'; import 'package:provider/provider.dart'; -import 'package:collection/collection.dart'; +import 'package:livekit_components/src/types/track_identifier.dart'; import '../../../context/room_context.dart'; import '../../../debug/logger.dart'; import 'participant_track.dart'; diff --git a/lib/src/ui/widgets/room/camera_select_button.dart b/lib/src/ui/widgets/room/camera_select_button.dart index 8eff2f9..8916b81 100644 --- a/lib/src/ui/widgets/room/camera_select_button.dart +++ b/lib/src/ui/widgets/room/camera_select_button.dart @@ -13,8 +13,8 @@ // limitations under the License. import 'package:flutter/material.dart'; -import 'package:livekit_components/src/ui/widgets/theme.dart'; +import 'package:livekit_components/src/ui/widgets/theme.dart'; import '../../builder/room/media_device_select_button.dart'; import 'media_device_select_button.dart'; From 2a626332ff32f7bdaa711d73eb9a62d18a1c4a64 Mon Sep 17 00:00:00 2001 From: cloudwebrtc Date: Wed, 9 Apr 2025 21:35:12 +0800 Subject: [PATCH 11/13] update. --- example/lib/main.dart | 71 ++++++++++++++++--- example/pubspec.yaml | 8 +-- lib/src/ui/prejoin/prejoin.dart | 4 +- lib/src/ui/widgets/room/chat_toggle.dart | 6 +- .../ui/widgets/room/disconnect_button.dart | 6 +- .../room/media_device_select_button.dart | 63 ++++++++-------- .../ui/widgets/room/screenshare_toggle.dart | 6 +- pubspec.yaml | 4 +- 8 files changed, 108 insertions(+), 60 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 0761df9..1458caf 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -4,6 +4,7 @@ import 'package:livekit_components/livekit_components.dart'; import 'package:logging/logging.dart'; import 'package:intl/intl.dart'; import 'package:responsive_builder/responsive_builder.dart'; +import 'package:shared_preferences/shared_preferences.dart'; void main() { final format = DateFormat('HH:mm:ss'); @@ -33,18 +34,53 @@ class MyApp extends StatelessWidget { } } -class MyHomePage extends StatelessWidget { - const MyHomePage({super.key}); +class MyHomePage extends StatefulWidget { + // + const MyHomePage({ + super.key, + }); + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + static const _storeKeyUri = 'uri'; + static const _storeKeyToken = 'token'; + + String _url = ''; + String _token = ''; + + void _readPrefs() async { + final prefs = await SharedPreferences.getInstance(); + _url = const bool.hasEnvironment('URL') + ? const String.fromEnvironment('URL') + : prefs.getString(_storeKeyUri) ?? 'your url here'; + _token = const bool.hasEnvironment('TOKEN') + ? const String.fromEnvironment('TOKEN') + : prefs.getString(_storeKeyToken) ?? 'your token here'; + } - final url = 'wss://livekit.example.com'; - final token = 'your_token_here'; + // Save URL and Token + Future _writePrefs(String url, String token) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_storeKeyUri, url); + await prefs.setString(_storeKeyToken, token); + } + + @override + void initState() { + super.initState(); + _readPrefs(); + } /// handle join button pressed, fetch connection details and connect to room. // ignore: unused_element - void _onJoinPressed(RoomContext roomCtx, String name, String roomName) async { + void _onJoinPressed(RoomContext roomCtx, String url, String token) async { if (kDebugMode) { - print('Joining room: name=$name, roomName=$roomName'); + print('Joining room: name=$url, roomName=$token'); } + await _writePrefs(url, token); try { await roomCtx.connect( url: url, @@ -61,9 +97,22 @@ class MyHomePage extends StatelessWidget { Widget build(BuildContext context) { return LivekitRoom( roomContext: RoomContext( - url: url, - token: token, enableAudioVisulizer: true, + onConnected: () { + if (kDebugMode) { + print('Connected to room'); + } + }, + onDisconnected: () { + if (kDebugMode) { + print('Disconnected from room'); + } + }, + onError: (error) { + if (kDebugMode) { + print('Error: $error'); + } + }, ), builder: (context, roomCtx) { var deviceScreenType = getDeviceType(MediaQuery.of(context).size); @@ -82,8 +131,8 @@ class MyHomePage extends StatelessWidget { /// show prejoin screen if not connected ? Prejoin( - token: token, - url: url, + token: _token, + url: _url, onJoinPressed: _onJoinPressed, ) : @@ -110,7 +159,7 @@ class MyHomePage extends StatelessWidget { ), ) : Expanded( - flex: 5, + flex: 6, child: Stack( children: [ /* Expanded( diff --git a/example/pubspec.yaml b/example/pubspec.yaml index dfdae5b..066b9e9 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -2,7 +2,7 @@ name: example description: "A new Flutter project." # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +publish_to: "none" # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 @@ -30,7 +30,7 @@ environment: dependencies: flutter: sdk: flutter - + livekit_components: path: .. @@ -40,9 +40,10 @@ dependencies: intl: ^0.19.0 logging: ^1.2.0 provider: ^6.1.2 - fluttertoast: ^8.2.8 + fluttertoast: ^8.2.12 responsive_builder: ^0.7.1 http: ^1.2.2 + shared_preferences: ^2.5.3 dev_dependencies: flutter_test: @@ -60,7 +61,6 @@ dev_dependencies: # The following section is specific to Flutter packages. flutter: - # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. diff --git a/lib/src/ui/prejoin/prejoin.dart b/lib/src/ui/prejoin/prejoin.dart index ebd997b..daa2109 100644 --- a/lib/src/ui/prejoin/prejoin.dart +++ b/lib/src/ui/prejoin/prejoin.dart @@ -37,11 +37,11 @@ class Prejoin extends StatelessWidget { String url; void onTextTokenChanged(String token) async { - token = token; + this.token = token; } void onTextUrlChanged(String url) async { - url = url; + this.url = url; } void _handleJoinPressed(RoomContext roomCtx) async { diff --git a/lib/src/ui/widgets/room/chat_toggle.dart b/lib/src/ui/widgets/room/chat_toggle.dart index 53ad380..bc9c26d 100644 --- a/lib/src/ui/widgets/room/chat_toggle.dart +++ b/lib/src/ui/widgets/room/chat_toggle.dart @@ -59,9 +59,9 @@ class ChatToggleWidget extends StatelessWidget { ), ), padding: WidgetStateProperty.all( - deviceScreenType == DeviceScreenType.desktop || lkPlatformIsDesktop() - ? const EdgeInsets.fromLTRB(10, 20, 10, 20) - : const EdgeInsets.all(12), + (lkPlatformIsMobile() || lkPlatformIsWebMobile()) + ? const EdgeInsets.all(12) + : const EdgeInsets.fromLTRB(12, 20, 12, 20), ), ), onPressed: () => toggleChat(!isChatOpen), diff --git a/lib/src/ui/widgets/room/disconnect_button.dart b/lib/src/ui/widgets/room/disconnect_button.dart index 0574455..6d9c524 100644 --- a/lib/src/ui/widgets/room/disconnect_button.dart +++ b/lib/src/ui/widgets/room/disconnect_button.dart @@ -65,9 +65,9 @@ class DisconnectButtonWidget extends StatelessWidget { ), ), padding: WidgetStateProperty.all( - deviceScreenType == DeviceScreenType.desktop || lkPlatformIsDesktop() - ? const EdgeInsets.fromLTRB(10, 20, 10, 20) - : const EdgeInsets.all(12), + (lkPlatformIsMobile() || lkPlatformIsWebMobile()) + ? const EdgeInsets.all(12) + : const EdgeInsets.fromLTRB(12, 20, 12, 20), ), ), onPressed: () => diff --git a/lib/src/ui/widgets/room/media_device_select_button.dart b/lib/src/ui/widgets/room/media_device_select_button.dart index 5c2cc3d..ca6baaa 100644 --- a/lib/src/ui/widgets/room/media_device_select_button.dart +++ b/lib/src/ui/widgets/room/media_device_select_button.dart @@ -58,44 +58,43 @@ class MediaDeviceSelectWidget extends StatelessWidget { @override Widget build(BuildContext context) { var deviceScreenType = getDeviceType(MediaQuery.of(context).size); - double edge = deviceScreenType == DeviceScreenType.mobile ? 12 : 20; return Row(mainAxisSize: MainAxisSize.min, children: [ - SizedBox( - height: 40, - child: ElevatedButton( - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all(deviceIsOpened - ? selectedColor - : backgroundColor.withValues(alpha: 0.9)), - foregroundColor: WidgetStateProperty.all(foregroundColor), - overlayColor: WidgetStateProperty.all( - deviceIsOpened ? selectedOverlayColor : backgroundColor), - shadowColor: WidgetStateProperty.all(Colors.transparent), - shape: WidgetStateProperty.all(const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20.0), - bottomLeft: Radius.circular(20.0)))), - padding: WidgetStateProperty.all( - EdgeInsets.fromLTRB(12, edge, 12, edge)), - ), - onPressed: () => onToggle?.call(!deviceIsOpened), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(deviceIsOpened || defaultSelectable ? iconOn : iconOff, - color: iconColor), - const SizedBox(width: 2), - if (titleWidget != null && - (deviceScreenType != DeviceScreenType.mobile || - showTitleWidget)) - titleWidget!, - ], + ElevatedButton( + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all(deviceIsOpened + ? selectedColor + : backgroundColor.withValues(alpha: 0.9)), + foregroundColor: WidgetStateProperty.all(foregroundColor), + overlayColor: WidgetStateProperty.all( + deviceIsOpened ? selectedOverlayColor : backgroundColor), + shadowColor: WidgetStateProperty.all(Colors.transparent), + shape: WidgetStateProperty.all(const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20.0), + bottomLeft: Radius.circular(20.0)))), + padding: WidgetStateProperty.all( + (lkPlatformIsMobile() || lkPlatformIsWebMobile()) + ? const EdgeInsets.all(12) + : const EdgeInsets.fromLTRB(12, 20, 12, 20), ), ), + onPressed: () => onToggle?.call(!deviceIsOpened), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(deviceIsOpened || defaultSelectable ? iconOn : iconOff, + color: iconColor), + const SizedBox(width: 2), + if (titleWidget != null && + (deviceScreenType != DeviceScreenType.mobile || + showTitleWidget)) + titleWidget!, + ], + ), ), const SizedBox(width: 0.2), SizedBox( - height: 40, + height: 42, child: PopupMenuButton( icon: Icon( Icons.arrow_drop_down, diff --git a/lib/src/ui/widgets/room/screenshare_toggle.dart b/lib/src/ui/widgets/room/screenshare_toggle.dart index 308d22a..18b4809 100644 --- a/lib/src/ui/widgets/room/screenshare_toggle.dart +++ b/lib/src/ui/widgets/room/screenshare_toggle.dart @@ -64,9 +64,9 @@ class ScreenShareToggleWidget extends StatelessWidget { ), ), padding: WidgetStateProperty.all( - deviceScreenType == DeviceScreenType.desktop || lkPlatformIsDesktop() - ? const EdgeInsets.fromLTRB(10, 20, 10, 20) - : const EdgeInsets.all(12), + (lkPlatformIsMobile() || lkPlatformIsWebMobile()) + ? const EdgeInsets.all(12) + : const EdgeInsets.fromLTRB(12, 20, 12, 20), ), ), onPressed: () => screenShareEnabled diff --git a/pubspec.yaml b/pubspec.yaml index 9ddee23..3a58e48 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,11 +12,11 @@ dependencies: flutter: sdk: flutter flutter_background: ^1.3.0+1 - flutter_webrtc: ^0.12.12+hotfix.1 + flutter_webrtc: ^0.13.1+hotfix.1 google_fonts: ^6.2.1 http: ^1.2.2 intl: ^0.19.0 - livekit_client: ^2.4.1 + livekit_client: ^2.4.3 provider: ^6.1.2 responsive_builder: ^0.7.1 From 25db1b1df5258a940adff0b9ea5c518e83450fcd Mon Sep 17 00:00:00 2001 From: cloudwebrtc Date: Wed, 9 Apr 2025 21:59:30 +0800 Subject: [PATCH 12/13] fix. --- lib/src/context/room_context.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/context/room_context.dart b/lib/src/context/room_context.dart index ebd714b..ea64147 100644 --- a/lib/src/context/room_context.dart +++ b/lib/src/context/room_context.dart @@ -164,8 +164,8 @@ class RoomContext extends ChangeNotifier }) async { if (cameraOpened || microphoneOpened) { _fastConnectOptions = FastConnectOptions( - microphone: TrackOption(track: localAudioTrack!), - camera: TrackOption(track: localVideoTrack!), + microphone: TrackOption(track: localAudioTrack), + camera: TrackOption(track: localVideoTrack), ); await resetLocalTracks(); } From cb2b773edc7ddc1df8d1167c8141d9173fa526b7 Mon Sep 17 00:00:00 2001 From: cloudwebrtc Date: Wed, 9 Apr 2025 22:29:27 +0800 Subject: [PATCH 13/13] release: 1.2.1. --- CHANGELOG.md | 4 ++++ example/lib/main.dart | 2 +- pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27fd35d..d54f72c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 1.2.1 (2025-04-09) + +* feat: Adding more components for Agents. + ## 1.2.0 (2025-03-26) * Bump version for livekit sdk. diff --git a/example/lib/main.dart b/example/lib/main.dart index 1458caf..46e4108 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -78,7 +78,7 @@ class _MyHomePageState extends State { // ignore: unused_element void _onJoinPressed(RoomContext roomCtx, String url, String token) async { if (kDebugMode) { - print('Joining room: name=$url, roomName=$token'); + print('Joining room: url=$url, token=$token'); } await _writePrefs(url, token); try { diff --git a/pubspec.yaml b/pubspec.yaml index 3a58e48..c23967c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: livekit_components description: "Out-of-the-box flutter widgets for livekit client." -version: 1.2.0 +version: 1.2.1 homepage: https://github.com/livekit/components-flutter environment: