diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 4ae46b2..78a6d59 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -13,9 +13,9 @@ jobs: - name: Checkout uses: actions/checkout@v1 - name: Publish - uses: sakebook/actions-flutter-pub-publisher@v1.3.1 + uses: k-paxian/dart-package-publisher@v1.5.1 with: - credential: ${{ secrets.CREDENTIAL_JSON }} - flutter_package: true - skip_test: true - dry_run: false + credentialJson: ${{ secrets.CREDENTIAL_JSON }} + flutter: true + skipTests: true + force: true diff --git a/CHANGELOG.md b/CHANGELOG.md index adceea3..63b67ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,71 @@ # Changelog -------------------------------------------- +[1.4.8] - 2024-07-12 + +* fix: missing streamCompleter complete for getUserMedia. +* fix: RTCPeerConnectionWeb.getRemoteStreams. + +[1.4.7] - 2024-07-12 + +* fix: MediaStreamTrack.getSettings. + +[1.4.6+hotfix.2] - 2024-06-07 + +[1.4.6+hotfix.1] - 2024-06-07 + +* Wider version dependencies for js/http. + +[1.4.6] - 2024-06-05 + +* chore: bump version for js and http. +* fix: decrypting audio when e2ee. +* fix: translate audio constraints for web. +* fix: missing fault tolerance, better worker reports and a increased timeout for worker tasks. +* fix type cast exception in getConstraints() + +[1.4.5] - 2024-05-13 + +* fix: negotiationNeeded listener. +* fix: fix type cast exception in getConstraints(). + +[1.4.4] - 2024-04-24 + +* fix: datachannel message parse for Firefox. +* fix: tryCatch editing mediaConstraints #34 + +[1.4.3] - 2024-04-18 + +* fix: do not fail if removing constraint fails + +[1.4.2] - 2024-04-15 + +* fix. + +[1.4.1] - 2024-04-12 + +* remove RTCConfiguration convert. + +[1.4.0] - 2024-04-09 + +* Fixed bug for RTCConfiguration convert. + +[1.3.3] - 2024-04-09 + +* Fix DC data parse. + +[1.3.2] - 2024-04-09 + +* Fix error when constructing RTCDataChannelInit. + +[1.3.1] - 2024-04-08 + +* Add keyRingSize/discardFrameWhenCryptorNotReady to KeyProviderOptions. + +[1.3.0] - 2024-04-08 + +* update to package:web by @jezell in #29. + [1.2.1] - 2024-02-05 * Downgrade some dependencies make more compatible. diff --git a/README.md b/README.md index c4cf777..54dc83d 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,6 @@ dart compile js ./lib/src/e2ee.worker/e2ee.worker.dart -o web/e2ee.worker.dart.j ## How to develop * `git clone https://github.com/flutter-webrtc/dart-webrtc && cd dart-webrtc` -* `pub get` -* `pub global activate webdev` +* `dart pub get` +* `dart pub global activate webdev` * `webdev serve --auto=refresh` diff --git a/lib/src/e2ee.worker/e2ee.cryptor.dart b/lib/src/e2ee.worker/e2ee.cryptor.dart index e731f2f..1aed4c1 100644 --- a/lib/src/e2ee.worker/e2ee.cryptor.dart +++ b/lib/src/e2ee.worker/e2ee.cryptor.dart @@ -5,9 +5,9 @@ import 'dart:js_util' as jsutil; import 'dart:math'; import 'dart:typed_data'; -import 'package:dart_webrtc/src/rtc_transform_stream.dart'; import 'package:web/web.dart' as web; +import 'package:dart_webrtc/src/rtc_transform_stream.dart'; import 'crypto.dart' as crypto; import 'e2ee.keyhandler.dart'; import 'e2ee.logger.dart'; @@ -301,6 +301,9 @@ class FrameCryptor { if (!enabled || // skip for encryption for empty dtx frames buffer.isEmpty) { + if (keyOptions.discardFrameWhenCryptorNotReady) { + return; + } controller.enqueue(frame); return; } @@ -405,6 +408,8 @@ class FrameCryptor { // skip for encryption for empty dtx frames buffer.isEmpty) { sifGuard.recordUserFrame(); + if (keyOptions.discardFrameWhenCryptorNotReady) return; + logger.fine('enqueing empty frame'); controller.enqueue(frame); return; } @@ -415,7 +420,7 @@ class FrameCryptor { var magicBytesBuffer = buffer.sublist( buffer.length - magicBytes.length - 1, buffer.length - 1); logger.finer( - 'magicBytesBuffer $magicBytesBuffer, magicBytes $magicBytes, '); + 'magicBytesBuffer $magicBytesBuffer, magicBytes $magicBytes'); if (magicBytesBuffer.toString() == magicBytes.toString()) { sifGuard.recordSif(); if (sifGuard.isSifAllowed()) { @@ -425,6 +430,7 @@ class FrameCryptor { finalBuffer.add(Uint8List.fromList( buffer.sublist(0, buffer.length - (magicBytes.length + 1)))); frame.data = crypto.jsArrayBufferFrom(finalBuffer.toBytes()); + logger.fine('enqueing silent frame'); controller.enqueue(frame); } else { logger.finer('SIF limit reached, dropping frame'); @@ -449,6 +455,12 @@ class FrameCryptor { initialKeySet = keyHandler.getKeySet(keyIndex); initialKeyIndex = keyIndex; + /// 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) { if (lastError != CryptorError.kMissingKey) { lastError = CryptorError.kMissingKey; @@ -462,14 +474,14 @@ class FrameCryptor { 'error': 'Missing key for track $trackId' }); } - controller.enqueue(frame); + // controller.enqueue(frame); return; } - var endDecLoop = false; var currentkeySet = initialKeySet; - while (!endDecLoop) { - try { - decrypted = await jsutil.promiseToFuture(crypto.decrypt( + + Future decryptFrameInternal() async { + decrypted = await jsutil.promiseToFuture( + crypto.decrypt( crypto.AesGcmParams( name: 'AES-GCM', iv: crypto.jsArrayBufferFrom(iv), @@ -478,56 +490,78 @@ class FrameCryptor { ), currentkeySet.encryptionKey, crypto.jsArrayBufferFrom( - buffer.sublist(headerLength, buffer.length - ivLength - 2)), - )); - - if (currentkeySet != initialKeySet) { - logger.fine( - 'ratchetKey: decryption ok, reset state to kKeyRatcheted'); - await keyHandler.setKeySetFromMaterial( - currentkeySet, initialKeyIndex); - } + buffer.sublist(headerLength, buffer.length - ivLength - 2), + ), + ), + ); + if (decrypted == null) { + throw Exception('[decryptFrameInternal] could not decrypt'); + } - endDecLoop = true; + if (currentkeySet != initialKeySet) { + logger.fine('ratchetKey: decryption ok, newState: kKeyRatcheted'); + await keyHandler.setKeySetFromMaterial( + currentkeySet, initialKeyIndex); + } - if (lastError != CryptorError.kOk && - lastError != CryptorError.kKeyRatcheted && - ratchetCount > 0) { - logger.finer( - 'KeyRatcheted: ssrc ${metaData.synchronizationSource} timestamp ${frame.timestamp} ratchetCount $ratchetCount participantId: $participantIdentity'); - logger.finer( - 'ratchetKey: lastError != CryptorError.kKeyRatcheted, reset state to kKeyRatcheted'); - - lastError = CryptorError.kKeyRatcheted; - postMessage({ - 'type': 'cryptorState', - 'msgType': 'event', - 'participantId': participantIdentity, - 'trackId': trackId, - 'kind': kind, - 'state': 'keyRatcheted', - 'error': 'Key ratcheted ok' - }); - } - } catch (e) { - lastError = CryptorError.kInternalError; - endDecLoop = ratchetCount >= keyOptions.ratchetWindowSize || - keyOptions.ratchetWindowSize <= 0; - if (endDecLoop) { - rethrow; - } - var newKeyBuffer = crypto.jsArrayBufferFrom(await keyHandler.ratchet( - currentkeySet.material, keyOptions.ratchetSalt)); - var newMaterial = await keyHandler.ratchetMaterial( - currentkeySet.material, newKeyBuffer); - currentkeySet = - await keyHandler.deriveKeys(newMaterial, keyOptions.ratchetSalt); - ratchetCount++; + if (lastError != CryptorError.kOk && + lastError != CryptorError.kKeyRatcheted && + ratchetCount > 0) { + logger.finer( + 'KeyRatcheted: ssrc ${metaData.synchronizationSource} timestamp ${frame.timestamp} ratchetCount $ratchetCount participantId: $participantIdentity'); + logger.finer( + 'ratchetKey: lastError != CryptorError.kKeyRatcheted, reset state to kKeyRatcheted'); + + lastError = CryptorError.kKeyRatcheted; + postMessage({ + 'type': 'cryptorState', + 'msgType': 'event', + 'participantId': participantIdentity, + 'trackId': trackId, + 'kind': kind, + 'state': 'keyRatcheted', + 'error': 'Key ratcheted ok' + }); } } + Future ratchedKeyInternal() async { + if (ratchetCount >= keyOptions.ratchetWindowSize || + keyOptions.ratchetWindowSize <= 0) { + throw Exception('[ratchedKeyInternal] cannot ratchet anymore'); + } + + var newKeyBuffer = crypto.jsArrayBufferFrom(await keyHandler.ratchet( + currentkeySet.material, keyOptions.ratchetSalt)); + var newMaterial = await keyHandler.ratchetMaterial( + currentkeySet.material, newKeyBuffer); + 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) { + lastError = CryptorError.kInternalError; + 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( - 'buffer: ${buffer.length}, decrypted: ${decrypted?.asUint8List().length ?? 0}'); + 'buffer: ${buffer.length}, decrypted: ${decrypted!.asUint8List().length}'); + var finalBuffer = BytesBuilder(); finalBuffer.add(Uint8List.fromList(buffer.sublist(0, headerLength))); @@ -564,15 +598,6 @@ class FrameCryptor { }); } - /// Since the key it is first send and only afterwards actually used for encrypting, there were - /// situations when the decrypting failed due to the fact that the received frame was not encrypted - /// yet and ratcheting, of course, did not solve the problem. So if we fail RATCHET_WINDOW_SIZE times, - /// we come back to the initial key. - if (initialKeySet != null) { - logger.warning( - 'decryption failed, ratcheting back to initial key, keyIndex: $initialKeyIndex'); - await keyHandler.setKeySetFromMaterial(initialKeySet, initialKeyIndex); - } keyHandler.decryptionFailure(); } } diff --git a/lib/src/e2ee.worker/e2ee.keyhandler.dart b/lib/src/e2ee.worker/e2ee.keyhandler.dart index 84f9c2a..e7feae6 100644 --- a/lib/src/e2ee.worker/e2ee.keyhandler.dart +++ b/lib/src/e2ee.worker/e2ee.keyhandler.dart @@ -8,6 +8,8 @@ import 'crypto.dart' as crypto; import 'e2ee.logger.dart'; import 'e2ee.utils.dart'; +const KEYRING_SIZE = 16; + class KeyOptions { KeyOptions({ required this.sharedKey, @@ -15,12 +17,16 @@ class KeyOptions { required this.ratchetWindowSize, this.uncryptedMagicBytes, this.failureTolerance = -1, + this.keyRingSze = KEYRING_SIZE, + this.discardFrameWhenCryptorNotReady = false, }); bool sharedKey; Uint8List ratchetSalt; int ratchetWindowSize = 0; int failureTolerance; Uint8List? uncryptedMagicBytes; + int keyRingSze; + bool discardFrameWhenCryptorNotReady; @override String toString() { @@ -77,8 +83,6 @@ class KeyProvider { } } -const KEYRING_SIZE = 16; - class KeySet { KeySet(this.material, this.encryptionKey); web.CryptoKey material; @@ -90,10 +94,15 @@ class ParticipantKeyHandler { required this.worker, required this.keyOptions, required this.participantIdentity, - }); + }) { + if (keyOptions.keyRingSze <= 0 || keyOptions.keyRingSze > 255) { + throw Exception('Invalid key ring size'); + } + cryptoKeyRing = List.filled(keyOptions.keyRingSze, null); + } int currentKeyIndex = 0; - List cryptoKeyRing = List.filled(KEYRING_SIZE, null); + late List cryptoKeyRing; bool _hasValidKey = false; diff --git a/lib/src/e2ee.worker/e2ee.worker.dart b/lib/src/e2ee.worker/e2ee.worker.dart index 17d555a..fce7f33 100644 --- a/lib/src/e2ee.worker/e2ee.worker.dart +++ b/lib/src/e2ee.worker/e2ee.worker.dart @@ -4,10 +4,10 @@ import 'dart:js_util' as js_util; import 'dart:typed_data'; import 'package:collection/collection.dart'; -import 'package:dart_webrtc/src/rtc_transform_stream.dart'; import 'package:logging/logging.dart'; import 'package:web/web.dart' as web; +import 'package:dart_webrtc/src/rtc_transform_stream.dart'; import 'e2ee.cryptor.dart'; import 'e2ee.keyhandler.dart'; import 'e2ee.logger.dart'; @@ -118,7 +118,10 @@ void main() async { uncryptedMagicBytes: options['uncryptedMagicBytes'] != null ? Uint8List.fromList( base64Decode(options['uncryptedMagicBytes'] as String)) - : null); + : null, + keyRingSze: options['keyRingSize'] ?? KEYRING_SIZE, + discardFrameWhenCryptorNotReady: + options['discardFrameWhenCryptorNotReady'] ?? false); logger.config( 'Init with keyProviderOptions:\n ${keyProviderOptions.toString()}'); diff --git a/lib/src/factory_impl.dart b/lib/src/factory_impl.dart index 9b99e79..20bd9c2 100644 --- a/lib/src/factory_impl.dart +++ b/lib/src/factory_impl.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; + import 'package:js/js.dart'; import 'package:js/js_util.dart'; import 'package:web/web.dart' as web; @@ -11,6 +12,7 @@ import 'media_stream_impl.dart'; import 'navigator_impl.dart'; import 'rtc_peerconnection_impl.dart'; import 'rtc_rtp_capailities_imp.dart'; +import 'utils.dart'; @JS('RTCRtpSender') @anonymous diff --git a/lib/src/frame_cryptor_impl.dart b/lib/src/frame_cryptor_impl.dart index a1dc9c5..4666c41 100644 --- a/lib/src/frame_cryptor_impl.dart +++ b/lib/src/frame_cryptor_impl.dart @@ -6,11 +6,12 @@ import 'dart:js_util' as jsutil; import 'dart:typed_data'; import 'package:collection/collection.dart'; -import 'package:dart_webrtc/src/event.dart'; import 'package:js/js_util.dart'; import 'package:web/web.dart' as web; import 'package:webrtc_interface/webrtc_interface.dart'; +import 'package:dart_webrtc/src/e2ee.worker/e2ee.logger.dart'; +import 'package:dart_webrtc/src/event.dart'; import 'rtc_rtp_receiver_impl.dart'; import 'rtc_rtp_sender_impl.dart'; import 'rtc_transform_stream.dart'; @@ -191,15 +192,22 @@ class KeyProviderImpl implements KeyProvider { '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, }, }) ]); await events.waitFor( - filter: (event) => event.msgId == msgId, - duration: Duration(seconds: 5)); + filter: (event) { + logger.fine('waiting for init on msg: $msgId'); + return event.msgId == msgId; + }, + duration: Duration(seconds: 15)); } @override @@ -214,8 +222,11 @@ class KeyProviderImpl implements KeyProvider { ]); await events.waitFor( - filter: (event) => event.msgId == msgId, - duration: Duration(seconds: 5)); + filter: (event) { + logger.fine('waiting for dispose on msg: $msgId'); + return event.msgId == msgId; + }, + duration: Duration(seconds: 15)); _keys.clear(); } @@ -238,8 +249,12 @@ class KeyProviderImpl implements KeyProvider { ]); await events.waitFor( - filter: (event) => event.msgId == msgId, - duration: Duration(seconds: 5)); + filter: (event) { + logger.fine('waiting for setKey on msg: $msgId'); + return event.msgId == msgId; + }, + duration: Duration(minutes: 15), + ); _keys[participantId] ??= []; if (_keys[participantId]!.length <= index) { @@ -265,8 +280,11 @@ class KeyProviderImpl implements KeyProvider { ]); var res = await events.waitFor( - filter: (event) => event.msgId == msgId, - duration: Duration(seconds: 5)); + filter: (event) { + logger.fine('waiting for ratchetKey on msg: $msgId'); + return event.msgId == msgId; + }, + duration: Duration(seconds: 15)); return base64Decode(res.data['newKey']); } @@ -286,8 +304,11 @@ class KeyProviderImpl implements KeyProvider { ]); var res = await events.waitFor( - filter: (event) => event.msgId == msgId, - duration: Duration(seconds: 5)); + filter: (event) { + logger.fine('waiting for exportKey on msg: $msgId'); + return event.msgId == msgId; + }, + duration: Duration(seconds: 15)); return base64Decode(res.data['exportedKey']); } @@ -305,8 +326,11 @@ class KeyProviderImpl implements KeyProvider { ]); var res = await events.waitFor( - filter: (event) => event.msgId == msgId, - duration: Duration(seconds: 5)); + filter: (event) { + logger.fine('waiting for exportSharedKey on msg: $msgId'); + return event.msgId == msgId; + }, + duration: Duration(seconds: 15)); return base64Decode(res.data['exportedKey']); } @@ -323,8 +347,11 @@ class KeyProviderImpl implements KeyProvider { }) ]); var res = await events.waitFor( - filter: (event) => event.msgId == msgId, - duration: Duration(seconds: 5)); + filter: (event) { + logger.fine('waiting for ratchetSharedKey on msg: $msgId'); + return event.msgId == msgId; + }, + duration: Duration(seconds: 15)); return base64Decode(res.data['newKey']); } @@ -343,8 +370,11 @@ class KeyProviderImpl implements KeyProvider { ]); await events.waitFor( - filter: (event) => event.msgId == msgId, - duration: Duration(seconds: 5)); + filter: (event) { + logger.fine('waiting for setSharedKey on msg: $msgId'); + return event.msgId == msgId; + }, + duration: Duration(seconds: 15)); } @override @@ -360,8 +390,11 @@ class KeyProviderImpl implements KeyProvider { ]); await events.waitFor( - filter: (event) => event.msgId == msgId, - duration: Duration(seconds: 5)); + filter: (event) { + logger.fine('waiting for setSifTrailer on msg: $msgId'); + return event.msgId == msgId; + }, + duration: Duration(seconds: 15)); } } diff --git a/lib/src/media_recorder_impl.dart b/lib/src/media_recorder_impl.dart index 3a3c206..12decec 100644 --- a/lib/src/media_recorder_impl.dart +++ b/lib/src/media_recorder_impl.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:js' as js; import 'dart:js_interop'; + import 'package:web/web.dart' as web; import 'package:webrtc_interface/webrtc_interface.dart'; diff --git a/lib/src/media_stream_impl.dart b/lib/src/media_stream_impl.dart index 55b15af..4f312d2 100644 --- a/lib/src/media_stream_impl.dart +++ b/lib/src/media_stream_impl.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:js_interop'; + import 'package:web/web.dart' as web; import 'package:webrtc_interface/webrtc_interface.dart'; diff --git a/lib/src/media_stream_track_impl.dart b/lib/src/media_stream_track_impl.dart index 39f6b70..abdf807 100644 --- a/lib/src/media_stream_track_impl.dart +++ b/lib/src/media_stream_track_impl.dart @@ -6,6 +6,8 @@ import 'dart:typed_data'; import 'package:web/web.dart' as web; import 'package:webrtc_interface/webrtc_interface.dart'; +import 'utils.dart'; + class MediaStreamTrackWeb extends MediaStreamTrack { MediaStreamTrackWeb(this.jsTrack) { jsTrack.addEventListener('ended', ((event) => onEnded?.call()).toJS); @@ -37,7 +39,9 @@ class MediaStreamTrackWeb extends MediaStreamTrack { @override Map getConstraints() { - return jsTrack.getConstraints() as Map; + final c = jsTrack.getConstraints(); + final jso = (c as JSObject).dartify(); + return (jso as Map).cast(); } @override @@ -47,7 +51,8 @@ class MediaStreamTrackWeb extends MediaStreamTrack { final arg = js.jsify(constraints ?? {}); final _val = await js.promiseToFuture( - js.callMethod(jsTrack, 'applyConstraints', [arg])); + js.callMethod(jsTrack, 'applyConstraints', [arg]), + ); return _val; } @@ -61,7 +66,29 @@ class MediaStreamTrackWeb extends MediaStreamTrack { @override Map getSettings() { - return jsTrack.getSettings() as Map; + 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; + } else { + _converted['width'] = settings.width; + _converted['height'] = settings.height; + _converted['aspectRatio'] = settings.aspectRatio; + _converted['frameRate'] = settings.frameRate; + if (isMobile) { + _converted['facingMode'] = settings.facingMode; + } + _converted['resizeMode'] = settings.resizeMode; + } + _converted['deviceId'] = settings.deviceId; + _converted['groupId'] = settings.groupId; + return _converted; } @override diff --git a/lib/src/mediadevices_impl.dart b/lib/src/mediadevices_impl.dart index 755c232..1f3b5cb 100644 --- a/lib/src/mediadevices_impl.dart +++ b/lib/src/mediadevices_impl.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:js' as js; import 'dart:js_interop'; import 'dart:js_util' as jsutil; + import 'package:web/web.dart' as web; import 'package:webrtc_interface/webrtc_interface.dart'; @@ -13,15 +14,41 @@ class MediaDevicesWeb extends MediaDevices { Future getUserMedia( Map mediaConstraints) async { try { - if (!isMobile) { - if (mediaConstraints['video'] is Map && - mediaConstraints['video']['facingMode'] != null) { - mediaConstraints['video'].remove('facingMode'); + try { + if (!isMobile) { + if (mediaConstraints['video'] is Map && + mediaConstraints['video']['facingMode'] != null) { + mediaConstraints['video'].remove('facingMode'); + } } + mediaConstraints.putIfAbsent('video', () => false); + mediaConstraints.putIfAbsent('audio', () => false); + } catch (e) { + print( + '[getUserMedia] failed to remove facingMode from mediaConstraints'); + } + try { + if (mediaConstraints['audio'] is Map && + Map.from(mediaConstraints['audio']).containsKey('optional') && + mediaConstraints['audio']['optional'] + is List>) { + List> optionalValues = + mediaConstraints['audio']['optional']; + final audioMap = {}; + + optionalValues.forEach((option) { + option.forEach((key, value) { + audioMap[key] = value; + }); + }); + + mediaConstraints['audio'].remove('optional'); + mediaConstraints['audio'].addAll(audioMap); + } + } catch (e, s) { + print( + '[getUserMedia] failed to translate optional audio constraints, $e, $s'); } - - mediaConstraints.putIfAbsent('video', () => false); - mediaConstraints.putIfAbsent('audio', () => false); final mediaDevices = web.window.navigator.mediaDevices; diff --git a/lib/src/rtc_data_channel_impl.dart b/lib/src/rtc_data_channel_impl.dart index 230e13d..697d75c 100644 --- a/lib/src/rtc_data_channel_impl.dart +++ b/lib/src/rtc_data_channel_impl.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:js_interop'; -import 'dart:js_util' as jsutil; + +import 'package:js/js_util.dart' as jsutil; import 'package:web/web.dart' as web; import 'package:webrtc_interface/webrtc_interface.dart'; @@ -70,12 +71,13 @@ class RTCDataChannelWeb extends RTCDataChannel { return RTCDataChannelMessage(data); } dynamic arrayBuffer; - if (data is web.Blob) { - // This should never happen actually + if (data is JSArrayBuffer) { + arrayBuffer = data.toDart; + } else if (data is web.Blob) { arrayBuffer = await jsutil .promiseToFuture(jsutil.callMethod(data, 'arrayBuffer', [])); } else { - arrayBuffer = data; + arrayBuffer = data.toDart; } return RTCDataChannelMessage.fromBinary(arrayBuffer.asUint8List()); } diff --git a/lib/src/rtc_peerconnection_impl.dart b/lib/src/rtc_peerconnection_impl.dart index 17e6f04..9b81367 100644 --- a/lib/src/rtc_peerconnection_impl.dart +++ b/lib/src/rtc_peerconnection_impl.dart @@ -4,13 +4,11 @@ import 'dart:js_interop'; import 'dart:js_interop_unsafe'; import 'dart:js_util' as jsutil; -import 'package:dart_webrtc/dart_webrtc.dart'; import 'package:js/js_util.dart'; import 'package:platform_detect/platform_detect.dart'; import 'package:web/web.dart' as web; -import 'package:webrtc_interface/webrtc_interface.dart'; -import 'media_stream_impl.dart'; +import 'package:dart_webrtc/dart_webrtc.dart'; import 'media_stream_track_impl.dart'; import 'rtc_data_channel_impl.dart'; import 'rtc_dtmf_sender_impl.dart'; @@ -18,46 +16,15 @@ import 'rtc_rtp_receiver_impl.dart'; import 'rtc_rtp_sender_impl.dart'; import 'rtc_rtp_transceiver_impl.dart'; +extension on web.RTCDataChannelInit { + external set binaryType(String value); +} + /* * PeerConnection */ class RTCPeerConnectionWeb extends RTCPeerConnection { RTCPeerConnectionWeb(this._peerConnectionId, this._jsPc) { - _jsPc.addEventListener( - 'addstream', - (_RTCMediaStreamEvent mediaStreamEvent) { - final jsStream = mediaStreamEvent.stream; - - final _remoteStream = _remoteStreams.putIfAbsent( - jsStream.id, () => MediaStreamWeb(jsStream, _peerConnectionId)); - - onAddStream?.call(_remoteStream); - - jsStream.addEventListener( - 'addtrack', - (web.RTCTrackEvent mediaStreamTrackEvent) { - final jsTrack = - (mediaStreamTrackEvent as web.MediaStreamTrackEvent).track; - final track = MediaStreamTrackWeb(jsTrack); - _remoteStream.addTrack(track, addToNative: false).then((_) { - onAddTrack?.call(_remoteStream, track); - }); - }.toJS); - - jsStream.addEventListener( - 'removetrack', - (web.RTCTrackEvent mediaStreamTrackEvent) { - final jsTrack = - (mediaStreamTrackEvent as web.MediaStreamTrackEvent).track; - final track = MediaStreamTrackWeb(jsTrack); - _remoteStream - .removeTrack(track, removeFromNative: false) - .then((_) { - onRemoveTrack?.call(_remoteStream, track); - }); - }.toJS); - }.toJS); - _jsPc.addEventListener( 'datachannel', (dataChannelEvent) { @@ -119,16 +86,6 @@ class RTCPeerConnectionWeb extends RTCPeerConnection { onIceGatheringState?.call(_iceGatheringState!); })); - _jsPc.addEventListener( - 'removestream', - (_RTCMediaStreamEvent mediaStreamEvent) { - final _remoteStream = - _remoteStreams.remove(mediaStreamEvent.stream.id); - if (_remoteStream != null) { - onRemoveStream?.call(_remoteStream); - } - }.toJS); - _jsPc.addEventListener( 'signalingstatechange', (_) { @@ -147,7 +104,7 @@ class RTCPeerConnectionWeb extends RTCPeerConnection { } _jsPc.addEventListener( - 'onnegotiationneeded', + 'negotiationneeded', (_) { onRenegotiationNeeded?.call(); }.toJS); @@ -172,7 +129,6 @@ class RTCPeerConnectionWeb extends RTCPeerConnection { final String _peerConnectionId; late final web.RTCPeerConnection _jsPc; final _localStreams = {}; - final _remoteStreams = {}; final _configuration = {}; RTCSignalingState? _signalingState; @@ -261,13 +217,7 @@ class RTCPeerConnectionWeb extends RTCPeerConnection { @override Future setConfiguration(Map configuration) { _configuration.addAll(configuration); - - final object = jsutil.newObject(); - for (var key in configuration.keys) { - jsutil.setProperty(object, key, configuration[key]); - } - - _jsPc.setConfiguration(object as web.RTCConfiguration); + _jsPc.setConfiguration(jsify(configuration) as web.RTCConfiguration); return Future.value(); } @@ -389,27 +339,35 @@ class RTCPeerConnectionWeb extends RTCPeerConnection { List getRemoteStreams() => _jsPc .getRemoteStreams() .toDart - .map((jsStream) => _remoteStreams[jsStream.id]!) + .map((e) => MediaStreamWeb(e, _peerConnectionId)) .toList(); @override Future createDataChannel( String label, RTCDataChannelInit dataChannelDict) { - final map = dataChannelDict.toMap(); + var dcInit = web.RTCDataChannelInit( + id: dataChannelDict.id, + ordered: dataChannelDict.ordered, + protocol: dataChannelDict.protocol, + negotiated: dataChannelDict.negotiated, + ); + if (dataChannelDict.binaryType == 'binary') { - map['binaryType'] = 'arraybuffer'; // Avoid Blob in data channel + dcInit.binaryType = 'arraybuffer'; // Avoid Blob in data channel + } + + if (dataChannelDict.maxRetransmits > 0) { + dcInit.maxRetransmits = dataChannelDict.maxRetransmits; + } + + if (dataChannelDict.maxRetransmitTime > 0) { + dcInit.maxPacketLifeTime = dataChannelDict.maxRetransmitTime; } final jsDc = _jsPc.createDataChannel( - label, - web.RTCDataChannelInit( - id: map['id'], - ordered: map['ordered'], - maxPacketLifeTime: map['maxPacketLifeTime'], - maxRetransmits: map['maxRetransmits'], - protocol: map['protocol'], - negotiated: map['negotiated'], - )); + label, + dcInit, + ); return Future.value(RTCDataChannelWeb(jsDc)); } diff --git a/lib/src/rtc_rtp_capailities_imp.dart b/lib/src/rtc_rtp_capailities_imp.dart index 4174d70..9a21314 100644 --- a/lib/src/rtc_rtp_capailities_imp.dart +++ b/lib/src/rtc_rtp_capailities_imp.dart @@ -1,4 +1,5 @@ import 'dart:js_util' as jsutil; + import 'package:webrtc_interface/webrtc_interface.dart'; class RTCRtpCapabilitiesWeb { diff --git a/lib/src/rtc_rtp_parameters_impl.dart b/lib/src/rtc_rtp_parameters_impl.dart index 94405b5..e9ed0c2 100644 --- a/lib/src/rtc_rtp_parameters_impl.dart +++ b/lib/src/rtc_rtp_parameters_impl.dart @@ -1,4 +1,5 @@ import 'dart:js_util' as jsutil; + import 'package:webrtc_interface/webrtc_interface.dart'; class RTCRtpParametersWeb { diff --git a/lib/src/rtc_rtp_receiver_impl.dart b/lib/src/rtc_rtp_receiver_impl.dart index e3cc1a8..d2c8c64 100644 --- a/lib/src/rtc_rtp_receiver_impl.dart +++ b/lib/src/rtc_rtp_receiver_impl.dart @@ -1,4 +1,5 @@ import 'dart:js_util' as jsutil; + import 'package:web/web.dart' as web; import 'package:webrtc_interface/webrtc_interface.dart'; diff --git a/lib/src/rtc_rtp_sender_impl.dart b/lib/src/rtc_rtp_sender_impl.dart index b7d3b9c..643020e 100644 --- a/lib/src/rtc_rtp_sender_impl.dart +++ b/lib/src/rtc_rtp_sender_impl.dart @@ -1,10 +1,10 @@ import 'dart:async'; import 'dart:js_util' as jsutil; -import 'package:dart_webrtc/src/media_stream_impl.dart'; import 'package:web/web.dart' as web; import 'package:webrtc_interface/webrtc_interface.dart'; +import 'package:dart_webrtc/src/media_stream_impl.dart'; import 'media_stream_track_impl.dart'; import 'rtc_dtmf_sender_impl.dart'; import 'rtc_rtp_parameters_impl.dart'; diff --git a/lib/src/rtc_transform_stream.dart b/lib/src/rtc_transform_stream.dart index d2b1952..f185a4f 100644 --- a/lib/src/rtc_transform_stream.dart +++ b/lib/src/rtc_transform_stream.dart @@ -1,5 +1,6 @@ import 'dart:js_util' as js_util; import 'dart:typed_data'; + import 'package:js/js.dart'; import 'package:web/web.dart'; diff --git a/lib/src/rtc_video_element.dart b/lib/src/rtc_video_element.dart index 6901a97..04d7416 100644 --- a/lib/src/rtc_video_element.dart +++ b/lib/src/rtc_video_element.dart @@ -1,5 +1,7 @@ import 'dart:js_interop'; + import 'package:web/web.dart' as web; + import '../dart_webrtc.dart'; class RTCVideoElement { diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 7bd218e..30bd835 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -1,4 +1,6 @@ +import 'dart:js_util' as jsutil; import 'dart:math'; + import 'package:web/web.dart' as web; bool get isMobile { diff --git a/pubspec.yaml b/pubspec.yaml index 51e76cc..ebcf797 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.3.0 +version: 1.4.8 homepage: https://github.com/flutter-webrtc/dart-webrtc environment: @@ -8,18 +8,18 @@ environment: dependencies: collection: ^1.17.1 - js: ^0.6.4 + js: ">0.6.0 <0.8.0" logging: ^1.1.0 meta: ^1.8.0 platform_detect: ^2.0.7 synchronized: ^3.0.0+3 web: ^1.0.0 - webrtc_interface: ^1.1.2 + webrtc_interface: ^1.2.0 dev_dependencies: build_runner: ^2.3.3 build_web_compilers: - http: ^0.13.3 + http: ">0.13.0 <1.3.0" import_sorter: ^4.6.0 pedantic: ^1.9.0 protoo_client: ^0.3.0 diff --git a/web/main.dart b/web/main.dart index 8dd8300..dd071fb 100644 --- a/web/main.dart +++ b/web/main.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:typed_data'; import 'package:dart_webrtc/dart_webrtc.dart'; @@ -49,7 +50,8 @@ void loopBackTest() async { sharedKey: false, ratchetWindowSize: 16, failureTolerance: -1, - ratchetSalt: Uint8List.fromList('testSalt'.codeUnits)); + ratchetSalt: Uint8List.fromList('testSalt'.codeUnits), + discardFrameWhenCryptorNotReady: true); var keyProvider = await frameCryptorFactory.createDefaultKeyProvider(keyProviderOptions); @@ -73,9 +75,18 @@ void loopBackTest() async { receiver: event.receiver!, algorithm: Algorithm.kAesGcm, keyProvider: keyProvider); - await fc.setEnabled(true); + if (keyProviderOptions.discardFrameWhenCryptorNotReady) { + Timer(Duration(seconds: 1), () { + fc.setEnabled(true); + }); + } else { + await fc.setEnabled(true); + } + await fc.setKeyIndex(0); - await fc.updateCodec('vp8'); + if (event.track.kind == 'video') { + await fc.updateCodec('vp8'); + } pc2FrameCryptors.add(fc); }; pc2.onConnectionState = (state) { @@ -129,7 +140,9 @@ void loopBackTest() async { keyProvider: keyProvider); await fc.setEnabled(true); await fc.setKeyIndex(0); - await fc.updateCodec('vp8'); + if (track.kind == 'video') { + await fc.updateCodec('vp8'); + } pc1FrameCryptors.add(fc); }); /* @@ -154,6 +167,8 @@ void loopBackTest() async { } }); */ + var dc = await pc1.createDataChannel( + 'label', RTCDataChannelInit()..binaryType = 'binary'); var offer = await pc1.createOffer(); await pc2.addTransceiver(