commit
280ccb1630
@ -16,8 +16,8 @@
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer.demo"
|
||||
android:versionCode="1012"
|
||||
android:versionName="1.0.12"
|
||||
android:versionCode="1013"
|
||||
android:versionName="1.0.13"
|
||||
android:theme="@style/RootTheme">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
@ -134,6 +134,8 @@ package com.google.android.exoplayer.demo;
|
||||
public static final Sample[] MISC = new Sample[] {
|
||||
new Sample("Dizzy", "uid:misc:dizzy", "http://html5demos.com/assets/dizzy.mp4",
|
||||
DemoUtil.TYPE_OTHER, false, true),
|
||||
new Sample("Dizzy (https->http redirect)", "uid:misc:dizzy2", "https://goo.gl/MtUDEj",
|
||||
DemoUtil.TYPE_OTHER, false, true),
|
||||
};
|
||||
|
||||
private Samples() {}
|
||||
|
@ -28,8 +28,7 @@ import com.google.android.exoplayer.chunk.Format;
|
||||
import com.google.android.exoplayer.chunk.FormatEvaluator;
|
||||
import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator;
|
||||
import com.google.android.exoplayer.chunk.MultiTrackChunkSource;
|
||||
import com.google.android.exoplayer.dash.DashMp4ChunkSource;
|
||||
import com.google.android.exoplayer.dash.DashWebmChunkSource;
|
||||
import com.google.android.exoplayer.dash.DashChunkSource;
|
||||
import com.google.android.exoplayer.dash.mpd.AdaptationSet;
|
||||
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
|
||||
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionFetcher;
|
||||
@ -163,14 +162,8 @@ public class DashVodRendererBuilder implements RendererBuilder,
|
||||
DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
|
||||
ChunkSource videoChunkSource;
|
||||
String mimeType = videoRepresentations[0].format.mimeType;
|
||||
if (mimeType.equals(MimeTypes.VIDEO_MP4)) {
|
||||
videoChunkSource = new DashMp4ChunkSource(videoDataSource,
|
||||
new AdaptiveEvaluator(bandwidthMeter), videoRepresentations);
|
||||
} else if (mimeType.equals(MimeTypes.VIDEO_WEBM)) {
|
||||
// TODO: Figure out how to query supported vpX resolutions. For now, restrict to standard
|
||||
// definition streams.
|
||||
videoRepresentations = getSdRepresentations(videoRepresentations);
|
||||
videoChunkSource = new DashWebmChunkSource(videoDataSource,
|
||||
if (mimeType.equals(MimeTypes.VIDEO_MP4) || mimeType.equals(MimeTypes.VIDEO_WEBM)) {
|
||||
videoChunkSource = new DashChunkSource(videoDataSource,
|
||||
new AdaptiveEvaluator(bandwidthMeter), videoRepresentations);
|
||||
} else {
|
||||
throw new IllegalStateException("Unexpected mime type: " + mimeType);
|
||||
@ -200,7 +193,7 @@ public class DashVodRendererBuilder implements RendererBuilder,
|
||||
Format format = representation.format;
|
||||
audioTrackNames[i] = format.id + " (" + format.numChannels + "ch, " +
|
||||
format.audioSamplingRate + "Hz)";
|
||||
audioChunkSources[i] = new DashMp4ChunkSource(audioDataSource,
|
||||
audioChunkSources[i] = new DashChunkSource(audioDataSource,
|
||||
audioEvaluator, representation);
|
||||
}
|
||||
audioChunkSource = new MultiTrackChunkSource(audioChunkSources);
|
||||
|
@ -26,7 +26,7 @@ import com.google.android.exoplayer.chunk.ChunkSource;
|
||||
import com.google.android.exoplayer.chunk.Format;
|
||||
import com.google.android.exoplayer.chunk.FormatEvaluator;
|
||||
import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator;
|
||||
import com.google.android.exoplayer.dash.DashMp4ChunkSource;
|
||||
import com.google.android.exoplayer.dash.DashChunkSource;
|
||||
import com.google.android.exoplayer.dash.mpd.AdaptationSet;
|
||||
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
|
||||
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionFetcher;
|
||||
@ -116,7 +116,7 @@ import java.util.ArrayList;
|
||||
|
||||
// Build the video renderer.
|
||||
DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
|
||||
ChunkSource videoChunkSource = new DashMp4ChunkSource(videoDataSource,
|
||||
ChunkSource videoChunkSource = new DashChunkSource(videoDataSource,
|
||||
new AdaptiveEvaluator(bandwidthMeter), videoRepresentations);
|
||||
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
|
||||
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
|
||||
@ -125,7 +125,7 @@ import java.util.ArrayList;
|
||||
|
||||
// Build the audio renderer.
|
||||
DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
|
||||
ChunkSource audioChunkSource = new DashMp4ChunkSource(audioDataSource,
|
||||
ChunkSource audioChunkSource = new DashChunkSource(audioDataSource,
|
||||
new FormatEvaluator.FixedEvaluator(), audioRepresentation);
|
||||
SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
|
||||
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
|
||||
|
@ -21,6 +21,12 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<!--
|
||||
Important: ExoPlayerLib specifies a minSdkVersion of 9 because various components provided by
|
||||
the library may be of use on older devices. However, please note that the core video playback
|
||||
functionality provided by the library requires API level 16 or greater.
|
||||
-->
|
||||
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="19"/>
|
||||
|
||||
</manifest>
|
||||
|
@ -26,7 +26,7 @@ public class ExoPlayerLibraryInfo {
|
||||
/**
|
||||
* The version of the library, expressed as a string.
|
||||
*/
|
||||
public static final String VERSION = "1.0.12";
|
||||
public static final String VERSION = "1.0.13";
|
||||
|
||||
/**
|
||||
* The version of the library, expressed as an integer.
|
||||
@ -34,7 +34,7 @@ public class ExoPlayerLibraryInfo {
|
||||
* Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the
|
||||
* corresponding integer version 1002003.
|
||||
*/
|
||||
public static final int VERSION_INT = 1000012;
|
||||
public static final int VERSION_INT = 1000013;
|
||||
|
||||
/**
|
||||
* Whether the library was compiled with {@link com.google.android.exoplayer.util.Assertions}
|
||||
|
@ -112,7 +112,7 @@ public final class FrameworkSampleSource implements SampleSource {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int readData(int track, long playbackPositionUs, FormatHolder formatHolder,
|
||||
public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder,
|
||||
SampleHolder sampleHolder, boolean onlyReadDiscontinuity) {
|
||||
Assertions.checkState(prepared);
|
||||
Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED);
|
||||
|
@ -27,6 +27,7 @@ import android.media.AudioTimestamp;
|
||||
import android.media.AudioTrack;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaFormat;
|
||||
import android.media.audiofx.Virtualizer;
|
||||
import android.os.ConditionVariable;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
@ -91,6 +92,13 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
|
||||
|
||||
private static final long MICROS_PER_SECOND = 1000000L;
|
||||
|
||||
/**
|
||||
* AudioTrack timestamps are deemed spurious if they are offset from the system clock by more
|
||||
* than this amount. This is a fail safe that should not be required on correctly functioning
|
||||
* devices.
|
||||
*/
|
||||
private static final long MAX_AUDIO_TIMSTAMP_OFFSET_US = 10 * MICROS_PER_SECOND;
|
||||
|
||||
private static final int MAX_PLAYHEAD_OFFSET_COUNT = 10;
|
||||
private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000;
|
||||
private static final int MIN_TIMESTAMP_SAMPLE_INTERVAL_US = 500000;
|
||||
@ -358,9 +366,9 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
|
||||
* subsequently re-enabled.
|
||||
* <p>
|
||||
* The default implementation is a no-op. One reason for overriding this method would be to
|
||||
* instantiate and enable a {@link android.media.audiofx.Virtualizer} in order to spatialize the
|
||||
* audio channels. For this use case, any {@link android.media.audiofx.Virtualizer} instances
|
||||
* should be released in {@link #onDisabled()} (if not before).
|
||||
* instantiate and enable a {@link Virtualizer} in order to spatialize the audio channels. For
|
||||
* this use case, any {@link Virtualizer} instances should be released in {@link #onDisabled()}
|
||||
* (if not before).
|
||||
*
|
||||
* @param audioSessionId The audio session id.
|
||||
*/
|
||||
@ -425,7 +433,8 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
|
||||
|
||||
@Override
|
||||
protected boolean isReady() {
|
||||
return super.isReady() || getPendingFrameCount() > 0;
|
||||
return getPendingFrameCount() > 0
|
||||
|| (super.isReady() && getSourceState() == SOURCE_STATE_READY_READ_MAY_FAIL);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -500,11 +509,18 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
|
||||
|
||||
if (systemClockUs - lastTimestampSampleTimeUs >= MIN_TIMESTAMP_SAMPLE_INTERVAL_US) {
|
||||
audioTimestampSet = audioTimestampCompat.initTimestamp(audioTrack);
|
||||
if (audioTimestampSet
|
||||
&& (audioTimestampCompat.getNanoTime() / 1000) < audioTrackResumeSystemTimeUs) {
|
||||
// The timestamp was set, but it corresponds to a time before the track was most recently
|
||||
// resumed.
|
||||
audioTimestampSet = false;
|
||||
if (audioTimestampSet) {
|
||||
// Perform sanity checks on the timestamp.
|
||||
long audioTimestampUs = audioTimestampCompat.getNanoTime() / 1000;
|
||||
if (audioTimestampUs < audioTrackResumeSystemTimeUs) {
|
||||
// The timestamp corresponds to a time before the track was most recently resumed.
|
||||
audioTimestampSet = false;
|
||||
} else if (Math.abs(audioTimestampUs - systemClockUs) > MAX_AUDIO_TIMSTAMP_OFFSET_US) {
|
||||
// The timestamp time base is probably wrong.
|
||||
audioTimestampSet = false;
|
||||
Log.w(TAG, "Spurious audio timestamp: " + audioTimestampCompat.getFramePosition() + ", "
|
||||
+ audioTimestampUs + ", " + systemClockUs);
|
||||
}
|
||||
}
|
||||
if (audioTrackGetLatencyMethod != null) {
|
||||
try {
|
||||
|
@ -78,6 +78,22 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Value of {@link #sourceState} when the source is not ready.
|
||||
*/
|
||||
protected static final int SOURCE_STATE_NOT_READY = 0;
|
||||
/**
|
||||
* Value of {@link #sourceState} when the source is ready and we're able to read from it.
|
||||
*/
|
||||
protected static final int SOURCE_STATE_READY = 1;
|
||||
/**
|
||||
* Value of {@link #sourceState} when the source is ready but we might not be able to read from
|
||||
* it. We transition to this state when an attempt to read a sample fails despite the source
|
||||
* reporting that samples are available. This can occur when the next sample to be provided by
|
||||
* the source is for another renderer.
|
||||
*/
|
||||
protected static final int SOURCE_STATE_READY_READ_MAY_FAIL = 2;
|
||||
|
||||
/**
|
||||
* If the {@link MediaCodec} is hotswapped (i.e. replaced during playback), this is the period of
|
||||
* time during which {@link #isReady()} will report true regardless of whether the new codec has
|
||||
@ -108,7 +124,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||
private final boolean playClearSamplesWithoutKeys;
|
||||
private final SampleSource source;
|
||||
private final SampleHolder sampleHolder;
|
||||
private final FormatHolder formatHolder;
|
||||
private final MediaFormatHolder formatHolder;
|
||||
private final HashSet<Long> decodeOnlyPresentationTimestamps;
|
||||
private final MediaCodec.BufferInfo outputBufferInfo;
|
||||
private final EventListener eventListener;
|
||||
@ -128,7 +144,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||
private int codecReconfigurationState;
|
||||
|
||||
private int trackIndex;
|
||||
private boolean sourceIsReady;
|
||||
private int sourceState;
|
||||
private boolean inputStreamEnded;
|
||||
private boolean outputStreamEnded;
|
||||
private boolean waitingForKeys;
|
||||
@ -158,7 +174,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||
this.eventListener = eventListener;
|
||||
codecCounters = new CodecCounters();
|
||||
sampleHolder = new SampleHolder(false);
|
||||
formatHolder = new FormatHolder();
|
||||
formatHolder = new MediaFormatHolder();
|
||||
decodeOnlyPresentationTimestamps = new HashSet<Long>();
|
||||
outputBufferInfo = new MediaCodec.BufferInfo();
|
||||
}
|
||||
@ -202,7 +218,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||
@Override
|
||||
protected void onEnabled(long timeUs, boolean joining) {
|
||||
source.enable(trackIndex, timeUs);
|
||||
sourceIsReady = false;
|
||||
sourceState = SOURCE_STATE_NOT_READY;
|
||||
inputStreamEnded = false;
|
||||
outputStreamEnded = false;
|
||||
waitingForKeys = false;
|
||||
@ -267,9 +283,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||
}
|
||||
codecHotswapTimeMs = getState() == TrackRenderer.STATE_STARTED ?
|
||||
SystemClock.elapsedRealtime() : -1;
|
||||
inputIndex = -1;
|
||||
outputIndex = -1;
|
||||
waitingForFirstSyncFrame = true;
|
||||
inputIndex = -1;
|
||||
outputIndex = -1;
|
||||
waitingForFirstSyncFrame = true;
|
||||
codecCounters.codecInitCount++;
|
||||
}
|
||||
|
||||
@ -353,7 +369,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||
protected void seekTo(long timeUs) throws ExoPlaybackException {
|
||||
currentPositionUs = timeUs;
|
||||
source.seekToUs(timeUs);
|
||||
sourceIsReady = false;
|
||||
sourceState = SOURCE_STATE_NOT_READY;
|
||||
inputStreamEnded = false;
|
||||
outputStreamEnded = false;
|
||||
waitingForKeys = false;
|
||||
@ -372,7 +388,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||
@Override
|
||||
protected void doSomeWork(long timeUs) throws ExoPlaybackException {
|
||||
try {
|
||||
sourceIsReady = source.continueBuffering(timeUs);
|
||||
sourceState = source.continueBuffering(timeUs)
|
||||
? (sourceState == SOURCE_STATE_NOT_READY ? SOURCE_STATE_READY : sourceState)
|
||||
: SOURCE_STATE_NOT_READY;
|
||||
checkForDiscontinuity();
|
||||
if (format == null) {
|
||||
readFormat();
|
||||
@ -384,7 +402,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||
}
|
||||
if (codec != null) {
|
||||
while (drainOutputBuffer(timeUs)) {}
|
||||
while (feedInputBuffer()) {}
|
||||
if (feedInputBuffer(true)) {
|
||||
while (feedInputBuffer(false)) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
codecCounters.ensureUpdated();
|
||||
@ -429,6 +449,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||
codecHotswapTimeMs = -1;
|
||||
inputIndex = -1;
|
||||
outputIndex = -1;
|
||||
waitingForFirstSyncFrame = true;
|
||||
decodeOnlyPresentationTimestamps.clear();
|
||||
// Workaround for framework bugs.
|
||||
// See [redacted], [redacted], [redacted].
|
||||
@ -446,11 +467,13 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param firstFeed True if this is the first call to this method from the current invocation of
|
||||
* {@link #doSomeWork(long)}. False otherwise.
|
||||
* @return True if it may be possible to feed more input data. False otherwise.
|
||||
* @throws IOException If an error occurs reading data from the upstream source.
|
||||
* @throws ExoPlaybackException If an error occurs feeding the input buffer.
|
||||
*/
|
||||
private boolean feedInputBuffer() throws IOException, ExoPlaybackException {
|
||||
private boolean feedInputBuffer(boolean firstFeed) throws IOException, ExoPlaybackException {
|
||||
if (inputStreamEnded) {
|
||||
return false;
|
||||
}
|
||||
@ -478,6 +501,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||
codecReconfigurationState = RECONFIGURATION_STATE_QUEUE_PENDING;
|
||||
}
|
||||
result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false);
|
||||
if (firstFeed && sourceState == SOURCE_STATE_READY && result == SampleSource.NOTHING_READ) {
|
||||
sourceState = SOURCE_STATE_READY_READ_MAY_FAIL;
|
||||
}
|
||||
}
|
||||
|
||||
if (result == SampleSource.NOTHING_READ) {
|
||||
@ -594,7 +620,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||
* @param formatHolder Holds the new format.
|
||||
* @throws ExoPlaybackException If an error occurs reinitializing the {@link MediaCodec}.
|
||||
*/
|
||||
private void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException {
|
||||
private void onInputFormatChanged(MediaFormatHolder formatHolder) throws ExoPlaybackException {
|
||||
MediaFormat oldFormat = format;
|
||||
format = formatHolder.format;
|
||||
drmInitData = formatHolder.drmInitData;
|
||||
@ -646,7 +672,17 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
||||
@Override
|
||||
protected boolean isReady() {
|
||||
return format != null && !waitingForKeys
|
||||
&& (sourceIsReady || outputIndex >= 0 || isWithinHotswapPeriod());
|
||||
&& sourceState != SOURCE_STATE_NOT_READY || outputIndex >= 0 || isWithinHotswapPeriod();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the source state.
|
||||
*
|
||||
* @return One of {@link #SOURCE_STATE_NOT_READY}, {@link #SOURCE_STATE_READY} and
|
||||
* {@link #SOURCE_STATE_READY_READ_MAY_FAIL}.
|
||||
*/
|
||||
protected final int getSourceState() {
|
||||
return sourceState;
|
||||
}
|
||||
|
||||
private boolean isWithinHotswapPeriod() {
|
||||
|
@ -235,7 +235,8 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
|
||||
|
||||
@Override
|
||||
protected boolean isReady() {
|
||||
if (super.isReady()) {
|
||||
if (super.isReady() && (renderedFirstFrame || !codecInitialized()
|
||||
|| getSourceState() == SOURCE_STATE_READY_READ_MAY_FAIL)) {
|
||||
// Ready. If we were joining then we've now joined, so clear the joining deadline.
|
||||
joiningDeadlineUs = -1;
|
||||
return true;
|
||||
|
@ -27,7 +27,7 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Encapsulates the information describing the format of media data, be it audio or video.
|
||||
* Defines the format of an elementary media stream.
|
||||
*/
|
||||
public class MediaFormat {
|
||||
|
||||
|
@ -21,7 +21,7 @@ import java.util.UUID;
|
||||
/**
|
||||
* Holds a {@link MediaFormat} and corresponding drm scheme initialization data.
|
||||
*/
|
||||
public final class FormatHolder {
|
||||
public final class MediaFormatHolder {
|
||||
|
||||
/**
|
||||
* The format of the media.
|
@ -80,7 +80,7 @@ public interface SampleSource {
|
||||
|
||||
/**
|
||||
* Enable the specified track. This allows the track's format and samples to be read from
|
||||
* {@link #readData(int, long, FormatHolder, SampleHolder, boolean)}.
|
||||
* {@link #readData(int, long, MediaFormatHolder, SampleHolder, boolean)}.
|
||||
* <p>
|
||||
* This method should not be called until after the source has been successfully prepared.
|
||||
*
|
||||
@ -119,7 +119,7 @@ public interface SampleSource {
|
||||
*
|
||||
* @param track The track from which to read.
|
||||
* @param playbackPositionUs The current playback position.
|
||||
* @param formatHolder A {@link FormatHolder} object to populate in the case of a new format.
|
||||
* @param formatHolder A {@link MediaFormatHolder} object to populate in the case of a new format.
|
||||
* @param sampleHolder A {@link SampleHolder} object to populate in the case of a new sample. If
|
||||
* the caller requires the sample data then it must ensure that {@link SampleHolder#data}
|
||||
* references a valid output buffer.
|
||||
@ -129,7 +129,7 @@ public interface SampleSource {
|
||||
* {@link #DISCONTINUITY_READ}, {@link #NOTHING_READ} or {@link #END_OF_STREAM}.
|
||||
* @throws IOException If an error occurred reading from the source.
|
||||
*/
|
||||
public int readData(int track, long playbackPositionUs, FormatHolder formatHolder,
|
||||
public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder,
|
||||
SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException;
|
||||
|
||||
/**
|
||||
|
@ -16,9 +16,9 @@
|
||||
package com.google.android.exoplayer.chunk;
|
||||
|
||||
import com.google.android.exoplayer.C;
|
||||
import com.google.android.exoplayer.FormatHolder;
|
||||
import com.google.android.exoplayer.LoadControl;
|
||||
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;
|
||||
@ -267,7 +267,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int readData(int track, long playbackPositionUs, FormatHolder formatHolder,
|
||||
public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder,
|
||||
SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException {
|
||||
Assertions.checkState(state == STATE_ENABLED);
|
||||
Assertions.checkState(track == 0);
|
||||
@ -318,6 +318,9 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
|
||||
}
|
||||
|
||||
if (!mediaChunk.prepare()) {
|
||||
if (currentLoadableException != null) {
|
||||
throw currentLoadableException;
|
||||
}
|
||||
return NOTHING_READ;
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ import com.google.android.exoplayer.util.Assertions;
|
||||
import java.util.Comparator;
|
||||
|
||||
/**
|
||||
* A format definition for streams.
|
||||
* Defines the high level format of a media stream.
|
||||
*/
|
||||
public class Format {
|
||||
|
||||
|
@ -18,7 +18,7 @@ package com.google.android.exoplayer.chunk;
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.ParserException;
|
||||
import com.google.android.exoplayer.SampleHolder;
|
||||
import com.google.android.exoplayer.parser.mp4.FragmentedMp4Extractor;
|
||||
import com.google.android.exoplayer.parser.Extractor;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.DataSpec;
|
||||
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
||||
@ -32,7 +32,7 @@ import java.util.UUID;
|
||||
*/
|
||||
public final class Mp4MediaChunk extends MediaChunk {
|
||||
|
||||
private final FragmentedMp4Extractor extractor;
|
||||
private final Extractor extractor;
|
||||
private final boolean maybeSelfContained;
|
||||
private final long sampleOffsetUs;
|
||||
|
||||
@ -57,7 +57,7 @@ public final class Mp4MediaChunk extends MediaChunk {
|
||||
*/
|
||||
public Mp4MediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
|
||||
int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex,
|
||||
FragmentedMp4Extractor extractor, boolean maybeSelfContained, long sampleOffsetUs) {
|
||||
Extractor extractor, boolean maybeSelfContained, long sampleOffsetUs) {
|
||||
super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex);
|
||||
this.extractor = extractor;
|
||||
this.maybeSelfContained = maybeSelfContained;
|
||||
@ -89,7 +89,7 @@ public final class Mp4MediaChunk extends MediaChunk {
|
||||
NonBlockingInputStream inputStream = getNonBlockingInputStream();
|
||||
Assertions.checkState(inputStream != null);
|
||||
int result = extractor.read(inputStream, null);
|
||||
prepared = (result & FragmentedMp4Extractor.RESULT_NEED_SAMPLE_HOLDER) != 0;
|
||||
prepared = (result & Extractor.RESULT_NEED_SAMPLE_HOLDER) != 0;
|
||||
} else {
|
||||
// We know there isn't a moov atom. The extractor must have parsed one from a separate
|
||||
// initialization chunk.
|
||||
@ -107,7 +107,7 @@ public final class Mp4MediaChunk extends MediaChunk {
|
||||
public boolean sampleAvailable() throws ParserException {
|
||||
NonBlockingInputStream inputStream = getNonBlockingInputStream();
|
||||
int result = extractor.read(inputStream, null);
|
||||
return (result & FragmentedMp4Extractor.RESULT_NEED_SAMPLE_HOLDER) != 0;
|
||||
return (result & Extractor.RESULT_NEED_SAMPLE_HOLDER) != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -115,7 +115,7 @@ public final class Mp4MediaChunk extends MediaChunk {
|
||||
NonBlockingInputStream inputStream = getNonBlockingInputStream();
|
||||
Assertions.checkState(inputStream != null);
|
||||
int result = extractor.read(inputStream, holder);
|
||||
boolean sampleRead = (result & FragmentedMp4Extractor.RESULT_READ_SAMPLE) != 0;
|
||||
boolean sampleRead = (result & Extractor.RESULT_READ_SAMPLE) != 0;
|
||||
if (sampleRead) {
|
||||
holder.timeUs -= sampleOffsetUs;
|
||||
}
|
||||
|
@ -1,99 +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.chunk;
|
||||
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.ParserException;
|
||||
import com.google.android.exoplayer.SampleHolder;
|
||||
import com.google.android.exoplayer.parser.webm.WebmExtractor;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.DataSpec;
|
||||
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* A WebM {@link MediaChunk}.
|
||||
*/
|
||||
public final class WebmMediaChunk extends MediaChunk {
|
||||
|
||||
private final WebmExtractor extractor;
|
||||
|
||||
/**
|
||||
* @param dataSource A {@link DataSource} for loading the data.
|
||||
* @param dataSpec Defines the data to be loaded.
|
||||
* @param format The format of the stream to which this chunk belongs.
|
||||
* @param extractor The extractor that will be used to extract the samples.
|
||||
* @param trigger The reason for this chunk being selected.
|
||||
* @param startTimeUs The start time of the media contained by the chunk, in microseconds.
|
||||
* @param endTimeUs The end time of the media contained by the chunk, in microseconds.
|
||||
* @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
|
||||
*/
|
||||
public WebmMediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
|
||||
int trigger, WebmExtractor extractor, long startTimeUs, long endTimeUs,
|
||||
int nextChunkIndex) {
|
||||
super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex);
|
||||
this.extractor = extractor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekToStart() {
|
||||
seekTo(0, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean seekTo(long positionUs, boolean allowNoop) {
|
||||
boolean isDiscontinuous = extractor.seekTo(positionUs, allowNoop);
|
||||
if (isDiscontinuous) {
|
||||
resetReadPosition();
|
||||
}
|
||||
return isDiscontinuous;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean prepare() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean sampleAvailable() throws ParserException {
|
||||
NonBlockingInputStream inputStream = getNonBlockingInputStream();
|
||||
int result = extractor.read(inputStream, null);
|
||||
return (result & WebmExtractor.RESULT_NEED_SAMPLE_HOLDER) != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean read(SampleHolder holder) {
|
||||
NonBlockingInputStream inputStream = getNonBlockingInputStream();
|
||||
Assertions.checkState(inputStream != null);
|
||||
int result = extractor.read(inputStream, holder);
|
||||
return (result & WebmExtractor.RESULT_READ_SAMPLE) != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaFormat getMediaFormat() {
|
||||
return extractor.getFormat();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<UUID, byte[]> getPsshInfo() {
|
||||
// TODO: Add support for Pssh to WebmExtractor
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
@ -29,10 +29,13 @@ import com.google.android.exoplayer.chunk.MediaChunk;
|
||||
import com.google.android.exoplayer.chunk.Mp4MediaChunk;
|
||||
import com.google.android.exoplayer.dash.mpd.RangedUri;
|
||||
import com.google.android.exoplayer.dash.mpd.Representation;
|
||||
import com.google.android.exoplayer.parser.Extractor;
|
||||
import com.google.android.exoplayer.parser.mp4.FragmentedMp4Extractor;
|
||||
import com.google.android.exoplayer.parser.webm.WebmExtractor;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.DataSpec;
|
||||
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
||||
import com.google.android.exoplayer.util.MimeTypes;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
@ -42,9 +45,11 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An {@link ChunkSource} for Mp4 DASH streams.
|
||||
* An {@link ChunkSource} for DASH streams.
|
||||
* <p>
|
||||
* This implementation currently supports fMP4 and webm.
|
||||
*/
|
||||
public class DashMp4ChunkSource implements ChunkSource {
|
||||
public class DashChunkSource implements ChunkSource {
|
||||
|
||||
private final TrackInfo trackInfo;
|
||||
private final DataSource dataSource;
|
||||
@ -55,7 +60,7 @@ public class DashMp4ChunkSource implements ChunkSource {
|
||||
|
||||
private final Format[] formats;
|
||||
private final HashMap<String, Representation> representations;
|
||||
private final HashMap<String, FragmentedMp4Extractor> extractors;
|
||||
private final HashMap<String, Extractor> extractors;
|
||||
private final HashMap<String, DashSegmentIndex> segmentIndexes;
|
||||
|
||||
private boolean lastChunkWasInitialization;
|
||||
@ -65,12 +70,12 @@ public class DashMp4ChunkSource implements ChunkSource {
|
||||
* @param evaluator Selects from the available formats.
|
||||
* @param representations The representations to be considered by the source.
|
||||
*/
|
||||
public DashMp4ChunkSource(DataSource dataSource, FormatEvaluator evaluator,
|
||||
public DashChunkSource(DataSource dataSource, FormatEvaluator evaluator,
|
||||
Representation... representations) {
|
||||
this.dataSource = dataSource;
|
||||
this.evaluator = evaluator;
|
||||
this.formats = new Format[representations.length];
|
||||
this.extractors = new HashMap<String, FragmentedMp4Extractor>();
|
||||
this.extractors = new HashMap<String, Extractor>();
|
||||
this.segmentIndexes = new HashMap<String, DashSegmentIndex>();
|
||||
this.representations = new HashMap<String, Representation>();
|
||||
this.trackInfo = new TrackInfo(representations[0].format.mimeType,
|
||||
@ -82,7 +87,9 @@ public class DashMp4ChunkSource implements ChunkSource {
|
||||
formats[i] = representations[i].format;
|
||||
maxWidth = Math.max(formats[i].width, maxWidth);
|
||||
maxHeight = Math.max(formats[i].height, maxHeight);
|
||||
extractors.put(formats[i].id, new FragmentedMp4Extractor());
|
||||
Extractor extractor = formats[i].mimeType.startsWith(MimeTypes.VIDEO_WEBM)
|
||||
? new WebmExtractor() : new FragmentedMp4Extractor();
|
||||
extractors.put(formats[i].id, extractor);
|
||||
this.representations.put(formats[i].id, representations[i]);
|
||||
DashSegmentIndex segmentIndex = representations[i].getIndex();
|
||||
if (segmentIndex != null) {
|
||||
@ -142,7 +149,7 @@ public class DashMp4ChunkSource implements ChunkSource {
|
||||
}
|
||||
|
||||
Representation selectedRepresentation = representations.get(selectedFormat.id);
|
||||
FragmentedMp4Extractor extractor = extractors.get(selectedRepresentation.format.id);
|
||||
Extractor extractor = extractors.get(selectedRepresentation.format.id);
|
||||
|
||||
RangedUri pendingInitializationUri = null;
|
||||
RangedUri pendingIndexUri = null;
|
||||
@ -191,35 +198,39 @@ public class DashMp4ChunkSource implements ChunkSource {
|
||||
}
|
||||
|
||||
private Chunk newInitializationChunk(RangedUri initializationUri, RangedUri indexUri,
|
||||
Representation representation, FragmentedMp4Extractor extractor, DataSource dataSource,
|
||||
Representation representation, Extractor extractor, DataSource dataSource,
|
||||
int trigger) {
|
||||
int expectedExtractorResult = FragmentedMp4Extractor.RESULT_END_OF_STREAM;
|
||||
int expectedExtractorResult = Extractor.RESULT_END_OF_STREAM;
|
||||
long indexAnchor = 0;
|
||||
RangedUri requestUri;
|
||||
if (initializationUri != null) {
|
||||
// It's common for initialization and index data to be stored adjacently. Attempt to merge
|
||||
// the two requests together to request both at once.
|
||||
expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_INIT;
|
||||
expectedExtractorResult |= Extractor.RESULT_READ_INIT;
|
||||
requestUri = initializationUri.attemptMerge(indexUri);
|
||||
if (requestUri != null) {
|
||||
expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_INDEX;
|
||||
indexAnchor = indexUri.start + indexUri.length;
|
||||
expectedExtractorResult |= Extractor.RESULT_READ_INDEX;
|
||||
if (extractor.hasRelativeIndexOffsets()) {
|
||||
indexAnchor = indexUri.start + indexUri.length;
|
||||
}
|
||||
} else {
|
||||
requestUri = initializationUri;
|
||||
}
|
||||
} else {
|
||||
requestUri = indexUri;
|
||||
indexAnchor = indexUri.start + indexUri.length;
|
||||
expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_INDEX;
|
||||
if (extractor.hasRelativeIndexOffsets()) {
|
||||
indexAnchor = indexUri.start + indexUri.length;
|
||||
}
|
||||
expectedExtractorResult |= Extractor.RESULT_READ_INDEX;
|
||||
}
|
||||
DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length,
|
||||
representation.getCacheKey());
|
||||
return new InitializationMp4Loadable(dataSource, dataSpec, trigger, representation.format,
|
||||
return new InitializationLoadable(dataSource, dataSpec, trigger, representation.format,
|
||||
extractor, expectedExtractorResult, indexAnchor);
|
||||
}
|
||||
|
||||
private Chunk newMediaChunk(Representation representation, DashSegmentIndex segmentIndex,
|
||||
FragmentedMp4Extractor extractor, DataSource dataSource, int segmentNum, int trigger) {
|
||||
Extractor extractor, DataSource dataSource, int segmentNum, int trigger) {
|
||||
int lastSegmentNum = segmentIndex.getLastSegmentNum();
|
||||
int nextSegmentNum = segmentNum == lastSegmentNum ? -1 : segmentNum + 1;
|
||||
long startTimeUs = segmentIndex.getTimeUs(segmentNum);
|
||||
@ -232,15 +243,15 @@ public class DashMp4ChunkSource implements ChunkSource {
|
||||
endTimeUs, nextSegmentNum, extractor, false, 0);
|
||||
}
|
||||
|
||||
private class InitializationMp4Loadable extends Chunk {
|
||||
private class InitializationLoadable extends Chunk {
|
||||
|
||||
private final FragmentedMp4Extractor extractor;
|
||||
private final Extractor extractor;
|
||||
private final int expectedExtractorResult;
|
||||
private final long indexAnchor;
|
||||
private final Uri uri;
|
||||
|
||||
public InitializationMp4Loadable(DataSource dataSource, DataSpec dataSpec, int trigger,
|
||||
Format format, FragmentedMp4Extractor extractor, int expectedExtractorResult,
|
||||
public InitializationLoadable(DataSource dataSource, DataSpec dataSpec, int trigger,
|
||||
Format format, Extractor extractor, int expectedExtractorResult,
|
||||
long indexAnchor) {
|
||||
super(dataSource, dataSpec, format, trigger);
|
||||
this.extractor = extractor;
|
||||
@ -256,7 +267,7 @@ public class DashMp4ChunkSource implements ChunkSource {
|
||||
throw new ParserException("Invalid extractor result. Expected "
|
||||
+ expectedExtractorResult + ", got " + result);
|
||||
}
|
||||
if ((result & FragmentedMp4Extractor.RESULT_READ_INDEX) != 0) {
|
||||
if ((result & Extractor.RESULT_READ_INDEX) != 0) {
|
||||
segmentIndexes.put(format.id,
|
||||
new DashWrappingSegmentIndex(extractor.getIndex(), uri, indexAnchor));
|
||||
}
|
@ -1,261 +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.dash;
|
||||
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.ParserException;
|
||||
import com.google.android.exoplayer.TrackInfo;
|
||||
import com.google.android.exoplayer.chunk.Chunk;
|
||||
import com.google.android.exoplayer.chunk.ChunkOperationHolder;
|
||||
import com.google.android.exoplayer.chunk.ChunkSource;
|
||||
import com.google.android.exoplayer.chunk.Format;
|
||||
import com.google.android.exoplayer.chunk.Format.DecreasingBandwidthComparator;
|
||||
import com.google.android.exoplayer.chunk.FormatEvaluator;
|
||||
import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation;
|
||||
import com.google.android.exoplayer.chunk.MediaChunk;
|
||||
import com.google.android.exoplayer.chunk.WebmMediaChunk;
|
||||
import com.google.android.exoplayer.dash.mpd.RangedUri;
|
||||
import com.google.android.exoplayer.dash.mpd.Representation;
|
||||
import com.google.android.exoplayer.parser.webm.DefaultWebmExtractor;
|
||||
import com.google.android.exoplayer.parser.webm.WebmExtractor;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.DataSpec;
|
||||
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An {@link ChunkSource} for WebM DASH streams.
|
||||
*/
|
||||
public class DashWebmChunkSource implements ChunkSource {
|
||||
|
||||
private final TrackInfo trackInfo;
|
||||
private final DataSource dataSource;
|
||||
private final FormatEvaluator evaluator;
|
||||
private final Evaluation evaluation;
|
||||
private final int maxWidth;
|
||||
private final int maxHeight;
|
||||
|
||||
private final Format[] formats;
|
||||
private final HashMap<String, Representation> representations;
|
||||
private final HashMap<String, DefaultWebmExtractor> extractors;
|
||||
private final HashMap<String, DashSegmentIndex> segmentIndexes;
|
||||
|
||||
private boolean lastChunkWasInitialization;
|
||||
|
||||
/**
|
||||
* @param dataSource A {@link DataSource} suitable for loading the media data.
|
||||
* @param evaluator Selects from the available formats.
|
||||
* @param representations The representations to be considered by the source.
|
||||
*/
|
||||
public DashWebmChunkSource(DataSource dataSource, FormatEvaluator evaluator,
|
||||
Representation... representations) {
|
||||
this.dataSource = dataSource;
|
||||
this.evaluator = evaluator;
|
||||
this.formats = new Format[representations.length];
|
||||
this.extractors = new HashMap<String, DefaultWebmExtractor>();
|
||||
this.segmentIndexes = new HashMap<String, DashSegmentIndex>();
|
||||
this.representations = new HashMap<String, Representation>();
|
||||
this.trackInfo = new TrackInfo(representations[0].format.mimeType,
|
||||
representations[0].periodDurationMs * 1000);
|
||||
this.evaluation = new Evaluation();
|
||||
int maxWidth = 0;
|
||||
int maxHeight = 0;
|
||||
for (int i = 0; i < representations.length; i++) {
|
||||
formats[i] = representations[i].format;
|
||||
maxWidth = Math.max(formats[i].width, maxWidth);
|
||||
maxHeight = Math.max(formats[i].height, maxHeight);
|
||||
extractors.put(formats[i].id, new DefaultWebmExtractor());
|
||||
this.representations.put(formats[i].id, representations[i]);
|
||||
DashSegmentIndex segmentIndex = representations[i].getIndex();
|
||||
if (segmentIndex != null) {
|
||||
segmentIndexes.put(formats[i].id, segmentIndex);
|
||||
}
|
||||
}
|
||||
this.maxWidth = maxWidth;
|
||||
this.maxHeight = maxHeight;
|
||||
Arrays.sort(formats, new DecreasingBandwidthComparator());
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void getMaxVideoDimensions(MediaFormat out) {
|
||||
if (trackInfo.mimeType.startsWith("video")) {
|
||||
out.setMaxVideoDimensions(maxWidth, maxHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final TrackInfo getTrackInfo() {
|
||||
return trackInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable() {
|
||||
evaluator.enable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disable(List<? extends MediaChunk> queue) {
|
||||
evaluator.disable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void continueBuffering(long playbackPositionUs) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void getChunkOperation(List<? extends MediaChunk> queue, long seekPositionUs,
|
||||
long playbackPositionUs, ChunkOperationHolder out) {
|
||||
evaluation.queueSize = queue.size();
|
||||
if (evaluation.format == null || !lastChunkWasInitialization) {
|
||||
evaluator.evaluate(queue, playbackPositionUs, formats, evaluation);
|
||||
}
|
||||
Format selectedFormat = evaluation.format;
|
||||
out.queueSize = evaluation.queueSize;
|
||||
|
||||
if (selectedFormat == null) {
|
||||
out.chunk = null;
|
||||
return;
|
||||
} else if (out.queueSize == queue.size() && out.chunk != null
|
||||
&& out.chunk.format.id.equals(selectedFormat.id)) {
|
||||
// We already have a chunk, and the evaluation hasn't changed either the format or the size
|
||||
// of the queue. Leave unchanged.
|
||||
return;
|
||||
}
|
||||
|
||||
Representation selectedRepresentation = representations.get(selectedFormat.id);
|
||||
WebmExtractor extractor = extractors.get(selectedRepresentation.format.id);
|
||||
|
||||
RangedUri pendingInitializationUri = null;
|
||||
RangedUri pendingIndexUri = null;
|
||||
if (extractor.getFormat() == null) {
|
||||
pendingInitializationUri = selectedRepresentation.getInitializationUri();
|
||||
}
|
||||
if (!segmentIndexes.containsKey(selectedRepresentation.format.id)) {
|
||||
pendingIndexUri = selectedRepresentation.getIndexUri();
|
||||
}
|
||||
if (pendingInitializationUri != null || pendingIndexUri != null) {
|
||||
// We have initialization and/or index requests to make.
|
||||
Chunk initializationChunk = newInitializationChunk(pendingInitializationUri, pendingIndexUri,
|
||||
selectedRepresentation, extractor, dataSource, evaluation.trigger);
|
||||
lastChunkWasInitialization = true;
|
||||
out.chunk = initializationChunk;
|
||||
return;
|
||||
}
|
||||
|
||||
int nextSegmentNum;
|
||||
DashSegmentIndex segmentIndex = segmentIndexes.get(selectedRepresentation.format.id);
|
||||
if (queue.isEmpty()) {
|
||||
nextSegmentNum = segmentIndex.getSegmentNum(seekPositionUs);
|
||||
} else {
|
||||
nextSegmentNum = queue.get(out.queueSize - 1).nextChunkIndex;
|
||||
}
|
||||
|
||||
if (nextSegmentNum == -1) {
|
||||
out.chunk = null;
|
||||
return;
|
||||
}
|
||||
|
||||
Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, segmentIndex, extractor,
|
||||
dataSource, nextSegmentNum, evaluation.trigger);
|
||||
lastChunkWasInitialization = false;
|
||||
out.chunk = nextMediaChunk;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IOException getError() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChunkLoadError(Chunk chunk, Exception e) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
private Chunk newInitializationChunk(RangedUri initializationUri, RangedUri indexUri,
|
||||
Representation representation, WebmExtractor extractor, DataSource dataSource,
|
||||
int trigger) {
|
||||
int expectedExtractorResult = WebmExtractor.RESULT_END_OF_STREAM;
|
||||
RangedUri requestUri;
|
||||
if (initializationUri != null) {
|
||||
// It's common for initialization and index data to be stored adjacently. Attempt to merge
|
||||
// the two requests together to request both at once.
|
||||
expectedExtractorResult |= WebmExtractor.RESULT_READ_INIT;
|
||||
requestUri = initializationUri.attemptMerge(indexUri);
|
||||
if (requestUri != null) {
|
||||
expectedExtractorResult |= WebmExtractor.RESULT_READ_INDEX;
|
||||
} else {
|
||||
requestUri = initializationUri;
|
||||
}
|
||||
} else {
|
||||
requestUri = indexUri;
|
||||
expectedExtractorResult |= WebmExtractor.RESULT_READ_INDEX;
|
||||
}
|
||||
DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length,
|
||||
representation.getCacheKey());
|
||||
return new InitializationWebmLoadable(dataSource, dataSpec, trigger, representation.format,
|
||||
extractor, expectedExtractorResult);
|
||||
}
|
||||
|
||||
private Chunk newMediaChunk(Representation representation, DashSegmentIndex segmentIndex,
|
||||
WebmExtractor extractor, DataSource dataSource, int segmentNum, int trigger) {
|
||||
int lastSegmentNum = segmentIndex.getLastSegmentNum();
|
||||
int nextSegmentNum = segmentNum == lastSegmentNum ? -1 : segmentNum + 1;
|
||||
long startTimeUs = segmentIndex.getTimeUs(segmentNum);
|
||||
long endTimeUs = segmentNum < lastSegmentNum ? segmentIndex.getTimeUs(segmentNum + 1)
|
||||
: startTimeUs + segmentIndex.getDurationUs(segmentNum);
|
||||
RangedUri segmentUri = segmentIndex.getSegmentUrl(segmentNum);
|
||||
DataSpec dataSpec = new DataSpec(segmentUri.getUri(), segmentUri.start, segmentUri.length,
|
||||
representation.getCacheKey());
|
||||
return new WebmMediaChunk(dataSource, dataSpec, representation.format, trigger, extractor,
|
||||
startTimeUs, endTimeUs, nextSegmentNum);
|
||||
}
|
||||
|
||||
private class InitializationWebmLoadable extends Chunk {
|
||||
|
||||
private final WebmExtractor extractor;
|
||||
private final int expectedExtractorResult;
|
||||
private final Uri uri;
|
||||
|
||||
public InitializationWebmLoadable(DataSource dataSource, DataSpec dataSpec, int trigger,
|
||||
Format format, WebmExtractor extractor, int expectedExtractorResult) {
|
||||
super(dataSource, dataSpec, format, trigger);
|
||||
this.extractor = extractor;
|
||||
this.expectedExtractorResult = expectedExtractorResult;
|
||||
this.uri = dataSpec.uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void consumeStream(NonBlockingInputStream stream) throws IOException {
|
||||
int result = extractor.read(stream, null);
|
||||
if (result != expectedExtractorResult) {
|
||||
throw new ParserException("Invalid extractor result. Expected "
|
||||
+ expectedExtractorResult + ", got " + result);
|
||||
}
|
||||
if ((result & WebmExtractor.RESULT_READ_INDEX) != 0) {
|
||||
segmentIndexes.put(format.id, new DashWrappingSegmentIndex(extractor.getIndex(), uri, 0));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -23,6 +23,8 @@ import java.util.List;
|
||||
*/
|
||||
public final class MediaPresentationDescription {
|
||||
|
||||
public final long availabilityStartTime;
|
||||
|
||||
public final long duration;
|
||||
|
||||
public final long minBufferTime;
|
||||
@ -31,14 +33,22 @@ public final class MediaPresentationDescription {
|
||||
|
||||
public final long minUpdatePeriod;
|
||||
|
||||
public final long timeShiftBufferDepth;
|
||||
|
||||
public final List<Period> periods;
|
||||
|
||||
public MediaPresentationDescription(long duration, long minBufferTime, boolean dynamic,
|
||||
long minUpdatePeriod, List<Period> periods) {
|
||||
public final UtcTimingElement utcTiming;
|
||||
|
||||
public MediaPresentationDescription(long availabilityStartTime, long duration, long minBufferTime,
|
||||
boolean dynamic, long minUpdatePeriod, long timeShiftBufferDepth, UtcTimingElement utcTiming,
|
||||
List<Period> periods) {
|
||||
this.availabilityStartTime = availabilityStartTime;
|
||||
this.duration = duration;
|
||||
this.minBufferTime = minBufferTime;
|
||||
this.dynamic = dynamic;
|
||||
this.minUpdatePeriod = minUpdatePeriod;
|
||||
this.timeShiftBufferDepth = timeShiftBufferDepth;
|
||||
this.utcTiming = utcTiming;
|
||||
this.periods = Collections.unmodifiableList(periods);
|
||||
}
|
||||
|
||||
|
@ -20,8 +20,6 @@ import com.google.android.exoplayer.util.ManifestFetcher;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
@ -60,11 +58,7 @@ public final class MediaPresentationDescriptionFetcher extends
|
||||
@Override
|
||||
protected MediaPresentationDescription parse(InputStream stream, String inputEncoding,
|
||||
String contentId, Uri baseUrl) throws IOException, ParserException {
|
||||
try {
|
||||
return parser.parseMediaPresentationDescription(stream, inputEncoding, contentId, baseUrl);
|
||||
} catch (XmlPullParserException e) {
|
||||
throw new ParserException(e);
|
||||
}
|
||||
return parser.parseMediaPresentationDescription(stream, inputEncoding, contentId, baseUrl);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -34,8 +34,13 @@ import org.xmlpull.v1.XmlPullParserFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.text.ParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.List;
|
||||
import java.util.TimeZone;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@ -48,6 +53,11 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
|
||||
private static final Pattern DURATION =
|
||||
Pattern.compile("^PT(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?$");
|
||||
|
||||
private static final Pattern DATE_TIME_PATTERN =
|
||||
Pattern.compile("(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)[Tt]"
|
||||
+ "(\\d\\d):(\\d\\d):(\\d\\d)(\\.(\\d+))?"
|
||||
+ "([Zz]|((\\+|\\-)(\\d\\d):(\\d\\d)))?");
|
||||
|
||||
private final XmlPullParserFactory xmlParserFactory;
|
||||
|
||||
public MediaPresentationDescriptionParser() {
|
||||
@ -69,42 +79,57 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
|
||||
* @param baseUrl The url that any relative urls defined within the manifest are relative to.
|
||||
* @return The parsed manifest.
|
||||
* @throws IOException If a problem occurred reading from the stream.
|
||||
* @throws XmlPullParserException If a problem occurred parsing the stream as xml.
|
||||
* @throws ParserException If a problem occurred parsing the xml as a DASH mpd.
|
||||
*/
|
||||
public MediaPresentationDescription parseMediaPresentationDescription(InputStream inputStream,
|
||||
String inputEncoding, String contentId, Uri baseUrl) throws XmlPullParserException,
|
||||
IOException, ParserException {
|
||||
XmlPullParser xpp = xmlParserFactory.newPullParser();
|
||||
xpp.setInput(inputStream, inputEncoding);
|
||||
int eventType = xpp.next();
|
||||
if (eventType != XmlPullParser.START_TAG || !"MPD".equals(xpp.getName())) {
|
||||
throw new ParserException(
|
||||
"inputStream does not contain a valid media presentation description");
|
||||
String inputEncoding, String contentId, Uri baseUrl) throws IOException, ParserException {
|
||||
try {
|
||||
XmlPullParser xpp = xmlParserFactory.newPullParser();
|
||||
xpp.setInput(inputStream, inputEncoding);
|
||||
int eventType = xpp.next();
|
||||
if (eventType != XmlPullParser.START_TAG || !"MPD".equals(xpp.getName())) {
|
||||
throw new ParserException(
|
||||
"inputStream does not contain a valid media presentation description");
|
||||
}
|
||||
return parseMediaPresentationDescription(xpp, contentId, baseUrl);
|
||||
} catch (XmlPullParserException e) {
|
||||
throw new ParserException(e);
|
||||
} catch (ParseException e) {
|
||||
throw new ParserException(e);
|
||||
}
|
||||
return parseMediaPresentationDescription(xpp, contentId, baseUrl);
|
||||
}
|
||||
|
||||
private MediaPresentationDescription parseMediaPresentationDescription(XmlPullParser xpp,
|
||||
String contentId, Uri baseUrl) throws XmlPullParserException, IOException {
|
||||
String contentId, Uri baseUrl) throws XmlPullParserException, IOException, ParseException {
|
||||
long availabilityStartTime = parseDateTime(xpp, "availabilityStartTime", -1);
|
||||
long durationMs = parseDurationMs(xpp, "mediaPresentationDuration");
|
||||
long minBufferTimeMs = parseDurationMs(xpp, "minBufferTime");
|
||||
String typeString = xpp.getAttributeValue(null, "type");
|
||||
boolean dynamic = (typeString != null) ? typeString.equals("dynamic") : false;
|
||||
long minUpdateTimeMs = (dynamic) ? parseDurationMs(xpp, "minimumUpdatePeriod", -1) : -1;
|
||||
long timeShiftBufferDepthMs = (dynamic) ? parseDurationMs(xpp, "timeShiftBufferDepth", -1) : -1;
|
||||
UtcTimingElement utcTiming = null;
|
||||
|
||||
List<Period> periods = new ArrayList<Period>();
|
||||
do {
|
||||
xpp.next();
|
||||
if (isStartTag(xpp, "BaseURL")) {
|
||||
baseUrl = parseBaseUrl(xpp, baseUrl);
|
||||
} else if (isStartTag(xpp, "UTCTiming")) {
|
||||
utcTiming = parseUtcTiming(xpp);
|
||||
} else if (isStartTag(xpp, "Period")) {
|
||||
periods.add(parsePeriod(xpp, contentId, baseUrl, durationMs));
|
||||
}
|
||||
} while (!isEndTag(xpp, "MPD"));
|
||||
|
||||
return new MediaPresentationDescription(durationMs, minBufferTimeMs, dynamic, minUpdateTimeMs,
|
||||
periods);
|
||||
return new MediaPresentationDescription(availabilityStartTime, durationMs, minBufferTimeMs,
|
||||
dynamic, minUpdateTimeMs, timeShiftBufferDepthMs, utcTiming, periods);
|
||||
}
|
||||
|
||||
private UtcTimingElement parseUtcTiming(XmlPullParser xpp) {
|
||||
String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri");
|
||||
String value = xpp.getAttributeValue(null, "value");
|
||||
return new UtcTimingElement(schemeIdUri, value);
|
||||
}
|
||||
|
||||
private Period parsePeriod(XmlPullParser xpp, String contentId, Uri baseUrl, long mpdDurationMs)
|
||||
@ -429,6 +454,62 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
|
||||
return parseDurationMs(xpp, name, -1);
|
||||
}
|
||||
|
||||
private static long parseDateTime(XmlPullParser xpp, String name, long defaultValue)
|
||||
throws ParseException {
|
||||
String value = xpp.getAttributeValue(null, name);
|
||||
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
} else {
|
||||
return parseDateTime(value);
|
||||
}
|
||||
}
|
||||
|
||||
// VisibleForTesting
|
||||
static long parseDateTime(String value) throws ParseException {
|
||||
Matcher matcher = DATE_TIME_PATTERN.matcher(value);
|
||||
if (!matcher.matches()) {
|
||||
throw new ParseException("Invalid date/time format: " + value, 0);
|
||||
}
|
||||
|
||||
int timezoneShift;
|
||||
if (matcher.group(9) == null) {
|
||||
// No time zone specified.
|
||||
timezoneShift = 0;
|
||||
} else if (matcher.group(9).equalsIgnoreCase("Z")) {
|
||||
timezoneShift = 0;
|
||||
} else {
|
||||
timezoneShift = ((Integer.valueOf(matcher.group(12)) * 60
|
||||
+ Integer.valueOf(matcher.group(13))));
|
||||
if (matcher.group(11).equals("-")) {
|
||||
timezoneShift *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
Calendar dateTime = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
|
||||
|
||||
dateTime.clear();
|
||||
// Note: The month value is 0-based, hence the -1 on group(2)
|
||||
dateTime.set(Integer.valueOf(matcher.group(1)),
|
||||
Integer.valueOf(matcher.group(2)) - 1,
|
||||
Integer.valueOf(matcher.group(3)),
|
||||
Integer.valueOf(matcher.group(4)),
|
||||
Integer.valueOf(matcher.group(5)),
|
||||
Integer.valueOf(matcher.group(6)));
|
||||
if (!TextUtils.isEmpty(matcher.group(8))) {
|
||||
final BigDecimal bd = new BigDecimal("0." + matcher.group(8));
|
||||
// we care only for milliseconds, so movePointRight(3)
|
||||
dateTime.set(Calendar.MILLISECOND, bd.movePointRight(3).intValue());
|
||||
}
|
||||
|
||||
long time = dateTime.getTimeInMillis();
|
||||
if (timezoneShift != 0) {
|
||||
time -= timezoneShift * 60000;
|
||||
}
|
||||
|
||||
return time;
|
||||
}
|
||||
|
||||
private static long parseDurationMs(XmlPullParser xpp, String name, long defaultValue) {
|
||||
String value = xpp.getAttributeValue(null, name);
|
||||
if (value != null) {
|
||||
|
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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.dash.mpd;
|
||||
|
||||
/**
|
||||
* Represents a UTCTiming element.
|
||||
*/
|
||||
public class UtcTimingElement {
|
||||
|
||||
public final String schemeIdUri;
|
||||
public final String value;
|
||||
|
||||
public UtcTimingElement(String schemeIdUri, String value) {
|
||||
this.schemeIdUri = schemeIdUri;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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.parser;
|
||||
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.ParserException;
|
||||
import com.google.android.exoplayer.SampleHolder;
|
||||
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Facilitates extraction of media samples from a container format.
|
||||
*/
|
||||
public interface Extractor {
|
||||
|
||||
/**
|
||||
* An attempt to read from the input stream returned insufficient data.
|
||||
*/
|
||||
public static final int RESULT_NEED_MORE_DATA = 1;
|
||||
/**
|
||||
* The end of the input stream was reached.
|
||||
*/
|
||||
public static final int RESULT_END_OF_STREAM = 2;
|
||||
/**
|
||||
* A media sample was read.
|
||||
*/
|
||||
public static final int RESULT_READ_SAMPLE = 4;
|
||||
/**
|
||||
* Initialization data was read. The parsed data can be read using {@link #getFormat()} and
|
||||
* {@link #getPsshInfo}.
|
||||
*/
|
||||
public static final int RESULT_READ_INIT = 8;
|
||||
/**
|
||||
* A sidx atom was read. The parsed data can be read using {@link #getIndex()}.
|
||||
*/
|
||||
public static final int RESULT_READ_INDEX = 16;
|
||||
/**
|
||||
* The next thing to be read is a sample, but a {@link SampleHolder} was not supplied.
|
||||
*/
|
||||
public static final int RESULT_NEED_SAMPLE_HOLDER = 32;
|
||||
|
||||
/**
|
||||
* Returns the segment index parsed from the stream.
|
||||
*
|
||||
* @return The segment index, or null if a SIDX atom has yet to be parsed.
|
||||
*/
|
||||
public SegmentIndex getIndex();
|
||||
|
||||
/**
|
||||
* Returns true if the offsets in the index returned by {@link #getIndex()} are relative to the
|
||||
* first byte following the initialization data, or false if they are absolute (i.e. relative to
|
||||
* the first byte of the stream).
|
||||
*
|
||||
* @return True if the offsets are relative to the first byte following the initialization data.
|
||||
* False otherwise.
|
||||
*/
|
||||
public boolean hasRelativeIndexOffsets();
|
||||
|
||||
/**
|
||||
* Returns the format of the samples contained within the media stream.
|
||||
*
|
||||
* @return The sample media format, or null if the format has yet to be parsed.
|
||||
*/
|
||||
public MediaFormat getFormat();
|
||||
|
||||
/**
|
||||
* Returns the pssh information parsed from the stream.
|
||||
*
|
||||
* @return The pssh information. May be null if pssh data has yet to be parsed, or if the stream
|
||||
* does not contain any pssh data.
|
||||
*/
|
||||
public Map<UUID, byte[]> getPsshInfo();
|
||||
|
||||
/**
|
||||
* Consumes data from a {@link NonBlockingInputStream}.
|
||||
* <p>
|
||||
* The read terminates if the end of the input stream is reached, if an attempt to read from the
|
||||
* input stream returned 0 bytes of data, or if a sample is read. The returned flags indicate
|
||||
* both the reason for termination and data that was parsed during the read.
|
||||
*
|
||||
* @param inputStream The input stream from which data should be read.
|
||||
* @param out A {@link SampleHolder} into which the next sample should be read. If null then
|
||||
* {@link #RESULT_NEED_SAMPLE_HOLDER} will be returned once a sample has been reached.
|
||||
* @return One or more of the {@code RESULT_*} flags defined in this class.
|
||||
* @throws ParserException If an error occurs parsing the media data.
|
||||
*/
|
||||
public int read(NonBlockingInputStream inputStream, SampleHolder out) throws ParserException;
|
||||
|
||||
/**
|
||||
* Seeks to a position before or equal to the requested time.
|
||||
*
|
||||
* @param seekTimeUs The desired seek time in microseconds.
|
||||
* @param allowNoop Allow the seek operation to do nothing if the seek time is in the current
|
||||
* fragment run, is equal to or greater than the time of the current sample, and if there
|
||||
* does not exist a sync frame between these two times.
|
||||
* @return True if the operation resulted in a change of state. False if it was a no-op.
|
||||
*/
|
||||
public boolean seekTo(long seekTimeUs, boolean allowNoop);
|
||||
|
||||
}
|
@ -18,11 +18,13 @@ package com.google.android.exoplayer.parser.mp4;
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.ParserException;
|
||||
import com.google.android.exoplayer.SampleHolder;
|
||||
import com.google.android.exoplayer.parser.Extractor;
|
||||
import com.google.android.exoplayer.parser.SegmentIndex;
|
||||
import com.google.android.exoplayer.parser.mp4.Atom.ContainerAtom;
|
||||
import com.google.android.exoplayer.parser.mp4.Atom.LeafAtom;
|
||||
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
|
||||
import com.google.android.exoplayer.util.MimeTypes;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
@ -47,7 +49,7 @@ import java.util.UUID;
|
||||
* <p>
|
||||
* This implementation only supports de-muxed (i.e. single track) streams.
|
||||
*/
|
||||
public final class FragmentedMp4Extractor {
|
||||
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.
|
||||
@ -58,32 +60,6 @@ public final class FragmentedMp4Extractor {
|
||||
*/
|
||||
public static final int WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME = 1;
|
||||
|
||||
/**
|
||||
* An attempt to read from the input stream returned insufficient data.
|
||||
*/
|
||||
public static final int RESULT_NEED_MORE_DATA = 1;
|
||||
/**
|
||||
* The end of the input stream was reached.
|
||||
*/
|
||||
public static final int RESULT_END_OF_STREAM = 2;
|
||||
/**
|
||||
* A media sample was read.
|
||||
*/
|
||||
public static final int RESULT_READ_SAMPLE = 4;
|
||||
/**
|
||||
* A moov atom was read. The parsed data can be read using {@link #getFormat()} and
|
||||
* {@link #getPsshInfo}.
|
||||
*/
|
||||
public static final int RESULT_READ_INIT = 8;
|
||||
/**
|
||||
* A sidx atom was read. The parsed data can be read using {@link #getIndex()}.
|
||||
*/
|
||||
public static final int RESULT_READ_INDEX = 16;
|
||||
/**
|
||||
* The next thing to be read is a sample, but a {@link SampleHolder} was not supplied.
|
||||
*/
|
||||
public static final int RESULT_NEED_SAMPLE_HOLDER = 32;
|
||||
|
||||
private static final int READ_TERMINATING_RESULTS = RESULT_NEED_MORE_DATA | RESULT_END_OF_STREAM
|
||||
| RESULT_READ_SAMPLE | RESULT_NEED_SAMPLE_HOLDER;
|
||||
private static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1};
|
||||
@ -196,22 +172,13 @@ public final class FragmentedMp4Extractor {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the segment index parsed from the stream.
|
||||
* Sideloads track information into the extractor.
|
||||
*
|
||||
* @return The segment index, or null if a SIDX atom has yet to be parsed.
|
||||
* @param track The track to sideload.
|
||||
*/
|
||||
public SegmentIndex getIndex() {
|
||||
return segmentIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the pssh information parsed from the stream.
|
||||
*
|
||||
* @return The pssh information. May be null if the MOOV atom has yet to be parsed of if it did
|
||||
* not contain any pssh information.
|
||||
*/
|
||||
public Map<UUID, byte[]> getPsshInfo() {
|
||||
return psshData.isEmpty() ? null : psshData;
|
||||
public void setTrack(Track track) {
|
||||
this.extendsDefaults = new DefaultSampleValues(0, 0, 0, 0);
|
||||
this.track = track;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -228,38 +195,27 @@ public final class FragmentedMp4Extractor {
|
||||
psshData.put(uuid, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the format of the samples contained within the media stream.
|
||||
*
|
||||
* @return The sample media format, or null if a MOOV atom has yet to be parsed.
|
||||
*/
|
||||
@Override
|
||||
public Map<UUID, byte[]> getPsshInfo() {
|
||||
return psshData.isEmpty() ? null : psshData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SegmentIndex getIndex() {
|
||||
return segmentIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasRelativeIndexOffsets() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaFormat getFormat() {
|
||||
return track == null ? null : track.mediaFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sideloads track information into the extractor.
|
||||
*
|
||||
* @param track The track to sideload.
|
||||
*/
|
||||
public void setTrack(Track track) {
|
||||
this.extendsDefaults = new DefaultSampleValues(0, 0, 0, 0);
|
||||
this.track = track;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consumes data from a {@link NonBlockingInputStream}.
|
||||
* <p>
|
||||
* The read terminates if the end of the input stream is reached, if an attempt to read from the
|
||||
* input stream returned 0 bytes of data, or if a sample is read. The returned flags indicate
|
||||
* both the reason for termination and data that was parsed during the read.
|
||||
*
|
||||
* @param inputStream The input stream from which data should be read.
|
||||
* @param out A {@link SampleHolder} into which the next sample should be read. If null then
|
||||
* {@link #RESULT_NEED_SAMPLE_HOLDER} will be returned once a sample has been reached.
|
||||
* @return One or more of the {@code RESULT_*} flags defined in this class.
|
||||
* @throws ParserException If an error occurs parsing the media data.
|
||||
*/
|
||||
@Override
|
||||
public int read(NonBlockingInputStream inputStream, SampleHolder out)
|
||||
throws ParserException {
|
||||
try {
|
||||
@ -286,15 +242,7 @@ public final class FragmentedMp4Extractor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeks to a position before or equal to the requested time.
|
||||
*
|
||||
* @param seekTimeUs The desired seek time in microseconds.
|
||||
* @param allowNoop Allow the seek operation to do nothing if the seek time is in the current
|
||||
* fragment run, is equal to or greater than the time of the current sample, and if there
|
||||
* does not exist a sync frame between these two times.
|
||||
* @return True if the operation resulted in a change of state. False if it was a no-op.
|
||||
*/
|
||||
@Override
|
||||
public boolean seekTo(long seekTimeUs, boolean allowNoop) {
|
||||
pendingSeekTimeMs = (int) (seekTimeUs / 1000);
|
||||
if (allowNoop && fragmentRun != null
|
||||
@ -780,10 +728,10 @@ public final class FragmentedMp4Extractor {
|
||||
LeafAtom trun = traf.getLeafAtomOfType(Atom.TYPE_trun);
|
||||
parseTrun(track, fragmentHeader, decodeTime, workaroundFlags, trun.data, out);
|
||||
|
||||
TrackEncryptionBox trackEncryptionBox =
|
||||
track.sampleDescriptionEncryptionBoxes[fragmentHeader.sampleDescriptionIndex];
|
||||
LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz);
|
||||
if (saiz != null) {
|
||||
TrackEncryptionBox trackEncryptionBox =
|
||||
track.sampleDescriptionEncryptionBoxes[fragmentHeader.sampleDescriptionIndex];
|
||||
parseSaiz(trackEncryptionBox, saiz.data, out);
|
||||
}
|
||||
|
||||
|
@ -1,462 +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.parser.webm;
|
||||
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.SampleHolder;
|
||||
import com.google.android.exoplayer.parser.SegmentIndex;
|
||||
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
||||
import com.google.android.exoplayer.util.LongArray;
|
||||
import com.google.android.exoplayer.util.MimeTypes;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.media.MediaExtractor;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Default version of an extractor to facilitate data retrieval from the WebM container format.
|
||||
*
|
||||
* <p>WebM is a subset of the EBML elements defined for Matroska. More information about EBML and
|
||||
* Matroska is available <a href="http://www.matroska.org/technical/specs/index.html">here</a>.
|
||||
* More info about WebM is <a href="http://www.webmproject.org/code/specs/container/">here</a>.
|
||||
*/
|
||||
@TargetApi(16)
|
||||
public final class DefaultWebmExtractor implements WebmExtractor {
|
||||
|
||||
private static final String DOC_TYPE_WEBM = "webm";
|
||||
private static final String CODEC_ID_VP9 = "V_VP9";
|
||||
private static final int UNKNOWN = -1;
|
||||
|
||||
// Element IDs
|
||||
private static final int ID_EBML = 0x1A45DFA3;
|
||||
private static final int ID_EBML_READ_VERSION = 0x42F7;
|
||||
private static final int ID_DOC_TYPE = 0x4282;
|
||||
private static final int ID_DOC_TYPE_READ_VERSION = 0x4285;
|
||||
|
||||
private static final int ID_SEGMENT = 0x18538067;
|
||||
|
||||
private static final int ID_INFO = 0x1549A966;
|
||||
private static final int ID_TIMECODE_SCALE = 0x2AD7B1;
|
||||
private static final int ID_DURATION = 0x4489;
|
||||
|
||||
private static final int ID_CLUSTER = 0x1F43B675;
|
||||
private static final int ID_TIME_CODE = 0xE7;
|
||||
private static final int ID_SIMPLE_BLOCK = 0xA3;
|
||||
|
||||
private static final int ID_TRACKS = 0x1654AE6B;
|
||||
private static final int ID_TRACK_ENTRY = 0xAE;
|
||||
private static final int ID_CODEC_ID = 0x86;
|
||||
private static final int ID_VIDEO = 0xE0;
|
||||
private static final int ID_PIXEL_WIDTH = 0xB0;
|
||||
private static final int ID_PIXEL_HEIGHT = 0xBA;
|
||||
|
||||
private static final int ID_CUES = 0x1C53BB6B;
|
||||
private static final int ID_CUE_POINT = 0xBB;
|
||||
private static final int ID_CUE_TIME = 0xB3;
|
||||
private static final int ID_CUE_TRACK_POSITIONS = 0xB7;
|
||||
private static final int ID_CUE_CLUSTER_POSITION = 0xF1;
|
||||
|
||||
// SimpleBlock Lacing Values
|
||||
private static final int LACING_NONE = 0;
|
||||
private static final int LACING_XIPH = 1;
|
||||
private static final int LACING_FIXED = 2;
|
||||
private static final int LACING_EBML = 3;
|
||||
|
||||
private static final int READ_TERMINATING_RESULTS = RESULT_NEED_MORE_DATA | RESULT_END_OF_STREAM
|
||||
| RESULT_READ_SAMPLE | RESULT_NEED_SAMPLE_HOLDER;
|
||||
|
||||
private final EbmlReader reader;
|
||||
private final byte[] simpleBlockTimecodeAndFlags = new byte[3];
|
||||
|
||||
private SampleHolder sampleHolder;
|
||||
private int readResults;
|
||||
|
||||
private long segmentStartOffsetBytes = UNKNOWN;
|
||||
private long segmentEndOffsetBytes = UNKNOWN;
|
||||
private long timecodeScale = 1000000L;
|
||||
private long durationUs = UNKNOWN;
|
||||
private int pixelWidth = UNKNOWN;
|
||||
private int pixelHeight = UNKNOWN;
|
||||
private long cuesSizeBytes = UNKNOWN;
|
||||
private long clusterTimecodeUs = UNKNOWN;
|
||||
private long simpleBlockTimecodeUs = UNKNOWN;
|
||||
private MediaFormat format;
|
||||
private SegmentIndex cues;
|
||||
private LongArray cueTimesUs;
|
||||
private LongArray cueClusterPositions;
|
||||
|
||||
public DefaultWebmExtractor() {
|
||||
this(new DefaultEbmlReader());
|
||||
}
|
||||
|
||||
/* package */ DefaultWebmExtractor(EbmlReader reader) {
|
||||
this.reader = reader;
|
||||
this.reader.setEventHandler(new InnerEbmlEventHandler());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(NonBlockingInputStream inputStream, SampleHolder sampleHolder) {
|
||||
this.sampleHolder = sampleHolder;
|
||||
this.readResults = 0;
|
||||
while ((readResults & READ_TERMINATING_RESULTS) == 0) {
|
||||
int ebmlReadResult = reader.read(inputStream);
|
||||
if (ebmlReadResult == EbmlReader.READ_RESULT_NEED_MORE_DATA) {
|
||||
readResults |= WebmExtractor.RESULT_NEED_MORE_DATA;
|
||||
} else if (ebmlReadResult == EbmlReader.READ_RESULT_END_OF_STREAM) {
|
||||
readResults |= WebmExtractor.RESULT_END_OF_STREAM;
|
||||
}
|
||||
}
|
||||
this.sampleHolder = null;
|
||||
return readResults;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean seekTo(long seekTimeUs, boolean allowNoop) {
|
||||
if (allowNoop
|
||||
&& cues != null
|
||||
&& clusterTimecodeUs != UNKNOWN
|
||||
&& simpleBlockTimecodeUs != UNKNOWN
|
||||
&& seekTimeUs >= simpleBlockTimecodeUs) {
|
||||
int clusterIndex = Arrays.binarySearch(cues.timesUs, clusterTimecodeUs);
|
||||
if (clusterIndex >= 0 && seekTimeUs < clusterTimecodeUs + cues.durationsUs[clusterIndex]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
clusterTimecodeUs = UNKNOWN;
|
||||
simpleBlockTimecodeUs = UNKNOWN;
|
||||
reader.reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SegmentIndex getIndex() {
|
||||
return cues;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaFormat getFormat() {
|
||||
return format;
|
||||
}
|
||||
|
||||
/* package */ int getElementType(int id) {
|
||||
switch (id) {
|
||||
case ID_EBML:
|
||||
case ID_SEGMENT:
|
||||
case ID_INFO:
|
||||
case ID_CLUSTER:
|
||||
case ID_TRACKS:
|
||||
case ID_TRACK_ENTRY:
|
||||
case ID_VIDEO:
|
||||
case ID_CUES:
|
||||
case ID_CUE_POINT:
|
||||
case ID_CUE_TRACK_POSITIONS:
|
||||
return EbmlReader.TYPE_MASTER;
|
||||
case ID_EBML_READ_VERSION:
|
||||
case ID_DOC_TYPE_READ_VERSION:
|
||||
case ID_TIMECODE_SCALE:
|
||||
case ID_TIME_CODE:
|
||||
case ID_PIXEL_WIDTH:
|
||||
case ID_PIXEL_HEIGHT:
|
||||
case ID_CUE_TIME:
|
||||
case ID_CUE_CLUSTER_POSITION:
|
||||
return EbmlReader.TYPE_UNSIGNED_INT;
|
||||
case ID_DOC_TYPE:
|
||||
case ID_CODEC_ID:
|
||||
return EbmlReader.TYPE_STRING;
|
||||
case ID_SIMPLE_BLOCK:
|
||||
return EbmlReader.TYPE_BINARY;
|
||||
case ID_DURATION:
|
||||
return EbmlReader.TYPE_FLOAT;
|
||||
default:
|
||||
return EbmlReader.TYPE_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
/* package */ boolean onMasterElementStart(
|
||||
int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) {
|
||||
switch (id) {
|
||||
case ID_SEGMENT:
|
||||
if (segmentStartOffsetBytes != UNKNOWN || segmentEndOffsetBytes != UNKNOWN) {
|
||||
throw new IllegalStateException("Multiple Segment elements not supported");
|
||||
}
|
||||
segmentStartOffsetBytes = elementOffsetBytes + headerSizeBytes;
|
||||
segmentEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes;
|
||||
break;
|
||||
case ID_CUES:
|
||||
cuesSizeBytes = headerSizeBytes + contentsSizeBytes;
|
||||
cueTimesUs = new LongArray();
|
||||
cueClusterPositions = new LongArray();
|
||||
break;
|
||||
default:
|
||||
// pass
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* package */ boolean onMasterElementEnd(int id) {
|
||||
switch (id) {
|
||||
case ID_CUES:
|
||||
buildCues();
|
||||
return false;
|
||||
case ID_VIDEO:
|
||||
buildFormat();
|
||||
return true;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/* package */ boolean onIntegerElement(int id, long value) {
|
||||
switch (id) {
|
||||
case ID_EBML_READ_VERSION:
|
||||
// Validate that EBMLReadVersion is supported. This extractor only supports v1.
|
||||
if (value != 1) {
|
||||
throw new IllegalArgumentException("EBMLReadVersion " + value + " not supported");
|
||||
}
|
||||
break;
|
||||
case ID_DOC_TYPE_READ_VERSION:
|
||||
// Validate that DocTypeReadVersion is supported. This extractor only supports up to v2.
|
||||
if (value < 1 || value > 2) {
|
||||
throw new IllegalArgumentException("DocTypeReadVersion " + value + " not supported");
|
||||
}
|
||||
break;
|
||||
case ID_TIMECODE_SCALE:
|
||||
timecodeScale = value;
|
||||
break;
|
||||
case ID_PIXEL_WIDTH:
|
||||
pixelWidth = (int) value;
|
||||
break;
|
||||
case ID_PIXEL_HEIGHT:
|
||||
pixelHeight = (int) value;
|
||||
break;
|
||||
case ID_CUE_TIME:
|
||||
cueTimesUs.add(scaleTimecodeToUs(value));
|
||||
break;
|
||||
case ID_CUE_CLUSTER_POSITION:
|
||||
cueClusterPositions.add(value);
|
||||
break;
|
||||
case ID_TIME_CODE:
|
||||
clusterTimecodeUs = scaleTimecodeToUs(value);
|
||||
break;
|
||||
default:
|
||||
// pass
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* package */ boolean onFloatElement(int id, double value) {
|
||||
if (id == ID_DURATION) {
|
||||
durationUs = scaleTimecodeToUs((long) value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* package */ boolean onStringElement(int id, String value) {
|
||||
switch (id) {
|
||||
case ID_DOC_TYPE:
|
||||
// Validate that DocType is supported. This extractor only supports "webm".
|
||||
if (!DOC_TYPE_WEBM.equals(value)) {
|
||||
throw new IllegalArgumentException("DocType " + value + " not supported");
|
||||
}
|
||||
break;
|
||||
case ID_CODEC_ID:
|
||||
// Validate that CodecID is supported. This extractor only supports "V_VP9".
|
||||
if (!CODEC_ID_VP9.equals(value)) {
|
||||
throw new IllegalArgumentException("CodecID " + value + " not supported");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// pass
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* package */ boolean onBinaryElement(
|
||||
int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,
|
||||
NonBlockingInputStream inputStream) {
|
||||
if (id == ID_SIMPLE_BLOCK) {
|
||||
// Please refer to http://www.matroska.org/technical/specs/index.html#simpleblock_structure
|
||||
// for info about how data is organized in a SimpleBlock element.
|
||||
|
||||
// If we don't have a sample holder then don't consume the data.
|
||||
if (sampleHolder == null) {
|
||||
readResults |= RESULT_NEED_SAMPLE_HOLDER;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Value of trackNumber is not used but needs to be read.
|
||||
reader.readVarint(inputStream);
|
||||
|
||||
// Next three bytes have timecode and flags.
|
||||
reader.readBytes(inputStream, simpleBlockTimecodeAndFlags, 3);
|
||||
|
||||
// First two bytes of the three are the relative timecode.
|
||||
int timecode =
|
||||
(simpleBlockTimecodeAndFlags[0] << 8) | (simpleBlockTimecodeAndFlags[1] & 0xff);
|
||||
long timecodeUs = scaleTimecodeToUs(timecode);
|
||||
|
||||
// Last byte of the three has some flags and the lacing value.
|
||||
boolean keyframe = (simpleBlockTimecodeAndFlags[2] & 0x80) == 0x80;
|
||||
boolean invisible = (simpleBlockTimecodeAndFlags[2] & 0x08) == 0x08;
|
||||
int lacing = (simpleBlockTimecodeAndFlags[2] & 0x06) >> 1;
|
||||
|
||||
// Validate lacing and set info into sample holder.
|
||||
switch (lacing) {
|
||||
case LACING_NONE:
|
||||
long elementEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes;
|
||||
simpleBlockTimecodeUs = clusterTimecodeUs + timecodeUs;
|
||||
sampleHolder.flags = keyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0;
|
||||
sampleHolder.decodeOnly = invisible;
|
||||
sampleHolder.timeUs = clusterTimecodeUs + timecodeUs;
|
||||
sampleHolder.size = (int) (elementEndOffsetBytes - reader.getBytesRead());
|
||||
break;
|
||||
case LACING_EBML:
|
||||
case LACING_FIXED:
|
||||
case LACING_XIPH:
|
||||
default:
|
||||
throw new IllegalStateException("Lacing mode " + lacing + " not supported");
|
||||
}
|
||||
|
||||
ByteBuffer outputData = sampleHolder.data;
|
||||
if (sampleHolder.allowDataBufferReplacement
|
||||
&& (sampleHolder.data == null || sampleHolder.data.capacity() < sampleHolder.size)) {
|
||||
outputData = ByteBuffer.allocate(sampleHolder.size);
|
||||
sampleHolder.data = outputData;
|
||||
}
|
||||
|
||||
if (outputData == null) {
|
||||
reader.skipBytes(inputStream, sampleHolder.size);
|
||||
sampleHolder.size = 0;
|
||||
} else {
|
||||
reader.readBytes(inputStream, outputData, sampleHolder.size);
|
||||
}
|
||||
readResults |= RESULT_READ_SAMPLE;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private long scaleTimecodeToUs(long unscaledTimecode) {
|
||||
return TimeUnit.NANOSECONDS.toMicros(unscaledTimecode * timecodeScale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a video {@link MediaFormat} containing recently gathered Video information, if needed.
|
||||
*
|
||||
* <p>Replaces the previous {@link #format} only if video width/height have changed.
|
||||
* {@link #format} is guaranteed to not be null after calling this method. In
|
||||
* the event that it can't be built, an {@link IllegalStateException} will be thrown.
|
||||
*/
|
||||
private void buildFormat() {
|
||||
if (pixelWidth != UNKNOWN && pixelHeight != UNKNOWN
|
||||
&& (format == null || format.width != pixelWidth || format.height != pixelHeight)) {
|
||||
format = MediaFormat.createVideoFormat(
|
||||
MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, pixelWidth, pixelHeight, null);
|
||||
readResults |= RESULT_READ_INIT;
|
||||
} else if (format == null) {
|
||||
throw new IllegalStateException("Unable to build format");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a {@link SegmentIndex} containing recently gathered Cues information.
|
||||
*
|
||||
* <p>{@link #cues} is guaranteed to not be null after calling this method. In
|
||||
* the event that it can't be built, an {@link IllegalStateException} will be thrown.
|
||||
*/
|
||||
private void buildCues() {
|
||||
if (segmentStartOffsetBytes == UNKNOWN) {
|
||||
throw new IllegalStateException("Segment start/end offsets unknown");
|
||||
} else if (durationUs == UNKNOWN) {
|
||||
throw new IllegalStateException("Duration unknown");
|
||||
} else if (cuesSizeBytes == UNKNOWN) {
|
||||
throw new IllegalStateException("Cues size unknown");
|
||||
} else if (cueTimesUs == null || cueClusterPositions == null
|
||||
|| cueTimesUs.size() == 0 || cueTimesUs.size() != cueClusterPositions.size()) {
|
||||
throw new IllegalStateException("Invalid/missing cue points");
|
||||
}
|
||||
int cuePointsSize = cueTimesUs.size();
|
||||
int[] sizes = new int[cuePointsSize];
|
||||
long[] offsets = new long[cuePointsSize];
|
||||
long[] durationsUs = new long[cuePointsSize];
|
||||
long[] timesUs = new long[cuePointsSize];
|
||||
for (int i = 0; i < cuePointsSize; i++) {
|
||||
timesUs[i] = cueTimesUs.get(i);
|
||||
offsets[i] = segmentStartOffsetBytes + cueClusterPositions.get(i);
|
||||
}
|
||||
for (int i = 0; i < cuePointsSize - 1; i++) {
|
||||
sizes[i] = (int) (offsets[i + 1] - offsets[i]);
|
||||
durationsUs[i] = timesUs[i + 1] - timesUs[i];
|
||||
}
|
||||
sizes[cuePointsSize - 1] = (int) (segmentEndOffsetBytes - offsets[cuePointsSize - 1]);
|
||||
durationsUs[cuePointsSize - 1] = durationUs - timesUs[cuePointsSize - 1];
|
||||
cues = new SegmentIndex((int) cuesSizeBytes, sizes, offsets, durationsUs, timesUs);
|
||||
cueTimesUs = null;
|
||||
cueClusterPositions = null;
|
||||
readResults |= RESULT_READ_INDEX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Passes events through to {@link DefaultWebmExtractor} as
|
||||
* callbacks from {@link EbmlReader} are received.
|
||||
*/
|
||||
private final class InnerEbmlEventHandler implements EbmlEventHandler {
|
||||
|
||||
@Override
|
||||
public int getElementType(int id) {
|
||||
return DefaultWebmExtractor.this.getElementType(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMasterElementStart(
|
||||
int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) {
|
||||
DefaultWebmExtractor.this.onMasterElementStart(
|
||||
id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMasterElementEnd(int id) {
|
||||
DefaultWebmExtractor.this.onMasterElementEnd(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIntegerElement(int id, long value) {
|
||||
DefaultWebmExtractor.this.onIntegerElement(id, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFloatElement(int id, double value) {
|
||||
DefaultWebmExtractor.this.onFloatElement(id, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStringElement(int id, String value) {
|
||||
DefaultWebmExtractor.this.onStringElement(id, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBinaryElement(
|
||||
int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,
|
||||
NonBlockingInputStream inputStream) {
|
||||
return DefaultWebmExtractor.this.onBinaryElement(
|
||||
id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes, inputStream);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -17,76 +17,460 @@ package com.google.android.exoplayer.parser.webm;
|
||||
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.SampleHolder;
|
||||
import com.google.android.exoplayer.parser.Extractor;
|
||||
import com.google.android.exoplayer.parser.SegmentIndex;
|
||||
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
||||
import com.google.android.exoplayer.util.LongArray;
|
||||
import com.google.android.exoplayer.util.MimeTypes;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.media.MediaExtractor;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Extractor to facilitate data retrieval from the WebM container format.
|
||||
* An extractor to facilitate data retrieval from the WebM container format.
|
||||
*
|
||||
* <p>WebM is a subset of the EBML elements defined for Matroska. More information about EBML and
|
||||
* Matroska is available <a href="http://www.matroska.org/technical/specs/index.html">here</a>.
|
||||
* More info about WebM is <a href="http://www.webmproject.org/code/specs/container/">here</a>.
|
||||
*/
|
||||
public interface WebmExtractor {
|
||||
@TargetApi(16)
|
||||
public final class WebmExtractor implements Extractor {
|
||||
|
||||
private static final String DOC_TYPE_WEBM = "webm";
|
||||
private static final String CODEC_ID_VP9 = "V_VP9";
|
||||
private static final int UNKNOWN = -1;
|
||||
|
||||
// Element IDs
|
||||
private static final int ID_EBML = 0x1A45DFA3;
|
||||
private static final int ID_EBML_READ_VERSION = 0x42F7;
|
||||
private static final int ID_DOC_TYPE = 0x4282;
|
||||
private static final int ID_DOC_TYPE_READ_VERSION = 0x4285;
|
||||
|
||||
private static final int ID_SEGMENT = 0x18538067;
|
||||
|
||||
private static final int ID_INFO = 0x1549A966;
|
||||
private static final int ID_TIMECODE_SCALE = 0x2AD7B1;
|
||||
private static final int ID_DURATION = 0x4489;
|
||||
|
||||
private static final int ID_CLUSTER = 0x1F43B675;
|
||||
private static final int ID_TIME_CODE = 0xE7;
|
||||
private static final int ID_SIMPLE_BLOCK = 0xA3;
|
||||
|
||||
private static final int ID_TRACKS = 0x1654AE6B;
|
||||
private static final int ID_TRACK_ENTRY = 0xAE;
|
||||
private static final int ID_CODEC_ID = 0x86;
|
||||
private static final int ID_VIDEO = 0xE0;
|
||||
private static final int ID_PIXEL_WIDTH = 0xB0;
|
||||
private static final int ID_PIXEL_HEIGHT = 0xBA;
|
||||
|
||||
private static final int ID_CUES = 0x1C53BB6B;
|
||||
private static final int ID_CUE_POINT = 0xBB;
|
||||
private static final int ID_CUE_TIME = 0xB3;
|
||||
private static final int ID_CUE_TRACK_POSITIONS = 0xB7;
|
||||
private static final int ID_CUE_CLUSTER_POSITION = 0xF1;
|
||||
|
||||
// SimpleBlock Lacing Values
|
||||
private static final int LACING_NONE = 0;
|
||||
private static final int LACING_XIPH = 1;
|
||||
private static final int LACING_FIXED = 2;
|
||||
private static final int LACING_EBML = 3;
|
||||
|
||||
private static final int READ_TERMINATING_RESULTS = RESULT_NEED_MORE_DATA | RESULT_END_OF_STREAM
|
||||
| RESULT_READ_SAMPLE | RESULT_NEED_SAMPLE_HOLDER;
|
||||
|
||||
private final EbmlReader reader;
|
||||
private final byte[] simpleBlockTimecodeAndFlags = new byte[3];
|
||||
|
||||
private SampleHolder sampleHolder;
|
||||
private int readResults;
|
||||
|
||||
private long segmentStartOffsetBytes = UNKNOWN;
|
||||
private long segmentEndOffsetBytes = UNKNOWN;
|
||||
private long timecodeScale = 1000000L;
|
||||
private long durationUs = UNKNOWN;
|
||||
private int pixelWidth = UNKNOWN;
|
||||
private int pixelHeight = UNKNOWN;
|
||||
private long cuesSizeBytes = UNKNOWN;
|
||||
private long clusterTimecodeUs = UNKNOWN;
|
||||
private long simpleBlockTimecodeUs = UNKNOWN;
|
||||
private MediaFormat format;
|
||||
private SegmentIndex cues;
|
||||
private LongArray cueTimesUs;
|
||||
private LongArray cueClusterPositions;
|
||||
|
||||
public WebmExtractor() {
|
||||
this(new DefaultEbmlReader());
|
||||
}
|
||||
|
||||
/* package */ WebmExtractor(EbmlReader reader) {
|
||||
this.reader = reader;
|
||||
this.reader.setEventHandler(new InnerEbmlEventHandler());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(NonBlockingInputStream inputStream, SampleHolder sampleHolder) {
|
||||
this.sampleHolder = sampleHolder;
|
||||
this.readResults = 0;
|
||||
while ((readResults & READ_TERMINATING_RESULTS) == 0) {
|
||||
int ebmlReadResult = reader.read(inputStream);
|
||||
if (ebmlReadResult == EbmlReader.READ_RESULT_NEED_MORE_DATA) {
|
||||
readResults |= WebmExtractor.RESULT_NEED_MORE_DATA;
|
||||
} else if (ebmlReadResult == EbmlReader.READ_RESULT_END_OF_STREAM) {
|
||||
readResults |= WebmExtractor.RESULT_END_OF_STREAM;
|
||||
}
|
||||
}
|
||||
this.sampleHolder = null;
|
||||
return readResults;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean seekTo(long seekTimeUs, boolean allowNoop) {
|
||||
if (allowNoop
|
||||
&& cues != null
|
||||
&& clusterTimecodeUs != UNKNOWN
|
||||
&& simpleBlockTimecodeUs != UNKNOWN
|
||||
&& seekTimeUs >= simpleBlockTimecodeUs) {
|
||||
int clusterIndex = Arrays.binarySearch(cues.timesUs, clusterTimecodeUs);
|
||||
if (clusterIndex >= 0 && seekTimeUs < clusterTimecodeUs + cues.durationsUs[clusterIndex]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
clusterTimecodeUs = UNKNOWN;
|
||||
simpleBlockTimecodeUs = UNKNOWN;
|
||||
reader.reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SegmentIndex getIndex() {
|
||||
return cues;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasRelativeIndexOffsets() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaFormat getFormat() {
|
||||
return format;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<UUID, byte[]> getPsshInfo() {
|
||||
// TODO: Parse pssh data from Webm streams.
|
||||
return null;
|
||||
}
|
||||
|
||||
/* package */ int getElementType(int id) {
|
||||
switch (id) {
|
||||
case ID_EBML:
|
||||
case ID_SEGMENT:
|
||||
case ID_INFO:
|
||||
case ID_CLUSTER:
|
||||
case ID_TRACKS:
|
||||
case ID_TRACK_ENTRY:
|
||||
case ID_VIDEO:
|
||||
case ID_CUES:
|
||||
case ID_CUE_POINT:
|
||||
case ID_CUE_TRACK_POSITIONS:
|
||||
return EbmlReader.TYPE_MASTER;
|
||||
case ID_EBML_READ_VERSION:
|
||||
case ID_DOC_TYPE_READ_VERSION:
|
||||
case ID_TIMECODE_SCALE:
|
||||
case ID_TIME_CODE:
|
||||
case ID_PIXEL_WIDTH:
|
||||
case ID_PIXEL_HEIGHT:
|
||||
case ID_CUE_TIME:
|
||||
case ID_CUE_CLUSTER_POSITION:
|
||||
return EbmlReader.TYPE_UNSIGNED_INT;
|
||||
case ID_DOC_TYPE:
|
||||
case ID_CODEC_ID:
|
||||
return EbmlReader.TYPE_STRING;
|
||||
case ID_SIMPLE_BLOCK:
|
||||
return EbmlReader.TYPE_BINARY;
|
||||
case ID_DURATION:
|
||||
return EbmlReader.TYPE_FLOAT;
|
||||
default:
|
||||
return EbmlReader.TYPE_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
/* package */ boolean onMasterElementStart(
|
||||
int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) {
|
||||
switch (id) {
|
||||
case ID_SEGMENT:
|
||||
if (segmentStartOffsetBytes != UNKNOWN || segmentEndOffsetBytes != UNKNOWN) {
|
||||
throw new IllegalStateException("Multiple Segment elements not supported");
|
||||
}
|
||||
segmentStartOffsetBytes = elementOffsetBytes + headerSizeBytes;
|
||||
segmentEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes;
|
||||
break;
|
||||
case ID_CUES:
|
||||
cuesSizeBytes = headerSizeBytes + contentsSizeBytes;
|
||||
cueTimesUs = new LongArray();
|
||||
cueClusterPositions = new LongArray();
|
||||
break;
|
||||
default:
|
||||
// pass
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* package */ boolean onMasterElementEnd(int id) {
|
||||
switch (id) {
|
||||
case ID_CUES:
|
||||
buildCues();
|
||||
return false;
|
||||
case ID_VIDEO:
|
||||
buildFormat();
|
||||
return true;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/* package */ boolean onIntegerElement(int id, long value) {
|
||||
switch (id) {
|
||||
case ID_EBML_READ_VERSION:
|
||||
// Validate that EBMLReadVersion is supported. This extractor only supports v1.
|
||||
if (value != 1) {
|
||||
throw new IllegalArgumentException("EBMLReadVersion " + value + " not supported");
|
||||
}
|
||||
break;
|
||||
case ID_DOC_TYPE_READ_VERSION:
|
||||
// Validate that DocTypeReadVersion is supported. This extractor only supports up to v2.
|
||||
if (value < 1 || value > 2) {
|
||||
throw new IllegalArgumentException("DocTypeReadVersion " + value + " not supported");
|
||||
}
|
||||
break;
|
||||
case ID_TIMECODE_SCALE:
|
||||
timecodeScale = value;
|
||||
break;
|
||||
case ID_PIXEL_WIDTH:
|
||||
pixelWidth = (int) value;
|
||||
break;
|
||||
case ID_PIXEL_HEIGHT:
|
||||
pixelHeight = (int) value;
|
||||
break;
|
||||
case ID_CUE_TIME:
|
||||
cueTimesUs.add(scaleTimecodeToUs(value));
|
||||
break;
|
||||
case ID_CUE_CLUSTER_POSITION:
|
||||
cueClusterPositions.add(value);
|
||||
break;
|
||||
case ID_TIME_CODE:
|
||||
clusterTimecodeUs = scaleTimecodeToUs(value);
|
||||
break;
|
||||
default:
|
||||
// pass
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* package */ boolean onFloatElement(int id, double value) {
|
||||
if (id == ID_DURATION) {
|
||||
durationUs = scaleTimecodeToUs((long) value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* package */ boolean onStringElement(int id, String value) {
|
||||
switch (id) {
|
||||
case ID_DOC_TYPE:
|
||||
// Validate that DocType is supported. This extractor only supports "webm".
|
||||
if (!DOC_TYPE_WEBM.equals(value)) {
|
||||
throw new IllegalArgumentException("DocType " + value + " not supported");
|
||||
}
|
||||
break;
|
||||
case ID_CODEC_ID:
|
||||
// Validate that CodecID is supported. This extractor only supports "V_VP9".
|
||||
if (!CODEC_ID_VP9.equals(value)) {
|
||||
throw new IllegalArgumentException("CodecID " + value + " not supported");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// pass
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* package */ boolean onBinaryElement(
|
||||
int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,
|
||||
NonBlockingInputStream inputStream) {
|
||||
if (id == ID_SIMPLE_BLOCK) {
|
||||
// Please refer to http://www.matroska.org/technical/specs/index.html#simpleblock_structure
|
||||
// for info about how data is organized in a SimpleBlock element.
|
||||
|
||||
// If we don't have a sample holder then don't consume the data.
|
||||
if (sampleHolder == null) {
|
||||
readResults |= RESULT_NEED_SAMPLE_HOLDER;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Value of trackNumber is not used but needs to be read.
|
||||
reader.readVarint(inputStream);
|
||||
|
||||
// Next three bytes have timecode and flags.
|
||||
reader.readBytes(inputStream, simpleBlockTimecodeAndFlags, 3);
|
||||
|
||||
// First two bytes of the three are the relative timecode.
|
||||
int timecode =
|
||||
(simpleBlockTimecodeAndFlags[0] << 8) | (simpleBlockTimecodeAndFlags[1] & 0xff);
|
||||
long timecodeUs = scaleTimecodeToUs(timecode);
|
||||
|
||||
// Last byte of the three has some flags and the lacing value.
|
||||
boolean keyframe = (simpleBlockTimecodeAndFlags[2] & 0x80) == 0x80;
|
||||
boolean invisible = (simpleBlockTimecodeAndFlags[2] & 0x08) == 0x08;
|
||||
int lacing = (simpleBlockTimecodeAndFlags[2] & 0x06) >> 1;
|
||||
|
||||
// Validate lacing and set info into sample holder.
|
||||
switch (lacing) {
|
||||
case LACING_NONE:
|
||||
long elementEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes;
|
||||
simpleBlockTimecodeUs = clusterTimecodeUs + timecodeUs;
|
||||
sampleHolder.flags = keyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0;
|
||||
sampleHolder.decodeOnly = invisible;
|
||||
sampleHolder.timeUs = clusterTimecodeUs + timecodeUs;
|
||||
sampleHolder.size = (int) (elementEndOffsetBytes - reader.getBytesRead());
|
||||
break;
|
||||
case LACING_EBML:
|
||||
case LACING_FIXED:
|
||||
case LACING_XIPH:
|
||||
default:
|
||||
throw new IllegalStateException("Lacing mode " + lacing + " not supported");
|
||||
}
|
||||
|
||||
ByteBuffer outputData = sampleHolder.data;
|
||||
if (sampleHolder.allowDataBufferReplacement
|
||||
&& (sampleHolder.data == null || sampleHolder.data.capacity() < sampleHolder.size)) {
|
||||
outputData = ByteBuffer.allocate(sampleHolder.size);
|
||||
sampleHolder.data = outputData;
|
||||
}
|
||||
|
||||
if (outputData == null) {
|
||||
reader.skipBytes(inputStream, sampleHolder.size);
|
||||
sampleHolder.size = 0;
|
||||
} else {
|
||||
reader.readBytes(inputStream, outputData, sampleHolder.size);
|
||||
}
|
||||
readResults |= RESULT_READ_SAMPLE;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private long scaleTimecodeToUs(long unscaledTimecode) {
|
||||
return TimeUnit.NANOSECONDS.toMicros(unscaledTimecode * timecodeScale);
|
||||
}
|
||||
|
||||
/**
|
||||
* An attempt to read from the input stream returned insufficient data.
|
||||
*/
|
||||
public static final int RESULT_NEED_MORE_DATA = 1;
|
||||
/**
|
||||
* The end of the input stream was reached.
|
||||
*/
|
||||
public static final int RESULT_END_OF_STREAM = 2;
|
||||
/**
|
||||
* A media sample was read.
|
||||
*/
|
||||
public static final int RESULT_READ_SAMPLE = 4;
|
||||
/**
|
||||
* Initialization data was read. The parsed data can be read using {@link #getFormat()}.
|
||||
*/
|
||||
public static final int RESULT_READ_INIT = 8;
|
||||
/**
|
||||
* A sidx atom was read. The parsed data can be read using {@link #getIndex()}.
|
||||
*/
|
||||
public static final int RESULT_READ_INDEX = 16;
|
||||
/**
|
||||
* The next thing to be read is a sample, but a {@link SampleHolder} was not supplied.
|
||||
*/
|
||||
public static final int RESULT_NEED_SAMPLE_HOLDER = 32;
|
||||
|
||||
/**
|
||||
* Consumes data from a {@link NonBlockingInputStream}.
|
||||
* Build a video {@link MediaFormat} containing recently gathered Video information, if needed.
|
||||
*
|
||||
* @param inputStream The input stream from which data should be read
|
||||
* @param sampleHolder A {@link SampleHolder} into which the sample should be read
|
||||
* @return One or more of the {@code RESULT_*} flags defined in this class.
|
||||
* <p>Replaces the previous {@link #format} only if video width/height have changed.
|
||||
* {@link #format} is guaranteed to not be null after calling this method. In
|
||||
* the event that it can't be built, an {@link IllegalStateException} will be thrown.
|
||||
*/
|
||||
public int read(NonBlockingInputStream inputStream, SampleHolder sampleHolder);
|
||||
private void buildFormat() {
|
||||
if (pixelWidth != UNKNOWN && pixelHeight != UNKNOWN
|
||||
&& (format == null || format.width != pixelWidth || format.height != pixelHeight)) {
|
||||
format = MediaFormat.createVideoFormat(
|
||||
MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, pixelWidth, pixelHeight, null);
|
||||
readResults |= RESULT_READ_INIT;
|
||||
} else if (format == null) {
|
||||
throw new IllegalStateException("Unable to build format");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeks to a position before or equal to the requested time.
|
||||
* Build a {@link SegmentIndex} containing recently gathered Cues information.
|
||||
*
|
||||
* @param seekTimeUs The desired seek time in microseconds
|
||||
* @param allowNoop Allow the seek operation to do nothing if the seek time is in the current
|
||||
* segment, is equal to or greater than the time of the current sample, and if there does not
|
||||
* exist a sync frame between these two times
|
||||
* @return True if the operation resulted in a change of state. False if it was a no-op
|
||||
* <p>{@link #cues} is guaranteed to not be null after calling this method. In
|
||||
* the event that it can't be built, an {@link IllegalStateException} will be thrown.
|
||||
*/
|
||||
public boolean seekTo(long seekTimeUs, boolean allowNoop);
|
||||
private void buildCues() {
|
||||
if (segmentStartOffsetBytes == UNKNOWN) {
|
||||
throw new IllegalStateException("Segment start/end offsets unknown");
|
||||
} else if (durationUs == UNKNOWN) {
|
||||
throw new IllegalStateException("Duration unknown");
|
||||
} else if (cuesSizeBytes == UNKNOWN) {
|
||||
throw new IllegalStateException("Cues size unknown");
|
||||
} else if (cueTimesUs == null || cueClusterPositions == null
|
||||
|| cueTimesUs.size() == 0 || cueTimesUs.size() != cueClusterPositions.size()) {
|
||||
throw new IllegalStateException("Invalid/missing cue points");
|
||||
}
|
||||
int cuePointsSize = cueTimesUs.size();
|
||||
int[] sizes = new int[cuePointsSize];
|
||||
long[] offsets = new long[cuePointsSize];
|
||||
long[] durationsUs = new long[cuePointsSize];
|
||||
long[] timesUs = new long[cuePointsSize];
|
||||
for (int i = 0; i < cuePointsSize; i++) {
|
||||
timesUs[i] = cueTimesUs.get(i);
|
||||
offsets[i] = segmentStartOffsetBytes + cueClusterPositions.get(i);
|
||||
}
|
||||
for (int i = 0; i < cuePointsSize - 1; i++) {
|
||||
sizes[i] = (int) (offsets[i + 1] - offsets[i]);
|
||||
durationsUs[i] = timesUs[i + 1] - timesUs[i];
|
||||
}
|
||||
sizes[cuePointsSize - 1] = (int) (segmentEndOffsetBytes - offsets[cuePointsSize - 1]);
|
||||
durationsUs[cuePointsSize - 1] = durationUs - timesUs[cuePointsSize - 1];
|
||||
cues = new SegmentIndex((int) cuesSizeBytes, sizes, offsets, durationsUs, timesUs);
|
||||
cueTimesUs = null;
|
||||
cueClusterPositions = null;
|
||||
readResults |= RESULT_READ_INDEX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cues for the media stream.
|
||||
*
|
||||
* @return The cues in the form of a {@link SegmentIndex}, or null if the extractor is not yet
|
||||
* prepared
|
||||
* Passes events through to {@link WebmExtractor} as
|
||||
* callbacks from {@link EbmlReader} are received.
|
||||
*/
|
||||
public SegmentIndex getIndex();
|
||||
private final class InnerEbmlEventHandler implements EbmlEventHandler {
|
||||
|
||||
/**
|
||||
* Returns the format of the samples contained within the media stream.
|
||||
*
|
||||
* @return The sample media format, or null if the extracted is not yet prepared
|
||||
*/
|
||||
public MediaFormat getFormat();
|
||||
@Override
|
||||
public int getElementType(int id) {
|
||||
return WebmExtractor.this.getElementType(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMasterElementStart(
|
||||
int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) {
|
||||
WebmExtractor.this.onMasterElementStart(
|
||||
id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMasterElementEnd(int id) {
|
||||
WebmExtractor.this.onMasterElementEnd(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIntegerElement(int id, long value) {
|
||||
WebmExtractor.this.onIntegerElement(id, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFloatElement(int id, double value) {
|
||||
WebmExtractor.this.onFloatElement(id, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStringElement(int id, String value) {
|
||||
WebmExtractor.this.onStringElement(id, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBinaryElement(
|
||||
int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,
|
||||
NonBlockingInputStream inputStream) {
|
||||
return WebmExtractor.this.onBinaryElement(
|
||||
id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes, inputStream);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ import com.google.android.exoplayer.chunk.FormatEvaluator;
|
||||
import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation;
|
||||
import com.google.android.exoplayer.chunk.MediaChunk;
|
||||
import com.google.android.exoplayer.chunk.Mp4MediaChunk;
|
||||
import com.google.android.exoplayer.parser.mp4.CodecSpecificDataUtil;
|
||||
import com.google.android.exoplayer.parser.Extractor;
|
||||
import com.google.android.exoplayer.parser.mp4.FragmentedMp4Extractor;
|
||||
import com.google.android.exoplayer.parser.mp4.Track;
|
||||
import com.google.android.exoplayer.parser.mp4.TrackEncryptionBox;
|
||||
@ -35,6 +35,7 @@ import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.Stre
|
||||
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.DataSpec;
|
||||
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.util.Base64;
|
||||
@ -227,7 +228,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
|
||||
}
|
||||
|
||||
private static MediaChunk newMediaChunk(Format formatInfo, Uri uri, String cacheKey,
|
||||
FragmentedMp4Extractor extractor, DataSource dataSource, int chunkIndex,
|
||||
Extractor extractor, DataSource dataSource, int chunkIndex,
|
||||
boolean isLast, long chunkStartTimeUs, long nextChunkStartTimeUs, int trigger) {
|
||||
int nextChunkIndex = isLast ? -1 : chunkIndex + 1;
|
||||
long nextStartTimeUs = isLast ? -1 : nextChunkStartTimeUs;
|
||||
|
@ -20,8 +20,6 @@ import com.google.android.exoplayer.util.ManifestFetcher;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
@ -59,11 +57,7 @@ public final class SmoothStreamingManifestFetcher extends ManifestFetcher<Smooth
|
||||
@Override
|
||||
protected SmoothStreamingManifest parse(InputStream stream, String inputEncoding,
|
||||
String contentId, Uri baseUrl) throws IOException, ParserException {
|
||||
try {
|
||||
return parser.parse(stream, inputEncoding);
|
||||
} catch (XmlPullParserException e) {
|
||||
throw new ParserException(e);
|
||||
}
|
||||
return parser.parse(stream, inputEncoding);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -16,11 +16,11 @@
|
||||
package com.google.android.exoplayer.smoothstreaming;
|
||||
|
||||
import com.google.android.exoplayer.ParserException;
|
||||
import com.google.android.exoplayer.parser.mp4.CodecSpecificDataUtil;
|
||||
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.ProtectionElement;
|
||||
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement;
|
||||
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement;
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
|
||||
|
||||
import android.util.Base64;
|
||||
import android.util.Pair;
|
||||
@ -60,15 +60,18 @@ public class SmoothStreamingManifestParser {
|
||||
* @param inputEncoding The encoding of the input.
|
||||
* @return The parsed manifest.
|
||||
* @throws IOException If a problem occurred reading from the stream.
|
||||
* @throws XmlPullParserException If a problem occurred parsing the stream as xml.
|
||||
* @throws ParserException If a problem occurred parsing the xml as a smooth streaming manifest.
|
||||
*/
|
||||
public SmoothStreamingManifest parse(InputStream inputStream, String inputEncoding) throws
|
||||
XmlPullParserException, IOException, ParserException {
|
||||
XmlPullParser xmlParser = xmlParserFactory.newPullParser();
|
||||
xmlParser.setInput(inputStream, inputEncoding);
|
||||
SmoothStreamMediaParser smoothStreamMediaParser = new SmoothStreamMediaParser(null);
|
||||
return (SmoothStreamingManifest) smoothStreamMediaParser.parse(xmlParser);
|
||||
IOException, ParserException {
|
||||
try {
|
||||
XmlPullParser xmlParser = xmlParserFactory.newPullParser();
|
||||
xmlParser.setInput(inputStream, inputEncoding);
|
||||
SmoothStreamMediaParser smoothStreamMediaParser = new SmoothStreamMediaParser(null);
|
||||
return (SmoothStreamingManifest) smoothStreamMediaParser.parse(xmlParser);
|
||||
} catch (XmlPullParserException e) {
|
||||
throw new ParserException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -102,6 +105,7 @@ public class SmoothStreamingManifestParser {
|
||||
ParserException {
|
||||
String tagName;
|
||||
boolean foundStartTag = false;
|
||||
int skippingElementDepth = 0;
|
||||
while (true) {
|
||||
int eventType = xmlParser.getEventType();
|
||||
switch (eventType) {
|
||||
@ -111,24 +115,35 @@ public class SmoothStreamingManifestParser {
|
||||
foundStartTag = true;
|
||||
parseStartTag(xmlParser);
|
||||
} else if (foundStartTag) {
|
||||
if (handleChildInline(tagName)) {
|
||||
if (skippingElementDepth > 0) {
|
||||
skippingElementDepth++;
|
||||
} else if (handleChildInline(tagName)) {
|
||||
parseStartTag(xmlParser);
|
||||
} else {
|
||||
addChild(newChildParser(this, tagName).parse(xmlParser));
|
||||
ElementParser childElementParser = newChildParser(this, tagName);
|
||||
if (childElementParser == null) {
|
||||
skippingElementDepth = 1;
|
||||
} else {
|
||||
addChild(childElementParser.parse(xmlParser));
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case XmlPullParser.TEXT:
|
||||
if (foundStartTag) {
|
||||
if (foundStartTag && skippingElementDepth == 0) {
|
||||
parseText(xmlParser);
|
||||
}
|
||||
break;
|
||||
case XmlPullParser.END_TAG:
|
||||
if (foundStartTag) {
|
||||
tagName = xmlParser.getName();
|
||||
parseEndTag(xmlParser);
|
||||
if (!handleChildInline(tagName)) {
|
||||
return build();
|
||||
if (skippingElementDepth > 0) {
|
||||
skippingElementDepth--;
|
||||
} else {
|
||||
tagName = xmlParser.getName();
|
||||
parseEndTag(xmlParser);
|
||||
if (!handleChildInline(tagName)) {
|
||||
return build();
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
@ -357,6 +372,7 @@ public class SmoothStreamingManifestParser {
|
||||
|
||||
public static final String KEY_SYSTEM_ID = "SystemID";
|
||||
|
||||
private boolean inProtectionHeader;
|
||||
private UUID uuid;
|
||||
private byte[] initData;
|
||||
|
||||
@ -371,16 +387,25 @@ public class SmoothStreamingManifestParser {
|
||||
|
||||
@Override
|
||||
public void parseStartTag(XmlPullParser parser) {
|
||||
if (!TAG_PROTECTION_HEADER.equals(parser.getName())) {
|
||||
return;
|
||||
if (TAG_PROTECTION_HEADER.equals(parser.getName())) {
|
||||
inProtectionHeader = true;
|
||||
String uuidString = parser.getAttributeValue(null, KEY_SYSTEM_ID);
|
||||
uuid = UUID.fromString(uuidString);
|
||||
}
|
||||
String uuidString = parser.getAttributeValue(null, KEY_SYSTEM_ID);
|
||||
uuid = UUID.fromString(uuidString);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseText(XmlPullParser parser) {
|
||||
initData = Base64.decode(parser.getText(), Base64.DEFAULT);
|
||||
if (inProtectionHeader) {
|
||||
initData = Base64.decode(parser.getText(), Base64.DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseEndTag(XmlPullParser parser) {
|
||||
if (TAG_PROTECTION_HEADER.equals(parser.getName())) {
|
||||
inProtectionHeader = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -579,9 +604,11 @@ public class SmoothStreamingManifestParser {
|
||||
if (type == StreamElement.TYPE_VIDEO) {
|
||||
maxHeight = parseRequiredInt(parser, KEY_MAX_HEIGHT);
|
||||
maxWidth = parseRequiredInt(parser, KEY_MAX_WIDTH);
|
||||
fourCC = parseRequiredString(parser, KEY_FOUR_CC);
|
||||
} else {
|
||||
maxHeight = -1;
|
||||
maxWidth = -1;
|
||||
fourCC = parser.getAttributeValue(null, KEY_FOUR_CC);
|
||||
}
|
||||
|
||||
if (type == StreamElement.TYPE_AUDIO) {
|
||||
@ -590,14 +617,12 @@ public class SmoothStreamingManifestParser {
|
||||
bitPerSample = parseRequiredInt(parser, KEY_BITS_PER_SAMPLE);
|
||||
packetSize = parseRequiredInt(parser, KEY_PACKET_SIZE);
|
||||
audioTag = parseRequiredInt(parser, KEY_AUDIO_TAG);
|
||||
fourCC = parseRequiredString(parser, KEY_FOUR_CC);
|
||||
} else {
|
||||
samplingRate = -1;
|
||||
channels = -1;
|
||||
bitPerSample = -1;
|
||||
packetSize = -1;
|
||||
audioTag = -1;
|
||||
fourCC = parser.getAttributeValue(null, KEY_FOUR_CC);
|
||||
}
|
||||
|
||||
value = parser.getAttributeValue(null, KEY_CODEC_PRIVATE_DATA);
|
||||
|
@ -16,7 +16,7 @@
|
||||
package com.google.android.exoplayer.text;
|
||||
|
||||
import com.google.android.exoplayer.ExoPlaybackException;
|
||||
import com.google.android.exoplayer.FormatHolder;
|
||||
import com.google.android.exoplayer.MediaFormatHolder;
|
||||
import com.google.android.exoplayer.SampleHolder;
|
||||
import com.google.android.exoplayer.SampleSource;
|
||||
import com.google.android.exoplayer.TrackRenderer;
|
||||
@ -64,7 +64,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
|
||||
private final TextRenderer textRenderer;
|
||||
private final SampleSource source;
|
||||
private final SampleHolder sampleHolder;
|
||||
private final FormatHolder formatHolder;
|
||||
private final MediaFormatHolder formatHolder;
|
||||
private final SubtitleParser subtitleParser;
|
||||
|
||||
private int trackIndex;
|
||||
@ -93,7 +93,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
|
||||
this.textRenderer = Assertions.checkNotNull(textRenderer);
|
||||
this.textRendererHandler = textRendererLooper == null ? null : new Handler(textRendererLooper,
|
||||
this);
|
||||
formatHolder = new FormatHolder();
|
||||
formatHolder = new MediaFormatHolder();
|
||||
sampleHolder = new SampleHolder(true);
|
||||
}
|
||||
|
||||
|
@ -41,10 +41,10 @@ public final class CacheDataSource implements DataSource {
|
||||
public interface EventListener {
|
||||
|
||||
/**
|
||||
* Invoked when bytes have been read from {@link #cache} since the last invocation.
|
||||
* Invoked when bytes have been read from the cache.
|
||||
*
|
||||
* @param cacheSizeBytes Current cache size in bytes.
|
||||
* @param cachedBytesRead Total bytes read from {@link #cache} since last report.
|
||||
* @param cachedBytesRead Total bytes read from the cache since this method was last invoked.
|
||||
*/
|
||||
void onCachedBytesRead(long cacheSizeBytes, long cachedBytesRead);
|
||||
|
||||
|
@ -13,9 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer.parser.mp4;
|
||||
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
package com.google.android.exoplayer.util;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.media.MediaCodecInfo.CodecProfileLevel;
|
||||
@ -35,6 +33,10 @@ public final class CodecSpecificDataUtil {
|
||||
96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350
|
||||
};
|
||||
|
||||
private static final int[] AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE = new int[] {
|
||||
0, 1, 2, 3, 4, 5, 6, 8
|
||||
};
|
||||
|
||||
private static final int SPS_NAL_UNIT_TYPE = 7;
|
||||
|
||||
private CodecSpecificDataUtil() {}
|
||||
@ -42,7 +44,7 @@ public final class CodecSpecificDataUtil {
|
||||
/**
|
||||
* Parses an AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1
|
||||
*
|
||||
* @param audioSpecificConfig
|
||||
* @param audioSpecificConfig The AudioSpecificConfig to parse.
|
||||
* @return A pair consisting of the sample rate in Hz and the channel count.
|
||||
*/
|
||||
public static Pair<Integer, Integer> parseAudioSpecificConfig(byte[] audioSpecificConfig) {
|
||||
@ -56,11 +58,27 @@ public final class CodecSpecificDataUtil {
|
||||
return Pair.create(sampleRate, channelCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a simple AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1
|
||||
*
|
||||
* @param audioObjectType The audio object type.
|
||||
* @param sampleRateIndex The sample rate index.
|
||||
* @param channelConfig The channel configuration.
|
||||
* @return The AudioSpecificConfig.
|
||||
*/
|
||||
public static byte[] buildAudioSpecificConfig(int audioObjectType, int sampleRateIndex,
|
||||
int channelConfig) {
|
||||
byte[] audioSpecificConfig = new byte[2];
|
||||
audioSpecificConfig[0] = (byte) ((audioObjectType << 3) & 0xF8 | (sampleRateIndex >> 1) & 0x07);
|
||||
audioSpecificConfig[1] = (byte) ((sampleRateIndex << 7) & 0x80 | (channelConfig << 3) & 0x78);
|
||||
return audioSpecificConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a simple HE-AAC LC AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1
|
||||
*
|
||||
* @param sampleRate The sample rate in Hz.
|
||||
* @param numChannels The number of channels
|
||||
* @param numChannels The number of channels.
|
||||
* @return The AudioSpecificConfig.
|
||||
*/
|
||||
public static byte[] buildAudioSpecificConfig(int sampleRate, int numChannels) {
|
||||
@ -70,10 +88,16 @@ public final class CodecSpecificDataUtil {
|
||||
sampleRateIndex = i;
|
||||
}
|
||||
}
|
||||
int channelConfig = -1;
|
||||
for (int i = 0; i < AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE.length; ++i) {
|
||||
if (numChannels == AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE[i]) {
|
||||
channelConfig = i;
|
||||
}
|
||||
}
|
||||
// The full specification for AudioSpecificConfig is stated in ISO 14496-3 Section 1.6.2.1
|
||||
byte[] csd = new byte[2];
|
||||
csd[0] = (byte) ((2 /* AAC LC */ << 3) | (sampleRateIndex >> 1));
|
||||
csd[1] = (byte) (((sampleRateIndex & 0x1) << 7) | (numChannels << 3));
|
||||
csd[1] = (byte) (((sampleRateIndex & 0x1) << 7) | (channelConfig << 3));
|
||||
return csd;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user