mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Add new style mp4/fmp4 extractors.
This commit is contained in:
parent
f002e6a76e
commit
587edf8e2b
31 changed files with 2081 additions and 1071 deletions
|
|
@ -49,6 +49,7 @@ public class DemoUtil {
|
||||||
public static final int TYPE_OTHER = 2;
|
public static final int TYPE_OTHER = 2;
|
||||||
public static final int TYPE_HLS = 3;
|
public static final int TYPE_HLS = 3;
|
||||||
public static final int TYPE_MP4 = 4;
|
public static final int TYPE_MP4 = 4;
|
||||||
|
public static final int TYPE_MP3 = 5;
|
||||||
|
|
||||||
private static final CookieManager defaultCookieManager;
|
private static final CookieManager defaultCookieManager;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,12 @@ import com.google.android.exoplayer.demo.player.DashRendererBuilder;
|
||||||
import com.google.android.exoplayer.demo.player.DefaultRendererBuilder;
|
import com.google.android.exoplayer.demo.player.DefaultRendererBuilder;
|
||||||
import com.google.android.exoplayer.demo.player.DemoPlayer;
|
import com.google.android.exoplayer.demo.player.DemoPlayer;
|
||||||
import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder;
|
import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder;
|
||||||
|
import com.google.android.exoplayer.demo.player.ExtractorRendererBuilder;
|
||||||
import com.google.android.exoplayer.demo.player.HlsRendererBuilder;
|
import com.google.android.exoplayer.demo.player.HlsRendererBuilder;
|
||||||
import com.google.android.exoplayer.demo.player.Mp4RendererBuilder;
|
|
||||||
import com.google.android.exoplayer.demo.player.SmoothStreamingRendererBuilder;
|
import com.google.android.exoplayer.demo.player.SmoothStreamingRendererBuilder;
|
||||||
import com.google.android.exoplayer.demo.player.UnsupportedDrmException;
|
import com.google.android.exoplayer.demo.player.UnsupportedDrmException;
|
||||||
|
import com.google.android.exoplayer.extractor.mp3.Mp3Extractor;
|
||||||
|
import com.google.android.exoplayer.extractor.mp4.Mp4Extractor;
|
||||||
import com.google.android.exoplayer.metadata.GeobMetadata;
|
import com.google.android.exoplayer.metadata.GeobMetadata;
|
||||||
import com.google.android.exoplayer.metadata.PrivMetadata;
|
import com.google.android.exoplayer.metadata.PrivMetadata;
|
||||||
import com.google.android.exoplayer.metadata.TxxxMetadata;
|
import com.google.android.exoplayer.metadata.TxxxMetadata;
|
||||||
|
|
@ -217,7 +219,11 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
|
||||||
case DemoUtil.TYPE_HLS:
|
case DemoUtil.TYPE_HLS:
|
||||||
return new HlsRendererBuilder(userAgent, contentUri.toString());
|
return new HlsRendererBuilder(userAgent, contentUri.toString());
|
||||||
case DemoUtil.TYPE_MP4:
|
case DemoUtil.TYPE_MP4:
|
||||||
return new Mp4RendererBuilder(contentUri, debugTextView);
|
return new ExtractorRendererBuilder(userAgent, contentUri, debugTextView,
|
||||||
|
new Mp4Extractor());
|
||||||
|
case DemoUtil.TYPE_MP3:
|
||||||
|
return new ExtractorRendererBuilder(userAgent, contentUri, debugTextView,
|
||||||
|
new Mp3Extractor());
|
||||||
default:
|
default:
|
||||||
return new DefaultRendererBuilder(this, contentUri, debugTextView);
|
return new DefaultRendererBuilder(this, contentUri, debugTextView);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -135,12 +135,15 @@ import java.util.Locale;
|
||||||
new Sample("Apple AAC 10s", "https://devimages.apple.com.edgekey.net/"
|
new Sample("Apple AAC 10s", "https://devimages.apple.com.edgekey.net/"
|
||||||
+ "streaming/examples/bipbop_4x3/gear0/fileSequence0.aac",
|
+ "streaming/examples/bipbop_4x3/gear0/fileSequence0.aac",
|
||||||
DemoUtil.TYPE_OTHER),
|
DemoUtil.TYPE_OTHER),
|
||||||
new Sample("Big Buck Bunny (MP4)",
|
new Sample("Big Buck Bunny (MP4 Video)",
|
||||||
"http://redirector.c.youtube.com/videoplayback?id=604ed5ce52eda7ee&itag=22&source=youtube"
|
"http://redirector.c.youtube.com/videoplayback?id=604ed5ce52eda7ee&itag=22&source=youtube"
|
||||||
+ "&sparams=ip,ipbits,expire&ip=0.0.0.0&ipbits=0&expire=19000000000&signature="
|
+ "&sparams=ip,ipbits,expire&ip=0.0.0.0&ipbits=0&expire=19000000000&signature="
|
||||||
+ "2E853B992F6CAB9D28CA3BEBD84A6F26709A8A55.94344B0D8BA83A7417AAD24DACC8C71A9A878ECE"
|
+ "2E853B992F6CAB9D28CA3BEBD84A6F26709A8A55.94344B0D8BA83A7417AAD24DACC8C71A9A878ECE"
|
||||||
+ "&key=ik0",
|
+ "&key=ik0",
|
||||||
DemoUtil.TYPE_MP4),
|
DemoUtil.TYPE_MP4),
|
||||||
|
new Sample("Google Play (MP3 Audio)",
|
||||||
|
"http://storage.googleapis.com/exoplayer-test-media-0/play.mp3",
|
||||||
|
DemoUtil.TYPE_MP3),
|
||||||
};
|
};
|
||||||
|
|
||||||
private Samples() {}
|
private Samples() {}
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,9 @@ import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
|
||||||
import com.google.android.exoplayer.TrackRenderer;
|
import com.google.android.exoplayer.TrackRenderer;
|
||||||
import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder;
|
import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder;
|
||||||
import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilderCallback;
|
import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilderCallback;
|
||||||
import com.google.android.exoplayer.source.DefaultSampleSource;
|
import com.google.android.exoplayer.extractor.Extractor;
|
||||||
import com.google.android.exoplayer.source.Mp4SampleExtractor;
|
import com.google.android.exoplayer.extractor.ExtractorSampleSource;
|
||||||
import com.google.android.exoplayer.upstream.DataSpec;
|
import com.google.android.exoplayer.upstream.DataSource;
|
||||||
import com.google.android.exoplayer.upstream.UriDataSource;
|
import com.google.android.exoplayer.upstream.UriDataSource;
|
||||||
|
|
||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
|
|
@ -30,23 +30,31 @@ import android.net.Uri;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link RendererBuilder} for streams that can be read using {@link Mp4SampleExtractor}.
|
* A {@link RendererBuilder} for streams that can be read using an {@link Extractor}.
|
||||||
*/
|
*/
|
||||||
public class Mp4RendererBuilder implements RendererBuilder {
|
public class ExtractorRendererBuilder implements RendererBuilder {
|
||||||
|
|
||||||
|
private static final int BUFFER_SIZE = 10 * 1024 * 1024;
|
||||||
|
|
||||||
|
private final String userAgent;
|
||||||
private final Uri uri;
|
private final Uri uri;
|
||||||
private final TextView debugTextView;
|
private final TextView debugTextView;
|
||||||
|
private final Extractor extractor;
|
||||||
|
|
||||||
public Mp4RendererBuilder(Uri uri, TextView debugTextView) {
|
public ExtractorRendererBuilder(String userAgent, Uri uri, TextView debugTextView,
|
||||||
|
Extractor extractor) {
|
||||||
|
this.userAgent = userAgent;
|
||||||
this.uri = uri;
|
this.uri = uri;
|
||||||
this.debugTextView = debugTextView;
|
this.debugTextView = debugTextView;
|
||||||
|
this.extractor = extractor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) {
|
public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) {
|
||||||
// Build the video and audio renderers.
|
// Build the video and audio renderers.
|
||||||
DefaultSampleSource sampleSource = new DefaultSampleSource(
|
DataSource dataSource = new UriDataSource(userAgent, null);
|
||||||
new Mp4SampleExtractor(new UriDataSource("exoplayer", null), new DataSpec(uri)), 2);
|
ExtractorSampleSource sampleSource = new ExtractorSampleSource(uri, dataSource, extractor, 2,
|
||||||
|
BUFFER_SIZE);
|
||||||
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource,
|
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource,
|
||||||
null, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null, player.getMainHandler(),
|
null, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null, player.getMainHandler(),
|
||||||
player, 50);
|
player, 50);
|
||||||
|
|
@ -22,11 +22,14 @@ import com.google.android.exoplayer.SampleHolder;
|
||||||
import com.google.android.exoplayer.chunk.parser.Extractor;
|
import com.google.android.exoplayer.chunk.parser.Extractor;
|
||||||
import com.google.android.exoplayer.chunk.parser.SegmentIndex;
|
import com.google.android.exoplayer.chunk.parser.SegmentIndex;
|
||||||
import com.google.android.exoplayer.drm.DrmInitData;
|
import com.google.android.exoplayer.drm.DrmInitData;
|
||||||
import com.google.android.exoplayer.mp4.Atom;
|
import com.google.android.exoplayer.extractor.mp4.Atom;
|
||||||
import com.google.android.exoplayer.mp4.Atom.ContainerAtom;
|
import com.google.android.exoplayer.extractor.mp4.Atom.ContainerAtom;
|
||||||
import com.google.android.exoplayer.mp4.Atom.LeafAtom;
|
import com.google.android.exoplayer.extractor.mp4.Atom.LeafAtom;
|
||||||
import com.google.android.exoplayer.mp4.CommonMp4AtomParsers;
|
import com.google.android.exoplayer.extractor.mp4.AtomParsers;
|
||||||
import com.google.android.exoplayer.mp4.Track;
|
import com.google.android.exoplayer.extractor.mp4.DefaultSampleValues;
|
||||||
|
import com.google.android.exoplayer.extractor.mp4.Track;
|
||||||
|
import com.google.android.exoplayer.extractor.mp4.TrackEncryptionBox;
|
||||||
|
import com.google.android.exoplayer.extractor.mp4.TrackFragment;
|
||||||
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
||||||
import com.google.android.exoplayer.util.H264Util;
|
import com.google.android.exoplayer.util.H264Util;
|
||||||
import com.google.android.exoplayer.util.MimeTypes;
|
import com.google.android.exoplayer.util.MimeTypes;
|
||||||
|
|
@ -157,7 +160,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||||
public FragmentedMp4Extractor(int workaroundFlags) {
|
public FragmentedMp4Extractor(int workaroundFlags) {
|
||||||
this.workaroundFlags = workaroundFlags;
|
this.workaroundFlags = workaroundFlags;
|
||||||
parserState = STATE_READING_ATOM_HEADER;
|
parserState = STATE_READING_ATOM_HEADER;
|
||||||
atomHeader = new ParsableByteArray(Atom.ATOM_HEADER_SIZE);
|
atomHeader = new ParsableByteArray(Atom.HEADER_SIZE);
|
||||||
extendedTypeScratch = new byte[16];
|
extendedTypeScratch = new byte[16];
|
||||||
containerAtoms = new Stack<ContainerAtom>();
|
containerAtoms = new Stack<ContainerAtom>();
|
||||||
fragmentRun = new TrackFragment();
|
fragmentRun = new TrackFragment();
|
||||||
|
|
@ -259,14 +262,14 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
private int readAtomHeader(NonBlockingInputStream inputStream) {
|
private int readAtomHeader(NonBlockingInputStream inputStream) {
|
||||||
int remainingBytes = Atom.ATOM_HEADER_SIZE - atomBytesRead;
|
int remainingBytes = Atom.HEADER_SIZE - atomBytesRead;
|
||||||
int bytesRead = inputStream.read(atomHeader.data, atomBytesRead, remainingBytes);
|
int bytesRead = inputStream.read(atomHeader.data, atomBytesRead, remainingBytes);
|
||||||
if (bytesRead == -1) {
|
if (bytesRead == -1) {
|
||||||
return RESULT_END_OF_STREAM;
|
return RESULT_END_OF_STREAM;
|
||||||
}
|
}
|
||||||
rootAtomBytesRead += bytesRead;
|
rootAtomBytesRead += bytesRead;
|
||||||
atomBytesRead += bytesRead;
|
atomBytesRead += bytesRead;
|
||||||
if (atomBytesRead != Atom.ATOM_HEADER_SIZE) {
|
if (atomBytesRead != Atom.HEADER_SIZE) {
|
||||||
return RESULT_NEED_MORE_DATA;
|
return RESULT_NEED_MORE_DATA;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -288,10 +291,10 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||||
if (CONTAINER_TYPES.contains(atomTypeInteger)) {
|
if (CONTAINER_TYPES.contains(atomTypeInteger)) {
|
||||||
enterState(STATE_READING_ATOM_HEADER);
|
enterState(STATE_READING_ATOM_HEADER);
|
||||||
containerAtoms.add(new ContainerAtom(atomType,
|
containerAtoms.add(new ContainerAtom(atomType,
|
||||||
rootAtomBytesRead + atomSize - Atom.ATOM_HEADER_SIZE));
|
rootAtomBytesRead + atomSize - Atom.HEADER_SIZE));
|
||||||
} else {
|
} else {
|
||||||
atomData = new ParsableByteArray(atomSize);
|
atomData = new ParsableByteArray(atomSize);
|
||||||
System.arraycopy(atomHeader.data, 0, atomData.data, 0, Atom.ATOM_HEADER_SIZE);
|
System.arraycopy(atomHeader.data, 0, atomData.data, 0, Atom.HEADER_SIZE);
|
||||||
enterState(STATE_READING_ATOM_PAYLOAD);
|
enterState(STATE_READING_ATOM_PAYLOAD);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -360,7 +363,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||||
LeafAtom child = moovChildren.get(i);
|
LeafAtom child = moovChildren.get(i);
|
||||||
if (child.type == Atom.TYPE_pssh) {
|
if (child.type == Atom.TYPE_pssh) {
|
||||||
ParsableByteArray psshAtom = child.data;
|
ParsableByteArray psshAtom = child.data;
|
||||||
psshAtom.setPosition(Atom.FULL_ATOM_HEADER_SIZE);
|
psshAtom.setPosition(Atom.FULL_HEADER_SIZE);
|
||||||
UUID uuid = new UUID(psshAtom.readLong(), psshAtom.readLong());
|
UUID uuid = new UUID(psshAtom.readLong(), psshAtom.readLong());
|
||||||
int dataSize = psshAtom.readInt();
|
int dataSize = psshAtom.readInt();
|
||||||
byte[] data = new byte[dataSize];
|
byte[] data = new byte[dataSize];
|
||||||
|
|
@ -373,7 +376,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||||
}
|
}
|
||||||
ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex);
|
ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex);
|
||||||
extendsDefaults = parseTrex(mvex.getLeafAtomOfType(Atom.TYPE_trex).data);
|
extendsDefaults = parseTrex(mvex.getLeafAtomOfType(Atom.TYPE_trex).data);
|
||||||
track = CommonMp4AtomParsers.parseTrak(moov.getContainerAtomOfType(Atom.TYPE_trak),
|
track = AtomParsers.parseTrak(moov.getContainerAtomOfType(Atom.TYPE_trak),
|
||||||
moov.getLeafAtomOfType(Atom.TYPE_mvhd));
|
moov.getLeafAtomOfType(Atom.TYPE_mvhd));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -399,7 +402,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||||
* Parses a trex atom (defined in 14496-12).
|
* Parses a trex atom (defined in 14496-12).
|
||||||
*/
|
*/
|
||||||
private static DefaultSampleValues parseTrex(ParsableByteArray trex) {
|
private static DefaultSampleValues parseTrex(ParsableByteArray trex) {
|
||||||
trex.setPosition(Atom.FULL_ATOM_HEADER_SIZE + 4);
|
trex.setPosition(Atom.FULL_HEADER_SIZE + 4);
|
||||||
int defaultSampleDescriptionIndex = trex.readUnsignedIntToInt() - 1;
|
int defaultSampleDescriptionIndex = trex.readUnsignedIntToInt() - 1;
|
||||||
int defaultSampleDuration = trex.readUnsignedIntToInt();
|
int defaultSampleDuration = trex.readUnsignedIntToInt();
|
||||||
int defaultSampleSize = trex.readUnsignedIntToInt();
|
int defaultSampleSize = trex.readUnsignedIntToInt();
|
||||||
|
|
@ -453,7 +456,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||||
private static void parseSaiz(TrackEncryptionBox encryptionBox, ParsableByteArray saiz,
|
private static void parseSaiz(TrackEncryptionBox encryptionBox, ParsableByteArray saiz,
|
||||||
TrackFragment out) {
|
TrackFragment out) {
|
||||||
int vectorSize = encryptionBox.initializationVectorSize;
|
int vectorSize = encryptionBox.initializationVectorSize;
|
||||||
saiz.setPosition(Atom.ATOM_HEADER_SIZE);
|
saiz.setPosition(Atom.HEADER_SIZE);
|
||||||
int fullAtom = saiz.readInt();
|
int fullAtom = saiz.readInt();
|
||||||
int flags = Atom.parseFullAtomFlags(fullAtom);
|
int flags = Atom.parseFullAtomFlags(fullAtom);
|
||||||
if ((flags & 0x01) == 1) {
|
if ((flags & 0x01) == 1) {
|
||||||
|
|
@ -490,7 +493,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||||
*/
|
*/
|
||||||
private static DefaultSampleValues parseTfhd(DefaultSampleValues extendsDefaults,
|
private static DefaultSampleValues parseTfhd(DefaultSampleValues extendsDefaults,
|
||||||
ParsableByteArray tfhd) {
|
ParsableByteArray tfhd) {
|
||||||
tfhd.setPosition(Atom.ATOM_HEADER_SIZE);
|
tfhd.setPosition(Atom.HEADER_SIZE);
|
||||||
int fullAtom = tfhd.readInt();
|
int fullAtom = tfhd.readInt();
|
||||||
int flags = Atom.parseFullAtomFlags(fullAtom);
|
int flags = Atom.parseFullAtomFlags(fullAtom);
|
||||||
|
|
||||||
|
|
@ -519,7 +522,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||||
* media, expressed in the media's timescale.
|
* media, expressed in the media's timescale.
|
||||||
*/
|
*/
|
||||||
private static long parseTfdt(ParsableByteArray tfdt) {
|
private static long parseTfdt(ParsableByteArray tfdt) {
|
||||||
tfdt.setPosition(Atom.ATOM_HEADER_SIZE);
|
tfdt.setPosition(Atom.HEADER_SIZE);
|
||||||
int fullAtom = tfdt.readInt();
|
int fullAtom = tfdt.readInt();
|
||||||
int version = Atom.parseFullAtomVersion(fullAtom);
|
int version = Atom.parseFullAtomVersion(fullAtom);
|
||||||
return version == 1 ? tfdt.readUnsignedLongToLong() : tfdt.readUnsignedInt();
|
return version == 1 ? tfdt.readUnsignedLongToLong() : tfdt.readUnsignedInt();
|
||||||
|
|
@ -536,7 +539,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||||
*/
|
*/
|
||||||
private static void parseTrun(Track track, DefaultSampleValues defaultSampleValues,
|
private static void parseTrun(Track track, DefaultSampleValues defaultSampleValues,
|
||||||
long decodeTime, int workaroundFlags, ParsableByteArray trun, TrackFragment out) {
|
long decodeTime, int workaroundFlags, ParsableByteArray trun, TrackFragment out) {
|
||||||
trun.setPosition(Atom.ATOM_HEADER_SIZE);
|
trun.setPosition(Atom.HEADER_SIZE);
|
||||||
int fullAtom = trun.readInt();
|
int fullAtom = trun.readInt();
|
||||||
int flags = Atom.parseFullAtomFlags(fullAtom);
|
int flags = Atom.parseFullAtomFlags(fullAtom);
|
||||||
|
|
||||||
|
|
@ -596,7 +599,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||||
|
|
||||||
private static void parseUuid(ParsableByteArray uuid, TrackFragment out,
|
private static void parseUuid(ParsableByteArray uuid, TrackFragment out,
|
||||||
byte[] extendedTypeScratch) {
|
byte[] extendedTypeScratch) {
|
||||||
uuid.setPosition(Atom.ATOM_HEADER_SIZE);
|
uuid.setPosition(Atom.HEADER_SIZE);
|
||||||
uuid.readBytes(extendedTypeScratch, 0, 16);
|
uuid.readBytes(extendedTypeScratch, 0, 16);
|
||||||
|
|
||||||
// Currently this parser only supports Microsoft's PIFF SampleEncryptionBox.
|
// Currently this parser only supports Microsoft's PIFF SampleEncryptionBox.
|
||||||
|
|
@ -615,7 +618,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void parseSenc(ParsableByteArray senc, int offset, TrackFragment out) {
|
private static void parseSenc(ParsableByteArray senc, int offset, TrackFragment out) {
|
||||||
senc.setPosition(Atom.ATOM_HEADER_SIZE + offset);
|
senc.setPosition(Atom.HEADER_SIZE + offset);
|
||||||
int fullAtom = senc.readInt();
|
int fullAtom = senc.readInt();
|
||||||
int flags = Atom.parseFullAtomFlags(fullAtom);
|
int flags = Atom.parseFullAtomFlags(fullAtom);
|
||||||
|
|
||||||
|
|
@ -639,7 +642,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||||
* Parses a sidx atom (defined in 14496-12).
|
* Parses a sidx atom (defined in 14496-12).
|
||||||
*/
|
*/
|
||||||
private static SegmentIndex parseSidx(ParsableByteArray atom) {
|
private static SegmentIndex parseSidx(ParsableByteArray atom) {
|
||||||
atom.setPosition(Atom.ATOM_HEADER_SIZE);
|
atom.setPosition(Atom.HEADER_SIZE);
|
||||||
int fullAtom = atom.readInt();
|
int fullAtom = atom.readInt();
|
||||||
int version = Atom.parseFullAtomVersion(fullAtom);
|
int version = Atom.parseFullAtomVersion(fullAtom);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,16 @@ public final class DefaultTrackOutput implements TrackOutput {
|
||||||
lastReadTimeUs = Long.MIN_VALUE;
|
lastReadTimeUs = Long.MIN_VALUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to skip to the keyframe before the specified time, if it's present in the buffer.
|
||||||
|
*
|
||||||
|
* @param timeUs The seek time.
|
||||||
|
* @return True if the skip was successful. False otherwise.
|
||||||
|
*/
|
||||||
|
public boolean skipToKeyframeBefore(long timeUs) {
|
||||||
|
return rollingBuffer.skipToKeyframeBefore(timeUs);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to configure a splice from this queue to the next.
|
* Attempts to configure a splice from this queue to the next.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -25,14 +25,20 @@ import java.io.IOException;
|
||||||
public interface Extractor {
|
public interface Extractor {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returned by {@link #read(ExtractorInput)} if the {@link ExtractorInput} passed to the next
|
* Returned by {@link #read(ExtractorInput, PositionHolder)} if the {@link ExtractorInput} passed
|
||||||
* {@link #read(ExtractorInput)} is required to provide data continuing from the position in the
|
* to the next {@link #read(ExtractorInput, PositionHolder)} is required to provide data
|
||||||
* stream reached by the returning call.
|
* continuing from the position in the stream reached by the returning call.
|
||||||
*/
|
*/
|
||||||
public static final int RESULT_CONTINUE = 0;
|
public static final int RESULT_CONTINUE = 0;
|
||||||
/**
|
/**
|
||||||
* Returned by {@link #read(ExtractorInput)} if the end of the {@link ExtractorInput} was reached.
|
* Returned by {@link #read(ExtractorInput, PositionHolder)} if the {@link ExtractorInput} passed
|
||||||
* Equal to {@link C#RESULT_END_OF_INPUT}.
|
* to the next {@link #read(ExtractorInput, PositionHolder)} is required to provide data starting
|
||||||
|
* from a specified position in the stream.
|
||||||
|
*/
|
||||||
|
public static final int RESULT_SEEK = 1;
|
||||||
|
/**
|
||||||
|
* Returned by {@link #read(ExtractorInput, PositionHolder)} if the end of the
|
||||||
|
* {@link ExtractorInput} was reached. Equal to {@link C#RESULT_END_OF_INPUT}.
|
||||||
*/
|
*/
|
||||||
public static final int RESULT_END_OF_INPUT = C.RESULT_END_OF_INPUT;
|
public static final int RESULT_END_OF_INPUT = C.RESULT_END_OF_INPUT;
|
||||||
|
|
||||||
|
|
@ -47,21 +53,31 @@ public interface Extractor {
|
||||||
* Extracts data read from a provided {@link ExtractorInput}.
|
* Extracts data read from a provided {@link ExtractorInput}.
|
||||||
* <p>
|
* <p>
|
||||||
* Each read will extract at most one sample from the stream before returning.
|
* Each read will extract at most one sample from the stream before returning.
|
||||||
|
* <p>
|
||||||
|
* In the common case, {@link #RESULT_CONTINUE} is returned to indicate that
|
||||||
|
* {@link ExtractorInput} passed to the next read is required to provide data continuing from the
|
||||||
|
* position in the stream reached by the returning call. If the extractor requires data to be
|
||||||
|
* provided from a different position, then that position is set in {@code seekPosition} and
|
||||||
|
* {@link #RESULT_SEEK} is returned. If the extractor reached the end of the data provided by the
|
||||||
|
* {@link ExtractorInput}, then {@link #RESULT_END_OF_INPUT} is returned.
|
||||||
*
|
*
|
||||||
* @param input The {@link ExtractorInput} from which data should be read.
|
* @param input The {@link ExtractorInput} from which data should be read.
|
||||||
|
* @param seekPosition If {@link #RESULT_SEEK} is returned, this holder is updated to hold the
|
||||||
|
* position of the required data.
|
||||||
* @return One of the {@code RESULT_} values defined in this interface.
|
* @return One of the {@code RESULT_} values defined in this interface.
|
||||||
* @throws IOException If an error occurred reading from the input.
|
* @throws IOException If an error occurred reading from the input.
|
||||||
* @throws InterruptedException If the thread was interrupted.
|
* @throws InterruptedException If the thread was interrupted.
|
||||||
*/
|
*/
|
||||||
int read(ExtractorInput input) throws IOException, InterruptedException;
|
int read(ExtractorInput input, PositionHolder seekPosition)
|
||||||
|
throws IOException, InterruptedException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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)} is required to provide data starting from any random access
|
* {@link #read(ExtractorInput, PositionHolder)} is required to provide data starting from any
|
||||||
* position in the stream. Random access positions can be obtained from a {@link SeekMap} that
|
* random access position in the stream. Random access positions can be obtained from a
|
||||||
* has been extracted and passed to the {@link ExtractorOutput}.
|
* {@link SeekMap} that has been extracted and passed to the {@link ExtractorOutput}.
|
||||||
*/
|
*/
|
||||||
void seek();
|
void seek();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,510 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2014 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer.extractor;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.C;
|
||||||
|
import com.google.android.exoplayer.MediaFormat;
|
||||||
|
import com.google.android.exoplayer.MediaFormatHolder;
|
||||||
|
import com.google.android.exoplayer.SampleHolder;
|
||||||
|
import com.google.android.exoplayer.SampleSource;
|
||||||
|
import com.google.android.exoplayer.TrackInfo;
|
||||||
|
import com.google.android.exoplayer.TrackRenderer;
|
||||||
|
import com.google.android.exoplayer.drm.DrmInitData;
|
||||||
|
import com.google.android.exoplayer.upstream.BufferPool;
|
||||||
|
import com.google.android.exoplayer.upstream.DataSource;
|
||||||
|
import com.google.android.exoplayer.upstream.DataSpec;
|
||||||
|
import com.google.android.exoplayer.upstream.Loader;
|
||||||
|
import com.google.android.exoplayer.upstream.Loader.Loadable;
|
||||||
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.SystemClock;
|
||||||
|
import android.util.SparseArray;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link SampleSource} that extracts sample data using an {@link Extractor}
|
||||||
|
*/
|
||||||
|
public class ExtractorSampleSource implements SampleSource, ExtractorOutput, Loader.Callback {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default minimum number of times to retry loading data prior to failing.
|
||||||
|
*/
|
||||||
|
public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3;
|
||||||
|
|
||||||
|
private static final int BUFFER_LENGTH = 256 * 1024;
|
||||||
|
|
||||||
|
private static final int NO_RESET_PENDING = -1;
|
||||||
|
|
||||||
|
private final Extractor extractor;
|
||||||
|
private final BufferPool bufferPool;
|
||||||
|
private final int requestedBufferSize;
|
||||||
|
private final SparseArray<DefaultTrackOutput> sampleQueues;
|
||||||
|
private final int minLoadableRetryCount;
|
||||||
|
private final boolean frameAccurateSeeking;
|
||||||
|
private final Uri uri;
|
||||||
|
private final DataSource dataSource;
|
||||||
|
|
||||||
|
private volatile boolean tracksBuilt;
|
||||||
|
private volatile SeekMap seekMap;
|
||||||
|
private volatile DrmInitData drmInitData;
|
||||||
|
|
||||||
|
private boolean prepared;
|
||||||
|
private int enabledTrackCount;
|
||||||
|
private TrackInfo[] trackInfos;
|
||||||
|
private boolean[] pendingMediaFormat;
|
||||||
|
private boolean[] pendingDiscontinuities;
|
||||||
|
private boolean[] trackEnabledStates;
|
||||||
|
|
||||||
|
private int remainingReleaseCount;
|
||||||
|
private long downstreamPositionUs;
|
||||||
|
private long lastSeekPositionUs;
|
||||||
|
private long pendingResetPositionUs;
|
||||||
|
|
||||||
|
private Loader loader;
|
||||||
|
private ExtractingLoadable loadable;
|
||||||
|
private IOException currentLoadableException;
|
||||||
|
private boolean currentLoadableExceptionFatal;
|
||||||
|
// TODO: Set this back to 0 in the correct place (some place indicative of making progress).
|
||||||
|
private int currentLoadableExceptionCount;
|
||||||
|
private long currentLoadableExceptionTimestamp;
|
||||||
|
private boolean loadingFinished;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param uri The {@link Uri} of the media stream.
|
||||||
|
* @param dataSource A data source to read the media stream.
|
||||||
|
* @param extractor An {@link Extractor} to extract the media stream.
|
||||||
|
* @param downstreamRendererCount Number of track renderers dependent on this sample source.
|
||||||
|
* @param requestedBufferSize The requested total buffer size for storing sample data, in bytes.
|
||||||
|
* The actual allocated size may exceed the value passed in if the implementation requires it.
|
||||||
|
*/
|
||||||
|
public ExtractorSampleSource(Uri uri, DataSource dataSource, Extractor extractor,
|
||||||
|
int downstreamRendererCount, int requestedBufferSize) {
|
||||||
|
this.uri = uri;
|
||||||
|
this.dataSource = dataSource;
|
||||||
|
this.extractor = extractor;
|
||||||
|
remainingReleaseCount = downstreamRendererCount;
|
||||||
|
this.requestedBufferSize = requestedBufferSize;
|
||||||
|
sampleQueues = new SparseArray<DefaultTrackOutput>();
|
||||||
|
bufferPool = new BufferPool(BUFFER_LENGTH);
|
||||||
|
minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT;
|
||||||
|
pendingResetPositionUs = NO_RESET_PENDING;
|
||||||
|
frameAccurateSeeking = true;
|
||||||
|
extractor.init(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean prepare() throws IOException {
|
||||||
|
if (prepared) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (loader == null) {
|
||||||
|
loader = new Loader("Loader:ExtractorSampleSource");
|
||||||
|
}
|
||||||
|
|
||||||
|
continueBufferingInternal();
|
||||||
|
|
||||||
|
// TODO: Support non-seekable content? Or at least avoid getting stuck here if a seekMap doesn't
|
||||||
|
// arrive (we may end up filling the sample buffers whilst we're still not prepared, and then
|
||||||
|
// getting stuck).
|
||||||
|
if (seekMap != null && tracksBuilt && haveFormatsForAllTracks()) {
|
||||||
|
int trackCount = sampleQueues.size();
|
||||||
|
trackEnabledStates = new boolean[trackCount];
|
||||||
|
pendingDiscontinuities = new boolean[trackCount];
|
||||||
|
pendingMediaFormat = new boolean[trackCount];
|
||||||
|
trackInfos = new TrackInfo[trackCount];
|
||||||
|
for (int i = 0; i < trackCount; i++) {
|
||||||
|
MediaFormat format = sampleQueues.valueAt(i).getFormat();
|
||||||
|
trackInfos[i] = new TrackInfo(format.mimeType, format.durationUs);
|
||||||
|
}
|
||||||
|
prepared = true;
|
||||||
|
if (isPendingReset()) {
|
||||||
|
restartFrom(pendingResetPositionUs);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
maybeThrowLoadableException();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getTrackCount() {
|
||||||
|
return sampleQueues.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TrackInfo getTrackInfo(int track) {
|
||||||
|
Assertions.checkState(prepared);
|
||||||
|
return trackInfos[track];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void enable(int track, long positionUs) {
|
||||||
|
Assertions.checkState(prepared);
|
||||||
|
Assertions.checkState(!trackEnabledStates[track]);
|
||||||
|
enabledTrackCount++;
|
||||||
|
trackEnabledStates[track] = true;
|
||||||
|
pendingMediaFormat[track] = true;
|
||||||
|
if (enabledTrackCount == 1) {
|
||||||
|
seekToUs(positionUs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disable(int track) {
|
||||||
|
Assertions.checkState(prepared);
|
||||||
|
Assertions.checkState(trackEnabledStates[track]);
|
||||||
|
enabledTrackCount--;
|
||||||
|
trackEnabledStates[track] = false;
|
||||||
|
pendingDiscontinuities[track] = false;
|
||||||
|
if (enabledTrackCount == 0) {
|
||||||
|
if (loader.isLoading()) {
|
||||||
|
loader.cancelLoading();
|
||||||
|
} else {
|
||||||
|
clearState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean continueBuffering(long playbackPositionUs) throws IOException {
|
||||||
|
Assertions.checkState(prepared);
|
||||||
|
Assertions.checkState(enabledTrackCount > 0);
|
||||||
|
downstreamPositionUs = playbackPositionUs;
|
||||||
|
discardSamplesForDisabledTracks(downstreamPositionUs);
|
||||||
|
return loadingFinished || continueBufferingInternal();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder,
|
||||||
|
SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException {
|
||||||
|
downstreamPositionUs = playbackPositionUs;
|
||||||
|
|
||||||
|
if (pendingDiscontinuities[track]) {
|
||||||
|
pendingDiscontinuities[track] = false;
|
||||||
|
return DISCONTINUITY_READ;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onlyReadDiscontinuity || isPendingReset()) {
|
||||||
|
maybeThrowLoadableException();
|
||||||
|
return NOTHING_READ;
|
||||||
|
}
|
||||||
|
|
||||||
|
DefaultTrackOutput sampleQueue = sampleQueues.valueAt(track);
|
||||||
|
if (pendingMediaFormat[track]) {
|
||||||
|
formatHolder.format = sampleQueue.getFormat();
|
||||||
|
formatHolder.drmInitData = drmInitData;
|
||||||
|
pendingMediaFormat[track] = false;
|
||||||
|
return FORMAT_READ;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sampleQueue.getSample(sampleHolder)) {
|
||||||
|
boolean decodeOnly = frameAccurateSeeking && sampleHolder.timeUs < lastSeekPositionUs;
|
||||||
|
sampleHolder.flags |= decodeOnly ? C.SAMPLE_FLAG_DECODE_ONLY : 0;
|
||||||
|
return SAMPLE_READ;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadingFinished) {
|
||||||
|
return END_OF_STREAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
maybeThrowLoadableException();
|
||||||
|
return NOTHING_READ;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void seekToUs(long positionUs) {
|
||||||
|
Assertions.checkState(prepared);
|
||||||
|
Assertions.checkState(enabledTrackCount > 0);
|
||||||
|
lastSeekPositionUs = positionUs;
|
||||||
|
if ((isPendingReset() ? pendingResetPositionUs : downstreamPositionUs) == positionUs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
downstreamPositionUs = positionUs;
|
||||||
|
|
||||||
|
// If we're not pending a reset, see if we can seek within the sample queues.
|
||||||
|
boolean seekInsideBuffer = !isPendingReset();
|
||||||
|
for (int i = 0; seekInsideBuffer && i < sampleQueues.size(); i++) {
|
||||||
|
seekInsideBuffer &= sampleQueues.valueAt(i).skipToKeyframeBefore(positionUs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we failed to seek within the sample queues, we need to restart.
|
||||||
|
if (!seekInsideBuffer) {
|
||||||
|
restartFrom(positionUs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Either way, we need to send discontinuities to the downstream components.
|
||||||
|
for (int i = 0; i < pendingDiscontinuities.length; i++) {
|
||||||
|
pendingDiscontinuities[i] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getBufferedPositionUs() {
|
||||||
|
if (loadingFinished) {
|
||||||
|
return TrackRenderer.END_OF_TRACK_US;
|
||||||
|
} else if (isPendingReset()) {
|
||||||
|
return pendingResetPositionUs;
|
||||||
|
} else {
|
||||||
|
long largestParsedTimestampUs = Long.MIN_VALUE;
|
||||||
|
for (int i = 0; i < sampleQueues.size(); i++) {
|
||||||
|
largestParsedTimestampUs = Math.max(largestParsedTimestampUs,
|
||||||
|
sampleQueues.valueAt(i).getLargestParsedTimestampUs());
|
||||||
|
}
|
||||||
|
return largestParsedTimestampUs == Long.MIN_VALUE ? downstreamPositionUs
|
||||||
|
: largestParsedTimestampUs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void release() {
|
||||||
|
Assertions.checkState(remainingReleaseCount > 0);
|
||||||
|
if (--remainingReleaseCount == 0) {
|
||||||
|
loader.release();
|
||||||
|
loader = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loader.Callback implementation.
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadCompleted(Loadable loadable) {
|
||||||
|
loadingFinished = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadCanceled(Loadable loadable) {
|
||||||
|
if (enabledTrackCount > 0) {
|
||||||
|
restartFrom(pendingResetPositionUs);
|
||||||
|
} else {
|
||||||
|
clearState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadError(Loadable loadable, IOException e) {
|
||||||
|
currentLoadableException = e;
|
||||||
|
currentLoadableExceptionCount++;
|
||||||
|
currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
|
||||||
|
maybeStartLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractorOutput implementation.
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TrackOutput track(int id) {
|
||||||
|
DefaultTrackOutput sampleQueue = sampleQueues.get(id);
|
||||||
|
if (sampleQueue == null) {
|
||||||
|
sampleQueue = new DefaultTrackOutput(bufferPool);
|
||||||
|
sampleQueues.put(id, sampleQueue);
|
||||||
|
}
|
||||||
|
return sampleQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void endTracks() {
|
||||||
|
tracksBuilt = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void seekMap(SeekMap seekMap) {
|
||||||
|
this.seekMap = seekMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void drmInitData(DrmInitData drmInitData) {
|
||||||
|
this.drmInitData = drmInitData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal stuff.
|
||||||
|
|
||||||
|
private boolean continueBufferingInternal() throws IOException {
|
||||||
|
maybeStartLoading();
|
||||||
|
if (isPendingReset()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
boolean haveSamples = prepared && haveSampleForOneEnabledTrack();
|
||||||
|
if (!haveSamples) {
|
||||||
|
maybeThrowLoadableException();
|
||||||
|
}
|
||||||
|
return haveSamples;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void restartFrom(long positionUs) {
|
||||||
|
pendingResetPositionUs = positionUs;
|
||||||
|
loadingFinished = false;
|
||||||
|
if (loader.isLoading()) {
|
||||||
|
loader.cancelLoading();
|
||||||
|
} else {
|
||||||
|
clearState();
|
||||||
|
maybeStartLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void maybeStartLoading() {
|
||||||
|
if (currentLoadableExceptionFatal || loadingFinished || loader.isLoading()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentLoadableException != null) {
|
||||||
|
Assertions.checkState(loadable != null);
|
||||||
|
long elapsedMillis = SystemClock.elapsedRealtime() - currentLoadableExceptionTimestamp;
|
||||||
|
if (elapsedMillis >= getRetryDelayMillis(currentLoadableExceptionCount)) {
|
||||||
|
currentLoadableException = null;
|
||||||
|
loader.startLoading(loadable, this);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!prepared) {
|
||||||
|
loadable = new ExtractingLoadable(uri, dataSource, extractor, bufferPool, requestedBufferSize,
|
||||||
|
0);
|
||||||
|
} else {
|
||||||
|
Assertions.checkState(isPendingReset());
|
||||||
|
loadable = new ExtractingLoadable(uri, dataSource, extractor, bufferPool, requestedBufferSize,
|
||||||
|
seekMap.getPosition(pendingResetPositionUs));
|
||||||
|
pendingResetPositionUs = NO_RESET_PENDING;
|
||||||
|
}
|
||||||
|
loader.startLoading(loadable, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void maybeThrowLoadableException() throws IOException {
|
||||||
|
if (currentLoadableException != null && (currentLoadableExceptionFatal
|
||||||
|
|| currentLoadableExceptionCount > minLoadableRetryCount)) {
|
||||||
|
throw currentLoadableException;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean haveFormatsForAllTracks() {
|
||||||
|
for (int i = 0; i < sampleQueues.size(); i++) {
|
||||||
|
if (!sampleQueues.valueAt(i).hasFormat()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean haveSampleForOneEnabledTrack() {
|
||||||
|
for (int i = 0; i < trackEnabledStates.length; i++) {
|
||||||
|
if (trackEnabledStates[i] && !sampleQueues.valueAt(i).isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void discardSamplesForDisabledTracks(long timeUs) {
|
||||||
|
for (int i = 0; i < trackEnabledStates.length; i++) {
|
||||||
|
if (!trackEnabledStates[i]) {
|
||||||
|
sampleQueues.valueAt(i).discardUntil(timeUs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearState() {
|
||||||
|
for (int i = 0; i < sampleQueues.size(); i++) {
|
||||||
|
sampleQueues.valueAt(i).clear();
|
||||||
|
}
|
||||||
|
loadable = null;
|
||||||
|
currentLoadableException = null;
|
||||||
|
currentLoadableExceptionCount = 0;
|
||||||
|
currentLoadableExceptionFatal = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isPendingReset() {
|
||||||
|
return pendingResetPositionUs != NO_RESET_PENDING;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getRetryDelayMillis(long errorCount) {
|
||||||
|
return Math.min((errorCount - 1) * 1000, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the media stream and extracts sample data from it.
|
||||||
|
*/
|
||||||
|
private static class ExtractingLoadable implements Loadable {
|
||||||
|
|
||||||
|
private final Uri uri;
|
||||||
|
private final DataSource dataSource;
|
||||||
|
private final Extractor extractor;
|
||||||
|
private final BufferPool bufferPool;
|
||||||
|
private final int bufferPoolSizeLimit;
|
||||||
|
private final PositionHolder positionHolder;
|
||||||
|
|
||||||
|
private volatile boolean loadCanceled;
|
||||||
|
|
||||||
|
private boolean pendingExtractorSeek;
|
||||||
|
|
||||||
|
public ExtractingLoadable(Uri uri, DataSource dataSource, Extractor extractor,
|
||||||
|
BufferPool bufferPool, int bufferPoolSizeLimit, long position) {
|
||||||
|
this.uri = Assertions.checkNotNull(uri);
|
||||||
|
this.dataSource = Assertions.checkNotNull(dataSource);
|
||||||
|
this.extractor = Assertions.checkNotNull(extractor);
|
||||||
|
this.bufferPool = Assertions.checkNotNull(bufferPool);
|
||||||
|
this.bufferPoolSizeLimit = bufferPoolSizeLimit;
|
||||||
|
positionHolder = new PositionHolder();
|
||||||
|
positionHolder.position = position;
|
||||||
|
pendingExtractorSeek = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cancelLoad() {
|
||||||
|
loadCanceled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isLoadCanceled() {
|
||||||
|
return loadCanceled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void load() throws IOException, InterruptedException {
|
||||||
|
if (pendingExtractorSeek) {
|
||||||
|
extractor.seek();
|
||||||
|
pendingExtractorSeek = false;
|
||||||
|
}
|
||||||
|
int result = Extractor.RESULT_CONTINUE;
|
||||||
|
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
|
||||||
|
ExtractorInput input = null;
|
||||||
|
try {
|
||||||
|
long position = positionHolder.position;
|
||||||
|
long length = dataSource.open(new DataSpec(uri, position, C.LENGTH_UNBOUNDED, null));
|
||||||
|
if (length != C.LENGTH_UNBOUNDED) {
|
||||||
|
length += position;
|
||||||
|
}
|
||||||
|
input = new DefaultExtractorInput(dataSource, position, length);
|
||||||
|
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
|
||||||
|
bufferPool.blockWhileAllocatedSizeExceeds(bufferPoolSizeLimit);
|
||||||
|
result = extractor.read(input, positionHolder);
|
||||||
|
// TODO: Implement throttling to stop us from buffering data too often.
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (result == Extractor.RESULT_SEEK) {
|
||||||
|
result = Extractor.RESULT_CONTINUE;
|
||||||
|
} else if (input != null) {
|
||||||
|
positionHolder.position = input.getPosition();
|
||||||
|
}
|
||||||
|
dataSource.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2014 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer.extractor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds a position in the stream.
|
||||||
|
*/
|
||||||
|
public final class PositionHolder {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The held position.
|
||||||
|
*/
|
||||||
|
public long position;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -111,6 +111,21 @@ import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
dropDownstreamTo(nextOffset);
|
dropDownstreamTo(nextOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to skip to the keyframe before the specified time, if it's present in the buffer.
|
||||||
|
*
|
||||||
|
* @param timeUs The seek time.
|
||||||
|
* @return True if the skip was successful. False otherwise.
|
||||||
|
*/
|
||||||
|
public boolean skipToKeyframeBefore(long timeUs) {
|
||||||
|
long nextOffset = infoQueue.skipToKeyframeBefore(timeUs);
|
||||||
|
if (nextOffset == -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
dropDownstreamTo(nextOffset);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads the current sample, advancing the read index to the next sample.
|
* Reads the current sample, advancing the read index to the next sample.
|
||||||
*
|
*
|
||||||
|
|
@ -471,6 +486,50 @@ import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
: (sizes[lastReadIndex] + offsets[lastReadIndex]);
|
: (sizes[lastReadIndex] + offsets[lastReadIndex]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to locate the keyframe before the specified time, if it's present in the buffer.
|
||||||
|
*
|
||||||
|
* @param timeUs The seek time.
|
||||||
|
* @return The offset of the keyframe's data if the keyframe was present. -1 otherwise.
|
||||||
|
*/
|
||||||
|
public synchronized long skipToKeyframeBefore(long timeUs) {
|
||||||
|
if (queueSize == 0 || timeUs < timesUs[relativeReadIndex]) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int lastWriteIndex = (relativeWriteIndex == 0 ? capacity : relativeWriteIndex) - 1;
|
||||||
|
long lastTimeUs = timesUs[lastWriteIndex];
|
||||||
|
if (timeUs > lastTimeUs) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: This can be optimized further using binary search, although the fact that the array
|
||||||
|
// is cyclic means we'd need to implement the binary search ourselves.
|
||||||
|
int sampleCount = 0;
|
||||||
|
int sampleCountToKeyframe = -1;
|
||||||
|
int searchIndex = relativeReadIndex;
|
||||||
|
while (searchIndex != relativeWriteIndex) {
|
||||||
|
if (timesUs[searchIndex] > timeUs) {
|
||||||
|
// We've gone too far.
|
||||||
|
break;
|
||||||
|
} else if ((flags[searchIndex] & C.SAMPLE_FLAG_SYNC) != 0) {
|
||||||
|
// We've found a keyframe, and we're still before the seek position.
|
||||||
|
sampleCountToKeyframe = sampleCount;
|
||||||
|
}
|
||||||
|
searchIndex = (searchIndex + 1) % capacity;
|
||||||
|
sampleCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sampleCountToKeyframe == -1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
queueSize -= sampleCountToKeyframe;
|
||||||
|
relativeReadIndex = (relativeReadIndex + sampleCountToKeyframe) % capacity;
|
||||||
|
absoluteReadIndex += sampleCountToKeyframe;
|
||||||
|
return offsets[relativeReadIndex];
|
||||||
|
}
|
||||||
|
|
||||||
// Called by the loading thread.
|
// Called by the loading thread.
|
||||||
|
|
||||||
public synchronized void commitSample(long timeUs, int sampleFlags, long offset, int size,
|
public synchronized void commitSample(long timeUs, int sampleFlags, long offset, int size,
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import com.google.android.exoplayer.ParserException;
|
||||||
import com.google.android.exoplayer.extractor.Extractor;
|
import com.google.android.exoplayer.extractor.Extractor;
|
||||||
import com.google.android.exoplayer.extractor.ExtractorInput;
|
import com.google.android.exoplayer.extractor.ExtractorInput;
|
||||||
import com.google.android.exoplayer.extractor.ExtractorOutput;
|
import com.google.android.exoplayer.extractor.ExtractorOutput;
|
||||||
|
import com.google.android.exoplayer.extractor.PositionHolder;
|
||||||
import com.google.android.exoplayer.extractor.SeekMap;
|
import com.google.android.exoplayer.extractor.SeekMap;
|
||||||
import com.google.android.exoplayer.extractor.TrackOutput;
|
import com.google.android.exoplayer.extractor.TrackOutput;
|
||||||
import com.google.android.exoplayer.util.MimeTypes;
|
import com.google.android.exoplayer.util.MimeTypes;
|
||||||
|
|
@ -111,7 +112,8 @@ public final class Mp3Extractor implements Extractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int read(ExtractorInput extractorInput) throws IOException, InterruptedException {
|
public int read(ExtractorInput extractorInput, PositionHolder seekPosition)
|
||||||
|
throws IOException, InterruptedException {
|
||||||
if (synchronizedHeaderData == 0
|
if (synchronizedHeaderData == 0
|
||||||
&& synchronizeCatchingEndOfInput(extractorInput) == RESULT_END_OF_INPUT) {
|
&& synchronizeCatchingEndOfInput(extractorInput) == RESULT_END_OF_INPUT) {
|
||||||
return RESULT_END_OF_INPUT;
|
return RESULT_END_OF_INPUT;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,190 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2014 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer.extractor.mp4;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.util.ParsableByteArray;
|
||||||
|
import com.google.android.exoplayer.util.Util;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public abstract class Atom {
|
||||||
|
|
||||||
|
/** Size of an atom header, in bytes. */
|
||||||
|
public static final int HEADER_SIZE = 8;
|
||||||
|
|
||||||
|
/** Size of a full atom header, in bytes. */
|
||||||
|
public static final int FULL_HEADER_SIZE = 12;
|
||||||
|
|
||||||
|
/** 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. */
|
||||||
|
public static final int LONG_SIZE_PREFIX = 1;
|
||||||
|
|
||||||
|
public static final int TYPE_ftyp = Util.getIntegerCodeForString("ftyp");
|
||||||
|
public static final int TYPE_avc1 = Util.getIntegerCodeForString("avc1");
|
||||||
|
public static final int TYPE_avc3 = Util.getIntegerCodeForString("avc3");
|
||||||
|
public static final int TYPE_esds = Util.getIntegerCodeForString("esds");
|
||||||
|
public static final int TYPE_mdat = Util.getIntegerCodeForString("mdat");
|
||||||
|
public static final int TYPE_mp4a = Util.getIntegerCodeForString("mp4a");
|
||||||
|
public static final int TYPE_ac_3 = Util.getIntegerCodeForString("ac-3");
|
||||||
|
public static final int TYPE_dac3 = Util.getIntegerCodeForString("dac3");
|
||||||
|
public static final int TYPE_ec_3 = Util.getIntegerCodeForString("ec-3");
|
||||||
|
public static final int TYPE_dec3 = Util.getIntegerCodeForString("dec3");
|
||||||
|
public static final int TYPE_tfdt = Util.getIntegerCodeForString("tfdt");
|
||||||
|
public static final int TYPE_tfhd = Util.getIntegerCodeForString("tfhd");
|
||||||
|
public static final int TYPE_trex = Util.getIntegerCodeForString("trex");
|
||||||
|
public static final int TYPE_trun = Util.getIntegerCodeForString("trun");
|
||||||
|
public static final int TYPE_sidx = Util.getIntegerCodeForString("sidx");
|
||||||
|
public static final int TYPE_moov = Util.getIntegerCodeForString("moov");
|
||||||
|
public static final int TYPE_mvhd = Util.getIntegerCodeForString("mvhd");
|
||||||
|
public static final int TYPE_trak = Util.getIntegerCodeForString("trak");
|
||||||
|
public static final int TYPE_mdia = Util.getIntegerCodeForString("mdia");
|
||||||
|
public static final int TYPE_minf = Util.getIntegerCodeForString("minf");
|
||||||
|
public static final int TYPE_stbl = Util.getIntegerCodeForString("stbl");
|
||||||
|
public static final int TYPE_avcC = Util.getIntegerCodeForString("avcC");
|
||||||
|
public static final int TYPE_moof = Util.getIntegerCodeForString("moof");
|
||||||
|
public static final int TYPE_traf = Util.getIntegerCodeForString("traf");
|
||||||
|
public static final int TYPE_mvex = Util.getIntegerCodeForString("mvex");
|
||||||
|
public static final int TYPE_tkhd = Util.getIntegerCodeForString("tkhd");
|
||||||
|
public static final int TYPE_mdhd = Util.getIntegerCodeForString("mdhd");
|
||||||
|
public static final int TYPE_hdlr = Util.getIntegerCodeForString("hdlr");
|
||||||
|
public static final int TYPE_stsd = Util.getIntegerCodeForString("stsd");
|
||||||
|
public static final int TYPE_pssh = Util.getIntegerCodeForString("pssh");
|
||||||
|
public static final int TYPE_sinf = Util.getIntegerCodeForString("sinf");
|
||||||
|
public static final int TYPE_schm = Util.getIntegerCodeForString("schm");
|
||||||
|
public static final int TYPE_schi = Util.getIntegerCodeForString("schi");
|
||||||
|
public static final int TYPE_tenc = Util.getIntegerCodeForString("tenc");
|
||||||
|
public static final int TYPE_encv = Util.getIntegerCodeForString("encv");
|
||||||
|
public static final int TYPE_enca = Util.getIntegerCodeForString("enca");
|
||||||
|
public static final int TYPE_frma = Util.getIntegerCodeForString("frma");
|
||||||
|
public static final int TYPE_saiz = Util.getIntegerCodeForString("saiz");
|
||||||
|
public static final int TYPE_uuid = Util.getIntegerCodeForString("uuid");
|
||||||
|
public static final int TYPE_senc = Util.getIntegerCodeForString("senc");
|
||||||
|
public static final int TYPE_pasp = Util.getIntegerCodeForString("pasp");
|
||||||
|
public static final int TYPE_TTML = Util.getIntegerCodeForString("TTML");
|
||||||
|
public static final int TYPE_vmhd = Util.getIntegerCodeForString("vmhd");
|
||||||
|
public static final int TYPE_smhd = Util.getIntegerCodeForString("smhd");
|
||||||
|
public static final int TYPE_mp4v = Util.getIntegerCodeForString("mp4v");
|
||||||
|
public static final int TYPE_stts = Util.getIntegerCodeForString("stts");
|
||||||
|
public static final int TYPE_stss = Util.getIntegerCodeForString("stss");
|
||||||
|
public static final int TYPE_ctts = Util.getIntegerCodeForString("ctts");
|
||||||
|
public static final int TYPE_stsc = Util.getIntegerCodeForString("stsc");
|
||||||
|
public static final int TYPE_stsz = Util.getIntegerCodeForString("stsz");
|
||||||
|
public static final int TYPE_stco = Util.getIntegerCodeForString("stco");
|
||||||
|
public static final int TYPE_co64 = Util.getIntegerCodeForString("co64");
|
||||||
|
|
||||||
|
public final int type;
|
||||||
|
|
||||||
|
Atom(int type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return getAtomTypeString(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An MP4 atom that is a leaf. */
|
||||||
|
public static final class LeafAtom extends Atom {
|
||||||
|
|
||||||
|
public final ParsableByteArray data;
|
||||||
|
|
||||||
|
public LeafAtom(int type, ParsableByteArray data) {
|
||||||
|
super(type);
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An MP4 atom that has child atoms. */
|
||||||
|
public static final class ContainerAtom extends Atom {
|
||||||
|
|
||||||
|
public final long endByteOffset;
|
||||||
|
public final List<LeafAtom> leafChildren;
|
||||||
|
public final List<ContainerAtom> containerChildren;
|
||||||
|
|
||||||
|
public ContainerAtom(int type, long endByteOffset) {
|
||||||
|
super(type);
|
||||||
|
|
||||||
|
leafChildren = new ArrayList<LeafAtom>();
|
||||||
|
containerChildren = new ArrayList<ContainerAtom>();
|
||||||
|
this.endByteOffset = endByteOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void add(LeafAtom atom) {
|
||||||
|
leafChildren.add(atom);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void add(ContainerAtom atom) {
|
||||||
|
containerChildren.add(atom);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LeafAtom getLeafAtomOfType(int type) {
|
||||||
|
int childrenSize = leafChildren.size();
|
||||||
|
for (int i = 0; i < childrenSize; i++) {
|
||||||
|
LeafAtom atom = leafChildren.get(i);
|
||||||
|
if (atom.type == type) {
|
||||||
|
return atom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContainerAtom getContainerAtomOfType(int type) {
|
||||||
|
int childrenSize = containerChildren.size();
|
||||||
|
for (int i = 0; i < childrenSize; i++) {
|
||||||
|
ContainerAtom atom = containerChildren.get(i);
|
||||||
|
if (atom.type == type) {
|
||||||
|
return atom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return getAtomTypeString(type)
|
||||||
|
+ " leaves: " + Arrays.toString(leafChildren.toArray(new LeafAtom[0]))
|
||||||
|
+ " containers: " + Arrays.toString(containerChildren.toArray(new ContainerAtom[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the version number out of the additional integer component of a full atom.
|
||||||
|
*/
|
||||||
|
public static int parseFullAtomVersion(int fullAtomInt) {
|
||||||
|
return 0x000000FF & (fullAtomInt >> 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the atom flags out of the additional integer component of a full atom.
|
||||||
|
*/
|
||||||
|
public static int parseFullAtomFlags(int fullAtomInt) {
|
||||||
|
return 0x00FFFFFF & fullAtomInt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getAtomTypeString(int type) {
|
||||||
|
return "" + (char) (type >> 24)
|
||||||
|
+ (char) ((type >> 16) & 0xFF)
|
||||||
|
+ (char) ((type >> 8) & 0xFF)
|
||||||
|
+ (char) (type & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -13,11 +13,10 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.mp4;
|
package com.google.android.exoplayer.extractor.mp4;
|
||||||
|
|
||||||
import com.google.android.exoplayer.C;
|
import com.google.android.exoplayer.C;
|
||||||
import com.google.android.exoplayer.MediaFormat;
|
import com.google.android.exoplayer.MediaFormat;
|
||||||
import com.google.android.exoplayer.chunk.parser.mp4.TrackEncryptionBox;
|
|
||||||
import com.google.android.exoplayer.util.Assertions;
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
|
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
|
||||||
import com.google.android.exoplayer.util.H264Util;
|
import com.google.android.exoplayer.util.H264Util;
|
||||||
|
|
@ -32,7 +31,7 @@ import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** Utility methods for parsing MP4 format atom payloads according to ISO 14496-12. */
|
/** Utility methods for parsing MP4 format atom payloads according to ISO 14496-12. */
|
||||||
public final class CommonMp4AtomParsers {
|
public final class AtomParsers {
|
||||||
|
|
||||||
/** Channel counts for AC-3 audio, indexed by acmod. (See ETSI TS 102 366.) */
|
/** Channel counts for AC-3 audio, indexed by acmod. (See ETSI TS 102 366.) */
|
||||||
private static final int[] AC3_CHANNEL_COUNTS = new int[] {2, 1, 2, 3, 3, 4, 4, 5};
|
private static final int[] AC3_CHANNEL_COUNTS = new int[] {2, 1, 2, 3, 3, 4, 4, 5};
|
||||||
|
|
@ -41,17 +40,19 @@ public final class CommonMp4AtomParsers {
|
||||||
192, 224, 256, 320, 384, 448, 512, 576, 640};
|
192, 224, 256, 320, 384, 448, 512, 576, 640};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a trak atom (defined in 14496-12)
|
* Parses a trak atom (defined in 14496-12).
|
||||||
*
|
*
|
||||||
* @param trak Atom to parse.
|
* @param trak Atom to parse.
|
||||||
* @param mvhd Movie header atom, used to get the timescale.
|
* @param mvhd Movie header atom, used to get the timescale.
|
||||||
* @return A {@link Track} instance.
|
* @return A {@link Track} instance, or {@code null} if the track's type isn't supported.
|
||||||
*/
|
*/
|
||||||
public static Track parseTrak(Atom.ContainerAtom trak, Atom.LeafAtom mvhd) {
|
public static Track parseTrak(Atom.ContainerAtom trak, Atom.LeafAtom mvhd) {
|
||||||
Atom.ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia);
|
Atom.ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia);
|
||||||
int trackType = parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data);
|
int trackType = parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data);
|
||||||
Assertions.checkState(trackType == Track.TYPE_AUDIO || trackType == Track.TYPE_VIDEO
|
if (trackType != Track.TYPE_AUDIO && trackType != Track.TYPE_VIDEO
|
||||||
|| trackType == Track.TYPE_TEXT || trackType == Track.TYPE_TIME_CODE);
|
&& trackType != Track.TYPE_TEXT && trackType != Track.TYPE_TIME_CODE) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
Pair<Integer, Long> header = parseTkhd(trak.getLeafAtomOfType(Atom.TYPE_tkhd).data);
|
Pair<Integer, Long> header = parseTkhd(trak.getLeafAtomOfType(Atom.TYPE_tkhd).data);
|
||||||
int id = header.first;
|
int id = header.first;
|
||||||
|
|
@ -80,7 +81,7 @@ public final class CommonMp4AtomParsers {
|
||||||
* @param stblAtom stbl (sample table) atom to parse.
|
* @param stblAtom stbl (sample table) atom to parse.
|
||||||
* @return Sample table described by the stbl atom.
|
* @return Sample table described by the stbl atom.
|
||||||
*/
|
*/
|
||||||
public static Mp4TrackSampleTable parseStbl(Track track, Atom.ContainerAtom stblAtom) {
|
public static TrackSampleTable parseStbl(Track track, Atom.ContainerAtom stblAtom) {
|
||||||
// Array of sample sizes.
|
// Array of sample sizes.
|
||||||
ParsableByteArray stsz = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz).data;
|
ParsableByteArray stsz = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz).data;
|
||||||
|
|
||||||
|
|
@ -103,7 +104,7 @@ public final class CommonMp4AtomParsers {
|
||||||
ParsableByteArray ctts = cttsAtom != null ? cttsAtom.data : null;
|
ParsableByteArray ctts = cttsAtom != null ? cttsAtom.data : null;
|
||||||
|
|
||||||
// Skip full atom.
|
// Skip full atom.
|
||||||
stsz.setPosition(Atom.FULL_ATOM_HEADER_SIZE);
|
stsz.setPosition(Atom.FULL_HEADER_SIZE);
|
||||||
int fixedSampleSize = stsz.readUnsignedIntToInt();
|
int fixedSampleSize = stsz.readUnsignedIntToInt();
|
||||||
int sampleCount = stsz.readUnsignedIntToInt();
|
int sampleCount = stsz.readUnsignedIntToInt();
|
||||||
|
|
||||||
|
|
@ -113,10 +114,10 @@ public final class CommonMp4AtomParsers {
|
||||||
int[] flags = new int[sampleCount];
|
int[] flags = new int[sampleCount];
|
||||||
|
|
||||||
// Prepare to read chunk offsets.
|
// Prepare to read chunk offsets.
|
||||||
chunkOffsets.setPosition(Atom.FULL_ATOM_HEADER_SIZE);
|
chunkOffsets.setPosition(Atom.FULL_HEADER_SIZE);
|
||||||
int chunkCount = chunkOffsets.readUnsignedIntToInt();
|
int chunkCount = chunkOffsets.readUnsignedIntToInt();
|
||||||
|
|
||||||
stsc.setPosition(Atom.FULL_ATOM_HEADER_SIZE);
|
stsc.setPosition(Atom.FULL_HEADER_SIZE);
|
||||||
int remainingSamplesPerChunkChanges = stsc.readUnsignedIntToInt() - 1;
|
int remainingSamplesPerChunkChanges = stsc.readUnsignedIntToInt() - 1;
|
||||||
Assertions.checkState(stsc.readInt() == 1, "stsc first chunk must be 1");
|
Assertions.checkState(stsc.readInt() == 1, "stsc first chunk must be 1");
|
||||||
int samplesPerChunk = stsc.readUnsignedIntToInt();
|
int samplesPerChunk = stsc.readUnsignedIntToInt();
|
||||||
|
|
@ -131,28 +132,31 @@ public final class CommonMp4AtomParsers {
|
||||||
int remainingSamplesInChunk = samplesPerChunk;
|
int remainingSamplesInChunk = samplesPerChunk;
|
||||||
|
|
||||||
// Prepare to read sample timestamps.
|
// Prepare to read sample timestamps.
|
||||||
stts.setPosition(Atom.FULL_ATOM_HEADER_SIZE);
|
stts.setPosition(Atom.FULL_HEADER_SIZE);
|
||||||
int remainingTimestampDeltaChanges = stts.readUnsignedIntToInt() - 1;
|
int remainingTimestampDeltaChanges = stts.readUnsignedIntToInt() - 1;
|
||||||
int remainingSamplesAtTimestampDelta = stts.readUnsignedIntToInt();
|
int remainingSamplesAtTimestampDelta = stts.readUnsignedIntToInt();
|
||||||
int timestampDeltaInTimeUnits = stts.readUnsignedIntToInt();
|
int timestampDeltaInTimeUnits = stts.readUnsignedIntToInt();
|
||||||
|
|
||||||
// Prepare to read sample timestamp offsets, if ctts is present.
|
// Prepare to read sample timestamp offsets, if ctts is present.
|
||||||
boolean cttsHasSignedOffsets = false;
|
|
||||||
int remainingSamplesAtTimestampOffset = 0;
|
int remainingSamplesAtTimestampOffset = 0;
|
||||||
int remainingTimestampOffsetChanges = 0;
|
int remainingTimestampOffsetChanges = 0;
|
||||||
int timestampOffset = 0;
|
int timestampOffset = 0;
|
||||||
if (ctts != null) {
|
if (ctts != null) {
|
||||||
ctts.setPosition(Atom.ATOM_HEADER_SIZE);
|
ctts.setPosition(Atom.FULL_HEADER_SIZE);
|
||||||
cttsHasSignedOffsets = Atom.parseFullAtomVersion(ctts.readInt()) == 1;
|
|
||||||
remainingTimestampOffsetChanges = ctts.readUnsignedIntToInt() - 1;
|
remainingTimestampOffsetChanges = ctts.readUnsignedIntToInt() - 1;
|
||||||
remainingSamplesAtTimestampOffset = ctts.readUnsignedIntToInt();
|
remainingSamplesAtTimestampOffset = ctts.readUnsignedIntToInt();
|
||||||
timestampOffset = cttsHasSignedOffsets ? ctts.readInt() : ctts.readUnsignedIntToInt();
|
// The BMFF spec (ISO 14496-12) states that sample offsets should be unsigned integers in
|
||||||
|
// version 0 ctts boxes, however some streams violate the spec and use signed integers
|
||||||
|
// instead. It's safe to always parse sample offsets as signed integers here, because
|
||||||
|
// unsigned integers will still be parsed correctly (unless their top bit is set, which
|
||||||
|
// is never true in practice because sample offsets are always small).
|
||||||
|
timestampOffset = ctts.readInt();
|
||||||
}
|
}
|
||||||
|
|
||||||
int nextSynchronizationSampleIndex = -1;
|
int nextSynchronizationSampleIndex = -1;
|
||||||
int remainingSynchronizationSamples = 0;
|
int remainingSynchronizationSamples = 0;
|
||||||
if (stss != null) {
|
if (stss != null) {
|
||||||
stss.setPosition(Atom.FULL_ATOM_HEADER_SIZE);
|
stss.setPosition(Atom.FULL_HEADER_SIZE);
|
||||||
remainingSynchronizationSamples = stss.readUnsignedIntToInt();
|
remainingSynchronizationSamples = stss.readUnsignedIntToInt();
|
||||||
nextSynchronizationSampleIndex = stss.readUnsignedIntToInt() - 1;
|
nextSynchronizationSampleIndex = stss.readUnsignedIntToInt() - 1;
|
||||||
}
|
}
|
||||||
|
|
@ -195,7 +199,8 @@ public final class CommonMp4AtomParsers {
|
||||||
remainingSamplesAtTimestampOffset--;
|
remainingSamplesAtTimestampOffset--;
|
||||||
if (remainingSamplesAtTimestampOffset == 0 && remainingTimestampOffsetChanges > 0) {
|
if (remainingSamplesAtTimestampOffset == 0 && remainingTimestampOffsetChanges > 0) {
|
||||||
remainingSamplesAtTimestampOffset = ctts.readUnsignedIntToInt();
|
remainingSamplesAtTimestampOffset = ctts.readUnsignedIntToInt();
|
||||||
timestampOffset = cttsHasSignedOffsets ? ctts.readInt() : ctts.readUnsignedIntToInt();
|
// Read a signed offset even for version 0 ctts boxes (see comment above).
|
||||||
|
timestampOffset = ctts.readInt();
|
||||||
remainingTimestampOffsetChanges--;
|
remainingTimestampOffsetChanges--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -240,7 +245,7 @@ public final class CommonMp4AtomParsers {
|
||||||
Assertions.checkArgument(remainingSamplesInChunk == 0);
|
Assertions.checkArgument(remainingSamplesInChunk == 0);
|
||||||
Assertions.checkArgument(remainingTimestampDeltaChanges == 0);
|
Assertions.checkArgument(remainingTimestampDeltaChanges == 0);
|
||||||
Assertions.checkArgument(remainingTimestampOffsetChanges == 0);
|
Assertions.checkArgument(remainingTimestampOffsetChanges == 0);
|
||||||
return new Mp4TrackSampleTable(offsets, sizes, timestamps, flags);
|
return new TrackSampleTable(offsets, sizes, timestamps, flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -250,7 +255,7 @@ public final class CommonMp4AtomParsers {
|
||||||
* @return Timescale for the movie.
|
* @return Timescale for the movie.
|
||||||
*/
|
*/
|
||||||
private static long parseMvhd(ParsableByteArray mvhd) {
|
private static long parseMvhd(ParsableByteArray mvhd) {
|
||||||
mvhd.setPosition(Atom.ATOM_HEADER_SIZE);
|
mvhd.setPosition(Atom.HEADER_SIZE);
|
||||||
|
|
||||||
int fullAtom = mvhd.readInt();
|
int fullAtom = mvhd.readInt();
|
||||||
int version = Atom.parseFullAtomVersion(fullAtom);
|
int version = Atom.parseFullAtomVersion(fullAtom);
|
||||||
|
|
@ -267,7 +272,7 @@ public final class CommonMp4AtomParsers {
|
||||||
* the movie header box). The duration is set to -1 if the duration is unspecified.
|
* the movie header box). The duration is set to -1 if the duration is unspecified.
|
||||||
*/
|
*/
|
||||||
private static Pair<Integer, Long> parseTkhd(ParsableByteArray tkhd) {
|
private static Pair<Integer, Long> parseTkhd(ParsableByteArray tkhd) {
|
||||||
tkhd.setPosition(Atom.ATOM_HEADER_SIZE);
|
tkhd.setPosition(Atom.HEADER_SIZE);
|
||||||
int fullAtom = tkhd.readInt();
|
int fullAtom = tkhd.readInt();
|
||||||
int version = Atom.parseFullAtomVersion(fullAtom);
|
int version = Atom.parseFullAtomVersion(fullAtom);
|
||||||
|
|
||||||
|
|
@ -303,7 +308,7 @@ public final class CommonMp4AtomParsers {
|
||||||
* @return The track type.
|
* @return The track type.
|
||||||
*/
|
*/
|
||||||
private static int parseHdlr(ParsableByteArray hdlr) {
|
private static int parseHdlr(ParsableByteArray hdlr) {
|
||||||
hdlr.setPosition(Atom.FULL_ATOM_HEADER_SIZE + 4);
|
hdlr.setPosition(Atom.FULL_HEADER_SIZE + 4);
|
||||||
return hdlr.readInt();
|
return hdlr.readInt();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -314,7 +319,7 @@ public final class CommonMp4AtomParsers {
|
||||||
* @return The media timescale, defined as the number of time units that pass in one second.
|
* @return The media timescale, defined as the number of time units that pass in one second.
|
||||||
*/
|
*/
|
||||||
private static long parseMdhd(ParsableByteArray mdhd) {
|
private static long parseMdhd(ParsableByteArray mdhd) {
|
||||||
mdhd.setPosition(Atom.ATOM_HEADER_SIZE);
|
mdhd.setPosition(Atom.HEADER_SIZE);
|
||||||
int fullAtom = mdhd.readInt();
|
int fullAtom = mdhd.readInt();
|
||||||
int version = Atom.parseFullAtomVersion(fullAtom);
|
int version = Atom.parseFullAtomVersion(fullAtom);
|
||||||
|
|
||||||
|
|
@ -324,7 +329,7 @@ public final class CommonMp4AtomParsers {
|
||||||
|
|
||||||
private static Pair<MediaFormat, TrackEncryptionBox[]> parseStsd(
|
private static Pair<MediaFormat, TrackEncryptionBox[]> parseStsd(
|
||||||
ParsableByteArray stsd, long durationUs) {
|
ParsableByteArray stsd, long durationUs) {
|
||||||
stsd.setPosition(Atom.FULL_ATOM_HEADER_SIZE);
|
stsd.setPosition(Atom.FULL_HEADER_SIZE);
|
||||||
int numberOfEntries = stsd.readInt();
|
int numberOfEntries = stsd.readInt();
|
||||||
MediaFormat mediaFormat = null;
|
MediaFormat mediaFormat = null;
|
||||||
TrackEncryptionBox[] trackEncryptionBoxes = new TrackEncryptionBox[numberOfEntries];
|
TrackEncryptionBox[] trackEncryptionBoxes = new TrackEncryptionBox[numberOfEntries];
|
||||||
|
|
@ -358,7 +363,7 @@ public final class CommonMp4AtomParsers {
|
||||||
/** Returns the media format for an avc1 box. */
|
/** Returns the media format for an avc1 box. */
|
||||||
private static Pair<MediaFormat, TrackEncryptionBox> parseAvcFromParent(ParsableByteArray parent,
|
private static Pair<MediaFormat, TrackEncryptionBox> parseAvcFromParent(ParsableByteArray parent,
|
||||||
int position, int size, long durationUs) {
|
int position, int size, long durationUs) {
|
||||||
parent.setPosition(position + Atom.ATOM_HEADER_SIZE);
|
parent.setPosition(position + Atom.HEADER_SIZE);
|
||||||
|
|
||||||
parent.skip(24);
|
parent.skip(24);
|
||||||
int width = parent.readUnsignedShort();
|
int width = parent.readUnsignedShort();
|
||||||
|
|
@ -395,7 +400,7 @@ public final class CommonMp4AtomParsers {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<byte[]> parseAvcCFromParent(ParsableByteArray parent, int position) {
|
private static List<byte[]> parseAvcCFromParent(ParsableByteArray parent, int position) {
|
||||||
parent.setPosition(position + Atom.ATOM_HEADER_SIZE + 4);
|
parent.setPosition(position + Atom.HEADER_SIZE + 4);
|
||||||
// Start of the AVCDecoderConfigurationRecord (defined in 14496-15)
|
// Start of the AVCDecoderConfigurationRecord (defined in 14496-15)
|
||||||
int nalUnitLength = (parent.readUnsignedByte() & 0x3) + 1;
|
int nalUnitLength = (parent.readUnsignedByte() & 0x3) + 1;
|
||||||
if (nalUnitLength != 4) {
|
if (nalUnitLength != 4) {
|
||||||
|
|
@ -419,7 +424,7 @@ public final class CommonMp4AtomParsers {
|
||||||
|
|
||||||
private static TrackEncryptionBox parseSinfFromParent(ParsableByteArray parent, int position,
|
private static TrackEncryptionBox parseSinfFromParent(ParsableByteArray parent, int position,
|
||||||
int size) {
|
int size) {
|
||||||
int childPosition = position + Atom.ATOM_HEADER_SIZE;
|
int childPosition = position + Atom.HEADER_SIZE;
|
||||||
|
|
||||||
TrackEncryptionBox trackEncryptionBox = null;
|
TrackEncryptionBox trackEncryptionBox = null;
|
||||||
while (childPosition - position < size) {
|
while (childPosition - position < size) {
|
||||||
|
|
@ -442,7 +447,7 @@ public final class CommonMp4AtomParsers {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static float parsePaspFromParent(ParsableByteArray parent, int position) {
|
private static float parsePaspFromParent(ParsableByteArray parent, int position) {
|
||||||
parent.setPosition(position + Atom.ATOM_HEADER_SIZE);
|
parent.setPosition(position + Atom.HEADER_SIZE);
|
||||||
int hSpacing = parent.readUnsignedIntToInt();
|
int hSpacing = parent.readUnsignedIntToInt();
|
||||||
int vSpacing = parent.readUnsignedIntToInt();
|
int vSpacing = parent.readUnsignedIntToInt();
|
||||||
return (float) hSpacing / vSpacing;
|
return (float) hSpacing / vSpacing;
|
||||||
|
|
@ -450,7 +455,7 @@ public final class CommonMp4AtomParsers {
|
||||||
|
|
||||||
private static TrackEncryptionBox parseSchiFromParent(ParsableByteArray parent, int position,
|
private static TrackEncryptionBox parseSchiFromParent(ParsableByteArray parent, int position,
|
||||||
int size) {
|
int size) {
|
||||||
int childPosition = position + Atom.ATOM_HEADER_SIZE;
|
int childPosition = position + Atom.HEADER_SIZE;
|
||||||
while (childPosition - position < size) {
|
while (childPosition - position < size) {
|
||||||
parent.setPosition(childPosition);
|
parent.setPosition(childPosition);
|
||||||
int childAtomSize = parent.readInt();
|
int childAtomSize = parent.readInt();
|
||||||
|
|
@ -472,7 +477,7 @@ public final class CommonMp4AtomParsers {
|
||||||
/** Returns the media format for an mp4v box. */
|
/** Returns the media format for an mp4v box. */
|
||||||
private static MediaFormat parseMp4vFromParent(ParsableByteArray parent, int position, int size,
|
private static MediaFormat parseMp4vFromParent(ParsableByteArray parent, int position, int size,
|
||||||
long durationUs) {
|
long durationUs) {
|
||||||
parent.setPosition(position + Atom.ATOM_HEADER_SIZE);
|
parent.setPosition(position + Atom.HEADER_SIZE);
|
||||||
|
|
||||||
parent.skip(24);
|
parent.skip(24);
|
||||||
int width = parent.readUnsignedShort();
|
int width = parent.readUnsignedShort();
|
||||||
|
|
@ -499,7 +504,7 @@ public final class CommonMp4AtomParsers {
|
||||||
|
|
||||||
private static Pair<MediaFormat, TrackEncryptionBox> parseAudioSampleEntry(
|
private static Pair<MediaFormat, TrackEncryptionBox> parseAudioSampleEntry(
|
||||||
ParsableByteArray parent, int atomType, int position, int size, long durationUs) {
|
ParsableByteArray parent, int atomType, int position, int size, long durationUs) {
|
||||||
parent.setPosition(position + Atom.ATOM_HEADER_SIZE);
|
parent.setPosition(position + Atom.HEADER_SIZE);
|
||||||
parent.skip(16);
|
parent.skip(16);
|
||||||
int channelCount = parent.readUnsignedShort();
|
int channelCount = parent.readUnsignedShort();
|
||||||
int sampleSize = parent.readUnsignedShort();
|
int sampleSize = parent.readUnsignedShort();
|
||||||
|
|
@ -564,7 +569,7 @@ public final class CommonMp4AtomParsers {
|
||||||
|
|
||||||
/** Returns codec-specific initialization data contained in an esds box. */
|
/** Returns codec-specific initialization data contained in an esds box. */
|
||||||
private static byte[] parseEsdsFromParent(ParsableByteArray parent, int position) {
|
private static byte[] parseEsdsFromParent(ParsableByteArray parent, int position) {
|
||||||
parent.setPosition(position + Atom.ATOM_HEADER_SIZE + 4);
|
parent.setPosition(position + Atom.HEADER_SIZE + 4);
|
||||||
// Start of the ES_Descriptor (defined in 14496-1)
|
// Start of the ES_Descriptor (defined in 14496-1)
|
||||||
parent.skip(1); // ES_Descriptor tag
|
parent.skip(1); // ES_Descriptor tag
|
||||||
int varIntByte = parent.readUnsignedByte();
|
int varIntByte = parent.readUnsignedByte();
|
||||||
|
|
@ -608,7 +613,7 @@ public final class CommonMp4AtomParsers {
|
||||||
|
|
||||||
private static Ac3Format parseAc3SpecificBoxFromParent(ParsableByteArray parent, int position) {
|
private static Ac3Format parseAc3SpecificBoxFromParent(ParsableByteArray parent, int position) {
|
||||||
// Start of the dac3 atom (defined in ETSI TS 102 366)
|
// Start of the dac3 atom (defined in ETSI TS 102 366)
|
||||||
parent.setPosition(position + Atom.ATOM_HEADER_SIZE);
|
parent.setPosition(position + Atom.HEADER_SIZE);
|
||||||
|
|
||||||
// fscod (sample rate code)
|
// fscod (sample rate code)
|
||||||
int fscod = (parent.readUnsignedByte() & 0xC0) >> 6;
|
int fscod = (parent.readUnsignedByte() & 0xC0) >> 6;
|
||||||
|
|
@ -646,12 +651,12 @@ public final class CommonMp4AtomParsers {
|
||||||
|
|
||||||
private static int parseEc3SpecificBoxFromParent(ParsableByteArray parent, int position) {
|
private static int parseEc3SpecificBoxFromParent(ParsableByteArray parent, int position) {
|
||||||
// Start of the dec3 atom (defined in ETSI TS 102 366)
|
// Start of the dec3 atom (defined in ETSI TS 102 366)
|
||||||
parent.setPosition(position + Atom.ATOM_HEADER_SIZE);
|
parent.setPosition(position + Atom.HEADER_SIZE);
|
||||||
// TODO: Implement parsing for enhanced AC-3 with multiple sub-streams.
|
// TODO: Implement parsing for enhanced AC-3 with multiple sub-streams.
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private CommonMp4AtomParsers() {
|
private AtomParsers() {
|
||||||
// Prevent instantiation.
|
// Prevent instantiation.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -13,9 +13,10 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.chunk.parser.mp4;
|
package com.google.android.exoplayer.extractor.mp4;
|
||||||
|
|
||||||
/* package */ final class DefaultSampleValues {
|
// TODO: Make package private.
|
||||||
|
public final class DefaultSampleValues {
|
||||||
|
|
||||||
public final int sampleDescriptionIndex;
|
public final int sampleDescriptionIndex;
|
||||||
public final int duration;
|
public final int duration;
|
||||||
|
|
@ -0,0 +1,700 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2014 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer.extractor.mp4;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.C;
|
||||||
|
import com.google.android.exoplayer.drm.DrmInitData;
|
||||||
|
import com.google.android.exoplayer.extractor.ChunkIndex;
|
||||||
|
import com.google.android.exoplayer.extractor.Extractor;
|
||||||
|
import com.google.android.exoplayer.extractor.ExtractorInput;
|
||||||
|
import com.google.android.exoplayer.extractor.ExtractorOutput;
|
||||||
|
import com.google.android.exoplayer.extractor.PositionHolder;
|
||||||
|
import com.google.android.exoplayer.extractor.TrackOutput;
|
||||||
|
import com.google.android.exoplayer.extractor.mp4.Atom.ContainerAtom;
|
||||||
|
import com.google.android.exoplayer.extractor.mp4.Atom.LeafAtom;
|
||||||
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
|
import com.google.android.exoplayer.util.H264Util;
|
||||||
|
import com.google.android.exoplayer.util.MimeTypes;
|
||||||
|
import com.google.android.exoplayer.util.ParsableByteArray;
|
||||||
|
import com.google.android.exoplayer.util.Util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Stack;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Facilitates the extraction of data from the fragmented mp4 container format.
|
||||||
|
* <p>
|
||||||
|
* This implementation only supports de-muxed (i.e. single track) streams.
|
||||||
|
*/
|
||||||
|
public final class FragmentedMp4Extractor implements Extractor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag to work around an issue in some video streams where every frame is marked as a sync frame.
|
||||||
|
* The workaround overrides the sync frame flags in the stream, forcing them to false except for
|
||||||
|
* the first sample in each segment.
|
||||||
|
* <p>
|
||||||
|
* This flag does nothing if the stream is not a video stream.
|
||||||
|
*/
|
||||||
|
public static final int WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME = 1;
|
||||||
|
|
||||||
|
private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE =
|
||||||
|
new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12};
|
||||||
|
|
||||||
|
// 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_ENCRYPTION_DATA = 2;
|
||||||
|
private static final int STATE_READING_SAMPLE_START = 3;
|
||||||
|
private static final int STATE_READING_SAMPLE_CONTINUE = 4;
|
||||||
|
|
||||||
|
private final int workaroundFlags;
|
||||||
|
|
||||||
|
// Temporary arrays.
|
||||||
|
private final ParsableByteArray nalStartCode;
|
||||||
|
private final ParsableByteArray nalLength;
|
||||||
|
private final ParsableByteArray encryptionSignalByte;
|
||||||
|
|
||||||
|
// Parser state
|
||||||
|
private final ParsableByteArray atomHeader;
|
||||||
|
private final byte[] extendedTypeScratch;
|
||||||
|
private final Stack<ContainerAtom> containerAtoms;
|
||||||
|
private final TrackFragment fragmentRun;
|
||||||
|
|
||||||
|
private int parserState;
|
||||||
|
private int rootAtomBytesRead;
|
||||||
|
private int atomType;
|
||||||
|
private int atomSize;
|
||||||
|
private ParsableByteArray atomData;
|
||||||
|
|
||||||
|
private int sampleIndex;
|
||||||
|
private int sampleSize;
|
||||||
|
private int sampleBytesWritten;
|
||||||
|
private int sampleCurrentNalBytesRemaining;
|
||||||
|
|
||||||
|
// Data parsed from moov atom.
|
||||||
|
private Track track;
|
||||||
|
private DefaultSampleValues extendsDefaults;
|
||||||
|
|
||||||
|
// Extractor outputs.
|
||||||
|
private ExtractorOutput extractorOutput;
|
||||||
|
private TrackOutput trackOutput;
|
||||||
|
|
||||||
|
public FragmentedMp4Extractor() {
|
||||||
|
this(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param workaroundFlags Flags to allow parsing of faulty streams.
|
||||||
|
* {@link #WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME} is currently the only flag defined.
|
||||||
|
*/
|
||||||
|
public FragmentedMp4Extractor(int workaroundFlags) {
|
||||||
|
this.workaroundFlags = workaroundFlags;
|
||||||
|
atomHeader = new ParsableByteArray(Atom.HEADER_SIZE);
|
||||||
|
nalStartCode = new ParsableByteArray(H264Util.NAL_START_CODE);
|
||||||
|
nalLength = new ParsableByteArray(4);
|
||||||
|
encryptionSignalByte = new ParsableByteArray(1);
|
||||||
|
extendedTypeScratch = new byte[16];
|
||||||
|
containerAtoms = new Stack<ContainerAtom>();
|
||||||
|
fragmentRun = new TrackFragment();
|
||||||
|
parserState = STATE_READING_ATOM_HEADER;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sideloads track information into the extractor.
|
||||||
|
* <p>
|
||||||
|
* Should be called before {@link #read(ExtractorInput, PositionHolder)} in the case that the
|
||||||
|
* extractor will not receive a moov atom in the input data, from which track information would
|
||||||
|
* normally be parsed.
|
||||||
|
*
|
||||||
|
* @param track The track to sideload.
|
||||||
|
*/
|
||||||
|
public void setTrack(Track track) {
|
||||||
|
this.extendsDefaults = new DefaultSampleValues(0, 0, 0, 0);
|
||||||
|
this.track = track;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(ExtractorOutput output) {
|
||||||
|
extractorOutput = output;
|
||||||
|
trackOutput = output.track(0);
|
||||||
|
extractorOutput.endTracks();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void seek() {
|
||||||
|
containerAtoms.clear();
|
||||||
|
rootAtomBytesRead = 0;
|
||||||
|
parserState = STATE_READING_ATOM_HEADER;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(ExtractorInput input, PositionHolder seekPosition)
|
||||||
|
throws IOException, InterruptedException {
|
||||||
|
while (true) {
|
||||||
|
switch (parserState) {
|
||||||
|
case STATE_READING_ATOM_HEADER:
|
||||||
|
if (!readAtomHeader(input)) {
|
||||||
|
return Extractor.RESULT_END_OF_INPUT;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case STATE_READING_ATOM_PAYLOAD:
|
||||||
|
readAtomPayload(input);
|
||||||
|
break;
|
||||||
|
case STATE_READING_ENCRYPTION_DATA:
|
||||||
|
readEncryptionData(input);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (readSample(input)) {
|
||||||
|
return RESULT_CONTINUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean readAtomHeader(ExtractorInput input) throws IOException, InterruptedException {
|
||||||
|
if (!input.readFully(atomHeader.data, 0, Atom.HEADER_SIZE, true)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
rootAtomBytesRead += Atom.HEADER_SIZE;
|
||||||
|
atomHeader.setPosition(0);
|
||||||
|
atomSize = atomHeader.readInt();
|
||||||
|
atomType = atomHeader.readInt();
|
||||||
|
|
||||||
|
if (atomType == Atom.TYPE_mdat) {
|
||||||
|
if (fragmentRun.sampleEncryptionDataNeedsFill) {
|
||||||
|
parserState = STATE_READING_ENCRYPTION_DATA;
|
||||||
|
} else {
|
||||||
|
parserState = STATE_READING_SAMPLE_START;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldParseAtom(atomType)) {
|
||||||
|
if (shouldParseContainerAtom(atomType)) {
|
||||||
|
parserState = STATE_READING_ATOM_HEADER;
|
||||||
|
containerAtoms.add(new ContainerAtom(atomType,
|
||||||
|
rootAtomBytesRead + atomSize - Atom.HEADER_SIZE));
|
||||||
|
} else {
|
||||||
|
atomData = new ParsableByteArray(atomSize);
|
||||||
|
System.arraycopy(atomHeader.data, 0, atomData.data, 0, Atom.HEADER_SIZE);
|
||||||
|
parserState = STATE_READING_ATOM_PAYLOAD;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
atomData = null;
|
||||||
|
parserState = STATE_READING_ATOM_PAYLOAD;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void readAtomPayload(ExtractorInput input) throws IOException, InterruptedException {
|
||||||
|
int payloadLength = atomSize - Atom.HEADER_SIZE;
|
||||||
|
if (atomData != null) {
|
||||||
|
input.readFully(atomData.data, Atom.HEADER_SIZE, payloadLength);
|
||||||
|
rootAtomBytesRead += payloadLength;
|
||||||
|
onLeafAtomRead(new LeafAtom(atomType, atomData), input.getPosition());
|
||||||
|
} else {
|
||||||
|
input.skipFully(payloadLength);
|
||||||
|
rootAtomBytesRead += payloadLength;
|
||||||
|
}
|
||||||
|
while (!containerAtoms.isEmpty() && containerAtoms.peek().endByteOffset == rootAtomBytesRead) {
|
||||||
|
onContainerAtomRead(containerAtoms.pop());
|
||||||
|
}
|
||||||
|
if (containerAtoms.isEmpty()) {
|
||||||
|
rootAtomBytesRead = 0;
|
||||||
|
}
|
||||||
|
parserState = STATE_READING_ATOM_HEADER;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onLeafAtomRead(LeafAtom leaf, long inputPosition) {
|
||||||
|
if (!containerAtoms.isEmpty()) {
|
||||||
|
containerAtoms.peek().add(leaf);
|
||||||
|
} else if (leaf.type == Atom.TYPE_sidx) {
|
||||||
|
ChunkIndex segmentIndex = parseSidx(leaf.data, inputPosition);
|
||||||
|
extractorOutput.seekMap(segmentIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onContainerAtomRead(ContainerAtom container) {
|
||||||
|
if (container.type == Atom.TYPE_moov) {
|
||||||
|
onMoovContainerAtomRead(container);
|
||||||
|
} else if (container.type == Atom.TYPE_moof) {
|
||||||
|
onMoofContainerAtomRead(container);
|
||||||
|
} else if (!containerAtoms.isEmpty()) {
|
||||||
|
containerAtoms.peek().add(container);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onMoovContainerAtomRead(ContainerAtom moov) {
|
||||||
|
List<Atom.LeafAtom> moovChildren = moov.leafChildren;
|
||||||
|
int moovChildrenSize = moovChildren.size();
|
||||||
|
|
||||||
|
DrmInitData.Mapped drmInitData = null;
|
||||||
|
for (int i = 0; i < moovChildrenSize; i++) {
|
||||||
|
LeafAtom child = moovChildren.get(i);
|
||||||
|
if (child.type == Atom.TYPE_pssh) {
|
||||||
|
ParsableByteArray psshAtom = child.data;
|
||||||
|
psshAtom.setPosition(Atom.FULL_HEADER_SIZE);
|
||||||
|
UUID uuid = new UUID(psshAtom.readLong(), psshAtom.readLong());
|
||||||
|
int dataSize = psshAtom.readInt();
|
||||||
|
byte[] data = new byte[dataSize];
|
||||||
|
psshAtom.readBytes(data, 0, dataSize);
|
||||||
|
if (drmInitData == null) {
|
||||||
|
drmInitData = new DrmInitData.Mapped(MimeTypes.VIDEO_MP4);
|
||||||
|
}
|
||||||
|
drmInitData.put(uuid, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (drmInitData != null) {
|
||||||
|
extractorOutput.drmInitData(drmInitData);
|
||||||
|
}
|
||||||
|
|
||||||
|
ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex);
|
||||||
|
extendsDefaults = parseTrex(mvex.getLeafAtomOfType(Atom.TYPE_trex).data);
|
||||||
|
track = AtomParsers.parseTrak(moov.getContainerAtomOfType(Atom.TYPE_trak),
|
||||||
|
moov.getLeafAtomOfType(Atom.TYPE_mvhd));
|
||||||
|
Assertions.checkState(track != null);
|
||||||
|
trackOutput.format(track.mediaFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onMoofContainerAtomRead(ContainerAtom moof) {
|
||||||
|
fragmentRun.reset();
|
||||||
|
parseMoof(track, extendsDefaults, moof, fragmentRun, workaroundFlags, extendedTypeScratch);
|
||||||
|
sampleIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a trex atom (defined in 14496-12).
|
||||||
|
*/
|
||||||
|
private static DefaultSampleValues parseTrex(ParsableByteArray trex) {
|
||||||
|
trex.setPosition(Atom.FULL_HEADER_SIZE + 4);
|
||||||
|
int defaultSampleDescriptionIndex = trex.readUnsignedIntToInt() - 1;
|
||||||
|
int defaultSampleDuration = trex.readUnsignedIntToInt();
|
||||||
|
int defaultSampleSize = trex.readUnsignedIntToInt();
|
||||||
|
int defaultSampleFlags = trex.readInt();
|
||||||
|
return new DefaultSampleValues(defaultSampleDescriptionIndex, defaultSampleDuration,
|
||||||
|
defaultSampleSize, defaultSampleFlags);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void parseMoof(Track track, DefaultSampleValues extendsDefaults,
|
||||||
|
ContainerAtom moof, TrackFragment out, int workaroundFlags, byte[] extendedTypeScratch) {
|
||||||
|
parseTraf(track, extendsDefaults, moof.getContainerAtomOfType(Atom.TYPE_traf),
|
||||||
|
out, workaroundFlags, extendedTypeScratch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a traf atom (defined in 14496-12).
|
||||||
|
*/
|
||||||
|
private static void parseTraf(Track track, DefaultSampleValues extendsDefaults,
|
||||||
|
ContainerAtom traf, TrackFragment out, int workaroundFlags, byte[] extendedTypeScratch) {
|
||||||
|
LeafAtom tfdtAtom = traf.getLeafAtomOfType(Atom.TYPE_tfdt);
|
||||||
|
long decodeTime = tfdtAtom == null ? 0 : parseTfdt(traf.getLeafAtomOfType(Atom.TYPE_tfdt).data);
|
||||||
|
|
||||||
|
LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd);
|
||||||
|
DefaultSampleValues fragmentHeader = parseTfhd(extendsDefaults, tfhd.data);
|
||||||
|
out.sampleDescriptionIndex = fragmentHeader.sampleDescriptionIndex;
|
||||||
|
|
||||||
|
LeafAtom trun = traf.getLeafAtomOfType(Atom.TYPE_trun);
|
||||||
|
parseTrun(track, fragmentHeader, decodeTime, workaroundFlags, trun.data, out);
|
||||||
|
|
||||||
|
LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz);
|
||||||
|
if (saiz != null) {
|
||||||
|
TrackEncryptionBox trackEncryptionBox =
|
||||||
|
track.sampleDescriptionEncryptionBoxes[fragmentHeader.sampleDescriptionIndex];
|
||||||
|
parseSaiz(trackEncryptionBox, saiz.data, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
LeafAtom senc = traf.getLeafAtomOfType(Atom.TYPE_senc);
|
||||||
|
if (senc != null) {
|
||||||
|
parseSenc(senc.data, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
int childrenSize = traf.leafChildren.size();
|
||||||
|
for (int i = 0; i < childrenSize; i++) {
|
||||||
|
LeafAtom atom = traf.leafChildren.get(i);
|
||||||
|
if (atom.type == Atom.TYPE_uuid) {
|
||||||
|
parseUuid(atom.data, out, extendedTypeScratch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void parseSaiz(TrackEncryptionBox encryptionBox, ParsableByteArray saiz,
|
||||||
|
TrackFragment out) {
|
||||||
|
int vectorSize = encryptionBox.initializationVectorSize;
|
||||||
|
saiz.setPosition(Atom.HEADER_SIZE);
|
||||||
|
int fullAtom = saiz.readInt();
|
||||||
|
int flags = Atom.parseFullAtomFlags(fullAtom);
|
||||||
|
if ((flags & 0x01) == 1) {
|
||||||
|
saiz.skip(8);
|
||||||
|
}
|
||||||
|
int defaultSampleInfoSize = saiz.readUnsignedByte();
|
||||||
|
|
||||||
|
int sampleCount = saiz.readUnsignedIntToInt();
|
||||||
|
if (sampleCount != out.length) {
|
||||||
|
throw new IllegalStateException("Length mismatch: " + sampleCount + ", " + out.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
int totalSize = 0;
|
||||||
|
if (defaultSampleInfoSize == 0) {
|
||||||
|
boolean[] sampleHasSubsampleEncryptionTable = out.sampleHasSubsampleEncryptionTable;
|
||||||
|
for (int i = 0; i < sampleCount; i++) {
|
||||||
|
int sampleInfoSize = saiz.readUnsignedByte();
|
||||||
|
totalSize += sampleInfoSize;
|
||||||
|
sampleHasSubsampleEncryptionTable[i] = sampleInfoSize > vectorSize;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
boolean subsampleEncryption = defaultSampleInfoSize > vectorSize;
|
||||||
|
totalSize += defaultSampleInfoSize * sampleCount;
|
||||||
|
Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption);
|
||||||
|
}
|
||||||
|
out.initEncryptionData(totalSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a tfhd atom (defined in 14496-12).
|
||||||
|
*
|
||||||
|
* @param extendsDefaults Default sample values from the trex atom.
|
||||||
|
* @return The parsed default sample values.
|
||||||
|
*/
|
||||||
|
private static DefaultSampleValues parseTfhd(DefaultSampleValues extendsDefaults,
|
||||||
|
ParsableByteArray tfhd) {
|
||||||
|
tfhd.setPosition(Atom.HEADER_SIZE);
|
||||||
|
int fullAtom = tfhd.readInt();
|
||||||
|
int flags = Atom.parseFullAtomFlags(fullAtom);
|
||||||
|
|
||||||
|
tfhd.skip(4); // trackId
|
||||||
|
if ((flags & 0x01 /* base_data_offset_present */) != 0) {
|
||||||
|
tfhd.skip(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
int defaultSampleDescriptionIndex =
|
||||||
|
((flags & 0x02 /* default_sample_description_index_present */) != 0)
|
||||||
|
? tfhd.readUnsignedIntToInt() - 1 : extendsDefaults.sampleDescriptionIndex;
|
||||||
|
int defaultSampleDuration = ((flags & 0x08 /* default_sample_duration_present */) != 0)
|
||||||
|
? tfhd.readUnsignedIntToInt() : extendsDefaults.duration;
|
||||||
|
int defaultSampleSize = ((flags & 0x10 /* default_sample_size_present */) != 0)
|
||||||
|
? tfhd.readUnsignedIntToInt() : extendsDefaults.size;
|
||||||
|
int defaultSampleFlags = ((flags & 0x20 /* default_sample_flags_present */) != 0)
|
||||||
|
? tfhd.readUnsignedIntToInt() : extendsDefaults.flags;
|
||||||
|
return new DefaultSampleValues(defaultSampleDescriptionIndex, defaultSampleDuration,
|
||||||
|
defaultSampleSize, defaultSampleFlags);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a tfdt atom (defined in 14496-12).
|
||||||
|
*
|
||||||
|
* @return baseMediaDecodeTime The sum of the decode durations of all earlier samples in the
|
||||||
|
* media, expressed in the media's timescale.
|
||||||
|
*/
|
||||||
|
private static long parseTfdt(ParsableByteArray tfdt) {
|
||||||
|
tfdt.setPosition(Atom.HEADER_SIZE);
|
||||||
|
int fullAtom = tfdt.readInt();
|
||||||
|
int version = Atom.parseFullAtomVersion(fullAtom);
|
||||||
|
return version == 1 ? tfdt.readUnsignedLongToLong() : tfdt.readUnsignedInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a trun atom (defined in 14496-12).
|
||||||
|
*
|
||||||
|
* @param track The corresponding track.
|
||||||
|
* @param defaultSampleValues Default sample values.
|
||||||
|
* @param decodeTime The decode time.
|
||||||
|
* @param trun The trun atom to parse.
|
||||||
|
* @param out The {@TrackFragment} into which parsed data should be placed.
|
||||||
|
*/
|
||||||
|
private static void parseTrun(Track track, DefaultSampleValues defaultSampleValues,
|
||||||
|
long decodeTime, int workaroundFlags, ParsableByteArray trun, TrackFragment out) {
|
||||||
|
trun.setPosition(Atom.HEADER_SIZE);
|
||||||
|
int fullAtom = trun.readInt();
|
||||||
|
int flags = Atom.parseFullAtomFlags(fullAtom);
|
||||||
|
|
||||||
|
int sampleCount = trun.readUnsignedIntToInt();
|
||||||
|
if ((flags & 0x01 /* data_offset_present */) != 0) {
|
||||||
|
trun.skip(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean firstSampleFlagsPresent = (flags & 0x04 /* first_sample_flags_present */) != 0;
|
||||||
|
int firstSampleFlags = defaultSampleValues.flags;
|
||||||
|
if (firstSampleFlagsPresent) {
|
||||||
|
firstSampleFlags = trun.readUnsignedIntToInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean sampleDurationsPresent = (flags & 0x100 /* sample_duration_present */) != 0;
|
||||||
|
boolean sampleSizesPresent = (flags & 0x200 /* sample_size_present */) != 0;
|
||||||
|
boolean sampleFlagsPresent = (flags & 0x400 /* sample_flags_present */) != 0;
|
||||||
|
boolean sampleCompositionTimeOffsetsPresent =
|
||||||
|
(flags & 0x800 /* sample_composition_time_offsets_present */) != 0;
|
||||||
|
|
||||||
|
out.initTables(sampleCount);
|
||||||
|
int[] sampleSizeTable = out.sampleSizeTable;
|
||||||
|
int[] sampleCompositionTimeOffsetTable = out.sampleCompositionTimeOffsetTable;
|
||||||
|
long[] sampleDecodingTimeTable = out.sampleDecodingTimeTable;
|
||||||
|
boolean[] sampleIsSyncFrameTable = out.sampleIsSyncFrameTable;
|
||||||
|
|
||||||
|
long timescale = track.timescale;
|
||||||
|
long cumulativeTime = decodeTime;
|
||||||
|
boolean workaroundEveryVideoFrameIsSyncFrame = track.type == Track.TYPE_VIDEO
|
||||||
|
&& ((workaroundFlags & WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME)
|
||||||
|
== WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME);
|
||||||
|
for (int i = 0; i < sampleCount; i++) {
|
||||||
|
// Use trun values if present, otherwise tfhd, otherwise trex.
|
||||||
|
int sampleDuration = sampleDurationsPresent ? trun.readUnsignedIntToInt()
|
||||||
|
: defaultSampleValues.duration;
|
||||||
|
int sampleSize = sampleSizesPresent ? trun.readUnsignedIntToInt() : defaultSampleValues.size;
|
||||||
|
int sampleFlags = (i == 0 && firstSampleFlagsPresent) ? firstSampleFlags
|
||||||
|
: sampleFlagsPresent ? trun.readInt() : defaultSampleValues.flags;
|
||||||
|
if (sampleCompositionTimeOffsetsPresent) {
|
||||||
|
// The BMFF spec (ISO 14496-12) states that sample offsets should be unsigned integers in
|
||||||
|
// version 0 trun boxes, however a significant number of streams violate the spec and use
|
||||||
|
// signed integers instead. It's safe to always parse sample offsets as signed integers
|
||||||
|
// here, because unsigned integers will still be parsed correctly (unless their top bit is
|
||||||
|
// set, which is never true in practice because sample offsets are always small).
|
||||||
|
int sampleOffset = trun.readInt();
|
||||||
|
sampleCompositionTimeOffsetTable[i] = (int) ((sampleOffset * 1000) / timescale);
|
||||||
|
} else {
|
||||||
|
sampleCompositionTimeOffsetTable[i] = 0;
|
||||||
|
}
|
||||||
|
sampleDecodingTimeTable[i] = (cumulativeTime * 1000) / timescale;
|
||||||
|
sampleSizeTable[i] = sampleSize;
|
||||||
|
sampleIsSyncFrameTable[i] = ((sampleFlags >> 16) & 0x1) == 0
|
||||||
|
&& (!workaroundEveryVideoFrameIsSyncFrame || i == 0);
|
||||||
|
cumulativeTime += sampleDuration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void parseUuid(ParsableByteArray uuid, TrackFragment out,
|
||||||
|
byte[] extendedTypeScratch) {
|
||||||
|
uuid.setPosition(Atom.HEADER_SIZE);
|
||||||
|
uuid.readBytes(extendedTypeScratch, 0, 16);
|
||||||
|
|
||||||
|
// Currently this parser only supports Microsoft's PIFF SampleEncryptionBox.
|
||||||
|
if (!Arrays.equals(extendedTypeScratch, PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Except for the extended type, this box is identical to a SENC box. See "Portable encoding of
|
||||||
|
// audio-video objects: The Protected Interoperable File Format (PIFF), John A. Bocharov et al,
|
||||||
|
// Section 5.3.2.1."
|
||||||
|
parseSenc(uuid, 16, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void parseSenc(ParsableByteArray senc, TrackFragment out) {
|
||||||
|
parseSenc(senc, 0, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void parseSenc(ParsableByteArray senc, int offset, TrackFragment out) {
|
||||||
|
senc.setPosition(Atom.HEADER_SIZE + offset);
|
||||||
|
int fullAtom = senc.readInt();
|
||||||
|
int flags = Atom.parseFullAtomFlags(fullAtom);
|
||||||
|
|
||||||
|
if ((flags & 0x01 /* override_track_encryption_box_parameters */) != 0) {
|
||||||
|
// TODO: Implement this.
|
||||||
|
throw new IllegalStateException("Overriding TrackEncryptionBox parameters is unsupported");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean subsampleEncryption = (flags & 0x02 /* use_subsample_encryption */) != 0;
|
||||||
|
int sampleCount = senc.readUnsignedIntToInt();
|
||||||
|
if (sampleCount != out.length) {
|
||||||
|
throw new IllegalStateException("Length mismatch: " + sampleCount + ", " + out.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption);
|
||||||
|
out.initEncryptionData(senc.bytesLeft());
|
||||||
|
out.fillEncryptionData(senc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a sidx atom (defined in 14496-12).
|
||||||
|
*/
|
||||||
|
private static ChunkIndex parseSidx(ParsableByteArray atom, long inputPosition) {
|
||||||
|
atom.setPosition(Atom.HEADER_SIZE);
|
||||||
|
int fullAtom = atom.readInt();
|
||||||
|
int version = Atom.parseFullAtomVersion(fullAtom);
|
||||||
|
|
||||||
|
atom.skip(4);
|
||||||
|
long timescale = atom.readUnsignedInt();
|
||||||
|
long earliestPresentationTime;
|
||||||
|
long offset = inputPosition;
|
||||||
|
if (version == 0) {
|
||||||
|
earliestPresentationTime = atom.readUnsignedInt();
|
||||||
|
offset += atom.readUnsignedInt();
|
||||||
|
} else {
|
||||||
|
earliestPresentationTime = atom.readUnsignedLongToLong();
|
||||||
|
offset += atom.readUnsignedLongToLong();
|
||||||
|
}
|
||||||
|
|
||||||
|
atom.skip(2);
|
||||||
|
|
||||||
|
int referenceCount = atom.readUnsignedShort();
|
||||||
|
int[] sizes = new int[referenceCount];
|
||||||
|
long[] offsets = new long[referenceCount];
|
||||||
|
long[] durationsUs = new long[referenceCount];
|
||||||
|
long[] timesUs = new long[referenceCount];
|
||||||
|
|
||||||
|
long time = earliestPresentationTime;
|
||||||
|
long timeUs = Util.scaleLargeTimestamp(time, C.MICROS_PER_SECOND, timescale);
|
||||||
|
for (int i = 0; i < referenceCount; i++) {
|
||||||
|
int firstInt = atom.readInt();
|
||||||
|
|
||||||
|
int type = 0x80000000 & firstInt;
|
||||||
|
if (type != 0) {
|
||||||
|
throw new IllegalStateException("Unhandled indirect reference");
|
||||||
|
}
|
||||||
|
long referenceDuration = atom.readUnsignedInt();
|
||||||
|
|
||||||
|
sizes[i] = 0x7fffffff & firstInt;
|
||||||
|
offsets[i] = offset;
|
||||||
|
|
||||||
|
// Calculate time and duration values such that any rounding errors are consistent. i.e. That
|
||||||
|
// timesUs[i] + durationsUs[i] == timesUs[i + 1].
|
||||||
|
timesUs[i] = timeUs;
|
||||||
|
time += referenceDuration;
|
||||||
|
timeUs = Util.scaleLargeTimestamp(time, C.MICROS_PER_SECOND, timescale);
|
||||||
|
durationsUs[i] = timeUs - timesUs[i];
|
||||||
|
|
||||||
|
atom.skip(4);
|
||||||
|
offset += sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ChunkIndex(sizes, offsets, durationsUs, timesUs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void readEncryptionData(ExtractorInput input) throws IOException, InterruptedException {
|
||||||
|
fragmentRun.fillEncryptionData(input);
|
||||||
|
parserState = STATE_READING_SAMPLE_START;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to extract the next sample in the current mdat atom.
|
||||||
|
* <p>
|
||||||
|
* If there are no more samples in the current mdat atom then the parser state is transitioned
|
||||||
|
* to {@link #STATE_READING_ATOM_HEADER} and {@code false} is returned.
|
||||||
|
* <p>
|
||||||
|
* It is possible for a sample to be extracted in part in the case that an exception is thrown. In
|
||||||
|
* this case the method can be called again to extract the remainder of the sample.
|
||||||
|
*
|
||||||
|
* @param input The {@link ExtractorInput} from which to read data.
|
||||||
|
* @return True if a sample was extracted. False otherwise.
|
||||||
|
* @throws IOException If an error occurs reading from the input.
|
||||||
|
* @throws InterruptedException If the thread is interrupted.
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parserState == STATE_READING_SAMPLE_START) {
|
||||||
|
sampleSize = fragmentRun.sampleSizeTable[sampleIndex];
|
||||||
|
if (fragmentRun.definesEncryptionData) {
|
||||||
|
sampleBytesWritten = appendSampleEncryptionData(fragmentRun.sampleEncryptionData);
|
||||||
|
sampleSize += sampleBytesWritten;
|
||||||
|
} else {
|
||||||
|
sampleBytesWritten = 0;
|
||||||
|
}
|
||||||
|
sampleCurrentNalBytesRemaining = 0;
|
||||||
|
parserState = STATE_READING_SAMPLE_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (track.type == Track.TYPE_VIDEO) {
|
||||||
|
while (sampleBytesWritten < sampleSize) {
|
||||||
|
// NAL units are length delimited, but the decoder requires start code delimited units.
|
||||||
|
if (sampleCurrentNalBytesRemaining == 0) {
|
||||||
|
// Read the NAL length so that we know where we find the next NAL unit.
|
||||||
|
input.readFully(nalLength.data, 0, 4);
|
||||||
|
nalLength.setPosition(0);
|
||||||
|
sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt();
|
||||||
|
// Write a start code for the current NAL unit.
|
||||||
|
nalStartCode.setPosition(0);
|
||||||
|
trackOutput.sampleData(nalStartCode, 4);
|
||||||
|
sampleBytesWritten += 4;
|
||||||
|
} else {
|
||||||
|
// Write the payload of the NAL unit.
|
||||||
|
int writtenBytes = trackOutput.sampleData(input, sampleCurrentNalBytesRemaining);
|
||||||
|
sampleBytesWritten += writtenBytes;
|
||||||
|
sampleCurrentNalBytesRemaining -= writtenBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
while (sampleBytesWritten < sampleSize) {
|
||||||
|
int writtenBytes = trackOutput.sampleData(input, sampleSize - sampleBytesWritten);
|
||||||
|
sampleBytesWritten += writtenBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
long sampleTimeUs = fragmentRun.getSamplePresentationTime(sampleIndex) * 1000L;
|
||||||
|
int sampleFlags = (fragmentRun.definesEncryptionData ? C.SAMPLE_FLAG_ENCRYPTED : 0)
|
||||||
|
| (fragmentRun.sampleIsSyncFrameTable[sampleIndex] ? C.SAMPLE_FLAG_SYNC : 0);
|
||||||
|
byte[] encryptionKey = fragmentRun.definesEncryptionData
|
||||||
|
? track.sampleDescriptionEncryptionBoxes[fragmentRun.sampleDescriptionIndex].keyId : null;
|
||||||
|
trackOutput.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, encryptionKey);
|
||||||
|
|
||||||
|
sampleIndex++;
|
||||||
|
parserState = STATE_READING_SAMPLE_START;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int appendSampleEncryptionData(ParsableByteArray sampleEncryptionData) {
|
||||||
|
TrackEncryptionBox encryptionBox =
|
||||||
|
track.sampleDescriptionEncryptionBoxes[fragmentRun.sampleDescriptionIndex];
|
||||||
|
int vectorSize = encryptionBox.initializationVectorSize;
|
||||||
|
boolean subsampleEncryption = fragmentRun.sampleHasSubsampleEncryptionTable[sampleIndex];
|
||||||
|
|
||||||
|
// Write the signal byte, containing the vector size and the subsample encryption flag.
|
||||||
|
encryptionSignalByte.data[0] = (byte) (vectorSize | (subsampleEncryption ? 0x80 : 0));
|
||||||
|
encryptionSignalByte.setPosition(0);
|
||||||
|
trackOutput.sampleData(encryptionSignalByte, 1);
|
||||||
|
// Write the vector.
|
||||||
|
trackOutput.sampleData(sampleEncryptionData, vectorSize);
|
||||||
|
// If we don't have subsample encryption data, we're done.
|
||||||
|
if (!subsampleEncryption) {
|
||||||
|
return 1 + vectorSize;
|
||||||
|
}
|
||||||
|
// Write the subsample encryption data.
|
||||||
|
int subsampleCount = sampleEncryptionData.readUnsignedShort();
|
||||||
|
sampleEncryptionData.skip(-2);
|
||||||
|
int subsampleDataLength = 2 + 6 * subsampleCount;
|
||||||
|
trackOutput.sampleData(sampleEncryptionData, subsampleDataLength);
|
||||||
|
return 1 + vectorSize + subsampleDataLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns whether the extractor should parse an atom with type {@code atom}. */
|
||||||
|
private static boolean shouldParseAtom(int atom) {
|
||||||
|
return atom == Atom.TYPE_avc1 || atom == Atom.TYPE_avc3 || atom == Atom.TYPE_esds
|
||||||
|
|| atom == Atom.TYPE_hdlr || atom == Atom.TYPE_mdat || atom == Atom.TYPE_mdhd
|
||||||
|
|| atom == Atom.TYPE_moof || atom == Atom.TYPE_moov || atom == Atom.TYPE_mp4a
|
||||||
|
|| atom == Atom.TYPE_mvhd || atom == Atom.TYPE_sidx || atom == Atom.TYPE_stsd
|
||||||
|
|| atom == Atom.TYPE_tfdt || atom == Atom.TYPE_tfhd || atom == Atom.TYPE_tkhd
|
||||||
|
|| atom == Atom.TYPE_traf || atom == Atom.TYPE_trak || atom == Atom.TYPE_trex
|
||||||
|
|| atom == Atom.TYPE_trun || atom == Atom.TYPE_mvex || atom == Atom.TYPE_mdia
|
||||||
|
|| atom == Atom.TYPE_minf || atom == Atom.TYPE_stbl || atom == Atom.TYPE_pssh
|
||||||
|
|| atom == Atom.TYPE_saiz || atom == Atom.TYPE_uuid || atom == Atom.TYPE_senc
|
||||||
|
|| atom == Atom.TYPE_pasp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns whether the extractor should parse a container atom with type {@code atom}. */
|
||||||
|
private static boolean shouldParseContainerAtom(int atom) {
|
||||||
|
return atom == Atom.TYPE_moov || atom == Atom.TYPE_trak || atom == Atom.TYPE_mdia
|
||||||
|
|| atom == Atom.TYPE_minf || atom == Atom.TYPE_stbl || atom == Atom.TYPE_avcC
|
||||||
|
|| atom == Atom.TYPE_moof || atom == Atom.TYPE_traf || atom == Atom.TYPE_mvex;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,371 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2014 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer.extractor.mp4;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.extractor.Extractor;
|
||||||
|
import com.google.android.exoplayer.extractor.ExtractorInput;
|
||||||
|
import com.google.android.exoplayer.extractor.ExtractorOutput;
|
||||||
|
import com.google.android.exoplayer.extractor.PositionHolder;
|
||||||
|
import com.google.android.exoplayer.extractor.SeekMap;
|
||||||
|
import com.google.android.exoplayer.extractor.TrackOutput;
|
||||||
|
import com.google.android.exoplayer.extractor.mp4.Atom.ContainerAtom;
|
||||||
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
|
import com.google.android.exoplayer.util.H264Util;
|
||||||
|
import com.google.android.exoplayer.util.MimeTypes;
|
||||||
|
import com.google.android.exoplayer.util.ParsableByteArray;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Stack;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts data from an unfragmented MP4 file.
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
// Temporary arrays.
|
||||||
|
private final ParsableByteArray nalStartCode;
|
||||||
|
private final ParsableByteArray nalLength;
|
||||||
|
|
||||||
|
private final ParsableByteArray atomHeader;
|
||||||
|
private final Stack<ContainerAtom> containerAtoms;
|
||||||
|
|
||||||
|
private int parserState;
|
||||||
|
private long rootAtomBytesRead;
|
||||||
|
private int atomType;
|
||||||
|
private long atomSize;
|
||||||
|
private int atomBytesRead;
|
||||||
|
private ParsableByteArray atomData;
|
||||||
|
|
||||||
|
private int sampleSize;
|
||||||
|
private int sampleBytesWritten;
|
||||||
|
private int sampleCurrentNalBytesRemaining;
|
||||||
|
|
||||||
|
// Extractor outputs.
|
||||||
|
private ExtractorOutput extractorOutput;
|
||||||
|
private Mp4Track[] tracks;
|
||||||
|
|
||||||
|
public Mp4Extractor() {
|
||||||
|
atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE);
|
||||||
|
containerAtoms = new Stack<Atom.ContainerAtom>();
|
||||||
|
nalStartCode = new ParsableByteArray(H264Util.NAL_START_CODE);
|
||||||
|
nalLength = new ParsableByteArray(4);
|
||||||
|
parserState = STATE_READING_ATOM_HEADER;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(ExtractorOutput output) {
|
||||||
|
extractorOutput = output;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void seek() {
|
||||||
|
rootAtomBytesRead = 0;
|
||||||
|
sampleBytesWritten = 0;
|
||||||
|
sampleCurrentNalBytesRemaining = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(ExtractorInput input, PositionHolder seekPosition)
|
||||||
|
throws IOException, InterruptedException {
|
||||||
|
while (true) {
|
||||||
|
switch (parserState) {
|
||||||
|
case STATE_READING_ATOM_HEADER:
|
||||||
|
if (!readAtomHeader(input)) {
|
||||||
|
return RESULT_END_OF_INPUT;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case STATE_READING_ATOM_PAYLOAD:
|
||||||
|
if (readAtomPayload(input, seekPosition)) {
|
||||||
|
return RESULT_SEEK;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return readSample(input, seekPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SeekMap implementation.
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getPosition(long timeUs) {
|
||||||
|
long earliestSamplePosition = Long.MAX_VALUE;
|
||||||
|
for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) {
|
||||||
|
TrackSampleTable sampleTable = tracks[trackIndex].sampleTable;
|
||||||
|
int sampleIndex = sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(timeUs);
|
||||||
|
if (sampleIndex == TrackSampleTable.NO_SAMPLE) {
|
||||||
|
sampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs);
|
||||||
|
}
|
||||||
|
tracks[trackIndex].sampleIndex = sampleIndex;
|
||||||
|
|
||||||
|
long offset = sampleTable.offsets[tracks[trackIndex].sampleIndex];
|
||||||
|
if (offset < earliestSamplePosition) {
|
||||||
|
earliestSamplePosition = offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return earliestSamplePosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean readAtomHeader(ExtractorInput input) throws IOException, InterruptedException {
|
||||||
|
if (!input.readFully(atomHeader.data, 0, Atom.HEADER_SIZE, true)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
} else if (shouldParseLeafAtom(atomType)) {
|
||||||
|
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 {
|
||||||
|
atomData = null;
|
||||||
|
parserState = STATE_READING_ATOM_PAYLOAD;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes the atom payload. If {@link #atomData} is null and the size is at or above the
|
||||||
|
* threshold {@link #RELOAD_MINIMUM_SEEK_DISTANCE}, {@code true} is returned and the caller should
|
||||||
|
* restart loading at the position in {@code positionHolder}. Otherwise, the atom is read/skipped.
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
if (!containerAtoms.isEmpty()) {
|
||||||
|
containerAtoms.peek().add(new Atom.LeafAtom(atomType, atomData));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
input.skipFully((int) atomRemainingBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!containerAtoms.isEmpty() && containerAtoms.peek().endByteOffset == rootAtomBytesRead) {
|
||||||
|
Atom.ContainerAtom containerAtom = containerAtoms.pop();
|
||||||
|
if (containerAtom.type == Atom.TYPE_moov) {
|
||||||
|
processMoovAtom(containerAtom);
|
||||||
|
} else if (!containerAtoms.isEmpty()) {
|
||||||
|
containerAtoms.peek().add(containerAtom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return seekRequired;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Updates the stored track metadata to reflect the contents of the specified moov atom. */
|
||||||
|
private void processMoovAtom(ContainerAtom moov) {
|
||||||
|
List<Mp4Track> tracks = new ArrayList<Mp4Track>();
|
||||||
|
long earliestSampleOffset = Long.MAX_VALUE;
|
||||||
|
for (int i = 0; i < moov.containerChildren.size(); i++) {
|
||||||
|
Atom.ContainerAtom atom = moov.containerChildren.get(i);
|
||||||
|
if (atom.type != Atom.TYPE_trak) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Track track = AtomParsers.parseTrak(atom, moov.getLeafAtomOfType(Atom.TYPE_mvhd));
|
||||||
|
if (track == null || (track.type != Track.TYPE_AUDIO && track.type != Track.TYPE_VIDEO)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia)
|
||||||
|
.getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl);
|
||||||
|
TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom);
|
||||||
|
if (trackSampleTable.sampleCount == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Mp4Track mp4Track = new Mp4Track(track, trackSampleTable, extractorOutput.track(i));
|
||||||
|
mp4Track.trackOutput.format(track.mediaFormat);
|
||||||
|
tracks.add(mp4Track);
|
||||||
|
|
||||||
|
long firstSampleOffset = trackSampleTable.offsets[0];
|
||||||
|
if (firstSampleOffset < earliestSampleOffset) {
|
||||||
|
earliestSampleOffset = firstSampleOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.tracks = tracks.toArray(new Mp4Track[0]);
|
||||||
|
extractorOutput.endTracks();
|
||||||
|
extractorOutput.seekMap(this);
|
||||||
|
parserState = STATE_READING_SAMPLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to extract the next sample in the current mdat atom for the specified track.
|
||||||
|
* <p>
|
||||||
|
* Returns {@link #RESULT_SEEK} if the source should be reloaded from the position in
|
||||||
|
* {@code positionHolder}.
|
||||||
|
* <p>
|
||||||
|
* Returns {@link #RESULT_END_OF_INPUT} if no samples are left. Otherwise, returns
|
||||||
|
* {@link #RESULT_CONTINUE}.
|
||||||
|
*
|
||||||
|
* @param input The {@link ExtractorInput} from which to read data.
|
||||||
|
* @param positionHolder If {@link #RESULT_SEEK} is returned, this holder is updated to hold the
|
||||||
|
* position of the required data.
|
||||||
|
* @return One of the {@code RESULT_*} flags in {@link Extractor}.
|
||||||
|
* @throws IOException If an error occurs reading from the input.
|
||||||
|
* @throws InterruptedException If the thread is interrupted.
|
||||||
|
*/
|
||||||
|
private int readSample(ExtractorInput input, PositionHolder positionHolder)
|
||||||
|
throws IOException, InterruptedException {
|
||||||
|
int trackIndex = getTrackIndexOfEarliestCurrentSample();
|
||||||
|
if (trackIndex == TrackSampleTable.NO_SAMPLE) {
|
||||||
|
return RESULT_END_OF_INPUT;
|
||||||
|
}
|
||||||
|
Mp4Track track = tracks[trackIndex];
|
||||||
|
int sampleIndex = track.sampleIndex;
|
||||||
|
long position = track.sampleTable.offsets[sampleIndex];
|
||||||
|
long skipAmount = position - input.getPosition() + sampleBytesWritten;
|
||||||
|
if (skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE) {
|
||||||
|
positionHolder.position = position;
|
||||||
|
return RESULT_SEEK;
|
||||||
|
}
|
||||||
|
input.skipFully((int) skipAmount);
|
||||||
|
sampleSize = track.sampleTable.sizes[sampleIndex];
|
||||||
|
if (track.track.type == Track.TYPE_VIDEO
|
||||||
|
&& MimeTypes.VIDEO_H264.equals(track.track.mediaFormat.mimeType)) {
|
||||||
|
while (sampleBytesWritten < sampleSize) {
|
||||||
|
// NAL units are length delimited, but the decoder requires start code delimited units.
|
||||||
|
if (sampleCurrentNalBytesRemaining == 0) {
|
||||||
|
// Read the NAL length so that we know where we find the next NAL unit.
|
||||||
|
input.readFully(nalLength.data, 0, 4);
|
||||||
|
nalLength.setPosition(0);
|
||||||
|
sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt();
|
||||||
|
// Write a start code for the current NAL unit.
|
||||||
|
nalStartCode.setPosition(0);
|
||||||
|
track.trackOutput.sampleData(nalStartCode, 4);
|
||||||
|
sampleBytesWritten += 4;
|
||||||
|
} else {
|
||||||
|
// Write the payload of the NAL unit.
|
||||||
|
int writtenBytes = track.trackOutput.sampleData(input, sampleCurrentNalBytesRemaining);
|
||||||
|
sampleBytesWritten += writtenBytes;
|
||||||
|
sampleCurrentNalBytesRemaining -= writtenBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
while (sampleBytesWritten < sampleSize) {
|
||||||
|
int writtenBytes = track.trackOutput.sampleData(input, sampleSize - sampleBytesWritten);
|
||||||
|
sampleBytesWritten += writtenBytes;
|
||||||
|
sampleCurrentNalBytesRemaining -= writtenBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
track.trackOutput.sampleMetadata(track.sampleTable.timestampsUs[sampleIndex],
|
||||||
|
track.sampleTable.flags[sampleIndex], sampleSize, 0, null);
|
||||||
|
track.sampleIndex++;
|
||||||
|
sampleBytesWritten = 0;
|
||||||
|
sampleCurrentNalBytesRemaining = 0;
|
||||||
|
return RESULT_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the index of the track that contains the earliest current sample, or
|
||||||
|
* {@link TrackSampleTable#NO_SAMPLE} if no samples remain.
|
||||||
|
*/
|
||||||
|
private int getTrackIndexOfEarliestCurrentSample() {
|
||||||
|
int earliestSampleTrackIndex = TrackSampleTable.NO_SAMPLE;
|
||||||
|
long earliestSampleOffset = Long.MAX_VALUE;
|
||||||
|
for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) {
|
||||||
|
Mp4Track track = tracks[trackIndex];
|
||||||
|
int sampleIndex = track.sampleIndex;
|
||||||
|
if (sampleIndex == track.sampleTable.sampleCount) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
long trackSampleOffset = track.sampleTable.offsets[sampleIndex];
|
||||||
|
if (trackSampleOffset < earliestSampleOffset) {
|
||||||
|
earliestSampleOffset = trackSampleOffset;
|
||||||
|
earliestSampleTrackIndex = trackIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return earliestSampleTrackIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns whether the extractor should parse a leaf atom with type {@code atom}. */
|
||||||
|
private static boolean shouldParseLeafAtom(int atom) {
|
||||||
|
return atom == Atom.TYPE_mdhd || atom == Atom.TYPE_mvhd || atom == Atom.TYPE_hdlr
|
||||||
|
|| atom == Atom.TYPE_vmhd || atom == Atom.TYPE_smhd || atom == Atom.TYPE_stsd
|
||||||
|
|| atom == Atom.TYPE_avc1 || atom == Atom.TYPE_avcC || atom == Atom.TYPE_mp4a
|
||||||
|
|| atom == Atom.TYPE_esds || atom == Atom.TYPE_stts || atom == Atom.TYPE_stss
|
||||||
|
|| atom == Atom.TYPE_ctts || atom == Atom.TYPE_stsc || atom == Atom.TYPE_stsz
|
||||||
|
|| atom == Atom.TYPE_stco || atom == Atom.TYPE_co64 || atom == Atom.TYPE_tkhd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns whether the extractor should parse a container atom with type {@code atom}. */
|
||||||
|
private static boolean shouldParseContainerAtom(int atom) {
|
||||||
|
return atom == Atom.TYPE_moov || atom == Atom.TYPE_trak || atom == Atom.TYPE_mdia
|
||||||
|
|| atom == Atom.TYPE_minf || atom == Atom.TYPE_stbl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Mp4Track {
|
||||||
|
|
||||||
|
public final Track track;
|
||||||
|
public final TrackSampleTable sampleTable;
|
||||||
|
public final TrackOutput trackOutput;
|
||||||
|
|
||||||
|
public int sampleIndex;
|
||||||
|
|
||||||
|
public Mp4Track(Track track, TrackSampleTable sampleTable, TrackOutput trackOutput) {
|
||||||
|
this.track = track;
|
||||||
|
this.sampleTable = sampleTable;
|
||||||
|
this.trackOutput = trackOutput;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -13,11 +13,10 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.mp4;
|
package com.google.android.exoplayer.extractor.mp4;
|
||||||
|
|
||||||
import com.google.android.exoplayer.C;
|
import com.google.android.exoplayer.C;
|
||||||
import com.google.android.exoplayer.MediaFormat;
|
import com.google.android.exoplayer.MediaFormat;
|
||||||
import com.google.android.exoplayer.chunk.parser.mp4.TrackEncryptionBox;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encapsulates information describing an MP4 track.
|
* Encapsulates information describing an MP4 track.
|
||||||
|
|
@ -13,11 +13,12 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.chunk.parser.mp4;
|
package com.google.android.exoplayer.extractor.mp4;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encapsulates information parsed from a track encryption (tenc) box in an MP4 stream.
|
* Encapsulates information parsed from a track encryption (tenc) box in an MP4 stream.
|
||||||
*/
|
*/
|
||||||
|
// TODO: Make package private.
|
||||||
public final class TrackEncryptionBox {
|
public final class TrackEncryptionBox {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -13,15 +13,19 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.chunk.parser.mp4;
|
package com.google.android.exoplayer.extractor.mp4;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.extractor.ExtractorInput;
|
||||||
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
||||||
import com.google.android.exoplayer.util.ParsableByteArray;
|
import com.google.android.exoplayer.util.ParsableByteArray;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A holder for information corresponding to a single fragment of an mp4 file.
|
* A holder for information corresponding to a single fragment of an mp4 file.
|
||||||
*/
|
*/
|
||||||
/* package */ final class TrackFragment {
|
// TODO: Make package private.
|
||||||
|
public final class TrackFragment {
|
||||||
|
|
||||||
public int sampleDescriptionIndex;
|
public int sampleDescriptionIndex;
|
||||||
|
|
||||||
|
|
@ -121,6 +125,17 @@ import com.google.android.exoplayer.util.ParsableByteArray;
|
||||||
sampleEncryptionDataNeedsFill = true;
|
sampleEncryptionDataNeedsFill = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fills {@link #sampleEncryptionData} from the provided input.
|
||||||
|
*
|
||||||
|
* @param input An {@link ExtractorInput} from which to read the encryption data.
|
||||||
|
*/
|
||||||
|
public void fillEncryptionData(ExtractorInput input) throws IOException, InterruptedException {
|
||||||
|
input.readFully(sampleEncryptionData.data, 0, sampleEncryptionDataLength);
|
||||||
|
sampleEncryptionData.setPosition(0);
|
||||||
|
sampleEncryptionDataNeedsFill = false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fills {@link #sampleEncryptionData} from the provided source.
|
* Fills {@link #sampleEncryptionData} from the provided source.
|
||||||
*
|
*
|
||||||
|
|
@ -13,18 +13,20 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.mp4;
|
package com.google.android.exoplayer.extractor.mp4;
|
||||||
|
|
||||||
import com.google.android.exoplayer.C;
|
import com.google.android.exoplayer.C;
|
||||||
import com.google.android.exoplayer.util.Assertions;
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
import com.google.android.exoplayer.util.Util;
|
import com.google.android.exoplayer.util.Util;
|
||||||
|
|
||||||
/** Sample table for a track in an MP4 file. */
|
/** Sample table for a track in an MP4 file. */
|
||||||
public final class Mp4TrackSampleTable {
|
public final class TrackSampleTable {
|
||||||
|
|
||||||
/** Sample index when no sample is available. */
|
/** Sample index when no sample is available. */
|
||||||
public static final int NO_SAMPLE = -1;
|
public static final int NO_SAMPLE = -1;
|
||||||
|
|
||||||
|
/** Number of samples. */
|
||||||
|
public final int sampleCount;
|
||||||
/** Sample offsets in bytes. */
|
/** Sample offsets in bytes. */
|
||||||
public final long[] offsets;
|
public final long[] offsets;
|
||||||
/** Sample sizes in bytes. */
|
/** Sample sizes in bytes. */
|
||||||
|
|
@ -34,7 +36,7 @@ public final class Mp4TrackSampleTable {
|
||||||
/** Sample flags. */
|
/** Sample flags. */
|
||||||
public final int[] flags;
|
public final int[] flags;
|
||||||
|
|
||||||
Mp4TrackSampleTable(
|
TrackSampleTable(
|
||||||
long[] offsets, int[] sizes, long[] timestampsUs, int[] flags) {
|
long[] offsets, int[] sizes, long[] timestampsUs, int[] flags) {
|
||||||
Assertions.checkArgument(sizes.length == timestampsUs.length);
|
Assertions.checkArgument(sizes.length == timestampsUs.length);
|
||||||
Assertions.checkArgument(offsets.length == timestampsUs.length);
|
Assertions.checkArgument(offsets.length == timestampsUs.length);
|
||||||
|
|
@ -44,11 +46,7 @@ public final class Mp4TrackSampleTable {
|
||||||
this.sizes = sizes;
|
this.sizes = sizes;
|
||||||
this.timestampsUs = timestampsUs;
|
this.timestampsUs = timestampsUs;
|
||||||
this.flags = flags;
|
this.flags = flags;
|
||||||
}
|
sampleCount = offsets.length;
|
||||||
|
|
||||||
/** Returns the number of samples in the table. */
|
|
||||||
public int getSampleCount() {
|
|
||||||
return sizes.length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -65,7 +63,6 @@ public final class Mp4TrackSampleTable {
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return NO_SAMPLE;
|
return NO_SAMPLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,7 +80,6 @@ public final class Mp4TrackSampleTable {
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return NO_SAMPLE;
|
return NO_SAMPLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer.extractor.ts;
|
||||||
import com.google.android.exoplayer.extractor.Extractor;
|
import com.google.android.exoplayer.extractor.Extractor;
|
||||||
import com.google.android.exoplayer.extractor.ExtractorInput;
|
import com.google.android.exoplayer.extractor.ExtractorInput;
|
||||||
import com.google.android.exoplayer.extractor.ExtractorOutput;
|
import com.google.android.exoplayer.extractor.ExtractorOutput;
|
||||||
|
import com.google.android.exoplayer.extractor.PositionHolder;
|
||||||
import com.google.android.exoplayer.util.ParsableByteArray;
|
import com.google.android.exoplayer.util.ParsableByteArray;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
@ -55,7 +56,8 @@ public class AdtsExtractor implements Extractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int read(ExtractorInput input) throws IOException, InterruptedException {
|
public int read(ExtractorInput input, PositionHolder seekPosition)
|
||||||
|
throws IOException, InterruptedException {
|
||||||
int bytesRead = input.read(packetBuffer.data, 0, MAX_PACKET_SIZE);
|
int bytesRead = input.read(packetBuffer.data, 0, MAX_PACKET_SIZE);
|
||||||
if (bytesRead == -1) {
|
if (bytesRead == -1) {
|
||||||
return RESULT_END_OF_INPUT;
|
return RESULT_END_OF_INPUT;
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import com.google.android.exoplayer.C;
|
||||||
import com.google.android.exoplayer.extractor.Extractor;
|
import com.google.android.exoplayer.extractor.Extractor;
|
||||||
import com.google.android.exoplayer.extractor.ExtractorInput;
|
import com.google.android.exoplayer.extractor.ExtractorInput;
|
||||||
import com.google.android.exoplayer.extractor.ExtractorOutput;
|
import com.google.android.exoplayer.extractor.ExtractorOutput;
|
||||||
|
import com.google.android.exoplayer.extractor.PositionHolder;
|
||||||
import com.google.android.exoplayer.util.ParsableBitArray;
|
import com.google.android.exoplayer.util.ParsableBitArray;
|
||||||
import com.google.android.exoplayer.util.ParsableByteArray;
|
import com.google.android.exoplayer.util.ParsableByteArray;
|
||||||
|
|
||||||
|
|
@ -77,7 +78,8 @@ public final class TsExtractor implements Extractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int read(ExtractorInput input) throws IOException, InterruptedException {
|
public int read(ExtractorInput input, PositionHolder seekPosition)
|
||||||
|
throws IOException, InterruptedException {
|
||||||
if (!input.readFully(tsPacketBuffer.data, 0, TS_PACKET_SIZE, true)) {
|
if (!input.readFully(tsPacketBuffer.data, 0, TS_PACKET_SIZE, true)) {
|
||||||
return RESULT_END_OF_INPUT;
|
return RESULT_END_OF_INPUT;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import com.google.android.exoplayer.extractor.ChunkIndex;
|
||||||
import com.google.android.exoplayer.extractor.Extractor;
|
import com.google.android.exoplayer.extractor.Extractor;
|
||||||
import com.google.android.exoplayer.extractor.ExtractorInput;
|
import com.google.android.exoplayer.extractor.ExtractorInput;
|
||||||
import com.google.android.exoplayer.extractor.ExtractorOutput;
|
import com.google.android.exoplayer.extractor.ExtractorOutput;
|
||||||
|
import com.google.android.exoplayer.extractor.PositionHolder;
|
||||||
import com.google.android.exoplayer.extractor.TrackOutput;
|
import com.google.android.exoplayer.extractor.TrackOutput;
|
||||||
import com.google.android.exoplayer.util.LongArray;
|
import com.google.android.exoplayer.util.LongArray;
|
||||||
import com.google.android.exoplayer.util.MimeTypes;
|
import com.google.android.exoplayer.util.MimeTypes;
|
||||||
|
|
@ -162,7 +163,8 @@ public final class WebmExtractor implements Extractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int read(ExtractorInput input) throws IOException, InterruptedException {
|
public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException,
|
||||||
|
InterruptedException {
|
||||||
sampleRead = false;
|
sampleRead = false;
|
||||||
boolean inputHasData = true;
|
boolean inputHasData = true;
|
||||||
while (!sampleRead && inputHasData) {
|
while (!sampleRead && inputHasData) {
|
||||||
|
|
|
||||||
|
|
@ -193,7 +193,8 @@ public final class HlsExtractorWrapper implements ExtractorOutput {
|
||||||
* @throws InterruptedException If the thread was interrupted.
|
* @throws InterruptedException If the thread was interrupted.
|
||||||
*/
|
*/
|
||||||
public int read(ExtractorInput input) throws IOException, InterruptedException {
|
public int read(ExtractorInput input) throws IOException, InterruptedException {
|
||||||
int result = extractor.read(input);
|
int result = extractor.read(input, null);
|
||||||
|
Assertions.checkState(result != Extractor.RESULT_SEEK);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,200 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (C) 2014 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.exoplayer.mp4;
|
|
||||||
|
|
||||||
import com.google.android.exoplayer.util.Assertions;
|
|
||||||
import com.google.android.exoplayer.util.ParsableByteArray;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public abstract class Atom {
|
|
||||||
|
|
||||||
/** Size of an atom header, in bytes. */
|
|
||||||
public static final int ATOM_HEADER_SIZE = 8;
|
|
||||||
|
|
||||||
/** Size of a long atom header, in bytes. */
|
|
||||||
public static final int LONG_ATOM_HEADER_SIZE = 16;
|
|
||||||
|
|
||||||
/** Size of a full atom header, in bytes. */
|
|
||||||
public static final int FULL_ATOM_HEADER_SIZE = 12;
|
|
||||||
|
|
||||||
/** 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 = getAtomTypeInteger("ftyp");
|
|
||||||
public static final int TYPE_avc1 = getAtomTypeInteger("avc1");
|
|
||||||
public static final int TYPE_avc3 = getAtomTypeInteger("avc3");
|
|
||||||
public static final int TYPE_esds = getAtomTypeInteger("esds");
|
|
||||||
public static final int TYPE_mdat = getAtomTypeInteger("mdat");
|
|
||||||
public static final int TYPE_mp4a = getAtomTypeInteger("mp4a");
|
|
||||||
public static final int TYPE_ac_3 = getAtomTypeInteger("ac-3");
|
|
||||||
public static final int TYPE_dac3 = getAtomTypeInteger("dac3");
|
|
||||||
public static final int TYPE_ec_3 = getAtomTypeInteger("ec-3");
|
|
||||||
public static final int TYPE_dec3 = getAtomTypeInteger("dec3");
|
|
||||||
public static final int TYPE_tfdt = getAtomTypeInteger("tfdt");
|
|
||||||
public static final int TYPE_tfhd = getAtomTypeInteger("tfhd");
|
|
||||||
public static final int TYPE_trex = getAtomTypeInteger("trex");
|
|
||||||
public static final int TYPE_trun = getAtomTypeInteger("trun");
|
|
||||||
public static final int TYPE_sidx = getAtomTypeInteger("sidx");
|
|
||||||
public static final int TYPE_moov = getAtomTypeInteger("moov");
|
|
||||||
public static final int TYPE_mvhd = getAtomTypeInteger("mvhd");
|
|
||||||
public static final int TYPE_trak = getAtomTypeInteger("trak");
|
|
||||||
public static final int TYPE_mdia = getAtomTypeInteger("mdia");
|
|
||||||
public static final int TYPE_minf = getAtomTypeInteger("minf");
|
|
||||||
public static final int TYPE_stbl = getAtomTypeInteger("stbl");
|
|
||||||
public static final int TYPE_avcC = getAtomTypeInteger("avcC");
|
|
||||||
public static final int TYPE_moof = getAtomTypeInteger("moof");
|
|
||||||
public static final int TYPE_traf = getAtomTypeInteger("traf");
|
|
||||||
public static final int TYPE_mvex = getAtomTypeInteger("mvex");
|
|
||||||
public static final int TYPE_tkhd = getAtomTypeInteger("tkhd");
|
|
||||||
public static final int TYPE_mdhd = getAtomTypeInteger("mdhd");
|
|
||||||
public static final int TYPE_hdlr = getAtomTypeInteger("hdlr");
|
|
||||||
public static final int TYPE_stsd = getAtomTypeInteger("stsd");
|
|
||||||
public static final int TYPE_pssh = getAtomTypeInteger("pssh");
|
|
||||||
public static final int TYPE_sinf = getAtomTypeInteger("sinf");
|
|
||||||
public static final int TYPE_schm = getAtomTypeInteger("schm");
|
|
||||||
public static final int TYPE_schi = getAtomTypeInteger("schi");
|
|
||||||
public static final int TYPE_tenc = getAtomTypeInteger("tenc");
|
|
||||||
public static final int TYPE_encv = getAtomTypeInteger("encv");
|
|
||||||
public static final int TYPE_enca = getAtomTypeInteger("enca");
|
|
||||||
public static final int TYPE_frma = getAtomTypeInteger("frma");
|
|
||||||
public static final int TYPE_saiz = getAtomTypeInteger("saiz");
|
|
||||||
public static final int TYPE_uuid = getAtomTypeInteger("uuid");
|
|
||||||
public static final int TYPE_senc = getAtomTypeInteger("senc");
|
|
||||||
public static final int TYPE_pasp = getAtomTypeInteger("pasp");
|
|
||||||
public static final int TYPE_TTML = getAtomTypeInteger("TTML");
|
|
||||||
public static final int TYPE_vmhd = getAtomTypeInteger("vmhd");
|
|
||||||
public static final int TYPE_smhd = getAtomTypeInteger("smhd");
|
|
||||||
public static final int TYPE_mp4v = getAtomTypeInteger("mp4v");
|
|
||||||
public static final int TYPE_stts = getAtomTypeInteger("stts");
|
|
||||||
public static final int TYPE_stss = getAtomTypeInteger("stss");
|
|
||||||
public static final int TYPE_ctts = getAtomTypeInteger("ctts");
|
|
||||||
public static final int TYPE_stsc = getAtomTypeInteger("stsc");
|
|
||||||
public static final int TYPE_stsz = getAtomTypeInteger("stsz");
|
|
||||||
public static final int TYPE_stco = getAtomTypeInteger("stco");
|
|
||||||
public static final int TYPE_co64 = getAtomTypeInteger("co64");
|
|
||||||
|
|
||||||
public final int type;
|
|
||||||
|
|
||||||
Atom(int type) {
|
|
||||||
this.type = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return getAtomTypeString(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** An MP4 atom that is a leaf. */
|
|
||||||
public static final class LeafAtom extends Atom {
|
|
||||||
|
|
||||||
public final ParsableByteArray data;
|
|
||||||
|
|
||||||
public LeafAtom(int type, ParsableByteArray data) {
|
|
||||||
super(type);
|
|
||||||
this.data = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/** An MP4 atom that has child atoms. */
|
|
||||||
public static final class ContainerAtom extends Atom {
|
|
||||||
|
|
||||||
public final long endByteOffset;
|
|
||||||
public final List<LeafAtom> leafChildren;
|
|
||||||
public final List<ContainerAtom> containerChildren;
|
|
||||||
|
|
||||||
public ContainerAtom(int type, long endByteOffset) {
|
|
||||||
super(type);
|
|
||||||
|
|
||||||
leafChildren = new ArrayList<LeafAtom>();
|
|
||||||
containerChildren = new ArrayList<ContainerAtom>();
|
|
||||||
this.endByteOffset = endByteOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void add(LeafAtom atom) {
|
|
||||||
leafChildren.add(atom);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void add(ContainerAtom atom) {
|
|
||||||
containerChildren.add(atom);
|
|
||||||
}
|
|
||||||
|
|
||||||
public LeafAtom getLeafAtomOfType(int type) {
|
|
||||||
int childrenSize = leafChildren.size();
|
|
||||||
for (int i = 0; i < childrenSize; i++) {
|
|
||||||
LeafAtom atom = leafChildren.get(i);
|
|
||||||
if (atom.type == type) {
|
|
||||||
return atom;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ContainerAtom getContainerAtomOfType(int type) {
|
|
||||||
int childrenSize = containerChildren.size();
|
|
||||||
for (int i = 0; i < childrenSize; i++) {
|
|
||||||
ContainerAtom atom = containerChildren.get(i);
|
|
||||||
if (atom.type == type) {
|
|
||||||
return atom;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return getAtomTypeString(type)
|
|
||||||
+ " leaves: " + Arrays.toString(leafChildren.toArray(new LeafAtom[0]))
|
|
||||||
+ " containers: " + Arrays.toString(containerChildren.toArray(new ContainerAtom[0]));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the version number out of the additional integer component of a full atom.
|
|
||||||
*/
|
|
||||||
public static int parseFullAtomVersion(int fullAtomInt) {
|
|
||||||
return 0x000000FF & (fullAtomInt >> 24);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the atom flags out of the additional integer component of a full atom.
|
|
||||||
*/
|
|
||||||
public static int parseFullAtomFlags(int fullAtomInt) {
|
|
||||||
return 0x00FFFFFF & fullAtomInt;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getAtomTypeString(int type) {
|
|
||||||
return "" + (char) (type >> 24)
|
|
||||||
+ (char) ((type >> 16) & 0xFF)
|
|
||||||
+ (char) ((type >> 8) & 0xFF)
|
|
||||||
+ (char) (type & 0xFF);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int getAtomTypeInteger(String typeName) {
|
|
||||||
Assertions.checkArgument(typeName.length() == 4);
|
|
||||||
int result = 0;
|
|
||||||
for (int i = 0; i < 4; i++) {
|
|
||||||
result <<= 8;
|
|
||||||
result |= typeName.charAt(i);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -29,9 +29,9 @@ import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation;
|
||||||
import com.google.android.exoplayer.chunk.MediaChunk;
|
import com.google.android.exoplayer.chunk.MediaChunk;
|
||||||
import com.google.android.exoplayer.chunk.parser.Extractor;
|
import com.google.android.exoplayer.chunk.parser.Extractor;
|
||||||
import com.google.android.exoplayer.chunk.parser.mp4.FragmentedMp4Extractor;
|
import com.google.android.exoplayer.chunk.parser.mp4.FragmentedMp4Extractor;
|
||||||
import com.google.android.exoplayer.chunk.parser.mp4.TrackEncryptionBox;
|
|
||||||
import com.google.android.exoplayer.drm.DrmInitData;
|
import com.google.android.exoplayer.drm.DrmInitData;
|
||||||
import com.google.android.exoplayer.mp4.Track;
|
import com.google.android.exoplayer.extractor.mp4.Track;
|
||||||
|
import com.google.android.exoplayer.extractor.mp4.TrackEncryptionBox;
|
||||||
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.ProtectionElement;
|
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.ProtectionElement;
|
||||||
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement;
|
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement;
|
||||||
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement;
|
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement;
|
||||||
|
|
|
||||||
|
|
@ -1,743 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (C) 2014 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.exoplayer.source;
|
|
||||||
|
|
||||||
import com.google.android.exoplayer.C;
|
|
||||||
import com.google.android.exoplayer.MediaFormat;
|
|
||||||
import com.google.android.exoplayer.SampleHolder;
|
|
||||||
import com.google.android.exoplayer.SampleSource;
|
|
||||||
import com.google.android.exoplayer.TrackRenderer;
|
|
||||||
import com.google.android.exoplayer.drm.DrmInitData;
|
|
||||||
import com.google.android.exoplayer.mp4.Atom;
|
|
||||||
import com.google.android.exoplayer.mp4.Atom.ContainerAtom;
|
|
||||||
import com.google.android.exoplayer.mp4.CommonMp4AtomParsers;
|
|
||||||
import com.google.android.exoplayer.mp4.Mp4TrackSampleTable;
|
|
||||||
import com.google.android.exoplayer.mp4.Track;
|
|
||||||
import com.google.android.exoplayer.upstream.BufferPool;
|
|
||||||
import com.google.android.exoplayer.upstream.BufferedNonBlockingInputStream;
|
|
||||||
import com.google.android.exoplayer.upstream.DataSource;
|
|
||||||
import com.google.android.exoplayer.upstream.DataSourceStream;
|
|
||||||
import com.google.android.exoplayer.upstream.DataSpec;
|
|
||||||
import com.google.android.exoplayer.upstream.Loader;
|
|
||||||
import com.google.android.exoplayer.upstream.Loader.Loadable;
|
|
||||||
import com.google.android.exoplayer.util.Assertions;
|
|
||||||
import com.google.android.exoplayer.util.H264Util;
|
|
||||||
import com.google.android.exoplayer.util.MimeTypes;
|
|
||||||
import com.google.android.exoplayer.util.ParsableByteArray;
|
|
||||||
import com.google.android.exoplayer.util.Util;
|
|
||||||
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.Stack;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts data from a {@link DataSpec} in unfragmented MP4 format (ISO 14496-12).
|
|
||||||
*/
|
|
||||||
public final class Mp4SampleExtractor implements SampleExtractor, Loader.Callback {
|
|
||||||
|
|
||||||
private static final String TAG = "Mp4SampleExtractor";
|
|
||||||
private static final String LOADER_THREAD_NAME = "Mp4SampleExtractor";
|
|
||||||
|
|
||||||
private static final int NO_TRACK = -1;
|
|
||||||
|
|
||||||
// Reading results
|
|
||||||
private static final int RESULT_NEED_MORE_DATA = 1;
|
|
||||||
private static final int RESULT_END_OF_STREAM = 2;
|
|
||||||
|
|
||||||
// Parser states
|
|
||||||
private static final int STATE_READING_ATOM_HEADER = 0;
|
|
||||||
private static final int STATE_READING_ATOM_PAYLOAD = 1;
|
|
||||||
|
|
||||||
/** Set of atom types that contain data to be parsed. */
|
|
||||||
private static final Set<Integer> LEAF_ATOM_TYPES = getAtomTypeSet(
|
|
||||||
Atom.TYPE_mdhd, Atom.TYPE_mvhd, Atom.TYPE_hdlr, Atom.TYPE_vmhd, Atom.TYPE_smhd,
|
|
||||||
Atom.TYPE_stsd, Atom.TYPE_avc1, Atom.TYPE_avcC, Atom.TYPE_mp4a, Atom.TYPE_esds,
|
|
||||||
Atom.TYPE_stts, Atom.TYPE_stss, Atom.TYPE_ctts, Atom.TYPE_stsc, Atom.TYPE_stsz,
|
|
||||||
Atom.TYPE_stco, Atom.TYPE_co64, Atom.TYPE_tkhd);
|
|
||||||
|
|
||||||
/** Set of atom types that contain other atoms that need to be parsed. */
|
|
||||||
private static final Set<Integer> CONTAINER_TYPES = getAtomTypeSet(
|
|
||||||
Atom.TYPE_moov, Atom.TYPE_trak, Atom.TYPE_mdia, Atom.TYPE_minf, Atom.TYPE_stbl);
|
|
||||||
|
|
||||||
/** Default number of times to retry loading data prior to failing. */
|
|
||||||
private static final int DEFAULT_LOADABLE_RETRY_COUNT = 3;
|
|
||||||
|
|
||||||
private final DataSource dataSource;
|
|
||||||
private final DataSpec dataSpec;
|
|
||||||
|
|
||||||
private final int readAheadAllocationSize;
|
|
||||||
private final int reloadMinimumSeekDistance;
|
|
||||||
private final int maximumTrackSampleInterval;
|
|
||||||
private final int loadRetryCount;
|
|
||||||
|
|
||||||
private final BufferPool bufferPool;
|
|
||||||
private final Loader loader;
|
|
||||||
private final ParsableByteArray atomHeader;
|
|
||||||
private final Stack<Atom.ContainerAtom> containerAtoms;
|
|
||||||
|
|
||||||
private DataSourceStream dataSourceStream;
|
|
||||||
private BufferedNonBlockingInputStream inputStream;
|
|
||||||
private long inputStreamOffset;
|
|
||||||
private long rootAtomBytesRead;
|
|
||||||
private boolean loadCompleted;
|
|
||||||
|
|
||||||
private int parserState;
|
|
||||||
private int atomBytesRead;
|
|
||||||
private int atomType;
|
|
||||||
private long atomSize;
|
|
||||||
private ParsableByteArray atomData;
|
|
||||||
|
|
||||||
private boolean prepared;
|
|
||||||
|
|
||||||
private int loadErrorCount;
|
|
||||||
|
|
||||||
private Mp4Track[] tracks;
|
|
||||||
|
|
||||||
/** An exception from {@link #inputStream}'s callbacks, or {@code null} if there was no error. */
|
|
||||||
private IOException lastLoadError;
|
|
||||||
private long loadErrorPosition;
|
|
||||||
|
|
||||||
/** If handling a call to {@link #seekTo}, the new required stream offset, or -1 otherwise. */
|
|
||||||
private long pendingSeekPosition;
|
|
||||||
/** If the input stream is being reopened at a new position, the new offset, or -1 otherwise. */
|
|
||||||
private long pendingLoadPosition;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new sample extractor for reading {@code dataSource} and {@code dataSpec} as an
|
|
||||||
* unfragmented MP4 file with default settings.
|
|
||||||
*
|
|
||||||
* <p>The default settings read ahead by 5 MiB, handle maximum offsets between samples at the same
|
|
||||||
* timestamp in different tracks of 3 MiB and restart loading when seeking forward by >= 256 KiB.
|
|
||||||
*
|
|
||||||
* @param dataSource Data source used to read from {@code dataSpec}.
|
|
||||||
* @param dataSpec Data specification specifying what to read.
|
|
||||||
*/
|
|
||||||
public Mp4SampleExtractor(DataSource dataSource, DataSpec dataSpec) {
|
|
||||||
this(dataSource, dataSpec, 5 * 1024 * 1024, 3 * 1024 * 1024, 256 * 1024,
|
|
||||||
DEFAULT_LOADABLE_RETRY_COUNT);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new sample extractor for reading {@code dataSource} and {@code dataSpec} as an
|
|
||||||
* unfragmented MP4 file.
|
|
||||||
*
|
|
||||||
* @param dataSource Data source used to read from {@code dataSpec}.
|
|
||||||
* @param dataSpec Data specification specifying what to read.
|
|
||||||
* @param readAheadAllocationSize Size of the allocation that buffers the stream, in bytes. The
|
|
||||||
* value must exceed the maximum sample size, so that a sample can be read in its entirety.
|
|
||||||
* @param maximumTrackSampleInterval Size of the buffer that handles reading from any selected
|
|
||||||
* track. The value should be chosen so that the buffer is as big as the interval in bytes
|
|
||||||
* between the start of the earliest and the end of the latest sample required to render media
|
|
||||||
* from all selected tracks, at any timestamp in the data source.
|
|
||||||
* @param reloadMinimumSeekDistance Determines when {@code dataSource} is reopened while seeking:
|
|
||||||
* if the number of bytes between the current position and the new position is greater than or
|
|
||||||
* equal to this value, or the new position is before the current position, loading will
|
|
||||||
* restart. The value should be set to the number of bytes that can be loaded/consumed from an
|
|
||||||
* existing connection in the time it takes to start a new connection.
|
|
||||||
* @param loadableRetryCount The number of times to retry loading if an error occurs.
|
|
||||||
*/
|
|
||||||
public Mp4SampleExtractor(DataSource dataSource, DataSpec dataSpec, int readAheadAllocationSize,
|
|
||||||
int maximumTrackSampleInterval, int reloadMinimumSeekDistance, int loadableRetryCount) {
|
|
||||||
// TODO: Handle minimumTrackSampleInterval specified in time not bytes.
|
|
||||||
this.dataSource = Assertions.checkNotNull(dataSource);
|
|
||||||
this.dataSpec = Assertions.checkNotNull(dataSpec);
|
|
||||||
this.readAheadAllocationSize = readAheadAllocationSize;
|
|
||||||
this.maximumTrackSampleInterval = maximumTrackSampleInterval;
|
|
||||||
this.reloadMinimumSeekDistance = reloadMinimumSeekDistance;
|
|
||||||
this.loadRetryCount = loadableRetryCount;
|
|
||||||
|
|
||||||
// TODO: Implement Allocator here so it is possible to check there is only one buffer at a time.
|
|
||||||
bufferPool = new BufferPool(readAheadAllocationSize);
|
|
||||||
loader = new Loader(LOADER_THREAD_NAME);
|
|
||||||
atomHeader = new ParsableByteArray(Atom.LONG_ATOM_HEADER_SIZE);
|
|
||||||
containerAtoms = new Stack<Atom.ContainerAtom>();
|
|
||||||
|
|
||||||
parserState = STATE_READING_ATOM_HEADER;
|
|
||||||
pendingLoadPosition = -1;
|
|
||||||
pendingSeekPosition = -1;
|
|
||||||
loadErrorPosition = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean prepare() throws IOException {
|
|
||||||
if (inputStream == null) {
|
|
||||||
loadFromOffset(0L);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!prepared) {
|
|
||||||
if (readHeaders() && !prepared) {
|
|
||||||
throw new IOException("moov atom not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!prepared) {
|
|
||||||
maybeThrowLoadError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return prepared;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void selectTrack(int trackIndex) {
|
|
||||||
Assertions.checkState(prepared);
|
|
||||||
|
|
||||||
if (tracks[trackIndex].selected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
tracks[trackIndex].selected = true;
|
|
||||||
|
|
||||||
// Get the timestamp of the earliest currently-selected sample.
|
|
||||||
int earliestSampleTrackIndex = getTrackIndexOfEarliestCurrentSample();
|
|
||||||
if (earliestSampleTrackIndex == NO_TRACK) {
|
|
||||||
tracks[trackIndex].sampleIndex = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (earliestSampleTrackIndex == Mp4TrackSampleTable.NO_SAMPLE) {
|
|
||||||
tracks[trackIndex].sampleIndex = Mp4TrackSampleTable.NO_SAMPLE;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
long timestampUs =
|
|
||||||
tracks[earliestSampleTrackIndex].sampleTable.timestampsUs[earliestSampleTrackIndex];
|
|
||||||
|
|
||||||
// Find the latest sync sample in the new track that has an earlier or equal timestamp.
|
|
||||||
tracks[trackIndex].sampleIndex =
|
|
||||||
tracks[trackIndex].sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(timestampUs);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void deselectTrack(int trackIndex) {
|
|
||||||
Assertions.checkState(prepared);
|
|
||||||
|
|
||||||
tracks[trackIndex].selected = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getBufferedPositionUs() {
|
|
||||||
Assertions.checkState(prepared);
|
|
||||||
|
|
||||||
if (pendingLoadPosition != -1) {
|
|
||||||
return TrackRenderer.UNKNOWN_TIME_US;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loadCompleted) {
|
|
||||||
return TrackRenderer.END_OF_TRACK_US;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the absolute position to which there is data buffered.
|
|
||||||
long bufferedPosition =
|
|
||||||
inputStreamOffset + inputStream.getReadPosition() + inputStream.getAvailableByteCount();
|
|
||||||
|
|
||||||
// Find the timestamp of the latest sample that does not exceed the buffered position.
|
|
||||||
long latestTimestampBeforeEnd = Long.MIN_VALUE;
|
|
||||||
for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) {
|
|
||||||
if (!tracks[trackIndex].selected) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Mp4TrackSampleTable sampleTable = tracks[trackIndex].sampleTable;
|
|
||||||
int sampleIndex = Util.binarySearchFloor(sampleTable.offsets, bufferedPosition, false, true);
|
|
||||||
if (sampleIndex > 0
|
|
||||||
&& sampleTable.offsets[sampleIndex] + sampleTable.sizes[sampleIndex] > bufferedPosition) {
|
|
||||||
sampleIndex--;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the latest timestamp if this is greater.
|
|
||||||
long timestamp = sampleTable.timestampsUs[sampleIndex];
|
|
||||||
if (timestamp > latestTimestampBeforeEnd) {
|
|
||||||
latestTimestampBeforeEnd = timestamp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return latestTimestampBeforeEnd < 0L ? C.UNKNOWN_TIME_US : latestTimestampBeforeEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void seekTo(long positionUs) {
|
|
||||||
Assertions.checkState(prepared);
|
|
||||||
|
|
||||||
long earliestSamplePosition = Long.MAX_VALUE;
|
|
||||||
for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) {
|
|
||||||
if (!tracks[trackIndex].selected) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Mp4TrackSampleTable sampleTable = tracks[trackIndex].sampleTable;
|
|
||||||
int sampleIndex = sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(positionUs);
|
|
||||||
if (sampleIndex == Mp4TrackSampleTable.NO_SAMPLE) {
|
|
||||||
sampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(positionUs);
|
|
||||||
}
|
|
||||||
tracks[trackIndex].sampleIndex = sampleIndex;
|
|
||||||
|
|
||||||
long offset = sampleTable.offsets[tracks[trackIndex].sampleIndex];
|
|
||||||
if (offset < earliestSamplePosition) {
|
|
||||||
earliestSamplePosition = offset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingSeekPosition = earliestSamplePosition;
|
|
||||||
if (pendingLoadPosition != -1) {
|
|
||||||
loadFromOffset(earliestSamplePosition);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
inputStream.returnToMark();
|
|
||||||
long earliestOffset = inputStreamOffset + inputStream.getReadPosition();
|
|
||||||
long latestOffset = earliestOffset + inputStream.getAvailableByteCount();
|
|
||||||
if (earliestSamplePosition < earliestOffset
|
|
||||||
|| earliestSamplePosition >= latestOffset + reloadMinimumSeekDistance) {
|
|
||||||
loadFromOffset(earliestSamplePosition);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getTrackCount() {
|
|
||||||
Assertions.checkState(prepared);
|
|
||||||
return tracks.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MediaFormat getMediaFormat(int track) {
|
|
||||||
Assertions.checkState(prepared);
|
|
||||||
return tracks[track].track.mediaFormat;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DrmInitData getDrmInitData(int track) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int readSample(int trackIndex, SampleHolder sampleHolder) throws IOException {
|
|
||||||
Assertions.checkState(prepared);
|
|
||||||
|
|
||||||
Mp4Track track = tracks[trackIndex];
|
|
||||||
Assertions.checkState(track.selected);
|
|
||||||
int sampleIndex = track.sampleIndex;
|
|
||||||
|
|
||||||
// Check for the end of the stream.
|
|
||||||
if (sampleIndex == Mp4TrackSampleTable.NO_SAMPLE) {
|
|
||||||
// TODO: Should END_OF_STREAM be returned as soon as this track has no more samples, or as
|
|
||||||
// soon as no tracks have a sample (as implemented here)?
|
|
||||||
return hasSampleInAnySelectedTrack() ? SampleSource.NOTHING_READ : SampleSource.END_OF_STREAM;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return if the input stream will be reopened at the requested position.
|
|
||||||
if (pendingLoadPosition != -1) {
|
|
||||||
return SampleSource.NOTHING_READ;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there was a seek request, try to skip forwards to the requested position.
|
|
||||||
if (pendingSeekPosition != -1) {
|
|
||||||
int bytesToSeekPosition =
|
|
||||||
(int) (pendingSeekPosition - (inputStreamOffset + inputStream.getReadPosition()));
|
|
||||||
int skippedByteCount = inputStream.skip(bytesToSeekPosition);
|
|
||||||
if (skippedByteCount == -1) {
|
|
||||||
throw new IOException("Unexpected end-of-stream while seeking to sample.");
|
|
||||||
}
|
|
||||||
bytesToSeekPosition -= skippedByteCount;
|
|
||||||
inputStream.mark();
|
|
||||||
if (bytesToSeekPosition == 0) {
|
|
||||||
pendingSeekPosition = -1;
|
|
||||||
} else {
|
|
||||||
maybeThrowLoadError();
|
|
||||||
return SampleSource.NOTHING_READ;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return if the sample offset hasn't been loaded yet.
|
|
||||||
inputStream.returnToMark();
|
|
||||||
long sampleOffset = track.sampleTable.offsets[sampleIndex];
|
|
||||||
long seekOffsetLong = (sampleOffset - inputStreamOffset) - inputStream.getReadPosition();
|
|
||||||
Assertions.checkState(seekOffsetLong <= Integer.MAX_VALUE);
|
|
||||||
int seekOffset = (int) seekOffsetLong;
|
|
||||||
if (inputStream.skip(seekOffset) != seekOffset) {
|
|
||||||
maybeThrowLoadError();
|
|
||||||
return SampleSource.NOTHING_READ;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return if the sample has been loaded.
|
|
||||||
int sampleSize = track.sampleTable.sizes[sampleIndex];
|
|
||||||
if (inputStream.getAvailableByteCount() < sampleSize) {
|
|
||||||
maybeThrowLoadError();
|
|
||||||
return SampleSource.NOTHING_READ;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sampleHolder.data == null || sampleHolder.data.capacity() < sampleSize) {
|
|
||||||
sampleHolder.replaceBuffer(sampleSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
ByteBuffer data = sampleHolder.data;
|
|
||||||
if (data == null) {
|
|
||||||
inputStream.skip(sampleSize);
|
|
||||||
sampleHolder.size = 0;
|
|
||||||
} else {
|
|
||||||
int bytesRead = inputStream.read(data, sampleSize);
|
|
||||||
Assertions.checkState(bytesRead == sampleSize);
|
|
||||||
|
|
||||||
if (MimeTypes.VIDEO_H264.equals(tracks[trackIndex].track.mediaFormat.mimeType)) {
|
|
||||||
// The mp4 file contains length-prefixed access units, but the decoder wants start code
|
|
||||||
// delimited content.
|
|
||||||
H264Util.replaceLengthPrefixesWithAvcStartCodes(sampleHolder.data, sampleSize);
|
|
||||||
}
|
|
||||||
sampleHolder.size = sampleSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move the input stream mark forwards if the earliest current sample was just read.
|
|
||||||
if (getTrackIndexOfEarliestCurrentSample() == trackIndex) {
|
|
||||||
inputStream.mark();
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Read encryption data.
|
|
||||||
sampleHolder.timeUs = track.sampleTable.timestampsUs[sampleIndex];
|
|
||||||
sampleHolder.flags = track.sampleTable.flags[sampleIndex];
|
|
||||||
|
|
||||||
// Advance to the next sample, checking if this was the last sample.
|
|
||||||
track.sampleIndex =
|
|
||||||
sampleIndex + 1 == track.sampleTable.getSampleCount() ? Mp4TrackSampleTable.NO_SAMPLE : sampleIndex + 1;
|
|
||||||
|
|
||||||
// Reset the loading error counter if we read past the offset at which the error was thrown.
|
|
||||||
if (dataSourceStream.getReadPosition() > loadErrorPosition) {
|
|
||||||
loadErrorCount = 0;
|
|
||||||
loadErrorPosition = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return SampleSource.SAMPLE_READ;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void release() {
|
|
||||||
pendingLoadPosition = -1;
|
|
||||||
loader.release();
|
|
||||||
|
|
||||||
if (inputStream != null) {
|
|
||||||
inputStream.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoadError(Loadable loadable, IOException exception) {
|
|
||||||
lastLoadError = exception;
|
|
||||||
|
|
||||||
loadErrorCount++;
|
|
||||||
if (loadErrorPosition == -1) {
|
|
||||||
loadErrorPosition = dataSourceStream.getLoadPosition();
|
|
||||||
}
|
|
||||||
int delayMs = getRetryDelayMs(loadErrorCount);
|
|
||||||
Log.w(TAG, "Retry loading (delay " + delayMs + " ms).");
|
|
||||||
loader.startLoading(dataSourceStream, this, delayMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoadCompleted(Loadable loadable) {
|
|
||||||
loadCompleted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoadCanceled(Loadable loadable) {
|
|
||||||
if (pendingLoadPosition != -1) {
|
|
||||||
loadFromOffset(pendingLoadPosition);
|
|
||||||
pendingLoadPosition = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadFromOffset(long offsetBytes) {
|
|
||||||
inputStreamOffset = offsetBytes;
|
|
||||||
rootAtomBytesRead = offsetBytes;
|
|
||||||
|
|
||||||
if (loader.isLoading()) {
|
|
||||||
// Wait for loading to be canceled before proceeding.
|
|
||||||
pendingLoadPosition = offsetBytes;
|
|
||||||
loader.cancelLoading();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputStream != null) {
|
|
||||||
inputStream.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
DataSpec dataSpec = new DataSpec(
|
|
||||||
this.dataSpec.uri, offsetBytes, C.LENGTH_UNBOUNDED, this.dataSpec.key);
|
|
||||||
dataSourceStream =
|
|
||||||
new DataSourceStream(dataSource, dataSpec, bufferPool, readAheadAllocationSize);
|
|
||||||
loader.startLoading(dataSourceStream, this);
|
|
||||||
|
|
||||||
// Wrap the input stream with a buffering stream so that it is possible to read from any track.
|
|
||||||
inputStream =
|
|
||||||
new BufferedNonBlockingInputStream(dataSourceStream, maximumTrackSampleInterval);
|
|
||||||
loadCompleted = false;
|
|
||||||
|
|
||||||
loadErrorCount = 0;
|
|
||||||
loadErrorPosition = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the index of the track that contains the earliest current sample, or {@link #NO_TRACK}
|
|
||||||
* if no track is selected, or {@link Mp4TrackSampleTable#NO_SAMPLE} if no samples remain in
|
|
||||||
* selected tracks.
|
|
||||||
*/
|
|
||||||
private int getTrackIndexOfEarliestCurrentSample() {
|
|
||||||
int earliestSampleTrackIndex = NO_TRACK;
|
|
||||||
long earliestSampleOffset = Long.MAX_VALUE;
|
|
||||||
for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) {
|
|
||||||
Mp4Track track = tracks[trackIndex];
|
|
||||||
if (!track.selected) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
int sampleIndex = track.sampleIndex;
|
|
||||||
if (sampleIndex == Mp4TrackSampleTable.NO_SAMPLE) {
|
|
||||||
if (earliestSampleTrackIndex == NO_TRACK) {
|
|
||||||
// A track is selected, but it has no more samples.
|
|
||||||
earliestSampleTrackIndex = Mp4TrackSampleTable.NO_SAMPLE;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
long trackSampleOffset = track.sampleTable.offsets[sampleIndex];
|
|
||||||
if (trackSampleOffset < earliestSampleOffset) {
|
|
||||||
earliestSampleOffset = trackSampleOffset;
|
|
||||||
earliestSampleTrackIndex = trackIndex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return earliestSampleTrackIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean hasSampleInAnySelectedTrack() {
|
|
||||||
boolean hasSample = false;
|
|
||||||
for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) {
|
|
||||||
if (tracks[trackIndex].selected && tracks[trackIndex].sampleIndex
|
|
||||||
!= Mp4TrackSampleTable.NO_SAMPLE) {
|
|
||||||
hasSample = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return hasSample;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Reads headers, returning whether the end of the stream was reached. */
|
|
||||||
private boolean readHeaders() {
|
|
||||||
int results = 0;
|
|
||||||
while (!prepared && (results & (RESULT_NEED_MORE_DATA | RESULT_END_OF_STREAM)) == 0) {
|
|
||||||
switch (parserState) {
|
|
||||||
case STATE_READING_ATOM_HEADER:
|
|
||||||
results |= readAtomHeader();
|
|
||||||
break;
|
|
||||||
case STATE_READING_ATOM_PAYLOAD:
|
|
||||||
results |= readAtomPayload();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (results & RESULT_END_OF_STREAM) != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int readAtomHeader() {
|
|
||||||
if (pendingLoadPosition != -1) {
|
|
||||||
return RESULT_NEED_MORE_DATA;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The size value is either 4 or 8 bytes long (in which case atomSize = Mp4Util.LONG_ATOM_SIZE).
|
|
||||||
int remainingBytes;
|
|
||||||
if (atomSize != Atom.LONG_SIZE_PREFIX) {
|
|
||||||
remainingBytes = Atom.ATOM_HEADER_SIZE - atomBytesRead;
|
|
||||||
} else {
|
|
||||||
remainingBytes = Atom.LONG_ATOM_HEADER_SIZE - atomBytesRead;
|
|
||||||
}
|
|
||||||
|
|
||||||
int bytesRead = inputStream.read(atomHeader.data, atomBytesRead, remainingBytes);
|
|
||||||
if (bytesRead == -1) {
|
|
||||||
return RESULT_END_OF_STREAM;
|
|
||||||
}
|
|
||||||
rootAtomBytesRead += bytesRead;
|
|
||||||
atomBytesRead += bytesRead;
|
|
||||||
if (atomBytesRead < Atom.ATOM_HEADER_SIZE
|
|
||||||
|| (atomSize == Atom.LONG_SIZE_PREFIX && atomBytesRead < Atom.LONG_ATOM_HEADER_SIZE)) {
|
|
||||||
return RESULT_NEED_MORE_DATA;
|
|
||||||
}
|
|
||||||
|
|
||||||
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.
|
|
||||||
if (atomBytesRead < Atom.LONG_ATOM_HEADER_SIZE) {
|
|
||||||
return readAtomHeader();
|
|
||||||
}
|
|
||||||
|
|
||||||
atomSize = atomHeader.readLong();
|
|
||||||
}
|
|
||||||
|
|
||||||
Integer atomTypeInteger = atomType; // Avoids boxing atomType twice.
|
|
||||||
if (CONTAINER_TYPES.contains(atomTypeInteger)) {
|
|
||||||
if (atomSize == Atom.LONG_SIZE_PREFIX) {
|
|
||||||
containerAtoms.add(new ContainerAtom(
|
|
||||||
atomType, rootAtomBytesRead + atomSize - Atom.LONG_ATOM_HEADER_SIZE));
|
|
||||||
} else {
|
|
||||||
containerAtoms.add(new ContainerAtom(
|
|
||||||
atomType, rootAtomBytesRead + atomSize - Atom.ATOM_HEADER_SIZE));
|
|
||||||
}
|
|
||||||
enterState(STATE_READING_ATOM_HEADER);
|
|
||||||
} else if (LEAF_ATOM_TYPES.contains(atomTypeInteger)) {
|
|
||||||
Assertions.checkState(atomSize <= Integer.MAX_VALUE);
|
|
||||||
atomData = new ParsableByteArray((int) atomSize);
|
|
||||||
System.arraycopy(atomHeader.data, 0, atomData.data, 0, Atom.ATOM_HEADER_SIZE);
|
|
||||||
enterState(STATE_READING_ATOM_PAYLOAD);
|
|
||||||
} else {
|
|
||||||
atomData = null;
|
|
||||||
enterState(STATE_READING_ATOM_PAYLOAD);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int readAtomPayload() {
|
|
||||||
int bytesRead;
|
|
||||||
if (atomData != null) {
|
|
||||||
bytesRead = inputStream.read(atomData.data, atomBytesRead, (int) atomSize - atomBytesRead);
|
|
||||||
} else {
|
|
||||||
if (atomSize >= reloadMinimumSeekDistance || atomSize > Integer.MAX_VALUE) {
|
|
||||||
loadFromOffset(rootAtomBytesRead + atomSize - atomBytesRead);
|
|
||||||
onContainerAtomRead();
|
|
||||||
enterState(STATE_READING_ATOM_HEADER);
|
|
||||||
return 0;
|
|
||||||
} else {
|
|
||||||
bytesRead = inputStream.skip((int) atomSize - atomBytesRead);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (bytesRead == -1) {
|
|
||||||
return RESULT_END_OF_STREAM;
|
|
||||||
}
|
|
||||||
rootAtomBytesRead += bytesRead;
|
|
||||||
atomBytesRead += bytesRead;
|
|
||||||
if (atomBytesRead != atomSize) {
|
|
||||||
return RESULT_NEED_MORE_DATA;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (atomData != null && !containerAtoms.isEmpty()) {
|
|
||||||
containerAtoms.peek().add(new Atom.LeafAtom(atomType, atomData));
|
|
||||||
}
|
|
||||||
|
|
||||||
onContainerAtomRead();
|
|
||||||
|
|
||||||
enterState(STATE_READING_ATOM_HEADER);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onContainerAtomRead() {
|
|
||||||
while (!containerAtoms.isEmpty() && containerAtoms.peek().endByteOffset == rootAtomBytesRead) {
|
|
||||||
Atom.ContainerAtom containerAtom = containerAtoms.pop();
|
|
||||||
if (containerAtom.type == Atom.TYPE_moov) {
|
|
||||||
processMoovAtom(containerAtom);
|
|
||||||
} else if (!containerAtoms.isEmpty()) {
|
|
||||||
containerAtoms.peek().add(containerAtom);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void enterState(int state) {
|
|
||||||
switch (state) {
|
|
||||||
case STATE_READING_ATOM_HEADER:
|
|
||||||
atomBytesRead = 0;
|
|
||||||
atomSize = 0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
parserState = state;
|
|
||||||
inputStream.mark();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Updates the stored track metadata to reflect the contents on the specified moov atom. */
|
|
||||||
private void processMoovAtom(Atom.ContainerAtom moov) {
|
|
||||||
List<Mp4Track> tracks = new ArrayList<Mp4Track>();
|
|
||||||
long earliestSampleOffset = Long.MAX_VALUE;
|
|
||||||
for (int i = 0; i < moov.containerChildren.size(); i++) {
|
|
||||||
Atom.ContainerAtom atom = moov.containerChildren.get(i);
|
|
||||||
if (atom.type != Atom.TYPE_trak) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Track track = CommonMp4AtomParsers.parseTrak(atom, moov.getLeafAtomOfType(Atom.TYPE_mvhd));
|
|
||||||
if (track.type != Track.TYPE_AUDIO && track.type != Track.TYPE_VIDEO) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia)
|
|
||||||
.getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl);
|
|
||||||
Mp4TrackSampleTable trackSampleTable = CommonMp4AtomParsers.parseStbl(track, stblAtom);
|
|
||||||
|
|
||||||
if (trackSampleTable.getSampleCount() == 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
tracks.add(new Mp4Track(track, trackSampleTable));
|
|
||||||
|
|
||||||
// Keep track of the byte offset of the earliest sample.
|
|
||||||
long firstSampleOffset = trackSampleTable.offsets[0];
|
|
||||||
if (firstSampleOffset < earliestSampleOffset) {
|
|
||||||
earliestSampleOffset = firstSampleOffset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.tracks = tracks.toArray(new Mp4Track[0]);
|
|
||||||
|
|
||||||
if (earliestSampleOffset < inputStream.getReadPosition()) {
|
|
||||||
loadFromOffset(earliestSampleOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
prepared = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns an unmodifiable set of atom types. */
|
|
||||||
private static Set<Integer> getAtomTypeSet(int... atomTypes) {
|
|
||||||
Set<Integer> atomTypeSet = new HashSet<Integer>();
|
|
||||||
for (int atomType : atomTypes) {
|
|
||||||
atomTypeSet.add(atomType);
|
|
||||||
}
|
|
||||||
return Collections.unmodifiableSet(atomTypeSet);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getRetryDelayMs(int errorCount) {
|
|
||||||
return Math.min((errorCount - 1) * 1000, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void maybeThrowLoadError() throws IOException {
|
|
||||||
if (loadErrorCount > loadRetryCount) {
|
|
||||||
throw lastLoadError;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class Mp4Track {
|
|
||||||
|
|
||||||
public final Track track;
|
|
||||||
public final Mp4TrackSampleTable sampleTable;
|
|
||||||
|
|
||||||
public boolean selected;
|
|
||||||
public int sampleIndex;
|
|
||||||
|
|
||||||
public Mp4Track(Track track, Mp4TrackSampleTable sampleTable) {
|
|
||||||
this.track = track;
|
|
||||||
this.sampleTable = sampleTable;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -128,6 +128,16 @@ public final class BufferPool implements Allocator {
|
||||||
recycledBuffers[recycledBufferCount++] = buffer;
|
recycledBuffers[recycledBufferCount++] = buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blocks execution until the allocated size is not greater than the threshold, or the thread is
|
||||||
|
* interrupted.
|
||||||
|
*/
|
||||||
|
public synchronized void blockWhileAllocatedSizeExceeds(int limit) throws InterruptedException {
|
||||||
|
while (getAllocatedSize() > limit) {
|
||||||
|
wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the buffers belonging to an allocation to the pool.
|
* Returns the buffers belonging to an allocation to the pool.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ public class ExtractorTest extends TestCase {
|
||||||
assertEquals(C.RESULT_END_OF_INPUT, Extractor.RESULT_END_OF_INPUT);
|
assertEquals(C.RESULT_END_OF_INPUT, Extractor.RESULT_END_OF_INPUT);
|
||||||
// Sanity check that the other constant values don't overlap.
|
// Sanity check that the other constant values don't overlap.
|
||||||
assertTrue(C.RESULT_END_OF_INPUT != Extractor.RESULT_CONTINUE);
|
assertTrue(C.RESULT_END_OF_INPUT != Extractor.RESULT_CONTINUE);
|
||||||
|
assertTrue(C.RESULT_END_OF_INPUT != Extractor.RESULT_SEEK);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,23 +13,22 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.source;
|
package com.google.android.exoplayer.extractor.mp4;
|
||||||
|
|
||||||
import com.google.android.exoplayer.C;
|
import com.google.android.exoplayer.C;
|
||||||
import com.google.android.exoplayer.MediaFormat;
|
import com.google.android.exoplayer.MediaFormat;
|
||||||
|
import com.google.android.exoplayer.MediaFormatHolder;
|
||||||
import com.google.android.exoplayer.SampleHolder;
|
import com.google.android.exoplayer.SampleHolder;
|
||||||
import com.google.android.exoplayer.SampleSource;
|
import com.google.android.exoplayer.SampleSource;
|
||||||
import com.google.android.exoplayer.mp4.Atom;
|
import com.google.android.exoplayer.extractor.ExtractorSampleSource;
|
||||||
import com.google.android.exoplayer.upstream.ByteArrayDataSource;
|
import com.google.android.exoplayer.upstream.ByteArrayDataSource;
|
||||||
import com.google.android.exoplayer.upstream.DataSource;
|
import com.google.android.exoplayer.upstream.DataSource;
|
||||||
import com.google.android.exoplayer.upstream.DataSpec;
|
|
||||||
import com.google.android.exoplayer.util.Assertions;
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
import com.google.android.exoplayer.util.MimeTypes;
|
import com.google.android.exoplayer.util.MimeTypes;
|
||||||
import com.google.android.exoplayer.util.Util;
|
import com.google.android.exoplayer.util.Util;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
import android.media.MediaExtractor;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
|
@ -43,10 +42,10 @@ import java.util.List;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link Mp4SampleExtractor}.
|
* Tests for {@link Mp4Extractor}.
|
||||||
*/
|
*/
|
||||||
@TargetApi(16)
|
@TargetApi(16)
|
||||||
public class Mp4SampleExtractorTest extends TestCase {
|
public class Mp4ExtractorTest extends TestCase {
|
||||||
|
|
||||||
/** String of hexadecimal bytes containing the video stsd payload from an AVC video. */
|
/** String of hexadecimal bytes containing the video stsd payload from an AVC video. */
|
||||||
private static final byte[] VIDEO_STSD_PAYLOAD = getByteArray(
|
private static final byte[] VIDEO_STSD_PAYLOAD = getByteArray(
|
||||||
|
|
@ -97,7 +96,7 @@ public class Mp4SampleExtractorTest extends TestCase {
|
||||||
/** Indices of key-frames. */
|
/** Indices of key-frames. */
|
||||||
private static final int[] SYNCHRONIZATION_SAMPLE_INDICES = {0, 4, 5};
|
private static final int[] SYNCHRONIZATION_SAMPLE_INDICES = {0, 4, 5};
|
||||||
/** Indices of video frame chunk offsets. */
|
/** Indices of video frame chunk offsets. */
|
||||||
private static final int[] CHUNK_OFFSETS = {1000, 2000, 3000, 4000};
|
private static final int[] CHUNK_OFFSETS = {1080, 2000, 3000, 4000};
|
||||||
/** Numbers of video frames in each chunk. */
|
/** Numbers of video frames in each chunk. */
|
||||||
private static final int[] SAMPLES_IN_CHUNK = {2, 2, 1, 1};
|
private static final int[] SAMPLES_IN_CHUNK = {2, 2, 1, 1};
|
||||||
/** The mdat box must be large enough to avoid reading chunk sample data out of bounds. */
|
/** The mdat box must be large enough to avoid reading chunk sample data out of bounds. */
|
||||||
|
|
@ -194,7 +193,7 @@ public class Mp4SampleExtractorTest extends TestCase {
|
||||||
while (true) {
|
while (true) {
|
||||||
int result = extractor.readSample(0, sampleHolder);
|
int result = extractor.readSample(0, sampleHolder);
|
||||||
if (result == SampleSource.SAMPLE_READ) {
|
if (result == SampleSource.SAMPLE_READ) {
|
||||||
assertTrue((sampleHolder.flags & MediaExtractor.SAMPLE_FLAG_SYNC) != 0);
|
assertTrue(sampleHolder.isSyncFrame());
|
||||||
sampleHolder.clearData();
|
sampleHolder.clearData();
|
||||||
sampleIndex++;
|
sampleIndex++;
|
||||||
} else if (result == SampleSource.END_OF_STREAM) {
|
} else if (result == SampleSource.END_OF_STREAM) {
|
||||||
|
|
@ -343,10 +342,18 @@ public class Mp4SampleExtractorTest extends TestCase {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] getMdat() {
|
private static byte[] getMdat(int mdatOffset) {
|
||||||
// TODO: Put NAL length tags in at each sample position so the sample lengths don't have to
|
ByteBuffer mdat = ByteBuffer.allocate(MDAT_SIZE);
|
||||||
// be multiples of four.
|
int sampleIndex = 0;
|
||||||
return new byte[MDAT_SIZE];
|
for (int chunk = 0; chunk < CHUNK_OFFSETS.length; chunk++) {
|
||||||
|
int sampleOffset = CHUNK_OFFSETS[chunk];
|
||||||
|
for (int sample = 0; sample < SAMPLES_IN_CHUNK[chunk]; sample++) {
|
||||||
|
int sampleSize = SAMPLE_SIZES[sampleIndex++];
|
||||||
|
mdat.putInt(sampleOffset - mdatOffset, sampleSize);
|
||||||
|
sampleOffset += sampleSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mdat.array();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final DataSource getFakeDataSource(boolean includeStss, boolean mp4vFormat) {
|
private static final DataSource getFakeDataSource(boolean includeStss, boolean mp4vFormat) {
|
||||||
|
|
@ -389,7 +396,7 @@ public class Mp4SampleExtractorTest extends TestCase {
|
||||||
atom(Atom.TYPE_stsc, getStsc()),
|
atom(Atom.TYPE_stsc, getStsc()),
|
||||||
atom(Atom.TYPE_stsz, getStsz()),
|
atom(Atom.TYPE_stsz, getStsz()),
|
||||||
atom(Atom.TYPE_stco, getStco())))))),
|
atom(Atom.TYPE_stco, getStco())))))),
|
||||||
atom(Atom.TYPE_mdat, getMdat()));
|
atom(Atom.TYPE_mdat, getMdat(mp4vFormat ? 1048 : 1038)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Gets a valid MP4 file with audio/video tracks and without a synchronization table. */
|
/** Gets a valid MP4 file with audio/video tracks and without a synchronization table. */
|
||||||
|
|
@ -425,7 +432,7 @@ public class Mp4SampleExtractorTest extends TestCase {
|
||||||
atom(Atom.TYPE_stsc, getStsc()),
|
atom(Atom.TYPE_stsc, getStsc()),
|
||||||
atom(Atom.TYPE_stsz, getStsz()),
|
atom(Atom.TYPE_stsz, getStsz()),
|
||||||
atom(Atom.TYPE_stco, getStco())))))),
|
atom(Atom.TYPE_stco, getStco())))))),
|
||||||
atom(Atom.TYPE_mdat, getMdat()));
|
atom(Atom.TYPE_mdat, getMdat(mp4vFormat ? 992 : 982)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Mp4Atom atom(int type, Mp4Atom... containedMp4Atoms) {
|
private static Mp4Atom atom(int type, Mp4Atom... containedMp4Atoms) {
|
||||||
|
|
@ -506,9 +513,8 @@ public class Mp4SampleExtractorTest extends TestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a {@link Mp4SampleExtractor} on a separate thread with a looper, so that it can use a
|
* Creates a {@link Mp4Extractor} on a separate thread with a looper, so that it can use a handler
|
||||||
* handler for loading, and provides blocking operations like {@link #seekTo} and
|
* for loading, and provides blocking operations like {@link #seekTo} and {@link #readSample}.
|
||||||
* {@link #readSample}.
|
|
||||||
*/
|
*/
|
||||||
private static final class Mp4ExtractorWrapper extends Thread {
|
private static final class Mp4ExtractorWrapper extends Thread {
|
||||||
|
|
||||||
|
|
@ -526,7 +532,7 @@ public class Mp4SampleExtractorTest extends TestCase {
|
||||||
private volatile CountDownLatch pendingOperationLatch;
|
private volatile CountDownLatch pendingOperationLatch;
|
||||||
|
|
||||||
public Mp4ExtractorWrapper(DataSource dataSource) {
|
public Mp4ExtractorWrapper(DataSource dataSource) {
|
||||||
super("Mp4SampleExtractorTest");
|
super("Mp4ExtractorTest");
|
||||||
this.dataSource = Assertions.checkNotNull(dataSource);
|
this.dataSource = Assertions.checkNotNull(dataSource);
|
||||||
pendingOperationLatch = new CountDownLatch(1);
|
pendingOperationLatch = new CountDownLatch(1);
|
||||||
start();
|
start();
|
||||||
|
|
@ -563,40 +569,45 @@ public class Mp4SampleExtractorTest extends TestCase {
|
||||||
@SuppressLint("HandlerLeak")
|
@SuppressLint("HandlerLeak")
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
final Mp4SampleExtractor mp4SampleExtractor =
|
final ExtractorSampleSource source = new ExtractorSampleSource(FAKE_URI, dataSource,
|
||||||
new Mp4SampleExtractor(dataSource, new DataSpec(FAKE_URI));
|
new Mp4Extractor(), 1, 2 * 1024 * 1024);
|
||||||
Looper.prepare();
|
Looper.prepare();
|
||||||
handler = new Handler() {
|
handler = new Handler() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleMessage(Message message) {
|
public void handleMessage(Message message) {
|
||||||
try {
|
try {
|
||||||
switch (message.what) {
|
switch (message.what) {
|
||||||
case MSG_PREPARE:
|
case MSG_PREPARE:
|
||||||
if (!mp4SampleExtractor.prepare()) {
|
if (!source.prepare()) {
|
||||||
sendEmptyMessage(MSG_PREPARE);
|
sendEmptyMessage(MSG_PREPARE);
|
||||||
} else {
|
} else {
|
||||||
// Select the video track and get its metadata.
|
// Select the video track and get its metadata.
|
||||||
mediaFormats = new MediaFormat[mp4SampleExtractor.getTrackCount()];
|
mediaFormats = new MediaFormat[source.getTrackCount()];
|
||||||
for (int track = 0; track < mp4SampleExtractor.getTrackCount(); track++) {
|
MediaFormatHolder mediaFormatHolder = new MediaFormatHolder();
|
||||||
MediaFormat mediaFormat = mp4SampleExtractor.getMediaFormat(track);
|
for (int track = 0; track < source.getTrackCount(); track++) {
|
||||||
|
source.enable(track, 0);
|
||||||
|
source.readData(track, 0, mediaFormatHolder, null, false);
|
||||||
|
MediaFormat mediaFormat = mediaFormatHolder.format;
|
||||||
mediaFormats[track] = mediaFormat;
|
mediaFormats[track] = mediaFormat;
|
||||||
if (MimeTypes.isVideo(mediaFormat.mimeType)) {
|
if (MimeTypes.isVideo(mediaFormat.mimeType)) {
|
||||||
mp4SampleExtractor.selectTrack(track);
|
|
||||||
selectedTrackMediaFormat = mediaFormat;
|
selectedTrackMediaFormat = mediaFormat;
|
||||||
|
} else {
|
||||||
|
source.disable(track);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pendingOperationLatch.countDown();
|
pendingOperationLatch.countDown();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case MSG_SEEK_TO:
|
case MSG_SEEK_TO:
|
||||||
long timestampUs = (long) message.obj;
|
long timestampUs = (Long) message.obj;
|
||||||
mp4SampleExtractor.seekTo(timestampUs);
|
source.seekToUs(timestampUs);
|
||||||
break;
|
break;
|
||||||
case MSG_READ_SAMPLE:
|
case MSG_READ_SAMPLE:
|
||||||
int trackIndex = message.arg1;
|
int trackIndex = message.arg1;
|
||||||
SampleHolder sampleHolder = (SampleHolder) message.obj;
|
SampleHolder sampleHolder = (SampleHolder) message.obj;
|
||||||
sampleHolder.clearData();
|
sampleHolder.clearData();
|
||||||
readSampleResult = mp4SampleExtractor.readSample(trackIndex, sampleHolder);
|
readSampleResult = source.readData(trackIndex, 0, null, sampleHolder, false);
|
||||||
if (readSampleResult == SampleSource.NOTHING_READ) {
|
if (readSampleResult == SampleSource.NOTHING_READ) {
|
||||||
Message.obtain(message).sendToTarget();
|
Message.obtain(message).sendToTarget();
|
||||||
return;
|
return;
|
||||||
|
|
@ -289,7 +289,7 @@ public class WebmExtractorTest extends InstrumentationTestCase {
|
||||||
ExtractorInput input = createTestInput(data);
|
ExtractorInput input = createTestInput(data);
|
||||||
int readResult = Extractor.RESULT_CONTINUE;
|
int readResult = Extractor.RESULT_CONTINUE;
|
||||||
while (readResult == Extractor.RESULT_CONTINUE) {
|
while (readResult == Extractor.RESULT_CONTINUE) {
|
||||||
readResult = extractor.read(input);
|
readResult = extractor.read(input, null);
|
||||||
}
|
}
|
||||||
assertEquals(Extractor.RESULT_END_OF_INPUT, readResult);
|
assertEquals(Extractor.RESULT_END_OF_INPUT, readResult);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue