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:
Hiroshi Horie
2021-09-11 03:14:14 +09:00
committed by GitHub
parent 8e66e324f6
commit 50f56c5e1e
69 changed files with 2656 additions and 2354 deletions
+1
View File
@@ -76,3 +76,4 @@ build/
# VS Code # VS Code
.vscode/ .vscode/
*.code-workspace
+27 -3
View File
@@ -6,7 +6,19 @@ This package is published to pub.dev as [livekit_client](https://pub.dev/package
## Docs ## 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 ## Installation
@@ -23,13 +35,25 @@ dependencies:
Camera and microphone usage need to be declared in your `Info.plist` file. Camera and microphone usage need to be declared in your `Info.plist` file.
```xml ```xml
...
<dict> <dict>
...
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) uses your camera</string> <string>$(PRODUCT_NAME) uses your camera</string>
<key>NSMicrophoneUsageDescription</key> <key>NSMicrophoneUsageDescription</key>
<string>$(PRODUCT_NAME) uses your microphone</string> <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 ### Android
+2
View File
@@ -37,3 +37,5 @@ linter:
# Additional recommended rules # Additional recommended rules
# #
prefer_single_quotes: true prefer_single_quotes: true
unnecessary_brace_in_string_interps: false
unawaited_futures: true
+2
View File
@@ -37,6 +37,8 @@ linter:
# Additional recommended rules # Additional recommended rules
# #
prefer_single_quotes: true prefer_single_quotes: true
unnecessary_brace_in_string_interps: false
unawaited_futures: true
# #
# Turn off avoid_print for example projects # Turn off avoid_print for example projects
+5 -5
View File
@@ -32,14 +32,14 @@ android {
main.java.srcDirs += 'src/main/kotlin' main.java.srcDirs += 'src/main/kotlin'
} }
compileOptions { // compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 // sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 // targetCompatibility JavaVersion.VERSION_1_8
} // }
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // 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 minSdkVersion 21
targetSdkVersion 30 targetSdkVersion 30
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
@@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <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 <!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc. to allow setting breakpoints, to provide hot reload, etc.
--> -->
@@ -1,51 +1,49 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <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" /> <uses-feature android:name="android.hardware.camera.autofocus" />
<uses-feature android:name="android.hardware.camera.autofocus" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.CHANGE_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.MODIFY_AUDIO_SETTINGS" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<application
<application
android:label="LiveKit Example" android:label="LiveKit Example"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as <!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. --> to determine the Window background behind the Flutter UI. -->
<meta-data <meta-data
android:name="io.flutter.embedding.android.NormalTheme" android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" android:resource="@style/NormalTheme"
/> />
<!-- Displays an Android View that continues showing the launch screen <!-- Displays an Android View that continues showing the launch screen
Drawable until Flutter paints its first frame, then this splash Drawable until Flutter paints its first frame, then this splash
screen fades out. A splash screen is useful to avoid any visual screen fades out. A splash screen is useful to avoid any visual
gap between the end of Android's launch screen and the painting of gap between the end of Android's launch screen and the painting of
Flutter's first frame. --> Flutter's first frame. -->
<meta-data <meta-data
android:name="io.flutter.embedding.android.SplashScreenDrawable" android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="@drawable/launch_background" android:resource="@drawable/launch_background"
/> />
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
</activity> </activity>
<!-- Don't delete the meta-data below.
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data <meta-data
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
</application> </application>
</manifest> </manifest>
@@ -1,4 +1,4 @@
package com.example.livekit_example package io.livekit.example
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
@@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <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 <!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc. to allow setting breakpoints, to provide hot reload, etc.
--> -->
+1 -1
View File
@@ -6,7 +6,7 @@ buildscript {
} }
dependencies { 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" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
} }
} }
+1 -1
View File
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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
View File
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project # Uncomment this line to define a global platform for your project
# platform :ios, '9.0' platform :ios, '12.1'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency. # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true' ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@@ -37,5 +37,14 @@ end
post_install do |installer| post_install do |installer|
installer.pods_project.targets.each do |target| installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(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
end end
+8 -2
View File
@@ -8,11 +8,14 @@ PODS:
- Libyuv (1703) - Libyuv (1703)
- path_provider (0.0.1): - path_provider (0.0.1):
- Flutter - Flutter
- shared_preferences (0.0.1):
- Flutter
DEPENDENCIES: DEPENDENCIES:
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`) - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
- path_provider (from `.symlinks/plugins/path_provider/ios`) - path_provider (from `.symlinks/plugins/path_provider/ios`)
- shared_preferences (from `.symlinks/plugins/shared_preferences/ios`)
SPEC REPOS: SPEC REPOS:
trunk: trunk:
@@ -26,6 +29,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_webrtc/ios" :path: ".symlinks/plugins/flutter_webrtc/ios"
path_provider: path_provider:
:path: ".symlinks/plugins/path_provider/ios" :path: ".symlinks/plugins/path_provider/ios"
shared_preferences:
:path: ".symlinks/plugins/shared_preferences/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c
@@ -33,7 +38,8 @@ SPEC CHECKSUMS:
GoogleWebRTC: b39a78c4f5cc6b0323415b9233db03a2faa7b0f0 GoogleWebRTC: b39a78c4f5cc6b0323415b9233db03a2faa7b0f0
Libyuv: 5f79ced0ee66e60a612ca97de1e6ccacd187a437 Libyuv: 5f79ced0ee66e60a612ca97de1e6ccacd187a437
path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c
shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d
PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c PODFILE CHECKSUM: 6055d9653e1011c0b3b671abb92cdea979357e63
COCOAPODS: 1.10.2 COCOAPODS: 1.10.1
+6 -6
View File
@@ -357,14 +357,14 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 9Z5V633C2T; DEVELOPMENT_TEAM = J48VV6BZV9;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.example.livekitExample; PRODUCT_BUNDLE_IDENTIFIER = example.livekit.io;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -489,14 +489,14 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 9Z5V633C2T; DEVELOPMENT_TEAM = J48VV6BZV9;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.example.livekitExample; PRODUCT_BUNDLE_IDENTIFIER = example.livekit.io;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -515,14 +515,14 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 9Z5V633C2T; DEVELOPMENT_TEAM = J48VV6BZV9;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.example.livekitExample; PRODUCT_BUNDLE_IDENTIFIER = example.livekit.io;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+8 -4
View File
@@ -22,6 +22,14 @@
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <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> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
@@ -41,9 +49,5 @@
</array> </array>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<false/> <false/>
<key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) will use camera</string>
<key>NSMicrophoneUsageDescription</key>
<string>$(PRODUCT_NAME) will use microphone</string>
</dict> </dict>
</plist> </plist>
+18
View File
@@ -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
View File
@@ -1,7 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:livekit_example/theme.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:livekit_client/livekit_client.dart';
import 'room.dart'; import 'pages/connect.dart';
void main() { void main() {
// configure logs for debugging // configure logs for debugging
@@ -10,113 +11,21 @@ void main() {
print('${record.level.name}: ${record.time}: ${record.message}'); 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, Key? key,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) => MaterialApp( Widget build(BuildContext context) => MaterialApp(
title: 'LiveKit Demo', title: 'LiveKit Flutter Example',
theme: ThemeData( theme: LiveKitTheme().buildThemeData(context),
primarySwatch: Colors.deepPurple, home: const ConnectPage(),
),
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'),
),
],
),
),
),
); );
} }
+170
View File
@@ -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'),
],
),
),
),
],
),
),
),
);
}
+152
View File
@@ -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),
),
],
),
),
);
}
-229
View File
@@ -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,
);
}
}
}
-178
View File
@@ -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,
);
}
}
+65
View File
@@ -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),
),
),
);
}
+166
View File
@@ -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),
)
],
);
}
}
+20
View File
@@ -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,
),
),
);
}
+91
View File
@@ -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,
),
),
],
),
);
}
+44
View File
@@ -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
View File
@@ -7,7 +7,7 @@ packages:
name: async name: async
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.6.1" version: "2.8.1"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@@ -28,7 +28,7 @@ packages:
name: charcode name: charcode
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0" version: "1.3.1"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@@ -50,13 +50,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.1" version: "3.0.1"
cupertino_icons: eva_icons_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
name: cupertino_icons name: eva_icons_flutter
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.3" version: "3.0.0"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@@ -102,13 +102,25 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
flutter_webrtc: flutter_webrtc:
dependency: transitive dependency: transitive
description: description:
name: flutter_webrtc name: flutter_webrtc
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted 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: http:
dependency: transitive dependency: transitive
description: description:
@@ -123,6 +135,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.0.0" version: "4.0.0"
js:
dependency: transitive
description:
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.3"
lints: lints:
dependency: transitive dependency: transitive
description: description:
@@ -157,7 +176,7 @@ packages:
name: meta name: meta
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.0" version: "1.7.0"
nested: nested:
dependency: transitive dependency: transitive
description: description:
@@ -178,7 +197,7 @@ packages:
name: path_provider name: path_provider
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.2" version: "2.0.3"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
@@ -256,6 +275,48 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.1" 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: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@@ -289,6 +350,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
synchronized:
dependency: transitive
description:
name: synchronized
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
@@ -302,7 +370,7 @@ packages:
name: test_api name: test_api
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.3.0" version: "0.4.2"
tuple: tuple:
dependency: transitive dependency: transitive
description: description:
@@ -331,20 +399,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0" 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: win32:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.7" version: "2.2.8"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:
+7 -3
View File
@@ -10,11 +10,15 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
cupertino_icons: ^1.0.2
livekit_client:
path: ../
provider: ^5.0.0 provider: ^5.0.0
logging: ^1.0.1 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: dev_dependencies:
flutter_test: flutter_test:
+1 -1
View File
@@ -13,7 +13,7 @@ import 'package:livekit_example/main.dart';
void main() { void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async { testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame. // Build our app and trigger a frame.
await tester.pumpWidget(const MyApp()); await tester.pumpWidget(const LiveKitExampleApp());
// Verify that our counter starts at 0. // Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget); expect(find.text('0'), findsOneWidget);
+11 -11
View File
@@ -1,22 +1,22 @@
/// Flutter Client SDK to LiveKit. /// Flutter Client SDK to LiveKit.
library livekit_client; library livekit_client;
export 'src/livekit.dart';
export 'src/errors.dart'; export 'src/errors.dart';
export 'src/room.dart'; export 'src/livekit.dart';
export 'src/options.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/participant.dart';
export 'src/participant/local_participant.dart';
export 'src/participant/remote_participant.dart'; export 'src/participant/remote_participant.dart';
export 'src/proto/livekit_models.pbenum.dart'; export 'src/proto/livekit_models.pb.dart' show TrackType;
export 'src/proto/livekit_rtc.pbenum.dart'; export 'src/proto/livekit_rtc.pb.dart' show VideoQuality;
export 'src/participant/local_participant.dart'; export 'src/room.dart';
export 'src/track/options.dart';
export 'src/track/track.dart';
export 'src/track/video_track.dart';
export 'src/track/local_audio_track.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_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/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'; export 'src/widget/video_track_renderer.dart';
-5
View File
@@ -1,5 +0,0 @@
import 'package:web_socket_channel/web_socket_channel.dart';
Future<WebSocketChannel> connectToWebSocket(Uri uri) {
throw UnsupportedError('no implementations found');
}
-19
View File
@@ -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;
}
-14
View File
@@ -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
View File
@@ -1,30 +1,32 @@
class LiveKitError extends Error { //
String message; // `Exception` implies runtime errors while, an `Error` object
// represents a program failure that the programmer
LiveKitError(this.message); // should have avoided.
//
class LiveKitException implements Exception {
final String message;
const LiveKitException._(this.message);
@override @override
String toString() { String toString() => 'LiveKitException $runtimeType $message';
return message;
}
} }
class ConnectError extends LiveKitError { class ConnectError extends LiveKitException {
ConnectError([String msg = 'Failed to connect to server']) : super(msg); ConnectError([String msg = 'Failed to connect to server']) : super._(msg);
} }
class UnexpectedConnectionState extends LiveKitError { class UnexpectedConnectionState extends LiveKitException {
UnexpectedConnectionState([String msg = 'Unexpected connection state']) : super(msg); UnexpectedConnectionState([String msg = 'Unexpected connection state']) : super._(msg);
} }
class TrackCreateError extends LiveKitError { class TrackCreateError extends LiveKitException {
TrackCreateError([String msg = 'Failed to create track']) : super(msg); TrackCreateError([String msg = 'Failed to create track']) : super._(msg);
} }
class TrackPublishError extends LiveKitError { class TrackPublishError extends LiveKitException {
TrackPublishError([String msg = 'Failed to publish track']) : super(msg); TrackPublishError([String msg = 'Failed to publish track']) : super._(msg);
} }
class DataPublishError extends LiveKitError { class DataPublishError extends LiveKitException {
DataPublishError([String msg = 'Failed to publish data']) : super(msg); DataPublishError([String msg = 'Failed to publish data']) : super._(msg);
} }
+39 -36
View File
@@ -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 { enum RTCIceTransportPolicy {
all, all,
relay, relay,
@@ -45,3 +9,42 @@ extension RTCIceTransportPolicyExt on RTCIceTransportPolicy {
RTCIceTransportPolicy.relay: 'relay', RTCIceTransportPolicy.relay: 'relay',
}[this]!; }[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
View File
@@ -1,12 +1,22 @@
import 'room.dart';
import 'options.dart'; import 'options.dart';
import 'room.dart';
/// Main entry point to connect to a room. /// Main entry point to connect to a room.
/// {@category Room} /// {@category Room}
class LiveKitClient { class LiveKitClient {
static const version = '0.4.0';
/// Connects to a LiveKit room /// 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(); final room = Room();
return room.connect(url, token, options); return room.connect(
url,
token,
options: options,
);
} }
} }
+26 -3
View File
@@ -1,7 +1,30 @@
import 'track/options.dart';
/// Options when joining a room. /// Options when joining a room.
/// {@category Room} /// {@category Room}
class JoinOptions { class ConnectOptions {
final bool? autoSubscribe; /// 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})';
} }
+108 -66
View File
@@ -1,23 +1,28 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart';
import '../errors.dart'; import '../errors.dart';
import '../proto/livekit_models.pb.dart'; import '../logger.dart';
import '../proto/livekit_rtc.pbserver.dart'; import '../options.dart';
import '../proto/livekit_models.pb.dart' as lk_models;
import '../rtc_engine.dart'; import '../rtc_engine.dart';
import '../track/local_audio_track.dart'; import '../track/local_audio_track.dart';
import '../track/local_track_publication.dart'; import '../track/local_track_publication.dart';
import '../track/local_video_track.dart'; import '../track/local_video_track.dart';
import '../track/track.dart'; import '../track/track.dart';
import '../track/track_publication.dart'; import '../track/track_publication.dart';
import '../utils.dart';
import 'participant.dart'; import 'participant.dart';
/// Represents the current participant in the room. /// Represents the current participant in the room.
class LocalParticipant extends Participant { class LocalParticipant extends Participant {
final RTCEngine _engine; final RTCEngine _engine;
final TrackPublishOptions? defaultPublishOptions;
LocalParticipant({ LocalParticipant({
required RTCEngine engine, required RTCEngine engine,
required ParticipantInfo info, required lk_models.ParticipantInfo info,
this.defaultPublishOptions,
}) : _engine = engine, }) : _engine = engine,
super(info.sid, info.identity) { super(info.sid, info.identity) {
updateFromInfo(info); updateFromInfo(info);
@@ -29,97 +34,134 @@ class LocalParticipant extends Participant {
/// publish an audio track to the room /// publish an audio track to the room
Future<TrackPublication> publishAudioTrack(LocalAudioTrack track) async { Future<TrackPublication> publishAudioTrack(LocalAudioTrack track) async {
if (audioTracks.values.any((element) => element.track?.mediaTrack.id == track.mediaTrack.id)) { if (audioTracks.any((e) => e.track?.mediaStreamTrack.id == track.mediaStreamTrack.id)) {
return Future.error(TrackPublishError('track already exists')); throw TrackPublishError('track already exists');
} }
try { // try {
final trackInfo = final trackInfo = await _engine.addTrack(
await _engine.addTrack(cid: track.getCid(), name: track.name, kind: track.kind); cid: track.getCid(),
final transceiverInit = RTCRtpTransceiverInit( name: track.name,
direction: TransceiverDirection.SendOnly, kind: track.kind,
); );
// 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,
init: transceiverInit,
);
final pub = LocalTrackPublication(trackInfo, track, this); final transceiverInit = RTCRtpTransceiverInit(
addTrackPublication(pub); direction: TransceiverDirection.SendOnly,
notifyListeners(); );
// addTransceiver cannot pass in a kind parameter due to a bug in flutter-webrtc (web)
track.transceiver = await _engine.publisher?.pc.addTransceiver(
track: track.mediaStreamTrack,
init: transceiverInit,
);
return pub; final pub = LocalTrackPublication(trackInfo, track, this);
} catch (e) { addTrackPublication(pub);
return Future.error(e); notifyListeners();
}
return pub;
} }
/// Publish a video track to the room /// Publish a video track to the room
Future<TrackPublication> publishVideoTrack(LocalVideoTrack track) async { Future<TrackPublication> publishVideoTrack(
if (videoTracks.values.any((element) => element.track?.mediaTrack.id == track.mediaTrack.id)) { LocalVideoTrack track, {
return Future.error(TrackPublishError('track already exists')); TrackPublishOptions? options,
}) async {
if (videoTracks.any((e) => e.track?.mediaStreamTrack.id == track.mediaStreamTrack.id)) {
throw TrackPublishError('track already exists');
} }
try { // Use default options from `ConnectOptions` if options is null
final trackInfo = options = options ?? defaultPublishOptions;
await _engine.addTrack(cid: track.getCid(), name: track.name, kind: track.kind);
final transceiverInit = RTCRtpTransceiverInit(
direction: TransceiverDirection.SendOnly,
);
// 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,
init: transceiverInit,
);
final pub = LocalTrackPublication(trackInfo, track, this); final trackInfo = await _engine.addTrack(
addTrackPublication(pub); cid: track.getCid(),
notifyListeners(); name: track.name,
kind: track.kind,
);
return pub; //
} catch (e) { // Video encodings and simulcasts
return Future.error(e); //
// 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 {
// 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],
);
//
// addTransceiver cannot pass in a kind parameter due to a bug in flutter-webrtc (web)
//
track.transceiver = await _engine.publisher?.pc.addTransceiver(
track: track.mediaStreamTrack,
init: transceiverInit,
);
final pub = LocalTrackPublication(trackInfo, track, this);
addTrackPublication(pub);
notifyListeners();
return pub;
} }
/// Unpublish a track that's already published /// 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); final existing = tracks.values.where((element) => element.track == track);
if (existing.isEmpty) { if (existing.isEmpty) return;
return;
}
final pub = existing.first; final pub = existing.first;
track.stop(); await track.stop();
final sender = track.transceiver?.sender; final sender = track.transceiver?.sender;
if (sender != null) { if (sender != null) {
engine.publisher?.pc.removeTrack(sender); await engine.publisher?.pc.removeTrack(sender);
} }
tracks.remove(pub.sid); 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. /// Publish a new data payload to the room.
/// @param destinationSids When empty, data will be forwarded to each participant in 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; RTCDataChannel? channel;
switch (reliability) { switch (reliability) {
case DataPacket_Kind.RELIABLE: case lk_models.DataPacket_Kind.RELIABLE:
channel = engine.reliableDC; channel = engine.reliableDC;
break; break;
case DataPacket_Kind.LOSSY: case lk_models.DataPacket_Kind.LOSSY:
channel = engine.lossyDC; channel = engine.lossyDC;
break; break;
} }
@@ -127,9 +169,9 @@ class LocalParticipant extends Participant {
return; return;
} }
final packet = DataPacket( final packet = lk_models.DataPacket(
kind: reliability, kind: reliability,
user: UserPacket( user: lk_models.UserPacket(
payload: data, payload: data,
participantSid: sid, participantSid: sid,
destinationSids: destinationSids, destinationSids: destinationSids,
@@ -143,7 +185,7 @@ class LocalParticipant extends Participant {
/// for internal use /// for internal use
/// {@nodoc} /// {@nodoc}
@override @override
void updateFromInfo(ParticipantInfo info) { void updateFromInfo(lk_models.ParticipantInfo info) {
super.updateFromInfo(info); super.updateFromInfo(info);
} }
} }
+16 -30
View File
@@ -1,10 +1,10 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'remote_participant.dart'; import '../proto/livekit_models.pb.dart' as lk_models;
import '../proto/livekit_models.pb.dart';
import '../track/remote_track_publication.dart'; import '../track/remote_track_publication.dart';
import '../track/track.dart'; import '../track/track.dart';
import '../track/track_publication.dart'; import '../track/track_publication.dart';
import 'remote_participant.dart';
/// Callbacks for participant changes /// Callbacks for participant changes
mixin ParticipantDelegate { mixin ParticipantDelegate {
@@ -51,9 +51,6 @@ mixin ParticipantDelegate {
/// - added/removed subscribed tracks /// - added/removed subscribed tracks
/// - metadata changed /// - metadata changed
class Participant extends ChangeNotifier { class Participant extends ChangeNotifier {
Map<String, TrackPublication> audioTracks = {};
Map<String, TrackPublication> videoTracks = {};
/// map of track sid => published track /// map of track sid => published track
Map<String, TrackPublication> tracks = {}; Map<String, TrackPublication> tracks = {};
@@ -77,7 +74,7 @@ class Participant extends ChangeNotifier {
/// delegate to receive participant callbacks /// delegate to receive participant callbacks
ParticipantDelegate? delegate; ParticipantDelegate? delegate;
ParticipantInfo? _participantInfo; lk_models.ParticipantInfo? _participantInfo;
bool _isSpeaking = false; bool _isSpeaking = false;
/// when the participant joined the room /// 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 /// true if participant is publishing an audio track and is muted
bool get isMuted { bool get isMuted {
if (audioTracks.values.isEmpty) { if (audioTracks.isEmpty) return false;
return false; return audioTracks.first.muted;
}
return audioTracks.values.first.muted;
} }
bool get hasAudio => audioTracks.isNotEmpty; bool get hasAudio => audioTracks.isNotEmpty;
@@ -105,15 +100,7 @@ class Participant extends ChangeNotifier {
bool get hasVideo => videoTracks.isNotEmpty; bool get hasVideo => videoTracks.isNotEmpty;
/// tracks that are subscribed to /// tracks that are subscribed to
List<TrackPublication> get subscribedTracks { List<TrackPublication> get subscribedTracks => tracks.values.where((e) => e.subscribed).toList();
List<TrackPublication> result = [];
for (final track in tracks.values) {
if (track.subscribed) {
result.add(track);
}
}
return result;
}
/// for internal use /// for internal use
/// {@nodoc} /// {@nodoc}
@@ -148,7 +135,7 @@ class Participant extends ChangeNotifier {
/// for internal use /// for internal use
/// {@nodoc} /// {@nodoc}
void updateFromInfo(ParticipantInfo info) { void updateFromInfo(lk_models.ParticipantInfo info) {
identity = info.identity; identity = info.identity;
sid = info.sid; sid = info.sid;
if (info.metadata.isNotEmpty) { if (info.metadata.isNotEmpty) {
@@ -168,15 +155,14 @@ class Participant extends ChangeNotifier {
void addTrackPublication(TrackPublication pub) { void addTrackPublication(TrackPublication pub) {
pub.track?.sid = pub.sid; pub.track?.sid = pub.sid;
tracks[pub.sid] = pub; 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();
}
+30 -26
View File
@@ -1,7 +1,9 @@
import 'package:flutter_webrtc/flutter_webrtc.dart'; 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 '../signal_client.dart';
import '../track/audio_track.dart';
import '../track/remote_track_publication.dart'; import '../track/remote_track_publication.dart';
import '../track/track.dart'; import '../track/track.dart';
import '../track/video_track.dart'; import '../track/video_track.dart';
@@ -13,17 +15,22 @@ class RemoteParticipant extends Participant {
SignalClient get client => _client; 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); updateFromInfo(info);
} }
RemoteTrackPublication? getTrackPublication(String sid) { RemoteTrackPublication? getTrackPublication(String sid) {
final pub = tracks[sid]; final pub = tracks[sid];
if (pub is RemoteTrackPublication) { if (pub is RemoteTrackPublication) return pub;
return pub;
}
} }
/// for internal use /// for internal use
@@ -49,11 +56,11 @@ class RemoteParticipant extends Participant {
} }
Track? track; Track? track;
if (pub.kind == TrackType.AUDIO) { if (pub.kind == lk_models.TrackType.AUDIO) {
final audioTrack = AudioTrack(pub.name, mediaTrack, stream); final audioTrack = AudioTrack(pub.name, mediaTrack, stream);
audioTrack.start(); audioTrack.start();
track = audioTrack; track = audioTrack;
} else if (pub.kind == TrackType.VIDEO) { } else if (pub.kind == lk_models.TrackType.VIDEO) {
track = VideoTrack(pub.name, mediaTrack, stream); track = VideoTrack(pub.name, mediaTrack, stream);
} else { } else {
final msg = 'unsupported track type ${pub.kind}'; final msg = 'unsupported track type ${pub.kind}';
@@ -73,7 +80,7 @@ class RemoteParticipant extends Participant {
/// for internal use /// for internal use
/// {@nodoc} /// {@nodoc}
@override @override
void updateFromInfo(ParticipantInfo info) { void updateFromInfo(lk_models.ParticipantInfo info) async {
final hadInfo = hasInfo; final hadInfo = hasInfo;
super.updateFromInfo(info); super.updateFromInfo(info);
@@ -105,30 +112,28 @@ class RemoteParticipant extends Participant {
} }
// remove tracks // remove tracks
for (final pub in tracks.values) { final removeTrackSids =
if (!validPubs.containsKey(pub.sid)) { tracks.values.where((e) => !validPubs.containsKey(e.sid)).map((e) => e.sid).toList();
unpublishTrack(sid, true);
} 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); final pub = tracks.remove(sid);
if (pub == null || pub is! RemoteTrackPublication) { if (pub == null || pub is! RemoteTrackPublication) return;
return;
}
audioTracks.remove(sid);
videoTracks.remove(sid);
final track = pub.track; final track = pub.track;
if (track != null) { if (track != null) {
track.stop(); await track.stop();
delegate?.onTrackUnsubscribed(this, track, pub); delegate?.onTrackUnsubscribed(this, track, pub);
roomDelegate?.onTrackUnsubscribed(this, track, pub); roomDelegate?.onTrackUnsubscribed(this, track, pub);
notifyListeners(); notifyListeners();
} }
if (sendUnpublish) {
if (notify) {
delegate?.onTrackUnpublished(this, pub); delegate?.onTrackUnpublished(this, pub);
roomDelegate?.onTrackUnpublished(this, pub); roomDelegate?.onTrackUnpublished(this, pub);
} }
@@ -141,9 +146,8 @@ class RemoteParticipant extends Participant {
await Future<RemoteTrackPublication?>.delayed(const Duration(milliseconds: 100), () { await Future<RemoteTrackPublication?>.delayed(const Duration(milliseconds: 100), () {
return getTrackPublication(sid); return getTrackPublication(sid);
}); });
if (pub != null) {
return pub; if (pub != null) return pub;
}
} }
} }
} }
+248 -554
View File
@@ -3,7 +3,7 @@
// source: livekit_models.proto // source: livekit_models.proto
// //
// @dart = 2.12 // @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; import 'dart:core' as $core;
@@ -572,659 +572,353 @@ class TrackInfo extends $pb.GeneratedMessage {
void clearSimulcast() => clearField(7); void clearSimulcast() => clearField(7);
} }
enum DataMessage_Value { text, binary, notSet } enum DataPacket_Value { user, speaker, notSet }
class DataMessage extends $pb.GeneratedMessage { class DataPacket extends $pb.GeneratedMessage {
static const $core.Map<$core.int, DataMessage_Value> _DataMessage_ValueByTag = { static const $core.Map<$core.int, DataPacket_Value> _DataPacket_ValueByTag = {
1: DataMessage_Value.text, 2: DataPacket_Value.user,
2: DataMessage_Value.binary, 3: DataPacket_Value.speaker,
0: DataMessage_Value.notSet 0: DataPacket_Value.notSet
}; };
static final $pb.BuilderInfo _i = $pb.BuilderInfo( 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( package: const $pb.PackageName(
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'livekit'), const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'livekit'),
createEmptyInstance: create) createEmptyInstance: create)
..oo(0, [1, 2]) ..oo(0, [2, 3])
..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'text') ..e<DataPacket_Kind>(
..a<$core.List<$core.int>>( 1,
2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'kind',
const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'binary', $pb.PbFieldType.OE,
$pb.PbFieldType.OY) 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; ..hasRequiredFields = false;
DataMessage._() : super(); DataPacket._() : super();
factory DataMessage({ factory DataPacket({
$core.String? text, DataPacket_Kind? kind,
$core.List<$core.int>? binary, UserPacket? user,
ActiveSpeakerUpdate? speaker,
}) { }) {
final _result = create(); final _result = create();
if (text != null) { if (kind != null) {
_result.text = text; _result.kind = kind;
} }
if (binary != null) { if (user != null) {
_result.binary = binary; _result.user = user;
}
if (speaker != null) {
_result.speaker = speaker;
} }
return _result; return _result;
} }
factory DataMessage.fromBuffer($core.List<$core.int> i, factory DataPacket.fromBuffer($core.List<$core.int> i,
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(i, r); create()..mergeFromBuffer(i, r);
factory DataMessage.fromJson($core.String i, factory DataPacket.fromJson($core.String i,
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(i, r); create()..mergeFromJson(i, r);
@$core.Deprecated('Using this can add significant overhead to your binary. ' @$core.Deprecated('Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version') '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. ' @$core.Deprecated('Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' 'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version') 'Will be removed in next major version')
DataMessage copyWith(void Function(DataMessage) updates) => DataPacket copyWith(void Function(DataPacket) updates) =>
super.copyWith((message) => updates(message as DataMessage)) super.copyWith((message) => updates(message as DataPacket))
as DataMessage; // ignore: deprecated_member_use as DataPacket; // ignore: deprecated_member_use
$pb.BuilderInfo get info_ => _i; $pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline') @$core.pragma('dart2js:noInline')
static DataMessage create() => DataMessage._(); static DataPacket create() => DataPacket._();
DataMessage createEmptyInstance() => create(); DataPacket createEmptyInstance() => create();
static $pb.PbList<DataMessage> createRepeated() => $pb.PbList<DataMessage>(); static $pb.PbList<DataPacket> createRepeated() => $pb.PbList<DataPacket>();
@$core.pragma('dart2js:noInline') @$core.pragma('dart2js:noInline')
static DataMessage getDefault() => static DataPacket getDefault() =>
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<DataMessage>(create); _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<DataPacket>(create);
static DataMessage? _defaultInstance; static DataPacket? _defaultInstance;
DataMessage_Value whichValue() => _DataMessage_ValueByTag[$_whichOneof(0)]!; DataPacket_Value whichValue() => _DataPacket_ValueByTag[$_whichOneof(0)]!;
void clearValue() => clearField($_whichOneof(0)); void clearValue() => clearField($_whichOneof(0));
@$pb.TagNumber(1) @$pb.TagNumber(1)
$core.String get text => $_getSZ(0); DataPacket_Kind get kind => $_getN(0);
@$pb.TagNumber(1) @$pb.TagNumber(1)
set text($core.String v) { set kind(DataPacket_Kind v) {
$_setString(0, v); setField(1, v);
} }
@$pb.TagNumber(1) @$pb.TagNumber(1)
$core.bool hasText() => $_has(0); $core.bool hasKind() => $_has(0);
@$pb.TagNumber(1) @$pb.TagNumber(1)
void clearText() => clearField(1); void clearKind() => clearField(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
$core.List<$core.int> get binary => $_getN(1); UserPacket get user => $_getN(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
set binary($core.List<$core.int> v) { set user(UserPacket 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) {
setField(2, v); setField(2, v);
} }
@$pb.TagNumber(2) @$pb.TagNumber(2)
$core.bool hasTemplate() => $_has(1); $core.bool hasUser() => $_has(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
void clearTemplate() => clearField(2); void clearUser() => clearField(2);
@$pb.TagNumber(2) @$pb.TagNumber(2)
RecordingTemplate ensureTemplate() => $_ensure(1); UserPacket ensureUser() => $_ensure(1);
@$pb.TagNumber(3) @$pb.TagNumber(3)
$core.int get width => $_getIZ(2); ActiveSpeakerUpdate get speaker => $_getN(2);
@$pb.TagNumber(3) @$pb.TagNumber(3)
set width($core.int v) { set speaker(ActiveSpeakerUpdate 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) {
setField(3, v); setField(3, v);
} }
@$pb.TagNumber(3) @$pb.TagNumber(3)
$core.bool hasS3() => $_has(2); $core.bool hasSpeaker() => $_has(2);
@$pb.TagNumber(3) @$pb.TagNumber(3)
void clearS3() => clearField(3); void clearSpeaker() => clearField(3);
@$pb.TagNumber(3) @$pb.TagNumber(3)
RecordingS3Output ensureS3() => $_ensure(2); ActiveSpeakerUpdate ensureSpeaker() => $_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);
} }
class RecordingS3Output extends $pb.GeneratedMessage { class ActiveSpeakerUpdate extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo( 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( package: const $pb.PackageName(
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'livekit'), const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'livekit'),
createEmptyInstance: create) createEmptyInstance: create)
..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'bucket') ..pc<SpeakerInfo>(
..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'key') 1,
..aOS(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'accessKey') const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'speakers',
..aOS(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'secret') $pb.PbFieldType.PM,
subBuilder: SpeakerInfo.create)
..hasRequiredFields = false; ..hasRequiredFields = false;
RecordingS3Output._() : super(); ActiveSpeakerUpdate._() : super();
factory RecordingS3Output({ factory ActiveSpeakerUpdate({
$core.String? bucket, $core.Iterable<SpeakerInfo>? speakers,
$core.String? key,
$core.String? accessKey,
$core.String? secret,
}) { }) {
final _result = create(); final _result = create();
if (bucket != null) { if (speakers != null) {
_result.bucket = bucket; _result.speakers.addAll(speakers);
}
if (key != null) {
_result.key = key;
}
if (accessKey != null) {
_result.accessKey = accessKey;
}
if (secret != null) {
_result.secret = secret;
} }
return _result; return _result;
} }
factory RecordingS3Output.fromBuffer($core.List<$core.int> i, factory ActiveSpeakerUpdate.fromBuffer($core.List<$core.int> i,
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(i, r); create()..mergeFromBuffer(i, r);
factory RecordingS3Output.fromJson($core.String i, factory ActiveSpeakerUpdate.fromJson($core.String i,
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(i, r); create()..mergeFromJson(i, r);
@$core.Deprecated('Using this can add significant overhead to your binary. ' @$core.Deprecated('Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version') '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. ' @$core.Deprecated('Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' 'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version') 'Will be removed in next major version')
RecordingS3Output copyWith(void Function(RecordingS3Output) updates) => ActiveSpeakerUpdate copyWith(void Function(ActiveSpeakerUpdate) updates) =>
super.copyWith((message) => updates(message as RecordingS3Output)) super.copyWith((message) => updates(message as ActiveSpeakerUpdate))
as RecordingS3Output; // ignore: deprecated_member_use as ActiveSpeakerUpdate; // ignore: deprecated_member_use
$pb.BuilderInfo get info_ => _i; $pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline') @$core.pragma('dart2js:noInline')
static RecordingS3Output create() => RecordingS3Output._(); static ActiveSpeakerUpdate create() => ActiveSpeakerUpdate._();
RecordingS3Output createEmptyInstance() => create(); ActiveSpeakerUpdate createEmptyInstance() => create();
static $pb.PbList<RecordingS3Output> createRepeated() => $pb.PbList<RecordingS3Output>(); static $pb.PbList<ActiveSpeakerUpdate> createRepeated() => $pb.PbList<ActiveSpeakerUpdate>();
@$core.pragma('dart2js:noInline') @$core.pragma('dart2js:noInline')
static RecordingS3Output getDefault() => static ActiveSpeakerUpdate getDefault() =>
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<RecordingS3Output>(create); _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<ActiveSpeakerUpdate>(create);
static RecordingS3Output? _defaultInstance; static ActiveSpeakerUpdate? _defaultInstance;
@$pb.TagNumber(1) @$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) @$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); $_setString(0, v);
} }
@$pb.TagNumber(1) @$pb.TagNumber(1)
$core.bool hasBucket() => $_has(0); $core.bool hasSid() => $_has(0);
@$pb.TagNumber(1) @$pb.TagNumber(1)
void clearBucket() => clearField(1); void clearSid() => clearField(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
$core.String get key => $_getSZ(1); $core.double get level => $_getN(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
set key($core.String v) { set level($core.double v) {
$_setString(1, v); $_setFloat(1, v);
} }
@$pb.TagNumber(2) @$pb.TagNumber(2)
$core.bool hasKey() => $_has(1); $core.bool hasLevel() => $_has(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
void clearKey() => clearField(2); void clearLevel() => clearField(2);
@$pb.TagNumber(3) @$pb.TagNumber(3)
$core.String get accessKey => $_getSZ(2); $core.bool get active => $_getBF(2);
@$pb.TagNumber(3) @$pb.TagNumber(3)
set accessKey($core.String v) { set active($core.bool v) {
$_setString(2, v); $_setBool(2, v);
} }
@$pb.TagNumber(3) @$pb.TagNumber(3)
$core.bool hasAccessKey() => $_has(2); $core.bool hasActive() => $_has(2);
@$pb.TagNumber(3) @$pb.TagNumber(3)
void clearAccessKey() => clearField(3); void clearActive() => clearField(3);
}
@$pb.TagNumber(4)
$core.String get secret => $_getSZ(3); class UserPacket extends $pb.GeneratedMessage {
@$pb.TagNumber(4) static final $pb.BuilderInfo _i = $pb.BuilderInfo(
set secret($core.String v) { const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'UserPacket',
$_setString(3, v); package: const $pb.PackageName(
} const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'livekit'),
createEmptyInstance: create)
@$pb.TagNumber(4) ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'participantSid')
$core.bool hasSecret() => $_has(3); ..a<$core.List<$core.int>>(
@$pb.TagNumber(4) 2,
void clearSecret() => clearField(4); 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);
} }
+19 -1
View File
@@ -3,7 +3,7 @@
// source: livekit_models.proto // source: livekit_models.proto
// //
// @dart = 2.12 // @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 // ignore_for_file: UNDEFINED_SHOWN_NAME
import 'dart:core' as $core; 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); 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);
}
+49 -63
View File
@@ -3,7 +3,7 @@
// source: livekit_models.proto // source: livekit_models.proto
// //
// @dart = 2.12 // @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:core' as $core;
import 'dart:convert' as $convert; import 'dart:convert' as $convert;
@@ -111,88 +111,74 @@ const TrackInfo$json = const {
/// Descriptor for `TrackInfo`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `TrackInfo`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List trackInfoDescriptor = $convert.base64Decode( final $typed_data.Uint8List trackInfoDescriptor = $convert.base64Decode(
'CglUcmFja0luZm8SEAoDc2lkGAEgASgJUgNzaWQSJgoEdHlwZRgCIAEoDjISLmxpdmVraXQuVHJhY2tUeXBlUgR0eXBlEhIKBG5hbWUYAyABKAlSBG5hbWUSFAoFbXV0ZWQYBCABKAhSBW11dGVkEhQKBXdpZHRoGAUgASgNUgV3aWR0aBIWCgZoZWlnaHQYBiABKA1SBmhlaWdodBIcCglzaW11bGNhc3QYByABKAhSCXNpbXVsY2FzdA=='); 'CglUcmFja0luZm8SEAoDc2lkGAEgASgJUgNzaWQSJgoEdHlwZRgCIAEoDjISLmxpdmVraXQuVHJhY2tUeXBlUgR0eXBlEhIKBG5hbWUYAyABKAlSBG5hbWUSFAoFbXV0ZWQYBCABKAhSBW11dGVkEhQKBXdpZHRoGAUgASgNUgV3aWR0aBIWCgZoZWlnaHQYBiABKA1SBmhlaWdodBIcCglzaW11bGNhc3QYByABKAhSCXNpbXVsY2FzdA==');
@$core.Deprecated('Use dataMessageDescriptor instead') @$core.Deprecated('Use dataPacketDescriptor instead')
const DataMessage$json = const { const DataPacket$json = const {
'1': 'DataMessage', '1': 'DataPacket',
'2': const [ '2': const [
const {'1': 'text', '3': 1, '4': 1, '5': 9, '9': 0, '10': 'text'}, const {'1': 'kind', '3': 1, '4': 1, '5': 14, '6': '.livekit.DataPacket.Kind', '10': 'kind'},
const {'1': 'binary', '3': 2, '4': 1, '5': 12, '9': 0, '10': 'binary'}, 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 [ '8': const [
const {'1': 'value'}, const {'1': 'value'},
], ],
}; };
/// Descriptor for `DataMessage`. Decode as a `google.protobuf.DescriptorProto`. @$core.Deprecated('Use dataPacketDescriptor instead')
final $typed_data.Uint8List dataMessageDescriptor = $convert.base64Decode( const DataPacket_Kind$json = const {
'CgtEYXRhTWVzc2FnZRIUCgR0ZXh0GAEgASgJSABSBHRleHQSGAoGYmluYXJ5GAIgASgMSABSBmJpbmFyeUIHCgV2YWx1ZQ=='); '1': 'Kind',
@$core.Deprecated('Use recordingInputDescriptor instead')
const RecordingInput$json = const {
'1': 'RecordingInput',
'2': const [ '2': const [
const {'1': 'url', '3': 1, '4': 1, '5': 9, '10': 'url'}, const {'1': 'RELIABLE', '2': 0},
const { const {'1': 'LOSSY', '2': 1},
'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'},
], ],
}; };
/// Descriptor for `RecordingInput`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `DataPacket`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List recordingInputDescriptor = $convert.base64Decode( final $typed_data.Uint8List dataPacketDescriptor = $convert.base64Decode(
'Cg5SZWNvcmRpbmdJbnB1dBIQCgN1cmwYASABKAlSA3VybBI2Cgh0ZW1wbGF0ZRgCIAEoCzIaLmxpdmVraXQuUmVjb3JkaW5nVGVtcGxhdGVSCHRlbXBsYXRlEhQKBXdpZHRoGAMgASgFUgV3aWR0aBIWCgZoZWlnaHQYBCABKAVSBmhlaWdodBIUCgVkZXB0aBgFIAEoBVIFZGVwdGgSHAoJZnJhbWVyYXRlGAYgASgFUglmcmFtZXJhdGU='); 'CgpEYXRhUGFja2V0EiwKBGtpbmQYASABKA4yGC5saXZla2l0LkRhdGFQYWNrZXQuS2luZFIEa2luZBIpCgR1c2VyGAIgASgLMhMubGl2ZWtpdC5Vc2VyUGFja2V0SABSBHVzZXISOAoHc3BlYWtlchgDIAEoCzIcLmxpdmVraXQuQWN0aXZlU3BlYWtlclVwZGF0ZUgAUgdzcGVha2VyIh8KBEtpbmQSDAoIUkVMSUFCTEUQABIJCgVMT1NTWRABQgcKBXZhbHVl');
@$core.Deprecated('Use recordingTemplateDescriptor instead') @$core.Deprecated('Use activeSpeakerUpdateDescriptor instead')
const RecordingTemplate$json = const { const ActiveSpeakerUpdate$json = const {
'1': 'RecordingTemplate', '1': 'ActiveSpeakerUpdate',
'2': const [ '2': const [
const {'1': 'type', '3': 1, '4': 1, '5': 9, '10': 'type'}, const {'1': 'speakers', '3': 1, '4': 3, '5': 11, '6': '.livekit.SpeakerInfo', '10': 'speakers'},
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'},
], ],
}; };
/// Descriptor for `RecordingTemplate`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `ActiveSpeakerUpdate`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List recordingTemplateDescriptor = $convert.base64Decode( final $typed_data.Uint8List activeSpeakerUpdateDescriptor = $convert.base64Decode(
'ChFSZWNvcmRpbmdUZW1wbGF0ZRISCgR0eXBlGAEgASgJUgR0eXBlEhUKBndzX3VybBgCIAEoCVIFd3NVcmwSFAoFdG9rZW4YAyABKAlSBXRva2VuEhsKCXJvb21fbmFtZRgEIAEoCVIIcm9vbU5hbWU='); 'ChNBY3RpdmVTcGVha2VyVXBkYXRlEjAKCHNwZWFrZXJzGAEgAygLMhQubGl2ZWtpdC5TcGVha2VySW5mb1IIc3BlYWtlcnM=');
@$core.Deprecated('Use recordingOutputDescriptor instead') @$core.Deprecated('Use speakerInfoDescriptor instead')
const RecordingOutput$json = const { const SpeakerInfo$json = const {
'1': 'RecordingOutput', '1': 'SpeakerInfo',
'2': const [ '2': const [
const {'1': 'file', '3': 1, '4': 1, '5': 9, '10': 'file'}, const {'1': 'sid', '3': 1, '4': 1, '5': 9, '10': 'sid'},
const {'1': 'rtmp', '3': 2, '4': 1, '5': 9, '10': 'rtmp'}, const {'1': 'level', '3': 2, '4': 1, '5': 2, '10': 'level'},
const {'1': 's3', '3': 3, '4': 1, '5': 11, '6': '.livekit.RecordingS3Output', '10': 's3'}, const {'1': 'active', '3': 3, '4': 1, '5': 8, '10': 'active'},
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'},
], ],
}; };
/// Descriptor for `RecordingOutput`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `SpeakerInfo`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List recordingOutputDescriptor = $convert.base64Decode( final $typed_data.Uint8List speakerInfoDescriptor = $convert.base64Decode(
'Cg9SZWNvcmRpbmdPdXRwdXQSEgoEZmlsZRgBIAEoCVIEZmlsZRISCgRydG1wGAIgASgJUgRydG1wEioKAnMzGAMgASgLMhoubGl2ZWtpdC5SZWNvcmRpbmdTM091dHB1dFICczMSFAoFd2lkdGgYBCABKAVSBXdpZHRoEhYKBmhlaWdodBgFIAEoBVIGaGVpZ2h0EiMKDWF1ZGlvX2JpdHJhdGUYBiABKAlSDGF1ZGlvQml0cmF0ZRInCg9hdWRpb19mcmVxdWVuY3kYByABKAlSDmF1ZGlvRnJlcXVlbmN5EiMKDXZpZGVvX2JpdHJhdGUYCCABKAlSDHZpZGVvQml0cmF0ZRIhCgx2aWRlb19idWZmZXIYCSABKAlSC3ZpZGVvQnVmZmVy'); 'CgtTcGVha2VySW5mbxIQCgNzaWQYASABKAlSA3NpZBIUCgVsZXZlbBgCIAEoAlIFbGV2ZWwSFgoGYWN0aXZlGAMgASgIUgZhY3RpdmU=');
@$core.Deprecated('Use recordingS3OutputDescriptor instead') @$core.Deprecated('Use userPacketDescriptor instead')
const RecordingS3Output$json = const { const UserPacket$json = const {
'1': 'RecordingS3Output', '1': 'UserPacket',
'2': const [ '2': const [
const {'1': 'bucket', '3': 1, '4': 1, '5': 9, '10': 'bucket'}, const {'1': 'participant_sid', '3': 1, '4': 1, '5': 9, '10': 'participantSid'},
const {'1': 'key', '3': 2, '4': 1, '5': 9, '10': 'key'}, const {'1': 'payload', '3': 2, '4': 1, '5': 12, '10': 'payload'},
const {'1': 'access_key', '3': 3, '4': 1, '5': 9, '10': 'accessKey'}, const {'1': 'destination_sids', '3': 3, '4': 3, '5': 9, '10': 'destinationSids'},
const {'1': 'secret', '3': 4, '4': 1, '5': 9, '10': 'secret'},
], ],
}; };
/// Descriptor for `RecordingS3Output`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `UserPacket`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List recordingS3OutputDescriptor = $convert.base64Decode( final $typed_data.Uint8List userPacketDescriptor = $convert.base64Decode(
'ChFSZWNvcmRpbmdTM091dHB1dBIWCgZidWNrZXQYASABKAlSBmJ1Y2tldBIQCgNrZXkYAiABKAlSA2tleRIdCgphY2Nlc3Nfa2V5GAMgASgJUglhY2Nlc3NLZXkSFgoGc2VjcmV0GAQgASgJUgZzZWNyZXQ='); 'CgpVc2VyUGFja2V0EicKD3BhcnRpY2lwYW50X3NpZBgBIAEoCVIOcGFydGljaXBhbnRTaWQSGAoHcGF5bG9hZBgCIAEoDFIHcGF5bG9hZBIpChBkZXN0aW5hdGlvbl9zaWRzGAMgAygJUg9kZXN0aW5hdGlvblNpZHM=');
+1 -1
View File
@@ -3,6 +3,6 @@
// source: livekit_models.proto // source: livekit_models.proto
// //
// @dart = 2.12 // @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'; export 'livekit_models.pb.dart';
+82 -371
View File
@@ -3,7 +3,7 @@
// source: livekit_rtc.proto // source: livekit_rtc.proto
// //
// @dart = 2.12 // @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; import 'dart:core' as $core;
@@ -48,22 +48,33 @@ class SignalRequest extends $pb.GeneratedMessage {
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'livekit'), const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'livekit'),
createEmptyInstance: create) createEmptyInstance: create)
..oo(0, [1, 2, 3, 4, 5, 6, 7, 8, 9]) ..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) subBuilder: SessionDescription.create)
..aOM<SessionDescription>( ..aOM<SessionDescription>(
2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'answer', 2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'answer',
subBuilder: SessionDescription.create) 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) subBuilder: TrickleRequest.create)
..aOM<AddTrackRequest>( ..aOM<AddTrackRequest>(
4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'addTrack', 4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'addTrack',
subBuilder: AddTrackRequest.create) 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) subBuilder: MuteTrackRequest.create)
..aOM<UpdateSubscription>(6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'subscription', subBuilder: UpdateSubscription.create) ..aOM<UpdateSubscription>(
..aOM<UpdateTrackSettings>(7, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'trackSetting', subBuilder: UpdateTrackSettings.create) 6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'subscription',
..aOM<LeaveRequest>(8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'leave', subBuilder: LeaveRequest.create) subBuilder: UpdateSubscription.create)
..aOM<SetSimulcastLayers>(9, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'simulcast', subBuilder: SetSimulcastLayers.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; ..hasRequiredFields = false;
SignalRequest._() : super(); SignalRequest._() : super();
@@ -273,6 +284,7 @@ enum SignalResponse_Message {
trackPublished, trackPublished,
speaker, speaker,
leave, leave,
mute,
notSet notSet
} }
@@ -286,6 +298,7 @@ class SignalResponse extends $pb.GeneratedMessage {
6: SignalResponse_Message.trackPublished, 6: SignalResponse_Message.trackPublished,
7: SignalResponse_Message.speaker, 7: SignalResponse_Message.speaker,
8: SignalResponse_Message.leave, 8: SignalResponse_Message.leave,
9: SignalResponse_Message.mute,
0: SignalResponse_Message.notSet 0: SignalResponse_Message.notSet
}; };
static final $pb.BuilderInfo _i = $pb.BuilderInfo( static final $pb.BuilderInfo _i = $pb.BuilderInfo(
@@ -293,22 +306,34 @@ class SignalResponse extends $pb.GeneratedMessage {
package: const $pb.PackageName( package: const $pb.PackageName(
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'livekit'), const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'livekit'),
createEmptyInstance: create) createEmptyInstance: create)
..oo(0, [1, 2, 3, 4, 5, 6, 7, 8]) ..oo(0, [1, 2, 3, 4, 5, 6, 7, 8, 9])
..aOM<JoinResponse>(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'join', ..aOM<JoinResponse>(
1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'join',
subBuilder: JoinResponse.create) subBuilder: JoinResponse.create)
..aOM<SessionDescription>( ..aOM<SessionDescription>(
2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'answer', 2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'answer',
subBuilder: SessionDescription.create) 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) 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) subBuilder: TrickleRequest.create)
..aOM<ParticipantUpdate>( ..aOM<ParticipantUpdate>(
5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'update', 5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'update',
subBuilder: ParticipantUpdate.create) subBuilder: ParticipantUpdate.create)
..aOM<TrackPublishedResponse>(6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'trackPublished', subBuilder: TrackPublishedResponse.create) ..aOM<TrackPublishedResponse>(
..aOM<ActiveSpeakerUpdate>(7, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'speaker', subBuilder: ActiveSpeakerUpdate.create) 6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'trackPublished',
..aOM<LeaveRequest>(8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'leave', subBuilder: LeaveRequest.create) 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; ..hasRequiredFields = false;
SignalResponse._() : super(); SignalResponse._() : super();
@@ -319,8 +344,9 @@ class SignalResponse extends $pb.GeneratedMessage {
TrickleRequest? trickle, TrickleRequest? trickle,
ParticipantUpdate? update, ParticipantUpdate? update,
TrackPublishedResponse? trackPublished, TrackPublishedResponse? trackPublished,
ActiveSpeakerUpdate? speaker, $0.ActiveSpeakerUpdate? speaker,
LeaveRequest? leave, LeaveRequest? leave,
MuteTrackRequest? mute,
}) { }) {
final _result = create(); final _result = create();
if (join != null) { if (join != null) {
@@ -347,6 +373,9 @@ class SignalResponse extends $pb.GeneratedMessage {
if (leave != null) { if (leave != null) {
_result.leave = leave; _result.leave = leave;
} }
if (mute != null) {
_result.mute = mute;
}
return _result; return _result;
} }
factory SignalResponse.fromBuffer($core.List<$core.int> i, factory SignalResponse.fromBuffer($core.List<$core.int> i,
@@ -463,9 +492,9 @@ class SignalResponse extends $pb.GeneratedMessage {
TrackPublishedResponse ensureTrackPublished() => $_ensure(5); TrackPublishedResponse ensureTrackPublished() => $_ensure(5);
@$pb.TagNumber(7) @$pb.TagNumber(7)
ActiveSpeakerUpdate get speaker => $_getN(6); $0.ActiveSpeakerUpdate get speaker => $_getN(6);
@$pb.TagNumber(7) @$pb.TagNumber(7)
set speaker(ActiveSpeakerUpdate v) { set speaker($0.ActiveSpeakerUpdate v) {
setField(7, v); setField(7, v);
} }
@@ -474,7 +503,7 @@ class SignalResponse extends $pb.GeneratedMessage {
@$pb.TagNumber(7) @$pb.TagNumber(7)
void clearSpeaker() => clearField(7); void clearSpeaker() => clearField(7);
@$pb.TagNumber(7) @$pb.TagNumber(7)
ActiveSpeakerUpdate ensureSpeaker() => $_ensure(6); $0.ActiveSpeakerUpdate ensureSpeaker() => $_ensure(6);
@$pb.TagNumber(8) @$pb.TagNumber(8)
LeaveRequest get leave => $_getN(7); LeaveRequest get leave => $_getN(7);
@@ -489,6 +518,20 @@ class SignalResponse extends $pb.GeneratedMessage {
void clearLeave() => clearField(8); void clearLeave() => clearField(8);
@$pb.TagNumber(8) @$pb.TagNumber(8)
LeaveRequest ensureLeave() => $_ensure(7); 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 { class AddTrackRequest extends $pb.GeneratedMessage {
@@ -510,6 +553,7 @@ class AddTrackRequest extends $pb.GeneratedMessage {
$pb.PbFieldType.OU3) $pb.PbFieldType.OU3)
..a<$core.int>(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'height', ..a<$core.int>(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'height',
$pb.PbFieldType.OU3) $pb.PbFieldType.OU3)
..aOB(6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'muted')
..hasRequiredFields = false; ..hasRequiredFields = false;
AddTrackRequest._() : super(); AddTrackRequest._() : super();
@@ -519,6 +563,7 @@ class AddTrackRequest extends $pb.GeneratedMessage {
$0.TrackType? type, $0.TrackType? type,
$core.int? width, $core.int? width,
$core.int? height, $core.int? height,
$core.bool? muted,
}) { }) {
final _result = create(); final _result = create();
if (cid != null) { if (cid != null) {
@@ -536,6 +581,9 @@ class AddTrackRequest extends $pb.GeneratedMessage {
if (height != null) { if (height != null) {
_result.height = height; _result.height = height;
} }
if (muted != null) {
_result.muted = muted;
}
return _result; return _result;
} }
factory AddTrackRequest.fromBuffer($core.List<$core.int> i, factory AddTrackRequest.fromBuffer($core.List<$core.int> i,
@@ -623,6 +671,18 @@ class AddTrackRequest extends $pb.GeneratedMessage {
$core.bool hasHeight() => $_has(4); $core.bool hasHeight() => $_has(4);
@$pb.TagNumber(5) @$pb.TagNumber(5)
void clearHeight() => clearField(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 { class TrickleRequest extends $pb.GeneratedMessage {
@@ -871,7 +931,9 @@ class JoinResponse extends $pb.GeneratedMessage {
subBuilder: $0.ParticipantInfo.create) subBuilder: $0.ParticipantInfo.create)
..aOS(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'serverVersion') ..aOS(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'serverVersion')
..pc<ICEServer>( ..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) subBuilder: ICEServer.create)
..hasRequiredFields = false; ..hasRequiredFields = false;
@@ -1184,154 +1246,6 @@ class ParticipantUpdate extends $pb.GeneratedMessage {
$core.List<$0.ParticipantInfo> get participants => $_getList(0); $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 { class UpdateSubscription extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo( static final $pb.BuilderInfo _i = $pb.BuilderInfo(
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'UpdateSubscription', const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'UpdateSubscription',
@@ -1627,206 +1541,3 @@ class ICEServer extends $pb.GeneratedMessage {
@$pb.TagNumber(3) @$pb.TagNumber(3)
void clearCredential() => clearField(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);
}
+1 -19
View File
@@ -3,7 +3,7 @@
// source: livekit_rtc.proto // source: livekit_rtc.proto
// //
// @dart = 2.12 // @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 // ignore_for_file: UNDEFINED_SHOWN_NAME
import 'dart:core' as $core; 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); 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);
}
+13 -74
View File
@@ -3,7 +3,7 @@
// source: livekit_rtc.proto // source: livekit_rtc.proto
// //
// @dart = 2.12 // @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:core' as $core;
import 'dart:convert' as $convert; import 'dart:convert' as $convert;
@@ -204,6 +204,15 @@ const SignalResponse$json = const {
'9': 0, '9': 0,
'10': 'leave' '10': 'leave'
}, },
const {
'1': 'mute',
'3': 9,
'4': 1,
'5': 11,
'6': '.livekit.MuteTrackRequest',
'9': 0,
'10': 'mute'
},
], ],
'8': const [ '8': const [
const {'1': 'message'}, const {'1': 'message'},
@@ -212,7 +221,7 @@ const SignalResponse$json = const {
/// Descriptor for `SignalResponse`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `SignalResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List signalResponseDescriptor = $convert.base64Decode( final $typed_data.Uint8List signalResponseDescriptor = $convert.base64Decode(
'Cg5TaWduYWxSZXNwb25zZRIrCgRqb2luGAEgASgLMhUubGl2ZWtpdC5Kb2luUmVzcG9uc2VIAFIEam9pbhI1CgZhbnN3ZXIYAiABKAsyGy5saXZla2l0LlNlc3Npb25EZXNjcmlwdGlvbkgAUgZhbnN3ZXISMwoFb2ZmZXIYAyABKAsyGy5saXZla2l0LlNlc3Npb25EZXNjcmlwdGlvbkgAUgVvZmZlchIzCgd0cmlja2xlGAQgASgLMhcubGl2ZWtpdC5Ucmlja2xlUmVxdWVzdEgAUgd0cmlja2xlEjQKBnVwZGF0ZRgFIAEoCzIaLmxpdmVraXQuUGFydGljaXBhbnRVcGRhdGVIAFIGdXBkYXRlEkoKD3RyYWNrX3B1Ymxpc2hlZBgGIAEoCzIfLmxpdmVraXQuVHJhY2tQdWJsaXNoZWRSZXNwb25zZUgAUg50cmFja1B1Ymxpc2hlZBI4CgdzcGVha2VyGAcgASgLMhwubGl2ZWtpdC5BY3RpdmVTcGVha2VyVXBkYXRlSABSB3NwZWFrZXISLQoFbGVhdmUYCCABKAsyFS5saXZla2l0LkxlYXZlUmVxdWVzdEgAUgVsZWF2ZUIJCgdtZXNzYWdl'); 'Cg5TaWduYWxSZXNwb25zZRIrCgRqb2luGAEgASgLMhUubGl2ZWtpdC5Kb2luUmVzcG9uc2VIAFIEam9pbhI1CgZhbnN3ZXIYAiABKAsyGy5saXZla2l0LlNlc3Npb25EZXNjcmlwdGlvbkgAUgZhbnN3ZXISMwoFb2ZmZXIYAyABKAsyGy5saXZla2l0LlNlc3Npb25EZXNjcmlwdGlvbkgAUgVvZmZlchIzCgd0cmlja2xlGAQgASgLMhcubGl2ZWtpdC5Ucmlja2xlUmVxdWVzdEgAUgd0cmlja2xlEjQKBnVwZGF0ZRgFIAEoCzIaLmxpdmVraXQuUGFydGljaXBhbnRVcGRhdGVIAFIGdXBkYXRlEkoKD3RyYWNrX3B1Ymxpc2hlZBgGIAEoCzIfLmxpdmVraXQuVHJhY2tQdWJsaXNoZWRSZXNwb25zZUgAUg50cmFja1B1Ymxpc2hlZBI4CgdzcGVha2VyGAcgASgLMhwubGl2ZWtpdC5BY3RpdmVTcGVha2VyVXBkYXRlSABSB3NwZWFrZXISLQoFbGVhdmUYCCABKAsyFS5saXZla2l0LkxlYXZlUmVxdWVzdEgAUgVsZWF2ZRIvCgRtdXRlGAkgASgLMhkubGl2ZWtpdC5NdXRlVHJhY2tSZXF1ZXN0SABSBG11dGVCCQoHbWVzc2FnZQ==');
@$core.Deprecated('Use addTrackRequestDescriptor instead') @$core.Deprecated('Use addTrackRequestDescriptor instead')
const AddTrackRequest$json = const { const AddTrackRequest$json = const {
'1': 'AddTrackRequest', '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': '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': 'width', '3': 4, '4': 1, '5': 13, '10': 'width'},
const {'1': 'height', '3': 5, '4': 1, '5': 13, '10': 'height'}, 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`. /// Descriptor for `AddTrackRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List addTrackRequestDescriptor = $convert.base64Decode( final $typed_data.Uint8List addTrackRequestDescriptor = $convert.base64Decode(
'Cg9BZGRUcmFja1JlcXVlc3QSEAoDY2lkGAEgASgJUgNjaWQSEgoEbmFtZRgCIAEoCVIEbmFtZRImCgR0eXBlGAMgASgOMhIubGl2ZWtpdC5UcmFja1R5cGVSBHR5cGUSFAoFd2lkdGgYBCABKA1SBXdpZHRoEhYKBmhlaWdodBgFIAEoDVIGaGVpZ2h0'); 'Cg9BZGRUcmFja1JlcXVlc3QSEAoDY2lkGAEgASgJUgNjaWQSEgoEbmFtZRgCIAEoCVIEbmFtZRImCgR0eXBlGAMgASgOMhIubGl2ZWtpdC5UcmFja1R5cGVSBHR5cGUSFAoFd2lkdGgYBCABKA1SBXdpZHRoEhYKBmhlaWdodBgFIAEoDVIGaGVpZ2h0EhQKBW11dGVkGAYgASgIUgVtdXRlZA==');
@$core.Deprecated('Use trickleRequestDescriptor instead') @$core.Deprecated('Use trickleRequestDescriptor instead')
const TrickleRequest$json = const { const TrickleRequest$json = const {
'1': 'TrickleRequest', '1': 'TrickleRequest',
@@ -342,30 +352,6 @@ const ParticipantUpdate$json = const {
/// Descriptor for `ParticipantUpdate`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `ParticipantUpdate`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List participantUpdateDescriptor = $convert.base64Decode( final $typed_data.Uint8List participantUpdateDescriptor = $convert.base64Decode(
'ChFQYXJ0aWNpcGFudFVwZGF0ZRI8CgxwYXJ0aWNpcGFudHMYASADKAsyGC5saXZla2l0LlBhcnRpY2lwYW50SW5mb1IMcGFydGljaXBhbnRz'); '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') @$core.Deprecated('Use updateSubscriptionDescriptor instead')
const UpdateSubscription$json = const { const UpdateSubscription$json = const {
'1': 'UpdateSubscription', '1': 'UpdateSubscription',
@@ -415,50 +401,3 @@ const ICEServer$json = const {
/// Descriptor for `ICEServer`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `ICEServer`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List iCEServerDescriptor = $convert.base64Decode( final $typed_data.Uint8List iCEServerDescriptor = $convert.base64Decode(
'CglJQ0VTZXJ2ZXISEgoEdXJscxgBIAMoCVIEdXJscxIaCgh1c2VybmFtZRgCIAEoCVIIdXNlcm5hbWUSHgoKY3JlZGVudGlhbBgDIAEoCVIKY3JlZGVudGlhbA=='); '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=');
+1 -1
View File
@@ -3,6 +3,6 @@
// source: livekit_rtc.proto // source: livekit_rtc.proto
// //
// @dart = 2.12 // @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'; export 'livekit_rtc.pb.dart';
+39 -21
View File
@@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
@@ -12,8 +12,7 @@ import 'options.dart';
import 'participant/local_participant.dart'; import 'participant/local_participant.dart';
import 'participant/participant.dart'; import 'participant/participant.dart';
import 'participant/remote_participant.dart'; import 'participant/remote_participant.dart';
import 'proto/livekit_models.pb.dart'; import 'proto/livekit_models.pb.dart' as lk_models;
import 'proto/livekit_rtc.pb.dart';
import 'rtc_engine.dart'; import 'rtc_engine.dart';
import 'signal_client.dart'; import 'signal_client.dart';
import 'track/remote_track_publication.dart'; import 'track/remote_track_publication.dart';
@@ -140,9 +139,10 @@ class Room extends ChangeNotifier with ParticipantDelegate {
_engine.onTrack = _onTrackAdded; _engine.onTrack = _onTrackAdded;
_engine.onICEConnected = _handleICEConnected; _engine.onICEConnected = _handleICEConnected;
_engine.onDisconnected = _handleDisconnect; _engine.onDisconnected = _handleDisconnect;
_engine.onParticipantUpdateCallback = _handleParticipantUpdate; _engine.onParticipantUpdated = _handleParticipantUpdate;
_engine.onActiveSpeakerchangedCallback = _handleSpeakerUpdate; _engine.onActiveSpeakerUpdated = _handleSpeakerUpdate;
_engine.onDataMessageCallback = _handleDataPacket; _engine.onDataMessage = _handleDataPacket;
_engine.onRemoteMute = _onRemoteMuteChanged;
_engine.onReconnected = () { _engine.onReconnected = () {
_state = RoomState.connected; _state = RoomState.connected;
delegate?.onReconnected(); 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>(); final completer = Completer<Room>();
_connectCompleter = completer; _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}'); logger.fine('connected to LiveKit server, version: ${joinResponse.serverVersion}');
localParticipant = LocalParticipant( localParticipant = LocalParticipant(
engine: _engine, engine: _engine,
info: joinResponse.participant, info: joinResponse.participant,
defaultPublishOptions: options?.defaultPublishOptions,
); );
localParticipant.roomDelegate = this; localParticipant.roomDelegate = this;
@@ -191,12 +201,12 @@ class Room extends ChangeNotifier with ParticipantDelegate {
} }
/// Disconnects from the room, notifying server of disconnection. /// Disconnects from the room, notifying server of disconnection.
void disconnect() { Future<void> disconnect() async {
_engine.client.sendLeave(); _engine.client.sendLeave();
_handleDisconnect(); await _handleDisconnect();
} }
RemoteParticipant _getOrCreateRemoteParticipant(String sid, ParticipantInfo? info) { RemoteParticipant _getOrCreateRemoteParticipant(String sid, lk_models.ParticipantInfo? info) {
var participant = _participants[sid]; var participant = _participants[sid];
if (participant != null) { if (participant != null) {
return participant; return participant;
@@ -220,7 +230,7 @@ class Room extends ChangeNotifier with ParticipantDelegate {
notifyListeners(); notifyListeners();
} }
void _handleDisconnect() { Future<void> _handleDisconnect() async {
if (_state == RoomState.disconnected) { if (_state == RoomState.disconnected) {
return; return;
} }
@@ -228,14 +238,14 @@ class Room extends ChangeNotifier with ParticipantDelegate {
for (final p in _participants.values) { for (final p in _participants.values) {
final tracks = List<TrackPublication>.from(p.tracks.values); final tracks = List<TrackPublication>.from(p.tracks.values);
for (final pub in tracks) { for (final pub in tracks) {
p.unpublishTrack(pub.sid); await p.unpublishTrack(pub.sid);
} }
} }
for (final pub in localParticipant.tracks.values) { for (final pub in localParticipant.tracks.values) {
pub.track?.stop(); await pub.track?.stop();
} }
_engine.close(); await _engine.close();
_participants.clear(); _participants.clear();
_activeSpeakers.clear(); _activeSpeakers.clear();
_state = RoomState.disconnected; _state = RoomState.disconnected;
@@ -243,7 +253,7 @@ class Room extends ChangeNotifier with ParticipantDelegate {
delegate?.onDisconnected(); 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 // trigger change notifier only if list of participants membership is changed
var hasChanged = false; var hasChanged = false;
for (final info in updates) { for (final info in updates) {
@@ -252,7 +262,7 @@ class Room extends ChangeNotifier with ParticipantDelegate {
continue; continue;
} }
if (info.state == ParticipantInfo_State.DISCONNECTED) { if (info.state == lk_models.ParticipantInfo_State.DISCONNECTED) {
hasChanged = true; hasChanged = true;
_handleParticipantDisconnect(info.sid); _handleParticipantDisconnect(info.sid);
continue; 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>{}; final seenSids = <String>{};
List<Participant> newSpeakers = []; List<Participant> newSpeakers = [];
for (final info in speakers) { for (final info in speakers) {
@@ -312,7 +322,7 @@ class Room extends ChangeNotifier with ParticipantDelegate {
notifyListeners(); notifyListeners();
} }
void _handleDataPacket(UserPacket packet, DataPacket_Kind kind) { void _handleDataPacket(lk_models.UserPacket packet, lk_models.DataPacket_Kind kind) {
final participant = participants[packet.participantSid]; final participant = participants[packet.participantSid];
if (participant == null) { if (participant == null) {
return; return;
@@ -322,6 +332,14 @@ class Room extends ChangeNotifier with ParticipantDelegate {
delegate?.onDataReceived(participant, packet.payload); 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) { void _onTrackAdded(MediaStreamTrack track, MediaStream? stream, RTCRtpReceiver? receiver) {
if (stream == null) { if (stream == null) {
// we need the stream to get the track's id // we need the stream to get the track's id
@@ -329,8 +347,8 @@ class Room extends ChangeNotifier with ParticipantDelegate {
return; return;
} }
var parsed = _unpackStreamId(stream.id); final parsed = _unpackStreamId(stream.id);
var trackSid = parsed.item2 ?? track.id; final trackSid = parsed.item2 ?? track.id;
final participant = _getOrCreateRemoteParticipant(parsed.item1, null); final participant = _getOrCreateRemoteParticipant(parsed.item1, null);
participant.addSubscribedMediaTrack(track, stream, trackSid); participant.addSubscribedMediaTrack(track, stream, trackSid);
+91 -86
View File
@@ -1,12 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'errors.dart'; import 'errors.dart';
import 'extensions.dart'; import 'extensions.dart';
import 'logger.dart'; import 'logger.dart';
import 'options.dart'; import 'options.dart';
import 'proto/livekit_rtc.pb.dart'; import 'proto/livekit_models.pb.dart' as lk_models;
import 'proto/livekit_models.pb.dart'; import 'proto/livekit_rtc.pb.dart' as lk_rtc;
import 'signal_client.dart'; import 'signal_client.dart';
import 'track/track.dart'; import 'track/track.dart';
import 'transport.dart'; import 'transport.dart';
@@ -19,10 +20,15 @@ const iceRestartTimeout = Duration(seconds: 10);
typedef GenericCallback = void Function(); typedef GenericCallback = void Function();
typedef TrackCallback = void Function( typedef TrackCallback = void Function(
MediaStreamTrack track, MediaStream? stream, RTCRtpReceiver? receiver); MediaStreamTrack track,
typedef ParticipantUpdateCallback = void Function(List<ParticipantInfo> participants); MediaStream? stream,
typedef ActiveSpeakerChangedCallback = void Function(List<SpeakerInfo> speakers); RTCRtpReceiver? receiver,
typedef DataPacketCallback = void Function(UserPacket packet, DataPacket_Kind kind); );
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 { class RTCEngine with SignalClientDelegate {
PCTransport? publisher; PCTransport? publisher;
@@ -36,10 +42,10 @@ class RTCEngine with SignalClientDelegate {
bool iceConnected = false; bool iceConnected = false;
bool isReconnecting = false; bool isReconnecting = false;
bool isClosed = true; bool isClosed = true;
Map<String, Completer<TrackInfo>> pendingTrackResolvers = {}; Map<String, Completer<lk_models.TrackInfo>> pendingTrackResolvers = {};
int reconnectAttempts = 0; int reconnectAttempts = 0;
// to complete join request // to complete join request
Completer<JoinResponse>? joinCompleter; Completer<lk_rtc.JoinResponse>? joinCompleter;
// remember url and token for reconnect // remember url and token for reconnect
String? url; String? url;
String? token; String? token;
@@ -47,9 +53,10 @@ class RTCEngine with SignalClientDelegate {
// delegate methods // delegate methods
GenericCallback? onICEConnected; GenericCallback? onICEConnected;
TrackCallback? onTrack; TrackCallback? onTrack;
ParticipantUpdateCallback? onParticipantUpdateCallback; ParticipantUpdateCallback? onParticipantUpdated;
ActiveSpeakerChangedCallback? onActiveSpeakerchangedCallback; ActiveSpeakerChangedCallback? onActiveSpeakerUpdated;
DataPacketCallback? onDataMessageCallback; DataPacketCallback? onDataMessage;
RemoteMuteCallback? onRemoteMute;
GenericCallback? onReconnecting; GenericCallback? onReconnecting;
GenericCallback? onReconnected; GenericCallback? onReconnected;
GenericCallback? onDisconnected; GenericCallback? onDisconnected;
@@ -62,18 +69,18 @@ class RTCEngine with SignalClientDelegate {
client.delegate = this; 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.url = url;
this.token = token; this.token = token;
final completer = Completer<JoinResponse>(); final completer = Completer<lk_rtc.JoinResponse>();
joinCompleter = completer; joinCompleter = completer;
try { await client.join(url, token, options: options);
await client.join(url, token, opts);
} catch (e) {
return Future.error(e);
}
// if it's not complete after 5 seconds, fail // if it's not complete after 5 seconds, fail
Timer(connectionTimeout, () { Timer(connectionTimeout, () {
@@ -84,35 +91,30 @@ class RTCEngine with SignalClientDelegate {
return completer.future; return completer.future;
} }
void close() async { Future<void> close() async {
isClosed = true; isClosed = true;
if (publisher != null) { // PCTransport is responsible for disposing RTCPeerConnection
final senders = await publisher?.pc.getSenders(); await publisher?.dispose();
for (final element in (senders ?? <RTCRtpSender>[])) { publisher = null;
await publisher?.pc.removeTrack(element);
} await subscriber?.dispose();
subscriber = null;
publisher?.pc.close();
publisher = null;
}
if (subscriber != null) {
subscriber?.pc.close();
subscriber = null;
}
client.close(); client.close();
} }
Future<TrackInfo> addTrack( Future<lk_models.TrackInfo> addTrack({
{required String cid, required String cid,
required String name, required String name,
required TrackType kind, required lk_models.TrackType kind,
TrackDimension? dimension}) async { TrackDimension? dimension,
}) async {
if (pendingTrackResolvers[cid] != null) { if (pendingTrackResolvers[cid] != null) {
throw TrackPublishError('a track with the same CID has already been published'); 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; pendingTrackResolvers[cid] = completer;
client.sendAddTrack(cid: cid, name: name, type: kind, dimension: dimension); client.sendAddTrack(cid: cid, name: name, type: kind, dimension: dimension);
@@ -122,9 +124,7 @@ class RTCEngine with SignalClientDelegate {
Future<void> negotiate({bool? iceRestart}) async { Future<void> negotiate({bool? iceRestart}) async {
final pub = publisher; final pub = publisher;
if (pub == null) { if (pub == null) return;
return;
}
final remoteDesc = await pub.getRemoteDescription(); final remoteDesc = await pub.getRemoteDescription();
@@ -142,14 +142,15 @@ class RTCEngine with SignalClientDelegate {
}; };
} }
final offer = await pub.pc.createOffer(constraints); final offer = await pub.pc.createOffer(constraints);
logger.fine('Created offer');
logger.finer('sdp: ${offer.sdp}');
await pub.pc.setLocalDescription(offer); await pub.pc.setLocalDescription(offer);
client.sendOffer(offer); client.sendOffer(offer);
} }
Future<void> reconnect() async { Future<void> reconnect() async {
if (isClosed) { if (isClosed) return;
return;
}
final url = this.url; final url = this.url;
final token = this.token; final token = this.token;
if (url == null || token == null) { if (url == null || token == null) {
@@ -174,9 +175,9 @@ class RTCEngine with SignalClientDelegate {
sub.restartingIce = true; sub.restartingIce = true;
await negotiate(iceRestart: true); await negotiate(iceRestart: true);
} catch (e) { } catch (error) {
isReconnecting = false; isReconnecting = false;
return Future.error(e); return Future.error(error);
} }
// wait for connectivity to change // wait for connectivity to change
@@ -190,7 +191,7 @@ class RTCEngine with SignalClientDelegate {
} }
isReconnecting = false; isReconnecting = false;
return Future.error(ConnectError('could not reconnect ICE')); throw ConnectError('could not reconnect ICE');
} }
Future<void> _configurePeerConnections() async { Future<void> _configurePeerConnections() async {
@@ -204,18 +205,18 @@ class RTCEngine with SignalClientDelegate {
subscriber = PCTransport(subPC); subscriber = PCTransport(subPC);
pubPC.onIceCandidate = (RTCIceCandidate candidate) { pubPC.onIceCandidate = (RTCIceCandidate candidate) {
client.sendIceCandidate(candidate, SignalTarget.PUBLISHER); client.sendIceCandidate(candidate, lk_rtc.SignalTarget.PUBLISHER);
}; };
subPC.onIceCandidate = (RTCIceCandidate candidate) { 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 || if (pubPC.iceConnectionState == null ||
pubPC.iceConnectionState == RTCIceConnectionState.RTCIceConnectionStateNew) { pubPC.iceConnectionState == RTCIceConnectionState.RTCIceConnectionStateNew) {
return; return;
} }
negotiate(); await negotiate();
}; };
pubPC.onIceConnectionState = (RTCIceConnectionState state) { pubPC.onIceConnectionState = (RTCIceConnectionState state) {
@@ -272,27 +273,26 @@ class RTCEngine with SignalClientDelegate {
return; return;
} }
final dp = DataPacket.fromBuffer(message.binary); final dp = lk_models.DataPacket.fromBuffer(message.binary);
switch (dp.whichValue()) { switch (dp.whichValue()) {
case DataPacket_Value.speaker: case lk_models.DataPacket_Value.speaker:
onActiveSpeakerchangedCallback?.call(dp.speaker.speakers); onActiveSpeakerUpdated?.call(dp.speaker.speakers);
break; break;
case DataPacket_Value.user: case lk_models.DataPacket_Value.user:
onDataMessageCallback?.call(dp.user, dp.kind); onDataMessage?.call(dp.user, dp.kind);
break; break;
default: default:
// do nothing // do nothing
} }
} }
void _handleDisconnect(String reason) { Future<void> _handleDisconnect(String reason) async {
if (isClosed) { if (isClosed) return;
return;
}
logger.fine('disconnected $reason'); logger.fine('disconnected $reason');
if (reconnectAttempts >= maxReconnectAttempts) { if (reconnectAttempts >= maxReconnectAttempts) {
logger.info('could not connect after $reconnectAttempts, giving up'); logger.info('could not connect after $reconnectAttempts, giving up');
close(); await close();
onDisconnected?.call(); onDisconnected?.call();
return; return;
} }
@@ -310,7 +310,7 @@ class RTCEngine with SignalClientDelegate {
//------------------ SignalClient Delegate methods -------------------------// //------------------ SignalClient Delegate methods -------------------------//
@override @override
void onConnected(JoinResponse response) async { Future<void> onConnected(lk_rtc.JoinResponse response) async {
// create peer connections // create peer connections
isClosed = false; isClosed = false;
@@ -331,67 +331,72 @@ class RTCEngine with SignalClientDelegate {
await _configurePeerConnections(); await _configurePeerConnections();
negotiate(); await negotiate();
joinCompleter?.complete(Future.value(response)); joinCompleter?.complete(Future.value(response));
joinCompleter = null; joinCompleter = null;
} }
@override @override
void onClose([String? reason]) { Future<void> onClose([String? reason]) async {
_handleDisconnect('signal'); await _handleDisconnect('signal');
} }
@override @override
void onOffer(RTCSessionDescription sd) async { Future<void> onOffer(RTCSessionDescription sd) async {
final sub = subscriber; final sub = subscriber;
if (sub == null) { if (sub == null) return;
return;
}
await sub.setRemoteDescription(sd); await sub.setRemoteDescription(sd);
final answer = await sub.pc.createAnswer(); final answer = await sub.pc.createAnswer();
logger.fine('Created answer');
logger.finer('sdp: ${answer.sdp}');
await sub.pc.setLocalDescription(answer); await sub.pc.setLocalDescription(answer);
client.sendAnswer(answer); client.sendAnswer(answer);
} }
@override @override
void onAnswer(RTCSessionDescription sd) { Future<void> onAnswer(RTCSessionDescription sd) async {
if (publisher == null) { if (publisher == null) return;
return; logger.fine('Received answer');
} logger.finer('sdp: ${sd.sdp}');
await publisher!.setRemoteDescription(sd);
publisher?.setRemoteDescription(sd);
} }
@override @override
void onTrickle(RTCIceCandidate candidate, SignalTarget target) { Future<void> onTrickle(RTCIceCandidate candidate, lk_rtc.SignalTarget target) async {
if (target == SignalTarget.SUBSCRIBER) { if (target == lk_rtc.SignalTarget.SUBSCRIBER) {
subscriber?.addIceCandidate(candidate); await subscriber?.addIceCandidate(candidate);
} else if (target == SignalTarget.PUBLISHER) { } else if (target == lk_rtc.SignalTarget.PUBLISHER) {
publisher?.addIceCandidate(candidate); await publisher?.addIceCandidate(candidate);
} }
} }
@override @override
void onParticipantUpdate(List<ParticipantInfo> updates) { Future<void> onParticipantUpdate(List<lk_models.ParticipantInfo> updates) async {
onParticipantUpdateCallback?.call(updates); onParticipantUpdated?.call(updates);
} }
@override @override
void onLocalTrackPublished(TrackPublishedResponse response) { Future<void> onLocalTrackPublished(lk_rtc.TrackPublishedResponse response) async {
final completer = pendingTrackResolvers.remove(response.cid); final completer = pendingTrackResolvers.remove(response.cid);
completer?.complete(Future.value(response.track)); completer?.complete(Future.value(response.track));
} }
@override @override
void onActiveSpeakersChanged(List<SpeakerInfo> speakers) { Future<void> onActiveSpeakersChanged(List<lk_models.SpeakerInfo> speakers) async {
onActiveSpeakerchangedCallback?.call(speakers); onActiveSpeakerUpdated?.call(speakers);
} }
@override @override
void onLeave(LeaveRequest req) { Future<void> onLeave(lk_rtc.LeaveRequest req) async {
close(); await close();
onDisconnected?.call(); onDisconnected?.call();
} }
@override
Future<void> onMuteTrack(lk_rtc.MuteTrackRequest req) async {
onRemoteMute?.call(req.sid, req.muted);
}
} }
+210 -153
View File
@@ -3,134 +3,191 @@ import 'dart:convert';
import 'dart:developer'; import 'dart:developer';
import 'package:flutter_webrtc/flutter_webrtc.dart'; 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:http/http.dart' as http;
import 'package:livekit_client/src/ws/interface.dart';
import 'package:synchronized/synchronized.dart' as sync;
import 'errors.dart'; import 'errors.dart';
import 'logger.dart'; import 'logger.dart';
import 'options.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 '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 { mixin SignalClientDelegate {
// initial connection established // initial connection established
void onConnected(JoinResponse response); Future<void> onConnected(lk_rtc.JoinResponse response);
// websocket has closed // websocket has closed
void onClose([String? reason]); Future<void> onClose([String? reason]);
// when a server offer is received // when a server offer is received
void onOffer(RTCSessionDescription sd); Future<void> onOffer(RTCSessionDescription sd);
// when an answer from server is received // when an answer from server is received
void onAnswer(RTCSessionDescription sd); Future<void> onAnswer(RTCSessionDescription sd);
// when server has a new ICE candidate // 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 // participant has changed
void onParticipantUpdate(List<ParticipantInfo> updates); Future<void> onParticipantUpdate(List<lk_models.ParticipantInfo> updates);
// when a track has been added successfully // when a track has been added successfully
void onLocalTrackPublished(TrackPublishedResponse response); Future<void> onLocalTrackPublished(lk_rtc.TrackPublishedResponse response);
// active speaker has changed // 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 // 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 { class SignalClient {
SignalClientDelegate? delegate; static const protocolVersion = 2;
final _lock = sync.Lock();
SignalClientDelegate? delegate;
bool _connected = false; bool _connected = false;
WebSocketChannel? _ws; LKWebSocket? _ws;
SignalClient(); SignalClient();
bool get connected => _connected; bool get connected => _connected;
Future<void> join(String url, String token, JoinOptions? options) async { Uri _buildUri(
final rtcUrl = '$url/rtc'; String uriOrString, {
var params = _joinParams(token); required String token,
if (options != null && options.autoSubscribe != null) { ConnectOptions? options,
params += '&auto_subscribe=${options.autoSubscribe! ? '1' : '0'}'; 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 { try {
final ws = await platform.connectToWebSocket(Uri.parse(rtcUrl + params)); _ws = await LKWebSocket.connect(
ws.stream.listen(_handleMessage, onError: _handleError, onDone: _handleDone); rtcUri,
_ws = ws; LKWebSocketOptions(
} catch (e) { onData: _onSocketData,
final completer = Completer<void>(); onDispose: _onSocketDone,
final validateUri = Uri.parse('http${rtcUrl.substring(2)}/validate$params'); onError: _handleError,
http.get(validateUri).then((response) { ),
if (response.statusCode != 200) { );
completer.completeError(ConnectError(response.body)); } catch (socketError) {
} else { // Re-build same uri for validate mode
completer.completeError(ConnectError()); final validateUri = _buildUri(
} uriString,
}).catchError((dynamic e) { token: token,
completer.completeError(ConnectError()); 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; _connected = false;
_ws?.sink.close(); _ws?.dispose();
_ws = null; _ws = null;
url += '/rtc'; final rtcUri = _buildUri(
var params = _joinParams(token); uriString,
params += '&reconnect=1'; token: token,
final uri = Uri.parse(url + params); reconnect: true,
);
_ws = await LKWebSocket.connect(
rtcUri,
LKWebSocketOptions(
onData: _onSocketData,
onDispose: _onSocketDone,
onError: _handleError,
),
);
final ws = await platform.connectToWebSocket(uri);
_ws = ws;
_connected = true; _connected = true;
} }
void close() { void close() {
_connected = false; _connected = false;
_ws?.sink.close(); _ws?.dispose();
} }
void sendOffer(RTCSessionDescription offer) { void sendOffer(RTCSessionDescription offer) => _sendRequest(lk_rtc.SignalRequest(
_sendRequest(SignalRequest( offer: fromRTCSessionDescription(offer),
offer: fromRTCSessionDescription(offer), ));
));
}
void sendAnswer(RTCSessionDescription answer) { void sendAnswer(RTCSessionDescription answer) => _sendRequest(lk_rtc.SignalRequest(
_sendRequest(SignalRequest( answer: fromRTCSessionDescription(answer),
answer: fromRTCSessionDescription(answer), ));
));
}
void sendIceCandidate(RTCIceCandidate candidate, SignalTarget target) { void sendIceCandidate(RTCIceCandidate candidate, lk_rtc.SignalTarget target) => _sendRequest(
_sendRequest(SignalRequest( lk_rtc.SignalRequest(
trickle: TrickleRequest( trickle: lk_rtc.TrickleRequest(
candidateInit: fromRTCIceCandidate(candidate), candidateInit: fromRTCIceCandidate(candidate),
target: target, target: target,
))); ),
} ),
);
void sendMuteTrack(String trackSid, bool muted) { void sendMuteTrack(String trackSid, bool muted) => _sendRequest(lk_rtc.SignalRequest(
_sendRequest(SignalRequest( mute: lk_rtc.MuteTrackRequest(
mute: MuteTrackRequest( sid: trackSid,
sid: trackSid, muted: muted,
muted: muted, ),
), ));
));
}
void sendAddTrack( void sendAddTrack({
{required String cid, required String cid,
required String name, required String name,
required TrackType type, required lk_models.TrackType type,
TrackDimension? dimension}) { TrackDimension? dimension,
final req = AddTrackRequest( }) {
final req = lk_rtc.AddTrackRequest(
cid: cid, cid: cid,
name: name, name: name,
type: type, type: type,
@@ -139,109 +196,109 @@ class SignalClient {
req.width = dimension.width; req.width = dimension.width;
req.height = dimension.height; req.height = dimension.height;
} }
_sendRequest(SignalRequest( _sendRequest(lk_rtc.SignalRequest(
addTrack: req, addTrack: req,
)); ));
} }
void sendUpdateTrackSettings(UpdateTrackSettings settings) { void sendUpdateTrackSettings(lk_rtc.UpdateTrackSettings settings) =>
_sendRequest(SignalRequest( _sendRequest(lk_rtc.SignalRequest(
trackSetting: settings, trackSetting: settings,
)); ));
}
void sendUpdateSubscription(UpdateSubscription subscription) { void sendUpdateSubscription(lk_rtc.UpdateSubscription subscription) =>
_sendRequest(SignalRequest( _sendRequest(lk_rtc.SignalRequest(
subscription: subscription, subscription: subscription,
)); ));
}
void sendSetSimulcastLayers(String trackSid, List<VideoQuality> layers) { void sendSetSimulcastLayers(String trackSid, List<lk_rtc.VideoQuality> layers) =>
_sendRequest(SignalRequest( _sendRequest(lk_rtc.SignalRequest(
simulcast: SetSimulcastLayers( simulcast: lk_rtc.SetSimulcastLayers(
trackSid: trackSid, trackSid: trackSid,
layers: layers, layers: layers,
))); ),
} ));
void sendLeave() { void sendLeave() => _sendRequest(lk_rtc.SignalRequest(
_sendRequest(SignalRequest( leave: lk_rtc.LeaveRequest(),
leave: LeaveRequest(), ));
));
}
void _sendRequest(SignalRequest req) { void _sendRequest(lk_rtc.SignalRequest req) {
if (_ws == null) { if (_ws == null) {
log('could not send message, not connected'); log('could not send message, not connected');
return; return;
} }
final buf = req.writeToBuffer(); final buf = req.writeToBuffer();
_ws?.sink.add(buf); _ws?.send(buf);
} }
void _handleMessage(dynamic message) { Future<void> _onSocketData(dynamic message) async {
if (message is! List<int>) { if (message is! List<int>) return;
return; final msg = lk_rtc.SignalResponse.fromBuffer(message);
}
final msg = SignalResponse.fromBuffer(message); // Ensure previous delegate method's future is completed
switch (msg.whichMessage()) { // before calling another method
case SignalResponse_Message.join: await _lock.synchronized(() async {
if (!_connected) { //
_connected = true; switch (msg.whichMessage()) {
delegate?.onConnected(msg.join); case lk_rtc.SignalResponse_Message.join:
} if (!_connected) {
break; _connected = true;
case SignalResponse_Message.answer: await delegate?.onConnected(msg.join);
delegate?.onAnswer(toRTCSessionDescription(msg.answer)); }
break; break;
case SignalResponse_Message.offer: case lk_rtc.SignalResponse_Message.answer:
delegate?.onOffer(toRTCSessionDescription(msg.offer)); await delegate?.onAnswer(toRTCSessionDescription(msg.answer));
break; break;
case SignalResponse_Message.trickle: case lk_rtc.SignalResponse_Message.offer:
delegate?.onTrickle(toRTCIceCandidate(msg.trickle.candidateInit), msg.trickle.target); await delegate?.onOffer(toRTCSessionDescription(msg.offer));
break; break;
case SignalResponse_Message.update: case lk_rtc.SignalResponse_Message.trickle:
delegate?.onParticipantUpdate(msg.update.participants); await delegate?.onTrickle(
break; toRTCIceCandidate(msg.trickle.candidateInit),
case SignalResponse_Message.trackPublished: msg.trickle.target,
delegate?.onLocalTrackPublished(msg.trackPublished); );
break; break;
case SignalResponse_Message.speaker: case lk_rtc.SignalResponse_Message.update:
delegate?.onActiveSpeakersChanged(msg.speaker.speakers); await delegate?.onParticipantUpdate(msg.update.participants);
break; break;
case SignalResponse_Message.leave: case lk_rtc.SignalResponse_Message.trackPublished:
delegate?.onLeave(msg.leave); await delegate?.onLocalTrackPublished(msg.trackPublished);
break; break;
default: case lk_rtc.SignalResponse_Message.speaker:
log('unsupported message: ' + json.encode(msg)); await delegate?.onActiveSpeakersChanged(msg.speaker.speakers);
} break;
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'); logger.warning('received websocket error $error');
} }
void _handleDone() { void _onSocketDone() {
if (!_connected) { if (!_connected) return;
return;
}
_ws = null; _ws = null;
_connected = false; _connected = false;
delegate?.onClose(); delegate?.onClose();
} }
} }
String _joinParams(String token) { RTCSessionDescription toRTCSessionDescription(lk_rtc.SessionDescription sd) {
return '?access_token=$token&protocol=$protocolVersion';
}
RTCSessionDescription toRTCSessionDescription(SessionDescription sd) {
return RTCSessionDescription(sd.sdp, sd.type); return RTCSessionDescription(sd.sdp, sd.type);
} }
SessionDescription fromRTCSessionDescription(RTCSessionDescription rsd) { lk_rtc.SessionDescription fromRTCSessionDescription(RTCSessionDescription rsd) {
return SessionDescription(type: rsd.type, sdp: rsd.sdp); return lk_rtc.SessionDescription(type: rsd.type, sdp: rsd.sdp);
} }
RTCIceCandidate toRTCIceCandidate(String candidateInit) { RTCIceCandidate toRTCIceCandidate(String candidateInit) {
+7 -7
View File
@@ -1,29 +1,29 @@
import 'package:flutter_webrtc/flutter_webrtc.dart'; 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 'local_audio_track.dart';
import 'track.dart'; import 'track.dart';
import '_audio_api.dart' if (dart.library.html) '_audio_html.dart' as audio;
class AudioTrack extends Track { class AudioTrack extends Track {
MediaStream? mediaStream; MediaStream? mediaStream;
AudioTrack(String name, MediaStreamTrack track, this.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 playing audio track. On web platform, create an audio element and
/// start playback /// start playback
void start() { void start() {
if (this is! LocalAudioTrack) { if (this is! LocalAudioTrack) {
audio.startAudio(getCid(), mediaTrack); audio.startAudio(getCid(), mediaStreamTrack);
} }
} }
@override @override
void stop() { Future<void> stop() async {
mediaStream?.dispose(); await mediaStream?.dispose();
mediaStream = null; mediaStream = null;
audio.stopAudio(getCid()); audio.stopAudio(getCid());
super.stop(); await super.stop();
} }
} }
+16 -16
View File
@@ -1,28 +1,28 @@
import 'dart:async';
import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:livekit_client/src/track/audio_track.dart';
import '../errors.dart'; import '../errors.dart';
import 'audio_track.dart';
import 'options.dart'; import 'options.dart';
class LocalAudioTrack extends AudioTrack { class LocalAudioTrack extends AudioTrack {
LocalAudioTrack(String name, MediaStreamTrack track, MediaStream stream) LocalAudioTrack(
: super(name, track, stream); String name,
MediaStreamTrack track,
MediaStream stream,
) : super(name, track, stream);
/// Creates a new audio track from the default audio input device. /// Creates a new audio track from the default audio input device.
static Future<LocalAudioTrack> createTrack([LocalAudioTrackOptions? options]) async { static Future<LocalAudioTrack> create([LocalAudioTrackOptions? options]) async {
try { // try {
final stream = await navigator.mediaDevices.getUserMedia(<String, dynamic>{ final stream = await navigator.mediaDevices.getUserMedia(<String, dynamic>{
'audio': true, 'audio': true,
'video': false, 'video': false,
}); });
if (stream.getAudioTracks().isEmpty) { if (stream.getAudioTracks().isEmpty) throw TrackCreateError();
return Future.error(TrackCreateError());
}
return LocalAudioTrack('', stream.getAudioTracks().first, stream); return LocalAudioTrack('', stream.getAudioTracks().first, stream);
} catch (e) {
return Future.error(e);
}
} }
} }
+12 -6
View File
@@ -1,23 +1,29 @@
import 'package:livekit_client/src/logger.dart';
import '../participant/local_participant.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.dart';
import 'track_publication.dart'; import 'track_publication.dart';
class LocalTrackPublication extends TrackPublication { class LocalTrackPublication extends TrackPublication {
final LocalParticipant _participant; 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; this.track = track;
} }
/// Mute or unmute the current track. When muted, track will stop sending data /// Mute or unmute the current track. When muted, track will stop sending data
@override @override
set muted(bool val) { set muted(bool val) {
if (val == muted) { if (val == muted) return;
return; logger.finer('setMute: ${val}');
}
super.muted = val; super.muted = val;
track?.mediaTrack.enabled = !val; track?.mediaStreamTrack.enabled = !val;
_participant.engine.client.sendMuteTrack(sid, val); _participant.engine.client.sendMuteTrack(sid, val);
if (val) { if (val) {
+89 -42
View File
@@ -1,66 +1,113 @@
import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart';
import '../errors.dart'; import '../errors.dart';
import '../logger.dart';
import 'options.dart'; import 'options.dart';
import 'track.dart';
import 'video_track.dart'; import 'video_track.dart';
/// A video track from the local device. Use static methods in this class to create /// A video track from the local device. Use static methods in this class to create
/// video tracks. /// video tracks.
class LocalVideoTrack extends VideoTrack { 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; 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 /// Restarts the track with new options. This is useful when switching between
/// front and back cameras. /// front and back cameras.
Future<void> restartTrack([LocalVideoTrackOptions? options]) async { Future<void> restartTrack([
if (sender == null) { LocalVideoTrackOptions? options,
return Future.error(TrackCreateError('could not restart track')); ]) 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 _createStream(currentOptions);
final stream = await _createCameraStream(options); final track = stream.getVideoTracks().first;
final track = stream.getVideoTracks().first; setMediaStream(stream);
mediaStream = stream; await mediaStreamTrack.stop();
await mediaTrack.stop(); mediaStreamTrack = track;
mediaTrack = track; await sender?.replaceTrack(track);
await sender?.replaceTrack(track);
} catch (e) {
return Future.error(e);
}
} }
static Future<MediaStream> _createCameraStream(LocalVideoTrackOptions? options) async { /// Creates a LocalVideoTrack from camera input.
options ??= LocalVideoTrackOptions(params: VideoPresets.qhd); 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 { static Future<LocalVideoTrack> createScreenTrack([
final stream = await navigator.mediaDevices.getUserMedia(<String, dynamic>{ ScreenTrackOptions? options,
'audio': false, ]) async {
'video': options.mediaConstraints, options ??= const ScreenTrackOptions();
}); final stream = await _createStream(options);
return LocalVideoTrack._(
Track.screenShareName,
stream.getVideoTracks().first,
stream,
options,
);
}
if (stream.getVideoTracks().isEmpty) { static Future<MediaStream> _createStream(
return Future.error(TrackCreateError()); LocalVideoTrackOptions options,
} ) async {
final constraints = <String, dynamic>{
'audio': false,
'video': options.toMediaConstraintsMap(),
};
return stream; final MediaStream stream;
} catch (e) { if (options is ScreenTrackOptions) {
return Future.error(e); stream = await navigator.mediaDevices.getDisplayMedia(constraints);
} else {
// options is CameraVideoTrackOptions
stream = await navigator.mediaDevices.getUserMedia(constraints);
} }
if (stream.getVideoTracks().isEmpty) throw TrackCreateError();
return stream;
}
}
//
// 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
View File
@@ -1,26 +1,8 @@
/// Options when creating a LocalVideoTrack. import 'package:flutter_webrtc/flutter_webrtc.dart';
class LocalVideoTrackOptions {
CameraPosition position = CameraPosition.front;
VideoParameter params;
LocalVideoTrackOptions({ enum LocalVideoTrackType {
VideoParameter? params, camera,
CameraPosition? position, display,
}) : 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 CameraPosition { enum CameraPosition {
@@ -28,43 +10,229 @@ enum CameraPosition {
back, back,
} }
class VideoParameter { extension LKCameraPositionExt on CameraPosition {
int width; CameraPosition swap() => {
int height; CameraPosition.front: CameraPosition.back,
int fps; CameraPosition.back: CameraPosition.front,
int? bitrate; }[this]!;
VideoParameter(
this.width,
this.height,
this.fps, {
this.bitrate,
});
Map<String, dynamic> get mediaConstraints {
return <String, dynamic>{
'minWidth': width,
'minHeight': height,
'minFrameRate': fps,
};
}
} }
class VideoPresets { class CameraTrackOptions extends LocalVideoTrackOptions {
static final qvga = VideoParameter(320, 180, 15); final CameraPosition cameraPosition;
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);
static final List<VideoParameter> all = [ const CameraTrackOptions({
qvga, this.cameraPosition = CameraPosition.front,
vga, VideoParameters params = VideoParameters.presetQHD169,
qhd, }) : super(params: params);
hd,
fhd, @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. /// Options when creating an LocalAudioTrack. Placeholder for now.
class LocalAudioTrackOptions {} class LocalAudioTrackOptions {
const LocalAudioTrackOptions();
}
+14 -9
View File
@@ -1,6 +1,6 @@
import '../proto/livekit_models.pb.dart';
import '../proto/livekit_rtc.pbserver.dart';
import '../participant/remote_participant.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.dart';
import 'track_publication.dart'; import 'track_publication.dart';
@@ -10,10 +10,11 @@ class RemoteTrackPublication extends TrackPublication {
final RemoteParticipant _participant; final RemoteParticipant _participant;
bool _unsubscribed = false; bool _unsubscribed = false;
bool _disabled = false; bool _disabled = false;
VideoQuality _videoQuality = VideoQuality.HIGH; lk_rtc.VideoQuality _videoQuality = lk_rtc.VideoQuality.HIGH;
VideoQuality get videoQuality => _videoQuality; lk_rtc.VideoQuality get videoQuality => _videoQuality;
set videoQuality(VideoQuality val) {
set videoQuality(lk_rtc.VideoQuality val) {
if (val == _videoQuality) return; if (val == _videoQuality) return;
_videoQuality = val; _videoQuality = val;
_sendUpdateTrackSettings(); _sendUpdateTrackSettings();
@@ -56,21 +57,25 @@ class RemoteTrackPublication extends TrackPublication {
_participant.roomDelegate?.onTrackUnmuted(_participant, this); _participant.roomDelegate?.onTrackUnmuted(_participant, this);
} }
if (subscribed) { if (subscribed) {
track?.mediaTrack.enabled = !val; track?.mediaStreamTrack.enabled = !val;
} }
_participant.muteChanged(); _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; this.track = track;
} }
void _sendUpdateTrackSettings() { void _sendUpdateTrackSettings() {
final settings = UpdateTrackSettings( final settings = lk_rtc.UpdateTrackSettings(
trackSids: [sid], trackSids: [sid],
disabled: _disabled, disabled: _disabled,
); );
if (kind == TrackType.VIDEO) { if (kind == lk_models.TrackType.VIDEO) {
settings.quality = _videoQuality; settings.quality = _videoQuality;
} }
_participant.client.sendUpdateTrackSettings(settings); _participant.client.sendUpdateTrackSettings(settings);
+11 -10
View File
@@ -1,7 +1,7 @@
import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import '../proto/livekit_models.pb.dart'; import '../proto/livekit_models.pb.dart' as lk_models;
class TrackDimension { class TrackDimension {
int width; int width;
@@ -12,24 +12,25 @@ class TrackDimension {
/// Wrapper around a MediaStreamTrack with additional metadata. /// Wrapper around a MediaStreamTrack with additional metadata.
class Track { class Track {
static const cameraName = 'camera';
static const screenShareName = 'screen'; static const screenShareName = 'screen';
String name; String name;
TrackType kind; lk_models.TrackType kind;
MediaStreamTrack mediaTrack; MediaStreamTrack mediaStreamTrack;
String? sid; String? sid;
RTCRtpTransceiver? transceiver; RTCRtpTransceiver? transceiver;
String? _cid; 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 { RTCRtpMediaType get mediaType {
switch (kind) { switch (kind) {
case TrackType.AUDIO: case lk_models.TrackType.AUDIO:
return RTCRtpMediaType.RTCRtpMediaTypeAudio; return RTCRtpMediaType.RTCRtpMediaTypeAudio;
case TrackType.VIDEO: case lk_models.TrackType.VIDEO:
return RTCRtpMediaType.RTCRtpMediaTypeVideo; return RTCRtpMediaType.RTCRtpMediaTypeVideo;
// this should never happen // this should never happen
default: default:
@@ -38,7 +39,7 @@ class Track {
} }
String getCid() { String getCid() {
var cid = _cid ?? mediaTrack.id; var cid = _cid ?? mediaStreamTrack.id;
if (cid == null) { if (cid == null) {
const uuid = Uuid(); const uuid = Uuid();
@@ -48,7 +49,7 @@ class Track {
return cid; return cid;
} }
void stop() { Future<void> stop() async {
mediaTrack.stop(); await mediaStreamTrack.stop();
} }
} }
+6 -6
View File
@@ -1,4 +1,4 @@
import '../proto/livekit_models.pb.dart'; import '../proto/livekit_models.pb.dart' as lk_models;
import 'track.dart'; import 'track.dart';
/// Represents a track that's published to the server. This class contains /// Represents a track that's published to the server. This class contains
@@ -7,14 +7,14 @@ class TrackPublication {
Track? track; Track? track;
String name; String name;
String sid; String sid;
TrackType kind; lk_models.TrackType kind;
bool muted = false; bool muted = false;
bool simulcasted = false; bool simulcasted = false;
TrackDimension? dimension; TrackDimension? dimension;
bool get subscribed => track != null; bool get subscribed => track != null;
TrackPublication.fromInfo(TrackInfo info) TrackPublication.fromInfo(lk_models.TrackInfo info)
: sid = info.sid, : sid = info.sid,
name = info.name, name = info.name,
kind = info.type { kind = info.type {
@@ -22,12 +22,12 @@ class TrackPublication {
} }
/// True when the track is published with name [Track.screenShareName]. /// 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; muted = info.muted;
simulcasted = info.simulcast; simulcasted = info.simulcast;
if (info.type == TrackType.VIDEO) { if (info.type == lk_models.TrackType.VIDEO) {
dimension = TrackDimension(info.width, info.height); dimension = TrackDimension(info.width, info.height);
} }
} }
+18 -10
View File
@@ -1,29 +1,37 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_webrtc/flutter_webrtc.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'; import 'track.dart';
/// A video track will notify when its mediaTrack has changed. /// A video track will notify when its mediaTrack has changed.
class VideoTrack extends Track with ChangeNotifier { class VideoTrack extends Track with ChangeNotifier {
MediaStream? _mediaStream; MediaStream _mediaStream;
VideoTrack(String name, MediaStreamTrack mediaTrack, this._mediaStream) VideoTrack(
: super(TrackType.VIDEO, name, mediaTrack); String name,
MediaStreamTrack mediaTrack,
this._mediaStream,
) : super(
lk_models.TrackType.VIDEO,
name,
mediaTrack,
);
MediaStream? get mediaStream => _mediaStream; MediaStream get mediaStream => _mediaStream;
/// internal use /// internal use
/// {@nodoc} /// {@nodoc}
set mediaStream(MediaStream? stream) { void setMediaStream(MediaStream stream) {
_mediaStream = stream; _mediaStream = stream;
notifyListeners(); notifyListeners();
} }
@override @override
stop() { Future<void> stop() async {
super.stop(); await super.stop();
_mediaStream?.dispose(); await _mediaStream.dispose();
_mediaStream = null; // _mediaStream = null;
} }
} }
+41 -9
View File
@@ -1,36 +1,68 @@
import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'logger.dart';
/// a wrapper around PeerConnection /// a wrapper around PeerConnection
class PCTransport { class PCTransport {
RTCPeerConnection pc; final RTCPeerConnection pc;
List<RTCIceCandidate> pendingCandidates = []; final List<RTCIceCandidate> _pendingCandidates = [];
bool restartingIce = false; bool restartingIce = false;
PCTransport(this.pc); 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 { Future<void> setRemoteDescription(RTCSessionDescription sd) async {
await pc.setRemoteDescription(sd); await pc.setRemoteDescription(sd);
await Future.forEach<RTCIceCandidate>(pendingCandidates, (candidate) async { await Future.forEach<RTCIceCandidate>(_pendingCandidates, (candidate) async {
await pc.addCandidate(candidate); await pc.addCandidate(candidate);
}); });
pendingCandidates.clear(); _pendingCandidates.clear();
restartingIce = false; restartingIce = false;
} }
Future<void> addIceCandidate(RTCIceCandidate candidate) async { Future<void> addIceCandidate(RTCIceCandidate candidate) async {
final desc = await getRemoteDescription(); final desc = await getRemoteDescription();
if (desc != null && !restartingIce) { if (desc != null && !restartingIce) {
return pc.addCandidate(candidate); await pc.addCandidate(candidate);
return;
} }
pendingCandidates.add(candidate);
_pendingCandidates.add(candidate);
} }
Future<RTCSessionDescription?> getRemoteDescription() async { Future<RTCSessionDescription?> getRemoteDescription() async {
if (pc.iceConnectionState == null) { // Checking agains null doesn't work as intended
return null; // 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();
} }
} }
+87
View File
@@ -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,
),
];
}
}
-2
View File
@@ -1,2 +0,0 @@
const version = '0.4.0';
const protocolVersion = 2;
+9 -6
View File
@@ -1,22 +1,24 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart';
import '../track/video_track.dart';
import '../track/local_video_track.dart'; import '../track/local_video_track.dart';
import '../track/video_track.dart';
/// Widget that renders a [VideoTrack]. /// Widget that renders a [VideoTrack].
class VideoTrackRenderer extends StatefulWidget { class VideoTrackRenderer extends StatefulWidget {
final VideoTrack track; final VideoTrack track;
final RTCVideoRenderer renderer; final RTCVideoRenderer renderer;
final RTCVideoViewObjectFit fit;
VideoTrackRenderer(this.track) VideoTrackRenderer(
: renderer = RTCVideoRenderer(), this.track, {
this.fit = RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
}) : renderer = RTCVideoRenderer(),
super(key: ValueKey(track.sid)); super(key: ValueKey(track.sid));
@override @override
State<StatefulWidget> createState() { State<StatefulWidget> createState() => _VideoTrackRendererState();
return _VideoTrackRendererState();
}
} }
class _VideoTrackRendererState extends State<VideoTrackRenderer> { class _VideoTrackRendererState extends State<VideoTrackRenderer> {
@@ -63,6 +65,7 @@ class _VideoTrackRendererState extends State<VideoTrackRenderer> {
_renderer, _renderer,
mirror: isLocal, mirror: isLocal,
filterQuality: FilterQuality.medium, filterQuality: FilterQuality.medium,
objectFit: widget.fit,
); );
} }
} }
+41
View File
@@ -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);
}
+53
View File
@@ -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();
}
}
}
+54
View File
@@ -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
View File
@@ -7,7 +7,7 @@ packages:
name: async name: async
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.6.1" version: "2.8.1"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@@ -28,7 +28,7 @@ packages:
name: charcode name: charcode
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0" version: "1.3.1"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@@ -101,7 +101,7 @@ packages:
name: flutter_webrtc name: flutter_webrtc
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.6.6" version: "0.6.7"
http: http:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -143,7 +143,7 @@ packages:
name: meta name: meta
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.0" version: "1.7.0"
path: path:
dependency: transitive dependency: transitive
description: description:
@@ -157,7 +157,7 @@ packages:
name: path_provider name: path_provider
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.2" version: "2.0.3"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
@@ -261,6 +261,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
synchronized:
dependency: "direct main"
description:
name: synchronized
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
@@ -274,7 +281,7 @@ packages:
name: test_api name: test_api
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.3.0" version: "0.4.2"
tuple: tuple:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -303,20 +310,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0" 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: win32:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.7" version: "2.2.9"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:
+24 -6
View File
@@ -8,18 +8,36 @@ environment:
flutter: ">=1.17.0" flutter: ">=1.17.0"
dependencies: dependencies:
collection: ^1.15.0
fixnum: ^1.0.0
flutter: flutter:
sdk: 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 http: ^0.13.3
logging: ^1.0.1 logging: ^1.0.1
protobuf: ^2.0.0 protobuf: ^2.0.0
tuple: ^2.0.0
web_socket_channel: ^2.1.0
uuid: ^3.0.4
tuple: ^2.0.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: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter