SCTE-35 programSplicePlaybackPositionUs value changed between 1.10.0 and 1.11.0 on live TS
@tonihei is already working on this.
Since Sep 15, 2026.
- Dominant language
- Java
- Stars
- 3k
- Forks
- 955
- Avg merge
- 12d 14h
- Merged PRs (30d)
- 2
Description
Question
After upgrading from Media3 1.10.0 to 1.11.0, the value of SpliceInsertCommand.programSplicePlaybackPositionUs produced from a live MPEG-2 TS stream changed. It was stable in 1.10.0 and is now consistently different on the same stream. This breaks our downstream PTS-based ad-switching logic, which normalizes the splice position against the decoder frame PTS timeline and assumed the 1.10.0 value range.
I've attached screenshots showing the parsed SCTE data on both versions for the same content (see Screenshots below).
What changed (bisected)
The value flips exactly at commit [55c6898417 (https://github.com/androidx/media/commit/55c6898417923dfc11e0e54cc689189efa9b12c3) ("Avoid reading too far ahead in metadata and text renderer", first released in 1.11.0). Using the checked-in dump libraries/test_data/src/test/assets/playbackdumps/ts/sample_scte35.ts.dump:
| Commit | Meaning | programSplicePlaybackPositionUs |
|---|---|---|
55c6898417~1 |
== 1.10.0 behavior | 1000000822222 |
55c6898417 |
post (1.11.0) | 954439399111 |
- delta:
45561423111us - This delta is consistent with a 33-bit 90 kHz PTS wraparound-reference shift
(2^33 / 90000 s ≈ 95443.7 s ≈ 95443717033 us, i.e. roughly half a wrap period). - The commit immediately before (
55c6898417~1) still emits the 1.10.0 value.
Root-cause analysis
programSplicePlaybackPositionUs is a derived playback timestamp, not raw parsed data. The SCTE-35 parsing code itself is unchanged; the value depends on the state of the shared MPEG-2 TS TimestampAdjuster at the instant the SCTE-35
section is read.
-
Value formula. In
SpliceInfoDecoder.decode(...), on the first
adjustSampleTimestampcall for an uninitialized adjuster,
timestampOffsetUs == subsampleOffsetUs. Then in
SpliceInsertCommand.parseFromSection:programSplicePlaybackPositionUs = timestampAdjuster.adjustTsTimestamp(programSplicePts) = programSplicePts_us + subsampleOffsetUsWith
programSplicePts = 200000 usin the dump:- old:
subsampleOffsetUs = 1000000822222 - 200000 = 1000000622222 - new:
subsampleOffsetUs = 954439399111 - 200000 = 954439199111
The only variable that changed is
subsampleOffsetUs. - old:
-
Where
subsampleOffsetUscomes from. It is captured by
PassthroughSectionPayloadReader.consume(...)from the TSTimestampAdjuster
at the moment the section is consumed:long sampleTimestampUs = timestampAdjuster.getLastAdjustedTimestampUs(); long subsampleOffsetUs = timestampAdjuster.getTimestampOffsetUs(); // captured here ... format = format.buildUpon().setSubsampleOffsetUs(subsampleOffsetUs).build();PassthroughSectionPayloadReaderitself is unchanged since 2024
(c3d4a3d683), so the reader is not the regression. -
The adjuster is shared. In
TsExtractor(MODE_HLS/MODE_SINGLE_PMT) the SCTE passthrough reader and the video/audio PES readers share a singleTimestampAdjuster(timestampAdjusters.get(0)). The PES readers advance that adjuster (and fixtimestampOffsetUs/ the PTS wrap reference on first call), so the offset captured by the SCTE reader depends on how far the video/audio readers have advanced the shared adjuster first. -
What
55c6898417changed.MetadataRenderer.readMetadata()now gates reads withFLAG_PEEK | FLAG_OMIT_SAMPLE_DATAandMAX_READ_AHEAD_DURATION_US = 1s, postponing the real consume:private static final long MAX_READ_AHEAD_DURATION_US = C.MICROS_PER_SECOND; // 1s ... int result = readSource(formatHolder, buffer, FLAG_PEEK | FLAG_OMIT_SAMPLE_DATA); if (RESULT_BUFFER_READ && !EOS && !outputMetadataEarly && positionUs < buffer.timeUs - MAX_READ_AHEAD_DURATION_US) { return; // postpone the real read } result = readSource(formatHolder, buffer, /* readFlags= */ 0); // real consumeThis changes the interleaving between the metadata read and the PES reads
that mutate the shared adjuster. When the SCTE section is finally consumed the
adjuster is in a different state (differenttimestampOffsetUs/ last-adjusted
PTS, including a different PTS wraparound reference), yielding a different
subsampleOffsetUsand therefore a differentprogramSplicePlaybackPositionUs.
Ruled out (unchanged since before 1.10.0)
SpliceInfoDecoder.java— unchanged since androidx.media3 migration (2021).SpliceInsertCommand.java— last functional change 2023 (3456382ae7,toStringonly).PassthroughSectionPayloadReader.java— last change 2024 (c3d4a3d683, MIME type only).TimestampAdjuster.java— last changed910b672b0(2025-08-06), predates 1.10.0.
Secondary observation (same area, no value change)
Commit 6dc42b45d2 ("Fix issue where end of stream wasn't signaled if last pes
had length field set", 1.11.0) refactored PesReader.packetFinished(...) and
affects end-of-input signalling/latency on the section-reader path. It added a
late video/audio sample to the same dump but did not change the SCTE value
(verified identical before/after). Flagging it only in case it is relevant to
end-of-stream/latency behavior on live TS.
Our questions
- Which value is correct? Is the 1.11.0 programSplicePlaybackPositionUs the intended one, and was the 1.10.0 value just a side effect of eager metadata read-ahead?
- Is this by design? Should programSplicePlaybackPositionUs depend on when the metadata renderer consumes the SCTE section (i.e. shared TimestampAdjuster state), or should the SCTE offset be independent of read ordering?
- How do we restore old behavior? Is outputMetadataEarly = true on MetadataRenderer the right way to get 1.10.0 timing back, and what are the side effects?
- What's the stable reference? How should we align the splice position to the decoder output PTS timeline without a hardcoded offset?
Impact on our app
We normalize the splice position against the frame PTS timeline to trigger an ad
switch:
private val ptsStartReference = 1_000_000_000_000L // chosen to match the 1.10.0 value range
val normalizedSplicePtsUs = scteData.programSplicePlaybackPositionUs - ptsStartReference
val ptsDiff = presentationTimeUs - normalizedSplicePtsUs
// switch when abs(ptsDiff) <= frameTimestampToleranceUs
We normalize with a fixed ptsStartReference = 1_000_000_000_000 (1e12 us). We picked this because in 1.10.0 the offset baked into programSplicePlaybackPositionUs was itself near 1e12 (1000000822222), so subtracting it left a small value aligned to the frame PTS timeline:
With the ~45.56e9 us shift, normalizedSplicePtsUs no longer lands near the frame PTS, so ptsDiff never falls within tolerance and the PTS-based ad switch does not fire (or fires at the wrong time).
Steps to reproduce
- Play the same live TS SCTE-35 stream.
- Log
programSplicePlaybackPositionUsfrom the receivedSpliceInsertCommand. - Compare Media3 1.10.0 vs 1.11.0 — the value differs by ~45561423111 us.
- (Optional) Add logging in
PassthroughSectionPayloadReader.consumefor
timestampAdjuster.getLastAdjustedTimestampUs()andgetTimestampOffsetUs()
at the moment the SCTE section is read, on both versions.
Screenshots
- 1.10.0 — SCTE data (correct/expected value):
- 1.11.0 — SCTE data (shifted value):
- Change — 55c6898417923dfc11e0e54cc689189efa9b12c3
Versions
- Reproduces on: 1.11.0 (1.11.0-alpha01 / beta01 / rc01)
- Works as expected on: 1.10.0, 1.10.1
- Suspected commit:
55c6898417923dfc11e0e54cc689189efa9b12c3 - Stream type: live MPEG-2 TS with SCTE-35 (
SpliceInsertCommand) - Extractor mode:
TsExtractorMODE_HLS / MODE_SINGLE_PMT (sharedTimestampAdjuster)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.