♻️ Trying out the new built-in webrtc
This commit is contained in:
287
lib/pods/chat/webrtc_manager.dart
Normal file
287
lib/pods/chat/webrtc_manager.dart
Normal file
@@ -0,0 +1,287 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
import 'package:island/models/account.dart';
|
||||
import 'package:island/pods/chat/webrtc_signaling.dart';
|
||||
import 'package:island/talker.dart';
|
||||
|
||||
class WebRTCParticipant {
|
||||
final String id;
|
||||
final String name;
|
||||
final SnAccount userinfo;
|
||||
RTCPeerConnection? peerConnection;
|
||||
MediaStream? remoteStream;
|
||||
bool isAudioEnabled = true;
|
||||
bool isVideoEnabled = false;
|
||||
bool isConnected = false;
|
||||
|
||||
WebRTCParticipant({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.userinfo,
|
||||
});
|
||||
}
|
||||
|
||||
class WebRTCManager {
|
||||
final String roomId;
|
||||
final String serverUrl;
|
||||
late WebRTCSignaling _signaling;
|
||||
final Map<String, WebRTCParticipant> _participants = {};
|
||||
final Map<String, RTCPeerConnection> _peerConnections = {};
|
||||
|
||||
MediaStream? _localStream;
|
||||
final StreamController<WebRTCParticipant> _participantController =
|
||||
StreamController<WebRTCParticipant>.broadcast();
|
||||
final StreamController<String> _participantLeftController =
|
||||
StreamController<String>.broadcast();
|
||||
|
||||
Stream<WebRTCParticipant> get onParticipantJoined =>
|
||||
_participantController.stream;
|
||||
Stream<String> get onParticipantLeft => _participantLeftController.stream;
|
||||
|
||||
WebRTCManager({required this.roomId, required this.serverUrl}) {
|
||||
_signaling = WebRTCSignaling(roomId: roomId);
|
||||
}
|
||||
|
||||
Future<void> initialize(Ref ref) async {
|
||||
await _initializeLocalStream();
|
||||
_setupSignalingListeners();
|
||||
await _signaling.connect(ref);
|
||||
}
|
||||
|
||||
Future<void> _initializeLocalStream() async {
|
||||
try {
|
||||
_localStream = await navigator.mediaDevices.getUserMedia({
|
||||
'audio': true,
|
||||
'video': false,
|
||||
});
|
||||
talker.info('[WebRTC] Local stream initialized');
|
||||
} catch (e) {
|
||||
talker.error('[WebRTC] Failed to initialize local stream: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
void _setupSignalingListeners() {
|
||||
_signaling.messages.listen((message) async {
|
||||
switch (message.type) {
|
||||
case 'offer':
|
||||
await _handleOffer(message.accountId, message.account, message.data);
|
||||
break;
|
||||
case 'answer':
|
||||
await _handleAnswer(message.accountId, message.data);
|
||||
break;
|
||||
case 'ice-candidate':
|
||||
await _handleIceCandidate(message.accountId, message.data);
|
||||
break;
|
||||
// CHANGED: Listen for new users joining the room.
|
||||
case 'user-joined':
|
||||
await _handleUserJoined(message.accountId, message.account);
|
||||
break;
|
||||
default:
|
||||
talker.warning(
|
||||
'[WebRTC Manager] Receieved an unknown type singaling message: ${message.type} with ${message.data}',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// CHANGED: The welcome message now drives connection initiation.
|
||||
_signaling.welcomeMessages.listen((welcome) {
|
||||
talker.info('[WebRTC Manager] Connected to room: ${welcome.roomId}');
|
||||
final existingParticipants =
|
||||
welcome.participants; // Assuming the server sends this.
|
||||
talker.info(
|
||||
'[WebRTC Manager] Existing participants: $existingParticipants',
|
||||
);
|
||||
|
||||
// The newcomer is responsible for initiating the connection to everyone else.
|
||||
for (final participant in existingParticipants) {
|
||||
if (participant.identity != _signaling.userId) {
|
||||
if (!_participants.containsKey(participant.identity)) {
|
||||
final webrtcParticipant = WebRTCParticipant(
|
||||
id: participant.identity,
|
||||
name: participant.name,
|
||||
userinfo: participant.account!,
|
||||
);
|
||||
_participants[participant.identity] = webrtcParticipant;
|
||||
_participantController.add(webrtcParticipant);
|
||||
}
|
||||
_createPeerConnection(participant.identity, isInitiator: true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// CHANGED: New handler for when an existing user is notified of a new peer.
|
||||
Future<void> _handleUserJoined(
|
||||
String participantId,
|
||||
SnAccount account,
|
||||
) async {
|
||||
talker.info(
|
||||
'[WebRTC Manager] User joined: $participantId. Waiting for their offer.',
|
||||
);
|
||||
// We don't need to be the initiator here. The newcomer will send us an offer.
|
||||
// We just create the peer connection to be ready for it.
|
||||
if (!_peerConnections.containsKey(participantId)) {
|
||||
// Create a participant object to represent the new user
|
||||
if (!_participants.containsKey(participantId)) {
|
||||
final participant = WebRTCParticipant(
|
||||
id: participantId,
|
||||
name: participantId,
|
||||
userinfo: account,
|
||||
); // Placeholder name
|
||||
_participants[participantId] = participant;
|
||||
_participantController.add(participant);
|
||||
}
|
||||
await _createPeerConnection(participantId, isInitiator: false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _createPeerConnection(
|
||||
String participantId, {
|
||||
bool isInitiator = false,
|
||||
}) async {
|
||||
talker.info(
|
||||
'[WebRTC] Creating peer connection to $participantId (initiator: $isInitiator)',
|
||||
);
|
||||
final configuration = {
|
||||
'iceServers': [
|
||||
{'urls': 'stun:stun.l.google.com:19302'},
|
||||
],
|
||||
};
|
||||
|
||||
final peerConnection = await createPeerConnection(configuration);
|
||||
_peerConnections[participantId] = peerConnection;
|
||||
|
||||
if (_localStream != null) {
|
||||
for (final track in _localStream!.getTracks()) {
|
||||
await peerConnection.addTrack(track, _localStream!);
|
||||
}
|
||||
}
|
||||
|
||||
peerConnection.onTrack = (event) {
|
||||
if (event.streams.isNotEmpty) {
|
||||
final participant = _participants[participantId];
|
||||
if (participant != null) {
|
||||
participant.remoteStream = event.streams[0];
|
||||
participant.isConnected = true;
|
||||
_participantController.add(participant);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
peerConnection.onIceCandidate = (candidate) {
|
||||
// CHANGED: Send candidate to the specific participant
|
||||
_signaling.sendIceCandidate(participantId, candidate);
|
||||
};
|
||||
|
||||
peerConnection.onConnectionState = (state) {
|
||||
talker.info('[WebRTC] Connection state for $participantId: $state');
|
||||
final participant = _participants[participantId];
|
||||
if (participant != null) {
|
||||
participant.isConnected =
|
||||
state == RTCPeerConnectionState.RTCPeerConnectionStateConnected;
|
||||
_participantController.add(participant);
|
||||
}
|
||||
};
|
||||
|
||||
if (isInitiator) {
|
||||
final offer = await peerConnection.createOffer();
|
||||
await peerConnection.setLocalDescription(offer);
|
||||
// CHANGED: Send offer to the specific participant
|
||||
_signaling.sendOffer(participantId, offer);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleOffer(
|
||||
String from,
|
||||
SnAccount account,
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
final participantId = from;
|
||||
talker.info('[WebRTC Manager] Handling offer from $participantId');
|
||||
final offer = RTCSessionDescription(data['sdp'], data['type']);
|
||||
|
||||
if (!_peerConnections.containsKey(participantId)) {
|
||||
if (!_participants.containsKey(participantId)) {
|
||||
final participant = WebRTCParticipant(
|
||||
id: participantId,
|
||||
name: participantId,
|
||||
userinfo: account,
|
||||
);
|
||||
_participants[participantId] = participant;
|
||||
_participantController.add(participant);
|
||||
}
|
||||
await _createPeerConnection(participantId, isInitiator: false);
|
||||
}
|
||||
|
||||
final peerConnection = _peerConnections[participantId]!;
|
||||
await peerConnection.setRemoteDescription(offer);
|
||||
|
||||
final answer = await peerConnection.createAnswer();
|
||||
await peerConnection.setLocalDescription(answer);
|
||||
// CHANGED: Send answer to the specific participant
|
||||
_signaling.sendAnswer(participantId, answer);
|
||||
}
|
||||
|
||||
Future<void> _handleAnswer(String from, Map<String, dynamic> data) async {
|
||||
final participantId = from;
|
||||
talker.info('[WebRTC Manager] Handling answer from $participantId');
|
||||
final answer = RTCSessionDescription(data['sdp'], data['type']);
|
||||
|
||||
final peerConnection = _peerConnections[participantId];
|
||||
if (peerConnection != null) {
|
||||
await peerConnection.setRemoteDescription(answer);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleIceCandidate(
|
||||
String from,
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
final participantId = from;
|
||||
final candidate = RTCIceCandidate(
|
||||
data['candidate'],
|
||||
data['sdpMid'],
|
||||
data['sdpMLineIndex'],
|
||||
);
|
||||
|
||||
final peerConnection = _peerConnections[participantId];
|
||||
if (peerConnection != null) {
|
||||
// It's possible for candidates to arrive before the remote description is set.
|
||||
// A robust implementation might queue them, but for now, we'll just add them.
|
||||
await peerConnection.addCandidate(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> toggleMicrophone(bool enabled) async {
|
||||
if (_localStream != null) {
|
||||
final audioTracks = _localStream!.getAudioTracks();
|
||||
for (final track in audioTracks) {
|
||||
track.enabled = enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> toggleCamera(bool enabled) async {
|
||||
if (_localStream != null) {
|
||||
_localStream!.getVideoTracks().forEach((track) {
|
||||
track.enabled = enabled;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
List<WebRTCParticipant> get participants => _participants.values.toList();
|
||||
|
||||
void dispose() {
|
||||
_signaling.disconnect();
|
||||
for (final pc in _peerConnections.values) {
|
||||
pc.close();
|
||||
}
|
||||
_peerConnections.clear();
|
||||
_participants.clear();
|
||||
_localStream?.dispose();
|
||||
_participantController.close();
|
||||
_participantLeftController.close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user