Simulcast, Screen sharing & Various improvements (#4)

* Respect `RTCIceTransportPolicy` enum and organize

* Simplify syntax where possible etc.

* Combine `VideoPreset` and `VideoPresets`

* Default values for `ConnectOptions`

* Build URI instead of String manipulation

* Slight modifications to Exception

* `LiveKitTheme` for example

* `VideoEncoding` class

* Organize imports

* First simulcast implementation

* Remove unnecessary try-catches

* Update Android settings

* Remember uri and token

* example improvements

* `fit` parameter for VideoTrackRenderer

* Simulcast option for example

* Pass `defaultPublishOptions`

* Show only `VideoQuality`

* Pass tests

* Better buildUri logic

* Named parameter to positional

* Explicit imports

* `VideoParameter` instead of `VideoPreset`

* Use `mediaTrack.getSettings` when possible

* Safer dispose logic

* Safer `PCTransport`

Update transport.dart

* Synchronized events for `SignalClient`

* Use logger instead of print

* Make example compile for iOS

* First screen share implementation

* Make example work with screen share

* Example improvement

* Code optimization

* Don't depend on web_socket_channel

* Fix: Unpublish track bug

* Show participant mute state & identity

* Update protos

* Remote mute/unmute

* iOS Background mode

* Separate `createCameraTrack` and `createScreenTrack`

* Clean up

* PB fix

* format

* Fix analyzer warning

* Android clean up

* Update README.md

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