androidx / androidx/media

SCTE-35 programSplicePlaybackPositionUs value changed between 1.10.0 and 1.11.0 on live TS

Open
#3,413 0 comments 0 reactions 1 assignee View on GitHub

@tonihei is already working on this.

Since Sep 15, 2026.

needs triage question
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: 45561423111 us
  • 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.

  1. Value formula. In SpliceInfoDecoder.decode(...), on the first
    adjustSampleTimestamp call for an uninitialized adjuster,
    timestampOffsetUs == subsampleOffsetUs. Then in
    SpliceInsertCommand.parseFromSection:

    programSplicePlaybackPositionUs = timestampAdjuster.adjustTsTimestamp(programSplicePts)
                                    = programSplicePts_us + subsampleOffsetUs
    

    With programSplicePts = 200000 us in the dump:

    • old: subsampleOffsetUs = 1000000822222 - 200000 = 1000000622222
    • new: subsampleOffsetUs = 954439399111 - 200000 = 954439199111

    The only variable that changed is subsampleOffsetUs.

  2. Where subsampleOffsetUs comes from. It is captured by
    PassthroughSectionPayloadReader.consume(...) from the TS TimestampAdjuster
    at the moment the section is consumed:

    long sampleTimestampUs = timestampAdjuster.getLastAdjustedTimestampUs();
    long subsampleOffsetUs = timestampAdjuster.getTimestampOffsetUs(); // captured here
    ...
    format = format.buildUpon().setSubsampleOffsetUs(subsampleOffsetUs).build();
    

    PassthroughSectionPayloadReader itself is unchanged since 2024
    (c3d4a3d683), so the reader is not the regression.

  3. The adjuster is shared. In TsExtractor (MODE_HLS / MODE_SINGLE_PMT) the SCTE passthrough reader and the video/audio PES readers share a single TimestampAdjuster (timestampAdjusters.get(0)). The PES readers advance that adjuster (and fix timestampOffsetUs / 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.

  4. What 55c6898417 changed. MetadataRenderer.readMetadata() now gates reads with FLAG_PEEK | FLAG_OMIT_SAMPLE_DATA and MAX_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 consume
    

    This 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 (different timestampOffsetUs / last-adjusted
    PTS, including a different PTS wraparound reference), yielding a different
    subsampleOffsetUs and therefore a different programSplicePlaybackPositionUs.

Ruled out (unchanged since before 1.10.0)
  • SpliceInfoDecoder.java — unchanged since androidx.media3 migration (2021).
  • SpliceInsertCommand.java — last functional change 2023 (3456382ae7, toString only).
  • PassthroughSectionPayloadReader.java — last change 2024 (c3d4a3d683, MIME type only).
  • TimestampAdjuster.java — last change d910b672b0 (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

  1. 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?
  2. 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?
  3. 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?
  4. 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

  1. Play the same live TS SCTE-35 stream.
  2. Log programSplicePlaybackPositionUs from the received SpliceInsertCommand.
  3. Compare Media3 1.10.0 vs 1.11.0 — the value differs by ~45561423111 us.
  4. (Optional) Add logging in PassthroughSectionPayloadReader.consume for
    timestampAdjuster.getLastAdjustedTimestampUs() and getTimestampOffsetUs()
    at the moment the SCTE section is read, on both versions.

Screenshots

  • 1.10.0 — SCTE data (correct/expected value):
Image
  • 1.11.0 — SCTE data (shifted value):
Image
  • Change — 55c6898417923dfc11e0e54cc689189efa9b12c3
Image

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: TsExtractor MODE_HLS / MODE_SINGLE_PMT (shared TimestampAdjuster)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.