From 1b39c06c5ba38f803087ab876cdd59f2ed74422e Mon Sep 17 00:00:00 2001 From: Alex Shepeliev Date: Sat, 4 May 2024 17:22:01 +0300 Subject: [PATCH 01/16] Add WasmJS wip --- gradle/libs.versions.toml | 1 + webrtc-kmp/build.gradle.kts | 35 ++ .../com/shepeliev/webrtckmp/IceServer.kt | 30 +- .../com/shepeliev/webrtckmp/PeerConnection.kt | 25 +- .../shepeliev/webrtckmp/RtcConfiguration.kt | 13 +- .../com/shepeliev/webrtckmp/IceServer.kt | 20 +- .../com/shepeliev/webrtckmp/MediaStream.kt | 12 +- .../webrtckmp/MediaTrackConstraints.kt | 9 +- .../com/shepeliev/webrtckmp/PeerConnection.kt | 4 +- .../shepeliev/webrtckmp/RtcCertificatePem.kt | 2 + .../shepeliev/webrtckmp/RtcConfiguration.kt | 14 +- .../shepeliev/webrtckmp/SessionDescription.kt | 2 + .../shepeliev/webrtckmp/DataChannelTest.kt | 1 + .../com/shepeliev/webrtckmp/IceServerTest.kt | 16 - .../shepeliev/webrtckmp/PeerConnectionTest.kt | 306 ++++++++++++++++-- .../shepeliev/webrtckmp/RtcCertificateTest.kt | 11 + .../com/shepeliev/webrtckmp/TestUtils.kt | 5 - .../com/shepeliev/webrtckmp/IceServer.kt | 41 --- .../com/shepeliev/webrtckmp/PeerConnection.kt | 21 +- .../shepeliev/webrtckmp/internal/IceServer.kt | 31 ++ .../{ => internal}/RtcConfiguration.kt | 32 +- .../com/shepeliev/webrtckmp/TestUtils.kt | 39 --- .../com/shepeliev/webrtckmp/DataChannel.kt | 18 +- .../com/shepeliev/webrtckmp/DtmfSender.kt | 2 + .../com/shepeliev/webrtckmp/IceCandidate.kt | 13 +- .../com/shepeliev/webrtckmp/MediaStream.kt | 25 ++ .../webrtckmp/MediaStreamTrackImpl.kt | 48 +++ .../com/shepeliev/webrtckmp/PeerConnection.kt | 280 ++++++++++++++++ .../shepeliev/webrtckmp/RtcCertificatePem.kt | 18 ++ .../com/shepeliev/webrtckmp/RtcStats.kt | 0 .../com/shepeliev/webrtckmp/RtcStatsReport.kt | 0 .../com/shepeliev/webrtckmp/RtpParameters.kt | 58 ++++ .../com/shepeliev/webrtckmp/RtpReceiver.kt | 9 + .../com/shepeliev/webrtckmp/RtpSender.kt | 11 +- .../com/shepeliev/webrtckmp/RtpTransceiver.kt | 4 +- .../shepeliev/webrtckmp/VideoStreamTrack.kt | 5 +- .../com/shepeliev/webrtckmp/externals/Date.kt | 3 + .../webrtckmp/externals/ErrorEvent.kt | 5 + .../externals/PlatformMediaStream.kt | 11 + .../externals/PlatformMediaStreamTrack.kt | 70 ++++ .../webrtckmp/externals/RTCCertificate.kt | 9 + .../webrtckmp/externals/RTCDTMFSender.kt | 6 + .../webrtckmp/externals/RTCDataChannel.kt | 22 ++ .../externals/RTCDataChannelEvent.kt | 5 + .../webrtckmp/externals/RTCIceCandidate.kt | 9 + .../webrtckmp/externals/RTCPeerConnection.kt | 49 +++ .../RTCPeerConnectionConfiguration.kt | 3 + .../externals/RTCPeerConnectionIceEvent.kt | 5 + .../webrtckmp/externals/RTCRtcpParameters.kt | 6 + .../externals/RTCRtpCodecParameters.kt | 9 + .../webrtckmp/externals/RTCRtpParameters.kt | 8 + .../webrtckmp/externals/RTCRtpReceiver.kt | 6 + .../webrtckmp/externals/RTCRtpSender.kt | 11 + .../webrtckmp/externals/RTCRtpTransceiver.kt | 12 + .../externals/RTCSessionDescription.kt | 17 + .../webrtckmp/externals/RTCStatsReport.kt | 7 + .../webrtckmp/externals/RTCTrackEvent.kt | 9 + .../shepeliev/webrtckmp/internal/Console.kt | 12 + .../com/shepeliev/webrtckmp/internal/JSON.kt | 3 + .../internal/JsMediaTrackConstraintsExt.kt | 72 +++++ .../webrtckmp/internal/PeerConnection.kt | 22 ++ .../webrtckmp/internal/RtcConfiguration.kt | 6 + .../webrtckmp/AudioStreamTrackImpl.kt | 7 - .../com/shepeliev/webrtckmp/IceServer.kt | 27 -- .../webrtckmp/JsMediaTrackSettiiingsExt.kt | 20 -- .../com/shepeliev/webrtckmp/MediaDevices.kt | 6 +- .../com/shepeliev/webrtckmp/MediaStream.kt | 28 -- .../webrtckmp/MediaStreamTrackImpl.kt | 68 ---- .../com/shepeliev/webrtckmp/PeerConnection.kt | 209 ------------ .../shepeliev/webrtckmp/RtcCertificatePem.kt | 31 -- .../shepeliev/webrtckmp/RtcConfiguration.kt | 46 --- .../com/shepeliev/webrtckmp/RtpParameters.kt | 62 ---- .../com/shepeliev/webrtckmp/RtpReceiver.kt | 12 - .../webrtckmp/SessionDescriptionExt.kt | 17 - .../webrtckmp/VideoStreamTrackImpl.kt | 11 - .../com/shepeliev/webrtckmp/externals.kt | 153 --------- .../externals/PlatformMediaStream.js.kt | 8 + .../externals/PlatformMediaStreamTrack.js.kt | 9 + .../webrtckmp/externals/RTCCertificate.js.kt | 28 ++ .../webrtckmp/externals/RTCDataChannel.js.kt | 23 ++ .../webrtckmp/externals/RTCIceCandidate.js.kt | 24 ++ .../externals/RTCPeerConnection.js.kt | 107 ++++++ .../externals/RTCRtpParameters.js.kt | 15 + .../webrtckmp/externals/RTCRtpSender.js.kt | 14 + .../webrtckmp/externals/RTCTrackEvent.js.kt | 10 + .../shepeliev/webrtckmp/internal/IceServer.kt | 11 + .../shepeliev/webrtckmp/internal/JSON.js.kt | 3 + .../MediaTrackConstraintsSet.kt} | 70 ++-- .../webrtckmp/internal/OfferAnswerOptions.kt | 14 + .../webrtckmp/internal/RtcConfiguration.js.kt | 22 ++ .../webrtckmp/internal/RtcConfiguration.kt | 17 + .../webrtckmp/internal/SessionDescription.kt | 10 + .../JsMediaTrackConstraintsExtTest.kt | 1 + .../com/shepeliev/webrtckmp/TestUtils.kt | 27 -- .../webrtckmp/MediaDevices.wasmJs.kt | 4 + .../externals/PlatformMediaStream.wasmJs.kt | 9 + .../PlatformMediaStreamTrack.wasmJs.kt | 9 + .../externals/RTCCertificate.wasmJs.kt | 45 +++ .../externals/RTCDataChannel.wasmJs.kt | 30 ++ .../externals/RTCIceCandidate.wasmJs.kt | 18 ++ .../externals/RTCPeerConnection.wasmJs.kt | 222 +++++++++++++ .../externals/RTCRtpParameters.wasmJs.kt | 20 ++ .../externals/RTCRtpReceiver.wasmJs.kt | 4 + .../externals/RTCRtpSender.wasmJs.kt | 15 + .../externals/RTCRtpTransceiver.wasmJs.kt | 4 + .../externals/RTCSessionDescription.wasmJs.kt | 4 + .../externals/RTCStatsReport.wasmJs.kt | 4 + .../externals/RTCTrackEvent.wasmJs.kt | 13 + .../shepeliev/webrtckmp/internal/IceServer.kt | 18 ++ .../webrtckmp/internal/JSON.wasmJs.kt | 10 + .../com/shepeliev/webrtckmp/internal/JsAny.kt | 9 + .../com/shepeliev/webrtckmp/internal/List.kt | 14 + .../internal/MediaTrackConstraintsSet.kt | 130 ++++++++ .../webrtckmp/internal/OfferAnswerOptions.kt | 28 ++ .../shepeliev/webrtckmp/internal/Promise.kt | 16 + .../webrtckmp/internal/RtcCertificatePem.kt | 9 + .../webrtckmp/internal/RtcConfiguration.kt | 5 + .../internal/RtcConfiguration.wasmJs.kt | 42 +++ .../webrtckmp/internal/SessionDescription.kt | 12 + .../internal/MediaTrackConstraintsSetTest.kt | 54 ++++ 120 files changed, 2410 insertions(+), 1006 deletions(-) delete mode 100644 webrtc-kmp/src/commonTest/kotlin/com/shepeliev/webrtckmp/IceServerTest.kt create mode 100644 webrtc-kmp/src/commonTest/kotlin/com/shepeliev/webrtckmp/RtcCertificateTest.kt delete mode 100644 webrtc-kmp/src/commonTest/kotlin/com/shepeliev/webrtckmp/TestUtils.kt delete mode 100644 webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/IceServer.kt create mode 100644 webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/internal/IceServer.kt rename webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/{ => internal}/RtcConfiguration.kt (61%) delete mode 100644 webrtc-kmp/src/iosTest/kotlin/com/shepeliev/webrtckmp/TestUtils.kt rename webrtc-kmp/src/{jsMain => jsAndWasmJsMain}/kotlin/com/shepeliev/webrtckmp/DataChannel.kt (78%) rename webrtc-kmp/src/{jsMain => jsAndWasmJsMain}/kotlin/com/shepeliev/webrtckmp/DtmfSender.kt (88%) rename webrtc-kmp/src/{jsMain => jsAndWasmJsMain}/kotlin/com/shepeliev/webrtckmp/IceCandidate.kt (55%) create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/MediaStreamTrackImpl.kt create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/RtcCertificatePem.kt rename webrtc-kmp/src/{jsMain => jsAndWasmJsMain}/kotlin/com/shepeliev/webrtckmp/RtcStats.kt (100%) rename webrtc-kmp/src/{jsMain => jsAndWasmJsMain}/kotlin/com/shepeliev/webrtckmp/RtcStatsReport.kt (100%) create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/RtpParameters.kt create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/RtpReceiver.kt rename webrtc-kmp/src/{jsMain => jsAndWasmJsMain}/kotlin/com/shepeliev/webrtckmp/RtpSender.kt (50%) rename webrtc-kmp/src/{jsMain => jsAndWasmJsMain}/kotlin/com/shepeliev/webrtckmp/RtpTransceiver.kt (90%) rename webrtc-kmp/src/{jsMain => jsAndWasmJsMain}/kotlin/com/shepeliev/webrtckmp/VideoStreamTrack.kt (58%) create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/Date.kt create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/ErrorEvent.kt create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStream.kt create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStreamTrack.kt create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCCertificate.kt create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCDTMFSender.kt create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCDataChannel.kt create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCDataChannelEvent.kt create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCIceCandidate.kt create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCPeerConnection.kt create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCPeerConnectionConfiguration.kt create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCPeerConnectionIceEvent.kt create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtcpParameters.kt create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpCodecParameters.kt create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpParameters.kt create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpReceiver.kt create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpSender.kt create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpTransceiver.kt create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCSessionDescription.kt create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCStatsReport.kt create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCTrackEvent.kt create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/Console.kt create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/JSON.kt create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/JsMediaTrackConstraintsExt.kt create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/PeerConnection.kt create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/RtcConfiguration.kt delete mode 100644 webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/AudioStreamTrackImpl.kt delete mode 100644 webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/IceServer.kt delete mode 100644 webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/JsMediaTrackSettiiingsExt.kt delete mode 100644 webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt delete mode 100644 webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/MediaStreamTrackImpl.kt delete mode 100644 webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt delete mode 100644 webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/RtcCertificatePem.kt delete mode 100644 webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/RtcConfiguration.kt delete mode 100644 webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/RtpParameters.kt delete mode 100644 webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/RtpReceiver.kt delete mode 100644 webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/SessionDescriptionExt.kt delete mode 100644 webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/VideoStreamTrackImpl.kt delete mode 100644 webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals.kt create mode 100644 webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStream.js.kt create mode 100644 webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStreamTrack.js.kt create mode 100644 webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCCertificate.js.kt create mode 100644 webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCDataChannel.js.kt create mode 100644 webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCIceCandidate.js.kt create mode 100644 webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCPeerConnection.js.kt create mode 100644 webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpParameters.js.kt create mode 100644 webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpSender.js.kt create mode 100644 webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCTrackEvent.js.kt create mode 100644 webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/internal/IceServer.kt create mode 100644 webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/internal/JSON.js.kt rename webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/{JsMediaTrackConstraintsExt.kt => internal/MediaTrackConstraintsSet.kt} (87%) create mode 100644 webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/internal/OfferAnswerOptions.kt create mode 100644 webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/internal/RtcConfiguration.js.kt create mode 100644 webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/internal/RtcConfiguration.kt create mode 100644 webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/internal/SessionDescription.kt delete mode 100644 webrtc-kmp/src/jsTest/kotlin/com/shepeliev/webrtckmp/TestUtils.kt create mode 100644 webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/MediaDevices.wasmJs.kt create mode 100644 webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStream.wasmJs.kt create mode 100644 webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStreamTrack.wasmJs.kt create mode 100644 webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCCertificate.wasmJs.kt create mode 100644 webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCDataChannel.wasmJs.kt create mode 100644 webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCIceCandidate.wasmJs.kt create mode 100644 webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCPeerConnection.wasmJs.kt create mode 100644 webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpParameters.wasmJs.kt create mode 100644 webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpReceiver.wasmJs.kt create mode 100644 webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpSender.wasmJs.kt create mode 100644 webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpTransceiver.wasmJs.kt create mode 100644 webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCSessionDescription.wasmJs.kt create mode 100644 webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCStatsReport.wasmJs.kt create mode 100644 webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCTrackEvent.wasmJs.kt create mode 100644 webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/IceServer.kt create mode 100644 webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/JSON.wasmJs.kt create mode 100644 webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/JsAny.kt create mode 100644 webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/List.kt create mode 100644 webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/MediaTrackConstraintsSet.kt create mode 100644 webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/OfferAnswerOptions.kt create mode 100644 webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/Promise.kt create mode 100644 webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/RtcCertificatePem.kt create mode 100644 webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/RtcConfiguration.kt create mode 100644 webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/RtcConfiguration.wasmJs.kt create mode 100644 webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/SessionDescription.kt create mode 100644 webrtc-kmp/src/wasmJsTest/kotlin/com/shepeliev/webrtckmp/internal/MediaTrackConstraintsSetTest.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d8ceb219..f091fd1f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,6 +32,7 @@ compose-plugin = "1.6.2" webrtc-sdk = { module = "io.github.webrtc-sdk:android", version.ref = "webrtc-android-sdk" } kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlin-coroutines" } kotlin-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlin-coroutines" } +kotlin-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlin-coroutines" } androidx-coreKtx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" } diff --git a/webrtc-kmp/build.gradle.kts b/webrtc-kmp/build.gradle.kts index 4c11bbb7..13f87be7 100644 --- a/webrtc-kmp/build.gradle.kts +++ b/webrtc-kmp/build.gradle.kts @@ -1,4 +1,8 @@ +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl +import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig plugins { id("webrtc.multiplatform") @@ -28,6 +32,10 @@ kotlin { androidTarget { publishAllLibraryVariants() + @OptIn(ExperimentalKotlinGradlePluginApi::class) + instrumentedTestVariant { + sourceSetTree.set(KotlinSourceSetTree.test) + } } iosX64 { configureWebRtcCinterops() } @@ -39,6 +47,31 @@ kotlin { browser() } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + binaries.executable() + browser { + commonWebpackConfig { + devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply { + static = (static ?: mutableListOf()).apply { + // Serve sources to debug inside browser + add(project.rootDir.path) + } + } + } + } + } + + @OptIn(ExperimentalKotlinGradlePluginApi::class) + applyDefaultHierarchyTemplate { + common { + group("jsAndWasmJs") { + withJs() + withWasm() + } + } + } + sourceSets { commonMain.dependencies { implementation(libs.kotlin.coroutines) @@ -58,6 +91,7 @@ kotlin { commonTest.dependencies { implementation(kotlin("test")) implementation(kotlin("test-annotations-common")) + implementation(libs.kotlin.coroutines.test) } } } @@ -66,6 +100,7 @@ android { namespace = "com.shepeliev.webrtckmp" defaultConfig { + targetSdk = libs.versions.targetSdk.get().toInt() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/webrtc-kmp/src/androidMain/kotlin/com/shepeliev/webrtckmp/IceServer.kt b/webrtc-kmp/src/androidMain/kotlin/com/shepeliev/webrtckmp/IceServer.kt index c928c6de..8c0452f9 100644 --- a/webrtc-kmp/src/androidMain/kotlin/com/shepeliev/webrtckmp/IceServer.kt +++ b/webrtc-kmp/src/androidMain/kotlin/com/shepeliev/webrtckmp/IceServer.kt @@ -2,28 +2,14 @@ package com.shepeliev.webrtckmp import org.webrtc.PeerConnection -actual class IceServer internal constructor(val native: PeerConnection.IceServer) { - actual constructor( - urls: List, - username: String, - password: String, - tlsCertPolicy: TlsCertPolicy, - hostname: String, - tlsAlpnProtocols: List?, - tlsEllipticCurves: List? - ) : this( - PeerConnection.IceServer.builder(urls) - .setUsername(username) - .setPassword(password) - .setTlsCertPolicy(tlsCertPolicy.asNative()) - .setHostname(hostname) - .setTlsAlpnProtocols(tlsAlpnProtocols) - .setTlsEllipticCurves(tlsEllipticCurves) - .createIceServer() - ) - - actual override fun toString(): String = native.toString() -} +internal fun IceServer.toPlatform() = PeerConnection.IceServer.builder(urls) + .setUsername(username) + .setPassword(password) + .setTlsCertPolicy(tlsCertPolicy.asNative()) + .setHostname(hostname) + .setTlsAlpnProtocols(tlsAlpnProtocols) + .setTlsEllipticCurves(tlsEllipticCurves) + .createIceServer() private fun TlsCertPolicy.asNative(): PeerConnection.TlsCertPolicy { return when (this) { diff --git a/webrtc-kmp/src/androidMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt b/webrtc-kmp/src/androidMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt index 8e30b5c7..94852731 100644 --- a/webrtc-kmp/src/androidMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt +++ b/webrtc-kmp/src/androidMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt @@ -13,6 +13,8 @@ import com.shepeliev.webrtckmp.PeerConnectionEvent.StandardizedIceConnectionChan import com.shepeliev.webrtckmp.PeerConnectionEvent.Track import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -37,9 +39,8 @@ import org.webrtc.RtpReceiver as AndroidRtpReceiver import org.webrtc.SessionDescription as AndroidSessionDescription actual class PeerConnection actual constructor(rtcConfiguration: RtcConfiguration) { - val android: AndroidPeerConnection = WebRtc.peerConnectionFactory.createPeerConnection( - rtcConfiguration.android, + rtcConfiguration.toPlatform(), AndroidPeerConnectionObserver() ) ?: error("Creating PeerConnection failed") @@ -50,7 +51,7 @@ actual class PeerConnection actual constructor(rtcConfiguration: RtcConfiguratio get() = android.remoteDescription?.asCommon() actual val signalingState: SignalingState - get() = android.signalingState().asCommon() + get() = if (closed) SignalingState.Closed else android.signalingState().asCommon() actual val iceConnectionState: IceConnectionState get() = android.iceConnectionState().asCommon() @@ -65,15 +66,16 @@ actual class PeerConnection actual constructor(rtcConfiguration: RtcConfiguratio MutableSharedFlow(extraBufferCapacity = FLOW_BUFFER_CAPACITY) internal actual val peerConnectionEvent: Flow = _peerConnectionEvent.asSharedFlow() - private val coroutineScope = CoroutineScope(Dispatchers.Main) + private val coroutineScope = MainScope() private val localTracks = mutableMapOf() private val remoteTracks = mutableMapOf() + private var closed = false actual fun createDataChannel( label: String, id: Int, ordered: Boolean, - maxRetransmitTimeMs: Int, + maxPacketLifeTimeMs: Int, maxRetransmits: Int, protocol: String, negotiated: Boolean @@ -81,7 +83,7 @@ actual class PeerConnection actual constructor(rtcConfiguration: RtcConfiguratio val init = AndroidDataChannel.Init().also { it.id = id it.ordered = ordered - it.maxRetransmitTimeMs = maxRetransmitTimeMs + it.maxRetransmitTimeMs = maxPacketLifeTimeMs it.maxRetransmits = maxRetransmits it.protocol = protocol it.negotiated = negotiated @@ -169,10 +171,10 @@ actual class PeerConnection actual constructor(rtcConfiguration: RtcConfiguratio } actual fun setConfiguration(configuration: RtcConfiguration): Boolean { - return android.setConfiguration(configuration.android) + return android.setConfiguration(configuration.toPlatform()) } - actual fun addIceCandidate(candidate: IceCandidate): Boolean { + actual suspend fun addIceCandidate(candidate: IceCandidate): Boolean { return android.addIceCandidate(candidate.native) } @@ -215,10 +217,15 @@ actual class PeerConnection actual constructor(rtcConfiguration: RtcConfiguratio } actual fun close() { + if (closed) return + closed = true remoteTracks.values.forEach(MediaStreamTrack::stop) remoteTracks.clear() android.dispose() - coroutineScope.cancel() + coroutineScope.launch { + _peerConnectionEvent.emit(SignalingStateChange(SignalingState.Closed)) + coroutineScope.cancel() + } } internal inner class AndroidPeerConnectionObserver : AndroidPeerConnection.Observer { diff --git a/webrtc-kmp/src/androidMain/kotlin/com/shepeliev/webrtckmp/RtcConfiguration.kt b/webrtc-kmp/src/androidMain/kotlin/com/shepeliev/webrtckmp/RtcConfiguration.kt index 02b4a1e4..e4109193 100644 --- a/webrtc-kmp/src/androidMain/kotlin/com/shepeliev/webrtckmp/RtcConfiguration.kt +++ b/webrtc-kmp/src/androidMain/kotlin/com/shepeliev/webrtckmp/RtcConfiguration.kt @@ -2,15 +2,8 @@ package com.shepeliev.webrtckmp import org.webrtc.PeerConnection -actual class RtcConfiguration actual constructor( - bundlePolicy: BundlePolicy, - certificates: List?, - iceCandidatePoolSize: Int, - iceServers: List, - iceTransportPolicy: IceTransportPolicy, - rtcpMuxPolicy: RtcpMuxPolicy, -) { - val android = PeerConnection.RTCConfiguration(iceServers.map { it.native }).also { +internal fun RtcConfiguration.toPlatform() = PeerConnection.RTCConfiguration(iceServers.map { it.toPlatform() }) + .also { it.bundlePolicy = bundlePolicy.asNative() it.certificate = certificates?.firstOrNull()?.native it.iceCandidatePoolSize = iceCandidatePoolSize @@ -18,7 +11,7 @@ actual class RtcConfiguration actual constructor( it.rtcpMuxPolicy = rtcpMuxPolicy.asNative() it.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN } -} + private fun RtcpMuxPolicy.asNative(): PeerConnection.RtcpMuxPolicy { return when (this) { diff --git a/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/IceServer.kt b/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/IceServer.kt index d1f393b9..2d495c5f 100644 --- a/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/IceServer.kt +++ b/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/IceServer.kt @@ -1,13 +1,11 @@ package com.shepeliev.webrtckmp -expect class IceServer( - urls: List, - username: String = "", - password: String = "", - tlsCertPolicy: TlsCertPolicy = TlsCertPolicy.TlsCertPolicySecure, - hostname: String = "", - tlsAlpnProtocols: List? = null, - tlsEllipticCurves: List? = null -) { - override fun toString(): String -} +data class IceServer( + val urls: List, + val username: String = "", + val password: String = "", + val tlsCertPolicy: TlsCertPolicy = TlsCertPolicy.TlsCertPolicySecure, + val hostname: String = "", + val tlsAlpnProtocols: List? = null, + val tlsEllipticCurves: List? = null +) diff --git a/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt b/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt index aa2a0da3..1587e709 100644 --- a/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt +++ b/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt @@ -1,5 +1,11 @@ package com.shepeliev.webrtckmp +val MediaStream.audioTracks: List + get() = tracks.mapNotNull { it as? AudioStreamTrack } + +val MediaStream.videoTracks: List + get() = tracks.mapNotNull { it as? VideoStreamTrack } + expect class MediaStream { val id: String val tracks: List @@ -9,9 +15,3 @@ expect class MediaStream { fun removeTrack(track: MediaStreamTrack) fun release() } - -val MediaStream.audioTracks: List - get() = tracks.mapNotNull { it as? AudioStreamTrack } - -val MediaStream.videoTracks: List - get() = tracks.mapNotNull { it as? VideoStreamTrack } diff --git a/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/MediaTrackConstraints.kt b/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/MediaTrackConstraints.kt index 02c4d9a6..4c203d32 100644 --- a/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/MediaTrackConstraints.kt +++ b/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/MediaTrackConstraints.kt @@ -35,7 +35,6 @@ sealed interface ValueOrConstrain { fun Boolean.asValueConstrain() = ValueOrConstrain.Value(this) fun Int.asValueConstrain() = ValueOrConstrain.Value(this) fun Double.asValueConstrain() = ValueOrConstrain.Value(this) - fun FacingMode.asValueConstrain() = ValueOrConstrain.Value(this) val ValueOrConstrain.value: T? @@ -56,6 +55,14 @@ val ValueOrConstrain.ideal: T? is ValueOrConstrain.Constrain -> ideal } +fun ValueOrConstrain.map(transform: (T) -> R): ValueOrConstrain = when (this) { + is ValueOrConstrain.Value -> ValueOrConstrain.Value(transform(value)) + is ValueOrConstrain.Constrain -> ValueOrConstrain.Constrain( + exact?.let(transform), + ideal?.let(transform) + ) +} + class MediaTrackConstraintsBuilder(internal var constraints: MediaTrackConstraints) { fun deviceId(id: String) { constraints = constraints.copy(deviceId = id) diff --git a/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt b/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt index 7d5afb06..6c1e9f1a 100644 --- a/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt +++ b/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt @@ -22,7 +22,7 @@ expect class PeerConnection(rtcConfiguration: RtcConfiguration = RtcConfiguratio label: String, id: Int = -1, ordered: Boolean = true, - maxRetransmitTimeMs: Int = -1, + maxPacketLifeTimeMs: Int = -1, maxRetransmits: Int = -1, protocol: String = "", negotiated: Boolean = false, @@ -34,7 +34,7 @@ expect class PeerConnection(rtcConfiguration: RtcConfiguration = RtcConfiguratio suspend fun setRemoteDescription(description: SessionDescription) fun setConfiguration(configuration: RtcConfiguration): Boolean - fun addIceCandidate(candidate: IceCandidate): Boolean + suspend fun addIceCandidate(candidate: IceCandidate): Boolean fun removeIceCandidates(candidates: List): Boolean /** diff --git a/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/RtcCertificatePem.kt b/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/RtcCertificatePem.kt index 8fa9052c..50c2360e 100644 --- a/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/RtcCertificatePem.kt +++ b/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/RtcCertificatePem.kt @@ -1,7 +1,9 @@ package com.shepeliev.webrtckmp expect class RtcCertificatePem { + @Deprecated("Will be removes in order to comply with JS/WASM") val privateKey: String + @Deprecated("Will be removes in order to comply with JS/WASM") val certificate: String companion object { diff --git a/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/RtcConfiguration.kt b/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/RtcConfiguration.kt index bf97335b..d238ab20 100644 --- a/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/RtcConfiguration.kt +++ b/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/RtcConfiguration.kt @@ -1,12 +1,12 @@ package com.shepeliev.webrtckmp -expect class RtcConfiguration( +data class RtcConfiguration( /** * Specifies how to handle negotiation of candidates when the remote peer is not compatible * with the SDP BUNDLE standard. This must be one of the values from the enum [BundlePolicy]. * If this value isn't included in the dictionary, "balanced" is assumed. */ - bundlePolicy: BundlePolicy = BundlePolicy.Balanced, + val bundlePolicy: BundlePolicy = BundlePolicy.Balanced, /** * A [List] of objects of type [RtcCertificatePem] which are used by the connection for @@ -16,7 +16,7 @@ expect class RtcConfiguration( * successfully connecting in some circumstances. @see Using certificates * for further information. */ - certificates: List? = null, + val certificates: List? = null, /** * An unsigned 16-bit integer value which specifies the size of the prefetched ICE candidate @@ -25,7 +25,7 @@ expect class RtcConfiguration( * start fetching ICE candidates before you start trying to connect, so that they're already * available for inspection when [PeerConnection.setLocalDescription] is called. */ - iceCandidatePoolSize: Int = 0, + val iceCandidatePoolSize: Int = 0, /** * A [List] of [IceServer] objects, each describing one server which may be used by the ICE @@ -33,7 +33,7 @@ expect class RtcConfiguration( * attempt will be made with no STUN or TURN server available, which limits the connection to * local peers. */ - iceServers: List = emptyList(), + val iceServers: List = emptyList(), /** * The current ICE transport policy; this must be one of the values from the [IceTransportPolicy] @@ -41,12 +41,12 @@ expect class RtcConfiguration( * allowing all candidates to be considered. A value of [IceTransportPolicy.Relay] limits the * candidates to those relayed through another server, such as a STUN or TURN server. */ - iceTransportPolicy: IceTransportPolicy = IceTransportPolicy.All, + val iceTransportPolicy: IceTransportPolicy = IceTransportPolicy.All, /** * The RTCP mux policy to use when gathering ICE candidates, in order to support non-multiplexed * RTCP. The value must be one of those from the [RtcpMuxPolicy] enum. The default is * [RtcpMuxPolicy.Require]. */ - rtcpMuxPolicy: RtcpMuxPolicy = RtcpMuxPolicy.Require, + val rtcpMuxPolicy: RtcpMuxPolicy = RtcpMuxPolicy.Require, ) diff --git a/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/SessionDescription.kt b/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/SessionDescription.kt index aeb0c97b..d71d7c8b 100644 --- a/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/SessionDescription.kt +++ b/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/SessionDescription.kt @@ -3,3 +3,5 @@ package com.shepeliev.webrtckmp data class SessionDescription(val type: SessionDescriptionType, val sdp: String) enum class SessionDescriptionType { Offer, Pranswer, Answer, Rollback } + +fun SessionDescriptionType.toCanonicalString(): String = name.lowercase() diff --git a/webrtc-kmp/src/commonTest/kotlin/com/shepeliev/webrtckmp/DataChannelTest.kt b/webrtc-kmp/src/commonTest/kotlin/com/shepeliev/webrtckmp/DataChannelTest.kt index 458aacc5..675a595d 100644 --- a/webrtc-kmp/src/commonTest/kotlin/com/shepeliev/webrtckmp/DataChannelTest.kt +++ b/webrtc-kmp/src/commonTest/kotlin/com/shepeliev/webrtckmp/DataChannelTest.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.runTest import kotlinx.coroutines.withTimeout import kotlin.test.Ignore import kotlin.test.Test diff --git a/webrtc-kmp/src/commonTest/kotlin/com/shepeliev/webrtckmp/IceServerTest.kt b/webrtc-kmp/src/commonTest/kotlin/com/shepeliev/webrtckmp/IceServerTest.kt deleted file mode 100644 index edd47ce8..00000000 --- a/webrtc-kmp/src/commonTest/kotlin/com/shepeliev/webrtckmp/IceServerTest.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.shepeliev.webrtckmp - -import kotlin.test.Test - -class IceServerTest { - - @Test - fun should_build_successfully() { - IceServer( - urls = listOf("stun:url.to.stun.com"), - username = "username", - password = "password", - tlsCertPolicy = TlsCertPolicy.TlsCertPolicySecure, - ) - } -} diff --git a/webrtc-kmp/src/commonTest/kotlin/com/shepeliev/webrtckmp/PeerConnectionTest.kt b/webrtc-kmp/src/commonTest/kotlin/com/shepeliev/webrtckmp/PeerConnectionTest.kt index dedf6640..7f13b013 100644 --- a/webrtc-kmp/src/commonTest/kotlin/com/shepeliev/webrtckmp/PeerConnectionTest.kt +++ b/webrtc-kmp/src/commonTest/kotlin/com/shepeliev/webrtckmp/PeerConnectionTest.kt @@ -1,77 +1,323 @@ package com.shepeliev.webrtckmp +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull +import kotlin.test.assertNull import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds +@OptIn(ExperimentalCoroutinesApi::class) class PeerConnectionTest { + private val scope = TestScope() - @Test - fun should_be_created_successfully() { - assertNotNull(PeerConnection()) + @BeforeTest + fun setUp() { + Dispatchers.setMain(StandardTestDispatcher(scope.testScheduler)) + } + + @AfterTest + fun tearDown() { + Dispatchers.resetMain() } @Test - fun should_be_closed_successfully() { - val peerConnection = PeerConnection() - peerConnection.close() + fun testClose() { + val pc = PeerConnection() + pc.close() + + assertEquals(SignalingState.Closed, pc.signalingState, "Unexpected signaling state after close") } @Test - fun should_create_data_channel() { - val peerConnection = PeerConnection() - val dataChannel = peerConnection.createDataChannel("test") + fun testCreateDataChannel() { + val pc = PeerConnection() + val dataChannel1 = pc.createDataChannel("test1") + val dataChannel2 = pc.createDataChannel("test2", id = 1) + val dataChannel3 = pc.createDataChannel("test3", id = 1, maxRetransmits = 10) + val dataChannel4 = pc.createDataChannel("test4", id = 1, maxPacketLifeTimeMs = 1000) + val dataChannel6 = pc.createDataChannel("test5", maxRetransmits = 10) + val dataChannel5 = pc.createDataChannel("test6", maxPacketLifeTimeMs = 1000) + + assertNotNull(dataChannel1) + assertNotNull(dataChannel2) + assertNotNull(dataChannel3) + assertNotNull(dataChannel4) + assertNotNull(dataChannel5) + assertNotNull(dataChannel6) - assertNotNull(dataChannel) + pc.close() } @Test - fun should_create_offer() = runTest { - val peerConnection = PeerConnection() - val offer = peerConnection.createOffer(DefaultOfferAnswerOptions) + fun testCreateOffer() = scope.runTest { + val pc = PeerConnection() + val offer = pc.createOffer(DefaultOfferAnswerOptions) assertEquals(SessionDescriptionType.Offer, offer.type) assertTrue(offer.sdp.isNotEmpty()) + + pc.close() } @Test - fun should_create_answer() = runTest { - val peerConnection1 = PeerConnection() - val peerConnection2 = PeerConnection() - val offer = peerConnection1.createOffer(DefaultOfferAnswerOptions) + fun testCreateAnswer() = scope.runTest { + val pc1 = PeerConnection() + val pc2 = PeerConnection() + val offer = pc1.createOffer(DefaultOfferAnswerOptions) - peerConnection2.setRemoteDescription(offer) - val answer = peerConnection2.createAnswer(DefaultOfferAnswerOptions) + pc2.setRemoteDescription(offer) + val answer = pc2.createAnswer(DefaultOfferAnswerOptions) assertEquals(SessionDescriptionType.Answer, answer.type) assertTrue(answer.sdp.isNotEmpty()) + + pc1.close() + pc2.close() } @Test - fun should_handle_signaling_state_properly() = runTest { - val peerConnection1 = PeerConnection() - val peerConnection2 = PeerConnection() + fun testSetLocalDescription() = scope.runTest { + val pc = PeerConnection() + val offer = pc.createOffer(DefaultOfferAnswerOptions) + + pc.setLocalDescription(offer) - assertEquals(SignalingState.Stable, peerConnection1.signalingState, "Unexpected initial signaling state") + assertNotNull(pc.localDescription) - val offer = peerConnection1.createOffer(DefaultOfferAnswerOptions) - peerConnection1.setLocalDescription(offer) + pc.close() + } + + @Test + fun testSetLocalDescriptionRollback() = scope.runTest { + val pc = PeerConnection() + val offer = pc.createOffer(DefaultOfferAnswerOptions) + pc.setLocalDescription(offer) + + pc.setLocalDescription(SessionDescription(SessionDescriptionType.Rollback, "")) + + assertNull(pc.localDescription) + + pc.close() + } + + @Test + fun testSetRemoteDescription() = scope.runTest { + val pc1 = PeerConnection() + val pc2 = PeerConnection() + val offer = pc1.createOffer(DefaultOfferAnswerOptions) + + pc1.setLocalDescription(offer) + pc2.setRemoteDescription(offer) + + assertEquals(offer, pc2.remoteDescription) + + pc1.close() + pc2.close() + } + + @Test + fun testSetRemoteDescriptionRollback() = scope.runTest { + val pc1 = PeerConnection() + val pc2 = PeerConnection() + val offer = pc1.createOffer(DefaultOfferAnswerOptions) + + pc1.setLocalDescription(offer) + pc2.setRemoteDescription(offer) + + pc2.setRemoteDescription(SessionDescription(SessionDescriptionType.Rollback, "")) + + assertNull(pc2.remoteDescription) + + pc1.close() + pc2.close() + } + + @Test + fun testSignalingStates() = scope.runTest { + val pc1 = PeerConnection() + val pc2 = PeerConnection() + + assertEquals(SignalingState.Stable, pc1.signalingState, "Unexpected initial signaling state") + + val offer = pc1.createOffer(DefaultOfferAnswerOptions) + pc1.setLocalDescription(offer) assertEquals( SignalingState.HaveLocalOffer, - peerConnection1.signalingState, + pc1.signalingState, "Unexpected signaling state after offer creation" ) - peerConnection2.setRemoteDescription(offer) - val answer = peerConnection2.createAnswer(DefaultOfferAnswerOptions) + pc2.setRemoteDescription(offer) + val answer = pc2.createAnswer(DefaultOfferAnswerOptions) - peerConnection1.setRemoteDescription(answer) + pc1.setRemoteDescription(answer) assertEquals( SignalingState.Stable, - peerConnection1.signalingState, + pc1.signalingState, "Unexpected signaling state after answer creation" ) + + pc1.close() + pc2.close() + } + + @Test + fun testPeerConnectionEvents() = scope.runTest(timeout = 5.seconds) { + val pc1 = PeerConnection() + val pc2 = PeerConnection() + + val iceConnectionStateEmitted = async(start = CoroutineStart.UNDISPATCHED) { + pc1.onIceConnectionStateChange.first { it == IceConnectionState.Checking } + true + } + + val connectionStateChangeEmitted = async(start = CoroutineStart.UNDISPATCHED) { + pc1.onConnectionStateChange.first { it == PeerConnectionState.Connecting } + true + } + + val signalingStateChangeEmitted = async(start = CoroutineStart.UNDISPATCHED) { + pc1.onSignalingStateChange.first { it == SignalingState.HaveLocalOffer } + true + } + + val pc1IceCandidates = async(start = CoroutineStart.UNDISPATCHED) { + val candidates = mutableListOf() + val job = pc1.onIceCandidate.onEach { candidates += it }.launchIn(this) + pc1.onIceGatheringState.first { it == IceGatheringState.Complete } + job.cancel() + candidates + } + + val pc2IceCandidates = async(start = CoroutineStart.UNDISPATCHED) { + val candidates = mutableListOf() + val job = pc2.onIceCandidate.onEach { candidates += it }.launchIn(this) + pc2.onIceGatheringState.first { it == IceGatheringState.Complete } + job.cancel() + candidates + } + + val offer = pc1.createOffer(DefaultOfferAnswerOptions) + pc1.setLocalDescription(offer) + pc2.setRemoteDescription(offer) + val answer = pc2.createAnswer(DefaultOfferAnswerOptions) + pc2.setLocalDescription(answer) + pc1.setRemoteDescription(answer) + + pc1IceCandidates.await().forEach { pc2.addIceCandidate(it) } + pc2IceCandidates.await().forEach { pc1.addIceCandidate(it) } + + assertTrue(iceConnectionStateEmitted.await()) + assertTrue(connectionStateChangeEmitted.await()) + assertTrue(signalingStateChangeEmitted.await()) + + pc1.close() + pc2.close() + } + + @Test + fun testAddIceCandidate() = scope.runTest { + val pc1 = PeerConnection() + val pc2 = PeerConnection() + + val pc1IceCandidate = async { pc1.onIceCandidate.first() } + val offer = pc1.createOffer(DefaultOfferAnswerOptions) + pc1.setLocalDescription(offer) + pc2.setRemoteDescription(offer) + val answer = pc2.createAnswer(DefaultOfferAnswerOptions) + pc2.setLocalDescription(answer) + pc1.setRemoteDescription(answer) + + assertTrue(pc2.addIceCandidate(pc1IceCandidate.await())) + + pc1.close() + pc2.close() + } + + @Test + fun testGetReceivers() = scope.runTest { + val pc = PeerConnection() + val offer = pc.createOffer(DefaultOfferAnswerOptions) + pc.setLocalDescription(offer) + + assertTrue(pc.getReceivers().isNotEmpty()) + + pc.close() + } + + @Test + fun testGetSenders() = scope.runTest { + val pc = PeerConnection() + val offer = pc.createOffer(DefaultOfferAnswerOptions) + pc.setLocalDescription(offer) + + assertTrue(pc.getSenders().isNotEmpty()) + + pc.close() + } + + @Test + fun testGetTransceivers() = scope.runTest { + val pc = PeerConnection() + val offer = pc.createOffer(DefaultOfferAnswerOptions) + pc.setLocalDescription(offer) + + assertTrue(pc.getTransceivers().isNotEmpty()) + + pc.close() + } + + @Test + @Ignore + fun testAddTrack() = scope.runTest { + val pc = PeerConnection() + val offer = pc.createOffer(DefaultOfferAnswerOptions) + pc.setLocalDescription(offer) + + assertTrue(pc.getSenders().isNotEmpty()) + + pc.close() + } + + @Test + @Ignore + fun testRemoveTrack() = scope.runTest { + val pc = PeerConnection() + val offer = pc.createOffer(DefaultOfferAnswerOptions) + pc.setLocalDescription(offer) + + assertTrue(pc.getSenders().isNotEmpty()) + + pc.close() + } + + @Test + @Ignore + fun testGetStats() = scope.runTest { + val pc = PeerConnection() + val offer = pc.createOffer(DefaultOfferAnswerOptions) + pc.setLocalDescription(offer) + + val stats = pc.getStats() + assertNotNull(stats) + + pc.close() } } diff --git a/webrtc-kmp/src/commonTest/kotlin/com/shepeliev/webrtckmp/RtcCertificateTest.kt b/webrtc-kmp/src/commonTest/kotlin/com/shepeliev/webrtckmp/RtcCertificateTest.kt new file mode 100644 index 00000000..cb0ad121 --- /dev/null +++ b/webrtc-kmp/src/commonTest/kotlin/com/shepeliev/webrtckmp/RtcCertificateTest.kt @@ -0,0 +1,11 @@ +package com.shepeliev.webrtckmp + +import kotlinx.coroutines.test.runTest +import kotlin.test.Test + +class RtcCertificateTest { + @Test + fun testGenerateCertificate() = runTest { + RtcCertificatePem.generateCertificate() + } +} diff --git a/webrtc-kmp/src/commonTest/kotlin/com/shepeliev/webrtckmp/TestUtils.kt b/webrtc-kmp/src/commonTest/kotlin/com/shepeliev/webrtckmp/TestUtils.kt deleted file mode 100644 index 9f1a4317..00000000 --- a/webrtc-kmp/src/commonTest/kotlin/com/shepeliev/webrtckmp/TestUtils.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.shepeliev.webrtckmp - -import kotlinx.coroutines.CoroutineScope - -expect fun runTest(timeout: Long = 30000, block: suspend CoroutineScope.() -> Unit) diff --git a/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/IceServer.kt b/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/IceServer.kt deleted file mode 100644 index 3f10731a..00000000 --- a/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/IceServer.kt +++ /dev/null @@ -1,41 +0,0 @@ -@file:OptIn(ExperimentalForeignApi::class) - -package com.shepeliev.webrtckmp - -import WebRTC.RTCIceServer -import WebRTC.RTCTlsCertPolicy -import kotlinx.cinterop.ExperimentalForeignApi - -actual class IceServer internal constructor(val native: RTCIceServer) { - actual constructor( - urls: List, - username: String, - password: String, - tlsCertPolicy: TlsCertPolicy, - hostname: String, - tlsAlpnProtocols: List?, - tlsEllipticCurves: List? - ) : this( - RTCIceServer( - uRLStrings = urls, - username = username, - credential = password, - tlsCertPolicy = tlsCertPolicy.asNative(), - hostname = hostname, - tlsAlpnProtocols = tlsAlpnProtocols, - tlsEllipticCurves = tlsEllipticCurves - ) - ) - - actual override fun toString(): String = native.toString() -} - -private fun TlsCertPolicy.asNative(): RTCTlsCertPolicy { - return when (this) { - TlsCertPolicy.TlsCertPolicySecure -> RTCTlsCertPolicy.RTCTlsCertPolicySecure - - TlsCertPolicy.TlsCertPolicyInsecureNoCheck -> { - RTCTlsCertPolicy.RTCTlsCertPolicyInsecureNoCheck - } - } -} diff --git a/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt b/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt index 1186dc4b..ada2066a 100644 --- a/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt +++ b/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt @@ -33,9 +33,9 @@ import com.shepeliev.webrtckmp.PeerConnectionEvent.RemovedIceCandidates import com.shepeliev.webrtckmp.PeerConnectionEvent.SignalingStateChange import com.shepeliev.webrtckmp.PeerConnectionEvent.StandardizedIceConnectionChange import com.shepeliev.webrtckmp.PeerConnectionEvent.Track +import com.shepeliev.webrtckmp.internal.toPlatform import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -49,7 +49,7 @@ actual class PeerConnection actual constructor( val ios: RTCPeerConnection = checkNotNull( WebRtc.peerConnectionFactory.peerConnectionWithConfiguration( - configuration = rtcConfiguration.native, + configuration = rtcConfiguration.toPlatform(), constraints = RTCMediaConstraints(), delegate = this ) @@ -75,7 +75,7 @@ actual class PeerConnection actual constructor( MutableSharedFlow(extraBufferCapacity = FLOW_BUFFER_CAPACITY) internal actual val peerConnectionEvent: Flow = _peerConnectionEvent.asSharedFlow() - private val coroutineScope = CoroutineScope(Dispatchers.Main) + private val coroutineScope = MainScope() private val localTracks = mutableMapOf() private val remoteTracks = mutableMapOf() @@ -83,7 +83,7 @@ actual class PeerConnection actual constructor( label: String, id: Int, ordered: Boolean, - maxRetransmitTimeMs: Int, + maxPacketLifeTimeMs: Int, maxRetransmits: Int, protocol: String, negotiated: Boolean @@ -91,7 +91,7 @@ actual class PeerConnection actual constructor( val config = RTCDataChannelConfiguration().also { it.channelId = id it.isOrdered = ordered - it.maxRetransmitTimeMs = maxRetransmitTimeMs.toLong() + it.maxRetransmitTimeMs = maxPacketLifeTimeMs.toLong() it.maxRetransmits = maxRetransmits it.protocol = protocol it.isNegotiated = negotiated @@ -134,10 +134,10 @@ actual class PeerConnection actual constructor( } actual fun setConfiguration(configuration: RtcConfiguration): Boolean { - return ios.setConfiguration(configuration.native) + return ios.setConfiguration(configuration.toPlatform()) } - actual fun addIceCandidate(candidate: IceCandidate): Boolean { + actual suspend fun addIceCandidate(candidate: IceCandidate): Boolean { ios.addIceCandidate(candidate.native) return true } @@ -187,7 +187,10 @@ actual class PeerConnection actual constructor( remoteTracks.values.forEach(MediaStreamTrack::stop) remoteTracks.clear() ios.close() - coroutineScope.cancel() + coroutineScope.launch { + _peerConnectionEvent.emit(PeerConnectionEvent.SignalingStateChange(SignalingState.Closed)) + coroutineScope.cancel() + } } override fun peerConnection(peerConnection: RTCPeerConnection, didChangeSignalingState: RTCSignalingState) { diff --git a/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/internal/IceServer.kt b/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/internal/IceServer.kt new file mode 100644 index 00000000..07750d7e --- /dev/null +++ b/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/internal/IceServer.kt @@ -0,0 +1,31 @@ +@file:OptIn(ExperimentalForeignApi::class) + +package com.shepeliev.webrtckmp.internal + +import WebRTC.RTCIceServer +import WebRTC.RTCTlsCertPolicy +import com.shepeliev.webrtckmp.IceServer +import com.shepeliev.webrtckmp.TlsCertPolicy +import kotlinx.cinterop.ExperimentalForeignApi + +internal fun IceServer.toPlatform(): RTCIceServer { + return RTCIceServer( + uRLStrings = urls, + username = username, + credential = password, + tlsCertPolicy = tlsCertPolicy.toPlatform(), + hostname = hostname, + tlsAlpnProtocols = tlsAlpnProtocols, + tlsEllipticCurves = tlsEllipticCurves + ) +} + +private fun TlsCertPolicy.toPlatform(): RTCTlsCertPolicy { + return when (this) { + TlsCertPolicy.TlsCertPolicySecure -> RTCTlsCertPolicy.RTCTlsCertPolicySecure + + TlsCertPolicy.TlsCertPolicyInsecureNoCheck -> { + RTCTlsCertPolicy.RTCTlsCertPolicyInsecureNoCheck + } + } +} diff --git a/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/RtcConfiguration.kt b/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/internal/RtcConfiguration.kt similarity index 61% rename from webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/RtcConfiguration.kt rename to webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/internal/RtcConfiguration.kt index f75fb6be..7c985d9b 100644 --- a/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/RtcConfiguration.kt +++ b/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/internal/RtcConfiguration.kt @@ -1,41 +1,39 @@ @file:OptIn(ExperimentalForeignApi::class) -package com.shepeliev.webrtckmp +package com.shepeliev.webrtckmp.internal import WebRTC.RTCBundlePolicy import WebRTC.RTCConfiguration import WebRTC.RTCIceTransportPolicy import WebRTC.RTCRtcpMuxPolicy import WebRTC.RTCSdpSemantics +import com.shepeliev.webrtckmp.BundlePolicy +import com.shepeliev.webrtckmp.IceServer +import com.shepeliev.webrtckmp.IceTransportPolicy +import com.shepeliev.webrtckmp.RtcConfiguration +import com.shepeliev.webrtckmp.RtcpMuxPolicy import kotlinx.cinterop.ExperimentalForeignApi -actual class RtcConfiguration actual constructor( - bundlePolicy: BundlePolicy, - certificates: List?, - iceCandidatePoolSize: Int, - iceServers: List, - iceTransportPolicy: IceTransportPolicy, - rtcpMuxPolicy: RtcpMuxPolicy, -) { - val native: RTCConfiguration = RTCConfiguration().also { - it.bundlePolicy = bundlePolicy.asNative() +internal fun RtcConfiguration.toPlatform(): RTCConfiguration { + return RTCConfiguration().also { + it.bundlePolicy = bundlePolicy.toPlatform() it.certificate = certificates?.firstOrNull()?.native it.iceCandidatePoolSize = iceCandidatePoolSize - it.iceServers = iceServers.map(IceServer::native) - it.rtcpMuxPolicy = rtcpMuxPolicy.asNative() - it.iceTransportPolicy = iceTransportPolicy.asNative() + it.iceServers = iceServers.map(IceServer::toPlatform) + it.rtcpMuxPolicy = rtcpMuxPolicy.toPlatform() + it.iceTransportPolicy = iceTransportPolicy.toPlatform() it.sdpSemantics = RTCSdpSemantics.RTCSdpSemanticsUnifiedPlan } } -private fun RtcpMuxPolicy.asNative(): RTCRtcpMuxPolicy { +private fun RtcpMuxPolicy.toPlatform(): RTCRtcpMuxPolicy { return when (this) { RtcpMuxPolicy.Negotiate -> RTCRtcpMuxPolicy.RTCRtcpMuxPolicyNegotiate RtcpMuxPolicy.Require -> RTCRtcpMuxPolicy.RTCRtcpMuxPolicyRequire } } -private fun BundlePolicy.asNative(): RTCBundlePolicy { +private fun BundlePolicy.toPlatform(): RTCBundlePolicy { return when (this) { BundlePolicy.Balanced -> RTCBundlePolicy.RTCBundlePolicyBalanced BundlePolicy.MaxBundle -> RTCBundlePolicy.RTCBundlePolicyMaxBundle @@ -43,7 +41,7 @@ private fun BundlePolicy.asNative(): RTCBundlePolicy { } } -private fun IceTransportPolicy.asNative(): RTCIceTransportPolicy { +private fun IceTransportPolicy.toPlatform(): RTCIceTransportPolicy { return when (this) { IceTransportPolicy.None -> RTCIceTransportPolicy.RTCIceTransportPolicyNone IceTransportPolicy.Relay -> RTCIceTransportPolicy.RTCIceTransportPolicyRelay diff --git a/webrtc-kmp/src/iosTest/kotlin/com/shepeliev/webrtckmp/TestUtils.kt b/webrtc-kmp/src/iosTest/kotlin/com/shepeliev/webrtckmp/TestUtils.kt deleted file mode 100644 index 4b97df38..00000000 --- a/webrtc-kmp/src/iosTest/kotlin/com/shepeliev/webrtckmp/TestUtils.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.shepeliev.webrtckmp - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.async -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withTimeout -import kotlinx.coroutines.yield -import platform.Foundation.NSDate -import platform.Foundation.NSDefaultRunLoopMode -import platform.Foundation.NSLog -import platform.Foundation.NSRunLoop -import platform.Foundation.create -import platform.Foundation.runMode - -actual inline fun runTest( - timeout: Long, - crossinline block: suspend CoroutineScope.() -> Unit -) { - val exception = runBlocking { - val testRun = MainScope().async { - runCatching { withTimeout(timeout) { block() } }.exceptionOrNull() - } - while (testRun.isActive) { - NSRunLoop.mainRunLoop.runMode( - NSDefaultRunLoopMode, - beforeDate = NSDate.create(timeInterval = 1.0, sinceDate = NSDate()) - ) - yield() - } - testRun.await() - } - - exception?.also { - NSLog("$it") - it.printStackTrace() - throw it - } -} diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/DataChannel.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/DataChannel.kt similarity index 78% rename from webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/DataChannel.kt rename to webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/DataChannel.kt index 3ebde299..6e5f412b 100644 --- a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/DataChannel.kt +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/DataChannel.kt @@ -1,11 +1,14 @@ package com.shepeliev.webrtckmp +import com.shepeliev.webrtckmp.externals.RTCDataChannel +import com.shepeliev.webrtckmp.externals.data +import com.shepeliev.webrtckmp.externals.send +import com.shepeliev.webrtckmp.internal.Console import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow -import org.khronos.webgl.Int8Array -actual class DataChannel internal constructor(val js: RTCDataChannel) { +actual class DataChannel internal constructor(internal val js: RTCDataChannel) { actual val id: Int get() = js.id @@ -44,15 +47,14 @@ actual class DataChannel internal constructor(val js: RTCDataChannel) { js.onclosing = { _onClosing.tryEmit(Unit) } js.onclose = { _onClose.tryEmit(Unit) } js.onerror = { _onError.tryEmit(it.message) } - js.onmessage = { - _onMessage.tryEmit(Int8Array(it.data).unsafeCast()) - } + js.onmessage = { _onMessage.tryEmit(it.data) } } actual fun send(data: ByteArray): Boolean { - val conversion = data.unsafeCast() - js.send(conversion) - return true + return runCatching { js.send(data) } + .onFailure { Console.error("Failed to send data: $it") } + .map { true } + .getOrDefault(false) } actual fun close() { diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/DtmfSender.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/DtmfSender.kt similarity index 88% rename from webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/DtmfSender.kt rename to webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/DtmfSender.kt index 967686dd..16dadcc0 100644 --- a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/DtmfSender.kt +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/DtmfSender.kt @@ -1,5 +1,7 @@ package com.shepeliev.webrtckmp +import com.shepeliev.webrtckmp.externals.RTCDTMFSender + actual class DtmfSender(val js: RTCDTMFSender) { actual val canInsertDtmf: Boolean = true diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/IceCandidate.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/IceCandidate.kt similarity index 55% rename from webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/IceCandidate.kt rename to webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/IceCandidate.kt index b2ededfd..aa11211b 100644 --- a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/IceCandidate.kt +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/IceCandidate.kt @@ -1,21 +1,16 @@ package com.shepeliev.webrtckmp -import kotlin.js.json +import com.shepeliev.webrtckmp.externals.RTCIceCandidate +import com.shepeliev.webrtckmp.internal.jsonStringify actual class IceCandidate internal constructor(val js: RTCIceCandidate) { actual constructor(sdpMid: String, sdpMLineIndex: Int, candidate: String) : this( - RTCIceCandidate( - json( - "sdpMid" to sdpMid, - "sdpMLineIndex" to sdpMLineIndex, - "candidate" to candidate - ) - ) + RTCIceCandidate(candidate, sdpMid, sdpMLineIndex) ) actual val sdpMid: String = js.sdpMid actual val sdpMLineIndex: Int = js.sdpMLineIndex actual val candidate: String = js.candidate - actual override fun toString(): String = JSON.stringify(js) + actual override fun toString(): String = js.jsonStringify() } diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt new file mode 100644 index 00000000..56feda4f --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt @@ -0,0 +1,25 @@ +package com.shepeliev.webrtckmp + +import com.shepeliev.webrtckmp.externals.PlatformMediaStream +import com.shepeliev.webrtckmp.externals.getTracks + +actual class MediaStream internal constructor(internal val js: PlatformMediaStream) { + actual val id: String get() = js.id + actual val tracks: List get() = js.getTracks().map { MediaStreamTrackImpl(it) } + + actual fun addTrack(track: MediaStreamTrack) { + require(track is MediaStreamTrackImpl) + js.addTrack(track.platform) + } + + actual fun getTrackById(id: String): MediaStreamTrack? = js.getTrackById(id)?.let { MediaStreamTrackImpl(it) } + + actual fun removeTrack(track: MediaStreamTrack) { + require(track is MediaStreamTrackImpl) + js.removeTrack(track.platform) + } + + actual fun release() { + tracks.forEach(MediaStreamTrack::stop) + } +} diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/MediaStreamTrackImpl.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/MediaStreamTrackImpl.kt new file mode 100644 index 00000000..902f1a07 --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/MediaStreamTrackImpl.kt @@ -0,0 +1,48 @@ +package com.shepeliev.webrtckmp + +import com.shepeliev.webrtckmp.externals.PlatformMediaStreamTrack +import com.shepeliev.webrtckmp.externals.asCommon +import com.shepeliev.webrtckmp.externals.getConstraints +import com.shepeliev.webrtckmp.externals.toMediaStreamTrackState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +internal open class MediaStreamTrackImpl(val platform: PlatformMediaStreamTrack) : MediaStreamTrack { + override val id: String get() = platform.id + override val kind: MediaStreamTrackKind get() = platform.kind.toMediaStreamTrackKind() + override val label: String get() = platform.label + + override var enabled: Boolean + get() = platform.enabled + set(value) { + platform.enabled = value + } + + private val _state = MutableStateFlow(platform.readyState.toMediaStreamTrackState(platform.muted)) + override val state: StateFlow = _state.asStateFlow() + + override val constraints: MediaTrackConstraints get() = platform.getConstraints() + + override val settings: MediaTrackSettings + get() = platform.getSettings().asCommon() + + init { + platform.onended = { _state.update { MediaStreamTrackState.Ended(platform.muted) } } + platform.onmute = { _state.update { it.mute() } } + platform.onunmute = { _state.update { it.unmute() } } + } + + override fun stop() { + platform.stop() + } + + private fun String.toMediaStreamTrackKind(): MediaStreamTrackKind { + return when (this) { + "audio" -> MediaStreamTrackKind.Audio + "video" -> MediaStreamTrackKind.Video + else -> error("Unknown media stream track kind: $this") + } + } +} diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt new file mode 100644 index 00000000..a53634bc --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt @@ -0,0 +1,280 @@ +package com.shepeliev.webrtckmp + +import com.shepeliev.webrtckmp.externals.RTCPeerConnection +import com.shepeliev.webrtckmp.externals.addIceCandidate +import com.shepeliev.webrtckmp.externals.createAnswer +import com.shepeliev.webrtckmp.externals.createDataChannel +import com.shepeliev.webrtckmp.externals.createOffer +import com.shepeliev.webrtckmp.externals.getReceivers +import com.shepeliev.webrtckmp.externals.getSenders +import com.shepeliev.webrtckmp.externals.getStats +import com.shepeliev.webrtckmp.externals.getTransceivers +import com.shepeliev.webrtckmp.externals.setLocalDescription +import com.shepeliev.webrtckmp.externals.setRemoteDescription +import com.shepeliev.webrtckmp.externals.streams +import com.shepeliev.webrtckmp.externals.toSessionDescription +import com.shepeliev.webrtckmp.internal.Console +import com.shepeliev.webrtckmp.internal.toPlatform +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch + +actual class PeerConnection actual constructor(rtcConfiguration: RtcConfiguration) { + actual val localDescription: SessionDescription? get() = platform.localDescription?.toSessionDescription() + actual val remoteDescription: SessionDescription? get() = platform.remoteDescription?.toSessionDescription() + actual val signalingState: SignalingState get() = platform.signalingState.toSignalingState() + actual val iceConnectionState: IceConnectionState get() = platform.iceConnectionState.toIceConnectionState() + actual val connectionState: PeerConnectionState get() = platform.connectionState.toPeerConnectionState() + actual val iceGatheringState: IceGatheringState get() = platform.iceGatheringState.toIceGatheringState() + + private val platform: RTCPeerConnection + + private val _peerConnectionEvent = + MutableSharedFlow(extraBufferCapacity = FLOW_BUFFER_CAPACITY) + internal actual val peerConnectionEvent: Flow = _peerConnectionEvent.asSharedFlow() + + private val scope = MainScope() + + init { + platform = RTCPeerConnection(rtcConfiguration).apply { + onsignalingstatechange = { + scope.launch { + val event = PeerConnectionEvent.SignalingStateChange(this@PeerConnection.signalingState) + _peerConnectionEvent.emit(event) + } + } + oniceconnectionstatechange = { + scope.launch { + val event = PeerConnectionEvent.IceConnectionStateChange(this@PeerConnection.iceConnectionState) + _peerConnectionEvent.emit(event) + } + } + onconnectionstatechange = { + scope.launch { + val event = PeerConnectionEvent.ConnectionStateChange(this@PeerConnection.connectionState) + _peerConnectionEvent.emit(event) + } + } + onicegatheringstatechange = { + scope.launch { + val event = PeerConnectionEvent.IceGatheringStateChange(this@PeerConnection.iceGatheringState) + _peerConnectionEvent.tryEmit(event) + } + } + onicecandidate = { iceEvent -> + scope.launch { + val event = iceEvent.candidate?.let { PeerConnectionEvent.NewIceCandidate(IceCandidate(it)) } + event?.let { _peerConnectionEvent.emit(it) } + } + } + ondatachannel = { dataChannelEvent -> + scope.launch { + val event = PeerConnectionEvent.NewDataChannel(DataChannel(dataChannelEvent.channel)) + _peerConnectionEvent.emit(event) + } + } + onnegotiationneeded = { + scope.launch { + _peerConnectionEvent.emit(PeerConnectionEvent.NegotiationNeeded) + } + } + ontrack = { rtcTrackEvent -> + scope.launch { + val trackEvent = TrackEvent( + receiver = RtpReceiver(rtcTrackEvent.receiver), + streams = rtcTrackEvent.streams.map { MediaStream(it) }, + track = MediaStreamTrackImpl(rtcTrackEvent.track), + transceiver = RtpTransceiver(rtcTrackEvent.transceiver) + ) + _peerConnectionEvent.emit(PeerConnectionEvent.Track(trackEvent)) + } + } + } + } + + actual fun createDataChannel( + label: String, + id: Int, + ordered: Boolean, + maxPacketLifeTimeMs: Int, + maxRetransmits: Int, + protocol: String, + negotiated: Boolean + ): DataChannel? { + return platform.createDataChannel( + label, + id, + ordered, + maxPacketLifeTimeMs, + maxRetransmits, + protocol, + negotiated + )?.let { return DataChannel(it) } + } + + actual suspend fun createOffer(options: OfferAnswerOptions): SessionDescription { + return platform.createOffer(options).toSessionDescription() + } + + actual suspend fun createAnswer(options: OfferAnswerOptions): SessionDescription { + return platform.createAnswer(options).toSessionDescription() + } + + actual suspend fun setLocalDescription(description: SessionDescription) { + platform.setLocalDescription(description) + } + + actual suspend fun setRemoteDescription(description: SessionDescription) { + platform.setRemoteDescription(description) + } + + actual fun setConfiguration(configuration: RtcConfiguration): Boolean { + return runCatching { platform.setConfiguration(configuration.toPlatform()) } + .onFailure { Console.error("Set RTCConfiguration failed: $it") } + .map { true } + .getOrDefault(false) + } + + actual suspend fun addIceCandidate(candidate: IceCandidate): Boolean { + return runCatching { platform.addIceCandidate(candidate) } + .onFailure { Console.error("Add ICE candidate failed: $it") } + .map { true } + .getOrDefault(false) + } + + actual fun removeIceCandidates(candidates: List): Boolean { + Console.warn("removeIceCandidates is not supported in JS") + return true + } + + /** + * Gets all RtpSenders associated with this peer connection. + * Note that calling getSenders will dispose of the senders previously + * returned. + */ + actual fun getSenders(): List { + return platform.getSenders().map { RtpSender(it) } + } + + /** + * Gets all RtpReceivers associated with this peer connection. + * Note that calling getReceivers will dispose of the receivers previously + * returned. + */ + actual fun getReceivers(): List { + return platform.getReceivers().map { RtpReceiver(it) } + } + + /** + * Gets all RtpTransceivers associated with this peer connection. + * Note that calling getTransceivers will dispose of the transceivers previously + * returned. + * Note: This is only available with SdpSemantics.UNIFIED_PLAN specified. + */ + actual fun getTransceivers(): List { + return platform.getTransceivers().map { RtpTransceiver(it) } + } + + /** + * Adds a new media stream track to be sent on this peer connection, and returns + * the newly created RtpSender. If streamIds are specified, the RtpSender will + * be associated with the streams specified in the streamIds list. + * + * @throws IllegalStateException if an error accors in C++ addTrack. + * An error can occur if: + * - A sender already exists for the track. + * - The peer connection is closed. + */ + actual fun addTrack(track: MediaStreamTrack, vararg streams: MediaStream): RtpSender { + require(track is MediaStreamTrackImpl) + val platformSender = platform.addTrack(track.platform, *Array(streams.size) { streams[it].js }) + return RtpSender(platformSender) + } + + /** + * Stops sending media from sender. The sender will still appear in getSenders. Future + * calls to createOffer will mark the m section for the corresponding transceiver as + * receive only or inactive, as defined in JSEP. Returns true on success. + */ + actual fun removeTrack(sender: RtpSender): Boolean { + return runCatching { platform.removeTrack(sender.js) } + .onFailure { Console.error("Remove track failed: $it") } + .map { true } + .getOrDefault(false) + } + + /** + * Gets stats using the new stats collection API, see webrtc/api/stats/. + */ + actual suspend fun getStats(): RtcStatsReport? { + return runCatching { platform.getStats() } + .map { RtcStatsReport() } + .onFailure { Console.error("Get stats failed: $it") } + .getOrNull() + } + + /** + * Free native resources associated with this PeerConnection instance. + * + * This method removes a reference count from the C++ PeerConnection object, + * which should result in it being destroyed. It also calls equivalent + * "dispose" methods on the Java objects attached to this PeerConnection + * (streams, senders, receivers), such that their associated C++ objects + * will also be destroyed. + * + * + * Note that this method cannot be safely called from an observer callback + * (PeerConnection.Observer, DataChannel.Observer, etc.). If you want to, for + * example, destroy the PeerConnection after an "ICE failed" callback, you + * must do this asynchronously (in other words, unwind the stack first). See + * [bug 3721](https://bugs.chromium.org/p/webrtc/issues/detail?id=3721) for more details. + */ + actual fun close() { + platform.close() + scope.launch { + _peerConnectionEvent.emit(PeerConnectionEvent.SignalingStateChange(SignalingState.Closed)) + scope.cancel() + } + } + + private fun String.toSignalingState(): SignalingState = when (this) { + "stable" -> SignalingState.Stable + "have-local-offer" -> SignalingState.HaveLocalOffer + "have-remote-offer" -> SignalingState.HaveRemoteOffer + "have-local-pranswer" -> SignalingState.HaveLocalPranswer + "have-remote-pranswer" -> SignalingState.HaveRemotePranswer + "closed" -> SignalingState.Closed + else -> throw IllegalArgumentException("Illegal signaling state: $this") + } + + private fun String.toIceConnectionState(): IceConnectionState = when (this) { + "new" -> IceConnectionState.New + "checking" -> IceConnectionState.Checking + "connected" -> IceConnectionState.Connected + "completed" -> IceConnectionState.Completed + "failed" -> IceConnectionState.Failed + "disconnected" -> IceConnectionState.Disconnected + "closed" -> IceConnectionState.Closed + else -> throw IllegalArgumentException("Illegal ICE connection state: $this") + } + + private fun String.toPeerConnectionState(): PeerConnectionState = when (this) { + "new" -> PeerConnectionState.New + "connecting" -> PeerConnectionState.Connecting + "connected" -> PeerConnectionState.Connected + "disconnected" -> PeerConnectionState.Disconnected + "failed" -> PeerConnectionState.Failed + "closed" -> PeerConnectionState.Closed + else -> throw IllegalArgumentException("Illegal connection state: $this") + } + + private fun String.toIceGatheringState(): IceGatheringState = when (this) { + "new" -> IceGatheringState.New + "gathering" -> IceGatheringState.Gathering + "complete" -> IceGatheringState.Complete + else -> throw IllegalArgumentException("Illegal ICE gathering state: $this") + } +} diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/RtcCertificatePem.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/RtcCertificatePem.kt new file mode 100644 index 00000000..161aba38 --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/RtcCertificatePem.kt @@ -0,0 +1,18 @@ +package com.shepeliev.webrtckmp + +import com.shepeliev.webrtckmp.externals.RTCCertificate +import com.shepeliev.webrtckmp.externals.generateRTCCertificate + +actual class RtcCertificatePem internal constructor(val js: RTCCertificate) { + actual val privateKey: String + get() = "" + + actual val certificate: String + get() = "" + + actual companion object { + actual suspend fun generateCertificate(keyType: KeyType, expires: Long): RtcCertificatePem { + return RtcCertificatePem(generateRTCCertificate(keyType, expires)) + } + } +} diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/RtcStats.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/RtcStats.kt similarity index 100% rename from webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/RtcStats.kt rename to webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/RtcStats.kt diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/RtcStatsReport.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/RtcStatsReport.kt similarity index 100% rename from webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/RtcStatsReport.kt rename to webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/RtcStatsReport.kt diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/RtpParameters.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/RtpParameters.kt new file mode 100644 index 00000000..12107b44 --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/RtpParameters.kt @@ -0,0 +1,58 @@ +package com.shepeliev.webrtckmp + +import com.shepeliev.webrtckmp.externals.RTCRtcpParameters +import com.shepeliev.webrtckmp.externals.RTCRtpCodecParameters +import com.shepeliev.webrtckmp.externals.RTCRtpParameters +import com.shepeliev.webrtckmp.externals.codes + +actual class RtpParameters(val platform: RTCRtpParameters) { + actual val codecs: List get() = platform.codes.map { RtpCodecParameters(it) } + actual val encodings: List get() = emptyList() // TODO + actual val headerExtension: List get() = emptyList() // TODO + actual val rtcp: RtcpParameters get() = RtcpParameters(platform.rtcp) + actual val transactionId: String get() = "" // TODO +} + +actual class RtpCodecParameters(val platform: RTCRtpCodecParameters) { + actual val payloadType: Int + get() = platform.payloadType ?: 0 + + actual val mimeType: String? + get() = platform.mimeType + + actual val clockRate: Int? + get() = platform.clockRate + + actual val numChannels: Int? + get() = platform.channels + + actual val parameters: Map + get() = mapOf("sdpFmtpLine" to "${platform.sdpFmtpLine}") // TODO +} + +actual class RtpEncodingParameters { + actual val rid: String? = null + actual val active: Boolean = false + actual val bitratePriority: Double = 0.0 + actual val networkPriority: Int = -1 + actual val maxBitrateBps: Int? = null + actual val minBitrateBps: Int? = null + actual val maxFramerate: Int? = null + actual val numTemporalLayers: Int? = null + actual val scaleResolutionDownBy: Double? = null + actual val ssrc: Long? = null +} + +actual class HeaderExtension { + actual val uri: String = "" + actual val id: Int = -1 + actual val encrypted: Boolean = false +} + +actual class RtcpParameters(val platform: RTCRtcpParameters) { + actual val cname: String + get() = platform.cname + + actual val reducedSize: Boolean + get() = platform.reducedSize +} diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/RtpReceiver.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/RtpReceiver.kt new file mode 100644 index 00000000..dfdf5019 --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/RtpReceiver.kt @@ -0,0 +1,9 @@ +package com.shepeliev.webrtckmp + +import com.shepeliev.webrtckmp.externals.RTCRtpReceiver + +actual class RtpReceiver(val platform: RTCRtpReceiver) { + actual val id: String get() = platform.track.id + actual val track: MediaStreamTrack? get() = MediaStreamTrackImpl(platform.track) + actual val parameters: RtpParameters get() = RtpParameters(platform.getParameters()) +} diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/RtpSender.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/RtpSender.kt similarity index 50% rename from webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/RtpSender.kt rename to webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/RtpSender.kt index e46f7dd2..2dc49b82 100644 --- a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/RtpSender.kt +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/RtpSender.kt @@ -1,22 +1,23 @@ package com.shepeliev.webrtckmp -import kotlinx.coroutines.await +import com.shepeliev.webrtckmp.externals.RTCRtpSender +import com.shepeliev.webrtckmp.externals.replaceTrack -actual class RtpSender(val js: RTCRtpSender) { +actual class RtpSender internal constructor(internal val js: RTCRtpSender) { actual val id: String get() = track?.id ?: "" actual val track: MediaStreamTrack? - get() = js.track?.asCommon() + get() = js.track?.let { MediaStreamTrackImpl(it) } actual var parameters: RtpParameters get() = RtpParameters(js.getParameters()) - set(value) = js.setParameters(value.js) + set(value) = js.setParameters(value.platform) actual val dtmf: DtmfSender? get() = js.dtmf?.let { DtmfSender(it) } actual suspend fun replaceTrack(track: MediaStreamTrack?) { - js.replaceTrack((track as? MediaStreamTrackImpl)?.js).await() + js.replaceTrack((track as? MediaStreamTrackImpl)?.platform) } } diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/RtpTransceiver.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/RtpTransceiver.kt similarity index 90% rename from webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/RtpTransceiver.kt rename to webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/RtpTransceiver.kt index 291b1e9a..1352b31f 100644 --- a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/RtpTransceiver.kt +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/RtpTransceiver.kt @@ -1,6 +1,8 @@ package com.shepeliev.webrtckmp -actual class RtpTransceiver(val js: RTCRtpTransceiver) { +import com.shepeliev.webrtckmp.externals.RTCRtpTransceiver + +actual class RtpTransceiver internal constructor(internal val js: RTCRtpTransceiver) { actual var direction: RtpTransceiverDirection get() = js.direction.toRtpTransceiverDirection() set(value) { diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/VideoStreamTrack.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/VideoStreamTrack.kt similarity index 58% rename from webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/VideoStreamTrack.kt rename to webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/VideoStreamTrack.kt index 6b3d364d..4359b627 100644 --- a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/VideoStreamTrack.kt +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/VideoStreamTrack.kt @@ -1,9 +1,8 @@ package com.shepeliev.webrtckmp -import org.w3c.dom.mediacapture.MediaStreamTrack as DomMediaStreamTrack - +import com.shepeliev.webrtckmp.externals.PlatformMediaStreamTrack actual interface VideoStreamTrack : MediaStreamTrack { - val js: DomMediaStreamTrack + val js: PlatformMediaStreamTrack actual suspend fun switchCamera(deviceId: String?) } diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/Date.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/Date.kt new file mode 100644 index 00000000..6dff5704 --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/Date.kt @@ -0,0 +1,3 @@ +package com.shepeliev.webrtckmp.externals + +external class Date diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/ErrorEvent.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/ErrorEvent.kt new file mode 100644 index 00000000..a110602c --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/ErrorEvent.kt @@ -0,0 +1,5 @@ +package com.shepeliev.webrtckmp.externals + +external class ErrorEvent { + val message: String +} diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStream.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStream.kt new file mode 100644 index 00000000..d5c574f1 --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStream.kt @@ -0,0 +1,11 @@ +package com.shepeliev.webrtckmp.externals + +@JsName("MediaStream") +internal external interface PlatformMediaStream { + val id: String + fun addTrack(track: PlatformMediaStreamTrack) + fun getTrackById(id: String): PlatformMediaStreamTrack? + fun removeTrack(track: PlatformMediaStreamTrack) +} + +internal expect fun PlatformMediaStream.getTracks(): List diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStreamTrack.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStreamTrack.kt new file mode 100644 index 00000000..5b3fc721 --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStreamTrack.kt @@ -0,0 +1,70 @@ +package com.shepeliev.webrtckmp.externals + +import com.shepeliev.webrtckmp.FacingMode +import com.shepeliev.webrtckmp.MediaStreamTrackState +import com.shepeliev.webrtckmp.MediaTrackConstraints +import com.shepeliev.webrtckmp.MediaTrackSettings + +@JsName("MediaStreamTrack") +external interface PlatformMediaStreamTrack { + val id: String + var contentHint: String + var enabled: Boolean + val kind: String + val label: String + val muted: Boolean + var onended: (() -> Unit)? + var onmute: (() -> Unit)? + var onunmute: (() -> Unit)? + val readyState: String + + fun getSettings(): PlatformMediaTrackSettings + fun stop() +} + +internal expect fun PlatformMediaStreamTrack.getConstraints(): MediaTrackConstraints + +@JsName("MediaTrackSettings") +external interface PlatformMediaTrackSettings { + var aspectRatio: Double? + var autoGainControl: Boolean? + var channelCount: Int? + var deviceId: String? + var displaySurface: String? + var echoCancellation: Boolean? + var facingMode: String? + var frameRate: Double? + var groupId: String? + var height: Int? + var noiseSuppression: Boolean? + var sampleRate: Int? + var sampleSize: Int? + var width: Int? +} + +internal fun PlatformMediaTrackSettings.asCommon() = MediaTrackSettings( + aspectRatio = aspectRatio, + autoGainControl = autoGainControl, + channelCount = channelCount, + deviceId = deviceId, + echoCancellation = echoCancellation, + facingMode = facingMode?.toFacingMode(), + frameRate = frameRate, + groupId = groupId, + height = height, + noiseSuppression = noiseSuppression, + sampleRate = sampleRate, + sampleSize = sampleSize, + width = width +) + +internal fun String.toFacingMode() = when (this) { + "user" -> FacingMode.User + else -> FacingMode.Environment +} + +internal fun String.toMediaStreamTrackState(muted: Boolean) = when (this) { + "live" -> MediaStreamTrackState.Live(muted) + "ended" -> MediaStreamTrackState.Ended(muted) + else -> error("Unknown media stream track state: $this") +} diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCCertificate.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCCertificate.kt new file mode 100644 index 00000000..5bed0e59 --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCCertificate.kt @@ -0,0 +1,9 @@ +package com.shepeliev.webrtckmp.externals + +import com.shepeliev.webrtckmp.KeyType + +external interface RTCCertificate { + val expires: Date +} + +internal expect suspend fun generateRTCCertificate(keyType: KeyType, expires: Long): RTCCertificate diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCDTMFSender.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCDTMFSender.kt new file mode 100644 index 00000000..3af26c7c --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCDTMFSender.kt @@ -0,0 +1,6 @@ +package com.shepeliev.webrtckmp.externals + +external interface RTCDTMFSender { + val toneBuffer: String + fun insertDTMF(tones: String, duration: Long, interToneGap: Long) +} diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCDataChannel.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCDataChannel.kt new file mode 100644 index 00000000..37f5fee2 --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCDataChannel.kt @@ -0,0 +1,22 @@ +package com.shepeliev.webrtckmp.externals + +internal external interface RTCDataChannel { + val id: Int + val label: String + val readyState: String + val bufferedAmount: Long + + var onopen: (() -> Unit)? + var onclose: (() -> Unit)? + var onclosing: (() -> Unit)? + var onerror: ((ErrorEvent) -> Unit)? + var onmessage: ((MessageEvent) -> Unit)? + + fun close() +} + +internal external interface RTCDataChannelOptions +internal external interface MessageEvent + +internal expect fun RTCDataChannel.send(data: ByteArray) +internal expect val MessageEvent.data: ByteArray diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCDataChannelEvent.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCDataChannelEvent.kt new file mode 100644 index 00000000..3f607e8d --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCDataChannelEvent.kt @@ -0,0 +1,5 @@ +package com.shepeliev.webrtckmp.externals + +internal external class RTCDataChannelEvent { + val channel: RTCDataChannel +} diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCIceCandidate.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCIceCandidate.kt new file mode 100644 index 00000000..c62e73d8 --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCIceCandidate.kt @@ -0,0 +1,9 @@ +package com.shepeliev.webrtckmp.externals + +external interface RTCIceCandidate { + val candidate: String + val sdpMid: String + val sdpMLineIndex: Int +} + +internal expect fun RTCIceCandidate(candidate: String, sdpMid: String, sdpMLineIndex: Int): RTCIceCandidate diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCPeerConnection.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCPeerConnection.kt new file mode 100644 index 00000000..5f89956c --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCPeerConnection.kt @@ -0,0 +1,49 @@ +package com.shepeliev.webrtckmp.externals + +import com.shepeliev.webrtckmp.IceCandidate +import com.shepeliev.webrtckmp.OfferAnswerOptions +import com.shepeliev.webrtckmp.RtcConfiguration +import com.shepeliev.webrtckmp.SessionDescription + +internal external interface RTCPeerConnection { + val localDescription: RTCSessionDescription? + val remoteDescription: RTCSessionDescription? + val signalingState: String + val iceConnectionState: String + val connectionState: String + val iceGatheringState: String + + var onsignalingstatechange: (() -> Unit)? + var oniceconnectionstatechange: (() -> Unit)? + var onconnectionstatechange: (() -> Unit)? + var onicegatheringstatechange: (() -> Unit)? + var onicecandidate: ((RTCPeerConnectionIceEvent) -> Unit)? + var ondatachannel: ((RTCDataChannelEvent) -> Unit)? + var onnegotiationneeded: (() -> Unit)? + var ontrack: ((RTCTrackEvent) -> Unit)? + + fun close() + fun addTrack(track: PlatformMediaStreamTrack, vararg streams: PlatformMediaStream): RTCRtpSender + fun setConfiguration(configuration: RTCPeerConnectionConfiguration) + fun removeTrack(sender: RTCRtpSender) +} + +internal expect fun RTCPeerConnection(configuration: RtcConfiguration): RTCPeerConnection +internal expect suspend fun RTCPeerConnection.createOffer(options: OfferAnswerOptions): RTCSessionDescription +internal expect suspend fun RTCPeerConnection.createAnswer(options: OfferAnswerOptions): RTCSessionDescription +internal expect suspend fun RTCPeerConnection.setLocalDescription(description: SessionDescription) +internal expect suspend fun RTCPeerConnection.setRemoteDescription(description: SessionDescription) +internal expect suspend fun RTCPeerConnection.addIceCandidate(candidate: IceCandidate) +internal expect fun RTCPeerConnection.getReceivers(): List +internal expect fun RTCPeerConnection.getSenders(): List +internal expect fun RTCPeerConnection.getTransceivers(): List +internal expect suspend fun RTCPeerConnection.getStats(): RTCStatsReport +internal expect fun RTCPeerConnection.createDataChannel( + label: String, + id: Int, + ordered: Boolean, + maxPacketLifeTimeMs: Int, + maxRetransmits: Int, + protocol: String, + negotiated: Boolean, +): RTCDataChannel? diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCPeerConnectionConfiguration.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCPeerConnectionConfiguration.kt new file mode 100644 index 00000000..f15959fd --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCPeerConnectionConfiguration.kt @@ -0,0 +1,3 @@ +package com.shepeliev.webrtckmp.externals + +external interface RTCPeerConnectionConfiguration diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCPeerConnectionIceEvent.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCPeerConnectionIceEvent.kt new file mode 100644 index 00000000..1fc35b70 --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCPeerConnectionIceEvent.kt @@ -0,0 +1,5 @@ +package com.shepeliev.webrtckmp.externals + +external class RTCPeerConnectionIceEvent { + val candidate: RTCIceCandidate? +} diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtcpParameters.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtcpParameters.kt new file mode 100644 index 00000000..26e86038 --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtcpParameters.kt @@ -0,0 +1,6 @@ +package com.shepeliev.webrtckmp.externals + +external interface RTCRtcpParameters { + val cname: String + val reducedSize: Boolean +} diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpCodecParameters.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpCodecParameters.kt new file mode 100644 index 00000000..8ac3c89d --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpCodecParameters.kt @@ -0,0 +1,9 @@ +package com.shepeliev.webrtckmp.externals + +external interface RTCRtpCodecParameters { + val payloadType: Int? + val mimeType: String? + val clockRate: Int? + val channels: Int? + val sdpFmtpLine: String? +} diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpParameters.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpParameters.kt new file mode 100644 index 00000000..ba850fe4 --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpParameters.kt @@ -0,0 +1,8 @@ +package com.shepeliev.webrtckmp.externals + +external interface RTCRtpParameters { + val rtcp: RTCRtcpParameters +} + +internal expect val RTCRtpParameters.codes: List +internal expect val RTCRtpParameters.headerExtensions: List diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpReceiver.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpReceiver.kt new file mode 100644 index 00000000..57f99879 --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpReceiver.kt @@ -0,0 +1,6 @@ +package com.shepeliev.webrtckmp.externals + +external interface RTCRtpReceiver { + val track: PlatformMediaStreamTrack + fun getParameters(): RTCRtpParameters +} diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpSender.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpSender.kt new file mode 100644 index 00000000..445d263a --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpSender.kt @@ -0,0 +1,11 @@ +package com.shepeliev.webrtckmp.externals + +internal external interface RTCRtpSender { + val dtmf: RTCDTMFSender? + val track: PlatformMediaStreamTrack? + + fun getParameters(): RTCRtpParameters + fun setParameters(parameters: RTCRtpParameters) +} + +internal expect suspend fun RTCRtpSender.replaceTrack(withTrack: PlatformMediaStreamTrack?) diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpTransceiver.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpTransceiver.kt new file mode 100644 index 00000000..7762818b --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpTransceiver.kt @@ -0,0 +1,12 @@ +package com.shepeliev.webrtckmp.externals + +internal external interface RTCRtpTransceiver { + val currentDirection: String? + var direction: String + val mid: String? + val receiver: RTCRtpReceiver + val sender: RTCRtpSender + val stopped: Boolean + + fun stop() +} diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCSessionDescription.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCSessionDescription.kt new file mode 100644 index 00000000..ca990a1b --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCSessionDescription.kt @@ -0,0 +1,17 @@ +package com.shepeliev.webrtckmp.externals + +import com.shepeliev.webrtckmp.SessionDescription +import com.shepeliev.webrtckmp.SessionDescriptionType + +external interface RTCSessionDescription { + val type: String + val sdp: String +} + +internal fun RTCSessionDescription.toSessionDescription(): SessionDescription { + val type = SessionDescriptionType.valueOf( + this.type.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } + ) + + return SessionDescription(type, sdp) +} diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCStatsReport.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCStatsReport.kt new file mode 100644 index 00000000..ea562d54 --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCStatsReport.kt @@ -0,0 +1,7 @@ +package com.shepeliev.webrtckmp.externals + +external interface RTCStatsReport { + val id: String + val timestamp: Double + val type: String +} diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCTrackEvent.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCTrackEvent.kt new file mode 100644 index 00000000..e0d6e088 --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCTrackEvent.kt @@ -0,0 +1,9 @@ +package com.shepeliev.webrtckmp.externals + +internal external interface RTCTrackEvent { + val receiver: RTCRtpReceiver + val track: PlatformMediaStreamTrack + val transceiver: RTCRtpTransceiver +} + +internal expect val RTCTrackEvent.streams: List diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/Console.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/Console.kt new file mode 100644 index 00000000..c0d8a61c --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/Console.kt @@ -0,0 +1,12 @@ +package com.shepeliev.webrtckmp.internal + +@JsName("console") +internal external object Console { + fun log(message: String) + fun warn(message: String) + fun error(message: String) +} + +internal fun Console.logln(message: String) = log("$message\n") +internal fun Console.warnln(message: String) = warn("$message\n") +internal fun Console.errorln(message: String) = error("$message\n") diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/JSON.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/JSON.kt new file mode 100644 index 00000000..dc9509bf --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/JSON.kt @@ -0,0 +1,3 @@ +package com.shepeliev.webrtckmp.internal + +internal expect fun Any.jsonStringify(): String diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/JsMediaTrackConstraintsExt.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/JsMediaTrackConstraintsExt.kt new file mode 100644 index 00000000..4cfa3a9e --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/JsMediaTrackConstraintsExt.kt @@ -0,0 +1,72 @@ +package com.shepeliev.webrtckmp.internal + +//internal fun ConstrainBooleanParameters?.asCommon(): ValueOrConstrain.Constrain? { +// this ?: return null +// return ValueOrConstrain.Constrain(exact, ideal) +//} +// +//internal fun ConstrainDOMStringParameters?.asCommon(): ValueOrConstrain.Constrain? { +// this ?: return null +// return ValueOrConstrain.Constrain(exact as? String, ideal as? String) +//} +// +//internal fun ConstrainDoubleRange?.asCommon(): ValueOrConstrain.Constrain? { +// this ?: return null +// return ValueOrConstrain.Constrain(exact, ideal) +//} +// +//internal fun ConstrainULongRange?.asCommon(): ValueOrConstrain.Constrain? { +// this ?: return null +// return ValueOrConstrain.Constrain(exact, ideal) +//} +// +//internal fun ConstrainDOMStringParameters?.asCommonFacingModeConstrain(): ValueOrConstrain.Constrain? { +// this ?: return null +// return ValueOrConstrain.Constrain( +// (exact as? String)?.asCommonFacingMode(), +// (ideal as? String)?.asCommonFacingMode() +// ) +//} + +//@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") +//internal fun JsMediaTrackConstraints.asCommon() = MediaTrackConstraints( +// aspectRatio = (aspectRatio as? Double)?.asValueConstrain() +// ?: (aspectRatio as? ConstrainDoubleRange)?.asCommon(), +// +// autoGainControl = (autoGainControl as? Boolean)?.asValueConstrain() +// ?: (autoGainControl as? ConstrainBooleanParameters)?.asCommon(), +// +// channelCount = (channelCount as? Number)?.toInt()?.asValueConstrain() +// ?: (channelCount as? ConstrainULongRange)?.asCommon(), +// +// deviceId = (deviceId as? String) ?: (deviceId.exact ?: deviceId.ideal) as? String, +// +// echoCancellation = (echoCancellation as? Boolean)?.asValueConstrain() +// ?: (echoCancellation as? ConstrainBooleanParameters)?.asCommon(), +// +// facingMode = (facingMode as? String)?.asCommonFacingMode()?.asValueConstrain() +// ?: (facingMode as? ConstrainDOMStringParameters).asCommonFacingModeConstrain(), +// +// frameRate = (frameRate as? Double)?.asValueConstrain() +// ?: (frameRate as? ConstrainDoubleRange)?.asCommon(), +// +// groupId = (groupId as? String) ?: (groupId.exact ?: groupId.ideal) as? String, +// +// height = (height as? Number)?.toInt()?.asValueConstrain() +// ?: (height as? ConstrainULongRange)?.asCommon(), +// +// latency = (latency as? Double)?.asValueConstrain() +// ?: (latency as? ConstrainDoubleRange)?.asCommon(), +// +// noiseSuppression = (noiseSuppression as? Boolean)?.asValueConstrain() +// ?: (noiseSuppression as? ConstrainBooleanParameters)?.asCommon(), +// +// sampleRate = (sampleRate as? Number)?.toInt()?.asValueConstrain() +// ?: (sampleRate as? ConstrainULongRange)?.asCommon(), +// +// sampleSize = (sampleSize as? Number)?.toInt()?.asValueConstrain() +// ?: (sampleSize as? ConstrainULongRange)?.asCommon(), +// +// width = (width as? Number)?.toInt()?.asValueConstrain() +// ?: (width as? ConstrainULongRange)?.asCommon(), +//) diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/PeerConnection.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/PeerConnection.kt new file mode 100644 index 00000000..d6ef810a --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/PeerConnection.kt @@ -0,0 +1,22 @@ +package com.shepeliev.webrtckmp.internal + +import com.shepeliev.webrtckmp.BundlePolicy +import com.shepeliev.webrtckmp.IceTransportPolicy +import com.shepeliev.webrtckmp.RtcpMuxPolicy + +internal fun BundlePolicy.toStringValue(): String = when (this) { + BundlePolicy.Balanced -> "balanced" + BundlePolicy.MaxBundle -> "max-bundle" + BundlePolicy.MaxCompat -> "max-compat" +} + +internal fun IceTransportPolicy.toStringValue(): String = when (this) { + IceTransportPolicy.All -> "all" + IceTransportPolicy.Relay -> "relay" + else -> "all" +} + +internal fun RtcpMuxPolicy.toStringValue(): String = when (this) { + RtcpMuxPolicy.Negotiate -> "negotiate" + RtcpMuxPolicy.Require -> "require" +} diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/RtcConfiguration.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/RtcConfiguration.kt new file mode 100644 index 00000000..9f2a17d5 --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/RtcConfiguration.kt @@ -0,0 +1,6 @@ +package com.shepeliev.webrtckmp.internal + +import com.shepeliev.webrtckmp.RtcConfiguration +import com.shepeliev.webrtckmp.externals.RTCPeerConnectionConfiguration + +internal expect fun RtcConfiguration.toPlatform(): RTCPeerConnectionConfiguration diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/AudioStreamTrackImpl.kt b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/AudioStreamTrackImpl.kt deleted file mode 100644 index 856159ac..00000000 --- a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/AudioStreamTrackImpl.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.shepeliev.webrtckmp - -import org.w3c.dom.mediacapture.MediaStreamTrack as JsMediaStreamTrack - -internal class AudioStreamTrackImpl( - js: JsMediaStreamTrack -) : MediaStreamTrackImpl(js), AudioStreamTrack diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/IceServer.kt b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/IceServer.kt deleted file mode 100644 index dba44273..00000000 --- a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/IceServer.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.shepeliev.webrtckmp - -import kotlin.js.Json -import kotlin.js.json - -actual class IceServer actual constructor( - urls: List, - username: String, - password: String, - tlsCertPolicy: TlsCertPolicy, - hostname: String, - tlsAlpnProtocols: List?, - tlsEllipticCurves: List? -) { - - val js: Json - - init { - js = json( - "urls" to urls.toTypedArray(), - "username" to username, - "credential" to password - ) - } - - actual override fun toString(): String = JSON.stringify(js) -} diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/JsMediaTrackSettiiingsExt.kt b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/JsMediaTrackSettiiingsExt.kt deleted file mode 100644 index d4ba195d..00000000 --- a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/JsMediaTrackSettiiingsExt.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.shepeliev.webrtckmp - -import org.w3c.dom.mediacapture.MediaTrackSettings as JsMediaTrackSettings - -internal fun JsMediaTrackSettings.asCommon(): MediaTrackSettings = MediaTrackSettings( - aspectRatio = aspectRatio, - autoGainControl = autoGainControl, - channelCount = channelCount, - deviceId = deviceId, - echoCancellation = echoCancellation, - facingMode = facingMode?.asCommonFacingMode(), - frameRate = frameRate, - groupId = groupId, - height = height, - latency = latency, - noiseSuppression = noiseSuppression, - sampleRate = sampleRate, - sampleSize = sampleSize, - width = width -) diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/MediaDevices.kt b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/MediaDevices.kt index 9a71adef..5ad1d80e 100644 --- a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/MediaDevices.kt +++ b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/MediaDevices.kt @@ -1,5 +1,6 @@ package com.shepeliev.webrtckmp +import com.shepeliev.webrtckmp.externals.PlatformMediaStream import kotlinx.browser.window import kotlinx.coroutines.await import org.w3c.dom.mediacapture.AUDIOINPUT @@ -11,6 +12,7 @@ import org.w3c.dom.mediacapture.MediaStreamConstraints as JsMediaStreamConstrain internal actual val mediaDevices: MediaDevices = MediaDevicesImpl +@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") private object MediaDevicesImpl : MediaDevices { override suspend fun getUserMedia(streamConstraints: MediaStreamConstraintsBuilder.() -> Unit): MediaStream { val constraints = MediaStreamConstraintsBuilder().let { @@ -18,7 +20,7 @@ private object MediaDevicesImpl : MediaDevices { it.constraints } val jsStream = window.navigator.mediaDevices.getUserMedia(constraints.toJson()).await() - return MediaStream(jsStream) + return MediaStream(jsStream as PlatformMediaStream) } override suspend fun getDisplayMedia(): MediaStream { @@ -27,7 +29,7 @@ private object MediaDevicesImpl : MediaDevices { } val jsStream = window.navigator.mediaDevices.getDisplayMedia().await() - return MediaStream(jsStream) + return MediaStream(jsStream as PlatformMediaStream) } override suspend fun supportsDisplayMedia(): Boolean = window.navigator.mediaDevices.supportsDisplayMedia() diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt deleted file mode 100644 index 41618ccc..00000000 --- a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.shepeliev.webrtckmp - -import org.w3c.dom.mediacapture.MediaStream as JsMediaStream - -actual class MediaStream internal constructor(val js: JsMediaStream) { - - actual val id: String - get() = js.id - - actual val tracks: List - get() = js.getTracks().map { it.asCommon() } - - actual fun addTrack(track: MediaStreamTrack) { - require(track is MediaStreamTrackImpl) - js.addTrack(track.js) - } - - actual fun getTrackById(id: String): MediaStreamTrack? = js.getTrackById(id)?.asCommon() - - actual fun removeTrack(track: MediaStreamTrack) { - require(track is MediaStreamTrackImpl) - js.removeTrack(track.js) - } - - actual fun release() { - tracks.forEach(MediaStreamTrack::stop) - } -} diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/MediaStreamTrackImpl.kt b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/MediaStreamTrackImpl.kt deleted file mode 100644 index 50582349..00000000 --- a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/MediaStreamTrackImpl.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.shepeliev.webrtckmp - -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import org.w3c.dom.mediacapture.ENDED -import org.w3c.dom.mediacapture.LIVE -import org.w3c.dom.mediacapture.MediaStreamTrack as JsMediaStreamTrack -import org.w3c.dom.mediacapture.MediaStreamTrackState as JsMediaStreamTrackState - -abstract class MediaStreamTrackImpl(val js: JsMediaStreamTrack) : MediaStreamTrack { - override val id: String - get() = js.id - - override val kind: MediaStreamTrackKind - get() = js.kind.toMediaStreamTrackKind() - - override val label: String - get() = js.label - - override var enabled: Boolean - get() = js.enabled - set(value) { - js.enabled = value - } - - private val _state = MutableStateFlow(getInitialState()) - override val state: StateFlow = _state.asStateFlow() - - override val constraints: MediaTrackConstraints - get() = js.getConstraints().asCommon() - - override val settings: MediaTrackSettings - get() = js.getSettings().asCommon() - - init { - js.onended = { _state.update { MediaStreamTrackState.Ended(js.muted) } } - js.onmute = { _state.update { it.mute() } } - js.onunmute = { _state.update { it.unmute() } } - } - - override fun stop() { - js.stop() - } - - private fun String.toMediaStreamTrackKind(): MediaStreamTrackKind { - return when (this) { - "audio" -> MediaStreamTrackKind.Audio - "video" -> MediaStreamTrackKind.Video - else -> error("Unknown media stream track kind: $this") - } - } - - private fun getInitialState(): MediaStreamTrackState { - return when (js.readyState) { - JsMediaStreamTrackState.LIVE -> MediaStreamTrackState.Live(js.muted) - JsMediaStreamTrackState.ENDED -> MediaStreamTrackState.Ended(js.muted) - else -> error("Unknown media stream track state: ${js.readyState}") - } - } -} - -internal fun JsMediaStreamTrack.asCommon(): MediaStreamTrackImpl = when (kind) { - "audio" -> AudioStreamTrackImpl(this) - "video" -> VideoStreamTrackImpl(this) - else -> error("Unknown kind of media stream track: $kind") -} diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt deleted file mode 100644 index b0064dd6..00000000 --- a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt +++ /dev/null @@ -1,209 +0,0 @@ -package com.shepeliev.webrtckmp - -import com.shepeliev.webrtckmp.PeerConnectionEvent.ConnectionStateChange -import com.shepeliev.webrtckmp.PeerConnectionEvent.IceConnectionStateChange -import com.shepeliev.webrtckmp.PeerConnectionEvent.IceGatheringStateChange -import com.shepeliev.webrtckmp.PeerConnectionEvent.NegotiationNeeded -import com.shepeliev.webrtckmp.PeerConnectionEvent.NewDataChannel -import com.shepeliev.webrtckmp.PeerConnectionEvent.NewIceCandidate -import com.shepeliev.webrtckmp.PeerConnectionEvent.SignalingStateChange -import com.shepeliev.webrtckmp.PeerConnectionEvent.Track -import kotlinx.coroutines.await -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlin.js.Json -import kotlin.js.json - -actual class PeerConnection actual constructor(rtcConfiguration: RtcConfiguration) { - - val js: RTCPeerConnection - - actual val localDescription: SessionDescription? get() = js.localDescription?.asCommon() - - actual val remoteDescription: SessionDescription? get() = js.remoteDescription?.asCommon() - - actual val signalingState: SignalingState - get() = js.signalingState.toSignalingState() - - actual val iceConnectionState: IceConnectionState - get() = js.iceConnectionState.toIceConnectionState() - - actual val connectionState: PeerConnectionState - get() = js.connectionState.toPeerConnectionState() - - actual val iceGatheringState: IceGatheringState - get() = js.iceGatheringState.toIceGatheringState() - - private val _peerConnectionEvent = - MutableSharedFlow(extraBufferCapacity = FLOW_BUFFER_CAPACITY) - internal actual val peerConnectionEvent: Flow = _peerConnectionEvent.asSharedFlow() - - init { - WebRtcAdapter - - js = RTCPeerConnection(rtcConfiguration.js).apply { - onsignalingstatechange = { - _peerConnectionEvent.tryEmit(SignalingStateChange(this@PeerConnection.signalingState)) - } - oniceconnectionstatechange = { - _peerConnectionEvent.tryEmit(IceConnectionStateChange(this@PeerConnection.iceConnectionState)) - } - onconnectionstatechange = { - _peerConnectionEvent.tryEmit(ConnectionStateChange(this@PeerConnection.connectionState)) - } - onicegatheringstatechange = { - _peerConnectionEvent.tryEmit(IceGatheringStateChange(this@PeerConnection.iceGatheringState)) - } - onicecandidate = { iceEvent -> - iceEvent.candidate?.let { _peerConnectionEvent.tryEmit(NewIceCandidate(IceCandidate(it))) } - } - ondatachannel = { dataChannelEvent -> - _peerConnectionEvent.tryEmit(NewDataChannel(DataChannel(dataChannelEvent.channel))) - } - onnegotiationneeded = { _peerConnectionEvent.tryEmit(NegotiationNeeded) } - ontrack = { rtcTrackEvent -> - val trackEvent = TrackEvent( - receiver = RtpReceiver(rtcTrackEvent.receiver), - streams = rtcTrackEvent.streams.map { MediaStream(it) }, - track = rtcTrackEvent.track.asCommon(), - transceiver = RtpTransceiver(rtcTrackEvent.transceiver) - ) - _peerConnectionEvent.tryEmit(Track(trackEvent)) - } - } - } - - actual fun createDataChannel( - label: String, - id: Int, - ordered: Boolean, - maxRetransmitTimeMs: Int, - maxRetransmits: Int, - protocol: String, - negotiated: Boolean, - ): DataChannel? { - val options = json().apply { - if (id > -1) add(json("id" to id)) - if (maxRetransmitTimeMs > -1) add(json("maxRetransmitTimeMs" to maxRetransmitTimeMs)) - if (maxRetransmits > -1) add(json("maxRetransmits" to maxRetransmits)) - if (protocol.isNotEmpty()) add(json("protocol" to protocol)) - add( - json( - "ordered" to ordered, - "negotiated" to negotiated - ) - ) - } - return js.createDataChannel(label, options)?.let { DataChannel(it) } - } - - actual suspend fun createOffer(options: OfferAnswerOptions): SessionDescription { - val sessionDescription = js.createOffer(options.toJson()).await() - return sessionDescription.asCommon() - } - - actual suspend fun createAnswer(options: OfferAnswerOptions): SessionDescription { - val sessionDescription = js.createAnswer(options.toJson()).await() - return sessionDescription.asCommon() - } - - actual suspend fun setLocalDescription(description: SessionDescription) { - js.setLocalDescription(description.asJs()).await() - } - - actual suspend fun setRemoteDescription(description: SessionDescription) { - js.setRemoteDescription(description.asJs()).await() - } - - actual fun setConfiguration(configuration: RtcConfiguration): Boolean { - js.setConfiguration(configuration.js) - return true - } - - actual fun addIceCandidate(candidate: IceCandidate): Boolean { - js.addIceCandidate(candidate.js) - return true - } - - actual fun removeIceCandidates(candidates: List): Boolean { - // not implemented for JS target - return true - } - - actual fun getSenders(): List = js.getSenders().map { RtpSender(it) } - - actual fun getReceivers(): List = js.getReceivers().map { RtpReceiver(it) } - - actual fun getTransceivers(): List { - return js.getTransceivers().map { RtpTransceiver(it) } - } - - actual fun addTrack(track: MediaStreamTrack, vararg streams: MediaStream): RtpSender { - require(track is MediaStreamTrackImpl) - - val jsStreams = streams.map { it.js }.toTypedArray() - return RtpSender(js.addTrack(track.js, *jsStreams)) - } - - actual fun removeTrack(sender: RtpSender): Boolean { - js.removeTrack(sender.js) - return true - } - - actual suspend fun getStats(): RtcStatsReport? { - // TODO implement - return null - } - - actual fun close() { - js.close() - } - - private fun OfferAnswerOptions.toJson(): Json { - return json().apply { - iceRestart?.also { add(json("iceRestart" to it)) } - offerToReceiveAudio?.also { add(json("offerToReceiveAudio" to it)) } - offerToReceiveVideo?.also { add(json("offerToReceiveVideo" to it)) } - voiceActivityDetection?.also { add(json("voiceActivityDetection" to it)) } - } - } - - private fun String.toSignalingState(): SignalingState = when (this) { - "stable" -> SignalingState.Stable - "have-local-offer" -> SignalingState.HaveLocalOffer - "have-remote-offer" -> SignalingState.HaveRemoteOffer - "have-local-pranswer" -> SignalingState.HaveLocalPranswer - "have-remote-pranswer" -> SignalingState.HaveRemotePranswer - "closed" -> SignalingState.Closed - else -> throw IllegalArgumentException("Illegal signaling state: $this") - } - - private fun String.toIceConnectionState(): IceConnectionState = when (this) { - "new" -> IceConnectionState.New - "checking" -> IceConnectionState.Checking - "connected" -> IceConnectionState.Connected - "completed" -> IceConnectionState.Completed - "failed" -> IceConnectionState.Failed - "disconnected" -> IceConnectionState.Disconnected - "closed" -> IceConnectionState.Closed - else -> throw IllegalArgumentException("Illegal ICE connection state: $this") - } - - private fun String.toPeerConnectionState(): PeerConnectionState = when (this) { - "new" -> PeerConnectionState.New - "connecting" -> PeerConnectionState.Connecting - "connected" -> PeerConnectionState.Connected - "disconnected" -> PeerConnectionState.Disconnected - "failed" -> PeerConnectionState.Failed - "closed" -> PeerConnectionState.Closed - else -> throw IllegalArgumentException("Illegal connection state: $this") - } - - private fun String.toIceGatheringState(): IceGatheringState = when (this) { - "new" -> IceGatheringState.New - "gathering" -> IceGatheringState.Gathering - "complete" -> IceGatheringState.Complete - else -> throw IllegalArgumentException("Illegal ICE gathering state: $this") - } -} diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/RtcCertificatePem.kt b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/RtcCertificatePem.kt deleted file mode 100644 index 3f9f031c..00000000 --- a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/RtcCertificatePem.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.shepeliev.webrtckmp - -import kotlinx.coroutines.await -import org.khronos.webgl.Uint8Array -import kotlin.js.json - -actual class RtcCertificatePem internal constructor(val js: RTCCertificate) { - actual val privateKey: String - get() = "" - - actual val certificate: String - get() = "" - - actual companion object { - actual suspend fun generateCertificate(keyType: KeyType, expires: Long): RtcCertificatePem { - val options = when (keyType) { - KeyType.RSA -> json( - "name" to "RSASSA-PKCS10-v1_5", - "modulusLength" to 2048, - "publicExponent" to Uint8Array(arrayOf(1, 0, 1)), - "hash" to "SHA-256", - ) - KeyType.ECDSA -> json( - "name" to "ECDSA", - "namedCurve" to "P-256", - ) - } - return RtcCertificatePem(RTCPeerConnection.generateCertificate(options).await()) - } - } -} diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/RtcConfiguration.kt b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/RtcConfiguration.kt deleted file mode 100644 index 14a02ab7..00000000 --- a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/RtcConfiguration.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.shepeliev.webrtckmp - -import kotlin.js.Json -import kotlin.js.json - -actual class RtcConfiguration actual constructor( - bundlePolicy: BundlePolicy, - certificates: List?, - iceCandidatePoolSize: Int, - iceServers: List, - iceTransportPolicy: IceTransportPolicy, - rtcpMuxPolicy: RtcpMuxPolicy, -) { - val js: Json - - init { - js = json( - "bundlePolicy" to bundlePolicy.toJs(), - "iceCandidatePoolSize" to iceCandidatePoolSize, - "iceServers" to iceServers.map { it.js }.toTypedArray(), - "iceTransportPolicy" to iceTransportPolicy.toJs(), - "rtcpMuxPolicy" to rtcpMuxPolicy.toJs(), - ) - - if (certificates != null) { - js.add(json("certificates" to certificates.map { it.js })) - } - } - - private fun BundlePolicy.toJs(): String = when (this) { - BundlePolicy.Balanced -> "balanced" - BundlePolicy.MaxBundle -> "max-bundle" - BundlePolicy.MaxCompat -> "max-compat" - } - - private fun IceTransportPolicy.toJs(): String = when (this) { - IceTransportPolicy.All -> "all" - IceTransportPolicy.Relay -> "relay" - else -> "all" - } - - private fun RtcpMuxPolicy.toJs(): String = when (this) { - RtcpMuxPolicy.Negotiate -> "negotiate" - RtcpMuxPolicy.Require -> "require" - } -} diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/RtpParameters.kt b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/RtpParameters.kt deleted file mode 100644 index 287e3f78..00000000 --- a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/RtpParameters.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.shepeliev.webrtckmp - -actual class RtpParameters(val js: RTCRtpParameters) { - actual val codecs: List - get() = js.codes.map { RtpCodecParameters(it) } - - actual val encodings: List - get() = emptyList() // TODO - - actual val headerExtension: List - get() = emptyList() // TODO - - actual val rtcp: RtcpParameters - get() = RtcpParameters(js.rtcp) - - actual val transactionId: String - get() = "" // TODO -} - -actual class RtpCodecParameters(val js: RTCRtpCodecParameters) { - actual val payloadType: Int - get() = js.payloadType ?: 0 - - actual val mimeType: String? - get() = js.mimeType - - actual val clockRate: Int? - get() = js.clockRate - - actual val numChannels: Int? - get() = js.channels - - actual val parameters: Map - get() = mapOf("sdpFmtpLine" to "${js.sdpFmtpLine}") // TODO -} - -actual class RtcpParameters(val js: RTCRtcpParameters) { - actual val cname: String - get() = js.cname - - actual val reducedSize: Boolean - get() = js.reducedSize -} - -actual class RtpEncodingParameters { - actual val rid: String? = null - actual val active: Boolean = false - actual val bitratePriority: Double = 0.0 - actual val networkPriority: Int = -1 - actual val maxBitrateBps: Int? = null - actual val minBitrateBps: Int? = null - actual val maxFramerate: Int? = null - actual val numTemporalLayers: Int? = null - actual val scaleResolutionDownBy: Double? = null - actual val ssrc: Long? = null -} - -actual class HeaderExtension { - actual val uri: String = "" - actual val id: Int = -1 - actual val encrypted: Boolean = false -} diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/RtpReceiver.kt b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/RtpReceiver.kt deleted file mode 100644 index 81fbca1a..00000000 --- a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/RtpReceiver.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.shepeliev.webrtckmp - -actual class RtpReceiver(val js: RTCRtpReceiver) { - actual val id: String - get() = js.track.id - - actual val track: MediaStreamTrack? - get() = js.track.asCommon() - - actual val parameters: RtpParameters - get() = RtpParameters(js.getParameters()) -} diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/SessionDescriptionExt.kt b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/SessionDescriptionExt.kt deleted file mode 100644 index 6dc63f9c..00000000 --- a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/SessionDescriptionExt.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.shepeliev.webrtckmp - -import kotlin.js.Json -import kotlin.js.json - -internal fun SessionDescription.asJs(): Json = json( - "type" to type.name.lowercase(), - "sdp" to sdp, -) - -internal fun RTCSessionDescription.asCommon(): SessionDescription { - val type = SessionDescriptionType.valueOf( - this.type.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } - ) - - return SessionDescription(type, sdp) -} diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/VideoStreamTrackImpl.kt b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/VideoStreamTrackImpl.kt deleted file mode 100644 index 7f70daff..00000000 --- a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/VideoStreamTrackImpl.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.shepeliev.webrtckmp - -import org.w3c.dom.mediacapture.MediaStreamTrack as JsMediaStreamTrack - -internal class VideoStreamTrackImpl( - js: JsMediaStreamTrack -) : MediaStreamTrackImpl(js), VideoStreamTrack { - override suspend fun switchCamera(deviceId: String?) { - console.warn("switchCamera is not supported in browser") - } -} diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals.kt b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals.kt deleted file mode 100644 index 03ad48c9..00000000 --- a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals.kt +++ /dev/null @@ -1,153 +0,0 @@ -package com.shepeliev.webrtckmp - -import org.khronos.webgl.ArrayBuffer -import org.w3c.dom.mediacapture.MediaStream -import org.w3c.dom.mediacapture.MediaStreamTrack -import kotlin.js.Date -import kotlin.js.Promise - -external class RTCPeerConnection(configuration: dynamic) { - val localDescription: RTCSessionDescription? - val remoteDescription: RTCSessionDescription? - val signalingState: String - val iceConnectionState: String - val connectionState: String - val iceGatheringState: String - - var onsignalingstatechange: (() -> Unit)? - var oniceconnectionstatechange: (() -> Unit)? - var onconnectionstatechange: (() -> Unit)? - var onicegatheringstatechange: (() -> Unit)? - var onicecandidate: ((RTCPeerConnectionIceEvent) -> Unit)? - var ondatachannel: ((RTCDataChannelEvent) -> Unit)? - var onnegotiationneeded: (() -> Unit)? - var ontrack: ((RTCTrackEvent) -> Unit)? - - fun addIceCandidate(candidate: RTCIceCandidate): Promise - fun addTrack(track: MediaStreamTrack, vararg streams: MediaStream): RTCRtpSender - fun close() - fun createAnswer(options: dynamic): Promise - fun createDataChannel(label: String, options: dynamic): RTCDataChannel? - fun createOffer(options: dynamic): Promise - fun getReceivers(): Array - fun getSenders(): Array - fun getStats(): Promise - fun getTransceivers(): Array - fun removeTrack(sender: RTCRtpSender) - fun setLocalDescription(sdp: dynamic): Promise - fun setRemoteDescription(sdp: dynamic): Promise - fun setConfiguration(configuration: dynamic) - - companion object { - fun generateCertificate(algorithm: dynamic): Promise - } -} - -external class RTCCertificate { - val expires: Date -} - -external class RTCStatsReport { - val id: String - val timestamp: Double - val type: String -} - -external class RTCSessionDescription { - val type: String - val sdp: String -} - -external class RTCPeerConnectionIceEvent { - val candidate: RTCIceCandidate? -} - -external class RTCIceCandidate(candidateInfo: dynamic) { - val candidate: String - val sdpMid: String - val sdpMLineIndex: Int -} - -external class RTCDataChannelEvent { - val channel: RTCDataChannel -} - -external class RTCDataChannel { - val id: Int - val label: String - val readyState: String - val bufferedAmount: Long - - var onopen: (() -> Unit)? - var onclose: (() -> Unit)? - var onclosing: (() -> Unit)? - var onerror: ((ErrorEvent) -> Unit)? - var onmessage: ((MessageEvent) -> Unit)? - - fun send(data: dynamic) - fun close() -} - -external class MessageEvent { - val data: ArrayBuffer -} - -external class ErrorEvent { - val message: String -} - -external class RTCTrackEvent { - val receiver: RTCRtpReceiver - val streams: Array - val track: MediaStreamTrack - val transceiver: RTCRtpTransceiver -} - -external class RTCRtpTransceiver { - val currentDirection: String? - var direction: String - val mid: String? - val receiver: RTCRtpReceiver - val sender: RTCRtpSender - val stopped: Boolean - - fun stop() -} - -external class RTCRtpSender { - val dtmf: RTCDTMFSender? - val track: MediaStreamTrack? - - fun getParameters(): RTCRtpParameters - fun setParameters(parameters: RTCRtpParameters) - fun replaceTrack(newTrack: MediaStreamTrack?): Promise -} - -external class RTCDTMFSender { - val toneBuffer: String - fun insertDTMF(tones: String, duration: Long, interToneGap: Long) -} - -external class RTCRtpReceiver { - val track: MediaStreamTrack - fun getParameters(): RTCRtpParameters -} - -external class RTCRtpParameters { - val codes: Array - val headerExtensions: Array - val rtcp: RTCRtcpParameters -} - -external class RTCRtpCodecParameters { - val payloadType: Int? - val mimeType: String? - val clockRate: Int? - val channels: Int? - val sdpFmtpLine: String? -} - -external class RTCRtcpParameters { - val cname: String - val reducedSize: Boolean -} diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStream.js.kt b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStream.js.kt new file mode 100644 index 00000000..8f13bcb5 --- /dev/null +++ b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStream.js.kt @@ -0,0 +1,8 @@ +package com.shepeliev.webrtckmp.externals + +import org.w3c.dom.mediacapture.MediaStream + +@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") +internal actual fun PlatformMediaStream.getTracks(): List { + return (this as MediaStream).getTracks().map { it as PlatformMediaStreamTrack }.toList() +} diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStreamTrack.js.kt b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStreamTrack.js.kt new file mode 100644 index 00000000..60136c0c --- /dev/null +++ b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStreamTrack.js.kt @@ -0,0 +1,9 @@ +package com.shepeliev.webrtckmp.externals + +import com.shepeliev.webrtckmp.MediaTrackConstraints +import com.shepeliev.webrtckmp.internal.asCommon +import org.w3c.dom.mediacapture.MediaStreamTrack + +internal actual fun PlatformMediaStreamTrack.getConstraints(): MediaTrackConstraints { + return (this as MediaStreamTrack).getConstraints().asCommon() +} diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCCertificate.js.kt b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCCertificate.js.kt new file mode 100644 index 00000000..e5b37152 --- /dev/null +++ b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCCertificate.js.kt @@ -0,0 +1,28 @@ +package com.shepeliev.webrtckmp.externals + +import com.shepeliev.webrtckmp.KeyType +import kotlinx.coroutines.await +import org.khronos.webgl.Uint8Array +import kotlin.js.Promise +import kotlin.js.json + +internal actual suspend fun generateRTCCertificate( + keyType: KeyType, + expires: Long +): RTCCertificate { + val options = when (keyType) { + KeyType.RSA -> json( + "name" to "RSASSA-PKCS10-v1_5", + "modulusLength" to 2048, + "publicExponent" to Uint8Array(arrayOf(1, 0, 1)), + "hash" to "SHA-256", + ) + + KeyType.ECDSA -> json( + "name" to "ECDSA", + "namedCurve" to "P-256", + ) + } + + return JsRTCPeerConnection.generateCertificate(options).await() +} diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCDataChannel.js.kt b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCDataChannel.js.kt new file mode 100644 index 00000000..f9bbbba9 --- /dev/null +++ b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCDataChannel.js.kt @@ -0,0 +1,23 @@ +package com.shepeliev.webrtckmp.externals + +import org.khronos.webgl.ArrayBuffer +import org.khronos.webgl.Int8Array + +@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") +internal actual val MessageEvent.data: ByteArray + get() = Int8Array((this as JsMessageEvent).data).unsafeCast() + +@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") +internal actual fun RTCDataChannel.send(data: ByteArray) { + (this as JsRTCDataChannel).send(data.unsafeCast()) +} + +@JsName("RTCDataChannel") +private external interface JsRTCDataChannel : RTCDataChannel { + fun send(data: Int8Array) +} + +@JsName("MessageEvent") +private external interface JsMessageEvent : MessageEvent { + val data: ArrayBuffer +} diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCIceCandidate.js.kt b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCIceCandidate.js.kt new file mode 100644 index 00000000..f24c061e --- /dev/null +++ b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCIceCandidate.js.kt @@ -0,0 +1,24 @@ +package com.shepeliev.webrtckmp.externals + +import kotlin.js.json + +internal actual fun RTCIceCandidate( + candidate: String, + sdpMid: String, + sdpMLineIndex: Int +): RTCIceCandidate { + return JsRTCIceCandidate( + json( + "sdpMid" to sdpMid, + "sdpMLineIndex" to sdpMLineIndex, + "candidate" to candidate + ) + ) +} + +@JsName("RTCIceCandidate") +private external class JsRTCIceCandidate(options: dynamic) : RTCIceCandidate { + override val candidate: String + override val sdpMid: String + override val sdpMLineIndex: Int +} diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCPeerConnection.js.kt b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCPeerConnection.js.kt new file mode 100644 index 00000000..db260354 --- /dev/null +++ b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCPeerConnection.js.kt @@ -0,0 +1,107 @@ +package com.shepeliev.webrtckmp.externals + +import com.shepeliev.webrtckmp.IceCandidate +import com.shepeliev.webrtckmp.OfferAnswerOptions +import com.shepeliev.webrtckmp.RtcConfiguration +import com.shepeliev.webrtckmp.SessionDescription +import com.shepeliev.webrtckmp.internal.toPlatform +import kotlinx.coroutines.await +import kotlin.js.Promise +import kotlin.js.json + +internal actual fun RTCPeerConnection(configuration: RtcConfiguration): RTCPeerConnection { + return JsRTCPeerConnection(configuration.toPlatform()) +} + +internal actual suspend fun RTCPeerConnection.createOffer(options: OfferAnswerOptions): RTCSessionDescription { + return (this as JsRTCPeerConnection).createOffer(options.toPlatform()).await() +} + +internal actual suspend fun RTCPeerConnection.createAnswer(options: OfferAnswerOptions): RTCSessionDescription { + return (this as JsRTCPeerConnection).createAnswer(options.toPlatform()).await() +} + +internal actual suspend fun RTCPeerConnection.setLocalDescription(description: SessionDescription) { + return (this as JsRTCPeerConnection).setLocalDescription(description.toPlatform()).await() +} + +internal actual suspend fun RTCPeerConnection.setRemoteDescription(description: SessionDescription) { + return (this as JsRTCPeerConnection).setRemoteDescription(description.toPlatform()).await() +} + +internal actual suspend fun RTCPeerConnection.addIceCandidate(candidate: IceCandidate) { + return (this as JsRTCPeerConnection).addIceCandidate(candidate.js).await() +} + +internal actual fun RTCPeerConnection.getReceivers(): List { + return (this as JsRTCPeerConnection).getReceivers().toList() +} + +internal actual fun RTCPeerConnection.getSenders(): List { + return (this as JsRTCPeerConnection).getSenders().toList() +} + +internal actual fun RTCPeerConnection.getTransceivers(): List { + return (this as JsRTCPeerConnection).getTransceivers().toList() +} + +internal actual suspend fun RTCPeerConnection.getStats(): RTCStatsReport { + return (this as JsRTCPeerConnection).getStats().await() +} + +internal actual fun RTCPeerConnection.createDataChannel( + label: String, + id: Int, + ordered: Boolean, + maxPacketLifeTimeMs: Int, + maxRetransmits: Int, + protocol: String, + negotiated: Boolean +): RTCDataChannel? { + val options = json().apply { + if (id > -1) add(json("id" to id)) + if (maxPacketLifeTimeMs > -1) add(json("maxRetransmitTimeMs" to maxPacketLifeTimeMs)) + if (maxRetransmits > -1) add(json("maxRetransmits" to maxRetransmits)) + if (protocol.isNotEmpty()) add(json("protocol" to protocol)) + add(json("ordered" to ordered, "negotiated" to negotiated)) + } + return (this as JsRTCPeerConnection).createDataChannel(label, options) +} + +@JsName("RTCPeerConnection") +internal external class JsRTCPeerConnection(configuration: dynamic) : RTCPeerConnection { + override val localDescription: RTCSessionDescription? + override val remoteDescription: RTCSessionDescription? + override val signalingState: String + override val iceConnectionState: String + override val connectionState: String + override val iceGatheringState: String + override var onsignalingstatechange: (() -> Unit)? + override var oniceconnectionstatechange: (() -> Unit)? + override var onconnectionstatechange: (() -> Unit)? + override var onicegatheringstatechange: (() -> Unit)? + override var onicecandidate: ((RTCPeerConnectionIceEvent) -> Unit)? + override var ondatachannel: ((RTCDataChannelEvent) -> Unit)? + override var onnegotiationneeded: (() -> Unit)? + override var ontrack: ((RTCTrackEvent) -> Unit)? + + override fun close() + override fun addTrack(track: PlatformMediaStreamTrack, vararg streams: PlatformMediaStream): RTCRtpSender + override fun setConfiguration(configuration: RTCPeerConnectionConfiguration) + override fun removeTrack(sender: RTCRtpSender) + + fun createDataChannel(label: String, options: dynamic): RTCDataChannel + fun createOffer(options: dynamic): Promise + fun createAnswer(options: dynamic): Promise + fun addIceCandidate(candidate: RTCIceCandidate): Promise + fun getReceivers(): Array + fun getSenders(): Array + fun getStats(): Promise + fun getTransceivers(): Array + fun setLocalDescription(sdp: dynamic): Promise + fun setRemoteDescription(sdp: dynamic): Promise + + companion object { + suspend fun generateCertificate(options: dynamic): Promise + } +} diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpParameters.js.kt b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpParameters.js.kt new file mode 100644 index 00000000..dcb14eec --- /dev/null +++ b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpParameters.js.kt @@ -0,0 +1,15 @@ +package com.shepeliev.webrtckmp.externals + +@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") +internal actual val RTCRtpParameters.codes: List + get() = (this as JsRTCRtpParameters).codecs.toList() + +@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") +internal actual val RTCRtpParameters.headerExtensions: List + get() = (this as JsRTCRtpParameters).headerExtensions.toList() + +@JsName("RTCRtpParameters") +private external interface JsRTCRtpParameters : RTCRtcpParameters { + val codecs: Array + val headerExtensions: Array +} diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpSender.js.kt b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpSender.js.kt new file mode 100644 index 00000000..928dbbd1 --- /dev/null +++ b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpSender.js.kt @@ -0,0 +1,14 @@ +package com.shepeliev.webrtckmp.externals + +import kotlinx.coroutines.await +import kotlin.js.Promise + +@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") +internal actual suspend fun RTCRtpSender.replaceTrack(withTrack: PlatformMediaStreamTrack?) { + (this as JsRTCRtpSender).replaceTrack(withTrack).await() +} + +@JsName("RTCRtpSender") +internal external interface JsRTCRtpSender : RTCRtpSender { + fun replaceTrack(newTrack: PlatformMediaStreamTrack?): Promise +} diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCTrackEvent.js.kt b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCTrackEvent.js.kt new file mode 100644 index 00000000..4ef591b2 --- /dev/null +++ b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCTrackEvent.js.kt @@ -0,0 +1,10 @@ +package com.shepeliev.webrtckmp.externals + +@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") +internal actual val RTCTrackEvent.streams: List + get() = (this as JsRTCTrackEvent).streams.toList() + +@JsName("RTCTrackEvent") +private external interface JsRTCTrackEvent : RTCTrackEvent { + val streams: Array +} diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/internal/IceServer.kt b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/internal/IceServer.kt new file mode 100644 index 00000000..8b3e0aca --- /dev/null +++ b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/internal/IceServer.kt @@ -0,0 +1,11 @@ +package com.shepeliev.webrtckmp.internal + +import com.shepeliev.webrtckmp.IceServer +import kotlin.js.Json +import kotlin.js.json + +internal fun IceServer.toPlatform(): Json = json( + "urls" to urls.toTypedArray(), + "username" to username, + "credential" to password +) diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/internal/JSON.js.kt b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/internal/JSON.js.kt new file mode 100644 index 00000000..a97d52ed --- /dev/null +++ b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/internal/JSON.js.kt @@ -0,0 +1,3 @@ +package com.shepeliev.webrtckmp.internal + +internal actual fun Any.jsonStringify(): String = JSON.stringify(this) diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/JsMediaTrackConstraintsExt.kt b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/internal/MediaTrackConstraintsSet.kt similarity index 87% rename from webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/JsMediaTrackConstraintsExt.kt rename to webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/internal/MediaTrackConstraintsSet.kt index 0fa4be86..e1af9265 100644 --- a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/JsMediaTrackConstraintsExt.kt +++ b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/internal/MediaTrackConstraintsSet.kt @@ -1,44 +1,16 @@ -package com.shepeliev.webrtckmp +package com.shepeliev.webrtckmp.internal +import com.shepeliev.webrtckmp.FacingMode +import com.shepeliev.webrtckmp.MediaTrackConstraints +import com.shepeliev.webrtckmp.ValueOrConstrain +import com.shepeliev.webrtckmp.asValueConstrain +import com.shepeliev.webrtckmp.externals.toFacingMode import org.w3c.dom.mediacapture.ConstrainBooleanParameters import org.w3c.dom.mediacapture.ConstrainDOMStringParameters import org.w3c.dom.mediacapture.ConstrainDoubleRange import org.w3c.dom.mediacapture.ConstrainULongRange import org.w3c.dom.mediacapture.MediaTrackConstraints as JsMediaTrackConstraints -internal fun ConstrainBooleanParameters?.asCommon(): ValueOrConstrain.Constrain? { - this ?: return null - return ValueOrConstrain.Constrain(exact, ideal) -} - -internal fun ConstrainDOMStringParameters?.asCommon(): ValueOrConstrain.Constrain? { - this ?: return null - return ValueOrConstrain.Constrain(exact as? String, ideal as? String) -} - -internal fun ConstrainDoubleRange?.asCommon(): ValueOrConstrain.Constrain? { - this ?: return null - return ValueOrConstrain.Constrain(exact, ideal) -} - -internal fun ConstrainULongRange?.asCommon(): ValueOrConstrain.Constrain? { - this ?: return null - return ValueOrConstrain.Constrain(exact, ideal) -} - -internal fun ConstrainDOMStringParameters?.asCommonFacingModeConstrain(): ValueOrConstrain.Constrain? { - this ?: return null - return ValueOrConstrain.Constrain( - (exact as? String)?.asCommonFacingMode(), - (ideal as? String)?.asCommonFacingMode() - ) -} - -internal fun String.asCommonFacingMode() = when (this) { - "user" -> FacingMode.User - else -> FacingMode.Environment -} - @Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") internal fun JsMediaTrackConstraints.asCommon() = MediaTrackConstraints( aspectRatio = (aspectRatio as? Double)?.asValueConstrain() @@ -55,7 +27,7 @@ internal fun JsMediaTrackConstraints.asCommon() = MediaTrackConstraints( echoCancellation = (echoCancellation as? Boolean)?.asValueConstrain() ?: (echoCancellation as? ConstrainBooleanParameters)?.asCommon(), - facingMode = (facingMode as? String)?.asCommonFacingMode()?.asValueConstrain() + facingMode = (facingMode as? String)?.toFacingMode()?.asValueConstrain() ?: (facingMode as? ConstrainDOMStringParameters).asCommonFacingModeConstrain(), frameRate = (frameRate as? Double)?.asValueConstrain() @@ -81,3 +53,31 @@ internal fun JsMediaTrackConstraints.asCommon() = MediaTrackConstraints( width = (width as? Number)?.toInt()?.asValueConstrain() ?: (width as? ConstrainULongRange)?.asCommon(), ) + +internal fun ConstrainBooleanParameters?.asCommon(): ValueOrConstrain.Constrain? { + this ?: return null + return ValueOrConstrain.Constrain(exact, ideal) +} + +internal fun ConstrainDOMStringParameters?.asCommon(): ValueOrConstrain.Constrain? { + this ?: return null + return ValueOrConstrain.Constrain(exact as? String, ideal as? String) +} + +internal fun ConstrainDoubleRange?.asCommon(): ValueOrConstrain.Constrain? { + this ?: return null + return ValueOrConstrain.Constrain(exact, ideal) +} + +internal fun ConstrainULongRange?.asCommon(): ValueOrConstrain.Constrain? { + this ?: return null + return ValueOrConstrain.Constrain(exact, ideal) +} + +internal fun ConstrainDOMStringParameters?.asCommonFacingModeConstrain(): ValueOrConstrain.Constrain? { + this ?: return null + return ValueOrConstrain.Constrain( + (exact as? String)?.toFacingMode(), + (ideal as? String)?.toFacingMode() + ) +} diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/internal/OfferAnswerOptions.kt b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/internal/OfferAnswerOptions.kt new file mode 100644 index 00000000..122431e9 --- /dev/null +++ b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/internal/OfferAnswerOptions.kt @@ -0,0 +1,14 @@ +package com.shepeliev.webrtckmp.internal + +import com.shepeliev.webrtckmp.OfferAnswerOptions +import kotlin.js.Json +import kotlin.js.json + +internal fun OfferAnswerOptions.toPlatform(): Json { + return json().apply { + iceRestart?.also { add(json("iceRestart" to it)) } + offerToReceiveAudio?.also { add(json("offerToReceiveAudio" to it)) } + offerToReceiveVideo?.also { add(json("offerToReceiveVideo" to it)) } + voiceActivityDetection?.also { add(json("voiceActivityDetection" to it)) } + } +} diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/internal/RtcConfiguration.js.kt b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/internal/RtcConfiguration.js.kt new file mode 100644 index 00000000..ad3d449d --- /dev/null +++ b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/internal/RtcConfiguration.js.kt @@ -0,0 +1,22 @@ +package com.shepeliev.webrtckmp.internal + +import com.shepeliev.webrtckmp.RtcConfiguration +import com.shepeliev.webrtckmp.externals.RTCPeerConnectionConfiguration +import kotlin.js.json + +internal actual fun RtcConfiguration.toPlatform(): RTCPeerConnectionConfiguration { + val js = json( + "bundlePolicy" to bundlePolicy.toStringValue(), + "iceCandidatePoolSize" to iceCandidatePoolSize, + "iceServers" to iceServers.map { it.toPlatform() }.toTypedArray(), + "iceTransportPolicy" to iceTransportPolicy.toStringValue(), + "rtcpMuxPolicy" to rtcpMuxPolicy.toStringValue(), + ) + + if (certificates != null) { + js.add(json("certificates" to certificates.map { it.js })) + } + + @Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") + return js.asDynamic() as RTCPeerConnectionConfiguration +} diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/internal/RtcConfiguration.kt b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/internal/RtcConfiguration.kt new file mode 100644 index 00000000..4232c590 --- /dev/null +++ b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/internal/RtcConfiguration.kt @@ -0,0 +1,17 @@ +package com.shepeliev.webrtckmp.internal + +import com.shepeliev.webrtckmp.RtcConfiguration +import kotlin.js.Json +import kotlin.js.json + +internal fun RtcConfiguration.toJs(): Json = json( + "bundlePolicy" to bundlePolicy.toStringValue(), + "iceCandidatePoolSize" to iceCandidatePoolSize, + "iceServers" to iceServers.map { it.toPlatform() }.toTypedArray(), + "iceTransportPolicy" to iceTransportPolicy.toStringValue(), + "rtcpMuxPolicy" to rtcpMuxPolicy.toStringValue(), +).apply { + if (certificates != null) { + add(json("certificates" to certificates.map { it.js })) + } +} diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/internal/SessionDescription.kt b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/internal/SessionDescription.kt new file mode 100644 index 00000000..df240ad9 --- /dev/null +++ b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/internal/SessionDescription.kt @@ -0,0 +1,10 @@ +package com.shepeliev.webrtckmp.internal + +import com.shepeliev.webrtckmp.SessionDescription +import com.shepeliev.webrtckmp.toCanonicalString +import kotlin.js.json + +internal fun SessionDescription.toPlatform() = json( + "type" to type.toCanonicalString(), + "sdp" to sdp +) diff --git a/webrtc-kmp/src/jsTest/kotlin/com/shepeliev/webrtckmp/JsMediaTrackConstraintsExtTest.kt b/webrtc-kmp/src/jsTest/kotlin/com/shepeliev/webrtckmp/JsMediaTrackConstraintsExtTest.kt index 74d28a07..8188c787 100644 --- a/webrtc-kmp/src/jsTest/kotlin/com/shepeliev/webrtckmp/JsMediaTrackConstraintsExtTest.kt +++ b/webrtc-kmp/src/jsTest/kotlin/com/shepeliev/webrtckmp/JsMediaTrackConstraintsExtTest.kt @@ -1,5 +1,6 @@ package com.shepeliev.webrtckmp +import com.shepeliev.webrtckmp.internal.asCommon import org.w3c.dom.mediacapture.ConstrainBooleanParameters import org.w3c.dom.mediacapture.ConstrainDOMStringParameters import org.w3c.dom.mediacapture.ConstrainDoubleRange diff --git a/webrtc-kmp/src/jsTest/kotlin/com/shepeliev/webrtckmp/TestUtils.kt b/webrtc-kmp/src/jsTest/kotlin/com/shepeliev/webrtckmp/TestUtils.kt deleted file mode 100644 index 45586e10..00000000 --- a/webrtc-kmp/src/jsTest/kotlin/com/shepeliev/webrtckmp/TestUtils.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.shepeliev.webrtckmp - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.promise -import kotlinx.coroutines.withTimeout - -@DelicateCoroutinesApi -actual inline fun runTest( - timeout: Long, - crossinline block: suspend CoroutineScope.() -> Unit -): dynamic { - return GlobalScope.promise { - runCatching { withTimeout(timeout) { block() } } - .onFailure { it.log() } - .exceptionOrNull()?.also { throw it } - }.asDynamic() -} - -fun Throwable.log() { - console.error(this) - cause?.let { - console.error("Caused by:") - it.log() - } -} diff --git a/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/MediaDevices.wasmJs.kt b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/MediaDevices.wasmJs.kt new file mode 100644 index 00000000..753d2916 --- /dev/null +++ b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/MediaDevices.wasmJs.kt @@ -0,0 +1,4 @@ +package com.shepeliev.webrtckmp + +internal actual val mediaDevices: MediaDevices + get() = TODO("Not yet implemented") diff --git a/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStream.wasmJs.kt b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStream.wasmJs.kt new file mode 100644 index 00000000..b142e90e --- /dev/null +++ b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStream.wasmJs.kt @@ -0,0 +1,9 @@ +package com.shepeliev.webrtckmp.externals + +import com.shepeliev.webrtckmp.internal.toList +import org.w3c.dom.mediacapture.MediaStream + +@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") +internal actual fun PlatformMediaStream.getTracks(): List { + return (this as MediaStream).getTracks().toList().filterNotNull().map { it as PlatformMediaStreamTrack } +} diff --git a/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStreamTrack.wasmJs.kt b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStreamTrack.wasmJs.kt new file mode 100644 index 00000000..0307eaf6 --- /dev/null +++ b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStreamTrack.wasmJs.kt @@ -0,0 +1,9 @@ +package com.shepeliev.webrtckmp.externals + +import com.shepeliev.webrtckmp.MediaTrackConstraints +import com.shepeliev.webrtckmp.internal.toMediaTrackConstraints +import org.w3c.dom.mediacapture.MediaStreamTrack + +internal actual fun PlatformMediaStreamTrack.getConstraints(): MediaTrackConstraints { + return (this as MediaStreamTrack).getConstraints().toMediaTrackConstraints() +} diff --git a/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCCertificate.wasmJs.kt b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCCertificate.wasmJs.kt new file mode 100644 index 00000000..efc2a5db --- /dev/null +++ b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCCertificate.wasmJs.kt @@ -0,0 +1,45 @@ +package com.shepeliev.webrtckmp.externals + +import com.shepeliev.webrtckmp.KeyType +import kotlinx.coroutines.await +import kotlin.js.Promise + +internal actual suspend fun generateRTCCertificate( + keyType: KeyType, + expires: Long +): RTCCertificate { + val options = when (keyType) { + KeyType.RSA -> createRsaOptions() + KeyType.ECDSA -> createEcdsaOptions() + } + return JsRTCPeerConnection.generateCertificate(options).await() +} + +private fun createRsaOptions(): JsAny = js( + """ + ({ + "name": "RSASSA-PKCS10-v1_5", + "modulusLength": 2048, + "publicExponent": new Uint8Array([1, 0, 1]), + "hash": "SHA-256" + }) + """ +) + +private fun createEcdsaOptions(): JsAny = js( + """ + ({ + "name": "ECDSA", + "namedCurve": "P-256" + }) + """ +) + +@JsName("RTCPeerConnection") +private external class JsRTCPeerConnection { + companion object { + fun generateCertificate(options: JsAny): Promise + } +} + +private external interface WasmRTCCertificate : RTCCertificate, JsAny diff --git a/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCDataChannel.wasmJs.kt b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCDataChannel.wasmJs.kt new file mode 100644 index 00000000..d1449519 --- /dev/null +++ b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCDataChannel.wasmJs.kt @@ -0,0 +1,30 @@ +package com.shepeliev.webrtckmp.externals + +import org.khronos.webgl.ArrayBuffer +import org.khronos.webgl.Int8Array +import org.khronos.webgl.get +import org.khronos.webgl.set + +@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") +internal actual val MessageEvent.data: ByteArray + get() { + val jsArray = Int8Array((this as WasmMessageEvent).data) + return ByteArray(jsArray.length) { jsArray[it] } + } + +@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") +internal actual fun RTCDataChannel.send(data: ByteArray) { + val jsArray = Int8Array(data.size) + data.forEachIndexed { index, byte -> jsArray[index] = byte } + (this as WasmRTCDataChannel).send(jsArray) +} + +@JsName("RTCDataChannel") +private external interface WasmRTCDataChannel : RTCDataChannel { + fun send(data: Int8Array) +} + +@JsName("MessageEvent") +private external interface WasmMessageEvent : MessageEvent { + val data: ArrayBuffer +} diff --git a/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCIceCandidate.wasmJs.kt b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCIceCandidate.wasmJs.kt new file mode 100644 index 00000000..504bbfea --- /dev/null +++ b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCIceCandidate.wasmJs.kt @@ -0,0 +1,18 @@ +package com.shepeliev.webrtckmp.externals + +internal actual fun RTCIceCandidate( + candidate: String, + sdpMid: String, + sdpMLineIndex: Int +): RTCIceCandidate { + return createRTCIceCandidate(candidate, sdpMid, sdpMLineIndex) +} + +@Suppress("UNUSED_PARAMETER") +private fun createRTCIceCandidate( + candidate: String, + sdpMid: String, + sdpMLineIndex: Int +): WasmRTCIceCandidate = js("new RTCIceCandidate({candidate: candidate, sdpMid: sdpMid, sdpMLineIndex: sdpMLineIndex})") + +internal external interface WasmRTCIceCandidate : JsAny, RTCIceCandidate diff --git a/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCPeerConnection.wasmJs.kt b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCPeerConnection.wasmJs.kt new file mode 100644 index 00000000..bbe15c86 --- /dev/null +++ b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCPeerConnection.wasmJs.kt @@ -0,0 +1,222 @@ +package com.shepeliev.webrtckmp.externals + +import com.shepeliev.webrtckmp.IceCandidate +import com.shepeliev.webrtckmp.OfferAnswerOptions +import com.shepeliev.webrtckmp.RtcConfiguration +import com.shepeliev.webrtckmp.SessionDescription +import com.shepeliev.webrtckmp.internal.WasmRtcConfiguration +import com.shepeliev.webrtckmp.internal.await +import com.shepeliev.webrtckmp.internal.toList +import com.shepeliev.webrtckmp.internal.toWasmJs +import kotlin.js.Promise + +internal actual fun RTCPeerConnection(configuration: RtcConfiguration): RTCPeerConnection = + WasmRTCPeerConnection(configuration.toWasmJs()) + +internal actual suspend fun RTCPeerConnection.createOffer(options: OfferAnswerOptions): RTCSessionDescription { + return (this as WasmRTCPeerConnection).createOffer(options.toWasmJs()).await() +} + +internal actual suspend fun RTCPeerConnection.createAnswer(options: OfferAnswerOptions): RTCSessionDescription { + return (this as WasmRTCPeerConnection).createAnswer(options.toWasmJs()).await() +} + +internal actual suspend fun RTCPeerConnection.setLocalDescription(description: SessionDescription) { + return (this as WasmRTCPeerConnection).setLocalDescription(description.toWasmJs()).await() +} + +internal actual suspend fun RTCPeerConnection.setRemoteDescription(description: SessionDescription) { + return (this as WasmRTCPeerConnection).setRemoteDescription(description.toWasmJs()).await() +} + +internal actual suspend fun RTCPeerConnection.addIceCandidate(candidate: IceCandidate) { + return (this as WasmRTCPeerConnection).addIceCandidate(candidate.js).await() +} + +internal actual fun RTCPeerConnection.getReceivers(): List { + return (this as WasmRTCPeerConnection).getReceivers().toList().filterNotNull() +} + +internal actual fun RTCPeerConnection.getSenders(): List { + return (this as WasmRTCPeerConnection).getSenders().toList().filterNotNull() +} + +internal actual fun RTCPeerConnection.getTransceivers(): List { + return (this as WasmRTCPeerConnection).getTransceivers().toList().filterNotNull() +} + +internal actual suspend fun RTCPeerConnection.getStats(): RTCStatsReport { + return (this as WasmRTCPeerConnection).getStats().await() +} + +internal actual fun RTCPeerConnection.createDataChannel( + label: String, + id: Int, + ordered: Boolean, + maxPacketLifeTimeMs: Int, + maxRetransmits: Int, + protocol: String, + negotiated: Boolean +): RTCDataChannel? { + val options = when { + id > -1 && maxPacketLifeTimeMs > -1 -> createRTCDataChannelOptionsWithMaxPacketLifeTime( + id, + ordered, + maxPacketLifeTimeMs, + protocol, + negotiated + ) + + id > -1 && maxRetransmits > -1 -> createRTCDataChannelOptionsWithMaxRetransmits( + id, + ordered, + maxRetransmits, + protocol, + negotiated + ) + + maxPacketLifeTimeMs > -1 -> createRTCDataChannelOptionsWithMaxPacketLifeTime( + ordered, + maxPacketLifeTimeMs, + protocol, + negotiated + ) + + maxRetransmits > -1 -> createRTCDataChannelOptionsWithMaxRetransmits( + ordered, + maxRetransmits, + protocol, + negotiated + ) + + else -> createRTCDataChannelOptions(ordered, protocol, negotiated) + } + + return (this as WasmRTCPeerConnection).createDataChannel(label, options) +} + +@JsName("RTCPeerConnection") +private external class WasmRTCPeerConnection(configuration: WasmRtcConfiguration) : RTCPeerConnection, JsAny { + override val localDescription: RTCSessionDescription? + override val remoteDescription: RTCSessionDescription? + override val signalingState: String + override val iceConnectionState: String + override val connectionState: String + override val iceGatheringState: String + override var onsignalingstatechange: (() -> Unit)? + override var oniceconnectionstatechange: (() -> Unit)? + override var onconnectionstatechange: (() -> Unit)? + override var onicegatheringstatechange: (() -> Unit)? + override var onicecandidate: ((RTCPeerConnectionIceEvent) -> Unit)? + override var ondatachannel: ((RTCDataChannelEvent) -> Unit)? + override var onnegotiationneeded: (() -> Unit)? + override var ontrack: ((RTCTrackEvent) -> Unit)? + + override fun close() + override fun addTrack(track: PlatformMediaStreamTrack, vararg streams: PlatformMediaStream): RTCRtpSender + override fun setConfiguration(configuration: RTCPeerConnectionConfiguration) + override fun removeTrack(sender: RTCRtpSender) + + fun createDataChannel(label: String, options: RTCDataChannelOptions): RTCDataChannel? + fun createOffer(options: JsAny?): Promise + fun createAnswer(options: JsAny?): Promise + fun addIceCandidate(candidate: RTCIceCandidate): Promise + fun getReceivers(): JsArray + fun getSenders(): JsArray + fun getStats(): Promise + fun getTransceivers(): JsArray + fun setLocalDescription(sdp: JsAny): Promise + fun setRemoteDescription(sdp: JsAny): Promise +} + +@Suppress("UNUSED_PARAMETER") +private fun createRTCDataChannelOptions( + id: Int, + ordered: Boolean, + protocol: String, + negotiated: Boolean +): RTCDataChannelOptions = js( + """({ + id: id, + ordered: ordered, + protocol: protocol, + negotiated: negotiated + })""" +) + +@Suppress("UNUSED_PARAMETER") +private fun createRTCDataChannelOptionsWithMaxRetransmits( + id: Int, + ordered: Boolean, + maxRetransmits: Int, + protocol: String, + negotiated: Boolean +): RTCDataChannelOptions = js( + """({ + id: id, + ordered: ordered, + maxRetransmits: maxRetransmits, + protocol: protocol, + negotiated: negotiated + })""" +) + +@Suppress("UNUSED_PARAMETER") +private fun createRTCDataChannelOptionsWithMaxPacketLifeTime( + id: Int, + ordered: Boolean, + maxPacketLifeTime: Int, + protocol: String, + negotiated: Boolean +): RTCDataChannelOptions = js( + """({ + id: id, + ordered: ordered, + maxPacketLifeTime: maxPacketLifeTime, + protocol: protocol, + negotiated: negotiated + })""" +) + +@Suppress("UNUSED_PARAMETER") +private fun createRTCDataChannelOptions( + ordered: Boolean, + protocol: String, + negotiated: Boolean +): RTCDataChannelOptions = js( + """({ + ordered: ordered, + protocol: protocol, + negotiated: negotiated + })""" +) + +@Suppress("UNUSED_PARAMETER") +private fun createRTCDataChannelOptionsWithMaxRetransmits( + ordered: Boolean, + maxRetransmits: Int, + protocol: String, + negotiated: Boolean +): RTCDataChannelOptions = js( + """({ + ordered: ordered, + maxRetransmits: maxRetransmits, + protocol: protocol, + negotiated: negotiated + })""" +) + +@Suppress("UNUSED_PARAMETER") +private fun createRTCDataChannelOptionsWithMaxPacketLifeTime( + ordered: Boolean, + maxPacketLifeTime: Int, + protocol: String, + negotiated: Boolean +): RTCDataChannelOptions = js( + """({ + ordered: ordered, + maxPacketLifeTime: maxPacketLifeTime, + protocol: protocol, + negotiated: negotiated + })""" +) diff --git a/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpParameters.wasmJs.kt b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpParameters.wasmJs.kt new file mode 100644 index 00000000..b24de71f --- /dev/null +++ b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpParameters.wasmJs.kt @@ -0,0 +1,20 @@ +package com.shepeliev.webrtckmp.externals + +import com.shepeliev.webrtckmp.internal.toList + +@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") +internal actual val RTCRtpParameters.codes: List + get() = (this as WasmJsRTCRtpParameters).codecs.toList().filterNotNull() + +@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") +internal actual val RTCRtpParameters.headerExtensions: List + get() = (this as WasmJsRTCRtpParameters).headerExtensions.toList().filterNotNull() + +@JsName("RTCRtpParameters") +private external interface WasmJsRTCRtpParameters : RTCRtcpParameters { + val codecs: JsArray + val headerExtensions: JsArray +} + +@JsName("RTCRtpCodecParameters") +private external interface WasmJsRTCRtpCodecParameters : RTCRtpCodecParameters, JsAny diff --git a/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpReceiver.wasmJs.kt b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpReceiver.wasmJs.kt new file mode 100644 index 00000000..7bf767ff --- /dev/null +++ b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpReceiver.wasmJs.kt @@ -0,0 +1,4 @@ +package com.shepeliev.webrtckmp.externals + +@JsName("RTCRtpReceiver") +internal external interface WasmRTCRtpReceiver : RTCRtpReceiver, JsAny diff --git a/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpSender.wasmJs.kt b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpSender.wasmJs.kt new file mode 100644 index 00000000..a2b2dfc3 --- /dev/null +++ b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpSender.wasmJs.kt @@ -0,0 +1,15 @@ +package com.shepeliev.webrtckmp.externals + +import com.shepeliev.webrtckmp.internal.await +import org.w3c.dom.mediacapture.MediaStreamTrack +import kotlin.js.Promise + +@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") +internal actual suspend fun RTCRtpSender.replaceTrack(withTrack: PlatformMediaStreamTrack?) { + (this as WasmRTCRtpSender).replaceTrack(withTrack).await() +} + +@JsName("RTCRtpSender") +internal external interface WasmRTCRtpSender : RTCRtpSender, JsAny { + fun replaceTrack(newTrack: PlatformMediaStreamTrack?): Promise +} diff --git a/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpTransceiver.wasmJs.kt b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpTransceiver.wasmJs.kt new file mode 100644 index 00000000..a3a78a72 --- /dev/null +++ b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCRtpTransceiver.wasmJs.kt @@ -0,0 +1,4 @@ +package com.shepeliev.webrtckmp.externals + +@JsName("RTCRtpTransceiver") +internal external interface WasmRTCRtpTransceiver : RTCRtpTransceiver, JsAny diff --git a/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCSessionDescription.wasmJs.kt b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCSessionDescription.wasmJs.kt new file mode 100644 index 00000000..a3e010cd --- /dev/null +++ b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCSessionDescription.wasmJs.kt @@ -0,0 +1,4 @@ +package com.shepeliev.webrtckmp.externals + +@JsName("RTCSessionDescription") +internal external interface WasmRTCSessionDescription : RTCSessionDescription, JsAny diff --git a/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCStatsReport.wasmJs.kt b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCStatsReport.wasmJs.kt new file mode 100644 index 00000000..4331206d --- /dev/null +++ b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCStatsReport.wasmJs.kt @@ -0,0 +1,4 @@ +package com.shepeliev.webrtckmp.externals + +@JsName("RTCStatsReport") +internal external interface WasmRTCStatsReport : RTCStatsReport, JsAny diff --git a/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCTrackEvent.wasmJs.kt b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCTrackEvent.wasmJs.kt new file mode 100644 index 00000000..26ac532d --- /dev/null +++ b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/RTCTrackEvent.wasmJs.kt @@ -0,0 +1,13 @@ +package com.shepeliev.webrtckmp.externals + +import com.shepeliev.webrtckmp.internal.toList +import org.w3c.dom.mediacapture.MediaStream + +@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") +internal actual val RTCTrackEvent.streams: List + get() = (this as WasmJsRTCTrackEvent).streams.toList().map { it as PlatformMediaStream } + +@JsName("RTCTrackEvent") +private external interface WasmJsRTCTrackEvent : RTCTrackEvent { + val streams: JsArray +} diff --git a/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/IceServer.kt b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/IceServer.kt new file mode 100644 index 00000000..03a99891 --- /dev/null +++ b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/IceServer.kt @@ -0,0 +1,18 @@ +package com.shepeliev.webrtckmp.internal + +import com.shepeliev.webrtckmp.IceServer + +internal external interface JsIceServer : JsAny + +internal fun IceServer.toWasmJs(): JsIceServer = createIceServer(urls.toJsArray(), username, password) + +@Suppress("UNUSED_PARAMETER") +private fun createIceServer(urls: JsArray, username: String?, credential: String?): JsIceServer = js( + """ + ({ + urls: urls, + username: username, + credential: credential + }) + """ +) diff --git a/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/JSON.wasmJs.kt b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/JSON.wasmJs.kt new file mode 100644 index 00000000..cea7a0ac --- /dev/null +++ b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/JSON.wasmJs.kt @@ -0,0 +1,10 @@ +package com.shepeliev.webrtckmp.internal + +@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") +internal actual fun Any.jsonStringify(): String { + return JSON.stringify(this as JsAny) +} + +private external object JSON { + fun stringify(obj: JsAny): String +} diff --git a/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/JsAny.kt b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/JsAny.kt new file mode 100644 index 00000000..a52bff8c --- /dev/null +++ b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/JsAny.kt @@ -0,0 +1,9 @@ +package com.shepeliev.webrtckmp.internal + +internal inline fun JsAny.asTypeOrNull(): T? { + val any = this as Any + + println("any: ${this::class}") + + return any as? T +} diff --git a/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/List.kt b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/List.kt new file mode 100644 index 00000000..c5a19de7 --- /dev/null +++ b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/List.kt @@ -0,0 +1,14 @@ +package com.shepeliev.webrtckmp.internal + +internal fun List.toJsArray(): JsArray = + JsArray().also { array -> forEach { jsArrayPush(array, it) } } + +internal fun List.toJsArray(): JsArray = + JsArray().also { array -> forEach { jsArrayPush(array, it.toJsString()) } } + +@Suppress("UNUSED_PARAMETER") +private fun jsArrayPush(array: JsArray, value: T) { + js("array.push(value)") +} + +internal fun JsArray.toList(): List = (0..()?.value, + echoCancellation = echoCancellation?.toValueOrConstrain(), + facingMode = facingMode?.toValueOrConstrain()?.map { it.toFacingMode() }, + frameRate = frameRate?.toValueOrConstrain(), + groupId = groupId?.toValueOrConstrain()?.value, + height = height?.toValueOrConstrain(), + latency = latency?.toValueOrConstrain(), + noiseSuppression = noiseSuppression?.toValueOrConstrain(), + sampleRate = sampleRate?.toValueOrConstrain(), + sampleSize = sampleSize?.toValueOrConstrain(), + width = width?.toValueOrConstrain() +) + +@Suppress("UNCHECKED_CAST") +internal inline fun JsAny.toValueOrConstrain(): ValueOrConstrain? { + return when (T::class) { + Int::class -> toIntValueOrConstrain() as ValueOrConstrain? + Double::class -> toDoubleValueOrConstrain() as ValueOrConstrain? + Boolean::class -> toBooleanValueOrConstrain() as ValueOrConstrain? + String::class -> toStringValueOrConstrain() as ValueOrConstrain? + else -> null + } +} + +private fun JsAny.toIntValueOrConstrain(): ValueOrConstrain? { + return when { + isJsNumber(this) && isInteger(this) -> ValueOrConstrain.Value(getInt(this)) + + isObject(this) -> { + val constrain = getConstrainULongRange(this) + ValueOrConstrain.Constrain(constrain.exact, constrain.ideal) + } + + else -> null + } +} + +private fun JsAny.toDoubleValueOrConstrain(): ValueOrConstrain? { + return when { + isJsNumber(this) -> ValueOrConstrain.Value(getDouble(this)) + + isObject(this) -> { + val constrain = getConstrainDoubleRange(this) + ValueOrConstrain.Constrain(constrain.exact, constrain.ideal) + } + + else -> null + } +} + +private fun JsAny.toBooleanValueOrConstrain(): ValueOrConstrain? { + return when { + isJsBoolean(this) -> ValueOrConstrain.Value(getBoolean(this)) + + isObject(this) -> { + val constrain = getConstrainBooleanParameters(this) + ValueOrConstrain.Constrain(constrain.exact, constrain.ideal) + } + + else -> null + } +} + +private fun JsAny.toStringValueOrConstrain(): ValueOrConstrain? { + return when { + isJsString(this) -> ValueOrConstrain.Value(getString(this)) + isObject(this) -> { + val constrain = getConstrainDOMStringParameters(this) + ValueOrConstrain.Constrain(constrain.exact.toString(), constrain.ideal.toString()) + } + + else -> null + } +} + +@Suppress("UNUSED_PARAMETER") +private fun isJsNumber(value: JsAny): Boolean = js("typeof value === 'number'") + +@Suppress("UNUSED_PARAMETER") +private fun isInteger(value: JsAny): Boolean = js("Number.isInteger(value)") + +@Suppress("UNUSED_PARAMETER") +private fun isJsString(value: JsAny): Boolean = js("typeof value === 'string'") + +@Suppress("UNUSED_PARAMETER") +private fun isJsBoolean(value: JsAny): Boolean = js("typeof value === 'boolean'") + +@Suppress("UNUSED_PARAMETER") +private fun isObject(value: JsAny): Boolean = js("typeof value === 'object'") + +@Suppress("UNUSED_PARAMETER") +private fun getInt(value: JsAny): Int = js("value") + +@Suppress("UNUSED_PARAMETER") +private fun getDouble(value: JsAny): Double = js("value") + +@Suppress("UNUSED_PARAMETER") +private fun getString(value: JsAny): String = js("value") + +@Suppress("UNUSED_PARAMETER") +private fun getBoolean(value: JsAny): Boolean = js("value") + +@Suppress("UNUSED_PARAMETER") +private fun getConstrainDoubleRange(value: JsAny): ConstrainDoubleRange = js("value") + +@Suppress("UNUSED_PARAMETER") +private fun getConstrainBooleanParameters(value: JsAny): ConstrainBooleanParameters = js("value") + +@Suppress("UNUSED_PARAMETER") +private fun getConstrainDOMStringParameters(value: JsAny): ConstrainDOMStringParameters = js("value") + +@Suppress("UNUSED_PARAMETER") +private fun getConstrainULongRange(value: JsAny): ConstrainULongRange = js("value") diff --git a/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/OfferAnswerOptions.kt b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/OfferAnswerOptions.kt new file mode 100644 index 00000000..12b8b2c4 --- /dev/null +++ b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/OfferAnswerOptions.kt @@ -0,0 +1,28 @@ +package com.shepeliev.webrtckmp.internal + +import com.shepeliev.webrtckmp.OfferAnswerOptions + +internal fun OfferAnswerOptions.toWasmJs(): JsAny { + return createOfferAnswerOptions( + iceRestart = iceRestart, + offerToReceiveAudio = offerToReceiveAudio, + offerToReceiveVideo = offerToReceiveVideo, + voiceActivityDetection = voiceActivityDetection + ) +} + +@Suppress("UNUSED_PARAMETER") +private fun createOfferAnswerOptions( + iceRestart: Boolean?, + offerToReceiveAudio: Boolean?, + offerToReceiveVideo: Boolean?, + voiceActivityDetection: Boolean? +): JsAny = js( + """ + ({ + iceRestart: iceRestart, + offerToReceiveAudio: offerToReceiveAudio, + offerToReceiveVideo: offerToReceiveVideo, + voiceActivityDetection: voiceActivityDetection + })""" +) diff --git a/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/Promise.kt b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/Promise.kt new file mode 100644 index 00000000..64eb833b --- /dev/null +++ b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/Promise.kt @@ -0,0 +1,16 @@ +package com.shepeliev.webrtckmp.internal + +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.js.Promise + +// TODO: replace it by kotlinx.coroutines.await after releasing https://github.com/Kotlin/kotlinx.coroutines/pull/4120 +@Suppress("UNCHECKED_CAST") +suspend fun Promise.await(): T = suspendCancellableCoroutine { cont: CancellableContinuation -> + this@await.then( + onFulfilled = { cont.resume(it as T); null }, + onRejected = { cont.resumeWithException(it.toThrowableOrNull() ?: Exception("Non-Kotlin exception $it")); null } + ) +} diff --git a/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/RtcCertificatePem.kt b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/RtcCertificatePem.kt new file mode 100644 index 00000000..0e95c816 --- /dev/null +++ b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/RtcCertificatePem.kt @@ -0,0 +1,9 @@ +package com.shepeliev.webrtckmp.internal + +import com.shepeliev.webrtckmp.RtcCertificatePem +import com.shepeliev.webrtckmp.externals.RTCCertificate + +internal external interface JsRTCCertificate : RTCCertificate, JsAny + +@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") +internal fun RtcCertificatePem.toWasmJs(): JsRTCCertificate = js as JsRTCCertificate diff --git a/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/RtcConfiguration.kt b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/RtcConfiguration.kt new file mode 100644 index 00000000..16e486fe --- /dev/null +++ b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/RtcConfiguration.kt @@ -0,0 +1,5 @@ +package com.shepeliev.webrtckmp.internal + +import com.shepeliev.webrtckmp.externals.RTCPeerConnectionConfiguration + +internal external interface WasmRtcConfiguration : RTCPeerConnectionConfiguration, JsAny diff --git a/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/RtcConfiguration.wasmJs.kt b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/RtcConfiguration.wasmJs.kt new file mode 100644 index 00000000..25a55180 --- /dev/null +++ b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/RtcConfiguration.wasmJs.kt @@ -0,0 +1,42 @@ +package com.shepeliev.webrtckmp.internal + +import com.shepeliev.webrtckmp.RtcConfiguration +import com.shepeliev.webrtckmp.externals.RTCPeerConnectionConfiguration + +internal actual fun RtcConfiguration.toPlatform(): RTCPeerConnectionConfiguration = toWasmJs() + +internal fun RtcConfiguration.toWasmJs(): WasmRtcConfiguration { + return createRtcConfiguration( + bundlePolicy.toStringValue().toJsString(), + iceCandidatePoolSize, + iceServers.map { it.toWasmJs() }.toJsArray(), + iceTransportPolicy.toStringValue().toJsString(), + rtcpMuxPolicy.toStringValue().toJsString(), + certificates?.map { it.toWasmJs() }?.toJsArray() + ).apply { + if (certificates?.isNotEmpty() == true) { + certificates.map { it.toWasmJs() }.toJsArray() + } + } +} + +@Suppress("UNUSED_PARAMETER") +private fun createRtcConfiguration( + bundlePolicy: JsString, + iceCandidatePoolSize: Int, + iceServers: JsArray, + iceTransportPolicy: JsString, + rtcpMuxPolicy: JsString, + certificates: JsArray?, +): WasmRtcConfiguration = js( + """ + ({ + bundlePolicy: bundlePolicy, + iceCandidatePoolSize: iceCandidatePoolSize, + iceServers: iceServers, + iceTransportPolicy: iceTransportPolicy, + rtcpMuxPolicy: rtcpMuxPolicy, + certificates: certificates || undefined + }) +""" +) diff --git a/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/SessionDescription.kt b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/SessionDescription.kt new file mode 100644 index 00000000..c3f51585 --- /dev/null +++ b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/SessionDescription.kt @@ -0,0 +1,12 @@ +package com.shepeliev.webrtckmp.internal + +import com.shepeliev.webrtckmp.SessionDescription +import com.shepeliev.webrtckmp.externals.WasmRTCSessionDescription +import com.shepeliev.webrtckmp.toCanonicalString + +internal fun SessionDescription.toWasmJs(): WasmRTCSessionDescription = + createRTCSessionDescription(type.toCanonicalString(), sdp) + +@Suppress("UNUSED_PARAMETER") +private fun createRTCSessionDescription(type: String, sdp: String): WasmRTCSessionDescription = + js("({type: type, sdp: sdp})") diff --git a/webrtc-kmp/src/wasmJsTest/kotlin/com/shepeliev/webrtckmp/internal/MediaTrackConstraintsSetTest.kt b/webrtc-kmp/src/wasmJsTest/kotlin/com/shepeliev/webrtckmp/internal/MediaTrackConstraintsSetTest.kt new file mode 100644 index 00000000..fe1c361b --- /dev/null +++ b/webrtc-kmp/src/wasmJsTest/kotlin/com/shepeliev/webrtckmp/internal/MediaTrackConstraintsSetTest.kt @@ -0,0 +1,54 @@ +package com.shepeliev.webrtckmp.internal + +import com.shepeliev.webrtckmp.ValueOrConstrain +import kotlin.test.Test +import kotlin.test.assertEquals + +class MediaTrackConstraintsSetTest { + @Test + fun testToValueOrConstrainAsValue() { + assertEquals(ValueOrConstrain.Value(42), jsValue(42).toValueOrConstrain()) + assertEquals(ValueOrConstrain.Value(42.0), jsValue(42).toValueOrConstrain()) + assertEquals(ValueOrConstrain.Value(true), jsValue(true).toValueOrConstrain()) + assertEquals(ValueOrConstrain.Value("exact"), jsValue("exact").toValueOrConstrain()) + } + + @Test + fun testToValueOrConstrainAsConstrain() { + assertEquals( + ValueOrConstrain.Constrain(42, 43), + jsConstrainParameters(42, 43).toValueOrConstrain() + ) + assertEquals( + ValueOrConstrain.Constrain(42.0, 43.0), + jsConstrainParameters(42, 43).toValueOrConstrain() + ) + assertEquals( + ValueOrConstrain.Constrain(exact = true, ideal = true), + jsConstrainParameters(exact = true, ideal = true).toValueOrConstrain() + ) + assertEquals( + ValueOrConstrain.Constrain("exact", "ideal"), + jsConstrainParameters("exact", "ideal").toValueOrConstrain() + ) + } + +} + +@Suppress("UNUSED_PARAMETER") +private fun jsValue(value: Int): JsAny = js("value") + +@Suppress("UNUSED_PARAMETER") +private fun jsValue(value: Boolean): JsAny = js("value") + +@Suppress("UNUSED_PARAMETER") +private fun jsValue(value: String): JsAny = js("value") + +@Suppress("UNUSED_PARAMETER") +private fun jsConstrainParameters(exact: Int, ideal: Int): JsAny = js("({exact: exact, ideal: ideal})") + +@Suppress("UNUSED_PARAMETER") +private fun jsConstrainParameters(exact: Boolean, ideal: Boolean): JsAny = js("({exact: exact, ideal: ideal})") + +@Suppress("UNUSED_PARAMETER") +private fun jsConstrainParameters(exact: String, ideal: String): JsAny = js("({exact: exact, ideal: ideal})") From 8c623695c36f7201ad0493431bf3a3f2a0bb8b25 Mon Sep 17 00:00:00 2001 From: Alex Shepeliev Date: Sat, 4 May 2024 17:39:30 +0300 Subject: [PATCH 02/16] Enable Android connected tests --- .github/workflows/pull_request.yml | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 46eacf64..b2339406 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -36,23 +36,21 @@ jobs: - name: Run Kotlin linter run: ./gradlew ktlintCheck -# TODO: Android instrumented tests don't work + - name: Run Android instrumented tests + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 29 + target: google_apis + arch: x86_64 + profile: Nexus 6 + script: ./gradlew webrtc-kmp:connectedAndroidTest -# - name: Run Android instrumented tests -# uses: reactivecircus/android-emulator-runner@v2 -# with: -# api-level: 29 -# target: google_apis -# arch: x86_64 -# profile: Nexus 6 -# script: ./gradlew webrtc-kmp:connectedAndroidTest -# -# - name: Upload Android test artifact -# uses: actions/upload-artifact@v2 -# if: failure() -# with: -# name: "Android Instrumented Tests Report HTML" -# path: "webrtc-kmp/build/reports/androidTests/connected" + - name: Upload Android test artifact + uses: actions/upload-artifact@v2 + if: failure() + with: + name: "Android Instrumented Tests Report HTML" + path: "webrtc-kmp/build/reports/androidTests/connected" - name: Run iOS tests run: ./gradlew cleanTest webrtc-kmp:iosX64Test From c6d2fbb8d47a1bae229288c02b43852b1077295a Mon Sep 17 00:00:00 2001 From: Alex Shepeliev Date: Sat, 4 May 2024 18:51:16 +0300 Subject: [PATCH 03/16] Add WasmJS tests action --- .github/workflows/pull_request.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index b2339406..04e29aa0 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -70,4 +70,14 @@ jobs: if: failure() with: name: "JS Tests Report HTML" - path: "webrtc-kmp/build/reports/tests/jsTest" + path: "webrtc-kmp/build/reports/tests/js*Test" + + - name: Run WasmJS tests + run: ./gradlew cleanTest kotlinUpgradeYarnLock webrtc-kmp:wasmJsTest + + - name: Upload WasmJS test artifact + uses: actions/upload-artifact@v2 + if: failure() + with: + name: "JS Tests Report HTML" + path: "webrtc-kmp/build/reports/tests/wasmJs*Test" From 4c9288bd297dc14fa300afa4fbe7dd47cad57fc7 Mon Sep 17 00:00:00 2001 From: Alex Shepeliev Date: Sat, 4 May 2024 19:07:51 +0300 Subject: [PATCH 04/16] Use arm64-v8a emulator for Android tests --- .github/workflows/pull_request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 04e29aa0..f9829ebf 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -41,7 +41,7 @@ jobs: with: api-level: 29 target: google_apis - arch: x86_64 + arch: arm64-v8a profile: Nexus 6 script: ./gradlew webrtc-kmp:connectedAndroidTest From e5a5c42f6c4086b2771b98066ff6d00bec7ced7d Mon Sep 17 00:00:00 2001 From: Alex Shepeliev Date: Sun, 5 May 2024 10:48:51 +0300 Subject: [PATCH 05/16] Fix web sample app --- sample/composeApp/src/jsMain/kotlin/App.kt | 4 +- .../com/shepeliev/webrtckmp/MediaDevices.kt | 11 +- .../com/shepeliev/webrtckmp/MediaStream.kt | 13 +- .../com/shepeliev/webrtckmp/PeerConnection.kt | 8 +- .../com/shepeliev/webrtckmp/MediaStream.kt | 14 +- .../com/shepeliev/webrtckmp/MediaDevices.kt | 3 +- .../com/shepeliev/webrtckmp/MediaStream.kt | 9 +- .../com/shepeliev/webrtckmp/PeerConnection.kt | 2 +- .../com/shepeliev/webrtckmp/MediaStream.kt | 4 +- .../com/shepeliev/webrtckmp/PeerConnection.kt | 10 +- .../externals/PlatformMediaStream.kt | 3 +- .../webrtckmp/internal/AudioTrackImpl.kt | 7 + .../webrtckmp/internal/VideoTrackImpl.kt | 11 ++ .../{MediaDevices.kt => MediaDevices.js.kt} | 0 .../externals/PlatformMediaStream.js.kt | 4 + .../webrtckmp/MediaDevices.wasmJs.kt | 155 +++++++++++++++++- .../externals/PlatformMediaStream.wasmJs.kt | 5 + 17 files changed, 217 insertions(+), 46 deletions(-) create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/AudioTrackImpl.kt create mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/VideoTrackImpl.kt rename webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/{MediaDevices.kt => MediaDevices.js.kt} (100%) diff --git a/sample/composeApp/src/jsMain/kotlin/App.kt b/sample/composeApp/src/jsMain/kotlin/App.kt index 5829a316..61200c33 100644 --- a/sample/composeApp/src/jsMain/kotlin/App.kt +++ b/sample/composeApp/src/jsMain/kotlin/App.kt @@ -34,9 +34,7 @@ val ReactApp = FC { _ -> } useEffect(remoteVideoTrack) { - val stream = org.w3c.dom.mediacapture.MediaStream().apply { - remoteVideoTrack?.js?.let { addTrack(it) } - } + val stream = MediaStream().apply { remoteVideoTrack?.let { addTrack(it) } }.js remoteVideoRef.current?.srcObject = stream } diff --git a/webrtc-kmp/src/androidMain/kotlin/com/shepeliev/webrtckmp/MediaDevices.kt b/webrtc-kmp/src/androidMain/kotlin/com/shepeliev/webrtckmp/MediaDevices.kt index 7848189d..ade6cf7c 100644 --- a/webrtc-kmp/src/androidMain/kotlin/com/shepeliev/webrtckmp/MediaDevices.kt +++ b/webrtc-kmp/src/androidMain/kotlin/com/shepeliev/webrtckmp/MediaDevices.kt @@ -5,7 +5,6 @@ package com.shepeliev.webrtckmp import android.Manifest import android.content.pm.PackageManager import androidx.core.content.ContextCompat -import org.webrtc.Camera2Enumerator import org.webrtc.MediaConstraints import java.util.UUID @@ -55,9 +54,7 @@ private object MediaDevicesImpl : MediaDevices { videoTrack = LocalVideoStreamTrack(androidTrack, videoCaptureController) } - val localMediaStream = - WebRtc.peerConnectionFactory.createLocalMediaStream(UUID.randomUUID().toString()) - return MediaStream(localMediaStream).apply { + return MediaStream().apply { if (audioTrack != null) addTrack(audioTrack) if (videoTrack != null) addTrack(videoTrack) } @@ -71,11 +68,7 @@ private object MediaDevicesImpl : MediaDevices { videoSource ) val videoStreamTrack = LocalVideoStreamTrack(videoTrack, screenCaptureController) - val localMediaStream = WebRtc.peerConnectionFactory - .createLocalMediaStream(UUID.randomUUID().toString()) - return MediaStream(localMediaStream).apply { - addTrack(videoStreamTrack) - } + return MediaStream().apply { addTrack(videoStreamTrack) } } override suspend fun supportsDisplayMedia(): Boolean = true diff --git a/webrtc-kmp/src/androidMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt b/webrtc-kmp/src/androidMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt index ccf2bbf2..1f9a999b 100644 --- a/webrtc-kmp/src/androidMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt +++ b/webrtc-kmp/src/androidMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt @@ -5,18 +5,17 @@ import org.webrtc.MediaStream import org.webrtc.VideoTrack import java.util.UUID -actual class MediaStream internal constructor( - val android: MediaStream?, - actual val id: String = android?.id ?: UUID.randomUUID().toString(), -) { +actual class MediaStream internal constructor(val android: MediaStream) { + actual constructor() : this(WebRtc.peerConnectionFactory.createLocalMediaStream(UUID.randomUUID().toString())) + actual val id: String = android.id private val _tracks = mutableListOf() actual val tracks: List = _tracks actual fun addTrack(track: MediaStreamTrack) { require(track is MediaStreamTrackImpl) - android?.let { + android.let { when (track.android) { is AudioTrack -> it.addTrack(track.android) is VideoTrack -> it.addTrack(track.android) @@ -33,7 +32,7 @@ actual class MediaStream internal constructor( actual fun removeTrack(track: MediaStreamTrack) { require(track is MediaStreamTrackImpl) - android?.let { + android.let { when (track.android) { is AudioTrack -> it.removeTrack(track.android) is VideoTrack -> it.removeTrack(track.android) @@ -45,6 +44,6 @@ actual class MediaStream internal constructor( actual fun release() { tracks.forEach(MediaStreamTrack::stop) - android?.dispose() + android.dispose() } } diff --git a/webrtc-kmp/src/androidMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt b/webrtc-kmp/src/androidMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt index 94852731..6dc98d4f 100644 --- a/webrtc-kmp/src/androidMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt +++ b/webrtc-kmp/src/androidMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt @@ -11,10 +11,7 @@ import com.shepeliev.webrtckmp.PeerConnectionEvent.RemovedIceCandidates import com.shepeliev.webrtckmp.PeerConnectionEvent.SignalingStateChange import com.shepeliev.webrtckmp.PeerConnectionEvent.StandardizedIceConnectionChange import com.shepeliev.webrtckmp.PeerConnectionEvent.Track -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope -import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -299,10 +296,7 @@ actual class PeerConnection actual constructor(rtcConfiguration: RtcConfiguratio } val streams = androidStreams.map { androidStream -> - MediaStream( - android = androidStream, - id = androidStream.id, - ).also { stream -> + MediaStream(androidStream).also { stream -> androidStream.audioTracks.forEach { stream.addTrack(RemoteAudioStreamTrack(it)) } androidStream.videoTracks.forEach { stream.addTrack(RemoteVideoStreamTrack(it)) } } diff --git a/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt b/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt index 1587e709..1275f2bb 100644 --- a/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt +++ b/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt @@ -1,12 +1,6 @@ package com.shepeliev.webrtckmp -val MediaStream.audioTracks: List - get() = tracks.mapNotNull { it as? AudioStreamTrack } - -val MediaStream.videoTracks: List - get() = tracks.mapNotNull { it as? VideoStreamTrack } - -expect class MediaStream { +expect class MediaStream() { val id: String val tracks: List @@ -15,3 +9,9 @@ expect class MediaStream { fun removeTrack(track: MediaStreamTrack) fun release() } + +val MediaStream.audioTracks: List + get() = tracks.mapNotNull { it as? AudioStreamTrack } + +val MediaStream.videoTracks: List + get() = tracks.mapNotNull { it as? VideoStreamTrack } diff --git a/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/MediaDevices.kt b/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/MediaDevices.kt index fbba9158..df9929fe 100644 --- a/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/MediaDevices.kt +++ b/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/MediaDevices.kt @@ -34,8 +34,7 @@ private object MediaDevicesImpl : MediaDevices { LocalVideoStreamTrack(iosVideoTrack, videoCaptureController) } - val localMediaStream = WebRtc.peerConnectionFactory.mediaStreamWithStreamId(NSUUID.UUID().UUIDString()) - return MediaStream(localMediaStream).apply { + return MediaStream().apply { if (audioTrack != null) addTrack(audioTrack) if (videoTrack != null) addTrack(videoTrack) } diff --git a/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt b/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt index 7c6d117f..e8376f3d 100644 --- a/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt +++ b/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt @@ -8,18 +8,17 @@ import WebRTC.RTCVideoTrack import kotlinx.cinterop.ExperimentalForeignApi import platform.Foundation.NSUUID -actual class MediaStream internal constructor( - val ios: RTCMediaStream?, - actual val id: String = ios?.streamId ?: NSUUID.UUID().UUIDString, -) { +actual class MediaStream internal constructor(val ios: RTCMediaStream) { + actual constructor() : this(WebRtc.peerConnectionFactory.mediaStreamWithStreamId(NSUUID.UUID().UUIDString)) + actual val id: String = ios.streamId private val _tracks = mutableListOf() actual val tracks: List = _tracks actual fun addTrack(track: MediaStreamTrack) { require(track is MediaStreamTrackImpl) - ios?.let { + ios.let { when (track.ios) { is RTCAudioTrack -> it.addAudioTrack(track.ios) is RTCVideoTrack -> it.addVideoTrack(track.ios) diff --git a/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt b/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt index ada2066a..631f7215 100644 --- a/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt +++ b/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt @@ -282,7 +282,7 @@ actual class PeerConnection actual constructor( val iosStreams = streams.map { it as RTCMediaStream } val commonStreams = iosStreams.map { iosStream -> - MediaStream(ios = iosStream, id = iosStream.streamId).also { stream -> + MediaStream(iosStream).also { stream -> iosStream.audioTracks.forEach { stream.addTrack(RemoteAudioStreamTrack(it as RTCAudioTrack)) } iosStream.videoTracks.forEach { stream.addTrack(RemoteVideoStreamTrack(it as RTCVideoTrack)) } } diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt index 56feda4f..26ef867d 100644 --- a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt @@ -3,7 +3,9 @@ package com.shepeliev.webrtckmp import com.shepeliev.webrtckmp.externals.PlatformMediaStream import com.shepeliev.webrtckmp.externals.getTracks -actual class MediaStream internal constructor(internal val js: PlatformMediaStream) { +actual class MediaStream internal constructor(val js: PlatformMediaStream) { + actual constructor() : this(PlatformMediaStream()) + actual val id: String get() = js.id actual val tracks: List get() = js.getTracks().map { MediaStreamTrackImpl(it) } diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt index a53634bc..8deea65f 100644 --- a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt @@ -13,7 +13,9 @@ import com.shepeliev.webrtckmp.externals.setLocalDescription import com.shepeliev.webrtckmp.externals.setRemoteDescription import com.shepeliev.webrtckmp.externals.streams import com.shepeliev.webrtckmp.externals.toSessionDescription +import com.shepeliev.webrtckmp.internal.AudioTrackImpl import com.shepeliev.webrtckmp.internal.Console +import com.shepeliev.webrtckmp.internal.VideoTrackImpl import com.shepeliev.webrtckmp.internal.toPlatform import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel @@ -86,7 +88,13 @@ actual class PeerConnection actual constructor(rtcConfiguration: RtcConfiguratio val trackEvent = TrackEvent( receiver = RtpReceiver(rtcTrackEvent.receiver), streams = rtcTrackEvent.streams.map { MediaStream(it) }, - track = MediaStreamTrackImpl(rtcTrackEvent.track), + track = rtcTrackEvent.track.let { + when (it.kind) { + "audio" -> AudioTrackImpl(it) + "video" -> VideoTrackImpl(it) + else -> throw IllegalArgumentException("Unsupported track kind: ${it.kind}") + } + }, transceiver = RtpTransceiver(rtcTrackEvent.transceiver) ) _peerConnectionEvent.emit(PeerConnectionEvent.Track(trackEvent)) diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStream.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStream.kt index d5c574f1..83923edb 100644 --- a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStream.kt +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStream.kt @@ -1,11 +1,12 @@ package com.shepeliev.webrtckmp.externals @JsName("MediaStream") -internal external interface PlatformMediaStream { +external interface PlatformMediaStream { val id: String fun addTrack(track: PlatformMediaStreamTrack) fun getTrackById(id: String): PlatformMediaStreamTrack? fun removeTrack(track: PlatformMediaStreamTrack) } +internal expect fun PlatformMediaStream(): PlatformMediaStream internal expect fun PlatformMediaStream.getTracks(): List diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/AudioTrackImpl.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/AudioTrackImpl.kt new file mode 100644 index 00000000..84a80405 --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/AudioTrackImpl.kt @@ -0,0 +1,7 @@ +package com.shepeliev.webrtckmp.internal + +import com.shepeliev.webrtckmp.AudioStreamTrack +import com.shepeliev.webrtckmp.MediaStreamTrackImpl +import com.shepeliev.webrtckmp.externals.PlatformMediaStreamTrack + +internal class AudioTrackImpl(platform: PlatformMediaStreamTrack) : MediaStreamTrackImpl(platform), AudioStreamTrack diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/VideoTrackImpl.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/VideoTrackImpl.kt new file mode 100644 index 00000000..e87e53b1 --- /dev/null +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/VideoTrackImpl.kt @@ -0,0 +1,11 @@ +package com.shepeliev.webrtckmp.internal + +import com.shepeliev.webrtckmp.MediaStreamTrackImpl +import com.shepeliev.webrtckmp.VideoStreamTrack +import com.shepeliev.webrtckmp.externals.PlatformMediaStreamTrack + +internal class VideoTrackImpl(override val js: PlatformMediaStreamTrack) : MediaStreamTrackImpl(js), VideoStreamTrack { + override suspend fun switchCamera(deviceId: String?) { + Console.warn("switchCamera is not supported in browser environment.") + } +} diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/MediaDevices.kt b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/MediaDevices.js.kt similarity index 100% rename from webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/MediaDevices.kt rename to webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/MediaDevices.js.kt diff --git a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStream.js.kt b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStream.js.kt index 8f13bcb5..eb61987b 100644 --- a/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStream.js.kt +++ b/webrtc-kmp/src/jsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStream.js.kt @@ -2,6 +2,10 @@ package com.shepeliev.webrtckmp.externals import org.w3c.dom.mediacapture.MediaStream +internal actual fun PlatformMediaStream(): PlatformMediaStream { + return MediaStream().unsafeCast() +} + @Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") internal actual fun PlatformMediaStream.getTracks(): List { return (this as MediaStream).getTracks().map { it as PlatformMediaStreamTrack }.toList() diff --git a/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/MediaDevices.wasmJs.kt b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/MediaDevices.wasmJs.kt index 753d2916..6bac067b 100644 --- a/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/MediaDevices.wasmJs.kt +++ b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/MediaDevices.wasmJs.kt @@ -1,4 +1,155 @@ package com.shepeliev.webrtckmp -internal actual val mediaDevices: MediaDevices - get() = TODO("Not yet implemented") +import com.shepeliev.webrtckmp.externals.PlatformMediaStream +import com.shepeliev.webrtckmp.internal.await +import com.shepeliev.webrtckmp.internal.toList +import kotlinx.browser.window +import org.w3c.dom.mediacapture.AUDIOINPUT +import org.w3c.dom.mediacapture.MediaTrackConstraints +import org.w3c.dom.mediacapture.VIDEOINPUT +import kotlin.js.Promise + +internal actual val mediaDevices: MediaDevices = MediaDevicesImpl + +private object MediaDevicesImpl : MediaDevices { + override suspend fun getUserMedia(streamConstraints: MediaStreamConstraintsBuilder.() -> Unit): MediaStream { + val constraints = MediaStreamConstraintsBuilder().let { + streamConstraints(it) + it.constraints + } + val jsStream = window.navigator.mediaDevices.getUserMedia(constraints.toJson()).await() + return MediaStream(jsStream) + } + + override suspend fun getDisplayMedia(): MediaStream { + if (!supportsDisplayMedia()) { + error("getDisplayMedia is not supported in this environment") + } + + return MediaStream(jsGetDisplayMedia().await()) + } + + override suspend fun supportsDisplayMedia(): Boolean = jsSupportsDisplayMedia() + + override suspend fun enumerateDevices(): List { + val devices = window.navigator.mediaDevices.enumerateDevices() + .await>() + .toList() + .filterNotNull() + return devices.map { + val kind = when (it.kind) { + org.w3c.dom.mediacapture.MediaDeviceKind.AUDIOINPUT -> MediaDeviceKind.AudioInput + org.w3c.dom.mediacapture.MediaDeviceKind.VIDEOINPUT -> MediaDeviceKind.VideoInput + else -> error("Unknown media device kind: ${it.kind}") + } + MediaDeviceInfo( + deviceId = it.deviceId, + label = it.label, + kind = kind + ) + } + } + + private fun MediaStreamConstraints.toJson(): org.w3c.dom.mediacapture.MediaStreamConstraints { + val audio = audio?.let { + jsMediaTrackConstraints( + exactDeviceId = it.deviceId, + exactGroupId = it.groupId, + ) + } + + val video = video?.let { + jsMediaTrackConstraints( + exactDeviceId = it.deviceId, + exactGroupId = it.groupId, + exactFacingMode = it.facingMode?.exact?.name?.lowercase(), + idealFacingMode = it.facingMode?.ideal?.name?.lowercase(), + exactAspectRatio = it.aspectRatio?.exact, + idealAspectRatio = it.aspectRatio?.ideal, + exactWidth = it.width?.exact, + idealWidth = it.width?.ideal, + exactHeight = it.height?.exact, + idealHeight = it.height?.ideal, + exactFrameRate = it.frameRate?.exact, + idealFrameRate = it.frameRate?.ideal + ) + } + + return jsMediaStreamConstraints(audio, video) + } +} + +private fun jsGetDisplayMedia(): Promise = + js("navigator.mediaDevices.getDisplayMedia()") + +private fun jsSupportsDisplayMedia(): Boolean = js("navigator.mediaDevices.supportsDisplayMedia()") + +@Suppress("UNUSED_PARAMETER") +private fun jsMediaStreamConstraints(audio: JsAny?, video: JsAny?): org.w3c.dom.mediacapture.MediaStreamConstraints = + js("({ audio: audio, video: video })") + +@Suppress("UNUSED_PARAMETER") +private fun jsMediaTrackConstraints( + exactDeviceId: String? = null, + idealDeviceId: String? = null, + exactGroupId: String? = null, + idealGroupId: String? = null, + exactFacingMode: String? = null, + idealFacingMode: String? = null, + exactAspectRatio: Double? = null, + idealAspectRatio: Double? = null, + exactWidth: Int? = null, + idealWidth: Int? = null, + exactHeight: Int? = null, + idealHeight: Int? = null, + exactFrameRate: Double? = null, + idealFrameRate: Double? = null, +): MediaTrackConstraints = js( + """ + var constraints = {}; + if (exactDeviceId) { + constraints.deviceId = { exact: exactDeviceId }; + } + if (idealDeviceId) { + constraints.deviceId = { ideal: idealDeviceId }; + } + if (exactGroupId) { + constraints.groupId = { exact: exactGroupId }; + } + if (idealGroupId) { + constraints.groupId = { ideal: idealGroupId }; + } + if (exactFacingMode) { + constraints.facingMode = { exact: exactFacingMode }; + } + if (idealFacingMode) { + constraints.facingMode = { ideal: idealFacingMode }; + } + if (exactAspectRatio) { + constraints.aspectRatio = { exact: exactAspectRatio }; + } + if (idealAspectRatio) { + constraints.aspectRatio = { ideal: idealAspectRatio }; + } + if (exactWidth) { + constraints.width = { exact: exactWidth }; + } + if (idealWidth) { + constraints.width = { ideal: idealWidth }; + } + if (exactHeight) { + constraints.height = { exact: exactHeight }; + } + if (idealHeight) { + constraints.height = { ideal: idealHeight }; + } + if (exactFrameRate) { + constraints.frameRate = { exact: exactFrameRate }; + } + if (idealFrameRate) { + constraints.frameRate = { ideal: idealFrameRate }; + } + + return constraints; +""" +) diff --git a/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStream.wasmJs.kt b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStream.wasmJs.kt index b142e90e..74511c5b 100644 --- a/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStream.wasmJs.kt +++ b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/externals/PlatformMediaStream.wasmJs.kt @@ -3,6 +3,11 @@ package com.shepeliev.webrtckmp.externals import com.shepeliev.webrtckmp.internal.toList import org.w3c.dom.mediacapture.MediaStream +@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") +internal actual fun PlatformMediaStream(): PlatformMediaStream { + return MediaStream() as PlatformMediaStream +} + @Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") internal actual fun PlatformMediaStream.getTracks(): List { return (this as MediaStream).getTracks().toList().filterNotNull().map { it as PlatformMediaStreamTrack } From ea32fcfbdb6907fa44fdd56f8b6738b385acac51 Mon Sep 17 00:00:00 2001 From: Alex Shepeliev Date: Mon, 6 May 2024 22:31:48 +0300 Subject: [PATCH 06/16] Implement wasmJS sample app --- gradle.properties | 1 + sample/composeApp/build.gradle.kts | 21 ++++++ .../src/androidMain/kotlin/Video.android.kt | 11 ++-- .../composeApp/src/commonMain/kotlin/App.kt | 24 +++++-- .../src/commonMain/kotlin/Hangup.kt | 9 +-- .../src/commonMain/kotlin/MakeCall.kt | 18 ++++-- .../composeApp/src/commonMain/kotlin/Video.kt | 3 +- .../src/iosMain/kotlin/Video.ios.kt | 5 +- sample/composeApp/src/jsMain/kotlin/App.kt | 30 +++++---- sample/composeApp/src/jsMain/kotlin/Main.kt | 3 +- .../src/jsMain/kotlin/UseCoroutineScope.kt | 4 +- .../composeApp/src/jsMain/kotlin/Video.js.kt | 3 +- .../wasmJsMain/kotlin/StartButton.wasmJs.kt | 11 ++++ .../src/wasmJsMain/kotlin/Video.wasmJs.kt | 64 +++++++++++++++++++ .../com/shepeliev/webrtckmp/sample/Main.kt | 10 +++ .../src/wasmJsMain/resources/index.html | 12 ++++ .../com/shepeliev/webrtckmp/MediaStream.kt | 10 ++- .../com/shepeliev/webrtckmp/MediaStream.kt | 10 ++- .../webrtckmp/MediaDevices.wasmJs.kt | 3 +- 19 files changed, 203 insertions(+), 49 deletions(-) create mode 100644 sample/composeApp/src/wasmJsMain/kotlin/StartButton.wasmJs.kt create mode 100644 sample/composeApp/src/wasmJsMain/kotlin/Video.wasmJs.kt create mode 100644 sample/composeApp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/sample/Main.kt create mode 100644 sample/composeApp/src/wasmJsMain/resources/index.html diff --git a/gradle.properties b/gradle.properties index af87efa2..ee661c15 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,6 +21,7 @@ kotlin.mpp.enableCInteropCommonization=true #Compose org.jetbrains.compose.experimental.jscanvas.enabled=true +org.jetbrains.compose.experimental.wasm.enabled=true # Versions webRtcKmpVersion=0.114.4 diff --git a/sample/composeApp/build.gradle.kts b/sample/composeApp/build.gradle.kts index a41e657a..250bdf42 100644 --- a/sample/composeApp/build.gradle.kts +++ b/sample/composeApp/build.gradle.kts @@ -47,6 +47,23 @@ kotlin { } } + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + moduleName = "composeApp" + browser { + commonWebpackConfig { + outputFileName = "composeApp.js" + devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply { + static = (static ?: mutableListOf()).apply { + // Serve sources to debug inside browser + add(project.projectDir.path) + } + } + } + } + binaries.executable() + } + sourceSets { commonMain.dependencies { implementation(compose.runtime) @@ -111,6 +128,10 @@ android { } } +compose.experimental { + web.application {} +} + fun KotlinNativeTarget.configureWebRtcCinterops() { val webRtcFrameworkPath = file("$buildDir/cocoapods/synthetic/IOS/Pods/WebRTC-SDK") .resolveArchPath(konanTarget, "WebRTC") diff --git a/sample/composeApp/src/androidMain/kotlin/Video.android.kt b/sample/composeApp/src/androidMain/kotlin/Video.android.kt index 260fa44d..933e7dbc 100644 --- a/sample/composeApp/src/androidMain/kotlin/Video.android.kt +++ b/sample/composeApp/src/androidMain/kotlin/Video.android.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver +import com.shepeliev.webrtckmp.AudioStreamTrack import com.shepeliev.webrtckmp.VideoStreamTrack import com.shepeliev.webrtckmp.WebRtc import org.webrtc.RendererCommon @@ -16,21 +17,21 @@ import org.webrtc.SurfaceViewRenderer import org.webrtc.VideoSink @Composable -actual fun Video(track: VideoStreamTrack, modifier: Modifier) { +actual fun Video(videoTrack: VideoStreamTrack, modifier: Modifier, audioTrack: AudioStreamTrack?) { var renderer by remember { mutableStateOf(null) } - val lifecycleEventObserver = remember(renderer, track) { + val lifecycleEventObserver = remember(renderer, videoTrack) { LifecycleEventObserver { _, event -> when (event) { Lifecycle.Event.ON_RESUME -> { renderer?.also { it.init(WebRtc.rootEglBase.eglBaseContext, null) - track.addSinkCatching(it) + videoTrack.addSinkCatching(it) } } Lifecycle.Event.ON_PAUSE -> { - renderer?.also { track.removeSinkCatching(it) } + renderer?.also { videoTrack.removeSinkCatching(it) } renderer?.release() } @@ -46,7 +47,7 @@ actual fun Video(track: VideoStreamTrack, modifier: Modifier) { lifecycle.addObserver(lifecycleEventObserver) onDispose { - renderer?.let { track.removeSinkCatching(it) } + renderer?.let { videoTrack.removeSinkCatching(it) } renderer?.release() lifecycle.removeObserver(lifecycleEventObserver) } diff --git a/sample/composeApp/src/commonMain/kotlin/App.kt b/sample/composeApp/src/commonMain/kotlin/App.kt index 7cbdb24e..7923ac6a 100644 --- a/sample/composeApp/src/commonMain/kotlin/App.kt +++ b/sample/composeApp/src/commonMain/kotlin/App.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import co.touchlab.kermit.Logger import co.touchlab.kermit.platformLogWriter +import com.shepeliev.webrtckmp.AudioStreamTrack import com.shepeliev.webrtckmp.MediaDevices import com.shepeliev.webrtckmp.MediaStream import com.shepeliev.webrtckmp.PeerConnection @@ -35,22 +36,29 @@ fun App() { val scope = rememberCoroutineScope() val (localStream, setLocalStream) = remember { mutableStateOf(null) } val (remoteVideoTrack, setRemoteVideoTrack) = remember { mutableStateOf(null) } + val (remoteAudioTrack, setRemoteAudioTrack) = remember { mutableStateOf(null) } val (peerConnections, setPeerConnections) = remember { mutableStateOf?>(null) } LaunchedEffect(localStream, peerConnections) { if (peerConnections == null || localStream == null) return@LaunchedEffect - makeCall(peerConnections, localStream, setRemoteVideoTrack) + makeCall(peerConnections, localStream, setRemoteVideoTrack, setRemoteAudioTrack) } Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { val localVideoTrack = localStream?.videoTracks?.firstOrNull() - localVideoTrack?.let { Video(track = it, modifier = Modifier.weight(1f)) } + localVideoTrack?.let { Video(videoTrack = it, modifier = Modifier.weight(1f)) } ?: Box(modifier = Modifier.weight(1f)) - remoteVideoTrack?.let { Video(track = it, modifier = Modifier.weight(1f)) } + remoteVideoTrack?.let { + Video( + videoTrack = it, + audioTrack = remoteAudioTrack, + modifier = Modifier.weight(1f), + ) + } ?: Box(modifier = Modifier.weight(1f)) Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { @@ -67,9 +75,12 @@ fun App() { StopButton( onClick = { - hangup(peerConnections, setPeerConnections, setRemoteVideoTrack) + hangup(peerConnections) localStream.release() setLocalStream(null) + setPeerConnections(null) + setRemoteVideoTrack(null) + setRemoteAudioTrack(null) } ) @@ -85,7 +96,10 @@ fun App() { ) } else { HangupButton(onClick = { - hangup(peerConnections, setPeerConnections, setRemoteVideoTrack) + hangup(peerConnections) + setPeerConnections(null) + setRemoteVideoTrack(null) + setRemoteAudioTrack(null) }) } } diff --git a/sample/composeApp/src/commonMain/kotlin/Hangup.kt b/sample/composeApp/src/commonMain/kotlin/Hangup.kt index 034d1c3d..eee2023c 100644 --- a/sample/composeApp/src/commonMain/kotlin/Hangup.kt +++ b/sample/composeApp/src/commonMain/kotlin/Hangup.kt @@ -1,15 +1,8 @@ import com.shepeliev.webrtckmp.PeerConnection -import com.shepeliev.webrtckmp.VideoStreamTrack -fun hangup( - peerConnections: Pair?, - setPeerConnections: (Pair?) -> Unit, - setRemoteVideoTrack: (VideoStreamTrack?) -> Unit -) { +fun hangup(peerConnections: Pair?) { val (pc1, pc2) = peerConnections ?: return pc1.getTransceivers().forEach { pc1.removeTrack(it.sender) } pc1.close() pc2.close() - setPeerConnections(null) - setRemoteVideoTrack(null) } diff --git a/sample/composeApp/src/commonMain/kotlin/MakeCall.kt b/sample/composeApp/src/commonMain/kotlin/MakeCall.kt index e2be7bc5..4addc802 100644 --- a/sample/composeApp/src/commonMain/kotlin/MakeCall.kt +++ b/sample/composeApp/src/commonMain/kotlin/MakeCall.kt @@ -1,4 +1,5 @@ import co.touchlab.kermit.Logger +import com.shepeliev.webrtckmp.AudioStreamTrack import com.shepeliev.webrtckmp.IceCandidate import com.shepeliev.webrtckmp.MediaStream import com.shepeliev.webrtckmp.MediaStreamTrackKind @@ -13,14 +14,16 @@ import com.shepeliev.webrtckmp.onSignalingStateChange import com.shepeliev.webrtckmp.onTrack import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach suspend fun makeCall( peerConnections: Pair, localStream: MediaStream, - setRemoteVideoTrack: (VideoStreamTrack?) -> Unit + onRemoteVideoTrack: (VideoStreamTrack) -> Unit, + onRemoteAudioTrack: (AudioStreamTrack) -> Unit = {}, ): Nothing = coroutineScope { val (pc1, pc2) = peerConnections localStream.tracks.forEach { pc1.addTrack(it) } @@ -81,8 +84,15 @@ suspend fun makeCall( .launchIn(this) pc2.onTrack .onEach { Logger.d { "PC2 onTrack: ${it.track?.kind}" } } - .filter { it.track?.kind == MediaStreamTrackKind.Video } - .onEach { setRemoteVideoTrack(it.track as VideoStreamTrack) } + .map { it.track } + .filterNotNull() + .onEach { + if (it.kind == MediaStreamTrackKind.Audio) { + onRemoteAudioTrack(it as AudioStreamTrack) + } else if (it.kind == MediaStreamTrackKind.Video) { + onRemoteVideoTrack(it as VideoStreamTrack) + } + } .launchIn(this) val offer = pc1.createOffer(OfferAnswerOptions(offerToReceiveVideo = true, offerToReceiveAudio = true)) pc1.setLocalDescription(offer) diff --git a/sample/composeApp/src/commonMain/kotlin/Video.kt b/sample/composeApp/src/commonMain/kotlin/Video.kt index 41449a61..d4d4b3d8 100644 --- a/sample/composeApp/src/commonMain/kotlin/Video.kt +++ b/sample/composeApp/src/commonMain/kotlin/Video.kt @@ -1,6 +1,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.shepeliev.webrtckmp.AudioStreamTrack import com.shepeliev.webrtckmp.VideoStreamTrack @Composable -expect fun Video(track: VideoStreamTrack, modifier: Modifier = Modifier) +expect fun Video(videoTrack: VideoStreamTrack, modifier: Modifier = Modifier, audioTrack: AudioStreamTrack? = null) diff --git a/sample/composeApp/src/iosMain/kotlin/Video.ios.kt b/sample/composeApp/src/iosMain/kotlin/Video.ios.kt index 2de7bab9..fcd1be18 100644 --- a/sample/composeApp/src/iosMain/kotlin/Video.ios.kt +++ b/sample/composeApp/src/iosMain/kotlin/Video.ios.kt @@ -2,18 +2,19 @@ import WebRTC.RTCMTLVideoView import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.interop.UIKitView +import com.shepeliev.webrtckmp.AudioStreamTrack import com.shepeliev.webrtckmp.VideoStreamTrack import kotlinx.cinterop.ExperimentalForeignApi import platform.UIKit.UIViewContentMode @OptIn(ExperimentalForeignApi::class) @Composable -actual fun Video(track: VideoStreamTrack, modifier: Modifier) { +actual fun Video(videoTrack: VideoStreamTrack, modifier: Modifier, audioTrack: AudioStreamTrack?) { UIKitView( factory = { RTCMTLVideoView().apply { contentMode = UIViewContentMode.UIViewContentModeScaleAspectFit - track.addRenderer(this) + videoTrack.addRenderer(this) } }, modifier = modifier, diff --git a/sample/composeApp/src/jsMain/kotlin/App.kt b/sample/composeApp/src/jsMain/kotlin/App.kt index 61200c33..b5168385 100644 --- a/sample/composeApp/src/jsMain/kotlin/App.kt +++ b/sample/composeApp/src/jsMain/kotlin/App.kt @@ -1,7 +1,7 @@ import com.shepeliev.webrtckmp.MediaDevices import com.shepeliev.webrtckmp.MediaStream import com.shepeliev.webrtckmp.PeerConnection -import com.shepeliev.webrtckmp.VideoStreamTrack +import com.shepeliev.webrtckmp.videoTracks import emotion.react.css import kotlinx.coroutines.launch import mui.material.Button @@ -25,26 +25,30 @@ val ReactApp = FC { _ -> val scope = useCoroutineScope() val localVideoRef = useRef(null) val remoteVideoRef = useRef(null) + val remoteStream = useRef(null) val (localStream, setLocalStream) = useState(null) - val (remoteVideoTrack, setRemoteVideoTrack) = useState(null) val (peerConnections, setPeerConnections) = useState?>(null) useEffect(localStream) { - localVideoRef.current?.srcObject = localStream?.js - } - - useEffect(remoteVideoTrack) { - val stream = MediaStream().apply { remoteVideoTrack?.let { addTrack(it) } }.js - remoteVideoRef.current?.srcObject = stream + val localVideoStream = MediaStream().apply { localStream?.videoTracks?.firstOrNull()?.let { addTrack(it) } } + localVideoRef.current?.srcObject = localVideoStream.js } useEffect(localStream, peerConnections) { if (peerConnections == null || localStream == null) return@useEffect + remoteStream.current = MediaStream() val job = scope.launch { makeCall( peerConnections = peerConnections, localStream = localStream, - setRemoteVideoTrack = { setRemoteVideoTrack(it) } + onRemoteVideoTrack = { track -> + remoteStream.current?.addTrack(track) + remoteVideoRef.current?.srcObject = remoteStream.current?.js + }, + onRemoteAudioTrack = { track -> + remoteStream.current?.addTrack(track) + remoteVideoRef.current?.srcObject = remoteStream.current?.js + }, ) } @@ -103,9 +107,11 @@ val ReactApp = FC { _ -> } variant = ButtonVariant.contained onClick = { - hangup(peerConnections, { setPeerConnections(it) }, { setRemoteVideoTrack(it) }) + hangup(peerConnections) localStream.release() setLocalStream(null) + setPeerConnections(null) + remoteVideoRef.current?.srcObject = null } +"Stop" } @@ -128,7 +134,9 @@ val ReactApp = FC { _ -> } variant = ButtonVariant.contained onClick = { - hangup(peerConnections, { setPeerConnections(it) }, { setRemoteVideoTrack(it) }) + hangup(peerConnections) + setPeerConnections(null) + remoteVideoRef.current?.srcObject = null } +"Hangup" } diff --git a/sample/composeApp/src/jsMain/kotlin/Main.kt b/sample/composeApp/src/jsMain/kotlin/Main.kt index 77801aa2..c0e6664c 100644 --- a/sample/composeApp/src/jsMain/kotlin/Main.kt +++ b/sample/composeApp/src/jsMain/kotlin/Main.kt @@ -10,8 +10,7 @@ fun main() { root.render( Fragment.create { - ReactApp { - } + ReactApp() } ) } diff --git a/sample/composeApp/src/jsMain/kotlin/UseCoroutineScope.kt b/sample/composeApp/src/jsMain/kotlin/UseCoroutineScope.kt index 5ae43781..ffa619c2 100644 --- a/sample/composeApp/src/jsMain/kotlin/UseCoroutineScope.kt +++ b/sample/composeApp/src/jsMain/kotlin/UseCoroutineScope.kt @@ -1,10 +1,10 @@ import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel import react.useEffect fun useCoroutineScope(): CoroutineScope { - val scope = CoroutineScope(Dispatchers.Main) + val scope = MainScope() useEffect(Unit) { cleanup { scope.cancel() } } diff --git a/sample/composeApp/src/jsMain/kotlin/Video.js.kt b/sample/composeApp/src/jsMain/kotlin/Video.js.kt index b93ab349..f9839f4e 100644 --- a/sample/composeApp/src/jsMain/kotlin/Video.js.kt +++ b/sample/composeApp/src/jsMain/kotlin/Video.js.kt @@ -1,8 +1,9 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.shepeliev.webrtckmp.AudioStreamTrack import com.shepeliev.webrtckmp.VideoStreamTrack @Composable -actual fun Video(track: VideoStreamTrack, modifier: Modifier) { +actual fun Video(videoTrack: VideoStreamTrack, modifier: Modifier, audioTrack: AudioStreamTrack?) { // Dummy actual for JS } diff --git a/sample/composeApp/src/wasmJsMain/kotlin/StartButton.wasmJs.kt b/sample/composeApp/src/wasmJsMain/kotlin/StartButton.wasmJs.kt new file mode 100644 index 00000000..62dc1b82 --- /dev/null +++ b/sample/composeApp/src/wasmJsMain/kotlin/StartButton.wasmJs.kt @@ -0,0 +1,11 @@ +import androidx.compose.material.Button +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +actual fun StartButton(onClick: () -> Unit, modifier: Modifier) { + Button(onClick = onClick, modifier = modifier) { + Text("Start") + } +} diff --git a/sample/composeApp/src/wasmJsMain/kotlin/Video.wasmJs.kt b/sample/composeApp/src/wasmJsMain/kotlin/Video.wasmJs.kt new file mode 100644 index 00000000..a5d297b0 --- /dev/null +++ b/sample/composeApp/src/wasmJsMain/kotlin/Video.wasmJs.kt @@ -0,0 +1,64 @@ +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInWindow +import androidx.compose.ui.platform.LocalDensity +import com.shepeliev.webrtckmp.AudioStreamTrack +import com.shepeliev.webrtckmp.MediaStream +import com.shepeliev.webrtckmp.VideoStreamTrack +import kotlinx.browser.document +import org.w3c.dom.HTMLVideoElement +import org.w3c.dom.MediaProvider + +@Composable +actual fun Video(videoTrack: VideoStreamTrack, modifier: Modifier, audioTrack: AudioStreamTrack?) { + val stream = remember { MediaStream() } + + val videoElement = remember { + (document.createElement("video") as HTMLVideoElement).apply { + srcObject = stream.js as MediaProvider + autoplay = true + style.position = "absolute" + } + } + + DisposableEffect(videoElement, stream) { + document.body?.appendChild(videoElement) + onDispose { + document.body?.removeChild(videoElement) + videoElement.srcObject = null + stream.removeTrack(videoTrack) + audioTrack?.let { stream.removeTrack(it) } + stream.release() + } + } + + DisposableEffect(videoTrack) { + stream.addTrack(videoTrack) + onDispose { stream.removeTrack(videoTrack) } + } + + DisposableEffect(audioTrack) { + audioTrack?.let { stream.addTrack(it) } + onDispose { audioTrack?.let { stream.removeTrack(it) } } + } + + val density = LocalDensity.current + + Box(modifier = modifier + .fillMaxSize() + .onGloballyPositioned { coordinates -> + with(density) { + with(videoElement.style) { + top = "${coordinates.positionInWindow().y.toDp().value}px" + left = "${coordinates.positionInWindow().x.toDp().value}px" + width = "${coordinates.size.width.toDp().value}px" + height = "${coordinates.size.height.toDp().value}px" + } + } + }) +} diff --git a/sample/composeApp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/sample/Main.kt b/sample/composeApp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/sample/Main.kt new file mode 100644 index 00000000..f034e816 --- /dev/null +++ b/sample/composeApp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/sample/Main.kt @@ -0,0 +1,10 @@ +package com.shepeliev.webrtckmp.sample + +import App +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.window.CanvasBasedWindow + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + CanvasBasedWindow(canvasElementId = "ComposeTarget") { App() } +} diff --git a/sample/composeApp/src/wasmJsMain/resources/index.html b/sample/composeApp/src/wasmJsMain/resources/index.html new file mode 100644 index 00000000..53d10e1d --- /dev/null +++ b/sample/composeApp/src/wasmJsMain/resources/index.html @@ -0,0 +1,12 @@ + + + + + KotlinProject + + + + + + + diff --git a/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt b/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt index e8376f3d..60639cab 100644 --- a/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt +++ b/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt @@ -35,12 +35,10 @@ actual class MediaStream internal constructor(val ios: RTCMediaStream) { actual fun removeTrack(track: MediaStreamTrack) { require(track is MediaStreamTrackImpl) - ios?.let { - when (track.ios) { - is RTCAudioTrack -> it.removeAudioTrack(track.ios) - is RTCVideoTrack -> it.removeVideoTrack(track.ios) - else -> error("Unknown MediaStreamTrack kind: ${track.kind}") - } + when (track.ios) { + is RTCAudioTrack -> ios.removeAudioTrack(track.ios) + is RTCVideoTrack -> ios.removeVideoTrack(track.ios) + else -> error("Unknown MediaStreamTrack kind: ${track.kind}") } _tracks -= track } diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt index 26ef867d..ac69ccbb 100644 --- a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/MediaStream.kt @@ -2,12 +2,20 @@ package com.shepeliev.webrtckmp import com.shepeliev.webrtckmp.externals.PlatformMediaStream import com.shepeliev.webrtckmp.externals.getTracks +import com.shepeliev.webrtckmp.internal.AudioTrackImpl +import com.shepeliev.webrtckmp.internal.VideoTrackImpl actual class MediaStream internal constructor(val js: PlatformMediaStream) { actual constructor() : this(PlatformMediaStream()) actual val id: String get() = js.id - actual val tracks: List get() = js.getTracks().map { MediaStreamTrackImpl(it) } + actual val tracks: List get() = js.getTracks().map { + when (it.kind) { + "audio" -> AudioTrackImpl(it) + "video" -> VideoTrackImpl(it) + else -> throw IllegalArgumentException("Unknown track kind: ${it.kind}") + } + } actual fun addTrack(track: MediaStreamTrack) { require(track is MediaStreamTrackImpl) diff --git a/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/MediaDevices.wasmJs.kt b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/MediaDevices.wasmJs.kt index 6bac067b..8e0583d5 100644 --- a/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/MediaDevices.wasmJs.kt +++ b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/MediaDevices.wasmJs.kt @@ -105,7 +105,7 @@ private fun jsMediaTrackConstraints( exactFrameRate: Double? = null, idealFrameRate: Double? = null, ): MediaTrackConstraints = js( - """ + """{ var constraints = {}; if (exactDeviceId) { constraints.deviceId = { exact: exactDeviceId }; @@ -151,5 +151,6 @@ private fun jsMediaTrackConstraints( } return constraints; + } """ ) From 288445fcce86730527996e8ae7d4567dbb0f95b6 Mon Sep 17 00:00:00 2001 From: Alex Shepeliev Date: Wed, 8 May 2024 10:11:15 +0300 Subject: [PATCH 07/16] Re-work GH actions --- .github/workflows/pull_request.yml | 81 +++++++++++++++++++----------- 1 file changed, 53 insertions(+), 28 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index f9829ebf..c227a785 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -1,26 +1,15 @@ -name: Pull Request +name: Pull Request Checks on: pull_request: branches: [ main ] jobs: - build: - runs-on: macos-latest + lint: + runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Cocoapods cache - uses: actions/cache@v3 - id: cocoapods-cache - with: - path: | - ~/.cocoapods - ~/Library/Caches/CocoaPods - */build/cocoapods - */build/classes - key: cocoapods-cache - - name: Set up JDK 17 uses: actions/setup-java@v1 with: @@ -30,18 +19,29 @@ jobs: - name: Gradle cache uses: gradle/gradle-build-action@v2 - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - name: Run Kotlin linter run: ./gradlew ktlintCheck + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up JDK 17 + uses: actions/setup-java@v1 + with: + java-version: 17 + cache: gradle + + - name: Gradle cache + uses: gradle/gradle-build-action@v2 + - name: Run Android instrumented tests uses: reactivecircus/android-emulator-runner@v2 with: api-level: 29 target: google_apis - arch: arm64-v8a + arch: x86_64 profile: Nexus 6 script: ./gradlew webrtc-kmp:connectedAndroidTest @@ -52,16 +52,6 @@ jobs: name: "Android Instrumented Tests Report HTML" path: "webrtc-kmp/build/reports/androidTests/connected" - - name: Run iOS tests - run: ./gradlew cleanTest webrtc-kmp:iosX64Test - - - name: Upload iOS test artifact - uses: actions/upload-artifact@v2 - if: failure() - with: - name: "iOS Tests Report HTML" - path: "webrtc-kmp/build/reports/tests/iosX64Test" - - name: Run JS tests run: ./gradlew cleanTest kotlinUpgradeYarnLock webrtc-kmp:jsTest @@ -81,3 +71,38 @@ jobs: with: name: "JS Tests Report HTML" path: "webrtc-kmp/build/reports/tests/wasmJs*Test" + + test-ios: + runs-on: macos-latest + steps: + - uses: actions/checkout@v2 + + - name: Cocoapods cache + uses: actions/cache@v3 + id: cocoapods-cache + with: + path: | + ~/.cocoapods + ~/Library/Caches/CocoaPods + */build/cocoapods + */build/classes + key: cocoapods-cache + + - name: Set up JDK 17 + uses: actions/setup-java@v1 + with: + java-version: 17 + cache: gradle + + - name: Gradle cache + uses: gradle/gradle-build-action@v2 + + - name: Run iOS tests + run: ./gradlew cleanTest webrtc-kmp:iosSimulatorArm64Test + + - name: Upload iOS test artifact + uses: actions/upload-artifact@v2 + if: failure() + with: + name: "iOS Tests Report HTML" + path: "webrtc-kmp/build/reports/tests/iosX64Test" From 52d774c018ea4a875e2c5188d1ed2f7e752cde60 Mon Sep 17 00:00:00 2001 From: Alex Shepeliev Date: Wed, 8 May 2024 10:35:24 +0300 Subject: [PATCH 08/16] Enable KVM in GH actions --- .github/workflows/pull_request.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index c227a785..1e4bb972 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -36,6 +36,12 @@ jobs: - name: Gradle cache uses: gradle/gradle-build-action@v2 + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + - name: Run Android instrumented tests uses: reactivecircus/android-emulator-runner@v2 with: From a63441cae6c2909a38c05b7f7d578b48af4cfbcd Mon Sep 17 00:00:00 2001 From: Alex Shepeliev Date: Wed, 8 May 2024 10:48:01 +0300 Subject: [PATCH 09/16] Update sample app README.md --- sample/README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/sample/README.md b/sample/README.md index d3cac238..c851ccb2 100644 --- a/sample/README.md +++ b/sample/README.md @@ -21,8 +21,14 @@ Run Android emulator or connect real device. Open `sample/iosApp/iosApp.xcodeproj` in XCode build and run -### Web +### Web JS ```bash -./gradlew jsBrowserRun +./gradlew sample:composeApp:jsBrowserRun +``` + +### Web WasmJS + +```bash +./gradlew sample:composeApp:wasmJsBrowserRun ``` From 221a90e5c6dcb16200b39fd4007189212991e4c4 Mon Sep 17 00:00:00 2001 From: Alex Shepeliev Date: Wed, 8 May 2024 10:57:23 +0300 Subject: [PATCH 10/16] Fix typo --- .../kotlin/com/shepeliev/webrtckmp/RtcCertificatePem.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/RtcCertificatePem.kt b/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/RtcCertificatePem.kt index 50c2360e..09a835a3 100644 --- a/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/RtcCertificatePem.kt +++ b/webrtc-kmp/src/commonMain/kotlin/com/shepeliev/webrtckmp/RtcCertificatePem.kt @@ -1,9 +1,9 @@ package com.shepeliev.webrtckmp expect class RtcCertificatePem { - @Deprecated("Will be removes in order to comply with JS/WASM") + @Deprecated("Will be removed in order to comply with JS/WASM") val privateKey: String - @Deprecated("Will be removes in order to comply with JS/WASM") + @Deprecated("Will be removed in order to comply with JS/WASM") val certificate: String companion object { From eb28b7d3ee5eec79c97a343187f05a26480e665d Mon Sep 17 00:00:00 2001 From: Alex Shepeliev Date: Wed, 8 May 2024 11:01:45 +0300 Subject: [PATCH 11/16] Remove redundant comments --- .../com/shepeliev/webrtckmp/PeerConnection.kt | 50 ------------------- 1 file changed, 50 deletions(-) diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt index 8deea65f..924f32b6 100644 --- a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt +++ b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/PeerConnection.kt @@ -158,55 +158,24 @@ actual class PeerConnection actual constructor(rtcConfiguration: RtcConfiguratio return true } - /** - * Gets all RtpSenders associated with this peer connection. - * Note that calling getSenders will dispose of the senders previously - * returned. - */ actual fun getSenders(): List { return platform.getSenders().map { RtpSender(it) } } - /** - * Gets all RtpReceivers associated with this peer connection. - * Note that calling getReceivers will dispose of the receivers previously - * returned. - */ actual fun getReceivers(): List { return platform.getReceivers().map { RtpReceiver(it) } } - /** - * Gets all RtpTransceivers associated with this peer connection. - * Note that calling getTransceivers will dispose of the transceivers previously - * returned. - * Note: This is only available with SdpSemantics.UNIFIED_PLAN specified. - */ actual fun getTransceivers(): List { return platform.getTransceivers().map { RtpTransceiver(it) } } - /** - * Adds a new media stream track to be sent on this peer connection, and returns - * the newly created RtpSender. If streamIds are specified, the RtpSender will - * be associated with the streams specified in the streamIds list. - * - * @throws IllegalStateException if an error accors in C++ addTrack. - * An error can occur if: - * - A sender already exists for the track. - * - The peer connection is closed. - */ actual fun addTrack(track: MediaStreamTrack, vararg streams: MediaStream): RtpSender { require(track is MediaStreamTrackImpl) val platformSender = platform.addTrack(track.platform, *Array(streams.size) { streams[it].js }) return RtpSender(platformSender) } - /** - * Stops sending media from sender. The sender will still appear in getSenders. Future - * calls to createOffer will mark the m section for the corresponding transceiver as - * receive only or inactive, as defined in JSEP. Returns true on success. - */ actual fun removeTrack(sender: RtpSender): Boolean { return runCatching { platform.removeTrack(sender.js) } .onFailure { Console.error("Remove track failed: $it") } @@ -214,9 +183,6 @@ actual class PeerConnection actual constructor(rtcConfiguration: RtcConfiguratio .getOrDefault(false) } - /** - * Gets stats using the new stats collection API, see webrtc/api/stats/. - */ actual suspend fun getStats(): RtcStatsReport? { return runCatching { platform.getStats() } .map { RtcStatsReport() } @@ -224,22 +190,6 @@ actual class PeerConnection actual constructor(rtcConfiguration: RtcConfiguratio .getOrNull() } - /** - * Free native resources associated with this PeerConnection instance. - * - * This method removes a reference count from the C++ PeerConnection object, - * which should result in it being destroyed. It also calls equivalent - * "dispose" methods on the Java objects attached to this PeerConnection - * (streams, senders, receivers), such that their associated C++ objects - * will also be destroyed. - * - * - * Note that this method cannot be safely called from an observer callback - * (PeerConnection.Observer, DataChannel.Observer, etc.). If you want to, for - * example, destroy the PeerConnection after an "ICE failed" callback, you - * must do this asynchronously (in other words, unwind the stack first). See - * [bug 3721](https://bugs.chromium.org/p/webrtc/issues/detail?id=3721) for more details. - */ actual fun close() { platform.close() scope.launch { From 137ab910af208ac9b92b537a3d28ae520199abad Mon Sep 17 00:00:00 2001 From: Alex Shepeliev Date: Wed, 8 May 2024 11:04:09 +0300 Subject: [PATCH 12/16] Remove unused code --- .../internal/JsMediaTrackConstraintsExt.kt | 72 ------------------- 1 file changed, 72 deletions(-) delete mode 100644 webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/JsMediaTrackConstraintsExt.kt diff --git a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/JsMediaTrackConstraintsExt.kt b/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/JsMediaTrackConstraintsExt.kt deleted file mode 100644 index 4cfa3a9e..00000000 --- a/webrtc-kmp/src/jsAndWasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/JsMediaTrackConstraintsExt.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.shepeliev.webrtckmp.internal - -//internal fun ConstrainBooleanParameters?.asCommon(): ValueOrConstrain.Constrain? { -// this ?: return null -// return ValueOrConstrain.Constrain(exact, ideal) -//} -// -//internal fun ConstrainDOMStringParameters?.asCommon(): ValueOrConstrain.Constrain? { -// this ?: return null -// return ValueOrConstrain.Constrain(exact as? String, ideal as? String) -//} -// -//internal fun ConstrainDoubleRange?.asCommon(): ValueOrConstrain.Constrain? { -// this ?: return null -// return ValueOrConstrain.Constrain(exact, ideal) -//} -// -//internal fun ConstrainULongRange?.asCommon(): ValueOrConstrain.Constrain? { -// this ?: return null -// return ValueOrConstrain.Constrain(exact, ideal) -//} -// -//internal fun ConstrainDOMStringParameters?.asCommonFacingModeConstrain(): ValueOrConstrain.Constrain? { -// this ?: return null -// return ValueOrConstrain.Constrain( -// (exact as? String)?.asCommonFacingMode(), -// (ideal as? String)?.asCommonFacingMode() -// ) -//} - -//@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") -//internal fun JsMediaTrackConstraints.asCommon() = MediaTrackConstraints( -// aspectRatio = (aspectRatio as? Double)?.asValueConstrain() -// ?: (aspectRatio as? ConstrainDoubleRange)?.asCommon(), -// -// autoGainControl = (autoGainControl as? Boolean)?.asValueConstrain() -// ?: (autoGainControl as? ConstrainBooleanParameters)?.asCommon(), -// -// channelCount = (channelCount as? Number)?.toInt()?.asValueConstrain() -// ?: (channelCount as? ConstrainULongRange)?.asCommon(), -// -// deviceId = (deviceId as? String) ?: (deviceId.exact ?: deviceId.ideal) as? String, -// -// echoCancellation = (echoCancellation as? Boolean)?.asValueConstrain() -// ?: (echoCancellation as? ConstrainBooleanParameters)?.asCommon(), -// -// facingMode = (facingMode as? String)?.asCommonFacingMode()?.asValueConstrain() -// ?: (facingMode as? ConstrainDOMStringParameters).asCommonFacingModeConstrain(), -// -// frameRate = (frameRate as? Double)?.asValueConstrain() -// ?: (frameRate as? ConstrainDoubleRange)?.asCommon(), -// -// groupId = (groupId as? String) ?: (groupId.exact ?: groupId.ideal) as? String, -// -// height = (height as? Number)?.toInt()?.asValueConstrain() -// ?: (height as? ConstrainULongRange)?.asCommon(), -// -// latency = (latency as? Double)?.asValueConstrain() -// ?: (latency as? ConstrainDoubleRange)?.asCommon(), -// -// noiseSuppression = (noiseSuppression as? Boolean)?.asValueConstrain() -// ?: (noiseSuppression as? ConstrainBooleanParameters)?.asCommon(), -// -// sampleRate = (sampleRate as? Number)?.toInt()?.asValueConstrain() -// ?: (sampleRate as? ConstrainULongRange)?.asCommon(), -// -// sampleSize = (sampleSize as? Number)?.toInt()?.asValueConstrain() -// ?: (sampleSize as? ConstrainULongRange)?.asCommon(), -// -// width = (width as? Number)?.toInt()?.asValueConstrain() -// ?: (width as? ConstrainULongRange)?.asCommon(), -//) From 4bc32aa3830757138119aba572478d659b320ab0 Mon Sep 17 00:00:00 2001 From: Alex Shepeliev Date: Wed, 8 May 2024 11:09:24 +0300 Subject: [PATCH 13/16] Update TODO comment --- .../kotlin/com/shepeliev/webrtckmp/internal/Promise.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/Promise.kt b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/Promise.kt index 64eb833b..bf6436c1 100644 --- a/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/Promise.kt +++ b/webrtc-kmp/src/wasmJsMain/kotlin/com/shepeliev/webrtckmp/internal/Promise.kt @@ -6,7 +6,8 @@ import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.js.Promise -// TODO: replace it by kotlinx.coroutines.await after releasing https://github.com/Kotlin/kotlinx.coroutines/pull/4120 +// TODO: remove after fixing in kotlinx.coroutines +// PR: https://github.com/Kotlin/kotlinx.coroutines/pull/4120 @Suppress("UNCHECKED_CAST") suspend fun Promise.await(): T = suspendCancellableCoroutine { cont: CancellableContinuation -> this@await.then( From 8545e14c3c79984efab3c94ce1fbbf364f5958c7 Mon Sep 17 00:00:00 2001 From: Alex Shepeliev Date: Wed, 8 May 2024 16:34:14 +0300 Subject: [PATCH 14/16] Enable DataChannel test --- .../shepeliev/webrtckmp/DataChannelTest.kt | 129 +++++++++++------- .../com/shepeliev/webrtckmp/DataChannel.kt | 81 ++++++++--- 2 files changed, 146 insertions(+), 64 deletions(-) diff --git a/webrtc-kmp/src/commonTest/kotlin/com/shepeliev/webrtckmp/DataChannelTest.kt b/webrtc-kmp/src/commonTest/kotlin/com/shepeliev/webrtckmp/DataChannelTest.kt index 675a595d..9b337b6b 100644 --- a/webrtc-kmp/src/commonTest/kotlin/com/shepeliev/webrtckmp/DataChannelTest.kt +++ b/webrtc-kmp/src/commonTest/kotlin/com/shepeliev/webrtckmp/DataChannelTest.kt @@ -1,65 +1,65 @@ package com.shepeliev.webrtckmp +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.withTimeout -import kotlin.test.Ignore +import kotlinx.coroutines.test.setMain +import kotlin.test.AfterTest +import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds -// TODO fix flaky test -@Ignore +@OptIn(ExperimentalCoroutinesApi::class) class DataChannelTest { - @Test - fun data_channel_should_work() = runTest { - val jobs = mutableListOf() + private val scope = TestScope() + + @BeforeTest + fun setUp() { + Dispatchers.setMain(StandardTestDispatcher(scope.testScheduler)) + } + + @AfterTest + fun tearDown() { + Dispatchers.resetMain() + } + @Test + fun data_channel_should_work() = runTest(timeout = 5.seconds) { val pc1 = PeerConnection() val pc2 = PeerConnection() - val dataChannel = pc1.createDataChannel("dataChannel")!! - val message = async { - withTimeout(5000) { dataChannel.onMessage.map { it.decodeToString() }.first() } + val pc1DataChannel = pc1.createDataChannel("dataChannel", maxRetransmits = 10)!! + val pc2DataChannelDeferred = async(start = CoroutineStart.UNDISPATCHED) { pc2.onDataChannel.first() } + + val pc1IceCandidates = async(start = CoroutineStart.UNDISPATCHED) { + val candidates = mutableListOf() + val job = pc1.onIceCandidate.onEach { candidates += it }.launchIn(this) + pc1.onIceGatheringState.first { it == IceGatheringState.Complete } + job.cancel() + candidates } - val pc1Candidates = mutableListOf() - val pc2Candidates = mutableListOf() - - jobs += pc1.onIceCandidate - .onEach { candidate -> - if (pc2.signalingState == SignalingState.Stable) { - pc1Candidates.forEach { pc2.addIceCandidate(it) } - pc1Candidates.clear() - pc2.addIceCandidate(candidate) - } else { - pc1Candidates += candidate - } - } - .launchIn(this) - - jobs += pc2.onIceCandidate - .onEach { candidate -> - if (pc1.signalingState == SignalingState.Stable) { - pc2Candidates.forEach { pc1.addIceCandidate(it) } - pc2Candidates.clear() - pc1.addIceCandidate(candidate) - } else { - pc2Candidates += candidate - } - } - .launchIn(this) - - jobs += pc2.onDataChannel - .onEach { pc2DataChannel -> - val data = "Hello WebRTC KMP!".encodeToByteArray() - pc2DataChannel.send(data) - } - .launchIn(this) + val pc2IceCandidates = async(start = CoroutineStart.UNDISPATCHED) { + val candidates = mutableListOf() + val job = pc2.onIceCandidate.onEach { candidates += it }.launchIn(this) + pc2.onIceGatheringState.first { it == IceGatheringState.Complete } + job.cancel() + candidates + } val offer = pc1.createOffer(OfferAnswerOptions()) pc1.setLocalDescription(offer) @@ -68,11 +68,48 @@ class DataChannelTest { pc2.setLocalDescription(answer) pc1.setRemoteDescription(answer) - assertEquals("Hello WebRTC KMP!", message.await()) + pc1IceCandidates.await().forEach { pc2.addIceCandidate(it) } + pc2IceCandidates.await().forEach { pc1.addIceCandidate(it) } + + val pc2DataChannel = pc2DataChannelDeferred.await() + if (pc2DataChannel.readyState != DataChannelState.Open) { + println("Waiting for pc2DataChannel: ${pc2DataChannel.readyState}") + pc2DataChannel.onOpen.first() + } + + if (pc1DataChannel.readyState != DataChannelState.Open) { + println("Waiting for pc1DataChannel: ${pc1DataChannel.readyState}") + pc1DataChannel.onOpen.first() + } + + // TODO: This fails in iOS simulator test +// val pc1MessageDeferred = async(start = CoroutineStart.UNDISPATCHED) { +// pc1DataChannel.onMessage +// .onEach { println("Message received PC1: ${it.decodeToString()}") } +// .map { it.decodeToString() } +// .first() +// } + + val pc2MessageDeferred = async(start = CoroutineStart.UNDISPATCHED) { + pc2DataChannel.onMessage + .onEach { println("Message received PC2: ${it.decodeToString()}") } + .map { it.decodeToString() } + .first() + } + + val data = "Hello WebRTC KMP!".encodeToByteArray() + + assertTrue { pc1DataChannel.send(data) } + assertTrue { pc2DataChannel.send(data) } + + assertEquals("Hello WebRTC KMP!", pc2MessageDeferred.await()) + + // TODO: This fails in iOS simulator test +// assertEquals("Hello WebRTC KMP!", pc1MessageDeferred.await()) pc1.close() pc2.close() - - jobs.forEach(Job::cancel) + pc1DataChannel.close() + pc2DataChannel.close() } } diff --git a/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/DataChannel.kt b/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/DataChannel.kt index f8180154..da018ec9 100644 --- a/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/DataChannel.kt +++ b/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/DataChannel.kt @@ -7,19 +7,21 @@ import WebRTC.RTCDataChannel import WebRTC.RTCDataChannelDelegateProtocol import WebRTC.RTCDataChannelState import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch import platform.darwin.NSObject +import platform.darwin.dispatch_async +import platform.darwin.dispatch_get_main_queue import platform.posix.uint64_t actual class DataChannel(val ios: RTCDataChannel) { - actual val label: String get() = ios.label() @@ -32,22 +34,25 @@ actual class DataChannel(val ios: RTCDataChannel) { actual val bufferedAmount: Long get() = ios.bufferedAmount.toLong() - private val dataChannelEvent = callbackFlow { + private val coroutineScope = MainScope() + private val dataChannelEvent = MutableSharedFlow() + + init { ios.delegate = object : NSObject(), RTCDataChannelDelegateProtocol { override fun dataChannel(dataChannel: RTCDataChannel, didChangeBufferedAmount: uint64_t) { // not implemented } override fun dataChannel(dataChannel: RTCDataChannel, didReceiveMessageWithBuffer: RTCDataBuffer) { - trySendBlocking(DataChannelEvent.MessageReceived(didReceiveMessageWithBuffer)) + coroutineScope.launch { + dataChannelEvent.emit(DataChannelEvent.MessageReceived(didReceiveMessageWithBuffer)) + } } override fun dataChannelDidChangeState(dataChannel: RTCDataChannel) { - trySendBlocking(DataChannelEvent.StateChanged) + coroutineScope.launch { dataChannelEvent.emit(DataChannelEvent.StateChanged) } } } - - awaitClose { ios.delegate = null } } actual val onOpen: Flow = dataChannelEvent @@ -74,20 +79,60 @@ actual class DataChannel(val ios: RTCDataChannel) { return ios.sendData(buffer) } - actual fun close() = ios.close() + actual fun close() { + ios.close() + coroutineScope.launch { + dataChannelEvent.emit(DataChannelEvent.StateChanged) + coroutineScope.cancel() + } + } private sealed interface DataChannelEvent { - object StateChanged : DataChannelEvent + data object StateChanged : DataChannelEvent data class MessageReceived(val buffer: RTCDataBuffer) : DataChannelEvent } + + private fun rtcDataChannelStateAsCommon(state: RTCDataChannelState): DataChannelState { + return when (state) { + RTCDataChannelState.RTCDataChannelStateConnecting -> DataChannelState.Connecting + RTCDataChannelState.RTCDataChannelStateOpen -> DataChannelState.Open + RTCDataChannelState.RTCDataChannelStateClosing -> DataChannelState.Closing + RTCDataChannelState.RTCDataChannelStateClosed -> DataChannelState.Closed + else -> error("Unknown RTCDataChannelState: $state") + } + } } -private fun rtcDataChannelStateAsCommon(state: RTCDataChannelState): DataChannelState { - return when (state) { - RTCDataChannelState.RTCDataChannelStateConnecting -> DataChannelState.Connecting - RTCDataChannelState.RTCDataChannelStateOpen -> DataChannelState.Open - RTCDataChannelState.RTCDataChannelStateClosing -> DataChannelState.Closing - RTCDataChannelState.RTCDataChannelStateClosed -> DataChannelState.Closed - else -> error("Unknown RTCDataChannelState: $state") +private class ProxyDataChannelDelegate : NSObject(), RTCDataChannelDelegateProtocol { + private val delegates: MutableSet = mutableSetOf() + + fun addDelegate(delegate: RTCDataChannelDelegateProtocol) { + dispatch_async(dispatch_get_main_queue()) { + delegates.add(delegate) + } + } + + fun removeDelegate(delegate: RTCDataChannelDelegateProtocol) { + dispatch_async(dispatch_get_main_queue()) { + delegates.remove(delegate) + } + } + + override fun dataChannel(dataChannel: RTCDataChannel, didChangeBufferedAmount: uint64_t) { + dispatch_async(dispatch_get_main_queue()) { + delegates.forEach { it.dataChannel(dataChannel, didChangeBufferedAmount) } + } + } + + override fun dataChannel(dataChannel: RTCDataChannel, didReceiveMessageWithBuffer: RTCDataBuffer) { + dispatch_async(dispatch_get_main_queue()) { + delegates.forEach { it.dataChannel(dataChannel, didReceiveMessageWithBuffer) } + } + } + + override fun dataChannelDidChangeState(dataChannel: RTCDataChannel) { + dispatch_async(dispatch_get_main_queue()) { + delegates.forEach { it.dataChannelDidChangeState(dataChannel) } + } } } From 45af3f569662ccbae2d6b4b951cb86befe669e43 Mon Sep 17 00:00:00 2001 From: Alex Shepeliev Date: Wed, 8 May 2024 19:57:37 +0300 Subject: [PATCH 15/16] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ec97b599..940d2f11 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ It supports Android, iOS, JS. Other platforms - PRs are welcome. ## API implementation map - API | Android | iOS | JS + API | Android | iOS | JS/WasmJS :-: |:------------------:| :-: | :---: Audio/Video | :white_check_mark: | :white_check_mark: | :white_check_mark: Data channel | :white_check_mark: | :white_check_mark: | :white_check_mark: From 76c4d32bd1bb98655ee16d0c3745dee0e3ac0713 Mon Sep 17 00:00:00 2001 From: Alex Shepeliev Date: Wed, 8 May 2024 19:59:13 +0300 Subject: [PATCH 16/16] Remove unused code --- .../com/shepeliev/webrtckmp/DataChannel.kt | 36 ------------------- 1 file changed, 36 deletions(-) diff --git a/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/DataChannel.kt b/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/DataChannel.kt index da018ec9..6fa74984 100644 --- a/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/DataChannel.kt +++ b/webrtc-kmp/src/iosMain/kotlin/com/shepeliev/webrtckmp/DataChannel.kt @@ -17,8 +17,6 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import platform.darwin.NSObject -import platform.darwin.dispatch_async -import platform.darwin.dispatch_get_main_queue import platform.posix.uint64_t actual class DataChannel(val ios: RTCDataChannel) { @@ -102,37 +100,3 @@ actual class DataChannel(val ios: RTCDataChannel) { } } } - -private class ProxyDataChannelDelegate : NSObject(), RTCDataChannelDelegateProtocol { - private val delegates: MutableSet = mutableSetOf() - - fun addDelegate(delegate: RTCDataChannelDelegateProtocol) { - dispatch_async(dispatch_get_main_queue()) { - delegates.add(delegate) - } - } - - fun removeDelegate(delegate: RTCDataChannelDelegateProtocol) { - dispatch_async(dispatch_get_main_queue()) { - delegates.remove(delegate) - } - } - - override fun dataChannel(dataChannel: RTCDataChannel, didChangeBufferedAmount: uint64_t) { - dispatch_async(dispatch_get_main_queue()) { - delegates.forEach { it.dataChannel(dataChannel, didChangeBufferedAmount) } - } - } - - override fun dataChannel(dataChannel: RTCDataChannel, didReceiveMessageWithBuffer: RTCDataBuffer) { - dispatch_async(dispatch_get_main_queue()) { - delegates.forEach { it.dataChannel(dataChannel, didReceiveMessageWithBuffer) } - } - } - - override fun dataChannelDidChangeState(dataChannel: RTCDataChannel) { - dispatch_async(dispatch_get_main_queue()) { - delegates.forEach { it.dataChannelDidChangeState(dataChannel) } - } - } -}