import 'dart:async'; import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:island/models/account.dart'; import 'package:island/models/chat.dart'; import 'package:island/pods/config.dart'; import 'package:island/pods/network.dart'; import 'package:island/pods/userinfo.dart'; import 'package:island/pods/websocket.dart'; import 'package:web_socket_channel/io.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:island/talker.dart'; part 'webrtc_signaling.freezed.dart'; part 'webrtc_signaling.g.dart'; @freezed sealed class SignalingMessage with _$SignalingMessage { const factory SignalingMessage({ required String type, // CHANGED: Added 'to' field for directed messaging String? to, required String accountId, required SnAccount account, required Map data, }) = _SignalingMessage; factory SignalingMessage.fromJson(Map json) => _$SignalingMessageFromJson(json); } @freezed sealed class WebRTCWelcomeMessage with _$WebRTCWelcomeMessage { const factory WebRTCWelcomeMessage({ required String userId, required String roomId, required String message, required String timestamp, // CHANGED: Added participants list @Default([]) List participants, }) = _WebRTCWelcomeMessage; factory WebRTCWelcomeMessage.fromJson(Map json) => _$WebRTCWelcomeMessageFromJson(json); } class WebRTCSignaling { final String roomId; late final String userId; late final String userName; late final SnAccount user; final StreamController _messageController = StreamController.broadcast(); final StreamController _welcomeController = StreamController.broadcast(); WebSocketChannel? _channel; Stream get messages => _messageController.stream; Stream get welcomeMessages => _welcomeController.stream; WebRTCSignaling({required this.roomId}); Future connect(Ref ref) async { user = ref.watch(userInfoProvider).value!; final baseUrl = ref.watch(serverUrlProvider); final token = await getToken(ref.watch(tokenProvider)); userId = user.id; userName = user.name; final url = '$baseUrl/sphere/chat/realtime/$roomId'.replaceFirst( 'http', 'ws', ); talker.info('[WebRTC Signaling] Trying connecting to $url'); try { if (kIsWeb) { _channel = WebSocketChannel.connect(Uri.parse('$url?tk=$token')); } else { _channel = IOWebSocketChannel.connect( Uri.parse(url), headers: {'Authorization': 'AtField $token'}, ); } await _channel!.ready; _channel!.stream.listen( (data) { final dataStr = data is Uint8List ? utf8.decode(data) : data.toString(); final packet = WebSocketPacket.fromJson(jsonDecode(dataStr)); talker.info( '[WebRTC Signaling] Recieved a singal message with packet type: ${packet.type}', ); if (packet.type == 'webrtc') { try { final welcomeMessage = WebRTCWelcomeMessage.fromJson( packet.data!, ); _welcomeController.add(welcomeMessage); talker.info( '[WebRTC Signaling] Welcome message received: ${welcomeMessage.message}', ); } catch (e) { talker.error( '[WebRTC Signaling] Failed to parse welcome message: $e', ); } } else if (packet.type == 'webrtc.signal') { try { final signalingMessage = SignalingMessage.fromJson(packet.data!); // CHANGED: Ensure we only process messages intended for us if the 'to' field is present if (signalingMessage.to == null || signalingMessage.to == userId) { _messageController.add(signalingMessage); } } catch (e) { talker.error( '[WebRTC Signaling] Failed to parse signaling message: $e', ); } } }, onError: (error) { talker.error('[WebRTC Signaling] WebSocket error: $error'); _messageController.addError(error); _welcomeController.addError(error); }, onDone: () { talker.info('[WebRTC Signaling] WebSocket connection closed'); _messageController.close(); _welcomeController.close(); }, ); } catch (err) { talker.error('[WebRTC Signaling] Failed to connect: $err'); _messageController.addError(err); _welcomeController.addError(err); } } void sendMessage(SignalingMessage message) { if (_channel == null) return; talker.info( '[WebRTC Signaling] Sending a message with message type: ${message.type} to ${message.to}', ); final packet = WebSocketPacket( type: 'webrtc.signal', data: message.toJson(), ); _channel!.sink.add(jsonEncode(packet.toJson())); } // CHANGED: All send methods now correctly use the `to` parameter void sendOffer(String to, RTCSessionDescription offer) { sendMessage( SignalingMessage( type: 'offer', to: to, accountId: userId, account: user, data: {'sdp': offer.sdp, 'type': offer.type}, ), ); } void sendAnswer(String to, RTCSessionDescription answer) { sendMessage( SignalingMessage( type: 'answer', to: to, accountId: userId, account: user, data: {'sdp': answer.sdp, 'type': answer.type}, ), ); } void sendIceCandidate(String to, RTCIceCandidate candidate) { sendMessage( SignalingMessage( type: 'ice-candidate', to: to, accountId: userId, account: user, data: { 'candidate': candidate.candidate, 'sdpMid': candidate.sdpMid, 'sdpMLineIndex': candidate.sdpMLineIndex, }, ), ); } void disconnect() { _channel?.sink.close(); _messageController.close(); _welcomeController.close(); } }