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