Merge pull request #15 from google/dev

Merge 1.0.11 to master
This commit is contained in:
ojw28 2014-07-18 14:43:59 +01:00
commit 4228f2cfa3
51 changed files with 2753 additions and 1540 deletions

View File

@ -26,6 +26,16 @@ get started.
[Class reference]: http://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer/package-summary.html [Class reference]: http://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer/package-summary.html
## Project branches ##
* The [master][] branch holds the most recent minor release.
* Most development work happens on the [dev][] branch.
* Additional development branches may be established for major features.
[master]: https://github.com/google/ExoPlayer/tree/master
[dev]: https://github.com/google/ExoPlayer/tree/dev
## Using Eclipse ## ## Using Eclipse ##
The repository includes Eclipse projects for both the ExoPlayer library and its The repository includes Eclipse projects for both the ExoPlayer library and its

View File

@ -18,7 +18,7 @@ android {
buildToolsVersion "19.1" buildToolsVersion "19.1"
defaultConfig { defaultConfig {
minSdkVersion 9 minSdkVersion 16
targetSdkVersion 19 targetSdkVersion 19
} }
buildTypes { buildTypes {

View File

@ -52,7 +52,7 @@ public class SampleChooserActivity extends Activity {
sampleAdapter.addAll((Object[]) Samples.SIMPLE); sampleAdapter.addAll((Object[]) Samples.SIMPLE);
sampleAdapter.add(new Header("YouTube DASH")); sampleAdapter.add(new Header("YouTube DASH"));
sampleAdapter.addAll((Object[]) Samples.YOUTUBE_DASH_MP4); sampleAdapter.addAll((Object[]) Samples.YOUTUBE_DASH_MP4);
sampleAdapter.add(new Header("Widevine DASH GTS")); sampleAdapter.add(new Header("Widevine GTS DASH"));
sampleAdapter.addAll((Object[]) Samples.WIDEVINE_GTS); sampleAdapter.addAll((Object[]) Samples.WIDEVINE_GTS);
sampleAdapter.add(new Header("SmoothStreaming")); sampleAdapter.add(new Header("SmoothStreaming"));
sampleAdapter.addAll((Object[]) Samples.SMOOTHSTREAMING); sampleAdapter.addAll((Object[]) Samples.SMOOTHSTREAMING);

View File

@ -91,7 +91,7 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener
} }
@Override @Override
public void onLoadStarted(int sourceId, int formatId, int trigger, boolean isInitialization, public void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization,
int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes) { int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes) {
loadStartTimeMs[sourceId] = SystemClock.elapsedRealtime(); loadStartTimeMs[sourceId] = SystemClock.elapsedRealtime();
if (VerboseLogUtil.isTagEnabled(TAG)) { if (VerboseLogUtil.isTagEnabled(TAG)) {
@ -110,13 +110,13 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener
} }
@Override @Override
public void onVideoFormatEnabled(int formatId, int trigger, int mediaTimeMs) { public void onVideoFormatEnabled(String formatId, int trigger, int mediaTimeMs) {
Log.d(TAG, "videoFormat [" + getSessionTimeString() + ", " + formatId + ", " + Log.d(TAG, "videoFormat [" + getSessionTimeString() + ", " + formatId + ", " +
Integer.toString(trigger) + "]"); Integer.toString(trigger) + "]");
} }
@Override @Override
public void onAudioFormatEnabled(int formatId, int trigger, int mediaTimeMs) { public void onAudioFormatEnabled(String formatId, int trigger, int mediaTimeMs) {
Log.d(TAG, "audioFormat [" + getSessionTimeString() + ", " + formatId + ", " + Log.d(TAG, "audioFormat [" + getSessionTimeString() + ", " + formatId + ", " +
Integer.toString(trigger) + "]"); Integer.toString(trigger) + "]");
} }

View File

@ -160,8 +160,7 @@ public class DashVodRendererBuilder implements RendererBuilder,
} }
// Build the video renderer. // Build the video renderer.
DataSource videoDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES, DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
bandwidthMeter);
ChunkSource videoChunkSource; ChunkSource videoChunkSource;
String mimeType = videoRepresentations[0].format.mimeType; String mimeType = videoRepresentations[0].format.mimeType;
if (mimeType.equals(MimeTypes.VIDEO_MP4)) { if (mimeType.equals(MimeTypes.VIDEO_MP4)) {
@ -192,8 +191,7 @@ public class DashVodRendererBuilder implements RendererBuilder,
audioChunkSource = null; audioChunkSource = null;
audioRenderer = null; audioRenderer = null;
} else { } else {
DataSource audioDataSource = new HttpDataSource(userAgent, DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
HttpDataSource.REJECT_PAYWALL_TYPES, bandwidthMeter);
audioTrackNames = new String[audioRepresentationsList.size()]; audioTrackNames = new String[audioRepresentationsList.size()];
ChunkSource[] audioChunkSources = new ChunkSource[audioRepresentationsList.size()]; ChunkSource[] audioChunkSources = new ChunkSource[audioRepresentationsList.size()];
FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator(); FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator();

View File

@ -118,11 +118,11 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
* A listener for debugging information. * A listener for debugging information.
*/ */
public interface InfoListener { public interface InfoListener {
void onVideoFormatEnabled(int formatId, int trigger, int mediaTimeMs); void onVideoFormatEnabled(String formatId, int trigger, int mediaTimeMs);
void onAudioFormatEnabled(int formatId, int trigger, int mediaTimeMs); void onAudioFormatEnabled(String formatId, int trigger, int mediaTimeMs);
void onDroppedFrames(int count, long elapsed); void onDroppedFrames(int count, long elapsed);
void onBandwidthSample(int elapsedMs, long bytes, long bandwidthEstimate); void onBandwidthSample(int elapsedMs, long bytes, long bandwidthEstimate);
void onLoadStarted(int sourceId, int formatId, int trigger, boolean isInitialization, void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization,
int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes); int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes);
void onLoadCompleted(int sourceId); void onLoadCompleted(int sourceId);
} }
@ -398,7 +398,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
} }
@Override @Override
public void onDownstreamFormatChanged(int sourceId, int formatId, int trigger, int mediaTimeMs) { public void onDownstreamFormatChanged(int sourceId, String formatId, int trigger,
int mediaTimeMs) {
if (infoListener == null) { if (infoListener == null) {
return; return;
} }
@ -469,7 +470,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
} }
@Override @Override
public void onLoadStarted(int sourceId, int formatId, int trigger, boolean isInitialization, public void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization,
int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes) { int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes) {
if (infoListener != null) { if (infoListener != null) {
infoListener.onLoadStarted(sourceId, formatId, trigger, isInitialization, mediaStartTimeMs, infoListener.onLoadStarted(sourceId, formatId, trigger, isInitialization, mediaStartTimeMs,

View File

@ -150,8 +150,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
} }
// Build the video renderer. // Build the video renderer.
DataSource videoDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES, DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
bandwidthMeter);
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(url, manifest, ChunkSource videoChunkSource = new SmoothStreamingChunkSource(url, manifest,
videoStreamElementIndex, videoTrackIndices, videoDataSource, videoStreamElementIndex, videoTrackIndices, videoDataSource,
new AdaptiveEvaluator(bandwidthMeter)); new AdaptiveEvaluator(bandwidthMeter));
@ -173,8 +172,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
} else { } else {
audioTrackNames = new String[audioStreamElementCount]; audioTrackNames = new String[audioStreamElementCount];
ChunkSource[] audioChunkSources = new ChunkSource[audioStreamElementCount]; ChunkSource[] audioChunkSources = new ChunkSource[audioStreamElementCount];
DataSource audioDataSource = new HttpDataSource(userAgent, DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
HttpDataSource.REJECT_PAYWALL_TYPES, bandwidthMeter);
FormatEvaluator audioFormatEvaluator = new FormatEvaluator.FixedEvaluator(); FormatEvaluator audioFormatEvaluator = new FormatEvaluator.FixedEvaluator();
audioStreamElementCount = 0; audioStreamElementCount = 0;
for (int i = 0; i < manifest.streamElements.length; i++) { for (int i = 0; i < manifest.streamElements.length; i++) {
@ -204,8 +202,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
} else { } else {
textTrackNames = new String[textStreamElementCount]; textTrackNames = new String[textStreamElementCount];
ChunkSource[] textChunkSources = new ChunkSource[textStreamElementCount]; ChunkSource[] textChunkSources = new ChunkSource[textStreamElementCount];
DataSource ttmlDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES, DataSource ttmlDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
bandwidthMeter);
FormatEvaluator ttmlFormatEvaluator = new FormatEvaluator.FixedEvaluator(); FormatEvaluator ttmlFormatEvaluator = new FormatEvaluator.FixedEvaluator();
textStreamElementCount = 0; textStreamElementCount = 0;
for (int i = 0; i < manifest.streamElements.length; i++) { for (int i = 0; i < manifest.streamElements.length; i++) {

View File

@ -115,8 +115,7 @@ import java.util.ArrayList;
videoRepresentationsList.toArray(videoRepresentations); videoRepresentationsList.toArray(videoRepresentations);
// Build the video renderer. // Build the video renderer.
DataSource videoDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES, DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
bandwidthMeter);
ChunkSource videoChunkSource = new DashMp4ChunkSource(videoDataSource, ChunkSource videoChunkSource = new DashMp4ChunkSource(videoDataSource,
new AdaptiveEvaluator(bandwidthMeter), videoRepresentations); new AdaptiveEvaluator(bandwidthMeter), videoRepresentations);
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
@ -125,8 +124,7 @@ import java.util.ArrayList;
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50); MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50);
// Build the audio renderer. // Build the audio renderer.
DataSource audioDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES, DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
bandwidthMeter);
ChunkSource audioChunkSource = new DashMp4ChunkSource(audioDataSource, ChunkSource audioChunkSource = new DashMp4ChunkSource(audioDataSource,
new FormatEvaluator.FixedEvaluator(), audioRepresentation); new FormatEvaluator.FixedEvaluator(), audioRepresentation);
SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,

View File

@ -115,8 +115,7 @@ import java.util.ArrayList;
} }
// Build the video renderer. // Build the video renderer.
DataSource videoDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES, DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
bandwidthMeter);
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(url, manifest, ChunkSource videoChunkSource = new SmoothStreamingChunkSource(url, manifest,
videoStreamElementIndex, videoTrackIndices, videoDataSource, videoStreamElementIndex, videoTrackIndices, videoDataSource,
new AdaptiveEvaluator(bandwidthMeter)); new AdaptiveEvaluator(bandwidthMeter));
@ -126,8 +125,7 @@ import java.util.ArrayList;
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50); MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50);
// Build the audio renderer. // Build the audio renderer.
DataSource audioDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES, DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
bandwidthMeter);
ChunkSource audioChunkSource = new SmoothStreamingChunkSource(url, manifest, ChunkSource audioChunkSource = new SmoothStreamingChunkSource(url, manifest,
audioStreamElementIndex, new int[] {0}, audioDataSource, audioStreamElementIndex, new int[] {0}, audioDataSource,
new FormatEvaluator.FixedEvaluator()); new FormatEvaluator.FixedEvaluator());

View File

@ -26,7 +26,7 @@ public class ExoPlayerLibraryInfo {
/** /**
* The version of the library, expressed as a string. * The version of the library, expressed as a string.
*/ */
public static final String VERSION = "1.0.10"; public static final String VERSION = "1.0.11";
/** /**
* The version of the library, expressed as an integer. * The version of the library, expressed as an integer.

View File

@ -95,6 +95,10 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000; private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000;
private static final int MIN_TIMESTAMP_SAMPLE_INTERVAL_US = 500000; private static final int MIN_TIMESTAMP_SAMPLE_INTERVAL_US = 500000;
private static final int START_NOT_SET = 0;
private static final int START_IN_SYNC = 1;
private static final int START_NEED_SYNC = 2;
private final EventListener eventListener; private final EventListener eventListener;
private final ConditionVariable audioTrackReleasingConditionVariable; private final ConditionVariable audioTrackReleasingConditionVariable;
private final AudioTimestampCompat audioTimestampCompat; private final AudioTimestampCompat audioTimestampCompat;
@ -119,7 +123,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
private Method audioTrackGetLatencyMethod; private Method audioTrackGetLatencyMethod;
private int audioSessionId; private int audioSessionId;
private long submittedBytes; private long submittedBytes;
private boolean audioTrackStartMediaTimeSet; private int audioTrackStartMediaTimeState;
private long audioTrackStartMediaTimeUs; private long audioTrackStartMediaTimeUs;
private long audioTrackResumeSystemTimeUs; private long audioTrackResumeSystemTimeUs;
private long lastReportedCurrentPositionUs; private long lastReportedCurrentPositionUs;
@ -363,7 +367,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
lastRawPlaybackHeadPosition = 0; lastRawPlaybackHeadPosition = 0;
rawPlaybackHeadWrapCount = 0; rawPlaybackHeadWrapCount = 0;
audioTrackStartMediaTimeUs = 0; audioTrackStartMediaTimeUs = 0;
audioTrackStartMediaTimeSet = false; audioTrackStartMediaTimeState = START_NOT_SET;
resetSyncParams(); resetSyncParams();
int playState = audioTrack.getPlayState(); int playState = audioTrack.getPlayState();
if (playState == AudioTrack.PLAYSTATE_PLAYING) { if (playState == AudioTrack.PLAYSTATE_PLAYING) {
@ -429,7 +433,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
protected long getCurrentPositionUs() { protected long getCurrentPositionUs() {
long systemClockUs = System.nanoTime() / 1000; long systemClockUs = System.nanoTime() / 1000;
long currentPositionUs; long currentPositionUs;
if (audioTrack == null || !audioTrackStartMediaTimeSet) { if (audioTrack == null || audioTrackStartMediaTimeState == START_NOT_SET) {
// The AudioTrack hasn't started. // The AudioTrack hasn't started.
currentPositionUs = super.getCurrentPositionUs(); currentPositionUs = super.getCurrentPositionUs();
} else if (audioTimestampSet) { } else if (audioTimestampSet) {
@ -461,7 +465,8 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
} }
private void maybeSampleSyncParams() { private void maybeSampleSyncParams() {
if (audioTrack == null || !audioTrackStartMediaTimeSet || getState() != STATE_STARTED) { if (audioTrack == null || audioTrackStartMediaTimeState == START_NOT_SET
|| getState() != STATE_STARTED) {
// The AudioTrack isn't playing. // The AudioTrack isn't playing.
return; return;
} }
@ -549,25 +554,40 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
@Override @Override
protected boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer, protected boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer,
MediaCodec.BufferInfo bufferInfo, int bufferIndex) throws ExoPlaybackException { MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip)
throws ExoPlaybackException {
if (shouldSkip) {
codec.releaseOutputBuffer(bufferIndex, false);
codecCounters.skippedOutputBufferCount++;
if (audioTrackStartMediaTimeState == START_IN_SYNC) {
// Skipping the sample will push track time out of sync. We'll need to sync again.
audioTrackStartMediaTimeState = START_NEED_SYNC;
}
return true;
}
if (temporaryBufferSize == 0) { if (temporaryBufferSize == 0) {
// This is the first time we've seen this {@code buffer}. // This is the first time we've seen this {@code buffer}.
// Note: presentationTimeUs corresponds to the end of the sample, not the start. // Note: presentationTimeUs corresponds to the end of the sample, not the start.
long bufferStartTime = bufferInfo.presentationTimeUs - long bufferStartTime = bufferInfo.presentationTimeUs -
framesToDurationUs(bufferInfo.size / frameSize); framesToDurationUs(bufferInfo.size / frameSize);
if (!audioTrackStartMediaTimeSet) { if (audioTrackStartMediaTimeState == START_NOT_SET) {
audioTrackStartMediaTimeUs = Math.max(0, bufferStartTime); audioTrackStartMediaTimeUs = Math.max(0, bufferStartTime);
audioTrackStartMediaTimeSet = true; audioTrackStartMediaTimeState = START_IN_SYNC;
} else { } else {
// Sanity check that bufferStartTime is consistent with the expected value. // Sanity check that bufferStartTime is consistent with the expected value.
long expectedBufferStartTime = audioTrackStartMediaTimeUs + long expectedBufferStartTime = audioTrackStartMediaTimeUs +
framesToDurationUs(submittedBytes / frameSize); framesToDurationUs(submittedBytes / frameSize);
if (Math.abs(expectedBufferStartTime - bufferStartTime) > 200000) { if (audioTrackStartMediaTimeState == START_IN_SYNC
&& Math.abs(expectedBufferStartTime - bufferStartTime) > 200000) {
Log.e(TAG, "Discontinuity detected [expected " + expectedBufferStartTime + ", got " + Log.e(TAG, "Discontinuity detected [expected " + expectedBufferStartTime + ", got " +
bufferStartTime + "]"); bufferStartTime + "]");
// Adjust audioTrackStartMediaTimeUs to compensate for the discontinuity. Also reset audioTrackStartMediaTimeState = START_NEED_SYNC;
// lastReportedCurrentPositionUs to allow time to jump backwards if it really wants to. }
if (audioTrackStartMediaTimeState == START_NEED_SYNC) {
// Adjust audioTrackStartMediaTimeUs to be consistent with the current buffer's start
// time and the number of bytes submitted. Also reset lastReportedCurrentPositionUs to
// allow time to jump backwards if it really wants to.
audioTrackStartMediaTimeUs += (bufferStartTime - expectedBufferStartTime); audioTrackStartMediaTimeUs += (bufferStartTime - expectedBufferStartTime);
lastReportedCurrentPositionUs = 0; lastReportedCurrentPositionUs = 0;
} }

View File

@ -391,7 +391,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
while (result == SampleSource.SAMPLE_READ && currentPositionUs <= timeUs) { while (result == SampleSource.SAMPLE_READ && currentPositionUs <= timeUs) {
result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false); result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false);
if (result == SampleSource.SAMPLE_READ) { if (result == SampleSource.SAMPLE_READ) {
if (!sampleHolder.decodeOnly) {
currentPositionUs = sampleHolder.timeUs; currentPositionUs = sampleHolder.timeUs;
}
codecCounters.discardedSamplesCount++; codecCounters.discardedSamplesCount++;
} else if (result == SampleSource.FORMAT_READ) { } else if (result == SampleSource.FORMAT_READ) {
onInputFormatChanged(formatHolder); onInputFormatChanged(formatHolder);
@ -677,15 +679,15 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
return false; return false;
} }
if (decodeOnlyPresentationTimestamps.remove(outputBufferInfo.presentationTimeUs)) { boolean decodeOnly = decodeOnlyPresentationTimestamps.contains(
codec.releaseOutputBuffer(outputIndex, false); outputBufferInfo.presentationTimeUs);
outputIndex = -1;
return true;
}
if (processOutputBuffer(timeUs, codec, outputBuffers[outputIndex], outputBufferInfo, if (processOutputBuffer(timeUs, codec, outputBuffers[outputIndex], outputBufferInfo,
outputIndex)) { outputIndex, decodeOnly)) {
if (decodeOnly) {
decodeOnlyPresentationTimestamps.remove(outputBufferInfo.presentationTimeUs);
} else {
currentPositionUs = outputBufferInfo.presentationTimeUs; currentPositionUs = outputBufferInfo.presentationTimeUs;
}
outputIndex = -1; outputIndex = -1;
return true; return true;
} }
@ -701,7 +703,8 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
* @throws ExoPlaybackException If an error occurs processing the output buffer. * @throws ExoPlaybackException If an error occurs processing the output buffer.
*/ */
protected abstract boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer, protected abstract boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer,
MediaCodec.BufferInfo bufferInfo, int bufferIndex) throws ExoPlaybackException; MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip)
throws ExoPlaybackException;
/** /**
* Returns the name of the secure variant of a given decoder. * Returns the name of the secure variant of a given decoder.

View File

@ -29,7 +29,7 @@ import android.view.Surface;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
/** /**
* Decodes and renders video using {@MediaCodec}. * Decodes and renders video using {@link MediaCodec}.
*/ */
@TargetApi(16) @TargetApi(16)
public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
@ -338,7 +338,12 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
@Override @Override
protected boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer, protected boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer,
MediaCodec.BufferInfo bufferInfo, int bufferIndex) { MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip) {
if (shouldSkip) {
skipOutputBuffer(codec, bufferIndex);
return true;
}
long earlyUs = bufferInfo.presentationTimeUs - timeUs; long earlyUs = bufferInfo.presentationTimeUs - timeUs;
if (earlyUs < -30000) { if (earlyUs < -30000) {
// We're more than 30ms late rendering the frame. // We're more than 30ms late rendering the frame.
@ -371,6 +376,13 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
return false; return false;
} }
private void skipOutputBuffer(MediaCodec codec, int bufferIndex) {
TraceUtil.beginSection("skipVideoBuffer");
codec.releaseOutputBuffer(bufferIndex, false);
TraceUtil.endSection();
codecCounters.skippedOutputBufferCount++;
}
private void dropOutputBuffer(MediaCodec codec, int bufferIndex) { private void dropOutputBuffer(MediaCodec codec, int bufferIndex) {
TraceUtil.beginSection("dropVideoBuffer"); TraceUtil.beginSection("dropVideoBuffer");
codec.releaseOutputBuffer(bufferIndex, false); codec.releaseOutputBuffer(bufferIndex, false);

View File

@ -54,8 +54,8 @@ public interface SampleSource {
* Prepares the source. * Prepares the source.
* <p> * <p>
* Preparation may require reading from the data source (e.g. to determine the available tracks * Preparation may require reading from the data source (e.g. to determine the available tracks
* and formats). If insufficient data is available then the call will return rather than block. * and formats). If insufficient data is available then the call will return {@code false} rather
* The method can be called repeatedly until the return value indicates success. * than block. The method can be called repeatedly until the return value indicates success.
* *
* @return True if the source was prepared successfully, false otherwise. * @return True if the source was prepared successfully, false otherwise.
* @throws IOException If an error occurred preparing the source. * @throws IOException If an error occurred preparing the source.

View File

@ -59,7 +59,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
* load is for initialization data. * load is for initialization data.
* @param totalBytes The length of the data being loaded in bytes. * @param totalBytes The length of the data being loaded in bytes.
*/ */
void onLoadStarted(int sourceId, int formatId, int trigger, boolean isInitialization, void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization,
int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes); int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes);
/** /**
@ -126,7 +126,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
* {@link ChunkSource}. * {@link ChunkSource}.
* @param mediaTimeMs The media time at which the change occurred. * @param mediaTimeMs The media time at which the change occurred.
*/ */
void onDownstreamFormatChanged(int sourceId, int formatId, int trigger, int mediaTimeMs); void onDownstreamFormatChanged(int sourceId, String formatId, int trigger, int mediaTimeMs);
} }
@ -160,6 +160,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
private int currentLoadableExceptionCount; private int currentLoadableExceptionCount;
private long currentLoadableExceptionTimestamp; private long currentLoadableExceptionTimestamp;
private MediaFormat downstreamMediaFormat;
private volatile Format downstreamFormat; private volatile Format downstreamFormat;
public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl, public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl,
@ -221,6 +222,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
chunkSource.enable(); chunkSource.enable();
loadControl.register(this, bufferSizeContribution); loadControl.register(this, bufferSizeContribution);
downstreamFormat = null; downstreamFormat = null;
downstreamMediaFormat = null;
downstreamPositionUs = timeUs; downstreamPositionUs = timeUs;
lastSeekPositionUs = timeUs; lastSeekPositionUs = timeUs;
restartFrom(timeUs); restartFrom(timeUs);
@ -288,21 +290,30 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
return readData(track, playbackPositionUs, formatHolder, sampleHolder, false); return readData(track, playbackPositionUs, formatHolder, sampleHolder, false);
} else if (mediaChunk.isLastChunk()) { } else if (mediaChunk.isLastChunk()) {
return END_OF_STREAM; return END_OF_STREAM;
} else { }
IOException chunkSourceException = chunkSource.getError(); IOException chunkSourceException = chunkSource.getError();
if (chunkSourceException != null) { if (chunkSourceException != null) {
throw chunkSourceException; throw chunkSourceException;
} }
return NOTHING_READ; return NOTHING_READ;
} }
} else if (downstreamFormat == null || downstreamFormat.id != mediaChunk.format.id) {
if (downstreamFormat == null || !downstreamFormat.equals(mediaChunk.format)) {
notifyDownstreamFormatChanged(mediaChunk.format.id, mediaChunk.trigger, notifyDownstreamFormatChanged(mediaChunk.format.id, mediaChunk.trigger,
mediaChunk.startTimeUs); mediaChunk.startTimeUs);
MediaFormat format = mediaChunk.getMediaFormat();
chunkSource.getMaxVideoDimensions(format);
formatHolder.format = format;
formatHolder.drmInitData = mediaChunk.getPsshInfo();
downstreamFormat = mediaChunk.format; downstreamFormat = mediaChunk.format;
}
if (!mediaChunk.prepare()) {
return NOTHING_READ;
}
MediaFormat mediaFormat = mediaChunk.getMediaFormat();
if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormat)) {
chunkSource.getMaxVideoDimensions(mediaFormat);
formatHolder.format = mediaFormat;
formatHolder.drmInitData = mediaChunk.getPsshInfo();
downstreamMediaFormat = mediaFormat;
return FORMAT_READ; return FORMAT_READ;
} }
@ -430,6 +441,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
currentLoadableExceptionCount++; currentLoadableExceptionCount++;
currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime(); currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
notifyUpstreamError(e); notifyUpstreamError(e);
chunkSource.onChunkLoadError(currentLoadableHolder.chunk, e);
updateLoadControl(); updateLoadControl();
} }
@ -653,7 +665,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
return (int) (timeUs / 1000); return (int) (timeUs / 1000);
} }
private void notifyLoadStarted(final int formatId, final int trigger, private void notifyLoadStarted(final String formatId, final int trigger,
final boolean isInitialization, final long mediaStartTimeUs, final long mediaEndTimeUs, final boolean isInitialization, final long mediaStartTimeUs, final long mediaEndTimeUs,
final long totalBytes) { final long totalBytes) {
if (eventHandler != null && eventListener != null) { if (eventHandler != null && eventListener != null) {
@ -724,7 +736,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
} }
} }
private void notifyDownstreamFormatChanged(final int formatId, final int trigger, private void notifyDownstreamFormatChanged(final String formatId, final int trigger,
final long mediaTimeUs) { final long mediaTimeUs) {
if (eventHandler != null && eventListener != null) { if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() { eventHandler.post(new Runnable() {

View File

@ -58,7 +58,7 @@ public interface ChunkSource {
* *
* @param queue A representation of the currently buffered {@link MediaChunk}s. * @param queue A representation of the currently buffered {@link MediaChunk}s.
*/ */
void disable(List<MediaChunk> queue); void disable(List<? extends MediaChunk> queue);
/** /**
* Indicates to the source that it should still be checking for updates to the stream. * Indicates to the source that it should still be checking for updates to the stream.
@ -100,4 +100,13 @@ public interface ChunkSource {
*/ */
IOException getError(); IOException getError();
/**
* Invoked when the {@link ChunkSampleSource} encounters an error loading a chunk obtained from
* this source.
*
* @param chunk The chunk whose load encountered the error.
* @param e The error.
*/
void onChunkLoadError(Chunk chunk, Exception e);
} }

View File

@ -15,12 +15,14 @@
*/ */
package com.google.android.exoplayer.chunk; package com.google.android.exoplayer.chunk;
import com.google.android.exoplayer.util.Assertions;
import java.util.Comparator; import java.util.Comparator;
/** /**
* A format definition for streams. * A format definition for streams.
*/ */
public final class Format { public class Format {
/** /**
* Sorts {@link Format} objects in order of decreasing bandwidth. * Sorts {@link Format} objects in order of decreasing bandwidth.
@ -29,7 +31,7 @@ public final class Format {
@Override @Override
public int compare(Format a, Format b) { public int compare(Format a, Format b) {
return b.bandwidth - a.bandwidth; return b.bitrate - a.bitrate;
} }
} }
@ -37,7 +39,7 @@ public final class Format {
/** /**
* An identifier for the format. * An identifier for the format.
*/ */
public final int id; public final String id;
/** /**
* The mime type of the format. * The mime type of the format.
@ -65,8 +67,16 @@ public final class Format {
public final int audioSamplingRate; public final int audioSamplingRate;
/** /**
* The average bandwidth in bytes per second. * The average bandwidth in bits per second.
*/ */
public final int bitrate;
/**
* The average bandwidth in bytes per second.
*
* @deprecated Use {@link #bitrate}. However note that the units of measurement are different.
*/
@Deprecated
public final int bandwidth; public final int bandwidth;
/** /**
@ -76,17 +86,38 @@ public final class Format {
* @param height The height of the video in pixels, or -1 for non-video formats. * @param height The height of the video in pixels, or -1 for non-video formats.
* @param numChannels The number of audio channels, or -1 for non-audio formats. * @param numChannels The number of audio channels, or -1 for non-audio formats.
* @param audioSamplingRate The audio sampling rate in Hz, or -1 for non-audio formats. * @param audioSamplingRate The audio sampling rate in Hz, or -1 for non-audio formats.
* @param bandwidth The average bandwidth of the format in bytes per second. * @param bitrate The average bandwidth of the format in bits per second.
*/ */
public Format(int id, String mimeType, int width, int height, int numChannels, public Format(String id, String mimeType, int width, int height, int numChannels,
int audioSamplingRate, int bandwidth) { int audioSamplingRate, int bitrate) {
this.id = id; this.id = Assertions.checkNotNull(id);
this.mimeType = mimeType; this.mimeType = mimeType;
this.width = width; this.width = width;
this.height = height; this.height = height;
this.numChannels = numChannels; this.numChannels = numChannels;
this.audioSamplingRate = audioSamplingRate; this.audioSamplingRate = audioSamplingRate;
this.bandwidth = bandwidth; this.bitrate = bitrate;
this.bandwidth = bitrate / 8;
}
@Override
public int hashCode() {
return id.hashCode();
}
/**
* Implements equality based on {@link #id} only.
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Format other = (Format) obj;
return other.id.equals(id);
} }
} }

View File

@ -146,7 +146,7 @@ public interface FormatEvaluator {
public void evaluate(List<? extends MediaChunk> queue, long playbackPositionUs, public void evaluate(List<? extends MediaChunk> queue, long playbackPositionUs,
Format[] formats, Evaluation evaluation) { Format[] formats, Evaluation evaluation) {
Format newFormat = formats[random.nextInt(formats.length)]; Format newFormat = formats[random.nextInt(formats.length)];
if (evaluation.format != null && evaluation.format.id != newFormat.id) { if (evaluation.format != null && !evaluation.format.id.equals(newFormat.id)) {
evaluation.trigger = TRIGGER_ADAPTIVE; evaluation.trigger = TRIGGER_ADAPTIVE;
} }
evaluation.format = newFormat; evaluation.format = newFormat;
@ -236,8 +236,8 @@ public interface FormatEvaluator {
: queue.get(queue.size() - 1).endTimeUs - playbackPositionUs; : queue.get(queue.size() - 1).endTimeUs - playbackPositionUs;
Format current = evaluation.format; Format current = evaluation.format;
Format ideal = determineIdealFormat(formats, bandwidthMeter.getEstimate()); Format ideal = determineIdealFormat(formats, bandwidthMeter.getEstimate());
boolean isHigher = ideal != null && current != null && ideal.bandwidth > current.bandwidth; boolean isHigher = ideal != null && current != null && ideal.bitrate > current.bitrate;
boolean isLower = ideal != null && current != null && ideal.bandwidth < current.bandwidth; boolean isLower = ideal != null && current != null && ideal.bitrate < current.bitrate;
if (isHigher) { if (isHigher) {
if (bufferedDurationUs < minDurationForQualityIncreaseUs) { if (bufferedDurationUs < minDurationForQualityIncreaseUs) {
// The ideal format is a higher quality, but we have insufficient buffer to // The ideal format is a higher quality, but we have insufficient buffer to
@ -247,11 +247,11 @@ public interface FormatEvaluator {
// We're switching from an SD stream to a stream of higher resolution. Consider // We're switching from an SD stream to a stream of higher resolution. Consider
// discarding already buffered media chunks. Specifically, discard media chunks starting // discarding already buffered media chunks. Specifically, discard media chunks starting
// from the first one that is of lower bandwidth, lower resolution and that is not HD. // from the first one that is of lower bandwidth, lower resolution and that is not HD.
for (int i = 0; i < queue.size(); i++) { for (int i = 1; i < queue.size(); i++) {
MediaChunk thisChunk = queue.get(i); MediaChunk thisChunk = queue.get(i);
long durationBeforeThisSegmentUs = thisChunk.startTimeUs - playbackPositionUs; long durationBeforeThisSegmentUs = thisChunk.startTimeUs - playbackPositionUs;
if (durationBeforeThisSegmentUs >= minDurationToRetainAfterDiscardUs if (durationBeforeThisSegmentUs >= minDurationToRetainAfterDiscardUs
&& thisChunk.format.bandwidth < ideal.bandwidth && thisChunk.format.bitrate < ideal.bitrate
&& thisChunk.format.height < ideal.height && thisChunk.format.height < ideal.height
&& thisChunk.format.height < 720 && thisChunk.format.height < 720
&& thisChunk.format.width < 1280) { && thisChunk.format.width < 1280) {
@ -280,7 +280,7 @@ public interface FormatEvaluator {
long effectiveBandwidth = computeEffectiveBandwidthEstimate(bandwidthEstimate); long effectiveBandwidth = computeEffectiveBandwidthEstimate(bandwidthEstimate);
for (int i = 0; i < formats.length; i++) { for (int i = 0; i < formats.length; i++) {
Format format = formats[i]; Format format = formats[i];
if (format.bandwidth <= effectiveBandwidth) { if ((format.bitrate / 8) <= effectiveBandwidth) {
return format; return format;
} }
} }

View File

@ -73,9 +73,7 @@ public abstract class MediaChunk extends Chunk {
/** /**
* Seeks to the beginning of the chunk. * Seeks to the beginning of the chunk.
*/ */
public final void seekToStart() { public abstract void seekToStart();
seekTo(startTimeUs, false);
}
/** /**
* Seeks to the specified position within the chunk. * Seeks to the specified position within the chunk.
@ -89,8 +87,22 @@ public abstract class MediaChunk extends Chunk {
*/ */
public abstract boolean seekTo(long positionUs, boolean allowNoop); public abstract boolean seekTo(long positionUs, boolean allowNoop);
/**
* Prepares the chunk for reading. Does nothing if the chunk is already prepared.
* <p>
* Preparation may require consuming some of the chunk. If the data is not yet available then
* this method will return {@code false} rather than block. The method can be called repeatedly
* until the return value indicates success.
*
* @return True if the chunk was prepared. False otherwise.
* @throws ParserException If an error occurs parsing the media data.
*/
public abstract boolean prepare() throws ParserException;
/** /**
* Reads the next media sample from the chunk. * Reads the next media sample from the chunk.
* <p>
* Should only be called after the chunk has been successfully prepared.
* *
* @param holder A holder to store the read sample. * @param holder A holder to store the read sample.
* @return True if a sample was read. False if more data is still required. * @return True if a sample was read. False if more data is still required.
@ -101,6 +113,8 @@ public abstract class MediaChunk extends Chunk {
/** /**
* Returns the media format of the samples contained within this chunk. * Returns the media format of the samples contained within this chunk.
* <p>
* Should only be called after the chunk has been successfully prepared.
* *
* @return The sample media format. * @return The sample media format.
*/ */
@ -108,6 +122,8 @@ public abstract class MediaChunk extends Chunk {
/** /**
* Returns the pssh information associated with the chunk. * Returns the pssh information associated with the chunk.
* <p>
* Should only be called after the chunk has been successfully prepared.
* *
* @return The pssh information. * @return The pssh information.
*/ */

View File

@ -33,27 +33,43 @@ import java.util.UUID;
public final class Mp4MediaChunk extends MediaChunk { public final class Mp4MediaChunk extends MediaChunk {
private final FragmentedMp4Extractor extractor; private final FragmentedMp4Extractor extractor;
private final boolean maybeSelfContained;
private final long sampleOffsetUs; private final long sampleOffsetUs;
private boolean prepared;
private MediaFormat mediaFormat;
private Map<UUID, byte[]> psshInfo;
/** /**
* @param dataSource A {@link DataSource} for loading the data. * @param dataSource A {@link DataSource} for loading the data.
* @param dataSpec Defines the data to be loaded. * @param dataSpec Defines the data to be loaded.
* @param format The format of the stream to which this chunk belongs. * @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 trigger The reason for this chunk being selected.
* @param startTimeUs The start time of the media contained by the chunk, in microseconds. * @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 endTimeUs The end time of the media contained by the chunk, in microseconds.
* @param sampleOffsetUs An offset to subtract from the sample timestamps parsed by the extractor.
* @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk. * @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
* @param extractor The extractor that will be used to extract the samples.
* @param maybeSelfContained Set to true if this chunk might be self contained, meaning it might
* contain a moov atom defining the media format of the chunk. This parameter can always be
* safely set to true. Setting to false where the chunk is known to not be self contained may
* improve startup latency.
* @param sampleOffsetUs An offset to subtract from the sample timestamps parsed by the extractor.
*/ */
public Mp4MediaChunk(DataSource dataSource, DataSpec dataSpec, Format format, public Mp4MediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
int trigger, FragmentedMp4Extractor extractor, long startTimeUs, long endTimeUs, int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex,
long sampleOffsetUs, int nextChunkIndex) { FragmentedMp4Extractor extractor, boolean maybeSelfContained, long sampleOffsetUs) {
super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex); super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex);
this.extractor = extractor; this.extractor = extractor;
this.maybeSelfContained = maybeSelfContained;
this.sampleOffsetUs = sampleOffsetUs; this.sampleOffsetUs = sampleOffsetUs;
} }
@Override
public void seekToStart() {
extractor.seekTo(0, false);
resetReadPosition();
}
@Override @Override
public boolean seekTo(long positionUs, boolean allowNoop) { public boolean seekTo(long positionUs, boolean allowNoop) {
long seekTimeUs = positionUs + sampleOffsetUs; long seekTimeUs = positionUs + sampleOffsetUs;
@ -64,6 +80,29 @@ public final class Mp4MediaChunk extends MediaChunk {
return isDiscontinuous; return isDiscontinuous;
} }
@Override
public boolean prepare() throws ParserException {
if (!prepared) {
if (maybeSelfContained) {
// Read up to the first sample. Once we're there, we know that the extractor must have
// parsed a moov atom if the chunk contains one.
NonBlockingInputStream inputStream = getNonBlockingInputStream();
Assertions.checkState(inputStream != null);
int result = extractor.read(inputStream, null);
prepared = (result & FragmentedMp4Extractor.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.
prepared = true;
}
if (prepared) {
mediaFormat = Assertions.checkNotNull(extractor.getFormat());
psshInfo = extractor.getPsshInfo();
}
}
return prepared;
}
@Override @Override
public boolean read(SampleHolder holder) throws ParserException { public boolean read(SampleHolder holder) throws ParserException {
NonBlockingInputStream inputStream = getNonBlockingInputStream(); NonBlockingInputStream inputStream = getNonBlockingInputStream();
@ -78,12 +117,12 @@ public final class Mp4MediaChunk extends MediaChunk {
@Override @Override
public MediaFormat getMediaFormat() { public MediaFormat getMediaFormat() {
return extractor.getFormat(); return mediaFormat;
} }
@Override @Override
public Map<UUID, byte[]> getPsshInfo() { public Map<UUID, byte[]> getPsshInfo() {
return extractor.getPsshInfo(); return psshInfo;
} }
} }

View File

@ -68,7 +68,7 @@ public class MultiTrackChunkSource implements ChunkSource, ExoPlayerComponent {
} }
@Override @Override
public void disable(List<MediaChunk> queue) { public void disable(List<? extends MediaChunk> queue) {
selectedSource.disable(queue); selectedSource.disable(queue);
enabled = false; enabled = false;
} }
@ -102,4 +102,9 @@ public class MultiTrackChunkSource implements ChunkSource, ExoPlayerComponent {
} }
} }
@Override
public void onChunkLoadError(Chunk chunk, Exception e) {
selectedSource.onChunkLoadError(chunk, e);
}
} }

View File

@ -77,6 +77,11 @@ public class SingleSampleMediaChunk extends MediaChunk {
this.headerData = headerData; this.headerData = headerData;
} }
@Override
public boolean prepare() {
return true;
}
@Override @Override
public boolean read(SampleHolder holder) { public boolean read(SampleHolder holder) {
NonBlockingInputStream inputStream = getNonBlockingInputStream(); NonBlockingInputStream inputStream = getNonBlockingInputStream();
@ -109,6 +114,11 @@ public class SingleSampleMediaChunk extends MediaChunk {
return true; return true;
} }
@Override
public void seekToStart() {
resetReadPosition();
}
@Override @Override
public boolean seekTo(long positionUs, boolean allowNoop) { public boolean seekTo(long positionUs, boolean allowNoop) {
resetReadPosition(); resetReadPosition();

View File

@ -50,6 +50,11 @@ public final class WebmMediaChunk extends MediaChunk {
this.extractor = extractor; this.extractor = extractor;
} }
@Override
public void seekToStart() {
seekTo(0, false);
}
@Override @Override
public boolean seekTo(long positionUs, boolean allowNoop) { public boolean seekTo(long positionUs, boolean allowNoop) {
boolean isDiscontinuous = extractor.seekTo(positionUs, allowNoop); boolean isDiscontinuous = extractor.seekTo(positionUs, allowNoop);
@ -59,6 +64,11 @@ public final class WebmMediaChunk extends MediaChunk {
return isDiscontinuous; return isDiscontinuous;
} }
@Override
public boolean prepare() {
return true;
}
@Override @Override
public boolean read(SampleHolder holder) { public boolean read(SampleHolder holder) {
NonBlockingInputStream inputStream = getNonBlockingInputStream(); NonBlockingInputStream inputStream = getNonBlockingInputStream();

View File

@ -27,18 +27,18 @@ import com.google.android.exoplayer.chunk.FormatEvaluator;
import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation; import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation;
import com.google.android.exoplayer.chunk.MediaChunk; import com.google.android.exoplayer.chunk.MediaChunk;
import com.google.android.exoplayer.chunk.Mp4MediaChunk; 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.dash.mpd.Representation;
import com.google.android.exoplayer.parser.SegmentIndex;
import com.google.android.exoplayer.parser.mp4.FragmentedMp4Extractor; import com.google.android.exoplayer.parser.mp4.FragmentedMp4Extractor;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.NonBlockingInputStream; import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import android.util.Log; import android.net.Uri;
import android.util.SparseArray;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap;
import java.util.List; import java.util.List;
/** /**
@ -46,26 +46,17 @@ import java.util.List;
*/ */
public class DashMp4ChunkSource implements ChunkSource { public class DashMp4ChunkSource implements ChunkSource {
public static final int DEFAULT_NUM_SEGMENTS_PER_CHUNK = 1;
private static final int EXPECTED_INITIALIZATION_RESULT =
FragmentedMp4Extractor.RESULT_END_OF_STREAM
| FragmentedMp4Extractor.RESULT_READ_MOOV
| FragmentedMp4Extractor.RESULT_READ_SIDX;
private static final String TAG = "DashMp4ChunkSource";
private final TrackInfo trackInfo; private final TrackInfo trackInfo;
private final DataSource dataSource; private final DataSource dataSource;
private final FormatEvaluator evaluator; private final FormatEvaluator evaluator;
private final Evaluation evaluation; private final Evaluation evaluation;
private final int maxWidth; private final int maxWidth;
private final int maxHeight; private final int maxHeight;
private final int numSegmentsPerChunk;
private final Format[] formats; private final Format[] formats;
private final SparseArray<Representation> representations; private final HashMap<String, Representation> representations;
private final SparseArray<FragmentedMp4Extractor> extractors; private final HashMap<String, FragmentedMp4Extractor> extractors;
private final HashMap<String, DashSegmentIndex> segmentIndexes;
private boolean lastChunkWasInitialization; private boolean lastChunkWasInitialization;
@ -76,26 +67,14 @@ public class DashMp4ChunkSource implements ChunkSource {
*/ */
public DashMp4ChunkSource(DataSource dataSource, FormatEvaluator evaluator, public DashMp4ChunkSource(DataSource dataSource, FormatEvaluator evaluator,
Representation... representations) { Representation... representations) {
this(dataSource, evaluator, DEFAULT_NUM_SEGMENTS_PER_CHUNK, representations);
}
/**
* @param dataSource A {@link DataSource} suitable for loading the media data.
* @param evaluator Selects from the available formats.
* @param numSegmentsPerChunk The number of segments (as defined in the stream's segment index)
* that should be grouped into a single chunk.
* @param representations The representations to be considered by the source.
*/
public DashMp4ChunkSource(DataSource dataSource, FormatEvaluator evaluator,
int numSegmentsPerChunk, Representation... representations) {
this.dataSource = dataSource; this.dataSource = dataSource;
this.evaluator = evaluator; this.evaluator = evaluator;
this.numSegmentsPerChunk = numSegmentsPerChunk;
this.formats = new Format[representations.length]; this.formats = new Format[representations.length];
this.extractors = new SparseArray<FragmentedMp4Extractor>(); this.extractors = new HashMap<String, FragmentedMp4Extractor>();
this.representations = new SparseArray<Representation>(); this.segmentIndexes = new HashMap<String, DashSegmentIndex>();
this.representations = new HashMap<String, Representation>();
this.trackInfo = new TrackInfo(representations[0].format.mimeType, this.trackInfo = new TrackInfo(representations[0].format.mimeType,
representations[0].periodDuration * 1000); representations[0].periodDurationMs * 1000);
this.evaluation = new Evaluation(); this.evaluation = new Evaluation();
int maxWidth = 0; int maxWidth = 0;
int maxHeight = 0; int maxHeight = 0;
@ -103,8 +82,12 @@ public class DashMp4ChunkSource implements ChunkSource {
formats[i] = representations[i].format; formats[i] = representations[i].format;
maxWidth = Math.max(formats[i].width, maxWidth); maxWidth = Math.max(formats[i].width, maxWidth);
maxHeight = Math.max(formats[i].height, maxHeight); maxHeight = Math.max(formats[i].height, maxHeight);
extractors.append(formats[i].id, new FragmentedMp4Extractor()); extractors.put(formats[i].id, new FragmentedMp4Extractor());
this.representations.put(formats[i].id, representations[i]); 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.maxWidth = maxWidth;
this.maxHeight = maxHeight; this.maxHeight = maxHeight;
@ -129,7 +112,7 @@ public class DashMp4ChunkSource implements ChunkSource {
} }
@Override @Override
public void disable(List<MediaChunk> queue) { public void disable(List<? extends MediaChunk> queue) {
evaluator.disable(); evaluator.disable();
} }
@ -152,7 +135,7 @@ public class DashMp4ChunkSource implements ChunkSource {
out.chunk = null; out.chunk = null;
return; return;
} else if (out.queueSize == queue.size() && out.chunk != null } else if (out.queueSize == queue.size() && out.chunk != null
&& out.chunk.format.id == selectedFormat.id) { && out.chunk.format.id.equals(selectedFormat.id)) {
// We already have a chunk, and the evaluation hasn't changed either the format or the size // We already have a chunk, and the evaluation hasn't changed either the format or the size
// of the queue. Leave unchanged. // of the queue. Leave unchanged.
return; return;
@ -160,29 +143,39 @@ public class DashMp4ChunkSource implements ChunkSource {
Representation selectedRepresentation = representations.get(selectedFormat.id); Representation selectedRepresentation = representations.get(selectedFormat.id);
FragmentedMp4Extractor extractor = extractors.get(selectedRepresentation.format.id); FragmentedMp4Extractor extractor = extractors.get(selectedRepresentation.format.id);
RangedUri pendingInitializationUri = null;
RangedUri pendingIndexUri = null;
if (extractor.getTrack() == null) { if (extractor.getTrack() == null) {
Chunk initializationChunk = newInitializationChunk(selectedRepresentation, extractor, pendingInitializationUri = selectedRepresentation.getInitializationUri();
dataSource, evaluation.trigger); }
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; lastChunkWasInitialization = true;
out.chunk = initializationChunk; out.chunk = initializationChunk;
return; return;
} }
int nextIndex; int nextSegmentNum;
DashSegmentIndex segmentIndex = segmentIndexes.get(selectedRepresentation.format.id);
if (queue.isEmpty()) { if (queue.isEmpty()) {
nextIndex = Arrays.binarySearch(extractor.getSegmentIndex().timesUs, seekPositionUs); nextSegmentNum = segmentIndex.getSegmentNum(seekPositionUs);
nextIndex = nextIndex < 0 ? -nextIndex - 2 : nextIndex;
} else { } else {
nextIndex = queue.get(out.queueSize - 1).nextChunkIndex; nextSegmentNum = queue.get(out.queueSize - 1).nextChunkIndex;
} }
if (nextIndex == -1) { if (nextSegmentNum == -1) {
out.chunk = null; out.chunk = null;
return; return;
} }
Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, extractor, dataSource, Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, segmentIndex, extractor,
extractor.getSegmentIndex(), nextIndex, evaluation.trigger, numSegmentsPerChunk); dataSource, nextSegmentNum, evaluation.trigger);
lastChunkWasInitialization = false; lastChunkWasInitialization = false;
out.chunk = nextMediaChunk; out.chunk = nextMediaChunk;
} }
@ -192,75 +185,80 @@ public class DashMp4ChunkSource implements ChunkSource {
return null; return null;
} }
private static Chunk newInitializationChunk(Representation representation, @Override
FragmentedMp4Extractor extractor, DataSource dataSource, int trigger) { public void onChunkLoadError(Chunk chunk, Exception e) {
DataSpec dataSpec = new DataSpec(representation.uri, 0, representation.indexEnd + 1, // Do nothing.
}
private Chunk newInitializationChunk(RangedUri initializationUri, RangedUri indexUri,
Representation representation, FragmentedMp4Extractor extractor, DataSource dataSource,
int trigger) {
int expectedExtractorResult = FragmentedMp4Extractor.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_MOOV;
requestUri = initializationUri.attemptMerge(indexUri);
if (requestUri != null) {
expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_SIDX;
indexAnchor = indexUri.start + indexUri.length;
} else {
requestUri = initializationUri;
}
} else {
requestUri = indexUri;
indexAnchor = indexUri.start + indexUri.length;
expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_SIDX;
}
DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length,
representation.getCacheKey()); representation.getCacheKey());
return new InitializationMp4Loadable(dataSource, dataSpec, trigger, extractor, representation); return new InitializationMp4Loadable(dataSource, dataSpec, trigger, representation.format,
extractor, expectedExtractorResult, indexAnchor);
} }
private static Chunk newMediaChunk(Representation representation, private Chunk newMediaChunk(Representation representation, DashSegmentIndex segmentIndex,
FragmentedMp4Extractor extractor, DataSource dataSource, SegmentIndex sidx, int index, FragmentedMp4Extractor extractor, DataSource dataSource, int segmentNum, int trigger) {
int trigger, int numSegmentsPerChunk) { int lastSegmentNum = segmentIndex.getLastSegmentNum();
int nextSegmentNum = segmentNum == lastSegmentNum ? -1 : segmentNum + 1;
// Computes the segments to included in the next fetch. long startTimeUs = segmentIndex.getTimeUs(segmentNum);
int numSegmentsToFetch = Math.min(numSegmentsPerChunk, sidx.length - index); long endTimeUs = segmentNum < lastSegmentNum ? segmentIndex.getTimeUs(segmentNum + 1)
int lastSegmentInChunk = index + numSegmentsToFetch - 1; : startTimeUs + segmentIndex.getDurationUs(segmentNum);
int nextIndex = lastSegmentInChunk == sidx.length - 1 ? -1 : lastSegmentInChunk + 1; RangedUri segmentUri = segmentIndex.getSegmentUrl(segmentNum);
DataSpec dataSpec = new DataSpec(segmentUri.getUri(), segmentUri.start, segmentUri.length,
long startTimeUs = sidx.timesUs[index];
// Compute the end time, prefer to use next segment start time if there is a next segment.
long endTimeUs = nextIndex == -1 ?
sidx.timesUs[lastSegmentInChunk] + sidx.durationsUs[lastSegmentInChunk] :
sidx.timesUs[nextIndex];
long offset = (int) representation.indexEnd + 1 + sidx.offsets[index];
// Compute combined segments byte length.
long size = 0;
for (int i = index; i <= lastSegmentInChunk; i++) {
size += sidx.sizes[i];
}
DataSpec dataSpec = new DataSpec(representation.uri, offset, size,
representation.getCacheKey()); representation.getCacheKey());
return new Mp4MediaChunk(dataSource, dataSpec, representation.format, trigger, extractor, return new Mp4MediaChunk(dataSource, dataSpec, representation.format, trigger, startTimeUs,
startTimeUs, endTimeUs, 0, nextIndex); endTimeUs, nextSegmentNum, extractor, false, 0);
} }
private static class InitializationMp4Loadable extends Chunk { private class InitializationMp4Loadable extends Chunk {
private final Representation representation;
private final FragmentedMp4Extractor extractor; private final FragmentedMp4Extractor extractor;
private final int expectedExtractorResult;
private final long indexAnchor;
private final Uri uri;
public InitializationMp4Loadable(DataSource dataSource, DataSpec dataSpec, int trigger, public InitializationMp4Loadable(DataSource dataSource, DataSpec dataSpec, int trigger,
FragmentedMp4Extractor extractor, Representation representation) { Format format, FragmentedMp4Extractor extractor, int expectedExtractorResult,
super(dataSource, dataSpec, representation.format, trigger); long indexAnchor) {
super(dataSource, dataSpec, format, trigger);
this.extractor = extractor; this.extractor = extractor;
this.representation = representation; this.expectedExtractorResult = expectedExtractorResult;
this.indexAnchor = indexAnchor;
this.uri = dataSpec.uri;
} }
@Override @Override
protected void consumeStream(NonBlockingInputStream stream) throws IOException { protected void consumeStream(NonBlockingInputStream stream) throws IOException {
int result = extractor.read(stream, null); int result = extractor.read(stream, null);
if (result != EXPECTED_INITIALIZATION_RESULT) { if (result != expectedExtractorResult) {
throw new ParserException("Invalid initialization data"); throw new ParserException("Invalid extractor result. Expected "
+ expectedExtractorResult + ", got " + result);
} }
validateSegmentIndex(extractor.getSegmentIndex()); if ((result & FragmentedMp4Extractor.RESULT_READ_SIDX) != 0) {
} segmentIndexes.put(format.id,
new DashWrappingSegmentIndex(extractor.getSegmentIndex(), uri, indexAnchor));
private void validateSegmentIndex(SegmentIndex segmentIndex) {
long expectedIndexLen = representation.indexEnd - representation.indexStart + 1;
if (segmentIndex.sizeBytes != expectedIndexLen) {
Log.w(TAG, "Sidx length mismatch: sidxLen = " + segmentIndex.sizeBytes +
", ExpectedLen = " + expectedIndexLen);
}
long sidxContentLength = segmentIndex.offsets[segmentIndex.length - 1] +
segmentIndex.sizes[segmentIndex.length - 1] + representation.indexEnd + 1;
if (sidxContentLength != representation.contentLength) {
Log.w(TAG, "ContentLength mismatch: Actual = " + sidxContentLength +
", Expected = " + representation.contentLength);
} }
} }

View File

@ -27,18 +27,19 @@ import com.google.android.exoplayer.chunk.FormatEvaluator;
import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation; import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation;
import com.google.android.exoplayer.chunk.MediaChunk; import com.google.android.exoplayer.chunk.MediaChunk;
import com.google.android.exoplayer.chunk.WebmMediaChunk; 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.dash.mpd.Representation;
import com.google.android.exoplayer.parser.SegmentIndex; import com.google.android.exoplayer.parser.webm.DefaultWebmExtractor;
import com.google.android.exoplayer.parser.webm.WebmExtractor; import com.google.android.exoplayer.parser.webm.WebmExtractor;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.NonBlockingInputStream; import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import android.util.Log; import android.net.Uri;
import android.util.SparseArray;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap;
import java.util.List; import java.util.List;
/** /**
@ -46,37 +47,30 @@ import java.util.List;
*/ */
public class DashWebmChunkSource implements ChunkSource { public class DashWebmChunkSource implements ChunkSource {
private static final String TAG = "DashWebmChunkSource";
private final TrackInfo trackInfo; private final TrackInfo trackInfo;
private final DataSource dataSource; private final DataSource dataSource;
private final FormatEvaluator evaluator; private final FormatEvaluator evaluator;
private final Evaluation evaluation; private final Evaluation evaluation;
private final int maxWidth; private final int maxWidth;
private final int maxHeight; private final int maxHeight;
private final int numSegmentsPerChunk;
private final Format[] formats; private final Format[] formats;
private final SparseArray<Representation> representations; private final HashMap<String, Representation> representations;
private final SparseArray<WebmExtractor> extractors; private final HashMap<String, WebmExtractor> extractors;
private final HashMap<String, DashSegmentIndex> segmentIndexes;
private boolean lastChunkWasInitialization; private boolean lastChunkWasInitialization;
public DashWebmChunkSource(
DataSource dataSource, FormatEvaluator evaluator, Representation... representations) {
this(dataSource, evaluator, 1, representations);
}
public DashWebmChunkSource(DataSource dataSource, FormatEvaluator evaluator, public DashWebmChunkSource(DataSource dataSource, FormatEvaluator evaluator,
int numSegmentsPerChunk, Representation... representations) { Representation... representations) {
this.dataSource = dataSource; this.dataSource = dataSource;
this.evaluator = evaluator; this.evaluator = evaluator;
this.numSegmentsPerChunk = numSegmentsPerChunk;
this.formats = new Format[representations.length]; this.formats = new Format[representations.length];
this.extractors = new SparseArray<WebmExtractor>(); this.extractors = new HashMap<String, WebmExtractor>();
this.representations = new SparseArray<Representation>(); this.segmentIndexes = new HashMap<String, DashSegmentIndex>();
this.representations = new HashMap<String, Representation>();
this.trackInfo = new TrackInfo( this.trackInfo = new TrackInfo(
representations[0].format.mimeType, representations[0].periodDuration * 1000); representations[0].format.mimeType, representations[0].periodDurationMs * 1000);
this.evaluation = new Evaluation(); this.evaluation = new Evaluation();
int maxWidth = 0; int maxWidth = 0;
int maxHeight = 0; int maxHeight = 0;
@ -84,8 +78,12 @@ public class DashWebmChunkSource implements ChunkSource {
formats[i] = representations[i].format; formats[i] = representations[i].format;
maxWidth = Math.max(formats[i].width, maxWidth); maxWidth = Math.max(formats[i].width, maxWidth);
maxHeight = Math.max(formats[i].height, maxHeight); maxHeight = Math.max(formats[i].height, maxHeight);
extractors.append(formats[i].id, new WebmExtractor()); extractors.put(formats[i].id, new DefaultWebmExtractor());
this.representations.put(formats[i].id, representations[i]); 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.maxWidth = maxWidth;
this.maxHeight = maxHeight; this.maxHeight = maxHeight;
@ -110,7 +108,7 @@ public class DashWebmChunkSource implements ChunkSource {
} }
@Override @Override
public void disable(List<MediaChunk> queue) { public void disable(List<? extends MediaChunk> queue) {
evaluator.disable(); evaluator.disable();
} }
@ -133,7 +131,7 @@ public class DashWebmChunkSource implements ChunkSource {
out.chunk = null; out.chunk = null;
return; return;
} else if (out.queueSize == queue.size() && out.chunk != null } else if (out.queueSize == queue.size() && out.chunk != null
&& out.chunk.format.id == selectedFormat.id) { && out.chunk.format.id.equals(selectedFormat.id)) {
// We already have a chunk, and the evaluation hasn't changed either the format or the size // We already have a chunk, and the evaluation hasn't changed either the format or the size
// of the queue. Leave unchanged. // of the queue. Leave unchanged.
return; return;
@ -141,29 +139,34 @@ public class DashWebmChunkSource implements ChunkSource {
Representation selectedRepresentation = representations.get(selectedFormat.id); Representation selectedRepresentation = representations.get(selectedFormat.id);
WebmExtractor extractor = extractors.get(selectedRepresentation.format.id); WebmExtractor extractor = extractors.get(selectedRepresentation.format.id);
if (!extractor.isPrepared()) { if (!extractor.isPrepared()) {
Chunk initializationChunk = newInitializationChunk(selectedRepresentation, extractor, // TODO: This code forces cues to exist and to immediately follow the initialization
dataSource, evaluation.trigger); // data. Webm extractor should be generalized to allow cues to be optional. See [redacted].
RangedUri initializationUri = selectedRepresentation.getInitializationUri().attemptMerge(
selectedRepresentation.getIndexUri());
Chunk initializationChunk = newInitializationChunk(initializationUri, selectedRepresentation,
extractor, dataSource, evaluation.trigger);
lastChunkWasInitialization = true; lastChunkWasInitialization = true;
out.chunk = initializationChunk; out.chunk = initializationChunk;
return; return;
} }
int nextIndex; int nextSegmentNum;
DashSegmentIndex segmentIndex = segmentIndexes.get(selectedRepresentation.format.id);
if (queue.isEmpty()) { if (queue.isEmpty()) {
nextIndex = Arrays.binarySearch(extractor.getCues().timesUs, seekPositionUs); nextSegmentNum = segmentIndex.getSegmentNum(seekPositionUs);
nextIndex = nextIndex < 0 ? -nextIndex - 2 : nextIndex;
} else { } else {
nextIndex = queue.get(out.queueSize - 1).nextChunkIndex; nextSegmentNum = queue.get(out.queueSize - 1).nextChunkIndex;
} }
if (nextIndex == -1) { if (nextSegmentNum == -1) {
out.chunk = null; out.chunk = null;
return; return;
} }
Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, extractor, dataSource, Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, segmentIndex, extractor,
extractor.getCues(), nextIndex, evaluation.trigger, numSegmentsPerChunk); dataSource, nextSegmentNum, evaluation.trigger);
lastChunkWasInitialization = false; lastChunkWasInitialization = false;
out.chunk = nextMediaChunk; out.chunk = nextMediaChunk;
} }
@ -173,53 +176,43 @@ public class DashWebmChunkSource implements ChunkSource {
return null; return null;
} }
private static Chunk newInitializationChunk(Representation representation, @Override
public void onChunkLoadError(Chunk chunk, Exception e) {
// Do nothing.
}
private Chunk newInitializationChunk(RangedUri initializationUri, Representation representation,
WebmExtractor extractor, DataSource dataSource, int trigger) { WebmExtractor extractor, DataSource dataSource, int trigger) {
DataSpec dataSpec = new DataSpec(representation.uri, 0, representation.indexEnd + 1, DataSpec dataSpec = new DataSpec(initializationUri.getUri(), initializationUri.start,
representation.getCacheKey()); initializationUri.length, representation.getCacheKey());
return new InitializationWebmLoadable(dataSource, dataSpec, trigger, extractor, representation); return new InitializationWebmLoadable(dataSource, dataSpec, trigger, representation.format,
extractor);
} }
private static Chunk newMediaChunk(Representation representation, private Chunk newMediaChunk(Representation representation, DashSegmentIndex segmentIndex,
WebmExtractor extractor, DataSource dataSource, SegmentIndex cues, int index, WebmExtractor extractor, DataSource dataSource, int segmentNum, int trigger) {
int trigger, int numSegmentsPerChunk) { int lastSegmentNum = segmentIndex.getLastSegmentNum();
int nextSegmentNum = segmentNum == lastSegmentNum ? -1 : segmentNum + 1;
// Computes the segments to included in the next fetch. long startTimeUs = segmentIndex.getTimeUs(segmentNum);
int numSegmentsToFetch = Math.min(numSegmentsPerChunk, cues.length - index); long endTimeUs = segmentNum < lastSegmentNum ? segmentIndex.getTimeUs(segmentNum + 1)
int lastSegmentInChunk = index + numSegmentsToFetch - 1; : startTimeUs + segmentIndex.getDurationUs(segmentNum);
int nextIndex = lastSegmentInChunk == cues.length - 1 ? -1 : lastSegmentInChunk + 1; RangedUri segmentUri = segmentIndex.getSegmentUrl(segmentNum);
DataSpec dataSpec = new DataSpec(segmentUri.getUri(), segmentUri.start, segmentUri.length,
long startTimeUs = cues.timesUs[index];
// Compute the end time, prefer to use next segment start time if there is a next segment.
long endTimeUs = nextIndex == -1 ?
cues.timesUs[lastSegmentInChunk] + cues.durationsUs[lastSegmentInChunk] :
cues.timesUs[nextIndex];
long offset = cues.offsets[index];
// Compute combined segments byte length.
long size = 0;
for (int i = index; i <= lastSegmentInChunk; i++) {
size += cues.sizes[i];
}
DataSpec dataSpec = new DataSpec(representation.uri, offset, size,
representation.getCacheKey()); representation.getCacheKey());
return new WebmMediaChunk(dataSource, dataSpec, representation.format, trigger, extractor, return new WebmMediaChunk(dataSource, dataSpec, representation.format, trigger, extractor,
startTimeUs, endTimeUs, nextIndex); startTimeUs, endTimeUs, nextSegmentNum);
} }
private static class InitializationWebmLoadable extends Chunk { private class InitializationWebmLoadable extends Chunk {
private final Representation representation;
private final WebmExtractor extractor; private final WebmExtractor extractor;
private final Uri uri;
public InitializationWebmLoadable(DataSource dataSource, DataSpec dataSpec, int trigger, public InitializationWebmLoadable(DataSource dataSource, DataSpec dataSpec, int trigger,
WebmExtractor extractor, Representation representation) { Format format, WebmExtractor extractor) {
super(dataSource, dataSpec, representation.format, trigger); super(dataSource, dataSpec, format, trigger);
this.extractor = extractor; this.extractor = extractor;
this.representation = representation; this.uri = dataSpec.uri;
} }
@Override @Override
@ -228,22 +221,7 @@ public class DashWebmChunkSource implements ChunkSource {
if (!extractor.isPrepared()) { if (!extractor.isPrepared()) {
throw new ParserException("Invalid initialization data"); throw new ParserException("Invalid initialization data");
} }
validateCues(extractor.getCues()); segmentIndexes.put(format.id, new DashWrappingSegmentIndex(extractor.getCues(), uri, 0));
}
private void validateCues(SegmentIndex cues) {
long expectedSizeBytes = representation.indexEnd - representation.indexStart + 1;
if (cues.sizeBytes != expectedSizeBytes) {
Log.w(TAG, "Cues length mismatch: got " + cues.sizeBytes +
" but expected " + expectedSizeBytes);
}
long expectedContentLength = cues.offsets[cues.length - 1] +
cues.sizes[cues.length - 1] + representation.indexEnd + 1;
if (representation.contentLength > 0
&& expectedContentLength != representation.contentLength) {
Log.w(TAG, "ContentLength mismatch: got " + expectedContentLength +
" but expected " + representation.contentLength);
}
} }
} }

View File

@ -18,6 +18,8 @@ package com.google.android.exoplayer.dash.mpd;
import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher;
import android.net.Uri;
import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException; import java.io.IOException;
@ -57,9 +59,9 @@ public final class MediaPresentationDescriptionFetcher extends
@Override @Override
protected MediaPresentationDescription parse(InputStream stream, String inputEncoding, protected MediaPresentationDescription parse(InputStream stream, String inputEncoding,
String contentId) throws IOException, ParserException { String contentId, Uri baseUrl) throws IOException, ParserException {
try { try {
return parser.parseMediaPresentationDescription(stream, inputEncoding, contentId); return parser.parseMediaPresentationDescription(stream, inputEncoding, contentId, baseUrl);
} catch (XmlPullParserException e) { } catch (XmlPullParserException e) {
throw new ParserException(e); throw new ParserException(e);
} }

View File

@ -17,11 +17,15 @@ package com.google.android.exoplayer.dash.mpd;
import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.chunk.Format; import com.google.android.exoplayer.chunk.Format;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.dash.mpd.SegmentBase.SegmentList;
import com.google.android.exoplayer.dash.mpd.SegmentBase.SegmentTemplate;
import com.google.android.exoplayer.dash.mpd.SegmentBase.SegmentTimelineElement;
import com.google.android.exoplayer.dash.mpd.SegmentBase.SingleSegmentBase;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.MimeTypes;
import android.net.Uri; import android.net.Uri;
import android.util.Log; import android.text.TextUtils;
import org.xml.sax.helpers.DefaultHandler; import org.xml.sax.helpers.DefaultHandler;
import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParser;
@ -38,15 +42,8 @@ import java.util.regex.Pattern;
/** /**
* A parser of media presentation description files. * A parser of media presentation description files.
*/ */
/*
* TODO: Parse representation base attributes at multiple levels, and normalize the resulting
* datastructure.
* TODO: Decide how best to represent missing integer/double/long attributes.
*/
public class MediaPresentationDescriptionParser extends DefaultHandler { public class MediaPresentationDescriptionParser extends DefaultHandler {
private static final String TAG = "MediaPresentationDescriptionParser";
// Note: Does not support the date part of ISO 8601 // Note: Does not support the date part of ISO 8601
private static final Pattern DURATION = private static final Pattern DURATION =
Pattern.compile("^PT(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?$"); Pattern.compile("^PT(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?$");
@ -61,20 +58,23 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
} }
} }
// MPD parsing.
/** /**
* Parses a manifest from the provided {@link InputStream}. * Parses a manifest from the provided {@link InputStream}.
* *
* @param inputStream The stream from which to parse the manifest. * @param inputStream The stream from which to parse the manifest.
* @param inputEncoding The encoding of the input. * @param inputEncoding The encoding of the input.
* @param contentId The content id of the media. * @param contentId The content id of the media.
* @param baseUrl The url that any relative urls defined within the manifest are relative to.
* @return The parsed manifest. * @return The parsed manifest.
* @throws IOException If a problem occurred reading from the stream. * @throws IOException If a problem occurred reading from the stream.
* @throws XmlPullParserException If a problem occurred parsing the stream as xml. * @throws XmlPullParserException If a problem occurred parsing the stream as xml.
* @throws ParserException If a problem occurred parsing the xml as a DASH mpd. * @throws ParserException If a problem occurred parsing the xml as a DASH mpd.
*/ */
public MediaPresentationDescription parseMediaPresentationDescription(InputStream inputStream, public MediaPresentationDescription parseMediaPresentationDescription(InputStream inputStream,
String inputEncoding, String contentId) throws XmlPullParserException, IOException, String inputEncoding, String contentId, Uri baseUrl) throws XmlPullParserException,
ParserException { IOException, ParserException {
XmlPullParser xpp = xmlParserFactory.newPullParser(); XmlPullParser xpp = xmlParserFactory.newPullParser();
xpp.setInput(inputStream, inputEncoding); xpp.setInput(inputStream, inputEncoding);
int eventType = xpp.next(); int eventType = xpp.next();
@ -82,123 +82,139 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
throw new ParserException( throw new ParserException(
"inputStream does not contain a valid media presentation description"); "inputStream does not contain a valid media presentation description");
} }
return parseMediaPresentationDescription(xpp, contentId); return parseMediaPresentationDescription(xpp, contentId, baseUrl);
} }
private MediaPresentationDescription parseMediaPresentationDescription(XmlPullParser xpp, private MediaPresentationDescription parseMediaPresentationDescription(XmlPullParser xpp,
String contentId) throws XmlPullParserException, IOException { String contentId, Uri baseUrl) throws XmlPullParserException, IOException {
long duration = parseDurationMs(xpp, "mediaPresentationDuration"); long durationMs = parseDurationMs(xpp, "mediaPresentationDuration");
long minBufferTime = parseDurationMs(xpp, "minBufferTime"); long minBufferTimeMs = parseDurationMs(xpp, "minBufferTime");
String typeString = xpp.getAttributeValue(null, "type"); String typeString = xpp.getAttributeValue(null, "type");
boolean dynamic = (typeString != null) ? typeString.equals("dynamic") : false; boolean dynamic = (typeString != null) ? typeString.equals("dynamic") : false;
long minUpdateTime = (dynamic) ? parseDurationMs(xpp, "minimumUpdatePeriod", -1) : -1; long minUpdateTimeMs = (dynamic) ? parseDurationMs(xpp, "minimumUpdatePeriod", -1) : -1;
List<Period> periods = new ArrayList<Period>(); List<Period> periods = new ArrayList<Period>();
do { do {
xpp.next(); xpp.next();
if (isStartTag(xpp, "Period")) { if (isStartTag(xpp, "BaseURL")) {
periods.add(parsePeriod(xpp, contentId, duration)); baseUrl = parseBaseUrl(xpp, baseUrl);
} else if (isStartTag(xpp, "Period")) {
periods.add(parsePeriod(xpp, contentId, baseUrl, durationMs));
} }
} while (!isEndTag(xpp, "MPD")); } while (!isEndTag(xpp, "MPD"));
return new MediaPresentationDescription(duration, minBufferTime, dynamic, minUpdateTime, return new MediaPresentationDescription(durationMs, minBufferTimeMs, dynamic, minUpdateTimeMs,
periods); periods);
} }
private Period parsePeriod(XmlPullParser xpp, String contentId, long mediaPresentationDuration) private Period parsePeriod(XmlPullParser xpp, String contentId, Uri baseUrl, long mpdDurationMs)
throws XmlPullParserException, IOException { throws XmlPullParserException, IOException {
int id = parseInt(xpp, "id"); String id = xpp.getAttributeValue(null, "id");
long start = parseDurationMs(xpp, "start", 0); long startMs = parseDurationMs(xpp, "start", 0);
long duration = parseDurationMs(xpp, "duration", mediaPresentationDuration); long durationMs = parseDurationMs(xpp, "duration", mpdDurationMs);
SegmentBase segmentBase = null;
List<AdaptationSet> adaptationSets = new ArrayList<AdaptationSet>(); List<AdaptationSet> adaptationSets = new ArrayList<AdaptationSet>();
List<Segment.Timeline> segmentTimelineList = null;
int segmentStartNumber = 0;
int segmentTimescale = 0;
long presentationTimeOffset = 0;
do { do {
xpp.next(); xpp.next();
if (isStartTag(xpp, "AdaptationSet")) { if (isStartTag(xpp, "BaseURL")) {
adaptationSets.add(parseAdaptationSet(xpp, contentId, start, duration, baseUrl = parseBaseUrl(xpp, baseUrl);
segmentTimelineList)); } else if (isStartTag(xpp, "AdaptationSet")) {
adaptationSets.add(parseAdaptationSet(xpp, contentId, baseUrl, startMs, durationMs,
segmentBase));
} else if (isStartTag(xpp, "SegmentBase")) {
segmentBase = parseSegmentBase(xpp, baseUrl, null);
} else if (isStartTag(xpp, "SegmentList")) { } else if (isStartTag(xpp, "SegmentList")) {
segmentStartNumber = parseInt(xpp, "startNumber"); segmentBase = parseSegmentList(xpp, baseUrl, null, durationMs);
segmentTimescale = parseInt(xpp, "timescale"); } else if (isStartTag(xpp, "SegmentTemplate")) {
presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", 0); segmentBase = parseSegmentTemplate(xpp, baseUrl, null, durationMs);
segmentTimelineList = parsePeriodSegmentList(xpp, segmentStartNumber);
} }
} while (!isEndTag(xpp, "Period")); } while (!isEndTag(xpp, "Period"));
return new Period(id, start, duration, adaptationSets, segmentTimelineList, return new Period(id, startMs, durationMs, adaptationSets);
segmentStartNumber, segmentTimescale, presentationTimeOffset);
} }
private List<Segment.Timeline> parsePeriodSegmentList( // AdaptationSet parsing.
XmlPullParser xpp, long segmentStartNumber) throws XmlPullParserException, IOException {
List<Segment.Timeline> segmentTimelineList = new ArrayList<Segment.Timeline>();
do { private AdaptationSet parseAdaptationSet(XmlPullParser xpp, String contentId, Uri baseUrl,
xpp.next(); long periodStartMs, long periodDurationMs, SegmentBase segmentBase)
if (isStartTag(xpp, "SegmentTimeline")) {
do {
xpp.next();
if (isStartTag(xpp, "S")) {
long duration = parseLong(xpp, "d");
segmentTimelineList.add(new Segment.Timeline(segmentStartNumber, duration));
segmentStartNumber++;
}
} while (!isEndTag(xpp, "SegmentTimeline"));
}
} while (!isEndTag(xpp, "SegmentList"));
return segmentTimelineList;
}
private AdaptationSet parseAdaptationSet(XmlPullParser xpp, String contentId, long periodStart,
long periodDuration, List<Segment.Timeline> segmentTimelineList)
throws XmlPullParserException, IOException { throws XmlPullParserException, IOException {
int id = -1;
int contentType = AdaptationSet.TYPE_UNKNOWN;
// TODO: Correctly handle other common attributes and elements. See 23009-1 Table 9.
String mimeType = xpp.getAttributeValue(null, "mimeType"); String mimeType = xpp.getAttributeValue(null, "mimeType");
if (mimeType != null) { int contentType = parseAdaptationSetTypeFromMimeType(mimeType);
if (MimeTypes.isAudio(mimeType)) {
contentType = AdaptationSet.TYPE_AUDIO;
} else if (MimeTypes.isVideo(mimeType)) {
contentType = AdaptationSet.TYPE_VIDEO;
} else if (MimeTypes.isText(mimeType)
|| mimeType.equalsIgnoreCase(MimeTypes.APPLICATION_TTML)) {
contentType = AdaptationSet.TYPE_TEXT;
}
}
int id = -1;
List<ContentProtection> contentProtections = null; List<ContentProtection> contentProtections = null;
List<Representation> representations = new ArrayList<Representation>(); List<Representation> representations = new ArrayList<Representation>();
do { do {
xpp.next(); xpp.next();
if (contentType != AdaptationSet.TYPE_UNKNOWN) { if (isStartTag(xpp, "BaseURL")) {
if (isStartTag(xpp, "ContentProtection")) { baseUrl = parseBaseUrl(xpp, baseUrl);
} else if (isStartTag(xpp, "ContentProtection")) {
if (contentProtections == null) { if (contentProtections == null) {
contentProtections = new ArrayList<ContentProtection>(); contentProtections = new ArrayList<ContentProtection>();
} }
contentProtections.add(parseContentProtection(xpp)); contentProtections.add(parseContentProtection(xpp));
} else if (isStartTag(xpp, "ContentComponent")) { } else if (isStartTag(xpp, "ContentComponent")) {
id = Integer.parseInt(xpp.getAttributeValue(null, "id")); id = Integer.parseInt(xpp.getAttributeValue(null, "id"));
String contentTypeString = xpp.getAttributeValue(null, "contentType"); contentType = checkAdaptationSetTypeConsistency(contentType,
contentType = "video".equals(contentTypeString) ? AdaptationSet.TYPE_VIDEO parseAdaptationSetType(xpp.getAttributeValue(null, "contentType")));
: "audio".equals(contentTypeString) ? AdaptationSet.TYPE_AUDIO
: AdaptationSet.TYPE_UNKNOWN;
} else if (isStartTag(xpp, "Representation")) { } else if (isStartTag(xpp, "Representation")) {
representations.add(parseRepresentation(xpp, contentId, periodStart, periodDuration, Representation representation = parseRepresentation(xpp, contentId, baseUrl, periodStartMs,
mimeType, segmentTimelineList)); periodDurationMs, mimeType, segmentBase);
} contentType = checkAdaptationSetTypeConsistency(contentType,
parseAdaptationSetTypeFromMimeType(representation.format.mimeType));
representations.add(representation);
} else if (isStartTag(xpp, "SegmentBase")) {
segmentBase = parseSegmentBase(xpp, baseUrl, (SingleSegmentBase) segmentBase);
} else if (isStartTag(xpp, "SegmentList")) {
segmentBase = parseSegmentList(xpp, baseUrl, (SegmentList) segmentBase, periodDurationMs);
} else if (isStartTag(xpp, "SegmentTemplate")) {
segmentBase = parseSegmentTemplate(xpp, baseUrl, (SegmentTemplate) segmentBase,
periodDurationMs);
} }
} while (!isEndTag(xpp, "AdaptationSet")); } while (!isEndTag(xpp, "AdaptationSet"));
return new AdaptationSet(id, contentType, representations, contentProtections); return new AdaptationSet(id, contentType, representations, contentProtections);
} }
private int parseAdaptationSetType(String contentType) {
return TextUtils.isEmpty(contentType) ? AdaptationSet.TYPE_UNKNOWN
: MimeTypes.BASE_TYPE_AUDIO.equals(contentType) ? AdaptationSet.TYPE_AUDIO
: MimeTypes.BASE_TYPE_VIDEO.equals(contentType) ? AdaptationSet.TYPE_VIDEO
: MimeTypes.BASE_TYPE_TEXT.equals(contentType) ? AdaptationSet.TYPE_TEXT
: AdaptationSet.TYPE_UNKNOWN;
}
private int parseAdaptationSetTypeFromMimeType(String mimeType) {
return TextUtils.isEmpty(mimeType) ? AdaptationSet.TYPE_UNKNOWN
: MimeTypes.isAudio(mimeType) ? AdaptationSet.TYPE_AUDIO
: MimeTypes.isVideo(mimeType) ? AdaptationSet.TYPE_VIDEO
: MimeTypes.isText(mimeType) || MimeTypes.isTtml(mimeType) ? AdaptationSet.TYPE_TEXT
: AdaptationSet.TYPE_UNKNOWN;
}
/**
* Checks two adaptation set types for consistency, returning the consistent type, or throwing an
* {@link IllegalStateException} if the types are inconsistent.
* <p>
* Two types are consistent if they are equal, or if one is {@link AdaptationSet#TYPE_UNKNOWN}.
* Where one of the types is {@link AdaptationSet#TYPE_UNKNOWN}, the other is returned.
*
* @param firstType The first type.
* @param secondType The second type.
* @return The consistent type.
*/
private int checkAdaptationSetTypeConsistency(int firstType, int secondType) {
if (firstType == AdaptationSet.TYPE_UNKNOWN) {
return secondType;
} else if (secondType == AdaptationSet.TYPE_UNKNOWN) {
return firstType;
} else {
Assertions.checkState(firstType == secondType);
return firstType;
}
}
/** /**
* Parses a ContentProtection element. * Parses a ContentProtection element.
* *
@ -211,99 +227,194 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
return new ContentProtection(schemeUriId, null); return new ContentProtection(schemeUriId, null);
} }
private Representation parseRepresentation(XmlPullParser xpp, String contentId, long periodStart, // Representation parsing.
long periodDuration, String parentMimeType, List<Segment.Timeline> segmentTimelineList)
private Representation parseRepresentation(XmlPullParser xpp, String contentId, Uri baseUrl,
long periodStartMs, long periodDurationMs, String mimeType, SegmentBase segmentBase)
throws XmlPullParserException, IOException { throws XmlPullParserException, IOException {
int id; String id = xpp.getAttributeValue(null, "id");
try { int bandwidth = parseInt(xpp, "bandwidth");
id = parseInt(xpp, "id");
} catch (NumberFormatException nfe) {
Log.d(TAG, "Unable to parse id; " + nfe.getMessage());
// TODO: need a way to generate a unique and stable id; use hashCode for now
id = xpp.getAttributeValue(null, "id").hashCode();
}
int bandwidth = parseInt(xpp, "bandwidth") / 8;
int audioSamplingRate = parseInt(xpp, "audioSamplingRate"); int audioSamplingRate = parseInt(xpp, "audioSamplingRate");
int width = parseInt(xpp, "width"); int width = parseInt(xpp, "width");
int height = parseInt(xpp, "height"); int height = parseInt(xpp, "height");
mimeType = parseString(xpp, "mimeType", mimeType);
String mimeType = xpp.getAttributeValue(null, "mimeType");
if (mimeType == null) {
mimeType = parentMimeType;
}
String representationUrl = null;
long indexStart = -1;
long indexEnd = -1;
long initializationStart = -1;
long initializationEnd = -1;
int numChannels = -1; int numChannels = -1;
List<Segment> segmentList = null;
do { do {
xpp.next(); xpp.next();
if (isStartTag(xpp, "BaseURL")) { if (isStartTag(xpp, "BaseURL")) {
xpp.next(); baseUrl = parseBaseUrl(xpp, baseUrl);
representationUrl = xpp.getText();
} else if (isStartTag(xpp, "AudioChannelConfiguration")) { } else if (isStartTag(xpp, "AudioChannelConfiguration")) {
numChannels = Integer.parseInt(xpp.getAttributeValue(null, "value")); numChannels = Integer.parseInt(xpp.getAttributeValue(null, "value"));
} else if (isStartTag(xpp, "SegmentBase")) { } else if (isStartTag(xpp, "SegmentBase")) {
String[] indexRange = xpp.getAttributeValue(null, "indexRange").split("-"); segmentBase = parseSegmentBase(xpp, baseUrl, (SingleSegmentBase) segmentBase);
indexStart = Long.parseLong(indexRange[0]);
indexEnd = Long.parseLong(indexRange[1]);
} else if (isStartTag(xpp, "SegmentList")) { } else if (isStartTag(xpp, "SegmentList")) {
segmentList = parseRepresentationSegmentList(xpp, segmentTimelineList); segmentBase = parseSegmentList(xpp, baseUrl, (SegmentList) segmentBase, periodDurationMs);
} else if (isStartTag(xpp, "Initialization")) { } else if (isStartTag(xpp, "SegmentTemplate")) {
String[] indexRange = xpp.getAttributeValue(null, "range").split("-"); segmentBase = parseSegmentTemplate(xpp, baseUrl, (SegmentTemplate) segmentBase,
initializationStart = Long.parseLong(indexRange[0]); periodDurationMs);
initializationEnd = Long.parseLong(indexRange[1]);
} }
} while (!isEndTag(xpp, "Representation")); } while (!isEndTag(xpp, "Representation"));
Uri uri = Uri.parse(representationUrl);
Format format = new Format(id, mimeType, width, height, numChannels, audioSamplingRate, Format format = new Format(id, mimeType, width, height, numChannels, audioSamplingRate,
bandwidth); bandwidth);
if (segmentList == null) { return Representation.newInstance(periodStartMs, periodDurationMs, contentId, -1, format,
return new Representation(contentId, -1, format, uri, DataSpec.LENGTH_UNBOUNDED, segmentBase);
initializationStart, initializationEnd, indexStart, indexEnd, periodStart,
periodDuration);
} else {
return new SegmentedRepresentation(contentId, format, uri, initializationStart,
initializationEnd, indexStart, indexEnd, periodStart, periodDuration, segmentList);
}
} }
private List<Segment> parseRepresentationSegmentList(XmlPullParser xpp, // SegmentBase, SegmentList and SegmentTemplate parsing.
List<Segment.Timeline> segmentTimelineList) throws XmlPullParserException, IOException {
List<Segment> segmentList = new ArrayList<Segment>(); private SingleSegmentBase parseSegmentBase(XmlPullParser xpp, Uri baseUrl,
int i = 0; SingleSegmentBase parent) throws XmlPullParserException, IOException {
long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1);
long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset",
parent != null ? parent.presentationTimeOffset : 0);
long indexStart = parent != null ? parent.indexStart : 0;
long indexLength = parent != null ? parent.indexLength : -1;
String indexRangeText = xpp.getAttributeValue(null, "indexRange");
if (indexRangeText != null) {
String[] indexRange = indexRangeText.split("-");
indexStart = Long.parseLong(indexRange[0]);
indexLength = Long.parseLong(indexRange[1]) - indexStart + 1;
}
RangedUri initialization = parent != null ? parent.initialization : null;
do {
xpp.next();
if (isStartTag(xpp, "Initialization")) {
initialization = parseInitialization(xpp, baseUrl);
}
} while (!isEndTag(xpp, "SegmentBase"));
return new SingleSegmentBase(initialization, timescale, presentationTimeOffset, baseUrl,
indexStart, indexLength);
}
private SegmentList parseSegmentList(XmlPullParser xpp, Uri baseUrl, SegmentList parent,
long periodDuration) throws XmlPullParserException, IOException {
long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1);
long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset",
parent != null ? parent.presentationTimeOffset : 0);
long duration = parseLong(xpp, "duration", parent != null ? parent.duration : -1);
int startNumber = parseInt(xpp, "startNumber", parent != null ? parent.startNumber : 0);
RangedUri initialization = null;
List<SegmentTimelineElement> timeline = null;
List<RangedUri> segments = null;
do { do {
xpp.next(); xpp.next();
if (isStartTag(xpp, "Initialization")) { if (isStartTag(xpp, "Initialization")) {
String url = xpp.getAttributeValue(null, "sourceURL"); initialization = parseInitialization(xpp, baseUrl);
String[] indexRange = xpp.getAttributeValue(null, "range").split("-"); } else if (isStartTag(xpp, "SegmentTimeline")) {
long initializationStart = Long.parseLong(indexRange[0]); timeline = parseSegmentTimeline(xpp);
long initializationEnd = Long.parseLong(indexRange[1]);
segmentList.add(new Segment.Initialization(url, initializationStart, initializationEnd));
} else if (isStartTag(xpp, "SegmentURL")) { } else if (isStartTag(xpp, "SegmentURL")) {
String url = xpp.getAttributeValue(null, "media"); if (segments == null) {
String mediaRange = xpp.getAttributeValue(null, "mediaRange"); segments = new ArrayList<RangedUri>();
long sequenceNumber = segmentTimelineList.get(i).sequenceNumber;
long duration = segmentTimelineList.get(i).duration;
i++;
if (mediaRange != null) {
String[] mediaRangeArray = xpp.getAttributeValue(null, "mediaRange").split("-");
long mediaStart = Long.parseLong(mediaRangeArray[0]);
segmentList.add(new Segment.Media(url, mediaStart, sequenceNumber, duration));
} else {
segmentList.add(new Segment.Media(url, sequenceNumber, duration));
} }
segments.add(parseSegmentUrl(xpp, baseUrl));
} }
} while (!isEndTag(xpp, "SegmentList")); } while (!isEndTag(xpp, "SegmentList"));
return segmentList; if (parent != null) {
initialization = initialization != null ? initialization : parent.initialization;
timeline = timeline != null ? timeline : parent.segmentTimeline;
segments = segments != null ? segments : parent.mediaSegments;
} }
return new SegmentList(initialization, timescale, presentationTimeOffset, periodDuration,
startNumber, duration, timeline, segments);
}
private SegmentTemplate parseSegmentTemplate(XmlPullParser xpp, Uri baseUrl,
SegmentTemplate parent, long periodDuration) throws XmlPullParserException, IOException {
long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1);
long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset",
parent != null ? parent.presentationTimeOffset : 0);
long duration = parseLong(xpp, "duration", parent != null ? parent.duration : -1);
int startNumber = parseInt(xpp, "startNumber", parent != null ? parent.startNumber : 0);
UrlTemplate mediaTemplate = parseUrlTemplate(xpp, "media",
parent != null ? parent.mediaTemplate : null);
UrlTemplate initializationTemplate = parseUrlTemplate(xpp, "initialization",
parent != null ? parent.initializationTemplate : null);
RangedUri initialization = null;
List<SegmentTimelineElement> timeline = null;
do {
xpp.next();
if (isStartTag(xpp, "Initialization")) {
initialization = parseInitialization(xpp, baseUrl);
} else if (isStartTag(xpp, "SegmentTimeline")) {
timeline = parseSegmentTimeline(xpp);
}
} while (!isEndTag(xpp, "SegmentTemplate"));
if (parent != null) {
initialization = initialization != null ? initialization : parent.initialization;
timeline = timeline != null ? timeline : parent.segmentTimeline;
}
return new SegmentTemplate(initialization, timescale, presentationTimeOffset, periodDuration,
startNumber, duration, timeline, initializationTemplate, mediaTemplate, baseUrl);
}
private List<SegmentTimelineElement> parseSegmentTimeline(XmlPullParser xpp)
throws XmlPullParserException, IOException {
List<SegmentTimelineElement> segmentTimeline = new ArrayList<SegmentTimelineElement>();
long elapsedTime = 0;
do {
xpp.next();
if (isStartTag(xpp, "S")) {
elapsedTime = parseLong(xpp, "t", elapsedTime);
long duration = parseLong(xpp, "d");
int count = 1 + parseInt(xpp, "r", 0);
for (int i = 0; i < count; i++) {
segmentTimeline.add(new SegmentTimelineElement(elapsedTime, duration));
elapsedTime += duration;
}
}
} while (!isEndTag(xpp, "SegmentTimeline"));
return segmentTimeline;
}
private UrlTemplate parseUrlTemplate(XmlPullParser xpp, String name,
UrlTemplate defaultValue) {
String valueString = xpp.getAttributeValue(null, name);
if (valueString != null) {
return UrlTemplate.compile(valueString);
}
return defaultValue;
}
private RangedUri parseInitialization(XmlPullParser xpp, Uri baseUrl) {
return parseRangedUrl(xpp, baseUrl, "sourceURL", "range");
}
private RangedUri parseSegmentUrl(XmlPullParser xpp, Uri baseUrl) {
return parseRangedUrl(xpp, baseUrl, "media", "mediaRange");
}
private RangedUri parseRangedUrl(XmlPullParser xpp, Uri baseUrl, String urlAttribute,
String rangeAttribute) {
String urlText = xpp.getAttributeValue(null, urlAttribute);
long rangeStart = 0;
long rangeLength = -1;
String rangeText = xpp.getAttributeValue(null, rangeAttribute);
if (rangeText != null) {
String[] rangeTextArray = rangeText.split("-");
rangeStart = Long.parseLong(rangeTextArray[0]);
rangeLength = Long.parseLong(rangeTextArray[1]) - rangeStart + 1;
}
return new RangedUri(baseUrl, urlText, rangeStart, rangeLength);
}
// Utility methods.
protected static boolean isEndTag(XmlPullParser xpp, String name) throws XmlPullParserException { protected static boolean isEndTag(XmlPullParser xpp, String name) throws XmlPullParserException {
return xpp.getEventType() == XmlPullParser.END_TAG && name.equals(xpp.getName()); return xpp.getEventType() == XmlPullParser.END_TAG && name.equals(xpp.getName());
} }
@ -313,25 +424,11 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
return xpp.getEventType() == XmlPullParser.START_TAG && name.equals(xpp.getName()); return xpp.getEventType() == XmlPullParser.START_TAG && name.equals(xpp.getName());
} }
protected static int parseInt(XmlPullParser xpp, String name) { private static long parseDurationMs(XmlPullParser xpp, String name) {
String value = xpp.getAttributeValue(null, name);
return value == null ? -1 : Integer.parseInt(value);
}
protected static long parseLong(XmlPullParser xpp, String name) {
return parseLong(xpp, name, -1);
}
protected static long parseLong(XmlPullParser xpp, String name, long defaultValue) {
String value = xpp.getAttributeValue(null, name);
return value == null ? defaultValue : Long.parseLong(value);
}
private long parseDurationMs(XmlPullParser xpp, String name) {
return parseDurationMs(xpp, name, -1); return parseDurationMs(xpp, name, -1);
} }
private long parseDurationMs(XmlPullParser xpp, String name, long defaultValue) { private static long parseDurationMs(XmlPullParser xpp, String name, long defaultValue) {
String value = xpp.getAttributeValue(null, name); String value = xpp.getAttributeValue(null, name);
if (value != null) { if (value != null) {
Matcher matcher = DURATION.matcher(value); Matcher matcher = DURATION.matcher(value);
@ -350,4 +447,38 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
return defaultValue; return defaultValue;
} }
protected static Uri parseBaseUrl(XmlPullParser xpp, Uri parentBaseUrl)
throws XmlPullParserException, IOException {
xpp.next();
String newBaseUrlText = xpp.getText();
Uri newBaseUri = Uri.parse(newBaseUrlText);
if (!newBaseUri.isAbsolute()) {
newBaseUri = Uri.withAppendedPath(parentBaseUrl, newBaseUrlText);
}
return newBaseUri;
}
protected static int parseInt(XmlPullParser xpp, String name) {
return parseInt(xpp, name, -1);
}
protected static int parseInt(XmlPullParser xpp, String name, int defaultValue) {
String value = xpp.getAttributeValue(null, name);
return value == null ? defaultValue : Integer.parseInt(value);
}
protected static long parseLong(XmlPullParser xpp, String name) {
return parseLong(xpp, name, -1);
}
protected static long parseLong(XmlPullParser xpp, String name, long defaultValue) {
String value = xpp.getAttributeValue(null, name);
return value == null ? defaultValue : Long.parseLong(value);
}
protected static String parseString(XmlPullParser xpp, String name, String defaultValue) {
String value = xpp.getAttributeValue(null, name);
return value == null ? defaultValue : value;
}
} }

View File

@ -23,46 +23,37 @@ import java.util.List;
*/ */
public final class Period { public final class Period {
public final int id; /**
* The period identifier, if one exists.
*/
public final String id;
public final long start; /**
* The start time of the period in milliseconds.
*/
public final long startMs;
public final long duration; /**
* The duration of the period in milliseconds, or -1 if the duration is unknown.
*/
public final long durationMs;
/**
* The adaptation sets belonging to the period.
*/
public final List<AdaptationSet> adaptationSets; public final List<AdaptationSet> adaptationSets;
public final List<Segment.Timeline> segmentList; /**
* @param id The period identifier. May be null.
public final int segmentStartNumber; * @param start The start time of the period in milliseconds.
* @param duration The duration of the period in milliseconds, or -1 if the duration is unknown.
public final int segmentTimescale; * @param adaptationSets The adaptation sets belonging to the period.
*/
public final long presentationTimeOffset; public Period(String id, long start, long duration, List<AdaptationSet> adaptationSets) {
public Period(int id, long start, long duration, List<AdaptationSet> adaptationSets) {
this(id, start, duration, adaptationSets, null, 0, 0, 0);
}
public Period(int id, long start, long duration, List<AdaptationSet> adaptationSets,
List<Segment.Timeline> segmentList, int segmentStartNumber, int segmentTimescale) {
this(id, start, duration, adaptationSets, segmentList, segmentStartNumber, segmentTimescale, 0);
}
public Period(int id, long start, long duration, List<AdaptationSet> adaptationSets,
List<Segment.Timeline> segmentList, int segmentStartNumber, int segmentTimescale,
long presentationTimeOffset) {
this.id = id; this.id = id;
this.start = start; this.startMs = start;
this.duration = duration; this.durationMs = duration;
this.adaptationSets = Collections.unmodifiableList(adaptationSets); this.adaptationSets = Collections.unmodifiableList(adaptationSets);
if (segmentList != null) {
this.segmentList = Collections.unmodifiableList(segmentList);
} else {
this.segmentList = null;
}
this.segmentStartNumber = segmentStartNumber;
this.segmentTimescale = segmentTimescale;
this.presentationTimeOffset = presentationTimeOffset;
} }
} }

View File

@ -0,0 +1,138 @@
/*
* 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;
import com.google.android.exoplayer.util.Assertions;
import android.net.Uri;
/**
* Defines a range of data located at a {@link Uri}.
*/
public final class RangedUri {
/**
* The (zero based) index of the first byte of the range.
*/
public final long start;
/**
* The length of the range, or -1 to indicate that the range is unbounded.
*/
public final long length;
// The {@link Uri} is stored internally in two parts, {@link #baseUri} and {@link uriString}.
// This helps optimize memory usage in the same way that DASH manifests allow many URLs to be
// expressed concisely in the form of a single BaseURL and many relative paths. Note that this
// optimization relies on the same {@code Uri} being passed as the {@link #baseUri} to many
// instances of this class.
private final Uri baseUri;
private final String stringUri;
private int hashCode;
/**
* Constructs an ranged uri.
* <p>
* The uri is built according to the following rules:
* <ul>
* <li>If {@code baseUri} is null or if {@code stringUri} is absolute, then {@code baseUri} is
* ignored and the url consists solely of {@code stringUri}.
* <li>If {@code stringUri} is null, then the url consists solely of {@code baseUrl}.
* <li>Otherwise, the url consists of the concatenation of {@code baseUri} and {@code stringUri}.
* </ul>
*
* @param baseUri An uri that can form the base of the uri defined by the instance.
* @param stringUri A relative or absolute uri in string form.
* @param start The (zero based) index of the first byte of the range.
* @param length The length of the range, or -1 to indicate that the range is unbounded.
*/
public RangedUri(Uri baseUri, String stringUri, long start, long length) {
Assertions.checkArgument(baseUri != null || stringUri != null);
this.baseUri = baseUri;
this.stringUri = stringUri;
this.start = start;
this.length = length;
}
/**
* Returns the {@link Uri} represented by the instance.
*
* @return The {@link Uri} represented by the instance.
*/
public Uri getUri() {
if (stringUri == null) {
return baseUri;
}
Uri uri = Uri.parse(stringUri);
if (!uri.isAbsolute() && baseUri != null) {
uri = Uri.withAppendedPath(baseUri, stringUri);
}
return uri;
}
/**
* Attempts to merge this {@link RangedUri} with another.
* <p>
* A merge is successful if both instances define the same {@link Uri}, and if one starte the
* byte after the other ends, forming a contiguous region with no overlap.
* <p>
* If {@code other} is null then the merge is considered unsuccessful, and null is returned.
*
* @param other The {@link RangedUri} to merge.
* @return The merged {@link RangedUri} if the merge was successful. Null otherwise.
*/
public RangedUri attemptMerge(RangedUri other) {
if (other == null || !getUri().equals(other.getUri())) {
return null;
} else if (length != -1 && start + length == other.start) {
return new RangedUri(baseUri, stringUri, start,
other.length == -1 ? -1 : length + other.length);
} else if (other.length != -1 && other.start + other.length == start) {
return new RangedUri(baseUri, stringUri, other.start,
length == -1 ? -1 : other.length + length);
} else {
return null;
}
}
@Override
public int hashCode() {
if (hashCode == 0) {
int result = 17;
result = 31 * result + (int) start;
result = 31 * result + (int) length;
result = 31 * result + getUri().hashCode();
hashCode = result;
}
return hashCode;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
RangedUri other = (RangedUri) obj;
return this.start == other.start
&& this.length == other.length
&& getUri().equals(other.getUri());
}
}

View File

@ -16,13 +16,16 @@
package com.google.android.exoplayer.dash.mpd; package com.google.android.exoplayer.dash.mpd;
import com.google.android.exoplayer.chunk.Format; import com.google.android.exoplayer.chunk.Format;
import com.google.android.exoplayer.dash.DashSegmentIndex;
import com.google.android.exoplayer.dash.mpd.SegmentBase.MultiSegmentBase;
import com.google.android.exoplayer.dash.mpd.SegmentBase.SingleSegmentBase;
import android.net.Uri; import android.net.Uri;
/** /**
* A flat version of a DASH representation. * A DASH representation.
*/ */
public class Representation { public abstract class Representation {
/** /**
* Identifies the piece of content to which this {@link Representation} belongs. * Identifies the piece of content to which this {@link Representation} belongs.
@ -33,7 +36,7 @@ public class Representation {
public final String contentId; public final String contentId;
/** /**
* Identifies the revision of the {@link Representation}. * Identifies the revision of the content.
* <p> * <p>
* If the media for a given ({@link #contentId} can change over time without a change to the * If the media for a given ({@link #contentId} can change over time without a change to the
* {@link #format}'s {@link Format#id} (e.g. as a result of re-encoding the media with an * {@link #format}'s {@link Format#id} (e.g. as a result of re-encoding the media with an
@ -43,45 +46,93 @@ public class Representation {
public final long revisionId; public final long revisionId;
/** /**
* The format in which the {@link Representation} is encoded. * The format of the representation.
*/ */
public final Format format; public final Format format;
public final long contentLength; /**
* The start time of the enclosing period in milliseconds since the epoch.
*/
public final long periodStartMs;
public final long initializationStart; /**
* The duration of the enclosing period in milliseconds.
*/
public final long periodDurationMs;
public final long initializationEnd; /**
* The offset of the presentation timestamps in the media stream relative to media time.
*/
public final long presentationTimeOffsetMs;
public final long indexStart; private final RangedUri initializationUri;
public final long indexEnd; /**
* Constructs a new instance.
*
* @param periodStartMs The start time of the enclosing period in milliseconds.
* @param periodDurationMs The duration of the enclosing period in milliseconds, or -1 if the
* duration is unknown.
* @param contentId Identifies the piece of content to which this representation belongs.
* @param revisionId Identifies the revision of the content.
* @param format The format of the representation.
* @param segmentBase A segment base element for the representation.
* @return The constructed instance.
*/
public static Representation newInstance(long periodStartMs, long periodDurationMs,
String contentId, long revisionId, Format format, SegmentBase segmentBase) {
if (segmentBase instanceof SingleSegmentBase) {
return new SingleSegmentRepresentation(periodStartMs, periodDurationMs, contentId, revisionId,
format, (SingleSegmentBase) segmentBase, -1);
} else if (segmentBase instanceof MultiSegmentBase) {
return new MultiSegmentRepresentation(periodStartMs, periodDurationMs, contentId, revisionId,
format, (MultiSegmentBase) segmentBase);
} else {
throw new IllegalArgumentException("segmentBase must be of type SingleSegmentBase or "
+ "MultiSegmentBase");
}
}
public final long periodStart; private Representation(long periodStartMs, long periodDurationMs, String contentId,
long revisionId, Format format, SegmentBase segmentBase) {
public final long periodDuration; this.periodStartMs = periodStartMs;
this.periodDurationMs = periodDurationMs;
public final Uri uri;
public Representation(String contentId, long revisionId, Format format, Uri uri,
long contentLength, long initializationStart, long initializationEnd, long indexStart,
long indexEnd, long periodStart, long periodDuration) {
this.contentId = contentId; this.contentId = contentId;
this.revisionId = revisionId; this.revisionId = revisionId;
this.format = format; this.format = format;
this.contentLength = contentLength; initializationUri = segmentBase.getInitialization(this);
this.initializationStart = initializationStart; presentationTimeOffsetMs = (segmentBase.presentationTimeOffset * 1000) / segmentBase.timescale;
this.initializationEnd = initializationEnd;
this.indexStart = indexStart;
this.indexEnd = indexEnd;
this.periodStart = periodStart;
this.periodDuration = periodDuration;
this.uri = uri;
} }
/**
* Gets a {@link RangedUri} defining the location of the representation's initialization data.
* May be null if no initialization data exists.
*
* @return A {@link RangedUri} defining the location of the initialization data, or null.
*/
public RangedUri getInitializationUri() {
return initializationUri;
}
/**
* Gets a {@link RangedUri} defining the location of the representation's segment index. Null if
* the representation provides an index directly.
*
* @return The location of the segment index, or null.
*/
public abstract RangedUri getIndexUri();
/**
* Gets a segment index, if the representation is able to provide one directly. Null if the
* segment index is defined externally.
*
* @return The segment index, or null.
*/
public abstract DashSegmentIndex getIndex();
/** /**
* Generates a cache key for the {@link Representation}, in the format * Generates a cache key for the {@link Representation}, in the format
* {@link #contentId}.{@link #format.id}.{@link #revisionId}. * {@code contentId + "." + format.id + "." + revisionId}.
* *
* @return A cache key. * @return A cache key.
*/ */
@ -89,4 +140,143 @@ public class Representation {
return contentId + "." + format.id + "." + revisionId; return contentId + "." + format.id + "." + revisionId;
} }
/**
* A DASH representation consisting of a single segment.
*/
public static class SingleSegmentRepresentation extends Representation {
/**
* The {@link Uri} of the single segment.
*/
public final Uri uri;
/**
* The content length, or -1 if unknown.
*/
public final long contentLength;
private final RangedUri indexUri;
/**
* @param periodStartMs The start time of the enclosing period in milliseconds.
* @param periodDurationMs The duration of the enclosing period in milliseconds, or -1 if the
* duration is unknown.
* @param contentId Identifies the piece of content to which this representation belongs.
* @param revisionId Identifies the revision of the content.
* @param format The format of the representation.
* @param uri The uri of the media.
* @param initializationStart The offset of the first byte of initialization data.
* @param initializationEnd The offset of the last byte of initialization data.
* @param indexStart The offset of the first byte of index data.
* @param indexEnd The offset of the last byte of index data.
* @param contentLength The content length, or -1 if unknown.
*/
public static SingleSegmentRepresentation newInstance(long periodStartMs, long periodDurationMs,
String contentId, long revisionId, Format format, Uri uri, long initializationStart,
long initializationEnd, long indexStart, long indexEnd, long contentLength) {
RangedUri rangedUri = new RangedUri(uri, null, initializationStart,
initializationEnd - initializationStart + 1);
SingleSegmentBase segmentBase = new SingleSegmentBase(rangedUri, 1, 0, uri, indexStart,
indexEnd - indexStart + 1);
return new SingleSegmentRepresentation(periodStartMs, periodDurationMs, contentId, revisionId,
format, segmentBase, contentLength);
}
/**
* @param periodStartMs The start time of the enclosing period in milliseconds.
* @param periodDurationMs The duration of the enclosing period in milliseconds, or -1 if the
* duration is unknown.
* @param contentId Identifies the piece of content to which this representation belongs.
* @param revisionId Identifies the revision of the content.
* @param format The format of the representation.
* @param segmentBase The segment base underlying the representation.
* @param contentLength The content length, or -1 if unknown.
*/
public SingleSegmentRepresentation(long periodStartMs, long periodDurationMs, String contentId,
long revisionId, Format format, SingleSegmentBase segmentBase, long contentLength) {
super(periodStartMs, periodDurationMs, contentId, revisionId, format, segmentBase);
this.uri = segmentBase.uri;
this.indexUri = segmentBase.getIndex();
this.contentLength = contentLength;
}
@Override
public RangedUri getIndexUri() {
return indexUri;
}
@Override
public DashSegmentIndex getIndex() {
return null;
}
}
/**
* A DASH representation consisting of multiple segments.
*/
public static class MultiSegmentRepresentation extends Representation
implements DashSegmentIndex {
private final MultiSegmentBase segmentBase;
/**
* @param periodStartMs The start time of the enclosing period in milliseconds.
* @param periodDurationMs The duration of the enclosing period in milliseconds, or -1 if the
* duration is unknown.
* @param contentId Identifies the piece of content to which this representation belongs.
* @param revisionId Identifies the revision of the content.
* @param format The format of the representation.
* @param segmentBase The segment base underlying the representation.
*/
public MultiSegmentRepresentation(long periodStartMs, long periodDurationMs, String contentId,
long revisionId, Format format, MultiSegmentBase segmentBase) {
super(periodStartMs, periodDurationMs, contentId, revisionId, format, segmentBase);
this.segmentBase = segmentBase;
}
@Override
public RangedUri getIndexUri() {
return null;
}
@Override
public DashSegmentIndex getIndex() {
return this;
}
// DashSegmentIndex implementation.
@Override
public RangedUri getSegmentUrl(int segmentIndex) {
return segmentBase.getSegmentUrl(this, segmentIndex);
}
@Override
public int getSegmentNum(long timeUs) {
return segmentBase.getSegmentNum(timeUs);
}
@Override
public long getTimeUs(int segmentIndex) {
return segmentBase.getSegmentTimeUs(segmentIndex);
}
@Override
public long getDurationUs(int segmentIndex) {
return segmentBase.getSegmentDurationUs(segmentIndex);
}
@Override
public int getFirstSegmentNum() {
return segmentBase.getFirstSegmentNum();
}
@Override
public int getLastSegmentNum() {
return segmentBase.getLastSegmentNum();
}
}
} }

View File

@ -1,81 +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.mpd;
/**
* Represents a particular segment in a Representation.
*
*/
public abstract class Segment {
public final String relativeUri;
public final long sequenceNumber;
public final long duration;
public Segment(String relativeUri, long sequenceNumber, long duration) {
this.relativeUri = relativeUri;
this.sequenceNumber = sequenceNumber;
this.duration = duration;
}
/**
* Represents a timeline segment from the MPD's SegmentTimeline list.
*/
public static class Timeline extends Segment {
public Timeline(long sequenceNumber, long duration) {
super(null, sequenceNumber, duration);
}
}
/**
* Represents an initialization segment.
*/
public static class Initialization extends Segment {
public final long initializationStart;
public final long initializationEnd;
public Initialization(String relativeUri, long initializationStart,
long initializationEnd) {
super(relativeUri, -1, -1);
this.initializationStart = initializationStart;
this.initializationEnd = initializationEnd;
}
}
/**
* Represents a media segment.
*/
public static class Media extends Segment {
public final long mediaStart;
public Media(String relativeUri, long sequenceNumber, long duration) {
this(relativeUri, 0, sequenceNumber, duration);
}
public Media(String uri, long mediaStart, long sequenceNumber, long duration) {
super(uri, sequenceNumber, duration);
this.mediaStart = mediaStart;
}
}
}

View File

@ -1,49 +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.mpd;
import com.google.android.exoplayer.chunk.Format;
import com.google.android.exoplayer.upstream.DataSpec;
import android.net.Uri;
import java.util.List;
/**
* Represents a DASH Representation which uses the SegmentList structure (i.e. it has a list of
* Segment URLs instead of a single URL).
*/
public class SegmentedRepresentation extends Representation {
private List<Segment> segmentList;
public SegmentedRepresentation(String contentId, Format format, Uri uri, long initializationStart,
long initializationEnd, long indexStart, long indexEnd, long periodStart, long periodDuration,
List<Segment> segmentList) {
super(contentId, -1, format, uri, DataSpec.LENGTH_UNBOUNDED, initializationStart,
initializationEnd, indexStart, indexEnd, periodStart, periodDuration);
this.segmentList = segmentList;
}
public int getNumSegments() {
return segmentList.size();
}
public Segment getSegment(int i) {
return segmentList.get(i);
}
}

View File

@ -0,0 +1,164 @@
/*
* 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;
/**
* A template from which URLs can be built.
* <p>
* URLs are built according to the substitution rules defined in ISO/IEC 23009-1:2014 5.3.9.4.4.
*/
public final class UrlTemplate {
private static final String REPRESENTATION = "RepresentationID";
private static final String NUMBER = "Number";
private static final String BANDWIDTH = "Bandwidth";
private static final String TIME = "Time";
private static final String ESCAPED_DOLLAR = "$$";
private static final String DEFAULT_FORMAT_TAG = "%01d";
private static final int REPRESENTATION_ID = 1;
private static final int NUMBER_ID = 2;
private static final int BANDWIDTH_ID = 3;
private static final int TIME_ID = 4;
private final String[] urlPieces;
private final int[] identifiers;
private final String[] identifierFormatTags;
private final int identifierCount;
/**
* Compile an instance from the provided template string.
*
* @param template The template.
* @return The compiled instance.
* @throws IllegalArgumentException If the template string is malformed.
*/
public static UrlTemplate compile(String template) {
// These arrays are sizes assuming each of the four possible identifiers will be present at
// most once in the template, which seems like a reasonable assumption.
String[] urlPieces = new String[5];
int[] identifiers = new int[4];
String[] identifierFormatTags = new String[4];
int identifierCount = parseTemplate(template, urlPieces, identifiers, identifierFormatTags);
return new UrlTemplate(urlPieces, identifiers, identifierFormatTags, identifierCount);
}
/**
* Internal constructor. Use {@link #compile(String)} to build instances of this class.
*/
private UrlTemplate(String[] urlPieces, int[] identifiers, String[] identifierFormatTags,
int identifierCount) {
this.urlPieces = urlPieces;
this.identifiers = identifiers;
this.identifierFormatTags = identifierFormatTags;
this.identifierCount = identifierCount;
}
/**
* Constructs a Uri from the template, substituting in the provided arguments.
* <p>
* Arguments whose corresponding identifiers are not present in the template will be ignored.
*
* @param representationId The representation identifier.
* @param segmentNumber The segment number.
* @param bandwidth The bandwidth.
* @param time The time as specified by the segment timeline.
* @return The built Uri.
*/
public String buildUri(String representationId, int segmentNumber, int bandwidth, long time) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < identifierCount; i++) {
builder.append(urlPieces[i]);
if (identifiers[i] == REPRESENTATION_ID) {
builder.append(representationId);
} else if (identifiers[i] == NUMBER_ID) {
builder.append(String.format(identifierFormatTags[i], segmentNumber));
} else if (identifiers[i] == BANDWIDTH_ID) {
builder.append(String.format(identifierFormatTags[i], bandwidth));
} else if (identifiers[i] == TIME_ID) {
builder.append(String.format(identifierFormatTags[i], time));
}
}
builder.append(urlPieces[identifierCount]);
return builder.toString();
}
/**
* Parses {@code template}, placing the decomposed components into the provided arrays.
* <p>
* If the return value is N, {@code urlPieces} will contain (N+1) strings that must be
* interleaved with N arguments in order to construct a url. The N identifiers that correspond to
* the required arguments, together with the tags that define their required formatting, are
* returned in {@code identifiers} and {@code identifierFormatTags} respectively.
*
* @param template The template to parse.
* @param urlPieces A holder for pieces of url parsed from the template.
* @param identifiers A holder for identifiers parsed from the template.
* @param identifierFormatTags A holder for format tags corresponding to the parsed identifiers.
* @return The number of identifiers in the template url.
* @throws IllegalArgumentException If the template string is malformed.
*/
private static int parseTemplate(String template, String[] urlPieces, int[] identifiers,
String[] identifierFormatTags) {
urlPieces[0] = "";
int templateIndex = 0;
int identifierCount = 0;
while (templateIndex < template.length()) {
int dollarIndex = template.indexOf("$", templateIndex);
if (dollarIndex == -1) {
urlPieces[identifierCount] += template.substring(templateIndex);
templateIndex = template.length();
} else if (dollarIndex != templateIndex) {
urlPieces[identifierCount] += template.substring(templateIndex, dollarIndex);
templateIndex = dollarIndex;
} else if (template.startsWith(ESCAPED_DOLLAR, templateIndex)) {
urlPieces[identifierCount] += "$";
templateIndex += 2;
} else {
int secondIndex = template.indexOf("$", templateIndex + 1);
String identifier = template.substring(templateIndex + 1, secondIndex);
if (identifier.equals(REPRESENTATION)) {
identifiers[identifierCount] = REPRESENTATION_ID;
} else {
int formatTagIndex = identifier.indexOf("%0");
String formatTag = DEFAULT_FORMAT_TAG;
if (formatTagIndex != -1) {
formatTag = identifier.substring(formatTagIndex);
if (!formatTag.endsWith("d")) {
formatTag += "d";
}
identifier = identifier.substring(0, formatTagIndex);
}
if (identifier.equals(NUMBER)) {
identifiers[identifierCount] = NUMBER_ID;
} else if (identifier.equals(BANDWIDTH)) {
identifiers[identifierCount] = BANDWIDTH_ID;
} else if (identifier.equals(TIME)) {
identifiers[identifierCount] = TIME_ID;
} else {
throw new IllegalArgumentException("Invalid template: " + template);
}
identifierFormatTags[identifierCount] = formatTag;
}
identifierCount++;
urlPieces[identifierCount] = "";
templateIndex = secondIndex + 1;
}
}
return identifierCount;
}
}

View File

@ -21,6 +21,7 @@ import java.util.List;
/* package */ abstract class Atom { /* package */ abstract class Atom {
public static final int TYPE_avc1 = 0x61766331; public static final int TYPE_avc1 = 0x61766331;
public static final int TYPE_avc3 = 0x61766333;
public static final int TYPE_esds = 0x65736473; public static final int TYPE_esds = 0x65736473;
public static final int TYPE_mdat = 0x6D646174; public static final int TYPE_mdat = 0x6D646174;
public static final int TYPE_mfhd = 0x6D666864; public static final int TYPE_mfhd = 0x6D666864;

View File

@ -49,6 +49,15 @@ import java.util.UUID;
*/ */
public final class FragmentedMp4Extractor { public final class FragmentedMp4Extractor {
/**
* Flag to work around an issue in some video streams where every frame is marked as a sync frame.
* The workaround overrides the sync frame flags in the stream, forcing them to false except for
* the first sample in each segment.
* <p>
* This flag does nothing if the stream is not a video stream.
*/
public static final int WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME = 1;
/** /**
* An attempt to read from the input stream returned 0 bytes of data. * An attempt to read from the input stream returned 0 bytes of data.
*/ */
@ -74,9 +83,13 @@ public final class FragmentedMp4Extractor {
* A sidx atom was read. The parsed data can be read using {@link #getSegmentIndex()}. * A sidx atom was read. The parsed data can be read using {@link #getSegmentIndex()}.
*/ */
public static final int RESULT_READ_SIDX = 32; public static final int RESULT_READ_SIDX = 32;
/**
* The next thing to be read is a sample, but a {@link SampleHolder} was not supplied.
*/
public static final int RESULT_NEED_SAMPLE_HOLDER = 64;
private static final int READ_TERMINATING_RESULTS = RESULT_NEED_MORE_DATA | RESULT_END_OF_STREAM private static final int READ_TERMINATING_RESULTS = RESULT_NEED_MORE_DATA | RESULT_END_OF_STREAM
| RESULT_READ_SAMPLE_FULL; | RESULT_READ_SAMPLE_FULL | RESULT_NEED_SAMPLE_HOLDER;
private static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1}; private static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1};
private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE = private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE =
new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12}; new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12};
@ -97,6 +110,7 @@ public final class FragmentedMp4Extractor {
static { static {
HashSet<Integer> parsedAtoms = new HashSet<Integer>(); HashSet<Integer> parsedAtoms = new HashSet<Integer>();
parsedAtoms.add(Atom.TYPE_avc1); parsedAtoms.add(Atom.TYPE_avc1);
parsedAtoms.add(Atom.TYPE_avc3);
parsedAtoms.add(Atom.TYPE_esds); parsedAtoms.add(Atom.TYPE_esds);
parsedAtoms.add(Atom.TYPE_hdlr); parsedAtoms.add(Atom.TYPE_hdlr);
parsedAtoms.add(Atom.TYPE_mdat); parsedAtoms.add(Atom.TYPE_mdat);
@ -140,7 +154,7 @@ public final class FragmentedMp4Extractor {
CONTAINER_TYPES = Collections.unmodifiableSet(atomContainerTypes); CONTAINER_TYPES = Collections.unmodifiableSet(atomContainerTypes);
} }
private final boolean enableSmoothStreamingWorkarounds; private final int workaroundFlags;
// Parser state // Parser state
private final ParsableByteArray atomHeader; private final ParsableByteArray atomHeader;
@ -172,16 +186,15 @@ public final class FragmentedMp4Extractor {
private TrackFragment fragmentRun; private TrackFragment fragmentRun;
public FragmentedMp4Extractor() { public FragmentedMp4Extractor() {
this(false); this(0);
} }
/** /**
* @param enableSmoothStreamingWorkarounds Set to true if this extractor will be used to parse * @param workaroundFlags Flags to allow parsing of faulty streams.
* SmoothStreaming streams. This will enable workarounds for SmoothStreaming violations of * {@link #WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME} is currently the only flag defined.
* the ISO base media file format (ISO 14496-12). Set to false otherwise.
*/ */
public FragmentedMp4Extractor(boolean enableSmoothStreamingWorkarounds) { public FragmentedMp4Extractor(int workaroundFlags) {
this.enableSmoothStreamingWorkarounds = enableSmoothStreamingWorkarounds; this.workaroundFlags = workaroundFlags;
parserState = STATE_READING_ATOM_HEADER; parserState = STATE_READING_ATOM_HEADER;
atomHeader = new ParsableByteArray(ATOM_HEADER_SIZE); atomHeader = new ParsableByteArray(ATOM_HEADER_SIZE);
containerAtoms = new Stack<ContainerAtom>(); containerAtoms = new Stack<ContainerAtom>();
@ -263,7 +276,8 @@ public final class FragmentedMp4Extractor {
* in subsequent calls until the whole sample has been read. * in subsequent calls until the whole sample has been read.
* *
* @param inputStream The input stream from which data should be read. * @param inputStream The input stream from which data should be read.
* @param out A {@link SampleHolder} into which the sample 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. * @return One or more of the {@code RESULT_*} flags defined in this class.
* @throws ParserException If an error occurs parsing the media data. * @throws ParserException If an error occurs parsing the media data.
*/ */
@ -466,7 +480,7 @@ public final class FragmentedMp4Extractor {
private void onMoofContainerAtomRead(ContainerAtom moof) { private void onMoofContainerAtomRead(ContainerAtom moof) {
fragmentRun = new TrackFragment(); fragmentRun = new TrackFragment();
parseMoof(track, extendsDefaults, moof, fragmentRun, enableSmoothStreamingWorkarounds); parseMoof(track, extendsDefaults, moof, fragmentRun, workaroundFlags);
sampleIndex = 0; sampleIndex = 0;
lastSyncSampleIndex = 0; lastSyncSampleIndex = 0;
pendingSeekSyncSampleIndex = 0; pendingSeekSyncSampleIndex = 0;
@ -572,11 +586,12 @@ public final class FragmentedMp4Extractor {
int childStartPosition = stsd.getPosition(); int childStartPosition = stsd.getPosition();
int childAtomSize = stsd.readInt(); int childAtomSize = stsd.readInt();
int childAtomType = stsd.readInt(); int childAtomType = stsd.readInt();
if (childAtomType == Atom.TYPE_avc1 || childAtomType == Atom.TYPE_encv) { if (childAtomType == Atom.TYPE_avc1 || childAtomType == Atom.TYPE_avc3
Pair<MediaFormat, TrackEncryptionBox> avc1 = || childAtomType == Atom.TYPE_encv) {
parseAvc1FromParent(stsd, childStartPosition, childAtomSize); Pair<MediaFormat, TrackEncryptionBox> avc =
mediaFormat = avc1.first; parseAvcFromParent(stsd, childStartPosition, childAtomSize);
trackEncryptionBoxes[i] = avc1.second; mediaFormat = avc.first;
trackEncryptionBoxes[i] = avc.second;
} else if (childAtomType == Atom.TYPE_mp4a || childAtomType == Atom.TYPE_enca) { } else if (childAtomType == Atom.TYPE_mp4a || childAtomType == Atom.TYPE_enca) {
Pair<MediaFormat, TrackEncryptionBox> mp4a = Pair<MediaFormat, TrackEncryptionBox> mp4a =
parseMp4aFromParent(stsd, childStartPosition, childAtomSize); parseMp4aFromParent(stsd, childStartPosition, childAtomSize);
@ -588,7 +603,7 @@ public final class FragmentedMp4Extractor {
return Pair.create(mediaFormat, trackEncryptionBoxes); return Pair.create(mediaFormat, trackEncryptionBoxes);
} }
private static Pair<MediaFormat, TrackEncryptionBox> parseAvc1FromParent(ParsableByteArray parent, private static Pair<MediaFormat, TrackEncryptionBox> parseAvcFromParent(ParsableByteArray parent,
int position, int size) { int position, int size) {
parent.setPosition(position + ATOM_HEADER_SIZE); parent.setPosition(position + ATOM_HEADER_SIZE);
@ -695,7 +710,7 @@ public final class FragmentedMp4Extractor {
int childAtomSize = parent.readInt(); int childAtomSize = parent.readInt();
int childAtomType = parent.readInt(); int childAtomType = parent.readInt();
if (childAtomType == Atom.TYPE_frma) { if (childAtomType == Atom.TYPE_frma) {
parent.readInt(); // dataFormat. Expect TYPE_avc1 (video) or TYPE_mp4a (audio). parent.readInt(); // dataFormat.
} else if (childAtomType == Atom.TYPE_schm) { } else if (childAtomType == Atom.TYPE_schm) {
parent.skip(4); parent.skip(4);
parent.readInt(); // schemeType. Expect cenc parent.readInt(); // schemeType. Expect cenc
@ -774,11 +789,11 @@ public final class FragmentedMp4Extractor {
} }
private static void parseMoof(Track track, DefaultSampleValues extendsDefaults, private static void parseMoof(Track track, DefaultSampleValues extendsDefaults,
ContainerAtom moof, TrackFragment out, boolean enableSmoothStreamingWorkarounds) { ContainerAtom moof, TrackFragment out, int workaroundFlags) {
// TODO: Consider checking that the sequence number returned by parseMfhd is as expected. // TODO: Consider checking that the sequence number returned by parseMfhd is as expected.
parseMfhd(moof.getLeafAtomOfType(Atom.TYPE_mfhd).getData()); parseMfhd(moof.getLeafAtomOfType(Atom.TYPE_mfhd).getData());
parseTraf(track, extendsDefaults, moof.getContainerAtomOfType(Atom.TYPE_traf), parseTraf(track, extendsDefaults, moof.getContainerAtomOfType(Atom.TYPE_traf),
out, enableSmoothStreamingWorkarounds); out, workaroundFlags);
} }
/** /**
@ -796,7 +811,7 @@ public final class FragmentedMp4Extractor {
* Parses a traf atom (defined in 14496-12). * Parses a traf atom (defined in 14496-12).
*/ */
private static void parseTraf(Track track, DefaultSampleValues extendsDefaults, private static void parseTraf(Track track, DefaultSampleValues extendsDefaults,
ContainerAtom traf, TrackFragment out, boolean enableSmoothStreamingWorkarounds) { ContainerAtom traf, TrackFragment out, int workaroundFlags) {
LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz); LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz);
if (saiz != null) { if (saiz != null) {
parseSaiz(saiz.getData(), out); parseSaiz(saiz.getData(), out);
@ -809,8 +824,7 @@ public final class FragmentedMp4Extractor {
out.setSampleDescriptionIndex(fragmentHeader.sampleDescriptionIndex); out.setSampleDescriptionIndex(fragmentHeader.sampleDescriptionIndex);
LeafAtom trun = traf.getLeafAtomOfType(Atom.TYPE_trun); LeafAtom trun = traf.getLeafAtomOfType(Atom.TYPE_trun);
parseTrun(track, fragmentHeader, decodeTime, enableSmoothStreamingWorkarounds, trun.getData(), parseTrun(track, fragmentHeader, decodeTime, workaroundFlags, trun.getData(), out);
out);
LeafAtom uuid = traf.getLeafAtomOfType(Atom.TYPE_uuid); LeafAtom uuid = traf.getLeafAtomOfType(Atom.TYPE_uuid);
if (uuid != null) { if (uuid != null) {
parseUuid(uuid.getData(), out); parseUuid(uuid.getData(), out);
@ -895,8 +909,7 @@ public final class FragmentedMp4Extractor {
* @param out The {@TrackFragment} into which parsed data should be placed. * @param out The {@TrackFragment} into which parsed data should be placed.
*/ */
private static void parseTrun(Track track, DefaultSampleValues defaultSampleValues, private static void parseTrun(Track track, DefaultSampleValues defaultSampleValues,
long decodeTime, boolean enableSmoothStreamingWorkarounds, ParsableByteArray trun, long decodeTime, int workaroundFlags, ParsableByteArray trun, TrackFragment out) {
TrackFragment out) {
trun.setPosition(ATOM_HEADER_SIZE); trun.setPosition(ATOM_HEADER_SIZE);
int fullAtom = trun.readInt(); int fullAtom = trun.readInt();
int version = parseFullAtomVersion(fullAtom); int version = parseFullAtomVersion(fullAtom);
@ -926,6 +939,9 @@ public final class FragmentedMp4Extractor {
long timescale = track.timescale; long timescale = track.timescale;
long cumulativeTime = decodeTime; long cumulativeTime = decodeTime;
boolean workaroundEveryVideoFrameIsSyncFrame = track.type == Track.TYPE_VIDEO
&& ((workaroundFlags & WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME)
== WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME);
for (int i = 0; i < numberOfEntries; i++) { for (int i = 0; i < numberOfEntries; i++) {
// Use trun values if present, otherwise tfhd, otherwise trex. // Use trun values if present, otherwise tfhd, otherwise trex.
int sampleDuration = sampleDurationsPresent ? trun.readUnsignedIntToInt() int sampleDuration = sampleDurationsPresent ? trun.readUnsignedIntToInt()
@ -934,11 +950,14 @@ public final class FragmentedMp4Extractor {
int sampleFlags = (i == 0 && firstSampleFlagsPresent) ? firstSampleFlags int sampleFlags = (i == 0 && firstSampleFlagsPresent) ? firstSampleFlags
: sampleFlagsPresent ? trun.readInt() : defaultSampleValues.flags; : sampleFlagsPresent ? trun.readInt() : defaultSampleValues.flags;
if (sampleCompositionTimeOffsetsPresent) { if (sampleCompositionTimeOffsetsPresent) {
// Fragmented mp4 streams packaged for smooth streaming violate the BMFF spec by specifying
// the sample offset as a signed integer in conjunction with a box version of 0.
int sampleOffset; int sampleOffset;
if (version == 0 && !enableSmoothStreamingWorkarounds) { if (version == 0) {
sampleOffset = trun.readUnsignedIntToInt(); // The BMFF spec (ISO 14496-12) states that sample offsets should be unsigned integers in
// version 0 trun boxes, however a significant number of streams violate the spec and use
// signed integers instead. It's safe to always parse sample offsets as signed integers
// here, because unsigned integers will still be parsed correctly (unless their top bit is
// set, which is never true in practice because sample offsets are always small).
sampleOffset = trun.readInt();
} else { } else {
sampleOffset = trun.readInt(); sampleOffset = trun.readInt();
} }
@ -947,9 +966,7 @@ public final class FragmentedMp4Extractor {
sampleDecodingTimeTable[i] = (int) ((cumulativeTime * 1000) / timescale); sampleDecodingTimeTable[i] = (int) ((cumulativeTime * 1000) / timescale);
sampleSizeTable[i] = sampleSize; sampleSizeTable[i] = sampleSize;
boolean isSync = ((sampleFlags >> 16) & 0x1) == 0; boolean isSync = ((sampleFlags >> 16) & 0x1) == 0;
if (track.type == Track.TYPE_VIDEO && enableSmoothStreamingWorkarounds && i != 0) { if (workaroundEveryVideoFrameIsSyncFrame && i != 0) {
// Fragmented mp4 streams packaged for smooth streaming violate the BMFF spec by indicating
// that every sample is a sync frame, when this is not actually the case.
isSync = false; isSync = false;
} }
if (isSync) { if (isSync) {
@ -1130,6 +1147,9 @@ public final class FragmentedMp4Extractor {
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
private int readSample(NonBlockingInputStream inputStream, SampleHolder out) { private int readSample(NonBlockingInputStream inputStream, SampleHolder out) {
if (out == null) {
return RESULT_NEED_SAMPLE_HOLDER;
}
int sampleSize = fragmentRun.sampleSizeTable[sampleIndex]; int sampleSize = fragmentRun.sampleSizeTable[sampleIndex];
ByteBuffer outputData = out.data; ByteBuffer outputData = out.data;
if (parserState == STATE_READING_SAMPLE_START) { if (parserState == STATE_READING_SAMPLE_START) {

View File

@ -0,0 +1,558 @@
/*
* 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.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.Assertions;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Stack;
/**
* Default version of a basic event-driven incremental EBML parser which needs an
* {@link EbmlEventHandler} to define IDs/types and react to events.
*
* <p>EBML can be summarized as a binary XML format somewhat similar to Protocol Buffers.
* It was originally designed for the Matroska container format. More information about EBML and
* Matroska is available <a href="http://www.matroska.org/technical/specs/index.html">here</a>.
*/
/* package */ final class DefaultEbmlReader implements EbmlReader {
// State values used in variables state, elementIdState, elementContentSizeState, and
// varintBytesState.
private static final int STATE_BEGIN_READING = 0;
private static final int STATE_READ_CONTENTS = 1;
private static final int STATE_FINISHED_READING = 2;
/**
* The first byte of a variable-length integer (varint) will have one of these bit masks
* indicating the total length in bytes.
*
* <p>{@code 0x80} is a one-byte integer, {@code 0x40} is two bytes, and so on up to eight bytes.
*/
private static final int[] VARINT_LENGTH_MASKS = new int[] {
0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01
};
private static final int MAX_INTEGER_ELEMENT_SIZE_BYTES = 8;
private static final int VALID_FLOAT32_ELEMENT_SIZE_BYTES = 4;
private static final int VALID_FLOAT64_ELEMENT_SIZE_BYTES = 8;
/**
* Scratch space to read in EBML varints, unsigned ints, and floats - each of which can be
* up to 8 bytes.
*/
private final byte[] tempByteArray = new byte[8];
private final Stack<MasterElement> masterElementsStack = new Stack<MasterElement>();
/**
* Current {@link EbmlEventHandler} which is queried for element types
* and informed of element events.
*/
private EbmlEventHandler eventHandler;
/**
* Overall state for the current element. Must be one of the {@code STATE_*} constants.
*/
private int state;
/**
* Total bytes read since starting or the last {@link #reset()}.
*/
private long bytesRead;
/**
* The starting byte offset of the current element being parsed.
*/
private long elementOffset;
/**
* Holds the current element ID after {@link #elementIdState} is {@link #STATE_FINISHED_READING}.
*/
private int elementId;
/**
* State for the ID of the current element. Must be one of the {@code STATE_*} constants.
*/
private int elementIdState;
/**
* Holds the current element content size after {@link #elementContentSizeState}
* is {@link #STATE_FINISHED_READING}.
*/
private long elementContentSize;
/**
* State for the content size of the current element.
* Must be one of the {@code STATE_*} constants.
*/
private int elementContentSizeState;
/**
* State for the current variable-length integer (varint) being read into
* {@link #tempByteArray}. Must be one of the {@code STATE_*} constants.
*/
private int varintBytesState;
/**
* Length in bytes of the current variable-length integer (varint) being read into
* {@link #tempByteArray}.
*/
private int varintBytesLength;
/**
* Counts the number of bytes being contiguously read into either {@link #tempByteArray} or
* {@link #stringBytes}. Used to determine when all required bytes have been read across
* multiple calls.
*/
private int bytesState;
/**
* Holds string element bytes as they're being read in. Allocated after the element content
* size is known and released after calling {@link EbmlEventHandler#onStringElement(int, String)}.
*/
private byte[] stringBytes;
@Override
public void setEventHandler(EbmlEventHandler eventHandler) {
this.eventHandler = eventHandler;
}
@Override
public int read(NonBlockingInputStream inputStream) {
Assertions.checkState(eventHandler != null);
while (true) {
while (!masterElementsStack.isEmpty()
&& bytesRead >= masterElementsStack.peek().elementEndOffsetBytes) {
if (!eventHandler.onMasterElementEnd(masterElementsStack.pop().elementId)) {
return READ_RESULT_CONTINUE;
}
}
if (state == STATE_BEGIN_READING) {
int idResult = readElementId(inputStream);
if (idResult != READ_RESULT_CONTINUE) {
return idResult;
}
int sizeResult = readElementContentSize(inputStream);
if (sizeResult != READ_RESULT_CONTINUE) {
return sizeResult;
}
state = STATE_READ_CONTENTS;
bytesState = 0;
}
int type = eventHandler.getElementType(elementId);
switch (type) {
case TYPE_MASTER:
int masterHeaderSize = (int) (bytesRead - elementOffset); // Header size is 12 bytes max.
masterElementsStack.add(new MasterElement(elementId, bytesRead + elementContentSize));
if (!eventHandler.onMasterElementStart(
elementId, elementOffset, masterHeaderSize, elementContentSize)) {
prepareForNextElement();
return READ_RESULT_CONTINUE;
}
break;
case TYPE_UNSIGNED_INT:
if (elementContentSize > MAX_INTEGER_ELEMENT_SIZE_BYTES) {
throw new IllegalStateException("Invalid integer size " + elementContentSize);
}
int intResult =
readBytesInternal(inputStream, tempByteArray, (int) elementContentSize);
if (intResult != READ_RESULT_CONTINUE) {
return intResult;
}
long intValue = getTempByteArrayValue((int) elementContentSize, false);
if (!eventHandler.onIntegerElement(elementId, intValue)) {
prepareForNextElement();
return READ_RESULT_CONTINUE;
}
break;
case TYPE_FLOAT:
if (elementContentSize != VALID_FLOAT32_ELEMENT_SIZE_BYTES
&& elementContentSize != VALID_FLOAT64_ELEMENT_SIZE_BYTES) {
throw new IllegalStateException("Invalid float size " + elementContentSize);
}
int floatResult =
readBytesInternal(inputStream, tempByteArray, (int) elementContentSize);
if (floatResult != READ_RESULT_CONTINUE) {
return floatResult;
}
long valueBits = getTempByteArrayValue((int) elementContentSize, false);
double floatValue;
if (elementContentSize == VALID_FLOAT32_ELEMENT_SIZE_BYTES) {
floatValue = Float.intBitsToFloat((int) valueBits);
} else {
floatValue = Double.longBitsToDouble(valueBits);
}
if (!eventHandler.onFloatElement(elementId, floatValue)) {
prepareForNextElement();
return READ_RESULT_CONTINUE;
}
break;
case TYPE_STRING:
if (elementContentSize > Integer.MAX_VALUE) {
throw new IllegalStateException(
"String element size " + elementContentSize + " is larger than MAX_INT");
}
if (stringBytes == null) {
stringBytes = new byte[(int) elementContentSize];
}
int stringResult =
readBytesInternal(inputStream, stringBytes, (int) elementContentSize);
if (stringResult != READ_RESULT_CONTINUE) {
return stringResult;
}
String stringValue = new String(stringBytes, Charset.forName("UTF-8"));
stringBytes = null;
if (!eventHandler.onStringElement(elementId, stringValue)) {
prepareForNextElement();
return READ_RESULT_CONTINUE;
}
break;
case TYPE_BINARY:
if (elementContentSize > Integer.MAX_VALUE) {
throw new IllegalStateException(
"Binary element size " + elementContentSize + " is larger than MAX_INT");
}
if (inputStream.getAvailableByteCount() < elementContentSize) {
return READ_RESULT_NEED_MORE_DATA;
}
int binaryHeaderSize = (int) (bytesRead - elementOffset); // Header size is 12 bytes max.
boolean keepGoing = eventHandler.onBinaryElement(
elementId, elementOffset, binaryHeaderSize, (int) elementContentSize, inputStream);
long expectedBytesRead = elementOffset + binaryHeaderSize + elementContentSize;
if (expectedBytesRead != bytesRead) {
throw new IllegalStateException("Incorrect total bytes read. Expected "
+ expectedBytesRead + " but actually " + bytesRead);
}
if (!keepGoing) {
prepareForNextElement();
return READ_RESULT_CONTINUE;
}
break;
case TYPE_UNKNOWN:
if (elementContentSize > Integer.MAX_VALUE) {
throw new IllegalStateException(
"Unknown element size " + elementContentSize + " is larger than MAX_INT");
}
int skipResult = skipBytesInternal(inputStream, (int) elementContentSize);
if (skipResult != READ_RESULT_CONTINUE) {
return skipResult;
}
break;
default:
throw new IllegalStateException("Invalid element type " + type);
}
prepareForNextElement();
}
}
@Override
public long getBytesRead() {
return bytesRead;
}
@Override
public void reset() {
prepareForNextElement();
masterElementsStack.clear();
bytesRead = 0;
}
@Override
public long readVarint(NonBlockingInputStream inputStream) {
varintBytesState = STATE_BEGIN_READING;
int result = readVarintBytes(inputStream);
if (result != READ_RESULT_CONTINUE) {
throw new IllegalStateException("Couldn't read varint");
}
return getTempByteArrayValue(varintBytesLength, true);
}
@Override
public void readBytes(NonBlockingInputStream inputStream, ByteBuffer byteBuffer, int totalBytes) {
bytesState = 0;
int result = readBytesInternal(inputStream, byteBuffer, totalBytes);
if (result != READ_RESULT_CONTINUE) {
throw new IllegalStateException("Couldn't read bytes into buffer");
}
}
@Override
public void readBytes(NonBlockingInputStream inputStream, byte[] byteArray, int totalBytes) {
bytesState = 0;
int result = readBytesInternal(inputStream, byteArray, totalBytes);
if (result != READ_RESULT_CONTINUE) {
throw new IllegalStateException("Couldn't read bytes into array");
}
}
@Override
public void skipBytes(NonBlockingInputStream inputStream, int totalBytes) {
bytesState = 0;
int result = skipBytesInternal(inputStream, totalBytes);
if (result != READ_RESULT_CONTINUE) {
throw new IllegalStateException("Couldn't skip bytes");
}
}
/**
* Resets the internal state of {@link #read(NonBlockingInputStream)} so that it can start
* reading a new element from scratch.
*/
private void prepareForNextElement() {
state = STATE_BEGIN_READING;
elementIdState = STATE_BEGIN_READING;
elementContentSizeState = STATE_BEGIN_READING;
elementOffset = bytesRead;
}
/**
* Reads an element ID such that reading can be stopped and started again in a later call
* if not enough bytes are available. Returns {@link #READ_RESULT_CONTINUE} if a full element ID
* has been read into {@link #elementId}. Reset {@link #elementIdState} to
* {@link #STATE_BEGIN_READING} before calling to indicate a new element ID should be read.
*
* @param inputStream The input stream from which an element ID should be read
* @return One of the {@code RESULT_*} flags defined in this class
*/
private int readElementId(NonBlockingInputStream inputStream) {
if (elementIdState == STATE_FINISHED_READING) {
return READ_RESULT_CONTINUE;
}
if (elementIdState == STATE_BEGIN_READING) {
varintBytesState = STATE_BEGIN_READING;
elementIdState = STATE_READ_CONTENTS;
}
int result = readVarintBytes(inputStream);
if (result != READ_RESULT_CONTINUE) {
return result;
}
// Element IDs are at most 4 bytes so cast to int now.
elementId = (int) getTempByteArrayValue(varintBytesLength, false);
elementIdState = STATE_FINISHED_READING;
return READ_RESULT_CONTINUE;
}
/**
* Reads an element's content size such that reading can be stopped and started again in a later
* call if not enough bytes are available.
*
* <p>Returns {@link #READ_RESULT_CONTINUE} if an entire element size has been
* read into {@link #elementContentSize}. Reset {@link #elementContentSizeState} to
* {@link #STATE_BEGIN_READING} before calling to indicate a new element size should be read.
*
* @param inputStream The input stream from which an element size should be read
* @return One of the {@code RESULT_*} flags defined in this class
*/
private int readElementContentSize(NonBlockingInputStream inputStream) {
if (elementContentSizeState == STATE_FINISHED_READING) {
return READ_RESULT_CONTINUE;
}
if (elementContentSizeState == STATE_BEGIN_READING) {
varintBytesState = STATE_BEGIN_READING;
elementContentSizeState = STATE_READ_CONTENTS;
}
int result = readVarintBytes(inputStream);
if (result != READ_RESULT_CONTINUE) {
return result;
}
elementContentSize = getTempByteArrayValue(varintBytesLength, true);
elementContentSizeState = STATE_FINISHED_READING;
return READ_RESULT_CONTINUE;
}
/**
* Reads an EBML variable-length integer (varint) such that reading can be stopped and started
* again in a later call if not enough bytes are available.
*
* <p>Returns {@link #READ_RESULT_CONTINUE} if an entire varint has been read into
* {@link #tempByteArray} and the length of the varint is in {@link #varintBytesLength}.
* Reset {@link #varintBytesState} to {@link #STATE_BEGIN_READING} before calling to indicate
* a new varint should be read.
*
* @param inputStream The input stream from which a varint should be read
* @return One of the {@code RESULT_*} flags defined in this class
*/
private int readVarintBytes(NonBlockingInputStream inputStream) {
if (varintBytesState == STATE_FINISHED_READING) {
return READ_RESULT_CONTINUE;
}
// Read first byte to get length.
if (varintBytesState == STATE_BEGIN_READING) {
bytesState = 0;
int result = readBytesInternal(inputStream, tempByteArray, 1);
if (result != READ_RESULT_CONTINUE) {
return result;
}
varintBytesState = STATE_READ_CONTENTS;
int firstByte = tempByteArray[0] & 0xff;
varintBytesLength = -1;
for (int i = 0; i < VARINT_LENGTH_MASKS.length; i++) {
if ((VARINT_LENGTH_MASKS[i] & firstByte) != 0) {
varintBytesLength = i + 1;
break;
}
}
if (varintBytesLength == -1) {
throw new IllegalStateException(
"No valid varint length mask found at bytesRead = " + bytesRead);
}
}
// Read remaining bytes.
int result = readBytesInternal(inputStream, tempByteArray, varintBytesLength);
if (result != READ_RESULT_CONTINUE) {
return result;
}
// All bytes have been read.
return READ_RESULT_CONTINUE;
}
/**
* Reads a set amount of bytes into a {@link ByteBuffer} such that reading can be stopped
* and started again later if not enough bytes are available.
*
* <p>Returns {@link #READ_RESULT_CONTINUE} if all bytes have been read. Reset
* {@link #bytesState} to {@code 0} before calling to indicate a new set of bytes should be read.
*
* @param inputStream The input stream from which bytes should be read
* @param byteBuffer The {@link ByteBuffer} into which bytes should be read
* @param totalBytes The total size of bytes to be read
* @return One of the {@code RESULT_*} flags defined in this class
*/
private int readBytesInternal(
NonBlockingInputStream inputStream, ByteBuffer byteBuffer, int totalBytes) {
if (bytesState == STATE_BEGIN_READING && totalBytes > byteBuffer.capacity()) {
throw new IllegalArgumentException("Byte buffer not large enough");
}
if (bytesState >= totalBytes) {
return READ_RESULT_CONTINUE;
}
int remainingBytes = totalBytes - bytesState;
int additionalBytesRead = inputStream.read(byteBuffer, remainingBytes);
return updateBytesState(additionalBytesRead, totalBytes);
}
/**
* Reads a set amount of bytes into a {@code byte[]} such that reading can be stopped
* and started again later if not enough bytes are available.
*
* <p>Returns {@link #READ_RESULT_CONTINUE} if all bytes have been read. Reset
* {@link #bytesState} to {@code 0} before calling to indicate a new set of bytes should be read.
*
* @param inputStream The input stream from which bytes should be read
* @param byteArray The {@code byte[]} into which bytes should be read
* @param totalBytes The total size of bytes to be read
* @return One of the {@code RESULT_*} flags defined in this class
*/
private int readBytesInternal(
NonBlockingInputStream inputStream, byte[] byteArray, int totalBytes) {
if (bytesState == STATE_BEGIN_READING && totalBytes > byteArray.length) {
throw new IllegalArgumentException("Byte array not large enough");
}
if (bytesState >= totalBytes) {
return READ_RESULT_CONTINUE;
}
int remainingBytes = totalBytes - bytesState;
int additionalBytesRead = inputStream.read(byteArray, bytesState, remainingBytes);
return updateBytesState(additionalBytesRead, totalBytes);
}
/**
* Skips a set amount of bytes such that reading can be stopped and started again later if
* not enough bytes are available.
*
* <p>Returns {@link #READ_RESULT_CONTINUE} if all bytes have been skipped. Reset
* {@link #bytesState} to {@code 0} before calling to indicate a new set of bytes
* should be skipped.
*
* @param inputStream The input stream from which bytes should be skipped
* @param totalBytes The total size of bytes to be skipped
* @return One of the {@code RESULT_*} flags defined in this class
*/
private int skipBytesInternal(NonBlockingInputStream inputStream, int totalBytes) {
if (bytesState >= totalBytes) {
return READ_RESULT_CONTINUE;
}
int remainingBytes = totalBytes - bytesState;
int additionalBytesRead = inputStream.skip(remainingBytes);
return updateBytesState(additionalBytesRead, totalBytes);
}
/**
* Updates {@link #bytesState} and {@link #bytesRead} after reading bytes in one of the
* {@code verbBytesInternal} methods.
*
* @param additionalBytesRead The number of additional bytes read to be accounted for
* @param totalBytes The total size of bytes to be read or skipped
* @return One of the {@code RESULT_*} flags defined in this class
*/
private int updateBytesState(int additionalBytesRead, int totalBytes) {
if (additionalBytesRead == -1) {
return READ_RESULT_END_OF_FILE;
}
bytesState += additionalBytesRead;
bytesRead += additionalBytesRead;
if (bytesState < totalBytes) {
return READ_RESULT_NEED_MORE_DATA;
} else {
return READ_RESULT_CONTINUE;
}
}
/**
* Parses and returns the integer value currently read into the first {@code byteLength} bytes
* of {@link #tempByteArray}. EBML varint length masks can optionally be removed.
*
* @param byteLength The number of bytes to parse from {@link #tempByteArray}
* @param removeLengthMask Removes the variable-length integer length mask from the value
* @return The resulting integer value. This value could be up to 8-bytes so a Java long is used
*/
private long getTempByteArrayValue(int byteLength, boolean removeLengthMask) {
if (removeLengthMask) {
tempByteArray[0] &= ~VARINT_LENGTH_MASKS[varintBytesLength - 1];
}
long varint = 0;
for (int i = 0; i < byteLength; i++) {
// Shift all existing bits up one byte and add the next byte at the bottom.
varint = (varint << 8) | (tempByteArray[i] & 0xff);
}
return varint;
}
/**
* Used in {@link #masterElementsStack} to track when the current master element ends so that
* {@link EbmlEventHandler#onMasterElementEnd(int)} is called.
*/
private static final class MasterElement {
private final int elementId;
private final long elementEndOffsetBytes;
private MasterElement(int elementId, long elementEndOffsetBytes) {
this.elementId = elementId;
this.elementEndOffsetBytes = elementEndOffsetBytes;
}
}
}

View File

@ -0,0 +1,427 @@
/*
* 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.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 final EbmlReader reader;
private final byte[] simpleBlockTimecodeAndFlags = new byte[3];
private SampleHolder tempSampleHolder;
private boolean sampleRead;
private boolean prepared = false;
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());
this.cueTimesUs = new LongArray();
this.cueClusterPositions = new LongArray();
}
@Override
public boolean isPrepared() {
return prepared;
}
@Override
public boolean read(NonBlockingInputStream inputStream, SampleHolder sampleHolder) {
tempSampleHolder = sampleHolder;
sampleRead = false;
reader.read(inputStream);
tempSampleHolder = null;
return sampleRead;
}
@Override
public boolean seekTo(long seekTimeUs, boolean allowNoop) {
checkPrepared();
if (allowNoop
&& simpleBlockTimecodeUs != UNKNOWN
&& seekTimeUs >= simpleBlockTimecodeUs) {
int clusterIndex = Arrays.binarySearch(cues.timesUs, clusterTimecodeUs);
if (clusterIndex >= 0 && seekTimeUs < clusterTimecodeUs + cues.durationsUs[clusterIndex]) {
return false;
}
}
reader.reset();
return true;
}
@Override
public SegmentIndex getCues() {
checkPrepared();
return cues;
}
@Override
public MediaFormat getFormat() {
checkPrepared();
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;
break;
default:
// pass
}
return true;
}
/* package */ boolean onMasterElementEnd(int id) {
if (id == ID_CUES) {
finishPreparing();
return false;
}
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.
// 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;
tempSampleHolder.flags = keyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0;
tempSampleHolder.decodeOnly = invisible;
tempSampleHolder.timeUs = clusterTimecodeUs + timecodeUs;
tempSampleHolder.size = (int) (elementEndOffsetBytes - reader.getBytesRead());
break;
case LACING_EBML:
case LACING_FIXED:
case LACING_XIPH:
default:
throw new IllegalStateException("Lacing mode " + lacing + " not supported");
}
// Read video data into sample holder.
reader.readBytes(inputStream, tempSampleHolder.data, tempSampleHolder.size);
sampleRead = true;
return false;
} else {
reader.skipBytes(inputStream, contentsSizeBytes);
return true;
}
}
private long scaleTimecodeToUs(long unscaledTimecode) {
return TimeUnit.NANOSECONDS.toMicros(unscaledTimecode * timecodeScale);
}
private void checkPrepared() {
if (!prepared) {
throw new IllegalStateException("Parser not yet prepared");
}
}
private void finishPreparing() {
if (prepared) {
throw new IllegalStateException("Already prepared");
} else if (segmentStartOffsetBytes == UNKNOWN) {
throw new IllegalStateException("Segment start/end offsets unknown");
} else if (durationUs == UNKNOWN) {
throw new IllegalStateException("Duration unknown");
} else if (pixelWidth == UNKNOWN || pixelHeight == UNKNOWN) {
throw new IllegalStateException("Pixel width/height unknown");
} else if (cuesSizeBytes == UNKNOWN) {
throw new IllegalStateException("Cues size unknown");
} else if (cueTimesUs.size() == 0 || cueTimesUs.size() != cueClusterPositions.size()) {
throw new IllegalStateException("Invalid/missing cue points");
}
format = MediaFormat.createVideoFormat(
MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, pixelWidth, pixelHeight, null);
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;
prepared = true;
}
/**
* 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 boolean onMasterElementStart(
int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) {
return DefaultWebmExtractor.this.onMasterElementStart(
id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes);
}
@Override
public boolean onMasterElementEnd(int id) {
return DefaultWebmExtractor.this.onMasterElementEnd(id);
}
@Override
public boolean onIntegerElement(int id, long value) {
return DefaultWebmExtractor.this.onIntegerElement(id, value);
}
@Override
public boolean onFloatElement(int id, double value) {
return DefaultWebmExtractor.this.onFloatElement(id, value);
}
@Override
public boolean onStringElement(int id, String value) {
return 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);
}
}
}

View File

@ -0,0 +1,120 @@
/*
* 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.upstream.NonBlockingInputStream;
import java.nio.ByteBuffer;
/**
* Defines EBML element IDs/types and reacts to events.
*/
/* package */ interface EbmlEventHandler {
/**
* Retrieves the type of an element ID.
*
* <p>If {@link EbmlReader#TYPE_UNKNOWN} is returned then the element is skipped.
* Note that all children of a skipped master element are also skipped.
*
* @param id The integer ID of this element
* @return One of the {@code TYPE_} constants defined in this class
*/
public int getElementType(int id);
/**
* Called when a master element is encountered in the {@link NonBlockingInputStream}.
*
* <p>Following events should be considered as taking place "within" this element until a
* matching call to {@link #onMasterElementEnd(int)} is made. Note that it is possible for
* another master element of the same ID to be nested within itself.
*
* @param id The integer ID of this element
* @param elementOffsetBytes The byte offset where this element starts
* @param headerSizeBytes The byte length of this element's ID and size header
* @param contentsSizeBytes The byte length of this element's children
* @return {@code false} if parsing should stop right away
*/
public boolean onMasterElementStart(
int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes);
/**
* Called when a master element has finished reading in all of its children from the
* {@link NonBlockingInputStream}.
*
* @param id The integer ID of this element
* @return {@code false} if parsing should stop right away
*/
public boolean onMasterElementEnd(int id);
/**
* Called when an integer element is encountered in the {@link NonBlockingInputStream}.
*
* @param id The integer ID of this element
* @param value The integer value this element contains
* @return {@code false} if parsing should stop right away
*/
public boolean onIntegerElement(int id, long value);
/**
* Called when a float element is encountered in the {@link NonBlockingInputStream}.
*
* @param id The integer ID of this element
* @param value The float value this element contains
* @return {@code false} if parsing should stop right away
*/
public boolean onFloatElement(int id, double value);
/**
* Called when a string element is encountered in the {@link NonBlockingInputStream}.
*
* @param id The integer ID of this element
* @param value The string value this element contains
* @return {@code false} if parsing should stop right away
*/
public boolean onStringElement(int id, String value);
/**
* Called when a binary element is encountered in the {@link NonBlockingInputStream}.
*
* <p>The element header (containing element ID and content size) will already have been read.
* Subclasses must exactly read the entire contents of the element, which is
* {@code contentsSizeBytes} in length. It's guaranteed that the full element contents will be
* immediately available from {@code inputStream}.
*
* <p>Several methods in {@link EbmlReader} are available for reading the contents of a
* binary element:
* <ul>
* <li>{@link EbmlReader#readVarint(NonBlockingInputStream)}.
* <li>{@link EbmlReader#readBytes(NonBlockingInputStream, byte[], int)}.
* <li>{@link EbmlReader#readBytes(NonBlockingInputStream, ByteBuffer, int)}.
* <li>{@link EbmlReader#skipBytes(NonBlockingInputStream, int)}.
* <li>{@link EbmlReader#getBytesRead()}.
* </ul>
*
* @param id The integer ID of this element
* @param elementOffsetBytes The byte offset where this element starts
* @param headerSizeBytes The byte length of this element's ID and size header
* @param contentsSizeBytes The byte length of this element's contents
* @param inputStream The {@link NonBlockingInputStream} from which this
* element's contents should be read
* @return {@code false} if parsing should stop right away
*/
public boolean onBinaryElement(
int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,
NonBlockingInputStream inputStream);
}

View File

@ -16,528 +16,92 @@
package com.google.android.exoplayer.parser.webm; package com.google.android.exoplayer.parser.webm;
import com.google.android.exoplayer.upstream.NonBlockingInputStream; import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.Assertions;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Stack;
/** /**
* An event-driven incremental EBML reader base class. * Basic event-driven incremental EBML parser which needs an {@link EbmlEventHandler} to
* define IDs/types and react to events.
* *
* <p>EBML can be summarized as a binary XML format somewhat similar to Protocol Buffers. * <p>EBML can be summarized as a binary XML format somewhat similar to Protocol Buffers.
* It was originally designed for the Matroska container format. More information about EBML and * It was originally designed for the Matroska container format. More information about EBML and
* Matroska is available <a href="http://www.matroska.org/technical/specs/index.html">here</a>. * Matroska is available <a href="http://www.matroska.org/technical/specs/index.html">here</a>.
*/ */
public abstract class EbmlReader { /* package */ interface EbmlReader {
// Element Types // Element Types
protected static final int TYPE_UNKNOWN = 0; // Undefined element. /** Undefined element. */
protected static final int TYPE_MASTER = 1; // Contains child elements. public static final int TYPE_UNKNOWN = 0;
protected static final int TYPE_UNSIGNED_INT = 2; /** Contains child elements. */
protected static final int TYPE_STRING = 3; public static final int TYPE_MASTER = 1;
protected static final int TYPE_BINARY = 4; /** Unsigned integer value of up to 8 bytes. */
protected static final int TYPE_FLOAT = 5; public static final int TYPE_UNSIGNED_INT = 2;
public static final int TYPE_STRING = 3;
public static final int TYPE_BINARY = 4;
/** IEEE floating point value of either 4 or 8 bytes. */
public static final int TYPE_FLOAT = 5;
// Return values for methods read, readElementId, readElementSize, readVarintBytes, and readBytes. // Return values for reading methods.
protected static final int RESULT_CONTINUE = 0; public static final int READ_RESULT_CONTINUE = 0;
protected static final int RESULT_NEED_MORE_DATA = 1; public static final int READ_RESULT_NEED_MORE_DATA = 1;
protected static final int RESULT_END_OF_FILE = 2; public static final int READ_RESULT_END_OF_FILE = 2;
// State values used in variables state, elementIdState, elementContentSizeState, and public void setEventHandler(EbmlEventHandler eventHandler);
// varintBytesState.
private static final int STATE_BEGIN_READING = 0;
private static final int STATE_READ_CONTENTS = 1;
private static final int STATE_FINISHED_READING = 2;
/**
* The first byte of a variable-length integer (varint) will have one of these bit masks
* indicating the total length in bytes. {@code 0x80} is a one-byte integer,
* {@code 0x40} is two bytes, and so on up to eight bytes.
*/
private static final int[] VARINT_LENGTH_MASKS = new int[] {
0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01
};
private final Stack<MasterElement> masterElementsStack = new Stack<MasterElement>();
private final byte[] tempByteArray = new byte[8];
private int state;
private long bytesRead;
private long elementOffset;
private int elementId;
private int elementIdState;
private long elementContentSize;
private int elementContentSizeState;
private int varintBytesState;
private int varintBytesLength;
private int bytesState;
private byte[] stringBytes;
/**
* Called to retrieve the type of an element ID. If {@link #TYPE_UNKNOWN} is returned then
* the element is skipped. Note that all children of a skipped master element are also skipped.
*
* @param id The integer ID of this element.
* @return One of the {@code TYPE_} constants defined in this class.
*/
protected abstract int getElementType(int id);
/**
* Called when a master element is encountered in the {@link NonBlockingInputStream}.
* Following events should be considered as taking place "within" this element until a
* matching call to {@link #onMasterElementEnd(int)} is made. Note that it
* is possible for the same master element to be nested within itself.
*
* @param id The integer ID of this element.
* @param elementOffset The byte offset where this element starts.
* @param headerSize The byte length of this element's ID and size header.
* @param contentsSize The byte length of this element's children.
* @return {@code true} if parsing should continue or {@code false} if it should stop right away.
*/
protected abstract boolean onMasterElementStart(
int id, long elementOffset, int headerSize, int contentsSize);
/**
* Called when a master element has finished reading in all of its children from the
* {@link NonBlockingInputStream}.
*
* @param id The integer ID of this element.
* @return {@code true} if parsing should continue or {@code false} if it should stop right away.
*/
protected abstract boolean onMasterElementEnd(int id);
/**
* Called when an integer element is encountered in the {@link NonBlockingInputStream}.
*
* @param id The integer ID of this element.
* @param value The integer value this element contains.
* @return {@code true} if parsing should continue or {@code false} if it should stop right away.
*/
protected abstract boolean onIntegerElement(int id, long value);
/**
* Called when a float element is encountered in the {@link NonBlockingInputStream}.
*
* @param id The integer ID of this element.
* @param value The float value this element contains.
* @return {@code true} if parsing should continue or {@code false} if it should stop right away.
*/
protected abstract boolean onFloatElement(int id, double value);
/**
* Called when a string element is encountered in the {@link NonBlockingInputStream}.
*
* @param id The integer ID of this element.
* @param value The string value this element contains.
* @return {@code true} if parsing should continue or {@code false} if it should stop right away.
*/
protected abstract boolean onStringElement(int id, String value);
/**
* Called when a binary element is encountered in the {@link NonBlockingInputStream}.
* The element header (containing element ID and content size) will already have been read.
* Subclasses must exactly read the entire contents of the element, which is {@code contentsSize}
* bytes in length. It's guaranteed that the full element contents will be immediately available
* from {@code inputStream}.
*
* <p>Several methods are available for reading the contents of a binary element:
* <ul>
* <li>{@link #readVarint(NonBlockingInputStream)}.
* <li>{@link #readBytes(NonBlockingInputStream, byte[], int)}.
* <li>{@link #readBytes(NonBlockingInputStream, ByteBuffer, int)}.
* <li>{@link #skipBytes(NonBlockingInputStream, int)}.
* <li>{@link #getBytesRead()}.
*
* @param inputStream The {@link NonBlockingInputStream} from which this
* element's contents should be read.
* @param id The integer ID of this element.
* @param elementOffset The byte offset where this element starts.
* @param headerSize The byte length of this element's ID and size header.
* @param contentsSize The byte length of this element's contents.
* @return {@code true} if parsing should continue or {@code false} if it should stop right away.
*/
protected abstract boolean onBinaryElement(NonBlockingInputStream inputStream,
int id, long elementOffset, int headerSize, int contentsSize);
/** /**
* Reads from a {@link NonBlockingInputStream} and calls event callbacks as needed. * Reads from a {@link NonBlockingInputStream} and calls event callbacks as needed.
* *
* @param inputStream The input stream from which data should be read. * @param inputStream The input stream from which data should be read
* @return One of the {@code RESULT_*} flags defined in this class. * @return One of the {@code RESULT_*} flags defined in this interface
*/ */
protected final int read(NonBlockingInputStream inputStream) { public int read(NonBlockingInputStream inputStream);
while (true) {
while (masterElementsStack.size() > 0
&& bytesRead >= masterElementsStack.peek().elementEndOffset) {
if (!onMasterElementEnd(masterElementsStack.pop().elementId)) {
return RESULT_CONTINUE;
}
}
if (state == STATE_BEGIN_READING) {
final int resultId = readElementId(inputStream);
if (resultId != RESULT_CONTINUE) {
return resultId;
}
final int resultSize = readElementContentSize(inputStream);
if (resultSize != RESULT_CONTINUE) {
return resultSize;
}
state = STATE_READ_CONTENTS;
bytesState = 0;
}
final int type = getElementType(elementId);
switch (type) {
case TYPE_MASTER:
final int masterHeaderSize = (int) (bytesRead - elementOffset);
masterElementsStack.add(new MasterElement(elementId, bytesRead + elementContentSize));
if (!onMasterElementStart(
elementId, elementOffset, masterHeaderSize, (int) elementContentSize)) {
prepareForNextElement();
return RESULT_CONTINUE;
}
break;
case TYPE_UNSIGNED_INT:
Assertions.checkState(elementContentSize <= 8);
final int resultInt =
readBytes(inputStream, null, tempByteArray, (int) elementContentSize);
if (resultInt != RESULT_CONTINUE) {
return resultInt;
}
final long intValue = parseTempByteArray((int) elementContentSize, false);
if (!onIntegerElement(elementId, intValue)) {
prepareForNextElement();
return RESULT_CONTINUE;
}
break;
case TYPE_FLOAT:
Assertions.checkState(elementContentSize == 4 || elementContentSize == 8);
final int resultFloat =
readBytes(inputStream, null, tempByteArray, (int) elementContentSize);
if (resultFloat != RESULT_CONTINUE) {
return resultFloat;
}
final long valueBits = parseTempByteArray((int) elementContentSize, false);
final double floatValue;
if (elementContentSize == 4) {
floatValue = Float.intBitsToFloat((int) valueBits);
} else {
floatValue = Double.longBitsToDouble(valueBits);
}
if (!onFloatElement(elementId, floatValue)) {
prepareForNextElement();
return RESULT_CONTINUE;
}
break;
case TYPE_STRING:
if (stringBytes == null) {
stringBytes = new byte[(int) elementContentSize];
}
final int resultString =
readBytes(inputStream, null, stringBytes, (int) elementContentSize);
if (resultString != RESULT_CONTINUE) {
return resultString;
}
final String stringValue = new String(stringBytes, Charset.forName("UTF-8"));
stringBytes = null;
if (!onStringElement(elementId, stringValue)) {
prepareForNextElement();
return RESULT_CONTINUE;
}
break;
case TYPE_BINARY:
if (inputStream.getAvailableByteCount() < elementContentSize) {
return RESULT_NEED_MORE_DATA;
}
final int binaryHeaderSize = (int) (bytesRead - elementOffset);
final boolean keepGoing = onBinaryElement(
inputStream, elementId, elementOffset, binaryHeaderSize, (int) elementContentSize);
Assertions.checkState(elementOffset + binaryHeaderSize + elementContentSize == bytesRead);
if (!keepGoing) {
prepareForNextElement();
return RESULT_CONTINUE;
}
break;
case TYPE_UNKNOWN:
// Unknown elements should be skipped.
Assertions.checkState(
readBytes(inputStream, null, null, (int) elementContentSize) == RESULT_CONTINUE);
break;
default:
throw new IllegalStateException("Invalid element type " + type);
}
prepareForNextElement();
}
}
/** /**
* @return The total number of bytes consumed by the reader since first created * The total number of bytes consumed by the reader since first created or last {@link #reset()}.
* or last {@link #reset()}.
*/ */
protected final long getBytesRead() { public long getBytesRead();
return bytesRead;
}
/** /**
* Resets the entire state of the reader so that it will read a new EBML structure from scratch. * Resets the entire state of the reader so that it will read a new EBML structure from scratch.
* This includes resetting {@link #bytesRead} back to 0 and discarding all pending *
* {@link #onMasterElementEnd(int)} events. * <p>This includes resetting the value returned from {@link #getBytesRead()} to 0 and discarding
* all pending {@link EbmlEventHandler#onMasterElementEnd(int)} events.
*/ */
protected final void reset() { public void reset();
prepareForNextElement();
masterElementsStack.clear();
bytesRead = 0;
}
/** /**
* Reads, parses, and returns an EBML variable-length integer (varint) from the contents * Reads, parses, and returns an EBML variable-length integer (varint) from the contents
* of a binary element. * of a binary element.
* *
* @param inputStream The input stream from which data should be read. * @param inputStream The input stream from which data should be read
* @return The varint value at the current position of the contents of a binary element. * @return The varint value at the current position of the contents of a binary element
*/ */
protected final long readVarint(NonBlockingInputStream inputStream) { public long readVarint(NonBlockingInputStream inputStream);
varintBytesState = STATE_BEGIN_READING;
Assertions.checkState(readVarintBytes(inputStream) == RESULT_CONTINUE);
return parseTempByteArray(varintBytesLength, true);
}
/** /**
* Reads a fixed number of bytes from the contents of a binary element into a {@link ByteBuffer}. * Reads a fixed number of bytes from the contents of a binary element into a {@link ByteBuffer}.
* *
* @param inputStream The input stream from which data should be read. * @param inputStream The input stream from which data should be read
* @param byteBuffer The {@link ByteBuffer} to which data should be written. * @param byteBuffer The {@link ByteBuffer} to which data should be written
* @param totalBytes The fixed number of bytes to be read and written. * @param totalBytes The fixed number of bytes to be read and written
*/ */
protected final void readBytes( public void readBytes(NonBlockingInputStream inputStream, ByteBuffer byteBuffer, int totalBytes);
NonBlockingInputStream inputStream, ByteBuffer byteBuffer, int totalBytes) {
bytesState = 0;
Assertions.checkState(readBytes(inputStream, byteBuffer, null, totalBytes) == RESULT_CONTINUE);
}
/** /**
* Reads a fixed number of bytes from the contents of a binary element into a {@code byte[]}. * Reads a fixed number of bytes from the contents of a binary element into a {@code byte[]}.
* *
* @param inputStream The input stream from which data should be read. * @param inputStream The input stream from which data should be read
* @param byteArray The byte array to which data should be written. * @param byteArray The byte array to which data should be written
* @param totalBytes The fixed number of bytes to be read and written. * @param totalBytes The fixed number of bytes to be read and written
*/ */
protected final void readBytes( public void readBytes(NonBlockingInputStream inputStream, byte[] byteArray, int totalBytes);
NonBlockingInputStream inputStream, byte[] byteArray, int totalBytes) {
bytesState = 0;
Assertions.checkState(readBytes(inputStream, null, byteArray, totalBytes) == RESULT_CONTINUE);
}
/** /**
* Skips a fixed number of bytes from the contents of a binary element. * Skips a fixed number of bytes from the contents of a binary element.
* *
* @param inputStream The input stream from which data should be skipped. * @param inputStream The input stream from which data should be skipped
* @param totalBytes The fixed number of bytes to be skipped. * @param totalBytes The fixed number of bytes to be skipped
*/ */
protected final void skipBytes(NonBlockingInputStream inputStream, int totalBytes) { public void skipBytes(NonBlockingInputStream inputStream, int totalBytes);
bytesState = 0;
Assertions.checkState(readBytes(inputStream, null, null, totalBytes) == RESULT_CONTINUE);
}
/**
* Resets the internal state of {@link #read(NonBlockingInputStream)} so that it can start
* reading a new element from scratch.
*/
private final void prepareForNextElement() {
state = STATE_BEGIN_READING;
elementIdState = STATE_BEGIN_READING;
elementContentSizeState = STATE_BEGIN_READING;
elementOffset = bytesRead;
}
/**
* Reads an element ID such that reading can be stopped and started again in a later call
* if not enough bytes are available. Returns {@link #RESULT_CONTINUE} if a full element ID
* has been read into {@link #elementId}. Reset {@link #elementIdState} to
* {@link #STATE_BEGIN_READING} before calling to indicate a new element ID should be read.
*
* @param inputStream The input stream from which an element ID should be read.
* @return One of the {@code RESULT_*} flags defined in this class.
*/
private int readElementId(NonBlockingInputStream inputStream) {
if (elementIdState == STATE_FINISHED_READING) {
return RESULT_CONTINUE;
}
if (elementIdState == STATE_BEGIN_READING) {
varintBytesState = STATE_BEGIN_READING;
elementIdState = STATE_READ_CONTENTS;
}
final int result = readVarintBytes(inputStream);
if (result != RESULT_CONTINUE) {
return result;
}
elementId = (int) parseTempByteArray(varintBytesLength, false);
elementIdState = STATE_FINISHED_READING;
return RESULT_CONTINUE;
}
/**
* Reads an element's content size such that reading can be stopped and started again in a later
* call if not enough bytes are available. Returns {@link #RESULT_CONTINUE} if an entire element
* size has been read into {@link #elementContentSize}. Reset {@link #elementContentSizeState} to
* {@link #STATE_BEGIN_READING} before calling to indicate a new element size should be read.
*
* @param inputStream The input stream from which an element size should be read.
* @return One of the {@code RESULT_*} flags defined in this class.
*/
private int readElementContentSize(NonBlockingInputStream inputStream) {
if (elementContentSizeState == STATE_FINISHED_READING) {
return RESULT_CONTINUE;
}
if (elementContentSizeState == STATE_BEGIN_READING) {
varintBytesState = STATE_BEGIN_READING;
elementContentSizeState = STATE_READ_CONTENTS;
}
final int result = readVarintBytes(inputStream);
if (result != RESULT_CONTINUE) {
return result;
}
elementContentSize = parseTempByteArray(varintBytesLength, true);
elementContentSizeState = STATE_FINISHED_READING;
return RESULT_CONTINUE;
}
/**
* Reads an EBML variable-length integer (varint) such that reading can be stopped and started
* again in a later call if not enough bytes are available. Returns {@link #RESULT_CONTINUE} if
* an entire varint has been read into {@link #tempByteArray} and the length of the varint is in
* {@link #varintBytesLength}. Reset {@link #varintBytesState} to {@link #STATE_BEGIN_READING}
* before calling to indicate a new varint should be read.
*
* @param inputStream The input stream from which a varint should be read.
* @return One of the {@code RESULT_*} flags defined in this class.
*/
private int readVarintBytes(NonBlockingInputStream inputStream) {
if (varintBytesState == STATE_FINISHED_READING) {
return RESULT_CONTINUE;
}
// Read first byte to get length.
if (varintBytesState == STATE_BEGIN_READING) {
bytesState = 0;
final int result = readBytes(inputStream, null, tempByteArray, 1);
if (result != RESULT_CONTINUE) {
return result;
}
varintBytesState = STATE_READ_CONTENTS;
final int firstByte = tempByteArray[0] & 0xff;
varintBytesLength = -1;
for (int i = 0; i < VARINT_LENGTH_MASKS.length; i++) {
if ((VARINT_LENGTH_MASKS[i] & firstByte) != 0) {
varintBytesLength = i + 1;
break;
}
}
if (varintBytesLength == -1) {
throw new IllegalStateException(
"No valid varint length mask found at bytesRead = " + bytesRead);
}
}
// Read remaining bytes.
final int result = readBytes(inputStream, null, tempByteArray, varintBytesLength);
if (result != RESULT_CONTINUE) {
return result;
}
// All bytes have been read.
return RESULT_CONTINUE;
}
/**
* Reads a set amount of bytes into a {@link ByteBuffer}, {@code byte[]}, or nowhere (skipping
* the bytes) such that reading can be stopped and started again later if not enough bytes are
* available. Returns {@link #RESULT_CONTINUE} if all bytes have been read. Reset
* {@link #bytesState} to {@code 0} before calling to indicate a new set of bytes should be read.
*
* <p>If both {@code byteBuffer} and {@code byteArray} are not null then bytes are only read
* into {@code byteBuffer}.
*
* @param inputStream The input stream from which bytes should be read.
* @param byteBuffer The optional {@link ByteBuffer} into which bytes should be read.
* @param byteArray The optional {@code byte[]} into which bytes should be read.
* @param totalBytes The total size of bytes to be read or skipped.
* @return One of the {@code RESULT_*} flags defined in this class.
*/
private int readBytes(
NonBlockingInputStream inputStream, ByteBuffer byteBuffer, byte[] byteArray, int totalBytes) {
if (bytesState == STATE_BEGIN_READING
&& ((byteBuffer != null && totalBytes > byteBuffer.capacity())
|| (byteArray != null && totalBytes > byteArray.length))) {
throw new IllegalStateException("Byte destination not large enough");
}
if (bytesState < totalBytes) {
final int remainingBytes = totalBytes - bytesState;
final int result;
if (byteBuffer != null) {
result = inputStream.read(byteBuffer, remainingBytes);
} else if (byteArray != null) {
result = inputStream.read(byteArray, bytesState, remainingBytes);
} else {
result = inputStream.skip(remainingBytes);
}
if (result == -1) {
return RESULT_END_OF_FILE;
}
bytesState += result;
bytesRead += result;
if (bytesState < totalBytes) {
return RESULT_NEED_MORE_DATA;
}
}
return RESULT_CONTINUE;
}
/**
* Parses and returns the integer value currently read into the first {@code byteLength} bytes
* of {@link #tempByteArray}. EBML varint length masks can optionally be removed.
*
* @param byteLength The number of bytes to parse from {@link #tempByteArray}.
* @param removeLengthMask Removes the variable-length integer length mask from the value.
* @return The resulting integer value. This value could be up to 8-bytes so a Java long is used.
*/
private long parseTempByteArray(int byteLength, boolean removeLengthMask) {
if (removeLengthMask) {
tempByteArray[0] &= ~VARINT_LENGTH_MASKS[varintBytesLength - 1];
}
long varint = 0;
for (int i = 0; i < byteLength; i++) {
// Shift all existing bits up one byte and add the next byte at the bottom.
varint = (varint << 8) | (tempByteArray[i] & 0xff);
}
return varint;
}
/**
* Used in {@link #masterElementsStack} to track when the current master element ends so that
* {@link #onMasterElementEnd(int)} is called.
*/
private static final class MasterElement {
private final int elementId;
private final long elementEndOffset;
private MasterElement(int elementId, long elementEndOffset) {
this.elementId = elementId;
this.elementEndOffset = elementEndOffset;
}
}
} }

View File

@ -19,97 +19,22 @@ import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.parser.SegmentIndex; import com.google.android.exoplayer.parser.SegmentIndex;
import com.google.android.exoplayer.upstream.NonBlockingInputStream; 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.util.Arrays;
/** /**
* Facilitates the extraction of data from the WebM container format with a * Extractor to facilitate data retrieval from the WebM container format.
* non-blocking, incremental parser based on {@link EbmlReader}.
* *
* <p>WebM is a subset of the EBML elements defined for Matroska. More information about EBML and * <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>. * 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>. * More info about WebM is <a href="http://www.webmproject.org/code/specs/container/">here</a>.
*/ */
@TargetApi(16) public interface WebmExtractor {
public final class WebmExtractor extends EbmlReader {
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 final byte[] simpleBlockTimecodeAndFlags = new byte[3];
private SampleHolder tempSampleHolder;
private boolean sampleRead;
private boolean prepared = false;
private long segmentStartPosition = UNKNOWN;
private long segmentEndPosition = UNKNOWN;
private long timecodeScale = 1000000L;
private long durationUs = UNKNOWN;
private int pixelWidth = UNKNOWN;
private int pixelHeight = UNKNOWN;
private int cuesByteSize = UNKNOWN;
private long clusterTimecodeUs = UNKNOWN;
private long simpleBlockTimecodeUs = UNKNOWN;
private MediaFormat format;
private SegmentIndex cues;
private LongArray cueTimesUs;
private LongArray cueClusterPositions;
public WebmExtractor() {
cueTimesUs = new LongArray();
cueClusterPositions = new LongArray();
}
/** /**
* Whether the has parsed the cues and sample format from the stream. * Whether the has parsed the cues and sample format from the stream.
* *
* @return True if the extractor is prepared. False otherwise. * @return True if the extractor is prepared. False otherwise
*/ */
public boolean isPrepared() { public boolean isPrepared();
return prepared;
}
/** /**
* Consumes data from a {@link NonBlockingInputStream}. * Consumes data from a {@link NonBlockingInputStream}.
@ -118,289 +43,36 @@ public final class WebmExtractor extends EbmlReader {
* {@code sampleHolder}. Hence the same {@link SampleHolder} instance must be passed * {@code sampleHolder}. Hence the same {@link SampleHolder} instance must be passed
* in subsequent calls until the whole sample has been read. * in subsequent calls until the whole sample has been read.
* *
* @param inputStream The input stream from which data should be read. * @param inputStream The input stream from which data should be read
* @param sampleHolder A {@link SampleHolder} into which the sample should be read. * @param sampleHolder A {@link SampleHolder} into which the sample should be read
* @return {@code true} if a sample has been read into the sample holder, otherwise {@code false}. * @return {@code true} if a sample has been read into the sample holder
*/ */
public boolean read(NonBlockingInputStream inputStream, SampleHolder sampleHolder) { public boolean read(NonBlockingInputStream inputStream, SampleHolder sampleHolder);
tempSampleHolder = sampleHolder;
sampleRead = false;
super.read(inputStream);
tempSampleHolder = null;
return sampleRead;
}
/** /**
* Seeks to a position before or equal to the requested time. * Seeks to a position before or equal to the requested time.
* *
* @param seekTimeUs The desired seek time in microseconds. * @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 * @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 * 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. * 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. * @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) { public boolean seekTo(long seekTimeUs, boolean allowNoop);
checkPrepared();
if (allowNoop && simpleBlockTimecodeUs != UNKNOWN && seekTimeUs >= simpleBlockTimecodeUs) {
final int clusterIndex = Arrays.binarySearch(cues.timesUs, clusterTimecodeUs);
if (clusterIndex >= 0 && seekTimeUs < clusterTimecodeUs + cues.durationsUs[clusterIndex]) {
return false;
}
}
reset();
return true;
}
/** /**
* Returns the cues for the media stream. * 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 * @return The cues in the form of a {@link SegmentIndex}, or null if the extractor is not yet
* prepared. * prepared
*/ */
public SegmentIndex getCues() { public SegmentIndex getCues();
checkPrepared();
return cues;
}
/** /**
* Returns the format of the samples contained within the media stream. * 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. * @return The sample media format, or null if the extracted is not yet prepared
*/ */
public MediaFormat getFormat() { public MediaFormat getFormat();
checkPrepared();
return format;
}
@Override
protected 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;
}
}
@Override
protected boolean onMasterElementStart(
int id, long elementOffset, int headerSize, int contentsSize) {
switch (id) {
case ID_SEGMENT:
if (segmentStartPosition != UNKNOWN || segmentEndPosition != UNKNOWN) {
throw new IllegalStateException("Multiple Segment elements not supported");
}
segmentStartPosition = elementOffset + headerSize;
segmentEndPosition = elementOffset + headerSize + contentsSize;
break;
case ID_CUES:
cuesByteSize = headerSize + contentsSize;
break;
}
return true;
}
@Override
protected boolean onMasterElementEnd(int id) {
switch (id) {
case ID_CUES:
finishPreparing();
return false;
}
return true;
}
@Override
protected 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 IllegalStateException("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 IllegalStateException("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;
}
return true;
}
@Override
protected boolean onFloatElement(int id, double value) {
switch (id) {
case ID_DURATION:
durationUs = scaleTimecodeToUs(value);
break;
}
return true;
}
@Override
protected 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 IllegalStateException("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 IllegalStateException("CodecID " + value + " not supported");
}
break;
}
return true;
}
@Override
protected boolean onBinaryElement(NonBlockingInputStream inputStream,
int id, long elementOffset, int headerSize, int contentsSize) {
switch (id) {
case 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.
// Value of trackNumber is not used but needs to be read.
readVarint(inputStream);
// Next three bytes have timecode and flags.
readBytes(inputStream, simpleBlockTimecodeAndFlags, 3);
// First two bytes of the three are the relative timecode.
final int timecode =
(simpleBlockTimecodeAndFlags[0] << 8) | (simpleBlockTimecodeAndFlags[1] & 0xff);
final long timecodeUs = scaleTimecodeToUs(timecode);
// Last byte of the three has some flags and the lacing value.
final boolean keyframe = (simpleBlockTimecodeAndFlags[2] & 0x80) == 0x80;
final boolean invisible = (simpleBlockTimecodeAndFlags[2] & 0x08) == 0x08;
final int lacing = (simpleBlockTimecodeAndFlags[2] & 0x06) >> 1;
//final boolean discardable = (simpleBlockTimecodeAndFlags[2] & 0x01) == 0x01; // Not used.
// Validate lacing and set info into sample holder.
switch (lacing) {
case LACING_NONE:
final long elementEndOffset = elementOffset + headerSize + contentsSize;
simpleBlockTimecodeUs = clusterTimecodeUs + timecodeUs;
tempSampleHolder.flags = keyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0;
tempSampleHolder.decodeOnly = invisible;
tempSampleHolder.timeUs = clusterTimecodeUs + timecodeUs;
tempSampleHolder.size = (int) (elementEndOffset - getBytesRead());
break;
case LACING_EBML:
case LACING_FIXED:
case LACING_XIPH:
default:
throw new IllegalStateException("Lacing mode " + lacing + " not supported");
}
// Read video data into sample holder.
readBytes(inputStream, tempSampleHolder.data, tempSampleHolder.size);
sampleRead = true;
return false;
default:
skipBytes(inputStream, contentsSize);
}
return true;
}
private long scaleTimecodeToUs(long unscaledTimecode) {
return (unscaledTimecode * timecodeScale) / 1000L;
}
private long scaleTimecodeToUs(double unscaledTimecode) {
return (long) ((unscaledTimecode * timecodeScale) / 1000.0);
}
private void checkPrepared() {
if (!prepared) {
throw new IllegalStateException("Parser not yet prepared");
}
}
private void finishPreparing() {
if (prepared
|| segmentStartPosition == UNKNOWN || segmentEndPosition == UNKNOWN
|| durationUs == UNKNOWN
|| pixelWidth == UNKNOWN || pixelHeight == UNKNOWN
|| cuesByteSize == UNKNOWN
|| cueTimesUs.size() == 0 || cueTimesUs.size() != cueClusterPositions.size()) {
throw new IllegalStateException("Incorrect state in finishPreparing()");
}
format = MediaFormat.createVideoFormat(MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, pixelWidth,
pixelHeight, null);
final int cuePointsSize = cueTimesUs.size();
final int sizeBytes = cuesByteSize;
final int[] sizes = new int[cuePointsSize];
final long[] offsets = new long[cuePointsSize];
final long[] durationsUs = new long[cuePointsSize];
final long[] timesUs = new long[cuePointsSize];
for (int i = 0; i < cuePointsSize; i++) {
timesUs[i] = cueTimesUs.get(i);
offsets[i] = segmentStartPosition + 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) (segmentEndPosition - offsets[cuePointsSize - 1]);
durationsUs[cuePointsSize - 1] = durationUs - timesUs[cuePointsSize - 1];
cues = new SegmentIndex(sizeBytes, sizes, offsets, durationsUs, timesUs);
cueTimesUs = null;
cueClusterPositions = null;
prepared = true;
}
} }

View File

@ -63,7 +63,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
private final int maxHeight; private final int maxHeight;
private final SparseArray<FragmentedMp4Extractor> extractors; private final SparseArray<FragmentedMp4Extractor> extractors;
private final Format[] formats; private final SmoothStreamingFormat[] formats;
/** /**
* @param baseUrl The base URL for the streams. * @param baseUrl The base URL for the streams.
@ -94,23 +94,24 @@ public class SmoothStreamingChunkSource implements ChunkSource {
} }
int trackCount = trackIndices != null ? trackIndices.length : streamElement.tracks.length; int trackCount = trackIndices != null ? trackIndices.length : streamElement.tracks.length;
formats = new Format[trackCount]; formats = new SmoothStreamingFormat[trackCount];
extractors = new SparseArray<FragmentedMp4Extractor>(); extractors = new SparseArray<FragmentedMp4Extractor>();
int maxWidth = 0; int maxWidth = 0;
int maxHeight = 0; int maxHeight = 0;
for (int i = 0; i < trackCount; i++) { for (int i = 0; i < trackCount; i++) {
int trackIndex = trackIndices != null ? trackIndices[i] : i; int trackIndex = trackIndices != null ? trackIndices[i] : i;
TrackElement trackElement = streamElement.tracks[trackIndex]; TrackElement trackElement = streamElement.tracks[trackIndex];
formats[i] = new Format(trackIndex, trackElement.mimeType, trackElement.maxWidth, formats[i] = new SmoothStreamingFormat(String.valueOf(trackIndex), trackElement.mimeType,
trackElement.maxHeight, trackElement.numChannels, trackElement.sampleRate, trackElement.maxWidth, trackElement.maxHeight, trackElement.numChannels,
trackElement.bitrate / 8); trackElement.sampleRate, trackElement.bitrate, trackIndex);
maxWidth = Math.max(maxWidth, trackElement.maxWidth); maxWidth = Math.max(maxWidth, trackElement.maxWidth);
maxHeight = Math.max(maxHeight, trackElement.maxHeight); maxHeight = Math.max(maxHeight, trackElement.maxHeight);
MediaFormat mediaFormat = getMediaFormat(streamElement, trackIndex); MediaFormat mediaFormat = getMediaFormat(streamElement, trackIndex);
int trackType = streamElement.type == StreamElement.TYPE_VIDEO ? Track.TYPE_VIDEO int trackType = streamElement.type == StreamElement.TYPE_VIDEO ? Track.TYPE_VIDEO
: Track.TYPE_AUDIO; : Track.TYPE_AUDIO;
FragmentedMp4Extractor extractor = new FragmentedMp4Extractor(true); FragmentedMp4Extractor extractor = new FragmentedMp4Extractor(
FragmentedMp4Extractor.WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME);
extractor.setTrack(new Track(trackIndex, trackType, streamElement.timeScale, mediaFormat, extractor.setTrack(new Track(trackIndex, trackType, streamElement.timeScale, mediaFormat,
trackEncryptionBoxes)); trackEncryptionBoxes));
if (protectionElement != null) { if (protectionElement != null) {
@ -141,7 +142,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
} }
@Override @Override
public void disable(List<MediaChunk> queue) { public void disable(List<? extends MediaChunk> queue) {
// Do nothing. // Do nothing.
} }
@ -155,14 +156,14 @@ public class SmoothStreamingChunkSource implements ChunkSource {
long playbackPositionUs, ChunkOperationHolder out) { long playbackPositionUs, ChunkOperationHolder out) {
evaluation.queueSize = queue.size(); evaluation.queueSize = queue.size();
formatEvaluator.evaluate(queue, playbackPositionUs, formats, evaluation); formatEvaluator.evaluate(queue, playbackPositionUs, formats, evaluation);
Format selectedFormat = evaluation.format; SmoothStreamingFormat selectedFormat = (SmoothStreamingFormat) evaluation.format;
out.queueSize = evaluation.queueSize; out.queueSize = evaluation.queueSize;
if (selectedFormat == null) { if (selectedFormat == null) {
out.chunk = null; out.chunk = null;
return; return;
} else if (out.queueSize == queue.size() && out.chunk != null } else if (out.queueSize == queue.size() && out.chunk != null
&& out.chunk.format.id == evaluation.format.id) { && out.chunk.format.id.equals(evaluation.format.id)) {
// We already have a chunk, and the evaluation hasn't changed either the format or the size // We already have a chunk, and the evaluation hasn't changed either the format or the size
// of the queue. Do nothing. // of the queue. Do nothing.
return; return;
@ -181,11 +182,12 @@ public class SmoothStreamingChunkSource implements ChunkSource {
} }
boolean isLastChunk = nextChunkIndex == streamElement.chunkCount - 1; boolean isLastChunk = nextChunkIndex == streamElement.chunkCount - 1;
String requestUrl = streamElement.buildRequestUrl(selectedFormat.id, nextChunkIndex); String requestUrl = streamElement.buildRequestUrl(selectedFormat.trackIndex,
nextChunkIndex);
Uri uri = Uri.parse(baseUrl + '/' + requestUrl); Uri uri = Uri.parse(baseUrl + '/' + requestUrl);
Chunk mediaChunk = newMediaChunk(selectedFormat, uri, null, Chunk mediaChunk = newMediaChunk(selectedFormat, uri, null,
extractors.get(selectedFormat.id), dataSource, nextChunkIndex, isLastChunk, extractors.get(Integer.parseInt(selectedFormat.id)), dataSource, nextChunkIndex,
streamElement.getStartTimeUs(nextChunkIndex), isLastChunk, streamElement.getStartTimeUs(nextChunkIndex),
isLastChunk ? -1 : streamElement.getStartTimeUs(nextChunkIndex + 1), 0); isLastChunk ? -1 : streamElement.getStartTimeUs(nextChunkIndex + 1), 0);
out.chunk = mediaChunk; out.chunk = mediaChunk;
} }
@ -195,6 +197,11 @@ public class SmoothStreamingChunkSource implements ChunkSource {
return null; return null;
} }
@Override
public void onChunkLoadError(Chunk chunk, Exception e) {
// Do nothing.
}
private static MediaFormat getMediaFormat(StreamElement streamElement, int trackIndex) { private static MediaFormat getMediaFormat(StreamElement streamElement, int trackIndex) {
TrackElement trackElement = streamElement.tracks[trackIndex]; TrackElement trackElement = streamElement.tracks[trackIndex];
String mimeType = trackElement.mimeType; String mimeType = trackElement.mimeType;
@ -228,8 +235,8 @@ public class SmoothStreamingChunkSource implements ChunkSource {
DataSpec dataSpec = new DataSpec(uri, offset, -1, cacheKey); DataSpec dataSpec = new DataSpec(uri, offset, -1, cacheKey);
// In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk. // In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk.
// To convert them the absolute timestamps, we need to set sampleOffsetUs to -chunkStartTimeUs. // To convert them the absolute timestamps, we need to set sampleOffsetUs to -chunkStartTimeUs.
return new Mp4MediaChunk(dataSource, dataSpec, formatInfo, trigger, extractor, return new Mp4MediaChunk(dataSource, dataSpec, formatInfo, trigger, chunkStartTimeUs,
chunkStartTimeUs, nextStartTimeUs, -chunkStartTimeUs, nextChunkIndex); nextStartTimeUs, nextChunkIndex, extractor, false, -chunkStartTimeUs);
} }
private static byte[] getKeyId(byte[] initData) { private static byte[] getKeyId(byte[] initData) {
@ -254,4 +261,16 @@ public class SmoothStreamingChunkSource implements ChunkSource {
data[secondPosition] = temp; data[secondPosition] = temp;
} }
private static final class SmoothStreamingFormat extends Format {
public final int trackIndex;
public SmoothStreamingFormat(String id, String mimeType, int width, int height,
int numChannels, int audioSamplingRate, int bitrate, int trackIndex) {
super(id, mimeType, width, height, numChannels, audioSamplingRate, bitrate);
this.trackIndex = trackIndex;
}
}
} }

View File

@ -16,8 +16,8 @@
package com.google.android.exoplayer.smoothstreaming; package com.google.android.exoplayer.smoothstreaming;
import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.Util;
import java.util.Arrays;
import java.util.UUID; import java.util.UUID;
/** /**
@ -195,9 +195,7 @@ public class SmoothStreamingManifest {
* @return The index of the corresponding chunk. * @return The index of the corresponding chunk.
*/ */
public int getChunkIndex(long timeUs) { public int getChunkIndex(long timeUs) {
long time = (timeUs * timeScale) / 1000000L; return Util.binarySearchFloor(chunkStartTimes, (timeUs * timeScale) / 1000000L, true, true);
int chunkIndex = Arrays.binarySearch(chunkStartTimes, time);
return chunkIndex < 0 ? -chunkIndex - 2 : chunkIndex;
} }
/** /**

View File

@ -18,6 +18,8 @@ package com.google.android.exoplayer.smoothstreaming;
import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher;
import android.net.Uri;
import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException; import java.io.IOException;
@ -56,7 +58,7 @@ public final class SmoothStreamingManifestFetcher extends ManifestFetcher<Smooth
@Override @Override
protected SmoothStreamingManifest parse(InputStream stream, String inputEncoding, protected SmoothStreamingManifest parse(InputStream stream, String inputEncoding,
String contentId) throws IOException, ParserException { String contentId, Uri baseUrl) throws IOException, ParserException {
try { try {
return parser.parse(stream, inputEncoding); return parser.parse(stream, inputEncoding);
} catch (XmlPullParserException e) { } catch (XmlPullParserException e) {

View File

@ -16,8 +16,7 @@
package com.google.android.exoplayer.text.ttml; package com.google.android.exoplayer.text.ttml;
import com.google.android.exoplayer.text.Subtitle; import com.google.android.exoplayer.text.Subtitle;
import com.google.android.exoplayer.util.Util;
import java.util.Arrays;
/** /**
* A representation of a TTML subtitle. * A representation of a TTML subtitle.
@ -41,8 +40,7 @@ public final class TtmlSubtitle implements Subtitle {
@Override @Override
public int getNextEventTimeIndex(long timeUs) { public int getNextEventTimeIndex(long timeUs) {
int index = Arrays.binarySearch(eventTimesUs, timeUs - startTimeUs); int index = Util.binarySearchCeil(eventTimesUs, timeUs - startTimeUs, false, false);
index = index >= 0 ? index + 1 : ~index;
return index < eventTimesUs.length ? index : -1; return index < eventTimesUs.length ? index : -1;
} }

View File

@ -176,7 +176,7 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
*/ */
private int read(ByteBuffer target, byte[] targetArray, int targetArrayOffset, private int read(ByteBuffer target, byte[] targetArray, int targetArrayOffset,
ReadHead readHead, int readLength) { ReadHead readHead, int readLength) {
if (readHead.position == dataSpec.length) { if (isEndOfStream()) {
return -1; return -1;
} }
int bytesToRead = (int) Math.min(loadPosition - readHead.position, readLength); int bytesToRead = (int) Math.min(loadPosition - readHead.position, readLength);

View File

@ -115,8 +115,8 @@ public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener {
float bytesPerSecond = accumulator * 1000 / elapsedMs; float bytesPerSecond = accumulator * 1000 / elapsedMs;
slidingPercentile.addSample(computeWeight(accumulator), bytesPerSecond); slidingPercentile.addSample(computeWeight(accumulator), bytesPerSecond);
float bandwidthEstimateFloat = slidingPercentile.getPercentile(0.5f); float bandwidthEstimateFloat = slidingPercentile.getPercentile(0.5f);
bandwidthEstimate = bandwidthEstimateFloat == Float.NaN bandwidthEstimate = Float.isNaN(bandwidthEstimateFloat) ? NO_ESTIMATE
? NO_ESTIMATE : (long) bandwidthEstimateFloat; : (long) bandwidthEstimateFloat;
notifyBandwidthSample(elapsedMs, accumulator, bandwidthEstimate); notifyBandwidthSample(elapsedMs, accumulator, bandwidthEstimate);
} }
streamCount--; streamCount--;

View File

@ -134,9 +134,8 @@ public interface Cache {
* @param key The key of the data being requested. * @param key The key of the data being requested.
* @param position The position of the data being requested. * @param position The position of the data being requested.
* @return The {@link CacheSpan}. Or null if the cache entry is locked. * @return The {@link CacheSpan}. Or null if the cache entry is locked.
* @throws InterruptedException
*/ */
CacheSpan startReadWriteNonBlocking(String key, long position) throws InterruptedException; CacheSpan startReadWriteNonBlocking(String key, long position);
/** /**
* Obtains a cache file into which data can be written. Must only be called when holding a * Obtains a cache file into which data can be written. Must only be called when holding a
@ -173,4 +172,14 @@ public interface Cache {
*/ */
void removeSpan(CacheSpan span); void removeSpan(CacheSpan span);
/**
* Queries if a range is entirely available in the cache.
*
* @param key The cache key for the data.
* @param position The starting position of the data.
* @param length The length of the data.
* @return true if the data is available in the Cache otherwise false;
*/
boolean isCached(String key, long position, long length);
} }

View File

@ -109,26 +109,29 @@ public class SimpleCache implements Cache {
public synchronized CacheSpan startReadWrite(String key, long position) public synchronized CacheSpan startReadWrite(String key, long position)
throws InterruptedException { throws InterruptedException {
CacheSpan lookupSpan = CacheSpan.createLookup(key, position); CacheSpan lookupSpan = CacheSpan.createLookup(key, position);
// Wait until no-one holds a lock for the key. while (true) {
while (lockedSpans.containsKey(key)) { CacheSpan span = startReadWriteNonBlocking(lookupSpan);
if (span != null) {
return span;
} else {
// Write case, lock not available. We'll be woken up when a locked span is released (if the
// released lock is for the requested key then we'll be able to make progress) or when a
// span is added to the cache (if the span is for the requested key and covers the requested
// position, then we'll become a read and be able to make progress).
wait(); wait();
} }
return getSpanningRegion(key, lookupSpan); }
} }
@Override @Override
public synchronized CacheSpan startReadWriteNonBlocking(String key, long position) public synchronized CacheSpan startReadWriteNonBlocking(String key, long position) {
throws InterruptedException { return startReadWriteNonBlocking(CacheSpan.createLookup(key, position));
CacheSpan lookupSpan = CacheSpan.createLookup(key, position);
// Return null if key is locked
if (lockedSpans.containsKey(key)) {
return null;
}
return getSpanningRegion(key, lookupSpan);
} }
private CacheSpan getSpanningRegion(String key, CacheSpan lookupSpan) { private synchronized CacheSpan startReadWriteNonBlocking(CacheSpan lookupSpan) {
CacheSpan spanningRegion = getSpan(lookupSpan); CacheSpan spanningRegion = getSpan(lookupSpan);
// Read case.
if (spanningRegion.isCached) { if (spanningRegion.isCached) {
CacheSpan oldCacheSpan = spanningRegion; CacheSpan oldCacheSpan = spanningRegion;
// Remove the old span from the in-memory representation. // Remove the old span from the in-memory representation.
@ -139,12 +142,19 @@ public class SimpleCache implements Cache {
// Add the updated span back into the in-memory representation. // Add the updated span back into the in-memory representation.
spansForKey.add(spanningRegion); spansForKey.add(spanningRegion);
notifySpanTouched(oldCacheSpan, spanningRegion); notifySpanTouched(oldCacheSpan, spanningRegion);
} else {
lockedSpans.put(key, spanningRegion);
}
return spanningRegion; return spanningRegion;
} }
// Write case, lock available.
if (!lockedSpans.containsKey(lookupSpan.key)) {
lockedSpans.put(lookupSpan.key, spanningRegion);
return spanningRegion;
}
// Write case, lock not available.
return null;
}
@Override @Override
public synchronized File startFile(String key, long position, long length) { public synchronized File startFile(String key, long position, long length) {
Assertions.checkState(lockedSpans.containsKey(key)); Assertions.checkState(lockedSpans.containsKey(key));
@ -173,6 +183,7 @@ public class SimpleCache implements Cache {
return; return;
} }
addSpan(span); addSpan(span);
notifyAll();
} }
@Override @Override
@ -330,4 +341,41 @@ public class SimpleCache implements Cache {
evictor.onSpanTouched(this, oldSpan, newSpan); evictor.onSpanTouched(this, oldSpan, newSpan);
} }
@Override
public synchronized boolean isCached(String key, long position, long length) {
TreeSet<CacheSpan> entries = cachedSpans.get(key);
if (entries == null) {
return false;
}
CacheSpan lookupSpan = CacheSpan.createLookup(key, position);
CacheSpan floorSpan = entries.floor(lookupSpan);
if (floorSpan == null || floorSpan.position + floorSpan.length <= position) {
// We don't have a span covering the start of the queried region.
return false;
}
long queryEndPosition = position + length;
long currentEndPosition = floorSpan.position + floorSpan.length;
if (currentEndPosition >= queryEndPosition) {
// floorSpan covers the queried region.
return true;
}
Iterator<CacheSpan> iterator = entries.tailSet(floorSpan, false).iterator();
while (iterator.hasNext()) {
CacheSpan next = iterator.next();
if (next.position > currentEndPosition) {
// There's a hole in the cache within the queried region.
return false;
}
// We expect currentEndPosition to always equal (next.position + next.length), but
// perform a max check anyway to guard against the existence of overlapping spans.
currentEndPosition = Math.max(currentEndPosition, next.position + next.length);
if (currentEndPosition >= queryEndPosition) {
// We've found spans covering the queried region.
return true;
}
}
// We ran out of spans before covering the queried region.
return false;
}
} }

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer.util;
import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.ParserException;
import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import java.io.IOException; import java.io.IOException;
@ -84,14 +85,15 @@ public abstract class ManifestFetcher<T> extends AsyncTask<String, Void, T> {
protected final T doInBackground(String... data) { protected final T doInBackground(String... data) {
try { try {
contentId = data.length > 1 ? data[1] : null; contentId = data.length > 1 ? data[1] : null;
URL url = new URL(data[0]); String urlString = data[0];
String inputEncoding = null; String inputEncoding = null;
InputStream inputStream = null; InputStream inputStream = null;
try { try {
HttpURLConnection connection = configureHttpConnection(url); Uri baseUrl = Util.parseBaseUri(urlString);
HttpURLConnection connection = configureHttpConnection(new URL(urlString));
inputStream = connection.getInputStream(); inputStream = connection.getInputStream();
inputEncoding = connection.getContentEncoding(); inputEncoding = connection.getContentEncoding();
return parse(inputStream, inputEncoding, contentId); return parse(inputStream, inputEncoding, contentId, baseUrl);
} finally { } finally {
if (inputStream != null) { if (inputStream != null) {
inputStream.close(); inputStream.close();
@ -119,11 +121,13 @@ public abstract class ManifestFetcher<T> extends AsyncTask<String, Void, T> {
* @param stream The input stream to read. * @param stream The input stream to read.
* @param inputEncoding The encoding of the input stream. * @param inputEncoding The encoding of the input stream.
* @param contentId The content id of the media. * @param contentId The content id of the media.
* @param baseUrl Required where the manifest contains urls that are relative to a base url. May
* be null where this is not the case.
* @throws IOException If an error occurred loading the data. * @throws IOException If an error occurred loading the data.
* @throws ParserException If an error occurred parsing the loaded data. * @throws ParserException If an error occurred parsing the loaded data.
*/ */
protected abstract T parse(InputStream stream, String inputEncoding, String contentId) throws protected abstract T parse(InputStream stream, String inputEncoding, String contentId,
IOException, ParserException; Uri baseUrl) throws IOException, ParserException;
private HttpURLConnection configureHttpConnection(URL url) throws IOException { private HttpURLConnection configureHttpConnection(URL url) throws IOException {
HttpURLConnection connection = (HttpURLConnection) url.openConnection(); HttpURLConnection connection = (HttpURLConnection) url.openConnection();

View File

@ -20,17 +20,39 @@ package com.google.android.exoplayer.util;
*/ */
public class MimeTypes { public class MimeTypes {
public static final String VIDEO_MP4 = "video/mp4"; public static final String BASE_TYPE_VIDEO = "video";
public static final String VIDEO_WEBM = "video/webm"; public static final String BASE_TYPE_AUDIO = "audio";
public static final String VIDEO_H264 = "video/avc"; public static final String BASE_TYPE_TEXT = "text";
public static final String VIDEO_VP9 = "video/x-vnd.on2.vp9"; public static final String BASE_TYPE_APPLICATION = "application";
public static final String AUDIO_MP4 = "audio/mp4";
public static final String AUDIO_AAC = "audio/mp4a-latm"; public static final String VIDEO_MP4 = BASE_TYPE_VIDEO + "/mp4";
public static final String TEXT_VTT = "text/vtt"; public static final String VIDEO_WEBM = BASE_TYPE_VIDEO + "/webm";
public static final String APPLICATION_TTML = "application/ttml+xml"; public static final String VIDEO_H264 = BASE_TYPE_VIDEO + "/avc";
public static final String VIDEO_VP9 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp9";
public static final String AUDIO_MP4 = BASE_TYPE_AUDIO + "/mp4";
public static final String AUDIO_AAC = BASE_TYPE_AUDIO + "/mp4a-latm";
public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt";
public static final String APPLICATION_TTML = BASE_TYPE_APPLICATION + "/ttml+xml";
private MimeTypes() {} private MimeTypes() {}
/**
* Returns the top-level type of {@code mimeType}.
*
* @param mimeType The mimeType whose top-level type is required.
* @return The top-level type.
*/
public static String getTopLevelType(String mimeType) {
int indexOfSlash = mimeType.indexOf('/');
if (indexOfSlash == -1) {
throw new IllegalArgumentException("Invalid mime type: " + mimeType);
}
return mimeType.substring(0, indexOfSlash);
}
/** /**
* Whether the top-level type of {@code mimeType} is audio. * Whether the top-level type of {@code mimeType} is audio.
* *
@ -38,7 +60,7 @@ public class MimeTypes {
* @return Whether the top level type is audio. * @return Whether the top level type is audio.
*/ */
public static boolean isAudio(String mimeType) { public static boolean isAudio(String mimeType) {
return mimeType.startsWith("audio/"); return getTopLevelType(mimeType).equals(BASE_TYPE_AUDIO);
} }
/** /**
@ -48,7 +70,7 @@ public class MimeTypes {
* @return Whether the top level type is video. * @return Whether the top level type is video.
*/ */
public static boolean isVideo(String mimeType) { public static boolean isVideo(String mimeType) {
return mimeType.startsWith("video/"); return getTopLevelType(mimeType).equals(BASE_TYPE_VIDEO);
} }
/** /**
@ -58,7 +80,27 @@ public class MimeTypes {
* @return Whether the top level type is text. * @return Whether the top level type is text.
*/ */
public static boolean isText(String mimeType) { public static boolean isText(String mimeType) {
return mimeType.startsWith("text/"); return getTopLevelType(mimeType).equals(BASE_TYPE_TEXT);
}
/**
* Whether the top-level type of {@code mimeType} is application.
*
* @param mimeType The mimeType to test.
* @return Whether the top level type is application.
*/
public static boolean isApplication(String mimeType) {
return getTopLevelType(mimeType).equals(BASE_TYPE_APPLICATION);
}
/**
* Whether the mimeType is {@link #APPLICATION_TTML}.
*
* @param mimeType The mimeType to test.
* @return Whether the mimeType is {@link #APPLICATION_TTML}.
*/
public static boolean isTtml(String mimeType) {
return mimeType.equals(APPLICATION_TTML);
} }
} }

View File

@ -17,8 +17,13 @@ package com.google.android.exoplayer.util;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import android.net.Uri;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@ -112,4 +117,99 @@ public final class Util {
return text == null ? null : text.toLowerCase(Locale.US); return text == null ? null : text.toLowerCase(Locale.US);
} }
/**
* Like {@link Uri#parse(String)}, but discards the part of the uri that follows the final
* forward slash.
*
* @param uriString An RFC 2396-compliant, encoded uri.
* @return The parsed base uri.
*/
public static Uri parseBaseUri(String uriString) {
return Uri.parse(uriString.substring(0, uriString.lastIndexOf('/')));
}
/**
* Returns the index of the largest value in an array that is less than (or optionally equal to)
* a specified key.
* <p>
* The search is performed using a binary search algorithm, and so the array must be sorted.
*
* @param a The array to search.
* @param key The key being searched for.
* @param inclusive If the key is present in the array, whether to return the corresponding index.
* If false then the returned index corresponds to the largest value in the array that is
* strictly less than the key.
* @param stayInBounds If true, then 0 will be returned in the case that the key is smaller than
* the smallest value in the array. If false then -1 will be returned.
*/
public static int binarySearchFloor(long[] a, long key, boolean inclusive, boolean stayInBounds) {
int index = Arrays.binarySearch(a, key);
index = index < 0 ? -(index + 2) : (inclusive ? index : (index - 1));
return stayInBounds ? Math.max(0, index) : index;
}
/**
* Returns the index of the smallest value in an array that is greater than (or optionally equal
* to) a specified key.
* <p>
* The search is performed using a binary search algorithm, and so the array must be sorted.
*
* @param a The array to search.
* @param key The key being searched for.
* @param inclusive If the key is present in the array, whether to return the corresponding index.
* If false then the returned index corresponds to the smallest value in the array that is
* strictly greater than the key.
* @param stayInBounds If true, then {@code (a.length - 1)} will be returned in the case that the
* key is greater than the largest value in the array. If false then {@code a.length} will be
* returned.
*/
public static int binarySearchCeil(long[] a, long key, boolean inclusive, boolean stayInBounds) {
int index = Arrays.binarySearch(a, key);
index = index < 0 ? ~index : (inclusive ? index : (index + 1));
return stayInBounds ? Math.min(a.length - 1, index) : index;
}
/**
* Returns the index of the largest value in an list that is less than (or optionally equal to)
* a specified key.
* <p>
* The search is performed using a binary search algorithm, and so the list must be sorted.
*
* @param list The list to search.
* @param key The key being searched for.
* @param inclusive If the key is present in the list, whether to return the corresponding index.
* If false then the returned index corresponds to the largest value in the list that is
* strictly less than the key.
* @param stayInBounds If true, then 0 will be returned in the case that the key is smaller than
* the smallest value in the list. If false then -1 will be returned.
*/
public static<T> int binarySearchFloor(List<? extends Comparable<? super T>> list, T key,
boolean inclusive, boolean stayInBounds) {
int index = Collections.binarySearch(list, key);
index = index < 0 ? -(index + 2) : (inclusive ? index : (index - 1));
return stayInBounds ? Math.max(0, index) : index;
}
/**
* Returns the index of the smallest value in an list that is greater than (or optionally equal
* to) a specified key.
* <p>
* The search is performed using a binary search algorithm, and so the list must be sorted.
*
* @param list The list to search.
* @param key The key being searched for.
* @param inclusive If the key is present in the list, whether to return the corresponding index.
* If false then the returned index corresponds to the smallest value in the list that is
* strictly greater than the key.
* @param stayInBounds If true, then {@code (list.size() - 1)} will be returned in the case that
* the key is greater than the largest value in the list. If false then {@code list.size()}
* will be returned.
*/
public static<T> int binarySearchCeil(List<? extends Comparable<? super T>> list, T key,
boolean inclusive, boolean stayInBounds) {
int index = Collections.binarySearch(list, key);
index = index < 0 ? ~index : (inclusive ? index : (index + 1));
return stayInBounds ? Math.min(list.size() - 1, index) : index;
}
} }