Simulcast, Screen sharing & Various improvements (#4)
* Respect `RTCIceTransportPolicy` enum and organize * Simplify syntax where possible etc. * Combine `VideoPreset` and `VideoPresets` * Default values for `ConnectOptions` * Build URI instead of String manipulation * Slight modifications to Exception * `LiveKitTheme` for example * `VideoEncoding` class * Organize imports * First simulcast implementation * Remove unnecessary try-catches * Update Android settings * Remember uri and token * example improvements * `fit` parameter for VideoTrackRenderer * Simulcast option for example * Pass `defaultPublishOptions` * Show only `VideoQuality` * Pass tests * Better buildUri logic * Named parameter to positional * Explicit imports * `VideoParameter` instead of `VideoPreset` * Use `mediaTrack.getSettings` when possible * Safer dispose logic * Safer `PCTransport` Update transport.dart * Synchronized events for `SignalClient` * Use logger instead of print * Make example compile for iOS * First screen share implementation * Make example work with screen share * Example improvement * Code optimization * Don't depend on web_socket_channel * Fix: Unpublish track bug * Show participant mute state & identity * Update protos * Remote mute/unmute * iOS Background mode * Separate `createCameraTrack` and `createScreenTrack` * Clean up * PB fix * format * Fix analyzer warning * Android clean up * Update README.md * Clean up
This commit is contained in:
@@ -76,3 +76,4 @@ build/
|
||||
|
||||
# VS Code
|
||||
.vscode/
|
||||
*.code-workspace
|
||||
|
||||
@@ -6,7 +6,19 @@ This package is published to pub.dev as [livekit_client](https://pub.dev/package
|
||||
|
||||
## Docs
|
||||
|
||||
Docs and guides at [https://docs.livekit.io](https://docs.livekit.io)
|
||||
More Docs and guides are available at [https://docs.livekit.io](https://docs.livekit.io)
|
||||
|
||||
## Current supported features
|
||||
|
||||
| Feature | Subscribe/Publish | Simulcast | Background mode | Screen sharing |
|
||||
| :-----: | :---------------: | :-------: | :-------------: | :------------: |
|
||||
| Web | 🟢 | 🟢 | 🟢 | 🟢 |
|
||||
| iOS | 🟢 | 🟡 | 🟡 | 🔴 |
|
||||
| Android | 🟢 | 🟡 | 🟡 | 🔴 |
|
||||
|
||||
🟢 = Available
|
||||
🟡 = Coming soon (Work in progress)
|
||||
🔴 = Not currently available (Possibly in the future)
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -23,13 +35,25 @@ dependencies:
|
||||
Camera and microphone usage need to be declared in your `Info.plist` file.
|
||||
|
||||
```xml
|
||||
...
|
||||
<dict>
|
||||
...
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>$(PRODUCT_NAME) uses your camera</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>$(PRODUCT_NAME) uses your microphone</string>
|
||||
</dict>
|
||||
```
|
||||
|
||||
Your application can still run the voice call when it is switched to the background if the background mode is enabled. Select the app target in Xcode, click the Capabilities tab, enable Background Modes, and check **Audio, AirPlay, and Picture in Picture**.
|
||||
|
||||
Your `Info.plist` should have the following entries.
|
||||
|
||||
```xml
|
||||
<dict>
|
||||
...
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
```
|
||||
|
||||
### Android
|
||||
|
||||
@@ -37,3 +37,5 @@ linter:
|
||||
# Additional recommended rules
|
||||
#
|
||||
prefer_single_quotes: true
|
||||
unnecessary_brace_in_string_interps: false
|
||||
unawaited_futures: true
|
||||
|
||||
@@ -37,6 +37,8 @@ linter:
|
||||
# Additional recommended rules
|
||||
#
|
||||
prefer_single_quotes: true
|
||||
unnecessary_brace_in_string_interps: false
|
||||
unawaited_futures: true
|
||||
|
||||
#
|
||||
# Turn off avoid_print for example projects
|
||||
|
||||
@@ -32,14 +32,14 @@ android {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
// compileOptions {
|
||||
// sourceCompatibility JavaVersion.VERSION_1_8
|
||||
// targetCompatibility JavaVersion.VERSION_1_8
|
||||
// }
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "io.livekit.flutter_example"
|
||||
applicationId "io.livekit.example"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 30
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.livekit_example">
|
||||
package="io.livekit.example">
|
||||
<!-- Flutter needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.livekit_example">
|
||||
|
||||
package="io.livekit.example">
|
||||
<uses-feature android:name="android.hardware.camera" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
@@ -8,7 +7,7 @@
|
||||
<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.FOREGROUND_SERVICE"/>
|
||||
<application
|
||||
android:label="LiveKit Example"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
@@ -41,7 +40,6 @@
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.example.livekit_example
|
||||
package io.livekit.example
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.livekit_example">
|
||||
package="io.livekit.example">
|
||||
<!-- Flutter needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
|
||||
@@ -6,7 +6,7 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.1.0'
|
||||
classpath 'com.android.tools.build:gradle:7.0.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
|
||||
|
||||
+10
-1
@@ -1,5 +1,5 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
# platform :ios, '9.0'
|
||||
platform :ios, '12.1'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
@@ -37,5 +37,14 @@ end
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
|
||||
#
|
||||
# Fix deployment target issue
|
||||
# https://stackoverflow.com/questions/63973136/the-ios-deployment-target-iphoneos-deployment-target-is-set-to-8-0-in-flutter
|
||||
#
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.1'
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,11 +8,14 @@ PODS:
|
||||
- Libyuv (1703)
|
||||
- path_provider (0.0.1):
|
||||
- Flutter
|
||||
- shared_preferences (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
|
||||
- path_provider (from `.symlinks/plugins/path_provider/ios`)
|
||||
- shared_preferences (from `.symlinks/plugins/shared_preferences/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
@@ -26,6 +29,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_webrtc/ios"
|
||||
path_provider:
|
||||
:path: ".symlinks/plugins/path_provider/ios"
|
||||
shared_preferences:
|
||||
:path: ".symlinks/plugins/shared_preferences/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c
|
||||
@@ -33,7 +38,8 @@ SPEC CHECKSUMS:
|
||||
GoogleWebRTC: b39a78c4f5cc6b0323415b9233db03a2faa7b0f0
|
||||
Libyuv: 5f79ced0ee66e60a612ca97de1e6ccacd187a437
|
||||
path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c
|
||||
shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d
|
||||
|
||||
PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c
|
||||
PODFILE CHECKSUM: 6055d9653e1011c0b3b671abb92cdea979357e63
|
||||
|
||||
COCOAPODS: 1.10.2
|
||||
COCOAPODS: 1.10.1
|
||||
|
||||
@@ -357,14 +357,14 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 9Z5V633C2T;
|
||||
DEVELOPMENT_TEAM = J48VV6BZV9;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.livekitExample;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = example.livekit.io;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
@@ -489,14 +489,14 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 9Z5V633C2T;
|
||||
DEVELOPMENT_TEAM = J48VV6BZV9;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.livekitExample;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = example.livekit.io;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
@@ -515,14 +515,14 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 9Z5V633C2T;
|
||||
DEVELOPMENT_TEAM = J48VV6BZV9;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.livekitExample;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = example.livekit.io;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
|
||||
@@ -22,6 +22,14 @@
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>$(PRODUCT_NAME) will use camera</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>$(PRODUCT_NAME) will use microphone</string>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
@@ -41,9 +49,5 @@
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>$(PRODUCT_NAME) will use camera</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>$(PRODUCT_NAME) will use microphone</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
extension LKExampleExt on BuildContext {
|
||||
//
|
||||
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'),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
+11
-102
@@ -1,7 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:livekit_example/theme.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'room.dart';
|
||||
|
||||
import 'pages/connect.dart';
|
||||
|
||||
void main() {
|
||||
// configure logs for debugging
|
||||
@@ -10,113 +11,21 @@ void main() {
|
||||
print('${record.level.name}: ${record.time}: ${record.message}');
|
||||
});
|
||||
|
||||
runApp(const MyApp());
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
runApp(const LiveKitExampleApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
class LiveKitExampleApp extends StatelessWidget {
|
||||
//
|
||||
const MyApp({
|
||||
const LiveKitExampleApp({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => MaterialApp(
|
||||
title: 'LiveKit Demo',
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.deepPurple,
|
||||
),
|
||||
home: const PreConnectWidget(
|
||||
url: '<livekit_host>',
|
||||
token: '<access_token>',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class PreConnectWidget extends StatefulWidget {
|
||||
//
|
||||
final String url;
|
||||
final String token;
|
||||
|
||||
const PreConnectWidget({
|
||||
required this.url,
|
||||
required this.token,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _PreConnectWidgetState();
|
||||
}
|
||||
|
||||
class _PreConnectWidgetState extends State<PreConnectWidget> {
|
||||
//
|
||||
final _urlCtrl = TextEditingController();
|
||||
final _tokenCtrl = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_urlCtrl.text = widget.url;
|
||||
_tokenCtrl.text = widget.token;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_urlCtrl.dispose();
|
||||
_tokenCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _connect(BuildContext context) async {
|
||||
try {
|
||||
print('Connecting with url: ${_urlCtrl.text}, token: ${_tokenCtrl.text}...');
|
||||
|
||||
final room = await LiveKitClient.connect(
|
||||
_urlCtrl.text,
|
||||
_tokenCtrl.text,
|
||||
);
|
||||
|
||||
Navigator.push<void>(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) {
|
||||
return RoomWidget(room);
|
||||
}),
|
||||
);
|
||||
} catch (e) {
|
||||
print('could not connect $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Connect to LiveKit'),
|
||||
),
|
||||
body: Center(
|
||||
child: Container(
|
||||
// width: 250,
|
||||
alignment: Alignment.center,
|
||||
margin: const EdgeInsets.all(10),
|
||||
child: Column(
|
||||
children: [
|
||||
TextField(
|
||||
controller: _urlCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'URL',
|
||||
),
|
||||
),
|
||||
TextField(
|
||||
controller: _tokenCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Token',
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => _connect(context),
|
||||
child: const Text('Connect'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
title: 'LiveKit Flutter Example',
|
||||
theme: LiveKitTheme().buildThemeData(context),
|
||||
home: const ConnectPage(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../exts.dart';
|
||||
import 'room.dart';
|
||||
|
||||
class ConnectPage extends StatefulWidget {
|
||||
//
|
||||
const ConnectPage({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _ConnectPageState();
|
||||
}
|
||||
|
||||
class _ConnectPageState extends State<ConnectPage> {
|
||||
//
|
||||
static const _storeKeyUri = 'uri';
|
||||
static const _storeKeyToken = 'token';
|
||||
static const _storeKeySimulcast = 'simulcast';
|
||||
|
||||
final _uriCtrl = TextEditingController();
|
||||
final _tokenCtrl = TextEditingController();
|
||||
bool _simulcast = false;
|
||||
bool _busy = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_readPrefs();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_uriCtrl.dispose();
|
||||
_tokenCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _readPrefs() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_uriCtrl.text = prefs.getString(_storeKeyUri) ?? '';
|
||||
_tokenCtrl.text = prefs.getString(_storeKeyToken) ?? '';
|
||||
setState(() {
|
||||
_simulcast = prefs.getBool(_storeKeySimulcast) ?? false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _writePrefs() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_storeKeyUri, _uriCtrl.text);
|
||||
await prefs.setString(_storeKeyToken, _tokenCtrl.text);
|
||||
await prefs.setBool(_storeKeySimulcast, _simulcast);
|
||||
}
|
||||
|
||||
Future<void> _connect(BuildContext ctx) async {
|
||||
//
|
||||
try {
|
||||
setState(() {
|
||||
_busy = true;
|
||||
});
|
||||
|
||||
print('Connecting with url: ${_uriCtrl.text}, token: ${_tokenCtrl.text}...');
|
||||
|
||||
final room = await LiveKitClient.connect(
|
||||
_uriCtrl.text,
|
||||
_tokenCtrl.text,
|
||||
options: ConnectOptions(
|
||||
defaultPublishOptions: TrackPublishOptions(
|
||||
simulcast: _simulcast,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Save for next time
|
||||
await _writePrefs();
|
||||
|
||||
await Navigator.push<void>(
|
||||
ctx,
|
||||
MaterialPageRoute(builder: (_) => RoomPage(room)),
|
||||
);
|
||||
} catch (error) {
|
||||
print('could not connect $error');
|
||||
await ctx.showErrorDialog(error);
|
||||
} finally {
|
||||
setState(() {
|
||||
_busy = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _setSimulcast(bool? value) async {
|
||||
if (value == null || _simulcast == value) return;
|
||||
setState(() {
|
||||
_simulcast = value;
|
||||
});
|
||||
// await _writePrefs();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Connect to LiveKit'),
|
||||
),
|
||||
body: Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 20,
|
||||
horizontal: 20,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Theme.of(context).colorScheme.secondary),
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 320,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: _uriCtrl,
|
||||
decoration: const InputDecoration(labelText: 'URL'),
|
||||
),
|
||||
TextField(
|
||||
controller: _tokenCtrl,
|
||||
decoration: const InputDecoration(labelText: 'Token'),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 20),
|
||||
child: CheckboxListTile(
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
onChanged: (value) => _setSimulcast(value),
|
||||
title: const Text('Use Simulcast'),
|
||||
value: _simulcast,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 20),
|
||||
child: ElevatedButton(
|
||||
onPressed: _busy ? null : () => _connect(context),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (_busy)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(right: 10),
|
||||
child: SizedBox(
|
||||
height: 15,
|
||||
width: 15,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Text('Connect'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../widgets/controls.dart';
|
||||
import '../widgets/participant.dart';
|
||||
|
||||
class RoomPage extends StatefulWidget {
|
||||
//
|
||||
final Room room;
|
||||
|
||||
const RoomPage(
|
||||
this.room, {
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _RoomPageState();
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomPageState extends State<RoomPage> with RoomDelegate {
|
||||
// BuildContext? _lastContext;
|
||||
//
|
||||
List<Participant> participants = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.room.delegate = this;
|
||||
widget.room.addListener(_onChange);
|
||||
_onConnected();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.room.delegate = null;
|
||||
widget.room.removeListener(_onChange);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onConnected() async {
|
||||
// video will fail when running in ios simulator
|
||||
try {
|
||||
final localVideo = await LocalVideoTrack.createCameraTrack(); // Defaults to camera
|
||||
await widget.room.localParticipant.publishVideoTrack(
|
||||
localVideo,
|
||||
// options: TrackPublishOptions(
|
||||
// // simulcast: true,
|
||||
// videoEncoding: VideoParameters.presetQVGA169.encoding,
|
||||
// ),
|
||||
);
|
||||
} catch (e) {
|
||||
print('could not publish video: $e');
|
||||
}
|
||||
|
||||
final localAudio = await LocalAudioTrack.create();
|
||||
await widget.room.localParticipant.publishAudioTrack(localAudio);
|
||||
sortParticipants();
|
||||
}
|
||||
|
||||
void _onChange() {
|
||||
sortParticipants();
|
||||
}
|
||||
|
||||
void sortParticipants() {
|
||||
List<Participant> participants = [];
|
||||
participants.addAll(widget.room.participants.values);
|
||||
// sort speakers for the grid
|
||||
participants.sort((a, b) {
|
||||
// loudest speaker first
|
||||
if (a.isSpeaking && b.isSpeaking) {
|
||||
if (a.audioLevel > b.audioLevel) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// last spoken at
|
||||
final aSpokeAt = a.lastSpokeAt?.millisecondsSinceEpoch ?? 0;
|
||||
final bSpokeAt = b.lastSpokeAt?.millisecondsSinceEpoch ?? 0;
|
||||
|
||||
if (aSpokeAt != bSpokeAt) {
|
||||
return aSpokeAt > bSpokeAt ? -1 : 1;
|
||||
}
|
||||
|
||||
// video on
|
||||
if (a.hasVideo != b.hasVideo) {
|
||||
return a.hasVideo ? -1 : 1;
|
||||
}
|
||||
|
||||
// joinedAt
|
||||
return a.joinedAt.millisecondsSinceEpoch - b.joinedAt.millisecondsSinceEpoch;
|
||||
});
|
||||
|
||||
if (participants.length > 1) {
|
||||
participants.insert(1, widget.room.localParticipant);
|
||||
} else {
|
||||
participants.add(widget.room.localParticipant);
|
||||
}
|
||||
setState(() {
|
||||
this.participants = participants;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onDisconnected() {
|
||||
// final context = _lastContext;
|
||||
print('disconnected: $context');
|
||||
// if (context != null) {
|
||||
Navigator.pop(context);
|
||||
// }
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
// with a provider, any child/descendent widget can be updated if they
|
||||
// are a Consumer of Room.
|
||||
body: ChangeNotifierProvider.value(
|
||||
value: widget.room,
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: participants.isNotEmpty
|
||||
? ParticipantWidget(participants.first)
|
||||
: Container()),
|
||||
SizedBox(
|
||||
height: 100,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: math.max(0, participants.length - 1),
|
||||
itemBuilder: (BuildContext context, int index) => Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: ParticipantWidget(participants[index + 1], quality: VideoQuality.LOW),
|
||||
),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
top: false,
|
||||
child: ControlsWidget(widget.room),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:livekit_example/src/controls.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class RoomWidget extends StatefulWidget {
|
||||
//
|
||||
final Room room;
|
||||
|
||||
const RoomWidget(
|
||||
this.room, {
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _RoomState();
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomState extends State<RoomWidget> with RoomDelegate {
|
||||
BuildContext? _lastContext;
|
||||
List<Participant> participants = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.room.delegate = this;
|
||||
widget.room.addListener(_onChange);
|
||||
_onConnected();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.room.delegate = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onConnected() async {
|
||||
// video will fail when running in ios simulator
|
||||
try {
|
||||
final localVideo = await LocalVideoTrack.createCameraTrack();
|
||||
await widget.room.localParticipant.publishVideoTrack(localVideo);
|
||||
} catch (e) {
|
||||
print('could not publish video: $e');
|
||||
}
|
||||
|
||||
final localAudio = await LocalAudioTrack.createTrack();
|
||||
await widget.room.localParticipant.publishAudioTrack(localAudio);
|
||||
sortParticipants();
|
||||
}
|
||||
|
||||
void _onChange() {
|
||||
sortParticipants();
|
||||
}
|
||||
|
||||
void sortParticipants() {
|
||||
List<Participant> participants = [];
|
||||
participants.addAll(widget.room.participants.values);
|
||||
// sort speakers for the grid
|
||||
participants.sort((a, b) {
|
||||
// loudest speaker first
|
||||
if (a.isSpeaking && b.isSpeaking) {
|
||||
if (a.audioLevel > b.audioLevel) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// last spoken at
|
||||
final aSpokeAt = a.lastSpokeAt?.millisecondsSinceEpoch ?? 0;
|
||||
final bSpokeAt = b.lastSpokeAt?.millisecondsSinceEpoch ?? 0;
|
||||
|
||||
if (aSpokeAt != bSpokeAt) {
|
||||
return aSpokeAt > bSpokeAt ? -1 : 1;
|
||||
}
|
||||
|
||||
// video on
|
||||
if (a.hasVideo != b.hasVideo) {
|
||||
return a.hasVideo ? -1 : 1;
|
||||
}
|
||||
|
||||
// joinedAt
|
||||
return a.joinedAt.millisecondsSinceEpoch - b.joinedAt.millisecondsSinceEpoch;
|
||||
});
|
||||
|
||||
if (participants.length > 1) {
|
||||
participants.insert(1, widget.room.localParticipant);
|
||||
} else {
|
||||
participants.add(widget.room.localParticipant);
|
||||
}
|
||||
setState(() {
|
||||
this.participants = participants;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onDisconnected() {
|
||||
final context = _lastContext;
|
||||
print('disconnected: $context');
|
||||
if (context != null) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_lastContext = context;
|
||||
|
||||
final mainWidgets = <Widget>[];
|
||||
final participants = this.participants;
|
||||
if (participants.isNotEmpty) {
|
||||
mainWidgets.add(Expanded(child: VideoView(participants.first)));
|
||||
} else {
|
||||
mainWidgets.add(Expanded(child: Container()));
|
||||
}
|
||||
|
||||
if (participants.length > 1) {
|
||||
final videoList = ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: participants.length - 1,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return Container(
|
||||
width: 100,
|
||||
height: 60,
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: VideoView(participants[index + 1], quality: VideoQuality.LOW),
|
||||
);
|
||||
},
|
||||
);
|
||||
mainWidgets.add(SizedBox(
|
||||
height: 60,
|
||||
child: videoList,
|
||||
));
|
||||
}
|
||||
|
||||
mainWidgets.add(Controls(widget.room));
|
||||
return MaterialApp(
|
||||
title: 'LiveKit Video Room',
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.deepPurple,
|
||||
),
|
||||
home: Scaffold(
|
||||
// with a provider, any child/descendent widget can be updated if they
|
||||
// are a Consumer of Room.
|
||||
body: ChangeNotifierProvider.value(
|
||||
value: widget.room,
|
||||
child: Column(
|
||||
children: mainWidgets,
|
||||
))));
|
||||
}
|
||||
}
|
||||
|
||||
// displays a participant in view
|
||||
class VideoView extends StatefulWidget {
|
||||
//
|
||||
final Participant participant;
|
||||
final VideoQuality quality;
|
||||
|
||||
const VideoView(
|
||||
this.participant, {
|
||||
this.quality = VideoQuality.MEDIUM,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _VideoViewState();
|
||||
}
|
||||
}
|
||||
|
||||
class _VideoViewState extends State<VideoView> with ParticipantDelegate {
|
||||
TrackPublication? videoPub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.participant.addListener(_onParticipantChanged);
|
||||
_onParticipantChanged();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.participant.removeListener(_onParticipantChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant VideoView oldWidget) {
|
||||
oldWidget.participant.removeListener(_onParticipantChanged);
|
||||
widget.participant.addListener(_onParticipantChanged);
|
||||
_onParticipantChanged();
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
// register for change so Flutter will re-build the widget upon change
|
||||
void _onParticipantChanged() {
|
||||
final subscribedVideos = widget.participant.videoTracks.values.where((pub) {
|
||||
return pub.kind == TrackType.VIDEO && !pub.isScreenShare && pub.subscribed;
|
||||
});
|
||||
setState(() {
|
||||
if (subscribedVideos.isNotEmpty) {
|
||||
final videoPub = subscribedVideos.first;
|
||||
if (videoPub is RemoteTrackPublication) {
|
||||
videoPub.videoQuality = widget.quality;
|
||||
}
|
||||
// when muted, show placeholder
|
||||
if (!videoPub.muted) {
|
||||
this.videoPub = videoPub;
|
||||
return;
|
||||
}
|
||||
}
|
||||
videoPub = null;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final videoPub = this.videoPub;
|
||||
if (videoPub != null) {
|
||||
return VideoTrackRenderer(videoPub.track as VideoTrack);
|
||||
} else {
|
||||
return Container(
|
||||
color: Colors.grey,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
|
||||
class Controls extends StatefulWidget {
|
||||
//
|
||||
final Room room;
|
||||
final LocalParticipant participant;
|
||||
|
||||
Controls(
|
||||
this.room, {
|
||||
Key? key,
|
||||
}) : participant = room.localParticipant,
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _ControlsState();
|
||||
}
|
||||
}
|
||||
|
||||
class _ControlsState extends State<Controls> {
|
||||
CameraPosition position = CameraPosition.front;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
participant.addListener(_onChange);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
participant.removeListener(_onChange);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
LocalParticipant get participant => widget.participant;
|
||||
|
||||
void _onChange() {
|
||||
// trigger refresh
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _muteAudio() {
|
||||
if (participant.hasAudio) {
|
||||
final audioPub = participant.audioTracks.values.first;
|
||||
audioPub.muted = true;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _unmuteAudio() async {
|
||||
if (participant.hasAudio) {
|
||||
final audioPub = participant.audioTracks.values.first;
|
||||
audioPub.muted = false;
|
||||
} else {
|
||||
// publish audio track
|
||||
final audioTrack = await LocalAudioTrack.createTrack();
|
||||
await participant.publishAudioTrack(audioTrack);
|
||||
}
|
||||
}
|
||||
|
||||
void _muteVideo() {
|
||||
if (participant.hasVideo) {
|
||||
final videoPub = participant.videoTracks.values.first;
|
||||
videoPub.muted = true;
|
||||
}
|
||||
}
|
||||
|
||||
void _unmuteVideo() async {
|
||||
if (participant.hasVideo) {
|
||||
final videoPub = participant.videoTracks.values.first;
|
||||
videoPub.muted = false;
|
||||
} else {
|
||||
// publish audio track
|
||||
final videoTrack = await LocalVideoTrack.createCameraTrack();
|
||||
await participant.publishVideoTrack(videoTrack);
|
||||
}
|
||||
}
|
||||
|
||||
void _setCameraPosition(TrackPublication? pub, CameraPosition position) async {
|
||||
if (this.position == position) {
|
||||
return;
|
||||
}
|
||||
LocalVideoTrack? track;
|
||||
if (pub?.track is LocalVideoTrack) {
|
||||
track = pub!.track as LocalVideoTrack;
|
||||
}
|
||||
|
||||
if (track == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await track.restartTrack(LocalVideoTrackOptions(position: position));
|
||||
} catch (e) {
|
||||
print('could not restart track: $e');
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
this.position = position;
|
||||
});
|
||||
}
|
||||
|
||||
void _exit() {
|
||||
widget.room.disconnect();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final buttons = <Widget>[];
|
||||
|
||||
// mute audio
|
||||
if (participant.hasAudio && !participant.isMuted) {
|
||||
buttons.add(
|
||||
IconButton(
|
||||
onPressed: _muteAudio,
|
||||
icon: const Icon(Icons.mic_rounded),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
buttons.add(
|
||||
IconButton(
|
||||
onPressed: _unmuteAudio,
|
||||
icon: const Icon(Icons.mic_off_rounded),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// mute video
|
||||
TrackPublication? videoPub;
|
||||
if (participant.hasVideo) {
|
||||
videoPub = participant.videoTracks.values.first;
|
||||
}
|
||||
|
||||
final videoEnabled = videoPub != null && !videoPub.muted;
|
||||
if (videoEnabled) {
|
||||
buttons.add(IconButton(
|
||||
onPressed: _muteVideo,
|
||||
icon: const Icon(Icons.videocam_rounded),
|
||||
));
|
||||
} else {
|
||||
buttons.add(IconButton(
|
||||
onPressed: _unmuteVideo,
|
||||
icon: const Icon(Icons.videocam_off_rounded),
|
||||
));
|
||||
}
|
||||
|
||||
if (position == CameraPosition.front) {
|
||||
buttons.add(IconButton(
|
||||
icon: const Icon(Icons.video_camera_front_rounded),
|
||||
onPressed: videoEnabled
|
||||
? () {
|
||||
_setCameraPosition(videoPub, CameraPosition.back);
|
||||
}
|
||||
: null,
|
||||
));
|
||||
} else {
|
||||
buttons.add(IconButton(
|
||||
icon: const Icon(Icons.video_camera_back_rounded),
|
||||
onPressed: videoEnabled
|
||||
? () {
|
||||
_setCameraPosition(videoPub, CameraPosition.front);
|
||||
}
|
||||
: null,
|
||||
));
|
||||
}
|
||||
|
||||
buttons.add(IconButton(
|
||||
onPressed: _exit,
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
));
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: buttons,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
//
|
||||
// Flutter has a color profile issue so colors will look different
|
||||
// on Apple devices.
|
||||
// https://github.com/flutter/flutter/issues/55092
|
||||
// https://github.com/flutter/flutter/issues/39113
|
||||
//
|
||||
class LiveKitTheme {
|
||||
//
|
||||
final bgColor = Colors.black;
|
||||
final textColor = Colors.white;
|
||||
final cardColor = const Color(0xFF00163c);
|
||||
final accentColor = const Color(0xFF2d6aef);
|
||||
|
||||
ThemeData buildThemeData(BuildContext ctx) => ThemeData(
|
||||
backgroundColor: bgColor,
|
||||
// accentColor: accentColor,
|
||||
colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.blue),
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor: cardColor,
|
||||
),
|
||||
cardColor: cardColor,
|
||||
scaffoldBackgroundColor: bgColor,
|
||||
canvasColor: bgColor,
|
||||
iconTheme: IconThemeData(
|
||||
color: textColor,
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
foregroundColor: MaterialStateProperty.all<Color>(Colors.white),
|
||||
// backgroundColor: MaterialStateProperty.all<Color>(accentColor),
|
||||
backgroundColor: MaterialStateProperty.resolveWith((states) {
|
||||
if (states.contains(MaterialState.disabled)) return accentColor.withOpacity(0.5);
|
||||
return accentColor;
|
||||
}),
|
||||
),
|
||||
),
|
||||
checkboxTheme: CheckboxThemeData(
|
||||
checkColor: MaterialStateProperty.all(Colors.white),
|
||||
fillColor: MaterialStateProperty.all(accentColor),
|
||||
),
|
||||
dialogBackgroundColor: cardColor,
|
||||
textTheme: GoogleFonts.latoTextTheme(
|
||||
Theme.of(ctx).textTheme,
|
||||
).apply(
|
||||
displayColor: textColor,
|
||||
bodyColor: textColor,
|
||||
decorationColor: textColor,
|
||||
),
|
||||
hintColor: Colors.red,
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
labelStyle: TextStyle(
|
||||
color: textColor.withOpacity(.5),
|
||||
),
|
||||
enabledBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: textColor.withOpacity(0.1)),
|
||||
),
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: accentColor),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import 'package:eva_icons_flutter/eva_icons_flutter.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
class ControlsWidget extends StatefulWidget {
|
||||
//
|
||||
final Room room;
|
||||
final LocalParticipant participant;
|
||||
|
||||
ControlsWidget(
|
||||
this.room, {
|
||||
Key? key,
|
||||
}) : participant = room.localParticipant,
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _ControlsWidgetState();
|
||||
}
|
||||
|
||||
class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
//
|
||||
CameraPosition position = CameraPosition.front;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
participant.addListener(_onChange);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
participant.removeListener(_onChange);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
LocalParticipant get participant => widget.participant;
|
||||
|
||||
void _onChange() {
|
||||
// trigger refresh
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _muteAudio() {
|
||||
if (participant.hasAudio) {
|
||||
final audioPub = participant.audioTracks.first;
|
||||
audioPub.muted = true;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _unmuteAudio() async {
|
||||
if (participant.hasAudio) {
|
||||
final audioPub = participant.audioTracks.first;
|
||||
audioPub.muted = false;
|
||||
} else {
|
||||
// publish audio track
|
||||
final audioTrack = await LocalAudioTrack.create();
|
||||
await participant.publishAudioTrack(audioTrack);
|
||||
}
|
||||
}
|
||||
|
||||
void _muteVideo() {
|
||||
if (participant.hasVideo) {
|
||||
final videoPub = participant.videoTracks.first;
|
||||
videoPub.muted = true;
|
||||
}
|
||||
}
|
||||
|
||||
void _unmuteVideo() async {
|
||||
if (participant.hasVideo) {
|
||||
print('Un-muting video');
|
||||
final videoPub = participant.videoTracks.first;
|
||||
videoPub.muted = false;
|
||||
} else {
|
||||
// publish audio track
|
||||
final videoTrack = await LocalVideoTrack.createCameraTrack();
|
||||
await participant.publishVideoTrack(videoTrack);
|
||||
}
|
||||
}
|
||||
|
||||
void _toggleCamera() async {
|
||||
//
|
||||
final track = participant.videoTracks.firstOrNull?.track as LocalVideoTrack?;
|
||||
if (track == null) return;
|
||||
|
||||
try {
|
||||
final newPosition = position.swap();
|
||||
await track.setCameraPosition(newPosition);
|
||||
setState(() {
|
||||
position = newPosition;
|
||||
});
|
||||
} catch (error) {
|
||||
print('could not restart track: $error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void _shareScreen() async {
|
||||
//
|
||||
final lp = widget.room.localParticipant;
|
||||
|
||||
for (final tracks in lp.videoTracks) {
|
||||
await lp.unpublishTrack(tracks.track!);
|
||||
}
|
||||
|
||||
try {
|
||||
final screenTrack = await LocalVideoTrack.createScreenTrack(); // Defaults to camera
|
||||
await widget.room.localParticipant.publishVideoTrack(
|
||||
screenTrack,
|
||||
);
|
||||
} catch (e) {
|
||||
print('could not publish video: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _exit() {
|
||||
widget.room.disconnect();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// mute audio
|
||||
final canMute = participant.hasAudio && !participant.isMuted;
|
||||
|
||||
final videoPub = participant.videoTracks.firstOrNull;
|
||||
final videoEnabled = videoPub != null && !videoPub.muted;
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (canMute)
|
||||
IconButton(
|
||||
onPressed: _muteAudio,
|
||||
icon: const Icon(EvaIcons.mic),
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
onPressed: _unmuteAudio,
|
||||
icon: const Icon(EvaIcons.micOff),
|
||||
),
|
||||
if (videoEnabled)
|
||||
IconButton(
|
||||
onPressed: _muteVideo,
|
||||
icon: const Icon(EvaIcons.video),
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
onPressed: _unmuteVideo,
|
||||
icon: const Icon(EvaIcons.videoOff),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(position == CameraPosition.back ? EvaIcons.camera : EvaIcons.person),
|
||||
onPressed: () => _toggleCamera(),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(EvaIcons.monitor),
|
||||
onPressed: () => _shareScreen(),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _exit,
|
||||
icon: const Icon(EvaIcons.closeCircle),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'package:eva_icons_flutter/eva_icons_flutter.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
class NoVideoWidget extends StatelessWidget {
|
||||
//
|
||||
const NoVideoWidget({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
alignment: Alignment.center,
|
||||
child: LayoutBuilder(
|
||||
builder: (ctx, constraints) => Icon(
|
||||
EvaIcons.videoOffOutline,
|
||||
color: Theme.of(ctx).colorScheme.secondary,
|
||||
size: math.min(constraints.maxHeight, constraints.maxWidth) * 0.3,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
|
||||
import 'no_video.dart';
|
||||
import 'participant_info.dart';
|
||||
|
||||
class ParticipantWidget extends StatefulWidget {
|
||||
//
|
||||
final Participant participant;
|
||||
final VideoQuality quality;
|
||||
|
||||
const ParticipantWidget(
|
||||
this.participant, {
|
||||
this.quality = VideoQuality.MEDIUM,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _ParticipantWidgetState();
|
||||
}
|
||||
|
||||
class _ParticipantWidgetState extends State<ParticipantWidget> with ParticipantDelegate {
|
||||
//
|
||||
TrackPublication? videoPub;
|
||||
TrackPublication? audioPub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.participant.addListener(_onParticipantChanged);
|
||||
_onParticipantChanged();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.participant.removeListener(_onParticipantChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant ParticipantWidget oldWidget) {
|
||||
oldWidget.participant.removeListener(_onParticipantChanged);
|
||||
widget.participant.addListener(_onParticipantChanged);
|
||||
_onParticipantChanged();
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
// register for change so Flutter will re-build the widget upon change
|
||||
void _onParticipantChanged() {
|
||||
//
|
||||
final firstAudio = widget.participant.audioTracks.firstWhereOrNull((pub) => pub.subscribed);
|
||||
final firstVideo = widget.participant.videoTracks
|
||||
.firstWhereOrNull((pub) => !pub.isScreenShare && pub.subscribed);
|
||||
|
||||
if (firstVideo is RemoteTrackPublication) {
|
||||
firstVideo.videoQuality = widget.quality;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
audioPub = !(firstAudio?.muted ?? true) ? firstAudio : null;
|
||||
videoPub = !(firstVideo?.muted ?? true) ? firstVideo : null;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext ctx) => Container(
|
||||
color: Theme.of(ctx).cardColor,
|
||||
child: Stack(
|
||||
children: [
|
||||
// Video
|
||||
if (videoPub != null)
|
||||
VideoTrackRenderer(
|
||||
videoPub!.track as VideoTrack,
|
||||
fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover,
|
||||
)
|
||||
else
|
||||
const NoVideoWidget(),
|
||||
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: ParticipantInfoWidget(
|
||||
title: widget.participant.identity,
|
||||
muted: audioPub == null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:eva_icons_flutter/eva_icons_flutter.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ParticipantInfoWidget extends StatelessWidget {
|
||||
//
|
||||
final String? title;
|
||||
final bool muted;
|
||||
|
||||
const ParticipantInfoWidget({
|
||||
this.title,
|
||||
this.muted = true,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@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,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 5),
|
||||
child: Icon(
|
||||
!muted ? EvaIcons.mic : EvaIcons.micOff,
|
||||
color: !muted ? Colors.white : Colors.red,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
+78
-17
@@ -7,7 +7,7 @@ packages:
|
||||
name: async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.6.1"
|
||||
version: "2.8.1"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -28,7 +28,7 @@ packages:
|
||||
name: charcode
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.3.1"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -50,13 +50,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
cupertino_icons:
|
||||
eva_icons_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cupertino_icons
|
||||
name: eva_icons_flutter
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
version: "3.0.0"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -102,13 +102,25 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_webrtc:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_webrtc
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.6"
|
||||
version: "0.6.7"
|
||||
google_fonts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: google_fonts
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -123,6 +135,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.3"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -157,7 +176,7 @@ packages:
|
||||
name: meta
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "1.7.0"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -178,7 +197,7 @@ packages:
|
||||
name: path_provider
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
version: "2.0.3"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -256,6 +275,48 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.7"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
shared_preferences_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_macos
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
shared_preferences_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
shared_preferences_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@@ -289,6 +350,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: synchronized
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -302,7 +370,7 @@ packages:
|
||||
name: test_api
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.0"
|
||||
version: "0.4.2"
|
||||
tuple:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -331,20 +399,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.7"
|
||||
version: "2.2.8"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -10,11 +10,15 @@ environment:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
cupertino_icons: ^1.0.2
|
||||
livekit_client:
|
||||
path: ../
|
||||
|
||||
provider: ^5.0.0
|
||||
logging: ^1.0.1
|
||||
google_fonts: ^2.1.0
|
||||
eva_icons_flutter: ^3.0.0
|
||||
shared_preferences: ^2.0.7
|
||||
|
||||
livekit_client:
|
||||
path: ../
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -13,7 +13,7 @@ import 'package:livekit_example/main.dart';
|
||||
void main() {
|
||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||
// Build our app and trigger a frame.
|
||||
await tester.pumpWidget(const MyApp());
|
||||
await tester.pumpWidget(const LiveKitExampleApp());
|
||||
|
||||
// Verify that our counter starts at 0.
|
||||
expect(find.text('0'), findsOneWidget);
|
||||
|
||||
+11
-11
@@ -1,22 +1,22 @@
|
||||
/// Flutter Client SDK to LiveKit.
|
||||
library livekit_client;
|
||||
|
||||
export 'src/livekit.dart';
|
||||
export 'src/errors.dart';
|
||||
export 'src/room.dart';
|
||||
export 'src/livekit.dart';
|
||||
export 'src/options.dart';
|
||||
export 'src/participant/local_participant.dart';
|
||||
export 'src/participant/local_participant.dart';
|
||||
export 'src/participant/participant.dart';
|
||||
export 'src/participant/local_participant.dart';
|
||||
export 'src/participant/remote_participant.dart';
|
||||
export 'src/proto/livekit_models.pbenum.dart';
|
||||
export 'src/proto/livekit_rtc.pbenum.dart';
|
||||
export 'src/participant/local_participant.dart';
|
||||
export 'src/track/options.dart';
|
||||
export 'src/track/track.dart';
|
||||
export 'src/track/video_track.dart';
|
||||
export 'src/proto/livekit_models.pb.dart' show TrackType;
|
||||
export 'src/proto/livekit_rtc.pb.dart' show VideoQuality;
|
||||
export 'src/room.dart';
|
||||
export 'src/track/local_audio_track.dart';
|
||||
export 'src/track/local_video_track.dart';
|
||||
export 'src/track/track_publication.dart';
|
||||
export 'src/track/local_track_publication.dart';
|
||||
export 'src/track/local_video_track.dart';
|
||||
export 'src/track/options.dart';
|
||||
export 'src/track/remote_track_publication.dart';
|
||||
export 'src/track/track.dart';
|
||||
export 'src/track/track_publication.dart';
|
||||
export 'src/track/video_track.dart';
|
||||
export 'src/widget/video_track_renderer.dart';
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
Future<WebSocketChannel> connectToWebSocket(Uri uri) {
|
||||
throw UnsupportedError('no implementations found');
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import 'dart:async';
|
||||
// ignore: avoid_web_libraries_in_flutter
|
||||
import 'dart:html';
|
||||
|
||||
import 'package:web_socket_channel/html.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
Future<WebSocketChannel> connectToWebSocket(Uri uri) {
|
||||
final ws = WebSocket(uri.toString());
|
||||
ws.binaryType = 'arraybuffer';
|
||||
final completer = Completer<WebSocketChannel>();
|
||||
ws.onOpen.first.then((_) {
|
||||
completer.complete(HtmlWebSocketChannel(ws));
|
||||
});
|
||||
ws.onError.first.then((e) {
|
||||
completer.completeError('could not connect');
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:web_socket_channel/io.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
Future<WebSocketChannel> connectToWebSocket(Uri uri) async {
|
||||
try {
|
||||
// ignore: close_sinks
|
||||
final ws = await WebSocket.connect(uri.toString());
|
||||
return IOWebSocketChannel(ws);
|
||||
} catch (e) {
|
||||
return Future.error(e);
|
||||
}
|
||||
}
|
||||
+19
-17
@@ -1,30 +1,32 @@
|
||||
class LiveKitError extends Error {
|
||||
String message;
|
||||
|
||||
LiveKitError(this.message);
|
||||
//
|
||||
// `Exception` implies runtime errors while, an `Error` object
|
||||
// represents a program failure that the programmer
|
||||
// should have avoided.
|
||||
//
|
||||
class LiveKitException implements Exception {
|
||||
final String message;
|
||||
const LiveKitException._(this.message);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return message;
|
||||
}
|
||||
String toString() => 'LiveKitException $runtimeType $message';
|
||||
}
|
||||
|
||||
class ConnectError extends LiveKitError {
|
||||
ConnectError([String msg = 'Failed to connect to server']) : super(msg);
|
||||
class ConnectError extends LiveKitException {
|
||||
ConnectError([String msg = 'Failed to connect to server']) : super._(msg);
|
||||
}
|
||||
|
||||
class UnexpectedConnectionState extends LiveKitError {
|
||||
UnexpectedConnectionState([String msg = 'Unexpected connection state']) : super(msg);
|
||||
class UnexpectedConnectionState extends LiveKitException {
|
||||
UnexpectedConnectionState([String msg = 'Unexpected connection state']) : super._(msg);
|
||||
}
|
||||
|
||||
class TrackCreateError extends LiveKitError {
|
||||
TrackCreateError([String msg = 'Failed to create track']) : super(msg);
|
||||
class TrackCreateError extends LiveKitException {
|
||||
TrackCreateError([String msg = 'Failed to create track']) : super._(msg);
|
||||
}
|
||||
|
||||
class TrackPublishError extends LiveKitError {
|
||||
TrackPublishError([String msg = 'Failed to publish track']) : super(msg);
|
||||
class TrackPublishError extends LiveKitException {
|
||||
TrackPublishError([String msg = 'Failed to publish track']) : super._(msg);
|
||||
}
|
||||
|
||||
class DataPublishError extends LiveKitError {
|
||||
DataPublishError([String msg = 'Failed to publish data']) : super(msg);
|
||||
class DataPublishError extends LiveKitException {
|
||||
DataPublishError([String msg = 'Failed to publish data']) : super._(msg);
|
||||
}
|
||||
|
||||
+39
-36
@@ -1,39 +1,3 @@
|
||||
class RTCConfiguration {
|
||||
int? iceCandidatePoolSize;
|
||||
List<RTCIceServer>? iceServers;
|
||||
String? iceTransportPolicy;
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
final iceServersMap = <Map<String, dynamic>>[];
|
||||
for (final element in (iceServers ?? <RTCIceServer>[])) {
|
||||
iceServersMap.add(element.toMap());
|
||||
}
|
||||
return <String, dynamic>{
|
||||
// only supports unified plan
|
||||
'sdpSemantics': 'unified-plan',
|
||||
if (iceCandidatePoolSize != null) 'iceCandidatePoolSize': iceCandidatePoolSize,
|
||||
'iceServers': iceServersMap,
|
||||
if (iceTransportPolicy != null) 'iceTransportPolicy': iceTransportPolicy,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class RTCIceServer {
|
||||
List<String> urls;
|
||||
String? username;
|
||||
String? credential;
|
||||
|
||||
RTCIceServer({required this.urls, this.username, this.credential});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return <String, dynamic>{
|
||||
'urls': urls,
|
||||
if (username != null) 'username': username,
|
||||
if (credential != null) 'credential': credential,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
enum RTCIceTransportPolicy {
|
||||
all,
|
||||
relay,
|
||||
@@ -45,3 +9,42 @@ extension RTCIceTransportPolicyExt on RTCIceTransportPolicy {
|
||||
RTCIceTransportPolicy.relay: 'relay',
|
||||
}[this]!;
|
||||
}
|
||||
|
||||
class RTCConfiguration {
|
||||
int? iceCandidatePoolSize;
|
||||
List<RTCIceServer>? iceServers;
|
||||
RTCIceTransportPolicy? iceTransportPolicy;
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
final iceServersMap = <Map<String, dynamic>>[
|
||||
if (iceServers != null)
|
||||
for (final element in iceServers!) element.toMap()
|
||||
];
|
||||
|
||||
return <String, dynamic>{
|
||||
// only supports unified plan
|
||||
'sdpSemantics': 'unified-plan',
|
||||
if (iceServersMap.isNotEmpty) 'iceServers': iceServersMap,
|
||||
if (iceCandidatePoolSize != null) 'iceCandidatePoolSize': iceCandidatePoolSize,
|
||||
if (iceTransportPolicy != null) 'iceTransportPolicy': iceTransportPolicy!.toStringValue(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class RTCIceServer {
|
||||
List<String> urls;
|
||||
String? username;
|
||||
String? credential;
|
||||
|
||||
RTCIceServer({
|
||||
required this.urls,
|
||||
this.username,
|
||||
this.credential,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() => <String, dynamic>{
|
||||
'urls': urls,
|
||||
if (username != null) 'username': username,
|
||||
if (credential != null) 'credential': credential,
|
||||
};
|
||||
}
|
||||
|
||||
+13
-3
@@ -1,12 +1,22 @@
|
||||
import 'room.dart';
|
||||
import 'options.dart';
|
||||
import 'room.dart';
|
||||
|
||||
/// Main entry point to connect to a room.
|
||||
/// {@category Room}
|
||||
class LiveKitClient {
|
||||
static const version = '0.4.0';
|
||||
|
||||
/// Connects to a LiveKit room
|
||||
static Future<Room> connect(String url, String token, [JoinOptions? options]) {
|
||||
static Future<Room> connect(
|
||||
String url,
|
||||
String token, {
|
||||
ConnectOptions? options,
|
||||
}) {
|
||||
final room = Room();
|
||||
return room.connect(url, token, options);
|
||||
return room.connect(
|
||||
url,
|
||||
token,
|
||||
options: options,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+26
-3
@@ -1,7 +1,30 @@
|
||||
import 'track/options.dart';
|
||||
|
||||
/// Options when joining a room.
|
||||
/// {@category Room}
|
||||
class JoinOptions {
|
||||
final bool? autoSubscribe;
|
||||
class ConnectOptions {
|
||||
/// Auto-subscribe to room tracks upon connect, defaults to true.
|
||||
final bool autoSubscribe;
|
||||
final TrackPublishOptions defaultPublishOptions;
|
||||
|
||||
const JoinOptions({this.autoSubscribe});
|
||||
const ConnectOptions({
|
||||
this.autoSubscribe = true,
|
||||
this.defaultPublishOptions = const TrackPublishOptions(),
|
||||
});
|
||||
}
|
||||
|
||||
class TrackPublishOptions {
|
||||
///
|
||||
final VideoEncoding? videoEncoding;
|
||||
|
||||
///
|
||||
final bool simulcast;
|
||||
|
||||
const TrackPublishOptions({
|
||||
this.videoEncoding,
|
||||
this.simulcast = false,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() => '${runtimeType}(videoEncoding: ${videoEncoding}, simulcast: ${simulcast})';
|
||||
}
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
|
||||
import '../errors.dart';
|
||||
import '../proto/livekit_models.pb.dart';
|
||||
import '../proto/livekit_rtc.pbserver.dart';
|
||||
import '../logger.dart';
|
||||
import '../options.dart';
|
||||
import '../proto/livekit_models.pb.dart' as lk_models;
|
||||
import '../rtc_engine.dart';
|
||||
import '../track/local_audio_track.dart';
|
||||
import '../track/local_track_publication.dart';
|
||||
import '../track/local_video_track.dart';
|
||||
import '../track/track.dart';
|
||||
import '../track/track_publication.dart';
|
||||
import '../utils.dart';
|
||||
import 'participant.dart';
|
||||
|
||||
/// Represents the current participant in the room.
|
||||
class LocalParticipant extends Participant {
|
||||
final RTCEngine _engine;
|
||||
final TrackPublishOptions? defaultPublishOptions;
|
||||
|
||||
LocalParticipant({
|
||||
required RTCEngine engine,
|
||||
required ParticipantInfo info,
|
||||
required lk_models.ParticipantInfo info,
|
||||
this.defaultPublishOptions,
|
||||
}) : _engine = engine,
|
||||
super(info.sid, info.identity) {
|
||||
updateFromInfo(info);
|
||||
@@ -29,19 +34,23 @@ class LocalParticipant extends Participant {
|
||||
|
||||
/// publish an audio track to the room
|
||||
Future<TrackPublication> publishAudioTrack(LocalAudioTrack track) async {
|
||||
if (audioTracks.values.any((element) => element.track?.mediaTrack.id == track.mediaTrack.id)) {
|
||||
return Future.error(TrackPublishError('track already exists'));
|
||||
if (audioTracks.any((e) => e.track?.mediaStreamTrack.id == track.mediaStreamTrack.id)) {
|
||||
throw TrackPublishError('track already exists');
|
||||
}
|
||||
|
||||
try {
|
||||
final trackInfo =
|
||||
await _engine.addTrack(cid: track.getCid(), name: track.name, kind: track.kind);
|
||||
// try {
|
||||
final trackInfo = await _engine.addTrack(
|
||||
cid: track.getCid(),
|
||||
name: track.name,
|
||||
kind: track.kind,
|
||||
);
|
||||
|
||||
final transceiverInit = RTCRtpTransceiverInit(
|
||||
direction: TransceiverDirection.SendOnly,
|
||||
);
|
||||
// addTransceiver cannot pass in a kind parameter due to a bug in flutter-webrtc (web)
|
||||
track.transceiver = await _engine.publisher?.pc.addTransceiver(
|
||||
track: track.mediaTrack,
|
||||
track: track.mediaStreamTrack,
|
||||
init: transceiverInit,
|
||||
);
|
||||
|
||||
@@ -50,27 +59,69 @@ class LocalParticipant extends Participant {
|
||||
notifyListeners();
|
||||
|
||||
return pub;
|
||||
} catch (e) {
|
||||
return Future.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Publish a video track to the room
|
||||
Future<TrackPublication> publishVideoTrack(LocalVideoTrack track) async {
|
||||
if (videoTracks.values.any((element) => element.track?.mediaTrack.id == track.mediaTrack.id)) {
|
||||
return Future.error(TrackPublishError('track already exists'));
|
||||
Future<TrackPublication> publishVideoTrack(
|
||||
LocalVideoTrack track, {
|
||||
TrackPublishOptions? options,
|
||||
}) async {
|
||||
if (videoTracks.any((e) => e.track?.mediaStreamTrack.id == track.mediaStreamTrack.id)) {
|
||||
throw TrackPublishError('track already exists');
|
||||
}
|
||||
|
||||
// Use default options from `ConnectOptions` if options is null
|
||||
options = options ?? defaultPublishOptions;
|
||||
|
||||
final trackInfo = await _engine.addTrack(
|
||||
cid: track.getCid(),
|
||||
name: track.name,
|
||||
kind: track.kind,
|
||||
);
|
||||
|
||||
//
|
||||
// Video encodings and simulcasts
|
||||
//
|
||||
|
||||
// use constraints passed to getUserMedia by default
|
||||
int? width = track.currentOptions.params.width;
|
||||
int? height = track.currentOptions.params.height;
|
||||
|
||||
if (kIsWeb) {
|
||||
// getSettings() is only implemented for Web
|
||||
try {
|
||||
final trackInfo =
|
||||
await _engine.addTrack(cid: track.getCid(), name: track.name, kind: track.kind);
|
||||
// try to use getSettings for more accurate resolution
|
||||
final settings = track.mediaStreamTrack.getSettings();
|
||||
width = settings['width'] as int?;
|
||||
height = settings['height'] as int?;
|
||||
// TODO: Get actual video dimensions to compute more accurately
|
||||
// mediaTrack.getConsstraints() is not implemented for mobile
|
||||
} catch (_) {
|
||||
logger.warning('Failed to call `mediaStreamTrack.getSettings()`');
|
||||
}
|
||||
}
|
||||
|
||||
logger.fine('Compute encodings with resolution: ${width}x${height}, options: ${options}');
|
||||
|
||||
final encodings = Utils.computeVideoEncodings(
|
||||
width: width,
|
||||
height: height,
|
||||
options: options,
|
||||
);
|
||||
|
||||
logger.fine('Using encodings: ${encodings?.map((e) => e.toMap())}');
|
||||
|
||||
final transceiverInit = RTCRtpTransceiverInit(
|
||||
direction: TransceiverDirection.SendOnly,
|
||||
sendEncodings: encodings,
|
||||
streams: [track.mediaStream],
|
||||
);
|
||||
// TODO: video encodings and simulcasts
|
||||
|
||||
//
|
||||
// addTransceiver cannot pass in a kind parameter due to a bug in flutter-webrtc (web)
|
||||
//
|
||||
track.transceiver = await _engine.publisher?.pc.addTransceiver(
|
||||
track: track.mediaTrack,
|
||||
track: track.mediaStreamTrack,
|
||||
init: transceiverInit,
|
||||
);
|
||||
|
||||
@@ -79,47 +130,38 @@ class LocalParticipant extends Participant {
|
||||
notifyListeners();
|
||||
|
||||
return pub;
|
||||
} catch (e) {
|
||||
return Future.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Unpublish a track that's already published
|
||||
void unpublishTrack(Track track) {
|
||||
Future<void> unpublishTrack(Track track) async {
|
||||
final existing = tracks.values.where((element) => element.track == track);
|
||||
if (existing.isEmpty) {
|
||||
return;
|
||||
}
|
||||
if (existing.isEmpty) return;
|
||||
|
||||
final pub = existing.first;
|
||||
|
||||
track.stop();
|
||||
await track.stop();
|
||||
|
||||
final sender = track.transceiver?.sender;
|
||||
if (sender != null) {
|
||||
engine.publisher?.pc.removeTrack(sender);
|
||||
await engine.publisher?.pc.removeTrack(sender);
|
||||
}
|
||||
|
||||
tracks.remove(pub.sid);
|
||||
switch (pub.kind) {
|
||||
case TrackType.AUDIO:
|
||||
audioTracks.remove(pub.sid);
|
||||
break;
|
||||
case TrackType.VIDEO:
|
||||
videoTracks.remove(pub.sid);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// Publish a new data payload to the room.
|
||||
/// @param destinationSids When empty, data will be forwarded to each participant in the room.
|
||||
void publishData(List<int> data, DataPacket_Kind reliability, {List<String>? destinationSids}) {
|
||||
void publishData(
|
||||
List<int> data,
|
||||
lk_models.DataPacket_Kind reliability, {
|
||||
List<String>? destinationSids,
|
||||
}) {
|
||||
RTCDataChannel? channel;
|
||||
switch (reliability) {
|
||||
case DataPacket_Kind.RELIABLE:
|
||||
case lk_models.DataPacket_Kind.RELIABLE:
|
||||
channel = engine.reliableDC;
|
||||
break;
|
||||
case DataPacket_Kind.LOSSY:
|
||||
case lk_models.DataPacket_Kind.LOSSY:
|
||||
channel = engine.lossyDC;
|
||||
break;
|
||||
}
|
||||
@@ -127,9 +169,9 @@ class LocalParticipant extends Participant {
|
||||
return;
|
||||
}
|
||||
|
||||
final packet = DataPacket(
|
||||
final packet = lk_models.DataPacket(
|
||||
kind: reliability,
|
||||
user: UserPacket(
|
||||
user: lk_models.UserPacket(
|
||||
payload: data,
|
||||
participantSid: sid,
|
||||
destinationSids: destinationSids,
|
||||
@@ -143,7 +185,7 @@ class LocalParticipant extends Participant {
|
||||
/// for internal use
|
||||
/// {@nodoc}
|
||||
@override
|
||||
void updateFromInfo(ParticipantInfo info) {
|
||||
void updateFromInfo(lk_models.ParticipantInfo info) {
|
||||
super.updateFromInfo(info);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'remote_participant.dart';
|
||||
import '../proto/livekit_models.pb.dart';
|
||||
import '../proto/livekit_models.pb.dart' as lk_models;
|
||||
import '../track/remote_track_publication.dart';
|
||||
import '../track/track.dart';
|
||||
import '../track/track_publication.dart';
|
||||
import 'remote_participant.dart';
|
||||
|
||||
/// Callbacks for participant changes
|
||||
mixin ParticipantDelegate {
|
||||
@@ -51,9 +51,6 @@ mixin ParticipantDelegate {
|
||||
/// - added/removed subscribed tracks
|
||||
/// - metadata changed
|
||||
class Participant extends ChangeNotifier {
|
||||
Map<String, TrackPublication> audioTracks = {};
|
||||
Map<String, TrackPublication> videoTracks = {};
|
||||
|
||||
/// map of track sid => published track
|
||||
Map<String, TrackPublication> tracks = {};
|
||||
|
||||
@@ -77,7 +74,7 @@ class Participant extends ChangeNotifier {
|
||||
/// delegate to receive participant callbacks
|
||||
ParticipantDelegate? delegate;
|
||||
|
||||
ParticipantInfo? _participantInfo;
|
||||
lk_models.ParticipantInfo? _participantInfo;
|
||||
bool _isSpeaking = false;
|
||||
|
||||
/// when the participant joined the room
|
||||
@@ -94,10 +91,8 @@ class Participant extends ChangeNotifier {
|
||||
|
||||
/// true if participant is publishing an audio track and is muted
|
||||
bool get isMuted {
|
||||
if (audioTracks.values.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
return audioTracks.values.first.muted;
|
||||
if (audioTracks.isEmpty) return false;
|
||||
return audioTracks.first.muted;
|
||||
}
|
||||
|
||||
bool get hasAudio => audioTracks.isNotEmpty;
|
||||
@@ -105,15 +100,7 @@ class Participant extends ChangeNotifier {
|
||||
bool get hasVideo => videoTracks.isNotEmpty;
|
||||
|
||||
/// tracks that are subscribed to
|
||||
List<TrackPublication> get subscribedTracks {
|
||||
List<TrackPublication> result = [];
|
||||
for (final track in tracks.values) {
|
||||
if (track.subscribed) {
|
||||
result.add(track);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
List<TrackPublication> get subscribedTracks => tracks.values.where((e) => e.subscribed).toList();
|
||||
|
||||
/// for internal use
|
||||
/// {@nodoc}
|
||||
@@ -148,7 +135,7 @@ class Participant extends ChangeNotifier {
|
||||
|
||||
/// for internal use
|
||||
/// {@nodoc}
|
||||
void updateFromInfo(ParticipantInfo info) {
|
||||
void updateFromInfo(lk_models.ParticipantInfo info) {
|
||||
identity = info.identity;
|
||||
sid = info.sid;
|
||||
if (info.metadata.isNotEmpty) {
|
||||
@@ -168,15 +155,14 @@ class Participant extends ChangeNotifier {
|
||||
void addTrackPublication(TrackPublication pub) {
|
||||
pub.track?.sid = pub.sid;
|
||||
tracks[pub.sid] = pub;
|
||||
switch (pub.kind) {
|
||||
case TrackType.AUDIO:
|
||||
audioTracks[pub.sid] = pub;
|
||||
break;
|
||||
case TrackType.VIDEO:
|
||||
videoTracks[pub.sid] = pub;
|
||||
break;
|
||||
default:
|
||||
// nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience extension
|
||||
extension LKParticipantExt on Participant {
|
||||
List<TrackPublication> get videoTracks =>
|
||||
tracks.values.where((e) => e.kind == lk_models.TrackType.VIDEO).toList();
|
||||
|
||||
List<TrackPublication> get audioTracks =>
|
||||
tracks.values.where((e) => e.kind == lk_models.TrackType.AUDIO).toList();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
import 'package:livekit_client/src/track/audio_track.dart';
|
||||
import '../proto/livekit_models.pb.dart';
|
||||
|
||||
import '../logger.dart';
|
||||
import '../proto/livekit_models.pb.dart' as lk_models;
|
||||
import '../signal_client.dart';
|
||||
import '../track/audio_track.dart';
|
||||
import '../track/remote_track_publication.dart';
|
||||
import '../track/track.dart';
|
||||
import '../track/video_track.dart';
|
||||
@@ -13,17 +15,22 @@ class RemoteParticipant extends Participant {
|
||||
|
||||
SignalClient get client => _client;
|
||||
|
||||
RemoteParticipant(this._client, String sid, String identity) : super(sid, identity);
|
||||
RemoteParticipant(
|
||||
this._client,
|
||||
String sid,
|
||||
String identity,
|
||||
) : super(sid, identity);
|
||||
|
||||
RemoteParticipant.fromInfo(this._client, ParticipantInfo info) : super(info.sid, info.identity) {
|
||||
RemoteParticipant.fromInfo(
|
||||
this._client,
|
||||
lk_models.ParticipantInfo info,
|
||||
) : super(info.sid, info.identity) {
|
||||
updateFromInfo(info);
|
||||
}
|
||||
|
||||
RemoteTrackPublication? getTrackPublication(String sid) {
|
||||
final pub = tracks[sid];
|
||||
if (pub is RemoteTrackPublication) {
|
||||
return pub;
|
||||
}
|
||||
if (pub is RemoteTrackPublication) return pub;
|
||||
}
|
||||
|
||||
/// for internal use
|
||||
@@ -49,11 +56,11 @@ class RemoteParticipant extends Participant {
|
||||
}
|
||||
|
||||
Track? track;
|
||||
if (pub.kind == TrackType.AUDIO) {
|
||||
if (pub.kind == lk_models.TrackType.AUDIO) {
|
||||
final audioTrack = AudioTrack(pub.name, mediaTrack, stream);
|
||||
audioTrack.start();
|
||||
track = audioTrack;
|
||||
} else if (pub.kind == TrackType.VIDEO) {
|
||||
} else if (pub.kind == lk_models.TrackType.VIDEO) {
|
||||
track = VideoTrack(pub.name, mediaTrack, stream);
|
||||
} else {
|
||||
final msg = 'unsupported track type ${pub.kind}';
|
||||
@@ -73,7 +80,7 @@ class RemoteParticipant extends Participant {
|
||||
/// for internal use
|
||||
/// {@nodoc}
|
||||
@override
|
||||
void updateFromInfo(ParticipantInfo info) {
|
||||
void updateFromInfo(lk_models.ParticipantInfo info) async {
|
||||
final hadInfo = hasInfo;
|
||||
super.updateFromInfo(info);
|
||||
|
||||
@@ -105,30 +112,28 @@ class RemoteParticipant extends Participant {
|
||||
}
|
||||
|
||||
// remove tracks
|
||||
for (final pub in tracks.values) {
|
||||
if (!validPubs.containsKey(pub.sid)) {
|
||||
unpublishTrack(sid, true);
|
||||
}
|
||||
final removeTrackSids =
|
||||
tracks.values.where((e) => !validPubs.containsKey(e.sid)).map((e) => e.sid).toList();
|
||||
|
||||
for (final sid in removeTrackSids) {
|
||||
await unpublishTrack(sid, true);
|
||||
}
|
||||
}
|
||||
|
||||
void unpublishTrack(String sid, [bool sendUnpublish = false]) {
|
||||
Future<void> unpublishTrack(String sid, [bool notify = false]) async {
|
||||
logger.finer('Unpublish track sid: $sid, notify: $notify');
|
||||
final pub = tracks.remove(sid);
|
||||
if (pub == null || pub is! RemoteTrackPublication) {
|
||||
return;
|
||||
}
|
||||
|
||||
audioTracks.remove(sid);
|
||||
videoTracks.remove(sid);
|
||||
if (pub == null || pub is! RemoteTrackPublication) return;
|
||||
|
||||
final track = pub.track;
|
||||
if (track != null) {
|
||||
track.stop();
|
||||
await track.stop();
|
||||
delegate?.onTrackUnsubscribed(this, track, pub);
|
||||
roomDelegate?.onTrackUnsubscribed(this, track, pub);
|
||||
notifyListeners();
|
||||
}
|
||||
if (sendUnpublish) {
|
||||
|
||||
if (notify) {
|
||||
delegate?.onTrackUnpublished(this, pub);
|
||||
roomDelegate?.onTrackUnpublished(this, pub);
|
||||
}
|
||||
@@ -141,9 +146,8 @@ class RemoteParticipant extends Participant {
|
||||
await Future<RemoteTrackPublication?>.delayed(const Duration(milliseconds: 100), () {
|
||||
return getTrackPublication(sid);
|
||||
});
|
||||
if (pub != null) {
|
||||
return pub;
|
||||
}
|
||||
|
||||
if (pub != null) return pub;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// source: livekit_models.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_this,unused_import,unused_shown_name
|
||||
|
||||
import 'dart:core' as $core;
|
||||
|
||||
@@ -572,659 +572,353 @@ class TrackInfo extends $pb.GeneratedMessage {
|
||||
void clearSimulcast() => clearField(7);
|
||||
}
|
||||
|
||||
enum DataMessage_Value { text, binary, notSet }
|
||||
enum DataPacket_Value { user, speaker, notSet }
|
||||
|
||||
class DataMessage extends $pb.GeneratedMessage {
|
||||
static const $core.Map<$core.int, DataMessage_Value> _DataMessage_ValueByTag = {
|
||||
1: DataMessage_Value.text,
|
||||
2: DataMessage_Value.binary,
|
||||
0: DataMessage_Value.notSet
|
||||
class DataPacket extends $pb.GeneratedMessage {
|
||||
static const $core.Map<$core.int, DataPacket_Value> _DataPacket_ValueByTag = {
|
||||
2: DataPacket_Value.user,
|
||||
3: DataPacket_Value.speaker,
|
||||
0: DataPacket_Value.notSet
|
||||
};
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'DataMessage',
|
||||
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'DataPacket',
|
||||
package: const $pb.PackageName(
|
||||
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'livekit'),
|
||||
createEmptyInstance: create)
|
||||
..oo(0, [1, 2])
|
||||
..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'text')
|
||||
..a<$core.List<$core.int>>(
|
||||
2,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'binary',
|
||||
$pb.PbFieldType.OY)
|
||||
..oo(0, [2, 3])
|
||||
..e<DataPacket_Kind>(
|
||||
1,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'kind',
|
||||
$pb.PbFieldType.OE,
|
||||
defaultOrMaker: DataPacket_Kind.RELIABLE,
|
||||
valueOf: DataPacket_Kind.valueOf,
|
||||
enumValues: DataPacket_Kind.values)
|
||||
..aOM<UserPacket>(
|
||||
2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'user',
|
||||
subBuilder: UserPacket.create)
|
||||
..aOM<ActiveSpeakerUpdate>(
|
||||
3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'speaker',
|
||||
subBuilder: ActiveSpeakerUpdate.create)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
DataMessage._() : super();
|
||||
factory DataMessage({
|
||||
$core.String? text,
|
||||
$core.List<$core.int>? binary,
|
||||
DataPacket._() : super();
|
||||
factory DataPacket({
|
||||
DataPacket_Kind? kind,
|
||||
UserPacket? user,
|
||||
ActiveSpeakerUpdate? speaker,
|
||||
}) {
|
||||
final _result = create();
|
||||
if (text != null) {
|
||||
_result.text = text;
|
||||
if (kind != null) {
|
||||
_result.kind = kind;
|
||||
}
|
||||
if (binary != null) {
|
||||
_result.binary = binary;
|
||||
if (user != null) {
|
||||
_result.user = user;
|
||||
}
|
||||
if (speaker != null) {
|
||||
_result.speaker = speaker;
|
||||
}
|
||||
return _result;
|
||||
}
|
||||
factory DataMessage.fromBuffer($core.List<$core.int> i,
|
||||
factory DataPacket.fromBuffer($core.List<$core.int> i,
|
||||
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(i, r);
|
||||
factory DataMessage.fromJson($core.String i,
|
||||
factory DataPacket.fromJson($core.String i,
|
||||
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(i, r);
|
||||
@$core.Deprecated('Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
DataMessage clone() => DataMessage()..mergeFromMessage(this);
|
||||
DataPacket clone() => DataPacket()..mergeFromMessage(this);
|
||||
@$core.Deprecated('Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
DataMessage copyWith(void Function(DataMessage) updates) =>
|
||||
super.copyWith((message) => updates(message as DataMessage))
|
||||
as DataMessage; // ignore: deprecated_member_use
|
||||
DataPacket copyWith(void Function(DataPacket) updates) =>
|
||||
super.copyWith((message) => updates(message as DataPacket))
|
||||
as DataPacket; // ignore: deprecated_member_use
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static DataMessage create() => DataMessage._();
|
||||
DataMessage createEmptyInstance() => create();
|
||||
static $pb.PbList<DataMessage> createRepeated() => $pb.PbList<DataMessage>();
|
||||
static DataPacket create() => DataPacket._();
|
||||
DataPacket createEmptyInstance() => create();
|
||||
static $pb.PbList<DataPacket> createRepeated() => $pb.PbList<DataPacket>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static DataMessage getDefault() =>
|
||||
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<DataMessage>(create);
|
||||
static DataMessage? _defaultInstance;
|
||||
static DataPacket getDefault() =>
|
||||
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<DataPacket>(create);
|
||||
static DataPacket? _defaultInstance;
|
||||
|
||||
DataMessage_Value whichValue() => _DataMessage_ValueByTag[$_whichOneof(0)]!;
|
||||
DataPacket_Value whichValue() => _DataPacket_ValueByTag[$_whichOneof(0)]!;
|
||||
void clearValue() => clearField($_whichOneof(0));
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.String get text => $_getSZ(0);
|
||||
DataPacket_Kind get kind => $_getN(0);
|
||||
@$pb.TagNumber(1)
|
||||
set text($core.String v) {
|
||||
$_setString(0, v);
|
||||
set kind(DataPacket_Kind v) {
|
||||
setField(1, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasText() => $_has(0);
|
||||
$core.bool hasKind() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearText() => clearField(1);
|
||||
void clearKind() => clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.List<$core.int> get binary => $_getN(1);
|
||||
UserPacket get user => $_getN(1);
|
||||
@$pb.TagNumber(2)
|
||||
set binary($core.List<$core.int> v) {
|
||||
$_setBytes(1, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasBinary() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearBinary() => clearField(2);
|
||||
}
|
||||
|
||||
class RecordingInput extends $pb.GeneratedMessage {
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'RecordingInput',
|
||||
package: const $pb.PackageName(
|
||||
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'livekit'),
|
||||
createEmptyInstance: create)
|
||||
..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'url')
|
||||
..aOM<RecordingTemplate>(
|
||||
2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'template',
|
||||
subBuilder: RecordingTemplate.create)
|
||||
..a<$core.int>(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'width',
|
||||
$pb.PbFieldType.O3)
|
||||
..a<$core.int>(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'height',
|
||||
$pb.PbFieldType.O3)
|
||||
..a<$core.int>(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'depth',
|
||||
$pb.PbFieldType.O3)
|
||||
..a<$core.int>(6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'framerate', $pb.PbFieldType.O3)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
RecordingInput._() : super();
|
||||
factory RecordingInput({
|
||||
$core.String? url,
|
||||
RecordingTemplate? template,
|
||||
$core.int? width,
|
||||
$core.int? height,
|
||||
$core.int? depth,
|
||||
$core.int? framerate,
|
||||
}) {
|
||||
final _result = create();
|
||||
if (url != null) {
|
||||
_result.url = url;
|
||||
}
|
||||
if (template != null) {
|
||||
_result.template = template;
|
||||
}
|
||||
if (width != null) {
|
||||
_result.width = width;
|
||||
}
|
||||
if (height != null) {
|
||||
_result.height = height;
|
||||
}
|
||||
if (depth != null) {
|
||||
_result.depth = depth;
|
||||
}
|
||||
if (framerate != null) {
|
||||
_result.framerate = framerate;
|
||||
}
|
||||
return _result;
|
||||
}
|
||||
factory RecordingInput.fromBuffer($core.List<$core.int> i,
|
||||
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(i, r);
|
||||
factory RecordingInput.fromJson($core.String i,
|
||||
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(i, r);
|
||||
@$core.Deprecated('Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
RecordingInput clone() => RecordingInput()..mergeFromMessage(this);
|
||||
@$core.Deprecated('Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
RecordingInput copyWith(void Function(RecordingInput) updates) =>
|
||||
super.copyWith((message) => updates(message as RecordingInput))
|
||||
as RecordingInput; // ignore: deprecated_member_use
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static RecordingInput create() => RecordingInput._();
|
||||
RecordingInput createEmptyInstance() => create();
|
||||
static $pb.PbList<RecordingInput> createRepeated() => $pb.PbList<RecordingInput>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static RecordingInput getDefault() =>
|
||||
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<RecordingInput>(create);
|
||||
static RecordingInput? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.String get url => $_getSZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set url($core.String v) {
|
||||
$_setString(0, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasUrl() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearUrl() => clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
RecordingTemplate get template => $_getN(1);
|
||||
@$pb.TagNumber(2)
|
||||
set template(RecordingTemplate v) {
|
||||
set user(UserPacket v) {
|
||||
setField(2, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasTemplate() => $_has(1);
|
||||
$core.bool hasUser() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearTemplate() => clearField(2);
|
||||
void clearUser() => clearField(2);
|
||||
@$pb.TagNumber(2)
|
||||
RecordingTemplate ensureTemplate() => $_ensure(1);
|
||||
UserPacket ensureUser() => $_ensure(1);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$core.int get width => $_getIZ(2);
|
||||
ActiveSpeakerUpdate get speaker => $_getN(2);
|
||||
@$pb.TagNumber(3)
|
||||
set width($core.int v) {
|
||||
$_setSignedInt32(2, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasWidth() => $_has(2);
|
||||
@$pb.TagNumber(3)
|
||||
void clearWidth() => clearField(3);
|
||||
|
||||
@$pb.TagNumber(4)
|
||||
$core.int get height => $_getIZ(3);
|
||||
@$pb.TagNumber(4)
|
||||
set height($core.int v) {
|
||||
$_setSignedInt32(3, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(4)
|
||||
$core.bool hasHeight() => $_has(3);
|
||||
@$pb.TagNumber(4)
|
||||
void clearHeight() => clearField(4);
|
||||
|
||||
@$pb.TagNumber(5)
|
||||
$core.int get depth => $_getIZ(4);
|
||||
@$pb.TagNumber(5)
|
||||
set depth($core.int v) {
|
||||
$_setSignedInt32(4, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(5)
|
||||
$core.bool hasDepth() => $_has(4);
|
||||
@$pb.TagNumber(5)
|
||||
void clearDepth() => clearField(5);
|
||||
|
||||
@$pb.TagNumber(6)
|
||||
$core.int get framerate => $_getIZ(5);
|
||||
@$pb.TagNumber(6)
|
||||
set framerate($core.int v) {
|
||||
$_setSignedInt32(5, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(6)
|
||||
$core.bool hasFramerate() => $_has(5);
|
||||
@$pb.TagNumber(6)
|
||||
void clearFramerate() => clearField(6);
|
||||
}
|
||||
|
||||
class RecordingTemplate extends $pb.GeneratedMessage {
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'RecordingTemplate',
|
||||
package: const $pb.PackageName(
|
||||
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'livekit'),
|
||||
createEmptyInstance: create)
|
||||
..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'type')
|
||||
..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'wsUrl')
|
||||
..aOS(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'token')
|
||||
..aOS(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'roomName')
|
||||
..hasRequiredFields = false;
|
||||
|
||||
RecordingTemplate._() : super();
|
||||
factory RecordingTemplate({
|
||||
$core.String? type,
|
||||
$core.String? wsUrl,
|
||||
$core.String? token,
|
||||
$core.String? roomName,
|
||||
}) {
|
||||
final _result = create();
|
||||
if (type != null) {
|
||||
_result.type = type;
|
||||
}
|
||||
if (wsUrl != null) {
|
||||
_result.wsUrl = wsUrl;
|
||||
}
|
||||
if (token != null) {
|
||||
_result.token = token;
|
||||
}
|
||||
if (roomName != null) {
|
||||
_result.roomName = roomName;
|
||||
}
|
||||
return _result;
|
||||
}
|
||||
factory RecordingTemplate.fromBuffer($core.List<$core.int> i,
|
||||
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(i, r);
|
||||
factory RecordingTemplate.fromJson($core.String i,
|
||||
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(i, r);
|
||||
@$core.Deprecated('Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
RecordingTemplate clone() => RecordingTemplate()..mergeFromMessage(this);
|
||||
@$core.Deprecated('Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
RecordingTemplate copyWith(void Function(RecordingTemplate) updates) =>
|
||||
super.copyWith((message) => updates(message as RecordingTemplate))
|
||||
as RecordingTemplate; // ignore: deprecated_member_use
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static RecordingTemplate create() => RecordingTemplate._();
|
||||
RecordingTemplate createEmptyInstance() => create();
|
||||
static $pb.PbList<RecordingTemplate> createRepeated() => $pb.PbList<RecordingTemplate>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static RecordingTemplate getDefault() =>
|
||||
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<RecordingTemplate>(create);
|
||||
static RecordingTemplate? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.String get type => $_getSZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set type($core.String v) {
|
||||
$_setString(0, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasType() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearType() => clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.String get wsUrl => $_getSZ(1);
|
||||
@$pb.TagNumber(2)
|
||||
set wsUrl($core.String v) {
|
||||
$_setString(1, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasWsUrl() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearWsUrl() => clearField(2);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$core.String get token => $_getSZ(2);
|
||||
@$pb.TagNumber(3)
|
||||
set token($core.String v) {
|
||||
$_setString(2, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasToken() => $_has(2);
|
||||
@$pb.TagNumber(3)
|
||||
void clearToken() => clearField(3);
|
||||
|
||||
@$pb.TagNumber(4)
|
||||
$core.String get roomName => $_getSZ(3);
|
||||
@$pb.TagNumber(4)
|
||||
set roomName($core.String v) {
|
||||
$_setString(3, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(4)
|
||||
$core.bool hasRoomName() => $_has(3);
|
||||
@$pb.TagNumber(4)
|
||||
void clearRoomName() => clearField(4);
|
||||
}
|
||||
|
||||
class RecordingOutput extends $pb.GeneratedMessage {
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'RecordingOutput',
|
||||
package: const $pb.PackageName(
|
||||
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'livekit'),
|
||||
createEmptyInstance: create)
|
||||
..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'file')
|
||||
..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'rtmp')
|
||||
..aOM<RecordingS3Output>(
|
||||
3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 's3',
|
||||
subBuilder: RecordingS3Output.create)
|
||||
..a<$core.int>(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'width',
|
||||
$pb.PbFieldType.O3)
|
||||
..a<$core.int>(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'height',
|
||||
$pb.PbFieldType.O3)
|
||||
..aOS(6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'audioBitrate')
|
||||
..aOS(7, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'audioFrequency')
|
||||
..aOS(8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'videoBitrate')
|
||||
..aOS(9, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'videoBuffer')
|
||||
..hasRequiredFields = false;
|
||||
|
||||
RecordingOutput._() : super();
|
||||
factory RecordingOutput({
|
||||
$core.String? file,
|
||||
$core.String? rtmp,
|
||||
RecordingS3Output? s3,
|
||||
$core.int? width,
|
||||
$core.int? height,
|
||||
$core.String? audioBitrate,
|
||||
$core.String? audioFrequency,
|
||||
$core.String? videoBitrate,
|
||||
$core.String? videoBuffer,
|
||||
}) {
|
||||
final _result = create();
|
||||
if (file != null) {
|
||||
_result.file = file;
|
||||
}
|
||||
if (rtmp != null) {
|
||||
_result.rtmp = rtmp;
|
||||
}
|
||||
if (s3 != null) {
|
||||
_result.s3 = s3;
|
||||
}
|
||||
if (width != null) {
|
||||
_result.width = width;
|
||||
}
|
||||
if (height != null) {
|
||||
_result.height = height;
|
||||
}
|
||||
if (audioBitrate != null) {
|
||||
_result.audioBitrate = audioBitrate;
|
||||
}
|
||||
if (audioFrequency != null) {
|
||||
_result.audioFrequency = audioFrequency;
|
||||
}
|
||||
if (videoBitrate != null) {
|
||||
_result.videoBitrate = videoBitrate;
|
||||
}
|
||||
if (videoBuffer != null) {
|
||||
_result.videoBuffer = videoBuffer;
|
||||
}
|
||||
return _result;
|
||||
}
|
||||
factory RecordingOutput.fromBuffer($core.List<$core.int> i,
|
||||
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(i, r);
|
||||
factory RecordingOutput.fromJson($core.String i,
|
||||
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(i, r);
|
||||
@$core.Deprecated('Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
RecordingOutput clone() => RecordingOutput()..mergeFromMessage(this);
|
||||
@$core.Deprecated('Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
RecordingOutput copyWith(void Function(RecordingOutput) updates) =>
|
||||
super.copyWith((message) => updates(message as RecordingOutput))
|
||||
as RecordingOutput; // ignore: deprecated_member_use
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static RecordingOutput create() => RecordingOutput._();
|
||||
RecordingOutput createEmptyInstance() => create();
|
||||
static $pb.PbList<RecordingOutput> createRepeated() => $pb.PbList<RecordingOutput>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static RecordingOutput getDefault() =>
|
||||
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<RecordingOutput>(create);
|
||||
static RecordingOutput? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.String get file => $_getSZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set file($core.String v) {
|
||||
$_setString(0, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasFile() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearFile() => clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.String get rtmp => $_getSZ(1);
|
||||
@$pb.TagNumber(2)
|
||||
set rtmp($core.String v) {
|
||||
$_setString(1, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasRtmp() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearRtmp() => clearField(2);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
RecordingS3Output get s3 => $_getN(2);
|
||||
@$pb.TagNumber(3)
|
||||
set s3(RecordingS3Output v) {
|
||||
set speaker(ActiveSpeakerUpdate v) {
|
||||
setField(3, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasS3() => $_has(2);
|
||||
$core.bool hasSpeaker() => $_has(2);
|
||||
@$pb.TagNumber(3)
|
||||
void clearS3() => clearField(3);
|
||||
void clearSpeaker() => clearField(3);
|
||||
@$pb.TagNumber(3)
|
||||
RecordingS3Output ensureS3() => $_ensure(2);
|
||||
|
||||
@$pb.TagNumber(4)
|
||||
$core.int get width => $_getIZ(3);
|
||||
@$pb.TagNumber(4)
|
||||
set width($core.int v) {
|
||||
$_setSignedInt32(3, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(4)
|
||||
$core.bool hasWidth() => $_has(3);
|
||||
@$pb.TagNumber(4)
|
||||
void clearWidth() => clearField(4);
|
||||
|
||||
@$pb.TagNumber(5)
|
||||
$core.int get height => $_getIZ(4);
|
||||
@$pb.TagNumber(5)
|
||||
set height($core.int v) {
|
||||
$_setSignedInt32(4, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(5)
|
||||
$core.bool hasHeight() => $_has(4);
|
||||
@$pb.TagNumber(5)
|
||||
void clearHeight() => clearField(5);
|
||||
|
||||
@$pb.TagNumber(6)
|
||||
$core.String get audioBitrate => $_getSZ(5);
|
||||
@$pb.TagNumber(6)
|
||||
set audioBitrate($core.String v) {
|
||||
$_setString(5, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(6)
|
||||
$core.bool hasAudioBitrate() => $_has(5);
|
||||
@$pb.TagNumber(6)
|
||||
void clearAudioBitrate() => clearField(6);
|
||||
|
||||
@$pb.TagNumber(7)
|
||||
$core.String get audioFrequency => $_getSZ(6);
|
||||
@$pb.TagNumber(7)
|
||||
set audioFrequency($core.String v) {
|
||||
$_setString(6, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(7)
|
||||
$core.bool hasAudioFrequency() => $_has(6);
|
||||
@$pb.TagNumber(7)
|
||||
void clearAudioFrequency() => clearField(7);
|
||||
|
||||
@$pb.TagNumber(8)
|
||||
$core.String get videoBitrate => $_getSZ(7);
|
||||
@$pb.TagNumber(8)
|
||||
set videoBitrate($core.String v) {
|
||||
$_setString(7, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(8)
|
||||
$core.bool hasVideoBitrate() => $_has(7);
|
||||
@$pb.TagNumber(8)
|
||||
void clearVideoBitrate() => clearField(8);
|
||||
|
||||
@$pb.TagNumber(9)
|
||||
$core.String get videoBuffer => $_getSZ(8);
|
||||
@$pb.TagNumber(9)
|
||||
set videoBuffer($core.String v) {
|
||||
$_setString(8, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(9)
|
||||
$core.bool hasVideoBuffer() => $_has(8);
|
||||
@$pb.TagNumber(9)
|
||||
void clearVideoBuffer() => clearField(9);
|
||||
ActiveSpeakerUpdate ensureSpeaker() => $_ensure(2);
|
||||
}
|
||||
|
||||
class RecordingS3Output extends $pb.GeneratedMessage {
|
||||
class ActiveSpeakerUpdate extends $pb.GeneratedMessage {
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'RecordingS3Output',
|
||||
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'ActiveSpeakerUpdate',
|
||||
package: const $pb.PackageName(
|
||||
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'livekit'),
|
||||
createEmptyInstance: create)
|
||||
..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'bucket')
|
||||
..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'key')
|
||||
..aOS(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'accessKey')
|
||||
..aOS(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'secret')
|
||||
..pc<SpeakerInfo>(
|
||||
1,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'speakers',
|
||||
$pb.PbFieldType.PM,
|
||||
subBuilder: SpeakerInfo.create)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
RecordingS3Output._() : super();
|
||||
factory RecordingS3Output({
|
||||
$core.String? bucket,
|
||||
$core.String? key,
|
||||
$core.String? accessKey,
|
||||
$core.String? secret,
|
||||
ActiveSpeakerUpdate._() : super();
|
||||
factory ActiveSpeakerUpdate({
|
||||
$core.Iterable<SpeakerInfo>? speakers,
|
||||
}) {
|
||||
final _result = create();
|
||||
if (bucket != null) {
|
||||
_result.bucket = bucket;
|
||||
}
|
||||
if (key != null) {
|
||||
_result.key = key;
|
||||
}
|
||||
if (accessKey != null) {
|
||||
_result.accessKey = accessKey;
|
||||
}
|
||||
if (secret != null) {
|
||||
_result.secret = secret;
|
||||
if (speakers != null) {
|
||||
_result.speakers.addAll(speakers);
|
||||
}
|
||||
return _result;
|
||||
}
|
||||
factory RecordingS3Output.fromBuffer($core.List<$core.int> i,
|
||||
factory ActiveSpeakerUpdate.fromBuffer($core.List<$core.int> i,
|
||||
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(i, r);
|
||||
factory RecordingS3Output.fromJson($core.String i,
|
||||
factory ActiveSpeakerUpdate.fromJson($core.String i,
|
||||
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(i, r);
|
||||
@$core.Deprecated('Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
RecordingS3Output clone() => RecordingS3Output()..mergeFromMessage(this);
|
||||
ActiveSpeakerUpdate clone() => ActiveSpeakerUpdate()..mergeFromMessage(this);
|
||||
@$core.Deprecated('Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
RecordingS3Output copyWith(void Function(RecordingS3Output) updates) =>
|
||||
super.copyWith((message) => updates(message as RecordingS3Output))
|
||||
as RecordingS3Output; // ignore: deprecated_member_use
|
||||
ActiveSpeakerUpdate copyWith(void Function(ActiveSpeakerUpdate) updates) =>
|
||||
super.copyWith((message) => updates(message as ActiveSpeakerUpdate))
|
||||
as ActiveSpeakerUpdate; // ignore: deprecated_member_use
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static RecordingS3Output create() => RecordingS3Output._();
|
||||
RecordingS3Output createEmptyInstance() => create();
|
||||
static $pb.PbList<RecordingS3Output> createRepeated() => $pb.PbList<RecordingS3Output>();
|
||||
static ActiveSpeakerUpdate create() => ActiveSpeakerUpdate._();
|
||||
ActiveSpeakerUpdate createEmptyInstance() => create();
|
||||
static $pb.PbList<ActiveSpeakerUpdate> createRepeated() => $pb.PbList<ActiveSpeakerUpdate>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static RecordingS3Output getDefault() =>
|
||||
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<RecordingS3Output>(create);
|
||||
static RecordingS3Output? _defaultInstance;
|
||||
static ActiveSpeakerUpdate getDefault() =>
|
||||
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<ActiveSpeakerUpdate>(create);
|
||||
static ActiveSpeakerUpdate? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.String get bucket => $_getSZ(0);
|
||||
$core.List<SpeakerInfo> get speakers => $_getList(0);
|
||||
}
|
||||
|
||||
class SpeakerInfo extends $pb.GeneratedMessage {
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'SpeakerInfo',
|
||||
package: const $pb.PackageName(
|
||||
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'livekit'),
|
||||
createEmptyInstance: create)
|
||||
..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'sid')
|
||||
..a<$core.double>(
|
||||
2,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'level',
|
||||
$pb.PbFieldType.OF)
|
||||
..aOB(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'active')
|
||||
..hasRequiredFields = false;
|
||||
|
||||
SpeakerInfo._() : super();
|
||||
factory SpeakerInfo({
|
||||
$core.String? sid,
|
||||
$core.double? level,
|
||||
$core.bool? active,
|
||||
}) {
|
||||
final _result = create();
|
||||
if (sid != null) {
|
||||
_result.sid = sid;
|
||||
}
|
||||
if (level != null) {
|
||||
_result.level = level;
|
||||
}
|
||||
if (active != null) {
|
||||
_result.active = active;
|
||||
}
|
||||
return _result;
|
||||
}
|
||||
factory SpeakerInfo.fromBuffer($core.List<$core.int> i,
|
||||
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(i, r);
|
||||
factory SpeakerInfo.fromJson($core.String i,
|
||||
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(i, r);
|
||||
@$core.Deprecated('Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
SpeakerInfo clone() => SpeakerInfo()..mergeFromMessage(this);
|
||||
@$core.Deprecated('Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
SpeakerInfo copyWith(void Function(SpeakerInfo) updates) =>
|
||||
super.copyWith((message) => updates(message as SpeakerInfo))
|
||||
as SpeakerInfo; // ignore: deprecated_member_use
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static SpeakerInfo create() => SpeakerInfo._();
|
||||
SpeakerInfo createEmptyInstance() => create();
|
||||
static $pb.PbList<SpeakerInfo> createRepeated() => $pb.PbList<SpeakerInfo>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static SpeakerInfo getDefault() =>
|
||||
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<SpeakerInfo>(create);
|
||||
static SpeakerInfo? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
set bucket($core.String v) {
|
||||
$core.String get sid => $_getSZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set sid($core.String v) {
|
||||
$_setString(0, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasBucket() => $_has(0);
|
||||
$core.bool hasSid() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearBucket() => clearField(1);
|
||||
void clearSid() => clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.String get key => $_getSZ(1);
|
||||
$core.double get level => $_getN(1);
|
||||
@$pb.TagNumber(2)
|
||||
set key($core.String v) {
|
||||
$_setString(1, v);
|
||||
set level($core.double v) {
|
||||
$_setFloat(1, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasKey() => $_has(1);
|
||||
$core.bool hasLevel() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearKey() => clearField(2);
|
||||
void clearLevel() => clearField(2);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$core.String get accessKey => $_getSZ(2);
|
||||
$core.bool get active => $_getBF(2);
|
||||
@$pb.TagNumber(3)
|
||||
set accessKey($core.String v) {
|
||||
$_setString(2, v);
|
||||
set active($core.bool v) {
|
||||
$_setBool(2, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasAccessKey() => $_has(2);
|
||||
$core.bool hasActive() => $_has(2);
|
||||
@$pb.TagNumber(3)
|
||||
void clearAccessKey() => clearField(3);
|
||||
|
||||
@$pb.TagNumber(4)
|
||||
$core.String get secret => $_getSZ(3);
|
||||
@$pb.TagNumber(4)
|
||||
set secret($core.String v) {
|
||||
$_setString(3, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(4)
|
||||
$core.bool hasSecret() => $_has(3);
|
||||
@$pb.TagNumber(4)
|
||||
void clearSecret() => clearField(4);
|
||||
void clearActive() => clearField(3);
|
||||
}
|
||||
|
||||
class UserPacket extends $pb.GeneratedMessage {
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'UserPacket',
|
||||
package: const $pb.PackageName(
|
||||
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'livekit'),
|
||||
createEmptyInstance: create)
|
||||
..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'participantSid')
|
||||
..a<$core.List<$core.int>>(
|
||||
2,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'payload',
|
||||
$pb.PbFieldType.OY)
|
||||
..pPS(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'destinationSids')
|
||||
..hasRequiredFields = false;
|
||||
|
||||
UserPacket._() : super();
|
||||
factory UserPacket({
|
||||
$core.String? participantSid,
|
||||
$core.List<$core.int>? payload,
|
||||
$core.Iterable<$core.String>? destinationSids,
|
||||
}) {
|
||||
final _result = create();
|
||||
if (participantSid != null) {
|
||||
_result.participantSid = participantSid;
|
||||
}
|
||||
if (payload != null) {
|
||||
_result.payload = payload;
|
||||
}
|
||||
if (destinationSids != null) {
|
||||
_result.destinationSids.addAll(destinationSids);
|
||||
}
|
||||
return _result;
|
||||
}
|
||||
factory UserPacket.fromBuffer($core.List<$core.int> i,
|
||||
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(i, r);
|
||||
factory UserPacket.fromJson($core.String i,
|
||||
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(i, r);
|
||||
@$core.Deprecated('Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
UserPacket clone() => UserPacket()..mergeFromMessage(this);
|
||||
@$core.Deprecated('Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
UserPacket copyWith(void Function(UserPacket) updates) =>
|
||||
super.copyWith((message) => updates(message as UserPacket))
|
||||
as UserPacket; // ignore: deprecated_member_use
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static UserPacket create() => UserPacket._();
|
||||
UserPacket createEmptyInstance() => create();
|
||||
static $pb.PbList<UserPacket> createRepeated() => $pb.PbList<UserPacket>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static UserPacket getDefault() =>
|
||||
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<UserPacket>(create);
|
||||
static UserPacket? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.String get participantSid => $_getSZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set participantSid($core.String v) {
|
||||
$_setString(0, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasParticipantSid() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearParticipantSid() => clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.List<$core.int> get payload => $_getN(1);
|
||||
@$pb.TagNumber(2)
|
||||
set payload($core.List<$core.int> v) {
|
||||
$_setBytes(1, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasPayload() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearPayload() => clearField(2);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$core.List<$core.String> get destinationSids => $_getList(2);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// source: livekit_models.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_this,unused_import,unused_shown_name
|
||||
|
||||
// ignore_for_file: UNDEFINED_SHOWN_NAME
|
||||
import 'dart:core' as $core;
|
||||
@@ -52,3 +52,21 @@ class ParticipantInfo_State extends $pb.ProtobufEnum {
|
||||
|
||||
const ParticipantInfo_State._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class DataPacket_Kind extends $pb.ProtobufEnum {
|
||||
static const DataPacket_Kind RELIABLE = DataPacket_Kind._(
|
||||
0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'RELIABLE');
|
||||
static const DataPacket_Kind LOSSY = DataPacket_Kind._(
|
||||
1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'LOSSY');
|
||||
|
||||
static const $core.List<DataPacket_Kind> values = <DataPacket_Kind>[
|
||||
RELIABLE,
|
||||
LOSSY,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, DataPacket_Kind> _byValue =
|
||||
$pb.ProtobufEnum.initByValue(values);
|
||||
static DataPacket_Kind? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const DataPacket_Kind._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// source: livekit_models.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,deprecated_member_use_from_same_package,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_this,unused_import,unused_shown_name
|
||||
|
||||
import 'dart:core' as $core;
|
||||
import 'dart:convert' as $convert;
|
||||
@@ -111,88 +111,74 @@ const TrackInfo$json = const {
|
||||
/// Descriptor for `TrackInfo`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List trackInfoDescriptor = $convert.base64Decode(
|
||||
'CglUcmFja0luZm8SEAoDc2lkGAEgASgJUgNzaWQSJgoEdHlwZRgCIAEoDjISLmxpdmVraXQuVHJhY2tUeXBlUgR0eXBlEhIKBG5hbWUYAyABKAlSBG5hbWUSFAoFbXV0ZWQYBCABKAhSBW11dGVkEhQKBXdpZHRoGAUgASgNUgV3aWR0aBIWCgZoZWlnaHQYBiABKA1SBmhlaWdodBIcCglzaW11bGNhc3QYByABKAhSCXNpbXVsY2FzdA==');
|
||||
@$core.Deprecated('Use dataMessageDescriptor instead')
|
||||
const DataMessage$json = const {
|
||||
'1': 'DataMessage',
|
||||
@$core.Deprecated('Use dataPacketDescriptor instead')
|
||||
const DataPacket$json = const {
|
||||
'1': 'DataPacket',
|
||||
'2': const [
|
||||
const {'1': 'text', '3': 1, '4': 1, '5': 9, '9': 0, '10': 'text'},
|
||||
const {'1': 'binary', '3': 2, '4': 1, '5': 12, '9': 0, '10': 'binary'},
|
||||
const {'1': 'kind', '3': 1, '4': 1, '5': 14, '6': '.livekit.DataPacket.Kind', '10': 'kind'},
|
||||
const {'1': 'user', '3': 2, '4': 1, '5': 11, '6': '.livekit.UserPacket', '9': 0, '10': 'user'},
|
||||
const {
|
||||
'1': 'speaker',
|
||||
'3': 3,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.livekit.ActiveSpeakerUpdate',
|
||||
'9': 0,
|
||||
'10': 'speaker'
|
||||
},
|
||||
],
|
||||
'4': const [DataPacket_Kind$json],
|
||||
'8': const [
|
||||
const {'1': 'value'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `DataMessage`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List dataMessageDescriptor = $convert.base64Decode(
|
||||
'CgtEYXRhTWVzc2FnZRIUCgR0ZXh0GAEgASgJSABSBHRleHQSGAoGYmluYXJ5GAIgASgMSABSBmJpbmFyeUIHCgV2YWx1ZQ==');
|
||||
@$core.Deprecated('Use recordingInputDescriptor instead')
|
||||
const RecordingInput$json = const {
|
||||
'1': 'RecordingInput',
|
||||
@$core.Deprecated('Use dataPacketDescriptor instead')
|
||||
const DataPacket_Kind$json = const {
|
||||
'1': 'Kind',
|
||||
'2': const [
|
||||
const {'1': 'url', '3': 1, '4': 1, '5': 9, '10': 'url'},
|
||||
const {
|
||||
'1': 'template',
|
||||
'3': 2,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.livekit.RecordingTemplate',
|
||||
'10': 'template'
|
||||
},
|
||||
const {'1': 'width', '3': 3, '4': 1, '5': 5, '10': 'width'},
|
||||
const {'1': 'height', '3': 4, '4': 1, '5': 5, '10': 'height'},
|
||||
const {'1': 'depth', '3': 5, '4': 1, '5': 5, '10': 'depth'},
|
||||
const {'1': 'framerate', '3': 6, '4': 1, '5': 5, '10': 'framerate'},
|
||||
const {'1': 'RELIABLE', '2': 0},
|
||||
const {'1': 'LOSSY', '2': 1},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `RecordingInput`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List recordingInputDescriptor = $convert.base64Decode(
|
||||
'Cg5SZWNvcmRpbmdJbnB1dBIQCgN1cmwYASABKAlSA3VybBI2Cgh0ZW1wbGF0ZRgCIAEoCzIaLmxpdmVraXQuUmVjb3JkaW5nVGVtcGxhdGVSCHRlbXBsYXRlEhQKBXdpZHRoGAMgASgFUgV3aWR0aBIWCgZoZWlnaHQYBCABKAVSBmhlaWdodBIUCgVkZXB0aBgFIAEoBVIFZGVwdGgSHAoJZnJhbWVyYXRlGAYgASgFUglmcmFtZXJhdGU=');
|
||||
@$core.Deprecated('Use recordingTemplateDescriptor instead')
|
||||
const RecordingTemplate$json = const {
|
||||
'1': 'RecordingTemplate',
|
||||
/// Descriptor for `DataPacket`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List dataPacketDescriptor = $convert.base64Decode(
|
||||
'CgpEYXRhUGFja2V0EiwKBGtpbmQYASABKA4yGC5saXZla2l0LkRhdGFQYWNrZXQuS2luZFIEa2luZBIpCgR1c2VyGAIgASgLMhMubGl2ZWtpdC5Vc2VyUGFja2V0SABSBHVzZXISOAoHc3BlYWtlchgDIAEoCzIcLmxpdmVraXQuQWN0aXZlU3BlYWtlclVwZGF0ZUgAUgdzcGVha2VyIh8KBEtpbmQSDAoIUkVMSUFCTEUQABIJCgVMT1NTWRABQgcKBXZhbHVl');
|
||||
@$core.Deprecated('Use activeSpeakerUpdateDescriptor instead')
|
||||
const ActiveSpeakerUpdate$json = const {
|
||||
'1': 'ActiveSpeakerUpdate',
|
||||
'2': const [
|
||||
const {'1': 'type', '3': 1, '4': 1, '5': 9, '10': 'type'},
|
||||
const {'1': 'ws_url', '3': 2, '4': 1, '5': 9, '10': 'wsUrl'},
|
||||
const {'1': 'token', '3': 3, '4': 1, '5': 9, '10': 'token'},
|
||||
const {'1': 'room_name', '3': 4, '4': 1, '5': 9, '10': 'roomName'},
|
||||
const {'1': 'speakers', '3': 1, '4': 3, '5': 11, '6': '.livekit.SpeakerInfo', '10': 'speakers'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `RecordingTemplate`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List recordingTemplateDescriptor = $convert.base64Decode(
|
||||
'ChFSZWNvcmRpbmdUZW1wbGF0ZRISCgR0eXBlGAEgASgJUgR0eXBlEhUKBndzX3VybBgCIAEoCVIFd3NVcmwSFAoFdG9rZW4YAyABKAlSBXRva2VuEhsKCXJvb21fbmFtZRgEIAEoCVIIcm9vbU5hbWU=');
|
||||
@$core.Deprecated('Use recordingOutputDescriptor instead')
|
||||
const RecordingOutput$json = const {
|
||||
'1': 'RecordingOutput',
|
||||
/// Descriptor for `ActiveSpeakerUpdate`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List activeSpeakerUpdateDescriptor = $convert.base64Decode(
|
||||
'ChNBY3RpdmVTcGVha2VyVXBkYXRlEjAKCHNwZWFrZXJzGAEgAygLMhQubGl2ZWtpdC5TcGVha2VySW5mb1IIc3BlYWtlcnM=');
|
||||
@$core.Deprecated('Use speakerInfoDescriptor instead')
|
||||
const SpeakerInfo$json = const {
|
||||
'1': 'SpeakerInfo',
|
||||
'2': const [
|
||||
const {'1': 'file', '3': 1, '4': 1, '5': 9, '10': 'file'},
|
||||
const {'1': 'rtmp', '3': 2, '4': 1, '5': 9, '10': 'rtmp'},
|
||||
const {'1': 's3', '3': 3, '4': 1, '5': 11, '6': '.livekit.RecordingS3Output', '10': 's3'},
|
||||
const {'1': 'width', '3': 4, '4': 1, '5': 5, '10': 'width'},
|
||||
const {'1': 'height', '3': 5, '4': 1, '5': 5, '10': 'height'},
|
||||
const {'1': 'audio_bitrate', '3': 6, '4': 1, '5': 9, '10': 'audioBitrate'},
|
||||
const {'1': 'audio_frequency', '3': 7, '4': 1, '5': 9, '10': 'audioFrequency'},
|
||||
const {'1': 'video_bitrate', '3': 8, '4': 1, '5': 9, '10': 'videoBitrate'},
|
||||
const {'1': 'video_buffer', '3': 9, '4': 1, '5': 9, '10': 'videoBuffer'},
|
||||
const {'1': 'sid', '3': 1, '4': 1, '5': 9, '10': 'sid'},
|
||||
const {'1': 'level', '3': 2, '4': 1, '5': 2, '10': 'level'},
|
||||
const {'1': 'active', '3': 3, '4': 1, '5': 8, '10': 'active'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `RecordingOutput`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List recordingOutputDescriptor = $convert.base64Decode(
|
||||
'Cg9SZWNvcmRpbmdPdXRwdXQSEgoEZmlsZRgBIAEoCVIEZmlsZRISCgRydG1wGAIgASgJUgRydG1wEioKAnMzGAMgASgLMhoubGl2ZWtpdC5SZWNvcmRpbmdTM091dHB1dFICczMSFAoFd2lkdGgYBCABKAVSBXdpZHRoEhYKBmhlaWdodBgFIAEoBVIGaGVpZ2h0EiMKDWF1ZGlvX2JpdHJhdGUYBiABKAlSDGF1ZGlvQml0cmF0ZRInCg9hdWRpb19mcmVxdWVuY3kYByABKAlSDmF1ZGlvRnJlcXVlbmN5EiMKDXZpZGVvX2JpdHJhdGUYCCABKAlSDHZpZGVvQml0cmF0ZRIhCgx2aWRlb19idWZmZXIYCSABKAlSC3ZpZGVvQnVmZmVy');
|
||||
@$core.Deprecated('Use recordingS3OutputDescriptor instead')
|
||||
const RecordingS3Output$json = const {
|
||||
'1': 'RecordingS3Output',
|
||||
/// Descriptor for `SpeakerInfo`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List speakerInfoDescriptor = $convert.base64Decode(
|
||||
'CgtTcGVha2VySW5mbxIQCgNzaWQYASABKAlSA3NpZBIUCgVsZXZlbBgCIAEoAlIFbGV2ZWwSFgoGYWN0aXZlGAMgASgIUgZhY3RpdmU=');
|
||||
@$core.Deprecated('Use userPacketDescriptor instead')
|
||||
const UserPacket$json = const {
|
||||
'1': 'UserPacket',
|
||||
'2': const [
|
||||
const {'1': 'bucket', '3': 1, '4': 1, '5': 9, '10': 'bucket'},
|
||||
const {'1': 'key', '3': 2, '4': 1, '5': 9, '10': 'key'},
|
||||
const {'1': 'access_key', '3': 3, '4': 1, '5': 9, '10': 'accessKey'},
|
||||
const {'1': 'secret', '3': 4, '4': 1, '5': 9, '10': 'secret'},
|
||||
const {'1': 'participant_sid', '3': 1, '4': 1, '5': 9, '10': 'participantSid'},
|
||||
const {'1': 'payload', '3': 2, '4': 1, '5': 12, '10': 'payload'},
|
||||
const {'1': 'destination_sids', '3': 3, '4': 3, '5': 9, '10': 'destinationSids'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `RecordingS3Output`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List recordingS3OutputDescriptor = $convert.base64Decode(
|
||||
'ChFSZWNvcmRpbmdTM091dHB1dBIWCgZidWNrZXQYASABKAlSBmJ1Y2tldBIQCgNrZXkYAiABKAlSA2tleRIdCgphY2Nlc3Nfa2V5GAMgASgJUglhY2Nlc3NLZXkSFgoGc2VjcmV0GAQgASgJUgZzZWNyZXQ=');
|
||||
/// Descriptor for `UserPacket`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List userPacketDescriptor = $convert.base64Decode(
|
||||
'CgpVc2VyUGFja2V0EicKD3BhcnRpY2lwYW50X3NpZBgBIAEoCVIOcGFydGljaXBhbnRTaWQSGAoHcGF5bG9hZBgCIAEoDFIHcGF5bG9hZBIpChBkZXN0aW5hdGlvbl9zaWRzGAMgAygJUg9kZXN0aW5hdGlvblNpZHM=');
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
// source: livekit_models.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,deprecated_member_use_from_same_package,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_this,unused_import,unused_shown_name
|
||||
|
||||
export 'livekit_models.pb.dart';
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// source: livekit_rtc.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_this,unused_import,unused_shown_name
|
||||
|
||||
import 'dart:core' as $core;
|
||||
|
||||
@@ -48,22 +48,33 @@ class SignalRequest extends $pb.GeneratedMessage {
|
||||
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'livekit'),
|
||||
createEmptyInstance: create)
|
||||
..oo(0, [1, 2, 3, 4, 5, 6, 7, 8, 9])
|
||||
..aOM<SessionDescription>(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'offer',
|
||||
..aOM<SessionDescription>(
|
||||
1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'offer',
|
||||
subBuilder: SessionDescription.create)
|
||||
..aOM<SessionDescription>(
|
||||
2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'answer',
|
||||
subBuilder: SessionDescription.create)
|
||||
..aOM<TrickleRequest>(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'trickle',
|
||||
..aOM<TrickleRequest>(
|
||||
3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'trickle',
|
||||
subBuilder: TrickleRequest.create)
|
||||
..aOM<AddTrackRequest>(
|
||||
4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'addTrack',
|
||||
subBuilder: AddTrackRequest.create)
|
||||
..aOM<MuteTrackRequest>(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'mute',
|
||||
..aOM<MuteTrackRequest>(
|
||||
5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'mute',
|
||||
subBuilder: MuteTrackRequest.create)
|
||||
..aOM<UpdateSubscription>(6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'subscription', subBuilder: UpdateSubscription.create)
|
||||
..aOM<UpdateTrackSettings>(7, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'trackSetting', subBuilder: UpdateTrackSettings.create)
|
||||
..aOM<LeaveRequest>(8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'leave', subBuilder: LeaveRequest.create)
|
||||
..aOM<SetSimulcastLayers>(9, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'simulcast', subBuilder: SetSimulcastLayers.create)
|
||||
..aOM<UpdateSubscription>(
|
||||
6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'subscription',
|
||||
subBuilder: UpdateSubscription.create)
|
||||
..aOM<UpdateTrackSettings>(
|
||||
7, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'trackSetting',
|
||||
subBuilder: UpdateTrackSettings.create)
|
||||
..aOM<LeaveRequest>(
|
||||
8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'leave',
|
||||
subBuilder: LeaveRequest.create)
|
||||
..aOM<SetSimulcastLayers>(
|
||||
9, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'simulcast',
|
||||
subBuilder: SetSimulcastLayers.create)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
SignalRequest._() : super();
|
||||
@@ -273,6 +284,7 @@ enum SignalResponse_Message {
|
||||
trackPublished,
|
||||
speaker,
|
||||
leave,
|
||||
mute,
|
||||
notSet
|
||||
}
|
||||
|
||||
@@ -286,6 +298,7 @@ class SignalResponse extends $pb.GeneratedMessage {
|
||||
6: SignalResponse_Message.trackPublished,
|
||||
7: SignalResponse_Message.speaker,
|
||||
8: SignalResponse_Message.leave,
|
||||
9: SignalResponse_Message.mute,
|
||||
0: SignalResponse_Message.notSet
|
||||
};
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
@@ -293,22 +306,34 @@ class SignalResponse extends $pb.GeneratedMessage {
|
||||
package: const $pb.PackageName(
|
||||
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'livekit'),
|
||||
createEmptyInstance: create)
|
||||
..oo(0, [1, 2, 3, 4, 5, 6, 7, 8])
|
||||
..aOM<JoinResponse>(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'join',
|
||||
..oo(0, [1, 2, 3, 4, 5, 6, 7, 8, 9])
|
||||
..aOM<JoinResponse>(
|
||||
1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'join',
|
||||
subBuilder: JoinResponse.create)
|
||||
..aOM<SessionDescription>(
|
||||
2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'answer',
|
||||
subBuilder: SessionDescription.create)
|
||||
..aOM<SessionDescription>(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'offer',
|
||||
..aOM<SessionDescription>(
|
||||
3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'offer',
|
||||
subBuilder: SessionDescription.create)
|
||||
..aOM<TrickleRequest>(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'trickle',
|
||||
..aOM<TrickleRequest>(
|
||||
4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'trickle',
|
||||
subBuilder: TrickleRequest.create)
|
||||
..aOM<ParticipantUpdate>(
|
||||
5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'update',
|
||||
subBuilder: ParticipantUpdate.create)
|
||||
..aOM<TrackPublishedResponse>(6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'trackPublished', subBuilder: TrackPublishedResponse.create)
|
||||
..aOM<ActiveSpeakerUpdate>(7, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'speaker', subBuilder: ActiveSpeakerUpdate.create)
|
||||
..aOM<LeaveRequest>(8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'leave', subBuilder: LeaveRequest.create)
|
||||
..aOM<TrackPublishedResponse>(
|
||||
6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'trackPublished',
|
||||
subBuilder: TrackPublishedResponse.create)
|
||||
..aOM<$0.ActiveSpeakerUpdate>(
|
||||
7, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'speaker',
|
||||
subBuilder: $0.ActiveSpeakerUpdate.create)
|
||||
..aOM<LeaveRequest>(
|
||||
8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'leave',
|
||||
subBuilder: LeaveRequest.create)
|
||||
..aOM<MuteTrackRequest>(
|
||||
9, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'mute',
|
||||
subBuilder: MuteTrackRequest.create)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
SignalResponse._() : super();
|
||||
@@ -319,8 +344,9 @@ class SignalResponse extends $pb.GeneratedMessage {
|
||||
TrickleRequest? trickle,
|
||||
ParticipantUpdate? update,
|
||||
TrackPublishedResponse? trackPublished,
|
||||
ActiveSpeakerUpdate? speaker,
|
||||
$0.ActiveSpeakerUpdate? speaker,
|
||||
LeaveRequest? leave,
|
||||
MuteTrackRequest? mute,
|
||||
}) {
|
||||
final _result = create();
|
||||
if (join != null) {
|
||||
@@ -347,6 +373,9 @@ class SignalResponse extends $pb.GeneratedMessage {
|
||||
if (leave != null) {
|
||||
_result.leave = leave;
|
||||
}
|
||||
if (mute != null) {
|
||||
_result.mute = mute;
|
||||
}
|
||||
return _result;
|
||||
}
|
||||
factory SignalResponse.fromBuffer($core.List<$core.int> i,
|
||||
@@ -463,9 +492,9 @@ class SignalResponse extends $pb.GeneratedMessage {
|
||||
TrackPublishedResponse ensureTrackPublished() => $_ensure(5);
|
||||
|
||||
@$pb.TagNumber(7)
|
||||
ActiveSpeakerUpdate get speaker => $_getN(6);
|
||||
$0.ActiveSpeakerUpdate get speaker => $_getN(6);
|
||||
@$pb.TagNumber(7)
|
||||
set speaker(ActiveSpeakerUpdate v) {
|
||||
set speaker($0.ActiveSpeakerUpdate v) {
|
||||
setField(7, v);
|
||||
}
|
||||
|
||||
@@ -474,7 +503,7 @@ class SignalResponse extends $pb.GeneratedMessage {
|
||||
@$pb.TagNumber(7)
|
||||
void clearSpeaker() => clearField(7);
|
||||
@$pb.TagNumber(7)
|
||||
ActiveSpeakerUpdate ensureSpeaker() => $_ensure(6);
|
||||
$0.ActiveSpeakerUpdate ensureSpeaker() => $_ensure(6);
|
||||
|
||||
@$pb.TagNumber(8)
|
||||
LeaveRequest get leave => $_getN(7);
|
||||
@@ -489,6 +518,20 @@ class SignalResponse extends $pb.GeneratedMessage {
|
||||
void clearLeave() => clearField(8);
|
||||
@$pb.TagNumber(8)
|
||||
LeaveRequest ensureLeave() => $_ensure(7);
|
||||
|
||||
@$pb.TagNumber(9)
|
||||
MuteTrackRequest get mute => $_getN(8);
|
||||
@$pb.TagNumber(9)
|
||||
set mute(MuteTrackRequest v) {
|
||||
setField(9, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(9)
|
||||
$core.bool hasMute() => $_has(8);
|
||||
@$pb.TagNumber(9)
|
||||
void clearMute() => clearField(9);
|
||||
@$pb.TagNumber(9)
|
||||
MuteTrackRequest ensureMute() => $_ensure(8);
|
||||
}
|
||||
|
||||
class AddTrackRequest extends $pb.GeneratedMessage {
|
||||
@@ -510,6 +553,7 @@ class AddTrackRequest extends $pb.GeneratedMessage {
|
||||
$pb.PbFieldType.OU3)
|
||||
..a<$core.int>(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'height',
|
||||
$pb.PbFieldType.OU3)
|
||||
..aOB(6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'muted')
|
||||
..hasRequiredFields = false;
|
||||
|
||||
AddTrackRequest._() : super();
|
||||
@@ -519,6 +563,7 @@ class AddTrackRequest extends $pb.GeneratedMessage {
|
||||
$0.TrackType? type,
|
||||
$core.int? width,
|
||||
$core.int? height,
|
||||
$core.bool? muted,
|
||||
}) {
|
||||
final _result = create();
|
||||
if (cid != null) {
|
||||
@@ -536,6 +581,9 @@ class AddTrackRequest extends $pb.GeneratedMessage {
|
||||
if (height != null) {
|
||||
_result.height = height;
|
||||
}
|
||||
if (muted != null) {
|
||||
_result.muted = muted;
|
||||
}
|
||||
return _result;
|
||||
}
|
||||
factory AddTrackRequest.fromBuffer($core.List<$core.int> i,
|
||||
@@ -623,6 +671,18 @@ class AddTrackRequest extends $pb.GeneratedMessage {
|
||||
$core.bool hasHeight() => $_has(4);
|
||||
@$pb.TagNumber(5)
|
||||
void clearHeight() => clearField(5);
|
||||
|
||||
@$pb.TagNumber(6)
|
||||
$core.bool get muted => $_getBF(5);
|
||||
@$pb.TagNumber(6)
|
||||
set muted($core.bool v) {
|
||||
$_setBool(5, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(6)
|
||||
$core.bool hasMuted() => $_has(5);
|
||||
@$pb.TagNumber(6)
|
||||
void clearMuted() => clearField(6);
|
||||
}
|
||||
|
||||
class TrickleRequest extends $pb.GeneratedMessage {
|
||||
@@ -871,7 +931,9 @@ class JoinResponse extends $pb.GeneratedMessage {
|
||||
subBuilder: $0.ParticipantInfo.create)
|
||||
..aOS(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'serverVersion')
|
||||
..pc<ICEServer>(
|
||||
5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'iceServers', $pb.PbFieldType.PM,
|
||||
5,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'iceServers',
|
||||
$pb.PbFieldType.PM,
|
||||
subBuilder: ICEServer.create)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@@ -1184,154 +1246,6 @@ class ParticipantUpdate extends $pb.GeneratedMessage {
|
||||
$core.List<$0.ParticipantInfo> get participants => $_getList(0);
|
||||
}
|
||||
|
||||
class ActiveSpeakerUpdate extends $pb.GeneratedMessage {
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'ActiveSpeakerUpdate',
|
||||
package: const $pb.PackageName(
|
||||
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'livekit'),
|
||||
createEmptyInstance: create)
|
||||
..pc<SpeakerInfo>(
|
||||
1,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'speakers',
|
||||
$pb.PbFieldType.PM,
|
||||
subBuilder: SpeakerInfo.create)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
ActiveSpeakerUpdate._() : super();
|
||||
factory ActiveSpeakerUpdate({
|
||||
$core.Iterable<SpeakerInfo>? speakers,
|
||||
}) {
|
||||
final _result = create();
|
||||
if (speakers != null) {
|
||||
_result.speakers.addAll(speakers);
|
||||
}
|
||||
return _result;
|
||||
}
|
||||
factory ActiveSpeakerUpdate.fromBuffer($core.List<$core.int> i,
|
||||
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(i, r);
|
||||
factory ActiveSpeakerUpdate.fromJson($core.String i,
|
||||
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(i, r);
|
||||
@$core.Deprecated('Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
ActiveSpeakerUpdate clone() => ActiveSpeakerUpdate()..mergeFromMessage(this);
|
||||
@$core.Deprecated('Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
ActiveSpeakerUpdate copyWith(void Function(ActiveSpeakerUpdate) updates) =>
|
||||
super.copyWith((message) => updates(message as ActiveSpeakerUpdate))
|
||||
as ActiveSpeakerUpdate; // ignore: deprecated_member_use
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static ActiveSpeakerUpdate create() => ActiveSpeakerUpdate._();
|
||||
ActiveSpeakerUpdate createEmptyInstance() => create();
|
||||
static $pb.PbList<ActiveSpeakerUpdate> createRepeated() => $pb.PbList<ActiveSpeakerUpdate>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static ActiveSpeakerUpdate getDefault() =>
|
||||
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<ActiveSpeakerUpdate>(create);
|
||||
static ActiveSpeakerUpdate? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.List<SpeakerInfo> get speakers => $_getList(0);
|
||||
}
|
||||
|
||||
class SpeakerInfo extends $pb.GeneratedMessage {
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'SpeakerInfo',
|
||||
package: const $pb.PackageName(
|
||||
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'livekit'),
|
||||
createEmptyInstance: create)
|
||||
..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'sid')
|
||||
..a<$core.double>(
|
||||
2,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'level',
|
||||
$pb.PbFieldType.OF)
|
||||
..aOB(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'active')
|
||||
..hasRequiredFields = false;
|
||||
|
||||
SpeakerInfo._() : super();
|
||||
factory SpeakerInfo({
|
||||
$core.String? sid,
|
||||
$core.double? level,
|
||||
$core.bool? active,
|
||||
}) {
|
||||
final _result = create();
|
||||
if (sid != null) {
|
||||
_result.sid = sid;
|
||||
}
|
||||
if (level != null) {
|
||||
_result.level = level;
|
||||
}
|
||||
if (active != null) {
|
||||
_result.active = active;
|
||||
}
|
||||
return _result;
|
||||
}
|
||||
factory SpeakerInfo.fromBuffer($core.List<$core.int> i,
|
||||
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(i, r);
|
||||
factory SpeakerInfo.fromJson($core.String i,
|
||||
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(i, r);
|
||||
@$core.Deprecated('Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
SpeakerInfo clone() => SpeakerInfo()..mergeFromMessage(this);
|
||||
@$core.Deprecated('Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
SpeakerInfo copyWith(void Function(SpeakerInfo) updates) =>
|
||||
super.copyWith((message) => updates(message as SpeakerInfo))
|
||||
as SpeakerInfo; // ignore: deprecated_member_use
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static SpeakerInfo create() => SpeakerInfo._();
|
||||
SpeakerInfo createEmptyInstance() => create();
|
||||
static $pb.PbList<SpeakerInfo> createRepeated() => $pb.PbList<SpeakerInfo>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static SpeakerInfo getDefault() =>
|
||||
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<SpeakerInfo>(create);
|
||||
static SpeakerInfo? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.String get sid => $_getSZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set sid($core.String v) {
|
||||
$_setString(0, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasSid() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearSid() => clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.double get level => $_getN(1);
|
||||
@$pb.TagNumber(2)
|
||||
set level($core.double v) {
|
||||
$_setFloat(1, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasLevel() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearLevel() => clearField(2);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool get active => $_getBF(2);
|
||||
@$pb.TagNumber(3)
|
||||
set active($core.bool v) {
|
||||
$_setBool(2, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasActive() => $_has(2);
|
||||
@$pb.TagNumber(3)
|
||||
void clearActive() => clearField(3);
|
||||
}
|
||||
|
||||
class UpdateSubscription extends $pb.GeneratedMessage {
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'UpdateSubscription',
|
||||
@@ -1627,206 +1541,3 @@ class ICEServer extends $pb.GeneratedMessage {
|
||||
@$pb.TagNumber(3)
|
||||
void clearCredential() => clearField(3);
|
||||
}
|
||||
|
||||
enum DataPacket_Value { user, speaker, notSet }
|
||||
|
||||
class DataPacket extends $pb.GeneratedMessage {
|
||||
static const $core.Map<$core.int, DataPacket_Value> _DataPacket_ValueByTag = {
|
||||
2: DataPacket_Value.user,
|
||||
3: DataPacket_Value.speaker,
|
||||
0: DataPacket_Value.notSet
|
||||
};
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'DataPacket',
|
||||
package: const $pb.PackageName(
|
||||
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'livekit'),
|
||||
createEmptyInstance: create)
|
||||
..oo(0, [2, 3])
|
||||
..e<DataPacket_Kind>(
|
||||
1,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'kind',
|
||||
$pb.PbFieldType.OE,
|
||||
defaultOrMaker: DataPacket_Kind.RELIABLE,
|
||||
valueOf: DataPacket_Kind.valueOf,
|
||||
enumValues: DataPacket_Kind.values)
|
||||
..aOM<UserPacket>(
|
||||
2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'user',
|
||||
subBuilder: UserPacket.create)
|
||||
..aOM<ActiveSpeakerUpdate>(
|
||||
3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'speaker',
|
||||
subBuilder: ActiveSpeakerUpdate.create)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
DataPacket._() : super();
|
||||
factory DataPacket({
|
||||
DataPacket_Kind? kind,
|
||||
UserPacket? user,
|
||||
ActiveSpeakerUpdate? speaker,
|
||||
}) {
|
||||
final _result = create();
|
||||
if (kind != null) {
|
||||
_result.kind = kind;
|
||||
}
|
||||
if (user != null) {
|
||||
_result.user = user;
|
||||
}
|
||||
if (speaker != null) {
|
||||
_result.speaker = speaker;
|
||||
}
|
||||
return _result;
|
||||
}
|
||||
factory DataPacket.fromBuffer($core.List<$core.int> i,
|
||||
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(i, r);
|
||||
factory DataPacket.fromJson($core.String i,
|
||||
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(i, r);
|
||||
@$core.Deprecated('Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
DataPacket clone() => DataPacket()..mergeFromMessage(this);
|
||||
@$core.Deprecated('Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
DataPacket copyWith(void Function(DataPacket) updates) =>
|
||||
super.copyWith((message) => updates(message as DataPacket))
|
||||
as DataPacket; // ignore: deprecated_member_use
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static DataPacket create() => DataPacket._();
|
||||
DataPacket createEmptyInstance() => create();
|
||||
static $pb.PbList<DataPacket> createRepeated() => $pb.PbList<DataPacket>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static DataPacket getDefault() =>
|
||||
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<DataPacket>(create);
|
||||
static DataPacket? _defaultInstance;
|
||||
|
||||
DataPacket_Value whichValue() => _DataPacket_ValueByTag[$_whichOneof(0)]!;
|
||||
void clearValue() => clearField($_whichOneof(0));
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
DataPacket_Kind get kind => $_getN(0);
|
||||
@$pb.TagNumber(1)
|
||||
set kind(DataPacket_Kind v) {
|
||||
setField(1, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasKind() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearKind() => clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
UserPacket get user => $_getN(1);
|
||||
@$pb.TagNumber(2)
|
||||
set user(UserPacket v) {
|
||||
setField(2, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasUser() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearUser() => clearField(2);
|
||||
@$pb.TagNumber(2)
|
||||
UserPacket ensureUser() => $_ensure(1);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
ActiveSpeakerUpdate get speaker => $_getN(2);
|
||||
@$pb.TagNumber(3)
|
||||
set speaker(ActiveSpeakerUpdate v) {
|
||||
setField(3, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasSpeaker() => $_has(2);
|
||||
@$pb.TagNumber(3)
|
||||
void clearSpeaker() => clearField(3);
|
||||
@$pb.TagNumber(3)
|
||||
ActiveSpeakerUpdate ensureSpeaker() => $_ensure(2);
|
||||
}
|
||||
|
||||
class UserPacket extends $pb.GeneratedMessage {
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'UserPacket',
|
||||
package: const $pb.PackageName(
|
||||
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'livekit'),
|
||||
createEmptyInstance: create)
|
||||
..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'participantSid')
|
||||
..a<$core.List<$core.int>>(
|
||||
2,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'payload',
|
||||
$pb.PbFieldType.OY)
|
||||
..pPS(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'destinationSids')
|
||||
..hasRequiredFields = false;
|
||||
|
||||
UserPacket._() : super();
|
||||
factory UserPacket({
|
||||
$core.String? participantSid,
|
||||
$core.List<$core.int>? payload,
|
||||
$core.Iterable<$core.String>? destinationSids,
|
||||
}) {
|
||||
final _result = create();
|
||||
if (participantSid != null) {
|
||||
_result.participantSid = participantSid;
|
||||
}
|
||||
if (payload != null) {
|
||||
_result.payload = payload;
|
||||
}
|
||||
if (destinationSids != null) {
|
||||
_result.destinationSids.addAll(destinationSids);
|
||||
}
|
||||
return _result;
|
||||
}
|
||||
factory UserPacket.fromBuffer($core.List<$core.int> i,
|
||||
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(i, r);
|
||||
factory UserPacket.fromJson($core.String i,
|
||||
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(i, r);
|
||||
@$core.Deprecated('Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
UserPacket clone() => UserPacket()..mergeFromMessage(this);
|
||||
@$core.Deprecated('Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
UserPacket copyWith(void Function(UserPacket) updates) =>
|
||||
super.copyWith((message) => updates(message as UserPacket))
|
||||
as UserPacket; // ignore: deprecated_member_use
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static UserPacket create() => UserPacket._();
|
||||
UserPacket createEmptyInstance() => create();
|
||||
static $pb.PbList<UserPacket> createRepeated() => $pb.PbList<UserPacket>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static UserPacket getDefault() =>
|
||||
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<UserPacket>(create);
|
||||
static UserPacket? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.String get participantSid => $_getSZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set participantSid($core.String v) {
|
||||
$_setString(0, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasParticipantSid() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearParticipantSid() => clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.List<$core.int> get payload => $_getN(1);
|
||||
@$pb.TagNumber(2)
|
||||
set payload($core.List<$core.int> v) {
|
||||
$_setBytes(1, v);
|
||||
}
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasPayload() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearPayload() => clearField(2);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$core.List<$core.String> get destinationSids => $_getList(2);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// source: livekit_rtc.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_this,unused_import,unused_shown_name
|
||||
|
||||
// ignore_for_file: UNDEFINED_SHOWN_NAME
|
||||
import 'dart:core' as $core;
|
||||
@@ -45,21 +45,3 @@ class VideoQuality extends $pb.ProtobufEnum {
|
||||
|
||||
const VideoQuality._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class DataPacket_Kind extends $pb.ProtobufEnum {
|
||||
static const DataPacket_Kind RELIABLE = DataPacket_Kind._(
|
||||
0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'RELIABLE');
|
||||
static const DataPacket_Kind LOSSY = DataPacket_Kind._(
|
||||
1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'LOSSY');
|
||||
|
||||
static const $core.List<DataPacket_Kind> values = <DataPacket_Kind>[
|
||||
RELIABLE,
|
||||
LOSSY,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, DataPacket_Kind> _byValue =
|
||||
$pb.ProtobufEnum.initByValue(values);
|
||||
static DataPacket_Kind? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const DataPacket_Kind._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// source: livekit_rtc.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,deprecated_member_use_from_same_package,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_this,unused_import,unused_shown_name
|
||||
|
||||
import 'dart:core' as $core;
|
||||
import 'dart:convert' as $convert;
|
||||
@@ -204,6 +204,15 @@ const SignalResponse$json = const {
|
||||
'9': 0,
|
||||
'10': 'leave'
|
||||
},
|
||||
const {
|
||||
'1': 'mute',
|
||||
'3': 9,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.livekit.MuteTrackRequest',
|
||||
'9': 0,
|
||||
'10': 'mute'
|
||||
},
|
||||
],
|
||||
'8': const [
|
||||
const {'1': 'message'},
|
||||
@@ -212,7 +221,7 @@ const SignalResponse$json = const {
|
||||
|
||||
/// Descriptor for `SignalResponse`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List signalResponseDescriptor = $convert.base64Decode(
|
||||
'Cg5TaWduYWxSZXNwb25zZRIrCgRqb2luGAEgASgLMhUubGl2ZWtpdC5Kb2luUmVzcG9uc2VIAFIEam9pbhI1CgZhbnN3ZXIYAiABKAsyGy5saXZla2l0LlNlc3Npb25EZXNjcmlwdGlvbkgAUgZhbnN3ZXISMwoFb2ZmZXIYAyABKAsyGy5saXZla2l0LlNlc3Npb25EZXNjcmlwdGlvbkgAUgVvZmZlchIzCgd0cmlja2xlGAQgASgLMhcubGl2ZWtpdC5Ucmlja2xlUmVxdWVzdEgAUgd0cmlja2xlEjQKBnVwZGF0ZRgFIAEoCzIaLmxpdmVraXQuUGFydGljaXBhbnRVcGRhdGVIAFIGdXBkYXRlEkoKD3RyYWNrX3B1Ymxpc2hlZBgGIAEoCzIfLmxpdmVraXQuVHJhY2tQdWJsaXNoZWRSZXNwb25zZUgAUg50cmFja1B1Ymxpc2hlZBI4CgdzcGVha2VyGAcgASgLMhwubGl2ZWtpdC5BY3RpdmVTcGVha2VyVXBkYXRlSABSB3NwZWFrZXISLQoFbGVhdmUYCCABKAsyFS5saXZla2l0LkxlYXZlUmVxdWVzdEgAUgVsZWF2ZUIJCgdtZXNzYWdl');
|
||||
'Cg5TaWduYWxSZXNwb25zZRIrCgRqb2luGAEgASgLMhUubGl2ZWtpdC5Kb2luUmVzcG9uc2VIAFIEam9pbhI1CgZhbnN3ZXIYAiABKAsyGy5saXZla2l0LlNlc3Npb25EZXNjcmlwdGlvbkgAUgZhbnN3ZXISMwoFb2ZmZXIYAyABKAsyGy5saXZla2l0LlNlc3Npb25EZXNjcmlwdGlvbkgAUgVvZmZlchIzCgd0cmlja2xlGAQgASgLMhcubGl2ZWtpdC5Ucmlja2xlUmVxdWVzdEgAUgd0cmlja2xlEjQKBnVwZGF0ZRgFIAEoCzIaLmxpdmVraXQuUGFydGljaXBhbnRVcGRhdGVIAFIGdXBkYXRlEkoKD3RyYWNrX3B1Ymxpc2hlZBgGIAEoCzIfLmxpdmVraXQuVHJhY2tQdWJsaXNoZWRSZXNwb25zZUgAUg50cmFja1B1Ymxpc2hlZBI4CgdzcGVha2VyGAcgASgLMhwubGl2ZWtpdC5BY3RpdmVTcGVha2VyVXBkYXRlSABSB3NwZWFrZXISLQoFbGVhdmUYCCABKAsyFS5saXZla2l0LkxlYXZlUmVxdWVzdEgAUgVsZWF2ZRIvCgRtdXRlGAkgASgLMhkubGl2ZWtpdC5NdXRlVHJhY2tSZXF1ZXN0SABSBG11dGVCCQoHbWVzc2FnZQ==');
|
||||
@$core.Deprecated('Use addTrackRequestDescriptor instead')
|
||||
const AddTrackRequest$json = const {
|
||||
'1': 'AddTrackRequest',
|
||||
@@ -222,12 +231,13 @@ const AddTrackRequest$json = const {
|
||||
const {'1': 'type', '3': 3, '4': 1, '5': 14, '6': '.livekit.TrackType', '10': 'type'},
|
||||
const {'1': 'width', '3': 4, '4': 1, '5': 13, '10': 'width'},
|
||||
const {'1': 'height', '3': 5, '4': 1, '5': 13, '10': 'height'},
|
||||
const {'1': 'muted', '3': 6, '4': 1, '5': 8, '10': 'muted'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `AddTrackRequest`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List addTrackRequestDescriptor = $convert.base64Decode(
|
||||
'Cg9BZGRUcmFja1JlcXVlc3QSEAoDY2lkGAEgASgJUgNjaWQSEgoEbmFtZRgCIAEoCVIEbmFtZRImCgR0eXBlGAMgASgOMhIubGl2ZWtpdC5UcmFja1R5cGVSBHR5cGUSFAoFd2lkdGgYBCABKA1SBXdpZHRoEhYKBmhlaWdodBgFIAEoDVIGaGVpZ2h0');
|
||||
'Cg9BZGRUcmFja1JlcXVlc3QSEAoDY2lkGAEgASgJUgNjaWQSEgoEbmFtZRgCIAEoCVIEbmFtZRImCgR0eXBlGAMgASgOMhIubGl2ZWtpdC5UcmFja1R5cGVSBHR5cGUSFAoFd2lkdGgYBCABKA1SBXdpZHRoEhYKBmhlaWdodBgFIAEoDVIGaGVpZ2h0EhQKBW11dGVkGAYgASgIUgVtdXRlZA==');
|
||||
@$core.Deprecated('Use trickleRequestDescriptor instead')
|
||||
const TrickleRequest$json = const {
|
||||
'1': 'TrickleRequest',
|
||||
@@ -342,30 +352,6 @@ const ParticipantUpdate$json = const {
|
||||
/// Descriptor for `ParticipantUpdate`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List participantUpdateDescriptor = $convert.base64Decode(
|
||||
'ChFQYXJ0aWNpcGFudFVwZGF0ZRI8CgxwYXJ0aWNpcGFudHMYASADKAsyGC5saXZla2l0LlBhcnRpY2lwYW50SW5mb1IMcGFydGljaXBhbnRz');
|
||||
@$core.Deprecated('Use activeSpeakerUpdateDescriptor instead')
|
||||
const ActiveSpeakerUpdate$json = const {
|
||||
'1': 'ActiveSpeakerUpdate',
|
||||
'2': const [
|
||||
const {'1': 'speakers', '3': 1, '4': 3, '5': 11, '6': '.livekit.SpeakerInfo', '10': 'speakers'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `ActiveSpeakerUpdate`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List activeSpeakerUpdateDescriptor = $convert.base64Decode(
|
||||
'ChNBY3RpdmVTcGVha2VyVXBkYXRlEjAKCHNwZWFrZXJzGAEgAygLMhQubGl2ZWtpdC5TcGVha2VySW5mb1IIc3BlYWtlcnM=');
|
||||
@$core.Deprecated('Use speakerInfoDescriptor instead')
|
||||
const SpeakerInfo$json = const {
|
||||
'1': 'SpeakerInfo',
|
||||
'2': const [
|
||||
const {'1': 'sid', '3': 1, '4': 1, '5': 9, '10': 'sid'},
|
||||
const {'1': 'level', '3': 2, '4': 1, '5': 2, '10': 'level'},
|
||||
const {'1': 'active', '3': 3, '4': 1, '5': 8, '10': 'active'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `SpeakerInfo`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List speakerInfoDescriptor = $convert.base64Decode(
|
||||
'CgtTcGVha2VySW5mbxIQCgNzaWQYASABKAlSA3NpZBIUCgVsZXZlbBgCIAEoAlIFbGV2ZWwSFgoGYWN0aXZlGAMgASgIUgZhY3RpdmU=');
|
||||
@$core.Deprecated('Use updateSubscriptionDescriptor instead')
|
||||
const UpdateSubscription$json = const {
|
||||
'1': 'UpdateSubscription',
|
||||
@@ -415,50 +401,3 @@ const ICEServer$json = const {
|
||||
/// Descriptor for `ICEServer`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List iCEServerDescriptor = $convert.base64Decode(
|
||||
'CglJQ0VTZXJ2ZXISEgoEdXJscxgBIAMoCVIEdXJscxIaCgh1c2VybmFtZRgCIAEoCVIIdXNlcm5hbWUSHgoKY3JlZGVudGlhbBgDIAEoCVIKY3JlZGVudGlhbA==');
|
||||
@$core.Deprecated('Use dataPacketDescriptor instead')
|
||||
const DataPacket$json = const {
|
||||
'1': 'DataPacket',
|
||||
'2': const [
|
||||
const {'1': 'kind', '3': 1, '4': 1, '5': 14, '6': '.livekit.DataPacket.Kind', '10': 'kind'},
|
||||
const {'1': 'user', '3': 2, '4': 1, '5': 11, '6': '.livekit.UserPacket', '9': 0, '10': 'user'},
|
||||
const {
|
||||
'1': 'speaker',
|
||||
'3': 3,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.livekit.ActiveSpeakerUpdate',
|
||||
'9': 0,
|
||||
'10': 'speaker'
|
||||
},
|
||||
],
|
||||
'4': const [DataPacket_Kind$json],
|
||||
'8': const [
|
||||
const {'1': 'value'},
|
||||
],
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use dataPacketDescriptor instead')
|
||||
const DataPacket_Kind$json = const {
|
||||
'1': 'Kind',
|
||||
'2': const [
|
||||
const {'1': 'RELIABLE', '2': 0},
|
||||
const {'1': 'LOSSY', '2': 1},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `DataPacket`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List dataPacketDescriptor = $convert.base64Decode(
|
||||
'CgpEYXRhUGFja2V0EiwKBGtpbmQYASABKA4yGC5saXZla2l0LkRhdGFQYWNrZXQuS2luZFIEa2luZBIpCgR1c2VyGAIgASgLMhMubGl2ZWtpdC5Vc2VyUGFja2V0SABSBHVzZXISOAoHc3BlYWtlchgDIAEoCzIcLmxpdmVraXQuQWN0aXZlU3BlYWtlclVwZGF0ZUgAUgdzcGVha2VyIh8KBEtpbmQSDAoIUkVMSUFCTEUQABIJCgVMT1NTWRABQgcKBXZhbHVl');
|
||||
@$core.Deprecated('Use userPacketDescriptor instead')
|
||||
const UserPacket$json = const {
|
||||
'1': 'UserPacket',
|
||||
'2': const [
|
||||
const {'1': 'participant_sid', '3': 1, '4': 1, '5': 9, '10': 'participantSid'},
|
||||
const {'1': 'payload', '3': 2, '4': 1, '5': 12, '10': 'payload'},
|
||||
const {'1': 'destination_sids', '3': 3, '4': 3, '5': 9, '10': 'destinationSids'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `UserPacket`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List userPacketDescriptor = $convert.base64Decode(
|
||||
'CgpVc2VyUGFja2V0EicKD3BhcnRpY2lwYW50X3NpZBgBIAEoCVIOcGFydGljaXBhbnRTaWQSGAoHcGF5bG9hZBgCIAEoDFIHcGF5bG9hZBIpChBkZXN0aW5hdGlvbl9zaWRzGAMgAygJUg9kZXN0aW5hdGlvblNpZHM=');
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
// source: livekit_rtc.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,deprecated_member_use_from_same_package,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_this,unused_import,unused_shown_name
|
||||
|
||||
export 'livekit_rtc.pb.dart';
|
||||
|
||||
+39
-21
@@ -1,6 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
@@ -12,8 +12,7 @@ import 'options.dart';
|
||||
import 'participant/local_participant.dart';
|
||||
import 'participant/participant.dart';
|
||||
import 'participant/remote_participant.dart';
|
||||
import 'proto/livekit_models.pb.dart';
|
||||
import 'proto/livekit_rtc.pb.dart';
|
||||
import 'proto/livekit_models.pb.dart' as lk_models;
|
||||
import 'rtc_engine.dart';
|
||||
import 'signal_client.dart';
|
||||
import 'track/remote_track_publication.dart';
|
||||
@@ -140,9 +139,10 @@ class Room extends ChangeNotifier with ParticipantDelegate {
|
||||
_engine.onTrack = _onTrackAdded;
|
||||
_engine.onICEConnected = _handleICEConnected;
|
||||
_engine.onDisconnected = _handleDisconnect;
|
||||
_engine.onParticipantUpdateCallback = _handleParticipantUpdate;
|
||||
_engine.onActiveSpeakerchangedCallback = _handleSpeakerUpdate;
|
||||
_engine.onDataMessageCallback = _handleDataPacket;
|
||||
_engine.onParticipantUpdated = _handleParticipantUpdate;
|
||||
_engine.onActiveSpeakerUpdated = _handleSpeakerUpdate;
|
||||
_engine.onDataMessage = _handleDataPacket;
|
||||
_engine.onRemoteMute = _onRemoteMuteChanged;
|
||||
_engine.onReconnected = () {
|
||||
_state = RoomState.connected;
|
||||
delegate?.onReconnected();
|
||||
@@ -155,16 +155,26 @@ class Room extends ChangeNotifier with ParticipantDelegate {
|
||||
};
|
||||
}
|
||||
|
||||
Future<Room> connect(String url, String token, [JoinOptions? opts]) async {
|
||||
Future<Room> connect(
|
||||
String url,
|
||||
String token, {
|
||||
ConnectOptions? options,
|
||||
}) async {
|
||||
final completer = Completer<Room>();
|
||||
_connectCompleter = completer;
|
||||
|
||||
final joinResponse = await _engine.join(url, token, opts);
|
||||
final joinResponse = await _engine.join(
|
||||
url,
|
||||
token,
|
||||
options: options,
|
||||
);
|
||||
|
||||
logger.fine('connected to LiveKit server, version: ${joinResponse.serverVersion}');
|
||||
|
||||
localParticipant = LocalParticipant(
|
||||
engine: _engine,
|
||||
info: joinResponse.participant,
|
||||
defaultPublishOptions: options?.defaultPublishOptions,
|
||||
);
|
||||
localParticipant.roomDelegate = this;
|
||||
|
||||
@@ -191,12 +201,12 @@ class Room extends ChangeNotifier with ParticipantDelegate {
|
||||
}
|
||||
|
||||
/// Disconnects from the room, notifying server of disconnection.
|
||||
void disconnect() {
|
||||
Future<void> disconnect() async {
|
||||
_engine.client.sendLeave();
|
||||
_handleDisconnect();
|
||||
await _handleDisconnect();
|
||||
}
|
||||
|
||||
RemoteParticipant _getOrCreateRemoteParticipant(String sid, ParticipantInfo? info) {
|
||||
RemoteParticipant _getOrCreateRemoteParticipant(String sid, lk_models.ParticipantInfo? info) {
|
||||
var participant = _participants[sid];
|
||||
if (participant != null) {
|
||||
return participant;
|
||||
@@ -220,7 +230,7 @@ class Room extends ChangeNotifier with ParticipantDelegate {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _handleDisconnect() {
|
||||
Future<void> _handleDisconnect() async {
|
||||
if (_state == RoomState.disconnected) {
|
||||
return;
|
||||
}
|
||||
@@ -228,14 +238,14 @@ class Room extends ChangeNotifier with ParticipantDelegate {
|
||||
for (final p in _participants.values) {
|
||||
final tracks = List<TrackPublication>.from(p.tracks.values);
|
||||
for (final pub in tracks) {
|
||||
p.unpublishTrack(pub.sid);
|
||||
await p.unpublishTrack(pub.sid);
|
||||
}
|
||||
}
|
||||
for (final pub in localParticipant.tracks.values) {
|
||||
pub.track?.stop();
|
||||
await pub.track?.stop();
|
||||
}
|
||||
|
||||
_engine.close();
|
||||
await _engine.close();
|
||||
_participants.clear();
|
||||
_activeSpeakers.clear();
|
||||
_state = RoomState.disconnected;
|
||||
@@ -243,7 +253,7 @@ class Room extends ChangeNotifier with ParticipantDelegate {
|
||||
delegate?.onDisconnected();
|
||||
}
|
||||
|
||||
void _handleParticipantUpdate(List<ParticipantInfo> updates) {
|
||||
void _handleParticipantUpdate(List<lk_models.ParticipantInfo> updates) {
|
||||
// trigger change notifier only if list of participants membership is changed
|
||||
var hasChanged = false;
|
||||
for (final info in updates) {
|
||||
@@ -252,7 +262,7 @@ class Room extends ChangeNotifier with ParticipantDelegate {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (info.state == ParticipantInfo_State.DISCONNECTED) {
|
||||
if (info.state == lk_models.ParticipantInfo_State.DISCONNECTED) {
|
||||
hasChanged = true;
|
||||
_handleParticipantDisconnect(info.sid);
|
||||
continue;
|
||||
@@ -274,7 +284,7 @@ class Room extends ChangeNotifier with ParticipantDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSpeakerUpdate(List<SpeakerInfo> speakers) {
|
||||
void _handleSpeakerUpdate(List<lk_models.SpeakerInfo> speakers) {
|
||||
final seenSids = <String>{};
|
||||
List<Participant> newSpeakers = [];
|
||||
for (final info in speakers) {
|
||||
@@ -312,7 +322,7 @@ class Room extends ChangeNotifier with ParticipantDelegate {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _handleDataPacket(UserPacket packet, DataPacket_Kind kind) {
|
||||
void _handleDataPacket(lk_models.UserPacket packet, lk_models.DataPacket_Kind kind) {
|
||||
final participant = participants[packet.participantSid];
|
||||
if (participant == null) {
|
||||
return;
|
||||
@@ -322,6 +332,14 @@ class Room extends ChangeNotifier with ParticipantDelegate {
|
||||
delegate?.onDataReceived(participant, packet.payload);
|
||||
}
|
||||
|
||||
void _onRemoteMuteChanged(String sid, bool mute) {
|
||||
final track = localParticipant.tracks[sid];
|
||||
//
|
||||
// This will trigger signalClient.sendMuteTrack(sid, mute);
|
||||
//
|
||||
track?.muted = mute;
|
||||
}
|
||||
|
||||
void _onTrackAdded(MediaStreamTrack track, MediaStream? stream, RTCRtpReceiver? receiver) {
|
||||
if (stream == null) {
|
||||
// we need the stream to get the track's id
|
||||
@@ -329,8 +347,8 @@ class Room extends ChangeNotifier with ParticipantDelegate {
|
||||
return;
|
||||
}
|
||||
|
||||
var parsed = _unpackStreamId(stream.id);
|
||||
var trackSid = parsed.item2 ?? track.id;
|
||||
final parsed = _unpackStreamId(stream.id);
|
||||
final trackSid = parsed.item2 ?? track.id;
|
||||
|
||||
final participant = _getOrCreateRemoteParticipant(parsed.item1, null);
|
||||
participant.addSubscribedMediaTrack(track, stream, trackSid);
|
||||
|
||||
+89
-84
@@ -1,12 +1,13 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
|
||||
import 'errors.dart';
|
||||
import 'extensions.dart';
|
||||
import 'logger.dart';
|
||||
import 'options.dart';
|
||||
import 'proto/livekit_rtc.pb.dart';
|
||||
import 'proto/livekit_models.pb.dart';
|
||||
import 'proto/livekit_models.pb.dart' as lk_models;
|
||||
import 'proto/livekit_rtc.pb.dart' as lk_rtc;
|
||||
import 'signal_client.dart';
|
||||
import 'track/track.dart';
|
||||
import 'transport.dart';
|
||||
@@ -19,10 +20,15 @@ const iceRestartTimeout = Duration(seconds: 10);
|
||||
|
||||
typedef GenericCallback = void Function();
|
||||
typedef TrackCallback = void Function(
|
||||
MediaStreamTrack track, MediaStream? stream, RTCRtpReceiver? receiver);
|
||||
typedef ParticipantUpdateCallback = void Function(List<ParticipantInfo> participants);
|
||||
typedef ActiveSpeakerChangedCallback = void Function(List<SpeakerInfo> speakers);
|
||||
typedef DataPacketCallback = void Function(UserPacket packet, DataPacket_Kind kind);
|
||||
MediaStreamTrack track,
|
||||
MediaStream? stream,
|
||||
RTCRtpReceiver? receiver,
|
||||
);
|
||||
typedef ParticipantUpdateCallback = void Function(List<lk_models.ParticipantInfo> participants);
|
||||
typedef ActiveSpeakerChangedCallback = void Function(List<lk_models.SpeakerInfo> speakers);
|
||||
typedef DataPacketCallback = void Function(
|
||||
lk_models.UserPacket packet, lk_models.DataPacket_Kind kind);
|
||||
typedef RemoteMuteCallback = void Function(String sid, bool mute);
|
||||
|
||||
class RTCEngine with SignalClientDelegate {
|
||||
PCTransport? publisher;
|
||||
@@ -36,10 +42,10 @@ class RTCEngine with SignalClientDelegate {
|
||||
bool iceConnected = false;
|
||||
bool isReconnecting = false;
|
||||
bool isClosed = true;
|
||||
Map<String, Completer<TrackInfo>> pendingTrackResolvers = {};
|
||||
Map<String, Completer<lk_models.TrackInfo>> pendingTrackResolvers = {};
|
||||
int reconnectAttempts = 0;
|
||||
// to complete join request
|
||||
Completer<JoinResponse>? joinCompleter;
|
||||
Completer<lk_rtc.JoinResponse>? joinCompleter;
|
||||
// remember url and token for reconnect
|
||||
String? url;
|
||||
String? token;
|
||||
@@ -47,9 +53,10 @@ class RTCEngine with SignalClientDelegate {
|
||||
// delegate methods
|
||||
GenericCallback? onICEConnected;
|
||||
TrackCallback? onTrack;
|
||||
ParticipantUpdateCallback? onParticipantUpdateCallback;
|
||||
ActiveSpeakerChangedCallback? onActiveSpeakerchangedCallback;
|
||||
DataPacketCallback? onDataMessageCallback;
|
||||
ParticipantUpdateCallback? onParticipantUpdated;
|
||||
ActiveSpeakerChangedCallback? onActiveSpeakerUpdated;
|
||||
DataPacketCallback? onDataMessage;
|
||||
RemoteMuteCallback? onRemoteMute;
|
||||
GenericCallback? onReconnecting;
|
||||
GenericCallback? onReconnected;
|
||||
GenericCallback? onDisconnected;
|
||||
@@ -62,18 +69,18 @@ class RTCEngine with SignalClientDelegate {
|
||||
client.delegate = this;
|
||||
}
|
||||
|
||||
Future<JoinResponse> join(String url, String token, JoinOptions? opts) async {
|
||||
Future<lk_rtc.JoinResponse> join(
|
||||
String url,
|
||||
String token, {
|
||||
ConnectOptions? options,
|
||||
}) async {
|
||||
this.url = url;
|
||||
this.token = token;
|
||||
|
||||
final completer = Completer<JoinResponse>();
|
||||
final completer = Completer<lk_rtc.JoinResponse>();
|
||||
joinCompleter = completer;
|
||||
|
||||
try {
|
||||
await client.join(url, token, opts);
|
||||
} catch (e) {
|
||||
return Future.error(e);
|
||||
}
|
||||
await client.join(url, token, options: options);
|
||||
|
||||
// if it's not complete after 5 seconds, fail
|
||||
Timer(connectionTimeout, () {
|
||||
@@ -84,35 +91,30 @@ class RTCEngine with SignalClientDelegate {
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
void close() async {
|
||||
Future<void> close() async {
|
||||
isClosed = true;
|
||||
|
||||
if (publisher != null) {
|
||||
final senders = await publisher?.pc.getSenders();
|
||||
for (final element in (senders ?? <RTCRtpSender>[])) {
|
||||
await publisher?.pc.removeTrack(element);
|
||||
}
|
||||
|
||||
publisher?.pc.close();
|
||||
// PCTransport is responsible for disposing RTCPeerConnection
|
||||
await publisher?.dispose();
|
||||
publisher = null;
|
||||
}
|
||||
if (subscriber != null) {
|
||||
subscriber?.pc.close();
|
||||
|
||||
await subscriber?.dispose();
|
||||
subscriber = null;
|
||||
}
|
||||
|
||||
client.close();
|
||||
}
|
||||
|
||||
Future<TrackInfo> addTrack(
|
||||
{required String cid,
|
||||
Future<lk_models.TrackInfo> addTrack({
|
||||
required String cid,
|
||||
required String name,
|
||||
required TrackType kind,
|
||||
TrackDimension? dimension}) async {
|
||||
required lk_models.TrackType kind,
|
||||
TrackDimension? dimension,
|
||||
}) async {
|
||||
if (pendingTrackResolvers[cid] != null) {
|
||||
throw TrackPublishError('a track with the same CID has already been published');
|
||||
}
|
||||
|
||||
final completer = Completer<TrackInfo>();
|
||||
final completer = Completer<lk_models.TrackInfo>();
|
||||
pendingTrackResolvers[cid] = completer;
|
||||
|
||||
client.sendAddTrack(cid: cid, name: name, type: kind, dimension: dimension);
|
||||
@@ -122,9 +124,7 @@ class RTCEngine with SignalClientDelegate {
|
||||
|
||||
Future<void> negotiate({bool? iceRestart}) async {
|
||||
final pub = publisher;
|
||||
if (pub == null) {
|
||||
return;
|
||||
}
|
||||
if (pub == null) return;
|
||||
|
||||
final remoteDesc = await pub.getRemoteDescription();
|
||||
|
||||
@@ -142,14 +142,15 @@ class RTCEngine with SignalClientDelegate {
|
||||
};
|
||||
}
|
||||
final offer = await pub.pc.createOffer(constraints);
|
||||
logger.fine('Created offer');
|
||||
logger.finer('sdp: ${offer.sdp}');
|
||||
await pub.pc.setLocalDescription(offer);
|
||||
client.sendOffer(offer);
|
||||
}
|
||||
|
||||
Future<void> reconnect() async {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
if (isClosed) return;
|
||||
|
||||
final url = this.url;
|
||||
final token = this.token;
|
||||
if (url == null || token == null) {
|
||||
@@ -174,9 +175,9 @@ class RTCEngine with SignalClientDelegate {
|
||||
sub.restartingIce = true;
|
||||
|
||||
await negotiate(iceRestart: true);
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
isReconnecting = false;
|
||||
return Future.error(e);
|
||||
return Future.error(error);
|
||||
}
|
||||
|
||||
// wait for connectivity to change
|
||||
@@ -190,7 +191,7 @@ class RTCEngine with SignalClientDelegate {
|
||||
}
|
||||
|
||||
isReconnecting = false;
|
||||
return Future.error(ConnectError('could not reconnect ICE'));
|
||||
throw ConnectError('could not reconnect ICE');
|
||||
}
|
||||
|
||||
Future<void> _configurePeerConnections() async {
|
||||
@@ -204,18 +205,18 @@ class RTCEngine with SignalClientDelegate {
|
||||
subscriber = PCTransport(subPC);
|
||||
|
||||
pubPC.onIceCandidate = (RTCIceCandidate candidate) {
|
||||
client.sendIceCandidate(candidate, SignalTarget.PUBLISHER);
|
||||
client.sendIceCandidate(candidate, lk_rtc.SignalTarget.PUBLISHER);
|
||||
};
|
||||
subPC.onIceCandidate = (RTCIceCandidate candidate) {
|
||||
client.sendIceCandidate(candidate, SignalTarget.SUBSCRIBER);
|
||||
client.sendIceCandidate(candidate, lk_rtc.SignalTarget.SUBSCRIBER);
|
||||
};
|
||||
|
||||
pubPC.onRenegotiationNeeded = () {
|
||||
pubPC.onRenegotiationNeeded = () async {
|
||||
if (pubPC.iceConnectionState == null ||
|
||||
pubPC.iceConnectionState == RTCIceConnectionState.RTCIceConnectionStateNew) {
|
||||
return;
|
||||
}
|
||||
negotiate();
|
||||
await negotiate();
|
||||
};
|
||||
|
||||
pubPC.onIceConnectionState = (RTCIceConnectionState state) {
|
||||
@@ -272,27 +273,26 @@ class RTCEngine with SignalClientDelegate {
|
||||
return;
|
||||
}
|
||||
|
||||
final dp = DataPacket.fromBuffer(message.binary);
|
||||
final dp = lk_models.DataPacket.fromBuffer(message.binary);
|
||||
switch (dp.whichValue()) {
|
||||
case DataPacket_Value.speaker:
|
||||
onActiveSpeakerchangedCallback?.call(dp.speaker.speakers);
|
||||
case lk_models.DataPacket_Value.speaker:
|
||||
onActiveSpeakerUpdated?.call(dp.speaker.speakers);
|
||||
break;
|
||||
case DataPacket_Value.user:
|
||||
onDataMessageCallback?.call(dp.user, dp.kind);
|
||||
case lk_models.DataPacket_Value.user:
|
||||
onDataMessage?.call(dp.user, dp.kind);
|
||||
break;
|
||||
default:
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
void _handleDisconnect(String reason) {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
Future<void> _handleDisconnect(String reason) async {
|
||||
if (isClosed) return;
|
||||
|
||||
logger.fine('disconnected $reason');
|
||||
if (reconnectAttempts >= maxReconnectAttempts) {
|
||||
logger.info('could not connect after $reconnectAttempts, giving up');
|
||||
close();
|
||||
await close();
|
||||
onDisconnected?.call();
|
||||
return;
|
||||
}
|
||||
@@ -310,7 +310,7 @@ class RTCEngine with SignalClientDelegate {
|
||||
//------------------ SignalClient Delegate methods -------------------------//
|
||||
|
||||
@override
|
||||
void onConnected(JoinResponse response) async {
|
||||
Future<void> onConnected(lk_rtc.JoinResponse response) async {
|
||||
// create peer connections
|
||||
isClosed = false;
|
||||
|
||||
@@ -331,67 +331,72 @@ class RTCEngine with SignalClientDelegate {
|
||||
|
||||
await _configurePeerConnections();
|
||||
|
||||
negotiate();
|
||||
await negotiate();
|
||||
|
||||
joinCompleter?.complete(Future.value(response));
|
||||
joinCompleter = null;
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose([String? reason]) {
|
||||
_handleDisconnect('signal');
|
||||
Future<void> onClose([String? reason]) async {
|
||||
await _handleDisconnect('signal');
|
||||
}
|
||||
|
||||
@override
|
||||
void onOffer(RTCSessionDescription sd) async {
|
||||
Future<void> onOffer(RTCSessionDescription sd) async {
|
||||
final sub = subscriber;
|
||||
if (sub == null) {
|
||||
return;
|
||||
}
|
||||
if (sub == null) return;
|
||||
|
||||
await sub.setRemoteDescription(sd);
|
||||
|
||||
final answer = await sub.pc.createAnswer();
|
||||
logger.fine('Created answer');
|
||||
logger.finer('sdp: ${answer.sdp}');
|
||||
await sub.pc.setLocalDescription(answer);
|
||||
client.sendAnswer(answer);
|
||||
}
|
||||
|
||||
@override
|
||||
void onAnswer(RTCSessionDescription sd) {
|
||||
if (publisher == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
publisher?.setRemoteDescription(sd);
|
||||
Future<void> onAnswer(RTCSessionDescription sd) async {
|
||||
if (publisher == null) return;
|
||||
logger.fine('Received answer');
|
||||
logger.finer('sdp: ${sd.sdp}');
|
||||
await publisher!.setRemoteDescription(sd);
|
||||
}
|
||||
|
||||
@override
|
||||
void onTrickle(RTCIceCandidate candidate, SignalTarget target) {
|
||||
if (target == SignalTarget.SUBSCRIBER) {
|
||||
subscriber?.addIceCandidate(candidate);
|
||||
} else if (target == SignalTarget.PUBLISHER) {
|
||||
publisher?.addIceCandidate(candidate);
|
||||
Future<void> onTrickle(RTCIceCandidate candidate, lk_rtc.SignalTarget target) async {
|
||||
if (target == lk_rtc.SignalTarget.SUBSCRIBER) {
|
||||
await subscriber?.addIceCandidate(candidate);
|
||||
} else if (target == lk_rtc.SignalTarget.PUBLISHER) {
|
||||
await publisher?.addIceCandidate(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onParticipantUpdate(List<ParticipantInfo> updates) {
|
||||
onParticipantUpdateCallback?.call(updates);
|
||||
Future<void> onParticipantUpdate(List<lk_models.ParticipantInfo> updates) async {
|
||||
onParticipantUpdated?.call(updates);
|
||||
}
|
||||
|
||||
@override
|
||||
void onLocalTrackPublished(TrackPublishedResponse response) {
|
||||
Future<void> onLocalTrackPublished(lk_rtc.TrackPublishedResponse response) async {
|
||||
final completer = pendingTrackResolvers.remove(response.cid);
|
||||
completer?.complete(Future.value(response.track));
|
||||
}
|
||||
|
||||
@override
|
||||
void onActiveSpeakersChanged(List<SpeakerInfo> speakers) {
|
||||
onActiveSpeakerchangedCallback?.call(speakers);
|
||||
Future<void> onActiveSpeakersChanged(List<lk_models.SpeakerInfo> speakers) async {
|
||||
onActiveSpeakerUpdated?.call(speakers);
|
||||
}
|
||||
|
||||
@override
|
||||
void onLeave(LeaveRequest req) {
|
||||
close();
|
||||
Future<void> onLeave(lk_rtc.LeaveRequest req) async {
|
||||
await close();
|
||||
onDisconnected?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onMuteTrack(lk_rtc.MuteTrackRequest req) async {
|
||||
onRemoteMute?.call(req.sid, req.muted);
|
||||
}
|
||||
}
|
||||
|
||||
+177
-120
@@ -3,134 +3,191 @@ import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:livekit_client/src/ws/interface.dart';
|
||||
import 'package:synchronized/synchronized.dart' as sync;
|
||||
|
||||
import 'errors.dart';
|
||||
import 'logger.dart';
|
||||
import 'options.dart';
|
||||
import 'proto/livekit_models.pb.dart' as lk_models;
|
||||
import 'proto/livekit_rtc.pb.dart' as lk_rtc;
|
||||
import 'track/track.dart';
|
||||
import 'version.dart';
|
||||
import 'proto/livekit_models.pb.dart';
|
||||
import 'proto/livekit_rtc.pb.dart';
|
||||
import '_websocket_api.dart'
|
||||
if (dart.library.io) '_websocket_io.dart'
|
||||
if (dart.library.html) '_websocket_html.dart' as platform;
|
||||
|
||||
mixin SignalClientDelegate {
|
||||
// initial connection established
|
||||
void onConnected(JoinResponse response);
|
||||
Future<void> onConnected(lk_rtc.JoinResponse response);
|
||||
// websocket has closed
|
||||
void onClose([String? reason]);
|
||||
Future<void> onClose([String? reason]);
|
||||
// when a server offer is received
|
||||
void onOffer(RTCSessionDescription sd);
|
||||
Future<void> onOffer(RTCSessionDescription sd);
|
||||
// when an answer from server is received
|
||||
void onAnswer(RTCSessionDescription sd);
|
||||
Future<void> onAnswer(RTCSessionDescription sd);
|
||||
// when server has a new ICE candidate
|
||||
void onTrickle(RTCIceCandidate candidate, SignalTarget target);
|
||||
Future<void> onTrickle(RTCIceCandidate candidate, lk_rtc.SignalTarget target);
|
||||
// participant has changed
|
||||
void onParticipantUpdate(List<ParticipantInfo> updates);
|
||||
Future<void> onParticipantUpdate(List<lk_models.ParticipantInfo> updates);
|
||||
// when a track has been added successfully
|
||||
void onLocalTrackPublished(TrackPublishedResponse response);
|
||||
Future<void> onLocalTrackPublished(lk_rtc.TrackPublishedResponse response);
|
||||
// active speaker has changed
|
||||
void onActiveSpeakersChanged(List<SpeakerInfo> speakers);
|
||||
Future<void> onActiveSpeakersChanged(List<lk_models.SpeakerInfo> speakers);
|
||||
// when server sends this client a leave message
|
||||
void onLeave(LeaveRequest req);
|
||||
Future<void> onLeave(lk_rtc.LeaveRequest req);
|
||||
// explicit mute track
|
||||
Future<void> onMuteTrack(lk_rtc.MuteTrackRequest req);
|
||||
}
|
||||
|
||||
extension LKUriExt on Uri {
|
||||
bool get isSecureScheme => ['https', 'wss'].contains(scheme);
|
||||
}
|
||||
|
||||
class SignalClient {
|
||||
SignalClientDelegate? delegate;
|
||||
static const protocolVersion = 2;
|
||||
|
||||
final _lock = sync.Lock();
|
||||
SignalClientDelegate? delegate;
|
||||
bool _connected = false;
|
||||
WebSocketChannel? _ws;
|
||||
LKWebSocket? _ws;
|
||||
|
||||
SignalClient();
|
||||
|
||||
bool get connected => _connected;
|
||||
|
||||
Future<void> join(String url, String token, JoinOptions? options) async {
|
||||
final rtcUrl = '$url/rtc';
|
||||
var params = _joinParams(token);
|
||||
if (options != null && options.autoSubscribe != null) {
|
||||
params += '&auto_subscribe=${options.autoSubscribe! ? '1' : '0'}';
|
||||
Uri _buildUri(
|
||||
String uriOrString, {
|
||||
required String token,
|
||||
ConnectOptions? options,
|
||||
bool reconnect = false,
|
||||
bool validate = false,
|
||||
bool forceSecure = false,
|
||||
}) {
|
||||
final Uri uri = Uri.parse(uriOrString);
|
||||
|
||||
final useSecure = uri.isSecureScheme || forceSecure;
|
||||
final httpScheme = useSecure ? 'https' : 'http';
|
||||
final wsScheme = useSecure ? 'wss' : 'ws';
|
||||
|
||||
return uri.replace(
|
||||
scheme: validate ? httpScheme : wsScheme,
|
||||
path: validate ? 'validate' : 'rtc',
|
||||
queryParameters: <String, String>{
|
||||
'access_token': token,
|
||||
if (options != null) 'auto_subscribe': options.autoSubscribe ? '1' : '0',
|
||||
if (reconnect) 'reconnect': '1',
|
||||
'protocol': protocolVersion.toString(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> join(
|
||||
String uriString,
|
||||
String token, {
|
||||
ConnectOptions? options,
|
||||
}) async {
|
||||
// Create default options if null
|
||||
options ??= const ConnectOptions();
|
||||
|
||||
final rtcUri = _buildUri(
|
||||
uriString,
|
||||
token: token,
|
||||
options: options,
|
||||
);
|
||||
|
||||
try {
|
||||
final ws = await platform.connectToWebSocket(Uri.parse(rtcUrl + params));
|
||||
ws.stream.listen(_handleMessage, onError: _handleError, onDone: _handleDone);
|
||||
_ws = ws;
|
||||
} catch (e) {
|
||||
final completer = Completer<void>();
|
||||
final validateUri = Uri.parse('http${rtcUrl.substring(2)}/validate$params');
|
||||
http.get(validateUri).then((response) {
|
||||
if (response.statusCode != 200) {
|
||||
completer.completeError(ConnectError(response.body));
|
||||
} else {
|
||||
completer.completeError(ConnectError());
|
||||
}
|
||||
}).catchError((dynamic e) {
|
||||
completer.completeError(ConnectError());
|
||||
});
|
||||
_ws = await LKWebSocket.connect(
|
||||
rtcUri,
|
||||
LKWebSocketOptions(
|
||||
onData: _onSocketData,
|
||||
onDispose: _onSocketDone,
|
||||
onError: _handleError,
|
||||
),
|
||||
);
|
||||
} catch (socketError) {
|
||||
// Re-build same uri for validate mode
|
||||
final validateUri = _buildUri(
|
||||
uriString,
|
||||
token: token,
|
||||
options: options,
|
||||
validate: true,
|
||||
forceSecure: rtcUri.isSecureScheme,
|
||||
);
|
||||
|
||||
return completer.future;
|
||||
// Attempt Validation
|
||||
try {
|
||||
final validateResponse = await http.get(validateUri);
|
||||
if (validateResponse.statusCode != 200) throw ConnectError(validateResponse.body);
|
||||
throw ConnectError();
|
||||
} catch (error) {
|
||||
// Pass it up if it's already a `ConnectError`
|
||||
if (error is ConnectError) rethrow;
|
||||
// HTTP doesn't work either
|
||||
throw ConnectError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reconnect(String url, String token) async {
|
||||
Future<void> reconnect(
|
||||
String uriString,
|
||||
String token,
|
||||
) async {
|
||||
_connected = false;
|
||||
_ws?.sink.close();
|
||||
_ws?.dispose();
|
||||
_ws = null;
|
||||
|
||||
url += '/rtc';
|
||||
var params = _joinParams(token);
|
||||
params += '&reconnect=1';
|
||||
final uri = Uri.parse(url + params);
|
||||
final rtcUri = _buildUri(
|
||||
uriString,
|
||||
token: token,
|
||||
reconnect: true,
|
||||
);
|
||||
|
||||
_ws = await LKWebSocket.connect(
|
||||
rtcUri,
|
||||
LKWebSocketOptions(
|
||||
onData: _onSocketData,
|
||||
onDispose: _onSocketDone,
|
||||
onError: _handleError,
|
||||
),
|
||||
);
|
||||
|
||||
final ws = await platform.connectToWebSocket(uri);
|
||||
_ws = ws;
|
||||
_connected = true;
|
||||
}
|
||||
|
||||
void close() {
|
||||
_connected = false;
|
||||
_ws?.sink.close();
|
||||
_ws?.dispose();
|
||||
}
|
||||
|
||||
void sendOffer(RTCSessionDescription offer) {
|
||||
_sendRequest(SignalRequest(
|
||||
void sendOffer(RTCSessionDescription offer) => _sendRequest(lk_rtc.SignalRequest(
|
||||
offer: fromRTCSessionDescription(offer),
|
||||
));
|
||||
}
|
||||
|
||||
void sendAnswer(RTCSessionDescription answer) {
|
||||
_sendRequest(SignalRequest(
|
||||
void sendAnswer(RTCSessionDescription answer) => _sendRequest(lk_rtc.SignalRequest(
|
||||
answer: fromRTCSessionDescription(answer),
|
||||
));
|
||||
}
|
||||
|
||||
void sendIceCandidate(RTCIceCandidate candidate, SignalTarget target) {
|
||||
_sendRequest(SignalRequest(
|
||||
trickle: TrickleRequest(
|
||||
void sendIceCandidate(RTCIceCandidate candidate, lk_rtc.SignalTarget target) => _sendRequest(
|
||||
lk_rtc.SignalRequest(
|
||||
trickle: lk_rtc.TrickleRequest(
|
||||
candidateInit: fromRTCIceCandidate(candidate),
|
||||
target: target,
|
||||
)));
|
||||
}
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
void sendMuteTrack(String trackSid, bool muted) {
|
||||
_sendRequest(SignalRequest(
|
||||
mute: MuteTrackRequest(
|
||||
void sendMuteTrack(String trackSid, bool muted) => _sendRequest(lk_rtc.SignalRequest(
|
||||
mute: lk_rtc.MuteTrackRequest(
|
||||
sid: trackSid,
|
||||
muted: muted,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
void sendAddTrack(
|
||||
{required String cid,
|
||||
void sendAddTrack({
|
||||
required String cid,
|
||||
required String name,
|
||||
required TrackType type,
|
||||
TrackDimension? dimension}) {
|
||||
final req = AddTrackRequest(
|
||||
required lk_models.TrackType type,
|
||||
TrackDimension? dimension,
|
||||
}) {
|
||||
final req = lk_rtc.AddTrackRequest(
|
||||
cid: cid,
|
||||
name: name,
|
||||
type: type,
|
||||
@@ -139,109 +196,109 @@ class SignalClient {
|
||||
req.width = dimension.width;
|
||||
req.height = dimension.height;
|
||||
}
|
||||
_sendRequest(SignalRequest(
|
||||
_sendRequest(lk_rtc.SignalRequest(
|
||||
addTrack: req,
|
||||
));
|
||||
}
|
||||
|
||||
void sendUpdateTrackSettings(UpdateTrackSettings settings) {
|
||||
_sendRequest(SignalRequest(
|
||||
void sendUpdateTrackSettings(lk_rtc.UpdateTrackSettings settings) =>
|
||||
_sendRequest(lk_rtc.SignalRequest(
|
||||
trackSetting: settings,
|
||||
));
|
||||
}
|
||||
|
||||
void sendUpdateSubscription(UpdateSubscription subscription) {
|
||||
_sendRequest(SignalRequest(
|
||||
void sendUpdateSubscription(lk_rtc.UpdateSubscription subscription) =>
|
||||
_sendRequest(lk_rtc.SignalRequest(
|
||||
subscription: subscription,
|
||||
));
|
||||
}
|
||||
|
||||
void sendSetSimulcastLayers(String trackSid, List<VideoQuality> layers) {
|
||||
_sendRequest(SignalRequest(
|
||||
simulcast: SetSimulcastLayers(
|
||||
void sendSetSimulcastLayers(String trackSid, List<lk_rtc.VideoQuality> layers) =>
|
||||
_sendRequest(lk_rtc.SignalRequest(
|
||||
simulcast: lk_rtc.SetSimulcastLayers(
|
||||
trackSid: trackSid,
|
||||
layers: layers,
|
||||
)));
|
||||
}
|
||||
|
||||
void sendLeave() {
|
||||
_sendRequest(SignalRequest(
|
||||
leave: LeaveRequest(),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
void _sendRequest(SignalRequest req) {
|
||||
void sendLeave() => _sendRequest(lk_rtc.SignalRequest(
|
||||
leave: lk_rtc.LeaveRequest(),
|
||||
));
|
||||
|
||||
void _sendRequest(lk_rtc.SignalRequest req) {
|
||||
if (_ws == null) {
|
||||
log('could not send message, not connected');
|
||||
return;
|
||||
}
|
||||
|
||||
final buf = req.writeToBuffer();
|
||||
_ws?.sink.add(buf);
|
||||
_ws?.send(buf);
|
||||
}
|
||||
|
||||
void _handleMessage(dynamic message) {
|
||||
if (message is! List<int>) {
|
||||
return;
|
||||
}
|
||||
final msg = SignalResponse.fromBuffer(message);
|
||||
Future<void> _onSocketData(dynamic message) async {
|
||||
if (message is! List<int>) return;
|
||||
final msg = lk_rtc.SignalResponse.fromBuffer(message);
|
||||
|
||||
// Ensure previous delegate method's future is completed
|
||||
// before calling another method
|
||||
await _lock.synchronized(() async {
|
||||
//
|
||||
switch (msg.whichMessage()) {
|
||||
case SignalResponse_Message.join:
|
||||
case lk_rtc.SignalResponse_Message.join:
|
||||
if (!_connected) {
|
||||
_connected = true;
|
||||
delegate?.onConnected(msg.join);
|
||||
await delegate?.onConnected(msg.join);
|
||||
}
|
||||
break;
|
||||
case SignalResponse_Message.answer:
|
||||
delegate?.onAnswer(toRTCSessionDescription(msg.answer));
|
||||
case lk_rtc.SignalResponse_Message.answer:
|
||||
await delegate?.onAnswer(toRTCSessionDescription(msg.answer));
|
||||
break;
|
||||
case SignalResponse_Message.offer:
|
||||
delegate?.onOffer(toRTCSessionDescription(msg.offer));
|
||||
case lk_rtc.SignalResponse_Message.offer:
|
||||
await delegate?.onOffer(toRTCSessionDescription(msg.offer));
|
||||
break;
|
||||
case SignalResponse_Message.trickle:
|
||||
delegate?.onTrickle(toRTCIceCandidate(msg.trickle.candidateInit), msg.trickle.target);
|
||||
case lk_rtc.SignalResponse_Message.trickle:
|
||||
await delegate?.onTrickle(
|
||||
toRTCIceCandidate(msg.trickle.candidateInit),
|
||||
msg.trickle.target,
|
||||
);
|
||||
break;
|
||||
case SignalResponse_Message.update:
|
||||
delegate?.onParticipantUpdate(msg.update.participants);
|
||||
case lk_rtc.SignalResponse_Message.update:
|
||||
await delegate?.onParticipantUpdate(msg.update.participants);
|
||||
break;
|
||||
case SignalResponse_Message.trackPublished:
|
||||
delegate?.onLocalTrackPublished(msg.trackPublished);
|
||||
case lk_rtc.SignalResponse_Message.trackPublished:
|
||||
await delegate?.onLocalTrackPublished(msg.trackPublished);
|
||||
break;
|
||||
case SignalResponse_Message.speaker:
|
||||
delegate?.onActiveSpeakersChanged(msg.speaker.speakers);
|
||||
case lk_rtc.SignalResponse_Message.speaker:
|
||||
await delegate?.onActiveSpeakersChanged(msg.speaker.speakers);
|
||||
break;
|
||||
case SignalResponse_Message.leave:
|
||||
delegate?.onLeave(msg.leave);
|
||||
case lk_rtc.SignalResponse_Message.leave:
|
||||
await delegate?.onLeave(msg.leave);
|
||||
break;
|
||||
case lk_rtc.SignalResponse_Message.mute:
|
||||
await delegate?.onMuteTrack(msg.mute);
|
||||
break;
|
||||
default:
|
||||
log('unsupported message: ' + json.encode(msg));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _handleError(Object error) {
|
||||
void _handleError(dynamic error) {
|
||||
logger.warning('received websocket error $error');
|
||||
}
|
||||
|
||||
void _handleDone() {
|
||||
if (!_connected) {
|
||||
return;
|
||||
}
|
||||
void _onSocketDone() {
|
||||
if (!_connected) return;
|
||||
_ws = null;
|
||||
_connected = false;
|
||||
delegate?.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
String _joinParams(String token) {
|
||||
return '?access_token=$token&protocol=$protocolVersion';
|
||||
}
|
||||
|
||||
RTCSessionDescription toRTCSessionDescription(SessionDescription sd) {
|
||||
RTCSessionDescription toRTCSessionDescription(lk_rtc.SessionDescription sd) {
|
||||
return RTCSessionDescription(sd.sdp, sd.type);
|
||||
}
|
||||
|
||||
SessionDescription fromRTCSessionDescription(RTCSessionDescription rsd) {
|
||||
return SessionDescription(type: rsd.type, sdp: rsd.sdp);
|
||||
lk_rtc.SessionDescription fromRTCSessionDescription(RTCSessionDescription rsd) {
|
||||
return lk_rtc.SessionDescription(type: rsd.type, sdp: rsd.sdp);
|
||||
}
|
||||
|
||||
RTCIceCandidate toRTCIceCandidate(String candidateInit) {
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
|
||||
import '../proto/livekit_models.pbenum.dart';
|
||||
import '../proto/livekit_models.pb.dart' as lk_models;
|
||||
import '_audio_api.dart' if (dart.library.html) '_audio_html.dart' as audio;
|
||||
import 'local_audio_track.dart';
|
||||
import 'track.dart';
|
||||
import '_audio_api.dart' if (dart.library.html) '_audio_html.dart' as audio;
|
||||
|
||||
class AudioTrack extends Track {
|
||||
MediaStream? mediaStream;
|
||||
|
||||
AudioTrack(String name, MediaStreamTrack track, this.mediaStream)
|
||||
: super(TrackType.AUDIO, name, track);
|
||||
: super(lk_models.TrackType.AUDIO, name, track);
|
||||
|
||||
/// Start playing audio track. On web platform, create an audio element and
|
||||
/// start playback
|
||||
void start() {
|
||||
if (this is! LocalAudioTrack) {
|
||||
audio.startAudio(getCid(), mediaTrack);
|
||||
audio.startAudio(getCid(), mediaStreamTrack);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void stop() {
|
||||
mediaStream?.dispose();
|
||||
Future<void> stop() async {
|
||||
await mediaStream?.dispose();
|
||||
mediaStream = null;
|
||||
audio.stopAudio(getCid());
|
||||
super.stop();
|
||||
await super.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
import 'package:livekit_client/src/track/audio_track.dart';
|
||||
|
||||
import '../errors.dart';
|
||||
import 'audio_track.dart';
|
||||
import 'options.dart';
|
||||
|
||||
class LocalAudioTrack extends AudioTrack {
|
||||
LocalAudioTrack(String name, MediaStreamTrack track, MediaStream stream)
|
||||
: super(name, track, stream);
|
||||
LocalAudioTrack(
|
||||
String name,
|
||||
MediaStreamTrack track,
|
||||
MediaStream stream,
|
||||
) : super(name, track, stream);
|
||||
|
||||
/// Creates a new audio track from the default audio input device.
|
||||
static Future<LocalAudioTrack> createTrack([LocalAudioTrackOptions? options]) async {
|
||||
try {
|
||||
static Future<LocalAudioTrack> create([LocalAudioTrackOptions? options]) async {
|
||||
// try {
|
||||
final stream = await navigator.mediaDevices.getUserMedia(<String, dynamic>{
|
||||
'audio': true,
|
||||
'video': false,
|
||||
});
|
||||
|
||||
if (stream.getAudioTracks().isEmpty) {
|
||||
return Future.error(TrackCreateError());
|
||||
}
|
||||
if (stream.getAudioTracks().isEmpty) throw TrackCreateError();
|
||||
|
||||
return LocalAudioTrack('', stream.getAudioTracks().first, stream);
|
||||
} catch (e) {
|
||||
return Future.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
import 'package:livekit_client/src/logger.dart';
|
||||
|
||||
import '../participant/local_participant.dart';
|
||||
import '../proto/livekit_models.pb.dart';
|
||||
import '../proto/livekit_models.pb.dart' as lk_models;
|
||||
import 'track.dart';
|
||||
import 'track_publication.dart';
|
||||
|
||||
class LocalTrackPublication extends TrackPublication {
|
||||
final LocalParticipant _participant;
|
||||
|
||||
LocalTrackPublication(TrackInfo info, Track track, this._participant) : super.fromInfo(info) {
|
||||
LocalTrackPublication(
|
||||
lk_models.TrackInfo info,
|
||||
Track track,
|
||||
this._participant,
|
||||
) : super.fromInfo(info) {
|
||||
this.track = track;
|
||||
}
|
||||
|
||||
/// Mute or unmute the current track. When muted, track will stop sending data
|
||||
@override
|
||||
set muted(bool val) {
|
||||
if (val == muted) {
|
||||
return;
|
||||
}
|
||||
if (val == muted) return;
|
||||
logger.finer('setMute: ${val}');
|
||||
|
||||
super.muted = val;
|
||||
track?.mediaTrack.enabled = !val;
|
||||
track?.mediaStreamTrack.enabled = !val;
|
||||
_participant.engine.client.sendMuteTrack(sid, val);
|
||||
|
||||
if (val) {
|
||||
|
||||
@@ -1,66 +1,113 @@
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
|
||||
import '../errors.dart';
|
||||
import '../logger.dart';
|
||||
import 'options.dart';
|
||||
import 'track.dart';
|
||||
import 'video_track.dart';
|
||||
|
||||
/// A video track from the local device. Use static methods in this class to create
|
||||
/// video tracks.
|
||||
class LocalVideoTrack extends VideoTrack {
|
||||
//
|
||||
// Options used for this track
|
||||
//
|
||||
LocalVideoTrackOptions currentOptions;
|
||||
|
||||
//
|
||||
// Private constructor
|
||||
//
|
||||
LocalVideoTrack._(
|
||||
String name,
|
||||
MediaStreamTrack mediaTrack,
|
||||
MediaStream stream,
|
||||
this.currentOptions,
|
||||
) : super(name, mediaTrack, stream);
|
||||
|
||||
RTCRtpSender? get sender => transceiver?.sender;
|
||||
|
||||
LocalVideoTrack(String name, MediaStreamTrack mediaTrack, MediaStream stream)
|
||||
: super(name, mediaTrack, stream);
|
||||
|
||||
/// Creates a LocalVideoTrack from camera input.
|
||||
static Future<LocalVideoTrack> createCameraTrack([LocalVideoTrackOptions? options]) async {
|
||||
options ??= LocalVideoTrackOptions(params: VideoPresets.qhd);
|
||||
|
||||
try {
|
||||
final stream = await _createCameraStream(options);
|
||||
return LocalVideoTrack('camera', stream.getVideoTracks().first, stream);
|
||||
} catch (e) {
|
||||
return Future.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Restarts the track with new options. This is useful when switching between
|
||||
/// front and back cameras.
|
||||
Future<void> restartTrack([LocalVideoTrackOptions? options]) async {
|
||||
if (sender == null) {
|
||||
return Future.error(TrackCreateError('could not restart track'));
|
||||
Future<void> restartTrack([
|
||||
LocalVideoTrackOptions? options,
|
||||
]) async {
|
||||
if (sender == null) throw TrackCreateError('could not restart track');
|
||||
if (options != null && currentOptions.runtimeType != options.runtimeType) {
|
||||
throw Exception('options must be a ${currentOptions.runtimeType}');
|
||||
}
|
||||
|
||||
options ??= LocalVideoTrackOptions(params: VideoPresets.qhd);
|
||||
currentOptions = options ?? currentOptions;
|
||||
|
||||
try {
|
||||
final stream = await _createCameraStream(options);
|
||||
final stream = await _createStream(currentOptions);
|
||||
final track = stream.getVideoTracks().first;
|
||||
mediaStream = stream;
|
||||
await mediaTrack.stop();
|
||||
mediaTrack = track;
|
||||
setMediaStream(stream);
|
||||
await mediaStreamTrack.stop();
|
||||
mediaStreamTrack = track;
|
||||
await sender?.replaceTrack(track);
|
||||
} catch (e) {
|
||||
return Future.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<MediaStream> _createCameraStream(LocalVideoTrackOptions? options) async {
|
||||
options ??= LocalVideoTrackOptions(params: VideoPresets.qhd);
|
||||
/// Creates a LocalVideoTrack from camera input.
|
||||
static Future<LocalVideoTrack> createCameraTrack([
|
||||
CameraTrackOptions? options,
|
||||
]) async {
|
||||
options ??= const CameraTrackOptions();
|
||||
final stream = await _createStream(options);
|
||||
return LocalVideoTrack._(
|
||||
Track.cameraName,
|
||||
stream.getVideoTracks().first,
|
||||
stream,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
final stream = await navigator.mediaDevices.getUserMedia(<String, dynamic>{
|
||||
static Future<LocalVideoTrack> createScreenTrack([
|
||||
ScreenTrackOptions? options,
|
||||
]) async {
|
||||
options ??= const ScreenTrackOptions();
|
||||
final stream = await _createStream(options);
|
||||
return LocalVideoTrack._(
|
||||
Track.screenShareName,
|
||||
stream.getVideoTracks().first,
|
||||
stream,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<MediaStream> _createStream(
|
||||
LocalVideoTrackOptions options,
|
||||
) async {
|
||||
final constraints = <String, dynamic>{
|
||||
'audio': false,
|
||||
'video': options.mediaConstraints,
|
||||
});
|
||||
'video': options.toMediaConstraintsMap(),
|
||||
};
|
||||
|
||||
if (stream.getVideoTracks().isEmpty) {
|
||||
return Future.error(TrackCreateError());
|
||||
final MediaStream stream;
|
||||
if (options is ScreenTrackOptions) {
|
||||
stream = await navigator.mediaDevices.getDisplayMedia(constraints);
|
||||
} else {
|
||||
// options is CameraVideoTrackOptions
|
||||
stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
}
|
||||
|
||||
if (stream.getVideoTracks().isEmpty) throw TrackCreateError();
|
||||
return stream;
|
||||
} catch (e) {
|
||||
return Future.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Convenience extensions
|
||||
//
|
||||
extension LKLocalVideoTrackExt on LocalVideoTrack {
|
||||
// Calls restartTrack under the hood
|
||||
Future<void> setCameraPosition(CameraPosition position) async {
|
||||
final options = currentOptions;
|
||||
if (options is! CameraTrackOptions) {
|
||||
logger.warning('Not a camera track');
|
||||
return;
|
||||
}
|
||||
|
||||
await restartTrack(
|
||||
options.copyWith(cameraPosition: position),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+223
-55
@@ -1,26 +1,8 @@
|
||||
/// Options when creating a LocalVideoTrack.
|
||||
class LocalVideoTrackOptions {
|
||||
CameraPosition position = CameraPosition.front;
|
||||
VideoParameter params;
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
|
||||
LocalVideoTrackOptions({
|
||||
VideoParameter? params,
|
||||
CameraPosition? position,
|
||||
}) : params = VideoPresets.qhd {
|
||||
if (params != null) {
|
||||
this.params = params;
|
||||
}
|
||||
if (position != null) {
|
||||
this.position = position;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> get mediaConstraints {
|
||||
return <String, dynamic>{
|
||||
'mandatory': params.mediaConstraints,
|
||||
'facingMode': position == CameraPosition.front ? 'user' : 'environment',
|
||||
};
|
||||
}
|
||||
enum LocalVideoTrackType {
|
||||
camera,
|
||||
display,
|
||||
}
|
||||
|
||||
enum CameraPosition {
|
||||
@@ -28,43 +10,229 @@ enum CameraPosition {
|
||||
back,
|
||||
}
|
||||
|
||||
class VideoParameter {
|
||||
int width;
|
||||
int height;
|
||||
int fps;
|
||||
int? bitrate;
|
||||
|
||||
VideoParameter(
|
||||
this.width,
|
||||
this.height,
|
||||
this.fps, {
|
||||
this.bitrate,
|
||||
});
|
||||
|
||||
Map<String, dynamic> get mediaConstraints {
|
||||
return <String, dynamic>{
|
||||
'minWidth': width,
|
||||
'minHeight': height,
|
||||
'minFrameRate': fps,
|
||||
};
|
||||
}
|
||||
extension LKCameraPositionExt on CameraPosition {
|
||||
CameraPosition swap() => {
|
||||
CameraPosition.front: CameraPosition.back,
|
||||
CameraPosition.back: CameraPosition.front,
|
||||
}[this]!;
|
||||
}
|
||||
|
||||
class VideoPresets {
|
||||
static final qvga = VideoParameter(320, 180, 15);
|
||||
static final vga = VideoParameter(640, 360, 30);
|
||||
static final qhd = VideoParameter(960, 540, 30);
|
||||
static final hd = VideoParameter(1280, 720, 30);
|
||||
static final fhd = VideoParameter(1920, 1080, 30);
|
||||
class CameraTrackOptions extends LocalVideoTrackOptions {
|
||||
final CameraPosition cameraPosition;
|
||||
|
||||
static final List<VideoParameter> all = [
|
||||
qvga,
|
||||
vga,
|
||||
qhd,
|
||||
hd,
|
||||
fhd,
|
||||
const CameraTrackOptions({
|
||||
this.cameraPosition = CameraPosition.front,
|
||||
VideoParameters params = VideoParameters.presetQHD169,
|
||||
}) : super(params: params);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toMediaConstraintsMap() => <String, dynamic>{
|
||||
...super.toMediaConstraintsMap(),
|
||||
'facingMode': cameraPosition == CameraPosition.front ? 'user' : 'environment',
|
||||
};
|
||||
|
||||
// Returns new options with updated properties
|
||||
CameraTrackOptions copyWith({
|
||||
VideoParameters? params,
|
||||
CameraPosition? cameraPosition,
|
||||
}) =>
|
||||
CameraTrackOptions(
|
||||
params: params ?? this.params,
|
||||
cameraPosition: cameraPosition ?? this.cameraPosition,
|
||||
);
|
||||
}
|
||||
|
||||
class ScreenTrackOptions extends LocalVideoTrackOptions {
|
||||
const ScreenTrackOptions();
|
||||
}
|
||||
|
||||
/// Options when creating a LocalVideoTrack.
|
||||
abstract class LocalVideoTrackOptions {
|
||||
// final LocalVideoTrackType type;
|
||||
final VideoParameters params;
|
||||
|
||||
const LocalVideoTrackOptions({
|
||||
this.params = VideoParameters.presetQHD169,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMediaConstraintsMap() => <String, dynamic>{
|
||||
'mandatory': params.toMediaConstraintsMap(),
|
||||
};
|
||||
}
|
||||
|
||||
class VideoEncoding {
|
||||
final int maxFramerate;
|
||||
final int? maxBitrate;
|
||||
|
||||
const VideoEncoding({
|
||||
required this.maxFramerate,
|
||||
this.maxBitrate,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() => '${runtimeType}(maxFramerate: ${maxFramerate}, maxBitrate: ${maxBitrate})';
|
||||
}
|
||||
|
||||
extension VideoEncodingExt on VideoEncoding {
|
||||
RTCRtpEncoding toRTCRtpEncoding({
|
||||
String? rid,
|
||||
double? scaleResolutionDownBy = 1.0,
|
||||
int? numTemporalLayers,
|
||||
}) =>
|
||||
RTCRtpEncoding(
|
||||
rid: rid,
|
||||
scaleResolutionDownBy: scaleResolutionDownBy,
|
||||
maxFramerate: maxFramerate,
|
||||
maxBitrate: maxBitrate,
|
||||
numTemporalLayers: numTemporalLayers,
|
||||
);
|
||||
}
|
||||
|
||||
class VideoParameters {
|
||||
final String description;
|
||||
final int width;
|
||||
final int height;
|
||||
final VideoEncoding encoding;
|
||||
|
||||
const VideoParameters({
|
||||
required this.description,
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.encoding,
|
||||
});
|
||||
|
||||
//
|
||||
// TODO: Make sure the resolutions are correct
|
||||
//
|
||||
|
||||
static const presetQVGA169 = VideoParameters(
|
||||
description: 'QVGA(320x180) 16:9',
|
||||
width: 320,
|
||||
height: 180,
|
||||
encoding: VideoEncoding(
|
||||
maxBitrate: 125000,
|
||||
maxFramerate: 15,
|
||||
),
|
||||
);
|
||||
|
||||
static const presetVGA169 = VideoParameters(
|
||||
description: 'VGA(640x360) 16:9',
|
||||
width: 640,
|
||||
height: 360,
|
||||
encoding: VideoEncoding(
|
||||
maxBitrate: 400000,
|
||||
maxFramerate: 30,
|
||||
),
|
||||
);
|
||||
|
||||
static const presetQHD169 = VideoParameters(
|
||||
description: 'QHD(960x540) 16:9',
|
||||
width: 960,
|
||||
height: 540,
|
||||
encoding: VideoEncoding(
|
||||
maxBitrate: 800000,
|
||||
maxFramerate: 30,
|
||||
),
|
||||
);
|
||||
|
||||
static const presetHD169 = VideoParameters(
|
||||
description: 'HD(1280x720) 16:9',
|
||||
width: 1280,
|
||||
height: 720,
|
||||
encoding: VideoEncoding(
|
||||
maxBitrate: 2500000,
|
||||
maxFramerate: 30,
|
||||
),
|
||||
);
|
||||
|
||||
static const presetFHD169 = VideoParameters(
|
||||
description: 'FHD(1920x1080) 16:9',
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
encoding: VideoEncoding(
|
||||
maxBitrate: 4000000,
|
||||
maxFramerate: 30,
|
||||
),
|
||||
);
|
||||
|
||||
static const presetQVGA43 = VideoParameters(
|
||||
description: 'QVGA(240x180) 4:3',
|
||||
width: 240,
|
||||
height: 180,
|
||||
encoding: VideoEncoding(
|
||||
maxBitrate: 100000,
|
||||
maxFramerate: 15,
|
||||
),
|
||||
);
|
||||
|
||||
static const presetVGA43 = VideoParameters(
|
||||
description: 'VGA(480x360) 4:3',
|
||||
width: 480,
|
||||
height: 360,
|
||||
encoding: VideoEncoding(
|
||||
maxBitrate: 320000,
|
||||
maxFramerate: 30,
|
||||
),
|
||||
);
|
||||
|
||||
static const presetQHD43 = VideoParameters(
|
||||
description: 'QHD(720x540) 4:3',
|
||||
width: 720,
|
||||
height: 540,
|
||||
encoding: VideoEncoding(
|
||||
maxBitrate: 640000,
|
||||
maxFramerate: 30,
|
||||
),
|
||||
);
|
||||
|
||||
static const presetHD43 = VideoParameters(
|
||||
description: 'HD(960x720) 4:3',
|
||||
width: 960,
|
||||
height: 720,
|
||||
encoding: VideoEncoding(
|
||||
maxBitrate: 2000000,
|
||||
maxFramerate: 30,
|
||||
),
|
||||
);
|
||||
|
||||
static const presetFHD43 = VideoParameters(
|
||||
description: 'FHD(1440x1080) 4:3',
|
||||
width: 1440,
|
||||
height: 1080,
|
||||
encoding: VideoEncoding(
|
||||
maxBitrate: 3200000,
|
||||
maxFramerate: 30,
|
||||
),
|
||||
);
|
||||
|
||||
static final List<VideoParameters> presets169 = [
|
||||
presetQVGA169,
|
||||
presetVGA169,
|
||||
presetQHD169,
|
||||
presetHD169,
|
||||
presetFHD169,
|
||||
];
|
||||
|
||||
static final List<VideoParameters> presets43 = [
|
||||
presetQVGA43,
|
||||
presetVGA43,
|
||||
presetQHD43,
|
||||
presetHD43,
|
||||
presetFHD43,
|
||||
];
|
||||
|
||||
//
|
||||
// TODO: Return constraints that will work for all platforms (Web & Mobile)
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
|
||||
//
|
||||
Map<String, dynamic> toMediaConstraintsMap() => <String, dynamic>{
|
||||
'maxWidth': width,
|
||||
'maxHeight': height,
|
||||
'maxFrameRate': encoding.maxFramerate,
|
||||
};
|
||||
}
|
||||
|
||||
/// Options when creating an LocalAudioTrack. Placeholder for now.
|
||||
class LocalAudioTrackOptions {}
|
||||
class LocalAudioTrackOptions {
|
||||
const LocalAudioTrackOptions();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import '../proto/livekit_models.pb.dart';
|
||||
import '../proto/livekit_rtc.pbserver.dart';
|
||||
import '../participant/remote_participant.dart';
|
||||
import '../proto/livekit_models.pb.dart' as lk_models;
|
||||
import '../proto/livekit_rtc.pb.dart' as lk_rtc;
|
||||
import 'track.dart';
|
||||
import 'track_publication.dart';
|
||||
|
||||
@@ -10,10 +10,11 @@ class RemoteTrackPublication extends TrackPublication {
|
||||
final RemoteParticipant _participant;
|
||||
bool _unsubscribed = false;
|
||||
bool _disabled = false;
|
||||
VideoQuality _videoQuality = VideoQuality.HIGH;
|
||||
lk_rtc.VideoQuality _videoQuality = lk_rtc.VideoQuality.HIGH;
|
||||
|
||||
VideoQuality get videoQuality => _videoQuality;
|
||||
set videoQuality(VideoQuality val) {
|
||||
lk_rtc.VideoQuality get videoQuality => _videoQuality;
|
||||
|
||||
set videoQuality(lk_rtc.VideoQuality val) {
|
||||
if (val == _videoQuality) return;
|
||||
_videoQuality = val;
|
||||
_sendUpdateTrackSettings();
|
||||
@@ -56,21 +57,25 @@ class RemoteTrackPublication extends TrackPublication {
|
||||
_participant.roomDelegate?.onTrackUnmuted(_participant, this);
|
||||
}
|
||||
if (subscribed) {
|
||||
track?.mediaTrack.enabled = !val;
|
||||
track?.mediaStreamTrack.enabled = !val;
|
||||
}
|
||||
_participant.muteChanged();
|
||||
}
|
||||
|
||||
RemoteTrackPublication(TrackInfo info, this._participant, [Track? track]) : super.fromInfo(info) {
|
||||
RemoteTrackPublication(
|
||||
lk_models.TrackInfo info,
|
||||
this._participant, [
|
||||
Track? track,
|
||||
]) : super.fromInfo(info) {
|
||||
this.track = track;
|
||||
}
|
||||
|
||||
void _sendUpdateTrackSettings() {
|
||||
final settings = UpdateTrackSettings(
|
||||
final settings = lk_rtc.UpdateTrackSettings(
|
||||
trackSids: [sid],
|
||||
disabled: _disabled,
|
||||
);
|
||||
if (kind == TrackType.VIDEO) {
|
||||
if (kind == lk_models.TrackType.VIDEO) {
|
||||
settings.quality = _videoQuality;
|
||||
}
|
||||
_participant.client.sendUpdateTrackSettings(settings);
|
||||
|
||||
+11
-10
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../proto/livekit_models.pb.dart';
|
||||
import '../proto/livekit_models.pb.dart' as lk_models;
|
||||
|
||||
class TrackDimension {
|
||||
int width;
|
||||
@@ -12,24 +12,25 @@ class TrackDimension {
|
||||
|
||||
/// Wrapper around a MediaStreamTrack with additional metadata.
|
||||
class Track {
|
||||
static const cameraName = 'camera';
|
||||
static const screenShareName = 'screen';
|
||||
|
||||
String name;
|
||||
TrackType kind;
|
||||
MediaStreamTrack mediaTrack;
|
||||
lk_models.TrackType kind;
|
||||
MediaStreamTrack mediaStreamTrack;
|
||||
String? sid;
|
||||
RTCRtpTransceiver? transceiver;
|
||||
String? _cid;
|
||||
|
||||
Track(this.kind, this.name, this.mediaTrack);
|
||||
Track(this.kind, this.name, this.mediaStreamTrack);
|
||||
|
||||
bool get muted => mediaTrack.muted == null ? false : mediaTrack.muted!;
|
||||
bool get muted => mediaStreamTrack.muted == null ? false : mediaStreamTrack.muted!;
|
||||
|
||||
RTCRtpMediaType get mediaType {
|
||||
switch (kind) {
|
||||
case TrackType.AUDIO:
|
||||
case lk_models.TrackType.AUDIO:
|
||||
return RTCRtpMediaType.RTCRtpMediaTypeAudio;
|
||||
case TrackType.VIDEO:
|
||||
case lk_models.TrackType.VIDEO:
|
||||
return RTCRtpMediaType.RTCRtpMediaTypeVideo;
|
||||
// this should never happen
|
||||
default:
|
||||
@@ -38,7 +39,7 @@ class Track {
|
||||
}
|
||||
|
||||
String getCid() {
|
||||
var cid = _cid ?? mediaTrack.id;
|
||||
var cid = _cid ?? mediaStreamTrack.id;
|
||||
|
||||
if (cid == null) {
|
||||
const uuid = Uuid();
|
||||
@@ -48,7 +49,7 @@ class Track {
|
||||
return cid;
|
||||
}
|
||||
|
||||
void stop() {
|
||||
mediaTrack.stop();
|
||||
Future<void> stop() async {
|
||||
await mediaStreamTrack.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import '../proto/livekit_models.pb.dart';
|
||||
import '../proto/livekit_models.pb.dart' as lk_models;
|
||||
import 'track.dart';
|
||||
|
||||
/// Represents a track that's published to the server. This class contains
|
||||
@@ -7,14 +7,14 @@ class TrackPublication {
|
||||
Track? track;
|
||||
String name;
|
||||
String sid;
|
||||
TrackType kind;
|
||||
lk_models.TrackType kind;
|
||||
bool muted = false;
|
||||
bool simulcasted = false;
|
||||
TrackDimension? dimension;
|
||||
|
||||
bool get subscribed => track != null;
|
||||
|
||||
TrackPublication.fromInfo(TrackInfo info)
|
||||
TrackPublication.fromInfo(lk_models.TrackInfo info)
|
||||
: sid = info.sid,
|
||||
name = info.name,
|
||||
kind = info.type {
|
||||
@@ -22,12 +22,12 @@ class TrackPublication {
|
||||
}
|
||||
|
||||
/// True when the track is published with name [Track.screenShareName].
|
||||
bool get isScreenShare => kind == TrackType.VIDEO && name == Track.screenShareName;
|
||||
bool get isScreenShare => kind == lk_models.TrackType.VIDEO && name == Track.screenShareName;
|
||||
|
||||
void updateFromInfo(TrackInfo info) {
|
||||
void updateFromInfo(lk_models.TrackInfo info) {
|
||||
muted = info.muted;
|
||||
simulcasted = info.simulcast;
|
||||
if (info.type == TrackType.VIDEO) {
|
||||
if (info.type == lk_models.TrackType.VIDEO) {
|
||||
dimension = TrackDimension(info.width, info.height);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,37 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
|
||||
import '../proto/livekit_models.pb.dart';
|
||||
import '../proto/livekit_models.pb.dart' as lk_models;
|
||||
import 'track.dart';
|
||||
|
||||
/// A video track will notify when its mediaTrack has changed.
|
||||
class VideoTrack extends Track with ChangeNotifier {
|
||||
MediaStream? _mediaStream;
|
||||
MediaStream _mediaStream;
|
||||
|
||||
VideoTrack(String name, MediaStreamTrack mediaTrack, this._mediaStream)
|
||||
: super(TrackType.VIDEO, name, mediaTrack);
|
||||
VideoTrack(
|
||||
String name,
|
||||
MediaStreamTrack mediaTrack,
|
||||
this._mediaStream,
|
||||
) : super(
|
||||
lk_models.TrackType.VIDEO,
|
||||
name,
|
||||
mediaTrack,
|
||||
);
|
||||
|
||||
MediaStream? get mediaStream => _mediaStream;
|
||||
MediaStream get mediaStream => _mediaStream;
|
||||
|
||||
/// internal use
|
||||
/// {@nodoc}
|
||||
set mediaStream(MediaStream? stream) {
|
||||
void setMediaStream(MediaStream stream) {
|
||||
_mediaStream = stream;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
stop() {
|
||||
super.stop();
|
||||
_mediaStream?.dispose();
|
||||
_mediaStream = null;
|
||||
Future<void> stop() async {
|
||||
await super.stop();
|
||||
await _mediaStream.dispose();
|
||||
// _mediaStream = null;
|
||||
}
|
||||
}
|
||||
|
||||
+41
-9
@@ -1,36 +1,68 @@
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
|
||||
import 'logger.dart';
|
||||
|
||||
/// a wrapper around PeerConnection
|
||||
class PCTransport {
|
||||
RTCPeerConnection pc;
|
||||
List<RTCIceCandidate> pendingCandidates = [];
|
||||
final RTCPeerConnection pc;
|
||||
final List<RTCIceCandidate> _pendingCandidates = [];
|
||||
bool restartingIce = false;
|
||||
|
||||
PCTransport(this.pc);
|
||||
|
||||
Future<void> dispose() async {
|
||||
// Ensure callbacks won't fire any more
|
||||
pc.onRenegotiationNeeded = null;
|
||||
pc.onIceCandidate = null;
|
||||
pc.onIceConnectionState = null;
|
||||
pc.onTrack = null;
|
||||
|
||||
List<RTCRtpSender> senders = [];
|
||||
try {
|
||||
senders = await pc.getSenders();
|
||||
} catch (_) {}
|
||||
|
||||
for (final e in senders) {
|
||||
try {
|
||||
await pc.removeTrack(e);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
await pc.close();
|
||||
await pc.dispose();
|
||||
}
|
||||
|
||||
Future<void> setRemoteDescription(RTCSessionDescription sd) async {
|
||||
await pc.setRemoteDescription(sd);
|
||||
|
||||
await Future.forEach<RTCIceCandidate>(pendingCandidates, (candidate) async {
|
||||
await Future.forEach<RTCIceCandidate>(_pendingCandidates, (candidate) async {
|
||||
await pc.addCandidate(candidate);
|
||||
});
|
||||
|
||||
pendingCandidates.clear();
|
||||
_pendingCandidates.clear();
|
||||
restartingIce = false;
|
||||
}
|
||||
|
||||
Future<void> addIceCandidate(RTCIceCandidate candidate) async {
|
||||
final desc = await getRemoteDescription();
|
||||
|
||||
if (desc != null && !restartingIce) {
|
||||
return pc.addCandidate(candidate);
|
||||
await pc.addCandidate(candidate);
|
||||
return;
|
||||
}
|
||||
pendingCandidates.add(candidate);
|
||||
|
||||
_pendingCandidates.add(candidate);
|
||||
}
|
||||
|
||||
Future<RTCSessionDescription?> getRemoteDescription() async {
|
||||
if (pc.iceConnectionState == null) {
|
||||
return null;
|
||||
// Checking agains null doesn't work as intended
|
||||
// if (pc.iceConnectionState == null) return null;
|
||||
try {
|
||||
final result = await pc.getRemoteDescription();
|
||||
logger.fine('pc.getRemoteDescription $result');
|
||||
return result;
|
||||
} catch (_) {
|
||||
logger.warning('pc.getRemoteDescription did throw: $_');
|
||||
}
|
||||
return pc.getRemoteDescription();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
//
|
||||
//
|
||||
//
|
||||
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
|
||||
import 'options.dart';
|
||||
import 'track/options.dart';
|
||||
|
||||
class Utils {
|
||||
static List<VideoParameters> _presetsForResolution(
|
||||
int width,
|
||||
int height,
|
||||
) {
|
||||
final double aspect = width / height;
|
||||
if ((aspect - 16.0 / 9.0).abs() < (aspect - 4.0 / 3.0).abs()) return VideoParameters.presets169;
|
||||
return VideoParameters.presets43;
|
||||
}
|
||||
|
||||
static VideoParameters _findPresetForResolution(
|
||||
int width,
|
||||
int height, {
|
||||
required List<VideoParameters> presets,
|
||||
}) {
|
||||
assert(presets.isNotEmpty, 'presets should not be empty');
|
||||
VideoParameters result = presets.first;
|
||||
for (final preset in presets) {
|
||||
if (width >= preset.width && height >= preset.height) result = preset;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static List<RTCRtpEncoding>? computeVideoEncodings({
|
||||
int? width,
|
||||
int? height,
|
||||
TrackPublishOptions? options,
|
||||
}) {
|
||||
options ??= const TrackPublishOptions();
|
||||
|
||||
VideoEncoding? videoEncoding = options.videoEncoding;
|
||||
|
||||
if ((videoEncoding == null && !options.simulcast) || width == null || height == null) {
|
||||
// don't set encoding when we are not simulcasting and user isn't restricting
|
||||
// encoding parameters
|
||||
return null;
|
||||
}
|
||||
|
||||
final presets = _presetsForResolution(width, height);
|
||||
|
||||
if (videoEncoding == null) {
|
||||
// find the right encoding based on width/height
|
||||
final preset = _findPresetForResolution(width, height, presets: presets);
|
||||
// print('Using preset: ${preset.id}');
|
||||
videoEncoding = preset.encoding;
|
||||
// log.debug('using video encoding', videoEncoding);
|
||||
}
|
||||
|
||||
// Not simulcast
|
||||
if (!options.simulcast) return [videoEncoding.toRTCRtpEncoding()];
|
||||
|
||||
// Compute for simulcast
|
||||
final midPreset = presets[1];
|
||||
final lowPreset = presets[0];
|
||||
return [
|
||||
videoEncoding.toRTCRtpEncoding(
|
||||
rid: 'f',
|
||||
),
|
||||
// if resolution is high enough, we would send both h and q res..
|
||||
// otherwise only send h
|
||||
if (height * 0.7 >= midPreset.height) ...[
|
||||
midPreset.encoding.toRTCRtpEncoding(
|
||||
rid: 'h',
|
||||
scaleResolutionDownBy: height / midPreset.height,
|
||||
),
|
||||
lowPreset.encoding.toRTCRtpEncoding(
|
||||
rid: 'q',
|
||||
scaleResolutionDownBy: height / lowPreset.height,
|
||||
),
|
||||
] else
|
||||
lowPreset.encoding.toRTCRtpEncoding(
|
||||
rid: 'h',
|
||||
scaleResolutionDownBy: height / lowPreset.height,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
const version = '0.4.0';
|
||||
const protocolVersion = 2;
|
||||
@@ -1,22 +1,24 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
|
||||
import '../track/video_track.dart';
|
||||
import '../track/local_video_track.dart';
|
||||
import '../track/video_track.dart';
|
||||
|
||||
/// Widget that renders a [VideoTrack].
|
||||
class VideoTrackRenderer extends StatefulWidget {
|
||||
final VideoTrack track;
|
||||
final RTCVideoRenderer renderer;
|
||||
final RTCVideoViewObjectFit fit;
|
||||
|
||||
VideoTrackRenderer(this.track)
|
||||
: renderer = RTCVideoRenderer(),
|
||||
VideoTrackRenderer(
|
||||
this.track, {
|
||||
this.fit = RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
|
||||
}) : renderer = RTCVideoRenderer(),
|
||||
super(key: ValueKey(track.sid));
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _VideoTrackRendererState();
|
||||
}
|
||||
State<StatefulWidget> createState() => _VideoTrackRendererState();
|
||||
}
|
||||
|
||||
class _VideoTrackRendererState extends State<VideoTrackRenderer> {
|
||||
@@ -63,6 +65,7 @@ class _VideoTrackRendererState extends State<VideoTrackRenderer> {
|
||||
_renderer,
|
||||
mirror: isLocal,
|
||||
filterQuality: FilterQuality.medium,
|
||||
objectFit: widget.fit,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import 'platform/io.dart' if (dart.library.html) 'platform/web.dart';
|
||||
|
||||
class LKWebSocketError implements Exception {
|
||||
final int code;
|
||||
const LKWebSocketError._(this.code);
|
||||
|
||||
static LKWebSocketError unknown() => const LKWebSocketError._(0);
|
||||
static LKWebSocketError connect() => const LKWebSocketError._(1);
|
||||
|
||||
@override
|
||||
String toString() => {
|
||||
LKWebSocketError.unknown(): 'Unknown error',
|
||||
LKWebSocketError.connect(): 'Failed to connect',
|
||||
}[this]!;
|
||||
}
|
||||
|
||||
typedef LKWebSocketOnData = Function(dynamic data);
|
||||
typedef LKWebSocketOnError = Function(dynamic error);
|
||||
typedef LKWebSocketOnDispose = Function();
|
||||
|
||||
class LKWebSocketOptions {
|
||||
final LKWebSocketOnData? onData;
|
||||
final LKWebSocketOnError? onError;
|
||||
final LKWebSocketOnDispose? onDispose;
|
||||
const LKWebSocketOptions({
|
||||
this.onData,
|
||||
this.onError,
|
||||
this.onDispose,
|
||||
});
|
||||
}
|
||||
|
||||
abstract class LKWebSocket {
|
||||
void send(List<int> data);
|
||||
void dispose();
|
||||
|
||||
static Future<LKWebSocket> connect(
|
||||
Uri uri, [
|
||||
LKWebSocketOptions? options,
|
||||
]) =>
|
||||
lkWebSocketConnect(uri, options);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io' as io;
|
||||
|
||||
import 'package:livekit_client/src/logger.dart';
|
||||
|
||||
import '../interface.dart';
|
||||
|
||||
Future<LKWebSocketIO> lkWebSocketConnect(
|
||||
Uri uri, [
|
||||
LKWebSocketOptions? options,
|
||||
]) =>
|
||||
LKWebSocketIO.connect(uri, options);
|
||||
|
||||
class LKWebSocketIO implements LKWebSocket {
|
||||
final io.WebSocket _ws;
|
||||
final LKWebSocketOptions? options;
|
||||
late final StreamSubscription _subscription;
|
||||
|
||||
LKWebSocketIO._(
|
||||
this._ws, [
|
||||
this.options,
|
||||
]) {
|
||||
_subscription = _ws.listen(
|
||||
(dynamic data) => options?.onData?.call(data),
|
||||
onDone: () => dispose(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
options?.onDispose?.call();
|
||||
_subscription.cancel();
|
||||
_ws.close();
|
||||
}
|
||||
|
||||
@override
|
||||
void send(List<int> data) => _ws.add(data);
|
||||
|
||||
static Future<LKWebSocketIO> connect(
|
||||
Uri uri, [
|
||||
LKWebSocketOptions? options,
|
||||
]) async {
|
||||
logger.fine('LKWebSocketIO connect (uri: ${uri.toString()})');
|
||||
try {
|
||||
final ws = await io.WebSocket.connect(uri.toString());
|
||||
logger.fine('LKWebSocketIO connected');
|
||||
return LKWebSocketIO._(ws, options);
|
||||
} catch (_) {
|
||||
logger.severe('LKWebSocketIO error ${_}');
|
||||
throw LKWebSocketError.connect();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'dart:async';
|
||||
|
||||
// ignore: avoid_web_libraries_in_flutter
|
||||
import 'dart:html' as html;
|
||||
import 'dart:typed_data';
|
||||
|
||||
import '../interface.dart';
|
||||
|
||||
Future<LKWebSocketWeb> lkWebSocketConnect(
|
||||
Uri uri, [
|
||||
LKWebSocketOptions? options,
|
||||
]) =>
|
||||
LKWebSocketWeb.connect(uri, options);
|
||||
|
||||
class LKWebSocketWeb implements LKWebSocket {
|
||||
final html.WebSocket _ws;
|
||||
final LKWebSocketOptions? options;
|
||||
late final StreamSubscription _messageSubscription;
|
||||
late final StreamSubscription _closeSubscription;
|
||||
|
||||
LKWebSocketWeb._(
|
||||
this._ws, [
|
||||
this.options,
|
||||
]) {
|
||||
_ws.binaryType = 'arraybuffer';
|
||||
_messageSubscription = _ws.onMessage.listen((_) {
|
||||
dynamic _data = _.data is ByteBuffer ? _.data.asUint8List() : _.data;
|
||||
options?.onData?.call(_data);
|
||||
});
|
||||
_closeSubscription = _ws.onClose.listen((_) => dispose());
|
||||
}
|
||||
|
||||
@override
|
||||
void send(List<int> data) => _ws.send(data);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
options?.onDispose?.call();
|
||||
_messageSubscription.cancel();
|
||||
_closeSubscription.cancel();
|
||||
_ws.close();
|
||||
}
|
||||
|
||||
static Future<LKWebSocketWeb> connect(
|
||||
Uri uri, [
|
||||
LKWebSocketOptions? options,
|
||||
]) async {
|
||||
final completer = Completer<LKWebSocketWeb>();
|
||||
final ws = html.WebSocket(uri.toString());
|
||||
ws.onOpen.listen((_) => completer.complete(LKWebSocketWeb._(ws, options)));
|
||||
ws.onError.listen((_) => completer.completeError(LKWebSocketError.connect()));
|
||||
return completer.future;
|
||||
}
|
||||
}
|
||||
+14
-14
@@ -7,7 +7,7 @@ packages:
|
||||
name: async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.6.1"
|
||||
version: "2.8.1"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -28,7 +28,7 @@ packages:
|
||||
name: charcode
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.3.1"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -101,7 +101,7 @@ packages:
|
||||
name: flutter_webrtc
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.6"
|
||||
version: "0.6.7"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -143,7 +143,7 @@ packages:
|
||||
name: meta
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "1.7.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -157,7 +157,7 @@ packages:
|
||||
name: path_provider
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
version: "2.0.3"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -261,6 +261,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
synchronized:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: synchronized
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -274,7 +281,7 @@ packages:
|
||||
name: test_api
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.0"
|
||||
version: "0.4.2"
|
||||
tuple:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -303,20 +310,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
web_socket_channel:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: web_socket_channel
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.7"
|
||||
version: "2.2.9"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
+22
-4
@@ -8,17 +8,35 @@ environment:
|
||||
flutter: ">=1.17.0"
|
||||
|
||||
dependencies:
|
||||
collection: ^1.15.0
|
||||
fixnum: ^1.0.0
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_webrtc: ^0.6.6
|
||||
|
||||
collection: ^1.15.0
|
||||
fixnum: ^1.0.0
|
||||
|
||||
flutter_webrtc: ^0.6.7
|
||||
|
||||
http: ^0.13.3
|
||||
logging: ^1.0.1
|
||||
protobuf: ^2.0.0
|
||||
|
||||
tuple: ^2.0.0
|
||||
web_socket_channel: ^2.1.0
|
||||
|
||||
uuid: ^3.0.4
|
||||
synchronized: ^3.0.0
|
||||
|
||||
#
|
||||
# protobuf:
|
||||
# git:
|
||||
# url: https://github.com/google/protobuf.dart.git
|
||||
# ref: master
|
||||
# path: protobuf/
|
||||
|
||||
#
|
||||
# WebSocketChannel has design flaws
|
||||
# https://github.com/dart-lang/web_socket_channel/issues/25
|
||||
#
|
||||
# web_socket_channel: ^2.1.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user