From 90da61c201228e6fab3a46ccf17746755fc921ed Mon Sep 17 00:00:00 2001 From: pedroSG94 Date: Fri, 17 Jul 2026 17:59:29 +0200 Subject: [PATCH 1/4] support ipv6 --- .../main/java/com/pedro/common/Extensions.kt | 8 + .../main/java/com/pedro/common/UrlParser.kt | 11 +- .../common/socket/java/UdpStreamSocketJava.kt | 5 +- .../common/socket/ktor/UdpStreamSocketKtor.kt | 6 +- .../java/com/pedro/common/UrlParserTest.kt | 44 ++++ .../rtmp/utils/socket/TcpTunneledSocket.kt | 3 +- rtsp/build.gradle.kts | 4 + .../rtsp/rtsp/commands/CommandsManager.kt | 19 +- srt/build.gradle.kts | 4 + .../java/com/pedro/srt/srt/CommandsManager.kt | 8 + .../packets/control/handshake/Handshake.kt | 14 +- .../SrtConnectionIntegrationTest.kt | 111 ++++++++++ .../pedro/srt/srt/control/HandshakeTest.kt | 16 ++ whip/build.gradle.kts | 4 + .../main/java/com/pedro/whip/WhipClient.kt | 14 +- .../com/pedro/whip/webrtc/CommandsManager.kt | 15 +- .../webrtc/stun/CandidatePairConnection.kt | 204 ------------------ .../webrtc/stun/StunAttributeValueParser.kt | 13 +- 18 files changed, 262 insertions(+), 241 deletions(-) create mode 100644 srt/src/test/java/com/pedro/srt/integration/SrtConnectionIntegrationTest.kt delete mode 100644 whip/src/main/java/com/pedro/whip/webrtc/stun/CandidatePairConnection.kt diff --git a/common/src/main/java/com/pedro/common/Extensions.kt b/common/src/main/java/com/pedro/common/Extensions.kt index 8d441a6eb8..e41e6ba4bf 100644 --- a/common/src/main/java/com/pedro/common/Extensions.kt +++ b/common/src/main/java/com/pedro/common/Extensions.kt @@ -38,6 +38,7 @@ import java.io.UnsupportedEncodingException import java.math.BigInteger import java.nio.ByteBuffer import java.security.MessageDigest +import java.net.InetAddress import java.security.NoSuchAlgorithmException import java.security.SecureRandom import java.util.concurrent.BlockingQueue @@ -342,4 +343,11 @@ fun ByteBuffer.put(buffer: ByteBuffer, offset: Int, length: Int) { buffer.limit(offset + length) this.put(buffer) buffer.limit(limit) +} + +/** + * Numeric IP as text, without reverse DNS lookup and without the IPv6 scope id (fe80::1%wlan0 -> fe80::1) + */ +fun InetAddress.addressToString(): String { + return (hostAddress ?: hostName).substringBefore("%") } \ No newline at end of file diff --git a/common/src/main/java/com/pedro/common/UrlParser.kt b/common/src/main/java/com/pedro/common/UrlParser.kt index 1b572b5395..765d7eefc1 100644 --- a/common/src/main/java/com/pedro/common/UrlParser.kt +++ b/common/src/main/java/com/pedro/common/UrlParser.kt @@ -44,7 +44,7 @@ class UrlParser private constructor( init { val url = uri.toString() scheme = uri.scheme - host = uri.host + host = uri.host.removeSurrounding("[", "]") port = if (uri.port < 0) null else uri.port path = uri.path.removePrefix("/") if (uri.query != null) { @@ -79,17 +79,22 @@ class UrlParser private constructor( fun getStreamName(): String = getFullPath().removePrefix(getAppName()).removePrefix("/").removePrefix("?") + /** + * Host ready to be concatenated in a url. IPv6 hosts need to be enclosed in brackets + */ + fun getHostForUrl(): String = if (host.contains(":")) "[$host]" else host + fun getTcUrl(): String { val port = if (port != null) ":$port" else "" val appName = if (getAppName().isNotEmpty()) "/${getAppName()}" else "" - return "$scheme://$host${port}${appName}" + return "$scheme://${getHostForUrl()}${port}${appName}" } fun getFullPath(): String { val fullPath = "$path${if (query == null) "" else "?$query"}".removePrefix("?") if (fullPath.isEmpty()) { val port = if (port != null) ":$port" else "" - return url.removePrefix("$scheme://$host$port").removePrefix("/") + return url.removePrefix("$scheme://${getHostForUrl()}$port").removePrefix("/") } return fullPath } diff --git a/common/src/main/java/com/pedro/common/socket/java/UdpStreamSocketJava.kt b/common/src/main/java/com/pedro/common/socket/java/UdpStreamSocketJava.kt index 17c97743d0..a2f91c0e5d 100644 --- a/common/src/main/java/com/pedro/common/socket/java/UdpStreamSocketJava.kt +++ b/common/src/main/java/com/pedro/common/socket/java/UdpStreamSocketJava.kt @@ -1,5 +1,6 @@ package com.pedro.common.socket.java +import com.pedro.common.addressToString import com.pedro.common.socket.base.UdpPacket import com.pedro.common.socket.base.UdpStreamSocket import com.pedro.common.socket.base.UdpType @@ -98,7 +99,7 @@ class UdpStreamSocketJava( val buffer = ByteArray(packetSize) val udpPacket = DatagramPacket(buffer, buffer.size) socket?.receive(udpPacket) - return UdpPacket(udpPacket.data, udpPacket.length, udpPacket.address.hostName, udpPacket.port) + return UdpPacket(udpPacket.data, udpPacket.length, udpPacket.address.addressToString(), udpPacket.port) } override suspend fun setRemoteAddress(host: String, port: Int) { @@ -107,7 +108,7 @@ class UdpStreamSocketJava( } override suspend fun getLocalHost(): String { - return sourceHost ?: "0.0.0.0" + return socket?.localAddress?.addressToString() ?: sourceHost ?: "::" } override suspend fun getLocalPort(): Int { diff --git a/common/src/main/java/com/pedro/common/socket/ktor/UdpStreamSocketKtor.kt b/common/src/main/java/com/pedro/common/socket/ktor/UdpStreamSocketKtor.kt index b056615db2..603b874767 100644 --- a/common/src/main/java/com/pedro/common/socket/ktor/UdpStreamSocketKtor.kt +++ b/common/src/main/java/com/pedro/common/socket/ktor/UdpStreamSocketKtor.kt @@ -37,7 +37,7 @@ class UdpStreamSocketKtor( override suspend fun connect() { val builder = aSocket(selectorManager).udp() val localAddress = if (sourcePort == null) null else { - val localAddress = InetSocketAddress(sourceHost ?: "0.0.0.0", sourcePort) + val localAddress = InetSocketAddress(sourceHost ?: "::", sourcePort) this.localAddress = localAddress localAddress } @@ -96,7 +96,7 @@ class UdpStreamSocketKtor( val length = datagram.packet.remaining.toInt() val data = datagram.packet.readByteArray() val address = datagram.address as? InetSocketAddress - return UdpPacket(data, length, address?.hostname, address?.port) + return UdpPacket(data, length, address?.hostname?.substringBefore("%"), address?.port) } override suspend fun setRemoteAddress(host: String, port: Int) { @@ -104,7 +104,7 @@ class UdpStreamSocketKtor( } override suspend fun getLocalHost(): String { - return localAddress?.hostname ?: "0.0.0.0" + return localAddress?.hostname ?: "::" } override suspend fun getLocalPort(): Int { diff --git a/common/src/test/java/com/pedro/common/UrlParserTest.kt b/common/src/test/java/com/pedro/common/UrlParserTest.kt index 80f31ac7b7..19e7adf85f 100644 --- a/common/src/test/java/com/pedro/common/UrlParserTest.kt +++ b/common/src/test/java/com/pedro/common/UrlParserTest.kt @@ -261,6 +261,50 @@ class UrlParserTest { assertEquals(1935, urlParser0.port) } + @Test + fun testIpv6Urls() { + val url = "rtmp://[::1]:1935/live/stream" + val urlParser = UrlParser.parse(url, arrayOf("rtmp")) + assertEquals("rtmp", urlParser.scheme) + assertEquals("::1", urlParser.host) + assertEquals(1935, urlParser.port) + assertEquals("live", urlParser.getAppName()) + assertEquals("stream", urlParser.getStreamName()) + assertEquals("rtmp://[::1]:1935/live", urlParser.getTcUrl()) + + val url1 = "rtsp://[2001:db8::1]/live/test" + val urlParser1 = UrlParser.parse(url1, arrayOf("rtsp")) + assertEquals("rtsp", urlParser1.scheme) + assertEquals("2001:db8::1", urlParser1.host) + assertEquals(null, urlParser1.port) + assertEquals("live/test", urlParser1.getFullPath()) + + val url2 = "srt://[2001:db8::aa:1]:9000?streamid=test/fake" + val urlParser2 = UrlParser.parse(url2, arrayOf("srt")) + assertEquals("srt", urlParser2.scheme) + assertEquals("2001:db8::aa:1", urlParser2.host) + assertEquals(9000, urlParser2.port) + assertEquals("test/fake", urlParser2.getQuery("streamid")) + + val url3 = "rtmp://[::1]:1234?live" + val urlParser3 = UrlParser.parse(url3, arrayOf("rtmp")) + assertEquals("::1", urlParser3.host) + assertEquals(1234, urlParser3.port) + assertEquals("live", urlParser3.getAppName()) + assertEquals("rtmp://[::1]:1234/live", urlParser3.getTcUrl()) + + val url4 = "udp://[ff0e::1]:5000" + val urlParser4 = UrlParser.parse(url4, arrayOf("udp")) + assertEquals("ff0e::1", urlParser4.host) + assertEquals(5000, urlParser4.port) + + val url5 = "http://[::1]:8889/publish/stream" + val urlParser5 = UrlParser.parse(url5, arrayOf("http")) + assertEquals("::1", urlParser5.host) + assertEquals(8889, urlParser5.port) + assertEquals("publish/stream", urlParser5.getFullPath()) + } + @Test fun testUrl2() { val urlParser = UrlParser.parse( diff --git a/rtmp/src/main/java/com/pedro/rtmp/utils/socket/TcpTunneledSocket.kt b/rtmp/src/main/java/com/pedro/rtmp/utils/socket/TcpTunneledSocket.kt index 8aecab59cb..9fe039afb7 100644 --- a/rtmp/src/main/java/com/pedro/rtmp/utils/socket/TcpTunneledSocket.kt +++ b/rtmp/src/main/java/com/pedro/rtmp/utils/socket/TcpTunneledSocket.kt @@ -108,7 +108,8 @@ class TcpTunneledSocket(private val host: String, private val port: Int, private private fun configureSocket(path: String, secured: Boolean): HttpURLConnection { val schema = if (secured) "https" else "http" - val url = URL("$schema://$host:$port/$path") + val urlHost = if (host.contains(":")) "[$host]" else host + val url = URL("$schema://$urlHost:$port/$path") val socket = if (secured) { url.openConnection() as HttpsURLConnection } else { diff --git a/rtsp/build.gradle.kts b/rtsp/build.gradle.kts index 36652067c6..b950d4edab 100644 --- a/rtsp/build.gradle.kts +++ b/rtsp/build.gradle.kts @@ -20,6 +20,10 @@ android { buildFeatures { buildConfig = true } + testOptions { + //allow use android classes like Log or SystemClock in integration tests (return 0 instead of crash) + unitTests.isReturnDefaultValues = true + } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 diff --git a/rtsp/src/main/java/com/pedro/rtsp/rtsp/commands/CommandsManager.kt b/rtsp/src/main/java/com/pedro/rtsp/rtsp/commands/CommandsManager.kt index fae3c694cc..e7d8f4dfe0 100644 --- a/rtsp/src/main/java/com/pedro/rtsp/rtsp/commands/CommandsManager.kt +++ b/rtsp/src/main/java/com/pedro/rtsp/rtsp/commands/CommandsManager.kt @@ -50,6 +50,8 @@ open class CommandsManager { private set var path: String? = null private set + private val urlHost: String + get() = host?.let { if (it.contains(":")) "[$it]" else it } ?: "" var sps: ByteBuffer? = null private set var pps: ByteBuffer? = null @@ -172,11 +174,14 @@ open class CommandsManager { AudioCodec.OPUS -> createOpusBody(rtpTracks.trackAudio) } } + val isIpv6 = host?.contains(":") == true + val addressType = if (isIpv6) "IP6" else "IP4" + val localhost = if (isIpv6) "::1" else "127.0.0.1" return "v=0\r\n" + - "o=- $timeStamp $timeStamp IN IP4 127.0.0.1\r\n" + + "o=- $timeStamp $timeStamp IN $addressType $localhost\r\n" + "s=Unnamed\r\n" + "i=N/A\r\n" + - "c=IN IP4 $host\r\n" + + "c=IN $addressType $host\r\n" + "t=0 0\r\n" + "a=recvonly\r\n" + videoBody + audioBody @@ -201,7 +206,7 @@ open class CommandsManager { //Commands fun createOptions(): String { - val uri = "rtsp://$host:$port$path" + val uri = "rtsp://$urlHost:$port$path" val options = "OPTIONS $uri RTSP/1.0\r\n" + addHeaders(Method.OPTIONS, uri) + "\r\n" Log.i(TAG, options) return options @@ -214,7 +219,7 @@ open class CommandsManager { } else { "TCP;unicast;interleaved=${2 * track}-${2 * track + 1};mode=record" } - val uri = "rtsp://$host:$port$path/streamid=$track" + val uri = "rtsp://$urlHost:$port$path/streamid=$track" val setup = "SETUP $uri RTSP/1.0\r\n" + "Transport: RTP/AVP/$params\r\n" + addHeaders(Method.SETUP, uri) + "\r\n" @@ -223,7 +228,7 @@ open class CommandsManager { } fun createRecord(): String { - val uri = "rtsp://$host:$port$path" + val uri = "rtsp://$urlHost:$port$path" val record = "RECORD $uri RTSP/1.0\r\n" + "Range: npt=0.000-\r\n" + addHeaders(Method.RECORD, uri) + "\r\n" Log.i(TAG, record) @@ -232,7 +237,7 @@ open class CommandsManager { fun createAnnounce(): String { val body = createBody() - val uri = "rtsp://$host:$port$path" + val uri = "rtsp://$urlHost:$port$path" val announce = "ANNOUNCE $uri RTSP/1.0\r\n" + "Content-Type: application/sdp\r\n" + addHeaders(Method.ANNOUNCE, uri) + @@ -254,7 +259,7 @@ open class CommandsManager { } fun createTeardown(): String { - val uri = "rtsp://$host:$port$path" + val uri = "rtsp://$urlHost:$port$path" val teardown = "TEARDOWN $uri RTSP/1.0\r\n" + addHeaders(Method.TEARDOWN, uri) + "\r\n" Log.i(TAG, teardown) return teardown diff --git a/srt/build.gradle.kts b/srt/build.gradle.kts index 3b8b50af09..da2de0a740 100644 --- a/srt/build.gradle.kts +++ b/srt/build.gradle.kts @@ -17,6 +17,10 @@ android { isMinifyEnabled = false } } + testOptions { + //allow use android classes like Log or SystemClock in integration tests (return 0 instead of crash) + unitTests.isReturnDefaultValues = true + } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 diff --git a/srt/src/main/java/com/pedro/srt/srt/CommandsManager.kt b/srt/src/main/java/com/pedro/srt/srt/CommandsManager.kt index e0799a862c..312541c271 100644 --- a/srt/src/main/java/com/pedro/srt/srt/CommandsManager.kt +++ b/srt/src/main/java/com/pedro/srt/srt/CommandsManager.kt @@ -51,6 +51,8 @@ class CommandsManager { var messageNumber = 1 var MTU = Constants.MTU var socketId = 0 + //own socket id, must be unique per connection or the server can discard the handshake as duplicated + private var localSocketId = generateSocketId() var startTS = 0L //microSeconds var audioDisabled = false var videoDisabled = false @@ -78,6 +80,7 @@ class CommandsManager { fun loadStartTs() { startTS = TimeUtils.getCurrentTimeMicro() + localSocketId = generateSocketId() } fun getTs(): Int { @@ -88,6 +91,7 @@ class CommandsManager { suspend fun writeHandshake(socket: SrtSocket?, handshake: Handshake = Handshake()) { writeSync.withLock { handshake.initialPacketSequence = sequenceNumber + handshake.srtSocketId = localSocketId handshake.ipAddress = host handshake.write(getTs(), 0) Log.i(TAG, handshake.toString()) @@ -201,4 +205,8 @@ class CommandsManager { private fun generateInitialSequence(): Int { return Random.nextInt(0, Int.MAX_VALUE) } + + private fun generateSocketId(): Int { + return Random.nextInt(1, Int.MAX_VALUE) + } } \ No newline at end of file diff --git a/srt/src/main/java/com/pedro/srt/srt/packets/control/handshake/Handshake.kt b/srt/src/main/java/com/pedro/srt/srt/packets/control/handshake/Handshake.kt index 7645b70f2f..23a9f3b190 100644 --- a/srt/src/main/java/com/pedro/srt/srt/packets/control/handshake/Handshake.kt +++ b/srt/src/main/java/com/pedro/srt/srt/packets/control/handshake/Handshake.kt @@ -19,6 +19,7 @@ package com.pedro.srt.srt.packets.control.handshake import com.pedro.common.readUInt16 import com.pedro.common.readUInt32 import com.pedro.common.readUInt32LittleEndian +import com.pedro.common.toUInt32 import com.pedro.common.writeUInt16 import com.pedro.common.writeUInt32 import com.pedro.srt.srt.packets.ControlPacket @@ -136,9 +137,16 @@ data class Handshake( } private fun readAddress(input: InputStream): String { - val ip = input.readUInt32LittleEndian() - repeat(3) { input.readUInt32LittleEndian() } - return "${(ip ushr 24) and 0xFF}.${(ip ushr 16) and 0xFF}.${(ip ushr 8) and 0xFF}.${ip and 0xFF}" + //4 words of 32 bits, each word in little endian like in writeAddress. IPv4 only use the first word + val words = List(4) { input.readUInt32LittleEndian() } + return if (words[1] == 0 && words[2] == 0 && words[3] == 0) { //ipv4 + val ip = words[0] + "${(ip ushr 24) and 0xFF}.${(ip ushr 16) and 0xFF}.${(ip ushr 8) and 0xFF}.${ip and 0xFF}" + } else { //ipv6 + val bytes = ByteArray(16) + words.forEachIndexed { index, word -> word.toUInt32().copyInto(bytes, index * 4) } + InetAddress.getByAddress(bytes).hostAddress ?: "::" + } } fun isErrorType(): Boolean { diff --git a/srt/src/test/java/com/pedro/srt/integration/SrtConnectionIntegrationTest.kt b/srt/src/test/java/com/pedro/srt/integration/SrtConnectionIntegrationTest.kt new file mode 100644 index 0000000000..8ea63394b8 --- /dev/null +++ b/srt/src/test/java/com/pedro/srt/integration/SrtConnectionIntegrationTest.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2024 pedroSG94. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.pedro.srt.integration + +import com.pedro.common.ConnectChecker +import com.pedro.srt.srt.SrtClient +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Assert.assertTrue +import org.junit.Assume.assumeTrue +import org.junit.Before +import org.junit.Test +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference + +/** + * Integration test: connects to a real SRT server (MediaMTX in CI) and verifies the full + * induction + conclusion handshake flow reaches onConnectionSuccess. + * + * It only exercises the handshake (no media is sent), so it needs neither a real encoder + * (MediaCodec) nor an Android device — it runs on the plain JVM unit-test task. + * + * Skipped unless the env var SRT_INTEGRATION=true, so it is a no-op in the regular + * `./gradlew test` build. The targets can be overridden with SRT_URL and SRT_URL_V6. + * See .github/workflows/integration.yml for how the server is started in CI. + * + * Prefer running it isolated with --tests (like CI does): the client can report uncaught + * coroutine exceptions after disconnect and kotlinx-coroutines-test flags them in unrelated + * tests of the same JVM run. + */ +@OptIn(ExperimentalCoroutinesApi::class) +class SrtConnectionIntegrationTest { + + @Before + fun setup() { + // SrtClient delivers its callbacks via withContext(Dispatchers.Main); on the JVM there is no + // real Main dispatcher, so install one (Unconfined runs the callback inline). + Dispatchers.setMain(Dispatchers.Unconfined) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `connect and publish to srt server`() { + assumeTrue("integration disabled (set SRT_INTEGRATION=true to run)", System.getenv("SRT_INTEGRATION") == "true") + val url = System.getenv("SRT_URL") ?: "srt://127.0.0.1:8890?streamid=publish:sinttest" + connectAndAssert(url) + } + + @Test + fun `connect and publish to srt server by ipv6`() { + assumeTrue("integration disabled (set SRT_INTEGRATION=true to run)", System.getenv("SRT_INTEGRATION") == "true") + val url = System.getenv("SRT_URL_V6") ?: "srt://[::1]:8890?streamid=publish:sinttestv6" + connectAndAssert(url) + } + + private fun connectAndAssert(url: String) { + val latch = CountDownLatch(1) + val success = AtomicBoolean(false) + val failReason = AtomicReference("") + + val client = SrtClient(object : ConnectChecker { + override fun onConnectionStarted(url: String) {} + override fun onConnectionSuccess() { + success.set(true) + latch.countDown() + } + override fun onConnectionFailed(reason: String) { + failReason.set(reason) + latch.countDown() + } + override fun onNewBitrate(bitrate: Long) {} + override fun onDisconnect() {} + override fun onAuthError() { + failReason.set("auth error") + latch.countDown() + } + override fun onAuthSuccess() {} + }) + + try { + client.connect(url) + assertTrue("no connection callback within timeout for $url", latch.await(20, TimeUnit.SECONDS)) + assertTrue("connection to $url failed: ${failReason.get()}", success.get()) + } finally { + client.disconnect() + } + } +} diff --git a/srt/src/test/java/com/pedro/srt/srt/control/HandshakeTest.kt b/srt/src/test/java/com/pedro/srt/srt/control/HandshakeTest.kt index ab552c5246..6b31d28ea8 100644 --- a/srt/src/test/java/com/pedro/srt/srt/control/HandshakeTest.kt +++ b/srt/src/test/java/com/pedro/srt/srt/control/HandshakeTest.kt @@ -61,6 +61,22 @@ class HandshakeTest { assertArrayEquals(expectedData, packetHandshake) } + @Test + fun `GIVEN a handshake packet with ip WHEN write and read it THEN get the same address`() { + val handshakeV4 = Handshake(ipAddress = "192.168.0.1") + handshakeV4.write(2500, 0x40) + val packetV4 = Handshake() + packetV4.read(ByteArrayInputStream(handshakeV4.getData())) + assertEquals("192.168.0.1", packetV4.ipAddress) + + val handshakeV6 = Handshake(ipAddress = "2001:db8::aa:1") + handshakeV6.write(2500, 0x40) + val packetV6 = Handshake() + packetV6.read(ByteArrayInputStream(handshakeV6.getData())) + //Inet6Address.hostAddress returns the address without zeros compression + assertEquals("2001:db8:0:0:0:0:aa:1", packetV6.ipAddress) + } + @Test fun `GIVEN a buffer WHEN read buffer as handshake packet THEN get expected handshake packet`() { val buffer = byteArrayOf(-128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, -60, 0, 0, 0, 64, 0, 0, 0, 4, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 5, -36, 0, 0, 32, 0, 0, 0, 0, 1, 45, 116, -9, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) diff --git a/whip/build.gradle.kts b/whip/build.gradle.kts index 08d97e6d43..0ede7582d2 100644 --- a/whip/build.gradle.kts +++ b/whip/build.gradle.kts @@ -17,6 +17,10 @@ android { isMinifyEnabled = false } } + testOptions { + //allow use android classes like Log or SystemClock in integration tests (return 0 instead of crash) + unitTests.isReturnDefaultValues = true + } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 diff --git a/whip/src/main/java/com/pedro/whip/WhipClient.kt b/whip/src/main/java/com/pedro/whip/WhipClient.kt index 8433e39b79..0c5488d8f8 100644 --- a/whip/src/main/java/com/pedro/whip/WhipClient.kt +++ b/whip/src/main/java/com/pedro/whip/WhipClient.kt @@ -19,6 +19,7 @@ import com.pedro.rtsp.utils.CryptoProperties import com.pedro.rtsp.utils.RtpConstants import com.pedro.whip.dtls.DtlsConnection import com.pedro.whip.dtls.DtlsTransport +import com.pedro.whip.utils.Network import com.pedro.whip.webrtc.CandidateType import com.pedro.whip.webrtc.CommandsManager import com.pedro.whip.webrtc.stun.AttributeType @@ -37,6 +38,7 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.withTimeoutOrNull +import java.net.Inet6Address import java.net.URISyntaxException import java.nio.ByteBuffer import javax.net.ssl.TrustManager @@ -258,8 +260,12 @@ class WhipClient(private val connectChecker: ConnectChecker) { val remoteFrag = commandsManager.remoteSdpInfo?.uFrag ?: return@launch val priority = commandsManager.calculatePriority(CandidateType.LOCAL, 65535L, 1) + val availableFamilies = Network.getNetworks(onlyV4 = false).map { it is Inet6Address }.toSet() val remoteCandidates = commandsManager.remoteSdpInfo?.candidates?.filter { - it.getRealHost() != "127.0.0.1" + val candidateHost = it.getRealHost() + candidateHost != "127.0.0.1" && candidateHost != "::1" + }?.sortedByDescending { + availableFamilies.contains(it.getRealHost().contains(":")) } ?: return@launch val host = remoteCandidates[0].getRealHost() @@ -284,7 +290,7 @@ class WhipClient(private val connectChecker: ConnectChecker) { requestSuccessReceived = true } else if (command.header.type == HeaderType.REQUEST) { candidateResponses++ - val xorAddress = StunAttributeValueParser.createXorMappedAddress(command.header.id, host, port, true) + val xorAddress = StunAttributeValueParser.createXorMappedAddress(command.header.id, host, port) val attributes = listOf( StunAttribute(AttributeType.XOR_MAPPED_ADDRESS, xorAddress) ) @@ -308,7 +314,7 @@ class WhipClient(private val connectChecker: ConnectChecker) { nominateSuccessReceived = true } else if (command.header.type == HeaderType.REQUEST) { candidateResponses++ - val xorAddress = StunAttributeValueParser.createXorMappedAddress(command.header.id, host, port, true) + val xorAddress = StunAttributeValueParser.createXorMappedAddress(command.header.id, host, port) val attributes = listOf( StunAttribute(AttributeType.XOR_MAPPED_ADDRESS, xorAddress) ) @@ -387,7 +393,7 @@ class WhipClient(private val connectChecker: ConnectChecker) { val command = commandsManager.readStun(bytes) if (command.header.type == HeaderType.REQUEST) { val xorAddress = StunAttributeValueParser.createXorMappedAddress( - command.header.id, host, port, true + command.header.id, host, port ) commandsManager.writeStun( HeaderType.SUCCESS, command.header.id, diff --git a/whip/src/main/java/com/pedro/whip/webrtc/CommandsManager.kt b/whip/src/main/java/com/pedro/whip/webrtc/CommandsManager.kt index 718b796841..82bc30ac82 100644 --- a/whip/src/main/java/com/pedro/whip/webrtc/CommandsManager.kt +++ b/whip/src/main/java/com/pedro/whip/webrtc/CommandsManager.kt @@ -3,6 +3,7 @@ package com.pedro.whip.webrtc import android.util.Log import com.pedro.common.AudioCodec import com.pedro.common.TimeUtils +import com.pedro.common.addressToString import com.pedro.common.VideoCodec import com.pedro.common.nextBytes import com.pedro.common.socket.base.SocketType @@ -42,6 +43,8 @@ class CommandsManager { private set var path: String? = null private set + private val urlHost: String + get() = host?.let { if (it.contains(":")) "[$it]" else it } ?: "" var token: String? = null private set private var shouldSendAuth = false @@ -135,7 +138,7 @@ class CommandsManager { val googleStunHost = "stun.l.google.com" val googleStunPort = 19302 val startPort = 5000 - val hosts = Network.getNetworks(onlyV4 = true).mapNotNull { it.hostAddress } + val hosts = Network.getNetworks(onlyV4 = false).map { it.addressToString() } return when (gatheringMode) { GatheringMode.LOCAL -> getLocalCandidates(hosts, startPort) GatheringMode.ALL -> { @@ -178,7 +181,7 @@ class CommandsManager { ) this.certificate = certificate localSdpInfo = SdpInfo(uFrag, uPass, certificate.fingerprint, listOf()) - val uri = "${if (tlsEnabled) "https" else "http"}://$host:$port/$path" + val uri = "${if (tlsEnabled) "https" else "http"}://$urlHost:$port/$path" val headers = mutableMapOf().apply { put("Content-Type", "application/sdp") if (!token.isNullOrEmpty() && sendAuth) put("Authorization", "Bearer $token") @@ -198,8 +201,8 @@ class CommandsManager { fun writeDelete(): RequestResponse { val uri = sessionUrl?.let { if (it.startsWith("http", true)) it - else "${if (tlsEnabled) "https" else "http"}://$host:$port/${it.removePrefix("/")}" - } ?: "${if (tlsEnabled) "https" else "http"}://$host:$port/$path" + else "${if (tlsEnabled) "https" else "http"}://$urlHost:$port/${it.removePrefix("/")}" + } ?: "${if (tlsEnabled) "https" else "http"}://$urlHost:$port/$path" val headers = mutableMapOf().apply { if (!token.isNullOrEmpty() && shouldSendAuth) put("Authorization", "Bearer $token") } @@ -244,9 +247,7 @@ class CommandsManager { val isSuccess = result.header.type == HeaderType.SUCCESS if (isSuccess) { val xorMappedAddress = result.attributes.find { it.type == AttributeType.XOR_MAPPED_ADDRESS }?.value!! - val value = StunAttributeValueParser.readXorMappedAddress(xorMappedAddress, result.header.id) - val publicHost = value.split(":")[0] - val publicPort = value.split(":")[1].toInt() + val (publicHost, publicPort) = StunAttributeValueParser.readXorMappedAddress(xorMappedAddress, result.header.id) val type = CandidateType.SRFLX val priority = calculatePriority(type, 65535L, 1) candidates.add(Candidate( diff --git a/whip/src/main/java/com/pedro/whip/webrtc/stun/CandidatePairConnection.kt b/whip/src/main/java/com/pedro/whip/webrtc/stun/CandidatePairConnection.kt deleted file mode 100644 index 99bb0e298c..0000000000 --- a/whip/src/main/java/com/pedro/whip/webrtc/stun/CandidatePairConnection.kt +++ /dev/null @@ -1,204 +0,0 @@ -/* - * - * * Copyright (C) 2024 pedroSG94. - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. - * - */ - -package com.pedro.whip.webrtc.stun - -import android.util.Log -import com.pedro.common.bytesToHex -import com.pedro.common.socket.base.UdpStreamSocket -import com.pedro.common.toUInt32 -import com.pedro.whip.webrtc.CommandsManager -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch - -/** - * Created by pedro on 7/8/25. - */ -class CandidatePairConnection( - val socket: UdpStreamSocket, - val candidatePair: CandidatePair, - private val commandsManager: CommandsManager -) { - - private val scope = CoroutineScope(Dispatchers.IO) - @Volatile - private var state = CandidatePairConnectionState.STOPPED - private val timeouts = arrayOf(100L, 200L, 400L, 800L, 1500L, 2000) - private val bindingRequestWaiting = mutableListOf() - private val nominatingRequestWaiting = mutableListOf() - private val confirmRequestWaiting = mutableListOf() - private val sync = Any() - - fun connect(onDone: (CandidatePairConnection) -> Unit) { - val localFrag = commandsManager.localSdpInfo?.uFrag ?: return - val remoteFrag = commandsManager.remoteSdpInfo?.uFrag ?: return - val host = candidatePair.remote.publicAddress ?: candidatePair.remote.localAddress - val port = candidatePair.remote.publicPort ?: candidatePair.remote.localPort - scope.launch { - state = CandidatePairConnectionState.BINDING - sendBindingRequestToCandidate(localFrag, remoteFrag, host, port, socket) - if (state == CandidatePairConnectionState.FAILED) { - Log.i("Pedro", "binding failed") - return@launch - } - sendNominateBindingRequestToCandidate(localFrag, remoteFrag, host, port, socket) - if (state == CandidatePairConnectionState.FAILED) { - Log.i("Pedro", "binding failed") - return@launch - } - sendConfirmRequestToCandidate(localFrag, remoteFrag, host, port, socket) - if (state == CandidatePairConnectionState.FAILED) { - Log.i("Pedro", "confirm failed") - return@launch - } - onDone(this@CandidatePairConnection) - } - } - - fun stop() { - scope.cancel() - state = CandidatePairConnectionState.STOPPED - bindingRequestWaiting.clear() - nominatingRequestWaiting.clear() - confirmRequestWaiting.clear() - } - - fun handleResponse(data: ByteArray, host: String , port: Int) { - val remoteHost = candidatePair.remote.getRealHost() - val remotePort = candidatePair.remote.getRealPort() - if (host != remoteHost || port != remotePort) return - val command = StunCommandReader.readPacket(data) - val id = command.header.id - Log.i("Pedro", "command received, handling: ${id.bytesToHex()}") - synchronized(sync) { - Log.i("Pedro", "binding waiting: ${bindingRequestWaiting.map { it.bytesToHex() }}") - var found = false - bindingRequestWaiting.forEach { - if (it.contentEquals(id)) { - state = CandidatePairConnectionState.NOMINATING - found = true - } - } - nominatingRequestWaiting.forEach { - if (it.contentEquals(id)) { - state = CandidatePairConnectionState.CONFIRMING - found = true - } - } - confirmRequestWaiting.forEach { - if (it.contentEquals(id)) { - state = CandidatePairConnectionState.DONE - found = true - } - } - if (!found) { - scope.launch { - val localFrag = commandsManager.localSdpInfo?.uFrag ?: return@launch - val remoteFrag = commandsManager.remoteSdpInfo?.uFrag ?: return@launch - val host = candidatePair.remote.publicAddress ?: candidatePair.remote.localAddress - val port = candidatePair.remote.publicPort ?: candidatePair.remote.localPort - sendSuccess(id, localFrag, remoteFrag, host, port, socket) - } - } - } - } - - private suspend fun sendBindingRequestToCandidate( - localFrag: String, remoteFrag: String, host: String, port: Int, - socket: UdpStreamSocket - ) { - for (i in 0..timeouts.size) { - if (state != CandidatePairConnectionState.BINDING) return - - val id = commandsManager.generateTransactionId() - val userName = StunAttributeValueParser.createUserName(localFrag, remoteFrag) - val attributes = listOf( - StunAttribute(AttributeType.PRIORITY, candidatePair.local.priority.toUInt32()), - StunAttribute(AttributeType.USERNAME, userName), - StunAttribute(AttributeType.ICE_CONTROLLING, commandsManager.tieBreak), - ) - synchronized(sync) { - bindingRequestWaiting.add(id) - } - //commandsManager.writeStun(HeaderType.REQUEST, id, attributes, socket, host, port) - Log.i("Pedro", "send binding $i") - delay(timeouts[i]) - } - state == CandidatePairConnectionState.FAILED - } - - private suspend fun sendNominateBindingRequestToCandidate( - localFrag: String, remoteFrag: String, host: String, port: Int, - socket: UdpStreamSocket - ) { - for (i in 0..timeouts.size) { - if (state != CandidatePairConnectionState.NOMINATING) return - - val id = commandsManager.generateTransactionId() - val userName = StunAttributeValueParser.createUserName(localFrag, remoteFrag) - val attributes = listOf( - StunAttribute(AttributeType.PRIORITY, candidatePair.local.priority.toUInt32()), - StunAttribute(AttributeType.USERNAME, userName), - StunAttribute(AttributeType.ICE_CONTROLLING, commandsManager.tieBreak), - StunAttribute(AttributeType.USE_CANDIDATE, byteArrayOf()), - ) - synchronized(sync) { - nominatingRequestWaiting.add(id) - } - //commandsManager.writeStun(HeaderType.REQUEST, id, attributes, socket, host, port) - sendConfirmRequestToCandidate(localFrag, remoteFrag, host, port, socket) - Log.i("Pedro", "send nominate $i") - delay(timeouts[i]) - } - state == CandidatePairConnectionState.FAILED - } - - private suspend fun sendConfirmRequestToCandidate( - localFrag: String, remoteFrag: String, host: String, port: Int, - socket: UdpStreamSocket - ) { - val id = commandsManager.generateTransactionId() - val userName = StunAttributeValueParser.createUserName(localFrag, remoteFrag) - val attributes = listOf( - StunAttribute(AttributeType.PRIORITY, candidatePair.local.priority.toUInt32()), - StunAttribute(AttributeType.USERNAME, userName), - StunAttribute(AttributeType.ICE_CONTROLLING, commandsManager.tieBreak), - ) - synchronized(sync) { - confirmRequestWaiting.add(id) - } - //commandsManager.writeStun(HeaderType.REQUEST, id, attributes, socket, host, port) - } - - private suspend fun sendSuccess( - id: ByteArray, localFrag: String, remoteFrag: String, host: String, port: Int, - socket: UdpStreamSocket - ) { - val userNameValue = StunAttributeValueParser.createUserName(remoteFrag, localFrag) - val xorAddress = StunAttributeValueParser.createXorMappedAddress(id, host, port, true) - val attributes = listOf( - StunAttribute(AttributeType.USERNAME, userNameValue), - StunAttribute(AttributeType.XOR_MAPPED_ADDRESS, xorAddress) - ) - //commandsManager.writeStun(HeaderType.SUCCESS, id, attributes, socket, host, port) - Log.i("Pedro", "send success") - } -} \ No newline at end of file diff --git a/whip/src/main/java/com/pedro/whip/webrtc/stun/StunAttributeValueParser.kt b/whip/src/main/java/com/pedro/whip/webrtc/stun/StunAttributeValueParser.kt index e78f68d5ef..205a50994f 100644 --- a/whip/src/main/java/com/pedro/whip/webrtc/stun/StunAttributeValueParser.kt +++ b/whip/src/main/java/com/pedro/whip/webrtc/stun/StunAttributeValueParser.kt @@ -18,10 +18,9 @@ package com.pedro.whip.webrtc.stun +import com.pedro.common.addressToString import com.pedro.common.readUInt16 -import com.pedro.common.readUInt32 import com.pedro.common.readUntil -import com.pedro.common.toByteArray import com.pedro.common.toUInt16 import com.pedro.common.toUInt32 import com.pedro.common.xorBytes @@ -29,7 +28,6 @@ import com.pedro.whip.dtls.CryptoUtils import com.pedro.whip.utils.Constants import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream -import java.math.BigInteger import java.net.InetAddress import java.util.zip.CRC32 @@ -44,13 +42,14 @@ object StunAttributeValueParser { } fun createXorMappedAddress( - id: ByteArray, host: String, port: Int, isIpv4: Boolean + id: ByteArray, host: String, port: Int ): ByteArray { + val address = InetAddress.getByName(host) + val isIpv4 = address.address.size == 4 val output = ByteArrayOutputStream() output.write(0) output.write(if (isIpv4) 0x01 else 0x02) output.write(port.toUInt16().xorBytes(Constants.MAGIC_COOKIE.toUInt32())) - val address = InetAddress.getByName(host) if (isIpv4) { output.write(address.address.xorBytes(Constants.MAGIC_COOKIE.toUInt32())) } else { @@ -60,7 +59,7 @@ object StunAttributeValueParser { return output.toByteArray() } - fun readXorMappedAddress(bytes: ByteArray, id: ByteArray): String { + fun readXorMappedAddress(bytes: ByteArray, id: ByteArray): Pair { val input = ByteArrayInputStream(bytes) input.read() val isIpv4 = input.read() == 0x01 @@ -70,7 +69,7 @@ object StunAttributeValueParser { input.readUntil(hostXor) val host = hostXor.xorBytes(xor) val address = InetAddress.getByAddress(host) - return "${address.hostAddress}:$port" + return Pair(address.addressToString(), port) } fun createMessageIntegrity(bytes: ByteArray, password: String): ByteArray { From 26cf2c5b046180dc84cfe747dbe87b62aff43e0f Mon Sep 17 00:00:00 2001 From: pedroSG94 Date: Fri, 17 Jul 2026 18:00:09 +0200 Subject: [PATCH 2/4] remove unused test --- .../SrtConnectionIntegrationTest.kt | 111 ------------------ 1 file changed, 111 deletions(-) delete mode 100644 srt/src/test/java/com/pedro/srt/integration/SrtConnectionIntegrationTest.kt diff --git a/srt/src/test/java/com/pedro/srt/integration/SrtConnectionIntegrationTest.kt b/srt/src/test/java/com/pedro/srt/integration/SrtConnectionIntegrationTest.kt deleted file mode 100644 index 8ea63394b8..0000000000 --- a/srt/src/test/java/com/pedro/srt/integration/SrtConnectionIntegrationTest.kt +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (C) 2024 pedroSG94. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.pedro.srt.integration - -import com.pedro.common.ConnectChecker -import com.pedro.srt.srt.SrtClient -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.setMain -import org.junit.After -import org.junit.Assert.assertTrue -import org.junit.Assume.assumeTrue -import org.junit.Before -import org.junit.Test -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicReference - -/** - * Integration test: connects to a real SRT server (MediaMTX in CI) and verifies the full - * induction + conclusion handshake flow reaches onConnectionSuccess. - * - * It only exercises the handshake (no media is sent), so it needs neither a real encoder - * (MediaCodec) nor an Android device — it runs on the plain JVM unit-test task. - * - * Skipped unless the env var SRT_INTEGRATION=true, so it is a no-op in the regular - * `./gradlew test` build. The targets can be overridden with SRT_URL and SRT_URL_V6. - * See .github/workflows/integration.yml for how the server is started in CI. - * - * Prefer running it isolated with --tests (like CI does): the client can report uncaught - * coroutine exceptions after disconnect and kotlinx-coroutines-test flags them in unrelated - * tests of the same JVM run. - */ -@OptIn(ExperimentalCoroutinesApi::class) -class SrtConnectionIntegrationTest { - - @Before - fun setup() { - // SrtClient delivers its callbacks via withContext(Dispatchers.Main); on the JVM there is no - // real Main dispatcher, so install one (Unconfined runs the callback inline). - Dispatchers.setMain(Dispatchers.Unconfined) - } - - @After - fun tearDown() { - Dispatchers.resetMain() - } - - @Test - fun `connect and publish to srt server`() { - assumeTrue("integration disabled (set SRT_INTEGRATION=true to run)", System.getenv("SRT_INTEGRATION") == "true") - val url = System.getenv("SRT_URL") ?: "srt://127.0.0.1:8890?streamid=publish:sinttest" - connectAndAssert(url) - } - - @Test - fun `connect and publish to srt server by ipv6`() { - assumeTrue("integration disabled (set SRT_INTEGRATION=true to run)", System.getenv("SRT_INTEGRATION") == "true") - val url = System.getenv("SRT_URL_V6") ?: "srt://[::1]:8890?streamid=publish:sinttestv6" - connectAndAssert(url) - } - - private fun connectAndAssert(url: String) { - val latch = CountDownLatch(1) - val success = AtomicBoolean(false) - val failReason = AtomicReference("") - - val client = SrtClient(object : ConnectChecker { - override fun onConnectionStarted(url: String) {} - override fun onConnectionSuccess() { - success.set(true) - latch.countDown() - } - override fun onConnectionFailed(reason: String) { - failReason.set(reason) - latch.countDown() - } - override fun onNewBitrate(bitrate: Long) {} - override fun onDisconnect() {} - override fun onAuthError() { - failReason.set("auth error") - latch.countDown() - } - override fun onAuthSuccess() {} - }) - - try { - client.connect(url) - assertTrue("no connection callback within timeout for $url", latch.await(20, TimeUnit.SECONDS)) - assertTrue("connection to $url failed: ${failReason.get()}", success.get()) - } finally { - client.disconnect() - } - } -} From 402431c7eb6888501c4a8f06e518d1858c301817 Mon Sep 17 00:00:00 2001 From: pedroSG94 Date: Sat, 18 Jul 2026 02:25:39 +0200 Subject: [PATCH 3/4] removing unused test code --- common/build.gradle.kts | 3 ++ .../main/java/com/pedro/common/Extensions.kt | 13 ----- encoder/build.gradle.kts | 3 ++ extra-sources/build.gradle.kts | 3 ++ library/build.gradle.kts | 3 ++ rtmp/build.gradle.kts | 3 ++ .../src/test/java/android/os/SystemClock.java | 11 ---- rtmp/src/test/java/android/util/Log.java | 53 ------------------- rtsp/build.gradle.kts | 1 - rtsp/src/test/java/android/util/Log.java | 53 ------------------- srt/build.gradle.kts | 1 - srt/src/test/java/android/os/SystemClock.java | 11 ---- srt/src/test/java/android/util/Log.java | 53 ------------------- udp/build.gradle.kts | 3 ++ udp/src/test/java/android/util/Log.java | 53 ------------------- whip/build.gradle.kts | 1 - 16 files changed, 18 insertions(+), 250 deletions(-) delete mode 100644 rtmp/src/test/java/android/os/SystemClock.java delete mode 100644 rtmp/src/test/java/android/util/Log.java delete mode 100644 rtsp/src/test/java/android/util/Log.java delete mode 100644 srt/src/test/java/android/os/SystemClock.java delete mode 100644 srt/src/test/java/android/util/Log.java delete mode 100644 udp/src/test/java/android/util/Log.java diff --git a/common/build.gradle.kts b/common/build.gradle.kts index dca4832078..5cce0586ba 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -17,6 +17,9 @@ android { isMinifyEnabled = false } } + testOptions { + unitTests.isReturnDefaultValues = true + } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 diff --git a/common/src/main/java/com/pedro/common/Extensions.kt b/common/src/main/java/com/pedro/common/Extensions.kt index e41e6ba4bf..06cdf9259b 100644 --- a/common/src/main/java/com/pedro/common/Extensions.kt +++ b/common/src/main/java/com/pedro/common/Extensions.kt @@ -144,13 +144,6 @@ fun newSingleThreadExecutor(queue: LinkedBlockingQueue): ExecutorServi return ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, queue) } -fun getSuspendContext(): Continuation { - return object : Continuation { - override val context = Dispatchers.IO - override fun resumeWith(result: Result) {} - } -} - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) fun CameraCharacteristics.secureGet(key: CameraCharacteristics.Key): T? { return try { get(key) } catch (e: IllegalArgumentException) { null } @@ -161,12 +154,6 @@ fun CaptureRequest.Builder.secureGet(key: CaptureRequest.Key): T? { return try { get(key) } catch (e: IllegalArgumentException) { null } } -fun String.getIndexes(char: Char): Array { - val indexes = mutableListOf() - forEachIndexed { index, c -> if (c == char) indexes.add(index) } - return indexes.toTypedArray() -} - fun Throwable.validMessage(): String { return (message ?: "").ifEmpty { javaClass.simpleName } } diff --git a/encoder/build.gradle.kts b/encoder/build.gradle.kts index d943a3117a..b31f773ea2 100644 --- a/encoder/build.gradle.kts +++ b/encoder/build.gradle.kts @@ -20,6 +20,9 @@ android { buildFeatures { buildConfig = true } + testOptions { + unitTests.isReturnDefaultValues = true + } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 diff --git a/extra-sources/build.gradle.kts b/extra-sources/build.gradle.kts index 63b4bc9ca9..f8efa5985e 100644 --- a/extra-sources/build.gradle.kts +++ b/extra-sources/build.gradle.kts @@ -17,6 +17,9 @@ android { isMinifyEnabled = false } } + testOptions { + unitTests.isReturnDefaultValues = true + } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 diff --git a/library/build.gradle.kts b/library/build.gradle.kts index f32f79b251..28bfb6bd93 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -17,6 +17,9 @@ android { isMinifyEnabled = false } } + testOptions { + unitTests.isReturnDefaultValues = true + } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 diff --git a/rtmp/build.gradle.kts b/rtmp/build.gradle.kts index 43cdee7d57..0972b57785 100644 --- a/rtmp/build.gradle.kts +++ b/rtmp/build.gradle.kts @@ -17,6 +17,9 @@ android { isMinifyEnabled = false } } + testOptions { + unitTests.isReturnDefaultValues = true + } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 diff --git a/rtmp/src/test/java/android/os/SystemClock.java b/rtmp/src/test/java/android/os/SystemClock.java deleted file mode 100644 index 6fb9cfac4d..0000000000 --- a/rtmp/src/test/java/android/os/SystemClock.java +++ /dev/null @@ -1,11 +0,0 @@ -package android.os; - -public class SystemClock { - public static long elapsedRealtime() { - return System.currentTimeMillis(); - } - - public static long elapsedRealtimeNano() { - return System.nanoTime(); - } -} diff --git a/rtmp/src/test/java/android/util/Log.java b/rtmp/src/test/java/android/util/Log.java deleted file mode 100644 index 71e7e323e6..0000000000 --- a/rtmp/src/test/java/android/util/Log.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2024 pedroSG94. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.util; - -/** - * Created by pedro on 16/6/21. - * - * Replace Log class of Android for testing purpose - */ -public class Log { - - public static int i(String tag, String message, Throwable throwable) { - return println(tag, message, throwable); - } - - public static int i(String tag, String message) { - return println(tag, message, null); - } - - public static int e(String tag, String message, Throwable throwable) { - return printlnError(tag, message, throwable); - } - - public static int e(String tag, String message) { - return printlnError(tag, message, null); - } - - private static int println(String tag, String message, Throwable throwable) { - System.out.println(tag + ": " + message); - if (throwable != null) throwable.printStackTrace(); - return 0; - } - - private static int printlnError(String tag, String message, Throwable throwable) { - System.err.println(tag + ": " + message); - if (throwable != null) throwable.printStackTrace(); - return 0; - } -} diff --git a/rtsp/build.gradle.kts b/rtsp/build.gradle.kts index b950d4edab..82b4778b12 100644 --- a/rtsp/build.gradle.kts +++ b/rtsp/build.gradle.kts @@ -21,7 +21,6 @@ android { buildConfig = true } testOptions { - //allow use android classes like Log or SystemClock in integration tests (return 0 instead of crash) unitTests.isReturnDefaultValues = true } compileOptions { diff --git a/rtsp/src/test/java/android/util/Log.java b/rtsp/src/test/java/android/util/Log.java deleted file mode 100644 index 71e7e323e6..0000000000 --- a/rtsp/src/test/java/android/util/Log.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2024 pedroSG94. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.util; - -/** - * Created by pedro on 16/6/21. - * - * Replace Log class of Android for testing purpose - */ -public class Log { - - public static int i(String tag, String message, Throwable throwable) { - return println(tag, message, throwable); - } - - public static int i(String tag, String message) { - return println(tag, message, null); - } - - public static int e(String tag, String message, Throwable throwable) { - return printlnError(tag, message, throwable); - } - - public static int e(String tag, String message) { - return printlnError(tag, message, null); - } - - private static int println(String tag, String message, Throwable throwable) { - System.out.println(tag + ": " + message); - if (throwable != null) throwable.printStackTrace(); - return 0; - } - - private static int printlnError(String tag, String message, Throwable throwable) { - System.err.println(tag + ": " + message); - if (throwable != null) throwable.printStackTrace(); - return 0; - } -} diff --git a/srt/build.gradle.kts b/srt/build.gradle.kts index da2de0a740..b5b0ec419d 100644 --- a/srt/build.gradle.kts +++ b/srt/build.gradle.kts @@ -18,7 +18,6 @@ android { } } testOptions { - //allow use android classes like Log or SystemClock in integration tests (return 0 instead of crash) unitTests.isReturnDefaultValues = true } compileOptions { diff --git a/srt/src/test/java/android/os/SystemClock.java b/srt/src/test/java/android/os/SystemClock.java deleted file mode 100644 index 6fb9cfac4d..0000000000 --- a/srt/src/test/java/android/os/SystemClock.java +++ /dev/null @@ -1,11 +0,0 @@ -package android.os; - -public class SystemClock { - public static long elapsedRealtime() { - return System.currentTimeMillis(); - } - - public static long elapsedRealtimeNano() { - return System.nanoTime(); - } -} diff --git a/srt/src/test/java/android/util/Log.java b/srt/src/test/java/android/util/Log.java deleted file mode 100644 index 71e7e323e6..0000000000 --- a/srt/src/test/java/android/util/Log.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2024 pedroSG94. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.util; - -/** - * Created by pedro on 16/6/21. - * - * Replace Log class of Android for testing purpose - */ -public class Log { - - public static int i(String tag, String message, Throwable throwable) { - return println(tag, message, throwable); - } - - public static int i(String tag, String message) { - return println(tag, message, null); - } - - public static int e(String tag, String message, Throwable throwable) { - return printlnError(tag, message, throwable); - } - - public static int e(String tag, String message) { - return printlnError(tag, message, null); - } - - private static int println(String tag, String message, Throwable throwable) { - System.out.println(tag + ": " + message); - if (throwable != null) throwable.printStackTrace(); - return 0; - } - - private static int printlnError(String tag, String message, Throwable throwable) { - System.err.println(tag + ": " + message); - if (throwable != null) throwable.printStackTrace(); - return 0; - } -} diff --git a/udp/build.gradle.kts b/udp/build.gradle.kts index 5e23dd887a..7147ca60e0 100644 --- a/udp/build.gradle.kts +++ b/udp/build.gradle.kts @@ -17,6 +17,9 @@ android { isMinifyEnabled = false } } + testOptions { + unitTests.isReturnDefaultValues = true + } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 diff --git a/udp/src/test/java/android/util/Log.java b/udp/src/test/java/android/util/Log.java deleted file mode 100644 index 71e7e323e6..0000000000 --- a/udp/src/test/java/android/util/Log.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2024 pedroSG94. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.util; - -/** - * Created by pedro on 16/6/21. - * - * Replace Log class of Android for testing purpose - */ -public class Log { - - public static int i(String tag, String message, Throwable throwable) { - return println(tag, message, throwable); - } - - public static int i(String tag, String message) { - return println(tag, message, null); - } - - public static int e(String tag, String message, Throwable throwable) { - return printlnError(tag, message, throwable); - } - - public static int e(String tag, String message) { - return printlnError(tag, message, null); - } - - private static int println(String tag, String message, Throwable throwable) { - System.out.println(tag + ": " + message); - if (throwable != null) throwable.printStackTrace(); - return 0; - } - - private static int printlnError(String tag, String message, Throwable throwable) { - System.err.println(tag + ": " + message); - if (throwable != null) throwable.printStackTrace(); - return 0; - } -} diff --git a/whip/build.gradle.kts b/whip/build.gradle.kts index 0ede7582d2..c86da0ed34 100644 --- a/whip/build.gradle.kts +++ b/whip/build.gradle.kts @@ -18,7 +18,6 @@ android { } } testOptions { - //allow use android classes like Log or SystemClock in integration tests (return 0 instead of crash) unitTests.isReturnDefaultValues = true } compileOptions { From ab0b6859600cc1036304056f732fce6fb2f775c4 Mon Sep 17 00:00:00 2001 From: pedroSG94 Date: Sat, 18 Jul 2026 02:48:54 +0200 Subject: [PATCH 4/4] refactor static mocks --- .../com/pedro/common/BitrateManagerTest.kt | 58 ++++++----- .../pedro/common/util/MainDispatcherRule.kt | 39 -------- .../test/java/com/pedro/common/util/Utils.kt | 38 ------- rtsp/src/test/java/com/pedro/rtsp/Utils.kt | 38 ------- .../com/pedro/rtsp/rtcp/RtcpReportTest.kt | 99 +++++++++---------- srt/src/test/java/com/pedro/srt/Utils.kt | 14 --- .../java/com/pedro/srt/mpeg2ts/PesTest.kt | 67 +++++++------ .../java/com/pedro/srt/mpeg2ts/PsiTest.kt | 96 +++++++++--------- 8 files changed, 165 insertions(+), 284 deletions(-) delete mode 100644 common/src/test/java/com/pedro/common/util/MainDispatcherRule.kt delete mode 100644 common/src/test/java/com/pedro/common/util/Utils.kt delete mode 100644 rtsp/src/test/java/com/pedro/rtsp/Utils.kt diff --git a/common/src/test/java/com/pedro/common/BitrateManagerTest.kt b/common/src/test/java/com/pedro/common/BitrateManagerTest.kt index e668b6a0f6..defa7ffcf2 100644 --- a/common/src/test/java/com/pedro/common/BitrateManagerTest.kt +++ b/common/src/test/java/com/pedro/common/BitrateManagerTest.kt @@ -16,16 +16,22 @@ package com.pedro.common -import com.pedro.common.util.MainDispatcherRule -import com.pedro.common.util.Utils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test +import org.junit.rules.TestWatcher +import org.junit.runner.Description import org.junit.runner.RunWith import org.mockito.Mock +import org.mockito.MockedStatic import org.mockito.Mockito import org.mockito.junit.MockitoJUnitRunner import org.mockito.kotlin.argumentCaptor @@ -39,41 +45,49 @@ import org.mockito.kotlin.verify @RunWith(MockitoJUnitRunner::class) class BitrateManagerTest { + @OptIn(ExperimentalCoroutinesApi::class) @get:Rule - val mainDispatcherRule = MainDispatcherRule() + val mainDispatcherRule = object: TestWatcher() { + override fun starting(description: Description) { + Dispatchers.setMain(UnconfinedTestDispatcher()) + } + + override fun finished(description: Description) { + Dispatchers.resetMain() + } + } @Mock private lateinit var connectChecker: ConnectChecker - private val timeUtilsMocked = Mockito.mockStatic(TimeUtils::class.java) + private lateinit var timeUtilsMocked: MockedStatic private var fakeTime = 7502849023L @Before fun setup() { + timeUtilsMocked = Mockito.mockStatic(TimeUtils::class.java) timeUtilsMocked.`when`(TimeUtils::getCurrentTimeMillis).then { fakeTime } } @After fun teardown() { - fakeTime = 7502849023L + timeUtilsMocked.close() } @Test fun `WHEN set multiple values THEN return total of values each second`() = runTest { - Utils.useStatics(listOf(timeUtilsMocked)) { - val bitrateManager = BitrateManager(connectChecker) - val fakeValues = arrayOf(100L, 200L, 300L, 400L, 500L) - var expectedResult = 0L - fakeValues.forEach { - bitrateManager.calculateBitrate(it) - expectedResult += it - } - fakeTime += 1000 - val value = 100L - bitrateManager.calculateBitrate(value) - expectedResult += value - val resultValue = argumentCaptor() - verify(connectChecker, times(1)).onNewBitrate(resultValue.capture()) - val marginError = 20 - assertTrue(expectedResult - marginError <= resultValue.firstValue && resultValue.firstValue <= expectedResult + marginError) + val bitrateManager = BitrateManager(connectChecker) + val fakeValues = arrayOf(100L, 200L, 300L, 400L, 500L) + var expectedResult = 0L + fakeValues.forEach { + bitrateManager.calculateBitrate(it) + expectedResult += it } + fakeTime += 1000 + val value = 100L + bitrateManager.calculateBitrate(value) + expectedResult += value + val resultValue = argumentCaptor() + verify(connectChecker, times(1)).onNewBitrate(resultValue.capture()) + val marginError = 20 + assertTrue(expectedResult - marginError <= resultValue.firstValue && resultValue.firstValue <= expectedResult + marginError) } -} \ No newline at end of file +} diff --git a/common/src/test/java/com/pedro/common/util/MainDispatcherRule.kt b/common/src/test/java/com/pedro/common/util/MainDispatcherRule.kt deleted file mode 100644 index 5f5d089581..0000000000 --- a/common/src/test/java/com/pedro/common/util/MainDispatcherRule.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 2024 pedroSG94. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.pedro.common.util - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestDispatcher -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.setMain -import org.junit.rules.TestWatcher -import org.junit.runner.Description - -@OptIn(ExperimentalCoroutinesApi::class) -class MainDispatcherRule( - private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher() -) : TestWatcher() { - override fun starting(description: Description) { - Dispatchers.setMain(testDispatcher) - } - - override fun finished(description: Description) { - Dispatchers.resetMain() - } -} \ No newline at end of file diff --git a/common/src/test/java/com/pedro/common/util/Utils.kt b/common/src/test/java/com/pedro/common/util/Utils.kt deleted file mode 100644 index 65560698d7..0000000000 --- a/common/src/test/java/com/pedro/common/util/Utils.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2024 pedroSG94. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.pedro.common.util - -import org.mockito.MockedStatic - -/** - * Created by pedro on 1/9/23. - */ -object Utils { - - suspend fun useStatics(statics: List>, callback: suspend () -> Unit) { - val list = statics.toMutableList() - if (list.isEmpty()) callback() - else if (list.size == 1) { - list[0].use { - callback() - } - } else { - val value = list.removeAt(0) - value.use { useStatics(list, callback) } - } - } -} diff --git a/rtsp/src/test/java/com/pedro/rtsp/Utils.kt b/rtsp/src/test/java/com/pedro/rtsp/Utils.kt deleted file mode 100644 index 75c36a2845..0000000000 --- a/rtsp/src/test/java/com/pedro/rtsp/Utils.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2024 pedroSG94. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.pedro.rtsp - -import org.mockito.MockedStatic - -/** - * Created by pedro on 1/9/23. - */ -object Utils { - - suspend fun useStatics(statics: List>, callback: suspend () -> Unit) { - val list = statics.toMutableList() - if (list.isEmpty()) callback() - else if (list.size == 1) { - list[0].use { - callback() - } - } else { - val value = list.removeAt(0) - value.use { useStatics(list, callback) } - } - } -} diff --git a/rtsp/src/test/java/com/pedro/rtsp/rtcp/RtcpReportTest.kt b/rtsp/src/test/java/com/pedro/rtsp/rtcp/RtcpReportTest.kt index 349ef419ae..7f88332c2b 100644 --- a/rtsp/src/test/java/com/pedro/rtsp/rtcp/RtcpReportTest.kt +++ b/rtsp/src/test/java/com/pedro/rtsp/rtcp/RtcpReportTest.kt @@ -21,7 +21,6 @@ import com.pedro.common.socket.base.SocketType import com.pedro.common.socket.base.StreamSocket import com.pedro.common.socket.base.TcpStreamSocket import com.pedro.common.socket.base.UdpStreamSocket -import com.pedro.rtsp.Utils import com.pedro.rtsp.rtsp.Protocol import com.pedro.rtsp.rtsp.RtpFrame import com.pedro.rtsp.utils.RtpConstants @@ -34,6 +33,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock +import org.mockito.MockedStatic import org.mockito.Mockito import org.mockito.junit.MockitoJUnitRunner import org.mockito.kotlin.argumentCaptor @@ -51,74 +51,71 @@ class RtcpReportTest { @Mock private lateinit var tcpSocket: TcpStreamSocket - private val timeUtilsMocked = Mockito.mockStatic(TimeUtils::class.java) + private lateinit var timeUtilsMocked: MockedStatic private var fakeTime = 7502849023L @Before fun setup() { + timeUtilsMocked = Mockito.mockStatic(TimeUtils::class.java) timeUtilsMocked.`when`(TimeUtils::getCurrentTimeMillis).then { fakeTime } } @After fun teardown() { - fakeTime = 7502849023L + timeUtilsMocked.close() } @Test fun `GIVEN multiple video or audio rtp frames WHEN update rtcp tcp send THEN send only 1 of video and 1 of audio each 3 seconds`() = runTest { - Utils.useStatics(listOf(timeUtilsMocked)) { - val rtpTracks = RtpTracks() - val senderReportTcp = BaseSenderReport.getInstance(rtpTracks, SocketType.JAVA, Protocol.TCP, "127.0.0.1", - StreamSocket.DEFAULT_TIMEOUT, 0, 1, 2, 3) - senderReportTcp.setSocket(tcpSocket) - senderReportTcp.setSSRC(0, 1) - val fakeFrameVideo = RtpFrame(byteArrayOf(0x00, 0x00, 0x00), 0, 3, rtpTracks.trackVideo) - val fakeFrameAudio = RtpFrame(byteArrayOf(0x00, 0x00, 0x00), 0, 3, rtpTracks.trackAudio) + val rtpTracks = RtpTracks() + val senderReportTcp = BaseSenderReport.getInstance(rtpTracks, SocketType.JAVA, Protocol.TCP, "127.0.0.1", + StreamSocket.DEFAULT_TIMEOUT, 0, 1, 2, 3) + senderReportTcp.setSocket(tcpSocket) + senderReportTcp.setSSRC(0, 1) + val fakeFrameVideo = RtpFrame(byteArrayOf(0x00, 0x00, 0x00), 0, 3, rtpTracks.trackVideo) + val fakeFrameAudio = RtpFrame(byteArrayOf(0x00, 0x00, 0x00), 0, 3, rtpTracks.trackAudio) - (0..10).forEach { value -> - val frame = if (value % 2 == 0) fakeFrameVideo else fakeFrameAudio - senderReportTcp.update(frame) - } - val resultValue = argumentCaptor() - withContext(Dispatchers.IO) { - verify(tcpSocket, times((2))).write(resultValue.capture()) - } - fakeTime += 3_000 //wait until next interval - (0..10).forEach { value -> - val frame = if (value % 2 == 0) fakeFrameVideo else fakeFrameAudio - senderReportTcp.update(frame) - } - withContext(Dispatchers.IO) { - verify(tcpSocket, times((4))).write(resultValue.capture()) - } + (0..10).forEach { value -> + val frame = if (value % 2 == 0) fakeFrameVideo else fakeFrameAudio + senderReportTcp.update(frame) + } + val resultValue = argumentCaptor() + withContext(Dispatchers.IO) { + verify(tcpSocket, times((2))).write(resultValue.capture()) + } + fakeTime += 3_000 //wait until next interval + (0..10).forEach { value -> + val frame = if (value % 2 == 0) fakeFrameVideo else fakeFrameAudio + senderReportTcp.update(frame) + } + withContext(Dispatchers.IO) { + verify(tcpSocket, times((4))).write(resultValue.capture()) } } @Test fun `GIVEN multiple video or audio rtp frames WHEN update rtcp udp send THEN send only 1 of video and 1 of audio each 3 seconds`() = runTest { - Utils.useStatics(listOf(timeUtilsMocked)) { - val rtpTracks = RtpTracks() - val senderReportUdp = SenderReportUdp(rtpTracks, udpSocket, udpSocket) - senderReportUdp.setSocket(tcpSocket) - senderReportUdp.setSSRC(0, 1) - val fakeFrameVideo = RtpFrame(byteArrayOf(0x00, 0x00, 0x00), 0, 3, rtpTracks.trackVideo) - val fakeFrameAudio = RtpFrame(byteArrayOf(0x00, 0x00, 0x00), 0, 3, rtpTracks.trackAudio) - (0..10).forEach { value -> - val frame = if (value % 2 == 0) fakeFrameVideo else fakeFrameAudio - senderReportUdp.update(frame) - } - val resultValue = argumentCaptor() - withContext(Dispatchers.IO) { - verify(udpSocket, times((2))).write(resultValue.capture()) - } - fakeTime += 3_000 //wait until next interval - (0..10).forEach { value -> - val frame = if (value % 2 == 0) fakeFrameVideo else fakeFrameAudio - senderReportUdp.update(frame) - } - withContext(Dispatchers.IO) { - verify(udpSocket, times((4))).write(resultValue.capture()) - } + val rtpTracks = RtpTracks() + val senderReportUdp = SenderReportUdp(rtpTracks, udpSocket, udpSocket) + senderReportUdp.setSocket(tcpSocket) + senderReportUdp.setSSRC(0, 1) + val fakeFrameVideo = RtpFrame(byteArrayOf(0x00, 0x00, 0x00), 0, 3, rtpTracks.trackVideo) + val fakeFrameAudio = RtpFrame(byteArrayOf(0x00, 0x00, 0x00), 0, 3, rtpTracks.trackAudio) + (0..10).forEach { value -> + val frame = if (value % 2 == 0) fakeFrameVideo else fakeFrameAudio + senderReportUdp.update(frame) + } + val resultValue = argumentCaptor() + withContext(Dispatchers.IO) { + verify(udpSocket, times((2))).write(resultValue.capture()) + } + fakeTime += 3_000 //wait until next interval + (0..10).forEach { value -> + val frame = if (value % 2 == 0) fakeFrameVideo else fakeFrameAudio + senderReportUdp.update(frame) + } + withContext(Dispatchers.IO) { + verify(udpSocket, times((4))).write(resultValue.capture()) } } -} \ No newline at end of file +} diff --git a/srt/src/test/java/com/pedro/srt/Utils.kt b/srt/src/test/java/com/pedro/srt/Utils.kt index cf06eb0e7e..236cf4e791 100644 --- a/srt/src/test/java/com/pedro/srt/Utils.kt +++ b/srt/src/test/java/com/pedro/srt/Utils.kt @@ -17,7 +17,6 @@ package com.pedro.srt import org.junit.Assert.assertEquals -import org.mockito.MockedStatic /** * Created by pedro on 1/9/23. @@ -26,17 +25,4 @@ object Utils { fun assertObjectEquals(actual: Any, expected: Any) { assertEquals(actual.toString(), expected.toString()) } - - suspend fun useStatics(statics: List>, callback: suspend () -> Unit) { - val list = statics.toMutableList() - if (list.isEmpty()) callback() - else if (list.size == 1) { - list[0].use { - callback() - } - } else { - val value = list.removeAt(0) - value.use { useStatics(list, callback) } - } - } } diff --git a/srt/src/test/java/com/pedro/srt/mpeg2ts/PesTest.kt b/srt/src/test/java/com/pedro/srt/mpeg2ts/PesTest.kt index bc4b335d5a..6c2db4bb14 100644 --- a/srt/src/test/java/com/pedro/srt/mpeg2ts/PesTest.kt +++ b/srt/src/test/java/com/pedro/srt/mpeg2ts/PesTest.kt @@ -17,16 +17,17 @@ package com.pedro.srt.mpeg2ts import com.pedro.common.TimeUtils -import com.pedro.srt.Utils import com.pedro.srt.mpeg2ts.psi.PsiManager import com.pedro.srt.mpeg2ts.service.Mpeg2TsService import com.pedro.srt.srt.packets.SrtPacket import com.pedro.srt.utils.Constants import com.pedro.srt.utils.chunkPackets import kotlinx.coroutines.test.runTest +import org.junit.After import org.junit.Assert.assertArrayEquals import org.junit.Before import org.junit.Test +import org.mockito.MockedStatic import org.mockito.Mockito import java.nio.ByteBuffer @@ -37,48 +38,50 @@ import java.nio.ByteBuffer class PesTest { private val service = Mpeg2TsService() - private val timeUtilsMock = Mockito.mockStatic(TimeUtils::class.java) + private lateinit var timeUtilsMock: MockedStatic private val chunkSize = (Constants.MTU - SrtPacket.headerSize) / MpegTsPacketizer.packetSize @Before fun setup() { + timeUtilsMock = Mockito.mockStatic(TimeUtils::class.java) timeUtilsMock.`when`(TimeUtils::getCurrentTimeMicro).thenReturn(700000) } + @After + fun teardown() { + timeUtilsMock.close() + } + @Test fun `GIVEN a fake aac buffer WHEN create a mpegts packet with pes packet THEN get the expected buffer`() = runTest { - Utils.useStatics(listOf(timeUtilsMock)) { - val data = ByteBuffer.wrap( - ByteArray(188) { 0xAA.toByte() } - ) - val expected = ByteBuffer.wrap( - byteArrayOf(71, 65, 0, 48, 7, 80, 0, 0, 123, 12, 126, 0, 0, 0, 1, -64, 0, -60, -127, -128, 5, 33, 0, 7, -40, 97, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, 71, 1, 0, 49, -99, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86) - ) - val psiManager = PsiManager(service) - val mpegTsPacketizer = MpegTsPacketizer(psiManager) - val pes = Pes(256, true, PesType.AUDIO, 1400000, data) - val buffer = mpegTsPacketizer.write(listOf(pes)).chunkPackets(chunkSize)[0] - assertArrayEquals(expected.array(), buffer) - } + val data = ByteBuffer.wrap( + ByteArray(188) { 0xAA.toByte() } + ) + val expected = ByteBuffer.wrap( + byteArrayOf(71, 65, 0, 48, 7, 80, 0, 0, 123, 12, 126, 0, 0, 0, 1, -64, 0, -60, -127, -128, 5, 33, 0, 7, -40, 97, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, 71, 1, 0, 49, -99, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86, -86) + ) + val psiManager = PsiManager(service) + val mpegTsPacketizer = MpegTsPacketizer(psiManager) + val pes = Pes(256, true, PesType.AUDIO, 1400000, data) + val buffer = mpegTsPacketizer.write(listOf(pes)).chunkPackets(chunkSize)[0] + assertArrayEquals(expected.array(), buffer) } @Test fun `GIVEN a fake aac buffer small WHEN create a mpegts packet with pes packet THEN get the expected buffer`() = runTest { - Utils.useStatics(listOf(timeUtilsMock)) { - val data = ByteBuffer.wrap( - ByteArray(10) { 0xAA.toByte() } - ) - val expected = ByteBuffer.wrap( - byteArrayOf(71, 65, 0, 48, -97, 80, 0, 0, 123, 12, 126, 0) + //ts header + adaptation field (len 159, flags, pcr) - ByteArray(152) { 0xFF.toByte() } + //adaptation field stuffing - byteArrayOf(0, 0, 1, -64, 0, 18, -127, -128, 5, 33, 0, 7, -40, 97) + //pes header - ByteArray(10) { 0xAA.toByte() } //payload - ) - val psiManager = PsiManager(service) - val mpegTsPacketizer = MpegTsPacketizer(psiManager) - val pes = Pes(256, true, PesType.AUDIO, 1400000, data) - val buffer = mpegTsPacketizer.write(listOf(pes)).chunkPackets(chunkSize)[0] - assertArrayEquals(expected.array(), buffer) - } + val data = ByteBuffer.wrap( + ByteArray(10) { 0xAA.toByte() } + ) + val expected = ByteBuffer.wrap( + byteArrayOf(71, 65, 0, 48, -97, 80, 0, 0, 123, 12, 126, 0) + //ts header + adaptation field (len 159, flags, pcr) + ByteArray(152) { 0xFF.toByte() } + //adaptation field stuffing + byteArrayOf(0, 0, 1, -64, 0, 18, -127, -128, 5, 33, 0, 7, -40, 97) + //pes header + ByteArray(10) { 0xAA.toByte() } //payload + ) + val psiManager = PsiManager(service) + val mpegTsPacketizer = MpegTsPacketizer(psiManager) + val pes = Pes(256, true, PesType.AUDIO, 1400000, data) + val buffer = mpegTsPacketizer.write(listOf(pes)).chunkPackets(chunkSize)[0] + assertArrayEquals(expected.array(), buffer) } -} \ No newline at end of file +} diff --git a/srt/src/test/java/com/pedro/srt/mpeg2ts/PsiTest.kt b/srt/src/test/java/com/pedro/srt/mpeg2ts/PsiTest.kt index 04dce61798..bfff941fa0 100644 --- a/srt/src/test/java/com/pedro/srt/mpeg2ts/PsiTest.kt +++ b/srt/src/test/java/com/pedro/srt/mpeg2ts/PsiTest.kt @@ -17,7 +17,6 @@ package com.pedro.srt.mpeg2ts import com.pedro.common.TimeUtils -import com.pedro.srt.Utils import com.pedro.srt.mpeg2ts.psi.Pat import com.pedro.srt.mpeg2ts.psi.Pmt import com.pedro.srt.mpeg2ts.psi.PsiManager @@ -30,6 +29,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock +import org.mockito.MockedStatic import org.mockito.Mockito import org.mockito.junit.MockitoJUnitRunner import java.nio.ByteBuffer @@ -43,77 +43,73 @@ class PsiTest { @Mock lateinit var pid: Pid private val service by lazy { Mpeg2TsService(pid = pid) } - private val timeUtilsMock = Mockito.mockStatic(TimeUtils::class.java) + private lateinit var timeUtilsMock: MockedStatic @Before - fun setup() = runTest { + fun setup() { + timeUtilsMock = Mockito.mockStatic(TimeUtils::class.java) timeUtilsMock.`when`(TimeUtils::getCurrentTimeMicro).thenReturn(700000) Mockito.`when`(pid.generatePID()).thenReturn(Pid.MIN_VALUE.toShort()) } @After fun teardown() { + timeUtilsMock.close() service.clear() } @Test fun `GIVEN a sdt table WHEN create mpegts packet with that table THEN get expected buffer`() = runTest { - Utils.useStatics(listOf(timeUtilsMock)) { - val expected = ByteBuffer.wrap( - byteArrayOf(71, 64, 17, 16, 0, 66, -16, 49, 0, 1, -63, 0, 0, -1, 1, -1, 70, -104, -4, -128, 32, 72, 30, 1, 13, 99, 111, 109, 46, 112, 101, 100, 114, 111, 46, 115, 114, 116, 14, 77, 112, 101, 103, 50, 84, 115, 83, 101, 114, 118, 105, 99, 101, 72, 33, 81, -10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1) - ) - val psiManager = PsiManager(service) - val mpegTsPacketizer = MpegTsPacketizer(psiManager) - val sdt = Sdt(1, 0, service = service) - val mpeg2tsPackets = mpegTsPacketizer.write(listOf(sdt)) - val chunked = mpeg2tsPackets - val size = chunked.sumOf { it.size } - val buffer = ByteBuffer.allocate(size) - chunked.forEach { - buffer.put(it) - } - assertArrayEquals(expected.array(), buffer.array()) + val expected = ByteBuffer.wrap( + byteArrayOf(71, 64, 17, 16, 0, 66, -16, 49, 0, 1, -63, 0, 0, -1, 1, -1, 70, -104, -4, -128, 32, 72, 30, 1, 13, 99, 111, 109, 46, 112, 101, 100, 114, 111, 46, 115, 114, 116, 14, 77, 112, 101, 103, 50, 84, 115, 83, 101, 114, 118, 105, 99, 101, 72, 33, 81, -10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1) + ) + val psiManager = PsiManager(service) + val mpegTsPacketizer = MpegTsPacketizer(psiManager) + val sdt = Sdt(1, 0, service = service) + val mpeg2tsPackets = mpegTsPacketizer.write(listOf(sdt)) + val chunked = mpeg2tsPackets + val size = chunked.sumOf { it.size } + val buffer = ByteBuffer.allocate(size) + chunked.forEach { + buffer.put(it) } + assertArrayEquals(expected.array(), buffer.array()) } @Test fun `GIVEN a pmt table WHEN create mpegts packet with that table THEN get expected buffer`() = runTest { - Utils.useStatics(listOf(timeUtilsMock)) { - service.addTrack(Codec.AAC) - val expected = ByteBuffer.wrap( - byteArrayOf(71, 64, 32, 16, 0, 2, -80, 18, 70, -104, -63, 0, 0, -32, 32, -16, 0, 15, -32, 32, -16, 0, 121, -48, -32, -74, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1) - ) - val psiManager = PsiManager(service) - val mpegTsPacketizer = MpegTsPacketizer(psiManager) - val pmt = Pmt(32, 0, service = service) - val mpeg2tsPackets = mpegTsPacketizer.write(listOf(pmt)) - val chunked = mpeg2tsPackets - val size = chunked.sumOf { it.size } - val buffer = ByteBuffer.allocate(size) - chunked.forEach { - buffer.put(it) - } - assertArrayEquals(expected.array(), buffer.array()) + service.addTrack(Codec.AAC) + val expected = ByteBuffer.wrap( + byteArrayOf(71, 64, 32, 16, 0, 2, -80, 18, 70, -104, -63, 0, 0, -32, 32, -16, 0, 15, -32, 32, -16, 0, 121, -48, -32, -74, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1) + ) + val psiManager = PsiManager(service) + val mpegTsPacketizer = MpegTsPacketizer(psiManager) + val pmt = Pmt(32, 0, service = service) + val mpeg2tsPackets = mpegTsPacketizer.write(listOf(pmt)) + val chunked = mpeg2tsPackets + val size = chunked.sumOf { it.size } + val buffer = ByteBuffer.allocate(size) + chunked.forEach { + buffer.put(it) } + assertArrayEquals(expected.array(), buffer.array()) } @Test fun `GIVEN a pat table WHEN create mpegts packet with that table THEN get expected buffer`() = runTest { - Utils.useStatics(listOf(timeUtilsMock)) { - val expected = ByteBuffer.wrap( - byteArrayOf(71, 64, 0, 16, 0, 0, -80, 13, 1, 0, -61, 0, 0, 70, -104, -32, 0, -30, -46, -114, -23, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1) - ) - val psiManager = PsiManager(service) - val mpegTsPacketizer = MpegTsPacketizer(psiManager) - val pat = Pat(256, 1, service = service) - val mpeg2tsPackets = mpegTsPacketizer.write(listOf(pat)) - val chunked = mpeg2tsPackets - val size = chunked.sumOf { it.size } - val buffer = ByteBuffer.allocate(size) - chunked.forEach { - buffer.put(it) - } - assertArrayEquals(expected.array(), buffer.array()) + val expected = ByteBuffer.wrap( + byteArrayOf(71, 64, 0, 16, 0, 0, -80, 13, 1, 0, -61, 0, 0, 70, -104, -32, 0, -30, -46, -114, -23, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1) + ) + val psiManager = PsiManager(service) + val mpegTsPacketizer = MpegTsPacketizer(psiManager) + val pat = Pat(256, 1, service = service) + val mpeg2tsPackets = mpegTsPacketizer.write(listOf(pat)) + val chunked = mpeg2tsPackets + val size = chunked.sumOf { it.size } + val buffer = ByteBuffer.allocate(size) + chunked.forEach { + buffer.put(it) } + assertArrayEquals(expected.array(), buffer.array()) } -} \ No newline at end of file +}