Commit 4228f2cf by ojw28

Merge pull request #15 from google/dev

Merge 1.0.11 to master
parents 273fad84 1ed65dfb
Showing with 1048 additions and 353 deletions
...@@ -26,6 +26,16 @@ get started. ...@@ -26,6 +26,16 @@ get started.
[Class reference]: http://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer/package-summary.html [Class reference]: http://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer/package-summary.html
## Project branches ##
* The [master][] branch holds the most recent minor release.
* Most development work happens on the [dev][] branch.
* Additional development branches may be established for major features.
[master]: https://github.com/google/ExoPlayer/tree/master
[dev]: https://github.com/google/ExoPlayer/tree/dev
## Using Eclipse ## ## Using Eclipse ##
The repository includes Eclipse projects for both the ExoPlayer library and its The repository includes Eclipse projects for both the ExoPlayer library and its
......
...@@ -18,7 +18,7 @@ android { ...@@ -18,7 +18,7 @@ android {
buildToolsVersion "19.1" buildToolsVersion "19.1"
defaultConfig { defaultConfig {
minSdkVersion 9 minSdkVersion 16
targetSdkVersion 19 targetSdkVersion 19
} }
buildTypes { buildTypes {
......
...@@ -52,7 +52,7 @@ public class SampleChooserActivity extends Activity { ...@@ -52,7 +52,7 @@ public class SampleChooserActivity extends Activity {
sampleAdapter.addAll((Object[]) Samples.SIMPLE); sampleAdapter.addAll((Object[]) Samples.SIMPLE);
sampleAdapter.add(new Header("YouTube DASH")); sampleAdapter.add(new Header("YouTube DASH"));
sampleAdapter.addAll((Object[]) Samples.YOUTUBE_DASH_MP4); sampleAdapter.addAll((Object[]) Samples.YOUTUBE_DASH_MP4);
sampleAdapter.add(new Header("Widevine DASH GTS")); sampleAdapter.add(new Header("Widevine GTS DASH"));
sampleAdapter.addAll((Object[]) Samples.WIDEVINE_GTS); sampleAdapter.addAll((Object[]) Samples.WIDEVINE_GTS);
sampleAdapter.add(new Header("SmoothStreaming")); sampleAdapter.add(new Header("SmoothStreaming"));
sampleAdapter.addAll((Object[]) Samples.SMOOTHSTREAMING); sampleAdapter.addAll((Object[]) Samples.SMOOTHSTREAMING);
......
...@@ -91,7 +91,7 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener ...@@ -91,7 +91,7 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener
} }
@Override @Override
public void onLoadStarted(int sourceId, int formatId, int trigger, boolean isInitialization, public void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization,
int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes) { int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes) {
loadStartTimeMs[sourceId] = SystemClock.elapsedRealtime(); loadStartTimeMs[sourceId] = SystemClock.elapsedRealtime();
if (VerboseLogUtil.isTagEnabled(TAG)) { if (VerboseLogUtil.isTagEnabled(TAG)) {
...@@ -110,13 +110,13 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener ...@@ -110,13 +110,13 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener
} }
@Override @Override
public void onVideoFormatEnabled(int formatId, int trigger, int mediaTimeMs) { public void onVideoFormatEnabled(String formatId, int trigger, int mediaTimeMs) {
Log.d(TAG, "videoFormat [" + getSessionTimeString() + ", " + formatId + ", " + Log.d(TAG, "videoFormat [" + getSessionTimeString() + ", " + formatId + ", " +
Integer.toString(trigger) + "]"); Integer.toString(trigger) + "]");
} }
@Override @Override
public void onAudioFormatEnabled(int formatId, int trigger, int mediaTimeMs) { public void onAudioFormatEnabled(String formatId, int trigger, int mediaTimeMs) {
Log.d(TAG, "audioFormat [" + getSessionTimeString() + ", " + formatId + ", " + Log.d(TAG, "audioFormat [" + getSessionTimeString() + ", " + formatId + ", " +
Integer.toString(trigger) + "]"); Integer.toString(trigger) + "]");
} }
......
...@@ -160,8 +160,7 @@ public class DashVodRendererBuilder implements RendererBuilder, ...@@ -160,8 +160,7 @@ public class DashVodRendererBuilder implements RendererBuilder,
} }
// Build the video renderer. // Build the video renderer.
DataSource videoDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES, DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
bandwidthMeter);
ChunkSource videoChunkSource; ChunkSource videoChunkSource;
String mimeType = videoRepresentations[0].format.mimeType; String mimeType = videoRepresentations[0].format.mimeType;
if (mimeType.equals(MimeTypes.VIDEO_MP4)) { if (mimeType.equals(MimeTypes.VIDEO_MP4)) {
...@@ -192,8 +191,7 @@ public class DashVodRendererBuilder implements RendererBuilder, ...@@ -192,8 +191,7 @@ public class DashVodRendererBuilder implements RendererBuilder,
audioChunkSource = null; audioChunkSource = null;
audioRenderer = null; audioRenderer = null;
} else { } else {
DataSource audioDataSource = new HttpDataSource(userAgent, DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
HttpDataSource.REJECT_PAYWALL_TYPES, bandwidthMeter);
audioTrackNames = new String[audioRepresentationsList.size()]; audioTrackNames = new String[audioRepresentationsList.size()];
ChunkSource[] audioChunkSources = new ChunkSource[audioRepresentationsList.size()]; ChunkSource[] audioChunkSources = new ChunkSource[audioRepresentationsList.size()];
FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator(); FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator();
......
...@@ -118,11 +118,11 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -118,11 +118,11 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
* A listener for debugging information. * A listener for debugging information.
*/ */
public interface InfoListener { public interface InfoListener {
void onVideoFormatEnabled(int formatId, int trigger, int mediaTimeMs); void onVideoFormatEnabled(String formatId, int trigger, int mediaTimeMs);
void onAudioFormatEnabled(int formatId, int trigger, int mediaTimeMs); void onAudioFormatEnabled(String formatId, int trigger, int mediaTimeMs);
void onDroppedFrames(int count, long elapsed); void onDroppedFrames(int count, long elapsed);
void onBandwidthSample(int elapsedMs, long bytes, long bandwidthEstimate); void onBandwidthSample(int elapsedMs, long bytes, long bandwidthEstimate);
void onLoadStarted(int sourceId, int formatId, int trigger, boolean isInitialization, void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization,
int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes); int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes);
void onLoadCompleted(int sourceId); void onLoadCompleted(int sourceId);
} }
...@@ -398,7 +398,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -398,7 +398,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
} }
@Override @Override
public void onDownstreamFormatChanged(int sourceId, int formatId, int trigger, int mediaTimeMs) { public void onDownstreamFormatChanged(int sourceId, String formatId, int trigger,
int mediaTimeMs) {
if (infoListener == null) { if (infoListener == null) {
return; return;
} }
...@@ -469,7 +470,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -469,7 +470,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
} }
@Override @Override
public void onLoadStarted(int sourceId, int formatId, int trigger, boolean isInitialization, public void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization,
int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes) { int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes) {
if (infoListener != null) { if (infoListener != null) {
infoListener.onLoadStarted(sourceId, formatId, trigger, isInitialization, mediaStartTimeMs, infoListener.onLoadStarted(sourceId, formatId, trigger, isInitialization, mediaStartTimeMs,
......
...@@ -150,8 +150,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, ...@@ -150,8 +150,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
} }
// Build the video renderer. // Build the video renderer.
DataSource videoDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES, DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
bandwidthMeter);
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(url, manifest, ChunkSource videoChunkSource = new SmoothStreamingChunkSource(url, manifest,
videoStreamElementIndex, videoTrackIndices, videoDataSource, videoStreamElementIndex, videoTrackIndices, videoDataSource,
new AdaptiveEvaluator(bandwidthMeter)); new AdaptiveEvaluator(bandwidthMeter));
...@@ -173,8 +172,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, ...@@ -173,8 +172,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
} else { } else {
audioTrackNames = new String[audioStreamElementCount]; audioTrackNames = new String[audioStreamElementCount];
ChunkSource[] audioChunkSources = new ChunkSource[audioStreamElementCount]; ChunkSource[] audioChunkSources = new ChunkSource[audioStreamElementCount];
DataSource audioDataSource = new HttpDataSource(userAgent, DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
HttpDataSource.REJECT_PAYWALL_TYPES, bandwidthMeter);
FormatEvaluator audioFormatEvaluator = new FormatEvaluator.FixedEvaluator(); FormatEvaluator audioFormatEvaluator = new FormatEvaluator.FixedEvaluator();
audioStreamElementCount = 0; audioStreamElementCount = 0;
for (int i = 0; i < manifest.streamElements.length; i++) { for (int i = 0; i < manifest.streamElements.length; i++) {
...@@ -204,8 +202,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, ...@@ -204,8 +202,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
} else { } else {
textTrackNames = new String[textStreamElementCount]; textTrackNames = new String[textStreamElementCount];
ChunkSource[] textChunkSources = new ChunkSource[textStreamElementCount]; ChunkSource[] textChunkSources = new ChunkSource[textStreamElementCount];
DataSource ttmlDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES, DataSource ttmlDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
bandwidthMeter);
FormatEvaluator ttmlFormatEvaluator = new FormatEvaluator.FixedEvaluator(); FormatEvaluator ttmlFormatEvaluator = new FormatEvaluator.FixedEvaluator();
textStreamElementCount = 0; textStreamElementCount = 0;
for (int i = 0; i < manifest.streamElements.length; i++) { for (int i = 0; i < manifest.streamElements.length; i++) {
......
...@@ -115,8 +115,7 @@ import java.util.ArrayList; ...@@ -115,8 +115,7 @@ import java.util.ArrayList;
videoRepresentationsList.toArray(videoRepresentations); videoRepresentationsList.toArray(videoRepresentations);
// Build the video renderer. // Build the video renderer.
DataSource videoDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES, DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
bandwidthMeter);
ChunkSource videoChunkSource = new DashMp4ChunkSource(videoDataSource, ChunkSource videoChunkSource = new DashMp4ChunkSource(videoDataSource,
new AdaptiveEvaluator(bandwidthMeter), videoRepresentations); new AdaptiveEvaluator(bandwidthMeter), videoRepresentations);
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
...@@ -125,8 +124,7 @@ import java.util.ArrayList; ...@@ -125,8 +124,7 @@ import java.util.ArrayList;
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50); MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50);
// Build the audio renderer. // Build the audio renderer.
DataSource audioDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES, DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
bandwidthMeter);
ChunkSource audioChunkSource = new DashMp4ChunkSource(audioDataSource, ChunkSource audioChunkSource = new DashMp4ChunkSource(audioDataSource,
new FormatEvaluator.FixedEvaluator(), audioRepresentation); new FormatEvaluator.FixedEvaluator(), audioRepresentation);
SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
......
...@@ -115,8 +115,7 @@ import java.util.ArrayList; ...@@ -115,8 +115,7 @@ import java.util.ArrayList;
} }
// Build the video renderer. // Build the video renderer.
DataSource videoDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES, DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
bandwidthMeter);
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(url, manifest, ChunkSource videoChunkSource = new SmoothStreamingChunkSource(url, manifest,
videoStreamElementIndex, videoTrackIndices, videoDataSource, videoStreamElementIndex, videoTrackIndices, videoDataSource,
new AdaptiveEvaluator(bandwidthMeter)); new AdaptiveEvaluator(bandwidthMeter));
...@@ -126,8 +125,7 @@ import java.util.ArrayList; ...@@ -126,8 +125,7 @@ import java.util.ArrayList;
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50); MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50);
// Build the audio renderer. // Build the audio renderer.
DataSource audioDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES, DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
bandwidthMeter);
ChunkSource audioChunkSource = new SmoothStreamingChunkSource(url, manifest, ChunkSource audioChunkSource = new SmoothStreamingChunkSource(url, manifest,
audioStreamElementIndex, new int[] {0}, audioDataSource, audioStreamElementIndex, new int[] {0}, audioDataSource,
new FormatEvaluator.FixedEvaluator()); new FormatEvaluator.FixedEvaluator());
......
...@@ -26,7 +26,7 @@ public class ExoPlayerLibraryInfo { ...@@ -26,7 +26,7 @@ public class ExoPlayerLibraryInfo {
/** /**
* The version of the library, expressed as a string. * The version of the library, expressed as a string.
*/ */
public static final String VERSION = "1.0.10"; public static final String VERSION = "1.0.11";
/** /**
* The version of the library, expressed as an integer. * The version of the library, expressed as an integer.
......
...@@ -95,6 +95,10 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { ...@@ -95,6 +95,10 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000; private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000;
private static final int MIN_TIMESTAMP_SAMPLE_INTERVAL_US = 500000; private static final int MIN_TIMESTAMP_SAMPLE_INTERVAL_US = 500000;
private static final int START_NOT_SET = 0;
private static final int START_IN_SYNC = 1;
private static final int START_NEED_SYNC = 2;
private final EventListener eventListener; private final EventListener eventListener;
private final ConditionVariable audioTrackReleasingConditionVariable; private final ConditionVariable audioTrackReleasingConditionVariable;
private final AudioTimestampCompat audioTimestampCompat; private final AudioTimestampCompat audioTimestampCompat;
...@@ -119,7 +123,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { ...@@ -119,7 +123,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
private Method audioTrackGetLatencyMethod; private Method audioTrackGetLatencyMethod;
private int audioSessionId; private int audioSessionId;
private long submittedBytes; private long submittedBytes;
private boolean audioTrackStartMediaTimeSet; private int audioTrackStartMediaTimeState;
private long audioTrackStartMediaTimeUs; private long audioTrackStartMediaTimeUs;
private long audioTrackResumeSystemTimeUs; private long audioTrackResumeSystemTimeUs;
private long lastReportedCurrentPositionUs; private long lastReportedCurrentPositionUs;
...@@ -363,7 +367,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { ...@@ -363,7 +367,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
lastRawPlaybackHeadPosition = 0; lastRawPlaybackHeadPosition = 0;
rawPlaybackHeadWrapCount = 0; rawPlaybackHeadWrapCount = 0;
audioTrackStartMediaTimeUs = 0; audioTrackStartMediaTimeUs = 0;
audioTrackStartMediaTimeSet = false; audioTrackStartMediaTimeState = START_NOT_SET;
resetSyncParams(); resetSyncParams();
int playState = audioTrack.getPlayState(); int playState = audioTrack.getPlayState();
if (playState == AudioTrack.PLAYSTATE_PLAYING) { if (playState == AudioTrack.PLAYSTATE_PLAYING) {
...@@ -429,7 +433,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { ...@@ -429,7 +433,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
protected long getCurrentPositionUs() { protected long getCurrentPositionUs() {
long systemClockUs = System.nanoTime() / 1000; long systemClockUs = System.nanoTime() / 1000;
long currentPositionUs; long currentPositionUs;
if (audioTrack == null || !audioTrackStartMediaTimeSet) { if (audioTrack == null || audioTrackStartMediaTimeState == START_NOT_SET) {
// The AudioTrack hasn't started. // The AudioTrack hasn't started.
currentPositionUs = super.getCurrentPositionUs(); currentPositionUs = super.getCurrentPositionUs();
} else if (audioTimestampSet) { } else if (audioTimestampSet) {
...@@ -461,7 +465,8 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { ...@@ -461,7 +465,8 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
} }
private void maybeSampleSyncParams() { private void maybeSampleSyncParams() {
if (audioTrack == null || !audioTrackStartMediaTimeSet || getState() != STATE_STARTED) { if (audioTrack == null || audioTrackStartMediaTimeState == START_NOT_SET
|| getState() != STATE_STARTED) {
// The AudioTrack isn't playing. // The AudioTrack isn't playing.
return; return;
} }
...@@ -549,25 +554,40 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { ...@@ -549,25 +554,40 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
@Override @Override
protected boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer, protected boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer,
MediaCodec.BufferInfo bufferInfo, int bufferIndex) throws ExoPlaybackException { MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip)
throws ExoPlaybackException {
if (shouldSkip) {
codec.releaseOutputBuffer(bufferIndex, false);
codecCounters.skippedOutputBufferCount++;
if (audioTrackStartMediaTimeState == START_IN_SYNC) {
// Skipping the sample will push track time out of sync. We'll need to sync again.
audioTrackStartMediaTimeState = START_NEED_SYNC;
}
return true;
}
if (temporaryBufferSize == 0) { if (temporaryBufferSize == 0) {
// This is the first time we've seen this {@code buffer}. // This is the first time we've seen this {@code buffer}.
// Note: presentationTimeUs corresponds to the end of the sample, not the start. // Note: presentationTimeUs corresponds to the end of the sample, not the start.
long bufferStartTime = bufferInfo.presentationTimeUs - long bufferStartTime = bufferInfo.presentationTimeUs -
framesToDurationUs(bufferInfo.size / frameSize); framesToDurationUs(bufferInfo.size / frameSize);
if (!audioTrackStartMediaTimeSet) { if (audioTrackStartMediaTimeState == START_NOT_SET) {
audioTrackStartMediaTimeUs = Math.max(0, bufferStartTime); audioTrackStartMediaTimeUs = Math.max(0, bufferStartTime);
audioTrackStartMediaTimeSet = true; audioTrackStartMediaTimeState = START_IN_SYNC;
} else { } else {
// Sanity check that bufferStartTime is consistent with the expected value. // Sanity check that bufferStartTime is consistent with the expected value.
long expectedBufferStartTime = audioTrackStartMediaTimeUs + long expectedBufferStartTime = audioTrackStartMediaTimeUs +
framesToDurationUs(submittedBytes / frameSize); framesToDurationUs(submittedBytes / frameSize);
if (Math.abs(expectedBufferStartTime - bufferStartTime) > 200000) { if (audioTrackStartMediaTimeState == START_IN_SYNC
&& Math.abs(expectedBufferStartTime - bufferStartTime) > 200000) {
Log.e(TAG, "Discontinuity detected [expected " + expectedBufferStartTime + ", got " + Log.e(TAG, "Discontinuity detected [expected " + expectedBufferStartTime + ", got " +
bufferStartTime + "]"); bufferStartTime + "]");
// Adjust audioTrackStartMediaTimeUs to compensate for the discontinuity. Also reset audioTrackStartMediaTimeState = START_NEED_SYNC;
// lastReportedCurrentPositionUs to allow time to jump backwards if it really wants to. }
if (audioTrackStartMediaTimeState == START_NEED_SYNC) {
// Adjust audioTrackStartMediaTimeUs to be consistent with the current buffer's start
// time and the number of bytes submitted. Also reset lastReportedCurrentPositionUs to
// allow time to jump backwards if it really wants to.
audioTrackStartMediaTimeUs += (bufferStartTime - expectedBufferStartTime); audioTrackStartMediaTimeUs += (bufferStartTime - expectedBufferStartTime);
lastReportedCurrentPositionUs = 0; lastReportedCurrentPositionUs = 0;
} }
......
...@@ -391,7 +391,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -391,7 +391,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
while (result == SampleSource.SAMPLE_READ && currentPositionUs <= timeUs) { while (result == SampleSource.SAMPLE_READ && currentPositionUs <= timeUs) {
result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false); result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false);
if (result == SampleSource.SAMPLE_READ) { if (result == SampleSource.SAMPLE_READ) {
currentPositionUs = sampleHolder.timeUs; if (!sampleHolder.decodeOnly) {
currentPositionUs = sampleHolder.timeUs;
}
codecCounters.discardedSamplesCount++; codecCounters.discardedSamplesCount++;
} else if (result == SampleSource.FORMAT_READ) { } else if (result == SampleSource.FORMAT_READ) {
onInputFormatChanged(formatHolder); onInputFormatChanged(formatHolder);
...@@ -677,15 +679,15 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -677,15 +679,15 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
return false; return false;
} }
if (decodeOnlyPresentationTimestamps.remove(outputBufferInfo.presentationTimeUs)) { boolean decodeOnly = decodeOnlyPresentationTimestamps.contains(
codec.releaseOutputBuffer(outputIndex, false); outputBufferInfo.presentationTimeUs);
outputIndex = -1;
return true;
}
if (processOutputBuffer(timeUs, codec, outputBuffers[outputIndex], outputBufferInfo, if (processOutputBuffer(timeUs, codec, outputBuffers[outputIndex], outputBufferInfo,
outputIndex)) { outputIndex, decodeOnly)) {
currentPositionUs = outputBufferInfo.presentationTimeUs; if (decodeOnly) {
decodeOnlyPresentationTimestamps.remove(outputBufferInfo.presentationTimeUs);
} else {
currentPositionUs = outputBufferInfo.presentationTimeUs;
}
outputIndex = -1; outputIndex = -1;
return true; return true;
} }
...@@ -701,7 +703,8 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { ...@@ -701,7 +703,8 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
* @throws ExoPlaybackException If an error occurs processing the output buffer. * @throws ExoPlaybackException If an error occurs processing the output buffer.
*/ */
protected abstract boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer, protected abstract boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer,
MediaCodec.BufferInfo bufferInfo, int bufferIndex) throws ExoPlaybackException; MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip)
throws ExoPlaybackException;
/** /**
* Returns the name of the secure variant of a given decoder. * Returns the name of the secure variant of a given decoder.
......
...@@ -29,7 +29,7 @@ import android.view.Surface; ...@@ -29,7 +29,7 @@ import android.view.Surface;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
/** /**
* Decodes and renders video using {@MediaCodec}. * Decodes and renders video using {@link MediaCodec}.
*/ */
@TargetApi(16) @TargetApi(16)
public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
...@@ -338,7 +338,12 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { ...@@ -338,7 +338,12 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
@Override @Override
protected boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer, protected boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer,
MediaCodec.BufferInfo bufferInfo, int bufferIndex) { MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip) {
if (shouldSkip) {
skipOutputBuffer(codec, bufferIndex);
return true;
}
long earlyUs = bufferInfo.presentationTimeUs - timeUs; long earlyUs = bufferInfo.presentationTimeUs - timeUs;
if (earlyUs < -30000) { if (earlyUs < -30000) {
// We're more than 30ms late rendering the frame. // We're more than 30ms late rendering the frame.
...@@ -371,6 +376,13 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { ...@@ -371,6 +376,13 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
return false; return false;
} }
private void skipOutputBuffer(MediaCodec codec, int bufferIndex) {
TraceUtil.beginSection("skipVideoBuffer");
codec.releaseOutputBuffer(bufferIndex, false);
TraceUtil.endSection();
codecCounters.skippedOutputBufferCount++;
}
private void dropOutputBuffer(MediaCodec codec, int bufferIndex) { private void dropOutputBuffer(MediaCodec codec, int bufferIndex) {
TraceUtil.beginSection("dropVideoBuffer"); TraceUtil.beginSection("dropVideoBuffer");
codec.releaseOutputBuffer(bufferIndex, false); codec.releaseOutputBuffer(bufferIndex, false);
......
...@@ -54,8 +54,8 @@ public interface SampleSource { ...@@ -54,8 +54,8 @@ public interface SampleSource {
* Prepares the source. * Prepares the source.
* <p> * <p>
* Preparation may require reading from the data source (e.g. to determine the available tracks * Preparation may require reading from the data source (e.g. to determine the available tracks
* and formats). If insufficient data is available then the call will return rather than block. * and formats). If insufficient data is available then the call will return {@code false} rather
* The method can be called repeatedly until the return value indicates success. * than block. The method can be called repeatedly until the return value indicates success.
* *
* @return True if the source was prepared successfully, false otherwise. * @return True if the source was prepared successfully, false otherwise.
* @throws IOException If an error occurred preparing the source. * @throws IOException If an error occurred preparing the source.
......
...@@ -59,7 +59,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { ...@@ -59,7 +59,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
* load is for initialization data. * load is for initialization data.
* @param totalBytes The length of the data being loaded in bytes. * @param totalBytes The length of the data being loaded in bytes.
*/ */
void onLoadStarted(int sourceId, int formatId, int trigger, boolean isInitialization, void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization,
int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes); int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes);
/** /**
...@@ -126,7 +126,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { ...@@ -126,7 +126,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
* {@link ChunkSource}. * {@link ChunkSource}.
* @param mediaTimeMs The media time at which the change occurred. * @param mediaTimeMs The media time at which the change occurred.
*/ */
void onDownstreamFormatChanged(int sourceId, int formatId, int trigger, int mediaTimeMs); void onDownstreamFormatChanged(int sourceId, String formatId, int trigger, int mediaTimeMs);
} }
...@@ -160,6 +160,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { ...@@ -160,6 +160,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
private int currentLoadableExceptionCount; private int currentLoadableExceptionCount;
private long currentLoadableExceptionTimestamp; private long currentLoadableExceptionTimestamp;
private MediaFormat downstreamMediaFormat;
private volatile Format downstreamFormat; private volatile Format downstreamFormat;
public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl, public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl,
...@@ -221,6 +222,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { ...@@ -221,6 +222,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
chunkSource.enable(); chunkSource.enable();
loadControl.register(this, bufferSizeContribution); loadControl.register(this, bufferSizeContribution);
downstreamFormat = null; downstreamFormat = null;
downstreamMediaFormat = null;
downstreamPositionUs = timeUs; downstreamPositionUs = timeUs;
lastSeekPositionUs = timeUs; lastSeekPositionUs = timeUs;
restartFrom(timeUs); restartFrom(timeUs);
...@@ -288,21 +290,30 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { ...@@ -288,21 +290,30 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
return readData(track, playbackPositionUs, formatHolder, sampleHolder, false); return readData(track, playbackPositionUs, formatHolder, sampleHolder, false);
} else if (mediaChunk.isLastChunk()) { } else if (mediaChunk.isLastChunk()) {
return END_OF_STREAM; return END_OF_STREAM;
} else {
IOException chunkSourceException = chunkSource.getError();
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, notifyDownstreamFormatChanged(mediaChunk.format.id, mediaChunk.trigger,
mediaChunk.startTimeUs); mediaChunk.startTimeUs);
MediaFormat format = mediaChunk.getMediaFormat();
chunkSource.getMaxVideoDimensions(format);
formatHolder.format = format;
formatHolder.drmInitData = mediaChunk.getPsshInfo();
downstreamFormat = mediaChunk.format; downstreamFormat = mediaChunk.format;
}
if (!mediaChunk.prepare()) {
return NOTHING_READ;
}
MediaFormat mediaFormat = mediaChunk.getMediaFormat();
if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormat)) {
chunkSource.getMaxVideoDimensions(mediaFormat);
formatHolder.format = mediaFormat;
formatHolder.drmInitData = mediaChunk.getPsshInfo();
downstreamMediaFormat = mediaFormat;
return FORMAT_READ; return FORMAT_READ;
} }
...@@ -430,6 +441,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { ...@@ -430,6 +441,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
currentLoadableExceptionCount++; currentLoadableExceptionCount++;
currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime(); currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
notifyUpstreamError(e); notifyUpstreamError(e);
chunkSource.onChunkLoadError(currentLoadableHolder.chunk, e);
updateLoadControl(); updateLoadControl();
} }
...@@ -653,7 +665,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { ...@@ -653,7 +665,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
return (int) (timeUs / 1000); return (int) (timeUs / 1000);
} }
private void notifyLoadStarted(final int formatId, final int trigger, private void notifyLoadStarted(final String formatId, final int trigger,
final boolean isInitialization, final long mediaStartTimeUs, final long mediaEndTimeUs, final boolean isInitialization, final long mediaStartTimeUs, final long mediaEndTimeUs,
final long totalBytes) { final long totalBytes) {
if (eventHandler != null && eventListener != null) { if (eventHandler != null && eventListener != null) {
...@@ -724,7 +736,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener { ...@@ -724,7 +736,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
} }
} }
private void notifyDownstreamFormatChanged(final int formatId, final int trigger, private void notifyDownstreamFormatChanged(final String formatId, final int trigger,
final long mediaTimeUs) { final long mediaTimeUs) {
if (eventHandler != null && eventListener != null) { if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() { eventHandler.post(new Runnable() {
......
...@@ -58,7 +58,7 @@ public interface ChunkSource { ...@@ -58,7 +58,7 @@ public interface ChunkSource {
* *
* @param queue A representation of the currently buffered {@link MediaChunk}s. * @param queue A representation of the currently buffered {@link MediaChunk}s.
*/ */
void disable(List<MediaChunk> queue); void disable(List<? extends MediaChunk> queue);
/** /**
* Indicates to the source that it should still be checking for updates to the stream. * Indicates to the source that it should still be checking for updates to the stream.
...@@ -100,4 +100,13 @@ public interface ChunkSource { ...@@ -100,4 +100,13 @@ public interface ChunkSource {
*/ */
IOException getError(); IOException getError();
/**
* Invoked when the {@link ChunkSampleSource} encounters an error loading a chunk obtained from
* this source.
*
* @param chunk The chunk whose load encountered the error.
* @param e The error.
*/
void onChunkLoadError(Chunk chunk, Exception e);
} }
...@@ -15,12 +15,14 @@ ...@@ -15,12 +15,14 @@
*/ */
package com.google.android.exoplayer.chunk; package com.google.android.exoplayer.chunk;
import com.google.android.exoplayer.util.Assertions;
import java.util.Comparator; import java.util.Comparator;
/** /**
* A format definition for streams. * A format definition for streams.
*/ */
public final class Format { public class Format {
/** /**
* Sorts {@link Format} objects in order of decreasing bandwidth. * Sorts {@link Format} objects in order of decreasing bandwidth.
...@@ -29,7 +31,7 @@ public final class Format { ...@@ -29,7 +31,7 @@ public final class Format {
@Override @Override
public int compare(Format a, Format b) { public int compare(Format a, Format b) {
return b.bandwidth - a.bandwidth; return b.bitrate - a.bitrate;
} }
} }
...@@ -37,7 +39,7 @@ public final class Format { ...@@ -37,7 +39,7 @@ public final class Format {
/** /**
* An identifier for the format. * An identifier for the format.
*/ */
public final int id; public final String id;
/** /**
* The mime type of the format. * The mime type of the format.
...@@ -65,8 +67,16 @@ public final class Format { ...@@ -65,8 +67,16 @@ public final class Format {
public final int audioSamplingRate; public final int audioSamplingRate;
/** /**
* The average bandwidth in bits per second.
*/
public final int bitrate;
/**
* The average bandwidth in bytes per second. * The average bandwidth in bytes per second.
*
* @deprecated Use {@link #bitrate}. However note that the units of measurement are different.
*/ */
@Deprecated
public final int bandwidth; public final int bandwidth;
/** /**
...@@ -76,17 +86,38 @@ public final class Format { ...@@ -76,17 +86,38 @@ public final class Format {
* @param height The height of the video in pixels, or -1 for non-video formats. * @param height The height of the video in pixels, or -1 for non-video formats.
* @param numChannels The number of audio channels, or -1 for non-audio formats. * @param numChannels The number of audio channels, or -1 for non-audio formats.
* @param audioSamplingRate The audio sampling rate in Hz, or -1 for non-audio formats. * @param audioSamplingRate The audio sampling rate in Hz, or -1 for non-audio formats.
* @param bandwidth The average bandwidth of the format in bytes per second. * @param bitrate The average bandwidth of the format in bits per second.
*/ */
public Format(int id, String mimeType, int width, int height, int numChannels, public Format(String id, String mimeType, int width, int height, int numChannels,
int audioSamplingRate, int bandwidth) { int audioSamplingRate, int bitrate) {
this.id = id; this.id = Assertions.checkNotNull(id);
this.mimeType = mimeType; this.mimeType = mimeType;
this.width = width; this.width = width;
this.height = height; this.height = height;
this.numChannels = numChannels; this.numChannels = numChannels;
this.audioSamplingRate = audioSamplingRate; this.audioSamplingRate = audioSamplingRate;
this.bandwidth = bandwidth; this.bitrate = bitrate;
this.bandwidth = bitrate / 8;
}
@Override
public int hashCode() {
return id.hashCode();
}
/**
* Implements equality based on {@link #id} only.
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Format other = (Format) obj;
return other.id.equals(id);
} }
} }
...@@ -146,7 +146,7 @@ public interface FormatEvaluator { ...@@ -146,7 +146,7 @@ public interface FormatEvaluator {
public void evaluate(List<? extends MediaChunk> queue, long playbackPositionUs, public void evaluate(List<? extends MediaChunk> queue, long playbackPositionUs,
Format[] formats, Evaluation evaluation) { Format[] formats, Evaluation evaluation) {
Format newFormat = formats[random.nextInt(formats.length)]; Format newFormat = formats[random.nextInt(formats.length)];
if (evaluation.format != null && evaluation.format.id != newFormat.id) { if (evaluation.format != null && !evaluation.format.id.equals(newFormat.id)) {
evaluation.trigger = TRIGGER_ADAPTIVE; evaluation.trigger = TRIGGER_ADAPTIVE;
} }
evaluation.format = newFormat; evaluation.format = newFormat;
...@@ -236,8 +236,8 @@ public interface FormatEvaluator { ...@@ -236,8 +236,8 @@ public interface FormatEvaluator {
: queue.get(queue.size() - 1).endTimeUs - playbackPositionUs; : queue.get(queue.size() - 1).endTimeUs - playbackPositionUs;
Format current = evaluation.format; Format current = evaluation.format;
Format ideal = determineIdealFormat(formats, bandwidthMeter.getEstimate()); Format ideal = determineIdealFormat(formats, bandwidthMeter.getEstimate());
boolean isHigher = ideal != null && current != null && ideal.bandwidth > current.bandwidth; boolean isHigher = ideal != null && current != null && ideal.bitrate > current.bitrate;
boolean isLower = ideal != null && current != null && ideal.bandwidth < current.bandwidth; boolean isLower = ideal != null && current != null && ideal.bitrate < current.bitrate;
if (isHigher) { if (isHigher) {
if (bufferedDurationUs < minDurationForQualityIncreaseUs) { if (bufferedDurationUs < minDurationForQualityIncreaseUs) {
// The ideal format is a higher quality, but we have insufficient buffer to // The ideal format is a higher quality, but we have insufficient buffer to
...@@ -247,11 +247,11 @@ public interface FormatEvaluator { ...@@ -247,11 +247,11 @@ public interface FormatEvaluator {
// We're switching from an SD stream to a stream of higher resolution. Consider // We're switching from an SD stream to a stream of higher resolution. Consider
// discarding already buffered media chunks. Specifically, discard media chunks starting // discarding already buffered media chunks. Specifically, discard media chunks starting
// from the first one that is of lower bandwidth, lower resolution and that is not HD. // from the first one that is of lower bandwidth, lower resolution and that is not HD.
for (int i = 0; i < queue.size(); i++) { for (int i = 1; i < queue.size(); i++) {
MediaChunk thisChunk = queue.get(i); MediaChunk thisChunk = queue.get(i);
long durationBeforeThisSegmentUs = thisChunk.startTimeUs - playbackPositionUs; long durationBeforeThisSegmentUs = thisChunk.startTimeUs - playbackPositionUs;
if (durationBeforeThisSegmentUs >= minDurationToRetainAfterDiscardUs if (durationBeforeThisSegmentUs >= minDurationToRetainAfterDiscardUs
&& thisChunk.format.bandwidth < ideal.bandwidth && thisChunk.format.bitrate < ideal.bitrate
&& thisChunk.format.height < ideal.height && thisChunk.format.height < ideal.height
&& thisChunk.format.height < 720 && thisChunk.format.height < 720
&& thisChunk.format.width < 1280) { && thisChunk.format.width < 1280) {
...@@ -280,7 +280,7 @@ public interface FormatEvaluator { ...@@ -280,7 +280,7 @@ public interface FormatEvaluator {
long effectiveBandwidth = computeEffectiveBandwidthEstimate(bandwidthEstimate); long effectiveBandwidth = computeEffectiveBandwidthEstimate(bandwidthEstimate);
for (int i = 0; i < formats.length; i++) { for (int i = 0; i < formats.length; i++) {
Format format = formats[i]; Format format = formats[i];
if (format.bandwidth <= effectiveBandwidth) { if ((format.bitrate / 8) <= effectiveBandwidth) {
return format; return format;
} }
} }
......
...@@ -73,9 +73,7 @@ public abstract class MediaChunk extends Chunk { ...@@ -73,9 +73,7 @@ public abstract class MediaChunk extends Chunk {
/** /**
* Seeks to the beginning of the chunk. * Seeks to the beginning of the chunk.
*/ */
public final void seekToStart() { public abstract void seekToStart();
seekTo(startTimeUs, false);
}
/** /**
* Seeks to the specified position within the chunk. * Seeks to the specified position within the chunk.
...@@ -90,7 +88,21 @@ public abstract class MediaChunk extends Chunk { ...@@ -90,7 +88,21 @@ public abstract class MediaChunk extends Chunk {
public abstract boolean seekTo(long positionUs, boolean allowNoop); public abstract boolean seekTo(long positionUs, boolean allowNoop);
/** /**
* Prepares the chunk for reading. Does nothing if the chunk is already prepared.
* <p>
* Preparation may require consuming some of the chunk. If the data is not yet available then
* this method will return {@code false} rather than block. The method can be called repeatedly
* until the return value indicates success.
*
* @return True if the chunk was prepared. False otherwise.
* @throws ParserException If an error occurs parsing the media data.
*/
public abstract boolean prepare() throws ParserException;
/**
* Reads the next media sample from the chunk. * Reads the next media sample from the chunk.
* <p>
* Should only be called after the chunk has been successfully prepared.
* *
* @param holder A holder to store the read sample. * @param holder A holder to store the read sample.
* @return True if a sample was read. False if more data is still required. * @return True if a sample was read. False if more data is still required.
...@@ -101,6 +113,8 @@ public abstract class MediaChunk extends Chunk { ...@@ -101,6 +113,8 @@ public abstract class MediaChunk extends Chunk {
/** /**
* Returns the media format of the samples contained within this chunk. * Returns the media format of the samples contained within this chunk.
* <p>
* Should only be called after the chunk has been successfully prepared.
* *
* @return The sample media format. * @return The sample media format.
*/ */
...@@ -108,6 +122,8 @@ public abstract class MediaChunk extends Chunk { ...@@ -108,6 +122,8 @@ public abstract class MediaChunk extends Chunk {
/** /**
* Returns the pssh information associated with the chunk. * Returns the pssh information associated with the chunk.
* <p>
* Should only be called after the chunk has been successfully prepared.
* *
* @return The pssh information. * @return The pssh information.
*/ */
......
...@@ -33,28 +33,44 @@ import java.util.UUID; ...@@ -33,28 +33,44 @@ import java.util.UUID;
public final class Mp4MediaChunk extends MediaChunk { public final class Mp4MediaChunk extends MediaChunk {
private final FragmentedMp4Extractor extractor; private final FragmentedMp4Extractor extractor;
private final boolean maybeSelfContained;
private final long sampleOffsetUs; private final long sampleOffsetUs;
private boolean prepared;
private MediaFormat mediaFormat;
private Map<UUID, byte[]> psshInfo;
/** /**
* @param dataSource A {@link DataSource} for loading the data. * @param dataSource A {@link DataSource} for loading the data.
* @param dataSpec Defines the data to be loaded. * @param dataSpec Defines the data to be loaded.
* @param format The format of the stream to which this chunk belongs. * @param format The format of the stream to which this chunk belongs.
* @param extractor The extractor that will be used to extract the samples.
* @param trigger The reason for this chunk being selected. * @param trigger The reason for this chunk being selected.
* @param startTimeUs The start time of the media contained by the chunk, in microseconds. * @param startTimeUs The start time of the media contained by the chunk, in microseconds.
* @param endTimeUs The end time of the media contained by the chunk, in microseconds. * @param endTimeUs The end time of the media contained by the chunk, in microseconds.
* @param sampleOffsetUs An offset to subtract from the sample timestamps parsed by the extractor.
* @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk. * @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
* @param extractor The extractor that will be used to extract the samples.
* @param maybeSelfContained Set to true if this chunk might be self contained, meaning it might
* contain a moov atom defining the media format of the chunk. This parameter can always be
* safely set to true. Setting to false where the chunk is known to not be self contained may
* improve startup latency.
* @param sampleOffsetUs An offset to subtract from the sample timestamps parsed by the extractor.
*/ */
public Mp4MediaChunk(DataSource dataSource, DataSpec dataSpec, Format format, public Mp4MediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
int trigger, FragmentedMp4Extractor extractor, long startTimeUs, long endTimeUs, int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex,
long sampleOffsetUs, int nextChunkIndex) { FragmentedMp4Extractor extractor, boolean maybeSelfContained, long sampleOffsetUs) {
super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex); super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex);
this.extractor = extractor; this.extractor = extractor;
this.maybeSelfContained = maybeSelfContained;
this.sampleOffsetUs = sampleOffsetUs; this.sampleOffsetUs = sampleOffsetUs;
} }
@Override @Override
public void seekToStart() {
extractor.seekTo(0, false);
resetReadPosition();
}
@Override
public boolean seekTo(long positionUs, boolean allowNoop) { public boolean seekTo(long positionUs, boolean allowNoop) {
long seekTimeUs = positionUs + sampleOffsetUs; long seekTimeUs = positionUs + sampleOffsetUs;
boolean isDiscontinuous = extractor.seekTo(seekTimeUs, allowNoop); boolean isDiscontinuous = extractor.seekTo(seekTimeUs, allowNoop);
...@@ -65,6 +81,29 @@ public final class Mp4MediaChunk extends MediaChunk { ...@@ -65,6 +81,29 @@ public final class Mp4MediaChunk extends MediaChunk {
} }
@Override @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 { public boolean read(SampleHolder holder) throws ParserException {
NonBlockingInputStream inputStream = getNonBlockingInputStream(); NonBlockingInputStream inputStream = getNonBlockingInputStream();
Assertions.checkState(inputStream != null); Assertions.checkState(inputStream != null);
...@@ -78,12 +117,12 @@ public final class Mp4MediaChunk extends MediaChunk { ...@@ -78,12 +117,12 @@ public final class Mp4MediaChunk extends MediaChunk {
@Override @Override
public MediaFormat getMediaFormat() { public MediaFormat getMediaFormat() {
return extractor.getFormat(); return mediaFormat;
} }
@Override @Override
public Map<UUID, byte[]> getPsshInfo() { public Map<UUID, byte[]> getPsshInfo() {
return extractor.getPsshInfo(); return psshInfo;
} }
} }
...@@ -68,7 +68,7 @@ public class MultiTrackChunkSource implements ChunkSource, ExoPlayerComponent { ...@@ -68,7 +68,7 @@ public class MultiTrackChunkSource implements ChunkSource, ExoPlayerComponent {
} }
@Override @Override
public void disable(List<MediaChunk> queue) { public void disable(List<? extends MediaChunk> queue) {
selectedSource.disable(queue); selectedSource.disable(queue);
enabled = false; enabled = false;
} }
...@@ -102,4 +102,9 @@ public class MultiTrackChunkSource implements ChunkSource, ExoPlayerComponent { ...@@ -102,4 +102,9 @@ public class MultiTrackChunkSource implements ChunkSource, ExoPlayerComponent {
} }
} }
@Override
public void onChunkLoadError(Chunk chunk, Exception e) {
selectedSource.onChunkLoadError(chunk, e);
}
} }
...@@ -78,6 +78,11 @@ public class SingleSampleMediaChunk extends MediaChunk { ...@@ -78,6 +78,11 @@ public class SingleSampleMediaChunk extends MediaChunk {
} }
@Override @Override
public boolean prepare() {
return true;
}
@Override
public boolean read(SampleHolder holder) { public boolean read(SampleHolder holder) {
NonBlockingInputStream inputStream = getNonBlockingInputStream(); NonBlockingInputStream inputStream = getNonBlockingInputStream();
Assertions.checkState(inputStream != null); Assertions.checkState(inputStream != null);
...@@ -110,6 +115,11 @@ public class SingleSampleMediaChunk extends MediaChunk { ...@@ -110,6 +115,11 @@ public class SingleSampleMediaChunk extends MediaChunk {
} }
@Override @Override
public void seekToStart() {
resetReadPosition();
}
@Override
public boolean seekTo(long positionUs, boolean allowNoop) { public boolean seekTo(long positionUs, boolean allowNoop) {
resetReadPosition(); resetReadPosition();
return true; return true;
......
...@@ -51,6 +51,11 @@ public final class WebmMediaChunk extends MediaChunk { ...@@ -51,6 +51,11 @@ public final class WebmMediaChunk extends MediaChunk {
} }
@Override @Override
public void seekToStart() {
seekTo(0, false);
}
@Override
public boolean seekTo(long positionUs, boolean allowNoop) { public boolean seekTo(long positionUs, boolean allowNoop) {
boolean isDiscontinuous = extractor.seekTo(positionUs, allowNoop); boolean isDiscontinuous = extractor.seekTo(positionUs, allowNoop);
if (isDiscontinuous) { if (isDiscontinuous) {
...@@ -60,6 +65,11 @@ public final class WebmMediaChunk extends MediaChunk { ...@@ -60,6 +65,11 @@ public final class WebmMediaChunk extends MediaChunk {
} }
@Override @Override
public boolean prepare() {
return true;
}
@Override
public boolean read(SampleHolder holder) { public boolean read(SampleHolder holder) {
NonBlockingInputStream inputStream = getNonBlockingInputStream(); NonBlockingInputStream inputStream = getNonBlockingInputStream();
Assertions.checkState(inputStream != null); Assertions.checkState(inputStream != null);
......
...@@ -18,6 +18,8 @@ package com.google.android.exoplayer.dash.mpd; ...@@ -18,6 +18,8 @@ package com.google.android.exoplayer.dash.mpd;
import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher;
import android.net.Uri;
import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException; import java.io.IOException;
...@@ -57,9 +59,9 @@ public final class MediaPresentationDescriptionFetcher extends ...@@ -57,9 +59,9 @@ public final class MediaPresentationDescriptionFetcher extends
@Override @Override
protected MediaPresentationDescription parse(InputStream stream, String inputEncoding, protected MediaPresentationDescription parse(InputStream stream, String inputEncoding,
String contentId) throws IOException, ParserException { String contentId, Uri baseUrl) throws IOException, ParserException {
try { try {
return parser.parseMediaPresentationDescription(stream, inputEncoding, contentId); return parser.parseMediaPresentationDescription(stream, inputEncoding, contentId, baseUrl);
} catch (XmlPullParserException e) { } catch (XmlPullParserException e) {
throw new ParserException(e); throw new ParserException(e);
} }
......
...@@ -23,46 +23,37 @@ import java.util.List; ...@@ -23,46 +23,37 @@ import java.util.List;
*/ */
public final class Period { public final class Period {
public final int id; /**
* The period identifier, if one exists.
public final long start; */
public final String id;
public final long duration;
/**
* The start time of the period in milliseconds.
*/
public final long startMs;
/**
* The duration of the period in milliseconds, or -1 if the duration is unknown.
*/
public final long durationMs;
/**
* The adaptation sets belonging to the period.
*/
public final List<AdaptationSet> adaptationSets; public final List<AdaptationSet> adaptationSets;
public final List<Segment.Timeline> segmentList; /**
* @param id The period identifier. May be null.
public final int segmentStartNumber; * @param start The start time of the period in milliseconds.
* @param duration The duration of the period in milliseconds, or -1 if the duration is unknown.
public final int segmentTimescale; * @param adaptationSets The adaptation sets belonging to the period.
*/
public final long presentationTimeOffset; public Period(String id, long start, long duration, List<AdaptationSet> adaptationSets) {
public Period(int id, long start, long duration, List<AdaptationSet> adaptationSets) {
this(id, start, duration, adaptationSets, null, 0, 0, 0);
}
public Period(int id, long start, long duration, List<AdaptationSet> adaptationSets,
List<Segment.Timeline> segmentList, int segmentStartNumber, int segmentTimescale) {
this(id, start, duration, adaptationSets, segmentList, segmentStartNumber, segmentTimescale, 0);
}
public Period(int id, long start, long duration, List<AdaptationSet> adaptationSets,
List<Segment.Timeline> segmentList, int segmentStartNumber, int segmentTimescale,
long presentationTimeOffset) {
this.id = id; this.id = id;
this.start = start; this.startMs = start;
this.duration = duration; this.durationMs = duration;
this.adaptationSets = Collections.unmodifiableList(adaptationSets); this.adaptationSets = Collections.unmodifiableList(adaptationSets);
if (segmentList != null) {
this.segmentList = Collections.unmodifiableList(segmentList);
} else {
this.segmentList = null;
}
this.segmentStartNumber = segmentStartNumber;
this.segmentTimescale = segmentTimescale;
this.presentationTimeOffset = presentationTimeOffset;
} }
} }
/*
* 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());
}
}
/*
* 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;
}
}
}
/*
* 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);
}
}
/*
* 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; ...@@ -21,6 +21,7 @@ import java.util.List;
/* package */ abstract class Atom { /* package */ abstract class Atom {
public static final int TYPE_avc1 = 0x61766331; public static final int TYPE_avc1 = 0x61766331;
public static final int TYPE_avc3 = 0x61766333;
public static final int TYPE_esds = 0x65736473; public static final int TYPE_esds = 0x65736473;
public static final int TYPE_mdat = 0x6D646174; public static final int TYPE_mdat = 0x6D646174;
public static final int TYPE_mfhd = 0x6D666864; public static final int TYPE_mfhd = 0x6D666864;
......
/*
* 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);
}
...@@ -63,7 +63,7 @@ public class SmoothStreamingChunkSource implements ChunkSource { ...@@ -63,7 +63,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
private final int maxHeight; private final int maxHeight;
private final SparseArray<FragmentedMp4Extractor> extractors; private final SparseArray<FragmentedMp4Extractor> extractors;
private final Format[] formats; private final SmoothStreamingFormat[] formats;
/** /**
* @param baseUrl The base URL for the streams. * @param baseUrl The base URL for the streams.
...@@ -94,23 +94,24 @@ public class SmoothStreamingChunkSource implements ChunkSource { ...@@ -94,23 +94,24 @@ public class SmoothStreamingChunkSource implements ChunkSource {
} }
int trackCount = trackIndices != null ? trackIndices.length : streamElement.tracks.length; int trackCount = trackIndices != null ? trackIndices.length : streamElement.tracks.length;
formats = new Format[trackCount]; formats = new SmoothStreamingFormat[trackCount];
extractors = new SparseArray<FragmentedMp4Extractor>(); extractors = new SparseArray<FragmentedMp4Extractor>();
int maxWidth = 0; int maxWidth = 0;
int maxHeight = 0; int maxHeight = 0;
for (int i = 0; i < trackCount; i++) { for (int i = 0; i < trackCount; i++) {
int trackIndex = trackIndices != null ? trackIndices[i] : i; int trackIndex = trackIndices != null ? trackIndices[i] : i;
TrackElement trackElement = streamElement.tracks[trackIndex]; TrackElement trackElement = streamElement.tracks[trackIndex];
formats[i] = new Format(trackIndex, trackElement.mimeType, trackElement.maxWidth, formats[i] = new SmoothStreamingFormat(String.valueOf(trackIndex), trackElement.mimeType,
trackElement.maxHeight, trackElement.numChannels, trackElement.sampleRate, trackElement.maxWidth, trackElement.maxHeight, trackElement.numChannels,
trackElement.bitrate / 8); trackElement.sampleRate, trackElement.bitrate, trackIndex);
maxWidth = Math.max(maxWidth, trackElement.maxWidth); maxWidth = Math.max(maxWidth, trackElement.maxWidth);
maxHeight = Math.max(maxHeight, trackElement.maxHeight); maxHeight = Math.max(maxHeight, trackElement.maxHeight);
MediaFormat mediaFormat = getMediaFormat(streamElement, trackIndex); MediaFormat mediaFormat = getMediaFormat(streamElement, trackIndex);
int trackType = streamElement.type == StreamElement.TYPE_VIDEO ? Track.TYPE_VIDEO int trackType = streamElement.type == StreamElement.TYPE_VIDEO ? Track.TYPE_VIDEO
: Track.TYPE_AUDIO; : Track.TYPE_AUDIO;
FragmentedMp4Extractor extractor = new FragmentedMp4Extractor(true); FragmentedMp4Extractor extractor = new FragmentedMp4Extractor(
FragmentedMp4Extractor.WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME);
extractor.setTrack(new Track(trackIndex, trackType, streamElement.timeScale, mediaFormat, extractor.setTrack(new Track(trackIndex, trackType, streamElement.timeScale, mediaFormat,
trackEncryptionBoxes)); trackEncryptionBoxes));
if (protectionElement != null) { if (protectionElement != null) {
...@@ -141,7 +142,7 @@ public class SmoothStreamingChunkSource implements ChunkSource { ...@@ -141,7 +142,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
} }
@Override @Override
public void disable(List<MediaChunk> queue) { public void disable(List<? extends MediaChunk> queue) {
// Do nothing. // Do nothing.
} }
...@@ -155,14 +156,14 @@ public class SmoothStreamingChunkSource implements ChunkSource { ...@@ -155,14 +156,14 @@ public class SmoothStreamingChunkSource implements ChunkSource {
long playbackPositionUs, ChunkOperationHolder out) { long playbackPositionUs, ChunkOperationHolder out) {
evaluation.queueSize = queue.size(); evaluation.queueSize = queue.size();
formatEvaluator.evaluate(queue, playbackPositionUs, formats, evaluation); formatEvaluator.evaluate(queue, playbackPositionUs, formats, evaluation);
Format selectedFormat = evaluation.format; SmoothStreamingFormat selectedFormat = (SmoothStreamingFormat) evaluation.format;
out.queueSize = evaluation.queueSize; out.queueSize = evaluation.queueSize;
if (selectedFormat == null) { if (selectedFormat == null) {
out.chunk = null; out.chunk = null;
return; return;
} else if (out.queueSize == queue.size() && out.chunk != null } else if (out.queueSize == queue.size() && out.chunk != null
&& out.chunk.format.id == evaluation.format.id) { && out.chunk.format.id.equals(evaluation.format.id)) {
// We already have a chunk, and the evaluation hasn't changed either the format or the size // We already have a chunk, and the evaluation hasn't changed either the format or the size
// of the queue. Do nothing. // of the queue. Do nothing.
return; return;
...@@ -181,11 +182,12 @@ public class SmoothStreamingChunkSource implements ChunkSource { ...@@ -181,11 +182,12 @@ public class SmoothStreamingChunkSource implements ChunkSource {
} }
boolean isLastChunk = nextChunkIndex == streamElement.chunkCount - 1; boolean isLastChunk = nextChunkIndex == streamElement.chunkCount - 1;
String requestUrl = streamElement.buildRequestUrl(selectedFormat.id, nextChunkIndex); String requestUrl = streamElement.buildRequestUrl(selectedFormat.trackIndex,
nextChunkIndex);
Uri uri = Uri.parse(baseUrl + '/' + requestUrl); Uri uri = Uri.parse(baseUrl + '/' + requestUrl);
Chunk mediaChunk = newMediaChunk(selectedFormat, uri, null, Chunk mediaChunk = newMediaChunk(selectedFormat, uri, null,
extractors.get(selectedFormat.id), dataSource, nextChunkIndex, isLastChunk, extractors.get(Integer.parseInt(selectedFormat.id)), dataSource, nextChunkIndex,
streamElement.getStartTimeUs(nextChunkIndex), isLastChunk, streamElement.getStartTimeUs(nextChunkIndex),
isLastChunk ? -1 : streamElement.getStartTimeUs(nextChunkIndex + 1), 0); isLastChunk ? -1 : streamElement.getStartTimeUs(nextChunkIndex + 1), 0);
out.chunk = mediaChunk; out.chunk = mediaChunk;
} }
...@@ -195,6 +197,11 @@ public class SmoothStreamingChunkSource implements ChunkSource { ...@@ -195,6 +197,11 @@ public class SmoothStreamingChunkSource implements ChunkSource {
return null; return null;
} }
@Override
public void onChunkLoadError(Chunk chunk, Exception e) {
// Do nothing.
}
private static MediaFormat getMediaFormat(StreamElement streamElement, int trackIndex) { private static MediaFormat getMediaFormat(StreamElement streamElement, int trackIndex) {
TrackElement trackElement = streamElement.tracks[trackIndex]; TrackElement trackElement = streamElement.tracks[trackIndex];
String mimeType = trackElement.mimeType; String mimeType = trackElement.mimeType;
...@@ -228,8 +235,8 @@ public class SmoothStreamingChunkSource implements ChunkSource { ...@@ -228,8 +235,8 @@ public class SmoothStreamingChunkSource implements ChunkSource {
DataSpec dataSpec = new DataSpec(uri, offset, -1, cacheKey); DataSpec dataSpec = new DataSpec(uri, offset, -1, cacheKey);
// In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk. // In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk.
// To convert them the absolute timestamps, we need to set sampleOffsetUs to -chunkStartTimeUs. // To convert them the absolute timestamps, we need to set sampleOffsetUs to -chunkStartTimeUs.
return new Mp4MediaChunk(dataSource, dataSpec, formatInfo, trigger, extractor, return new Mp4MediaChunk(dataSource, dataSpec, formatInfo, trigger, chunkStartTimeUs,
chunkStartTimeUs, nextStartTimeUs, -chunkStartTimeUs, nextChunkIndex); nextStartTimeUs, nextChunkIndex, extractor, false, -chunkStartTimeUs);
} }
private static byte[] getKeyId(byte[] initData) { private static byte[] getKeyId(byte[] initData) {
...@@ -254,4 +261,16 @@ public class SmoothStreamingChunkSource implements ChunkSource { ...@@ -254,4 +261,16 @@ public class SmoothStreamingChunkSource implements ChunkSource {
data[secondPosition] = temp; data[secondPosition] = temp;
} }
private static final class SmoothStreamingFormat extends Format {
public final int trackIndex;
public SmoothStreamingFormat(String id, String mimeType, int width, int height,
int numChannels, int audioSamplingRate, int bitrate, int trackIndex) {
super(id, mimeType, width, height, numChannels, audioSamplingRate, bitrate);
this.trackIndex = trackIndex;
}
}
} }
...@@ -16,8 +16,8 @@ ...@@ -16,8 +16,8 @@
package com.google.android.exoplayer.smoothstreaming; package com.google.android.exoplayer.smoothstreaming;
import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.Util;
import java.util.Arrays;
import java.util.UUID; import java.util.UUID;
/** /**
...@@ -195,9 +195,7 @@ public class SmoothStreamingManifest { ...@@ -195,9 +195,7 @@ public class SmoothStreamingManifest {
* @return The index of the corresponding chunk. * @return The index of the corresponding chunk.
*/ */
public int getChunkIndex(long timeUs) { public int getChunkIndex(long timeUs) {
long time = (timeUs * timeScale) / 1000000L; return Util.binarySearchFloor(chunkStartTimes, (timeUs * timeScale) / 1000000L, true, true);
int chunkIndex = Arrays.binarySearch(chunkStartTimes, time);
return chunkIndex < 0 ? -chunkIndex - 2 : chunkIndex;
} }
/** /**
......
...@@ -18,6 +18,8 @@ package com.google.android.exoplayer.smoothstreaming; ...@@ -18,6 +18,8 @@ package com.google.android.exoplayer.smoothstreaming;
import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher;
import android.net.Uri;
import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException; import java.io.IOException;
...@@ -56,7 +58,7 @@ public final class SmoothStreamingManifestFetcher extends ManifestFetcher<Smooth ...@@ -56,7 +58,7 @@ public final class SmoothStreamingManifestFetcher extends ManifestFetcher<Smooth
@Override @Override
protected SmoothStreamingManifest parse(InputStream stream, String inputEncoding, protected SmoothStreamingManifest parse(InputStream stream, String inputEncoding,
String contentId) throws IOException, ParserException { String contentId, Uri baseUrl) throws IOException, ParserException {
try { try {
return parser.parse(stream, inputEncoding); return parser.parse(stream, inputEncoding);
} catch (XmlPullParserException e) { } catch (XmlPullParserException e) {
......
...@@ -16,8 +16,7 @@ ...@@ -16,8 +16,7 @@
package com.google.android.exoplayer.text.ttml; package com.google.android.exoplayer.text.ttml;
import com.google.android.exoplayer.text.Subtitle; import com.google.android.exoplayer.text.Subtitle;
import com.google.android.exoplayer.util.Util;
import java.util.Arrays;
/** /**
* A representation of a TTML subtitle. * A representation of a TTML subtitle.
...@@ -41,8 +40,7 @@ public final class TtmlSubtitle implements Subtitle { ...@@ -41,8 +40,7 @@ public final class TtmlSubtitle implements Subtitle {
@Override @Override
public int getNextEventTimeIndex(long timeUs) { public int getNextEventTimeIndex(long timeUs) {
int index = Arrays.binarySearch(eventTimesUs, timeUs - startTimeUs); int index = Util.binarySearchCeil(eventTimesUs, timeUs - startTimeUs, false, false);
index = index >= 0 ? index + 1 : ~index;
return index < eventTimesUs.length ? index : -1; return index < eventTimesUs.length ? index : -1;
} }
......
...@@ -176,7 +176,7 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream ...@@ -176,7 +176,7 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
*/ */
private int read(ByteBuffer target, byte[] targetArray, int targetArrayOffset, private int read(ByteBuffer target, byte[] targetArray, int targetArrayOffset,
ReadHead readHead, int readLength) { ReadHead readHead, int readLength) {
if (readHead.position == dataSpec.length) { if (isEndOfStream()) {
return -1; return -1;
} }
int bytesToRead = (int) Math.min(loadPosition - readHead.position, readLength); int bytesToRead = (int) Math.min(loadPosition - readHead.position, readLength);
......
...@@ -115,8 +115,8 @@ public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener { ...@@ -115,8 +115,8 @@ public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener {
float bytesPerSecond = accumulator * 1000 / elapsedMs; float bytesPerSecond = accumulator * 1000 / elapsedMs;
slidingPercentile.addSample(computeWeight(accumulator), bytesPerSecond); slidingPercentile.addSample(computeWeight(accumulator), bytesPerSecond);
float bandwidthEstimateFloat = slidingPercentile.getPercentile(0.5f); float bandwidthEstimateFloat = slidingPercentile.getPercentile(0.5f);
bandwidthEstimate = bandwidthEstimateFloat == Float.NaN bandwidthEstimate = Float.isNaN(bandwidthEstimateFloat) ? NO_ESTIMATE
? NO_ESTIMATE : (long) bandwidthEstimateFloat; : (long) bandwidthEstimateFloat;
notifyBandwidthSample(elapsedMs, accumulator, bandwidthEstimate); notifyBandwidthSample(elapsedMs, accumulator, bandwidthEstimate);
} }
streamCount--; streamCount--;
......
...@@ -134,9 +134,8 @@ public interface Cache { ...@@ -134,9 +134,8 @@ public interface Cache {
* @param key The key of the data being requested. * @param key The key of the data being requested.
* @param position The position of the data being requested. * @param position The position of the data being requested.
* @return The {@link CacheSpan}. Or null if the cache entry is locked. * @return The {@link CacheSpan}. Or null if the cache entry is locked.
* @throws InterruptedException
*/ */
CacheSpan startReadWriteNonBlocking(String key, long position) throws InterruptedException; CacheSpan startReadWriteNonBlocking(String key, long position);
/** /**
* Obtains a cache file into which data can be written. Must only be called when holding a * Obtains a cache file into which data can be written. Must only be called when holding a
...@@ -173,4 +172,14 @@ public interface Cache { ...@@ -173,4 +172,14 @@ public interface Cache {
*/ */
void removeSpan(CacheSpan span); void removeSpan(CacheSpan span);
/**
* Queries if a range is entirely available in the cache.
*
* @param key The cache key for the data.
* @param position The starting position of the data.
* @param length The length of the data.
* @return true if the data is available in the Cache otherwise false;
*/
boolean isCached(String key, long position, long length);
} }
...@@ -109,26 +109,29 @@ public class SimpleCache implements Cache { ...@@ -109,26 +109,29 @@ public class SimpleCache implements Cache {
public synchronized CacheSpan startReadWrite(String key, long position) public synchronized CacheSpan startReadWrite(String key, long position)
throws InterruptedException { throws InterruptedException {
CacheSpan lookupSpan = CacheSpan.createLookup(key, position); CacheSpan lookupSpan = CacheSpan.createLookup(key, position);
// Wait until no-one holds a lock for the key. while (true) {
while (lockedSpans.containsKey(key)) { CacheSpan span = startReadWriteNonBlocking(lookupSpan);
wait(); 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 @Override
public synchronized CacheSpan startReadWriteNonBlocking(String key, long position) public synchronized CacheSpan startReadWriteNonBlocking(String key, long position) {
throws InterruptedException { return startReadWriteNonBlocking(CacheSpan.createLookup(key, position));
CacheSpan lookupSpan = CacheSpan.createLookup(key, position);
// Return null if key is locked
if (lockedSpans.containsKey(key)) {
return null;
}
return getSpanningRegion(key, lookupSpan);
} }
private CacheSpan getSpanningRegion(String key, CacheSpan lookupSpan) { private synchronized CacheSpan startReadWriteNonBlocking(CacheSpan lookupSpan) {
CacheSpan spanningRegion = getSpan(lookupSpan); CacheSpan spanningRegion = getSpan(lookupSpan);
// Read case.
if (spanningRegion.isCached) { if (spanningRegion.isCached) {
CacheSpan oldCacheSpan = spanningRegion; CacheSpan oldCacheSpan = spanningRegion;
// Remove the old span from the in-memory representation. // Remove the old span from the in-memory representation.
...@@ -139,10 +142,17 @@ public class SimpleCache implements Cache { ...@@ -139,10 +142,17 @@ public class SimpleCache implements Cache {
// Add the updated span back into the in-memory representation. // Add the updated span back into the in-memory representation.
spansForKey.add(spanningRegion); spansForKey.add(spanningRegion);
notifySpanTouched(oldCacheSpan, spanningRegion); notifySpanTouched(oldCacheSpan, spanningRegion);
} else { return spanningRegion;
lockedSpans.put(key, spanningRegion);
} }
return spanningRegion;
// Write case, lock available.
if (!lockedSpans.containsKey(lookupSpan.key)) {
lockedSpans.put(lookupSpan.key, spanningRegion);
return spanningRegion;
}
// Write case, lock not available.
return null;
} }
@Override @Override
...@@ -173,6 +183,7 @@ public class SimpleCache implements Cache { ...@@ -173,6 +183,7 @@ public class SimpleCache implements Cache {
return; return;
} }
addSpan(span); addSpan(span);
notifyAll();
} }
@Override @Override
...@@ -330,4 +341,41 @@ public class SimpleCache implements Cache { ...@@ -330,4 +341,41 @@ public class SimpleCache implements Cache {
evictor.onSpanTouched(this, oldSpan, newSpan); evictor.onSpanTouched(this, oldSpan, newSpan);
} }
@Override
public synchronized boolean isCached(String key, long position, long length) {
TreeSet<CacheSpan> entries = cachedSpans.get(key);
if (entries == null) {
return false;
}
CacheSpan lookupSpan = CacheSpan.createLookup(key, position);
CacheSpan floorSpan = entries.floor(lookupSpan);
if (floorSpan == null || floorSpan.position + floorSpan.length <= position) {
// We don't have a span covering the start of the queried region.
return false;
}
long queryEndPosition = position + length;
long currentEndPosition = floorSpan.position + floorSpan.length;
if (currentEndPosition >= queryEndPosition) {
// floorSpan covers the queried region.
return true;
}
Iterator<CacheSpan> iterator = entries.tailSet(floorSpan, false).iterator();
while (iterator.hasNext()) {
CacheSpan next = iterator.next();
if (next.position > currentEndPosition) {
// There's a hole in the cache within the queried region.
return false;
}
// We expect currentEndPosition to always equal (next.position + next.length), but
// perform a max check anyway to guard against the existence of overlapping spans.
currentEndPosition = Math.max(currentEndPosition, next.position + next.length);
if (currentEndPosition >= queryEndPosition) {
// We've found spans covering the queried region.
return true;
}
}
// We ran out of spans before covering the queried region.
return false;
}
} }
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer.util; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer.util;
import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.ParserException;
import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import java.io.IOException; import java.io.IOException;
...@@ -84,14 +85,15 @@ public abstract class ManifestFetcher<T> extends AsyncTask<String, Void, T> { ...@@ -84,14 +85,15 @@ public abstract class ManifestFetcher<T> extends AsyncTask<String, Void, T> {
protected final T doInBackground(String... data) { protected final T doInBackground(String... data) {
try { try {
contentId = data.length > 1 ? data[1] : null; contentId = data.length > 1 ? data[1] : null;
URL url = new URL(data[0]); String urlString = data[0];
String inputEncoding = null; String inputEncoding = null;
InputStream inputStream = null; InputStream inputStream = null;
try { try {
HttpURLConnection connection = configureHttpConnection(url); Uri baseUrl = Util.parseBaseUri(urlString);
HttpURLConnection connection = configureHttpConnection(new URL(urlString));
inputStream = connection.getInputStream(); inputStream = connection.getInputStream();
inputEncoding = connection.getContentEncoding(); inputEncoding = connection.getContentEncoding();
return parse(inputStream, inputEncoding, contentId); return parse(inputStream, inputEncoding, contentId, baseUrl);
} finally { } finally {
if (inputStream != null) { if (inputStream != null) {
inputStream.close(); inputStream.close();
...@@ -119,11 +121,13 @@ public abstract class ManifestFetcher<T> extends AsyncTask<String, Void, T> { ...@@ -119,11 +121,13 @@ public abstract class ManifestFetcher<T> extends AsyncTask<String, Void, T> {
* @param stream The input stream to read. * @param stream The input stream to read.
* @param inputEncoding The encoding of the input stream. * @param inputEncoding The encoding of the input stream.
* @param contentId The content id of the media. * @param contentId The content id of the media.
* @param baseUrl Required where the manifest contains urls that are relative to a base url. May
* be null where this is not the case.
* @throws IOException If an error occurred loading the data. * @throws IOException If an error occurred loading the data.
* @throws ParserException If an error occurred parsing the loaded data. * @throws ParserException If an error occurred parsing the loaded data.
*/ */
protected abstract T parse(InputStream stream, String inputEncoding, String contentId) throws protected abstract T parse(InputStream stream, String inputEncoding, String contentId,
IOException, ParserException; Uri baseUrl) throws IOException, ParserException;
private HttpURLConnection configureHttpConnection(URL url) throws IOException { private HttpURLConnection configureHttpConnection(URL url) throws IOException {
HttpURLConnection connection = (HttpURLConnection) url.openConnection(); HttpURLConnection connection = (HttpURLConnection) url.openConnection();
......
...@@ -20,25 +20,47 @@ package com.google.android.exoplayer.util; ...@@ -20,25 +20,47 @@ package com.google.android.exoplayer.util;
*/ */
public class MimeTypes { public class MimeTypes {
public static final String VIDEO_MP4 = "video/mp4"; public static final String BASE_TYPE_VIDEO = "video";
public static final String VIDEO_WEBM = "video/webm"; public static final String BASE_TYPE_AUDIO = "audio";
public static final String VIDEO_H264 = "video/avc"; public static final String BASE_TYPE_TEXT = "text";
public static final String VIDEO_VP9 = "video/x-vnd.on2.vp9"; public static final String BASE_TYPE_APPLICATION = "application";
public static final String AUDIO_MP4 = "audio/mp4";
public static final String AUDIO_AAC = "audio/mp4a-latm"; public static final String VIDEO_MP4 = BASE_TYPE_VIDEO + "/mp4";
public static final String TEXT_VTT = "text/vtt"; public static final String VIDEO_WEBM = BASE_TYPE_VIDEO + "/webm";
public static final String APPLICATION_TTML = "application/ttml+xml"; public static final String VIDEO_H264 = BASE_TYPE_VIDEO + "/avc";
public static final String VIDEO_VP9 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp9";
public static final String AUDIO_MP4 = BASE_TYPE_AUDIO + "/mp4";
public static final String AUDIO_AAC = BASE_TYPE_AUDIO + "/mp4a-latm";
public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt";
public static final String APPLICATION_TTML = BASE_TYPE_APPLICATION + "/ttml+xml";
private MimeTypes() {} private MimeTypes() {}
/** /**
* Returns the top-level type of {@code mimeType}.
*
* @param mimeType The mimeType whose top-level type is required.
* @return The top-level type.
*/
public static String getTopLevelType(String mimeType) {
int indexOfSlash = mimeType.indexOf('/');
if (indexOfSlash == -1) {
throw new IllegalArgumentException("Invalid mime type: " + mimeType);
}
return mimeType.substring(0, indexOfSlash);
}
/**
* Whether the top-level type of {@code mimeType} is audio. * Whether the top-level type of {@code mimeType} is audio.
* *
* @param mimeType The mimeType to test. * @param mimeType The mimeType to test.
* @return Whether the top level type is audio. * @return Whether the top level type is audio.
*/ */
public static boolean isAudio(String mimeType) { public static boolean isAudio(String mimeType) {
return mimeType.startsWith("audio/"); return getTopLevelType(mimeType).equals(BASE_TYPE_AUDIO);
} }
/** /**
...@@ -48,7 +70,7 @@ public class MimeTypes { ...@@ -48,7 +70,7 @@ public class MimeTypes {
* @return Whether the top level type is video. * @return Whether the top level type is video.
*/ */
public static boolean isVideo(String mimeType) { public static boolean isVideo(String mimeType) {
return mimeType.startsWith("video/"); return getTopLevelType(mimeType).equals(BASE_TYPE_VIDEO);
} }
/** /**
...@@ -58,7 +80,27 @@ public class MimeTypes { ...@@ -58,7 +80,27 @@ public class MimeTypes {
* @return Whether the top level type is text. * @return Whether the top level type is text.
*/ */
public static boolean isText(String mimeType) { public static boolean isText(String mimeType) {
return mimeType.startsWith("text/"); return getTopLevelType(mimeType).equals(BASE_TYPE_TEXT);
}
/**
* Whether the top-level type of {@code mimeType} is application.
*
* @param mimeType The mimeType to test.
* @return Whether the top level type is application.
*/
public static boolean isApplication(String mimeType) {
return getTopLevelType(mimeType).equals(BASE_TYPE_APPLICATION);
}
/**
* Whether the mimeType is {@link #APPLICATION_TTML}.
*
* @param mimeType The mimeType to test.
* @return Whether the mimeType is {@link #APPLICATION_TTML}.
*/
public static boolean isTtml(String mimeType) {
return mimeType.equals(APPLICATION_TTML);
} }
} }
...@@ -17,8 +17,13 @@ package com.google.android.exoplayer.util; ...@@ -17,8 +17,13 @@ package com.google.android.exoplayer.util;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import android.net.Uri;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
...@@ -112,4 +117,99 @@ public final class Util { ...@@ -112,4 +117,99 @@ public final class Util {
return text == null ? null : text.toLowerCase(Locale.US); return text == null ? null : text.toLowerCase(Locale.US);
} }
/**
* Like {@link Uri#parse(String)}, but discards the part of the uri that follows the final
* forward slash.
*
* @param uriString An RFC 2396-compliant, encoded uri.
* @return The parsed base uri.
*/
public static Uri parseBaseUri(String uriString) {
return Uri.parse(uriString.substring(0, uriString.lastIndexOf('/')));
}
/**
* Returns the index of the largest value in an array that is less than (or optionally equal to)
* a specified key.
* <p>
* The search is performed using a binary search algorithm, and so the array must be sorted.
*
* @param a The array to search.
* @param key The key being searched for.
* @param inclusive If the key is present in the array, whether to return the corresponding index.
* If false then the returned index corresponds to the largest value in the array that is
* strictly less than the key.
* @param stayInBounds If true, then 0 will be returned in the case that the key is smaller than
* the smallest value in the array. If false then -1 will be returned.
*/
public static int binarySearchFloor(long[] a, long key, boolean inclusive, boolean stayInBounds) {
int index = Arrays.binarySearch(a, key);
index = index < 0 ? -(index + 2) : (inclusive ? index : (index - 1));
return stayInBounds ? Math.max(0, index) : index;
}
/**
* Returns the index of the smallest value in an array that is greater than (or optionally equal
* to) a specified key.
* <p>
* The search is performed using a binary search algorithm, and so the array must be sorted.
*
* @param a The array to search.
* @param key The key being searched for.
* @param inclusive If the key is present in the array, whether to return the corresponding index.
* If false then the returned index corresponds to the smallest value in the array that is
* strictly greater than the key.
* @param stayInBounds If true, then {@code (a.length - 1)} will be returned in the case that the
* key is greater than the largest value in the array. If false then {@code a.length} will be
* returned.
*/
public static int binarySearchCeil(long[] a, long key, boolean inclusive, boolean stayInBounds) {
int index = Arrays.binarySearch(a, key);
index = index < 0 ? ~index : (inclusive ? index : (index + 1));
return stayInBounds ? Math.min(a.length - 1, index) : index;
}
/**
* Returns the index of the largest value in an list that is less than (or optionally equal to)
* a specified key.
* <p>
* The search is performed using a binary search algorithm, and so the list must be sorted.
*
* @param list The list to search.
* @param key The key being searched for.
* @param inclusive If the key is present in the list, whether to return the corresponding index.
* If false then the returned index corresponds to the largest value in the list that is
* strictly less than the key.
* @param stayInBounds If true, then 0 will be returned in the case that the key is smaller than
* the smallest value in the list. If false then -1 will be returned.
*/
public static<T> int binarySearchFloor(List<? extends Comparable<? super T>> list, T key,
boolean inclusive, boolean stayInBounds) {
int index = Collections.binarySearch(list, key);
index = index < 0 ? -(index + 2) : (inclusive ? index : (index - 1));
return stayInBounds ? Math.max(0, index) : index;
}
/**
* Returns the index of the smallest value in an list that is greater than (or optionally equal
* to) a specified key.
* <p>
* The search is performed using a binary search algorithm, and so the list must be sorted.
*
* @param list The list to search.
* @param key The key being searched for.
* @param inclusive If the key is present in the list, whether to return the corresponding index.
* If false then the returned index corresponds to the smallest value in the list that is
* strictly greater than the key.
* @param stayInBounds If true, then {@code (list.size() - 1)} will be returned in the case that
* the key is greater than the largest value in the list. If false then {@code list.size()}
* will be returned.
*/
public static<T> int binarySearchCeil(List<? extends Comparable<? super T>> list, T key,
boolean inclusive, boolean stayInBounds) {
int index = Collections.binarySearch(list, key);
index = index < 0 ? ~index : (inclusive ? index : (index + 1));
return stayInBounds ? Math.min(list.size() - 1, index) : index;
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment