Skip to content

Commit

Permalink
feat: add CameraSwith/SpeakerSwitch components.
Browse files Browse the repository at this point in the history
  • Loading branch information
cloudwebrtc committed Oct 25, 2024
1 parent 61b8708 commit 36b673d
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 4 deletions.
41 changes: 37 additions & 4 deletions lib/src/context/media_device_context.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ class MediaDeviceContext extends ChangeNotifier {
final RoomContext _roomCtx;
final Room? _room;

CameraPosition position = CameraPosition.front;

List<MediaDevice>? _audioInputs;
List<MediaDevice>? _audioOutputs;
List<MediaDevice>? _videoInputs;
Expand Down Expand Up @@ -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,
),
);
Expand Down Expand Up @@ -152,7 +150,7 @@ class MediaDeviceContext extends ChangeNotifier {
} else {
_roomCtx.localVideoTrack ??= await LocalVideoTrack.createCameraTrack(
CameraCaptureOptions(
cameraPosition: position,
cameraPosition: currentPosition ?? CameraPosition.front,
deviceId: selectedVideoInputDeviceId,
),
);
Expand Down Expand Up @@ -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();
}
}
38 changes: 38 additions & 0 deletions lib/src/ui/builder/room/camera_switch.dart
Original file line number Diff line number Diff line change
@@ -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<RoomContext>(
builder: (context, roomCtx, child) {
return Consumer<MediaDeviceContext>(
builder: (context, deviceCtx, child) {
return Selector<MediaDeviceContext, CameraPosition?>(
selector: (context, position) => deviceCtx.currentPosition,
builder: (context, position, child) => builder(
context,
roomCtx,
deviceCtx,
position,
),
);
},
);
},
);
}
}
37 changes: 37 additions & 0 deletions lib/src/ui/builder/room/speaker_switch.dart
Original file line number Diff line number Diff line change
@@ -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<RoomContext>(
builder: (context, roomCtx, child) {
return Consumer<MediaDeviceContext>(
builder: (context, deviceCtx, child) {
return Selector<MediaDeviceContext, bool?>(
selector: (context, isSpeakerOn) => deviceCtx.isSpeakerOn,
builder: (context, isSpeakerOn, child) => builder(
context,
roomCtx,
deviceCtx,
isSpeakerOn,
),
);
},
);
},
);
}
}
43 changes: 43 additions & 0 deletions lib/src/ui/widgets/room/camera_switch_button.dart
Original file line number Diff line number Diff line change
@@ -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),
],
),
);
}
}
22 changes: 22 additions & 0 deletions lib/src/ui/widgets/room/control_bar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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) =>
Expand Down
35 changes: 35 additions & 0 deletions lib/src/ui/widgets/room/speaker_switch_button.dart
Original file line number Diff line number Diff line change
@@ -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),
],
),
);
}
}

0 comments on commit 36b673d

Please sign in to comment.