2024-04-27 05:12:26 +00:00
|
|
|
import 'dart:async';
|
|
|
|
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:flutter_background/flutter_background.dart';
|
|
|
|
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
|
|
|
import 'package:livekit_client/livekit_client.dart';
|
2024-04-27 12:10:15 +00:00
|
|
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
2024-04-27 05:12:26 +00:00
|
|
|
|
|
|
|
class ControlsWidget extends StatefulWidget {
|
|
|
|
final Room room;
|
|
|
|
final LocalParticipant participant;
|
|
|
|
|
|
|
|
const ControlsWidget(
|
|
|
|
this.room,
|
|
|
|
this.participant, {
|
|
|
|
super.key,
|
|
|
|
});
|
|
|
|
|
|
|
|
@override
|
|
|
|
State<StatefulWidget> createState() => _ControlsWidgetState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class _ControlsWidgetState extends State<ControlsWidget> {
|
|
|
|
CameraPosition position = CameraPosition.front;
|
|
|
|
|
|
|
|
List<MediaDevice>? _audioInputs;
|
|
|
|
List<MediaDevice>? _audioOutputs;
|
|
|
|
List<MediaDevice>? _videoInputs;
|
|
|
|
|
|
|
|
StreamSubscription? _subscription;
|
|
|
|
|
|
|
|
bool _speakerphoneOn = false;
|
|
|
|
|
|
|
|
@override
|
|
|
|
void initState() {
|
|
|
|
super.initState();
|
|
|
|
participant.addListener(onChange);
|
|
|
|
_subscription = Hardware.instance.onDeviceChange.stream.listen((List<MediaDevice> devices) {
|
|
|
|
revertDevices(devices);
|
|
|
|
});
|
|
|
|
Hardware.instance.enumerateDevices().then(revertDevices);
|
|
|
|
_speakerphoneOn = Hardware.instance.speakerOn ?? false;
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void dispose() {
|
|
|
|
_subscription?.cancel();
|
|
|
|
participant.removeListener(onChange);
|
|
|
|
super.dispose();
|
|
|
|
}
|
|
|
|
|
|
|
|
LocalParticipant get participant => widget.participant;
|
|
|
|
|
|
|
|
void revertDevices(List<MediaDevice> devices) async {
|
|
|
|
_audioInputs = devices.where((d) => d.kind == 'audioinput').toList();
|
|
|
|
_audioOutputs = devices.where((d) => d.kind == 'audiooutput').toList();
|
|
|
|
_videoInputs = devices.where((d) => d.kind == 'videoinput').toList();
|
|
|
|
setState(() {});
|
|
|
|
}
|
|
|
|
|
|
|
|
void onChange() => setState(() {});
|
|
|
|
|
|
|
|
bool get isMuted => participant.isMuted;
|
|
|
|
|
|
|
|
void disableAudio() async {
|
|
|
|
await participant.setMicrophoneEnabled(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> enableAudio() async {
|
|
|
|
await participant.setMicrophoneEnabled(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
void disableVideo() async {
|
|
|
|
await participant.setCameraEnabled(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
void enableVideo() async {
|
|
|
|
await participant.setCameraEnabled(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
void selectAudioOutput(MediaDevice device) async {
|
|
|
|
await widget.room.setAudioOutputDevice(device);
|
|
|
|
setState(() {});
|
|
|
|
}
|
|
|
|
|
|
|
|
void selectAudioInput(MediaDevice device) async {
|
|
|
|
await widget.room.setAudioInputDevice(device);
|
|
|
|
setState(() {});
|
|
|
|
}
|
|
|
|
|
|
|
|
void selectVideoInput(MediaDevice device) async {
|
|
|
|
await widget.room.setVideoInputDevice(device);
|
|
|
|
setState(() {});
|
|
|
|
}
|
|
|
|
|
|
|
|
void setSpeakerphoneOn() {
|
|
|
|
_speakerphoneOn = !_speakerphoneOn;
|
|
|
|
Hardware.instance.setSpeakerphoneOn(_speakerphoneOn);
|
|
|
|
setState(() {});
|
|
|
|
}
|
|
|
|
|
|
|
|
void toggleCamera() async {
|
|
|
|
final track = participant.videoTrackPublications.firstOrNull?.track;
|
|
|
|
if (track == null) return;
|
|
|
|
|
|
|
|
try {
|
|
|
|
final newPosition = position.switched();
|
|
|
|
await track.setCameraPosition(newPosition);
|
|
|
|
setState(() {
|
|
|
|
position = newPosition;
|
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void enableScreenShare() async {
|
|
|
|
if (lkPlatformIsDesktop()) {
|
|
|
|
try {
|
|
|
|
final source = await showDialog<DesktopCapturerSource>(
|
|
|
|
context: context,
|
|
|
|
builder: (context) => ScreenSelectDialog(),
|
|
|
|
);
|
|
|
|
if (source == null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
var track = await LocalVideoTrack.createScreenShareTrack(
|
|
|
|
ScreenShareCaptureOptions(
|
|
|
|
sourceId: source.id,
|
|
|
|
maxFrameRate: 15.0,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
await participant.publishVideoTrack(track);
|
|
|
|
} catch (e) {
|
|
|
|
final message = e.toString();
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
|
|
|
content: Text('Something went wrong... $message'),
|
|
|
|
));
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (lkPlatformIs(PlatformType.android)) {
|
|
|
|
requestBackgroundPermission([bool isRetry = false]) async {
|
|
|
|
try {
|
|
|
|
bool hasPermissions = await FlutterBackground.hasPermissions;
|
|
|
|
if (!isRetry) {
|
|
|
|
const androidConfig = FlutterBackgroundAndroidConfig(
|
|
|
|
notificationTitle: 'Screen Sharing',
|
|
|
|
notificationText: 'A Solar Messager\'s Call is sharing your screen',
|
|
|
|
notificationImportance: AndroidNotificationImportance.Default,
|
|
|
|
notificationIcon: AndroidResource(name: 'launcher_icon', defType: 'mipmap'),
|
|
|
|
);
|
|
|
|
hasPermissions = await FlutterBackground.initialize(androidConfig: androidConfig);
|
|
|
|
}
|
|
|
|
if (hasPermissions && !FlutterBackground.isBackgroundExecutionEnabled) {
|
|
|
|
await FlutterBackground.enableBackgroundExecution();
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
if (!isRetry) {
|
|
|
|
return await Future<void>.delayed(const Duration(seconds: 1), () => requestBackgroundPermission(true));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
await requestBackgroundPermission();
|
|
|
|
}
|
|
|
|
if (lkPlatformIs(PlatformType.iOS)) {
|
|
|
|
var track = await LocalVideoTrack.createScreenShareTrack(
|
|
|
|
const ScreenShareCaptureOptions(
|
|
|
|
useiOSBroadcastExtension: true,
|
|
|
|
maxFrameRate: 30.0,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
await participant.publishVideoTrack(track);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (lkPlatformIsWebMobile()) {
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
|
|
|
content: Text('Screen share is not supported mobile platform.'),
|
|
|
|
));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
await participant.setScreenShareEnabled(true, captureScreenAudio: true);
|
|
|
|
}
|
|
|
|
|
|
|
|
void disableScreenShare() async {
|
|
|
|
await participant.setScreenShareEnabled(false);
|
|
|
|
if (lkPlatformIs(PlatformType.android)) {
|
|
|
|
// Android specific
|
|
|
|
try {
|
|
|
|
await FlutterBackground.disableBackgroundExecution();
|
|
|
|
} catch (_) {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return Padding(
|
|
|
|
padding: const EdgeInsets.symmetric(
|
|
|
|
vertical: 15,
|
|
|
|
horizontal: 15,
|
|
|
|
),
|
|
|
|
child: Wrap(
|
|
|
|
alignment: WrapAlignment.center,
|
|
|
|
spacing: 5,
|
|
|
|
runSpacing: 5,
|
|
|
|
children: [
|
|
|
|
if (participant.isMicrophoneEnabled())
|
|
|
|
if (lkPlatformIs(PlatformType.android))
|
|
|
|
IconButton(
|
|
|
|
onPressed: disableAudio,
|
|
|
|
icon: const Icon(Icons.mic),
|
2024-04-27 12:10:15 +00:00
|
|
|
tooltip: AppLocalizations.of(context)!.chatCallMute,
|
2024-04-27 05:12:26 +00:00
|
|
|
)
|
|
|
|
else
|
|
|
|
PopupMenuButton<MediaDevice>(
|
|
|
|
icon: const Icon(Icons.settings_voice),
|
|
|
|
itemBuilder: (BuildContext context) {
|
|
|
|
return [
|
|
|
|
PopupMenuItem<MediaDevice>(
|
|
|
|
value: null,
|
|
|
|
onTap: isMuted ? enableAudio : disableAudio,
|
2024-04-27 12:10:15 +00:00
|
|
|
child: ListTile(
|
|
|
|
leading: const Icon(Icons.mic_off),
|
|
|
|
title: Text(AppLocalizations.of(context)!.chatCallMute),
|
2024-04-27 05:12:26 +00:00
|
|
|
),
|
|
|
|
),
|
|
|
|
if (_audioInputs != null)
|
|
|
|
..._audioInputs!.map((device) {
|
|
|
|
return PopupMenuItem<MediaDevice>(
|
|
|
|
value: device,
|
|
|
|
child: ListTile(
|
|
|
|
leading: (device.deviceId == widget.room.selectedAudioInputDeviceId)
|
|
|
|
? const Icon(Icons.check_box_outlined)
|
|
|
|
: const Icon(Icons.check_box_outline_blank),
|
|
|
|
title: Text(device.label),
|
|
|
|
),
|
|
|
|
onTap: () => selectAudioInput(device),
|
|
|
|
);
|
|
|
|
})
|
|
|
|
];
|
|
|
|
},
|
|
|
|
)
|
|
|
|
else
|
|
|
|
IconButton(
|
|
|
|
onPressed: enableAudio,
|
|
|
|
icon: const Icon(Icons.mic_off),
|
2024-04-27 12:10:15 +00:00
|
|
|
tooltip: AppLocalizations.of(context)!.chatCallUnMute,
|
2024-04-27 05:12:26 +00:00
|
|
|
),
|
|
|
|
if (!lkPlatformIs(PlatformType.iOS))
|
|
|
|
PopupMenuButton<MediaDevice>(
|
|
|
|
icon: const Icon(Icons.volume_up),
|
|
|
|
itemBuilder: (BuildContext context) {
|
|
|
|
return [
|
|
|
|
const PopupMenuItem<MediaDevice>(
|
|
|
|
value: null,
|
|
|
|
child: ListTile(
|
|
|
|
leading: Icon(Icons.speaker),
|
|
|
|
title: Text('Select Audio Output'),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
if (_audioOutputs != null)
|
|
|
|
..._audioOutputs!.map((device) {
|
|
|
|
return PopupMenuItem<MediaDevice>(
|
|
|
|
value: device,
|
|
|
|
child: ListTile(
|
|
|
|
leading: (device.deviceId == widget.room.selectedAudioOutputDeviceId)
|
|
|
|
? const Icon(Icons.check_box_outlined)
|
|
|
|
: const Icon(Icons.check_box_outline_blank),
|
|
|
|
title: Text(device.label),
|
|
|
|
),
|
|
|
|
onTap: () => selectAudioOutput(device),
|
|
|
|
);
|
|
|
|
})
|
|
|
|
];
|
|
|
|
},
|
|
|
|
),
|
|
|
|
if (!kIsWeb && lkPlatformIs(PlatformType.iOS))
|
|
|
|
IconButton(
|
|
|
|
disabledColor: Colors.grey,
|
|
|
|
onPressed: Hardware.instance.canSwitchSpeakerphone ? setSpeakerphoneOn : null,
|
|
|
|
icon: Icon(_speakerphoneOn ? Icons.speaker_phone : Icons.phone_android),
|
2024-04-27 12:10:15 +00:00
|
|
|
tooltip: AppLocalizations.of(context)!.chatCallChangeSpeaker,
|
2024-04-27 05:12:26 +00:00
|
|
|
),
|
|
|
|
if (participant.isCameraEnabled())
|
|
|
|
PopupMenuButton<MediaDevice>(
|
|
|
|
icon: const Icon(Icons.videocam_sharp),
|
|
|
|
itemBuilder: (BuildContext context) {
|
|
|
|
return [
|
|
|
|
PopupMenuItem<MediaDevice>(
|
|
|
|
value: null,
|
|
|
|
onTap: disableVideo,
|
2024-04-27 12:10:15 +00:00
|
|
|
child: ListTile(
|
|
|
|
leading: const Icon(Icons.videocam_off, color: Colors.white),
|
|
|
|
title: Text(AppLocalizations.of(context)!.chatCallVideoOff),
|
2024-04-27 05:12:26 +00:00
|
|
|
),
|
|
|
|
),
|
|
|
|
if (_videoInputs != null)
|
|
|
|
..._videoInputs!.map((device) {
|
|
|
|
return PopupMenuItem<MediaDevice>(
|
|
|
|
value: device,
|
|
|
|
child: ListTile(
|
|
|
|
leading: (device.deviceId == widget.room.selectedVideoInputDeviceId)
|
|
|
|
? const Icon(Icons.check_box_outlined)
|
|
|
|
: const Icon(Icons.check_box_outline_blank),
|
|
|
|
title: Text(device.label),
|
|
|
|
),
|
|
|
|
onTap: () => selectVideoInput(device),
|
|
|
|
);
|
|
|
|
})
|
|
|
|
];
|
|
|
|
},
|
|
|
|
)
|
|
|
|
else
|
|
|
|
IconButton(
|
|
|
|
onPressed: enableVideo,
|
|
|
|
icon: const Icon(Icons.videocam_off),
|
2024-04-27 12:10:15 +00:00
|
|
|
tooltip: AppLocalizations.of(context)!.chatCallVideoOn,
|
2024-04-27 05:12:26 +00:00
|
|
|
),
|
|
|
|
IconButton(
|
|
|
|
icon: Icon(position == CameraPosition.back ? Icons.video_camera_back : Icons.video_camera_front),
|
|
|
|
onPressed: () => toggleCamera(),
|
2024-04-27 12:10:15 +00:00
|
|
|
tooltip: AppLocalizations.of(context)!.chatCallVideoFlip,
|
2024-04-27 05:12:26 +00:00
|
|
|
),
|
|
|
|
if (participant.isScreenShareEnabled())
|
|
|
|
IconButton(
|
|
|
|
icon: const Icon(Icons.monitor_outlined),
|
|
|
|
onPressed: () => disableScreenShare(),
|
2024-04-27 12:10:15 +00:00
|
|
|
tooltip: AppLocalizations.of(context)!.chatCallScreenOff,
|
2024-04-27 05:12:26 +00:00
|
|
|
)
|
|
|
|
else
|
|
|
|
IconButton(
|
|
|
|
icon: const Icon(Icons.monitor),
|
|
|
|
onPressed: () => enableScreenShare(),
|
2024-04-27 12:10:15 +00:00
|
|
|
tooltip: AppLocalizations.of(context)!.chatCallScreenOn,
|
2024-04-27 05:12:26 +00:00
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|