♻️ Proper singaling
This commit is contained in:
		| @@ -2241,7 +2241,7 @@ as List<CallParticipant>, | ||||
| /// @nodoc | ||||
| mixin _$CallParticipant { | ||||
|  | ||||
|  String get identity; String get name; String get accountId; SnAccount? get account; DateTime get joinedAt; bool get isLocal; | ||||
|  String get identity; String get name; String get accountId; SnAccount? get account; DateTime get joinedAt; | ||||
| /// Create a copy of CallParticipant | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @@ -2254,16 +2254,16 @@ $CallParticipantCopyWith<CallParticipant> get copyWith => _$CallParticipantCopyW | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is CallParticipant&&(identical(other.identity, identity) || other.identity == identity)&&(identical(other.name, name) || other.name == name)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt)&&(identical(other.isLocal, isLocal) || other.isLocal == isLocal)); | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is CallParticipant&&(identical(other.identity, identity) || other.identity == identity)&&(identical(other.name, name) || other.name == name)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,identity,name,accountId,account,joinedAt,isLocal); | ||||
| int get hashCode => Object.hash(runtimeType,identity,name,accountId,account,joinedAt); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'CallParticipant(identity: $identity, name: $name, accountId: $accountId, account: $account, joinedAt: $joinedAt, isLocal: $isLocal)'; | ||||
|   return 'CallParticipant(identity: $identity, name: $name, accountId: $accountId, account: $account, joinedAt: $joinedAt)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -2274,7 +2274,7 @@ abstract mixin class $CallParticipantCopyWith<$Res>  { | ||||
|   factory $CallParticipantCopyWith(CallParticipant value, $Res Function(CallParticipant) _then) = _$CallParticipantCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  String identity, String name, String accountId, SnAccount? account, DateTime joinedAt, bool isLocal | ||||
|  String identity, String name, String accountId, SnAccount? account, DateTime joinedAt | ||||
| }); | ||||
|  | ||||
|  | ||||
| @@ -2291,15 +2291,14 @@ class _$CallParticipantCopyWithImpl<$Res> | ||||
|  | ||||
| /// Create a copy of CallParticipant | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? identity = null,Object? name = null,Object? accountId = null,Object? account = freezed,Object? joinedAt = null,Object? isLocal = null,}) { | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? identity = null,Object? name = null,Object? accountId = null,Object? account = freezed,Object? joinedAt = null,}) { | ||||
|   return _then(_self.copyWith( | ||||
| identity: null == identity ? _self.identity : identity // ignore: cast_nullable_to_non_nullable | ||||
| as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable | ||||
| as String,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable | ||||
| as String,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable | ||||
| as SnAccount?,joinedAt: null == joinedAt ? _self.joinedAt : joinedAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime,isLocal: null == isLocal ? _self.isLocal : isLocal // ignore: cast_nullable_to_non_nullable | ||||
| as bool, | ||||
| as DateTime, | ||||
|   )); | ||||
| } | ||||
| /// Create a copy of CallParticipant | ||||
| @@ -2393,10 +2392,10 @@ return $default(_that);case _: | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String identity,  String name,  String accountId,  SnAccount? account,  DateTime joinedAt,  bool isLocal)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String identity,  String name,  String accountId,  SnAccount? account,  DateTime joinedAt)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| switch (_that) { | ||||
| case _CallParticipant() when $default != null: | ||||
| return $default(_that.identity,_that.name,_that.accountId,_that.account,_that.joinedAt,_that.isLocal);case _: | ||||
| return $default(_that.identity,_that.name,_that.accountId,_that.account,_that.joinedAt);case _: | ||||
|   return orElse(); | ||||
|  | ||||
| } | ||||
| @@ -2414,10 +2413,10 @@ return $default(_that.identity,_that.name,_that.accountId,_that.account,_that.jo | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String identity,  String name,  String accountId,  SnAccount? account,  DateTime joinedAt,  bool isLocal)  $default,) {final _that = this; | ||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String identity,  String name,  String accountId,  SnAccount? account,  DateTime joinedAt)  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _CallParticipant(): | ||||
| return $default(_that.identity,_that.name,_that.accountId,_that.account,_that.joinedAt,_that.isLocal);} | ||||
| return $default(_that.identity,_that.name,_that.accountId,_that.account,_that.joinedAt);} | ||||
| } | ||||
| /// A variant of `when` that fallback to returning `null` | ||||
| /// | ||||
| @@ -2431,10 +2430,10 @@ return $default(_that.identity,_that.name,_that.accountId,_that.account,_that.jo | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String identity,  String name,  String accountId,  SnAccount? account,  DateTime joinedAt,  bool isLocal)?  $default,) {final _that = this; | ||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String identity,  String name,  String accountId,  SnAccount? account,  DateTime joinedAt)?  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _CallParticipant() when $default != null: | ||||
| return $default(_that.identity,_that.name,_that.accountId,_that.account,_that.joinedAt,_that.isLocal);case _: | ||||
| return $default(_that.identity,_that.name,_that.accountId,_that.account,_that.joinedAt);case _: | ||||
|   return null; | ||||
|  | ||||
| } | ||||
| @@ -2446,7 +2445,7 @@ return $default(_that.identity,_that.name,_that.accountId,_that.account,_that.jo | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _CallParticipant implements CallParticipant { | ||||
|   const _CallParticipant({required this.identity, required this.name, required this.accountId, this.account = null, required this.joinedAt, this.isLocal = false}); | ||||
|   const _CallParticipant({required this.identity, required this.name, required this.accountId, this.account = null, required this.joinedAt}); | ||||
|   factory _CallParticipant.fromJson(Map<String, dynamic> json) => _$CallParticipantFromJson(json); | ||||
|  | ||||
| @override final  String identity; | ||||
| @@ -2454,7 +2453,6 @@ class _CallParticipant implements CallParticipant { | ||||
| @override final  String accountId; | ||||
| @override@JsonKey() final  SnAccount? account; | ||||
| @override final  DateTime joinedAt; | ||||
| @override@JsonKey() final  bool isLocal; | ||||
|  | ||||
| /// Create a copy of CallParticipant | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @@ -2469,16 +2467,16 @@ Map<String, dynamic> toJson() { | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _CallParticipant&&(identical(other.identity, identity) || other.identity == identity)&&(identical(other.name, name) || other.name == name)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt)&&(identical(other.isLocal, isLocal) || other.isLocal == isLocal)); | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _CallParticipant&&(identical(other.identity, identity) || other.identity == identity)&&(identical(other.name, name) || other.name == name)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,identity,name,accountId,account,joinedAt,isLocal); | ||||
| int get hashCode => Object.hash(runtimeType,identity,name,accountId,account,joinedAt); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'CallParticipant(identity: $identity, name: $name, accountId: $accountId, account: $account, joinedAt: $joinedAt, isLocal: $isLocal)'; | ||||
|   return 'CallParticipant(identity: $identity, name: $name, accountId: $accountId, account: $account, joinedAt: $joinedAt)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -2489,7 +2487,7 @@ abstract mixin class _$CallParticipantCopyWith<$Res> implements $CallParticipant | ||||
|   factory _$CallParticipantCopyWith(_CallParticipant value, $Res Function(_CallParticipant) _then) = __$CallParticipantCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  String identity, String name, String accountId, SnAccount? account, DateTime joinedAt, bool isLocal | ||||
|  String identity, String name, String accountId, SnAccount? account, DateTime joinedAt | ||||
| }); | ||||
|  | ||||
|  | ||||
| @@ -2506,15 +2504,14 @@ class __$CallParticipantCopyWithImpl<$Res> | ||||
|  | ||||
| /// Create a copy of CallParticipant | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? identity = null,Object? name = null,Object? accountId = null,Object? account = freezed,Object? joinedAt = null,Object? isLocal = null,}) { | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? identity = null,Object? name = null,Object? accountId = null,Object? account = freezed,Object? joinedAt = null,}) { | ||||
|   return _then(_CallParticipant( | ||||
| identity: null == identity ? _self.identity : identity // ignore: cast_nullable_to_non_nullable | ||||
| as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable | ||||
| as String,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable | ||||
| as String,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable | ||||
| as SnAccount?,joinedAt: null == joinedAt ? _self.joinedAt : joinedAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime,isLocal: null == isLocal ? _self.isLocal : isLocal // ignore: cast_nullable_to_non_nullable | ||||
| as bool, | ||||
| as DateTime, | ||||
|   )); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -281,7 +281,6 @@ _CallParticipant _$CallParticipantFromJson(Map<String, dynamic> json) => | ||||
|               ? null | ||||
|               : SnAccount.fromJson(json['account'] as Map<String, dynamic>), | ||||
|       joinedAt: DateTime.parse(json['joined_at'] as String), | ||||
|       isLocal: json['is_local'] as bool? ?? false, | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$CallParticipantToJson(_CallParticipant instance) => | ||||
| @@ -291,7 +290,6 @@ Map<String, dynamic> _$CallParticipantToJson(_CallParticipant instance) => | ||||
|       'account_id': instance.accountId, | ||||
|       'account': instance.account?.toJson(), | ||||
|       'joined_at': instance.joinedAt.toIso8601String(), | ||||
|       'is_local': instance.isLocal, | ||||
|     }; | ||||
|  | ||||
| _SnRealtimeCall _$SnRealtimeCallFromJson(Map<String, dynamic> json) => | ||||
|   | ||||
| @@ -6,7 +6,7 @@ part of 'call.dart'; | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$callNotifierHash() => r'6db330370d473eaea313dd9f9439d261c355095f'; | ||||
| String _$callNotifierHash() => r'4015d326388553c46859fe537e84d2c9da4236c9'; | ||||
|  | ||||
| /// See also [CallNotifier]. | ||||
| @ProviderFor(CallNotifier) | ||||
|   | ||||
| @@ -3,6 +3,7 @@ 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/pods/userinfo.dart'; | ||||
| import 'package:island/talker.dart'; | ||||
|  | ||||
| class WebRTCParticipant { | ||||
| @@ -11,6 +12,7 @@ class WebRTCParticipant { | ||||
|   final SnAccount userinfo; | ||||
|   RTCPeerConnection? peerConnection; | ||||
|   MediaStream? remoteStream; | ||||
|   List<RTCIceCandidate> remoteCandidates = []; | ||||
|   bool isAudioEnabled = true; | ||||
|   bool isVideoEnabled = false; | ||||
|   bool isConnected = false; | ||||
| @@ -20,6 +22,8 @@ class WebRTCParticipant { | ||||
|     required this.id, | ||||
|     required this.name, | ||||
|     required this.userinfo, | ||||
|     this.isAudioEnabled = true, | ||||
|     this.isVideoEnabled = false, | ||||
|     this.isLocal = false, | ||||
|   }); | ||||
| } | ||||
| @@ -48,6 +52,10 @@ class WebRTCManager { | ||||
|   } | ||||
|  | ||||
|   Future<void> initialize(Ref ref) async { | ||||
|     final user = ref.watch(userInfoProvider).value!; | ||||
|     _signaling.userId = user.id; | ||||
|     _signaling.userName = user.name; | ||||
|     _signaling.user = user; | ||||
|     await _initializeLocalStream(); | ||||
|     _setupSignalingListeners(); | ||||
|     await _signaling.connect(ref); | ||||
| @@ -60,6 +68,19 @@ class WebRTCManager { | ||||
|         'video': true, | ||||
|       }); | ||||
|       talker.info('[WebRTC] Local stream initialized'); | ||||
|  | ||||
|       // Add local participant | ||||
|       bool videoEnabled = _localStream!.getVideoTracks().isNotEmpty; | ||||
|       WebRTCParticipant localParticipant = WebRTCParticipant( | ||||
|         id: _signaling.userId, | ||||
|         name: _signaling.userName, | ||||
|         userinfo: _signaling.user, | ||||
|         isLocal: true, | ||||
|         isAudioEnabled: true, | ||||
|         isVideoEnabled: videoEnabled, | ||||
|       ); | ||||
|       _participants[_signaling.userId] = localParticipant; | ||||
|       _participantController.add(localParticipant); | ||||
|     } catch (e) { | ||||
|       talker.error('[WebRTC] Failed to initialize local stream: $e'); | ||||
|       rethrow; | ||||
| @@ -156,6 +177,7 @@ class WebRTCManager { | ||||
|  | ||||
|     final peerConnection = await createPeerConnection(configuration); | ||||
|     _peerConnections[participantId] = peerConnection; | ||||
|     _participants[participantId]!.peerConnection = peerConnection; | ||||
|  | ||||
|     if (_localStream != null) { | ||||
|       for (final track in _localStream!.getTracks()) { | ||||
| @@ -233,6 +255,15 @@ class WebRTCManager { | ||||
|     await peerConnection.setLocalDescription(answer); | ||||
|     // CHANGED: Send answer to the specific participant | ||||
|     _signaling.sendAnswer(participantId, answer); | ||||
|  | ||||
|     // Process any queued ICE candidates | ||||
|     final participant = _participants[participantId]; | ||||
|     if (participant != null) { | ||||
|       for (final candidate in participant.remoteCandidates) { | ||||
|         await peerConnection.addCandidate(candidate); | ||||
|       } | ||||
|       participant.remoteCandidates.clear(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> _handleAnswer(String from, Map<String, dynamic> data) async { | ||||
| @@ -243,6 +274,15 @@ class WebRTCManager { | ||||
|     final peerConnection = _peerConnections[participantId]; | ||||
|     if (peerConnection != null) { | ||||
|       await peerConnection.setRemoteDescription(answer); | ||||
|  | ||||
|       // Process any queued ICE candidates | ||||
|       final participant = _participants[participantId]; | ||||
|       if (participant != null) { | ||||
|         for (final candidate in participant.remoteCandidates) { | ||||
|           await peerConnection.addCandidate(candidate); | ||||
|         } | ||||
|         participant.remoteCandidates.clear(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -257,11 +297,14 @@ class WebRTCManager { | ||||
|       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); | ||||
|     final participant = _participants[participantId]; | ||||
|     if (participant != null) { | ||||
|       final pc = participant.peerConnection; | ||||
|       if (pc != null) { | ||||
|         await pc.addCandidate(candidate); | ||||
|       } else { | ||||
|         participant.remoteCandidates.add(candidate); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -273,7 +316,7 @@ class WebRTCManager { | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     _participants.values.where((e) => e.isLocal).firstOrNull?.isAudioEnabled = true; | ||||
|     _participants[_signaling.userId]?.isAudioEnabled = enabled; | ||||
|   } | ||||
|  | ||||
|   Future<void> toggleCamera(bool enabled) async { | ||||
| @@ -283,7 +326,7 @@ class WebRTCManager { | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     _participants.values.where((e) => e.isLocal).firstOrNull?.isVideoEnabled = true; | ||||
|     _participants[_signaling.userId]?.isVideoEnabled = enabled; | ||||
|   } | ||||
|  | ||||
|   List<WebRTCParticipant> get participants => _participants.values.toList(); | ||||
| @@ -294,6 +337,7 @@ class WebRTCManager { | ||||
|       pc.close(); | ||||
|     } | ||||
|     _peerConnections.clear(); | ||||
|     _participants.values.forEach((p) => p.remoteCandidates.clear()); | ||||
|     _participants.clear(); | ||||
|     _localStream?.dispose(); | ||||
|     _participantController.close(); | ||||
|   | ||||
| @@ -8,7 +8,6 @@ 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'; | ||||
| @@ -51,7 +50,7 @@ class WebRTCSignaling { | ||||
|   final String roomId; | ||||
|   late final String userId; | ||||
|   late final String userName; | ||||
|   late final SnAccount user; | ||||
|   late SnAccount user; | ||||
|   final StreamController<SignalingMessage> _messageController = | ||||
|       StreamController<SignalingMessage>.broadcast(); | ||||
|   final StreamController<WebRTCWelcomeMessage> _welcomeController = | ||||
| @@ -65,13 +64,9 @@ class WebRTCSignaling { | ||||
|   WebRTCSignaling({required this.roomId}); | ||||
|  | ||||
|   Future<void> 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', | ||||
|   | ||||
		Reference in New Issue
	
	Block a user