✨ Call join
This commit is contained in:
parent
15c8c0fe8f
commit
7ac5c651aa
@ -23,7 +23,7 @@ if (flutterVersionName == null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace "com.example.solian"
|
namespace "dev.solsynth.solian"
|
||||||
compileSdk flutter.compileSdkVersion
|
compileSdk flutter.compileSdkVersion
|
||||||
ndkVersion flutter.ndkVersion
|
ndkVersion flutter.ndkVersion
|
||||||
|
|
||||||
@ -41,11 +41,8 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
applicationId "dev.solsynth.solian"
|
||||||
applicationId "com.example.solian"
|
minSdkVersion 21
|
||||||
// You can update the following values to match your application needs.
|
|
||||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
|
||||||
minSdkVersion flutter.minSdkVersion
|
|
||||||
targetSdkVersion flutter.targetSdkVersion
|
targetSdkVersion flutter.targetSdkVersion
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
@ -53,8 +50,6 @@ android {
|
|||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
// TODO: Add your own signing config for the release build.
|
|
||||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
|
||||||
signingConfig signingConfigs.debug
|
signingConfig signingConfigs.debug
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,29 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<uses-feature android:name="android.hardware.camera" />
|
||||||
|
<uses-feature android:name="android.hardware.camera.autofocus" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
|
||||||
|
<application>
|
||||||
|
...
|
||||||
|
<service
|
||||||
|
android:name="de.julianassmann.flutter_background.IsolateHolderService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="mediaProjection" />
|
||||||
|
</application>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="Solian"
|
android:label="Solian"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package com.example.solian
|
package dev.solsynth.solian
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
@ -496,7 +496,7 @@
|
|||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.solian.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
@ -514,7 +514,7 @@
|
|||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.solian.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
@ -530,7 +530,7 @@
|
|||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.solian.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
|
@ -58,5 +58,9 @@
|
|||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>audio</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
import 'package:solian/models/channel.dart';
|
import 'package:solian/models/channel.dart';
|
||||||
|
|
||||||
class Call {
|
class Call {
|
||||||
@ -46,4 +47,22 @@ class Call {
|
|||||||
"channel_id": channelId,
|
"channel_id": channelId,
|
||||||
"channel": channel.toJson(),
|
"channel": channel.toJson(),
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ParticipantStatsType {
|
||||||
|
unknown,
|
||||||
|
localAudioSender,
|
||||||
|
localVideoSender,
|
||||||
|
remoteAudioReceiver,
|
||||||
|
remoteVideoReceiver,
|
||||||
|
}
|
||||||
|
|
||||||
|
class ParticipantTrack {
|
||||||
|
ParticipantTrack(
|
||||||
|
{required this.participant,
|
||||||
|
required this.videoTrack,
|
||||||
|
required this.isScreenShare});
|
||||||
|
VideoTrack? videoTrack;
|
||||||
|
Participant participant;
|
||||||
|
final bool isScreenShare;
|
||||||
}
|
}
|
@ -1,13 +1,21 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:solian/models/call.dart';
|
import 'package:solian/models/call.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/router.dart';
|
import 'package:solian/router.dart';
|
||||||
import 'package:solian/utils/service_url.dart';
|
import 'package:solian/utils/service_url.dart';
|
||||||
|
import 'package:solian/widgets/chat/call/exts.dart';
|
||||||
|
import 'package:solian/widgets/chat/call/participant.dart';
|
||||||
import 'package:solian/widgets/indent_wrapper.dart';
|
import 'package:solian/widgets/indent_wrapper.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import '../../widgets/chat/call/controls.dart';
|
||||||
|
|
||||||
class ChatCall extends StatefulWidget {
|
class ChatCall extends StatefulWidget {
|
||||||
final Call call;
|
final Call call;
|
||||||
@ -20,8 +28,40 @@ class ChatCall extends StatefulWidget {
|
|||||||
|
|
||||||
class _ChatCallState extends State<ChatCall> {
|
class _ChatCallState extends State<ChatCall> {
|
||||||
String? _token;
|
String? _token;
|
||||||
|
String? _endpoint;
|
||||||
|
|
||||||
|
bool _isMounted = false;
|
||||||
|
|
||||||
|
StreamSubscription? _subscription;
|
||||||
|
List<MediaDevice> _audioInputs = [];
|
||||||
|
List<MediaDevice> _videoInputs = [];
|
||||||
|
|
||||||
|
bool _enableAudio = true;
|
||||||
|
bool _enableVideo = false;
|
||||||
|
LocalAudioTrack? _audioTrack;
|
||||||
|
LocalVideoTrack? _videoTrack;
|
||||||
|
MediaDevice? _videoDevice;
|
||||||
|
MediaDevice? _audioDevice;
|
||||||
|
|
||||||
|
final VideoParameters _videoParameters = VideoParametersPresets.h720_169;
|
||||||
|
|
||||||
|
late Room _callRoom;
|
||||||
|
late EventsListener<RoomEvent> _callListener;
|
||||||
|
|
||||||
|
List<ParticipantTrack> _participantTracks = [];
|
||||||
|
|
||||||
|
bool get _fastConnection => _callRoom.engine.fastConnectOptions != null;
|
||||||
|
|
||||||
|
Future<void> checkPermissions() async {
|
||||||
|
await Permission.camera.request();
|
||||||
|
await Permission.microphone.request();
|
||||||
|
await Permission.bluetooth.request();
|
||||||
|
await Permission.bluetoothConnect.request();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<(String, String)> exchangeToken() async {
|
||||||
|
await checkPermissions();
|
||||||
|
|
||||||
Future<String> exchangeToken() async {
|
|
||||||
final auth = context.read<AuthProvider>();
|
final auth = context.read<AuthProvider>();
|
||||||
if (!await auth.isAuthorized()) {
|
if (!await auth.isAuthorized()) {
|
||||||
router.pop();
|
router.pop();
|
||||||
@ -34,7 +74,9 @@ class _ChatCallState extends State<ChatCall> {
|
|||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
final result = jsonDecode(utf8.decode(res.bodyBytes));
|
final result = jsonDecode(utf8.decode(res.bodyBytes));
|
||||||
_token = result['token'];
|
_token = result['token'];
|
||||||
return _token!;
|
_endpoint = 'wss://${result['endpoint']}';
|
||||||
|
joinRoom(_endpoint!, _token!);
|
||||||
|
return (_token!, _endpoint!);
|
||||||
} else {
|
} else {
|
||||||
var message = utf8.decode(res.bodyBytes);
|
var message = utf8.decode(res.bodyBytes);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@ -44,6 +86,284 @@ class _ChatCallState extends State<ChatCall> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void joinRoom(String url, String token) async {
|
||||||
|
if(_isMounted) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
_isMounted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).clearSnackBars();
|
||||||
|
|
||||||
|
final notify = ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(AppLocalizations.of(context)!.connectingServer),
|
||||||
|
duration: const Duration(minutes: 1),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _callRoom.connect(
|
||||||
|
url,
|
||||||
|
token,
|
||||||
|
roomOptions: RoomOptions(
|
||||||
|
dynacast: true,
|
||||||
|
adaptiveStream: true,
|
||||||
|
defaultAudioPublishOptions: const AudioPublishOptions(
|
||||||
|
name: 'call_voice',
|
||||||
|
stream: 'call_stream',
|
||||||
|
),
|
||||||
|
defaultVideoPublishOptions: const VideoPublishOptions(
|
||||||
|
name: 'call_video',
|
||||||
|
stream: 'call_stream',
|
||||||
|
simulcast: true,
|
||||||
|
backupVideoCodec: BackupVideoCodec(enabled: true),
|
||||||
|
),
|
||||||
|
defaultScreenShareCaptureOptions: const ScreenShareCaptureOptions(
|
||||||
|
useiOSBroadcastExtension: true,
|
||||||
|
params: VideoParameters(
|
||||||
|
dimensions: VideoDimensionsPresets.h1080_169,
|
||||||
|
encoding: VideoEncoding(maxBitrate: 3 * 1000 * 1000, maxFramerate: 30),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
defaultCameraCaptureOptions: CameraCaptureOptions(maxFrameRate: 30, params: _videoParameters),
|
||||||
|
),
|
||||||
|
fastConnectOptions: FastConnectOptions(
|
||||||
|
microphone: TrackOption(track: _audioTrack),
|
||||||
|
camera: TrackOption(track: _videoTrack),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
setupRoom();
|
||||||
|
} catch (e) {
|
||||||
|
final message = e.toString();
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Something went wrong... $message")),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
notify.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void askPublish() async {
|
||||||
|
final result = await context.showPublishDialog();
|
||||||
|
if (result != true) return;
|
||||||
|
try {
|
||||||
|
await _callRoom.localParticipant?.setCameraEnabled(true);
|
||||||
|
} catch (error) {
|
||||||
|
await context.showErrorDialog(error);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await _callRoom.localParticipant?.setMicrophoneEnabled(true);
|
||||||
|
} catch (error) {
|
||||||
|
await context.showErrorDialog(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setupRoom() {
|
||||||
|
_callRoom.addListener(onRoomDidUpdate);
|
||||||
|
setupRoomListeners();
|
||||||
|
sortParticipants();
|
||||||
|
WidgetsBindingCompatible.instance?.addPostFrameCallback((_) {
|
||||||
|
if (!_fastConnection) {
|
||||||
|
askPublish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (lkPlatformIsMobile()) {
|
||||||
|
Hardware.instance.setSpeakerphoneOn(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setupRoomListeners() {
|
||||||
|
_callListener
|
||||||
|
..on<RoomDisconnectedEvent>((event) async {
|
||||||
|
if (event.reason != null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||||
|
content: Text('Call disconnected... ${event.reason}'),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if (router.canPop()) router.pop();
|
||||||
|
})
|
||||||
|
..on<ParticipantEvent>((event) => sortParticipants())
|
||||||
|
..on<RoomRecordingStatusChanged>((event) {
|
||||||
|
context.showRecordingStatusChangedDialog(event.activeRecording);
|
||||||
|
})
|
||||||
|
..on<LocalTrackPublishedEvent>((_) => sortParticipants())
|
||||||
|
..on<LocalTrackUnpublishedEvent>((_) => sortParticipants())
|
||||||
|
..on<TrackSubscribedEvent>((_) => sortParticipants())
|
||||||
|
..on<TrackUnsubscribedEvent>((_) => sortParticipants())
|
||||||
|
..on<ParticipantNameUpdatedEvent>((event) {
|
||||||
|
sortParticipants();
|
||||||
|
})
|
||||||
|
..on<AudioPlaybackStatusChanged>((event) async {
|
||||||
|
if (!_callRoom.canPlaybackAudio) {
|
||||||
|
bool? yesno = await context.showPlayAudioManuallyDialog();
|
||||||
|
if (yesno == true) {
|
||||||
|
await _callRoom.startAudio();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void sortParticipants() {
|
||||||
|
List<ParticipantTrack> userMediaTracks = [];
|
||||||
|
List<ParticipantTrack> screenTracks = [];
|
||||||
|
for (var participant in _callRoom.remoteParticipants.values) {
|
||||||
|
for (var t in participant.videoTrackPublications) {
|
||||||
|
if (t.isScreenShare) {
|
||||||
|
screenTracks.add(ParticipantTrack(
|
||||||
|
participant: participant,
|
||||||
|
videoTrack: t.track,
|
||||||
|
isScreenShare: true,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
userMediaTracks.add(ParticipantTrack(
|
||||||
|
participant: participant,
|
||||||
|
videoTrack: t.track,
|
||||||
|
isScreenShare: false,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
userMediaTracks.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;
|
||||||
|
});
|
||||||
|
|
||||||
|
final localParticipantTracks = _callRoom.localParticipant?.videoTrackPublications;
|
||||||
|
if (localParticipantTracks != null) {
|
||||||
|
for (var t in localParticipantTracks) {
|
||||||
|
if (t.isScreenShare) {
|
||||||
|
screenTracks.add(ParticipantTrack(
|
||||||
|
participant: _callRoom.localParticipant!,
|
||||||
|
videoTrack: t.track,
|
||||||
|
isScreenShare: true,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
userMediaTracks.add(ParticipantTrack(
|
||||||
|
participant: _callRoom.localParticipant!,
|
||||||
|
videoTrack: t.track,
|
||||||
|
isScreenShare: false,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_participantTracks = [...screenTracks, ...userMediaTracks];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void onRoomDidUpdate() => sortParticipants();
|
||||||
|
|
||||||
|
void revertDevices(List<MediaDevice> devices) async {
|
||||||
|
_audioInputs = devices.where((d) => d.kind == 'audioinput').toList();
|
||||||
|
_videoInputs = devices.where((d) => d.kind == 'videoinput').toList();
|
||||||
|
|
||||||
|
if (_audioInputs.isNotEmpty) {
|
||||||
|
if (_audioDevice == null && _enableAudio) {
|
||||||
|
_audioDevice = _audioInputs.first;
|
||||||
|
Future.delayed(const Duration(milliseconds: 100), () async {
|
||||||
|
await changeLocalAudioTrack();
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_videoInputs.isNotEmpty) {
|
||||||
|
if (_videoDevice == null && _enableVideo) {
|
||||||
|
_videoDevice = _videoInputs.first;
|
||||||
|
Future.delayed(const Duration(milliseconds: 100), () async {
|
||||||
|
await changeLocalVideoTrack();
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setEnableVideo(value) async {
|
||||||
|
_enableVideo = value;
|
||||||
|
if (!_enableVideo) {
|
||||||
|
await _videoTrack?.stop();
|
||||||
|
_videoTrack = null;
|
||||||
|
} else {
|
||||||
|
await changeLocalVideoTrack();
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setEnableAudio(value) async {
|
||||||
|
_enableAudio = value;
|
||||||
|
if (!_enableAudio) {
|
||||||
|
await _audioTrack?.stop();
|
||||||
|
_audioTrack = null;
|
||||||
|
} else {
|
||||||
|
await changeLocalAudioTrack();
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
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: _videoParameters,
|
||||||
|
));
|
||||||
|
await _videoTrack!.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_subscription = Hardware.instance.onDeviceChange.stream.listen(revertDevices);
|
||||||
|
_callRoom = Room();
|
||||||
|
_callListener = _callRoom.createListener();
|
||||||
|
Hardware.instance.enumerateDevices().then(revertDevices);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return IndentWrapper(
|
return IndentWrapper(
|
||||||
@ -57,10 +377,60 @@ class _ChatCallState extends State<ChatCall> {
|
|||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
||||||
print(snapshot.data!);
|
return Stack(
|
||||||
return Container();
|
children: [
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _participantTracks.isNotEmpty
|
||||||
|
? ParticipantWidget.widgetFor(_participantTracks.first, showStatsLayer: true)
|
||||||
|
: Container(),
|
||||||
|
),
|
||||||
|
if (_callRoom.localParticipant != null)
|
||||||
|
SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: ControlsWidget(_callRoom, _callRoom.localParticipant!),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
child: SizedBox(
|
||||||
|
height: 120,
|
||||||
|
child: ListView.builder(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: math.max(0, _participantTracks.length - 1),
|
||||||
|
itemBuilder: (BuildContext context, int index) => SizedBox(
|
||||||
|
width: 180,
|
||||||
|
height: 120,
|
||||||
|
child: ParticipantWidget.widgetFor(_participantTracks[index + 1]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void deactivate() {
|
||||||
|
_subscription?.cancel();
|
||||||
|
super.deactivate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
(() async {
|
||||||
|
_callRoom.removeListener(onRoomDidUpdate);
|
||||||
|
await _callRoom.disconnect();
|
||||||
|
await _callListener.dispose();
|
||||||
|
await _callRoom.dispose();
|
||||||
|
})();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:solian/models/channel.dart';
|
import 'package:solian/models/channel.dart';
|
||||||
|
379
lib/widgets/chat/call/controls.dart
Normal file
379
lib/widgets/chat/call/controls.dart
Normal file
@ -0,0 +1,379 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_background/flutter_background.dart';
|
||||||
|
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||||
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
|
import 'package:solian/widgets/chat/call/exts.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(() {});
|
||||||
|
|
||||||
|
void unpublishAll() async {
|
||||||
|
final result = await context.showUnPublishDialog();
|
||||||
|
if (result == true) await participant.unpublishAllTracks();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isMuted => participant.isMuted;
|
||||||
|
|
||||||
|
void disableAudio() async {
|
||||||
|
await participant.setMicrophoneEnabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<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 setSpeakerphoneOn() {
|
||||||
|
_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(
|
||||||
|
sourceId: source.id,
|
||||||
|
maxFrameRate: 15.0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await participant.publishVideoTrack(track);
|
||||||
|
} catch (e) {
|
||||||
|
final message = e.toString();
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||||
|
content: Text('Something went wrong... $message'),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (lkPlatformIs(PlatformType.android)) {
|
||||||
|
requestBackgroundPermission([bool isRetry = false]) async {
|
||||||
|
try {
|
||||||
|
bool hasPermissions = await FlutterBackground.hasPermissions;
|
||||||
|
if (!isRetry) {
|
||||||
|
const androidConfig = FlutterBackgroundAndroidConfig(
|
||||||
|
notificationTitle: 'Screen Sharing',
|
||||||
|
notificationText: 'A Solar Messager\'s Call is sharing your screen',
|
||||||
|
notificationImportance: AndroidNotificationImportance.Default,
|
||||||
|
notificationIcon: AndroidResource(name: 'launcher_icon', defType: 'mipmap'),
|
||||||
|
);
|
||||||
|
hasPermissions = await FlutterBackground.initialize(androidConfig: androidConfig);
|
||||||
|
}
|
||||||
|
if (hasPermissions && !FlutterBackground.isBackgroundExecutionEnabled) {
|
||||||
|
await FlutterBackground.enableBackgroundExecution();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (!isRetry) {
|
||||||
|
return await Future<void>.delayed(const Duration(seconds: 1), () => requestBackgroundPermission(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await requestBackgroundPermission();
|
||||||
|
}
|
||||||
|
if (lkPlatformIs(PlatformType.iOS)) {
|
||||||
|
var track = await LocalVideoTrack.createScreenShareTrack(
|
||||||
|
const ScreenShareCaptureOptions(
|
||||||
|
useiOSBroadcastExtension: 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);
|
||||||
|
if (lkPlatformIs(PlatformType.android)) {
|
||||||
|
// Android specific
|
||||||
|
try {
|
||||||
|
await FlutterBackground.disableBackgroundExecution();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onTapUpdateSubscribePermission() async {
|
||||||
|
final result = await context.showSubscribePermissionDialog();
|
||||||
|
if (result != null) {
|
||||||
|
try {
|
||||||
|
widget.room.localParticipant?.setTrackSubscriptionPermissions(
|
||||||
|
allParticipantsAllowed: result,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
final message = e.toString();
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||||
|
content: Text('Something went wrong... $message'),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 15,
|
||||||
|
horizontal: 15,
|
||||||
|
),
|
||||||
|
child: Wrap(
|
||||||
|
alignment: WrapAlignment.center,
|
||||||
|
spacing: 5,
|
||||||
|
runSpacing: 5,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: unpublishAll,
|
||||||
|
icon: const Icon(Icons.cancel),
|
||||||
|
tooltip: 'Unpublish all',
|
||||||
|
),
|
||||||
|
if (participant.isMicrophoneEnabled())
|
||||||
|
if (lkPlatformIs(PlatformType.android))
|
||||||
|
IconButton(
|
||||||
|
onPressed: disableAudio,
|
||||||
|
icon: const Icon(Icons.mic),
|
||||||
|
tooltip: 'mute audio',
|
||||||
|
)
|
||||||
|
else
|
||||||
|
PopupMenuButton<MediaDevice>(
|
||||||
|
icon: const Icon(Icons.settings_voice),
|
||||||
|
itemBuilder: (BuildContext context) {
|
||||||
|
return [
|
||||||
|
PopupMenuItem<MediaDevice>(
|
||||||
|
value: null,
|
||||||
|
onTap: isMuted ? enableAudio : disableAudio,
|
||||||
|
child: const ListTile(
|
||||||
|
leading: Icon(Icons.mic_off),
|
||||||
|
title: Text('Mute Microphone'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_audioInputs != null)
|
||||||
|
..._audioInputs!.map((device) {
|
||||||
|
return PopupMenuItem<MediaDevice>(
|
||||||
|
value: device,
|
||||||
|
child: ListTile(
|
||||||
|
leading: (device.deviceId == widget.room.selectedAudioInputDeviceId)
|
||||||
|
? const Icon(Icons.check_box_outlined)
|
||||||
|
: const Icon(Icons.check_box_outline_blank),
|
||||||
|
title: Text(device.label),
|
||||||
|
),
|
||||||
|
onTap: () => selectAudioInput(device),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
];
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else
|
||||||
|
IconButton(
|
||||||
|
onPressed: enableAudio,
|
||||||
|
icon: const Icon(Icons.mic_off),
|
||||||
|
tooltip: 'un-mute audio',
|
||||||
|
),
|
||||||
|
if (!lkPlatformIs(PlatformType.iOS))
|
||||||
|
PopupMenuButton<MediaDevice>(
|
||||||
|
icon: const Icon(Icons.volume_up),
|
||||||
|
itemBuilder: (BuildContext context) {
|
||||||
|
return [
|
||||||
|
const PopupMenuItem<MediaDevice>(
|
||||||
|
value: null,
|
||||||
|
child: ListTile(
|
||||||
|
leading: Icon(Icons.speaker),
|
||||||
|
title: Text('Select Audio Output'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_audioOutputs != null)
|
||||||
|
..._audioOutputs!.map((device) {
|
||||||
|
return PopupMenuItem<MediaDevice>(
|
||||||
|
value: device,
|
||||||
|
child: ListTile(
|
||||||
|
leading: (device.deviceId == widget.room.selectedAudioOutputDeviceId)
|
||||||
|
? const Icon(Icons.check_box_outlined)
|
||||||
|
: const Icon(Icons.check_box_outline_blank),
|
||||||
|
title: Text(device.label),
|
||||||
|
),
|
||||||
|
onTap: () => selectAudioOutput(device),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
];
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (!kIsWeb && lkPlatformIs(PlatformType.iOS))
|
||||||
|
IconButton(
|
||||||
|
disabledColor: Colors.grey,
|
||||||
|
onPressed: Hardware.instance.canSwitchSpeakerphone ? setSpeakerphoneOn : null,
|
||||||
|
icon: Icon(_speakerphoneOn ? Icons.speaker_phone : Icons.phone_android),
|
||||||
|
tooltip: 'Switch SpeakerPhone',
|
||||||
|
),
|
||||||
|
if (participant.isCameraEnabled())
|
||||||
|
PopupMenuButton<MediaDevice>(
|
||||||
|
icon: const Icon(Icons.videocam_sharp),
|
||||||
|
itemBuilder: (BuildContext context) {
|
||||||
|
return [
|
||||||
|
PopupMenuItem<MediaDevice>(
|
||||||
|
value: null,
|
||||||
|
onTap: disableVideo,
|
||||||
|
child: const ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
Icons.videocam_off,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
title: Text('Disable Camera'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_videoInputs != null)
|
||||||
|
..._videoInputs!.map((device) {
|
||||||
|
return PopupMenuItem<MediaDevice>(
|
||||||
|
value: device,
|
||||||
|
child: ListTile(
|
||||||
|
leading: (device.deviceId == widget.room.selectedVideoInputDeviceId)
|
||||||
|
? const Icon(Icons.check_box_outlined)
|
||||||
|
: const Icon(Icons.check_box_outline_blank),
|
||||||
|
title: Text(device.label),
|
||||||
|
),
|
||||||
|
onTap: () => selectVideoInput(device),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
];
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else
|
||||||
|
IconButton(
|
||||||
|
onPressed: enableVideo,
|
||||||
|
icon: const Icon(Icons.videocam_off),
|
||||||
|
tooltip: 'un-mute video',
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(position == CameraPosition.back ? Icons.video_camera_back : Icons.video_camera_front),
|
||||||
|
onPressed: () => toggleCamera(),
|
||||||
|
tooltip: 'toggle camera',
|
||||||
|
),
|
||||||
|
if (participant.isScreenShareEnabled())
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.monitor_outlined),
|
||||||
|
onPressed: () => disableScreenShare(),
|
||||||
|
tooltip: 'unshare screen (experimental)',
|
||||||
|
)
|
||||||
|
else
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.monitor),
|
||||||
|
onPressed: () => enableScreenShare(),
|
||||||
|
tooltip: 'share screen (experimental)',
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: onTapUpdateSubscribePermission,
|
||||||
|
icon: const Icon(Icons.settings),
|
||||||
|
tooltip: 'Subscribe permission',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
206
lib/widgets/chat/call/exts.dart
Normal file
206
lib/widgets/chat/call/exts.dart
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
extension SolianCallExt on BuildContext {
|
||||||
|
Future<bool?> showPublishDialog() => showDialog<bool>(
|
||||||
|
context: this,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('Publish'),
|
||||||
|
content: const Text('Would you like to publish your Camera & Mic ?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, false),
|
||||||
|
child: const Text('NO'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, true),
|
||||||
|
child: const Text('YES'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<bool?> showPlayAudioManuallyDialog() => showDialog<bool>(
|
||||||
|
context: this,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('Play Audio'),
|
||||||
|
content: const Text(
|
||||||
|
'You need to manually activate audio PlayBack for iOS Safari !'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, false),
|
||||||
|
child: const Text('Ignore'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, true),
|
||||||
|
child: const Text('Play Audio'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<bool?> showUnPublishDialog() => showDialog<bool>(
|
||||||
|
context: this,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('UnPublish'),
|
||||||
|
content:
|
||||||
|
const Text('Would you like to un-publish your Camera & Mic ?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, false),
|
||||||
|
child: const Text('NO'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, true),
|
||||||
|
child: const Text('YES'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<void> showErrorDialog(dynamic exception) => showDialog<void>(
|
||||||
|
context: this,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('Error'),
|
||||||
|
content: Text(exception.toString()),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx),
|
||||||
|
child: const Text('OK'),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<bool?> showDisconnectDialog() => showDialog<bool>(
|
||||||
|
context: this,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('Disconnect'),
|
||||||
|
content: const Text('Are you sure to disconnect?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, false),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, true),
|
||||||
|
child: const Text('Disconnect'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<bool?> showReconnectDialog() => showDialog<bool>(
|
||||||
|
context: this,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('Reconnect'),
|
||||||
|
content: const Text('This will force a reconnection'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, false),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, true),
|
||||||
|
child: const Text('Reconnect'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<void> showReconnectSuccessDialog() => showDialog<void>(
|
||||||
|
context: this,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('Reconnect'),
|
||||||
|
content: const Text('Reconnection was successful.'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx),
|
||||||
|
child: const Text('OK'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<bool?> showSendDataDialog() => showDialog<bool>(
|
||||||
|
context: this,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('Send data'),
|
||||||
|
content: const Text(
|
||||||
|
'This will send a sample data to all participants in the room'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, false),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, true),
|
||||||
|
child: const Text('Send'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<bool?> showDataReceivedDialog(String data) => showDialog<bool>(
|
||||||
|
context: this,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('Received data'),
|
||||||
|
content: Text(data),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, true),
|
||||||
|
child: const Text('OK'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<bool?> showRecordingStatusChangedDialog(bool isActiveRecording) =>
|
||||||
|
showDialog<bool>(
|
||||||
|
context: this,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('Room recording reminder'),
|
||||||
|
content: Text(isActiveRecording
|
||||||
|
? 'Room recording is active.'
|
||||||
|
: 'Room recording is stoped.'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, true),
|
||||||
|
child: const Text('OK'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<bool?> showSubscribePermissionDialog() => showDialog<bool>(
|
||||||
|
context: this,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('Allow subscription'),
|
||||||
|
content: const Text(
|
||||||
|
'Allow all participants to subscribe tracks published by local participant?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, false),
|
||||||
|
child: const Text('NO'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, true),
|
||||||
|
child: const Text('YES'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SimulateScenarioResult {
|
||||||
|
signalReconnect,
|
||||||
|
fullReconnect,
|
||||||
|
speakerUpdate,
|
||||||
|
nodeFailure,
|
||||||
|
migration,
|
||||||
|
serverLeave,
|
||||||
|
switchCandidate,
|
||||||
|
e2eeKeyRatchet,
|
||||||
|
participantName,
|
||||||
|
participantMetadata,
|
||||||
|
clear,
|
||||||
|
}
|
18
lib/widgets/chat/call/no_video.dart
Normal file
18
lib/widgets/chat/call/no_video.dart
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
class NoVideoWidget extends StatelessWidget {
|
||||||
|
const NoVideoWidget({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Container(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (ctx, constraints) => Icon(
|
||||||
|
Icons.videocam_off_outlined,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
size: math.min(constraints.maxHeight, constraints.maxWidth) * 0.3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
333
lib/widgets/chat/call/participant.dart
Normal file
333
lib/widgets/chat/call/participant.dart
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||||
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
|
import 'package:solian/models/call.dart';
|
||||||
|
import 'package:solian/widgets/chat/call/no_video.dart';
|
||||||
|
import 'package:solian/widgets/chat/call/participant_info.dart';
|
||||||
|
import 'package:solian/widgets/chat/call/participant_stats.dart';
|
||||||
|
|
||||||
|
abstract class ParticipantWidget extends StatefulWidget {
|
||||||
|
static ParticipantWidget widgetFor(ParticipantTrack participantTrack, {bool showStatsLayer = false}) {
|
||||||
|
if (participantTrack.participant is LocalParticipant) {
|
||||||
|
return LocalParticipantWidget(participantTrack.participant as LocalParticipant, participantTrack.videoTrack,
|
||||||
|
participantTrack.isScreenShare, showStatsLayer);
|
||||||
|
} else if (participantTrack.participant is RemoteParticipant) {
|
||||||
|
return RemoteParticipantWidget(participantTrack.participant as RemoteParticipant, participantTrack.videoTrack,
|
||||||
|
participantTrack.isScreenShare, showStatsLayer);
|
||||||
|
}
|
||||||
|
throw UnimplementedError('Unknown participant type');
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract final Participant participant;
|
||||||
|
abstract final VideoTrack? videoTrack;
|
||||||
|
abstract final bool isScreenShare;
|
||||||
|
abstract final bool showStatsLayer;
|
||||||
|
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 bool isScreenShare;
|
||||||
|
@override
|
||||||
|
final bool showStatsLayer;
|
||||||
|
|
||||||
|
const LocalParticipantWidget(
|
||||||
|
this.participant,
|
||||||
|
this.videoTrack,
|
||||||
|
this.isScreenShare,
|
||||||
|
this.showStatsLayer, {
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _LocalParticipantWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class RemoteParticipantWidget extends ParticipantWidget {
|
||||||
|
@override
|
||||||
|
final RemoteParticipant participant;
|
||||||
|
@override
|
||||||
|
final VideoTrack? videoTrack;
|
||||||
|
@override
|
||||||
|
final bool isScreenShare;
|
||||||
|
@override
|
||||||
|
final bool showStatsLayer;
|
||||||
|
|
||||||
|
const RemoteParticipantWidget(
|
||||||
|
this.participant,
|
||||||
|
this.videoTrack,
|
||||||
|
this.isScreenShare,
|
||||||
|
this.showStatsLayer, {
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _RemoteParticipantWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _ParticipantWidgetState<T extends ParticipantWidget> extends State<T> {
|
||||||
|
bool _visible = true;
|
||||||
|
|
||||||
|
VideoTrack? get _activeVideoTrack;
|
||||||
|
|
||||||
|
TrackPublication? get _videoPublication;
|
||||||
|
|
||||||
|
TrackPublication? get _firstAudioPublication;
|
||||||
|
|
||||||
|
@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(() {});
|
||||||
|
|
||||||
|
List<Widget> extraWidgets(bool isScreenShare) => [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext ctx) => Container(
|
||||||
|
foregroundDecoration: BoxDecoration(
|
||||||
|
border: widget.participant.isSpeaking && !widget.isScreenShare
|
||||||
|
? Border.all(
|
||||||
|
width: 5,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(ctx).cardColor,
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
// Video
|
||||||
|
InkWell(
|
||||||
|
onTap: () => setState(() => _visible = !_visible),
|
||||||
|
child: _activeVideoTrack != null && !_activeVideoTrack!.muted
|
||||||
|
? VideoTrackRenderer(
|
||||||
|
_activeVideoTrack!,
|
||||||
|
fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
|
||||||
|
)
|
||||||
|
: const NoVideoWidget(),
|
||||||
|
),
|
||||||
|
if (widget.showStatsLayer)
|
||||||
|
Positioned(
|
||||||
|
top: 30,
|
||||||
|
right: 30,
|
||||||
|
child: ParticipantStatsWidget(
|
||||||
|
participant: widget.participant,
|
||||||
|
)),
|
||||||
|
// Bottom bar
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
...extraWidgets(widget.isScreenShare),
|
||||||
|
ParticipantInfoWidget(
|
||||||
|
title: widget.participant.name.isNotEmpty
|
||||||
|
? '${widget.participant.name} (${widget.participant.identity})'
|
||||||
|
: widget.participant.identity,
|
||||||
|
audioAvailable:
|
||||||
|
_firstAudioPublication?.muted == false && _firstAudioPublication?.subscribed == true,
|
||||||
|
connectionQuality: widget.participant.connectionQuality,
|
||||||
|
isScreenShare: widget.isScreenShare,
|
||||||
|
enabledE2EE: widget.participant.isEncrypted,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LocalParticipantWidgetState extends _ParticipantWidgetState<LocalParticipantWidget> {
|
||||||
|
@override
|
||||||
|
LocalTrackPublication<LocalVideoTrack>? get _videoPublication =>
|
||||||
|
widget.participant.videoTrackPublications.where((element) => element.sid == widget.videoTrack?.sid).firstOrNull;
|
||||||
|
|
||||||
|
@override
|
||||||
|
LocalTrackPublication<LocalAudioTrack>? get _firstAudioPublication =>
|
||||||
|
widget.participant.audioTrackPublications.firstOrNull;
|
||||||
|
|
||||||
|
@override
|
||||||
|
VideoTrack? get _activeVideoTrack => widget.videoTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RemoteParticipantWidgetState extends _ParticipantWidgetState<RemoteParticipantWidget> {
|
||||||
|
@override
|
||||||
|
RemoteTrackPublication<RemoteVideoTrack>? get _videoPublication =>
|
||||||
|
widget.participant.videoTrackPublications.where((element) => element.sid == widget.videoTrack?.sid).firstOrNull;
|
||||||
|
|
||||||
|
@override
|
||||||
|
RemoteTrackPublication<RemoteAudioTrack>? get _firstAudioPublication =>
|
||||||
|
widget.participant.audioTrackPublications.firstOrNull;
|
||||||
|
|
||||||
|
@override
|
||||||
|
VideoTrack? get _activeVideoTrack => widget.videoTrack;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Widget> extraWidgets(bool isScreenShare) => [
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
// Menu for RemoteTrackPublication<RemoteAudioTrack>
|
||||||
|
if (_firstAudioPublication != null && !isScreenShare)
|
||||||
|
RemoteTrackPublicationMenuWidget(
|
||||||
|
pub: _firstAudioPublication!,
|
||||||
|
icon: Icons.volume_up,
|
||||||
|
),
|
||||||
|
// Menu for RemoteTrackPublication<RemoteVideoTrack>
|
||||||
|
if (_videoPublication != null)
|
||||||
|
RemoteTrackPublicationMenuWidget(
|
||||||
|
pub: _videoPublication!,
|
||||||
|
icon: isScreenShare ? Icons.monitor : Icons.videocam,
|
||||||
|
),
|
||||||
|
if (_videoPublication != null)
|
||||||
|
RemoteTrackFPSMenuWidget(
|
||||||
|
pub: _videoPublication!,
|
||||||
|
icon: Icons.menu,
|
||||||
|
),
|
||||||
|
if (_videoPublication != null)
|
||||||
|
RemoteTrackQualityMenuWidget(
|
||||||
|
pub: _videoPublication!,
|
||||||
|
icon: Icons.monitor_outlined,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
class RemoteTrackPublicationMenuWidget extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final RemoteTrackPublication pub;
|
||||||
|
|
||||||
|
const RemoteTrackPublicationMenuWidget({
|
||||||
|
required this.pub,
|
||||||
|
required this.icon,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Material(
|
||||||
|
color: Colors.black.withOpacity(0.3),
|
||||||
|
child: PopupMenuButton<Function>(
|
||||||
|
tooltip: 'Subscribe menu',
|
||||||
|
icon: Icon(icon,
|
||||||
|
color: {
|
||||||
|
TrackSubscriptionState.notAllowed: Colors.red,
|
||||||
|
TrackSubscriptionState.unsubscribed: Colors.grey,
|
||||||
|
TrackSubscriptionState.subscribed: Colors.green,
|
||||||
|
}[pub.subscriptionState]),
|
||||||
|
onSelected: (value) => value(),
|
||||||
|
itemBuilder: (BuildContext context) => <PopupMenuEntry<Function>>[
|
||||||
|
if (pub.subscribed == false)
|
||||||
|
PopupMenuItem(
|
||||||
|
child: const Text('Subscribe'),
|
||||||
|
value: () => pub.subscribe(),
|
||||||
|
)
|
||||||
|
else if (pub.subscribed == true)
|
||||||
|
PopupMenuItem(
|
||||||
|
child: const Text('Un-subscribe'),
|
||||||
|
value: () => pub.unsubscribe(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class RemoteTrackFPSMenuWidget extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final RemoteTrackPublication pub;
|
||||||
|
|
||||||
|
const RemoteTrackFPSMenuWidget({
|
||||||
|
required this.pub,
|
||||||
|
required this.icon,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Material(
|
||||||
|
color: Colors.black.withOpacity(0.3),
|
||||||
|
child: PopupMenuButton<Function>(
|
||||||
|
tooltip: 'Preferred FPS',
|
||||||
|
icon: Icon(icon, color: Colors.white),
|
||||||
|
onSelected: (value) => value(),
|
||||||
|
itemBuilder: (BuildContext context) => <PopupMenuEntry<Function>>[
|
||||||
|
PopupMenuItem(
|
||||||
|
child: const Text('30'),
|
||||||
|
value: () => pub.setVideoFPS(30),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: const Text('15'),
|
||||||
|
value: () => pub.setVideoFPS(15),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: const Text('8'),
|
||||||
|
value: () => pub.setVideoFPS(8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class RemoteTrackQualityMenuWidget extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final RemoteTrackPublication pub;
|
||||||
|
|
||||||
|
const RemoteTrackQualityMenuWidget({
|
||||||
|
required this.pub,
|
||||||
|
required this.icon,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Material(
|
||||||
|
color: Colors.black.withOpacity(0.3),
|
||||||
|
child: PopupMenuButton<Function>(
|
||||||
|
tooltip: 'Preferred Quality',
|
||||||
|
icon: Icon(icon, color: Colors.white),
|
||||||
|
onSelected: (value) => value(),
|
||||||
|
itemBuilder: (BuildContext context) => <PopupMenuEntry<Function>>[
|
||||||
|
PopupMenuItem(
|
||||||
|
child: const Text('HIGH'),
|
||||||
|
value: () => pub.setVideoQuality(VideoQuality.HIGH),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: const Text('MEDIUM'),
|
||||||
|
value: () => pub.setVideoQuality(VideoQuality.MEDIUM),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: const Text('LOW'),
|
||||||
|
value: () => pub.setVideoQuality(VideoQuality.LOW),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
79
lib/widgets/chat/call/participant_info.dart
Normal file
79
lib/widgets/chat/call/participant_info.dart
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
|
|
||||||
|
class ParticipantInfoWidget extends StatelessWidget {
|
||||||
|
final String? title;
|
||||||
|
final bool audioAvailable;
|
||||||
|
final ConnectionQuality connectionQuality;
|
||||||
|
final bool isScreenShare;
|
||||||
|
final bool enabledE2EE;
|
||||||
|
|
||||||
|
const ParticipantInfoWidget({
|
||||||
|
super.key,
|
||||||
|
this.title,
|
||||||
|
this.audioAvailable = true,
|
||||||
|
this.connectionQuality = ConnectionQuality.unknown,
|
||||||
|
this.isScreenShare = false,
|
||||||
|
this.enabledE2EE = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Container(
|
||||||
|
color: Colors.black.withOpacity(0.3),
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
isScreenShare
|
||||||
|
? const Padding(
|
||||||
|
padding: EdgeInsets.only(left: 5),
|
||||||
|
child: Icon(
|
||||||
|
Icons.monitor,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 5),
|
||||||
|
child: Icon(
|
||||||
|
audioAvailable ? Icons.mic : Icons.mic_off,
|
||||||
|
color: audioAvailable ? Colors.white : Colors.red,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (connectionQuality != ConnectionQuality.unknown)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 5),
|
||||||
|
child: Icon(
|
||||||
|
connectionQuality == ConnectionQuality.poor ? Icons.wifi_off_outlined : Icons.wifi,
|
||||||
|
color: {
|
||||||
|
ConnectionQuality.excellent: Colors.green,
|
||||||
|
ConnectionQuality.good: Colors.orange,
|
||||||
|
ConnectionQuality.poor: Colors.red,
|
||||||
|
}[connectionQuality],
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 5),
|
||||||
|
child: Icon(
|
||||||
|
enabledE2EE ? Icons.lock : Icons.lock_open,
|
||||||
|
color: enabledE2EE ? Colors.green : Colors.red,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
124
lib/widgets/chat/call/participant_stats.dart
Normal file
124
lib/widgets/chat/call/participant_stats.dart
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
|
import 'package:solian/models/call.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: Colors.black.withOpacity(0.3),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 8,
|
||||||
|
horizontal: 8,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: stats.entries.map((e) => Text('${e.key}: ${e.value}')).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -19,7 +19,7 @@ class NotificationNotifier extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _NotificationNotifierState extends State<NotificationNotifier> {
|
class _NotificationNotifierState extends State<NotificationNotifier> {
|
||||||
void connect() {
|
void connect() async {
|
||||||
final notify = ScaffoldMessenger.of(context).showSnackBar(
|
final notify = ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(AppLocalizations.of(context)!.connectingServer),
|
content: Text(AppLocalizations.of(context)!.connectingServer),
|
||||||
@ -30,19 +30,21 @@ class _NotificationNotifierState extends State<NotificationNotifier> {
|
|||||||
final auth = context.read<AuthProvider>();
|
final auth = context.read<AuthProvider>();
|
||||||
final nty = context.read<NotifyProvider>();
|
final nty = context.read<NotifyProvider>();
|
||||||
|
|
||||||
nty.fetch(auth);
|
if (await auth.isAuthorized()) {
|
||||||
nty.connect(auth).then((snapshot) {
|
nty.fetch(auth);
|
||||||
snapshot!.stream.listen(
|
nty.connect(auth).then((snapshot) {
|
||||||
(event) {
|
snapshot!.stream.listen(
|
||||||
final result = model.Notification.fromJson(jsonDecode(event));
|
(event) {
|
||||||
nty.onRemoteMessage(result);
|
final result = model.Notification.fromJson(jsonDecode(event));
|
||||||
},
|
nty.onRemoteMessage(result);
|
||||||
onError: (_, __) => connect(),
|
},
|
||||||
onDone: () => connect(),
|
onError: (_, __) => connect(),
|
||||||
);
|
onDone: () => connect(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
notify.close();
|
notify.close();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -7,7 +7,7 @@ project(runner LANGUAGES CXX)
|
|||||||
set(BINARY_NAME "solian")
|
set(BINARY_NAME "solian")
|
||||||
# The unique GTK application identifier for this application. See:
|
# The unique GTK application identifier for this application. See:
|
||||||
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
||||||
set(APPLICATION_ID "com.example.solian")
|
set(APPLICATION_ID "dev.solsynth.solian")
|
||||||
|
|
||||||
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||||
# versions of CMake.
|
# versions of CMake.
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include <file_selector_linux/file_selector_plugin.h>
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||||
|
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
||||||
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
||||||
#include <media_kit_video/media_kit_video_plugin.h>
|
#include <media_kit_video/media_kit_video_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
@ -19,6 +20,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
|||||||
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
||||||
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_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) media_kit_libs_linux_registrar =
|
g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin");
|
||||||
media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar);
|
media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar);
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
file_selector_linux
|
file_selector_linux
|
||||||
flutter_secure_storage_linux
|
flutter_secure_storage_linux
|
||||||
|
flutter_webrtc
|
||||||
media_kit_libs_linux
|
media_kit_libs_linux
|
||||||
media_kit_video
|
media_kit_video
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
|
@ -5,8 +5,12 @@
|
|||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import connectivity_plus
|
||||||
|
import device_info_plus
|
||||||
import file_selector_macos
|
import file_selector_macos
|
||||||
import flutter_secure_storage_macos
|
import flutter_secure_storage_macos
|
||||||
|
import flutter_webrtc
|
||||||
|
import livekit_client
|
||||||
import media_kit_libs_macos_video
|
import media_kit_libs_macos_video
|
||||||
import media_kit_video
|
import media_kit_video
|
||||||
import package_info_plus
|
import package_info_plus
|
||||||
@ -16,8 +20,12 @@ import url_launcher_macos
|
|||||||
import wakelock_plus
|
import wakelock_plus
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
|
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
||||||
|
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||||
|
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
|
||||||
|
LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin"))
|
||||||
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"))
|
||||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||||
|
@ -477,7 +477,7 @@
|
|||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.solian.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/solian.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/solian";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/solian.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/solian";
|
||||||
@ -492,7 +492,7 @@
|
|||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.solian.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/solian.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/solian";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/solian.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/solian";
|
||||||
@ -507,7 +507,7 @@
|
|||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.solian.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/solian.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/solian";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/solian.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/solian";
|
||||||
|
184
pubspec.lock
184
pubspec.lock
@ -81,6 +81,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.18.0"
|
version: "1.18.0"
|
||||||
|
connectivity_plus:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: connectivity_plus
|
||||||
|
sha256: db7a4e143dc72cc3cb2044ef9b052a7ebfe729513e6a82943bc3526f784365b8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.3"
|
||||||
|
connectivity_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: connectivity_plus_platform_interface
|
||||||
|
sha256: b6a56efe1e6675be240de39107281d4034b64ac23438026355b4234042a35adb
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
convert:
|
convert:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -105,6 +121,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.3"
|
version: "3.0.3"
|
||||||
|
cryptography:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cryptography
|
||||||
|
sha256: d146b76d33d94548cf035233fbc2f4338c1242fa119013bead807d033fc4ae05
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.0"
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -113,6 +137,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.8"
|
version: "1.0.8"
|
||||||
|
dart_webrtc:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dart_webrtc
|
||||||
|
sha256: b3a4f109c551a10170ece8fc79b5ca1b98223f24bcebc0f971d7fe35daad7a3b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.4"
|
||||||
dbus:
|
dbus:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -121,6 +153,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.10"
|
version: "0.7.10"
|
||||||
|
device_info_plus:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: device_info_plus
|
||||||
|
sha256: eead12d1a1ed83d8283ab4c2f3fca23ac4082f29f25f29dff0f758f57d06ec91
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "10.1.0"
|
||||||
|
device_info_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: device_info_plus_platform_interface
|
||||||
|
sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.0"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -137,6 +185,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.2"
|
||||||
|
file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file
|
||||||
|
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.0"
|
||||||
file_selector_linux:
|
file_selector_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -190,6 +246,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.5.0"
|
version: "4.5.0"
|
||||||
|
flutter_background:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_background
|
||||||
|
sha256: "035c31a738509d67ee70bbf174e5aa7db462c371e838ec8259700c5c4e7ca17f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.0"
|
||||||
flutter_carousel_widget:
|
flutter_carousel_widget:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -309,6 +373,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: "20eac28848a2dffb26cc2b2870a5164613904511a0b7e8f4825e31a2768175d2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.10.3"
|
||||||
go_router:
|
go_router:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -485,6 +557,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "3.0.0"
|
||||||
|
livekit_client:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: livekit_client
|
||||||
|
sha256: "9b7e471584b34d914dfea71ecbe4d1d5169690cc1055850509841827c489ddbb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.3"
|
||||||
logging:
|
logging:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -613,6 +693,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
|
nm:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: nm
|
||||||
|
sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.5.0"
|
||||||
oauth2:
|
oauth2:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -693,6 +781,54 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.2.1"
|
||||||
|
permission_handler:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: permission_handler
|
||||||
|
sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "11.3.1"
|
||||||
|
permission_handler_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_android
|
||||||
|
sha256: "1acac6bae58144b442f11e66621c062aead9c99841093c38f5bcdcc24c1c3474"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "12.0.5"
|
||||||
|
permission_handler_apple:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_apple
|
||||||
|
sha256: e9ad66020b89ff1b63908f247c2c6f931c6e62699b756ef8b3c4569350cd8662
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.4.4"
|
||||||
|
permission_handler_html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_html
|
||||||
|
sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.1"
|
||||||
|
permission_handler_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_platform_interface
|
||||||
|
sha256: "48d4fcf201a1dad93ee869ab0d4101d084f49136ec82a8a06ed9cfeacab9fd20"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.2.1"
|
||||||
|
permission_handler_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_windows
|
||||||
|
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.1"
|
||||||
petitparser:
|
petitparser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -709,6 +845,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.4"
|
version: "3.1.4"
|
||||||
|
platform_detect:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: platform_detect
|
||||||
|
sha256: "08f4ee79c0e1c4858d37e06b22352a3ebdef5466b613749a3adb03e703d4f5b0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.11"
|
||||||
plugin_platform_interface:
|
plugin_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -725,6 +869,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.9.0"
|
version: "3.9.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:
|
||||||
@ -733,6 +885,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.2"
|
version: "6.1.2"
|
||||||
|
pub_semver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pub_semver
|
||||||
|
sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.4"
|
||||||
safe_local_storage:
|
safe_local_storage:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -789,6 +949,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.3"
|
version: "0.1.3"
|
||||||
|
sdp_transform:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sdp_transform
|
||||||
|
sha256: "73e412a5279a5c2de74001535208e20fff88f225c9a4571af0f7146202755e45"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.2"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -1026,6 +1194,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.5"
|
version: "2.4.5"
|
||||||
|
webrtc_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: webrtc_interface
|
||||||
|
sha256: abec3ab7956bd5ac539cf34a42fa0c82ea26675847c0966bb85160400eea9388
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.0"
|
||||||
webview_flutter:
|
webview_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1066,6 +1242,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.4.0"
|
version: "5.4.0"
|
||||||
|
win32_registry:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: win32_registry
|
||||||
|
sha256: "10589e0d7f4e053f2c61023a31c9ce01146656a70b7b7f0828c0b46d7da2a9bb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.3"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -60,6 +60,10 @@ dependencies:
|
|||||||
web_socket_channel: ^2.4.5
|
web_socket_channel: ^2.4.5
|
||||||
badges: ^3.1.2
|
badges: ^3.1.2
|
||||||
flutter_animate: ^4.5.0
|
flutter_animate: ^4.5.0
|
||||||
|
livekit_client: ^2.1.3
|
||||||
|
permission_handler: ^11.3.1
|
||||||
|
flutter_webrtc: ^0.10.3
|
||||||
|
flutter_background: ^1.2.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
@ -6,22 +6,34 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
||||||
#include <file_selector_windows/file_selector_windows.h>
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||||
|
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
||||||
|
#include <livekit_client/live_kit_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>
|
||||||
|
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||||
#include <screen_brightness_windows/screen_brightness_windows_plugin.h>
|
#include <screen_brightness_windows/screen_brightness_windows_plugin.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
|
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
||||||
FileSelectorWindowsRegisterWithRegistrar(
|
FileSelectorWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||||
|
FlutterWebRTCPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FlutterWebRTCPlugin"));
|
||||||
|
LiveKitPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("LiveKitPlugin"));
|
||||||
MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar(
|
MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi"));
|
registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi"));
|
||||||
MediaKitVideoPluginCApiRegisterWithRegistrar(
|
MediaKitVideoPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi"));
|
registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi"));
|
||||||
|
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||||
ScreenBrightnessWindowsPluginRegisterWithRegistrar(
|
ScreenBrightnessWindowsPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("ScreenBrightnessWindowsPlugin"));
|
registry->GetRegistrarForPlugin("ScreenBrightnessWindowsPlugin"));
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
|
@ -3,10 +3,14 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
connectivity_plus
|
||||||
file_selector_windows
|
file_selector_windows
|
||||||
flutter_secure_storage_windows
|
flutter_secure_storage_windows
|
||||||
|
flutter_webrtc
|
||||||
|
livekit_client
|
||||||
media_kit_libs_windows_video
|
media_kit_libs_windows_video
|
||||||
media_kit_video
|
media_kit_video
|
||||||
|
permission_handler_windows
|
||||||
screen_brightness_windows
|
screen_brightness_windows
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user