Better audio management (#10)

* add audio_session package

* `AudioManager` initial design

* try to integrate audio manager

* configure audio session

* `DisposeAware`

disposing if already published causes exception.

* organize

* guard flutter_webrtc calls

* websocket async fix

* use `ConnectionState` instead of `_isClosed` and `isReconnecting`

* change create audio track defaults

* update protos

* protocol 3 speaker updates

* fix exception

* emit `RoomDisconnectedEvent` only once

* fix exception

* keep track of local / remote audio tracks

* explicit types

* manage track state

* re-structure audio management

* change defaults

* unpublish all

* use experimental build

* change defaults

* configuring is optional

* call native `RTCAudioSession.setConfiguration`

* defaults adjustment

* iOS only for now

* use lib 92.4515.07

* clean up

* revert audio options for now

* remove pod source

* rename apple related audio

* `createListener` method

* organize native audio

* `SpeakingChangedEvent` only on `Participant`

* refactoring

* fix ios compile

* fix configure audio only for iOS logic

* minor fix & clean up

* change dispose logic

* format protos

* fix unpublishTrack

* `createListener` into a mixin

* simplify

* use flutter_webrtc master

* unpublish for example

* update ios icon

* android icon

* web icon

* favicon
This commit is contained in:
Hiroshi Horie
2021-10-01 02:47:18 +09:00
committed by GitHub
parent ec17907cdc
commit 001f688729
101 changed files with 1658 additions and 681 deletions
@@ -10,7 +10,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<application <application
android:label="LiveKit Example" android:label="LiveKit Example"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/livekit_ic_launcher">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 867 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

+11 -4
View File
@@ -3,17 +3,21 @@ PODS:
- flutter_webrtc (0.2.2): - flutter_webrtc (0.2.2):
- Flutter - Flutter
- Libyuv (= 1703) - Libyuv (= 1703)
- WebRTC-SDK (= 92.4515.05) - WebRTC-SDK (= 92.4515.07)
- Libyuv (1703) - Libyuv (1703)
- livekit_client (0.0.1):
- Flutter
- WebRTC-SDK (= 92.4515.07)
- path_provider (0.0.1): - path_provider (0.0.1):
- Flutter - Flutter
- shared_preferences (0.0.1): - shared_preferences (0.0.1):
- Flutter - Flutter
- WebRTC-SDK (92.4515.05) - WebRTC-SDK (92.4515.07)
DEPENDENCIES: DEPENDENCIES:
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`) - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
- livekit_client (from `.symlinks/plugins/livekit_client/ios`)
- path_provider (from `.symlinks/plugins/path_provider/ios`) - path_provider (from `.symlinks/plugins/path_provider/ios`)
- shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`)
@@ -27,6 +31,8 @@ EXTERNAL SOURCES:
:path: Flutter :path: Flutter
flutter_webrtc: flutter_webrtc:
:path: ".symlinks/plugins/flutter_webrtc/ios" :path: ".symlinks/plugins/flutter_webrtc/ios"
livekit_client:
:path: ".symlinks/plugins/livekit_client/ios"
path_provider: path_provider:
:path: ".symlinks/plugins/path_provider/ios" :path: ".symlinks/plugins/path_provider/ios"
shared_preferences: shared_preferences:
@@ -34,11 +40,12 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS: SPEC CHECKSUMS:
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
flutter_webrtc: c0cb88c7cbd057e6e667ab1560e10c74e2ceb65b flutter_webrtc: 6b71a1fc1d931a3dfe879e578ea10e2e6cd84f53
Libyuv: 5f79ced0ee66e60a612ca97de1e6ccacd187a437 Libyuv: 5f79ced0ee66e60a612ca97de1e6ccacd187a437
livekit_client: 9216ac9adba8d54cefc870bbcb657ec1da534309
path_provider: d1e9807085df1f9cc9318206cd649dc0b76be3de path_provider: d1e9807085df1f9cc9318206cd649dc0b76be3de
shared_preferences: 5033afbb22d372e15aff8ff766df9021b845f273 shared_preferences: 5033afbb22d372e15aff8ff766df9021b845f273
WebRTC-SDK: 7c76a541dbbffb0fc212aeb9902ec45a43c23996 WebRTC-SDK: 540a44f2e30e89c75e331d5c4a372253ba668233
PODFILE CHECKSUM: 82aed1035f46bfa5b522f0d0dbf4730f17ec65ff PODFILE CHECKSUM: 82aed1035f46bfa5b522f0d0dbf4730f17ec65ff
+7 -4
View File
@@ -155,7 +155,7 @@
97C146E61CF9000F007C117D /* Project object */ = { 97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
LastUpgradeCheck = 1020; LastUpgradeCheck = 1300;
ORGANIZATIONNAME = ""; ORGANIZATIONNAME = "";
TargetAttributes = { TargetAttributes = {
97C146ED1CF9000F007C117D = { 97C146ED1CF9000F007C117D = {
@@ -321,6 +321,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -352,7 +353,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-LiveKit";
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
@@ -396,6 +397,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -452,6 +454,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -485,7 +488,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-LiveKit";
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
@@ -511,7 +514,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-LiveKit";
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1020" LastUpgradeVersion = "1300"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
@@ -27,8 +27,6 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"> shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
@@ -38,8 +36,8 @@
ReferencedContainer = "container:Runner.xcodeproj"> ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
<AdditionalOptions> <Testables>
</AdditionalOptions> </Testables>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
@@ -61,8 +59,6 @@
ReferencedContainer = "container:Runner.xcodeproj"> ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Profile" buildConfiguration = "Profile"
@@ -0,0 +1,116 @@
{
"images" : [
{
"filename" : "[email protected]",
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "[email protected]",
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"filename" : "[email protected]",
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "[email protected]",
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"filename" : "[email protected]",
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "[email protected]",
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"filename" : "[email protected]",
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"filename" : "[email protected]",
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"filename" : "[email protected]",
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"filename" : "[email protected]",
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "[email protected]",
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "[email protected]",
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "[email protected]",
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"filename" : "[email protected]",
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "[email protected]",
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"filename" : "[email protected]",
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"filename" : "[email protected]",
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"filename" : "[email protected]",
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 568 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 619 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 881 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 954 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 651 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 771 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 771 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 955 B

@@ -1,122 +0,0 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "[email protected]",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "[email protected]",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "[email protected]",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "[email protected]",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "[email protected]",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "[email protected]",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "[email protected]",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "[email protected]",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "[email protected]",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "[email protected]",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "[email protected]",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "[email protected]",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "[email protected]",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "[email protected]",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "[email protected]",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "[email protected]",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "[email protected]",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "[email protected]",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "[email protected]",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
+1 -1
View File
@@ -11,7 +11,7 @@
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>livekit_example</string> <string>LiveKit</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
+18
View File
@@ -20,6 +20,24 @@ extension LKExampleExt on BuildContext {
), ),
); );
Future<bool?> showUnPublishDialog() => showDialog<bool>(
context: this,
builder: (ctx) => AlertDialog(
title: const Text('UnPublish'),
content: const Text('Would you like to un-publish your Camera & Mic ?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('NO'),
),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('YES'),
),
],
),
);
Future<void> showErrorDialog(dynamic exception) => showDialog<void>( Future<void> showErrorDialog(dynamic exception) => showDialog<void>(
context: this, context: this,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
+1 -3
View File
@@ -1,16 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:livekit_example/theme.dart'; import 'package:livekit_example/theme.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'pages/connect.dart'; import 'pages/connect.dart';
void main() { void main() {
print('This is a test for ${SignalTrickleEvent} test.');
// configure logs for debugging // configure logs for debugging
Logger.root.level = Level.FINE; Logger.root.level = Level.FINE;
Logger.root.onRecord.listen((record) { Logger.root.onRecord.listen((record) {
print('${record.level.name}: ${record.time}: ${record.message}'); print('${record.level.name}: ${record.message}');
}); });
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
+5 -2
View File
@@ -1,6 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:livekit_client/livekit_client.dart'; import 'package:livekit_client/livekit_client.dart';
@@ -25,7 +26,7 @@ class RoomPage extends StatefulWidget {
class _RoomPageState extends State<RoomPage> { class _RoomPageState extends State<RoomPage> {
// //
List<Participant> participants = []; List<Participant> participants = [];
late final _listener = EventsListener<RoomEvent>(widget.room.events); late final EventsListener<RoomEvent> _listener = widget.room.createListener();
@override @override
void initState() { void initState() {
@@ -48,7 +49,9 @@ class _RoomPageState extends State<RoomPage> {
} }
void _setUpListeners() => _listener void _setUpListeners() => _listener
..on<RoomDisconnectedEvent>((_) => Navigator.pop(context)) ..on<RoomDisconnectedEvent>((_) async {
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) => Navigator.pop(context));
})
..on<DataReceivedEvent>((event) { ..on<DataReceivedEvent>((event) {
String decoded = 'Failed to decode'; String decoded = 'Failed to decode';
try { try {
+15 -1
View File
@@ -1,9 +1,10 @@
import 'dart:convert'; import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:eva_icons_flutter/eva_icons_flutter.dart'; import 'package:eva_icons_flutter/eva_icons_flutter.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:livekit_client/livekit_client.dart'; import 'package:livekit_client/livekit_client.dart';
import 'package:collection/collection.dart';
import '../exts.dart'; import '../exts.dart';
class ControlsWidget extends StatefulWidget { class ControlsWidget extends StatefulWidget {
@@ -44,6 +45,15 @@ class _ControlsWidgetState extends State<ControlsWidget> {
setState(() {}); setState(() {});
} }
void _unpublishAll() async {
final result = await context.showUnPublishDialog();
if (result == true) {
await participant.unpublishAllTracks();
// Force to update UI for now
participant.notifyListeners();
}
}
void _muteAudio() { void _muteAudio() {
if (participant.hasAudio) { if (participant.hasAudio) {
final audioPub = participant.audioTracks.first; final audioPub = participant.audioTracks.first;
@@ -146,6 +156,10 @@ class _ControlsWidgetState extends State<ControlsWidget> {
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
IconButton(
onPressed: _unpublishAll,
icon: const Icon(EvaIcons.closeCircleOutline),
),
if (canMute) if (canMute)
IconButton( IconButton(
onPressed: _muteAudio, onPressed: _muteAudio,
+4 -4
View File
@@ -111,11 +111,11 @@ packages:
dependency: transitive dependency: transitive
description: description:
path: "." path: "."
ref: use-custom-webrtc-build ref: master
resolved-ref: "4942e7faec2e5775d35c42e22c2929ca6ca53769" resolved-ref: "32d83d85fa2faebcd37a19534f8a84273b12cbce"
url: "https://github.com/livekit/flutter-webrtc" url: "https://github.com/flutter-webrtc/flutter-webrtc"
source: git source: git
version: "0.6.7" version: "0.6.8"
google_fonts: google_fonts:
dependency: "direct main" dependency: "direct main"
description: description:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 917 B

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

+38
View File
@@ -0,0 +1,38 @@
.idea/
.vagrant/
.sconsign.dblite
.svn/
.DS_Store
*.swp
profile
DerivedData/
build/
GeneratedPluginRegistrant.h
GeneratedPluginRegistrant.m
.generated/
*.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
!default.pbxuser
!default.mode1v3
!default.mode2v3
!default.perspectivev3
xcuserdata
*.moved-aside
*.pyc
*sync/
Icon?
.tags*
/Flutter/Generated.xcconfig
/Flutter/ephemeral/
/Flutter/flutter_export_environment.sh
View File
+4
View File
@@ -0,0 +1,4 @@
#import <Flutter/Flutter.h>
@interface LiveKitClientPlugin : NSObject<FlutterPlugin>
@end
+15
View File
@@ -0,0 +1,15 @@
#import "LiveKitClientPlugin.h"
#if __has_include(<livekit_client/livekit_client-Swift.h>)
#import <livekit_client/livekit_client-Swift.h>
#else
// Support project import fallback if the generated compatibility header
// is not copied when this plugin is created as a library.
// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816
#import "livekit_client-Swift.h"
#endif
@implementation LiveKitClientPlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
[SwiftLiveKitClientPlugin registerWithRegistrar:registrar];
}
@end
+114
View File
@@ -0,0 +1,114 @@
import Flutter
import UIKit
import WebRTC
public class SwiftLiveKitClientPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "livekit_client", binaryMessenger: registrar.messenger())
let instance = SwiftLiveKitClientPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
// https://developer.apple.com/documentation/avfaudio/avaudiosession/category
let categoryMap: [String: AVAudioSession.Category] = [
"ambient": .ambient,
"multiRoute": .multiRoute,
"playAndRecord": .playAndRecord,
"playback": .playback,
"record": .record,
"soloAmbient": .soloAmbient,
]
// https://developer.apple.com/documentation/avfaudio/avaudiosession/categoryoptions
let categoryOptionsMap: [String: AVAudioSession.CategoryOptions] = [
"mixWithOthers": .mixWithOthers,
"duckOthers": .duckOthers,
"interruptSpokenAudioAndMixWithOthers": .interruptSpokenAudioAndMixWithOthers,
"allowBluetooth": .allowBluetooth,
"allowBluetoothA2DP": .allowBluetoothA2DP,
"allowAirPlay": .allowAirPlay,
"defaultToSpeaker": .defaultToSpeaker,
// @available(iOS 14.5, *)
// "overrideMutedMicrophoneInterruption": .overrideMutedMicrophoneInterruption,
]
// https://developer.apple.com/documentation/avfaudio/avaudiosession/mode
let modeMap: [String: AVAudioSession.Mode] = [
"default": .default,
"gameChat": .gameChat,
"measurement": .measurement,
"moviePlayback": .moviePlayback,
"spokenAudio": .spokenAudio,
"videoChat": .videoChat,
"videoRecording": .videoRecording,
"voiceChat": .voiceChat,
"voicePrompt": .voicePrompt,
]
private func categoryOptions(fromFlutter options: [String]) -> AVAudioSession.CategoryOptions {
var result: AVAudioSession.CategoryOptions = []
for option in categoryOptionsMap {
if (options.contains(option.key)) {
result.insert(option.value)
}
}
return result
}
public func handleConfigureNativeAudio(args: [String: Any?], result: @escaping FlutterResult) {
let configuration = RTCAudioSessionConfiguration.webRTC()
// Category
if let string = args["appleAudioCategory"] as? String,
let category = categoryMap[string] {
configuration.category = category.rawValue
print("[LiveKit] Configuring category: ", configuration.category)
}
// CategoryOptions
if let strings = args["appleAudioCategoryOptions"] as? [String] {
configuration.categoryOptions = categoryOptions(fromFlutter: strings)
print("[LiveKit] Configuring categoryOptions: ", strings)
}
// Mode
if let string = args["appleAudioMode"] as? String,
let mode = modeMap[string] {
configuration.mode = mode.rawValue
print("[LiveKit] Configuring mode: ", configuration.mode)
}
let session = RTCAudioSession.sharedInstance()
session.lockForConfiguration()
defer {
session.unlockForConfiguration()
}
do {
try session.setConfiguration(configuration, active: true)
print("[LiveKit] Configure success")
result(true)
} catch let error {
print("[LiveKit] Configure audio error: ", error)
result(FlutterError(code: "configure", message: error.localizedDescription, details: nil))
}
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? [String: Any?] else {
print("[LiveKit] arguments must be a dictionary")
result(FlutterMethodNotImplemented)
return
}
switch call.method {
case "configureNativeAudio":
handleConfigureNativeAudio(args: args, result: result)
default:
print("[LiveKit] method not found: ", call.method)
result(FlutterMethodNotImplemented)
}
}
}
+26
View File
@@ -0,0 +1,26 @@
#
# LiveKit
# https://livekit.io/
# https://github.com/livekit
#
Pod::Spec.new do |s|
s.name = 'livekit_client'
s.version = '0.0.1'
s.summary = 'Open source platform for real-time audio and video.'
s.description = 'Open source platform for real-time audio and video.'
s.homepage = 'https://livekit.io/'
s.license = { :file => '../LICENSE' }
s.author = { 'LiveKit' => '[email protected]' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.public_header_files = 'Classes/**/*.h'
s.platform = :ios, '12.1'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'
s.static_framework = true
s.dependency 'Flutter'
s.dependency 'WebRTC-SDK', '92.4515.07'
end
+1 -1
View File
@@ -1,7 +1,7 @@
/// Flutter Client SDK to LiveKit. /// Flutter Client SDK to LiveKit.
library livekit_client; library livekit_client;
export 'src/errors.dart'; export 'src/exceptions.dart';
export 'src/events.dart'; export 'src/events.dart';
export 'src/livekit.dart'; export 'src/livekit.dart';
export 'src/managers/event.dart'; export 'src/managers/event.dart';
-32
View File
@@ -1,32 +0,0 @@
import 'package:flutter/material.dart';
import 'package:livekit_client/src/logger.dart';
// dispose safe change notifier
abstract class LKChangeNotifier extends ChangeNotifier {
bool _disposed = false;
bool get isDisposed => _disposed;
@override
void dispose() {
_disposed = true;
super.dispose();
}
@override
void addListener(VoidCallback listener) {
if (_disposed) {
logger.warning('calling addListener on a disposed ChangeNotifier');
return;
}
super.addListener(listener);
}
@override
void removeListener(VoidCallback listener) {
if (_disposed) {
logger.warning('calling removeListener on a disposed ChangeNotifier');
return;
}
super.removeListener(listener);
}
}
+23 -28
View File
@@ -1,6 +1,4 @@
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
import 'package:livekit_client/livekit_client.dart';
import 'package:meta/meta.dart';
import 'participant/participant.dart'; import 'participant/participant.dart';
import 'participant/remote_participant.dart'; import 'participant/remote_participant.dart';
@@ -8,6 +6,7 @@ import 'proto/livekit_models.pb.dart' as lk_models;
import 'proto/livekit_rtc.pb.dart' as lk_rtc; import 'proto/livekit_rtc.pb.dart' as lk_rtc;
import 'track/remote_track_publication.dart'; import 'track/remote_track_publication.dart';
import 'track/track.dart'; import 'track/track.dart';
import 'track/track_publication.dart';
import 'types.dart'; import 'types.dart';
abstract class LiveKitEvent {} abstract class LiveKitEvent {}
@@ -110,14 +109,6 @@ class TrackSubscribedEvent with RoomEvent, ParticipantEvent {
}); });
} }
@internal
class ParticipantInfoUpdatedEvent with ParticipantEvent {
final RemoteParticipant participant;
const ParticipantInfoUpdatedEvent({
required this.participant,
});
}
/// An error has occured during track subscription. /// An error has occured during track subscription.
/// Emitted by [Room] and [RemoteParticipant]. /// Emitted by [Room] and [RemoteParticipant].
class TrackSubscriptionExceptionEvent with RoomEvent, ParticipantEvent { class TrackSubscriptionExceptionEvent with RoomEvent, ParticipantEvent {
@@ -226,12 +217,12 @@ class EngineReconnectedEvent with EngineEvent {
const EngineReconnectedEvent(); const EngineReconnectedEvent();
} }
class EngineParticipantUpdateEvent with EngineEvent { // class EngineParticipantUpdateEvent with EngineEvent {
final List<lk_models.ParticipantInfo> participants; // final List<lk_models.ParticipantInfo> participants;
const EngineParticipantUpdateEvent({ // const EngineParticipantUpdateEvent({
required this.participants, // required this.participants,
}); // });
} // }
class EngineTrackAddedEvent with EngineEvent { class EngineTrackAddedEvent with EngineEvent {
final rtc.MediaStreamTrack track; final rtc.MediaStreamTrack track;
@@ -244,13 +235,6 @@ class EngineTrackAddedEvent with EngineEvent {
}); });
} }
class EngineSpeakersUpdateEvent with EngineEvent {
final List<lk_models.SpeakerInfo> speakers;
const EngineSpeakersUpdateEvent({
required this.speakers,
});
}
class EngineDataPacketReceivedEvent with EngineEvent { class EngineDataPacketReceivedEvent with EngineEvent {
final lk_models.UserPacket packet; final lk_models.UserPacket packet;
final lk_models.DataPacket_Kind kind; final lk_models.DataPacket_Kind kind;
@@ -363,10 +347,11 @@ class SignalTrickleEvent with SignalEvent {
}); });
} }
class SignalParticipantUpdateEvent with SignalEvent { // relayed by Engine
final List<lk_models.ParticipantInfo> updates; class SignalParticipantUpdateEvent with SignalEvent, EngineEvent {
final List<lk_models.ParticipantInfo> participants;
const SignalParticipantUpdateEvent({ const SignalParticipantUpdateEvent({
required this.updates, required this.participants,
}); });
} }
@@ -379,9 +364,19 @@ class SignalLocalTrackPublishedEvent with SignalEvent {
}); });
} }
class SignalActiveSpeakersChangedEvent with SignalEvent { // speaker update received through websocket
// relayed by Engine
class SignalSpeakersChangedEvent with SignalEvent, EngineEvent {
final List<lk_models.SpeakerInfo> speakers; final List<lk_models.SpeakerInfo> speakers;
const SignalActiveSpeakersChangedEvent({ const SignalSpeakersChangedEvent({
required this.speakers,
});
}
// Event received through data channel
class EngineActiveSpeakersUpdateEvent with EngineEvent {
final List<lk_models.SpeakerInfo> speakers;
const EngineActiveSpeakersUpdateEvent({
required this.speakers, required this.speakers,
}); });
} }
@@ -1,6 +1,3 @@
//
//
//
class LiveKitException implements Exception { class LiveKitException implements Exception {
final String message; final String message;
const LiveKitException._(this.message); const LiveKitException._(this.message);
+16 -2
View File
@@ -1,9 +1,14 @@
import 'package:async/async.dart'; import 'package:async/async.dart';
class CancelableDelayManager { import '../support/disposable.dart';
class CancelableDelayManager extends Disposable {
// //
final _delays = <CancelableOperation<void>>[]; final _delays = <CancelableOperation<void>>[];
CancelableDelayManager() {
onDispose(() async => await cancelAll());
}
// delay but cancelable // delay but cancelable
Future<void> waitFor( Future<void> waitFor(
Duration wait, { Duration wait, {
@@ -19,7 +24,16 @@ class CancelableDelayManager {
if (!op.isCanceled) ifNotCancelled?.call(); if (!op.isCanceled) ifNotCancelled?.call();
} }
Future<void> dispose() async { // @override
// Future<bool> dispose() async {
// final didDispose = await super.dispose();
// if (didDispose) {
// await cancelAll();
// }
// return didDispose;
// }
Future<void> cancelAll() async {
// cancel all delays // cancel all delays
if (_delays.isEmpty) return; if (_delays.isEmpty) return;
// make a copy so we don't mutate while iterating // make a copy so we don't mutate while iterating
+38 -20
View File
@@ -1,13 +1,19 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart';
import 'package:synchronized/synchronized.dart' as sync; import 'package:synchronized/synchronized.dart' as sync;
import '../errors.dart'; import '../exceptions.dart';
import '../extensions.dart'; import '../extensions.dart';
import '../logger.dart'; import '../logger.dart';
import '../support/disposable.dart';
import '../types.dart'; import '../types.dart';
mixin EventsEmittable<T> {
final events = EventsEmitter<T>();
EventsListener<T> createListener({bool synchronized = false}) =>
EventsListener<T>(events, synchronized: synchronized);
}
// Type-safe, multi-listenable, dispose safe event handling // Type-safe, multi-listenable, dispose safe event handling
// TODO: Move to a separate package // TODO: Move to a separate package
@@ -17,7 +23,9 @@ class EventsEmitter<T> extends EventsListenable<T> {
EventsEmitter({ EventsEmitter({
bool listenSynchronized = false, bool listenSynchronized = false,
}) : super(synchronized: listenSynchronized); }) : super(synchronized: listenSynchronized) {
onDispose(() async => await streamCtrl.close());
}
@override @override
EventsEmitter<T> get emitter => this; EventsEmitter<T> get emitter => this;
@@ -28,13 +36,6 @@ class EventsEmitter<T> extends EventsListenable<T> {
// emit the event // emit the event
streamCtrl.add(event); streamCtrl.add(event);
} }
@override
@mustCallSuper
Future<void> dispose() async {
await streamCtrl.close();
await super.dispose();
}
} }
// for listening only // for listening only
@@ -51,7 +52,7 @@ class EventsListener<T> extends EventsListenable<T> {
} }
// ensures all listeners will close on dispose // ensures all listeners will close on dispose
abstract class EventsListenable<T> { abstract class EventsListenable<T> extends Disposable {
// the emitter to listen to // the emitter to listen to
EventsEmitter<T> get emitter; EventsEmitter<T> get emitter;
@@ -62,17 +63,34 @@ abstract class EventsListenable<T> {
EventsListenable({ EventsListenable({
required this.synchronized, required this.synchronized,
}); }) {
onDispose(() async {
@mustCallSuper if (_listeners.isNotEmpty) {
Future<void> dispose() async { // Stop listening to all events
// Stop listening to all events logger.fine('${objectId} cancelling ${_listeners.length} listeners(s)');
logger.fine('${objectId} dispose() cancelling ${_listeners.length} event(s)'); for (final listener in _listeners) {
for (final listener in _listeners) { await listener.cancel();
await listener.cancel(); }
} }
});
} }
// @override
// @mustCallSuper
// Future<bool> dispose() async {
// // mark as disposed
// final didDispose = await super.dispose();
// if (didDispose && _listeners.isNotEmpty) {
// // Stop listening to all events
// logger.fine('${objectId} dispose() cancelling ${_listeners.length} event(s)');
// for (final listener in _listeners) {
// await listener.cancel();
// }
// }
// return didDispose;
// }
// listens to all events, guaranteed to be cancelled on dispose // listens to all events, guaranteed to be cancelled on dispose
CancelListenFunc listen(FutureOr<void> Function(T) onEvent) { CancelListenFunc listen(FutureOr<void> Function(T) onEvent) {
// //
+53 -35
View File
@@ -1,8 +1,9 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
import 'package:meta/meta.dart';
import '../errors.dart';
import '../events.dart'; import '../events.dart';
import '../exceptions.dart';
import '../extensions.dart'; import '../extensions.dart';
import '../logger.dart'; import '../logger.dart';
import '../managers/event.dart'; import '../managers/event.dart';
@@ -19,16 +20,17 @@ import 'participant.dart';
/// Represents the current participant in the room. /// Represents the current participant in the room.
class LocalParticipant extends Participant { class LocalParticipant extends Participant {
final RTCEngine _engine; @internal
final RTCEngine engine;
@internal
final TrackPublishOptions? defaultPublishOptions; final TrackPublishOptions? defaultPublishOptions;
LocalParticipant({ LocalParticipant({
required RTCEngine engine, required this.engine,
required lk_models.ParticipantInfo info, required lk_models.ParticipantInfo info,
this.defaultPublishOptions, this.defaultPublishOptions,
required EventsEmitter<RoomEvent> roomEvents, required EventsEmitter<RoomEvent> roomEvents,
}) : _engine = engine, }) : super(
super(
info.sid, info.sid,
info.identity, info.identity,
roomEvents: roomEvents, roomEvents: roomEvents,
@@ -36,39 +38,43 @@ class LocalParticipant extends Participant {
updateFromInfo(info); updateFromInfo(info);
} }
/// for internal use
/// {@nodoc}
RTCEngine get engine => _engine;
/// publish an audio track to the room /// publish an audio track to the room
Future<TrackPublication> publishAudioTrack(LocalAudioTrack track) async { Future<TrackPublication> publishAudioTrack(LocalAudioTrack track) async {
if (audioTracks.any((e) => e.track?.mediaStreamTrack.id == track.mediaStreamTrack.id)) { if (audioTracks.any((e) => e.track?.mediaStreamTrack.id == track.mediaStreamTrack.id)) {
throw TrackPublishException('track already exists'); throw TrackPublishException('track already exists');
} }
// try { // await AudioManager().incrementPublishCounter();
final trackInfo = await _engine.addTrack(
cid: track.getCid(),
name: track.name,
kind: track.kind,
);
final transceiverInit = rtc.RTCRtpTransceiverInit( try {
direction: rtc.TransceiverDirection.SendOnly, final trackInfo = await engine.addTrack(
); cid: track.getCid(),
// addTransceiver cannot pass in a kind parameter due to a bug in flutter-webrtc (web) name: track.name,
track.transceiver = await _engine.publisher?.pc.addTransceiver( kind: track.kind,
track: track.mediaStreamTrack, );
kind: rtc.RTCRtpMediaType.RTCRtpMediaTypeAudio,
init: transceiverInit,
);
await _engine.negotiate();
final pub = LocalTrackPublication(trackInfo, track, this); await track.start();
addTrackPublication(pub);
notifyListeners();
return pub; final transceiverInit = rtc.RTCRtpTransceiverInit(
direction: rtc.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,
kind: rtc.RTCRtpMediaType.RTCRtpMediaTypeAudio,
init: transceiverInit,
);
await engine.negotiate();
final pub = LocalTrackPublication(trackInfo, track, this);
addTrackPublication(pub);
notifyListeners();
return pub;
} catch (e) {
// In any case there was an exception, revert the count.
// await AudioManager().decrementPublishCounter();
rethrow;
}
} }
/// Publish a video track to the room /// Publish a video track to the room
@@ -83,13 +89,16 @@ class LocalParticipant extends Participant {
// Use default options from `ConnectOptions` if options is null // Use default options from `ConnectOptions` if options is null
options = options ?? defaultPublishOptions; options = options ?? defaultPublishOptions;
final trackInfo = await _engine.addTrack( final trackInfo = await engine.addTrack(
cid: track.getCid(), cid: track.getCid(),
name: track.name, name: track.name,
kind: track.kind, kind: track.kind,
); );
logger.fine('publishVideoTrack addTrack response: ${trackInfo}'); logger.fine('publishVideoTrack addTrack response: ${trackInfo}');
await track.start();
// Video encodings and simulcasts // Video encodings and simulcasts
// use constraints passed to getUserMedia by default // use constraints passed to getUserMedia by default
@@ -124,14 +133,14 @@ class LocalParticipant extends Participant {
streams: [track.mediaStream], streams: [track.mediaStream],
); );
logger.fine('publishVideoTrack publisher: ${_engine.publisher}'); logger.fine('publishVideoTrack publisher: ${engine.publisher}');
track.transceiver = await _engine.publisher?.pc.addTransceiver( track.transceiver = await engine.publisher?.pc.addTransceiver(
track: track.mediaStreamTrack, track: track.mediaStreamTrack,
kind: rtc.RTCRtpMediaType.RTCRtpMediaTypeVideo, kind: rtc.RTCRtpMediaType.RTCRtpMediaTypeVideo,
init: transceiverInit, init: transceiverInit,
); );
await _engine.negotiate(); await engine.negotiate();
final pub = LocalTrackPublication(trackInfo, track, this); final pub = LocalTrackPublication(trackInfo, track, this);
addTrackPublication(pub); addTrackPublication(pub);
@@ -156,8 +165,17 @@ class LocalParticipant extends Participant {
final sender = track.transceiver?.sender; final sender = track.transceiver?.sender;
if (sender != null) { if (sender != null) {
await engine.publisher?.pc.removeTrack(sender); try {
await engine.negotiate(); await engine.publisher?.pc.removeTrack(sender);
} catch (_) {
logger.warning('[$objectId] rtc.removeTrack() did throw ${_}');
}
// doesn't make sense to negotiate if already disposed
if (!isDisposed) {
// manual negotiation since track changed
await engine.negotiate();
}
} }
} }
} }
+13 -16
View File
@@ -1,13 +1,12 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import '../classes/change_notifier.dart';
import '../events.dart'; import '../events.dart';
import '../extensions.dart'; import '../extensions.dart';
import '../logger.dart'; import '../logger.dart';
import '../managers/event.dart'; import '../managers/event.dart';
import '../proto/livekit_models.pb.dart' as lk_models; import '../proto/livekit_models.pb.dart' as lk_models;
import '../support/disposable.dart';
import '../track/track_publication.dart'; import '../track/track_publication.dart';
import 'remote_participant.dart'; import 'remote_participant.dart';
@@ -21,7 +20,7 @@ import 'remote_participant.dart';
/// Base for [RemoteParticipant] and [LocalParticipant], /// Base for [RemoteParticipant] and [LocalParticipant],
/// can not be instantiated directly. /// can not be instantiated directly.
abstract class Participant extends LKChangeNotifier { abstract class Participant extends DisposableChangeNotifier with EventsEmittable<ParticipantEvent> {
/// map of track sid => published track /// map of track sid => published track
final trackPublications = <String, TrackPublication>{}; final trackPublications = <String, TrackPublication>{};
@@ -44,7 +43,6 @@ abstract class Participant extends LKChangeNotifier {
bool _isSpeaking = false; bool _isSpeaking = false;
// suppport for multiple event listeners // suppport for multiple event listeners
final events = EventsEmitter<ParticipantEvent>();
final EventsEmitter<RoomEvent> roomEvents; final EventsEmitter<RoomEvent> roomEvents;
/// when the participant joined the room /// when the participant joined the room
@@ -85,14 +83,11 @@ abstract class Participant extends LKChangeNotifier {
logger.fine('[ParticipantEvent] $event, will notifyListeners()'); logger.fine('[ParticipantEvent] $event, will notifyListeners()');
notifyListeners(); notifyListeners();
}); });
}
@override onDispose(() async {
@mustCallSuper await events.dispose();
Future<void> dispose() async { await unpublishAllTracks();
logger.fine('$objectId dispose()'); });
await events.dispose();
super.dispose();
} }
/// for internal use /// for internal use
@@ -106,7 +101,7 @@ abstract class Participant extends LKChangeNotifier {
lastSpokeAt = DateTime.now(); lastSpokeAt = DateTime.now();
} }
[events, roomEvents].emit(SpeakingChangedEvent( events.emit(SpeakingChangedEvent(
participant: this, participant: this,
speaking: speaking, speaking: speaking,
)); ));
@@ -145,15 +140,17 @@ abstract class Participant extends LKChangeNotifier {
// Must implement // Must implement
Future<void> unpublishTrack(String trackSid, {bool notify = false}); Future<void> unpublishTrack(String trackSid, {bool notify = false});
Future<void> unpublishAllTracks() async { Future<void> unpublishAllTracks({bool notify = false}) async {
final _ = List<TrackPublication>.from(trackPublications.values); final trackSids = trackPublications.keys.toSet();
for (final track in _) { for (final trackid in trackSids) {
await unpublishTrack(track.sid); await unpublishTrack(trackid, notify: notify);
} }
} }
//
// Equality operators // Equality operators
// Object is considered equal when sid is equal // Object is considered equal when sid is equal
//
@override @override
int get hashCode => sid.hashCode; int get hashCode => sid.hashCode;
+8 -3
View File
@@ -90,8 +90,10 @@ class RemoteParticipant extends Participant {
final Track track; final Track track;
if (pub.kind == lk_models.TrackType.AUDIO) { if (pub.kind == lk_models.TrackType.AUDIO) {
// audio track // audio track
// await AudioManager().incrementSubscriptionCounter();
final audioTrack = AudioTrack(pub.name, mediaTrack, stream); final audioTrack = AudioTrack(pub.name, mediaTrack, stream);
audioTrack.start(); await audioTrack.start();
track = audioTrack; track = audioTrack;
} else { } else {
// video track // video track
@@ -143,8 +145,7 @@ class RemoteParticipant extends Participant {
// unpublish any track that is not in the info // unpublish any track that is not in the info
final validSids = info.tracks.map((e) => e.sid); final validSids = info.tracks.map((e) => e.sid);
final removeSids = final removeSids = trackPublications.keys.where((e) => !validSids.contains(e)).toSet();
trackPublications.values.where((e) => !validSids.contains(e.sid)).map((e) => e.sid);
for (final sid in removeSids) { for (final sid in removeSids) {
await unpublishTrack(sid, notify: true); await unpublishTrack(sid, notify: true);
} }
@@ -165,6 +166,10 @@ class RemoteParticipant extends Participant {
track: track, track: track,
publication: pub, publication: pub,
)); ));
// if (track is AudioTrack) {
// await AudioManager().decrementSubscriptionCounter();
// }
} }
if (notify) { if (notify) {
+126
View File
@@ -37,6 +37,7 @@ class Room extends $pb.GeneratedMessage {
const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'enabledCodecs', const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'enabledCodecs',
$pb.PbFieldType.PM, $pb.PbFieldType.PM,
subBuilder: Codec.create) subBuilder: Codec.create)
..aOS(8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'metadata')
..hasRequiredFields = false; ..hasRequiredFields = false;
Room._() : super(); Room._() : super();
@@ -48,6 +49,7 @@ class Room extends $pb.GeneratedMessage {
$fixnum.Int64? creationTime, $fixnum.Int64? creationTime,
$core.String? turnPassword, $core.String? turnPassword,
$core.Iterable<Codec>? enabledCodecs, $core.Iterable<Codec>? enabledCodecs,
$core.String? metadata,
}) { }) {
final _result = create(); final _result = create();
if (sid != null) { if (sid != null) {
@@ -71,6 +73,9 @@ class Room extends $pb.GeneratedMessage {
if (enabledCodecs != null) { if (enabledCodecs != null) {
_result.enabledCodecs.addAll(enabledCodecs); _result.enabledCodecs.addAll(enabledCodecs);
} }
if (metadata != null) {
_result.metadata = metadata;
}
return _result; return _result;
} }
factory Room.fromBuffer($core.List<$core.int> i, factory Room.fromBuffer($core.List<$core.int> i,
@@ -171,6 +176,18 @@ class Room extends $pb.GeneratedMessage {
@$pb.TagNumber(7) @$pb.TagNumber(7)
$core.List<Codec> get enabledCodecs => $_getList(6); $core.List<Codec> get enabledCodecs => $_getList(6);
@$pb.TagNumber(8)
$core.String get metadata => $_getSZ(7);
@$pb.TagNumber(8)
set metadata($core.String v) {
$_setString(7, v);
}
@$pb.TagNumber(8)
$core.bool hasMetadata() => $_has(7);
@$pb.TagNumber(8)
void clearMetadata() => clearField(8);
} }
class Codec extends $pb.GeneratedMessage { class Codec extends $pb.GeneratedMessage {
@@ -922,3 +939,112 @@ class UserPacket extends $pb.GeneratedMessage {
@$pb.TagNumber(3) @$pb.TagNumber(3)
$core.List<$core.String> get destinationSids => $_getList(2); $core.List<$core.String> get destinationSids => $_getList(2);
} }
class RecordingResult extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'RecordingResult',
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') ? '' : 'id')
..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'error')
..aInt64(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'duration')
..aOS(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'location')
..hasRequiredFields = false;
RecordingResult._() : super();
factory RecordingResult({
$core.String? id,
$core.String? error,
$fixnum.Int64? duration,
$core.String? location,
}) {
final _result = create();
if (id != null) {
_result.id = id;
}
if (error != null) {
_result.error = error;
}
if (duration != null) {
_result.duration = duration;
}
if (location != null) {
_result.location = location;
}
return _result;
}
factory RecordingResult.fromBuffer($core.List<$core.int> i,
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(i, r);
factory RecordingResult.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')
RecordingResult clone() => RecordingResult()..mergeFromMessage(this);
@$core.Deprecated('Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
RecordingResult copyWith(void Function(RecordingResult) updates) =>
super.copyWith((message) => updates(message as RecordingResult))
as RecordingResult; // ignore: deprecated_member_use
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static RecordingResult create() => RecordingResult._();
RecordingResult createEmptyInstance() => create();
static $pb.PbList<RecordingResult> createRepeated() => $pb.PbList<RecordingResult>();
@$core.pragma('dart2js:noInline')
static RecordingResult getDefault() =>
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<RecordingResult>(create);
static RecordingResult? _defaultInstance;
@$pb.TagNumber(1)
$core.String get id => $_getSZ(0);
@$pb.TagNumber(1)
set id($core.String v) {
$_setString(0, v);
}
@$pb.TagNumber(1)
$core.bool hasId() => $_has(0);
@$pb.TagNumber(1)
void clearId() => clearField(1);
@$pb.TagNumber(2)
$core.String get error => $_getSZ(1);
@$pb.TagNumber(2)
set error($core.String v) {
$_setString(1, v);
}
@$pb.TagNumber(2)
$core.bool hasError() => $_has(1);
@$pb.TagNumber(2)
void clearError() => clearField(2);
@$pb.TagNumber(3)
$fixnum.Int64 get duration => $_getI64(2);
@$pb.TagNumber(3)
set duration($fixnum.Int64 v) {
$_setInt64(2, v);
}
@$pb.TagNumber(3)
$core.bool hasDuration() => $_has(2);
@$pb.TagNumber(3)
void clearDuration() => clearField(3);
@$pb.TagNumber(4)
$core.String get location => $_getSZ(3);
@$pb.TagNumber(4)
set location($core.String v) {
$_setString(3, v);
}
@$pb.TagNumber(4)
$core.bool hasLocation() => $_has(3);
@$pb.TagNumber(4)
void clearLocation() => clearField(4);
}
+16 -1
View File
@@ -40,12 +40,13 @@ const Room$json = const {
'6': '.livekit.Codec', '6': '.livekit.Codec',
'10': 'enabledCodecs' '10': 'enabledCodecs'
}, },
const {'1': 'metadata', '3': 8, '4': 1, '5': 9, '10': 'metadata'},
], ],
}; };
/// Descriptor for `Room`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `Room`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List roomDescriptor = $convert.base64Decode( final $typed_data.Uint8List roomDescriptor = $convert.base64Decode(
'CgRSb29tEhAKA3NpZBgBIAEoCVIDc2lkEhIKBG5hbWUYAiABKAlSBG5hbWUSIwoNZW1wdHlfdGltZW91dBgDIAEoDVIMZW1wdHlUaW1lb3V0EikKEG1heF9wYXJ0aWNpcGFudHMYBCABKA1SD21heFBhcnRpY2lwYW50cxIjCg1jcmVhdGlvbl90aW1lGAUgASgDUgxjcmVhdGlvblRpbWUSIwoNdHVybl9wYXNzd29yZBgGIAEoCVIMdHVyblBhc3N3b3JkEjUKDmVuYWJsZWRfY29kZWNzGAcgAygLMg4ubGl2ZWtpdC5Db2RlY1INZW5hYmxlZENvZGVjcw=='); 'CgRSb29tEhAKA3NpZBgBIAEoCVIDc2lkEhIKBG5hbWUYAiABKAlSBG5hbWUSIwoNZW1wdHlfdGltZW91dBgDIAEoDVIMZW1wdHlUaW1lb3V0EikKEG1heF9wYXJ0aWNpcGFudHMYBCABKA1SD21heFBhcnRpY2lwYW50cxIjCg1jcmVhdGlvbl90aW1lGAUgASgDUgxjcmVhdGlvblRpbWUSIwoNdHVybl9wYXNzd29yZBgGIAEoCVIMdHVyblBhc3N3b3JkEjUKDmVuYWJsZWRfY29kZWNzGAcgAygLMg4ubGl2ZWtpdC5Db2RlY1INZW5hYmxlZENvZGVjcxIaCghtZXRhZGF0YRgIIAEoCVIIbWV0YWRhdGE=');
@$core.Deprecated('Use codecDescriptor instead') @$core.Deprecated('Use codecDescriptor instead')
const Codec$json = const { const Codec$json = const {
'1': 'Codec', '1': 'Codec',
@@ -182,3 +183,17 @@ const UserPacket$json = const {
/// Descriptor for `UserPacket`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `UserPacket`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List userPacketDescriptor = $convert.base64Decode( final $typed_data.Uint8List userPacketDescriptor = $convert.base64Decode(
'CgpVc2VyUGFja2V0EicKD3BhcnRpY2lwYW50X3NpZBgBIAEoCVIOcGFydGljaXBhbnRTaWQSGAoHcGF5bG9hZBgCIAEoDFIHcGF5bG9hZBIpChBkZXN0aW5hdGlvbl9zaWRzGAMgAygJUg9kZXN0aW5hdGlvblNpZHM='); 'CgpVc2VyUGFja2V0EicKD3BhcnRpY2lwYW50X3NpZBgBIAEoCVIOcGFydGljaXBhbnRTaWQSGAoHcGF5bG9hZBgCIAEoDFIHcGF5bG9hZBIpChBkZXN0aW5hdGlvbl9zaWRzGAMgAygJUg9kZXN0aW5hdGlvblNpZHM=');
@$core.Deprecated('Use recordingResultDescriptor instead')
const RecordingResult$json = const {
'1': 'RecordingResult',
'2': const [
const {'1': 'id', '3': 1, '4': 1, '5': 9, '10': 'id'},
const {'1': 'error', '3': 2, '4': 1, '5': 9, '10': 'error'},
const {'1': 'duration', '3': 3, '4': 1, '5': 3, '10': 'duration'},
const {'1': 'location', '3': 4, '4': 1, '5': 9, '10': 'location'},
],
};
/// Descriptor for `RecordingResult`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List recordingResultDescriptor = $convert.base64Decode(
'Cg9SZWNvcmRpbmdSZXN1bHQSDgoCaWQYASABKAlSAmlkEhQKBWVycm9yGAIgASgJUgVlcnJvchIaCghkdXJhdGlvbhgDIAEoA1IIZHVyYXRpb24SGgoIbG9jYXRpb24YBCABKAlSCGxvY2F0aW9u');
+167 -30
View File
@@ -282,9 +282,10 @@ enum SignalResponse_Message {
trickle, trickle,
update, update,
trackPublished, trackPublished,
speaker,
leave, leave,
mute, mute,
speakersChanged,
roomUpdate,
notSet notSet
} }
@@ -296,9 +297,10 @@ class SignalResponse extends $pb.GeneratedMessage {
4: SignalResponse_Message.trickle, 4: SignalResponse_Message.trickle,
5: SignalResponse_Message.update, 5: SignalResponse_Message.update,
6: SignalResponse_Message.trackPublished, 6: SignalResponse_Message.trackPublished,
7: SignalResponse_Message.speaker,
8: SignalResponse_Message.leave, 8: SignalResponse_Message.leave,
9: SignalResponse_Message.mute, 9: SignalResponse_Message.mute,
10: SignalResponse_Message.speakersChanged,
11: SignalResponse_Message.roomUpdate,
0: SignalResponse_Message.notSet 0: SignalResponse_Message.notSet
}; };
static final $pb.BuilderInfo _i = $pb.BuilderInfo( static final $pb.BuilderInfo _i = $pb.BuilderInfo(
@@ -306,7 +308,7 @@ class SignalResponse extends $pb.GeneratedMessage {
package: const $pb.PackageName( package: const $pb.PackageName(
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'livekit'), const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'livekit'),
createEmptyInstance: create) createEmptyInstance: create)
..oo(0, [1, 2, 3, 4, 5, 6, 7, 8, 9]) ..oo(0, [1, 2, 3, 4, 5, 6, 8, 9, 10, 11])
..aOM<JoinResponse>( ..aOM<JoinResponse>(
1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'join', 1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'join',
subBuilder: JoinResponse.create) subBuilder: JoinResponse.create)
@@ -325,15 +327,18 @@ class SignalResponse extends $pb.GeneratedMessage {
..aOM<TrackPublishedResponse>( ..aOM<TrackPublishedResponse>(
6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'trackPublished', 6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'trackPublished',
subBuilder: TrackPublishedResponse.create) subBuilder: TrackPublishedResponse.create)
..aOM<$0.ActiveSpeakerUpdate>(
7, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'speaker',
subBuilder: $0.ActiveSpeakerUpdate.create)
..aOM<LeaveRequest>( ..aOM<LeaveRequest>(
8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'leave', 8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'leave',
subBuilder: LeaveRequest.create) subBuilder: LeaveRequest.create)
..aOM<MuteTrackRequest>( ..aOM<MuteTrackRequest>(
9, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'mute', 9, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'mute',
subBuilder: MuteTrackRequest.create) subBuilder: MuteTrackRequest.create)
..aOM<SpeakersChanged>(
10, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'speakersChanged',
subBuilder: SpeakersChanged.create)
..aOM<RoomUpdate>(
11, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'roomUpdate',
subBuilder: RoomUpdate.create)
..hasRequiredFields = false; ..hasRequiredFields = false;
SignalResponse._() : super(); SignalResponse._() : super();
@@ -344,9 +349,10 @@ class SignalResponse extends $pb.GeneratedMessage {
TrickleRequest? trickle, TrickleRequest? trickle,
ParticipantUpdate? update, ParticipantUpdate? update,
TrackPublishedResponse? trackPublished, TrackPublishedResponse? trackPublished,
$0.ActiveSpeakerUpdate? speaker,
LeaveRequest? leave, LeaveRequest? leave,
MuteTrackRequest? mute, MuteTrackRequest? mute,
SpeakersChanged? speakersChanged,
RoomUpdate? roomUpdate,
}) { }) {
final _result = create(); final _result = create();
if (join != null) { if (join != null) {
@@ -367,15 +373,18 @@ class SignalResponse extends $pb.GeneratedMessage {
if (trackPublished != null) { if (trackPublished != null) {
_result.trackPublished = trackPublished; _result.trackPublished = trackPublished;
} }
if (speaker != null) {
_result.speaker = speaker;
}
if (leave != null) { if (leave != null) {
_result.leave = leave; _result.leave = leave;
} }
if (mute != null) { if (mute != null) {
_result.mute = mute; _result.mute = mute;
} }
if (speakersChanged != null) {
_result.speakersChanged = speakersChanged;
}
if (roomUpdate != null) {
_result.roomUpdate = roomUpdate;
}
return _result; return _result;
} }
factory SignalResponse.fromBuffer($core.List<$core.int> i, factory SignalResponse.fromBuffer($core.List<$core.int> i,
@@ -491,47 +500,61 @@ class SignalResponse extends $pb.GeneratedMessage {
@$pb.TagNumber(6) @$pb.TagNumber(6)
TrackPublishedResponse ensureTrackPublished() => $_ensure(5); TrackPublishedResponse ensureTrackPublished() => $_ensure(5);
@$pb.TagNumber(7)
$0.ActiveSpeakerUpdate get speaker => $_getN(6);
@$pb.TagNumber(7)
set speaker($0.ActiveSpeakerUpdate v) {
setField(7, v);
}
@$pb.TagNumber(7)
$core.bool hasSpeaker() => $_has(6);
@$pb.TagNumber(7)
void clearSpeaker() => clearField(7);
@$pb.TagNumber(7)
$0.ActiveSpeakerUpdate ensureSpeaker() => $_ensure(6);
@$pb.TagNumber(8) @$pb.TagNumber(8)
LeaveRequest get leave => $_getN(7); LeaveRequest get leave => $_getN(6);
@$pb.TagNumber(8) @$pb.TagNumber(8)
set leave(LeaveRequest v) { set leave(LeaveRequest v) {
setField(8, v); setField(8, v);
} }
@$pb.TagNumber(8) @$pb.TagNumber(8)
$core.bool hasLeave() => $_has(7); $core.bool hasLeave() => $_has(6);
@$pb.TagNumber(8) @$pb.TagNumber(8)
void clearLeave() => clearField(8); void clearLeave() => clearField(8);
@$pb.TagNumber(8) @$pb.TagNumber(8)
LeaveRequest ensureLeave() => $_ensure(7); LeaveRequest ensureLeave() => $_ensure(6);
@$pb.TagNumber(9) @$pb.TagNumber(9)
MuteTrackRequest get mute => $_getN(8); MuteTrackRequest get mute => $_getN(7);
@$pb.TagNumber(9) @$pb.TagNumber(9)
set mute(MuteTrackRequest v) { set mute(MuteTrackRequest v) {
setField(9, v); setField(9, v);
} }
@$pb.TagNumber(9) @$pb.TagNumber(9)
$core.bool hasMute() => $_has(8); $core.bool hasMute() => $_has(7);
@$pb.TagNumber(9) @$pb.TagNumber(9)
void clearMute() => clearField(9); void clearMute() => clearField(9);
@$pb.TagNumber(9) @$pb.TagNumber(9)
MuteTrackRequest ensureMute() => $_ensure(8); MuteTrackRequest ensureMute() => $_ensure(7);
@$pb.TagNumber(10)
SpeakersChanged get speakersChanged => $_getN(8);
@$pb.TagNumber(10)
set speakersChanged(SpeakersChanged v) {
setField(10, v);
}
@$pb.TagNumber(10)
$core.bool hasSpeakersChanged() => $_has(8);
@$pb.TagNumber(10)
void clearSpeakersChanged() => clearField(10);
@$pb.TagNumber(10)
SpeakersChanged ensureSpeakersChanged() => $_ensure(8);
@$pb.TagNumber(11)
RoomUpdate get roomUpdate => $_getN(9);
@$pb.TagNumber(11)
set roomUpdate(RoomUpdate v) {
setField(11, v);
}
@$pb.TagNumber(11)
$core.bool hasRoomUpdate() => $_has(9);
@$pb.TagNumber(11)
void clearRoomUpdate() => clearField(11);
@$pb.TagNumber(11)
RoomUpdate ensureRoomUpdate() => $_ensure(9);
} }
class AddTrackRequest extends $pb.GeneratedMessage { class AddTrackRequest extends $pb.GeneratedMessage {
@@ -1559,3 +1582,117 @@ class ICEServer extends $pb.GeneratedMessage {
@$pb.TagNumber(3) @$pb.TagNumber(3)
void clearCredential() => clearField(3); void clearCredential() => clearField(3);
} }
class SpeakersChanged extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'SpeakersChanged',
package: const $pb.PackageName(
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'livekit'),
createEmptyInstance: create)
..pc<$0.SpeakerInfo>(
1,
const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'speakers',
$pb.PbFieldType.PM,
subBuilder: $0.SpeakerInfo.create)
..hasRequiredFields = false;
SpeakersChanged._() : super();
factory SpeakersChanged({
$core.Iterable<$0.SpeakerInfo>? speakers,
}) {
final _result = create();
if (speakers != null) {
_result.speakers.addAll(speakers);
}
return _result;
}
factory SpeakersChanged.fromBuffer($core.List<$core.int> i,
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(i, r);
factory SpeakersChanged.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')
SpeakersChanged clone() => SpeakersChanged()..mergeFromMessage(this);
@$core.Deprecated('Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
SpeakersChanged copyWith(void Function(SpeakersChanged) updates) =>
super.copyWith((message) => updates(message as SpeakersChanged))
as SpeakersChanged; // ignore: deprecated_member_use
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static SpeakersChanged create() => SpeakersChanged._();
SpeakersChanged createEmptyInstance() => create();
static $pb.PbList<SpeakersChanged> createRepeated() => $pb.PbList<SpeakersChanged>();
@$core.pragma('dart2js:noInline')
static SpeakersChanged getDefault() =>
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<SpeakersChanged>(create);
static SpeakersChanged? _defaultInstance;
@$pb.TagNumber(1)
$core.List<$0.SpeakerInfo> get speakers => $_getList(0);
}
class RoomUpdate extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'RoomUpdate',
package: const $pb.PackageName(
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'livekit'),
createEmptyInstance: create)
..aOM<$0.Room>(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'room',
subBuilder: $0.Room.create)
..hasRequiredFields = false;
RoomUpdate._() : super();
factory RoomUpdate({
$0.Room? room,
}) {
final _result = create();
if (room != null) {
_result.room = room;
}
return _result;
}
factory RoomUpdate.fromBuffer($core.List<$core.int> i,
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(i, r);
factory RoomUpdate.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')
RoomUpdate clone() => RoomUpdate()..mergeFromMessage(this);
@$core.Deprecated('Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
RoomUpdate copyWith(void Function(RoomUpdate) updates) =>
super.copyWith((message) => updates(message as RoomUpdate))
as RoomUpdate; // ignore: deprecated_member_use
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static RoomUpdate create() => RoomUpdate._();
RoomUpdate createEmptyInstance() => create();
static $pb.PbList<RoomUpdate> createRepeated() => $pb.PbList<RoomUpdate>();
@$core.pragma('dart2js:noInline')
static RoomUpdate getDefault() =>
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<RoomUpdate>(create);
static RoomUpdate? _defaultInstance;
@$pb.TagNumber(1)
$0.Room get room => $_getN(0);
@$pb.TagNumber(1)
set room($0.Room v) {
setField(1, v);
}
@$pb.TagNumber(1)
$core.bool hasRoom() => $_has(0);
@$pb.TagNumber(1)
void clearRoom() => clearField(1);
@$pb.TagNumber(1)
$0.Room ensureRoom() => $_ensure(0);
}
+41 -10
View File
@@ -186,15 +186,6 @@ const SignalResponse$json = const {
'9': 0, '9': 0,
'10': 'trackPublished' '10': 'trackPublished'
}, },
const {
'1': 'speaker',
'3': 7,
'4': 1,
'5': 11,
'6': '.livekit.ActiveSpeakerUpdate',
'9': 0,
'10': 'speaker'
},
const { const {
'1': 'leave', '1': 'leave',
'3': 8, '3': 8,
@@ -213,6 +204,24 @@ const SignalResponse$json = const {
'9': 0, '9': 0,
'10': 'mute' '10': 'mute'
}, },
const {
'1': 'speakers_changed',
'3': 10,
'4': 1,
'5': 11,
'6': '.livekit.SpeakersChanged',
'9': 0,
'10': 'speakersChanged'
},
const {
'1': 'room_update',
'3': 11,
'4': 1,
'5': 11,
'6': '.livekit.RoomUpdate',
'9': 0,
'10': 'roomUpdate'
},
], ],
'8': const [ '8': const [
const {'1': 'message'}, const {'1': 'message'},
@@ -221,7 +230,7 @@ const SignalResponse$json = const {
/// Descriptor for `SignalResponse`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `SignalResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List signalResponseDescriptor = $convert.base64Decode( final $typed_data.Uint8List signalResponseDescriptor = $convert.base64Decode(
'Cg5TaWduYWxSZXNwb25zZRIrCgRqb2luGAEgASgLMhUubGl2ZWtpdC5Kb2luUmVzcG9uc2VIAFIEam9pbhI1CgZhbnN3ZXIYAiABKAsyGy5saXZla2l0LlNlc3Npb25EZXNjcmlwdGlvbkgAUgZhbnN3ZXISMwoFb2ZmZXIYAyABKAsyGy5saXZla2l0LlNlc3Npb25EZXNjcmlwdGlvbkgAUgVvZmZlchIzCgd0cmlja2xlGAQgASgLMhcubGl2ZWtpdC5Ucmlja2xlUmVxdWVzdEgAUgd0cmlja2xlEjQKBnVwZGF0ZRgFIAEoCzIaLmxpdmVraXQuUGFydGljaXBhbnRVcGRhdGVIAFIGdXBkYXRlEkoKD3RyYWNrX3B1Ymxpc2hlZBgGIAEoCzIfLmxpdmVraXQuVHJhY2tQdWJsaXNoZWRSZXNwb25zZUgAUg50cmFja1B1Ymxpc2hlZBI4CgdzcGVha2VyGAcgASgLMhwubGl2ZWtpdC5BY3RpdmVTcGVha2VyVXBkYXRlSABSB3NwZWFrZXISLQoFbGVhdmUYCCABKAsyFS5saXZla2l0LkxlYXZlUmVxdWVzdEgAUgVsZWF2ZRIvCgRtdXRlGAkgASgLMhkubGl2ZWtpdC5NdXRlVHJhY2tSZXF1ZXN0SABSBG11dGVCCQoHbWVzc2FnZQ=='); 'Cg5TaWduYWxSZXNwb25zZRIrCgRqb2luGAEgASgLMhUubGl2ZWtpdC5Kb2luUmVzcG9uc2VIAFIEam9pbhI1CgZhbnN3ZXIYAiABKAsyGy5saXZla2l0LlNlc3Npb25EZXNjcmlwdGlvbkgAUgZhbnN3ZXISMwoFb2ZmZXIYAyABKAsyGy5saXZla2l0LlNlc3Npb25EZXNjcmlwdGlvbkgAUgVvZmZlchIzCgd0cmlja2xlGAQgASgLMhcubGl2ZWtpdC5Ucmlja2xlUmVxdWVzdEgAUgd0cmlja2xlEjQKBnVwZGF0ZRgFIAEoCzIaLmxpdmVraXQuUGFydGljaXBhbnRVcGRhdGVIAFIGdXBkYXRlEkoKD3RyYWNrX3B1Ymxpc2hlZBgGIAEoCzIfLmxpdmVraXQuVHJhY2tQdWJsaXNoZWRSZXNwb25zZUgAUg50cmFja1B1Ymxpc2hlZBItCgVsZWF2ZRgIIAEoCzIVLmxpdmVraXQuTGVhdmVSZXF1ZXN0SABSBWxlYXZlEi8KBG11dGUYCSABKAsyGS5saXZla2l0Lk11dGVUcmFja1JlcXVlc3RIAFIEbXV0ZRJFChBzcGVha2Vyc19jaGFuZ2VkGAogASgLMhgubGl2ZWtpdC5TcGVha2Vyc0NoYW5nZWRIAFIPc3BlYWtlcnNDaGFuZ2VkEjYKC3Jvb21fdXBkYXRlGAsgASgLMhMubGl2ZWtpdC5Sb29tVXBkYXRlSABSCnJvb21VcGRhdGVCCQoHbWVzc2FnZQ==');
@$core.Deprecated('Use addTrackRequestDescriptor instead') @$core.Deprecated('Use addTrackRequestDescriptor instead')
const AddTrackRequest$json = const { const AddTrackRequest$json = const {
'1': 'AddTrackRequest', '1': 'AddTrackRequest',
@@ -402,3 +411,25 @@ const ICEServer$json = const {
/// Descriptor for `ICEServer`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `ICEServer`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List iCEServerDescriptor = $convert.base64Decode( final $typed_data.Uint8List iCEServerDescriptor = $convert.base64Decode(
'CglJQ0VTZXJ2ZXISEgoEdXJscxgBIAMoCVIEdXJscxIaCgh1c2VybmFtZRgCIAEoCVIIdXNlcm5hbWUSHgoKY3JlZGVudGlhbBgDIAEoCVIKY3JlZGVudGlhbA=='); 'CglJQ0VTZXJ2ZXISEgoEdXJscxgBIAMoCVIEdXJscxIaCgh1c2VybmFtZRgCIAEoCVIIdXNlcm5hbWUSHgoKY3JlZGVudGlhbBgDIAEoCVIKY3JlZGVudGlhbA==');
@$core.Deprecated('Use speakersChangedDescriptor instead')
const SpeakersChanged$json = const {
'1': 'SpeakersChanged',
'2': const [
const {'1': 'speakers', '3': 1, '4': 3, '5': 11, '6': '.livekit.SpeakerInfo', '10': 'speakers'},
],
};
/// Descriptor for `SpeakersChanged`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List speakersChangedDescriptor = $convert.base64Decode(
'Cg9TcGVha2Vyc0NoYW5nZWQSMAoIc3BlYWtlcnMYASADKAsyFC5saXZla2l0LlNwZWFrZXJJbmZvUghzcGVha2Vycw==');
@$core.Deprecated('Use roomUpdateDescriptor instead')
const RoomUpdate$json = const {
'1': 'RoomUpdate',
'2': const [
const {'1': 'room', '3': 1, '4': 1, '5': 11, '6': '.livekit.Room', '10': 'room'},
],
};
/// Descriptor for `RoomUpdate`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List roomUpdateDescriptor =
$convert.base64Decode('CgpSb29tVXBkYXRlEiEKBHJvb20YASABKAsyDS5saXZla2l0LlJvb21SBHJvb20=');
+94 -88
View File
@@ -1,10 +1,11 @@
import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'classes/change_notifier.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'constants.dart'; import 'constants.dart';
import 'errors.dart';
import 'events.dart'; import 'events.dart';
import 'exceptions.dart';
import 'extensions.dart'; import 'extensions.dart';
import 'logger.dart'; import 'logger.dart';
import 'managers/event.dart'; import 'managers/event.dart';
@@ -15,9 +16,8 @@ import 'participant/remote_participant.dart';
import 'proto/livekit_models.pb.dart' as lk_models; import 'proto/livekit_models.pb.dart' as lk_models;
import 'proto/livekit_rtc.pb.dart' as lk_rtc; import 'proto/livekit_rtc.pb.dart' as lk_rtc;
import 'rtc_engine.dart'; import 'rtc_engine.dart';
import 'signal_client.dart'; import 'support/disposable.dart';
import 'track/track.dart'; import 'track/track.dart';
import 'track/track_publication.dart';
import 'types.dart'; import 'types.dart';
/// Room is the primary construct for LiveKit conferences. It contains a /// Room is the primary construct for LiveKit conferences. It contains a
@@ -29,14 +29,14 @@ import 'types.dart';
/// * participant membership changes /// * participant membership changes
/// * active speakers are different /// * active speakers are different
/// {@category Room} /// {@category Room}
class Room extends LKChangeNotifier { class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
// Room is only instantiated if connected, so defaults to connected. // Room is only instantiated if connected, so defaults to connected.
ConnectionState _connectionState = ConnectionState.connected; ConnectionState _connectionState = ConnectionState.connected;
/// connection state of the room /// connection state of the room
ConnectionState get connectionState => _connectionState; ConnectionState get connectionState => _connectionState;
final Map<String, RemoteParticipant> _participants = {}; final _participants = <String, RemoteParticipant>{};
/// map of SID to RemoteParticipant /// map of SID to RemoteParticipant
UnmodifiableMapView<String, RemoteParticipant> get participants => UnmodifiableMapView<String, RemoteParticipant> get participants =>
@@ -46,10 +46,10 @@ class Room extends LKChangeNotifier {
late final LocalParticipant localParticipant; late final LocalParticipant localParticipant;
/// name of the room /// name of the room
late final String name; final String name;
/// sid of the room /// sid of the room
late final String sid; final String sid;
List<Participant> _activeSpeakers = []; List<Participant> _activeSpeakers = [];
@@ -60,8 +60,7 @@ class Room extends LKChangeNotifier {
final RTCEngine engine; final RTCEngine engine;
// suppport for multiple event listeners // suppport for multiple event listeners
final events = EventsEmitter<RoomEvent>(); late final _engineListener = engine.createListener();
late final _engineListener = EventsListener<EngineEvent>(engine.events);
/// internal use /// internal use
/// {@nodoc} /// {@nodoc}
@@ -69,7 +68,8 @@ class Room extends LKChangeNotifier {
required this.engine, required this.engine,
required lk_rtc.JoinResponse joinResponse, required lk_rtc.JoinResponse joinResponse,
ConnectOptions? connectOptions, ConnectOptions? connectOptions,
}) { }) : sid = joinResponse.room.sid,
name = joinResponse.room.name {
// //
_setUpListeners(); _setUpListeners();
@@ -80,9 +80,6 @@ class Room extends LKChangeNotifier {
roomEvents: events, roomEvents: events,
); );
sid = joinResponse.room.sid;
name = joinResponse.room.name;
for (final info in joinResponse.otherParticipants) { for (final info in joinResponse.otherParticipants) {
_getOrCreateRemoteParticipant(info.sid, info); _getOrCreateRemoteParticipant(info.sid, info);
} }
@@ -92,20 +89,17 @@ class Room extends LKChangeNotifier {
logger.fine('[RoomEvent] $event, will notifyListeners()'); logger.fine('[RoomEvent] $event, will notifyListeners()');
notifyListeners(); notifyListeners();
}); });
}
@override onDispose(() async {
Future<void> dispose() async { // dispose events
// dispose local participant await events.dispose();
await localParticipant.dispose(); // dispose local participant
// dispose Room's events emitter await localParticipant.dispose();
await events.dispose(); // dispose all listeners for RTCEngine
// dispose all listeners for RTCEngine await _engineListener.dispose();
await _engineListener.dispose(); // dispose the engine
// dispose the engine await engine.dispose();
await engine.dispose(); });
super.dispose();
} }
static Future<Room> connect( static Future<Room> connect(
@@ -116,7 +110,6 @@ class Room extends LKChangeNotifier {
}) async { }) async {
// //
final engine = RTCEngine( final engine = RTCEngine(
SignalClient(),
rtcConfig, rtcConfig,
); );
@@ -167,16 +160,16 @@ class Room extends LKChangeNotifier {
..on<EngineReconnectedEvent>((event) async { ..on<EngineReconnectedEvent>((event) async {
_connectionState = ConnectionState.connected; _connectionState = ConnectionState.connected;
events.emit(const RoomReconnectedEvent()); events.emit(const RoomReconnectedEvent());
notifyListeners();
}) })
..on<EngineReconnectingEvent>((event) async { ..on<EngineReconnectingEvent>((event) async {
_connectionState = ConnectionState.reconnecting; _connectionState = ConnectionState.reconnecting;
events.emit(const RoomReconnectingEvent()); events.emit(const RoomReconnectingEvent());
notifyListeners();
}) })
..on<EngineDisconnectedEvent>((event) => _onDisconnectedEvent()) ..on<EngineDisconnectedEvent>((event) => _handleClose())
..on<EngineParticipantUpdateEvent>((event) => _onParticipantUpdateEvent(event.participants)) ..on<SignalParticipantUpdateEvent>((event) => _onParticipantUpdateEvent(event.participants))
..on<EngineSpeakersUpdateEvent>((event) => _onSpeakerUpdateEvent(event.speakers)) ..on<EngineActiveSpeakersUpdateEvent>(
(event) => _onEngineActiveSpeakersUpdateEvent(event.speakers))
..on<SignalSpeakersChangedEvent>((event) => _onSignalSpeakersChangedEvent(event.speakers))
..on<EngineDataPacketReceivedEvent>(_onDataMessageEvent) ..on<EngineDataPacketReceivedEvent>(_onDataMessageEvent)
..on<EngineRemoteMuteChangedEvent>((event) async { ..on<EngineRemoteMuteChangedEvent>((event) async {
final track = localParticipant.trackPublications[event.sid]; final track = localParticipant.trackPublications[event.sid];
@@ -210,8 +203,10 @@ class Room extends LKChangeNotifier {
/// Disconnects from the room, notifying server of disconnection. /// Disconnects from the room, notifying server of disconnection.
Future<void> disconnect() async { Future<void> disconnect() async {
engine.signalClient.sendLeave(); if (_connectionState != ConnectionState.disconnected) {
await _onDisconnectedEvent(); engine.signalClient.sendLeave();
}
await _handleClose();
} }
Future<void> reconnect() async { Future<void> reconnect() async {
@@ -244,42 +239,36 @@ class Room extends LKChangeNotifier {
return participant; return participant;
} }
Future<void> _onDisconnectedEvent() async { // there should be no problem calling this method multiple times
Future<void> _handleClose() async {
logger.fine('[$objectId] _handleClose()');
if (_connectionState == ConnectionState.disconnected) { if (_connectionState == ConnectionState.disconnected) {
logger.fine('$objectId: _handleDisconnect() already disconnected'); logger.warning('[$objectId]: close() already disconnected');
return;
} }
// we need to flag room as disconnected immediately to avoid
// this method firing multiple times since the following code
// is being awaited
_connectionState = ConnectionState.disconnected;
// clean up RemoteParticipants // clean up RemoteParticipants
for (final _ in _participants.values) { for (final _ in _participants.values.toList()) {
// RemoteParticipant is responsible for disposing resources // RemoteParticipant is responsible for disposing resources
await _.unpublishAllTracks();
await _.dispose(); await _.dispose();
} }
_participants.clear(); _participants.clear();
// clean up LocalParticipant // clean up LocalParticipant
// for (final pub in localParticipant.tracks.values) {
// await pub.track?.stop();
// }
await localParticipant.unpublishAllTracks(); await localParticipant.unpublishAllTracks();
// await localParticipant.dispose(); // clean up engine
// localParticipant = null;
await engine.close(); await engine.close();
_activeSpeakers.clear(); _activeSpeakers.clear();
notifyListeners(); // only notify if was not disconnected
events.emit(const RoomDisconnectedEvent()); if (_connectionState != ConnectionState.disconnected) {
_connectionState = ConnectionState.disconnected;
events.emit(const RoomDisconnectedEvent());
}
} }
void _onParticipantUpdateEvent(List<lk_models.ParticipantInfo> updates) async { Future<void> _onParticipantUpdateEvent(List<lk_models.ParticipantInfo> updates) async {
// trigger change notifier only if list of participants membership is changed // trigger change notifier only if list of participants membership is changed
var hasChanged = false; var hasChanged = false;
for (final info in updates) { for (final info in updates) {
@@ -290,7 +279,7 @@ class Room extends LKChangeNotifier {
if (info.state == lk_models.ParticipantInfo_State.DISCONNECTED) { if (info.state == lk_models.ParticipantInfo_State.DISCONNECTED) {
hasChanged = true; hasChanged = true;
_handleParticipantDisconnect(info.sid); await _handleParticipantDisconnect(info.sid);
continue; continue;
} }
@@ -310,43 +299,63 @@ class Room extends LKChangeNotifier {
} }
} }
void _onSpeakerUpdateEvent(List<lk_models.SpeakerInfo> speakers) { void _onSignalSpeakersChangedEvent(List<lk_models.SpeakerInfo> speakers) {
final seenSids = <String>{}; //
List<Participant> newSpeakers = []; final lastSpeakers = {
for (final info in speakers) { for (final p in _activeSpeakers) p.sid: p,
seenSids.add(info.sid); };
if (info.sid == localParticipant.sid) { for (final speaker in speakers) {
localParticipant.audioLevel = info.level; Participant? p = _participants[speaker.sid];
localParticipant.isSpeaking = true; if (speaker.sid == localParticipant.sid) p = localParticipant;
newSpeakers.add(localParticipant); if (p == null) continue;
continue;
}
final participant = participants[info.sid]; p.audioLevel = speaker.level;
if (participant != null) { p.isSpeaking = speaker.active;
participant.audioLevel = info.level; if (speaker.active) {
participant.isSpeaking = true; lastSpeakers[speaker.sid] = p;
newSpeakers.add(participant); } else {
lastSpeakers.remove(speaker.sid);
} }
} }
// clear previous speakers final activeSpeakers = lastSpeakers.values.toList();
if (seenSids.contains(localParticipant.sid)) { activeSpeakers.sort((a, b) => b.audioLevel.compareTo(a.audioLevel));
localParticipant.audioLevel = 0; _activeSpeakers = activeSpeakers;
localParticipant.isSpeaking = false; events.emit(ActiveSpeakersChangedEvent(speakers: activeSpeakers));
} }
for (final participant in _participants.values) {
if (!seenSids.contains(participant.sid)) { // from data channel
participant.audioLevel = 0; // updates are sent only when there's a change to speaker ordering
participant.isSpeaking = false; void _onEngineActiveSpeakersUpdateEvent(List<lk_models.SpeakerInfo> speakers) {
List<Participant> activeSpeakers = [];
// localParticipant & remote participants
final allParticipants = <String, Participant>{
localParticipant.sid: localParticipant,
..._participants,
};
for (final speaker in speakers) {
final p = allParticipants[speaker.sid];
if (p != null) {
p.audioLevel = speaker.level;
p.isSpeaking = true;
activeSpeakers.add(p);
} }
} }
events.emit(ActiveSpeakersChangedEvent(speakers: newSpeakers)); // clear if not in the speakers list
final speakerSids = speakers.map((e) => e.sid).toSet();
for (final p in allParticipants.values) {
if (!speakerSids.contains(p.sid)) {
p.audioLevel = 0;
p.isSpeaking = false;
}
}
_activeSpeakers = newSpeakers; _activeSpeakers = activeSpeakers;
notifyListeners(); events.emit(ActiveSpeakersChangedEvent(speakers: activeSpeakers));
} }
void _onDataMessageEvent(EngineDataPacketReceivedEvent dataPacketEvent) { void _onDataMessageEvent(EngineDataPacketReceivedEvent dataPacketEvent) {
@@ -368,16 +377,13 @@ class Room extends LKChangeNotifier {
events.emit(event); events.emit(event);
} }
void _handleParticipantDisconnect(String sid) { Future<void> _handleParticipantDisconnect(String sid) async {
final participant = _participants.remove(sid); final participant = _participants.remove(sid);
if (participant == null) { if (participant == null) {
return; return;
} }
final toRemove = List<TrackPublication>.from(participant.trackPublications.values); await participant.unpublishAllTracks(notify: true);
for (final track in toRemove) {
participant.unpublishTrack(track.sid, notify: true);
}
events.emit(ParticipantDisconnectedEvent(participant: participant)); events.emit(ParticipantDisconnectedEvent(participant: participant));
} }
+111 -119
View File
@@ -3,10 +3,11 @@ import 'dart:async';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
import 'package:meta/meta.dart';
import 'constants.dart'; import 'constants.dart';
import 'errors.dart';
import 'events.dart'; import 'events.dart';
import 'exceptions.dart';
import 'extensions.dart'; import 'extensions.dart';
import 'logger.dart'; import 'logger.dart';
import 'managers/delay.dart'; import 'managers/delay.dart';
@@ -15,10 +16,11 @@ import 'options.dart';
import 'proto/livekit_models.pb.dart' as lk_models; import 'proto/livekit_models.pb.dart' as lk_models;
import 'proto/livekit_rtc.pb.dart' as lk_rtc; import 'proto/livekit_rtc.pb.dart' as lk_rtc;
import 'signal_client.dart'; import 'signal_client.dart';
import 'support/disposable.dart';
import 'transport.dart'; import 'transport.dart';
import 'types.dart'; import 'types.dart';
class RTCEngine { class RTCEngine extends Disposable with EventsEmittable<EngineEvent> {
static const _lossyDCLabel = '_lossy'; static const _lossyDCLabel = '_lossy';
static const _reliableDCLabel = '_reliable'; static const _reliableDCLabel = '_reliable';
static const _maxReconnectAttempts = 5; static const _maxReconnectAttempts = 5;
@@ -27,19 +29,25 @@ class RTCEngine {
// config for RTCPeerConnection // config for RTCPeerConnection
final RTCConfiguration? rtcConfig; final RTCConfiguration? rtcConfig;
@internal
PCTransport? publisher; PCTransport? publisher;
@internal
PCTransport? subscriber; PCTransport? subscriber;
@internal
PCTransport? get primary => _subscriberPrimary ? subscriber : publisher; PCTransport? get primary => _subscriberPrimary ? subscriber : publisher;
// used for ice state notifications
CancelListenFunc? _primaryIceStateListener;
// data channels for packets // data channels for packets
rtc.RTCDataChannel? reliableDC; rtc.RTCDataChannel? _reliableDC;
rtc.RTCDataChannel? lossyDC; rtc.RTCDataChannel? _lossyDC;
bool iceConnected = false; bool _iceConnected = false;
bool isReconnecting = false;
bool isClosed = true; ConnectionState _connectionState = ConnectionState.disconnected;
/// connection state of the room
ConnectionState get connectionState => _connectionState;
// true if publisher connection has already been established. // true if publisher connection has already been established.
// this is helpful to know if we need to restart ICE on the publisher connection // this is helpful to know if we need to restart ICE on the publisher connection
bool _hasPublished = false; bool _hasPublished = false;
@@ -55,41 +63,29 @@ class RTCEngine {
// internal // internal
int _reconnectAttempts = 0; int _reconnectAttempts = 0;
final events = EventsEmitter<EngineEvent>(); late final _signalListener = signalClient.createListener(synchronized: true);
late final _signalListener = EventsListener<SignalEvent>(signalClient.events, synchronized: true);
final delays = CancelableDelayManager(); final delays = CancelableDelayManager();
// late final Timer _statsTimer;
RTCEngine( RTCEngine(
this.signalClient, this.rtcConfig, {
this.rtcConfig, SignalClient? signalClient,
) { }) : signalClient = signalClient ?? SignalClient() {
if (kDebugMode) { if (kDebugMode) {
// log all EngineEvents // log all EngineEvents
events.listen((event) => logger.fine('[EngineEvent] $objectId ${event.runtimeType}')); events.listen((event) => logger.fine('[EngineEvent] $objectId ${event.runtimeType}'));
} }
_setUpListeners(); _setUpListeners();
// _statsTimer = Timer.periodic(const Duration(seconds: 1), _onStatTimer);
onDispose(() async {
await events.dispose();
await delays.dispose();
await close();
await _signalListener.dispose();
});
} }
Future<void> dispose() async {
await events.dispose();
await _signalListener.dispose();
}
// void _onStatTimer(Timer _) async {
// //
// final stats = await publisher?.pc.getStats();
// if (stats == null || stats.isEmpty) return;
// for (final s in stats) {
// logger.fine('STATS ${s.values}');
// }
// }
Future<lk_rtc.JoinResponse> join( Future<lk_rtc.JoinResponse> join(
String url, String url,
String token, { String token, {
@@ -110,22 +106,15 @@ class RTCEngine {
return event.response; return event.response;
} }
// there is no side-effect calling this method multiple times
Future<void> close() async { Future<void> close() async {
logger.fine('${objectId} close()'); logger.fine('[$objectId] close()');
if (isClosed) { if (_connectionState == ConnectionState.disconnected) {
logger.fine('${objectId} close() already closed'); logger.warning('[$objectId]: close() already disconnected');
return;
} }
isClosed = true;
// _statsTimer.cancel(); // _statsTimer.cancel();
// cancel events
await _primaryIceStateListener?.call();
_primaryIceStateListener = null;
// cancel all ongoing delays // cancel all ongoing delays
await delays.dispose(); await delays.cancelAll();
// PCTransport is responsible for disposing RTCPeerConnection // PCTransport is responsible for disposing RTCPeerConnection
await publisher?.dispose(); await publisher?.dispose();
@@ -134,7 +123,10 @@ class RTCEngine {
await subscriber?.dispose(); await subscriber?.dispose();
subscriber = null; subscriber = null;
signalClient.close(); await signalClient.close();
_connectionState = ConnectionState.disconnected;
// notifyListeners();
} }
Future<lk_models.TrackInfo> addTrack({ Future<lk_models.TrackInfo> addTrack({
@@ -174,10 +166,10 @@ class RTCEngine {
final dcMessage = rtc.RTCDataChannelMessage.fromBinary(packet.writeToBuffer()); final dcMessage = rtc.RTCDataChannelMessage.fromBinary(packet.writeToBuffer());
if (packet.kind == lk_models.DataPacket_Kind.LOSSY && lossyDC != null) { if (packet.kind == lk_models.DataPacket_Kind.LOSSY && _lossyDC != null) {
await lossyDC?.send(dcMessage); await _lossyDC?.send(dcMessage);
} else if (packet.kind == lk_models.DataPacket_Kind.RELIABLE && reliableDC != null) { } else if (packet.kind == lk_models.DataPacket_Kind.RELIABLE && _reliableDC != null) {
await reliableDC?.send(dcMessage); await _reliableDC?.send(dcMessage);
} }
} }
@@ -188,7 +180,7 @@ class RTCEngine {
} }
if (publisher?.pc.iceConnectionState?.isConnected() == true) { if (publisher?.pc.iceConnectionState?.isConnected() == true) {
logger.warning('publisher is already connected'); logger.warning('[$objectId] publisher is already connected');
return; return;
} }
@@ -207,7 +199,7 @@ class RTCEngine {
} }
Future<void> reconnect() async { Future<void> reconnect() async {
if (isClosed) { if (_connectionState == ConnectionState.disconnected) {
logger.fine('$objectId reconnect() already closed'); logger.fine('$objectId reconnect() already closed');
return; return;
} }
@@ -225,7 +217,8 @@ class RTCEngine {
_reconnectAttempts++; _reconnectAttempts++;
try { try {
isReconnecting = true; // isReconnecting = true;
_connectionState = ConnectionState.reconnecting;
await signalClient.reconnect(url, token); await signalClient.reconnect(url, token);
if (publisher == null || subscriber == null) { if (publisher == null || subscriber == null) {
@@ -256,7 +249,8 @@ class RTCEngine {
// don't catch and pass up any exception // don't catch and pass up any exception
} finally { } finally {
// always set reconnecting to false // always set reconnecting to false
isReconnecting = false; // isReconnecting = false;
_connectionState = ConnectionState.disconnected;
} }
} }
@@ -296,31 +290,25 @@ class RTCEngine {
subscriber?.pc.onDataChannel = _onDataChannel; subscriber?.pc.onDataChannel = _onDataChannel;
} }
// logger.fine('subscriber.pc: ${subscriber?.pc}'); subscriber?.pc.onIceConnectionState =
subscriber?.pc.onIceConnectionState = (state) { (state) => events.emit(EngineSubscriberIceStateUpdatedEvent(
// state: state,
events.emit(EngineSubscriberIceStateUpdatedEvent( isPrimary: _subscriberPrimary,
state: state, ));
isPrimary: _subscriberPrimary,
));
};
publisher?.pc.onIceConnectionState = (state) { publisher?.pc.onIceConnectionState = (state) => events.emit(EnginePublisherIceStateUpdatedEvent(
// state: state,
events.emit(EnginePublisherIceStateUpdatedEvent( isPrimary: !_subscriberPrimary,
state: state, ));
isPrimary: !_subscriberPrimary,
));
};
_primaryIceStateListener ??= events.on<EngineIceStateUpdatedEvent>((event) { events.on<EngineIceStateUpdatedEvent>((event) {
// only listen to primary ice events // only listen to primary ice events
if (!event.isPrimary) return; if (!event.isPrimary) return;
if (event.iceState == rtc.RTCIceConnectionState.RTCIceConnectionStateConnected) { if (event.iceState == rtc.RTCIceConnectionState.RTCIceConnectionStateConnected) {
if (!iceConnected) { if (!_iceConnected) {
iceConnected = true; _iceConnected = true;
if (isReconnecting) { if (_connectionState == ConnectionState.reconnecting) {
events.emit(const EngineReconnectedEvent()); events.emit(const EngineReconnectedEvent());
} else { } else {
events.emit(const EngineConnectedEvent()); events.emit(const EngineConnectedEvent());
@@ -328,8 +316,8 @@ class RTCEngine {
} }
} else if (event.iceState == rtc.RTCIceConnectionState.RTCIceConnectionStateFailed) { } else if (event.iceState == rtc.RTCIceConnectionState.RTCIceConnectionStateFailed) {
// trigger reconnect sequence // trigger reconnect sequence
if (iceConnected) { if (_iceConnected) {
iceConnected = false; _iceConnected = false;
_onDisconnected('peerconnection'); _onDisconnected('peerconnection');
} }
} }
@@ -350,34 +338,40 @@ class RTCEngine {
)); ));
}; };
// data channels
final lossyInit = rtc.RTCDataChannelInit()
..binaryType = 'binary'
..ordered = true
..maxRetransmits = 0;
lossyDC = await publisher?.pc.createDataChannel(_lossyDCLabel, lossyInit);
final reliableInit = rtc.RTCDataChannelInit()
..binaryType = 'binary'
..ordered = true;
reliableDC = await publisher?.pc.createDataChannel(_reliableDCLabel, reliableInit);
// also handle messages over the pub channel, for backwards compatibility // also handle messages over the pub channel, for backwards compatibility
lossyDC?.onMessage = _onDCMessage; try {
reliableDC?.onMessage = _onDCMessage; final lossyInit = rtc.RTCDataChannelInit()
..binaryType = 'binary'
..ordered = true
..maxRetransmits = 0;
_lossyDC = await publisher?.pc.createDataChannel(_lossyDCLabel, lossyInit);
_lossyDC?.onMessage = _onDCMessage;
} catch (_) {
logger.severe('[$objectId] createDataChannel() did throw $_');
}
try {
final reliableInit = rtc.RTCDataChannelInit()
..binaryType = 'binary'
..ordered = true;
_reliableDC = await publisher?.pc.createDataChannel(_reliableDCLabel, reliableInit);
_reliableDC?.onMessage = _onDCMessage;
} catch (_) {
logger.severe('[$objectId] createDataChannel() did throw $_');
}
} }
void _onDataChannel(rtc.RTCDataChannel dc) { void _onDataChannel(rtc.RTCDataChannel dc) {
switch (dc.label) { switch (dc.label) {
case _reliableDCLabel: case _reliableDCLabel:
logger.fine('Server opened DC label: ${dc.label}'); logger.fine('Server opened DC label: ${dc.label}');
reliableDC = dc; _reliableDC = dc;
reliableDC?.onMessage = _onDCMessage; _reliableDC?.onMessage = _onDCMessage;
break; break;
case _lossyDCLabel: case _lossyDCLabel:
logger.fine('Server opened DC label: ${dc.label}'); logger.fine('Server opened DC label: ${dc.label}');
lossyDC = dc; _lossyDC = dc;
lossyDC?.onMessage = _onDCMessage; _lossyDC?.onMessage = _onDCMessage;
break; break;
default: default:
logger.warning('Unknown DC label: ${dc.label}'); logger.warning('Unknown DC label: ${dc.label}');
@@ -395,7 +389,7 @@ class RTCEngine {
final dp = lk_models.DataPacket.fromBuffer(message.binary); final dp = lk_models.DataPacket.fromBuffer(message.binary);
if (dp.whichValue() == lk_models.DataPacket_Value.speaker) { if (dp.whichValue() == lk_models.DataPacket_Value.speaker) {
// Speaker packet // Speaker packet
events.emit(EngineSpeakersUpdateEvent(speakers: dp.speaker.speakers)); events.emit(EngineActiveSpeakersUpdateEvent(speakers: dp.speaker.speakers));
} else if (dp.whichValue() == lk_models.DataPacket_Value.user) { } else if (dp.whichValue() == lk_models.DataPacket_Value.user) {
// User packet // User packet
events.emit(EngineDataPacketReceivedEvent( events.emit(EngineDataPacketReceivedEvent(
@@ -406,11 +400,15 @@ class RTCEngine {
} }
Future<void> _onDisconnected(String reason) async { Future<void> _onDisconnected(String reason) async {
if (isClosed) return; if (_connectionState == ConnectionState.disconnected) {
logger.fine('[$objectId] Already disconnected $reason');
return;
}
logger.fine('[$objectId] Disconnected $reason');
logger.fine('disconnected $reason');
if (_reconnectAttempts >= _maxReconnectAttempts) { if (_reconnectAttempts >= _maxReconnectAttempts) {
logger.info('could not connect after $_reconnectAttempts, giving up'); logger.info('[$objectId] Could not connect after ${_reconnectAttempts} attempts, giving up');
await close(); await close();
events.emit(const EngineDisconnectedEvent()); events.emit(const EngineDisconnectedEvent());
return; return;
@@ -432,12 +430,10 @@ class RTCEngine {
}); });
} }
//------------------ SignalClient Delegate methods -------------------------//
void _setUpListeners() => _signalListener void _setUpListeners() => _signalListener
..on<SignalConnectedEvent>((event) async { ..on<SignalConnectedEvent>((event) async {
// create peer connections // create peer connections
isClosed = false; _connectionState = ConnectionState.connected;
_subscriberPrimary = event.response.subscriberPrimary; _subscriberPrimary = event.response.subscriberPrimary;
_providedIceServers = event.response.iceServers; _providedIceServers = event.response.iceServers;
@@ -451,28 +447,30 @@ class RTCEngine {
// for subscriberPrimary, we negotiate when necessary (lazy) // for subscriberPrimary, we negotiate when necessary (lazy)
await negotiate(); await negotiate();
} }
// _joinCompleter?.complete(Future.value(event.response));
// _joinCompleter = null;
}) })
..on<SignalCloseEvent>((_) async { ..on<SignalCloseEvent>((_) async {
await _onDisconnected('signal'); await _onDisconnected('signal');
}) })
..on<SignalOfferEvent>((event) async { ..on<SignalOfferEvent>((event) async {
if (subscriber == null) { if (subscriber == null) {
logger.warning('[$objectId] subscriber is null');
return; return;
} }
logger.fine('received server offer(type: ${event.sd.type}, ' logger.fine('[$objectId] Received server offer(type: ${event.sd.type}, '
'${subscriber!.pc.signalingState})'); '${subscriber!.pc.signalingState})');
await subscriber!.setRemoteDescription(event.sd); await subscriber!.setRemoteDescription(event.sd);
final answer = await subscriber!.pc.createAnswer(); try {
logger.fine('Created answer'); final answer = await subscriber!.pc.createAnswer();
logger.finer('sdp: ${answer.sdp}'); logger.fine('Created answer');
await subscriber!.pc.setLocalDescription(answer); logger.finer('sdp: ${answer.sdp}');
signalClient.sendAnswer(answer); await subscriber!.pc.setLocalDescription(answer);
signalClient.sendAnswer(answer);
} catch (_) {
logger.severe('[$objectId] Failed to createAnswer()');
}
}) })
..on<SignalAnswerEvent>((event) async { ..on<SignalAnswerEvent>((event) async {
if (publisher == null) { if (publisher == null) {
@@ -494,16 +492,10 @@ class RTCEngine {
await publisher!.addIceCandidate(event.candidate); await publisher!.addIceCandidate(event.candidate);
} }
}) })
..on<SignalParticipantUpdateEvent>((event) async { // relay
events.emit(EngineParticipantUpdateEvent(participants: event.updates)); ..on<SignalParticipantUpdateEvent>((event) => events.emit(event))
}) // relay
// ..on<SignalLocalTrackPublishedEvent>((event) async { ..on<SignalSpeakersChangedEvent>((event) => events.emit(event))
// final completer = _pendingTrackResolvers.remove(event.cid);
// completer?.complete(event.track);
// })
..on<SignalActiveSpeakersChangedEvent>((event) async {
events.emit(EngineSpeakersUpdateEvent(speakers: event.speakers));
})
..on<SignalLeaveEvent>((event) async { ..on<SignalLeaveEvent>((event) async {
await close(); await close();
events.emit(const EngineDisconnectedEvent()); events.emit(const EngineDisconnectedEvent());
+33 -30
View File
@@ -1,23 +1,23 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'errors.dart';
import 'events.dart'; import 'events.dart';
import 'exceptions.dart';
import 'extensions.dart'; import 'extensions.dart';
import 'logger.dart'; import 'logger.dart';
import 'managers/event.dart'; import 'managers/event.dart';
import 'options.dart'; import 'options.dart';
import 'proto/livekit_models.pb.dart' as lk_models; import 'proto/livekit_models.pb.dart' as lk_models;
import 'proto/livekit_rtc.pb.dart' as lk_rtc; import 'proto/livekit_rtc.pb.dart' as lk_rtc;
import 'support/disposable.dart';
import 'support/websocket.dart';
import 'types.dart'; import 'types.dart';
import 'utils.dart'; import 'utils.dart';
import 'ws/interface.dart';
class SignalClient { class SignalClient extends Disposable with EventsEmittable<SignalEvent> {
final events = EventsEmitter<SignalEvent>(); //
final ProtocolVersion protocol; final ProtocolVersion protocol;
bool _connected = false; bool _connected = false;
@@ -29,6 +29,11 @@ class SignalClient {
events.listen((event) { events.listen((event) {
logger.fine('[SignalEvent] $event'); logger.fine('[SignalEvent] $event');
}); });
onDispose(() async {
await events.dispose();
await close();
});
} }
bool get connected => _connected; bool get connected => _connected;
@@ -51,7 +56,7 @@ class SignalClient {
try { try {
_ws = await LiveKitWebSocket.connect( _ws = await LiveKitWebSocket.connect(
rtcUri, rtcUri,
WebSocketOptions( WebSocketEventHandlers(
onData: _onSocketData, onData: _onSocketData,
onDispose: _onSocketDone, onDispose: _onSocketDone,
onError: _handleError, onError: _handleError,
@@ -71,8 +76,7 @@ class SignalClient {
// Attempt Validation // Attempt Validation
try { try {
final validateResponse = await http.get(validateUri); final validateResponse = await http.get(validateUri);
if (validateResponse.statusCode != 200) if (validateResponse.statusCode != 200) throw ConnectException(validateResponse.body);
throw ConnectException(validateResponse.body);
throw ConnectException(); throw ConnectException();
} catch (error) { } catch (error) {
// Pass it up if it's already a `ConnectError` // Pass it up if it's already a `ConnectError`
@@ -88,7 +92,7 @@ class SignalClient {
String token, String token,
) async { ) async {
_connected = false; _connected = false;
_ws?.dispose(); await _ws?.dispose();
_ws = null; _ws = null;
final rtcUri = Utils.buildUri( final rtcUri = Utils.buildUri(
@@ -100,7 +104,7 @@ class SignalClient {
_ws = await LiveKitWebSocket.connect( _ws = await LiveKitWebSocket.connect(
rtcUri, rtcUri,
WebSocketOptions( WebSocketEventHandlers(
onData: _onSocketData, onData: _onSocketData,
onDispose: _onSocketDone, onDispose: _onSocketDone,
onError: _handleError, onError: _handleError,
@@ -110,24 +114,27 @@ class SignalClient {
_connected = true; _connected = true;
} }
void close() { Future<void> close() async {
_connected = false; _connected = false;
_ws?.dispose(); await _ws?.dispose();
} }
void sendOffer(rtc.RTCSessionDescription offer) => // @override
_sendRequest(lk_rtc.SignalRequest( // Future<void> dispose() async {
// super.dispose();
// await events.dispose();
// await close();
// }
void sendOffer(rtc.RTCSessionDescription offer) => _sendRequest(lk_rtc.SignalRequest(
offer: offer.toSDKType(), offer: offer.toSDKType(),
)); ));
void sendAnswer(rtc.RTCSessionDescription answer) => void sendAnswer(rtc.RTCSessionDescription answer) => _sendRequest(lk_rtc.SignalRequest(
_sendRequest(lk_rtc.SignalRequest(
answer: answer.toSDKType(), answer: answer.toSDKType(),
)); ));
void sendIceCandidate( void sendIceCandidate(rtc.RTCIceCandidate candidate, lk_rtc.SignalTarget target) => _sendRequest(
rtc.RTCIceCandidate candidate, lk_rtc.SignalTarget target) =>
_sendRequest(
lk_rtc.SignalRequest( lk_rtc.SignalRequest(
trickle: lk_rtc.TrickleRequest( trickle: lk_rtc.TrickleRequest(
candidateInit: candidate.toJson(), candidateInit: candidate.toJson(),
@@ -136,8 +143,7 @@ class SignalClient {
), ),
); );
void sendMuteTrack(String trackSid, bool muted) => void sendMuteTrack(String trackSid, bool muted) => _sendRequest(lk_rtc.SignalRequest(
_sendRequest(lk_rtc.SignalRequest(
mute: lk_rtc.MuteTrackRequest( mute: lk_rtc.MuteTrackRequest(
sid: trackSid, sid: trackSid,
muted: muted, muted: muted,
@@ -174,8 +180,7 @@ class SignalClient {
subscription: subscription, subscription: subscription,
)); ));
void sendSetSimulcastLayers( void sendSetSimulcastLayers(String trackSid, List<lk_rtc.VideoQuality> layers) =>
String trackSid, List<lk_rtc.VideoQuality> layers) =>
_sendRequest(lk_rtc.SignalRequest( _sendRequest(lk_rtc.SignalRequest(
simulcast: lk_rtc.SetSimulcastLayers( simulcast: lk_rtc.SetSimulcastLayers(
trackSid: trackSid, trackSid: trackSid,
@@ -188,8 +193,8 @@ class SignalClient {
)); ));
void _sendRequest(lk_rtc.SignalRequest req) { void _sendRequest(lk_rtc.SignalRequest req) {
if (_ws == null) { if (_ws == null || isDisposed) {
logger.warning('could not send message, not connected'); logger.warning('[$objectId] Could not send message, not connected or already disposed');
return; return;
} }
@@ -221,8 +226,7 @@ class SignalClient {
)); ));
break; break;
case lk_rtc.SignalResponse_Message.update: case lk_rtc.SignalResponse_Message.update:
events.emit( events.emit(SignalParticipantUpdateEvent(participants: msg.update.participants));
SignalParticipantUpdateEvent(updates: msg.update.participants));
break; break;
case lk_rtc.SignalResponse_Message.trackPublished: case lk_rtc.SignalResponse_Message.trackPublished:
events.emit(SignalLocalTrackPublishedEvent( events.emit(SignalLocalTrackPublishedEvent(
@@ -230,9 +234,8 @@ class SignalClient {
track: msg.trackPublished.track, track: msg.trackPublished.track,
)); ));
break; break;
case lk_rtc.SignalResponse_Message.speaker: case lk_rtc.SignalResponse_Message.speakersChanged:
events.emit( events.emit(SignalSpeakersChangedEvent(speakers: msg.speakersChanged.speakers));
SignalActiveSpeakersChangedEvent(speakers: msg.speaker.speakers));
break; break;
case lk_rtc.SignalResponse_Message.leave: case lk_rtc.SignalResponse_Message.leave:
events.emit(SignalLeaveEvent(canReconnect: msg.leave.canReconnect)); events.emit(SignalLeaveEvent(canReconnect: msg.leave.canReconnect));
+90
View File
@@ -0,0 +1,90 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:meta/meta.dart';
import '../extensions.dart';
import '../logger.dart';
typedef OnDisposeFunc = Future<void> Function();
mixin _Disposer {
//
final _disposeFuncs = <OnDisposeFunc>[];
bool _isDisposed = false;
bool get isDisposed => _isDisposed;
// last added func will be called first when disposing
void onDispose(OnDisposeFunc func) => _disposeFuncs.add(func);
Future<bool> _dispose() async {
if (!_isDisposed) {
logger.fine('[${objectId}] dispose()');
_isDisposed = true;
if (_disposeFuncs.isNotEmpty) {
logger.fine('[$objectId] running ${_disposeFuncs.length} dispose funcs...');
// call dispose funcs in reverse order
for (final _func in _disposeFuncs.reversed) {
await _func();
}
_disposeFuncs.clear();
logger.fine('[$objectId] dispose complete.');
}
return true;
} else {
logger.warning('[$objectId] unnecessary dispose() called.');
return false;
}
}
}
abstract class Disposable with _Disposer {
@mustCallSuper
Future<bool> dispose() async {
return await _dispose();
}
}
abstract class DisposableChangeNotifier extends ChangeNotifier with _Disposer {
@override
Future<bool> dispose() async {
if (!isDisposed) super.dispose();
return await super._dispose();
}
@override
bool get hasListeners {
if (isDisposed) {
logger.warning('called hasListeners on a disposed ChangeNotifier');
return false;
}
return super.hasListeners;
}
@override
void addListener(VoidCallback listener) {
if (isDisposed) {
logger.warning('called addListener() on a disposed ChangeNotifier');
return;
}
super.addListener(listener);
}
@override
void notifyListeners() {
if (isDisposed) {
logger.warning('called notifyListeners() on a disposed ChangeNotifier');
return;
}
super.notifyListeners();
}
@override
void removeListener(VoidCallback listener) {
if (isDisposed) {
logger.warning('called removeListener() on a disposed ChangeNotifier');
return;
}
super.removeListener(listener);
}
}
+122
View File
@@ -0,0 +1,122 @@
// https://developer.apple.com/documentation/avfaudio/avaudiosession/category
import 'package:flutter/services.dart';
import '../logger.dart';
enum AppleAudioCategory {
soloAmbient,
playback,
record,
playAndRecord,
multiRoute,
}
// https://developer.apple.com/documentation/avfaudio/avaudiosession/categoryoptions
enum AppleAudioCategoryOption {
mixWithOthers, // Only playAndRecord, playback, or multiRoute.
duckOthers, // Only playAndRecord, playback, or multiRoute.
interruptSpokenAudioAndMixWithOthers,
allowBluetooth, // Only playAndRecord or record.
allowBluetoothA2DP,
allowAirPlay,
defaultToSpeaker,
}
// https://developer.apple.com/documentation/avfaudio/avaudiosession/mode
enum AppleAudioMode {
default_,
gameChat,
measurement,
moviePlayback,
spokenAudio,
videoChat,
videoRecording,
voiceChat,
voicePrompt,
}
extension AppleAudioCategoryExt on AppleAudioCategory {
String toStringValue() => <AppleAudioCategory, String>{
AppleAudioCategory.soloAmbient: 'soloAmbient',
AppleAudioCategory.playback: 'playback',
AppleAudioCategory.record: 'record',
AppleAudioCategory.playAndRecord: 'playAndRecord',
AppleAudioCategory.multiRoute: 'multiRoute',
}[this]!;
}
extension AppleAudioCategoryOptionExt on AppleAudioCategoryOption {
String toStringValue() => <AppleAudioCategoryOption, String>{
AppleAudioCategoryOption.mixWithOthers: 'mixWithOthers',
AppleAudioCategoryOption.duckOthers: 'duckOthers',
AppleAudioCategoryOption.interruptSpokenAudioAndMixWithOthers:
'interruptSpokenAudioAndMixWithOthers',
AppleAudioCategoryOption.allowBluetooth: 'allowBluetooth',
AppleAudioCategoryOption.allowBluetoothA2DP: 'allowBluetoothA2DP',
AppleAudioCategoryOption.allowAirPlay: 'allowAirPlay',
AppleAudioCategoryOption.defaultToSpeaker: 'defaultToSpeaker',
}[this]!;
}
extension AppleAudioModeExt on AppleAudioMode {
String toStringValue() => <AppleAudioMode, String>{
AppleAudioMode.default_: 'default',
AppleAudioMode.gameChat: 'gameChat',
AppleAudioMode.measurement: 'measurement',
AppleAudioMode.moviePlayback: 'moviePlayback',
AppleAudioMode.spokenAudio: 'spokenAudio',
AppleAudioMode.videoChat: 'videoChat',
AppleAudioMode.videoRecording: 'videoRecording',
AppleAudioMode.voiceChat: 'voiceChat',
AppleAudioMode.voicePrompt: 'voicePrompt',
}[this]!;
}
class NativeAudioConfiguration {
final AppleAudioCategory? appleAudioCategory;
final Set<AppleAudioCategoryOption>? appleAudioCategoryOptions;
final AppleAudioMode? appleAudioMode;
NativeAudioConfiguration({
// for iOS / Mac
this.appleAudioCategory,
this.appleAudioCategoryOptions,
this.appleAudioMode,
// Android options
// ...
});
Map<String, dynamic> toMap() => <String, dynamic>{
if (appleAudioCategory != null) 'appleAudioCategory': appleAudioCategory!.toStringValue(),
if (appleAudioCategoryOptions != null)
'appleAudioCategoryOptions':
appleAudioCategoryOptions!.map((e) => e.toStringValue()).toList(),
if (appleAudioMode != null) 'appleAudioMode': appleAudioMode!.toStringValue(),
};
NativeAudioConfiguration copyWith({
AppleAudioCategory? appleAudioCategory,
Set<AppleAudioCategoryOption>? appleAudioCategoryOptions,
AppleAudioMode? appleAudioMode,
}) =>
NativeAudioConfiguration(
appleAudioCategory: appleAudioCategory ?? this.appleAudioCategory,
appleAudioCategoryOptions: appleAudioCategoryOptions ?? this.appleAudioCategoryOptions,
appleAudioMode: appleAudioMode ?? this.appleAudioMode,
);
}
const _lkMethodChannel = MethodChannel('livekit_client');
Future<bool> configureNativeAudio(NativeAudioConfiguration configuration) async {
try {
final result = await _lkMethodChannel.invokeMethod<bool>(
'configureNativeAudio',
configuration.toMap(),
);
return result == true;
} catch (_) {
logger.warning('configureAudioSession did throw $_');
return false;
}
}
@@ -2,17 +2,18 @@ import 'dart:async';
import 'dart:io' as io; import 'dart:io' as io;
import '../../logger.dart'; import '../../logger.dart';
import '../interface.dart'; import '../websocket.dart';
import '../../extensions.dart';
Future<LiveKitWebSocketIO> lkWebSocketConnect( Future<LiveKitWebSocketIO> lkWebSocketConnect(
Uri uri, [ Uri uri, [
WebSocketOptions? options, WebSocketEventHandlers? options,
]) => ]) =>
LiveKitWebSocketIO.connect(uri, options); LiveKitWebSocketIO.connect(uri, options);
class LiveKitWebSocketIO implements LiveKitWebSocket { class LiveKitWebSocketIO implements LiveKitWebSocket {
final io.WebSocket _ws; final io.WebSocket _ws;
final WebSocketOptions? options; final WebSocketEventHandlers? options;
late final StreamSubscription _subscription; late final StreamSubscription _subscription;
LiveKitWebSocketIO._( LiveKitWebSocketIO._(
@@ -26,38 +27,38 @@ class LiveKitWebSocketIO implements LiveKitWebSocket {
} }
@override @override
void dispose() { Future<void> dispose() async {
await _subscription.cancel();
await _ws.close();
options?.onDispose?.call(); options?.onDispose?.call();
_subscription.cancel();
_ws.close();
} }
@override @override
void send(List<int> data) { void send(List<int> data) {
// 0 CONNECTING // 0 CONNECTING, 1 OPEN, 2 CLOSING, 3 CLOSED
// 1 OPEN if (_ws.readyState != 1) {
// 2 CLOSING logger.fine('[$objectId] Tried to send data (readyState: ${_ws.readyState})');
// 3 CLOSED return;
if (_ws.readyState == 1) { }
try {
_ws.add(data); try {
} catch (e) { _ws.add(data);
// } catch (_) {
} logger.fine('[$objectId] send did throw ${_}');
} }
} }
static Future<LiveKitWebSocketIO> connect( static Future<LiveKitWebSocketIO> connect(
Uri uri, [ Uri uri, [
WebSocketOptions? options, WebSocketEventHandlers? options,
]) async { ]) async {
logger.fine('WebSocketIO connect (uri: ${uri.toString()})'); logger.fine('[WebSocketIO] Connecting(uri: ${uri.toString()})...');
try { try {
final ws = await io.WebSocket.connect(uri.toString()); final ws = await io.WebSocket.connect(uri.toString());
logger.fine('WebSocketIO connected'); logger.fine('[WebSocketIO] Connected');
return LiveKitWebSocketIO._(ws, options); return LiveKitWebSocketIO._(ws, options);
} catch (_) { } catch (_) {
logger.severe('WebSocketIO error ${_}'); logger.severe('[WebSocketIO] did throw ${_}');
throw WebSocketException.connect(); throw WebSocketException.connect();
} }
} }
@@ -4,17 +4,17 @@ import 'dart:async';
import 'dart:html' as html; import 'dart:html' as html;
import 'dart:typed_data'; import 'dart:typed_data';
import '../interface.dart'; import '../websocket.dart';
Future<LiveKitWebSocketWeb> lkWebSocketConnect( Future<LiveKitWebSocketWeb> lkWebSocketConnect(
Uri uri, [ Uri uri, [
WebSocketOptions? options, WebSocketEventHandlers? options,
]) => ]) =>
LiveKitWebSocketWeb.connect(uri, options); LiveKitWebSocketWeb.connect(uri, options);
class LiveKitWebSocketWeb implements LiveKitWebSocket { class LiveKitWebSocketWeb implements LiveKitWebSocket {
final html.WebSocket _ws; final html.WebSocket _ws;
final WebSocketOptions? options; final WebSocketEventHandlers? options;
late final StreamSubscription _messageSubscription; late final StreamSubscription _messageSubscription;
late final StreamSubscription _closeSubscription; late final StreamSubscription _closeSubscription;
@@ -34,16 +34,16 @@ class LiveKitWebSocketWeb implements LiveKitWebSocket {
void send(List<int> data) => _ws.send(data); void send(List<int> data) => _ws.send(data);
@override @override
void dispose() { Future<void> dispose() async {
options?.onDispose?.call(); options?.onDispose?.call();
_messageSubscription.cancel(); await _messageSubscription.cancel();
_closeSubscription.cancel(); await _closeSubscription.cancel();
_ws.close(); _ws.close();
} }
static Future<LiveKitWebSocketWeb> connect( static Future<LiveKitWebSocketWeb> connect(
Uri uri, [ Uri uri, [
WebSocketOptions? options, WebSocketEventHandlers? options,
]) async { ]) async {
final completer = Completer<LiveKitWebSocketWeb>(); final completer = Completer<LiveKitWebSocketWeb>();
final ws = html.WebSocket(uri.toString()); final ws = html.WebSocket(uri.toString());
@@ -1,4 +1,4 @@
import 'platform/io.dart' if (dart.library.html) 'platform/web.dart'; import 'platforms/io.dart' if (dart.library.html) 'platforms/web.dart';
class WebSocketException implements Exception { class WebSocketException implements Exception {
final int code; final int code;
@@ -18,11 +18,11 @@ typedef WebSocketOnData = Function(dynamic data);
typedef WebSocketOnError = Function(dynamic error); typedef WebSocketOnError = Function(dynamic error);
typedef WebSocketOnDispose = Function(); typedef WebSocketOnDispose = Function();
class WebSocketOptions { class WebSocketEventHandlers {
final WebSocketOnData? onData; final WebSocketOnData? onData;
final WebSocketOnError? onError; final WebSocketOnError? onError;
final WebSocketOnDispose? onDispose; final WebSocketOnDispose? onDispose;
const WebSocketOptions({ const WebSocketEventHandlers({
this.onData, this.onData,
this.onError, this.onError,
this.onDispose, this.onDispose,
@@ -31,11 +31,11 @@ class WebSocketOptions {
abstract class LiveKitWebSocket { abstract class LiveKitWebSocket {
void send(List<int> data); void send(List<int> data);
void dispose(); Future<void> dispose();
static Future<LiveKitWebSocket> connect( static Future<LiveKitWebSocket> connect(
Uri uri, [ Uri uri, [
WebSocketOptions? options, WebSocketEventHandlers? options,
]) => ]) =>
lkWebSocketConnect(uri, options); lkWebSocketConnect(uri, options);
} }
+156 -11
View File
@@ -1,29 +1,174 @@
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; // import 'package:audio_session/audio_session.dart' as _as;
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
import 'package:synchronized/synchronized.dart' as sync;
import '../logger.dart';
import '../proto/livekit_models.pb.dart' as lk_models; import '../proto/livekit_models.pb.dart' as lk_models;
import '../support/native_audio.dart';
import '_audio_api.dart' if (dart.library.html) '_audio_html.dart' as audio; import '_audio_api.dart' if (dart.library.html) '_audio_html.dart' as audio;
import 'local_audio_track.dart'; import 'local_audio_track.dart';
import 'track.dart'; import 'track.dart';
enum AudioTrackState {
none,
remoteOnly,
localOnly,
localAndRemote,
}
typedef ConfigureNativeAudioFunc = Future<NativeAudioConfiguration> Function(AudioTrackState state);
class AudioTrack extends Track { class AudioTrack extends Track {
// it's possible to set custom function here to customize audio session configuration
static ConfigureNativeAudioFunc nativeAudioConfigurationForAudioTrackState =
defaultNativeAudioConfigurationFunc;
static final _trackCounterLock = sync.Lock();
static AudioTrackState audioTrackState = AudioTrackState.none;
static int _localTrackCount = 0;
static int _remoteTrackCount = 0;
rtc.MediaStream? mediaStream; rtc.MediaStream? mediaStream;
AudioTrack(String name, rtc.MediaStreamTrack track, this.mediaStream) AudioTrack(
: super(lk_models.TrackType.AUDIO, name, track); String name,
rtc.MediaStreamTrack track,
this.mediaStream,
) : super(
lk_models.TrackType.AUDIO,
name,
track,
);
/// Start playing audio track. On web platform, create an audio element and /// Start playing audio track. On web platform, create an audio element and
/// start playback /// start playback
void start() { @override
if (this is! LocalAudioTrack) { Future<bool> start() async {
audio.startAudio(getCid(), mediaStreamTrack); final didStart = await super.start();
if (didStart) {
if (this is! LocalAudioTrack) {
audio.startAudio(getCid(), mediaStreamTrack);
}
// update counter
await _trackCounterLock.synchronized(() async {
if (this is LocalAudioTrack) {
_localTrackCount++;
} else if (this is! LocalAudioTrack) {
_remoteTrackCount++;
}
await _onAudioTrackCountDidChange();
});
} }
return didStart;
} }
@override @override
Future<void> stop() async { Future<bool> stop() async {
await mediaStream?.dispose(); final didStop = await super.stop();
mediaStream = null; if (didStop) {
audio.stopAudio(getCid()); await mediaStream?.dispose();
await super.stop(); mediaStream = null;
audio.stopAudio(getCid());
// update counter
await _trackCounterLock.synchronized(() async {
if (this is LocalAudioTrack) {
_localTrackCount--;
} else if (this is! LocalAudioTrack) {
_remoteTrackCount--;
}
await _onAudioTrackCountDidChange();
});
}
return didStop;
}
Future<void> _onAudioTrackCountDidChange() async {
logger.fine('[$runtimeType] onAudioTrackCountDidChange: '
'local: $_localTrackCount, remote: $_remoteTrackCount');
final newState = _computeAudioTrackState();
if (audioTrackState != newState) {
audioTrackState = newState;
logger.fine('[$runtimeType] didUpdateSate: $audioTrackState');
NativeAudioConfiguration? config;
if (!kIsWeb && Platform.isIOS) {
// Only iOS for now...
config = await nativeAudioConfigurationForAudioTrackState.call(audioTrackState);
}
if (config != null) {
logger.fine('[$runtimeType] configuring for ${audioTrackState} using ${config}...');
try {
await configureNativeAudio(config);
} catch (error) {
logger.warning('[$runtimeType] Failed to configure ${error}');
}
}
}
}
static AudioTrackState _computeAudioTrackState() {
if (_localTrackCount > 0 && _remoteTrackCount == 0) {
return AudioTrackState.localOnly;
} else if (_localTrackCount == 0 && _remoteTrackCount > 0) {
return AudioTrackState.remoteOnly;
} else if (_localTrackCount > 0 && _remoteTrackCount > 0) {
return AudioTrackState.localAndRemote;
}
// Default
return AudioTrackState.none;
} }
} }
Future<NativeAudioConfiguration> defaultNativeAudioConfigurationFunc(AudioTrackState state) async {
//
if (state == AudioTrackState.remoteOnly) {
return NativeAudioConfiguration(
appleAudioCategory: AppleAudioCategory.playback,
appleAudioCategoryOptions: {
AppleAudioCategoryOption.mixWithOthers,
// IosAudioCategoryOption.duckOthers,
},
appleAudioMode: AppleAudioMode.spokenAudio,
);
} else if ([
AudioTrackState.localOnly,
AudioTrackState.localAndRemote,
].contains(state)) {
return NativeAudioConfiguration(
appleAudioCategory: AppleAudioCategory.playAndRecord,
appleAudioCategoryOptions: {
AppleAudioCategoryOption.allowBluetooth,
AppleAudioCategoryOption.mixWithOthers,
// IosAudioCategoryOption.duckOthers,
},
appleAudioMode: AppleAudioMode.voiceChat,
);
}
// TODO: .record category causes exception in WebRTC lib for unknown reason
// if (this == AudioTrackState.localOnly) {
// return NativeAudioConfiguration(
// iosCategory: IosAudioCategory.record,
// iosCategoryOptions: {
// // IosAudioCategoryOption.allowBluetooth,
// },
// iosMode: IosAudioMode.spokenAudio,
// );
// }
return NativeAudioConfiguration(
appleAudioCategory: AppleAudioCategory.soloAmbient,
appleAudioCategoryOptions: {},
appleAudioMode: AppleAudioMode.default_,
);
}
+6 -2
View File
@@ -2,7 +2,7 @@ import 'dart:async';
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
import '../errors.dart'; import '../exceptions.dart';
import 'audio_track.dart'; import 'audio_track.dart';
import 'options.dart'; import 'options.dart';
@@ -15,8 +15,12 @@ class LocalAudioTrack extends AudioTrack {
/// Creates a new audio track from the default audio input device. /// Creates a new audio track from the default audio input device.
static Future<LocalAudioTrack> create([LocalAudioTrackOptions? options]) async { static Future<LocalAudioTrack> create([LocalAudioTrackOptions? options]) async {
// try { // TODO: have back up incase the options fail
final stream = await rtc.navigator.mediaDevices.getUserMedia(<String, dynamic>{ final stream = await rtc.navigator.mediaDevices.getUserMedia(<String, dynamic>{
// 'audio': <String, dynamic>{
// 'echoCancellation': true,
// 'noiseSuppression': true,
// },
'audio': true, 'audio': true,
'video': false, 'video': false,
}); });
+1 -1
View File
@@ -1,6 +1,6 @@
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
import '../errors.dart'; import '../exceptions.dart';
import '../logger.dart'; import '../logger.dart';
import 'options.dart'; import 'options.dart';
import 'track.dart'; import 'track.dart';
+2 -4
View File
@@ -1,11 +1,9 @@
import 'package:livekit_client/livekit_client.dart'; import '../events.dart';
import '../extensions.dart';
import '../participant/remote_participant.dart'; import '../participant/remote_participant.dart';
import '../proto/livekit_models.pb.dart' as lk_models; import '../proto/livekit_models.pb.dart' as lk_models;
import '../proto/livekit_rtc.pb.dart' as lk_rtc; import '../proto/livekit_rtc.pb.dart' as lk_rtc;
import 'track.dart'; import 'track.dart';
import '../extensions.dart';
import 'track_publication.dart'; import 'track_publication.dart';
/// Represents a track publication from a RemoteParticipant. Provides methods to /// Represents a track publication from a RemoteParticipant. Provides methods to
+38 -4
View File
@@ -1,13 +1,17 @@
import 'package:flutter/material.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
import 'package:livekit_client/src/classes/change_notifier.dart'; import 'package:meta/meta.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import '../extensions.dart';
import '../logger.dart';
import '../proto/livekit_models.pb.dart' as lk_models; import '../proto/livekit_models.pb.dart' as lk_models;
import '../support/disposable.dart';
/// Wrapper around a MediaStreamTrack with additional metadata. /// Wrapper around a MediaStreamTrack with additional metadata.
/// Base for [AudioTrack] and [VideoTrack], /// Base for [AudioTrack] and [VideoTrack],
/// can not be instantiated directly. /// can not be instantiated directly.
abstract class Track extends LKChangeNotifier { abstract class Track extends DisposableChangeNotifier {
static const cameraName = 'camera'; static const cameraName = 'camera';
static const screenShareName = 'screen'; static const screenShareName = 'screen';
@@ -19,6 +23,10 @@ abstract class Track extends LKChangeNotifier {
rtc.RTCRtpTransceiver? transceiver; rtc.RTCRtpTransceiver? transceiver;
String? _cid; String? _cid;
// started / stopped
bool _active = false;
bool get isActive => _active;
Track( Track(
this.kind, this.kind,
this.name, this.name,
@@ -50,7 +58,33 @@ abstract class Track extends LKChangeNotifier {
return cid; return cid;
} }
Future<void> stop() async { // returns true if started, false if already started
await mediaStreamTrack.stop(); @mustCallSuper
Future<bool> start() async {
if (_active) {
// already started
return false;
}
_active = true;
return true;
}
// returns true if stopped, false if already stopped
@mustCallSuper
Future<bool> stop() async {
if (!_active) {
// already stopped
return false;
}
try {
await mediaStreamTrack.stop();
} catch (_) {
logger.warning('[$objectId] rtc.mediaStreamTrack.stop() did throw ${_}');
}
_active = false;
return true;
} }
} }
+3 -2
View File
@@ -1,3 +1,4 @@
import '../support/disposable.dart';
import '../proto/livekit_models.pb.dart' as lk_models; import '../proto/livekit_models.pb.dart' as lk_models;
import '../types.dart'; import '../types.dart';
import 'track.dart'; import 'track.dart';
@@ -8,9 +9,9 @@ import 'track.dart';
/// Base for [RemoteTrackPublication] and [LocalTrackPublication], /// Base for [RemoteTrackPublication] and [LocalTrackPublication],
/// can not be instantiated directly. /// can not be instantiated directly.
abstract class TrackPublication { abstract class TrackPublication extends Disposable {
final String name;
final String sid; final String sid;
final String name;
final lk_models.TrackType kind; final lk_models.TrackType kind;
Track? track; Track? track;
+7 -3
View File
@@ -5,6 +5,7 @@ import 'track.dart';
/// A video track will notify when its mediaTrack has changed. /// A video track will notify when its mediaTrack has changed.
class VideoTrack extends Track { class VideoTrack extends Track {
//
rtc.MediaStream _mediaStream; rtc.MediaStream _mediaStream;
VideoTrack( VideoTrack(
@@ -27,9 +28,12 @@ class VideoTrack extends Track {
} }
@override @override
Future<void> stop() async { Future<bool> stop() async {
await super.stop(); final didStop = await super.stop();
await _mediaStream.dispose(); if (didStop) {
await _mediaStream.dispose();
}
// _mediaStream = null; // _mediaStream = null;
return didStop;
} }
} }
+60 -33
View File
@@ -5,13 +5,14 @@ import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
import 'constants.dart'; import 'constants.dart';
import 'extensions.dart'; import 'extensions.dart';
import 'logger.dart'; import 'logger.dart';
import 'support/disposable.dart';
import 'types.dart'; import 'types.dart';
import 'utils.dart'; import 'utils.dart';
typedef PCTransportOnOffer = void Function(rtc.RTCSessionDescription offer); typedef PCTransportOnOffer = void Function(rtc.RTCSessionDescription offer);
/// a wrapper around PeerConnection /// a wrapper around PeerConnection
class PCTransport { class PCTransport extends Disposable {
final rtc.RTCPeerConnection pc; final rtc.RTCPeerConnection pc;
final List<rtc.RTCIceCandidate> _pendingCandidates = []; final List<rtc.RTCIceCandidate> _pendingCandidates = [];
bool restartingIce = false; bool restartingIce = false;
@@ -20,11 +21,42 @@ class PCTransport {
Function? _cancelDebounce; Function? _cancelDebounce;
// private constructor // private constructor
PCTransport._(this.pc); PCTransport._(this.pc) {
//
onDispose(() async {
_cancelDebounce?.call();
_cancelDebounce = null;
// Ensure callbacks won't fire any more
pc.onRenegotiationNeeded = null;
pc.onIceCandidate = null;
pc.onIceConnectionState = null;
pc.onTrack = null;
// Remove all senders
List<rtc.RTCRtpSender> senders = [];
try {
senders = await pc.getSenders();
} catch (_) {
logger.warning('getSenders() failed with error: $_');
}
for (final e in senders) {
try {
await pc.removeTrack(e);
} catch (_) {
logger.warning('removeTrack() failed with error: $_');
}
}
await pc.close();
await pc.dispose();
});
}
static Future<PCTransport> create([RTCConfiguration? rtcConfig]) async { static Future<PCTransport> create([RTCConfiguration? rtcConfig]) async {
rtcConfig ??= const RTCConfiguration(); rtcConfig ??= const RTCConfiguration();
logger.fine('PCTransport creating ${rtcConfig.toMap()}'); logger.fine('[PCTransport] creating ${rtcConfig.toMap()}');
final _ = await rtc.createPeerConnection(rtcConfig.toMap()); final _ = await rtc.createPeerConnection(rtcConfig.toMap());
return PCTransport._(_); return PCTransport._(_);
} }
@@ -35,39 +67,19 @@ class PCTransport {
wait: Timeouts.debounce, wait: Timeouts.debounce,
); );
Future<void> dispose() async { // @override
logger.fine('${objectId} dispose()'); // Future<void> dispose() async {
// Ensure debounce won't fire // super.dispose();
_cancelDebounce?.call(); // // Ensure debounce won't fire
_cancelDebounce = null;
// Ensure callbacks won't fire any more // }
pc.onRenegotiationNeeded = null;
pc.onIceCandidate = null;
pc.onIceConnectionState = null;
pc.onTrack = null;
// Remove all senders
List<rtc.RTCRtpSender> senders = [];
try {
senders = await pc.getSenders();
} catch (_) {
logger.warning('getSenders() failed with error: $_');
}
for (final e in senders) {
try {
await pc.removeTrack(e);
} catch (_) {
logger.warning('removeTrack() failed with error: $_');
}
}
await pc.close();
await pc.dispose();
}
Future<void> setRemoteDescription(rtc.RTCSessionDescription sd) async { Future<void> setRemoteDescription(rtc.RTCSessionDescription sd) async {
if (isDisposed) {
logger.warning('[$objectId] setRemoteDescription() already disposed');
return;
}
await pc.setRemoteDescription(sd); await pc.setRemoteDescription(sd);
for (final candidate in _pendingCandidates) { for (final candidate in _pendingCandidates) {
@@ -84,6 +96,11 @@ class PCTransport {
} }
Future<void> createAndSendOffer([RTCOfferOptions? options]) async { Future<void> createAndSendOffer([RTCOfferOptions? options]) async {
if (isDisposed) {
logger.warning('[$objectId] createAndSendOffer() already disposed');
return;
}
if (onOffer == null) { if (onOffer == null) {
logger.warning('onOffer is null'); logger.warning('onOffer is null');
return; return;
@@ -116,6 +133,11 @@ class PCTransport {
} }
Future<void> addIceCandidate(rtc.RTCIceCandidate candidate) async { Future<void> addIceCandidate(rtc.RTCIceCandidate candidate) async {
if (isDisposed) {
logger.warning('[$objectId] addIceCandidate() already disposed');
return;
}
final desc = await getRemoteDescription(); final desc = await getRemoteDescription();
if (desc != null && !restartingIce) { if (desc != null && !restartingIce) {
@@ -127,6 +149,11 @@ class PCTransport {
} }
Future<rtc.RTCSessionDescription?> getRemoteDescription() async { Future<rtc.RTCSessionDescription?> getRemoteDescription() async {
if (isDisposed) {
logger.warning('[$objectId] getRemoteDescription() already disposed');
return null;
}
// Checking agains null doesn't work as intended // Checking agains null doesn't work as intended
// if (pc.iceConnectionState == null) return null; // if (pc.iceConnectionState == null) return null;
+4 -4
View File
@@ -99,11 +99,11 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: use-custom-webrtc-build ref: master
resolved-ref: "4942e7faec2e5775d35c42e22c2929ca6ca53769" resolved-ref: "32d83d85fa2faebcd37a19534f8a84273b12cbce"
url: "https://github.com/livekit/flutter-webrtc" url: "https://github.com/flutter-webrtc/flutter-webrtc"
source: git source: git
version: "0.6.7" version: "0.6.8"
http: http:
dependency: "direct main" dependency: "direct main"
description: description:

Some files were not shown because too many files have changed in this diff Show More