From 50f56c5e1efa300f3cd27f747c17bcf8d2459ffe Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Sat, 11 Sep 2021 03:14:14 +0900 Subject: [PATCH] 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 --- .gitignore | 1 + README.md | 30 +- analysis_options.yaml | 2 + example/analysis_options.yaml | 2 + example/android/app/build.gradle | 10 +- .../android/app/src/debug/AndroidManifest.xml | 2 +- .../android/app/src/main/AndroidManifest.xml | 50 +- .../example/livekit_example/MainActivity.kt | 2 +- .../app/src/profile/AndroidManifest.xml | 2 +- example/android/build.gradle | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- example/ios/Podfile | 11 +- example/ios/Podfile.lock | 10 +- example/ios/Runner.xcodeproj/project.pbxproj | 12 +- example/ios/Runner/Info.plist | 12 +- example/lib/exts.dart | 18 + example/lib/main.dart | 113 +-- example/lib/pages/connect.dart | 170 ++++ example/lib/pages/room.dart | 152 ++++ example/lib/room.dart | 229 ----- example/lib/src/controls.dart | 178 ---- example/lib/theme.dart | 65 ++ example/lib/widgets/controls.dart | 166 ++++ example/lib/widgets/no_video.dart | 20 + example/lib/widgets/participant.dart | 91 ++ example/lib/widgets/participant_info.dart | 44 + example/pubspec.lock | 95 ++- example/pubspec.yaml | 10 +- example/test/widget_test.dart | 2 +- lib/livekit_client.dart | 22 +- lib/src/_websocket_api.dart | 5 - lib/src/_websocket_html.dart | 19 - lib/src/_websocket_io.dart | 14 - lib/src/errors.dart | 36 +- lib/src/extensions.dart | 75 +- lib/src/livekit.dart | 16 +- lib/src/options.dart | 29 +- lib/src/participant/local_participant.dart | 174 ++-- lib/src/participant/participant.dart | 46 +- lib/src/participant/remote_participant.dart | 56 +- lib/src/proto/livekit_models.pb.dart | 802 ++++++------------ lib/src/proto/livekit_models.pbenum.dart | 20 +- lib/src/proto/livekit_models.pbjson.dart | 112 ++- lib/src/proto/livekit_models.pbserver.dart | 2 +- lib/src/proto/livekit_rtc.pb.dart | 453 ++-------- lib/src/proto/livekit_rtc.pbenum.dart | 20 +- lib/src/proto/livekit_rtc.pbjson.dart | 87 +- lib/src/proto/livekit_rtc.pbserver.dart | 2 +- lib/src/room.dart | 60 +- lib/src/rtc_engine.dart | 177 ++-- lib/src/signal_client.dart | 363 ++++---- lib/src/track/audio_track.dart | 14 +- lib/src/track/local_audio_track.dart | 32 +- lib/src/track/local_track_publication.dart | 18 +- lib/src/track/local_video_track.dart | 131 ++- lib/src/track/options.dart | 278 ++++-- lib/src/track/remote_track_publication.dart | 23 +- lib/src/track/track.dart | 21 +- lib/src/track/track_publication.dart | 12 +- lib/src/track/video_track.dart | 28 +- lib/src/transport.dart | 50 +- lib/src/utils.dart | 87 ++ lib/src/version.dart | 2 - lib/src/widget/video_track_renderer.dart | 15 +- lib/src/ws/interface.dart | 41 + lib/src/ws/platform/io.dart | 53 ++ lib/src/ws/platform/web.dart | 54 ++ pubspec.lock | 28 +- pubspec.yaml | 30 +- 69 files changed, 2656 insertions(+), 2354 deletions(-) create mode 100644 example/lib/exts.dart create mode 100644 example/lib/pages/connect.dart create mode 100644 example/lib/pages/room.dart delete mode 100644 example/lib/room.dart delete mode 100644 example/lib/src/controls.dart create mode 100644 example/lib/theme.dart create mode 100644 example/lib/widgets/controls.dart create mode 100644 example/lib/widgets/no_video.dart create mode 100644 example/lib/widgets/participant.dart create mode 100644 example/lib/widgets/participant_info.dart delete mode 100644 lib/src/_websocket_api.dart delete mode 100644 lib/src/_websocket_html.dart delete mode 100644 lib/src/_websocket_io.dart create mode 100644 lib/src/utils.dart delete mode 100644 lib/src/version.dart create mode 100644 lib/src/ws/interface.dart create mode 100644 lib/src/ws/platform/io.dart create mode 100644 lib/src/ws/platform/web.dart diff --git a/.gitignore b/.gitignore index 6d523c2..9824846 100644 --- a/.gitignore +++ b/.gitignore @@ -76,3 +76,4 @@ build/ # VS Code .vscode/ +*.code-workspace diff --git a/README.md b/README.md index 4196a81..a66e403 100644 --- a/README.md +++ b/README.md @@ -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 -... + ... NSCameraUsageDescription $(PRODUCT_NAME) uses your camera NSMicrophoneUsageDescription $(PRODUCT_NAME) uses your microphone - +``` + +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 + + ... + UIBackgroundModes + + audio + ``` ### Android diff --git a/analysis_options.yaml b/analysis_options.yaml index fd25a87..a63dcfc 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -37,3 +37,5 @@ linter: # Additional recommended rules # prefer_single_quotes: true + unnecessary_brace_in_string_interps: false + unawaited_futures: true diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index aa55c82..8c01024 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -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 diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index ff36834..0090881 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -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() diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml index 9d0f6de..85263f8 100644 --- a/example/android/app/src/debug/AndroidManifest.xml +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="io.livekit.example"> diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index fec9da2..e439f3f 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -1,51 +1,49 @@ - - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - - - - + + \ No newline at end of file diff --git a/example/android/app/src/main/kotlin/com/example/livekit_example/MainActivity.kt b/example/android/app/src/main/kotlin/com/example/livekit_example/MainActivity.kt index 943fd3d..8096b9c 100644 --- a/example/android/app/src/main/kotlin/com/example/livekit_example/MainActivity.kt +++ b/example/android/app/src/main/kotlin/com/example/livekit_example/MainActivity.kt @@ -1,4 +1,4 @@ -package com.example.livekit_example +package io.livekit.example import io.flutter.embedding.android.FlutterActivity diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml index 9d0f6de..85263f8 100644 --- a/example/android/app/src/profile/AndroidManifest.xml +++ b/example/android/app/src/profile/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="io.livekit.example"> diff --git a/example/android/build.gradle b/example/android/build.gradle index 9b6ed06..609cfbd 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -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" } } diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index bc6a58a..b8793d3 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/example/ios/Podfile b/example/ios/Podfile index 1e8c3c9..f99a34a 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -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 diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 73398cb..555d945 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -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 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index cf4b653..1a4d320 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -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"; diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index ea42fc4..626df2f 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -22,6 +22,14 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + NSCameraUsageDescription + $(PRODUCT_NAME) will use camera + NSMicrophoneUsageDescription + $(PRODUCT_NAME) will use microphone + UIBackgroundModes + + audio + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -41,9 +49,5 @@ UIViewControllerBasedStatusBarAppearance - NSCameraUsageDescription - $(PRODUCT_NAME) will use camera - NSMicrophoneUsageDescription - $(PRODUCT_NAME) will use microphone diff --git a/example/lib/exts.dart b/example/lib/exts.dart new file mode 100644 index 0000000..55308f3 --- /dev/null +++ b/example/lib/exts.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +extension LKExampleExt on BuildContext { + // + Future showErrorDialog(dynamic exception) => showDialog( + context: this, + builder: (ctx) => AlertDialog( + title: const Text('Error'), + content: Text(exception.toString()), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('OK'), + ) + ], + ), + ); +} diff --git a/example/lib/main.dart b/example/lib/main.dart index d59a61a..1b5a2aa 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -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: '', - 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 createState() => _PreConnectWidgetState(); -} - -class _PreConnectWidgetState extends State { - // - 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( - 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(), ); } diff --git a/example/lib/pages/connect.dart b/example/lib/pages/connect.dart new file mode 100644 index 0000000..04a3992 --- /dev/null +++ b/example/lib/pages/connect.dart @@ -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 createState() => _ConnectPageState(); +} + +class _ConnectPageState extends State { + // + 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 _readPrefs() async { + final prefs = await SharedPreferences.getInstance(); + _uriCtrl.text = prefs.getString(_storeKeyUri) ?? ''; + _tokenCtrl.text = prefs.getString(_storeKeyToken) ?? ''; + setState(() { + _simulcast = prefs.getBool(_storeKeySimulcast) ?? false; + }); + } + + Future _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 _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( + 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'), + ], + ), + ), + ), + ], + ), + ), + ), + ); +} diff --git a/example/lib/pages/room.dart b/example/lib/pages/room.dart new file mode 100644 index 0000000..78128c2 --- /dev/null +++ b/example/lib/pages/room.dart @@ -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 createState() { + return _RoomPageState(); + } +} + +class _RoomPageState extends State with RoomDelegate { + // BuildContext? _lastContext; + // + List 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 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), + ), + ], + ), + ), + ); +} diff --git a/example/lib/room.dart b/example/lib/room.dart deleted file mode 100644 index 913d649..0000000 --- a/example/lib/room.dart +++ /dev/null @@ -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 createState() { - return _RoomState(); - } -} - -class _RoomState extends State with RoomDelegate { - BuildContext? _lastContext; - List 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 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 = []; - 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 createState() { - return _VideoViewState(); - } -} - -class _VideoViewState extends State 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, - ); - } - } -} diff --git a/example/lib/src/controls.dart b/example/lib/src/controls.dart deleted file mode 100644 index 35beee9..0000000 --- a/example/lib/src/controls.dart +++ /dev/null @@ -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 createState() { - return _ControlsState(); - } -} - -class _ControlsState extends State { - 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 _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 = []; - - // 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, - ); - } -} diff --git a/example/lib/theme.dart b/example/lib/theme.dart new file mode 100644 index 0000000..fbb96df --- /dev/null +++ b/example/lib/theme.dart @@ -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(Colors.white), + // backgroundColor: MaterialStateProperty.all(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), + ), + ), + ); +} diff --git a/example/lib/widgets/controls.dart b/example/lib/widgets/controls.dart new file mode 100644 index 0000000..11cdc99 --- /dev/null +++ b/example/lib/widgets/controls.dart @@ -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 createState() => _ControlsWidgetState(); +} + +class _ControlsWidgetState extends State { + // + 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 _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), + ) + ], + ); + } +} diff --git a/example/lib/widgets/no_video.dart b/example/lib/widgets/no_video.dart new file mode 100644 index 0000000..0a4c371 --- /dev/null +++ b/example/lib/widgets/no_video.dart @@ -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, + ), + ), + ); +} diff --git a/example/lib/widgets/participant.dart b/example/lib/widgets/participant.dart new file mode 100644 index 0000000..8e49e60 --- /dev/null +++ b/example/lib/widgets/participant.dart @@ -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 createState() => _ParticipantWidgetState(); +} + +class _ParticipantWidgetState extends State 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, + ), + ), + ], + ), + ); +} diff --git a/example/lib/widgets/participant_info.dart b/example/lib/widgets/participant_info.dart new file mode 100644 index 0000000..d1354d3 --- /dev/null +++ b/example/lib/widgets/participant_info.dart @@ -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, + ), + ), + ], + ), + ); +} diff --git a/example/pubspec.lock b/example/pubspec.lock index 47b99da..3f0d7b6 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -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: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index a2c7171..04a4ed2 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -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: diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart index 90f4438..2034c17 100644 --- a/example/test/widget_test.dart +++ b/example/test/widget_test.dart @@ -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); diff --git a/lib/livekit_client.dart b/lib/livekit_client.dart index c679729..365ff00 100644 --- a/lib/livekit_client.dart +++ b/lib/livekit_client.dart @@ -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'; diff --git a/lib/src/_websocket_api.dart b/lib/src/_websocket_api.dart deleted file mode 100644 index ab79b0b..0000000 --- a/lib/src/_websocket_api.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:web_socket_channel/web_socket_channel.dart'; - -Future connectToWebSocket(Uri uri) { - throw UnsupportedError('no implementations found'); -} diff --git a/lib/src/_websocket_html.dart b/lib/src/_websocket_html.dart deleted file mode 100644 index bd84c64..0000000 --- a/lib/src/_websocket_html.dart +++ /dev/null @@ -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 connectToWebSocket(Uri uri) { - final ws = WebSocket(uri.toString()); - ws.binaryType = 'arraybuffer'; - final completer = Completer(); - ws.onOpen.first.then((_) { - completer.complete(HtmlWebSocketChannel(ws)); - }); - ws.onError.first.then((e) { - completer.completeError('could not connect'); - }); - return completer.future; -} diff --git a/lib/src/_websocket_io.dart b/lib/src/_websocket_io.dart deleted file mode 100644 index 67b2e5d..0000000 --- a/lib/src/_websocket_io.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'dart:io'; - -import 'package:web_socket_channel/io.dart'; -import 'package:web_socket_channel/web_socket_channel.dart'; - -Future connectToWebSocket(Uri uri) async { - try { - // ignore: close_sinks - final ws = await WebSocket.connect(uri.toString()); - return IOWebSocketChannel(ws); - } catch (e) { - return Future.error(e); - } -} diff --git a/lib/src/errors.dart b/lib/src/errors.dart index 9e2dd8a..6f3f7d7 100644 --- a/lib/src/errors.dart +++ b/lib/src/errors.dart @@ -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); } diff --git a/lib/src/extensions.dart b/lib/src/extensions.dart index e2fffda..365fbfa 100644 --- a/lib/src/extensions.dart +++ b/lib/src/extensions.dart @@ -1,39 +1,3 @@ -class RTCConfiguration { - int? iceCandidatePoolSize; - List? iceServers; - String? iceTransportPolicy; - - Map toMap() { - final iceServersMap = >[]; - for (final element in (iceServers ?? [])) { - iceServersMap.add(element.toMap()); - } - return { - // only supports unified plan - 'sdpSemantics': 'unified-plan', - if (iceCandidatePoolSize != null) 'iceCandidatePoolSize': iceCandidatePoolSize, - 'iceServers': iceServersMap, - if (iceTransportPolicy != null) 'iceTransportPolicy': iceTransportPolicy, - }; - } -} - -class RTCIceServer { - List urls; - String? username; - String? credential; - - RTCIceServer({required this.urls, this.username, this.credential}); - - Map toMap() { - return { - '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? iceServers; + RTCIceTransportPolicy? iceTransportPolicy; + + Map toMap() { + final iceServersMap = >[ + if (iceServers != null) + for (final element in iceServers!) element.toMap() + ]; + + return { + // 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 urls; + String? username; + String? credential; + + RTCIceServer({ + required this.urls, + this.username, + this.credential, + }); + + Map toMap() => { + 'urls': urls, + if (username != null) 'username': username, + if (credential != null) 'credential': credential, + }; +} diff --git a/lib/src/livekit.dart b/lib/src/livekit.dart index f2193d0..57bc64e 100644 --- a/lib/src/livekit.dart +++ b/lib/src/livekit.dart @@ -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 connect(String url, String token, [JoinOptions? options]) { + static Future connect( + String url, + String token, { + ConnectOptions? options, + }) { final room = Room(); - return room.connect(url, token, options); + return room.connect( + url, + token, + options: options, + ); } } diff --git a/lib/src/options.dart b/lib/src/options.dart index ef80ccc..7580288 100644 --- a/lib/src/options.dart +++ b/lib/src/options.dart @@ -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})'; } diff --git a/lib/src/participant/local_participant.dart b/lib/src/participant/local_participant.dart index f44167d..cd9c55f 100644 --- a/lib/src/participant/local_participant.dart +++ b/lib/src/participant/local_participant.dart @@ -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,97 +34,134 @@ class LocalParticipant extends Participant { /// publish an audio track to the room Future 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); - 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, - init: transceiverInit, - ); + // try { + final trackInfo = await _engine.addTrack( + cid: track.getCid(), + name: track.name, + kind: track.kind, + ); - final pub = LocalTrackPublication(trackInfo, track, this); - addTrackPublication(pub); - notifyListeners(); + 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.mediaStreamTrack, + init: transceiverInit, + ); - return pub; - } catch (e) { - return Future.error(e); - } + final pub = LocalTrackPublication(trackInfo, track, this); + addTrackPublication(pub); + notifyListeners(); + + return pub; } /// Publish a video track to the room - Future publishVideoTrack(LocalVideoTrack track) async { - if (videoTracks.values.any((element) => element.track?.mediaTrack.id == track.mediaTrack.id)) { - return Future.error(TrackPublishError('track already exists')); + Future publishVideoTrack( + LocalVideoTrack track, { + TrackPublishOptions? options, + }) async { + if (videoTracks.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); - final transceiverInit = RTCRtpTransceiverInit( - direction: TransceiverDirection.SendOnly, - ); - // TODO: video encodings and simulcasts - // addTransceiver cannot pass in a kind parameter due to a bug in flutter-webrtc (web) - track.transceiver = await _engine.publisher?.pc.addTransceiver( - track: track.mediaTrack, - init: transceiverInit, - ); + // Use default options from `ConnectOptions` if options is null + options = options ?? defaultPublishOptions; - final pub = LocalTrackPublication(trackInfo, track, this); - addTrackPublication(pub); - notifyListeners(); + final trackInfo = await _engine.addTrack( + cid: track.getCid(), + name: track.name, + kind: track.kind, + ); - return pub; - } catch (e) { - return Future.error(e); + // + // 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 { + // try to use getSettings for more accurate resolution + final settings = track.mediaStreamTrack.getSettings(); + width = settings['width'] as int?; + height = settings['height'] as int?; + // TODO: Get actual video dimensions to compute more accurately + // mediaTrack.getConsstraints() is not implemented for mobile + } catch (_) { + logger.warning('Failed to call `mediaStreamTrack.getSettings()`'); + } } + + logger.fine('Compute encodings with resolution: ${width}x${height}, options: ${options}'); + + final encodings = Utils.computeVideoEncodings( + width: width, + height: height, + options: options, + ); + + logger.fine('Using encodings: ${encodings?.map((e) => e.toMap())}'); + + final transceiverInit = RTCRtpTransceiverInit( + direction: TransceiverDirection.SendOnly, + sendEncodings: encodings, + streams: [track.mediaStream], + ); + + // + // addTransceiver cannot pass in a kind parameter due to a bug in flutter-webrtc (web) + // + track.transceiver = await _engine.publisher?.pc.addTransceiver( + track: track.mediaStreamTrack, + init: transceiverInit, + ); + + final pub = LocalTrackPublication(trackInfo, track, this); + addTrackPublication(pub); + notifyListeners(); + + return pub; } /// Unpublish a track that's already published - void unpublishTrack(Track track) { + Future 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 data, DataPacket_Kind reliability, {List? destinationSids}) { + void publishData( + List data, + lk_models.DataPacket_Kind reliability, { + List? 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); } } diff --git a/lib/src/participant/participant.dart b/lib/src/participant/participant.dart index 4804dbb..f1e4b0d 100644 --- a/lib/src/participant/participant.dart +++ b/lib/src/participant/participant.dart @@ -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 audioTracks = {}; - Map videoTracks = {}; - /// map of track sid => published track Map 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 get subscribedTracks { - List result = []; - for (final track in tracks.values) { - if (track.subscribed) { - result.add(track); - } - } - return result; - } + List 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 get videoTracks => + tracks.values.where((e) => e.kind == lk_models.TrackType.VIDEO).toList(); + + List get audioTracks => + tracks.values.where((e) => e.kind == lk_models.TrackType.AUDIO).toList(); +} diff --git a/lib/src/participant/remote_participant.dart b/lib/src/participant/remote_participant.dart index f09a040..caa9719 100644 --- a/lib/src/participant/remote_participant.dart +++ b/lib/src/participant/remote_participant.dart @@ -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 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.delayed(const Duration(milliseconds: 100), () { return getTrackPublication(sid); }); - if (pub != null) { - return pub; - } + + if (pub != null) return pub; } } } diff --git a/lib/src/proto/livekit_models.pb.dart b/lib/src/proto/livekit_models.pb.dart index 412aad6..09f934c 100644 --- a/lib/src/proto/livekit_models.pb.dart +++ b/lib/src/proto/livekit_models.pb.dart @@ -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( + 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( + 2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'user', + subBuilder: UserPacket.create) + ..aOM( + 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 createRepeated() => $pb.PbList(); + static DataPacket create() => DataPacket._(); + DataPacket createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') - static DataMessage getDefault() => - _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static DataMessage? _defaultInstance; + static DataPacket getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(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( - 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 createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static RecordingInput getDefault() => - _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(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 createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static RecordingTemplate getDefault() => - _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(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( - 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 createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static RecordingOutput getDefault() => - _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(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( + 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? 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 createRepeated() => $pb.PbList(); + static ActiveSpeakerUpdate create() => ActiveSpeakerUpdate._(); + ActiveSpeakerUpdate createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') - static RecordingS3Output getDefault() => - _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static RecordingS3Output? _defaultInstance; + static ActiveSpeakerUpdate getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static ActiveSpeakerUpdate? _defaultInstance; @$pb.TagNumber(1) - $core.String get bucket => $_getSZ(0); + $core.List 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 createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SpeakerInfo getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(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 createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static UserPacket getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(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); } diff --git a/lib/src/proto/livekit_models.pbenum.dart b/lib/src/proto/livekit_models.pbenum.dart index 11b9b40..e4f8aea 100644 --- a/lib/src/proto/livekit_models.pbenum.dart +++ b/lib/src/proto/livekit_models.pbenum.dart @@ -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 values = [ + 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); +} diff --git a/lib/src/proto/livekit_models.pbjson.dart b/lib/src/proto/livekit_models.pbjson.dart index 78b095a..c03167f 100644 --- a/lib/src/proto/livekit_models.pbjson.dart +++ b/lib/src/proto/livekit_models.pbjson.dart @@ -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='); diff --git a/lib/src/proto/livekit_models.pbserver.dart b/lib/src/proto/livekit_models.pbserver.dart index b7ae4ec..c28eba2 100644 --- a/lib/src/proto/livekit_models.pbserver.dart +++ b/lib/src/proto/livekit_models.pbserver.dart @@ -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'; diff --git a/lib/src/proto/livekit_rtc.pb.dart b/lib/src/proto/livekit_rtc.pb.dart index d3e52e7..06f7f67 100644 --- a/lib/src/proto/livekit_rtc.pb.dart +++ b/lib/src/proto/livekit_rtc.pb.dart @@ -3,7 +3,7 @@ // source: livekit_rtc.proto // // @dart = 2.12 -// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields +// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_this,unused_import,unused_shown_name import 'dart:core' as $core; @@ -48,22 +48,33 @@ class SignalRequest extends $pb.GeneratedMessage { const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'livekit'), createEmptyInstance: create) ..oo(0, [1, 2, 3, 4, 5, 6, 7, 8, 9]) - ..aOM(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'offer', + ..aOM( + 1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'offer', subBuilder: SessionDescription.create) ..aOM( 2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'answer', subBuilder: SessionDescription.create) - ..aOM(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'trickle', + ..aOM( + 3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'trickle', subBuilder: TrickleRequest.create) ..aOM( 4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'addTrack', subBuilder: AddTrackRequest.create) - ..aOM(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'mute', + ..aOM( + 5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'mute', subBuilder: MuteTrackRequest.create) - ..aOM(6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'subscription', subBuilder: UpdateSubscription.create) - ..aOM(7, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'trackSetting', subBuilder: UpdateTrackSettings.create) - ..aOM(8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'leave', subBuilder: LeaveRequest.create) - ..aOM(9, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'simulcast', subBuilder: SetSimulcastLayers.create) + ..aOM( + 6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'subscription', + subBuilder: UpdateSubscription.create) + ..aOM( + 7, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'trackSetting', + subBuilder: UpdateTrackSettings.create) + ..aOM( + 8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'leave', + subBuilder: LeaveRequest.create) + ..aOM( + 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(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'join', + ..oo(0, [1, 2, 3, 4, 5, 6, 7, 8, 9]) + ..aOM( + 1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'join', subBuilder: JoinResponse.create) ..aOM( 2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'answer', subBuilder: SessionDescription.create) - ..aOM(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'offer', + ..aOM( + 3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'offer', subBuilder: SessionDescription.create) - ..aOM(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'trickle', + ..aOM( + 4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'trickle', subBuilder: TrickleRequest.create) ..aOM( 5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'update', subBuilder: ParticipantUpdate.create) - ..aOM(6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'trackPublished', subBuilder: TrackPublishedResponse.create) - ..aOM(7, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'speaker', subBuilder: ActiveSpeakerUpdate.create) - ..aOM(8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'leave', subBuilder: LeaveRequest.create) + ..aOM( + 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( + 8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'leave', + subBuilder: LeaveRequest.create) + ..aOM( + 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( - 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( - 1, - const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'speakers', - $pb.PbFieldType.PM, - subBuilder: SpeakerInfo.create) - ..hasRequiredFields = false; - - ActiveSpeakerUpdate._() : super(); - factory ActiveSpeakerUpdate({ - $core.Iterable? 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 createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static ActiveSpeakerUpdate getDefault() => - _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static ActiveSpeakerUpdate? _defaultInstance; - - @$pb.TagNumber(1) - $core.List 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 createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static SpeakerInfo getDefault() => - _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(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( - 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( - 2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'user', - subBuilder: UserPacket.create) - ..aOM( - 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 createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static DataPacket getDefault() => - _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(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 createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static UserPacket getDefault() => - _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(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); -} diff --git a/lib/src/proto/livekit_rtc.pbenum.dart b/lib/src/proto/livekit_rtc.pbenum.dart index b2096ff..b95f930 100644 --- a/lib/src/proto/livekit_rtc.pbenum.dart +++ b/lib/src/proto/livekit_rtc.pbenum.dart @@ -3,7 +3,7 @@ // source: livekit_rtc.proto // // @dart = 2.12 -// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields +// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_this,unused_import,unused_shown_name // 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 values = [ - 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); -} diff --git a/lib/src/proto/livekit_rtc.pbjson.dart b/lib/src/proto/livekit_rtc.pbjson.dart index 9c6a18a..01e77d4 100644 --- a/lib/src/proto/livekit_rtc.pbjson.dart +++ b/lib/src/proto/livekit_rtc.pbjson.dart @@ -3,7 +3,7 @@ // source: livekit_rtc.proto // // @dart = 2.12 -// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,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='); diff --git a/lib/src/proto/livekit_rtc.pbserver.dart b/lib/src/proto/livekit_rtc.pbserver.dart index 7413d13..904b71c 100644 --- a/lib/src/proto/livekit_rtc.pbserver.dart +++ b/lib/src/proto/livekit_rtc.pbserver.dart @@ -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'; diff --git a/lib/src/room.dart b/lib/src/room.dart index 38fcfdf..b86b1a5 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -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 connect(String url, String token, [JoinOptions? opts]) async { + Future connect( + String url, + String token, { + ConnectOptions? options, + }) async { final completer = Completer(); _connectCompleter = completer; - final joinResponse = await _engine.join(url, token, opts); + final joinResponse = await _engine.join( + url, + token, + options: options, + ); + logger.fine('connected to LiveKit server, version: ${joinResponse.serverVersion}'); 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 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 _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.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 updates) { + void _handleParticipantUpdate(List 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 speakers) { + void _handleSpeakerUpdate(List speakers) { final seenSids = {}; List 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); diff --git a/lib/src/rtc_engine.dart b/lib/src/rtc_engine.dart index 4e0c7f1..3ce461c 100644 --- a/lib/src/rtc_engine.dart +++ b/lib/src/rtc_engine.dart @@ -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 participants); -typedef ActiveSpeakerChangedCallback = void Function(List speakers); -typedef DataPacketCallback = void Function(UserPacket packet, DataPacket_Kind kind); + MediaStreamTrack track, + MediaStream? stream, + RTCRtpReceiver? receiver, +); +typedef ParticipantUpdateCallback = void Function(List participants); +typedef ActiveSpeakerChangedCallback = void Function(List 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> pendingTrackResolvers = {}; + Map> pendingTrackResolvers = {}; int reconnectAttempts = 0; // to complete join request - Completer? joinCompleter; + Completer? 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 join(String url, String token, JoinOptions? opts) async { + Future join( + String url, + String token, { + ConnectOptions? options, + }) async { this.url = url; this.token = token; - final completer = Completer(); + final completer = Completer(); 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 close() async { isClosed = true; - if (publisher != null) { - final senders = await publisher?.pc.getSenders(); - for (final element in (senders ?? [])) { - await publisher?.pc.removeTrack(element); - } + // PCTransport is responsible for disposing RTCPeerConnection + await publisher?.dispose(); + publisher = null; + + await subscriber?.dispose(); + subscriber = null; - publisher?.pc.close(); - publisher = null; - } - if (subscriber != null) { - subscriber?.pc.close(); - subscriber = null; - } client.close(); } - Future addTrack( - {required String cid, - required String name, - required TrackType kind, - TrackDimension? dimension}) async { + Future addTrack({ + required String cid, + required String name, + 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(); + final completer = Completer(); pendingTrackResolvers[cid] = completer; client.sendAddTrack(cid: cid, name: name, type: kind, dimension: dimension); @@ -122,9 +124,7 @@ class RTCEngine with SignalClientDelegate { Future 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 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 _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 _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 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 onClose([String? reason]) async { + await _handleDisconnect('signal'); } @override - void onOffer(RTCSessionDescription sd) async { + Future 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 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 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 updates) { - onParticipantUpdateCallback?.call(updates); + Future onParticipantUpdate(List updates) async { + onParticipantUpdated?.call(updates); } @override - void onLocalTrackPublished(TrackPublishedResponse response) { + Future onLocalTrackPublished(lk_rtc.TrackPublishedResponse response) async { final completer = pendingTrackResolvers.remove(response.cid); completer?.complete(Future.value(response.track)); } @override - void onActiveSpeakersChanged(List speakers) { - onActiveSpeakerchangedCallback?.call(speakers); + Future onActiveSpeakersChanged(List speakers) async { + onActiveSpeakerUpdated?.call(speakers); } @override - void onLeave(LeaveRequest req) { - close(); + Future onLeave(lk_rtc.LeaveRequest req) async { + await close(); onDisconnected?.call(); } + + @override + Future onMuteTrack(lk_rtc.MuteTrackRequest req) async { + onRemoteMute?.call(req.sid, req.muted); + } } diff --git a/lib/src/signal_client.dart b/lib/src/signal_client.dart index b4e07f1..34ccd42 100644 --- a/lib/src/signal_client.dart +++ b/lib/src/signal_client.dart @@ -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 onConnected(lk_rtc.JoinResponse response); // websocket has closed - void onClose([String? reason]); + Future onClose([String? reason]); // when a server offer is received - void onOffer(RTCSessionDescription sd); + Future onOffer(RTCSessionDescription sd); // when an answer from server is received - void onAnswer(RTCSessionDescription sd); + Future onAnswer(RTCSessionDescription sd); // when server has a new ICE candidate - void onTrickle(RTCIceCandidate candidate, SignalTarget target); + Future onTrickle(RTCIceCandidate candidate, lk_rtc.SignalTarget target); // participant has changed - void onParticipantUpdate(List updates); + Future onParticipantUpdate(List updates); // when a track has been added successfully - void onLocalTrackPublished(TrackPublishedResponse response); + Future onLocalTrackPublished(lk_rtc.TrackPublishedResponse response); // active speaker has changed - void onActiveSpeakersChanged(List speakers); + Future onActiveSpeakersChanged(List speakers); // when server sends this client a leave message - void onLeave(LeaveRequest req); + Future onLeave(lk_rtc.LeaveRequest req); + // explicit mute track + Future 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 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: { + 'access_token': token, + if (options != null) 'auto_subscribe': options.autoSubscribe ? '1' : '0', + if (reconnect) 'reconnect': '1', + 'protocol': protocolVersion.toString(), + }, + ); + } + + Future 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(); - 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 reconnect(String url, String token) async { + Future 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( - offer: fromRTCSessionDescription(offer), - )); - } + void sendOffer(RTCSessionDescription offer) => _sendRequest(lk_rtc.SignalRequest( + offer: fromRTCSessionDescription(offer), + )); - void sendAnswer(RTCSessionDescription answer) { - _sendRequest(SignalRequest( - answer: fromRTCSessionDescription(answer), - )); - } + void sendAnswer(RTCSessionDescription answer) => _sendRequest(lk_rtc.SignalRequest( + answer: fromRTCSessionDescription(answer), + )); - void sendIceCandidate(RTCIceCandidate candidate, SignalTarget target) { - _sendRequest(SignalRequest( - trickle: TrickleRequest( - candidateInit: fromRTCIceCandidate(candidate), - target: target, - ))); - } + 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( - sid: trackSid, - muted: muted, - ), - )); - } + void sendMuteTrack(String trackSid, bool muted) => _sendRequest(lk_rtc.SignalRequest( + mute: lk_rtc.MuteTrackRequest( + sid: trackSid, + muted: muted, + ), + )); - void sendAddTrack( - {required String cid, - required String name, - required TrackType type, - TrackDimension? dimension}) { - final req = AddTrackRequest( + void sendAddTrack({ + required String cid, + required String name, + 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( - trackSetting: settings, - )); - } + void sendUpdateTrackSettings(lk_rtc.UpdateTrackSettings settings) => + _sendRequest(lk_rtc.SignalRequest( + trackSetting: settings, + )); - void sendUpdateSubscription(UpdateSubscription subscription) { - _sendRequest(SignalRequest( - subscription: subscription, - )); - } + void sendUpdateSubscription(lk_rtc.UpdateSubscription subscription) => + _sendRequest(lk_rtc.SignalRequest( + subscription: subscription, + )); - void sendSetSimulcastLayers(String trackSid, List layers) { - _sendRequest(SignalRequest( - simulcast: SetSimulcastLayers( - trackSid: trackSid, - layers: layers, - ))); - } + void sendSetSimulcastLayers(String trackSid, List layers) => + _sendRequest(lk_rtc.SignalRequest( + simulcast: lk_rtc.SetSimulcastLayers( + trackSid: trackSid, + layers: layers, + ), + )); - void sendLeave() { - _sendRequest(SignalRequest( - leave: LeaveRequest(), - )); - } + void sendLeave() => _sendRequest(lk_rtc.SignalRequest( + leave: lk_rtc.LeaveRequest(), + )); - void _sendRequest(SignalRequest req) { + 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) { - return; - } - final msg = SignalResponse.fromBuffer(message); - switch (msg.whichMessage()) { - case SignalResponse_Message.join: - if (!_connected) { - _connected = true; - delegate?.onConnected(msg.join); - } - break; - case SignalResponse_Message.answer: - delegate?.onAnswer(toRTCSessionDescription(msg.answer)); - break; - case SignalResponse_Message.offer: - delegate?.onOffer(toRTCSessionDescription(msg.offer)); - break; - case SignalResponse_Message.trickle: - delegate?.onTrickle(toRTCIceCandidate(msg.trickle.candidateInit), msg.trickle.target); - break; - case SignalResponse_Message.update: - delegate?.onParticipantUpdate(msg.update.participants); - break; - case SignalResponse_Message.trackPublished: - delegate?.onLocalTrackPublished(msg.trackPublished); - break; - case SignalResponse_Message.speaker: - delegate?.onActiveSpeakersChanged(msg.speaker.speakers); - break; - case SignalResponse_Message.leave: - delegate?.onLeave(msg.leave); - break; - default: - log('unsupported message: ' + json.encode(msg)); - } + Future _onSocketData(dynamic message) async { + if (message is! List) 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 lk_rtc.SignalResponse_Message.join: + if (!_connected) { + _connected = true; + await delegate?.onConnected(msg.join); + } + break; + case lk_rtc.SignalResponse_Message.answer: + await delegate?.onAnswer(toRTCSessionDescription(msg.answer)); + break; + case lk_rtc.SignalResponse_Message.offer: + await delegate?.onOffer(toRTCSessionDescription(msg.offer)); + break; + case lk_rtc.SignalResponse_Message.trickle: + await delegate?.onTrickle( + toRTCIceCandidate(msg.trickle.candidateInit), + msg.trickle.target, + ); + break; + case lk_rtc.SignalResponse_Message.update: + await delegate?.onParticipantUpdate(msg.update.participants); + break; + case lk_rtc.SignalResponse_Message.trackPublished: + await delegate?.onLocalTrackPublished(msg.trackPublished); + break; + case lk_rtc.SignalResponse_Message.speaker: + await delegate?.onActiveSpeakersChanged(msg.speaker.speakers); + break; + case lk_rtc.SignalResponse_Message.leave: + await delegate?.onLeave(msg.leave); + break; + case lk_rtc.SignalResponse_Message.mute: + await delegate?.onMuteTrack(msg.mute); + break; + default: + log('unsupported message: ' + json.encode(msg)); + } + }); } - void _handleError(Object error) { + void _handleError(dynamic error) { logger.warning('received websocket error $error'); } - 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) { diff --git a/lib/src/track/audio_track.dart b/lib/src/track/audio_track.dart index 4f20157..a196eda 100644 --- a/lib/src/track/audio_track.dart +++ b/lib/src/track/audio_track.dart @@ -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 stop() async { + await mediaStream?.dispose(); mediaStream = null; audio.stopAudio(getCid()); - super.stop(); + await super.stop(); } } diff --git a/lib/src/track/local_audio_track.dart b/lib/src/track/local_audio_track.dart index d546cb9..e926302 100644 --- a/lib/src/track/local_audio_track.dart +++ b/lib/src/track/local_audio_track.dart @@ -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 createTrack([LocalAudioTrackOptions? options]) async { - try { - final stream = await navigator.mediaDevices.getUserMedia({ - 'audio': true, - 'video': false, - }); + static Future create([LocalAudioTrackOptions? options]) async { + // try { + final stream = await navigator.mediaDevices.getUserMedia({ + '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); - } + return LocalAudioTrack('', stream.getAudioTracks().first, stream); } } diff --git a/lib/src/track/local_track_publication.dart b/lib/src/track/local_track_publication.dart index 8ebf436..6945b69 100644 --- a/lib/src/track/local_track_publication.dart +++ b/lib/src/track/local_track_publication.dart @@ -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) { diff --git a/lib/src/track/local_video_track.dart b/lib/src/track/local_video_track.dart index 22fb718..8b45857 100644 --- a/lib/src/track/local_video_track.dart +++ b/lib/src/track/local_video_track.dart @@ -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 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 restartTrack([LocalVideoTrackOptions? options]) async { - if (sender == null) { - return Future.error(TrackCreateError('could not restart track')); + Future 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 track = stream.getVideoTracks().first; - mediaStream = stream; - await mediaTrack.stop(); - mediaTrack = track; - await sender?.replaceTrack(track); - } catch (e) { - return Future.error(e); - } + final stream = await _createStream(currentOptions); + final track = stream.getVideoTracks().first; + setMediaStream(stream); + await mediaStreamTrack.stop(); + mediaStreamTrack = track; + await sender?.replaceTrack(track); } - static Future _createCameraStream(LocalVideoTrackOptions? options) async { - options ??= LocalVideoTrackOptions(params: VideoPresets.qhd); + /// Creates a LocalVideoTrack from camera input. + static Future 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({ - 'audio': false, - 'video': options.mediaConstraints, - }); + static Future createScreenTrack([ + ScreenTrackOptions? options, + ]) async { + options ??= const ScreenTrackOptions(); + final stream = await _createStream(options); + return LocalVideoTrack._( + Track.screenShareName, + stream.getVideoTracks().first, + stream, + options, + ); + } - if (stream.getVideoTracks().isEmpty) { - return Future.error(TrackCreateError()); - } + static Future _createStream( + LocalVideoTrackOptions options, + ) async { + final constraints = { + 'audio': false, + 'video': options.toMediaConstraintsMap(), + }; - return stream; - } catch (e) { - return Future.error(e); + 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; + } +} + +// +// Convenience extensions +// +extension LKLocalVideoTrackExt on LocalVideoTrack { + // Calls restartTrack under the hood + Future setCameraPosition(CameraPosition position) async { + final options = currentOptions; + if (options is! CameraTrackOptions) { + logger.warning('Not a camera track'); + return; + } + + await restartTrack( + options.copyWith(cameraPosition: position), + ); } } diff --git a/lib/src/track/options.dart b/lib/src/track/options.dart index edb6902..4a8b907 100644 --- a/lib/src/track/options.dart +++ b/lib/src/track/options.dart @@ -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 get mediaConstraints { - return { - '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 get mediaConstraints { - return { - '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 all = [ - qvga, - vga, - qhd, - hd, - fhd, + const CameraTrackOptions({ + this.cameraPosition = CameraPosition.front, + VideoParameters params = VideoParameters.presetQHD169, + }) : super(params: params); + + @override + Map toMediaConstraintsMap() => { + ...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 toMediaConstraintsMap() => { + '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 presets169 = [ + presetQVGA169, + presetVGA169, + presetQHD169, + presetHD169, + presetFHD169, ]; + + static final List 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 toMediaConstraintsMap() => { + 'maxWidth': width, + 'maxHeight': height, + 'maxFrameRate': encoding.maxFramerate, + }; } /// Options when creating an LocalAudioTrack. Placeholder for now. -class LocalAudioTrackOptions {} +class LocalAudioTrackOptions { + const LocalAudioTrackOptions(); +} diff --git a/lib/src/track/remote_track_publication.dart b/lib/src/track/remote_track_publication.dart index 0f5cbf3..5b0e3f0 100644 --- a/lib/src/track/remote_track_publication.dart +++ b/lib/src/track/remote_track_publication.dart @@ -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); diff --git a/lib/src/track/track.dart b/lib/src/track/track.dart index a25850e..15ec331 100644 --- a/lib/src/track/track.dart +++ b/lib/src/track/track.dart @@ -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 stop() async { + await mediaStreamTrack.stop(); } } diff --git a/lib/src/track/track_publication.dart b/lib/src/track/track_publication.dart index e913c74..775d058 100644 --- a/lib/src/track/track_publication.dart +++ b/lib/src/track/track_publication.dart @@ -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); } } diff --git a/lib/src/track/video_track.dart b/lib/src/track/video_track.dart index 78caebb..960bfa9 100644 --- a/lib/src/track/video_track.dart +++ b/lib/src/track/video_track.dart @@ -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 stop() async { + await super.stop(); + await _mediaStream.dispose(); + // _mediaStream = null; } } diff --git a/lib/src/transport.dart b/lib/src/transport.dart index 623a918..70c4b82 100644 --- a/lib/src/transport.dart +++ b/lib/src/transport.dart @@ -1,36 +1,68 @@ import 'package:flutter_webrtc/flutter_webrtc.dart'; +import 'logger.dart'; + /// a wrapper around PeerConnection class PCTransport { - RTCPeerConnection pc; - List pendingCandidates = []; + final RTCPeerConnection pc; + final List _pendingCandidates = []; bool restartingIce = false; PCTransport(this.pc); + Future dispose() async { + // Ensure callbacks won't fire any more + pc.onRenegotiationNeeded = null; + pc.onIceCandidate = null; + pc.onIceConnectionState = null; + pc.onTrack = null; + + List 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 setRemoteDescription(RTCSessionDescription sd) async { await pc.setRemoteDescription(sd); - await Future.forEach(pendingCandidates, (candidate) async { + await Future.forEach(_pendingCandidates, (candidate) async { await pc.addCandidate(candidate); }); - pendingCandidates.clear(); + _pendingCandidates.clear(); restartingIce = false; } Future 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 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(); } } diff --git a/lib/src/utils.dart b/lib/src/utils.dart new file mode 100644 index 0000000..ee9798d --- /dev/null +++ b/lib/src/utils.dart @@ -0,0 +1,87 @@ +// +// +// + +import 'package:flutter_webrtc/flutter_webrtc.dart'; + +import 'options.dart'; +import 'track/options.dart'; + +class Utils { + static List _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 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? 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, + ), + ]; + } +} diff --git a/lib/src/version.dart b/lib/src/version.dart deleted file mode 100644 index 06677c3..0000000 --- a/lib/src/version.dart +++ /dev/null @@ -1,2 +0,0 @@ -const version = '0.4.0'; -const protocolVersion = 2; diff --git a/lib/src/widget/video_track_renderer.dart b/lib/src/widget/video_track_renderer.dart index 03c7f32..08717b2 100644 --- a/lib/src/widget/video_track_renderer.dart +++ b/lib/src/widget/video_track_renderer.dart @@ -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 createState() { - return _VideoTrackRendererState(); - } + State createState() => _VideoTrackRendererState(); } class _VideoTrackRendererState extends State { @@ -63,6 +65,7 @@ class _VideoTrackRendererState extends State { _renderer, mirror: isLocal, filterQuality: FilterQuality.medium, + objectFit: widget.fit, ); } } diff --git a/lib/src/ws/interface.dart b/lib/src/ws/interface.dart new file mode 100644 index 0000000..0041666 --- /dev/null +++ b/lib/src/ws/interface.dart @@ -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 data); + void dispose(); + + static Future connect( + Uri uri, [ + LKWebSocketOptions? options, + ]) => + lkWebSocketConnect(uri, options); +} diff --git a/lib/src/ws/platform/io.dart b/lib/src/ws/platform/io.dart new file mode 100644 index 0000000..c951541 --- /dev/null +++ b/lib/src/ws/platform/io.dart @@ -0,0 +1,53 @@ +import 'dart:async'; +import 'dart:io' as io; + +import 'package:livekit_client/src/logger.dart'; + +import '../interface.dart'; + +Future 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 data) => _ws.add(data); + + static Future 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(); + } + } +} diff --git a/lib/src/ws/platform/web.dart b/lib/src/ws/platform/web.dart new file mode 100644 index 0000000..d4f7e8b --- /dev/null +++ b/lib/src/ws/platform/web.dart @@ -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 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 data) => _ws.send(data); + + @override + void dispose() { + options?.onDispose?.call(); + _messageSubscription.cancel(); + _closeSubscription.cancel(); + _ws.close(); + } + + static Future connect( + Uri uri, [ + LKWebSocketOptions? options, + ]) async { + final completer = Completer(); + final ws = html.WebSocket(uri.toString()); + ws.onOpen.listen((_) => completer.complete(LKWebSocketWeb._(ws, options))); + ws.onError.listen((_) => completer.completeError(LKWebSocketError.connect())); + return completer.future; + } +} diff --git a/pubspec.lock b/pubspec.lock index 78775cd..9f20fb9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index b06a1b6..f8200dd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,18 +8,36 @@ 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 + tuple: ^2.0.0 + + uuid: ^3.0.4 + synchronized: ^3.0.0 + + # + # protobuf: + # git: + # url: https://github.com/google/protobuf.dart.git + # ref: master + # path: protobuf/ + + # + # WebSocketChannel has design flaws + # https://github.com/dart-lang/web_socket_channel/issues/25 + # + # web_socket_channel: ^2.1.0 + dev_dependencies: flutter_test: sdk: flutter