diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 6847ba1..299c0ab 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -8,7 +8,7 @@ plugins { android { namespace = "io.livekit.fluttercomponents.example.example" compileSdk = flutter.compileSdkVersion - ndkVersion = flutter.ndkVersion + ndkVersion = "27.0.12077973" compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 diff --git a/lib/livekit_components.dart b/lib/livekit_components.dart index 5d7e64a..0914b11 100644 --- a/lib/livekit_components.dart +++ b/lib/livekit_components.dart @@ -15,73 +15,64 @@ library livekit_components; export 'src/context/chat_context.dart'; -export 'src/context/room_context.dart'; +export 'src/context/media_device_context.dart'; export 'src/context/participant_context.dart'; +export 'src/context/room_context.dart'; 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'; +export 'src/ui/builder/participant/participant_attributes.dart'; +export 'src/ui/builder/participant/participant_kind.dart'; +export 'src/ui/builder/participant/participant_loop.dart'; +export 'src/ui/builder/participant/participant_metadata.dart'; +export 'src/ui/builder/participant/participant_muted_indicator.dart'; +export 'src/ui/builder/participant/participant_name.dart'; +export 'src/ui/builder/participant/participant_permissions.dart'; +export 'src/ui/builder/participant/participant_selector.dart'; +export 'src/ui/builder/participant/participant_track.dart'; +export 'src/ui/builder/participant/participant_transcription.dart'; export 'src/ui/builder/room/chat.dart'; +export 'src/ui/builder/room/chat_toggle.dart'; export 'src/ui/builder/room/disconnect_button.dart'; export 'src/ui/builder/room/join_button.dart'; -export 'src/ui/builder/room/media_device_select_button.dart'; export 'src/ui/builder/room/media_device.dart'; +export 'src/ui/builder/room/media_device_select_button.dart'; +export 'src/ui/builder/room/room.dart'; +export 'src/ui/builder/room/room_active_recording_indicator.dart'; export 'src/ui/builder/room/room_connection_state.dart'; export 'src/ui/builder/room/room_metadata.dart'; export 'src/ui/builder/room/room_name.dart'; 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'; -export 'src/ui/builder/participant/participant_loop.dart'; -export 'src/ui/builder/participant/participant_metadata.dart'; -export 'src/ui/builder/participant/participant_muted_indicator.dart'; -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'; export 'src/ui/builder/track/is_speaking_indicator.dart'; - +export 'src/ui/layout/carousel_layout.dart'; +export 'src/ui/layout/grid_layout.dart'; +export 'src/ui/layout/layouts.dart'; +export 'src/ui/prejoin/prejoin.dart'; +export 'src/ui/widgets/camera_preview.dart'; export 'src/ui/widgets/participant/connection_quality_indicator.dart'; +export 'src/ui/widgets/participant/is_speaking_indicator.dart'; export 'src/ui/widgets/participant/participant_status_bar.dart'; export 'src/ui/widgets/participant/participant_tile_widget.dart'; -export 'src/ui/widgets/participant/is_speaking_indicator.dart'; - +export 'src/ui/widgets/participant/transcription_widget.dart'; export 'src/ui/widgets/room/camera_select_button.dart'; export 'src/ui/widgets/room/chat_toggle.dart'; export 'src/ui/widgets/room/chat_widget.dart'; -export 'src/ui/widgets/room/media_device_select_button.dart'; -export 'src/ui/widgets/room/microphone_select_button.dart'; +export 'src/ui/widgets/room/clear_pin_button.dart'; export 'src/ui/widgets/room/control_bar.dart'; export 'src/ui/widgets/room/disconnect_button.dart'; +export 'src/ui/widgets/room/media_device_select_button.dart'; +export 'src/ui/widgets/room/microphone_select_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/theme.dart'; +export 'src/ui/widgets/toast.dart'; export 'src/ui/widgets/track/audio_visualizer_widget.dart'; export 'src/ui/widgets/track/focus_toggle.dart'; export 'src/ui/widgets/track/no_track_widget.dart'; -export 'src/ui/widgets/track/video_track_widget.dart'; export 'src/ui/widgets/track/track_stats_widget.dart'; -export 'src/ui/widgets/theme.dart'; -export 'src/ui/widgets/toast.dart'; -export 'src/ui/widgets/camera_preview.dart'; - -export 'src/ui/layout/layouts.dart'; -export 'src/ui/layout/grid_layout.dart'; -export 'src/ui/layout/carousel_layout.dart'; - -export 'src/ui/prejoin/prejoin.dart'; +export 'src/ui/widgets/track/video_track_widget.dart'; diff --git a/lib/src/context/media_device_context.dart b/lib/src/context/media_device_context.dart index e8800ba..fa7e96e 100644 --- a/lib/src/context/media_device_context.dart +++ b/lib/src/context/media_device_context.dart @@ -66,8 +66,7 @@ class MediaDeviceContext extends ChangeNotifier { Future loadDevices() async { _loadDevices(await Hardware.instance.enumerateDevices()); - _deviceChangeSub = - Hardware.instance.onDeviceChange.stream.listen(_loadDevices); + _deviceChangeSub = Hardware.instance.onDeviceChange.stream.listen(_loadDevices); } _loadDevices(List devices) { @@ -76,14 +75,11 @@ class MediaDeviceContext extends ChangeNotifier { _videoInputs = devices.where((d) => d.kind == 'videoinput').toList(); selectedAudioInputDeviceId ??= - Hardware.instance.selectedAudioInput?.deviceId ?? - _audioInputs?.firstOrNull?.deviceId; + Hardware.instance.selectedAudioInput?.deviceId ?? _audioInputs?.firstOrNull?.deviceId; selectedVideoInputDeviceId ??= - Hardware.instance.selectedVideoInput?.deviceId ?? - _videoInputs?.firstOrNull?.deviceId; + Hardware.instance.selectedVideoInput?.deviceId ?? _videoInputs?.firstOrNull?.deviceId; selectedAudioOutputDeviceId ??= - Hardware.instance.selectedAudioOutput?.deviceId ?? - _audioOutputs?.firstOrNull?.deviceId; + Hardware.instance.selectedAudioOutput?.deviceId ?? _audioOutputs?.firstOrNull?.deviceId; notifyListeners(); } @@ -189,8 +185,7 @@ class MediaDeviceContext extends ChangeNotifier { notifyListeners(); } - bool get isScreenShareEnabled => - _room?.localParticipant?.isScreenShareEnabled() ?? false; + bool get isScreenShareEnabled => _room?.localParticipant?.isScreenShareEnabled() ?? false; Future enableScreenShare(context) async { if (lkPlatformIsDesktop()) { @@ -232,20 +227,16 @@ class MediaDeviceContext extends ChangeNotifier { notificationTitle: 'Screen Sharing', notificationText: 'LiveKit Example is sharing the screen.', notificationImportance: AndroidNotificationImportance.normal, - notificationIcon: AndroidResource( - name: 'livekit_ic_launcher', defType: 'mipmap'), + notificationIcon: AndroidResource(name: 'livekit_ic_launcher', defType: 'mipmap'), ); - hasPermissions = await FlutterBackground.initialize( - androidConfig: androidConfig); + hasPermissions = await FlutterBackground.initialize(androidConfig: androidConfig); } - if (hasPermissions && - !FlutterBackground.isBackgroundExecutionEnabled) { + if (hasPermissions && !FlutterBackground.isBackgroundExecutionEnabled) { await FlutterBackground.enableBackgroundExecution(); } } catch (e) { if (!isRetry) { - return await Future.delayed(const Duration(seconds: 1), - () => requestBackgroundPermission(true)); + return await Future.delayed(const Duration(seconds: 1), () => requestBackgroundPermission(true)); } Debug.log('could not publish video: $e'); } @@ -265,13 +256,11 @@ class MediaDeviceContext extends ChangeNotifier { } if (lkPlatformIsWebMobile()) { - await context - .showErrorDialog('Screen share is not supported on mobile web'); + await context.showErrorDialog('Screen share is not supported on mobile web'); return; } - await _room?.localParticipant - ?.setScreenShareEnabled(true, captureScreenAudio: true); + await _room?.localParticipant?.setScreenShareEnabled(true, captureScreenAudio: true); notifyListeners(); } @@ -284,23 +273,19 @@ class MediaDeviceContext extends ChangeNotifier { bool? get isSpeakerOn => Hardware.instance.speakerOn; - void setSpeakerphoneOn(bool speakerOn, - {bool forceSpeakerOutput = false}) async { - await Hardware.instance - .setSpeakerphoneOn(speakerOn, forceSpeakerOutput: forceSpeakerOutput); + void setSpeakerphoneOn(bool speakerOn, {bool forceSpeakerOutput = false}) async { + await Hardware.instance.setSpeakerphoneOn(speakerOn, forceSpeakerOutput: forceSpeakerOutput); notifyListeners(); } CameraPosition? get currentPosition { - final track = - _room?.localParticipant?.videoTrackPublications.firstOrNull?.track; + final track = _room?.localParticipant?.videoTrackPublications.firstOrNull?.track; if (track == null) return null; return (track.currentOptions as CameraCaptureOptions).cameraPosition; } - void switchCamera(CameraPosition newPosition) async { - final track = - _room?.localParticipant?.videoTrackPublications.firstOrNull?.track; + void switchCameraPosition(CameraPosition newPosition) async { + final track = _room?.localParticipant?.videoTrackPublications.firstOrNull?.track; if (track == null) return; try { @@ -312,6 +297,23 @@ class MediaDeviceContext extends ChangeNotifier { notifyListeners(); } + void toggleCameraPosition() async { + final track = _room?.localParticipant?.videoTrackPublications.firstOrNull?.track; + if (track == null) return; + final currentPosition = (track.currentOptions as CameraCaptureOptions).cameraPosition; + + final newPosition = currentPosition == CameraPosition.back ? CameraPosition.front : CameraPosition.back; + + try { + await track.setCameraPosition(newPosition); + } catch (error) { + print('could not restart track: $error'); + return; + } + + notifyListeners(); + } + @override void dispose() { super.dispose(); diff --git a/lib/src/context/participant_context.dart b/lib/src/context/participant_context.dart index 85581cb..12a2b44 100644 --- a/lib/src/context/participant_context.dart +++ b/lib/src/context/participant_context.dart @@ -94,7 +94,7 @@ class ParticipantContext extends ChangeNotifier { ..on((event) { if (event.participant.identity == identity && event.publication.kind == TrackType.AUDIO) { - Debug.event('TrackContext: TrackMutedEvent for ${_participant.sid}'); + Debug.event('ParticipantContext: TrackMutedEvent for ${_participant.sid}'); notifyListeners(); } }) @@ -102,9 +102,17 @@ class ParticipantContext extends ChangeNotifier { if (event.participant.identity == identity && event.publication.kind == TrackType.AUDIO) { Debug.event( - 'TrackContext: TrackUnmutedEvent for ${_participant.sid}'); + 'ParticipantContext: TrackUnmutedEvent for ${_participant.sid}'); notifyListeners(); } + }) + ..on((event) { + Debug.event('ParticipantContext: TrackPublishedEvent for ${_participant.sid}'); + notifyListeners(); + }) + ..on((event) { + Debug.event('ParticipantContext: TrackUnpublishedEvent for ${_participant.sid}'); + notifyListeners(); }); _metadata = _participant.metadata; diff --git a/lib/src/ui/widgets/room/control_bar.dart b/lib/src/ui/widgets/room/control_bar.dart index 06f0e60..21c96dc 100644 --- a/lib/src/ui/widgets/room/control_bar.dart +++ b/lib/src/ui/widgets/room/control_bar.dart @@ -162,7 +162,7 @@ class ControlBar extends StatelessWidget { CameraSwitchButton( currentPosition: position, onToggle: (newPosition) => - deviceCtx.switchCamera(newPosition), + deviceCtx.switchCameraPosition(newPosition), )), if (screenShare) ScreenShareToggle( diff --git a/lib/src/ui/widgets/track/video_track_widget.dart b/lib/src/ui/widgets/track/video_track_widget.dart index 1987cb9..8b9203e 100644 --- a/lib/src/ui/widgets/track/video_track_widget.dart +++ b/lib/src/ui/widgets/track/video_track_widget.dart @@ -13,20 +13,28 @@ // limitations under the License. import 'package:flutter/material.dart'; - -import 'package:livekit_client/livekit_client.dart'; +import 'package:livekit_client/livekit_client.dart' as sdk; import 'package:provider/provider.dart'; import '../../../context/track_reference_context.dart'; import '../../../debug/logger.dart'; -import '../theme.dart'; import 'no_track_widget.dart'; class VideoTrackWidget extends StatelessWidget { + final sdk.VideoViewFit fit; + final WidgetBuilder? noTrackBuilder; + const VideoTrackWidget({ super.key, + this.fit = sdk.VideoViewFit.contain, + this.noTrackBuilder, }); + Widget _buildNoTrack(BuildContext ctx) { + if (noTrackBuilder != null) return noTrackBuilder!(ctx); + return const NoTrackWidget(); + } + @override Widget build(BuildContext context) { var trackCtx = Provider.of(context); @@ -39,14 +47,14 @@ class VideoTrackWidget extends StatelessWidget { } return Selector( - selector: (context, isMuted) => trackCtx.isMuted, - builder: (BuildContext context, isMuted, child) => !isMuted && - trackCtx.videoTrack != null - ? Container( - color: LKColors.lkDarkBlue, - child: - VideoTrackRenderer(trackCtx.videoTrack!, key: ValueKey(sid))) - : const NoTrackWidget(), + selector: (ctx, isMuted) => trackCtx.isMuted, + builder: (BuildContext ctx, isMuted, child) => !isMuted && trackCtx.videoTrack != null + ? sdk.VideoTrackRenderer( + trackCtx.videoTrack!, + key: ValueKey(sid), + fit: fit, + ) + : _buildNoTrack(ctx), ); } }