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