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"/>
<application
android:label="LiveKit Example"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/livekit_ic_launcher">
<activity
android:name=".MainActivity"
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
- Libyuv (= 1703)
- WebRTC-SDK (= 92.4515.05)
- WebRTC-SDK (= 92.4515.07)
- Libyuv (1703)
- livekit_client (0.0.1):
- Flutter
- WebRTC-SDK (= 92.4515.07)
- path_provider (0.0.1):
- Flutter
- shared_preferences (0.0.1):
- Flutter
- WebRTC-SDK (92.4515.05)
- WebRTC-SDK (92.4515.07)
DEPENDENCIES:
- Flutter (from `Flutter`)
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
- livekit_client (from `.symlinks/plugins/livekit_client/ios`)
- path_provider (from `.symlinks/plugins/path_provider/ios`)
- shared_preferences (from `.symlinks/plugins/shared_preferences/ios`)
@@ -27,6 +31,8 @@ EXTERNAL SOURCES:
:path: Flutter
flutter_webrtc:
:path: ".symlinks/plugins/flutter_webrtc/ios"
livekit_client:
:path: ".symlinks/plugins/livekit_client/ios"
path_provider:
:path: ".symlinks/plugins/path_provider/ios"
shared_preferences:
@@ -34,11 +40,12 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
flutter_webrtc: c0cb88c7cbd057e6e667ab1560e10c74e2ceb65b
flutter_webrtc: 6b71a1fc1d931a3dfe879e578ea10e2e6cd84f53
Libyuv: 5f79ced0ee66e60a612ca97de1e6ccacd187a437
livekit_client: 9216ac9adba8d54cefc870bbcb657ec1da534309
path_provider: d1e9807085df1f9cc9318206cd649dc0b76be3de
shared_preferences: 5033afbb22d372e15aff8ff766df9021b845f273
WebRTC-SDK: 7c76a541dbbffb0fc212aeb9902ec45a43c23996
WebRTC-SDK: 540a44f2e30e89c75e331d5c4a372253ba668233
PODFILE CHECKSUM: 82aed1035f46bfa5b522f0d0dbf4730f17ec65ff
+7 -4
View File
@@ -155,7 +155,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1020;
LastUpgradeCheck = 1300;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
@@ -321,6 +321,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -352,7 +353,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-LiveKit";
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
@@ -396,6 +397,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -452,6 +454,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -485,7 +488,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-LiveKit";
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
@@ -511,7 +514,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-LiveKit";
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1020"
LastUpgradeVersion = "1300"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -27,8 +27,6 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
@@ -38,8 +36,8 @@
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
@@ -61,8 +59,6 @@
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
@@ -0,0 +1,116 @@
{
"images" : [
{
"filename" : "iphone-20@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "iphone-20@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"filename" : "iphone-29@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "iphone-29@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"filename" : "iphone-40@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "iphone-40@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"filename" : "iphone-60@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"filename" : "iphone-60@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"filename" : "ipad-20@1x.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"filename" : "ipad-20@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "ipad-29@1x.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "ipad-29@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "ipad-40@1x.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"filename" : "ipad-40@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "ipad-76@1x.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"filename" : "ipad-76@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"filename" : "ipad-83.5@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"filename" : "ios-marketing-1024@1x.png",
"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" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"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>
<string>6.0</string>
<key>CFBundleName</key>
<string>livekit_example</string>
<string>LiveKit</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<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>(
context: this,
builder: (ctx) => AlertDialog(
+1 -3
View File
@@ -1,16 +1,14 @@
import 'package:flutter/material.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:livekit_example/theme.dart';
import 'package:logging/logging.dart';
import 'pages/connect.dart';
void main() {
print('This is a test for ${SignalTrickleEvent} test.');
// configure logs for debugging
Logger.root.level = Level.FINE;
Logger.root.onRecord.listen((record) {
print('${record.level.name}: ${record.time}: ${record.message}');
print('${record.level.name}: ${record.message}');
});
WidgetsFlutterBinding.ensureInitialized();
+5 -2
View File
@@ -1,6 +1,7 @@
import 'dart:convert';
import 'dart:math' as math;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:livekit_client/livekit_client.dart';
@@ -25,7 +26,7 @@ class RoomPage extends StatefulWidget {
class _RoomPageState extends State<RoomPage> {
//
List<Participant> participants = [];
late final _listener = EventsListener<RoomEvent>(widget.room.events);
late final EventsListener<RoomEvent> _listener = widget.room.createListener();
@override
void initState() {
@@ -48,7 +49,9 @@ class _RoomPageState extends State<RoomPage> {
}
void _setUpListeners() => _listener
..on<RoomDisconnectedEvent>((_) => Navigator.pop(context))
..on<RoomDisconnectedEvent>((_) async {
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) => Navigator.pop(context));
})
..on<DataReceivedEvent>((event) {
String decoded = 'Failed to decode';
try {
+15 -1
View File
@@ -1,9 +1,10 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:eva_icons_flutter/eva_icons_flutter.dart';
import 'package:flutter/material.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:collection/collection.dart';
import '../exts.dart';
class ControlsWidget extends StatefulWidget {
@@ -44,6 +45,15 @@ class _ControlsWidgetState extends State<ControlsWidget> {
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() {
if (participant.hasAudio) {
final audioPub = participant.audioTracks.first;
@@ -146,6 +156,10 @@ class _ControlsWidgetState extends State<ControlsWidget> {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
onPressed: _unpublishAll,
icon: const Icon(EvaIcons.closeCircleOutline),
),
if (canMute)
IconButton(
onPressed: _muteAudio,
+4 -4
View File
@@ -111,11 +111,11 @@ packages:
dependency: transitive
description:
path: "."
ref: use-custom-webrtc-build
resolved-ref: "4942e7faec2e5775d35c42e22c2929ca6ca53769"
url: "https://github.com/livekit/flutter-webrtc"
ref: master
resolved-ref: "32d83d85fa2faebcd37a19534f8a84273b12cbce"
url: "https://github.com/flutter-webrtc/flutter-webrtc"
source: git
version: "0.6.7"
version: "0.6.8"
google_fonts:
dependency: "direct main"
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' => 'contact@livekit.io' }
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.
library livekit_client;
export 'src/errors.dart';
export 'src/exceptions.dart';
export 'src/events.dart';
export 'src/livekit.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:livekit_client/livekit_client.dart';
import 'package:meta/meta.dart';
import 'participant/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 'track/remote_track_publication.dart';
import 'track/track.dart';
import 'track/track_publication.dart';
import 'types.dart';
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.
/// Emitted by [Room] and [RemoteParticipant].
class TrackSubscriptionExceptionEvent with RoomEvent, ParticipantEvent {
@@ -226,12 +217,12 @@ class EngineReconnectedEvent with EngineEvent {
const EngineReconnectedEvent();
}
class EngineParticipantUpdateEvent with EngineEvent {
final List<lk_models.ParticipantInfo> participants;
const EngineParticipantUpdateEvent({
required this.participants,
});
}
// class EngineParticipantUpdateEvent with EngineEvent {
// final List<lk_models.ParticipantInfo> participants;
// const EngineParticipantUpdateEvent({
// required this.participants,
// });
// }
class EngineTrackAddedEvent with EngineEvent {
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 {
final lk_models.UserPacket packet;
final lk_models.DataPacket_Kind kind;
@@ -363,10 +347,11 @@ class SignalTrickleEvent with SignalEvent {
});
}
class SignalParticipantUpdateEvent with SignalEvent {
final List<lk_models.ParticipantInfo> updates;
// relayed by Engine
class SignalParticipantUpdateEvent with SignalEvent, EngineEvent {
final List<lk_models.ParticipantInfo> participants;
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;
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,
});
}
@@ -1,6 +1,3 @@
//
//
//
class LiveKitException implements Exception {
final String message;
const LiveKitException._(this.message);
+16 -2
View File
@@ -1,9 +1,14 @@
import 'package:async/async.dart';
class CancelableDelayManager {
import '../support/disposable.dart';
class CancelableDelayManager extends Disposable {
//
final _delays = <CancelableOperation<void>>[];
CancelableDelayManager() {
onDispose(() async => await cancelAll());
}
// delay but cancelable
Future<void> waitFor(
Duration wait, {
@@ -19,7 +24,16 @@ class CancelableDelayManager {
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
if (_delays.isEmpty) return;
// make a copy so we don't mutate while iterating
+38 -20
View File
@@ -1,13 +1,19 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:synchronized/synchronized.dart' as sync;
import '../errors.dart';
import '../exceptions.dart';
import '../extensions.dart';
import '../logger.dart';
import '../support/disposable.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
// TODO: Move to a separate package
@@ -17,7 +23,9 @@ class EventsEmitter<T> extends EventsListenable<T> {
EventsEmitter({
bool listenSynchronized = false,
}) : super(synchronized: listenSynchronized);
}) : super(synchronized: listenSynchronized) {
onDispose(() async => await streamCtrl.close());
}
@override
EventsEmitter<T> get emitter => this;
@@ -28,13 +36,6 @@ class EventsEmitter<T> extends EventsListenable<T> {
// emit the event
streamCtrl.add(event);
}
@override
@mustCallSuper
Future<void> dispose() async {
await streamCtrl.close();
await super.dispose();
}
}
// for listening only
@@ -51,7 +52,7 @@ class EventsListener<T> extends EventsListenable<T> {
}
// ensures all listeners will close on dispose
abstract class EventsListenable<T> {
abstract class EventsListenable<T> extends Disposable {
// the emitter to listen to
EventsEmitter<T> get emitter;
@@ -62,17 +63,34 @@ abstract class EventsListenable<T> {
EventsListenable({
required this.synchronized,
});
@mustCallSuper
Future<void> dispose() async {
// Stop listening to all events
logger.fine('${objectId} dispose() cancelling ${_listeners.length} event(s)');
for (final listener in _listeners) {
await listener.cancel();
}
}) {
onDispose(() async {
if (_listeners.isNotEmpty) {
// Stop listening to all events
logger.fine('${objectId} cancelling ${_listeners.length} listeners(s)');
for (final listener in _listeners) {
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
CancelListenFunc listen(FutureOr<void> Function(T) onEvent) {
//
+53 -35
View File
@@ -1,8 +1,9 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
import 'package:meta/meta.dart';
import '../errors.dart';
import '../events.dart';
import '../exceptions.dart';
import '../extensions.dart';
import '../logger.dart';
import '../managers/event.dart';
@@ -19,16 +20,17 @@ import 'participant.dart';
/// Represents the current participant in the room.
class LocalParticipant extends Participant {
final RTCEngine _engine;
@internal
final RTCEngine engine;
@internal
final TrackPublishOptions? defaultPublishOptions;
LocalParticipant({
required RTCEngine engine,
required this.engine,
required lk_models.ParticipantInfo info,
this.defaultPublishOptions,
required EventsEmitter<RoomEvent> roomEvents,
}) : _engine = engine,
super(
}) : super(
info.sid,
info.identity,
roomEvents: roomEvents,
@@ -36,39 +38,43 @@ class LocalParticipant extends Participant {
updateFromInfo(info);
}
/// for internal use
/// {@nodoc}
RTCEngine get engine => _engine;
/// publish an audio track to the room
Future<TrackPublication> publishAudioTrack(LocalAudioTrack track) async {
if (audioTracks.any((e) => e.track?.mediaStreamTrack.id == track.mediaStreamTrack.id)) {
throw TrackPublishException('track already exists');
}
// try {
final trackInfo = await _engine.addTrack(
cid: track.getCid(),
name: track.name,
kind: track.kind,
);
// await AudioManager().incrementPublishCounter();
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();
try {
final trackInfo = await engine.addTrack(
cid: track.getCid(),
name: track.name,
kind: track.kind,
);
final pub = LocalTrackPublication(trackInfo, track, this);
addTrackPublication(pub);
notifyListeners();
await track.start();
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
@@ -83,13 +89,16 @@ class LocalParticipant extends Participant {
// Use default options from `ConnectOptions` if options is null
options = options ?? defaultPublishOptions;
final trackInfo = await _engine.addTrack(
final trackInfo = await engine.addTrack(
cid: track.getCid(),
name: track.name,
kind: track.kind,
);
logger.fine('publishVideoTrack addTrack response: ${trackInfo}');
await track.start();
// Video encodings and simulcasts
// use constraints passed to getUserMedia by default
@@ -124,14 +133,14 @@ class LocalParticipant extends Participant {
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,
kind: rtc.RTCRtpMediaType.RTCRtpMediaTypeVideo,
init: transceiverInit,
);
await _engine.negotiate();
await engine.negotiate();
final pub = LocalTrackPublication(trackInfo, track, this);
addTrackPublication(pub);
@@ -156,8 +165,17 @@ class LocalParticipant extends Participant {
final sender = track.transceiver?.sender;
if (sender != null) {
await engine.publisher?.pc.removeTrack(sender);
await engine.negotiate();
try {
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:flutter/foundation.dart';
import 'package:meta/meta.dart';
import '../classes/change_notifier.dart';
import '../events.dart';
import '../extensions.dart';
import '../logger.dart';
import '../managers/event.dart';
import '../proto/livekit_models.pb.dart' as lk_models;
import '../support/disposable.dart';
import '../track/track_publication.dart';
import 'remote_participant.dart';
@@ -21,7 +20,7 @@ import 'remote_participant.dart';
/// Base for [RemoteParticipant] and [LocalParticipant],
/// can not be instantiated directly.
abstract class Participant extends LKChangeNotifier {
abstract class Participant extends DisposableChangeNotifier with EventsEmittable<ParticipantEvent> {
/// map of track sid => published track
final trackPublications = <String, TrackPublication>{};
@@ -44,7 +43,6 @@ abstract class Participant extends LKChangeNotifier {
bool _isSpeaking = false;
// suppport for multiple event listeners
final events = EventsEmitter<ParticipantEvent>();
final EventsEmitter<RoomEvent> roomEvents;
/// when the participant joined the room
@@ -85,14 +83,11 @@ abstract class Participant extends LKChangeNotifier {
logger.fine('[ParticipantEvent] $event, will notifyListeners()');
notifyListeners();
});
}
@override
@mustCallSuper
Future<void> dispose() async {
logger.fine('$objectId dispose()');
await events.dispose();
super.dispose();
onDispose(() async {
await events.dispose();
await unpublishAllTracks();
});
}
/// for internal use
@@ -106,7 +101,7 @@ abstract class Participant extends LKChangeNotifier {
lastSpokeAt = DateTime.now();
}
[events, roomEvents].emit(SpeakingChangedEvent(
events.emit(SpeakingChangedEvent(
participant: this,
speaking: speaking,
));
@@ -145,15 +140,17 @@ abstract class Participant extends LKChangeNotifier {
// Must implement
Future<void> unpublishTrack(String trackSid, {bool notify = false});
Future<void> unpublishAllTracks() async {
final _ = List<TrackPublication>.from(trackPublications.values);
for (final track in _) {
await unpublishTrack(track.sid);
Future<void> unpublishAllTracks({bool notify = false}) async {
final trackSids = trackPublications.keys.toSet();
for (final trackid in trackSids) {
await unpublishTrack(trackid, notify: notify);
}
}
//
// Equality operators
// Object is considered equal when sid is equal
//
@override
int get hashCode => sid.hashCode;
+8 -3
View File
@@ -90,8 +90,10 @@ class RemoteParticipant extends Participant {
final Track track;
if (pub.kind == lk_models.TrackType.AUDIO) {
// audio track
// await AudioManager().incrementSubscriptionCounter();
final audioTrack = AudioTrack(pub.name, mediaTrack, stream);
audioTrack.start();
await audioTrack.start();
track = audioTrack;
} else {
// video track
@@ -143,8 +145,7 @@ class RemoteParticipant extends Participant {
// unpublish any track that is not in the info
final validSids = info.tracks.map((e) => e.sid);
final removeSids =
trackPublications.values.where((e) => !validSids.contains(e.sid)).map((e) => e.sid);
final removeSids = trackPublications.keys.where((e) => !validSids.contains(e)).toSet();
for (final sid in removeSids) {
await unpublishTrack(sid, notify: true);
}
@@ -165,6 +166,10 @@ class RemoteParticipant extends Participant {
track: track,
publication: pub,
));
// if (track is AudioTrack) {
// await AudioManager().decrementSubscriptionCounter();
// }
}
if (notify) {
+126
View File
@@ -37,6 +37,7 @@ class Room extends $pb.GeneratedMessage {
const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'enabledCodecs',
$pb.PbFieldType.PM,
subBuilder: Codec.create)
..aOS(8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'metadata')
..hasRequiredFields = false;
Room._() : super();
@@ -48,6 +49,7 @@ class Room extends $pb.GeneratedMessage {
$fixnum.Int64? creationTime,
$core.String? turnPassword,
$core.Iterable<Codec>? enabledCodecs,
$core.String? metadata,
}) {
final _result = create();
if (sid != null) {
@@ -71,6 +73,9 @@ class Room extends $pb.GeneratedMessage {
if (enabledCodecs != null) {
_result.enabledCodecs.addAll(enabledCodecs);
}
if (metadata != null) {
_result.metadata = metadata;
}
return _result;
}
factory Room.fromBuffer($core.List<$core.int> i,
@@ -171,6 +176,18 @@ class Room extends $pb.GeneratedMessage {
@$pb.TagNumber(7)
$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 {
@@ -922,3 +939,112 @@ class UserPacket extends $pb.GeneratedMessage {
@$pb.TagNumber(3)
$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',
'10': 'enabledCodecs'
},
const {'1': 'metadata', '3': 8, '4': 1, '5': 9, '10': 'metadata'},
],
};
/// Descriptor for `Room`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List roomDescriptor = $convert.base64Decode(
'CgRSb29tEhAKA3NpZBgBIAEoCVIDc2lkEhIKBG5hbWUYAiABKAlSBG5hbWUSIwoNZW1wdHlfdGltZW91dBgDIAEoDVIMZW1wdHlUaW1lb3V0EikKEG1heF9wYXJ0aWNpcGFudHMYBCABKA1SD21heFBhcnRpY2lwYW50cxIjCg1jcmVhdGlvbl90aW1lGAUgASgDUgxjcmVhdGlvblRpbWUSIwoNdHVybl9wYXNzd29yZBgGIAEoCVIMdHVyblBhc3N3b3JkEjUKDmVuYWJsZWRfY29kZWNzGAcgAygLMg4ubGl2ZWtpdC5Db2RlY1INZW5hYmxlZENvZGVjcw==');
'CgRSb29tEhAKA3NpZBgBIAEoCVIDc2lkEhIKBG5hbWUYAiABKAlSBG5hbWUSIwoNZW1wdHlfdGltZW91dBgDIAEoDVIMZW1wdHlUaW1lb3V0EikKEG1heF9wYXJ0aWNpcGFudHMYBCABKA1SD21heFBhcnRpY2lwYW50cxIjCg1jcmVhdGlvbl90aW1lGAUgASgDUgxjcmVhdGlvblRpbWUSIwoNdHVybl9wYXNzd29yZBgGIAEoCVIMdHVyblBhc3N3b3JkEjUKDmVuYWJsZWRfY29kZWNzGAcgAygLMg4ubGl2ZWtpdC5Db2RlY1INZW5hYmxlZENvZGVjcxIaCghtZXRhZGF0YRgIIAEoCVIIbWV0YWRhdGE=');
@$core.Deprecated('Use codecDescriptor instead')
const Codec$json = const {
'1': 'Codec',
@@ -182,3 +183,17 @@ const UserPacket$json = const {
/// Descriptor for `UserPacket`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List userPacketDescriptor = $convert.base64Decode(
'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,
update,
trackPublished,
speaker,
leave,
mute,
speakersChanged,
roomUpdate,
notSet
}
@@ -296,9 +297,10 @@ class SignalResponse extends $pb.GeneratedMessage {
4: SignalResponse_Message.trickle,
5: SignalResponse_Message.update,
6: SignalResponse_Message.trackPublished,
7: SignalResponse_Message.speaker,
8: SignalResponse_Message.leave,
9: SignalResponse_Message.mute,
10: SignalResponse_Message.speakersChanged,
11: SignalResponse_Message.roomUpdate,
0: SignalResponse_Message.notSet
};
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
@@ -306,7 +308,7 @@ class SignalResponse extends $pb.GeneratedMessage {
package: const $pb.PackageName(
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'livekit'),
createEmptyInstance: create)
..oo(0, [1, 2, 3, 4, 5, 6, 7, 8, 9])
..oo(0, [1, 2, 3, 4, 5, 6, 8, 9, 10, 11])
..aOM<JoinResponse>(
1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'join',
subBuilder: JoinResponse.create)
@@ -325,15 +327,18 @@ class SignalResponse extends $pb.GeneratedMessage {
..aOM<TrackPublishedResponse>(
6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'trackPublished',
subBuilder: TrackPublishedResponse.create)
..aOM<$0.ActiveSpeakerUpdate>(
7, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'speaker',
subBuilder: $0.ActiveSpeakerUpdate.create)
..aOM<LeaveRequest>(
8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'leave',
subBuilder: LeaveRequest.create)
..aOM<MuteTrackRequest>(
9, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'mute',
subBuilder: MuteTrackRequest.create)
..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;
SignalResponse._() : super();
@@ -344,9 +349,10 @@ class SignalResponse extends $pb.GeneratedMessage {
TrickleRequest? trickle,
ParticipantUpdate? update,
TrackPublishedResponse? trackPublished,
$0.ActiveSpeakerUpdate? speaker,
LeaveRequest? leave,
MuteTrackRequest? mute,
SpeakersChanged? speakersChanged,
RoomUpdate? roomUpdate,
}) {
final _result = create();
if (join != null) {
@@ -367,15 +373,18 @@ class SignalResponse extends $pb.GeneratedMessage {
if (trackPublished != null) {
_result.trackPublished = trackPublished;
}
if (speaker != null) {
_result.speaker = speaker;
}
if (leave != null) {
_result.leave = leave;
}
if (mute != null) {
_result.mute = mute;
}
if (speakersChanged != null) {
_result.speakersChanged = speakersChanged;
}
if (roomUpdate != null) {
_result.roomUpdate = roomUpdate;
}
return _result;
}
factory SignalResponse.fromBuffer($core.List<$core.int> i,
@@ -491,47 +500,61 @@ class SignalResponse extends $pb.GeneratedMessage {
@$pb.TagNumber(6)
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)
LeaveRequest get leave => $_getN(7);
LeaveRequest get leave => $_getN(6);
@$pb.TagNumber(8)
set leave(LeaveRequest v) {
setField(8, v);
}
@$pb.TagNumber(8)
$core.bool hasLeave() => $_has(7);
$core.bool hasLeave() => $_has(6);
@$pb.TagNumber(8)
void clearLeave() => clearField(8);
@$pb.TagNumber(8)
LeaveRequest ensureLeave() => $_ensure(7);
LeaveRequest ensureLeave() => $_ensure(6);
@$pb.TagNumber(9)
MuteTrackRequest get mute => $_getN(8);
MuteTrackRequest get mute => $_getN(7);
@$pb.TagNumber(9)
set mute(MuteTrackRequest v) {
setField(9, v);
}
@$pb.TagNumber(9)
$core.bool hasMute() => $_has(8);
$core.bool hasMute() => $_has(7);
@$pb.TagNumber(9)
void clearMute() => clearField(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 {
@@ -1559,3 +1582,117 @@ class ICEServer extends $pb.GeneratedMessage {
@$pb.TagNumber(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,
'10': 'trackPublished'
},
const {
'1': 'speaker',
'3': 7,
'4': 1,
'5': 11,
'6': '.livekit.ActiveSpeakerUpdate',
'9': 0,
'10': 'speaker'
},
const {
'1': 'leave',
'3': 8,
@@ -213,6 +204,24 @@ const SignalResponse$json = const {
'9': 0,
'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 [
const {'1': 'message'},
@@ -221,7 +230,7 @@ const SignalResponse$json = const {
/// Descriptor for `SignalResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List signalResponseDescriptor = $convert.base64Decode(
'Cg5TaWduYWxSZXNwb25zZRIrCgRqb2luGAEgASgLMhUubGl2ZWtpdC5Kb2luUmVzcG9uc2VIAFIEam9pbhI1CgZhbnN3ZXIYAiABKAsyGy5saXZla2l0LlNlc3Npb25EZXNjcmlwdGlvbkgAUgZhbnN3ZXISMwoFb2ZmZXIYAyABKAsyGy5saXZla2l0LlNlc3Npb25EZXNjcmlwdGlvbkgAUgVvZmZlchIzCgd0cmlja2xlGAQgASgLMhcubGl2ZWtpdC5Ucmlja2xlUmVxdWVzdEgAUgd0cmlja2xlEjQKBnVwZGF0ZRgFIAEoCzIaLmxpdmVraXQuUGFydGljaXBhbnRVcGRhdGVIAFIGdXBkYXRlEkoKD3RyYWNrX3B1Ymxpc2hlZBgGIAEoCzIfLmxpdmVraXQuVHJhY2tQdWJsaXNoZWRSZXNwb25zZUgAUg50cmFja1B1Ymxpc2hlZBI4CgdzcGVha2VyGAcgASgLMhwubGl2ZWtpdC5BY3RpdmVTcGVha2VyVXBkYXRlSABSB3NwZWFrZXISLQoFbGVhdmUYCCABKAsyFS5saXZla2l0LkxlYXZlUmVxdWVzdEgAUgVsZWF2ZRIvCgRtdXRlGAkgASgLMhkubGl2ZWtpdC5NdXRlVHJhY2tSZXF1ZXN0SABSBG11dGVCCQoHbWVzc2FnZQ==');
'Cg5TaWduYWxSZXNwb25zZRIrCgRqb2luGAEgASgLMhUubGl2ZWtpdC5Kb2luUmVzcG9uc2VIAFIEam9pbhI1CgZhbnN3ZXIYAiABKAsyGy5saXZla2l0LlNlc3Npb25EZXNjcmlwdGlvbkgAUgZhbnN3ZXISMwoFb2ZmZXIYAyABKAsyGy5saXZla2l0LlNlc3Npb25EZXNjcmlwdGlvbkgAUgVvZmZlchIzCgd0cmlja2xlGAQgASgLMhcubGl2ZWtpdC5Ucmlja2xlUmVxdWVzdEgAUgd0cmlja2xlEjQKBnVwZGF0ZRgFIAEoCzIaLmxpdmVraXQuUGFydGljaXBhbnRVcGRhdGVIAFIGdXBkYXRlEkoKD3RyYWNrX3B1Ymxpc2hlZBgGIAEoCzIfLmxpdmVraXQuVHJhY2tQdWJsaXNoZWRSZXNwb25zZUgAUg50cmFja1B1Ymxpc2hlZBItCgVsZWF2ZRgIIAEoCzIVLmxpdmVraXQuTGVhdmVSZXF1ZXN0SABSBWxlYXZlEi8KBG11dGUYCSABKAsyGS5saXZla2l0Lk11dGVUcmFja1JlcXVlc3RIAFIEbXV0ZRJFChBzcGVha2Vyc19jaGFuZ2VkGAogASgLMhgubGl2ZWtpdC5TcGVha2Vyc0NoYW5nZWRIAFIPc3BlYWtlcnNDaGFuZ2VkEjYKC3Jvb21fdXBkYXRlGAsgASgLMhMubGl2ZWtpdC5Sb29tVXBkYXRlSABSCnJvb21VcGRhdGVCCQoHbWVzc2FnZQ==');
@$core.Deprecated('Use addTrackRequestDescriptor instead')
const AddTrackRequest$json = const {
'1': 'AddTrackRequest',
@@ -402,3 +411,25 @@ const ICEServer$json = const {
/// Descriptor for `ICEServer`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List iCEServerDescriptor = $convert.base64Decode(
'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 'classes/change_notifier.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'constants.dart';
import 'errors.dart';
import 'events.dart';
import 'exceptions.dart';
import 'extensions.dart';
import 'logger.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_rtc.pb.dart' as lk_rtc;
import 'rtc_engine.dart';
import 'signal_client.dart';
import 'support/disposable.dart';
import 'track/track.dart';
import 'track/track_publication.dart';
import 'types.dart';
/// Room is the primary construct for LiveKit conferences. It contains a
@@ -29,14 +29,14 @@ import 'types.dart';
/// * participant membership changes
/// * active speakers are different
/// {@category Room}
class Room extends LKChangeNotifier {
class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
// Room is only instantiated if connected, so defaults to connected.
ConnectionState _connectionState = ConnectionState.connected;
/// connection state of the room
ConnectionState get connectionState => _connectionState;
final Map<String, RemoteParticipant> _participants = {};
final _participants = <String, RemoteParticipant>{};
/// map of SID to RemoteParticipant
UnmodifiableMapView<String, RemoteParticipant> get participants =>
@@ -46,10 +46,10 @@ class Room extends LKChangeNotifier {
late final LocalParticipant localParticipant;
/// name of the room
late final String name;
final String name;
/// sid of the room
late final String sid;
final String sid;
List<Participant> _activeSpeakers = [];
@@ -60,8 +60,7 @@ class Room extends LKChangeNotifier {
final RTCEngine engine;
// suppport for multiple event listeners
final events = EventsEmitter<RoomEvent>();
late final _engineListener = EventsListener<EngineEvent>(engine.events);
late final _engineListener = engine.createListener();
/// internal use
/// {@nodoc}
@@ -69,7 +68,8 @@ class Room extends LKChangeNotifier {
required this.engine,
required lk_rtc.JoinResponse joinResponse,
ConnectOptions? connectOptions,
}) {
}) : sid = joinResponse.room.sid,
name = joinResponse.room.name {
//
_setUpListeners();
@@ -80,9 +80,6 @@ class Room extends LKChangeNotifier {
roomEvents: events,
);
sid = joinResponse.room.sid;
name = joinResponse.room.name;
for (final info in joinResponse.otherParticipants) {
_getOrCreateRemoteParticipant(info.sid, info);
}
@@ -92,20 +89,17 @@ class Room extends LKChangeNotifier {
logger.fine('[RoomEvent] $event, will notifyListeners()');
notifyListeners();
});
}
@override
Future<void> dispose() async {
// dispose local participant
await localParticipant.dispose();
// dispose Room's events emitter
await events.dispose();
// dispose all listeners for RTCEngine
await _engineListener.dispose();
// dispose the engine
await engine.dispose();
super.dispose();
onDispose(() async {
// dispose events
await events.dispose();
// dispose local participant
await localParticipant.dispose();
// dispose all listeners for RTCEngine
await _engineListener.dispose();
// dispose the engine
await engine.dispose();
});
}
static Future<Room> connect(
@@ -116,7 +110,6 @@ class Room extends LKChangeNotifier {
}) async {
//
final engine = RTCEngine(
SignalClient(),
rtcConfig,
);
@@ -167,16 +160,16 @@ class Room extends LKChangeNotifier {
..on<EngineReconnectedEvent>((event) async {
_connectionState = ConnectionState.connected;
events.emit(const RoomReconnectedEvent());
notifyListeners();
})
..on<EngineReconnectingEvent>((event) async {
_connectionState = ConnectionState.reconnecting;
events.emit(const RoomReconnectingEvent());
notifyListeners();
})
..on<EngineDisconnectedEvent>((event) => _onDisconnectedEvent())
..on<EngineParticipantUpdateEvent>((event) => _onParticipantUpdateEvent(event.participants))
..on<EngineSpeakersUpdateEvent>((event) => _onSpeakerUpdateEvent(event.speakers))
..on<EngineDisconnectedEvent>((event) => _handleClose())
..on<SignalParticipantUpdateEvent>((event) => _onParticipantUpdateEvent(event.participants))
..on<EngineActiveSpeakersUpdateEvent>(
(event) => _onEngineActiveSpeakersUpdateEvent(event.speakers))
..on<SignalSpeakersChangedEvent>((event) => _onSignalSpeakersChangedEvent(event.speakers))
..on<EngineDataPacketReceivedEvent>(_onDataMessageEvent)
..on<EngineRemoteMuteChangedEvent>((event) async {
final track = localParticipant.trackPublications[event.sid];
@@ -210,8 +203,10 @@ class Room extends LKChangeNotifier {
/// Disconnects from the room, notifying server of disconnection.
Future<void> disconnect() async {
engine.signalClient.sendLeave();
await _onDisconnectedEvent();
if (_connectionState != ConnectionState.disconnected) {
engine.signalClient.sendLeave();
}
await _handleClose();
}
Future<void> reconnect() async {
@@ -244,42 +239,36 @@ class Room extends LKChangeNotifier {
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) {
logger.fine('$objectId: _handleDisconnect() already disconnected');
return;
logger.warning('[$objectId]: close() already disconnected');
}
// 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
for (final _ in _participants.values) {
for (final _ in _participants.values.toList()) {
// RemoteParticipant is responsible for disposing resources
await _.unpublishAllTracks();
await _.dispose();
}
_participants.clear();
// clean up LocalParticipant
// for (final pub in localParticipant.tracks.values) {
// await pub.track?.stop();
// }
await localParticipant.unpublishAllTracks();
// await localParticipant.dispose();
// localParticipant = null;
// clean up engine
await engine.close();
_activeSpeakers.clear();
notifyListeners();
events.emit(const RoomDisconnectedEvent());
// only notify if was not disconnected
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
var hasChanged = false;
for (final info in updates) {
@@ -290,7 +279,7 @@ class Room extends LKChangeNotifier {
if (info.state == lk_models.ParticipantInfo_State.DISCONNECTED) {
hasChanged = true;
_handleParticipantDisconnect(info.sid);
await _handleParticipantDisconnect(info.sid);
continue;
}
@@ -310,43 +299,63 @@ class Room extends LKChangeNotifier {
}
}
void _onSpeakerUpdateEvent(List<lk_models.SpeakerInfo> speakers) {
final seenSids = <String>{};
List<Participant> newSpeakers = [];
for (final info in speakers) {
seenSids.add(info.sid);
void _onSignalSpeakersChangedEvent(List<lk_models.SpeakerInfo> speakers) {
//
final lastSpeakers = {
for (final p in _activeSpeakers) p.sid: p,
};
if (info.sid == localParticipant.sid) {
localParticipant.audioLevel = info.level;
localParticipant.isSpeaking = true;
newSpeakers.add(localParticipant);
continue;
}
for (final speaker in speakers) {
Participant? p = _participants[speaker.sid];
if (speaker.sid == localParticipant.sid) p = localParticipant;
if (p == null) continue;
final participant = participants[info.sid];
if (participant != null) {
participant.audioLevel = info.level;
participant.isSpeaking = true;
newSpeakers.add(participant);
p.audioLevel = speaker.level;
p.isSpeaking = speaker.active;
if (speaker.active) {
lastSpeakers[speaker.sid] = p;
} else {
lastSpeakers.remove(speaker.sid);
}
}
// clear previous speakers
if (seenSids.contains(localParticipant.sid)) {
localParticipant.audioLevel = 0;
localParticipant.isSpeaking = false;
}
for (final participant in _participants.values) {
if (!seenSids.contains(participant.sid)) {
participant.audioLevel = 0;
participant.isSpeaking = false;
final activeSpeakers = lastSpeakers.values.toList();
activeSpeakers.sort((a, b) => b.audioLevel.compareTo(a.audioLevel));
_activeSpeakers = activeSpeakers;
events.emit(ActiveSpeakersChangedEvent(speakers: activeSpeakers));
}
// from data channel
// updates are sent only when there's a change to speaker ordering
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;
notifyListeners();
_activeSpeakers = activeSpeakers;
events.emit(ActiveSpeakersChangedEvent(speakers: activeSpeakers));
}
void _onDataMessageEvent(EngineDataPacketReceivedEvent dataPacketEvent) {
@@ -368,16 +377,13 @@ class Room extends LKChangeNotifier {
events.emit(event);
}
void _handleParticipantDisconnect(String sid) {
Future<void> _handleParticipantDisconnect(String sid) async {
final participant = _participants.remove(sid);
if (participant == null) {
return;
}
final toRemove = List<TrackPublication>.from(participant.trackPublications.values);
for (final track in toRemove) {
participant.unpublishTrack(track.sid, notify: true);
}
await participant.unpublishAllTracks(notify: true);
events.emit(ParticipantDisconnectedEvent(participant: participant));
}
+111 -119
View File
@@ -3,10 +3,11 @@ import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
import 'package:meta/meta.dart';
import 'constants.dart';
import 'errors.dart';
import 'events.dart';
import 'exceptions.dart';
import 'extensions.dart';
import 'logger.dart';
import 'managers/delay.dart';
@@ -15,10 +16,11 @@ import 'options.dart';
import 'proto/livekit_models.pb.dart' as lk_models;
import 'proto/livekit_rtc.pb.dart' as lk_rtc;
import 'signal_client.dart';
import 'support/disposable.dart';
import 'transport.dart';
import 'types.dart';
class RTCEngine {
class RTCEngine extends Disposable with EventsEmittable<EngineEvent> {
static const _lossyDCLabel = '_lossy';
static const _reliableDCLabel = '_reliable';
static const _maxReconnectAttempts = 5;
@@ -27,19 +29,25 @@ class RTCEngine {
// config for RTCPeerConnection
final RTCConfiguration? rtcConfig;
@internal
PCTransport? publisher;
@internal
PCTransport? subscriber;
@internal
PCTransport? get primary => _subscriberPrimary ? subscriber : publisher;
// used for ice state notifications
CancelListenFunc? _primaryIceStateListener;
// data channels for packets
rtc.RTCDataChannel? reliableDC;
rtc.RTCDataChannel? lossyDC;
bool iceConnected = false;
bool isReconnecting = false;
bool isClosed = true;
rtc.RTCDataChannel? _reliableDC;
rtc.RTCDataChannel? _lossyDC;
bool _iceConnected = false;
ConnectionState _connectionState = ConnectionState.disconnected;
/// connection state of the room
ConnectionState get connectionState => _connectionState;
// true if publisher connection has already been established.
// this is helpful to know if we need to restart ICE on the publisher connection
bool _hasPublished = false;
@@ -55,41 +63,29 @@ class RTCEngine {
// internal
int _reconnectAttempts = 0;
final events = EventsEmitter<EngineEvent>();
late final _signalListener = EventsListener<SignalEvent>(signalClient.events, synchronized: true);
late final _signalListener = signalClient.createListener(synchronized: true);
final delays = CancelableDelayManager();
// late final Timer _statsTimer;
RTCEngine(
this.signalClient,
this.rtcConfig,
) {
this.rtcConfig, {
SignalClient? signalClient,
}) : signalClient = signalClient ?? SignalClient() {
if (kDebugMode) {
// log all EngineEvents
events.listen((event) => logger.fine('[EngineEvent] $objectId ${event.runtimeType}'));
}
_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(
String url,
String token, {
@@ -110,22 +106,15 @@ class RTCEngine {
return event.response;
}
// there is no side-effect calling this method multiple times
Future<void> close() async {
logger.fine('${objectId} close()');
if (isClosed) {
logger.fine('${objectId} close() already closed');
return;
logger.fine('[$objectId] close()');
if (_connectionState == ConnectionState.disconnected) {
logger.warning('[$objectId]: close() already disconnected');
}
isClosed = true;
// _statsTimer.cancel();
// cancel events
await _primaryIceStateListener?.call();
_primaryIceStateListener = null;
// cancel all ongoing delays
await delays.dispose();
await delays.cancelAll();
// PCTransport is responsible for disposing RTCPeerConnection
await publisher?.dispose();
@@ -134,7 +123,10 @@ class RTCEngine {
await subscriber?.dispose();
subscriber = null;
signalClient.close();
await signalClient.close();
_connectionState = ConnectionState.disconnected;
// notifyListeners();
}
Future<lk_models.TrackInfo> addTrack({
@@ -174,10 +166,10 @@ class RTCEngine {
final dcMessage = rtc.RTCDataChannelMessage.fromBinary(packet.writeToBuffer());
if (packet.kind == lk_models.DataPacket_Kind.LOSSY && lossyDC != null) {
await lossyDC?.send(dcMessage);
} else if (packet.kind == lk_models.DataPacket_Kind.RELIABLE && reliableDC != null) {
await reliableDC?.send(dcMessage);
if (packet.kind == lk_models.DataPacket_Kind.LOSSY && _lossyDC != null) {
await _lossyDC?.send(dcMessage);
} else if (packet.kind == lk_models.DataPacket_Kind.RELIABLE && _reliableDC != null) {
await _reliableDC?.send(dcMessage);
}
}
@@ -188,7 +180,7 @@ class RTCEngine {
}
if (publisher?.pc.iceConnectionState?.isConnected() == true) {
logger.warning('publisher is already connected');
logger.warning('[$objectId] publisher is already connected');
return;
}
@@ -207,7 +199,7 @@ class RTCEngine {
}
Future<void> reconnect() async {
if (isClosed) {
if (_connectionState == ConnectionState.disconnected) {
logger.fine('$objectId reconnect() already closed');
return;
}
@@ -225,7 +217,8 @@ class RTCEngine {
_reconnectAttempts++;
try {
isReconnecting = true;
// isReconnecting = true;
_connectionState = ConnectionState.reconnecting;
await signalClient.reconnect(url, token);
if (publisher == null || subscriber == null) {
@@ -256,7 +249,8 @@ class RTCEngine {
// don't catch and pass up any exception
} finally {
// always set reconnecting to false
isReconnecting = false;
// isReconnecting = false;
_connectionState = ConnectionState.disconnected;
}
}
@@ -296,31 +290,25 @@ class RTCEngine {
subscriber?.pc.onDataChannel = _onDataChannel;
}
// logger.fine('subscriber.pc: ${subscriber?.pc}');
subscriber?.pc.onIceConnectionState = (state) {
//
events.emit(EngineSubscriberIceStateUpdatedEvent(
state: state,
isPrimary: _subscriberPrimary,
));
};
subscriber?.pc.onIceConnectionState =
(state) => events.emit(EngineSubscriberIceStateUpdatedEvent(
state: state,
isPrimary: _subscriberPrimary,
));
publisher?.pc.onIceConnectionState = (state) {
//
events.emit(EnginePublisherIceStateUpdatedEvent(
state: state,
isPrimary: !_subscriberPrimary,
));
};
publisher?.pc.onIceConnectionState = (state) => events.emit(EnginePublisherIceStateUpdatedEvent(
state: state,
isPrimary: !_subscriberPrimary,
));
_primaryIceStateListener ??= events.on<EngineIceStateUpdatedEvent>((event) {
events.on<EngineIceStateUpdatedEvent>((event) {
// only listen to primary ice events
if (!event.isPrimary) return;
if (event.iceState == rtc.RTCIceConnectionState.RTCIceConnectionStateConnected) {
if (!iceConnected) {
iceConnected = true;
if (isReconnecting) {
if (!_iceConnected) {
_iceConnected = true;
if (_connectionState == ConnectionState.reconnecting) {
events.emit(const EngineReconnectedEvent());
} else {
events.emit(const EngineConnectedEvent());
@@ -328,8 +316,8 @@ class RTCEngine {
}
} else if (event.iceState == rtc.RTCIceConnectionState.RTCIceConnectionStateFailed) {
// trigger reconnect sequence
if (iceConnected) {
iceConnected = false;
if (_iceConnected) {
_iceConnected = false;
_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
lossyDC?.onMessage = _onDCMessage;
reliableDC?.onMessage = _onDCMessage;
try {
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) {
switch (dc.label) {
case _reliableDCLabel:
logger.fine('Server opened DC label: ${dc.label}');
reliableDC = dc;
reliableDC?.onMessage = _onDCMessage;
_reliableDC = dc;
_reliableDC?.onMessage = _onDCMessage;
break;
case _lossyDCLabel:
logger.fine('Server opened DC label: ${dc.label}');
lossyDC = dc;
lossyDC?.onMessage = _onDCMessage;
_lossyDC = dc;
_lossyDC?.onMessage = _onDCMessage;
break;
default:
logger.warning('Unknown DC label: ${dc.label}');
@@ -395,7 +389,7 @@ class RTCEngine {
final dp = lk_models.DataPacket.fromBuffer(message.binary);
if (dp.whichValue() == lk_models.DataPacket_Value.speaker) {
// 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) {
// User packet
events.emit(EngineDataPacketReceivedEvent(
@@ -406,11 +400,15 @@ class RTCEngine {
}
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) {
logger.info('could not connect after $_reconnectAttempts, giving up');
logger.info('[$objectId] Could not connect after ${_reconnectAttempts} attempts, giving up');
await close();
events.emit(const EngineDisconnectedEvent());
return;
@@ -432,12 +430,10 @@ class RTCEngine {
});
}
//------------------ SignalClient Delegate methods -------------------------//
void _setUpListeners() => _signalListener
..on<SignalConnectedEvent>((event) async {
// create peer connections
isClosed = false;
_connectionState = ConnectionState.connected;
_subscriberPrimary = event.response.subscriberPrimary;
_providedIceServers = event.response.iceServers;
@@ -451,28 +447,30 @@ class RTCEngine {
// for subscriberPrimary, we negotiate when necessary (lazy)
await negotiate();
}
// _joinCompleter?.complete(Future.value(event.response));
// _joinCompleter = null;
})
..on<SignalCloseEvent>((_) async {
await _onDisconnected('signal');
})
..on<SignalOfferEvent>((event) async {
if (subscriber == null) {
logger.warning('[$objectId] subscriber is null');
return;
}
logger.fine('received server offer(type: ${event.sd.type}, '
logger.fine('[$objectId] Received server offer(type: ${event.sd.type}, '
'${subscriber!.pc.signalingState})');
await subscriber!.setRemoteDescription(event.sd);
final answer = await subscriber!.pc.createAnswer();
logger.fine('Created answer');
logger.finer('sdp: ${answer.sdp}');
await subscriber!.pc.setLocalDescription(answer);
signalClient.sendAnswer(answer);
try {
final answer = await subscriber!.pc.createAnswer();
logger.fine('Created answer');
logger.finer('sdp: ${answer.sdp}');
await subscriber!.pc.setLocalDescription(answer);
signalClient.sendAnswer(answer);
} catch (_) {
logger.severe('[$objectId] Failed to createAnswer()');
}
})
..on<SignalAnswerEvent>((event) async {
if (publisher == null) {
@@ -494,16 +492,10 @@ class RTCEngine {
await publisher!.addIceCandidate(event.candidate);
}
})
..on<SignalParticipantUpdateEvent>((event) async {
events.emit(EngineParticipantUpdateEvent(participants: event.updates));
})
// ..on<SignalLocalTrackPublishedEvent>((event) async {
// final completer = _pendingTrackResolvers.remove(event.cid);
// completer?.complete(event.track);
// })
..on<SignalActiveSpeakersChangedEvent>((event) async {
events.emit(EngineSpeakersUpdateEvent(speakers: event.speakers));
})
// relay
..on<SignalParticipantUpdateEvent>((event) => events.emit(event))
// relay
..on<SignalSpeakersChangedEvent>((event) => events.emit(event))
..on<SignalLeaveEvent>((event) async {
await close();
events.emit(const EngineDisconnectedEvent());
+33 -30
View File
@@ -1,23 +1,23 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
import 'package:http/http.dart' as http;
import 'errors.dart';
import 'events.dart';
import 'exceptions.dart';
import 'extensions.dart';
import 'logger.dart';
import 'managers/event.dart';
import 'options.dart';
import 'proto/livekit_models.pb.dart' as lk_models;
import 'proto/livekit_rtc.pb.dart' as lk_rtc;
import 'support/disposable.dart';
import 'support/websocket.dart';
import 'types.dart';
import 'utils.dart';
import 'ws/interface.dart';
class SignalClient {
final events = EventsEmitter<SignalEvent>();
class SignalClient extends Disposable with EventsEmittable<SignalEvent> {
//
final ProtocolVersion protocol;
bool _connected = false;
@@ -29,6 +29,11 @@ class SignalClient {
events.listen((event) {
logger.fine('[SignalEvent] $event');
});
onDispose(() async {
await events.dispose();
await close();
});
}
bool get connected => _connected;
@@ -51,7 +56,7 @@ class SignalClient {
try {
_ws = await LiveKitWebSocket.connect(
rtcUri,
WebSocketOptions(
WebSocketEventHandlers(
onData: _onSocketData,
onDispose: _onSocketDone,
onError: _handleError,
@@ -71,8 +76,7 @@ class SignalClient {
// Attempt Validation
try {
final validateResponse = await http.get(validateUri);
if (validateResponse.statusCode != 200)
throw ConnectException(validateResponse.body);
if (validateResponse.statusCode != 200) throw ConnectException(validateResponse.body);
throw ConnectException();
} catch (error) {
// Pass it up if it's already a `ConnectError`
@@ -88,7 +92,7 @@ class SignalClient {
String token,
) async {
_connected = false;
_ws?.dispose();
await _ws?.dispose();
_ws = null;
final rtcUri = Utils.buildUri(
@@ -100,7 +104,7 @@ class SignalClient {
_ws = await LiveKitWebSocket.connect(
rtcUri,
WebSocketOptions(
WebSocketEventHandlers(
onData: _onSocketData,
onDispose: _onSocketDone,
onError: _handleError,
@@ -110,24 +114,27 @@ class SignalClient {
_connected = true;
}
void close() {
Future<void> close() async {
_connected = false;
_ws?.dispose();
await _ws?.dispose();
}
void sendOffer(rtc.RTCSessionDescription offer) =>
_sendRequest(lk_rtc.SignalRequest(
// @override
// Future<void> dispose() async {
// super.dispose();
// await events.dispose();
// await close();
// }
void sendOffer(rtc.RTCSessionDescription offer) => _sendRequest(lk_rtc.SignalRequest(
offer: offer.toSDKType(),
));
void sendAnswer(rtc.RTCSessionDescription answer) =>
_sendRequest(lk_rtc.SignalRequest(
void sendAnswer(rtc.RTCSessionDescription answer) => _sendRequest(lk_rtc.SignalRequest(
answer: answer.toSDKType(),
));
void sendIceCandidate(
rtc.RTCIceCandidate candidate, lk_rtc.SignalTarget target) =>
_sendRequest(
void sendIceCandidate(rtc.RTCIceCandidate candidate, lk_rtc.SignalTarget target) => _sendRequest(
lk_rtc.SignalRequest(
trickle: lk_rtc.TrickleRequest(
candidateInit: candidate.toJson(),
@@ -136,8 +143,7 @@ class SignalClient {
),
);
void sendMuteTrack(String trackSid, bool muted) =>
_sendRequest(lk_rtc.SignalRequest(
void sendMuteTrack(String trackSid, bool muted) => _sendRequest(lk_rtc.SignalRequest(
mute: lk_rtc.MuteTrackRequest(
sid: trackSid,
muted: muted,
@@ -174,8 +180,7 @@ class SignalClient {
subscription: subscription,
));
void sendSetSimulcastLayers(
String trackSid, List<lk_rtc.VideoQuality> layers) =>
void sendSetSimulcastLayers(String trackSid, List<lk_rtc.VideoQuality> layers) =>
_sendRequest(lk_rtc.SignalRequest(
simulcast: lk_rtc.SetSimulcastLayers(
trackSid: trackSid,
@@ -188,8 +193,8 @@ class SignalClient {
));
void _sendRequest(lk_rtc.SignalRequest req) {
if (_ws == null) {
logger.warning('could not send message, not connected');
if (_ws == null || isDisposed) {
logger.warning('[$objectId] Could not send message, not connected or already disposed');
return;
}
@@ -221,8 +226,7 @@ class SignalClient {
));
break;
case lk_rtc.SignalResponse_Message.update:
events.emit(
SignalParticipantUpdateEvent(updates: msg.update.participants));
events.emit(SignalParticipantUpdateEvent(participants: msg.update.participants));
break;
case lk_rtc.SignalResponse_Message.trackPublished:
events.emit(SignalLocalTrackPublishedEvent(
@@ -230,9 +234,8 @@ class SignalClient {
track: msg.trackPublished.track,
));
break;
case lk_rtc.SignalResponse_Message.speaker:
events.emit(
SignalActiveSpeakersChangedEvent(speakers: msg.speaker.speakers));
case lk_rtc.SignalResponse_Message.speakersChanged:
events.emit(SignalSpeakersChangedEvent(speakers: msg.speakersChanged.speakers));
break;
case lk_rtc.SignalResponse_Message.leave:
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 '../../logger.dart';
import '../interface.dart';
import '../websocket.dart';
import '../../extensions.dart';
Future<LiveKitWebSocketIO> lkWebSocketConnect(
Uri uri, [
WebSocketOptions? options,
WebSocketEventHandlers? options,
]) =>
LiveKitWebSocketIO.connect(uri, options);
class LiveKitWebSocketIO implements LiveKitWebSocket {
final io.WebSocket _ws;
final WebSocketOptions? options;
final WebSocketEventHandlers? options;
late final StreamSubscription _subscription;
LiveKitWebSocketIO._(
@@ -26,38 +27,38 @@ class LiveKitWebSocketIO implements LiveKitWebSocket {
}
@override
void dispose() {
Future<void> dispose() async {
await _subscription.cancel();
await _ws.close();
options?.onDispose?.call();
_subscription.cancel();
_ws.close();
}
@override
void send(List<int> data) {
// 0 CONNECTING
// 1 OPEN
// 2 CLOSING
// 3 CLOSED
if (_ws.readyState == 1) {
try {
_ws.add(data);
} catch (e) {
//
}
// 0 CONNECTING, 1 OPEN, 2 CLOSING, 3 CLOSED
if (_ws.readyState != 1) {
logger.fine('[$objectId] Tried to send data (readyState: ${_ws.readyState})');
return;
}
try {
_ws.add(data);
} catch (_) {
logger.fine('[$objectId] send did throw ${_}');
}
}
static Future<LiveKitWebSocketIO> connect(
Uri uri, [
WebSocketOptions? options,
WebSocketEventHandlers? options,
]) async {
logger.fine('WebSocketIO connect (uri: ${uri.toString()})');
logger.fine('[WebSocketIO] Connecting(uri: ${uri.toString()})...');
try {
final ws = await io.WebSocket.connect(uri.toString());
logger.fine('WebSocketIO connected');
logger.fine('[WebSocketIO] Connected');
return LiveKitWebSocketIO._(ws, options);
} catch (_) {
logger.severe('WebSocketIO error ${_}');
logger.severe('[WebSocketIO] did throw ${_}');
throw WebSocketException.connect();
}
}
@@ -4,17 +4,17 @@ import 'dart:async';
import 'dart:html' as html;
import 'dart:typed_data';
import '../interface.dart';
import '../websocket.dart';
Future<LiveKitWebSocketWeb> lkWebSocketConnect(
Uri uri, [
WebSocketOptions? options,
WebSocketEventHandlers? options,
]) =>
LiveKitWebSocketWeb.connect(uri, options);
class LiveKitWebSocketWeb implements LiveKitWebSocket {
final html.WebSocket _ws;
final WebSocketOptions? options;
final WebSocketEventHandlers? options;
late final StreamSubscription _messageSubscription;
late final StreamSubscription _closeSubscription;
@@ -34,16 +34,16 @@ class LiveKitWebSocketWeb implements LiveKitWebSocket {
void send(List<int> data) => _ws.send(data);
@override
void dispose() {
Future<void> dispose() async {
options?.onDispose?.call();
_messageSubscription.cancel();
_closeSubscription.cancel();
await _messageSubscription.cancel();
await _closeSubscription.cancel();
_ws.close();
}
static Future<LiveKitWebSocketWeb> connect(
Uri uri, [
WebSocketOptions? options,
WebSocketEventHandlers? options,
]) async {
final completer = Completer<LiveKitWebSocketWeb>();
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 {
final int code;
@@ -18,11 +18,11 @@ typedef WebSocketOnData = Function(dynamic data);
typedef WebSocketOnError = Function(dynamic error);
typedef WebSocketOnDispose = Function();
class WebSocketOptions {
class WebSocketEventHandlers {
final WebSocketOnData? onData;
final WebSocketOnError? onError;
final WebSocketOnDispose? onDispose;
const WebSocketOptions({
const WebSocketEventHandlers({
this.onData,
this.onError,
this.onDispose,
@@ -31,11 +31,11 @@ class WebSocketOptions {
abstract class LiveKitWebSocket {
void send(List<int> data);
void dispose();
Future<void> dispose();
static Future<LiveKitWebSocket> connect(
Uri uri, [
WebSocketOptions? options,
WebSocketEventHandlers? 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 '../support/native_audio.dart';
import '_audio_api.dart' if (dart.library.html) '_audio_html.dart' as audio;
import 'local_audio_track.dart';
import 'track.dart';
enum AudioTrackState {
none,
remoteOnly,
localOnly,
localAndRemote,
}
typedef ConfigureNativeAudioFunc = Future<NativeAudioConfiguration> Function(AudioTrackState state);
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;
AudioTrack(String name, rtc.MediaStreamTrack track, this.mediaStream)
: super(lk_models.TrackType.AUDIO, name, track);
AudioTrack(
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 playback
void start() {
if (this is! LocalAudioTrack) {
audio.startAudio(getCid(), mediaStreamTrack);
@override
Future<bool> start() async {
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
Future<void> stop() async {
await mediaStream?.dispose();
mediaStream = null;
audio.stopAudio(getCid());
await super.stop();
Future<bool> stop() async {
final didStop = await super.stop();
if (didStop) {
await mediaStream?.dispose();
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 '../errors.dart';
import '../exceptions.dart';
import 'audio_track.dart';
import 'options.dart';
@@ -15,8 +15,12 @@ class LocalAudioTrack extends AudioTrack {
/// Creates a new audio track from the default audio input device.
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>{
// 'audio': <String, dynamic>{
// 'echoCancellation': true,
// 'noiseSuppression': true,
// },
'audio': true,
'video': false,
});
+1 -1
View File
@@ -1,6 +1,6 @@
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
import '../errors.dart';
import '../exceptions.dart';
import '../logger.dart';
import 'options.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 '../proto/livekit_models.pb.dart' as lk_models;
import '../proto/livekit_rtc.pb.dart' as lk_rtc;
import 'track.dart';
import '../extensions.dart';
import 'track_publication.dart';
/// 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:livekit_client/src/classes/change_notifier.dart';
import 'package:meta/meta.dart';
import 'package:uuid/uuid.dart';
import '../extensions.dart';
import '../logger.dart';
import '../proto/livekit_models.pb.dart' as lk_models;
import '../support/disposable.dart';
/// Wrapper around a MediaStreamTrack with additional metadata.
/// Base for [AudioTrack] and [VideoTrack],
/// can not be instantiated directly.
abstract class Track extends LKChangeNotifier {
abstract class Track extends DisposableChangeNotifier {
static const cameraName = 'camera';
static const screenShareName = 'screen';
@@ -19,6 +23,10 @@ abstract class Track extends LKChangeNotifier {
rtc.RTCRtpTransceiver? transceiver;
String? _cid;
// started / stopped
bool _active = false;
bool get isActive => _active;
Track(
this.kind,
this.name,
@@ -50,7 +58,33 @@ abstract class Track extends LKChangeNotifier {
return cid;
}
Future<void> stop() async {
await mediaStreamTrack.stop();
// returns true if started, false if already started
@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 '../types.dart';
import 'track.dart';
@@ -8,9 +9,9 @@ import 'track.dart';
/// Base for [RemoteTrackPublication] and [LocalTrackPublication],
/// can not be instantiated directly.
abstract class TrackPublication {
final String name;
abstract class TrackPublication extends Disposable {
final String sid;
final String name;
final lk_models.TrackType kind;
Track? track;
+7 -3
View File
@@ -5,6 +5,7 @@ import 'track.dart';
/// A video track will notify when its mediaTrack has changed.
class VideoTrack extends Track {
//
rtc.MediaStream _mediaStream;
VideoTrack(
@@ -27,9 +28,12 @@ class VideoTrack extends Track {
}
@override
Future<void> stop() async {
await super.stop();
await _mediaStream.dispose();
Future<bool> stop() async {
final didStop = await super.stop();
if (didStop) {
await _mediaStream.dispose();
}
// _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 'extensions.dart';
import 'logger.dart';
import 'support/disposable.dart';
import 'types.dart';
import 'utils.dart';
typedef PCTransportOnOffer = void Function(rtc.RTCSessionDescription offer);
/// a wrapper around PeerConnection
class PCTransport {
class PCTransport extends Disposable {
final rtc.RTCPeerConnection pc;
final List<rtc.RTCIceCandidate> _pendingCandidates = [];
bool restartingIce = false;
@@ -20,11 +21,42 @@ class PCTransport {
Function? _cancelDebounce;
// 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 {
rtcConfig ??= const RTCConfiguration();
logger.fine('PCTransport creating ${rtcConfig.toMap()}');
logger.fine('[PCTransport] creating ${rtcConfig.toMap()}');
final _ = await rtc.createPeerConnection(rtcConfig.toMap());
return PCTransport._(_);
}
@@ -35,39 +67,19 @@ class PCTransport {
wait: Timeouts.debounce,
);
Future<void> dispose() async {
logger.fine('${objectId} dispose()');
// Ensure debounce won't fire
_cancelDebounce?.call();
_cancelDebounce = null;
// @override
// Future<void> dispose() async {
// super.dispose();
// // Ensure debounce won't fire
// 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 {
if (isDisposed) {
logger.warning('[$objectId] setRemoteDescription() already disposed');
return;
}
await pc.setRemoteDescription(sd);
for (final candidate in _pendingCandidates) {
@@ -84,6 +96,11 @@ class PCTransport {
}
Future<void> createAndSendOffer([RTCOfferOptions? options]) async {
if (isDisposed) {
logger.warning('[$objectId] createAndSendOffer() already disposed');
return;
}
if (onOffer == null) {
logger.warning('onOffer is null');
return;
@@ -116,6 +133,11 @@ class PCTransport {
}
Future<void> addIceCandidate(rtc.RTCIceCandidate candidate) async {
if (isDisposed) {
logger.warning('[$objectId] addIceCandidate() already disposed');
return;
}
final desc = await getRemoteDescription();
if (desc != null && !restartingIce) {
@@ -127,6 +149,11 @@ class PCTransport {
}
Future<rtc.RTCSessionDescription?> getRemoteDescription() async {
if (isDisposed) {
logger.warning('[$objectId] getRemoteDescription() already disposed');
return null;
}
// Checking agains null doesn't work as intended
// if (pc.iceConnectionState == null) return null;
+4 -4
View File
@@ -99,11 +99,11 @@ packages:
dependency: "direct main"
description:
path: "."
ref: use-custom-webrtc-build
resolved-ref: "4942e7faec2e5775d35c42e22c2929ca6ca53769"
url: "https://github.com/livekit/flutter-webrtc"
ref: master
resolved-ref: "32d83d85fa2faebcd37a19534f8a84273b12cbce"
url: "https://github.com/flutter-webrtc/flutter-webrtc"
source: git
version: "0.6.7"
version: "0.6.8"
http:
dependency: "direct main"
description:

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