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 =