Skip to content

Commit 08c0440

Browse files
authored
Fix WebRTC JS tests (#5132)
* WebRTC JS Client. Update `kotlin-wrappers`. * WebRTC JS Client. Fix tests. * WebRTC JS Client. Send `Int8Array` instead of `ArrayBuffer` in `JsWebRtcDataChannel`.
1 parent dab3e88 commit 08c0440

File tree

12 files changed

+83
-92
lines changed

12 files changed

+83
-92
lines changed

build-settings-logic/src/main/kotlin/ktorsettings.dependency-resolution-management.settings.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ dependencyResolutionManagement {
2626
}
2727

2828
create("kotlinWrappers") {
29-
from("org.jetbrains.kotlin-wrappers:kotlin-wrappers-catalog:2025.7.10")
29+
from("org.jetbrains.kotlin-wrappers:kotlin-wrappers-catalog:2025.10.8")
3030
}
3131
}
3232
}

karma/chrome_bin.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ config.set({
1414
"--disable-web-security",
1515
"--disable-setuid-sandbox",
1616
"--enable-logging",
17-
"--v=1"
17+
"--v=1",
18+
"--use-fake-device-for-media-stream",
19+
"--use-fake-ui-for-media-stream"
1820
]
1921
}
2022
},

ktor-client/ktor-client-webrtc/js/src/io/ktor/client/webrtc/Utils.js.kt

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,6 @@
44

55
package io.ktor.client.webrtc
66

7-
import js.array.JsArray
8-
import js.core.JsAny
97
import web.errors.DOMException
108

11-
internal actual fun <T : JsAny?> JsArray<T>.toArray(): Array<T> = this.copyOf()
12-
13-
internal actual fun <T : JsAny?> List<T>.toJs(): JsArray<T> = toTypedArray()
14-
159
internal actual fun Throwable.asDomException(): DOMException? = this as? DOMException

ktor-client/ktor-client-webrtc/jsAndWasmShared/src/io/ktor/client/webrtc/Browser.kt

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,29 @@
11
/*
22
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
33
*/
4+
@file:OptIn(ExperimentalWasmJsInterop::class)
5+
46
package io.ktor.client.webrtc
57

68
import js.array.component1
79
import js.array.component2
810
import js.core.JsPrimitives.toDouble
9-
import js.core.JsPrimitives.toJsBoolean
1011
import js.core.JsPrimitives.toJsDouble
1112
import js.core.JsPrimitives.toJsInt
12-
import js.core.JsPrimitives.toJsString
13-
import js.core.JsString
1413
import js.objects.Object
1514
import js.objects.unsafeJso
1615
import js.reflect.unsafeCast
1716
import web.mediastreams.ConstrainDouble
1817
import web.mediastreams.MediaTrackConstraints
1918
import web.rtc.*
19+
import kotlin.collections.associate
20+
import kotlin.js.ExperimentalWasmJsInterop
21+
import kotlin.js.JsString
22+
import kotlin.js.toArray
23+
import kotlin.js.toJsArray
24+
import kotlin.js.toJsBoolean
25+
import kotlin.js.toJsString
26+
import kotlin.toString
2027

2128
// Mapping from Browser interfaces for the web platform
2229
// TODO: add missing fields in the `kotlin-wrappers` api
@@ -25,7 +32,7 @@ internal fun WebRtcConnectionConfig.toJs(): RTCConfiguration = unsafeJso {
2532
bundlePolicy = this@toJs.bundlePolicy.toJs()
2633
rtcpMuxPolicy = this@toJs.rtcpMuxPolicy.toJs()
2734
iceTransportPolicy = this@toJs.iceTransportPolicy.toJs()
28-
iceServers = this@toJs.iceServers.map { it.toJs() }.toJs()
35+
iceServers = this@toJs.iceServers.map { it.toJs() }.toJsArray()
2936
iceCandidatePoolSize = this@toJs.iceCandidatePoolSize.toShort()
3037
}
3138

@@ -201,12 +208,8 @@ internal fun WebRtcDataChannelOptions.toJs(): RTCDataChannelInit = unsafeJso {
201208
ordered = this@toJs.ordered
202209
protocol = this@toJs.protocol
203210
negotiated = this@toJs.negotiated
204-
if (this@toJs.maxRetransmits != null) {
205-
maxRetransmits = this@toJs.maxRetransmits!!.toShort()
206-
}
207-
if (this@toJs.maxPacketLifeTime != null) {
208-
maxPacketLifeTime = this@toJs.maxPacketLifeTime?.inWholeMilliseconds?.toShort()
209-
}
211+
this@toJs.maxRetransmits?.let { maxRetransmits = it.toShort() }
212+
this@toJs.maxPacketLifeTime?.let { maxPacketLifeTime = it.inWholeMilliseconds.toShort() }
210213
}
211214

212215
/**
@@ -216,6 +219,7 @@ internal fun WebRtcDataChannelOptions.toJs(): RTCDataChannelInit = unsafeJso {
216219
internal fun RTCStatsReport.toKtor(): List<WebRtc.Stats> {
217220
val statsList = mutableListOf<WebRtc.Stats>()
218221
forEach { value, _ ->
222+
@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE")
219223
val rtcStats = value as RTCStats
220224
statsList.add(rtcStats.toKtor())
221225
}

ktor-client/ktor-client-webrtc/jsAndWasmShared/src/io/ktor/client/webrtc/DataChannel.kt

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@
55
package io.ktor.client.webrtc
66

77
import js.buffer.ArrayBuffer
8-
import js.core.JsString
8+
import js.buffer.toByteArray
9+
import js.typedarrays.toInt8Array
910
import kotlinx.coroutines.CoroutineScope
1011
import web.blob.Blob
1112
import web.blob.arrayBuffer
13+
import web.buffer.BinaryType
14+
import web.buffer.arraybuffer
15+
import web.buffer.blob
1216
import web.rtc.RTCDataChannel
13-
import web.sockets.BinaryType
14-
import web.sockets.arraybuffer
15-
import web.sockets.blob
17+
import kotlin.js.ExperimentalWasmJsInterop
18+
import kotlin.js.JsString
1619

1720
/**
1821
* WebRtc data channel implementation for the JavaScript platform.
@@ -60,7 +63,7 @@ public class JsWebRtcDataChannel(
6063
}
6164

6265
override suspend fun send(bytes: ByteArray) {
63-
channel.send(bytes.toArrayBuffer())
66+
channel.send(bytes.toInt8Array())
6467
}
6568

6669
override fun setBufferedAmountLowThreshold(threshold: Long) {
@@ -71,6 +74,7 @@ public class JsWebRtcDataChannel(
7174
channel.close()
7275
}
7376

77+
@OptIn(ExperimentalWasmJsInterop::class)
7478
internal fun setupEvents(eventsEmitter: WebRtcConnectionEventsEmitter) {
7579
channel.onopen = eventHandler(coroutineScope) {
7680
eventsEmitter.emitDataChannelEvent(DataChannelEvent.Open(this))

ktor-client/ktor-client-webrtc/jsAndWasmShared/src/io/ktor/client/webrtc/MediaDevices.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/*
22
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
33
*/
4+
@file:OptIn(ExperimentalWasmJsInterop::class)
45

56
package io.ktor.client.webrtc
67

@@ -10,6 +11,8 @@ import web.mediastreams.MediaStreamConstraints
1011
import web.mediastreams.MediaStreamTrack
1112
import web.mediastreams.MediaTrackConstraints
1213
import web.navigator.navigator
14+
import kotlin.js.ExperimentalWasmJsInterop
15+
import kotlin.js.toArray
1316
import kotlin.js.undefined
1417

1518
private fun makeStreamConstraints(

ktor-client/ktor-client-webrtc/jsAndWasmShared/src/io/ktor/client/webrtc/PeerConnection.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
/*
22
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
33
*/
4-
4+
@file:OptIn(ExperimentalWasmJsInterop::class)
55
package io.ktor.client.webrtc
66

77
import web.mediastreams.MediaStream
88
import web.rtc.*
99
import kotlin.coroutines.CoroutineContext
10+
import kotlin.js.ExperimentalWasmJsInterop
11+
import kotlin.js.toArray
1012

1113
/**
1214
* WebRtc peer connection implementation for JavaScript platform.

ktor-client/ktor-client-webrtc/jsAndWasmShared/src/io/ktor/client/webrtc/Rtp.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
/*
22
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
33
*/
4+
@file:OptIn(ExperimentalWasmJsInterop::class)
5+
46
package io.ktor.client.webrtc
57

68
import web.rtc.RTCDTMFSender
@@ -11,6 +13,8 @@ import web.rtc.RTCRtpSendParameters
1113
import web.rtc.RTCRtpSender
1214
import web.rtc.replaceTrack
1315
import web.rtc.setParameters
16+
import kotlin.js.ExperimentalWasmJsInterop
17+
import kotlin.js.toArray
1418

1519
/**
1620
* Wrapper for RTCRtpSender.

ktor-client/ktor-client-webrtc/jsAndWasmShared/src/io/ktor/client/webrtc/Utils.kt

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,10 @@
22
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

5+
@file:OptIn(ExperimentalWasmJsInterop::class)
6+
57
package io.ktor.client.webrtc
68

7-
import js.array.JsArray
8-
import js.buffer.ArrayBuffer
9-
import js.core.JsAny
10-
import js.core.JsPrimitives.toByte
11-
import js.core.JsPrimitives.toJsByte
12-
import js.typedarrays.Int8Array
139
import kotlinx.coroutines.CoroutineScope
1410
import kotlinx.coroutines.CoroutineStart
1511
import kotlinx.coroutines.launch
@@ -20,9 +16,7 @@ import web.events.Event
2016
import web.events.EventHandler
2117
import web.events.EventTarget
2218
import web.events.HasTargets
23-
24-
internal expect fun <T : JsAny?> JsArray<T>.toArray(): Array<T>
25-
internal expect fun <T : JsAny?> List<T>.toJs(): JsArray<T>
19+
import kotlin.js.ExperimentalWasmJsInterop
2620

2721
internal inline fun <T> withSdpException(message: String, block: () -> T): T {
2822
try {
@@ -56,19 +50,6 @@ internal inline fun <T> withPermissionException(mediaType: String, block: () ->
5650
}
5751
}
5852

59-
internal fun ByteArray.toArrayBuffer(): ArrayBuffer {
60-
val array = Int8Array<ArrayBuffer>(size)
61-
repeat(size) { i ->
62-
array[i] = this[i].toJsByte()
63-
}
64-
return array.buffer
65-
}
66-
67-
internal fun ArrayBuffer.toByteArray(): ByteArray {
68-
val arr = Int8Array(this)
69-
return ByteArray(byteLength) { arr[it].toByte() }
70-
}
71-
7253
// A helper to run the event handler in the coroutine scope
7354
@Suppress("BOUNDS_NOT_ALLOWED_IF_BOUNDED_BY_TYPE_PARAMETER")
7455
internal fun <E : Event, C : EventTarget, T : EventTarget, D> eventHandler(

ktor-client/ktor-client-webrtc/jsAndWasmShared/test/io/ktor/client/webrtc/JsWebRtcMediaTest.kt

Lines changed: 39 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

5+
@file:OptIn(ExperimentalWasmJsInterop::class)
6+
57
package io.ktor.client.webrtc
68

79
import io.ktor.utils.io.*
810
import kotlinx.coroutines.test.runTest
9-
import web.errors.DOMException
10-
import web.errors.NotFoundError
11+
import kotlin.js.ExperimentalWasmJsInterop
1112
import kotlin.test.*
1213

1314
@OptIn(ExperimentalKtorApi::class)
@@ -25,59 +26,56 @@ class JsWebRtcMediaTest {
2526
client.close()
2627
}
2728

28-
private inline fun assertNoMediaDevice(block: () -> Unit) {
29-
try {
30-
block()
31-
assertTrue(false, "Expected NotFoundError to be thrown")
32-
} catch (e: Throwable) {
33-
val exception = e.asDomException() ?: throw e
34-
assertEquals(DOMException.NotFoundError, exception.name, "Expected NotFoundError")
35-
}
36-
}
29+
private fun WebRtcMedia.Track.getSettings() = getNative().getSettings()
3730

3831
@Test
3932
fun testCreateAudioTrackConstraints() = runTest {
40-
// Assert that constraints are mapped correctly, though ChromeHeadless does not have any media devices
41-
assertNoMediaDevice {
42-
client.createAudioTrack()
43-
}
44-
assertNoMediaDevice {
45-
client.createAudioTrack {
33+
val tracks = mutableListOf<WebRtcMedia.Track>()
34+
try {
35+
tracks.add(client.createAudioTrack())
36+
37+
val audioTrack1 = client.createAudioTrack {
4638
autoGainControl = true
4739
echoCancellation = true
48-
noiseSuppression = true
49-
latency = 1.0
50-
channelCount = 1
51-
sampleRate = 10
52-
volume = 0.5
53-
}
54-
}
55-
// check overloading
56-
assertNoMediaDevice {
57-
val constraints = WebRtcMedia.AudioTrackConstraints(echoCancellation = true)
58-
client.createAudioTrack(constraints)
40+
}.also { tracks.add(it) }
41+
42+
val settings1 = audioTrack1.getSettings()
43+
assertEquals(true, settings1.autoGainControl)
44+
assertEquals(true, settings1.echoCancellation.toString().toBoolean())
45+
46+
// check overloading
47+
val constraints = WebRtcMedia.AudioTrackConstraints(autoGainControl = false)
48+
val audioTrack2 = client.createAudioTrack(constraints).also { tracks.add(it) }
49+
val settings2 = audioTrack2.getSettings()
50+
assertEquals(false, settings2.autoGainControl)
51+
} finally {
52+
tracks.forEach { it.close() }
5953
}
6054
}
6155

6256
@Test
6357
fun testCreateVideoTrack() = runTest {
64-
// Assert that constraints are mapped correctly, though ChromeHeadless does not have any media devices
65-
assertNoMediaDevice {
66-
client.createVideoTrack()
67-
}
68-
assertNoMediaDevice {
69-
client.createVideoTrack {
58+
val tracks = mutableListOf<WebRtcMedia.Track>()
59+
try {
60+
tracks.add(client.createVideoTrack())
61+
62+
val videoTrack1 = client.createVideoTrack {
7063
width = 100
7164
height = 100
7265
frameRate = 30
7366
facingMode = WebRtcMedia.FacingMode.USER
74-
aspectRatio = 1.4
75-
}
76-
}
77-
// check overloading
78-
assertNoMediaDevice {
79-
val constraints = WebRtcMedia.VideoTrackConstraints(height = 100)
80-
client.createVideoTrack(constraints)
67+
}.also { tracks.add(it) }
68+
val settings1 = videoTrack1.getSettings()
69+
assertEquals(100, settings1.width)
70+
assertEquals(100, settings1.height)
71+
72+
// check overloading
73+
val constraints = WebRtcMedia.VideoTrackConstraints(aspectRatio = 2.0)
74+
val videoTrack2 = client.createVideoTrack(constraints).also { tracks.add(it) }
75+
val settings2 = videoTrack2.getSettings()
76+
assertEquals(2.0, settings2.aspectRatio)
77+
} finally {
78+
tracks.forEach { it.close() }
8179
}
8280
}
8381
}

0 commit comments

Comments
 (0)