Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion karma/chrome_bin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,6 @@

package io.ktor.client.webrtc

import js.array.JsArray
import js.core.JsAny
import web.errors.DOMException

internal actual fun <T : JsAny?> JsArray<T>.toArray(): Array<T> = this.copyOf()

internal actual fun <T : JsAny?> List<T>.toJs(): JsArray<T> = toTypedArray()

internal actual fun Throwable.asDomException(): DOMException? = this as? DOMException
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -25,7 +32,7 @@ internal fun WebRtcConnectionConfig.toJs(): RTCConfiguration = unsafeJso {
bundlePolicy = [email protected]()
rtcpMuxPolicy = [email protected]()
iceTransportPolicy = [email protected]()
iceServers = [email protected] { it.toJs() }.toJs()
iceServers = [email protected] { it.toJs() }.toJsArray()
iceCandidatePoolSize = [email protected]()
}

Expand Down Expand Up @@ -201,12 +208,8 @@ internal fun WebRtcDataChannelOptions.toJs(): RTCDataChannelInit = unsafeJso {
ordered = [email protected]
protocol = [email protected]
negotiated = [email protected]
if ([email protected] != null) {
maxRetransmits = [email protected]!!.toShort()
}
if ([email protected] != null) {
maxPacketLifeTime = [email protected]?.inWholeMilliseconds?.toShort()
}
[email protected]?.let { maxRetransmits = it.toShort() }
[email protected]?.let { maxPacketLifeTime = it.inWholeMilliseconds.toShort() }
}

/**
Expand All @@ -216,6 +219,7 @@ internal fun WebRtcDataChannelOptions.toJs(): RTCDataChannelInit = unsafeJso {
internal fun RTCStatsReport.toKtor(): List<WebRtc.Stats> {
val statsList = mutableListOf<WebRtc.Stats>()
forEach { value, _ ->
@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE")
val rtcStats = value as RTCStats
statsList.add(rtcStats.toKtor())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Expand All @@ -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))
Expand Down
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,9 +16,7 @@ import web.events.Event
import web.events.EventHandler
import web.events.EventTarget
import web.events.HasTargets

internal expect fun <T : JsAny?> JsArray<T>.toArray(): Array<T>
internal expect fun <T : JsAny?> List<T>.toJs(): JsArray<T>
import kotlin.js.ExperimentalWasmJsInterop

internal inline fun <T> withSdpException(message: String, block: () -> T): T {
try {
Expand Down Expand Up @@ -56,19 +50,6 @@ internal inline fun <T> withPermissionException(mediaType: String, block: () ->
}
}

internal fun ByteArray.toArrayBuffer(): ArrayBuffer {
val array = Int8Array<ArrayBuffer>(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 <E : Event, C : EventTarget, T : EventTarget, D> eventHandler(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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<WebRtcMedia.Track>()
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<WebRtcMedia.Track>()
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() }
}
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@file:OptIn(ExperimentalWasmJsInterop::class)
package io.ktor.client.webrtc

import web.audio.AudioContext
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <T : JsAny?> JsArray<T>.toArray(): Array<T> = toKotlinArray()

internal actual fun <T : JsAny?> List<T>.toJs(): JsArray<T> = toJsArray()

internal actual fun Throwable.asDomException(): DOMException? {
return (this as? JsException)?.thrownValue as? DOMException
Expand Down