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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -43,4 +47,49 @@ class AdaptationFieldTest {
adaptationField.toByteBuffer().toByteArray()
)
}

@Test
fun `pcr does not overflow after long device uptime`() {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better to only test addClockReference?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, done: addClockReference is now internal so the test can call it directly, and the unit test exercises just that method (allocates a 6-byte buffer, calls addClockReference, checks the encoded PCR base/ext against the BigInteger ground truth) instead of parsing the full adaptation field. Same commit c3abd4a; core unit tests pass.

// 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)
}
}