Commit 98ecc209 by Oliver Woodman

Improvements to Mp4Extractor and FragmentedMp4Extractor.

- Make Mp4Extractor more robust when resuming from read failures.
- Made FragmentedMp4Extractor handle atoms with extended sizes.

Issue #652
parent 2f0aec43
......@@ -85,9 +85,10 @@ public interface Extractor {
* Notifies the extractor that a seek has occurred.
* <p>
* Following a call to this method, the {@link ExtractorInput} passed to the next invocation of
* {@link #read(ExtractorInput, PositionHolder)} is required to provide data starting from any
* random access position in the stream. Random access positions can be obtained from a
* {@link SeekMap} that has been extracted and passed to the {@link ExtractorOutput}.
* {@link #read(ExtractorInput, PositionHolder)} is required to provide data starting from a
* random access position in the stream. Valid random access positions are the start of the
* stream and positions that can be obtained from any {@link SeekMap} passed to the
* {@link ExtractorOutput}.
*/
void seek();
......
......@@ -24,16 +24,24 @@ import java.util.List;
/* package*/ abstract class Atom {
/** Size of an atom header, in bytes. */
/**
* Size of an atom header, in bytes.
*/
public static final int HEADER_SIZE = 8;
/** Size of a full atom header, in bytes. */
/**
* Size of a full atom header, in bytes.
*/
public static final int FULL_HEADER_SIZE = 12;
/** Size of a long atom header, in bytes. */
/**
* Size of a long atom header, in bytes.
*/
public static final int LONG_HEADER_SIZE = 16;
/** Value for the first 32 bits of atomSize when the atom size is actually a long value. */
/**
* Value for the first 32 bits of atomSize when the atom size is actually a long value.
*/
public static final int LONG_SIZE_PREFIX = 1;
public static final int TYPE_ftyp = Util.getIntegerCodeForString("ftyp");
......@@ -97,7 +105,7 @@ import java.util.List;
public final int type;
Atom(int type) {
public Atom(int type) {
this.type = type;
}
......@@ -106,11 +114,20 @@ import java.util.List;
return getAtomTypeString(type);
}
/** An MP4 atom that is a leaf. */
public static final class LeafAtom extends Atom {
/**
* An MP4 atom that is a leaf.
*/
/* package */ static final class LeafAtom extends Atom {
/**
* The atom data.
*/
public final ParsableByteArray data;
/**
* @param type The type of the atom.
* @param data The atom data.
*/
public LeafAtom(int type, ParsableByteArray data) {
super(type);
this.data = data;
......@@ -118,29 +135,53 @@ import java.util.List;
}
/** An MP4 atom that has child atoms. */
public static final class ContainerAtom extends Atom {
/**
* An MP4 atom that has child atoms.
*/
/* package */ static final class ContainerAtom extends Atom {
public final long endByteOffset;
public final long endPosition;
public final List<LeafAtom> leafChildren;
public final List<ContainerAtom> containerChildren;
public ContainerAtom(int type, long endByteOffset) {
/**
* @param type The type of the atom.
* @param endPosition The position of the first byte after the end of the atom.
*/
public ContainerAtom(int type, long endPosition) {
super(type);
this.endPosition = endPosition;
leafChildren = new ArrayList<>();
containerChildren = new ArrayList<>();
this.endByteOffset = endByteOffset;
}
/**
* Adds a child leaf to this container.
*
* @param atom The child to add.
*/
public void add(LeafAtom atom) {
leafChildren.add(atom);
}
/**
* Adds a child container to this container.
*
* @param atom The child to add.
*/
public void add(ContainerAtom atom) {
containerChildren.add(atom);
}
/**
* Gets the child leaf of the given type.
* <p>
* If no child exists with the given type then null is returned. If multiple children exist with
* the given type then the first one to have been added is returned.
*
* @param type The leaf type.
* @return The child leaf of the given type, or null if no such child exists.
*/
public LeafAtom getLeafAtomOfType(int type) {
int childrenSize = leafChildren.size();
for (int i = 0; i < childrenSize; i++) {
......@@ -152,6 +193,15 @@ import java.util.List;
return null;
}
/**
* Gets the child container of the given type.
* <p>
* If no child exists with the given type then null is returned. If multiple children exist with
* the given type then the first one to have been added is returned.
*
* @param type The container type.
* @return The child container of the given type, or null if no such child exists.
*/
public ContainerAtom getContainerAtomOfType(int type) {
int childrenSize = containerChildren.size();
for (int i = 0; i < childrenSize; i++) {
......
......@@ -77,9 +77,9 @@ public final class FragmentedMp4Extractor implements Extractor {
private final TrackFragment fragmentRun;
private int parserState;
private int rootAtomBytesRead;
private int atomType;
private int atomSize;
private long atomSize;
private int atomHeaderBytesRead;
private ParsableByteArray atomData;
private int sampleIndex;
......@@ -108,14 +108,14 @@ public final class FragmentedMp4Extractor implements Extractor {
*/
public FragmentedMp4Extractor(int workaroundFlags) {
this.workaroundFlags = workaroundFlags;
atomHeader = new ParsableByteArray(Atom.HEADER_SIZE);
atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE);
nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
nalLength = new ParsableByteArray(4);
encryptionSignalByte = new ParsableByteArray(1);
extendedTypeScratch = new byte[16];
containerAtoms = new Stack<>();
fragmentRun = new TrackFragment();
parserState = STATE_READING_ATOM_HEADER;
enterReadingAtomHeaderState();
}
@Override
......@@ -147,8 +147,7 @@ public final class FragmentedMp4Extractor implements Extractor {
@Override
public void seek() {
containerAtoms.clear();
rootAtomBytesRead = 0;
parserState = STATE_READING_ATOM_HEADER;
enterReadingAtomHeaderState();
}
@Override
......@@ -175,15 +174,30 @@ public final class FragmentedMp4Extractor implements Extractor {
}
}
private void enterReadingAtomHeaderState() {
parserState = STATE_READING_ATOM_HEADER;
atomHeaderBytesRead = 0;
}
private boolean readAtomHeader(ExtractorInput input) throws IOException, InterruptedException {
if (!input.readFully(atomHeader.data, 0, Atom.HEADER_SIZE, true)) {
return false;
if (atomHeaderBytesRead == 0) {
// Read the standard length atom header.
if (!input.readFully(atomHeader.data, 0, Atom.HEADER_SIZE, true)) {
return false;
}
atomHeaderBytesRead = Atom.HEADER_SIZE;
atomHeader.setPosition(0);
atomSize = atomHeader.readUnsignedInt();
atomType = atomHeader.readInt();
}
rootAtomBytesRead += Atom.HEADER_SIZE;
atomHeader.setPosition(0);
atomSize = atomHeader.readInt();
atomType = atomHeader.readInt();
if (atomSize == Atom.LONG_SIZE_PREFIX) {
// Read the extended atom size.
int headerBytesRemaining = Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE;
input.readFully(atomHeader.data, Atom.HEADER_SIZE, headerBytesRemaining);
atomHeaderBytesRead += headerBytesRemaining;
atomSize = atomHeader.readUnsignedLongToLong();
}
if (atomType == Atom.TYPE_mdat) {
if (!haveOutputSeekMap) {
......@@ -200,15 +214,21 @@ public final class FragmentedMp4Extractor implements Extractor {
if (shouldParseAtom(atomType)) {
if (shouldParseContainerAtom(atomType)) {
parserState = STATE_READING_ATOM_HEADER;
containerAtoms.add(new ContainerAtom(atomType,
rootAtomBytesRead + atomSize - Atom.HEADER_SIZE));
long endPosition = input.getPosition() + atomSize - Atom.HEADER_SIZE;
containerAtoms.add(new ContainerAtom(atomType, endPosition));
enterReadingAtomHeaderState();
} else {
atomData = new ParsableByteArray(atomSize);
// We don't support parsing of leaf atoms that define extended atom sizes, or that have
// lengths greater than Integer.MAX_VALUE.
Assertions.checkState(atomHeaderBytesRead == Atom.HEADER_SIZE);
Assertions.checkState(atomSize <= Integer.MAX_VALUE);
atomData = new ParsableByteArray((int) atomSize);
System.arraycopy(atomHeader.data, 0, atomData.data, 0, Atom.HEADER_SIZE);
parserState = STATE_READING_ATOM_PAYLOAD;
}
} else {
// We don't support skipping of atoms that have lengths greater than Integer.MAX_VALUE.
Assertions.checkState(atomSize <= Integer.MAX_VALUE);
atomData = null;
parserState = STATE_READING_ATOM_PAYLOAD;
}
......@@ -217,22 +237,18 @@ public final class FragmentedMp4Extractor implements Extractor {
}
private void readAtomPayload(ExtractorInput input) throws IOException, InterruptedException {
int payloadLength = atomSize - Atom.HEADER_SIZE;
int atomPayloadSize = (int) atomSize - atomHeaderBytesRead;
if (atomData != null) {
input.readFully(atomData.data, Atom.HEADER_SIZE, payloadLength);
rootAtomBytesRead += payloadLength;
input.readFully(atomData.data, Atom.HEADER_SIZE, atomPayloadSize);
onLeafAtomRead(new LeafAtom(atomType, atomData), input.getPosition());
} else {
input.skipFully(payloadLength);
rootAtomBytesRead += payloadLength;
input.skipFully(atomPayloadSize);
}
while (!containerAtoms.isEmpty() && containerAtoms.peek().endByteOffset == rootAtomBytesRead) {
long currentPosition = input.getPosition();
while (!containerAtoms.isEmpty() && containerAtoms.peek().endPosition == currentPosition) {
onContainerAtomRead(containerAtoms.pop());
}
if (containerAtoms.isEmpty()) {
rootAtomBytesRead = 0;
}
parserState = STATE_READING_ATOM_HEADER;
enterReadingAtomHeaderState();
}
private void onLeafAtomRead(LeafAtom leaf, long inputPosition) {
......@@ -606,7 +622,7 @@ public final class FragmentedMp4Extractor implements Extractor {
private boolean readSample(ExtractorInput input) throws IOException, InterruptedException {
if (sampleIndex >= fragmentRun.length) {
// We've run out of samples in the current mdat atom.
parserState = STATE_READING_ATOM_HEADER;
enterReadingAtomHeaderState();
return false;
}
......
......@@ -37,15 +37,16 @@ import java.util.Stack;
public final class Mp4Extractor implements Extractor, SeekMap {
// Parser states.
private static final int STATE_READING_ATOM_HEADER = 0;
private static final int STATE_READING_ATOM_PAYLOAD = 1;
private static final int STATE_READING_SAMPLE = 2;
private static final int STATE_AFTER_SEEK = 0;
private static final int STATE_READING_ATOM_HEADER = 1;
private static final int STATE_READING_ATOM_PAYLOAD = 2;
private static final int STATE_READING_SAMPLE = 3;
/**
* When seeking within the source, if the offset is greater than or equal to this value (or the
* offset is negative), the source will be reloaded.
*/
private static final int RELOAD_MINIMUM_SEEK_DISTANCE = 256 * 1024;
private static final long RELOAD_MINIMUM_SEEK_DISTANCE = 256 * 1024;
// Temporary arrays.
private final ParsableByteArray nalStartCode;
......@@ -55,10 +56,9 @@ public final class Mp4Extractor implements Extractor, SeekMap {
private final Stack<ContainerAtom> containerAtoms;
private int parserState;
private long rootAtomBytesRead;
private int atomType;
private long atomSize;
private int atomBytesRead;
private int atomHeaderBytesRead;
private ParsableByteArray atomData;
private int sampleSize;
......@@ -74,7 +74,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
containerAtoms = new Stack<>();
nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
nalLength = new ParsableByteArray(4);
parserState = STATE_READING_ATOM_HEADER;
enterReadingAtomHeaderState();
}
@Override
......@@ -89,9 +89,11 @@ public final class Mp4Extractor implements Extractor, SeekMap {
@Override
public void seek() {
rootAtomBytesRead = 0;
containerAtoms.clear();
atomHeaderBytesRead = 0;
sampleBytesWritten = 0;
sampleCurrentNalBytesRemaining = 0;
parserState = STATE_AFTER_SEEK;
}
@Override
......@@ -99,6 +101,13 @@ public final class Mp4Extractor implements Extractor, SeekMap {
throws IOException, InterruptedException {
while (true) {
switch (parserState) {
case STATE_AFTER_SEEK:
if (input.getPosition() == 0) {
enterReadingAtomHeaderState();
} else {
parserState = STATE_READING_SAMPLE;
}
break;
case STATE_READING_ATOM_HEADER:
if (!readAtomHeader(input)) {
return RESULT_END_OF_INPUT;
......@@ -143,36 +152,40 @@ public final class Mp4Extractor implements Extractor, SeekMap {
// Private methods.
private void enterReadingAtomHeaderState() {
parserState = STATE_READING_ATOM_HEADER;
atomHeaderBytesRead = 0;
}
private boolean readAtomHeader(ExtractorInput input) throws IOException, InterruptedException {
if (!input.readFully(atomHeader.data, 0, Atom.HEADER_SIZE, true)) {
return false;
if (atomHeaderBytesRead == 0) {
// Read the standard length atom header.
if (!input.readFully(atomHeader.data, 0, Atom.HEADER_SIZE, true)) {
return false;
}
atomHeaderBytesRead = Atom.HEADER_SIZE;
atomHeader.setPosition(0);
atomSize = atomHeader.readUnsignedInt();
atomType = atomHeader.readInt();
}
atomHeader.setPosition(0);
atomSize = atomHeader.readUnsignedInt();
atomType = atomHeader.readInt();
if (atomSize == Atom.LONG_SIZE_PREFIX) {
// The extended atom size is contained in the next 8 bytes, so try to read it now.
input.readFully(atomHeader.data, Atom.HEADER_SIZE, Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE);
atomSize = atomHeader.readLong();
rootAtomBytesRead += Atom.LONG_HEADER_SIZE;
atomBytesRead = Atom.LONG_HEADER_SIZE;
} else {
rootAtomBytesRead += Atom.HEADER_SIZE;
atomBytesRead = Atom.HEADER_SIZE;
// Read the extended atom size.
int headerBytesRemaining = Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE;
input.readFully(atomHeader.data, Atom.HEADER_SIZE, headerBytesRemaining);
atomHeaderBytesRead += headerBytesRemaining;
atomSize = atomHeader.readUnsignedLongToLong();
}
if (shouldParseContainerAtom(atomType)) {
if (atomSize == Atom.LONG_SIZE_PREFIX) {
containerAtoms.add(
new ContainerAtom(atomType, rootAtomBytesRead + atomSize - atomBytesRead));
} else {
containerAtoms.add(
new ContainerAtom(atomType, rootAtomBytesRead + atomSize - atomBytesRead));
}
parserState = STATE_READING_ATOM_HEADER;
long endPosition = input.getPosition() + atomSize - atomHeaderBytesRead;
containerAtoms.add(new ContainerAtom(atomType, endPosition));
enterReadingAtomHeaderState();
} else if (shouldParseLeafAtom(atomType)) {
Assertions.checkState(atomSize < Integer.MAX_VALUE);
// We don't support parsing of leaf atoms that define extended atom sizes, or that have
// lengths greater than Integer.MAX_VALUE.
Assertions.checkState(atomHeaderBytesRead == Atom.HEADER_SIZE);
Assertions.checkState(atomSize <= Integer.MAX_VALUE);
atomData = new ParsableByteArray((int) atomSize);
System.arraycopy(atomHeader.data, 0, atomData.data, 0, Atom.HEADER_SIZE);
parserState = STATE_READING_ATOM_PAYLOAD;
......@@ -191,31 +204,38 @@ public final class Mp4Extractor implements Extractor, SeekMap {
*/
private boolean readAtomPayload(ExtractorInput input, PositionHolder positionHolder)
throws IOException, InterruptedException {
parserState = STATE_READING_ATOM_HEADER;
rootAtomBytesRead += atomSize - atomBytesRead;
long atomRemainingBytes = atomSize - atomBytesRead;
boolean seekRequired = atomData == null
&& (atomSize >= RELOAD_MINIMUM_SEEK_DISTANCE || atomSize > Integer.MAX_VALUE);
if (seekRequired) {
positionHolder.position = rootAtomBytesRead;
} else if (atomData != null) {
input.readFully(atomData.data, atomBytesRead, (int) atomRemainingBytes);
long atomPayloadSize = atomSize - atomHeaderBytesRead;
long atomEndPosition = input.getPosition() + atomPayloadSize;
boolean seekRequired = false;
if (atomData != null) {
input.readFully(atomData.data, atomHeaderBytesRead, (int) atomPayloadSize);
if (!containerAtoms.isEmpty()) {
containerAtoms.peek().add(new Atom.LeafAtom(atomType, atomData));
}
} else {
input.skipFully((int) atomRemainingBytes);
// We don't need the data. Skip or seek, depending on how large the atom is.
if (atomPayloadSize < RELOAD_MINIMUM_SEEK_DISTANCE) {
input.skipFully((int) atomPayloadSize);
} else {
positionHolder.position = input.getPosition() + atomPayloadSize;
seekRequired = true;
}
}
while (!containerAtoms.isEmpty() && containerAtoms.peek().endByteOffset == rootAtomBytesRead) {
while (!containerAtoms.isEmpty() && containerAtoms.peek().endPosition == atomEndPosition) {
Atom.ContainerAtom containerAtom = containerAtoms.pop();
if (containerAtom.type == Atom.TYPE_moov) {
// We've reached the end of the moov atom. Process it and prepare to read samples.
processMoovAtom(containerAtom);
containerAtoms.clear();
parserState = STATE_READING_SAMPLE;
return false;
} else if (!containerAtoms.isEmpty()) {
containerAtoms.peek().add(containerAtom);
}
}
enterReadingAtomHeaderState();
return seekRequired;
}
......@@ -253,7 +273,6 @@ public final class Mp4Extractor implements Extractor, SeekMap {
this.tracks = tracks.toArray(new Mp4Track[0]);
extractorOutput.endTracks();
extractorOutput.seekMap(this);
parserState = STATE_READING_SAMPLE;
}
/**
......
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