♻️ Trying out the new built-in webrtc
This commit is contained in:
@@ -2,14 +2,13 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/widgets/chat/call_button.dart';
|
||||
import 'package:livekit_client/livekit_client.dart' as lk;
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/models/chat.dart';
|
||||
import 'package:island/pods/chat/webrtc_manager.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
import 'package:island/talker.dart';
|
||||
|
||||
@@ -43,37 +42,35 @@ sealed class CallParticipantLive with _$CallParticipantLive {
|
||||
|
||||
const factory CallParticipantLive({
|
||||
required CallParticipant participant,
|
||||
required lk.Participant remoteParticipant,
|
||||
required WebRTCParticipant remoteParticipant,
|
||||
}) = _CallParticipantLive;
|
||||
|
||||
bool get isSpeaking => remoteParticipant.isSpeaking;
|
||||
bool get isMuted =>
|
||||
remoteParticipant.isMuted || !remoteParticipant.isMicrophoneEnabled();
|
||||
bool get isScreenSharing => remoteParticipant.isScreenShareEnabled();
|
||||
bool get isScreenSharingWithAudio =>
|
||||
remoteParticipant.isScreenShareAudioEnabled();
|
||||
bool get isSpeaking => false; // TODO: Implement speaking detection
|
||||
bool get isMuted => !remoteParticipant.isAudioEnabled;
|
||||
bool get isScreenSharing => remoteParticipant.isVideoEnabled; // Simplified
|
||||
bool get isScreenSharingWithAudio => false; // TODO: Implement screen sharing
|
||||
|
||||
bool get hasVideo => remoteParticipant.hasVideo;
|
||||
bool get hasAudio => remoteParticipant.hasAudio;
|
||||
bool get hasVideo => remoteParticipant.isVideoEnabled;
|
||||
bool get hasAudio => remoteParticipant.isAudioEnabled;
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class CallNotifier extends _$CallNotifier {
|
||||
lk.Room? _room;
|
||||
lk.LocalParticipant? _localParticipant;
|
||||
WebRTCManager? _webrtcManager;
|
||||
List<CallParticipantLive> _participants = [];
|
||||
final Map<String, CallParticipant> _participantInfoByIdentity = {};
|
||||
lk.EventsListener? _roomListener;
|
||||
StreamSubscription<WebRTCParticipant>? _participantJoinedSubscription;
|
||||
StreamSubscription<String>? _participantLeftSubscription;
|
||||
|
||||
List<CallParticipantLive> get participants =>
|
||||
List.unmodifiable(_participants);
|
||||
lk.LocalParticipant? get localParticipant => _localParticipant;
|
||||
|
||||
Map<String, double> participantsVolumes = {};
|
||||
|
||||
Timer? _durationTimer;
|
||||
|
||||
lk.Room? get room => _room;
|
||||
String? _roomId;
|
||||
String? get roomId => _roomId;
|
||||
|
||||
@override
|
||||
CallState build() {
|
||||
@@ -87,149 +84,62 @@ class CallNotifier extends _$CallNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
void _initRoomListeners() {
|
||||
if (_room == null) return;
|
||||
_roomListener?.dispose();
|
||||
_roomListener = _room!.createListener();
|
||||
_room!.addListener(_onRoomChange);
|
||||
_roomListener!
|
||||
..on<lk.ParticipantConnectedEvent>((e) {
|
||||
_refreshLiveParticipants();
|
||||
})
|
||||
..on<lk.RoomDisconnectedEvent>((e) {
|
||||
_participants = [];
|
||||
state = state.copyWith();
|
||||
});
|
||||
}
|
||||
void _initWebRTCListeners() {
|
||||
_participantJoinedSubscription?.cancel();
|
||||
_participantLeftSubscription?.cancel();
|
||||
|
||||
void _onRoomChange() {
|
||||
_refreshLiveParticipants();
|
||||
}
|
||||
|
||||
void _refreshLiveParticipants() {
|
||||
if (_room == null) return;
|
||||
final remoteParticipants = _room!.remoteParticipants;
|
||||
_participants = [];
|
||||
// Add local participant first if available
|
||||
if (_localParticipant != null) {
|
||||
final localInfo = _buildParticipant();
|
||||
_participants.add(
|
||||
CallParticipantLive(
|
||||
participant: localInfo,
|
||||
remoteParticipant: _localParticipant!,
|
||||
),
|
||||
);
|
||||
}
|
||||
// Add remote participants
|
||||
_participants.addAll(
|
||||
remoteParticipants.values.map((remote) {
|
||||
final match =
|
||||
_participantInfoByIdentity[remote.identity] ??
|
||||
CallParticipant(
|
||||
identity: remote.identity,
|
||||
name: remote.identity,
|
||||
joinedAt: DateTime.now(),
|
||||
);
|
||||
return CallParticipantLive(
|
||||
participant: match,
|
||||
remoteParticipant: remote,
|
||||
);
|
||||
}),
|
||||
_participantJoinedSubscription = _webrtcManager?.onParticipantJoined.listen(
|
||||
(participant) {
|
||||
_updateLiveParticipantsFromWebRTC();
|
||||
},
|
||||
);
|
||||
|
||||
_participantLeftSubscription = _webrtcManager?.onParticipantLeft.listen((
|
||||
participantId,
|
||||
) {
|
||||
_participants.removeWhere((p) => p.remoteParticipant.id == participantId);
|
||||
state = state.copyWith();
|
||||
});
|
||||
}
|
||||
|
||||
void _updateLiveParticipantsFromWebRTC() {
|
||||
if (_webrtcManager == null) return;
|
||||
|
||||
final webrtcParticipants = _webrtcManager!.participants;
|
||||
_participants =
|
||||
webrtcParticipants.map((p) {
|
||||
final participantInfo =
|
||||
_participantInfoByIdentity[p.id] ??
|
||||
CallParticipant(
|
||||
identity: p.id,
|
||||
name: p.name,
|
||||
accountId: p.userinfo.id,
|
||||
account: p.userinfo,
|
||||
joinedAt: DateTime.now(),
|
||||
);
|
||||
return CallParticipantLive(
|
||||
participant: participantInfo,
|
||||
remoteParticipant: p,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
state = state.copyWith();
|
||||
}
|
||||
|
||||
/// Builds the CallParticipant object for the local participant.
|
||||
/// Optionally, pass [participants] if you want to prioritize info from the latest list.
|
||||
CallParticipant _buildParticipant({List<CallParticipant>? participants}) {
|
||||
if (_localParticipant == null) {
|
||||
throw StateError('No local participant available');
|
||||
}
|
||||
// Prefer info from the latest participants list if available
|
||||
if (participants != null) {
|
||||
final idx = participants.indexWhere(
|
||||
(p) => p.identity == _localParticipant!.identity,
|
||||
);
|
||||
if (idx != -1) return participants[idx];
|
||||
}
|
||||
|
||||
// Otherwise, use info from the identity map or fallback to minimal
|
||||
return _participantInfoByIdentity[_localParticipant!.identity] ??
|
||||
CallParticipant(
|
||||
identity: _localParticipant!.identity,
|
||||
name: _localParticipant!.identity,
|
||||
joinedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
void _updateLiveParticipants(List<CallParticipant> participants) {
|
||||
// Update the info map for lookup
|
||||
for (final p in participants) {
|
||||
_participantInfoByIdentity[p.identity] = p;
|
||||
}
|
||||
if (_room == null) {
|
||||
// Can't build live objects, just store empty
|
||||
_participants = [];
|
||||
state = state.copyWith();
|
||||
return;
|
||||
}
|
||||
final remoteParticipants = _room!.remoteParticipants;
|
||||
final remotes = remoteParticipants.values.toList();
|
||||
_participants = [];
|
||||
// Add local participant if present in the list
|
||||
if (_localParticipant != null) {
|
||||
final localInfo = _buildParticipant(participants: participants);
|
||||
_participants.add(
|
||||
CallParticipantLive(
|
||||
participant: localInfo,
|
||||
remoteParticipant: _localParticipant!,
|
||||
),
|
||||
);
|
||||
state = state.copyWith();
|
||||
}
|
||||
// Add remote participants
|
||||
_participants.addAll(
|
||||
participants.map((p) {
|
||||
lk.RemoteParticipant? remote;
|
||||
for (final r in remotes) {
|
||||
if (r.identity == p.identity) {
|
||||
remote = r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (_localParticipant != null &&
|
||||
p.identity == _localParticipant!.identity) {
|
||||
return null; // Already added local
|
||||
}
|
||||
return remote != null
|
||||
? CallParticipantLive(participant: p, remoteParticipant: remote)
|
||||
: null;
|
||||
}).whereType<CallParticipantLive>(),
|
||||
);
|
||||
state = state.copyWith();
|
||||
}
|
||||
|
||||
String? _roomId;
|
||||
String? get roomId => _roomId;
|
||||
|
||||
Future<void> joinRoom(String roomId) async {
|
||||
if (_roomId == roomId && _room != null) {
|
||||
talker.info('[Call] Call skipped. Already has data');
|
||||
return;
|
||||
} else if (_room != null) {
|
||||
if (!_room!.isDisposed &&
|
||||
_room!.connectionState != lk.ConnectionState.disconnected) {
|
||||
throw Exception('Call already connected');
|
||||
if (_roomId == roomId && _webrtcManager != null) {
|
||||
talker.info('[Call] Call skipped. Already connected to this room');
|
||||
// Ensure state is connected even if we skip the join process
|
||||
if (!state.isConnected) {
|
||||
state = state.copyWith(isConnected: true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
_roomId = roomId;
|
||||
if (_room != null) {
|
||||
await _room!.disconnect();
|
||||
await _room!.dispose();
|
||||
_room = null;
|
||||
_localParticipant = null;
|
||||
_participants = [];
|
||||
}
|
||||
|
||||
// Clean up existing connection
|
||||
await disconnect();
|
||||
|
||||
try {
|
||||
final apiClient = ref.read(apiClientProvider);
|
||||
final ongoingCall = await ref.read(ongoingCallProvider(roomId).future);
|
||||
@@ -241,8 +151,11 @@ class CallNotifier extends _$CallNotifier {
|
||||
// Parse join response
|
||||
final joinResponse = ChatRealtimeJoinResponse.fromJson(data);
|
||||
final participants = joinResponse.participants;
|
||||
final String endpoint = joinResponse.endpoint;
|
||||
final String token = joinResponse.token;
|
||||
|
||||
// Update participant info map
|
||||
for (final p in participants) {
|
||||
_participantInfoByIdentity[p.identity] = p;
|
||||
}
|
||||
|
||||
// Setup duration timer
|
||||
_durationTimer?.cancel();
|
||||
@@ -257,47 +170,18 @@ class CallNotifier extends _$CallNotifier {
|
||||
);
|
||||
});
|
||||
|
||||
// Connect to LiveKit
|
||||
_room = lk.Room();
|
||||
// Initialize WebRTC manager
|
||||
final serverUrl = ref.watch(serverUrlProvider);
|
||||
|
||||
await _room!.connect(
|
||||
endpoint,
|
||||
token,
|
||||
connectOptions: lk.ConnectOptions(autoSubscribe: true),
|
||||
roomOptions: lk.RoomOptions(adaptiveStream: true, dynacast: true),
|
||||
fastConnectOptions: lk.FastConnectOptions(
|
||||
microphone: lk.TrackOption(enabled: true),
|
||||
),
|
||||
);
|
||||
_localParticipant = _room!.localParticipant;
|
||||
_webrtcManager = WebRTCManager(roomId: roomId, serverUrl: serverUrl);
|
||||
|
||||
_initRoomListeners();
|
||||
_updateLiveParticipants(participants);
|
||||
await _webrtcManager!.initialize(ref);
|
||||
_initWebRTCListeners();
|
||||
|
||||
if (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) {
|
||||
lk.Hardware.instance.setSpeakerphoneOn(true);
|
||||
// TODO: Implement speakerphone control for WebRTC
|
||||
}
|
||||
|
||||
// Listen for connection updates
|
||||
_room!.addListener(() {
|
||||
final wasConnected = state.isConnected;
|
||||
final isNowConnected =
|
||||
_room!.connectionState == lk.ConnectionState.connected;
|
||||
state = state.copyWith(
|
||||
isConnected: isNowConnected,
|
||||
isMicrophoneEnabled: _localParticipant!.isMicrophoneEnabled(),
|
||||
isCameraEnabled: _localParticipant!.isCameraEnabled(),
|
||||
isScreenSharing: _localParticipant!.isScreenShareEnabled(),
|
||||
);
|
||||
// Enable wakelock when call connects
|
||||
if (!wasConnected && isNowConnected) {
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
// Disable wakelock when call disconnects
|
||||
else if (wasConnected && !isNowConnected) {
|
||||
WakelockPlus.disable();
|
||||
}
|
||||
});
|
||||
state = state.copyWith(isConnected: true);
|
||||
// Enable wakelock when call connects
|
||||
WakelockPlus.enable();
|
||||
@@ -310,104 +194,53 @@ class CallNotifier extends _$CallNotifier {
|
||||
}
|
||||
|
||||
Future<void> toggleMicrophone() async {
|
||||
if (_localParticipant != null) {
|
||||
const autostop = true;
|
||||
final target = !_localParticipant!.isMicrophoneEnabled();
|
||||
state = state.copyWith(isMicrophoneEnabled: target);
|
||||
if (target) {
|
||||
await _localParticipant!.audioTrackPublications.firstOrNull?.unmute(
|
||||
stopOnMute: autostop,
|
||||
);
|
||||
} else {
|
||||
await _localParticipant!.audioTrackPublications.firstOrNull?.mute(
|
||||
stopOnMute: autostop,
|
||||
);
|
||||
}
|
||||
state = state.copyWith();
|
||||
}
|
||||
final target = !state.isMicrophoneEnabled;
|
||||
state = state.copyWith(isMicrophoneEnabled: target);
|
||||
await _webrtcManager?.toggleMicrophone(target);
|
||||
}
|
||||
|
||||
Future<void> toggleCamera() async {
|
||||
if (_localParticipant != null) {
|
||||
final target = !_localParticipant!.isCameraEnabled();
|
||||
state = state.copyWith(isCameraEnabled: target);
|
||||
await _localParticipant!.setCameraEnabled(target);
|
||||
state = state.copyWith();
|
||||
}
|
||||
final target = !state.isCameraEnabled;
|
||||
state = state.copyWith(isCameraEnabled: target);
|
||||
await _webrtcManager?.toggleCamera(target);
|
||||
}
|
||||
|
||||
Future<void> toggleScreenShare(BuildContext context) async {
|
||||
if (_localParticipant != null) {
|
||||
final target = !_localParticipant!.isScreenShareEnabled();
|
||||
state = state.copyWith(isScreenSharing: target);
|
||||
|
||||
if (target && lk.lkPlatformIsDesktop()) {
|
||||
try {
|
||||
final source = await showDialog<DesktopCapturerSource>(
|
||||
context: context,
|
||||
builder: (context) => lk.ScreenSelectDialog(),
|
||||
);
|
||||
if (source == null) {
|
||||
return;
|
||||
}
|
||||
var track = await lk.LocalVideoTrack.createScreenShareTrack(
|
||||
lk.ScreenShareCaptureOptions(
|
||||
sourceId: source.id,
|
||||
maxFrameRate: 30.0,
|
||||
captureScreenAudio: true,
|
||||
),
|
||||
);
|
||||
await _localParticipant!.publishVideoTrack(track);
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
await _localParticipant!.setScreenShareEnabled(target);
|
||||
}
|
||||
|
||||
state = state.copyWith();
|
||||
}
|
||||
// TODO: Implement screen sharing for WebRTC
|
||||
state = state.copyWith(isScreenSharing: !state.isScreenSharing);
|
||||
}
|
||||
|
||||
Future<void> toggleSpeakerphone() async {
|
||||
state = state.copyWith(isSpeakerphone: !state.isSpeakerphone);
|
||||
await lk.Hardware.instance.setSpeakerphoneOn(state.isSpeakerphone);
|
||||
state = state.copyWith();
|
||||
// TODO: Implement speakerphone control for WebRTC
|
||||
}
|
||||
|
||||
Future<void> disconnect() async {
|
||||
if (_room != null) {
|
||||
await _room!.disconnect();
|
||||
state = state.copyWith(
|
||||
isConnected: false,
|
||||
isMicrophoneEnabled: false,
|
||||
isCameraEnabled: false,
|
||||
isScreenSharing: false,
|
||||
);
|
||||
// Disable wakelock when call disconnects
|
||||
WakelockPlus.disable();
|
||||
}
|
||||
_webrtcManager?.dispose();
|
||||
_webrtcManager = null;
|
||||
_participantJoinedSubscription?.cancel();
|
||||
_participantLeftSubscription?.cancel();
|
||||
_participants.clear();
|
||||
state = state.copyWith(
|
||||
isConnected: false,
|
||||
isMicrophoneEnabled: false,
|
||||
isCameraEnabled: false,
|
||||
isScreenSharing: false,
|
||||
);
|
||||
// Disable wakelock when call disconnects
|
||||
WakelockPlus.disable();
|
||||
}
|
||||
|
||||
void setParticipantVolume(CallParticipantLive live, double volume) {
|
||||
if (participantsVolumes[live.remoteParticipant.sid] == null) {
|
||||
participantsVolumes[live.remoteParticipant.sid] = 1;
|
||||
if (participantsVolumes[live.remoteParticipant.id] == null) {
|
||||
participantsVolumes[live.remoteParticipant.id] = 1;
|
||||
}
|
||||
Helper.setVolume(
|
||||
volume,
|
||||
live
|
||||
.remoteParticipant
|
||||
.audioTrackPublications
|
||||
.first
|
||||
.track!
|
||||
.mediaStreamTrack,
|
||||
);
|
||||
participantsVolumes[live.remoteParticipant.sid] = volume;
|
||||
// TODO: Implement volume control for WebRTC
|
||||
participantsVolumes[live.remoteParticipant.id] = volume;
|
||||
}
|
||||
|
||||
double getParticipantVolume(CallParticipantLive live) {
|
||||
return participantsVolumes[live.remoteParticipant.sid] ?? 1;
|
||||
return participantsVolumes[live.remoteParticipant.id] ?? 1;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
@@ -418,9 +251,10 @@ class CallNotifier extends _$CallNotifier {
|
||||
isCameraEnabled: false,
|
||||
isScreenSharing: false,
|
||||
);
|
||||
_roomListener?.dispose();
|
||||
_room?.removeListener(_onRoomChange);
|
||||
_room?.dispose();
|
||||
_participantJoinedSubscription?.cancel();
|
||||
_participantLeftSubscription?.cancel();
|
||||
_webrtcManager?.dispose();
|
||||
_webrtcManager = null;
|
||||
_durationTimer?.cancel();
|
||||
_roomId = null;
|
||||
participantsVolumes = {};
|
||||
|
Reference in New Issue
Block a user