commit
4228f2cfa3
10
README.md
10
README.md
@ -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
|
||||
|
@ -18,7 +18,7 @@ android {
|
||||
buildToolsVersion "19.1"
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 9
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 19
|
||||
}
|
||||
buildTypes {
|
||||
|
@ -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);
|
||||
|
@ -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) + "]");
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
|
@ -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++) {
|
||||
|
@ -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,
|
||||
|
@ -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());
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
|
@ -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.
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
|
||||
}
|
@ -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);
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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--;
|
||||
|
@ -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);
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user