e2ee. (#226)
* 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:
@@ -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
@@ -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];
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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})';
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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});
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 [],
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user