Add new style mp4/fmp4 extractors.

This commit is contained in:
Oliver Woodman 2015-04-11 01:58:34 +01:00
parent f002e6a76e
commit 587edf8e2b
31 changed files with 2081 additions and 1071 deletions

View file

@ -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;

View file

@ -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);
}

View file

@ -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() {}

View file

@ -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);

View file

@ -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<ContainerAtom>();
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);

View file

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

View file

@ -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}.
* <p>
* Each read will extract at most one sample from the stream before returning.
* <p>
* In the common case, {@link #RESULT_CONTINUE} is returned to indicate that
* {@link ExtractorInput} passed to the next read is required to provide data continuing from the
* position in the stream reached by the returning call. If the extractor requires data to be
* provided from a different position, then that position is set in {@code seekPosition} and
* {@link #RESULT_SEEK} is returned. If the extractor reached the end of the data provided by the
* {@link ExtractorInput}, then {@link #RESULT_END_OF_INPUT} is returned.
*
* @param input The {@link ExtractorInput} from which data should be read.
* @param 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.
* <p>
* 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();

View file

@ -0,0 +1,510 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.extractor;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackInfo;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.Loader;
import com.google.android.exoplayer.upstream.Loader.Loadable;
import com.google.android.exoplayer.util.Assertions;
import android.net.Uri;
import android.os.SystemClock;
import android.util.SparseArray;
import java.io.IOException;
/**
* A {@link SampleSource} that extracts sample data using an {@link Extractor}
*/
public class ExtractorSampleSource implements SampleSource, ExtractorOutput, Loader.Callback {
/**
* The default minimum number of times to retry loading data prior to failing.
*/
public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3;
private static final int BUFFER_LENGTH = 256 * 1024;
private static final int NO_RESET_PENDING = -1;
private final Extractor extractor;
private final BufferPool bufferPool;
private final int requestedBufferSize;
private final SparseArray<DefaultTrackOutput> sampleQueues;
private final int minLoadableRetryCount;
private final boolean frameAccurateSeeking;
private final Uri uri;
private final DataSource dataSource;
private volatile boolean tracksBuilt;
private volatile SeekMap seekMap;
private volatile DrmInitData drmInitData;
private boolean prepared;
private int enabledTrackCount;
private TrackInfo[] trackInfos;
private boolean[] pendingMediaFormat;
private boolean[] pendingDiscontinuities;
private boolean[] trackEnabledStates;
private int remainingReleaseCount;
private long downstreamPositionUs;
private long lastSeekPositionUs;
private long pendingResetPositionUs;
private Loader loader;
private ExtractingLoadable loadable;
private IOException currentLoadableException;
private boolean currentLoadableExceptionFatal;
// TODO: Set this back to 0 in the correct place (some place indicative of making progress).
private int currentLoadableExceptionCount;
private long currentLoadableExceptionTimestamp;
private boolean loadingFinished;
/**
* @param uri The {@link Uri} of the media stream.
* @param dataSource A data source to read the media stream.
* @param extractor An {@link Extractor} to extract the media stream.
* @param downstreamRendererCount Number of track renderers dependent on this sample source.
* @param requestedBufferSize The requested total buffer size for storing sample data, in bytes.
* The actual allocated size may exceed the value passed in if the implementation requires it.
*/
public ExtractorSampleSource(Uri uri, DataSource dataSource, Extractor extractor,
int downstreamRendererCount, int requestedBufferSize) {
this.uri = uri;
this.dataSource = dataSource;
this.extractor = extractor;
remainingReleaseCount = downstreamRendererCount;
this.requestedBufferSize = requestedBufferSize;
sampleQueues = new SparseArray<DefaultTrackOutput>();
bufferPool = new BufferPool(BUFFER_LENGTH);
minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT;
pendingResetPositionUs = NO_RESET_PENDING;
frameAccurateSeeking = true;
extractor.init(this);
}
@Override
public boolean prepare() throws IOException {
if (prepared) {
return true;
}
if (loader == null) {
loader = new Loader("Loader:ExtractorSampleSource");
}
continueBufferingInternal();
// TODO: Support non-seekable content? Or at least avoid getting stuck here if a seekMap doesn't
// arrive (we may end up filling the sample buffers whilst we're still not prepared, and then
// getting stuck).
if (seekMap != null && tracksBuilt && haveFormatsForAllTracks()) {
int trackCount = sampleQueues.size();
trackEnabledStates = new boolean[trackCount];
pendingDiscontinuities = new boolean[trackCount];
pendingMediaFormat = new boolean[trackCount];
trackInfos = new TrackInfo[trackCount];
for (int i = 0; i < trackCount; i++) {
MediaFormat format = sampleQueues.valueAt(i).getFormat();
trackInfos[i] = new TrackInfo(format.mimeType, format.durationUs);
}
prepared = true;
if (isPendingReset()) {
restartFrom(pendingResetPositionUs);
}
return true;
} else {
maybeThrowLoadableException();
return false;
}
}
@Override
public int getTrackCount() {
return sampleQueues.size();
}
@Override
public TrackInfo getTrackInfo(int track) {
Assertions.checkState(prepared);
return trackInfos[track];
}
@Override
public void enable(int track, long positionUs) {
Assertions.checkState(prepared);
Assertions.checkState(!trackEnabledStates[track]);
enabledTrackCount++;
trackEnabledStates[track] = true;
pendingMediaFormat[track] = true;
if (enabledTrackCount == 1) {
seekToUs(positionUs);
}
}
@Override
public void disable(int track) {
Assertions.checkState(prepared);
Assertions.checkState(trackEnabledStates[track]);
enabledTrackCount--;
trackEnabledStates[track] = false;
pendingDiscontinuities[track] = false;
if (enabledTrackCount == 0) {
if (loader.isLoading()) {
loader.cancelLoading();
} else {
clearState();
}
}
}
@Override
public boolean continueBuffering(long playbackPositionUs) throws IOException {
Assertions.checkState(prepared);
Assertions.checkState(enabledTrackCount > 0);
downstreamPositionUs = playbackPositionUs;
discardSamplesForDisabledTracks(downstreamPositionUs);
return loadingFinished || continueBufferingInternal();
}
@Override
public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder,
SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException {
downstreamPositionUs = playbackPositionUs;
if (pendingDiscontinuities[track]) {
pendingDiscontinuities[track] = false;
return DISCONTINUITY_READ;
}
if (onlyReadDiscontinuity || isPendingReset()) {
maybeThrowLoadableException();
return NOTHING_READ;
}
DefaultTrackOutput sampleQueue = sampleQueues.valueAt(track);
if (pendingMediaFormat[track]) {
formatHolder.format = sampleQueue.getFormat();
formatHolder.drmInitData = drmInitData;
pendingMediaFormat[track] = false;
return FORMAT_READ;
}
if (sampleQueue.getSample(sampleHolder)) {
boolean decodeOnly = frameAccurateSeeking && sampleHolder.timeUs < lastSeekPositionUs;
sampleHolder.flags |= decodeOnly ? C.SAMPLE_FLAG_DECODE_ONLY : 0;
return SAMPLE_READ;
}
if (loadingFinished) {
return END_OF_STREAM;
}
maybeThrowLoadableException();
return NOTHING_READ;
}
@Override
public void seekToUs(long positionUs) {
Assertions.checkState(prepared);
Assertions.checkState(enabledTrackCount > 0);
lastSeekPositionUs = positionUs;
if ((isPendingReset() ? pendingResetPositionUs : downstreamPositionUs) == positionUs) {
return;
}
downstreamPositionUs = positionUs;
// If we're not pending a reset, see if we can seek within the sample queues.
boolean seekInsideBuffer = !isPendingReset();
for (int i = 0; seekInsideBuffer && i < sampleQueues.size(); i++) {
seekInsideBuffer &= sampleQueues.valueAt(i).skipToKeyframeBefore(positionUs);
}
// If we failed to seek within the sample queues, we need to restart.
if (!seekInsideBuffer) {
restartFrom(positionUs);
}
// Either way, we need to send discontinuities to the downstream components.
for (int i = 0; i < pendingDiscontinuities.length; i++) {
pendingDiscontinuities[i] = true;
}
}
@Override
public long getBufferedPositionUs() {
if (loadingFinished) {
return TrackRenderer.END_OF_TRACK_US;
} else if (isPendingReset()) {
return pendingResetPositionUs;
} else {
long largestParsedTimestampUs = Long.MIN_VALUE;
for (int i = 0; i < sampleQueues.size(); i++) {
largestParsedTimestampUs = Math.max(largestParsedTimestampUs,
sampleQueues.valueAt(i).getLargestParsedTimestampUs());
}
return largestParsedTimestampUs == Long.MIN_VALUE ? downstreamPositionUs
: largestParsedTimestampUs;
}
}
@Override
public void release() {
Assertions.checkState(remainingReleaseCount > 0);
if (--remainingReleaseCount == 0) {
loader.release();
loader = null;
}
}
// Loader.Callback implementation.
@Override
public void onLoadCompleted(Loadable loadable) {
loadingFinished = true;
}
@Override
public void onLoadCanceled(Loadable loadable) {
if (enabledTrackCount > 0) {
restartFrom(pendingResetPositionUs);
} else {
clearState();
}
}
@Override
public void onLoadError(Loadable loadable, IOException e) {
currentLoadableException = e;
currentLoadableExceptionCount++;
currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
maybeStartLoading();
}
// ExtractorOutput implementation.
@Override
public TrackOutput track(int id) {
DefaultTrackOutput sampleQueue = sampleQueues.get(id);
if (sampleQueue == null) {
sampleQueue = new DefaultTrackOutput(bufferPool);
sampleQueues.put(id, sampleQueue);
}
return sampleQueue;
}
@Override
public void endTracks() {
tracksBuilt = true;
}
@Override
public void seekMap(SeekMap seekMap) {
this.seekMap = seekMap;
}
@Override
public void drmInitData(DrmInitData drmInitData) {
this.drmInitData = drmInitData;
}
// Internal stuff.
private boolean continueBufferingInternal() throws IOException {
maybeStartLoading();
if (isPendingReset()) {
return false;
}
boolean haveSamples = prepared && haveSampleForOneEnabledTrack();
if (!haveSamples) {
maybeThrowLoadableException();
}
return haveSamples;
}
private void restartFrom(long positionUs) {
pendingResetPositionUs = positionUs;
loadingFinished = false;
if (loader.isLoading()) {
loader.cancelLoading();
} else {
clearState();
maybeStartLoading();
}
}
private void maybeStartLoading() {
if (currentLoadableExceptionFatal || loadingFinished || loader.isLoading()) {
return;
}
if (currentLoadableException != null) {
Assertions.checkState(loadable != null);
long elapsedMillis = SystemClock.elapsedRealtime() - currentLoadableExceptionTimestamp;
if (elapsedMillis >= getRetryDelayMillis(currentLoadableExceptionCount)) {
currentLoadableException = null;
loader.startLoading(loadable, this);
}
return;
}
if (!prepared) {
loadable = new ExtractingLoadable(uri, dataSource, extractor, bufferPool, requestedBufferSize,
0);
} else {
Assertions.checkState(isPendingReset());
loadable = new ExtractingLoadable(uri, dataSource, extractor, bufferPool, requestedBufferSize,
seekMap.getPosition(pendingResetPositionUs));
pendingResetPositionUs = NO_RESET_PENDING;
}
loader.startLoading(loadable, this);
}
private void maybeThrowLoadableException() throws IOException {
if (currentLoadableException != null && (currentLoadableExceptionFatal
|| currentLoadableExceptionCount > minLoadableRetryCount)) {
throw currentLoadableException;
}
}
private boolean haveFormatsForAllTracks() {
for (int i = 0; i < sampleQueues.size(); i++) {
if (!sampleQueues.valueAt(i).hasFormat()) {
return false;
}
}
return true;
}
private boolean haveSampleForOneEnabledTrack() {
for (int i = 0; i < trackEnabledStates.length; i++) {
if (trackEnabledStates[i] && !sampleQueues.valueAt(i).isEmpty()) {
return true;
}
}
return false;
}
private void discardSamplesForDisabledTracks(long timeUs) {
for (int i = 0; i < trackEnabledStates.length; i++) {
if (!trackEnabledStates[i]) {
sampleQueues.valueAt(i).discardUntil(timeUs);
}
}
}
private void clearState() {
for (int i = 0; i < sampleQueues.size(); i++) {
sampleQueues.valueAt(i).clear();
}
loadable = null;
currentLoadableException = null;
currentLoadableExceptionCount = 0;
currentLoadableExceptionFatal = false;
}
private boolean isPendingReset() {
return pendingResetPositionUs != NO_RESET_PENDING;
}
private long getRetryDelayMillis(long errorCount) {
return Math.min((errorCount - 1) * 1000, 5000);
}
/**
* Loads the media stream and extracts sample data from it.
*/
private static class ExtractingLoadable implements Loadable {
private final Uri uri;
private final DataSource dataSource;
private final Extractor extractor;
private final BufferPool bufferPool;
private final int bufferPoolSizeLimit;
private final PositionHolder positionHolder;
private volatile boolean loadCanceled;
private boolean pendingExtractorSeek;
public ExtractingLoadable(Uri uri, DataSource dataSource, Extractor extractor,
BufferPool bufferPool, int bufferPoolSizeLimit, long position) {
this.uri = Assertions.checkNotNull(uri);
this.dataSource = Assertions.checkNotNull(dataSource);
this.extractor = Assertions.checkNotNull(extractor);
this.bufferPool = Assertions.checkNotNull(bufferPool);
this.bufferPoolSizeLimit = bufferPoolSizeLimit;
positionHolder = new PositionHolder();
positionHolder.position = position;
pendingExtractorSeek = true;
}
@Override
public void cancelLoad() {
loadCanceled = true;
}
@Override
public boolean isLoadCanceled() {
return loadCanceled;
}
@Override
public void load() throws IOException, InterruptedException {
if (pendingExtractorSeek) {
extractor.seek();
pendingExtractorSeek = false;
}
int result = Extractor.RESULT_CONTINUE;
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
ExtractorInput input = null;
try {
long position = positionHolder.position;
long length = dataSource.open(new DataSpec(uri, position, C.LENGTH_UNBOUNDED, null));
if (length != C.LENGTH_UNBOUNDED) {
length += position;
}
input = new DefaultExtractorInput(dataSource, position, length);
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
bufferPool.blockWhileAllocatedSizeExceeds(bufferPoolSizeLimit);
result = extractor.read(input, positionHolder);
// TODO: Implement throttling to stop us from buffering data too often.
}
} finally {
if (result == Extractor.RESULT_SEEK) {
result = Extractor.RESULT_CONTINUE;
} else if (input != null) {
positionHolder.position = input.getPosition();
}
dataSource.close();
}
}
}
}
}

View file

@ -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;
}

View file

@ -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,

View file

@ -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;

View file

@ -0,0 +1,190 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.extractor.mp4;
import com.google.android.exoplayer.util.ParsableByteArray;
import com.google.android.exoplayer.util.Util;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public abstract class Atom {
/** Size of an atom header, in bytes. */
public static final int HEADER_SIZE = 8;
/** Size of a full atom header, in bytes. */
public static final int FULL_HEADER_SIZE = 12;
/** Size of a long atom header, in bytes. */
public static final int LONG_HEADER_SIZE = 16;
/** Value for the first 32 bits of atomSize when the atom size is actually a long value. */
public static final int LONG_SIZE_PREFIX = 1;
public static final int TYPE_ftyp = Util.getIntegerCodeForString("ftyp");
public static final int TYPE_avc1 = Util.getIntegerCodeForString("avc1");
public static final int TYPE_avc3 = Util.getIntegerCodeForString("avc3");
public static final int TYPE_esds = Util.getIntegerCodeForString("esds");
public static final int TYPE_mdat = Util.getIntegerCodeForString("mdat");
public static final int TYPE_mp4a = Util.getIntegerCodeForString("mp4a");
public static final int TYPE_ac_3 = Util.getIntegerCodeForString("ac-3");
public static final int TYPE_dac3 = Util.getIntegerCodeForString("dac3");
public static final int TYPE_ec_3 = Util.getIntegerCodeForString("ec-3");
public static final int TYPE_dec3 = Util.getIntegerCodeForString("dec3");
public static final int TYPE_tfdt = Util.getIntegerCodeForString("tfdt");
public static final int TYPE_tfhd = Util.getIntegerCodeForString("tfhd");
public static final int TYPE_trex = Util.getIntegerCodeForString("trex");
public static final int TYPE_trun = Util.getIntegerCodeForString("trun");
public static final int TYPE_sidx = Util.getIntegerCodeForString("sidx");
public static final int TYPE_moov = Util.getIntegerCodeForString("moov");
public static final int TYPE_mvhd = Util.getIntegerCodeForString("mvhd");
public static final int TYPE_trak = Util.getIntegerCodeForString("trak");
public static final int TYPE_mdia = Util.getIntegerCodeForString("mdia");
public static final int TYPE_minf = Util.getIntegerCodeForString("minf");
public static final int TYPE_stbl = Util.getIntegerCodeForString("stbl");
public static final int TYPE_avcC = Util.getIntegerCodeForString("avcC");
public static final int TYPE_moof = Util.getIntegerCodeForString("moof");
public static final int TYPE_traf = Util.getIntegerCodeForString("traf");
public static final int TYPE_mvex = Util.getIntegerCodeForString("mvex");
public static final int TYPE_tkhd = Util.getIntegerCodeForString("tkhd");
public static final int TYPE_mdhd = Util.getIntegerCodeForString("mdhd");
public static final int TYPE_hdlr = Util.getIntegerCodeForString("hdlr");
public static final int TYPE_stsd = Util.getIntegerCodeForString("stsd");
public static final int TYPE_pssh = Util.getIntegerCodeForString("pssh");
public static final int TYPE_sinf = Util.getIntegerCodeForString("sinf");
public static final int TYPE_schm = Util.getIntegerCodeForString("schm");
public static final int TYPE_schi = Util.getIntegerCodeForString("schi");
public static final int TYPE_tenc = Util.getIntegerCodeForString("tenc");
public static final int TYPE_encv = Util.getIntegerCodeForString("encv");
public static final int TYPE_enca = Util.getIntegerCodeForString("enca");
public static final int TYPE_frma = Util.getIntegerCodeForString("frma");
public static final int TYPE_saiz = Util.getIntegerCodeForString("saiz");
public static final int TYPE_uuid = Util.getIntegerCodeForString("uuid");
public static final int TYPE_senc = Util.getIntegerCodeForString("senc");
public static final int TYPE_pasp = Util.getIntegerCodeForString("pasp");
public static final int TYPE_TTML = Util.getIntegerCodeForString("TTML");
public static final int TYPE_vmhd = Util.getIntegerCodeForString("vmhd");
public static final int TYPE_smhd = Util.getIntegerCodeForString("smhd");
public static final int TYPE_mp4v = Util.getIntegerCodeForString("mp4v");
public static final int TYPE_stts = Util.getIntegerCodeForString("stts");
public static final int TYPE_stss = Util.getIntegerCodeForString("stss");
public static final int TYPE_ctts = Util.getIntegerCodeForString("ctts");
public static final int TYPE_stsc = Util.getIntegerCodeForString("stsc");
public static final int TYPE_stsz = Util.getIntegerCodeForString("stsz");
public static final int TYPE_stco = Util.getIntegerCodeForString("stco");
public static final int TYPE_co64 = Util.getIntegerCodeForString("co64");
public final int type;
Atom(int type) {
this.type = type;
}
@Override
public String toString() {
return getAtomTypeString(type);
}
/** An MP4 atom that is a leaf. */
public static final class LeafAtom extends Atom {
public final ParsableByteArray data;
public LeafAtom(int type, ParsableByteArray data) {
super(type);
this.data = data;
}
}
/** An MP4 atom that has child atoms. */
public static final class ContainerAtom extends Atom {
public final long endByteOffset;
public final List<LeafAtom> leafChildren;
public final List<ContainerAtom> containerChildren;
public ContainerAtom(int type, long endByteOffset) {
super(type);
leafChildren = new ArrayList<LeafAtom>();
containerChildren = new ArrayList<ContainerAtom>();
this.endByteOffset = endByteOffset;
}
public void add(LeafAtom atom) {
leafChildren.add(atom);
}
public void add(ContainerAtom atom) {
containerChildren.add(atom);
}
public LeafAtom getLeafAtomOfType(int type) {
int childrenSize = leafChildren.size();
for (int i = 0; i < childrenSize; i++) {
LeafAtom atom = leafChildren.get(i);
if (atom.type == type) {
return atom;
}
}
return null;
}
public ContainerAtom getContainerAtomOfType(int type) {
int childrenSize = containerChildren.size();
for (int i = 0; i < childrenSize; i++) {
ContainerAtom atom = containerChildren.get(i);
if (atom.type == type) {
return atom;
}
}
return null;
}
@Override
public String toString() {
return getAtomTypeString(type)
+ " leaves: " + Arrays.toString(leafChildren.toArray(new LeafAtom[0]))
+ " containers: " + Arrays.toString(containerChildren.toArray(new ContainerAtom[0]));
}
}
/**
* Parses the version number out of the additional integer component of a full atom.
*/
public static int parseFullAtomVersion(int fullAtomInt) {
return 0x000000FF & (fullAtomInt >> 24);
}
/**
* Parses the atom flags out of the additional integer component of a full atom.
*/
public static int parseFullAtomFlags(int fullAtomInt) {
return 0x00FFFFFF & fullAtomInt;
}
private static String getAtomTypeString(int type) {
return "" + (char) (type >> 24)
+ (char) ((type >> 16) & 0xFF)
+ (char) ((type >> 8) & 0xFF)
+ (char) (type & 0xFF);
}
}

View file

@ -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<Integer, Long> 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<Integer, Long> 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<MediaFormat, TrackEncryptionBox[]> 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<MediaFormat, TrackEncryptionBox> 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<byte[]> parseAvcCFromParent(ParsableByteArray parent, int position) {
parent.setPosition(position + Atom.ATOM_HEADER_SIZE + 4);
parent.setPosition(position + Atom.HEADER_SIZE + 4);
// Start of the AVCDecoderConfigurationRecord (defined in 14496-15)
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<MediaFormat, TrackEncryptionBox> 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.
}

View file

@ -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;

View file

@ -0,0 +1,700 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.extractor.mp4;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.extractor.ChunkIndex;
import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.ExtractorOutput;
import com.google.android.exoplayer.extractor.PositionHolder;
import com.google.android.exoplayer.extractor.TrackOutput;
import com.google.android.exoplayer.extractor.mp4.Atom.ContainerAtom;
import com.google.android.exoplayer.extractor.mp4.Atom.LeafAtom;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.H264Util;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableByteArray;
import com.google.android.exoplayer.util.Util;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Stack;
import java.util.UUID;
/**
* Facilitates the extraction of data from the fragmented mp4 container format.
* <p>
* This implementation only supports de-muxed (i.e. single track) streams.
*/
public final class FragmentedMp4Extractor implements Extractor {
/**
* Flag to work around an issue in some video streams where every frame is marked as a sync frame.
* The workaround overrides the sync frame flags in the stream, forcing them to false except for
* the first sample in each segment.
* <p>
* This flag does nothing if the stream is not a video stream.
*/
public static final int WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME = 1;
private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE =
new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12};
// Parser states
private static final int STATE_READING_ATOM_HEADER = 0;
private static final int STATE_READING_ATOM_PAYLOAD = 1;
private static final int STATE_READING_ENCRYPTION_DATA = 2;
private static final int STATE_READING_SAMPLE_START = 3;
private static final int STATE_READING_SAMPLE_CONTINUE = 4;
private final int workaroundFlags;
// Temporary arrays.
private final ParsableByteArray nalStartCode;
private final ParsableByteArray nalLength;
private final ParsableByteArray encryptionSignalByte;
// Parser state
private final ParsableByteArray atomHeader;
private final byte[] extendedTypeScratch;
private final Stack<ContainerAtom> containerAtoms;
private final TrackFragment fragmentRun;
private int parserState;
private int rootAtomBytesRead;
private int atomType;
private int atomSize;
private ParsableByteArray atomData;
private int sampleIndex;
private int sampleSize;
private int sampleBytesWritten;
private int sampleCurrentNalBytesRemaining;
// Data parsed from moov atom.
private Track track;
private DefaultSampleValues extendsDefaults;
// Extractor outputs.
private ExtractorOutput extractorOutput;
private TrackOutput trackOutput;
public FragmentedMp4Extractor() {
this(0);
}
/**
* @param workaroundFlags Flags to allow parsing of faulty streams.
* {@link #WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME} is currently the only flag defined.
*/
public FragmentedMp4Extractor(int workaroundFlags) {
this.workaroundFlags = workaroundFlags;
atomHeader = new ParsableByteArray(Atom.HEADER_SIZE);
nalStartCode = new ParsableByteArray(H264Util.NAL_START_CODE);
nalLength = new ParsableByteArray(4);
encryptionSignalByte = new ParsableByteArray(1);
extendedTypeScratch = new byte[16];
containerAtoms = new Stack<ContainerAtom>();
fragmentRun = new TrackFragment();
parserState = STATE_READING_ATOM_HEADER;
}
/**
* Sideloads track information into the extractor.
* <p>
* Should be called before {@link #read(ExtractorInput, PositionHolder)} in the case that the
* extractor will not receive a moov atom in the input data, from which track information would
* normally be parsed.
*
* @param track The track to sideload.
*/
public void setTrack(Track track) {
this.extendsDefaults = new DefaultSampleValues(0, 0, 0, 0);
this.track = track;
}
@Override
public void init(ExtractorOutput output) {
extractorOutput = output;
trackOutput = output.track(0);
extractorOutput.endTracks();
}
@Override
public void seek() {
containerAtoms.clear();
rootAtomBytesRead = 0;
parserState = STATE_READING_ATOM_HEADER;
}
@Override
public int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException {
while (true) {
switch (parserState) {
case STATE_READING_ATOM_HEADER:
if (!readAtomHeader(input)) {
return Extractor.RESULT_END_OF_INPUT;
}
break;
case STATE_READING_ATOM_PAYLOAD:
readAtomPayload(input);
break;
case STATE_READING_ENCRYPTION_DATA:
readEncryptionData(input);
break;
default:
if (readSample(input)) {
return RESULT_CONTINUE;
}
}
}
}
private boolean readAtomHeader(ExtractorInput input) throws IOException, InterruptedException {
if (!input.readFully(atomHeader.data, 0, Atom.HEADER_SIZE, true)) {
return false;
}
rootAtomBytesRead += Atom.HEADER_SIZE;
atomHeader.setPosition(0);
atomSize = atomHeader.readInt();
atomType = atomHeader.readInt();
if (atomType == Atom.TYPE_mdat) {
if (fragmentRun.sampleEncryptionDataNeedsFill) {
parserState = STATE_READING_ENCRYPTION_DATA;
} else {
parserState = STATE_READING_SAMPLE_START;
}
return true;
}
if (shouldParseAtom(atomType)) {
if (shouldParseContainerAtom(atomType)) {
parserState = STATE_READING_ATOM_HEADER;
containerAtoms.add(new ContainerAtom(atomType,
rootAtomBytesRead + atomSize - Atom.HEADER_SIZE));
} else {
atomData = new ParsableByteArray(atomSize);
System.arraycopy(atomHeader.data, 0, atomData.data, 0, Atom.HEADER_SIZE);
parserState = STATE_READING_ATOM_PAYLOAD;
}
} else {
atomData = null;
parserState = STATE_READING_ATOM_PAYLOAD;
}
return true;
}
private void readAtomPayload(ExtractorInput input) throws IOException, InterruptedException {
int payloadLength = atomSize - Atom.HEADER_SIZE;
if (atomData != null) {
input.readFully(atomData.data, Atom.HEADER_SIZE, payloadLength);
rootAtomBytesRead += payloadLength;
onLeafAtomRead(new LeafAtom(atomType, atomData), input.getPosition());
} else {
input.skipFully(payloadLength);
rootAtomBytesRead += payloadLength;
}
while (!containerAtoms.isEmpty() && containerAtoms.peek().endByteOffset == rootAtomBytesRead) {
onContainerAtomRead(containerAtoms.pop());
}
if (containerAtoms.isEmpty()) {
rootAtomBytesRead = 0;
}
parserState = STATE_READING_ATOM_HEADER;
}
private void onLeafAtomRead(LeafAtom leaf, long inputPosition) {
if (!containerAtoms.isEmpty()) {
containerAtoms.peek().add(leaf);
} else if (leaf.type == Atom.TYPE_sidx) {
ChunkIndex segmentIndex = parseSidx(leaf.data, inputPosition);
extractorOutput.seekMap(segmentIndex);
}
}
private void onContainerAtomRead(ContainerAtom container) {
if (container.type == Atom.TYPE_moov) {
onMoovContainerAtomRead(container);
} else if (container.type == Atom.TYPE_moof) {
onMoofContainerAtomRead(container);
} else if (!containerAtoms.isEmpty()) {
containerAtoms.peek().add(container);
}
}
private void onMoovContainerAtomRead(ContainerAtom moov) {
List<Atom.LeafAtom> moovChildren = moov.leafChildren;
int moovChildrenSize = moovChildren.size();
DrmInitData.Mapped drmInitData = null;
for (int i = 0; i < moovChildrenSize; i++) {
LeafAtom child = moovChildren.get(i);
if (child.type == Atom.TYPE_pssh) {
ParsableByteArray psshAtom = child.data;
psshAtom.setPosition(Atom.FULL_HEADER_SIZE);
UUID uuid = new UUID(psshAtom.readLong(), psshAtom.readLong());
int dataSize = psshAtom.readInt();
byte[] data = new byte[dataSize];
psshAtom.readBytes(data, 0, dataSize);
if (drmInitData == null) {
drmInitData = new DrmInitData.Mapped(MimeTypes.VIDEO_MP4);
}
drmInitData.put(uuid, data);
}
}
if (drmInitData != null) {
extractorOutput.drmInitData(drmInitData);
}
ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex);
extendsDefaults = parseTrex(mvex.getLeafAtomOfType(Atom.TYPE_trex).data);
track = AtomParsers.parseTrak(moov.getContainerAtomOfType(Atom.TYPE_trak),
moov.getLeafAtomOfType(Atom.TYPE_mvhd));
Assertions.checkState(track != null);
trackOutput.format(track.mediaFormat);
}
private void onMoofContainerAtomRead(ContainerAtom moof) {
fragmentRun.reset();
parseMoof(track, extendsDefaults, moof, fragmentRun, workaroundFlags, extendedTypeScratch);
sampleIndex = 0;
}
/**
* Parses a trex atom (defined in 14496-12).
*/
private static DefaultSampleValues parseTrex(ParsableByteArray trex) {
trex.setPosition(Atom.FULL_HEADER_SIZE + 4);
int defaultSampleDescriptionIndex = trex.readUnsignedIntToInt() - 1;
int defaultSampleDuration = trex.readUnsignedIntToInt();
int defaultSampleSize = trex.readUnsignedIntToInt();
int defaultSampleFlags = trex.readInt();
return new DefaultSampleValues(defaultSampleDescriptionIndex, defaultSampleDuration,
defaultSampleSize, defaultSampleFlags);
}
private static void parseMoof(Track track, DefaultSampleValues extendsDefaults,
ContainerAtom moof, TrackFragment out, int workaroundFlags, byte[] extendedTypeScratch) {
parseTraf(track, extendsDefaults, moof.getContainerAtomOfType(Atom.TYPE_traf),
out, workaroundFlags, extendedTypeScratch);
}
/**
* Parses a traf atom (defined in 14496-12).
*/
private static void parseTraf(Track track, DefaultSampleValues extendsDefaults,
ContainerAtom traf, TrackFragment out, int workaroundFlags, byte[] extendedTypeScratch) {
LeafAtom tfdtAtom = traf.getLeafAtomOfType(Atom.TYPE_tfdt);
long decodeTime = tfdtAtom == null ? 0 : parseTfdt(traf.getLeafAtomOfType(Atom.TYPE_tfdt).data);
LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd);
DefaultSampleValues fragmentHeader = parseTfhd(extendsDefaults, tfhd.data);
out.sampleDescriptionIndex = fragmentHeader.sampleDescriptionIndex;
LeafAtom trun = traf.getLeafAtomOfType(Atom.TYPE_trun);
parseTrun(track, fragmentHeader, decodeTime, workaroundFlags, trun.data, out);
LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz);
if (saiz != null) {
TrackEncryptionBox trackEncryptionBox =
track.sampleDescriptionEncryptionBoxes[fragmentHeader.sampleDescriptionIndex];
parseSaiz(trackEncryptionBox, saiz.data, out);
}
LeafAtom senc = traf.getLeafAtomOfType(Atom.TYPE_senc);
if (senc != null) {
parseSenc(senc.data, out);
}
int childrenSize = traf.leafChildren.size();
for (int i = 0; i < childrenSize; i++) {
LeafAtom atom = traf.leafChildren.get(i);
if (atom.type == Atom.TYPE_uuid) {
parseUuid(atom.data, out, extendedTypeScratch);
}
}
}
private static void parseSaiz(TrackEncryptionBox encryptionBox, ParsableByteArray saiz,
TrackFragment out) {
int vectorSize = encryptionBox.initializationVectorSize;
saiz.setPosition(Atom.HEADER_SIZE);
int fullAtom = saiz.readInt();
int flags = Atom.parseFullAtomFlags(fullAtom);
if ((flags & 0x01) == 1) {
saiz.skip(8);
}
int defaultSampleInfoSize = saiz.readUnsignedByte();
int sampleCount = saiz.readUnsignedIntToInt();
if (sampleCount != out.length) {
throw new IllegalStateException("Length mismatch: " + sampleCount + ", " + out.length);
}
int totalSize = 0;
if (defaultSampleInfoSize == 0) {
boolean[] sampleHasSubsampleEncryptionTable = out.sampleHasSubsampleEncryptionTable;
for (int i = 0; i < sampleCount; i++) {
int sampleInfoSize = saiz.readUnsignedByte();
totalSize += sampleInfoSize;
sampleHasSubsampleEncryptionTable[i] = sampleInfoSize > vectorSize;
}
} else {
boolean subsampleEncryption = defaultSampleInfoSize > vectorSize;
totalSize += defaultSampleInfoSize * sampleCount;
Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption);
}
out.initEncryptionData(totalSize);
}
/**
* Parses a tfhd atom (defined in 14496-12).
*
* @param extendsDefaults Default sample values from the trex atom.
* @return The parsed default sample values.
*/
private static DefaultSampleValues parseTfhd(DefaultSampleValues extendsDefaults,
ParsableByteArray tfhd) {
tfhd.setPosition(Atom.HEADER_SIZE);
int fullAtom = tfhd.readInt();
int flags = Atom.parseFullAtomFlags(fullAtom);
tfhd.skip(4); // trackId
if ((flags & 0x01 /* base_data_offset_present */) != 0) {
tfhd.skip(8);
}
int defaultSampleDescriptionIndex =
((flags & 0x02 /* default_sample_description_index_present */) != 0)
? tfhd.readUnsignedIntToInt() - 1 : extendsDefaults.sampleDescriptionIndex;
int defaultSampleDuration = ((flags & 0x08 /* default_sample_duration_present */) != 0)
? tfhd.readUnsignedIntToInt() : extendsDefaults.duration;
int defaultSampleSize = ((flags & 0x10 /* default_sample_size_present */) != 0)
? tfhd.readUnsignedIntToInt() : extendsDefaults.size;
int defaultSampleFlags = ((flags & 0x20 /* default_sample_flags_present */) != 0)
? tfhd.readUnsignedIntToInt() : extendsDefaults.flags;
return new DefaultSampleValues(defaultSampleDescriptionIndex, defaultSampleDuration,
defaultSampleSize, defaultSampleFlags);
}
/**
* Parses a tfdt atom (defined in 14496-12).
*
* @return baseMediaDecodeTime The sum of the decode durations of all earlier samples in the
* media, expressed in the media's timescale.
*/
private static long parseTfdt(ParsableByteArray tfdt) {
tfdt.setPosition(Atom.HEADER_SIZE);
int fullAtom = tfdt.readInt();
int version = Atom.parseFullAtomVersion(fullAtom);
return version == 1 ? tfdt.readUnsignedLongToLong() : tfdt.readUnsignedInt();
}
/**
* Parses a trun atom (defined in 14496-12).
*
* @param track The corresponding track.
* @param defaultSampleValues Default sample values.
* @param decodeTime The decode time.
* @param trun The trun atom to parse.
* @param out The {@TrackFragment} into which parsed data should be placed.
*/
private static void parseTrun(Track track, DefaultSampleValues defaultSampleValues,
long decodeTime, int workaroundFlags, ParsableByteArray trun, TrackFragment out) {
trun.setPosition(Atom.HEADER_SIZE);
int fullAtom = trun.readInt();
int flags = Atom.parseFullAtomFlags(fullAtom);
int sampleCount = trun.readUnsignedIntToInt();
if ((flags & 0x01 /* data_offset_present */) != 0) {
trun.skip(4);
}
boolean firstSampleFlagsPresent = (flags & 0x04 /* first_sample_flags_present */) != 0;
int firstSampleFlags = defaultSampleValues.flags;
if (firstSampleFlagsPresent) {
firstSampleFlags = trun.readUnsignedIntToInt();
}
boolean sampleDurationsPresent = (flags & 0x100 /* sample_duration_present */) != 0;
boolean sampleSizesPresent = (flags & 0x200 /* sample_size_present */) != 0;
boolean sampleFlagsPresent = (flags & 0x400 /* sample_flags_present */) != 0;
boolean sampleCompositionTimeOffsetsPresent =
(flags & 0x800 /* sample_composition_time_offsets_present */) != 0;
out.initTables(sampleCount);
int[] sampleSizeTable = out.sampleSizeTable;
int[] sampleCompositionTimeOffsetTable = out.sampleCompositionTimeOffsetTable;
long[] sampleDecodingTimeTable = out.sampleDecodingTimeTable;
boolean[] sampleIsSyncFrameTable = out.sampleIsSyncFrameTable;
long timescale = track.timescale;
long cumulativeTime = decodeTime;
boolean workaroundEveryVideoFrameIsSyncFrame = track.type == Track.TYPE_VIDEO
&& ((workaroundFlags & WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME)
== WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME);
for (int i = 0; i < sampleCount; i++) {
// Use trun values if present, otherwise tfhd, otherwise trex.
int sampleDuration = sampleDurationsPresent ? trun.readUnsignedIntToInt()
: defaultSampleValues.duration;
int sampleSize = sampleSizesPresent ? trun.readUnsignedIntToInt() : defaultSampleValues.size;
int sampleFlags = (i == 0 && firstSampleFlagsPresent) ? firstSampleFlags
: sampleFlagsPresent ? trun.readInt() : defaultSampleValues.flags;
if (sampleCompositionTimeOffsetsPresent) {
// The BMFF spec (ISO 14496-12) states that sample offsets should be unsigned integers in
// version 0 trun boxes, however a significant number of streams violate the spec and use
// signed integers instead. It's safe to always parse sample offsets as signed integers
// here, because unsigned integers will still be parsed correctly (unless their top bit is
// set, which is never true in practice because sample offsets are always small).
int sampleOffset = trun.readInt();
sampleCompositionTimeOffsetTable[i] = (int) ((sampleOffset * 1000) / timescale);
} else {
sampleCompositionTimeOffsetTable[i] = 0;
}
sampleDecodingTimeTable[i] = (cumulativeTime * 1000) / timescale;
sampleSizeTable[i] = sampleSize;
sampleIsSyncFrameTable[i] = ((sampleFlags >> 16) & 0x1) == 0
&& (!workaroundEveryVideoFrameIsSyncFrame || i == 0);
cumulativeTime += sampleDuration;
}
}
private static void parseUuid(ParsableByteArray uuid, TrackFragment out,
byte[] extendedTypeScratch) {
uuid.setPosition(Atom.HEADER_SIZE);
uuid.readBytes(extendedTypeScratch, 0, 16);
// Currently this parser only supports Microsoft's PIFF SampleEncryptionBox.
if (!Arrays.equals(extendedTypeScratch, PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE)) {
return;
}
// Except for the extended type, this box is identical to a SENC box. See "Portable encoding of
// audio-video objects: The Protected Interoperable File Format (PIFF), John A. Bocharov et al,
// Section 5.3.2.1."
parseSenc(uuid, 16, out);
}
private static void parseSenc(ParsableByteArray senc, TrackFragment out) {
parseSenc(senc, 0, out);
}
private static void parseSenc(ParsableByteArray senc, int offset, TrackFragment out) {
senc.setPosition(Atom.HEADER_SIZE + offset);
int fullAtom = senc.readInt();
int flags = Atom.parseFullAtomFlags(fullAtom);
if ((flags & 0x01 /* override_track_encryption_box_parameters */) != 0) {
// TODO: Implement this.
throw new IllegalStateException("Overriding TrackEncryptionBox parameters is unsupported");
}
boolean subsampleEncryption = (flags & 0x02 /* use_subsample_encryption */) != 0;
int sampleCount = senc.readUnsignedIntToInt();
if (sampleCount != out.length) {
throw new IllegalStateException("Length mismatch: " + sampleCount + ", " + out.length);
}
Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption);
out.initEncryptionData(senc.bytesLeft());
out.fillEncryptionData(senc);
}
/**
* Parses a sidx atom (defined in 14496-12).
*/
private static ChunkIndex parseSidx(ParsableByteArray atom, long inputPosition) {
atom.setPosition(Atom.HEADER_SIZE);
int fullAtom = atom.readInt();
int version = Atom.parseFullAtomVersion(fullAtom);
atom.skip(4);
long timescale = atom.readUnsignedInt();
long earliestPresentationTime;
long offset = inputPosition;
if (version == 0) {
earliestPresentationTime = atom.readUnsignedInt();
offset += atom.readUnsignedInt();
} else {
earliestPresentationTime = atom.readUnsignedLongToLong();
offset += atom.readUnsignedLongToLong();
}
atom.skip(2);
int referenceCount = atom.readUnsignedShort();
int[] sizes = new int[referenceCount];
long[] offsets = new long[referenceCount];
long[] durationsUs = new long[referenceCount];
long[] timesUs = new long[referenceCount];
long time = earliestPresentationTime;
long timeUs = Util.scaleLargeTimestamp(time, C.MICROS_PER_SECOND, timescale);
for (int i = 0; i < referenceCount; i++) {
int firstInt = atom.readInt();
int type = 0x80000000 & firstInt;
if (type != 0) {
throw new IllegalStateException("Unhandled indirect reference");
}
long referenceDuration = atom.readUnsignedInt();
sizes[i] = 0x7fffffff & firstInt;
offsets[i] = offset;
// Calculate time and duration values such that any rounding errors are consistent. i.e. That
// timesUs[i] + durationsUs[i] == timesUs[i + 1].
timesUs[i] = timeUs;
time += referenceDuration;
timeUs = Util.scaleLargeTimestamp(time, C.MICROS_PER_SECOND, timescale);
durationsUs[i] = timeUs - timesUs[i];
atom.skip(4);
offset += sizes[i];
}
return new ChunkIndex(sizes, offsets, durationsUs, timesUs);
}
private void readEncryptionData(ExtractorInput input) throws IOException, InterruptedException {
fragmentRun.fillEncryptionData(input);
parserState = STATE_READING_SAMPLE_START;
}
/**
* Attempts to extract the next sample in the current mdat atom.
* <p>
* If there are no more samples in the current mdat atom then the parser state is transitioned
* to {@link #STATE_READING_ATOM_HEADER} and {@code false} is returned.
* <p>
* It is possible for a sample to be extracted in part in the case that an exception is thrown. In
* this case the method can be called again to extract the remainder of the sample.
*
* @param input The {@link ExtractorInput} from which to read data.
* @return True if a sample was extracted. False otherwise.
* @throws IOException If an error occurs reading from the input.
* @throws InterruptedException If the thread is interrupted.
*/
private boolean readSample(ExtractorInput input) throws IOException, InterruptedException {
if (sampleIndex >= fragmentRun.length) {
// We've run out of samples in the current mdat atom.
parserState = STATE_READING_ATOM_HEADER;
return false;
}
if (parserState == STATE_READING_SAMPLE_START) {
sampleSize = fragmentRun.sampleSizeTable[sampleIndex];
if (fragmentRun.definesEncryptionData) {
sampleBytesWritten = appendSampleEncryptionData(fragmentRun.sampleEncryptionData);
sampleSize += sampleBytesWritten;
} else {
sampleBytesWritten = 0;
}
sampleCurrentNalBytesRemaining = 0;
parserState = STATE_READING_SAMPLE_CONTINUE;
}
if (track.type == Track.TYPE_VIDEO) {
while (sampleBytesWritten < sampleSize) {
// NAL units are length delimited, but the decoder requires start code delimited units.
if (sampleCurrentNalBytesRemaining == 0) {
// Read the NAL length so that we know where we find the next NAL unit.
input.readFully(nalLength.data, 0, 4);
nalLength.setPosition(0);
sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt();
// Write a start code for the current NAL unit.
nalStartCode.setPosition(0);
trackOutput.sampleData(nalStartCode, 4);
sampleBytesWritten += 4;
} else {
// Write the payload of the NAL unit.
int writtenBytes = trackOutput.sampleData(input, sampleCurrentNalBytesRemaining);
sampleBytesWritten += writtenBytes;
sampleCurrentNalBytesRemaining -= writtenBytes;
}
}
} else {
while (sampleBytesWritten < sampleSize) {
int writtenBytes = trackOutput.sampleData(input, sampleSize - sampleBytesWritten);
sampleBytesWritten += writtenBytes;
}
}
long sampleTimeUs = fragmentRun.getSamplePresentationTime(sampleIndex) * 1000L;
int sampleFlags = (fragmentRun.definesEncryptionData ? C.SAMPLE_FLAG_ENCRYPTED : 0)
| (fragmentRun.sampleIsSyncFrameTable[sampleIndex] ? C.SAMPLE_FLAG_SYNC : 0);
byte[] encryptionKey = fragmentRun.definesEncryptionData
? track.sampleDescriptionEncryptionBoxes[fragmentRun.sampleDescriptionIndex].keyId : null;
trackOutput.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, encryptionKey);
sampleIndex++;
parserState = STATE_READING_SAMPLE_START;
return true;
}
private int appendSampleEncryptionData(ParsableByteArray sampleEncryptionData) {
TrackEncryptionBox encryptionBox =
track.sampleDescriptionEncryptionBoxes[fragmentRun.sampleDescriptionIndex];
int vectorSize = encryptionBox.initializationVectorSize;
boolean subsampleEncryption = fragmentRun.sampleHasSubsampleEncryptionTable[sampleIndex];
// Write the signal byte, containing the vector size and the subsample encryption flag.
encryptionSignalByte.data[0] = (byte) (vectorSize | (subsampleEncryption ? 0x80 : 0));
encryptionSignalByte.setPosition(0);
trackOutput.sampleData(encryptionSignalByte, 1);
// Write the vector.
trackOutput.sampleData(sampleEncryptionData, vectorSize);
// If we don't have subsample encryption data, we're done.
if (!subsampleEncryption) {
return 1 + vectorSize;
}
// Write the subsample encryption data.
int subsampleCount = sampleEncryptionData.readUnsignedShort();
sampleEncryptionData.skip(-2);
int subsampleDataLength = 2 + 6 * subsampleCount;
trackOutput.sampleData(sampleEncryptionData, subsampleDataLength);
return 1 + vectorSize + subsampleDataLength;
}
/** Returns whether the extractor should parse an atom with type {@code atom}. */
private static boolean shouldParseAtom(int atom) {
return atom == Atom.TYPE_avc1 || atom == Atom.TYPE_avc3 || atom == Atom.TYPE_esds
|| atom == Atom.TYPE_hdlr || atom == Atom.TYPE_mdat || atom == Atom.TYPE_mdhd
|| atom == Atom.TYPE_moof || atom == Atom.TYPE_moov || atom == Atom.TYPE_mp4a
|| atom == Atom.TYPE_mvhd || atom == Atom.TYPE_sidx || atom == Atom.TYPE_stsd
|| atom == Atom.TYPE_tfdt || atom == Atom.TYPE_tfhd || atom == Atom.TYPE_tkhd
|| atom == Atom.TYPE_traf || atom == Atom.TYPE_trak || atom == Atom.TYPE_trex
|| atom == Atom.TYPE_trun || atom == Atom.TYPE_mvex || atom == Atom.TYPE_mdia
|| atom == Atom.TYPE_minf || atom == Atom.TYPE_stbl || atom == Atom.TYPE_pssh
|| atom == Atom.TYPE_saiz || atom == Atom.TYPE_uuid || atom == Atom.TYPE_senc
|| atom == Atom.TYPE_pasp;
}
/** Returns whether the extractor should parse a container atom with type {@code atom}. */
private static boolean shouldParseContainerAtom(int atom) {
return atom == Atom.TYPE_moov || atom == Atom.TYPE_trak || atom == Atom.TYPE_mdia
|| atom == Atom.TYPE_minf || atom == Atom.TYPE_stbl || atom == Atom.TYPE_avcC
|| atom == Atom.TYPE_moof || atom == Atom.TYPE_traf || atom == Atom.TYPE_mvex;
}
}

View file

@ -0,0 +1,371 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.extractor.mp4;
import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.ExtractorOutput;
import com.google.android.exoplayer.extractor.PositionHolder;
import com.google.android.exoplayer.extractor.SeekMap;
import com.google.android.exoplayer.extractor.TrackOutput;
import com.google.android.exoplayer.extractor.mp4.Atom.ContainerAtom;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.H264Util;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableByteArray;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
/**
* Extracts data from an unfragmented MP4 file.
*/
public final class Mp4Extractor implements Extractor, SeekMap {
// Parser states.
private static final int STATE_READING_ATOM_HEADER = 0;
private static final int STATE_READING_ATOM_PAYLOAD = 1;
private static final int STATE_READING_SAMPLE = 2;
/**
* When seeking within the source, if the offset is greater than or equal to this value (or the
* offset is negative), the source will be reloaded.
*/
private static final int RELOAD_MINIMUM_SEEK_DISTANCE = 256 * 1024;
// Temporary arrays.
private final ParsableByteArray nalStartCode;
private final ParsableByteArray nalLength;
private final ParsableByteArray atomHeader;
private final Stack<ContainerAtom> containerAtoms;
private int parserState;
private long rootAtomBytesRead;
private int atomType;
private long atomSize;
private int atomBytesRead;
private ParsableByteArray atomData;
private int sampleSize;
private int sampleBytesWritten;
private int sampleCurrentNalBytesRemaining;
// Extractor outputs.
private ExtractorOutput extractorOutput;
private Mp4Track[] tracks;
public Mp4Extractor() {
atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE);
containerAtoms = new Stack<Atom.ContainerAtom>();
nalStartCode = new ParsableByteArray(H264Util.NAL_START_CODE);
nalLength = new ParsableByteArray(4);
parserState = STATE_READING_ATOM_HEADER;
}
@Override
public void init(ExtractorOutput output) {
extractorOutput = output;
}
@Override
public void seek() {
rootAtomBytesRead = 0;
sampleBytesWritten = 0;
sampleCurrentNalBytesRemaining = 0;
}
@Override
public int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException {
while (true) {
switch (parserState) {
case STATE_READING_ATOM_HEADER:
if (!readAtomHeader(input)) {
return RESULT_END_OF_INPUT;
}
break;
case STATE_READING_ATOM_PAYLOAD:
if (readAtomPayload(input, seekPosition)) {
return RESULT_SEEK;
}
break;
default:
return readSample(input, seekPosition);
}
}
}
// SeekMap implementation.
@Override
public long getPosition(long timeUs) {
long earliestSamplePosition = Long.MAX_VALUE;
for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) {
TrackSampleTable sampleTable = tracks[trackIndex].sampleTable;
int sampleIndex = sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(timeUs);
if (sampleIndex == TrackSampleTable.NO_SAMPLE) {
sampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs);
}
tracks[trackIndex].sampleIndex = sampleIndex;
long offset = sampleTable.offsets[tracks[trackIndex].sampleIndex];
if (offset < earliestSamplePosition) {
earliestSamplePosition = offset;
}
}
return earliestSamplePosition;
}
private boolean readAtomHeader(ExtractorInput input) throws IOException, InterruptedException {
if (!input.readFully(atomHeader.data, 0, Atom.HEADER_SIZE, true)) {
return false;
}
atomHeader.setPosition(0);
atomSize = atomHeader.readUnsignedInt();
atomType = atomHeader.readInt();
if (atomSize == Atom.LONG_SIZE_PREFIX) {
// The extended atom size is contained in the next 8 bytes, so try to read it now.
input.readFully(atomHeader.data, Atom.HEADER_SIZE, Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE);
atomSize = atomHeader.readLong();
rootAtomBytesRead += Atom.LONG_HEADER_SIZE;
atomBytesRead = Atom.LONG_HEADER_SIZE;
} else {
rootAtomBytesRead += Atom.HEADER_SIZE;
atomBytesRead = Atom.HEADER_SIZE;
}
if (shouldParseContainerAtom(atomType)) {
if (atomSize == Atom.LONG_SIZE_PREFIX) {
containerAtoms.add(
new ContainerAtom(atomType, rootAtomBytesRead + atomSize - atomBytesRead));
} else {
containerAtoms.add(
new ContainerAtom(atomType, rootAtomBytesRead + atomSize - atomBytesRead));
}
parserState = STATE_READING_ATOM_HEADER;
} else if (shouldParseLeafAtom(atomType)) {
Assertions.checkState(atomSize < Integer.MAX_VALUE);
atomData = new ParsableByteArray((int) atomSize);
System.arraycopy(atomHeader.data, 0, atomData.data, 0, Atom.HEADER_SIZE);
parserState = STATE_READING_ATOM_PAYLOAD;
} else {
atomData = null;
parserState = STATE_READING_ATOM_PAYLOAD;
}
return true;
}
/**
* Processes the atom payload. If {@link #atomData} is null and the size is at or above the
* threshold {@link #RELOAD_MINIMUM_SEEK_DISTANCE}, {@code true} is returned and the caller should
* restart loading at the position in {@code positionHolder}. Otherwise, the atom is read/skipped.
*/
private boolean readAtomPayload(ExtractorInput input, PositionHolder positionHolder)
throws IOException, InterruptedException {
parserState = STATE_READING_ATOM_HEADER;
rootAtomBytesRead += atomSize - atomBytesRead;
long atomRemainingBytes = atomSize - atomBytesRead;
boolean seekRequired = atomData == null
&& (atomSize >= RELOAD_MINIMUM_SEEK_DISTANCE || atomSize > Integer.MAX_VALUE);
if (seekRequired) {
positionHolder.position = rootAtomBytesRead;
} else if (atomData != null) {
input.readFully(atomData.data, atomBytesRead, (int) atomRemainingBytes);
if (!containerAtoms.isEmpty()) {
containerAtoms.peek().add(new Atom.LeafAtom(atomType, atomData));
}
} else {
input.skipFully((int) atomRemainingBytes);
}
while (!containerAtoms.isEmpty() && containerAtoms.peek().endByteOffset == rootAtomBytesRead) {
Atom.ContainerAtom containerAtom = containerAtoms.pop();
if (containerAtom.type == Atom.TYPE_moov) {
processMoovAtom(containerAtom);
} else if (!containerAtoms.isEmpty()) {
containerAtoms.peek().add(containerAtom);
}
}
return seekRequired;
}
/** Updates the stored track metadata to reflect the contents of the specified moov atom. */
private void processMoovAtom(ContainerAtom moov) {
List<Mp4Track> tracks = new ArrayList<Mp4Track>();
long earliestSampleOffset = Long.MAX_VALUE;
for (int i = 0; i < moov.containerChildren.size(); i++) {
Atom.ContainerAtom atom = moov.containerChildren.get(i);
if (atom.type != Atom.TYPE_trak) {
continue;
}
Track track = AtomParsers.parseTrak(atom, moov.getLeafAtomOfType(Atom.TYPE_mvhd));
if (track == null || (track.type != Track.TYPE_AUDIO && track.type != Track.TYPE_VIDEO)) {
continue;
}
Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia)
.getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl);
TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom);
if (trackSampleTable.sampleCount == 0) {
continue;
}
Mp4Track mp4Track = new Mp4Track(track, trackSampleTable, extractorOutput.track(i));
mp4Track.trackOutput.format(track.mediaFormat);
tracks.add(mp4Track);
long firstSampleOffset = trackSampleTable.offsets[0];
if (firstSampleOffset < earliestSampleOffset) {
earliestSampleOffset = firstSampleOffset;
}
}
this.tracks = tracks.toArray(new Mp4Track[0]);
extractorOutput.endTracks();
extractorOutput.seekMap(this);
parserState = STATE_READING_SAMPLE;
}
/**
* Attempts to extract the next sample in the current mdat atom for the specified track.
* <p>
* Returns {@link #RESULT_SEEK} if the source should be reloaded from the position in
* {@code positionHolder}.
* <p>
* Returns {@link #RESULT_END_OF_INPUT} if no samples are left. Otherwise, returns
* {@link #RESULT_CONTINUE}.
*
* @param input The {@link ExtractorInput} from which to read data.
* @param positionHolder If {@link #RESULT_SEEK} is returned, this holder is updated to hold the
* position of the required data.
* @return One of the {@code RESULT_*} flags in {@link Extractor}.
* @throws IOException If an error occurs reading from the input.
* @throws InterruptedException If the thread is interrupted.
*/
private int readSample(ExtractorInput input, PositionHolder positionHolder)
throws IOException, InterruptedException {
int trackIndex = getTrackIndexOfEarliestCurrentSample();
if (trackIndex == TrackSampleTable.NO_SAMPLE) {
return RESULT_END_OF_INPUT;
}
Mp4Track track = tracks[trackIndex];
int sampleIndex = track.sampleIndex;
long position = track.sampleTable.offsets[sampleIndex];
long skipAmount = position - input.getPosition() + sampleBytesWritten;
if (skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE) {
positionHolder.position = position;
return RESULT_SEEK;
}
input.skipFully((int) skipAmount);
sampleSize = track.sampleTable.sizes[sampleIndex];
if (track.track.type == Track.TYPE_VIDEO
&& MimeTypes.VIDEO_H264.equals(track.track.mediaFormat.mimeType)) {
while (sampleBytesWritten < sampleSize) {
// NAL units are length delimited, but the decoder requires start code delimited units.
if (sampleCurrentNalBytesRemaining == 0) {
// Read the NAL length so that we know where we find the next NAL unit.
input.readFully(nalLength.data, 0, 4);
nalLength.setPosition(0);
sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt();
// Write a start code for the current NAL unit.
nalStartCode.setPosition(0);
track.trackOutput.sampleData(nalStartCode, 4);
sampleBytesWritten += 4;
} else {
// Write the payload of the NAL unit.
int writtenBytes = track.trackOutput.sampleData(input, sampleCurrentNalBytesRemaining);
sampleBytesWritten += writtenBytes;
sampleCurrentNalBytesRemaining -= writtenBytes;
}
}
} else {
while (sampleBytesWritten < sampleSize) {
int writtenBytes = track.trackOutput.sampleData(input, sampleSize - sampleBytesWritten);
sampleBytesWritten += writtenBytes;
sampleCurrentNalBytesRemaining -= writtenBytes;
}
}
track.trackOutput.sampleMetadata(track.sampleTable.timestampsUs[sampleIndex],
track.sampleTable.flags[sampleIndex], sampleSize, 0, null);
track.sampleIndex++;
sampleBytesWritten = 0;
sampleCurrentNalBytesRemaining = 0;
return RESULT_CONTINUE;
}
/**
* Returns the index of the track that contains the earliest current sample, or
* {@link TrackSampleTable#NO_SAMPLE} if no samples remain.
*/
private int getTrackIndexOfEarliestCurrentSample() {
int earliestSampleTrackIndex = TrackSampleTable.NO_SAMPLE;
long earliestSampleOffset = Long.MAX_VALUE;
for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) {
Mp4Track track = tracks[trackIndex];
int sampleIndex = track.sampleIndex;
if (sampleIndex == track.sampleTable.sampleCount) {
continue;
}
long trackSampleOffset = track.sampleTable.offsets[sampleIndex];
if (trackSampleOffset < earliestSampleOffset) {
earliestSampleOffset = trackSampleOffset;
earliestSampleTrackIndex = trackIndex;
}
}
return earliestSampleTrackIndex;
}
/** Returns whether the extractor should parse a leaf atom with type {@code atom}. */
private static boolean shouldParseLeafAtom(int atom) {
return atom == Atom.TYPE_mdhd || atom == Atom.TYPE_mvhd || atom == Atom.TYPE_hdlr
|| atom == Atom.TYPE_vmhd || atom == Atom.TYPE_smhd || atom == Atom.TYPE_stsd
|| atom == Atom.TYPE_avc1 || atom == Atom.TYPE_avcC || atom == Atom.TYPE_mp4a
|| atom == Atom.TYPE_esds || atom == Atom.TYPE_stts || atom == Atom.TYPE_stss
|| atom == Atom.TYPE_ctts || atom == Atom.TYPE_stsc || atom == Atom.TYPE_stsz
|| atom == Atom.TYPE_stco || atom == Atom.TYPE_co64 || atom == Atom.TYPE_tkhd;
}
/** Returns whether the extractor should parse a container atom with type {@code atom}. */
private static boolean shouldParseContainerAtom(int atom) {
return atom == Atom.TYPE_moov || atom == Atom.TYPE_trak || atom == Atom.TYPE_mdia
|| atom == Atom.TYPE_minf || atom == Atom.TYPE_stbl;
}
private static final class Mp4Track {
public final Track track;
public final TrackSampleTable sampleTable;
public final TrackOutput trackOutput;
public int sampleIndex;
public Mp4Track(Track track, TrackSampleTable sampleTable, TrackOutput trackOutput) {
this.track = track;
this.sampleTable = sampleTable;
this.trackOutput = trackOutput;
}
}
}

View file

@ -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.

View file

@ -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 {
/**

View file

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

View file

@ -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;
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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) {

View file

@ -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;
}

View file

@ -1,200 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.mp4;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.ParsableByteArray;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public abstract class Atom {
/** Size of an atom header, in bytes. */
public static final int ATOM_HEADER_SIZE = 8;
/** Size of a long atom header, in bytes. */
public static final int LONG_ATOM_HEADER_SIZE = 16;
/** Size of a full atom header, in bytes. */
public static final int FULL_ATOM_HEADER_SIZE = 12;
/** Value for the first 32 bits of atomSize when the atom size is actually a long value. */
public static final int LONG_SIZE_PREFIX = 1;
public static final int TYPE_ftyp = getAtomTypeInteger("ftyp");
public static final int TYPE_avc1 = getAtomTypeInteger("avc1");
public static final int TYPE_avc3 = getAtomTypeInteger("avc3");
public static final int TYPE_esds = getAtomTypeInteger("esds");
public static final int TYPE_mdat = getAtomTypeInteger("mdat");
public static final int TYPE_mp4a = getAtomTypeInteger("mp4a");
public static final int TYPE_ac_3 = getAtomTypeInteger("ac-3");
public static final int TYPE_dac3 = getAtomTypeInteger("dac3");
public static final int TYPE_ec_3 = getAtomTypeInteger("ec-3");
public static final int TYPE_dec3 = getAtomTypeInteger("dec3");
public static final int TYPE_tfdt = getAtomTypeInteger("tfdt");
public static final int TYPE_tfhd = getAtomTypeInteger("tfhd");
public static final int TYPE_trex = getAtomTypeInteger("trex");
public static final int TYPE_trun = getAtomTypeInteger("trun");
public static final int TYPE_sidx = getAtomTypeInteger("sidx");
public static final int TYPE_moov = getAtomTypeInteger("moov");
public static final int TYPE_mvhd = getAtomTypeInteger("mvhd");
public static final int TYPE_trak = getAtomTypeInteger("trak");
public static final int TYPE_mdia = getAtomTypeInteger("mdia");
public static final int TYPE_minf = getAtomTypeInteger("minf");
public static final int TYPE_stbl = getAtomTypeInteger("stbl");
public static final int TYPE_avcC = getAtomTypeInteger("avcC");
public static final int TYPE_moof = getAtomTypeInteger("moof");
public static final int TYPE_traf = getAtomTypeInteger("traf");
public static final int TYPE_mvex = getAtomTypeInteger("mvex");
public static final int TYPE_tkhd = getAtomTypeInteger("tkhd");
public static final int TYPE_mdhd = getAtomTypeInteger("mdhd");
public static final int TYPE_hdlr = getAtomTypeInteger("hdlr");
public static final int TYPE_stsd = getAtomTypeInteger("stsd");
public static final int TYPE_pssh = getAtomTypeInteger("pssh");
public static final int TYPE_sinf = getAtomTypeInteger("sinf");
public static final int TYPE_schm = getAtomTypeInteger("schm");
public static final int TYPE_schi = getAtomTypeInteger("schi");
public static final int TYPE_tenc = getAtomTypeInteger("tenc");
public static final int TYPE_encv = getAtomTypeInteger("encv");
public static final int TYPE_enca = getAtomTypeInteger("enca");
public static final int TYPE_frma = getAtomTypeInteger("frma");
public static final int TYPE_saiz = getAtomTypeInteger("saiz");
public static final int TYPE_uuid = getAtomTypeInteger("uuid");
public static final int TYPE_senc = getAtomTypeInteger("senc");
public static final int TYPE_pasp = getAtomTypeInteger("pasp");
public static final int TYPE_TTML = getAtomTypeInteger("TTML");
public static final int TYPE_vmhd = getAtomTypeInteger("vmhd");
public static final int TYPE_smhd = getAtomTypeInteger("smhd");
public static final int TYPE_mp4v = getAtomTypeInteger("mp4v");
public static final int TYPE_stts = getAtomTypeInteger("stts");
public static final int TYPE_stss = getAtomTypeInteger("stss");
public static final int TYPE_ctts = getAtomTypeInteger("ctts");
public static final int TYPE_stsc = getAtomTypeInteger("stsc");
public static final int TYPE_stsz = getAtomTypeInteger("stsz");
public static final int TYPE_stco = getAtomTypeInteger("stco");
public static final int TYPE_co64 = getAtomTypeInteger("co64");
public final int type;
Atom(int type) {
this.type = type;
}
@Override
public String toString() {
return getAtomTypeString(type);
}
/** An MP4 atom that is a leaf. */
public static final class LeafAtom extends Atom {
public final ParsableByteArray data;
public LeafAtom(int type, ParsableByteArray data) {
super(type);
this.data = data;
}
}
/** An MP4 atom that has child atoms. */
public static final class ContainerAtom extends Atom {
public final long endByteOffset;
public final List<LeafAtom> leafChildren;
public final List<ContainerAtom> containerChildren;
public ContainerAtom(int type, long endByteOffset) {
super(type);
leafChildren = new ArrayList<LeafAtom>();
containerChildren = new ArrayList<ContainerAtom>();
this.endByteOffset = endByteOffset;
}
public void add(LeafAtom atom) {
leafChildren.add(atom);
}
public void add(ContainerAtom atom) {
containerChildren.add(atom);
}
public LeafAtom getLeafAtomOfType(int type) {
int childrenSize = leafChildren.size();
for (int i = 0; i < childrenSize; i++) {
LeafAtom atom = leafChildren.get(i);
if (atom.type == type) {
return atom;
}
}
return null;
}
public ContainerAtom getContainerAtomOfType(int type) {
int childrenSize = containerChildren.size();
for (int i = 0; i < childrenSize; i++) {
ContainerAtom atom = containerChildren.get(i);
if (atom.type == type) {
return atom;
}
}
return null;
}
@Override
public String toString() {
return getAtomTypeString(type)
+ " leaves: " + Arrays.toString(leafChildren.toArray(new LeafAtom[0]))
+ " containers: " + Arrays.toString(containerChildren.toArray(new ContainerAtom[0]));
}
}
/**
* Parses the version number out of the additional integer component of a full atom.
*/
public static int parseFullAtomVersion(int fullAtomInt) {
return 0x000000FF & (fullAtomInt >> 24);
}
/**
* Parses the atom flags out of the additional integer component of a full atom.
*/
public static int parseFullAtomFlags(int fullAtomInt) {
return 0x00FFFFFF & fullAtomInt;
}
private static String getAtomTypeString(int type) {
return "" + (char) (type >> 24)
+ (char) ((type >> 16) & 0xFF)
+ (char) ((type >> 8) & 0xFF)
+ (char) (type & 0xFF);
}
private static int getAtomTypeInteger(String typeName) {
Assertions.checkArgument(typeName.length() == 4);
int result = 0;
for (int i = 0; i < 4; i++) {
result <<= 8;
result |= typeName.charAt(i);
}
return result;
}
}

View file

@ -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;

View file

@ -1,743 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.source;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.mp4.Atom;
import com.google.android.exoplayer.mp4.Atom.ContainerAtom;
import com.google.android.exoplayer.mp4.CommonMp4AtomParsers;
import com.google.android.exoplayer.mp4.Mp4TrackSampleTable;
import com.google.android.exoplayer.mp4.Track;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.upstream.BufferedNonBlockingInputStream;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSourceStream;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.Loader;
import com.google.android.exoplayer.upstream.Loader.Loadable;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.H264Util;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableByteArray;
import com.google.android.exoplayer.util.Util;
import android.util.Log;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Stack;
/**
* Extracts data from a {@link DataSpec} in unfragmented MP4 format (ISO 14496-12).
*/
public final class Mp4SampleExtractor implements SampleExtractor, Loader.Callback {
private static final String TAG = "Mp4SampleExtractor";
private static final String LOADER_THREAD_NAME = "Mp4SampleExtractor";
private static final int NO_TRACK = -1;
// Reading results
private static final int RESULT_NEED_MORE_DATA = 1;
private static final int RESULT_END_OF_STREAM = 2;
// Parser states
private static final int STATE_READING_ATOM_HEADER = 0;
private static final int STATE_READING_ATOM_PAYLOAD = 1;
/** Set of atom types that contain data to be parsed. */
private static final Set<Integer> LEAF_ATOM_TYPES = getAtomTypeSet(
Atom.TYPE_mdhd, Atom.TYPE_mvhd, Atom.TYPE_hdlr, Atom.TYPE_vmhd, Atom.TYPE_smhd,
Atom.TYPE_stsd, Atom.TYPE_avc1, Atom.TYPE_avcC, Atom.TYPE_mp4a, Atom.TYPE_esds,
Atom.TYPE_stts, Atom.TYPE_stss, Atom.TYPE_ctts, Atom.TYPE_stsc, Atom.TYPE_stsz,
Atom.TYPE_stco, Atom.TYPE_co64, Atom.TYPE_tkhd);
/** Set of atom types that contain other atoms that need to be parsed. */
private static final Set<Integer> CONTAINER_TYPES = getAtomTypeSet(
Atom.TYPE_moov, Atom.TYPE_trak, Atom.TYPE_mdia, Atom.TYPE_minf, Atom.TYPE_stbl);
/** Default number of times to retry loading data prior to failing. */
private static final int DEFAULT_LOADABLE_RETRY_COUNT = 3;
private final DataSource dataSource;
private final DataSpec dataSpec;
private final int readAheadAllocationSize;
private final int reloadMinimumSeekDistance;
private final int maximumTrackSampleInterval;
private final int loadRetryCount;
private final BufferPool bufferPool;
private final Loader loader;
private final ParsableByteArray atomHeader;
private final Stack<Atom.ContainerAtom> containerAtoms;
private DataSourceStream dataSourceStream;
private BufferedNonBlockingInputStream inputStream;
private long inputStreamOffset;
private long rootAtomBytesRead;
private boolean loadCompleted;
private int parserState;
private int atomBytesRead;
private int atomType;
private long atomSize;
private ParsableByteArray atomData;
private boolean prepared;
private int loadErrorCount;
private Mp4Track[] tracks;
/** An exception from {@link #inputStream}'s callbacks, or {@code null} if there was no error. */
private IOException lastLoadError;
private long loadErrorPosition;
/** If handling a call to {@link #seekTo}, the new required stream offset, or -1 otherwise. */
private long pendingSeekPosition;
/** If the input stream is being reopened at a new position, the new offset, or -1 otherwise. */
private long pendingLoadPosition;
/**
* Creates a new sample extractor for reading {@code dataSource} and {@code dataSpec} as an
* unfragmented MP4 file with default settings.
*
* <p>The default settings read ahead by 5 MiB, handle maximum offsets between samples at the same
* timestamp in different tracks of 3 MiB and restart loading when seeking forward by >= 256 KiB.
*
* @param dataSource Data source used to read from {@code dataSpec}.
* @param dataSpec Data specification specifying what to read.
*/
public Mp4SampleExtractor(DataSource dataSource, DataSpec dataSpec) {
this(dataSource, dataSpec, 5 * 1024 * 1024, 3 * 1024 * 1024, 256 * 1024,
DEFAULT_LOADABLE_RETRY_COUNT);
}
/**
* Creates a new sample extractor for reading {@code dataSource} and {@code dataSpec} as an
* unfragmented MP4 file.
*
* @param dataSource Data source used to read from {@code dataSpec}.
* @param dataSpec Data specification specifying what to read.
* @param readAheadAllocationSize Size of the allocation that buffers the stream, in bytes. The
* value must exceed the maximum sample size, so that a sample can be read in its entirety.
* @param maximumTrackSampleInterval Size of the buffer that handles reading from any selected
* track. The value should be chosen so that the buffer is as big as the interval in bytes
* between the start of the earliest and the end of the latest sample required to render media
* from all selected tracks, at any timestamp in the data source.
* @param reloadMinimumSeekDistance Determines when {@code dataSource} is reopened while seeking:
* if the number of bytes between the current position and the new position is greater than or
* equal to this value, or the new position is before the current position, loading will
* restart. The value should be set to the number of bytes that can be loaded/consumed from an
* existing connection in the time it takes to start a new connection.
* @param loadableRetryCount The number of times to retry loading if an error occurs.
*/
public Mp4SampleExtractor(DataSource dataSource, DataSpec dataSpec, int readAheadAllocationSize,
int maximumTrackSampleInterval, int reloadMinimumSeekDistance, int loadableRetryCount) {
// TODO: Handle minimumTrackSampleInterval specified in time not bytes.
this.dataSource = Assertions.checkNotNull(dataSource);
this.dataSpec = Assertions.checkNotNull(dataSpec);
this.readAheadAllocationSize = readAheadAllocationSize;
this.maximumTrackSampleInterval = maximumTrackSampleInterval;
this.reloadMinimumSeekDistance = reloadMinimumSeekDistance;
this.loadRetryCount = loadableRetryCount;
// TODO: Implement Allocator here so it is possible to check there is only one buffer at a time.
bufferPool = new BufferPool(readAheadAllocationSize);
loader = new Loader(LOADER_THREAD_NAME);
atomHeader = new ParsableByteArray(Atom.LONG_ATOM_HEADER_SIZE);
containerAtoms = new Stack<Atom.ContainerAtom>();
parserState = STATE_READING_ATOM_HEADER;
pendingLoadPosition = -1;
pendingSeekPosition = -1;
loadErrorPosition = -1;
}
@Override
public boolean prepare() throws IOException {
if (inputStream == null) {
loadFromOffset(0L);
}
if (!prepared) {
if (readHeaders() && !prepared) {
throw new IOException("moov atom not found.");
}
if (!prepared) {
maybeThrowLoadError();
}
}
return prepared;
}
@Override
public void selectTrack(int trackIndex) {
Assertions.checkState(prepared);
if (tracks[trackIndex].selected) {
return;
}
tracks[trackIndex].selected = true;
// Get the timestamp of the earliest currently-selected sample.
int earliestSampleTrackIndex = getTrackIndexOfEarliestCurrentSample();
if (earliestSampleTrackIndex == NO_TRACK) {
tracks[trackIndex].sampleIndex = 0;
return;
}
if (earliestSampleTrackIndex == Mp4TrackSampleTable.NO_SAMPLE) {
tracks[trackIndex].sampleIndex = Mp4TrackSampleTable.NO_SAMPLE;
return;
}
long timestampUs =
tracks[earliestSampleTrackIndex].sampleTable.timestampsUs[earliestSampleTrackIndex];
// Find the latest sync sample in the new track that has an earlier or equal timestamp.
tracks[trackIndex].sampleIndex =
tracks[trackIndex].sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(timestampUs);
}
@Override
public void deselectTrack(int trackIndex) {
Assertions.checkState(prepared);
tracks[trackIndex].selected = false;
}
@Override
public long getBufferedPositionUs() {
Assertions.checkState(prepared);
if (pendingLoadPosition != -1) {
return TrackRenderer.UNKNOWN_TIME_US;
}
if (loadCompleted) {
return TrackRenderer.END_OF_TRACK_US;
}
// Get the absolute position to which there is data buffered.
long bufferedPosition =
inputStreamOffset + inputStream.getReadPosition() + inputStream.getAvailableByteCount();
// Find the timestamp of the latest sample that does not exceed the buffered position.
long latestTimestampBeforeEnd = Long.MIN_VALUE;
for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) {
if (!tracks[trackIndex].selected) {
continue;
}
Mp4TrackSampleTable sampleTable = tracks[trackIndex].sampleTable;
int sampleIndex = Util.binarySearchFloor(sampleTable.offsets, bufferedPosition, false, true);
if (sampleIndex > 0
&& sampleTable.offsets[sampleIndex] + sampleTable.sizes[sampleIndex] > bufferedPosition) {
sampleIndex--;
}
// Update the latest timestamp if this is greater.
long timestamp = sampleTable.timestampsUs[sampleIndex];
if (timestamp > latestTimestampBeforeEnd) {
latestTimestampBeforeEnd = timestamp;
}
}
return latestTimestampBeforeEnd < 0L ? C.UNKNOWN_TIME_US : latestTimestampBeforeEnd;
}
@Override
public void seekTo(long positionUs) {
Assertions.checkState(prepared);
long earliestSamplePosition = Long.MAX_VALUE;
for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) {
if (!tracks[trackIndex].selected) {
continue;
}
Mp4TrackSampleTable sampleTable = tracks[trackIndex].sampleTable;
int sampleIndex = sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(positionUs);
if (sampleIndex == Mp4TrackSampleTable.NO_SAMPLE) {
sampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(positionUs);
}
tracks[trackIndex].sampleIndex = sampleIndex;
long offset = sampleTable.offsets[tracks[trackIndex].sampleIndex];
if (offset < earliestSamplePosition) {
earliestSamplePosition = offset;
}
}
pendingSeekPosition = earliestSamplePosition;
if (pendingLoadPosition != -1) {
loadFromOffset(earliestSamplePosition);
return;
}
inputStream.returnToMark();
long earliestOffset = inputStreamOffset + inputStream.getReadPosition();
long latestOffset = earliestOffset + inputStream.getAvailableByteCount();
if (earliestSamplePosition < earliestOffset
|| earliestSamplePosition >= latestOffset + reloadMinimumSeekDistance) {
loadFromOffset(earliestSamplePosition);
}
}
@Override
public int getTrackCount() {
Assertions.checkState(prepared);
return tracks.length;
}
@Override
public MediaFormat getMediaFormat(int track) {
Assertions.checkState(prepared);
return tracks[track].track.mediaFormat;
}
@Override
public DrmInitData getDrmInitData(int track) {
return null;
}
@Override
public int readSample(int trackIndex, SampleHolder sampleHolder) throws IOException {
Assertions.checkState(prepared);
Mp4Track track = tracks[trackIndex];
Assertions.checkState(track.selected);
int sampleIndex = track.sampleIndex;
// Check for the end of the stream.
if (sampleIndex == Mp4TrackSampleTable.NO_SAMPLE) {
// TODO: Should END_OF_STREAM be returned as soon as this track has no more samples, or as
// soon as no tracks have a sample (as implemented here)?
return hasSampleInAnySelectedTrack() ? SampleSource.NOTHING_READ : SampleSource.END_OF_STREAM;
}
// Return if the input stream will be reopened at the requested position.
if (pendingLoadPosition != -1) {
return SampleSource.NOTHING_READ;
}
// If there was a seek request, try to skip forwards to the requested position.
if (pendingSeekPosition != -1) {
int bytesToSeekPosition =
(int) (pendingSeekPosition - (inputStreamOffset + inputStream.getReadPosition()));
int skippedByteCount = inputStream.skip(bytesToSeekPosition);
if (skippedByteCount == -1) {
throw new IOException("Unexpected end-of-stream while seeking to sample.");
}
bytesToSeekPosition -= skippedByteCount;
inputStream.mark();
if (bytesToSeekPosition == 0) {
pendingSeekPosition = -1;
} else {
maybeThrowLoadError();
return SampleSource.NOTHING_READ;
}
}
// Return if the sample offset hasn't been loaded yet.
inputStream.returnToMark();
long sampleOffset = track.sampleTable.offsets[sampleIndex];
long seekOffsetLong = (sampleOffset - inputStreamOffset) - inputStream.getReadPosition();
Assertions.checkState(seekOffsetLong <= Integer.MAX_VALUE);
int seekOffset = (int) seekOffsetLong;
if (inputStream.skip(seekOffset) != seekOffset) {
maybeThrowLoadError();
return SampleSource.NOTHING_READ;
}
// Return if the sample has been loaded.
int sampleSize = track.sampleTable.sizes[sampleIndex];
if (inputStream.getAvailableByteCount() < sampleSize) {
maybeThrowLoadError();
return SampleSource.NOTHING_READ;
}
if (sampleHolder.data == null || sampleHolder.data.capacity() < sampleSize) {
sampleHolder.replaceBuffer(sampleSize);
}
ByteBuffer data = sampleHolder.data;
if (data == null) {
inputStream.skip(sampleSize);
sampleHolder.size = 0;
} else {
int bytesRead = inputStream.read(data, sampleSize);
Assertions.checkState(bytesRead == sampleSize);
if (MimeTypes.VIDEO_H264.equals(tracks[trackIndex].track.mediaFormat.mimeType)) {
// The mp4 file contains length-prefixed access units, but the decoder wants start code
// delimited content.
H264Util.replaceLengthPrefixesWithAvcStartCodes(sampleHolder.data, sampleSize);
}
sampleHolder.size = sampleSize;
}
// Move the input stream mark forwards if the earliest current sample was just read.
if (getTrackIndexOfEarliestCurrentSample() == trackIndex) {
inputStream.mark();
}
// TODO: Read encryption data.
sampleHolder.timeUs = track.sampleTable.timestampsUs[sampleIndex];
sampleHolder.flags = track.sampleTable.flags[sampleIndex];
// Advance to the next sample, checking if this was the last sample.
track.sampleIndex =
sampleIndex + 1 == track.sampleTable.getSampleCount() ? Mp4TrackSampleTable.NO_SAMPLE : sampleIndex + 1;
// Reset the loading error counter if we read past the offset at which the error was thrown.
if (dataSourceStream.getReadPosition() > loadErrorPosition) {
loadErrorCount = 0;
loadErrorPosition = -1;
}
return SampleSource.SAMPLE_READ;
}
@Override
public void release() {
pendingLoadPosition = -1;
loader.release();
if (inputStream != null) {
inputStream.close();
}
}
@Override
public void onLoadError(Loadable loadable, IOException exception) {
lastLoadError = exception;
loadErrorCount++;
if (loadErrorPosition == -1) {
loadErrorPosition = dataSourceStream.getLoadPosition();
}
int delayMs = getRetryDelayMs(loadErrorCount);
Log.w(TAG, "Retry loading (delay " + delayMs + " ms).");
loader.startLoading(dataSourceStream, this, delayMs);
}
@Override
public void onLoadCompleted(Loadable loadable) {
loadCompleted = true;
}
@Override
public void onLoadCanceled(Loadable loadable) {
if (pendingLoadPosition != -1) {
loadFromOffset(pendingLoadPosition);
pendingLoadPosition = -1;
}
}
private void loadFromOffset(long offsetBytes) {
inputStreamOffset = offsetBytes;
rootAtomBytesRead = offsetBytes;
if (loader.isLoading()) {
// Wait for loading to be canceled before proceeding.
pendingLoadPosition = offsetBytes;
loader.cancelLoading();
return;
}
if (inputStream != null) {
inputStream.close();
}
DataSpec dataSpec = new DataSpec(
this.dataSpec.uri, offsetBytes, C.LENGTH_UNBOUNDED, this.dataSpec.key);
dataSourceStream =
new DataSourceStream(dataSource, dataSpec, bufferPool, readAheadAllocationSize);
loader.startLoading(dataSourceStream, this);
// Wrap the input stream with a buffering stream so that it is possible to read from any track.
inputStream =
new BufferedNonBlockingInputStream(dataSourceStream, maximumTrackSampleInterval);
loadCompleted = false;
loadErrorCount = 0;
loadErrorPosition = -1;
}
/**
* Returns the index of the track that contains the earliest current sample, or {@link #NO_TRACK}
* if no track is selected, or {@link Mp4TrackSampleTable#NO_SAMPLE} if no samples remain in
* selected tracks.
*/
private int getTrackIndexOfEarliestCurrentSample() {
int earliestSampleTrackIndex = NO_TRACK;
long earliestSampleOffset = Long.MAX_VALUE;
for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) {
Mp4Track track = tracks[trackIndex];
if (!track.selected) {
continue;
}
int sampleIndex = track.sampleIndex;
if (sampleIndex == Mp4TrackSampleTable.NO_SAMPLE) {
if (earliestSampleTrackIndex == NO_TRACK) {
// A track is selected, but it has no more samples.
earliestSampleTrackIndex = Mp4TrackSampleTable.NO_SAMPLE;
}
continue;
}
long trackSampleOffset = track.sampleTable.offsets[sampleIndex];
if (trackSampleOffset < earliestSampleOffset) {
earliestSampleOffset = trackSampleOffset;
earliestSampleTrackIndex = trackIndex;
}
}
return earliestSampleTrackIndex;
}
private boolean hasSampleInAnySelectedTrack() {
boolean hasSample = false;
for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) {
if (tracks[trackIndex].selected && tracks[trackIndex].sampleIndex
!= Mp4TrackSampleTable.NO_SAMPLE) {
hasSample = true;
break;
}
}
return hasSample;
}
/** Reads headers, returning whether the end of the stream was reached. */
private boolean readHeaders() {
int results = 0;
while (!prepared && (results & (RESULT_NEED_MORE_DATA | RESULT_END_OF_STREAM)) == 0) {
switch (parserState) {
case STATE_READING_ATOM_HEADER:
results |= readAtomHeader();
break;
case STATE_READING_ATOM_PAYLOAD:
results |= readAtomPayload();
break;
}
}
return (results & RESULT_END_OF_STREAM) != 0;
}
private int readAtomHeader() {
if (pendingLoadPosition != -1) {
return RESULT_NEED_MORE_DATA;
}
// The size value is either 4 or 8 bytes long (in which case atomSize = Mp4Util.LONG_ATOM_SIZE).
int remainingBytes;
if (atomSize != Atom.LONG_SIZE_PREFIX) {
remainingBytes = Atom.ATOM_HEADER_SIZE - atomBytesRead;
} else {
remainingBytes = Atom.LONG_ATOM_HEADER_SIZE - atomBytesRead;
}
int bytesRead = inputStream.read(atomHeader.data, atomBytesRead, remainingBytes);
if (bytesRead == -1) {
return RESULT_END_OF_STREAM;
}
rootAtomBytesRead += bytesRead;
atomBytesRead += bytesRead;
if (atomBytesRead < Atom.ATOM_HEADER_SIZE
|| (atomSize == Atom.LONG_SIZE_PREFIX && atomBytesRead < Atom.LONG_ATOM_HEADER_SIZE)) {
return RESULT_NEED_MORE_DATA;
}
atomHeader.setPosition(0);
atomSize = atomHeader.readUnsignedInt();
atomType = atomHeader.readInt();
if (atomSize == Atom.LONG_SIZE_PREFIX) {
// The extended atom size is contained in the next 8 bytes, so try to read it now.
if (atomBytesRead < Atom.LONG_ATOM_HEADER_SIZE) {
return readAtomHeader();
}
atomSize = atomHeader.readLong();
}
Integer atomTypeInteger = atomType; // Avoids boxing atomType twice.
if (CONTAINER_TYPES.contains(atomTypeInteger)) {
if (atomSize == Atom.LONG_SIZE_PREFIX) {
containerAtoms.add(new ContainerAtom(
atomType, rootAtomBytesRead + atomSize - Atom.LONG_ATOM_HEADER_SIZE));
} else {
containerAtoms.add(new ContainerAtom(
atomType, rootAtomBytesRead + atomSize - Atom.ATOM_HEADER_SIZE));
}
enterState(STATE_READING_ATOM_HEADER);
} else if (LEAF_ATOM_TYPES.contains(atomTypeInteger)) {
Assertions.checkState(atomSize <= Integer.MAX_VALUE);
atomData = new ParsableByteArray((int) atomSize);
System.arraycopy(atomHeader.data, 0, atomData.data, 0, Atom.ATOM_HEADER_SIZE);
enterState(STATE_READING_ATOM_PAYLOAD);
} else {
atomData = null;
enterState(STATE_READING_ATOM_PAYLOAD);
}
return 0;
}
private int readAtomPayload() {
int bytesRead;
if (atomData != null) {
bytesRead = inputStream.read(atomData.data, atomBytesRead, (int) atomSize - atomBytesRead);
} else {
if (atomSize >= reloadMinimumSeekDistance || atomSize > Integer.MAX_VALUE) {
loadFromOffset(rootAtomBytesRead + atomSize - atomBytesRead);
onContainerAtomRead();
enterState(STATE_READING_ATOM_HEADER);
return 0;
} else {
bytesRead = inputStream.skip((int) atomSize - atomBytesRead);
}
}
if (bytesRead == -1) {
return RESULT_END_OF_STREAM;
}
rootAtomBytesRead += bytesRead;
atomBytesRead += bytesRead;
if (atomBytesRead != atomSize) {
return RESULT_NEED_MORE_DATA;
}
if (atomData != null && !containerAtoms.isEmpty()) {
containerAtoms.peek().add(new Atom.LeafAtom(atomType, atomData));
}
onContainerAtomRead();
enterState(STATE_READING_ATOM_HEADER);
return 0;
}
private void onContainerAtomRead() {
while (!containerAtoms.isEmpty() && containerAtoms.peek().endByteOffset == rootAtomBytesRead) {
Atom.ContainerAtom containerAtom = containerAtoms.pop();
if (containerAtom.type == Atom.TYPE_moov) {
processMoovAtom(containerAtom);
} else if (!containerAtoms.isEmpty()) {
containerAtoms.peek().add(containerAtom);
}
}
}
private void enterState(int state) {
switch (state) {
case STATE_READING_ATOM_HEADER:
atomBytesRead = 0;
atomSize = 0;
break;
}
parserState = state;
inputStream.mark();
}
/** Updates the stored track metadata to reflect the contents on the specified moov atom. */
private void processMoovAtom(Atom.ContainerAtom moov) {
List<Mp4Track> tracks = new ArrayList<Mp4Track>();
long earliestSampleOffset = Long.MAX_VALUE;
for (int i = 0; i < moov.containerChildren.size(); i++) {
Atom.ContainerAtom atom = moov.containerChildren.get(i);
if (atom.type != Atom.TYPE_trak) {
continue;
}
Track track = CommonMp4AtomParsers.parseTrak(atom, moov.getLeafAtomOfType(Atom.TYPE_mvhd));
if (track.type != Track.TYPE_AUDIO && track.type != Track.TYPE_VIDEO) {
continue;
}
Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia)
.getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl);
Mp4TrackSampleTable trackSampleTable = CommonMp4AtomParsers.parseStbl(track, stblAtom);
if (trackSampleTable.getSampleCount() == 0) {
continue;
}
tracks.add(new Mp4Track(track, trackSampleTable));
// Keep track of the byte offset of the earliest sample.
long firstSampleOffset = trackSampleTable.offsets[0];
if (firstSampleOffset < earliestSampleOffset) {
earliestSampleOffset = firstSampleOffset;
}
}
this.tracks = tracks.toArray(new Mp4Track[0]);
if (earliestSampleOffset < inputStream.getReadPosition()) {
loadFromOffset(earliestSampleOffset);
}
prepared = true;
}
/** Returns an unmodifiable set of atom types. */
private static Set<Integer> getAtomTypeSet(int... atomTypes) {
Set<Integer> atomTypeSet = new HashSet<Integer>();
for (int atomType : atomTypes) {
atomTypeSet.add(atomType);
}
return Collections.unmodifiableSet(atomTypeSet);
}
private int getRetryDelayMs(int errorCount) {
return Math.min((errorCount - 1) * 1000, 5000);
}
private void maybeThrowLoadError() throws IOException {
if (loadErrorCount > loadRetryCount) {
throw lastLoadError;
}
}
private static final class Mp4Track {
public final Track track;
public final Mp4TrackSampleTable sampleTable;
public boolean selected;
public int sampleIndex;
public Mp4Track(Track track, Mp4TrackSampleTable sampleTable) {
this.track = track;
this.sampleTable = sampleTable;
}
}
}

View file

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

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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);
}