mirror of
https://github.com/samsonjs/media.git
synced 2026-03-29 10:05:48 +00:00
Add new style mp4/fmp4 extractors.
This commit is contained in:
parent
f002e6a76e
commit
587edf8e2b
31 changed files with 2081 additions and 1071 deletions
|
|
@ -49,6 +49,7 @@ public class DemoUtil {
|
|||
public static final int TYPE_OTHER = 2;
|
||||
public static final int TYPE_HLS = 3;
|
||||
public static final int TYPE_MP4 = 4;
|
||||
public static final int TYPE_MP3 = 5;
|
||||
|
||||
private static final CookieManager defaultCookieManager;
|
||||
|
||||
|
|
|
|||
|
|
@ -23,10 +23,12 @@ import com.google.android.exoplayer.demo.player.DashRendererBuilder;
|
|||
import com.google.android.exoplayer.demo.player.DefaultRendererBuilder;
|
||||
import com.google.android.exoplayer.demo.player.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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,510 @@
|
|||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer.extractor;
|
||||
|
||||
import com.google.android.exoplayer.C;
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.MediaFormatHolder;
|
||||
import com.google.android.exoplayer.SampleHolder;
|
||||
import com.google.android.exoplayer.SampleSource;
|
||||
import com.google.android.exoplayer.TrackInfo;
|
||||
import com.google.android.exoplayer.TrackRenderer;
|
||||
import com.google.android.exoplayer.drm.DrmInitData;
|
||||
import com.google.android.exoplayer.upstream.BufferPool;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.DataSpec;
|
||||
import com.google.android.exoplayer.upstream.Loader;
|
||||
import com.google.android.exoplayer.upstream.Loader.Loadable;
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.SystemClock;
|
||||
import android.util.SparseArray;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A {@link SampleSource} that extracts sample data using an {@link Extractor}
|
||||
*/
|
||||
public class ExtractorSampleSource implements SampleSource, ExtractorOutput, Loader.Callback {
|
||||
|
||||
/**
|
||||
* The default minimum number of times to retry loading data prior to failing.
|
||||
*/
|
||||
public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3;
|
||||
|
||||
private static final int BUFFER_LENGTH = 256 * 1024;
|
||||
|
||||
private static final int NO_RESET_PENDING = -1;
|
||||
|
||||
private final Extractor extractor;
|
||||
private final BufferPool bufferPool;
|
||||
private final int requestedBufferSize;
|
||||
private final SparseArray<DefaultTrackOutput> sampleQueues;
|
||||
private final int minLoadableRetryCount;
|
||||
private final boolean frameAccurateSeeking;
|
||||
private final Uri uri;
|
||||
private final DataSource dataSource;
|
||||
|
||||
private volatile boolean tracksBuilt;
|
||||
private volatile SeekMap seekMap;
|
||||
private volatile DrmInitData drmInitData;
|
||||
|
||||
private boolean prepared;
|
||||
private int enabledTrackCount;
|
||||
private TrackInfo[] trackInfos;
|
||||
private boolean[] pendingMediaFormat;
|
||||
private boolean[] pendingDiscontinuities;
|
||||
private boolean[] trackEnabledStates;
|
||||
|
||||
private int remainingReleaseCount;
|
||||
private long downstreamPositionUs;
|
||||
private long lastSeekPositionUs;
|
||||
private long pendingResetPositionUs;
|
||||
|
||||
private Loader loader;
|
||||
private ExtractingLoadable loadable;
|
||||
private IOException currentLoadableException;
|
||||
private boolean currentLoadableExceptionFatal;
|
||||
// TODO: Set this back to 0 in the correct place (some place indicative of making progress).
|
||||
private int currentLoadableExceptionCount;
|
||||
private long currentLoadableExceptionTimestamp;
|
||||
private boolean loadingFinished;
|
||||
|
||||
/**
|
||||
* @param uri The {@link Uri} of the media stream.
|
||||
* @param dataSource A data source to read the media stream.
|
||||
* @param extractor An {@link Extractor} to extract the media stream.
|
||||
* @param downstreamRendererCount Number of track renderers dependent on this sample source.
|
||||
* @param requestedBufferSize The requested total buffer size for storing sample data, in bytes.
|
||||
* The actual allocated size may exceed the value passed in if the implementation requires it.
|
||||
*/
|
||||
public ExtractorSampleSource(Uri uri, DataSource dataSource, Extractor extractor,
|
||||
int downstreamRendererCount, int requestedBufferSize) {
|
||||
this.uri = uri;
|
||||
this.dataSource = dataSource;
|
||||
this.extractor = extractor;
|
||||
remainingReleaseCount = downstreamRendererCount;
|
||||
this.requestedBufferSize = requestedBufferSize;
|
||||
sampleQueues = new SparseArray<DefaultTrackOutput>();
|
||||
bufferPool = new BufferPool(BUFFER_LENGTH);
|
||||
minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT;
|
||||
pendingResetPositionUs = NO_RESET_PENDING;
|
||||
frameAccurateSeeking = true;
|
||||
extractor.init(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean prepare() throws IOException {
|
||||
if (prepared) {
|
||||
return true;
|
||||
}
|
||||
if (loader == null) {
|
||||
loader = new Loader("Loader:ExtractorSampleSource");
|
||||
}
|
||||
|
||||
continueBufferingInternal();
|
||||
|
||||
// TODO: Support non-seekable content? Or at least avoid getting stuck here if a seekMap doesn't
|
||||
// arrive (we may end up filling the sample buffers whilst we're still not prepared, and then
|
||||
// getting stuck).
|
||||
if (seekMap != null && tracksBuilt && haveFormatsForAllTracks()) {
|
||||
int trackCount = sampleQueues.size();
|
||||
trackEnabledStates = new boolean[trackCount];
|
||||
pendingDiscontinuities = new boolean[trackCount];
|
||||
pendingMediaFormat = new boolean[trackCount];
|
||||
trackInfos = new TrackInfo[trackCount];
|
||||
for (int i = 0; i < trackCount; i++) {
|
||||
MediaFormat format = sampleQueues.valueAt(i).getFormat();
|
||||
trackInfos[i] = new TrackInfo(format.mimeType, format.durationUs);
|
||||
}
|
||||
prepared = true;
|
||||
if (isPendingReset()) {
|
||||
restartFrom(pendingResetPositionUs);
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
maybeThrowLoadableException();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTrackCount() {
|
||||
return sampleQueues.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrackInfo getTrackInfo(int track) {
|
||||
Assertions.checkState(prepared);
|
||||
return trackInfos[track];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable(int track, long positionUs) {
|
||||
Assertions.checkState(prepared);
|
||||
Assertions.checkState(!trackEnabledStates[track]);
|
||||
enabledTrackCount++;
|
||||
trackEnabledStates[track] = true;
|
||||
pendingMediaFormat[track] = true;
|
||||
if (enabledTrackCount == 1) {
|
||||
seekToUs(positionUs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disable(int track) {
|
||||
Assertions.checkState(prepared);
|
||||
Assertions.checkState(trackEnabledStates[track]);
|
||||
enabledTrackCount--;
|
||||
trackEnabledStates[track] = false;
|
||||
pendingDiscontinuities[track] = false;
|
||||
if (enabledTrackCount == 0) {
|
||||
if (loader.isLoading()) {
|
||||
loader.cancelLoading();
|
||||
} else {
|
||||
clearState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean continueBuffering(long playbackPositionUs) throws IOException {
|
||||
Assertions.checkState(prepared);
|
||||
Assertions.checkState(enabledTrackCount > 0);
|
||||
downstreamPositionUs = playbackPositionUs;
|
||||
discardSamplesForDisabledTracks(downstreamPositionUs);
|
||||
return loadingFinished || continueBufferingInternal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder,
|
||||
SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException {
|
||||
downstreamPositionUs = playbackPositionUs;
|
||||
|
||||
if (pendingDiscontinuities[track]) {
|
||||
pendingDiscontinuities[track] = false;
|
||||
return DISCONTINUITY_READ;
|
||||
}
|
||||
|
||||
if (onlyReadDiscontinuity || isPendingReset()) {
|
||||
maybeThrowLoadableException();
|
||||
return NOTHING_READ;
|
||||
}
|
||||
|
||||
DefaultTrackOutput sampleQueue = sampleQueues.valueAt(track);
|
||||
if (pendingMediaFormat[track]) {
|
||||
formatHolder.format = sampleQueue.getFormat();
|
||||
formatHolder.drmInitData = drmInitData;
|
||||
pendingMediaFormat[track] = false;
|
||||
return FORMAT_READ;
|
||||
}
|
||||
|
||||
if (sampleQueue.getSample(sampleHolder)) {
|
||||
boolean decodeOnly = frameAccurateSeeking && sampleHolder.timeUs < lastSeekPositionUs;
|
||||
sampleHolder.flags |= decodeOnly ? C.SAMPLE_FLAG_DECODE_ONLY : 0;
|
||||
return SAMPLE_READ;
|
||||
}
|
||||
|
||||
if (loadingFinished) {
|
||||
return END_OF_STREAM;
|
||||
}
|
||||
|
||||
maybeThrowLoadableException();
|
||||
return NOTHING_READ;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekToUs(long positionUs) {
|
||||
Assertions.checkState(prepared);
|
||||
Assertions.checkState(enabledTrackCount > 0);
|
||||
lastSeekPositionUs = positionUs;
|
||||
if ((isPendingReset() ? pendingResetPositionUs : downstreamPositionUs) == positionUs) {
|
||||
return;
|
||||
}
|
||||
|
||||
downstreamPositionUs = positionUs;
|
||||
|
||||
// If we're not pending a reset, see if we can seek within the sample queues.
|
||||
boolean seekInsideBuffer = !isPendingReset();
|
||||
for (int i = 0; seekInsideBuffer && i < sampleQueues.size(); i++) {
|
||||
seekInsideBuffer &= sampleQueues.valueAt(i).skipToKeyframeBefore(positionUs);
|
||||
}
|
||||
|
||||
// If we failed to seek within the sample queues, we need to restart.
|
||||
if (!seekInsideBuffer) {
|
||||
restartFrom(positionUs);
|
||||
}
|
||||
|
||||
// Either way, we need to send discontinuities to the downstream components.
|
||||
for (int i = 0; i < pendingDiscontinuities.length; i++) {
|
||||
pendingDiscontinuities[i] = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getBufferedPositionUs() {
|
||||
if (loadingFinished) {
|
||||
return TrackRenderer.END_OF_TRACK_US;
|
||||
} else if (isPendingReset()) {
|
||||
return pendingResetPositionUs;
|
||||
} else {
|
||||
long largestParsedTimestampUs = Long.MIN_VALUE;
|
||||
for (int i = 0; i < sampleQueues.size(); i++) {
|
||||
largestParsedTimestampUs = Math.max(largestParsedTimestampUs,
|
||||
sampleQueues.valueAt(i).getLargestParsedTimestampUs());
|
||||
}
|
||||
return largestParsedTimestampUs == Long.MIN_VALUE ? downstreamPositionUs
|
||||
: largestParsedTimestampUs;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
Assertions.checkState(remainingReleaseCount > 0);
|
||||
if (--remainingReleaseCount == 0) {
|
||||
loader.release();
|
||||
loader = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Loader.Callback implementation.
|
||||
|
||||
@Override
|
||||
public void onLoadCompleted(Loadable loadable) {
|
||||
loadingFinished = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCanceled(Loadable loadable) {
|
||||
if (enabledTrackCount > 0) {
|
||||
restartFrom(pendingResetPositionUs);
|
||||
} else {
|
||||
clearState();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadError(Loadable loadable, IOException e) {
|
||||
currentLoadableException = e;
|
||||
currentLoadableExceptionCount++;
|
||||
currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
|
||||
maybeStartLoading();
|
||||
}
|
||||
|
||||
// ExtractorOutput implementation.
|
||||
|
||||
@Override
|
||||
public TrackOutput track(int id) {
|
||||
DefaultTrackOutput sampleQueue = sampleQueues.get(id);
|
||||
if (sampleQueue == null) {
|
||||
sampleQueue = new DefaultTrackOutput(bufferPool);
|
||||
sampleQueues.put(id, sampleQueue);
|
||||
}
|
||||
return sampleQueue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endTracks() {
|
||||
tracksBuilt = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekMap(SeekMap seekMap) {
|
||||
this.seekMap = seekMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drmInitData(DrmInitData drmInitData) {
|
||||
this.drmInitData = drmInitData;
|
||||
}
|
||||
|
||||
// Internal stuff.
|
||||
|
||||
private boolean continueBufferingInternal() throws IOException {
|
||||
maybeStartLoading();
|
||||
if (isPendingReset()) {
|
||||
return false;
|
||||
}
|
||||
boolean haveSamples = prepared && haveSampleForOneEnabledTrack();
|
||||
if (!haveSamples) {
|
||||
maybeThrowLoadableException();
|
||||
}
|
||||
return haveSamples;
|
||||
}
|
||||
|
||||
private void restartFrom(long positionUs) {
|
||||
pendingResetPositionUs = positionUs;
|
||||
loadingFinished = false;
|
||||
if (loader.isLoading()) {
|
||||
loader.cancelLoading();
|
||||
} else {
|
||||
clearState();
|
||||
maybeStartLoading();
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeStartLoading() {
|
||||
if (currentLoadableExceptionFatal || loadingFinished || loader.isLoading()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentLoadableException != null) {
|
||||
Assertions.checkState(loadable != null);
|
||||
long elapsedMillis = SystemClock.elapsedRealtime() - currentLoadableExceptionTimestamp;
|
||||
if (elapsedMillis >= getRetryDelayMillis(currentLoadableExceptionCount)) {
|
||||
currentLoadableException = null;
|
||||
loader.startLoading(loadable, this);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!prepared) {
|
||||
loadable = new ExtractingLoadable(uri, dataSource, extractor, bufferPool, requestedBufferSize,
|
||||
0);
|
||||
} else {
|
||||
Assertions.checkState(isPendingReset());
|
||||
loadable = new ExtractingLoadable(uri, dataSource, extractor, bufferPool, requestedBufferSize,
|
||||
seekMap.getPosition(pendingResetPositionUs));
|
||||
pendingResetPositionUs = NO_RESET_PENDING;
|
||||
}
|
||||
loader.startLoading(loadable, this);
|
||||
}
|
||||
|
||||
private void maybeThrowLoadableException() throws IOException {
|
||||
if (currentLoadableException != null && (currentLoadableExceptionFatal
|
||||
|| currentLoadableExceptionCount > minLoadableRetryCount)) {
|
||||
throw currentLoadableException;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean haveFormatsForAllTracks() {
|
||||
for (int i = 0; i < sampleQueues.size(); i++) {
|
||||
if (!sampleQueues.valueAt(i).hasFormat()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean haveSampleForOneEnabledTrack() {
|
||||
for (int i = 0; i < trackEnabledStates.length; i++) {
|
||||
if (trackEnabledStates[i] && !sampleQueues.valueAt(i).isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void discardSamplesForDisabledTracks(long timeUs) {
|
||||
for (int i = 0; i < trackEnabledStates.length; i++) {
|
||||
if (!trackEnabledStates[i]) {
|
||||
sampleQueues.valueAt(i).discardUntil(timeUs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void clearState() {
|
||||
for (int i = 0; i < sampleQueues.size(); i++) {
|
||||
sampleQueues.valueAt(i).clear();
|
||||
}
|
||||
loadable = null;
|
||||
currentLoadableException = null;
|
||||
currentLoadableExceptionCount = 0;
|
||||
currentLoadableExceptionFatal = false;
|
||||
}
|
||||
|
||||
private boolean isPendingReset() {
|
||||
return pendingResetPositionUs != NO_RESET_PENDING;
|
||||
}
|
||||
|
||||
private long getRetryDelayMillis(long errorCount) {
|
||||
return Math.min((errorCount - 1) * 1000, 5000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the media stream and extracts sample data from it.
|
||||
*/
|
||||
private static class ExtractingLoadable implements Loadable {
|
||||
|
||||
private final Uri uri;
|
||||
private final DataSource dataSource;
|
||||
private final Extractor extractor;
|
||||
private final BufferPool bufferPool;
|
||||
private final int bufferPoolSizeLimit;
|
||||
private final PositionHolder positionHolder;
|
||||
|
||||
private volatile boolean loadCanceled;
|
||||
|
||||
private boolean pendingExtractorSeek;
|
||||
|
||||
public ExtractingLoadable(Uri uri, DataSource dataSource, Extractor extractor,
|
||||
BufferPool bufferPool, int bufferPoolSizeLimit, long position) {
|
||||
this.uri = Assertions.checkNotNull(uri);
|
||||
this.dataSource = Assertions.checkNotNull(dataSource);
|
||||
this.extractor = Assertions.checkNotNull(extractor);
|
||||
this.bufferPool = Assertions.checkNotNull(bufferPool);
|
||||
this.bufferPoolSizeLimit = bufferPoolSizeLimit;
|
||||
positionHolder = new PositionHolder();
|
||||
positionHolder.position = position;
|
||||
pendingExtractorSeek = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelLoad() {
|
||||
loadCanceled = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoadCanceled() {
|
||||
return loadCanceled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load() throws IOException, InterruptedException {
|
||||
if (pendingExtractorSeek) {
|
||||
extractor.seek();
|
||||
pendingExtractorSeek = false;
|
||||
}
|
||||
int result = Extractor.RESULT_CONTINUE;
|
||||
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
|
||||
ExtractorInput input = null;
|
||||
try {
|
||||
long position = positionHolder.position;
|
||||
long length = dataSource.open(new DataSpec(uri, position, C.LENGTH_UNBOUNDED, null));
|
||||
if (length != C.LENGTH_UNBOUNDED) {
|
||||
length += position;
|
||||
}
|
||||
input = new DefaultExtractorInput(dataSource, position, length);
|
||||
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
|
||||
bufferPool.blockWhileAllocatedSizeExceeds(bufferPoolSizeLimit);
|
||||
result = extractor.read(input, positionHolder);
|
||||
// TODO: Implement throttling to stop us from buffering data too often.
|
||||
}
|
||||
} finally {
|
||||
if (result == Extractor.RESULT_SEEK) {
|
||||
result = Extractor.RESULT_CONTINUE;
|
||||
} else if (input != null) {
|
||||
positionHolder.position = input.getPosition();
|
||||
}
|
||||
dataSource.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer.extractor;
|
||||
|
||||
/**
|
||||
* Holds a position in the stream.
|
||||
*/
|
||||
public final class PositionHolder {
|
||||
|
||||
/**
|
||||
* The held position.
|
||||
*/
|
||||
public long position;
|
||||
|
||||
}
|
||||
|
|
@ -111,6 +111,21 @@ import java.util.concurrent.ConcurrentLinkedQueue;
|
|||
dropDownstreamTo(nextOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,190 @@
|
|||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer.extractor.mp4;
|
||||
|
||||
import com.google.android.exoplayer.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class Atom {
|
||||
|
||||
/** Size of an atom header, in bytes. */
|
||||
public static final int HEADER_SIZE = 8;
|
||||
|
||||
/** Size of a full atom header, in bytes. */
|
||||
public static final int FULL_HEADER_SIZE = 12;
|
||||
|
||||
/** Size of a long atom header, in bytes. */
|
||||
public static final int LONG_HEADER_SIZE = 16;
|
||||
|
||||
/** Value for the first 32 bits of atomSize when the atom size is actually a long value. */
|
||||
public static final int LONG_SIZE_PREFIX = 1;
|
||||
|
||||
public static final int TYPE_ftyp = Util.getIntegerCodeForString("ftyp");
|
||||
public static final int TYPE_avc1 = Util.getIntegerCodeForString("avc1");
|
||||
public static final int TYPE_avc3 = Util.getIntegerCodeForString("avc3");
|
||||
public static final int TYPE_esds = Util.getIntegerCodeForString("esds");
|
||||
public static final int TYPE_mdat = Util.getIntegerCodeForString("mdat");
|
||||
public static final int TYPE_mp4a = Util.getIntegerCodeForString("mp4a");
|
||||
public static final int TYPE_ac_3 = Util.getIntegerCodeForString("ac-3");
|
||||
public static final int TYPE_dac3 = Util.getIntegerCodeForString("dac3");
|
||||
public static final int TYPE_ec_3 = Util.getIntegerCodeForString("ec-3");
|
||||
public static final int TYPE_dec3 = Util.getIntegerCodeForString("dec3");
|
||||
public static final int TYPE_tfdt = Util.getIntegerCodeForString("tfdt");
|
||||
public static final int TYPE_tfhd = Util.getIntegerCodeForString("tfhd");
|
||||
public static final int TYPE_trex = Util.getIntegerCodeForString("trex");
|
||||
public static final int TYPE_trun = Util.getIntegerCodeForString("trun");
|
||||
public static final int TYPE_sidx = Util.getIntegerCodeForString("sidx");
|
||||
public static final int TYPE_moov = Util.getIntegerCodeForString("moov");
|
||||
public static final int TYPE_mvhd = Util.getIntegerCodeForString("mvhd");
|
||||
public static final int TYPE_trak = Util.getIntegerCodeForString("trak");
|
||||
public static final int TYPE_mdia = Util.getIntegerCodeForString("mdia");
|
||||
public static final int TYPE_minf = Util.getIntegerCodeForString("minf");
|
||||
public static final int TYPE_stbl = Util.getIntegerCodeForString("stbl");
|
||||
public static final int TYPE_avcC = Util.getIntegerCodeForString("avcC");
|
||||
public static final int TYPE_moof = Util.getIntegerCodeForString("moof");
|
||||
public static final int TYPE_traf = Util.getIntegerCodeForString("traf");
|
||||
public static final int TYPE_mvex = Util.getIntegerCodeForString("mvex");
|
||||
public static final int TYPE_tkhd = Util.getIntegerCodeForString("tkhd");
|
||||
public static final int TYPE_mdhd = Util.getIntegerCodeForString("mdhd");
|
||||
public static final int TYPE_hdlr = Util.getIntegerCodeForString("hdlr");
|
||||
public static final int TYPE_stsd = Util.getIntegerCodeForString("stsd");
|
||||
public static final int TYPE_pssh = Util.getIntegerCodeForString("pssh");
|
||||
public static final int TYPE_sinf = Util.getIntegerCodeForString("sinf");
|
||||
public static final int TYPE_schm = Util.getIntegerCodeForString("schm");
|
||||
public static final int TYPE_schi = Util.getIntegerCodeForString("schi");
|
||||
public static final int TYPE_tenc = Util.getIntegerCodeForString("tenc");
|
||||
public static final int TYPE_encv = Util.getIntegerCodeForString("encv");
|
||||
public static final int TYPE_enca = Util.getIntegerCodeForString("enca");
|
||||
public static final int TYPE_frma = Util.getIntegerCodeForString("frma");
|
||||
public static final int TYPE_saiz = Util.getIntegerCodeForString("saiz");
|
||||
public static final int TYPE_uuid = Util.getIntegerCodeForString("uuid");
|
||||
public static final int TYPE_senc = Util.getIntegerCodeForString("senc");
|
||||
public static final int TYPE_pasp = Util.getIntegerCodeForString("pasp");
|
||||
public static final int TYPE_TTML = Util.getIntegerCodeForString("TTML");
|
||||
public static final int TYPE_vmhd = Util.getIntegerCodeForString("vmhd");
|
||||
public static final int TYPE_smhd = Util.getIntegerCodeForString("smhd");
|
||||
public static final int TYPE_mp4v = Util.getIntegerCodeForString("mp4v");
|
||||
public static final int TYPE_stts = Util.getIntegerCodeForString("stts");
|
||||
public static final int TYPE_stss = Util.getIntegerCodeForString("stss");
|
||||
public static final int TYPE_ctts = Util.getIntegerCodeForString("ctts");
|
||||
public static final int TYPE_stsc = Util.getIntegerCodeForString("stsc");
|
||||
public static final int TYPE_stsz = Util.getIntegerCodeForString("stsz");
|
||||
public static final int TYPE_stco = Util.getIntegerCodeForString("stco");
|
||||
public static final int TYPE_co64 = Util.getIntegerCodeForString("co64");
|
||||
|
||||
public final int type;
|
||||
|
||||
Atom(int type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getAtomTypeString(type);
|
||||
}
|
||||
|
||||
/** An MP4 atom that is a leaf. */
|
||||
public static final class LeafAtom extends Atom {
|
||||
|
||||
public final ParsableByteArray data;
|
||||
|
||||
public LeafAtom(int type, ParsableByteArray data) {
|
||||
super(type);
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** An MP4 atom that has child atoms. */
|
||||
public static final class ContainerAtom extends Atom {
|
||||
|
||||
public final long endByteOffset;
|
||||
public final List<LeafAtom> leafChildren;
|
||||
public final List<ContainerAtom> containerChildren;
|
||||
|
||||
public ContainerAtom(int type, long endByteOffset) {
|
||||
super(type);
|
||||
|
||||
leafChildren = new ArrayList<LeafAtom>();
|
||||
containerChildren = new ArrayList<ContainerAtom>();
|
||||
this.endByteOffset = endByteOffset;
|
||||
}
|
||||
|
||||
public void add(LeafAtom atom) {
|
||||
leafChildren.add(atom);
|
||||
}
|
||||
|
||||
public void add(ContainerAtom atom) {
|
||||
containerChildren.add(atom);
|
||||
}
|
||||
|
||||
public LeafAtom getLeafAtomOfType(int type) {
|
||||
int childrenSize = leafChildren.size();
|
||||
for (int i = 0; i < childrenSize; i++) {
|
||||
LeafAtom atom = leafChildren.get(i);
|
||||
if (atom.type == type) {
|
||||
return atom;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ContainerAtom getContainerAtomOfType(int type) {
|
||||
int childrenSize = containerChildren.size();
|
||||
for (int i = 0; i < childrenSize; i++) {
|
||||
ContainerAtom atom = containerChildren.get(i);
|
||||
if (atom.type == type) {
|
||||
return atom;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getAtomTypeString(type)
|
||||
+ " leaves: " + Arrays.toString(leafChildren.toArray(new LeafAtom[0]))
|
||||
+ " containers: " + Arrays.toString(containerChildren.toArray(new ContainerAtom[0]));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the version number out of the additional integer component of a full atom.
|
||||
*/
|
||||
public static int parseFullAtomVersion(int fullAtomInt) {
|
||||
return 0x000000FF & (fullAtomInt >> 24);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the atom flags out of the additional integer component of a full atom.
|
||||
*/
|
||||
public static int parseFullAtomFlags(int fullAtomInt) {
|
||||
return 0x00FFFFFF & fullAtomInt;
|
||||
}
|
||||
|
||||
private static String getAtomTypeString(int type) {
|
||||
return "" + (char) (type >> 24)
|
||||
+ (char) ((type >> 16) & 0xFF)
|
||||
+ (char) ((type >> 8) & 0xFF)
|
||||
+ (char) (type & 0xFF);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -13,11 +13,10 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* 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.
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,700 @@
|
|||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer.extractor.mp4;
|
||||
|
||||
import com.google.android.exoplayer.C;
|
||||
import com.google.android.exoplayer.drm.DrmInitData;
|
||||
import com.google.android.exoplayer.extractor.ChunkIndex;
|
||||
import com.google.android.exoplayer.extractor.Extractor;
|
||||
import com.google.android.exoplayer.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer.extractor.PositionHolder;
|
||||
import com.google.android.exoplayer.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer.extractor.mp4.Atom.ContainerAtom;
|
||||
import com.google.android.exoplayer.extractor.mp4.Atom.LeafAtom;
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
import com.google.android.exoplayer.util.H264Util;
|
||||
import com.google.android.exoplayer.util.MimeTypes;
|
||||
import com.google.android.exoplayer.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Stack;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Facilitates the extraction of data from the fragmented mp4 container format.
|
||||
* <p>
|
||||
* This implementation only supports de-muxed (i.e. single track) streams.
|
||||
*/
|
||||
public final class FragmentedMp4Extractor implements Extractor {
|
||||
|
||||
/**
|
||||
* Flag to work around an issue in some video streams where every frame is marked as a sync frame.
|
||||
* The workaround overrides the sync frame flags in the stream, forcing them to false except for
|
||||
* the first sample in each segment.
|
||||
* <p>
|
||||
* This flag does nothing if the stream is not a video stream.
|
||||
*/
|
||||
public static final int WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME = 1;
|
||||
|
||||
private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE =
|
||||
new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12};
|
||||
|
||||
// Parser states
|
||||
private static final int STATE_READING_ATOM_HEADER = 0;
|
||||
private static final int STATE_READING_ATOM_PAYLOAD = 1;
|
||||
private static final int STATE_READING_ENCRYPTION_DATA = 2;
|
||||
private static final int STATE_READING_SAMPLE_START = 3;
|
||||
private static final int STATE_READING_SAMPLE_CONTINUE = 4;
|
||||
|
||||
private final int workaroundFlags;
|
||||
|
||||
// Temporary arrays.
|
||||
private final ParsableByteArray nalStartCode;
|
||||
private final ParsableByteArray nalLength;
|
||||
private final ParsableByteArray encryptionSignalByte;
|
||||
|
||||
// Parser state
|
||||
private final ParsableByteArray atomHeader;
|
||||
private final byte[] extendedTypeScratch;
|
||||
private final Stack<ContainerAtom> containerAtoms;
|
||||
private final TrackFragment fragmentRun;
|
||||
|
||||
private int parserState;
|
||||
private int rootAtomBytesRead;
|
||||
private int atomType;
|
||||
private int atomSize;
|
||||
private ParsableByteArray atomData;
|
||||
|
||||
private int sampleIndex;
|
||||
private int sampleSize;
|
||||
private int sampleBytesWritten;
|
||||
private int sampleCurrentNalBytesRemaining;
|
||||
|
||||
// Data parsed from moov atom.
|
||||
private Track track;
|
||||
private DefaultSampleValues extendsDefaults;
|
||||
|
||||
// Extractor outputs.
|
||||
private ExtractorOutput extractorOutput;
|
||||
private TrackOutput trackOutput;
|
||||
|
||||
public FragmentedMp4Extractor() {
|
||||
this(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param workaroundFlags Flags to allow parsing of faulty streams.
|
||||
* {@link #WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME} is currently the only flag defined.
|
||||
*/
|
||||
public FragmentedMp4Extractor(int workaroundFlags) {
|
||||
this.workaroundFlags = workaroundFlags;
|
||||
atomHeader = new ParsableByteArray(Atom.HEADER_SIZE);
|
||||
nalStartCode = new ParsableByteArray(H264Util.NAL_START_CODE);
|
||||
nalLength = new ParsableByteArray(4);
|
||||
encryptionSignalByte = new ParsableByteArray(1);
|
||||
extendedTypeScratch = new byte[16];
|
||||
containerAtoms = new Stack<ContainerAtom>();
|
||||
fragmentRun = new TrackFragment();
|
||||
parserState = STATE_READING_ATOM_HEADER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sideloads track information into the extractor.
|
||||
* <p>
|
||||
* Should be called before {@link #read(ExtractorInput, PositionHolder)} in the case that the
|
||||
* extractor will not receive a moov atom in the input data, from which track information would
|
||||
* normally be parsed.
|
||||
*
|
||||
* @param track The track to sideload.
|
||||
*/
|
||||
public void setTrack(Track track) {
|
||||
this.extendsDefaults = new DefaultSampleValues(0, 0, 0, 0);
|
||||
this.track = track;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(ExtractorOutput output) {
|
||||
extractorOutput = output;
|
||||
trackOutput = output.track(0);
|
||||
extractorOutput.endTracks();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seek() {
|
||||
containerAtoms.clear();
|
||||
rootAtomBytesRead = 0;
|
||||
parserState = STATE_READING_ATOM_HEADER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(ExtractorInput input, PositionHolder seekPosition)
|
||||
throws IOException, InterruptedException {
|
||||
while (true) {
|
||||
switch (parserState) {
|
||||
case STATE_READING_ATOM_HEADER:
|
||||
if (!readAtomHeader(input)) {
|
||||
return Extractor.RESULT_END_OF_INPUT;
|
||||
}
|
||||
break;
|
||||
case STATE_READING_ATOM_PAYLOAD:
|
||||
readAtomPayload(input);
|
||||
break;
|
||||
case STATE_READING_ENCRYPTION_DATA:
|
||||
readEncryptionData(input);
|
||||
break;
|
||||
default:
|
||||
if (readSample(input)) {
|
||||
return RESULT_CONTINUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean readAtomHeader(ExtractorInput input) throws IOException, InterruptedException {
|
||||
if (!input.readFully(atomHeader.data, 0, Atom.HEADER_SIZE, true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
rootAtomBytesRead += Atom.HEADER_SIZE;
|
||||
atomHeader.setPosition(0);
|
||||
atomSize = atomHeader.readInt();
|
||||
atomType = atomHeader.readInt();
|
||||
|
||||
if (atomType == Atom.TYPE_mdat) {
|
||||
if (fragmentRun.sampleEncryptionDataNeedsFill) {
|
||||
parserState = STATE_READING_ENCRYPTION_DATA;
|
||||
} else {
|
||||
parserState = STATE_READING_SAMPLE_START;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (shouldParseAtom(atomType)) {
|
||||
if (shouldParseContainerAtom(atomType)) {
|
||||
parserState = STATE_READING_ATOM_HEADER;
|
||||
containerAtoms.add(new ContainerAtom(atomType,
|
||||
rootAtomBytesRead + atomSize - Atom.HEADER_SIZE));
|
||||
} else {
|
||||
atomData = new ParsableByteArray(atomSize);
|
||||
System.arraycopy(atomHeader.data, 0, atomData.data, 0, Atom.HEADER_SIZE);
|
||||
parserState = STATE_READING_ATOM_PAYLOAD;
|
||||
}
|
||||
} else {
|
||||
atomData = null;
|
||||
parserState = STATE_READING_ATOM_PAYLOAD;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void readAtomPayload(ExtractorInput input) throws IOException, InterruptedException {
|
||||
int payloadLength = atomSize - Atom.HEADER_SIZE;
|
||||
if (atomData != null) {
|
||||
input.readFully(atomData.data, Atom.HEADER_SIZE, payloadLength);
|
||||
rootAtomBytesRead += payloadLength;
|
||||
onLeafAtomRead(new LeafAtom(atomType, atomData), input.getPosition());
|
||||
} else {
|
||||
input.skipFully(payloadLength);
|
||||
rootAtomBytesRead += payloadLength;
|
||||
}
|
||||
while (!containerAtoms.isEmpty() && containerAtoms.peek().endByteOffset == rootAtomBytesRead) {
|
||||
onContainerAtomRead(containerAtoms.pop());
|
||||
}
|
||||
if (containerAtoms.isEmpty()) {
|
||||
rootAtomBytesRead = 0;
|
||||
}
|
||||
parserState = STATE_READING_ATOM_HEADER;
|
||||
}
|
||||
|
||||
private void onLeafAtomRead(LeafAtom leaf, long inputPosition) {
|
||||
if (!containerAtoms.isEmpty()) {
|
||||
containerAtoms.peek().add(leaf);
|
||||
} else if (leaf.type == Atom.TYPE_sidx) {
|
||||
ChunkIndex segmentIndex = parseSidx(leaf.data, inputPosition);
|
||||
extractorOutput.seekMap(segmentIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private void onContainerAtomRead(ContainerAtom container) {
|
||||
if (container.type == Atom.TYPE_moov) {
|
||||
onMoovContainerAtomRead(container);
|
||||
} else if (container.type == Atom.TYPE_moof) {
|
||||
onMoofContainerAtomRead(container);
|
||||
} else if (!containerAtoms.isEmpty()) {
|
||||
containerAtoms.peek().add(container);
|
||||
}
|
||||
}
|
||||
|
||||
private void onMoovContainerAtomRead(ContainerAtom moov) {
|
||||
List<Atom.LeafAtom> moovChildren = moov.leafChildren;
|
||||
int moovChildrenSize = moovChildren.size();
|
||||
|
||||
DrmInitData.Mapped drmInitData = null;
|
||||
for (int i = 0; i < moovChildrenSize; i++) {
|
||||
LeafAtom child = moovChildren.get(i);
|
||||
if (child.type == Atom.TYPE_pssh) {
|
||||
ParsableByteArray psshAtom = child.data;
|
||||
psshAtom.setPosition(Atom.FULL_HEADER_SIZE);
|
||||
UUID uuid = new UUID(psshAtom.readLong(), psshAtom.readLong());
|
||||
int dataSize = psshAtom.readInt();
|
||||
byte[] data = new byte[dataSize];
|
||||
psshAtom.readBytes(data, 0, dataSize);
|
||||
if (drmInitData == null) {
|
||||
drmInitData = new DrmInitData.Mapped(MimeTypes.VIDEO_MP4);
|
||||
}
|
||||
drmInitData.put(uuid, data);
|
||||
}
|
||||
}
|
||||
if (drmInitData != null) {
|
||||
extractorOutput.drmInitData(drmInitData);
|
||||
}
|
||||
|
||||
ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex);
|
||||
extendsDefaults = parseTrex(mvex.getLeafAtomOfType(Atom.TYPE_trex).data);
|
||||
track = AtomParsers.parseTrak(moov.getContainerAtomOfType(Atom.TYPE_trak),
|
||||
moov.getLeafAtomOfType(Atom.TYPE_mvhd));
|
||||
Assertions.checkState(track != null);
|
||||
trackOutput.format(track.mediaFormat);
|
||||
}
|
||||
|
||||
private void onMoofContainerAtomRead(ContainerAtom moof) {
|
||||
fragmentRun.reset();
|
||||
parseMoof(track, extendsDefaults, moof, fragmentRun, workaroundFlags, extendedTypeScratch);
|
||||
sampleIndex = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a trex atom (defined in 14496-12).
|
||||
*/
|
||||
private static DefaultSampleValues parseTrex(ParsableByteArray trex) {
|
||||
trex.setPosition(Atom.FULL_HEADER_SIZE + 4);
|
||||
int defaultSampleDescriptionIndex = trex.readUnsignedIntToInt() - 1;
|
||||
int defaultSampleDuration = trex.readUnsignedIntToInt();
|
||||
int defaultSampleSize = trex.readUnsignedIntToInt();
|
||||
int defaultSampleFlags = trex.readInt();
|
||||
return new DefaultSampleValues(defaultSampleDescriptionIndex, defaultSampleDuration,
|
||||
defaultSampleSize, defaultSampleFlags);
|
||||
}
|
||||
|
||||
private static void parseMoof(Track track, DefaultSampleValues extendsDefaults,
|
||||
ContainerAtom moof, TrackFragment out, int workaroundFlags, byte[] extendedTypeScratch) {
|
||||
parseTraf(track, extendsDefaults, moof.getContainerAtomOfType(Atom.TYPE_traf),
|
||||
out, workaroundFlags, extendedTypeScratch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a traf atom (defined in 14496-12).
|
||||
*/
|
||||
private static void parseTraf(Track track, DefaultSampleValues extendsDefaults,
|
||||
ContainerAtom traf, TrackFragment out, int workaroundFlags, byte[] extendedTypeScratch) {
|
||||
LeafAtom tfdtAtom = traf.getLeafAtomOfType(Atom.TYPE_tfdt);
|
||||
long decodeTime = tfdtAtom == null ? 0 : parseTfdt(traf.getLeafAtomOfType(Atom.TYPE_tfdt).data);
|
||||
|
||||
LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd);
|
||||
DefaultSampleValues fragmentHeader = parseTfhd(extendsDefaults, tfhd.data);
|
||||
out.sampleDescriptionIndex = fragmentHeader.sampleDescriptionIndex;
|
||||
|
||||
LeafAtom trun = traf.getLeafAtomOfType(Atom.TYPE_trun);
|
||||
parseTrun(track, fragmentHeader, decodeTime, workaroundFlags, trun.data, out);
|
||||
|
||||
LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz);
|
||||
if (saiz != null) {
|
||||
TrackEncryptionBox trackEncryptionBox =
|
||||
track.sampleDescriptionEncryptionBoxes[fragmentHeader.sampleDescriptionIndex];
|
||||
parseSaiz(trackEncryptionBox, saiz.data, out);
|
||||
}
|
||||
|
||||
LeafAtom senc = traf.getLeafAtomOfType(Atom.TYPE_senc);
|
||||
if (senc != null) {
|
||||
parseSenc(senc.data, out);
|
||||
}
|
||||
|
||||
int childrenSize = traf.leafChildren.size();
|
||||
for (int i = 0; i < childrenSize; i++) {
|
||||
LeafAtom atom = traf.leafChildren.get(i);
|
||||
if (atom.type == Atom.TYPE_uuid) {
|
||||
parseUuid(atom.data, out, extendedTypeScratch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void parseSaiz(TrackEncryptionBox encryptionBox, ParsableByteArray saiz,
|
||||
TrackFragment out) {
|
||||
int vectorSize = encryptionBox.initializationVectorSize;
|
||||
saiz.setPosition(Atom.HEADER_SIZE);
|
||||
int fullAtom = saiz.readInt();
|
||||
int flags = Atom.parseFullAtomFlags(fullAtom);
|
||||
if ((flags & 0x01) == 1) {
|
||||
saiz.skip(8);
|
||||
}
|
||||
int defaultSampleInfoSize = saiz.readUnsignedByte();
|
||||
|
||||
int sampleCount = saiz.readUnsignedIntToInt();
|
||||
if (sampleCount != out.length) {
|
||||
throw new IllegalStateException("Length mismatch: " + sampleCount + ", " + out.length);
|
||||
}
|
||||
|
||||
int totalSize = 0;
|
||||
if (defaultSampleInfoSize == 0) {
|
||||
boolean[] sampleHasSubsampleEncryptionTable = out.sampleHasSubsampleEncryptionTable;
|
||||
for (int i = 0; i < sampleCount; i++) {
|
||||
int sampleInfoSize = saiz.readUnsignedByte();
|
||||
totalSize += sampleInfoSize;
|
||||
sampleHasSubsampleEncryptionTable[i] = sampleInfoSize > vectorSize;
|
||||
}
|
||||
} else {
|
||||
boolean subsampleEncryption = defaultSampleInfoSize > vectorSize;
|
||||
totalSize += defaultSampleInfoSize * sampleCount;
|
||||
Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption);
|
||||
}
|
||||
out.initEncryptionData(totalSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a tfhd atom (defined in 14496-12).
|
||||
*
|
||||
* @param extendsDefaults Default sample values from the trex atom.
|
||||
* @return The parsed default sample values.
|
||||
*/
|
||||
private static DefaultSampleValues parseTfhd(DefaultSampleValues extendsDefaults,
|
||||
ParsableByteArray tfhd) {
|
||||
tfhd.setPosition(Atom.HEADER_SIZE);
|
||||
int fullAtom = tfhd.readInt();
|
||||
int flags = Atom.parseFullAtomFlags(fullAtom);
|
||||
|
||||
tfhd.skip(4); // trackId
|
||||
if ((flags & 0x01 /* base_data_offset_present */) != 0) {
|
||||
tfhd.skip(8);
|
||||
}
|
||||
|
||||
int defaultSampleDescriptionIndex =
|
||||
((flags & 0x02 /* default_sample_description_index_present */) != 0)
|
||||
? tfhd.readUnsignedIntToInt() - 1 : extendsDefaults.sampleDescriptionIndex;
|
||||
int defaultSampleDuration = ((flags & 0x08 /* default_sample_duration_present */) != 0)
|
||||
? tfhd.readUnsignedIntToInt() : extendsDefaults.duration;
|
||||
int defaultSampleSize = ((flags & 0x10 /* default_sample_size_present */) != 0)
|
||||
? tfhd.readUnsignedIntToInt() : extendsDefaults.size;
|
||||
int defaultSampleFlags = ((flags & 0x20 /* default_sample_flags_present */) != 0)
|
||||
? tfhd.readUnsignedIntToInt() : extendsDefaults.flags;
|
||||
return new DefaultSampleValues(defaultSampleDescriptionIndex, defaultSampleDuration,
|
||||
defaultSampleSize, defaultSampleFlags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a tfdt atom (defined in 14496-12).
|
||||
*
|
||||
* @return baseMediaDecodeTime The sum of the decode durations of all earlier samples in the
|
||||
* media, expressed in the media's timescale.
|
||||
*/
|
||||
private static long parseTfdt(ParsableByteArray tfdt) {
|
||||
tfdt.setPosition(Atom.HEADER_SIZE);
|
||||
int fullAtom = tfdt.readInt();
|
||||
int version = Atom.parseFullAtomVersion(fullAtom);
|
||||
return version == 1 ? tfdt.readUnsignedLongToLong() : tfdt.readUnsignedInt();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a trun atom (defined in 14496-12).
|
||||
*
|
||||
* @param track The corresponding track.
|
||||
* @param defaultSampleValues Default sample values.
|
||||
* @param decodeTime The decode time.
|
||||
* @param trun The trun atom to parse.
|
||||
* @param out The {@TrackFragment} into which parsed data should be placed.
|
||||
*/
|
||||
private static void parseTrun(Track track, DefaultSampleValues defaultSampleValues,
|
||||
long decodeTime, int workaroundFlags, ParsableByteArray trun, TrackFragment out) {
|
||||
trun.setPosition(Atom.HEADER_SIZE);
|
||||
int fullAtom = trun.readInt();
|
||||
int flags = Atom.parseFullAtomFlags(fullAtom);
|
||||
|
||||
int sampleCount = trun.readUnsignedIntToInt();
|
||||
if ((flags & 0x01 /* data_offset_present */) != 0) {
|
||||
trun.skip(4);
|
||||
}
|
||||
|
||||
boolean firstSampleFlagsPresent = (flags & 0x04 /* first_sample_flags_present */) != 0;
|
||||
int firstSampleFlags = defaultSampleValues.flags;
|
||||
if (firstSampleFlagsPresent) {
|
||||
firstSampleFlags = trun.readUnsignedIntToInt();
|
||||
}
|
||||
|
||||
boolean sampleDurationsPresent = (flags & 0x100 /* sample_duration_present */) != 0;
|
||||
boolean sampleSizesPresent = (flags & 0x200 /* sample_size_present */) != 0;
|
||||
boolean sampleFlagsPresent = (flags & 0x400 /* sample_flags_present */) != 0;
|
||||
boolean sampleCompositionTimeOffsetsPresent =
|
||||
(flags & 0x800 /* sample_composition_time_offsets_present */) != 0;
|
||||
|
||||
out.initTables(sampleCount);
|
||||
int[] sampleSizeTable = out.sampleSizeTable;
|
||||
int[] sampleCompositionTimeOffsetTable = out.sampleCompositionTimeOffsetTable;
|
||||
long[] sampleDecodingTimeTable = out.sampleDecodingTimeTable;
|
||||
boolean[] sampleIsSyncFrameTable = out.sampleIsSyncFrameTable;
|
||||
|
||||
long timescale = track.timescale;
|
||||
long cumulativeTime = decodeTime;
|
||||
boolean workaroundEveryVideoFrameIsSyncFrame = track.type == Track.TYPE_VIDEO
|
||||
&& ((workaroundFlags & WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME)
|
||||
== WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME);
|
||||
for (int i = 0; i < sampleCount; i++) {
|
||||
// Use trun values if present, otherwise tfhd, otherwise trex.
|
||||
int sampleDuration = sampleDurationsPresent ? trun.readUnsignedIntToInt()
|
||||
: defaultSampleValues.duration;
|
||||
int sampleSize = sampleSizesPresent ? trun.readUnsignedIntToInt() : defaultSampleValues.size;
|
||||
int sampleFlags = (i == 0 && firstSampleFlagsPresent) ? firstSampleFlags
|
||||
: sampleFlagsPresent ? trun.readInt() : defaultSampleValues.flags;
|
||||
if (sampleCompositionTimeOffsetsPresent) {
|
||||
// The BMFF spec (ISO 14496-12) states that sample offsets should be unsigned integers in
|
||||
// version 0 trun boxes, however a significant number of streams violate the spec and use
|
||||
// signed integers instead. It's safe to always parse sample offsets as signed integers
|
||||
// here, because unsigned integers will still be parsed correctly (unless their top bit is
|
||||
// set, which is never true in practice because sample offsets are always small).
|
||||
int sampleOffset = trun.readInt();
|
||||
sampleCompositionTimeOffsetTable[i] = (int) ((sampleOffset * 1000) / timescale);
|
||||
} else {
|
||||
sampleCompositionTimeOffsetTable[i] = 0;
|
||||
}
|
||||
sampleDecodingTimeTable[i] = (cumulativeTime * 1000) / timescale;
|
||||
sampleSizeTable[i] = sampleSize;
|
||||
sampleIsSyncFrameTable[i] = ((sampleFlags >> 16) & 0x1) == 0
|
||||
&& (!workaroundEveryVideoFrameIsSyncFrame || i == 0);
|
||||
cumulativeTime += sampleDuration;
|
||||
}
|
||||
}
|
||||
|
||||
private static void parseUuid(ParsableByteArray uuid, TrackFragment out,
|
||||
byte[] extendedTypeScratch) {
|
||||
uuid.setPosition(Atom.HEADER_SIZE);
|
||||
uuid.readBytes(extendedTypeScratch, 0, 16);
|
||||
|
||||
// Currently this parser only supports Microsoft's PIFF SampleEncryptionBox.
|
||||
if (!Arrays.equals(extendedTypeScratch, PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Except for the extended type, this box is identical to a SENC box. See "Portable encoding of
|
||||
// audio-video objects: The Protected Interoperable File Format (PIFF), John A. Bocharov et al,
|
||||
// Section 5.3.2.1."
|
||||
parseSenc(uuid, 16, out);
|
||||
}
|
||||
|
||||
private static void parseSenc(ParsableByteArray senc, TrackFragment out) {
|
||||
parseSenc(senc, 0, out);
|
||||
}
|
||||
|
||||
private static void parseSenc(ParsableByteArray senc, int offset, TrackFragment out) {
|
||||
senc.setPosition(Atom.HEADER_SIZE + offset);
|
||||
int fullAtom = senc.readInt();
|
||||
int flags = Atom.parseFullAtomFlags(fullAtom);
|
||||
|
||||
if ((flags & 0x01 /* override_track_encryption_box_parameters */) != 0) {
|
||||
// TODO: Implement this.
|
||||
throw new IllegalStateException("Overriding TrackEncryptionBox parameters is unsupported");
|
||||
}
|
||||
|
||||
boolean subsampleEncryption = (flags & 0x02 /* use_subsample_encryption */) != 0;
|
||||
int sampleCount = senc.readUnsignedIntToInt();
|
||||
if (sampleCount != out.length) {
|
||||
throw new IllegalStateException("Length mismatch: " + sampleCount + ", " + out.length);
|
||||
}
|
||||
|
||||
Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption);
|
||||
out.initEncryptionData(senc.bytesLeft());
|
||||
out.fillEncryptionData(senc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a sidx atom (defined in 14496-12).
|
||||
*/
|
||||
private static ChunkIndex parseSidx(ParsableByteArray atom, long inputPosition) {
|
||||
atom.setPosition(Atom.HEADER_SIZE);
|
||||
int fullAtom = atom.readInt();
|
||||
int version = Atom.parseFullAtomVersion(fullAtom);
|
||||
|
||||
atom.skip(4);
|
||||
long timescale = atom.readUnsignedInt();
|
||||
long earliestPresentationTime;
|
||||
long offset = inputPosition;
|
||||
if (version == 0) {
|
||||
earliestPresentationTime = atom.readUnsignedInt();
|
||||
offset += atom.readUnsignedInt();
|
||||
} else {
|
||||
earliestPresentationTime = atom.readUnsignedLongToLong();
|
||||
offset += atom.readUnsignedLongToLong();
|
||||
}
|
||||
|
||||
atom.skip(2);
|
||||
|
||||
int referenceCount = atom.readUnsignedShort();
|
||||
int[] sizes = new int[referenceCount];
|
||||
long[] offsets = new long[referenceCount];
|
||||
long[] durationsUs = new long[referenceCount];
|
||||
long[] timesUs = new long[referenceCount];
|
||||
|
||||
long time = earliestPresentationTime;
|
||||
long timeUs = Util.scaleLargeTimestamp(time, C.MICROS_PER_SECOND, timescale);
|
||||
for (int i = 0; i < referenceCount; i++) {
|
||||
int firstInt = atom.readInt();
|
||||
|
||||
int type = 0x80000000 & firstInt;
|
||||
if (type != 0) {
|
||||
throw new IllegalStateException("Unhandled indirect reference");
|
||||
}
|
||||
long referenceDuration = atom.readUnsignedInt();
|
||||
|
||||
sizes[i] = 0x7fffffff & firstInt;
|
||||
offsets[i] = offset;
|
||||
|
||||
// Calculate time and duration values such that any rounding errors are consistent. i.e. That
|
||||
// timesUs[i] + durationsUs[i] == timesUs[i + 1].
|
||||
timesUs[i] = timeUs;
|
||||
time += referenceDuration;
|
||||
timeUs = Util.scaleLargeTimestamp(time, C.MICROS_PER_SECOND, timescale);
|
||||
durationsUs[i] = timeUs - timesUs[i];
|
||||
|
||||
atom.skip(4);
|
||||
offset += sizes[i];
|
||||
}
|
||||
|
||||
return new ChunkIndex(sizes, offsets, durationsUs, timesUs);
|
||||
}
|
||||
|
||||
private void readEncryptionData(ExtractorInput input) throws IOException, InterruptedException {
|
||||
fragmentRun.fillEncryptionData(input);
|
||||
parserState = STATE_READING_SAMPLE_START;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to extract the next sample in the current mdat atom.
|
||||
* <p>
|
||||
* If there are no more samples in the current mdat atom then the parser state is transitioned
|
||||
* to {@link #STATE_READING_ATOM_HEADER} and {@code false} is returned.
|
||||
* <p>
|
||||
* It is possible for a sample to be extracted in part in the case that an exception is thrown. In
|
||||
* this case the method can be called again to extract the remainder of the sample.
|
||||
*
|
||||
* @param input The {@link ExtractorInput} from which to read data.
|
||||
* @return True if a sample was extracted. False otherwise.
|
||||
* @throws IOException If an error occurs reading from the input.
|
||||
* @throws InterruptedException If the thread is interrupted.
|
||||
*/
|
||||
private boolean readSample(ExtractorInput input) throws IOException, InterruptedException {
|
||||
if (sampleIndex >= fragmentRun.length) {
|
||||
// We've run out of samples in the current mdat atom.
|
||||
parserState = STATE_READING_ATOM_HEADER;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parserState == STATE_READING_SAMPLE_START) {
|
||||
sampleSize = fragmentRun.sampleSizeTable[sampleIndex];
|
||||
if (fragmentRun.definesEncryptionData) {
|
||||
sampleBytesWritten = appendSampleEncryptionData(fragmentRun.sampleEncryptionData);
|
||||
sampleSize += sampleBytesWritten;
|
||||
} else {
|
||||
sampleBytesWritten = 0;
|
||||
}
|
||||
sampleCurrentNalBytesRemaining = 0;
|
||||
parserState = STATE_READING_SAMPLE_CONTINUE;
|
||||
}
|
||||
|
||||
if (track.type == Track.TYPE_VIDEO) {
|
||||
while (sampleBytesWritten < sampleSize) {
|
||||
// NAL units are length delimited, but the decoder requires start code delimited units.
|
||||
if (sampleCurrentNalBytesRemaining == 0) {
|
||||
// Read the NAL length so that we know where we find the next NAL unit.
|
||||
input.readFully(nalLength.data, 0, 4);
|
||||
nalLength.setPosition(0);
|
||||
sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt();
|
||||
// Write a start code for the current NAL unit.
|
||||
nalStartCode.setPosition(0);
|
||||
trackOutput.sampleData(nalStartCode, 4);
|
||||
sampleBytesWritten += 4;
|
||||
} else {
|
||||
// Write the payload of the NAL unit.
|
||||
int writtenBytes = trackOutput.sampleData(input, sampleCurrentNalBytesRemaining);
|
||||
sampleBytesWritten += writtenBytes;
|
||||
sampleCurrentNalBytesRemaining -= writtenBytes;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
while (sampleBytesWritten < sampleSize) {
|
||||
int writtenBytes = trackOutput.sampleData(input, sampleSize - sampleBytesWritten);
|
||||
sampleBytesWritten += writtenBytes;
|
||||
}
|
||||
}
|
||||
|
||||
long sampleTimeUs = fragmentRun.getSamplePresentationTime(sampleIndex) * 1000L;
|
||||
int sampleFlags = (fragmentRun.definesEncryptionData ? C.SAMPLE_FLAG_ENCRYPTED : 0)
|
||||
| (fragmentRun.sampleIsSyncFrameTable[sampleIndex] ? C.SAMPLE_FLAG_SYNC : 0);
|
||||
byte[] encryptionKey = fragmentRun.definesEncryptionData
|
||||
? track.sampleDescriptionEncryptionBoxes[fragmentRun.sampleDescriptionIndex].keyId : null;
|
||||
trackOutput.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, encryptionKey);
|
||||
|
||||
sampleIndex++;
|
||||
parserState = STATE_READING_SAMPLE_START;
|
||||
return true;
|
||||
}
|
||||
|
||||
private int appendSampleEncryptionData(ParsableByteArray sampleEncryptionData) {
|
||||
TrackEncryptionBox encryptionBox =
|
||||
track.sampleDescriptionEncryptionBoxes[fragmentRun.sampleDescriptionIndex];
|
||||
int vectorSize = encryptionBox.initializationVectorSize;
|
||||
boolean subsampleEncryption = fragmentRun.sampleHasSubsampleEncryptionTable[sampleIndex];
|
||||
|
||||
// Write the signal byte, containing the vector size and the subsample encryption flag.
|
||||
encryptionSignalByte.data[0] = (byte) (vectorSize | (subsampleEncryption ? 0x80 : 0));
|
||||
encryptionSignalByte.setPosition(0);
|
||||
trackOutput.sampleData(encryptionSignalByte, 1);
|
||||
// Write the vector.
|
||||
trackOutput.sampleData(sampleEncryptionData, vectorSize);
|
||||
// If we don't have subsample encryption data, we're done.
|
||||
if (!subsampleEncryption) {
|
||||
return 1 + vectorSize;
|
||||
}
|
||||
// Write the subsample encryption data.
|
||||
int subsampleCount = sampleEncryptionData.readUnsignedShort();
|
||||
sampleEncryptionData.skip(-2);
|
||||
int subsampleDataLength = 2 + 6 * subsampleCount;
|
||||
trackOutput.sampleData(sampleEncryptionData, subsampleDataLength);
|
||||
return 1 + vectorSize + subsampleDataLength;
|
||||
}
|
||||
|
||||
/** Returns whether the extractor should parse an atom with type {@code atom}. */
|
||||
private static boolean shouldParseAtom(int atom) {
|
||||
return atom == Atom.TYPE_avc1 || atom == Atom.TYPE_avc3 || atom == Atom.TYPE_esds
|
||||
|| atom == Atom.TYPE_hdlr || atom == Atom.TYPE_mdat || atom == Atom.TYPE_mdhd
|
||||
|| atom == Atom.TYPE_moof || atom == Atom.TYPE_moov || atom == Atom.TYPE_mp4a
|
||||
|| atom == Atom.TYPE_mvhd || atom == Atom.TYPE_sidx || atom == Atom.TYPE_stsd
|
||||
|| atom == Atom.TYPE_tfdt || atom == Atom.TYPE_tfhd || atom == Atom.TYPE_tkhd
|
||||
|| atom == Atom.TYPE_traf || atom == Atom.TYPE_trak || atom == Atom.TYPE_trex
|
||||
|| atom == Atom.TYPE_trun || atom == Atom.TYPE_mvex || atom == Atom.TYPE_mdia
|
||||
|| atom == Atom.TYPE_minf || atom == Atom.TYPE_stbl || atom == Atom.TYPE_pssh
|
||||
|| atom == Atom.TYPE_saiz || atom == Atom.TYPE_uuid || atom == Atom.TYPE_senc
|
||||
|| atom == Atom.TYPE_pasp;
|
||||
}
|
||||
|
||||
/** Returns whether the extractor should parse a container atom with type {@code atom}. */
|
||||
private static boolean shouldParseContainerAtom(int atom) {
|
||||
return atom == Atom.TYPE_moov || atom == Atom.TYPE_trak || atom == Atom.TYPE_mdia
|
||||
|| atom == Atom.TYPE_minf || atom == Atom.TYPE_stbl || atom == Atom.TYPE_avcC
|
||||
|| atom == Atom.TYPE_moof || atom == Atom.TYPE_traf || atom == Atom.TYPE_mvex;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,371 @@
|
|||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer.extractor.mp4;
|
||||
|
||||
import com.google.android.exoplayer.extractor.Extractor;
|
||||
import com.google.android.exoplayer.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer.extractor.PositionHolder;
|
||||
import com.google.android.exoplayer.extractor.SeekMap;
|
||||
import com.google.android.exoplayer.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer.extractor.mp4.Atom.ContainerAtom;
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
import com.google.android.exoplayer.util.H264Util;
|
||||
import com.google.android.exoplayer.util.MimeTypes;
|
||||
import com.google.android.exoplayer.util.ParsableByteArray;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Stack;
|
||||
|
||||
/**
|
||||
* Extracts data from an unfragmented MP4 file.
|
||||
*/
|
||||
public final class Mp4Extractor implements Extractor, SeekMap {
|
||||
|
||||
// Parser states.
|
||||
private static final int STATE_READING_ATOM_HEADER = 0;
|
||||
private static final int STATE_READING_ATOM_PAYLOAD = 1;
|
||||
private static final int STATE_READING_SAMPLE = 2;
|
||||
|
||||
/**
|
||||
* When seeking within the source, if the offset is greater than or equal to this value (or the
|
||||
* offset is negative), the source will be reloaded.
|
||||
*/
|
||||
private static final int RELOAD_MINIMUM_SEEK_DISTANCE = 256 * 1024;
|
||||
|
||||
// Temporary arrays.
|
||||
private final ParsableByteArray nalStartCode;
|
||||
private final ParsableByteArray nalLength;
|
||||
|
||||
private final ParsableByteArray atomHeader;
|
||||
private final Stack<ContainerAtom> containerAtoms;
|
||||
|
||||
private int parserState;
|
||||
private long rootAtomBytesRead;
|
||||
private int atomType;
|
||||
private long atomSize;
|
||||
private int atomBytesRead;
|
||||
private ParsableByteArray atomData;
|
||||
|
||||
private int sampleSize;
|
||||
private int sampleBytesWritten;
|
||||
private int sampleCurrentNalBytesRemaining;
|
||||
|
||||
// Extractor outputs.
|
||||
private ExtractorOutput extractorOutput;
|
||||
private Mp4Track[] tracks;
|
||||
|
||||
public Mp4Extractor() {
|
||||
atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE);
|
||||
containerAtoms = new Stack<Atom.ContainerAtom>();
|
||||
nalStartCode = new ParsableByteArray(H264Util.NAL_START_CODE);
|
||||
nalLength = new ParsableByteArray(4);
|
||||
parserState = STATE_READING_ATOM_HEADER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(ExtractorOutput output) {
|
||||
extractorOutput = output;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seek() {
|
||||
rootAtomBytesRead = 0;
|
||||
sampleBytesWritten = 0;
|
||||
sampleCurrentNalBytesRemaining = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(ExtractorInput input, PositionHolder seekPosition)
|
||||
throws IOException, InterruptedException {
|
||||
while (true) {
|
||||
switch (parserState) {
|
||||
case STATE_READING_ATOM_HEADER:
|
||||
if (!readAtomHeader(input)) {
|
||||
return RESULT_END_OF_INPUT;
|
||||
}
|
||||
break;
|
||||
case STATE_READING_ATOM_PAYLOAD:
|
||||
if (readAtomPayload(input, seekPosition)) {
|
||||
return RESULT_SEEK;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return readSample(input, seekPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SeekMap implementation.
|
||||
|
||||
@Override
|
||||
public long getPosition(long timeUs) {
|
||||
long earliestSamplePosition = Long.MAX_VALUE;
|
||||
for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) {
|
||||
TrackSampleTable sampleTable = tracks[trackIndex].sampleTable;
|
||||
int sampleIndex = sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(timeUs);
|
||||
if (sampleIndex == TrackSampleTable.NO_SAMPLE) {
|
||||
sampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs);
|
||||
}
|
||||
tracks[trackIndex].sampleIndex = sampleIndex;
|
||||
|
||||
long offset = sampleTable.offsets[tracks[trackIndex].sampleIndex];
|
||||
if (offset < earliestSamplePosition) {
|
||||
earliestSamplePosition = offset;
|
||||
}
|
||||
}
|
||||
return earliestSamplePosition;
|
||||
}
|
||||
|
||||
private boolean readAtomHeader(ExtractorInput input) throws IOException, InterruptedException {
|
||||
if (!input.readFully(atomHeader.data, 0, Atom.HEADER_SIZE, true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
atomHeader.setPosition(0);
|
||||
atomSize = atomHeader.readUnsignedInt();
|
||||
atomType = atomHeader.readInt();
|
||||
if (atomSize == Atom.LONG_SIZE_PREFIX) {
|
||||
// The extended atom size is contained in the next 8 bytes, so try to read it now.
|
||||
input.readFully(atomHeader.data, Atom.HEADER_SIZE, Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE);
|
||||
atomSize = atomHeader.readLong();
|
||||
rootAtomBytesRead += Atom.LONG_HEADER_SIZE;
|
||||
atomBytesRead = Atom.LONG_HEADER_SIZE;
|
||||
} else {
|
||||
rootAtomBytesRead += Atom.HEADER_SIZE;
|
||||
atomBytesRead = Atom.HEADER_SIZE;
|
||||
}
|
||||
|
||||
if (shouldParseContainerAtom(atomType)) {
|
||||
if (atomSize == Atom.LONG_SIZE_PREFIX) {
|
||||
containerAtoms.add(
|
||||
new ContainerAtom(atomType, rootAtomBytesRead + atomSize - atomBytesRead));
|
||||
} else {
|
||||
containerAtoms.add(
|
||||
new ContainerAtom(atomType, rootAtomBytesRead + atomSize - atomBytesRead));
|
||||
}
|
||||
parserState = STATE_READING_ATOM_HEADER;
|
||||
} else if (shouldParseLeafAtom(atomType)) {
|
||||
Assertions.checkState(atomSize < Integer.MAX_VALUE);
|
||||
atomData = new ParsableByteArray((int) atomSize);
|
||||
System.arraycopy(atomHeader.data, 0, atomData.data, 0, Atom.HEADER_SIZE);
|
||||
parserState = STATE_READING_ATOM_PAYLOAD;
|
||||
} else {
|
||||
atomData = null;
|
||||
parserState = STATE_READING_ATOM_PAYLOAD;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the atom payload. If {@link #atomData} is null and the size is at or above the
|
||||
* threshold {@link #RELOAD_MINIMUM_SEEK_DISTANCE}, {@code true} is returned and the caller should
|
||||
* restart loading at the position in {@code positionHolder}. Otherwise, the atom is read/skipped.
|
||||
*/
|
||||
private boolean readAtomPayload(ExtractorInput input, PositionHolder positionHolder)
|
||||
throws IOException, InterruptedException {
|
||||
parserState = STATE_READING_ATOM_HEADER;
|
||||
rootAtomBytesRead += atomSize - atomBytesRead;
|
||||
long atomRemainingBytes = atomSize - atomBytesRead;
|
||||
boolean seekRequired = atomData == null
|
||||
&& (atomSize >= RELOAD_MINIMUM_SEEK_DISTANCE || atomSize > Integer.MAX_VALUE);
|
||||
if (seekRequired) {
|
||||
positionHolder.position = rootAtomBytesRead;
|
||||
} else if (atomData != null) {
|
||||
input.readFully(atomData.data, atomBytesRead, (int) atomRemainingBytes);
|
||||
if (!containerAtoms.isEmpty()) {
|
||||
containerAtoms.peek().add(new Atom.LeafAtom(atomType, atomData));
|
||||
}
|
||||
} else {
|
||||
input.skipFully((int) atomRemainingBytes);
|
||||
}
|
||||
|
||||
while (!containerAtoms.isEmpty() && containerAtoms.peek().endByteOffset == rootAtomBytesRead) {
|
||||
Atom.ContainerAtom containerAtom = containerAtoms.pop();
|
||||
if (containerAtom.type == Atom.TYPE_moov) {
|
||||
processMoovAtom(containerAtom);
|
||||
} else if (!containerAtoms.isEmpty()) {
|
||||
containerAtoms.peek().add(containerAtom);
|
||||
}
|
||||
}
|
||||
|
||||
return seekRequired;
|
||||
}
|
||||
|
||||
/** Updates the stored track metadata to reflect the contents of the specified moov atom. */
|
||||
private void processMoovAtom(ContainerAtom moov) {
|
||||
List<Mp4Track> tracks = new ArrayList<Mp4Track>();
|
||||
long earliestSampleOffset = Long.MAX_VALUE;
|
||||
for (int i = 0; i < moov.containerChildren.size(); i++) {
|
||||
Atom.ContainerAtom atom = moov.containerChildren.get(i);
|
||||
if (atom.type != Atom.TYPE_trak) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Track track = AtomParsers.parseTrak(atom, moov.getLeafAtomOfType(Atom.TYPE_mvhd));
|
||||
if (track == null || (track.type != Track.TYPE_AUDIO && track.type != Track.TYPE_VIDEO)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia)
|
||||
.getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl);
|
||||
TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom);
|
||||
if (trackSampleTable.sampleCount == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Mp4Track mp4Track = new Mp4Track(track, trackSampleTable, extractorOutput.track(i));
|
||||
mp4Track.trackOutput.format(track.mediaFormat);
|
||||
tracks.add(mp4Track);
|
||||
|
||||
long firstSampleOffset = trackSampleTable.offsets[0];
|
||||
if (firstSampleOffset < earliestSampleOffset) {
|
||||
earliestSampleOffset = firstSampleOffset;
|
||||
}
|
||||
}
|
||||
this.tracks = tracks.toArray(new Mp4Track[0]);
|
||||
extractorOutput.endTracks();
|
||||
extractorOutput.seekMap(this);
|
||||
parserState = STATE_READING_SAMPLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to extract the next sample in the current mdat atom for the specified track.
|
||||
* <p>
|
||||
* Returns {@link #RESULT_SEEK} if the source should be reloaded from the position in
|
||||
* {@code positionHolder}.
|
||||
* <p>
|
||||
* Returns {@link #RESULT_END_OF_INPUT} if no samples are left. Otherwise, returns
|
||||
* {@link #RESULT_CONTINUE}.
|
||||
*
|
||||
* @param input The {@link ExtractorInput} from which to read data.
|
||||
* @param positionHolder If {@link #RESULT_SEEK} is returned, this holder is updated to hold the
|
||||
* position of the required data.
|
||||
* @return One of the {@code RESULT_*} flags in {@link Extractor}.
|
||||
* @throws IOException If an error occurs reading from the input.
|
||||
* @throws InterruptedException If the thread is interrupted.
|
||||
*/
|
||||
private int readSample(ExtractorInput input, PositionHolder positionHolder)
|
||||
throws IOException, InterruptedException {
|
||||
int trackIndex = getTrackIndexOfEarliestCurrentSample();
|
||||
if (trackIndex == TrackSampleTable.NO_SAMPLE) {
|
||||
return RESULT_END_OF_INPUT;
|
||||
}
|
||||
Mp4Track track = tracks[trackIndex];
|
||||
int sampleIndex = track.sampleIndex;
|
||||
long position = track.sampleTable.offsets[sampleIndex];
|
||||
long skipAmount = position - input.getPosition() + sampleBytesWritten;
|
||||
if (skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE) {
|
||||
positionHolder.position = position;
|
||||
return RESULT_SEEK;
|
||||
}
|
||||
input.skipFully((int) skipAmount);
|
||||
sampleSize = track.sampleTable.sizes[sampleIndex];
|
||||
if (track.track.type == Track.TYPE_VIDEO
|
||||
&& MimeTypes.VIDEO_H264.equals(track.track.mediaFormat.mimeType)) {
|
||||
while (sampleBytesWritten < sampleSize) {
|
||||
// NAL units are length delimited, but the decoder requires start code delimited units.
|
||||
if (sampleCurrentNalBytesRemaining == 0) {
|
||||
// Read the NAL length so that we know where we find the next NAL unit.
|
||||
input.readFully(nalLength.data, 0, 4);
|
||||
nalLength.setPosition(0);
|
||||
sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt();
|
||||
// Write a start code for the current NAL unit.
|
||||
nalStartCode.setPosition(0);
|
||||
track.trackOutput.sampleData(nalStartCode, 4);
|
||||
sampleBytesWritten += 4;
|
||||
} else {
|
||||
// Write the payload of the NAL unit.
|
||||
int writtenBytes = track.trackOutput.sampleData(input, sampleCurrentNalBytesRemaining);
|
||||
sampleBytesWritten += writtenBytes;
|
||||
sampleCurrentNalBytesRemaining -= writtenBytes;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
while (sampleBytesWritten < sampleSize) {
|
||||
int writtenBytes = track.trackOutput.sampleData(input, sampleSize - sampleBytesWritten);
|
||||
sampleBytesWritten += writtenBytes;
|
||||
sampleCurrentNalBytesRemaining -= writtenBytes;
|
||||
}
|
||||
}
|
||||
track.trackOutput.sampleMetadata(track.sampleTable.timestampsUs[sampleIndex],
|
||||
track.sampleTable.flags[sampleIndex], sampleSize, 0, null);
|
||||
track.sampleIndex++;
|
||||
sampleBytesWritten = 0;
|
||||
sampleCurrentNalBytesRemaining = 0;
|
||||
return RESULT_CONTINUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the track that contains the earliest current sample, or
|
||||
* {@link TrackSampleTable#NO_SAMPLE} if no samples remain.
|
||||
*/
|
||||
private int getTrackIndexOfEarliestCurrentSample() {
|
||||
int earliestSampleTrackIndex = TrackSampleTable.NO_SAMPLE;
|
||||
long earliestSampleOffset = Long.MAX_VALUE;
|
||||
for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) {
|
||||
Mp4Track track = tracks[trackIndex];
|
||||
int sampleIndex = track.sampleIndex;
|
||||
if (sampleIndex == track.sampleTable.sampleCount) {
|
||||
continue;
|
||||
}
|
||||
|
||||
long trackSampleOffset = track.sampleTable.offsets[sampleIndex];
|
||||
if (trackSampleOffset < earliestSampleOffset) {
|
||||
earliestSampleOffset = trackSampleOffset;
|
||||
earliestSampleTrackIndex = trackIndex;
|
||||
}
|
||||
}
|
||||
|
||||
return earliestSampleTrackIndex;
|
||||
}
|
||||
|
||||
/** Returns whether the extractor should parse a leaf atom with type {@code atom}. */
|
||||
private static boolean shouldParseLeafAtom(int atom) {
|
||||
return atom == Atom.TYPE_mdhd || atom == Atom.TYPE_mvhd || atom == Atom.TYPE_hdlr
|
||||
|| atom == Atom.TYPE_vmhd || atom == Atom.TYPE_smhd || atom == Atom.TYPE_stsd
|
||||
|| atom == Atom.TYPE_avc1 || atom == Atom.TYPE_avcC || atom == Atom.TYPE_mp4a
|
||||
|| atom == Atom.TYPE_esds || atom == Atom.TYPE_stts || atom == Atom.TYPE_stss
|
||||
|| atom == Atom.TYPE_ctts || atom == Atom.TYPE_stsc || atom == Atom.TYPE_stsz
|
||||
|| atom == Atom.TYPE_stco || atom == Atom.TYPE_co64 || atom == Atom.TYPE_tkhd;
|
||||
}
|
||||
|
||||
/** Returns whether the extractor should parse a container atom with type {@code atom}. */
|
||||
private static boolean shouldParseContainerAtom(int atom) {
|
||||
return atom == Atom.TYPE_moov || atom == Atom.TYPE_trak || atom == Atom.TYPE_mdia
|
||||
|| atom == Atom.TYPE_minf || atom == Atom.TYPE_stbl;
|
||||
}
|
||||
|
||||
private static final class Mp4Track {
|
||||
|
||||
public final Track track;
|
||||
public final TrackSampleTable sampleTable;
|
||||
public final TrackOutput trackOutput;
|
||||
|
||||
public int sampleIndex;
|
||||
|
||||
public Mp4Track(Track track, TrackSampleTable sampleTable, TrackOutput trackOutput) {
|
||||
this.track = track;
|
||||
this.sampleTable = sampleTable;
|
||||
this.trackOutput = trackOutput;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -13,11 +13,10 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* 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.
|
||||
|
|
@ -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 {
|
||||
|
||||
/**
|
||||
|
|
@ -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.
|
||||
*
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,200 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer.mp4;
|
||||
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
import com.google.android.exoplayer.util.ParsableByteArray;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class Atom {
|
||||
|
||||
/** Size of an atom header, in bytes. */
|
||||
public static final int ATOM_HEADER_SIZE = 8;
|
||||
|
||||
/** Size of a long atom header, in bytes. */
|
||||
public static final int LONG_ATOM_HEADER_SIZE = 16;
|
||||
|
||||
/** Size of a full atom header, in bytes. */
|
||||
public static final int FULL_ATOM_HEADER_SIZE = 12;
|
||||
|
||||
/** Value for the first 32 bits of atomSize when the atom size is actually a long value. */
|
||||
public static final int LONG_SIZE_PREFIX = 1;
|
||||
|
||||
public static final int TYPE_ftyp = getAtomTypeInteger("ftyp");
|
||||
public static final int TYPE_avc1 = getAtomTypeInteger("avc1");
|
||||
public static final int TYPE_avc3 = getAtomTypeInteger("avc3");
|
||||
public static final int TYPE_esds = getAtomTypeInteger("esds");
|
||||
public static final int TYPE_mdat = getAtomTypeInteger("mdat");
|
||||
public static final int TYPE_mp4a = getAtomTypeInteger("mp4a");
|
||||
public static final int TYPE_ac_3 = getAtomTypeInteger("ac-3");
|
||||
public static final int TYPE_dac3 = getAtomTypeInteger("dac3");
|
||||
public static final int TYPE_ec_3 = getAtomTypeInteger("ec-3");
|
||||
public static final int TYPE_dec3 = getAtomTypeInteger("dec3");
|
||||
public static final int TYPE_tfdt = getAtomTypeInteger("tfdt");
|
||||
public static final int TYPE_tfhd = getAtomTypeInteger("tfhd");
|
||||
public static final int TYPE_trex = getAtomTypeInteger("trex");
|
||||
public static final int TYPE_trun = getAtomTypeInteger("trun");
|
||||
public static final int TYPE_sidx = getAtomTypeInteger("sidx");
|
||||
public static final int TYPE_moov = getAtomTypeInteger("moov");
|
||||
public static final int TYPE_mvhd = getAtomTypeInteger("mvhd");
|
||||
public static final int TYPE_trak = getAtomTypeInteger("trak");
|
||||
public static final int TYPE_mdia = getAtomTypeInteger("mdia");
|
||||
public static final int TYPE_minf = getAtomTypeInteger("minf");
|
||||
public static final int TYPE_stbl = getAtomTypeInteger("stbl");
|
||||
public static final int TYPE_avcC = getAtomTypeInteger("avcC");
|
||||
public static final int TYPE_moof = getAtomTypeInteger("moof");
|
||||
public static final int TYPE_traf = getAtomTypeInteger("traf");
|
||||
public static final int TYPE_mvex = getAtomTypeInteger("mvex");
|
||||
public static final int TYPE_tkhd = getAtomTypeInteger("tkhd");
|
||||
public static final int TYPE_mdhd = getAtomTypeInteger("mdhd");
|
||||
public static final int TYPE_hdlr = getAtomTypeInteger("hdlr");
|
||||
public static final int TYPE_stsd = getAtomTypeInteger("stsd");
|
||||
public static final int TYPE_pssh = getAtomTypeInteger("pssh");
|
||||
public static final int TYPE_sinf = getAtomTypeInteger("sinf");
|
||||
public static final int TYPE_schm = getAtomTypeInteger("schm");
|
||||
public static final int TYPE_schi = getAtomTypeInteger("schi");
|
||||
public static final int TYPE_tenc = getAtomTypeInteger("tenc");
|
||||
public static final int TYPE_encv = getAtomTypeInteger("encv");
|
||||
public static final int TYPE_enca = getAtomTypeInteger("enca");
|
||||
public static final int TYPE_frma = getAtomTypeInteger("frma");
|
||||
public static final int TYPE_saiz = getAtomTypeInteger("saiz");
|
||||
public static final int TYPE_uuid = getAtomTypeInteger("uuid");
|
||||
public static final int TYPE_senc = getAtomTypeInteger("senc");
|
||||
public static final int TYPE_pasp = getAtomTypeInteger("pasp");
|
||||
public static final int TYPE_TTML = getAtomTypeInteger("TTML");
|
||||
public static final int TYPE_vmhd = getAtomTypeInteger("vmhd");
|
||||
public static final int TYPE_smhd = getAtomTypeInteger("smhd");
|
||||
public static final int TYPE_mp4v = getAtomTypeInteger("mp4v");
|
||||
public static final int TYPE_stts = getAtomTypeInteger("stts");
|
||||
public static final int TYPE_stss = getAtomTypeInteger("stss");
|
||||
public static final int TYPE_ctts = getAtomTypeInteger("ctts");
|
||||
public static final int TYPE_stsc = getAtomTypeInteger("stsc");
|
||||
public static final int TYPE_stsz = getAtomTypeInteger("stsz");
|
||||
public static final int TYPE_stco = getAtomTypeInteger("stco");
|
||||
public static final int TYPE_co64 = getAtomTypeInteger("co64");
|
||||
|
||||
public final int type;
|
||||
|
||||
Atom(int type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getAtomTypeString(type);
|
||||
}
|
||||
|
||||
/** An MP4 atom that is a leaf. */
|
||||
public static final class LeafAtom extends Atom {
|
||||
|
||||
public final ParsableByteArray data;
|
||||
|
||||
public LeafAtom(int type, ParsableByteArray data) {
|
||||
super(type);
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** An MP4 atom that has child atoms. */
|
||||
public static final class ContainerAtom extends Atom {
|
||||
|
||||
public final long endByteOffset;
|
||||
public final List<LeafAtom> leafChildren;
|
||||
public final List<ContainerAtom> containerChildren;
|
||||
|
||||
public ContainerAtom(int type, long endByteOffset) {
|
||||
super(type);
|
||||
|
||||
leafChildren = new ArrayList<LeafAtom>();
|
||||
containerChildren = new ArrayList<ContainerAtom>();
|
||||
this.endByteOffset = endByteOffset;
|
||||
}
|
||||
|
||||
public void add(LeafAtom atom) {
|
||||
leafChildren.add(atom);
|
||||
}
|
||||
|
||||
public void add(ContainerAtom atom) {
|
||||
containerChildren.add(atom);
|
||||
}
|
||||
|
||||
public LeafAtom getLeafAtomOfType(int type) {
|
||||
int childrenSize = leafChildren.size();
|
||||
for (int i = 0; i < childrenSize; i++) {
|
||||
LeafAtom atom = leafChildren.get(i);
|
||||
if (atom.type == type) {
|
||||
return atom;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ContainerAtom getContainerAtomOfType(int type) {
|
||||
int childrenSize = containerChildren.size();
|
||||
for (int i = 0; i < childrenSize; i++) {
|
||||
ContainerAtom atom = containerChildren.get(i);
|
||||
if (atom.type == type) {
|
||||
return atom;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getAtomTypeString(type)
|
||||
+ " leaves: " + Arrays.toString(leafChildren.toArray(new LeafAtom[0]))
|
||||
+ " containers: " + Arrays.toString(containerChildren.toArray(new ContainerAtom[0]));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the version number out of the additional integer component of a full atom.
|
||||
*/
|
||||
public static int parseFullAtomVersion(int fullAtomInt) {
|
||||
return 0x000000FF & (fullAtomInt >> 24);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the atom flags out of the additional integer component of a full atom.
|
||||
*/
|
||||
public static int parseFullAtomFlags(int fullAtomInt) {
|
||||
return 0x00FFFFFF & fullAtomInt;
|
||||
}
|
||||
|
||||
private static String getAtomTypeString(int type) {
|
||||
return "" + (char) (type >> 24)
|
||||
+ (char) ((type >> 16) & 0xFF)
|
||||
+ (char) ((type >> 8) & 0xFF)
|
||||
+ (char) (type & 0xFF);
|
||||
}
|
||||
|
||||
private static int getAtomTypeInteger(String typeName) {
|
||||
Assertions.checkArgument(typeName.length() == 4);
|
||||
int result = 0;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
result <<= 8;
|
||||
result |= typeName.charAt(i);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -29,9 +29,9 @@ import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation;
|
|||
import com.google.android.exoplayer.chunk.MediaChunk;
|
||||
import com.google.android.exoplayer.chunk.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;
|
||||
|
|
|
|||
|
|
@ -1,743 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer.source;
|
||||
|
||||
import com.google.android.exoplayer.C;
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.SampleHolder;
|
||||
import com.google.android.exoplayer.SampleSource;
|
||||
import com.google.android.exoplayer.TrackRenderer;
|
||||
import com.google.android.exoplayer.drm.DrmInitData;
|
||||
import com.google.android.exoplayer.mp4.Atom;
|
||||
import com.google.android.exoplayer.mp4.Atom.ContainerAtom;
|
||||
import com.google.android.exoplayer.mp4.CommonMp4AtomParsers;
|
||||
import com.google.android.exoplayer.mp4.Mp4TrackSampleTable;
|
||||
import com.google.android.exoplayer.mp4.Track;
|
||||
import com.google.android.exoplayer.upstream.BufferPool;
|
||||
import com.google.android.exoplayer.upstream.BufferedNonBlockingInputStream;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.DataSourceStream;
|
||||
import com.google.android.exoplayer.upstream.DataSpec;
|
||||
import com.google.android.exoplayer.upstream.Loader;
|
||||
import com.google.android.exoplayer.upstream.Loader.Loadable;
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
import com.google.android.exoplayer.util.H264Util;
|
||||
import com.google.android.exoplayer.util.MimeTypes;
|
||||
import com.google.android.exoplayer.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.Stack;
|
||||
|
||||
/**
|
||||
* Extracts data from a {@link DataSpec} in unfragmented MP4 format (ISO 14496-12).
|
||||
*/
|
||||
public final class Mp4SampleExtractor implements SampleExtractor, Loader.Callback {
|
||||
|
||||
private static final String TAG = "Mp4SampleExtractor";
|
||||
private static final String LOADER_THREAD_NAME = "Mp4SampleExtractor";
|
||||
|
||||
private static final int NO_TRACK = -1;
|
||||
|
||||
// Reading results
|
||||
private static final int RESULT_NEED_MORE_DATA = 1;
|
||||
private static final int RESULT_END_OF_STREAM = 2;
|
||||
|
||||
// Parser states
|
||||
private static final int STATE_READING_ATOM_HEADER = 0;
|
||||
private static final int STATE_READING_ATOM_PAYLOAD = 1;
|
||||
|
||||
/** Set of atom types that contain data to be parsed. */
|
||||
private static final Set<Integer> LEAF_ATOM_TYPES = getAtomTypeSet(
|
||||
Atom.TYPE_mdhd, Atom.TYPE_mvhd, Atom.TYPE_hdlr, Atom.TYPE_vmhd, Atom.TYPE_smhd,
|
||||
Atom.TYPE_stsd, Atom.TYPE_avc1, Atom.TYPE_avcC, Atom.TYPE_mp4a, Atom.TYPE_esds,
|
||||
Atom.TYPE_stts, Atom.TYPE_stss, Atom.TYPE_ctts, Atom.TYPE_stsc, Atom.TYPE_stsz,
|
||||
Atom.TYPE_stco, Atom.TYPE_co64, Atom.TYPE_tkhd);
|
||||
|
||||
/** Set of atom types that contain other atoms that need to be parsed. */
|
||||
private static final Set<Integer> CONTAINER_TYPES = getAtomTypeSet(
|
||||
Atom.TYPE_moov, Atom.TYPE_trak, Atom.TYPE_mdia, Atom.TYPE_minf, Atom.TYPE_stbl);
|
||||
|
||||
/** Default number of times to retry loading data prior to failing. */
|
||||
private static final int DEFAULT_LOADABLE_RETRY_COUNT = 3;
|
||||
|
||||
private final DataSource dataSource;
|
||||
private final DataSpec dataSpec;
|
||||
|
||||
private final int readAheadAllocationSize;
|
||||
private final int reloadMinimumSeekDistance;
|
||||
private final int maximumTrackSampleInterval;
|
||||
private final int loadRetryCount;
|
||||
|
||||
private final BufferPool bufferPool;
|
||||
private final Loader loader;
|
||||
private final ParsableByteArray atomHeader;
|
||||
private final Stack<Atom.ContainerAtom> containerAtoms;
|
||||
|
||||
private DataSourceStream dataSourceStream;
|
||||
private BufferedNonBlockingInputStream inputStream;
|
||||
private long inputStreamOffset;
|
||||
private long rootAtomBytesRead;
|
||||
private boolean loadCompleted;
|
||||
|
||||
private int parserState;
|
||||
private int atomBytesRead;
|
||||
private int atomType;
|
||||
private long atomSize;
|
||||
private ParsableByteArray atomData;
|
||||
|
||||
private boolean prepared;
|
||||
|
||||
private int loadErrorCount;
|
||||
|
||||
private Mp4Track[] tracks;
|
||||
|
||||
/** An exception from {@link #inputStream}'s callbacks, or {@code null} if there was no error. */
|
||||
private IOException lastLoadError;
|
||||
private long loadErrorPosition;
|
||||
|
||||
/** If handling a call to {@link #seekTo}, the new required stream offset, or -1 otherwise. */
|
||||
private long pendingSeekPosition;
|
||||
/** If the input stream is being reopened at a new position, the new offset, or -1 otherwise. */
|
||||
private long pendingLoadPosition;
|
||||
|
||||
/**
|
||||
* Creates a new sample extractor for reading {@code dataSource} and {@code dataSpec} as an
|
||||
* unfragmented MP4 file with default settings.
|
||||
*
|
||||
* <p>The default settings read ahead by 5 MiB, handle maximum offsets between samples at the same
|
||||
* timestamp in different tracks of 3 MiB and restart loading when seeking forward by >= 256 KiB.
|
||||
*
|
||||
* @param dataSource Data source used to read from {@code dataSpec}.
|
||||
* @param dataSpec Data specification specifying what to read.
|
||||
*/
|
||||
public Mp4SampleExtractor(DataSource dataSource, DataSpec dataSpec) {
|
||||
this(dataSource, dataSpec, 5 * 1024 * 1024, 3 * 1024 * 1024, 256 * 1024,
|
||||
DEFAULT_LOADABLE_RETRY_COUNT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new sample extractor for reading {@code dataSource} and {@code dataSpec} as an
|
||||
* unfragmented MP4 file.
|
||||
*
|
||||
* @param dataSource Data source used to read from {@code dataSpec}.
|
||||
* @param dataSpec Data specification specifying what to read.
|
||||
* @param readAheadAllocationSize Size of the allocation that buffers the stream, in bytes. The
|
||||
* value must exceed the maximum sample size, so that a sample can be read in its entirety.
|
||||
* @param maximumTrackSampleInterval Size of the buffer that handles reading from any selected
|
||||
* track. The value should be chosen so that the buffer is as big as the interval in bytes
|
||||
* between the start of the earliest and the end of the latest sample required to render media
|
||||
* from all selected tracks, at any timestamp in the data source.
|
||||
* @param reloadMinimumSeekDistance Determines when {@code dataSource} is reopened while seeking:
|
||||
* if the number of bytes between the current position and the new position is greater than or
|
||||
* equal to this value, or the new position is before the current position, loading will
|
||||
* restart. The value should be set to the number of bytes that can be loaded/consumed from an
|
||||
* existing connection in the time it takes to start a new connection.
|
||||
* @param loadableRetryCount The number of times to retry loading if an error occurs.
|
||||
*/
|
||||
public Mp4SampleExtractor(DataSource dataSource, DataSpec dataSpec, int readAheadAllocationSize,
|
||||
int maximumTrackSampleInterval, int reloadMinimumSeekDistance, int loadableRetryCount) {
|
||||
// TODO: Handle minimumTrackSampleInterval specified in time not bytes.
|
||||
this.dataSource = Assertions.checkNotNull(dataSource);
|
||||
this.dataSpec = Assertions.checkNotNull(dataSpec);
|
||||
this.readAheadAllocationSize = readAheadAllocationSize;
|
||||
this.maximumTrackSampleInterval = maximumTrackSampleInterval;
|
||||
this.reloadMinimumSeekDistance = reloadMinimumSeekDistance;
|
||||
this.loadRetryCount = loadableRetryCount;
|
||||
|
||||
// TODO: Implement Allocator here so it is possible to check there is only one buffer at a time.
|
||||
bufferPool = new BufferPool(readAheadAllocationSize);
|
||||
loader = new Loader(LOADER_THREAD_NAME);
|
||||
atomHeader = new ParsableByteArray(Atom.LONG_ATOM_HEADER_SIZE);
|
||||
containerAtoms = new Stack<Atom.ContainerAtom>();
|
||||
|
||||
parserState = STATE_READING_ATOM_HEADER;
|
||||
pendingLoadPosition = -1;
|
||||
pendingSeekPosition = -1;
|
||||
loadErrorPosition = -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean prepare() throws IOException {
|
||||
if (inputStream == null) {
|
||||
loadFromOffset(0L);
|
||||
}
|
||||
|
||||
if (!prepared) {
|
||||
if (readHeaders() && !prepared) {
|
||||
throw new IOException("moov atom not found.");
|
||||
}
|
||||
|
||||
if (!prepared) {
|
||||
maybeThrowLoadError();
|
||||
}
|
||||
}
|
||||
|
||||
return prepared;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void selectTrack(int trackIndex) {
|
||||
Assertions.checkState(prepared);
|
||||
|
||||
if (tracks[trackIndex].selected) {
|
||||
return;
|
||||
}
|
||||
tracks[trackIndex].selected = true;
|
||||
|
||||
// Get the timestamp of the earliest currently-selected sample.
|
||||
int earliestSampleTrackIndex = getTrackIndexOfEarliestCurrentSample();
|
||||
if (earliestSampleTrackIndex == NO_TRACK) {
|
||||
tracks[trackIndex].sampleIndex = 0;
|
||||
return;
|
||||
}
|
||||
if (earliestSampleTrackIndex == Mp4TrackSampleTable.NO_SAMPLE) {
|
||||
tracks[trackIndex].sampleIndex = Mp4TrackSampleTable.NO_SAMPLE;
|
||||
return;
|
||||
}
|
||||
long timestampUs =
|
||||
tracks[earliestSampleTrackIndex].sampleTable.timestampsUs[earliestSampleTrackIndex];
|
||||
|
||||
// Find the latest sync sample in the new track that has an earlier or equal timestamp.
|
||||
tracks[trackIndex].sampleIndex =
|
||||
tracks[trackIndex].sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(timestampUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deselectTrack(int trackIndex) {
|
||||
Assertions.checkState(prepared);
|
||||
|
||||
tracks[trackIndex].selected = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getBufferedPositionUs() {
|
||||
Assertions.checkState(prepared);
|
||||
|
||||
if (pendingLoadPosition != -1) {
|
||||
return TrackRenderer.UNKNOWN_TIME_US;
|
||||
}
|
||||
|
||||
if (loadCompleted) {
|
||||
return TrackRenderer.END_OF_TRACK_US;
|
||||
}
|
||||
|
||||
// Get the absolute position to which there is data buffered.
|
||||
long bufferedPosition =
|
||||
inputStreamOffset + inputStream.getReadPosition() + inputStream.getAvailableByteCount();
|
||||
|
||||
// Find the timestamp of the latest sample that does not exceed the buffered position.
|
||||
long latestTimestampBeforeEnd = Long.MIN_VALUE;
|
||||
for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) {
|
||||
if (!tracks[trackIndex].selected) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Mp4TrackSampleTable sampleTable = tracks[trackIndex].sampleTable;
|
||||
int sampleIndex = Util.binarySearchFloor(sampleTable.offsets, bufferedPosition, false, true);
|
||||
if (sampleIndex > 0
|
||||
&& sampleTable.offsets[sampleIndex] + sampleTable.sizes[sampleIndex] > bufferedPosition) {
|
||||
sampleIndex--;
|
||||
}
|
||||
|
||||
// Update the latest timestamp if this is greater.
|
||||
long timestamp = sampleTable.timestampsUs[sampleIndex];
|
||||
if (timestamp > latestTimestampBeforeEnd) {
|
||||
latestTimestampBeforeEnd = timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
return latestTimestampBeforeEnd < 0L ? C.UNKNOWN_TIME_US : latestTimestampBeforeEnd;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekTo(long positionUs) {
|
||||
Assertions.checkState(prepared);
|
||||
|
||||
long earliestSamplePosition = Long.MAX_VALUE;
|
||||
for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) {
|
||||
if (!tracks[trackIndex].selected) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Mp4TrackSampleTable sampleTable = tracks[trackIndex].sampleTable;
|
||||
int sampleIndex = sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(positionUs);
|
||||
if (sampleIndex == Mp4TrackSampleTable.NO_SAMPLE) {
|
||||
sampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(positionUs);
|
||||
}
|
||||
tracks[trackIndex].sampleIndex = sampleIndex;
|
||||
|
||||
long offset = sampleTable.offsets[tracks[trackIndex].sampleIndex];
|
||||
if (offset < earliestSamplePosition) {
|
||||
earliestSamplePosition = offset;
|
||||
}
|
||||
}
|
||||
|
||||
pendingSeekPosition = earliestSamplePosition;
|
||||
if (pendingLoadPosition != -1) {
|
||||
loadFromOffset(earliestSamplePosition);
|
||||
return;
|
||||
}
|
||||
|
||||
inputStream.returnToMark();
|
||||
long earliestOffset = inputStreamOffset + inputStream.getReadPosition();
|
||||
long latestOffset = earliestOffset + inputStream.getAvailableByteCount();
|
||||
if (earliestSamplePosition < earliestOffset
|
||||
|| earliestSamplePosition >= latestOffset + reloadMinimumSeekDistance) {
|
||||
loadFromOffset(earliestSamplePosition);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTrackCount() {
|
||||
Assertions.checkState(prepared);
|
||||
return tracks.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaFormat getMediaFormat(int track) {
|
||||
Assertions.checkState(prepared);
|
||||
return tracks[track].track.mediaFormat;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DrmInitData getDrmInitData(int track) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int readSample(int trackIndex, SampleHolder sampleHolder) throws IOException {
|
||||
Assertions.checkState(prepared);
|
||||
|
||||
Mp4Track track = tracks[trackIndex];
|
||||
Assertions.checkState(track.selected);
|
||||
int sampleIndex = track.sampleIndex;
|
||||
|
||||
// Check for the end of the stream.
|
||||
if (sampleIndex == Mp4TrackSampleTable.NO_SAMPLE) {
|
||||
// TODO: Should END_OF_STREAM be returned as soon as this track has no more samples, or as
|
||||
// soon as no tracks have a sample (as implemented here)?
|
||||
return hasSampleInAnySelectedTrack() ? SampleSource.NOTHING_READ : SampleSource.END_OF_STREAM;
|
||||
}
|
||||
|
||||
// Return if the input stream will be reopened at the requested position.
|
||||
if (pendingLoadPosition != -1) {
|
||||
return SampleSource.NOTHING_READ;
|
||||
}
|
||||
|
||||
// If there was a seek request, try to skip forwards to the requested position.
|
||||
if (pendingSeekPosition != -1) {
|
||||
int bytesToSeekPosition =
|
||||
(int) (pendingSeekPosition - (inputStreamOffset + inputStream.getReadPosition()));
|
||||
int skippedByteCount = inputStream.skip(bytesToSeekPosition);
|
||||
if (skippedByteCount == -1) {
|
||||
throw new IOException("Unexpected end-of-stream while seeking to sample.");
|
||||
}
|
||||
bytesToSeekPosition -= skippedByteCount;
|
||||
inputStream.mark();
|
||||
if (bytesToSeekPosition == 0) {
|
||||
pendingSeekPosition = -1;
|
||||
} else {
|
||||
maybeThrowLoadError();
|
||||
return SampleSource.NOTHING_READ;
|
||||
}
|
||||
}
|
||||
|
||||
// Return if the sample offset hasn't been loaded yet.
|
||||
inputStream.returnToMark();
|
||||
long sampleOffset = track.sampleTable.offsets[sampleIndex];
|
||||
long seekOffsetLong = (sampleOffset - inputStreamOffset) - inputStream.getReadPosition();
|
||||
Assertions.checkState(seekOffsetLong <= Integer.MAX_VALUE);
|
||||
int seekOffset = (int) seekOffsetLong;
|
||||
if (inputStream.skip(seekOffset) != seekOffset) {
|
||||
maybeThrowLoadError();
|
||||
return SampleSource.NOTHING_READ;
|
||||
}
|
||||
|
||||
// Return if the sample has been loaded.
|
||||
int sampleSize = track.sampleTable.sizes[sampleIndex];
|
||||
if (inputStream.getAvailableByteCount() < sampleSize) {
|
||||
maybeThrowLoadError();
|
||||
return SampleSource.NOTHING_READ;
|
||||
}
|
||||
|
||||
if (sampleHolder.data == null || sampleHolder.data.capacity() < sampleSize) {
|
||||
sampleHolder.replaceBuffer(sampleSize);
|
||||
}
|
||||
|
||||
ByteBuffer data = sampleHolder.data;
|
||||
if (data == null) {
|
||||
inputStream.skip(sampleSize);
|
||||
sampleHolder.size = 0;
|
||||
} else {
|
||||
int bytesRead = inputStream.read(data, sampleSize);
|
||||
Assertions.checkState(bytesRead == sampleSize);
|
||||
|
||||
if (MimeTypes.VIDEO_H264.equals(tracks[trackIndex].track.mediaFormat.mimeType)) {
|
||||
// The mp4 file contains length-prefixed access units, but the decoder wants start code
|
||||
// delimited content.
|
||||
H264Util.replaceLengthPrefixesWithAvcStartCodes(sampleHolder.data, sampleSize);
|
||||
}
|
||||
sampleHolder.size = sampleSize;
|
||||
}
|
||||
|
||||
// Move the input stream mark forwards if the earliest current sample was just read.
|
||||
if (getTrackIndexOfEarliestCurrentSample() == trackIndex) {
|
||||
inputStream.mark();
|
||||
}
|
||||
|
||||
// TODO: Read encryption data.
|
||||
sampleHolder.timeUs = track.sampleTable.timestampsUs[sampleIndex];
|
||||
sampleHolder.flags = track.sampleTable.flags[sampleIndex];
|
||||
|
||||
// Advance to the next sample, checking if this was the last sample.
|
||||
track.sampleIndex =
|
||||
sampleIndex + 1 == track.sampleTable.getSampleCount() ? Mp4TrackSampleTable.NO_SAMPLE : sampleIndex + 1;
|
||||
|
||||
// Reset the loading error counter if we read past the offset at which the error was thrown.
|
||||
if (dataSourceStream.getReadPosition() > loadErrorPosition) {
|
||||
loadErrorCount = 0;
|
||||
loadErrorPosition = -1;
|
||||
}
|
||||
|
||||
return SampleSource.SAMPLE_READ;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
pendingLoadPosition = -1;
|
||||
loader.release();
|
||||
|
||||
if (inputStream != null) {
|
||||
inputStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadError(Loadable loadable, IOException exception) {
|
||||
lastLoadError = exception;
|
||||
|
||||
loadErrorCount++;
|
||||
if (loadErrorPosition == -1) {
|
||||
loadErrorPosition = dataSourceStream.getLoadPosition();
|
||||
}
|
||||
int delayMs = getRetryDelayMs(loadErrorCount);
|
||||
Log.w(TAG, "Retry loading (delay " + delayMs + " ms).");
|
||||
loader.startLoading(dataSourceStream, this, delayMs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCompleted(Loadable loadable) {
|
||||
loadCompleted = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCanceled(Loadable loadable) {
|
||||
if (pendingLoadPosition != -1) {
|
||||
loadFromOffset(pendingLoadPosition);
|
||||
pendingLoadPosition = -1;
|
||||
}
|
||||
}
|
||||
|
||||
private void loadFromOffset(long offsetBytes) {
|
||||
inputStreamOffset = offsetBytes;
|
||||
rootAtomBytesRead = offsetBytes;
|
||||
|
||||
if (loader.isLoading()) {
|
||||
// Wait for loading to be canceled before proceeding.
|
||||
pendingLoadPosition = offsetBytes;
|
||||
loader.cancelLoading();
|
||||
return;
|
||||
}
|
||||
|
||||
if (inputStream != null) {
|
||||
inputStream.close();
|
||||
}
|
||||
|
||||
DataSpec dataSpec = new DataSpec(
|
||||
this.dataSpec.uri, offsetBytes, C.LENGTH_UNBOUNDED, this.dataSpec.key);
|
||||
dataSourceStream =
|
||||
new DataSourceStream(dataSource, dataSpec, bufferPool, readAheadAllocationSize);
|
||||
loader.startLoading(dataSourceStream, this);
|
||||
|
||||
// Wrap the input stream with a buffering stream so that it is possible to read from any track.
|
||||
inputStream =
|
||||
new BufferedNonBlockingInputStream(dataSourceStream, maximumTrackSampleInterval);
|
||||
loadCompleted = false;
|
||||
|
||||
loadErrorCount = 0;
|
||||
loadErrorPosition = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the track that contains the earliest current sample, or {@link #NO_TRACK}
|
||||
* if no track is selected, or {@link Mp4TrackSampleTable#NO_SAMPLE} if no samples remain in
|
||||
* selected tracks.
|
||||
*/
|
||||
private int getTrackIndexOfEarliestCurrentSample() {
|
||||
int earliestSampleTrackIndex = NO_TRACK;
|
||||
long earliestSampleOffset = Long.MAX_VALUE;
|
||||
for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) {
|
||||
Mp4Track track = tracks[trackIndex];
|
||||
if (!track.selected) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int sampleIndex = track.sampleIndex;
|
||||
if (sampleIndex == Mp4TrackSampleTable.NO_SAMPLE) {
|
||||
if (earliestSampleTrackIndex == NO_TRACK) {
|
||||
// A track is selected, but it has no more samples.
|
||||
earliestSampleTrackIndex = Mp4TrackSampleTable.NO_SAMPLE;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
long trackSampleOffset = track.sampleTable.offsets[sampleIndex];
|
||||
if (trackSampleOffset < earliestSampleOffset) {
|
||||
earliestSampleOffset = trackSampleOffset;
|
||||
earliestSampleTrackIndex = trackIndex;
|
||||
}
|
||||
}
|
||||
|
||||
return earliestSampleTrackIndex;
|
||||
}
|
||||
|
||||
private boolean hasSampleInAnySelectedTrack() {
|
||||
boolean hasSample = false;
|
||||
for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) {
|
||||
if (tracks[trackIndex].selected && tracks[trackIndex].sampleIndex
|
||||
!= Mp4TrackSampleTable.NO_SAMPLE) {
|
||||
hasSample = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return hasSample;
|
||||
}
|
||||
|
||||
/** Reads headers, returning whether the end of the stream was reached. */
|
||||
private boolean readHeaders() {
|
||||
int results = 0;
|
||||
while (!prepared && (results & (RESULT_NEED_MORE_DATA | RESULT_END_OF_STREAM)) == 0) {
|
||||
switch (parserState) {
|
||||
case STATE_READING_ATOM_HEADER:
|
||||
results |= readAtomHeader();
|
||||
break;
|
||||
case STATE_READING_ATOM_PAYLOAD:
|
||||
results |= readAtomPayload();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (results & RESULT_END_OF_STREAM) != 0;
|
||||
}
|
||||
|
||||
private int readAtomHeader() {
|
||||
if (pendingLoadPosition != -1) {
|
||||
return RESULT_NEED_MORE_DATA;
|
||||
}
|
||||
|
||||
// The size value is either 4 or 8 bytes long (in which case atomSize = Mp4Util.LONG_ATOM_SIZE).
|
||||
int remainingBytes;
|
||||
if (atomSize != Atom.LONG_SIZE_PREFIX) {
|
||||
remainingBytes = Atom.ATOM_HEADER_SIZE - atomBytesRead;
|
||||
} else {
|
||||
remainingBytes = Atom.LONG_ATOM_HEADER_SIZE - atomBytesRead;
|
||||
}
|
||||
|
||||
int bytesRead = inputStream.read(atomHeader.data, atomBytesRead, remainingBytes);
|
||||
if (bytesRead == -1) {
|
||||
return RESULT_END_OF_STREAM;
|
||||
}
|
||||
rootAtomBytesRead += bytesRead;
|
||||
atomBytesRead += bytesRead;
|
||||
if (atomBytesRead < Atom.ATOM_HEADER_SIZE
|
||||
|| (atomSize == Atom.LONG_SIZE_PREFIX && atomBytesRead < Atom.LONG_ATOM_HEADER_SIZE)) {
|
||||
return RESULT_NEED_MORE_DATA;
|
||||
}
|
||||
|
||||
atomHeader.setPosition(0);
|
||||
atomSize = atomHeader.readUnsignedInt();
|
||||
atomType = atomHeader.readInt();
|
||||
if (atomSize == Atom.LONG_SIZE_PREFIX) {
|
||||
// The extended atom size is contained in the next 8 bytes, so try to read it now.
|
||||
if (atomBytesRead < Atom.LONG_ATOM_HEADER_SIZE) {
|
||||
return readAtomHeader();
|
||||
}
|
||||
|
||||
atomSize = atomHeader.readLong();
|
||||
}
|
||||
|
||||
Integer atomTypeInteger = atomType; // Avoids boxing atomType twice.
|
||||
if (CONTAINER_TYPES.contains(atomTypeInteger)) {
|
||||
if (atomSize == Atom.LONG_SIZE_PREFIX) {
|
||||
containerAtoms.add(new ContainerAtom(
|
||||
atomType, rootAtomBytesRead + atomSize - Atom.LONG_ATOM_HEADER_SIZE));
|
||||
} else {
|
||||
containerAtoms.add(new ContainerAtom(
|
||||
atomType, rootAtomBytesRead + atomSize - Atom.ATOM_HEADER_SIZE));
|
||||
}
|
||||
enterState(STATE_READING_ATOM_HEADER);
|
||||
} else if (LEAF_ATOM_TYPES.contains(atomTypeInteger)) {
|
||||
Assertions.checkState(atomSize <= Integer.MAX_VALUE);
|
||||
atomData = new ParsableByteArray((int) atomSize);
|
||||
System.arraycopy(atomHeader.data, 0, atomData.data, 0, Atom.ATOM_HEADER_SIZE);
|
||||
enterState(STATE_READING_ATOM_PAYLOAD);
|
||||
} else {
|
||||
atomData = null;
|
||||
enterState(STATE_READING_ATOM_PAYLOAD);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private int readAtomPayload() {
|
||||
int bytesRead;
|
||||
if (atomData != null) {
|
||||
bytesRead = inputStream.read(atomData.data, atomBytesRead, (int) atomSize - atomBytesRead);
|
||||
} else {
|
||||
if (atomSize >= reloadMinimumSeekDistance || atomSize > Integer.MAX_VALUE) {
|
||||
loadFromOffset(rootAtomBytesRead + atomSize - atomBytesRead);
|
||||
onContainerAtomRead();
|
||||
enterState(STATE_READING_ATOM_HEADER);
|
||||
return 0;
|
||||
} else {
|
||||
bytesRead = inputStream.skip((int) atomSize - atomBytesRead);
|
||||
}
|
||||
}
|
||||
if (bytesRead == -1) {
|
||||
return RESULT_END_OF_STREAM;
|
||||
}
|
||||
rootAtomBytesRead += bytesRead;
|
||||
atomBytesRead += bytesRead;
|
||||
if (atomBytesRead != atomSize) {
|
||||
return RESULT_NEED_MORE_DATA;
|
||||
}
|
||||
|
||||
if (atomData != null && !containerAtoms.isEmpty()) {
|
||||
containerAtoms.peek().add(new Atom.LeafAtom(atomType, atomData));
|
||||
}
|
||||
|
||||
onContainerAtomRead();
|
||||
|
||||
enterState(STATE_READING_ATOM_HEADER);
|
||||
return 0;
|
||||
}
|
||||
|
||||
private void onContainerAtomRead() {
|
||||
while (!containerAtoms.isEmpty() && containerAtoms.peek().endByteOffset == rootAtomBytesRead) {
|
||||
Atom.ContainerAtom containerAtom = containerAtoms.pop();
|
||||
if (containerAtom.type == Atom.TYPE_moov) {
|
||||
processMoovAtom(containerAtom);
|
||||
} else if (!containerAtoms.isEmpty()) {
|
||||
containerAtoms.peek().add(containerAtom);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void enterState(int state) {
|
||||
switch (state) {
|
||||
case STATE_READING_ATOM_HEADER:
|
||||
atomBytesRead = 0;
|
||||
atomSize = 0;
|
||||
break;
|
||||
}
|
||||
parserState = state;
|
||||
inputStream.mark();
|
||||
}
|
||||
|
||||
/** Updates the stored track metadata to reflect the contents on the specified moov atom. */
|
||||
private void processMoovAtom(Atom.ContainerAtom moov) {
|
||||
List<Mp4Track> tracks = new ArrayList<Mp4Track>();
|
||||
long earliestSampleOffset = Long.MAX_VALUE;
|
||||
for (int i = 0; i < moov.containerChildren.size(); i++) {
|
||||
Atom.ContainerAtom atom = moov.containerChildren.get(i);
|
||||
if (atom.type != Atom.TYPE_trak) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Track track = CommonMp4AtomParsers.parseTrak(atom, moov.getLeafAtomOfType(Atom.TYPE_mvhd));
|
||||
if (track.type != Track.TYPE_AUDIO && track.type != Track.TYPE_VIDEO) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia)
|
||||
.getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl);
|
||||
Mp4TrackSampleTable trackSampleTable = CommonMp4AtomParsers.parseStbl(track, stblAtom);
|
||||
|
||||
if (trackSampleTable.getSampleCount() == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
tracks.add(new Mp4Track(track, trackSampleTable));
|
||||
|
||||
// Keep track of the byte offset of the earliest sample.
|
||||
long firstSampleOffset = trackSampleTable.offsets[0];
|
||||
if (firstSampleOffset < earliestSampleOffset) {
|
||||
earliestSampleOffset = firstSampleOffset;
|
||||
}
|
||||
}
|
||||
this.tracks = tracks.toArray(new Mp4Track[0]);
|
||||
|
||||
if (earliestSampleOffset < inputStream.getReadPosition()) {
|
||||
loadFromOffset(earliestSampleOffset);
|
||||
}
|
||||
|
||||
prepared = true;
|
||||
}
|
||||
|
||||
/** Returns an unmodifiable set of atom types. */
|
||||
private static Set<Integer> getAtomTypeSet(int... atomTypes) {
|
||||
Set<Integer> atomTypeSet = new HashSet<Integer>();
|
||||
for (int atomType : atomTypes) {
|
||||
atomTypeSet.add(atomType);
|
||||
}
|
||||
return Collections.unmodifiableSet(atomTypeSet);
|
||||
}
|
||||
|
||||
private int getRetryDelayMs(int errorCount) {
|
||||
return Math.min((errorCount - 1) * 1000, 5000);
|
||||
}
|
||||
|
||||
private void maybeThrowLoadError() throws IOException {
|
||||
if (loadErrorCount > loadRetryCount) {
|
||||
throw lastLoadError;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Mp4Track {
|
||||
|
||||
public final Track track;
|
||||
public final Mp4TrackSampleTable sampleTable;
|
||||
|
||||
public boolean selected;
|
||||
public int sampleIndex;
|
||||
|
||||
public Mp4Track(Track track, Mp4TrackSampleTable sampleTable) {
|
||||
this.track = track;
|
||||
this.sampleTable = sampleTable;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -128,6 +128,16 @@ public final class BufferPool implements Allocator {
|
|||
recycledBuffers[recycledBufferCount++] = buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue