373 lines
10 KiB
Dart
373 lines
10 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:livekit_client/livekit_client.dart';
|
|
import 'package:permission_handler/permission_handler.dart';
|
|
import 'package:solian/models/call.dart';
|
|
import 'package:solian/models/channel.dart';
|
|
import 'package:solian/providers/auth.dart';
|
|
import 'package:solian/screens/channel/call/call.dart';
|
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
|
|
|
class ChatCallProvider extends GetxController {
|
|
Rx<Call?> current = Rx(null);
|
|
Rx<Channel?> channel = Rx(null);
|
|
|
|
RxBool isReady = false.obs;
|
|
RxBool isMounted = false.obs;
|
|
|
|
String? token;
|
|
String? endpoint;
|
|
|
|
StreamSubscription? hwSubscription;
|
|
RxList audioInputs = [].obs;
|
|
RxList videoInputs = [].obs;
|
|
|
|
RxBool enableAudio = true.obs;
|
|
RxBool enableVideo = false.obs;
|
|
Rx<LocalAudioTrack?> audioTrack = Rx(null);
|
|
Rx<LocalVideoTrack?> videoTrack = Rx(null);
|
|
Rx<MediaDevice?> videoDevice = Rx(null);
|
|
Rx<MediaDevice?> audioDevice = Rx(null);
|
|
|
|
late Room room;
|
|
late EventsListener<RoomEvent> listener;
|
|
|
|
RxList participantTracks = [].obs;
|
|
Rx<ParticipantTrack?> focusTrack = Rx(null);
|
|
|
|
Future<void> checkPermissions() async {
|
|
if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) {
|
|
return;
|
|
}
|
|
|
|
await Permission.camera.request();
|
|
await Permission.microphone.request();
|
|
await Permission.bluetooth.request();
|
|
await Permission.bluetoothConnect.request();
|
|
}
|
|
|
|
void setCall(Call call, Channel related) {
|
|
current.value = call;
|
|
channel.value = related;
|
|
}
|
|
|
|
Future<(String, String)> getRoomToken() async {
|
|
final AuthProvider auth = Get.find();
|
|
if (!await auth.isAuthorized) throw Exception('unauthorized');
|
|
|
|
final client = auth.configureClient('messaging');
|
|
|
|
final resp = await client.post(
|
|
'/api/channels/global/${channel.value!.alias}/calls/ongoing/token',
|
|
{},
|
|
);
|
|
if (resp.statusCode == 200) {
|
|
token = resp.body['token'];
|
|
endpoint = 'wss://${resp.body['endpoint']}';
|
|
return (token!, endpoint!);
|
|
} else {
|
|
throw Exception(resp.bodyString);
|
|
}
|
|
}
|
|
|
|
void initHardware() {
|
|
if (isReady.value) {
|
|
return;
|
|
} else {
|
|
isReady.value = true;
|
|
}
|
|
|
|
hwSubscription = Hardware.instance.onDeviceChange.stream.listen(
|
|
revertDevices,
|
|
);
|
|
Hardware.instance.enumerateDevices().then(revertDevices);
|
|
}
|
|
|
|
void initRoom() {
|
|
initHardware();
|
|
room = Room();
|
|
listener = room.createListener();
|
|
WakelockPlus.enable();
|
|
}
|
|
|
|
void joinRoom(String url, String token) async {
|
|
if (isMounted.value) {
|
|
return;
|
|
} else {
|
|
isMounted.value = true;
|
|
}
|
|
|
|
try {
|
|
await room.connect(
|
|
url,
|
|
token,
|
|
roomOptions: const RoomOptions(
|
|
dynacast: true,
|
|
adaptiveStream: true,
|
|
defaultAudioPublishOptions: AudioPublishOptions(
|
|
name: 'call_voice',
|
|
stream: 'call_stream',
|
|
),
|
|
defaultVideoPublishOptions: VideoPublishOptions(
|
|
name: 'call_video',
|
|
stream: 'call_stream',
|
|
simulcast: true,
|
|
backupVideoCodec: BackupVideoCodec(enabled: true),
|
|
),
|
|
defaultScreenShareCaptureOptions: ScreenShareCaptureOptions(
|
|
useiOSBroadcastExtension: true,
|
|
params: VideoParametersPresets.screenShareH1080FPS30,
|
|
),
|
|
defaultCameraCaptureOptions: CameraCaptureOptions(
|
|
maxFrameRate: 30,
|
|
params: VideoParametersPresets.h1080_169,
|
|
),
|
|
),
|
|
fastConnectOptions: FastConnectOptions(
|
|
microphone: TrackOption(track: audioTrack.value),
|
|
camera: TrackOption(track: videoTrack.value),
|
|
),
|
|
);
|
|
} catch (e) {
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
void autoPublish() async {
|
|
try {
|
|
if (enableVideo.value) {
|
|
await room.localParticipant?.setCameraEnabled(true);
|
|
}
|
|
if (enableAudio.value) {
|
|
await room.localParticipant?.setMicrophoneEnabled(true);
|
|
}
|
|
} catch (error) {
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
void onRoomDidUpdate() => sortParticipants();
|
|
|
|
void setupRoom() {
|
|
sortParticipants();
|
|
room.addListener(onRoomDidUpdate);
|
|
WidgetsBindingCompatible.instance?.addPostFrameCallback(
|
|
(_) => autoPublish(),
|
|
);
|
|
|
|
if (lkPlatformIsMobile()) {
|
|
Hardware.instance.setSpeakerphoneOn(true);
|
|
}
|
|
}
|
|
|
|
void setupRoomListeners({
|
|
required Function(DisconnectReason?) onDisconnected,
|
|
}) {
|
|
listener
|
|
..on<RoomDisconnectedEvent>((event) async {
|
|
onDisconnected(event.reason);
|
|
})
|
|
..on<ParticipantEvent>((event) => sortParticipants())
|
|
..on<LocalTrackPublishedEvent>((_) => sortParticipants())
|
|
..on<LocalTrackUnpublishedEvent>((_) => sortParticipants())
|
|
..on<TrackSubscribedEvent>((_) => sortParticipants())
|
|
..on<TrackUnsubscribedEvent>((_) => sortParticipants())
|
|
..on<ParticipantNameUpdatedEvent>((event) {
|
|
sortParticipants();
|
|
});
|
|
}
|
|
|
|
void sortParticipants() {
|
|
Map<String, ParticipantTrack> mediaTracks = {};
|
|
for (var participant in room.remoteParticipants.values) {
|
|
mediaTracks[participant.sid] = ParticipantTrack(
|
|
participant: participant,
|
|
videoTrack: null,
|
|
isScreenShare: false,
|
|
);
|
|
|
|
for (var t in participant.videoTrackPublications) {
|
|
mediaTracks[participant.sid]?.videoTrack = t.track;
|
|
mediaTracks[participant.sid]?.isScreenShare = t.isScreenShare;
|
|
}
|
|
}
|
|
|
|
final newTracks = List.empty(growable: true);
|
|
|
|
final mediaTrackList = mediaTracks.values.toList();
|
|
mediaTrackList.sort((a, b) {
|
|
// Loudest people first
|
|
if (a.participant.isSpeaking && b.participant.isSpeaking) {
|
|
if (a.participant.audioLevel > b.participant.audioLevel) {
|
|
return -1;
|
|
} else {
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
// Last spoke first
|
|
final aSpokeAt = a.participant.lastSpokeAt?.millisecondsSinceEpoch ?? 0;
|
|
final bSpokeAt = b.participant.lastSpokeAt?.millisecondsSinceEpoch ?? 0;
|
|
|
|
if (aSpokeAt != bSpokeAt) {
|
|
return aSpokeAt > bSpokeAt ? -1 : 1;
|
|
}
|
|
|
|
// Has video first
|
|
if (a.participant.hasVideo != b.participant.hasVideo) {
|
|
return a.participant.hasVideo ? -1 : 1;
|
|
}
|
|
|
|
// First joined people first
|
|
return a.participant.joinedAt.millisecondsSinceEpoch -
|
|
b.participant.joinedAt.millisecondsSinceEpoch;
|
|
});
|
|
|
|
newTracks.addAll(mediaTrackList);
|
|
|
|
if (room.localParticipant != null) {
|
|
ParticipantTrack localTrack = ParticipantTrack(
|
|
participant: room.localParticipant!,
|
|
videoTrack: null,
|
|
isScreenShare: false,
|
|
);
|
|
|
|
final localParticipantTracks =
|
|
room.localParticipant?.videoTrackPublications;
|
|
if (localParticipantTracks != null) {
|
|
for (var t in localParticipantTracks) {
|
|
localTrack.videoTrack = t.track;
|
|
localTrack.isScreenShare = t.isScreenShare;
|
|
}
|
|
}
|
|
|
|
newTracks.add(localTrack);
|
|
}
|
|
|
|
participantTracks.value = newTracks;
|
|
if (focusTrack.value == null) {
|
|
focusTrack.value = participantTracks.firstOrNull;
|
|
} else {
|
|
final idx = participantTracks.indexWhere(
|
|
(x) => focusTrack.value!.participant.sid == x.participant.sid,
|
|
);
|
|
if (idx > -1) {
|
|
focusTrack.value = participantTracks[idx];
|
|
}
|
|
}
|
|
}
|
|
|
|
void revertDevices(List<MediaDevice> devices) async {
|
|
audioInputs.clear();
|
|
audioInputs.addAll(devices.where((d) => d.kind == 'audioinput'));
|
|
videoInputs.clear();
|
|
videoInputs.addAll(devices.where((d) => d.kind == 'videoinput'));
|
|
|
|
if (audioInputs.isNotEmpty) {
|
|
if (audioDevice.value == null && enableAudio.value) {
|
|
audioDevice.value = audioInputs.first;
|
|
Future.delayed(const Duration(milliseconds: 100), () async {
|
|
await changeLocalAudioTrack();
|
|
});
|
|
}
|
|
}
|
|
|
|
if (videoInputs.isNotEmpty) {
|
|
if (videoDevice.value == null && enableVideo.value) {
|
|
videoDevice.value = videoInputs.first;
|
|
Future.delayed(const Duration(milliseconds: 100), () async {
|
|
await changeLocalVideoTrack();
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> setEnableVideo(value) async {
|
|
enableVideo.value = value;
|
|
if (!enableVideo.value) {
|
|
await videoTrack.value?.stop();
|
|
videoTrack.value = null;
|
|
} else {
|
|
await changeLocalVideoTrack();
|
|
}
|
|
}
|
|
|
|
Future<void> setEnableAudio(value) async {
|
|
enableAudio.value = value;
|
|
if (!enableAudio.value) {
|
|
await audioTrack.value?.stop();
|
|
audioTrack.value = null;
|
|
} else {
|
|
await changeLocalAudioTrack();
|
|
}
|
|
}
|
|
|
|
Future<void> changeLocalAudioTrack() async {
|
|
if (audioTrack.value != null) {
|
|
await audioTrack.value!.stop();
|
|
audioTrack.value = null;
|
|
}
|
|
|
|
if (audioDevice.value != null) {
|
|
audioTrack.value = await LocalAudioTrack.create(
|
|
AudioCaptureOptions(
|
|
deviceId: audioDevice.value!.deviceId,
|
|
),
|
|
);
|
|
await audioTrack.value!.start();
|
|
}
|
|
}
|
|
|
|
Future<void> changeLocalVideoTrack() async {
|
|
if (videoTrack.value != null) {
|
|
await videoTrack.value!.stop();
|
|
videoTrack.value = null;
|
|
}
|
|
|
|
if (videoDevice.value != null) {
|
|
videoTrack.value = await LocalVideoTrack.createCameraTrack(
|
|
CameraCaptureOptions(
|
|
deviceId: videoDevice.value!.deviceId,
|
|
params: VideoParametersPresets.h1080_169,
|
|
),
|
|
);
|
|
await videoTrack.value!.start();
|
|
}
|
|
}
|
|
|
|
void changeFocusTrack(ParticipantTrack track) {
|
|
focusTrack.value = track;
|
|
}
|
|
|
|
Future gotoScreen(BuildContext context) {
|
|
return Navigator.of(context, rootNavigator: true).push(
|
|
MaterialPageRoute(builder: (context) => const CallScreen()),
|
|
);
|
|
}
|
|
|
|
void deactivateHardware() {
|
|
hwSubscription?.cancel();
|
|
}
|
|
|
|
void disposeRoom() {
|
|
isMounted.value = false;
|
|
current.value = null;
|
|
channel.value = null;
|
|
room.removeListener(onRoomDidUpdate);
|
|
room.disconnect();
|
|
room.dispose();
|
|
listener.dispose();
|
|
WakelockPlus.disable();
|
|
}
|
|
|
|
void disposeHardware() {
|
|
isReady.value = false;
|
|
audioTrack.value?.stop();
|
|
audioTrack.value = null;
|
|
videoTrack.value?.stop();
|
|
videoTrack.value = null;
|
|
}
|
|
}
|