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:
@@ -13,4 +13,6 @@ proto:
|
|||||||
format:
|
format:
|
||||||
flutter format --set-exit-if-changed -l 100 .
|
flutter format --set-exit-if-changed -l 100 .
|
||||||
|
|
||||||
|
e2ee: dart compile js .\web\e2ee.worker.dart -o .\example\web\e2ee.worker.dart.js
|
||||||
|
|
||||||
.PHONY: proto format
|
.PHONY: proto format
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ More Docs and guides are available at [https://docs.livekit.io](https://docs.liv
|
|||||||
|
|
||||||
## Current supported features
|
## Current supported features
|
||||||
|
|
||||||
| Feature | Subscribe/Publish | Simulcast | Background audio | Screen sharing |
|
| Feature | Subscribe/Publish | Simulcast | Background audio | Screen sharing | End to End Encryption |
|
||||||
| :-----: | :---------------: | :-------: | :--------------: | :------------: |
|
| :-----: | :---------------: | :-------: | :--------------: | :------------: | :------------: |
|
||||||
| Web | 🟢 | 🟢 | 🟢 | 🟢 |
|
| Web | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 |
|
||||||
| iOS | 🟢 | 🟢 | 🟢 | 🟢 |
|
| iOS | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 |
|
||||||
| Android | 🟢 | 🟢 | 🟢 | 🟢 |
|
| Android | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 |
|
||||||
| Mac | 🟢 | 🟢 | 🟢 | 🟢 |
|
| Mac | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 |
|
||||||
| Windows | 🟢 | 🟢 | 🟢 | 🟢 |
|
| Windows | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 |
|
||||||
|
|
||||||
🟢 = Available
|
🟢 = Available
|
||||||
|
|
||||||
@@ -215,6 +215,21 @@ try {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### End to End Encryption
|
||||||
|
|
||||||
|
LiveKit supports end-to-end encryption for audio/video data sent over the network.
|
||||||
|
By default, the native platform can support E2EE without any settings, but for flutter web, you need to use the following steps to create `e2ee.worker.dart.js` file.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# for example app
|
||||||
|
dart compile js .\web\e2ee.worker.dart -o .\example\web\e2ee.worker.dart.js
|
||||||
|
# for your project
|
||||||
|
export YOU_PROJECT_DIR=your_project_dir
|
||||||
|
git clone https://github.com/livekit/client-sdk-flutter.git
|
||||||
|
cd client-sdk-flutter && flutter pub get
|
||||||
|
dart compile js .\web\e2ee.worker.dart -o ${YOU_PROJECT_DIR}\web\e2ee.worker.dart.js
|
||||||
|
```
|
||||||
|
|
||||||
### Advanced track manipulation
|
### Advanced track manipulation
|
||||||
|
|
||||||
The setCameraEnabled/setMicrophoneEnabled helpers are wrappers around the Track API.
|
The setCameraEnabled/setMicrophoneEnabled helpers are wrappers around the Track API.
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ analyzer:
|
|||||||
- '**/*.pbenum.dart'
|
- '**/*.pbenum.dart'
|
||||||
- '**/*.pbjson.dart'
|
- '**/*.pbjson.dart'
|
||||||
- '**/*.pbserver.dart'
|
- '**/*.pbserver.dart'
|
||||||
|
- 'web/*.dart'
|
||||||
|
|
||||||
linter:
|
linter:
|
||||||
rules:
|
rules:
|
||||||
@@ -39,3 +40,4 @@ linter:
|
|||||||
prefer_single_quotes: true
|
prefer_single_quotes: true
|
||||||
unnecessary_brace_in_string_interps: false
|
unnecessary_brace_in_string_interps: false
|
||||||
unawaited_futures: true
|
unawaited_futures: true
|
||||||
|
depend_on_referenced_packages: false
|
||||||
|
|||||||
@@ -194,4 +194,5 @@ enum SimulateScenarioResult {
|
|||||||
serverLeave,
|
serverLeave,
|
||||||
switchCandidate,
|
switchCandidate,
|
||||||
clear,
|
clear,
|
||||||
|
e2eeKeyRatchet,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,14 +25,18 @@ class _ConnectPageState extends State<ConnectPage> {
|
|||||||
static const _storeKeyAdaptiveStream = 'adaptive-stream';
|
static const _storeKeyAdaptiveStream = 'adaptive-stream';
|
||||||
static const _storeKeyDynacast = 'dynacast';
|
static const _storeKeyDynacast = 'dynacast';
|
||||||
static const _storeKeyFastConnect = 'fast-connect';
|
static const _storeKeyFastConnect = 'fast-connect';
|
||||||
|
static const _storeKeyE2EE = 'e2ee';
|
||||||
|
static const _storeKeySharedKey = 'shared-key';
|
||||||
|
|
||||||
final _uriCtrl = TextEditingController();
|
final _uriCtrl = TextEditingController();
|
||||||
final _tokenCtrl = TextEditingController();
|
final _tokenCtrl = TextEditingController();
|
||||||
|
final _sharedKeyCtrl = TextEditingController();
|
||||||
bool _simulcast = true;
|
bool _simulcast = true;
|
||||||
bool _adaptiveStream = true;
|
bool _adaptiveStream = true;
|
||||||
bool _dynacast = true;
|
bool _dynacast = true;
|
||||||
bool _busy = false;
|
bool _busy = false;
|
||||||
bool _fastConnect = false;
|
bool _fastConnect = false;
|
||||||
|
bool _e2ee = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -56,11 +60,15 @@ class _ConnectPageState extends State<ConnectPage> {
|
|||||||
_tokenCtrl.text = const bool.hasEnvironment('TOKEN')
|
_tokenCtrl.text = const bool.hasEnvironment('TOKEN')
|
||||||
? const String.fromEnvironment('TOKEN')
|
? const String.fromEnvironment('TOKEN')
|
||||||
: prefs.getString(_storeKeyToken) ?? '';
|
: prefs.getString(_storeKeyToken) ?? '';
|
||||||
|
_sharedKeyCtrl.text = const bool.hasEnvironment('E2EEKEY')
|
||||||
|
? const String.fromEnvironment('E2EEKEY')
|
||||||
|
: prefs.getString(_storeKeySharedKey) ?? '';
|
||||||
setState(() {
|
setState(() {
|
||||||
_simulcast = prefs.getBool(_storeKeySimulcast) ?? true;
|
_simulcast = prefs.getBool(_storeKeySimulcast) ?? true;
|
||||||
_adaptiveStream = prefs.getBool(_storeKeyAdaptiveStream) ?? true;
|
_adaptiveStream = prefs.getBool(_storeKeyAdaptiveStream) ?? true;
|
||||||
_dynacast = prefs.getBool(_storeKeyDynacast) ?? true;
|
_dynacast = prefs.getBool(_storeKeyDynacast) ?? true;
|
||||||
_fastConnect = prefs.getBool(_storeKeyFastConnect) ?? false;
|
_fastConnect = prefs.getBool(_storeKeyFastConnect) ?? false;
|
||||||
|
_e2ee = prefs.getBool(_storeKeyE2EE) ?? false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,10 +77,12 @@ class _ConnectPageState extends State<ConnectPage> {
|
|||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setString(_storeKeyUri, _uriCtrl.text);
|
await prefs.setString(_storeKeyUri, _uriCtrl.text);
|
||||||
await prefs.setString(_storeKeyToken, _tokenCtrl.text);
|
await prefs.setString(_storeKeyToken, _tokenCtrl.text);
|
||||||
|
await prefs.setString(_storeKeySharedKey, _sharedKeyCtrl.text);
|
||||||
await prefs.setBool(_storeKeySimulcast, _simulcast);
|
await prefs.setBool(_storeKeySimulcast, _simulcast);
|
||||||
await prefs.setBool(_storeKeyAdaptiveStream, _adaptiveStream);
|
await prefs.setBool(_storeKeyAdaptiveStream, _adaptiveStream);
|
||||||
await prefs.setBool(_storeKeyDynacast, _dynacast);
|
await prefs.setBool(_storeKeyDynacast, _dynacast);
|
||||||
await prefs.setBool(_storeKeyFastConnect, _fastConnect);
|
await prefs.setBool(_storeKeyFastConnect, _fastConnect);
|
||||||
|
await prefs.setBool(_storeKeyE2EE, _e2ee);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _connect(BuildContext ctx) async {
|
Future<void> _connect(BuildContext ctx) async {
|
||||||
@@ -93,6 +103,13 @@ class _ConnectPageState extends State<ConnectPage> {
|
|||||||
|
|
||||||
// Create a Listener before connecting
|
// Create a Listener before connecting
|
||||||
final listener = room.createListener();
|
final listener = room.createListener();
|
||||||
|
E2EEOptions? e2eeOptions;
|
||||||
|
if (_e2ee) {
|
||||||
|
final keyProvider = await BaseKeyProvider.create();
|
||||||
|
e2eeOptions = E2EEOptions(keyProvider: keyProvider);
|
||||||
|
var sharedKey = _sharedKeyCtrl.text;
|
||||||
|
await keyProvider.setKey(sharedKey);
|
||||||
|
}
|
||||||
|
|
||||||
// Try to connect to the room
|
// Try to connect to the room
|
||||||
// This will throw an Exception if it fails for any reason.
|
// This will throw an Exception if it fails for any reason.
|
||||||
@@ -107,6 +124,7 @@ class _ConnectPageState extends State<ConnectPage> {
|
|||||||
),
|
),
|
||||||
defaultScreenShareCaptureOptions:
|
defaultScreenShareCaptureOptions:
|
||||||
const ScreenShareCaptureOptions(useiOSBroadcastExtension: true),
|
const ScreenShareCaptureOptions(useiOSBroadcastExtension: true),
|
||||||
|
e2eeOptions: e2eeOptions,
|
||||||
),
|
),
|
||||||
fastConnectOptions: _fastConnect
|
fastConnectOptions: _fastConnect
|
||||||
? FastConnectOptions(
|
? FastConnectOptions(
|
||||||
@@ -115,6 +133,7 @@ class _ConnectPageState extends State<ConnectPage> {
|
|||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
await Navigator.push<void>(
|
await Navigator.push<void>(
|
||||||
ctx,
|
ctx,
|
||||||
MaterialPageRoute(builder: (_) => RoomPage(room, listener)),
|
MaterialPageRoute(builder: (_) => RoomPage(room, listener)),
|
||||||
@@ -136,6 +155,13 @@ class _ConnectPageState extends State<ConnectPage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _setE2EE(bool? value) async {
|
||||||
|
if (value == null || _e2ee == value) return;
|
||||||
|
setState(() {
|
||||||
|
_e2ee = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void _setAdaptiveStream(bool? value) async {
|
void _setAdaptiveStream(bool? value) async {
|
||||||
if (value == null || _adaptiveStream == value) return;
|
if (value == null || _adaptiveStream == value) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -192,6 +218,26 @@ class _ConnectPageState extends State<ConnectPage> {
|
|||||||
ctrl: _tokenCtrl,
|
ctrl: _tokenCtrl,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 25),
|
||||||
|
child: LKTextField(
|
||||||
|
label: 'Shared Key',
|
||||||
|
ctrl: _sharedKeyCtrl,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 5),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const Text('E2EE'),
|
||||||
|
Switch(
|
||||||
|
value: _e2ee,
|
||||||
|
onChanged: (value) => _setE2EE(value),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 5),
|
padding: const EdgeInsets.only(bottom: 5),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ class _RoomPageState extends State<RoomPage> {
|
|||||||
})
|
})
|
||||||
..on<LocalTrackPublishedEvent>((_) => _sortParticipants())
|
..on<LocalTrackPublishedEvent>((_) => _sortParticipants())
|
||||||
..on<LocalTrackUnpublishedEvent>((_) => _sortParticipants())
|
..on<LocalTrackUnpublishedEvent>((_) => _sortParticipants())
|
||||||
|
..on<TrackE2EEStateEvent>(_onE2EEStateEvent)
|
||||||
..on<ParticipantNameUpdatedEvent>((event) {
|
..on<ParticipantNameUpdatedEvent>((event) {
|
||||||
print(
|
print(
|
||||||
'Participant name updated: ${event.participant.identity}, name => ${event.name}');
|
'Participant name updated: ${event.participant.identity}, name => ${event.name}');
|
||||||
@@ -102,6 +103,10 @@ class _RoomPageState extends State<RoomPage> {
|
|||||||
_sortParticipants();
|
_sortParticipants();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onE2EEStateEvent(TrackE2EEStateEvent e2eeState) {
|
||||||
|
print('e2ee state: $e2eeState');
|
||||||
|
}
|
||||||
|
|
||||||
void _sortParticipants() {
|
void _sortParticipants() {
|
||||||
List<ParticipantTrack> userMediaTracks = [];
|
List<ParticipantTrack> userMediaTracks = [];
|
||||||
List<ParticipantTrack> screenTracks = [];
|
List<ParticipantTrack> screenTracks = [];
|
||||||
|
|||||||
@@ -226,6 +226,11 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
|||||||
final result = await context.showSimulateScenarioDialog();
|
final result = await context.showSimulateScenarioDialog();
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
print('${result}');
|
print('${result}');
|
||||||
|
|
||||||
|
if (SimulateScenarioResult.e2eeKeyRatchet == result) {
|
||||||
|
await widget.room.e2eeManager?.ratchetKey();
|
||||||
|
}
|
||||||
|
|
||||||
await widget.room.sendSimulateScenario(
|
await widget.room.sendSimulateScenario(
|
||||||
signalReconnect:
|
signalReconnect:
|
||||||
result == SimulateScenarioResult.signalReconnect ? true : null,
|
result == SimulateScenarioResult.signalReconnect ? true : null,
|
||||||
|
|||||||
@@ -153,6 +153,8 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget>
|
|||||||
firstAudioPublication?.subscribed == true,
|
firstAudioPublication?.subscribed == true,
|
||||||
connectionQuality: widget.participant.connectionQuality,
|
connectionQuality: widget.participant.connectionQuality,
|
||||||
isScreenShare: widget.isScreenShare,
|
isScreenShare: widget.isScreenShare,
|
||||||
|
enabledE2EE: widget.participant.firstTrackEncryptionType !=
|
||||||
|
EncryptionType.kNone,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -18,12 +18,14 @@ class ParticipantInfoWidget extends StatelessWidget {
|
|||||||
final bool audioAvailable;
|
final bool audioAvailable;
|
||||||
final ConnectionQuality connectionQuality;
|
final ConnectionQuality connectionQuality;
|
||||||
final bool isScreenShare;
|
final bool isScreenShare;
|
||||||
|
final bool enabledE2EE;
|
||||||
|
|
||||||
const ParticipantInfoWidget({
|
const ParticipantInfoWidget({
|
||||||
this.title,
|
this.title,
|
||||||
this.audioAvailable = true,
|
this.audioAvailable = true,
|
||||||
this.connectionQuality = ConnectionQuality.unknown,
|
this.connectionQuality = ConnectionQuality.unknown,
|
||||||
this.isScreenShare = false,
|
this.isScreenShare = false,
|
||||||
|
this.enabledE2EE = false,
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@@ -77,6 +79,14 @@ class ParticipantInfoWidget extends StatelessWidget {
|
|||||||
size: 16,
|
size: 16,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 5),
|
||||||
|
child: Icon(
|
||||||
|
enabledE2EE ? EvaIcons.lock : EvaIcons.unlock,
|
||||||
|
color: enabledE2EE ? Colors.green : Colors.red,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,232 @@
|
|||||||
|
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/collection.dart
|
||||||
|
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/algorithms.dart
|
||||||
|
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/boollist.dart
|
||||||
|
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/canonicalized_map.dart
|
||||||
|
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/combined_wrappers/combined_iterable.dart
|
||||||
|
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/combined_wrappers/combined_iterator.dart
|
||||||
|
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/combined_wrappers/combined_list.dart
|
||||||
|
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/combined_wrappers/combined_map.dart
|
||||||
|
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/comparators.dart
|
||||||
|
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/empty_unmodifiable_set.dart
|
||||||
|
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/equality.dart
|
||||||
|
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/equality_map.dart
|
||||||
|
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/equality_set.dart
|
||||||
|
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/functions.dart
|
||||||
|
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/iterable_extensions.dart
|
||||||
|
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/iterable_zip.dart
|
||||||
|
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/list_extensions.dart
|
||||||
|
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/priority_queue.dart
|
||||||
|
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/queue_list.dart
|
||||||
|
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/union_set.dart
|
||||||
|
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/union_set_controller.dart
|
||||||
|
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/unmodifiable_wrappers.dart
|
||||||
|
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/utils.dart
|
||||||
|
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/collection-1.17.0/lib/src/wrappers.dart
|
||||||
|
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/js-0.6.5/lib/js.dart
|
||||||
|
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/meta-1.8.0/lib/meta.dart
|
||||||
|
file:///Users/weiweiduan/.pub-cache/hosted/pub.flutter-io.cn/meta-1.8.0/lib/meta_meta.dart
|
||||||
|
file:///Users/weiweiduan/Desktop/projects/flutter-webrtc/lib/src/web/rtc_transform_stream.dart
|
||||||
|
file:///Users/weiweiduan/Desktop/projects/livekit/client-sdk-flutter/.dart_tool/package_config.json
|
||||||
|
file:///Users/weiweiduan/Desktop/projects/livekit/client-sdk-flutter/web/crypto.dart
|
||||||
|
file:///Users/weiweiduan/Desktop/projects/livekit/client-sdk-flutter/web/e2ee.cryptor.dart
|
||||||
|
file:///Users/weiweiduan/Desktop/projects/livekit/client-sdk-flutter/web/e2ee.utils.dart
|
||||||
|
file:///Users/weiweiduan/Desktop/projects/livekit/client-sdk-flutter/web/e2ee.worker.dart
|
||||||
|
file:///Users/weiweiduan/bin/flutter/bin/cache/dart-sdk/lib/_internal/dart2js_platform.dill
|
||||||
|
file:///Users/weiweiduan/bin/flutter/bin/cache/dart-sdk/lib/libraries.json
|
||||||
|
org-dartlang-sdk:///lib/_http/crypto.dart
|
||||||
|
org-dartlang-sdk:///lib/_http/embedder_config.dart
|
||||||
|
org-dartlang-sdk:///lib/_http/http.dart
|
||||||
|
org-dartlang-sdk:///lib/_http/http_date.dart
|
||||||
|
org-dartlang-sdk:///lib/_http/http_headers.dart
|
||||||
|
org-dartlang-sdk:///lib/_http/http_impl.dart
|
||||||
|
org-dartlang-sdk:///lib/_http/http_parser.dart
|
||||||
|
org-dartlang-sdk:///lib/_http/http_session.dart
|
||||||
|
org-dartlang-sdk:///lib/_http/http_testing.dart
|
||||||
|
org-dartlang-sdk:///lib/_http/overrides.dart
|
||||||
|
org-dartlang-sdk:///lib/_http/websocket.dart
|
||||||
|
org-dartlang-sdk:///lib/_http/websocket_impl.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_runtime/lib/annotations.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_runtime/lib/async_patch.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_runtime/lib/collection_patch.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_runtime/lib/constant_map.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_runtime/lib/convert_patch.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_runtime/lib/core_patch.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_runtime/lib/dart2js_runtime_metrics.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_runtime/lib/developer_patch.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_runtime/lib/foreign_helper.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_runtime/lib/instantiation.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_runtime/lib/interceptors.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_runtime/lib/internal_patch.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_runtime/lib/io_patch.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_runtime/lib/isolate_patch.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_runtime/lib/js_array.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_runtime/lib/js_helper.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_runtime/lib/js_names.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_runtime/lib/js_number.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_runtime/lib/js_patch.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_runtime/lib/js_primitives.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_runtime/lib/js_string.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_runtime/lib/late_helper.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_runtime/lib/linked_hash_map.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_runtime/lib/math_patch.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_runtime/lib/native_helper.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_runtime/lib/native_typed_data.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_runtime/lib/regexp_helper.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_runtime/lib/string_helper.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_runtime/lib/synced/async_await_error_codes.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_runtime/lib/synced/embedded_names.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_runtime/lib/synced/load_library_priority.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_runtime/lib/typed_data_patch.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_shared/lib/js_util_patch.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_shared/lib/rti.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_shared/lib/synced/embedded_names.dart
|
||||||
|
org-dartlang-sdk:///lib/_internal/js_shared/lib/synced/recipe_syntax.dart
|
||||||
|
org-dartlang-sdk:///lib/async/async.dart
|
||||||
|
org-dartlang-sdk:///lib/async/async_error.dart
|
||||||
|
org-dartlang-sdk:///lib/async/broadcast_stream_controller.dart
|
||||||
|
org-dartlang-sdk:///lib/async/deferred_load.dart
|
||||||
|
org-dartlang-sdk:///lib/async/future.dart
|
||||||
|
org-dartlang-sdk:///lib/async/future_impl.dart
|
||||||
|
org-dartlang-sdk:///lib/async/schedule_microtask.dart
|
||||||
|
org-dartlang-sdk:///lib/async/stream.dart
|
||||||
|
org-dartlang-sdk:///lib/async/stream_controller.dart
|
||||||
|
org-dartlang-sdk:///lib/async/stream_impl.dart
|
||||||
|
org-dartlang-sdk:///lib/async/stream_pipe.dart
|
||||||
|
org-dartlang-sdk:///lib/async/stream_transformers.dart
|
||||||
|
org-dartlang-sdk:///lib/async/timer.dart
|
||||||
|
org-dartlang-sdk:///lib/async/zone.dart
|
||||||
|
org-dartlang-sdk:///lib/collection/collection.dart
|
||||||
|
org-dartlang-sdk:///lib/collection/collections.dart
|
||||||
|
org-dartlang-sdk:///lib/collection/hash_map.dart
|
||||||
|
org-dartlang-sdk:///lib/collection/hash_set.dart
|
||||||
|
org-dartlang-sdk:///lib/collection/iterable.dart
|
||||||
|
org-dartlang-sdk:///lib/collection/iterator.dart
|
||||||
|
org-dartlang-sdk:///lib/collection/linked_hash_map.dart
|
||||||
|
org-dartlang-sdk:///lib/collection/linked_hash_set.dart
|
||||||
|
org-dartlang-sdk:///lib/collection/linked_list.dart
|
||||||
|
org-dartlang-sdk:///lib/collection/list.dart
|
||||||
|
org-dartlang-sdk:///lib/collection/maps.dart
|
||||||
|
org-dartlang-sdk:///lib/collection/queue.dart
|
||||||
|
org-dartlang-sdk:///lib/collection/set.dart
|
||||||
|
org-dartlang-sdk:///lib/collection/splay_tree.dart
|
||||||
|
org-dartlang-sdk:///lib/convert/ascii.dart
|
||||||
|
org-dartlang-sdk:///lib/convert/base64.dart
|
||||||
|
org-dartlang-sdk:///lib/convert/byte_conversion.dart
|
||||||
|
org-dartlang-sdk:///lib/convert/chunked_conversion.dart
|
||||||
|
org-dartlang-sdk:///lib/convert/codec.dart
|
||||||
|
org-dartlang-sdk:///lib/convert/convert.dart
|
||||||
|
org-dartlang-sdk:///lib/convert/converter.dart
|
||||||
|
org-dartlang-sdk:///lib/convert/encoding.dart
|
||||||
|
org-dartlang-sdk:///lib/convert/html_escape.dart
|
||||||
|
org-dartlang-sdk:///lib/convert/json.dart
|
||||||
|
org-dartlang-sdk:///lib/convert/latin1.dart
|
||||||
|
org-dartlang-sdk:///lib/convert/line_splitter.dart
|
||||||
|
org-dartlang-sdk:///lib/convert/string_conversion.dart
|
||||||
|
org-dartlang-sdk:///lib/convert/utf.dart
|
||||||
|
org-dartlang-sdk:///lib/core/annotations.dart
|
||||||
|
org-dartlang-sdk:///lib/core/bigint.dart
|
||||||
|
org-dartlang-sdk:///lib/core/bool.dart
|
||||||
|
org-dartlang-sdk:///lib/core/comparable.dart
|
||||||
|
org-dartlang-sdk:///lib/core/core.dart
|
||||||
|
org-dartlang-sdk:///lib/core/date_time.dart
|
||||||
|
org-dartlang-sdk:///lib/core/double.dart
|
||||||
|
org-dartlang-sdk:///lib/core/duration.dart
|
||||||
|
org-dartlang-sdk:///lib/core/enum.dart
|
||||||
|
org-dartlang-sdk:///lib/core/errors.dart
|
||||||
|
org-dartlang-sdk:///lib/core/exceptions.dart
|
||||||
|
org-dartlang-sdk:///lib/core/function.dart
|
||||||
|
org-dartlang-sdk:///lib/core/int.dart
|
||||||
|
org-dartlang-sdk:///lib/core/invocation.dart
|
||||||
|
org-dartlang-sdk:///lib/core/iterable.dart
|
||||||
|
org-dartlang-sdk:///lib/core/iterator.dart
|
||||||
|
org-dartlang-sdk:///lib/core/list.dart
|
||||||
|
org-dartlang-sdk:///lib/core/map.dart
|
||||||
|
org-dartlang-sdk:///lib/core/null.dart
|
||||||
|
org-dartlang-sdk:///lib/core/num.dart
|
||||||
|
org-dartlang-sdk:///lib/core/object.dart
|
||||||
|
org-dartlang-sdk:///lib/core/pattern.dart
|
||||||
|
org-dartlang-sdk:///lib/core/print.dart
|
||||||
|
org-dartlang-sdk:///lib/core/record.dart
|
||||||
|
org-dartlang-sdk:///lib/core/regexp.dart
|
||||||
|
org-dartlang-sdk:///lib/core/set.dart
|
||||||
|
org-dartlang-sdk:///lib/core/sink.dart
|
||||||
|
org-dartlang-sdk:///lib/core/stacktrace.dart
|
||||||
|
org-dartlang-sdk:///lib/core/stopwatch.dart
|
||||||
|
org-dartlang-sdk:///lib/core/string.dart
|
||||||
|
org-dartlang-sdk:///lib/core/string_buffer.dart
|
||||||
|
org-dartlang-sdk:///lib/core/string_sink.dart
|
||||||
|
org-dartlang-sdk:///lib/core/symbol.dart
|
||||||
|
org-dartlang-sdk:///lib/core/type.dart
|
||||||
|
org-dartlang-sdk:///lib/core/uri.dart
|
||||||
|
org-dartlang-sdk:///lib/core/weak.dart
|
||||||
|
org-dartlang-sdk:///lib/developer/developer.dart
|
||||||
|
org-dartlang-sdk:///lib/developer/extension.dart
|
||||||
|
org-dartlang-sdk:///lib/developer/profiler.dart
|
||||||
|
org-dartlang-sdk:///lib/developer/service.dart
|
||||||
|
org-dartlang-sdk:///lib/developer/timeline.dart
|
||||||
|
org-dartlang-sdk:///lib/html/dart2js/html_dart2js.dart
|
||||||
|
org-dartlang-sdk:///lib/html/html_common/conversions.dart
|
||||||
|
org-dartlang-sdk:///lib/html/html_common/conversions_dart2js.dart
|
||||||
|
org-dartlang-sdk:///lib/html/html_common/css_class_set.dart
|
||||||
|
org-dartlang-sdk:///lib/html/html_common/device.dart
|
||||||
|
org-dartlang-sdk:///lib/html/html_common/filtered_element_list.dart
|
||||||
|
org-dartlang-sdk:///lib/html/html_common/html_common_dart2js.dart
|
||||||
|
org-dartlang-sdk:///lib/html/html_common/lists.dart
|
||||||
|
org-dartlang-sdk:///lib/html/html_common/metadata.dart
|
||||||
|
org-dartlang-sdk:///lib/indexed_db/dart2js/indexed_db_dart2js.dart
|
||||||
|
org-dartlang-sdk:///lib/internal/async_cast.dart
|
||||||
|
org-dartlang-sdk:///lib/internal/bytes_builder.dart
|
||||||
|
org-dartlang-sdk:///lib/internal/cast.dart
|
||||||
|
org-dartlang-sdk:///lib/internal/errors.dart
|
||||||
|
org-dartlang-sdk:///lib/internal/internal.dart
|
||||||
|
org-dartlang-sdk:///lib/internal/iterable.dart
|
||||||
|
org-dartlang-sdk:///lib/internal/linked_list.dart
|
||||||
|
org-dartlang-sdk:///lib/internal/list.dart
|
||||||
|
org-dartlang-sdk:///lib/internal/patch.dart
|
||||||
|
org-dartlang-sdk:///lib/internal/print.dart
|
||||||
|
org-dartlang-sdk:///lib/internal/sort.dart
|
||||||
|
org-dartlang-sdk:///lib/internal/symbol.dart
|
||||||
|
org-dartlang-sdk:///lib/io/common.dart
|
||||||
|
org-dartlang-sdk:///lib/io/data_transformer.dart
|
||||||
|
org-dartlang-sdk:///lib/io/directory.dart
|
||||||
|
org-dartlang-sdk:///lib/io/directory_impl.dart
|
||||||
|
org-dartlang-sdk:///lib/io/embedder_config.dart
|
||||||
|
org-dartlang-sdk:///lib/io/eventhandler.dart
|
||||||
|
org-dartlang-sdk:///lib/io/file.dart
|
||||||
|
org-dartlang-sdk:///lib/io/file_impl.dart
|
||||||
|
org-dartlang-sdk:///lib/io/file_system_entity.dart
|
||||||
|
org-dartlang-sdk:///lib/io/io.dart
|
||||||
|
org-dartlang-sdk:///lib/io/io_resource_info.dart
|
||||||
|
org-dartlang-sdk:///lib/io/io_service.dart
|
||||||
|
org-dartlang-sdk:///lib/io/io_sink.dart
|
||||||
|
org-dartlang-sdk:///lib/io/link.dart
|
||||||
|
org-dartlang-sdk:///lib/io/namespace_impl.dart
|
||||||
|
org-dartlang-sdk:///lib/io/network_profiling.dart
|
||||||
|
org-dartlang-sdk:///lib/io/overrides.dart
|
||||||
|
org-dartlang-sdk:///lib/io/platform.dart
|
||||||
|
org-dartlang-sdk:///lib/io/platform_impl.dart
|
||||||
|
org-dartlang-sdk:///lib/io/process.dart
|
||||||
|
org-dartlang-sdk:///lib/io/secure_server_socket.dart
|
||||||
|
org-dartlang-sdk:///lib/io/secure_socket.dart
|
||||||
|
org-dartlang-sdk:///lib/io/security_context.dart
|
||||||
|
org-dartlang-sdk:///lib/io/service_object.dart
|
||||||
|
org-dartlang-sdk:///lib/io/socket.dart
|
||||||
|
org-dartlang-sdk:///lib/io/stdio.dart
|
||||||
|
org-dartlang-sdk:///lib/io/string_transformer.dart
|
||||||
|
org-dartlang-sdk:///lib/io/sync_socket.dart
|
||||||
|
org-dartlang-sdk:///lib/isolate/capability.dart
|
||||||
|
org-dartlang-sdk:///lib/isolate/isolate.dart
|
||||||
|
org-dartlang-sdk:///lib/js/_js.dart
|
||||||
|
org-dartlang-sdk:///lib/js/_js_annotations.dart
|
||||||
|
org-dartlang-sdk:///lib/js/_js_client.dart
|
||||||
|
org-dartlang-sdk:///lib/js/js.dart
|
||||||
|
org-dartlang-sdk:///lib/js_util/js_util.dart
|
||||||
|
org-dartlang-sdk:///lib/math/math.dart
|
||||||
|
org-dartlang-sdk:///lib/math/point.dart
|
||||||
|
org-dartlang-sdk:///lib/math/random.dart
|
||||||
|
org-dartlang-sdk:///lib/math/rectangle.dart
|
||||||
|
org-dartlang-sdk:///lib/svg/dart2js/svg_dart2js.dart
|
||||||
|
org-dartlang-sdk:///lib/typed_data/typed_data.dart
|
||||||
|
org-dartlang-sdk:///lib/typed_data/unmodifiable_typed_data.dart
|
||||||
|
org-dartlang-sdk:///lib/web_audio/dart2js/web_audio_dart2js.dart
|
||||||
|
org-dartlang-sdk:///lib/web_gl/dart2js/web_gl_dart2js.dart
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -1,6 +1,6 @@
|
|||||||
Pod::Spec.new do |s|
|
Pod::Spec.new do |s|
|
||||||
s.name = 'livekit_client'
|
s.name = 'livekit_client'
|
||||||
s.version = '1.1.13'
|
s.version = '1.2.2'
|
||||||
s.summary = 'Open source platform for real-time audio and video.'
|
s.summary = 'Open source platform for real-time audio and video.'
|
||||||
s.description = 'Open source platform for real-time audio and video.'
|
s.description = 'Open source platform for real-time audio and video.'
|
||||||
s.homepage = 'https://livekit.io/'
|
s.homepage = 'https://livekit.io/'
|
||||||
@@ -16,5 +16,5 @@ Pod::Spec.new do |s|
|
|||||||
s.static_framework = true
|
s.static_framework = true
|
||||||
|
|
||||||
s.dependency 'Flutter'
|
s.dependency 'Flutter'
|
||||||
s.dependency 'WebRTC-SDK', '104.5112.09'
|
s.dependency 'WebRTC-SDK', '104.5112.16'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ library livekit_client;
|
|||||||
export 'src/core/room.dart';
|
export 'src/core/room.dart';
|
||||||
export 'src/events.dart';
|
export 'src/events.dart';
|
||||||
export 'src/exceptions.dart';
|
export 'src/exceptions.dart';
|
||||||
|
export 'src/e2ee/e2ee_manager.dart';
|
||||||
|
export 'src/e2ee/events.dart';
|
||||||
|
export 'src/e2ee/options.dart';
|
||||||
|
export 'src/e2ee/key_provider.dart';
|
||||||
export 'src/extensions.dart' show WidgetsBindingCompatible;
|
export 'src/extensions.dart' show WidgetsBindingCompatible;
|
||||||
export 'src/hardware/hardware.dart';
|
export 'src/hardware/hardware.dart';
|
||||||
export 'src/livekit.dart';
|
export 'src/livekit.dart';
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:collection/collection.dart';
|
|||||||
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
import '../e2ee/options.dart';
|
||||||
import '../events.dart';
|
import '../events.dart';
|
||||||
import '../exceptions.dart';
|
import '../exceptions.dart';
|
||||||
import '../extensions.dart';
|
import '../extensions.dart';
|
||||||
@@ -188,6 +189,21 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
|
|||||||
}) async {
|
}) async {
|
||||||
// TODO: Check if cid already published
|
// 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
|
// send request to add track
|
||||||
signalClient.sendAddTrack(
|
signalClient.sendAddTrack(
|
||||||
cid: cid,
|
cid: cid,
|
||||||
@@ -197,6 +213,7 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
|
|||||||
dimensions: dimensions,
|
dimensions: dimensions,
|
||||||
dtx: dtx,
|
dtx: dtx,
|
||||||
videoLayers: videoLayers,
|
videoLayers: videoLayers,
|
||||||
|
encryptionType: encryptionType,
|
||||||
);
|
);
|
||||||
|
|
||||||
// wait for response, or timeout
|
// 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;
|
return rtcConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+23
-2
@@ -7,7 +7,9 @@ import 'package:livekit_client/src/support/app_state.dart';
|
|||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
import '../core/signal_client.dart';
|
import '../core/signal_client.dart';
|
||||||
|
import '../e2ee/e2ee_manager.dart';
|
||||||
import '../events.dart';
|
import '../events.dart';
|
||||||
|
import '../exceptions.dart';
|
||||||
import '../extensions.dart';
|
import '../extensions.dart';
|
||||||
import '../internal/events.dart';
|
import '../internal/events.dart';
|
||||||
import '../logger.dart';
|
import '../logger.dart';
|
||||||
@@ -71,6 +73,9 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
|
|||||||
String? get serverRegion => _serverRegion;
|
String? get serverRegion => _serverRegion;
|
||||||
String? _serverRegion;
|
String? _serverRegion;
|
||||||
|
|
||||||
|
E2EEManager? get e2eeManager => _e2eeManager;
|
||||||
|
|
||||||
|
E2EEManager? _e2eeManager;
|
||||||
bool get isRecording => _isRecording;
|
bool get isRecording => _isRecording;
|
||||||
bool _isRecording = false;
|
bool _isRecording = false;
|
||||||
|
|
||||||
@@ -140,14 +145,22 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
|
|||||||
ConnectOptions? connectOptions,
|
ConnectOptions? connectOptions,
|
||||||
RoomOptions? roomOptions,
|
RoomOptions? roomOptions,
|
||||||
FastConnectOptions? fastConnectOptions,
|
FastConnectOptions? fastConnectOptions,
|
||||||
}) =>
|
}) {
|
||||||
engine.connect(
|
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,
|
url,
|
||||||
token,
|
token,
|
||||||
connectOptions: connectOptions,
|
connectOptions: connectOptions,
|
||||||
roomOptions: roomOptions,
|
roomOptions: roomOptions,
|
||||||
fastConnectOptions: fastConnectOptions,
|
fastConnectOptions: fastConnectOptions,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _setUpSignalListeners() => _signalListener
|
void _setUpSignalListeners() => _signalListener
|
||||||
..on<SignalJoinResponseEvent>((event) {
|
..on<SignalJoinResponseEvent>((event) {
|
||||||
@@ -378,6 +391,14 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
|
|||||||
await _cleanUp();
|
await _cleanUp();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setE2EEEnabled(bool enabled) async {
|
||||||
|
if (_e2eeManager != null) {
|
||||||
|
await _e2eeManager!.setEnabled(enabled);
|
||||||
|
} else {
|
||||||
|
throw LiveKitE2EEException('_e2eeManager not setup!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
RemoteParticipant _getOrCreateRemoteParticipant(
|
RemoteParticipant _getOrCreateRemoteParticipant(
|
||||||
String sid, lk_models.ParticipantInfo? info) {
|
String sid, lk_models.ParticipantInfo? info) {
|
||||||
RemoteParticipant? participant = _participants[sid];
|
RemoteParticipant? participant = _participants[sid];
|
||||||
|
|||||||
@@ -350,6 +350,7 @@ extension SignalClientRequests on SignalClient {
|
|||||||
required String name,
|
required String name,
|
||||||
required lk_models.TrackType type,
|
required lk_models.TrackType type,
|
||||||
required lk_models.TrackSource source,
|
required lk_models.TrackSource source,
|
||||||
|
required lk_models.Encryption_Type encryptionType,
|
||||||
VideoDimensions? dimensions,
|
VideoDimensions? dimensions,
|
||||||
bool? dtx,
|
bool? dtx,
|
||||||
Iterable<lk_models.VideoLayer>? videoLayers,
|
Iterable<lk_models.VideoLayer>? videoLayers,
|
||||||
@@ -359,6 +360,7 @@ extension SignalClientRequests on SignalClient {
|
|||||||
name: name,
|
name: name,
|
||||||
type: type,
|
type: type,
|
||||||
source: source,
|
source: source,
|
||||||
|
encryption: encryptionType,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (type == lk_models.TrackType.VIDEO) {
|
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 {
|
class TimeoutException extends LiveKitException {
|
||||||
TimeoutException([String msg = 'Timeout']) : super._(msg);
|
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/material.dart';
|
||||||
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
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_models.pb.dart' as lk_models;
|
||||||
import 'proto/livekit_rtc.pb.dart' as lk_rtc;
|
import 'proto/livekit_rtc.pb.dart' as lk_rtc;
|
||||||
import 'types/other.dart';
|
|
||||||
|
|
||||||
extension DataPacketKindExt on lk_models.DataPacket_Kind {
|
extension DataPacketKindExt on lk_models.DataPacket_Kind {
|
||||||
Reliability toSDKType() => {
|
Reliability toSDKType() => {
|
||||||
@@ -172,6 +170,14 @@ extension WidgetsBindingCompatible on WidgetsBinding {
|
|||||||
static WidgetsBinding? get instance => WidgetsBinding.instance;
|
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 {
|
extension DisconnectReasonExt on lk_models.DisconnectReason {
|
||||||
DisconnectReason toSDKType() => {
|
DisconnectReason toSDKType() => {
|
||||||
lk_models.DisconnectReason.UNKNOWN_REASON: DisconnectReason.unknown,
|
lk_models.DisconnectReason.UNKNOWN_REASON: DisconnectReason.unknown,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'constants.dart';
|
import 'constants.dart';
|
||||||
import 'core/room.dart';
|
import 'core/room.dart';
|
||||||
|
import 'e2ee/options.dart';
|
||||||
import 'publication/remote.dart';
|
import 'publication/remote.dart';
|
||||||
import 'track/local/audio.dart';
|
import 'track/local/audio.dart';
|
||||||
import 'track/local/video.dart';
|
import 'track/local/video.dart';
|
||||||
@@ -88,6 +89,9 @@ class RoomOptions {
|
|||||||
/// Defaults to true.
|
/// Defaults to true.
|
||||||
final bool stopLocalTrackOnUnpublish;
|
final bool stopLocalTrackOnUnpublish;
|
||||||
|
|
||||||
|
/// Options for end-to-end encryption.
|
||||||
|
final E2EEOptions? e2eeOptions;
|
||||||
|
|
||||||
const RoomOptions({
|
const RoomOptions({
|
||||||
this.defaultCameraCaptureOptions = const CameraCaptureOptions(),
|
this.defaultCameraCaptureOptions = const CameraCaptureOptions(),
|
||||||
this.defaultScreenShareCaptureOptions = const ScreenShareCaptureOptions(),
|
this.defaultScreenShareCaptureOptions = const ScreenShareCaptureOptions(),
|
||||||
@@ -98,6 +102,7 @@ class RoomOptions {
|
|||||||
this.adaptiveStream = false,
|
this.adaptiveStream = false,
|
||||||
this.dynacast = false,
|
this.dynacast = false,
|
||||||
this.stopLocalTrackOnUnpublish = true,
|
this.stopLocalTrackOnUnpublish = true,
|
||||||
|
this.e2eeOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
RoomOptions copyWith({
|
RoomOptions copyWith({
|
||||||
@@ -134,6 +139,9 @@ class RoomOptions {
|
|||||||
|
|
||||||
/// Options used when publishing video.
|
/// Options used when publishing video.
|
||||||
class VideoPublishOptions {
|
class VideoPublishOptions {
|
||||||
|
/// The video codec to use.
|
||||||
|
final String videoCodec;
|
||||||
|
|
||||||
/// If provided, this will be used instead of the SDK's suggested encodings.
|
/// If provided, this will be used instead of the SDK's suggested encodings.
|
||||||
/// Usually you don't need to provide this.
|
/// Usually you don't need to provide this.
|
||||||
/// Defaults to null.
|
/// Defaults to null.
|
||||||
@@ -149,6 +157,7 @@ class VideoPublishOptions {
|
|||||||
final List<VideoParameters> screenShareSimulcastLayers;
|
final List<VideoParameters> screenShareSimulcastLayers;
|
||||||
|
|
||||||
const VideoPublishOptions({
|
const VideoPublishOptions({
|
||||||
|
this.videoCodec = 'H264',
|
||||||
this.videoEncoding,
|
this.videoEncoding,
|
||||||
this.simulcast = true,
|
this.simulcast = true,
|
||||||
this.videoSimulcastLayers = const [],
|
this.videoSimulcastLayers = const [],
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import '../options.dart';
|
|||||||
import '../proto/livekit_models.pb.dart' as lk_models;
|
import '../proto/livekit_models.pb.dart' as lk_models;
|
||||||
import '../proto/livekit_rtc.pb.dart' as lk_rtc;
|
import '../proto/livekit_rtc.pb.dart' as lk_rtc;
|
||||||
import '../publication/local.dart';
|
import '../publication/local.dart';
|
||||||
|
import '../support/platform.dart';
|
||||||
import '../track/local/audio.dart';
|
import '../track/local/audio.dart';
|
||||||
import '../track/local/local.dart';
|
import '../track/local/local.dart';
|
||||||
import '../track/local/video.dart';
|
import '../track/local/video.dart';
|
||||||
@@ -182,6 +183,40 @@ class LocalParticipant extends Participant<LocalTrackPublication> {
|
|||||||
init: transceiverInit,
|
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
|
// prefer to maintainResolution for screen share
|
||||||
if (track.source == TrackSource.screenShareVideo) {
|
if (track.source == TrackSource.screenShareVideo) {
|
||||||
var sender = track.transceiver!.sender;
|
var sender = track.transceiver!.sender;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:collection/collection.dart';
|
|||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
import '../core/room.dart';
|
import '../core/room.dart';
|
||||||
|
import '../e2ee/options.dart';
|
||||||
import '../events.dart';
|
import '../events.dart';
|
||||||
import '../extensions.dart';
|
import '../extensions.dart';
|
||||||
import '../logger.dart';
|
import '../logger.dart';
|
||||||
@@ -9,12 +10,9 @@ import '../managers/event.dart';
|
|||||||
import '../proto/livekit_models.pb.dart' as lk_models;
|
import '../proto/livekit_models.pb.dart' as lk_models;
|
||||||
import '../publication/track_publication.dart';
|
import '../publication/track_publication.dart';
|
||||||
import '../support/disposable.dart';
|
import '../support/disposable.dart';
|
||||||
import '../track/local/local.dart';
|
|
||||||
import '../track/track.dart';
|
import '../track/track.dart';
|
||||||
import '../types/other.dart';
|
import '../types/other.dart';
|
||||||
import '../types/participant_permissions.dart';
|
import '../types/participant_permissions.dart';
|
||||||
import 'local.dart';
|
|
||||||
import 'remote.dart';
|
|
||||||
|
|
||||||
/// Represents a Participant in the room, notifies changes via delegates as
|
/// Represents a Participant in the room, notifies changes via delegates as
|
||||||
/// well as ChangeNotifier/providers.
|
/// well as ChangeNotifier/providers.
|
||||||
@@ -94,6 +92,16 @@ abstract class Participant<T extends TrackPublication>
|
|||||||
// Must be implemented by child class.
|
// Must be implemented by child class.
|
||||||
List<T> get audioTracks;
|
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
|
@internal
|
||||||
bool get hasInfo => _participantInfo != null;
|
bool get hasInfo => _participantInfo != null;
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
|
import 'package:livekit_client/livekit_client.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
import '../core/signal_client.dart';
|
import '../core/signal_client.dart';
|
||||||
import '../events.dart';
|
|
||||||
import '../extensions.dart';
|
import '../extensions.dart';
|
||||||
import '../internal/events.dart';
|
import '../internal/events.dart';
|
||||||
import '../logger.dart';
|
|
||||||
import '../participant/participant.dart';
|
|
||||||
import '../proto/livekit_models.pb.dart' as lk_models;
|
import '../proto/livekit_models.pb.dart' as lk_models;
|
||||||
import '../support/disposable.dart';
|
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
|
/// Represents a track that's published to the server. This class contains
|
||||||
/// metadata associated with tracks.
|
/// metadata associated with tracks.
|
||||||
@@ -49,6 +43,11 @@ abstract class TrackPublication<T extends Track> extends Disposable {
|
|||||||
|
|
||||||
bool get subscribed => track != null;
|
bool get subscribed => track != null;
|
||||||
|
|
||||||
|
EncryptionType get encryptionType {
|
||||||
|
if (latestInfo == null) return EncryptionType.kNone;
|
||||||
|
return latestInfo!.encryption.toLkType();
|
||||||
|
}
|
||||||
|
|
||||||
@internal
|
@internal
|
||||||
lk_models.TrackInfo? latestInfo;
|
lk_models.TrackInfo? latestInfo;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
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.
|
||||||
@@ -7,6 +6,8 @@ PlatformType lkPlatform() => lkPlatformImplementation();
|
|||||||
|
|
||||||
bool lkPlatformIs(PlatformType type) => lkPlatform() == type;
|
bool lkPlatformIs(PlatformType type) => lkPlatform() == type;
|
||||||
|
|
||||||
|
bool lkPlatformSupportsE2EE() => lkE2EESupportedImplementation();
|
||||||
|
|
||||||
bool lkPlatformIsTest() => Platform.environment.containsKey('FLUTTER_TEST');
|
bool lkPlatformIsTest() => Platform.environment.containsKey('FLUTTER_TEST');
|
||||||
|
|
||||||
BrowserType lkBrowser() => lkBrowserImplementation();
|
BrowserType lkBrowser() => lkBrowserImplementation();
|
||||||
|
|||||||
@@ -12,6 +12,16 @@ PlatformType lkPlatformImplementation() {
|
|||||||
throw UnsupportedError('Unknown Platform');
|
throw UnsupportedError('Unknown Platform');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool lkE2EESupportedImplementation() {
|
||||||
|
return [
|
||||||
|
PlatformType.windows,
|
||||||
|
PlatformType.linux,
|
||||||
|
PlatformType.macOS,
|
||||||
|
PlatformType.iOS,
|
||||||
|
PlatformType.android,
|
||||||
|
].contains(lkPlatformImplementation());
|
||||||
|
}
|
||||||
|
|
||||||
BrowserType lkBrowserImplementation() {
|
BrowserType lkBrowserImplementation() {
|
||||||
return BrowserType.unknown;
|
return BrowserType.unknown;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,23 @@
|
|||||||
import '../platform.dart';
|
import '../platform.dart';
|
||||||
|
import 'dart:js' as js;
|
||||||
|
|
||||||
import 'package:platform_detect/platform_detect.dart';
|
import 'package:platform_detect/platform_detect.dart';
|
||||||
|
|
||||||
PlatformType lkPlatformImplementation() => PlatformType.web;
|
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() {
|
BrowserType lkBrowserImplementation() {
|
||||||
if (browser.isChrome) return BrowserType.chrome;
|
if (browser.isChrome) return BrowserType.chrome;
|
||||||
if (browser.isFirefox) return BrowserType.firefox;
|
if (browser.isFirefox) return BrowserType.firefox;
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ abstract class LocalTrack extends Track {
|
|||||||
bool _published = false;
|
bool _published = false;
|
||||||
bool get isPublished => _published;
|
bool get isPublished => _published;
|
||||||
|
|
||||||
|
String? codec;
|
||||||
|
|
||||||
LocalTrack(
|
LocalTrack(
|
||||||
String name,
|
String name,
|
||||||
lk_models.TrackType kind,
|
lk_models.TrackType kind,
|
||||||
|
|||||||
@@ -92,11 +92,13 @@ class RTCConfiguration {
|
|||||||
final int? iceCandidatePoolSize;
|
final int? iceCandidatePoolSize;
|
||||||
final List<RTCIceServer>? iceServers;
|
final List<RTCIceServer>? iceServers;
|
||||||
final RTCIceTransportPolicy? iceTransportPolicy;
|
final RTCIceTransportPolicy? iceTransportPolicy;
|
||||||
|
final bool? encodedInsertableStreams;
|
||||||
|
|
||||||
const RTCConfiguration({
|
const RTCConfiguration({
|
||||||
this.iceCandidatePoolSize,
|
this.iceCandidatePoolSize,
|
||||||
this.iceServers,
|
this.iceServers,
|
||||||
this.iceTransportPolicy,
|
this.iceTransportPolicy,
|
||||||
|
this.encodedInsertableStreams,
|
||||||
});
|
});
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
Map<String, dynamic> toMap() {
|
||||||
@@ -108,6 +110,8 @@ class RTCConfiguration {
|
|||||||
return <String, dynamic>{
|
return <String, dynamic>{
|
||||||
// only supports unified plan
|
// only supports unified plan
|
||||||
'sdpSemantics': 'unified-plan',
|
'sdpSemantics': 'unified-plan',
|
||||||
|
if (encodedInsertableStreams != null)
|
||||||
|
'encodedInsertableStreams': encodedInsertableStreams,
|
||||||
if (iceServersMap.isNotEmpty) 'iceServers': iceServersMap,
|
if (iceServersMap.isNotEmpty) 'iceServers': iceServersMap,
|
||||||
if (iceCandidatePoolSize != null)
|
if (iceCandidatePoolSize != null)
|
||||||
'iceCandidatePoolSize': iceCandidatePoolSize,
|
'iceCandidatePoolSize': iceCandidatePoolSize,
|
||||||
@@ -121,11 +125,14 @@ class RTCConfiguration {
|
|||||||
int? iceCandidatePoolSize,
|
int? iceCandidatePoolSize,
|
||||||
List<RTCIceServer>? iceServers,
|
List<RTCIceServer>? iceServers,
|
||||||
RTCIceTransportPolicy? iceTransportPolicy,
|
RTCIceTransportPolicy? iceTransportPolicy,
|
||||||
|
bool? encodedInsertableStreams,
|
||||||
}) =>
|
}) =>
|
||||||
RTCConfiguration(
|
RTCConfiguration(
|
||||||
iceCandidatePoolSize: iceCandidatePoolSize ?? this.iceCandidatePoolSize,
|
iceCandidatePoolSize: iceCandidatePoolSize ?? this.iceCandidatePoolSize,
|
||||||
iceServers: iceServers ?? this.iceServers,
|
iceServers: iceServers ?? this.iceServers,
|
||||||
iceTransportPolicy: iceTransportPolicy ?? this.iceTransportPolicy,
|
iceTransportPolicy: iceTransportPolicy ?? this.iceTransportPolicy,
|
||||||
|
encodedInsertableStreams:
|
||||||
|
encodedInsertableStreams ?? this.encodedInsertableStreams,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
Pod::Spec.new do |s|
|
Pod::Spec.new do |s|
|
||||||
s.name = 'livekit_client'
|
s.name = 'livekit_client'
|
||||||
s.version = '1.1.14'
|
s.version = '1.2.2'
|
||||||
s.summary = 'Open source platform for real-time audio and video.'
|
s.summary = 'Open source platform for real-time audio and video.'
|
||||||
s.description = 'Open source platform for real-time audio and video.'
|
s.description = 'Open source platform for real-time audio and video.'
|
||||||
s.homepage = 'https://livekit.io/'
|
s.homepage = 'https://livekit.io/'
|
||||||
@@ -16,5 +16,5 @@ Pod::Spec.new do |s|
|
|||||||
s.static_framework = true
|
s.static_framework = true
|
||||||
|
|
||||||
s.dependency 'FlutterMacOS'
|
s.dependency 'FlutterMacOS'
|
||||||
s.dependency 'WebRTC-SDK', '104.5112.12'
|
s.dependency 'WebRTC-SDK', '104.5112.16'
|
||||||
end
|
end
|
||||||
|
|||||||
+14
-5
@@ -129,6 +129,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.2"
|
version: "3.0.2"
|
||||||
|
cryptography:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: cryptography
|
||||||
|
sha256: e0e37f79665cd5c86e8897f9abe1accfe813c0cc5299dab22256e22fddc1fef8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.5"
|
||||||
dart_style:
|
dart_style:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -227,11 +235,12 @@ packages:
|
|||||||
flutter_webrtc:
|
flutter_webrtc:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_webrtc
|
path: "."
|
||||||
sha256: f8904369f3ad57945a797481bf079c281f4944ad4635e9d412529141d81c9c76
|
ref: "feat/frame-encryption"
|
||||||
url: "https://pub.dev"
|
resolved-ref: abb6aba36434fa3a30ce5c56b6c521acb83b1139
|
||||||
source: hosted
|
url: "https://github.com/flutter-webrtc/flutter-webrtc.git"
|
||||||
version: "0.9.25"
|
source: git
|
||||||
|
version: "0.9.20"
|
||||||
flutter_window_close:
|
flutter_window_close:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
+3
-1
@@ -16,6 +16,7 @@ dependencies:
|
|||||||
async: ^2.9.0
|
async: ^2.9.0
|
||||||
collection: ^1.16.0
|
collection: ^1.16.0
|
||||||
connectivity_plus: ^3.0.2
|
connectivity_plus: ^3.0.2
|
||||||
|
cryptography: ^2.0.5
|
||||||
fixnum: ^1.0.1
|
fixnum: ^1.0.1
|
||||||
meta: ^1.8.0
|
meta: ^1.8.0
|
||||||
http: ^0.13.5
|
http: ^0.13.5
|
||||||
@@ -23,11 +24,12 @@ dependencies:
|
|||||||
uuid: ^3.0.6
|
uuid: ^3.0.6
|
||||||
synchronized: ^3.0.0+3
|
synchronized: ^3.0.0+3
|
||||||
protobuf: ^2.1.0
|
protobuf: ^2.1.0
|
||||||
flutter_webrtc: 0.9.26
|
flutter_webrtc: 0.9.27
|
||||||
flutter_window_close: ^0.2.2
|
flutter_window_close: ^0.2.2
|
||||||
device_info_plus: ^8.0.0
|
device_info_plus: ^8.0.0
|
||||||
webrtc_interface: 1.0.13
|
webrtc_interface: 1.0.13
|
||||||
dart_webrtc: 1.0.16
|
dart_webrtc: 1.0.16
|
||||||
|
js: ^0.6.4
|
||||||
platform_detect: ^2.0.7
|
platform_detect: ^2.0.7
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Upgrade Notes
|
||||||
|
|
||||||
|
When the flutter-webrtc version is upgraded ([web/e2ee*.dart](https://github.com/flutter-webrtc/flutter-webrtc/tree/main/web) changes), please directly overwrite these files from the corresponding flutter-webrtc version
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'dart:js_util' as jsutil;
|
||||||
|
import 'dart:html' as html;
|
||||||
|
|
||||||
|
import 'package:js/js.dart';
|
||||||
|
|
||||||
|
@JS('Promise')
|
||||||
|
class Promise<T> {
|
||||||
|
external factory Promise._();
|
||||||
|
}
|
||||||
|
|
||||||
|
@JS('Algorithm')
|
||||||
|
class Algorithm {
|
||||||
|
external String get name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JS('crypto.subtle.encrypt')
|
||||||
|
external Promise<ByteBuffer> encrypt(
|
||||||
|
dynamic algorithm,
|
||||||
|
html.CryptoKey key,
|
||||||
|
ByteBuffer data,
|
||||||
|
);
|
||||||
|
|
||||||
|
@JS('crypto.subtle.decrypt')
|
||||||
|
external Promise<ByteBuffer> decrypt(
|
||||||
|
dynamic algorithm,
|
||||||
|
html.CryptoKey key,
|
||||||
|
ByteBuffer data,
|
||||||
|
);
|
||||||
|
|
||||||
|
@JS()
|
||||||
|
@anonymous
|
||||||
|
class AesGcmParams {
|
||||||
|
external factory AesGcmParams({
|
||||||
|
required String name,
|
||||||
|
required ByteBuffer iv,
|
||||||
|
ByteBuffer? additionalData,
|
||||||
|
int tagLength = 128,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteBuffer jsArrayBufferFrom(List<int> data) {
|
||||||
|
// Avoid copying if possible
|
||||||
|
if (data is Uint8List &&
|
||||||
|
data.offsetInBytes == 0 &&
|
||||||
|
data.lengthInBytes == data.buffer.lengthInBytes) {
|
||||||
|
return data.buffer;
|
||||||
|
}
|
||||||
|
// Copy
|
||||||
|
return Uint8List.fromList(data).buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JS('crypto.subtle.importKey')
|
||||||
|
external Promise<html.CryptoKey> importKey(
|
||||||
|
String format,
|
||||||
|
ByteBuffer keyData,
|
||||||
|
dynamic algorithm,
|
||||||
|
bool extractable,
|
||||||
|
List<String> keyUsages,
|
||||||
|
);
|
||||||
|
|
||||||
|
@JS('crypto.subtle.exportKey')
|
||||||
|
external Promise<ByteBuffer> exportKey(
|
||||||
|
String format,
|
||||||
|
html.CryptoKey key,
|
||||||
|
);
|
||||||
|
|
||||||
|
@JS('crypto.subtle.deriveKey')
|
||||||
|
external Promise<html.CryptoKey> deriveKey(
|
||||||
|
dynamic algorithm,
|
||||||
|
html.CryptoKey baseKey,
|
||||||
|
dynamic derivedKeyAlgorithm,
|
||||||
|
bool extractable,
|
||||||
|
List<String> keyUsages);
|
||||||
|
|
||||||
|
@JS('crypto.subtle.deriveBits')
|
||||||
|
external Promise<ByteBuffer> deriveBits(
|
||||||
|
dynamic algorithm,
|
||||||
|
html.CryptoKey baseKey,
|
||||||
|
int length,
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<html.CryptoKey> impportKeyFromRawData(List<int> secretKeyData,
|
||||||
|
{required String webCryptoAlgorithm,
|
||||||
|
required List<String> keyUsages}) async {
|
||||||
|
return jsutil.promiseToFuture<html.CryptoKey>(importKey(
|
||||||
|
'raw',
|
||||||
|
jsArrayBufferFrom(secretKeyData),
|
||||||
|
jsutil.jsify({'name': webCryptoAlgorithm}),
|
||||||
|
false,
|
||||||
|
keyUsages,
|
||||||
|
));
|
||||||
|
}
|
||||||
@@ -0,0 +1,643 @@
|
|||||||
|
import 'dart:html';
|
||||||
|
import 'dart:js';
|
||||||
|
import 'dart:js_util' as jsutil;
|
||||||
|
import 'dart:math';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'dart:collection';
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter_webrtc/src/web/rtc_transform_stream.dart';
|
||||||
|
|
||||||
|
import 'crypto.dart' as crypto;
|
||||||
|
import 'e2ee.utils.dart';
|
||||||
|
|
||||||
|
class KeyOptions {
|
||||||
|
KeyOptions({
|
||||||
|
required this.sharedKey,
|
||||||
|
required this.ratchetSalt,
|
||||||
|
required this.ratchetWindowSize,
|
||||||
|
this.uncryptedMagicBytes,
|
||||||
|
});
|
||||||
|
bool sharedKey;
|
||||||
|
Uint8List ratchetSalt;
|
||||||
|
int ratchetWindowSize;
|
||||||
|
Uint8List? uncryptedMagicBytes;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'KeyOptions{sharedKey: $sharedKey, ratchetWindowSize: $ratchetWindowSize}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const IV_LENGTH = 12;
|
||||||
|
|
||||||
|
const kNaluTypeMask = 0x1f;
|
||||||
|
|
||||||
|
/// Coded slice of a non-IDR picture
|
||||||
|
const SLICE_NON_IDR = 1;
|
||||||
|
|
||||||
|
/// Coded slice data partition A
|
||||||
|
const SLICE_PARTITION_A = 2;
|
||||||
|
|
||||||
|
/// Coded slice data partition B
|
||||||
|
const SLICE_PARTITION_B = 3;
|
||||||
|
|
||||||
|
/// Coded slice data partition C
|
||||||
|
const SLICE_PARTITION_C = 4;
|
||||||
|
|
||||||
|
/// Coded slice of an IDR picture
|
||||||
|
const SLICE_IDR = 5;
|
||||||
|
|
||||||
|
/// Supplemental enhancement information
|
||||||
|
const SEI = 6;
|
||||||
|
|
||||||
|
/// Sequence parameter set
|
||||||
|
const SPS = 7;
|
||||||
|
|
||||||
|
/// Picture parameter set
|
||||||
|
const PPS = 8;
|
||||||
|
|
||||||
|
/// Access unit delimiter
|
||||||
|
const AUD = 9;
|
||||||
|
|
||||||
|
/// End of sequence
|
||||||
|
const END_SEQ = 10;
|
||||||
|
|
||||||
|
/// End of stream
|
||||||
|
const END_STREAM = 11;
|
||||||
|
|
||||||
|
/// Filler data
|
||||||
|
const FILLER_DATA = 12;
|
||||||
|
|
||||||
|
/// Sequence parameter set extension
|
||||||
|
const SPS_EXT = 13;
|
||||||
|
|
||||||
|
/// Prefix NAL unit
|
||||||
|
const PREFIX_NALU = 14;
|
||||||
|
|
||||||
|
/// Subset sequence parameter set
|
||||||
|
const SUBSET_SPS = 15;
|
||||||
|
|
||||||
|
/// Depth parameter set
|
||||||
|
const DPS = 16;
|
||||||
|
|
||||||
|
// 17, 18 reserved
|
||||||
|
|
||||||
|
/// Coded slice of an auxiliary coded picture without partitioning
|
||||||
|
const SLICE_AUX = 19;
|
||||||
|
|
||||||
|
/// Coded slice extension
|
||||||
|
const SLICE_EXT = 20;
|
||||||
|
|
||||||
|
/// Coded slice extension for a depth view component or a 3D-AVC texture view component
|
||||||
|
const SLICE_LAYER_EXT = 21;
|
||||||
|
|
||||||
|
// 22, 23 reserved
|
||||||
|
|
||||||
|
List<int> findNALUIndices(Uint8List stream) {
|
||||||
|
var result = <int>[];
|
||||||
|
var start = 0, pos = 0, searchLength = stream.length - 2;
|
||||||
|
while (pos < searchLength) {
|
||||||
|
// skip until end of current NALU
|
||||||
|
while (pos < searchLength &&
|
||||||
|
!(stream[pos] == 0 && stream[pos + 1] == 0 && stream[pos + 2] == 1)) {
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
if (pos >= searchLength) pos = stream.length;
|
||||||
|
// remove trailing zeros from current NALU
|
||||||
|
var end = pos;
|
||||||
|
while (end > start && stream[end - 1] == 0) {
|
||||||
|
end--;
|
||||||
|
}
|
||||||
|
// save current NALU
|
||||||
|
if (start == 0) {
|
||||||
|
if (end != start) throw Exception('byte stream contains leading data');
|
||||||
|
} else {
|
||||||
|
result.add(start);
|
||||||
|
}
|
||||||
|
// begin new NALU
|
||||||
|
start = pos = pos + 3;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
int parseNALUType(int startByte) {
|
||||||
|
return startByte & kNaluTypeMask;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CryptorError {
|
||||||
|
kNew,
|
||||||
|
kOk,
|
||||||
|
kDecryptError,
|
||||||
|
kEncryptError,
|
||||||
|
kUnsupportedCodec,
|
||||||
|
kMissingKey,
|
||||||
|
kKeyRatcheted,
|
||||||
|
kInternalError,
|
||||||
|
kDisposed,
|
||||||
|
}
|
||||||
|
|
||||||
|
const KEYRING_SIZE = 16;
|
||||||
|
|
||||||
|
class KeySet {
|
||||||
|
KeySet(this.material, this.encryptionKey);
|
||||||
|
CryptoKey material;
|
||||||
|
CryptoKey encryptionKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
class FrameCryptor {
|
||||||
|
FrameCryptor(
|
||||||
|
{required this.worker,
|
||||||
|
required this.participantId,
|
||||||
|
required this.trackId,
|
||||||
|
required this.keyOptions});
|
||||||
|
Map<int, int> sendCounts = {};
|
||||||
|
String? participantId;
|
||||||
|
String? trackId;
|
||||||
|
String? codec;
|
||||||
|
final KeyOptions keyOptions;
|
||||||
|
late String kind;
|
||||||
|
bool enabled = false;
|
||||||
|
CryptorError lastError = CryptorError.kNew;
|
||||||
|
final DedicatedWorkerGlobalScope worker;
|
||||||
|
int currentKeyIndex = 0;
|
||||||
|
|
||||||
|
Completer? _ratchetCompleter;
|
||||||
|
|
||||||
|
List<KeySet?> cryptoKeyRing = List.filled(KEYRING_SIZE, null);
|
||||||
|
|
||||||
|
Future<void> ratchetKey(int? keyIndex) async {
|
||||||
|
if (_ratchetCompleter == null) {
|
||||||
|
_ratchetCompleter = Completer<void>();
|
||||||
|
var currentMaterial = getKeySet(keyIndex)?.material;
|
||||||
|
if (currentMaterial == null) {
|
||||||
|
_ratchetCompleter!.complete();
|
||||||
|
_ratchetCompleter = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ratchetMaterial(currentMaterial).then((newMaterial) {
|
||||||
|
deriveKeys(newMaterial, keyOptions.ratchetSalt).then((newKeySet) {
|
||||||
|
setKeySetFromMaterial(newKeySet, keyIndex ?? currentKeyIndex)
|
||||||
|
.then((_) {
|
||||||
|
_ratchetCompleter!.complete();
|
||||||
|
_ratchetCompleter = null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return _ratchetCompleter!.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<CryptoKey> ratchetMaterial(CryptoKey currentMaterial) async {
|
||||||
|
var newMaterial = await jsutil.promiseToFuture(crypto.importKey(
|
||||||
|
'raw',
|
||||||
|
crypto.jsArrayBufferFrom(
|
||||||
|
await ratchet(currentMaterial, keyOptions.ratchetSalt)),
|
||||||
|
(currentMaterial.algorithm as crypto.Algorithm).name,
|
||||||
|
false,
|
||||||
|
['deriveBits', 'deriveKey'],
|
||||||
|
));
|
||||||
|
return newMaterial;
|
||||||
|
}
|
||||||
|
|
||||||
|
KeySet? getKeySet(int? keyIndex) {
|
||||||
|
return cryptoKeyRing[keyIndex ?? currentKeyIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
void setParticipantId(String participantId) {
|
||||||
|
if (lastError != CryptorError.kOk) {
|
||||||
|
print(
|
||||||
|
'setParticipantId: lastError != CryptorError.kOk, reset state to kNew');
|
||||||
|
lastError = CryptorError.kNew;
|
||||||
|
}
|
||||||
|
this.participantId = participantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setKeyIndex(int keyIndex) {
|
||||||
|
if (lastError != CryptorError.kOk) {
|
||||||
|
print('setKeyIndex: lastError != CryptorError.kOk, reset state to kNew');
|
||||||
|
lastError = CryptorError.kNew;
|
||||||
|
}
|
||||||
|
currentKeyIndex = keyIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setEnabled(bool enabled) {
|
||||||
|
if (lastError != CryptorError.kOk) {
|
||||||
|
print(
|
||||||
|
'setEnabled[$enabled]: lastError != CryptorError.kOk, reset state to kNew');
|
||||||
|
lastError = CryptorError.kNew;
|
||||||
|
}
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setKey(int keyIndex, Uint8List key) async {
|
||||||
|
if (lastError != CryptorError.kOk) {
|
||||||
|
print('setKey: lastError != CryptorError.kOk, reset state to kNew');
|
||||||
|
lastError = CryptorError.kNew;
|
||||||
|
}
|
||||||
|
var keyMaterial = await crypto.impportKeyFromRawData(key,
|
||||||
|
webCryptoAlgorithm: 'PBKDF2', keyUsages: ['deriveBits', 'deriveKey']);
|
||||||
|
var keySet = await deriveKeys(
|
||||||
|
keyMaterial,
|
||||||
|
keyOptions.ratchetSalt,
|
||||||
|
);
|
||||||
|
await setKeySetFromMaterial(keySet, keyIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setKeySetFromMaterial(KeySet keySet, int keyIndex) async {
|
||||||
|
print('setting new key');
|
||||||
|
if (keyIndex >= 0) {
|
||||||
|
currentKeyIndex = keyIndex % cryptoKeyRing.length;
|
||||||
|
}
|
||||||
|
cryptoKeyRing[currentKeyIndex] = keySet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Derives a set of keys from the master key.
|
||||||
|
/// See https://tools.ietf.org/html/draft-omara-sframe-00#section-4.3.1
|
||||||
|
Future<KeySet> deriveKeys(CryptoKey material, Uint8List salt) async {
|
||||||
|
var algorithmOptions =
|
||||||
|
getAlgoOptions((material.algorithm as crypto.Algorithm).name, salt);
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveKey#HKDF
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/HkdfParams
|
||||||
|
var encryptionKey =
|
||||||
|
await jsutil.promiseToFuture<CryptoKey>(crypto.deriveKey(
|
||||||
|
jsutil.jsify(algorithmOptions),
|
||||||
|
material,
|
||||||
|
jsutil.jsify({'name': 'AES-GCM', 'length': 128}),
|
||||||
|
false,
|
||||||
|
['encrypt', 'decrypt'],
|
||||||
|
));
|
||||||
|
|
||||||
|
return KeySet(material, encryptionKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ratchets a key. See
|
||||||
|
/// https://tools.ietf.org/html/draft-omara-sframe-00#section-4.3.5.1
|
||||||
|
|
||||||
|
Future<Uint8List> ratchet(CryptoKey material, Uint8List salt) async {
|
||||||
|
var algorithmOptions = getAlgoOptions('PBKDF2', salt);
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveBits
|
||||||
|
var newKey = await jsutil.promiseToFuture<ByteBuffer>(
|
||||||
|
crypto.deriveBits(jsutil.jsify(algorithmOptions), material, 256));
|
||||||
|
return newKey.asUint8List();
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateCodec(String codec) {
|
||||||
|
if (lastError != CryptorError.kOk) {
|
||||||
|
print(
|
||||||
|
'updateCodec[$codec]: lastError != CryptorError.kOk, reset state to kNew');
|
||||||
|
lastError = CryptorError.kNew;
|
||||||
|
}
|
||||||
|
this.codec = codec;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint8List makeIv(
|
||||||
|
{required int synchronizationSource, required int timestamp}) {
|
||||||
|
var iv = ByteData(IV_LENGTH);
|
||||||
|
|
||||||
|
// having to keep our own send count (similar to a picture id) is not ideal.
|
||||||
|
if (sendCounts[synchronizationSource] == null) {
|
||||||
|
// Initialize with a random offset, similar to the RTP sequence number.
|
||||||
|
sendCounts[synchronizationSource] = Random.secure().nextInt(0xffff);
|
||||||
|
}
|
||||||
|
|
||||||
|
var sendCount = sendCounts[synchronizationSource] ?? 0;
|
||||||
|
|
||||||
|
iv.setUint32(0, synchronizationSource);
|
||||||
|
iv.setUint32(4, timestamp);
|
||||||
|
iv.setUint32(8, timestamp - (sendCount % 0xffff));
|
||||||
|
|
||||||
|
sendCounts[synchronizationSource] = sendCount + 1;
|
||||||
|
|
||||||
|
return iv.buffer.asUint8List();
|
||||||
|
}
|
||||||
|
|
||||||
|
void postMessage(Object message) {
|
||||||
|
worker.postMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setupTransform({
|
||||||
|
required String operation,
|
||||||
|
required ReadableStream readable,
|
||||||
|
required WritableStream writable,
|
||||||
|
required String trackId,
|
||||||
|
required String kind,
|
||||||
|
String? codec,
|
||||||
|
}) async {
|
||||||
|
print('setupTransform $operation');
|
||||||
|
this.kind = kind;
|
||||||
|
if (codec != null) {
|
||||||
|
print('setting codec on cryptor to $codec');
|
||||||
|
this.codec = codec;
|
||||||
|
}
|
||||||
|
var transformer = TransformStream(jsutil.jsify({
|
||||||
|
'transform':
|
||||||
|
allowInterop(operation == 'encode' ? encodeFunction : decodeFunction)
|
||||||
|
}));
|
||||||
|
try {
|
||||||
|
readable.pipeThrough(transformer).pipeTo(writable);
|
||||||
|
} catch (e) {
|
||||||
|
print('e ${e.toString()}');
|
||||||
|
if (lastError != CryptorError.kInternalError) {
|
||||||
|
lastError = CryptorError.kInternalError;
|
||||||
|
postMessage({
|
||||||
|
'type': 'cryptorState',
|
||||||
|
'participantId': participantId,
|
||||||
|
'state': 'internalError',
|
||||||
|
'error': 'Internal error: ${e.toString()}'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.trackId = trackId;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getUnencryptedBytes(RTCEncodedFrame frame, String? codec) {
|
||||||
|
if (codec != null && codec.toLowerCase() == 'h264') {
|
||||||
|
var data = frame.data.asUint8List();
|
||||||
|
var naluIndices = findNALUIndices(data);
|
||||||
|
for (var index in naluIndices) {
|
||||||
|
var type = parseNALUType(data[index]);
|
||||||
|
switch (type) {
|
||||||
|
case SLICE_IDR:
|
||||||
|
case SLICE_NON_IDR:
|
||||||
|
// skipping
|
||||||
|
//print('unEncryptedBytes NALU of type $type, offset ${index + 2}');
|
||||||
|
return index + 2;
|
||||||
|
default:
|
||||||
|
//print('skipping NALU of type $type');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw Exception('Could not find NALU');
|
||||||
|
}
|
||||||
|
switch (frame.type) {
|
||||||
|
case 'key':
|
||||||
|
return 10;
|
||||||
|
case 'delta':
|
||||||
|
return 3;
|
||||||
|
case 'audio':
|
||||||
|
return 1; // frame.type is not set on audio, so this is set manually
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> encodeFunction(
|
||||||
|
RTCEncodedFrame frame,
|
||||||
|
TransformStreamDefaultController controller,
|
||||||
|
) async {
|
||||||
|
var buffer = frame.data.asUint8List();
|
||||||
|
|
||||||
|
if (!enabled ||
|
||||||
|
// skip for encryption for empty dtx frames
|
||||||
|
buffer.isEmpty) {
|
||||||
|
controller.enqueue(frame);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var secretKey = getKeySet(currentKeyIndex)?.encryptionKey;
|
||||||
|
var keyIndex = currentKeyIndex;
|
||||||
|
|
||||||
|
if (secretKey == null) {
|
||||||
|
if (lastError != CryptorError.kMissingKey) {
|
||||||
|
lastError = CryptorError.kMissingKey;
|
||||||
|
postMessage({
|
||||||
|
'type': 'cryptorState',
|
||||||
|
'participantId': participantId,
|
||||||
|
'trackId': trackId,
|
||||||
|
'kind': kind,
|
||||||
|
'state': 'missingKey',
|
||||||
|
'error': 'Missing key for track $trackId',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var headerLength =
|
||||||
|
kind == 'video' ? getUnencryptedBytes(frame, codec) : 1;
|
||||||
|
var metaData = frame.getMetadata();
|
||||||
|
var iv = makeIv(
|
||||||
|
synchronizationSource: metaData.synchronizationSource,
|
||||||
|
timestamp: frame.timestamp);
|
||||||
|
|
||||||
|
var frameTrailer = ByteData(2);
|
||||||
|
frameTrailer.setInt8(0, IV_LENGTH);
|
||||||
|
frameTrailer.setInt8(1, keyIndex);
|
||||||
|
|
||||||
|
var cipherText = await jsutil.promiseToFuture<ByteBuffer>(crypto.encrypt(
|
||||||
|
crypto.AesGcmParams(
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: crypto.jsArrayBufferFrom(iv),
|
||||||
|
additionalData:
|
||||||
|
crypto.jsArrayBufferFrom(buffer.sublist(0, headerLength)),
|
||||||
|
),
|
||||||
|
secretKey,
|
||||||
|
crypto.jsArrayBufferFrom(buffer.sublist(headerLength, buffer.length)),
|
||||||
|
));
|
||||||
|
|
||||||
|
//print(
|
||||||
|
// 'buffer: ${buffer.length}, cipherText: ${cipherText.asUint8List().length}');
|
||||||
|
var finalBuffer = BytesBuilder();
|
||||||
|
|
||||||
|
finalBuffer.add(Uint8List.fromList(buffer.sublist(0, headerLength)));
|
||||||
|
finalBuffer.add(cipherText.asUint8List());
|
||||||
|
finalBuffer.add(iv);
|
||||||
|
finalBuffer.add(frameTrailer.buffer.asUint8List());
|
||||||
|
frame.data = crypto.jsArrayBufferFrom(finalBuffer.toBytes());
|
||||||
|
|
||||||
|
controller.enqueue(frame);
|
||||||
|
|
||||||
|
if (lastError != CryptorError.kOk) {
|
||||||
|
lastError = CryptorError.kOk;
|
||||||
|
postMessage({
|
||||||
|
'type': 'cryptorState',
|
||||||
|
'participantId': participantId,
|
||||||
|
'trackId': trackId,
|
||||||
|
'kind': kind,
|
||||||
|
'state': 'ok',
|
||||||
|
'error': 'encryption ok'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//print(
|
||||||
|
// 'encrypto kind $kind,codec $codec headerLength: $headerLength, timestamp: ${frame.timestamp}, ssrc: ${metaData.synchronizationSource}, data length: ${buffer.length}, encrypted length: ${finalBuffer.toBytes().length}, key ${secretKey.toString()} , iv $iv');
|
||||||
|
} catch (e) {
|
||||||
|
//print('encrypt: e ${e.toString()}');
|
||||||
|
if (lastError != CryptorError.kEncryptError) {
|
||||||
|
lastError = CryptorError.kEncryptError;
|
||||||
|
postMessage({
|
||||||
|
'type': 'cryptorState',
|
||||||
|
'participantId': participantId,
|
||||||
|
'trackId': trackId,
|
||||||
|
'kind': kind,
|
||||||
|
'state': 'encryptError',
|
||||||
|
'error': e.toString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> decodeFunction(
|
||||||
|
RTCEncodedFrame frame,
|
||||||
|
TransformStreamDefaultController controller,
|
||||||
|
) async {
|
||||||
|
var ratchetCount = 0;
|
||||||
|
var buffer = frame.data.asUint8List();
|
||||||
|
ByteBuffer? decrypted;
|
||||||
|
KeySet? initialKeySet;
|
||||||
|
int initialKeyIndex = currentKeyIndex;
|
||||||
|
|
||||||
|
if (!enabled ||
|
||||||
|
// skip for encryption for empty dtx frames
|
||||||
|
buffer.isEmpty) {
|
||||||
|
controller.enqueue(frame);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyOptions.uncryptedMagicBytes != null) {
|
||||||
|
var magicBytes = keyOptions.uncryptedMagicBytes!;
|
||||||
|
if (buffer.length >= magicBytes.length + 1) {
|
||||||
|
var magicBytesBuffer = buffer.sublist(
|
||||||
|
buffer.length - (magicBytes.length + 1), magicBytes.length);
|
||||||
|
if (magicBytesBuffer.toString() == magicBytes.toString()) {
|
||||||
|
var finalBuffer = BytesBuilder();
|
||||||
|
finalBuffer.add(Uint8List.fromList(
|
||||||
|
buffer.sublist(0, buffer.length - (magicBytes.length + 1))));
|
||||||
|
frame.data = crypto.jsArrayBufferFrom(finalBuffer.toBytes());
|
||||||
|
controller.enqueue(frame);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var headerLength =
|
||||||
|
kind == 'video' ? getUnencryptedBytes(frame, codec) : 1;
|
||||||
|
var metaData = frame.getMetadata();
|
||||||
|
|
||||||
|
var frameTrailer = buffer.sublist(buffer.length - 2);
|
||||||
|
var ivLength = frameTrailer[0];
|
||||||
|
var keyIndex = frameTrailer[1];
|
||||||
|
var iv = buffer.sublist(buffer.length - ivLength - 2, buffer.length - 2);
|
||||||
|
|
||||||
|
var initialKeySet = getKeySet(keyIndex);
|
||||||
|
initialKeyIndex = keyIndex;
|
||||||
|
|
||||||
|
if (initialKeySet == null) {
|
||||||
|
if (lastError != CryptorError.kMissingKey) {
|
||||||
|
lastError = CryptorError.kMissingKey;
|
||||||
|
postMessage({
|
||||||
|
'type': 'cryptorState',
|
||||||
|
'participantId': participantId,
|
||||||
|
'trackId': trackId,
|
||||||
|
'kind': kind,
|
||||||
|
'state': 'missingKey',
|
||||||
|
'error': 'Missing key for track $trackId'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
controller.enqueue(frame);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bool endDecLoop = false;
|
||||||
|
var currentkeySet = initialKeySet;
|
||||||
|
while (!endDecLoop) {
|
||||||
|
try {
|
||||||
|
decrypted = await jsutil.promiseToFuture<ByteBuffer>(crypto.decrypt(
|
||||||
|
crypto.AesGcmParams(
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: crypto.jsArrayBufferFrom(iv),
|
||||||
|
additionalData:
|
||||||
|
crypto.jsArrayBufferFrom(buffer.sublist(0, headerLength)),
|
||||||
|
),
|
||||||
|
currentkeySet.encryptionKey,
|
||||||
|
crypto.jsArrayBufferFrom(
|
||||||
|
buffer.sublist(headerLength, buffer.length - ivLength - 2)),
|
||||||
|
));
|
||||||
|
|
||||||
|
if (decrypted != null && currentkeySet != initialKeySet) {
|
||||||
|
await setKeySetFromMaterial(currentkeySet, initialKeyIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
endDecLoop = true;
|
||||||
|
|
||||||
|
if (lastError != CryptorError.kOk &&
|
||||||
|
lastError != CryptorError.kKeyRatcheted &&
|
||||||
|
ratchetCount > 0) {
|
||||||
|
print(
|
||||||
|
'KeyRatcheted: ssrc ${metaData.synchronizationSource} timestamp ${frame.timestamp} ratchetCount $ratchetCount participantId: $participantId');
|
||||||
|
print(
|
||||||
|
'ratchetKey: lastError != CryptorError.kKeyRatcheted, reset state to kKeyRatcheted');
|
||||||
|
|
||||||
|
lastError = CryptorError.kKeyRatcheted;
|
||||||
|
postMessage({
|
||||||
|
'type': 'cryptorState',
|
||||||
|
'participantId': participantId,
|
||||||
|
'trackId': trackId,
|
||||||
|
'kind': kind,
|
||||||
|
'state': 'keyRatcheted',
|
||||||
|
'error': 'Key ratcheted ok'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
lastError = CryptorError.kInternalError;
|
||||||
|
endDecLoop = ratchetCount >= keyOptions.ratchetWindowSize ||
|
||||||
|
keyOptions.ratchetWindowSize <= 0;
|
||||||
|
if (endDecLoop) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
var newMaterial = await ratchetMaterial(currentkeySet.material);
|
||||||
|
currentkeySet = await deriveKeys(newMaterial, keyOptions.ratchetSalt);
|
||||||
|
ratchetCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//print(
|
||||||
|
// 'buffer: ${buffer.length}, decrypted: ${decrypted.asUint8List().length}');
|
||||||
|
var finalBuffer = BytesBuilder();
|
||||||
|
|
||||||
|
finalBuffer.add(Uint8List.fromList(buffer.sublist(0, headerLength)));
|
||||||
|
finalBuffer.add(decrypted!.asUint8List());
|
||||||
|
frame.data = crypto.jsArrayBufferFrom(finalBuffer.toBytes());
|
||||||
|
controller.enqueue(frame);
|
||||||
|
|
||||||
|
if (lastError != CryptorError.kOk) {
|
||||||
|
lastError = CryptorError.kOk;
|
||||||
|
postMessage({
|
||||||
|
'type': 'cryptorState',
|
||||||
|
'participantId': participantId,
|
||||||
|
'trackId': trackId,
|
||||||
|
'kind': kind,
|
||||||
|
'state': 'ok',
|
||||||
|
'error': 'decryption ok'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//print(
|
||||||
|
// 'decrypto kind $kind,codec $codec headerLength: $headerLength, timestamp: ${frame.timestamp}, ssrc: ${metaData.synchronizationSource}, data length: ${buffer.length}, decrypted length: ${finalBuffer.toBytes().length}, key ${secretKey.toString()}, keyindex $keyIndex iv $iv');
|
||||||
|
} catch (e) {
|
||||||
|
if (lastError != CryptorError.kDecryptError) {
|
||||||
|
lastError = CryptorError.kDecryptError;
|
||||||
|
postMessage({
|
||||||
|
'type': 'cryptorState',
|
||||||
|
'participantId': participantId,
|
||||||
|
'trackId': trackId,
|
||||||
|
'kind': kind,
|
||||||
|
'state': 'decryptError',
|
||||||
|
'error': e.toString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Since the key it is first send and only afterwards actually used for encrypting, there were
|
||||||
|
/// situations when the decrypting failed due to the fact that the received frame was not encrypted
|
||||||
|
/// yet and ratcheting, of course, did not solve the problem. So if we fail RATCHET_WINDOW_SIZE times,
|
||||||
|
/// we come back to the initial key.
|
||||||
|
if (initialKeySet != null) {
|
||||||
|
await setKeySetFromMaterial(initialKeySet, initialKeyIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import 'dart:html';
|
||||||
|
import 'dart:js' as js;
|
||||||
|
import 'dart:js_util';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'crypto.dart' as crypto;
|
||||||
|
|
||||||
|
bool isE2EESupported() {
|
||||||
|
return isInsertableStreamSupported() || isScriptTransformSupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isScriptTransformSupported() {
|
||||||
|
return js.context['RTCRtpScriptTransform'] != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isInsertableStreamSupported() {
|
||||||
|
return js.context['RTCRtpSender'] != null &&
|
||||||
|
js.context['RTCRtpSender']['prototype']['createEncodedStreams'] != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<CryptoKey> importKey(
|
||||||
|
Uint8List keyBytes, String algorithm, String usage) {
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey
|
||||||
|
return promiseToFuture<CryptoKey>(crypto.importKey(
|
||||||
|
'raw',
|
||||||
|
crypto.jsArrayBufferFrom(keyBytes),
|
||||||
|
js.JsObject.jsify({'name': algorithm}),
|
||||||
|
false,
|
||||||
|
usage == 'derive' ? ['deriveBits', 'deriveKey'] : ['encrypt', 'decrypt'],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<CryptoKey> createKeyMaterialFromString(
|
||||||
|
Uint8List keyBytes, String algorithm, String usage) {
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey
|
||||||
|
return promiseToFuture<CryptoKey>(crypto.importKey(
|
||||||
|
'raw',
|
||||||
|
crypto.jsArrayBufferFrom(keyBytes),
|
||||||
|
js.JsObject.jsify({'name': 'PBKDF2'}),
|
||||||
|
false,
|
||||||
|
['deriveBits', 'deriveKey'],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamic getAlgoOptions(String algorithmName, Uint8List salt) {
|
||||||
|
switch (algorithmName) {
|
||||||
|
case 'HKDF':
|
||||||
|
return {
|
||||||
|
'name': 'HKDF',
|
||||||
|
'salt': crypto.jsArrayBufferFrom(salt),
|
||||||
|
'hash': 'SHA-256',
|
||||||
|
'info': crypto.jsArrayBufferFrom(Uint8List(128)),
|
||||||
|
};
|
||||||
|
case 'PBKDF2':
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
'name': 'PBKDF2',
|
||||||
|
'salt': crypto.jsArrayBufferFrom(salt),
|
||||||
|
'hash': 'SHA-256',
|
||||||
|
'iterations': 100000,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw Exception('algorithm $algorithmName is currently unsupported');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:html' as html;
|
||||||
|
import 'dart:js_util' as js_util;
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:js/js.dart';
|
||||||
|
|
||||||
|
import 'e2ee.cryptor.dart';
|
||||||
|
|
||||||
|
import 'package:flutter_webrtc/src/web/rtc_transform_stream.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
|
import 'crypto.dart' as crypto;
|
||||||
|
|
||||||
|
@JS()
|
||||||
|
abstract class TransformMessage {
|
||||||
|
external String get msgType;
|
||||||
|
external String get kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
@anonymous
|
||||||
|
@JS()
|
||||||
|
class EnableTransformMessage {
|
||||||
|
external factory EnableTransformMessage({
|
||||||
|
ReadableStream readable,
|
||||||
|
WritableStream writable,
|
||||||
|
String msgType,
|
||||||
|
String kind,
|
||||||
|
String participantId,
|
||||||
|
String trackId,
|
||||||
|
String codec,
|
||||||
|
});
|
||||||
|
external ReadableStream get readable;
|
||||||
|
external WritableStream get writable;
|
||||||
|
external String get msgType; // 'encode' or 'decode'
|
||||||
|
external String get participantId;
|
||||||
|
external String get trackId;
|
||||||
|
external String get kind;
|
||||||
|
external String get codec;
|
||||||
|
}
|
||||||
|
|
||||||
|
@anonymous
|
||||||
|
@JS()
|
||||||
|
class RemoveTransformMessage {
|
||||||
|
external factory RemoveTransformMessage(
|
||||||
|
{String msgType, String participantId, String trackId});
|
||||||
|
external String get msgType; // 'removeTransform'
|
||||||
|
external String get participantId;
|
||||||
|
external String get trackId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JS('self')
|
||||||
|
external html.DedicatedWorkerGlobalScope get self;
|
||||||
|
|
||||||
|
extension PropsRTCTransformEventHandler on html.DedicatedWorkerGlobalScope {
|
||||||
|
set onrtctransform(Function(dynamic) callback) =>
|
||||||
|
js_util.setProperty<Function>(this, 'onrtctransform', callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
var participantCryptors = <FrameCryptor>[];
|
||||||
|
var publisherKeys = <String, html.CryptoKey>{};
|
||||||
|
bool isEncryptionEnabled = false;
|
||||||
|
|
||||||
|
KeyOptions keyProviderOptions = KeyOptions(
|
||||||
|
sharedKey: true,
|
||||||
|
ratchetSalt: Uint8List.fromList('ratchetSalt'.codeUnits),
|
||||||
|
ratchetWindowSize: 16);
|
||||||
|
|
||||||
|
void main() async {
|
||||||
|
print('E2EE Worker created');
|
||||||
|
|
||||||
|
if (js_util.getProperty(self, 'RTCTransformEvent') != null) {
|
||||||
|
print('setup transform event handler');
|
||||||
|
self.onrtctransform = allowInterop((event) {
|
||||||
|
print('got transform event');
|
||||||
|
var transformer = (event as RTCTransformEvent).transformer;
|
||||||
|
transformer.handled = true;
|
||||||
|
var options = transformer.options;
|
||||||
|
var kind = options.kind;
|
||||||
|
var participantId = options.participantId;
|
||||||
|
var trackId = options.trackId;
|
||||||
|
var codec = options.codec;
|
||||||
|
var msgType = options.msgType;
|
||||||
|
|
||||||
|
var cryptor =
|
||||||
|
participantCryptors.firstWhereOrNull((c) => c.trackId == trackId);
|
||||||
|
|
||||||
|
if (cryptor == null) {
|
||||||
|
cryptor = FrameCryptor(
|
||||||
|
worker: self,
|
||||||
|
participantId: participantId,
|
||||||
|
trackId: trackId,
|
||||||
|
keyOptions: keyProviderOptions,
|
||||||
|
);
|
||||||
|
participantCryptors.add(cryptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
cryptor.setupTransform(
|
||||||
|
operation: msgType,
|
||||||
|
readable: transformer.readable,
|
||||||
|
writable: transformer.writable,
|
||||||
|
trackId: trackId,
|
||||||
|
kind: kind,
|
||||||
|
codec: codec);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
self.onMessage.listen((e) {
|
||||||
|
var msg = e.data;
|
||||||
|
var msgType = msg['msgType'];
|
||||||
|
switch (msgType) {
|
||||||
|
case 'init':
|
||||||
|
var options = msg['keyOptions'];
|
||||||
|
keyProviderOptions = KeyOptions(
|
||||||
|
sharedKey: options['sharedKey'],
|
||||||
|
ratchetSalt: Uint8List.fromList(
|
||||||
|
base64Decode(options['ratchetSalt'] as String)),
|
||||||
|
ratchetWindowSize: options['ratchetWindowSize'],
|
||||||
|
uncryptedMagicBytes: options['ratchetSalt'] != null
|
||||||
|
? Uint8List.fromList(
|
||||||
|
base64Decode(options['uncryptedMagicBytes'] as String))
|
||||||
|
: null);
|
||||||
|
print('worker: init with keyOptions ${keyProviderOptions.toString()}');
|
||||||
|
break;
|
||||||
|
case 'enable':
|
||||||
|
{
|
||||||
|
var enabled = msg['enabled'] as bool;
|
||||||
|
var participantId = msg['participantId'] as String;
|
||||||
|
print('worker: set enable $enabled for participantId $participantId');
|
||||||
|
var cryptors = participantCryptors
|
||||||
|
.where((c) => c.participantId == participantId)
|
||||||
|
.toList();
|
||||||
|
for (var cryptor in cryptors) {
|
||||||
|
cryptor.setEnabled(enabled);
|
||||||
|
}
|
||||||
|
self.postMessage({
|
||||||
|
'type': 'cryptorEnabled',
|
||||||
|
'participantId': participantId,
|
||||||
|
'enable': enabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'decode':
|
||||||
|
case 'encode':
|
||||||
|
{
|
||||||
|
var kind = msg['kind'];
|
||||||
|
var exist = msg['exist'] as bool;
|
||||||
|
var participantId = msg['participantId'] as String;
|
||||||
|
var trackId = msg['trackId'];
|
||||||
|
var readable = msg['readableStream'] as ReadableStream;
|
||||||
|
var writable = msg['writableStream'] as WritableStream;
|
||||||
|
|
||||||
|
print(
|
||||||
|
'worker: got $msgType, kind $kind, trackId $trackId, participantId $participantId, ${readable.runtimeType} ${writable.runtimeType}}');
|
||||||
|
var cryptor =
|
||||||
|
participantCryptors.firstWhereOrNull((c) => c.trackId == trackId);
|
||||||
|
|
||||||
|
if (cryptor == null) {
|
||||||
|
cryptor = FrameCryptor(
|
||||||
|
worker: self,
|
||||||
|
participantId: participantId,
|
||||||
|
trackId: trackId,
|
||||||
|
keyOptions: keyProviderOptions);
|
||||||
|
participantCryptors.add(cryptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!exist) {
|
||||||
|
cryptor.setupTransform(
|
||||||
|
operation: msgType,
|
||||||
|
readable: readable,
|
||||||
|
writable: writable,
|
||||||
|
trackId: trackId,
|
||||||
|
kind: kind);
|
||||||
|
}
|
||||||
|
cryptor.setParticipantId(participantId);
|
||||||
|
self.postMessage({
|
||||||
|
'type': 'cryptorSetup',
|
||||||
|
'participantId': participantId,
|
||||||
|
'trackId': trackId,
|
||||||
|
'exist': exist,
|
||||||
|
'operation': msgType,
|
||||||
|
});
|
||||||
|
cryptor.lastError = CryptorError.kNew;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'removeTransform':
|
||||||
|
{
|
||||||
|
var trackId = msg['trackId'] as String;
|
||||||
|
print('worker: removing trackId $trackId');
|
||||||
|
participantCryptors.removeWhere((c) => c.trackId == trackId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'setKey':
|
||||||
|
{
|
||||||
|
var key = Uint8List.fromList(base64Decode(msg['key'] as String));
|
||||||
|
var keyIndex = msg['keyIndex'];
|
||||||
|
//print('worker: got setKey ${msg['key']}, key $key');
|
||||||
|
var participantId = msg['participantId'] as String;
|
||||||
|
print('worker: setup key for participant $participantId');
|
||||||
|
|
||||||
|
if (keyProviderOptions.sharedKey) {
|
||||||
|
for (var c in participantCryptors) {
|
||||||
|
c.setKey(keyIndex, key);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var cryptors = participantCryptors
|
||||||
|
.where((c) => c.participantId == participantId)
|
||||||
|
.toList();
|
||||||
|
for (var c in cryptors) {
|
||||||
|
c.setKey(keyIndex, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'ratchetKey':
|
||||||
|
{
|
||||||
|
var keyIndex = msg['keyIndex'];
|
||||||
|
var participantId = msg['participantId'] as String;
|
||||||
|
print(
|
||||||
|
'worker: ratchetKey for participant $participantId, keyIndex $keyIndex');
|
||||||
|
var cryptors = participantCryptors
|
||||||
|
.where((c) => c.participantId == participantId)
|
||||||
|
.toList();
|
||||||
|
for (var c in cryptors) {
|
||||||
|
var keySet = c.getKeySet(keyIndex);
|
||||||
|
c.ratchetKey(keyIndex).then((_) async {
|
||||||
|
var newKey = await c.ratchet(
|
||||||
|
keySet!.material, keyProviderOptions.ratchetSalt);
|
||||||
|
self.postMessage({
|
||||||
|
'type': 'ratchetKey',
|
||||||
|
'participantId': participantId,
|
||||||
|
'trackId': c.trackId,
|
||||||
|
'key': base64Encode(newKey),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'setKeyIndex':
|
||||||
|
{
|
||||||
|
var keyIndex = msg['index'];
|
||||||
|
var participantId = msg['participantId'] as String;
|
||||||
|
print('worker: setup key index for participant $participantId');
|
||||||
|
var cryptors = participantCryptors
|
||||||
|
.where((c) => c.participantId == participantId)
|
||||||
|
.toList();
|
||||||
|
for (var c in cryptors) {
|
||||||
|
c.setKeyIndex(keyIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'updateCodec':
|
||||||
|
{
|
||||||
|
var codec = msg['codec'] as String;
|
||||||
|
var trackId = msg['trackId'] as String;
|
||||||
|
print('worker: update codec for trackId $trackId, codec $codec');
|
||||||
|
var cryptor =
|
||||||
|
participantCryptors.firstWhereOrNull((c) => c.trackId == trackId);
|
||||||
|
cryptor?.updateCodec(codec);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'dispose':
|
||||||
|
{
|
||||||
|
var trackId = msg['trackId'] as String;
|
||||||
|
print('worker: dispose trackId $trackId');
|
||||||
|
var cryptor =
|
||||||
|
participantCryptors.firstWhereOrNull((c) => c.trackId == trackId);
|
||||||
|
if (cryptor != null) {
|
||||||
|
cryptor.lastError = CryptorError.kDisposed;
|
||||||
|
self.postMessage({
|
||||||
|
'type': 'cryptorDispose',
|
||||||
|
'participantId': cryptor.participantId,
|
||||||
|
'trackId': trackId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
print('worker: unknown message kind $msg');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user