Skip to content

Commit ef35943

Browse files
committed
Adds support for fragmented mp4 with multiple sidx boxes
1 parent 2c866ce commit ef35943

15 files changed

+18432
-24
lines changed

RELEASENOTES.md

+3
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@ This release includes the following changes since the
153153
`DefaultRenderersFactory.experimentalSetParseAv1SampleDependencies` API.
154154
* Muxers:
155155
* Disable `Mp4Muxer` sample batching and copying by default.
156+
* Extractors:
157+
* Fix seek on fragmented mp4 with multiple sidx atoms.
158+
([#9373](https://github.com/google/ExoPlayer/issues/9373))
156159
* Remove deprecated symbols:
157160
* Removed `androidx.media3.exoplayer.audio.SonicAudioProcessor`.
158161

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package androidx.media3.extractor;
2+
3+
import java.util.ArrayList;
4+
import java.util.HashSet;
5+
import java.util.List;
6+
7+
public class ChunkIndicesWrapper {
8+
9+
/** The chunk sizes, in bytes. */
10+
private final ArrayList<ChunkIndex> chunks = new ArrayList<>();
11+
12+
private final HashSet<Long> timesIndexed = new HashSet<>();
13+
14+
public void merge(ChunkIndex chunk) {
15+
if (chunk.timesUs != null
16+
&& chunk.timesUs.length > 0
17+
&& !timesIndexed.contains(chunk.timesUs[0])) {
18+
chunks.add(chunk);
19+
timesIndexed.add(chunk.timesUs[0]);
20+
}
21+
}
22+
23+
public ChunkIndex toChunkIndex() {
24+
ArrayList<int[]> sizesList = new ArrayList<>();
25+
ArrayList<long[]> offsetsList = new ArrayList<>();
26+
ArrayList<long[]> durationsList = new ArrayList<>();
27+
ArrayList<long[]> timesList = new ArrayList<>();
28+
29+
for (ChunkIndex chunk : chunks) {
30+
sizesList.add(chunk.sizes);
31+
offsetsList.add(chunk.offsets);
32+
durationsList.add(chunk.durationsUs);
33+
timesList.add(chunk.timesUs);
34+
}
35+
36+
return new ChunkIndex(
37+
concatInts(sizesList),
38+
concatLongs(offsetsList),
39+
concatLongs(durationsList),
40+
concatLongs(timesList));
41+
}
42+
43+
public void clear() {
44+
chunks.clear();
45+
timesIndexed.clear();
46+
}
47+
48+
public int size() {
49+
return chunks.size();
50+
}
51+
52+
private long[] concatLongs(List<long[]> arrays) {
53+
int totalLength = 0;
54+
for (long[] array : arrays) {
55+
totalLength += array.length;
56+
}
57+
58+
long[] res = new long[totalLength];
59+
60+
int offset = 0;
61+
for (long[] array : arrays) {
62+
System.arraycopy(array, 0, res, offset, array.length);
63+
offset += array.length;
64+
}
65+
66+
return res;
67+
}
68+
69+
private int[] concatInts(List<int[]> arrays) {
70+
int totalLength = 0;
71+
for (int[] array : arrays) {
72+
totalLength += array.length;
73+
}
74+
75+
int[] res = new int[totalLength];
76+
77+
int offset = 0;
78+
for (int[] array : arrays) {
79+
System.arraycopy(array, 0, res, offset, array.length);
80+
offset += array.length;
81+
}
82+
83+
return res;
84+
}
85+
}

libraries/extractor/src/main/java/androidx/media3/extractor/mp4/FragmentedMp4Extractor.java

+77-24
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import androidx.media3.extractor.Ac4Util;
4848
import androidx.media3.extractor.CeaUtil;
4949
import androidx.media3.extractor.ChunkIndex;
50+
import androidx.media3.extractor.ChunkIndicesWrapper;
5051
import androidx.media3.extractor.Extractor;
5152
import androidx.media3.extractor.ExtractorInput;
5253
import androidx.media3.extractor.ExtractorOutput;
@@ -246,6 +247,11 @@ public static ExtractorsFactory newFactory(SubtitleParser.Factory subtitleParser
246247
// Whether extractorOutput.seekMap has been called.
247248
private boolean haveOutputSeekMap;
248249

250+
// Whether a fragmented sidx has been fully collected and output.
251+
private boolean haveOutputSeekMapComplete;
252+
private final ChunkIndicesWrapper wrappingSegmentIndex = new ChunkIndicesWrapper();
253+
private long resetCallerSeekTo;
254+
249255
/**
250256
* @deprecated Use {@link #FragmentedMp4Extractor(SubtitleParser.Factory)} instead
251257
*/
@@ -509,26 +515,56 @@ public void release() {
509515
// Do nothing
510516
}
511517

518+
private void readRemainingSidxAtomsIntoTrack(ExtractorInput input) throws IOException {
519+
enterReadingAtomHeaderState();
520+
while (readAtomHeader(input, true)) {
521+
if (atomType == Mp4Box.TYPE_sidx) {
522+
ParsableByteArray inputArray = new ParsableByteArray((int) atomSize);
523+
System.arraycopy(atomHeader.getData(), 0, inputArray.getData(), 0, Mp4Box.HEADER_SIZE);
524+
input.readFully(
525+
inputArray.getData(), Mp4Box.HEADER_SIZE, (int) (atomSize - atomHeaderBytesRead));
526+
LeafBox leafBox = new LeafBox(Mp4Box.TYPE_sidx, inputArray);
527+
Pair<Long, ChunkIndex> sidxCur = parseSidx(leafBox.data, input.getPeekPosition());
528+
wrappingSegmentIndex.merge(sidxCur.second);
529+
} else {
530+
input.skipFully((int) atomSize - atomHeaderBytesRead, true);
531+
}
532+
enterReadingAtomHeaderState();
533+
}
534+
}
535+
512536
@Override
513537
public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException {
514-
while (true) {
515-
switch (parserState) {
516-
case STATE_READING_ATOM_HEADER:
517-
if (!readAtomHeader(input)) {
518-
reorderingSeiMessageQueue.flush();
519-
return Extractor.RESULT_END_OF_INPUT;
520-
}
521-
break;
522-
case STATE_READING_ATOM_PAYLOAD:
523-
readAtomPayload(input);
524-
break;
525-
case STATE_READING_ENCRYPTION_DATA:
526-
readEncryptionData(input);
527-
break;
528-
default:
529-
if (readSample(input)) {
530-
return RESULT_CONTINUE;
531-
}
538+
try {
539+
while (true) {
540+
switch (parserState) {
541+
case STATE_READING_ATOM_HEADER:
542+
if (!readAtomHeader(input, false)) {
543+
if (resetCallerSeekTo > 0) {
544+
seekPosition.position = resetCallerSeekTo;
545+
return Extractor.RESULT_SEEK;
546+
} else {
547+
reorderingSeiMessageQueue.flush();
548+
return Extractor.RESULT_END_OF_INPUT;
549+
}
550+
}
551+
break;
552+
case STATE_READING_ATOM_PAYLOAD:
553+
readAtomPayload(input);
554+
break;
555+
case STATE_READING_ENCRYPTION_DATA:
556+
readEncryptionData(input);
557+
break;
558+
default:
559+
if (readSample(input)) {
560+
return RESULT_CONTINUE;
561+
}
562+
}
563+
}
564+
} finally {
565+
if (resetCallerSeekTo > 0) {
566+
seekPosition.position = resetCallerSeekTo;
567+
resetCallerSeekTo = 0;
532568
}
533569
}
534570
}
@@ -538,7 +574,7 @@ private void enterReadingAtomHeaderState() {
538574
atomHeaderBytesRead = 0;
539575
}
540576

541-
private boolean readAtomHeader(ExtractorInput input) throws IOException {
577+
private boolean readAtomHeader(ExtractorInput input, boolean skipAtomParse) throws IOException {
542578
if (atomHeaderBytesRead == 0) {
543579
// Read the standard length atom header.
544580
if (!input.readFully(atomHeader.getData(), 0, Mp4Box.HEADER_SIZE, true)) {
@@ -573,6 +609,10 @@ private boolean readAtomHeader(ExtractorInput input) throws IOException {
573609
"Atom size less than header length (unsupported).");
574610
}
575611

612+
if (skipAtomParse) {
613+
return true;
614+
}
615+
576616
long atomPosition = input.getPosition() - atomHeaderBytesRead;
577617
if (atomType == Mp4Box.TYPE_moof || atomType == Mp4Box.TYPE_mdat) {
578618
if (!haveOutputSeekMap) {
@@ -639,7 +679,7 @@ private void readAtomPayload(ExtractorInput input) throws IOException {
639679
@Nullable ParsableByteArray atomData = this.atomData;
640680
if (atomData != null) {
641681
input.readFully(atomData.getData(), Mp4Box.HEADER_SIZE, atomPayloadSize);
642-
onLeafAtomRead(new LeafBox(atomType, atomData), input.getPosition());
682+
onLeafAtomRead(new LeafBox(atomType, atomData), input.getPosition(), input);
643683
} else {
644684
input.skipFully(atomPayloadSize);
645685
}
@@ -653,14 +693,27 @@ private void processAtomEnded(long atomEndPosition) throws ParserException {
653693
enterReadingAtomHeaderState();
654694
}
655695

656-
private void onLeafAtomRead(LeafBox leaf, long inputPosition) throws ParserException {
696+
private void onLeafAtomRead(LeafBox leaf, long inputPosition, ExtractorInput input)
697+
throws ParserException, IOException {
657698
if (!containerAtoms.isEmpty()) {
658699
containerAtoms.peek().add(leaf);
659700
} else if (leaf.type == Mp4Box.TYPE_sidx) {
660701
Pair<Long, ChunkIndex> result = parseSidx(leaf.data, inputPosition);
661-
segmentIndexEarliestPresentationTimeUs = result.first;
662-
extractorOutput.seekMap(result.second);
663-
haveOutputSeekMap = true;
702+
703+
wrappingSegmentIndex.merge(result.second);
704+
if (!haveOutputSeekMap) {
705+
segmentIndexEarliestPresentationTimeUs = result.first;
706+
extractorOutput.seekMap(result.second);
707+
haveOutputSeekMap = true;
708+
} else if (!haveOutputSeekMapComplete && wrappingSegmentIndex.size() > 1) {
709+
resetCallerSeekTo = inputPosition;
710+
try {
711+
readRemainingSidxAtomsIntoTrack(input);
712+
haveOutputSeekMapComplete = true;
713+
} finally {
714+
extractorOutput.seekMap(wrappingSegmentIndex.toChunkIndex());
715+
}
716+
}
664717
} else if (leaf.type == Mp4Box.TYPE_emsg) {
665718
onEmsgLeafAtomRead(leaf.data);
666719
}

libraries/extractor/src/test/java/androidx/media3/extractor/mp4/FragmentedMp4ExtractorParameterizedTest.java

+7
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,13 @@ public void sampleSeekable() throws Exception {
8888
/* closedCaptionFormats= */ ImmutableList.of(), "media/mp4/sample_fragmented_seekable.mp4");
8989
}
9090

91+
@Test
92+
public void sampleSeekableMultipleSidx() throws Exception {
93+
assertExtractorBehavior(
94+
/* closedCaptionFormats= */ ImmutableList.of(),
95+
"media/mp4/sample_fragmented_seekable_multi_sidx.mp4");
96+
}
97+
9198
@Test
9299
public void sampleWithSeiPayloadInputHasNoCaptions() throws Exception {
93100
// Enabling the CEA-608 track enables SEI payload parsing.

0 commit comments

Comments
 (0)