From 07ce3c03e76f136ee6fca4768e8965b611621681 Mon Sep 17 00:00:00 2001 From: Tobias Ollmann Date: Mon, 10 Feb 2025 16:55:48 +0100 Subject: [PATCH 01/23] fix: don't try to access optional track settings Some properties defined in `package:web/web.dart` are defined as non-nullable, while some browsers don't support them. In my case, `aspectRatio` is not defined for [Firefox](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSettings/aspectRatio#browser_compatibility). This seems to be a problem with the IDL itself being inconsistent (see https://github.com/dart-lang/web/issues/309). Checking availability before accessing the fields seems to be an endorsed method: https://github.com/dart-lang/web/issues/181#issuecomment-1955207827 --- lib/src/media_stream_track_impl.dart | 56 ++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/lib/src/media_stream_track_impl.dart b/lib/src/media_stream_track_impl.dart index d52066a..03d959d 100644 --- a/lib/src/media_stream_track_impl.dart +++ b/lib/src/media_stream_track_impl.dart @@ -1,5 +1,7 @@ import 'dart:async'; import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; +import 'dart:js_util' as js; import 'dart:typed_data'; import 'package:web/web.dart' as web; @@ -77,25 +79,49 @@ class MediaStreamTrackWeb extends MediaStreamTrack { var settings = jsTrack.getSettings(); var _converted = {}; if (kind == 'audio') { - _converted['sampleRate'] = settings.sampleRate; - _converted['sampleSize'] = settings.sampleSize; - _converted['echoCancellation'] = settings.echoCancellation; - _converted['autoGainControl'] = settings.autoGainControl; - _converted['noiseSuppression'] = settings.noiseSuppression; - _converted['latency'] = settings.latency; - _converted['channelCount'] = settings.channelCount; + if (settings.has('sampleRate')) { + _converted['sampleRate'] = settings.sampleRate; + } + if (settings.has('sampleSize')) { + _converted['sampleSize'] = settings.sampleSize; + } + if (settings.has('echoCancellation')) { + _converted['echoCancellation'] = settings.echoCancellation; + } + if (settings.has('autoGainControl')) { + _converted['autoGainControl'] = settings.autoGainControl; + } + if (settings.has('noiseSuppression')) { + _converted['noiseSuppression'] = settings.noiseSuppression; + } + if (settings.has('latency')) _converted['latency'] = settings.latency; + if (settings.has('channelCount')) { + _converted['channelCount'] = settings.channelCount; + } } else { - _converted['width'] = settings.width; - _converted['height'] = settings.height; - _converted['aspectRatio'] = settings.aspectRatio; - _converted['frameRate'] = settings.frameRate; - if (isMobile) { + if (settings.has('width')) { + _converted['width'] = settings.width; + } + if (settings.has('height')) { + _converted['height'] = settings.height; + } + if (settings.has('aspectRatio')) { + _converted['aspectRatio'] = settings.aspectRatio; + } + if (settings.has('frameRate')) { + _converted['frameRate'] = settings.frameRate; + } + if (isMobile && settings.has('facingMode')) { _converted['facingMode'] = settings.facingMode; } - _converted['resizeMode'] = settings.resizeMode; + if (settings.has('resizeMode')) { + _converted['resizeMode'] = settings.resizeMode; + } + } + if (settings.has('deviceId')) _converted['deviceId'] = settings.deviceId; + if (settings.has('groupId')) { + _converted['groupId'] = settings.groupId; } - _converted['deviceId'] = settings.deviceId; - _converted['groupId'] = settings.groupId; return _converted; } From fc82d90fd4900404cdc0740c60014f181ab652c9 Mon Sep 17 00:00:00 2001 From: Tobias Ollmann Date: Mon, 17 Feb 2025 11:59:37 +0100 Subject: [PATCH 02/23] fix: remove unused import --- lib/src/media_stream_track_impl.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/media_stream_track_impl.dart b/lib/src/media_stream_track_impl.dart index 03d959d..fb16d46 100644 --- a/lib/src/media_stream_track_impl.dart +++ b/lib/src/media_stream_track_impl.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:js_interop'; import 'dart:js_interop_unsafe'; -import 'dart:js_util' as js; import 'dart:typed_data'; import 'package:web/web.dart' as web; From 14ca0f7a4e81afb784ed24a2f928203b4863e9ac Mon Sep 17 00:00:00 2001 From: daniel-g-favoreto-opl Date: Fri, 4 Apr 2025 17:19:58 -0300 Subject: [PATCH 03/23] change media recorder --- lib/src/media_recorder.dart | 18 +++++++++++++++--- lib/src/media_recorder_impl.dart | 12 +++++++++--- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/lib/src/media_recorder.dart b/lib/src/media_recorder.dart index 7ed9dd3..c7028d2 100644 --- a/lib/src/media_recorder.dart +++ b/lib/src/media_recorder.dart @@ -7,12 +7,18 @@ class MediaRecorder extends _interface.MediaRecorder { final _interface.MediaRecorder _delegate; @override - Future start(String path, - {MediaStreamTrack? videoTrack, RecorderAudioChannel? audioChannel}) => + Future start( + String path, { + MediaStreamTrack? videoTrack, + RecorderAudioChannel? audioChannel, + MediaStreamTrack? audioTrack, + int rotationDegrees = 0, + }) => _delegate.start(path, videoTrack: videoTrack, audioChannel: audioChannel); @override - Future stop() => _delegate.stop(); + Future stop({String? albumName}) => + _delegate.stop(albumName: albumName ?? 'FlutterWebRtc'); @override void startWeb( @@ -27,4 +33,10 @@ class MediaRecorder extends _interface.MediaRecorder { mimeType: mimeType ?? 'video/webm', timeSlice: timeSlice, ); + + @override + Future changeVideoTrack(MediaStreamTrack videoTrack) { + // TODO: implement changeVideoTrack + throw UnimplementedError(); + } } diff --git a/lib/src/media_recorder_impl.dart b/lib/src/media_recorder_impl.dart index c03a9bc..14a12cd 100644 --- a/lib/src/media_recorder_impl.dart +++ b/lib/src/media_recorder_impl.dart @@ -15,9 +15,9 @@ class MediaRecorderWeb extends MediaRecorder { Future start( String path, { MediaStreamTrack? videoTrack, - MediaStreamTrack? audioTrack, RecorderAudioChannel? audioChannel, - int? rotation, + MediaStreamTrack? audioTrack, + int rotationDegrees = 0, }) { throw 'Use startWeb on Flutter Web!'; } @@ -64,8 +64,14 @@ class MediaRecorderWeb extends MediaRecorder { } @override - Future stop() { + Future stop({String? albumName}) { _recorder.stop(); return _completer.future; } + + @override + Future changeVideoTrack(MediaStreamTrack videoTrack) { + // TODO: implement changeVideoTrack + throw UnimplementedError(); + } } From 700d8d6d445f17d2f0a5d17213d275a538042810 Mon Sep 17 00:00:00 2001 From: daniel-g-favoreto-opl Date: Fri, 4 Apr 2025 17:31:19 -0300 Subject: [PATCH 04/23] add throw message --- lib/src/media_recorder.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/media_recorder.dart b/lib/src/media_recorder.dart index c7028d2..1e4f1b1 100644 --- a/lib/src/media_recorder.dart +++ b/lib/src/media_recorder.dart @@ -36,7 +36,6 @@ class MediaRecorder extends _interface.MediaRecorder { @override Future changeVideoTrack(MediaStreamTrack videoTrack) { - // TODO: implement changeVideoTrack - throw UnimplementedError(); + throw 'Unimplemented on Web'; } } From e7f14ebb6351a33634c90da173240d73c624a9cb Mon Sep 17 00:00:00 2001 From: daniel-g-favoreto-opl Date: Thu, 17 Apr 2025 08:41:36 -0300 Subject: [PATCH 05/23] review changes --- lib/src/media_recorder.dart | 7 ------- lib/src/media_recorder_impl.dart | 8 -------- 2 files changed, 15 deletions(-) diff --git a/lib/src/media_recorder.dart b/lib/src/media_recorder.dart index 1e4f1b1..82880d1 100644 --- a/lib/src/media_recorder.dart +++ b/lib/src/media_recorder.dart @@ -11,8 +11,6 @@ class MediaRecorder extends _interface.MediaRecorder { String path, { MediaStreamTrack? videoTrack, RecorderAudioChannel? audioChannel, - MediaStreamTrack? audioTrack, - int rotationDegrees = 0, }) => _delegate.start(path, videoTrack: videoTrack, audioChannel: audioChannel); @@ -33,9 +31,4 @@ class MediaRecorder extends _interface.MediaRecorder { mimeType: mimeType ?? 'video/webm', timeSlice: timeSlice, ); - - @override - Future changeVideoTrack(MediaStreamTrack videoTrack) { - throw 'Unimplemented on Web'; - } } diff --git a/lib/src/media_recorder_impl.dart b/lib/src/media_recorder_impl.dart index 14a12cd..f10b5a5 100644 --- a/lib/src/media_recorder_impl.dart +++ b/lib/src/media_recorder_impl.dart @@ -16,8 +16,6 @@ class MediaRecorderWeb extends MediaRecorder { String path, { MediaStreamTrack? videoTrack, RecorderAudioChannel? audioChannel, - MediaStreamTrack? audioTrack, - int rotationDegrees = 0, }) { throw 'Use startWeb on Flutter Web!'; } @@ -68,10 +66,4 @@ class MediaRecorderWeb extends MediaRecorder { _recorder.stop(); return _completer.future; } - - @override - Future changeVideoTrack(MediaStreamTrack videoTrack) { - // TODO: implement changeVideoTrack - throw UnimplementedError(); - } } From a76eebb277f40ad98313f21a693d5e66d93e6990 Mon Sep 17 00:00:00 2001 From: cloudwebrtc Date: Fri, 25 Apr 2025 09:54:59 +0800 Subject: [PATCH 06/23] hotfix: Add getter override for dc.bufferedAmountLowThreshold. --- CHANGELOG.md | 3 +++ lib/src/rtc_data_channel_impl.dart | 3 +++ pubspec.yaml | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e02c3c7..f0e8e6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog -------------------------------------------- +[1.5.3+hotfix.1] - 2025-04-25 + +* add getter override for dc.bufferedAmountLowThreshold. [1.5.3] - 2025-03-24 diff --git a/lib/src/rtc_data_channel_impl.dart b/lib/src/rtc_data_channel_impl.dart index f41be4e..864c7b3 100644 --- a/lib/src/rtc_data_channel_impl.dart +++ b/lib/src/rtc_data_channel_impl.dart @@ -53,6 +53,9 @@ class RTCDataChannelWeb extends RTCDataChannel { return _jsDc.bufferedAmount; } + @override + int? get bufferedAmountLowThreshold => _jsDc.bufferedAmountLowThreshold; + @override set bufferedAmountLowThreshold(int? bufferedAmountLowThreshold) { _jsDc.bufferedAmountLowThreshold = bufferedAmountLowThreshold ?? 0; diff --git a/pubspec.yaml b/pubspec.yaml index 910244b..9074d90 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: dart_webrtc description: Use the dart/js library to re-wrap the webrtc js interface of the browser, to adapted common browsers. -version: 1.5.3 +version: 1.5.3+hotfix.1 homepage: https://github.com/flutter-webrtc/dart-webrtc environment: From 6507c593357d7667e669d7c1b05133febc98c27a Mon Sep 17 00:00:00 2001 From: cloudwebrtc Date: Fri, 25 Apr 2025 11:47:31 +0800 Subject: [PATCH 07/23] fix: fix bug for dc.onMessage. --- CHANGELOG.md | 4 ++++ lib/src/rtc_data_channel_impl.dart | 13 ++++++------- pubspec.yaml | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0e8e6b..4961ea7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog -------------------------------------------- +[1.5.3+hotfix.2] - 2025-04-25 + +* fix bug for dc.onMessage. + [1.5.3+hotfix.1] - 2025-04-25 * add getter override for dc.bufferedAmountLowThreshold. diff --git a/lib/src/rtc_data_channel_impl.dart b/lib/src/rtc_data_channel_impl.dart index 864c7b3..c3b1bd2 100644 --- a/lib/src/rtc_data_channel_impl.dart +++ b/lib/src/rtc_data_channel_impl.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:js_interop'; +import 'dart:typed_data'; import 'package:web/web.dart' as web; import 'package:webrtc_interface/webrtc_interface.dart'; @@ -70,15 +71,13 @@ class RTCDataChannelWeb extends RTCDataChannel { if (data is String) { return RTCDataChannelMessage(data); } - dynamic arrayBuffer; - if (data is JSArrayBuffer) { - arrayBuffer = data.toDart; + if (data is ByteBuffer) { + return RTCDataChannelMessage.fromBinary(data.asUint8List()); } else if (data is web.Blob) { - arrayBuffer = await data.arrayBuffer().toDart; - } else { - arrayBuffer = data.toDart; + final arrayBuffer = await data.arrayBuffer().toDart; + return RTCDataChannelMessage.fromBinary(arrayBuffer.toDart.asUint8List()); } - return RTCDataChannelMessage.fromBinary(arrayBuffer.asUint8List()); + return RTCDataChannelMessage.fromBinary(Uint8List(0)); } @override diff --git a/pubspec.yaml b/pubspec.yaml index 9074d90..49b8637 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: dart_webrtc description: Use the dart/js library to re-wrap the webrtc js interface of the browser, to adapted common browsers. -version: 1.5.3+hotfix.1 +version: 1.5.3+hotfix.2 homepage: https://github.com/flutter-webrtc/dart-webrtc environment: From 79f566fe1e0b5f051522f92f24ec1fb10786355e Mon Sep 17 00:00:00 2001 From: cloudwebrtc Date: Tue, 29 Apr 2025 13:25:58 +0800 Subject: [PATCH 08/23] release: 1.5.4. --- CHANGELOG.md | 4 ++++ pubspec.yaml | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4961ea7..0692f7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog -------------------------------------------- +[1.5.4] - 2025-04-29 + +* Media recording changes. + [1.5.3+hotfix.2] - 2025-04-25 * fix bug for dc.onMessage. diff --git a/pubspec.yaml b/pubspec.yaml index 49b8637..302d97a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: dart_webrtc description: Use the dart/js library to re-wrap the webrtc js interface of the browser, to adapted common browsers. -version: 1.5.3+hotfix.2 +version: 1.5.4 homepage: https://github.com/flutter-webrtc/dart-webrtc environment: @@ -13,7 +13,7 @@ dependencies: meta: ^1.8.0 synchronized: ^3.0.0+3 web: ^1.0.0 - webrtc_interface: ^1.2.2+hotfix.1 + webrtc_interface: ^1.2.3 dev_dependencies: build_runner: ^2.3.3 From 233f617988cf507c6d3b1fc675b9edbfb713f4a3 Mon Sep 17 00:00:00 2001 From: cloudwebrtc Date: Tue, 6 May 2025 11:54:47 +0800 Subject: [PATCH 09/23] revert destructive changes. --- lib/src/media_recorder.dart | 3 +-- lib/src/media_recorder_impl.dart | 2 +- pubspec.yaml | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/src/media_recorder.dart b/lib/src/media_recorder.dart index 82880d1..5a2a4e6 100644 --- a/lib/src/media_recorder.dart +++ b/lib/src/media_recorder.dart @@ -15,8 +15,7 @@ class MediaRecorder extends _interface.MediaRecorder { _delegate.start(path, videoTrack: videoTrack, audioChannel: audioChannel); @override - Future stop({String? albumName}) => - _delegate.stop(albumName: albumName ?? 'FlutterWebRtc'); + Future stop() => _delegate.stop(); @override void startWeb( diff --git a/lib/src/media_recorder_impl.dart b/lib/src/media_recorder_impl.dart index f10b5a5..38bc50e 100644 --- a/lib/src/media_recorder_impl.dart +++ b/lib/src/media_recorder_impl.dart @@ -62,7 +62,7 @@ class MediaRecorderWeb extends MediaRecorder { } @override - Future stop({String? albumName}) { + Future stop() { _recorder.stop(); return _completer.future; } diff --git a/pubspec.yaml b/pubspec.yaml index 302d97a..542b26d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: meta: ^1.8.0 synchronized: ^3.0.0+3 web: ^1.0.0 - webrtc_interface: ^1.2.3 + webrtc_interface: ^1.2.2+hotfix.2 dev_dependencies: build_runner: ^2.3.3 From 1a0a38cf71edddb4106a3f3562069f3dadad5b57 Mon Sep 17 00:00:00 2001 From: Tobias Ollmann Date: Tue, 8 Jul 2025 11:18:36 +0200 Subject: [PATCH 10/23] fix: export MediaStreamTrack --- lib/dart_webrtc.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/dart_webrtc.dart b/lib/dart_webrtc.dart index 744176d..14b78b7 100644 --- a/lib/dart_webrtc.dart +++ b/lib/dart_webrtc.dart @@ -7,4 +7,5 @@ export 'src/factory_impl.dart'; export 'src/media_devices.dart'; export 'src/media_recorder.dart'; export 'src/media_stream_impl.dart'; +export 'src/media_stream_track_impl.dart'; export 'src/rtc_video_element.dart'; From 3e5cf304863199b27898f86e5f47695dc1b10c60 Mon Sep 17 00:00:00 2001 From: cloudwebrtc Date: Thu, 24 Jul 2025 21:08:37 +0800 Subject: [PATCH 11/23] disable getStats logs. --- web/main.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/main.dart b/web/main.dart index cf1cd78..0ddcb24 100644 --- a/web/main.dart +++ b/web/main.dart @@ -254,7 +254,7 @@ void loopBackTest() async { key: Uint8List.fromList('testkey2'.codeUnits)); */ - + /* Timer.periodic(Duration(seconds: 1), (timer) async { var senders = await pc1.getSenders(); var receivers = await pc2.getReceivers(); @@ -276,4 +276,5 @@ void loopBackTest() async { }); }); }); + */ } From 2421e880d8bb062935d51e03965f0813305d7e3d Mon Sep 17 00:00:00 2001 From: cloudwebrtc Date: Thu, 24 Jul 2025 22:03:52 +0800 Subject: [PATCH 12/23] release: 1.5.3+hotfix.3. --- CHANGELOG.md | 4 + lib/src/frame_cryptor_impl.dart | 177 +++++++++++--------------------- pubspec.yaml | 4 +- web/main.dart | 3 +- 4 files changed, 66 insertions(+), 122 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0692f7e..dfe286e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ * Media recording changes. +[1.5.3+hotfix.3] - 2025-07-24 + +* fixed E2EE issue for Chrome. + [1.5.3+hotfix.2] - 2025-04-25 * fix bug for dc.onMessage. diff --git a/lib/src/frame_cryptor_impl.dart b/lib/src/frame_cryptor_impl.dart index e8365f7..6020d9d 100644 --- a/lib/src/frame_cryptor_impl.dart +++ b/lib/src/frame_cryptor_impl.dart @@ -20,70 +20,6 @@ class WorkerResponse { dynamic data; } -extension RtcRtpReceiverExt on web.RTCRtpReceiver { - static Map readableStreams_ = {}; - static Map writableStreams_ = {}; - - web.ReadableStream? get readable { - if (readableStreams_.containsKey(hashCode)) { - return readableStreams_[hashCode]!; - } - return null; - } - - web.WritableStream? get writable { - if (writableStreams_.containsKey(hashCode)) { - return writableStreams_[hashCode]!; - } - return null; - } - - set readableStream(web.ReadableStream stream) { - readableStreams_[hashCode] = stream; - } - - set writableStream(web.WritableStream stream) { - writableStreams_[hashCode] = stream; - } - - void closeStreams() { - readableStreams_.remove(hashCode); - writableStreams_.remove(hashCode); - } -} - -extension RtcRtpSenderExt on web.RTCRtpSender { - static Map readableStreams_ = {}; - static Map writableStreams_ = {}; - - web.ReadableStream? get readable { - if (readableStreams_.containsKey(hashCode)) { - return readableStreams_[hashCode]!; - } - return null; - } - - web.WritableStream? get writable { - if (writableStreams_.containsKey(hashCode)) { - return writableStreams_[hashCode]!; - } - return null; - } - - set readableStream(web.ReadableStream stream) { - readableStreams_[hashCode] = stream; - } - - set writableStream(web.WritableStream stream) { - writableStreams_[hashCode] = stream; - } - - void closeStreams() { - readableStreams_.remove(hashCode); - writableStreams_.remove(hashCode); - } -} - class FrameCryptorImpl extends FrameCryptor { FrameCryptorImpl( this._factory, this.worker, this._participantId, this._trackId, @@ -452,7 +388,7 @@ class FrameCryptorFactoryImpl implements FrameCryptorFactory { required KeyProvider keyProvider}) { var jsReceiver = (receiver as RTCRtpReceiverWeb).jsRtpReceiver; - var trackId = jsReceiver.hashCode.toString(); + var trackId = jsReceiver.track.id; var kind = jsReceiver.track.kind; if (web.window.hasProperty('RTCRtpScriptTransform'.toJS).toDart) { @@ -469,33 +405,35 @@ class FrameCryptorFactoryImpl implements FrameCryptorFactory { jsReceiver.transform = web.RTCRtpScriptTransform(worker, options.jsify()); } else { - var writable = jsReceiver.writable; - var readable = jsReceiver.readable; - var exist = true; - if (writable == null || readable == null) { - final streams = - jsReceiver.callMethod('createEncodedStreams'.toJS); - readable = streams.getProperty('readable'.toJS) as web.ReadableStream; - jsReceiver.readableStream = readable; - writable = streams.getProperty('writable'.toJS) as web.WritableStream; - jsReceiver.writableStream = writable; - exist = false; - } + var exist = false; + final streams = + jsReceiver.callMethod('createEncodedStreams'.toJS); + final readable = + streams.getProperty('readable'.toJS) as web.ReadableStream; + final writable = + streams.getProperty('writable'.toJS) as web.WritableStream; + var msgId = randomString(12); - worker.postMessage( - { - 'msgType': 'decode', - 'msgId': msgId, - 'keyProviderId': (keyProvider as KeyProviderImpl).id, - 'kind': kind, - 'exist': exist, - 'participantId': participantId, - 'trackId': trackId, - 'readableStream': readable, - 'writableStream': writable - }.jsify(), - [readable, writable].jsify() as JSObject, - ); + try { + worker.postMessage( + { + 'msgType': 'decode', + 'msgId': msgId, + 'keyProviderId': (keyProvider as KeyProviderImpl).id, + 'kind': kind, + 'exist': exist, + 'participantId': participantId, + 'trackId': trackId, + 'options': keyProvider.options.toJson(), + 'readableStream': readable, + 'writableStream': writable + }.jsify(), + [readable, writable] as JSObject, + ); + } catch (e) { + print('Error posting message: $e'); + rethrow; + } } FrameCryptor cryptor = FrameCryptorImpl( this, worker, participantId, trackId, @@ -529,39 +467,42 @@ class FrameCryptorFactoryImpl implements FrameCryptorFactory { print('object: ${options['keyProviderId']}'); jsSender.transform = web.RTCRtpScriptTransform(worker, options.jsify()); } else { - var writable = jsSender.writable; - var readable = jsSender.readable; - var exist = true; - if (writable == null || readable == null) { - final streams = - jsSender.callMethod('createEncodedStreams'.toJS); - readable = streams.getProperty('readable'.toJS) as web.ReadableStream; - jsSender.readableStream = readable; - writable = streams.getProperty('writable'.toJS) as web.WritableStream; - - exist = false; - } + var exist = false; + final streams = + jsSender.callMethod('createEncodedStreams'.toJS); + final readable = + streams.getProperty('readable'.toJS) as web.ReadableStream; + final writable = + streams.getProperty('writable'.toJS) as web.WritableStream; + var msgId = randomString(12); - worker.postMessage( - { - 'msgType': 'encode', - 'msgId': msgId, - 'keyProviderId': (keyProvider as KeyProviderImpl).id, - 'kind': kind, - 'exist': exist, - 'participantId': participantId, - 'trackId': trackId, - 'options': keyProvider.options.toJson(), - 'readableStream': readable, - 'writableStream': writable - }.jsify(), - [readable, writable].jsify() as JSObject, - ); + + try { + worker.postMessage( + { + 'msgType': 'encode', + 'msgId': msgId, + 'keyProviderId': (keyProvider as KeyProviderImpl).id, + 'kind': kind, + 'exist': exist, + 'participantId': participantId, + 'trackId': trackId, + 'options': keyProvider.options.toJson(), + 'readableStream': readable, + 'writableStream': writable + }.jsify(), + [readable, writable] as JSObject, + ); + } catch (e) { + print('Error posting message: $e'); + rethrow; + } } FrameCryptor cryptor = FrameCryptorImpl( this, worker, participantId, trackId, jsSender: jsSender, keyProvider: keyProvider); _frameCryptors[trackId] = cryptor; + return Future.value(cryptor); } diff --git a/pubspec.yaml b/pubspec.yaml index 542b26d..5507ef7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: dart_webrtc description: Use the dart/js library to re-wrap the webrtc js interface of the browser, to adapted common browsers. -version: 1.5.4 +version: 1.5.3+hotfix.3 homepage: https://github.com/flutter-webrtc/dart-webrtc environment: @@ -22,4 +22,4 @@ dev_dependencies: import_sorter: ^4.6.0 pedantic: ^1.9.0 protoo_client: ^0.3.0 - test: ^1.15.4 \ No newline at end of file + test: ^1.15.4 diff --git a/web/main.dart b/web/main.dart index 0ddcb24..cf1cd78 100644 --- a/web/main.dart +++ b/web/main.dart @@ -254,7 +254,7 @@ void loopBackTest() async { key: Uint8List.fromList('testkey2'.codeUnits)); */ - /* + Timer.periodic(Duration(seconds: 1), (timer) async { var senders = await pc1.getSenders(); var receivers = await pc2.getReceivers(); @@ -276,5 +276,4 @@ void loopBackTest() async { }); }); }); - */ } From 4e068450a6d61d999c6c1925d067cad6e3d4f836 Mon Sep 17 00:00:00 2001 From: cloudwebrtc Date: Tue, 29 Jul 2025 00:49:35 +0800 Subject: [PATCH 13/23] release: 1.5.4+hotfix.4 --- CHANGELOG.md | 4 ++-- lib/src/frame_cryptor_impl.dart | 2 +- pubspec.yaml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfe286e..e74d924 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,9 @@ # Changelog -------------------------------------------- -[1.5.4] - 2025-04-29 +[1.5.3+hotfix.4] - 2025-07-29 -* Media recording changes. +* fixed E2EE issue for Chrome. [1.5.3+hotfix.3] - 2025-07-24 diff --git a/lib/src/frame_cryptor_impl.dart b/lib/src/frame_cryptor_impl.dart index 6020d9d..eb449e7 100644 --- a/lib/src/frame_cryptor_impl.dart +++ b/lib/src/frame_cryptor_impl.dart @@ -449,7 +449,7 @@ class FrameCryptorFactoryImpl implements FrameCryptorFactory { required Algorithm algorithm, required KeyProvider keyProvider}) { var jsSender = (sender as RTCRtpSenderWeb).jsRtpSender; - var trackId = jsSender.hashCode.toString(); + var trackId = jsSender.track?.id ?? sender.senderId; var kind = jsSender.track!.kind; if (web.window.hasProperty('RTCRtpScriptTransform'.toJS).toDart) { diff --git a/pubspec.yaml b/pubspec.yaml index 5507ef7..835c297 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: dart_webrtc description: Use the dart/js library to re-wrap the webrtc js interface of the browser, to adapted common browsers. -version: 1.5.3+hotfix.3 +version: 1.5.3+hotfix.4 homepage: https://github.com/flutter-webrtc/dart-webrtc environment: From cf0d135c5c7319058c3aa11df29973b558cf12bc Mon Sep 17 00:00:00 2001 From: cloudwebrtc Date: Mon, 11 Aug 2025 16:01:25 +0800 Subject: [PATCH 14/23] fix: E2EE bug in chrome on rejoin. --- lib/src/frame_cryptor_impl.dart | 50 +++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/lib/src/frame_cryptor_impl.dart b/lib/src/frame_cryptor_impl.dart index eb449e7..b1fd504 100644 --- a/lib/src/frame_cryptor_impl.dart +++ b/lib/src/frame_cryptor_impl.dart @@ -405,14 +405,21 @@ class FrameCryptorFactoryImpl implements FrameCryptorFactory { jsReceiver.transform = web.RTCRtpScriptTransform(worker, options.jsify()); } else { - var exist = false; - final streams = - jsReceiver.callMethod('createEncodedStreams'.toJS); - final readable = - streams.getProperty('readable'.toJS) as web.ReadableStream; - final writable = - streams.getProperty('writable'.toJS) as web.WritableStream; - + var writable = + jsReceiver.getProperty('writable'.toJS) as web.WritableStream?; + var readable = + jsReceiver.getProperty('readable'.toJS) as web.ReadableStream?; + var exist = true; + if (writable == null || readable == null) { + final streams = jsReceiver.callMethod( + 'createEncodedStreams'.toJS, + ); + readable = streams.getProperty('readable'.toJS) as web.ReadableStream; + jsReceiver.setProperty('readable'.toJS, readable); + writable = streams.getProperty('writable'.toJS) as web.WritableStream; + jsReceiver.setProperty('writable'.toJS, writable); + exist = false; + } var msgId = randomString(12); try { worker.postMessage( @@ -426,7 +433,7 @@ class FrameCryptorFactoryImpl implements FrameCryptorFactory { 'trackId': trackId, 'options': keyProvider.options.toJson(), 'readableStream': readable, - 'writableStream': writable + 'writableStream': writable, }.jsify(), [readable, writable] as JSObject, ); @@ -467,14 +474,21 @@ class FrameCryptorFactoryImpl implements FrameCryptorFactory { print('object: ${options['keyProviderId']}'); jsSender.transform = web.RTCRtpScriptTransform(worker, options.jsify()); } else { - var exist = false; - final streams = - jsSender.callMethod('createEncodedStreams'.toJS); - final readable = - streams.getProperty('readable'.toJS) as web.ReadableStream; - final writable = - streams.getProperty('writable'.toJS) as web.WritableStream; - + var writable = + jsSender.getProperty('writable'.toJS) as web.WritableStream?; + var readable = + jsSender.getProperty('readable'.toJS) as web.ReadableStream?; + var exist = true; + if (writable == null || readable == null) { + final streams = jsSender.callMethod( + 'createEncodedStreams'.toJS, + ); + readable = streams.getProperty('readable'.toJS) as web.ReadableStream; + jsSender.setProperty('readable'.toJS, readable); + writable = streams.getProperty('writable'.toJS) as web.WritableStream; + jsSender.setProperty('writable'.toJS, writable); + exist = false; + } var msgId = randomString(12); try { @@ -489,7 +503,7 @@ class FrameCryptorFactoryImpl implements FrameCryptorFactory { 'trackId': trackId, 'options': keyProvider.options.toJson(), 'readableStream': readable, - 'writableStream': writable + 'writableStream': writable, }.jsify(), [readable, writable] as JSObject, ); From f55660c3e94006e1cfbae3e676ed301d687c0568 Mon Sep 17 00:00:00 2001 From: cloudwebrtc Date: Mon, 11 Aug 2025 18:22:12 +0800 Subject: [PATCH 15/23] update. --- lib/src/frame_cryptor_impl.dart | 438 ++++++++++++++++++-------------- 1 file changed, 252 insertions(+), 186 deletions(-) diff --git a/lib/src/frame_cryptor_impl.dart b/lib/src/frame_cryptor_impl.dart index b1fd504..4445f42 100644 --- a/lib/src/frame_cryptor_impl.dart +++ b/lib/src/frame_cryptor_impl.dart @@ -14,6 +14,11 @@ import 'rtc_rtp_receiver_impl.dart'; import 'rtc_rtp_sender_impl.dart'; import 'utils.dart'; +extension type RTCInsertableStreams._(JSObject _) implements JSObject { + external web.WritableStream get writable; + external web.ReadableStream get readable; +} + class WorkerResponse { WorkerResponse(this.msgId, this.data); String msgId; @@ -22,8 +27,14 @@ class WorkerResponse { class FrameCryptorImpl extends FrameCryptor { FrameCryptorImpl( - this._factory, this.worker, this._participantId, this._trackId, - {this.jsSender, this.jsReceiver, required this.keyProvider}); + this._factory, + this.worker, + this._participantId, + this._trackId, { + this.jsSender, + this.jsReceiver, + required this.keyProvider, + }); web.Worker worker; bool _enabled = false; int _keyIndex = 0; @@ -37,11 +48,10 @@ class FrameCryptorImpl extends FrameCryptor { @override Future dispose() async { var msgId = randomString(12); - worker.postMessage({ - 'msgType': 'dispose', - 'msgId': msgId, - 'trackId': _trackId, - }.jsify()); + worker.postMessage( + {'msgType': 'dispose', 'msgId': msgId, 'trackId': _trackId}.jsify(), + ); + _enabled = false; _factory.removeFrameCryptor(_trackId); return; } @@ -60,12 +70,14 @@ class FrameCryptorImpl extends FrameCryptor { @override Future setEnabled(bool enabled) async { var msgId = randomString(12); - worker.postMessage({ - 'msgType': 'enable', - 'msgId': msgId, - 'trackId': _trackId, - 'enabled': enabled - }.jsify()); + worker.postMessage( + { + 'msgType': 'enable', + 'msgId': msgId, + 'trackId': _trackId, + 'enabled': enabled, + }.jsify(), + ); _enabled = enabled; return true; } @@ -73,12 +85,14 @@ class FrameCryptorImpl extends FrameCryptor { @override Future setKeyIndex(int index) async { var msgId = randomString(12); - worker.postMessage({ - 'msgType': 'setKeyIndex', - 'msgId': msgId, - 'trackId': _trackId, - 'index': index, - }.jsify()); + worker.postMessage( + { + 'msgType': 'setKeyIndex', + 'msgId': msgId, + 'trackId': _trackId, + 'index': index, + }.jsify(), + ); _keyIndex = index; return true; } @@ -86,12 +100,14 @@ class FrameCryptorImpl extends FrameCryptor { @override Future updateCodec(String codec) async { var msgId = randomString(12); - worker.postMessage({ - 'msgType': 'updateCodec', - 'msgId': msgId, - 'trackId': _trackId, - 'codec': codec, - }.jsify()); + worker.postMessage( + { + 'msgType': 'updateCodec', + 'msgId': msgId, + 'trackId': _trackId, + 'codec': codec, + }.jsify(), + ); } } @@ -108,64 +124,73 @@ class KeyProviderImpl implements KeyProvider { Future init() async { var msgId = randomString(12); - worker.postMessage({ - 'msgType': 'keyProviderInit', - 'msgId': msgId, - 'keyProviderId': id, - 'keyOptions': { - 'sharedKey': options.sharedKey, - 'ratchetSalt': base64Encode(options.ratchetSalt), - 'ratchetWindowSize': options.ratchetWindowSize, - 'failureTolerance': options.failureTolerance, - if (options.uncryptedMagicBytes != null) - 'uncryptedMagicBytes': base64Encode(options.uncryptedMagicBytes!), - 'keyRingSize': options.keyRingSize, - 'discardFrameWhenCryptorNotReady': - options.discardFrameWhenCryptorNotReady, - }, - }.jsify()); + worker.postMessage( + { + 'msgType': 'keyProviderInit', + 'msgId': msgId, + 'keyProviderId': id, + 'keyOptions': { + 'sharedKey': options.sharedKey, + 'ratchetSalt': base64Encode(options.ratchetSalt), + 'ratchetWindowSize': options.ratchetWindowSize, + 'failureTolerance': options.failureTolerance, + if (options.uncryptedMagicBytes != null) + 'uncryptedMagicBytes': base64Encode(options.uncryptedMagicBytes!), + 'keyRingSize': options.keyRingSize, + 'discardFrameWhenCryptorNotReady': + options.discardFrameWhenCryptorNotReady, + }, + }.jsify(), + ); await events.waitFor( - filter: (event) { - logger.fine('waiting for init on msg: $msgId'); - return event.msgId == msgId; - }, - duration: Duration(seconds: 15)); + filter: (event) { + logger.fine('waiting for init on msg: $msgId'); + return event.msgId == msgId; + }, + duration: Duration(seconds: 15), + ); } @override Future dispose() async { var msgId = randomString(12); - worker.postMessage({ - 'msgType': 'keyProviderDispose', - 'msgId': msgId, - 'keyProviderId': id, - }.jsify()); + worker.postMessage( + { + 'msgType': 'keyProviderDispose', + 'msgId': msgId, + 'keyProviderId': id, + }.jsify(), + ); await events.waitFor( - filter: (event) { - logger.fine('waiting for dispose on msg: $msgId'); - return event.msgId == msgId; - }, - duration: Duration(seconds: 15)); + filter: (event) { + logger.fine('waiting for dispose on msg: $msgId'); + return event.msgId == msgId; + }, + duration: Duration(seconds: 15), + ); _keys.clear(); } @override - Future setKey( - {required String participantId, - required int index, - required Uint8List key}) async { + Future setKey({ + required String participantId, + required int index, + required Uint8List key, + }) async { var msgId = randomString(12); - worker.postMessage({ - 'msgType': 'setKey', - 'msgId': msgId, - 'keyProviderId': id, - 'participantId': participantId, - 'keyIndex': index, - 'key': base64Encode(key), - }.jsify()); + worker.postMessage( + { + 'msgType': 'setKey', + 'msgId': msgId, + 'keyProviderId': id, + 'participantId': participantId, + 'keyIndex': index, + 'key': base64Encode(key), + }.jsify(), + ); await events.waitFor( filter: (event) { @@ -185,45 +210,55 @@ class KeyProviderImpl implements KeyProvider { } @override - Future ratchetKey( - {required String participantId, required int index}) async { + Future ratchetKey({ + required String participantId, + required int index, + }) async { var msgId = randomString(12); - worker.postMessage({ - 'msgType': 'ratchetKey', - 'msgId': msgId, - 'keyProviderId': id, - 'participantId': participantId, - 'keyIndex': index, - }.jsify()); + worker.postMessage( + { + 'msgType': 'ratchetKey', + 'msgId': msgId, + 'keyProviderId': id, + 'participantId': participantId, + 'keyIndex': index, + }.jsify(), + ); var res = await events.waitFor( - filter: (event) { - logger.fine('waiting for ratchetKey on msg: $msgId'); - return event.msgId == msgId; - }, - duration: Duration(seconds: 15)); + filter: (event) { + logger.fine('waiting for ratchetKey on msg: $msgId'); + return event.msgId == msgId; + }, + duration: Duration(seconds: 15), + ); return base64Decode(res.data['newKey']); } @override - Future exportKey( - {required String participantId, required int index}) async { + Future exportKey({ + required String participantId, + required int index, + }) async { var msgId = randomString(12); - worker.postMessage({ - 'msgType': 'exportKey', - 'msgId': msgId, - 'keyProviderId': id, - 'participantId': participantId, - 'keyIndex': index, - }.jsify()); + worker.postMessage( + { + 'msgType': 'exportKey', + 'msgId': msgId, + 'keyProviderId': id, + 'participantId': participantId, + 'keyIndex': index, + }.jsify(), + ); var res = await events.waitFor( - filter: (event) { - logger.fine('waiting for exportKey on msg: $msgId'); - return event.msgId == msgId; - }, - duration: Duration(seconds: 15)); + filter: (event) { + logger.fine('waiting for exportKey on msg: $msgId'); + return event.msgId == msgId; + }, + duration: Duration(seconds: 15), + ); return base64Decode(res.data['exportedKey']); } @@ -231,19 +266,22 @@ class KeyProviderImpl implements KeyProvider { @override Future exportSharedKey({int index = 0}) async { var msgId = randomString(12); - worker.postMessage({ - 'msgType': 'exportSharedKey', - 'msgId': msgId, - 'keyProviderId': id, - 'keyIndex': index, - }.jsify()); + worker.postMessage( + { + 'msgType': 'exportSharedKey', + 'msgId': msgId, + 'keyProviderId': id, + 'keyIndex': index, + }.jsify(), + ); var res = await events.waitFor( - filter: (event) { - logger.fine('waiting for exportSharedKey on msg: $msgId'); - return event.msgId == msgId; - }, - duration: Duration(seconds: 15)); + filter: (event) { + logger.fine('waiting for exportSharedKey on msg: $msgId'); + return event.msgId == msgId; + }, + duration: Duration(seconds: 15), + ); return base64Decode(res.data['exportedKey']); } @@ -251,18 +289,21 @@ class KeyProviderImpl implements KeyProvider { @override Future ratchetSharedKey({int index = 0}) async { var msgId = randomString(12); - worker.postMessage({ - 'msgType': 'ratchetSharedKey', - 'msgId': msgId, - 'keyProviderId': id, - 'keyIndex': index, - }.jsify()); + worker.postMessage( + { + 'msgType': 'ratchetSharedKey', + 'msgId': msgId, + 'keyProviderId': id, + 'keyIndex': index, + }.jsify(), + ); var res = await events.waitFor( - filter: (event) { - logger.fine('waiting for ratchetSharedKey on msg: $msgId'); - return event.msgId == msgId; - }, - duration: Duration(seconds: 15)); + filter: (event) { + logger.fine('waiting for ratchetSharedKey on msg: $msgId'); + return event.msgId == msgId; + }, + duration: Duration(seconds: 15), + ); return base64Decode(res.data['newKey']); } @@ -270,38 +311,44 @@ class KeyProviderImpl implements KeyProvider { @override Future setSharedKey({required Uint8List key, int index = 0}) async { var msgId = randomString(12); - worker.postMessage({ - 'msgType': 'setSharedKey', - 'msgId': msgId, - 'keyProviderId': id, - 'keyIndex': index, - 'key': base64Encode(key), - }.jsify()); + worker.postMessage( + { + 'msgType': 'setSharedKey', + 'msgId': msgId, + 'keyProviderId': id, + 'keyIndex': index, + 'key': base64Encode(key), + }.jsify(), + ); await events.waitFor( - filter: (event) { - logger.fine('waiting for setSharedKey on msg: $msgId'); - return event.msgId == msgId; - }, - duration: Duration(seconds: 15)); + filter: (event) { + logger.fine('waiting for setSharedKey on msg: $msgId'); + return event.msgId == msgId; + }, + duration: Duration(seconds: 15), + ); } @override Future setSifTrailer({required Uint8List trailer}) async { var msgId = randomString(12); - worker.postMessage({ - 'msgType': 'setSifTrailer', - 'msgId': msgId, - 'keyProviderId': id, - 'sifTrailer': base64Encode(trailer), - }.jsify()); + worker.postMessage( + { + 'msgType': 'setSifTrailer', + 'msgId': msgId, + 'keyProviderId': id, + 'sifTrailer': base64Encode(trailer), + }.jsify(), + ); await events.waitFor( - filter: (event) { - logger.fine('waiting for setSifTrailer on msg: $msgId'); - return event.msgId == msgId; - }, - duration: Duration(seconds: 15)); + filter: (event) { + logger.fine('waiting for setSifTrailer on msg: $msgId'); + return event.msgId == msgId; + }, + duration: Duration(seconds: 15), + ); } } @@ -323,7 +370,8 @@ class FrameCryptorFactoryImpl implements FrameCryptorFactory { var trackId = data['trackId']; var participantId = data['participantId']; var frameCryptor = _frameCryptors.values.firstWhereOrNull( - (element) => (element as FrameCryptorImpl).trackId == trackId); + (element) => (element as FrameCryptorImpl).trackId == trackId, + ); var state = data['state']; var frameCryptorState = FrameCryptorState.FrameCryptorStateNew; switch (state) { @@ -350,8 +398,10 @@ class FrameCryptorFactoryImpl implements FrameCryptorFactory { FrameCryptorState.FrameCryptorStateKeyRatcheted; break; } - frameCryptor?.onFrameCryptorStateChanged - ?.call(participantId, frameCryptorState); + frameCryptor?.onFrameCryptorStateChanged?.call( + participantId, + frameCryptorState, + ); } } }; @@ -373,19 +423,25 @@ class FrameCryptorFactoryImpl implements FrameCryptorFactory { @override Future createDefaultKeyProvider( - KeyProviderOptions options) async { - var keyProvider = - KeyProviderImpl(randomString(12), worker, options, events); + KeyProviderOptions options, + ) async { + var keyProvider = KeyProviderImpl( + randomString(12), + worker, + options, + events, + ); await keyProvider.init(); return keyProvider; } @override - Future createFrameCryptorForRtpReceiver( - {required String participantId, - required RTCRtpReceiver receiver, - required Algorithm algorithm, - required KeyProvider keyProvider}) { + Future createFrameCryptorForRtpReceiver({ + required String participantId, + required RTCRtpReceiver receiver, + required Algorithm algorithm, + required KeyProvider keyProvider, + }) async { var jsReceiver = (receiver as RTCRtpReceiverWeb).jsRtpReceiver; var trackId = jsReceiver.track.id; @@ -405,22 +461,22 @@ class FrameCryptorFactoryImpl implements FrameCryptorFactory { jsReceiver.transform = web.RTCRtpScriptTransform(worker, options.jsify()); } else { - var writable = - jsReceiver.getProperty('writable'.toJS) as web.WritableStream?; - var readable = - jsReceiver.getProperty('readable'.toJS) as web.ReadableStream?; - var exist = true; - if (writable == null || readable == null) { - final streams = jsReceiver.callMethod( + RTCInsertableStreams? insertableStreams = jsReceiver + .getProperty('insertableStreams'.toJS) as RTCInsertableStreams?; + + var exist = insertableStreams != null; + + if (insertableStreams == null) { + insertableStreams = jsReceiver.callMethod( 'createEncodedStreams'.toJS, ); - readable = streams.getProperty('readable'.toJS) as web.ReadableStream; - jsReceiver.setProperty('readable'.toJS, readable); - writable = streams.getProperty('writable'.toJS) as web.WritableStream; - jsReceiver.setProperty('writable'.toJS, writable); - exist = false; + jsReceiver.setProperty('insertableStreams'.toJS, insertableStreams); } + + var readable = insertableStreams!.readable; + var writable = insertableStreams!.writable; var msgId = randomString(12); + try { worker.postMessage( { @@ -443,18 +499,24 @@ class FrameCryptorFactoryImpl implements FrameCryptorFactory { } } FrameCryptor cryptor = FrameCryptorImpl( - this, worker, participantId, trackId, - jsReceiver: jsReceiver, keyProvider: keyProvider); + this, + worker, + participantId, + trackId, + jsReceiver: jsReceiver, + keyProvider: keyProvider, + ); _frameCryptors[trackId] = cryptor; return Future.value(cryptor); } @override - Future createFrameCryptorForRtpSender( - {required String participantId, - required RTCRtpSender sender, - required Algorithm algorithm, - required KeyProvider keyProvider}) { + Future createFrameCryptorForRtpSender({ + required String participantId, + required RTCRtpSender sender, + required Algorithm algorithm, + required KeyProvider keyProvider, + }) { var jsSender = (sender as RTCRtpSenderWeb).jsRtpSender; var trackId = jsSender.track?.id ?? sender.senderId; var kind = jsSender.track!.kind; @@ -474,23 +536,22 @@ class FrameCryptorFactoryImpl implements FrameCryptorFactory { print('object: ${options['keyProviderId']}'); jsSender.transform = web.RTCRtpScriptTransform(worker, options.jsify()); } else { - var writable = - jsSender.getProperty('writable'.toJS) as web.WritableStream?; - var readable = - jsSender.getProperty('readable'.toJS) as web.ReadableStream?; - var exist = true; - if (writable == null || readable == null) { - final streams = jsSender.callMethod( + RTCInsertableStreams? insertableStreams = jsSender + .getProperty('insertableStreams'.toJS) as RTCInsertableStreams?; + + var exist = insertableStreams != null; + + if (insertableStreams == null) { + insertableStreams = jsSender.callMethod( 'createEncodedStreams'.toJS, ); - readable = streams.getProperty('readable'.toJS) as web.ReadableStream; - jsSender.setProperty('readable'.toJS, readable); - writable = streams.getProperty('writable'.toJS) as web.WritableStream; - jsSender.setProperty('writable'.toJS, writable); - exist = false; + jsSender.setProperty('insertableStreams'.toJS, insertableStreams); } - var msgId = randomString(12); + var readable = insertableStreams!.readable; + var writable = insertableStreams!.writable; + + var msgId = randomString(12); try { worker.postMessage( { @@ -513,8 +574,13 @@ class FrameCryptorFactoryImpl implements FrameCryptorFactory { } } FrameCryptor cryptor = FrameCryptorImpl( - this, worker, participantId, trackId, - jsSender: jsSender, keyProvider: keyProvider); + this, + worker, + participantId, + trackId, + jsSender: jsSender, + keyProvider: keyProvider, + ); _frameCryptors[trackId] = cryptor; return Future.value(cryptor); From 643b08a8a972013dc125804dad7b0d702bdface9 Mon Sep 17 00:00:00 2001 From: cloudwebrtc Date: Mon, 11 Aug 2025 18:23:02 +0800 Subject: [PATCH 16/23] release: 1.5.3+hotfix.5. --- CHANGELOG.md | 4 ++++ pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e74d924..a5e5d3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog -------------------------------------------- +[1.5.3+hotfix.5] - 2025-08-11 + +* fixed E2EE bug for Chrome rejoin. + [1.5.3+hotfix.4] - 2025-07-29 * fixed E2EE issue for Chrome. diff --git a/pubspec.yaml b/pubspec.yaml index 835c297..eac0c1c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: dart_webrtc description: Use the dart/js library to re-wrap the webrtc js interface of the browser, to adapted common browsers. -version: 1.5.3+hotfix.4 +version: 1.5.3+hotfix.5 homepage: https://github.com/flutter-webrtc/dart-webrtc environment: From 7b574641eb5aeb2f647e9be518b33958368b2b4c Mon Sep 17 00:00:00 2001 From: cloudwebrtc Date: Mon, 11 Aug 2025 18:28:32 +0800 Subject: [PATCH 17/23] fix analyzer. --- lib/src/e2ee.worker/e2ee.cryptor.dart | 1 + lib/src/e2ee.worker/e2ee.utils.dart | 1 + lib/src/frame_cryptor_impl.dart | 16 ++++++++-------- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/src/e2ee.worker/e2ee.cryptor.dart b/lib/src/e2ee.worker/e2ee.cryptor.dart index c3e6a15..079d38f 100644 --- a/lib/src/e2ee.worker/e2ee.cryptor.dart +++ b/lib/src/e2ee.worker/e2ee.cryptor.dart @@ -4,6 +4,7 @@ import 'dart:js_interop_unsafe'; import 'dart:math'; import 'dart:typed_data'; +// ignore: deprecated_member_use import 'package:js/js.dart'; import 'package:web/web.dart' as web; import 'e2ee.keyhandler.dart'; diff --git a/lib/src/e2ee.worker/e2ee.utils.dart b/lib/src/e2ee.worker/e2ee.utils.dart index 7e58bd2..e91c4da 100644 --- a/lib/src/e2ee.worker/e2ee.utils.dart +++ b/lib/src/e2ee.worker/e2ee.utils.dart @@ -2,6 +2,7 @@ import 'dart:js_interop'; import 'dart:js_interop_unsafe'; import 'dart:typed_data'; +// ignore: deprecated_member_use import 'package:js/js_util.dart'; import 'package:web/web.dart' as web; diff --git a/lib/src/frame_cryptor_impl.dart b/lib/src/frame_cryptor_impl.dart index 4445f42..2a940e4 100644 --- a/lib/src/frame_cryptor_impl.dart +++ b/lib/src/frame_cryptor_impl.dart @@ -461,8 +461,8 @@ class FrameCryptorFactoryImpl implements FrameCryptorFactory { jsReceiver.transform = web.RTCRtpScriptTransform(worker, options.jsify()); } else { - RTCInsertableStreams? insertableStreams = jsReceiver - .getProperty('insertableStreams'.toJS) as RTCInsertableStreams?; + var insertableStreams = jsReceiver.getProperty('insertableStreams'.toJS) + as RTCInsertableStreams?; var exist = insertableStreams != null; @@ -473,8 +473,8 @@ class FrameCryptorFactoryImpl implements FrameCryptorFactory { jsReceiver.setProperty('insertableStreams'.toJS, insertableStreams); } - var readable = insertableStreams!.readable; - var writable = insertableStreams!.writable; + var readable = insertableStreams.readable; + var writable = insertableStreams.writable; var msgId = randomString(12); try { @@ -536,8 +536,8 @@ class FrameCryptorFactoryImpl implements FrameCryptorFactory { print('object: ${options['keyProviderId']}'); jsSender.transform = web.RTCRtpScriptTransform(worker, options.jsify()); } else { - RTCInsertableStreams? insertableStreams = jsSender - .getProperty('insertableStreams'.toJS) as RTCInsertableStreams?; + var insertableStreams = jsSender.getProperty('insertableStreams'.toJS) + as RTCInsertableStreams?; var exist = insertableStreams != null; @@ -548,8 +548,8 @@ class FrameCryptorFactoryImpl implements FrameCryptorFactory { jsSender.setProperty('insertableStreams'.toJS, insertableStreams); } - var readable = insertableStreams!.readable; - var writable = insertableStreams!.writable; + var readable = insertableStreams.readable; + var writable = insertableStreams.writable; var msgId = randomString(12); try { From a47043540f173dc92519a3a3cc54d4257bd50c83 Mon Sep 17 00:00:00 2001 From: Yash Garg Date: Tue, 12 Aug 2025 22:23:53 +0530 Subject: [PATCH 18/23] fix: parse `rid` and `ssrc` to proper types This caused a TypeError on Safari where rid was "0" and being parsed as an integer instead of a string. --- lib/src/rtc_rtp_parameters_impl.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/rtc_rtp_parameters_impl.dart b/lib/src/rtc_rtp_parameters_impl.dart index 54c7881..f1a0b59 100644 --- a/lib/src/rtc_rtp_parameters_impl.dart +++ b/lib/src/rtc_rtp_parameters_impl.dart @@ -70,7 +70,7 @@ class RTCHeaderExtensionWeb { class RTCRtpEncodingWeb { static RTCRtpEncoding fromJsObject(web.RTCRtpEncodingParameters object) { return RTCRtpEncoding.fromMap({ - 'rid': object.getProperty('rid'.toJS)?.toDart, + 'rid': object.getProperty('rid'.toJS).dartify(), 'active': object.active, 'maxBitrate': object.getProperty('maxBitrate'.toJS)?.toDartInt, 'maxFramerate': @@ -81,7 +81,7 @@ class RTCRtpEncodingWeb { 'scaleResolutionDownBy': object .getProperty('scaleResolutionDownBy'.toJS) ?.toDartDouble, - 'ssrc': object.getProperty('ssrc'.toJS)?.toDart + 'ssrc': object.getProperty('ssrc'.toJS)?.toDartInt }); } } From 83a1a3dab406fcd67e94657031a6b81052e491e9 Mon Sep 17 00:00:00 2001 From: td Date: Mon, 8 Sep 2025 16:43:47 +0200 Subject: [PATCH 19/23] fix: better error messages on worker message timeouts --- lib/src/frame_cryptor_impl.dart | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/lib/src/frame_cryptor_impl.dart b/lib/src/frame_cryptor_impl.dart index 2a940e4..2156697 100644 --- a/lib/src/frame_cryptor_impl.dart +++ b/lib/src/frame_cryptor_impl.dart @@ -148,7 +148,8 @@ class KeyProviderImpl implements KeyProvider { logger.fine('waiting for init on msg: $msgId'); return event.msgId == msgId; }, - duration: Duration(seconds: 15), + duration: Duration(seconds: 5), + onTimeout: () => throw Exception('waiting for init on msg timed out'), ); } @@ -168,7 +169,8 @@ class KeyProviderImpl implements KeyProvider { logger.fine('waiting for dispose on msg: $msgId'); return event.msgId == msgId; }, - duration: Duration(seconds: 15), + duration: Duration(seconds: 5), + onTimeout: () => throw Exception('waiting for dispose on msg timed out'), ); _keys.clear(); @@ -197,7 +199,8 @@ class KeyProviderImpl implements KeyProvider { logger.fine('waiting for setKey on msg: $msgId'); return event.msgId == msgId; }, - duration: Duration(minutes: 15), + duration: Duration(seconds: 5), + onTimeout: () => throw Exception('waiting for setKey on msg timed out'), ); _keys[participantId] ??= []; @@ -230,7 +233,9 @@ class KeyProviderImpl implements KeyProvider { logger.fine('waiting for ratchetKey on msg: $msgId'); return event.msgId == msgId; }, - duration: Duration(seconds: 15), + duration: Duration(seconds: 5), + onTimeout: () => + throw Exception('waiting for ratchetKey on msg timed out'), ); return base64Decode(res.data['newKey']); @@ -257,7 +262,9 @@ class KeyProviderImpl implements KeyProvider { logger.fine('waiting for exportKey on msg: $msgId'); return event.msgId == msgId; }, - duration: Duration(seconds: 15), + duration: Duration(seconds: 5), + onTimeout: () => + throw Exception('waiting for exportKey on msg timed out'), ); return base64Decode(res.data['exportedKey']); @@ -280,7 +287,9 @@ class KeyProviderImpl implements KeyProvider { logger.fine('waiting for exportSharedKey on msg: $msgId'); return event.msgId == msgId; }, - duration: Duration(seconds: 15), + duration: Duration(seconds: 5), + onTimeout: () => + throw Exception('waiting for exportSharedKey on msg timed out'), ); return base64Decode(res.data['exportedKey']); @@ -302,7 +311,9 @@ class KeyProviderImpl implements KeyProvider { logger.fine('waiting for ratchetSharedKey on msg: $msgId'); return event.msgId == msgId; }, - duration: Duration(seconds: 15), + duration: Duration(seconds: 5), + onTimeout: () => + throw Exception('waiting for ratchetSharedKey on msg timed out'), ); return base64Decode(res.data['newKey']); @@ -326,7 +337,9 @@ class KeyProviderImpl implements KeyProvider { logger.fine('waiting for setSharedKey on msg: $msgId'); return event.msgId == msgId; }, - duration: Duration(seconds: 15), + duration: Duration(seconds: 5), + onTimeout: () => + throw Exception('waiting for setSharedKey on msg timed out'), ); } @@ -347,7 +360,9 @@ class KeyProviderImpl implements KeyProvider { logger.fine('waiting for setSifTrailer on msg: $msgId'); return event.msgId == msgId; }, - duration: Duration(seconds: 15), + duration: Duration(seconds: 5), + onTimeout: () => + throw Exception('waiting for setSifTrailer on msg timed out'), ); } } From 727469602927146025d7901e552bbb63cc68a9cf Mon Sep 17 00:00:00 2001 From: cloudwebrtc Date: Wed, 10 Sep 2025 09:10:27 +0800 Subject: [PATCH 20/23] feat: data packet cryptor. --- lib/dart_webrtc.dart | 1 + lib/src/data_packet_cryptor_impl.dart | 128 ++++++++++ .../e2ee.worker/e2ee.data_packet_cryptor.dart | 234 ++++++++++++++++++ ...e.cryptor.dart => e2ee.frame_cryptor.dart} | 0 lib/src/e2ee.worker/e2ee.worker.dart | 169 ++++++++++++- pubspec.yaml | 2 +- web/main.dart | 23 +- 7 files changed, 554 insertions(+), 3 deletions(-) create mode 100644 lib/src/data_packet_cryptor_impl.dart create mode 100644 lib/src/e2ee.worker/e2ee.data_packet_cryptor.dart rename lib/src/e2ee.worker/{e2ee.cryptor.dart => e2ee.frame_cryptor.dart} (100%) diff --git a/lib/dart_webrtc.dart b/lib/dart_webrtc.dart index 744176d..122f806 100644 --- a/lib/dart_webrtc.dart +++ b/lib/dart_webrtc.dart @@ -3,6 +3,7 @@ library dart_webrtc; export 'package:webrtc_interface/webrtc_interface.dart' hide MediaDevices, MediaRecorder, Navigator; +export 'src/data_packet_cryptor_impl.dart'; export 'src/factory_impl.dart'; export 'src/media_devices.dart'; export 'src/media_recorder.dart'; diff --git a/lib/src/data_packet_cryptor_impl.dart b/lib/src/data_packet_cryptor_impl.dart new file mode 100644 index 0000000..7bf1c4b --- /dev/null +++ b/lib/src/data_packet_cryptor_impl.dart @@ -0,0 +1,128 @@ +import 'dart:js_interop'; +import 'dart:typed_data'; + +import 'package:web/web.dart' as web; +import 'package:webrtc_interface/webrtc_interface.dart'; + +import 'e2ee.worker/e2ee.logger.dart'; +import 'event.dart'; +import 'frame_cryptor_impl.dart' show KeyProviderImpl, WorkerResponse; +import 'utils.dart'; + +class DataPacketCryptorImpl implements DataPacketCryptor { + DataPacketCryptorImpl({ + required this.keyProvider, + required this.algorithm, + }); + + final KeyProviderImpl keyProvider; + final Algorithm algorithm; + web.Worker get worker => keyProvider.worker; + final String _dataCyrptorId = randomString(24); + EventsEmitter get events => keyProvider.events; + + @override + Future encrypt({ + required String participantId, + required int keyIndex, + required Uint8List data, + }) async { + var msgId = randomString(12); + worker.postMessage( + { + 'msgType': 'dataCyrptorEncrypt', + 'msgId': msgId, + 'keyProviderId': keyProvider.id, + 'dataCyrptorId': _dataCyrptorId, + 'participantId': participantId, + 'keyIndex': keyIndex, + 'data': data, + 'algorithm': algorithm.name, + }.jsify(), + ); + + var res = await events.waitFor( + filter: (event) { + logger.fine('waiting for encrypt on msg: $msgId'); + return event.msgId == msgId; + }, + duration: Duration(seconds: 5), + onTimeout: () => throw Exception('waiting for encrypt on msg timed out'), + ); + + return EncryptedPacket( + data: res.data['data'] as Uint8List, + keyIndex: res.data['keyIndex'] as int, + iv: res.data['iv'] as Uint8List, + ); + } + + @override + Future decrypt({ + required String participantId, + required EncryptedPacket encryptedPacket, + }) async { + var msgId = randomString(12); + worker.postMessage( + { + 'msgType': 'dataCyrptorDecrypt', + 'msgId': msgId, + 'keyProviderId': keyProvider.id, + 'dataCyrptorId': _dataCyrptorId, + 'participantId': participantId, + 'keyIndex': encryptedPacket.keyIndex, + 'data': encryptedPacket.data, + 'iv': encryptedPacket.iv, + 'algorithm': algorithm.name, + }.jsify(), + ); + + var res = await events.waitFor( + filter: (event) { + logger.fine('waiting for decrypt on msg: $msgId'); + return event.msgId == msgId; + }, + duration: Duration(seconds: 5), + onTimeout: () => throw Exception('waiting for decrypt on msg timed out'), + ); + + return res.data['data'] as Uint8List; + } + + @override + Future dispose() async { + var msgId = randomString(12); + worker.postMessage( + { + 'msgType': 'dataCyrptorDispose', + 'msgId': msgId, + 'dataCyrptorId': _dataCyrptorId + }.jsify(), + ); + + await events.waitFor( + filter: (event) { + logger.fine('waiting for dispose on msg: $msgId'); + return event.msgId == msgId; + }, + duration: Duration(seconds: 5), + onTimeout: () => throw Exception('waiting for dispose on msg timed out'), + ); + } +} + +class DataPacketCryptorFactoryImpl implements DataPacketCryptorFactory { + DataPacketCryptorFactoryImpl._internal(); + + static final DataPacketCryptorFactoryImpl instance = + DataPacketCryptorFactoryImpl._internal(); + @override + Future createDataPacketCryptor( + {required Algorithm algorithm, required KeyProvider keyProvider}) async { + return Future.value(DataPacketCryptorImpl( + algorithm: algorithm, keyProvider: keyProvider as KeyProviderImpl)); + } +} + +DataPacketCryptorFactory get dataPacketCryptorFactory => + DataPacketCryptorFactoryImpl.instance; diff --git a/lib/src/e2ee.worker/e2ee.data_packet_cryptor.dart b/lib/src/e2ee.worker/e2ee.data_packet_cryptor.dart new file mode 100644 index 0000000..cbb8312 --- /dev/null +++ b/lib/src/e2ee.worker/e2ee.data_packet_cryptor.dart @@ -0,0 +1,234 @@ +import 'dart:async'; +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; +import 'dart:math'; +import 'dart:typed_data'; + +// ignore: deprecated_member_use +import 'package:dart_webrtc/src/e2ee.worker/e2ee.frame_cryptor.dart' + show IV_LENGTH; +import 'package:js/js.dart'; +import 'package:web/web.dart' as web; +import 'e2ee.keyhandler.dart'; +import 'e2ee.logger.dart'; + +class EncryptedPacket { + EncryptedPacket({ + required this.data, + required this.keyIndex, + required this.iv, + }); + + Uint8List data; + int keyIndex; + Uint8List iv; +} + +class E2EEDataPacketCryptor { + E2EEDataPacketCryptor({ + required this.worker, + required this.participantIdentity, + required this.dataCryptorId, + required this.keyHandler, + }); + int sendCount_ = -1; + String? participantIdentity; + String? dataCryptorId; + ParticipantKeyHandler keyHandler; + KeyOptions get keyOptions => keyHandler.keyOptions; + int currentKeyIndex = 0; + final web.DedicatedWorkerGlobalScope worker; + + void setParticipant(String identity, ParticipantKeyHandler keys) { + participantIdentity = identity; + keyHandler = keys; + } + + void unsetParticipant() { + participantIdentity = null; + } + + void setKeyIndex(int keyIndex) { + logger.config('setKeyIndex for $participantIdentity, newIndex: $keyIndex'); + currentKeyIndex = keyIndex; + } + + Uint8List makeIv({required int timestamp}) { + var iv = ByteData(IV_LENGTH); + + // having to keep our own send count (similar to a picture id) is not ideal. + if (sendCount_ == -1) { + // Initialize with a random offset, similar to the RTP sequence number. + sendCount_ = Random.secure().nextInt(0xffff); + } + + var sendCount = sendCount_ ?? 0; + final randomBytes = + Random.secure().nextInt(max(0, 0xffffffff)).toUnsigned(32); + + iv.setUint32(0, randomBytes); + iv.setUint32(4, timestamp); + iv.setUint32(8, timestamp - (sendCount % 0xffff)); + + sendCount = sendCount + 1; + + return iv.buffer.asUint8List(); + } + + void postMessage(Object message) { + worker.postMessage(message.jsify()); + } + + Future encrypt( + ParticipantKeyHandler keys, + Uint8List data, + ) async { + logger.fine('encodeFunction: buffer ${data.length}'); + + var secretKey = keyHandler.getKeySet(currentKeyIndex)?.encryptionKey; + var keyIndex = currentKeyIndex; + + if (secretKey == null) { + logger.warning( + 'encodeFunction: no secretKey for index $keyIndex, cannot encrypt'); + return null; + } + + var iv = makeIv(timestamp: DateTime.timestamp().millisecondsSinceEpoch); + + var frameTrailer = ByteData(2); + frameTrailer.setInt8(0, IV_LENGTH); + frameTrailer.setInt8(1, keyIndex); + + try { + var cipherText = await worker.crypto.subtle + .encrypt( + { + 'name': 'AES-GCM', + 'iv': iv, + }.jsify() as web.AlgorithmIdentifier, + secretKey, + data.toJS, + ) + .toDart as JSArrayBuffer; + + logger.finer( + 'encodeFunction: encrypted buffer: ${data.length}, cipherText: ${cipherText.toDart.asUint8List().length}'); + + return EncryptedPacket( + data: cipherText.toDart.asUint8List(), + keyIndex: keyIndex, + iv: iv, + ); + } catch (e) { + logger.warning('encodeFunction encrypt: e ${e.toString()}'); + rethrow; + } + } + + Future decrypt( + ParticipantKeyHandler keys, + EncryptedPacket encryptedPacket, + ) async { + var ratchetCount = 0; + + logger.fine( + 'decodeFunction: data packet lenght ${encryptedPacket.data.length}'); + + ByteBuffer? decrypted; + KeySet? initialKeySet; + var initialKeyIndex = currentKeyIndex; + + try { + var ivLength = encryptedPacket.iv.length; + var keyIndex = encryptedPacket.keyIndex; + var iv = encryptedPacket.iv; + var payload = encryptedPacket.data; + initialKeySet = keyHandler.getKeySet(initialKeyIndex); + + logger.finer( + 'decodeFunction: start decrypting data packet length ${payload.length}, ivLength $ivLength, keyIndex $keyIndex, iv $iv'); + + /// missingKey flow: + /// tries to decrypt once, fails, tries to ratchet once and decrypt again, + /// fails (does not save ratcheted key), bumps _decryptionFailureCount, + /// if higher than failuretolerance hasValidKey is set to false, on next + /// frame it fires a missingkey + /// to throw missingkeys faster lower your failureTolerance + if (initialKeySet == null || !keyHandler.hasValidKey) { + return null; + } + var currentkeySet = initialKeySet; + + Future decryptFrameInternal() async { + decrypted = ((await worker.crypto.subtle + .decrypt( + { + 'name': 'AES-GCM', + 'iv': iv, + }.jsify() as web.AlgorithmIdentifier, + currentkeySet.encryptionKey, + payload.toJS, + ) + .toDart) as JSArrayBuffer) + .toDart; + logger.finer( + 'decodeFunction::decryptFrameInternal: decrypted: ${decrypted!.asUint8List().length}'); + + if (decrypted == null) { + throw Exception('[decryptFrameInternal] could not decrypt'); + } + logger.finer( + 'decodeFunction::decryptFrameInternal: decrypted: ${decrypted!.asUint8List().length}'); + if (currentkeySet != initialKeySet) { + logger.fine( + 'decodeFunction::decryptFrameInternal: ratchetKey: decryption ok, newState: kKeyRatcheted'); + await keyHandler.setKeySetFromMaterial( + currentkeySet, initialKeyIndex); + } + } + + Future ratchedKeyInternal() async { + if (ratchetCount >= keyOptions.ratchetWindowSize || + keyOptions.ratchetWindowSize <= 0) { + throw Exception('[ratchedKeyInternal] cannot ratchet anymore'); + } + + var newKeyBuffer = await keyHandler.ratchet( + currentkeySet.material, keyOptions.ratchetSalt); + var newMaterial = await keyHandler.ratchetMaterial( + currentkeySet.material, newKeyBuffer.buffer); + currentkeySet = + await keyHandler.deriveKeys(newMaterial, keyOptions.ratchetSalt); + ratchetCount++; + await decryptFrameInternal(); + } + + try { + /// gets frame -> tries to decrypt -> tries to ratchet (does this failureTolerance + /// times, then says missing key) + /// we only save the new key after ratcheting if we were able to decrypt something + await decryptFrameInternal(); + } catch (e) { + logger.finer('decodeFunction: kInternalError catch $e'); + await ratchedKeyInternal(); + } + + if (decrypted == null) { + throw Exception( + '[decodeFunction] decryption failed even after ratchting'); + } + + // we can now be sure that decryption was a success + keyHandler.decryptionSuccess(); + + logger.finer( + 'decodeFunction: decryption success, buffer length ${payload.length}, decrypted: ${decrypted!.asUint8List().length}'); + + return decrypted!.asUint8List(); + } catch (e) { + keyHandler.decryptionFailure(); + rethrow; + } + } +} diff --git a/lib/src/e2ee.worker/e2ee.cryptor.dart b/lib/src/e2ee.worker/e2ee.frame_cryptor.dart similarity index 100% rename from lib/src/e2ee.worker/e2ee.cryptor.dart rename to lib/src/e2ee.worker/e2ee.frame_cryptor.dart diff --git a/lib/src/e2ee.worker/e2ee.worker.dart b/lib/src/e2ee.worker/e2ee.worker.dart index 729390f..d55bed0 100644 --- a/lib/src/e2ee.worker/e2ee.worker.dart +++ b/lib/src/e2ee.worker/e2ee.worker.dart @@ -6,7 +6,9 @@ import 'dart:typed_data'; import 'package:collection/collection.dart'; import 'package:logging/logging.dart'; import 'package:web/web.dart' as web; -import 'e2ee.cryptor.dart'; +import 'package:webrtc_interface/webrtc_interface.dart' show Algorithm; +import 'e2ee.data_packet_cryptor.dart'; +import 'e2ee.frame_cryptor.dart'; import 'e2ee.keyhandler.dart'; import 'e2ee.logger.dart'; @@ -14,6 +16,7 @@ import 'e2ee.logger.dart'; external web.DedicatedWorkerGlobalScope get self; var participantCryptors = []; +var participantDataCryptors = []; var keyProviders = {}; FrameCryptor getTrackCryptor( @@ -41,12 +44,43 @@ FrameCryptor getTrackCryptor( return cryptor; } +E2EEDataPacketCryptor getDataPacketCryptor( + String participantIdentity, String dataCryptorId, KeyProvider keyProvider) { + var cryptor = participantDataCryptors + .firstWhereOrNull((c) => c.dataCryptorId == dataCryptorId); + if (cryptor == null) { + logger.info( + 'creating new cryptor for $participantIdentity, dataCryptorId $dataCryptorId'); + + cryptor = E2EEDataPacketCryptor( + worker: self, + participantIdentity: participantIdentity, + dataCryptorId: dataCryptorId, + keyHandler: keyProvider.getParticipantKeyHandler(participantIdentity), + ); + //setupCryptorErrorEvents(cryptor); + participantDataCryptors.add(cryptor); + } else if (participantIdentity != cryptor.participantIdentity) { + // assign new participant id to track cryptor and pass in correct key handler + cryptor.setParticipant(participantIdentity, + keyProvider.getParticipantKeyHandler(participantIdentity)); + } + if (keyProvider.keyProviderOptions.sharedKey) {} + return cryptor; +} + void unsetCryptorParticipant(String trackId) { participantCryptors .firstWhereOrNull((c) => c.trackId == trackId) ?.unsetParticipant(); } +void unsetDataPacketCryptorParticipant(String dataCryptorId) { + participantDataCryptors + .firstWhereOrNull((c) => c.dataCryptorId == dataCryptorId) + ?.unsetParticipant(); +} + void main() async { // configure logs for debugging Logger.root.level = Level.WARNING; @@ -435,6 +469,139 @@ void main() async { }.jsify()); } } + case 'dataCyrptorEncrypt': + { + var participantId = msg['participantId'] as String; + var data = msg['data'] as Uint8List; + var keyIndex = msg['keyIndex'] as int; + var dataCryptorId = msg['dataCyrptorId'] as String; + var algorithmStr = msg['algorithm'] as String; + var algorithm = + Algorithm.values.firstWhereOrNull((a) => a.name == algorithmStr); + if (algorithm == null) { + self.postMessage({ + 'type': 'dataCyrptorEncrypt', + 'error': 'algorithm not found', + 'msgId': msgId, + 'msgType': 'response', + }.jsify()); + return; + } + logger.config( + 'Encrypt for dataCryptorId $dataCryptorId, participantId $participantId, keyIndex $keyIndex, data length ${data.length}, algorithm $algorithmStr'); + var keyProviderId = msg['keyProviderId'] as String; + var keyProvider = keyProviders[keyProviderId]; + if (keyProvider == null) { + logger.warning('KeyProvider not found for $keyProviderId'); + self.postMessage({ + 'type': 'dataCyrptorEncrypt', + 'error': 'KeyProvider not found', + 'msgId': msgId, + 'msgType': 'response', + }.jsify()); + return; + } + var cryptor = + getDataPacketCryptor(participantId, dataCryptorId, keyProvider); + try { + var encryptedPacket = + await cryptor.encrypt(cryptor.keyHandler, data); + self.postMessage({ + 'type': 'dataCyrptorEncrypt', + 'participantId': participantId, + 'dataCryptorId': dataCryptorId, + 'data': encryptedPacket!.data, + 'keyIndex': encryptedPacket.keyIndex, + 'iv': encryptedPacket.iv, + 'msgId': msgId, + 'msgType': 'response', + }.jsify()); + } catch (e) { + logger.warning('Error encrypting data: $e'); + self.postMessage({ + 'type': 'dataCyrptorEncrypt', + 'error': e.toString(), + 'msgId': msgId, + 'msgType': 'response', + }.jsify()); + break; + } + } + case 'dataCyrptorDecrypt': + { + var participantId = msg['participantId'] as String; + var data = msg['data'] as Uint8List; + var iv = msg['iv'] as Uint8List; + var keyIndex = msg['keyIndex'] as int; + var dataCryptorId = msg['dataCyrptorId'] as String; + var algorithmStr = msg['algorithm'] as String; + var algorithm = + Algorithm.values.firstWhereOrNull((a) => a.name == algorithmStr); + if (algorithm == null) { + self.postMessage({ + 'type': 'dataCyrptorDecrypt', + 'error': 'algorithm not found', + 'msgId': msgId, + 'msgType': 'response', + }.jsify()); + return; + } + logger.config( + 'Decrypt for dataCryptorId $dataCryptorId, participantId $participantId, keyIndex $keyIndex, data length ${data.length}, algorithm $algorithmStr'); + var keyProviderId = msg['keyProviderId'] as String; + var keyProvider = keyProviders[keyProviderId]; + if (keyProvider == null) { + logger.warning('KeyProvider not found for $keyProviderId'); + self.postMessage({ + 'type': 'dataCyrptorDecrypt', + 'error': 'KeyProvider not found', + 'msgId': msgId, + 'msgType': 'response', + }.jsify()); + return; + } + var cryptor = + getDataPacketCryptor(participantId, dataCryptorId, keyProvider); + try { + var decryptedData = await cryptor.decrypt( + cryptor.keyHandler, + EncryptedPacket( + data: data, + keyIndex: keyIndex, + iv: iv, + )); + self.postMessage({ + 'type': 'dataCyrptorDecrypt', + 'participantId': participantId, + 'dataCryptorId': dataCryptorId, + 'data': decryptedData, + 'msgId': msgId, + 'msgType': 'response', + }.jsify()); + } catch (e) { + logger.warning('Error decrypting data: $e'); + self.postMessage({ + 'type': 'dataCyrptorDecrypt', + 'error': e.toString(), + 'msgId': msgId, + 'msgType': 'response', + }.jsify()); + break; + } + break; + } + case 'dataCyrptorDispose': + { + var dataCryptorId = msg['dataCryptorId'] as String; + logger.config('Dispose for dataCryptorId $dataCryptorId'); + unsetDataPacketCryptorParticipant(dataCryptorId); + self.postMessage({ + 'type': 'dataCryptorDispose', + 'dataCryptorId': dataCryptorId, + 'msgId': msgId, + 'msgType': 'response', + }.jsify()); + } break; default: logger.warning('Unknown message kind $msg'); diff --git a/pubspec.yaml b/pubspec.yaml index eac0c1c..7f42fa4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: meta: ^1.8.0 synchronized: ^3.0.0+3 web: ^1.0.0 - webrtc_interface: ^1.2.2+hotfix.2 + webrtc_interface: ^1.3.0 dev_dependencies: build_runner: ^2.3.3 diff --git a/web/main.dart b/web/main.dart index cf1cd78..ddbff41 100644 --- a/web/main.dart +++ b/web/main.dart @@ -238,6 +238,26 @@ void loopBackTest() async { await keyProviderForSender.ratchetKey(index: 2, participantId: 'sender'); print('ratchetKey key: ${key.toList()}'); + var participantId = 'participantId_1'; + + await keyProviderForSender.setKey( + participantId: participantId, index: 0, key: key); + + final dataPacketCryptor = + await dataPacketCryptorFactory.createDataPacketCryptor( + algorithm: Algorithm.kAesGcm, keyProvider: keyProviderForSender!); + + var data = Uint8List.fromList('Hello world!'.codeUnits); + print('plain data: $data'); + var encryptedPacket = await dataPacketCryptor.encrypt( + participantId: participantId, keyIndex: 0, data: data); + print( + 'encrypted data: ${encryptedPacket?.data}, keyIndex: ${encryptedPacket?.keyIndex}, iv: ${encryptedPacket?.iv}'); + var decryptedData = await dataPacketCryptor.decrypt( + participantId: participantId, encryptedPacket: encryptedPacket!); + print('decrypted data: $decryptedData'); + print('decrypted string: ${String.fromCharCodes(decryptedData!)}'); + /* var key1 = await keyProviderForSender.ratchetKey(index: 0, participantId: 'sender'); @@ -254,7 +274,7 @@ void loopBackTest() async { key: Uint8List.fromList('testkey2'.codeUnits)); */ - + /* Timer.periodic(Duration(seconds: 1), (timer) async { var senders = await pc1.getSenders(); var receivers = await pc2.getReceivers(); @@ -276,4 +296,5 @@ void loopBackTest() async { }); }); }); + */ } From ddbb3984b205483f11230392c52fae57ae1bad89 Mon Sep 17 00:00:00 2001 From: cloudwebrtc Date: Wed, 10 Sep 2025 09:14:07 +0800 Subject: [PATCH 21/23] tidy. --- lib/src/data_packet_cryptor_impl.dart | 14 ++++---- .../e2ee.worker/e2ee.data_packet_cryptor.dart | 10 ++---- lib/src/e2ee.worker/e2ee.frame_cryptor.dart | 2 -- lib/src/e2ee.worker/e2ee.keyhandler.dart | 1 + lib/src/e2ee.worker/e2ee.worker.dart | 32 +++++++++---------- web/main.dart | 11 ++++--- 6 files changed, 34 insertions(+), 36 deletions(-) diff --git a/lib/src/data_packet_cryptor_impl.dart b/lib/src/data_packet_cryptor_impl.dart index 7bf1c4b..96d2b17 100644 --- a/lib/src/data_packet_cryptor_impl.dart +++ b/lib/src/data_packet_cryptor_impl.dart @@ -18,7 +18,7 @@ class DataPacketCryptorImpl implements DataPacketCryptor { final KeyProviderImpl keyProvider; final Algorithm algorithm; web.Worker get worker => keyProvider.worker; - final String _dataCyrptorId = randomString(24); + final String _dataCryptorId = randomString(24); EventsEmitter get events => keyProvider.events; @override @@ -30,10 +30,10 @@ class DataPacketCryptorImpl implements DataPacketCryptor { var msgId = randomString(12); worker.postMessage( { - 'msgType': 'dataCyrptorEncrypt', + 'msgType': 'dataCryptorEncrypt', 'msgId': msgId, 'keyProviderId': keyProvider.id, - 'dataCyrptorId': _dataCyrptorId, + 'dataCryptorId': _dataCryptorId, 'participantId': participantId, 'keyIndex': keyIndex, 'data': data, @@ -65,10 +65,10 @@ class DataPacketCryptorImpl implements DataPacketCryptor { var msgId = randomString(12); worker.postMessage( { - 'msgType': 'dataCyrptorDecrypt', + 'msgType': 'dataCryptorDecrypt', 'msgId': msgId, 'keyProviderId': keyProvider.id, - 'dataCyrptorId': _dataCyrptorId, + 'dataCryptorId': _dataCryptorId, 'participantId': participantId, 'keyIndex': encryptedPacket.keyIndex, 'data': encryptedPacket.data, @@ -94,9 +94,9 @@ class DataPacketCryptorImpl implements DataPacketCryptor { var msgId = randomString(12); worker.postMessage( { - 'msgType': 'dataCyrptorDispose', + 'msgType': 'dataCryptorDispose', 'msgId': msgId, - 'dataCyrptorId': _dataCyrptorId + 'dataCryptorId': _dataCryptorId }.jsify(), ); diff --git a/lib/src/e2ee.worker/e2ee.data_packet_cryptor.dart b/lib/src/e2ee.worker/e2ee.data_packet_cryptor.dart index cbb8312..a70b1ef 100644 --- a/lib/src/e2ee.worker/e2ee.data_packet_cryptor.dart +++ b/lib/src/e2ee.worker/e2ee.data_packet_cryptor.dart @@ -1,14 +1,10 @@ import 'dart:async'; import 'dart:js_interop'; -import 'dart:js_interop_unsafe'; import 'dart:math'; import 'dart:typed_data'; -// ignore: deprecated_member_use -import 'package:dart_webrtc/src/e2ee.worker/e2ee.frame_cryptor.dart' - show IV_LENGTH; -import 'package:js/js.dart'; import 'package:web/web.dart' as web; + import 'e2ee.keyhandler.dart'; import 'e2ee.logger.dart'; @@ -62,7 +58,7 @@ class E2EEDataPacketCryptor { sendCount_ = Random.secure().nextInt(0xffff); } - var sendCount = sendCount_ ?? 0; + var sendCount = sendCount_; final randomBytes = Random.secure().nextInt(max(0, 0xffffffff)).toUnsigned(32); @@ -70,7 +66,7 @@ class E2EEDataPacketCryptor { iv.setUint32(4, timestamp); iv.setUint32(8, timestamp - (sendCount % 0xffff)); - sendCount = sendCount + 1; + sendCount_ = sendCount + 1; return iv.buffer.asUint8List(); } diff --git a/lib/src/e2ee.worker/e2ee.frame_cryptor.dart b/lib/src/e2ee.worker/e2ee.frame_cryptor.dart index 079d38f..137e993 100644 --- a/lib/src/e2ee.worker/e2ee.frame_cryptor.dart +++ b/lib/src/e2ee.worker/e2ee.frame_cryptor.dart @@ -11,8 +11,6 @@ import 'e2ee.keyhandler.dart'; import 'e2ee.logger.dart'; import 'e2ee.sfi_guard.dart'; -const IV_LENGTH = 12; - const kNaluTypeMask = 0x1f; /// Coded slice of a non-IDR picture diff --git a/lib/src/e2ee.worker/e2ee.keyhandler.dart b/lib/src/e2ee.worker/e2ee.keyhandler.dart index e09f1a1..365d8e3 100644 --- a/lib/src/e2ee.worker/e2ee.keyhandler.dart +++ b/lib/src/e2ee.worker/e2ee.keyhandler.dart @@ -9,6 +9,7 @@ import 'e2ee.logger.dart'; import 'e2ee.utils.dart'; const KEYRING_SIZE = 16; +const IV_LENGTH = 12; class KeyOptions { KeyOptions({ diff --git a/lib/src/e2ee.worker/e2ee.worker.dart b/lib/src/e2ee.worker/e2ee.worker.dart index d55bed0..d5f8f10 100644 --- a/lib/src/e2ee.worker/e2ee.worker.dart +++ b/lib/src/e2ee.worker/e2ee.worker.dart @@ -469,18 +469,19 @@ void main() async { }.jsify()); } } - case 'dataCyrptorEncrypt': + break; + case 'dataCryptorEncrypt': { var participantId = msg['participantId'] as String; var data = msg['data'] as Uint8List; var keyIndex = msg['keyIndex'] as int; - var dataCryptorId = msg['dataCyrptorId'] as String; + var dataCryptorId = msg['dataCryptorId'] as String; var algorithmStr = msg['algorithm'] as String; var algorithm = Algorithm.values.firstWhereOrNull((a) => a.name == algorithmStr); if (algorithm == null) { self.postMessage({ - 'type': 'dataCyrptorEncrypt', + 'type': 'dataCryptorEncrypt', 'error': 'algorithm not found', 'msgId': msgId, 'msgType': 'response', @@ -494,7 +495,7 @@ void main() async { if (keyProvider == null) { logger.warning('KeyProvider not found for $keyProviderId'); self.postMessage({ - 'type': 'dataCyrptorEncrypt', + 'type': 'dataCryptorEncrypt', 'error': 'KeyProvider not found', 'msgId': msgId, 'msgType': 'response', @@ -507,7 +508,7 @@ void main() async { var encryptedPacket = await cryptor.encrypt(cryptor.keyHandler, data); self.postMessage({ - 'type': 'dataCyrptorEncrypt', + 'type': 'dataCryptorEncrypt', 'participantId': participantId, 'dataCryptorId': dataCryptorId, 'data': encryptedPacket!.data, @@ -519,27 +520,27 @@ void main() async { } catch (e) { logger.warning('Error encrypting data: $e'); self.postMessage({ - 'type': 'dataCyrptorEncrypt', + 'type': 'dataCryptorEncrypt', 'error': e.toString(), 'msgId': msgId, 'msgType': 'response', }.jsify()); - break; } } - case 'dataCyrptorDecrypt': + break; + case 'dataCryptorDecrypt': { var participantId = msg['participantId'] as String; var data = msg['data'] as Uint8List; var iv = msg['iv'] as Uint8List; var keyIndex = msg['keyIndex'] as int; - var dataCryptorId = msg['dataCyrptorId'] as String; + var dataCryptorId = msg['dataCryptorId'] as String; var algorithmStr = msg['algorithm'] as String; var algorithm = Algorithm.values.firstWhereOrNull((a) => a.name == algorithmStr); if (algorithm == null) { self.postMessage({ - 'type': 'dataCyrptorDecrypt', + 'type': 'dataCryptorDecrypt', 'error': 'algorithm not found', 'msgId': msgId, 'msgType': 'response', @@ -553,7 +554,7 @@ void main() async { if (keyProvider == null) { logger.warning('KeyProvider not found for $keyProviderId'); self.postMessage({ - 'type': 'dataCyrptorDecrypt', + 'type': 'dataCryptorDecrypt', 'error': 'KeyProvider not found', 'msgId': msgId, 'msgType': 'response', @@ -571,7 +572,7 @@ void main() async { iv: iv, )); self.postMessage({ - 'type': 'dataCyrptorDecrypt', + 'type': 'dataCryptorDecrypt', 'participantId': participantId, 'dataCryptorId': dataCryptorId, 'data': decryptedData, @@ -581,16 +582,15 @@ void main() async { } catch (e) { logger.warning('Error decrypting data: $e'); self.postMessage({ - 'type': 'dataCyrptorDecrypt', + 'type': 'dataCryptorDecrypt', 'error': e.toString(), 'msgId': msgId, 'msgType': 'response', }.jsify()); - break; } - break; } - case 'dataCyrptorDispose': + break; + case 'dataCryptorDispose': { var dataCryptorId = msg['dataCryptorId'] as String; logger.config('Dispose for dataCryptorId $dataCryptorId'); diff --git a/web/main.dart b/web/main.dart index ddbff41..18495cd 100644 --- a/web/main.dart +++ b/web/main.dart @@ -245,18 +245,21 @@ void loopBackTest() async { final dataPacketCryptor = await dataPacketCryptorFactory.createDataPacketCryptor( - algorithm: Algorithm.kAesGcm, keyProvider: keyProviderForSender!); + algorithm: Algorithm.kAesGcm, keyProvider: keyProviderForSender); var data = Uint8List.fromList('Hello world!'.codeUnits); + print('plain string: ${String.fromCharCodes(data)}'); print('plain data: $data'); var encryptedPacket = await dataPacketCryptor.encrypt( participantId: participantId, keyIndex: 0, data: data); print( - 'encrypted data: ${encryptedPacket?.data}, keyIndex: ${encryptedPacket?.keyIndex}, iv: ${encryptedPacket?.iv}'); + 'encrypted data: ${encryptedPacket.data}, keyIndex: ${encryptedPacket.keyIndex}, iv: ${encryptedPacket.iv}'); var decryptedData = await dataPacketCryptor.decrypt( - participantId: participantId, encryptedPacket: encryptedPacket!); + participantId: participantId, encryptedPacket: encryptedPacket); print('decrypted data: $decryptedData'); - print('decrypted string: ${String.fromCharCodes(decryptedData!)}'); + print('decrypted string: ${String.fromCharCodes(decryptedData)}'); + + await dataPacketCryptor.dispose(); /* var key1 = From b49fa55de46351daf9a767e03e2c76c9defa8d59 Mon Sep 17 00:00:00 2001 From: CloudWebRTC Date: Sat, 13 Sep 2025 17:55:23 +0800 Subject: [PATCH 22/23] Update pubspec.yaml --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 7f42fa4..659bbb9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: dart_webrtc description: Use the dart/js library to re-wrap the webrtc js interface of the browser, to adapted common browsers. -version: 1.5.3+hotfix.5 +version: 1.6.0 homepage: https://github.com/flutter-webrtc/dart-webrtc environment: From 312cbf78648d89bcbcf4e95eb72805b32b7fd23f Mon Sep 17 00:00:00 2001 From: CloudWebRTC Date: Sat, 13 Sep 2025 17:57:37 +0800 Subject: [PATCH 23/23] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5e5d3a..9845e55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog -------------------------------------------- +[1.6.0] - 2025-09-13 + +* feat: data packet cryptor. + [1.5.3+hotfix.5] - 2025-08-11 * fixed E2EE bug for Chrome rejoin.