* chore: e2ee.

* update.

* update.

* update.

* update.

* chore: Use feat/frame-encryption branch of flutter-webrtc.

* chore: Add E2EEKEY defines for dart environment, and e2ee switch.

* Add encodedInsertableStreams to RTCConfiguration.

* update.

* feat: Add e2ee indicator for Participant.

* feat: add e2ee worker js for flutter web.

* dart format.

* remove unused file.

* fix flutter analyze .

* update.

* update.

* add: indicate for decryption failure, and string key.

* remove .lock files.

* update.

* update.

* update e2ee.worker for web.

* feat: support setCodecPreferences.

* state TrackE2EEStateEvent.

* fix wrong import interface from dart_webrtc.

* update.

* update.

* update.

* update.

* update pubspec.lock.

* chore: update protocol and add EncryptionType for Participant.

* Update lib/src/e2ee/options.dart

Co-authored-by: Théo Monnom <theo.monnom@outlook.com>

* fix typo.

* revert changes for internal import.

* Add _cleanUp() for previous room.

* Add e2ee supports detection method for native/web.

* Remove redundant overriding methods.

* dart format.

* Add e2ee.worker code and deployment docs.

* chore: remove duplicate words.

* chore: using Pbkdf2 derive the key.

* Update pubspec.yaml

* fix e2ee for safari.

* fix key length.

* update e2ee.worker.dart.js.

* fix.

* update proto.

* update.

* chore: add simulate for rachetKey.

* update.

* chore: key ratchet for flutter web.

* update.

* update.

* update.

* chore: key ratchet export for web.

* bump version for xframeworks.

* update.

* chore: some changes for key safety ratcheting.

* update.

* fix typo.

* update.

* rename.

* magic bytes for web.

* bump version for flutter-webrtc.

* fix analyzer.

---------

Co-authored-by: Théo Monnom <theo.monnom@outlook.com>
This commit is contained in:
CloudWebRTC
2023-04-27 17:54:32 +08:00
committed by GitHub
parent 3407221fc4
commit 26947e96d6
42 changed files with 12523 additions and 39 deletions
+22
View File
@@ -6,6 +6,7 @@ import 'package:collection/collection.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
import 'package:meta/meta.dart';
import '../e2ee/options.dart';
import '../events.dart';
import '../exceptions.dart';
import '../extensions.dart';
@@ -188,6 +189,21 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
}) async {
// TODO: Check if cid already published
lk_models.Encryption_Type encryptionType = lk_models.Encryption_Type.NONE;
if (roomOptions.e2eeOptions != null) {
switch (roomOptions.e2eeOptions!.encryptionType) {
case EncryptionType.kNone:
encryptionType = lk_models.Encryption_Type.NONE;
break;
case EncryptionType.kGcm:
encryptionType = lk_models.Encryption_Type.GCM;
break;
case EncryptionType.kCustom:
encryptionType = lk_models.Encryption_Type.CUSTOM;
break;
}
}
// send request to add track
signalClient.sendAddTrack(
cid: cid,
@@ -197,6 +213,7 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
dimensions: dimensions,
dtx: dtx,
videoLayers: videoLayers,
encryptionType: encryptionType,
);
// wait for response, or timeout
@@ -299,6 +316,11 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
);
}
if (kIsWeb && roomOptions.e2eeOptions != null) {
rtcConfiguration =
rtcConfiguration.copyWith(encodedInsertableStreams: true);
}
return rtcConfiguration;
}
+29 -8
View File
@@ -7,7 +7,9 @@ import 'package:livekit_client/src/support/app_state.dart';
import 'package:meta/meta.dart';
import '../core/signal_client.dart';
import '../e2ee/e2ee_manager.dart';
import '../events.dart';
import '../exceptions.dart';
import '../extensions.dart';
import '../internal/events.dart';
import '../logger.dart';
@@ -71,6 +73,9 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
String? get serverRegion => _serverRegion;
String? _serverRegion;
E2EEManager? get e2eeManager => _e2eeManager;
E2EEManager? _e2eeManager;
bool get isRecording => _isRecording;
bool _isRecording = false;
@@ -140,14 +145,22 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
ConnectOptions? connectOptions,
RoomOptions? roomOptions,
FastConnectOptions? fastConnectOptions,
}) =>
engine.connect(
url,
token,
connectOptions: connectOptions,
roomOptions: roomOptions,
fastConnectOptions: fastConnectOptions,
);
}) {
if (roomOptions?.e2eeOptions != null) {
if (!lkPlatformSupportsE2EE()) {
throw LiveKitE2EEException('E2EE is not supported on this platform');
}
_e2eeManager = E2EEManager(roomOptions!.e2eeOptions!.keyProvider);
_e2eeManager!.setup(this);
}
return engine.connect(
url,
token,
connectOptions: connectOptions,
roomOptions: roomOptions,
fastConnectOptions: fastConnectOptions,
);
}
void _setUpSignalListeners() => _signalListener
..on<SignalJoinResponseEvent>((event) {
@@ -378,6 +391,14 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
await _cleanUp();
}
Future<void> setE2EEEnabled(bool enabled) async {
if (_e2eeManager != null) {
await _e2eeManager!.setEnabled(enabled);
} else {
throw LiveKitE2EEException('_e2eeManager not setup!');
}
}
RemoteParticipant _getOrCreateRemoteParticipant(
String sid, lk_models.ParticipantInfo? info) {
RemoteParticipant? participant = _participants[sid];
+2
View File
@@ -350,6 +350,7 @@ extension SignalClientRequests on SignalClient {
required String name,
required lk_models.TrackType type,
required lk_models.TrackSource source,
required lk_models.Encryption_Type encryptionType,
VideoDimensions? dimensions,
bool? dtx,
Iterable<lk_models.VideoLayer>? videoLayers,
@@ -359,6 +360,7 @@ extension SignalClientRequests on SignalClient {
name: name,
type: type,
source: source,
encryption: encryptionType,
);
if (type == lk_models.TrackType.VIDEO) {
+179
View File
@@ -0,0 +1,179 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:livekit_client/src/e2ee/events.dart';
import 'package:livekit_client/src/extensions.dart';
import '../events.dart';
import '../core/room.dart';
import '../managers/event.dart';
import 'key_provider.dart';
class E2EEManager {
Room? _room;
final Map<String, FrameCryptor> _frameCryptors = {};
final List<FrameCryptor> _senderFrameCryptors = [];
final BaseKeyProvider _keyProvider;
final Algorithm _algorithm = Algorithm.kAesGcm;
bool _enabled = true;
EventsListener<RoomEvent>? _listener;
E2EEManager(this._keyProvider);
Future<void> setup(Room room) async {
if (_room != room) {
await _cleanUp();
_room = room;
_listener = _room!.createListener();
_listener!
..on<LocalTrackPublishedEvent>((event) async {
var trackId = event.publication.sid;
var participantId = event.participant.sid;
var frameCryptor = await _addRtpSender(
event.publication.track!.sender!,
participantId,
trackId,
event.publication.track!.kind.name.toLowerCase());
if (kIsWeb && event.publication.track!.codec != null) {
await frameCryptor.updateCodec(event.publication.track!.codec!);
}
frameCryptor.onFrameCryptorStateChanged = (trackId, state) {
if (kDebugMode) {
print(
'Sender::onFrameCryptorStateChanged: $state, trackId: $trackId');
}
var participant = event.participant;
[event.participant.events, participant.room.events]
.emit(TrackE2EEStateEvent(
participant: participant,
publication: event.publication,
state: _e2eeStateFromFrameCryptoState(state),
));
};
_senderFrameCryptors.add(frameCryptor);
})
..on<LocalTrackUnpublishedEvent>((event) async {
var trackId = event.publication.sid;
var frameCryptor = _frameCryptors.remove(trackId);
_senderFrameCryptors.remove(frameCryptor);
await frameCryptor?.dispose();
})
..on<TrackSubscribedEvent>((event) async {
var trackId = event.publication.sid;
var participantId = event.participant.sid;
var frameCryptor = await _addRtpReceiver(event.track.receiver!,
participantId, trackId, event.track.kind.name.toLowerCase());
if (kIsWeb) {
var codec = event.publication.mimeType.split('/')[1];
await frameCryptor.updateCodec(codec.toLowerCase());
}
frameCryptor.onFrameCryptorStateChanged = (trackId, state) {
if (kDebugMode) {
print(
'Receiver::onFrameCryptorStateChanged: $state, trackId: $trackId');
}
var participant = event.participant;
[event.participant.events, participant.room.events]
.emit(TrackE2EEStateEvent(
participant: participant,
publication: event.publication,
state: _e2eeStateFromFrameCryptoState(state),
));
};
})
..on<TrackUnsubscribedEvent>((event) async {
var trackId = event.publication.sid;
var frameCryptor = _frameCryptors.remove(trackId);
await frameCryptor?.dispose();
});
}
}
Future<void> ratchetKey() async {
for (var frameCryptor in _senderFrameCryptors) {
var newKey = await _keyProvider.ratchetKey(frameCryptor.participantId, 0);
if (kDebugMode) {
print('newKey: $newKey');
}
}
}
Future<void> _cleanUp() async {
await _listener?.cancelAll();
await _listener?.dispose();
_listener = null;
for (var frameCryptor in _frameCryptors.values) {
await frameCryptor.dispose();
}
_frameCryptors.clear();
}
Future<FrameCryptor> _addRtpSender(RTCRtpSender sender, String participantId,
String trackId, String kind) async {
var pid = '$kind-sender-$participantId-$trackId';
var frameCryptor = await FrameCryptorFactory.instance
.createFrameCryptorForRtpSender(
participantId: pid,
sender: sender,
algorithm: _algorithm,
keyProvider: _keyProvider.keyProvider);
_frameCryptors[trackId] = frameCryptor;
await frameCryptor.setEnabled(_enabled);
if (_keyProvider.options.sharedKey) {
await _keyProvider.keyProvider
.setKey(participantId: pid, index: 0, key: _keyProvider.sharedKey!);
await frameCryptor.setKeyIndex(0);
}
return frameCryptor;
}
Future<FrameCryptor> _addRtpReceiver(RTCRtpReceiver receiver,
String participantId, String trackId, String kind) async {
var pid = '$kind-receiver-$participantId-$trackId';
var frameCryptor = await FrameCryptorFactory.instance
.createFrameCryptorForRtpReceiver(
participantId: pid,
receiver: receiver,
algorithm: _algorithm,
keyProvider: _keyProvider.keyProvider);
_frameCryptors[trackId] = frameCryptor;
await frameCryptor.setEnabled(_enabled);
if (_keyProvider.options.sharedKey) {
await _keyProvider.keyProvider
.setKey(participantId: pid, index: 0, key: _keyProvider.sharedKey!);
await frameCryptor.setKeyIndex(0);
}
return frameCryptor;
}
Future<void> setEnabled(bool enabled) async {
_enabled = enabled;
for (var frameCryptor in _frameCryptors.entries) {
await frameCryptor.value.setEnabled(enabled);
if (_keyProvider.options.sharedKey) {
await _keyProvider.keyProvider.setKey(
participantId: frameCryptor.key,
index: 0,
key: _keyProvider.sharedKey!);
await frameCryptor.value.setKeyIndex(0);
}
}
}
E2EEState _e2eeStateFromFrameCryptoState(FrameCryptorState state) {
switch (state) {
case FrameCryptorState.FrameCryptorStateNew:
return E2EEState.kNew;
case FrameCryptorState.FrameCryptorStateOk:
return E2EEState.kOk;
case FrameCryptorState.FrameCryptorStateMissingKey:
return E2EEState.kMissingKey;
case FrameCryptorState.FrameCryptorStateEncryptionFailed:
return E2EEState.kEncryptionFailed;
case FrameCryptorState.FrameCryptorStateDecryptionFailed:
return E2EEState.kDecryptionFailed;
case FrameCryptorState.FrameCryptorStateInternalError:
return E2EEState.kInternalError;
case FrameCryptorState.FrameCryptorStateKeyRatcheted:
return E2EEState.kKeyRatcheted;
}
}
}
+28
View File
@@ -0,0 +1,28 @@
import '../../livekit_client.dart';
enum E2EEState {
kNew,
kOk,
kKeyRatcheted,
kMissingKey,
kEncryptionFailed,
kDecryptionFailed,
kInternalError,
}
/// The [E2EEState] on the track.
/// Emitted by [E2EEManager].
class TrackE2EEStateEvent with RoomEvent, ParticipantEvent {
final Participant participant;
final TrackPublication publication;
final E2EEState state;
const TrackE2EEStateEvent({
required this.participant,
required this.publication,
required this.state,
});
@override
String toString() => '${runtimeType}'
'(participant: ${participant}, publication: ${publication}, state: ${state})';
}
+87
View File
@@ -0,0 +1,87 @@
import 'dart:typed_data';
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
const defaultRatchetSalt = 'LKFrameEncryptionKey';
const defaultMagicBytes = 'LK-ROCKS';
const defaultRatchetWindowSize = 16;
class KeyInfo {
final String participantId;
final int keyIndex;
final Uint8List key;
KeyInfo({
required this.participantId,
required this.keyIndex,
required this.key,
});
}
abstract class KeyProvider {
Future<void> setKey(String key, {String? participantId, int keyIndex = 0});
Future<Uint8List> ratchetKey(String participantId, int index);
rtc.KeyProvider get keyProvider;
}
class BaseKeyProvider implements KeyProvider {
final Map<String, Map<int, Uint8List>> _keys = {};
Uint8List? _sharedKey;
final rtc.KeyProviderOptions options;
final rtc.KeyProvider _keyProvider;
@override
rtc.KeyProvider get keyProvider => _keyProvider;
Uint8List? get sharedKey => _sharedKey;
BaseKeyProvider(this._keyProvider, this.options);
static Future<BaseKeyProvider> create({
bool sharedKey = true,
String? ratchetSalt,
String? uncryptedMagicBytes,
int? ratchetWindowSize,
}) async {
rtc.KeyProviderOptions options = rtc.KeyProviderOptions(
sharedKey: sharedKey,
ratchetSalt:
Uint8List.fromList((ratchetSalt ?? defaultRatchetSalt).codeUnits),
ratchetWindowSize: ratchetWindowSize ?? defaultRatchetWindowSize,
uncryptedMagicBytes: Uint8List.fromList(
(uncryptedMagicBytes ?? defaultMagicBytes).codeUnits),
);
final keyProvider = await rtc.FrameCryptorFactory.instance
.createDefaultKeyProvider(options);
return BaseKeyProvider(keyProvider, options);
}
@override
Future<Uint8List> ratchetKey(String participantId, int index) =>
_keyProvider.ratchetKey(participantId: participantId, index: index);
@override
Future<void> setKey(String key,
{String? participantId, int keyIndex = 0}) async {
if (options.sharedKey) {
_sharedKey = Uint8List.fromList(key.codeUnits);
return;
}
final keyInfo = KeyInfo(
participantId: participantId ?? '',
keyIndex: keyIndex,
key: Uint8List.fromList(key.codeUnits),
);
return _setKey(keyInfo);
}
Future<void> _setKey(KeyInfo keyInfo) async {
if (!_keys.containsKey(keyInfo.participantId)) {
_keys[keyInfo.participantId] = {};
}
_keys[keyInfo.participantId]![keyInfo.keyIndex] = keyInfo.key;
await _keyProvider.setKey(
participantId: keyInfo.participantId,
index: keyInfo.keyIndex,
key: keyInfo.key,
);
}
}
+13
View File
@@ -0,0 +1,13 @@
import 'key_provider.dart';
enum EncryptionType {
kNone,
kGcm,
kCustom,
}
class E2EEOptions {
final BaseKeyProvider keyProvider;
final EncryptionType encryptionType = EncryptionType.kGcm;
const E2EEOptions({required this.keyProvider});
}
+8
View File
@@ -57,3 +57,11 @@ class DataPublishException extends LiveKitException {
class TimeoutException extends LiveKitException {
TimeoutException([String msg = 'Timeout']) : super._(msg);
}
/// An exception for End to End Encryption.
class LiveKitE2EEException extends LiveKitException {
LiveKitE2EEException([String msg = 'E2EE error']) : super._(msg);
@override
String toString() => 'E2EE Exception: [$runtimeType] $message';
}
+9 -3
View File
@@ -2,12 +2,10 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
import 'package:livekit_client/livekit_client.dart';
import 'events.dart';
import 'managers/event.dart';
import 'proto/livekit_models.pb.dart' as lk_models;
import 'proto/livekit_rtc.pb.dart' as lk_rtc;
import 'types/other.dart';
extension DataPacketKindExt on lk_models.DataPacket_Kind {
Reliability toSDKType() => {
@@ -172,6 +170,14 @@ extension WidgetsBindingCompatible on WidgetsBinding {
static WidgetsBinding? get instance => WidgetsBinding.instance;
}
extension EncryptionTypeExt on lk_models.Encryption_Type {
EncryptionType toLkType() => {
lk_models.Encryption_Type.NONE: EncryptionType.kNone,
lk_models.Encryption_Type.GCM: EncryptionType.kGcm,
lk_models.Encryption_Type.CUSTOM: EncryptionType.kCustom,
}[this]!;
}
extension DisconnectReasonExt on lk_models.DisconnectReason {
DisconnectReason toSDKType() => {
lk_models.DisconnectReason.UNKNOWN_REASON: DisconnectReason.unknown,
+9
View File
@@ -1,5 +1,6 @@
import 'constants.dart';
import 'core/room.dart';
import 'e2ee/options.dart';
import 'publication/remote.dart';
import 'track/local/audio.dart';
import 'track/local/video.dart';
@@ -88,6 +89,9 @@ class RoomOptions {
/// Defaults to true.
final bool stopLocalTrackOnUnpublish;
/// Options for end-to-end encryption.
final E2EEOptions? e2eeOptions;
const RoomOptions({
this.defaultCameraCaptureOptions = const CameraCaptureOptions(),
this.defaultScreenShareCaptureOptions = const ScreenShareCaptureOptions(),
@@ -98,6 +102,7 @@ class RoomOptions {
this.adaptiveStream = false,
this.dynacast = false,
this.stopLocalTrackOnUnpublish = true,
this.e2eeOptions,
});
RoomOptions copyWith({
@@ -134,6 +139,9 @@ class RoomOptions {
/// Options used when publishing video.
class VideoPublishOptions {
/// The video codec to use.
final String videoCodec;
/// If provided, this will be used instead of the SDK's suggested encodings.
/// Usually you don't need to provide this.
/// Defaults to null.
@@ -149,6 +157,7 @@ class VideoPublishOptions {
final List<VideoParameters> screenShareSimulcastLayers;
const VideoPublishOptions({
this.videoCodec = 'H264',
this.videoEncoding,
this.simulcast = true,
this.videoSimulcastLayers = const [],
+35
View File
@@ -12,6 +12,7 @@ import '../options.dart';
import '../proto/livekit_models.pb.dart' as lk_models;
import '../proto/livekit_rtc.pb.dart' as lk_rtc;
import '../publication/local.dart';
import '../support/platform.dart';
import '../track/local/audio.dart';
import '../track/local/local.dart';
import '../track/local/video.dart';
@@ -182,6 +183,40 @@ class LocalParticipant extends Participant<LocalTrackPublication> {
init: transceiverInit,
);
if (lkBrowser() != BrowserType.firefox) {
var videoCodec = publishOptions.videoCodec.toLowerCase();
var caps = await rtc.getRtpSenderCapabilities('video');
List<rtc.RTCRtpCodecCapability> matched = [];
List<rtc.RTCRtpCodecCapability> partialMatched = [];
List<rtc.RTCRtpCodecCapability> unmatched = [];
for (var c in caps.codecs!) {
var codec = c.mimeType.toLowerCase();
if (codec == 'audio/opus') {
matched.add(c);
continue;
}
var matchesVideoCodec = codec == 'video/$videoCodec';
if (!matchesVideoCodec) {
unmatched.add(c);
continue;
}
if (publishOptions.videoCodec == 'h264') {
if (c.sdpFmtpLine != null &&
c.sdpFmtpLine!.contains('profile-level-id=42e01f')) {
matched.add(c);
} else {
partialMatched.add(c);
}
continue;
}
matched.add(c);
}
matched.addAll([...partialMatched, ...unmatched]);
await track.transceiver?.setCodecPreferences(matched);
track.codec = videoCodec;
}
// prefer to maintainResolution for screen share
if (track.source == TrackSource.screenShareVideo) {
var sender = track.transceiver!.sender;
+11 -3
View File
@@ -2,6 +2,7 @@ import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
import '../core/room.dart';
import '../e2ee/options.dart';
import '../events.dart';
import '../extensions.dart';
import '../logger.dart';
@@ -9,12 +10,9 @@ import '../managers/event.dart';
import '../proto/livekit_models.pb.dart' as lk_models;
import '../publication/track_publication.dart';
import '../support/disposable.dart';
import '../track/local/local.dart';
import '../track/track.dart';
import '../types/other.dart';
import '../types/participant_permissions.dart';
import 'local.dart';
import 'remote.dart';
/// Represents a Participant in the room, notifies changes via delegates as
/// well as ChangeNotifier/providers.
@@ -94,6 +92,16 @@ abstract class Participant<T extends TrackPublication>
// Must be implemented by child class.
List<T> get audioTracks;
EncryptionType get firstTrackEncryptionType {
if (hasAudio) {
return audioTracks.first.encryptionType;
} else if (hasVideo) {
return videoTracks.first.encryptionType;
} else {
return EncryptionType.kNone;
}
}
@internal
bool get hasInfo => _participantInfo != null;
+6 -7
View File
@@ -1,17 +1,11 @@
import 'package:livekit_client/livekit_client.dart';
import 'package:meta/meta.dart';
import '../core/signal_client.dart';
import '../events.dart';
import '../extensions.dart';
import '../internal/events.dart';
import '../logger.dart';
import '../participant/participant.dart';
import '../proto/livekit_models.pb.dart' as lk_models;
import '../support/disposable.dart';
import '../track/local/local.dart';
import '../track/track.dart';
import '../types/other.dart';
import '../types/video_dimensions.dart';
/// Represents a track that's published to the server. This class contains
/// metadata associated with tracks.
@@ -49,6 +43,11 @@ abstract class TrackPublication<T extends Track> extends Disposable {
bool get subscribed => track != null;
EncryptionType get encryptionType {
if (latestInfo == null) return EncryptionType.kNone;
return latestInfo!.encryption.toLkType();
}
@internal
lk_models.TrackInfo? latestInfo;
+2 -1
View File
@@ -1,5 +1,4 @@
import 'dart:io';
import 'platform/io.dart' if (dart.library.html) 'platform/web.dart';
// Returns the current platform which works for both web and devices.
@@ -7,6 +6,8 @@ PlatformType lkPlatform() => lkPlatformImplementation();
bool lkPlatformIs(PlatformType type) => lkPlatform() == type;
bool lkPlatformSupportsE2EE() => lkE2EESupportedImplementation();
bool lkPlatformIsTest() => Platform.environment.containsKey('FLUTTER_TEST');
BrowserType lkBrowser() => lkBrowserImplementation();
+10
View File
@@ -12,6 +12,16 @@ PlatformType lkPlatformImplementation() {
throw UnsupportedError('Unknown Platform');
}
bool lkE2EESupportedImplementation() {
return [
PlatformType.windows,
PlatformType.linux,
PlatformType.macOS,
PlatformType.iOS,
PlatformType.android,
].contains(lkPlatformImplementation());
}
BrowserType lkBrowserImplementation() {
return BrowserType.unknown;
}
+14
View File
@@ -1,9 +1,23 @@
import '../platform.dart';
import 'dart:js' as js;
import 'package:platform_detect/platform_detect.dart';
PlatformType lkPlatformImplementation() => PlatformType.web;
bool lkE2EESupportedImplementation() {
return isInsertableStreamSupported() || isScriptTransformSupported();
}
bool isScriptTransformSupported() {
return js.context['RTCRtpScriptTransform'] != null;
}
bool isInsertableStreamSupported() {
return js.context['RTCRtpSender'] != null &&
js.context['RTCRtpSender']['prototype']['createEncodedStreams'] != null;
}
BrowserType lkBrowserImplementation() {
if (browser.isChrome) return BrowserType.chrome;
if (browser.isFirefox) return BrowserType.firefox;
+2
View File
@@ -50,6 +50,8 @@ abstract class LocalTrack extends Track {
bool _published = false;
bool get isPublished => _published;
String? codec;
LocalTrack(
String name,
lk_models.TrackType kind,
+7
View File
@@ -92,11 +92,13 @@ class RTCConfiguration {
final int? iceCandidatePoolSize;
final List<RTCIceServer>? iceServers;
final RTCIceTransportPolicy? iceTransportPolicy;
final bool? encodedInsertableStreams;
const RTCConfiguration({
this.iceCandidatePoolSize,
this.iceServers,
this.iceTransportPolicy,
this.encodedInsertableStreams,
});
Map<String, dynamic> toMap() {
@@ -108,6 +110,8 @@ class RTCConfiguration {
return <String, dynamic>{
// only supports unified plan
'sdpSemantics': 'unified-plan',
if (encodedInsertableStreams != null)
'encodedInsertableStreams': encodedInsertableStreams,
if (iceServersMap.isNotEmpty) 'iceServers': iceServersMap,
if (iceCandidatePoolSize != null)
'iceCandidatePoolSize': iceCandidatePoolSize,
@@ -121,11 +125,14 @@ class RTCConfiguration {
int? iceCandidatePoolSize,
List<RTCIceServer>? iceServers,
RTCIceTransportPolicy? iceTransportPolicy,
bool? encodedInsertableStreams,
}) =>
RTCConfiguration(
iceCandidatePoolSize: iceCandidatePoolSize ?? this.iceCandidatePoolSize,
iceServers: iceServers ?? this.iceServers,
iceTransportPolicy: iceTransportPolicy ?? this.iceTransportPolicy,
encodedInsertableStreams:
encodedInsertableStreams ?? this.encodedInsertableStreams,
);
}