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:
Hiroshi Horie
2021-09-24 23:37:26 +09:00
committed by GitHub
parent f4bd82a919
commit 6445b8d2a8
36 changed files with 1270 additions and 948 deletions
+1 -1
View File
@@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>8.0</string>
<string>9.0</string>
</dict>
</plist>
+4
View File
@@ -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
View File
@@ -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
+3 -2
View File
@@ -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
View File
@@ -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),
+2
View File
@@ -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) {
+7 -3
View File
@@ -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
View File
@@ -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),
),
],
),
);
}
+2 -2
View File
@@ -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 {
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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';
+32
View File
@@ -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);
}
}
+7
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
}
+6 -8
View File
@@ -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,
);
}
-3
View File
@@ -1,6 +1,3 @@
//
//
//
import 'package:async/async.dart';
class CancelableDelayManager {
+45 -18
View File
@@ -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(),
);
+24 -14
View File
@@ -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.
+67 -69
View File
@@ -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();
}
+94 -74
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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());
}
}
+14 -6
View File
@@ -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();
}
}
+14 -5
View File
@@ -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
View File
@@ -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!;
+18 -4
View File
@@ -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 -3
View File
@@ -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(
+3 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+13 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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