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;
}
- At the first Cluster,
startMasterElement(ID_CLUSTER) sets both seekForTracks and seekForCues.
maybeSeekForTracks wins the short-circuit ||, so seekForCues stays pending and the extractor jumps to the Tracks position.
- On the very next
reader.read() — before a single TrackEntry has parsed — the pending maybeSeekForCues diverts to the Cues position.
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).
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.
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,noDecoderExtensionsflavor) built from the 1.11.0 tag, Pixel 7 / Android 16:exo_progressreportsenabled=falsein the accessibility tree) and taps on it produce noEventLoggerevents — seeking is impossible, becauseCOMMAND_SEEK_IN_CURRENT_MEDIA_ITEMis unavailable for the unseekable window.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 sameisSeekable() == 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):
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),
MatroskaExtractoremits a cues-backedMatroskaSeekMapthat permanently reportsisSeekable() == falseand resolves everygetSeekPoints(timeUs)toSeekPoint.START.Cause is an ordering defect in
read():startMasterElement(ID_CLUSTER)sets bothseekForTracksandseekForCues.maybeSeekForTrackswins the short-circuit||, soseekForCuesstays pending and the extractor jumps to the Tracks position.reader.read()— before a single TrackEntry has parsed — the pendingmaybeSeekForCuesdiverts to the Cues position.endMasterElement(ID_CUES)constructsMatroskaSeekMapwhileprimarySeekTrackNumberis stillC.INDEX_UNSET(it is only assigned inendMasterElement(ID_TRACKS), which has not run yet).MatroskaSeekMap.isSeekable()delegates toisSeekable(primarySeekTrackNumber)→perTrackCues.get(-1)→false, andbuildChunkIndexreturnsnull, sogetSeekPoints(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.mkvhas a valid Cues element (CueTime=67, CueTrack=1, CueClusterPosition=5510) and a known duration, yet the checked-in dump records the bug:The same dump contains
ThumbnailMetadata: presentationTimeUs=67000, which is derived fromperTrackCues— proving the Cues were parsed and theMatroskaSeekMapbranch was taken, notSeekMap.Unseekable.Note the seek path of this feature has no test coverage:
ExtractorAssertscomputesnumberSeekTests = seekMap.isSeekable() && durationUs != C.TIME_UNSET ? 4 : 1, so the committedsample_last_cluster.mkv.1/.2/.3.dumpfiles are unreachable (and are byte-identical placeholders).Possible fixes:
seekForCueswhiletracksContentPosition != C.INDEX_UNSET && !readTracks(finish the Tracks detour first), orprimarySeekTrackNumberlazily inMatroskaSeekMap.The
TrackAwareSeekMapper-track overloads already work (perTrackCuesis 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.isSeekableis 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;), andselectTracks()routes post-first track reselections through the same path viaseekRequired, 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 bugreportis attached; happy to provide one on request.