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
## 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 ##
The repository includes Eclipse projects for both the ExoPlayer library and its

View File

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

View File

@ -52,7 +52,7 @@ public class SampleChooserActivity extends Activity {
sampleAdapter.addAll((Object[]) Samples.SIMPLE);
sampleAdapter.add(new Header("YouTube DASH"));
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.add(new Header("SmoothStreaming"));
sampleAdapter.addAll((Object[]) Samples.SMOOTHSTREAMING);

View File

@ -91,7 +91,7 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener
}
@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) {
loadStartTimeMs[sourceId] = SystemClock.elapsedRealtime();
if (VerboseLogUtil.isTagEnabled(TAG)) {
@ -110,13 +110,13 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener
}
@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 + ", " +
Integer.toString(trigger) + "]");
}
@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 + ", " +
Integer.toString(trigger) + "]");
}

View File

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

View File

@ -118,11 +118,11 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
* A listener for debugging information.
*/
public interface InfoListener {
void onVideoFormatEnabled(int formatId, int trigger, int mediaTimeMs);
void onAudioFormatEnabled(int formatId, int trigger, int mediaTimeMs);
void onVideoFormatEnabled(String formatId, int trigger, int mediaTimeMs);
void onAudioFormatEnabled(String formatId, int trigger, int mediaTimeMs);
void onDroppedFrames(int count, long elapsed);
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);
void onLoadCompleted(int sourceId);
}
@ -398,7 +398,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
}
@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) {
return;
}
@ -469,7 +470,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
}
@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) {
if (infoListener != null) {
infoListener.onLoadStarted(sourceId, formatId, trigger, isInitialization, mediaStartTimeMs,

View File

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

View File

@ -115,8 +115,7 @@ import java.util.ArrayList;
videoRepresentationsList.toArray(videoRepresentations);
// Build the video renderer.
DataSource videoDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES,
bandwidthMeter);
DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
ChunkSource videoChunkSource = new DashMp4ChunkSource(videoDataSource,
new AdaptiveEvaluator(bandwidthMeter), videoRepresentations);
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);
// Build the audio renderer.
DataSource audioDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES,
bandwidthMeter);
DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
ChunkSource audioChunkSource = new DashMp4ChunkSource(audioDataSource,
new FormatEvaluator.FixedEvaluator(), audioRepresentation);
SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,

View File

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

View File

@ -26,7 +26,7 @@ public class ExoPlayerLibraryInfo {
/**
* 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.

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_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 ConditionVariable audioTrackReleasingConditionVariable;
private final AudioTimestampCompat audioTimestampCompat;
@ -119,7 +123,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
private Method audioTrackGetLatencyMethod;
private int audioSessionId;
private long submittedBytes;
private boolean audioTrackStartMediaTimeSet;
private int audioTrackStartMediaTimeState;
private long audioTrackStartMediaTimeUs;
private long audioTrackResumeSystemTimeUs;
private long lastReportedCurrentPositionUs;
@ -363,7 +367,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
lastRawPlaybackHeadPosition = 0;
rawPlaybackHeadWrapCount = 0;
audioTrackStartMediaTimeUs = 0;
audioTrackStartMediaTimeSet = false;
audioTrackStartMediaTimeState = START_NOT_SET;
resetSyncParams();
int playState = audioTrack.getPlayState();
if (playState == AudioTrack.PLAYSTATE_PLAYING) {
@ -429,7 +433,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
protected long getCurrentPositionUs() {
long systemClockUs = System.nanoTime() / 1000;
long currentPositionUs;
if (audioTrack == null || !audioTrackStartMediaTimeSet) {
if (audioTrack == null || audioTrackStartMediaTimeState == START_NOT_SET) {
// The AudioTrack hasn't started.
currentPositionUs = super.getCurrentPositionUs();
} else if (audioTimestampSet) {
@ -461,7 +465,8 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
}
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.
return;
}
@ -549,25 +554,40 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
@Override
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) {
// This is the first time we've seen this {@code buffer}.
// Note: presentationTimeUs corresponds to the end of the sample, not the start.
long bufferStartTime = bufferInfo.presentationTimeUs -
framesToDurationUs(bufferInfo.size / frameSize);
if (!audioTrackStartMediaTimeSet) {
if (audioTrackStartMediaTimeState == START_NOT_SET) {
audioTrackStartMediaTimeUs = Math.max(0, bufferStartTime);
audioTrackStartMediaTimeSet = true;
audioTrackStartMediaTimeState = START_IN_SYNC;
} else {
// Sanity check that bufferStartTime is consistent with the expected value.
long expectedBufferStartTime = audioTrackStartMediaTimeUs +
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 " +
bufferStartTime + "]");
// Adjust audioTrackStartMediaTimeUs to compensate for the discontinuity. Also reset
// lastReportedCurrentPositionUs to allow time to jump backwards if it really wants to.
audioTrackStartMediaTimeState = START_NEED_SYNC;
}
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);
lastReportedCurrentPositionUs = 0;
}

View File

@ -391,7 +391,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
while (result == SampleSource.SAMPLE_READ && currentPositionUs <= timeUs) {
result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false);
if (result == SampleSource.SAMPLE_READ) {
currentPositionUs = sampleHolder.timeUs;
if (!sampleHolder.decodeOnly) {
currentPositionUs = sampleHolder.timeUs;
}
codecCounters.discardedSamplesCount++;
} else if (result == SampleSource.FORMAT_READ) {
onInputFormatChanged(formatHolder);
@ -677,15 +679,15 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
return false;
}
if (decodeOnlyPresentationTimestamps.remove(outputBufferInfo.presentationTimeUs)) {
codec.releaseOutputBuffer(outputIndex, false);
outputIndex = -1;
return true;
}
boolean decodeOnly = decodeOnlyPresentationTimestamps.contains(
outputBufferInfo.presentationTimeUs);
if (processOutputBuffer(timeUs, codec, outputBuffers[outputIndex], outputBufferInfo,
outputIndex)) {
currentPositionUs = outputBufferInfo.presentationTimeUs;
outputIndex, decodeOnly)) {
if (decodeOnly) {
decodeOnlyPresentationTimestamps.remove(outputBufferInfo.presentationTimeUs);
} else {
currentPositionUs = outputBufferInfo.presentationTimeUs;
}
outputIndex = -1;
return true;
}
@ -701,7 +703,8 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
* @throws ExoPlaybackException If an error occurs processing the output 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.

View File

@ -29,7 +29,7 @@ import android.view.Surface;
import java.nio.ByteBuffer;
/**
* Decodes and renders video using {@MediaCodec}.
* Decodes and renders video using {@link MediaCodec}.
*/
@TargetApi(16)
public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
@ -338,7 +338,12 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
@Override
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;
if (earlyUs < -30000) {
// We're more than 30ms late rendering the frame.
@ -371,6 +376,13 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
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) {
TraceUtil.beginSection("dropVideoBuffer");
codec.releaseOutputBuffer(bufferIndex, false);

View File

@ -54,8 +54,8 @@ public interface SampleSource {
* Prepares the source.
* <p>
* 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.
* The method can be called repeatedly until the return value indicates success.
* and formats). If insufficient data is available then the call will return {@code false} rather
* than block. The method can be called repeatedly until the return value indicates success.
*
* @return True if the source was prepared successfully, false otherwise.
* @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.
* @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);
/**
@ -126,7 +126,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
* {@link ChunkSource}.
* @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 long currentLoadableExceptionTimestamp;
private MediaFormat downstreamMediaFormat;
private volatile Format downstreamFormat;
public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl,
@ -221,6 +222,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
chunkSource.enable();
loadControl.register(this, bufferSizeContribution);
downstreamFormat = null;
downstreamMediaFormat = null;
downstreamPositionUs = timeUs;
lastSeekPositionUs = timeUs;
restartFrom(timeUs);
@ -288,21 +290,30 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
return readData(track, playbackPositionUs, formatHolder, sampleHolder, false);
} else if (mediaChunk.isLastChunk()) {
return END_OF_STREAM;
} else {
IOException chunkSourceException = chunkSource.getError();
if (chunkSourceException != null) {
throw chunkSourceException;
}
return NOTHING_READ;
}
} else if (downstreamFormat == null || downstreamFormat.id != mediaChunk.format.id) {
IOException chunkSourceException = chunkSource.getError();
if (chunkSourceException != null) {
throw chunkSourceException;
}
return NOTHING_READ;
}
if (downstreamFormat == null || !downstreamFormat.equals(mediaChunk.format)) {
notifyDownstreamFormatChanged(mediaChunk.format.id, mediaChunk.trigger,
mediaChunk.startTimeUs);
MediaFormat format = mediaChunk.getMediaFormat();
chunkSource.getMaxVideoDimensions(format);
formatHolder.format = format;
formatHolder.drmInitData = mediaChunk.getPsshInfo();
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;
}
@ -430,6 +441,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
currentLoadableExceptionCount++;
currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
notifyUpstreamError(e);
chunkSource.onChunkLoadError(currentLoadableHolder.chunk, e);
updateLoadControl();
}
@ -653,7 +665,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
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 long totalBytes) {
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) {
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {

View File

@ -58,7 +58,7 @@ public interface ChunkSource {
*
* @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.
@ -100,4 +100,13 @@ public interface ChunkSource {
*/
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;
import com.google.android.exoplayer.util.Assertions;
import java.util.Comparator;
/**
* A format definition for streams.
*/
public final class Format {
public class Format {
/**
* Sorts {@link Format} objects in order of decreasing bandwidth.
@ -29,7 +31,7 @@ public final class Format {
@Override
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.
*/
public final int id;
public final String id;
/**
* The mime type of the format.
@ -65,8 +67,16 @@ public final class Format {
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;
/**
@ -76,17 +86,38 @@ public final class Format {
* @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 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,
int audioSamplingRate, int bandwidth) {
this.id = id;
public Format(String id, String mimeType, int width, int height, int numChannels,
int audioSamplingRate, int bitrate) {
this.id = Assertions.checkNotNull(id);
this.mimeType = mimeType;
this.width = width;
this.height = height;
this.numChannels = numChannels;
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,
Format[] formats, Evaluation evaluation) {
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.format = newFormat;
@ -236,8 +236,8 @@ public interface FormatEvaluator {
: queue.get(queue.size() - 1).endTimeUs - playbackPositionUs;
Format current = evaluation.format;
Format ideal = determineIdealFormat(formats, bandwidthMeter.getEstimate());
boolean isHigher = ideal != null && current != null && ideal.bandwidth > current.bandwidth;
boolean isLower = 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.bitrate < current.bitrate;
if (isHigher) {
if (bufferedDurationUs < minDurationForQualityIncreaseUs) {
// 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
// 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.
for (int i = 0; i < queue.size(); i++) {
for (int i = 1; i < queue.size(); i++) {
MediaChunk thisChunk = queue.get(i);
long durationBeforeThisSegmentUs = thisChunk.startTimeUs - playbackPositionUs;
if (durationBeforeThisSegmentUs >= minDurationToRetainAfterDiscardUs
&& thisChunk.format.bandwidth < ideal.bandwidth
&& thisChunk.format.bitrate < ideal.bitrate
&& thisChunk.format.height < ideal.height
&& thisChunk.format.height < 720
&& thisChunk.format.width < 1280) {
@ -280,7 +280,7 @@ public interface FormatEvaluator {
long effectiveBandwidth = computeEffectiveBandwidthEstimate(bandwidthEstimate);
for (int i = 0; i < formats.length; i++) {
Format format = formats[i];
if (format.bandwidth <= effectiveBandwidth) {
if ((format.bitrate / 8) <= effectiveBandwidth) {
return format;
}
}

View File

@ -73,9 +73,7 @@ public abstract class MediaChunk extends Chunk {
/**
* Seeks to the beginning of the chunk.
*/
public final void seekToStart() {
seekTo(startTimeUs, false);
}
public abstract void seekToStart();
/**
* 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);
/**
* 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.
* <p>
* Should only be called after the chunk has been successfully prepared.
*
* @param holder A holder to store the read sample.
* @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.
* <p>
* Should only be called after the chunk has been successfully prepared.
*
* @return The sample media format.
*/
@ -108,6 +122,8 @@ public abstract class MediaChunk extends 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.
*/

View File

@ -33,27 +33,43 @@ import java.util.UUID;
public final class Mp4MediaChunk extends MediaChunk {
private final FragmentedMp4Extractor extractor;
private final boolean maybeSelfContained;
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 dataSpec Defines the data to be loaded.
* @param format The format of the stream to which this chunk belongs.
* @param extractor The extractor that will be used to extract the samples.
* @param trigger The reason for this chunk being selected.
* @param startTimeUs The start time of the media contained by the chunk, in microseconds.
* @param endTimeUs The end time of the media contained by the chunk, in microseconds.
* @param 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 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,
int trigger, FragmentedMp4Extractor extractor, long startTimeUs, long endTimeUs,
long sampleOffsetUs, int nextChunkIndex) {
int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex,
FragmentedMp4Extractor extractor, boolean maybeSelfContained, long sampleOffsetUs) {
super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex);
this.extractor = extractor;
this.maybeSelfContained = maybeSelfContained;
this.sampleOffsetUs = sampleOffsetUs;
}
@Override
public void seekToStart() {
extractor.seekTo(0, false);
resetReadPosition();
}
@Override
public boolean seekTo(long positionUs, boolean allowNoop) {
long seekTimeUs = positionUs + sampleOffsetUs;
@ -64,6 +80,29 @@ public final class Mp4MediaChunk extends MediaChunk {
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
public boolean read(SampleHolder holder) throws ParserException {
NonBlockingInputStream inputStream = getNonBlockingInputStream();
@ -78,12 +117,12 @@ public final class Mp4MediaChunk extends MediaChunk {
@Override
public MediaFormat getMediaFormat() {
return extractor.getFormat();
return mediaFormat;
}
@Override
public Map<UUID, byte[]> getPsshInfo() {
return extractor.getPsshInfo();
return psshInfo;
}
}

View File

@ -68,7 +68,7 @@ public class MultiTrackChunkSource implements ChunkSource, ExoPlayerComponent {
}
@Override
public void disable(List<MediaChunk> queue) {
public void disable(List<? extends MediaChunk> queue) {
selectedSource.disable(queue);
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;
}
@Override
public boolean prepare() {
return true;
}
@Override
public boolean read(SampleHolder holder) {
NonBlockingInputStream inputStream = getNonBlockingInputStream();
@ -109,6 +114,11 @@ public class SingleSampleMediaChunk extends MediaChunk {
return true;
}
@Override
public void seekToStart() {
resetReadPosition();
}
@Override
public boolean seekTo(long positionUs, boolean allowNoop) {
resetReadPosition();

View File

@ -50,6 +50,11 @@ public final class WebmMediaChunk extends MediaChunk {
this.extractor = extractor;
}
@Override
public void seekToStart() {
seekTo(0, false);
}
@Override
public boolean seekTo(long positionUs, boolean allowNoop) {
boolean isDiscontinuous = extractor.seekTo(positionUs, allowNoop);
@ -59,6 +64,11 @@ public final class WebmMediaChunk extends MediaChunk {
return isDiscontinuous;
}
@Override
public boolean prepare() {
return true;
}
@Override
public boolean read(SampleHolder holder) {
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.MediaChunk;
import com.google.android.exoplayer.chunk.Mp4MediaChunk;
import com.google.android.exoplayer.dash.mpd.RangedUri;
import com.google.android.exoplayer.dash.mpd.Representation;
import com.google.android.exoplayer.parser.SegmentIndex;
import com.google.android.exoplayer.parser.mp4.FragmentedMp4Extractor;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import android.util.Log;
import android.util.SparseArray;
import android.net.Uri;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
/**
@ -46,26 +46,17 @@ import java.util.List;
*/
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 DataSource dataSource;
private final FormatEvaluator evaluator;
private final Evaluation evaluation;
private final int maxWidth;
private final int maxHeight;
private final int numSegmentsPerChunk;
private final Format[] formats;
private final SparseArray<Representation> representations;
private final SparseArray<FragmentedMp4Extractor> extractors;
private final HashMap<String, Representation> representations;
private final HashMap<String, FragmentedMp4Extractor> extractors;
private final HashMap<String, DashSegmentIndex> segmentIndexes;
private boolean lastChunkWasInitialization;
@ -76,26 +67,14 @@ public class DashMp4ChunkSource implements ChunkSource {
*/
public DashMp4ChunkSource(DataSource dataSource, FormatEvaluator evaluator,
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.evaluator = evaluator;
this.numSegmentsPerChunk = numSegmentsPerChunk;
this.formats = new Format[representations.length];
this.extractors = new SparseArray<FragmentedMp4Extractor>();
this.representations = new SparseArray<Representation>();
this.extractors = new HashMap<String, FragmentedMp4Extractor>();
this.segmentIndexes = new HashMap<String, DashSegmentIndex>();
this.representations = new HashMap<String, Representation>();
this.trackInfo = new TrackInfo(representations[0].format.mimeType,
representations[0].periodDuration * 1000);
representations[0].periodDurationMs * 1000);
this.evaluation = new Evaluation();
int maxWidth = 0;
int maxHeight = 0;
@ -103,8 +82,12 @@ public class DashMp4ChunkSource implements ChunkSource {
formats[i] = representations[i].format;
maxWidth = Math.max(formats[i].width, maxWidth);
maxHeight = Math.max(formats[i].height, maxHeight);
extractors.append(formats[i].id, new FragmentedMp4Extractor());
extractors.put(formats[i].id, new FragmentedMp4Extractor());
this.representations.put(formats[i].id, representations[i]);
DashSegmentIndex segmentIndex = representations[i].getIndex();
if (segmentIndex != null) {
segmentIndexes.put(formats[i].id, segmentIndex);
}
}
this.maxWidth = maxWidth;
this.maxHeight = maxHeight;
@ -129,7 +112,7 @@ public class DashMp4ChunkSource implements ChunkSource {
}
@Override
public void disable(List<MediaChunk> queue) {
public void disable(List<? extends MediaChunk> queue) {
evaluator.disable();
}
@ -152,7 +135,7 @@ public class DashMp4ChunkSource implements ChunkSource {
out.chunk = null;
return;
} 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
// of the queue. Leave unchanged.
return;
@ -160,29 +143,39 @@ public class DashMp4ChunkSource implements ChunkSource {
Representation selectedRepresentation = representations.get(selectedFormat.id);
FragmentedMp4Extractor extractor = extractors.get(selectedRepresentation.format.id);
RangedUri pendingInitializationUri = null;
RangedUri pendingIndexUri = null;
if (extractor.getTrack() == null) {
Chunk initializationChunk = newInitializationChunk(selectedRepresentation, extractor,
dataSource, evaluation.trigger);
pendingInitializationUri = selectedRepresentation.getInitializationUri();
}
if (!segmentIndexes.containsKey(selectedRepresentation.format.id)) {
pendingIndexUri = selectedRepresentation.getIndexUri();
}
if (pendingInitializationUri != null || pendingIndexUri != null) {
// We have initialization and/or index requests to make.
Chunk initializationChunk = newInitializationChunk(pendingInitializationUri, pendingIndexUri,
selectedRepresentation, extractor, dataSource, evaluation.trigger);
lastChunkWasInitialization = true;
out.chunk = initializationChunk;
return;
}
int nextIndex;
int nextSegmentNum;
DashSegmentIndex segmentIndex = segmentIndexes.get(selectedRepresentation.format.id);
if (queue.isEmpty()) {
nextIndex = Arrays.binarySearch(extractor.getSegmentIndex().timesUs, seekPositionUs);
nextIndex = nextIndex < 0 ? -nextIndex - 2 : nextIndex;
nextSegmentNum = segmentIndex.getSegmentNum(seekPositionUs);
} else {
nextIndex = queue.get(out.queueSize - 1).nextChunkIndex;
nextSegmentNum = queue.get(out.queueSize - 1).nextChunkIndex;
}
if (nextIndex == -1) {
if (nextSegmentNum == -1) {
out.chunk = null;
return;
}
Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, extractor, dataSource,
extractor.getSegmentIndex(), nextIndex, evaluation.trigger, numSegmentsPerChunk);
Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, segmentIndex, extractor,
dataSource, nextSegmentNum, evaluation.trigger);
lastChunkWasInitialization = false;
out.chunk = nextMediaChunk;
}
@ -192,75 +185,80 @@ public class DashMp4ChunkSource implements ChunkSource {
return null;
}
private static Chunk newInitializationChunk(Representation representation,
FragmentedMp4Extractor extractor, DataSource dataSource, int trigger) {
DataSpec dataSpec = new DataSpec(representation.uri, 0, representation.indexEnd + 1,
representation.getCacheKey());
return new InitializationMp4Loadable(dataSource, dataSpec, trigger, extractor, representation);
@Override
public void onChunkLoadError(Chunk chunk, Exception e) {
// Do nothing.
}
private static Chunk newMediaChunk(Representation representation,
FragmentedMp4Extractor extractor, DataSource dataSource, SegmentIndex sidx, int index,
int trigger, int numSegmentsPerChunk) {
// Computes the segments to included in the next fetch.
int numSegmentsToFetch = Math.min(numSegmentsPerChunk, sidx.length - index);
int lastSegmentInChunk = index + numSegmentsToFetch - 1;
int nextIndex = lastSegmentInChunk == sidx.length - 1 ? -1 : lastSegmentInChunk + 1;
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];
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(representation.uri, offset, size,
DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length,
representation.getCacheKey());
return new Mp4MediaChunk(dataSource, dataSpec, representation.format, trigger, extractor,
startTimeUs, endTimeUs, 0, nextIndex);
return new InitializationMp4Loadable(dataSource, dataSpec, trigger, representation.format,
extractor, expectedExtractorResult, indexAnchor);
}
private static class InitializationMp4Loadable extends Chunk {
private Chunk newMediaChunk(Representation representation, DashSegmentIndex segmentIndex,
FragmentedMp4Extractor extractor, DataSource dataSource, int segmentNum, int trigger) {
int lastSegmentNum = segmentIndex.getLastSegmentNum();
int nextSegmentNum = segmentNum == lastSegmentNum ? -1 : segmentNum + 1;
long startTimeUs = segmentIndex.getTimeUs(segmentNum);
long endTimeUs = segmentNum < lastSegmentNum ? segmentIndex.getTimeUs(segmentNum + 1)
: startTimeUs + segmentIndex.getDurationUs(segmentNum);
RangedUri segmentUri = segmentIndex.getSegmentUrl(segmentNum);
DataSpec dataSpec = new DataSpec(segmentUri.getUri(), segmentUri.start, segmentUri.length,
representation.getCacheKey());
return new Mp4MediaChunk(dataSource, dataSpec, representation.format, trigger, startTimeUs,
endTimeUs, nextSegmentNum, extractor, false, 0);
}
private class InitializationMp4Loadable extends Chunk {
private final Representation representation;
private final FragmentedMp4Extractor extractor;
private final int expectedExtractorResult;
private final long indexAnchor;
private final Uri uri;
public InitializationMp4Loadable(DataSource dataSource, DataSpec dataSpec, int trigger,
FragmentedMp4Extractor extractor, Representation representation) {
super(dataSource, dataSpec, representation.format, trigger);
Format format, FragmentedMp4Extractor extractor, int expectedExtractorResult,
long indexAnchor) {
super(dataSource, dataSpec, format, trigger);
this.extractor = extractor;
this.representation = representation;
this.expectedExtractorResult = expectedExtractorResult;
this.indexAnchor = indexAnchor;
this.uri = dataSpec.uri;
}
@Override
protected void consumeStream(NonBlockingInputStream stream) throws IOException {
int result = extractor.read(stream, null);
if (result != EXPECTED_INITIALIZATION_RESULT) {
throw new ParserException("Invalid initialization data");
if (result != expectedExtractorResult) {
throw new ParserException("Invalid extractor result. Expected "
+ expectedExtractorResult + ", got " + result);
}
validateSegmentIndex(extractor.getSegmentIndex());
}
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);
if ((result & FragmentedMp4Extractor.RESULT_READ_SIDX) != 0) {
segmentIndexes.put(format.id,
new DashWrappingSegmentIndex(extractor.getSegmentIndex(), uri, indexAnchor));
}
}

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.MediaChunk;
import com.google.android.exoplayer.chunk.WebmMediaChunk;
import com.google.android.exoplayer.dash.mpd.RangedUri;
import com.google.android.exoplayer.dash.mpd.Representation;
import com.google.android.exoplayer.parser.SegmentIndex;
import com.google.android.exoplayer.parser.webm.DefaultWebmExtractor;
import com.google.android.exoplayer.parser.webm.WebmExtractor;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import android.util.Log;
import android.util.SparseArray;
import android.net.Uri;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
/**
@ -46,37 +47,30 @@ import java.util.List;
*/
public class DashWebmChunkSource implements ChunkSource {
private static final String TAG = "DashWebmChunkSource";
private final TrackInfo trackInfo;
private final DataSource dataSource;
private final FormatEvaluator evaluator;
private final Evaluation evaluation;
private final int maxWidth;
private final int maxHeight;
private final int numSegmentsPerChunk;
private final Format[] formats;
private final SparseArray<Representation> representations;
private final SparseArray<WebmExtractor> extractors;
private final HashMap<String, Representation> representations;
private final HashMap<String, WebmExtractor> extractors;
private final HashMap<String, DashSegmentIndex> segmentIndexes;
private boolean lastChunkWasInitialization;
public DashWebmChunkSource(
DataSource dataSource, FormatEvaluator evaluator, Representation... representations) {
this(dataSource, evaluator, 1, representations);
}
public DashWebmChunkSource(DataSource dataSource, FormatEvaluator evaluator,
int numSegmentsPerChunk, Representation... representations) {
Representation... representations) {
this.dataSource = dataSource;
this.evaluator = evaluator;
this.numSegmentsPerChunk = numSegmentsPerChunk;
this.formats = new Format[representations.length];
this.extractors = new SparseArray<WebmExtractor>();
this.representations = new SparseArray<Representation>();
this.extractors = new HashMap<String, WebmExtractor>();
this.segmentIndexes = new HashMap<String, DashSegmentIndex>();
this.representations = new HashMap<String, Representation>();
this.trackInfo = new TrackInfo(
representations[0].format.mimeType, representations[0].periodDuration * 1000);
representations[0].format.mimeType, representations[0].periodDurationMs * 1000);
this.evaluation = new Evaluation();
int maxWidth = 0;
int maxHeight = 0;
@ -84,8 +78,12 @@ public class DashWebmChunkSource implements ChunkSource {
formats[i] = representations[i].format;
maxWidth = Math.max(formats[i].width, maxWidth);
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]);
DashSegmentIndex segmentIndex = representations[i].getIndex();
if (segmentIndex != null) {
segmentIndexes.put(formats[i].id, segmentIndex);
}
}
this.maxWidth = maxWidth;
this.maxHeight = maxHeight;
@ -110,7 +108,7 @@ public class DashWebmChunkSource implements ChunkSource {
}
@Override
public void disable(List<MediaChunk> queue) {
public void disable(List<? extends MediaChunk> queue) {
evaluator.disable();
}
@ -133,7 +131,7 @@ public class DashWebmChunkSource implements ChunkSource {
out.chunk = null;
return;
} 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
// of the queue. Leave unchanged.
return;
@ -141,29 +139,34 @@ public class DashWebmChunkSource implements ChunkSource {
Representation selectedRepresentation = representations.get(selectedFormat.id);
WebmExtractor extractor = extractors.get(selectedRepresentation.format.id);
if (!extractor.isPrepared()) {
Chunk initializationChunk = newInitializationChunk(selectedRepresentation, extractor,
dataSource, evaluation.trigger);
// TODO: This code forces cues to exist and to immediately follow the initialization
// 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;
out.chunk = initializationChunk;
return;
}
int nextIndex;
int nextSegmentNum;
DashSegmentIndex segmentIndex = segmentIndexes.get(selectedRepresentation.format.id);
if (queue.isEmpty()) {
nextIndex = Arrays.binarySearch(extractor.getCues().timesUs, seekPositionUs);
nextIndex = nextIndex < 0 ? -nextIndex - 2 : nextIndex;
nextSegmentNum = segmentIndex.getSegmentNum(seekPositionUs);
} else {
nextIndex = queue.get(out.queueSize - 1).nextChunkIndex;
nextSegmentNum = queue.get(out.queueSize - 1).nextChunkIndex;
}
if (nextIndex == -1) {
if (nextSegmentNum == -1) {
out.chunk = null;
return;
}
Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, extractor, dataSource,
extractor.getCues(), nextIndex, evaluation.trigger, numSegmentsPerChunk);
Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, segmentIndex, extractor,
dataSource, nextSegmentNum, evaluation.trigger);
lastChunkWasInitialization = false;
out.chunk = nextMediaChunk;
}
@ -173,53 +176,43 @@ public class DashWebmChunkSource implements ChunkSource {
return null;
}
private static Chunk newInitializationChunk(Representation representation,
WebmExtractor extractor, DataSource dataSource, int trigger) {
DataSpec dataSpec = new DataSpec(representation.uri, 0, representation.indexEnd + 1,
representation.getCacheKey());
return new InitializationWebmLoadable(dataSource, dataSpec, trigger, extractor, representation);
@Override
public void onChunkLoadError(Chunk chunk, Exception e) {
// Do nothing.
}
private static Chunk newMediaChunk(Representation representation,
WebmExtractor extractor, DataSource dataSource, SegmentIndex cues, int index,
int trigger, int numSegmentsPerChunk) {
private Chunk newInitializationChunk(RangedUri initializationUri, Representation representation,
WebmExtractor extractor, DataSource dataSource, int trigger) {
DataSpec dataSpec = new DataSpec(initializationUri.getUri(), initializationUri.start,
initializationUri.length, representation.getCacheKey());
return new InitializationWebmLoadable(dataSource, dataSpec, trigger, representation.format,
extractor);
}
// Computes the segments to included in the next fetch.
int numSegmentsToFetch = Math.min(numSegmentsPerChunk, cues.length - index);
int lastSegmentInChunk = index + numSegmentsToFetch - 1;
int nextIndex = lastSegmentInChunk == cues.length - 1 ? -1 : lastSegmentInChunk + 1;
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,
private Chunk newMediaChunk(Representation representation, DashSegmentIndex segmentIndex,
WebmExtractor extractor, DataSource dataSource, int segmentNum, int trigger) {
int lastSegmentNum = segmentIndex.getLastSegmentNum();
int nextSegmentNum = segmentNum == lastSegmentNum ? -1 : segmentNum + 1;
long startTimeUs = segmentIndex.getTimeUs(segmentNum);
long endTimeUs = segmentNum < lastSegmentNum ? segmentIndex.getTimeUs(segmentNum + 1)
: startTimeUs + segmentIndex.getDurationUs(segmentNum);
RangedUri segmentUri = segmentIndex.getSegmentUrl(segmentNum);
DataSpec dataSpec = new DataSpec(segmentUri.getUri(), segmentUri.start, segmentUri.length,
representation.getCacheKey());
return new WebmMediaChunk(dataSource, dataSpec, representation.format, trigger, extractor,
startTimeUs, endTimeUs, 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 Uri uri;
public InitializationWebmLoadable(DataSource dataSource, DataSpec dataSpec, int trigger,
WebmExtractor extractor, Representation representation) {
super(dataSource, dataSpec, representation.format, trigger);
Format format, WebmExtractor extractor) {
super(dataSource, dataSpec, format, trigger);
this.extractor = extractor;
this.representation = representation;
this.uri = dataSpec.uri;
}
@Override
@ -228,22 +221,7 @@ public class DashWebmChunkSource implements ChunkSource {
if (!extractor.isPrepared()) {
throw new ParserException("Invalid initialization data");
}
validateCues(extractor.getCues());
}
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);
}
segmentIndexes.put(format.id, new DashWrappingSegmentIndex(extractor.getCues(), uri, 0));
}
}

View File

@ -18,6 +18,8 @@ package com.google.android.exoplayer.dash.mpd;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.util.ManifestFetcher;
import android.net.Uri;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
@ -57,9 +59,9 @@ public final class MediaPresentationDescriptionFetcher extends
@Override
protected MediaPresentationDescription parse(InputStream stream, String inputEncoding,
String contentId) throws IOException, ParserException {
String contentId, Uri baseUrl) throws IOException, ParserException {
try {
return parser.parseMediaPresentationDescription(stream, inputEncoding, contentId);
return parser.parseMediaPresentationDescription(stream, inputEncoding, contentId, baseUrl);
} catch (XmlPullParserException 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.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 android.net.Uri;
import android.util.Log;
import android.text.TextUtils;
import org.xml.sax.helpers.DefaultHandler;
import org.xmlpull.v1.XmlPullParser;
@ -38,15 +42,8 @@ import java.util.regex.Pattern;
/**
* 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 {
private static final String TAG = "MediaPresentationDescriptionParser";
// Note: Does not support the date part of ISO 8601
private static final Pattern DURATION =
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}.
*
* @param inputStream The stream from which to parse the manifest.
* @param inputEncoding The encoding of the input.
* @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.
* @throws IOException If a problem occurred reading from the stream.
* @throws XmlPullParserException If a problem occurred parsing the stream as xml.
* @throws ParserException If a problem occurred parsing the xml as a DASH mpd.
*/
public MediaPresentationDescription parseMediaPresentationDescription(InputStream inputStream,
String inputEncoding, String contentId) throws XmlPullParserException, IOException,
ParserException {
String inputEncoding, String contentId, Uri baseUrl) throws XmlPullParserException,
IOException, ParserException {
XmlPullParser xpp = xmlParserFactory.newPullParser();
xpp.setInput(inputStream, inputEncoding);
int eventType = xpp.next();
@ -82,123 +82,139 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
throw new ParserException(
"inputStream does not contain a valid media presentation description");
}
return parseMediaPresentationDescription(xpp, contentId);
return parseMediaPresentationDescription(xpp, contentId, baseUrl);
}
private MediaPresentationDescription parseMediaPresentationDescription(XmlPullParser xpp,
String contentId) throws XmlPullParserException, IOException {
long duration = parseDurationMs(xpp, "mediaPresentationDuration");
long minBufferTime = parseDurationMs(xpp, "minBufferTime");
String contentId, Uri baseUrl) throws XmlPullParserException, IOException {
long durationMs = parseDurationMs(xpp, "mediaPresentationDuration");
long minBufferTimeMs = parseDurationMs(xpp, "minBufferTime");
String typeString = xpp.getAttributeValue(null, "type");
boolean dynamic = (typeString != null) ? typeString.equals("dynamic") : false;
long minUpdateTime = (dynamic) ? parseDurationMs(xpp, "minimumUpdatePeriod", -1) : -1;
long minUpdateTimeMs = (dynamic) ? parseDurationMs(xpp, "minimumUpdatePeriod", -1) : -1;
List<Period> periods = new ArrayList<Period>();
do {
xpp.next();
if (isStartTag(xpp, "Period")) {
periods.add(parsePeriod(xpp, contentId, duration));
if (isStartTag(xpp, "BaseURL")) {
baseUrl = parseBaseUrl(xpp, baseUrl);
} else if (isStartTag(xpp, "Period")) {
periods.add(parsePeriod(xpp, contentId, baseUrl, durationMs));
}
} while (!isEndTag(xpp, "MPD"));
return new MediaPresentationDescription(duration, minBufferTime, dynamic, minUpdateTime,
return new MediaPresentationDescription(durationMs, minBufferTimeMs, dynamic, minUpdateTimeMs,
periods);
}
private Period parsePeriod(XmlPullParser xpp, String contentId, long mediaPresentationDuration)
private Period parsePeriod(XmlPullParser xpp, String contentId, Uri baseUrl, long mpdDurationMs)
throws XmlPullParserException, IOException {
int id = parseInt(xpp, "id");
long start = parseDurationMs(xpp, "start", 0);
long duration = parseDurationMs(xpp, "duration", mediaPresentationDuration);
String id = xpp.getAttributeValue(null, "id");
long startMs = parseDurationMs(xpp, "start", 0);
long durationMs = parseDurationMs(xpp, "duration", mpdDurationMs);
SegmentBase segmentBase = null;
List<AdaptationSet> adaptationSets = new ArrayList<AdaptationSet>();
List<Segment.Timeline> segmentTimelineList = null;
int segmentStartNumber = 0;
int segmentTimescale = 0;
long presentationTimeOffset = 0;
do {
xpp.next();
if (isStartTag(xpp, "AdaptationSet")) {
adaptationSets.add(parseAdaptationSet(xpp, contentId, start, duration,
segmentTimelineList));
if (isStartTag(xpp, "BaseURL")) {
baseUrl = parseBaseUrl(xpp, baseUrl);
} 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")) {
segmentStartNumber = parseInt(xpp, "startNumber");
segmentTimescale = parseInt(xpp, "timescale");
presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", 0);
segmentTimelineList = parsePeriodSegmentList(xpp, segmentStartNumber);
segmentBase = parseSegmentList(xpp, baseUrl, null, durationMs);
} else if (isStartTag(xpp, "SegmentTemplate")) {
segmentBase = parseSegmentTemplate(xpp, baseUrl, null, durationMs);
}
} while (!isEndTag(xpp, "Period"));
return new Period(id, start, duration, adaptationSets, segmentTimelineList,
segmentStartNumber, segmentTimescale, presentationTimeOffset);
return new Period(id, startMs, durationMs, adaptationSets);
}
private List<Segment.Timeline> parsePeriodSegmentList(
XmlPullParser xpp, long segmentStartNumber) throws XmlPullParserException, IOException {
List<Segment.Timeline> segmentTimelineList = new ArrayList<Segment.Timeline>();
// AdaptationSet parsing.
do {
xpp.next();
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)
private AdaptationSet parseAdaptationSet(XmlPullParser xpp, String contentId, Uri baseUrl,
long periodStartMs, long periodDurationMs, SegmentBase segmentBase)
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");
if (mimeType != null) {
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 contentType = parseAdaptationSetTypeFromMimeType(mimeType);
int id = -1;
List<ContentProtection> contentProtections = null;
List<Representation> representations = new ArrayList<Representation>();
do {
xpp.next();
if (contentType != AdaptationSet.TYPE_UNKNOWN) {
if (isStartTag(xpp, "ContentProtection")) {
if (contentProtections == null) {
contentProtections = new ArrayList<ContentProtection>();
}
contentProtections.add(parseContentProtection(xpp));
} else if (isStartTag(xpp, "ContentComponent")) {
id = Integer.parseInt(xpp.getAttributeValue(null, "id"));
String contentTypeString = xpp.getAttributeValue(null, "contentType");
contentType = "video".equals(contentTypeString) ? AdaptationSet.TYPE_VIDEO
: "audio".equals(contentTypeString) ? AdaptationSet.TYPE_AUDIO
: AdaptationSet.TYPE_UNKNOWN;
} else if (isStartTag(xpp, "Representation")) {
representations.add(parseRepresentation(xpp, contentId, periodStart, periodDuration,
mimeType, segmentTimelineList));
if (isStartTag(xpp, "BaseURL")) {
baseUrl = parseBaseUrl(xpp, baseUrl);
} else if (isStartTag(xpp, "ContentProtection")) {
if (contentProtections == null) {
contentProtections = new ArrayList<ContentProtection>();
}
contentProtections.add(parseContentProtection(xpp));
} else if (isStartTag(xpp, "ContentComponent")) {
id = Integer.parseInt(xpp.getAttributeValue(null, "id"));
contentType = checkAdaptationSetTypeConsistency(contentType,
parseAdaptationSetType(xpp.getAttributeValue(null, "contentType")));
} else if (isStartTag(xpp, "Representation")) {
Representation representation = parseRepresentation(xpp, contentId, baseUrl, periodStartMs,
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"));
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.
*
@ -211,99 +227,194 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
return new ContentProtection(schemeUriId, null);
}
private Representation parseRepresentation(XmlPullParser xpp, String contentId, long periodStart,
long periodDuration, String parentMimeType, List<Segment.Timeline> segmentTimelineList)
// Representation parsing.
private Representation parseRepresentation(XmlPullParser xpp, String contentId, Uri baseUrl,
long periodStartMs, long periodDurationMs, String mimeType, SegmentBase segmentBase)
throws XmlPullParserException, IOException {
int id;
try {
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;
String id = xpp.getAttributeValue(null, "id");
int bandwidth = parseInt(xpp, "bandwidth");
int audioSamplingRate = parseInt(xpp, "audioSamplingRate");
int width = parseInt(xpp, "width");
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;
List<Segment> segmentList = null;
do {
xpp.next();
if (isStartTag(xpp, "BaseURL")) {
xpp.next();
representationUrl = xpp.getText();
baseUrl = parseBaseUrl(xpp, baseUrl);
} else if (isStartTag(xpp, "AudioChannelConfiguration")) {
numChannels = Integer.parseInt(xpp.getAttributeValue(null, "value"));
} else if (isStartTag(xpp, "SegmentBase")) {
String[] indexRange = xpp.getAttributeValue(null, "indexRange").split("-");
indexStart = Long.parseLong(indexRange[0]);
indexEnd = Long.parseLong(indexRange[1]);
segmentBase = parseSegmentBase(xpp, baseUrl, (SingleSegmentBase) segmentBase);
} else if (isStartTag(xpp, "SegmentList")) {
segmentList = parseRepresentationSegmentList(xpp, segmentTimelineList);
} else if (isStartTag(xpp, "Initialization")) {
String[] indexRange = xpp.getAttributeValue(null, "range").split("-");
initializationStart = Long.parseLong(indexRange[0]);
initializationEnd = Long.parseLong(indexRange[1]);
segmentBase = parseSegmentList(xpp, baseUrl, (SegmentList) segmentBase, periodDurationMs);
} else if (isStartTag(xpp, "SegmentTemplate")) {
segmentBase = parseSegmentTemplate(xpp, baseUrl, (SegmentTemplate) segmentBase,
periodDurationMs);
}
} while (!isEndTag(xpp, "Representation"));
Uri uri = Uri.parse(representationUrl);
Format format = new Format(id, mimeType, width, height, numChannels, audioSamplingRate,
bandwidth);
if (segmentList == null) {
return new Representation(contentId, -1, format, uri, DataSpec.LENGTH_UNBOUNDED,
initializationStart, initializationEnd, indexStart, indexEnd, periodStart,
periodDuration);
} else {
return new SegmentedRepresentation(contentId, format, uri, initializationStart,
initializationEnd, indexStart, indexEnd, periodStart, periodDuration, segmentList);
}
return Representation.newInstance(periodStartMs, periodDurationMs, contentId, -1, format,
segmentBase);
}
private List<Segment> parseRepresentationSegmentList(XmlPullParser xpp,
List<Segment.Timeline> segmentTimelineList) throws XmlPullParserException, IOException {
List<Segment> segmentList = new ArrayList<Segment>();
int i = 0;
// SegmentBase, SegmentList and SegmentTemplate parsing.
private SingleSegmentBase parseSegmentBase(XmlPullParser xpp, Uri baseUrl,
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 {
xpp.next();
if (isStartTag(xpp, "Initialization")) {
String url = xpp.getAttributeValue(null, "sourceURL");
String[] indexRange = xpp.getAttributeValue(null, "range").split("-");
long initializationStart = Long.parseLong(indexRange[0]);
long initializationEnd = Long.parseLong(indexRange[1]);
segmentList.add(new Segment.Initialization(url, initializationStart, initializationEnd));
initialization = parseInitialization(xpp, baseUrl);
} else if (isStartTag(xpp, "SegmentTimeline")) {
timeline = parseSegmentTimeline(xpp);
} else if (isStartTag(xpp, "SegmentURL")) {
String url = xpp.getAttributeValue(null, "media");
String mediaRange = xpp.getAttributeValue(null, "mediaRange");
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));
if (segments == null) {
segments = new ArrayList<RangedUri>();
}
segments.add(parseSegmentUrl(xpp, baseUrl));
}
} 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 {
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());
}
protected static int parseInt(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) {
private static long parseDurationMs(XmlPullParser xpp, String name) {
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);
if (value != null) {
Matcher matcher = DURATION.matcher(value);
@ -350,4 +447,38 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
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 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<Segment.Timeline> segmentList;
public final int segmentStartNumber;
public final int segmentTimescale;
public final long presentationTimeOffset;
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) {
/**
* @param id The period identifier. May be null.
* @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.
* @param adaptationSets The adaptation sets belonging to the period.
*/
public Period(String id, long start, long duration, List<AdaptationSet> adaptationSets) {
this.id = id;
this.start = start;
this.duration = duration;
this.startMs = start;
this.durationMs = duration;
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;
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;
/**
* 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.
@ -33,7 +36,7 @@ public class Representation {
public final String contentId;
/**
* Identifies the revision of the {@link Representation}.
* Identifies the revision of the content.
* <p>
* 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
@ -43,45 +46,93 @@ public class Representation {
public final long revisionId;
/**
* The format in which the {@link Representation} is encoded.
* The format of the representation.
*/
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;
public final long periodDuration;
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) {
private Representation(long periodStartMs, long periodDurationMs, String contentId,
long revisionId, Format format, SegmentBase segmentBase) {
this.periodStartMs = periodStartMs;
this.periodDurationMs = periodDurationMs;
this.contentId = contentId;
this.revisionId = revisionId;
this.format = format;
this.contentLength = contentLength;
this.initializationStart = initializationStart;
this.initializationEnd = initializationEnd;
this.indexStart = indexStart;
this.indexEnd = indexEnd;
this.periodStart = periodStart;
this.periodDuration = periodDuration;
this.uri = uri;
initializationUri = segmentBase.getInitialization(this);
presentationTimeOffsetMs = (segmentBase.presentationTimeOffset * 1000) / segmentBase.timescale;
}
/**
* 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
* {@link #contentId}.{@link #format.id}.{@link #revisionId}.
* {@code contentId + "." + format.id + "." + revisionId}.
*
* @return A cache key.
*/
@ -89,4 +140,143 @@ public class Representation {
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 {
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_mdat = 0x6D646174;
public static final int TYPE_mfhd = 0x6D666864;

View File

@ -49,6 +49,15 @@ import java.util.UUID;
*/
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.
*/
@ -74,9 +83,13 @@ public final class FragmentedMp4Extractor {
* A sidx atom was read. The parsed data can be read using {@link #getSegmentIndex()}.
*/
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
| 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[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE =
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 {
HashSet<Integer> parsedAtoms = new HashSet<Integer>();
parsedAtoms.add(Atom.TYPE_avc1);
parsedAtoms.add(Atom.TYPE_avc3);
parsedAtoms.add(Atom.TYPE_esds);
parsedAtoms.add(Atom.TYPE_hdlr);
parsedAtoms.add(Atom.TYPE_mdat);
@ -140,7 +154,7 @@ public final class FragmentedMp4Extractor {
CONTAINER_TYPES = Collections.unmodifiableSet(atomContainerTypes);
}
private final boolean enableSmoothStreamingWorkarounds;
private final int workaroundFlags;
// Parser state
private final ParsableByteArray atomHeader;
@ -172,16 +186,15 @@ public final class FragmentedMp4Extractor {
private TrackFragment fragmentRun;
public FragmentedMp4Extractor() {
this(false);
this(0);
}
/**
* @param enableSmoothStreamingWorkarounds Set to true if this extractor will be used to parse
* SmoothStreaming streams. This will enable workarounds for SmoothStreaming violations of
* the ISO base media file format (ISO 14496-12). Set to false otherwise.
* @param workaroundFlags Flags to allow parsing of faulty streams.
* {@link #WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME} is currently the only flag defined.
*/
public FragmentedMp4Extractor(boolean enableSmoothStreamingWorkarounds) {
this.enableSmoothStreamingWorkarounds = enableSmoothStreamingWorkarounds;
public FragmentedMp4Extractor(int workaroundFlags) {
this.workaroundFlags = workaroundFlags;
parserState = STATE_READING_ATOM_HEADER;
atomHeader = new ParsableByteArray(ATOM_HEADER_SIZE);
containerAtoms = new Stack<ContainerAtom>();
@ -263,7 +276,8 @@ public final class FragmentedMp4Extractor {
* in subsequent calls until the whole sample has been 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.
* @throws ParserException If an error occurs parsing the media data.
*/
@ -466,7 +480,7 @@ public final class FragmentedMp4Extractor {
private void onMoofContainerAtomRead(ContainerAtom moof) {
fragmentRun = new TrackFragment();
parseMoof(track, extendsDefaults, moof, fragmentRun, enableSmoothStreamingWorkarounds);
parseMoof(track, extendsDefaults, moof, fragmentRun, workaroundFlags);
sampleIndex = 0;
lastSyncSampleIndex = 0;
pendingSeekSyncSampleIndex = 0;
@ -572,11 +586,12 @@ public final class FragmentedMp4Extractor {
int childStartPosition = stsd.getPosition();
int childAtomSize = stsd.readInt();
int childAtomType = stsd.readInt();
if (childAtomType == Atom.TYPE_avc1 || childAtomType == Atom.TYPE_encv) {
Pair<MediaFormat, TrackEncryptionBox> avc1 =
parseAvc1FromParent(stsd, childStartPosition, childAtomSize);
mediaFormat = avc1.first;
trackEncryptionBoxes[i] = avc1.second;
if (childAtomType == Atom.TYPE_avc1 || childAtomType == Atom.TYPE_avc3
|| childAtomType == Atom.TYPE_encv) {
Pair<MediaFormat, TrackEncryptionBox> avc =
parseAvcFromParent(stsd, childStartPosition, childAtomSize);
mediaFormat = avc.first;
trackEncryptionBoxes[i] = avc.second;
} else if (childAtomType == Atom.TYPE_mp4a || childAtomType == Atom.TYPE_enca) {
Pair<MediaFormat, TrackEncryptionBox> mp4a =
parseMp4aFromParent(stsd, childStartPosition, childAtomSize);
@ -588,7 +603,7 @@ public final class FragmentedMp4Extractor {
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) {
parent.setPosition(position + ATOM_HEADER_SIZE);
@ -695,7 +710,7 @@ public final class FragmentedMp4Extractor {
int childAtomSize = parent.readInt();
int childAtomType = parent.readInt();
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) {
parent.skip(4);
parent.readInt(); // schemeType. Expect cenc
@ -774,11 +789,11 @@ public final class FragmentedMp4Extractor {
}
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.
parseMfhd(moof.getLeafAtomOfType(Atom.TYPE_mfhd).getData());
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).
*/
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);
if (saiz != null) {
parseSaiz(saiz.getData(), out);
@ -809,8 +824,7 @@ public final class FragmentedMp4Extractor {
out.setSampleDescriptionIndex(fragmentHeader.sampleDescriptionIndex);
LeafAtom trun = traf.getLeafAtomOfType(Atom.TYPE_trun);
parseTrun(track, fragmentHeader, decodeTime, enableSmoothStreamingWorkarounds, trun.getData(),
out);
parseTrun(track, fragmentHeader, decodeTime, workaroundFlags, trun.getData(), out);
LeafAtom uuid = traf.getLeafAtomOfType(Atom.TYPE_uuid);
if (uuid != null) {
parseUuid(uuid.getData(), out);
@ -895,8 +909,7 @@ public final class FragmentedMp4Extractor {
* @param out The {@TrackFragment} into which parsed data should be placed.
*/
private static void parseTrun(Track track, DefaultSampleValues defaultSampleValues,
long decodeTime, boolean enableSmoothStreamingWorkarounds, ParsableByteArray trun,
TrackFragment out) {
long decodeTime, int workaroundFlags, ParsableByteArray trun, TrackFragment out) {
trun.setPosition(ATOM_HEADER_SIZE);
int fullAtom = trun.readInt();
int version = parseFullAtomVersion(fullAtom);
@ -926,6 +939,9 @@ public final class FragmentedMp4Extractor {
long timescale = track.timescale;
long cumulativeTime = decodeTime;
boolean workaroundEveryVideoFrameIsSyncFrame = track.type == Track.TYPE_VIDEO
&& ((workaroundFlags & WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME)
== WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME);
for (int i = 0; i < numberOfEntries; i++) {
// Use trun values if present, otherwise tfhd, otherwise trex.
int sampleDuration = sampleDurationsPresent ? trun.readUnsignedIntToInt()
@ -934,11 +950,14 @@ public final class FragmentedMp4Extractor {
int sampleFlags = (i == 0 && firstSampleFlagsPresent) ? firstSampleFlags
: sampleFlagsPresent ? trun.readInt() : defaultSampleValues.flags;
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;
if (version == 0 && !enableSmoothStreamingWorkarounds) {
sampleOffset = trun.readUnsignedIntToInt();
if (version == 0) {
// 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 {
sampleOffset = trun.readInt();
}
@ -947,9 +966,7 @@ public final class FragmentedMp4Extractor {
sampleDecodingTimeTable[i] = (int) ((cumulativeTime * 1000) / timescale);
sampleSizeTable[i] = sampleSize;
boolean isSync = ((sampleFlags >> 16) & 0x1) == 0;
if (track.type == Track.TYPE_VIDEO && enableSmoothStreamingWorkarounds && 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.
if (workaroundEveryVideoFrameIsSyncFrame && i != 0) {
isSync = false;
}
if (isSync) {
@ -1130,6 +1147,9 @@ public final class FragmentedMp4Extractor {
@SuppressLint("InlinedApi")
private int readSample(NonBlockingInputStream inputStream, SampleHolder out) {
if (out == null) {
return RESULT_NEED_SAMPLE_HOLDER;
}
int sampleSize = fragmentRun.sampleSizeTable[sampleIndex];
ByteBuffer outputData = out.data;
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;
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;
/**
* 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.
* 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>.
*/
public abstract class EbmlReader {
/* package */ interface EbmlReader {
// Element Types
protected static final int TYPE_UNKNOWN = 0; // Undefined element.
protected static final int TYPE_MASTER = 1; // Contains child elements.
protected static final int TYPE_UNSIGNED_INT = 2;
protected static final int TYPE_STRING = 3;
protected static final int TYPE_BINARY = 4;
protected static final int TYPE_FLOAT = 5;
/** Undefined element. */
public static final int TYPE_UNKNOWN = 0;
/** Contains child elements. */
public static final int TYPE_MASTER = 1;
/** Unsigned integer value of up to 8 bytes. */
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.
protected static final int RESULT_CONTINUE = 0;
protected static final int RESULT_NEED_MORE_DATA = 1;
protected static final int RESULT_END_OF_FILE = 2;
// Return values for reading methods.
public static final int READ_RESULT_CONTINUE = 0;
public static final int READ_RESULT_NEED_MORE_DATA = 1;
public static final int READ_RESULT_END_OF_FILE = 2;
// 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. {@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);
public void setEventHandler(EbmlEventHandler eventHandler);
/**
* Reads from a {@link NonBlockingInputStream} and calls event callbacks as needed.
*
* @param inputStream The input stream from which data should be read.
* @return One of the {@code RESULT_*} flags defined in this class.
* @param inputStream The input stream from which data should be read
* @return One of the {@code RESULT_*} flags defined in this interface
*/
protected final 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();
}
}
public int read(NonBlockingInputStream inputStream);
/**
* @return The total number of bytes consumed by the reader since first created
* or last {@link #reset()}.
* The total number of bytes consumed by the reader since first created or last {@link #reset()}.
*/
protected final long getBytesRead() {
return bytesRead;
}
public long getBytesRead();
/**
* 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() {
prepareForNextElement();
masterElementsStack.clear();
bytesRead = 0;
}
public void reset();
/**
* Reads, parses, and returns an EBML variable-length integer (varint) from the contents
* of a binary element.
*
* @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.
* @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
*/
protected final long readVarint(NonBlockingInputStream inputStream) {
varintBytesState = STATE_BEGIN_READING;
Assertions.checkState(readVarintBytes(inputStream) == RESULT_CONTINUE);
return parseTempByteArray(varintBytesLength, true);
}
public long readVarint(NonBlockingInputStream inputStream);
/**
* 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 byteBuffer The {@link ByteBuffer} to which data should be written.
* @param totalBytes The fixed number of bytes to be read and written.
* @param inputStream The input stream from which data should be read
* @param byteBuffer The {@link ByteBuffer} to which data should be written
* @param totalBytes The fixed number of bytes to be read and written
*/
protected final void readBytes(
NonBlockingInputStream inputStream, ByteBuffer byteBuffer, int totalBytes) {
bytesState = 0;
Assertions.checkState(readBytes(inputStream, byteBuffer, null, totalBytes) == RESULT_CONTINUE);
}
public void readBytes(NonBlockingInputStream inputStream, ByteBuffer byteBuffer, int totalBytes);
/**
* 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 byteArray The byte array to which data should be written.
* @param totalBytes The fixed number of bytes to be read and written.
* @param inputStream The input stream from which data should be read
* @param byteArray The byte array to which data should be written
* @param totalBytes The fixed number of bytes to be read and written
*/
protected final void readBytes(
NonBlockingInputStream inputStream, byte[] byteArray, int totalBytes) {
bytesState = 0;
Assertions.checkState(readBytes(inputStream, null, byteArray, totalBytes) == RESULT_CONTINUE);
}
public void readBytes(NonBlockingInputStream inputStream, byte[] byteArray, int totalBytes);
/**
* 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 totalBytes The fixed number of bytes to be skipped.
* @param inputStream The input stream from which data should be skipped
* @param totalBytes The fixed number of bytes to be skipped
*/
protected final 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;
}
}
public void skipBytes(NonBlockingInputStream inputStream, int totalBytes);
}

View File

@ -19,97 +19,22 @@ 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;
/**
* Facilitates the extraction of data from the WebM container format with a
* non-blocking, incremental parser based on {@link EbmlReader}.
* 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 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();
}
public interface WebmExtractor {
/**
* 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() {
return prepared;
}
public boolean isPrepared();
/**
* 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
* in subsequent calls until the whole sample has been read.
*
* @param inputStream The input stream from which data 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}.
* @param inputStream The input stream from which data 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
*/
public boolean read(NonBlockingInputStream inputStream, SampleHolder sampleHolder) {
tempSampleHolder = sampleHolder;
sampleRead = false;
super.read(inputStream);
tempSampleHolder = null;
return sampleRead;
}
public boolean read(NonBlockingInputStream inputStream, SampleHolder sampleHolder);
/**
* 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
* segment, is equal to or greater than the time of the current sample, and if there does not
* exist a sync frame between these two times.
* @return True if the operation resulted in a change of state. False if it was a no-op.
* exist a sync frame between these two times
* @return True if the operation resulted in a change of state. False if it was a no-op
*/
public boolean seekTo(long seekTimeUs, boolean allowNoop) {
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;
}
public boolean seekTo(long seekTimeUs, boolean allowNoop);
/**
* Returns the cues for the media stream.
*
* @return The cues in the form of a {@link SegmentIndex}, or null if the extractor is not yet
* prepared.
* prepared
*/
public SegmentIndex getCues() {
checkPrepared();
return cues;
}
public SegmentIndex getCues();
/**
* 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() {
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;
}
public MediaFormat getFormat();
}

View File

@ -63,7 +63,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
private final int maxHeight;
private final SparseArray<FragmentedMp4Extractor> extractors;
private final Format[] formats;
private final SmoothStreamingFormat[] formats;
/**
* @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;
formats = new Format[trackCount];
formats = new SmoothStreamingFormat[trackCount];
extractors = new SparseArray<FragmentedMp4Extractor>();
int maxWidth = 0;
int maxHeight = 0;
for (int i = 0; i < trackCount; i++) {
int trackIndex = trackIndices != null ? trackIndices[i] : i;
TrackElement trackElement = streamElement.tracks[trackIndex];
formats[i] = new Format(trackIndex, trackElement.mimeType, trackElement.maxWidth,
trackElement.maxHeight, trackElement.numChannels, trackElement.sampleRate,
trackElement.bitrate / 8);
formats[i] = new SmoothStreamingFormat(String.valueOf(trackIndex), trackElement.mimeType,
trackElement.maxWidth, trackElement.maxHeight, trackElement.numChannels,
trackElement.sampleRate, trackElement.bitrate, trackIndex);
maxWidth = Math.max(maxWidth, trackElement.maxWidth);
maxHeight = Math.max(maxHeight, trackElement.maxHeight);
MediaFormat mediaFormat = getMediaFormat(streamElement, trackIndex);
int trackType = streamElement.type == StreamElement.TYPE_VIDEO ? Track.TYPE_VIDEO
: 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,
trackEncryptionBoxes));
if (protectionElement != null) {
@ -141,7 +142,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
}
@Override
public void disable(List<MediaChunk> queue) {
public void disable(List<? extends MediaChunk> queue) {
// Do nothing.
}
@ -155,14 +156,14 @@ public class SmoothStreamingChunkSource implements ChunkSource {
long playbackPositionUs, ChunkOperationHolder out) {
evaluation.queueSize = queue.size();
formatEvaluator.evaluate(queue, playbackPositionUs, formats, evaluation);
Format selectedFormat = evaluation.format;
SmoothStreamingFormat selectedFormat = (SmoothStreamingFormat) evaluation.format;
out.queueSize = evaluation.queueSize;
if (selectedFormat == null) {
out.chunk = null;
return;
} else if (out.queueSize == queue.size() && out.chunk != null
&& out.chunk.format.id == 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
// of the queue. Do nothing.
return;
@ -181,11 +182,12 @@ public class SmoothStreamingChunkSource implements ChunkSource {
}
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);
Chunk mediaChunk = newMediaChunk(selectedFormat, uri, null,
extractors.get(selectedFormat.id), dataSource, nextChunkIndex, isLastChunk,
streamElement.getStartTimeUs(nextChunkIndex),
extractors.get(Integer.parseInt(selectedFormat.id)), dataSource, nextChunkIndex,
isLastChunk, streamElement.getStartTimeUs(nextChunkIndex),
isLastChunk ? -1 : streamElement.getStartTimeUs(nextChunkIndex + 1), 0);
out.chunk = mediaChunk;
}
@ -195,6 +197,11 @@ public class SmoothStreamingChunkSource implements ChunkSource {
return null;
}
@Override
public void onChunkLoadError(Chunk chunk, Exception e) {
// Do nothing.
}
private static MediaFormat getMediaFormat(StreamElement streamElement, int trackIndex) {
TrackElement trackElement = streamElement.tracks[trackIndex];
String mimeType = trackElement.mimeType;
@ -228,8 +235,8 @@ public class SmoothStreamingChunkSource implements ChunkSource {
DataSpec dataSpec = new DataSpec(uri, offset, -1, cacheKey);
// 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.
return new Mp4MediaChunk(dataSource, dataSpec, formatInfo, trigger, extractor,
chunkStartTimeUs, nextStartTimeUs, -chunkStartTimeUs, nextChunkIndex);
return new Mp4MediaChunk(dataSource, dataSpec, formatInfo, trigger, chunkStartTimeUs,
nextStartTimeUs, nextChunkIndex, extractor, false, -chunkStartTimeUs);
}
private static byte[] getKeyId(byte[] initData) {
@ -254,4 +261,16 @@ public class SmoothStreamingChunkSource implements ChunkSource {
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;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.Util;
import java.util.Arrays;
import java.util.UUID;
/**
@ -195,9 +195,7 @@ public class SmoothStreamingManifest {
* @return The index of the corresponding chunk.
*/
public int getChunkIndex(long timeUs) {
long time = (timeUs * timeScale) / 1000000L;
int chunkIndex = Arrays.binarySearch(chunkStartTimes, time);
return chunkIndex < 0 ? -chunkIndex - 2 : chunkIndex;
return Util.binarySearchFloor(chunkStartTimes, (timeUs * timeScale) / 1000000L, true, true);
}
/**

View File

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

View File

@ -16,8 +16,7 @@
package com.google.android.exoplayer.text.ttml;
import com.google.android.exoplayer.text.Subtitle;
import java.util.Arrays;
import com.google.android.exoplayer.util.Util;
/**
* A representation of a TTML subtitle.
@ -41,8 +40,7 @@ public final class TtmlSubtitle implements Subtitle {
@Override
public int getNextEventTimeIndex(long timeUs) {
int index = Arrays.binarySearch(eventTimesUs, timeUs - startTimeUs);
index = index >= 0 ? index + 1 : ~index;
int index = Util.binarySearchCeil(eventTimesUs, timeUs - startTimeUs, false, false);
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,
ReadHead readHead, int readLength) {
if (readHead.position == dataSpec.length) {
if (isEndOfStream()) {
return -1;
}
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;
slidingPercentile.addSample(computeWeight(accumulator), bytesPerSecond);
float bandwidthEstimateFloat = slidingPercentile.getPercentile(0.5f);
bandwidthEstimate = bandwidthEstimateFloat == Float.NaN
? NO_ESTIMATE : (long) bandwidthEstimateFloat;
bandwidthEstimate = Float.isNaN(bandwidthEstimateFloat) ? NO_ESTIMATE
: (long) bandwidthEstimateFloat;
notifyBandwidthSample(elapsedMs, accumulator, bandwidthEstimate);
}
streamCount--;

View File

@ -134,9 +134,8 @@ public interface Cache {
* @param key The key 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.
* @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
@ -173,4 +172,14 @@ public interface Cache {
*/
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)
throws InterruptedException {
CacheSpan lookupSpan = CacheSpan.createLookup(key, position);
// Wait until no-one holds a lock for the key.
while (lockedSpans.containsKey(key)) {
wait();
while (true) {
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();
}
}
return getSpanningRegion(key, lookupSpan);
}
@Override
public synchronized CacheSpan startReadWriteNonBlocking(String key, long position)
throws InterruptedException {
CacheSpan lookupSpan = CacheSpan.createLookup(key, position);
// Return null if key is locked
if (lockedSpans.containsKey(key)) {
return null;
}
return getSpanningRegion(key, lookupSpan);
public synchronized CacheSpan startReadWriteNonBlocking(String key, long position) {
return startReadWriteNonBlocking(CacheSpan.createLookup(key, position));
}
private CacheSpan getSpanningRegion(String key, CacheSpan lookupSpan) {
private synchronized CacheSpan startReadWriteNonBlocking(CacheSpan lookupSpan) {
CacheSpan spanningRegion = getSpan(lookupSpan);
// Read case.
if (spanningRegion.isCached) {
CacheSpan oldCacheSpan = spanningRegion;
// Remove the old span from the in-memory representation.
@ -139,10 +142,17 @@ public class SimpleCache implements Cache {
// Add the updated span back into the in-memory representation.
spansForKey.add(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
@ -173,6 +183,7 @@ public class SimpleCache implements Cache {
return;
}
addSpan(span);
notifyAll();
}
@Override
@ -330,4 +341,41 @@ public class SimpleCache implements Cache {
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 android.net.Uri;
import android.os.AsyncTask;
import java.io.IOException;
@ -84,14 +85,15 @@ public abstract class ManifestFetcher<T> extends AsyncTask<String, Void, T> {
protected final T doInBackground(String... data) {
try {
contentId = data.length > 1 ? data[1] : null;
URL url = new URL(data[0]);
String urlString = data[0];
String inputEncoding = null;
InputStream inputStream = null;
try {
HttpURLConnection connection = configureHttpConnection(url);
Uri baseUrl = Util.parseBaseUri(urlString);
HttpURLConnection connection = configureHttpConnection(new URL(urlString));
inputStream = connection.getInputStream();
inputEncoding = connection.getContentEncoding();
return parse(inputStream, inputEncoding, contentId);
return parse(inputStream, inputEncoding, contentId, baseUrl);
} finally {
if (inputStream != null) {
inputStream.close();
@ -119,11 +121,13 @@ public abstract class ManifestFetcher<T> extends AsyncTask<String, Void, T> {
* @param stream The input stream to read.
* @param inputEncoding The encoding of the input stream.
* @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 ParserException If an error occurred parsing the loaded data.
*/
protected abstract T parse(InputStream stream, String inputEncoding, String contentId) throws
IOException, ParserException;
protected abstract T parse(InputStream stream, String inputEncoding, String contentId,
Uri baseUrl) throws IOException, ParserException;
private HttpURLConnection configureHttpConnection(URL url) throws IOException {
HttpURLConnection connection = (HttpURLConnection) url.openConnection();

View File

@ -20,17 +20,39 @@ package com.google.android.exoplayer.util;
*/
public class MimeTypes {
public static final String VIDEO_MP4 = "video/mp4";
public static final String VIDEO_WEBM = "video/webm";
public static final String VIDEO_H264 = "video/avc";
public static final String VIDEO_VP9 = "video/x-vnd.on2.vp9";
public static final String AUDIO_MP4 = "audio/mp4";
public static final String AUDIO_AAC = "audio/mp4a-latm";
public static final String TEXT_VTT = "text/vtt";
public static final String APPLICATION_TTML = "application/ttml+xml";
public static final String BASE_TYPE_VIDEO = "video";
public static final String BASE_TYPE_AUDIO = "audio";
public static final String BASE_TYPE_TEXT = "text";
public static final String BASE_TYPE_APPLICATION = "application";
public static final String VIDEO_MP4 = BASE_TYPE_VIDEO + "/mp4";
public static final String VIDEO_WEBM = BASE_TYPE_VIDEO + "/webm";
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() {}
/**
* 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.
*
@ -38,7 +60,7 @@ public class MimeTypes {
* @return Whether the top level type is audio.
*/
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.
*/
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.
*/
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 android.net.Uri;
import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@ -112,4 +117,99 @@ public final class Util {
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;
}
}