diff --git a/CHANGELOG.md b/CHANGELOG.md index 82999b3..9845e55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,41 @@ # 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. + +[1.5.3+hotfix.4] - 2025-07-29 + +* fixed E2EE issue for Chrome. + +[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. + +[1.5.3+hotfix.1] - 2025-04-25 + +* add getter override for dc.bufferedAmountLowThreshold. + +[1.5.3] - 2025-03-24 + +* add getBufferedAmount for DC. + +[1.5.2+hotfix.1] - 2025-02-23. + +* remove platform_detect. + +[1.5.2] - 2025-02-23. + +* fix stats for web. [1.5.1] - 2025-02-15 diff --git a/lib/dart_webrtc.dart b/lib/dart_webrtc.dart index 744176d..fdd0488 100644 --- a/lib/dart_webrtc.dart +++ b/lib/dart_webrtc.dart @@ -3,8 +3,10 @@ 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'; export 'src/media_stream_impl.dart'; +export 'src/media_stream_track_impl.dart'; export 'src/rtc_video_element.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..96d2b17 --- /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 _dataCryptorId = 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': 'dataCryptorEncrypt', + 'msgId': msgId, + 'keyProviderId': keyProvider.id, + 'dataCryptorId': _dataCryptorId, + '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': 'dataCryptorDecrypt', + 'msgId': msgId, + 'keyProviderId': keyProvider.id, + 'dataCryptorId': _dataCryptorId, + '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': 'dataCryptorDispose', + 'msgId': msgId, + 'dataCryptorId': _dataCryptorId + }.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..a70b1ef --- /dev/null +++ b/lib/src/e2ee.worker/e2ee.data_packet_cryptor.dart @@ -0,0 +1,230 @@ +import 'dart:async'; +import 'dart:js_interop'; +import 'dart:math'; +import 'dart:typed_data'; + +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_; + 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 99% rename from lib/src/e2ee.worker/e2ee.cryptor.dart rename to lib/src/e2ee.worker/e2ee.frame_cryptor.dart index c3e6a15..137e993 100644 --- a/lib/src/e2ee.worker/e2ee.cryptor.dart +++ b/lib/src/e2ee.worker/e2ee.frame_cryptor.dart @@ -4,14 +4,13 @@ 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'; 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.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/e2ee.worker/e2ee.worker.dart b/lib/src/e2ee.worker/e2ee.worker.dart index 729390f..d5f8f10 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; @@ -436,6 +470,139 @@ void main() async { } } break; + case 'dataCryptorEncrypt': + { + var participantId = msg['participantId'] as String; + var data = msg['data'] as Uint8List; + var keyIndex = msg['keyIndex'] as int; + 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': 'dataCryptorEncrypt', + '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': 'dataCryptorEncrypt', + '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': 'dataCryptorEncrypt', + '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': 'dataCryptorEncrypt', + 'error': e.toString(), + 'msgId': msgId, + 'msgType': 'response', + }.jsify()); + } + } + 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['dataCryptorId'] as String; + var algorithmStr = msg['algorithm'] as String; + var algorithm = + Algorithm.values.firstWhereOrNull((a) => a.name == algorithmStr); + if (algorithm == null) { + self.postMessage({ + 'type': 'dataCryptorDecrypt', + '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': 'dataCryptorDecrypt', + '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': 'dataCryptorDecrypt', + 'participantId': participantId, + 'dataCryptorId': dataCryptorId, + 'data': decryptedData, + 'msgId': msgId, + 'msgType': 'response', + }.jsify()); + } catch (e) { + logger.warning('Error decrypting data: $e'); + self.postMessage({ + 'type': 'dataCryptorDecrypt', + 'error': e.toString(), + 'msgId': msgId, + 'msgType': 'response', + }.jsify()); + } + } + break; + case 'dataCryptorDispose': + { + 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/lib/src/frame_cryptor_impl.dart b/lib/src/frame_cryptor_impl.dart index e8365f7..2156697 100644 --- a/lib/src/frame_cryptor_impl.dart +++ b/lib/src/frame_cryptor_impl.dart @@ -14,80 +14,27 @@ 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; 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, - {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; @@ -101,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; } @@ -124,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; } @@ -137,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; } @@ -150,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(), + ); } } @@ -172,71 +124,83 @@ 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: 5), + onTimeout: () => throw Exception('waiting for init on msg timed out'), + ); } @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: 5), + onTimeout: () => throw Exception('waiting for dispose on msg timed out'), + ); _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) { 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] ??= []; @@ -249,45 +213,59 @@ 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: 5), + onTimeout: () => + throw Exception('waiting for ratchetKey on msg timed out'), + ); 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: 5), + onTimeout: () => + throw Exception('waiting for exportKey on msg timed out'), + ); return base64Decode(res.data['exportedKey']); } @@ -295,19 +273,24 @@ 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: 5), + onTimeout: () => + throw Exception('waiting for exportSharedKey on msg timed out'), + ); return base64Decode(res.data['exportedKey']); } @@ -315,18 +298,23 @@ 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: 5), + onTimeout: () => + throw Exception('waiting for ratchetSharedKey on msg timed out'), + ); return base64Decode(res.data['newKey']); } @@ -334,38 +322,48 @@ 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: 5), + onTimeout: () => + throw Exception('waiting for setSharedKey on msg timed out'), + ); } @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: 5), + onTimeout: () => + throw Exception('waiting for setSifTrailer on msg timed out'), + ); } } @@ -387,7 +385,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) { @@ -414,8 +413,10 @@ class FrameCryptorFactoryImpl implements FrameCryptorFactory { FrameCryptorState.FrameCryptorStateKeyRatcheted; break; } - frameCryptor?.onFrameCryptorStateChanged - ?.call(participantId, frameCryptorState); + frameCryptor?.onFrameCryptorStateChanged?.call( + participantId, + frameCryptorState, + ); } } }; @@ -437,22 +438,28 @@ 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.hashCode.toString(); + var trackId = jsReceiver.track.id; var kind = jsReceiver.track.kind; if (web.window.hasProperty('RTCRtpScriptTransform'.toJS).toDart) { @@ -469,49 +476,64 @@ 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 insertableStreams = jsReceiver.getProperty('insertableStreams'.toJS) + as RTCInsertableStreams?; + + var exist = insertableStreams != null; + + if (insertableStreams == null) { + insertableStreams = jsReceiver.callMethod( + 'createEncodedStreams'.toJS, + ); + jsReceiver.setProperty('insertableStreams'.toJS, insertableStreams); } + + var readable = insertableStreams.readable; + var writable = insertableStreams.writable; 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, - 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.hashCode.toString(); + var trackId = jsSender.track?.id ?? sender.senderId; var kind = jsSender.track!.kind; if (web.window.hasProperty('RTCRtpScriptTransform'.toJS).toDart) { @@ -529,39 +551,53 @@ 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 insertableStreams = jsSender.getProperty('insertableStreams'.toJS) + as RTCInsertableStreams?; + + var exist = insertableStreams != null; + + if (insertableStreams == null) { + insertableStreams = jsSender.callMethod( + 'createEncodedStreams'.toJS, + ); + jsSender.setProperty('insertableStreams'.toJS, insertableStreams); } + + var readable = insertableStreams.readable; + var writable = insertableStreams.writable; + 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); + this, + worker, + participantId, + trackId, + jsSender: jsSender, + keyProvider: keyProvider, + ); _frameCryptors[trackId] = cryptor; + return Future.value(cryptor); } diff --git a/lib/src/media_recorder.dart b/lib/src/media_recorder.dart index 7ed9dd3..5a2a4e6 100644 --- a/lib/src/media_recorder.dart +++ b/lib/src/media_recorder.dart @@ -7,8 +7,11 @@ 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, + }) => _delegate.start(path, videoTrack: videoTrack, audioChannel: audioChannel); @override diff --git a/lib/src/media_recorder_impl.dart b/lib/src/media_recorder_impl.dart index c03a9bc..38bc50e 100644 --- a/lib/src/media_recorder_impl.dart +++ b/lib/src/media_recorder_impl.dart @@ -15,9 +15,7 @@ class MediaRecorderWeb extends MediaRecorder { Future start( String path, { MediaStreamTrack? videoTrack, - MediaStreamTrack? audioTrack, RecorderAudioChannel? audioChannel, - int? rotation, }) { throw 'Use startWeb on Flutter Web!'; } diff --git a/lib/src/media_stream_track_impl.dart b/lib/src/media_stream_track_impl.dart index d52066a..fb16d46 100644 --- a/lib/src/media_stream_track_impl.dart +++ b/lib/src/media_stream_track_impl.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; import 'dart:typed_data'; import 'package:web/web.dart' as web; @@ -77,25 +78,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; } diff --git a/lib/src/rtc_data_channel_impl.dart b/lib/src/rtc_data_channel_impl.dart index 71218d1..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'; @@ -8,37 +9,29 @@ class RTCDataChannelWeb extends RTCDataChannel { RTCDataChannelWeb(this._jsDc) { stateChangeStream = _stateChangeController.stream; messageStream = _messageController.stream; - _jsDc.addEventListener( - 'close', - (web.Event _) { - _state = RTCDataChannelState.RTCDataChannelClosed; - _stateChangeController.add(_state); - onDataChannelState?.call(_state); - }.toJS, - false.toJS); - _jsDc.addEventListener( - 'open', - (web.Event _) { - _state = RTCDataChannelState.RTCDataChannelOpen; - _stateChangeController.add(_state); - onDataChannelState?.call(_state); - }.toJS, - false.toJS); - _jsDc.addEventListener( - 'message', - (web.MessageEvent event) { - _parse(event.data).then((msg) { - _messageController.add(msg); - onMessage?.call(msg); - }); - }.toJS, - false.toJS); - _jsDc.addEventListener( - 'bufferedamountlow', - (web.Event _) { - onBufferedAmountLow?.call(bufferedAmount ?? 0); - }.toJS, - false.toJS); + + _jsDc.onclose = (web.Event _) { + _state = RTCDataChannelState.RTCDataChannelClosed; + _stateChangeController.add(_state); + onDataChannelState?.call(_state); + }.toJS; + + _jsDc.onopen = (web.Event _) { + _state = RTCDataChannelState.RTCDataChannelOpen; + _stateChangeController.add(_state); + onDataChannelState?.call(_state); + }.toJS; + + _jsDc.onmessage = (web.MessageEvent event) { + _parse(event.data.dartify()).then((msg) { + _messageController.add(msg); + onMessage?.call(msg); + }); + }.toJS; + + _jsDc.onbufferedamountlow = (web.Event _) { + onBufferedAmountLow?.call(bufferedAmount ?? 0); + }.toJS; } final web.RTCDataChannel _jsDc; @@ -56,6 +49,14 @@ class RTCDataChannelWeb extends RTCDataChannel { @override int? get bufferedAmount => _jsDc.bufferedAmount; + @override + Future getBufferedAmount() async { + return _jsDc.bufferedAmount; + } + + @override + int? get bufferedAmountLowThreshold => _jsDc.bufferedAmountLowThreshold; + @override set bufferedAmountLowThreshold(int? bufferedAmountLowThreshold) { _jsDc.bufferedAmountLowThreshold = bufferedAmountLowThreshold ?? 0; @@ -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/lib/src/rtc_peerconnection_impl.dart b/lib/src/rtc_peerconnection_impl.dart index 1cc958c..f0c8b99 100644 --- a/lib/src/rtc_peerconnection_impl.dart +++ b/lib/src/rtc_peerconnection_impl.dart @@ -1,8 +1,9 @@ import 'dart:async'; +import 'dart:collection'; import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; import 'package:dart_webrtc/dart_webrtc.dart'; -import 'package:platform_detect/platform_detect.dart'; import 'package:web/web.dart' as web; import 'media_stream_track_impl.dart'; @@ -42,7 +43,7 @@ class RTCPeerConnectionWeb extends RTCPeerConnection { iceConnectionStateForString(_jsPc.iceConnectionState); onIceConnectionState?.call(_iceConnectionState!); - if (browser.isFirefox) { + if (web.Device.isFirefox) { switch (_iceConnectionState!) { case RTCIceConnectionState.RTCIceConnectionStateNew: _connectionState = RTCPeerConnectionState.RTCPeerConnectionStateNew; @@ -91,7 +92,7 @@ class RTCPeerConnectionWeb extends RTCPeerConnection { _jsPc.addEventListener('signalingstatechange', onSignalingStateChange.toJS); - if (!browser.isFirefox) { + if (!web.Device.isFirefox) { final void Function(JSAny) onConnectionStateChange = (_) { _connectionState = peerConnectionStateForString(_jsPc.connectionState); onConnectionState?.call(_connectionState!); @@ -157,7 +158,7 @@ class RTCPeerConnectionWeb extends RTCPeerConnection { @override Future getIceConnectionState() async { _iceConnectionState = iceConnectionStateForString(_jsPc.iceConnectionState); - if (browser.isFirefox) { + if (web.Device.isFirefox) { switch (_iceConnectionState!) { case RTCIceConnectionState.RTCIceConnectionStateNew: _connectionState = RTCPeerConnectionState.RTCPeerConnectionStateNew; @@ -194,7 +195,8 @@ class RTCPeerConnectionWeb extends RTCPeerConnection { @override Future getConnectionState() async { - if (browser.isFirefox) { + /// platform is Firefox + if (web.Device.isFirefox) { await getIceConnectionState(); } else { _connectionState = peerConnectionStateForString(_jsPc.connectionState); @@ -312,7 +314,7 @@ class RTCPeerConnectionWeb extends RTCPeerConnection { @override Future> getStats([MediaStreamTrack? track]) async { - var stats; + web.RTCStatsReport stats; if (track != null) { var jsTrack = (track as MediaStreamTrackWeb).jsTrack; stats = await _jsPc.getStats(jsTrack).toDart; @@ -321,10 +323,20 @@ class RTCPeerConnectionWeb extends RTCPeerConnection { } var report = []; - stats.forEach((key, value) { - report.add( - StatsReport(value['id'], value['type'], value['timestamp'], value)); - }); + stats.callMethodVarArgs('forEach'.toJS, [ + (JSObject value, JSAny key) { + var map = value.dartify() as LinkedHashMap; + var stats = {}; + for (var entry in map.entries) { + stats[(entry.key as JSString).toDart] = entry.value; + } + report.add(StatsReport( + value.getProperty('id'.toJS).toDart, + value.getProperty('type'.toJS).toDart, + value.getProperty('timestamp'.toJS).toDartDouble, + stats)); + }.toJS, + ]); return report; } diff --git a/lib/src/rtc_rtp_parameters_impl.dart b/lib/src/rtc_rtp_parameters_impl.dart index b192dd9..f1a0b59 100644 --- a/lib/src/rtc_rtp_parameters_impl.dart +++ b/lib/src/rtc_rtp_parameters_impl.dart @@ -70,15 +70,18 @@ class RTCHeaderExtensionWeb { class RTCRtpEncodingWeb { static RTCRtpEncoding fromJsObject(web.RTCRtpEncodingParameters object) { return RTCRtpEncoding.fromMap({ - 'rid': object.rid, + 'rid': object.getProperty('rid'.toJS).dartify(), 'active': object.active, - 'maxBitrate': object.maxBitrate, - 'maxFramerate': object.maxFramerate.toInt(), + 'maxBitrate': object.getProperty('maxBitrate'.toJS)?.toDartInt, + 'maxFramerate': + object.getProperty('maxFramerate'.toJS)?.toDartInt, 'minBitrate': object.getProperty('minBitrate'.toJS)?.toDartInt, 'numTemporalLayers': object.getProperty('numTemporalLayers'.toJS)?.toDartInt, - 'scaleResolutionDownBy': object.scaleResolutionDownBy, - 'ssrc': object.getProperty('ssrc'.toJS)?.toDart + 'scaleResolutionDownBy': object + .getProperty('scaleResolutionDownBy'.toJS) + ?.toDartDouble, + 'ssrc': object.getProperty('ssrc'.toJS)?.toDartInt }); } } diff --git a/lib/src/rtc_rtp_receiver_impl.dart b/lib/src/rtc_rtp_receiver_impl.dart index 99db83d..fd56929 100644 --- a/lib/src/rtc_rtp_receiver_impl.dart +++ b/lib/src/rtc_rtp_receiver_impl.dart @@ -1,4 +1,6 @@ +import 'dart:collection'; import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; import 'package:web/web.dart' as web; import 'package:webrtc_interface/webrtc_interface.dart'; @@ -14,12 +16,22 @@ class RTCRtpReceiverWeb extends RTCRtpReceiver { @override Future> getStats() async { - var stats = (await _jsRtpReceiver.getStats().toDart) as JSObject; + var stats = await _jsRtpReceiver.getStats().toDart; var report = []; - (stats.dartify() as Map).forEach((key, value) { - report.add( - StatsReport(value['id'], value['type'], value['timestamp'], value)); - }); + stats.callMethodVarArgs('forEach'.toJS, [ + (JSObject value, JSAny key) { + var map = value.dartify() as LinkedHashMap; + var stats = {}; + for (var entry in map.entries) { + stats[(entry.key as JSString).toDart] = entry.value; + } + report.add(StatsReport( + value.getProperty('id'.toJS).toDart, + value.getProperty('type'.toJS).toDart, + value.getProperty('timestamp'.toJS).toDartDouble, + stats)); + }.toJS, + ]); return report; } diff --git a/lib/src/rtc_rtp_sender_impl.dart b/lib/src/rtc_rtp_sender_impl.dart index ae20e6c..c62aaaf 100644 --- a/lib/src/rtc_rtp_sender_impl.dart +++ b/lib/src/rtc_rtp_sender_impl.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:collection'; import 'dart:js_interop'; import 'dart:js_interop_unsafe'; @@ -85,10 +86,20 @@ class RTCRtpSenderWeb extends RTCRtpSender { Future> getStats() async { var stats = await _jsRtpSender.getStats().toDart; var report = []; - (stats.dartify() as Map).forEach((key, value) { - report.add( - StatsReport(value['id'], value['type'], value['timestamp'], value)); - }); + stats.callMethodVarArgs('forEach'.toJS, [ + (JSObject value, JSAny key) { + var map = value.dartify() as LinkedHashMap; + var stats = {}; + for (var entry in map.entries) { + stats[(entry.key as JSString).toDart] = entry.value; + } + report.add(StatsReport( + value.getProperty('id'.toJS).toDart, + value.getProperty('type'.toJS).toDart, + value.getProperty('timestamp'.toJS).toDartDouble, + stats)); + }.toJS, + ]); return report; } diff --git a/pubspec.yaml b/pubspec.yaml index a39e83a..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.1 +version: 1.6.0 homepage: https://github.com/flutter-webrtc/dart-webrtc environment: @@ -11,10 +11,9 @@ dependencies: js: ">0.6.0 <0.8.0" logging: ^1.1.0 meta: ^1.8.0 - platform_detect: ^2.1.0 synchronized: ^3.0.0+3 web: ^1.0.0 - webrtc_interface: ^1.2.1 + webrtc_interface: ^1.3.0 dev_dependencies: build_runner: ^2.3.3 @@ -23,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 7bcbe7e..18495cd 100644 --- a/web/main.dart +++ b/web/main.dart @@ -238,6 +238,29 @@ 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 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}'); + var decryptedData = await dataPacketCryptor.decrypt( + participantId: participantId, encryptedPacket: encryptedPacket); + print('decrypted data: $decryptedData'); + print('decrypted string: ${String.fromCharCodes(decryptedData)}'); + + await dataPacketCryptor.dispose(); + /* var key1 = await keyProviderForSender.ratchetKey(index: 0, participantId: 'sender'); @@ -254,4 +277,27 @@ 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(); + + print('senders: ${senders.length}'); + print('receivers: ${receivers.length}'); + + senders.forEach((sender) { + sender.getStats().then((stats) { + print( + 'sender stats: ${stats.map((e) => 'id: ${e.id}, type: ${e.type}, timestamp: ${e.timestamp}, values: ${e.values.toString()} ')}'); + }); + }); + + receivers.forEach((receiver) { + receiver.getStats().then((stats) { + print( + 'receiver stats: ${stats.map((e) => 'id: ${e.id}, type: ${e.type}, timestamp: ${e.timestamp}, values: ${e.values.toString()} ')}'); + }); + }); + }); + */ }