Session migration (#72)
* impl * retry logic * simulate * pass checks * adjust log level * retry condition * fix simulate scenario params * format * use `RTCPeerConnectionState` instead of `RTCIceConnectionState` * adjust retry * additional check for completer * more logging * fix id * pub * change package * doc update * retry tests * format * apply suggestions
This commit is contained in:
@@ -154,4 +154,25 @@ extension LKExampleExt on BuildContext {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Future<SimulateScenarioResult?> showSimulateScenarioDialog() =>
|
||||||
|
showDialog<SimulateScenarioResult>(
|
||||||
|
context: this,
|
||||||
|
builder: (ctx) => SimpleDialog(
|
||||||
|
title: const Text('Simulate Scenario'),
|
||||||
|
children: SimulateScenarioResult.values
|
||||||
|
.map((e) => SimpleDialogOption(
|
||||||
|
child: Text(e.name),
|
||||||
|
onPressed: () => Navigator.pop(ctx, e),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SimulateScenarioResult {
|
||||||
|
nodeFailure,
|
||||||
|
migration,
|
||||||
|
serverLeave,
|
||||||
|
clear,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:livekit_example/theme.dart';
|
import 'package:livekit_example/theme.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
import 'pages/connect.dart';
|
import 'pages/connect.dart';
|
||||||
|
|
||||||
void main() {
|
void main() async {
|
||||||
final format = DateFormat('HH:mm:ss');
|
final format = DateFormat('HH:mm:ss');
|
||||||
// configure logs for debugging
|
// configure logs for debugging
|
||||||
Logger.root.level = Level.FINE;
|
Logger.root.level = Level.FINE;
|
||||||
|
|||||||
@@ -149,6 +149,18 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onTapSimulateScenario() async {
|
||||||
|
final result = await context.showSimulateScenarioDialog();
|
||||||
|
if (result != null) {
|
||||||
|
print('${result}');
|
||||||
|
await widget.room.simulateScenario(
|
||||||
|
nodeFailure: result == SimulateScenarioResult.nodeFailure ? true : null,
|
||||||
|
migration: result == SimulateScenarioResult.migration ? true : null,
|
||||||
|
serverLeave: result == SimulateScenarioResult.serverLeave ? true : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _onTapSendData() async {
|
void _onTapSendData() async {
|
||||||
final result = await context.showSendDataDialog();
|
final result = await context.showSendDataDialog();
|
||||||
if (result == true) {
|
if (result == true) {
|
||||||
@@ -238,6 +250,11 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
|||||||
icon: const Icon(EvaIcons.settings2),
|
icon: const Icon(EvaIcons.settings2),
|
||||||
tooltip: 'Subscribe permission',
|
tooltip: 'Subscribe permission',
|
||||||
),
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: _onTapSimulateScenario,
|
||||||
|
icon: const Icon(EvaIcons.alertTriangle),
|
||||||
|
tooltip: 'Simulate scenario',
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
+36
-15
@@ -50,6 +50,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.0.1"
|
||||||
|
csslib:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: csslib
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.17.1"
|
||||||
dart_webrtc:
|
dart_webrtc:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -84,7 +91,7 @@ packages:
|
|||||||
name: device_info_plus_platform_interface
|
name: device_info_plus_platform_interface
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.0+1"
|
||||||
device_info_plus_web:
|
device_info_plus_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -184,6 +191,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
|
html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: html
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.15.0"
|
||||||
http:
|
http:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -338,13 +352,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.0"
|
version: "3.1.0"
|
||||||
platform_detect:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: platform_detect
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.3"
|
|
||||||
plugin_platform_interface:
|
plugin_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -373,13 +380,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.2"
|
version: "6.0.2"
|
||||||
pub_semver:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: pub_semver
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.0"
|
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -497,6 +497,20 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.3.0"
|
||||||
|
universal_html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: universal_html
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.8"
|
||||||
|
universal_io:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: universal_io
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.4"
|
||||||
uuid:
|
uuid:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -511,6 +525,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.1.1"
|
||||||
|
web_browser_detect:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web_browser_detect
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.2"
|
||||||
webrtc_interface:
|
webrtc_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -24,4 +24,5 @@ export 'src/track/remote/remote.dart';
|
|||||||
export 'src/track/remote/video.dart';
|
export 'src/track/remote/video.dart';
|
||||||
export 'src/track/track.dart';
|
export 'src/track/track.dart';
|
||||||
export 'src/types.dart';
|
export 'src/types.dart';
|
||||||
|
export 'src/utils.dart';
|
||||||
export 'src/widget/video_track_renderer.dart';
|
export 'src/widget/video_track_renderer.dart';
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ class Timeouts {
|
|||||||
static const connection = Duration(seconds: 10);
|
static const connection = Duration(seconds: 10);
|
||||||
static const debounce = Duration(milliseconds: 100);
|
static const debounce = Duration(milliseconds: 100);
|
||||||
static const publish = Duration(seconds: 10);
|
static const publish = Duration(seconds: 10);
|
||||||
static const iceConnection = Duration(seconds: 10);
|
static const peerConnection = Duration(seconds: 10);
|
||||||
static const iceRestart = Duration(seconds: 10);
|
static const iceRestart = Duration(seconds: 10);
|
||||||
}
|
}
|
||||||
|
|||||||
+142
-114
@@ -19,6 +19,7 @@ import '../proto/livekit_models.pb.dart' as lk_models;
|
|||||||
import '../proto/livekit_rtc.pb.dart' as lk_rtc;
|
import '../proto/livekit_rtc.pb.dart' as lk_rtc;
|
||||||
import '../support/disposable.dart';
|
import '../support/disposable.dart';
|
||||||
import '../types.dart';
|
import '../types.dart';
|
||||||
|
import '../utils.dart';
|
||||||
import 'room.dart';
|
import 'room.dart';
|
||||||
import 'signal_client.dart';
|
import 'signal_client.dart';
|
||||||
import 'transport.dart';
|
import 'transport.dart';
|
||||||
@@ -26,7 +27,6 @@ import 'transport.dart';
|
|||||||
class Engine extends Disposable with EventsEmittable<EngineEvent> {
|
class Engine extends Disposable with EventsEmittable<EngineEvent> {
|
||||||
static const _lossyDCLabel = '_lossy';
|
static const _lossyDCLabel = '_lossy';
|
||||||
static const _reliableDCLabel = '_reliable';
|
static const _reliableDCLabel = '_reliable';
|
||||||
static const _maxReconnectAttempts = 5;
|
|
||||||
|
|
||||||
// Reference to the Room
|
// Reference to the Room
|
||||||
final Room room;
|
final Room room;
|
||||||
@@ -48,8 +48,6 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
|
|||||||
rtc.RTCDataChannel? _reliableDCSub;
|
rtc.RTCDataChannel? _reliableDCSub;
|
||||||
rtc.RTCDataChannel? _lossyDCSub;
|
rtc.RTCDataChannel? _lossyDCSub;
|
||||||
|
|
||||||
bool _iceConnected = false;
|
|
||||||
|
|
||||||
ConnectionState _connectionState = ConnectionState.disconnected;
|
ConnectionState _connectionState = ConnectionState.disconnected;
|
||||||
|
|
||||||
/// Connection state of the [Room].
|
/// Connection state of the [Room].
|
||||||
@@ -68,9 +66,6 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
|
|||||||
// server-provided ice servers
|
// server-provided ice servers
|
||||||
List<lk_rtc.ICEServer> _serverProvidedIceServers = [];
|
List<lk_rtc.ICEServer> _serverProvidedIceServers = [];
|
||||||
|
|
||||||
// internal
|
|
||||||
int _reconnectAttempts = 0;
|
|
||||||
|
|
||||||
late final _signalListener = signalClient.createListener(synchronized: true);
|
late final _signalListener = signalClient.createListener(synchronized: true);
|
||||||
|
|
||||||
final delays = CancelableDelayManager();
|
final delays = CancelableDelayManager();
|
||||||
@@ -94,34 +89,51 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<lk_rtc.JoinResponse> connect(
|
Future<void> connect(
|
||||||
String url,
|
String url,
|
||||||
String token,
|
String token,
|
||||||
) async {
|
) async {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.token = token;
|
this.token = token;
|
||||||
|
|
||||||
// connect to rtc server
|
_updateConnectionState(ConnectionState.connecting);
|
||||||
await signalClient.connect(
|
|
||||||
url,
|
|
||||||
token,
|
|
||||||
connectOptions: room.connectOptions,
|
|
||||||
);
|
|
||||||
|
|
||||||
// wait for join response
|
try {
|
||||||
final event = await _signalListener.waitFor<SignalConnectedEvent>(
|
// wait for socket to connect rtc server
|
||||||
duration: Timeouts.connection,
|
await signalClient.connect(
|
||||||
onTimeout: () => throw ConnectException(),
|
url,
|
||||||
);
|
token,
|
||||||
|
connectOptions: room.connectOptions,
|
||||||
|
);
|
||||||
|
|
||||||
return event.response;
|
// wait for join response
|
||||||
|
await _signalListener.waitFor<SignalJoinResponseEvent>(
|
||||||
|
duration: Timeouts.connection,
|
||||||
|
onTimeout: () => throw ConnectException(),
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.fine('Waiting for engine to connect...');
|
||||||
|
|
||||||
|
// wait until engine is connected
|
||||||
|
await events.waitFor<EnginePeerStateUpdatedEvent>(
|
||||||
|
filter: (event) => event.isPrimary && event.state.isConnected(),
|
||||||
|
duration: Timeouts.connection,
|
||||||
|
onTimeout: () => throw ConnectException(),
|
||||||
|
);
|
||||||
|
|
||||||
|
_updateConnectionState(ConnectionState.connected);
|
||||||
|
} catch (error) {
|
||||||
|
logger.fine('Connect Error $error');
|
||||||
|
_updateConnectionState(ConnectionState.disconnected);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Close connection between the server.
|
/// Close connection between the server.
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
logger.fine('[$objectId] close()');
|
logger.fine('${runtimeType}.close()');
|
||||||
if (_connectionState == ConnectionState.disconnected) {
|
if (_connectionState == ConnectionState.disconnected) {
|
||||||
logger.warning('[$objectId]: close() already disconnected');
|
logger.warning('${runtimeType}.close() already disconnected');
|
||||||
}
|
}
|
||||||
// _statsTimer.cancel();
|
// _statsTimer.cancel();
|
||||||
// cancel all ongoing delays
|
// cancel all ongoing delays
|
||||||
@@ -134,9 +146,9 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
|
|||||||
await subscriber?.dispose();
|
await subscriber?.dispose();
|
||||||
subscriber = null;
|
subscriber = null;
|
||||||
|
|
||||||
await signalClient.close();
|
await signalClient.disconnect();
|
||||||
|
|
||||||
_connectionState = ConnectionState.disconnected;
|
_updateConnectionState(ConnectionState.disconnected);
|
||||||
// notifyListeners();
|
// notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +160,7 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
|
|||||||
required lk_models.TrackSource source,
|
required lk_models.TrackSource source,
|
||||||
VideoDimensions? dimensions,
|
VideoDimensions? dimensions,
|
||||||
bool? dtx,
|
bool? dtx,
|
||||||
List<lk_models.VideoLayer>? videoLayers,
|
Iterable<lk_models.VideoLayer>? videoLayers,
|
||||||
}) async {
|
}) async {
|
||||||
// TODO: Check if cid already published
|
// TODO: Check if cid already published
|
||||||
|
|
||||||
@@ -205,19 +217,19 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
|
|||||||
if (_subscriberPrimary) {
|
if (_subscriberPrimary) {
|
||||||
// make sure publisher transport is connected
|
// make sure publisher transport is connected
|
||||||
|
|
||||||
if (publisher?.pc.iceConnectionState?.isConnected() != true) {
|
if (publisher?.pc.connectionState?.isConnected() != true) {
|
||||||
logger.fine('Publisher is not connected...');
|
logger.fine('Publisher is not connected...');
|
||||||
|
|
||||||
// start negotiation
|
// start negotiation
|
||||||
if (publisher?.pc.iceConnectionState !=
|
if (publisher?.pc.connectionState !=
|
||||||
rtc.RTCIceConnectionState.RTCIceConnectionStateChecking) {
|
rtc.RTCPeerConnectionState.RTCPeerConnectionStateConnecting) {
|
||||||
await negotiate();
|
await negotiate();
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.fine('Waiting for publisher to ice-connect...');
|
logger.fine('Waiting for publisher to ice-connect...');
|
||||||
await events.waitFor<EnginePublisherIceStateUpdatedEvent>(
|
await events.waitFor<EnginePublisherPeerStateUpdatedEvent>(
|
||||||
filter: (event) => event.iceState.isConnected(),
|
filter: (event) => event.state.isConnected(),
|
||||||
duration: Timeouts.iceConnection,
|
duration: Timeouts.peerConnection,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,25 +263,17 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final url = this.url;
|
|
||||||
final token = this.token;
|
|
||||||
|
|
||||||
if (url == null || token == null) {
|
if (url == null || token == null) {
|
||||||
throw ConnectException('could not reconnect without url and token');
|
throw ConnectException('could not reconnect without url and token');
|
||||||
}
|
}
|
||||||
|
|
||||||
_connectionState = ConnectionState.reconnecting;
|
Future<void> sequence() async {
|
||||||
|
//
|
||||||
if (_reconnectAttempts == 0) {
|
await signalClient.connect(
|
||||||
events.emit(const EngineReconnectingEvent());
|
url!,
|
||||||
}
|
token!,
|
||||||
_reconnectAttempts++;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await signalClient.reconnect(
|
|
||||||
url,
|
|
||||||
token,
|
|
||||||
connectOptions: room.connectOptions,
|
connectOptions: room.connectOptions,
|
||||||
|
reconnect: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (publisher == null || subscriber == null) {
|
if (publisher == null || subscriber == null) {
|
||||||
@@ -285,29 +289,37 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
final iceConnected =
|
final iceConnected = primary?.pc.connectionState?.isConnected() ?? false;
|
||||||
primary?.pc.iceConnectionState?.isConnected() ?? false;
|
|
||||||
|
|
||||||
logger.fine('Reconnect: iceConnected: $iceConnected');
|
logger.fine('Reconnect: iceConnected: $iceConnected');
|
||||||
|
|
||||||
if (!iceConnected) {
|
if (!iceConnected) {
|
||||||
logger.fine('Reconnect: Waiting for primary to connect...');
|
logger.fine('Reconnect: Waiting for primary to connect...');
|
||||||
|
|
||||||
await events.waitFor<EngineIceStateUpdatedEvent>(
|
await events.waitFor<EnginePeerStateUpdatedEvent>(
|
||||||
filter: (event) => event.isPrimary && event.iceState.isConnected(),
|
filter: (event) => event.isPrimary && event.state.isConnected(),
|
||||||
duration: Timeouts.iceRestart,
|
duration: Timeouts.iceRestart,
|
||||||
|
onTimeout: () => throw ConnectException(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.fine('Reconnect: success');
|
try {
|
||||||
_connectionState = ConnectionState.connected;
|
_updateConnectionState(ConnectionState.reconnecting);
|
||||||
events.emit(const EngineReconnectedEvent());
|
await Utils.retry<void>(
|
||||||
_reconnectAttempts = 0;
|
(tries, errors) {
|
||||||
|
logger.fine('Retrying connect sequence remaining ${tries} tries...');
|
||||||
|
return sequence();
|
||||||
|
},
|
||||||
|
retryCondition: (_, __) =>
|
||||||
|
_connectionState == ConnectionState.reconnecting,
|
||||||
|
tries: 3,
|
||||||
|
delay: const Duration(seconds: 3),
|
||||||
|
);
|
||||||
|
_updateConnectionState(ConnectionState.connected);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.fine('Reconnect: error ${error}');
|
//
|
||||||
// Pass up all exceptions
|
_updateConnectionState(ConnectionState.disconnected);
|
||||||
_connectionState = ConnectionState.disconnected;
|
|
||||||
rethrow;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,39 +365,26 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
|
|||||||
subscriber?.pc.onDataChannel = _onDataChannel;
|
subscriber?.pc.onDataChannel = _onDataChannel;
|
||||||
}
|
}
|
||||||
|
|
||||||
subscriber?.pc.onIceConnectionState =
|
subscriber?.pc.onConnectionState =
|
||||||
(state) => events.emit(EngineSubscriberIceStateUpdatedEvent(
|
(state) => events.emit(EngineSubscriberPeerStateUpdatedEvent(
|
||||||
iceState: state,
|
state: state,
|
||||||
isPrimary: _subscriberPrimary,
|
isPrimary: _subscriberPrimary,
|
||||||
));
|
));
|
||||||
|
|
||||||
publisher?.pc.onIceConnectionState =
|
publisher?.pc.onConnectionState =
|
||||||
(state) => events.emit(EnginePublisherIceStateUpdatedEvent(
|
(state) => events.emit(EnginePublisherPeerStateUpdatedEvent(
|
||||||
state: state,
|
state: state,
|
||||||
isPrimary: !_subscriberPrimary,
|
isPrimary: !_subscriberPrimary,
|
||||||
));
|
));
|
||||||
|
|
||||||
events.on<EngineIceStateUpdatedEvent>((event) {
|
events.on<EnginePeerStateUpdatedEvent>((event) {
|
||||||
// only listen to primary ice events
|
// only listen to primary ice events
|
||||||
if (!event.isPrimary) return;
|
if (!event.isPrimary) return;
|
||||||
|
|
||||||
if (event.iceState ==
|
if (event.state ==
|
||||||
rtc.RTCIceConnectionState.RTCIceConnectionStateConnected) {
|
rtc.RTCPeerConnectionState.RTCPeerConnectionStateFailed) {
|
||||||
if (!_iceConnected) {
|
|
||||||
_iceConnected = true;
|
|
||||||
if (_connectionState == ConnectionState.reconnecting) {
|
|
||||||
events.emit(const EngineReconnectedEvent());
|
|
||||||
} else {
|
|
||||||
events.emit(const EngineConnectedEvent());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (event.iceState ==
|
|
||||||
rtc.RTCIceConnectionState.RTCIceConnectionStateFailed) {
|
|
||||||
// trigger reconnect sequence
|
// trigger reconnect sequence
|
||||||
if (_iceConnected) {
|
_onDisconnected(DisconnectReason.peerConnection);
|
||||||
_iceConnected = false;
|
|
||||||
_onDisconnected('peerconnection');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -502,8 +501,9 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
|
|||||||
final dp = lk_models.DataPacket.fromBuffer(message.binary);
|
final dp = lk_models.DataPacket.fromBuffer(message.binary);
|
||||||
if (dp.whichValue() == lk_models.DataPacket_Value.speaker) {
|
if (dp.whichValue() == lk_models.DataPacket_Value.speaker) {
|
||||||
// Speaker packet
|
// Speaker packet
|
||||||
events
|
events.emit(EngineActiveSpeakersUpdateEvent(
|
||||||
.emit(EngineActiveSpeakersUpdateEvent(speakers: dp.speaker.speakers));
|
speakers: dp.speaker.speakers,
|
||||||
|
));
|
||||||
} else if (dp.whichValue() == lk_models.DataPacket_Value.user) {
|
} else if (dp.whichValue() == lk_models.DataPacket_Value.user) {
|
||||||
// User packet
|
// User packet
|
||||||
events.emit(EngineDataPacketReceivedEvent(
|
events.emit(EngineDataPacketReceivedEvent(
|
||||||
@@ -513,44 +513,63 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onDisconnected(String reason) async {
|
Future<void> _onDisconnected(DisconnectReason reason) async {
|
||||||
logger.info('onDisconnected reason: $reason');
|
logger
|
||||||
|
.info('onDisconnected state:${_connectionState} reason:${reason.name}');
|
||||||
if (_connectionState == ConnectionState.disconnected) {
|
if (_connectionState == ConnectionState.disconnected) {
|
||||||
logger.fine('[$objectId] Already disconnected $reason');
|
logger.fine('[$objectId] Already disconnected... $reason');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_connectionState == ConnectionState.reconnecting) {
|
||||||
|
logger.fine('[$objectId] Already reconnecting...');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.fine('[$objectId] Disconnected $reason');
|
logger.fine('[$runtimeType] Should attempt reconnect sequence...');
|
||||||
|
|
||||||
if (_reconnectAttempts >= _maxReconnectAttempts) {
|
await reconnect();
|
||||||
logger.info('[$objectId] Could not connect '
|
}
|
||||||
'after ${_reconnectAttempts} attempts, giving up');
|
|
||||||
await close();
|
|
||||||
events.emit(const EngineDisconnectedEvent());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final delay =
|
void _updateConnectionState(ConnectionState newValue) {
|
||||||
Duration(milliseconds: (_reconnectAttempts * _reconnectAttempts) * 300);
|
if (_connectionState == newValue) return;
|
||||||
|
|
||||||
// if this instance is disposed, we probably don't want to continue any more
|
logger.fine('Engine ConnectionState '
|
||||||
// so the whole block will be canceled from being executed
|
'${_connectionState.name} -> ${newValue.name}');
|
||||||
await delays.waitFor(delay, ifNotCancelled: () async {
|
|
||||||
try {
|
bool didReconnect = _connectionState == ConnectionState.reconnecting &&
|
||||||
await reconnect();
|
newValue == ConnectionState.connected;
|
||||||
_reconnectAttempts = 0;
|
// update internal value
|
||||||
} catch (_) {
|
_connectionState = newValue;
|
||||||
// doesn't need to be awaited
|
// emit event
|
||||||
// ignore: unawaited_futures
|
if (_connectionState == ConnectionState.connected) {
|
||||||
_onDisconnected(reason);
|
if (didReconnect) {
|
||||||
|
events.emit(const EngineReconnectedEvent());
|
||||||
|
} else {
|
||||||
|
events.emit(const EngineConnectedEvent());
|
||||||
}
|
}
|
||||||
});
|
} else if (_connectionState == ConnectionState.reconnecting) {
|
||||||
|
events.emit(const EngineReconnectingEvent());
|
||||||
|
} else if (_connectionState == ConnectionState.disconnected) {
|
||||||
|
events.emit(const EngineDisconnectedEvent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@internal
|
||||||
|
void sendSyncState({
|
||||||
|
required lk_rtc.UpdateSubscription subscription,
|
||||||
|
required Iterable<lk_rtc.TrackPublishedResponse>? publishTracks,
|
||||||
|
}) async {
|
||||||
|
final answer = (await subscriber?.pc.getLocalDescription())?.toPBType();
|
||||||
|
signalClient.sendSyncState(
|
||||||
|
answer: answer,
|
||||||
|
subscription: subscription,
|
||||||
|
publishTracks: publishTracks,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setUpListeners() => _signalListener
|
void _setUpListeners() => _signalListener
|
||||||
..on<SignalConnectedEvent>((event) async {
|
..on<SignalJoinResponseEvent>((event) async {
|
||||||
// create peer connections
|
// create peer connections
|
||||||
_connectionState = ConnectionState.connected;
|
|
||||||
_subscriberPrimary = event.response.subscriberPrimary;
|
_subscriberPrimary = event.response.subscriberPrimary;
|
||||||
_serverProvidedIceServers = event.response.iceServers;
|
_serverProvidedIceServers = event.response.iceServers;
|
||||||
|
|
||||||
@@ -564,9 +583,16 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
|
|||||||
// for subscriberPrimary, we negotiate when necessary (lazy)
|
// for subscriberPrimary, we negotiate when necessary (lazy)
|
||||||
await negotiate();
|
await negotiate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Relay to Room
|
||||||
|
events.emit(event);
|
||||||
})
|
})
|
||||||
..on<SignalCloseEvent>((_) async {
|
..on<SignalConnectionStateUpdatedEvent>((event) async {
|
||||||
await _onDisconnected('signal');
|
if (event.connectionState == ConnectionState.disconnected) {
|
||||||
|
await _onDisconnected(DisconnectReason.signal);
|
||||||
|
}
|
||||||
|
// Relay to Room
|
||||||
|
events.emit(event);
|
||||||
})
|
})
|
||||||
..on<SignalOfferEvent>((event) async {
|
..on<SignalOfferEvent>((event) async {
|
||||||
if (subscriber == null) {
|
if (subscriber == null) {
|
||||||
@@ -611,21 +637,23 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
|
|||||||
await publisher!.addIceCandidate(event.candidate);
|
await publisher!.addIceCandidate(event.candidate);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// relay
|
// relay to Room
|
||||||
..on<SignalParticipantUpdateEvent>((event) => events.emit(event))
|
..on<SignalParticipantUpdateEvent>((event) => events.emit(event))
|
||||||
// relay
|
// relay to Room
|
||||||
..on<SignalSpeakersChangedEvent>((event) => events.emit(event))
|
..on<SignalSpeakersChangedEvent>((event) => events.emit(event))
|
||||||
// relay
|
// relay to Room
|
||||||
..on<SignalConnectionQualityUpdateEvent>((event) => events.emit(event))
|
..on<SignalConnectionQualityUpdateEvent>((event) => events.emit(event))
|
||||||
// relay
|
// relay to Room
|
||||||
..on<SignalStreamStateUpdatedEvent>((event) => events.emit(event))
|
..on<SignalStreamStateUpdatedEvent>((event) => events.emit(event))
|
||||||
// relay to Room
|
// relay to Room
|
||||||
..on<SignalSubscribedQualityUpdatedEvent>((event) => events.emit(event))
|
..on<SignalSubscribedQualityUpdatedEvent>((event) => events.emit(event))
|
||||||
// relay to Room
|
// relay to Room
|
||||||
..on<SignalSubscriptionPermissionUpdateEvent>((event) => events.emit(event))
|
..on<SignalSubscriptionPermissionUpdateEvent>((event) => events.emit(event))
|
||||||
..on<SignalLeaveEvent>((event) async {
|
..on<SignalLeaveEvent>((event) async {
|
||||||
if (connectionState == ConnectionState.reconnecting) {
|
if (_connectionState == ConnectionState.reconnecting) {
|
||||||
logger.warning('Received leave signal while engine is reconnecting.');
|
logger.warning(
|
||||||
|
'[Signal] Received Leave while engine is reconnecting, ignoring...');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
await close();
|
await close();
|
||||||
events.emit(const EngineDisconnectedEvent());
|
events.emit(const EngineDisconnectedEvent());
|
||||||
|
|||||||
+58
-32
@@ -2,9 +2,8 @@ import 'dart:collection';
|
|||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
import '../constants.dart';
|
import '../core/signal_client.dart';
|
||||||
import '../events.dart';
|
import '../events.dart';
|
||||||
import '../exceptions.dart';
|
|
||||||
import '../extensions.dart';
|
import '../extensions.dart';
|
||||||
import '../internal/events.dart';
|
import '../internal/events.dart';
|
||||||
import '../logger.dart';
|
import '../logger.dart';
|
||||||
@@ -18,7 +17,6 @@ import '../proto/livekit_rtc.pb.dart' as lk_rtc;
|
|||||||
import '../support/disposable.dart';
|
import '../support/disposable.dart';
|
||||||
import '../track/track.dart';
|
import '../track/track.dart';
|
||||||
import '../types.dart';
|
import '../types.dart';
|
||||||
import '../core/signal_client.dart';
|
|
||||||
import 'engine.dart';
|
import 'engine.dart';
|
||||||
|
|
||||||
/// Room is the primary construct for LiveKit conferences. It contains a
|
/// Room is the primary construct for LiveKit conferences. It contains a
|
||||||
@@ -106,38 +104,10 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
|
|||||||
this.connectOptions = connectOptions ?? this.connectOptions;
|
this.connectOptions = connectOptions ?? this.connectOptions;
|
||||||
this.roomOptions = roomOptions ?? this.roomOptions;
|
this.roomOptions = roomOptions ?? this.roomOptions;
|
||||||
|
|
||||||
final joinResponse = await engine.connect(
|
return engine.connect(
|
||||||
url,
|
url,
|
||||||
token,
|
token,
|
||||||
);
|
);
|
||||||
|
|
||||||
sid = joinResponse.room.sid;
|
|
||||||
name = joinResponse.room.name;
|
|
||||||
_serverVersion = joinResponse.serverVersion;
|
|
||||||
|
|
||||||
logger.fine(
|
|
||||||
'Connected to LiveKit server, version: ${joinResponse.serverVersion}');
|
|
||||||
|
|
||||||
localParticipant = LocalParticipant(
|
|
||||||
room: this,
|
|
||||||
info: joinResponse.participant,
|
|
||||||
);
|
|
||||||
|
|
||||||
for (final info in joinResponse.otherParticipants) {
|
|
||||||
logger.fine('Creating RemoteParticipant: ${info.sid}(${info.identity}) '
|
|
||||||
'tracks:${info.tracks.map((e) => e.sid)}');
|
|
||||||
_getOrCreateRemoteParticipant(info.sid, info);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.fine('Waiting to engine connect...');
|
|
||||||
|
|
||||||
// wait until engine is connected
|
|
||||||
await _engineListener.waitFor<EngineConnectedEvent>(
|
|
||||||
duration: Timeouts.connection,
|
|
||||||
onTimeout: () => throw ConnectException(),
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.fine('Room Connect completed');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setUpListeners() => _engineListener
|
void _setUpListeners() => _engineListener
|
||||||
@@ -154,6 +124,34 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
|
|||||||
events.emit(const RoomReconnectingEvent());
|
events.emit(const RoomReconnectingEvent());
|
||||||
})
|
})
|
||||||
..on<EngineDisconnectedEvent>((event) => _handleClose())
|
..on<EngineDisconnectedEvent>((event) => _handleClose())
|
||||||
|
..on<SignalConnectionStateUpdatedEvent>((event) {
|
||||||
|
// during reconnection, need to send sync state upon signal connection.
|
||||||
|
if (event.didReconnect) {
|
||||||
|
logger.fine('Sending syncState');
|
||||||
|
_sendSyncState();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
..on<SignalJoinResponseEvent>((event) {
|
||||||
|
sid = event.response.room.sid;
|
||||||
|
name = event.response.room.name;
|
||||||
|
_serverVersion = event.response.serverVersion;
|
||||||
|
|
||||||
|
logger.fine('[Engine] Received JoinResponse, '
|
||||||
|
'serverVersion: ${event.response.serverVersion}');
|
||||||
|
|
||||||
|
localParticipant = LocalParticipant(
|
||||||
|
room: this,
|
||||||
|
info: event.response.participant,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (final info in event.response.otherParticipants) {
|
||||||
|
logger.fine('Creating RemoteParticipant: ${info.sid}(${info.identity}) '
|
||||||
|
'tracks:${info.tracks.map((e) => e.sid)}');
|
||||||
|
_getOrCreateRemoteParticipant(info.sid, info);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.fine('Room Connect completed');
|
||||||
|
})
|
||||||
..on<SignalParticipantUpdateEvent>(
|
..on<SignalParticipantUpdateEvent>(
|
||||||
(event) => _onParticipantUpdateEvent(event.participants))
|
(event) => _onParticipantUpdateEvent(event.participants))
|
||||||
..on<EngineActiveSpeakersUpdateEvent>(
|
..on<EngineActiveSpeakersUpdateEvent>(
|
||||||
@@ -459,4 +457,32 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
|
|||||||
|
|
||||||
events.emit(ParticipantDisconnectedEvent(participant: participant));
|
events.emit(ParticipantDisconnectedEvent(participant: participant));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _sendSyncState() async {
|
||||||
|
final connectOptions = this.connectOptions ?? const ConnectOptions();
|
||||||
|
final sendUnSub = connectOptions.autoSubscribe;
|
||||||
|
engine.sendSyncState(
|
||||||
|
subscription: lk_rtc.UpdateSubscription(
|
||||||
|
subscribe: !sendUnSub,
|
||||||
|
participantTracks:
|
||||||
|
participants.values.map((e) => e.participantTracks()),
|
||||||
|
),
|
||||||
|
publishTracks: localParticipant?.publishedTracksInfo(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// To be used for internal testing purposes only.
|
||||||
|
Future<void> simulateScenario({
|
||||||
|
int? speakerUpdate,
|
||||||
|
bool? nodeFailure,
|
||||||
|
bool? migration,
|
||||||
|
bool? serverLeave,
|
||||||
|
}) async {
|
||||||
|
engine.signalClient.sendSimulateScenario(
|
||||||
|
speakerUpdate: speakerUpdate,
|
||||||
|
nodeFailure: nodeFailure,
|
||||||
|
migration: migration,
|
||||||
|
serverLeave: serverLeave,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
import '../events.dart';
|
import '../events.dart';
|
||||||
import '../exceptions.dart';
|
import '../exceptions.dart';
|
||||||
@@ -23,6 +24,7 @@ class SignalClient extends Disposable with EventsEmittable<SignalEvent> {
|
|||||||
|
|
||||||
LiveKitWebSocket? _ws;
|
LiveKitWebSocket? _ws;
|
||||||
|
|
||||||
|
@internal
|
||||||
SignalClient() {
|
SignalClient() {
|
||||||
events.listen((event) {
|
events.listen((event) {
|
||||||
logger.fine('[SignalEvent] $event');
|
logger.fine('[SignalEvent] $event');
|
||||||
@@ -30,24 +32,33 @@ class SignalClient extends Disposable with EventsEmittable<SignalEvent> {
|
|||||||
|
|
||||||
onDispose(() async {
|
onDispose(() async {
|
||||||
await events.dispose();
|
await events.dispose();
|
||||||
await close();
|
await _cleanUp();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@internal
|
||||||
Future<void> connect(
|
Future<void> connect(
|
||||||
String uriString,
|
String uriString,
|
||||||
String token, {
|
String token, {
|
||||||
ConnectOptions? connectOptions,
|
ConnectOptions? connectOptions,
|
||||||
|
bool reconnect = false,
|
||||||
}) async {
|
}) async {
|
||||||
final rtcUri = await Utils.buildUri(
|
final rtcUri = await Utils.buildUri(
|
||||||
uriString,
|
uriString,
|
||||||
token: token,
|
token: token,
|
||||||
connectOptions: connectOptions,
|
connectOptions: connectOptions,
|
||||||
|
reconnect: reconnect,
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.fine('SignalClient connecting with url: $rtcUri');
|
logger.fine('SignalClient connecting with url: $rtcUri');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
_updateConnectionState(reconnect
|
||||||
|
? ConnectionState.reconnecting
|
||||||
|
: ConnectionState.connecting);
|
||||||
|
// Clean up existing socket
|
||||||
|
await _cleanUp();
|
||||||
|
// Attempt to connect
|
||||||
_ws = await LiveKitWebSocket.connect(
|
_ws = await LiveKitWebSocket.connect(
|
||||||
rtcUri,
|
rtcUri,
|
||||||
WebSocketEventHandlers(
|
WebSocketEventHandlers(
|
||||||
@@ -56,18 +67,23 @@ class SignalClient extends Disposable with EventsEmittable<SignalEvent> {
|
|||||||
onError: _handleError,
|
onError: _handleError,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
// Successful connection
|
||||||
|
_updateConnectionState(ConnectionState.connected);
|
||||||
} catch (socketError) {
|
} catch (socketError) {
|
||||||
// Re-build same uri for validate mode
|
|
||||||
final validateUri = await Utils.buildUri(
|
|
||||||
uriString,
|
|
||||||
token: token,
|
|
||||||
connectOptions: connectOptions,
|
|
||||||
validate: true,
|
|
||||||
forceSecure: rtcUri.isSecureScheme,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Attempt Validation
|
// Attempt Validation
|
||||||
try {
|
try {
|
||||||
|
// Skip validation if reconnect mode
|
||||||
|
if (reconnect) rethrow;
|
||||||
|
|
||||||
|
// Re-build same uri for validate mode
|
||||||
|
final validateUri = await Utils.buildUri(
|
||||||
|
uriString,
|
||||||
|
token: token,
|
||||||
|
connectOptions: connectOptions,
|
||||||
|
validate: true,
|
||||||
|
forceSecure: rtcUri.isSecureScheme,
|
||||||
|
);
|
||||||
|
|
||||||
final validateResponse = await http.get(validateUri);
|
final validateResponse = await http.get(validateUri);
|
||||||
if (validateResponse.statusCode != 200) {
|
if (validateResponse.statusCode != 200) {
|
||||||
throw ConnectException(validateResponse.body);
|
throw ConnectException(validateResponse.body);
|
||||||
@@ -78,44 +94,22 @@ class SignalClient extends Disposable with EventsEmittable<SignalEvent> {
|
|||||||
if (error is ConnectException) rethrow;
|
if (error is ConnectException) rethrow;
|
||||||
// HTTP doesn't work either
|
// HTTP doesn't work either
|
||||||
throw ConnectException();
|
throw ConnectException();
|
||||||
|
} finally {
|
||||||
|
_updateConnectionState(ConnectionState.disconnected);
|
||||||
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> reconnect(
|
Future<void> _cleanUp() async {
|
||||||
String uriString,
|
|
||||||
String token, {
|
|
||||||
ConnectOptions? connectOptions,
|
|
||||||
}) async {
|
|
||||||
logger.fine('SignalClient reconnecting...');
|
|
||||||
_connectionState = ConnectionState.reconnecting;
|
|
||||||
await _ws?.dispose();
|
await _ws?.dispose();
|
||||||
_ws = null;
|
_ws = null;
|
||||||
|
|
||||||
final rtcUri = await Utils.buildUri(
|
|
||||||
uriString,
|
|
||||||
token: token,
|
|
||||||
reconnect: true,
|
|
||||||
connectOptions: connectOptions,
|
|
||||||
);
|
|
||||||
|
|
||||||
_ws = await LiveKitWebSocket.connect(
|
|
||||||
rtcUri,
|
|
||||||
WebSocketEventHandlers(
|
|
||||||
onData: _onSocketData,
|
|
||||||
onDispose: _onSocketDispose,
|
|
||||||
onError: _handleError,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.fine('SignalClient socket reconnected');
|
|
||||||
_connectionState = ConnectionState.connected;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> close() async {
|
@internal
|
||||||
logger.fine('SignalClient close');
|
Future<void> disconnect() async {
|
||||||
await _ws?.dispose();
|
logger.fine('SignalClient disconnect');
|
||||||
_ws = null;
|
await _cleanUp();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _sendRequest(lk_rtc.SignalRequest req) {
|
void _sendRequest(lk_rtc.SignalRequest req) {
|
||||||
@@ -129,13 +123,30 @@ class SignalClient extends Disposable with EventsEmittable<SignalEvent> {
|
|||||||
_ws?.send(buf);
|
_ws?.send(buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _updateConnectionState(ConnectionState newValue) {
|
||||||
|
if (_connectionState == newValue) return;
|
||||||
|
|
||||||
|
logger.fine('SignalClient ConnectionState '
|
||||||
|
'${_connectionState.name} -> ${newValue.name}');
|
||||||
|
|
||||||
|
bool didReconnect = _connectionState == ConnectionState.reconnecting &&
|
||||||
|
newValue == ConnectionState.connected;
|
||||||
|
|
||||||
|
_connectionState = newValue;
|
||||||
|
|
||||||
|
events.emit(SignalConnectionStateUpdatedEvent(
|
||||||
|
connectionState: _connectionState,
|
||||||
|
didReconnect: didReconnect,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _onSocketData(dynamic message) async {
|
Future<void> _onSocketData(dynamic message) async {
|
||||||
if (message is! List<int>) return;
|
if (message is! List<int>) return;
|
||||||
final msg = lk_rtc.SignalResponse.fromBuffer(message);
|
final msg = lk_rtc.SignalResponse.fromBuffer(message);
|
||||||
|
|
||||||
switch (msg.whichMessage()) {
|
switch (msg.whichMessage()) {
|
||||||
case lk_rtc.SignalResponse_Message.join:
|
case lk_rtc.SignalResponse_Message.join:
|
||||||
events.emit(SignalConnectedEvent(response: msg.join));
|
events.emit(SignalJoinResponseEvent(response: msg.join));
|
||||||
break;
|
break;
|
||||||
case lk_rtc.SignalResponse_Message.answer:
|
case lk_rtc.SignalResponse_Message.answer:
|
||||||
events.emit(SignalAnswerEvent(sd: msg.answer.toSDKType()));
|
events.emit(SignalAnswerEvent(sd: msg.answer.toSDKType()));
|
||||||
@@ -209,23 +220,25 @@ class SignalClient extends Disposable with EventsEmittable<SignalEvent> {
|
|||||||
// don't emit event's when reconnecting state
|
// don't emit event's when reconnecting state
|
||||||
if (_connectionState != ConnectionState.reconnecting) {
|
if (_connectionState != ConnectionState.reconnecting) {
|
||||||
logger.fine('SignalClient did disconnect ${_connectionState}');
|
logger.fine('SignalClient did disconnect ${_connectionState}');
|
||||||
_connectionState = ConnectionState.disconnected;
|
_updateConnectionState(ConnectionState.disconnected);
|
||||||
events.emit(const SignalCloseEvent());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SignalClientRequests on SignalClient {
|
extension SignalClientRequests on SignalClient {
|
||||||
|
@internal
|
||||||
void sendOffer(rtc.RTCSessionDescription offer) =>
|
void sendOffer(rtc.RTCSessionDescription offer) =>
|
||||||
_sendRequest(lk_rtc.SignalRequest(
|
_sendRequest(lk_rtc.SignalRequest(
|
||||||
offer: offer.toSDKType(),
|
offer: offer.toPBType(),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@internal
|
||||||
void sendAnswer(rtc.RTCSessionDescription answer) =>
|
void sendAnswer(rtc.RTCSessionDescription answer) =>
|
||||||
_sendRequest(lk_rtc.SignalRequest(
|
_sendRequest(lk_rtc.SignalRequest(
|
||||||
answer: answer.toSDKType(),
|
answer: answer.toPBType(),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@internal
|
||||||
void sendIceCandidate(
|
void sendIceCandidate(
|
||||||
rtc.RTCIceCandidate candidate, lk_rtc.SignalTarget target) =>
|
rtc.RTCIceCandidate candidate, lk_rtc.SignalTarget target) =>
|
||||||
_sendRequest(
|
_sendRequest(
|
||||||
@@ -237,6 +250,7 @@ extension SignalClientRequests on SignalClient {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@internal
|
||||||
void sendMuteTrack(String trackSid, bool muted) =>
|
void sendMuteTrack(String trackSid, bool muted) =>
|
||||||
_sendRequest(lk_rtc.SignalRequest(
|
_sendRequest(lk_rtc.SignalRequest(
|
||||||
mute: lk_rtc.MuteTrackRequest(
|
mute: lk_rtc.MuteTrackRequest(
|
||||||
@@ -245,6 +259,7 @@ extension SignalClientRequests on SignalClient {
|
|||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@internal
|
||||||
void sendAddTrack({
|
void sendAddTrack({
|
||||||
required String cid,
|
required String cid,
|
||||||
required String name,
|
required String name,
|
||||||
@@ -252,7 +267,7 @@ extension SignalClientRequests on SignalClient {
|
|||||||
required lk_models.TrackSource source,
|
required lk_models.TrackSource source,
|
||||||
VideoDimensions? dimensions,
|
VideoDimensions? dimensions,
|
||||||
bool? dtx,
|
bool? dtx,
|
||||||
List<lk_models.VideoLayer>? videoLayers,
|
Iterable<lk_models.VideoLayer>? videoLayers,
|
||||||
}) {
|
}) {
|
||||||
final req = lk_rtc.AddTrackRequest(
|
final req = lk_rtc.AddTrackRequest(
|
||||||
cid: cid,
|
cid: cid,
|
||||||
@@ -284,19 +299,22 @@ extension SignalClientRequests on SignalClient {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@internal
|
||||||
void sendUpdateTrackSettings(lk_rtc.UpdateTrackSettings settings) =>
|
void sendUpdateTrackSettings(lk_rtc.UpdateTrackSettings settings) =>
|
||||||
_sendRequest(lk_rtc.SignalRequest(
|
_sendRequest(lk_rtc.SignalRequest(
|
||||||
trackSetting: settings,
|
trackSetting: settings,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@internal
|
||||||
void sendUpdateSubscription(lk_rtc.UpdateSubscription subscription) =>
|
void sendUpdateSubscription(lk_rtc.UpdateSubscription subscription) =>
|
||||||
_sendRequest(lk_rtc.SignalRequest(
|
_sendRequest(lk_rtc.SignalRequest(
|
||||||
subscription: subscription,
|
subscription: subscription,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@internal
|
||||||
void sendUpdateVideoLayers(
|
void sendUpdateVideoLayers(
|
||||||
String trackSid,
|
String trackSid,
|
||||||
List<lk_models.VideoLayer> layers,
|
Iterable<lk_models.VideoLayer> layers,
|
||||||
) =>
|
) =>
|
||||||
_sendRequest(lk_rtc.SignalRequest(
|
_sendRequest(lk_rtc.SignalRequest(
|
||||||
updateLayers: lk_rtc.UpdateVideoLayers(
|
updateLayers: lk_rtc.UpdateVideoLayers(
|
||||||
@@ -305,9 +323,10 @@ extension SignalClientRequests on SignalClient {
|
|||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@internal
|
||||||
void sendUpdateSubscriptionPermissions({
|
void sendUpdateSubscriptionPermissions({
|
||||||
required bool allParticipants,
|
required bool allParticipants,
|
||||||
required List<lk_rtc.TrackPermission> trackPermissions,
|
required Iterable<lk_rtc.TrackPermission> trackPermissions,
|
||||||
}) =>
|
}) =>
|
||||||
_sendRequest(lk_rtc.SignalRequest(
|
_sendRequest(lk_rtc.SignalRequest(
|
||||||
subscriptionPermissions: lk_rtc.UpdateSubscriptionPermissions(
|
subscriptionPermissions: lk_rtc.UpdateSubscriptionPermissions(
|
||||||
@@ -316,7 +335,38 @@ extension SignalClientRequests on SignalClient {
|
|||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@internal
|
||||||
void sendLeave() => _sendRequest(lk_rtc.SignalRequest(
|
void sendLeave() => _sendRequest(lk_rtc.SignalRequest(
|
||||||
leave: lk_rtc.LeaveRequest(),
|
leave: lk_rtc.LeaveRequest(),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@internal
|
||||||
|
void sendSyncState({
|
||||||
|
required lk_rtc.SessionDescription? answer,
|
||||||
|
required lk_rtc.UpdateSubscription subscription,
|
||||||
|
required Iterable<lk_rtc.TrackPublishedResponse>? publishTracks,
|
||||||
|
}) =>
|
||||||
|
_sendRequest(lk_rtc.SignalRequest(
|
||||||
|
syncState: lk_rtc.SyncState(
|
||||||
|
answer: answer,
|
||||||
|
subscription: subscription,
|
||||||
|
publishTracks: publishTracks,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
@internal
|
||||||
|
void sendSimulateScenario({
|
||||||
|
int? speakerUpdate,
|
||||||
|
bool? nodeFailure,
|
||||||
|
bool? migration,
|
||||||
|
bool? serverLeave,
|
||||||
|
}) =>
|
||||||
|
_sendRequest(lk_rtc.SignalRequest(
|
||||||
|
simulate: lk_rtc.SimulateScenario(
|
||||||
|
speakerUpdate: speakerUpdate,
|
||||||
|
nodeFailure: nodeFailure,
|
||||||
|
migration: migration,
|
||||||
|
serverLeave: serverLeave,
|
||||||
|
),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class PCTransport extends Disposable {
|
|||||||
// Ensure callbacks won't fire any more
|
// Ensure callbacks won't fire any more
|
||||||
pc.onRenegotiationNeeded = null;
|
pc.onRenegotiationNeeded = null;
|
||||||
pc.onIceCandidate = null;
|
pc.onIceCandidate = null;
|
||||||
pc.onIceConnectionState = null;
|
pc.onConnectionState = null;
|
||||||
pc.onTrack = null;
|
pc.onTrack = null;
|
||||||
|
|
||||||
// Remove all senders
|
// Remove all senders
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ extension ProtocolVersionExt on ProtocolVersion {
|
|||||||
ProtocolVersion.v3: '3',
|
ProtocolVersion.v3: '3',
|
||||||
ProtocolVersion.v4: '4',
|
ProtocolVersion.v4: '4',
|
||||||
ProtocolVersion.v5: '5',
|
ProtocolVersion.v5: '5',
|
||||||
|
ProtocolVersion.v6: '6',
|
||||||
}[this]!;
|
}[this]!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,11 +66,9 @@ extension RTCIceCandidateExt on rtc.RTCIceCandidate {
|
|||||||
String toJson() => json.encode(toMap());
|
String toJson() => json.encode(toMap());
|
||||||
}
|
}
|
||||||
|
|
||||||
extension RTCIceConnectionStateExt on rtc.RTCIceConnectionState {
|
extension RTCPeerConnectionStateExt on rtc.RTCPeerConnectionState {
|
||||||
bool isConnected() => [
|
bool isConnected() =>
|
||||||
rtc.RTCIceConnectionState.RTCIceConnectionStateConnected,
|
this == rtc.RTCPeerConnectionState.RTCPeerConnectionStateConnected;
|
||||||
rtc.RTCIceConnectionState.RTCIceConnectionStateCompleted,
|
|
||||||
].contains(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension RTCIceTransportPolicyExt on RTCIceTransportPolicy {
|
extension RTCIceTransportPolicyExt on RTCIceTransportPolicy {
|
||||||
@@ -82,7 +81,7 @@ extension RTCIceTransportPolicyExt on RTCIceTransportPolicy {
|
|||||||
// not so neat to directly expose protobuf types so we
|
// not so neat to directly expose protobuf types so we
|
||||||
// define our own types (and convert methods)
|
// define our own types (and convert methods)
|
||||||
extension RTCSessionDescriptionExt on rtc.RTCSessionDescription {
|
extension RTCSessionDescriptionExt on rtc.RTCSessionDescription {
|
||||||
lk_rtc.SessionDescription toSDKType() {
|
lk_rtc.SessionDescription toPBType() {
|
||||||
return lk_rtc.SessionDescription(type: type, sdp: sdp);
|
return lk_rtc.SessionDescription(type: type, sdp: sdp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,42 +10,43 @@ import '../types.dart';
|
|||||||
abstract class InternalEvent implements LiveKitEvent {}
|
abstract class InternalEvent implements LiveKitEvent {}
|
||||||
|
|
||||||
@internal
|
@internal
|
||||||
abstract class EngineIceStateUpdatedEvent with EngineEvent, InternalEvent {
|
abstract class EnginePeerStateUpdatedEvent with EngineEvent, InternalEvent {
|
||||||
final rtc.RTCIceConnectionState iceState;
|
final rtc.RTCPeerConnectionState state;
|
||||||
final bool isPrimary;
|
final bool isPrimary;
|
||||||
const EngineIceStateUpdatedEvent({
|
const EnginePeerStateUpdatedEvent({
|
||||||
required this.iceState,
|
required this.state,
|
||||||
required this.isPrimary,
|
required this.isPrimary,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@internal
|
@internal
|
||||||
class EngineSubscriberIceStateUpdatedEvent extends EngineIceStateUpdatedEvent {
|
class EngineSubscriberPeerStateUpdatedEvent
|
||||||
const EngineSubscriberIceStateUpdatedEvent({
|
extends EnginePeerStateUpdatedEvent {
|
||||||
required rtc.RTCIceConnectionState iceState,
|
const EngineSubscriberPeerStateUpdatedEvent({
|
||||||
|
required rtc.RTCPeerConnectionState state,
|
||||||
required bool isPrimary,
|
required bool isPrimary,
|
||||||
}) : super(
|
}) : super(
|
||||||
iceState: iceState,
|
state: state,
|
||||||
isPrimary: isPrimary,
|
isPrimary: isPrimary,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() =>
|
String toString() =>
|
||||||
'${runtimeType}(state: ${iceState}, isPrimary: ${isPrimary})';
|
'${runtimeType}(state: ${state}, isPrimary: ${isPrimary})';
|
||||||
}
|
}
|
||||||
|
|
||||||
@internal
|
@internal
|
||||||
class EnginePublisherIceStateUpdatedEvent extends EngineIceStateUpdatedEvent {
|
class EnginePublisherPeerStateUpdatedEvent extends EnginePeerStateUpdatedEvent {
|
||||||
const EnginePublisherIceStateUpdatedEvent({
|
const EnginePublisherPeerStateUpdatedEvent({
|
||||||
required rtc.RTCIceConnectionState state,
|
required rtc.RTCPeerConnectionState state,
|
||||||
required bool isPrimary,
|
required bool isPrimary,
|
||||||
}) : super(
|
}) : super(
|
||||||
iceState: state,
|
state: state,
|
||||||
isPrimary: isPrimary,
|
isPrimary: isPrimary,
|
||||||
);
|
);
|
||||||
@override
|
@override
|
||||||
String toString() =>
|
String toString() =>
|
||||||
'${runtimeType}(state: ${iceState}, isPrimary: ${isPrimary})';
|
'${runtimeType}(state: ${state}, isPrimary: ${isPrimary})';
|
||||||
}
|
}
|
||||||
|
|
||||||
@internal
|
@internal
|
||||||
@@ -80,19 +81,29 @@ class InternalTrackMuteUpdatedEvent with TrackEvent, InternalEvent {
|
|||||||
//
|
//
|
||||||
|
|
||||||
@internal
|
@internal
|
||||||
class SignalConnectedEvent with SignalEvent, InternalEvent {
|
// Received a JoinResponse from the server.
|
||||||
|
class SignalJoinResponseEvent with SignalEvent, EngineEvent, InternalEvent {
|
||||||
final lk_rtc.JoinResponse response;
|
final lk_rtc.JoinResponse response;
|
||||||
const SignalConnectedEvent({
|
const SignalJoinResponseEvent({
|
||||||
required this.response,
|
required this.response,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@internal
|
@internal
|
||||||
class SignalCloseEvent with SignalEvent, InternalEvent {
|
class SignalConnectionStateUpdatedEvent
|
||||||
final CloseReason? reason;
|
with SignalEvent, EngineEvent, InternalEvent {
|
||||||
const SignalCloseEvent({
|
final ConnectionState connectionState;
|
||||||
this.reason,
|
final bool didReconnect;
|
||||||
|
final DisconnectReason? disconnectReason;
|
||||||
|
const SignalConnectionStateUpdatedEvent({
|
||||||
|
required this.connectionState,
|
||||||
|
required this.didReconnect,
|
||||||
|
this.disconnectReason,
|
||||||
});
|
});
|
||||||
|
@override
|
||||||
|
String toString() => '$runtimeType(state: ${connectionState.name}, '
|
||||||
|
'didReconnect: ${didReconnect}, '
|
||||||
|
'disconnectReason: ${disconnectReason})';
|
||||||
}
|
}
|
||||||
|
|
||||||
@internal
|
@internal
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ abstract class EventsListenable<T> extends Disposable {
|
|||||||
Future<void> cancelAll() async {
|
Future<void> cancelAll() async {
|
||||||
if (_listeners.isNotEmpty) {
|
if (_listeners.isNotEmpty) {
|
||||||
// Stop listening to all events
|
// Stop listening to all events
|
||||||
logger.fine('${objectId} cancelling ${_listeners.length} listeners(s)');
|
logger.finer('${objectId} cancelling ${_listeners.length} listeners(s)');
|
||||||
for (final listener in _listeners) {
|
for (final listener in _listeners) {
|
||||||
await listener.cancel();
|
await listener.cancel();
|
||||||
}
|
}
|
||||||
@@ -135,7 +135,11 @@ abstract class EventsListenable<T> extends Disposable {
|
|||||||
final completer = Completer<E>();
|
final completer = Completer<E>();
|
||||||
|
|
||||||
final _cancelFunc = on<E>(
|
final _cancelFunc = on<E>(
|
||||||
(event) => completer.complete(event),
|
(event) {
|
||||||
|
if (!completer.isCompleted) {
|
||||||
|
completer.complete(event);
|
||||||
|
}
|
||||||
|
},
|
||||||
filter: filter,
|
filter: filter,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class ConnectOptions {
|
|||||||
const ConnectOptions({
|
const ConnectOptions({
|
||||||
this.autoSubscribe = true,
|
this.autoSubscribe = true,
|
||||||
this.rtcConfiguration = const RTCConfiguration(),
|
this.rtcConfiguration = const RTCConfiguration(),
|
||||||
this.protocolVersion = ProtocolVersion.v5,
|
this.protocolVersion = ProtocolVersion.v6,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import '../extensions.dart';
|
|||||||
import '../logger.dart';
|
import '../logger.dart';
|
||||||
import '../options.dart';
|
import '../options.dart';
|
||||||
import '../proto/livekit_models.pb.dart' as lk_models;
|
import '../proto/livekit_models.pb.dart' as lk_models;
|
||||||
|
import '../proto/livekit_rtc.pb.dart' as lk_rtc;
|
||||||
import '../publication/local.dart';
|
import '../publication/local.dart';
|
||||||
import '../track/local/audio.dart';
|
import '../track/local/audio.dart';
|
||||||
import '../track/local/local.dart';
|
import '../track/local/local.dart';
|
||||||
@@ -331,4 +332,8 @@ class LocalParticipant extends Participant<LocalTrackPublication> {
|
|||||||
allParticipants: allParticipantsAllowed,
|
allParticipants: allParticipantsAllowed,
|
||||||
trackPermissions: trackPermissions.map((e) => e.toPBType()).toList(),
|
trackPermissions: trackPermissions.map((e) => e.toPBType()).toList(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@internal
|
||||||
|
Iterable<lk_rtc.TrackPublishedResponse> publishedTracksInfo() =>
|
||||||
|
trackPublications.values.map((e) => e.toPBTrackPublishedResponse());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,4 +216,11 @@ class RemoteParticipant extends Participant<RemoteTrackPublication> {
|
|||||||
|
|
||||||
await pub.dispose();
|
await pub.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@internal
|
||||||
|
lk_models.ParticipantTracks participantTracks() =>
|
||||||
|
lk_models.ParticipantTracks(
|
||||||
|
participantSid: sid,
|
||||||
|
trackSids: trackPublications.values.map((e) => e.sid),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,4 +80,10 @@ class LocalTrackPublication<T extends LocalTrack> extends TrackPublication<T> {
|
|||||||
logger.fine('Update publishing layers: nothing to change');
|
logger.fine('Update publishing layers: nothing to change');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lk_rtc.TrackPublishedResponse toPBTrackPublishedResponse() =>
|
||||||
|
lk_rtc.TrackPublishedResponse(
|
||||||
|
cid: track?.mediaStreamTrack.id,
|
||||||
|
track: latestInfo,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ abstract class TrackPublication<T extends Track> extends Disposable {
|
|||||||
|
|
||||||
bool get subscribed => track != null;
|
bool get subscribed => track != null;
|
||||||
|
|
||||||
|
@internal
|
||||||
|
lk_models.TrackInfo? latestInfo;
|
||||||
|
|
||||||
TrackPublication({
|
TrackPublication({
|
||||||
required lk_models.TrackInfo info,
|
required lk_models.TrackInfo info,
|
||||||
}) : sid = info.sid,
|
}) : sid = info.sid,
|
||||||
@@ -69,6 +72,7 @@ abstract class TrackPublication<T extends Track> extends Disposable {
|
|||||||
if (info.type == lk_models.TrackType.VIDEO) {
|
if (info.type == lk_models.TrackType.VIDEO) {
|
||||||
_dimensions = VideoDimensions(info.width, info.height);
|
_dimensions = VideoDimensions(info.width, info.height);
|
||||||
}
|
}
|
||||||
|
latestInfo = info;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Equality operators
|
// Equality operators
|
||||||
|
|||||||
@@ -19,17 +19,17 @@ mixin _Disposer {
|
|||||||
|
|
||||||
Future<bool> _dispose() async {
|
Future<bool> _dispose() async {
|
||||||
if (!_isDisposed) {
|
if (!_isDisposed) {
|
||||||
logger.fine('[${objectId}] dispose()');
|
logger.finer('[${objectId}] dispose()');
|
||||||
_isDisposed = true;
|
_isDisposed = true;
|
||||||
if (_disposeFuncs.isNotEmpty) {
|
if (_disposeFuncs.isNotEmpty) {
|
||||||
logger.fine(
|
logger.finer(
|
||||||
'[$objectId] running ${_disposeFuncs.length} dispose funcs...');
|
'[$objectId] running ${_disposeFuncs.length} dispose funcs...');
|
||||||
// call dispose funcs in reverse order
|
// call dispose funcs in reverse order
|
||||||
for (final _func in _disposeFuncs.reversed) {
|
for (final _func in _disposeFuncs.reversed) {
|
||||||
await _func();
|
await _func();
|
||||||
}
|
}
|
||||||
_disposeFuncs.clear();
|
_disposeFuncs.clear();
|
||||||
logger.fine('[$objectId] dispose complete.');
|
logger.finer('[$objectId] dispose complete.');
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+8
-5
@@ -11,16 +11,18 @@ typedef CancelListenFunc = Function();
|
|||||||
/// Usually it's not recommended to change this.
|
/// Usually it's not recommended to change this.
|
||||||
enum ProtocolVersion {
|
enum ProtocolVersion {
|
||||||
v2,
|
v2,
|
||||||
v3,
|
v3, // Subscriber as primary
|
||||||
v4,
|
v4,
|
||||||
v5,
|
v5,
|
||||||
|
v6, // Session migration
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Connection state type used throughout the SDK.
|
/// Connection state type used throughout the SDK.
|
||||||
enum ConnectionState {
|
enum ConnectionState {
|
||||||
disconnected,
|
disconnected,
|
||||||
connected,
|
connecting,
|
||||||
reconnecting,
|
reconnecting,
|
||||||
|
connected,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Connection quality between the [Participant] and server.
|
/// Connection quality between the [Participant] and server.
|
||||||
@@ -58,9 +60,10 @@ enum StreamState {
|
|||||||
active,
|
active,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CloseReason {
|
enum DisconnectReason {
|
||||||
network,
|
user,
|
||||||
// ...
|
peerConnection,
|
||||||
|
signal,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The reason why a track failed to publish.
|
/// The reason why a track failed to publish.
|
||||||
|
|||||||
+44
-4
@@ -2,9 +2,10 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
import 'package:platform_detect/platform_detect.dart' as pd;
|
import 'package:web_browser_detect/web_browser_detect.dart' as bd;
|
||||||
|
|
||||||
import './proto/livekit_models.pb.dart' as lk_models;
|
import './proto/livekit_models.pb.dart' as lk_models;
|
||||||
import './support/native.dart';
|
import './support/native.dart';
|
||||||
@@ -21,18 +22,57 @@ extension UriExt on Uri {
|
|||||||
bool get isSecureScheme => ['https', 'wss'].contains(scheme);
|
bool get isSecureScheme => ['https', 'wss'].contains(scheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef RetryFuture<T> = Future<T> Function(
|
||||||
|
int triesLeft,
|
||||||
|
List<Object> errors,
|
||||||
|
);
|
||||||
|
typedef RetryCondition = bool Function(
|
||||||
|
int triesLeft,
|
||||||
|
List<Object> errors,
|
||||||
|
);
|
||||||
|
|
||||||
// Collection of state-less static methods
|
// Collection of state-less static methods
|
||||||
class Utils {
|
class Utils {
|
||||||
|
/// Returns a [Future] that will retry [future] while it throws
|
||||||
|
/// for a maximum of [tries] times with [delay] in between.
|
||||||
|
/// If all the attempts throws, the future will throw a [List] of the
|
||||||
|
/// thrown objects by the [future].
|
||||||
|
static Future<T> retry<T>(
|
||||||
|
RetryFuture<T> future, {
|
||||||
|
|
||||||
|
/// number of total tries (first try + retries)
|
||||||
|
int tries = 1,
|
||||||
|
Duration delay = const Duration(seconds: 1),
|
||||||
|
RetryCondition? retryCondition,
|
||||||
|
}) async {
|
||||||
|
List<Object> errors = [];
|
||||||
|
while (tries-- > 0) {
|
||||||
|
try {
|
||||||
|
return await future(tries, errors);
|
||||||
|
} catch (error) {
|
||||||
|
logger.fine('[Retry] Caught error ${error}...');
|
||||||
|
errors.add(error);
|
||||||
|
if (!(retryCondition?.call(tries, errors) ?? true)) break;
|
||||||
|
}
|
||||||
|
if (tries > 0) {
|
||||||
|
logger.fine('[Retry] Waiting ${delay}...');
|
||||||
|
await Future<dynamic>.delayed(delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw errors;
|
||||||
|
}
|
||||||
|
|
||||||
// DeviceInfoPlugin caches internally
|
// DeviceInfoPlugin caches internally
|
||||||
static final _deviceInfoPlugin = DeviceInfoPlugin();
|
static final _deviceInfoPlugin = DeviceInfoPlugin();
|
||||||
|
|
||||||
static Future<lk_models.ClientInfo?> _clientInfo() async {
|
static Future<lk_models.ClientInfo?> _clientInfo() async {
|
||||||
switch (lkPlatform()) {
|
switch (lkPlatform()) {
|
||||||
case PlatformType.web:
|
case PlatformType.web:
|
||||||
|
final browser = bd.Browser();
|
||||||
return lk_models.ClientInfo(
|
return lk_models.ClientInfo(
|
||||||
os: pd.operatingSystem.name.toLowerCase(),
|
os: defaultTargetPlatform.name,
|
||||||
browser: pd.browser.name.toLowerCase(),
|
browser: browser.browserAgent.name,
|
||||||
browserVersion: pd.browser.version.canonicalizedVersion,
|
browserVersion: browser.version,
|
||||||
);
|
);
|
||||||
case PlatformType.windows:
|
case PlatformType.windows:
|
||||||
return lk_models.ClientInfo(
|
return lk_models.ClientInfo(
|
||||||
|
|||||||
+36
-15
@@ -57,6 +57,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.0.1"
|
||||||
|
csslib:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: csslib
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.17.1"
|
||||||
dart_webrtc:
|
dart_webrtc:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -163,6 +170,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.1"
|
version: "0.8.1"
|
||||||
|
html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: html
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.15.0"
|
||||||
http:
|
http:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -282,13 +296,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.0"
|
version: "3.1.0"
|
||||||
platform_detect:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: platform_detect
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.3"
|
|
||||||
plugin_platform_interface:
|
plugin_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -310,13 +317,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "2.0.1"
|
||||||
pub_semver:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: pub_semver
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.0"
|
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -385,6 +385,20 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.3.0"
|
||||||
|
universal_html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: universal_html
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.8"
|
||||||
|
universal_io:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: universal_io
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.4"
|
||||||
uuid:
|
uuid:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -399,6 +413,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.1.1"
|
||||||
|
web_browser_detect:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: web_browser_detect
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.2"
|
||||||
webrtc_interface:
|
webrtc_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -412,7 +433,7 @@ packages:
|
|||||||
name: win32
|
name: win32
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.4"
|
version: "2.3.6"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
+1
-1
@@ -25,7 +25,7 @@ dependencies:
|
|||||||
flutter_webrtc: ^0.8.1
|
flutter_webrtc: ^0.8.1
|
||||||
dart_webrtc: ^1.0.3
|
dart_webrtc: ^1.0.3
|
||||||
device_info_plus: ^3.2.1
|
device_info_plus: ^3.2.1
|
||||||
platform_detect: ^2.0.3
|
web_browser_detect: ^2.0.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
test('placeholder', () {});
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:livekit_client/src/utils.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('retry', () {
|
||||||
|
// test if List of errors are thrown
|
||||||
|
test(
|
||||||
|
'throw all and throw error list',
|
||||||
|
() => expect(
|
||||||
|
Utils.retry<void>(
|
||||||
|
(triesLeft, _) => throw 'error-${triesLeft}',
|
||||||
|
tries: 3,
|
||||||
|
delay: Duration.zero,
|
||||||
|
),
|
||||||
|
throwsA([
|
||||||
|
'error-2',
|
||||||
|
'error-1',
|
||||||
|
'error-0',
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
test(
|
||||||
|
'throw once and return result',
|
||||||
|
() => expectLater(
|
||||||
|
Utils.retry<String>(
|
||||||
|
(triesLeft, _) async {
|
||||||
|
expect(
|
||||||
|
triesLeft,
|
||||||
|
isNot(0),
|
||||||
|
reason: 'should be never 0 because returning on 1',
|
||||||
|
);
|
||||||
|
if (triesLeft == 1) return 'result-${triesLeft}';
|
||||||
|
throw 'error${triesLeft}';
|
||||||
|
},
|
||||||
|
tries: 3,
|
||||||
|
delay: Duration.zero,
|
||||||
|
),
|
||||||
|
completion('result-1'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user