diff --git a/build-settings-logic/src/main/kotlin/ktorsettings.dependency-resolution-management.settings.gradle.kts b/build-settings-logic/src/main/kotlin/ktorsettings.dependency-resolution-management.settings.gradle.kts index e583120b3d9..dfe368bf86d 100644 --- a/build-settings-logic/src/main/kotlin/ktorsettings.dependency-resolution-management.settings.gradle.kts +++ b/build-settings-logic/src/main/kotlin/ktorsettings.dependency-resolution-management.settings.gradle.kts @@ -26,7 +26,7 @@ dependencyResolutionManagement { } create("kotlinWrappers") { - from("org.jetbrains.kotlin-wrappers:kotlin-wrappers-catalog:2025.7.10") + from("org.jetbrains.kotlin-wrappers:kotlin-wrappers-catalog:2025.10.8") } } } diff --git a/karma/chrome_bin.js b/karma/chrome_bin.js index 9a0cea2858f..6562256a2a7 100644 --- a/karma/chrome_bin.js +++ b/karma/chrome_bin.js @@ -14,7 +14,9 @@ config.set({ "--disable-web-security", "--disable-setuid-sandbox", "--enable-logging", - "--v=1" + "--v=1", + "--use-fake-device-for-media-stream", + "--use-fake-ui-for-media-stream" ] } }, diff --git a/ktor-client/ktor-client-webrtc/js/src/io/ktor/client/webrtc/Utils.js.kt b/ktor-client/ktor-client-webrtc/js/src/io/ktor/client/webrtc/Utils.js.kt index f47ccf3c9c8..2dcb065a2d9 100644 --- a/ktor-client/ktor-client-webrtc/js/src/io/ktor/client/webrtc/Utils.js.kt +++ b/ktor-client/ktor-client-webrtc/js/src/io/ktor/client/webrtc/Utils.js.kt @@ -4,12 +4,6 @@ package io.ktor.client.webrtc -import js.array.JsArray -import js.core.JsAny import web.errors.DOMException -internal actual fun JsArray.toArray(): Array = this.copyOf() - -internal actual fun List.toJs(): JsArray = toTypedArray() - internal actual fun Throwable.asDomException(): DOMException? = this as? DOMException diff --git a/ktor-client/ktor-client-webrtc/jsAndWasmShared/src/io/ktor/client/webrtc/Browser.kt b/ktor-client/ktor-client-webrtc/jsAndWasmShared/src/io/ktor/client/webrtc/Browser.kt index 293d37befc5..263ee1568cc 100644 --- a/ktor-client/ktor-client-webrtc/jsAndWasmShared/src/io/ktor/client/webrtc/Browser.kt +++ b/ktor-client/ktor-client-webrtc/jsAndWasmShared/src/io/ktor/client/webrtc/Browser.kt @@ -1,22 +1,29 @@ /* * Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ +@file:OptIn(ExperimentalWasmJsInterop::class) + package io.ktor.client.webrtc import js.array.component1 import js.array.component2 import js.core.JsPrimitives.toDouble -import js.core.JsPrimitives.toJsBoolean import js.core.JsPrimitives.toJsDouble import js.core.JsPrimitives.toJsInt -import js.core.JsPrimitives.toJsString -import js.core.JsString import js.objects.Object import js.objects.unsafeJso import js.reflect.unsafeCast import web.mediastreams.ConstrainDouble import web.mediastreams.MediaTrackConstraints import web.rtc.* +import kotlin.collections.associate +import kotlin.js.ExperimentalWasmJsInterop +import kotlin.js.JsString +import kotlin.js.toArray +import kotlin.js.toJsArray +import kotlin.js.toJsBoolean +import kotlin.js.toJsString +import kotlin.toString // Mapping from Browser interfaces for the web platform // TODO: add missing fields in the `kotlin-wrappers` api @@ -25,7 +32,7 @@ internal fun WebRtcConnectionConfig.toJs(): RTCConfiguration = unsafeJso { bundlePolicy = this@toJs.bundlePolicy.toJs() rtcpMuxPolicy = this@toJs.rtcpMuxPolicy.toJs() iceTransportPolicy = this@toJs.iceTransportPolicy.toJs() - iceServers = this@toJs.iceServers.map { it.toJs() }.toJs() + iceServers = this@toJs.iceServers.map { it.toJs() }.toJsArray() iceCandidatePoolSize = this@toJs.iceCandidatePoolSize.toShort() } @@ -201,12 +208,8 @@ internal fun WebRtcDataChannelOptions.toJs(): RTCDataChannelInit = unsafeJso { ordered = this@toJs.ordered protocol = this@toJs.protocol negotiated = this@toJs.negotiated - if (this@toJs.maxRetransmits != null) { - maxRetransmits = this@toJs.maxRetransmits!!.toShort() - } - if (this@toJs.maxPacketLifeTime != null) { - maxPacketLifeTime = this@toJs.maxPacketLifeTime?.inWholeMilliseconds?.toShort() - } + this@toJs.maxRetransmits?.let { maxRetransmits = it.toShort() } + this@toJs.maxPacketLifeTime?.let { maxPacketLifeTime = it.inWholeMilliseconds.toShort() } } /** @@ -216,6 +219,7 @@ internal fun WebRtcDataChannelOptions.toJs(): RTCDataChannelInit = unsafeJso { internal fun RTCStatsReport.toKtor(): List { val statsList = mutableListOf() forEach { value, _ -> + @Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") val rtcStats = value as RTCStats statsList.add(rtcStats.toKtor()) } diff --git a/ktor-client/ktor-client-webrtc/jsAndWasmShared/src/io/ktor/client/webrtc/DataChannel.kt b/ktor-client/ktor-client-webrtc/jsAndWasmShared/src/io/ktor/client/webrtc/DataChannel.kt index 91e53edefe0..d475b86570d 100644 --- a/ktor-client/ktor-client-webrtc/jsAndWasmShared/src/io/ktor/client/webrtc/DataChannel.kt +++ b/ktor-client/ktor-client-webrtc/jsAndWasmShared/src/io/ktor/client/webrtc/DataChannel.kt @@ -5,14 +5,17 @@ package io.ktor.client.webrtc import js.buffer.ArrayBuffer -import js.core.JsString +import js.buffer.toByteArray +import js.typedarrays.toInt8Array import kotlinx.coroutines.CoroutineScope import web.blob.Blob import web.blob.arrayBuffer +import web.buffer.BinaryType +import web.buffer.arraybuffer +import web.buffer.blob import web.rtc.RTCDataChannel -import web.sockets.BinaryType -import web.sockets.arraybuffer -import web.sockets.blob +import kotlin.js.ExperimentalWasmJsInterop +import kotlin.js.JsString /** * WebRtc data channel implementation for the JavaScript platform. @@ -60,7 +63,7 @@ public class JsWebRtcDataChannel( } override suspend fun send(bytes: ByteArray) { - channel.send(bytes.toArrayBuffer()) + channel.send(bytes.toInt8Array()) } override fun setBufferedAmountLowThreshold(threshold: Long) { @@ -71,6 +74,7 @@ public class JsWebRtcDataChannel( channel.close() } + @OptIn(ExperimentalWasmJsInterop::class) internal fun setupEvents(eventsEmitter: WebRtcConnectionEventsEmitter) { channel.onopen = eventHandler(coroutineScope) { eventsEmitter.emitDataChannelEvent(DataChannelEvent.Open(this)) diff --git a/ktor-client/ktor-client-webrtc/jsAndWasmShared/src/io/ktor/client/webrtc/MediaDevices.kt b/ktor-client/ktor-client-webrtc/jsAndWasmShared/src/io/ktor/client/webrtc/MediaDevices.kt index f56851b19c8..a6138044385 100644 --- a/ktor-client/ktor-client-webrtc/jsAndWasmShared/src/io/ktor/client/webrtc/MediaDevices.kt +++ b/ktor-client/ktor-client-webrtc/jsAndWasmShared/src/io/ktor/client/webrtc/MediaDevices.kt @@ -1,6 +1,7 @@ /* * Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ +@file:OptIn(ExperimentalWasmJsInterop::class) package io.ktor.client.webrtc @@ -10,6 +11,8 @@ import web.mediastreams.MediaStreamConstraints import web.mediastreams.MediaStreamTrack import web.mediastreams.MediaTrackConstraints import web.navigator.navigator +import kotlin.js.ExperimentalWasmJsInterop +import kotlin.js.toArray import kotlin.js.undefined private fun makeStreamConstraints( diff --git a/ktor-client/ktor-client-webrtc/jsAndWasmShared/src/io/ktor/client/webrtc/PeerConnection.kt b/ktor-client/ktor-client-webrtc/jsAndWasmShared/src/io/ktor/client/webrtc/PeerConnection.kt index dc135374c38..a601231d802 100644 --- a/ktor-client/ktor-client-webrtc/jsAndWasmShared/src/io/ktor/client/webrtc/PeerConnection.kt +++ b/ktor-client/ktor-client-webrtc/jsAndWasmShared/src/io/ktor/client/webrtc/PeerConnection.kt @@ -1,12 +1,14 @@ /* * Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ - +@file:OptIn(ExperimentalWasmJsInterop::class) package io.ktor.client.webrtc import web.mediastreams.MediaStream import web.rtc.* import kotlin.coroutines.CoroutineContext +import kotlin.js.ExperimentalWasmJsInterop +import kotlin.js.toArray /** * WebRtc peer connection implementation for JavaScript platform. diff --git a/ktor-client/ktor-client-webrtc/jsAndWasmShared/src/io/ktor/client/webrtc/Rtp.kt b/ktor-client/ktor-client-webrtc/jsAndWasmShared/src/io/ktor/client/webrtc/Rtp.kt index cffefc1133a..a16feab72a3 100644 --- a/ktor-client/ktor-client-webrtc/jsAndWasmShared/src/io/ktor/client/webrtc/Rtp.kt +++ b/ktor-client/ktor-client-webrtc/jsAndWasmShared/src/io/ktor/client/webrtc/Rtp.kt @@ -1,6 +1,8 @@ /* * Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ +@file:OptIn(ExperimentalWasmJsInterop::class) + package io.ktor.client.webrtc import web.rtc.RTCDTMFSender @@ -11,6 +13,8 @@ import web.rtc.RTCRtpSendParameters import web.rtc.RTCRtpSender import web.rtc.replaceTrack import web.rtc.setParameters +import kotlin.js.ExperimentalWasmJsInterop +import kotlin.js.toArray /** * Wrapper for RTCRtpSender. diff --git a/ktor-client/ktor-client-webrtc/jsAndWasmShared/src/io/ktor/client/webrtc/Utils.kt b/ktor-client/ktor-client-webrtc/jsAndWasmShared/src/io/ktor/client/webrtc/Utils.kt index 91027c350a1..4735edd51c7 100644 --- a/ktor-client/ktor-client-webrtc/jsAndWasmShared/src/io/ktor/client/webrtc/Utils.kt +++ b/ktor-client/ktor-client-webrtc/jsAndWasmShared/src/io/ktor/client/webrtc/Utils.kt @@ -2,14 +2,10 @@ * Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ +@file:OptIn(ExperimentalWasmJsInterop::class) + package io.ktor.client.webrtc -import js.array.JsArray -import js.buffer.ArrayBuffer -import js.core.JsAny -import js.core.JsPrimitives.toByte -import js.core.JsPrimitives.toJsByte -import js.typedarrays.Int8Array import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.launch @@ -20,9 +16,7 @@ import web.events.Event import web.events.EventHandler import web.events.EventTarget import web.events.HasTargets - -internal expect fun JsArray.toArray(): Array -internal expect fun List.toJs(): JsArray +import kotlin.js.ExperimentalWasmJsInterop internal inline fun withSdpException(message: String, block: () -> T): T { try { @@ -56,19 +50,6 @@ internal inline fun withPermissionException(mediaType: String, block: () -> } } -internal fun ByteArray.toArrayBuffer(): ArrayBuffer { - val array = Int8Array(size) - repeat(size) { i -> - array[i] = this[i].toJsByte() - } - return array.buffer -} - -internal fun ArrayBuffer.toByteArray(): ByteArray { - val arr = Int8Array(this) - return ByteArray(byteLength) { arr[it].toByte() } -} - // A helper to run the event handler in the coroutine scope @Suppress("BOUNDS_NOT_ALLOWED_IF_BOUNDED_BY_TYPE_PARAMETER") internal fun eventHandler( diff --git a/ktor-client/ktor-client-webrtc/jsAndWasmShared/test/io/ktor/client/webrtc/JsWebRtcMediaTest.kt b/ktor-client/ktor-client-webrtc/jsAndWasmShared/test/io/ktor/client/webrtc/JsWebRtcMediaTest.kt index e29c2bf8ad7..c8b1ef1957e 100644 --- a/ktor-client/ktor-client-webrtc/jsAndWasmShared/test/io/ktor/client/webrtc/JsWebRtcMediaTest.kt +++ b/ktor-client/ktor-client-webrtc/jsAndWasmShared/test/io/ktor/client/webrtc/JsWebRtcMediaTest.kt @@ -2,12 +2,13 @@ * Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ +@file:OptIn(ExperimentalWasmJsInterop::class) + package io.ktor.client.webrtc import io.ktor.utils.io.* import kotlinx.coroutines.test.runTest -import web.errors.DOMException -import web.errors.NotFoundError +import kotlin.js.ExperimentalWasmJsInterop import kotlin.test.* @OptIn(ExperimentalKtorApi::class) @@ -25,59 +26,56 @@ class JsWebRtcMediaTest { client.close() } - private inline fun assertNoMediaDevice(block: () -> Unit) { - try { - block() - assertTrue(false, "Expected NotFoundError to be thrown") - } catch (e: Throwable) { - val exception = e.asDomException() ?: throw e - assertEquals(DOMException.NotFoundError, exception.name, "Expected NotFoundError") - } - } + private fun WebRtcMedia.Track.getSettings() = getNative().getSettings() @Test fun testCreateAudioTrackConstraints() = runTest { - // Assert that constraints are mapped correctly, though ChromeHeadless does not have any media devices - assertNoMediaDevice { - client.createAudioTrack() - } - assertNoMediaDevice { - client.createAudioTrack { + val tracks = mutableListOf() + try { + tracks.add(client.createAudioTrack()) + + val audioTrack1 = client.createAudioTrack { autoGainControl = true echoCancellation = true - noiseSuppression = true - latency = 1.0 - channelCount = 1 - sampleRate = 10 - volume = 0.5 - } - } - // check overloading - assertNoMediaDevice { - val constraints = WebRtcMedia.AudioTrackConstraints(echoCancellation = true) - client.createAudioTrack(constraints) + }.also { tracks.add(it) } + + val settings1 = audioTrack1.getSettings() + assertEquals(true, settings1.autoGainControl) + assertEquals(true, settings1.echoCancellation.toString().toBoolean()) + + // check overloading + val constraints = WebRtcMedia.AudioTrackConstraints(autoGainControl = false) + val audioTrack2 = client.createAudioTrack(constraints).also { tracks.add(it) } + val settings2 = audioTrack2.getSettings() + assertEquals(false, settings2.autoGainControl) + } finally { + tracks.forEach { it.close() } } } @Test fun testCreateVideoTrack() = runTest { - // Assert that constraints are mapped correctly, though ChromeHeadless does not have any media devices - assertNoMediaDevice { - client.createVideoTrack() - } - assertNoMediaDevice { - client.createVideoTrack { + val tracks = mutableListOf() + try { + tracks.add(client.createVideoTrack()) + + val videoTrack1 = client.createVideoTrack { width = 100 height = 100 frameRate = 30 facingMode = WebRtcMedia.FacingMode.USER - aspectRatio = 1.4 - } - } - // check overloading - assertNoMediaDevice { - val constraints = WebRtcMedia.VideoTrackConstraints(height = 100) - client.createVideoTrack(constraints) + }.also { tracks.add(it) } + val settings1 = videoTrack1.getSettings() + assertEquals(100, settings1.width) + assertEquals(100, settings1.height) + + // check overloading + val constraints = WebRtcMedia.VideoTrackConstraints(aspectRatio = 2.0) + val videoTrack2 = client.createVideoTrack(constraints).also { tracks.add(it) } + val settings2 = videoTrack2.getSettings() + assertEquals(2.0, settings2.aspectRatio) + } finally { + tracks.forEach { it.close() } } } } diff --git a/ktor-client/ktor-client-webrtc/jsAndWasmShared/test/io/ktor/client/webrtc/MockMediaDevices.kt b/ktor-client/ktor-client-webrtc/jsAndWasmShared/test/io/ktor/client/webrtc/MockMediaDevices.kt index df06468d585..fe552db3a40 100644 --- a/ktor-client/ktor-client-webrtc/jsAndWasmShared/test/io/ktor/client/webrtc/MockMediaDevices.kt +++ b/ktor-client/ktor-client-webrtc/jsAndWasmShared/test/io/ktor/client/webrtc/MockMediaDevices.kt @@ -1,3 +1,4 @@ +@file:OptIn(ExperimentalWasmJsInterop::class) package io.ktor.client.webrtc import web.audio.AudioContext @@ -6,6 +7,7 @@ import web.canvas.CanvasRenderingContext2D import web.canvas.ID import web.dom.document import web.html.HTMLCanvasElement +import kotlin.js.ExperimentalWasmJsInterop object MockMediaTrackFactory : MediaTrackFactory { private var allowVideo = false diff --git a/ktor-client/ktor-client-webrtc/wasmJs/src/io/ktor/client/webrtc/Utils.wasmJs.kt b/ktor-client/ktor-client-webrtc/wasmJs/src/io/ktor/client/webrtc/Utils.wasmJs.kt index 5eba8f98d51..afdfd2ee650 100644 --- a/ktor-client/ktor-client-webrtc/wasmJs/src/io/ktor/client/webrtc/Utils.wasmJs.kt +++ b/ktor-client/ktor-client-webrtc/wasmJs/src/io/ktor/client/webrtc/Utils.wasmJs.kt @@ -2,14 +2,11 @@ * Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ +@file:OptIn(ExperimentalWasmJsInterop::class) + package io.ktor.client.webrtc import web.errors.DOMException -import kotlin.js.toArray as toKotlinArray - -internal actual fun JsArray.toArray(): Array = toKotlinArray() - -internal actual fun List.toJs(): JsArray = toJsArray() internal actual fun Throwable.asDomException(): DOMException? { return (this as? JsException)?.thrownValue as? DOMException