fix(ts): avoid Long overflow in PCR computation after long uptime - #303
fix(ts): avoid Long overflow in PCR computation after long uptime#303filippoadessi wants to merge 2 commits into
Conversation
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). |
There was a problem hiding this comment.
Every comment should be in English please.
There was a problem hiding this comment.
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`() { |
There was a problem hiding this comment.
Would it be better to only test addClockReference?
There was a problem hiding this comment.
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.
|
Replaced by #305 |
Title: PCR computation overflows Long after ~95h of device uptime, corrupting the TS mux (silent, breaks RTSP/SRT-pull/HLS re-distribution)
Environment
threshold is deterministic and codec-independent, see below)
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():timestampis a frame PTS in microseconds, typically derived from deviceuptime (
SystemClock-based clocks), so it grows unbounded across a long-livedsession.
TSConst.SYSTEM_CLOCK_FREQis27_000_000.The multiplication
SYSTEM_CLOCK_FREQ * timestampis done before anydivision. On the JVM/Kotlin,
Long * Longsilently wraps on overflow (noexception). This product overflows
Long.MAX_VALUE(2^63-1) oncetimestamp > ~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/pcrExtbecome effectively randomper-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/100and
27_000_000 / 1_000_000 = 27, but safe up totimestampon the order of10^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 33replaces2.toDouble().pow(33).toLong()— same value, avoids anunnecessary 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.