From f8117d2b1cc097e419e65b2aae670e36a20c6893 Mon Sep 17 00:00:00 2001 From: Filippo Adessi Date: Tue, 8 Sep 2026 23:18:34 +0200 Subject: [PATCH 1/2] fix(ts): avoid Long overflow in PCR computation after long uptime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SYSTEM_CLOCK_FREQ * timestamp (timestamp in µs, uptime-based) overflows Long once timestamp > ~3.4e11 µs (~95h of uptime), silently wrapping and producing a garbage PCR on every subsequent frame. Reorder to divide before multiplying (same approach as RootEncoder's AdaptationField: timestamp * 9 / 100), mathematically equivalent but safe up to ~10^17 µs. Adds a regression test that reproduces the overflow magnitude and checks the encoded PCR against a BigInteger ground truth (same original semantics, immune to overflow at this size) instead of against the patched formula itself. Confirmed this test fails on the pre-fix formula and passes after the fix. Confirmed on-device: an SRT push kept running past the previous corruption threshold re-distributes cleanly via RTSP-pull/SRT-pull on mediamtx with no PCR-related desync. Fixes #301 --- .../muxers/ts/descriptors/AdaptationField.kt | 23 ++++++++-- .../ts/descriptors/AdaptationFieldTest.kt | 46 +++++++++++++++++++ 2 files changed, 64 insertions(+), 5 deletions(-) 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..904bdd34b 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 @@ -87,11 +87,24 @@ class AdaptationField( } 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 + // PATCH LOCALE (regia-rtmp, 9/8): la formula originale calcola + // SYSTEM_CLOCK_FREQ(27_000_000) * timestamp PRIMA di dividere — con + // timestamp in microsecondi di uptime del device (TimeUtils.currentTime() + // usa SystemClock.uptimeMillis()-equivalente), questo prodotto sfora un + // Long a 64 bit (overflow silenzioso in Kotlin/JVM) non appena l'uptime + // supera ~95 ore (~3.95 giorni): 27_000_000 * timestamp > Long.MAX_VALUE + // quando timestamp > ~3.416e11 µs. Il valore avvolto (wrapped) produce + // un PCR sostanzialmente casuale ad ogni frame — root cause isolata sul + // campo di un blocco totale/intermittente della ridistribuzione RTSP/ + // SRT-pull/HLS su mediamtx per stream pubblicati da device con uptime + // lungo (7 giorni nel caso diagnosticato). Riscritto nello stesso ordine + // di operazioni già usato da RootEncoder (pedroSG94/RootEncoder, + // srt/.../AdaptationField.kt: "timestamp * 9 / 100"), che moltiplica per + // un fattore piccolo prima di dividere — matematicamente equivalente + // (27_000_000/1_000_000/300 = 9/100) ma sicuro fino a timestamp + // dell'ordine di 10^17 µs (~milioni di anni di uptime). + 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..c07c4ca5a 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,12 @@ 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 class AdaptationFieldTest { @@ -43,4 +46,47 @@ 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). This reproduces that + // magnitude and checks the encoded PCR against a ground truth computed + // with BigInteger (same original semantics, immune to Long overflow at + // this size) instead of against 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 + ) + + val bytes = adaptationField.toByteBuffer() + bytes.position(2) // skip adaptation_field_length + flags byte + val pcrBaseHigh32 = bytes.int.toLong() and 0xFFFFFFFFL + val tail = bytes.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 From c3abd4a13075b22b0c059c1cfd2d9775bdd46f61 Mon Sep 17 00:00:00 2001 From: Filippo Adessi Date: Wed, 9 Sep 2026 20:44:32 +0200 Subject: [PATCH 2/2] fix(ts): english comments and test addClockReference directly Address review feedback: - Rewrite the in-code comment in English (was Italian, local debugging notes) and keep only the technical explanation. - Make addClockReference internal so the unit test can exercise it directly instead of going through the full AdaptationField buffer parsing. --- .../muxers/ts/descriptors/AdaptationField.kt | 25 ++++++------------- .../ts/descriptors/AdaptationFieldTest.kt | 19 ++++++++------ 2 files changed, 19 insertions(+), 25 deletions(-) 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 904bdd34b..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,23 +86,14 @@ class AdaptationField( return buffer } - private fun addClockReference(buffer: ByteBuffer, timestamp: Long) { - // PATCH LOCALE (regia-rtmp, 9/8): la formula originale calcola - // SYSTEM_CLOCK_FREQ(27_000_000) * timestamp PRIMA di dividere — con - // timestamp in microsecondi di uptime del device (TimeUtils.currentTime() - // usa SystemClock.uptimeMillis()-equivalente), questo prodotto sfora un - // Long a 64 bit (overflow silenzioso in Kotlin/JVM) non appena l'uptime - // supera ~95 ore (~3.95 giorni): 27_000_000 * timestamp > Long.MAX_VALUE - // quando timestamp > ~3.416e11 µs. Il valore avvolto (wrapped) produce - // un PCR sostanzialmente casuale ad ogni frame — root cause isolata sul - // campo di un blocco totale/intermittente della ridistribuzione RTSP/ - // SRT-pull/HLS su mediamtx per stream pubblicati da device con uptime - // lungo (7 giorni nel caso diagnosticato). Riscritto nello stesso ordine - // di operazioni già usato da RootEncoder (pedroSG94/RootEncoder, - // srt/.../AdaptationField.kt: "timestamp * 9 / 100"), che moltiplica per - // un fattore piccolo prima di dividere — matematicamente equivalente - // (27_000_000/1_000_000/300 = 9/100) ma sicuro fino a timestamp - // dell'ordine di 10^17 µs (~milioni di anni di uptime). + 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 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 c07c4ca5a..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 @@ -22,6 +22,7 @@ import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Test import java.math.BigInteger +import java.nio.ByteBuffer class AdaptationFieldTest { @@ -53,10 +54,9 @@ class AdaptationFieldTest { // (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). This reproduces that - // magnitude and checks the encoded PCR against a ground truth computed - // with BigInteger (same original semantics, immune to Long overflow at - // this size) instead of against the patched formula itself. + // 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( @@ -70,10 +70,13 @@ class AdaptationFieldTest { adaptationFieldExtension = null ) - val bytes = adaptationField.toByteBuffer() - bytes.position(2) // skip adaptation_field_length + flags byte - val pcrBaseHigh32 = bytes.int.toLong() and 0xFFFFFFFFL - val tail = bytes.short.toInt() and 0xFFFF + // 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