Simulcast, Screen sharing & Various improvements (#4)
* Respect `RTCIceTransportPolicy` enum and organize * Simplify syntax where possible etc. * Combine `VideoPreset` and `VideoPresets` * Default values for `ConnectOptions` * Build URI instead of String manipulation * Slight modifications to Exception * `LiveKitTheme` for example * `VideoEncoding` class * Organize imports * First simulcast implementation * Remove unnecessary try-catches * Update Android settings * Remember uri and token * example improvements * `fit` parameter for VideoTrackRenderer * Simulcast option for example * Pass `defaultPublishOptions` * Show only `VideoQuality` * Pass tests * Better buildUri logic * Named parameter to positional * Explicit imports * `VideoParameter` instead of `VideoPreset` * Use `mediaTrack.getSettings` when possible * Safer dispose logic * Safer `PCTransport` Update transport.dart * Synchronized events for `SignalClient` * Use logger instead of print * Make example compile for iOS * First screen share implementation * Make example work with screen share * Example improvement * Code optimization * Don't depend on web_socket_channel * Fix: Unpublish track bug * Show participant mute state & identity * Update protos * Remote mute/unmute * iOS Background mode * Separate `createCameraTrack` and `createScreenTrack` * Clean up * PB fix * format * Fix analyzer warning * Android clean up * Update README.md * Clean up
This commit is contained in:
@@ -76,3 +76,4 @@ build/
|
|||||||
|
|
||||||
# VS Code
|
# VS Code
|
||||||
.vscode/
|
.vscode/
|
||||||
|
*.code-workspace
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
-->
|
-->
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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,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
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
extension LKExampleExt on BuildContext {
|
||||||
|
//
|
||||||
|
Future<void> showErrorDialog(dynamic exception) => showDialog<void>(
|
||||||
|
context: this,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('Error'),
|
||||||
|
content: Text(exception.toString()),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx),
|
||||||
|
child: const Text('OK'),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
+11
-102
@@ -1,7 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package: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'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import '../exts.dart';
|
||||||
|
import 'room.dart';
|
||||||
|
|
||||||
|
class ConnectPage extends StatefulWidget {
|
||||||
|
//
|
||||||
|
const ConnectPage({
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _ConnectPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConnectPageState extends State<ConnectPage> {
|
||||||
|
//
|
||||||
|
static const _storeKeyUri = 'uri';
|
||||||
|
static const _storeKeyToken = 'token';
|
||||||
|
static const _storeKeySimulcast = 'simulcast';
|
||||||
|
|
||||||
|
final _uriCtrl = TextEditingController();
|
||||||
|
final _tokenCtrl = TextEditingController();
|
||||||
|
bool _simulcast = false;
|
||||||
|
bool _busy = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_readPrefs();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_uriCtrl.dispose();
|
||||||
|
_tokenCtrl.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _readPrefs() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
_uriCtrl.text = prefs.getString(_storeKeyUri) ?? '';
|
||||||
|
_tokenCtrl.text = prefs.getString(_storeKeyToken) ?? '';
|
||||||
|
setState(() {
|
||||||
|
_simulcast = prefs.getBool(_storeKeySimulcast) ?? false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _writePrefs() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setString(_storeKeyUri, _uriCtrl.text);
|
||||||
|
await prefs.setString(_storeKeyToken, _tokenCtrl.text);
|
||||||
|
await prefs.setBool(_storeKeySimulcast, _simulcast);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _connect(BuildContext ctx) async {
|
||||||
|
//
|
||||||
|
try {
|
||||||
|
setState(() {
|
||||||
|
_busy = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
print('Connecting with url: ${_uriCtrl.text}, token: ${_tokenCtrl.text}...');
|
||||||
|
|
||||||
|
final room = await LiveKitClient.connect(
|
||||||
|
_uriCtrl.text,
|
||||||
|
_tokenCtrl.text,
|
||||||
|
options: ConnectOptions(
|
||||||
|
defaultPublishOptions: TrackPublishOptions(
|
||||||
|
simulcast: _simulcast,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Save for next time
|
||||||
|
await _writePrefs();
|
||||||
|
|
||||||
|
await Navigator.push<void>(
|
||||||
|
ctx,
|
||||||
|
MaterialPageRoute(builder: (_) => RoomPage(room)),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
print('could not connect $error');
|
||||||
|
await ctx.showErrorDialog(error);
|
||||||
|
} finally {
|
||||||
|
setState(() {
|
||||||
|
_busy = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setSimulcast(bool? value) async {
|
||||||
|
if (value == null || _simulcast == value) return;
|
||||||
|
setState(() {
|
||||||
|
_simulcast = value;
|
||||||
|
});
|
||||||
|
// await _writePrefs();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Connect to LiveKit'),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 20,
|
||||||
|
horizontal: 20,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).cardColor,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Theme.of(context).colorScheme.secondary),
|
||||||
|
),
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
maxWidth: 320,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: _uriCtrl,
|
||||||
|
decoration: const InputDecoration(labelText: 'URL'),
|
||||||
|
),
|
||||||
|
TextField(
|
||||||
|
controller: _tokenCtrl,
|
||||||
|
decoration: const InputDecoration(labelText: 'Token'),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 20),
|
||||||
|
child: CheckboxListTile(
|
||||||
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
|
onChanged: (value) => _setSimulcast(value),
|
||||||
|
title: const Text('Use Simulcast'),
|
||||||
|
value: _simulcast,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 20),
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: _busy ? null : () => _connect(context),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (_busy)
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.only(right: 10),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 15,
|
||||||
|
width: 15,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: Colors.white,
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Text('Connect'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../widgets/controls.dart';
|
||||||
|
import '../widgets/participant.dart';
|
||||||
|
|
||||||
|
class RoomPage extends StatefulWidget {
|
||||||
|
//
|
||||||
|
final Room room;
|
||||||
|
|
||||||
|
const RoomPage(
|
||||||
|
this.room, {
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() {
|
||||||
|
return _RoomPageState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RoomPageState extends State<RoomPage> with RoomDelegate {
|
||||||
|
// BuildContext? _lastContext;
|
||||||
|
//
|
||||||
|
List<Participant> participants = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
widget.room.delegate = this;
|
||||||
|
widget.room.addListener(_onChange);
|
||||||
|
_onConnected();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
widget.room.delegate = null;
|
||||||
|
widget.room.removeListener(_onChange);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onConnected() async {
|
||||||
|
// video will fail when running in ios simulator
|
||||||
|
try {
|
||||||
|
final localVideo = await LocalVideoTrack.createCameraTrack(); // Defaults to camera
|
||||||
|
await widget.room.localParticipant.publishVideoTrack(
|
||||||
|
localVideo,
|
||||||
|
// options: TrackPublishOptions(
|
||||||
|
// // simulcast: true,
|
||||||
|
// videoEncoding: VideoParameters.presetQVGA169.encoding,
|
||||||
|
// ),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
print('could not publish video: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
final localAudio = await LocalAudioTrack.create();
|
||||||
|
await widget.room.localParticipant.publishAudioTrack(localAudio);
|
||||||
|
sortParticipants();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onChange() {
|
||||||
|
sortParticipants();
|
||||||
|
}
|
||||||
|
|
||||||
|
void sortParticipants() {
|
||||||
|
List<Participant> participants = [];
|
||||||
|
participants.addAll(widget.room.participants.values);
|
||||||
|
// sort speakers for the grid
|
||||||
|
participants.sort((a, b) {
|
||||||
|
// loudest speaker first
|
||||||
|
if (a.isSpeaking && b.isSpeaking) {
|
||||||
|
if (a.audioLevel > b.audioLevel) {
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// last spoken at
|
||||||
|
final aSpokeAt = a.lastSpokeAt?.millisecondsSinceEpoch ?? 0;
|
||||||
|
final bSpokeAt = b.lastSpokeAt?.millisecondsSinceEpoch ?? 0;
|
||||||
|
|
||||||
|
if (aSpokeAt != bSpokeAt) {
|
||||||
|
return aSpokeAt > bSpokeAt ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// video on
|
||||||
|
if (a.hasVideo != b.hasVideo) {
|
||||||
|
return a.hasVideo ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// joinedAt
|
||||||
|
return a.joinedAt.millisecondsSinceEpoch - b.joinedAt.millisecondsSinceEpoch;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (participants.length > 1) {
|
||||||
|
participants.insert(1, widget.room.localParticipant);
|
||||||
|
} else {
|
||||||
|
participants.add(widget.room.localParticipant);
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
this.participants = participants;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onDisconnected() {
|
||||||
|
// final context = _lastContext;
|
||||||
|
print('disconnected: $context');
|
||||||
|
// if (context != null) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
// with a provider, any child/descendent widget can be updated if they
|
||||||
|
// are a Consumer of Room.
|
||||||
|
body: ChangeNotifierProvider.value(
|
||||||
|
value: widget.room,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: participants.isNotEmpty
|
||||||
|
? ParticipantWidget(participants.first)
|
||||||
|
: Container()),
|
||||||
|
SizedBox(
|
||||||
|
height: 100,
|
||||||
|
child: ListView.builder(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: math.max(0, participants.length - 1),
|
||||||
|
itemBuilder: (BuildContext context, int index) => Container(
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
padding: const EdgeInsets.all(2),
|
||||||
|
child: ParticipantWidget(participants[index + 1], quality: VideoQuality.LOW),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: ControlsWidget(widget.room),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
|
||||||
import 'package:livekit_example/src/controls.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class RoomWidget extends StatefulWidget {
|
|
||||||
//
|
|
||||||
final Room room;
|
|
||||||
|
|
||||||
const RoomWidget(
|
|
||||||
this.room, {
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<StatefulWidget> createState() {
|
|
||||||
return _RoomState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _RoomState extends State<RoomWidget> with RoomDelegate {
|
|
||||||
BuildContext? _lastContext;
|
|
||||||
List<Participant> participants = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
widget.room.delegate = this;
|
|
||||||
widget.room.addListener(_onChange);
|
|
||||||
_onConnected();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
widget.room.delegate = null;
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onConnected() async {
|
|
||||||
// video will fail when running in ios simulator
|
|
||||||
try {
|
|
||||||
final localVideo = await LocalVideoTrack.createCameraTrack();
|
|
||||||
await widget.room.localParticipant.publishVideoTrack(localVideo);
|
|
||||||
} catch (e) {
|
|
||||||
print('could not publish video: $e');
|
|
||||||
}
|
|
||||||
|
|
||||||
final localAudio = await LocalAudioTrack.createTrack();
|
|
||||||
await widget.room.localParticipant.publishAudioTrack(localAudio);
|
|
||||||
sortParticipants();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onChange() {
|
|
||||||
sortParticipants();
|
|
||||||
}
|
|
||||||
|
|
||||||
void sortParticipants() {
|
|
||||||
List<Participant> participants = [];
|
|
||||||
participants.addAll(widget.room.participants.values);
|
|
||||||
// sort speakers for the grid
|
|
||||||
participants.sort((a, b) {
|
|
||||||
// loudest speaker first
|
|
||||||
if (a.isSpeaking && b.isSpeaking) {
|
|
||||||
if (a.audioLevel > b.audioLevel) {
|
|
||||||
return -1;
|
|
||||||
} else {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// last spoken at
|
|
||||||
final aSpokeAt = a.lastSpokeAt?.millisecondsSinceEpoch ?? 0;
|
|
||||||
final bSpokeAt = b.lastSpokeAt?.millisecondsSinceEpoch ?? 0;
|
|
||||||
|
|
||||||
if (aSpokeAt != bSpokeAt) {
|
|
||||||
return aSpokeAt > bSpokeAt ? -1 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// video on
|
|
||||||
if (a.hasVideo != b.hasVideo) {
|
|
||||||
return a.hasVideo ? -1 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// joinedAt
|
|
||||||
return a.joinedAt.millisecondsSinceEpoch - b.joinedAt.millisecondsSinceEpoch;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (participants.length > 1) {
|
|
||||||
participants.insert(1, widget.room.localParticipant);
|
|
||||||
} else {
|
|
||||||
participants.add(widget.room.localParticipant);
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
this.participants = participants;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onDisconnected() {
|
|
||||||
final context = _lastContext;
|
|
||||||
print('disconnected: $context');
|
|
||||||
if (context != null) {
|
|
||||||
Navigator.pop(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
_lastContext = context;
|
|
||||||
|
|
||||||
final mainWidgets = <Widget>[];
|
|
||||||
final participants = this.participants;
|
|
||||||
if (participants.isNotEmpty) {
|
|
||||||
mainWidgets.add(Expanded(child: VideoView(participants.first)));
|
|
||||||
} else {
|
|
||||||
mainWidgets.add(Expanded(child: Container()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (participants.length > 1) {
|
|
||||||
final videoList = ListView.builder(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
itemCount: participants.length - 1,
|
|
||||||
itemBuilder: (BuildContext context, int index) {
|
|
||||||
return Container(
|
|
||||||
width: 100,
|
|
||||||
height: 60,
|
|
||||||
padding: const EdgeInsets.all(2),
|
|
||||||
child: VideoView(participants[index + 1], quality: VideoQuality.LOW),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
mainWidgets.add(SizedBox(
|
|
||||||
height: 60,
|
|
||||||
child: videoList,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
mainWidgets.add(Controls(widget.room));
|
|
||||||
return MaterialApp(
|
|
||||||
title: 'LiveKit Video Room',
|
|
||||||
theme: ThemeData(
|
|
||||||
primarySwatch: Colors.deepPurple,
|
|
||||||
),
|
|
||||||
home: Scaffold(
|
|
||||||
// with a provider, any child/descendent widget can be updated if they
|
|
||||||
// are a Consumer of Room.
|
|
||||||
body: ChangeNotifierProvider.value(
|
|
||||||
value: widget.room,
|
|
||||||
child: Column(
|
|
||||||
children: mainWidgets,
|
|
||||||
))));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// displays a participant in view
|
|
||||||
class VideoView extends StatefulWidget {
|
|
||||||
//
|
|
||||||
final Participant participant;
|
|
||||||
final VideoQuality quality;
|
|
||||||
|
|
||||||
const VideoView(
|
|
||||||
this.participant, {
|
|
||||||
this.quality = VideoQuality.MEDIUM,
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<StatefulWidget> createState() {
|
|
||||||
return _VideoViewState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _VideoViewState extends State<VideoView> with ParticipantDelegate {
|
|
||||||
TrackPublication? videoPub;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
widget.participant.addListener(_onParticipantChanged);
|
|
||||||
_onParticipantChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
widget.participant.removeListener(_onParticipantChanged);
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(covariant VideoView oldWidget) {
|
|
||||||
oldWidget.participant.removeListener(_onParticipantChanged);
|
|
||||||
widget.participant.addListener(_onParticipantChanged);
|
|
||||||
_onParticipantChanged();
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
}
|
|
||||||
|
|
||||||
// register for change so Flutter will re-build the widget upon change
|
|
||||||
void _onParticipantChanged() {
|
|
||||||
final subscribedVideos = widget.participant.videoTracks.values.where((pub) {
|
|
||||||
return pub.kind == TrackType.VIDEO && !pub.isScreenShare && pub.subscribed;
|
|
||||||
});
|
|
||||||
setState(() {
|
|
||||||
if (subscribedVideos.isNotEmpty) {
|
|
||||||
final videoPub = subscribedVideos.first;
|
|
||||||
if (videoPub is RemoteTrackPublication) {
|
|
||||||
videoPub.videoQuality = widget.quality;
|
|
||||||
}
|
|
||||||
// when muted, show placeholder
|
|
||||||
if (!videoPub.muted) {
|
|
||||||
this.videoPub = videoPub;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
videoPub = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final videoPub = this.videoPub;
|
|
||||||
if (videoPub != null) {
|
|
||||||
return VideoTrackRenderer(videoPub.track as VideoTrack);
|
|
||||||
} else {
|
|
||||||
return Container(
|
|
||||||
color: Colors.grey,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
|
||||||
|
|
||||||
class Controls extends StatefulWidget {
|
|
||||||
//
|
|
||||||
final Room room;
|
|
||||||
final LocalParticipant participant;
|
|
||||||
|
|
||||||
Controls(
|
|
||||||
this.room, {
|
|
||||||
Key? key,
|
|
||||||
}) : participant = room.localParticipant,
|
|
||||||
super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<StatefulWidget> createState() {
|
|
||||||
return _ControlsState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ControlsState extends State<Controls> {
|
|
||||||
CameraPosition position = CameraPosition.front;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
participant.addListener(_onChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
participant.removeListener(_onChange);
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
LocalParticipant get participant => widget.participant;
|
|
||||||
|
|
||||||
void _onChange() {
|
|
||||||
// trigger refresh
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _muteAudio() {
|
|
||||||
if (participant.hasAudio) {
|
|
||||||
final audioPub = participant.audioTracks.values.first;
|
|
||||||
audioPub.muted = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _unmuteAudio() async {
|
|
||||||
if (participant.hasAudio) {
|
|
||||||
final audioPub = participant.audioTracks.values.first;
|
|
||||||
audioPub.muted = false;
|
|
||||||
} else {
|
|
||||||
// publish audio track
|
|
||||||
final audioTrack = await LocalAudioTrack.createTrack();
|
|
||||||
await participant.publishAudioTrack(audioTrack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _muteVideo() {
|
|
||||||
if (participant.hasVideo) {
|
|
||||||
final videoPub = participant.videoTracks.values.first;
|
|
||||||
videoPub.muted = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _unmuteVideo() async {
|
|
||||||
if (participant.hasVideo) {
|
|
||||||
final videoPub = participant.videoTracks.values.first;
|
|
||||||
videoPub.muted = false;
|
|
||||||
} else {
|
|
||||||
// publish audio track
|
|
||||||
final videoTrack = await LocalVideoTrack.createCameraTrack();
|
|
||||||
await participant.publishVideoTrack(videoTrack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _setCameraPosition(TrackPublication? pub, CameraPosition position) async {
|
|
||||||
if (this.position == position) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
LocalVideoTrack? track;
|
|
||||||
if (pub?.track is LocalVideoTrack) {
|
|
||||||
track = pub!.track as LocalVideoTrack;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (track == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await track.restartTrack(LocalVideoTrackOptions(position: position));
|
|
||||||
} catch (e) {
|
|
||||||
print('could not restart track: $e');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
this.position = position;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _exit() {
|
|
||||||
widget.room.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final buttons = <Widget>[];
|
|
||||||
|
|
||||||
// mute audio
|
|
||||||
if (participant.hasAudio && !participant.isMuted) {
|
|
||||||
buttons.add(
|
|
||||||
IconButton(
|
|
||||||
onPressed: _muteAudio,
|
|
||||||
icon: const Icon(Icons.mic_rounded),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
buttons.add(
|
|
||||||
IconButton(
|
|
||||||
onPressed: _unmuteAudio,
|
|
||||||
icon: const Icon(Icons.mic_off_rounded),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// mute video
|
|
||||||
TrackPublication? videoPub;
|
|
||||||
if (participant.hasVideo) {
|
|
||||||
videoPub = participant.videoTracks.values.first;
|
|
||||||
}
|
|
||||||
|
|
||||||
final videoEnabled = videoPub != null && !videoPub.muted;
|
|
||||||
if (videoEnabled) {
|
|
||||||
buttons.add(IconButton(
|
|
||||||
onPressed: _muteVideo,
|
|
||||||
icon: const Icon(Icons.videocam_rounded),
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
buttons.add(IconButton(
|
|
||||||
onPressed: _unmuteVideo,
|
|
||||||
icon: const Icon(Icons.videocam_off_rounded),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (position == CameraPosition.front) {
|
|
||||||
buttons.add(IconButton(
|
|
||||||
icon: const Icon(Icons.video_camera_front_rounded),
|
|
||||||
onPressed: videoEnabled
|
|
||||||
? () {
|
|
||||||
_setCameraPosition(videoPub, CameraPosition.back);
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
buttons.add(IconButton(
|
|
||||||
icon: const Icon(Icons.video_camera_back_rounded),
|
|
||||||
onPressed: videoEnabled
|
|
||||||
? () {
|
|
||||||
_setCameraPosition(videoPub, CameraPosition.front);
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
buttons.add(IconButton(
|
|
||||||
onPressed: _exit,
|
|
||||||
icon: const Icon(Icons.close_rounded),
|
|
||||||
));
|
|
||||||
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: buttons,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Flutter has a color profile issue so colors will look different
|
||||||
|
// on Apple devices.
|
||||||
|
// https://github.com/flutter/flutter/issues/55092
|
||||||
|
// https://github.com/flutter/flutter/issues/39113
|
||||||
|
//
|
||||||
|
class LiveKitTheme {
|
||||||
|
//
|
||||||
|
final bgColor = Colors.black;
|
||||||
|
final textColor = Colors.white;
|
||||||
|
final cardColor = const Color(0xFF00163c);
|
||||||
|
final accentColor = const Color(0xFF2d6aef);
|
||||||
|
|
||||||
|
ThemeData buildThemeData(BuildContext ctx) => ThemeData(
|
||||||
|
backgroundColor: bgColor,
|
||||||
|
// accentColor: accentColor,
|
||||||
|
colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.blue),
|
||||||
|
appBarTheme: AppBarTheme(
|
||||||
|
backgroundColor: cardColor,
|
||||||
|
),
|
||||||
|
cardColor: cardColor,
|
||||||
|
scaffoldBackgroundColor: bgColor,
|
||||||
|
canvasColor: bgColor,
|
||||||
|
iconTheme: IconThemeData(
|
||||||
|
color: textColor,
|
||||||
|
),
|
||||||
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||||
|
style: ButtonStyle(
|
||||||
|
foregroundColor: MaterialStateProperty.all<Color>(Colors.white),
|
||||||
|
// backgroundColor: MaterialStateProperty.all<Color>(accentColor),
|
||||||
|
backgroundColor: MaterialStateProperty.resolveWith((states) {
|
||||||
|
if (states.contains(MaterialState.disabled)) return accentColor.withOpacity(0.5);
|
||||||
|
return accentColor;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
checkboxTheme: CheckboxThemeData(
|
||||||
|
checkColor: MaterialStateProperty.all(Colors.white),
|
||||||
|
fillColor: MaterialStateProperty.all(accentColor),
|
||||||
|
),
|
||||||
|
dialogBackgroundColor: cardColor,
|
||||||
|
textTheme: GoogleFonts.latoTextTheme(
|
||||||
|
Theme.of(ctx).textTheme,
|
||||||
|
).apply(
|
||||||
|
displayColor: textColor,
|
||||||
|
bodyColor: textColor,
|
||||||
|
decorationColor: textColor,
|
||||||
|
),
|
||||||
|
hintColor: Colors.red,
|
||||||
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
|
labelStyle: TextStyle(
|
||||||
|
color: textColor.withOpacity(.5),
|
||||||
|
),
|
||||||
|
enabledBorder: UnderlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: textColor.withOpacity(0.1)),
|
||||||
|
),
|
||||||
|
focusedBorder: UnderlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: accentColor),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import 'package:eva_icons_flutter/eva_icons_flutter.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
|
class ControlsWidget extends StatefulWidget {
|
||||||
|
//
|
||||||
|
final Room room;
|
||||||
|
final LocalParticipant participant;
|
||||||
|
|
||||||
|
ControlsWidget(
|
||||||
|
this.room, {
|
||||||
|
Key? key,
|
||||||
|
}) : participant = room.localParticipant,
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _ControlsWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ControlsWidgetState extends State<ControlsWidget> {
|
||||||
|
//
|
||||||
|
CameraPosition position = CameraPosition.front;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
participant.addListener(_onChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
participant.removeListener(_onChange);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalParticipant get participant => widget.participant;
|
||||||
|
|
||||||
|
void _onChange() {
|
||||||
|
// trigger refresh
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _muteAudio() {
|
||||||
|
if (participant.hasAudio) {
|
||||||
|
final audioPub = participant.audioTracks.first;
|
||||||
|
audioPub.muted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _unmuteAudio() async {
|
||||||
|
if (participant.hasAudio) {
|
||||||
|
final audioPub = participant.audioTracks.first;
|
||||||
|
audioPub.muted = false;
|
||||||
|
} else {
|
||||||
|
// publish audio track
|
||||||
|
final audioTrack = await LocalAudioTrack.create();
|
||||||
|
await participant.publishAudioTrack(audioTrack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _muteVideo() {
|
||||||
|
if (participant.hasVideo) {
|
||||||
|
final videoPub = participant.videoTracks.first;
|
||||||
|
videoPub.muted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _unmuteVideo() async {
|
||||||
|
if (participant.hasVideo) {
|
||||||
|
print('Un-muting video');
|
||||||
|
final videoPub = participant.videoTracks.first;
|
||||||
|
videoPub.muted = false;
|
||||||
|
} else {
|
||||||
|
// publish audio track
|
||||||
|
final videoTrack = await LocalVideoTrack.createCameraTrack();
|
||||||
|
await participant.publishVideoTrack(videoTrack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _toggleCamera() async {
|
||||||
|
//
|
||||||
|
final track = participant.videoTracks.firstOrNull?.track as LocalVideoTrack?;
|
||||||
|
if (track == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final newPosition = position.swap();
|
||||||
|
await track.setCameraPosition(newPosition);
|
||||||
|
setState(() {
|
||||||
|
position = newPosition;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
print('could not restart track: $error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _shareScreen() async {
|
||||||
|
//
|
||||||
|
final lp = widget.room.localParticipant;
|
||||||
|
|
||||||
|
for (final tracks in lp.videoTracks) {
|
||||||
|
await lp.unpublishTrack(tracks.track!);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final screenTrack = await LocalVideoTrack.createScreenTrack(); // Defaults to camera
|
||||||
|
await widget.room.localParticipant.publishVideoTrack(
|
||||||
|
screenTrack,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
print('could not publish video: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _exit() {
|
||||||
|
widget.room.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// mute audio
|
||||||
|
final canMute = participant.hasAudio && !participant.isMuted;
|
||||||
|
|
||||||
|
final videoPub = participant.videoTracks.firstOrNull;
|
||||||
|
final videoEnabled = videoPub != null && !videoPub.muted;
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (canMute)
|
||||||
|
IconButton(
|
||||||
|
onPressed: _muteAudio,
|
||||||
|
icon: const Icon(EvaIcons.mic),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
IconButton(
|
||||||
|
onPressed: _unmuteAudio,
|
||||||
|
icon: const Icon(EvaIcons.micOff),
|
||||||
|
),
|
||||||
|
if (videoEnabled)
|
||||||
|
IconButton(
|
||||||
|
onPressed: _muteVideo,
|
||||||
|
icon: const Icon(EvaIcons.video),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
IconButton(
|
||||||
|
onPressed: _unmuteVideo,
|
||||||
|
icon: const Icon(EvaIcons.videoOff),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(position == CameraPosition.back ? EvaIcons.camera : EvaIcons.person),
|
||||||
|
onPressed: () => _toggleCamera(),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(EvaIcons.monitor),
|
||||||
|
onPressed: () => _shareScreen(),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: _exit,
|
||||||
|
icon: const Icon(EvaIcons.closeCircle),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import 'package:eva_icons_flutter/eva_icons_flutter.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
class NoVideoWidget extends StatelessWidget {
|
||||||
|
//
|
||||||
|
const NoVideoWidget({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Container(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (ctx, constraints) => Icon(
|
||||||
|
EvaIcons.videoOffOutline,
|
||||||
|
color: Theme.of(ctx).colorScheme.secondary,
|
||||||
|
size: math.min(constraints.maxHeight, constraints.maxWidth) * 0.3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||||
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
|
|
||||||
|
import 'no_video.dart';
|
||||||
|
import 'participant_info.dart';
|
||||||
|
|
||||||
|
class ParticipantWidget extends StatefulWidget {
|
||||||
|
//
|
||||||
|
final Participant participant;
|
||||||
|
final VideoQuality quality;
|
||||||
|
|
||||||
|
const ParticipantWidget(
|
||||||
|
this.participant, {
|
||||||
|
this.quality = VideoQuality.MEDIUM,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _ParticipantWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ParticipantWidgetState extends State<ParticipantWidget> with ParticipantDelegate {
|
||||||
|
//
|
||||||
|
TrackPublication? videoPub;
|
||||||
|
TrackPublication? audioPub;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
widget.participant.addListener(_onParticipantChanged);
|
||||||
|
_onParticipantChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
widget.participant.removeListener(_onParticipantChanged);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant ParticipantWidget oldWidget) {
|
||||||
|
oldWidget.participant.removeListener(_onParticipantChanged);
|
||||||
|
widget.participant.addListener(_onParticipantChanged);
|
||||||
|
_onParticipantChanged();
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
// register for change so Flutter will re-build the widget upon change
|
||||||
|
void _onParticipantChanged() {
|
||||||
|
//
|
||||||
|
final firstAudio = widget.participant.audioTracks.firstWhereOrNull((pub) => pub.subscribed);
|
||||||
|
final firstVideo = widget.participant.videoTracks
|
||||||
|
.firstWhereOrNull((pub) => !pub.isScreenShare && pub.subscribed);
|
||||||
|
|
||||||
|
if (firstVideo is RemoteTrackPublication) {
|
||||||
|
firstVideo.videoQuality = widget.quality;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
audioPub = !(firstAudio?.muted ?? true) ? firstAudio : null;
|
||||||
|
videoPub = !(firstVideo?.muted ?? true) ? firstVideo : null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext ctx) => Container(
|
||||||
|
color: Theme.of(ctx).cardColor,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
// Video
|
||||||
|
if (videoPub != null)
|
||||||
|
VideoTrackRenderer(
|
||||||
|
videoPub!.track as VideoTrack,
|
||||||
|
fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const NoVideoWidget(),
|
||||||
|
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: ParticipantInfoWidget(
|
||||||
|
title: widget.participant.identity,
|
||||||
|
muted: audioPub == null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import 'package:eva_icons_flutter/eva_icons_flutter.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class ParticipantInfoWidget extends StatelessWidget {
|
||||||
|
//
|
||||||
|
final String? title;
|
||||||
|
final bool muted;
|
||||||
|
|
||||||
|
const ParticipantInfoWidget({
|
||||||
|
this.title,
|
||||||
|
this.muted = true,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Container(
|
||||||
|
color: Colors.black.withOpacity(0.3),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 7,
|
||||||
|
horizontal: 10,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (title != null)
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
title!,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 5),
|
||||||
|
child: Icon(
|
||||||
|
!muted ? EvaIcons.mic : EvaIcons.micOff,
|
||||||
|
color: !muted ? Colors.white : Colors.red,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
+78
-17
@@ -7,7 +7,7 @@ packages:
|
|||||||
name: async
|
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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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';
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
|
||||||
|
|
||||||
Future<WebSocketChannel> connectToWebSocket(Uri uri) {
|
|
||||||
throw UnsupportedError('no implementations found');
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
// ignore: avoid_web_libraries_in_flutter
|
|
||||||
import 'dart:html';
|
|
||||||
|
|
||||||
import 'package:web_socket_channel/html.dart';
|
|
||||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
|
||||||
|
|
||||||
Future<WebSocketChannel> connectToWebSocket(Uri uri) {
|
|
||||||
final ws = WebSocket(uri.toString());
|
|
||||||
ws.binaryType = 'arraybuffer';
|
|
||||||
final completer = Completer<WebSocketChannel>();
|
|
||||||
ws.onOpen.first.then((_) {
|
|
||||||
completer.complete(HtmlWebSocketChannel(ws));
|
|
||||||
});
|
|
||||||
ws.onError.first.then((e) {
|
|
||||||
completer.completeError('could not connect');
|
|
||||||
});
|
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:web_socket_channel/io.dart';
|
|
||||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
|
||||||
|
|
||||||
Future<WebSocketChannel> connectToWebSocket(Uri uri) async {
|
|
||||||
try {
|
|
||||||
// ignore: close_sinks
|
|
||||||
final ws = await WebSocket.connect(uri.toString());
|
|
||||||
return IOWebSocketChannel(ws);
|
|
||||||
} catch (e) {
|
|
||||||
return Future.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+19
-17
@@ -1,30 +1,32 @@
|
|||||||
class LiveKitError extends Error {
|
//
|
||||||
String message;
|
// `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
@@ -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
@@ -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
@@ -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})';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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=');
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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=');
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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) {
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
@@ -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();
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||||
|
|
||||||
|
import 'options.dart';
|
||||||
|
import 'track/options.dart';
|
||||||
|
|
||||||
|
class Utils {
|
||||||
|
static List<VideoParameters> _presetsForResolution(
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
) {
|
||||||
|
final double aspect = width / height;
|
||||||
|
if ((aspect - 16.0 / 9.0).abs() < (aspect - 4.0 / 3.0).abs()) return VideoParameters.presets169;
|
||||||
|
return VideoParameters.presets43;
|
||||||
|
}
|
||||||
|
|
||||||
|
static VideoParameters _findPresetForResolution(
|
||||||
|
int width,
|
||||||
|
int height, {
|
||||||
|
required List<VideoParameters> presets,
|
||||||
|
}) {
|
||||||
|
assert(presets.isNotEmpty, 'presets should not be empty');
|
||||||
|
VideoParameters result = presets.first;
|
||||||
|
for (final preset in presets) {
|
||||||
|
if (width >= preset.width && height >= preset.height) result = preset;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<RTCRtpEncoding>? computeVideoEncodings({
|
||||||
|
int? width,
|
||||||
|
int? height,
|
||||||
|
TrackPublishOptions? options,
|
||||||
|
}) {
|
||||||
|
options ??= const TrackPublishOptions();
|
||||||
|
|
||||||
|
VideoEncoding? videoEncoding = options.videoEncoding;
|
||||||
|
|
||||||
|
if ((videoEncoding == null && !options.simulcast) || width == null || height == null) {
|
||||||
|
// don't set encoding when we are not simulcasting and user isn't restricting
|
||||||
|
// encoding parameters
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final presets = _presetsForResolution(width, height);
|
||||||
|
|
||||||
|
if (videoEncoding == null) {
|
||||||
|
// find the right encoding based on width/height
|
||||||
|
final preset = _findPresetForResolution(width, height, presets: presets);
|
||||||
|
// print('Using preset: ${preset.id}');
|
||||||
|
videoEncoding = preset.encoding;
|
||||||
|
// log.debug('using video encoding', videoEncoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not simulcast
|
||||||
|
if (!options.simulcast) return [videoEncoding.toRTCRtpEncoding()];
|
||||||
|
|
||||||
|
// Compute for simulcast
|
||||||
|
final midPreset = presets[1];
|
||||||
|
final lowPreset = presets[0];
|
||||||
|
return [
|
||||||
|
videoEncoding.toRTCRtpEncoding(
|
||||||
|
rid: 'f',
|
||||||
|
),
|
||||||
|
// if resolution is high enough, we would send both h and q res..
|
||||||
|
// otherwise only send h
|
||||||
|
if (height * 0.7 >= midPreset.height) ...[
|
||||||
|
midPreset.encoding.toRTCRtpEncoding(
|
||||||
|
rid: 'h',
|
||||||
|
scaleResolutionDownBy: height / midPreset.height,
|
||||||
|
),
|
||||||
|
lowPreset.encoding.toRTCRtpEncoding(
|
||||||
|
rid: 'q',
|
||||||
|
scaleResolutionDownBy: height / lowPreset.height,
|
||||||
|
),
|
||||||
|
] else
|
||||||
|
lowPreset.encoding.toRTCRtpEncoding(
|
||||||
|
rid: 'h',
|
||||||
|
scaleResolutionDownBy: height / lowPreset.height,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
const version = '0.4.0';
|
|
||||||
const protocolVersion = 2;
|
|
||||||
@@ -1,22 +1,24 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import 'platform/io.dart' if (dart.library.html) 'platform/web.dart';
|
||||||
|
|
||||||
|
class LKWebSocketError implements Exception {
|
||||||
|
final int code;
|
||||||
|
const LKWebSocketError._(this.code);
|
||||||
|
|
||||||
|
static LKWebSocketError unknown() => const LKWebSocketError._(0);
|
||||||
|
static LKWebSocketError connect() => const LKWebSocketError._(1);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => {
|
||||||
|
LKWebSocketError.unknown(): 'Unknown error',
|
||||||
|
LKWebSocketError.connect(): 'Failed to connect',
|
||||||
|
}[this]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef LKWebSocketOnData = Function(dynamic data);
|
||||||
|
typedef LKWebSocketOnError = Function(dynamic error);
|
||||||
|
typedef LKWebSocketOnDispose = Function();
|
||||||
|
|
||||||
|
class LKWebSocketOptions {
|
||||||
|
final LKWebSocketOnData? onData;
|
||||||
|
final LKWebSocketOnError? onError;
|
||||||
|
final LKWebSocketOnDispose? onDispose;
|
||||||
|
const LKWebSocketOptions({
|
||||||
|
this.onData,
|
||||||
|
this.onError,
|
||||||
|
this.onDispose,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class LKWebSocket {
|
||||||
|
void send(List<int> data);
|
||||||
|
void dispose();
|
||||||
|
|
||||||
|
static Future<LKWebSocket> connect(
|
||||||
|
Uri uri, [
|
||||||
|
LKWebSocketOptions? options,
|
||||||
|
]) =>
|
||||||
|
lkWebSocketConnect(uri, options);
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io' as io;
|
||||||
|
|
||||||
|
import 'package:livekit_client/src/logger.dart';
|
||||||
|
|
||||||
|
import '../interface.dart';
|
||||||
|
|
||||||
|
Future<LKWebSocketIO> lkWebSocketConnect(
|
||||||
|
Uri uri, [
|
||||||
|
LKWebSocketOptions? options,
|
||||||
|
]) =>
|
||||||
|
LKWebSocketIO.connect(uri, options);
|
||||||
|
|
||||||
|
class LKWebSocketIO implements LKWebSocket {
|
||||||
|
final io.WebSocket _ws;
|
||||||
|
final LKWebSocketOptions? options;
|
||||||
|
late final StreamSubscription _subscription;
|
||||||
|
|
||||||
|
LKWebSocketIO._(
|
||||||
|
this._ws, [
|
||||||
|
this.options,
|
||||||
|
]) {
|
||||||
|
_subscription = _ws.listen(
|
||||||
|
(dynamic data) => options?.onData?.call(data),
|
||||||
|
onDone: () => dispose(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
options?.onDispose?.call();
|
||||||
|
_subscription.cancel();
|
||||||
|
_ws.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void send(List<int> data) => _ws.add(data);
|
||||||
|
|
||||||
|
static Future<LKWebSocketIO> connect(
|
||||||
|
Uri uri, [
|
||||||
|
LKWebSocketOptions? options,
|
||||||
|
]) async {
|
||||||
|
logger.fine('LKWebSocketIO connect (uri: ${uri.toString()})');
|
||||||
|
try {
|
||||||
|
final ws = await io.WebSocket.connect(uri.toString());
|
||||||
|
logger.fine('LKWebSocketIO connected');
|
||||||
|
return LKWebSocketIO._(ws, options);
|
||||||
|
} catch (_) {
|
||||||
|
logger.severe('LKWebSocketIO error ${_}');
|
||||||
|
throw LKWebSocketError.connect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
// ignore: avoid_web_libraries_in_flutter
|
||||||
|
import 'dart:html' as html;
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import '../interface.dart';
|
||||||
|
|
||||||
|
Future<LKWebSocketWeb> lkWebSocketConnect(
|
||||||
|
Uri uri, [
|
||||||
|
LKWebSocketOptions? options,
|
||||||
|
]) =>
|
||||||
|
LKWebSocketWeb.connect(uri, options);
|
||||||
|
|
||||||
|
class LKWebSocketWeb implements LKWebSocket {
|
||||||
|
final html.WebSocket _ws;
|
||||||
|
final LKWebSocketOptions? options;
|
||||||
|
late final StreamSubscription _messageSubscription;
|
||||||
|
late final StreamSubscription _closeSubscription;
|
||||||
|
|
||||||
|
LKWebSocketWeb._(
|
||||||
|
this._ws, [
|
||||||
|
this.options,
|
||||||
|
]) {
|
||||||
|
_ws.binaryType = 'arraybuffer';
|
||||||
|
_messageSubscription = _ws.onMessage.listen((_) {
|
||||||
|
dynamic _data = _.data is ByteBuffer ? _.data.asUint8List() : _.data;
|
||||||
|
options?.onData?.call(_data);
|
||||||
|
});
|
||||||
|
_closeSubscription = _ws.onClose.listen((_) => dispose());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void send(List<int> data) => _ws.send(data);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
options?.onDispose?.call();
|
||||||
|
_messageSubscription.cancel();
|
||||||
|
_closeSubscription.cancel();
|
||||||
|
_ws.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<LKWebSocketWeb> connect(
|
||||||
|
Uri uri, [
|
||||||
|
LKWebSocketOptions? options,
|
||||||
|
]) async {
|
||||||
|
final completer = Completer<LKWebSocketWeb>();
|
||||||
|
final ws = html.WebSocket(uri.toString());
|
||||||
|
ws.onOpen.listen((_) => completer.complete(LKWebSocketWeb._(ws, options)));
|
||||||
|
ws.onError.listen((_) => completer.completeError(LKWebSocketError.connect()));
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
-14
@@ -7,7 +7,7 @@ packages:
|
|||||||
name: async
|
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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user