♻️ Replace livekit with jitsi in calling
This commit is contained in:
parent
48f40099f4
commit
5c9569ef36
@ -15,6 +15,7 @@
|
|||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
tools:replace="android:label"
|
||||||
android:label="Solian"
|
android:label="Solian"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Uncomment this line to define a global platform for your project
|
# Uncomment this line to define a global platform for your project
|
||||||
platform :ios, '13.0'
|
platform :ios, '15.1'
|
||||||
|
|
||||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
@ -122,12 +122,11 @@ PODS:
|
|||||||
- flutter_udid (0.0.1):
|
- flutter_udid (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- SAMKeychain
|
- SAMKeychain
|
||||||
- flutter_webrtc (0.12.6):
|
|
||||||
- Flutter
|
|
||||||
- WebRTC-SDK (= 125.6422.06)
|
|
||||||
- gal (1.0.0):
|
- gal (1.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- Giphy (2.2.12):
|
||||||
|
- libwebp
|
||||||
- GoogleAppMeasurement (11.10.0):
|
- GoogleAppMeasurement (11.10.0):
|
||||||
- GoogleAppMeasurement/AdIdSupport (= 11.10.0)
|
- GoogleAppMeasurement/AdIdSupport (= 11.10.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
@ -184,16 +183,26 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- in_app_review (2.0.0):
|
- in_app_review (2.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- jitsi_meet_flutter_sdk (11.1.1):
|
||||||
|
- Flutter
|
||||||
|
- JitsiMeetSDK (= 11.1.1)
|
||||||
|
- JitsiMeetSDK (11.1.1):
|
||||||
|
- Giphy (= 2.2.12)
|
||||||
|
- JitsiWebRTC (~> 124.0)
|
||||||
|
- JitsiWebRTC (124.0.2)
|
||||||
- Kingfisher (8.3.1)
|
- Kingfisher (8.3.1)
|
||||||
- livekit_client (2.4.2):
|
- libwebp (1.5.0):
|
||||||
- Flutter
|
- libwebp/demux (= 1.5.0)
|
||||||
- flutter_webrtc
|
- libwebp/mux (= 1.5.0)
|
||||||
- WebRTC-SDK (= 125.6422.06)
|
- libwebp/sharpyuv (= 1.5.0)
|
||||||
- livekit_noise_filter (0.0.1):
|
- libwebp/webp (= 1.5.0)
|
||||||
- Flutter
|
- libwebp/demux (1.5.0):
|
||||||
- flutter_webrtc
|
- libwebp/webp
|
||||||
- LiveKitKrispNoiseFilter (= 0.0.7)
|
- libwebp/mux (1.5.0):
|
||||||
- LiveKitKrispNoiseFilter (0.0.7)
|
- libwebp/demux
|
||||||
|
- libwebp/sharpyuv (1.5.0)
|
||||||
|
- libwebp/webp (1.5.0):
|
||||||
|
- libwebp/sharpyuv
|
||||||
- media_kit_libs_ios_video (1.0.4):
|
- media_kit_libs_ios_video (1.0.4):
|
||||||
- Flutter
|
- Flutter
|
||||||
- media_kit_video (0.0.1):
|
- media_kit_video (0.0.1):
|
||||||
@ -259,7 +268,6 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- wakelock_plus (0.0.1):
|
- wakelock_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- WebRTC-SDK (125.6422.06)
|
|
||||||
- workmanager (0.0.1):
|
- workmanager (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
|
||||||
@ -281,14 +289,12 @@ DEPENDENCIES:
|
|||||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||||
- flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`)
|
- flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`)
|
||||||
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
||||||
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
|
|
||||||
- gal (from `.symlinks/plugins/gal/darwin`)
|
- gal (from `.symlinks/plugins/gal/darwin`)
|
||||||
- home_widget (from `.symlinks/plugins/home_widget/ios`)
|
- home_widget (from `.symlinks/plugins/home_widget/ios`)
|
||||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||||
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
|
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
|
||||||
|
- jitsi_meet_flutter_sdk (from `.symlinks/plugins/jitsi_meet_flutter_sdk/ios`)
|
||||||
- Kingfisher (~> 8.0)
|
- Kingfisher (~> 8.0)
|
||||||
- livekit_client (from `.symlinks/plugins/livekit_client/ios`)
|
|
||||||
- livekit_noise_filter (from `.symlinks/plugins/livekit_noise_filter/ios`)
|
|
||||||
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
||||||
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
|
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
|
||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
@ -317,11 +323,14 @@ SPEC REPOS:
|
|||||||
- FirebaseCoreInternal
|
- FirebaseCoreInternal
|
||||||
- FirebaseInstallations
|
- FirebaseInstallations
|
||||||
- FirebaseMessaging
|
- FirebaseMessaging
|
||||||
|
- Giphy
|
||||||
- GoogleAppMeasurement
|
- GoogleAppMeasurement
|
||||||
- GoogleDataTransport
|
- GoogleDataTransport
|
||||||
- GoogleUtilities
|
- GoogleUtilities
|
||||||
|
- JitsiMeetSDK
|
||||||
|
- JitsiWebRTC
|
||||||
- Kingfisher
|
- Kingfisher
|
||||||
- LiveKitKrispNoiseFilter
|
- libwebp
|
||||||
- nanopb
|
- nanopb
|
||||||
- OrderedSet
|
- OrderedSet
|
||||||
- PromisesObjC
|
- PromisesObjC
|
||||||
@ -329,7 +338,6 @@ SPEC REPOS:
|
|||||||
- SDWebImage
|
- SDWebImage
|
||||||
- sqlite3
|
- sqlite3
|
||||||
- SwiftyGif
|
- SwiftyGif
|
||||||
- WebRTC-SDK
|
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
audioplayers_darwin:
|
audioplayers_darwin:
|
||||||
@ -364,8 +372,6 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/flutter_timezone/ios"
|
:path: ".symlinks/plugins/flutter_timezone/ios"
|
||||||
flutter_udid:
|
flutter_udid:
|
||||||
:path: ".symlinks/plugins/flutter_udid/ios"
|
:path: ".symlinks/plugins/flutter_udid/ios"
|
||||||
flutter_webrtc:
|
|
||||||
:path: ".symlinks/plugins/flutter_webrtc/ios"
|
|
||||||
gal:
|
gal:
|
||||||
:path: ".symlinks/plugins/gal/darwin"
|
:path: ".symlinks/plugins/gal/darwin"
|
||||||
home_widget:
|
home_widget:
|
||||||
@ -374,10 +380,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||||
in_app_review:
|
in_app_review:
|
||||||
:path: ".symlinks/plugins/in_app_review/ios"
|
:path: ".symlinks/plugins/in_app_review/ios"
|
||||||
livekit_client:
|
jitsi_meet_flutter_sdk:
|
||||||
:path: ".symlinks/plugins/livekit_client/ios"
|
:path: ".symlinks/plugins/jitsi_meet_flutter_sdk/ios"
|
||||||
livekit_noise_filter:
|
|
||||||
:path: ".symlinks/plugins/livekit_noise_filter/ios"
|
|
||||||
media_kit_libs_ios_video:
|
media_kit_libs_ios_video:
|
||||||
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
|
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
|
||||||
media_kit_video:
|
media_kit_video:
|
||||||
@ -437,18 +441,19 @@ SPEC CHECKSUMS:
|
|||||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||||
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
|
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
|
||||||
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
|
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
|
||||||
flutter_webrtc: 57f32415b8744e806f9c2a96ccdb60c6a627ba33
|
|
||||||
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
||||||
|
Giphy: 83628960ed04e1c3428ff1b4fb2b027f65e82f50
|
||||||
GoogleAppMeasurement: 36684bfb3ee034e2b42b4321eb19da3a1b81e65d
|
GoogleAppMeasurement: 36684bfb3ee034e2b42b4321eb19da3a1b81e65d
|
||||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||||
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
|
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
|
||||||
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
||||||
in_app_review: 5596fe56fab799e8edb3561c03d053363ab13457
|
in_app_review: 5596fe56fab799e8edb3561c03d053363ab13457
|
||||||
|
jitsi_meet_flutter_sdk: 0283a60730922d608fbad9872e07afdd5bb3578a
|
||||||
|
JitsiMeetSDK: 4e1c269aaaed8f2cb7b0fff2d3c00f08359b170e
|
||||||
|
JitsiWebRTC: b47805ab5668be38e7ee60e2258f49badfe8e1d0
|
||||||
Kingfisher: 3204d23de16b5ea53541c44ca5a8efb55741dec3
|
Kingfisher: 3204d23de16b5ea53541c44ca5a8efb55741dec3
|
||||||
livekit_client: 78bb2ff0d409268886804151d4fc9e006093e6ce
|
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||||
livekit_noise_filter: a26aeb1c1eae6db0a023fd2f6ea3ff108c3ecbb0
|
|
||||||
LiveKitKrispNoiseFilter: efe418ceca28163ace0ff222bd2cc02384645d84
|
|
||||||
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
|
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
|
||||||
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
||||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||||
@ -471,9 +476,8 @@ SPEC CHECKSUMS:
|
|||||||
video_compress: f2133a07762889d67f0711ac831faa26f956980e
|
video_compress: f2133a07762889d67f0711ac831faa26f956980e
|
||||||
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
|
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
|
||||||
wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49
|
wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49
|
||||||
WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db
|
|
||||||
workmanager: 01be2de7f184bd15de93a1812936a2b7f42ef07e
|
workmanager: 01be2de7f184bd15de93a1812936a2b7f42ef07e
|
||||||
|
|
||||||
PODFILE CHECKSUM: 9b244e02f87527430136c8d21cbdcf1cd586b6bc
|
PODFILE CHECKSUM: d278ce52a331dda323590121247d2046cd085ae7
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
@ -961,7 +961,7 @@
|
|||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@ -1521,7 +1521,7 @@
|
|||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@ -1549,7 +1549,7 @@
|
|||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -26,7 +26,6 @@ import 'package:styled_widget/styled_widget.dart';
|
|||||||
import 'package:surface/firebase_options.dart';
|
import 'package:surface/firebase_options.dart';
|
||||||
import 'package:surface/logger.dart';
|
import 'package:surface/logger.dart';
|
||||||
import 'package:surface/providers/channel.dart';
|
import 'package:surface/providers/channel.dart';
|
||||||
import 'package:surface/providers/chat_call.dart';
|
|
||||||
import 'package:surface/providers/config.dart';
|
import 'package:surface/providers/config.dart';
|
||||||
import 'package:surface/providers/database.dart';
|
import 'package:surface/providers/database.dart';
|
||||||
import 'package:surface/providers/keypair.dart';
|
import 'package:surface/providers/keypair.dart';
|
||||||
@ -198,7 +197,6 @@ class SolianApp extends StatelessWidget {
|
|||||||
Provider(create: (ctx) => KeyPairProvider(ctx)),
|
Provider(create: (ctx) => KeyPairProvider(ctx)),
|
||||||
ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)),
|
ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)),
|
||||||
ChangeNotifierProvider(create: (ctx) => ChatChannelProvider(ctx)),
|
ChangeNotifierProvider(create: (ctx) => ChatChannelProvider(ctx)),
|
||||||
ChangeNotifierProvider(create: (ctx) => ChatCallProvider(ctx)),
|
|
||||||
Provider(create: (ctx) => SnTranslator()),
|
Provider(create: (ctx) => SnTranslator()),
|
||||||
|
|
||||||
// Additional helper layer
|
// Additional helper layer
|
||||||
|
@ -1,474 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
|
||||||
import 'package:livekit_noise_filter/livekit_noise_filter.dart';
|
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:surface/providers/sn_network.dart';
|
|
||||||
import 'package:surface/types/chat.dart';
|
|
||||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
|
||||||
|
|
||||||
class ChatCallProvider extends ChangeNotifier {
|
|
||||||
late final SnNetworkProvider _sn;
|
|
||||||
|
|
||||||
ChatCallProvider(BuildContext context) {
|
|
||||||
_sn = context.read<SnNetworkProvider>();
|
|
||||||
}
|
|
||||||
|
|
||||||
SnChatCall? _current;
|
|
||||||
SnChannel? _channel;
|
|
||||||
|
|
||||||
bool _isReady = false;
|
|
||||||
bool _isMounted = false;
|
|
||||||
bool _isInitialized = false;
|
|
||||||
bool _isBusy = false;
|
|
||||||
|
|
||||||
String _lastDuration = '00:00:00';
|
|
||||||
Timer? _lastDurationUpdateTimer;
|
|
||||||
|
|
||||||
String? token;
|
|
||||||
String? endpoint;
|
|
||||||
|
|
||||||
StreamSubscription? hwSubscription;
|
|
||||||
List<MediaDevice> _audioInputs = [];
|
|
||||||
List<MediaDevice> _videoInputs = [];
|
|
||||||
|
|
||||||
bool _enableAudio = true;
|
|
||||||
bool _enableVideo = false;
|
|
||||||
LocalAudioTrack? _audioTrack;
|
|
||||||
LocalVideoTrack? _videoTrack;
|
|
||||||
MediaDevice? _videoDevice;
|
|
||||||
MediaDevice? _audioDevice;
|
|
||||||
|
|
||||||
late Room _room;
|
|
||||||
late EventsListener<RoomEvent> _listener;
|
|
||||||
|
|
||||||
List<ParticipantTrack> _participantTracks = [];
|
|
||||||
ParticipantTrack? _focusTrack;
|
|
||||||
|
|
||||||
// Getters for private fields
|
|
||||||
SnChatCall? get current => _current;
|
|
||||||
SnChannel? get channel => _channel;
|
|
||||||
bool get isReady => _isReady;
|
|
||||||
bool get isMounted => _isMounted;
|
|
||||||
bool get isInitialized => _isInitialized;
|
|
||||||
bool get isBusy => _isBusy;
|
|
||||||
String get lastDuration => _lastDuration;
|
|
||||||
List<MediaDevice> get audioInputs => _audioInputs;
|
|
||||||
List<MediaDevice> get videoInputs => _videoInputs;
|
|
||||||
bool get enableAudio => _enableAudio;
|
|
||||||
bool get enableVideo => _enableVideo;
|
|
||||||
LocalAudioTrack? get audioTrack => _audioTrack;
|
|
||||||
LocalVideoTrack? get videoTrack => _videoTrack;
|
|
||||||
MediaDevice? get videoDevice => _videoDevice;
|
|
||||||
MediaDevice? get audioDevice => _audioDevice;
|
|
||||||
List<ParticipantTrack> get participantTracks => _participantTracks;
|
|
||||||
ParticipantTrack? get focusTrack => _focusTrack;
|
|
||||||
Room get room => _room;
|
|
||||||
|
|
||||||
void _updateDuration() {
|
|
||||||
if (_current == null) {
|
|
||||||
_lastDuration = '00:00:00';
|
|
||||||
} else {
|
|
||||||
Duration duration = DateTime.now().difference(_current!.createdAt);
|
|
||||||
String twoDigits(int n) => n.toString().padLeft(2, '0');
|
|
||||||
_lastDuration = '${twoDigits(duration.inHours)}:'
|
|
||||||
'${twoDigits(duration.inMinutes.remainder(60))}:'
|
|
||||||
'${twoDigits(duration.inSeconds.remainder(60))}';
|
|
||||||
}
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void enableDurationUpdater() {
|
|
||||||
_updateDuration();
|
|
||||||
_lastDurationUpdateTimer = Timer.periodic(
|
|
||||||
const Duration(seconds: 1),
|
|
||||||
(_) => _updateDuration(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void disableDurationUpdater() {
|
|
||||||
_lastDurationUpdateTimer?.cancel();
|
|
||||||
_lastDurationUpdateTimer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> checkPermissions() async {
|
|
||||||
if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Permission.camera.request();
|
|
||||||
await Permission.microphone.request();
|
|
||||||
await Permission.bluetooth.request();
|
|
||||||
await Permission.bluetoothConnect.request();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setCall(SnChatCall call, SnChannel related) {
|
|
||||||
_current = call;
|
|
||||||
_channel = related;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<(String, String)> getRoomToken() async {
|
|
||||||
final resp = await _sn.client.post(
|
|
||||||
'/cgi/im/channels/${_channel!.keyPath}/calls/ongoing/token',
|
|
||||||
);
|
|
||||||
token = resp.data['token'];
|
|
||||||
endpoint = 'wss://${resp.data['endpoint']}';
|
|
||||||
return (token!, endpoint!);
|
|
||||||
}
|
|
||||||
|
|
||||||
void initHardware() {
|
|
||||||
if (_isReady) return;
|
|
||||||
|
|
||||||
_isReady = true;
|
|
||||||
hwSubscription = Hardware.instance.onDeviceChange.stream.listen(
|
|
||||||
_revertDevices,
|
|
||||||
);
|
|
||||||
Hardware.instance.enumerateDevices().then(_revertDevices);
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void initRoom() {
|
|
||||||
initHardware();
|
|
||||||
final timeout = const Duration(seconds: 60);
|
|
||||||
_room = Room(
|
|
||||||
roomOptions: RoomOptions(
|
|
||||||
dynacast: true,
|
|
||||||
adaptiveStream: true,
|
|
||||||
defaultAudioCaptureOptions: AudioCaptureOptions(
|
|
||||||
processor: LiveKitNoiseFilter(),
|
|
||||||
),
|
|
||||||
defaultAudioPublishOptions: AudioPublishOptions(
|
|
||||||
name: 'call_voice',
|
|
||||||
stream: 'call_stream',
|
|
||||||
),
|
|
||||||
defaultVideoPublishOptions: VideoPublishOptions(
|
|
||||||
name: 'call_video',
|
|
||||||
stream: 'call_stream',
|
|
||||||
simulcast: true,
|
|
||||||
backupVideoCodec: BackupVideoCodec(enabled: true),
|
|
||||||
),
|
|
||||||
defaultScreenShareCaptureOptions: ScreenShareCaptureOptions(
|
|
||||||
useiOSBroadcastExtension: true,
|
|
||||||
params: VideoParametersPresets.screenShareH1080FPS30,
|
|
||||||
),
|
|
||||||
defaultCameraCaptureOptions: CameraCaptureOptions(
|
|
||||||
maxFrameRate: 30,
|
|
||||||
params: VideoParametersPresets.h1080_169,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
connectOptions: ConnectOptions(
|
|
||||||
autoSubscribe: true,
|
|
||||||
timeouts: Timeouts(
|
|
||||||
connection: timeout,
|
|
||||||
debounce: timeout,
|
|
||||||
publish: timeout,
|
|
||||||
peerConnection: timeout,
|
|
||||||
iceRestart: timeout,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
_listener = _room.createListener();
|
|
||||||
WakelockPlus.enable();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> joinRoom(String url, String token) async {
|
|
||||||
if (_isMounted) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await _room.connect(
|
|
||||||
url,
|
|
||||||
token,
|
|
||||||
fastConnectOptions: FastConnectOptions(
|
|
||||||
microphone: TrackOption(track: _audioTrack),
|
|
||||||
camera: TrackOption(track: _videoTrack),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
_isMounted = true;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void setupRoom() {
|
|
||||||
if (isInitialized) return;
|
|
||||||
|
|
||||||
sortParticipants();
|
|
||||||
_room.addListener(_onRoomDidUpdate);
|
|
||||||
WidgetsBindingCompatible.instance?.addPostFrameCallback(
|
|
||||||
(_) => autoPublish(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (lkPlatformIsMobile()) {
|
|
||||||
Hardware.instance.setSpeakerphoneOn(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
_isBusy = false;
|
|
||||||
_isInitialized = true;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void autoPublish() async {
|
|
||||||
try {
|
|
||||||
if (enableVideo) {
|
|
||||||
await _room.localParticipant?.setCameraEnabled(true);
|
|
||||||
}
|
|
||||||
if (enableAudio) {
|
|
||||||
await _room.localParticipant?.setMicrophoneEnabled(true);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> setEnableAudio(bool value) async {
|
|
||||||
_enableAudio = value;
|
|
||||||
if (!_enableAudio) {
|
|
||||||
await _audioTrack?.stop();
|
|
||||||
_audioTrack = null;
|
|
||||||
} else {
|
|
||||||
await _changeLocalAudioTrack();
|
|
||||||
}
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> setEnableVideo(bool value) async {
|
|
||||||
_enableVideo = value;
|
|
||||||
if (!_enableVideo) {
|
|
||||||
await _videoTrack?.stop();
|
|
||||||
_videoTrack = null;
|
|
||||||
} else {
|
|
||||||
await _changeLocalVideoTrack();
|
|
||||||
}
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setupRoomListeners({
|
|
||||||
required Function(DisconnectReason?) onDisconnected,
|
|
||||||
}) {
|
|
||||||
_listener
|
|
||||||
..on<RoomDisconnectedEvent>((event) async {
|
|
||||||
onDisconnected(event.reason);
|
|
||||||
})
|
|
||||||
..on<ParticipantEvent>((event) => sortParticipants())
|
|
||||||
..on<LocalTrackPublishedEvent>((_) => sortParticipants())
|
|
||||||
..on<LocalTrackUnpublishedEvent>((_) => sortParticipants())
|
|
||||||
..on<TrackSubscribedEvent>((_) => sortParticipants())
|
|
||||||
..on<TrackUnsubscribedEvent>((_) => sortParticipants())
|
|
||||||
..on<ParticipantNameUpdatedEvent>((event) {
|
|
||||||
sortParticipants();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void sortParticipants() {
|
|
||||||
Map<String, ParticipantTrack> mediaTracks = {};
|
|
||||||
for (var participant in _room.remoteParticipants.values) {
|
|
||||||
mediaTracks[participant.sid] = ParticipantTrack(
|
|
||||||
participant: participant,
|
|
||||||
videoTrack: null,
|
|
||||||
isScreenShare: false,
|
|
||||||
);
|
|
||||||
|
|
||||||
for (var t in participant.videoTrackPublications) {
|
|
||||||
mediaTracks[participant.sid]?.videoTrack = t.track;
|
|
||||||
mediaTracks[participant.sid]?.isScreenShare = t.isScreenShare;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final newTracks = List<ParticipantTrack>.empty(growable: true);
|
|
||||||
|
|
||||||
final mediaTrackList = mediaTracks.values.toList();
|
|
||||||
mediaTrackList.sort((a, b) {
|
|
||||||
// Loudest people first
|
|
||||||
if (a.participant.isSpeaking && b.participant.isSpeaking) {
|
|
||||||
if (a.participant.audioLevel > b.participant.audioLevel) {
|
|
||||||
return -1;
|
|
||||||
} else {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Last spoke first
|
|
||||||
final aSpokeAt = a.participant.lastSpokeAt?.millisecondsSinceEpoch ?? 0;
|
|
||||||
final bSpokeAt = b.participant.lastSpokeAt?.millisecondsSinceEpoch ?? 0;
|
|
||||||
|
|
||||||
if (aSpokeAt != bSpokeAt) {
|
|
||||||
return aSpokeAt > bSpokeAt ? -1 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Has video first
|
|
||||||
if (a.participant.hasVideo != b.participant.hasVideo) {
|
|
||||||
return a.participant.hasVideo ? -1 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// First joined people first
|
|
||||||
return a.participant.joinedAt.millisecondsSinceEpoch -
|
|
||||||
b.participant.joinedAt.millisecondsSinceEpoch;
|
|
||||||
});
|
|
||||||
|
|
||||||
newTracks.addAll(mediaTrackList);
|
|
||||||
|
|
||||||
if (_room.localParticipant != null) {
|
|
||||||
ParticipantTrack localTrack = ParticipantTrack(
|
|
||||||
participant: _room.localParticipant!,
|
|
||||||
videoTrack: null,
|
|
||||||
isScreenShare: false,
|
|
||||||
);
|
|
||||||
|
|
||||||
final localParticipantTracks =
|
|
||||||
_room.localParticipant?.videoTrackPublications;
|
|
||||||
if (localParticipantTracks != null) {
|
|
||||||
for (var t in localParticipantTracks) {
|
|
||||||
localTrack.videoTrack = t.track;
|
|
||||||
localTrack.isScreenShare = t.isScreenShare;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newTracks.add(localTrack);
|
|
||||||
}
|
|
||||||
|
|
||||||
_participantTracks = newTracks;
|
|
||||||
|
|
||||||
if (focusTrack != null) {
|
|
||||||
final idx = participantTracks
|
|
||||||
.indexWhere((x) => x.participant.sid == _focusTrack!.participant.sid);
|
|
||||||
if (idx == -1) {
|
|
||||||
_focusTrack = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (focusTrack == null) {
|
|
||||||
_focusTrack = participantTracks.firstOrNull;
|
|
||||||
} else {
|
|
||||||
final idx = participantTracks.indexWhere(
|
|
||||||
(x) => _focusTrack!.participant.sid == x.participant.sid,
|
|
||||||
);
|
|
||||||
if (idx > -1) {
|
|
||||||
_focusTrack = participantTracks[idx];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _changeLocalAudioTrack() async {
|
|
||||||
if (_audioTrack != null) {
|
|
||||||
await _audioTrack!.stop();
|
|
||||||
_audioTrack = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_audioDevice != null) {
|
|
||||||
_audioTrack = await LocalAudioTrack.create(
|
|
||||||
AudioCaptureOptions(deviceId: _audioDevice!.deviceId),
|
|
||||||
);
|
|
||||||
await _audioTrack!.start();
|
|
||||||
}
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _changeLocalVideoTrack() async {
|
|
||||||
if (_videoTrack != null) {
|
|
||||||
await _videoTrack!.stop();
|
|
||||||
_videoTrack = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_videoDevice != null) {
|
|
||||||
_videoTrack = await LocalVideoTrack.createCameraTrack(
|
|
||||||
CameraCaptureOptions(
|
|
||||||
deviceId: _videoDevice!.deviceId,
|
|
||||||
params: VideoParametersPresets.h1080_169,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await _videoTrack!.start();
|
|
||||||
}
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _revertDevices(List<MediaDevice> devices) {
|
|
||||||
_audioInputs = devices.where((d) => d.kind == 'audioinput').toList();
|
|
||||||
_videoInputs = devices.where((d) => d.kind == 'videoinput').toList();
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onRoomDidUpdate() => sortParticipants();
|
|
||||||
|
|
||||||
Future<void> changeLocalAudioTrack() async {
|
|
||||||
if (audioTrack != null) {
|
|
||||||
await audioTrack!.stop();
|
|
||||||
_audioTrack = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (audioDevice != null) {
|
|
||||||
_audioTrack = await LocalAudioTrack.create(
|
|
||||||
AudioCaptureOptions(
|
|
||||||
deviceId: audioDevice!.deviceId,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await audioTrack!.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> changeLocalVideoTrack() async {
|
|
||||||
if (videoTrack != null) {
|
|
||||||
await _videoTrack!.stop();
|
|
||||||
_videoTrack = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (videoDevice != null) {
|
|
||||||
_videoTrack = await LocalVideoTrack.createCameraTrack(
|
|
||||||
CameraCaptureOptions(
|
|
||||||
deviceId: videoDevice!.deviceId,
|
|
||||||
params: VideoParametersPresets.h1080_169,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await videoTrack!.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void deactivateHardware() {
|
|
||||||
hwSubscription?.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
void disposeRoom() {
|
|
||||||
_isBusy = false;
|
|
||||||
_isMounted = false;
|
|
||||||
_isInitialized = false;
|
|
||||||
_current = null;
|
|
||||||
_channel = null;
|
|
||||||
_room.removeListener(_onRoomDidUpdate);
|
|
||||||
_room.disconnect();
|
|
||||||
_room.dispose();
|
|
||||||
_listener.dispose();
|
|
||||||
WakelockPlus.disable();
|
|
||||||
}
|
|
||||||
|
|
||||||
void disposeHardware() {
|
|
||||||
_isReady = false;
|
|
||||||
_audioTrack?.stop();
|
|
||||||
_audioTrack = null;
|
|
||||||
_videoTrack?.stop();
|
|
||||||
_videoTrack = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setVideoDevice(MediaDevice? value) {
|
|
||||||
_videoDevice = value;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setAudioDevice(MediaDevice? value) {
|
|
||||||
_audioDevice = value;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setFocusTrack(ParticipantTrack? value) {
|
|
||||||
_focusTrack = value;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setIsBusy(bool value) {
|
|
||||||
_isBusy = value;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
@ -22,7 +22,6 @@ import 'package:surface/screens/album.dart';
|
|||||||
import 'package:surface/screens/auth/login.dart';
|
import 'package:surface/screens/auth/login.dart';
|
||||||
import 'package:surface/screens/auth/register.dart';
|
import 'package:surface/screens/auth/register.dart';
|
||||||
import 'package:surface/screens/chat.dart';
|
import 'package:surface/screens/chat.dart';
|
||||||
import 'package:surface/screens/chat/call_room.dart';
|
|
||||||
import 'package:surface/screens/chat/channel_detail.dart';
|
import 'package:surface/screens/chat/channel_detail.dart';
|
||||||
import 'package:surface/screens/chat/manage.dart';
|
import 'package:surface/screens/chat/manage.dart';
|
||||||
import 'package:surface/screens/chat/room.dart';
|
import 'package:surface/screens/chat/room.dart';
|
||||||
@ -264,14 +263,6 @@ final _appRoutes = [
|
|||||||
extra: state.extra as ChatRoomScreenExtra?,
|
extra: state.extra as ChatRoomScreenExtra?,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
|
||||||
path: '/:scope/:alias/call',
|
|
||||||
name: 'chatCallRoom',
|
|
||||||
builder: (context, state) => CallRoomScreen(
|
|
||||||
scope: state.pathParameters['scope']!,
|
|
||||||
alias: state.pathParameters['alias']!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/:scope/:alias/detail',
|
path: '/:scope/:alias/detail',
|
||||||
name: 'channelDetail',
|
name: 'channelDetail',
|
||||||
|
@ -1,289 +0,0 @@
|
|||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:gap/gap.dart';
|
|
||||||
import 'package:livekit_client/livekit_client.dart' as livekit;
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
|
||||||
import 'package:surface/providers/chat_call.dart';
|
|
||||||
import 'package:surface/widgets/chat/call/call_controls.dart';
|
|
||||||
import 'package:surface/widgets/chat/call/call_participant.dart';
|
|
||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
|
||||||
|
|
||||||
class CallRoomScreen extends StatefulWidget {
|
|
||||||
final String scope;
|
|
||||||
final String alias;
|
|
||||||
|
|
||||||
const CallRoomScreen({super.key, required this.scope, required this.alias});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<CallRoomScreen> createState() => _CallRoomScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CallRoomScreenState extends State<CallRoomScreen> {
|
|
||||||
int _layoutMode = 0;
|
|
||||||
|
|
||||||
void _switchLayout() {
|
|
||||||
if (_layoutMode < 1) {
|
|
||||||
setState(() => _layoutMode++);
|
|
||||||
} else {
|
|
||||||
setState(() => _layoutMode = 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildMeetLayout() {
|
|
||||||
final call = context.read<ChatCallProvider>();
|
|
||||||
return Stack(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
color:
|
|
||||||
Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75),
|
|
||||||
child: call.focusTrack != null
|
|
||||||
? InteractiveParticipantWidget(
|
|
||||||
participant: call.focusTrack!,
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
top: 0,
|
|
||||||
child: SizedBox(
|
|
||||||
height: 128,
|
|
||||||
child: ListView.builder(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
itemCount: math.max(0, call.participantTracks.length),
|
|
||||||
itemBuilder: (BuildContext context, int index) {
|
|
||||||
final track = call.participantTracks[index];
|
|
||||||
if (track.participant.sid == call.focusTrack?.participant.sid) {
|
|
||||||
return Container();
|
|
||||||
}
|
|
||||||
|
|
||||||
return SizedBox(
|
|
||||||
height: 128,
|
|
||||||
width: 128,
|
|
||||||
child: InteractiveParticipantWidget(
|
|
||||||
participant: track,
|
|
||||||
avatarSize: 32,
|
|
||||||
onTap: () {
|
|
||||||
if (track.participant.sid !=
|
|
||||||
call.focusTrack?.participant.sid) {
|
|
||||||
call.setFocusTrack(track);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildListLayout() {
|
|
||||||
final call = context.read<ChatCallProvider>();
|
|
||||||
|
|
||||||
return LayoutBuilder(
|
|
||||||
builder: (context, constraints) {
|
|
||||||
return ListView.builder(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
itemCount: math.max(0, call.participantTracks.length),
|
|
||||||
itemBuilder: (BuildContext context, int index) {
|
|
||||||
final track = call.participantTracks[index];
|
|
||||||
return InteractiveParticipantWidget(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
||||||
isList: true,
|
|
||||||
avatarSize: 24,
|
|
||||||
participant: track,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
final call = context.read<ChatCallProvider>();
|
|
||||||
|
|
||||||
Future.delayed(Duration.zero, () {
|
|
||||||
call
|
|
||||||
..setupRoom()
|
|
||||||
..enableDurationUpdater();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final call = context.read<ChatCallProvider>();
|
|
||||||
|
|
||||||
return ListenableBuilder(
|
|
||||||
listenable: call,
|
|
||||||
builder: (context, _) {
|
|
||||||
return AppScaffold(
|
|
||||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
|
||||||
appBar: AppBar(
|
|
||||||
title: RichText(
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
text: TextSpan(children: [
|
|
||||||
TextSpan(
|
|
||||||
text: 'call'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
|
||||||
color: Theme.of(context).appBarTheme.foregroundColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const TextSpan(text: '\n'),
|
|
||||||
TextSpan(
|
|
||||||
text: call.lastDuration.toString(),
|
|
||||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
|
||||||
color: Theme.of(context).appBarTheme.foregroundColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
body: Column(
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: MediaQuery.of(context).size.width,
|
|
||||||
height: 64,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Builder(builder: (context) {
|
|
||||||
final call = context.read<ChatCallProvider>();
|
|
||||||
final connectionQuality =
|
|
||||||
call.room.localParticipant?.connectionQuality ??
|
|
||||||
livekit.ConnectionQuality.unknown;
|
|
||||||
return Expanded(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
call.channel?.name ?? 'unknown'.tr(),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(6),
|
|
||||||
Text(call.lastDuration.toString())
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
{
|
|
||||||
livekit.ConnectionState.disconnected:
|
|
||||||
'callStatusDisconnected'.tr(),
|
|
||||||
livekit.ConnectionState.connected:
|
|
||||||
'callStatusConnected'.tr(),
|
|
||||||
livekit.ConnectionState.connecting:
|
|
||||||
'callStatusConnecting'.tr(),
|
|
||||||
livekit.ConnectionState.reconnecting:
|
|
||||||
'callStatusReconnecting'.tr(),
|
|
||||||
}[call.room.connectionState]!,
|
|
||||||
),
|
|
||||||
const Gap(6),
|
|
||||||
if (connectionQuality !=
|
|
||||||
livekit.ConnectionQuality.unknown)
|
|
||||||
Icon(
|
|
||||||
{
|
|
||||||
livekit.ConnectionQuality.excellent:
|
|
||||||
Icons.signal_cellular_alt,
|
|
||||||
livekit.ConnectionQuality.good:
|
|
||||||
Icons.signal_cellular_alt_2_bar,
|
|
||||||
livekit.ConnectionQuality.poor:
|
|
||||||
Icons.signal_cellular_alt_1_bar,
|
|
||||||
}[connectionQuality],
|
|
||||||
color: {
|
|
||||||
livekit.ConnectionQuality.excellent:
|
|
||||||
Colors.green,
|
|
||||||
livekit.ConnectionQuality.good:
|
|
||||||
Colors.orange,
|
|
||||||
livekit.ConnectionQuality.poor:
|
|
||||||
Colors.red,
|
|
||||||
}[connectionQuality],
|
|
||||||
size: 16,
|
|
||||||
)
|
|
||||||
else
|
|
||||||
const SizedBox(
|
|
||||||
width: 12,
|
|
||||||
height: 12,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
color: Colors.white,
|
|
||||||
strokeWidth: 2,
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
),
|
|
||||||
).padding(all: 3),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
icon: _layoutMode == 0
|
|
||||||
? const Icon(Icons.view_list)
|
|
||||||
: const Icon(Icons.grid_view),
|
|
||||||
onPressed: () {
|
|
||||||
_switchLayout();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padding(left: 20, right: 16),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Material(
|
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
|
||||||
child: Builder(
|
|
||||||
builder: (context) {
|
|
||||||
switch (_layoutMode) {
|
|
||||||
case 1:
|
|
||||||
return _buildListLayout();
|
|
||||||
default:
|
|
||||||
return _buildMeetLayout();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (call.room.localParticipant != null)
|
|
||||||
SizedBox(
|
|
||||||
width: MediaQuery.of(context).size.width,
|
|
||||||
child: ControlsWidget(
|
|
||||||
call.room,
|
|
||||||
call.room.localParticipant!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Gap(MediaQuery.of(context).padding.bottom),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void deactivate() {
|
|
||||||
final call = context.read<ChatCallProvider>();
|
|
||||||
call.disableDurationUpdater();
|
|
||||||
super.deactivate();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void activate() {
|
|
||||||
final call = context.read<ChatCallProvider>();
|
|
||||||
call.enableDurationUpdater();
|
|
||||||
super.activate();
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,18 +2,17 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:jitsi_meet_flutter_sdk/jitsi_meet_flutter_sdk.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/controllers/chat_message_controller.dart';
|
import 'package:surface/controllers/chat_message_controller.dart';
|
||||||
import 'package:surface/controllers/post_write_controller.dart';
|
import 'package:surface/controllers/post_write_controller.dart';
|
||||||
import 'package:surface/providers/channel.dart';
|
import 'package:surface/providers/channel.dart';
|
||||||
import 'package:surface/providers/chat_call.dart';
|
|
||||||
import 'package:surface/providers/notification.dart';
|
import 'package:surface/providers/notification.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/providers/user_directory.dart';
|
import 'package:surface/providers/user_directory.dart';
|
||||||
@ -21,7 +20,6 @@ import 'package:surface/providers/userinfo.dart';
|
|||||||
import 'package:surface/providers/websocket.dart';
|
import 'package:surface/providers/websocket.dart';
|
||||||
import 'package:surface/types/chat.dart';
|
import 'package:surface/types/chat.dart';
|
||||||
import 'package:surface/types/websocket.dart';
|
import 'package:surface/types/websocket.dart';
|
||||||
import 'package:surface/widgets/chat/call/call_prejoin.dart';
|
|
||||||
import 'package:surface/widgets/chat/chat_message.dart';
|
import 'package:surface/widgets/chat/chat_message.dart';
|
||||||
import 'package:surface/widgets/chat/chat_message_input.dart';
|
import 'package:surface/widgets/chat/chat_message_input.dart';
|
||||||
import 'package:surface/widgets/chat/chat_typing_indicator.dart';
|
import 'package:surface/widgets/chat/chat_typing_indicator.dart';
|
||||||
@ -51,13 +49,11 @@ class ChatRoomScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||||
bool _isBusy = false;
|
bool _isBusy = false;
|
||||||
bool _isCalling = false;
|
|
||||||
bool _isJoining = false;
|
bool _isJoining = false;
|
||||||
|
|
||||||
SnChannel? _channel;
|
SnChannel? _channel;
|
||||||
SnChannelMember? _currentMember;
|
SnChannelMember? _currentMember;
|
||||||
SnChannelMember? _otherMember;
|
SnChannelMember? _otherMember;
|
||||||
SnChatCall? _ongoingCall;
|
|
||||||
|
|
||||||
final GlobalKey<ChatMessageInputState> _inputGlobalKey = GlobalKey();
|
final GlobalKey<ChatMessageInputState> _inputGlobalKey = GlobalKey();
|
||||||
late final ChatMessageController _messageController;
|
late final ChatMessageController _messageController;
|
||||||
@ -139,88 +135,25 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _fetchOngoingCall() async {
|
|
||||||
setState(() => _isCalling = true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
final sn = context.read<SnNetworkProvider>();
|
|
||||||
final resp = await sn.client.get(
|
|
||||||
'/cgi/im/channels/${_messageController.channel!.keyPath}/calls/ongoing',
|
|
||||||
options: Options(
|
|
||||||
validateStatus: (status) => status != null && status < 500,
|
|
||||||
receiveTimeout: const Duration(seconds: 60),
|
|
||||||
sendTimeout: const Duration(seconds: 60),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (resp.statusCode == 200) {
|
|
||||||
_ongoingCall = SnChatCall.fromJson(resp.data);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (!mounted) return;
|
|
||||||
context.showErrorDialog(err);
|
|
||||||
} finally {
|
|
||||||
setState(() => _isCalling = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _makeCall() async {
|
|
||||||
setState(() => _isCalling = true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
final sn = context.read<SnNetworkProvider>();
|
|
||||||
await sn.client.post(
|
|
||||||
'/cgi/im/channels/${_messageController.channel!.keyPath}/calls',
|
|
||||||
options: Options(
|
|
||||||
sendTimeout: const Duration(seconds: 30),
|
|
||||||
receiveTimeout: const Duration(seconds: 30),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
if (!mounted) return;
|
|
||||||
if (_ongoingCall == null) {
|
|
||||||
// ignore the error because the call is already ongoing
|
|
||||||
context.showErrorDialog(err);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setState(() => _isCalling = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _endCall() async {
|
|
||||||
setState(() => _isCalling = true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
final sn = context.read<SnNetworkProvider>();
|
|
||||||
await sn.client.delete(
|
|
||||||
'/cgi/im/channels/${_messageController.channel!.keyPath}/calls/ongoing',
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
if (!mounted) return;
|
|
||||||
context.showErrorDialog(err);
|
|
||||||
} finally {
|
|
||||||
setState(() => _isCalling = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onCallJoin() async {
|
Future<void> _onCallJoin() async {
|
||||||
await showModalBottomSheet(
|
final sn = context.read<SnNetworkProvider>();
|
||||||
context: context,
|
final ua = context.read<UserProvider>();
|
||||||
builder: (context) => ChatCallPrejoinPopup(
|
final meet = JitsiMeet();
|
||||||
ongoingCall: _ongoingCall!,
|
final confOpts = JitsiMeetConferenceOptions(
|
||||||
channel: _channel!,
|
room: 'sn-chat-${_channel!.id}',
|
||||||
onJoin: _onCallResume,
|
serverURL:
|
||||||
|
'https://meet.element.io', // TODO fetch this as config from remote
|
||||||
|
configOverrides: {
|
||||||
|
"subject": _channel!.name,
|
||||||
|
},
|
||||||
|
userInfo: JitsiMeetUserInfo(
|
||||||
|
avatar: ua.user!.avatar.isNotEmpty
|
||||||
|
? sn.getAttachmentUrl(ua.user!.avatar)
|
||||||
|
: null,
|
||||||
|
displayName: _currentMember!.nick ?? ua.user!.nick,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
meet.join(confOpts);
|
||||||
|
|
||||||
void _onCallResume() {
|
|
||||||
GoRouter.of(context).pushNamed(
|
|
||||||
'chatCallRoom',
|
|
||||||
pathParameters: {
|
|
||||||
'scope': _channel!.realm?.alias ?? 'global',
|
|
||||||
'alias': _channel!.alias,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _checkMessageMergeable(SnChatMessage? a, SnChatMessage? b) {
|
bool _checkMessageMergeable(SnChatMessage? a, SnChatMessage? b) {
|
||||||
@ -248,10 +181,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await Future.wait([
|
await _messageController.checkUpdate();
|
||||||
_messageController.checkUpdate(),
|
|
||||||
_fetchOngoingCall(),
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -260,23 +190,6 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
_messageController = ChatMessageController(context);
|
_messageController = ChatMessageController(context);
|
||||||
_initializeChat();
|
_initializeChat();
|
||||||
|
|
||||||
_wsSubscription = _ws.pk.stream.listen((event) {
|
|
||||||
switch (event.method) {
|
|
||||||
case 'calls.new':
|
|
||||||
final payload = SnChatCall.fromJson(event.payload!);
|
|
||||||
if (payload.channelId == _channel?.id) {
|
|
||||||
setState(() => _ongoingCall = payload);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'calls.end':
|
|
||||||
final payload = SnChatCall.fromJson(event.payload!);
|
|
||||||
if (payload.channelId == _channel?.id) {
|
|
||||||
setState(() => _ongoingCall = null);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -300,7 +213,6 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final call = context.watch<ChatCallProvider>();
|
|
||||||
final ud = context.read<UserDirectoryProvider>();
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
@ -324,14 +236,8 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
),
|
),
|
||||||
if (_currentMember != null)
|
if (_currentMember != null)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: _ongoingCall == null
|
icon: const Icon(Symbols.video_call),
|
||||||
? const Icon(Symbols.call)
|
onPressed: _onCallJoin,
|
||||||
: const Icon(Symbols.call_end),
|
|
||||||
onPressed: _isCalling
|
|
||||||
? null
|
|
||||||
: _ongoingCall == null
|
|
||||||
? _makeCall
|
|
||||||
: _endCall,
|
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Symbols.more_vert),
|
icon: const Icon(Symbols.more_vert),
|
||||||
@ -359,28 +265,6 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
LoadingIndicator(
|
LoadingIndicator(
|
||||||
isActive: _isBusy || _messageController.isAggressiveLoading,
|
isActive: _isBusy || _messageController.isAggressiveLoading,
|
||||||
),
|
),
|
||||||
SingleChildScrollView(
|
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
child: MaterialBanner(
|
|
||||||
dividerColor: Colors.transparent,
|
|
||||||
leading: const Icon(Symbols.call_received),
|
|
||||||
content: Text('callOngoingNotice').tr().padding(top: 2),
|
|
||||||
actions: [
|
|
||||||
if (call.current == null)
|
|
||||||
TextButton(
|
|
||||||
onPressed: _onCallJoin,
|
|
||||||
child: Text('callJoin').tr(),
|
|
||||||
)
|
|
||||||
else if (call.current?.channelId == _channel?.id)
|
|
||||||
TextButton(
|
|
||||||
onPressed: _onCallResume,
|
|
||||||
child: Text('callResume').tr(),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
).height(_ongoingCall != null ? 54 : 0, animate: true).animate(
|
|
||||||
const Duration(milliseconds: 300),
|
|
||||||
Curves.fastLinearToSlowEaseIn),
|
|
||||||
if (_currentMember == null && !_isBusy)
|
if (_currentMember == null && !_isBusy)
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Center(
|
child: Center(
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
|
||||||
import 'package:surface/types/account.dart';
|
import 'package:surface/types/account.dart';
|
||||||
import 'package:surface/types/attachment.dart';
|
import 'package:surface/types/attachment.dart';
|
||||||
import 'package:surface/types/realm.dart';
|
import 'package:surface/types/realm.dart';
|
||||||
@ -116,24 +115,3 @@ abstract class SnChatCall with _$SnChatCall {
|
|||||||
factory SnChatCall.fromJson(Map<String, dynamic> json) =>
|
factory SnChatCall.fromJson(Map<String, dynamic> json) =>
|
||||||
_$SnChatCallFromJson(json);
|
_$SnChatCallFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call stuff
|
|
||||||
|
|
||||||
enum ParticipantStatsType {
|
|
||||||
unknown,
|
|
||||||
localAudioSender,
|
|
||||||
localVideoSender,
|
|
||||||
remoteAudioReceiver,
|
|
||||||
remoteVideoReceiver,
|
|
||||||
}
|
|
||||||
|
|
||||||
class ParticipantTrack {
|
|
||||||
ParticipantTrack(
|
|
||||||
{required this.participant,
|
|
||||||
required this.videoTrack,
|
|
||||||
required this.isScreenShare});
|
|
||||||
|
|
||||||
VideoTrack? videoTrack;
|
|
||||||
Participant participant;
|
|
||||||
bool isScreenShare;
|
|
||||||
}
|
|
||||||
|
@ -1,369 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:surface/providers/chat_call.dart';
|
|
||||||
import 'package:surface/widgets/dialog.dart';
|
|
||||||
|
|
||||||
class ControlsWidget extends StatefulWidget {
|
|
||||||
final Room room;
|
|
||||||
final LocalParticipant participant;
|
|
||||||
|
|
||||||
const ControlsWidget(
|
|
||||||
this.room,
|
|
||||||
this.participant, {
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<StatefulWidget> createState() => _ControlsWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ControlsWidgetState extends State<ControlsWidget> {
|
|
||||||
CameraPosition _position = CameraPosition.front;
|
|
||||||
|
|
||||||
List<MediaDevice>? _audioInputs;
|
|
||||||
List<MediaDevice>? _audioOutputs;
|
|
||||||
List<MediaDevice>? _videoInputs;
|
|
||||||
|
|
||||||
StreamSubscription? _subscription;
|
|
||||||
|
|
||||||
bool _speakerphoneOn = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_participant.addListener(onChange);
|
|
||||||
_subscription = Hardware.instance.onDeviceChange.stream
|
|
||||||
.listen((List<MediaDevice> devices) {
|
|
||||||
_revertDevices(devices);
|
|
||||||
});
|
|
||||||
Hardware.instance.enumerateDevices().then(_revertDevices);
|
|
||||||
_speakerphoneOn = Hardware.instance.speakerOn ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_subscription?.cancel();
|
|
||||||
_participant.removeListener(onChange);
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
LocalParticipant get _participant => widget.participant;
|
|
||||||
|
|
||||||
void _revertDevices(List<MediaDevice> devices) async {
|
|
||||||
_audioInputs = devices.where((d) => d.kind == 'audioinput').toList();
|
|
||||||
_audioOutputs = devices.where((d) => d.kind == 'audiooutput').toList();
|
|
||||||
_videoInputs = devices.where((d) => d.kind == 'videoinput').toList();
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
void onChange() => setState(() {});
|
|
||||||
|
|
||||||
bool get isMuted => _participant.isMuted;
|
|
||||||
|
|
||||||
Future<bool?> showDisconnectDialog() {
|
|
||||||
return showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
builder: (ctx) => AlertDialog(
|
|
||||||
title: Text('callDisconnect').tr(),
|
|
||||||
content: Text('callDisconnectDescription').tr(),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(ctx, false),
|
|
||||||
child: Text('cancel').tr(),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(ctx, true),
|
|
||||||
child: Text('dialogConfirm').tr(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _disconnect() async {
|
|
||||||
if (await showDisconnectDialog() != true) return;
|
|
||||||
if (!mounted) return;
|
|
||||||
|
|
||||||
final call = context.read<ChatCallProvider>();
|
|
||||||
if (call.current != null) {
|
|
||||||
call.disposeRoom();
|
|
||||||
if (Navigator.canPop(context)) {
|
|
||||||
Navigator.pop(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _disableAudio() async {
|
|
||||||
await _participant.setMicrophoneEnabled(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _enableAudio() async {
|
|
||||||
await _participant.setMicrophoneEnabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _disableVideo() async {
|
|
||||||
await _participant.setCameraEnabled(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _enableVideo() async {
|
|
||||||
await _participant.setCameraEnabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _selectAudioOutput(MediaDevice device) async {
|
|
||||||
await widget.room.setAudioOutputDevice(device);
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _selectAudioInput(MediaDevice device) async {
|
|
||||||
await widget.room.setAudioInputDevice(device);
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _selectVideoInput(MediaDevice device) async {
|
|
||||||
await widget.room.setVideoInputDevice(device);
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _toggleSpeakerphoneOn() {
|
|
||||||
_speakerphoneOn = !_speakerphoneOn;
|
|
||||||
Hardware.instance.setSpeakerphoneOn(_speakerphoneOn);
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _toggleCamera() async {
|
|
||||||
final track = _participant.videoTrackPublications.firstOrNull?.track;
|
|
||||||
if (track == null) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
final newPosition = _position.switched();
|
|
||||||
await track.setCameraPosition(newPosition);
|
|
||||||
setState(() {
|
|
||||||
_position = newPosition;
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _enableScreenShare() async {
|
|
||||||
if (lkPlatformIsDesktop()) {
|
|
||||||
try {
|
|
||||||
final source = await showDialog<DesktopCapturerSource>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => ScreenSelectDialog(),
|
|
||||||
);
|
|
||||||
if (source == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var track = await LocalVideoTrack.createScreenShareTrack(
|
|
||||||
ScreenShareCaptureOptions(
|
|
||||||
captureScreenAudio: true,
|
|
||||||
sourceId: source.id,
|
|
||||||
maxFrameRate: 30.0,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await _participant.publishVideoTrack(track);
|
|
||||||
} catch (err) {
|
|
||||||
if (!mounted) return;
|
|
||||||
context.showErrorDialog(err);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (lkPlatformIs(PlatformType.iOS)) {
|
|
||||||
var track = await LocalVideoTrack.createScreenShareTrack(
|
|
||||||
const ScreenShareCaptureOptions(
|
|
||||||
useiOSBroadcastExtension: true,
|
|
||||||
captureScreenAudio: true,
|
|
||||||
maxFrameRate: 30.0,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await _participant.publishVideoTrack(track);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lkPlatformIsWebMobile()) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
|
||||||
content: Text('Screen share is not supported mobile platform.'),
|
|
||||||
));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _participant.setScreenShareEnabled(true, captureScreenAudio: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _disableScreenShare() async {
|
|
||||||
await _participant.setScreenShareEnabled(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
vertical: 10,
|
|
||||||
),
|
|
||||||
child: Wrap(
|
|
||||||
alignment: WrapAlignment.center,
|
|
||||||
spacing: 5,
|
|
||||||
runSpacing: 5,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Symbols.exit_to_app),
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
onPressed: _disconnect,
|
|
||||||
),
|
|
||||||
if (_participant.isMicrophoneEnabled())
|
|
||||||
if (lkPlatformIs(PlatformType.android))
|
|
||||||
IconButton(
|
|
||||||
onPressed: _disableAudio,
|
|
||||||
icon: const Icon(Symbols.mic),
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
tooltip: 'callMicrophoneOff'.tr(),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
PopupMenuButton<MediaDevice>(
|
|
||||||
icon: const Icon(Symbols.settings_voice),
|
|
||||||
itemBuilder: (BuildContext context) {
|
|
||||||
return [
|
|
||||||
PopupMenuItem<MediaDevice>(
|
|
||||||
value: null,
|
|
||||||
onTap: isMuted ? _enableAudio : _disableAudio,
|
|
||||||
child: ListTile(
|
|
||||||
leading: const Icon(Symbols.mic_off),
|
|
||||||
title: Text(isMuted
|
|
||||||
? 'callMicrophoneOn'.tr()
|
|
||||||
: 'callMicrophoneOff'.tr()),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_audioInputs != null)
|
|
||||||
..._audioInputs!.map((device) {
|
|
||||||
return PopupMenuItem<MediaDevice>(
|
|
||||||
value: device,
|
|
||||||
child: ListTile(
|
|
||||||
leading: (device.deviceId ==
|
|
||||||
widget.room.selectedAudioInputDeviceId)
|
|
||||||
? const Icon(Symbols.check_box)
|
|
||||||
: const Icon(Symbols.check_box_outline_blank),
|
|
||||||
title: Text(device.label),
|
|
||||||
),
|
|
||||||
onTap: () => _selectAudioInput(device),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
];
|
|
||||||
},
|
|
||||||
)
|
|
||||||
else
|
|
||||||
IconButton(
|
|
||||||
onPressed: _enableAudio,
|
|
||||||
icon: const Icon(Symbols.mic_off),
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
tooltip: 'callMicrophoneOn'.tr(),
|
|
||||||
),
|
|
||||||
if (_participant.isCameraEnabled())
|
|
||||||
PopupMenuButton<MediaDevice>(
|
|
||||||
icon: const Icon(Symbols.videocam_sharp),
|
|
||||||
itemBuilder: (BuildContext context) {
|
|
||||||
return [
|
|
||||||
PopupMenuItem<MediaDevice>(
|
|
||||||
value: null,
|
|
||||||
onTap: _disableVideo,
|
|
||||||
child: ListTile(
|
|
||||||
leading: const Icon(Symbols.videocam_off),
|
|
||||||
title: Text('callCameraOff'.tr()),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_videoInputs != null)
|
|
||||||
..._videoInputs!.map((device) {
|
|
||||||
return PopupMenuItem<MediaDevice>(
|
|
||||||
value: device,
|
|
||||||
child: ListTile(
|
|
||||||
leading: (device.deviceId ==
|
|
||||||
widget.room.selectedVideoInputDeviceId)
|
|
||||||
? const Icon(Symbols.check_box)
|
|
||||||
: const Icon(Symbols.check_box_outline_blank),
|
|
||||||
title: Text(device.label),
|
|
||||||
),
|
|
||||||
onTap: () => _selectVideoInput(device),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
];
|
|
||||||
},
|
|
||||||
)
|
|
||||||
else
|
|
||||||
IconButton(
|
|
||||||
onPressed: _enableVideo,
|
|
||||||
icon: const Icon(Symbols.videocam_off),
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
tooltip: 'callCameraOn'.tr(),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(_position == CameraPosition.back
|
|
||||||
? Symbols.video_camera_back
|
|
||||||
: Symbols.video_camera_front),
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
onPressed: () => _toggleCamera(),
|
|
||||||
tooltip: 'callVideoFlip'.tr(),
|
|
||||||
),
|
|
||||||
if (!lkPlatformIs(PlatformType.iOS))
|
|
||||||
PopupMenuButton<MediaDevice>(
|
|
||||||
icon: const Icon(Symbols.volume_up),
|
|
||||||
itemBuilder: (BuildContext context) {
|
|
||||||
return [
|
|
||||||
PopupMenuItem<MediaDevice>(
|
|
||||||
value: null,
|
|
||||||
child: ListTile(
|
|
||||||
leading: const Icon(Symbols.speaker),
|
|
||||||
title: Text('callSpeakerSelect').tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_audioOutputs != null)
|
|
||||||
..._audioOutputs!.map((device) {
|
|
||||||
return PopupMenuItem<MediaDevice>(
|
|
||||||
value: device,
|
|
||||||
child: ListTile(
|
|
||||||
leading: (device.deviceId ==
|
|
||||||
widget.room.selectedAudioOutputDeviceId)
|
|
||||||
? const Icon(Symbols.check_box)
|
|
||||||
: const Icon(Symbols.check_box_outline_blank),
|
|
||||||
title: Text(device.label),
|
|
||||||
),
|
|
||||||
onTap: () => _selectAudioOutput(device),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
];
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (!kIsWeb && Hardware.instance.canSwitchSpeakerphone)
|
|
||||||
IconButton(
|
|
||||||
onPressed: _toggleSpeakerphoneOn,
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
icon: _speakerphoneOn
|
|
||||||
? Icon(Symbols.volume_up)
|
|
||||||
: Icon(Symbols.volume_down),
|
|
||||||
tooltip: 'callSpeakerphoneToggle'.tr(),
|
|
||||||
),
|
|
||||||
if (_participant.isScreenShareEnabled())
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Symbols.stop_screen_share),
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
onPressed: () => _disableScreenShare(),
|
|
||||||
tooltip: 'callScreenOff'.tr(),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Symbols.screen_share),
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
onPressed: () => _enableScreenShare(),
|
|
||||||
tooltip: 'callScreenOn'.tr(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,86 +0,0 @@
|
|||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
|
||||||
import 'package:surface/types/account.dart';
|
|
||||||
import 'package:surface/widgets/account/account_image.dart';
|
|
||||||
|
|
||||||
class NoContentWidget extends StatefulWidget {
|
|
||||||
final SnAccount? userinfo;
|
|
||||||
final bool isSpeaking;
|
|
||||||
final double? avatarSize;
|
|
||||||
|
|
||||||
const NoContentWidget({
|
|
||||||
super.key,
|
|
||||||
this.userinfo,
|
|
||||||
this.avatarSize,
|
|
||||||
required this.isSpeaking,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<NoContentWidget> createState() => _NoContentWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _NoContentWidgetState extends State<NoContentWidget>
|
|
||||||
with SingleTickerProviderStateMixin {
|
|
||||||
late final AnimationController _animationController;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_animationController = AnimationController(vsync: this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(NoContentWidget old) {
|
|
||||||
super.didUpdateWidget(old);
|
|
||||||
if (widget.isSpeaking) {
|
|
||||||
_animationController.repeat(reverse: true);
|
|
||||||
} else {
|
|
||||||
_animationController
|
|
||||||
.animateTo(0, duration: 300.ms)
|
|
||||||
.then((_) => _animationController.reset());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final double radius = widget.avatarSize ??
|
|
||||||
math.min(
|
|
||||||
MediaQuery.of(context).size.width * 0.1,
|
|
||||||
MediaQuery.of(context).size.height * 0.1,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Animate(
|
|
||||||
autoPlay: false,
|
|
||||||
controller: _animationController,
|
|
||||||
effects: [
|
|
||||||
CustomEffect(
|
|
||||||
begin: widget.isSpeaking ? 2 : 0,
|
|
||||||
end: 8,
|
|
||||||
curve: Curves.easeInOut,
|
|
||||||
duration: 1250.ms,
|
|
||||||
builder: (context, value, child) => Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.all(Radius.circular(radius + 8)),
|
|
||||||
border: value > 0
|
|
||||||
? Border.all(color: Colors.green, width: value)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
child: AccountImage(
|
|
||||||
content: widget.userinfo?.avatar,
|
|
||||||
radius: radius,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_animationController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,337 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
|
||||||
import 'package:gap/gap.dart';
|
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
|
||||||
import 'package:surface/types/account.dart';
|
|
||||||
import 'package:surface/types/chat.dart';
|
|
||||||
import 'package:surface/widgets/chat/call/call_no_content.dart';
|
|
||||||
import 'package:surface/widgets/chat/call/call_participant_info.dart';
|
|
||||||
import 'package:surface/widgets/chat/call/call_participant_menu.dart';
|
|
||||||
import 'package:surface/widgets/chat/call/call_participant_stats.dart';
|
|
||||||
|
|
||||||
abstract class ParticipantWidget extends StatefulWidget {
|
|
||||||
static ParticipantWidget widgetFor(
|
|
||||||
ParticipantTrack participantTrack, {
|
|
||||||
double? avatarSize,
|
|
||||||
EdgeInsets? padding,
|
|
||||||
bool showStatsLayer = false,
|
|
||||||
bool isList = false,
|
|
||||||
}) {
|
|
||||||
if (participantTrack.participant is LocalParticipant) {
|
|
||||||
return LocalParticipantWidget(
|
|
||||||
participantTrack.participant as LocalParticipant,
|
|
||||||
participantTrack.videoTrack,
|
|
||||||
avatarSize,
|
|
||||||
participantTrack.isScreenShare,
|
|
||||||
showStatsLayer,
|
|
||||||
isList,
|
|
||||||
padding,
|
|
||||||
);
|
|
||||||
} else if (participantTrack.participant is RemoteParticipant) {
|
|
||||||
return RemoteParticipantWidget(
|
|
||||||
participantTrack.participant as RemoteParticipant,
|
|
||||||
participantTrack.videoTrack,
|
|
||||||
avatarSize,
|
|
||||||
participantTrack.isScreenShare,
|
|
||||||
showStatsLayer,
|
|
||||||
isList,
|
|
||||||
padding,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw UnimplementedError('Unknown participant type');
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract final Participant participant;
|
|
||||||
abstract final VideoTrack? videoTrack;
|
|
||||||
abstract final bool isScreenShare;
|
|
||||||
abstract final double? avatarSize;
|
|
||||||
abstract final bool showStatsLayer;
|
|
||||||
abstract final bool isList;
|
|
||||||
abstract final EdgeInsets? padding;
|
|
||||||
final VideoQuality quality;
|
|
||||||
|
|
||||||
const ParticipantWidget({
|
|
||||||
super.key,
|
|
||||||
this.quality = VideoQuality.MEDIUM,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class LocalParticipantWidget extends ParticipantWidget {
|
|
||||||
@override
|
|
||||||
final LocalParticipant participant;
|
|
||||||
@override
|
|
||||||
final VideoTrack? videoTrack;
|
|
||||||
@override
|
|
||||||
final double? avatarSize;
|
|
||||||
@override
|
|
||||||
final bool isScreenShare;
|
|
||||||
@override
|
|
||||||
final bool showStatsLayer;
|
|
||||||
@override
|
|
||||||
final bool isList;
|
|
||||||
@override
|
|
||||||
final EdgeInsets? padding;
|
|
||||||
|
|
||||||
const LocalParticipantWidget(
|
|
||||||
this.participant,
|
|
||||||
this.videoTrack,
|
|
||||||
this.avatarSize,
|
|
||||||
this.isScreenShare,
|
|
||||||
this.showStatsLayer,
|
|
||||||
this.isList,
|
|
||||||
this.padding, {
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<StatefulWidget> createState() => _LocalParticipantWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class RemoteParticipantWidget extends ParticipantWidget {
|
|
||||||
@override
|
|
||||||
final RemoteParticipant participant;
|
|
||||||
@override
|
|
||||||
final VideoTrack? videoTrack;
|
|
||||||
@override
|
|
||||||
final double? avatarSize;
|
|
||||||
@override
|
|
||||||
final bool isScreenShare;
|
|
||||||
@override
|
|
||||||
final bool showStatsLayer;
|
|
||||||
@override
|
|
||||||
final bool isList;
|
|
||||||
@override
|
|
||||||
final EdgeInsets? padding;
|
|
||||||
|
|
||||||
const RemoteParticipantWidget(
|
|
||||||
this.participant,
|
|
||||||
this.videoTrack,
|
|
||||||
this.avatarSize,
|
|
||||||
this.isScreenShare,
|
|
||||||
this.showStatsLayer,
|
|
||||||
this.isList,
|
|
||||||
this.padding, {
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<StatefulWidget> createState() => _RemoteParticipantWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class _ParticipantWidgetState<T extends ParticipantWidget>
|
|
||||||
extends State<T> {
|
|
||||||
VideoTrack? get _activeVideoTrack;
|
|
||||||
|
|
||||||
TrackPublication? get _firstAudioPublication;
|
|
||||||
|
|
||||||
SnAccount? _userinfoMetadata;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
widget.participant.addListener(onParticipantChanged);
|
|
||||||
onParticipantChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
widget.participant.removeListener(onParticipantChanged);
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(covariant T oldWidget) {
|
|
||||||
oldWidget.participant.removeListener(onParticipantChanged);
|
|
||||||
widget.participant.addListener(onParticipantChanged);
|
|
||||||
onParticipantChanged();
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
}
|
|
||||||
|
|
||||||
void onParticipantChanged() {
|
|
||||||
setState(() {
|
|
||||||
if (widget.participant.metadata != null) {
|
|
||||||
_userinfoMetadata = SnAccount.fromJson(
|
|
||||||
jsonDecode(widget.participant.metadata!),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (widget.isList) {
|
|
||||||
return Padding(
|
|
||||||
padding: widget.padding ?? EdgeInsets.zero,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: (widget.avatarSize ?? 32) * 2,
|
|
||||||
height: (widget.avatarSize ?? 32) * 2,
|
|
||||||
child: Center(
|
|
||||||
child: NoContentWidget(
|
|
||||||
userinfo: _userinfoMetadata,
|
|
||||||
avatarSize: widget.avatarSize,
|
|
||||||
isSpeaking: widget.participant.isSpeaking,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
Expanded(
|
|
||||||
child: SizedBox(
|
|
||||||
height: (widget.avatarSize ?? 32) * 2,
|
|
||||||
child: ParticipantInfoWidget(
|
|
||||||
isList: true,
|
|
||||||
title: widget.participant.name.isNotEmpty
|
|
||||||
? widget.participant.name
|
|
||||||
: widget.participant.identity,
|
|
||||||
audioAvailable: _firstAudioPublication?.muted == false &&
|
|
||||||
_firstAudioPublication?.subscribed == true,
|
|
||||||
connectionQuality: widget.participant.connectionQuality,
|
|
||||||
isScreenShare: widget.isScreenShare,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (_activeVideoTrack != null && !_activeVideoTrack!.muted)
|
|
||||||
ClipRRect(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio: 16 / 9,
|
|
||||||
child: Material(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.surfaceContainer
|
|
||||||
.withOpacity(0.75),
|
|
||||||
child: VideoTrackRenderer(
|
|
||||||
_activeVideoTrack!,
|
|
||||||
fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).padding(top: 8),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Stack(
|
|
||||||
children: [
|
|
||||||
if (_activeVideoTrack != null && !_activeVideoTrack!.muted)
|
|
||||||
VideoTrackRenderer(
|
|
||||||
_activeVideoTrack!,
|
|
||||||
fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
|
|
||||||
)
|
|
||||||
else
|
|
||||||
Center(
|
|
||||||
child: NoContentWidget(
|
|
||||||
userinfo: _userinfoMetadata,
|
|
||||||
avatarSize: widget.avatarSize,
|
|
||||||
isSpeaking: widget.participant.isSpeaking,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (widget.showStatsLayer)
|
|
||||||
Positioned(
|
|
||||||
top: 30,
|
|
||||||
right: 30,
|
|
||||||
child: ParticipantStatsWidget(participant: widget.participant),
|
|
||||||
),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
ParticipantInfoWidget(
|
|
||||||
title: widget.participant.name.isNotEmpty
|
|
||||||
? widget.participant.name
|
|
||||||
: widget.participant.identity,
|
|
||||||
audioAvailable: _firstAudioPublication?.muted == false &&
|
|
||||||
_firstAudioPublication?.subscribed == true,
|
|
||||||
connectionQuality: widget.participant.connectionQuality,
|
|
||||||
isScreenShare: widget.isScreenShare,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LocalParticipantWidgetState
|
|
||||||
extends _ParticipantWidgetState<LocalParticipantWidget> {
|
|
||||||
@override
|
|
||||||
LocalTrackPublication<LocalAudioTrack>? get _firstAudioPublication =>
|
|
||||||
widget.participant.audioTrackPublications.firstOrNull;
|
|
||||||
|
|
||||||
@override
|
|
||||||
VideoTrack? get _activeVideoTrack => widget.videoTrack;
|
|
||||||
}
|
|
||||||
|
|
||||||
class _RemoteParticipantWidgetState
|
|
||||||
extends _ParticipantWidgetState<RemoteParticipantWidget> {
|
|
||||||
@override
|
|
||||||
RemoteTrackPublication<RemoteAudioTrack>? get _firstAudioPublication =>
|
|
||||||
widget.participant.audioTrackPublications.firstOrNull;
|
|
||||||
|
|
||||||
@override
|
|
||||||
VideoTrack? get _activeVideoTrack => widget.videoTrack;
|
|
||||||
}
|
|
||||||
|
|
||||||
class InteractiveParticipantWidget extends StatelessWidget {
|
|
||||||
final double? avatarSize;
|
|
||||||
final bool isList;
|
|
||||||
final ParticipantTrack participant;
|
|
||||||
final Function? onTap;
|
|
||||||
final EdgeInsets? padding;
|
|
||||||
|
|
||||||
const InteractiveParticipantWidget({
|
|
||||||
super.key,
|
|
||||||
this.avatarSize,
|
|
||||||
this.isList = false,
|
|
||||||
this.padding,
|
|
||||||
required this.participant,
|
|
||||||
this.onTap,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Material(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: InkWell(
|
|
||||||
onTap: onTap != null
|
|
||||||
? () {
|
|
||||||
onTap?.call();
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
onLongPress: () {
|
|
||||||
if (participant.participant is LocalParticipant) return;
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => ParticipantMenu(
|
|
||||||
participant: participant.participant as RemoteParticipant,
|
|
||||||
videoTrack: participant.videoTrack,
|
|
||||||
isScreenShare: participant.isScreenShare,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
child: ParticipantWidget.widgetFor(
|
|
||||||
participant,
|
|
||||||
avatarSize: avatarSize,
|
|
||||||
isList: isList,
|
|
||||||
padding: padding,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,140 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:gap/gap.dart';
|
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
|
||||||
|
|
||||||
class ParticipantInfoWidget extends StatelessWidget {
|
|
||||||
final String? title;
|
|
||||||
final bool audioAvailable;
|
|
||||||
final ConnectionQuality connectionQuality;
|
|
||||||
final bool isScreenShare;
|
|
||||||
final bool isList;
|
|
||||||
|
|
||||||
const ParticipantInfoWidget({
|
|
||||||
super.key,
|
|
||||||
this.title,
|
|
||||||
this.audioAvailable = true,
|
|
||||||
this.connectionQuality = ConnectionQuality.unknown,
|
|
||||||
this.isScreenShare = false,
|
|
||||||
this.isList = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (isList) {
|
|
||||||
return Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (title != null)
|
|
||||||
Text(
|
|
||||||
title!,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
).padding(left: 2),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
isScreenShare
|
|
||||||
? const Icon(
|
|
||||||
Symbols.monitor,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 16,
|
|
||||||
)
|
|
||||||
: Icon(
|
|
||||||
audioAvailable ? Symbols.mic : Symbols.mic_off,
|
|
||||||
color: audioAvailable ? Colors.white : Colors.red,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
const Gap(3),
|
|
||||||
if (connectionQuality != ConnectionQuality.unknown)
|
|
||||||
Icon(
|
|
||||||
{
|
|
||||||
ConnectionQuality.excellent: Symbols.signal_cellular_alt,
|
|
||||||
ConnectionQuality.good: Symbols.signal_cellular_alt_2_bar,
|
|
||||||
ConnectionQuality.poor: Symbols.signal_cellular_alt_1_bar,
|
|
||||||
}[connectionQuality],
|
|
||||||
color: {
|
|
||||||
ConnectionQuality.excellent: Colors.green,
|
|
||||||
ConnectionQuality.good: Colors.orange,
|
|
||||||
ConnectionQuality.poor: Colors.red,
|
|
||||||
}[connectionQuality],
|
|
||||||
size: 16,
|
|
||||||
)
|
|
||||||
else
|
|
||||||
const SizedBox(
|
|
||||||
width: 12,
|
|
||||||
height: 12,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
color: Colors.white,
|
|
||||||
strokeWidth: 2,
|
|
||||||
),
|
|
||||||
).padding(all: 3),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75),
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
vertical: 7,
|
|
||||||
horizontal: 10,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
if (title != null)
|
|
||||||
Flexible(
|
|
||||||
child: Text(
|
|
||||||
title!,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: const TextStyle(color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(5),
|
|
||||||
isScreenShare
|
|
||||||
? const Icon(
|
|
||||||
Symbols.monitor,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 16,
|
|
||||||
)
|
|
||||||
: Icon(
|
|
||||||
audioAvailable ? Symbols.mic : Symbols.mic_off,
|
|
||||||
color: audioAvailable ? Colors.white : Colors.red,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
const Gap(3),
|
|
||||||
if (connectionQuality != ConnectionQuality.unknown)
|
|
||||||
Icon(
|
|
||||||
{
|
|
||||||
ConnectionQuality.excellent: Symbols.signal_cellular_alt,
|
|
||||||
ConnectionQuality.good: Symbols.signal_cellular_alt_2_bar,
|
|
||||||
ConnectionQuality.poor: Symbols.signal_cellular_alt_1_bar,
|
|
||||||
}[connectionQuality],
|
|
||||||
color: {
|
|
||||||
ConnectionQuality.excellent: Colors.green,
|
|
||||||
ConnectionQuality.good: Colors.orange,
|
|
||||||
ConnectionQuality.poor: Colors.red,
|
|
||||||
}[connectionQuality],
|
|
||||||
size: 16,
|
|
||||||
)
|
|
||||||
else
|
|
||||||
const SizedBox(
|
|
||||||
width: 12,
|
|
||||||
height: 12,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
color: Colors.white,
|
|
||||||
strokeWidth: 2,
|
|
||||||
),
|
|
||||||
).padding(all: 3),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,161 +0,0 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
|
||||||
|
|
||||||
class ParticipantMenu extends StatefulWidget {
|
|
||||||
final RemoteParticipant participant;
|
|
||||||
final VideoTrack? videoTrack;
|
|
||||||
final bool isScreenShare;
|
|
||||||
final bool showStatsLayer;
|
|
||||||
|
|
||||||
const ParticipantMenu({
|
|
||||||
super.key,
|
|
||||||
required this.participant,
|
|
||||||
this.videoTrack,
|
|
||||||
this.isScreenShare = false,
|
|
||||||
this.showStatsLayer = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ParticipantMenu> createState() => _ParticipantMenuState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ParticipantMenuState extends State<ParticipantMenu> {
|
|
||||||
RemoteTrackPublication<RemoteVideoTrack>? get _videoPublication =>
|
|
||||||
widget.participant.videoTrackPublications
|
|
||||||
.where((element) => element.sid == widget.videoTrack?.sid)
|
|
||||||
.firstOrNull;
|
|
||||||
|
|
||||||
RemoteTrackPublication<RemoteAudioTrack>? get _firstAudioPublication =>
|
|
||||||
widget.participant.audioTrackPublications.firstOrNull;
|
|
||||||
|
|
||||||
void tookAction() {
|
|
||||||
if (Navigator.canPop(context)) {
|
|
||||||
Navigator.pop(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.only(left: 8, right: 8, top: 20, bottom: 12),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 8,
|
|
||||||
vertical: 12,
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'callParticipantAction',
|
|
||||||
style: Theme.of(context).textTheme.headlineSmall,
|
|
||||||
).tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: ListView(
|
|
||||||
children: [
|
|
||||||
if (_firstAudioPublication != null && !widget.isScreenShare)
|
|
||||||
ListTile(
|
|
||||||
leading: Icon(
|
|
||||||
Symbols.volume_up,
|
|
||||||
color: {
|
|
||||||
TrackSubscriptionState.notAllowed:
|
|
||||||
Theme.of(context).colorScheme.error,
|
|
||||||
TrackSubscriptionState.unsubscribed: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.onSurface
|
|
||||||
.withOpacity(0.6),
|
|
||||||
TrackSubscriptionState.subscribed:
|
|
||||||
Theme.of(context).colorScheme.primary,
|
|
||||||
}[_firstAudioPublication!.subscriptionState],
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
_firstAudioPublication!.subscribed
|
|
||||||
? 'callParticipantMicrophoneOff'.tr()
|
|
||||||
: 'callParticipantMicrophoneOn'.tr(),
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
if (_firstAudioPublication!.subscribed) {
|
|
||||||
_firstAudioPublication!.unsubscribe();
|
|
||||||
} else {
|
|
||||||
_firstAudioPublication!.subscribe();
|
|
||||||
}
|
|
||||||
tookAction();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (_videoPublication != null)
|
|
||||||
ListTile(
|
|
||||||
leading: Icon(
|
|
||||||
widget.isScreenShare ? Symbols.monitor : Symbols.videocam,
|
|
||||||
color: {
|
|
||||||
TrackSubscriptionState.notAllowed:
|
|
||||||
Theme.of(context).colorScheme.error,
|
|
||||||
TrackSubscriptionState.unsubscribed: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.onSurface
|
|
||||||
.withOpacity(0.6),
|
|
||||||
TrackSubscriptionState.subscribed:
|
|
||||||
Theme.of(context).colorScheme.primary,
|
|
||||||
}[_videoPublication!.subscriptionState],
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
_videoPublication!.subscribed
|
|
||||||
? 'callParticipantVideoOff'.tr()
|
|
||||||
: 'callParticipantVideoOn'.tr(),
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
if (_videoPublication!.subscribed) {
|
|
||||||
_videoPublication!.unsubscribe();
|
|
||||||
} else {
|
|
||||||
_videoPublication!.subscribe();
|
|
||||||
}
|
|
||||||
tookAction();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (_videoPublication != null) const Divider(thickness: 0.3),
|
|
||||||
if (_videoPublication != null)
|
|
||||||
...[30, 15, 8].map(
|
|
||||||
(x) => ListTile(
|
|
||||||
leading: Icon(
|
|
||||||
_videoPublication?.fps == x
|
|
||||||
? Symbols.check_box
|
|
||||||
: Symbols.check_box_outline_blank,
|
|
||||||
),
|
|
||||||
title: Text('Set preferred frame-per-second to $x'),
|
|
||||||
onTap: () {
|
|
||||||
_videoPublication!.setVideoFPS(x);
|
|
||||||
tookAction();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_videoPublication != null) const Divider(thickness: 0.3),
|
|
||||||
if (_videoPublication != null)
|
|
||||||
...[
|
|
||||||
('High', VideoQuality.HIGH),
|
|
||||||
('Medium', VideoQuality.MEDIUM),
|
|
||||||
('Low', VideoQuality.LOW),
|
|
||||||
].map(
|
|
||||||
(x) => ListTile(
|
|
||||||
leading: Icon(
|
|
||||||
_videoPublication?.videoQuality == x.$2
|
|
||||||
? Symbols.check_box
|
|
||||||
: Symbols.check_box_outline_blank,
|
|
||||||
),
|
|
||||||
title: Text('Set preferred quality to ${x.$1}'),
|
|
||||||
onTap: () {
|
|
||||||
_videoPublication!.setVideoQuality(x.$2);
|
|
||||||
tookAction();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,133 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
|
||||||
import 'package:surface/types/chat.dart';
|
|
||||||
|
|
||||||
class ParticipantStatsWidget extends StatefulWidget {
|
|
||||||
const ParticipantStatsWidget({super.key, required this.participant});
|
|
||||||
|
|
||||||
final Participant participant;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<StatefulWidget> createState() => _ParticipantStatsWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ParticipantStatsWidgetState extends State<ParticipantStatsWidget> {
|
|
||||||
List<EventsListener<TrackEvent>> listeners = [];
|
|
||||||
ParticipantStatsType statsType = ParticipantStatsType.unknown;
|
|
||||||
Map<String, String> stats = {};
|
|
||||||
|
|
||||||
void _setUpListener(Track track) {
|
|
||||||
var listener = track.createListener();
|
|
||||||
listeners.add(listener);
|
|
||||||
if (track is LocalVideoTrack) {
|
|
||||||
statsType = ParticipantStatsType.localVideoSender;
|
|
||||||
listener.on<VideoSenderStatsEvent>((event) {
|
|
||||||
setState(() {
|
|
||||||
stats['video tx'] = 'total sent ${event.currentBitrate.toInt()} kpbs';
|
|
||||||
event.stats.forEach((key, value) {
|
|
||||||
stats['layer-$key'] =
|
|
||||||
'${value.frameWidth ?? 0}x${value.frameHeight ?? 0} ${value.framesPerSecond?.toDouble() ?? 0} fps, ${event.bitrateForLayers[key] ?? 0} kbps';
|
|
||||||
});
|
|
||||||
var firstStats =
|
|
||||||
event.stats['f'] ?? event.stats['h'] ?? event.stats['q'];
|
|
||||||
if (firstStats != null) {
|
|
||||||
stats['encoder'] = firstStats.encoderImplementation ?? '';
|
|
||||||
stats['video codec'] =
|
|
||||||
'${firstStats.mimeType}, ${firstStats.clockRate}hz, pt: ${firstStats.payloadType}';
|
|
||||||
stats['qualityLimitationReason'] =
|
|
||||||
firstStats.qualityLimitationReason ?? '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else if (track is RemoteVideoTrack) {
|
|
||||||
statsType = ParticipantStatsType.remoteVideoReceiver;
|
|
||||||
listener.on<VideoReceiverStatsEvent>((event) {
|
|
||||||
setState(() {
|
|
||||||
stats['video rx'] = '${event.currentBitrate.toInt()} kpbs';
|
|
||||||
stats['video codec'] =
|
|
||||||
'${event.stats.mimeType}, ${event.stats.clockRate}hz, pt: ${event.stats.payloadType}';
|
|
||||||
stats['video size'] =
|
|
||||||
'${event.stats.frameWidth}x${event.stats.frameHeight} ${event.stats.framesPerSecond?.toDouble()}fps';
|
|
||||||
stats['video jitter'] = '${event.stats.jitter} s';
|
|
||||||
stats['video decoder'] = '${event.stats.decoderImplementation}';
|
|
||||||
stats['video packets lost'] = '${event.stats.packetsLost}';
|
|
||||||
stats['video packets received'] = '${event.stats.packetsReceived}';
|
|
||||||
stats['video frames received'] = '${event.stats.framesReceived}';
|
|
||||||
stats['video frames decoded'] = '${event.stats.framesDecoded}';
|
|
||||||
stats['video frames dropped'] = '${event.stats.framesDropped}';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else if (track is LocalAudioTrack) {
|
|
||||||
statsType = ParticipantStatsType.localAudioSender;
|
|
||||||
listener.on<AudioSenderStatsEvent>((event) {
|
|
||||||
setState(() {
|
|
||||||
stats['audio tx'] = '${event.currentBitrate.toInt()} kpbs';
|
|
||||||
stats['audio codec'] =
|
|
||||||
'${event.stats.mimeType}, ${event.stats.clockRate}hz, ${event.stats.channels}ch, pt: ${event.stats.payloadType}';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else if (track is RemoteAudioTrack) {
|
|
||||||
statsType = ParticipantStatsType.remoteAudioReceiver;
|
|
||||||
listener.on<AudioReceiverStatsEvent>((event) {
|
|
||||||
setState(() {
|
|
||||||
stats['audio rx'] = '${event.currentBitrate.toInt()} kpbs';
|
|
||||||
stats['audio codec'] =
|
|
||||||
'${event.stats.mimeType}, ${event.stats.clockRate}hz, ${event.stats.channels}ch, pt: ${event.stats.payloadType}';
|
|
||||||
stats['audio jitter'] = '${event.stats.jitter} s';
|
|
||||||
stats['audio concealed samples'] =
|
|
||||||
'${event.stats.concealedSamples} / ${event.stats.concealmentEvents}';
|
|
||||||
stats['audio packets lost'] = '${event.stats.packetsLost}';
|
|
||||||
stats['audio packets received'] = '${event.stats.packetsReceived}';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onParticipantChanged() {
|
|
||||||
for (var element in listeners) {
|
|
||||||
element.dispose();
|
|
||||||
}
|
|
||||||
listeners.clear();
|
|
||||||
for (var track in [
|
|
||||||
...widget.participant.videoTrackPublications,
|
|
||||||
...widget.participant.audioTrackPublications
|
|
||||||
]) {
|
|
||||||
if (track.track != null) {
|
|
||||||
_setUpListener(track.track!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
widget.participant.addListener(onParticipantChanged);
|
|
||||||
onParticipantChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void deactivate() {
|
|
||||||
for (var element in listeners) {
|
|
||||||
element.dispose();
|
|
||||||
}
|
|
||||||
widget.participant.removeListener(onParticipantChanged);
|
|
||||||
super.deactivate();
|
|
||||||
}
|
|
||||||
|
|
||||||
num sendBitrate = 0;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75),
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
vertical: 8,
|
|
||||||
horizontal: 8,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children:
|
|
||||||
stats.entries.map((e) => Text('${e.key}: ${e.value}')).toList(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,191 +0,0 @@
|
|||||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
|
||||||
import 'package:surface/providers/chat_call.dart';
|
|
||||||
import 'package:surface/types/chat.dart';
|
|
||||||
import 'package:surface/widgets/dialog.dart';
|
|
||||||
|
|
||||||
class ChatCallPrejoinPopup extends StatefulWidget {
|
|
||||||
final SnChatCall ongoingCall;
|
|
||||||
final SnChannel channel;
|
|
||||||
final void Function() onJoin;
|
|
||||||
|
|
||||||
const ChatCallPrejoinPopup({
|
|
||||||
super.key,
|
|
||||||
required this.ongoingCall,
|
|
||||||
required this.channel,
|
|
||||||
required this.onJoin,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ChatCallPrejoinPopup> createState() => _ChatCallPrejoinPopupState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ChatCallPrejoinPopupState extends State<ChatCallPrejoinPopup> {
|
|
||||||
bool _isBusy = false;
|
|
||||||
|
|
||||||
late final ChatCallProvider _call = context.read<ChatCallProvider>();
|
|
||||||
|
|
||||||
void _performJoin() async {
|
|
||||||
setState(() => _isBusy = true);
|
|
||||||
|
|
||||||
_call.setCall(widget.ongoingCall, widget.channel);
|
|
||||||
_call.setIsBusy(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
final resp = await _call.getRoomToken();
|
|
||||||
final token = resp.$1;
|
|
||||||
final endpoint = resp.$2;
|
|
||||||
|
|
||||||
_call.initRoom();
|
|
||||||
_call.setupRoomListeners(
|
|
||||||
onDisconnected: (reason) {
|
|
||||||
context.showSnackbar(
|
|
||||||
'callDisconnected'.tr(args: [reason.toString()]),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await _call.joinRoom(endpoint, token);
|
|
||||||
widget.onJoin();
|
|
||||||
|
|
||||||
if (!mounted) return;
|
|
||||||
Navigator.pop(context);
|
|
||||||
} catch (e) {
|
|
||||||
if (!mounted) return;
|
|
||||||
context.showErrorDialog(e);
|
|
||||||
} finally {
|
|
||||||
setState(() => _isBusy = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
final call = context.read<ChatCallProvider>();
|
|
||||||
call.checkPermissions().then((_) {
|
|
||||||
call.initHardware();
|
|
||||||
});
|
|
||||||
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final call = context.read<ChatCallProvider>();
|
|
||||||
return ListenableBuilder(
|
|
||||||
listenable: call,
|
|
||||||
builder: (context, _) {
|
|
||||||
return Center(
|
|
||||||
child: Container(
|
|
||||||
constraints: const BoxConstraints(maxWidth: 320),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text('callMicrophone').tr(),
|
|
||||||
Switch(
|
|
||||||
value: call.enableAudio,
|
|
||||||
onChanged: null,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padding(bottom: 5),
|
|
||||||
DropdownButtonHideUnderline(
|
|
||||||
child: DropdownButton2<MediaDevice>(
|
|
||||||
isExpanded: true,
|
|
||||||
disabledHint: Text('callMicrophoneDisabled').tr(),
|
|
||||||
hint: Text('callMicrophoneSelect').tr(),
|
|
||||||
items: call.enableAudio
|
|
||||||
? call.audioInputs
|
|
||||||
.map(
|
|
||||||
(item) => DropdownMenuItem<MediaDevice>(
|
|
||||||
value: item,
|
|
||||||
child: Text(item.label),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList()
|
|
||||||
.cast<DropdownMenuItem<MediaDevice>>()
|
|
||||||
: [],
|
|
||||||
value: call.audioDevice,
|
|
||||||
onChanged: (MediaDevice? value) async {
|
|
||||||
if (value != null) {
|
|
||||||
call.setAudioDevice(value);
|
|
||||||
await call.changeLocalAudioTrack();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
buttonStyleData: const ButtonStyleData(
|
|
||||||
height: 40,
|
|
||||||
width: 320,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).padding(bottom: 25),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text('callCamera').tr(),
|
|
||||||
Switch(
|
|
||||||
value: call.enableVideo,
|
|
||||||
onChanged: call.setEnableVideo,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padding(bottom: 5),
|
|
||||||
DropdownButtonHideUnderline(
|
|
||||||
child: DropdownButton2<MediaDevice>(
|
|
||||||
isExpanded: true,
|
|
||||||
disabledHint: Text('callCameraDisabled').tr(),
|
|
||||||
hint: Text('callCameraSelect').tr(),
|
|
||||||
items: call.enableVideo
|
|
||||||
? call.videoInputs
|
|
||||||
.map(
|
|
||||||
(item) => DropdownMenuItem<MediaDevice>(
|
|
||||||
value: item,
|
|
||||||
child: Text(item.label),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList()
|
|
||||||
.cast<DropdownMenuItem<MediaDevice>>()
|
|
||||||
: [],
|
|
||||||
value: call.videoDevice,
|
|
||||||
onChanged: (MediaDevice? value) async {
|
|
||||||
if (value != null) {
|
|
||||||
call.setVideoDevice(value);
|
|
||||||
await call.changeLocalVideoTrack();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
buttonStyleData: const ButtonStyleData(
|
|
||||||
height: 40,
|
|
||||||
width: 320,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).padding(bottom: 25),
|
|
||||||
if (_isBusy)
|
|
||||||
const Center(child: CircularProgressIndicator())
|
|
||||||
else
|
|
||||||
ElevatedButton(
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
minimumSize: const Size(320, 56),
|
|
||||||
),
|
|
||||||
onPressed: _isBusy ? null : _performJoin,
|
|
||||||
child: Text('callJoin').tr(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_call
|
|
||||||
..deactivateHardware()
|
|
||||||
..disposeHardware();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
@ -13,7 +13,6 @@
|
|||||||
#include <file_selector_linux/file_selector_plugin.h>
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
#include <flutter_timezone/flutter_timezone_plugin.h>
|
#include <flutter_timezone/flutter_timezone_plugin.h>
|
||||||
#include <flutter_udid/flutter_udid_plugin.h>
|
#include <flutter_udid/flutter_udid_plugin.h>
|
||||||
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
|
||||||
#include <hotkey_manager_linux/hotkey_manager_linux_plugin.h>
|
#include <hotkey_manager_linux/hotkey_manager_linux_plugin.h>
|
||||||
#include <local_notifier/local_notifier_plugin.h>
|
#include <local_notifier/local_notifier_plugin.h>
|
||||||
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
||||||
@ -45,9 +44,6 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
|||||||
g_autoptr(FlPluginRegistrar) flutter_udid_registrar =
|
g_autoptr(FlPluginRegistrar) flutter_udid_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterUdidPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterUdidPlugin");
|
||||||
flutter_udid_plugin_register_with_registrar(flutter_udid_registrar);
|
flutter_udid_plugin_register_with_registrar(flutter_udid_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar =
|
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin");
|
|
||||||
flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar);
|
|
||||||
g_autoptr(FlPluginRegistrar) hotkey_manager_linux_registrar =
|
g_autoptr(FlPluginRegistrar) hotkey_manager_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "HotkeyManagerLinuxPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "HotkeyManagerLinuxPlugin");
|
||||||
hotkey_manager_linux_plugin_register_with_registrar(hotkey_manager_linux_registrar);
|
hotkey_manager_linux_plugin_register_with_registrar(hotkey_manager_linux_registrar);
|
||||||
|
@ -10,7 +10,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
file_selector_linux
|
file_selector_linux
|
||||||
flutter_timezone
|
flutter_timezone
|
||||||
flutter_udid
|
flutter_udid
|
||||||
flutter_webrtc
|
|
||||||
hotkey_manager_linux
|
hotkey_manager_linux
|
||||||
local_notifier
|
local_notifier
|
||||||
media_kit_libs_linux
|
media_kit_libs_linux
|
||||||
|
@ -19,12 +19,9 @@ import firebase_messaging
|
|||||||
import flutter_inappwebview_macos
|
import flutter_inappwebview_macos
|
||||||
import flutter_timezone
|
import flutter_timezone
|
||||||
import flutter_udid
|
import flutter_udid
|
||||||
import flutter_webrtc
|
|
||||||
import gal
|
import gal
|
||||||
import hotkey_manager_macos
|
import hotkey_manager_macos
|
||||||
import in_app_review
|
import in_app_review
|
||||||
import livekit_client
|
|
||||||
import livekit_noise_filter
|
|
||||||
import local_notifier
|
import local_notifier
|
||||||
import media_kit_libs_macos_video
|
import media_kit_libs_macos_video
|
||||||
import media_kit_video
|
import media_kit_video
|
||||||
@ -56,12 +53,9 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
|
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
|
||||||
FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin"))
|
FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin"))
|
||||||
FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin"))
|
FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin"))
|
||||||
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
|
|
||||||
GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
|
GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
|
||||||
HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin"))
|
HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin"))
|
||||||
InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin"))
|
InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin"))
|
||||||
LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin"))
|
|
||||||
LiveKitKrispNoiseFilterPlugin.register(with: registry.registrar(forPlugin: "LiveKitKrispNoiseFilterPlugin"))
|
|
||||||
LocalNotifierPlugin.register(with: registry.registrar(forPlugin: "LocalNotifierPlugin"))
|
LocalNotifierPlugin.register(with: registry.registrar(forPlugin: "LocalNotifierPlugin"))
|
||||||
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
|
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
|
||||||
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
|
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
|
||||||
|
76
pubspec.lock
76
pubspec.lock
@ -417,14 +417,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.0.1"
|
||||||
dart_webrtc:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: dart_webrtc
|
|
||||||
sha256: "8565f1f1f412b8a6fd862f3a157560811e61eeeac26741c735a5d2ff409a0202"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.5.3"
|
|
||||||
dbus:
|
dbus:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -911,10 +903,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_markdown
|
name: flutter_markdown
|
||||||
sha256: e7bbc718adc9476aa14cfddc1ef048d2e21e4e8f18311aaac723266db9f9e7b5
|
sha256: "634622a3a826d67cb05c0e3e576d1812c430fa98404e95b60b131775c73d76ec"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.6+2"
|
version: "0.7.7"
|
||||||
flutter_markdown_latex:
|
flutter_markdown_latex:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -997,22 +989,14 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
flutter_webrtc:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: flutter_webrtc
|
|
||||||
sha256: "4f0d6e248f178e617f249b6a2f432b5981e3300c2896fc8d476fc2aa1f525547"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.13.1"
|
|
||||||
freezed:
|
freezed:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: freezed
|
name: freezed
|
||||||
sha256: "7ed2ddaa47524976d5f2aa91432a79da36a76969edd84170777ac5bea82d797c"
|
sha256: "19e64d719a9f0d2e7f74a2f59624acee0e96b3e897ecf72edcae52ccc36a424f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.4"
|
version: "3.0.5"
|
||||||
freezed_annotation:
|
freezed_annotation:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1293,6 +1277,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.5"
|
version: "1.0.5"
|
||||||
|
jitsi_meet_flutter_sdk:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: jitsi_meet_flutter_sdk
|
||||||
|
sha256: ad72f7ae8db1508c944a7a7f135c4cccdc676efb31ab7617b9f5b0dac4791ccd
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "11.1.1"
|
||||||
js:
|
js:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1373,22 +1365,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.1"
|
version: "1.0.1"
|
||||||
livekit_client:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: livekit_client
|
|
||||||
sha256: caff013563dc034b9858380318dd341c8bab453fc1a033405c3ab8677d91225c
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.4.2+hotfix.1"
|
|
||||||
livekit_noise_filter:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: livekit_noise_filter
|
|
||||||
sha256: "667fd572bc45f18f09cf9764b6d323ee816905fd3afaf40e1e701ea2de8fd567"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.1.0+hotfix.1"
|
|
||||||
local_notifier:
|
local_notifier:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1797,14 +1773,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
protobuf:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: protobuf
|
|
||||||
sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.1.0"
|
|
||||||
provider:
|
provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1917,14 +1885,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "3.0.0"
|
||||||
sdp_transform:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: sdp_transform
|
|
||||||
sha256: "73e412a5279a5c2de74001535208e20fff88f225c9a4571af0f7146202755e45"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.3.2"
|
|
||||||
share_plus:
|
share_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -2342,10 +2302,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_ios
|
name: url_launcher_ios
|
||||||
sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626"
|
sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.3.2"
|
version: "6.3.3"
|
||||||
url_launcher_linux:
|
url_launcher_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -2514,14 +2474,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.2"
|
version: "3.0.2"
|
||||||
webrtc_interface:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: webrtc_interface
|
|
||||||
sha256: e92afec11152a9ccb5c9f35482754edd99696e886ab6acaf90c06dd2d09f09eb
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.2.2+hotfix.1"
|
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -82,8 +82,6 @@ dependencies:
|
|||||||
media_kit_libs_video: ^1.0.5
|
media_kit_libs_video: ^1.0.5
|
||||||
pasteboard: ^0.3.0
|
pasteboard: ^0.3.0
|
||||||
synchronized: ^3.3.0+3
|
synchronized: ^3.3.0+3
|
||||||
dart_webrtc: ^1.4.10
|
|
||||||
livekit_client: ^2.3.1+hotfix.1
|
|
||||||
wakelock_plus: ^1.2.8
|
wakelock_plus: ^1.2.8
|
||||||
permission_handler: ^11.3.1
|
permission_handler: ^11.3.1
|
||||||
flutter_staggered_grid_view: ^0.7.0
|
flutter_staggered_grid_view: ^0.7.0
|
||||||
@ -113,7 +111,6 @@ dependencies:
|
|||||||
version: ^3.0.2
|
version: ^3.0.2
|
||||||
flutter_colorpicker: ^1.1.0
|
flutter_colorpicker: ^1.1.0
|
||||||
fl_chart: ^0.70.0
|
fl_chart: ^0.70.0
|
||||||
flutter_webrtc: ^0.13.1
|
|
||||||
slide_countdown: ^2.0.2
|
slide_countdown: ^2.0.2
|
||||||
video_compress: ^3.1.3
|
video_compress: ^3.1.3
|
||||||
cached_network_image: ^3.4.1
|
cached_network_image: ^3.4.1
|
||||||
@ -144,7 +141,7 @@ dependencies:
|
|||||||
latlong2: ^0.9.1
|
latlong2: ^0.9.1
|
||||||
crypto: ^3.0.6
|
crypto: ^3.0.6
|
||||||
audioplayers: ^6.4.0
|
audioplayers: ^6.4.0
|
||||||
livekit_noise_filter: ^0.1.0
|
jitsi_meet_flutter_sdk: ^11.1.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
@ -16,10 +16,8 @@
|
|||||||
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
|
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
|
||||||
#include <flutter_timezone/flutter_timezone_plugin_c_api.h>
|
#include <flutter_timezone/flutter_timezone_plugin_c_api.h>
|
||||||
#include <flutter_udid/flutter_udid_plugin_c_api.h>
|
#include <flutter_udid/flutter_udid_plugin_c_api.h>
|
||||||
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
|
||||||
#include <gal/gal_plugin_c_api.h>
|
#include <gal/gal_plugin_c_api.h>
|
||||||
#include <hotkey_manager_windows/hotkey_manager_windows_plugin_c_api.h>
|
#include <hotkey_manager_windows/hotkey_manager_windows_plugin_c_api.h>
|
||||||
#include <livekit_client/live_kit_plugin.h>
|
|
||||||
#include <local_notifier/local_notifier_plugin.h>
|
#include <local_notifier/local_notifier_plugin.h>
|
||||||
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.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>
|
#include <media_kit_video/media_kit_video_plugin_c_api.h>
|
||||||
@ -52,14 +50,10 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||||||
registry->GetRegistrarForPlugin("FlutterTimezonePluginCApi"));
|
registry->GetRegistrarForPlugin("FlutterTimezonePluginCApi"));
|
||||||
FlutterUdidPluginCApiRegisterWithRegistrar(
|
FlutterUdidPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FlutterUdidPluginCApi"));
|
registry->GetRegistrarForPlugin("FlutterUdidPluginCApi"));
|
||||||
FlutterWebRTCPluginRegisterWithRegistrar(
|
|
||||||
registry->GetRegistrarForPlugin("FlutterWebRTCPlugin"));
|
|
||||||
GalPluginCApiRegisterWithRegistrar(
|
GalPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("GalPluginCApi"));
|
registry->GetRegistrarForPlugin("GalPluginCApi"));
|
||||||
HotkeyManagerWindowsPluginCApiRegisterWithRegistrar(
|
HotkeyManagerWindowsPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("HotkeyManagerWindowsPluginCApi"));
|
registry->GetRegistrarForPlugin("HotkeyManagerWindowsPluginCApi"));
|
||||||
LiveKitPluginRegisterWithRegistrar(
|
|
||||||
registry->GetRegistrarForPlugin("LiveKitPlugin"));
|
|
||||||
LocalNotifierPluginRegisterWithRegistrar(
|
LocalNotifierPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("LocalNotifierPlugin"));
|
registry->GetRegistrarForPlugin("LocalNotifierPlugin"));
|
||||||
MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar(
|
MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar(
|
||||||
|
@ -13,10 +13,8 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
flutter_inappwebview_windows
|
flutter_inappwebview_windows
|
||||||
flutter_timezone
|
flutter_timezone
|
||||||
flutter_udid
|
flutter_udid
|
||||||
flutter_webrtc
|
|
||||||
gal
|
gal
|
||||||
hotkey_manager_windows
|
hotkey_manager_windows
|
||||||
livekit_client
|
|
||||||
local_notifier
|
local_notifier
|
||||||
media_kit_libs_windows_video
|
media_kit_libs_windows_video
|
||||||
media_kit_video
|
media_kit_video
|
||||||
|
Loading…
x
Reference in New Issue
Block a user