Commit 7cacbfed by tonihei Committed by Oliver Woodman

Mitigate OOM at poorly interleaved Mp4 streams.

When determining the next sample to load, the Mp4Extractor now takes
into account how far one stream is reading ahead of the others.
If one stream is reading ahead more than a threshold (default: 10 seconds),
the extractor continues reading the other stream even though it needs
to reload the source at a new position.

GitHub:#3481
GitHub:#3214
GitHub:#3670

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=182504396
parent 06be0fd7
...@@ -88,6 +88,12 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -88,6 +88,12 @@ public final class Mp4Extractor implements Extractor, SeekMap {
*/ */
private static final long RELOAD_MINIMUM_SEEK_DISTANCE = 256 * 1024; private static final long RELOAD_MINIMUM_SEEK_DISTANCE = 256 * 1024;
/**
* For poorly interleaved streams, the maximum byte difference one track is allowed to be read
* ahead before the source will be reloaded at a new position to read another track.
*/
private static final long MAXIMUM_READ_AHEAD_BYTES_STREAM = 10 * 1024 * 1024;
private final @Flags int flags; private final @Flags int flags;
// Temporary arrays. // Temporary arrays.
...@@ -103,12 +109,14 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -103,12 +109,14 @@ public final class Mp4Extractor implements Extractor, SeekMap {
private int atomHeaderBytesRead; private int atomHeaderBytesRead;
private ParsableByteArray atomData; private ParsableByteArray atomData;
private int sampleTrackIndex;
private int sampleBytesWritten; private int sampleBytesWritten;
private int sampleCurrentNalBytesRemaining; private int sampleCurrentNalBytesRemaining;
// Extractor outputs. // Extractor outputs.
private ExtractorOutput extractorOutput; private ExtractorOutput extractorOutput;
private Mp4Track[] tracks; private Mp4Track[] tracks;
private long[][] accumulatedSampleSizes;
private int firstVideoTrackIndex; private int firstVideoTrackIndex;
private long durationUs; private long durationUs;
private boolean isQuickTime; private boolean isQuickTime;
...@@ -132,6 +140,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -132,6 +140,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
containerAtoms = new Stack<>(); containerAtoms = new Stack<>();
nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
nalLength = new ParsableByteArray(4); nalLength = new ParsableByteArray(4);
sampleTrackIndex = C.INDEX_UNSET;
} }
@Override @Override
...@@ -148,6 +157,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -148,6 +157,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
public void seek(long position, long timeUs) { public void seek(long position, long timeUs) {
containerAtoms.clear(); containerAtoms.clear();
atomHeaderBytesRead = 0; atomHeaderBytesRead = 0;
sampleTrackIndex = C.INDEX_UNSET;
sampleBytesWritten = 0; sampleBytesWritten = 0;
sampleCurrentNalBytesRemaining = 0; sampleCurrentNalBytesRemaining = 0;
if (position == 0) { if (position == 0) {
...@@ -426,6 +436,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -426,6 +436,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
this.firstVideoTrackIndex = firstVideoTrackIndex; this.firstVideoTrackIndex = firstVideoTrackIndex;
this.durationUs = durationUs; this.durationUs = durationUs;
this.tracks = tracks.toArray(new Mp4Track[tracks.size()]); this.tracks = tracks.toArray(new Mp4Track[tracks.size()]);
accumulatedSampleSizes = calculateAccumulatedSampleSizes(this.tracks);
extractorOutput.endTracks(); extractorOutput.endTracks();
extractorOutput.seekMap(this); extractorOutput.seekMap(this);
...@@ -449,26 +460,29 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -449,26 +460,29 @@ public final class Mp4Extractor implements Extractor, SeekMap {
*/ */
private int readSample(ExtractorInput input, PositionHolder positionHolder) private int readSample(ExtractorInput input, PositionHolder positionHolder)
throws IOException, InterruptedException { throws IOException, InterruptedException {
int trackIndex = getTrackIndexOfEarliestCurrentSample(); long inputPosition = input.getPosition();
if (trackIndex == C.INDEX_UNSET) { if (sampleTrackIndex == C.INDEX_UNSET) {
sampleTrackIndex = getTrackIndexOfNextReadSample(inputPosition);
if (sampleTrackIndex == C.INDEX_UNSET) {
return RESULT_END_OF_INPUT; return RESULT_END_OF_INPUT;
} }
Mp4Track track = tracks[trackIndex]; }
Mp4Track track = tracks[sampleTrackIndex];
TrackOutput trackOutput = track.trackOutput; TrackOutput trackOutput = track.trackOutput;
int sampleIndex = track.sampleIndex; int sampleIndex = track.sampleIndex;
long position = track.sampleTable.offsets[sampleIndex]; long position = track.sampleTable.offsets[sampleIndex];
int sampleSize = track.sampleTable.sizes[sampleIndex]; int sampleSize = track.sampleTable.sizes[sampleIndex];
long skipAmount = position - inputPosition + sampleBytesWritten;
if (skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE) {
positionHolder.position = position;
return RESULT_SEEK;
}
if (track.track.sampleTransformation == Track.TRANSFORMATION_CEA608_CDAT) { if (track.track.sampleTransformation == Track.TRANSFORMATION_CEA608_CDAT) {
// The sample information is contained in a cdat atom. The header must be discarded for // The sample information is contained in a cdat atom. The header must be discarded for
// committing. // committing.
position += Atom.HEADER_SIZE; skipAmount += Atom.HEADER_SIZE;
sampleSize -= Atom.HEADER_SIZE; sampleSize -= Atom.HEADER_SIZE;
} }
long skipAmount = position - input.getPosition() + sampleBytesWritten;
if (skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE) {
positionHolder.position = position;
return RESULT_SEEK;
}
input.skipFully((int) skipAmount); input.skipFully((int) skipAmount);
if (track.track.nalUnitLengthFieldLength != 0) { if (track.track.nalUnitLengthFieldLength != 0) {
// Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case
...@@ -510,33 +524,61 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -510,33 +524,61 @@ public final class Mp4Extractor implements Extractor, SeekMap {
trackOutput.sampleMetadata(track.sampleTable.timestampsUs[sampleIndex], trackOutput.sampleMetadata(track.sampleTable.timestampsUs[sampleIndex],
track.sampleTable.flags[sampleIndex], sampleSize, 0, null); track.sampleTable.flags[sampleIndex], sampleSize, 0, null);
track.sampleIndex++; track.sampleIndex++;
sampleTrackIndex = C.INDEX_UNSET;
sampleBytesWritten = 0; sampleBytesWritten = 0;
sampleCurrentNalBytesRemaining = 0; sampleCurrentNalBytesRemaining = 0;
return RESULT_CONTINUE; return RESULT_CONTINUE;
} }
/** /**
* Returns the index of the track that contains the earliest current sample, or * Returns the index of the track that contains the next sample to be read, or {@link
* {@link C#INDEX_UNSET} if no samples remain. * C#INDEX_UNSET} if no samples remain.
*
* <p>The preferred choice is the sample with the smallest offset not requiring a source reload,
* or if not available the sample with the smallest overall offset to avoid subsequent source
* reloads.
*
* <p>To deal with poor sample interleaving, we also check whether the required memory to catch up
* with the next logical sample (based on sample time) exceeds {@link
* #MAXIMUM_READ_AHEAD_BYTES_STREAM}. If this is the case, we continue with this sample even
* though it may require a source reload.
*/ */
private int getTrackIndexOfEarliestCurrentSample() { private int getTrackIndexOfNextReadSample(long inputPosition) {
int earliestSampleTrackIndex = C.INDEX_UNSET; long preferredSkipAmount = Long.MAX_VALUE;
long earliestSampleOffset = Long.MAX_VALUE; boolean preferredRequiresReload = true;
int preferredTrackIndex = C.INDEX_UNSET;
long preferredAccumulatedBytes = Long.MAX_VALUE;
long minAccumulatedBytes = Long.MAX_VALUE;
boolean minAccumulatedBytesRequiresReload = true;
int minAccumulatedBytesTrackIndex = C.INDEX_UNSET;
for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) { for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) {
Mp4Track track = tracks[trackIndex]; Mp4Track track = tracks[trackIndex];
int sampleIndex = track.sampleIndex; int sampleIndex = track.sampleIndex;
if (sampleIndex == track.sampleTable.sampleCount) { if (sampleIndex == track.sampleTable.sampleCount) {
continue; continue;
} }
long sampleOffset = track.sampleTable.offsets[sampleIndex];
long trackSampleOffset = track.sampleTable.offsets[sampleIndex]; long sampleAccumulatedBytes = accumulatedSampleSizes[trackIndex][sampleIndex];
if (trackSampleOffset < earliestSampleOffset) { long skipAmount = sampleOffset - inputPosition;
earliestSampleOffset = trackSampleOffset; boolean requiresReload = skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE;
earliestSampleTrackIndex = trackIndex; if ((!requiresReload && preferredRequiresReload)
|| (requiresReload == preferredRequiresReload && skipAmount < preferredSkipAmount)) {
preferredRequiresReload = requiresReload;
preferredSkipAmount = skipAmount;
preferredTrackIndex = trackIndex;
preferredAccumulatedBytes = sampleAccumulatedBytes;
} }
if (sampleAccumulatedBytes < minAccumulatedBytes) {
minAccumulatedBytes = sampleAccumulatedBytes;
minAccumulatedBytesRequiresReload = requiresReload;
minAccumulatedBytesTrackIndex = trackIndex;
} }
}
return earliestSampleTrackIndex; return minAccumulatedBytes == Long.MAX_VALUE
|| !minAccumulatedBytesRequiresReload
|| preferredAccumulatedBytes < minAccumulatedBytes + MAXIMUM_READ_AHEAD_BYTES_STREAM
? preferredTrackIndex
: minAccumulatedBytesTrackIndex;
} }
/** /**
...@@ -555,6 +597,45 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -555,6 +597,45 @@ public final class Mp4Extractor implements Extractor, SeekMap {
} }
/** /**
* For each sample of each track, calculates accumulated size of all samples which need to be read
* before this sample can be used.
*/
private static long[][] calculateAccumulatedSampleSizes(Mp4Track[] tracks) {
long[][] accumulatedSampleSizes = new long[tracks.length][];
int[] nextSampleIndex = new int[tracks.length];
long[] nextSampleTimesUs = new long[tracks.length];
boolean[] tracksFinished = new boolean[tracks.length];
for (int i = 0; i < tracks.length; i++) {
accumulatedSampleSizes[i] = new long[tracks[i].sampleTable.sampleCount];
nextSampleTimesUs[i] = tracks[i].sampleTable.timestampsUs[0];
}
long accumulatedSampleSize = 0;
int finishedTracks = 0;
while (finishedTracks < tracks.length) {
long minTimeUs = Long.MAX_VALUE;
int minTimeTrackIndex = -1;
for (int i = 0; i < tracks.length; i++) {
if (!tracksFinished[i] && nextSampleTimesUs[i] <= minTimeUs) {
minTimeTrackIndex = i;
minTimeUs = nextSampleTimesUs[i];
}
}
int trackSampleIndex = nextSampleIndex[minTimeTrackIndex];
accumulatedSampleSizes[minTimeTrackIndex][trackSampleIndex] = accumulatedSampleSize;
accumulatedSampleSize += tracks[minTimeTrackIndex].sampleTable.sizes[trackSampleIndex];
nextSampleIndex[minTimeTrackIndex] = ++trackSampleIndex;
if (trackSampleIndex < accumulatedSampleSizes[minTimeTrackIndex].length) {
nextSampleTimesUs[minTimeTrackIndex] =
tracks[minTimeTrackIndex].sampleTable.timestampsUs[trackSampleIndex];
} else {
tracksFinished[minTimeTrackIndex] = true;
finishedTracks++;
}
}
return accumulatedSampleSizes;
}
/**
* Adjusts a seek point offset to take into account the track with the given {@code sampleTable}, * Adjusts a seek point offset to take into account the track with the given {@code sampleTable},
* for a given {@code seekTimeUs}. * for a given {@code seekTimeUs}.
* *
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment