diff --git a/lib/src/e2ee.worker/crypto.dart b/lib/src/e2ee.worker/crypto.dart index 6d9b912..4770044 100644 --- a/lib/src/e2ee.worker/crypto.dart +++ b/lib/src/e2ee.worker/crypto.dart @@ -1,9 +1,9 @@ import 'dart:async'; -import 'dart:html' as html; import 'dart:js_util' as jsutil; import 'dart:typed_data'; import 'package:js/js.dart'; +import 'package:web/web.dart' as web; @JS('Promise') class Promise { @@ -18,14 +18,14 @@ class Algorithm { @JS('crypto.subtle.encrypt') external Promise encrypt( dynamic algorithm, - html.CryptoKey key, + web.CryptoKey key, ByteBuffer data, ); @JS('crypto.subtle.decrypt') external Promise decrypt( dynamic algorithm, - html.CryptoKey key, + web.CryptoKey key, ByteBuffer data, ); @@ -52,7 +52,7 @@ ByteBuffer jsArrayBufferFrom(List data) { } @JS('crypto.subtle.importKey') -external Promise importKey( +external Promise importKey( String format, ByteBuffer keyData, dynamic algorithm, @@ -63,13 +63,13 @@ external Promise importKey( @JS('crypto.subtle.exportKey') external Promise exportKey( String format, - html.CryptoKey key, + web.CryptoKey key, ); @JS('crypto.subtle.deriveKey') -external Promise deriveKey( +external Promise deriveKey( dynamic algorithm, - html.CryptoKey baseKey, + web.CryptoKey baseKey, dynamic derivedKeyAlgorithm, bool extractable, List keyUsages); @@ -77,14 +77,14 @@ external Promise deriveKey( @JS('crypto.subtle.deriveBits') external Promise deriveBits( dynamic algorithm, - html.CryptoKey baseKey, + web.CryptoKey baseKey, int length, ); -Future impportKeyFromRawData(List secretKeyData, +Future impportKeyFromRawData(List secretKeyData, {required String webCryptoAlgorithm, required List keyUsages}) async { - return jsutil.promiseToFuture(importKey( + return jsutil.promiseToFuture(importKey( 'raw', jsArrayBufferFrom(secretKeyData), jsutil.jsify({'name': webCryptoAlgorithm}), diff --git a/lib/src/e2ee.worker/e2ee.cryptor.dart b/lib/src/e2ee.worker/e2ee.cryptor.dart index f4937c4..e731f2f 100644 --- a/lib/src/e2ee.worker/e2ee.cryptor.dart +++ b/lib/src/e2ee.worker/e2ee.cryptor.dart @@ -1,11 +1,13 @@ import 'dart:async'; -import 'dart:html'; import 'dart:js'; +import 'dart:js_interop'; 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 'crypto.dart' as crypto; import 'e2ee.keyhandler.dart'; import 'e2ee.logger.dart'; @@ -136,7 +138,7 @@ class FrameCryptor { bool _enabled = false; CryptorError lastError = CryptorError.kNew; int currentKeyIndex = 0; - final DedicatedWorkerGlobalScope worker; + final web.DedicatedWorkerGlobalScope worker; SifGuard sifGuard = SifGuard(); void setParticipant(String identity, ParticipantKeyHandler keys) { @@ -219,7 +221,7 @@ class FrameCryptor { } void postMessage(Object message) { - worker.postMessage(message); + worker.postMessage(message.jsify()); } Future setupTransform({ @@ -480,7 +482,7 @@ class FrameCryptor { )); if (currentkeySet != initialKeySet) { - logger.warning( + logger.fine( 'ratchetKey: decryption ok, reset state to kKeyRatcheted'); await keyHandler.setKeySetFromMaterial( currentkeySet, initialKeyIndex); diff --git a/lib/src/e2ee.worker/e2ee.keyhandler.dart b/lib/src/e2ee.worker/e2ee.keyhandler.dart index 6de7667..84f9c2a 100644 --- a/lib/src/e2ee.worker/e2ee.keyhandler.dart +++ b/lib/src/e2ee.worker/e2ee.keyhandler.dart @@ -1,8 +1,9 @@ import 'dart:async'; -import 'dart:html'; import 'dart:js_util' as jsutil; import 'dart:typed_data'; +import 'package:web/web.dart' as web; + import 'crypto.dart' as crypto; import 'e2ee.logger.dart'; import 'e2ee.utils.dart'; @@ -29,7 +30,7 @@ class KeyOptions { class KeyProvider { KeyProvider(this.worker, this.id, this.keyProviderOptions); - final DedicatedWorkerGlobalScope worker; + final web.DedicatedWorkerGlobalScope worker; final String id; final KeyOptions keyProviderOptions; var participantKeys = {}; @@ -80,8 +81,8 @@ const KEYRING_SIZE = 16; class KeySet { KeySet(this.material, this.encryptionKey); - CryptoKey material; - CryptoKey encryptionKey; + web.CryptoKey material; + web.CryptoKey encryptionKey; } class ParticipantKeyHandler { @@ -100,7 +101,7 @@ class ParticipantKeyHandler { final KeyOptions keyOptions; - final DedicatedWorkerGlobalScope worker; + final web.DedicatedWorkerGlobalScope worker; final String participantIdentity; @@ -157,8 +158,8 @@ class ParticipantKeyHandler { return newKey; } - Future ratchetMaterial( - CryptoKey currentMaterial, ByteBuffer newKeyBuffer) async { + Future ratchetMaterial( + web.CryptoKey currentMaterial, ByteBuffer newKeyBuffer) async { var newMaterial = await jsutil.promiseToFuture(crypto.importKey( 'raw', newKeyBuffer, @@ -194,14 +195,14 @@ class ParticipantKeyHandler { /// Derives a set of keys from the master key. /// See https://tools.ietf.org/html/draft-omara-sframe-00#section-4.3.1 - Future deriveKeys(CryptoKey material, Uint8List salt) async { + Future deriveKeys(web.CryptoKey material, Uint8List salt) async { var algorithmOptions = getAlgoOptions((material.algorithm as crypto.Algorithm).name, salt); // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveKey#HKDF // https://developer.mozilla.org/en-US/docs/Web/API/HkdfParams var encryptionKey = - await jsutil.promiseToFuture(crypto.deriveKey( + await jsutil.promiseToFuture(crypto.deriveKey( jsutil.jsify(algorithmOptions), material, jsutil.jsify({'name': 'AES-GCM', 'length': 128}), @@ -215,7 +216,7 @@ class ParticipantKeyHandler { /// Ratchets a key. See /// https://tools.ietf.org/html/draft-omara-sframe-00#section-4.3.5.1 - Future ratchet(CryptoKey material, Uint8List salt) async { + Future ratchet(web.CryptoKey material, Uint8List salt) async { var algorithmOptions = getAlgoOptions('PBKDF2', salt); // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveBits diff --git a/lib/src/e2ee.worker/e2ee.utils.dart b/lib/src/e2ee.worker/e2ee.utils.dart index 076eba0..2eb921d 100644 --- a/lib/src/e2ee.worker/e2ee.utils.dart +++ b/lib/src/e2ee.worker/e2ee.utils.dart @@ -1,7 +1,9 @@ -import 'dart:html'; import 'dart:js' as js; import 'dart:typed_data'; +import 'package:js/js_util.dart'; +import 'package:web/web.dart' as web; + import 'crypto.dart' as crypto; bool isE2EESupported() { @@ -17,10 +19,10 @@ bool isInsertableStreamSupported() { js.context['RTCRtpSender']['prototype']['createEncodedStreams'] != null; } -Future importKey( +Future importKey( Uint8List keyBytes, String algorithm, String usage) { // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey - return promiseToFuture(crypto.importKey( + return promiseToFuture(crypto.importKey( 'raw', crypto.jsArrayBufferFrom(keyBytes), js.JsObject.jsify({'name': algorithm}), @@ -29,10 +31,10 @@ Future importKey( )); } -Future createKeyMaterialFromString( +Future createKeyMaterialFromString( Uint8List keyBytes, String algorithm, String usage) { // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey - return promiseToFuture(crypto.importKey( + return promiseToFuture(crypto.importKey( 'raw', crypto.jsArrayBufferFrom(keyBytes), js.JsObject.jsify({'name': 'PBKDF2'}), diff --git a/lib/src/e2ee.worker/e2ee.worker.dart b/lib/src/e2ee.worker/e2ee.worker.dart index 0d49b8f..17d555a 100644 --- a/lib/src/e2ee.worker/e2ee.worker.dart +++ b/lib/src/e2ee.worker/e2ee.worker.dart @@ -1,58 +1,21 @@ import 'dart:convert'; -import 'dart:html' as html; +import 'dart:js_interop'; 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:js/js.dart'; import 'package:logging/logging.dart'; +import 'package:web/web.dart' as web; import 'e2ee.cryptor.dart'; import 'e2ee.keyhandler.dart'; import 'e2ee.logger.dart'; @JS() -abstract class TransformMessage { - external String get msgType; - external String get kind; -} - -@anonymous -@JS() -class EnableTransformMessage { - external factory EnableTransformMessage({ - ReadableStream readable, - WritableStream writable, - String msgType, - String kind, - String participantId, - String trackId, - String codec, - }); - external ReadableStream get readable; - external WritableStream get writable; - external String get msgType; // 'encode' or 'decode' - external String get participantId; - external String get trackId; - external String get kind; - external String get codec; -} - -@anonymous -@JS() -class RemoveTransformMessage { - external factory RemoveTransformMessage( - {String msgType, String participantId, String trackId}); - external String get msgType; // 'removeTransform' - external String get participantId; - external String get trackId; -} +external web.DedicatedWorkerGlobalScope get self; -@JS('self') -external html.DedicatedWorkerGlobalScope get self; - -extension PropsRTCTransformEventHandler on html.DedicatedWorkerGlobalScope { +extension PropsRTCTransformEventHandler on web.DedicatedWorkerGlobalScope { set onrtctransform(Function(dynamic) callback) => js_util.setProperty(this, 'onrtctransform', callback); } @@ -102,7 +65,7 @@ void main() async { if (js_util.getProperty(self, 'RTCTransformEvent') != null) { logger.info('setup RTCTransformEvent event handler'); - self.onrtctransform = allowInterop((event) { + self.onrtctransform = (web.RTCTransformEvent event) { logger.info('Got onrtctransform event'); var transformer = (event as RTCTransformEvent).transformer; @@ -132,352 +95,355 @@ void main() async { trackId: trackId, kind: kind, codec: codec); - }); + }.toJS; } - self.onMessage.listen((e) async { - var msg = e.data; - var msgType = msg['msgType']; - var msgId = msg['msgId'] as String?; - logger.info('Got message $msgType, msgId $msgId'); - switch (msgType) { - case 'keyProviderInit': - { - var options = msg['keyOptions']; - var keyProviderId = msg['keyProviderId'] as String; - var keyProviderOptions = KeyOptions( - sharedKey: options['sharedKey'], - ratchetSalt: Uint8List.fromList( - base64Decode(options['ratchetSalt'] as String)), - ratchetWindowSize: options['ratchetWindowSize'], - failureTolerance: options['failureTolerance'] ?? -1, - uncryptedMagicBytes: options['uncryptedMagicBytes'] != null - ? Uint8List.fromList( - base64Decode(options['uncryptedMagicBytes'] as String)) - : null); - logger.config( - 'Init with keyProviderOptions:\n ${keyProviderOptions.toString()}'); + self.onmessage = (web.MessageEvent e) { + (e) async { + var msg = e.data; + var msgType = msg['msgType']; + var msgId = msg['msgId'] as String?; + logger.fine('Got message $msgType, msgId $msgId'); + switch (msgType) { + case 'keyProviderInit': + { + var options = msg['keyOptions']; + var keyProviderId = msg['keyProviderId'] as String; + var keyProviderOptions = KeyOptions( + sharedKey: options['sharedKey'], + ratchetSalt: Uint8List.fromList( + base64Decode(options['ratchetSalt'] as String)), + ratchetWindowSize: options['ratchetWindowSize'], + failureTolerance: options['failureTolerance'] ?? -1, + uncryptedMagicBytes: options['uncryptedMagicBytes'] != null + ? Uint8List.fromList( + base64Decode(options['uncryptedMagicBytes'] as String)) + : null); + logger.config( + 'Init with keyProviderOptions:\n ${keyProviderOptions.toString()}'); - var keyProvider = - KeyProvider(self, keyProviderId, keyProviderOptions); - keyProviders[keyProviderId] = keyProvider; + var keyProvider = + KeyProvider(self, keyProviderId, keyProviderOptions); + keyProviders[keyProviderId] = keyProvider; - self.postMessage({ - 'type': 'init', - 'msgId': msgId, - 'msgType': 'response', - }); + self.postMessage({ + 'type': 'init', + 'msgId': msgId, + 'msgType': 'response', + }.jsify()); + break; + } + case 'keyProviderDispose': + { + var keyProviderId = msg['keyProviderId'] as String; + logger.config('Dispose keyProvider $keyProviderId'); + keyProviders.remove(keyProviderId); + self.postMessage({ + 'type': 'dispose', + 'msgId': msgId, + 'msgType': 'response', + }.jsify()); + } break; - } - case 'keyProviderDispose': - { - var keyProviderId = msg['keyProviderId'] as String; - logger.config('Dispose keyProvider $keyProviderId'); - keyProviders.remove(keyProviderId); - self.postMessage({ - 'type': 'dispose', - 'msgId': msgId, - 'msgType': 'response', - }); - } - break; - case 'enable': - { - var enabled = msg['enabled'] as bool; - var trackId = msg['trackId'] as String; - - var cryptors = - participantCryptors.where((c) => c.trackId == trackId).toList(); - for (var cryptor in cryptors) { - logger.config('Set enable $enabled for trackId ${cryptor.trackId}'); - cryptor.setEnabled(enabled); + case 'enable': + { + var enabled = msg['enabled'] as bool; + var trackId = msg['trackId'] as String; + + var cryptors = + participantCryptors.where((c) => c.trackId == trackId).toList(); + for (var cryptor in cryptors) { + logger + .config('Set enable $enabled for trackId ${cryptor.trackId}'); + cryptor.setEnabled(enabled); + } + self.postMessage({ + 'type': 'cryptorEnabled', + 'enable': enabled, + 'msgId': msgId, + 'msgType': 'response', + }.jsify()); } - self.postMessage({ - 'type': 'cryptorEnabled', - 'enable': enabled, - 'msgId': msgId, - 'msgType': 'response', - }); - } - break; - case 'decode': - case 'encode': - { - var kind = msg['kind']; - var exist = msg['exist'] as bool; - var participantId = msg['participantId'] as String; - var trackId = msg['trackId']; - var readable = msg['readableStream'] as ReadableStream; - var writable = msg['writableStream'] as WritableStream; - var keyProviderId = msg['keyProviderId'] as String; + break; + case 'decode': + case 'encode': + { + var kind = msg['kind']; + var exist = msg['exist'] as bool; + var participantId = msg['participantId'] as String; + var trackId = msg['trackId']; + var readable = msg['readableStream'] as ReadableStream; + var writable = msg['writableStream'] as WritableStream; + var keyProviderId = msg['keyProviderId'] as String; - logger.config( - 'SetupTransform for kind $kind, trackId $trackId, participantId $participantId, ${readable.runtimeType} ${writable.runtimeType}}'); + logger.config( + 'SetupTransform for kind $kind, trackId $trackId, participantId $participantId, ${readable.runtimeType} ${writable.runtimeType}}'); + + var keyProvider = keyProviders[keyProviderId]; + if (keyProvider == null) { + logger.warning('KeyProvider not found for $keyProviderId'); + self.postMessage({ + 'type': 'cryptorSetup', + 'participantId': participantId, + 'trackId': trackId, + 'exist': exist, + 'operation': msgType, + 'error': 'KeyProvider not found', + 'msgId': msgId, + 'msgType': 'response', + }.jsify()); + return; + } + + var cryptor = getTrackCryptor(participantId, trackId, keyProvider); + + await cryptor.setupTransform( + operation: msgType, + readable: readable, + writable: writable, + trackId: trackId, + kind: kind, + ); - var keyProvider = keyProviders[keyProviderId]; - if (keyProvider == null) { - logger.warning('KeyProvider not found for $keyProviderId'); self.postMessage({ 'type': 'cryptorSetup', 'participantId': participantId, 'trackId': trackId, 'exist': exist, 'operation': msgType, - 'error': 'KeyProvider not found', 'msgId': msgId, 'msgType': 'response', - }); - return; + }.jsify()); + cryptor.lastError = CryptorError.kNew; } - - var cryptor = getTrackCryptor(participantId, trackId, keyProvider); - - await cryptor.setupTransform( - operation: msgType, - readable: readable, - writable: writable, - trackId: trackId, - kind: kind, - ); - - self.postMessage({ - 'type': 'cryptorSetup', - 'participantId': participantId, - 'trackId': trackId, - 'exist': exist, - 'operation': msgType, - 'msgId': msgId, - 'msgType': 'response', - }); - cryptor.lastError = CryptorError.kNew; - } - break; - case 'removeTransform': - { - var trackId = msg['trackId'] as String; - logger.config('Removing trackId $trackId'); - unsetCryptorParticipant(trackId); - self.postMessage({ - 'type': 'cryptorRemoved', - 'trackId': trackId, - 'msgId': msgId, - 'msgType': 'response', - }); - } - break; - case 'setKey': - case 'setSharedKey': - { - var key = Uint8List.fromList(base64Decode(msg['key'] as String)); - var keyIndex = msg['keyIndex'] as int; - var keyProviderId = msg['keyProviderId'] as String; - var keyProvider = keyProviders[keyProviderId]; - if (keyProvider == null) { - logger.warning('KeyProvider not found for $keyProviderId'); + break; + case 'removeTransform': + { + var trackId = msg['trackId'] as String; + logger.config('Removing trackId $trackId'); + unsetCryptorParticipant(trackId); self.postMessage({ - 'type': 'setKey', - 'error': 'KeyProvider not found', + 'type': 'cryptorRemoved', + 'trackId': trackId, 'msgId': msgId, 'msgType': 'response', - }); - return; - } - var keyProviderOptions = keyProvider.keyProviderOptions; - if (keyProviderOptions.sharedKey) { - logger.config('Set SharedKey keyIndex $keyIndex'); - keyProvider.setSharedKey(key, keyIndex: keyIndex); - } else { - var participantId = msg['participantId'] as String; - logger.config( - 'Set key for participant $participantId, keyIndex $keyIndex'); - await keyProvider - .getParticipantKeyHandler(participantId) - .setKey(key, keyIndex: keyIndex); + }.jsify()); } + break; + case 'setKey': + case 'setSharedKey': + { + var key = Uint8List.fromList(base64Decode(msg['key'] as String)); + var keyIndex = msg['keyIndex'] as int; + var keyProviderId = msg['keyProviderId'] as String; + var keyProvider = keyProviders[keyProviderId]; + if (keyProvider == null) { + logger.warning('KeyProvider not found for $keyProviderId'); + self.postMessage({ + 'type': 'setKey', + 'error': 'KeyProvider not found', + 'msgId': msgId, + 'msgType': 'response', + }.jsify()); + return; + } + var keyProviderOptions = keyProvider.keyProviderOptions; + if (keyProviderOptions.sharedKey) { + logger.config('Set SharedKey keyIndex $keyIndex'); + keyProvider.setSharedKey(key, keyIndex: keyIndex); + } else { + var participantId = msg['participantId'] as String; + logger.config( + 'Set key for participant $participantId, keyIndex $keyIndex'); + await keyProvider + .getParticipantKeyHandler(participantId) + .setKey(key, keyIndex: keyIndex); + } - self.postMessage({ - 'type': 'setKey', - 'participantId': msg['participantId'], - 'sharedKey': keyProviderOptions.sharedKey, - 'keyIndex': keyIndex, - 'msgId': msgId, - 'msgType': 'response', - }); - } - break; - case 'ratchetKey': - case 'ratchetSharedKey': - { - var keyIndex = msg['keyIndex']; - var participantId = msg['participantId'] as String; - var keyProviderId = msg['keyProviderId'] as String; - var keyProvider = keyProviders[keyProviderId]; - if (keyProvider == null) { - logger.warning('KeyProvider not found for $keyProviderId'); self.postMessage({ 'type': 'setKey', - 'error': 'KeyProvider not found', + 'participantId': msg['participantId'], + 'sharedKey': keyProviderOptions.sharedKey, + 'keyIndex': keyIndex, 'msgId': msgId, 'msgType': 'response', - }); - return; - } - var keyProviderOptions = keyProvider.keyProviderOptions; - Uint8List? newKey; - if (keyProviderOptions.sharedKey) { - logger.config('RatchetKey for SharedKey, keyIndex $keyIndex'); - newKey = - await keyProvider.getSharedKeyHandler().ratchetKey(keyIndex); - } else { - logger.config( - 'RatchetKey for participant $participantId, keyIndex $keyIndex'); - newKey = await keyProvider - .getParticipantKeyHandler(participantId) - .ratchetKey(keyIndex); + }.jsify()); } + break; + case 'ratchetKey': + case 'ratchetSharedKey': + { + var keyIndex = msg['keyIndex']; + var participantId = msg['participantId'] as String; + var keyProviderId = msg['keyProviderId'] as String; + var keyProvider = keyProviders[keyProviderId]; + if (keyProvider == null) { + logger.warning('KeyProvider not found for $keyProviderId'); + self.postMessage({ + 'type': 'setKey', + 'error': 'KeyProvider not found', + 'msgId': msgId, + 'msgType': 'response', + }.jsify()); + return; + } + var keyProviderOptions = keyProvider.keyProviderOptions; + Uint8List? newKey; + if (keyProviderOptions.sharedKey) { + logger.config('RatchetKey for SharedKey, keyIndex $keyIndex'); + newKey = + await keyProvider.getSharedKeyHandler().ratchetKey(keyIndex); + } else { + logger.config( + 'RatchetKey for participant $participantId, keyIndex $keyIndex'); + newKey = await keyProvider + .getParticipantKeyHandler(participantId) + .ratchetKey(keyIndex); + } - self.postMessage({ - 'type': 'ratchetKey', - 'sharedKey': keyProviderOptions.sharedKey, - 'participantId': participantId, - 'newKey': newKey != null ? base64Encode(newKey) : '', - 'keyIndex': keyIndex, - 'msgId': msgId, - 'msgType': 'response', - }); - } - break; - case 'setKeyIndex': - { - var keyIndex = msg['index']; - var trackId = msg['trackId'] as String; - logger.config('Setup key index for track $trackId'); - var cryptors = - participantCryptors.where((c) => c.trackId == trackId).toList(); - for (var c in cryptors) { - logger.config('Set keyIndex for trackId ${c.trackId}'); - c.setKeyIndex(keyIndex); + self.postMessage({ + 'type': 'ratchetKey', + 'sharedKey': keyProviderOptions.sharedKey, + 'participantId': participantId, + 'newKey': newKey != null ? base64Encode(newKey) : '', + 'keyIndex': keyIndex, + 'msgId': msgId, + 'msgType': 'response', + }.jsify()); } + break; + case 'setKeyIndex': + { + var keyIndex = msg['index']; + var trackId = msg['trackId'] as String; + logger.config('Setup key index for track $trackId'); + var cryptors = + participantCryptors.where((c) => c.trackId == trackId).toList(); + for (var c in cryptors) { + logger.config('Set keyIndex for trackId ${c.trackId}'); + c.setKeyIndex(keyIndex); + } - self.postMessage({ - 'type': 'setKeyIndex', - 'keyIndex': keyIndex, - 'msgId': msgId, - 'msgType': 'response', - }); - } - break; - case 'exportKey': - case 'exportSharedKey': - { - var keyIndex = msg['keyIndex'] as int; - var participantId = msg['participantId'] as String; - var keyProviderId = msg['keyProviderId'] as String; - var keyProvider = keyProviders[keyProviderId]; - if (keyProvider == null) { - logger.warning('KeyProvider not found for $keyProviderId'); self.postMessage({ - 'type': 'setKey', - 'error': 'KeyProvider not found', + 'type': 'setKeyIndex', + 'keyIndex': keyIndex, 'msgId': msgId, 'msgType': 'response', - }); - return; - } - var keyProviderOptions = keyProvider.keyProviderOptions; - Uint8List? key; - if (keyProviderOptions.sharedKey) { - logger.config('Export SharedKey keyIndex $keyIndex'); - key = await keyProvider.getSharedKeyHandler().exportKey(keyIndex); - } else { - logger.config( - 'Export key for participant $participantId, keyIndex $keyIndex'); - key = await keyProvider - .getParticipantKeyHandler(participantId) - .exportKey(keyIndex); + }.jsify()); } - self.postMessage({ - 'type': 'exportKey', - 'participantId': participantId, - 'keyIndex': keyIndex, - 'exportedKey': key != null ? base64Encode(key) : '', - 'msgId': msgId, - 'msgType': 'response', - }); - } - break; - case 'setSifTrailer': - { - var sifTrailer = - Uint8List.fromList(base64Decode(msg['sifTrailer'] as String)); - var keyProviderId = msg['keyProviderId'] as String; - var keyProvider = keyProviders[keyProviderId]; - if (keyProvider == null) { - logger.warning('KeyProvider not found for $keyProviderId'); + break; + case 'exportKey': + case 'exportSharedKey': + { + var keyIndex = msg['keyIndex'] as int; + var participantId = msg['participantId'] as String; + var keyProviderId = msg['keyProviderId'] as String; + var keyProvider = keyProviders[keyProviderId]; + if (keyProvider == null) { + logger.warning('KeyProvider not found for $keyProviderId'); + self.postMessage({ + 'type': 'setKey', + 'error': 'KeyProvider not found', + 'msgId': msgId, + 'msgType': 'response', + }.jsify()); + return; + } + var keyProviderOptions = keyProvider.keyProviderOptions; + Uint8List? key; + if (keyProviderOptions.sharedKey) { + logger.config('Export SharedKey keyIndex $keyIndex'); + key = await keyProvider.getSharedKeyHandler().exportKey(keyIndex); + } else { + logger.config( + 'Export key for participant $participantId, keyIndex $keyIndex'); + key = await keyProvider + .getParticipantKeyHandler(participantId) + .exportKey(keyIndex); + } self.postMessage({ - 'type': 'setKey', - 'error': 'KeyProvider not found', + 'type': 'exportKey', + 'participantId': participantId, + 'keyIndex': keyIndex, + 'exportedKey': key != null ? base64Encode(key) : '', 'msgId': msgId, 'msgType': 'response', - }); - return; - } - keyProvider.setSifTrailer(sifTrailer); - logger.config('SetSifTrailer = $sifTrailer'); - for (var c in participantCryptors) { - c.setSifTrailer(sifTrailer); + }.jsify()); } + break; + case 'setSifTrailer': + { + var sifTrailer = + Uint8List.fromList(base64Decode(msg['sifTrailer'] as String)); + var keyProviderId = msg['keyProviderId'] as String; + var keyProvider = keyProviders[keyProviderId]; + if (keyProvider == null) { + logger.warning('KeyProvider not found for $keyProviderId'); + self.postMessage({ + 'type': 'setKey', + 'error': 'KeyProvider not found', + 'msgId': msgId, + 'msgType': 'response', + }.jsify()); + return; + } + keyProvider.setSifTrailer(sifTrailer); + logger.config('SetSifTrailer = $sifTrailer'); + for (var c in participantCryptors) { + c.setSifTrailer(sifTrailer); + } - self.postMessage({ - 'type': 'setSifTrailer', - 'msgId': msgId, - 'msgType': 'response', - }); - } - break; - case 'updateCodec': - { - var codec = msg['codec'] as String; - var trackId = msg['trackId'] as String; - logger.config('Update codec for trackId $trackId, codec $codec'); - var cryptor = - participantCryptors.firstWhereOrNull((c) => c.trackId == trackId); - cryptor?.updateCodec(codec); - - self.postMessage({ - 'type': 'updateCodec', - 'msgId': msgId, - 'msgType': 'response', - }); - } - break; - case 'dispose': - { - var trackId = msg['trackId'] as String; - logger.config('Dispose for trackId $trackId'); - var cryptor = - participantCryptors.firstWhereOrNull((c) => c.trackId == trackId); - if (cryptor != null) { - cryptor.lastError = CryptorError.kDisposed; self.postMessage({ - 'type': 'cryptorDispose', - 'participantId': cryptor.participantIdentity, - 'trackId': trackId, + 'type': 'setSifTrailer', 'msgId': msgId, 'msgType': 'response', - }); - } else { + }.jsify()); + } + break; + case 'updateCodec': + { + var codec = msg['codec'] as String; + var trackId = msg['trackId'] as String; + logger.config('Update codec for trackId $trackId, codec $codec'); + var cryptor = participantCryptors + .firstWhereOrNull((c) => c.trackId == trackId); + cryptor?.updateCodec(codec); + self.postMessage({ - 'type': 'cryptorDispose', - 'error': 'cryptor not found', + 'type': 'updateCodec', 'msgId': msgId, 'msgType': 'response', - }); + }.jsify()); } - } - break; - default: - logger.warning('Unknown message kind $msg'); - } - }); + break; + case 'dispose': + { + var trackId = msg['trackId'] as String; + logger.config('Dispose for trackId $trackId'); + var cryptor = participantCryptors + .firstWhereOrNull((c) => c.trackId == trackId); + if (cryptor != null) { + cryptor.lastError = CryptorError.kDisposed; + self.postMessage({ + 'type': 'cryptorDispose', + 'participantId': cryptor.participantIdentity, + 'trackId': trackId, + 'msgId': msgId, + 'msgType': 'response', + }.jsify()); + } else { + self.postMessage({ + 'type': 'cryptorDispose', + 'error': 'cryptor not found', + 'msgId': msgId, + 'msgType': 'response', + }.jsify()); + } + } + break; + default: + logger.warning('Unknown message kind $msg'); + } + }(e); + }.toJS; } diff --git a/lib/src/factory_impl.dart b/lib/src/factory_impl.dart index 52ff7d8..9b99e79 100644 --- a/lib/src/factory_impl.dart +++ b/lib/src/factory_impl.dart @@ -1,7 +1,8 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:html' as html; import 'package:js/js.dart'; +import 'package:js/js_util.dart'; +import 'package:web/web.dart' as web; import 'package:webrtc_interface/webrtc_interface.dart'; import 'frame_cryptor_impl.dart'; @@ -39,14 +40,15 @@ class RTCFactoryWeb extends RTCFactory { {'DtlsSrtpKeyAgreement': true}, ], }; - final jsRtcPc = html.RtcPeerConnection({...constr, ...configuration}); + final jsRtcPc = web.RTCPeerConnection( + jsify({...constr, ...configuration}) as web.RTCConfiguration); final _peerConnectionId = base64Encode(jsRtcPc.toString().codeUnits); return RTCPeerConnectionWeb(_peerConnectionId, jsRtcPc); } @override Future createLocalMediaStream(String label) async { - final jsMs = html.MediaStream(); + final jsMs = web.MediaStream(); return MediaStreamWeb(jsMs, 'local'); } diff --git a/lib/src/frame_cryptor_impl.dart b/lib/src/frame_cryptor_impl.dart index 913805d..9e16bf4 100644 --- a/lib/src/frame_cryptor_impl.dart +++ b/lib/src/frame_cryptor_impl.dart @@ -1,12 +1,14 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:html' as html; import 'dart:js' as js; +import 'dart:js_interop'; 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 'rtc_rtp_receiver_impl.dart'; @@ -20,7 +22,7 @@ class WorkerResponse { dynamic data; } -extension RtcRtpReceiverExt on html.RtcRtpReceiver { +extension RtcRtpReceiverExt on web.RTCRtpReceiver { static Map readableStreams_ = {}; static Map writableStreams_ = {}; @@ -52,7 +54,7 @@ extension RtcRtpReceiverExt on html.RtcRtpReceiver { } } -extension RtcRtpSenderExt on html.RtcRtpSender { +extension RtcRtpSenderExt on web.RTCRtpSender { static Map readableStreams_ = {}; static Map writableStreams_ = {}; @@ -88,13 +90,13 @@ class FrameCryptorImpl extends FrameCryptor { FrameCryptorImpl( this._factory, this.worker, this._participantId, this._trackId, {this.jsSender, this.jsReceiver, required this.keyProvider}); - html.Worker worker; + web.Worker worker; bool _enabled = false; int _keyIndex = 0; final String _participantId; final String _trackId; - final html.RtcRtpSender? jsSender; - final html.RtcRtpReceiver? jsReceiver; + final web.RTCRtpSender? jsSender; + final web.RTCRtpReceiver? jsReceiver; final FrameCryptorFactoryImpl _factory; final KeyProviderImpl keyProvider; @@ -170,7 +172,7 @@ class FrameCryptorImpl extends FrameCryptor { class KeyProviderImpl implements KeyProvider { KeyProviderImpl(this._id, this.worker, this.options, this.events); final String _id; - final html.Worker worker; + final web.Worker worker; final KeyProviderOptions options; final Map> _keys = {}; final EventsEmitter events; @@ -365,61 +367,70 @@ class KeyProviderImpl implements KeyProvider { class FrameCryptorFactoryImpl implements FrameCryptorFactory { FrameCryptorFactoryImpl._internal() { - worker = html.Worker('e2ee.worker.dart.js'); - worker.onMessage.listen((msg) { - print('master got ${msg.data}'); - var type = msg.data['type']; - var msgId = msg.data['msgId']; - var msgType = msg.data['msgType']; - - if (msgType == 'response') { - events.emit(WorkerResponse(msgId, msg.data)); - } else if (msgType == 'event') { - if (type == 'cryptorState') { - var trackId = msg.data['trackId']; - var participantId = msg.data['participantId']; - var frameCryptor = _frameCryptors.values.firstWhereOrNull( - (element) => (element as FrameCryptorImpl).trackId == trackId); - var state = msg.data['state']; - var frameCryptorState = FrameCryptorState.FrameCryptorStateNew; - switch (state) { - case 'ok': - frameCryptorState = FrameCryptorState.FrameCryptorStateOk; - break; - case 'decryptError': - frameCryptorState = - FrameCryptorState.FrameCryptorStateDecryptionFailed; - break; - case 'encryptError': - frameCryptorState = - FrameCryptorState.FrameCryptorStateEncryptionFailed; - break; - case 'missingKey': - frameCryptorState = FrameCryptorState.FrameCryptorStateMissingKey; - break; - case 'internalError': - frameCryptorState = - FrameCryptorState.FrameCryptorStateInternalError; - break; - case 'keyRatcheted': - frameCryptorState = - FrameCryptorState.FrameCryptorStateKeyRatcheted; - break; + worker = web.Worker('e2ee.worker.dart.js'); + worker.addEventListener( + 'message', + (web.MessageEvent msg) { + final data = dartify(msg.data) as Map; + //print('master got $data'); + var type = data['type']; + var msgId = data['msgId']; + var msgType = data['msgType']; + + if (msgType == 'response') { + events.emit(WorkerResponse(msgId, data)); + } else if (msgType == 'event') { + if (type == 'cryptorState') { + var trackId = data['trackId']; + var participantId = data['participantId']; + var frameCryptor = _frameCryptors.values.firstWhereOrNull( + (element) => + (element as FrameCryptorImpl).trackId == trackId); + var state = data['state']; + var frameCryptorState = FrameCryptorState.FrameCryptorStateNew; + switch (state) { + case 'ok': + frameCryptorState = FrameCryptorState.FrameCryptorStateOk; + break; + case 'decryptError': + frameCryptorState = + FrameCryptorState.FrameCryptorStateDecryptionFailed; + break; + case 'encryptError': + frameCryptorState = + FrameCryptorState.FrameCryptorStateEncryptionFailed; + break; + case 'missingKey': + frameCryptorState = + FrameCryptorState.FrameCryptorStateMissingKey; + break; + case 'internalError': + frameCryptorState = + FrameCryptorState.FrameCryptorStateInternalError; + break; + case 'keyRatcheted': + frameCryptorState = + FrameCryptorState.FrameCryptorStateKeyRatcheted; + break; + } + frameCryptor?.onFrameCryptorStateChanged + ?.call(participantId, frameCryptorState); + } } - frameCryptor?.onFrameCryptorStateChanged - ?.call(participantId, frameCryptorState); - } - } - }); - worker.onError.listen((err) { - print('worker error: $err'); - }); + }.toJS, + false.toJS); + worker.addEventListener( + 'error', + (web.ErrorEvent err) { + print('worker error: $err'); + }.toJS, + false.toJS); } static final FrameCryptorFactoryImpl instance = FrameCryptorFactoryImpl._internal(); - late html.Worker worker; + late web.Worker worker; final Map _frameCryptors = {}; final EventsEmitter events = EventsEmitter(); @@ -441,7 +452,7 @@ class FrameCryptorFactoryImpl implements FrameCryptorFactory { var jsReceiver = (receiver as RTCRtpReceiverWeb).jsRtpReceiver; var trackId = jsReceiver.hashCode.toString(); - var kind = jsReceiver.track!.kind!; + var kind = jsReceiver.track.kind; if (js.context['RTCRtpScriptTransform'] != null) { print('support RTCRtpScriptTransform'); @@ -500,7 +511,7 @@ class FrameCryptorFactoryImpl implements FrameCryptorFactory { required KeyProvider keyProvider}) { var jsSender = (sender as RTCRtpSenderWeb).jsRtpSender; var trackId = jsSender.hashCode.toString(); - var kind = jsSender.track!.kind!; + var kind = jsSender.track!.kind; if (js.context['RTCRtpScriptTransform'] != null) { print('support RTCRtpScriptTransform'); diff --git a/lib/src/media_recorder_impl.dart b/lib/src/media_recorder_impl.dart index 45cacbd..3a3c206 100644 --- a/lib/src/media_recorder_impl.dart +++ b/lib/src/media_recorder_impl.dart @@ -1,12 +1,13 @@ import 'dart:async'; -import 'dart:html' as html; import 'dart:js' as js; +import 'dart:js_interop'; +import 'package:web/web.dart' as web; import 'package:webrtc_interface/webrtc_interface.dart'; import 'media_stream_impl.dart'; class MediaRecorderWeb extends MediaRecorder { - late html.MediaRecorder _recorder; + late web.MediaRecorder _recorder; late Completer _completer; @override @@ -28,30 +29,38 @@ class MediaRecorderWeb extends MediaRecorder { int timeSlice = 1000, }) { var _native = stream as MediaStreamWeb; - _recorder = html.MediaRecorder(_native.jsStream, {'mimeType': mimeType}); + _recorder = web.MediaRecorder( + _native.jsStream, web.MediaRecorderOptions(mimeType: mimeType)); if (onDataChunk == null) { - var _chunks = []; + var _chunks = []; _completer = Completer(); - _recorder.addEventListener('dataavailable', (html.Event event) { - final html.Blob blob = js.JsObject.fromBrowserObject(event)['data']; - if (blob.size > 0) { - _chunks.add(blob); - } - if (_recorder.state == 'inactive') { - final blob = html.Blob(_chunks, mimeType); - _completer.complete(html.Url.createObjectUrlFromBlob(blob)); - } - }); - _recorder.onError.listen((error) { - _completer.completeError(error); - }); + _recorder.addEventListener( + 'dataavailable', + (web.Event event) { + final web.Blob blob = js.JsObject.fromBrowserObject(event)['data']; + if (blob.size > 0) { + _chunks.add(blob); + } + if (_recorder.state == 'inactive') { + final blob = + web.Blob(_chunks.toJS, web.BlobPropertyBag(type: mimeType)); + _completer.complete(web.URL.createObjectURL(blob)); + } + }.toJS); + _recorder.addEventListener( + 'error', + (JSAny error) { + _completer.completeError(error); + }.toJS); } else { - _recorder.addEventListener('dataavailable', (html.Event event) { - onDataChunk( - js.JsObject.fromBrowserObject(event)['data'], - _recorder.state == 'inactive', - ); - }); + _recorder.addEventListener( + 'dataavailable', + (web.Event event) { + onDataChunk( + js.JsObject.fromBrowserObject(event)['data'], + _recorder.state == 'inactive', + ); + }.toJS); } _recorder.start(timeSlice); } diff --git a/lib/src/media_stream_impl.dart b/lib/src/media_stream_impl.dart index d68f895..55b15af 100644 --- a/lib/src/media_stream_impl.dart +++ b/lib/src/media_stream_impl.dart @@ -1,14 +1,13 @@ import 'dart:async'; -import 'dart:html' as html; +import 'dart:js_interop'; +import 'package:web/web.dart' as web; import 'package:webrtc_interface/webrtc_interface.dart'; import 'media_stream_track_impl.dart'; class MediaStreamWeb extends MediaStream { - MediaStreamWeb(this.jsStream, String ownerTag) - : assert(jsStream.id != null), - super(jsStream.id!, ownerTag); - final html.MediaStream jsStream; + MediaStreamWeb(this.jsStream, String ownerTag) : super(jsStream.id, ownerTag); + final web.MediaStream jsStream; @override Future getMediaTracks() { @@ -36,7 +35,7 @@ class MediaStreamWeb extends MediaStream { @override List getAudioTracks() { var audioTracks = []; - jsStream.getAudioTracks().forEach( + jsStream.getAudioTracks().toDart.forEach( (dynamic jsTrack) => audioTracks.add(MediaStreamTrackWeb(jsTrack))); return audioTracks; } @@ -44,7 +43,7 @@ class MediaStreamWeb extends MediaStream { @override List getVideoTracks() { var audioTracks = []; - jsStream.getVideoTracks().forEach( + jsStream.getVideoTracks().toDart.forEach( (dynamic jsTrack) => audioTracks.add(MediaStreamTrackWeb(jsTrack))); return audioTracks; } diff --git a/lib/src/media_stream_track_impl.dart b/lib/src/media_stream_track_impl.dart index dc6f6bd..39f6b70 100644 --- a/lib/src/media_stream_track_impl.dart +++ b/lib/src/media_stream_track_impl.dart @@ -1,17 +1,19 @@ import 'dart:async'; -import 'dart:html' as html; +import 'dart:js_interop'; import 'dart:js_util' as js; import 'dart:typed_data'; + +import 'package:web/web.dart' as web; import 'package:webrtc_interface/webrtc_interface.dart'; class MediaStreamTrackWeb extends MediaStreamTrack { MediaStreamTrackWeb(this.jsTrack) { - jsTrack.onEnded.listen((event) => onEnded?.call()); - jsTrack.onMute.listen((event) => onMute?.call()); - jsTrack.onUnmute.listen((event) => onUnMute?.call()); + jsTrack.addEventListener('ended', ((event) => onEnded?.call()).toJS); + jsTrack.addEventListener('mute', ((event) => onMute?.call()).toJS); + jsTrack.addEventListener('unmute', ((event) => onUnMute?.call()).toJS); } - final html.MediaStreamTrack jsTrack; + final web.MediaStreamTrack jsTrack; @override String? get id => jsTrack.id; @@ -23,14 +25,14 @@ class MediaStreamTrackWeb extends MediaStreamTrack { String? get label => jsTrack.label; @override - bool get enabled => jsTrack.enabled ?? false; + bool get enabled => jsTrack.enabled; @override bool? get muted => jsTrack.muted; @override set enabled(bool? b) { - jsTrack.enabled = b; + jsTrack.enabled = b ?? false; } @override @@ -64,19 +66,25 @@ class MediaStreamTrackWeb extends MediaStreamTrack { @override Future captureFrame() async { - final imageCapture = html.ImageCapture(jsTrack); - final bitmap = await imageCapture.grabFrame(); - final canvas = html.CanvasElement(); + final imageCapture = ImageCapture(jsTrack); + final bitmap = await imageCapture.grabFrame().toDart as web.ImageBitmap; + final canvas = web.HTMLCanvasElement(); canvas.width = bitmap.width; canvas.height = bitmap.height; final renderer = - canvas.getContext('bitmaprenderer') as html.ImageBitmapRenderingContext; + canvas.getContext('bitmaprenderer') as web.ImageBitmapRenderingContext; js.callMethod(renderer, 'transferFromImageBitmap', [bitmap]); - final blod = await canvas.toBlob(); - var array = - await js.promiseToFuture(js.callMethod(blod, 'arrayBuffer', [])); + + final blobCompleter = Completer(); + canvas.toBlob((web.Blob blob) { + blobCompleter.complete(blob); + }.toJS); + + final blod = await blobCompleter.future; + + var array = await blod.arrayBuffer().toDart; bitmap.close(); - return array; + return array.toDart; } @override @@ -102,3 +110,9 @@ class MediaStreamTrackWeb extends MediaStreamTrack { return MediaStreamTrackWeb(jsTrack.clone()); } } + +extension type ImageCapture._(JSObject _) implements JSObject { + external factory ImageCapture(web.MediaStreamTrack track); + + external JSPromise grabFrame(); +} diff --git a/lib/src/mediadevices_impl.dart b/lib/src/mediadevices_impl.dart index 763e8f2..105a034 100644 --- a/lib/src/mediadevices_impl.dart +++ b/lib/src/mediadevices_impl.dart @@ -1,7 +1,8 @@ import 'dart:async'; -import 'dart:html' as html; 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'; import 'media_stream_impl.dart'; @@ -22,20 +23,28 @@ class MediaDevicesWeb extends MediaDevices { mediaConstraints.putIfAbsent('video', () => false); mediaConstraints.putIfAbsent('audio', () => false); - final mediaDevices = html.window.navigator.mediaDevices; - if (mediaDevices == null) throw Exception('MediaDevices is null'); + final mediaDevices = web.window.navigator.mediaDevices; if (jsutil.hasProperty(mediaDevices, 'getUserMedia')) { var args = jsutil.jsify(mediaConstraints); - final jsStream = await jsutil.promiseToFuture( + final jsStream = await jsutil.promiseToFuture( jsutil.callMethod(mediaDevices, 'getUserMedia', [args])); return MediaStreamWeb(jsStream, 'local'); } else { - final jsStream = await html.window.navigator.getUserMedia( - audio: mediaConstraints['audio'], - video: mediaConstraints['video'], - ); + final streamCompleter = Completer(); + + web.window.navigator.getUserMedia( + web.MediaStreamConstraints( + audio: mediaConstraints['audio'], + video: mediaConstraints['video'], + ), + (web.MediaStream stream) {}.toJS, + (JSAny err) { + streamCompleter.completeError(err); + }.toJS); + + final jsStream = await streamCompleter.future; return MediaStreamWeb(jsStream, 'local'); } } catch (e) { @@ -47,18 +56,27 @@ class MediaDevicesWeb extends MediaDevices { Future getDisplayMedia( Map mediaConstraints) async { try { - final mediaDevices = html.window.navigator.mediaDevices; - if (mediaDevices == null) throw Exception('MediaDevices is null'); + final mediaDevices = web.window.navigator.mediaDevices; if (jsutil.hasProperty(mediaDevices, 'getDisplayMedia')) { final arg = jsutil.jsify(mediaConstraints); - final jsStream = await jsutil.promiseToFuture( + final jsStream = await jsutil.promiseToFuture( jsutil.callMethod(mediaDevices, 'getDisplayMedia', [arg])); return MediaStreamWeb(jsStream, 'local'); } else { - final jsStream = await html.window.navigator.getUserMedia( - video: {'mediaSource': 'screen'}, - audio: mediaConstraints['audio'] ?? false); + final streamCompleter = Completer(); + + web.window.navigator.getUserMedia( + web.MediaStreamConstraints( + video: jsutil.jsify({'mediaSource': 'screen'}), + audio: mediaConstraints['audio'] ?? false), + (web.MediaStream stream) { + streamCompleter.complete(stream); + }.toJS, + (JSAny err) { + streamCompleter.completeError(err); + }.toJS); + final jsStream = await streamCompleter.future; return MediaStreamWeb(jsStream, 'local'); } } catch (e) { @@ -71,90 +89,88 @@ class MediaDevicesWeb extends MediaDevices { final devices = await getSources(); return devices.map((e) { - var input = e as html.MediaDeviceInfo; + var input = e; return MediaDeviceInfo( - deviceId: - input.deviceId ?? 'Generated Device Id :(${devices.indexOf(e)})', + deviceId: input.deviceId, groupId: input.groupId, kind: input.kind, - label: input.label ?? 'Generated label :(${devices.indexOf(e)})', + label: input.label, ); }).toList(); } @override - Future> getSources() async { - return html.window.navigator.mediaDevices?.enumerateDevices() ?? - Future.value([]); + Future> getSources() async { + final devices = + await web.window.navigator.mediaDevices.enumerateDevices().toDart; + return devices.toDart; } @override MediaTrackSupportedConstraints getSupportedConstraints() { - final mediaDevices = html.window.navigator.mediaDevices; - if (mediaDevices == null) throw Exception('Mediadevices is null'); + final mediaDevices = web.window.navigator.mediaDevices; var _mapConstraints = mediaDevices.getSupportedConstraints(); return MediaTrackSupportedConstraints( - aspectRatio: _mapConstraints['aspectRatio'], - autoGainControl: _mapConstraints['autoGainControl'], - brightness: _mapConstraints['brightness'], - channelCount: _mapConstraints['channelCount'], - colorTemperature: _mapConstraints['colorTemperature'], - contrast: _mapConstraints['contrast'], - deviceId: _mapConstraints['deviceId'], - echoCancellation: _mapConstraints['echoCancellation'], - exposureCompensation: _mapConstraints['exposureCompensation'], - exposureMode: _mapConstraints['exposureMode'], - exposureTime: _mapConstraints['exposureTime'], - facingMode: _mapConstraints['facingMode'], - focusDistance: _mapConstraints['focusDistance'], - focusMode: _mapConstraints['focusMode'], - frameRate: _mapConstraints['frameRate'], - groupId: _mapConstraints['groupId'], - height: _mapConstraints['height'], - iso: _mapConstraints['iso'], - latency: _mapConstraints['latency'], - noiseSuppression: _mapConstraints['noiseSuppression'], - pan: _mapConstraints['pan'], - pointsOfInterest: _mapConstraints['pointsOfInterest'], - resizeMode: _mapConstraints['resizeMode'], - saturation: _mapConstraints['saturation'], - sampleRate: _mapConstraints['sampleRate'], - sampleSize: _mapConstraints['sampleSize'], - sharpness: _mapConstraints['sharpness'], - tilt: _mapConstraints['tilt'], - torch: _mapConstraints['torch'], - whiteBalanceMode: _mapConstraints['whiteBalanceMode'], - width: _mapConstraints['width'], - zoom: _mapConstraints['zoom']); + aspectRatio: _mapConstraints.aspectRatio, + autoGainControl: _mapConstraints.autoGainControl, + brightness: _mapConstraints.brightness, + channelCount: _mapConstraints.channelCount, + colorTemperature: _mapConstraints.colorTemperature, + contrast: _mapConstraints.contrast, + deviceId: _mapConstraints.deviceId, + echoCancellation: _mapConstraints.echoCancellation, + exposureCompensation: _mapConstraints.exposureCompensation, + exposureMode: _mapConstraints.exposureMode, + exposureTime: _mapConstraints.exposureTime, + facingMode: _mapConstraints.facingMode, + focusDistance: _mapConstraints.focusDistance, + focusMode: _mapConstraints.focusMode, + frameRate: _mapConstraints.frameRate, + groupId: _mapConstraints.groupId, + height: _mapConstraints.height, + iso: _mapConstraints.iso, + latency: _mapConstraints.latency, + noiseSuppression: _mapConstraints.noiseSuppression, + pan: _mapConstraints.pan, + pointsOfInterest: _mapConstraints.pointsOfInterest, + resizeMode: _mapConstraints.resizeMode, + saturation: _mapConstraints.saturation, + sampleRate: _mapConstraints.sampleRate, + sampleSize: _mapConstraints.sampleSize, + sharpness: _mapConstraints.sharpness, + tilt: _mapConstraints.tilt, + torch: _mapConstraints.torch, + whiteBalanceMode: _mapConstraints.whiteBalanceMode, + width: _mapConstraints.width, + zoom: _mapConstraints.zoom); } @override Future selectAudioOutput( [AudioOutputOptions? options]) async { try { - final mediaDevices = html.window.navigator.mediaDevices; - if (mediaDevices == null) throw Exception('MediaDevices is null'); + final mediaDevices = web.window.navigator.mediaDevices; if (jsutil.hasProperty(mediaDevices, 'selectAudioOutput')) { if (options != null) { final arg = jsutil.jsify(options); - final deviceInfo = await jsutil.promiseToFuture( + final deviceInfo = await jsutil.promiseToFuture( jsutil.callMethod(mediaDevices, 'selectAudioOutput', [arg])); return MediaDeviceInfo( kind: deviceInfo.kind, - label: deviceInfo.label ?? '', - deviceId: deviceInfo.deviceId ?? '', + label: deviceInfo.label, + deviceId: deviceInfo.deviceId, groupId: deviceInfo.groupId, ); } else { - final deviceInfo = await jsutil.promiseToFuture( + final deviceInfo = await jsutil.promiseToFuture( jsutil.callMethod(mediaDevices, 'selectAudioOutput', [])); return MediaDeviceInfo( kind: deviceInfo.kind, - label: deviceInfo.label ?? '', - deviceId: deviceInfo.deviceId ?? '', + label: deviceInfo.label, + deviceId: deviceInfo.deviceId, groupId: deviceInfo.groupId, ); } @@ -169,8 +185,7 @@ class MediaDevicesWeb extends MediaDevices { @override set ondevicechange(Function(dynamic event)? listener) { try { - final mediaDevices = html.window.navigator.mediaDevices; - if (mediaDevices == null) throw Exception('MediaDevices is null'); + final mediaDevices = web.window.navigator.mediaDevices; jsutil.setProperty(mediaDevices, 'ondevicechange', js.allowInterop((evt) => listener?.call(evt))); @@ -182,8 +197,7 @@ class MediaDevicesWeb extends MediaDevices { @override Function(dynamic event)? get ondevicechange { try { - final mediaDevices = html.window.navigator.mediaDevices; - if (mediaDevices == null) throw Exception('MediaDevices is null'); + final mediaDevices = web.window.navigator.mediaDevices; jsutil.getProperty(mediaDevices, 'ondevicechange'); } catch (e) { @@ -192,3 +206,23 @@ class MediaDevicesWeb extends MediaDevices { return null; } } + +extension _MediaTrackConstraints on web.MediaTrackSupportedConstraints { + external bool get brightness; + external bool get colorTemperature; + external bool get contrast; + external bool get exposureCompensation; + external bool get exposureMode; + external bool get exposureTime; + external bool get focusDistance; + external bool get focusMode; + external bool get iso; + external bool get pan; + external bool get pointsOfInterest; + external bool get saturation; + external bool get sharpness; + external bool get tilt; + external bool get torch; + external bool get whiteBalanceMode; + external bool get zoom; +} diff --git a/lib/src/rtc_data_channel_impl.dart b/lib/src/rtc_data_channel_impl.dart index f503784..230e13d 100644 --- a/lib/src/rtc_data_channel_impl.dart +++ b/lib/src/rtc_data_channel_impl.dart @@ -1,33 +1,46 @@ import 'dart:async'; -import 'dart:html' as html; +import 'dart:js_interop'; import 'dart:js_util' as jsutil; +import 'package:web/web.dart' as web; import 'package:webrtc_interface/webrtc_interface.dart'; class RTCDataChannelWeb extends RTCDataChannel { RTCDataChannelWeb(this._jsDc) { stateChangeStream = _stateChangeController.stream; messageStream = _messageController.stream; - _jsDc.onClose.listen((_) { - _state = RTCDataChannelState.RTCDataChannelClosed; - _stateChangeController.add(_state); - onDataChannelState?.call(_state); - }); - _jsDc.onOpen.listen((_) { - _state = RTCDataChannelState.RTCDataChannelOpen; - _stateChangeController.add(_state); - onDataChannelState?.call(_state); - }); - _jsDc.onMessage.listen((event) async { - var msg = await _parse(event.data); - _messageController.add(msg); - onMessage?.call(msg); - }); - _jsDc.addEventListener('bufferedamountlow', (_) { - onBufferedAmountLow?.call(bufferedAmount ?? 0); - }); + _jsDc.addEventListener( + 'close', + (_) { + _state = RTCDataChannelState.RTCDataChannelClosed; + _stateChangeController.add(_state); + onDataChannelState?.call(_state); + }.toJS, + false.toJS); + _jsDc.addEventListener( + 'open', + (_) { + _state = RTCDataChannelState.RTCDataChannelOpen; + _stateChangeController.add(_state); + onDataChannelState?.call(_state); + }.toJS, + false.toJS); + _jsDc.addEventListener( + 'message', + (web.MessageEvent event) async { + var msg = await _parse(event.data); + _messageController.add(msg); + onMessage?.call(msg); + }.toJS, + false.toJS); + _jsDc.addEventListener( + 'bufferedamountlow', + (_) { + onBufferedAmountLow?.call(bufferedAmount ?? 0); + }.toJS, + false.toJS); } - final html.RtcDataChannel _jsDc; + final web.RTCDataChannel _jsDc; RTCDataChannelState _state = RTCDataChannelState.RTCDataChannelConnecting; @override @@ -44,7 +57,7 @@ class RTCDataChannelWeb extends RTCDataChannel { @override set bufferedAmountLowThreshold(int? bufferedAmountLowThreshold) { - _jsDc.bufferedAmountLowThreshold = bufferedAmountLowThreshold; + _jsDc.bufferedAmountLowThreshold = bufferedAmountLowThreshold ?? 0; } final _stateChangeController = @@ -53,9 +66,11 @@ class RTCDataChannelWeb extends RTCDataChannel { StreamController.broadcast(sync: true); Future _parse(dynamic data) async { - if (data is String) return RTCDataChannelMessage(data); + if (data is String) { + return RTCDataChannelMessage(data); + } dynamic arrayBuffer; - if (data is html.Blob) { + if (data is web.Blob) { // This should never happen actually arrayBuffer = await jsutil .promiseToFuture(jsutil.callMethod(data, 'arrayBuffer', [])); @@ -68,9 +83,9 @@ class RTCDataChannelWeb extends RTCDataChannel { @override Future send(RTCDataChannelMessage message) { if (!message.isBinary) { - _jsDc.send(message.text); + _jsDc.send(message.text.toJS); } else { - _jsDc.sendTypedData(message.binary); + _jsDc.send(message.binary.toJS); } return Future.value(); } diff --git a/lib/src/rtc_dtmf_sender_impl.dart b/lib/src/rtc_dtmf_sender_impl.dart index 553a775..b8e3e03 100644 --- a/lib/src/rtc_dtmf_sender_impl.dart +++ b/lib/src/rtc_dtmf_sender_impl.dart @@ -1,18 +1,18 @@ -import 'dart:html' as html; +import 'package:web/web.dart' as web; import 'package:webrtc_interface/webrtc_interface.dart'; class RTCDTMFSenderWeb extends RTCDTMFSender { RTCDTMFSenderWeb(this._jsDtmfSender); - final html.RtcDtmfSender _jsDtmfSender; + final web.RTCDTMFSender _jsDtmfSender; @override Future insertDTMF(String tones, {int duration = 100, int interToneGap = 70}) async { - return _jsDtmfSender.insertDtmf(tones, duration, interToneGap); + return _jsDtmfSender.insertDTMF(tones, duration, interToneGap); } @override Future canInsertDtmf() async { - return _jsDtmfSender.canInsertDtmf ?? false; + return _jsDtmfSender.canInsertDTMF; } } diff --git a/lib/src/rtc_peerconnection_impl.dart b/lib/src/rtc_peerconnection_impl.dart index 1e35987..17e6f04 100644 --- a/lib/src/rtc_peerconnection_impl.dart +++ b/lib/src/rtc_peerconnection_impl.dart @@ -1,9 +1,13 @@ import 'dart:async'; -import 'dart:html' as html; import 'dart:js' as js; +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'; @@ -19,147 +23,154 @@ import 'rtc_rtp_transceiver_impl.dart'; */ class RTCPeerConnectionWeb extends RTCPeerConnection { RTCPeerConnectionWeb(this._peerConnectionId, this._jsPc) { - _jsPc.onAddStream.listen((mediaStreamEvent) { - final jsStream = mediaStreamEvent.stream; - if (jsStream == null) { - throw Exception('Unable to get the stream from the event'); - } - if (jsStream.id == null) { - throw Exception('The stream must have a valid identifier'); - } - - final _remoteStream = _remoteStreams.putIfAbsent( - jsStream.id!, () => MediaStreamWeb(jsStream, _peerConnectionId)); - - onAddStream?.call(_remoteStream); - - jsStream.onAddTrack.listen((mediaStreamTrackEvent) { - final jsTrack = - (mediaStreamTrackEvent as html.MediaStreamTrackEvent).track; - if (jsTrack == null) { - throw Exception('The Media Stream track is null'); - } - final track = MediaStreamTrackWeb(jsTrack); - _remoteStream.addTrack(track, addToNative: false).then((_) { - onAddTrack?.call(_remoteStream, track); - }); - }); - - jsStream.onRemoveTrack.listen((mediaStreamTrackEvent) { - final jsTrack = - (mediaStreamTrackEvent as html.MediaStreamTrackEvent).track; - if (jsTrack == null) { - throw Exception('The Media Stream track is null'); - } - final track = MediaStreamTrackWeb(jsTrack); - _remoteStream.removeTrack(track, removeFromNative: false).then((_) { - onRemoveTrack?.call(_remoteStream, track); - }); - }); - }); - - _jsPc.onDataChannel.listen((dataChannelEvent) { - if (dataChannelEvent.channel != null) { - onDataChannel?.call(RTCDataChannelWeb(dataChannelEvent.channel!)); - } - }); - - _jsPc.onIceCandidate.listen((iceEvent) { - if (iceEvent.candidate != null) { - onIceCandidate?.call(_iceFromJs(iceEvent.candidate!)); - } - }); - - _jsPc.onIceConnectionStateChange.listen((_) { - _iceConnectionState = - iceConnectionStateForString(_jsPc.iceConnectionState); - onIceConnectionState?.call(_iceConnectionState!); - - if (browser.isFirefox) { - switch (_iceConnectionState!) { - case RTCIceConnectionState.RTCIceConnectionStateNew: - _connectionState = RTCPeerConnectionState.RTCPeerConnectionStateNew; - break; - case RTCIceConnectionState.RTCIceConnectionStateChecking: - _connectionState = - RTCPeerConnectionState.RTCPeerConnectionStateConnecting; - break; - case RTCIceConnectionState.RTCIceConnectionStateConnected: - _connectionState = - RTCPeerConnectionState.RTCPeerConnectionStateConnected; - break; - case RTCIceConnectionState.RTCIceConnectionStateFailed: - _connectionState = - RTCPeerConnectionState.RTCPeerConnectionStateFailed; - break; - case RTCIceConnectionState.RTCIceConnectionStateDisconnected: - _connectionState = - RTCPeerConnectionState.RTCPeerConnectionStateDisconnected; - break; - case RTCIceConnectionState.RTCIceConnectionStateClosed: - _connectionState = - RTCPeerConnectionState.RTCPeerConnectionStateClosed; - break; - default: - break; - } - onConnectionState?.call(_connectionState!); - } - }); + _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) { + if (dataChannelEvent.channel != null) { + onDataChannel?.call(RTCDataChannelWeb(dataChannelEvent.channel!)); + } + }.toJS); + + _jsPc.addEventListener( + 'icecandidate', + (iceEvent) { + if (iceEvent.candidate != null) { + onIceCandidate?.call(_iceFromJs(iceEvent.candidate!)); + } + }.toJS); + + _jsPc.addEventListener( + 'iceconnectionstatechange', + (_) { + _iceConnectionState = + iceConnectionStateForString(_jsPc.iceConnectionState); + onIceConnectionState?.call(_iceConnectionState!); + + if (browser.isFirefox) { + switch (_iceConnectionState!) { + case RTCIceConnectionState.RTCIceConnectionStateNew: + _connectionState = + RTCPeerConnectionState.RTCPeerConnectionStateNew; + break; + case RTCIceConnectionState.RTCIceConnectionStateChecking: + _connectionState = + RTCPeerConnectionState.RTCPeerConnectionStateConnecting; + break; + case RTCIceConnectionState.RTCIceConnectionStateConnected: + _connectionState = + RTCPeerConnectionState.RTCPeerConnectionStateConnected; + break; + case RTCIceConnectionState.RTCIceConnectionStateFailed: + _connectionState = + RTCPeerConnectionState.RTCPeerConnectionStateFailed; + break; + case RTCIceConnectionState.RTCIceConnectionStateDisconnected: + _connectionState = + RTCPeerConnectionState.RTCPeerConnectionStateDisconnected; + break; + case RTCIceConnectionState.RTCIceConnectionStateClosed: + _connectionState = + RTCPeerConnectionState.RTCPeerConnectionStateClosed; + break; + default: + break; + } + onConnectionState?.call(_connectionState!); + } + }.toJS); jsutil.setProperty(_jsPc, 'onicegatheringstatechange', js.allowInterop((_) { _iceGatheringState = iceGatheringStateforString(_jsPc.iceGatheringState); onIceGatheringState?.call(_iceGatheringState!); })); - _jsPc.onRemoveStream.listen((mediaStreamEvent) { - if (mediaStreamEvent.stream?.id != null) { - final _remoteStream = - _remoteStreams.remove(mediaStreamEvent.stream!.id); - if (_remoteStream != null) { - onRemoveStream?.call(_remoteStream); - } - } - }); - - _jsPc.onSignalingStateChange.listen((_) { - _signalingState = signalingStateForString(_jsPc.signalingState); - onSignalingState?.call(_signalingState!); - }); + _jsPc.addEventListener( + 'removestream', + (_RTCMediaStreamEvent mediaStreamEvent) { + final _remoteStream = + _remoteStreams.remove(mediaStreamEvent.stream.id); + if (_remoteStream != null) { + onRemoveStream?.call(_remoteStream); + } + }.toJS); + + _jsPc.addEventListener( + 'signalingstatechange', + (_) { + _signalingState = signalingStateForString(_jsPc.signalingState); + onSignalingState?.call(_signalingState!); + }.toJS); if (!browser.isFirefox) { - _jsPc.onConnectionStateChange.listen((_) { - _connectionState = peerConnectionStateForString(_jsPc.connectionState); - onConnectionState?.call(_connectionState!); - }); + _jsPc.addEventListener( + 'connectionstatechange', + (_) { + _connectionState = + peerConnectionStateForString(_jsPc.connectionState); + onConnectionState?.call(_connectionState!); + }.toJS); } - _jsPc.onNegotiationNeeded.listen((_) { - onRenegotiationNeeded?.call(); - }); - - _jsPc.onTrack.listen((trackEvent) { - if (trackEvent.track != null && trackEvent.receiver != null) { - onTrack?.call( - RTCTrackEvent( - track: MediaStreamTrackWeb(trackEvent.track!), - receiver: RTCRtpReceiverWeb(trackEvent.receiver!), - transceiver: RTCRtpTransceiverWeb.fromJsObject( - jsutil.getProperty(trackEvent, 'transceiver')), - streams: (trackEvent.streams != null) - ? trackEvent.streams! + _jsPc.addEventListener( + 'onnegotiationneeded', + (_) { + onRenegotiationNeeded?.call(); + }.toJS); + + _jsPc.addEventListener( + 'track', + (web.RTCTrackEvent trackEvent) { + onTrack?.call( + RTCTrackEvent( + track: MediaStreamTrackWeb(trackEvent.track), + receiver: RTCRtpReceiverWeb(trackEvent.receiver), + transceiver: RTCRtpTransceiverWeb.fromJsObject( + jsutil.getProperty(trackEvent, 'transceiver')), + streams: trackEvent.streams.toDart .map((dynamic stream) => MediaStreamWeb(stream, _peerConnectionId)) - .toList() - : [], - ), - ); - } - }); + .toList()), + ); + }.toJS); } final String _peerConnectionId; - late final html.RtcPeerConnection _jsPc; + late final web.RTCPeerConnection _jsPc; final _localStreams = {}; final _remoteStreams = {}; final _configuration = {}; @@ -251,28 +262,40 @@ class RTCPeerConnectionWeb extends RTCPeerConnection { Future setConfiguration(Map configuration) { _configuration.addAll(configuration); - _jsPc.setConfiguration(configuration); + final object = jsutil.newObject(); + for (var key in configuration.keys) { + jsutil.setProperty(object, key, configuration[key]); + } + + _jsPc.setConfiguration(object as web.RTCConfiguration); return Future.value(); } @override Future createOffer( [Map? constraints]) async { - final args = constraints != null ? [jsutil.jsify(constraints)] : []; - final desc = await jsutil.promiseToFuture( - jsutil.callMethod(_jsPc, 'createOffer', args)); - return RTCSessionDescription( - jsutil.getProperty(desc, 'sdp'), jsutil.getProperty(desc, 'type')); + final args = newObject(); + if (constraints != null) { + for (var key in constraints.keys) { + args.setProperty(key.toJS, constraints[key]); + } + } + final desc = await _jsPc.createOffer(args).toDart; + + return RTCSessionDescription(desc!.sdp, desc.type); } @override Future createAnswer( [Map? constraints]) async { - final args = constraints != null ? [jsutil.jsify(constraints)] : []; - final desc = await jsutil.promiseToFuture( - jsutil.callMethod(_jsPc, 'createAnswer', args)); - return RTCSessionDescription( - jsutil.getProperty(desc, 'sdp'), jsutil.getProperty(desc, 'type')); + final args = newObject(); + if (constraints != null) { + for (var key in constraints.keys) { + args.setProperty(key.toJS, constraints[key]); + } + } + final desc = await _jsPc.createAnswer(args).toDart; + return RTCSessionDescription(desc!.sdp, desc.type); } @override @@ -280,7 +303,9 @@ class RTCPeerConnectionWeb extends RTCPeerConnection { var _native = stream as MediaStreamWeb; _localStreams.putIfAbsent( stream.id, () => MediaStreamWeb(_native.jsStream, _peerConnectionId)); - _jsPc.addStream(_native.jsStream); + + _jsPc.addStream(stream.jsStream); + return Future.value(); } @@ -294,12 +319,22 @@ class RTCPeerConnectionWeb extends RTCPeerConnection { @override Future setLocalDescription(RTCSessionDescription description) async { - await _jsPc.setLocalDescription(description.toMap()); + await _jsPc + .setLocalDescription(web.RTCLocalSessionDescriptionInit( + type: description.type!, + sdp: description.sdp!, + )) + .toDart; } @override Future setRemoteDescription(RTCSessionDescription description) async { - await _jsPc.setRemoteDescription(description.toMap()); + await _jsPc + .setRemoteDescription(web.RTCSessionDescriptionInit( + type: description.type!, + sdp: description.sdp!, + )) + .toDart; } @override @@ -319,9 +354,13 @@ class RTCPeerConnectionWeb extends RTCPeerConnection { } @override - Future addCandidate(RTCIceCandidate candidate) { - return jsutil.promiseToFuture( - jsutil.callMethod(_jsPc, 'addIceCandidate', [_iceToJs(candidate)])); + Future addCandidate(RTCIceCandidate candidate) async { + await _jsPc + .addIceCandidate(web.RTCIceCandidateInit( + candidate: candidate.candidate!, + sdpMid: candidate.sdpMid!, + sdpMLineIndex: candidate.sdpMLineIndex)) + .toDart; } @override @@ -329,10 +368,9 @@ class RTCPeerConnectionWeb extends RTCPeerConnection { var stats; if (track != null) { var jsTrack = (track as MediaStreamTrackWeb).jsTrack; - stats = await jsutil.promiseToFuture( - jsutil.callMethod(_jsPc, 'getStats', [jsTrack])); + stats = await _jsPc.getStats(jsTrack).toDart; } else { - stats = await _jsPc.getStats(); + stats = await _jsPc.getStats().toDart; } var report = []; @@ -345,11 +383,12 @@ class RTCPeerConnectionWeb extends RTCPeerConnection { @override List getLocalStreams() => - _jsPc.getLocalStreams().map((e) => _localStreams[e.id]!).toList(); + _jsPc.getLocalStreams().toDart.map((e) => _localStreams[e.id]!).toList(); @override List getRemoteStreams() => _jsPc .getRemoteStreams() + .toDart .map((jsStream) => _remoteStreams[jsStream.id]!) .toList(); @@ -361,7 +400,17 @@ class RTCPeerConnectionWeb extends RTCPeerConnection { map['binaryType'] = 'arraybuffer'; // Avoid Blob in data channel } - final jsDc = _jsPc.createDataChannel(label, map); + 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'], + )); + return Future.value(RTCDataChannelWeb(jsDc)); } @@ -380,7 +429,7 @@ class RTCPeerConnectionWeb extends RTCPeerConnection { @override RTCDTMFSender createDtmfSender(MediaStreamTrack track) { var _native = track as MediaStreamTrackWeb; - var jsDtmfSender = _jsPc.createDtmfSender(_native.jsTrack); + var jsDtmfSender = _jsPc.createDTMFSender(_native.jsTrack); return RTCDTMFSenderWeb(jsDtmfSender); } @@ -388,16 +437,13 @@ class RTCPeerConnectionWeb extends RTCPeerConnection { // utility section // - RTCIceCandidate _iceFromJs(html.RtcIceCandidate candidate) => RTCIceCandidate( + RTCIceCandidate _iceFromJs(web.RTCIceCandidate candidate) => RTCIceCandidate( candidate.candidate, candidate.sdpMid, candidate.sdpMLineIndex, ); - html.RtcIceCandidate _iceToJs(RTCIceCandidate c) => - html.RtcIceCandidate(c.toMap()); - - RTCSessionDescription _sessionFromJs(html.RtcSessionDescription? sd) => + RTCSessionDescription _sessionFromJs(web.RTCSessionDescription? sd) => RTCSessionDescription(sd?.sdp, sd?.type); @override @@ -481,3 +527,19 @@ class RTCPeerConnectionWeb extends RTCPeerConnection { ); } } + +extension type _RTCMediaStreamEvent._(JSObject _) + implements web.Event, JSObject { + external web.MediaStream get stream; +} + +extension _AddRemoveStream on web.RTCPeerConnection { + external void addStream(web.MediaStream stream); + + external void removeStream(web.MediaStream stream); + + external JSArray getLocalStreams(); + external JSArray getRemoteStreams(); + + external web.RTCDTMFSender createDTMFSender(web.MediaStreamTrack track); +} diff --git a/lib/src/rtc_rtp_receiver_impl.dart b/lib/src/rtc_rtp_receiver_impl.dart index ae33210..e3cc1a8 100644 --- a/lib/src/rtc_rtp_receiver_impl.dart +++ b/lib/src/rtc_rtp_receiver_impl.dart @@ -1,5 +1,5 @@ -import 'dart:html'; import 'dart:js_util' as jsutil; +import 'package:web/web.dart' as web; import 'package:webrtc_interface/webrtc_interface.dart'; import 'media_stream_track_impl.dart'; @@ -9,7 +9,7 @@ class RTCRtpReceiverWeb extends RTCRtpReceiver { RTCRtpReceiverWeb(this._jsRtpReceiver); /// private: - final RtcRtpReceiver _jsRtpReceiver; + final web.RTCRtpReceiver _jsRtpReceiver; @override Future> getStats() async { @@ -33,10 +33,10 @@ class RTCRtpReceiverWeb extends RTCRtpReceiver { } @override - MediaStreamTrack get track => MediaStreamTrackWeb(_jsRtpReceiver.track!); + MediaStreamTrack get track => MediaStreamTrackWeb(_jsRtpReceiver.track); @override String get receiverId => '${_jsRtpReceiver.hashCode}'; - RtcRtpReceiver get jsRtpReceiver => _jsRtpReceiver; + web.RTCRtpReceiver get jsRtpReceiver => _jsRtpReceiver; } diff --git a/lib/src/rtc_rtp_sender_impl.dart b/lib/src/rtc_rtp_sender_impl.dart index b5b0bd8..b7d3b9c 100644 --- a/lib/src/rtc_rtp_sender_impl.dart +++ b/lib/src/rtc_rtp_sender_impl.dart @@ -1,8 +1,8 @@ import 'dart:async'; -import 'dart:html'; 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 'media_stream_track_impl.dart'; @@ -12,11 +12,11 @@ import 'rtc_rtp_parameters_impl.dart'; class RTCRtpSenderWeb extends RTCRtpSender { RTCRtpSenderWeb(this._jsRtpSender, this._ownsTrack); - factory RTCRtpSenderWeb.fromJsSender(RtcRtpSender jsRtpSender) { + factory RTCRtpSenderWeb.fromJsSender(web.RTCRtpSender jsRtpSender) { return RTCRtpSenderWeb(jsRtpSender, jsRtpSender.track != null); } - final RtcRtpSender _jsRtpSender; + final web.RTCRtpSender _jsRtpSender; bool _ownsTrack = false; @override @@ -115,5 +115,5 @@ class RTCRtpSenderWeb extends RTCRtpSender { @override Future dispose() async {} - RtcRtpSender get jsRtpSender => _jsRtpSender; + web.RTCRtpSender get jsRtpSender => _jsRtpSender; } diff --git a/lib/src/rtc_transform_stream.dart b/lib/src/rtc_transform_stream.dart index bdb7a00..d2b1952 100644 --- a/lib/src/rtc_transform_stream.dart +++ b/lib/src/rtc_transform_stream.dart @@ -1,8 +1,7 @@ -import 'dart:html'; import 'dart:js_util' as js_util; import 'dart:typed_data'; - import 'package:js/js.dart'; +import 'package:web/web.dart'; @JS('WritableStream') abstract class WritableStream { diff --git a/lib/src/rtc_video_element.dart b/lib/src/rtc_video_element.dart index 9afd795..6901a97 100644 --- a/lib/src/rtc_video_element.dart +++ b/lib/src/rtc_video_element.dart @@ -1,10 +1,10 @@ -import 'dart:html' as html; - +import 'dart:js_interop'; +import 'package:web/web.dart' as web; import '../dart_webrtc.dart'; class RTCVideoElement { RTCVideoElement() { - _html = html.VideoElement() + _html = web.HTMLVideoElement() ..autoplay = true ..muted = false ..controls = false @@ -17,8 +17,8 @@ class RTCVideoElement { MediaStream? _stream; - late html.VideoElement _html; - html.VideoElement get htmlElement => _html; + late web.HTMLVideoElement _html; + web.HTMLVideoElement get htmlElement => _html; /// contain or cover set objectFit(String fit) => _html.style.objectFit = fit; @@ -36,13 +36,13 @@ class RTCVideoElement { int get videoHeight => _html.videoHeight; - Stream get onEnded => _html.onEnded; + Stream get onEnded => _html.onEnded; - Stream get onError => _html.onError; + Stream get onError => _html.onError; - Stream get onCanPlay => _html.onCanPlay; + Stream get onCanPlay => _html.onCanPlay; - Stream get onResize => _html.onResize; + Stream get onResize => _html.onResize; dynamic get error => _html.error; @@ -61,5 +61,9 @@ class RTCVideoElement { void removeAttribute(String name) => _html.removeAttribute(name); - Future setSinkId(String sinkId) => _html.setSinkId(sinkId); + Future setSinkId(String sinkId) => _html.setSinkId(sinkId).toDart; +} + +extension _SetSinkId on web.HTMLMediaElement { + external JSPromise setSinkId(String sinkId); } diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 9a3ad17..7bd218e 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -1,5 +1,5 @@ -import 'dart:html' as html; import 'dart:math'; +import 'package:web/web.dart' as web; bool get isMobile { final toMatch = [ @@ -11,7 +11,7 @@ bool get isMobile { 'BlackBerry', 'Windows Phone' ]; - return toMatch.indexWhere((device) => html.window.navigator.userAgent + return toMatch.indexWhere((device) => web.window.navigator.userAgent .contains(RegExp(device, caseSensitive: false))) != -1; } diff --git a/pubspec.yaml b/pubspec.yaml index c334d17..5682271 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,10 +1,10 @@ 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.2.1 +version: 1.3.0 homepage: https://github.com/flutter-webrtc/dart-webrtc environment: - sdk: '>=2.13.0 <4.0.0' + sdk: '>=3.3.0 <4.0.0' dependencies: collection: ^1.17.1 @@ -13,6 +13,7 @@ dependencies: meta: ^1.8.0 platform_detect: ^2.0.7 synchronized: ^3.0.0+3 + web: ^0.5.1 webrtc_interface: ^1.1.2 dev_dependencies: diff --git a/web/main.dart b/web/main.dart index 79bbd35..8dd8300 100644 --- a/web/main.dart +++ b/web/main.dart @@ -1,7 +1,7 @@ -import 'dart:html' as html; import 'dart:typed_data'; import 'package:dart_webrtc/dart_webrtc.dart'; +import 'package:web/web.dart' as web; /* import 'test_media_devices.dart' as media_devices_tests; @@ -25,11 +25,11 @@ List pc1FrameCryptors = []; List pc2FrameCryptors = []; void loopBackTest() async { - var local = html.document.querySelector('#local'); + var local = web.document.querySelector('#local'); var localVideo = RTCVideoElement(); local!.append(localVideo.htmlElement); - var remote = html.document.querySelector('#remote'); + var remote = web.document.querySelector('#remote'); var remotelVideo = RTCVideoElement(); remote!.append(remotelVideo.htmlElement); diff --git a/web/p2p/p2p.dart b/web/p2p/p2p.dart index a3f5ec7..f7fbbdd 100644 --- a/web/p2p/p2p.dart +++ b/web/p2p/p2p.dart @@ -1,8 +1,7 @@ -import 'dart:html' as html; - import 'package:dart_webrtc/dart_webrtc.dart'; import 'package:js/js.dart'; import 'package:test/test.dart'; +import 'package:web/web.dart' as web; import 'signaling.dart'; @@ -19,13 +18,13 @@ void main() { var signaling = Signaling('demo.cloudwebrtc.com'); - var local = html.document.querySelector('#local'); + var local = web.document.querySelector('#local'); var localVideo = RTCVideoElement(); local?.append(localVideo.htmlElement); - var remote = html.document.querySelector('#remote'); + var remote = web.document.querySelector('#remote'); var remoteVideo = RTCVideoElement(); @@ -41,7 +40,7 @@ void main() { signaling.connect(); signaling.onStateChange = (SignalingState state) { - html.document.querySelector('#output')?.text = state.toString(); + web.document.querySelector('#output')?.text = state.toString(); if (state == SignalingState.CallStateBye) { localVideo.srcObject = null; remoteVideo.srcObject = null; diff --git a/web/p2p/simple_websocket.dart b/web/p2p/simple_websocket.dart index 04ca58b..07ec162 100644 --- a/web/p2p/simple_websocket.dart +++ b/web/p2p/simple_websocket.dart @@ -1,7 +1,6 @@ import 'dart:convert'; -import 'dart:html'; - import 'package:http/http.dart' as http; +import 'package:web/web.dart' as web; typedef OnMessageCallback = void Function(dynamic msg); typedef OnCloseCallback = void Function(int code, String reason); @@ -20,7 +19,7 @@ class SimpleWebSocket { Future connect() async { try { - _socket = WebSocket(_url); + _socket = web.WebSocket(_url); _socket.onOpen.listen((e) { onOpen?.call(); }); @@ -38,7 +37,7 @@ class SimpleWebSocket { } void send(data) { - if (_socket != null && _socket.readyState == WebSocket.OPEN) { + if (_socket != null && _socket.readyState == web.WebSocket.OPEN) { _socket.send(data); print('send: $data'); } else { diff --git a/web/test_media_devices.dart b/web/test_media_devices.dart index e9d8eae..a492f91 100644 --- a/web/test_media_devices.dart +++ b/web/test_media_devices.dart @@ -8,21 +8,16 @@ void closeMediaStream(MediaStream stream) { } List testFunctions = [ - () => test('MediaDevices.constructor()', () { - expect(navigator.mediaDevices != null, true); - }), () => test('MediaDevices.enumerateDevices()', () async { var list = await navigator.mediaDevices.enumerateDevices(); list.forEach((e) { print('${e.runtimeType}: ${e.label}, type => ${e.kind}'); }); - expect(list != null, true); }), () => test('MediaDevices.getUserMedia()', () async { var stream = await navigator.mediaDevices .getUserMedia({'audio': true, 'video': true}); print('getUserMedia: stream.id => ${stream.id}'); - expect(stream != null, true); print( 'getUserMedia: audio track.id => ${stream.getAudioTracks()[0].id}');