diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 052e58fd..861772b8 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2,8 +2,6 @@ PODS: - Alamofire (5.10.2) - app_links (6.4.1): - Flutter - - connectivity_plus (0.0.1): - - Flutter - croppy (0.0.1): - Flutter - device_info_plus (0.0.1): @@ -219,10 +217,6 @@ PODS: - irondash_engine_context (0.0.1): - Flutter - Kingfisher (8.6.0) - - livekit_client (2.5.0): - - Flutter - - flutter_webrtc - - WebRTC-SDK (= 137.7151.04) - local_auth_darwin (0.0.1): - Flutter - FlutterMacOS @@ -309,7 +303,6 @@ PODS: DEPENDENCIES: - Alamofire - app_links (from `.symlinks/plugins/app_links/ios`) - - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - croppy (from `.symlinks/plugins/croppy/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) @@ -333,7 +326,6 @@ DEPENDENCIES: - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`) - Kingfisher (~> 8.0) - - livekit_client (from `.symlinks/plugins/livekit_client/ios`) - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`) - media_kit_video (from `.symlinks/plugins/media_kit_video/ios`) @@ -388,8 +380,6 @@ SPEC REPOS: EXTERNAL SOURCES: app_links: :path: ".symlinks/plugins/app_links/ios" - connectivity_plus: - :path: ".symlinks/plugins/connectivity_plus/ios" croppy: :path: ".symlinks/plugins/croppy/ios" device_info_plus: @@ -434,8 +424,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/image_picker_ios/ios" irondash_engine_context: :path: ".symlinks/plugins/irondash_engine_context/ios" - livekit_client: - :path: ".symlinks/plugins/livekit_client/ios" local_auth_darwin: :path: ".symlinks/plugins/local_auth_darwin/darwin" media_kit_libs_ios_video: @@ -480,7 +468,6 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496 app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a - connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30 device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c @@ -520,7 +507,6 @@ SPEC CHECKSUMS: image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 Kingfisher: 64278f126a815d0e2d391cdf71311b85882c4de0 - livekit_client: a6f5fa86ac28ccd7ded53626a5379961db311ab4 local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854 media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474 diff --git a/lib/models/chat.dart b/lib/models/chat.dart index 4dac1325..7b37effc 100644 --- a/lib/models/chat.dart +++ b/lib/models/chat.dart @@ -149,6 +149,8 @@ sealed class CallParticipant with _$CallParticipant { const factory CallParticipant({ required String identity, required String name, + required String accountId, + @Default(null) SnAccount? account, required DateTime joinedAt, }) = _CallParticipant; diff --git a/lib/models/chat.freezed.dart b/lib/models/chat.freezed.dart index 74540e24..378bcc03 100644 --- a/lib/models/chat.freezed.dart +++ b/lib/models/chat.freezed.dart @@ -2241,7 +2241,7 @@ as List, /// @nodoc mixin _$CallParticipant { - String get identity; String get name; DateTime get joinedAt; + 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 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.joinedAt, joinedAt) || other.joinedAt == joinedAt)); + 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,joinedAt); +int get hashCode => Object.hash(runtimeType,identity,name,accountId,account,joinedAt); @override String toString() { - return 'CallParticipant(identity: $identity, name: $name, joinedAt: $joinedAt)'; + return 'CallParticipant(identity: $identity, name: $name, accountId: $accountId, account: $account, joinedAt: $joinedAt)'; } @@ -2274,11 +2274,11 @@ abstract mixin class $CallParticipantCopyWith<$Res> { factory $CallParticipantCopyWith(CallParticipant value, $Res Function(CallParticipant) _then) = _$CallParticipantCopyWithImpl; @useResult $Res call({ - String identity, String name, DateTime joinedAt + String identity, String name, String accountId, SnAccount? account, DateTime joinedAt }); - +$SnAccountCopyWith<$Res>? get account; } /// @nodoc @@ -2291,15 +2291,29 @@ 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? joinedAt = 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,joinedAt: null == joinedAt ? _self.joinedAt : joinedAt // 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, )); } +/// Create a copy of CallParticipant +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnAccountCopyWith<$Res>? get account { + if (_self.account == null) { + return null; + } + return $SnAccountCopyWith<$Res>(_self.account!, (value) { + return _then(_self.copyWith(account: value)); + }); +} } @@ -2378,10 +2392,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( String identity, String name, DateTime joinedAt)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(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.joinedAt);case _: +return $default(_that.identity,_that.name,_that.accountId,_that.account,_that.joinedAt);case _: return orElse(); } @@ -2399,10 +2413,10 @@ return $default(_that.identity,_that.name,_that.joinedAt);case _: /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( String identity, String name, DateTime joinedAt) $default,) {final _that = this; +@optionalTypeArgs TResult when(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.joinedAt);} +return $default(_that.identity,_that.name,_that.accountId,_that.account,_that.joinedAt);} } /// A variant of `when` that fallback to returning `null` /// @@ -2416,10 +2430,10 @@ return $default(_that.identity,_that.name,_that.joinedAt);} /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( String identity, String name, DateTime joinedAt)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(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.joinedAt);case _: +return $default(_that.identity,_that.name,_that.accountId,_that.account,_that.joinedAt);case _: return null; } @@ -2431,11 +2445,13 @@ return $default(_that.identity,_that.name,_that.joinedAt);case _: @JsonSerializable() class _CallParticipant implements CallParticipant { - const _CallParticipant({required this.identity, required this.name, required this.joinedAt}); + const _CallParticipant({required this.identity, required this.name, required this.accountId, this.account = null, required this.joinedAt}); factory _CallParticipant.fromJson(Map json) => _$CallParticipantFromJson(json); @override final String identity; @override final String name; +@override final String accountId; +@override@JsonKey() final SnAccount? account; @override final DateTime joinedAt; /// Create a copy of CallParticipant @@ -2451,16 +2467,16 @@ Map 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.joinedAt, joinedAt) || other.joinedAt == joinedAt)); + 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,joinedAt); +int get hashCode => Object.hash(runtimeType,identity,name,accountId,account,joinedAt); @override String toString() { - return 'CallParticipant(identity: $identity, name: $name, joinedAt: $joinedAt)'; + return 'CallParticipant(identity: $identity, name: $name, accountId: $accountId, account: $account, joinedAt: $joinedAt)'; } @@ -2471,11 +2487,11 @@ abstract mixin class _$CallParticipantCopyWith<$Res> implements $CallParticipant factory _$CallParticipantCopyWith(_CallParticipant value, $Res Function(_CallParticipant) _then) = __$CallParticipantCopyWithImpl; @override @useResult $Res call({ - String identity, String name, DateTime joinedAt + String identity, String name, String accountId, SnAccount? account, DateTime joinedAt }); - +@override $SnAccountCopyWith<$Res>? get account; } /// @nodoc @@ -2488,16 +2504,30 @@ 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? joinedAt = 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,joinedAt: null == joinedAt ? _self.joinedAt : joinedAt // 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, )); } +/// Create a copy of CallParticipant +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnAccountCopyWith<$Res>? get account { + if (_self.account == null) { + return null; + } + return $SnAccountCopyWith<$Res>(_self.account!, (value) { + return _then(_self.copyWith(account: value)); + }); +} } diff --git a/lib/models/chat.g.dart b/lib/models/chat.g.dart index 40e59081..2b559957 100644 --- a/lib/models/chat.g.dart +++ b/lib/models/chat.g.dart @@ -275,6 +275,11 @@ _CallParticipant _$CallParticipantFromJson(Map json) => _CallParticipant( identity: json['identity'] as String, name: json['name'] as String, + accountId: json['account_id'] as String, + account: + json['account'] == null + ? null + : SnAccount.fromJson(json['account'] as Map), joinedAt: DateTime.parse(json['joined_at'] as String), ); @@ -282,6 +287,8 @@ Map _$CallParticipantToJson(_CallParticipant instance) => { 'identity': instance.identity, 'name': instance.name, + 'account_id': instance.accountId, + 'account': instance.account?.toJson(), 'joined_at': instance.joinedAt.toIso8601String(), }; diff --git a/lib/pods/chat/call.dart b/lib/pods/chat/call.dart index b17af936..888c81d9 100644 --- a/lib/pods/chat/call.dart +++ b/lib/pods/chat/call.dart @@ -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 _participants = []; final Map _participantInfoByIdentity = {}; - lk.EventsListener? _roomListener; + StreamSubscription? _participantJoinedSubscription; + StreamSubscription? _participantLeftSubscription; List get participants => List.unmodifiable(_participants); - lk.LocalParticipant? get localParticipant => _localParticipant; Map 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((e) { - _refreshLiveParticipants(); - }) - ..on((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? 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 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(), - ); - state = state.copyWith(); - } - - String? _roomId; - String? get roomId => _roomId; - Future 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 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 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 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( - 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 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 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 = {}; diff --git a/lib/pods/chat/call.freezed.dart b/lib/pods/chat/call.freezed.dart index f1254f9b..bd1f9595 100644 --- a/lib/pods/chat/call.freezed.dart +++ b/lib/pods/chat/call.freezed.dart @@ -295,7 +295,7 @@ as String?, /// @nodoc mixin _$CallParticipantLive implements DiagnosticableTreeMixin { - CallParticipant get participant; lk.Participant get remoteParticipant; + CallParticipant get participant; WebRTCParticipant get remoteParticipant; /// Create a copy of CallParticipantLive /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -332,7 +332,7 @@ abstract mixin class $CallParticipantLiveCopyWith<$Res> { factory $CallParticipantLiveCopyWith(CallParticipantLive value, $Res Function(CallParticipantLive) _then) = _$CallParticipantLiveCopyWithImpl; @useResult $Res call({ - CallParticipant participant, lk.Participant remoteParticipant + CallParticipant participant, WebRTCParticipant remoteParticipant }); @@ -353,7 +353,7 @@ class _$CallParticipantLiveCopyWithImpl<$Res> return _then(_self.copyWith( participant: null == participant ? _self.participant : participant // ignore: cast_nullable_to_non_nullable as CallParticipant,remoteParticipant: null == remoteParticipant ? _self.remoteParticipant : remoteParticipant // ignore: cast_nullable_to_non_nullable -as lk.Participant, +as WebRTCParticipant, )); } /// Create a copy of CallParticipantLive @@ -444,7 +444,7 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( CallParticipant participant, lk.Participant remoteParticipant)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( CallParticipant participant, WebRTCParticipant remoteParticipant)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _CallParticipantLive() when $default != null: return $default(_that.participant,_that.remoteParticipant);case _: @@ -465,7 +465,7 @@ return $default(_that.participant,_that.remoteParticipant);case _: /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( CallParticipant participant, lk.Participant remoteParticipant) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( CallParticipant participant, WebRTCParticipant remoteParticipant) $default,) {final _that = this; switch (_that) { case _CallParticipantLive(): return $default(_that.participant,_that.remoteParticipant);} @@ -482,7 +482,7 @@ return $default(_that.participant,_that.remoteParticipant);} /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( CallParticipant participant, lk.Participant remoteParticipant)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( CallParticipant participant, WebRTCParticipant remoteParticipant)? $default,) {final _that = this; switch (_that) { case _CallParticipantLive() when $default != null: return $default(_that.participant,_that.remoteParticipant);case _: @@ -501,7 +501,7 @@ class _CallParticipantLive extends CallParticipantLive with DiagnosticableTreeMi @override final CallParticipant participant; -@override final lk.Participant remoteParticipant; +@override final WebRTCParticipant remoteParticipant; /// Create a copy of CallParticipantLive /// with the given fields replaced by the non-null parameter values. @@ -539,7 +539,7 @@ abstract mixin class _$CallParticipantLiveCopyWith<$Res> implements $CallPartici factory _$CallParticipantLiveCopyWith(_CallParticipantLive value, $Res Function(_CallParticipantLive) _then) = __$CallParticipantLiveCopyWithImpl; @override @useResult $Res call({ - CallParticipant participant, lk.Participant remoteParticipant + CallParticipant participant, WebRTCParticipant remoteParticipant }); @@ -560,7 +560,7 @@ class __$CallParticipantLiveCopyWithImpl<$Res> return _then(_CallParticipantLive( participant: null == participant ? _self.participant : participant // ignore: cast_nullable_to_non_nullable as CallParticipant,remoteParticipant: null == remoteParticipant ? _self.remoteParticipant : remoteParticipant // ignore: cast_nullable_to_non_nullable -as lk.Participant, +as WebRTCParticipant, )); } diff --git a/lib/pods/chat/call.g.dart b/lib/pods/chat/call.g.dart index 57a92fde..a5c0ec1f 100644 --- a/lib/pods/chat/call.g.dart +++ b/lib/pods/chat/call.g.dart @@ -6,7 +6,7 @@ part of 'call.dart'; // RiverpodGenerator // ************************************************************************** -String _$callNotifierHash() => r'a8ca3f625c0db3ad9992033ae70864ce15efc281'; +String _$callNotifierHash() => r'91e546c8711d1b46740ad592cbe481173b227e7b'; /// See also [CallNotifier]. @ProviderFor(CallNotifier) diff --git a/lib/pods/chat/webrtc_manager.dart b/lib/pods/chat/webrtc_manager.dart new file mode 100644 index 00000000..1478e571 --- /dev/null +++ b/lib/pods/chat/webrtc_manager.dart @@ -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 _participants = {}; + final Map _peerConnections = {}; + + MediaStream? _localStream; + final StreamController _participantController = + StreamController.broadcast(); + final StreamController _participantLeftController = + StreamController.broadcast(); + + Stream get onParticipantJoined => + _participantController.stream; + Stream get onParticipantLeft => _participantLeftController.stream; + + WebRTCManager({required this.roomId, required this.serverUrl}) { + _signaling = WebRTCSignaling(roomId: roomId); + } + + Future initialize(Ref ref) async { + await _initializeLocalStream(); + _setupSignalingListeners(); + await _signaling.connect(ref); + } + + Future _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 _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 _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 _handleOffer( + String from, + SnAccount account, + Map 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 _handleAnswer(String from, Map 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 _handleIceCandidate( + String from, + Map 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 toggleMicrophone(bool enabled) async { + if (_localStream != null) { + final audioTracks = _localStream!.getAudioTracks(); + for (final track in audioTracks) { + track.enabled = enabled; + } + } + } + + Future toggleCamera(bool enabled) async { + if (_localStream != null) { + _localStream!.getVideoTracks().forEach((track) { + track.enabled = enabled; + }); + } + } + + List 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(); + } +} diff --git a/lib/pods/chat/webrtc_signaling.dart b/lib/pods/chat/webrtc_signaling.dart new file mode 100644 index 00000000..4379de4f --- /dev/null +++ b/lib/pods/chat/webrtc_signaling.dart @@ -0,0 +1,204 @@ +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(); + } +} diff --git a/lib/pods/chat/webrtc_signaling.freezed.dart b/lib/pods/chat/webrtc_signaling.freezed.dart new file mode 100644 index 00000000..4a6df4d4 --- /dev/null +++ b/lib/pods/chat/webrtc_signaling.freezed.dart @@ -0,0 +1,611 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'webrtc_signaling.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$SignalingMessage implements DiagnosticableTreeMixin { + + String get type;// CHANGED: Added 'to' field for directed messaging + String? get to; String get accountId; SnAccount get account; Map get data; +/// Create a copy of SignalingMessage +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$SignalingMessageCopyWith get copyWith => _$SignalingMessageCopyWithImpl(this as SignalingMessage, _$identity); + + /// Serializes this SignalingMessage to a JSON map. + Map toJson(); + +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'SignalingMessage')) + ..add(DiagnosticsProperty('type', type))..add(DiagnosticsProperty('to', to))..add(DiagnosticsProperty('accountId', accountId))..add(DiagnosticsProperty('account', account))..add(DiagnosticsProperty('data', data)); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SignalingMessage&&(identical(other.type, type) || other.type == type)&&(identical(other.to, to) || other.to == to)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&const DeepCollectionEquality().equals(other.data, data)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,type,to,accountId,account,const DeepCollectionEquality().hash(data)); + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + return 'SignalingMessage(type: $type, to: $to, accountId: $accountId, account: $account, data: $data)'; +} + + +} + +/// @nodoc +abstract mixin class $SignalingMessageCopyWith<$Res> { + factory $SignalingMessageCopyWith(SignalingMessage value, $Res Function(SignalingMessage) _then) = _$SignalingMessageCopyWithImpl; +@useResult +$Res call({ + String type, String? to, String accountId, SnAccount account, Map data +}); + + +$SnAccountCopyWith<$Res> get account; + +} +/// @nodoc +class _$SignalingMessageCopyWithImpl<$Res> + implements $SignalingMessageCopyWith<$Res> { + _$SignalingMessageCopyWithImpl(this._self, this._then); + + final SignalingMessage _self; + final $Res Function(SignalingMessage) _then; + +/// Create a copy of SignalingMessage +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? to = freezed,Object? accountId = null,Object? account = null,Object? data = null,}) { + return _then(_self.copyWith( +type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable +as String,to: freezed == to ? _self.to : to // ignore: cast_nullable_to_non_nullable +as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable +as String,account: null == account ? _self.account : account // ignore: cast_nullable_to_non_nullable +as SnAccount,data: null == data ? _self.data : data // ignore: cast_nullable_to_non_nullable +as Map, + )); +} +/// Create a copy of SignalingMessage +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnAccountCopyWith<$Res> get account { + + return $SnAccountCopyWith<$Res>(_self.account, (value) { + return _then(_self.copyWith(account: value)); + }); +} +} + + +/// Adds pattern-matching-related methods to [SignalingMessage]. +extension SignalingMessagePatterns on SignalingMessage { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _SignalingMessage value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _SignalingMessage() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _SignalingMessage value) $default,){ +final _that = this; +switch (_that) { +case _SignalingMessage(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _SignalingMessage value)? $default,){ +final _that = this; +switch (_that) { +case _SignalingMessage() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String type, String? to, String accountId, SnAccount account, Map data)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _SignalingMessage() when $default != null: +return $default(_that.type,_that.to,_that.accountId,_that.account,_that.data);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String type, String? to, String accountId, SnAccount account, Map data) $default,) {final _that = this; +switch (_that) { +case _SignalingMessage(): +return $default(_that.type,_that.to,_that.accountId,_that.account,_that.data);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String type, String? to, String accountId, SnAccount account, Map data)? $default,) {final _that = this; +switch (_that) { +case _SignalingMessage() when $default != null: +return $default(_that.type,_that.to,_that.accountId,_that.account,_that.data);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _SignalingMessage with DiagnosticableTreeMixin implements SignalingMessage { + const _SignalingMessage({required this.type, this.to, required this.accountId, required this.account, required final Map data}): _data = data; + factory _SignalingMessage.fromJson(Map json) => _$SignalingMessageFromJson(json); + +@override final String type; +// CHANGED: Added 'to' field for directed messaging +@override final String? to; +@override final String accountId; +@override final SnAccount account; + final Map _data; +@override Map get data { + if (_data is EqualUnmodifiableMapView) return _data; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_data); +} + + +/// Create a copy of SignalingMessage +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$SignalingMessageCopyWith<_SignalingMessage> get copyWith => __$SignalingMessageCopyWithImpl<_SignalingMessage>(this, _$identity); + +@override +Map toJson() { + return _$SignalingMessageToJson(this, ); +} +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'SignalingMessage')) + ..add(DiagnosticsProperty('type', type))..add(DiagnosticsProperty('to', to))..add(DiagnosticsProperty('accountId', accountId))..add(DiagnosticsProperty('account', account))..add(DiagnosticsProperty('data', data)); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _SignalingMessage&&(identical(other.type, type) || other.type == type)&&(identical(other.to, to) || other.to == to)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&const DeepCollectionEquality().equals(other._data, _data)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,type,to,accountId,account,const DeepCollectionEquality().hash(_data)); + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + return 'SignalingMessage(type: $type, to: $to, accountId: $accountId, account: $account, data: $data)'; +} + + +} + +/// @nodoc +abstract mixin class _$SignalingMessageCopyWith<$Res> implements $SignalingMessageCopyWith<$Res> { + factory _$SignalingMessageCopyWith(_SignalingMessage value, $Res Function(_SignalingMessage) _then) = __$SignalingMessageCopyWithImpl; +@override @useResult +$Res call({ + String type, String? to, String accountId, SnAccount account, Map data +}); + + +@override $SnAccountCopyWith<$Res> get account; + +} +/// @nodoc +class __$SignalingMessageCopyWithImpl<$Res> + implements _$SignalingMessageCopyWith<$Res> { + __$SignalingMessageCopyWithImpl(this._self, this._then); + + final _SignalingMessage _self; + final $Res Function(_SignalingMessage) _then; + +/// Create a copy of SignalingMessage +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? to = freezed,Object? accountId = null,Object? account = null,Object? data = null,}) { + return _then(_SignalingMessage( +type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable +as String,to: freezed == to ? _self.to : to // ignore: cast_nullable_to_non_nullable +as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable +as String,account: null == account ? _self.account : account // ignore: cast_nullable_to_non_nullable +as SnAccount,data: null == data ? _self._data : data // ignore: cast_nullable_to_non_nullable +as Map, + )); +} + +/// Create a copy of SignalingMessage +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnAccountCopyWith<$Res> get account { + + return $SnAccountCopyWith<$Res>(_self.account, (value) { + return _then(_self.copyWith(account: value)); + }); +} +} + + +/// @nodoc +mixin _$WebRTCWelcomeMessage implements DiagnosticableTreeMixin { + + String get userId; String get roomId; String get message; String get timestamp;// CHANGED: Added participants list + List get participants; +/// Create a copy of WebRTCWelcomeMessage +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$WebRTCWelcomeMessageCopyWith get copyWith => _$WebRTCWelcomeMessageCopyWithImpl(this as WebRTCWelcomeMessage, _$identity); + + /// Serializes this WebRTCWelcomeMessage to a JSON map. + Map toJson(); + +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'WebRTCWelcomeMessage')) + ..add(DiagnosticsProperty('userId', userId))..add(DiagnosticsProperty('roomId', roomId))..add(DiagnosticsProperty('message', message))..add(DiagnosticsProperty('timestamp', timestamp))..add(DiagnosticsProperty('participants', participants)); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is WebRTCWelcomeMessage&&(identical(other.userId, userId) || other.userId == userId)&&(identical(other.roomId, roomId) || other.roomId == roomId)&&(identical(other.message, message) || other.message == message)&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp)&&const DeepCollectionEquality().equals(other.participants, participants)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,userId,roomId,message,timestamp,const DeepCollectionEquality().hash(participants)); + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + return 'WebRTCWelcomeMessage(userId: $userId, roomId: $roomId, message: $message, timestamp: $timestamp, participants: $participants)'; +} + + +} + +/// @nodoc +abstract mixin class $WebRTCWelcomeMessageCopyWith<$Res> { + factory $WebRTCWelcomeMessageCopyWith(WebRTCWelcomeMessage value, $Res Function(WebRTCWelcomeMessage) _then) = _$WebRTCWelcomeMessageCopyWithImpl; +@useResult +$Res call({ + String userId, String roomId, String message, String timestamp, List participants +}); + + + + +} +/// @nodoc +class _$WebRTCWelcomeMessageCopyWithImpl<$Res> + implements $WebRTCWelcomeMessageCopyWith<$Res> { + _$WebRTCWelcomeMessageCopyWithImpl(this._self, this._then); + + final WebRTCWelcomeMessage _self; + final $Res Function(WebRTCWelcomeMessage) _then; + +/// Create a copy of WebRTCWelcomeMessage +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? userId = null,Object? roomId = null,Object? message = null,Object? timestamp = null,Object? participants = null,}) { + return _then(_self.copyWith( +userId: null == userId ? _self.userId : userId // ignore: cast_nullable_to_non_nullable +as String,roomId: null == roomId ? _self.roomId : roomId // ignore: cast_nullable_to_non_nullable +as String,message: null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable +as String,timestamp: null == timestamp ? _self.timestamp : timestamp // ignore: cast_nullable_to_non_nullable +as String,participants: null == participants ? _self.participants : participants // ignore: cast_nullable_to_non_nullable +as List, + )); +} + +} + + +/// Adds pattern-matching-related methods to [WebRTCWelcomeMessage]. +extension WebRTCWelcomeMessagePatterns on WebRTCWelcomeMessage { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _WebRTCWelcomeMessage value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _WebRTCWelcomeMessage() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _WebRTCWelcomeMessage value) $default,){ +final _that = this; +switch (_that) { +case _WebRTCWelcomeMessage(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _WebRTCWelcomeMessage value)? $default,){ +final _that = this; +switch (_that) { +case _WebRTCWelcomeMessage() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String userId, String roomId, String message, String timestamp, List participants)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _WebRTCWelcomeMessage() when $default != null: +return $default(_that.userId,_that.roomId,_that.message,_that.timestamp,_that.participants);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String userId, String roomId, String message, String timestamp, List participants) $default,) {final _that = this; +switch (_that) { +case _WebRTCWelcomeMessage(): +return $default(_that.userId,_that.roomId,_that.message,_that.timestamp,_that.participants);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String userId, String roomId, String message, String timestamp, List participants)? $default,) {final _that = this; +switch (_that) { +case _WebRTCWelcomeMessage() when $default != null: +return $default(_that.userId,_that.roomId,_that.message,_that.timestamp,_that.participants);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _WebRTCWelcomeMessage with DiagnosticableTreeMixin implements WebRTCWelcomeMessage { + const _WebRTCWelcomeMessage({required this.userId, required this.roomId, required this.message, required this.timestamp, final List participants = const []}): _participants = participants; + factory _WebRTCWelcomeMessage.fromJson(Map json) => _$WebRTCWelcomeMessageFromJson(json); + +@override final String userId; +@override final String roomId; +@override final String message; +@override final String timestamp; +// CHANGED: Added participants list + final List _participants; +// CHANGED: Added participants list +@override@JsonKey() List get participants { + if (_participants is EqualUnmodifiableListView) return _participants; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_participants); +} + + +/// Create a copy of WebRTCWelcomeMessage +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$WebRTCWelcomeMessageCopyWith<_WebRTCWelcomeMessage> get copyWith => __$WebRTCWelcomeMessageCopyWithImpl<_WebRTCWelcomeMessage>(this, _$identity); + +@override +Map toJson() { + return _$WebRTCWelcomeMessageToJson(this, ); +} +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'WebRTCWelcomeMessage')) + ..add(DiagnosticsProperty('userId', userId))..add(DiagnosticsProperty('roomId', roomId))..add(DiagnosticsProperty('message', message))..add(DiagnosticsProperty('timestamp', timestamp))..add(DiagnosticsProperty('participants', participants)); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _WebRTCWelcomeMessage&&(identical(other.userId, userId) || other.userId == userId)&&(identical(other.roomId, roomId) || other.roomId == roomId)&&(identical(other.message, message) || other.message == message)&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp)&&const DeepCollectionEquality().equals(other._participants, _participants)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,userId,roomId,message,timestamp,const DeepCollectionEquality().hash(_participants)); + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + return 'WebRTCWelcomeMessage(userId: $userId, roomId: $roomId, message: $message, timestamp: $timestamp, participants: $participants)'; +} + + +} + +/// @nodoc +abstract mixin class _$WebRTCWelcomeMessageCopyWith<$Res> implements $WebRTCWelcomeMessageCopyWith<$Res> { + factory _$WebRTCWelcomeMessageCopyWith(_WebRTCWelcomeMessage value, $Res Function(_WebRTCWelcomeMessage) _then) = __$WebRTCWelcomeMessageCopyWithImpl; +@override @useResult +$Res call({ + String userId, String roomId, String message, String timestamp, List participants +}); + + + + +} +/// @nodoc +class __$WebRTCWelcomeMessageCopyWithImpl<$Res> + implements _$WebRTCWelcomeMessageCopyWith<$Res> { + __$WebRTCWelcomeMessageCopyWithImpl(this._self, this._then); + + final _WebRTCWelcomeMessage _self; + final $Res Function(_WebRTCWelcomeMessage) _then; + +/// Create a copy of WebRTCWelcomeMessage +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? userId = null,Object? roomId = null,Object? message = null,Object? timestamp = null,Object? participants = null,}) { + return _then(_WebRTCWelcomeMessage( +userId: null == userId ? _self.userId : userId // ignore: cast_nullable_to_non_nullable +as String,roomId: null == roomId ? _self.roomId : roomId // ignore: cast_nullable_to_non_nullable +as String,message: null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable +as String,timestamp: null == timestamp ? _self.timestamp : timestamp // ignore: cast_nullable_to_non_nullable +as String,participants: null == participants ? _self._participants : participants // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + +// dart format on diff --git a/lib/pods/chat/webrtc_signaling.g.dart b/lib/pods/chat/webrtc_signaling.g.dart new file mode 100644 index 00000000..d1d3710e --- /dev/null +++ b/lib/pods/chat/webrtc_signaling.g.dart @@ -0,0 +1,49 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'webrtc_signaling.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_SignalingMessage _$SignalingMessageFromJson(Map json) => + _SignalingMessage( + type: json['type'] as String, + to: json['to'] as String?, + accountId: json['account_id'] as String, + account: SnAccount.fromJson(json['account'] as Map), + data: json['data'] as Map, + ); + +Map _$SignalingMessageToJson(_SignalingMessage instance) => + { + 'type': instance.type, + 'to': instance.to, + 'account_id': instance.accountId, + 'account': instance.account.toJson(), + 'data': instance.data, + }; + +_WebRTCWelcomeMessage _$WebRTCWelcomeMessageFromJson( + Map json, +) => _WebRTCWelcomeMessage( + userId: json['user_id'] as String, + roomId: json['room_id'] as String, + message: json['message'] as String, + timestamp: json['timestamp'] as String, + participants: + (json['participants'] as List?) + ?.map((e) => CallParticipant.fromJson(e as Map)) + .toList() ?? + const [], +); + +Map _$WebRTCWelcomeMessageToJson( + _WebRTCWelcomeMessage instance, +) => { + 'user_id': instance.userId, + 'room_id': instance.roomId, + 'message': instance.message, + 'timestamp': instance.timestamp, + 'participants': instance.participants.map((e) => e.toJson()).toList(), +}; diff --git a/lib/pods/websocket.dart b/lib/pods/websocket.dart index 0f57792e..54bd4cba 100644 --- a/lib/pods/websocket.dart +++ b/lib/pods/websocket.dart @@ -100,12 +100,16 @@ class WebSocketService { } }, onDone: () { - talker.info('[WebSocket] Connection closed, attempting to reconnect...'); + talker.info( + '[WebSocket] Connection closed, attempting to reconnect...', + ); _scheduleReconnect(); _statusStreamController.sink.add(WebSocketState.disconnected()); }, onError: (error) { - talker.error('[WebSocket] Error occurred: $error, attempting to reconnect...'); + talker.error( + '[WebSocket] Error occurred: $error, attempting to reconnect...', + ); _scheduleReconnect(); _statusStreamController.sink.add( WebSocketState.error(error.toString()), diff --git a/lib/screens/chat/call.dart b/lib/screens/chat/call.dart index dc1ce75a..7c0d46f8 100644 --- a/lib/screens/chat/call.dart +++ b/lib/screens/chat/call.dart @@ -1,5 +1,5 @@ import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart' hide ConnectionState; +import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -9,8 +9,6 @@ import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/chat/call_button.dart'; import 'package:island/widgets/chat/call_overlay.dart'; import 'package:island/widgets/chat/call_participant_tile.dart'; -import 'package:island/widgets/alert.dart'; -import 'package:livekit_client/livekit_client.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:styled_widget/styled_widget.dart'; @@ -26,32 +24,13 @@ class CallScreen extends HookConsumerWidget { useEffect(() { talker.info('[Call] Joining the call...'); - callNotifier.joinRoom(roomId).catchError((_) { - showConfirmAlert( - 'Seems there already has a call connected, do you want override it?', - 'Call already connected', - ).then((value) { - if (value != true) return; - talker.info('[Call] Joining the call... with overrides'); - callNotifier.disconnect(); - callNotifier.dispose(); - callNotifier.joinRoom(roomId); - }); + Future(() { + callNotifier.joinRoom(roomId); }); return null; }, []); - final allAudioOnly = callNotifier.participants.every( - (p) => - !(p.hasVideo && - p.remoteParticipant.trackPublications.values.any( - (pub) => - pub.track != null && - pub.kind == TrackType.VIDEO && - !pub.muted && - !pub.isDisposed, - )), - ); + final allAudioOnly = callNotifier.participants.every((p) => !p.hasVideo); return AppScaffold( isNoBackground: false, @@ -67,12 +46,7 @@ class CallScreen extends HookConsumerWidget { Text( callState.isConnected ? formatDuration(callState.duration) - : (switch (callNotifier.room?.connectionState) { - ConnectionState.connected => 'connected', - ConnectionState.connecting => 'connecting', - ConnectionState.reconnecting => 'reconnecting', - _ => 'disconnected', - }).tr(), + : 'connecting'.tr(), style: const TextStyle(fontSize: 14), ), ], @@ -159,19 +133,7 @@ class CallScreen extends HookConsumerWidget { // Stage view: show main speaker(s) large, others in row final mainSpeakers = - participants - .where( - (p) => p - .remoteParticipant - .trackPublications - .values - .any( - (pub) => - pub.track != null && - pub.kind == TrackType.VIDEO, - ), - ) - .toList(); + participants.where((p) => p.hasVideo).toList(); if (mainSpeakers.isEmpty && participants.isNotEmpty) { mainSpeakers.add(participants.first); } diff --git a/lib/widgets/chat/call_button.dart b/lib/widgets/chat/call_button.dart index 9d0508b4..e335e17d 100644 --- a/lib/widgets/chat/call_button.dart +++ b/lib/widgets/chat/call_button.dart @@ -16,7 +16,7 @@ Future ongoingCall(Ref ref, String roomId) async { if (roomId.isEmpty) return null; try { final apiClient = ref.watch(apiClientProvider); - final resp = await apiClient.get('/sphere/chat/realtime/$roomId'); + final resp = await apiClient.get('/sphere/chat/realtime/$roomId/status'); return SnRealtimeCall.fromJson(resp.data); } catch (e) { if (e is DioException && e.response?.statusCode == 404) { diff --git a/lib/widgets/chat/call_button.g.dart b/lib/widgets/chat/call_button.g.dart index d2d28f7e..45abfb07 100644 --- a/lib/widgets/chat/call_button.g.dart +++ b/lib/widgets/chat/call_button.g.dart @@ -6,7 +6,7 @@ part of 'call_button.dart'; // RiverpodGenerator // ************************************************************************** -String _$ongoingCallHash() => r'48031badb79efa07aefb3a4fc51635be457bd3f9'; +String _$ongoingCallHash() => r'0f14b36393276720a06190cab3dc8d5e4c88cd57'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/widgets/chat/call_overlay.dart b/lib/widgets/chat/call_overlay.dart index 2688c48a..5f6c9013 100644 --- a/lib/widgets/chat/call_overlay.dart +++ b/lib/widgets/chat/call_overlay.dart @@ -10,7 +10,7 @@ import 'package:island/widgets/chat/call_participant_tile.dart'; import 'package:island/widgets/content/sheet.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:styled_widget/styled_widget.dart'; -import 'package:livekit_client/livekit_client.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart'; class CallControlsBar extends HookConsumerWidget { const CallControlsBar({super.key}); @@ -194,9 +194,16 @@ class CallControlsBar extends HookConsumerWidget { String deviceType, ) async { try { - final devices = await Hardware.instance.enumerateDevices( - type: deviceType, - ); + final devices = await navigator.mediaDevices.enumerateDevices(); + final filteredDevices = + devices.where((device) { + if (deviceType == 'videoinput') { + return device.kind == 'videoinput'; + } else if (deviceType == 'audioinput') { + return device.kind == 'audioinput'; + } + return false; + }).toList(); if (!context.mounted) return; @@ -209,9 +216,9 @@ class CallControlsBar extends HookConsumerWidget { ? 'selectCamera'.tr() : 'selectMicrophone'.tr(), child: ListView.builder( - itemCount: devices.length, + itemCount: filteredDevices.length, itemBuilder: (context, index) { - final device = devices[index]; + final device = filteredDevices[index]; return ListTile( title: Text( device.label.isNotEmpty @@ -236,35 +243,12 @@ class CallControlsBar extends HookConsumerWidget { Future _switchDevice( BuildContext context, WidgetRef ref, - MediaDevice device, + MediaDeviceInfo device, String deviceType, ) async { try { - final callNotifier = ref.read(callNotifierProvider.notifier); - - if (deviceType == 'videoinput') { - // Switch camera device - final localParticipant = callNotifier.room?.localParticipant; - final videoTrack = - localParticipant?.videoTrackPublications.firstOrNull?.track; - - if (videoTrack is LocalVideoTrack) { - await videoTrack.switchCamera(device.deviceId); - } - } else if (deviceType == 'audioinput') { - // Switch microphone device - final localParticipant = callNotifier.room?.localParticipant; - final audioTrack = - localParticipant?.audioTrackPublications.firstOrNull?.track; - - if (audioTrack is LocalAudioTrack) { - // For audio devices, we need to restart the track with new device - await audioTrack.restartTrack( - AudioCaptureOptions(deviceId: device.deviceId), - ); - } - } - + // TODO: Implement device switching for WebRTC + // This would require restarting the media stream with the new device if (context.mounted) { showSnackBar( 'switchedTo'.tr( @@ -289,31 +273,9 @@ class CallOverlayBar extends HookConsumerWidget { if (!callState.isConnected) return const SizedBox.shrink(); final lastSpeaker = - callNotifier.participants - .where( - (element) => element.remoteParticipant.lastSpokeAt != null, - ) - .isEmpty + callNotifier.participants.isNotEmpty ? callNotifier.participants.first - : callNotifier.participants - .where( - (element) => element.remoteParticipant.lastSpokeAt != null, - ) - .fold( - callNotifier.participants.first, - (value, element) => - element.remoteParticipant.lastSpokeAt != null && - (value.remoteParticipant.lastSpokeAt == null || - element.remoteParticipant.lastSpokeAt! - .compareTo( - value - .remoteParticipant - .lastSpokeAt!, - ) > - 0) - ? element - : value, - ); + : null; final actionButtonStyle = ButtonStyle( minimumSize: const MaterialStatePropertyAll(Size(24, 24)), @@ -330,17 +292,16 @@ class CallOverlayBar extends HookConsumerWidget { children: [ Builder( builder: (context) { - if (callNotifier.localParticipant == null) { - return CircularProgressIndicator().center(); + if (lastSpeaker == null) { + return const CircularProgressIndicator(); } return SizedBox( width: 40, height: 40, - child: - SpeakingRippleAvatar( - live: lastSpeaker, - size: 36, - ).center(), + child: SpeakingRippleAvatar( + live: lastSpeaker, + size: 36, + ), ); }, ), @@ -348,7 +309,9 @@ class CallOverlayBar extends HookConsumerWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('@${lastSpeaker.participant.identity}').bold(), + Text( + '@${lastSpeaker?.participant.identity ?? 'Unknown'}', + ), Text( formatDuration(callState.duration), style: Theme.of(context).textTheme.bodySmall, diff --git a/lib/widgets/chat/call_participant_card.dart b/lib/widgets/chat/call_participant_card.dart index 6f9f278d..504f486f 100644 --- a/lib/widgets/chat/call_participant_card.dart +++ b/lib/widgets/chat/call_participant_card.dart @@ -7,7 +7,6 @@ import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/pods/chat/call.dart'; import 'package:island/widgets/account/account_nameplate.dart'; -import 'package:livekit_client/livekit_client.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:styled_widget/styled_widget.dart'; @@ -66,19 +65,17 @@ class CallParticipantCard extends HookConsumerWidget { children: [ const Icon(Symbols.wifi, size: 16), const Gap(8), - Text(switch (live.remoteParticipant.connectionQuality) { - ConnectionQuality.excellent => 'Excellent', - ConnectionQuality.good => 'Good', - ConnectionQuality.poor => 'Bad', - ConnectionQuality.lost => 'Lost', - _ => 'Connecting', - }), + Text( + live.remoteParticipant.isConnected + ? 'Connected' + : 'Connecting', + ), ], ), ], ).padding(horizontal: 20, top: 16), AccountNameplate( - name: live.participant.identity, + name: live.remoteParticipant.userinfo.name, isOutlined: false, ), ], diff --git a/lib/widgets/chat/call_participant_tile.dart b/lib/widgets/chat/call_participant_tile.dart index 75f67278..70e66974 100644 --- a/lib/widgets/chat/call_participant_tile.dart +++ b/lib/widgets/chat/call_participant_tile.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/pods/chat/call.dart'; -import 'package:island/screens/account/profile.dart'; import 'package:island/widgets/chat/call_participant_card.dart'; import 'package:island/widgets/content/cloud_files.dart'; -import 'package:livekit_client/livekit_client.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:styled_widget/styled_widget.dart'; @@ -16,10 +15,9 @@ class SpeakingRippleAvatar extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final account = ref.watch(accountProvider(live.participant.identity)); - final avatarRadius = size / 2; - final clampedLevel = live.remoteParticipant.audioLevel.clamp(0.0, 1.0); + // TODO: Implement audio level detection for WebRTC + final clampedLevel = 0.0; final rippleRadius = avatarRadius + clampedLevel * (size * 0.333); return SizedBox( width: size + 8, @@ -27,7 +25,7 @@ class SpeakingRippleAvatar extends HookConsumerWidget { child: TweenAnimationBuilder( tween: Tween( begin: avatarRadius, - end: live.remoteParticipant.isSpeaking ? rippleRadius : avatarRadius, + end: live.isSpeaking ? rippleRadius : avatarRadius, ), duration: const Duration(milliseconds: 250), curve: Curves.easeOut, @@ -35,7 +33,7 @@ class SpeakingRippleAvatar extends HookConsumerWidget { return Stack( alignment: Alignment.center, children: [ - if (live.remoteParticipant.isSpeaking) + if (live.isSpeaking) Container( width: animatedRadius * 2, height: animatedRadius * 2, @@ -49,28 +47,15 @@ class SpeakingRippleAvatar extends HookConsumerWidget { height: size, alignment: Alignment.center, decoration: BoxDecoration(shape: BoxShape.circle), - child: account.when( - data: - (value) => CallParticipantGestureDetector( - participant: live, - child: ProfilePictureWidget( - file: value.profile.picture, - radius: size / 2, - ), - ), - error: - (_, _) => CircleAvatar( - radius: size / 2, - child: const Icon(Symbols.person_remove), - ), - loading: - () => CircleAvatar( - radius: size / 2, - child: CircularProgressIndicator(), - ), + child: CallParticipantGestureDetector( + participant: live, + child: ProfilePictureWidget( + file: live.remoteParticipant.userinfo.profile.picture, + radius: size / 2, + ), ), ), - if (live.remoteParticipant.isMuted) + if (live.isMuted) Positioned( bottom: 4, right: 4, @@ -103,25 +88,15 @@ class CallParticipantTile extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final hasVideo = - live.hasVideo && - live.remoteParticipant.trackPublications.values - .where((pub) => pub.track != null && pub.kind == TrackType.VIDEO) - .isNotEmpty; - - if (hasVideo) { + if (live.hasVideo && live.remoteParticipant.remoteStream != null) { return Stack( fit: StackFit.loose, children: [ AspectRatio( aspectRatio: 16 / 9, - child: VideoTrackRenderer( - live.remoteParticipant.trackPublications.values - .where((track) => track.kind == TrackType.VIDEO) - .first - .track - as VideoTrack, - renderMode: VideoRenderMode.platformView, + child: RTCVideoView( + RTCVideoRenderer() + ..srcObject = live.remoteParticipant.remoteStream, ), ), Positioned( diff --git a/lib/widgets/content/alert.web.dart b/lib/widgets/content/alert.web.dart index 397e719f..b1e97eaa 100644 --- a/lib/widgets/content/alert.web.dart +++ b/lib/widgets/content/alert.web.dart @@ -44,10 +44,12 @@ void showInfoAlert(String message, String title) async { Future showConfirmAlert(String message, String title) async { final result = await js.context.callMethod('swal', [ - title, - message, - 'question', - {'buttons': true}, + js.JsObject.jsify({ + 'title': title, + 'text': message, + 'icon': 'info', + 'buttons': {'cancel': true, 'confirm': true}, + }), ]); return result == true; } diff --git a/lib/widgets/payment/payment_overlay.dart b/lib/widgets/payment/payment_overlay.dart index 580d6ba4..e87e8310 100644 --- a/lib/widgets/payment/payment_overlay.dart +++ b/lib/widgets/payment/payment_overlay.dart @@ -193,10 +193,10 @@ class _PaymentContentState extends ConsumerState<_PaymentContent> { // Perform biometric authentication final bool didAuthenticate = await _localAuth.authenticate( localizedReason: 'biometricPrompt'.tr(), - options: const AuthenticationOptions( - biometricOnly: true, - stickyAuth: true, - ), + // options: const AuthenticationOptions( + // biometricOnly: true, + // stickyAuth: true, + // ), ); if (didAuthenticate) { diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index d6aee932..e63f2d27 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -15,7 +15,6 @@ #include #include #include -#include #include #include #include @@ -57,9 +56,6 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) irondash_engine_context_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "IrondashEngineContextPlugin"); irondash_engine_context_plugin_register_with_registrar(irondash_engine_context_registrar); - g_autoptr(FlPluginRegistrar) livekit_client_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "LiveKitPlugin"); - live_kit_plugin_register_with_registrar(livekit_client_registrar); g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 6ff0e93a..a121698f 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -12,7 +12,6 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_webrtc gtk irondash_engine_context - livekit_client media_kit_libs_linux media_kit_video pasteboard diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 92655d2d..b0ae1cca 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,7 +6,6 @@ import FlutterMacOS import Foundation import app_links -import connectivity_plus import device_info_plus import file_picker import file_saver @@ -24,7 +23,6 @@ import flutter_udid import flutter_webrtc import gal import irondash_engine_context -import livekit_client import local_auth_darwin import media_kit_libs_macos_video import media_kit_video @@ -48,7 +46,6 @@ import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) - ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin")) @@ -66,7 +63,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin")) - LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin")) LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin")) MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 0465ca0c..b6834384 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -289,22 +289,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" - connectivity_plus: - dependency: transitive - description: - name: connectivity_plus - sha256: b5e72753cf63becce2c61fd04dfe0f1c430cc5278b53a1342dc5ad839eab29ec - url: "https://pub.dev" - source: hosted - version: "6.1.5" - connectivity_plus_platform_interface: - dependency: transitive - description: - name: connectivity_plus_platform_interface - sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" - url: "https://pub.dev" - source: hosted - version: "2.0.1" console: dependency: transitive description: @@ -930,10 +914,10 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: "7ed76be64e8a7d01dfdf250b8434618e2a028c9dfa2a3c41dc9b531d4b3fc8a5" + sha256: "19ffb0a8bb7407875555e5e98d7343a633bb73707bae6c6a5f37c90014077875" url: "https://pub.dev" source: hosted - version: "19.4.2" + version: "19.5.0" flutter_local_notifications_linux: dependency: transitive description: @@ -991,10 +975,10 @@ packages: dependency: "direct main" description: name: flutter_native_splash - sha256: "8321a6d11a8d13977fa780c89de8d257cce3d841eecfb7a4cadffcc4f12d82dc" + sha256: "4fb9f4113350d3a80841ce05ebf1976a36de622af7d19aca0ca9a9911c7ff002" url: "https://pub.dev" source: hosted - version: "2.4.6" + version: "2.4.7" flutter_otp_text_field: dependency: "direct main" description: @@ -1201,10 +1185,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: c752e2d08d088bf83742cb05bf83003f3e9d276ff1519b5c92f9d5e60e5ddd23 + sha256: e1d7ffb0db475e6e845eb58b44768f50b830e23960e3df6908924acd8f7f70ea url: "https://pub.dev" source: hosted - version: "16.2.4" + version: "16.2.5" google_fonts: dependency: "direct main" description: @@ -1321,10 +1305,10 @@ packages: dependency: "direct main" description: name: image_picker_android - sha256: dd7a61daaa5896cc34b7bc95f66c60225ae6bee0d167dde0e21a9d9016cac0dc + sha256: "58a85e6f09fe9c4484d53d18a0bd6271b72c53fce1d05e6f745ae36d8c18efca" url: "https://pub.dev" source: hosted - version: "0.8.13+4" + version: "0.8.13+5" image_picker_for_web: dependency: transitive description: @@ -1469,38 +1453,30 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" - livekit_client: - dependency: "direct main" - description: - name: livekit_client - sha256: "4c1663c1e6ac20a743d9a46c7bc71f17e1949db99d245750c68661d554e30cd2" - url: "https://pub.dev" - source: hosted - version: "2.5.1" local_auth: dependency: "direct main" description: name: local_auth - sha256: "434d854cf478f17f12ab29a76a02b3067f86a63a6d6c4eb8fbfdcfe4879c1b7b" + sha256: a4f1bf57f0236a4aeb5e8f0ec180e197f4b112a3456baa6c1e73b546630b0422 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "3.0.0" local_auth_android: dependency: transitive description: name: local_auth_android - sha256: b2446c74fab1db37f828d4c54adaa3f003df80a29f5cbd710bbb8883d302e991 + sha256: d836715ed95b16b2de3a8c47a88ba5e607976bb1e27c9446d193152ea1429fae url: "https://pub.dev" source: hosted - version: "1.0.55" + version: "2.0.0" local_auth_darwin: dependency: transitive description: name: local_auth_darwin - sha256: "699873970067a40ef2f2c09b4c72eb1cfef64224ef041b3df9fdc5c4c1f91f49" + sha256: "15d9db4ad4d58a11d7269e55d46ff8d49ed5e856226c8a5a91280f0d7c37b3a6" url: "https://pub.dev" source: hosted - version: "1.6.1" + version: "2.0.0" local_auth_platform_interface: dependency: transitive description: @@ -1513,10 +1489,10 @@ packages: dependency: transitive description: name: local_auth_windows - sha256: bc4e66a29b0fdf751aafbec923b5bed7ad6ed3614875d8151afe2578520b2ab5 + sha256: d95535a73eddf57ce5930d5e78a0fa4f294c31981fdeeee83325b797302be454 url: "https://pub.dev" source: hosted - version: "1.0.11" + version: "2.0.0" logger: dependency: transitive description: @@ -1669,14 +1645,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" - mime_type: - dependency: transitive - description: - name: mime_type - sha256: d652b613e84dac1af28030a9fba82c0999be05b98163f9e18a0849c6e63838bb - url: "https://pub.dev" - source: hosted - version: "1.0.1" modal_bottom_sheet: dependency: "direct main" description: @@ -1709,14 +1677,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" - nm: - dependency: transitive - description: - name: nm - sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" - url: "https://pub.dev" - source: hosted - version: "0.5.0" octo_image: dependency: transitive description: @@ -1933,14 +1893,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.4" - protobuf: - dependency: transitive - description: - name: protobuf - sha256: de9c9eb2c33f8e933a42932fe1dc504800ca45ebc3d673e6ed7f39754ee4053e - url: "https://pub.dev" - source: hosted - version: "4.2.0" provider: dependency: transitive description: @@ -2206,14 +2158,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" - sdp_transform: - dependency: transitive - description: - name: sdp_transform - sha256: "73e412a5279a5c2de74001535208e20fff88f225c9a4571af0f7146202755e45" - url: "https://pub.dev" - source: hosted - version: "0.3.2" share_plus: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index d9d42ec9..3149dc55 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,7 +38,7 @@ dependencies: cupertino_icons: ^1.0.8 flutter_hooks: ^0.21.3+1 hooks_riverpod: ^2.6.1 - go_router: ^16.2.4 + go_router: ^16.2.5 styled_widget: ^0.4.1 shared_preferences: ^2.5.3 flutter_riverpod: ^2.6.1 @@ -75,7 +75,7 @@ dependencies: file_picker: ^10.3.3 riverpod_annotation: ^2.6.1 image_picker_platform_interface: ^2.11.0 - image_picker_android: ^0.8.13+4 + image_picker_android: ^0.8.13+5 super_context_menu: ^0.9.1 modal_bottom_sheet: ^3.0.0 firebase_messaging: ^16.0.3 @@ -97,12 +97,12 @@ dependencies: avatar_stack: ^3.0.0 markdown_widget: ^2.3.2+8 visibility_detector: ^0.4.0+2 - flutter_native_splash: ^2.4.6 + flutter_native_splash: ^2.4.7 photo_view: ^0.15.0 gal: ^2.3.2 dismissible_page: ^1.0.2 super_sliver_list: ^0.4.1 - livekit_client: ^2.5.1 + pasteboard: ^0.4.0 flutter_colorpicker: ^1.1.0 image: ^4.5.4 @@ -117,7 +117,7 @@ dependencies: sign_in_with_apple: ^7.0.1 flutter_svg: ^2.2.1 native_exif: ^0.6.2 - local_auth: ^2.3.0 + local_auth: ^3.0.0 flutter_secure_storage: ^9.2.4 flutter_math_fork: ^0.7.4 share_plus: ^12.0.0 @@ -142,7 +142,7 @@ dependencies: file_saver: ^0.3.1 tray_manager: ^0.5.1 flutter_webrtc: ^1.2.0 - flutter_local_notifications: ^19.4.2 + flutter_local_notifications: ^19.5.0 wakelock_plus: ^1.4.0 slide_countdown: ^2.0.2 shelf: ^1.4.2 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 73c96506..387a4d68 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,7 +7,6 @@ #include "generated_plugin_registrant.h" #include -#include #include #include #include @@ -20,7 +19,6 @@ #include #include #include -#include #include #include #include @@ -40,8 +38,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { AppLinksPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("AppLinksPluginCApi")); - ConnectivityPlusWindowsPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); DartIpcPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("DartIpcPluginCApi")); FileSaverPluginRegisterWithRegistrar( @@ -66,8 +62,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("GalPluginCApi")); IrondashEngineContextPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("IrondashEngineContextPluginCApi")); - LiveKitPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("LiveKitPlugin")); LocalAuthPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("LocalAuthPlugin")); MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index c6964180..e39f7a77 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,7 +4,6 @@ list(APPEND FLUTTER_PLUGIN_LIST app_links - connectivity_plus dart_ipc file_saver file_selector_windows @@ -17,7 +16,6 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_webrtc gal irondash_engine_context - livekit_client local_auth_windows media_kit_libs_windows_video media_kit_video