Switch to new event system (#9)
* `SignalEvents` and `synchronized` mode for `EventsListenable` * cascade syntax * clean up * `RoomEvents` * `ParticipantEvents` * all events implemented * make it compile with M1 macs * dispose logic * cleaner connect logic * ask to publish * fix initial EngineTrackAddedEvent glitch * fix unpublishTrack bug * clean up * better events docs * organize event types * clean up * cleaner wait logic * wait for event instead of using Completer * notifyListeners
This commit is contained in:
@@ -21,6 +21,6 @@
|
|||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1.0</string>
|
<string>1.0</string>
|
||||||
<key>MinimumOSVersion</key>
|
<key>MinimumOSVersion</key>
|
||||||
<string>8.0</string>
|
<string>9.0</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ post_install do |installer|
|
|||||||
#
|
#
|
||||||
target.build_configurations.each do |config|
|
target.build_configurations.each do |config|
|
||||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.1'
|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.1'
|
||||||
|
#
|
||||||
|
# Make it compile with M1 macs
|
||||||
|
#
|
||||||
|
config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = 'arm64'
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
+10
-10
@@ -2,14 +2,14 @@ PODS:
|
|||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
- flutter_webrtc (0.2.2):
|
- flutter_webrtc (0.2.2):
|
||||||
- Flutter
|
- Flutter
|
||||||
- GoogleWebRTC (= 1.1.31999)
|
|
||||||
- Libyuv (= 1703)
|
- Libyuv (= 1703)
|
||||||
- GoogleWebRTC (1.1.31999)
|
- WebRTC-SDK (= 92.4515.05)
|
||||||
- Libyuv (1703)
|
- Libyuv (1703)
|
||||||
- path_provider (0.0.1):
|
- path_provider (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- shared_preferences (0.0.1):
|
- shared_preferences (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- WebRTC-SDK (92.4515.05)
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
@@ -19,8 +19,8 @@ DEPENDENCIES:
|
|||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
trunk:
|
trunk:
|
||||||
- GoogleWebRTC
|
|
||||||
- Libyuv
|
- Libyuv
|
||||||
|
- WebRTC-SDK
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
Flutter:
|
Flutter:
|
||||||
@@ -33,13 +33,13 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/shared_preferences/ios"
|
:path: ".symlinks/plugins/shared_preferences/ios"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c
|
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
|
||||||
flutter_webrtc: 39898454258b54ba51996850d5da8d5d53bf1524
|
flutter_webrtc: c0cb88c7cbd057e6e667ab1560e10c74e2ceb65b
|
||||||
GoogleWebRTC: b39a78c4f5cc6b0323415b9233db03a2faa7b0f0
|
|
||||||
Libyuv: 5f79ced0ee66e60a612ca97de1e6ccacd187a437
|
Libyuv: 5f79ced0ee66e60a612ca97de1e6ccacd187a437
|
||||||
path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c
|
path_provider: d1e9807085df1f9cc9318206cd649dc0b76be3de
|
||||||
shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d
|
shared_preferences: 5033afbb22d372e15aff8ff766df9021b845f273
|
||||||
|
WebRTC-SDK: 7c76a541dbbffb0fc212aeb9902ec45a43c23996
|
||||||
|
|
||||||
PODFILE CHECKSUM: 6055d9653e1011c0b3b671abb92cdea979357e63
|
PODFILE CHECKSUM: 82aed1035f46bfa5b522f0d0dbf4730f17ec65ff
|
||||||
|
|
||||||
COCOAPODS: 1.10.1
|
COCOAPODS: 1.11.2
|
||||||
|
|||||||
@@ -331,6 +331,7 @@
|
|||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
EXCLUDED_ARCHS = "";
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
@@ -342,7 +343,6 @@
|
|||||||
IPHONEOS_DEPLOYMENT_TARGET = 12.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 12.1;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTED_PLATFORMS = iphoneos;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
VALIDATE_PRODUCT = YES;
|
VALIDATE_PRODUCT = YES;
|
||||||
};
|
};
|
||||||
@@ -406,6 +406,7 @@
|
|||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
ENABLE_TESTABILITY = YES;
|
ENABLE_TESTABILITY = YES;
|
||||||
|
EXCLUDED_ARCHS = "";
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
GCC_DYNAMIC_NO_PIC = NO;
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
@@ -461,6 +462,7 @@
|
|||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
EXCLUDED_ARCHS = "";
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
@@ -472,7 +474,6 @@
|
|||||||
IPHONEOS_DEPLOYMENT_TARGET = 12.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 12.1;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTED_PLATFORMS = iphoneos;
|
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
|||||||
+19
-1
@@ -2,6 +2,24 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
extension LKExampleExt on BuildContext {
|
extension LKExampleExt on BuildContext {
|
||||||
//
|
//
|
||||||
|
Future<bool?> showPublishDialog() => showDialog<bool>(
|
||||||
|
context: this,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('Publish'),
|
||||||
|
content: const Text('Would you like to publish your Camera & Mic ?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, false),
|
||||||
|
child: const Text('NO'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, true),
|
||||||
|
child: const Text('YES'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
Future<void> showErrorDialog(dynamic exception) => showDialog<void>(
|
Future<void> showErrorDialog(dynamic exception) => showDialog<void>(
|
||||||
context: this,
|
context: this,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
@@ -74,7 +92,7 @@ extension LKExampleExt on BuildContext {
|
|||||||
context: this,
|
context: this,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
title: const Text('Received data'),
|
title: const Text('Received data'),
|
||||||
content: Text('"${data}"'),
|
content: Text(data),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(ctx, true),
|
onPressed: () => Navigator.pop(ctx, true),
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
import 'package:livekit_example/theme.dart';
|
import 'package:livekit_example/theme.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
import 'pages/connect.dart';
|
import 'pages/connect.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
print('This is a test for ${SignalTrickleEvent} test.');
|
||||||
// configure logs for debugging
|
// configure logs for debugging
|
||||||
Logger.root.level = Level.FINE;
|
Logger.root.level = Level.FINE;
|
||||||
Logger.root.onRecord.listen((record) {
|
Logger.root.onRecord.listen((record) {
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ class _ConnectPageState extends State<ConnectPage> {
|
|||||||
_busy = true;
|
_busy = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Save for next time
|
||||||
|
await _writePrefs();
|
||||||
|
|
||||||
print('Connecting with url: ${_uriCtrl.text}, token: ${_tokenCtrl.text}...');
|
print('Connecting with url: ${_uriCtrl.text}, token: ${_tokenCtrl.text}...');
|
||||||
|
|
||||||
final room = await LiveKitClient.connect(
|
final room = await LiveKitClient.connect(
|
||||||
@@ -74,9 +77,6 @@ class _ConnectPageState extends State<ConnectPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Save for next time
|
|
||||||
await _writePrefs();
|
|
||||||
|
|
||||||
await Navigator.push<void>(
|
await Navigator.push<void>(
|
||||||
ctx,
|
ctx,
|
||||||
MaterialPageRoute(builder: (_) => RoomPage(room)),
|
MaterialPageRoute(builder: (_) => RoomPage(room)),
|
||||||
@@ -122,10 +122,14 @@ class _ConnectPageState extends State<ConnectPage> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
TextField(
|
TextField(
|
||||||
|
enableSuggestions: false,
|
||||||
|
autocorrect: false,
|
||||||
controller: _uriCtrl,
|
controller: _uriCtrl,
|
||||||
decoration: const InputDecoration(labelText: 'URL'),
|
decoration: const InputDecoration(labelText: 'URL'),
|
||||||
),
|
),
|
||||||
TextField(
|
TextField(
|
||||||
|
enableSuggestions: false,
|
||||||
|
autocorrect: false,
|
||||||
controller: _tokenCtrl,
|
controller: _tokenCtrl,
|
||||||
decoration: const InputDecoration(labelText: 'Token'),
|
decoration: const InputDecoration(labelText: 'Token'),
|
||||||
),
|
),
|
||||||
|
|||||||
+44
-37
@@ -2,12 +2,12 @@ import 'dart:convert';
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
|
import '../exts.dart';
|
||||||
import '../widgets/controls.dart';
|
import '../widgets/controls.dart';
|
||||||
import '../widgets/participant.dart';
|
import '../widgets/participant.dart';
|
||||||
import '../exts.dart';
|
|
||||||
|
|
||||||
class RoomPage extends StatefulWidget {
|
class RoomPage extends StatefulWidget {
|
||||||
//
|
//
|
||||||
@@ -22,29 +22,51 @@ class RoomPage extends StatefulWidget {
|
|||||||
State<StatefulWidget> createState() => _RoomPageState();
|
State<StatefulWidget> createState() => _RoomPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RoomPageState extends State<RoomPage> with RoomDelegate {
|
class _RoomPageState extends State<RoomPage> {
|
||||||
//
|
//
|
||||||
List<Participant> participants = [];
|
List<Participant> participants = [];
|
||||||
|
late final _listener = EventsListener<LiveKitEvent>(widget.room.events);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
widget.room.delegate = this;
|
widget.room.addListener(_onRoomDidUpdate);
|
||||||
widget.room.addListener(_onChange);
|
_setUpListeners();
|
||||||
_onConnected();
|
_sortParticipants();
|
||||||
|
WidgetsBinding.instance?.addPostFrameCallback((_) => _askPublish());
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
widget.room.delegate = null;
|
// always dispose listener
|
||||||
widget.room.removeListener(_onChange);
|
(() async {
|
||||||
|
widget.room.removeListener(_onRoomDidUpdate);
|
||||||
|
await _listener.dispose();
|
||||||
|
await widget.room.dispose();
|
||||||
|
})();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onConnected() async {
|
void _setUpListeners() => _listener
|
||||||
|
..on<RoomDisconnectedEvent>((_) => Navigator.pop(context))
|
||||||
|
..on<DataReceivedEvent>((event) {
|
||||||
|
String decoded = 'Failed to decode';
|
||||||
|
try {
|
||||||
|
decoded = utf8.decode(event.data);
|
||||||
|
} catch (_) {
|
||||||
|
print('Failed to decode: $_');
|
||||||
|
}
|
||||||
|
context.showDataReceivedDialog(decoded);
|
||||||
|
});
|
||||||
|
|
||||||
|
void _askPublish() async {
|
||||||
|
final result = await context.showPublishDialog();
|
||||||
|
if (result != true) return;
|
||||||
// video will fail when running in ios simulator
|
// video will fail when running in ios simulator
|
||||||
try {
|
try {
|
||||||
final localVideo = await LocalVideoTrack.createCameraTrack(); // Defaults to camera
|
// Create video track
|
||||||
|
final localVideo = await LocalVideoTrack.createCameraTrack();
|
||||||
|
// Try to publish the video
|
||||||
await widget.room.localParticipant.publishVideoTrack(
|
await widget.room.localParticipant.publishVideoTrack(
|
||||||
localVideo,
|
localVideo,
|
||||||
// options: TrackPublishOptions(
|
// options: TrackPublishOptions(
|
||||||
@@ -52,20 +74,22 @@ class _RoomPageState extends State<RoomPage> with RoomDelegate {
|
|||||||
// videoEncoding: VideoParameters.presetQVGA169.encoding,
|
// videoEncoding: VideoParameters.presetQVGA169.encoding,
|
||||||
// ),
|
// ),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
|
||||||
print('could not publish video: $e');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Create mic track
|
||||||
final localAudio = await LocalAudioTrack.create();
|
final localAudio = await LocalAudioTrack.create();
|
||||||
|
// // Try to publish audio
|
||||||
await widget.room.localParticipant.publishAudioTrack(localAudio);
|
await widget.room.localParticipant.publishAudioTrack(localAudio);
|
||||||
sortParticipants();
|
} catch (error) {
|
||||||
|
print('could not publish video: $error');
|
||||||
|
await context.showErrorDialog(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onChange() {
|
void _onRoomDidUpdate() {
|
||||||
sortParticipants();
|
_sortParticipants();
|
||||||
}
|
}
|
||||||
|
|
||||||
void sortParticipants() {
|
void _sortParticipants() {
|
||||||
List<Participant> participants = [];
|
List<Participant> participants = [];
|
||||||
participants.addAll(widget.room.participants.values);
|
participants.addAll(widget.room.participants.values);
|
||||||
// sort speakers for the grid
|
// sort speakers for the grid
|
||||||
@@ -106,29 +130,13 @@ class _RoomPageState extends State<RoomPage> with RoomDelegate {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void onDataReceived(RemoteParticipant participant, List<int> data) async {
|
|
||||||
await context.showDataReceivedDialog(utf8.decode(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onDisconnected() {
|
|
||||||
print('disconnected: $context');
|
|
||||||
Navigator.pop(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Scaffold(
|
Widget build(BuildContext context) => Scaffold(
|
||||||
// with a provider, any child/descendent widget can be updated if they
|
body: Column(
|
||||||
// are a Consumer of Room.
|
|
||||||
body: ChangeNotifierProvider.value(
|
|
||||||
value: widget.room,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: participants.isNotEmpty
|
child:
|
||||||
? ParticipantWidget(participants.first)
|
participants.isNotEmpty ? ParticipantWidget(participants.first) : Container()),
|
||||||
: Container()),
|
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 100,
|
height: 100,
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
@@ -148,6 +156,5 @@ class _RoomPageState extends State<RoomPage> with RoomDelegate {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,8 +102,8 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
|||||||
//
|
//
|
||||||
final lp = widget.room.localParticipant;
|
final lp = widget.room.localParticipant;
|
||||||
|
|
||||||
for (final tracks in lp.videoTracks) {
|
for (final track in lp.videoTracks) {
|
||||||
await lp.unpublishTrack(tracks.track!);
|
await lp.unpublishTrack(track.sid);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class ParticipantWidget extends StatefulWidget {
|
|||||||
State<StatefulWidget> createState() => _ParticipantWidgetState();
|
State<StatefulWidget> createState() => _ParticipantWidgetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ParticipantWidgetState extends State<ParticipantWidget> with ParticipantDelegate {
|
class _ParticipantWidgetState extends State<ParticipantWidget> {
|
||||||
//
|
//
|
||||||
TrackPublication? videoPub;
|
TrackPublication? videoPub;
|
||||||
TrackPublication? audioPub;
|
TrackPublication? audioPub;
|
||||||
|
|||||||
+12
-10
@@ -56,7 +56,7 @@ packages:
|
|||||||
name: eva_icons_flutter
|
name: eva_icons_flutter
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "3.0.2"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -110,9 +110,11 @@ packages:
|
|||||||
flutter_webrtc:
|
flutter_webrtc:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_webrtc
|
path: "."
|
||||||
url: "https://pub.dartlang.org"
|
ref: use-custom-webrtc-build
|
||||||
source: hosted
|
resolved-ref: "4942e7faec2e5775d35c42e22c2929ca6ca53769"
|
||||||
|
url: "https://github.com/livekit/flutter-webrtc"
|
||||||
|
source: git
|
||||||
version: "0.6.7"
|
version: "0.6.7"
|
||||||
google_fonts:
|
google_fonts:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
@@ -197,14 +199,14 @@ packages:
|
|||||||
name: path_provider
|
name: path_provider
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.3"
|
version: "2.0.5"
|
||||||
path_provider_linux:
|
path_provider_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_linux
|
name: path_provider_linux
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.2"
|
version: "2.1.0"
|
||||||
path_provider_macos:
|
path_provider_macos:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -274,7 +276,7 @@ packages:
|
|||||||
name: shared_preferences
|
name: shared_preferences
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.7"
|
version: "2.0.8"
|
||||||
shared_preferences_linux:
|
shared_preferences_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -391,7 +393,7 @@ packages:
|
|||||||
name: win32
|
name: win32
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.8"
|
version: "2.2.9"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -400,5 +402,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.0"
|
version: "0.2.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.13.0 <3.0.0"
|
dart: ">=2.14.0 <3.0.0"
|
||||||
flutter: ">=2.0.0"
|
flutter: ">=2.5.0"
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
library livekit_client;
|
library livekit_client;
|
||||||
|
|
||||||
export 'src/errors.dart';
|
export 'src/errors.dart';
|
||||||
|
export 'src/events.dart';
|
||||||
export 'src/livekit.dart';
|
export 'src/livekit.dart';
|
||||||
|
export 'src/managers/event.dart';
|
||||||
export 'src/options.dart';
|
export 'src/options.dart';
|
||||||
export 'src/participant/local_participant.dart';
|
export 'src/participant/local_participant.dart';
|
||||||
export 'src/participant/local_participant.dart';
|
export 'src/participant/local_participant.dart';
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
class Timeouts {
|
||||||
|
static const connection = Duration(seconds: 5);
|
||||||
|
static const debounce = Duration(milliseconds: 100);
|
||||||
|
static const publish = Duration(seconds: 3);
|
||||||
|
static const iceConnection = Duration(seconds: 5);
|
||||||
|
static const iceRestart = Duration(seconds: 10);
|
||||||
|
}
|
||||||
+301
-79
@@ -1,120 +1,257 @@
|
|||||||
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
||||||
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
import 'participant/participant.dart';
|
||||||
|
import 'participant/remote_participant.dart';
|
||||||
import 'proto/livekit_models.pb.dart' as lk_models;
|
import 'proto/livekit_models.pb.dart' as lk_models;
|
||||||
|
import 'proto/livekit_rtc.pb.dart' as lk_rtc;
|
||||||
|
import 'track/remote_track_publication.dart';
|
||||||
|
import 'track/track.dart';
|
||||||
|
import 'types.dart';
|
||||||
|
|
||||||
abstract class LiveKitEvent {}
|
abstract class LiveKitEvent {}
|
||||||
|
|
||||||
abstract class RoomEvent implements LiveKitEvent {
|
abstract class RoomEvent implements LiveKitEvent {}
|
||||||
const RoomEvent();
|
|
||||||
|
abstract class ParticipantEvent implements LiveKitEvent {}
|
||||||
|
|
||||||
|
abstract class TrackEvent implements LiveKitEvent {}
|
||||||
|
|
||||||
|
abstract class EngineEvent implements LiveKitEvent {}
|
||||||
|
|
||||||
|
abstract class SignalEvent implements LiveKitEvent {}
|
||||||
|
|
||||||
|
/// When the connection to the server has been interrupted and it's attempting
|
||||||
|
/// to reconnect.
|
||||||
|
/// Emitted by [Room].
|
||||||
|
class RoomReconnectingEvent with RoomEvent {
|
||||||
|
const RoomReconnectingEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class ParticipantEvent implements LiveKitEvent {
|
/// Connection to room is re-established. All existing state is preserved.
|
||||||
const ParticipantEvent();
|
/// Emitted by [Room].
|
||||||
|
class RoomReconnectedEvent with RoomEvent {
|
||||||
|
const RoomReconnectedEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class EngineEvent implements LiveKitEvent {
|
/// Disconnected from the room
|
||||||
const EngineEvent();
|
/// Emitted by [Room].
|
||||||
|
class RoomDisconnectedEvent with RoomEvent {
|
||||||
|
const RoomDisconnectedEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class TrackEvent implements LiveKitEvent {
|
/// When a new [RemoteParticipant] joins *after* the current participant has connected
|
||||||
const TrackEvent();
|
/// It will not fire for participants that are already in the room
|
||||||
|
/// Emitted by [Room].
|
||||||
|
class ParticipantConnectedEvent with RoomEvent {
|
||||||
|
final RemoteParticipant participant;
|
||||||
|
const ParticipantConnectedEvent({
|
||||||
|
required this.participant,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When a [RemoteParticipant] leaves the room
|
||||||
|
/// Emitted by [Room].
|
||||||
|
class ParticipantDisconnectedEvent with RoomEvent {
|
||||||
|
final RemoteParticipant participant;
|
||||||
|
const ParticipantDisconnectedEvent({
|
||||||
|
required this.participant,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Active speakers changed. List of speakers are ordered by their audio level.
|
||||||
|
/// loudest speakers first. This will include the [LocalParticipant] too.
|
||||||
|
class ActiveSpeakersChangedEvent with RoomEvent {
|
||||||
|
final List<Participant> speakers;
|
||||||
|
const ActiveSpeakersChangedEvent({
|
||||||
|
required this.speakers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class AudioPlaybackChangedEvent with RoomEvent {
|
||||||
|
const AudioPlaybackChangedEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When a new [Track] is published to [Room] *after* the current participant has
|
||||||
|
/// joined. It will not fire for tracks that are already published.
|
||||||
|
/// Emitted by [Room] and [RemoteParticipant].
|
||||||
|
class TrackPublishedEvent with RoomEvent, ParticipantEvent {
|
||||||
|
final RemoteParticipant participant;
|
||||||
|
final RemoteTrackPublication publication;
|
||||||
|
const TrackPublishedEvent({
|
||||||
|
required this.participant,
|
||||||
|
required this.publication,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The participant has unpublished one of their [Track].
|
||||||
|
/// Emitted by [Room] and [RemoteParticipant].
|
||||||
|
class TrackUnpublishedEvent with RoomEvent, ParticipantEvent {
|
||||||
|
final RemoteParticipant participant;
|
||||||
|
final RemoteTrackPublication publication;
|
||||||
|
const TrackUnpublishedEvent({
|
||||||
|
required this.participant,
|
||||||
|
required this.publication,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [LocalParticipant] has subscribed to a new track published by a
|
||||||
|
/// [RemoteParticipant].
|
||||||
|
/// Emitted by [Room] and [RemoteParticipant].
|
||||||
|
class TrackSubscribedEvent with RoomEvent, ParticipantEvent {
|
||||||
|
final RemoteParticipant participant;
|
||||||
|
final Track track;
|
||||||
|
final RemoteTrackPublication publication;
|
||||||
|
const TrackSubscribedEvent({
|
||||||
|
required this.participant,
|
||||||
|
required this.track,
|
||||||
|
required this.publication,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 {
|
||||||
|
final RemoteParticipant participant;
|
||||||
|
final String? sid;
|
||||||
|
final TrackSubscribeFailReason reason;
|
||||||
|
const TrackSubscriptionExceptionEvent({
|
||||||
|
required this.participant,
|
||||||
|
this.sid,
|
||||||
|
required this.reason,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The [LocalParticipant] has unsubscribed from a track published by a
|
||||||
|
/// [RemoteParticipant]. This event is fired when the track was unpublished.
|
||||||
|
/// Emitted by [Room] and [RemoteParticipant].
|
||||||
|
class TrackUnsubscribedEvent with RoomEvent, ParticipantEvent {
|
||||||
|
final RemoteParticipant participant;
|
||||||
|
final Track track;
|
||||||
|
final RemoteTrackPublication publication;
|
||||||
|
const TrackUnsubscribedEvent({
|
||||||
|
required this.participant,
|
||||||
|
required this.track,
|
||||||
|
required this.publication,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A Participant has muted one of the track.
|
||||||
|
/// Emitted on [RemoteParticipant] and [LocalParticipant].
|
||||||
|
class TrackMutedEvent with RoomEvent, ParticipantEvent {
|
||||||
|
final Participant participant;
|
||||||
|
final TrackPublication track;
|
||||||
|
const TrackMutedEvent({
|
||||||
|
required this.participant,
|
||||||
|
required this.track,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This participant has unmuted one of their tracks
|
||||||
|
/// Emitted on [RemoteParticipant] and [LocalParticipant].
|
||||||
|
class TrackUnmutedEvent with RoomEvent, ParticipantEvent {
|
||||||
|
final Participant participant;
|
||||||
|
final TrackPublication track;
|
||||||
|
const TrackUnmutedEvent({
|
||||||
|
required this.participant,
|
||||||
|
required this.track,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Room events
|
// common events for both Room/Participant.
|
||||||
//
|
//
|
||||||
class RoomReconnectingEvent extends RoomEvent {}
|
|
||||||
|
|
||||||
class RoomReconnectedEvent extends RoomEvent {}
|
/// Participant metadata is a simple way for app-specific state to be pushed to
|
||||||
|
/// all users. When RoomService.UpdateParticipantMetadata is called to change a
|
||||||
|
/// [Participant]'s state, *all* [Participant]s in the room will fire this event.
|
||||||
|
/// Emitted on [Participant].
|
||||||
|
class ParticipantMetadataUpdatedEvent with RoomEvent, ParticipantEvent {
|
||||||
|
final Participant participant;
|
||||||
|
const ParticipantMetadataUpdatedEvent({
|
||||||
|
required this.participant,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class RoomDisconnectedEvent extends RoomEvent {}
|
/// Data received from [RemoteParticipant].
|
||||||
|
/// Data packets provides the ability to use LiveKit to send/receive arbitrary
|
||||||
|
/// payloads.
|
||||||
|
/// Emitted on [Room] and [RemoteParticipant].
|
||||||
|
class DataReceivedEvent with RoomEvent, ParticipantEvent {
|
||||||
|
/// Sender of the data. This may be null if data is sent from Server API.
|
||||||
|
final RemoteParticipant? participant;
|
||||||
|
final List<int> data;
|
||||||
|
const DataReceivedEvent({
|
||||||
|
required this.participant,
|
||||||
|
required this.data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class RoomParticipantConnectedEvent extends RoomEvent {}
|
/// The participant's isSpeaking property has changed
|
||||||
|
/// Emitted on [Participant].
|
||||||
class RoomParticipantDisconnectedEvent extends RoomEvent {}
|
class SpeakingChangedEvent with RoomEvent, ParticipantEvent {
|
||||||
|
final Participant participant;
|
||||||
class RoomTrackPublishedEvent extends RoomEvent {}
|
final bool speaking;
|
||||||
|
const SpeakingChangedEvent({
|
||||||
class RoomTrackSubscribedEvent extends RoomEvent {}
|
required this.participant,
|
||||||
|
required this.speaking,
|
||||||
class RoomTrackSubscriptionFailedEvent extends RoomEvent {}
|
});
|
||||||
|
}
|
||||||
class RoomTrackUnpublishedEvent extends RoomEvent {}
|
|
||||||
|
|
||||||
class RoomTrackUnsubscribedEvent extends RoomEvent {}
|
|
||||||
|
|
||||||
class RoomTrackMutedEvent extends RoomEvent {}
|
|
||||||
|
|
||||||
class RoomTrackUnmutedEvent extends RoomEvent {}
|
|
||||||
|
|
||||||
class RoomActiveSpeakerChangedEvent extends RoomEvent {}
|
|
||||||
|
|
||||||
class RoomMetadataChangedEvent extends RoomEvent {}
|
|
||||||
|
|
||||||
class RoomDataReceivedEvent extends RoomEvent {}
|
|
||||||
|
|
||||||
class RoomAudioPlaybackChangedEvent extends RoomEvent {}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Participant events
|
|
||||||
//
|
|
||||||
class ParticipantTrackPublishedEvent extends ParticipantEvent {}
|
|
||||||
|
|
||||||
class ParticipantTrackSubscribedEvent extends ParticipantEvent {}
|
|
||||||
|
|
||||||
class ParticipantTrackSubscriptionFailedEvent extends ParticipantEvent {}
|
|
||||||
|
|
||||||
class ParticipantTrackUnpublishedEvent extends ParticipantEvent {}
|
|
||||||
|
|
||||||
class ParticipantTrackUnsubscribedEvent extends ParticipantEvent {}
|
|
||||||
|
|
||||||
class ParticipantTrackMutedEvent extends ParticipantEvent {}
|
|
||||||
|
|
||||||
class ParticipantTrackUnmutedEvent extends ParticipantEvent {}
|
|
||||||
|
|
||||||
class ParticipantMetadataChangedEvent extends ParticipantEvent {}
|
|
||||||
|
|
||||||
class ParticipantDataReceivedEvent extends ParticipantEvent {}
|
|
||||||
|
|
||||||
class ParticipantSpeakingChangedEvent extends ParticipantEvent {}
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Engine events
|
// Engine events
|
||||||
//
|
//
|
||||||
class EngineConnectedEvent extends EngineEvent {}
|
class EngineConnectedEvent with EngineEvent {
|
||||||
|
const EngineConnectedEvent();
|
||||||
|
}
|
||||||
|
|
||||||
class EngineDisconnectedEvent extends EngineEvent {}
|
class EngineDisconnectedEvent with EngineEvent {
|
||||||
|
const EngineDisconnectedEvent();
|
||||||
|
}
|
||||||
|
|
||||||
class EngineReconnectingEvent extends EngineEvent {}
|
class EngineReconnectingEvent with EngineEvent {
|
||||||
|
const EngineReconnectingEvent();
|
||||||
|
}
|
||||||
|
|
||||||
class EngineReconnectedEvent extends EngineEvent {}
|
class EngineReconnectedEvent with EngineEvent {
|
||||||
|
const EngineReconnectedEvent();
|
||||||
|
}
|
||||||
|
|
||||||
class EngineParticipantUpdateEvent extends EngineEvent {
|
class EngineParticipantUpdateEvent with EngineEvent {
|
||||||
final List<lk_models.ParticipantInfo> participants;
|
final List<lk_models.ParticipantInfo> participants;
|
||||||
const EngineParticipantUpdateEvent({
|
const EngineParticipantUpdateEvent({
|
||||||
required this.participants,
|
required this.participants,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class EngineMediaTrackAddedEvent extends EngineEvent {
|
class EngineTrackAddedEvent with EngineEvent {
|
||||||
final rtc.MediaStreamTrack track;
|
final rtc.MediaStreamTrack track;
|
||||||
final rtc.MediaStream? stream;
|
final rtc.MediaStream stream;
|
||||||
final rtc.RTCRtpReceiver? receiver;
|
final rtc.RTCRtpReceiver? receiver;
|
||||||
const EngineMediaTrackAddedEvent({
|
const EngineTrackAddedEvent({
|
||||||
required this.track,
|
required this.track,
|
||||||
required this.stream,
|
required this.stream,
|
||||||
required this.receiver,
|
required this.receiver,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class EngineSpeakersUpdateEvent extends EngineEvent {
|
class EngineSpeakersUpdateEvent with EngineEvent {
|
||||||
final List<lk_models.SpeakerInfo> speakers;
|
final List<lk_models.SpeakerInfo> speakers;
|
||||||
const EngineSpeakersUpdateEvent({
|
const EngineSpeakersUpdateEvent({
|
||||||
required this.speakers,
|
required this.speakers,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class EngineDataPacketReceivedEvent extends EngineEvent {
|
class EngineDataPacketReceivedEvent with EngineEvent {
|
||||||
final lk_models.UserPacket packet;
|
final lk_models.UserPacket packet;
|
||||||
final lk_models.DataPacket_Kind kind;
|
final lk_models.DataPacket_Kind kind;
|
||||||
const EngineDataPacketReceivedEvent({
|
const EngineDataPacketReceivedEvent({
|
||||||
@@ -123,7 +260,7 @@ class EngineDataPacketReceivedEvent extends EngineEvent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class EngineRemoteMuteChangedEvent extends EngineEvent {
|
class EngineRemoteMuteChangedEvent with EngineEvent {
|
||||||
final String sid;
|
final String sid;
|
||||||
final bool muted;
|
final bool muted;
|
||||||
const EngineRemoteMuteChangedEvent({
|
const EngineRemoteMuteChangedEvent({
|
||||||
@@ -133,7 +270,7 @@ class EngineRemoteMuteChangedEvent extends EngineEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// added
|
// added
|
||||||
abstract class EngineIceStateUpdatedEvent implements EngineEvent {
|
abstract class EngineIceStateUpdatedEvent with EngineEvent {
|
||||||
final rtc.RTCIceConnectionState iceState;
|
final rtc.RTCIceConnectionState iceState;
|
||||||
final bool isPrimary;
|
final bool isPrimary;
|
||||||
const EngineIceStateUpdatedEvent({
|
const EngineIceStateUpdatedEvent({
|
||||||
@@ -166,16 +303,101 @@ class EnginePublisherIceStateUpdatedEvent extends EngineIceStateUpdatedEvent {
|
|||||||
// Track events
|
// Track events
|
||||||
//
|
//
|
||||||
|
|
||||||
class TrackMessageEvent extends TrackEvent {}
|
class TrackMessageEvent with TrackEvent {
|
||||||
|
const TrackMessageEvent();
|
||||||
|
}
|
||||||
|
|
||||||
class TrackMutedEvent extends TrackEvent {}
|
class TrackUpdateSettingsEvent with TrackEvent {
|
||||||
|
const TrackUpdateSettingsEvent();
|
||||||
|
}
|
||||||
|
|
||||||
class TrackUnmutedEvent extends TrackEvent {}
|
class TrackUpdateSubscriptionEvent with TrackEvent {
|
||||||
|
const TrackUpdateSubscriptionEvent();
|
||||||
|
}
|
||||||
|
|
||||||
class TrackUpdateSettingsEvent extends TrackEvent {}
|
class TrackAudioPlaybackStartedEvent with TrackEvent {
|
||||||
|
const TrackAudioPlaybackStartedEvent();
|
||||||
|
}
|
||||||
|
|
||||||
class TrackUpdateSubscriptionEvent extends TrackEvent {}
|
class TrackAudioPlaybackFailedEvent with TrackEvent {
|
||||||
|
const TrackAudioPlaybackFailedEvent();
|
||||||
|
}
|
||||||
|
|
||||||
class TrackAudioPlaybackStartedEvent extends TrackEvent {}
|
//
|
||||||
|
// Signal events
|
||||||
|
//
|
||||||
|
class SignalConnectedEvent with SignalEvent {
|
||||||
|
final lk_rtc.JoinResponse response;
|
||||||
|
const SignalConnectedEvent({
|
||||||
|
required this.response,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class TrackAudioPlaybackFailedEvent extends TrackEvent {}
|
class SignalCloseEvent with SignalEvent {
|
||||||
|
final CloseReason? reason;
|
||||||
|
const SignalCloseEvent({
|
||||||
|
this.reason,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class SignalOfferEvent with SignalEvent {
|
||||||
|
final rtc.RTCSessionDescription sd;
|
||||||
|
const SignalOfferEvent({
|
||||||
|
required this.sd,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class SignalAnswerEvent with SignalEvent {
|
||||||
|
final rtc.RTCSessionDescription sd;
|
||||||
|
const SignalAnswerEvent({
|
||||||
|
required this.sd,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class SignalTrickleEvent with SignalEvent {
|
||||||
|
final rtc.RTCIceCandidate candidate;
|
||||||
|
final lk_rtc.SignalTarget target;
|
||||||
|
const SignalTrickleEvent({
|
||||||
|
required this.candidate,
|
||||||
|
required this.target,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class SignalParticipantUpdateEvent with SignalEvent {
|
||||||
|
final List<lk_models.ParticipantInfo> updates;
|
||||||
|
const SignalParticipantUpdateEvent({
|
||||||
|
required this.updates,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class SignalLocalTrackPublishedEvent with SignalEvent {
|
||||||
|
final String cid;
|
||||||
|
final lk_models.TrackInfo track;
|
||||||
|
const SignalLocalTrackPublishedEvent({
|
||||||
|
required this.cid,
|
||||||
|
required this.track,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class SignalActiveSpeakersChangedEvent with SignalEvent {
|
||||||
|
final List<lk_models.SpeakerInfo> speakers;
|
||||||
|
const SignalActiveSpeakersChangedEvent({
|
||||||
|
required this.speakers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class SignalLeaveEvent with SignalEvent {
|
||||||
|
final bool canReconnect;
|
||||||
|
const SignalLeaveEvent({
|
||||||
|
required this.canReconnect,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class SignalMuteTrackEvent with SignalEvent {
|
||||||
|
final String sid;
|
||||||
|
final bool muted;
|
||||||
|
const SignalMuteTrackEvent({
|
||||||
|
required this.sid,
|
||||||
|
required this.muted,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
+50
-38
@@ -2,43 +2,51 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
||||||
|
|
||||||
import 'proto/livekit_rtc.pb.dart' as lk_rtc;
|
import 'events.dart';
|
||||||
|
import 'managers/event.dart';
|
||||||
import 'proto/livekit_models.pb.dart' as lk_models;
|
import 'proto/livekit_models.pb.dart' as lk_models;
|
||||||
|
import 'proto/livekit_rtc.pb.dart' as lk_rtc;
|
||||||
import 'types.dart';
|
import 'types.dart';
|
||||||
|
|
||||||
|
extension DataPacketKindExt on lk_models.DataPacket_Kind {
|
||||||
|
Reliability toSDKType() => {
|
||||||
|
lk_models.DataPacket_Kind.RELIABLE: Reliability.reliable,
|
||||||
|
lk_models.DataPacket_Kind.LOSSY: Reliability.lossy,
|
||||||
|
}[this]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
extension LiveKitEventExt on Iterable<EventsEmitter<LiveKitEvent>> {
|
||||||
|
void emit(LiveKitEvent event) => forEach((emitter) => emitter.emit(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ICEServerExt on lk_rtc.ICEServer {
|
||||||
|
RTCIceServer toSDKType() => RTCIceServer(
|
||||||
|
urls: urls,
|
||||||
|
username: username.isNotEmpty ? username : null,
|
||||||
|
credential: credential.isNotEmpty ? username : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
extension IterableExt<E> on Iterable<E> {
|
extension IterableExt<E> on Iterable<E> {
|
||||||
E? elementAtOrNull(int index) => (index >= 0 && index < length) ? elementAt(index) : null;
|
E? elementAtOrNull(int index) => (index >= 0 && index < length) ? elementAt(index) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
extension RTCIceConnectionStateExt on rtc.RTCIceConnectionState {
|
|
||||||
bool isConnected() => [
|
|
||||||
rtc.RTCIceConnectionState.RTCIceConnectionStateConnected,
|
|
||||||
rtc.RTCIceConnectionState.RTCIceConnectionStateCompleted,
|
|
||||||
].contains(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ObjectExt on Object {
|
extension ObjectExt on Object {
|
||||||
String get objectId => '${runtimeType}#${hashCode}';
|
String get objectId => '${runtimeType}#${hashCode}';
|
||||||
}
|
}
|
||||||
|
|
||||||
extension RTCIceTransportPolicyExt on RTCIceTransportPolicy {
|
extension ProtocolVersionExt on ProtocolVersion {
|
||||||
String toStringValue() => {
|
String toStringValue() => {
|
||||||
RTCIceTransportPolicy.all: 'all',
|
ProtocolVersion.protocol2: '2',
|
||||||
RTCIceTransportPolicy.relay: 'relay',
|
ProtocolVersion.protocol3: '3',
|
||||||
}[this]!;
|
}[this]!;
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SessionDescriptionExt on lk_rtc.SessionDescription {
|
extension ReliabilityExt on Reliability {
|
||||||
rtc.RTCSessionDescription toSDKType() {
|
lk_models.DataPacket_Kind toPBType() => {
|
||||||
return rtc.RTCSessionDescription(sdp, type);
|
Reliability.reliable: lk_models.DataPacket_Kind.RELIABLE,
|
||||||
}
|
Reliability.lossy: lk_models.DataPacket_Kind.LOSSY,
|
||||||
}
|
}[this]!;
|
||||||
|
|
||||||
extension RTCSessionDescriptionExt on rtc.RTCSessionDescription {
|
|
||||||
lk_rtc.SessionDescription toSDKType() {
|
|
||||||
return lk_rtc.SessionDescription(type: type, sdp: sdp);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension RTCIceCandidateExt on rtc.RTCIceCandidate {
|
extension RTCIceCandidateExt on rtc.RTCIceCandidate {
|
||||||
@@ -54,26 +62,30 @@ extension RTCIceCandidateExt on rtc.RTCIceCandidate {
|
|||||||
String toJson() => json.encode(toMap());
|
String toJson() => json.encode(toMap());
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ICEServerExt on lk_rtc.ICEServer {
|
extension RTCIceConnectionStateExt on rtc.RTCIceConnectionState {
|
||||||
RTCIceServer toSDKType() => RTCIceServer(
|
bool isConnected() => [
|
||||||
urls: urls,
|
rtc.RTCIceConnectionState.RTCIceConnectionStateConnected,
|
||||||
username: username.isNotEmpty ? username : null,
|
rtc.RTCIceConnectionState.RTCIceConnectionStateCompleted,
|
||||||
credential: credential.isNotEmpty ? username : null,
|
].contains(this);
|
||||||
);
|
}
|
||||||
|
|
||||||
|
extension RTCIceTransportPolicyExt on RTCIceTransportPolicy {
|
||||||
|
String toStringValue() => {
|
||||||
|
RTCIceTransportPolicy.all: 'all',
|
||||||
|
RTCIceTransportPolicy.relay: 'relay',
|
||||||
|
}[this]!;
|
||||||
}
|
}
|
||||||
|
|
||||||
// not so neat to directly expose protobuf types so we
|
// not so neat to directly expose protobuf types so we
|
||||||
// define our own types (and convert methods)
|
// define our own types (and convert methods)
|
||||||
extension DataPacketKindExt on lk_models.DataPacket_Kind {
|
extension RTCSessionDescriptionExt on rtc.RTCSessionDescription {
|
||||||
Reliability toSDKType() => {
|
lk_rtc.SessionDescription toSDKType() {
|
||||||
lk_models.DataPacket_Kind.RELIABLE: Reliability.reliable,
|
return lk_rtc.SessionDescription(type: type, sdp: sdp);
|
||||||
lk_models.DataPacket_Kind.LOSSY: Reliability.lossy,
|
}
|
||||||
}[this]!;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ReliabilityExt on Reliability {
|
extension SessionDescriptionExt on lk_rtc.SessionDescription {
|
||||||
lk_models.DataPacket_Kind toPBType() => {
|
rtc.RTCSessionDescription toSDKType() {
|
||||||
Reliability.reliable: lk_models.DataPacket_Kind.RELIABLE,
|
return rtc.RTCSessionDescription(sdp, type);
|
||||||
Reliability.lossy: lk_models.DataPacket_Kind.LOSSY,
|
}
|
||||||
}[this]!;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,10 @@ class LiveKitClient {
|
|||||||
String url,
|
String url,
|
||||||
String token, {
|
String token, {
|
||||||
ConnectOptions? options,
|
ConnectOptions? options,
|
||||||
}) {
|
}) =>
|
||||||
final room = Room();
|
Room.connect(
|
||||||
return room.connect(
|
|
||||||
url,
|
url,
|
||||||
token,
|
token,
|
||||||
options: options,
|
options: options,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
//
|
|
||||||
//
|
|
||||||
//
|
|
||||||
import 'package:async/async.dart';
|
import 'package:async/async.dart';
|
||||||
|
|
||||||
class CancelableDelayManager {
|
class CancelableDelayManager {
|
||||||
|
|||||||
+45
-18
@@ -1,19 +1,24 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:synchronized/synchronized.dart' as sync;
|
||||||
|
|
||||||
import '../errors.dart';
|
import '../errors.dart';
|
||||||
import '../events.dart';
|
|
||||||
import '../extensions.dart';
|
import '../extensions.dart';
|
||||||
import '../logger.dart';
|
import '../logger.dart';
|
||||||
import '../types.dart';
|
import '../types.dart';
|
||||||
|
|
||||||
// Type-safe, multi-listenable, dispose safe event handling
|
// Type-safe, multi-listenable, dispose safe event handling
|
||||||
|
// TODO: Move to a separate package
|
||||||
|
|
||||||
class EventsEmitter<T extends LiveKitEvent> extends EventsListenable<T> {
|
class EventsEmitter<T> extends EventsListenable<T> {
|
||||||
// suppport for multiple event listeners
|
// suppport for multiple event listeners
|
||||||
final streamCtrl = StreamController<T>.broadcast(sync: false);
|
final streamCtrl = StreamController<T>.broadcast(sync: false);
|
||||||
|
|
||||||
|
EventsEmitter({
|
||||||
|
bool listenSynchronized = false,
|
||||||
|
}) : super(synchronized: listenSynchronized);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
EventsEmitter<T> get emitter => this;
|
EventsEmitter<T> get emitter => this;
|
||||||
|
|
||||||
@@ -25,6 +30,7 @@ class EventsEmitter<T extends LiveKitEvent> extends EventsListenable<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@mustCallSuper
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
await streamCtrl.close();
|
await streamCtrl.close();
|
||||||
await super.dispose();
|
await super.dispose();
|
||||||
@@ -32,21 +38,31 @@ class EventsEmitter<T extends LiveKitEvent> extends EventsListenable<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// for listening only
|
// for listening only
|
||||||
class EventsListener<T extends LiveKitEvent> extends EventsListenable<T> {
|
class EventsListener<T> extends EventsListenable<T> {
|
||||||
@override
|
@override
|
||||||
final EventsEmitter<T> emitter;
|
final EventsEmitter<T> emitter;
|
||||||
|
|
||||||
EventsListener({
|
EventsListener(
|
||||||
required this.emitter,
|
this.emitter, {
|
||||||
});
|
bool synchronized = false,
|
||||||
|
}) : super(
|
||||||
|
synchronized: synchronized,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensures all listeners will close on dispose
|
// ensures all listeners will close on dispose
|
||||||
abstract class EventsListenable<T extends LiveKitEvent> {
|
abstract class EventsListenable<T> {
|
||||||
// the emitter to listen to
|
// the emitter to listen to
|
||||||
EventsEmitter<T> get emitter;
|
EventsEmitter<T> get emitter;
|
||||||
|
|
||||||
|
bool synchronized;
|
||||||
// keep track of listeners to cancel later
|
// keep track of listeners to cancel later
|
||||||
final _listeners = <StreamSubscription<T>>[];
|
final _listeners = <StreamSubscription<T>>[];
|
||||||
|
final _syncLock = sync.Lock();
|
||||||
|
|
||||||
|
EventsListenable({
|
||||||
|
required this.synchronized,
|
||||||
|
});
|
||||||
|
|
||||||
@mustCallSuper
|
@mustCallSuper
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
@@ -58,8 +74,19 @@ abstract class EventsListenable<T extends LiveKitEvent> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// listens to all events, guaranteed to be cancelled on dispose
|
// listens to all events, guaranteed to be cancelled on dispose
|
||||||
CancelListenFunc listen(Function(T) onEvent) {
|
CancelListenFunc listen(FutureOr<void> Function(T) onEvent) {
|
||||||
final listener = emitter.streamCtrl.stream.listen(onEvent);
|
//
|
||||||
|
FutureOr<void> Function(T) _func = onEvent;
|
||||||
|
if (synchronized) {
|
||||||
|
// ensure `onEvent` will trigger one by one (waits for previous `onEvent` to complete)
|
||||||
|
_func = (event) async {
|
||||||
|
await _syncLock.synchronized(() async {
|
||||||
|
await onEvent(event);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
final listener = emitter.streamCtrl.stream.listen(_func);
|
||||||
_listeners.add(listener);
|
_listeners.add(listener);
|
||||||
|
|
||||||
// make a cancel func to cancel listening and remove from list in 1 call
|
// make a cancel func to cancel listening and remove from list in 1 call
|
||||||
@@ -74,34 +101,34 @@ abstract class EventsListenable<T extends LiveKitEvent> {
|
|||||||
|
|
||||||
// convenience method to listen & filter a specific event type
|
// convenience method to listen & filter a specific event type
|
||||||
CancelListenFunc on<E>(
|
CancelListenFunc on<E>(
|
||||||
Function(E) then, {
|
FutureOr<void> Function(E) then, {
|
||||||
bool Function(E)? filter,
|
bool Function(E)? filter,
|
||||||
}) =>
|
}) =>
|
||||||
listen((event) {
|
listen((event) async {
|
||||||
// event must be E
|
// event must be E
|
||||||
if (event is! E) return;
|
if (event is! E) return;
|
||||||
// filter must be true (if filter is used)
|
// filter must be true (if filter is used)
|
||||||
if (filter != null && !filter(event as E)) return;
|
if (filter != null && !filter(event)) return;
|
||||||
// cast to E
|
// cast to E
|
||||||
then(event as E);
|
await then(event);
|
||||||
});
|
});
|
||||||
|
|
||||||
// waits for a specific event type
|
// waits for a specific event type
|
||||||
Future<void> waitFor<E>({
|
Future<E> waitFor<E>({
|
||||||
required Duration duration,
|
required Duration duration,
|
||||||
bool Function(E)? filter,
|
bool Function(E)? filter,
|
||||||
FutureOr<void> Function()? onTimeout,
|
FutureOr<E> Function()? onTimeout,
|
||||||
}) async {
|
}) async {
|
||||||
final completer = Completer<void>();
|
final completer = Completer<E>();
|
||||||
|
|
||||||
final _cancelFunc = on<E>(
|
final _cancelFunc = on<E>(
|
||||||
(event) => completer.complete(),
|
(event) => completer.complete(event),
|
||||||
filter: filter,
|
filter: filter,
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// wait to complete with timeout
|
// wait to complete with timeout
|
||||||
await completer.future.timeout(
|
return await completer.future.timeout(
|
||||||
duration,
|
duration,
|
||||||
onTimeout: onTimeout ?? () => throw TimeoutException(),
|
onTimeout: onTimeout ?? () => throw TimeoutException(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,15 +2,16 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
||||||
|
|
||||||
import '../errors.dart';
|
import '../errors.dart';
|
||||||
|
import '../events.dart';
|
||||||
import '../extensions.dart';
|
import '../extensions.dart';
|
||||||
import '../logger.dart';
|
import '../logger.dart';
|
||||||
|
import '../managers/event.dart';
|
||||||
import '../options.dart';
|
import '../options.dart';
|
||||||
import '../proto/livekit_models.pb.dart' as lk_models;
|
import '../proto/livekit_models.pb.dart' as lk_models;
|
||||||
import '../rtc_engine.dart';
|
import '../rtc_engine.dart';
|
||||||
import '../track/local_audio_track.dart';
|
import '../track/local_audio_track.dart';
|
||||||
import '../track/local_track_publication.dart';
|
import '../track/local_track_publication.dart';
|
||||||
import '../track/local_video_track.dart';
|
import '../track/local_video_track.dart';
|
||||||
import '../track/track.dart';
|
|
||||||
import '../track/track_publication.dart';
|
import '../track/track_publication.dart';
|
||||||
import '../types.dart';
|
import '../types.dart';
|
||||||
import '../utils.dart';
|
import '../utils.dart';
|
||||||
@@ -25,8 +26,13 @@ class LocalParticipant extends Participant {
|
|||||||
required RTCEngine engine,
|
required RTCEngine engine,
|
||||||
required lk_models.ParticipantInfo info,
|
required lk_models.ParticipantInfo info,
|
||||||
this.defaultPublishOptions,
|
this.defaultPublishOptions,
|
||||||
|
required EventsEmitter<RoomEvent> roomEvents,
|
||||||
}) : _engine = engine,
|
}) : _engine = engine,
|
||||||
super(info.sid, info.identity) {
|
super(
|
||||||
|
info.sid,
|
||||||
|
info.identity,
|
||||||
|
roomEvents: roomEvents,
|
||||||
|
) {
|
||||||
updateFromInfo(info);
|
updateFromInfo(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,12 +141,17 @@ class LocalParticipant extends Participant {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Unpublish a track that's already published
|
/// Unpublish a track that's already published
|
||||||
Future<void> unpublishTrack(Track track) async {
|
@override
|
||||||
final existing = tracks.values.where((element) => element.track == track);
|
Future<void> unpublishTrack(String trackSid, {bool notify = false}) async {
|
||||||
if (existing.isEmpty) return;
|
logger.finer('Unpublish track sid: $trackSid, notify: $notify');
|
||||||
|
final pub = trackPublications.remove(trackSid);
|
||||||
final pub = existing.first;
|
if (pub is! LocalTrackPublication) return;
|
||||||
|
|
||||||
|
// final existing = tracks.values.where((element) => element.track == track);
|
||||||
|
// if (existing.isEmpty) return;
|
||||||
|
// final pub = existing.first;
|
||||||
|
final track = pub.track;
|
||||||
|
if (track != null) {
|
||||||
await track.stop();
|
await track.stop();
|
||||||
|
|
||||||
final sender = track.transceiver?.sender;
|
final sender = track.transceiver?.sender;
|
||||||
@@ -148,8 +159,7 @@ class LocalParticipant extends Participant {
|
|||||||
await engine.publisher?.pc.removeTrack(sender);
|
await engine.publisher?.pc.removeTrack(sender);
|
||||||
await engine.negotiate();
|
await engine.negotiate();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
tracks.remove(pub.sid);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Publish a new data payload to the room.
|
/// Publish a new data payload to the room.
|
||||||
|
|||||||
@@ -1,50 +1,16 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
import '../classes/change_notifier.dart';
|
||||||
import '../events.dart';
|
import '../events.dart';
|
||||||
|
import '../extensions.dart';
|
||||||
|
import '../logger.dart';
|
||||||
import '../managers/event.dart';
|
import '../managers/event.dart';
|
||||||
import '../proto/livekit_models.pb.dart' as lk_models;
|
import '../proto/livekit_models.pb.dart' as lk_models;
|
||||||
import '../track/remote_track_publication.dart';
|
|
||||||
import '../track/track.dart';
|
|
||||||
import '../track/track_publication.dart';
|
import '../track/track_publication.dart';
|
||||||
import 'remote_participant.dart';
|
import 'remote_participant.dart';
|
||||||
|
|
||||||
/// Callbacks for participant changes
|
|
||||||
mixin ParticipantDelegate {
|
|
||||||
/// The participant's metadata has changed
|
|
||||||
void onMetadataChanged(Participant participant) {}
|
|
||||||
|
|
||||||
/// The participant's isSpeaking property has changed
|
|
||||||
void onSpeakingChanged(Participant participant, bool speaking) {}
|
|
||||||
|
|
||||||
/// This participant has muted one of their tracks
|
|
||||||
void onTrackMuted(Participant participant, TrackPublication publication) {}
|
|
||||||
|
|
||||||
/// This participant has unmuted one of their tracks
|
|
||||||
void onTrackUnmuted(Participant participant, TrackPublication publication) {}
|
|
||||||
|
|
||||||
/// This participant has published a new [Track] to the [Room].
|
|
||||||
void onTrackPublished(RemoteParticipant participant, RemoteTrackPublication publication) {}
|
|
||||||
|
|
||||||
/// This participant has unpublished one of their [Track].
|
|
||||||
void onTrackUnpublished(RemoteParticipant participant, RemoteTrackPublication publication) {}
|
|
||||||
|
|
||||||
/// The [LocalParticipant] has subscribed to a new track published by this
|
|
||||||
/// [RemoteParticipant]
|
|
||||||
void onTrackSubscribed(
|
|
||||||
RemoteParticipant participant, Track track, RemoteTrackPublication publication) {}
|
|
||||||
|
|
||||||
/// The [LocalParticipant] has unsubscribed from a track published by this
|
|
||||||
/// [RemoteParticipant]. This event is fired when the track was unpublished
|
|
||||||
void onTrackUnsubscribed(
|
|
||||||
RemoteParticipant participant, Track track, RemoteTrackPublication publication) {}
|
|
||||||
|
|
||||||
/// Data received from this [RemoteParticipant].
|
|
||||||
void onDataReceived(RemoteParticipant participant, List<int> data) {}
|
|
||||||
|
|
||||||
/// An error has occured during track subscription.
|
|
||||||
void onTrackSubscriptionFailed(RemoteParticipant participant, String sid, String? message) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents a Participant in the room, notifies changes via delegates as
|
/// Represents a Participant in the room, notifies changes via delegates as
|
||||||
/// well as ChangeNotifier/providers.
|
/// well as ChangeNotifier/providers.
|
||||||
/// A change notification is triggered when
|
/// A change notification is triggered when
|
||||||
@@ -52,15 +18,18 @@ mixin ParticipantDelegate {
|
|||||||
/// - mute status changed
|
/// - mute status changed
|
||||||
/// - added/removed subscribed tracks
|
/// - added/removed subscribed tracks
|
||||||
/// - metadata changed
|
/// - metadata changed
|
||||||
class Participant extends ChangeNotifier {
|
|
||||||
|
/// Base for [RemoteParticipant] and [LocalParticipant],
|
||||||
|
/// can not be instantiated directly.
|
||||||
|
abstract class Participant extends LKChangeNotifier {
|
||||||
/// map of track sid => published track
|
/// map of track sid => published track
|
||||||
Map<String, TrackPublication> tracks = {};
|
final trackPublications = <String, TrackPublication>{};
|
||||||
|
|
||||||
/// audio level between 0-1, 1 being the loudest
|
/// audio level between 0-1, 1 being the loudest
|
||||||
double audioLevel = 0;
|
double audioLevel = 0;
|
||||||
|
|
||||||
/// server assigned unique id
|
/// server assigned unique id
|
||||||
String sid;
|
final String sid;
|
||||||
|
|
||||||
/// user-assigned identity
|
/// user-assigned identity
|
||||||
String identity;
|
String identity;
|
||||||
@@ -71,16 +40,12 @@ class Participant extends ChangeNotifier {
|
|||||||
/// when the participant had last spoken
|
/// when the participant had last spoken
|
||||||
DateTime? lastSpokeAt;
|
DateTime? lastSpokeAt;
|
||||||
|
|
||||||
ParticipantDelegate? roomDelegate;
|
|
||||||
|
|
||||||
/// delegate to receive participant callbacks
|
|
||||||
ParticipantDelegate? delegate;
|
|
||||||
|
|
||||||
lk_models.ParticipantInfo? _participantInfo;
|
lk_models.ParticipantInfo? _participantInfo;
|
||||||
bool _isSpeaking = false;
|
bool _isSpeaking = false;
|
||||||
|
|
||||||
// suppport for multiple event listeners
|
// suppport for multiple event listeners
|
||||||
final events = EventsEmitter<ParticipantEvent>();
|
final events = EventsEmitter<ParticipantEvent>();
|
||||||
|
final EventsEmitter<RoomEvent> roomEvents;
|
||||||
|
|
||||||
/// when the participant joined the room
|
/// when the participant joined the room
|
||||||
DateTime get joinedAt {
|
DateTime get joinedAt {
|
||||||
@@ -95,23 +60,40 @@ class Participant extends ChangeNotifier {
|
|||||||
bool get isSpeaking => _isSpeaking;
|
bool get isSpeaking => _isSpeaking;
|
||||||
|
|
||||||
/// true if participant is publishing an audio track and is muted
|
/// true if participant is publishing an audio track and is muted
|
||||||
bool get isMuted {
|
bool get isMuted => audioTracks.firstOrNull?.muted ?? true;
|
||||||
if (audioTracks.isEmpty) return false;
|
|
||||||
return audioTracks.first.muted;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get hasAudio => audioTracks.isNotEmpty;
|
bool get hasAudio => audioTracks.isNotEmpty;
|
||||||
|
|
||||||
bool get hasVideo => videoTracks.isNotEmpty;
|
bool get hasVideo => videoTracks.isNotEmpty;
|
||||||
|
|
||||||
/// tracks that are subscribed to
|
/// tracks that are subscribed to
|
||||||
List<TrackPublication> get subscribedTracks => tracks.values.where((e) => e.subscribed).toList();
|
List<TrackPublication> get subscribedTracks =>
|
||||||
|
trackPublications.values.where((e) => e.subscribed).toList();
|
||||||
|
|
||||||
/// for internal use
|
/// for internal use
|
||||||
/// {@nodoc}
|
/// {@nodoc}
|
||||||
|
@internal
|
||||||
bool get hasInfo => _participantInfo != null;
|
bool get hasInfo => _participantInfo != null;
|
||||||
|
|
||||||
Participant(this.sid, this.identity);
|
Participant(
|
||||||
|
this.sid,
|
||||||
|
this.identity, {
|
||||||
|
required this.roomEvents,
|
||||||
|
}) {
|
||||||
|
// Any event emitted will trigger ChangeNotifier
|
||||||
|
events.listen((event) {
|
||||||
|
logger.fine('[ParticipantEvent] $event, will notifyListeners()');
|
||||||
|
notifyListeners();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@mustCallSuper
|
||||||
|
Future<void> dispose() async {
|
||||||
|
logger.fine('$objectId dispose()');
|
||||||
|
await events.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
/// for internal use
|
/// for internal use
|
||||||
/// {@nodoc}
|
/// {@nodoc}
|
||||||
@@ -123,26 +105,29 @@ class Participant extends ChangeNotifier {
|
|||||||
if (speaking) {
|
if (speaking) {
|
||||||
lastSpokeAt = DateTime.now();
|
lastSpokeAt = DateTime.now();
|
||||||
}
|
}
|
||||||
delegate?.onSpeakingChanged(this, speaking);
|
|
||||||
roomDelegate?.onSpeakingChanged(this, speaking);
|
[events, roomEvents].emit(SpeakingChangedEvent(
|
||||||
notifyListeners();
|
participant: this,
|
||||||
|
speaking: speaking,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setMetadata(String md) {
|
void _setMetadata(String md) {
|
||||||
final changed = _participantInfo?.metadata != md;
|
final changed = _participantInfo?.metadata != md;
|
||||||
metadata = md;
|
metadata = md;
|
||||||
if (changed) {
|
if (changed) {
|
||||||
delegate?.onMetadataChanged(this);
|
[events, roomEvents].emit(ParticipantMetadataUpdatedEvent(
|
||||||
roomDelegate?.onMetadataChanged(this);
|
participant: this,
|
||||||
notifyListeners();
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// for internal use
|
/// for internal use
|
||||||
/// {@nodoc}
|
/// {@nodoc}
|
||||||
|
@internal
|
||||||
void updateFromInfo(lk_models.ParticipantInfo info) {
|
void updateFromInfo(lk_models.ParticipantInfo info) {
|
||||||
identity = info.identity;
|
identity = info.identity;
|
||||||
sid = info.sid;
|
// participantSid = info.sid;
|
||||||
if (info.metadata.isNotEmpty) {
|
if (info.metadata.isNotEmpty) {
|
||||||
_setMetadata(info.metadata);
|
_setMetadata(info.metadata);
|
||||||
}
|
}
|
||||||
@@ -151,23 +136,36 @@ class Participant extends ChangeNotifier {
|
|||||||
|
|
||||||
/// for internal use
|
/// for internal use
|
||||||
/// {@nodoc}
|
/// {@nodoc}
|
||||||
void muteChanged() {
|
@internal
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// for internal use
|
|
||||||
/// {@nodoc}
|
|
||||||
void addTrackPublication(TrackPublication pub) {
|
void addTrackPublication(TrackPublication pub) {
|
||||||
pub.track?.sid = pub.sid;
|
pub.track?.sid = pub.sid;
|
||||||
tracks[pub.sid] = pub;
|
trackPublications[pub.sid] = pub;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equality operators
|
||||||
|
// Object is considered equal when sid is equal
|
||||||
|
@override
|
||||||
|
int get hashCode => sid.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => other is Participant && sid == other.sid;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convenience extension
|
// Convenience extension
|
||||||
extension ParticipantExt on Participant {
|
extension ParticipantExt on Participant {
|
||||||
List<TrackPublication> get videoTracks =>
|
List<TrackPublication> get videoTracks =>
|
||||||
tracks.values.where((e) => e.kind == lk_models.TrackType.VIDEO).toList();
|
trackPublications.values.where((e) => e.kind == lk_models.TrackType.VIDEO).toList();
|
||||||
|
|
||||||
List<TrackPublication> get audioTracks =>
|
List<TrackPublication> get audioTracks =>
|
||||||
tracks.values.where((e) => e.kind == lk_models.TrackType.AUDIO).toList();
|
trackPublications.values.where((e) => e.kind == lk_models.TrackType.AUDIO).toList();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
import '../constants.dart';
|
||||||
|
import '../events.dart';
|
||||||
|
import '../extensions.dart';
|
||||||
import '../logger.dart';
|
import '../logger.dart';
|
||||||
|
import '../managers/event.dart';
|
||||||
import '../proto/livekit_models.pb.dart' as lk_models;
|
import '../proto/livekit_models.pb.dart' as lk_models;
|
||||||
import '../signal_client.dart';
|
import '../signal_client.dart';
|
||||||
import '../track/audio_track.dart';
|
import '../track/audio_track.dart';
|
||||||
import '../track/remote_track_publication.dart';
|
import '../track/remote_track_publication.dart';
|
||||||
import '../track/track.dart';
|
import '../track/track.dart';
|
||||||
import '../track/video_track.dart';
|
import '../track/video_track.dart';
|
||||||
|
import '../types.dart';
|
||||||
import 'participant.dart';
|
import 'participant.dart';
|
||||||
|
|
||||||
/// Represents other participant in the [Room].
|
/// Represents other participant in the [Room].
|
||||||
@@ -18,140 +24,154 @@ class RemoteParticipant extends Participant {
|
|||||||
RemoteParticipant(
|
RemoteParticipant(
|
||||||
this._client,
|
this._client,
|
||||||
String sid,
|
String sid,
|
||||||
String identity,
|
String identity, {
|
||||||
) : super(sid, identity);
|
required EventsEmitter<RoomEvent> roomEvents,
|
||||||
|
}) : super(
|
||||||
|
sid,
|
||||||
|
identity,
|
||||||
|
roomEvents: roomEvents,
|
||||||
|
);
|
||||||
|
|
||||||
RemoteParticipant.fromInfo(
|
RemoteParticipant.fromInfo(
|
||||||
this._client,
|
this._client,
|
||||||
lk_models.ParticipantInfo info,
|
lk_models.ParticipantInfo info, {
|
||||||
) : super(info.sid, info.identity) {
|
required EventsEmitter<RoomEvent> roomEvents,
|
||||||
|
}) : super(
|
||||||
|
info.sid,
|
||||||
|
info.identity,
|
||||||
|
roomEvents: roomEvents,
|
||||||
|
) {
|
||||||
updateFromInfo(info);
|
updateFromInfo(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoteTrackPublication? getTrackPublication(String sid) {
|
RemoteTrackPublication? getTrackPublication(String sid) {
|
||||||
final pub = tracks[sid];
|
final pub = trackPublications[sid];
|
||||||
if (pub is RemoteTrackPublication) return pub;
|
if (pub is RemoteTrackPublication) return pub;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// for internal use
|
/// for internal use
|
||||||
/// {@nodoc}
|
/// {@nodoc}
|
||||||
void addSubscribedMediaTrack(
|
@internal
|
||||||
|
Future<void> addSubscribedMediaTrack(
|
||||||
rtc.MediaStreamTrack mediaTrack,
|
rtc.MediaStreamTrack mediaTrack,
|
||||||
rtc.MediaStream stream,
|
rtc.MediaStream stream,
|
||||||
String? sid,
|
String trackSid,
|
||||||
) async {
|
) async {
|
||||||
if (sid == null) {
|
logger.fine('addSubscribedMediaTrack()');
|
||||||
const msg = 'addSubscribedMediaTrack received null sid';
|
|
||||||
delegate?.onTrackSubscriptionFailed(this, '', msg);
|
// If publication doesn't exist yet...
|
||||||
roomDelegate?.onTrackSubscriptionFailed(this, '', msg);
|
RemoteTrackPublication? pub = getTrackPublication(trackSid);
|
||||||
return;
|
if (pub == null) {
|
||||||
|
logger.fine('addSubscribedMediaTrack() pub is null, will wait...');
|
||||||
|
// Wait for the metadata to arrive
|
||||||
|
final event = await events.waitFor<TrackPublishedEvent>(
|
||||||
|
filter: (event) => event.participant == this && event.publication.sid == trackSid,
|
||||||
|
duration: Timeouts.publish,
|
||||||
|
onTimeout: () => throw TrackSubscriptionExceptionEvent(
|
||||||
|
participant: this,
|
||||||
|
sid: trackSid,
|
||||||
|
reason: TrackSubscribeFailReason.notTrackMetadataFound,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
pub = event.publication;
|
||||||
|
logger.fine('addSubscribedMediaTrack() did receive pub');
|
||||||
}
|
}
|
||||||
|
|
||||||
var pub = getTrackPublication(sid);
|
// Check if track type is supported, throw if not.
|
||||||
if (pub == null) {
|
if (![lk_models.TrackType.AUDIO, lk_models.TrackType.VIDEO].contains(pub.kind)) {
|
||||||
// we may have received the track prior to metadata. wait up to 3s
|
throw TrackSubscriptionExceptionEvent(
|
||||||
pub = await _waitForTrackPublication(sid, const Duration(seconds: 3));
|
participant: this,
|
||||||
if (pub == null) {
|
sid: trackSid,
|
||||||
const msg = 'no track metadata found';
|
reason: TrackSubscribeFailReason.unsupportedTrackType,
|
||||||
delegate?.onTrackSubscriptionFailed(this, sid, msg);
|
);
|
||||||
roomDelegate?.onTrackSubscriptionFailed(this, sid, msg);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Track? track;
|
// create Track
|
||||||
|
final Track track;
|
||||||
if (pub.kind == lk_models.TrackType.AUDIO) {
|
if (pub.kind == lk_models.TrackType.AUDIO) {
|
||||||
|
// audio track
|
||||||
final audioTrack = AudioTrack(pub.name, mediaTrack, stream);
|
final audioTrack = AudioTrack(pub.name, mediaTrack, stream);
|
||||||
audioTrack.start();
|
audioTrack.start();
|
||||||
track = audioTrack;
|
track = audioTrack;
|
||||||
} else if (pub.kind == lk_models.TrackType.VIDEO) {
|
|
||||||
track = VideoTrack(pub.name, mediaTrack, stream);
|
|
||||||
} else {
|
} else {
|
||||||
final msg = 'unsupported track type ${pub.kind}';
|
// video track
|
||||||
delegate?.onTrackSubscriptionFailed(this, sid, msg);
|
track = VideoTrack(pub.name, mediaTrack, stream);
|
||||||
roomDelegate?.onTrackSubscriptionFailed(this, sid, msg);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub.track = track;
|
pub.track = track;
|
||||||
addTrackPublication(pub);
|
addTrackPublication(pub);
|
||||||
|
|
||||||
delegate?.onTrackSubscribed(this, track, pub);
|
[events, roomEvents].emit(TrackSubscribedEvent(
|
||||||
roomDelegate?.onTrackSubscribed(this, track, pub);
|
participant: this,
|
||||||
notifyListeners();
|
track: track,
|
||||||
|
publication: pub,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// for internal use
|
/// for internal use
|
||||||
/// {@nodoc}
|
/// {@nodoc}
|
||||||
@override
|
@override
|
||||||
void updateFromInfo(lk_models.ParticipantInfo info) async {
|
@internal
|
||||||
|
Future<void> updateFromInfo(lk_models.ParticipantInfo info) async {
|
||||||
final hadInfo = hasInfo;
|
final hadInfo = hasInfo;
|
||||||
super.updateFromInfo(info);
|
super.updateFromInfo(info);
|
||||||
|
|
||||||
// figuring out deltas between tracks
|
// figuring out deltas between tracks
|
||||||
final validPubs = <String, RemoteTrackPublication>{};
|
final newPubs = <RemoteTrackPublication>{};
|
||||||
final newPubs = <String, RemoteTrackPublication>{};
|
|
||||||
|
|
||||||
for (final info in info.tracks) {
|
|
||||||
final sid = info.sid;
|
|
||||||
var pub = getTrackPublication(sid);
|
|
||||||
|
|
||||||
|
for (final trackInfo in info.tracks) {
|
||||||
|
RemoteTrackPublication? pub = getTrackPublication(trackInfo.sid);
|
||||||
if (pub == null) {
|
if (pub == null) {
|
||||||
pub = RemoteTrackPublication(info, this);
|
pub = RemoteTrackPublication(trackInfo, this);
|
||||||
newPubs[sid] = pub;
|
newPubs.add(pub);
|
||||||
addTrackPublication(pub);
|
addTrackPublication(pub);
|
||||||
} else {
|
} else {
|
||||||
pub.updateFromInfo(info);
|
pub.updateFromInfo(trackInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
validPubs[sid] = pub;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// notify listeners when it's not a new participant
|
// notify listeners when it's not a new participant
|
||||||
if (hadInfo) {
|
if (hadInfo) {
|
||||||
for (final pub in newPubs.values) {
|
for (final pub in newPubs) {
|
||||||
delegate?.onTrackPublished(this, pub);
|
final event = TrackPublishedEvent(
|
||||||
roomDelegate?.onTrackPublished(this, pub);
|
participant: this,
|
||||||
|
publication: pub,
|
||||||
|
);
|
||||||
|
[events, roomEvents].emit(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove tracks
|
// unpublish any track that is not in the info
|
||||||
final removeTrackSids =
|
final validSids = info.tracks.map((e) => e.sid);
|
||||||
tracks.values.where((e) => !validPubs.containsKey(e.sid)).map((e) => e.sid).toList();
|
final removeSids =
|
||||||
|
trackPublications.values.where((e) => !validSids.contains(e.sid)).map((e) => e.sid);
|
||||||
for (final sid in removeTrackSids) {
|
for (final sid in removeSids) {
|
||||||
await unpublishTrack(sid, true);
|
await unpublishTrack(sid, notify: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> unpublishTrack(String sid, [bool notify = false]) async {
|
@override
|
||||||
logger.finer('Unpublish track sid: $sid, notify: $notify');
|
Future<void> unpublishTrack(String trackSid, {bool notify = false}) async {
|
||||||
final pub = tracks.remove(sid);
|
logger.finer('Unpublish track sid: $trackSid, notify: $notify');
|
||||||
if (pub == null || pub is! RemoteTrackPublication) return;
|
final pub = trackPublications.remove(trackSid);
|
||||||
|
if (pub is! RemoteTrackPublication) return;
|
||||||
|
|
||||||
final track = pub.track;
|
final track = pub.track;
|
||||||
|
// if has track
|
||||||
if (track != null) {
|
if (track != null) {
|
||||||
await track.stop();
|
await track.stop();
|
||||||
delegate?.onTrackUnsubscribed(this, track, pub);
|
[events, roomEvents].emit(TrackUnsubscribedEvent(
|
||||||
roomDelegate?.onTrackUnsubscribed(this, track, pub);
|
participant: this,
|
||||||
notifyListeners();
|
track: track,
|
||||||
|
publication: pub,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (notify) {
|
if (notify) {
|
||||||
delegate?.onTrackUnpublished(this, pub);
|
[events, roomEvents].emit(TrackUnpublishedEvent(
|
||||||
roomDelegate?.onTrackUnpublished(this, pub);
|
participant: this,
|
||||||
}
|
publication: pub,
|
||||||
}
|
));
|
||||||
|
|
||||||
Future<RemoteTrackPublication?> _waitForTrackPublication(String sid, Duration delay) async {
|
|
||||||
final endTime = DateTime.now().add(delay);
|
|
||||||
while (DateTime.now().isBefore(endTime)) {
|
|
||||||
final pub =
|
|
||||||
await Future<RemoteTrackPublication?>.delayed(const Duration(milliseconds: 100), () {
|
|
||||||
return getTrackPublication(sid);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (pub != null) return pub;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+192
-251
@@ -2,8 +2,9 @@ import 'dart:async';
|
|||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
|
||||||
|
|
||||||
|
import 'classes/change_notifier.dart';
|
||||||
|
import 'constants.dart';
|
||||||
import 'errors.dart';
|
import 'errors.dart';
|
||||||
import 'events.dart';
|
import 'events.dart';
|
||||||
import 'extensions.dart';
|
import 'extensions.dart';
|
||||||
@@ -14,84 +15,13 @@ import 'participant/local_participant.dart';
|
|||||||
import 'participant/participant.dart';
|
import 'participant/participant.dart';
|
||||||
import 'participant/remote_participant.dart';
|
import 'participant/remote_participant.dart';
|
||||||
import 'proto/livekit_models.pb.dart' as lk_models;
|
import 'proto/livekit_models.pb.dart' as lk_models;
|
||||||
|
import 'proto/livekit_rtc.pb.dart' as lk_rtc;
|
||||||
import 'rtc_engine.dart';
|
import 'rtc_engine.dart';
|
||||||
import 'signal_client.dart';
|
import 'signal_client.dart';
|
||||||
import 'track/remote_track_publication.dart';
|
|
||||||
import 'track/track.dart';
|
import 'track/track.dart';
|
||||||
import 'track/track_publication.dart';
|
import 'track/track_publication.dart';
|
||||||
import 'types.dart';
|
import 'types.dart';
|
||||||
|
|
||||||
enum RoomState {
|
|
||||||
disconnected,
|
|
||||||
connected,
|
|
||||||
reconnecting,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delegate for [Room] callbacks
|
|
||||||
mixin RoomDelegate {
|
|
||||||
// room level callbacks
|
|
||||||
/// When the connection to the server has been interrupted and it's attempting
|
|
||||||
/// to reconnect.
|
|
||||||
void onReconnecting() {}
|
|
||||||
|
|
||||||
/// Connection to room is re-established. All existing state is preserved.
|
|
||||||
void onReconnected() {}
|
|
||||||
|
|
||||||
/// Disconnected from the room
|
|
||||||
void onDisconnected() {}
|
|
||||||
|
|
||||||
/// When a new [RemoteParticipant] joins *after* the current participant has connected
|
|
||||||
/// It will not fire for participants that are already in the room
|
|
||||||
void onParticipantConnected(Participant participant) {}
|
|
||||||
|
|
||||||
/// When a [RemoteParticipant] leaves the room
|
|
||||||
void onParticipantDisconnected(Participant participant) {}
|
|
||||||
|
|
||||||
/// Active speakers changed. List of speakers are ordered by their audio level.
|
|
||||||
/// loudest speakers first. This will include the [LocalParticipant] too.
|
|
||||||
void onActiveSpeakersChanged(List<Participant> participants) {}
|
|
||||||
|
|
||||||
// callbacks about participant events
|
|
||||||
|
|
||||||
/// Participant metadata is a simple way for app-specific state to be pushed to
|
|
||||||
/// all users.
|
|
||||||
/// When RoomService.UpdateParticipantMetadata is called to change a
|
|
||||||
/// participant's state, *all* participants in the room will fire this event.
|
|
||||||
void onMetadataChanged(Participant participant) {}
|
|
||||||
|
|
||||||
/// A track that was muted, fires on both [RemoteParticipant]s and
|
|
||||||
/// [LocalParticipant]
|
|
||||||
void onTrackMuted(Participant participant, TrackPublication publication) {}
|
|
||||||
|
|
||||||
/// A track that was unmuted, fires on both [RemoteParticipant]s and
|
|
||||||
/// [LocalParticipant]
|
|
||||||
void onTrackUnmuted(Participant participant, TrackPublication publication) {}
|
|
||||||
|
|
||||||
/// When a new track is published to room *after* the current participant has
|
|
||||||
/// joined. It will not fire for tracks that are already published
|
|
||||||
void onTrackPublished(RemoteParticipant participant, RemoteTrackPublication publication) {}
|
|
||||||
|
|
||||||
/// A [RemoteParticipant] has unpublished a track
|
|
||||||
void onTrackUnpublished(RemoteParticipant participant, RemoteTrackPublication publication) {}
|
|
||||||
|
|
||||||
/// The [LocalParticipant] has subscribed to a new track. This event will **always**
|
|
||||||
/// fire as long as new tracks are ready for use.
|
|
||||||
void onTrackSubscribed(
|
|
||||||
RemoteParticipant participant, Track track, RemoteTrackPublication publication) {}
|
|
||||||
|
|
||||||
/// A subscribed track is no longer available.
|
|
||||||
void onTrackUnsubscribed(
|
|
||||||
RemoteParticipant participant, Track track, RemoteTrackPublication publication) {}
|
|
||||||
|
|
||||||
/// Data received from another [RemoteParticipant].
|
|
||||||
/// Data packets provides the ability to use LiveKit to send/receive arbitrary
|
|
||||||
/// payloads.
|
|
||||||
void onDataReceived(RemoteParticipant participant, List<int> data) {}
|
|
||||||
|
|
||||||
/// Encountered failure attempting to subscribe to track.
|
|
||||||
void onTrackSubscriptionFailed(RemoteParticipant participant, String sid, String? message) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Room is the primary construct for LiveKit conferences. It contains a
|
/// Room is the primary construct for LiveKit conferences. It contains a
|
||||||
/// group of [Participant]s, each publishing and subscribing to [Track]s.
|
/// group of [Participant]s, each publishing and subscribing to [Track]s.
|
||||||
/// Notifies changes to its state via two ways, by assigning a delegate, or using
|
/// Notifies changes to its state via two ways, by assigning a delegate, or using
|
||||||
@@ -101,11 +31,12 @@ mixin RoomDelegate {
|
|||||||
/// * participant membership changes
|
/// * participant membership changes
|
||||||
/// * active speakers are different
|
/// * active speakers are different
|
||||||
/// {@category Room}
|
/// {@category Room}
|
||||||
class Room extends ChangeNotifier with ParticipantDelegate {
|
class Room extends LKChangeNotifier {
|
||||||
RoomState _connectionState = RoomState.disconnected;
|
// Room is only instantiated if connected, so defaults to connected.
|
||||||
|
ConnectionState _connectionState = ConnectionState.connected;
|
||||||
|
|
||||||
/// connection state of the room
|
/// connection state of the room
|
||||||
RoomState get state => _connectionState;
|
ConnectionState get connectionState => _connectionState;
|
||||||
|
|
||||||
final Map<String, RemoteParticipant> _participants = {};
|
final Map<String, RemoteParticipant> _participants = {};
|
||||||
|
|
||||||
@@ -114,13 +45,13 @@ class Room extends ChangeNotifier with ParticipantDelegate {
|
|||||||
UnmodifiableMapView(_participants);
|
UnmodifiableMapView(_participants);
|
||||||
|
|
||||||
/// the current participant
|
/// the current participant
|
||||||
late LocalParticipant localParticipant;
|
late final LocalParticipant localParticipant;
|
||||||
|
|
||||||
/// name of the room
|
/// name of the room
|
||||||
late String name;
|
late final String name;
|
||||||
|
|
||||||
/// sid of the room
|
/// sid of the room
|
||||||
late String sid;
|
late final String sid;
|
||||||
|
|
||||||
List<Participant> _activeSpeakers = [];
|
List<Participant> _activeSpeakers = [];
|
||||||
|
|
||||||
@@ -128,63 +59,28 @@ class Room extends ChangeNotifier with ParticipantDelegate {
|
|||||||
UnmodifiableListView<Participant> get activeSpeakers =>
|
UnmodifiableListView<Participant> get activeSpeakers =>
|
||||||
UnmodifiableListView<Participant>(_activeSpeakers);
|
UnmodifiableListView<Participant>(_activeSpeakers);
|
||||||
|
|
||||||
/// delegate for room events
|
final RTCEngine engine;
|
||||||
RoomDelegate? delegate;
|
|
||||||
|
|
||||||
final RTCEngine _engine;
|
|
||||||
|
|
||||||
// suppport for multiple event listeners
|
// suppport for multiple event listeners
|
||||||
final events = EventsEmitter<RoomEvent>();
|
final events = EventsEmitter<RoomEvent>();
|
||||||
late final _engineListener = EventsListener<EngineEvent>(emitter: _engine.events);
|
late final _engineListener = EventsListener<LiveKitEvent>(engine.events);
|
||||||
|
|
||||||
/// internal use
|
/// internal use
|
||||||
/// {@nodoc}
|
/// {@nodoc}
|
||||||
Room([RTCConfiguration? rtcConfig]) : _engine = RTCEngine(SignalClient(), rtcConfig) {
|
Room._({
|
||||||
_engine.onTrack = _onTrackAdded;
|
required this.engine,
|
||||||
_engine.onICEConnected = _handleICEConnected;
|
required lk_rtc.JoinResponse joinResponse,
|
||||||
_engine.onDisconnected = _handleDisconnect;
|
ConnectOptions? connectOptions,
|
||||||
_engine.onParticipantUpdated = _handleParticipantUpdate;
|
}) {
|
||||||
_engine.onActiveSpeakerUpdated = _handleSpeakerUpdate;
|
//
|
||||||
_engine.onDataMessage = _handleDataPacket;
|
_setUpListeners();
|
||||||
_engine.onRemoteMute = _onRemoteMuteChanged;
|
|
||||||
_engine.onReconnected = () {
|
|
||||||
_connectionState = RoomState.connected;
|
|
||||||
delegate?.onReconnected();
|
|
||||||
notifyListeners();
|
|
||||||
};
|
|
||||||
_engine.onReconnecting = () {
|
|
||||||
_connectionState = RoomState.reconnecting;
|
|
||||||
delegate?.onReconnecting();
|
|
||||||
notifyListeners();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> dispose() async {
|
|
||||||
await events.dispose();
|
|
||||||
await _engineListener.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Room> connect(
|
|
||||||
String url,
|
|
||||||
String token, {
|
|
||||||
ConnectOptions? options,
|
|
||||||
}) async {
|
|
||||||
final joinResponse = await _engine.join(
|
|
||||||
url,
|
|
||||||
token,
|
|
||||||
options: options,
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.fine('connected to LiveKit server, version: ${joinResponse.serverVersion}');
|
|
||||||
|
|
||||||
localParticipant = LocalParticipant(
|
localParticipant = LocalParticipant(
|
||||||
engine: _engine,
|
engine: engine,
|
||||||
info: joinResponse.participant,
|
info: joinResponse.participant,
|
||||||
defaultPublishOptions: options?.defaultPublishOptions,
|
defaultPublishOptions: connectOptions?.defaultPublishOptions,
|
||||||
|
roomEvents: events,
|
||||||
);
|
);
|
||||||
localParticipant.roomDelegate = this;
|
|
||||||
|
|
||||||
sid = joinResponse.room.sid;
|
sid = joinResponse.room.sid;
|
||||||
name = joinResponse.room.name;
|
name = joinResponse.room.name;
|
||||||
@@ -193,89 +89,199 @@ class Room extends ChangeNotifier with ParticipantDelegate {
|
|||||||
_getOrCreateRemoteParticipant(info.sid, info);
|
_getOrCreateRemoteParticipant(info.sid, info);
|
||||||
}
|
}
|
||||||
|
|
||||||
// room is not ready until ICE is connected.
|
// Any event emitted will trigger ChangeNotifier
|
||||||
|
events.listen((event) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Room> connect(
|
||||||
|
String url,
|
||||||
|
String token, {
|
||||||
|
ConnectOptions? options,
|
||||||
|
RTCConfiguration? rtcConfig,
|
||||||
|
}) async {
|
||||||
|
//
|
||||||
|
final engine = RTCEngine(
|
||||||
|
SignalClient(),
|
||||||
|
rtcConfig,
|
||||||
|
);
|
||||||
|
|
||||||
|
Room? room;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await _engineListener.waitFor<EngineIceStateUpdatedEvent>(
|
final joinResponse = await engine.join(
|
||||||
filter: (event) => event.iceState.isConnected(),
|
url,
|
||||||
duration: const Duration(seconds: 5),
|
token,
|
||||||
|
options: options,
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.fine('Connected to LiveKit server, version: ${joinResponse.serverVersion}');
|
||||||
|
|
||||||
|
// create Room first to listen to events
|
||||||
|
room = Room._(
|
||||||
|
engine: engine,
|
||||||
|
joinResponse: joinResponse,
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.fine('Waiting to engine connect...');
|
||||||
|
|
||||||
|
// wait until engine is connected
|
||||||
|
await room._engineListener.waitFor<EngineConnectedEvent>(
|
||||||
|
duration: Timeouts.connection,
|
||||||
onTimeout: () => throw ConnectException(),
|
onTimeout: () => throw ConnectException(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return room;
|
||||||
// catch any exception
|
// catch any exception
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
_connectionState = RoomState.disconnected;
|
// dispose engine if there was any exception while connecting
|
||||||
notifyListeners();
|
if (room != null) {
|
||||||
|
// room.dispose will also dispose engine
|
||||||
// pass on the exception
|
await room.dispose();
|
||||||
|
} else {
|
||||||
|
await engine.dispose();
|
||||||
|
}
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _setUpListeners() => _engineListener
|
||||||
|
..on<EngineConnectedEvent>((event) async {
|
||||||
|
_connectionState = ConnectionState.connected;
|
||||||
|
notifyListeners();
|
||||||
|
})
|
||||||
|
..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<EngineDataPacketReceivedEvent>(_onDataMessageEvent)
|
||||||
|
..on<EngineRemoteMuteChangedEvent>((event) async {
|
||||||
|
final track = localParticipant.trackPublications[event.sid];
|
||||||
|
track?.muted = event.muted;
|
||||||
|
})
|
||||||
|
..on<EngineTrackAddedEvent>((event) async {
|
||||||
|
final idParts = event.stream.id.split('|');
|
||||||
|
final participantSid = idParts[0];
|
||||||
|
final trackSid = idParts.elementAtOrNull(1) ?? event.track.id;
|
||||||
|
final participant = _getOrCreateRemoteParticipant(participantSid, null);
|
||||||
|
try {
|
||||||
|
if (trackSid == null || trackSid.isEmpty) {
|
||||||
|
throw TrackSubscriptionExceptionEvent(
|
||||||
|
participant: participant,
|
||||||
|
reason: TrackSubscribeFailReason.invalidServerResponse,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await participant.addSubscribedMediaTrack(
|
||||||
|
event.track,
|
||||||
|
event.stream,
|
||||||
|
trackSid,
|
||||||
|
);
|
||||||
|
} on TrackSubscriptionExceptionEvent catch (event) {
|
||||||
|
logger.warning('addSubscribedMediaTrack() throwed ${event}');
|
||||||
|
[participant.roomEvents, participant.events].emit(event);
|
||||||
|
} catch (exception) {
|
||||||
|
// We don't want to pass up any exception so catch everything here.
|
||||||
|
logger.warning('Unknown exception on addSubscribedMediaTrack() ${exception}');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/// Disconnects from the room, notifying server of disconnection.
|
/// Disconnects from the room, notifying server of disconnection.
|
||||||
Future<void> disconnect() async {
|
Future<void> disconnect() async {
|
||||||
_engine.client.sendLeave();
|
engine.signalClient.sendLeave();
|
||||||
await _handleDisconnect();
|
await _onDisconnectedEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> reconnect() async {
|
Future<void> reconnect() async {
|
||||||
await _engine.reconnect();
|
await engine.reconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoteParticipant _getOrCreateRemoteParticipant(String sid, lk_models.ParticipantInfo? info) {
|
RemoteParticipant _getOrCreateRemoteParticipant(String sid, lk_models.ParticipantInfo? info) {
|
||||||
var participant = _participants[sid];
|
RemoteParticipant? participant = _participants[sid];
|
||||||
if (participant != null) {
|
if (participant != null) {
|
||||||
return participant;
|
return participant;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info == null) {
|
if (info == null) {
|
||||||
participant = RemoteParticipant(_engine.client, sid, '');
|
participant = RemoteParticipant(
|
||||||
|
engine.signalClient,
|
||||||
|
sid,
|
||||||
|
'',
|
||||||
|
roomEvents: events,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
participant = RemoteParticipant.fromInfo(_engine.client, info);
|
participant = RemoteParticipant.fromInfo(
|
||||||
|
engine.signalClient,
|
||||||
|
info,
|
||||||
|
roomEvents: events,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
participant.roomDelegate = this;
|
|
||||||
_participants[sid] = participant;
|
_participants[sid] = participant;
|
||||||
|
|
||||||
return participant;
|
return participant;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleICEConnected() {
|
Future<void> _onDisconnectedEvent() async {
|
||||||
// _connectCompleter?.complete(this);
|
if (_connectionState == ConnectionState.disconnected) {
|
||||||
// _connectCompleter = null;
|
|
||||||
_connectionState = RoomState.connected;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _handleDisconnect() async {
|
|
||||||
if (_connectionState == RoomState.disconnected) {
|
|
||||||
logger.fine('$objectId: _handleDisconnect() already disconnected');
|
logger.fine('$objectId: _handleDisconnect() already disconnected');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// we need to flag room as disconnected immediately to avoid
|
// we need to flag room as disconnected immediately to avoid
|
||||||
// this method firing multiple times since the following code
|
// this method firing multiple times since the following code
|
||||||
// is being awaited
|
// is being awaited
|
||||||
_connectionState = RoomState.disconnected;
|
_connectionState = ConnectionState.disconnected;
|
||||||
|
|
||||||
for (final p in _participants.values) {
|
// clean up RemoteParticipants
|
||||||
final tracks = List<TrackPublication>.from(p.tracks.values);
|
for (final _ in _participants.values) {
|
||||||
for (final pub in tracks) {
|
// RemoteParticipant is responsible for disposing resources
|
||||||
await p.unpublishTrack(pub.sid);
|
await _.unpublishAllTracks();
|
||||||
|
await _.dispose();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
for (final pub in localParticipant.tracks.values) {
|
|
||||||
await pub.track?.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
await _engine.close();
|
|
||||||
_participants.clear();
|
_participants.clear();
|
||||||
|
|
||||||
|
// clean up LocalParticipant
|
||||||
|
// for (final pub in localParticipant.tracks.values) {
|
||||||
|
// await pub.track?.stop();
|
||||||
|
// }
|
||||||
|
await localParticipant.unpublishAllTracks();
|
||||||
|
|
||||||
|
// await localParticipant.dispose();
|
||||||
|
// localParticipant = null;
|
||||||
|
|
||||||
|
await engine.close();
|
||||||
|
|
||||||
_activeSpeakers.clear();
|
_activeSpeakers.clear();
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
delegate?.onDisconnected();
|
events.emit(const RoomDisconnectedEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleParticipantUpdate(List<lk_models.ParticipantInfo> updates) {
|
void _onParticipantUpdateEvent(List<lk_models.ParticipantInfo> updates) async {
|
||||||
// trigger change notifier only if list of participants membership is changed
|
// trigger change notifier only if list of participants membership is changed
|
||||||
var hasChanged = false;
|
var hasChanged = false;
|
||||||
for (final info in updates) {
|
for (final info in updates) {
|
||||||
@@ -295,9 +301,9 @@ class Room extends ChangeNotifier with ParticipantDelegate {
|
|||||||
|
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
hasChanged = true;
|
hasChanged = true;
|
||||||
delegate?.onParticipantConnected(participant);
|
events.emit(ParticipantConnectedEvent(participant: participant));
|
||||||
} else {
|
} else {
|
||||||
participant.updateFromInfo(info);
|
await participant.updateFromInfo(info);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,7 +312,7 @@ class Room extends ChangeNotifier with ParticipantDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleSpeakerUpdate(List<lk_models.SpeakerInfo> speakers) {
|
void _onSpeakerUpdateEvent(List<lk_models.SpeakerInfo> speakers) {
|
||||||
final seenSids = <String>{};
|
final seenSids = <String>{};
|
||||||
List<Participant> newSpeakers = [];
|
List<Participant> newSpeakers = [];
|
||||||
for (final info in speakers) {
|
for (final info in speakers) {
|
||||||
@@ -339,47 +345,29 @@ class Room extends ChangeNotifier with ParticipantDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
events.emit(ActiveSpeakersChangedEvent(speakers: newSpeakers));
|
||||||
|
|
||||||
_activeSpeakers = newSpeakers;
|
_activeSpeakers = newSpeakers;
|
||||||
delegate?.onActiveSpeakersChanged(newSpeakers);
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleDataPacket(lk_models.UserPacket packet, lk_models.DataPacket_Kind kind) {
|
void _onDataMessageEvent(EngineDataPacketReceivedEvent dataPacketEvent) {
|
||||||
final participant = participants[packet.participantSid];
|
// participant may be null if data is sent from Server-API
|
||||||
if (participant == null) {
|
final senderSid = dataPacketEvent.packet.participantSid;
|
||||||
return;
|
RemoteParticipant? senderParticipant;
|
||||||
|
if (senderSid.isNotEmpty) {
|
||||||
|
senderParticipant = participants[dataPacketEvent.packet.participantSid];
|
||||||
}
|
}
|
||||||
|
|
||||||
participant.delegate?.onDataReceived(participant, packet.payload);
|
// participant.delegate?.onDataReceived(participant, event.packet.payload);
|
||||||
delegate?.onDataReceived(participant, packet.payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onRemoteMuteChanged(String sid, bool mute) {
|
final event = DataReceivedEvent(
|
||||||
final track = localParticipant.tracks[sid];
|
participant: senderParticipant,
|
||||||
//
|
data: dataPacketEvent.packet.payload,
|
||||||
// This will trigger signalClient.sendMuteTrack(sid, mute);
|
);
|
||||||
//
|
|
||||||
track?.muted = mute;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onTrackAdded(
|
senderParticipant?.events.emit(event);
|
||||||
rtc.MediaStreamTrack track,
|
events.emit(event);
|
||||||
rtc.MediaStream? stream,
|
|
||||||
rtc.RTCRtpReceiver? receiver,
|
|
||||||
) {
|
|
||||||
if (stream == null) {
|
|
||||||
// we need the stream to get the track's id
|
|
||||||
logger.severe('received track without mediastream');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final idParts = stream.id.split('|');
|
|
||||||
|
|
||||||
final participantSid = idParts[0];
|
|
||||||
final trackSid = idParts.elementAtOrNull(1) ?? track.id;
|
|
||||||
|
|
||||||
final participant = _getOrCreateRemoteParticipant(participantSid, null);
|
|
||||||
participant.addSubscribedMediaTrack(track, stream, trackSid);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleParticipantDisconnect(String sid) {
|
void _handleParticipantDisconnect(String sid) {
|
||||||
@@ -388,58 +376,11 @@ class Room extends ChangeNotifier with ParticipantDelegate {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final toRemove = List<TrackPublication>.from(participant.tracks.values);
|
final toRemove = List<TrackPublication>.from(participant.trackPublications.values);
|
||||||
for (final track in toRemove) {
|
for (final track in toRemove) {
|
||||||
participant.unpublishTrack(track.sid, true);
|
participant.unpublishTrack(track.sid, notify: true);
|
||||||
}
|
|
||||||
delegate?.onParticipantDisconnected(participant);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//----------------- forward participant delegate calls ---------------------//
|
events.emit(ParticipantDisconnectedEvent(participant: participant));
|
||||||
|
|
||||||
@override
|
|
||||||
void onMetadataChanged(Participant participant) {
|
|
||||||
delegate?.onMetadataChanged(participant);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onTrackMuted(Participant participant, TrackPublication publication) {
|
|
||||||
delegate?.onTrackMuted(participant, publication);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onTrackUnmuted(Participant participant, TrackPublication publication) {
|
|
||||||
delegate?.onTrackUnmuted(participant, publication);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onTrackPublished(RemoteParticipant participant, RemoteTrackPublication publication) {
|
|
||||||
delegate?.onTrackPublished(participant, publication);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onTrackUnpublished(RemoteParticipant participant, RemoteTrackPublication publication) {
|
|
||||||
delegate?.onTrackUnpublished(participant, publication);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onTrackSubscribed(
|
|
||||||
RemoteParticipant participant, Track track, RemoteTrackPublication publication) {
|
|
||||||
delegate?.onTrackSubscribed(participant, track, publication);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onTrackUnsubscribed(
|
|
||||||
RemoteParticipant participant, Track track, RemoteTrackPublication publication) {
|
|
||||||
delegate?.onTrackUnsubscribed(participant, track, publication);
|
|
||||||
}
|
|
||||||
|
|
||||||
// omitted because data dispatching is handled in _handleDataPacket
|
|
||||||
@override
|
|
||||||
void onDataReceived(RemoteParticipant participant, List<int> data) {}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onTrackSubscriptionFailed(RemoteParticipant participant, String sid, String? message) {
|
|
||||||
delegate?.onTrackSubscriptionFailed(participant, sid, message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+113
-148
@@ -4,6 +4,7 @@ import 'package:collection/collection.dart';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
||||||
|
|
||||||
|
import 'constants.dart';
|
||||||
import 'errors.dart';
|
import 'errors.dart';
|
||||||
import 'events.dart';
|
import 'events.dart';
|
||||||
import 'extensions.dart';
|
import 'extensions.dart';
|
||||||
@@ -14,31 +15,15 @@ import 'options.dart';
|
|||||||
import 'proto/livekit_models.pb.dart' as lk_models;
|
import 'proto/livekit_models.pb.dart' as lk_models;
|
||||||
import 'proto/livekit_rtc.pb.dart' as lk_rtc;
|
import 'proto/livekit_rtc.pb.dart' as lk_rtc;
|
||||||
import 'signal_client.dart';
|
import 'signal_client.dart';
|
||||||
import 'track/track.dart';
|
|
||||||
import 'transport.dart';
|
import 'transport.dart';
|
||||||
import 'types.dart';
|
import 'types.dart';
|
||||||
|
|
||||||
typedef GenericCallback = void Function();
|
class RTCEngine {
|
||||||
typedef TrackCallback = void Function(
|
|
||||||
rtc.MediaStreamTrack track,
|
|
||||||
rtc.MediaStream? stream,
|
|
||||||
rtc.RTCRtpReceiver? receiver,
|
|
||||||
);
|
|
||||||
typedef ParticipantUpdateCallback = void Function(List<lk_models.ParticipantInfo> participants);
|
|
||||||
typedef ActiveSpeakerChangedCallback = void Function(List<lk_models.SpeakerInfo> speakers);
|
|
||||||
typedef DataPacketCallback = void Function(
|
|
||||||
lk_models.UserPacket packet, lk_models.DataPacket_Kind kind);
|
|
||||||
typedef RemoteMuteCallback = void Function(String sid, bool mute);
|
|
||||||
|
|
||||||
class RTCEngine with SignalClientDelegate {
|
|
||||||
static const _lossyDCLabel = '_lossy';
|
static const _lossyDCLabel = '_lossy';
|
||||||
static const _reliableDCLabel = '_reliable';
|
static const _reliableDCLabel = '_reliable';
|
||||||
static const _maxReconnectAttempts = 5;
|
static const _maxReconnectAttempts = 5;
|
||||||
static const _maxICEConnectTimeout = Duration(seconds: 5);
|
|
||||||
static const _connectionTimeout = Duration(seconds: 5);
|
|
||||||
static const _iceRestartTimeout = Duration(seconds: 10);
|
|
||||||
|
|
||||||
final SignalClient client;
|
final SignalClient signalClient;
|
||||||
// config for RTCPeerConnection
|
// config for RTCPeerConnection
|
||||||
final RTCConfiguration? rtcConfig;
|
final RTCConfiguration? rtcConfig;
|
||||||
|
|
||||||
@@ -67,42 +52,44 @@ class RTCEngine with SignalClientDelegate {
|
|||||||
// server-provided ice servers
|
// server-provided ice servers
|
||||||
List<lk_rtc.ICEServer> _providedIceServers = [];
|
List<lk_rtc.ICEServer> _providedIceServers = [];
|
||||||
|
|
||||||
// delegate methods
|
|
||||||
GenericCallback? onICEConnected;
|
|
||||||
TrackCallback? onTrack;
|
|
||||||
ParticipantUpdateCallback? onParticipantUpdated;
|
|
||||||
ActiveSpeakerChangedCallback? onActiveSpeakerUpdated;
|
|
||||||
DataPacketCallback? onDataMessage;
|
|
||||||
RemoteMuteCallback? onRemoteMute;
|
|
||||||
GenericCallback? onReconnecting;
|
|
||||||
GenericCallback? onReconnected;
|
|
||||||
GenericCallback? onDisconnected;
|
|
||||||
|
|
||||||
//
|
|
||||||
// internal
|
// internal
|
||||||
//
|
|
||||||
final Map<String, Completer<lk_models.TrackInfo>> _pendingTrackResolvers = {};
|
|
||||||
int _reconnectAttempts = 0;
|
int _reconnectAttempts = 0;
|
||||||
// to complete join request
|
|
||||||
Completer<lk_rtc.JoinResponse>? _joinCompleter;
|
|
||||||
|
|
||||||
final events = EventsEmitter<EngineEvent>();
|
final events = EventsEmitter<EngineEvent>();
|
||||||
|
late final _signalListener = EventsListener(signalClient.events, synchronized: true);
|
||||||
|
|
||||||
final delays = CancelableDelayManager();
|
final delays = CancelableDelayManager();
|
||||||
|
|
||||||
|
// late final Timer _statsTimer;
|
||||||
|
|
||||||
RTCEngine(
|
RTCEngine(
|
||||||
this.client,
|
this.signalClient,
|
||||||
this.rtcConfig,
|
this.rtcConfig,
|
||||||
) {
|
) {
|
||||||
client.delegate = this;
|
|
||||||
|
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
events.listen((event) => logger.fine('[LISTENER] $objectId ${event.runtimeType}'));
|
// log all EngineEvents
|
||||||
events.on<EngineIceStateUpdatedEvent>(
|
events.listen((event) => logger.fine('[EngineEvent] $objectId ${event.runtimeType}'));
|
||||||
(event) => logger.fine('[LISTENER] event is a EngineIceStateUpdatedEvent'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_setUpListeners();
|
||||||
|
// _statsTimer = Timer.periodic(const Duration(seconds: 1), _onStatTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> dispose() async {
|
||||||
|
await events.dispose();
|
||||||
|
await _signalListener.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// void _onStatTimer(Timer _) async {
|
||||||
|
// //
|
||||||
|
// final stats = await publisher?.pc.getStats();
|
||||||
|
// if (stats == null || stats.isEmpty) return;
|
||||||
|
|
||||||
|
// for (final s in stats) {
|
||||||
|
// logger.fine('STATS ${s.values}');
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
Future<lk_rtc.JoinResponse> join(
|
Future<lk_rtc.JoinResponse> join(
|
||||||
String url,
|
String url,
|
||||||
String token, {
|
String token, {
|
||||||
@@ -111,18 +98,16 @@ class RTCEngine with SignalClientDelegate {
|
|||||||
this.url = url;
|
this.url = url;
|
||||||
this.token = token;
|
this.token = token;
|
||||||
|
|
||||||
final completer = Completer<lk_rtc.JoinResponse>();
|
// connect to rtc server
|
||||||
_joinCompleter = completer;
|
await signalClient.connect(url, token, options: options);
|
||||||
|
|
||||||
await client.join(url, token, options: options);
|
// wait for join response
|
||||||
|
final event = await _signalListener.waitFor<SignalConnectedEvent>(
|
||||||
|
duration: Timeouts.connection,
|
||||||
|
onTimeout: () => throw ConnectException(),
|
||||||
|
);
|
||||||
|
|
||||||
// if it's not complete after 5 seconds, fail
|
return event.response;
|
||||||
Timer(_connectionTimeout, () {
|
|
||||||
_joinCompleter?.completeError(ConnectException());
|
|
||||||
_joinCompleter = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
return completer.future;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
@@ -133,12 +118,12 @@ class RTCEngine with SignalClientDelegate {
|
|||||||
}
|
}
|
||||||
isClosed = true;
|
isClosed = true;
|
||||||
|
|
||||||
|
// _statsTimer.cancel();
|
||||||
|
|
||||||
// cancel events
|
// cancel events
|
||||||
await _primaryIceStateListener?.call();
|
await _primaryIceStateListener?.call();
|
||||||
_primaryIceStateListener = null;
|
_primaryIceStateListener = null;
|
||||||
|
|
||||||
await events.dispose();
|
|
||||||
|
|
||||||
// cancel all ongoing delays
|
// cancel all ongoing delays
|
||||||
await delays.dispose();
|
await delays.dispose();
|
||||||
|
|
||||||
@@ -149,7 +134,7 @@ class RTCEngine with SignalClientDelegate {
|
|||||||
await subscriber?.dispose();
|
await subscriber?.dispose();
|
||||||
subscriber = null;
|
subscriber = null;
|
||||||
|
|
||||||
client.close();
|
signalClient.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<lk_models.TrackInfo> addTrack({
|
Future<lk_models.TrackInfo> addTrack({
|
||||||
@@ -158,16 +143,17 @@ class RTCEngine with SignalClientDelegate {
|
|||||||
required lk_models.TrackType kind,
|
required lk_models.TrackType kind,
|
||||||
TrackDimension? dimension,
|
TrackDimension? dimension,
|
||||||
}) async {
|
}) async {
|
||||||
if (_pendingTrackResolvers[cid] != null) {
|
// send request to add track
|
||||||
throw TrackPublishException('a track with the same CID has already been published');
|
signalClient.sendAddTrack(cid: cid, name: name, type: kind, dimension: dimension);
|
||||||
}
|
|
||||||
|
|
||||||
final completer = Completer<lk_models.TrackInfo>();
|
// wait for response, or timeout
|
||||||
_pendingTrackResolvers[cid] = completer;
|
final event = await _signalListener.waitFor<SignalLocalTrackPublishedEvent>(
|
||||||
|
filter: (event) => event.cid == cid,
|
||||||
|
duration: Timeouts.publish,
|
||||||
|
onTimeout: () => throw TrackPublishException(),
|
||||||
|
);
|
||||||
|
|
||||||
client.sendAddTrack(cid: cid, name: name, type: kind, dimension: dimension);
|
return event.track;
|
||||||
|
|
||||||
return completer.future;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> negotiate({bool? iceRestart}) async {
|
Future<void> negotiate({bool? iceRestart}) async {
|
||||||
@@ -214,7 +200,7 @@ class RTCEngine with SignalClientDelegate {
|
|||||||
|
|
||||||
await events.waitFor<EnginePublisherIceStateUpdatedEvent>(
|
await events.waitFor<EnginePublisherIceStateUpdatedEvent>(
|
||||||
filter: (event) => event.iceState.isConnected(),
|
filter: (event) => event.iceState.isConnected(),
|
||||||
duration: _maxICEConnectTimeout,
|
duration: Timeouts.iceConnection,
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.fine('[PUBLISHER] connected');
|
logger.fine('[PUBLISHER] connected');
|
||||||
@@ -234,14 +220,13 @@ class RTCEngine with SignalClientDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (_reconnectAttempts == 0) {
|
if (_reconnectAttempts == 0) {
|
||||||
onReconnecting?.call();
|
events.emit(const EngineReconnectingEvent());
|
||||||
events.emit(EngineReconnectingEvent());
|
|
||||||
}
|
}
|
||||||
_reconnectAttempts++;
|
_reconnectAttempts++;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
isReconnecting = true;
|
isReconnecting = true;
|
||||||
await client.reconnect(url, token);
|
await signalClient.reconnect(url, token);
|
||||||
|
|
||||||
if (publisher == null || subscriber == null) {
|
if (publisher == null || subscriber == null) {
|
||||||
throw UnexpectedStateException('publisher or subscribers is null');
|
throw UnexpectedStateException('publisher or subscribers is null');
|
||||||
@@ -260,12 +245,12 @@ class RTCEngine with SignalClientDelegate {
|
|||||||
|
|
||||||
await events.waitFor<EngineIceStateUpdatedEvent>(
|
await events.waitFor<EngineIceStateUpdatedEvent>(
|
||||||
filter: (event) => event.isPrimary && event.iceState.isConnected(),
|
filter: (event) => event.isPrimary && event.iceState.isConnected(),
|
||||||
duration: _iceRestartTimeout,
|
duration: Timeouts.iceRestart,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.fine('reconnect: success');
|
logger.fine('reconnect: success');
|
||||||
events.emit(EngineReconnectedEvent());
|
events.emit(const EngineReconnectedEvent());
|
||||||
_reconnectAttempts = 0;
|
_reconnectAttempts = 0;
|
||||||
|
|
||||||
// don't catch and pass up any exception
|
// don't catch and pass up any exception
|
||||||
@@ -293,17 +278,17 @@ class RTCEngine with SignalClientDelegate {
|
|||||||
|
|
||||||
publisher?.pc.onIceCandidate = (rtc.RTCIceCandidate candidate) {
|
publisher?.pc.onIceCandidate = (rtc.RTCIceCandidate candidate) {
|
||||||
logger.fine('publisher onIceCandidate');
|
logger.fine('publisher onIceCandidate');
|
||||||
client.sendIceCandidate(candidate, lk_rtc.SignalTarget.PUBLISHER);
|
signalClient.sendIceCandidate(candidate, lk_rtc.SignalTarget.PUBLISHER);
|
||||||
};
|
};
|
||||||
|
|
||||||
subscriber?.pc.onIceCandidate = (rtc.RTCIceCandidate candidate) {
|
subscriber?.pc.onIceCandidate = (rtc.RTCIceCandidate candidate) {
|
||||||
logger.fine('subscriber onIceCandidate');
|
logger.fine('subscriber onIceCandidate');
|
||||||
client.sendIceCandidate(candidate, lk_rtc.SignalTarget.SUBSCRIBER);
|
signalClient.sendIceCandidate(candidate, lk_rtc.SignalTarget.SUBSCRIBER);
|
||||||
};
|
};
|
||||||
|
|
||||||
publisher?.onOffer = (offer) {
|
publisher?.onOffer = (offer) {
|
||||||
logger.fine('publisher onOffer');
|
logger.fine('publisher onOffer');
|
||||||
client.sendOffer(offer);
|
signalClient.sendOffer(offer);
|
||||||
};
|
};
|
||||||
|
|
||||||
// in subscriber primary mode, server side opens sub data channels.
|
// in subscriber primary mode, server side opens sub data channels.
|
||||||
@@ -336,10 +321,9 @@ class RTCEngine with SignalClientDelegate {
|
|||||||
if (!iceConnected) {
|
if (!iceConnected) {
|
||||||
iceConnected = true;
|
iceConnected = true;
|
||||||
if (isReconnecting) {
|
if (isReconnecting) {
|
||||||
onReconnected?.call();
|
events.emit(const EngineReconnectedEvent());
|
||||||
} else {
|
} else {
|
||||||
onICEConnected?.call();
|
events.emit(const EngineConnectedEvent());
|
||||||
events.emit(EngineConnectedEvent());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (event.iceState == rtc.RTCIceConnectionState.RTCIceConnectionStateFailed) {
|
} else if (event.iceState == rtc.RTCIceConnectionState.RTCIceConnectionStateFailed) {
|
||||||
@@ -352,10 +336,16 @@ class RTCEngine with SignalClientDelegate {
|
|||||||
});
|
});
|
||||||
|
|
||||||
subscriber?.pc.onTrack = (rtc.RTCTrackEvent event) {
|
subscriber?.pc.onTrack = (rtc.RTCTrackEvent event) {
|
||||||
onTrack?.call(event.track, event.streams.firstOrNull, event.receiver);
|
final stream = event.streams.firstOrNull;
|
||||||
events.emit(EngineMediaTrackAddedEvent(
|
if (stream == null) {
|
||||||
|
// we need the stream to get the track's id
|
||||||
|
logger.severe('received track without mediastream');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
events.emit(EngineTrackAddedEvent(
|
||||||
track: event.track,
|
track: event.track,
|
||||||
stream: event.streams.firstOrNull,
|
stream: stream,
|
||||||
receiver: event.receiver,
|
receiver: event.receiver,
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
@@ -405,11 +395,9 @@ class RTCEngine with SignalClientDelegate {
|
|||||||
final dp = lk_models.DataPacket.fromBuffer(message.binary);
|
final dp = lk_models.DataPacket.fromBuffer(message.binary);
|
||||||
if (dp.whichValue() == lk_models.DataPacket_Value.speaker) {
|
if (dp.whichValue() == lk_models.DataPacket_Value.speaker) {
|
||||||
// Speaker packet
|
// Speaker packet
|
||||||
onActiveSpeakerUpdated?.call(dp.speaker.speakers);
|
|
||||||
events.emit(EngineSpeakersUpdateEvent(speakers: dp.speaker.speakers));
|
events.emit(EngineSpeakersUpdateEvent(speakers: dp.speaker.speakers));
|
||||||
} else if (dp.whichValue() == lk_models.DataPacket_Value.user) {
|
} else if (dp.whichValue() == lk_models.DataPacket_Value.user) {
|
||||||
// User packet
|
// User packet
|
||||||
onDataMessage?.call(dp.user, dp.kind);
|
|
||||||
events.emit(EngineDataPacketReceivedEvent(
|
events.emit(EngineDataPacketReceivedEvent(
|
||||||
packet: dp.user,
|
packet: dp.user,
|
||||||
kind: dp.kind,
|
kind: dp.kind,
|
||||||
@@ -424,8 +412,7 @@ class RTCEngine with SignalClientDelegate {
|
|||||||
if (_reconnectAttempts >= _maxReconnectAttempts) {
|
if (_reconnectAttempts >= _maxReconnectAttempts) {
|
||||||
logger.info('could not connect after $_reconnectAttempts, giving up');
|
logger.info('could not connect after $_reconnectAttempts, giving up');
|
||||||
await close();
|
await close();
|
||||||
onDisconnected?.call();
|
events.emit(const EngineDisconnectedEvent());
|
||||||
events.emit(EngineDisconnectedEvent());
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,16 +434,16 @@ class RTCEngine with SignalClientDelegate {
|
|||||||
|
|
||||||
//------------------ SignalClient Delegate methods -------------------------//
|
//------------------ SignalClient Delegate methods -------------------------//
|
||||||
|
|
||||||
@override
|
void _setUpListeners() => _signalListener
|
||||||
Future<void> onConnected(lk_rtc.JoinResponse response) async {
|
..on<SignalConnectedEvent>((event) async {
|
||||||
// create peer connections
|
// create peer connections
|
||||||
isClosed = false;
|
isClosed = false;
|
||||||
_subscriberPrimary = response.subscriberPrimary;
|
_subscriberPrimary = event.response.subscriberPrimary;
|
||||||
_providedIceServers = response.iceServers;
|
_providedIceServers = event.response.iceServers;
|
||||||
|
|
||||||
logger.fine('onConnected subscriberPrimary: ${_subscriberPrimary}, '
|
logger.fine('onConnected subscriberPrimary: ${_subscriberPrimary}, '
|
||||||
'serverVersion: ${response.serverVersion}, '
|
'serverVersion: ${event.response.serverVersion}, '
|
||||||
'iceServers: ${response.iceServers}');
|
'iceServers: ${event.response.iceServers}');
|
||||||
|
|
||||||
await _configurePeerConnections();
|
await _configurePeerConnections();
|
||||||
|
|
||||||
@@ -465,86 +452,64 @@ class RTCEngine with SignalClientDelegate {
|
|||||||
await negotiate();
|
await negotiate();
|
||||||
}
|
}
|
||||||
|
|
||||||
_joinCompleter?.complete(Future.value(response));
|
// _joinCompleter?.complete(Future.value(event.response));
|
||||||
_joinCompleter = null;
|
// _joinCompleter = null;
|
||||||
}
|
})
|
||||||
|
..on<SignalCloseEvent>((_) async {
|
||||||
@override
|
|
||||||
Future<void> onClose([String? reason]) async {
|
|
||||||
await _onDisconnected('signal');
|
await _onDisconnected('signal');
|
||||||
}
|
})
|
||||||
|
..on<SignalOfferEvent>((event) async {
|
||||||
@override
|
|
||||||
Future<void> onOffer(rtc.RTCSessionDescription sd) async {
|
|
||||||
if (subscriber == null) {
|
if (subscriber == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.fine('received server offer(type: ${sd.type}, ${subscriber!.pc.signalingState})');
|
logger.fine('received server offer(type: ${event.sd.type}, '
|
||||||
|
'${subscriber!.pc.signalingState})');
|
||||||
|
|
||||||
await subscriber!.setRemoteDescription(sd);
|
await subscriber!.setRemoteDescription(event.sd);
|
||||||
|
|
||||||
final answer = await subscriber!.pc.createAnswer();
|
final answer = await subscriber!.pc.createAnswer();
|
||||||
logger.fine('Created answer');
|
logger.fine('Created answer');
|
||||||
logger.finer('sdp: ${answer.sdp}');
|
logger.finer('sdp: ${answer.sdp}');
|
||||||
await subscriber!.pc.setLocalDescription(answer);
|
await subscriber!.pc.setLocalDescription(answer);
|
||||||
client.sendAnswer(answer);
|
signalClient.sendAnswer(answer);
|
||||||
}
|
})
|
||||||
|
..on<SignalAnswerEvent>((event) async {
|
||||||
@override
|
|
||||||
Future<void> onAnswer(rtc.RTCSessionDescription sd) async {
|
|
||||||
if (publisher == null) {
|
if (publisher == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logger.fine('received answer (type: ${sd.type})');
|
logger.fine('received answer (type: ${event.sd.type})');
|
||||||
logger.finer('sdp: ${sd.sdp}');
|
logger.finer('sdp: ${event.sd.sdp}');
|
||||||
await publisher!.setRemoteDescription(sd);
|
await publisher!.setRemoteDescription(event.sd);
|
||||||
}
|
})
|
||||||
|
..on<SignalTrickleEvent>((event) async {
|
||||||
@override
|
|
||||||
Future<void> onTrickle(rtc.RTCIceCandidate candidate, lk_rtc.SignalTarget target) async {
|
|
||||||
if (publisher == null || subscriber == null) {
|
if (publisher == null || subscriber == null) {
|
||||||
|
logger.warning('Received ${SignalTrickleEvent} but publisher or subscriber was null.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logger.fine('got ICE candidate from peer');
|
logger.fine('got ICE candidate from peer');
|
||||||
if (target == lk_rtc.SignalTarget.SUBSCRIBER) {
|
if (event.target == lk_rtc.SignalTarget.SUBSCRIBER) {
|
||||||
await subscriber!.addIceCandidate(candidate);
|
await subscriber!.addIceCandidate(event.candidate);
|
||||||
} else if (target == lk_rtc.SignalTarget.PUBLISHER) {
|
} else if (event.target == lk_rtc.SignalTarget.PUBLISHER) {
|
||||||
await publisher!.addIceCandidate(candidate);
|
await publisher!.addIceCandidate(event.candidate);
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
..on<SignalParticipantUpdateEvent>((event) async {
|
||||||
@override
|
events.emit(EngineParticipantUpdateEvent(participants: event.updates));
|
||||||
Future<void> onParticipantUpdate(List<lk_models.ParticipantInfo> updates) async {
|
})
|
||||||
onParticipantUpdated?.call(updates);
|
// ..on<SignalLocalTrackPublishedEvent>((event) async {
|
||||||
events.emit(EngineParticipantUpdateEvent(participants: updates));
|
// final completer = _pendingTrackResolvers.remove(event.cid);
|
||||||
}
|
// completer?.complete(event.track);
|
||||||
|
// })
|
||||||
@override
|
..on<SignalActiveSpeakersChangedEvent>((event) async {
|
||||||
Future<void> onLocalTrackPublished(lk_rtc.TrackPublishedResponse response) async {
|
events.emit(EngineSpeakersUpdateEvent(speakers: event.speakers));
|
||||||
final completer = _pendingTrackResolvers.remove(response.cid);
|
})
|
||||||
completer?.complete(Future.value(response.track));
|
..on<SignalLeaveEvent>((event) async {
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onActiveSpeakersChanged(List<lk_models.SpeakerInfo> speakers) async {
|
|
||||||
onActiveSpeakerUpdated?.call(speakers);
|
|
||||||
events.emit(EngineSpeakersUpdateEvent(speakers: speakers));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onLeave(lk_rtc.LeaveRequest req) async {
|
|
||||||
await close();
|
await close();
|
||||||
onDisconnected?.call();
|
events.emit(const EngineDisconnectedEvent());
|
||||||
events.emit(EngineDisconnectedEvent());
|
})
|
||||||
}
|
..on<SignalMuteTrackEvent>((event) => events.emit(EngineRemoteMuteChangedEvent(
|
||||||
|
sid: event.sid,
|
||||||
@override
|
muted: event.muted,
|
||||||
Future<void> onMuteTrack(lk_rtc.MuteTrackRequest req) async {
|
)));
|
||||||
onRemoteMute?.call(req.sid, req.muted);
|
|
||||||
events.emit(EngineRemoteMuteChangedEvent(
|
|
||||||
sid: req.sid,
|
|
||||||
muted: req.muted,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-50
@@ -1,59 +1,40 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:developer';
|
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:synchronized/synchronized.dart' as sync;
|
|
||||||
|
|
||||||
import 'errors.dart';
|
import 'errors.dart';
|
||||||
|
import 'events.dart';
|
||||||
import 'extensions.dart';
|
import 'extensions.dart';
|
||||||
import 'logger.dart';
|
import 'logger.dart';
|
||||||
|
import 'managers/event.dart';
|
||||||
import 'options.dart';
|
import 'options.dart';
|
||||||
import 'proto/livekit_models.pb.dart' as lk_models;
|
import 'proto/livekit_models.pb.dart' as lk_models;
|
||||||
import 'proto/livekit_rtc.pb.dart' as lk_rtc;
|
import 'proto/livekit_rtc.pb.dart' as lk_rtc;
|
||||||
import 'track/track.dart';
|
import 'types.dart';
|
||||||
import 'utils.dart';
|
import 'utils.dart';
|
||||||
import 'ws/interface.dart';
|
import 'ws/interface.dart';
|
||||||
|
|
||||||
mixin SignalClientDelegate {
|
|
||||||
// initial connection established
|
|
||||||
Future<void> onConnected(lk_rtc.JoinResponse response);
|
|
||||||
// websocket has closed
|
|
||||||
Future<void> onClose([String? reason]);
|
|
||||||
// when a server offer is received
|
|
||||||
Future<void> onOffer(rtc.RTCSessionDescription sd);
|
|
||||||
// when an answer from server is received
|
|
||||||
Future<void> onAnswer(rtc.RTCSessionDescription sd);
|
|
||||||
// when server has a new ICE candidate
|
|
||||||
Future<void> onTrickle(rtc.RTCIceCandidate candidate, lk_rtc.SignalTarget target);
|
|
||||||
// participant has changed
|
|
||||||
Future<void> onParticipantUpdate(List<lk_models.ParticipantInfo> updates);
|
|
||||||
// when a track has been added successfully
|
|
||||||
Future<void> onLocalTrackPublished(lk_rtc.TrackPublishedResponse response);
|
|
||||||
// active speaker has changed
|
|
||||||
Future<void> onActiveSpeakersChanged(List<lk_models.SpeakerInfo> speakers);
|
|
||||||
// when server sends this client a leave message
|
|
||||||
Future<void> onLeave(lk_rtc.LeaveRequest req);
|
|
||||||
// explicit mute track
|
|
||||||
Future<void> onMuteTrack(lk_rtc.MuteTrackRequest req);
|
|
||||||
}
|
|
||||||
|
|
||||||
class SignalClient {
|
class SignalClient {
|
||||||
final _lock = sync.Lock();
|
final events = EventsEmitter<SignalEvent>();
|
||||||
|
final ProtocolVersion protocol;
|
||||||
|
|
||||||
ProtocolVersion protocol;
|
|
||||||
SignalClientDelegate? delegate;
|
|
||||||
bool _connected = false;
|
bool _connected = false;
|
||||||
LiveKitWebSocket? _ws;
|
LiveKitWebSocket? _ws;
|
||||||
|
|
||||||
SignalClient({
|
SignalClient({
|
||||||
this.protocol = ProtocolVersion.protocol3,
|
this.protocol = ProtocolVersion.protocol3,
|
||||||
|
}) {
|
||||||
|
events.listen((event) {
|
||||||
|
logger.fine('[SignalEvent] $event');
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
bool get connected => _connected;
|
bool get connected => _connected;
|
||||||
|
|
||||||
Future<void> join(
|
Future<void> connect(
|
||||||
String uriString,
|
String uriString,
|
||||||
String token, {
|
String token, {
|
||||||
ConnectOptions? options,
|
ConnectOptions? options,
|
||||||
@@ -202,7 +183,7 @@ class SignalClient {
|
|||||||
|
|
||||||
void _sendRequest(lk_rtc.SignalRequest req) {
|
void _sendRequest(lk_rtc.SignalRequest req) {
|
||||||
if (_ws == null) {
|
if (_ws == null) {
|
||||||
log('could not send message, not connected');
|
logger.warning('could not send message, not connected');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,48 +195,49 @@ class SignalClient {
|
|||||||
if (message is! List<int>) return;
|
if (message is! List<int>) return;
|
||||||
final msg = lk_rtc.SignalResponse.fromBuffer(message);
|
final msg = lk_rtc.SignalResponse.fromBuffer(message);
|
||||||
|
|
||||||
// Ensure previous delegate method's future is completed
|
|
||||||
// before calling another method
|
|
||||||
await _lock.synchronized(() async {
|
|
||||||
//
|
|
||||||
switch (msg.whichMessage()) {
|
switch (msg.whichMessage()) {
|
||||||
case lk_rtc.SignalResponse_Message.join:
|
case lk_rtc.SignalResponse_Message.join:
|
||||||
if (!_connected) {
|
if (!_connected) {
|
||||||
_connected = true;
|
_connected = true;
|
||||||
await delegate?.onConnected(msg.join);
|
events.emit(SignalConnectedEvent(response: msg.join));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case lk_rtc.SignalResponse_Message.answer:
|
case lk_rtc.SignalResponse_Message.answer:
|
||||||
await delegate?.onAnswer(msg.answer.toSDKType());
|
events.emit(SignalAnswerEvent(sd: msg.answer.toSDKType()));
|
||||||
break;
|
break;
|
||||||
case lk_rtc.SignalResponse_Message.offer:
|
case lk_rtc.SignalResponse_Message.offer:
|
||||||
await delegate?.onOffer(msg.offer.toSDKType());
|
events.emit(SignalOfferEvent(sd: msg.offer.toSDKType()));
|
||||||
break;
|
break;
|
||||||
case lk_rtc.SignalResponse_Message.trickle:
|
case lk_rtc.SignalResponse_Message.trickle:
|
||||||
await delegate?.onTrickle(
|
events.emit(SignalTrickleEvent(
|
||||||
RTCIceCandidateExt.fromJson(msg.trickle.candidateInit),
|
candidate: RTCIceCandidateExt.fromJson(msg.trickle.candidateInit),
|
||||||
msg.trickle.target,
|
target: msg.trickle.target,
|
||||||
);
|
));
|
||||||
break;
|
break;
|
||||||
case lk_rtc.SignalResponse_Message.update:
|
case lk_rtc.SignalResponse_Message.update:
|
||||||
await delegate?.onParticipantUpdate(msg.update.participants);
|
events.emit(SignalParticipantUpdateEvent(updates: msg.update.participants));
|
||||||
break;
|
break;
|
||||||
case lk_rtc.SignalResponse_Message.trackPublished:
|
case lk_rtc.SignalResponse_Message.trackPublished:
|
||||||
await delegate?.onLocalTrackPublished(msg.trackPublished);
|
events.emit(SignalLocalTrackPublishedEvent(
|
||||||
|
cid: msg.trackPublished.cid,
|
||||||
|
track: msg.trackPublished.track,
|
||||||
|
));
|
||||||
break;
|
break;
|
||||||
case lk_rtc.SignalResponse_Message.speaker:
|
case lk_rtc.SignalResponse_Message.speaker:
|
||||||
await delegate?.onActiveSpeakersChanged(msg.speaker.speakers);
|
events.emit(SignalActiveSpeakersChangedEvent(speakers: msg.speaker.speakers));
|
||||||
break;
|
break;
|
||||||
case lk_rtc.SignalResponse_Message.leave:
|
case lk_rtc.SignalResponse_Message.leave:
|
||||||
await delegate?.onLeave(msg.leave);
|
events.emit(SignalLeaveEvent(canReconnect: msg.leave.canReconnect));
|
||||||
break;
|
break;
|
||||||
case lk_rtc.SignalResponse_Message.mute:
|
case lk_rtc.SignalResponse_Message.mute:
|
||||||
await delegate?.onMuteTrack(msg.mute);
|
events.emit(SignalMuteTrackEvent(
|
||||||
|
sid: msg.mute.sid,
|
||||||
|
muted: msg.mute.muted,
|
||||||
|
));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
log('unsupported message: ' + json.encode(msg));
|
logger.warning('unsupported message: ' + json.encode(msg));
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleError(dynamic error) {
|
void _handleError(dynamic error) {
|
||||||
@@ -266,6 +248,6 @@ class SignalClient {
|
|||||||
if (!_connected) return;
|
if (!_connected) return;
|
||||||
_ws = null;
|
_ws = null;
|
||||||
_connected = false;
|
_connected = false;
|
||||||
delegate?.onClose();
|
events.emit(const SignalCloseEvent());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
import '../events.dart';
|
||||||
|
import '../extensions.dart';
|
||||||
import '../logger.dart';
|
import '../logger.dart';
|
||||||
|
import '../managers/event.dart';
|
||||||
import '../participant/local_participant.dart';
|
import '../participant/local_participant.dart';
|
||||||
import '../proto/livekit_models.pb.dart' as lk_models;
|
import '../proto/livekit_models.pb.dart' as lk_models;
|
||||||
import 'track.dart';
|
import 'track.dart';
|
||||||
@@ -23,15 +26,20 @@ class LocalTrackPublication extends TrackPublication {
|
|||||||
|
|
||||||
super.muted = val;
|
super.muted = val;
|
||||||
track?.mediaStreamTrack.enabled = !val;
|
track?.mediaStreamTrack.enabled = !val;
|
||||||
_participant.engine.client.sendMuteTrack(sid, val);
|
_participant.engine.signalClient.sendMuteTrack(sid, val);
|
||||||
|
|
||||||
if (val) {
|
if (val) {
|
||||||
_participant.delegate?.onTrackMuted(_participant, this);
|
// Track muted
|
||||||
_participant.roomDelegate?.onTrackMuted(_participant, this);
|
[_participant.events, _participant.roomEvents].emit(TrackMutedEvent(
|
||||||
|
participant: _participant,
|
||||||
|
track: this,
|
||||||
|
));
|
||||||
} else {
|
} else {
|
||||||
_participant.delegate?.onTrackUnmuted(_participant, this);
|
// Track un-muted
|
||||||
_participant.roomDelegate?.onTrackUnmuted(_participant, this);
|
[_participant.events, _participant.roomEvents].emit(TrackUnmutedEvent(
|
||||||
|
participant: _participant,
|
||||||
|
track: this,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
_participant.muteChanged();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
|
|
||||||
import '../participant/remote_participant.dart';
|
import '../participant/remote_participant.dart';
|
||||||
import '../proto/livekit_models.pb.dart' as lk_models;
|
import '../proto/livekit_models.pb.dart' as lk_models;
|
||||||
import '../proto/livekit_rtc.pb.dart' as lk_rtc;
|
import '../proto/livekit_rtc.pb.dart' as lk_rtc;
|
||||||
import 'track.dart';
|
import 'track.dart';
|
||||||
|
import '../extensions.dart';
|
||||||
|
|
||||||
import 'track_publication.dart';
|
import 'track_publication.dart';
|
||||||
|
|
||||||
/// Represents a track publication from a RemoteParticipant. Provides methods to
|
/// Represents a track publication from a RemoteParticipant. Provides methods to
|
||||||
@@ -50,16 +54,21 @@ class RemoteTrackPublication extends TrackPublication {
|
|||||||
}
|
}
|
||||||
super.muted = val;
|
super.muted = val;
|
||||||
if (val) {
|
if (val) {
|
||||||
_participant.delegate?.onTrackMuted(_participant, this);
|
// Track muted
|
||||||
_participant.roomDelegate?.onTrackMuted(_participant, this);
|
[_participant.events, _participant.roomEvents].emit(TrackMutedEvent(
|
||||||
|
participant: _participant,
|
||||||
|
track: this,
|
||||||
|
));
|
||||||
} else {
|
} else {
|
||||||
_participant.delegate?.onTrackUnmuted(_participant, this);
|
// Track un-muted
|
||||||
_participant.roomDelegate?.onTrackUnmuted(_participant, this);
|
[_participant.events, _participant.roomEvents].emit(TrackUnmutedEvent(
|
||||||
|
participant: _participant,
|
||||||
|
track: this,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
if (subscribed) {
|
if (subscribed) {
|
||||||
track?.mediaStreamTrack.enabled = !val;
|
track?.mediaStreamTrack.enabled = !val;
|
||||||
}
|
}
|
||||||
_participant.muteChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoteTrackPublication(
|
RemoteTrackPublication(
|
||||||
|
|||||||
+12
-11
@@ -1,28 +1,29 @@
|
|||||||
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
||||||
|
import 'package:livekit_client/src/classes/change_notifier.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
import '../proto/livekit_models.pb.dart' as lk_models;
|
import '../proto/livekit_models.pb.dart' as lk_models;
|
||||||
|
|
||||||
class TrackDimension {
|
|
||||||
int width;
|
|
||||||
int height;
|
|
||||||
|
|
||||||
TrackDimension(this.width, this.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wrapper around a MediaStreamTrack with additional metadata.
|
/// Wrapper around a MediaStreamTrack with additional metadata.
|
||||||
class Track {
|
/// Base for [AudioTrack] and [VideoTrack],
|
||||||
|
/// can not be instantiated directly.
|
||||||
|
abstract class Track extends LKChangeNotifier {
|
||||||
static const cameraName = 'camera';
|
static const cameraName = 'camera';
|
||||||
static const screenShareName = 'screen';
|
static const screenShareName = 'screen';
|
||||||
|
|
||||||
String name;
|
final String name;
|
||||||
lk_models.TrackType kind;
|
final lk_models.TrackType kind;
|
||||||
rtc.MediaStreamTrack mediaStreamTrack;
|
rtc.MediaStreamTrack mediaStreamTrack;
|
||||||
|
|
||||||
String? sid;
|
String? sid;
|
||||||
rtc.RTCRtpTransceiver? transceiver;
|
rtc.RTCRtpTransceiver? transceiver;
|
||||||
String? _cid;
|
String? _cid;
|
||||||
|
|
||||||
Track(this.kind, this.name, this.mediaStreamTrack);
|
Track(
|
||||||
|
this.kind,
|
||||||
|
this.name,
|
||||||
|
this.mediaStreamTrack,
|
||||||
|
);
|
||||||
|
|
||||||
bool get muted => mediaStreamTrack.muted == null ? false : mediaStreamTrack.muted!;
|
bool get muted => mediaStreamTrack.muted == null ? false : mediaStreamTrack.muted!;
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
import '../proto/livekit_models.pb.dart' as lk_models;
|
import '../proto/livekit_models.pb.dart' as lk_models;
|
||||||
|
import '../types.dart';
|
||||||
import 'track.dart';
|
import 'track.dart';
|
||||||
|
|
||||||
/// Represents a track that's published to the server. This class contains
|
/// Represents a track that's published to the server. This class contains
|
||||||
/// metadata associated with tracks.
|
/// metadata associated with tracks.
|
||||||
class TrackPublication {
|
///
|
||||||
|
/// Base for [RemoteTrackPublication] and [LocalTrackPublication],
|
||||||
|
/// can not be instantiated directly.
|
||||||
|
|
||||||
|
abstract class TrackPublication {
|
||||||
|
final String name;
|
||||||
|
final String sid;
|
||||||
|
final lk_models.TrackType kind;
|
||||||
|
|
||||||
Track? track;
|
Track? track;
|
||||||
String name;
|
|
||||||
String sid;
|
|
||||||
lk_models.TrackType kind;
|
|
||||||
bool muted = false;
|
bool muted = false;
|
||||||
bool simulcasted = false;
|
bool simulcasted = false;
|
||||||
TrackDimension? dimension;
|
TrackDimension? dimension;
|
||||||
@@ -31,4 +37,12 @@ class TrackPublication {
|
|||||||
dimension = TrackDimension(info.width, info.height);
|
dimension = TrackDimension(info.width, info.height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Equality operators
|
||||||
|
// Object is considered equal when sid is equal
|
||||||
|
@override
|
||||||
|
int get hashCode => sid.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => other is TrackPublication && sid == other.sid;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
||||||
|
|
||||||
import '../proto/livekit_models.pb.dart' as lk_models;
|
import '../proto/livekit_models.pb.dart' as lk_models;
|
||||||
import 'track.dart';
|
import 'track.dart';
|
||||||
|
|
||||||
/// A video track will notify when its mediaTrack has changed.
|
/// A video track will notify when its mediaTrack has changed.
|
||||||
class VideoTrack extends Track with ChangeNotifier {
|
class VideoTrack extends Track {
|
||||||
rtc.MediaStream _mediaStream;
|
rtc.MediaStream _mediaStream;
|
||||||
|
|
||||||
VideoTrack(
|
VideoTrack(
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
||||||
|
|
||||||
|
import 'constants.dart';
|
||||||
|
import 'extensions.dart';
|
||||||
import 'logger.dart';
|
import 'logger.dart';
|
||||||
import 'types.dart';
|
import 'types.dart';
|
||||||
import 'utils.dart';
|
import 'utils.dart';
|
||||||
import 'extensions.dart';
|
|
||||||
|
|
||||||
typedef PCTransportOnOffer = void Function(rtc.RTCSessionDescription offer);
|
typedef PCTransportOnOffer = void Function(rtc.RTCSessionDescription offer);
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ class PCTransport {
|
|||||||
late final negotiate = Utils.createDebounceFunc(
|
late final negotiate = Utils.createDebounceFunc(
|
||||||
() => createAndSendOffer(),
|
() => createAndSendOffer(),
|
||||||
cancelFunc: (f) => _cancelDebounce = f,
|
cancelFunc: (f) => _cancelDebounce = f,
|
||||||
wait: const Duration(milliseconds: 100),
|
wait: Timeouts.debounce,
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
|
|||||||
+34
-4
@@ -1,18 +1,37 @@
|
|||||||
//
|
|
||||||
// LiveKit
|
|
||||||
//
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'extensions.dart';
|
import 'extensions.dart';
|
||||||
|
|
||||||
typedef CancelListenFunc = Function();
|
typedef CancelListenFunc = Function();
|
||||||
|
|
||||||
|
enum ProtocolVersion {
|
||||||
|
protocol2,
|
||||||
|
protocol3,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ConnectionState {
|
||||||
|
disconnected,
|
||||||
|
connected,
|
||||||
|
reconnecting,
|
||||||
|
}
|
||||||
|
|
||||||
enum Reliability {
|
enum Reliability {
|
||||||
reliable,
|
reliable,
|
||||||
lossy,
|
lossy,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum CloseReason {
|
||||||
|
network,
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TrackSubscribeFailReason {
|
||||||
|
invalidServerResponse,
|
||||||
|
notTrackMetadataFound,
|
||||||
|
unsupportedTrackType,
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
enum RTCIceTransportPolicy {
|
enum RTCIceTransportPolicy {
|
||||||
all,
|
all,
|
||||||
relay,
|
relay,
|
||||||
@@ -89,3 +108,14 @@ class RTCIceServer {
|
|||||||
if (credential?.isNotEmpty ?? false) 'credential': credential,
|
if (credential?.isNotEmpty ?? false) 'credential': credential,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
class TrackDimension {
|
||||||
|
final int width;
|
||||||
|
final int height;
|
||||||
|
|
||||||
|
const TrackDimension(
|
||||||
|
this.width,
|
||||||
|
this.height,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
+2
-16
@@ -1,25 +1,11 @@
|
|||||||
//
|
|
||||||
//
|
|
||||||
//
|
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
||||||
|
|
||||||
|
import 'extensions.dart';
|
||||||
import 'options.dart';
|
import 'options.dart';
|
||||||
import 'track/options.dart';
|
import 'track/options.dart';
|
||||||
|
import 'types.dart';
|
||||||
enum ProtocolVersion {
|
|
||||||
protocol2,
|
|
||||||
protocol3,
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ProtocolVersionExt on ProtocolVersion {
|
|
||||||
String toStringValue() => {
|
|
||||||
ProtocolVersion.protocol2: '2',
|
|
||||||
ProtocolVersion.protocol3: '3',
|
|
||||||
}[this]!;
|
|
||||||
}
|
|
||||||
|
|
||||||
extension UriExt on Uri {
|
extension UriExt on Uri {
|
||||||
bool get isSecureScheme => ['https', 'wss'].contains(scheme);
|
bool get isSecureScheme => ['https', 'wss'].contains(scheme);
|
||||||
|
|||||||
@@ -33,7 +33,19 @@ class LiveKitWebSocketIO implements LiveKitWebSocket {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void send(List<int> data) => _ws.add(data);
|
void send(List<int> data) {
|
||||||
|
// 0 CONNECTING
|
||||||
|
// 1 OPEN
|
||||||
|
// 2 CLOSING
|
||||||
|
// 3 CLOSED
|
||||||
|
if (_ws.readyState == 1) {
|
||||||
|
try {
|
||||||
|
_ws.add(data);
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static Future<LiveKitWebSocketIO> connect(
|
static Future<LiveKitWebSocketIO> connect(
|
||||||
Uri uri, [
|
Uri uri, [
|
||||||
|
|||||||
+8
-6
@@ -98,9 +98,11 @@ packages:
|
|||||||
flutter_webrtc:
|
flutter_webrtc:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_webrtc
|
path: "."
|
||||||
url: "https://pub.dartlang.org"
|
ref: use-custom-webrtc-build
|
||||||
source: hosted
|
resolved-ref: "4942e7faec2e5775d35c42e22c2929ca6ca53769"
|
||||||
|
url: "https://github.com/livekit/flutter-webrtc"
|
||||||
|
source: git
|
||||||
version: "0.6.7"
|
version: "0.6.7"
|
||||||
http:
|
http:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
@@ -157,7 +159,7 @@ packages:
|
|||||||
name: path_provider
|
name: path_provider
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.4"
|
version: "2.0.5"
|
||||||
path_provider_linux:
|
path_provider_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -311,5 +313,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.0"
|
version: "0.2.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.13.0 <3.0.0"
|
dart: ">=2.14.0 <3.0.0"
|
||||||
flutter: ">=2.0.0"
|
flutter: ">=2.5.0"
|
||||||
|
|||||||
+11
-1
@@ -10,13 +10,23 @@ environment:
|
|||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_webrtc: ^0.6.7
|
|
||||||
http: ^0.13.3
|
http: ^0.13.3
|
||||||
logging: ^1.0.2
|
logging: ^1.0.2
|
||||||
uuid: ^3.0.4
|
uuid: ^3.0.4
|
||||||
synchronized: ^3.0.0
|
synchronized: ^3.0.0
|
||||||
protobuf: ^2.0.0
|
protobuf: ^2.0.0
|
||||||
|
|
||||||
|
flutter_webrtc:
|
||||||
|
git:
|
||||||
|
url: https://github.com/livekit/flutter-webrtc
|
||||||
|
ref: use-custom-webrtc-build
|
||||||
|
|
||||||
|
# ^0.6.7
|
||||||
|
# path: ../../repos_livekit/flutter-webrtc/
|
||||||
|
# This will use custom webrtc build from
|
||||||
|
# https://github.com/webrtc-sdk/Specs/releases
|
||||||
|
|
||||||
# protobuf:
|
# protobuf:
|
||||||
# git:
|
# git:
|
||||||
# url: https://github.com/google/protobuf.dart.git
|
# url: https://github.com/google/protobuf.dart.git
|
||||||
|
|||||||
Reference in New Issue
Block a user