diff --git a/lib/src/context/media_device_context.dart b/lib/src/context/media_device_context.dart index 9898ccf..93e64f3 100644 --- a/lib/src/context/media_device_context.dart +++ b/lib/src/context/media_device_context.dart @@ -26,8 +26,6 @@ class MediaDeviceContext extends ChangeNotifier { final RoomContext _roomCtx; final Room? _room; - CameraPosition position = CameraPosition.front; - List? _audioInputs; List? _audioOutputs; List? _videoInputs; @@ -102,7 +100,7 @@ class MediaDeviceContext extends ChangeNotifier { await _roomCtx.localVideoTrack?.dispose(); _roomCtx.localVideoTrack = await LocalVideoTrack.createCameraTrack( CameraCaptureOptions( - cameraPosition: position, + cameraPosition: currentPosition ?? CameraPosition.front, deviceId: device.deviceId, ), ); @@ -152,7 +150,7 @@ class MediaDeviceContext extends ChangeNotifier { } else { _roomCtx.localVideoTrack ??= await LocalVideoTrack.createCameraTrack( CameraCaptureOptions( - cameraPosition: position, + cameraPosition: currentPosition ?? CameraPosition.front, deviceId: selectedVideoInputDeviceId, ), ); @@ -264,4 +262,39 @@ class MediaDeviceContext extends ChangeNotifier { await _room?.localParticipant?.setScreenShareEnabled(false); notifyListeners(); } + + bool get canSwitchSpeakerphone => Hardware.instance.canSwitchSpeakerphone; + + bool? get isSpeakerOn => Hardware.instance.speakerOn; + + void setSpeakerphoneOn(bool speakerOn) async { + if (lkPlatformIs(PlatformType.iOS)) { + if (!speakerOn && Hardware.instance.preferSpeakerOutput) { + await Hardware.instance.setPreferSpeakerOutput(false); + } + } + await Hardware.instance.setSpeakerphoneOn(speakerOn); + notifyListeners(); + } + + CameraPosition? get currentPosition { + 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; + if (track == null) return; + + try { + await track.setCameraPosition(newPosition); + } catch (error) { + print('could not restart track: $error'); + return; + } + notifyListeners(); + } } diff --git a/lib/src/ui/builder/room/camera_switch.dart b/lib/src/ui/builder/room/camera_switch.dart new file mode 100644 index 0000000..7d2eae7 --- /dev/null +++ b/lib/src/ui/builder/room/camera_switch.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +import 'package:livekit_client/livekit_client.dart'; +import 'package:provider/provider.dart'; + +import '../../../context/media_device_context.dart'; +import '../../../context/room_context.dart'; + +class CameraSwitch extends StatelessWidget { + const CameraSwitch({ + super.key, + required this.builder, + }); + + final Function(BuildContext context, RoomContext roomCtx, + MediaDeviceContext deviceCtx, CameraPosition? position) builder; + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, roomCtx, child) { + return Consumer( + builder: (context, deviceCtx, child) { + return Selector( + selector: (context, position) => deviceCtx.currentPosition, + builder: (context, position, child) => builder( + context, + roomCtx, + deviceCtx, + position, + ), + ); + }, + ); + }, + ); + } +} diff --git a/lib/src/ui/builder/room/speaker_switch.dart b/lib/src/ui/builder/room/speaker_switch.dart new file mode 100644 index 0000000..86efef6 --- /dev/null +++ b/lib/src/ui/builder/room/speaker_switch.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import '../../../context/media_device_context.dart'; +import '../../../context/room_context.dart'; + +class SpeakerSwitch extends StatelessWidget { + const SpeakerSwitch({ + super.key, + required this.builder, + }); + + final Function(BuildContext context, RoomContext roomCtx, + MediaDeviceContext deviceCtx, bool? isSpeakerOn) builder; + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, roomCtx, child) { + return Consumer( + builder: (context, deviceCtx, child) { + return Selector( + selector: (context, isSpeakerOn) => deviceCtx.isSpeakerOn, + builder: (context, isSpeakerOn, child) => builder( + context, + roomCtx, + deviceCtx, + isSpeakerOn, + ), + ); + }, + ); + }, + ); + } +} diff --git a/lib/src/ui/widgets/room/camera_switch_button.dart b/lib/src/ui/widgets/room/camera_switch_button.dart new file mode 100644 index 0000000..79efd23 --- /dev/null +++ b/lib/src/ui/widgets/room/camera_switch_button.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; + +import 'package:livekit_client/livekit_client.dart'; + +class CameraSwitchButton extends StatelessWidget { + const CameraSwitchButton({ + super.key, + this.currentPosition = CameraPosition.front, + this.onToggle, + this.disabled = false, + }); + + final CameraPosition? currentPosition; + final Function(CameraPosition position)? onToggle; + final bool disabled; + + @override + Widget build(BuildContext context) { + return ElevatedButton( + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all(Colors.grey.withOpacity(0.6)), + foregroundColor: WidgetStateProperty.all(Colors.white), + overlayColor: WidgetStateProperty.all(Colors.grey), + shape: WidgetStateProperty.all(const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(20.0)))), + padding: WidgetStateProperty.all( + const EdgeInsets.all(12), + ), + ), + onPressed: () => onToggle?.call(currentPosition == CameraPosition.front + ? CameraPosition.back + : CameraPosition.front), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(currentPosition == CameraPosition.back + ? Icons.video_camera_back + : Icons.video_camera_front), + ], + ), + ); + } +} diff --git a/lib/src/ui/widgets/room/control_bar.dart b/lib/src/ui/widgets/room/control_bar.dart index 0352b49..37a28dd 100644 --- a/lib/src/ui/widgets/room/control_bar.dart +++ b/lib/src/ui/widgets/room/control_bar.dart @@ -2,10 +2,14 @@ import 'package:flutter/material.dart'; import 'package:livekit_client/livekit_client.dart'; +import 'package:livekit_components/src/ui/builder/room/camera_switch.dart'; +import 'package:livekit_components/src/ui/widgets/room/speaker_switch_button.dart'; import '../../builder/room/chat_toggle.dart'; 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 'camera_switch_button.dart'; import 'chat_toggle.dart'; import 'disconnect_button.dart'; import 'media_device_select_button.dart'; @@ -33,6 +37,8 @@ class ControlBar extends StatelessWidget { final bool settings; final bool showLabels; + bool get isMobile => lkPlatformIsMobile(); + @override Widget build(BuildContext context) { return Padding( @@ -92,6 +98,22 @@ class ControlBar extends StatelessWidget { showLabel: showLabels, ), ), + if (isMobile && microphone) + SpeakerSwitch( + builder: (context, roomCtx, deviceCtx, isSpeakerOn) => + SpeakerSwitchButton( + isSpeakerOn: isSpeakerOn ?? false, + onToggle: (speakerOn) => + deviceCtx.setSpeakerphoneOn(speakerOn), + )), + if (isMobile && camera) + CameraSwitch( + builder: (context, roomCtx, deviceCtx, position) => + CameraSwitchButton( + currentPosition: position, + onToggle: (newPosition) => + deviceCtx.switchCamera(newPosition), + )), if (screenShare) ScreenShareToggle( builder: (context, roomCtx, deviceCtx, screenShareEnabled) => diff --git a/lib/src/ui/widgets/room/speaker_switch_button.dart b/lib/src/ui/widgets/room/speaker_switch_button.dart new file mode 100644 index 0000000..28a3ee8 --- /dev/null +++ b/lib/src/ui/widgets/room/speaker_switch_button.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; + +class SpeakerSwitchButton extends StatelessWidget { + const SpeakerSwitchButton({ + super.key, + this.isSpeakerOn = false, + this.onToggle, + this.disabled = false, + }); + + final bool isSpeakerOn; + final Function(bool speakerOn)? onToggle; + final bool disabled; + + @override + Widget build(BuildContext context) { + return ElevatedButton( + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all(Colors.grey.withOpacity(0.6)), + foregroundColor: WidgetStateProperty.all(Colors.white), + overlayColor: WidgetStateProperty.all(Colors.grey), + shape: WidgetStateProperty.all(const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(20.0)))), + padding: WidgetStateProperty.all(const EdgeInsets.all(12)), + ), + onPressed: () => onToggle?.call(!isSpeakerOn), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(isSpeakerOn ? Icons.speaker_phone : Icons.phone_android), + ], + ), + ); + } +}