From 587edf8e2b5a227f6b869ce0ea51361cfbedb9b3 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Sat, 11 Apr 2015 01:58:34 +0100 Subject: [PATCH] Add new style mp4/fmp4 extractors. --- .../android/exoplayer/demo/DemoUtil.java | 1 + .../exoplayer/demo/PlayerActivity.java | 10 +- .../android/exoplayer/demo/Samples.java | 5 +- ...der.java => ExtractorRendererBuilder.java} | 24 +- .../parser/mp4/FragmentedMp4Extractor.java | 43 +- .../extractor/DefaultTrackOutput.java | 10 + .../exoplayer/extractor/Extractor.java | 34 +- .../extractor/ExtractorSampleSource.java | 510 ++++++++++++ .../exoplayer/extractor/PositionHolder.java | 28 + .../extractor/RollingSampleBuffer.java | 59 ++ .../exoplayer/extractor/mp3/Mp3Extractor.java | 4 +- .../android/exoplayer/extractor/mp4/Atom.java | 190 +++++ .../mp4/AtomParsers.java} | 75 +- .../mp4/DefaultSampleValues.java | 5 +- .../extractor/mp4/FragmentedMp4Extractor.java | 700 +++++++++++++++++ .../exoplayer/extractor/mp4/Mp4Extractor.java | 371 +++++++++ .../exoplayer/{ => extractor}/mp4/Track.java | 3 +- .../mp4/TrackEncryptionBox.java | 3 +- .../mp4/TrackFragment.java | 19 +- .../mp4/TrackSampleTable.java} | 16 +- .../exoplayer/extractor/ts/AdtsExtractor.java | 4 +- .../exoplayer/extractor/ts/TsExtractor.java | 4 +- .../extractor/webm/WebmExtractor.java | 4 +- .../exoplayer/hls/HlsExtractorWrapper.java | 3 +- .../google/android/exoplayer/mp4/Atom.java | 200 ----- .../SmoothStreamingChunkSource.java | 4 +- .../exoplayer/source/Mp4SampleExtractor.java | 743 ------------------ .../exoplayer/upstream/BufferPool.java | 10 + .../exoplayer/extractor/ExtractorTest.java | 1 + .../mp4/Mp4ExtractorTest.java} | 67 +- .../extractor/webm/WebmExtractorTest.java | 2 +- 31 files changed, 2081 insertions(+), 1071 deletions(-) rename demo/src/main/java/com/google/android/exoplayer/demo/player/{Mp4RendererBuilder.java => ExtractorRendererBuilder.java} (75%) create mode 100644 library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java create mode 100644 library/src/main/java/com/google/android/exoplayer/extractor/PositionHolder.java create mode 100644 library/src/main/java/com/google/android/exoplayer/extractor/mp4/Atom.java rename library/src/main/java/com/google/android/exoplayer/{mp4/CommonMp4AtomParsers.java => extractor/mp4/AtomParsers.java} (91%) rename library/src/main/java/com/google/android/exoplayer/{chunk/parser => extractor}/mp4/DefaultSampleValues.java (88%) create mode 100644 library/src/main/java/com/google/android/exoplayer/extractor/mp4/FragmentedMp4Extractor.java create mode 100644 library/src/main/java/com/google/android/exoplayer/extractor/mp4/Mp4Extractor.java rename library/src/main/java/com/google/android/exoplayer/{ => extractor}/mp4/Track.java (95%) rename library/src/main/java/com/google/android/exoplayer/{chunk/parser => extractor}/mp4/TrackEncryptionBox.java (95%) rename library/src/main/java/com/google/android/exoplayer/{chunk/parser => extractor}/mp4/TrackFragment.java (89%) rename library/src/main/java/com/google/android/exoplayer/{mp4/Mp4TrackSampleTable.java => extractor/mp4/TrackSampleTable.java} (92%) delete mode 100644 library/src/main/java/com/google/android/exoplayer/mp4/Atom.java delete mode 100644 library/src/main/java/com/google/android/exoplayer/source/Mp4SampleExtractor.java rename library/src/test/java/com/google/android/exoplayer/{source/Mp4SampleExtractorTest.java => extractor/mp4/Mp4ExtractorTest.java} (91%) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java b/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java index b021e37683..80ef0c5c50 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java @@ -49,6 +49,7 @@ public class DemoUtil { public static final int TYPE_OTHER = 2; public static final int TYPE_HLS = 3; public static final int TYPE_MP4 = 4; + public static final int TYPE_MP3 = 5; private static final CookieManager defaultCookieManager; diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java index fcf61fb276..7a21790e76 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java @@ -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.DemoPlayer; 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.Mp4RendererBuilder; import com.google.android.exoplayer.demo.player.SmoothStreamingRendererBuilder; 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.PrivMetadata; import com.google.android.exoplayer.metadata.TxxxMetadata; @@ -217,7 +219,11 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, case DemoUtil.TYPE_HLS: return new HlsRendererBuilder(userAgent, contentUri.toString()); 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: return new DefaultRendererBuilder(this, contentUri, debugTextView); } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java index d6e16785ff..624f5b8568 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java @@ -135,12 +135,15 @@ import java.util.Locale; new Sample("Apple AAC 10s", "https://devimages.apple.com.edgekey.net/" + "streaming/examples/bipbop_4x3/gear0/fileSequence0.aac", 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" + "&sparams=ip,ipbits,expire&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=" + "2E853B992F6CAB9D28CA3BEBD84A6F26709A8A55.94344B0D8BA83A7417AAD24DACC8C71A9A878ECE" + "&key=ik0", DemoUtil.TYPE_MP4), + new Sample("Google Play (MP3 Audio)", + "http://storage.googleapis.com/exoplayer-test-media-0/play.mp3", + DemoUtil.TYPE_MP3), }; private Samples() {} diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/Mp4RendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/ExtractorRendererBuilder.java similarity index 75% rename from demo/src/main/java/com/google/android/exoplayer/demo/player/Mp4RendererBuilder.java rename to demo/src/main/java/com/google/android/exoplayer/demo/player/ExtractorRendererBuilder.java index 68fe2b8366..88302ba064 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/Mp4RendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/ExtractorRendererBuilder.java @@ -20,9 +20,9 @@ import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder; import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilderCallback; -import com.google.android.exoplayer.source.DefaultSampleSource; -import com.google.android.exoplayer.source.Mp4SampleExtractor; -import com.google.android.exoplayer.upstream.DataSpec; +import com.google.android.exoplayer.extractor.Extractor; +import com.google.android.exoplayer.extractor.ExtractorSampleSource; +import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.UriDataSource; import android.media.MediaCodec; @@ -30,23 +30,31 @@ import android.net.Uri; 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 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.debugTextView = debugTextView; + this.extractor = extractor; } @Override public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) { // Build the video and audio renderers. - DefaultSampleSource sampleSource = new DefaultSampleSource( - new Mp4SampleExtractor(new UriDataSource("exoplayer", null), new DataSpec(uri)), 2); + DataSource dataSource = new UriDataSource(userAgent, null); + ExtractorSampleSource sampleSource = new ExtractorSampleSource(uri, dataSource, extractor, 2, + BUFFER_SIZE); MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource, null, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null, player.getMainHandler(), player, 50); diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/FragmentedMp4Extractor.java index 42de648051..3e53a9ac3d 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/FragmentedMp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/FragmentedMp4Extractor.java @@ -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.SegmentIndex; 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.Atom.LeafAtom; -import com.google.android.exoplayer.mp4.CommonMp4AtomParsers; -import com.google.android.exoplayer.mp4.Track; +import com.google.android.exoplayer.extractor.mp4.Atom; +import com.google.android.exoplayer.extractor.mp4.Atom.ContainerAtom; +import com.google.android.exoplayer.extractor.mp4.Atom.LeafAtom; +import com.google.android.exoplayer.extractor.mp4.AtomParsers; +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.util.H264Util; import com.google.android.exoplayer.util.MimeTypes; @@ -157,7 +160,7 @@ public final class FragmentedMp4Extractor implements Extractor { public FragmentedMp4Extractor(int workaroundFlags) { this.workaroundFlags = workaroundFlags; parserState = STATE_READING_ATOM_HEADER; - atomHeader = new ParsableByteArray(Atom.ATOM_HEADER_SIZE); + atomHeader = new ParsableByteArray(Atom.HEADER_SIZE); extendedTypeScratch = new byte[16]; containerAtoms = new Stack(); fragmentRun = new TrackFragment(); @@ -259,14 +262,14 @@ public final class FragmentedMp4Extractor implements Extractor { } 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); if (bytesRead == -1) { return RESULT_END_OF_STREAM; } rootAtomBytesRead += bytesRead; atomBytesRead += bytesRead; - if (atomBytesRead != Atom.ATOM_HEADER_SIZE) { + if (atomBytesRead != Atom.HEADER_SIZE) { return RESULT_NEED_MORE_DATA; } @@ -288,10 +291,10 @@ public final class FragmentedMp4Extractor implements Extractor { if (CONTAINER_TYPES.contains(atomTypeInteger)) { enterState(STATE_READING_ATOM_HEADER); containerAtoms.add(new ContainerAtom(atomType, - rootAtomBytesRead + atomSize - Atom.ATOM_HEADER_SIZE)); + rootAtomBytesRead + atomSize - Atom.HEADER_SIZE)); } else { 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); } } else { @@ -360,7 +363,7 @@ public final class FragmentedMp4Extractor implements Extractor { LeafAtom child = moovChildren.get(i); if (child.type == Atom.TYPE_pssh) { 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()); int dataSize = psshAtom.readInt(); byte[] data = new byte[dataSize]; @@ -373,7 +376,7 @@ public final class FragmentedMp4Extractor implements Extractor { } ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex); 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)); } @@ -399,7 +402,7 @@ public final class FragmentedMp4Extractor implements Extractor { * Parses a trex atom (defined in 14496-12). */ 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 defaultSampleDuration = trex.readUnsignedIntToInt(); int defaultSampleSize = trex.readUnsignedIntToInt(); @@ -453,7 +456,7 @@ public final class FragmentedMp4Extractor implements Extractor { private static void parseSaiz(TrackEncryptionBox encryptionBox, ParsableByteArray saiz, TrackFragment out) { int vectorSize = encryptionBox.initializationVectorSize; - saiz.setPosition(Atom.ATOM_HEADER_SIZE); + saiz.setPosition(Atom.HEADER_SIZE); int fullAtom = saiz.readInt(); int flags = Atom.parseFullAtomFlags(fullAtom); if ((flags & 0x01) == 1) { @@ -490,7 +493,7 @@ public final class FragmentedMp4Extractor implements Extractor { */ private static DefaultSampleValues parseTfhd(DefaultSampleValues extendsDefaults, ParsableByteArray tfhd) { - tfhd.setPosition(Atom.ATOM_HEADER_SIZE); + tfhd.setPosition(Atom.HEADER_SIZE); int fullAtom = tfhd.readInt(); int flags = Atom.parseFullAtomFlags(fullAtom); @@ -519,7 +522,7 @@ public final class FragmentedMp4Extractor implements Extractor { * media, expressed in the media's timescale. */ private static long parseTfdt(ParsableByteArray tfdt) { - tfdt.setPosition(Atom.ATOM_HEADER_SIZE); + tfdt.setPosition(Atom.HEADER_SIZE); int fullAtom = tfdt.readInt(); int version = Atom.parseFullAtomVersion(fullAtom); 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, long decodeTime, int workaroundFlags, ParsableByteArray trun, TrackFragment out) { - trun.setPosition(Atom.ATOM_HEADER_SIZE); + trun.setPosition(Atom.HEADER_SIZE); int fullAtom = trun.readInt(); int flags = Atom.parseFullAtomFlags(fullAtom); @@ -596,7 +599,7 @@ public final class FragmentedMp4Extractor implements Extractor { private static void parseUuid(ParsableByteArray uuid, TrackFragment out, byte[] extendedTypeScratch) { - uuid.setPosition(Atom.ATOM_HEADER_SIZE); + uuid.setPosition(Atom.HEADER_SIZE); uuid.readBytes(extendedTypeScratch, 0, 16); // 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) { - senc.setPosition(Atom.ATOM_HEADER_SIZE + offset); + senc.setPosition(Atom.HEADER_SIZE + offset); int fullAtom = senc.readInt(); int flags = Atom.parseFullAtomFlags(fullAtom); @@ -639,7 +642,7 @@ public final class FragmentedMp4Extractor implements Extractor { * Parses a sidx atom (defined in 14496-12). */ private static SegmentIndex parseSidx(ParsableByteArray atom) { - atom.setPosition(Atom.ATOM_HEADER_SIZE); + atom.setPosition(Atom.HEADER_SIZE); int fullAtom = atom.readInt(); int version = Atom.parseFullAtomVersion(fullAtom); diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/DefaultTrackOutput.java b/library/src/main/java/com/google/android/exoplayer/extractor/DefaultTrackOutput.java index 2ae1316162..5e3cdbd829 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/DefaultTrackOutput.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/DefaultTrackOutput.java @@ -143,6 +143,16 @@ public final class DefaultTrackOutput implements TrackOutput { 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. * diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/Extractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/Extractor.java index 0eabcd23f3..c38a62a688 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/Extractor.java @@ -25,14 +25,20 @@ import java.io.IOException; public interface Extractor { /** - * Returned by {@link #read(ExtractorInput)} if the {@link ExtractorInput} passed to the next - * {@link #read(ExtractorInput)} is required to provide data continuing from the position in the - * stream reached by the returning call. + * Returned by {@link #read(ExtractorInput, PositionHolder)} if the {@link ExtractorInput} passed + * to the next {@link #read(ExtractorInput, PositionHolder)} is required to provide data + * continuing from the position in the stream reached by the returning call. */ public static final int RESULT_CONTINUE = 0; /** - * Returned by {@link #read(ExtractorInput)} if the end of the {@link ExtractorInput} was reached. - * Equal to {@link C#RESULT_END_OF_INPUT}. + * Returned by {@link #read(ExtractorInput, PositionHolder)} if the {@link ExtractorInput} passed + * 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; @@ -47,21 +53,31 @@ public interface Extractor { * Extracts data read from a provided {@link ExtractorInput}. *

* Each read will extract at most one sample from the stream before returning. + *

+ * 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 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. * @throws IOException If an error occurred reading from the input. * @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. *

* 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 - * position in the stream. Random access positions can be obtained from a {@link SeekMap} that - * has been extracted and passed to the {@link ExtractorOutput}. + * {@link #read(ExtractorInput, PositionHolder)} is required to provide data starting from any + * random access position in the stream. Random access positions can be obtained from a + * {@link SeekMap} that has been extracted and passed to the {@link ExtractorOutput}. */ void seek(); diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java b/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java new file mode 100644 index 0000000000..24a2da3d06 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java @@ -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 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(); + 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(); + } + } + } + + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/PositionHolder.java b/library/src/main/java/com/google/android/exoplayer/extractor/PositionHolder.java new file mode 100644 index 0000000000..2c947323b7 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/extractor/PositionHolder.java @@ -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; + +} diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/RollingSampleBuffer.java b/library/src/main/java/com/google/android/exoplayer/extractor/RollingSampleBuffer.java index b95940e53a..8aafa18385 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/RollingSampleBuffer.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/RollingSampleBuffer.java @@ -111,6 +111,21 @@ import java.util.concurrent.ConcurrentLinkedQueue; 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. * @@ -471,6 +486,50 @@ import java.util.concurrent.ConcurrentLinkedQueue; : (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. public synchronized void commitSample(long timeUs, int sampleFlags, long offset, int size, diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp3/Mp3Extractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/Mp3Extractor.java index 177cbea306..aff0923eb4 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mp3/Mp3Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/Mp3Extractor.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer.ParserException; 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.util.MimeTypes; @@ -111,7 +112,8 @@ public final class Mp3Extractor implements Extractor { } @Override - public int read(ExtractorInput extractorInput) throws IOException, InterruptedException { + public int read(ExtractorInput extractorInput, PositionHolder seekPosition) + throws IOException, InterruptedException { if (synchronizedHeaderData == 0 && synchronizeCatchingEndOfInput(extractorInput) == RESULT_END_OF_INPUT) { return RESULT_END_OF_INPUT; diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Atom.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Atom.java new file mode 100644 index 0000000000..c56fb46630 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Atom.java @@ -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 leafChildren; + public final List containerChildren; + + public ContainerAtom(int type, long endByteOffset) { + super(type); + + leafChildren = new ArrayList(); + containerChildren = new ArrayList(); + 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); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/mp4/CommonMp4AtomParsers.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/AtomParsers.java similarity index 91% rename from library/src/main/java/com/google/android/exoplayer/mp4/CommonMp4AtomParsers.java rename to library/src/main/java/com/google/android/exoplayer/extractor/mp4/AtomParsers.java index 2170425058..5b090dd017 100644 --- a/library/src/main/java/com/google/android/exoplayer/mp4/CommonMp4AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/AtomParsers.java @@ -13,11 +13,10 @@ * See the License for the specific language governing permissions and * 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.MediaFormat; -import com.google.android.exoplayer.chunk.parser.mp4.TrackEncryptionBox; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.CodecSpecificDataUtil; import com.google.android.exoplayer.util.H264Util; @@ -32,7 +31,7 @@ import java.util.Collections; import java.util.List; /** 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.) */ 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}; /** - * Parses a trak atom (defined in 14496-12) + * Parses a trak atom (defined in 14496-12). * * @param trak Atom to parse. * @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) { Atom.ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia); int trackType = parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data); - Assertions.checkState(trackType == Track.TYPE_AUDIO || trackType == Track.TYPE_VIDEO - || trackType == Track.TYPE_TEXT || trackType == Track.TYPE_TIME_CODE); + if (trackType != Track.TYPE_AUDIO && trackType != Track.TYPE_VIDEO + && trackType != Track.TYPE_TEXT && trackType != Track.TYPE_TIME_CODE) { + return null; + } Pair header = parseTkhd(trak.getLeafAtomOfType(Atom.TYPE_tkhd).data); int id = header.first; @@ -80,7 +81,7 @@ public final class CommonMp4AtomParsers { * @param stblAtom stbl (sample table) atom to parse. * @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. ParsableByteArray stsz = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz).data; @@ -103,7 +104,7 @@ public final class CommonMp4AtomParsers { ParsableByteArray ctts = cttsAtom != null ? cttsAtom.data : null; // Skip full atom. - stsz.setPosition(Atom.FULL_ATOM_HEADER_SIZE); + stsz.setPosition(Atom.FULL_HEADER_SIZE); int fixedSampleSize = stsz.readUnsignedIntToInt(); int sampleCount = stsz.readUnsignedIntToInt(); @@ -113,10 +114,10 @@ public final class CommonMp4AtomParsers { int[] flags = new int[sampleCount]; // Prepare to read chunk offsets. - chunkOffsets.setPosition(Atom.FULL_ATOM_HEADER_SIZE); + chunkOffsets.setPosition(Atom.FULL_HEADER_SIZE); int chunkCount = chunkOffsets.readUnsignedIntToInt(); - stsc.setPosition(Atom.FULL_ATOM_HEADER_SIZE); + stsc.setPosition(Atom.FULL_HEADER_SIZE); int remainingSamplesPerChunkChanges = stsc.readUnsignedIntToInt() - 1; Assertions.checkState(stsc.readInt() == 1, "stsc first chunk must be 1"); int samplesPerChunk = stsc.readUnsignedIntToInt(); @@ -131,28 +132,31 @@ public final class CommonMp4AtomParsers { int remainingSamplesInChunk = samplesPerChunk; // Prepare to read sample timestamps. - stts.setPosition(Atom.FULL_ATOM_HEADER_SIZE); + stts.setPosition(Atom.FULL_HEADER_SIZE); int remainingTimestampDeltaChanges = stts.readUnsignedIntToInt() - 1; int remainingSamplesAtTimestampDelta = stts.readUnsignedIntToInt(); int timestampDeltaInTimeUnits = stts.readUnsignedIntToInt(); // Prepare to read sample timestamp offsets, if ctts is present. - boolean cttsHasSignedOffsets = false; int remainingSamplesAtTimestampOffset = 0; int remainingTimestampOffsetChanges = 0; int timestampOffset = 0; if (ctts != null) { - ctts.setPosition(Atom.ATOM_HEADER_SIZE); - cttsHasSignedOffsets = Atom.parseFullAtomVersion(ctts.readInt()) == 1; + ctts.setPosition(Atom.FULL_HEADER_SIZE); remainingTimestampOffsetChanges = ctts.readUnsignedIntToInt() - 1; 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 remainingSynchronizationSamples = 0; if (stss != null) { - stss.setPosition(Atom.FULL_ATOM_HEADER_SIZE); + stss.setPosition(Atom.FULL_HEADER_SIZE); remainingSynchronizationSamples = stss.readUnsignedIntToInt(); nextSynchronizationSampleIndex = stss.readUnsignedIntToInt() - 1; } @@ -195,7 +199,8 @@ public final class CommonMp4AtomParsers { remainingSamplesAtTimestampOffset--; if (remainingSamplesAtTimestampOffset == 0 && remainingTimestampOffsetChanges > 0) { 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--; } } @@ -240,7 +245,7 @@ public final class CommonMp4AtomParsers { Assertions.checkArgument(remainingSamplesInChunk == 0); Assertions.checkArgument(remainingTimestampDeltaChanges == 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. */ private static long parseMvhd(ParsableByteArray mvhd) { - mvhd.setPosition(Atom.ATOM_HEADER_SIZE); + mvhd.setPosition(Atom.HEADER_SIZE); int fullAtom = mvhd.readInt(); 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. */ private static Pair parseTkhd(ParsableByteArray tkhd) { - tkhd.setPosition(Atom.ATOM_HEADER_SIZE); + tkhd.setPosition(Atom.HEADER_SIZE); int fullAtom = tkhd.readInt(); int version = Atom.parseFullAtomVersion(fullAtom); @@ -303,7 +308,7 @@ public final class CommonMp4AtomParsers { * @return The track type. */ private static int parseHdlr(ParsableByteArray hdlr) { - hdlr.setPosition(Atom.FULL_ATOM_HEADER_SIZE + 4); + hdlr.setPosition(Atom.FULL_HEADER_SIZE + 4); 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. */ private static long parseMdhd(ParsableByteArray mdhd) { - mdhd.setPosition(Atom.ATOM_HEADER_SIZE); + mdhd.setPosition(Atom.HEADER_SIZE); int fullAtom = mdhd.readInt(); int version = Atom.parseFullAtomVersion(fullAtom); @@ -324,7 +329,7 @@ public final class CommonMp4AtomParsers { private static Pair parseStsd( ParsableByteArray stsd, long durationUs) { - stsd.setPosition(Atom.FULL_ATOM_HEADER_SIZE); + stsd.setPosition(Atom.FULL_HEADER_SIZE); int numberOfEntries = stsd.readInt(); MediaFormat mediaFormat = null; TrackEncryptionBox[] trackEncryptionBoxes = new TrackEncryptionBox[numberOfEntries]; @@ -358,7 +363,7 @@ public final class CommonMp4AtomParsers { /** Returns the media format for an avc1 box. */ private static Pair parseAvcFromParent(ParsableByteArray parent, int position, int size, long durationUs) { - parent.setPosition(position + Atom.ATOM_HEADER_SIZE); + parent.setPosition(position + Atom.HEADER_SIZE); parent.skip(24); int width = parent.readUnsignedShort(); @@ -395,7 +400,7 @@ public final class CommonMp4AtomParsers { } private static List 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) int nalUnitLength = (parent.readUnsignedByte() & 0x3) + 1; if (nalUnitLength != 4) { @@ -419,7 +424,7 @@ public final class CommonMp4AtomParsers { private static TrackEncryptionBox parseSinfFromParent(ParsableByteArray parent, int position, int size) { - int childPosition = position + Atom.ATOM_HEADER_SIZE; + int childPosition = position + Atom.HEADER_SIZE; TrackEncryptionBox trackEncryptionBox = null; while (childPosition - position < size) { @@ -442,7 +447,7 @@ public final class CommonMp4AtomParsers { } 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 vSpacing = parent.readUnsignedIntToInt(); return (float) hSpacing / vSpacing; @@ -450,7 +455,7 @@ public final class CommonMp4AtomParsers { private static TrackEncryptionBox parseSchiFromParent(ParsableByteArray parent, int position, int size) { - int childPosition = position + Atom.ATOM_HEADER_SIZE; + int childPosition = position + Atom.HEADER_SIZE; while (childPosition - position < size) { parent.setPosition(childPosition); int childAtomSize = parent.readInt(); @@ -472,7 +477,7 @@ public final class CommonMp4AtomParsers { /** Returns the media format for an mp4v box. */ private static MediaFormat parseMp4vFromParent(ParsableByteArray parent, int position, int size, long durationUs) { - parent.setPosition(position + Atom.ATOM_HEADER_SIZE); + parent.setPosition(position + Atom.HEADER_SIZE); parent.skip(24); int width = parent.readUnsignedShort(); @@ -499,7 +504,7 @@ public final class CommonMp4AtomParsers { private static Pair parseAudioSampleEntry( 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); int channelCount = parent.readUnsignedShort(); int sampleSize = parent.readUnsignedShort(); @@ -564,7 +569,7 @@ public final class CommonMp4AtomParsers { /** Returns codec-specific initialization data contained in an esds box. */ 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) parent.skip(1); // ES_Descriptor tag int varIntByte = parent.readUnsignedByte(); @@ -608,7 +613,7 @@ public final class CommonMp4AtomParsers { private static Ac3Format parseAc3SpecificBoxFromParent(ParsableByteArray parent, int position) { // 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) int fscod = (parent.readUnsignedByte() & 0xC0) >> 6; @@ -646,12 +651,12 @@ public final class CommonMp4AtomParsers { private static int parseEc3SpecificBoxFromParent(ParsableByteArray parent, int position) { // 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. return 0; } - private CommonMp4AtomParsers() { + private AtomParsers() { // Prevent instantiation. } diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/DefaultSampleValues.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/DefaultSampleValues.java similarity index 88% rename from library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/DefaultSampleValues.java rename to library/src/main/java/com/google/android/exoplayer/extractor/mp4/DefaultSampleValues.java index da20828100..45bba0007c 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/DefaultSampleValues.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/DefaultSampleValues.java @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * 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 duration; diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/FragmentedMp4Extractor.java new file mode 100644 index 0000000000..f11011fac8 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/FragmentedMp4Extractor.java @@ -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. + *

+ * 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. + *

+ * 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 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(); + fragmentRun = new TrackFragment(); + parserState = STATE_READING_ATOM_HEADER; + } + + /** + * Sideloads track information into the extractor. + *

+ * 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 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. + *

+ * 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. + *

+ * 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; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Mp4Extractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Mp4Extractor.java new file mode 100644 index 0000000000..31141a0b32 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Mp4Extractor.java @@ -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 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(); + 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 tracks = new ArrayList(); + 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. + *

+ * Returns {@link #RESULT_SEEK} if the source should be reloaded from the position in + * {@code positionHolder}. + *

+ * 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; + } + + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/mp4/Track.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Track.java similarity index 95% rename from library/src/main/java/com/google/android/exoplayer/mp4/Track.java rename to library/src/main/java/com/google/android/exoplayer/extractor/mp4/Track.java index 313e3272f6..7110a3a7a1 100644 --- a/library/src/main/java/com/google/android/exoplayer/mp4/Track.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Track.java @@ -13,11 +13,10 @@ * See the License for the specific language governing permissions and * 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.MediaFormat; -import com.google.android.exoplayer.chunk.parser.mp4.TrackEncryptionBox; /** * Encapsulates information describing an MP4 track. diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/TrackEncryptionBox.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/TrackEncryptionBox.java similarity index 95% rename from library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/TrackEncryptionBox.java rename to library/src/main/java/com/google/android/exoplayer/extractor/mp4/TrackEncryptionBox.java index 7615cdd041..e4481b13c8 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/TrackEncryptionBox.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/TrackEncryptionBox.java @@ -13,11 +13,12 @@ * See the License for the specific language governing permissions and * 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. */ +// TODO: Make package private. public final class TrackEncryptionBox { /** diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/TrackFragment.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/TrackFragment.java similarity index 89% rename from library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/TrackFragment.java rename to library/src/main/java/com/google/android/exoplayer/extractor/mp4/TrackFragment.java index eb16a87650..d8be80f5ca 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/TrackFragment.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/TrackFragment.java @@ -13,15 +13,19 @@ * See the License for the specific language governing permissions and * 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.util.ParsableByteArray; +import java.io.IOException; + /** * 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; @@ -121,6 +125,17 @@ import com.google.android.exoplayer.util.ParsableByteArray; 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. * diff --git a/library/src/main/java/com/google/android/exoplayer/mp4/Mp4TrackSampleTable.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/TrackSampleTable.java similarity index 92% rename from library/src/main/java/com/google/android/exoplayer/mp4/Mp4TrackSampleTable.java rename to library/src/main/java/com/google/android/exoplayer/extractor/mp4/TrackSampleTable.java index d3ce98225f..7e1e11ff82 100644 --- a/library/src/main/java/com/google/android/exoplayer/mp4/Mp4TrackSampleTable.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/TrackSampleTable.java @@ -13,18 +13,20 @@ * See the License for the specific language governing permissions and * 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.util.Assertions; import com.google.android.exoplayer.util.Util; /** Sample table for a track in an MP4 file. */ -public final class Mp4TrackSampleTable { +public final class TrackSampleTable { /** Sample index when no sample is available. */ public static final int NO_SAMPLE = -1; + /** Number of samples. */ + public final int sampleCount; /** Sample offsets in bytes. */ public final long[] offsets; /** Sample sizes in bytes. */ @@ -34,7 +36,7 @@ public final class Mp4TrackSampleTable { /** Sample flags. */ public final int[] flags; - Mp4TrackSampleTable( + TrackSampleTable( long[] offsets, int[] sizes, long[] timestampsUs, int[] flags) { Assertions.checkArgument(sizes.length == timestampsUs.length); Assertions.checkArgument(offsets.length == timestampsUs.length); @@ -44,11 +46,7 @@ public final class Mp4TrackSampleTable { this.sizes = sizes; this.timestampsUs = timestampsUs; this.flags = flags; - } - - /** Returns the number of samples in the table. */ - public int getSampleCount() { - return sizes.length; + sampleCount = offsets.length; } /** @@ -65,7 +63,6 @@ public final class Mp4TrackSampleTable { return i; } } - return NO_SAMPLE; } @@ -83,7 +80,6 @@ public final class Mp4TrackSampleTable { return i; } } - return NO_SAMPLE; } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/AdtsExtractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/AdtsExtractor.java index a71357e538..be22bad6cb 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/AdtsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/AdtsExtractor.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer.extractor.ts; 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.util.ParsableByteArray; import java.io.IOException; @@ -55,7 +56,8 @@ public class AdtsExtractor implements Extractor { } @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); if (bytesRead == -1) { return RESULT_END_OF_INPUT; diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java index 7f4203b1fe..50a081cb74 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer.C; 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.util.ParsableBitArray; import com.google.android.exoplayer.util.ParsableByteArray; @@ -77,7 +78,8 @@ public final class TsExtractor implements Extractor { } @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)) { return RESULT_END_OF_INPUT; } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/webm/WebmExtractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/webm/WebmExtractor.java index f81012592a..dd89265aba 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/webm/WebmExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/webm/WebmExtractor.java @@ -23,6 +23,7 @@ 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.util.LongArray; import com.google.android.exoplayer.util.MimeTypes; @@ -162,7 +163,8 @@ public final class WebmExtractor implements Extractor { } @Override - public int read(ExtractorInput input) throws IOException, InterruptedException { + public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, + InterruptedException { sampleRead = false; boolean inputHasData = true; while (!sampleRead && inputHasData) { diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsExtractorWrapper.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsExtractorWrapper.java index ddd57ceb6c..61267d5c67 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsExtractorWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsExtractorWrapper.java @@ -193,7 +193,8 @@ public final class HlsExtractorWrapper implements ExtractorOutput { * @throws InterruptedException If the thread was interrupted. */ 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; } diff --git a/library/src/main/java/com/google/android/exoplayer/mp4/Atom.java b/library/src/main/java/com/google/android/exoplayer/mp4/Atom.java deleted file mode 100644 index ffe0f9cc77..0000000000 --- a/library/src/main/java/com/google/android/exoplayer/mp4/Atom.java +++ /dev/null @@ -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 leafChildren; - public final List containerChildren; - - public ContainerAtom(int type, long endByteOffset) { - super(type); - - leafChildren = new ArrayList(); - containerChildren = new ArrayList(); - 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; - } - -} diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java index 9e9cb92bae..39bf46180c 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java @@ -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.parser.Extractor; 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.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.StreamElement; import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement; diff --git a/library/src/main/java/com/google/android/exoplayer/source/Mp4SampleExtractor.java b/library/src/main/java/com/google/android/exoplayer/source/Mp4SampleExtractor.java deleted file mode 100644 index f7154c2acf..0000000000 --- a/library/src/main/java/com/google/android/exoplayer/source/Mp4SampleExtractor.java +++ /dev/null @@ -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 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 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 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. - * - *

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(); - - 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 tracks = new ArrayList(); - 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 getAtomTypeSet(int... atomTypes) { - Set atomTypeSet = new HashSet(); - 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; - } - - } - -} diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/BufferPool.java b/library/src/main/java/com/google/android/exoplayer/upstream/BufferPool.java index af2ce03a20..44a82e98a0 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/BufferPool.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/BufferPool.java @@ -128,6 +128,16 @@ public final class BufferPool implements Allocator { 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. * diff --git a/library/src/test/java/com/google/android/exoplayer/extractor/ExtractorTest.java b/library/src/test/java/com/google/android/exoplayer/extractor/ExtractorTest.java index 6851afe500..e7ffdae63d 100644 --- a/library/src/test/java/com/google/android/exoplayer/extractor/ExtractorTest.java +++ b/library/src/test/java/com/google/android/exoplayer/extractor/ExtractorTest.java @@ -29,6 +29,7 @@ public class ExtractorTest extends TestCase { assertEquals(C.RESULT_END_OF_INPUT, Extractor.RESULT_END_OF_INPUT); // 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_SEEK); } } diff --git a/library/src/test/java/com/google/android/exoplayer/source/Mp4SampleExtractorTest.java b/library/src/test/java/com/google/android/exoplayer/extractor/mp4/Mp4ExtractorTest.java similarity index 91% rename from library/src/test/java/com/google/android/exoplayer/source/Mp4SampleExtractorTest.java rename to library/src/test/java/com/google/android/exoplayer/extractor/mp4/Mp4ExtractorTest.java index 1045b7b7dc..42bb872b31 100644 --- a/library/src/test/java/com/google/android/exoplayer/source/Mp4SampleExtractorTest.java +++ b/library/src/test/java/com/google/android/exoplayer/extractor/mp4/Mp4ExtractorTest.java @@ -13,23 +13,22 @@ * See the License for the specific language governing permissions and * 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.MediaFormat; +import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.SampleHolder; 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.DataSource; -import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.Util; import android.annotation.SuppressLint; import android.annotation.TargetApi; -import android.media.MediaExtractor; import android.net.Uri; import android.os.Handler; import android.os.Looper; @@ -43,10 +42,10 @@ import java.util.List; import java.util.concurrent.CountDownLatch; /** - * Tests for {@link Mp4SampleExtractor}. + * Tests for {@link Mp4Extractor}. */ @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. */ private static final byte[] VIDEO_STSD_PAYLOAD = getByteArray( @@ -97,7 +96,7 @@ public class Mp4SampleExtractorTest extends TestCase { /** Indices of key-frames. */ private static final int[] SYNCHRONIZATION_SAMPLE_INDICES = {0, 4, 5}; /** 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. */ 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. */ @@ -194,7 +193,7 @@ public class Mp4SampleExtractorTest extends TestCase { while (true) { int result = extractor.readSample(0, sampleHolder); if (result == SampleSource.SAMPLE_READ) { - assertTrue((sampleHolder.flags & MediaExtractor.SAMPLE_FLAG_SYNC) != 0); + assertTrue(sampleHolder.isSyncFrame()); sampleHolder.clearData(); sampleIndex++; } else if (result == SampleSource.END_OF_STREAM) { @@ -343,10 +342,18 @@ public class Mp4SampleExtractorTest extends TestCase { return result; } - private static byte[] getMdat() { - // TODO: Put NAL length tags in at each sample position so the sample lengths don't have to - // be multiples of four. - return new byte[MDAT_SIZE]; + private static byte[] getMdat(int mdatOffset) { + ByteBuffer mdat = ByteBuffer.allocate(MDAT_SIZE); + int sampleIndex = 0; + 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) { @@ -389,7 +396,7 @@ public class Mp4SampleExtractorTest extends TestCase { atom(Atom.TYPE_stsc, getStsc()), atom(Atom.TYPE_stsz, getStsz()), 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. */ @@ -425,7 +432,7 @@ public class Mp4SampleExtractorTest extends TestCase { atom(Atom.TYPE_stsc, getStsc()), atom(Atom.TYPE_stsz, getStsz()), 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) { @@ -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 - * handler for loading, and provides blocking operations like {@link #seekTo} and - * {@link #readSample}. + * Creates a {@link Mp4Extractor} on a separate thread with a looper, so that it can use a handler + * for loading, and provides blocking operations like {@link #seekTo} and {@link #readSample}. */ private static final class Mp4ExtractorWrapper extends Thread { @@ -526,7 +532,7 @@ public class Mp4SampleExtractorTest extends TestCase { private volatile CountDownLatch pendingOperationLatch; public Mp4ExtractorWrapper(DataSource dataSource) { - super("Mp4SampleExtractorTest"); + super("Mp4ExtractorTest"); this.dataSource = Assertions.checkNotNull(dataSource); pendingOperationLatch = new CountDownLatch(1); start(); @@ -563,40 +569,45 @@ public class Mp4SampleExtractorTest extends TestCase { @SuppressLint("HandlerLeak") @Override public void run() { - final Mp4SampleExtractor mp4SampleExtractor = - new Mp4SampleExtractor(dataSource, new DataSpec(FAKE_URI)); + final ExtractorSampleSource source = new ExtractorSampleSource(FAKE_URI, dataSource, + new Mp4Extractor(), 1, 2 * 1024 * 1024); Looper.prepare(); handler = new Handler() { + @Override public void handleMessage(Message message) { try { switch (message.what) { case MSG_PREPARE: - if (!mp4SampleExtractor.prepare()) { + if (!source.prepare()) { sendEmptyMessage(MSG_PREPARE); } else { // Select the video track and get its metadata. - mediaFormats = new MediaFormat[mp4SampleExtractor.getTrackCount()]; - for (int track = 0; track < mp4SampleExtractor.getTrackCount(); track++) { - MediaFormat mediaFormat = mp4SampleExtractor.getMediaFormat(track); + mediaFormats = new MediaFormat[source.getTrackCount()]; + MediaFormatHolder mediaFormatHolder = new MediaFormatHolder(); + 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; if (MimeTypes.isVideo(mediaFormat.mimeType)) { - mp4SampleExtractor.selectTrack(track); selectedTrackMediaFormat = mediaFormat; + } else { + source.disable(track); } } pendingOperationLatch.countDown(); } break; case MSG_SEEK_TO: - long timestampUs = (long) message.obj; - mp4SampleExtractor.seekTo(timestampUs); + long timestampUs = (Long) message.obj; + source.seekToUs(timestampUs); break; case MSG_READ_SAMPLE: int trackIndex = message.arg1; SampleHolder sampleHolder = (SampleHolder) message.obj; sampleHolder.clearData(); - readSampleResult = mp4SampleExtractor.readSample(trackIndex, sampleHolder); + readSampleResult = source.readData(trackIndex, 0, null, sampleHolder, false); if (readSampleResult == SampleSource.NOTHING_READ) { Message.obtain(message).sendToTarget(); return; diff --git a/library/src/test/java/com/google/android/exoplayer/extractor/webm/WebmExtractorTest.java b/library/src/test/java/com/google/android/exoplayer/extractor/webm/WebmExtractorTest.java index 19c61992a8..c3c5d0c20d 100644 --- a/library/src/test/java/com/google/android/exoplayer/extractor/webm/WebmExtractorTest.java +++ b/library/src/test/java/com/google/android/exoplayer/extractor/webm/WebmExtractorTest.java @@ -289,7 +289,7 @@ public class WebmExtractorTest extends InstrumentationTestCase { ExtractorInput input = createTestInput(data); int 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); }