Skip to content

fix(ts): avoid Long overflow in PCR computation after long uptime - #303

Closed
filippoadessi wants to merge 2 commits into
ThibaultBee:mainfrom
filippoadessi:fix/pcr-overflow-long-uptime
Closed

fix(ts): avoid Long overflow in PCR computation after long uptime#303
filippoadessi wants to merge 2 commits into
ThibaultBee:mainfrom
filippoadessi:fix/pcr-overflow-long-uptime

Conversation

@filippoadessi

Copy link
Copy Markdown

Title: PCR computation overflows Long after ~95h of device uptime, corrupting the TS mux (silent, breaks RTSP/SRT-pull/HLS re-distribution)

Environment

  • StreamPack 3.2.0 (core + srt extension)
  • Android device with long uptime (observed after ~7 days of uptime, but the
    threshold is deterministic and codec-independent, see below)
  • MPEG-TS output (TS muxer, used by the SRT endpoint)
  • Downstream: media server re-distributing the SRT-pushed TS (mediamtx, but
    this is a bug in the TS stream itself, not in the downstream server)

Symptom

After the device has been running for a long time (multiple days), the
MPEG-TS stream pushed over SRT becomes corrupt in a way that a re-distributing
media server can no longer parse it: total or intermittent failure of
RTSP-pull / SRT-pull / HLS re-serving of the same stream from a server that
otherwise successfully receives and forwards it (confirmed the bytes were
arriving over the SRT socket — the corruption is in the PCR field itself, not
in transport). The device's own SRT push connection stays up throughout; only
downstream re-muxing/re-parsing breaks.

Root cause

AdaptationField.addClockReference():

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
    ...
}

timestamp is a frame PTS in microseconds, typically derived from device
uptime (SystemClock-based clocks), so it grows unbounded across a long-lived
session. TSConst.SYSTEM_CLOCK_FREQ is 27_000_000.

The multiplication SYSTEM_CLOCK_FREQ * timestamp is done before any
division. On the JVM/Kotlin, Long * Long silently wraps on overflow (no
exception). This product overflows Long.MAX_VALUE (2^63-1) once
timestamp > ~3.416 × 10^11 µs, i.e. ~95 hours (~3.95 days) of uptime
independent of video codec, resolution, or bitrate; it only depends on how
long the encoder has been feeding PTS values that trace real elapsed time.

Once the product overflows, pcrBase/pcrExt become effectively random
per-frame values instead of a monotonically increasing clock reference. The
resulting PCR field in every adaptation field from that point on is garbage.
Most simple TS consumers (HLS segmenters, players that ignore PCR jitter
within tolerance) are lenient enough not to notice; standards-strict RTSP/SRT
re-muxers reject or desync on it, which is how this was found (works for
HLS on the same server/path, breaks for RTSP/SRT re-pull of the identical
input).

This is silent and cumulative: nothing throws, nothing logs, and the bug only
manifests after days of uptime, which makes it easy to miss in normal
development/testing cycles (restart the app => PTS/uptime resets => bug
disappears until the next multi-day session).

Fix

Reorder the arithmetic to divide before multiplying, same approach already
used by RootEncoder (pedroSG94/RootEncoder,
extensions/srt/.../descriptor/AdaptationField.kt, timestamp * 9 / 100),
which is mathematically equivalent since 27_000_000 / 1_000_000 / 300 = 9/100
and 27_000_000 / 1_000_000 = 27, but safe up to timestamp on the order of
10^17 µs (millions of years of uptime) instead of ~3.4 × 10^11 µs:

     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
+        val pcrBase = (timestamp * 9 / 100) % (1L shl 33)
+        val pcrExt = (timestamp * 27) % 300

(1L shl 33 replaces 2.toDouble().pow(33).toLong() — same value, avoids an
unnecessary double round-trip, but that part is cosmetic; the load-bearing
change is the multiply/divide order.)

Verified on-device: after this fix, an SRT push kept running well past the
previous corruption threshold re-distributes cleanly via RTSP-pull/SRT-pull on
mediamtx with no PCR-related desync.

I'm happy to open a PR with this change if useful.

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 ThibaultBee#301
// 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).

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.

Every comment should be in English please.

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.

You're right, sorry — that comment was leftover local debugging context. It's now rewritten in English and trimmed to the technical explanation only (commit c3abd4a).

}

@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.

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.
@ThibaultBee

Copy link
Copy Markdown
Owner

Replaced by #305

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants