tests and mocks for e2e testing (#78)
* tests and mocks for e2e testing * formatting * fix analyze errors
This commit is contained in:
@@ -158,6 +158,7 @@ class _LocalParticipantWidgetState
|
|||||||
_visible) {
|
_visible) {
|
||||||
return firstVideoPublication?.track;
|
return firstVideoPublication?.track;
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,6 +181,7 @@ class _RemoteParticipantWidgetState
|
|||||||
return trackPublication.track;
|
return trackPublication.track;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -240,6 +240,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.11"
|
version: "0.12.11"
|
||||||
|
material_color_utilities:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: material_color_utilities
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.3"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -475,7 +482,7 @@ packages:
|
|||||||
name: test_api
|
name: test_api
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.3"
|
version: "0.4.8"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
+17
-10
@@ -3,6 +3,7 @@ import 'dart:async';
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
||||||
|
import 'package:livekit_client/src/support/websocket.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
import '../constants.dart';
|
import '../constants.dart';
|
||||||
@@ -28,11 +29,10 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
|
|||||||
static const _lossyDCLabel = '_lossy';
|
static const _lossyDCLabel = '_lossy';
|
||||||
static const _reliableDCLabel = '_reliable';
|
static const _reliableDCLabel = '_reliable';
|
||||||
|
|
||||||
// Reference to the Room
|
|
||||||
final Room room;
|
|
||||||
|
|
||||||
final SignalClient signalClient;
|
final SignalClient signalClient;
|
||||||
|
|
||||||
|
final PeerConnectionCreate _peerConnectionCreate;
|
||||||
|
|
||||||
@internal
|
@internal
|
||||||
PCTransport? publisher;
|
PCTransport? publisher;
|
||||||
|
|
||||||
@@ -60,6 +60,7 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
|
|||||||
// remember url and token for reconnect
|
// remember url and token for reconnect
|
||||||
String? url;
|
String? url;
|
||||||
String? token;
|
String? token;
|
||||||
|
ConnectOptions? connectOptions;
|
||||||
|
|
||||||
bool _subscriberPrimary = false;
|
bool _subscriberPrimary = false;
|
||||||
|
|
||||||
@@ -71,9 +72,11 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
|
|||||||
final delays = CancelableDelayManager();
|
final delays = CancelableDelayManager();
|
||||||
|
|
||||||
Engine({
|
Engine({
|
||||||
required this.room,
|
|
||||||
SignalClient? signalClient,
|
SignalClient? signalClient,
|
||||||
}) : signalClient = signalClient ?? SignalClient() {
|
PeerConnectionCreate? peerConnectionCreate,
|
||||||
|
}) : signalClient = signalClient ?? SignalClient(LiveKitWebSocket.connect),
|
||||||
|
_peerConnectionCreate =
|
||||||
|
peerConnectionCreate ?? rtc.createPeerConnection {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
// log all EngineEvents
|
// log all EngineEvents
|
||||||
events.listen((event) => logger.fine('[EngineEvent] $objectId ${event}'));
|
events.listen((event) => logger.fine('[EngineEvent] $objectId ${event}'));
|
||||||
@@ -92,9 +95,11 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
|
|||||||
Future<void> connect(
|
Future<void> connect(
|
||||||
String url,
|
String url,
|
||||||
String token,
|
String token,
|
||||||
|
ConnectOptions? connectOptions,
|
||||||
) async {
|
) async {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.token = token;
|
this.token = token;
|
||||||
|
this.connectOptions = connectOptions ?? const ConnectOptions();
|
||||||
|
|
||||||
_updateConnectionState(ConnectionState.connecting);
|
_updateConnectionState(ConnectionState.connecting);
|
||||||
|
|
||||||
@@ -103,7 +108,7 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
|
|||||||
await signalClient.connect(
|
await signalClient.connect(
|
||||||
url,
|
url,
|
||||||
token,
|
token,
|
||||||
connectOptions: room.connectOptions,
|
connectOptions: this.connectOptions,
|
||||||
);
|
);
|
||||||
|
|
||||||
// wait for join response
|
// wait for join response
|
||||||
@@ -272,7 +277,7 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
|
|||||||
await signalClient.connect(
|
await signalClient.connect(
|
||||||
url!,
|
url!,
|
||||||
token!,
|
token!,
|
||||||
connectOptions: room.connectOptions,
|
connectOptions: connectOptions,
|
||||||
reconnect: true,
|
reconnect: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -331,7 +336,7 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
|
|||||||
|
|
||||||
// RTCConfiguration? config;
|
// RTCConfiguration? config;
|
||||||
// use server-provided iceServers if not provided by user
|
// use server-provided iceServers if not provided by user
|
||||||
final connectOptions = room.connectOptions ?? const ConnectOptions();
|
final connectOptions = this.connectOptions ?? const ConnectOptions();
|
||||||
final serverIceServers =
|
final serverIceServers =
|
||||||
_serverProvidedIceServers.map((e) => e.toSDKType()).toList();
|
_serverProvidedIceServers.map((e) => e.toSDKType()).toList();
|
||||||
|
|
||||||
@@ -342,8 +347,10 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
|
|||||||
.copyWith(iceServers: serverIceServers);
|
.copyWith(iceServers: serverIceServers);
|
||||||
}
|
}
|
||||||
|
|
||||||
publisher = await PCTransport.create(rtcConfiguration);
|
publisher =
|
||||||
subscriber = await PCTransport.create(rtcConfiguration);
|
await PCTransport.create(_peerConnectionCreate, rtcConfiguration);
|
||||||
|
subscriber =
|
||||||
|
await PCTransport.create(_peerConnectionCreate, rtcConfiguration);
|
||||||
|
|
||||||
publisher?.pc.onIceCandidate = (rtc.RTCIceCandidate candidate) {
|
publisher?.pc.onIceCandidate = (rtc.RTCIceCandidate candidate) {
|
||||||
logger.fine('publisher onIceCandidate');
|
logger.fine('publisher onIceCandidate');
|
||||||
|
|||||||
+7
-12
@@ -62,16 +62,14 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
|
|||||||
UnmodifiableListView<Participant> get activeSpeakers =>
|
UnmodifiableListView<Participant> get activeSpeakers =>
|
||||||
UnmodifiableListView<Participant>(_activeSpeakers);
|
UnmodifiableListView<Participant>(_activeSpeakers);
|
||||||
|
|
||||||
late final engine = Engine(room: this);
|
final Engine engine;
|
||||||
|
|
||||||
// suppport for multiple event listeners
|
// suppport for multiple event listeners
|
||||||
late final _engineListener = engine.createListener();
|
late final EventsListener<EngineEvent> _engineListener;
|
||||||
|
|
||||||
Room({
|
Room({this.connectOptions, this.roomOptions, Engine? engine})
|
||||||
this.connectOptions,
|
: engine = engine ?? Engine() {
|
||||||
this.roomOptions,
|
_engineListener = this.engine.createListener();
|
||||||
}) {
|
|
||||||
//
|
|
||||||
_setUpListeners();
|
_setUpListeners();
|
||||||
|
|
||||||
// Any event emitted will trigger ChangeNotifier
|
// Any event emitted will trigger ChangeNotifier
|
||||||
@@ -88,7 +86,7 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
|
|||||||
// dispose all listeners for RTCEngine
|
// dispose all listeners for RTCEngine
|
||||||
await _engineListener.dispose();
|
await _engineListener.dispose();
|
||||||
// dispose the engine
|
// dispose the engine
|
||||||
await engine.dispose();
|
await this.engine.dispose();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,10 +100,7 @@ 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;
|
||||||
|
|
||||||
return engine.connect(
|
return engine.connect(url, token, this.connectOptions);
|
||||||
url,
|
|
||||||
token,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setUpListeners() => _engineListener
|
void _setUpListeners() => _engineListener
|
||||||
|
|||||||
@@ -22,10 +22,11 @@ class SignalClient extends Disposable with EventsEmittable<SignalEvent> {
|
|||||||
// Connection state of the socket conection.
|
// Connection state of the socket conection.
|
||||||
ConnectionState _connectionState = ConnectionState.disconnected;
|
ConnectionState _connectionState = ConnectionState.disconnected;
|
||||||
|
|
||||||
|
final WebSocketConnector _wsConnector;
|
||||||
LiveKitWebSocket? _ws;
|
LiveKitWebSocket? _ws;
|
||||||
|
|
||||||
@internal
|
@internal
|
||||||
SignalClient() {
|
SignalClient(WebSocketConnector wsConnector) : _wsConnector = wsConnector {
|
||||||
events.listen((event) {
|
events.listen((event) {
|
||||||
logger.fine('[SignalEvent] $event');
|
logger.fine('[SignalEvent] $event');
|
||||||
});
|
});
|
||||||
@@ -59,7 +60,7 @@ class SignalClient extends Disposable with EventsEmittable<SignalEvent> {
|
|||||||
// Clean up existing socket
|
// Clean up existing socket
|
||||||
await _cleanUp();
|
await _cleanUp();
|
||||||
// Attempt to connect
|
// Attempt to connect
|
||||||
_ws = await LiveKitWebSocket.connect(
|
_ws = await _wsConnector(
|
||||||
rtcUri,
|
rtcUri,
|
||||||
WebSocketEventHandlers(
|
WebSocketEventHandlers(
|
||||||
onData: _onSocketData,
|
onData: _onSocketData,
|
||||||
@@ -174,6 +175,9 @@ class SignalClient extends Disposable with EventsEmittable<SignalEvent> {
|
|||||||
events.emit(
|
events.emit(
|
||||||
SignalSpeakersChangedEvent(speakers: msg.speakersChanged.speakers));
|
SignalSpeakersChangedEvent(speakers: msg.speakersChanged.speakers));
|
||||||
break;
|
break;
|
||||||
|
case lk_rtc.SignalResponse_Message.roomUpdate:
|
||||||
|
// TODO
|
||||||
|
break;
|
||||||
case lk_rtc.SignalResponse_Message.connectionQuality:
|
case lk_rtc.SignalResponse_Message.connectionQuality:
|
||||||
events.emit(SignalConnectionQualityUpdateEvent(
|
events.emit(SignalConnectionQualityUpdateEvent(
|
||||||
updates: msg.connectionQuality.updates,
|
updates: msg.connectionQuality.updates,
|
||||||
@@ -209,8 +213,9 @@ class SignalClient extends Disposable with EventsEmittable<SignalEvent> {
|
|||||||
case lk_rtc.SignalResponse_Message.refreshToken:
|
case lk_rtc.SignalResponse_Message.refreshToken:
|
||||||
events.emit(SignalTokenUpdatedEvent(token: msg.refreshToken));
|
events.emit(SignalTokenUpdatedEvent(token: msg.refreshToken));
|
||||||
break;
|
break;
|
||||||
default:
|
case lk_rtc.SignalResponse_Message.notSet:
|
||||||
logger.warning('skipping unsupported signal message');
|
logger.info('signal message not set');
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import '../types.dart';
|
|||||||
import '../utils.dart';
|
import '../utils.dart';
|
||||||
|
|
||||||
typedef PCTransportOnOffer = void Function(rtc.RTCSessionDescription offer);
|
typedef PCTransportOnOffer = void Function(rtc.RTCSessionDescription offer);
|
||||||
|
typedef PeerConnectionCreate = Future<rtc.RTCPeerConnection> Function(
|
||||||
|
Map<String, dynamic> configuration,
|
||||||
|
[Map<String, dynamic> constraints]);
|
||||||
|
|
||||||
/// a wrapper around PeerConnection
|
/// a wrapper around PeerConnection
|
||||||
class PCTransport extends Disposable {
|
class PCTransport extends Disposable {
|
||||||
@@ -55,10 +58,11 @@ class PCTransport extends Disposable {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<PCTransport> create([RTCConfiguration? rtcConfig]) async {
|
static Future<PCTransport> create(PeerConnectionCreate peerConnectionCreate,
|
||||||
|
[RTCConfiguration? rtcConfig]) async {
|
||||||
rtcConfig ??= const RTCConfiguration();
|
rtcConfig ??= const RTCConfiguration();
|
||||||
logger.fine('[PCTransport] creating ${rtcConfig.toMap()}');
|
logger.fine('[PCTransport] creating ${rtcConfig.toMap()}');
|
||||||
final _ = await rtc.createPeerConnection(rtcConfig.toMap());
|
final _ = await peerConnectionCreate(rtcConfig.toMap());
|
||||||
return PCTransport._(_);
|
return PCTransport._(_);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,5 +163,6 @@ class PCTransport extends Disposable {
|
|||||||
} catch (_) {
|
} catch (_) {
|
||||||
logger.warning('pc.getRemoteDescription failed with error: $_');
|
logger.warning('pc.getRemoteDescription failed with error: $_');
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -305,6 +305,7 @@ class LocalParticipant extends Participant<LocalTrackPublication> {
|
|||||||
return await publishVideoTrack(track);
|
return await publishVideoTrack(track);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Control who can subscribe to LocalParticipant's published tracks.
|
/// Control who can subscribe to LocalParticipant's published tracks.
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ class RemoteParticipant extends Participant<RemoteTrackPublication> {
|
|||||||
RemoteTrackPublication? getTrackPublication(String sid) {
|
RemoteTrackPublication? getTrackPublication(String sid) {
|
||||||
final pub = trackPublications[sid];
|
final pub = trackPublications[sid];
|
||||||
if (pub is RemoteTrackPublication) return pub;
|
if (pub is RemoteTrackPublication) return pub;
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// for internal use
|
/// for internal use
|
||||||
|
|||||||
@@ -36,5 +36,6 @@ class Native {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warning('appleOSVersionString did throw error: ${error}');
|
logger.warning('appleOSVersionString did throw error: ${error}');
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
import 'platform/io.dart' if (dart.library.html) 'platform/web.dart';
|
import 'platform/io.dart' if (dart.library.html) 'platform/web.dart';
|
||||||
|
|
||||||
// Returns the current platform which works for both web and devices.
|
// Returns the current platform which works for both web and devices.
|
||||||
PlatformType lkPlatform() => lkPlatformImplementation();
|
PlatformType lkPlatform() => lkPlatformImplementation();
|
||||||
|
|
||||||
bool lkPlatformIs(PlatformType type) => lkPlatform() == type;
|
bool lkPlatformIs(PlatformType type) => lkPlatform() == type;
|
||||||
|
|
||||||
|
@internal
|
||||||
|
bool lkPlatformIsTest() => Platform.environment.containsKey('FLUTTER_TEST');
|
||||||
|
|
||||||
enum PlatformType {
|
enum PlatformType {
|
||||||
web,
|
web,
|
||||||
windows,
|
windows,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class WebSocketEventHandlers {
|
|||||||
final WebSocketOnData? onData;
|
final WebSocketOnData? onData;
|
||||||
final WebSocketOnError? onError;
|
final WebSocketOnError? onError;
|
||||||
final WebSocketOnDispose? onDispose;
|
final WebSocketOnDispose? onDispose;
|
||||||
|
|
||||||
const WebSocketEventHandlers({
|
const WebSocketEventHandlers({
|
||||||
this.onData,
|
this.onData,
|
||||||
this.onError,
|
this.onError,
|
||||||
@@ -30,12 +31,13 @@ class WebSocketEventHandlers {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef WebSocketConnector = Future<LiveKitWebSocket> Function(Uri uri,
|
||||||
|
[WebSocketEventHandlers? options]);
|
||||||
|
|
||||||
abstract class LiveKitWebSocket extends Disposable {
|
abstract class LiveKitWebSocket extends Disposable {
|
||||||
void send(List<int> data);
|
void send(List<int> data);
|
||||||
|
|
||||||
static Future<LiveKitWebSocket> connect(
|
static Future<LiveKitWebSocket> connect(Uri uri,
|
||||||
Uri uri, [
|
[WebSocketEventHandlers? options]) =>
|
||||||
WebSocketEventHandlers? options,
|
|
||||||
]) =>
|
|
||||||
lkWebSocketConnect(uri, options);
|
lkWebSocketConnect(uri, options);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,11 @@ class Utils {
|
|||||||
static final _deviceInfoPlugin = DeviceInfoPlugin();
|
static final _deviceInfoPlugin = DeviceInfoPlugin();
|
||||||
|
|
||||||
static Future<lk_models.ClientInfo?> _clientInfo() async {
|
static Future<lk_models.ClientInfo?> _clientInfo() async {
|
||||||
|
if (lkPlatformIsTest()) {
|
||||||
|
return lk_models.ClientInfo(
|
||||||
|
os: 'test',
|
||||||
|
);
|
||||||
|
}
|
||||||
switch (lkPlatform()) {
|
switch (lkPlatform()) {
|
||||||
case PlatformType.web:
|
case PlatformType.web:
|
||||||
return lk_models.ClientInfo(
|
return lk_models.ClientInfo(
|
||||||
@@ -123,6 +128,7 @@ class Utils {
|
|||||||
default:
|
default:
|
||||||
// case PlatformType.fuchsia:
|
// case PlatformType.fuchsia:
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@internal
|
@internal
|
||||||
|
|||||||
+113
-1
@@ -1,6 +1,20 @@
|
|||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
|
_fe_analyzer_shared:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: _fe_analyzer_shared
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "34.0.0"
|
||||||
|
analyzer:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: analyzer
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.0"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -22,6 +36,27 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
|
build:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: build
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.1"
|
||||||
|
built_collection:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: built_collection
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "5.1.1"
|
||||||
|
built_value:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: built_value
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "8.1.4"
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -36,6 +71,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.1"
|
version: "1.3.1"
|
||||||
|
cli_util:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cli_util
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.5"
|
||||||
clock:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -43,6 +85,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
|
code_builder:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: code_builder
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.0"
|
||||||
collection:
|
collection:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -50,6 +99,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.15.0"
|
version: "1.15.0"
|
||||||
|
convert:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: convert
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.1"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -57,6 +113,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.0.1"
|
||||||
|
dart_style:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dart_style
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.1"
|
||||||
dart_webrtc:
|
dart_webrtc:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -163,6 +226,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.1"
|
version: "0.8.1"
|
||||||
|
glob:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: glob
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.2"
|
||||||
http:
|
http:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -212,6 +282,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.11"
|
version: "0.12.11"
|
||||||
|
material_color_utilities:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: material_color_utilities
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.3"
|
||||||
meta:
|
meta:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -219,6 +296,20 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.7.0"
|
version: "1.7.0"
|
||||||
|
mockito:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: mockito
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "5.0.17"
|
||||||
|
package_config:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: package_config
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.2"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -303,11 +394,25 @@ 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
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.99"
|
version: "0.0.99"
|
||||||
|
source_gen:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_gen
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.1"
|
||||||
source_span:
|
source_span:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -356,7 +461,7 @@ packages:
|
|||||||
name: test_api
|
name: test_api
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.3"
|
version: "0.4.8"
|
||||||
tint:
|
tint:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -385,6 +490,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.1.1"
|
||||||
|
watcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: watcher
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
webrtc_interface:
|
webrtc_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ dev_dependencies:
|
|||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_lints: ^1.0.4
|
flutter_lints: ^1.0.4
|
||||||
|
mockito: ^5.0.17
|
||||||
import_sorter: ^4.6.0
|
import_sorter: ^4.6.0
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
@Timeout(Duration(seconds: 5))
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
|
|
||||||
|
import '../mock/e2e_container.dart';
|
||||||
|
import '../mock/test_data.dart';
|
||||||
|
import '../mock/websocket_mock.dart';
|
||||||
|
import 'signal_client_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late E2EContainer container;
|
||||||
|
late Room room;
|
||||||
|
late MockWebSocketConnector ws;
|
||||||
|
setUp(() async {
|
||||||
|
container = E2EContainer();
|
||||||
|
room = container.room;
|
||||||
|
ws = container.wsConnector;
|
||||||
|
await container.connectRoom();
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await container.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
group('connection', () {
|
||||||
|
test('disconnect and connect', () async {
|
||||||
|
await room.disconnect();
|
||||||
|
await container.connectRoom();
|
||||||
|
expect(room.connectionState, ConnectionState.connected);
|
||||||
|
expect(room.localParticipant?.sid, joinResponse.join.participant.sid);
|
||||||
|
}, skip: 'todo');
|
||||||
|
});
|
||||||
|
|
||||||
|
group('room updates', () {
|
||||||
|
test('participant join', () async {
|
||||||
|
expect(
|
||||||
|
room.events.streamCtrl.stream,
|
||||||
|
emits(predicate<ParticipantConnectedEvent>(
|
||||||
|
(event) => event.participant.sid == remoteParticipantData.sid,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
ws.onData(participantJoinResponse.writeToBuffer());
|
||||||
|
|
||||||
|
await room.events.waitFor<ParticipantConnectedEvent>(
|
||||||
|
duration: const Duration(seconds: 1));
|
||||||
|
expect(room.participants.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('participant disconnect', () async {
|
||||||
|
ws.onData(participantJoinResponse.writeToBuffer());
|
||||||
|
await room.events.waitFor<ParticipantConnectedEvent>(
|
||||||
|
duration: const Duration(seconds: 1));
|
||||||
|
|
||||||
|
ws.onData(participantDisconnectResponse.writeToBuffer());
|
||||||
|
expect(
|
||||||
|
room.events.streamCtrl.stream,
|
||||||
|
emitsInOrder(<Matcher>[
|
||||||
|
predicate<TrackUnpublishedEvent>(
|
||||||
|
(event) => event.participant.sid == remoteParticipantData.sid),
|
||||||
|
predicate<ParticipantDisconnectedEvent>(
|
||||||
|
(event) => event.participant.sid == remoteParticipantData.sid),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
await room.events.waitFor<ParticipantDisconnectedEvent>(
|
||||||
|
duration: const Duration(seconds: 1));
|
||||||
|
expect(room.participants.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('participant metadata changed', () async {
|
||||||
|
ws.onData(participantJoinResponse.writeToBuffer());
|
||||||
|
await room.events.waitFor<ParticipantConnectedEvent>(
|
||||||
|
duration: const Duration(seconds: 1));
|
||||||
|
|
||||||
|
ws.onData(participantMetadataChangedResponse.writeToBuffer());
|
||||||
|
expect(
|
||||||
|
room.events.streamCtrl.stream,
|
||||||
|
emits(
|
||||||
|
predicate<ParticipantMetadataUpdatedEvent>((event) =>
|
||||||
|
event.participant.metadata ==
|
||||||
|
participantMetadataChangedResponse
|
||||||
|
.update.participants[0].metadata),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('room update', () async {
|
||||||
|
ws.onData(roomUpdateResponse.writeToBuffer());
|
||||||
|
// TODO: room update event and handling
|
||||||
|
}, skip: 'todo');
|
||||||
|
|
||||||
|
test('connection quality', () async {
|
||||||
|
expect(
|
||||||
|
room.events.streamCtrl.stream,
|
||||||
|
emits(predicate<ParticipantConnectionQualityUpdatedEvent>((event) =>
|
||||||
|
event.participant.sid == localParticipantData.sid &&
|
||||||
|
event.connectionQuality == ConnectionQuality.excellent)),
|
||||||
|
);
|
||||||
|
ws.onData(connectionQualityResponse.writeToBuffer());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('active speakers changed', () async {
|
||||||
|
ws.onData(participantJoinResponse.writeToBuffer());
|
||||||
|
await room.events.waitFor<ParticipantConnectedEvent>(
|
||||||
|
duration: const Duration(seconds: 1));
|
||||||
|
|
||||||
|
expect(
|
||||||
|
room.events.streamCtrl.stream,
|
||||||
|
emits(
|
||||||
|
predicate<ActiveSpeakersChangedEvent>(
|
||||||
|
(event) => event.speakers[0].sid == remoteParticipantData.sid),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
ws.onData(activeSpeakerResponse.writeToBuffer());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('leave', () async {
|
||||||
|
expect(
|
||||||
|
room.events.streamCtrl.stream,
|
||||||
|
emits(predicate<RoomDisconnectedEvent>((event) =>
|
||||||
|
room.connectionState == ConnectionState.disconnected)));
|
||||||
|
ws.onData(leaveResponse.writeToBuffer());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
@Timeout(Duration(seconds: 5))
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
|
import 'package:livekit_client/src/core/signal_client.dart';
|
||||||
|
import 'package:livekit_client/src/internal/events.dart';
|
||||||
|
import 'package:livekit_client/src/proto/livekit_models.pb.dart' as lk_models;
|
||||||
|
import 'package:livekit_client/src/proto/livekit_rtc.pb.dart' as lk_rtc;
|
||||||
|
import 'package:protobuf/protobuf.dart';
|
||||||
|
|
||||||
|
import '../mock/test_data.dart';
|
||||||
|
import '../mock/websocket_mock.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late SignalClient client;
|
||||||
|
late MockWebSocketConnector connector;
|
||||||
|
setUp(() async {
|
||||||
|
connector = MockWebSocketConnector();
|
||||||
|
client = SignalClient(connector.connect);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('connection', () {
|
||||||
|
test('connect', () async {
|
||||||
|
expect(
|
||||||
|
client.events.streamCtrl.stream,
|
||||||
|
emitsInOrder(<Matcher>[
|
||||||
|
predicate<SignalConnectionStateUpdatedEvent>(
|
||||||
|
(event) => event.connectionState == ConnectionState.connecting),
|
||||||
|
predicate<SignalConnectionStateUpdatedEvent>(
|
||||||
|
(event) => event.connectionState == ConnectionState.connected),
|
||||||
|
]));
|
||||||
|
await client.connect(exampleUri, token);
|
||||||
|
});
|
||||||
|
test('reconnect', () async {
|
||||||
|
expect(
|
||||||
|
client.events.streamCtrl.stream,
|
||||||
|
emitsInOrder(<Matcher>[
|
||||||
|
predicate<SignalConnectionStateUpdatedEvent>((event) =>
|
||||||
|
event.connectionState == ConnectionState.reconnecting),
|
||||||
|
predicate<SignalConnectionStateUpdatedEvent>((event) =>
|
||||||
|
event.connectionState == ConnectionState.connected &&
|
||||||
|
event.didReconnect == true),
|
||||||
|
]));
|
||||||
|
await client.connect(exampleUri, token, reconnect: true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('messaging', () {
|
||||||
|
test('join', () async {
|
||||||
|
await client.connect(exampleUri, token);
|
||||||
|
expect(client.events.streamCtrl.stream,
|
||||||
|
emits(isA<SignalJoinResponseEvent>()));
|
||||||
|
connector.handlers?.onData!(joinResponse.writeToBuffer());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final lk_rtc.SignalResponse joinResponse = lk_rtc.SignalResponse(
|
||||||
|
join: lk_rtc.JoinResponse(
|
||||||
|
room: lk_models.Room(
|
||||||
|
name: 'room_name',
|
||||||
|
sid: 'room_sid',
|
||||||
|
),
|
||||||
|
participant: localParticipantData,
|
||||||
|
subscriberPrimary: true,
|
||||||
|
serverVersion: '99.999',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final lk_rtc.SignalResponse offerResponse = lk_rtc.SignalResponse(
|
||||||
|
offer: lk_rtc.SessionDescription(
|
||||||
|
sdp: 'remote_offer',
|
||||||
|
type: 'offer',
|
||||||
|
));
|
||||||
|
|
||||||
|
final lk_rtc.SignalResponse participantJoinResponse = lk_rtc.SignalResponse(
|
||||||
|
update: lk_rtc.ParticipantUpdate(
|
||||||
|
participants: [remoteParticipantData],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final lk_rtc.SignalResponse participantDisconnectResponse =
|
||||||
|
lk_rtc.SignalResponse(
|
||||||
|
update: lk_rtc.ParticipantUpdate(
|
||||||
|
participants: [
|
||||||
|
remoteParticipantData.deepCopy()
|
||||||
|
..state = lk_models.ParticipantInfo_State.DISCONNECTED,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final lk_rtc.SignalResponse participantMetadataChangedResponse =
|
||||||
|
lk_rtc.SignalResponse(
|
||||||
|
update: lk_rtc.ParticipantUpdate(
|
||||||
|
participants: [
|
||||||
|
remoteParticipantData.deepCopy()..metadata = 'metadata_changed',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final lk_rtc.SignalResponse roomUpdateResponse = lk_rtc.SignalResponse(
|
||||||
|
roomUpdate: lk_rtc.RoomUpdate(
|
||||||
|
room: lk_models.Room(
|
||||||
|
metadata: 'changed_metadata',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final lk_rtc.SignalResponse connectionQualityResponse = lk_rtc.SignalResponse(
|
||||||
|
connectionQuality: lk_rtc.ConnectionQualityUpdate(updates: [
|
||||||
|
lk_rtc.ConnectionQualityInfo(
|
||||||
|
participantSid: localParticipantData.sid,
|
||||||
|
quality: lk_models.ConnectionQuality.EXCELLENT,
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
final lk_rtc.SignalResponse activeSpeakerResponse = lk_rtc.SignalResponse(
|
||||||
|
speakersChanged: lk_rtc.SpeakersChanged(
|
||||||
|
speakers: [remoteSpeakerInfo],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final lk_rtc.SignalResponse leaveResponse =
|
||||||
|
lk_rtc.SignalResponse(leave: lk_rtc.LeaveRequest());
|
||||||
|
const exampleUri = 'ws://www.example.com';
|
||||||
|
const token = 'token';
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||||
|
|
||||||
|
class MockDataChannel extends RTCDataChannel {
|
||||||
|
final String? _label;
|
||||||
|
RTCDataChannelState? _state = RTCDataChannelState.RTCDataChannelOpen;
|
||||||
|
|
||||||
|
MockDataChannel(this._label);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get label => _label;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> send(RTCDataChannelMessage message) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
RTCDataChannelState? get state => _state;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() async {
|
||||||
|
_state = RTCDataChannelState.RTCDataChannelClosing;
|
||||||
|
_state = RTCDataChannelState.RTCDataChannelClosed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
|
import 'package:livekit_client/src/core/engine.dart';
|
||||||
|
import 'package:livekit_client/src/core/signal_client.dart';
|
||||||
|
|
||||||
|
import '../core/signal_client_test.dart';
|
||||||
|
import 'peerconnection_mock.dart';
|
||||||
|
import 'websocket_mock.dart';
|
||||||
|
|
||||||
|
class E2EContainer {
|
||||||
|
late MockWebSocketConnector wsConnector;
|
||||||
|
late SignalClient client;
|
||||||
|
late Room room;
|
||||||
|
late Engine engine;
|
||||||
|
|
||||||
|
E2EContainer() {
|
||||||
|
wsConnector = MockWebSocketConnector();
|
||||||
|
client = SignalClient(wsConnector.connect);
|
||||||
|
engine = Engine(
|
||||||
|
signalClient: client, peerConnectionCreate: MockPeerConnection.create);
|
||||||
|
room = Room(engine: engine);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> dispose() async {
|
||||||
|
await room.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> connectRoom() async {
|
||||||
|
final connectFuture = room.connect(exampleUri, token);
|
||||||
|
Future.delayed(const Duration(milliseconds: 1), () {
|
||||||
|
wsConnector.onData(joinResponse.writeToBuffer());
|
||||||
|
wsConnector.onData(offerResponse.writeToBuffer());
|
||||||
|
});
|
||||||
|
await connectFuture;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
@Timeout(Duration(seconds: 5))
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
|
|
||||||
|
import '../core/signal_client_test.dart';
|
||||||
|
import '../mock/e2e_container.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late E2EContainer container;
|
||||||
|
late Room room;
|
||||||
|
setUp(() async {
|
||||||
|
container = E2EContainer();
|
||||||
|
room = container.room;
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await container.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('connect', () async {
|
||||||
|
await container.connectRoom();
|
||||||
|
expect(room.connectionState, ConnectionState.connected);
|
||||||
|
expect(room.localParticipant?.sid, joinResponse.join.participant.sid);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||||
|
|
||||||
|
import 'datachannel_mock.dart';
|
||||||
|
|
||||||
|
class MockPeerConnection extends RTCPeerConnection {
|
||||||
|
static const _offerType = 'offer';
|
||||||
|
static const _answerType = 'answer';
|
||||||
|
|
||||||
|
bool closed = false;
|
||||||
|
RTCSessionDescription? _localDescription;
|
||||||
|
RTCSessionDescription? _remoteDescription;
|
||||||
|
|
||||||
|
RTCPeerConnectionState _connectionState =
|
||||||
|
RTCPeerConnectionState.RTCPeerConnectionStateNew;
|
||||||
|
RTCIceConnectionState _iceConnectionState =
|
||||||
|
RTCIceConnectionState.RTCIceConnectionStateNew;
|
||||||
|
RTCIceGatheringState _iceGatheringState =
|
||||||
|
RTCIceGatheringState.RTCIceGatheringStateNew;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<RTCSessionDescription?> getLocalDescription() async =>
|
||||||
|
_localDescription;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setLocalDescription(RTCSessionDescription description) async {
|
||||||
|
_localDescription = description;
|
||||||
|
_handleIceConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<RTCSessionDescription?> getRemoteDescription() async =>
|
||||||
|
_remoteDescription;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setRemoteDescription(RTCSessionDescription description) async {
|
||||||
|
_remoteDescription = description;
|
||||||
|
_handleIceConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleIceConnection() {
|
||||||
|
if ((_localDescription?.type == _offerType &&
|
||||||
|
_remoteDescription?.type == _answerType) ||
|
||||||
|
(_localDescription?.type == _answerType &&
|
||||||
|
_remoteDescription?.type == _offerType)) {
|
||||||
|
iceConnectionState = RTCIceConnectionState.RTCIceConnectionStateCompleted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
RTCPeerConnectionState get connectionState => _connectionState;
|
||||||
|
|
||||||
|
set connectionState(RTCPeerConnectionState newState) {
|
||||||
|
if (newState != _connectionState) {
|
||||||
|
_connectionState = newState;
|
||||||
|
onConnectionState?.call(newState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
RTCIceConnectionState get iceConnectionState => _iceConnectionState;
|
||||||
|
|
||||||
|
set iceConnectionState(RTCIceConnectionState newState) {
|
||||||
|
if (newState != _iceConnectionState) {
|
||||||
|
_iceConnectionState = newState;
|
||||||
|
onIceConnectionState?.call(newState);
|
||||||
|
|
||||||
|
switch (newState) {
|
||||||
|
case RTCIceConnectionState.RTCIceConnectionStateNew:
|
||||||
|
connectionState = RTCPeerConnectionState.RTCPeerConnectionStateNew;
|
||||||
|
break;
|
||||||
|
case RTCIceConnectionState.RTCIceConnectionStateChecking:
|
||||||
|
connectionState =
|
||||||
|
RTCPeerConnectionState.RTCPeerConnectionStateConnecting;
|
||||||
|
break;
|
||||||
|
case RTCIceConnectionState.RTCIceConnectionStateConnected:
|
||||||
|
case RTCIceConnectionState.RTCIceConnectionStateCompleted:
|
||||||
|
connectionState =
|
||||||
|
RTCPeerConnectionState.RTCPeerConnectionStateConnected;
|
||||||
|
break;
|
||||||
|
case RTCIceConnectionState.RTCIceConnectionStateFailed:
|
||||||
|
connectionState = RTCPeerConnectionState.RTCPeerConnectionStateFailed;
|
||||||
|
break;
|
||||||
|
case RTCIceConnectionState.RTCIceConnectionStateDisconnected:
|
||||||
|
connectionState =
|
||||||
|
RTCPeerConnectionState.RTCPeerConnectionStateDisconnected;
|
||||||
|
break;
|
||||||
|
case RTCIceConnectionState.RTCIceConnectionStateClosed:
|
||||||
|
connectionState = RTCPeerConnectionState.RTCPeerConnectionStateClosed;
|
||||||
|
break;
|
||||||
|
case RTCIceConnectionState.RTCIceConnectionStateCount:
|
||||||
|
throw Exception("This state shouldn't exist (not in RFC).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
RTCIceGatheringState get iceGatheringState => _iceGatheringState;
|
||||||
|
|
||||||
|
set iceGatheringState(RTCIceGatheringState newState) {
|
||||||
|
if (newState != _iceGatheringState) {
|
||||||
|
_iceGatheringState = newState;
|
||||||
|
onIceGatheringState?.call(newState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> addCandidate(RTCIceCandidate candidate) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> addStream(MediaStream stream) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<RTCRtpSender> addTrack(MediaStreamTrack track,
|
||||||
|
[MediaStream? stream]) async {
|
||||||
|
// TODO: implement addTrack
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<RTCRtpTransceiver> addTransceiver(
|
||||||
|
{MediaStreamTrack? track,
|
||||||
|
RTCRtpMediaType? kind,
|
||||||
|
RTCRtpTransceiverInit? init}) {
|
||||||
|
// TODO: implement addTransceiver
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() async {
|
||||||
|
closed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<RTCSessionDescription> createAnswer(
|
||||||
|
[Map<String, dynamic>? constraints]) async {
|
||||||
|
return RTCSessionDescription('local_answer', 'answer');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<RTCSessionDescription> createOffer(
|
||||||
|
[Map<String, dynamic>? constraints]) async {
|
||||||
|
return RTCSessionDescription('local_offer', 'offer');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
RTCSignalingState? get signalingState {
|
||||||
|
if (closed) {
|
||||||
|
return RTCSignalingState.RTCSignalingStateClosed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((_localDescription?.type == null && _remoteDescription?.type == null) ||
|
||||||
|
(_localDescription?.type == _offerType &&
|
||||||
|
_remoteDescription?.type == _answerType) ||
|
||||||
|
(_localDescription?.type == _answerType &&
|
||||||
|
_remoteDescription?.type == _offerType)) {
|
||||||
|
return RTCSignalingState.RTCSignalingStateStable;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_localDescription?.type == _offerType &&
|
||||||
|
_remoteDescription?.type == null) {
|
||||||
|
return RTCSignalingState.RTCSignalingStateHaveLocalOffer;
|
||||||
|
}
|
||||||
|
if (_remoteDescription?.type == _offerType &&
|
||||||
|
_localDescription?.type == null) {
|
||||||
|
return RTCSignalingState.RTCSignalingStateHaveRemoteOffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception(
|
||||||
|
'Illegal signalling state? localDesc: $_localDescription, remoteDesc: $_remoteDescription');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<RTCDataChannel> createDataChannel(
|
||||||
|
String label, RTCDataChannelInit dataChannelDict) async {
|
||||||
|
return MockDataChannel(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
RTCDTMFSender createDtmfSender(MediaStreamTrack track) {
|
||||||
|
// TODO: implement createDtmfSender
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> dispose() async {
|
||||||
|
await close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
// TODO: implement getConfiguration
|
||||||
|
Map<String, dynamic> get getConfiguration => throw UnimplementedError();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<MediaStream?> getLocalStreams() => List.empty();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<RTCRtpReceiver>> getReceivers() async => List.empty();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<MediaStream?> getRemoteStreams() => List.empty();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<RTCRtpSender>> getSenders() async => List.empty();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<StatsReport>> getStats([MediaStreamTrack? track]) async =>
|
||||||
|
List.empty();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<RTCRtpTransceiver>> getTransceivers() async => List.empty();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> removeStream(MediaStream stream) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> removeTrack(RTCRtpSender sender) async => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setConfiguration(Map<String, dynamic> configuration) {
|
||||||
|
// TODO: implement setConfiguration
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<RTCPeerConnection> create(Map<String, dynamic> configuration,
|
||||||
|
[Map<String, dynamic>? constraints]) async =>
|
||||||
|
MockPeerConnection();
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
|
import 'package:livekit_client/src/proto/livekit_models.pb.dart' as lk_models;
|
||||||
|
|
||||||
|
final localAudioTrack = lk_models.TrackInfo(
|
||||||
|
sid: 'local_audio_track_sid',
|
||||||
|
type: TrackType.AUDIO,
|
||||||
|
);
|
||||||
|
|
||||||
|
final remoteAudioTrack = lk_models.TrackInfo(
|
||||||
|
sid: 'remote_audio_track_sid',
|
||||||
|
type: TrackType.AUDIO,
|
||||||
|
);
|
||||||
|
|
||||||
|
final localParticipantData = lk_models.ParticipantInfo(
|
||||||
|
sid: 'local_participant_sid',
|
||||||
|
identity: 'local_participant_identity',
|
||||||
|
state: lk_models.ParticipantInfo_State.ACTIVE,
|
||||||
|
);
|
||||||
|
|
||||||
|
final remoteParticipantData = lk_models.ParticipantInfo(
|
||||||
|
sid: 'remote_participant_sid',
|
||||||
|
identity: 'remote_participant_identity',
|
||||||
|
state: lk_models.ParticipantInfo_State.ACTIVE,
|
||||||
|
tracks: [remoteAudioTrack],
|
||||||
|
);
|
||||||
|
|
||||||
|
final remoteSpeakerInfo = lk_models.SpeakerInfo(
|
||||||
|
sid: remoteParticipantData.sid,
|
||||||
|
level: 1.0,
|
||||||
|
active: true,
|
||||||
|
);
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import 'package:livekit_client/src/support/websocket.dart';
|
||||||
|
|
||||||
|
class MockWebSocket extends LiveKitWebSocket {
|
||||||
|
@override
|
||||||
|
void send(List<int> data) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockWebSocketConnector {
|
||||||
|
WebSocketEventHandlers? handlers;
|
||||||
|
|
||||||
|
WebSocketOnData get onData => handlers!.onData!;
|
||||||
|
|
||||||
|
WebSocketOnDispose get onDispose => handlers!.onDispose!;
|
||||||
|
|
||||||
|
WebSocketOnError get onError => handlers!.onError!;
|
||||||
|
|
||||||
|
Future<LiveKitWebSocket> connect(Uri uri,
|
||||||
|
[WebSocketEventHandlers? options]) async {
|
||||||
|
handlers = options;
|
||||||
|
return MockWebSocket();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user