Skip to content

MatroskaExtractor: files with Tracks after Clusters are always reported unseekable, even with valid Cues — every seek restarts from t=0 #3377

Description

@edde746

Version

Media3 1.11.0

More version details

Regression introduced with the tracks-after-clusters support from d386bbf (#3250), first released in 1.11.0. Still present on main.

Devices that reproduce the issue

Deterministic extractor logic, not device-specific. Confirmed on Pixel 7 (Android 16) and Samsung SM-S936B (Android 16) with a stock ExoPlayer.Builder + MatroskaExtractor, and host-side by the repository's own extractor dump (see below).

Devices that do not reproduce the issue

None known.

Reproducible in the demo app?

Yes. Demo app (demos/main, noDecoderExtensions flavor) built from the 1.11.0 tag, Pixel 7 / Android 16:

  • Tracks-after-clusters file: plays fine, but the time bar is disabled (exo_progress reports enabled=false in the accessibility tree) and taps on it produce no EventLogger events — seeking is impossible, because COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM is unavailable for the unseekable window.
  • Control file (identical content, Tracks in front): time bar enabled; tapping it seeks normally (positionDiscontinuity … reason=SEEK, PositionInfo:old […, pos=60031], PositionInfo:new […, pos=42199]).

The demo-visible symptom is "seeking disabled" rather than "snap to start" because the demo UI gates seeks on command availability; a direct player.seekTo() (as in the instrumentation test above) restarts from 0. Both stem from the same isSeekable() == false.

Repro file recipe (any length, per the #3250 report — an oversized track name forces mkvtoolnix to relocate the Tracks element to the end of the file):

ffmpeg -f lavfi -i "testsrc2=size=1280x720:rate=30:duration=60" \
       -f lavfi -i "sine=frequency=440:duration=60" \
       -c:v libx264 -pix_fmt yuv420p -c:a aac repro.mkv
mkvpropedit repro.mkv --edit track:1 --set name="$(printf 'e%.0s' $(seq 1 1400))"

Resulting layout: SeekHead, Info, Void, Tags, Cluster…, Cues, Tracks, with the SeekHead referencing both Cues and Tracks after the clusters.

Reproduction steps

For a Matroska file whose SeekHead references both a Tracks and a Cues element located after the Cluster elements (FFmpeg produces this layout), MatroskaExtractor emits a cues-backed MatroskaSeekMap that permanently reports isSeekable() == false and resolves every getSeekPoints(timeUs) to SeekPoint.START.

Cause is an ordering defect in read():

if (continueReading
    && (maybeSeekForTracks(seekPosition, input.getPosition())
        || maybeSeekForCues(seekPosition, input.getPosition()))) {
  return Extractor.RESULT_SEEK;
}
  1. At the first Cluster, startMasterElement(ID_CLUSTER) sets both seekForTracks and seekForCues.
  2. maybeSeekForTracks wins the short-circuit ||, so seekForCues stays pending and the extractor jumps to the Tracks position.
  3. On the very next reader.read() — before a single TrackEntry has parsed — the pending maybeSeekForCues diverts to the Cues position.
  4. endMasterElement(ID_CUES) constructs MatroskaSeekMap while primarySeekTrackNumber is still C.INDEX_UNSET (it is only assigned in endMasterElement(ID_TRACKS), which has not run yet).
  5. MatroskaSeekMap.isSeekable() delegates to isSeekable(primarySeekTrackNumber) → perTrackCues.get(-1) → false, and buildChunkIndex returns null, so getSeekPoints(timeUs) → new SeekPoints(SeekPoint.START).

The ordering is deterministic, not racy: it happens for every tracks-after-clusters file that also has Cues.

The repository already demonstrates it: libraries/test_data/src/test/assets/media/mkv/sample_last_cluster.mkv has a valid Cues element (CueTime=67, CueTrack=1, CueClusterPosition=5510) and a known duration, yet the checked-in dump records the bug:

// extractordumps/mkv/sample_last_cluster.mkv.0.dump
seekMap:
  isSeekable = false
  duration = 1072000
  getPosition(0) = [[timeUs=0, position=0]]

The same dump contains ThumbnailMetadata: presentationTimeUs=67000, which is derived from perTrackCues — proving the Cues were parsed and the MatroskaSeekMap branch was taken, not SeekMap.Unseekable.

Note the seek path of this feature has no test coverage: ExtractorAsserts computes numberSeekTests = seekMap.isSeekable() && durationUs != C.TIME_UNSET ? 4 : 1, so the committed sample_last_cluster.mkv.1/.2/.3.dump files are unreachable (and are byte-identical placeholders).

Possible fixes:

  • Don't honor seekForCues while tracksContentPosition != C.INDEX_UNSET && !readTracks (finish the Tracks detour first), or
  • Construct/emit the seek map only once both Cues and Tracks have been read, or resolve primarySeekTrackNumber lazily in MatroskaSeekMap.

The TrackAwareSeekMap per-track overloads already work (perTrackCues is held by reference and populated by seek time), which is how we work around this downstream.

Expected result

A tracks-after-clusters file with valid Cues is seekable, like the same file with Tracks in front.

Actual result

Timeline.Window.isSeekable is false, and every seek restarts playback from the beginning: ProgressiveMediaPeriod.seekToUs() coerces all seeks into non-seekable media to 0 (positionUs = seekMap.isSeekable() ? positionUs : 0;), and selectTracks() routes post-first track reselections through the same path via seekRequired, so track changes rewind too.

On-device confirmation (Pixel 7, Android 16, stock ExoPlayer.Builder + MatroskaExtractor(DefaultSubtitleParserFactory()), playing a tracks-after-clusters MKV, paused): isCurrentMediaItemSeekable == false; seekTo(500) settles at position 0.

Reported downstream against a real FFmpeg-muxed file (Lavf62.3.100) in a Plex client: edde746/plezy#1969.

Media

libraries/test_data/src/test/assets/media/mkv/sample_last_cluster.mkv (already in the repository).

Bug Report

The repro is deterministic from the checked-in asset and dump, so no adb bugreport is attached; happy to provide one on request.

Activity

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions