diff --git a/core/src/main/java/io/github/thibaultbee/streampack/core/elements/endpoints/composites/muxers/ts/descriptors/AdaptationField.kt b/core/src/main/java/io/github/thibaultbee/streampack/core/elements/endpoints/composites/muxers/ts/descriptors/AdaptationField.kt index be451e5d5..f7e2caa6b 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/core/elements/endpoints/composites/muxers/ts/descriptors/AdaptationField.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/core/elements/endpoints/composites/muxers/ts/descriptors/AdaptationField.kt @@ -86,12 +86,16 @@ class AdaptationField( return buffer } - private fun addClockReference(buffer: ByteBuffer, timestamp: Long) { - val pcrBase = - (TSConst.SYSTEM_CLOCK_FREQ * timestamp / 1000000 /* µs -> s */ / 300) % 2.toDouble() - .pow(33) - .toLong() - val pcrExt = (TSConst.SYSTEM_CLOCK_FREQ * timestamp / 1000000 /* µs -> s */) % 300 + internal fun addClockReference(buffer: ByteBuffer, timestamp: Long) { + // SYSTEM_CLOCK_FREQ * timestamp is evaluated before any division: with + // timestamp in microseconds (device uptime based), the product overflows + // Long after ~95h of uptime (~3.4e11 µs) and silently wraps in Kotlin/ + // JVM, producing a garbage PCR on every subsequent frame. Rewritten in + // the same order as RootEncoder's AdaptationField (timestamp * 9 / 100): + // mathematically equivalent (27_000_000 / 1_000_000 / 300 = 9/100) but + // safe for timestamps up to ~10^17 µs. + val pcrBase = (timestamp * 9 / 100) % (1L shl 33) + val pcrExt = (timestamp * 27) % 300 /** * PCR Base -> 33 bits diff --git a/core/src/test/java/io/github/thibaultbee/streampack/core/elements/endpoints/composites/muxers/ts/descriptors/AdaptationFieldTest.kt b/core/src/test/java/io/github/thibaultbee/streampack/core/elements/endpoints/composites/muxers/ts/descriptors/AdaptationFieldTest.kt index 223c00938..ad9469d1d 100644 --- a/core/src/test/java/io/github/thibaultbee/streampack/core/elements/endpoints/composites/muxers/ts/descriptors/AdaptationFieldTest.kt +++ b/core/src/test/java/io/github/thibaultbee/streampack/core/elements/endpoints/composites/muxers/ts/descriptors/AdaptationFieldTest.kt @@ -16,9 +16,13 @@ package io.github.thibaultbee.streampack.core.elements.endpoints.composites.muxers.ts.descriptors import io.github.thibaultbee.streampack.core.elements.endpoints.composites.muxers.ts.TSResourcesUtils +import io.github.thibaultbee.streampack.core.elements.endpoints.composites.muxers.ts.utils.TSConst import io.github.thibaultbee.streampack.core.elements.utils.extensions.toByteArray import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals import org.junit.Test +import java.math.BigInteger +import java.nio.ByteBuffer class AdaptationFieldTest { @@ -43,4 +47,49 @@ class AdaptationFieldTest { adaptationField.toByteBuffer().toByteArray() ) } + + @Test + fun `pcr does not overflow after long device uptime`() { + // 400_000_000_000 microseconds ~= 4.63 days. With the pre-fix formula + // (SYSTEM_CLOCK_FREQ * timestamp evaluated before any division), + // 27_000_000 * 400_000_000_000 = 1.08e19 overflows Long.MAX_VALUE + // (~9.223e18) and silently wraps in Kotlin/JVM, corrupting the PCR of + // every frame past ~95h of uptime (~3.4e11 us). The expected PCR is + // computed with BigInteger (same original semantics, immune to Long + // overflow at this size) instead of with the patched formula itself. + val timestamp = 400_000_000_000L + + val adaptationField = AdaptationField( + discontinuityIndicator = false, + randomAccessIndicator = true, + elementaryStreamPriorityIndicator = false, + programClockReference = timestamp, + originalProgramClockReference = null, + spliceCountdown = null, + transportPrivateData = null, + adaptationFieldExtension = null + ) + + // addClockReference writes pcrBase (4 bytes, 33 bits shifted left by + // one) then pcrExt + reserved (2 bytes). + val buffer = ByteBuffer.allocate(6) + adaptationField.addClockReference(buffer, timestamp) + buffer.rewind() + val pcrBaseHigh32 = buffer.int.toLong() and 0xFFFFFFFFL + val tail = buffer.short.toInt() and 0xFFFF + val actualPcrBase = (pcrBaseHigh32 shl 1) or ((tail.toLong() shr 15) and 0x1L) + val actualPcrExt = tail and 0x1FF + + val freq = BigInteger.valueOf(TSConst.SYSTEM_CLOCK_FREQ.toLong()) + val ts = BigInteger.valueOf(timestamp) + val twoPow33 = BigInteger.ONE.shiftLeft(33) + val expectedPcrBase = + freq.multiply(ts).divide(BigInteger.valueOf(1_000_000)).divide(BigInteger.valueOf(300)) + .mod(twoPow33).toLong() + val expectedPcrExt = + freq.multiply(ts).divide(BigInteger.valueOf(1_000_000)).mod(BigInteger.valueOf(300)).toInt() + + assertEquals(expectedPcrBase, actualPcrBase) + assertEquals(expectedPcrExt, actualPcrExt) + } } \ No newline at end of file