diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index bdcd15c..354b262 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -15,6 +15,7 @@
8.0)
@@ -184,16 +183,26 @@ PODS:
- Flutter
- in_app_review (2.0.0):
- 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)
- - livekit_client (2.4.2):
- - Flutter
- - flutter_webrtc
- - WebRTC-SDK (= 125.6422.06)
- - livekit_noise_filter (0.0.1):
- - Flutter
- - flutter_webrtc
- - LiveKitKrispNoiseFilter (= 0.0.7)
- - LiveKitKrispNoiseFilter (0.0.7)
+ - libwebp (1.5.0):
+ - libwebp/demux (= 1.5.0)
+ - libwebp/mux (= 1.5.0)
+ - libwebp/sharpyuv (= 1.5.0)
+ - libwebp/webp (= 1.5.0)
+ - libwebp/demux (1.5.0):
+ - libwebp/webp
+ - libwebp/mux (1.5.0):
+ - libwebp/demux
+ - libwebp/sharpyuv (1.5.0)
+ - libwebp/webp (1.5.0):
+ - libwebp/sharpyuv
- media_kit_libs_ios_video (1.0.4):
- Flutter
- media_kit_video (0.0.1):
@@ -259,7 +268,6 @@ PODS:
- Flutter
- wakelock_plus (0.0.1):
- Flutter
- - WebRTC-SDK (125.6422.06)
- workmanager (0.0.1):
- Flutter
@@ -281,14 +289,12 @@ DEPENDENCIES:
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`)
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
- - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
- gal (from `.symlinks/plugins/gal/darwin`)
- home_widget (from `.symlinks/plugins/home_widget/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/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)
- - 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_video (from `.symlinks/plugins/media_kit_video/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
@@ -317,11 +323,14 @@ SPEC REPOS:
- FirebaseCoreInternal
- FirebaseInstallations
- FirebaseMessaging
+ - Giphy
- GoogleAppMeasurement
- GoogleDataTransport
- GoogleUtilities
+ - JitsiMeetSDK
+ - JitsiWebRTC
- Kingfisher
- - LiveKitKrispNoiseFilter
+ - libwebp
- nanopb
- OrderedSet
- PromisesObjC
@@ -329,7 +338,6 @@ SPEC REPOS:
- SDWebImage
- sqlite3
- SwiftyGif
- - WebRTC-SDK
EXTERNAL SOURCES:
audioplayers_darwin:
@@ -364,8 +372,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_timezone/ios"
flutter_udid:
:path: ".symlinks/plugins/flutter_udid/ios"
- flutter_webrtc:
- :path: ".symlinks/plugins/flutter_webrtc/ios"
gal:
:path: ".symlinks/plugins/gal/darwin"
home_widget:
@@ -374,10 +380,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/image_picker_ios/ios"
in_app_review:
:path: ".symlinks/plugins/in_app_review/ios"
- livekit_client:
- :path: ".symlinks/plugins/livekit_client/ios"
- livekit_noise_filter:
- :path: ".symlinks/plugins/livekit_noise_filter/ios"
+ jitsi_meet_flutter_sdk:
+ :path: ".symlinks/plugins/jitsi_meet_flutter_sdk/ios"
media_kit_libs_ios_video:
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
media_kit_video:
@@ -437,18 +441,19 @@ SPEC CHECKSUMS:
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
- flutter_webrtc: 57f32415b8744e806f9c2a96ccdb60c6a627ba33
gal: baecd024ebfd13c441269ca7404792a7152fde89
+ Giphy: 83628960ed04e1c3428ff1b4fb2b027f65e82f50
GoogleAppMeasurement: 36684bfb3ee034e2b42b4321eb19da3a1b81e65d
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
in_app_review: 5596fe56fab799e8edb3561c03d053363ab13457
+ jitsi_meet_flutter_sdk: 0283a60730922d608fbad9872e07afdd5bb3578a
+ JitsiMeetSDK: 4e1c269aaaed8f2cb7b0fff2d3c00f08359b170e
+ JitsiWebRTC: b47805ab5668be38e7ee60e2258f49badfe8e1d0
Kingfisher: 3204d23de16b5ea53541c44ca5a8efb55741dec3
- livekit_client: 78bb2ff0d409268886804151d4fc9e006093e6ce
- livekit_noise_filter: a26aeb1c1eae6db0a023fd2f6ea3ff108c3ecbb0
- LiveKitKrispNoiseFilter: efe418ceca28163ace0ff222bd2cc02384645d84
+ libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
@@ -471,9 +476,8 @@ SPEC CHECKSUMS:
video_compress: f2133a07762889d67f0711ac831faa26f956980e
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49
- WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db
workmanager: 01be2de7f184bd15de93a1812936a2b7f42ef07e
-PODFILE CHECKSUM: 9b244e02f87527430136c8d21cbdcf1cd586b6bc
+PODFILE CHECKSUM: d278ce52a331dda323590121247d2046cd085ae7
COCOAPODS: 1.16.2
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index b006d1b..7258acb 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -961,7 +961,7 @@
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
- IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -1521,7 +1521,7 @@
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
- IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -1549,7 +1549,7 @@
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
- IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
diff --git a/lib/main.dart b/lib/main.dart
index ba55b0e..16aecc9 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -26,7 +26,6 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:surface/firebase_options.dart';
import 'package:surface/logger.dart';
import 'package:surface/providers/channel.dart';
-import 'package:surface/providers/chat_call.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/database.dart';
import 'package:surface/providers/keypair.dart';
@@ -198,7 +197,6 @@ class SolianApp extends StatelessWidget {
Provider(create: (ctx) => KeyPairProvider(ctx)),
ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)),
ChangeNotifierProvider(create: (ctx) => ChatChannelProvider(ctx)),
- ChangeNotifierProvider(create: (ctx) => ChatCallProvider(ctx)),
Provider(create: (ctx) => SnTranslator()),
// Additional helper layer
diff --git a/lib/providers/chat_call.dart b/lib/providers/chat_call.dart
deleted file mode 100644
index 31d5dc9..0000000
--- a/lib/providers/chat_call.dart
+++ /dev/null
@@ -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();
- }
-
- 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 _audioInputs = [];
- List _videoInputs = [];
-
- bool _enableAudio = true;
- bool _enableVideo = false;
- LocalAudioTrack? _audioTrack;
- LocalVideoTrack? _videoTrack;
- MediaDevice? _videoDevice;
- MediaDevice? _audioDevice;
-
- late Room _room;
- late EventsListener _listener;
-
- List _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 get audioInputs => _audioInputs;
- List 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 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 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 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 setEnableAudio(bool value) async {
- _enableAudio = value;
- if (!_enableAudio) {
- await _audioTrack?.stop();
- _audioTrack = null;
- } else {
- await _changeLocalAudioTrack();
- }
- notifyListeners();
- }
-
- Future 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((event) async {
- onDisconnected(event.reason);
- })
- ..on((event) => sortParticipants())
- ..on((_) => sortParticipants())
- ..on((_) => sortParticipants())
- ..on((_) => sortParticipants())
- ..on((_) => sortParticipants())
- ..on((event) {
- sortParticipants();
- });
- }
-
- void sortParticipants() {
- Map 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.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 _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 _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 devices) {
- _audioInputs = devices.where((d) => d.kind == 'audioinput').toList();
- _videoInputs = devices.where((d) => d.kind == 'videoinput').toList();
- notifyListeners();
- }
-
- void _onRoomDidUpdate() => sortParticipants();
-
- Future 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 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();
- }
-}
diff --git a/lib/router.dart b/lib/router.dart
index ecc7f2c..378b0d3 100644
--- a/lib/router.dart
+++ b/lib/router.dart
@@ -22,7 +22,6 @@ import 'package:surface/screens/album.dart';
import 'package:surface/screens/auth/login.dart';
import 'package:surface/screens/auth/register.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/manage.dart';
import 'package:surface/screens/chat/room.dart';
@@ -264,14 +263,6 @@ final _appRoutes = [
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(
path: '/:scope/:alias/detail',
name: 'channelDetail',
diff --git a/lib/screens/chat/call_room.dart b/lib/screens/chat/call_room.dart
deleted file mode 100644
index 192a8a9..0000000
--- a/lib/screens/chat/call_room.dart
+++ /dev/null
@@ -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 createState() => _CallRoomScreenState();
-}
-
-class _CallRoomScreenState extends State {
- int _layoutMode = 0;
-
- void _switchLayout() {
- if (_layoutMode < 1) {
- setState(() => _layoutMode++);
- } else {
- setState(() => _layoutMode = 0);
- }
- }
-
- Widget _buildMeetLayout() {
- final call = context.read();
- 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();
-
- 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();
-
- Future.delayed(Duration.zero, () {
- call
- ..setupRoom()
- ..enableDurationUpdater();
- });
- }
-
- @override
- Widget build(BuildContext context) {
- final call = context.read();
-
- 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();
- 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();
- call.disableDurationUpdater();
- super.deactivate();
- }
-
- @override
- void activate() {
- final call = context.read();
- call.enableDurationUpdater();
- super.activate();
- }
-}
diff --git a/lib/screens/chat/room.dart b/lib/screens/chat/room.dart
index 6330384..853d0d1 100644
--- a/lib/screens/chat/room.dart
+++ b/lib/screens/chat/room.dart
@@ -2,18 +2,17 @@ import 'dart:async';
import 'dart:convert';
import 'dart:developer';
-import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.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:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/controllers/chat_message_controller.dart';
import 'package:surface/controllers/post_write_controller.dart';
import 'package:surface/providers/channel.dart';
-import 'package:surface/providers/chat_call.dart';
import 'package:surface/providers/notification.dart';
import 'package:surface/providers/sn_network.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/types/chat.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_input.dart';
import 'package:surface/widgets/chat/chat_typing_indicator.dart';
@@ -51,13 +49,11 @@ class ChatRoomScreen extends StatefulWidget {
class _ChatRoomScreenState extends State {
bool _isBusy = false;
- bool _isCalling = false;
bool _isJoining = false;
SnChannel? _channel;
SnChannelMember? _currentMember;
SnChannelMember? _otherMember;
- SnChatCall? _ongoingCall;
final GlobalKey _inputGlobalKey = GlobalKey();
late final ChatMessageController _messageController;
@@ -139,88 +135,25 @@ class _ChatRoomScreenState extends State {
}
}
- Future _fetchOngoingCall() async {
- setState(() => _isCalling = true);
-
- try {
- final sn = context.read();
- 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 _makeCall() async {
- setState(() => _isCalling = true);
-
- try {
- final sn = context.read();
- 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 _endCall() async {
- setState(() => _isCalling = true);
-
- try {
- final sn = context.read();
- 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 _onCallJoin() async {
- await showModalBottomSheet(
- context: context,
- builder: (context) => ChatCallPrejoinPopup(
- ongoingCall: _ongoingCall!,
- channel: _channel!,
- onJoin: _onCallResume,
+ final sn = context.read();
+ final ua = context.read();
+ final meet = JitsiMeet();
+ final confOpts = JitsiMeetConferenceOptions(
+ room: 'sn-chat-${_channel!.id}',
+ 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,
),
);
- }
-
- void _onCallResume() {
- GoRouter.of(context).pushNamed(
- 'chatCallRoom',
- pathParameters: {
- 'scope': _channel!.realm?.alias ?? 'global',
- 'alias': _channel!.alias,
- },
- );
+ meet.join(confOpts);
}
bool _checkMessageMergeable(SnChatMessage? a, SnChatMessage? b) {
@@ -248,10 +181,7 @@ class _ChatRoomScreenState extends State {
});
}
- await Future.wait([
- _messageController.checkUpdate(),
- _fetchOngoingCall(),
- ]);
+ await _messageController.checkUpdate();
});
}
@@ -260,23 +190,6 @@ class _ChatRoomScreenState extends State {
super.initState();
_messageController = ChatMessageController(context);
_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
@@ -300,7 +213,6 @@ class _ChatRoomScreenState extends State {
@override
Widget build(BuildContext context) {
- final call = context.watch();
final ud = context.read();
return AppScaffold(
@@ -324,14 +236,8 @@ class _ChatRoomScreenState extends State {
),
if (_currentMember != null)
IconButton(
- icon: _ongoingCall == null
- ? const Icon(Symbols.call)
- : const Icon(Symbols.call_end),
- onPressed: _isCalling
- ? null
- : _ongoingCall == null
- ? _makeCall
- : _endCall,
+ icon: const Icon(Symbols.video_call),
+ onPressed: _onCallJoin,
),
IconButton(
icon: const Icon(Symbols.more_vert),
@@ -359,28 +265,6 @@ class _ChatRoomScreenState extends State {
LoadingIndicator(
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)
Expanded(
child: Center(
diff --git a/lib/types/chat.dart b/lib/types/chat.dart
index 5b8e9fa..7ccfd5e 100644
--- a/lib/types/chat.dart
+++ b/lib/types/chat.dart
@@ -1,5 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart';
-import 'package:livekit_client/livekit_client.dart';
import 'package:surface/types/account.dart';
import 'package:surface/types/attachment.dart';
import 'package:surface/types/realm.dart';
@@ -116,24 +115,3 @@ abstract class SnChatCall with _$SnChatCall {
factory SnChatCall.fromJson(Map 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;
-}
diff --git a/lib/widgets/chat/call/call_controls.dart b/lib/widgets/chat/call/call_controls.dart
deleted file mode 100644
index 88b34e1..0000000
--- a/lib/widgets/chat/call/call_controls.dart
+++ /dev/null
@@ -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 createState() => _ControlsWidgetState();
-}
-
-class _ControlsWidgetState extends State {
- CameraPosition _position = CameraPosition.front;
-
- List? _audioInputs;
- List? _audioOutputs;
- List? _videoInputs;
-
- StreamSubscription? _subscription;
-
- bool _speakerphoneOn = false;
-
- @override
- void initState() {
- super.initState();
- _participant.addListener(onChange);
- _subscription = Hardware.instance.onDeviceChange.stream
- .listen((List 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 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 showDisconnectDialog() {
- return showDialog(
- 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();
- 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(
- 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(
- icon: const Icon(Symbols.settings_voice),
- itemBuilder: (BuildContext context) {
- return [
- PopupMenuItem(
- 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(
- 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(
- icon: const Icon(Symbols.videocam_sharp),
- itemBuilder: (BuildContext context) {
- return [
- PopupMenuItem(
- value: null,
- onTap: _disableVideo,
- child: ListTile(
- leading: const Icon(Symbols.videocam_off),
- title: Text('callCameraOff'.tr()),
- ),
- ),
- if (_videoInputs != null)
- ..._videoInputs!.map((device) {
- return PopupMenuItem(
- 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(
- icon: const Icon(Symbols.volume_up),
- itemBuilder: (BuildContext context) {
- return [
- PopupMenuItem(
- value: null,
- child: ListTile(
- leading: const Icon(Symbols.speaker),
- title: Text('callSpeakerSelect').tr(),
- ),
- ),
- if (_audioOutputs != null)
- ..._audioOutputs!.map((device) {
- return PopupMenuItem(
- 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(),
- ),
- ],
- ),
- );
- }
-}
diff --git a/lib/widgets/chat/call/call_no_content.dart b/lib/widgets/chat/call/call_no_content.dart
deleted file mode 100644
index 9b95756..0000000
--- a/lib/widgets/chat/call/call_no_content.dart
+++ /dev/null
@@ -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 createState() => _NoContentWidgetState();
-}
-
-class _NoContentWidgetState extends State
- 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();
- }
-}
diff --git a/lib/widgets/chat/call/call_participant.dart b/lib/widgets/chat/call/call_participant.dart
deleted file mode 100644
index b15c3a7..0000000
--- a/lib/widgets/chat/call/call_participant.dart
+++ /dev/null
@@ -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 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 createState() => _RemoteParticipantWidgetState();
-}
-
-abstract class _ParticipantWidgetState
- extends State {
- 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 {
- @override
- LocalTrackPublication? get _firstAudioPublication =>
- widget.participant.audioTrackPublications.firstOrNull;
-
- @override
- VideoTrack? get _activeVideoTrack => widget.videoTrack;
-}
-
-class _RemoteParticipantWidgetState
- extends _ParticipantWidgetState {
- @override
- RemoteTrackPublication? 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,
- ),
- ),
- ),
- );
- }
-}
diff --git a/lib/widgets/chat/call/call_participant_info.dart b/lib/widgets/chat/call/call_participant_info.dart
deleted file mode 100644
index 15dbead..0000000
--- a/lib/widgets/chat/call/call_participant_info.dart
+++ /dev/null
@@ -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),
- ],
- ),
- );
- }
-}
diff --git a/lib/widgets/chat/call/call_participant_menu.dart b/lib/widgets/chat/call/call_participant_menu.dart
deleted file mode 100644
index bfe62d5..0000000
--- a/lib/widgets/chat/call/call_participant_menu.dart
+++ /dev/null
@@ -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 createState() => _ParticipantMenuState();
-}
-
-class _ParticipantMenuState extends State {
- RemoteTrackPublication? get _videoPublication =>
- widget.participant.videoTrackPublications
- .where((element) => element.sid == widget.videoTrack?.sid)
- .firstOrNull;
-
- RemoteTrackPublication? 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();
- },
- ),
- ),
- ],
- ),
- ),
- ],
- );
- }
-}
diff --git a/lib/widgets/chat/call/call_participant_stats.dart b/lib/widgets/chat/call/call_participant_stats.dart
deleted file mode 100644
index 7c9629d..0000000
--- a/lib/widgets/chat/call/call_participant_stats.dart
+++ /dev/null
@@ -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 createState() => _ParticipantStatsWidgetState();
-}
-
-class _ParticipantStatsWidgetState extends State {
- List> listeners = [];
- ParticipantStatsType statsType = ParticipantStatsType.unknown;
- Map stats = {};
-
- void _setUpListener(Track track) {
- var listener = track.createListener();
- listeners.add(listener);
- if (track is LocalVideoTrack) {
- statsType = ParticipantStatsType.localVideoSender;
- listener.on((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((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((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((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(),
- ),
- );
- }
-}
diff --git a/lib/widgets/chat/call/call_prejoin.dart b/lib/widgets/chat/call/call_prejoin.dart
deleted file mode 100644
index 68109e8..0000000
--- a/lib/widgets/chat/call/call_prejoin.dart
+++ /dev/null
@@ -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 createState() => _ChatCallPrejoinPopupState();
-}
-
-class _ChatCallPrejoinPopupState extends State {
- bool _isBusy = false;
-
- late final ChatCallProvider _call = context.read();
-
- 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();
- call.checkPermissions().then((_) {
- call.initHardware();
- });
-
- super.initState();
- }
-
- @override
- Widget build(BuildContext context) {
- final call = context.read();
- 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(
- isExpanded: true,
- disabledHint: Text('callMicrophoneDisabled').tr(),
- hint: Text('callMicrophoneSelect').tr(),
- items: call.enableAudio
- ? call.audioInputs
- .map(
- (item) => DropdownMenuItem(
- value: item,
- child: Text(item.label),
- ),
- )
- .toList()
- .cast>()
- : [],
- 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(
- isExpanded: true,
- disabledHint: Text('callCameraDisabled').tr(),
- hint: Text('callCameraSelect').tr(),
- items: call.enableVideo
- ? call.videoInputs
- .map(
- (item) => DropdownMenuItem(
- value: item,
- child: Text(item.label),
- ),
- )
- .toList()
- .cast>()
- : [],
- 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();
- }
-}
diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc
index 4e06f15..6e42b25 100644
--- a/linux/flutter/generated_plugin_registrant.cc
+++ b/linux/flutter/generated_plugin_registrant.cc
@@ -13,7 +13,6 @@
#include
#include
#include
-#include
#include
#include
#include
@@ -45,9 +44,6 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) flutter_udid_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterUdidPlugin");
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 =
fl_plugin_registry_get_registrar_for_plugin(registry, "HotkeyManagerLinuxPlugin");
hotkey_manager_linux_plugin_register_with_registrar(hotkey_manager_linux_registrar);
diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake
index ced48cb..00c9eac 100644
--- a/linux/flutter/generated_plugins.cmake
+++ b/linux/flutter/generated_plugins.cmake
@@ -10,7 +10,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
flutter_timezone
flutter_udid
- flutter_webrtc
hotkey_manager_linux
local_notifier
media_kit_libs_linux
diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift
index ba801f9..152737a 100644
--- a/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -19,12 +19,9 @@ import firebase_messaging
import flutter_inappwebview_macos
import flutter_timezone
import flutter_udid
-import flutter_webrtc
import gal
import hotkey_manager_macos
import in_app_review
-import livekit_client
-import livekit_noise_filter
import local_notifier
import media_kit_libs_macos_video
import media_kit_video
@@ -56,12 +53,9 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin"))
FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin"))
- FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin"))
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"))
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
diff --git a/pubspec.lock b/pubspec.lock
index c671507..38cf73e 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -417,14 +417,6 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@@ -911,10 +903,10 @@ packages:
dependency: "direct main"
description:
name: flutter_markdown
- sha256: e7bbc718adc9476aa14cfddc1ef048d2e21e4e8f18311aaac723266db9f9e7b5
+ sha256: "634622a3a826d67cb05c0e3e576d1812c430fa98404e95b60b131775c73d76ec"
url: "https://pub.dev"
source: hosted
- version: "0.7.6+2"
+ version: "0.7.7"
flutter_markdown_latex:
dependency: "direct main"
description:
@@ -997,22 +989,14 @@ packages:
description: flutter
source: sdk
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:
dependency: "direct dev"
description:
name: freezed
- sha256: "7ed2ddaa47524976d5f2aa91432a79da36a76969edd84170777ac5bea82d797c"
+ sha256: "19e64d719a9f0d2e7f74a2f59624acee0e96b3e897ecf72edcae52ccc36a424f"
url: "https://pub.dev"
source: hosted
- version: "3.0.4"
+ version: "3.0.5"
freezed_annotation:
dependency: "direct main"
description:
@@ -1293,6 +1277,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@@ -1373,22 +1365,6 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct main"
description:
@@ -1797,14 +1773,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.0"
- protobuf:
- dependency: transitive
- description:
- name: protobuf
- sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d"
- url: "https://pub.dev"
- source: hosted
- version: "3.1.0"
provider:
dependency: "direct main"
description:
@@ -1917,14 +1885,6 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct main"
description:
@@ -2342,10 +2302,10 @@ packages:
dependency: transitive
description:
name: url_launcher_ios
- sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626"
+ sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb"
url: "https://pub.dev"
source: hosted
- version: "6.3.2"
+ version: "6.3.3"
url_launcher_linux:
dependency: transitive
description:
@@ -2514,14 +2474,6 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 1b109e3..1e2d613 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -82,8 +82,6 @@ dependencies:
media_kit_libs_video: ^1.0.5
pasteboard: ^0.3.0
synchronized: ^3.3.0+3
- dart_webrtc: ^1.4.10
- livekit_client: ^2.3.1+hotfix.1
wakelock_plus: ^1.2.8
permission_handler: ^11.3.1
flutter_staggered_grid_view: ^0.7.0
@@ -113,7 +111,6 @@ dependencies:
version: ^3.0.2
flutter_colorpicker: ^1.1.0
fl_chart: ^0.70.0
- flutter_webrtc: ^0.13.1
slide_countdown: ^2.0.2
video_compress: ^3.1.3
cached_network_image: ^3.4.1
@@ -144,7 +141,7 @@ dependencies:
latlong2: ^0.9.1
crypto: ^3.0.6
audioplayers: ^6.4.0
- livekit_noise_filter: ^0.1.0
+ jitsi_meet_flutter_sdk: ^11.1.1
dev_dependencies:
flutter_test:
diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc
index fc46ce6..84a26fa 100644
--- a/windows/flutter/generated_plugin_registrant.cc
+++ b/windows/flutter/generated_plugin_registrant.cc
@@ -16,10 +16,8 @@
#include
#include
#include
-#include
#include
#include
-#include
#include
#include
#include
@@ -52,14 +50,10 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FlutterTimezonePluginCApi"));
FlutterUdidPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterUdidPluginCApi"));
- FlutterWebRTCPluginRegisterWithRegistrar(
- registry->GetRegistrarForPlugin("FlutterWebRTCPlugin"));
GalPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("GalPluginCApi"));
HotkeyManagerWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("HotkeyManagerWindowsPluginCApi"));
- LiveKitPluginRegisterWithRegistrar(
- registry->GetRegistrarForPlugin("LiveKitPlugin"));
LocalNotifierPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("LocalNotifierPlugin"));
MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar(
diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake
index fa2bb59..89d9714 100644
--- a/windows/flutter/generated_plugins.cmake
+++ b/windows/flutter/generated_plugins.cmake
@@ -13,10 +13,8 @@ list(APPEND FLUTTER_PLUGIN_LIST
flutter_inappwebview_windows
flutter_timezone
flutter_udid
- flutter_webrtc
gal
hotkey_manager_windows
- livekit_client
local_notifier
media_kit_libs_windows_video
media_kit_video