Compare commits
16 Commits
3.3.0+136
...
refactor/w
Author | SHA1 | Date | |
---|---|---|---|
0622498f4e | |||
844efcda1a | |||
98e39cce6a | |||
0c459bf7e3 | |||
a2576abee0 | |||
f4b28c3fa2 | |||
43d767bc03
|
|||
0910be88ef
|
|||
e96b1fd9d4
|
|||
3f83bbc1d8
|
|||
001549b190 | |||
4595865ad3 | |||
|
1834643167 | ||
|
0e816eaa3e | ||
|
7c1f24b824 | ||
|
b7d44d96ba |
@@ -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
|
||||
|
@@ -120,13 +120,24 @@ void main() async {
|
||||
windowButtonVisibility: true,
|
||||
);
|
||||
windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||
final env = Platform.environment;
|
||||
final isWayland = env.containsKey('WAYLAND_DISPLAY');
|
||||
|
||||
if (isWayland) {
|
||||
try {
|
||||
await windowManager.setAsFrameless();
|
||||
} catch (e) {
|
||||
debugPrint('[Wayland] setAsFrameless failed: $e');
|
||||
}
|
||||
}
|
||||
await windowManager.setMinimumSize(defaultSize);
|
||||
await windowManager.show();
|
||||
await windowManager.focus();
|
||||
final opacity = prefs.getDouble(kAppWindowOpacity) ?? 1.0;
|
||||
await windowManager.setOpacity(opacity);
|
||||
talker.info(
|
||||
"[SplashScreen] Desktop window is ready with size: ${initialSize.width}x${initialSize.height}",
|
||||
"[SplashScreen] Desktop window is ready with size: ${initialSize.width}x${initialSize.height}"
|
||||
"${isWayland ? " (Wayland frameless fix applied)" : ""}",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -2241,7 +2241,7 @@ as List<CallParticipant>,
|
||||
/// @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<CallParticipant> get copyWith => _$CallParticipantCopyW
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is CallParticipant&&(identical(other.identity, identity) || other.identity == identity)&&(identical(other.name, name) || other.name == name)&&(identical(other.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 extends Object?>(TResult Function( String identity, String name, DateTime joinedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String identity, String name, String accountId, SnAccount? account, DateTime joinedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _CallParticipant() when $default != null:
|
||||
return $default(_that.identity,_that.name,_that.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 extends Object?>(TResult Function( String identity, String name, DateTime joinedAt) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String identity, String name, String accountId, SnAccount? account, DateTime joinedAt) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _CallParticipant():
|
||||
return $default(_that.identity,_that.name,_that.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 extends Object?>(TResult? Function( String identity, String name, DateTime joinedAt)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String identity, String name, String accountId, SnAccount? account, DateTime joinedAt)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _CallParticipant() when $default != null:
|
||||
return $default(_that.identity,_that.name,_that.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<String, dynamic> 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<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CallParticipant&&(identical(other.identity, identity) || other.identity == identity)&&(identical(other.name, name) || other.name == name)&&(identical(other.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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@@ -275,6 +275,11 @@ _CallParticipant _$CallParticipantFromJson(Map<String, dynamic> 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<String, dynamic>),
|
||||
joinedAt: DateTime.parse(json['joined_at'] as String),
|
||||
);
|
||||
|
||||
@@ -282,6 +287,8 @@ Map<String, dynamic> _$CallParticipantToJson(_CallParticipant instance) =>
|
||||
<String, dynamic>{
|
||||
'identity': instance.identity,
|
||||
'name': instance.name,
|
||||
'account_id': instance.accountId,
|
||||
'account': instance.account?.toJson(),
|
||||
'joined_at': instance.joinedAt.toIso8601String(),
|
||||
};
|
||||
|
||||
|
@@ -3,13 +3,15 @@ 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/pods/userinfo.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/models/account.dart';
|
||||
import 'package:island/pods/chat/webrtc_manager.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
import 'package:island/talker.dart';
|
||||
|
||||
@@ -43,193 +45,212 @@ 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 {
|
||||
// Use the actual audio level from WebRTC monitoring
|
||||
return remoteParticipant.audioLevel > 0.1; // Threshold for speaking
|
||||
}
|
||||
|
||||
bool get hasVideo => remoteParticipant.hasVideo;
|
||||
bool get hasAudio => remoteParticipant.hasAudio;
|
||||
double get audioLevel => remoteParticipant.audioLevel;
|
||||
|
||||
bool get isMuted => !remoteParticipant.isAudioEnabled;
|
||||
bool get isScreenSharing => remoteParticipant.isVideoEnabled; // Simplified
|
||||
bool get isScreenSharingWithAudio => false; // TODO: Implement screen sharing
|
||||
|
||||
bool get hasVideo => remoteParticipant.isVideoEnabled;
|
||||
bool get hasAudio => remoteParticipant.isAudioEnabled;
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class CallNotifier extends _$CallNotifier {
|
||||
lk.Room? _room;
|
||||
lk.LocalParticipant? _localParticipant;
|
||||
WebRTCManager? _webrtcManager;
|
||||
List<CallParticipantLive> _participants = [];
|
||||
final Map<String, CallParticipant> _participantInfoByIdentity = {};
|
||||
lk.EventsListener? _roomListener;
|
||||
StreamSubscription<WebRTCParticipant>? _participantJoinedSubscription;
|
||||
StreamSubscription<String>? _participantLeftSubscription;
|
||||
|
||||
List<CallParticipantLive> get participants =>
|
||||
List.unmodifiable(_participants);
|
||||
lk.LocalParticipant? get localParticipant => _localParticipant;
|
||||
|
||||
Map<String, double> participantsVolumes = {};
|
||||
|
||||
Timer? _durationTimer;
|
||||
|
||||
lk.Room? get room => _room;
|
||||
String? _roomId;
|
||||
String? get roomId => _roomId;
|
||||
WebRTCManager? get webrtcManager => _webrtcManager;
|
||||
|
||||
@override
|
||||
CallState build() {
|
||||
// Subscribe to websocket updates
|
||||
return const CallState(
|
||||
isConnected: false,
|
||||
isMicrophoneEnabled: true,
|
||||
isCameraEnabled: false,
|
||||
isMicrophoneEnabled:
|
||||
true, // Audio enabled by default (matches WebRTC init)
|
||||
isCameraEnabled: true, // Video enabled by default (matches WebRTC init)
|
||||
isScreenSharing: false,
|
||||
isSpeakerphone: true,
|
||||
);
|
||||
}
|
||||
|
||||
void _initRoomListeners() {
|
||||
if (_room == null) return;
|
||||
_roomListener?.dispose();
|
||||
_roomListener = _room!.createListener();
|
||||
_room!.addListener(_onRoomChange);
|
||||
_roomListener!
|
||||
..on<lk.ParticipantConnectedEvent>((e) {
|
||||
_refreshLiveParticipants();
|
||||
})
|
||||
..on<lk.RoomDisconnectedEvent>((e) {
|
||||
_participants = [];
|
||||
void _initWebRTCListeners() {
|
||||
_participantJoinedSubscription?.cancel();
|
||||
_participantLeftSubscription?.cancel();
|
||||
|
||||
_participantJoinedSubscription = _webrtcManager?.onParticipantJoined.listen(
|
||||
(participant) {
|
||||
_updateLiveParticipantsFromWebRTC();
|
||||
},
|
||||
);
|
||||
|
||||
_participantLeftSubscription = _webrtcManager?.onParticipantLeft.listen((
|
||||
participantId,
|
||||
) {
|
||||
_participants.removeWhere((p) => p.remoteParticipant.id == participantId);
|
||||
state = state.copyWith();
|
||||
});
|
||||
|
||||
// Add local participant immediately when WebRTC is initialized
|
||||
final userinfo = ref.watch(userInfoProvider);
|
||||
if (userinfo.value != null) {
|
||||
_addLocalParticipant(userinfo.value!);
|
||||
}
|
||||
}
|
||||
|
||||
void _onRoomChange() {
|
||||
_refreshLiveParticipants();
|
||||
}
|
||||
void _addLocalParticipant(SnAccount userinfo) {
|
||||
if (_webrtcManager == null) return;
|
||||
|
||||
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!,
|
||||
// Remove any existing local participant first
|
||||
_participants.removeWhere((p) => p.participant.identity == userinfo.id);
|
||||
|
||||
// Add local participant (current user)
|
||||
final localParticipant = CallParticipantLive(
|
||||
participant: CallParticipant(
|
||||
identity: userinfo.id, // Use roomId as local identity
|
||||
name: userinfo.name,
|
||||
accountId: userinfo.id,
|
||||
account: userinfo,
|
||||
joinedAt: DateTime.now(),
|
||||
),
|
||||
remoteParticipant: WebRTCParticipant(
|
||||
id: _webrtcManager!.roomId,
|
||||
name: userinfo.nick,
|
||||
userinfo: userinfo,
|
||||
isLocal: true,
|
||||
)..remoteStream = _webrtcManager!.localStream, // Access local stream
|
||||
);
|
||||
|
||||
_participants.insert(0, localParticipant); // Add at the beginning
|
||||
state = state.copyWith();
|
||||
}
|
||||
|
||||
void _updateLiveParticipantsFromWebRTC() {
|
||||
if (_webrtcManager == null) return;
|
||||
|
||||
final webrtcParticipants = _webrtcManager!.participants;
|
||||
|
||||
// Always ensure local participant exists
|
||||
final existingLocalParticipant =
|
||||
_participants.isNotEmpty &&
|
||||
_participants[0].remoteParticipant.id == _webrtcManager!.roomId
|
||||
? _participants[0]
|
||||
: null;
|
||||
|
||||
final localParticipant =
|
||||
existingLocalParticipant ?? _createLocalParticipant();
|
||||
|
||||
// Add remote participants
|
||||
_participants.addAll(
|
||||
remoteParticipants.values.map((remote) {
|
||||
final match =
|
||||
_participantInfoByIdentity[remote.identity] ??
|
||||
final remoteParticipants =
|
||||
webrtcParticipants.map((p) {
|
||||
final participantInfo =
|
||||
_participantInfoByIdentity[p.id] ??
|
||||
CallParticipant(
|
||||
identity: remote.identity,
|
||||
name: remote.identity,
|
||||
identity: p.id,
|
||||
name: p.name,
|
||||
accountId: p.userinfo.id,
|
||||
account: p.userinfo,
|
||||
joinedAt: DateTime.now(),
|
||||
);
|
||||
return CallParticipantLive(
|
||||
participant: match,
|
||||
remoteParticipant: remote,
|
||||
);
|
||||
}),
|
||||
participant: participantInfo,
|
||||
remoteParticipant: p,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
// Combine local participant with remote participants
|
||||
_participants = [localParticipant, ...remoteParticipants];
|
||||
|
||||
state = state.copyWith();
|
||||
}
|
||||
|
||||
/// Builds the CallParticipant object for the local participant.
|
||||
/// Optionally, pass [participants] if you want to prioritize info from the latest list.
|
||||
CallParticipant _buildParticipant({List<CallParticipant>? participants}) {
|
||||
if (_localParticipant == null) {
|
||||
throw StateError('No local participant available');
|
||||
}
|
||||
// Prefer info from the latest participants list if available
|
||||
if (participants != null) {
|
||||
final idx = participants.indexWhere(
|
||||
(p) => p.identity == _localParticipant!.identity,
|
||||
);
|
||||
if (idx != -1) return participants[idx];
|
||||
}
|
||||
|
||||
// Otherwise, use info from the identity map or fallback to minimal
|
||||
return _participantInfoByIdentity[_localParticipant!.identity] ??
|
||||
CallParticipant(
|
||||
identity: _localParticipant!.identity,
|
||||
name: _localParticipant!.identity,
|
||||
CallParticipantLive _createLocalParticipant() {
|
||||
return CallParticipantLive(
|
||||
participant: CallParticipant(
|
||||
identity: _webrtcManager!.roomId, // Use roomId as local identity
|
||||
name: 'You',
|
||||
accountId: '',
|
||||
account: null,
|
||||
joinedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
void _updateLiveParticipants(List<CallParticipant> participants) {
|
||||
// Update the info map for lookup
|
||||
for (final p in participants) {
|
||||
_participantInfoByIdentity[p.identity] = p;
|
||||
}
|
||||
if (_room == null) {
|
||||
// Can't build live objects, just store empty
|
||||
_participants = [];
|
||||
state = state.copyWith();
|
||||
return;
|
||||
}
|
||||
final remoteParticipants = _room!.remoteParticipants;
|
||||
final remotes = remoteParticipants.values.toList();
|
||||
_participants = [];
|
||||
// Add local participant if present in the list
|
||||
if (_localParticipant != null) {
|
||||
final localInfo = _buildParticipant(participants: participants);
|
||||
_participants.add(
|
||||
CallParticipantLive(
|
||||
participant: localInfo,
|
||||
remoteParticipant: _localParticipant!,
|
||||
),
|
||||
remoteParticipant: WebRTCParticipant(
|
||||
id: _webrtcManager!.roomId,
|
||||
name: 'You',
|
||||
userinfo: SnAccount(
|
||||
id: '',
|
||||
name: '',
|
||||
nick: '',
|
||||
language: '',
|
||||
isSuperuser: false,
|
||||
automatedId: null,
|
||||
profile: SnAccountProfile(
|
||||
id: '',
|
||||
firstName: '',
|
||||
middleName: '',
|
||||
lastName: '',
|
||||
bio: '',
|
||||
gender: '',
|
||||
pronouns: '',
|
||||
location: '',
|
||||
timeZone: '',
|
||||
links: [],
|
||||
experience: 0,
|
||||
level: 0,
|
||||
socialCredits: 0,
|
||||
socialCreditsLevel: 0,
|
||||
levelingProgress: 0,
|
||||
picture: null,
|
||||
background: null,
|
||||
verification: null,
|
||||
usernameColor: null,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
deletedAt: null,
|
||||
),
|
||||
perkSubscription: null,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
deletedAt: null,
|
||||
),
|
||||
)..remoteStream = _webrtcManager!.localStream, // Access local stream
|
||||
);
|
||||
state = state.copyWith();
|
||||
}
|
||||
// Add remote participants
|
||||
_participants.addAll(
|
||||
participants.map((p) {
|
||||
lk.RemoteParticipant? remote;
|
||||
for (final r in remotes) {
|
||||
if (r.identity == p.identity) {
|
||||
remote = r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (_localParticipant != null &&
|
||||
p.identity == _localParticipant!.identity) {
|
||||
return null; // Already added local
|
||||
}
|
||||
return remote != null
|
||||
? CallParticipantLive(participant: p, remoteParticipant: remote)
|
||||
: null;
|
||||
}).whereType<CallParticipantLive>(),
|
||||
);
|
||||
state = state.copyWith();
|
||||
}
|
||||
|
||||
String? _roomId;
|
||||
String? get roomId => _roomId;
|
||||
|
||||
Future<void> joinRoom(String roomId) async {
|
||||
if (_roomId == roomId && _room != null) {
|
||||
talker.info('[Call] Call skipped. Already has data');
|
||||
return;
|
||||
} else if (_room != null) {
|
||||
if (!_room!.isDisposed &&
|
||||
_room!.connectionState != lk.ConnectionState.disconnected) {
|
||||
throw Exception('Call already connected');
|
||||
if (_roomId == roomId && _webrtcManager != null) {
|
||||
talker.info('[Call] Call skipped. Already connected to this room');
|
||||
// Ensure state is connected even if we skip the join process
|
||||
if (!state.isConnected) {
|
||||
state = state.copyWith(isConnected: true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
_roomId = roomId;
|
||||
if (_room != null) {
|
||||
await _room!.disconnect();
|
||||
await _room!.dispose();
|
||||
_room = null;
|
||||
_localParticipant = null;
|
||||
_participants = [];
|
||||
}
|
||||
|
||||
// Clean up existing connection
|
||||
await disconnect();
|
||||
|
||||
try {
|
||||
final apiClient = ref.read(apiClientProvider);
|
||||
final ongoingCall = await ref.read(ongoingCallProvider(roomId).future);
|
||||
@@ -241,8 +262,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 +281,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,75 +305,91 @@ class CallNotifier extends _$CallNotifier {
|
||||
}
|
||||
|
||||
Future<void> toggleMicrophone() async {
|
||||
if (_localParticipant != null) {
|
||||
const autostop = true;
|
||||
final target = !_localParticipant!.isMicrophoneEnabled();
|
||||
final target = !state.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();
|
||||
await _webrtcManager?.toggleMicrophone(target);
|
||||
|
||||
// Update local participant's audio state
|
||||
if (_participants.isNotEmpty) {
|
||||
_participants[0].remoteParticipant.isAudioEnabled = target;
|
||||
state = state.copyWith(); // Trigger UI update
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> toggleCamera() async {
|
||||
if (_localParticipant != null) {
|
||||
final target = !_localParticipant!.isCameraEnabled();
|
||||
final target = !state.isCameraEnabled;
|
||||
state = state.copyWith(isCameraEnabled: target);
|
||||
await _localParticipant!.setCameraEnabled(target);
|
||||
state = state.copyWith();
|
||||
await _webrtcManager?.toggleCamera(target);
|
||||
|
||||
// Update local participant's video state
|
||||
if (_participants.isNotEmpty) {
|
||||
_participants[0].remoteParticipant.isVideoEnabled = target;
|
||||
state = state.copyWith(); // Trigger UI update
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> toggleScreenShare(BuildContext context) async {
|
||||
if (_localParticipant != null) {
|
||||
final target = !_localParticipant!.isScreenShareEnabled();
|
||||
state = state.copyWith(isScreenSharing: target);
|
||||
if (_webrtcManager == null) return;
|
||||
|
||||
if (target && lk.lkPlatformIsDesktop()) {
|
||||
try {
|
||||
final source = await showDialog<DesktopCapturerSource>(
|
||||
context: context,
|
||||
builder: (context) => lk.ScreenSelectDialog(),
|
||||
);
|
||||
if (source == null) {
|
||||
return;
|
||||
}
|
||||
var track = await lk.LocalVideoTrack.createScreenShareTrack(
|
||||
lk.ScreenShareCaptureOptions(
|
||||
sourceId: source.id,
|
||||
maxFrameRate: 30.0,
|
||||
captureScreenAudio: true,
|
||||
),
|
||||
);
|
||||
await _localParticipant!.publishVideoTrack(track);
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
}
|
||||
return;
|
||||
if (state.isScreenSharing) {
|
||||
// Stop screen sharing - switch back to camera
|
||||
await _webrtcManager!.toggleCamera(state.isCameraEnabled);
|
||||
state = state.copyWith(isScreenSharing: false);
|
||||
} else {
|
||||
await _localParticipant!.setScreenShareEnabled(target);
|
||||
}
|
||||
// Start screen sharing
|
||||
if (WebRTC.platformIsDesktop) {
|
||||
// For desktop, we need to get screen capture source
|
||||
// This would require implementing a screen selection dialog
|
||||
// For now, just toggle the state
|
||||
state = state.copyWith(isScreenSharing: true);
|
||||
} else if (WebRTC.platformIsWeb) {
|
||||
// For web, get display media directly
|
||||
await navigator.mediaDevices.getDisplayMedia({
|
||||
'video': true,
|
||||
'audio':
|
||||
false, // Screen sharing typically doesn't include system audio
|
||||
});
|
||||
|
||||
state = state.copyWith();
|
||||
// Replace video track with screen sharing track
|
||||
// This is a simplified implementation
|
||||
state = state.copyWith(isScreenSharing: true);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
talker.error('[Call] Screen sharing error: $e');
|
||||
state = state.copyWith(error: 'Failed to toggle screen sharing: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> toggleSpeakerphone() async {
|
||||
if (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) {
|
||||
try {
|
||||
// For mobile platforms, we can control audio routing
|
||||
// This is a simplified implementation
|
||||
final newSpeakerphoneState = !state.isSpeakerphone;
|
||||
state = state.copyWith(isSpeakerphone: newSpeakerphoneState);
|
||||
|
||||
// Note: Actual speakerphone control would require platform-specific code
|
||||
// For a full implementation, you'd need to use platform channels
|
||||
// to control audio routing on iOS/Android
|
||||
talker.info('[Call] Speakerphone toggled to: $newSpeakerphoneState');
|
||||
} catch (e) {
|
||||
talker.error('[Call] Speakerphone control error: $e');
|
||||
state = state.copyWith(error: 'Failed to toggle speakerphone: $e');
|
||||
}
|
||||
} else {
|
||||
// For web/desktop, speakerphone control is handled by the browser/OS
|
||||
state = state.copyWith(isSpeakerphone: !state.isSpeakerphone);
|
||||
await lk.Hardware.instance.setSpeakerphoneOn(state.isSpeakerphone);
|
||||
state = state.copyWith();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> disconnect() async {
|
||||
if (_room != null) {
|
||||
await _room!.disconnect();
|
||||
_webrtcManager?.dispose();
|
||||
_webrtcManager = null;
|
||||
_participantJoinedSubscription?.cancel();
|
||||
_participantLeftSubscription?.cancel();
|
||||
_participants.clear();
|
||||
state = state.copyWith(
|
||||
isConnected: false,
|
||||
isMicrophoneEnabled: false,
|
||||
@@ -388,26 +399,20 @@ class CallNotifier extends _$CallNotifier {
|
||||
// Disable wakelock when call disconnects
|
||||
WakelockPlus.disable();
|
||||
}
|
||||
}
|
||||
|
||||
void setParticipantVolume(CallParticipantLive live, double volume) {
|
||||
if (participantsVolumes[live.remoteParticipant.sid] == null) {
|
||||
participantsVolumes[live.remoteParticipant.sid] = 1;
|
||||
}
|
||||
Helper.setVolume(
|
||||
volume,
|
||||
live
|
||||
.remoteParticipant
|
||||
.audioTrackPublications
|
||||
.first
|
||||
.track!
|
||||
.mediaStreamTrack,
|
||||
// Store volume setting for this participant
|
||||
// Note: WebRTC doesn't have built-in per-participant volume control
|
||||
// This is just storing the preference for UI purposes
|
||||
// Actual volume control would need to be implemented at the audio rendering level
|
||||
participantsVolumes[live.remoteParticipant.id] = volume.clamp(0.0, 1.0);
|
||||
talker.info(
|
||||
'[Call] Volume set to $volume for participant ${live.remoteParticipant.id}',
|
||||
);
|
||||
participantsVolumes[live.remoteParticipant.sid] = volume;
|
||||
}
|
||||
|
||||
double getParticipantVolume(CallParticipantLive live) {
|
||||
return participantsVolumes[live.remoteParticipant.sid] ?? 1;
|
||||
return participantsVolumes[live.remoteParticipant.id] ?? 1.0;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
@@ -418,9 +423,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 = {};
|
||||
|
@@ -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 extends Object?>(TResult Function( CallParticipant participant, lk.Participant remoteParticipant)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(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 extends Object?>(TResult Function( CallParticipant participant, lk.Participant remoteParticipant) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(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 extends Object?>(TResult? Function( CallParticipant participant, lk.Participant remoteParticipant)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(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,
|
||||
));
|
||||
}
|
||||
|
||||
|
@@ -6,7 +6,7 @@ part of 'call.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$callNotifierHash() => r'a8ca3f625c0db3ad9992033ae70864ce15efc281';
|
||||
String _$callNotifierHash() => r'4015d326388553c46859fe537e84d2c9da4236c9';
|
||||
|
||||
/// See also [CallNotifier].
|
||||
@ProviderFor(CallNotifier)
|
||||
|
476
lib/pods/chat/webrtc_manager.dart
Normal file
476
lib/pods/chat/webrtc_manager.dart
Normal file
@@ -0,0 +1,476 @@
|
||||
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/pods/userinfo.dart';
|
||||
import 'package:island/talker.dart';
|
||||
|
||||
class WebRTCParticipant {
|
||||
final String id;
|
||||
final String name;
|
||||
final SnAccount userinfo;
|
||||
RTCPeerConnection? peerConnection;
|
||||
MediaStream? remoteStream;
|
||||
List<RTCIceCandidate> remoteCandidates = [];
|
||||
bool isAudioEnabled = true;
|
||||
bool isVideoEnabled = false;
|
||||
bool isConnected = false;
|
||||
bool isLocal = false;
|
||||
double audioLevel = 0.0;
|
||||
|
||||
WebRTCParticipant({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.userinfo,
|
||||
this.isAudioEnabled = true,
|
||||
this.isVideoEnabled = false,
|
||||
this.isLocal = false,
|
||||
});
|
||||
}
|
||||
|
||||
class WebRTCManager {
|
||||
final String roomId;
|
||||
final String serverUrl;
|
||||
late WebRTCSignaling _signaling;
|
||||
final Map<String, WebRTCParticipant> _participants = {};
|
||||
final Map<String, RTCPeerConnection> _peerConnections = {};
|
||||
|
||||
MediaStream? _localStream;
|
||||
Timer? _audioLevelTimer;
|
||||
|
||||
MediaStream? get localStream => _localStream;
|
||||
final StreamController<WebRTCParticipant> _participantController =
|
||||
StreamController<WebRTCParticipant>.broadcast();
|
||||
final StreamController<String> _participantLeftController =
|
||||
StreamController<String>.broadcast();
|
||||
|
||||
Stream<WebRTCParticipant> get onParticipantJoined =>
|
||||
_participantController.stream;
|
||||
Stream<String> get onParticipantLeft => _participantLeftController.stream;
|
||||
|
||||
WebRTCManager({required this.roomId, required this.serverUrl}) {
|
||||
_signaling = WebRTCSignaling(roomId: roomId);
|
||||
}
|
||||
|
||||
Future<void> initialize(Ref ref) async {
|
||||
final user = ref.watch(userInfoProvider).value!;
|
||||
_signaling.userId = user.id;
|
||||
_signaling.userName = user.name;
|
||||
_signaling.user = user;
|
||||
await _initializeLocalStream();
|
||||
_setupSignalingListeners();
|
||||
await _signaling.connect(ref);
|
||||
_startAudioLevelMonitoring();
|
||||
}
|
||||
|
||||
Future<void> _initializeLocalStream() async {
|
||||
try {
|
||||
_localStream = await navigator.mediaDevices.getUserMedia({
|
||||
'audio': true,
|
||||
'video': true,
|
||||
});
|
||||
talker.info('[WebRTC] Local stream initialized');
|
||||
} catch (e) {
|
||||
talker.error('[WebRTC] Failed to initialize local stream: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
void _setupSignalingListeners() {
|
||||
_signaling.messages.listen((message) async {
|
||||
switch (message.type) {
|
||||
case 'offer':
|
||||
await _handleOffer(message.accountId, message.account, message.data);
|
||||
break;
|
||||
case 'answer':
|
||||
await _handleAnswer(message.accountId, message.data);
|
||||
break;
|
||||
case 'ice-candidate':
|
||||
await _handleIceCandidate(message.accountId, message.data);
|
||||
break;
|
||||
// CHANGED: Listen for new users joining the room.
|
||||
case 'user-joined':
|
||||
await _handleUserJoined(message.accountId, message.account);
|
||||
break;
|
||||
default:
|
||||
talker.warning(
|
||||
'[WebRTC Manager] Receieved an unknown type singaling message: ${message.type} with ${message.data}',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// CHANGED: The welcome message now drives connection initiation.
|
||||
_signaling.welcomeMessages.listen((welcome) {
|
||||
talker.info('[WebRTC Manager] Connected to room: ${welcome.roomId}');
|
||||
final existingParticipants =
|
||||
welcome.participants; // Assuming the server sends this.
|
||||
talker.info(
|
||||
'[WebRTC Manager] Existing participants: $existingParticipants',
|
||||
);
|
||||
|
||||
// The newcomer is responsible for initiating the connection to everyone else.
|
||||
for (final participant in existingParticipants) {
|
||||
if (participant.identity != _signaling.userId) {
|
||||
if (!_participants.containsKey(participant.identity)) {
|
||||
final webrtcParticipant = WebRTCParticipant(
|
||||
id: participant.identity,
|
||||
name: participant.name,
|
||||
userinfo: participant.account!,
|
||||
);
|
||||
_participants[participant.identity] = webrtcParticipant;
|
||||
_participantController.add(webrtcParticipant);
|
||||
}
|
||||
_createPeerConnection(participant.identity, isInitiator: true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// CHANGED: New handler for when an existing user is notified of a new peer.
|
||||
Future<void> _handleUserJoined(
|
||||
String participantId,
|
||||
SnAccount account,
|
||||
) async {
|
||||
talker.info(
|
||||
'[WebRTC Manager] User joined: $participantId. Waiting for their offer.',
|
||||
);
|
||||
// We don't need to be the initiator here. The newcomer will send us an offer.
|
||||
// We just create the peer connection to be ready for it.
|
||||
if (!_peerConnections.containsKey(participantId)) {
|
||||
// Create a participant object to represent the new user
|
||||
if (!_participants.containsKey(participantId)) {
|
||||
final participant = WebRTCParticipant(
|
||||
id: participantId,
|
||||
name: participantId,
|
||||
userinfo: account,
|
||||
); // Placeholder name
|
||||
_participants[participantId] = participant;
|
||||
_participantController.add(participant);
|
||||
}
|
||||
await _createPeerConnection(participantId, isInitiator: false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _createPeerConnection(
|
||||
String participantId, {
|
||||
bool isInitiator = false,
|
||||
}) async {
|
||||
talker.info(
|
||||
'[WebRTC] Creating peer connection to $participantId (initiator: $isInitiator)',
|
||||
);
|
||||
final configuration = {
|
||||
'iceServers': [
|
||||
{'urls': 'stun:stun.l.google.com:19302'},
|
||||
],
|
||||
};
|
||||
|
||||
final peerConnection = await createPeerConnection(configuration);
|
||||
_peerConnections[participantId] = peerConnection;
|
||||
_participants[participantId]!.peerConnection = 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;
|
||||
|
||||
// Detect video tracks and update video enabled state
|
||||
final videoTracks = event.streams[0].getVideoTracks();
|
||||
if (videoTracks.isNotEmpty) {
|
||||
participant.isVideoEnabled = true;
|
||||
}
|
||||
|
||||
_participantController.add(participant);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
peerConnection.onIceCandidate = (candidate) {
|
||||
// CHANGED: Send candidate to the specific participant
|
||||
_signaling.sendIceCandidate(participantId, candidate);
|
||||
};
|
||||
|
||||
peerConnection.onConnectionState = (state) {
|
||||
talker.info('[WebRTC] Connection state for $participantId: $state');
|
||||
final participant = _participants[participantId];
|
||||
if (participant != null) {
|
||||
participant.isConnected =
|
||||
state == RTCPeerConnectionState.RTCPeerConnectionStateConnected;
|
||||
_participantController.add(participant);
|
||||
}
|
||||
};
|
||||
|
||||
if (isInitiator) {
|
||||
final offer = await peerConnection.createOffer();
|
||||
await peerConnection.setLocalDescription(offer);
|
||||
// CHANGED: Send offer to the specific participant
|
||||
_signaling.sendOffer(participantId, offer);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleOffer(
|
||||
String from,
|
||||
SnAccount account,
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
final participantId = from;
|
||||
talker.info('[WebRTC Manager] Handling offer from $participantId');
|
||||
final offer = RTCSessionDescription(data['sdp'], data['type']);
|
||||
|
||||
if (!_peerConnections.containsKey(participantId)) {
|
||||
if (!_participants.containsKey(participantId)) {
|
||||
final participant = WebRTCParticipant(
|
||||
id: participantId,
|
||||
name: participantId,
|
||||
userinfo: account,
|
||||
);
|
||||
_participants[participantId] = participant;
|
||||
_participantController.add(participant);
|
||||
}
|
||||
await _createPeerConnection(participantId, isInitiator: false);
|
||||
}
|
||||
|
||||
final peerConnection = _peerConnections[participantId]!;
|
||||
await peerConnection.setRemoteDescription(offer);
|
||||
|
||||
final answer = await peerConnection.createAnswer();
|
||||
await peerConnection.setLocalDescription(answer);
|
||||
// CHANGED: Send answer to the specific participant
|
||||
_signaling.sendAnswer(participantId, answer);
|
||||
|
||||
// Process any queued ICE candidates
|
||||
final participant = _participants[participantId];
|
||||
if (participant != null) {
|
||||
for (final candidate in participant.remoteCandidates) {
|
||||
await peerConnection.addCandidate(candidate);
|
||||
}
|
||||
participant.remoteCandidates.clear();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleAnswer(String from, Map<String, dynamic> data) async {
|
||||
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);
|
||||
|
||||
// Process any queued ICE candidates
|
||||
final participant = _participants[participantId];
|
||||
if (participant != null) {
|
||||
for (final candidate in participant.remoteCandidates) {
|
||||
await peerConnection.addCandidate(candidate);
|
||||
}
|
||||
participant.remoteCandidates.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleIceCandidate(
|
||||
String from,
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
final participantId = from;
|
||||
final candidate = RTCIceCandidate(
|
||||
data['candidate'],
|
||||
data['sdpMid'],
|
||||
data['sdpMLineIndex'],
|
||||
);
|
||||
|
||||
final participant = _participants[participantId];
|
||||
if (participant != null) {
|
||||
final pc = participant.peerConnection;
|
||||
if (pc != null) {
|
||||
await pc.addCandidate(candidate);
|
||||
} else {
|
||||
participant.remoteCandidates.add(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> replaceMediaStream(Map<String, dynamic> constraints) async {
|
||||
try {
|
||||
final newStream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
final newVideoTrack = newStream.getVideoTracks().firstOrNull;
|
||||
final newAudioTrack = newStream.getAudioTracks().firstOrNull;
|
||||
|
||||
if (_localStream != null) {
|
||||
final oldVideoTrack = _localStream!.getVideoTracks().firstOrNull;
|
||||
final oldAudioTrack = _localStream!.getAudioTracks().firstOrNull;
|
||||
|
||||
// Replace tracks in all existing peer connections
|
||||
for (final pc in _peerConnections.values) {
|
||||
final senders = await pc.getSenders();
|
||||
for (final sender in senders) {
|
||||
if (newVideoTrack != null && sender.track == oldVideoTrack) {
|
||||
await sender.replaceTrack(newVideoTrack);
|
||||
} else if (newAudioTrack != null && sender.track == oldAudioTrack) {
|
||||
await sender.replaceTrack(newAudioTrack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop old tracks and update local stream
|
||||
for (final track in _localStream!.getTracks()) {
|
||||
track.stop();
|
||||
}
|
||||
}
|
||||
|
||||
_localStream = newStream;
|
||||
talker.info('[WebRTC] Media stream replaced with new constraints');
|
||||
} catch (e) {
|
||||
talker.error('[WebRTC] Failed to replace media stream: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> toggleMicrophone(bool enabled) async {
|
||||
if (_localStream != null) {
|
||||
final audioTracks = _localStream!.getAudioTracks();
|
||||
for (final track in audioTracks) {
|
||||
track.enabled = enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> toggleCamera(bool enabled) async {
|
||||
if (_localStream != null) {
|
||||
_localStream!.getVideoTracks().forEach((track) {
|
||||
track.enabled = enabled;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> switchCamera(String deviceId) async {
|
||||
await replaceMediaStream({
|
||||
'audio': _localStream?.getAudioTracks().isNotEmpty ?? true,
|
||||
'video': {'deviceId': deviceId},
|
||||
});
|
||||
talker.info('[WebRTC] Switched to camera device: $deviceId');
|
||||
}
|
||||
|
||||
Future<void> switchMicrophone(String deviceId) async {
|
||||
await replaceMediaStream({
|
||||
'audio': {'deviceId': deviceId},
|
||||
'video': _localStream?.getVideoTracks().isNotEmpty ?? true,
|
||||
});
|
||||
talker.info('[WebRTC] Switched to microphone device: $deviceId');
|
||||
}
|
||||
|
||||
Future<List<MediaDeviceInfo>> getVideoDevices() async {
|
||||
try {
|
||||
final devices = await navigator.mediaDevices.enumerateDevices();
|
||||
return devices.where((device) => device.kind == 'videoinput').toList();
|
||||
} catch (e) {
|
||||
talker.error('[WebRTC] Failed to enumerate video devices: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<MediaDeviceInfo>> getAudioDevices() async {
|
||||
try {
|
||||
final devices = await navigator.mediaDevices.enumerateDevices();
|
||||
return devices.where((device) => device.kind == 'audioinput').toList();
|
||||
} catch (e) {
|
||||
talker.error('[WebRTC] Failed to enumerate audio devices: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
void _startAudioLevelMonitoring() {
|
||||
_audioLevelTimer?.cancel();
|
||||
_audioLevelTimer = Timer.periodic(const Duration(milliseconds: 100), (_) {
|
||||
_updateAudioLevels();
|
||||
});
|
||||
}
|
||||
|
||||
void _stopAudioLevelMonitoring() {
|
||||
_audioLevelTimer?.cancel();
|
||||
_audioLevelTimer = null;
|
||||
}
|
||||
|
||||
Future<void> _updateAudioLevels() async {
|
||||
bool hasUpdates = false;
|
||||
|
||||
for (final participant in _participants.values) {
|
||||
if (participant.remoteStream != null && participant.isAudioEnabled) {
|
||||
final audioTracks = participant.remoteStream!.getAudioTracks();
|
||||
if (audioTracks.isNotEmpty) {
|
||||
try {
|
||||
// Try to get stats for more accurate audio level detection
|
||||
final pc = participant.peerConnection;
|
||||
if (pc != null) {
|
||||
final stats = await pc.getStats();
|
||||
double maxAudioLevel = 0.0;
|
||||
|
||||
// Look for audio receiver stats
|
||||
for (var report in stats) {
|
||||
if (report.type == 'inbound-rtp' &&
|
||||
report.values['mediaType'] == 'audio') {
|
||||
final audioLevel = report.values['audioLevel'] as double?;
|
||||
if (audioLevel != null && audioLevel > maxAudioLevel) {
|
||||
maxAudioLevel = audioLevel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we got stats, use them; otherwise use a simple heuristic
|
||||
if (maxAudioLevel > 0) {
|
||||
participant.audioLevel = maxAudioLevel.clamp(0.0, 1.0);
|
||||
} else {
|
||||
// Simple heuristic: if audio track is enabled, assume some level
|
||||
// In a real app, you'd analyze the actual audio data
|
||||
participant.audioLevel = audioTracks[0].enabled ? 0.5 : 0.0;
|
||||
}
|
||||
} else {
|
||||
// Fallback for local participant or when no PC available
|
||||
participant.audioLevel = participant.isLocal ? 0.0 : 0.3;
|
||||
}
|
||||
|
||||
hasUpdates = true;
|
||||
} catch (e) {
|
||||
talker.warning('[WebRTC] Failed to update audio level for ${participant.id}: $e');
|
||||
participant.audioLevel = 0.0;
|
||||
}
|
||||
} else {
|
||||
participant.audioLevel = 0.0;
|
||||
}
|
||||
} else {
|
||||
participant.audioLevel = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
// Notify listeners if there were updates (throttled to avoid excessive updates)
|
||||
if (hasUpdates) {
|
||||
// This will trigger UI updates for speaking indicators
|
||||
}
|
||||
}
|
||||
|
||||
List<WebRTCParticipant> get participants => _participants.values.toList();
|
||||
|
||||
void dispose() {
|
||||
_stopAudioLevelMonitoring();
|
||||
_signaling.disconnect();
|
||||
for (final pc in _peerConnections.values) {
|
||||
pc.close();
|
||||
}
|
||||
_peerConnections.clear();
|
||||
for (var p in _participants.values) {
|
||||
p.remoteCandidates.clear();
|
||||
}
|
||||
_participants.clear();
|
||||
_localStream?.dispose();
|
||||
_participantController.close();
|
||||
_participantLeftController.close();
|
||||
}
|
||||
}
|
211
lib/pods/chat/webrtc_signaling.dart
Normal file
211
lib/pods/chat/webrtc_signaling.dart
Normal file
@@ -0,0 +1,211 @@
|
||||
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/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<String, dynamic> data,
|
||||
}) = _SignalingMessage;
|
||||
|
||||
factory SignalingMessage.fromJson(Map<String, dynamic> 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<CallParticipant> participants,
|
||||
}) = _WebRTCWelcomeMessage;
|
||||
|
||||
factory WebRTCWelcomeMessage.fromJson(Map<String, dynamic> json) =>
|
||||
_$WebRTCWelcomeMessageFromJson(json);
|
||||
}
|
||||
|
||||
class WebRTCSignaling {
|
||||
final String roomId;
|
||||
late final String userId;
|
||||
late final String userName;
|
||||
late SnAccount user;
|
||||
final StreamController<SignalingMessage> _messageController =
|
||||
StreamController<SignalingMessage>.broadcast();
|
||||
final StreamController<WebRTCWelcomeMessage> _welcomeController =
|
||||
StreamController<WebRTCWelcomeMessage>.broadcast();
|
||||
WebSocketChannel? _channel;
|
||||
Timer? _heartbeatTimer;
|
||||
|
||||
Stream<SignalingMessage> get messages => _messageController.stream;
|
||||
Stream<WebRTCWelcomeMessage> get welcomeMessages => _welcomeController.stream;
|
||||
|
||||
WebRTCSignaling({required this.roomId});
|
||||
|
||||
Future<void> connect(Ref ref) async {
|
||||
final baseUrl = ref.watch(serverUrlProvider);
|
||||
final token = await getToken(ref.watch(tokenProvider));
|
||||
|
||||
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;
|
||||
|
||||
// Start heartbeat timer
|
||||
_heartbeatTimer = Timer.periodic(const Duration(seconds: 30), (timer) => _sendHeartbeat());
|
||||
|
||||
_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 _sendHeartbeat() {
|
||||
if (_channel == null) return;
|
||||
talker.info('[WebRTC Signaling] Sending heartbeat');
|
||||
final packet = WebSocketPacket(type: 'heartbeat', data: null);
|
||||
_channel!.sink.add(jsonEncode(packet.toJson()));
|
||||
}
|
||||
|
||||
void disconnect() {
|
||||
_heartbeatTimer?.cancel();
|
||||
_channel?.sink.close();
|
||||
_messageController.close();
|
||||
_welcomeController.close();
|
||||
}
|
||||
}
|
611
lib/pods/chat/webrtc_signaling.freezed.dart
Normal file
611
lib/pods/chat/webrtc_signaling.freezed.dart
Normal file
@@ -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>(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<String, dynamic> 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<SignalingMessage> get copyWith => _$SignalingMessageCopyWithImpl<SignalingMessage>(this as SignalingMessage, _$identity);
|
||||
|
||||
/// Serializes this SignalingMessage to a JSON map.
|
||||
Map<String, dynamic> 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<String, dynamic> 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<String, dynamic>,
|
||||
));
|
||||
}
|
||||
/// 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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(TResult Function( String type, String? to, String accountId, SnAccount account, Map<String, dynamic> 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 extends Object?>(TResult Function( String type, String? to, String accountId, SnAccount account, Map<String, dynamic> 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 extends Object?>(TResult? Function( String type, String? to, String accountId, SnAccount account, Map<String, dynamic> 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<String, dynamic> data}): _data = data;
|
||||
factory _SignalingMessage.fromJson(Map<String, dynamic> 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<String, dynamic> _data;
|
||||
@override Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic>,
|
||||
));
|
||||
}
|
||||
|
||||
/// 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<CallParticipant> 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<WebRTCWelcomeMessage> get copyWith => _$WebRTCWelcomeMessageCopyWithImpl<WebRTCWelcomeMessage>(this as WebRTCWelcomeMessage, _$identity);
|
||||
|
||||
/// Serializes this WebRTCWelcomeMessage to a JSON map.
|
||||
Map<String, dynamic> 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<CallParticipant> 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<CallParticipant>,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// 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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(TResult Function( String userId, String roomId, String message, String timestamp, List<CallParticipant> 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 extends Object?>(TResult Function( String userId, String roomId, String message, String timestamp, List<CallParticipant> 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 extends Object?>(TResult? Function( String userId, String roomId, String message, String timestamp, List<CallParticipant> 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<CallParticipant> participants = const []}): _participants = participants;
|
||||
factory _WebRTCWelcomeMessage.fromJson(Map<String, dynamic> 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<CallParticipant> _participants;
|
||||
// CHANGED: Added participants list
|
||||
@override@JsonKey() List<CallParticipant> 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<String, dynamic> 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<CallParticipant> 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<CallParticipant>,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
49
lib/pods/chat/webrtc_signaling.g.dart
Normal file
49
lib/pods/chat/webrtc_signaling.g.dart
Normal file
@@ -0,0 +1,49 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'webrtc_signaling.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_SignalingMessage _$SignalingMessageFromJson(Map<String, dynamic> 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<String, dynamic>),
|
||||
data: json['data'] as Map<String, dynamic>,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SignalingMessageToJson(_SignalingMessage instance) =>
|
||||
<String, dynamic>{
|
||||
'type': instance.type,
|
||||
'to': instance.to,
|
||||
'account_id': instance.accountId,
|
||||
'account': instance.account.toJson(),
|
||||
'data': instance.data,
|
||||
};
|
||||
|
||||
_WebRTCWelcomeMessage _$WebRTCWelcomeMessageFromJson(
|
||||
Map<String, dynamic> 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<dynamic>?)
|
||||
?.map((e) => CallParticipant.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$WebRTCWelcomeMessageToJson(
|
||||
_WebRTCWelcomeMessage instance,
|
||||
) => <String, dynamic>{
|
||||
'user_id': instance.userId,
|
||||
'room_id': instance.roomId,
|
||||
'message': instance.message,
|
||||
'timestamp': instance.timestamp,
|
||||
'participants': instance.participants.map((e) => e.toJson()).toList(),
|
||||
};
|
@@ -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()),
|
||||
|
@@ -25,7 +25,6 @@ import 'package:island/screens/tabs.dart';
|
||||
import 'package:island/screens/explore.dart';
|
||||
import 'package:island/screens/discovery/article_detail.dart';
|
||||
import 'package:island/screens/account.dart';
|
||||
import 'package:island/screens/notification.dart';
|
||||
import 'package:island/screens/wallet.dart';
|
||||
import 'package:island/screens/account/relationship.dart';
|
||||
import 'package:island/screens/account/profile.dart';
|
||||
@@ -392,11 +391,6 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
name: 'notifications',
|
||||
path: '/account/notifications',
|
||||
builder: (context, state) => const NotificationScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'wallet',
|
||||
path: '/account/wallet',
|
||||
|
@@ -285,7 +285,8 @@ class AccountScreen extends HookConsumerWidget {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => const NotificationScreen(),
|
||||
useRootNavigator: true,
|
||||
builder: (context) => const NotificationSheet(),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@@ -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();
|
||||
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);
|
||||
}
|
||||
|
@@ -57,7 +57,8 @@ Widget notificationIndicatorWidget(
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => const NotificationScreen(),
|
||||
useRootNavigator: true,
|
||||
builder: (context) => const NotificationSheet(),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@@ -112,8 +112,8 @@ class NotificationListNotifier extends _$NotificationListNotifier
|
||||
}
|
||||
}
|
||||
|
||||
class NotificationScreen extends HookConsumerWidget {
|
||||
const NotificationScreen({super.key});
|
||||
class NotificationSheet extends HookConsumerWidget {
|
||||
const NotificationSheet({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
|
@@ -374,10 +374,10 @@ class PageBackButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDesktop =
|
||||
!kIsWeb && (Platform.isMacOS || Platform.isLinux || Platform.isWindows);
|
||||
final hasPageAction =
|
||||
!kIsWeb && Platform.isMacOS;
|
||||
|
||||
if (isDesktop && isWideScreen(context)) return const SizedBox.shrink();
|
||||
if (hasPageAction && isWideScreen(context)) return const SizedBox.shrink();
|
||||
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
|
@@ -16,7 +16,7 @@ Future<SnRealtimeCall?> 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) {
|
||||
|
@@ -6,7 +6,7 @@ part of 'call_button.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$ongoingCallHash() => r'48031badb79efa07aefb3a4fc51635be457bd3f9';
|
||||
String _$ongoingCallHash() => r'0f14b36393276720a06190cab3dc8d5e4c88cd57';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
@@ -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,33 +243,17 @@ class CallControlsBar extends HookConsumerWidget {
|
||||
Future<void> _switchDevice(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
MediaDevice device,
|
||||
MediaDeviceInfo device,
|
||||
String deviceType,
|
||||
) async {
|
||||
try {
|
||||
final callNotifier = ref.read(callNotifierProvider.notifier);
|
||||
if (callNotifier.webrtcManager == null) return;
|
||||
|
||||
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);
|
||||
}
|
||||
await callNotifier.webrtcManager!.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),
|
||||
);
|
||||
}
|
||||
await callNotifier.webrtcManager!.switchMicrophone(device.deviceId);
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
@@ -289,31 +280,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 +299,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(
|
||||
child: SpeakingRippleAvatar(
|
||||
live: lastSpeaker,
|
||||
size: 36,
|
||||
).center(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -348,7 +316,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,
|
||||
|
@@ -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,
|
||||
),
|
||||
],
|
||||
|
@@ -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,8 @@ 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);
|
||||
final clampedLevel = live.audioLevel.clamp(0.0, 1.0);
|
||||
final rippleRadius = avatarRadius + clampedLevel * (size * 0.333);
|
||||
return SizedBox(
|
||||
width: size + 8,
|
||||
@@ -27,7 +24,7 @@ class SpeakingRippleAvatar extends HookConsumerWidget {
|
||||
child: TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(
|
||||
begin: avatarRadius,
|
||||
end: live.remoteParticipant.isSpeaking ? rippleRadius : avatarRadius,
|
||||
end: live.isSpeaking ? rippleRadius : avatarRadius,
|
||||
),
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeOut,
|
||||
@@ -35,7 +32,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 +46,15 @@ class SpeakingRippleAvatar extends HookConsumerWidget {
|
||||
height: size,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle),
|
||||
child: account.when(
|
||||
data:
|
||||
(value) => CallParticipantGestureDetector(
|
||||
child: CallParticipantGestureDetector(
|
||||
participant: live,
|
||||
child: ProfilePictureWidget(
|
||||
file: value.profile.picture,
|
||||
file: live.remoteParticipant.userinfo.profile.picture,
|
||||
radius: size / 2,
|
||||
),
|
||||
),
|
||||
error:
|
||||
(_, _) => CircleAvatar(
|
||||
radius: size / 2,
|
||||
child: const Icon(Symbols.person_remove),
|
||||
),
|
||||
loading:
|
||||
() => CircleAvatar(
|
||||
radius: size / 2,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (live.remoteParticipant.isMuted)
|
||||
if (live.isMuted)
|
||||
Positioned(
|
||||
bottom: 4,
|
||||
right: 4,
|
||||
@@ -96,40 +80,65 @@ class SpeakingRippleAvatar extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class CallParticipantTile extends HookConsumerWidget {
|
||||
class CallParticipantTile extends StatefulWidget {
|
||||
final CallParticipantLive live;
|
||||
|
||||
const CallParticipantTile({super.key, required this.live});
|
||||
|
||||
@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;
|
||||
State<CallParticipantTile> createState() => _CallParticipantTileState();
|
||||
}
|
||||
|
||||
if (hasVideo) {
|
||||
class _CallParticipantTileState extends State<CallParticipantTile> {
|
||||
RTCVideoRenderer? _renderer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initRenderer();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CallParticipantTile oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
// Update renderer source when the stream changes
|
||||
if (_renderer != null &&
|
||||
widget.live.remoteParticipant.remoteStream !=
|
||||
oldWidget.live.remoteParticipant.remoteStream) {
|
||||
_renderer!.srcObject = widget.live.remoteParticipant.remoteStream;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initRenderer() async {
|
||||
_renderer = RTCVideoRenderer();
|
||||
await _renderer!.initialize();
|
||||
_renderer!.srcObject = widget.live.remoteParticipant.remoteStream;
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_renderer?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.live.hasVideo &&
|
||||
widget.live.remoteParticipant.remoteStream != null &&
|
||||
_renderer != 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,
|
||||
),
|
||||
),
|
||||
AspectRatio(aspectRatio: 16 / 9, child: RTCVideoView(_renderer!)),
|
||||
Positioned(
|
||||
left: 8,
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
child: Text(
|
||||
'@${live.participant.name}',
|
||||
'@${widget.live.participant.name}',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
@@ -148,7 +157,7 @@ class CallParticipantTile extends HookConsumerWidget {
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return SpeakingRippleAvatar(size: 84, live: live);
|
||||
return SpeakingRippleAvatar(size: 84, live: widget.live);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -14,7 +14,6 @@ import 'package:island/pods/chat/messages_notifier.dart';
|
||||
import 'package:island/pods/translate.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/widgets/account/account_pfc.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/chat/message_content.dart';
|
||||
import 'package:island/widgets/chat/message_indicators.dart';
|
||||
import 'package:island/widgets/chat/message_sender_info.dart';
|
||||
@@ -666,17 +665,11 @@ class MessageItemDisplayBubble extends HookConsumerWidget {
|
||||
? Theme.of(context).colorScheme.primaryContainer.withOpacity(0.5)
|
||||
: Theme.of(context).colorScheme.surfaceContainer;
|
||||
|
||||
final hasBackground =
|
||||
ref.watch(backgroundImageFileProvider).valueOrNull != null;
|
||||
|
||||
final remoteMessage = message.toRemoteMessage();
|
||||
final sender = remoteMessage.sender;
|
||||
|
||||
return Material(
|
||||
color:
|
||||
hasBackground
|
||||
? Colors.transparent
|
||||
: Theme.of(context).colorScheme.surface,
|
||||
color: Colors.transparent,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
child: Column(
|
||||
|
@@ -44,10 +44,12 @@ void showInfoAlert(String message, String title) async {
|
||||
|
||||
Future<bool> 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;
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -15,7 +15,6 @@
|
||||
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
||||
#include <gtk/gtk_plugin.h>
|
||||
#include <irondash_engine_context/irondash_engine_context_plugin.h>
|
||||
#include <livekit_client/live_kit_plugin.h>
|
||||
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
||||
#include <media_kit_video/media_kit_video_plugin.h>
|
||||
#include <pasteboard/pasteboard_plugin.h>
|
||||
@@ -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);
|
||||
|
@@ -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
|
||||
|
@@ -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"))
|
||||
|
88
pubspec.lock
88
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:
|
||||
|
12
pubspec.yaml
12
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
|
||||
|
@@ -1,6 +1,6 @@
|
||||
; ==================================================
|
||||
#define AppVersion "3.2.0"
|
||||
#define BuildNumber "134"
|
||||
#define AppVersion "3.3.0"
|
||||
#define BuildNumber "136"
|
||||
; ==================================================
|
||||
|
||||
#define FullVersion AppVersion + "." + BuildNumber
|
||||
|
@@ -7,7 +7,6 @@
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <app_links/app_links_plugin_c_api.h>
|
||||
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
||||
#include <dart_ipc/dart_ipc_plugin_c_api.h>
|
||||
#include <file_saver/file_saver_plugin.h>
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
@@ -20,7 +19,6 @@
|
||||
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
||||
#include <gal/gal_plugin_c_api.h>
|
||||
#include <irondash_engine_context/irondash_engine_context_plugin_c_api.h>
|
||||
#include <livekit_client/live_kit_plugin.h>
|
||||
#include <local_auth_windows/local_auth_plugin.h>
|
||||
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
|
||||
#include <media_kit_video/media_kit_video_plugin_c_api.h>
|
||||
@@ -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(
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user