getDrmSessionManagerData(DemoPlayer player,
+ MediaDrmCallback drmCallback) throws UnsupportedSchemeException {
+ StreamingDrmSessionManager streamingDrmSessionManager = new StreamingDrmSessionManager(
+ DemoUtil.WIDEVINE_UUID, player.getPlaybackLooper(), drmCallback, player.getMainHandler(),
+ player);
+ return Pair.create((DrmSessionManager) streamingDrmSessionManager,
+ getWidevineSecurityLevel(streamingDrmSessionManager) == SECURITY_LEVEL_1);
+ }
+
+ private static int getWidevineSecurityLevel(StreamingDrmSessionManager sessionManager) {
+ String securityLevelProperty = sessionManager.getPropertyString("securityLevel");
+ return securityLevelProperty.equals("L1") ? SECURITY_LEVEL_1 : securityLevelProperty
+ .equals("L3") ? SECURITY_LEVEL_3 : SECURITY_LEVEL_UNKNOWN;
+ }
+
+ }
+
+}
diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DebugTrackRenderer.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DebugTrackRenderer.java
new file mode 100644
index 00000000000..498e087d12d
--- /dev/null
+++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DebugTrackRenderer.java
@@ -0,0 +1,121 @@
+/*
+ * 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.demo.full.player;
+
+import com.google.android.exoplayer.ExoPlaybackException;
+import com.google.android.exoplayer.MediaCodecTrackRenderer;
+import com.google.android.exoplayer.TrackRenderer;
+import com.google.android.exoplayer.chunk.ChunkSampleSource;
+import com.google.android.exoplayer.chunk.Format;
+
+import android.widget.TextView;
+
+/**
+ * A {@link TrackRenderer} that periodically updates debugging information displayed by a
+ * {@link TextView}.
+ */
+/* package */ class DebugTrackRenderer extends TrackRenderer implements Runnable {
+
+ private final TextView textView;
+ private final MediaCodecTrackRenderer renderer;
+ private final ChunkSampleSource videoSampleSource;
+
+ private volatile boolean pendingFailure;
+ private volatile long currentPositionUs;
+
+ public DebugTrackRenderer(TextView textView, MediaCodecTrackRenderer renderer) {
+ this(textView, renderer, null);
+ }
+
+ public DebugTrackRenderer(TextView textView, MediaCodecTrackRenderer renderer,
+ ChunkSampleSource videoSampleSource) {
+ this.textView = textView;
+ this.renderer = renderer;
+ this.videoSampleSource = videoSampleSource;
+ }
+
+ public void injectFailure() {
+ pendingFailure = true;
+ }
+
+ @Override
+ protected boolean isEnded() {
+ return true;
+ }
+
+ @Override
+ protected boolean isReady() {
+ return true;
+ }
+
+ @Override
+ protected int doPrepare() throws ExoPlaybackException {
+ maybeFail();
+ return STATE_PREPARED;
+ }
+
+ @Override
+ protected void doSomeWork(long timeUs) throws ExoPlaybackException {
+ maybeFail();
+ if (timeUs < currentPositionUs || timeUs > currentPositionUs + 1000000) {
+ currentPositionUs = timeUs;
+ textView.post(this);
+ }
+ }
+
+ @Override
+ public void run() {
+ textView.setText(getRenderString());
+ }
+
+ private String getRenderString() {
+ return "ms(" + (currentPositionUs / 1000) + "), " + getQualityString() +
+ ", " + renderer.codecCounters.getDebugString();
+ }
+
+ private String getQualityString() {
+ Format format = videoSampleSource == null ? null : videoSampleSource.getFormat();
+ return format == null ? "null" : "height(" + format.height + "), itag(" + format.id + ")";
+ }
+
+ @Override
+ protected long getCurrentPositionUs() {
+ return currentPositionUs;
+ }
+
+ @Override
+ protected long getDurationUs() {
+ return TrackRenderer.MATCH_LONGEST;
+ }
+
+ @Override
+ protected long getBufferedPositionUs() {
+ return TrackRenderer.END_OF_TRACK;
+ }
+
+ @Override
+ protected void seekTo(long timeUs) {
+ currentPositionUs = timeUs;
+ }
+
+ private void maybeFail() throws ExoPlaybackException {
+ if (pendingFailure) {
+ pendingFailure = false;
+ throw new ExoPlaybackException("fail() was called on DebugTrackRenderer");
+ }
+ }
+
+}
diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DefaultRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DefaultRendererBuilder.java
new file mode 100644
index 00000000000..1afca8f54f6
--- /dev/null
+++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DefaultRendererBuilder.java
@@ -0,0 +1,69 @@
+/*
+ * 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.demo.full.player;
+
+import com.google.android.exoplayer.FrameworkSampleSource;
+import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
+import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
+import com.google.android.exoplayer.TrackRenderer;
+import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder;
+import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilderCallback;
+
+import android.content.Context;
+import android.media.MediaCodec;
+import android.net.Uri;
+import android.widget.TextView;
+
+/**
+ * A {@link RendererBuilder} for streams that can be read using
+ * {@link android.media.MediaExtractor}.
+ */
+public class DefaultRendererBuilder implements RendererBuilder {
+
+ private final Context context;
+ private final Uri uri;
+ private final TextView debugTextView;
+
+ public DefaultRendererBuilder(Context context, Uri uri, TextView debugTextView) {
+ this.context = context;
+ this.uri = uri;
+ this.debugTextView = debugTextView;
+ }
+
+ @Override
+ public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) {
+ // Build the video and audio renderers.
+ FrameworkSampleSource sampleSource = new FrameworkSampleSource(context, uri, null, 2);
+ MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource,
+ null, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000,
+ player.getMainHandler(), player, 50);
+ MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource,
+ null, true, player.getMainHandler(), player);
+
+ // Build the debug renderer.
+ TrackRenderer debugRenderer = debugTextView != null
+ ? new DebugTrackRenderer(debugTextView, videoRenderer)
+ : null;
+
+ // Invoke the callback.
+ TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT];
+ renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer;
+ renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer;
+ renderers[DemoPlayer.TYPE_DEBUG] = debugRenderer;
+ callback.onRenderers(null, null, renderers);
+ }
+
+}
diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java
new file mode 100644
index 00000000000..79934c712b3
--- /dev/null
+++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java
@@ -0,0 +1,577 @@
+/*
+ * 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.demo.full.player;
+
+import com.google.android.exoplayer.DummyTrackRenderer;
+import com.google.android.exoplayer.ExoPlaybackException;
+import com.google.android.exoplayer.ExoPlayer;
+import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
+import com.google.android.exoplayer.MediaCodecAudioTrackRenderer.AudioTrackInitializationException;
+import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException;
+import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
+import com.google.android.exoplayer.TrackRenderer;
+import com.google.android.exoplayer.chunk.ChunkSampleSource;
+import com.google.android.exoplayer.chunk.MultiTrackChunkSource;
+import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
+import com.google.android.exoplayer.text.TextTrackRenderer;
+import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
+import com.google.android.exoplayer.util.PlayerControl;
+
+import android.media.MediaCodec.CryptoException;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.Surface;
+
+import java.io.IOException;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * A wrapper around {@link ExoPlayer} that provides a higher level interface. It can be prepared
+ * with one of a number of {@link RendererBuilder} classes to suit different use cases (e.g. DASH,
+ * SmoothStreaming and so on).
+ */
+public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener,
+ DefaultBandwidthMeter.EventListener, MediaCodecVideoTrackRenderer.EventListener,
+ MediaCodecAudioTrackRenderer.EventListener, TextTrackRenderer.TextRenderer,
+ StreamingDrmSessionManager.EventListener {
+
+ /**
+ * Builds renderers for the player.
+ */
+ public interface RendererBuilder {
+ /**
+ * Constructs the necessary components for playback.
+ *
+ * @param player The parent player.
+ * @param callback The callback to invoke with the constructed components.
+ */
+ void buildRenderers(DemoPlayer player, RendererBuilderCallback callback);
+ }
+
+ /**
+ * A callback invoked by a {@link RendererBuilder}.
+ */
+ public interface RendererBuilderCallback {
+ /**
+ * Invoked with the results from a {@link RendererBuilder}.
+ *
+ * @param trackNames The names of the available tracks, indexed by {@link DemoPlayer} TYPE_*
+ * constants. May be null if the track names are unknown. An individual element may be null
+ * if the track names are unknown for the corresponding type.
+ * @param multiTrackSources Sources capable of switching between multiple available tracks,
+ * indexed by {@link DemoPlayer} TYPE_* constants. May be null if there are no types with
+ * multiple tracks. An individual element may be null if it does not have multiple tracks.
+ * @param renderers Renderers indexed by {@link DemoPlayer} TYPE_* constants. An individual
+ * element may be null if there do not exist tracks of the corresponding type.
+ */
+ void onRenderers(String[][] trackNames, MultiTrackChunkSource[] multiTrackSources,
+ TrackRenderer[] renderers);
+ /**
+ * Invoked if a {@link RendererBuilder} encounters an error.
+ *
+ * @param e Describes the error.
+ */
+ void onRenderersError(Exception e);
+ }
+
+ /**
+ * A listener for core events.
+ */
+ public interface Listener {
+ void onStateChanged(boolean playWhenReady, int playbackState);
+ void onError(Exception e);
+ void onVideoSizeChanged(int width, int height);
+ }
+
+ /**
+ * A listener for internal errors.
+ *
+ * These errors are not visible to the user, and hence this listener is provided for
+ * informational purposes only. Note however that an internal error may cause a fatal
+ * error if the player fails to recover. If this happens, {@link Listener#onError(Exception)}
+ * will be invoked.
+ */
+ public interface InternalErrorListener {
+ void onRendererInitializationError(Exception e);
+ void onAudioTrackInitializationError(AudioTrackInitializationException e);
+ void onDecoderInitializationError(DecoderInitializationException e);
+ void onCryptoError(CryptoException e);
+ void onUpstreamError(int sourceId, IOException e);
+ void onConsumptionError(int sourceId, IOException e);
+ void onDrmSessionManagerError(Exception e);
+ }
+
+ /**
+ * A listener for debugging information.
+ */
+ public interface InfoListener {
+ void onVideoFormatEnabled(int formatId, int trigger, int mediaTimeMs);
+ void onAudioFormatEnabled(int formatId, int trigger, int mediaTimeMs);
+ void onDroppedFrames(int count, long elapsed);
+ void onBandwidthSample(int elapsedMs, long bytes, long bandwidthEstimate);
+ void onLoadStarted(int sourceId, int formatId, int trigger, boolean isInitialization,
+ int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes);
+ void onLoadCompleted(int sourceId);
+ }
+
+ /**
+ * A listener for receiving notifications of timed text.
+ */
+ public interface TextListener {
+ public abstract void onText(String text);
+ }
+
+ // Constants pulled into this class for convenience.
+ public static final int STATE_IDLE = ExoPlayer.STATE_IDLE;
+ public static final int STATE_PREPARING = ExoPlayer.STATE_PREPARING;
+ public static final int STATE_BUFFERING = ExoPlayer.STATE_BUFFERING;
+ public static final int STATE_READY = ExoPlayer.STATE_READY;
+ public static final int STATE_ENDED = ExoPlayer.STATE_ENDED;
+
+ public static final int DISABLED_TRACK = -1;
+ public static final int PRIMARY_TRACK = 0;
+
+ public static final int RENDERER_COUNT = 4;
+ public static final int TYPE_VIDEO = 0;
+ public static final int TYPE_AUDIO = 1;
+ public static final int TYPE_TEXT = 2;
+ public static final int TYPE_DEBUG = 3;
+
+ private static final int RENDERER_BUILDING_STATE_IDLE = 1;
+ private static final int RENDERER_BUILDING_STATE_BUILDING = 2;
+ private static final int RENDERER_BUILDING_STATE_BUILT = 3;
+
+ private final RendererBuilder rendererBuilder;
+ private final ExoPlayer player;
+ private final PlayerControl playerControl;
+ private final Handler mainHandler;
+ private final CopyOnWriteArrayList listeners;
+
+ private int rendererBuildingState;
+ private int lastReportedPlaybackState;
+ private boolean lastReportedPlayWhenReady;
+
+ private Surface surface;
+ private InternalRendererBuilderCallback builderCallback;
+ private TrackRenderer videoRenderer;
+
+ private MultiTrackChunkSource[] multiTrackSources;
+ private String[][] trackNames;
+ private int[] selectedTracks;
+
+ private TextListener textListener;
+ private InternalErrorListener internalErrorListener;
+ private InfoListener infoListener;
+
+ public DemoPlayer(RendererBuilder rendererBuilder) {
+ this.rendererBuilder = rendererBuilder;
+ player = ExoPlayer.Factory.newInstance(RENDERER_COUNT, 1000, 5000);
+ player.addListener(this);
+ playerControl = new PlayerControl(player);
+ mainHandler = new Handler();
+ listeners = new CopyOnWriteArrayList();
+ lastReportedPlaybackState = STATE_IDLE;
+ rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
+ selectedTracks = new int[RENDERER_COUNT];
+ // Disable text initially.
+ selectedTracks[TYPE_TEXT] = DISABLED_TRACK;
+ }
+
+ public PlayerControl getPlayerControl() {
+ return playerControl;
+ }
+
+ public void addListener(Listener listener) {
+ listeners.add(listener);
+ }
+
+ public void removeListener(Listener listener) {
+ listeners.remove(listener);
+ }
+
+ public void setInternalErrorListener(InternalErrorListener listener) {
+ internalErrorListener = listener;
+ }
+
+ public void setInfoListener(InfoListener listener) {
+ infoListener = listener;
+ }
+
+ public void setTextListener(TextListener listener) {
+ textListener = listener;
+ }
+
+ public void setSurface(Surface surface) {
+ this.surface = surface;
+ pushSurfaceAndVideoTrack(false);
+ }
+
+ public Surface getSurface() {
+ return surface;
+ }
+
+ public void blockingClearSurface() {
+ surface = null;
+ pushSurfaceAndVideoTrack(true);
+ }
+
+ public String[] getTracks(int type) {
+ return trackNames == null ? null : trackNames[type];
+ }
+
+ public int getSelectedTrackIndex(int type) {
+ return selectedTracks[type];
+ }
+
+ public void selectTrack(int type, int index) {
+ if (selectedTracks[type] == index) {
+ return;
+ }
+ selectedTracks[type] = index;
+ if (type == TYPE_VIDEO) {
+ pushSurfaceAndVideoTrack(false);
+ } else {
+ pushTrackSelection(type, true);
+ }
+ }
+
+ public void prepare() {
+ if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT) {
+ player.stop();
+ }
+ if (builderCallback != null) {
+ builderCallback.cancel();
+ }
+ rendererBuildingState = RENDERER_BUILDING_STATE_BUILDING;
+ maybeReportPlayerState();
+ builderCallback = new InternalRendererBuilderCallback();
+ rendererBuilder.buildRenderers(this, builderCallback);
+ }
+
+ /* package */ void onRenderers(String[][] trackNames,
+ MultiTrackChunkSource[] multiTrackSources, TrackRenderer[] renderers) {
+ builderCallback = null;
+ // Normalize the results.
+ if (trackNames == null) {
+ trackNames = new String[RENDERER_COUNT][];
+ }
+ if (multiTrackSources == null) {
+ multiTrackSources = new MultiTrackChunkSource[RENDERER_COUNT];
+ }
+ for (int i = 0; i < RENDERER_COUNT; i++) {
+ if (renderers[i] == null) {
+ // Convert a null renderer to a dummy renderer.
+ renderers[i] = new DummyTrackRenderer();
+ } else if (trackNames[i] == null) {
+ // We have a renderer so we must have at least one track, but the names are unknown.
+ // Initialize the correct number of null track names.
+ int trackCount = multiTrackSources[i] == null ? 1 : multiTrackSources[i].getTrackCount();
+ trackNames[i] = new String[trackCount];
+ }
+ }
+ // Complete preparation.
+ this.videoRenderer = renderers[TYPE_VIDEO];
+ this.trackNames = trackNames;
+ this.multiTrackSources = multiTrackSources;
+ rendererBuildingState = RENDERER_BUILDING_STATE_BUILT;
+ maybeReportPlayerState();
+ pushSurfaceAndVideoTrack(false);
+ pushTrackSelection(TYPE_AUDIO, true);
+ pushTrackSelection(TYPE_TEXT, true);
+ player.prepare(renderers);
+ }
+
+ /* package */ void onRenderersError(Exception e) {
+ builderCallback = null;
+ if (internalErrorListener != null) {
+ internalErrorListener.onRendererInitializationError(e);
+ }
+ for (Listener listener : listeners) {
+ listener.onError(e);
+ }
+ rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
+ maybeReportPlayerState();
+ }
+
+ public void setPlayWhenReady(boolean playWhenReady) {
+ player.setPlayWhenReady(playWhenReady);
+ }
+
+ public void seekTo(int positionMs) {
+ player.seekTo(positionMs);
+ }
+
+ public void release() {
+ if (builderCallback != null) {
+ builderCallback.cancel();
+ builderCallback = null;
+ }
+ rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
+ surface = null;
+ player.release();
+ }
+
+
+ public int getPlaybackState() {
+ if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILDING) {
+ return ExoPlayer.STATE_PREPARING;
+ }
+ int playerState = player.getPlaybackState();
+ if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT
+ && rendererBuildingState == RENDERER_BUILDING_STATE_IDLE) {
+ // This is an edge case where the renderers are built, but are still being passed to the
+ // player's playback thread.
+ return ExoPlayer.STATE_PREPARING;
+ }
+ return playerState;
+ }
+
+ public int getCurrentPosition() {
+ return player.getCurrentPosition();
+ }
+
+ public int getDuration() {
+ return player.getDuration();
+ }
+
+ public int getBufferedPercentage() {
+ return player.getBufferedPercentage();
+ }
+
+ public boolean getPlayWhenReady() {
+ return player.getPlayWhenReady();
+ }
+
+ /* package */ Looper getPlaybackLooper() {
+ return player.getPlaybackLooper();
+ }
+
+ /* package */ Handler getMainHandler() {
+ return mainHandler;
+ }
+
+ @Override
+ public void onPlayerStateChanged(boolean playWhenReady, int state) {
+ maybeReportPlayerState();
+ }
+
+ @Override
+ public void onPlayerError(ExoPlaybackException exception) {
+ rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
+ for (Listener listener : listeners) {
+ listener.onError(exception);
+ }
+ }
+
+ @Override
+ public void onVideoSizeChanged(int width, int height) {
+ for (Listener listener : listeners) {
+ listener.onVideoSizeChanged(width, height);
+ }
+ }
+
+ @Override
+ public void onDroppedFrames(int count, long elapsed) {
+ if (infoListener != null) {
+ infoListener.onDroppedFrames(count, elapsed);
+ }
+ }
+
+ @Override
+ public void onBandwidthSample(int elapsedMs, long bytes, long bandwidthEstimate) {
+ if (infoListener != null) {
+ infoListener.onBandwidthSample(elapsedMs, bytes, bandwidthEstimate);
+ }
+ }
+
+ @Override
+ public void onDownstreamFormatChanged(int sourceId, int formatId, int trigger, int mediaTimeMs) {
+ if (infoListener == null) {
+ return;
+ }
+ if (sourceId == TYPE_VIDEO) {
+ infoListener.onVideoFormatEnabled(formatId, trigger, mediaTimeMs);
+ } else if (sourceId == TYPE_AUDIO) {
+ infoListener.onAudioFormatEnabled(formatId, trigger, mediaTimeMs);
+ }
+ }
+
+ @Override
+ public void onDrmSessionManagerError(Exception e) {
+ if (internalErrorListener != null) {
+ internalErrorListener.onDrmSessionManagerError(e);
+ }
+ }
+
+ @Override
+ public void onDecoderInitializationError(DecoderInitializationException e) {
+ if (internalErrorListener != null) {
+ internalErrorListener.onDecoderInitializationError(e);
+ }
+ }
+
+ @Override
+ public void onAudioTrackInitializationError(AudioTrackInitializationException e) {
+ if (internalErrorListener != null) {
+ internalErrorListener.onAudioTrackInitializationError(e);
+ }
+ }
+
+ @Override
+ public void onCryptoError(CryptoException e) {
+ if (internalErrorListener != null) {
+ internalErrorListener.onCryptoError(e);
+ }
+ }
+
+ @Override
+ public void onUpstreamError(int sourceId, IOException e) {
+ if (internalErrorListener != null) {
+ internalErrorListener.onUpstreamError(sourceId, e);
+ }
+ }
+
+ @Override
+ public void onConsumptionError(int sourceId, IOException e) {
+ if (internalErrorListener != null) {
+ internalErrorListener.onConsumptionError(sourceId, e);
+ }
+ }
+
+ @Override
+ public void onText(String text) {
+ if (textListener != null) {
+ textListener.onText(text);
+ }
+ }
+
+ @Override
+ public void onPlayWhenReadyCommitted() {
+ // Do nothing.
+ }
+
+ @Override
+ public void onDrawnToSurface(Surface surface) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onLoadStarted(int sourceId, int formatId, int trigger, boolean isInitialization,
+ int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes) {
+ if (infoListener != null) {
+ infoListener.onLoadStarted(sourceId, formatId, trigger, isInitialization, mediaStartTimeMs,
+ mediaEndTimeMs, totalBytes);
+ }
+ }
+
+ @Override
+ public void onLoadCompleted(int sourceId) {
+ if (infoListener != null) {
+ infoListener.onLoadCompleted(sourceId);
+ }
+ }
+
+ @Override
+ public void onLoadCanceled(int sourceId) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onUpstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs,
+ long totalBytes) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onDownstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs,
+ long totalBytes) {
+ // Do nothing.
+ }
+
+ private void maybeReportPlayerState() {
+ boolean playWhenReady = player.getPlayWhenReady();
+ int playbackState = getPlaybackState();
+ if (lastReportedPlayWhenReady != playWhenReady || lastReportedPlaybackState != playbackState) {
+ for (Listener listener : listeners) {
+ listener.onStateChanged(playWhenReady, playbackState);
+ }
+ lastReportedPlayWhenReady = playWhenReady;
+ lastReportedPlaybackState = playbackState;
+ }
+ }
+
+ private void pushSurfaceAndVideoTrack(boolean blockForSurfacePush) {
+ if (rendererBuildingState != RENDERER_BUILDING_STATE_BUILT) {
+ return;
+ }
+
+ if (blockForSurfacePush) {
+ player.blockingSendMessage(
+ videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface);
+ } else {
+ player.sendMessage(
+ videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface);
+ }
+ pushTrackSelection(TYPE_VIDEO, surface != null && surface.isValid());
+ }
+
+ private void pushTrackSelection(int type, boolean allowRendererEnable) {
+ if (rendererBuildingState != RENDERER_BUILDING_STATE_BUILT) {
+ return;
+ }
+
+ int trackIndex = selectedTracks[type];
+ if (trackIndex == DISABLED_TRACK) {
+ player.setRendererEnabled(type, false);
+ } else if (multiTrackSources[type] == null) {
+ player.setRendererEnabled(type, allowRendererEnable);
+ } else {
+ boolean playWhenReady = player.getPlayWhenReady();
+ player.setPlayWhenReady(false);
+ player.setRendererEnabled(type, false);
+ player.sendMessage(multiTrackSources[type], MultiTrackChunkSource.MSG_SELECT_TRACK,
+ trackIndex);
+ player.setRendererEnabled(type, allowRendererEnable);
+ player.setPlayWhenReady(playWhenReady);
+ }
+ }
+
+ private class InternalRendererBuilderCallback implements RendererBuilderCallback {
+
+ private boolean canceled;
+
+ public void cancel() {
+ canceled = true;
+ }
+
+ @Override
+ public void onRenderers(String[][] trackNames, MultiTrackChunkSource[] multiTrackSources,
+ TrackRenderer[] renderers) {
+ if (!canceled) {
+ DemoPlayer.this.onRenderers(trackNames, multiTrackSources, renderers);
+ }
+ }
+
+ @Override
+ public void onRenderersError(Exception e) {
+ if (!canceled) {
+ DemoPlayer.this.onRenderersError(e);
+ }
+ }
+
+ }
+
+}
diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java
new file mode 100644
index 00000000000..ea4dccee67f
--- /dev/null
+++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java
@@ -0,0 +1,261 @@
+/*
+ * 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.demo.full.player;
+
+import com.google.android.exoplayer.DefaultLoadControl;
+import com.google.android.exoplayer.LoadControl;
+import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
+import com.google.android.exoplayer.MediaCodecUtil;
+import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
+import com.google.android.exoplayer.TrackRenderer;
+import com.google.android.exoplayer.chunk.ChunkSampleSource;
+import com.google.android.exoplayer.chunk.ChunkSource;
+import com.google.android.exoplayer.chunk.FormatEvaluator;
+import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator;
+import com.google.android.exoplayer.chunk.MultiTrackChunkSource;
+import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder;
+import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilderCallback;
+import com.google.android.exoplayer.drm.DrmSessionManager;
+import com.google.android.exoplayer.drm.MediaDrmCallback;
+import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
+import com.google.android.exoplayer.smoothstreaming.SmoothStreamingChunkSource;
+import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest;
+import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement;
+import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement;
+import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifestFetcher;
+import com.google.android.exoplayer.text.TextTrackRenderer;
+import com.google.android.exoplayer.text.ttml.TtmlParser;
+import com.google.android.exoplayer.upstream.BufferPool;
+import com.google.android.exoplayer.upstream.DataSource;
+import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
+import com.google.android.exoplayer.upstream.HttpDataSource;
+import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
+import com.google.android.exoplayer.util.Util;
+
+import android.annotation.TargetApi;
+import android.media.MediaCodec;
+import android.media.UnsupportedSchemeException;
+import android.os.Handler;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.UUID;
+
+/**
+ * A {@link RendererBuilder} for SmoothStreaming.
+ */
+public class SmoothStreamingRendererBuilder implements RendererBuilder,
+ ManifestCallback {
+
+ private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
+ private static final int VIDEO_BUFFER_SEGMENTS = 200;
+ private static final int AUDIO_BUFFER_SEGMENTS = 60;
+ private static final int TTML_BUFFER_SEGMENTS = 2;
+
+ private final String userAgent;
+ private final String url;
+ private final String contentId;
+ private final MediaDrmCallback drmCallback;
+ private final TextView debugTextView;
+
+ private DemoPlayer player;
+ private RendererBuilderCallback callback;
+
+ public SmoothStreamingRendererBuilder(String userAgent, String url, String contentId,
+ MediaDrmCallback drmCallback, TextView debugTextView) {
+ this.userAgent = userAgent;
+ this.url = url;
+ this.contentId = contentId;
+ this.drmCallback = drmCallback;
+ this.debugTextView = debugTextView;
+ }
+
+ @Override
+ public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) {
+ this.player = player;
+ this.callback = callback;
+ SmoothStreamingManifestFetcher mpdFetcher = new SmoothStreamingManifestFetcher(this);
+ mpdFetcher.execute(url + "/Manifest", contentId);
+ }
+
+ @Override
+ public void onManifestError(String contentId, Exception e) {
+ callback.onRenderersError(e);
+ }
+
+ @Override
+ public void onManifest(String contentId, SmoothStreamingManifest manifest) {
+ Handler mainHandler = player.getMainHandler();
+ LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE));
+ DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player);
+
+ // Check drm support if necessary.
+ DrmSessionManager drmSessionManager = null;
+ if (manifest.protectionElement != null) {
+ if (Util.SDK_INT < 18) {
+ callback.onRenderersError(new UnsupportedOperationException(
+ "Protected content not supported on API level " + Util.SDK_INT));
+ return;
+ }
+ try {
+ drmSessionManager = V18Compat.getDrmSessionManager(manifest.protectionElement.uuid, player,
+ drmCallback);
+ } catch (UnsupportedSchemeException e) {
+ callback.onRenderersError(e);
+ return;
+ }
+ }
+
+ // Obtain stream elements for playback.
+ int maxDecodableFrameSize = MediaCodecUtil.maxH264DecodableFrameSize();
+ int audioStreamElementCount = 0;
+ int textStreamElementCount = 0;
+ int videoStreamElementIndex = -1;
+ ArrayList videoTrackIndexList = new ArrayList();
+ for (int i = 0; i < manifest.streamElements.length; i++) {
+ if (manifest.streamElements[i].type == StreamElement.TYPE_AUDIO) {
+ audioStreamElementCount++;
+ } else if (manifest.streamElements[i].type == StreamElement.TYPE_TEXT) {
+ textStreamElementCount++;
+ } else if (videoStreamElementIndex == -1
+ && manifest.streamElements[i].type == StreamElement.TYPE_VIDEO) {
+ videoStreamElementIndex = i;
+ StreamElement streamElement = manifest.streamElements[i];
+ for (int j = 0; j < streamElement.tracks.length; j++) {
+ TrackElement trackElement = streamElement.tracks[j];
+ if (trackElement.maxWidth * trackElement.maxHeight <= maxDecodableFrameSize) {
+ videoTrackIndexList.add(j);
+ } else {
+ // The device isn't capable of playing this stream.
+ }
+ }
+ }
+ }
+ int[] videoTrackIndices = new int[videoTrackIndexList.size()];
+ for (int i = 0; i < videoTrackIndexList.size(); i++) {
+ videoTrackIndices[i] = videoTrackIndexList.get(i);
+ }
+
+ // Build the video renderer.
+ DataSource videoDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES,
+ bandwidthMeter);
+ ChunkSource videoChunkSource = new SmoothStreamingChunkSource(url, manifest,
+ videoStreamElementIndex, videoTrackIndices, videoDataSource,
+ new AdaptiveEvaluator(bandwidthMeter));
+ ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
+ VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
+ DemoPlayer.TYPE_VIDEO);
+ MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource,
+ drmSessionManager, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000,
+ mainHandler, player, 50);
+
+ // Build the audio renderer.
+ final String[] audioTrackNames;
+ final MultiTrackChunkSource audioChunkSource;
+ final MediaCodecAudioTrackRenderer audioRenderer;
+ if (audioStreamElementCount == 0) {
+ audioTrackNames = null;
+ audioChunkSource = null;
+ audioRenderer = null;
+ } else {
+ audioTrackNames = new String[audioStreamElementCount];
+ ChunkSource[] audioChunkSources = new ChunkSource[audioStreamElementCount];
+ DataSource audioDataSource = new HttpDataSource(userAgent,
+ HttpDataSource.REJECT_PAYWALL_TYPES, bandwidthMeter);
+ FormatEvaluator audioFormatEvaluator = new FormatEvaluator.FixedEvaluator();
+ audioStreamElementCount = 0;
+ for (int i = 0; i < manifest.streamElements.length; i++) {
+ if (manifest.streamElements[i].type == StreamElement.TYPE_AUDIO) {
+ audioTrackNames[audioStreamElementCount] = manifest.streamElements[i].name;
+ audioChunkSources[audioStreamElementCount] = new SmoothStreamingChunkSource(url, manifest,
+ i, new int[] {0}, audioDataSource, audioFormatEvaluator);
+ audioStreamElementCount++;
+ }
+ }
+ audioChunkSource = new MultiTrackChunkSource(audioChunkSources);
+ ChunkSampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
+ AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
+ DemoPlayer.TYPE_AUDIO);
+ audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource, drmSessionManager, true,
+ mainHandler, player);
+ }
+
+ // Build the text renderer.
+ final String[] textTrackNames;
+ final MultiTrackChunkSource textChunkSource;
+ final TrackRenderer textRenderer;
+ if (textStreamElementCount == 0) {
+ textTrackNames = null;
+ textChunkSource = null;
+ textRenderer = null;
+ } else {
+ textTrackNames = new String[textStreamElementCount];
+ ChunkSource[] textChunkSources = new ChunkSource[textStreamElementCount];
+ DataSource ttmlDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES,
+ bandwidthMeter);
+ FormatEvaluator ttmlFormatEvaluator = new FormatEvaluator.FixedEvaluator();
+ textStreamElementCount = 0;
+ for (int i = 0; i < manifest.streamElements.length; i++) {
+ if (manifest.streamElements[i].type == StreamElement.TYPE_TEXT) {
+ textTrackNames[textStreamElementCount] = manifest.streamElements[i].language;
+ textChunkSources[textStreamElementCount] = new SmoothStreamingChunkSource(url, manifest,
+ i, new int[] {0}, ttmlDataSource, ttmlFormatEvaluator);
+ textStreamElementCount++;
+ }
+ }
+ textChunkSource = new MultiTrackChunkSource(textChunkSources);
+ ChunkSampleSource ttmlSampleSource = new ChunkSampleSource(textChunkSource, loadControl,
+ TTML_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
+ DemoPlayer.TYPE_TEXT);
+ textRenderer = new TextTrackRenderer(ttmlSampleSource, new TtmlParser(), player,
+ mainHandler.getLooper());
+ }
+
+ // Build the debug renderer.
+ TrackRenderer debugRenderer = debugTextView != null
+ ? new DebugTrackRenderer(debugTextView, videoRenderer, videoSampleSource)
+ : null;
+
+ // Invoke the callback.
+ String[][] trackNames = new String[DemoPlayer.RENDERER_COUNT][];
+ trackNames[DemoPlayer.TYPE_AUDIO] = audioTrackNames;
+ trackNames[DemoPlayer.TYPE_TEXT] = textTrackNames;
+
+ MultiTrackChunkSource[] multiTrackChunkSources =
+ new MultiTrackChunkSource[DemoPlayer.RENDERER_COUNT];
+ multiTrackChunkSources[DemoPlayer.TYPE_AUDIO] = audioChunkSource;
+ multiTrackChunkSources[DemoPlayer.TYPE_TEXT] = textChunkSource;
+
+ TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT];
+ renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer;
+ renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer;
+ renderers[DemoPlayer.TYPE_TEXT] = textRenderer;
+ renderers[DemoPlayer.TYPE_DEBUG] = debugRenderer;
+ callback.onRenderers(trackNames, multiTrackChunkSources, renderers);
+ }
+
+ @TargetApi(18)
+ private static class V18Compat {
+
+ public static DrmSessionManager getDrmSessionManager(UUID uuid, DemoPlayer player,
+ MediaDrmCallback drmCallback) throws UnsupportedSchemeException {
+ return new StreamingDrmSessionManager(uuid, player.getPlaybackLooper(), drmCallback,
+ player.getMainHandler(), player);
+ }
+
+ }
+
+}
diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java
new file mode 100644
index 00000000000..ec5bde031f8
--- /dev/null
+++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java
@@ -0,0 +1,139 @@
+/*
+ * 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.demo.simple;
+
+import com.google.android.exoplayer.DefaultLoadControl;
+import com.google.android.exoplayer.LoadControl;
+import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
+import com.google.android.exoplayer.MediaCodecUtil;
+import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
+import com.google.android.exoplayer.SampleSource;
+import com.google.android.exoplayer.chunk.ChunkSampleSource;
+import com.google.android.exoplayer.chunk.ChunkSource;
+import com.google.android.exoplayer.chunk.Format;
+import com.google.android.exoplayer.chunk.FormatEvaluator;
+import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator;
+import com.google.android.exoplayer.dash.DashMp4ChunkSource;
+import com.google.android.exoplayer.dash.mpd.AdaptationSet;
+import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
+import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionFetcher;
+import com.google.android.exoplayer.dash.mpd.Period;
+import com.google.android.exoplayer.dash.mpd.Representation;
+import com.google.android.exoplayer.demo.simple.SimplePlayerActivity.RendererBuilder;
+import com.google.android.exoplayer.demo.simple.SimplePlayerActivity.RendererBuilderCallback;
+import com.google.android.exoplayer.upstream.BufferPool;
+import com.google.android.exoplayer.upstream.DataSource;
+import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
+import com.google.android.exoplayer.upstream.HttpDataSource;
+import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
+
+import android.media.MediaCodec;
+import android.os.AsyncTask;
+import android.os.Handler;
+
+import java.util.ArrayList;
+
+/**
+ * A {@link RendererBuilder} for DASH VOD.
+ */
+/* package */ class DashVodRendererBuilder implements RendererBuilder,
+ ManifestCallback {
+
+ private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
+ private static final int VIDEO_BUFFER_SEGMENTS = 200;
+ private static final int AUDIO_BUFFER_SEGMENTS = 60;
+
+ private final SimplePlayerActivity playerActivity;
+ private final String userAgent;
+ private final String url;
+ private final String contentId;
+
+ private RendererBuilderCallback callback;
+
+ public DashVodRendererBuilder(SimplePlayerActivity playerActivity, String userAgent, String url,
+ String contentId) {
+ this.playerActivity = playerActivity;
+ this.userAgent = userAgent;
+ this.url = url;
+ this.contentId = contentId;
+ }
+
+ @Override
+ public void buildRenderers(RendererBuilderCallback callback) {
+ this.callback = callback;
+ MediaPresentationDescriptionFetcher mpdFetcher = new MediaPresentationDescriptionFetcher(this);
+ mpdFetcher.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url, contentId);
+ }
+
+ @Override
+ public void onManifestError(String contentId, Exception e) {
+ callback.onRenderersError(e);
+ }
+
+ @Override
+ public void onManifest(String contentId, MediaPresentationDescription manifest) {
+ Handler mainHandler = playerActivity.getMainHandler();
+ LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE));
+ DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
+
+ // Obtain Representations for playback.
+ int maxDecodableFrameSize = MediaCodecUtil.maxH264DecodableFrameSize();
+ Representation audioRepresentation = null;
+ ArrayList videoRepresentationsList = new ArrayList();
+ Period period = manifest.periods.get(0);
+ for (int i = 0; i < period.adaptationSets.size(); i++) {
+ AdaptationSet adaptationSet = period.adaptationSets.get(i);
+ int adaptationSetType = adaptationSet.type;
+ for (int j = 0; j < adaptationSet.representations.size(); j++) {
+ Representation representation = adaptationSet.representations.get(j);
+ if (audioRepresentation == null && adaptationSetType == AdaptationSet.TYPE_AUDIO) {
+ audioRepresentation = representation;
+ } else if (adaptationSetType == AdaptationSet.TYPE_VIDEO) {
+ Format format = representation.format;
+ if (format.width * format.height <= maxDecodableFrameSize) {
+ videoRepresentationsList.add(representation);
+ } else {
+ // The device isn't capable of playing this stream.
+ }
+ }
+ }
+ }
+ Representation[] videoRepresentations = new Representation[videoRepresentationsList.size()];
+ videoRepresentationsList.toArray(videoRepresentations);
+
+ // Build the video renderer.
+ DataSource videoDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES,
+ bandwidthMeter);
+ ChunkSource videoChunkSource = new DashMp4ChunkSource(videoDataSource,
+ new AdaptiveEvaluator(bandwidthMeter), videoRepresentations);
+ ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
+ VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
+ MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource,
+ MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50);
+
+ // Build the audio renderer.
+ DataSource audioDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES,
+ bandwidthMeter);
+ ChunkSource audioChunkSource = new DashMp4ChunkSource(audioDataSource,
+ new FormatEvaluator.FixedEvaluator(), audioRepresentation);
+ SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
+ AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
+ MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(
+ audioSampleSource);
+ callback.onRenderers(videoRenderer, audioRenderer);
+ }
+
+}
diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/DefaultRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/DefaultRendererBuilder.java
new file mode 100644
index 00000000000..aef46d38d96
--- /dev/null
+++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/DefaultRendererBuilder.java
@@ -0,0 +1,54 @@
+/*
+ * 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.demo.simple;
+
+import com.google.android.exoplayer.FrameworkSampleSource;
+import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
+import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
+import com.google.android.exoplayer.demo.simple.SimplePlayerActivity.RendererBuilder;
+import com.google.android.exoplayer.demo.simple.SimplePlayerActivity.RendererBuilderCallback;
+
+import android.media.MediaCodec;
+import android.net.Uri;
+
+/**
+ * A {@link RendererBuilder} for streams that can be read using
+ * {@link android.media.MediaExtractor}.
+ */
+/* package */ class DefaultRendererBuilder implements RendererBuilder {
+
+ private final SimplePlayerActivity playerActivity;
+ private final Uri uri;
+
+ public DefaultRendererBuilder(SimplePlayerActivity playerActivity, Uri uri) {
+ this.playerActivity = playerActivity;
+ this.uri = uri;
+ }
+
+ @Override
+ public void buildRenderers(RendererBuilderCallback callback) {
+ // Build the video and audio renderers.
+ FrameworkSampleSource sampleSource = new FrameworkSampleSource(playerActivity, uri, null, 2);
+ MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource,
+ MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, playerActivity.getMainHandler(),
+ playerActivity, 50);
+ MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource);
+
+ // Invoke the callback.
+ callback.onRenderers(videoRenderer, audioRenderer);
+ }
+
+}
diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java
new file mode 100644
index 00000000000..fa66b5c5523
--- /dev/null
+++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SimplePlayerActivity.java
@@ -0,0 +1,290 @@
+/*
+ * 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.demo.simple;
+
+import com.google.android.exoplayer.ExoPlaybackException;
+import com.google.android.exoplayer.ExoPlayer;
+import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
+import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException;
+import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
+import com.google.android.exoplayer.VideoSurfaceView;
+import com.google.android.exoplayer.demo.DemoUtil;
+import com.google.android.exoplayer.demo.R;
+import com.google.android.exoplayer.util.PlayerControl;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.media.MediaCodec.CryptoException;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.widget.MediaController;
+import android.widget.Toast;
+
+/**
+ * An activity that plays media using {@link ExoPlayer}.
+ */
+public class SimplePlayerActivity extends Activity implements SurfaceHolder.Callback,
+ ExoPlayer.Listener, MediaCodecVideoTrackRenderer.EventListener {
+
+ /**
+ * Builds renderers for the player.
+ */
+ public interface RendererBuilder {
+
+ void buildRenderers(RendererBuilderCallback callback);
+
+ }
+
+ public static final int RENDERER_COUNT = 2;
+ public static final int TYPE_VIDEO = 0;
+ public static final int TYPE_AUDIO = 1;
+
+ private static final String TAG = "PlayerActivity";
+
+ public static final int TYPE_DASH_VOD = 0;
+ public static final int TYPE_SS_VOD = 1;
+ public static final int TYPE_OTHER = 2;
+
+ private MediaController mediaController;
+ private Handler mainHandler;
+ private View shutterView;
+ private VideoSurfaceView surfaceView;
+
+ private ExoPlayer player;
+ private RendererBuilder builder;
+ private RendererBuilderCallback callback;
+ private MediaCodecVideoTrackRenderer videoRenderer;
+
+ private boolean autoPlay = true;
+ private int playerPosition;
+
+ private Uri contentUri;
+ private int contentType;
+ private String contentId;
+
+ // Activity lifecycle
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Intent intent = getIntent();
+ contentUri = intent.getData();
+ contentType = intent.getIntExtra(DemoUtil.CONTENT_TYPE_EXTRA, TYPE_OTHER);
+ contentId = intent.getStringExtra(DemoUtil.CONTENT_ID_EXTRA);
+
+ mainHandler = new Handler(getMainLooper());
+ builder = getRendererBuilder();
+
+ setContentView(R.layout.player_activity_simple);
+ View root = findViewById(R.id.root);
+ root.setOnTouchListener(new OnTouchListener() {
+ @Override
+ public boolean onTouch(View arg0, MotionEvent arg1) {
+ if (arg1.getAction() == MotionEvent.ACTION_DOWN) {
+ toggleControlsVisibility();
+ }
+ return true;
+ }
+ });
+
+ mediaController = new MediaController(this);
+ mediaController.setAnchorView(root);
+ shutterView = findViewById(R.id.shutter);
+ surfaceView = (VideoSurfaceView) findViewById(R.id.surface_view);
+ surfaceView.getHolder().addCallback(this);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ // Setup the player
+ player = ExoPlayer.Factory.newInstance(RENDERER_COUNT, 1000, 5000);
+ player.addListener(this);
+ player.seekTo(playerPosition);
+ // Build the player controls
+ mediaController.setMediaPlayer(new PlayerControl(player));
+ mediaController.setEnabled(true);
+ // Request the renderers
+ callback = new RendererBuilderCallback();
+ builder.buildRenderers(callback);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ // Release the player
+ if (player != null) {
+ playerPosition = player.getCurrentPosition();
+ player.release();
+ player = null;
+ }
+ callback = null;
+ videoRenderer = null;
+ shutterView.setVisibility(View.VISIBLE);
+ }
+
+ // Public methods
+
+ public Handler getMainHandler() {
+ return mainHandler;
+ }
+
+ // Internal methods
+
+ private void toggleControlsVisibility() {
+ if (mediaController.isShowing()) {
+ mediaController.hide();
+ } else {
+ mediaController.show(0);
+ }
+ }
+
+ private RendererBuilder getRendererBuilder() {
+ String userAgent = DemoUtil.getUserAgent(this);
+ switch (contentType) {
+ case TYPE_SS_VOD:
+ return new SmoothStreamingRendererBuilder(this, userAgent, contentUri.toString(),
+ contentId);
+ case TYPE_DASH_VOD:
+ return new DashVodRendererBuilder(this, userAgent, contentUri.toString(), contentId);
+ default:
+ return new DefaultRendererBuilder(this, contentUri);
+ }
+ }
+
+ private void onRenderers(RendererBuilderCallback callback,
+ MediaCodecVideoTrackRenderer videoRenderer, MediaCodecAudioTrackRenderer audioRenderer) {
+ if (this.callback != callback) {
+ return;
+ }
+ this.callback = null;
+ this.videoRenderer = videoRenderer;
+ player.prepare(videoRenderer, audioRenderer);
+ maybeStartPlayback();
+ }
+
+ private void maybeStartPlayback() {
+ Surface surface = surfaceView.getHolder().getSurface();
+ if (videoRenderer == null || surface == null || !surface.isValid()) {
+ // We're not ready yet.
+ return;
+ }
+ player.sendMessage(videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface);
+ if (autoPlay) {
+ player.setPlayWhenReady(true);
+ autoPlay = false;
+ }
+ }
+
+ private void onRenderersError(RendererBuilderCallback callback, Exception e) {
+ if (this.callback != callback) {
+ return;
+ }
+ this.callback = null;
+ onError(e);
+ }
+
+ private void onError(Exception e) {
+ Log.e(TAG, "Playback failed", e);
+ Toast.makeText(this, R.string.failed, Toast.LENGTH_SHORT).show();
+ finish();
+ }
+
+ // ExoPlayer.Listener implementation
+
+ @Override
+ public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onPlayWhenReadyCommitted() {
+ // Do nothing.
+ }
+
+ @Override
+ public void onPlayerError(ExoPlaybackException e) {
+ onError(e);
+ }
+
+ // MediaCodecVideoTrackRenderer.Listener
+
+ @Override
+ public void onVideoSizeChanged(int width, int height) {
+ surfaceView.setVideoWidthHeightRatio(height == 0 ? 1 : (float) width / height);
+ }
+
+ @Override
+ public void onDrawnToSurface(Surface surface) {
+ shutterView.setVisibility(View.GONE);
+ }
+
+ @Override
+ public void onDroppedFrames(int count, long elapsed) {
+ Log.d(TAG, "Dropped frames: " + count);
+ }
+
+ @Override
+ public void onDecoderInitializationError(DecoderInitializationException e) {
+ // This is for informational purposes only. Do nothing.
+ }
+
+ @Override
+ public void onCryptoError(CryptoException e) {
+ // This is for informational purposes only. Do nothing.
+ }
+
+ // SurfaceHolder.Callback implementation
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ maybeStartPlayback();
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ // Do nothing.
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ if (videoRenderer != null) {
+ player.blockingSendMessage(videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, null);
+ }
+ }
+
+ /* package */ final class RendererBuilderCallback {
+
+ public void onRenderers(MediaCodecVideoTrackRenderer videoRenderer,
+ MediaCodecAudioTrackRenderer audioRenderer) {
+ SimplePlayerActivity.this.onRenderers(this, videoRenderer, audioRenderer);
+ }
+
+ public void onRenderersError(Exception e) {
+ SimplePlayerActivity.this.onRenderersError(this, e);
+ }
+
+ }
+
+}
diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java
new file mode 100644
index 00000000000..80e4c105de1
--- /dev/null
+++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java
@@ -0,0 +1,141 @@
+/*
+ * 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.demo.simple;
+
+import com.google.android.exoplayer.DefaultLoadControl;
+import com.google.android.exoplayer.LoadControl;
+import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
+import com.google.android.exoplayer.MediaCodecUtil;
+import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
+import com.google.android.exoplayer.SampleSource;
+import com.google.android.exoplayer.chunk.ChunkSampleSource;
+import com.google.android.exoplayer.chunk.ChunkSource;
+import com.google.android.exoplayer.chunk.FormatEvaluator;
+import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator;
+import com.google.android.exoplayer.demo.simple.SimplePlayerActivity.RendererBuilder;
+import com.google.android.exoplayer.demo.simple.SimplePlayerActivity.RendererBuilderCallback;
+import com.google.android.exoplayer.smoothstreaming.SmoothStreamingChunkSource;
+import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest;
+import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement;
+import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement;
+import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifestFetcher;
+import com.google.android.exoplayer.upstream.BufferPool;
+import com.google.android.exoplayer.upstream.DataSource;
+import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
+import com.google.android.exoplayer.upstream.HttpDataSource;
+import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
+
+import android.media.MediaCodec;
+import android.os.Handler;
+
+import java.util.ArrayList;
+
+/**
+ * A {@link RendererBuilder} for SmoothStreaming.
+ */
+/* package */ class SmoothStreamingRendererBuilder implements RendererBuilder,
+ ManifestCallback {
+
+ private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
+ private static final int VIDEO_BUFFER_SEGMENTS = 200;
+ private static final int AUDIO_BUFFER_SEGMENTS = 60;
+
+ private final SimplePlayerActivity playerActivity;
+ private final String userAgent;
+ private final String url;
+ private final String contentId;
+
+ private RendererBuilderCallback callback;
+
+ public SmoothStreamingRendererBuilder(SimplePlayerActivity playerActivity, String userAgent,
+ String url, String contentId) {
+ this.playerActivity = playerActivity;
+ this.userAgent = userAgent;
+ this.url = url;
+ this.contentId = contentId;
+ }
+
+ @Override
+ public void buildRenderers(RendererBuilderCallback callback) {
+ this.callback = callback;
+ SmoothStreamingManifestFetcher mpdFetcher = new SmoothStreamingManifestFetcher(this);
+ mpdFetcher.execute(url + "/Manifest", contentId);
+ }
+
+ @Override
+ public void onManifestError(String contentId, Exception e) {
+ callback.onRenderersError(e);
+ }
+
+ @Override
+ public void onManifest(String contentId, SmoothStreamingManifest manifest) {
+ Handler mainHandler = playerActivity.getMainHandler();
+ LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE));
+ DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
+
+ // Obtain stream elements for playback.
+ int maxDecodableFrameSize = MediaCodecUtil.maxH264DecodableFrameSize();
+ int audioStreamElementIndex = -1;
+ int videoStreamElementIndex = -1;
+ ArrayList videoTrackIndexList = new ArrayList();
+ for (int i = 0; i < manifest.streamElements.length; i++) {
+ if (audioStreamElementIndex == -1
+ && manifest.streamElements[i].type == StreamElement.TYPE_AUDIO) {
+ audioStreamElementIndex = i;
+ } else if (videoStreamElementIndex == -1
+ && manifest.streamElements[i].type == StreamElement.TYPE_VIDEO) {
+ videoStreamElementIndex = i;
+ StreamElement streamElement = manifest.streamElements[i];
+ for (int j = 0; j < streamElement.tracks.length; j++) {
+ TrackElement trackElement = streamElement.tracks[j];
+ if (trackElement.maxWidth * trackElement.maxHeight <= maxDecodableFrameSize) {
+ videoTrackIndexList.add(j);
+ } else {
+ // The device isn't capable of playing this stream.
+ }
+ }
+ }
+ }
+ int[] videoTrackIndices = new int[videoTrackIndexList.size()];
+ for (int i = 0; i < videoTrackIndexList.size(); i++) {
+ videoTrackIndices[i] = videoTrackIndexList.get(i);
+ }
+
+ // Build the video renderer.
+ DataSource videoDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES,
+ bandwidthMeter);
+ ChunkSource videoChunkSource = new SmoothStreamingChunkSource(url, manifest,
+ videoStreamElementIndex, videoTrackIndices, videoDataSource,
+ new AdaptiveEvaluator(bandwidthMeter));
+ ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
+ VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
+ MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource,
+ MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50);
+
+ // Build the audio renderer.
+ DataSource audioDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES,
+ bandwidthMeter);
+ ChunkSource audioChunkSource = new SmoothStreamingChunkSource(url, manifest,
+ audioStreamElementIndex, new int[] {0}, audioDataSource,
+ new FormatEvaluator.FixedEvaluator());
+ SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
+ AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
+ MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(
+ audioSampleSource);
+ callback.onRenderers(videoRenderer, audioRenderer);
+ }
+
+}
diff --git a/demo/src/main/project.properties b/demo/src/main/project.properties
new file mode 100644
index 00000000000..d194d6402ef
--- /dev/null
+++ b/demo/src/main/project.properties
@@ -0,0 +1,13 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system use,
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+
+# Project target.
+target=android-19
+android.library=false
+android.library.reference.1=../../../library/src/main
diff --git a/demo/src/main/res/layout/player_activity_full.xml b/demo/src/main/res/layout/player_activity_full.xml
new file mode 100644
index 00000000000..8d3e132995f
--- /dev/null
+++ b/demo/src/main/res/layout/player_activity_full.xml
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demo/src/main/res/layout/player_activity_simple.xml b/demo/src/main/res/layout/player_activity_simple.xml
new file mode 100644
index 00000000000..767f20439f2
--- /dev/null
+++ b/demo/src/main/res/layout/player_activity_simple.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
diff --git a/demo/src/main/res/layout/sample_chooser_activity.xml b/demo/src/main/res/layout/sample_chooser_activity.xml
new file mode 100644
index 00000000000..ae9be537962
--- /dev/null
+++ b/demo/src/main/res/layout/sample_chooser_activity.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
diff --git a/demo/src/main/res/layout/sample_chooser_inline_header.xml b/demo/src/main/res/layout/sample_chooser_inline_header.xml
new file mode 100644
index 00000000000..8df32d76a16
--- /dev/null
+++ b/demo/src/main/res/layout/sample_chooser_inline_header.xml
@@ -0,0 +1,25 @@
+
+
+
diff --git a/demo/src/main/res/values/strings.xml b/demo/src/main/res/values/strings.xml
new file mode 100644
index 00000000000..f71c2fb3771
--- /dev/null
+++ b/demo/src/main/res/values/strings.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+ ExoPlayer Demo
+
+ Play in background
+
+ Video
+
+ Audio
+
+ Text
+
+ Logging
+
+ Normal
+
+ Verbose
+
+ Retry
+
+ [off]
+
+ [on]
+
+ Protected content not supported on API levels below 18
+
+ Playback failed
+
+
diff --git a/demo/src/main/res/values/styles.xml b/demo/src/main/res/values/styles.xml
new file mode 100644
index 00000000000..6bdac1a50e7
--- /dev/null
+++ b/demo/src/main/res/values/styles.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 00000000000..34fb4618f05
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,18 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Settings specified in this file will override any Gradle settings
+# configured through the IDE.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+# Default value: -Xmx10248m -XX:MaxPermSize=256m
+# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000000..d5c591c9c53
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000000..2bbb67b83ab
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Jun 10 20:02:28 BST 2014
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=http\://services.gradle.org/distributions/gradle-1.12-bin.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 00000000000..91a7e269e19
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 00000000000..aec99730b4e
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/library/.project~ b/library/.project~
new file mode 100644
index 00000000000..5d04c5fa5c7
--- /dev/null
+++ b/library/.project~
@@ -0,0 +1,53 @@
+
+
+ ExoPlayerLib
+
+
+
+
+
+ com.android.ide.eclipse.adt.ResourceManagerBuilder
+
+
+
+
+ com.android.ide.eclipse.adt.PreCompilerBuilder
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ com.android.ide.eclipse.adt.ApkBuilder
+
+
+
+
+
+ com.android.ide.eclipse.adt.AndroidNature
+ org.eclipse.jdt.core.javanature
+
+
+
+ 1363908161147
+
+ 22
+
+ org.eclipse.ui.ide.multiFilter
+ 1.0-name-matches-false-false-BUILD
+
+
+
+ 1363908161148
+
+ 10
+
+ org.eclipse.ui.ide.multiFilter
+ 1.0-name-matches-true-false-build
+
+
+
+
diff --git a/library/build.gradle b/library/build.gradle
new file mode 100644
index 00000000000..5b751a0820e
--- /dev/null
+++ b/library/build.gradle
@@ -0,0 +1,38 @@
+// 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.
+apply plugin: 'android-library'
+
+android {
+ compileSdkVersion 19
+ buildToolsVersion "19.1"
+
+ defaultConfig {
+ minSdkVersion 9
+ targetSdkVersion 19
+ }
+
+ buildTypes {
+ release {
+ runProguard false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
+ }
+ }
+
+ lintOptions {
+ abortOnError false
+ }
+}
+
+dependencies {
+}
diff --git a/library/doc_src/images/exoplayer_playbackstate.png b/library/doc_src/images/exoplayer_playbackstate.png
new file mode 100644
index 00000000000..fb0ba72a609
Binary files /dev/null and b/library/doc_src/images/exoplayer_playbackstate.png differ
diff --git a/library/doc_src/images/exoplayer_state.png b/library/doc_src/images/exoplayer_state.png
new file mode 100644
index 00000000000..d37a51e23ac
Binary files /dev/null and b/library/doc_src/images/exoplayer_state.png differ
diff --git a/library/doc_src/images/exoplayer_threading_model.png b/library/doc_src/images/exoplayer_threading_model.png
new file mode 100644
index 00000000000..9f0306c1110
Binary files /dev/null and b/library/doc_src/images/exoplayer_threading_model.png differ
diff --git a/library/doc_src/images/trackrenderer_state.png b/library/doc_src/images/trackrenderer_state.png
new file mode 100644
index 00000000000..604a447a6aa
Binary files /dev/null and b/library/doc_src/images/trackrenderer_state.png differ
diff --git a/library/src/main/.classpath b/library/src/main/.classpath
new file mode 100644
index 00000000000..26c3fb17cde
--- /dev/null
+++ b/library/src/main/.classpath
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/library/src/main/.project b/library/src/main/.project
new file mode 100644
index 00000000000..5d04c5fa5c7
--- /dev/null
+++ b/library/src/main/.project
@@ -0,0 +1,53 @@
+
+
+ ExoPlayerLib
+
+
+
+
+
+ com.android.ide.eclipse.adt.ResourceManagerBuilder
+
+
+
+
+ com.android.ide.eclipse.adt.PreCompilerBuilder
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ com.android.ide.eclipse.adt.ApkBuilder
+
+
+
+
+
+ com.android.ide.eclipse.adt.AndroidNature
+ org.eclipse.jdt.core.javanature
+
+
+
+ 1363908161147
+
+ 22
+
+ org.eclipse.ui.ide.multiFilter
+ 1.0-name-matches-false-false-BUILD
+
+
+
+ 1363908161148
+
+ 10
+
+ org.eclipse.ui.ide.multiFilter
+ 1.0-name-matches-true-false-build
+
+
+
+
diff --git a/library/src/main/.settings/org.eclipse.jdt.core.prefs b/library/src/main/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 00000000000..b080d2ddc88
--- /dev/null
+++ b/library/src/main/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,4 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.source=1.6
diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000..99c378adadf
--- /dev/null
+++ b/library/src/main/AndroidManifest.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/library/src/main/java/com/google/android/exoplayer/CodecCounters.java b/library/src/main/java/com/google/android/exoplayer/CodecCounters.java
new file mode 100644
index 00000000000..7136ef2b1c2
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/CodecCounters.java
@@ -0,0 +1,71 @@
+/*
+ * 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;
+
+/**
+ * Maintains codec event counts, for debugging purposes only.
+ */
+public final class CodecCounters {
+
+ public volatile long codecInitCount;
+ public volatile long codecReleaseCount;
+ public volatile long outputFormatChangedCount;
+ public volatile long outputBuffersChangedCount;
+ public volatile long queuedInputBufferCount;
+ public volatile long inputBufferWaitingForSampleCount;
+ public volatile long keyframeCount;
+ public volatile long queuedEndOfStreamCount;
+ public volatile long renderedOutputBufferCount;
+ public volatile long skippedOutputBufferCount;
+ public volatile long droppedOutputBufferCount;
+ public volatile long discardedSamplesCount;
+
+ /**
+ * Resets all counts to zero.
+ */
+ public void zeroAllCounts() {
+ codecInitCount = 0;
+ codecReleaseCount = 0;
+ outputFormatChangedCount = 0;
+ outputBuffersChangedCount = 0;
+ queuedInputBufferCount = 0;
+ inputBufferWaitingForSampleCount = 0;
+ keyframeCount = 0;
+ queuedEndOfStreamCount = 0;
+ renderedOutputBufferCount = 0;
+ skippedOutputBufferCount = 0;
+ droppedOutputBufferCount = 0;
+ discardedSamplesCount = 0;
+ }
+
+ public String getDebugString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("cic(").append(codecInitCount).append(")");
+ builder.append("crc(").append(codecReleaseCount).append(")");
+ builder.append("ofc(").append(outputFormatChangedCount).append(")");
+ builder.append("obc(").append(outputBuffersChangedCount).append(")");
+ builder.append("qib(").append(queuedInputBufferCount).append(")");
+ builder.append("wib(").append(inputBufferWaitingForSampleCount).append(")");
+ builder.append("kfc(").append(keyframeCount).append(")");
+ builder.append("qes(").append(queuedEndOfStreamCount).append(")");
+ builder.append("ren(").append(renderedOutputBufferCount).append(")");
+ builder.append("sob(").append(skippedOutputBufferCount).append(")");
+ builder.append("dob(").append(droppedOutputBufferCount).append(")");
+ builder.append("dsc(").append(discardedSamplesCount).append(")");
+ return builder.toString();
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/CryptoInfo.java b/library/src/main/java/com/google/android/exoplayer/CryptoInfo.java
new file mode 100644
index 00000000000..81e55446eb6
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/CryptoInfo.java
@@ -0,0 +1,116 @@
+/*
+ * 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;
+
+import com.google.android.exoplayer.util.Util;
+
+import android.annotation.TargetApi;
+import android.media.MediaExtractor;
+
+/**
+ * Compatibility wrapper around {@link android.media.MediaCodec.CryptoInfo}.
+ */
+public class CryptoInfo {
+
+ /**
+ * @see android.media.MediaCodec.CryptoInfo#iv
+ */
+ public byte[] iv;
+ /**
+ * @see android.media.MediaCodec.CryptoInfo#key
+ */
+ public byte[] key;
+ /**
+ * @see android.media.MediaCodec.CryptoInfo#mode
+ */
+ public int mode;
+ /**
+ * @see android.media.MediaCodec.CryptoInfo#numBytesOfClearData
+ */
+ public int[] numBytesOfClearData;
+ /**
+ * @see android.media.MediaCodec.CryptoInfo#numBytesOfEncryptedData
+ */
+ public int[] numBytesOfEncryptedData;
+ /**
+ * @see android.media.MediaCodec.CryptoInfo#numSubSamples
+ */
+ public int numSubSamples;
+
+ private final android.media.MediaCodec.CryptoInfo frameworkCryptoInfo;
+
+ public CryptoInfo() {
+ frameworkCryptoInfo = Util.SDK_INT >= 16 ? newFrameworkCryptoInfoV16() : null;
+ }
+
+ /**
+ * @see android.media.MediaCodec.CryptoInfo#set(int, int[], int[], byte[], byte[], int)
+ */
+ public void set(int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData,
+ byte[] key, byte[] iv, int mode) {
+ this.numSubSamples = numSubSamples;
+ this.numBytesOfClearData = numBytesOfClearData;
+ this.numBytesOfEncryptedData = numBytesOfEncryptedData;
+ this.key = key;
+ this.iv = iv;
+ this.mode = mode;
+ if (Util.SDK_INT >= 16) {
+ updateFrameworkCryptoInfoV16();
+ }
+ }
+
+ /**
+ * Equivalent to {@link MediaExtractor#getSampleCryptoInfo(android.media.MediaCodec.CryptoInfo)}.
+ *
+ * @param extractor The extractor from which to retrieve the crypto information.
+ */
+ @TargetApi(16)
+ public void setFromExtractorV16(MediaExtractor extractor) {
+ extractor.getSampleCryptoInfo(frameworkCryptoInfo);
+ numSubSamples = frameworkCryptoInfo.numSubSamples;
+ numBytesOfClearData = frameworkCryptoInfo.numBytesOfClearData;
+ numBytesOfEncryptedData = frameworkCryptoInfo.numBytesOfEncryptedData;
+ key = frameworkCryptoInfo.key;
+ iv = frameworkCryptoInfo.iv;
+ mode = frameworkCryptoInfo.mode;
+ }
+
+ /**
+ * Returns an equivalent {@link android.media.MediaCodec.CryptoInfo} instance.
+ *
+ * Successive calls to this method on a single {@link CryptoInfo} will return the same instance.
+ * Changes to the {@link CryptoInfo} will be reflected in the returned object. The return object
+ * should not be modified directly.
+ *
+ * @return The equivalent {@link android.media.MediaCodec.CryptoInfo} instance.
+ */
+ @TargetApi(16)
+ public android.media.MediaCodec.CryptoInfo getFrameworkCryptoInfoV16() {
+ return frameworkCryptoInfo;
+ }
+
+ @TargetApi(16)
+ private android.media.MediaCodec.CryptoInfo newFrameworkCryptoInfoV16() {
+ return new android.media.MediaCodec.CryptoInfo();
+ }
+
+ @TargetApi(16)
+ private void updateFrameworkCryptoInfoV16() {
+ frameworkCryptoInfo.set(numSubSamples, numBytesOfClearData, numBytesOfEncryptedData, key, iv,
+ mode);
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/DecoderInfo.java b/library/src/main/java/com/google/android/exoplayer/DecoderInfo.java
new file mode 100644
index 00000000000..8b2a0dd4a81
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/DecoderInfo.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+/**
+ * Contains information about a media decoder.
+ */
+public final class DecoderInfo {
+
+ /**
+ * The name of the decoder.
+ *
+ * May be passed to {@link android.media.MediaCodec#createByCodecName(String)} to create an
+ * instance of the decoder.
+ */
+ public final String name;
+
+ /**
+ * Whether the decoder is adaptive.
+ *
+ * @see android.media.MediaCodecInfo.CodecCapabilities#isFeatureSupported(String)
+ * @see android.media.MediaCodecInfo.CodecCapabilities#FEATURE_AdaptivePlayback
+ */
+ public final boolean adaptive;
+
+ /**
+ * @param name The name of the decoder.
+ * @param adaptive Whether the decoder is adaptive.
+ */
+ /* package */ DecoderInfo(String name, boolean adaptive) {
+ this.name = name;
+ this.adaptive = adaptive;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/DefaultLoadControl.java b/library/src/main/java/com/google/android/exoplayer/DefaultLoadControl.java
new file mode 100644
index 00000000000..91bcac53cef
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/DefaultLoadControl.java
@@ -0,0 +1,284 @@
+/*
+ * 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;
+
+import com.google.android.exoplayer.upstream.Allocator;
+import com.google.android.exoplayer.upstream.NetworkLock;
+
+import android.os.Handler;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * A {@link LoadControl} implementation that allows loads to continue in a sequence that prevents
+ * any loader from getting too far ahead or behind any of the other loaders.
+ *
+ * Loads are scheduled so as to fill the available buffer space as rapidly as possible. Once the
+ * duration of buffered media and the buffer utilization both exceed respective thresholds, the
+ * control switches to a draining state during which no loads are permitted to start. During
+ * draining periods, resources such as the device radio have an opportunity to switch into low
+ * power modes. The control reverts back to the loading state when either the duration of buffered
+ * media or the buffer utilization fall below respective thresholds.
+ *
+ * This implementation of {@link LoadControl} integrates with {@link NetworkLock}, by registering
+ * itself as a task with priority {@link NetworkLock#STREAMING_PRIORITY} during loading periods,
+ * and unregistering itself during draining periods.
+ */
+public class DefaultLoadControl implements LoadControl {
+
+ /**
+ * Interface definition for a callback to be notified of {@link DefaultLoadControl} events.
+ */
+ public interface EventListener {
+
+ /**
+ * Invoked when the control transitions from a loading to a draining state, or vice versa.
+ *
+ * @param loading Whether the control is now in a loading state.
+ */
+ void onLoadingChanged(boolean loading);
+
+ }
+
+ public static final int DEFAULT_LOW_WATERMARK_MS = 15000;
+ public static final int DEFAULT_HIGH_WATERMARK_MS = 30000;
+ public static final float DEFAULT_LOW_POOL_LOAD = 0.2f;
+ public static final float DEFAULT_HIGH_POOL_LOAD = 0.8f;
+
+ private static final int ABOVE_HIGH_WATERMARK = 0;
+ private static final int BETWEEN_WATERMARKS = 1;
+ private static final int BELOW_LOW_WATERMARK = 2;
+
+ private final Allocator allocator;
+ private final List loaders;
+ private final HashMap loaderStates;
+ private final Handler eventHandler;
+ private final EventListener eventListener;
+
+ private final long lowWatermarkUs;
+ private final long highWatermarkUs;
+ private final float lowPoolLoad;
+ private final float highPoolLoad;
+
+ private int targetBufferSize;
+ private long maxLoadStartPositionUs;
+ private int bufferPoolState;
+ private boolean fillingBuffers;
+ private boolean streamingPrioritySet;
+
+ /**
+ * Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class.
+ *
+ * @param allocator The {@link Allocator} used by the loader.
+ */
+ public DefaultLoadControl(Allocator allocator) {
+ this(allocator, null, null);
+ }
+
+ /**
+ * Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class.
+ *
+ * @param allocator The {@link Allocator} used by the loader.
+ * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+ * null if delivery of events is not required.
+ * @param eventListener A listener of events. May be null if delivery of events is not required.
+ */
+ public DefaultLoadControl(Allocator allocator, Handler eventHandler,
+ EventListener eventListener) {
+ this(allocator, eventHandler, eventListener, DEFAULT_LOW_WATERMARK_MS,
+ DEFAULT_HIGH_WATERMARK_MS, DEFAULT_LOW_POOL_LOAD, DEFAULT_HIGH_POOL_LOAD);
+ }
+
+ /**
+ * Constructs a new instance.
+ *
+ * @param allocator The {@link Allocator} used by the loader.
+ * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+ * null if delivery of events is not required.
+ * @param eventListener A listener of events. May be null if delivery of events is not required.
+ * @param lowWatermarkMs The minimum duration of media that can be buffered for the control to
+ * be in the draining state. If less media is buffered, then the control will transition to
+ * the filling state.
+ * @param highWatermarkMs The minimum duration of media that can be buffered for the control to
+ * transition from filling to draining.
+ * @param lowPoolLoad The minimum fraction of the buffer that must be utilized for the control
+ * to be in the draining state. If the utilization is lower, then the control will transition
+ * to the filling state.
+ * @param highPoolLoad The minimum fraction of the buffer that must be utilized for the control
+ * to transition from the loading state to the draining state.
+ */
+ public DefaultLoadControl(Allocator allocator, Handler eventHandler, EventListener eventListener,
+ int lowWatermarkMs, int highWatermarkMs, float lowPoolLoad, float highPoolLoad) {
+ this.allocator = allocator;
+ this.eventHandler = eventHandler;
+ this.eventListener = eventListener;
+ this.loaders = new ArrayList();
+ this.loaderStates = new HashMap();
+ this.lowWatermarkUs = lowWatermarkMs * 1000L;
+ this.highWatermarkUs = highWatermarkMs * 1000L;
+ this.lowPoolLoad = lowPoolLoad;
+ this.highPoolLoad = highPoolLoad;
+ }
+
+ @Override
+ public void register(Object loader, int bufferSizeContribution) {
+ loaders.add(loader);
+ loaderStates.put(loader, new LoaderState(bufferSizeContribution));
+ targetBufferSize += bufferSizeContribution;
+ }
+
+ @Override
+ public void unregister(Object loader) {
+ loaders.remove(loader);
+ LoaderState state = loaderStates.remove(loader);
+ targetBufferSize -= state.bufferSizeContribution;
+ updateControlState();
+ }
+
+ @Override
+ public void trimAllocator() {
+ allocator.trim(targetBufferSize);
+ }
+
+ @Override
+ public Allocator getAllocator() {
+ return allocator;
+ }
+
+ @Override
+ public boolean update(Object loader, long playbackPositionUs, long nextLoadPositionUs,
+ boolean loading, boolean failed) {
+ // Update the loader state.
+ int loaderBufferState = getLoaderBufferState(playbackPositionUs, nextLoadPositionUs);
+ LoaderState loaderState = loaderStates.get(loader);
+ boolean loaderStateChanged = loaderState.bufferState != loaderBufferState ||
+ loaderState.nextLoadPositionUs != nextLoadPositionUs || loaderState.loading != loading ||
+ loaderState.failed != failed;
+ if (loaderStateChanged) {
+ loaderState.bufferState = loaderBufferState;
+ loaderState.nextLoadPositionUs = nextLoadPositionUs;
+ loaderState.loading = loading;
+ loaderState.failed = failed;
+ }
+
+ // Update the buffer pool state.
+ int allocatedSize = allocator.getAllocatedSize();
+ int bufferPoolState = getBufferPoolState(allocatedSize);
+ boolean bufferPoolStateChanged = this.bufferPoolState != bufferPoolState;
+ if (bufferPoolStateChanged) {
+ this.bufferPoolState = bufferPoolState;
+ }
+
+ // If either of the individual states have changed, update the shared control state.
+ if (loaderStateChanged || bufferPoolStateChanged) {
+ updateControlState();
+ }
+
+ return allocatedSize < targetBufferSize && nextLoadPositionUs != -1
+ && nextLoadPositionUs <= maxLoadStartPositionUs;
+ }
+
+ private int getLoaderBufferState(long playbackPositionUs, long nextLoadPositionUs) {
+ if (nextLoadPositionUs == -1) {
+ return ABOVE_HIGH_WATERMARK;
+ } else {
+ long timeUntilNextLoadPosition = nextLoadPositionUs - playbackPositionUs;
+ return timeUntilNextLoadPosition > highWatermarkUs ? ABOVE_HIGH_WATERMARK :
+ timeUntilNextLoadPosition < lowWatermarkUs ? BELOW_LOW_WATERMARK :
+ BETWEEN_WATERMARKS;
+ }
+ }
+
+ private int getBufferPoolState(int allocatedSize) {
+ float bufferPoolLoad = (float) allocatedSize / targetBufferSize;
+ return bufferPoolLoad > highPoolLoad ? ABOVE_HIGH_WATERMARK :
+ bufferPoolLoad < lowPoolLoad ? BELOW_LOW_WATERMARK :
+ BETWEEN_WATERMARKS;
+ }
+
+ private void updateControlState() {
+ boolean loading = false;
+ boolean failed = false;
+ boolean finished = true;
+ int highestState = bufferPoolState;
+ for (int i = 0; i < loaders.size(); i++) {
+ LoaderState loaderState = loaderStates.get(loaders.get(i));
+ loading |= loaderState.loading;
+ failed |= loaderState.failed;
+ finished &= loaderState.nextLoadPositionUs == -1;
+ highestState = Math.max(highestState, loaderState.bufferState);
+ }
+
+ fillingBuffers = !loaders.isEmpty() && !finished && !failed
+ && (highestState == BELOW_LOW_WATERMARK
+ || (highestState == BETWEEN_WATERMARKS && fillingBuffers));
+ if (fillingBuffers && !streamingPrioritySet) {
+ NetworkLock.instance.add(NetworkLock.STREAMING_PRIORITY);
+ streamingPrioritySet = true;
+ notifyLoadingChanged(true);
+ } else if (!fillingBuffers && streamingPrioritySet && !loading) {
+ NetworkLock.instance.remove(NetworkLock.STREAMING_PRIORITY);
+ streamingPrioritySet = false;
+ notifyLoadingChanged(false);
+ }
+
+ maxLoadStartPositionUs = -1;
+ if (fillingBuffers) {
+ for (int i = 0; i < loaders.size(); i++) {
+ Object loader = loaders.get(i);
+ LoaderState loaderState = loaderStates.get(loader);
+ long loaderTime = loaderState.nextLoadPositionUs;
+ if (loaderTime != -1
+ && (maxLoadStartPositionUs == -1 || loaderTime < maxLoadStartPositionUs)) {
+ maxLoadStartPositionUs = loaderTime;
+ }
+ }
+ }
+ }
+
+ private void notifyLoadingChanged(final boolean loading) {
+ if (eventHandler != null && eventListener != null) {
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ eventListener.onLoadingChanged(loading);
+ }
+ });
+ }
+ }
+
+ private static class LoaderState {
+
+ public final int bufferSizeContribution;
+
+ public int bufferState;
+ public boolean loading;
+ public boolean failed;
+ public long nextLoadPositionUs;
+
+ public LoaderState(int bufferSizeContribution) {
+ this.bufferSizeContribution = bufferSizeContribution;
+ bufferState = ABOVE_HIGH_WATERMARK;
+ loading = false;
+ failed = false;
+ nextLoadPositionUs = -1;
+ }
+
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/DummyTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/DummyTrackRenderer.java
new file mode 100644
index 00000000000..4bafdd07b84
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/DummyTrackRenderer.java
@@ -0,0 +1,67 @@
+/*
+ * 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;
+
+/**
+ * A {@link TrackRenderer} that does nothing.
+ *
+ * This renderer returns {@link TrackRenderer#STATE_IGNORE} from {@link #doPrepare()} in order to
+ * request that it should be ignored. {@link IllegalStateException} is thrown from all methods that
+ * are documented to indicate that they should not be invoked unless the renderer is prepared.
+ */
+public class DummyTrackRenderer extends TrackRenderer {
+
+ @Override
+ protected int doPrepare() throws ExoPlaybackException {
+ return STATE_IGNORE;
+ }
+
+ @Override
+ protected boolean isEnded() {
+ throw new IllegalStateException();
+ }
+
+ @Override
+ protected boolean isReady() {
+ throw new IllegalStateException();
+ }
+
+ @Override
+ protected void seekTo(long timeUs) {
+ throw new IllegalStateException();
+ }
+
+ @Override
+ protected void doSomeWork(long timeUs) {
+ throw new IllegalStateException();
+ }
+
+ @Override
+ protected long getDurationUs() {
+ throw new IllegalStateException();
+ }
+
+ @Override
+ protected long getBufferedPositionUs() {
+ throw new IllegalStateException();
+ }
+
+ @Override
+ protected long getCurrentPositionUs() {
+ throw new IllegalStateException();
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlaybackException.java b/library/src/main/java/com/google/android/exoplayer/ExoPlaybackException.java
new file mode 100644
index 00000000000..4c111047247
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/ExoPlaybackException.java
@@ -0,0 +1,37 @@
+/*
+ * 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;
+
+/**
+ * Thrown when a non-recoverable playback failure occurs.
+ *
+ * Where possible, the cause returned by {@link #getCause()} will indicate the reason for failure.
+ */
+public class ExoPlaybackException extends Exception {
+
+ public ExoPlaybackException(String message) {
+ super(message);
+ }
+
+ public ExoPlaybackException(Throwable cause) {
+ super(cause);
+ }
+
+ public ExoPlaybackException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayer.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayer.java
new file mode 100644
index 00000000000..15288280aac
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayer.java
@@ -0,0 +1,389 @@
+/*
+ * 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;
+
+import android.os.Looper;
+
+/**
+ * An extensible media player exposing traditional high-level media player functionality, such as
+ * the ability to prepare, play, pause and seek.
+ *
+ *
Topics covered here are:
+ *
+ * Assumptions and player composition
+ * Threading model
+ * Player state
+ *
+ *
+ *
+ * Assumptions and player construction
+ *
+ * The implementation is designed make no assumptions about (and hence impose no restrictions
+ * on) the type of the media being played, how and where it is stored, or how it is rendered.
+ * Rather than implementing the loading and rendering of media directly, {@link ExoPlayer} instead
+ * delegates this work to one or more {@link TrackRenderer}s, which are injected when the player
+ * is prepared. Hence {@link ExoPlayer} is capable of loading and playing any media for which a
+ * {@link TrackRenderer} implementation can be provided.
+ *
+ *
{@link MediaCodecAudioTrackRenderer} and {@link MediaCodecVideoTrackRenderer} can be used for
+ * the common cases of rendering audio and video. These components in turn require an
+ * upstream {@link SampleSource} to be injected through their constructors, where upstream
+ * is defined to denote a component that is closer to the source of the media. This pattern of
+ * upstream dependency injection is actively encouraged, since it means that the functionality of
+ * the player is built up through the composition of components that can easily be exchanged for
+ * alternate implementations. For example a {@link SampleSource} implementation may require a
+ * further upstream data loading component to be injected through its constructor, with different
+ * implementations enabling the loading of data from various sources.
+ *
+ *
+ *
Threading model
+ *
+ * The figure below shows the {@link ExoPlayer} threading model.
+ *
+ *
+ *
+ * It is recommended that instances are created and accessed from a single application thread.
+ * An application's main thread is ideal. Accessing an instance from multiple threads is
+ * discouraged, however if an application does wish to do this then it may do so provided that it
+ * ensures accesses are synchronized.
+ *
+ * Registered {@link Listener}s are invoked on the thread that created the {@link ExoPlayer}
+ * instance.
+ * An internal playback thread is responsible for managing playback and invoking the
+ * {@link TrackRenderer}s in order to load and play the media.
+ * {@link TrackRenderer} implementations (or any upstream components that they depend on) may
+ * use additional background threads (e.g. to load data). These are implementation specific.
+ *
+ *
+ *
+ * Player state
+ *
+ * The components of an {@link ExoPlayer}'s state can be divided into two distinct groups. State
+ * accessed by {@link #getRendererEnabled(int)} and {@link #getPlayWhenReady()} are only ever
+ * changed by invoking the player's methods, and are never changed as a result of operations that
+ * have been performed asynchronously by the playback thread. In contrast, the playback state
+ * accessed by {@link #getPlaybackState()} is only ever changed as a result of operations
+ * completing on the playback thread, as illustrated below.
+ *
+ *
+ * The possible playback state transitions are shown below. Transitions can be triggered either
+ * by changes in the state of the {@link TrackRenderer}s being used, or as a result of
+ * {@link #prepare(TrackRenderer[])}, {@link #stop()} or {@link #release()} being invoked.
+ *
+ */
+public interface ExoPlayer {
+
+ /**
+ * A factory for instantiating ExoPlayer instances.
+ */
+ public static final class Factory {
+
+ /**
+ * The default minimum duration of data that must be buffered for playback to start or resume
+ * following a user action such as a seek.
+ */
+ public static final int DEFAULT_MIN_BUFFER_MS = 500;
+
+ /**
+ * The default minimum duration of data that must be buffered for playback to resume
+ * after a player invoked rebuffer (i.e. a rebuffer that occurs due to buffer depletion, and
+ * not due to a user action such as starting playback or seeking).
+ */
+ public static final int DEFAULT_MIN_REBUFFER_MS = 5000;
+
+ private Factory() {}
+
+ /**
+ * Obtains an {@link ExoPlayer} instance.
+ *
+ * Must be invoked from a thread that has an associated {@link Looper}.
+ *
+ * @param rendererCount The number of {@link TrackRenderer}s that will be passed to
+ * {@link #prepare(TrackRenderer[])}.
+ * @param minBufferMs A minimum duration of data that must be buffered for playback to start
+ * or resume following a user action such as a seek.
+ * @param minRebufferMs A minimum duration of data that must be buffered for playback to resume
+ * after a player invoked rebuffer (i.e. a rebuffer that occurs due to buffer depletion, and
+ * not due to a user action such as starting playback or seeking).
+ */
+ public static ExoPlayer newInstance(int rendererCount, int minBufferMs, int minRebufferMs) {
+ return new ExoPlayerImpl(rendererCount, minBufferMs, minRebufferMs);
+ }
+
+ /**
+ * Obtains an {@link ExoPlayer} instance.
+ *
+ * Must be invoked from a thread that has an associated {@link Looper}.
+ *
+ * @param rendererCount The number of {@link TrackRenderer}s that will be passed to
+ * {@link #prepare(TrackRenderer[])}.
+ */
+ public static ExoPlayer newInstance(int rendererCount) {
+ return new ExoPlayerImpl(rendererCount, DEFAULT_MIN_BUFFER_MS, DEFAULT_MIN_REBUFFER_MS);
+ }
+
+ /**
+ * @deprecated Please use {@link #newInstance(int, int, int)}.
+ */
+ @Deprecated
+ public static ExoPlayer newInstance(int rendererCount, int minRebufferMs) {
+ return new ExoPlayerImpl(rendererCount, DEFAULT_MIN_BUFFER_MS, minRebufferMs);
+ }
+
+ }
+
+ /**
+ * Interface definition for a callback to be notified of changes in player state.
+ */
+ public interface Listener {
+ /**
+ * Invoked when the value returned from either {@link ExoPlayer#getPlayWhenReady()} or
+ * {@link ExoPlayer#getPlaybackState()} changes.
+ *
+ * @param playWhenReady Whether playback will proceed when ready.
+ * @param playbackState One of the {@code STATE} constants defined in this class.
+ */
+ void onPlayerStateChanged(boolean playWhenReady, int playbackState);
+ /**
+ * Invoked when the current value of {@link ExoPlayer#getPlayWhenReady()} has been reflected
+ * by the internal playback thread.
+ *
+ * An invocation of this method will shortly follow any call to
+ * {@link ExoPlayer#setPlayWhenReady(boolean)} that changes the state. If multiple calls are
+ * made in rapid succession, then this method will be invoked only once, after the final state
+ * has been reflected.
+ */
+ void onPlayWhenReadyCommitted();
+ /**
+ * Invoked when an error occurs. The playback state will transition to
+ * {@link ExoPlayer#STATE_IDLE} immediately after this method is invoked. The player instance
+ * can still be used, and {@link ExoPlayer#release()} must still be called on the player should
+ * it no longer be required.
+ *
+ * @param error The error.
+ */
+ void onPlayerError(ExoPlaybackException error);
+ }
+
+ /**
+ * A component of an {@link ExoPlayer} that can receive messages on the playback thread.
+ *
+ * Messages can be delivered to a component via {@link ExoPlayer#sendMessage} and
+ * {@link ExoPlayer#blockingSendMessage}.
+ */
+ public interface ExoPlayerComponent {
+
+ /**
+ * Handles a message delivered to the component. Invoked on the playback thread.
+ *
+ * @param messageType An integer identifying the type of message.
+ * @param message The message object.
+ * @throws ExoPlaybackException If an error occurred whilst handling the message.
+ */
+ void handleMessage(int messageType, Object message) throws ExoPlaybackException;
+
+ }
+
+ /**
+ * The player is neither prepared or being prepared.
+ */
+ public static final int STATE_IDLE = 1;
+ /**
+ * The player is being prepared.
+ */
+ public static final int STATE_PREPARING = 2;
+ /**
+ * The player is prepared but not able to immediately play from the current position. The cause
+ * is {@link TrackRenderer} specific, but this state typically occurs when more data needs
+ * to be buffered for playback to start.
+ */
+ public static final int STATE_BUFFERING = 3;
+ /**
+ * The player is prepared and able to immediately play from the current position. The player will
+ * be playing if {@link #setPlayWhenReady(boolean)} returns true, and paused otherwise.
+ */
+ public static final int STATE_READY = 4;
+ /**
+ * The player has finished playing the media.
+ */
+ public static final int STATE_ENDED = 5;
+ /**
+ * Represents an unknown time or duration.
+ */
+ public static final int UNKNOWN_TIME = -1;
+
+ /**
+ * Gets the {@link Looper} associated with the playback thread.
+ *
+ * @return The {@link Looper} associated with the playback thread.
+ */
+ public Looper getPlaybackLooper();
+
+ /**
+ * Register a listener to receive events from the player. The listener's methods will be invoked
+ * on the thread that was used to construct the player.
+ *
+ * @param listener The listener to register.
+ */
+ public void addListener(Listener listener);
+
+ /**
+ * Unregister a listener. The listener will no longer receive events from the player.
+ *
+ * @param listener The listener to unregister.
+ */
+ public void removeListener(Listener listener);
+
+ /**
+ * Returns the current state of the player.
+ *
+ * @return One of the {@code STATE} constants defined in this class.
+ */
+ public int getPlaybackState();
+
+ /**
+ * Prepares the player for playback.
+ *
+ * @param renderers The {@link TrackRenderer}s to use. The number of renderers must match the
+ * value that was passed to the {@link ExoPlayer.Factory#newInstance} method.
+ */
+ public void prepare(TrackRenderer... renderers);
+
+ /**
+ * Sets whether the renderer at the given index is enabled.
+ *
+ * @param index The index of the renderer.
+ * @param enabled Whether the renderer at the given index should be enabled.
+ */
+ public void setRendererEnabled(int index, boolean enabled);
+
+ /**
+ * Whether the renderer at the given index is enabled.
+ *
+ * @param index The index of the renderer.
+ * @return Whether the renderer is enabled.
+ */
+ public boolean getRendererEnabled(int index);
+
+ /**
+ * Sets whether playback should proceed when {@link #getPlaybackState()} == {@link #STATE_READY}.
+ * If the player is already in this state, then this method can be used to pause and resume
+ * playback.
+ *
+ * @param playWhenReady Whether playback should proceed when ready.
+ */
+ public void setPlayWhenReady(boolean playWhenReady);
+
+ /**
+ * Whether playback will proceed when {@link #getPlaybackState()} == {@link #STATE_READY}.
+ *
+ * @return Whether playback will proceed when ready.
+ */
+ public boolean getPlayWhenReady();
+
+ /**
+ * Whether the current value of {@link ExoPlayer#getPlayWhenReady()} has been reflected by the
+ * internal playback thread.
+ *
+ * @return True if the current value has been reflected. False otherwise.
+ */
+ public boolean isPlayWhenReadyCommitted();
+
+ /**
+ * Seeks to a position specified in milliseconds.
+ *
+ * @param positionMs The seek position.
+ */
+ public void seekTo(int positionMs);
+
+ /**
+ * Stops playback.
+ *
+ * Calling this method will cause the playback state to transition to
+ * {@link ExoPlayer#STATE_IDLE}. Note that the player instance can still be used, and that
+ * {@link ExoPlayer#release()} must still be called on the player should it no longer be required.
+ *
+ * Use {@code setPlayWhenReady(false)} rather than this method if the intention is to pause
+ * playback.
+ */
+ public void stop();
+
+ /**
+ * Releases the player. This method must be called when the player is no longer required.
+ *
+ * The player must not be used after calling this method.
+ */
+ public void release();
+
+ /**
+ * Sends a message to a specified component. The message is delivered to the component on the
+ * playback thread. If the component throws a {@link ExoPlaybackException}, then it is
+ * propagated out of the player as an error.
+ *
+ * @param target The target to which the message should be delivered.
+ * @param messageType An integer that can be used to identify the type of the message.
+ * @param message The message object.
+ */
+ public void sendMessage(ExoPlayerComponent target, int messageType, Object message);
+
+ /**
+ * Blocking variant of {@link #sendMessage(ExoPlayerComponent, int, Object)} that does not return
+ * until after the message has been delivered.
+ *
+ * @param target The target to which the message should be delivered.
+ * @param messageType An integer that can be used to identify the type of the message.
+ * @param message The message object.
+ */
+ public void blockingSendMessage(ExoPlayerComponent target, int messageType, Object message);
+
+ /**
+ * Gets the duration of the track in milliseconds.
+ *
+ * @return The duration of the track in milliseconds, or {@link ExoPlayer#UNKNOWN_TIME} if the
+ * duration is not known.
+ */
+ public int getDuration();
+
+ /**
+ * Gets the current playback position in milliseconds.
+ *
+ * @return The current playback position in milliseconds.
+ */
+ public int getCurrentPosition();
+
+ /**
+ * Gets an estimate of the absolute position in milliseconds up to which data is buffered.
+ *
+ * @return An estimate of the absolute position in milliseconds up to which data is buffered,
+ * or {@link ExoPlayer#UNKNOWN_TIME} if no estimate is available.
+ */
+ public int getBufferedPosition();
+
+ /**
+ * Gets an estimate of the percentage into the media up to which data is buffered.
+ *
+ * @return An estimate of the percentage into the media up to which data is buffered. 0 if the
+ * duration of the media is not known or if no estimate is available.
+ */
+ public int getBufferedPercentage();
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImpl.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImpl.java
new file mode 100644
index 00000000000..efbe5ce1e81
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImpl.java
@@ -0,0 +1,210 @@
+/*
+ * 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;
+
+import android.annotation.SuppressLint;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+
+import java.util.concurrent.CopyOnWriteArraySet;
+
+/**
+ * Concrete implementation of {@link ExoPlayer}.
+ */
+/* package */ final class ExoPlayerImpl implements ExoPlayer {
+
+ private static final String TAG = "ExoPlayerImpl";
+
+ private final Handler eventHandler;
+ private final ExoPlayerImplInternal internalPlayer;
+ private final CopyOnWriteArraySet listeners;
+ private final boolean[] rendererEnabledFlags;
+
+ private boolean playWhenReady;
+ private int playbackState;
+ private int pendingPlayWhenReadyAcks;
+
+ /**
+ * Constructs an instance. Must be invoked from a thread that has an associated {@link Looper}.
+ *
+ * @param rendererCount The number of {@link TrackRenderer}s that will be passed to
+ * {@link #prepare(TrackRenderer[])}.
+ * @param minBufferMs A minimum duration of data that must be buffered for playback to start
+ * or resume following a user action such as a seek.
+ * @param minRebufferMs A minimum duration of data that must be buffered for playback to resume
+ * after a player invoked rebuffer (i.e. a rebuffer that occurs due to buffer depletion, and
+ * not due to a user action such as starting playback or seeking).
+ */
+ @SuppressLint("HandlerLeak")
+ public ExoPlayerImpl(int rendererCount, int minBufferMs, int minRebufferMs) {
+ Log.i(TAG, "Init " + ExoPlayerLibraryInfo.VERSION);
+ this.playbackState = STATE_IDLE;
+ this.listeners = new CopyOnWriteArraySet();
+ this.rendererEnabledFlags = new boolean[rendererCount];
+ for (int i = 0; i < rendererEnabledFlags.length; i++) {
+ rendererEnabledFlags[i] = true;
+ }
+ eventHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ ExoPlayerImpl.this.handleEvent(msg);
+ }
+ };
+ internalPlayer = new ExoPlayerImplInternal(eventHandler, playWhenReady, rendererEnabledFlags,
+ minBufferMs, minRebufferMs);
+ }
+
+ @Override
+ public Looper getPlaybackLooper() {
+ return internalPlayer.getPlaybackLooper();
+ }
+
+ @Override
+ public void addListener(Listener listener) {
+ listeners.add(listener);
+ }
+
+ @Override
+ public void removeListener(Listener listener) {
+ listeners.remove(listener);
+ }
+
+ @Override
+ public int getPlaybackState() {
+ return playbackState;
+ }
+
+ @Override
+ public void prepare(TrackRenderer... renderers) {
+ internalPlayer.prepare(renderers);
+ }
+
+ @Override
+ public void setRendererEnabled(int index, boolean enabled) {
+ if (rendererEnabledFlags[index] != enabled) {
+ rendererEnabledFlags[index] = enabled;
+ internalPlayer.setRendererEnabled(index, enabled);
+ }
+ }
+
+ @Override
+ public boolean getRendererEnabled(int index) {
+ return rendererEnabledFlags[index];
+ }
+
+ @Override
+ public void setPlayWhenReady(boolean playWhenReady) {
+ if (this.playWhenReady != playWhenReady) {
+ this.playWhenReady = playWhenReady;
+ pendingPlayWhenReadyAcks++;
+ internalPlayer.setPlayWhenReady(playWhenReady);
+ for (Listener listener : listeners) {
+ listener.onPlayerStateChanged(playWhenReady, playbackState);
+ }
+ }
+ }
+
+ @Override
+ public boolean getPlayWhenReady() {
+ return playWhenReady;
+ }
+
+ @Override
+ public boolean isPlayWhenReadyCommitted() {
+ return pendingPlayWhenReadyAcks == 0;
+ }
+
+ @Override
+ public void seekTo(int positionMs) {
+ internalPlayer.seekTo(positionMs);
+ }
+
+ @Override
+ public void stop() {
+ internalPlayer.stop();
+ }
+
+ @Override
+ public void release() {
+ internalPlayer.release();
+ eventHandler.removeCallbacksAndMessages(null);
+ }
+
+ @Override
+ public void sendMessage(ExoPlayerComponent target, int messageType, Object message) {
+ internalPlayer.sendMessage(target, messageType, message);
+ }
+
+ @Override
+ public void blockingSendMessage(ExoPlayerComponent target, int messageType, Object message) {
+ internalPlayer.blockingSendMessage(target, messageType, message);
+ }
+
+ @Override
+ public int getDuration() {
+ return internalPlayer.getDuration();
+ }
+
+ @Override
+ public int getCurrentPosition() {
+ return internalPlayer.getCurrentPosition();
+ }
+
+ @Override
+ public int getBufferedPosition() {
+ return internalPlayer.getBufferedPosition();
+ }
+
+ @Override
+ public int getBufferedPercentage() {
+ int bufferedPosition = getBufferedPosition();
+ int duration = getDuration();
+ return bufferedPosition == ExoPlayer.UNKNOWN_TIME || duration == ExoPlayer.UNKNOWN_TIME ? 0
+ : (duration == 0 ? 100 : (bufferedPosition * 100) / duration);
+ }
+
+ // Not private so it can be called from an inner class without going through a thunk method.
+ /* package */ void handleEvent(Message msg) {
+ switch (msg.what) {
+ case ExoPlayerImplInternal.MSG_STATE_CHANGED: {
+ playbackState = msg.arg1;
+ for (Listener listener : listeners) {
+ listener.onPlayerStateChanged(playWhenReady, playbackState);
+ }
+ break;
+ }
+ case ExoPlayerImplInternal.MSG_SET_PLAY_WHEN_READY_ACK: {
+ pendingPlayWhenReadyAcks--;
+ if (pendingPlayWhenReadyAcks == 0) {
+ for (Listener listener : listeners) {
+ listener.onPlayWhenReadyCommitted();
+ }
+ }
+ break;
+ }
+ case ExoPlayerImplInternal.MSG_ERROR: {
+ ExoPlaybackException exception = (ExoPlaybackException) msg.obj;
+ for (Listener listener : listeners) {
+ listener.onPlayerError(exception);
+ }
+ break;
+ }
+ }
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java
new file mode 100644
index 00000000000..c6858eef3cc
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java
@@ -0,0 +1,579 @@
+/*
+ * 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;
+
+import com.google.android.exoplayer.ExoPlayer.ExoPlayerComponent;
+import com.google.android.exoplayer.util.Assertions;
+import com.google.android.exoplayer.util.TraceUtil;
+
+import android.annotation.SuppressLint;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.Pair;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Implements the internal behavior of {@link ExoPlayerImpl}.
+ */
+/* package */ final class ExoPlayerImplInternal implements Handler.Callback {
+
+ private static final String TAG = "ExoPlayerImplInternal";
+
+ // External messages
+ public static final int MSG_STATE_CHANGED = 1;
+ public static final int MSG_SET_PLAY_WHEN_READY_ACK = 2;
+ public static final int MSG_ERROR = 3;
+
+ // Internal messages
+ private static final int MSG_PREPARE = 1;
+ private static final int MSG_INCREMENTAL_PREPARE = 2;
+ private static final int MSG_SET_PLAY_WHEN_READY = 3;
+ private static final int MSG_STOP = 4;
+ private static final int MSG_RELEASE = 5;
+ private static final int MSG_SEEK_TO = 6;
+ private static final int MSG_DO_SOME_WORK = 7;
+ private static final int MSG_SET_RENDERER_ENABLED = 8;
+ private static final int MSG_CUSTOM = 9;
+
+ private static final int PREPARE_INTERVAL_MS = 10;
+ private static final int RENDERING_INTERVAL_MS = 10;
+ private static final int IDLE_INTERVAL_MS = 1000;
+
+ private final Handler handler;
+ private final HandlerThread internalPlayerThread;
+ private final Handler eventHandler;
+ private final MediaClock mediaClock;
+ private final boolean[] rendererEnabledFlags;
+ private final long minBufferUs;
+ private final long minRebufferUs;
+
+ private final List enabledRenderers;
+ private TrackRenderer[] renderers;
+ private TrackRenderer timeSourceTrackRenderer;
+
+ private boolean released;
+ private boolean playWhenReady;
+ private boolean rebuffering;
+ private int state;
+ private int customMessagesSent = 0;
+ private int customMessagesProcessed = 0;
+
+ private volatile long durationUs;
+ private volatile long positionUs;
+ private volatile long bufferedPositionUs;
+
+ @SuppressLint("HandlerLeak")
+ public ExoPlayerImplInternal(Handler eventHandler, boolean playWhenReady,
+ boolean[] rendererEnabledFlags, int minBufferMs, int minRebufferMs) {
+ this.eventHandler = eventHandler;
+ this.playWhenReady = playWhenReady;
+ this.rendererEnabledFlags = new boolean[rendererEnabledFlags.length];
+ this.minBufferUs = minBufferMs * 1000L;
+ this.minRebufferUs = minRebufferMs * 1000L;
+ for (int i = 0; i < rendererEnabledFlags.length; i++) {
+ this.rendererEnabledFlags[i] = rendererEnabledFlags[i];
+ }
+
+ this.state = ExoPlayer.STATE_IDLE;
+ this.durationUs = TrackRenderer.UNKNOWN_TIME;
+ this.bufferedPositionUs = TrackRenderer.UNKNOWN_TIME;
+
+ mediaClock = new MediaClock();
+ enabledRenderers = new ArrayList(rendererEnabledFlags.length);
+ internalPlayerThread = new HandlerThread(getClass().getSimpleName() + ":Handler") {
+ @Override
+ public void run() {
+ // Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can
+ // not normally change to this priority" is incorrect.
+ Process.setThreadPriority(Process.THREAD_PRIORITY_AUDIO);
+ super.run();
+ }
+ };
+ internalPlayerThread.start();
+ handler = new Handler(internalPlayerThread.getLooper(), this);
+ }
+
+ public Looper getPlaybackLooper() {
+ return internalPlayerThread.getLooper();
+ }
+
+ public int getCurrentPosition() {
+ return (int) (positionUs / 1000);
+ }
+
+ public int getBufferedPosition() {
+ return bufferedPositionUs == TrackRenderer.UNKNOWN_TIME ? ExoPlayer.UNKNOWN_TIME
+ : (int) (bufferedPositionUs / 1000);
+ }
+
+ public int getDuration() {
+ return durationUs == TrackRenderer.UNKNOWN_TIME ? ExoPlayer.UNKNOWN_TIME
+ : (int) (durationUs / 1000);
+ }
+
+ public void prepare(TrackRenderer... renderers) {
+ handler.obtainMessage(MSG_PREPARE, renderers).sendToTarget();
+ }
+
+ public void setPlayWhenReady(boolean playWhenReady) {
+ handler.obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 1 : 0, 0).sendToTarget();
+ }
+
+ public void seekTo(int positionMs) {
+ handler.obtainMessage(MSG_SEEK_TO, positionMs, 0).sendToTarget();
+ }
+
+ public void stop() {
+ handler.sendEmptyMessage(MSG_STOP);
+ }
+
+ public void setRendererEnabled(int index, boolean enabled) {
+ handler.obtainMessage(MSG_SET_RENDERER_ENABLED, index, enabled ? 1 : 0).sendToTarget();
+ }
+
+ public void sendMessage(ExoPlayerComponent target, int messageType, Object message) {
+ customMessagesSent++;
+ handler.obtainMessage(MSG_CUSTOM, messageType, 0, Pair.create(target, message)).sendToTarget();
+ }
+
+ public synchronized void blockingSendMessage(ExoPlayerComponent target, int messageType,
+ Object message) {
+ int messageNumber = customMessagesSent++;
+ handler.obtainMessage(MSG_CUSTOM, messageType, 0, Pair.create(target, message)).sendToTarget();
+ while (customMessagesProcessed <= messageNumber) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
+ public synchronized void release() {
+ if (!released) {
+ handler.sendEmptyMessage(MSG_RELEASE);
+ while (!released) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ internalPlayerThread.quit();
+ }
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ try {
+ switch (msg.what) {
+ case MSG_PREPARE: {
+ prepareInternal((TrackRenderer[]) msg.obj);
+ return true;
+ }
+ case MSG_INCREMENTAL_PREPARE: {
+ incrementalPrepareInternal();
+ return true;
+ }
+ case MSG_SET_PLAY_WHEN_READY: {
+ setPlayWhenReadyInternal(msg.arg1 != 0);
+ return true;
+ }
+ case MSG_DO_SOME_WORK: {
+ doSomeWork();
+ return true;
+ }
+ case MSG_SEEK_TO: {
+ seekToInternal(msg.arg1);
+ return true;
+ }
+ case MSG_STOP: {
+ stopInternal();
+ return true;
+ }
+ case MSG_RELEASE: {
+ releaseInternal();
+ return true;
+ }
+ case MSG_CUSTOM: {
+ sendMessageInternal(msg.arg1, msg.obj);
+ return true;
+ }
+ case MSG_SET_RENDERER_ENABLED: {
+ setRendererEnabledInternal(msg.arg1, msg.arg2 != 0);
+ return true;
+ }
+ default:
+ return false;
+ }
+ } catch (ExoPlaybackException e) {
+ Log.e(TAG, "Internal track renderer error.", e);
+ eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget();
+ stopInternal();
+ return true;
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Internal runtime error.", e);
+ eventHandler.obtainMessage(MSG_ERROR, new ExoPlaybackException(e)).sendToTarget();
+ stopInternal();
+ return true;
+ }
+ }
+
+ private void setState(int state) {
+ if (this.state != state) {
+ this.state = state;
+ eventHandler.obtainMessage(MSG_STATE_CHANGED, state, 0).sendToTarget();
+ }
+ }
+
+ private void prepareInternal(TrackRenderer[] renderers) {
+ rebuffering = false;
+ this.renderers = renderers;
+ for (int i = 0; i < renderers.length; i++) {
+ if (renderers[i].isTimeSource()) {
+ Assertions.checkState(timeSourceTrackRenderer == null);
+ timeSourceTrackRenderer = renderers[i];
+ }
+ }
+ setState(ExoPlayer.STATE_PREPARING);
+ handler.sendEmptyMessage(MSG_INCREMENTAL_PREPARE);
+ }
+
+ private void incrementalPrepareInternal() throws ExoPlaybackException {
+ long operationStartTimeMs = SystemClock.elapsedRealtime();
+ boolean prepared = true;
+ for (int i = 0; i < renderers.length; i++) {
+ if (renderers[i].getState() == TrackRenderer.STATE_UNPREPARED) {
+ int state = renderers[i].prepare();
+ if (state == TrackRenderer.STATE_UNPREPARED) {
+ prepared = false;
+ }
+ }
+ }
+
+ if (!prepared) {
+ // We're still waiting for some sources to be prepared.
+ scheduleNextOperation(MSG_INCREMENTAL_PREPARE, operationStartTimeMs, PREPARE_INTERVAL_MS);
+ return;
+ }
+
+ long durationUs = 0;
+ boolean isEnded = true;
+ boolean allRenderersReadyOrEnded = true;
+ for (int i = 0; i < renderers.length; i++) {
+ TrackRenderer renderer = renderers[i];
+ if (rendererEnabledFlags[i] && renderer.getState() == TrackRenderer.STATE_PREPARED) {
+ renderer.enable(positionUs, false);
+ enabledRenderers.add(renderer);
+ isEnded = isEnded && renderer.isEnded();
+ allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded(renderer);
+ if (durationUs == TrackRenderer.UNKNOWN_TIME) {
+ // We've already encountered a track for which the duration is unknown, so the media
+ // duration is unknown regardless of the duration of this track.
+ } else {
+ long trackDurationUs = renderer.getDurationUs();
+ if (trackDurationUs == TrackRenderer.UNKNOWN_TIME) {
+ durationUs = TrackRenderer.UNKNOWN_TIME;
+ } else if (trackDurationUs == TrackRenderer.MATCH_LONGEST) {
+ // Do nothing.
+ } else {
+ durationUs = Math.max(durationUs, trackDurationUs);
+ }
+ }
+ }
+ }
+ this.durationUs = durationUs;
+
+ if (isEnded) {
+ // We don't expect this case, but handle it anyway.
+ setState(ExoPlayer.STATE_ENDED);
+ } else {
+ setState(allRenderersReadyOrEnded ? ExoPlayer.STATE_READY : ExoPlayer.STATE_BUFFERING);
+ if (playWhenReady && state == ExoPlayer.STATE_READY) {
+ startRenderers();
+ }
+ }
+
+ handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+ }
+
+ private boolean rendererReadyOrEnded(TrackRenderer renderer) {
+ if (renderer.isEnded()) {
+ return true;
+ }
+ if (!renderer.isReady()) {
+ return false;
+ }
+ if (state == ExoPlayer.STATE_READY) {
+ return true;
+ }
+ long rendererDurationUs = renderer.getDurationUs();
+ long rendererBufferedPositionUs = renderer.getBufferedPositionUs();
+ long minBufferDurationUs = rebuffering ? minRebufferUs : minBufferUs;
+ return minBufferDurationUs <= 0
+ || rendererBufferedPositionUs == TrackRenderer.UNKNOWN_TIME
+ || rendererBufferedPositionUs == TrackRenderer.END_OF_TRACK
+ || rendererBufferedPositionUs >= positionUs + minBufferDurationUs
+ || (rendererDurationUs != TrackRenderer.UNKNOWN_TIME
+ && rendererDurationUs != TrackRenderer.MATCH_LONGEST
+ && rendererBufferedPositionUs >= rendererDurationUs);
+ }
+
+ private void setPlayWhenReadyInternal(boolean playWhenReady) throws ExoPlaybackException {
+ try {
+ rebuffering = false;
+ this.playWhenReady = playWhenReady;
+ if (!playWhenReady) {
+ stopRenderers();
+ updatePositionUs();
+ } else {
+ if (state == ExoPlayer.STATE_READY) {
+ startRenderers();
+ handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+ } else if (state == ExoPlayer.STATE_BUFFERING) {
+ handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+ }
+ }
+ } finally {
+ eventHandler.obtainMessage(MSG_SET_PLAY_WHEN_READY_ACK).sendToTarget();
+ }
+ }
+
+ private void startRenderers() throws ExoPlaybackException {
+ rebuffering = false;
+ mediaClock.start();
+ for (int i = 0; i < enabledRenderers.size(); i++) {
+ enabledRenderers.get(i).start();
+ }
+ }
+
+ private void stopRenderers() throws ExoPlaybackException {
+ mediaClock.stop();
+ for (int i = 0; i < enabledRenderers.size(); i++) {
+ ensureStopped(enabledRenderers.get(i));
+ }
+ }
+
+ private void updatePositionUs() {
+ positionUs = timeSourceTrackRenderer != null &&
+ enabledRenderers.contains(timeSourceTrackRenderer) ?
+ timeSourceTrackRenderer.getCurrentPositionUs() :
+ mediaClock.getTimeUs();
+ }
+
+ private void doSomeWork() throws ExoPlaybackException {
+ TraceUtil.beginSection("doSomeWork");
+ long operationStartTimeMs = SystemClock.elapsedRealtime();
+ long bufferedPositionUs = durationUs != TrackRenderer.UNKNOWN_TIME ? durationUs
+ : Long.MAX_VALUE;
+ boolean isEnded = true;
+ boolean allRenderersReadyOrEnded = true;
+ updatePositionUs();
+ for (int i = 0; i < enabledRenderers.size(); i++) {
+ TrackRenderer renderer = enabledRenderers.get(i);
+ // TODO: Each renderer should return the maximum delay before which it wishes to be
+ // invoked again. The minimum of these values should then be used as the delay before the next
+ // invocation of this method.
+ renderer.doSomeWork(positionUs);
+ isEnded = isEnded && renderer.isEnded();
+ allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded(renderer);
+
+ if (bufferedPositionUs == TrackRenderer.UNKNOWN_TIME) {
+ // We've already encountered a track for which the buffered position is unknown. Hence the
+ // media buffer position unknown regardless of the buffered position of this track.
+ } else {
+ long rendererDurationUs = renderer.getDurationUs();
+ long rendererBufferedPositionUs = renderer.getBufferedPositionUs();
+ if (rendererBufferedPositionUs == TrackRenderer.UNKNOWN_TIME) {
+ bufferedPositionUs = TrackRenderer.UNKNOWN_TIME;
+ } else if (rendererBufferedPositionUs == TrackRenderer.END_OF_TRACK
+ || (rendererDurationUs != TrackRenderer.UNKNOWN_TIME
+ && rendererDurationUs != TrackRenderer.MATCH_LONGEST
+ && rendererBufferedPositionUs >= rendererDurationUs)) {
+ // This track is fully buffered.
+ } else {
+ bufferedPositionUs = Math.min(bufferedPositionUs, rendererBufferedPositionUs);
+ }
+ }
+ }
+ this.bufferedPositionUs = bufferedPositionUs;
+
+ if (isEnded) {
+ setState(ExoPlayer.STATE_ENDED);
+ stopRenderers();
+ } else if (state == ExoPlayer.STATE_BUFFERING && allRenderersReadyOrEnded) {
+ setState(ExoPlayer.STATE_READY);
+ if (playWhenReady) {
+ startRenderers();
+ }
+ } else if (state == ExoPlayer.STATE_READY && !allRenderersReadyOrEnded) {
+ rebuffering = playWhenReady;
+ setState(ExoPlayer.STATE_BUFFERING);
+ stopRenderers();
+ }
+
+ handler.removeMessages(MSG_DO_SOME_WORK);
+ if ((playWhenReady && state == ExoPlayer.STATE_READY) || state == ExoPlayer.STATE_BUFFERING) {
+ scheduleNextOperation(MSG_DO_SOME_WORK, operationStartTimeMs, RENDERING_INTERVAL_MS);
+ } else if (!enabledRenderers.isEmpty()) {
+ scheduleNextOperation(MSG_DO_SOME_WORK, operationStartTimeMs, IDLE_INTERVAL_MS);
+ }
+
+ TraceUtil.endSection();
+ }
+
+ private void scheduleNextOperation(int operationType, long thisOperationStartTimeMs,
+ long intervalMs) {
+ long nextOperationStartTimeMs = thisOperationStartTimeMs + intervalMs;
+ long nextOperationDelayMs = nextOperationStartTimeMs - SystemClock.elapsedRealtime();
+ if (nextOperationDelayMs <= 0) {
+ handler.sendEmptyMessage(operationType);
+ } else {
+ handler.sendEmptyMessageDelayed(operationType, nextOperationDelayMs);
+ }
+ }
+
+ private void seekToInternal(int positionMs) throws ExoPlaybackException {
+ rebuffering = false;
+ positionUs = positionMs * 1000L;
+ mediaClock.stop();
+ mediaClock.setTimeUs(positionUs);
+ if (state == ExoPlayer.STATE_IDLE || state == ExoPlayer.STATE_PREPARING) {
+ return;
+ }
+ for (int i = 0; i < enabledRenderers.size(); i++) {
+ TrackRenderer renderer = enabledRenderers.get(i);
+ ensureStopped(renderer);
+ renderer.seekTo(positionUs);
+ }
+ setState(ExoPlayer.STATE_BUFFERING);
+ handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+ }
+
+ private void stopInternal() {
+ rebuffering = false;
+ resetInternal();
+ }
+
+ private void releaseInternal() {
+ resetInternal();
+ synchronized (this) {
+ released = true;
+ notifyAll();
+ }
+ }
+
+ private void resetInternal() {
+ handler.removeMessages(MSG_DO_SOME_WORK);
+ handler.removeMessages(MSG_INCREMENTAL_PREPARE);
+ mediaClock.stop();
+ if (renderers == null) {
+ return;
+ }
+ for (int i = 0; i < renderers.length; i++) {
+ try {
+ TrackRenderer renderer = renderers[i];
+ ensureStopped(renderer);
+ if (renderer.getState() == TrackRenderer.STATE_ENABLED) {
+ renderer.disable();
+ }
+ renderer.release();
+ } catch (ExoPlaybackException e) {
+ // There's nothing we can do. Catch the exception here so that other renderers still have
+ // a chance of being cleaned up correctly.
+ Log.e(TAG, "Stop failed.", e);
+ } catch (RuntimeException e) {
+ // Ditto.
+ Log.e(TAG, "Stop failed.", e);
+ }
+ }
+ renderers = null;
+ timeSourceTrackRenderer = null;
+ enabledRenderers.clear();
+ setState(ExoPlayer.STATE_IDLE);
+ }
+
+ private void sendMessageInternal(int what, Object obj)
+ throws ExoPlaybackException {
+ try {
+ @SuppressWarnings("unchecked")
+ Pair targetAndMessage = (Pair) obj;
+ targetAndMessage.first.handleMessage(what, targetAndMessage.second);
+ } finally {
+ synchronized (this) {
+ customMessagesProcessed++;
+ notifyAll();
+ }
+ }
+ if (state != ExoPlayer.STATE_IDLE) {
+ // The message may have caused something to change that now requires us to do work.
+ handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+ }
+ }
+
+ private void setRendererEnabledInternal(int index, boolean enabled)
+ throws ExoPlaybackException {
+ if (rendererEnabledFlags[index] == enabled) {
+ return;
+ }
+
+ rendererEnabledFlags[index] = enabled;
+ if (state == ExoPlayer.STATE_IDLE || state == ExoPlayer.STATE_PREPARING) {
+ return;
+ }
+
+ TrackRenderer renderer = renderers[index];
+ int rendererState = renderer.getState();
+ if (rendererState != TrackRenderer.STATE_PREPARED &&
+ rendererState != TrackRenderer.STATE_ENABLED &&
+ rendererState != TrackRenderer.STATE_STARTED) {
+ return;
+ }
+
+ if (enabled) {
+ boolean playing = playWhenReady && state == ExoPlayer.STATE_READY;
+ renderer.enable(positionUs, playing);
+ enabledRenderers.add(renderer);
+ if (playing) {
+ renderer.start();
+ }
+ handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+ } else {
+ if (renderer == timeSourceTrackRenderer) {
+ // We've been using timeSourceTrackRenderer to advance the current position, but it's
+ // being disabled. Sync mediaClock so that it can take over timing responsibilities.
+ mediaClock.setTimeUs(renderer.getCurrentPositionUs());
+ }
+ ensureStopped(renderer);
+ enabledRenderers.remove(renderer);
+ renderer.disable();
+ }
+ }
+
+ private void ensureStopped(TrackRenderer renderer) throws ExoPlaybackException {
+ if (renderer.getState() == TrackRenderer.STATE_STARTED) {
+ renderer.stop();
+ }
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java
new file mode 100644
index 00000000000..8406b4e17d1
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java
@@ -0,0 +1,51 @@
+/*
+ * 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;
+
+/**
+ * Information about the ExoPlayer library.
+ */
+// TODO: This file should be automatically generated by the build system.
+public class ExoPlayerLibraryInfo {
+
+ private ExoPlayerLibraryInfo() {}
+
+ /**
+ * The version of the library, expressed as a string.
+ */
+ public static final String VERSION = "1.0.10";
+
+ /**
+ * The version of the library, expressed as an integer.
+ *
+ * Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the
+ * corresponding integer version 1002003.
+ */
+ public static final int VERSION_INT = 1000010;
+
+ /**
+ * Whether the library was compiled with {@link com.google.android.exoplayer.util.Assertions}
+ * checks enabled.
+ */
+ public static final boolean ASSERTIONS_ENABLED = true;
+
+ /**
+ * Whether the library was compiled with {@link com.google.android.exoplayer.util.TraceUtil}
+ * trace enabled.
+ */
+ public static final boolean TRACE_ENABLED = true;
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/FormatHolder.java b/library/src/main/java/com/google/android/exoplayer/FormatHolder.java
new file mode 100644
index 00000000000..04a6d7d85f5
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/FormatHolder.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * Holds a {@link MediaFormat} and corresponding drm scheme initialization data.
+ */
+public final class FormatHolder {
+
+ /**
+ * The format of the media.
+ */
+ public MediaFormat format;
+ /**
+ * Initialization data for each of the drm schemes supported by the media, keyed by scheme UUID.
+ * Null if the media is not encrypted.
+ */
+ public Map drmInitData;
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java
new file mode 100644
index 00000000000..653dc2d6282
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java
@@ -0,0 +1,214 @@
+/*
+ * 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;
+
+import com.google.android.exoplayer.util.Assertions;
+import com.google.android.exoplayer.util.Util;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.media.MediaExtractor;
+import android.net.Uri;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * Extracts samples from a stream using Android's {@link MediaExtractor}.
+ */
+// TODO: This implementation needs to be fixed so that its methods are non-blocking (either
+// through use of a background thread, or through changes to the framework's MediaExtractor API).
+@TargetApi(16)
+public final class FrameworkSampleSource implements SampleSource {
+
+ private static final int TRACK_STATE_DISABLED = 0;
+ private static final int TRACK_STATE_ENABLED = 1;
+ private static final int TRACK_STATE_FORMAT_SENT = 2;
+
+ private final Context context;
+ private final Uri uri;
+ private final Map headers;
+
+ private MediaExtractor extractor;
+ private TrackInfo[] trackInfos;
+ private boolean prepared;
+ private int remainingReleaseCount;
+ private int[] trackStates;
+ private boolean[] pendingDiscontinuities;
+
+ private long seekTimeUs;
+
+ public FrameworkSampleSource(Context context, Uri uri, Map headers,
+ int downstreamRendererCount) {
+ Assertions.checkState(Util.SDK_INT >= 16);
+ this.context = context;
+ this.uri = uri;
+ this.headers = headers;
+ this.remainingReleaseCount = downstreamRendererCount;
+ }
+
+ @Override
+ public boolean prepare() throws IOException {
+ if (!prepared) {
+ extractor = new MediaExtractor();
+ extractor.setDataSource(context, uri, headers);
+ trackStates = new int[extractor.getTrackCount()];
+ pendingDiscontinuities = new boolean[extractor.getTrackCount()];
+ trackInfos = new TrackInfo[trackStates.length];
+ for (int i = 0; i < trackStates.length; i++) {
+ android.media.MediaFormat format = extractor.getTrackFormat(i);
+ long duration = format.containsKey(android.media.MediaFormat.KEY_DURATION) ?
+ format.getLong(android.media.MediaFormat.KEY_DURATION) : TrackRenderer.UNKNOWN_TIME;
+ String mime = format.getString(android.media.MediaFormat.KEY_MIME);
+ trackInfos[i] = new TrackInfo(mime, duration);
+ }
+ prepared = true;
+ }
+ return true;
+ }
+
+ @Override
+ public int getTrackCount() {
+ Assertions.checkState(prepared);
+ return extractor.getTrackCount();
+ }
+
+ @Override
+ public TrackInfo getTrackInfo(int track) {
+ Assertions.checkState(prepared);
+ return trackInfos[track];
+ }
+
+ @Override
+ public void enable(int track, long timeUs) {
+ Assertions.checkState(prepared);
+ Assertions.checkState(trackStates[track] == TRACK_STATE_DISABLED);
+ boolean wasSourceEnabled = isEnabled();
+ trackStates[track] = TRACK_STATE_ENABLED;
+ extractor.selectTrack(track);
+ if (!wasSourceEnabled) {
+ seekToUs(timeUs);
+ }
+ }
+
+ @Override
+ public void continueBuffering(long playbackPositionUs) {
+ // Do nothing. The MediaExtractor instance is responsible for buffering.
+ }
+
+ @Override
+ public int readData(int track, long playbackPositionUs, FormatHolder formatHolder,
+ SampleHolder sampleHolder, boolean onlyReadDiscontinuity) {
+ Assertions.checkState(prepared);
+ Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED);
+ if (pendingDiscontinuities[track]) {
+ pendingDiscontinuities[track] = false;
+ return DISCONTINUITY_READ;
+ }
+ if (onlyReadDiscontinuity) {
+ return NOTHING_READ;
+ }
+ int extractorTrackIndex = extractor.getSampleTrackIndex();
+ if (extractorTrackIndex == track) {
+ if (trackStates[track] != TRACK_STATE_FORMAT_SENT) {
+ formatHolder.format = MediaFormat.createFromFrameworkMediaFormatV16(
+ extractor.getTrackFormat(track));
+ formatHolder.drmInitData = Util.SDK_INT >= 18 ? getPsshInfoV18() : null;
+ trackStates[track] = TRACK_STATE_FORMAT_SENT;
+ return FORMAT_READ;
+ }
+ if (sampleHolder.data != null) {
+ int offset = sampleHolder.data.position();
+ sampleHolder.size = extractor.readSampleData(sampleHolder.data, offset);
+ sampleHolder.data.position(offset + sampleHolder.size);
+ } else {
+ sampleHolder.size = 0;
+ }
+ sampleHolder.timeUs = extractor.getSampleTime();
+ sampleHolder.flags = extractor.getSampleFlags();
+ if ((sampleHolder.flags & MediaExtractor.SAMPLE_FLAG_ENCRYPTED) != 0) {
+ sampleHolder.cryptoInfo.setFromExtractorV16(extractor);
+ }
+ seekTimeUs = -1;
+ extractor.advance();
+ return SAMPLE_READ;
+ } else {
+ return extractorTrackIndex < 0 ? END_OF_STREAM : NOTHING_READ;
+ }
+ }
+
+ @TargetApi(18)
+ private Map getPsshInfoV18() {
+ Map psshInfo = extractor.getPsshInfo();
+ return (psshInfo == null || psshInfo.isEmpty()) ? null : psshInfo;
+ }
+
+ @Override
+ public void disable(int track) {
+ Assertions.checkState(prepared);
+ Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED);
+ extractor.unselectTrack(track);
+ pendingDiscontinuities[track] = false;
+ trackStates[track] = TRACK_STATE_DISABLED;
+ }
+
+ @Override
+ public void seekToUs(long timeUs) {
+ Assertions.checkState(prepared);
+ if (seekTimeUs != timeUs) {
+ // Avoid duplicate calls to the underlying extractor's seek method in the case that there
+ // have been no interleaving calls to advance.
+ seekTimeUs = timeUs;
+ extractor.seekTo(timeUs, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
+ for (int i = 0; i < trackStates.length; ++i) {
+ if (trackStates[i] != TRACK_STATE_DISABLED) {
+ pendingDiscontinuities[i] = true;
+ }
+ }
+ }
+ }
+
+ @Override
+ public long getBufferedPositionUs() {
+ Assertions.checkState(prepared);
+ long bufferedDurationUs = extractor.getCachedDuration();
+ if (bufferedDurationUs == -1) {
+ return TrackRenderer.UNKNOWN_TIME;
+ } else {
+ return extractor.getSampleTime() + bufferedDurationUs;
+ }
+ }
+
+ @Override
+ public void release() {
+ Assertions.checkState(remainingReleaseCount > 0);
+ if (--remainingReleaseCount == 0) {
+ extractor.release();
+ extractor = null;
+ }
+ }
+
+ private boolean isEnabled() {
+ for (int i = 0; i < trackStates.length; i++) {
+ if (trackStates[i] != TRACK_STATE_DISABLED) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/LoadControl.java b/library/src/main/java/com/google/android/exoplayer/LoadControl.java
new file mode 100644
index 00000000000..edc6ff023f5
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/LoadControl.java
@@ -0,0 +1,76 @@
+/*
+ * 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;
+
+import com.google.android.exoplayer.upstream.Allocator;
+
+/**
+ * Coordinates multiple loaders of time series data.
+ */
+public interface LoadControl {
+
+ /**
+ * Registers a loader.
+ *
+ * @param loader The loader being registered.
+ * @param bufferSizeContribution For controls whose {@link Allocator}s maintain a pool of memory
+ * for the purpose of satisfying allocation requests, this is a hint indicating the loader's
+ * desired contribution to the size of the pool, in bytes.
+ */
+ void register(Object loader, int bufferSizeContribution);
+
+ /**
+ * Unregisters a loader.
+ *
+ * @param loader The loader being unregistered.
+ */
+ void unregister(Object loader);
+
+ /**
+ * Gets the {@link Allocator} that loaders should use to obtain memory allocations into which
+ * data can be loaded.
+ *
+ * @return The {@link Allocator} to use.
+ */
+ Allocator getAllocator();
+
+ /**
+ * Hints to the control that it should consider trimming any unused memory being held in order
+ * to satisfy allocation requests.
+ *
+ * This method is typically invoked by a recently unregistered loader, once it has released all
+ * of its allocations back to the {@link Allocator}.
+ */
+ void trimAllocator();
+
+ /**
+ * Invoked by a loader to update the control with its current state.
+ *
+ * This method must be called by a registered loader whenever its state changes. This is true
+ * even if the registered loader does not itself wish to start its next load (since the state of
+ * the loader will still affect whether other registered loaders are allowed to proceed).
+ *
+ * @param loader The loader invoking the update.
+ * @param playbackPositionUs The loader's playback position.
+ * @param nextLoadPositionUs The loader's next load position, or -1 if finished.
+ * @param loading Whether the loader is currently loading data.
+ * @param failed Whether the loader has failed, meaning it does not wish to load more data.
+ * @return True if the loader is allowed to start its next load. False otherwise.
+ */
+ boolean update(Object loader, long playbackPositionUs, long nextLoadPositionUs,
+ boolean loading, boolean failed);
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/MediaClock.java b/library/src/main/java/com/google/android/exoplayer/MediaClock.java
new file mode 100644
index 00000000000..9abd3c1f033
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/MediaClock.java
@@ -0,0 +1,79 @@
+/*
+ * 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;
+
+import android.os.SystemClock;
+
+/**
+ * A simple clock for tracking the progression of media time. The clock can be started, stopped and
+ * its time can be set and retrieved. When started, this clock is based on
+ * {@link SystemClock#elapsedRealtime()}.
+ */
+/* package */ class MediaClock {
+
+ private boolean started;
+
+ /**
+ * The media time when the clock was last set or stopped.
+ */
+ private long timeUs;
+
+ /**
+ * The difference between {@link SystemClock#elapsedRealtime()} and {@link #timeUs}
+ * when the clock was last set or started.
+ */
+ private long deltaUs;
+
+ /**
+ * Starts the clock. Does nothing if the clock is already started.
+ */
+ public void start() {
+ if (!started) {
+ started = true;
+ deltaUs = elapsedRealtimeMinus(timeUs);
+ }
+ }
+
+ /**
+ * Stops the clock. Does nothing if the clock is already stopped.
+ */
+ public void stop() {
+ if (started) {
+ timeUs = elapsedRealtimeMinus(deltaUs);
+ started = false;
+ }
+ }
+
+ /**
+ * @param timeUs The time to set in microseconds.
+ */
+ public void setTimeUs(long timeUs) {
+ this.timeUs = timeUs;
+ deltaUs = elapsedRealtimeMinus(timeUs);
+ }
+
+ /**
+ * @return The current time in microseconds.
+ */
+ public long getTimeUs() {
+ return started ? elapsedRealtimeMinus(deltaUs) : timeUs;
+ }
+
+ private long elapsedRealtimeMinus(long microSeconds) {
+ return SystemClock.elapsedRealtime() * 1000 - microSeconds;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java
new file mode 100644
index 00000000000..a8f34c50396
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java
@@ -0,0 +1,733 @@
+/*
+ * 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;
+
+import com.google.android.exoplayer.drm.DrmSessionManager;
+import com.google.android.exoplayer.util.Assertions;
+import com.google.android.exoplayer.util.MimeTypes;
+import com.google.android.exoplayer.util.Util;
+
+import android.annotation.TargetApi;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioTimestamp;
+import android.media.AudioTrack;
+import android.media.MediaCodec;
+import android.media.MediaFormat;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.util.Log;
+
+import java.lang.reflect.Method;
+import java.nio.ByteBuffer;
+
+/**
+ * Decodes and renders audio using {@link MediaCodec} and {@link AudioTrack}.
+ */
+@TargetApi(16)
+public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
+
+ /**
+ * Interface definition for a callback to be notified of {@link MediaCodecAudioTrackRenderer}
+ * events.
+ */
+ public interface EventListener extends MediaCodecTrackRenderer.EventListener {
+
+ /**
+ * Invoked when an {@link AudioTrack} fails to initialize.
+ *
+ * @param e The corresponding exception.
+ */
+ void onAudioTrackInitializationError(AudioTrackInitializationException e);
+
+ }
+
+ /**
+ * Thrown when a failure occurs instantiating an audio track.
+ */
+ public static class AudioTrackInitializationException extends Exception {
+
+ /**
+ * The state as reported by {@link AudioTrack#getState()}
+ */
+ public final int audioTrackState;
+
+ public AudioTrackInitializationException(int audioTrackState, int sampleRate,
+ int channelConfig, int bufferSize) {
+ super("AudioTrack init failed: " + audioTrackState + ", Config(" + sampleRate + ", " +
+ channelConfig + ", " + bufferSize + ")");
+ this.audioTrackState = audioTrackState;
+ }
+
+ }
+
+ /**
+ * The type of a message that can be passed to an instance of this class via
+ * {@link ExoPlayer#sendMessage} or {@link ExoPlayer#blockingSendMessage}. The message object
+ * should be a {@link Float} with 0 being silence and 1 being unity gain.
+ */
+ public static final int MSG_SET_VOLUME = 1;
+
+ /**
+ * The default multiplication factor used when determining the size of the underlying
+ * {@link AudioTrack}'s buffer.
+ */
+ public static final float DEFAULT_MIN_BUFFER_MULTIPLICATION_FACTOR = 4;
+
+ private static final String TAG = "MediaCodecAudioTrackRenderer";
+
+ private static final long MICROS_PER_SECOND = 1000000L;
+
+ private static final int MAX_PLAYHEAD_OFFSET_COUNT = 10;
+ private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000;
+ private static final int MIN_TIMESTAMP_SAMPLE_INTERVAL_US = 500000;
+
+ private final EventListener eventListener;
+ private final ConditionVariable audioTrackReleasingConditionVariable;
+ private final AudioTimestampCompat audioTimestampCompat;
+ private final long[] playheadOffsets;
+ private final float minBufferMultiplicationFactor;
+ private int nextPlayheadOffsetIndex;
+ private int playheadOffsetCount;
+ private long smoothedPlayheadOffsetUs;
+ private long lastPlayheadSampleTimeUs;
+ private boolean audioTimestampSet;
+ private long lastTimestampSampleTimeUs;
+ private long lastRawPlaybackHeadPosition;
+ private long rawPlaybackHeadWrapCount;
+
+ private int sampleRate;
+ private int frameSize;
+ private int channelConfig;
+ private int minBufferSize;
+ private int bufferSize;
+
+ private AudioTrack audioTrack;
+ private Method audioTrackGetLatencyMethod;
+ private int audioSessionId;
+ private long submittedBytes;
+ private boolean audioTrackStartMediaTimeSet;
+ private long audioTrackStartMediaTimeUs;
+ private long audioTrackResumeSystemTimeUs;
+ private long lastReportedCurrentPositionUs;
+ private long audioTrackLatencyUs;
+ private float volume;
+
+ private byte[] temporaryBuffer;
+ private int temporaryBufferOffset;
+ private int temporaryBufferSize;
+
+ /**
+ * @param source The upstream source from which the renderer obtains samples.
+ */
+ public MediaCodecAudioTrackRenderer(SampleSource source) {
+ this(source, null, true);
+ }
+
+ /**
+ * @param source The upstream source from which the renderer obtains samples.
+ * @param drmSessionManager For use with encrypted content. May be null if support for encrypted
+ * content is not required.
+ * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
+ * For example a media file may start with a short clear region so as to allow playback to
+ * begin in parallel with key acquisision. This parameter specifies whether the renderer is
+ * permitted to play clear regions of encrypted media files before {@code drmSessionManager}
+ * has obtained the keys necessary to decrypt encrypted regions of the media.
+ */
+ public MediaCodecAudioTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager,
+ boolean playClearSamplesWithoutKeys) {
+ this(source, drmSessionManager, playClearSamplesWithoutKeys, null, null);
+ }
+
+ /**
+ * @param source The upstream source from which the renderer obtains samples.
+ * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+ * null if delivery of events is not required.
+ * @param eventListener A listener of events. May be null if delivery of events is not required.
+ */
+ public MediaCodecAudioTrackRenderer(SampleSource source, Handler eventHandler,
+ EventListener eventListener) {
+ this(source, null, true, eventHandler, eventListener);
+ }
+
+ /**
+ * @param source The upstream source from which the renderer obtains samples.
+ * @param drmSessionManager For use with encrypted content. May be null if support for encrypted
+ * content is not required.
+ * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
+ * For example a media file may start with a short clear region so as to allow playback to
+ * begin in parallel with key acquisision. This parameter specifies whether the renderer is
+ * permitted to play clear regions of encrypted media files before {@code drmSessionManager}
+ * has obtained the keys necessary to decrypt encrypted regions of the media.
+ * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+ * null if delivery of events is not required.
+ * @param eventListener A listener of events. May be null if delivery of events is not required.
+ */
+ public MediaCodecAudioTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager,
+ boolean playClearSamplesWithoutKeys, Handler eventHandler, EventListener eventListener) {
+ this(source, drmSessionManager, playClearSamplesWithoutKeys,
+ DEFAULT_MIN_BUFFER_MULTIPLICATION_FACTOR, eventHandler, eventListener);
+ }
+
+ /**
+ * @param source The upstream source from which the renderer obtains samples.
+ * @param minBufferMultiplicationFactor When instantiating an underlying {@link AudioTrack},
+ * the size of the track's is calculated as this value multiplied by the minimum buffer size
+ * obtained from {@link AudioTrack#getMinBufferSize(int, int, int)}. The multiplication
+ * factor must be greater than or equal to 1.
+ * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+ * null if delivery of events is not required.
+ * @param eventListener A listener of events. May be null if delivery of events is not required.
+ */
+ public MediaCodecAudioTrackRenderer(SampleSource source, float minBufferMultiplicationFactor,
+ Handler eventHandler, EventListener eventListener) {
+ this(source, null, true, minBufferMultiplicationFactor, eventHandler, eventListener);
+ }
+
+ /**
+ * @param source The upstream source from which the renderer obtains samples.
+ * @param drmSessionManager For use with encrypted content. May be null if support for encrypted
+ * content is not required.
+ * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
+ * For example a media file may start with a short clear region so as to allow playback to
+ * begin in parallel with key acquisision. This parameter specifies whether the renderer is
+ * permitted to play clear regions of encrypted media files before {@code drmSessionManager}
+ * has obtained the keys necessary to decrypt encrypted regions of the media.
+ * @param minBufferMultiplicationFactor When instantiating an underlying {@link AudioTrack},
+ * the size of the track's is calculated as this value multiplied by the minimum buffer size
+ * obtained from {@link AudioTrack#getMinBufferSize(int, int, int)}. The multiplication
+ * factor must be greater than or equal to 1.
+ * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+ * null if delivery of events is not required.
+ * @param eventListener A listener of events. May be null if delivery of events is not required.
+ */
+ public MediaCodecAudioTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager,
+ boolean playClearSamplesWithoutKeys, float minBufferMultiplicationFactor,
+ Handler eventHandler, EventListener eventListener) {
+ super(source, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, eventListener);
+ Assertions.checkState(minBufferMultiplicationFactor >= 1);
+ this.minBufferMultiplicationFactor = minBufferMultiplicationFactor;
+ this.eventListener = eventListener;
+ audioTrackReleasingConditionVariable = new ConditionVariable(true);
+ if (Util.SDK_INT >= 19) {
+ audioTimestampCompat = new AudioTimestampCompatV19();
+ } else {
+ audioTimestampCompat = new NoopAudioTimestampCompat();
+ }
+ if (Util.SDK_INT >= 18) {
+ try {
+ audioTrackGetLatencyMethod = AudioTrack.class.getMethod("getLatency", (Class>[]) null);
+ } catch (NoSuchMethodException e) {
+ // There's no guarantee this method exists. Do nothing.
+ }
+ }
+ playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT];
+ volume = 1.0f;
+ }
+
+ @Override
+ protected boolean isTimeSource() {
+ return true;
+ }
+
+ @Override
+ protected boolean handlesMimeType(String mimeType) {
+ return MimeTypes.isAudio(mimeType) && super.handlesMimeType(mimeType);
+ }
+
+ @Override
+ protected void onEnabled(long timeUs, boolean joining) {
+ super.onEnabled(timeUs, joining);
+ lastReportedCurrentPositionUs = 0;
+ }
+
+ @Override
+ protected void doSomeWork(long timeUs) throws ExoPlaybackException {
+ super.doSomeWork(timeUs);
+ maybeSampleSyncParams();
+ }
+
+ @Override
+ protected void onOutputFormatChanged(MediaFormat format) {
+ releaseAudioTrack();
+ this.sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
+ int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
+ int channelConfig;
+ switch (channelCount) {
+ case 1:
+ channelConfig = AudioFormat.CHANNEL_OUT_MONO;
+ break;
+ case 2:
+ channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
+ break;
+ case 6:
+ channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
+ break;
+ default:
+ throw new IllegalArgumentException("Unsupported channel count: " + channelCount);
+ }
+ this.channelConfig = channelConfig;
+ this.minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig,
+ AudioFormat.ENCODING_PCM_16BIT);
+ this.bufferSize = (int) (minBufferMultiplicationFactor * minBufferSize);
+ this.frameSize = 2 * channelCount; // 2 bytes per 16 bit sample * number of channels.
+ }
+
+ private void initAudioTrack() throws ExoPlaybackException {
+ // If we're asynchronously releasing a previous audio track then we block until it has been
+ // released. This guarantees that we cannot end up in a state where we have multiple audio
+ // track instances. Without this guarantee it would be possible, in extreme cases, to exhaust
+ // the shared memory that's available for audio track buffers. This would in turn cause the
+ // initialization of the audio track to fail.
+ audioTrackReleasingConditionVariable.block();
+ if (audioSessionId == 0) {
+ audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig,
+ AudioFormat.ENCODING_PCM_16BIT, bufferSize, AudioTrack.MODE_STREAM);
+ checkAudioTrackInitialized();
+ audioSessionId = audioTrack.getAudioSessionId();
+ onAudioSessionId(audioSessionId);
+ } else {
+ // Re-attach to the same audio session.
+ audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig,
+ AudioFormat.ENCODING_PCM_16BIT, bufferSize, AudioTrack.MODE_STREAM, audioSessionId);
+ checkAudioTrackInitialized();
+ }
+ audioTrack.setStereoVolume(volume, volume);
+ if (getState() == TrackRenderer.STATE_STARTED) {
+ audioTrackResumeSystemTimeUs = System.nanoTime() / 1000;
+ audioTrack.play();
+ }
+ }
+
+ /**
+ * Checks that {@link #audioTrack} has been successfully initialized. If it has then calling this
+ * method is a no-op. If it hasn't then {@link #audioTrack} is released and set to null, and an
+ * exception is thrown.
+ *
+ * @throws ExoPlaybackException If {@link #audioTrack} has not been successfully initialized.
+ */
+ private void checkAudioTrackInitialized() throws ExoPlaybackException {
+ int audioTrackState = audioTrack.getState();
+ if (audioTrackState == AudioTrack.STATE_INITIALIZED) {
+ return;
+ }
+ // The track is not successfully initialized. Release and null the track.
+ try {
+ audioTrack.release();
+ } catch (Exception e) {
+ // The track has already failed to initialize, so it wouldn't be that surprising if release
+ // were to fail too. Swallow the exception.
+ } finally {
+ audioTrack = null;
+ }
+ // Propagate the relevant exceptions.
+ AudioTrackInitializationException exception = new AudioTrackInitializationException(
+ audioTrackState, sampleRate, channelConfig, bufferSize);
+ notifyAudioTrackInitializationError(exception);
+ throw new ExoPlaybackException(exception);
+ }
+
+ /**
+ * Invoked when the audio session id becomes known. Once the id is known it will not change
+ * (and hence this method will not be invoked again) unless the renderer is disabled and then
+ * subsequently re-enabled.
+ *
+ * The default implementation is a no-op. One reason for overriding this method would be to
+ * instantiate and enable a {@link android.media.audiofx.Virtualizer} in order to spatialize the
+ * audio channels. For this use case, any {@link android.media.audiofx.Virtualizer} instances
+ * should be released in {@link #onDisabled()} (if not before).
+ *
+ * @param audioSessionId The audio session id.
+ */
+ protected void onAudioSessionId(int audioSessionId) {
+ // Do nothing.
+ }
+
+ private void releaseAudioTrack() {
+ if (audioTrack != null) {
+ submittedBytes = 0;
+ temporaryBufferSize = 0;
+ lastRawPlaybackHeadPosition = 0;
+ rawPlaybackHeadWrapCount = 0;
+ audioTrackStartMediaTimeUs = 0;
+ audioTrackStartMediaTimeSet = false;
+ resetSyncParams();
+ int playState = audioTrack.getPlayState();
+ if (playState == AudioTrack.PLAYSTATE_PLAYING) {
+ audioTrack.pause();
+ }
+ // AudioTrack.release can take some time, so we call it on a background thread.
+ final AudioTrack toRelease = audioTrack;
+ audioTrack = null;
+ audioTrackReleasingConditionVariable.close();
+ new Thread() {
+ @Override
+ public void run() {
+ try {
+ toRelease.release();
+ } finally {
+ audioTrackReleasingConditionVariable.open();
+ }
+ }
+ }.start();
+ }
+ }
+
+ @Override
+ protected void onStarted() {
+ super.onStarted();
+ if (audioTrack != null) {
+ audioTrackResumeSystemTimeUs = System.nanoTime() / 1000;
+ audioTrack.play();
+ }
+ }
+
+ @Override
+ protected void onStopped() {
+ super.onStopped();
+ if (audioTrack != null) {
+ resetSyncParams();
+ audioTrack.pause();
+ }
+ }
+
+ @Override
+ protected boolean isEnded() {
+ // We've exhausted the output stream, and the AudioTrack has either played all of the data
+ // submitted, or has been fed insufficient data to begin playback.
+ return super.isEnded() && (getPendingFrameCount() == 0 || submittedBytes < minBufferSize);
+ }
+
+ @Override
+ protected boolean isReady() {
+ return getPendingFrameCount() > 0;
+ }
+
+ /**
+ * This method uses a variety of techniques to compute the current position:
+ *
+ * 1. Prior to playback having started, calls up to the super class to obtain the pending seek
+ * position.
+ * 2. During playback, uses AudioTimestamps obtained from AudioTrack.getTimestamp on supported
+ * devices.
+ * 3. Else, derives a smoothed position by sampling the AudioTrack's frame position.
+ */
+ @Override
+ protected long getCurrentPositionUs() {
+ long systemClockUs = System.nanoTime() / 1000;
+ long currentPositionUs;
+ if (audioTrack == null || !audioTrackStartMediaTimeSet) {
+ // The AudioTrack hasn't started.
+ currentPositionUs = super.getCurrentPositionUs();
+ } else if (audioTimestampSet) {
+ // How long ago in the past the audio timestamp is (negative if it's in the future)
+ long presentationDiff = systemClockUs - (audioTimestampCompat.getNanoTime() / 1000);
+ long framesDiff = durationUsToFrames(presentationDiff);
+ // The position of the frame that's currently being presented.
+ long currentFramePosition = audioTimestampCompat.getFramePosition() + framesDiff;
+ currentPositionUs = framesToDurationUs(currentFramePosition) + audioTrackStartMediaTimeUs;
+ } else {
+ if (playheadOffsetCount == 0) {
+ // The AudioTrack has started, but we don't have any samples to compute a smoothed position.
+ currentPositionUs = getPlayheadPositionUs() + audioTrackStartMediaTimeUs;
+ } else {
+ // getPlayheadPositionUs() only has a granularity of ~20ms, so we base the position off the
+ // system clock (and a smoothed offset between it and the playhead position) so as to
+ // prevent jitter in the reported positions.
+ currentPositionUs = systemClockUs + smoothedPlayheadOffsetUs + audioTrackStartMediaTimeUs;
+ }
+ if (!isEnded()) {
+ currentPositionUs -= audioTrackLatencyUs;
+ }
+ }
+ // Make sure we don't ever report time moving backwards as a result of smoothing or switching
+ // between the various code paths above.
+ currentPositionUs = Math.max(lastReportedCurrentPositionUs, currentPositionUs);
+ lastReportedCurrentPositionUs = currentPositionUs;
+ return currentPositionUs;
+ }
+
+ private void maybeSampleSyncParams() {
+ if (audioTrack == null || !audioTrackStartMediaTimeSet || getState() != STATE_STARTED) {
+ // The AudioTrack isn't playing.
+ return;
+ }
+ long playheadPositionUs = getPlayheadPositionUs();
+ if (playheadPositionUs == 0) {
+ // The AudioTrack hasn't output anything yet.
+ return;
+ }
+ long systemClockUs = System.nanoTime() / 1000;
+ if (systemClockUs - lastPlayheadSampleTimeUs >= MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US) {
+ // Take a new sample and update the smoothed offset between the system clock and the playhead.
+ playheadOffsets[nextPlayheadOffsetIndex] = playheadPositionUs - systemClockUs;
+ nextPlayheadOffsetIndex = (nextPlayheadOffsetIndex + 1) % MAX_PLAYHEAD_OFFSET_COUNT;
+ if (playheadOffsetCount < MAX_PLAYHEAD_OFFSET_COUNT) {
+ playheadOffsetCount++;
+ }
+ lastPlayheadSampleTimeUs = systemClockUs;
+ smoothedPlayheadOffsetUs = 0;
+ for (int i = 0; i < playheadOffsetCount; i++) {
+ smoothedPlayheadOffsetUs += playheadOffsets[i] / playheadOffsetCount;
+ }
+ }
+
+ if (systemClockUs - lastTimestampSampleTimeUs >= MIN_TIMESTAMP_SAMPLE_INTERVAL_US) {
+ audioTimestampSet = audioTimestampCompat.initTimestamp(audioTrack);
+ if (audioTimestampSet
+ && (audioTimestampCompat.getNanoTime() / 1000) < audioTrackResumeSystemTimeUs) {
+ // The timestamp was set, but it corresponds to a time before the track was most recently
+ // resumed.
+ audioTimestampSet = false;
+ }
+ if (audioTrackGetLatencyMethod != null) {
+ try {
+ // Compute the audio track latency, excluding the latency due to the buffer (leaving
+ // latency due to the mixer and audio hardware driver).
+ audioTrackLatencyUs =
+ (Integer) audioTrackGetLatencyMethod.invoke(audioTrack, (Object[]) null) * 1000L -
+ framesToDurationUs(bufferSize / frameSize);
+ // Sanity check that the latency is non-negative.
+ audioTrackLatencyUs = Math.max(audioTrackLatencyUs, 0);
+ } catch (Exception e) {
+ // The method existed, but doesn't work. Don't try again.
+ audioTrackGetLatencyMethod = null;
+ }
+ }
+ lastTimestampSampleTimeUs = systemClockUs;
+ }
+ }
+
+ private void resetSyncParams() {
+ smoothedPlayheadOffsetUs = 0;
+ playheadOffsetCount = 0;
+ nextPlayheadOffsetIndex = 0;
+ lastPlayheadSampleTimeUs = 0;
+ audioTimestampSet = false;
+ lastTimestampSampleTimeUs = 0;
+ }
+
+ private long getPlayheadPositionUs() {
+ return framesToDurationUs(getPlaybackHeadPosition());
+ }
+
+ private long framesToDurationUs(long frameCount) {
+ return (frameCount * MICROS_PER_SECOND) / sampleRate;
+ }
+
+ private long durationUsToFrames(long durationUs) {
+ return (durationUs * sampleRate) / MICROS_PER_SECOND;
+ }
+
+ @Override
+ protected void onDisabled() {
+ super.onDisabled();
+ releaseAudioTrack();
+ audioSessionId = 0;
+ }
+
+ @Override
+ protected void seekTo(long timeUs) throws ExoPlaybackException {
+ super.seekTo(timeUs);
+ // TODO: Try and re-use the same AudioTrack instance once [redacted] is fixed.
+ releaseAudioTrack();
+ lastReportedCurrentPositionUs = 0;
+ }
+
+ @Override
+ protected boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer,
+ MediaCodec.BufferInfo bufferInfo, int bufferIndex) throws ExoPlaybackException {
+ if (temporaryBufferSize == 0) {
+ // This is the first time we've seen this {@code buffer}.
+
+ // Note: presentationTimeUs corresponds to the end of the sample, not the start.
+ long bufferStartTime = bufferInfo.presentationTimeUs -
+ framesToDurationUs(bufferInfo.size / frameSize);
+ if (!audioTrackStartMediaTimeSet) {
+ audioTrackStartMediaTimeUs = Math.max(0, bufferStartTime);
+ audioTrackStartMediaTimeSet = true;
+ } else {
+ // Sanity check that bufferStartTime is consistent with the expected value.
+ long expectedBufferStartTime = audioTrackStartMediaTimeUs +
+ framesToDurationUs(submittedBytes / frameSize);
+ if (Math.abs(expectedBufferStartTime - bufferStartTime) > 200000) {
+ Log.e(TAG, "Discontinuity detected [expected " + expectedBufferStartTime + ", got " +
+ bufferStartTime + "]");
+ // Adjust audioTrackStartMediaTimeUs to compensate for the discontinuity. Also reset
+ // lastReportedCurrentPositionUs to allow time to jump backwards if it really wants to.
+ audioTrackStartMediaTimeUs += (bufferStartTime - expectedBufferStartTime);
+ lastReportedCurrentPositionUs = 0;
+ }
+ }
+
+ // Copy {@code buffer} into {@code temporaryBuffer}.
+ // TODO: Bypass this copy step on versions of Android where [redacted] is implemented.
+ if (temporaryBuffer == null || temporaryBuffer.length < bufferInfo.size) {
+ temporaryBuffer = new byte[bufferInfo.size];
+ }
+ buffer.position(bufferInfo.offset);
+ buffer.get(temporaryBuffer, 0, bufferInfo.size);
+ temporaryBufferOffset = 0;
+ temporaryBufferSize = bufferInfo.size;
+ }
+
+ if (audioTrack == null) {
+ initAudioTrack();
+ }
+
+ // TODO: Don't bother doing this once [redacted] is fixed.
+ // Work out how many bytes we can write without the risk of blocking.
+ int bytesPending = (int) (submittedBytes - getPlaybackHeadPosition() * frameSize);
+ int bytesToWrite = bufferSize - bytesPending;
+
+ if (bytesToWrite > 0) {
+ bytesToWrite = Math.min(temporaryBufferSize, bytesToWrite);
+ audioTrack.write(temporaryBuffer, temporaryBufferOffset, bytesToWrite);
+ temporaryBufferOffset += bytesToWrite;
+ temporaryBufferSize -= bytesToWrite;
+ submittedBytes += bytesToWrite;
+ if (temporaryBufferSize == 0) {
+ codec.releaseOutputBuffer(bufferIndex, false);
+ codecCounters.renderedOutputBufferCount++;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * {@link AudioTrack#getPlaybackHeadPosition()} returns a value intended to be interpreted as
+ * an unsigned 32 bit integer, which also wraps around periodically. This method returns the
+ * playback head position as a long that will only wrap around if the value exceeds
+ * {@link Long#MAX_VALUE} (which in practice will never happen).
+ *
+ * @return {@link AudioTrack#getPlaybackHeadPosition()} of {@link #audioTrack} expressed as a
+ * long.
+ */
+ private long getPlaybackHeadPosition() {
+ long rawPlaybackHeadPosition = 0xFFFFFFFFL & audioTrack.getPlaybackHeadPosition();
+ if (lastRawPlaybackHeadPosition > rawPlaybackHeadPosition) {
+ // The value must have wrapped around.
+ rawPlaybackHeadWrapCount++;
+ }
+ lastRawPlaybackHeadPosition = rawPlaybackHeadPosition;
+ return rawPlaybackHeadPosition + (rawPlaybackHeadWrapCount << 32);
+ }
+
+ private int getPendingFrameCount() {
+ return audioTrack == null ?
+ 0 : (int) (submittedBytes / frameSize - getPlaybackHeadPosition());
+ }
+
+ @Override
+ public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
+ if (messageType == MSG_SET_VOLUME) {
+ setVolume((Float) message);
+ } else {
+ super.handleMessage(messageType, message);
+ }
+ }
+
+ private void setVolume(float volume) {
+ this.volume = volume;
+ if (audioTrack != null) {
+ audioTrack.setStereoVolume(volume, volume);
+ }
+ }
+
+ private void notifyAudioTrackInitializationError(final AudioTrackInitializationException e) {
+ if (eventHandler != null && eventListener != null) {
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ eventListener.onAudioTrackInitializationError(e);
+ }
+ });
+ }
+ }
+
+ /**
+ * Interface exposing the {@link AudioTimestamp} methods we need that were added in SDK 19.
+ */
+ private interface AudioTimestampCompat {
+
+ /**
+ * Returns true if the audioTimestamp was retrieved from the audioTrack.
+ */
+ boolean initTimestamp(AudioTrack audioTrack);
+
+ long getNanoTime();
+
+ long getFramePosition();
+
+ }
+
+ /**
+ * The AudioTimestampCompat implementation for SDK < 19 that does nothing or throws an exception.
+ */
+ private static final class NoopAudioTimestampCompat implements AudioTimestampCompat {
+
+ @Override
+ public boolean initTimestamp(AudioTrack audioTrack) {
+ return false;
+ }
+
+ @Override
+ public long getNanoTime() {
+ // Should never be called if initTimestamp() returned false.
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long getFramePosition() {
+ // Should never be called if initTimestamp() returned false.
+ throw new UnsupportedOperationException();
+ }
+
+ }
+
+ /**
+ * The AudioTimestampCompat implementation for SDK >= 19 that simply calls through to the actual
+ * implementations added in SDK 19.
+ */
+ @TargetApi(19)
+ private static final class AudioTimestampCompatV19 implements AudioTimestampCompat {
+
+ private final AudioTimestamp audioTimestamp;
+
+ public AudioTimestampCompatV19() {
+ audioTimestamp = new AudioTimestamp();
+ }
+
+ @Override
+ public boolean initTimestamp(AudioTrack audioTrack) {
+ return audioTrack.getTimestamp(audioTimestamp);
+ }
+
+ @Override
+ public long getNanoTime() {
+ return audioTimestamp.nanoTime;
+ }
+
+ @Override
+ public long getFramePosition() {
+ return audioTimestamp.framePosition;
+ }
+
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java
new file mode 100644
index 00000000000..5d6fb810aa8
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java
@@ -0,0 +1,735 @@
+/*
+ * 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;
+
+import com.google.android.exoplayer.drm.DrmSessionManager;
+import com.google.android.exoplayer.util.Assertions;
+import com.google.android.exoplayer.util.Util;
+
+import android.annotation.TargetApi;
+import android.media.MediaCodec;
+import android.media.MediaCodec.CryptoException;
+import android.media.MediaCrypto;
+import android.media.MediaExtractor;
+import android.os.Handler;
+import android.os.SystemClock;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * An abstract {@link TrackRenderer} that uses {@link MediaCodec} to decode samples for rendering.
+ */
+@TargetApi(16)
+public abstract class MediaCodecTrackRenderer extends TrackRenderer {
+
+ /**
+ * Interface definition for a callback to be notified of {@link MediaCodecTrackRenderer} events.
+ */
+ public interface EventListener {
+
+ /**
+ * Invoked when a decoder fails to initialize.
+ *
+ * @param e The corresponding exception.
+ */
+ void onDecoderInitializationError(DecoderInitializationException e);
+
+ /**
+ * Invoked when a decoder operation raises a {@link CryptoException}.
+ *
+ * @param e The corresponding exception.
+ */
+ void onCryptoError(CryptoException e);
+
+ }
+
+ /**
+ * Thrown when a failure occurs instantiating a decoder.
+ */
+ public static class DecoderInitializationException extends Exception {
+
+ /**
+ * The name of the decoder that failed to initialize.
+ */
+ public final String decoderName;
+
+ public DecoderInitializationException(String decoderName, MediaFormat mediaFormat,
+ Exception cause) {
+ super("Decoder init failed: " + decoderName + ", " + mediaFormat, cause);
+ this.decoderName = decoderName;
+ }
+
+ }
+
+ /**
+ * If the {@link MediaCodec} is hotswapped (i.e. replaced during playback), this is the period of
+ * time during which {@link #isReady()} will report true regardless of whether the new codec has
+ * output frames that are ready to be rendered.
+ *
+ * This allows codec hotswapping to be performed seamlessly, without interrupting the playback of
+ * other renderers, provided the new codec is able to decode some frames within this time period.
+ */
+ private static final long MAX_CODEC_HOTSWAP_TIME_MS = 1000;
+
+ /**
+ * There is no pending adaptive reconfiguration work.
+ */
+ private static final int RECONFIGURATION_STATE_NONE = 0;
+ /**
+ * Codec configuration data needs to be written into the next buffer.
+ */
+ private static final int RECONFIGURATION_STATE_WRITE_PENDING = 1;
+ /**
+ * Codec configuration data has been written into the next buffer, but that buffer still needs to
+ * be returned to the codec.
+ */
+ private static final int RECONFIGURATION_STATE_QUEUE_PENDING = 2;
+
+ public final CodecCounters codecCounters;
+
+ private final DrmSessionManager drmSessionManager;
+ private final boolean playClearSamplesWithoutKeys;
+ private final SampleSource source;
+ private final SampleHolder sampleHolder;
+ private final FormatHolder formatHolder;
+ private final HashSet decodeOnlyPresentationTimestamps;
+ private final MediaCodec.BufferInfo outputBufferInfo;
+ private final EventListener eventListener;
+ protected final Handler eventHandler;
+
+ private MediaFormat format;
+ private Map drmInitData;
+ private MediaCodec codec;
+ private boolean codecIsAdaptive;
+ private ByteBuffer[] inputBuffers;
+ private ByteBuffer[] outputBuffers;
+ private long codecHotswapTimeMs;
+ private int inputIndex;
+ private int outputIndex;
+ private boolean openedDrmSession;
+ private boolean codecReconfigured;
+ private int codecReconfigurationState;
+
+ private int trackIndex;
+ private boolean inputStreamEnded;
+ private boolean outputStreamEnded;
+ private boolean waitingForKeys;
+ private boolean waitingForFirstSyncFrame;
+ private long currentPositionUs;
+
+ /**
+ * @param source The upstream source from which the renderer obtains samples.
+ * @param drmSessionManager For use with encrypted media. May be null if support for encrypted
+ * media is not required.
+ * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
+ * For example a media file may start with a short clear region so as to allow playback to
+ * begin in parallel with key acquisision. This parameter specifies whether the renderer is
+ * permitted to play clear regions of encrypted media files before {@code drmSessionManager}
+ * has obtained the keys necessary to decrypt encrypted regions of the media.
+ * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+ * null if delivery of events is not required.
+ * @param eventListener A listener of events. May be null if delivery of events is not required.
+ */
+ public MediaCodecTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager,
+ boolean playClearSamplesWithoutKeys, Handler eventHandler, EventListener eventListener) {
+ Assertions.checkState(Util.SDK_INT >= 16);
+ this.source = source;
+ this.drmSessionManager = drmSessionManager;
+ this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
+ this.eventHandler = eventHandler;
+ this.eventListener = eventListener;
+ codecCounters = new CodecCounters();
+ sampleHolder = new SampleHolder(false);
+ formatHolder = new FormatHolder();
+ decodeOnlyPresentationTimestamps = new HashSet();
+ outputBufferInfo = new MediaCodec.BufferInfo();
+ }
+
+ @Override
+ protected int doPrepare() throws ExoPlaybackException {
+ try {
+ boolean sourcePrepared = source.prepare();
+ if (!sourcePrepared) {
+ return TrackRenderer.STATE_UNPREPARED;
+ }
+ } catch (IOException e) {
+ throw new ExoPlaybackException(e);
+ }
+
+ for (int i = 0; i < source.getTrackCount(); i++) {
+ // TODO: Right now this is getting the mime types of the container format
+ // (e.g. audio/mp4 and video/mp4 for fragmented mp4). It needs to be getting the mime types
+ // of the actual samples (e.g. audio/mp4a-latm and video/avc).
+ if (handlesMimeType(source.getTrackInfo(i).mimeType)) {
+ trackIndex = i;
+ return TrackRenderer.STATE_PREPARED;
+ }
+ }
+
+ return TrackRenderer.STATE_IGNORE;
+ }
+
+ @SuppressWarnings("unused")
+ protected boolean handlesMimeType(String mimeType) {
+ return true;
+ // TODO: Uncomment once the TODO above is fixed.
+ // DecoderInfoUtil.getDecoder(mimeType) != null;
+ }
+
+ @Override
+ protected void onEnabled(long timeUs, boolean joining) {
+ source.enable(trackIndex, timeUs);
+ inputStreamEnded = false;
+ outputStreamEnded = false;
+ waitingForKeys = false;
+ currentPositionUs = timeUs;
+ }
+
+ /**
+ * Configures a newly created {@link MediaCodec}. Sub-classes should
+ * override this method if they wish to configure the codec with a
+ * non-null surface.
+ **/
+ protected void configureCodec(MediaCodec codec, android.media.MediaFormat x, MediaCrypto crypto) {
+ codec.configure(x, null, crypto, 0);
+ }
+
+ protected final void maybeInitCodec() throws ExoPlaybackException {
+ if (!shouldInitCodec()) {
+ return;
+ }
+
+ String mimeType = format.mimeType;
+ MediaCrypto mediaCrypto = null;
+ boolean requiresSecureDecoder = false;
+ if (drmInitData != null) {
+ if (drmSessionManager == null) {
+ throw new ExoPlaybackException("Media requires a DrmSessionManager");
+ }
+ if (!openedDrmSession) {
+ drmSessionManager.open(drmInitData, mimeType);
+ openedDrmSession = true;
+ }
+ int drmSessionState = drmSessionManager.getState();
+ if (drmSessionState == DrmSessionManager.STATE_ERROR) {
+ throw new ExoPlaybackException(drmSessionManager.getError());
+ } else if (drmSessionState == DrmSessionManager.STATE_OPENED
+ || drmSessionState == DrmSessionManager.STATE_OPENED_WITH_KEYS) {
+ mediaCrypto = drmSessionManager.getMediaCrypto();
+ requiresSecureDecoder = drmSessionManager.requiresSecureDecoderComponent(mimeType);
+ } else {
+ // The drm session isn't open yet.
+ return;
+ }
+ }
+
+ DecoderInfo selectedDecoderInfo = MediaCodecUtil.getDecoderInfo(mimeType);
+ String selectedDecoderName = selectedDecoderInfo.name;
+ if (requiresSecureDecoder) {
+ selectedDecoderName = getSecureDecoderName(selectedDecoderName);
+ }
+ codecIsAdaptive = selectedDecoderInfo.adaptive;
+ try {
+ codec = MediaCodec.createByCodecName(selectedDecoderName);
+ configureCodec(codec, format.getFrameworkMediaFormatV16(), mediaCrypto);
+ codec.start();
+ inputBuffers = codec.getInputBuffers();
+ outputBuffers = codec.getOutputBuffers();
+ } catch (Exception e) {
+ DecoderInitializationException exception = new DecoderInitializationException(
+ selectedDecoderName, format, e);
+ notifyDecoderInitializationError(exception);
+ throw new ExoPlaybackException(exception);
+ }
+ codecHotswapTimeMs = getState() == TrackRenderer.STATE_STARTED ?
+ SystemClock.elapsedRealtime() : -1;
+ inputIndex = -1;
+ outputIndex = -1;
+ waitingForFirstSyncFrame = true;
+ codecCounters.codecInitCount++;
+ }
+
+ protected boolean shouldInitCodec() {
+ return codec == null && format != null;
+ }
+
+ protected final boolean codecInitialized() {
+ return codec != null;
+ }
+
+ protected final boolean haveFormat() {
+ return format != null;
+ }
+
+ @Override
+ protected void onDisabled() {
+ releaseCodec();
+ format = null;
+ drmInitData = null;
+ if (openedDrmSession) {
+ drmSessionManager.close();
+ openedDrmSession = false;
+ }
+ source.disable(trackIndex);
+ }
+
+ protected void releaseCodec() {
+ if (codec != null) {
+ codecHotswapTimeMs = -1;
+ inputIndex = -1;
+ outputIndex = -1;
+ decodeOnlyPresentationTimestamps.clear();
+ inputBuffers = null;
+ outputBuffers = null;
+ codecReconfigured = false;
+ codecIsAdaptive = false;
+ codecReconfigurationState = RECONFIGURATION_STATE_NONE;
+ codecCounters.codecReleaseCount++;
+ try {
+ codec.stop();
+ } finally {
+ try {
+ codec.release();
+ } finally {
+ codec = null;
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void onReleased() {
+ source.release();
+ }
+
+ @Override
+ protected long getCurrentPositionUs() {
+ return currentPositionUs;
+ }
+
+ @Override
+ protected long getDurationUs() {
+ return source.getTrackInfo(trackIndex).durationUs;
+ }
+
+ @Override
+ protected long getBufferedPositionUs() {
+ long sourceBufferedPosition = source.getBufferedPositionUs();
+ return sourceBufferedPosition == UNKNOWN_TIME || sourceBufferedPosition == END_OF_TRACK
+ ? sourceBufferedPosition : Math.max(sourceBufferedPosition, getCurrentPositionUs());
+ }
+
+ @Override
+ protected void seekTo(long timeUs) throws ExoPlaybackException {
+ currentPositionUs = timeUs;
+ source.seekToUs(timeUs);
+ inputStreamEnded = false;
+ outputStreamEnded = false;
+ waitingForKeys = false;
+ }
+
+ @Override
+ protected void onStarted() {
+ // Do nothing. Overridden to remove throws clause.
+ }
+
+ @Override
+ protected void onStopped() {
+ // Do nothing. Overridden to remove throws clause.
+ }
+
+ @Override
+ protected void doSomeWork(long timeUs) throws ExoPlaybackException {
+ try {
+ source.continueBuffering(timeUs);
+ checkForDiscontinuity();
+ if (format == null) {
+ readFormat();
+ } else if (codec == null && !shouldInitCodec() && getState() == TrackRenderer.STATE_STARTED) {
+ discardSamples(timeUs);
+ } else {
+ if (codec == null && shouldInitCodec()) {
+ maybeInitCodec();
+ }
+ if (codec != null) {
+ while (drainOutputBuffer(timeUs)) {}
+ while (feedInputBuffer()) {}
+ }
+ }
+ } catch (IOException e) {
+ throw new ExoPlaybackException(e);
+ }
+ }
+
+ private void readFormat() throws IOException, ExoPlaybackException {
+ int result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false);
+ if (result == SampleSource.FORMAT_READ) {
+ onInputFormatChanged(formatHolder);
+ }
+ }
+
+ private void discardSamples(long timeUs) throws IOException, ExoPlaybackException {
+ sampleHolder.data = null;
+ int result = SampleSource.SAMPLE_READ;
+ while (result == SampleSource.SAMPLE_READ && currentPositionUs <= timeUs) {
+ result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false);
+ if (result == SampleSource.SAMPLE_READ) {
+ currentPositionUs = sampleHolder.timeUs;
+ codecCounters.discardedSamplesCount++;
+ } else if (result == SampleSource.FORMAT_READ) {
+ onInputFormatChanged(formatHolder);
+ }
+ }
+ }
+
+ private void checkForDiscontinuity() throws IOException, ExoPlaybackException {
+ if (codec == null) {
+ return;
+ }
+ int result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, true);
+ if (result == SampleSource.DISCONTINUITY_READ) {
+ flushCodec();
+ }
+ }
+
+ private void flushCodec() throws ExoPlaybackException {
+ codecHotswapTimeMs = -1;
+ inputIndex = -1;
+ outputIndex = -1;
+ decodeOnlyPresentationTimestamps.clear();
+ // Workaround for framework bugs.
+ // See [redacted], [redacted], [redacted].
+ if (Util.SDK_INT >= 18) {
+ codec.flush();
+ } else {
+ releaseCodec();
+ maybeInitCodec();
+ }
+ if (codecReconfigured && format != null) {
+ // Any reconfiguration data that we send shortly before the flush may be discarded. We
+ // avoid this issue by sending reconfiguration data following every flush.
+ codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
+ }
+ }
+
+ /**
+ * @return True if it may be possible to feed more input data. False otherwise.
+ * @throws IOException If an error occurs reading data from the upstream source.
+ * @throws ExoPlaybackException If an error occurs feeding the input buffer.
+ */
+ private boolean feedInputBuffer() throws IOException, ExoPlaybackException {
+ if (inputStreamEnded) {
+ return false;
+ }
+ if (inputIndex < 0) {
+ inputIndex = codec.dequeueInputBuffer(0);
+ if (inputIndex < 0) {
+ return false;
+ }
+ sampleHolder.data = inputBuffers[inputIndex];
+ sampleHolder.data.clear();
+ }
+
+ int result;
+ if (waitingForKeys) {
+ // We've already read an encrypted sample into sampleHolder, and are waiting for keys.
+ result = SampleSource.SAMPLE_READ;
+ } else {
+ // For adaptive reconfiguration OMX decoders expect all reconfiguration data to be supplied
+ // at the start of the buffer that also contains the first frame in the new format.
+ if (codecReconfigurationState == RECONFIGURATION_STATE_WRITE_PENDING) {
+ for (int i = 0; i < format.initializationData.size(); i++) {
+ byte[] data = format.initializationData.get(i);
+ sampleHolder.data.put(data);
+ }
+ codecReconfigurationState = RECONFIGURATION_STATE_QUEUE_PENDING;
+ }
+ result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false);
+ }
+
+ if (result == SampleSource.NOTHING_READ) {
+ codecCounters.inputBufferWaitingForSampleCount++;
+ return false;
+ }
+ if (result == SampleSource.DISCONTINUITY_READ) {
+ flushCodec();
+ return true;
+ }
+ if (result == SampleSource.FORMAT_READ) {
+ if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) {
+ // We received two formats in a row. Clear the current buffer of any reconfiguration data
+ // associated with the first format.
+ sampleHolder.data.clear();
+ codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
+ }
+ onInputFormatChanged(formatHolder);
+ return true;
+ }
+ if (result == SampleSource.END_OF_STREAM) {
+ if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) {
+ // We received a new format immediately before the end of the stream. We need to clear
+ // the corresponding reconfiguration data from the current buffer, but re-write it into
+ // a subsequent buffer if there are any (e.g. if the user seeks backwards).
+ sampleHolder.data.clear();
+ codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
+ }
+ inputStreamEnded = true;
+ try {
+ codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
+ inputIndex = -1;
+ codecCounters.queuedEndOfStreamCount++;
+ } catch (CryptoException e) {
+ notifyCryptoError(e);
+ throw new ExoPlaybackException(e);
+ }
+ return false;
+ }
+ if (waitingForFirstSyncFrame) {
+ // TODO: Find out if it's possible to supply samples prior to the first sync
+ // frame for HE-AAC.
+ if ((sampleHolder.flags & MediaExtractor.SAMPLE_FLAG_SYNC) == 0) {
+ sampleHolder.data.clear();
+ if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) {
+ // The buffer we just cleared contained reconfiguration data. We need to re-write this
+ // data into a subsequent buffer (if there is one).
+ codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
+ }
+ return true;
+ }
+ waitingForFirstSyncFrame = false;
+ }
+ boolean sampleEncrypted = (sampleHolder.flags & MediaExtractor.SAMPLE_FLAG_ENCRYPTED) != 0;
+ waitingForKeys = shouldWaitForKeys(sampleEncrypted);
+ if (waitingForKeys) {
+ return false;
+ }
+ try {
+ int bufferSize = sampleHolder.data.position();
+ int adaptiveReconfigurationBytes = bufferSize - sampleHolder.size;
+ long presentationTimeUs = sampleHolder.timeUs;
+ if (sampleHolder.decodeOnly) {
+ decodeOnlyPresentationTimestamps.add(presentationTimeUs);
+ }
+ if (sampleEncrypted) {
+ MediaCodec.CryptoInfo cryptoInfo = getFrameworkCryptoInfo(sampleHolder,
+ adaptiveReconfigurationBytes);
+ codec.queueSecureInputBuffer(inputIndex, 0, cryptoInfo, presentationTimeUs, 0);
+ } else {
+ codec.queueInputBuffer(inputIndex, 0 , bufferSize, presentationTimeUs, 0);
+ }
+ codecCounters.queuedInputBufferCount++;
+ if ((sampleHolder.flags & MediaExtractor.SAMPLE_FLAG_SYNC) != 0) {
+ codecCounters.keyframeCount++;
+ }
+ inputIndex = -1;
+ codecReconfigurationState = RECONFIGURATION_STATE_NONE;
+ } catch (CryptoException e) {
+ notifyCryptoError(e);
+ throw new ExoPlaybackException(e);
+ }
+ return true;
+ }
+
+ private static MediaCodec.CryptoInfo getFrameworkCryptoInfo(SampleHolder sampleHolder,
+ int adaptiveReconfigurationBytes) {
+ MediaCodec.CryptoInfo cryptoInfo = sampleHolder.cryptoInfo.getFrameworkCryptoInfoV16();
+ if (adaptiveReconfigurationBytes == 0) {
+ return cryptoInfo;
+ }
+ // There must be at least one sub-sample, although numBytesOfClearData is permitted to be
+ // null if it contains no clear data. Instantiate it if needed, and add the reconfiguration
+ // bytes to the clear byte count of the first sub-sample.
+ if (cryptoInfo.numBytesOfClearData == null) {
+ cryptoInfo.numBytesOfClearData = new int[1];
+ }
+ cryptoInfo.numBytesOfClearData[0] += adaptiveReconfigurationBytes;
+ return cryptoInfo;
+ }
+
+ private boolean shouldWaitForKeys(boolean sampleEncrypted) throws ExoPlaybackException {
+ if (!openedDrmSession) {
+ return false;
+ }
+ int drmManagerState = drmSessionManager.getState();
+ if (drmManagerState == DrmSessionManager.STATE_ERROR) {
+ throw new ExoPlaybackException(drmSessionManager.getError());
+ }
+ if (drmManagerState != DrmSessionManager.STATE_OPENED_WITH_KEYS &&
+ (sampleEncrypted || !playClearSamplesWithoutKeys)) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Invoked when a new format is read from the upstream {@link SampleSource}.
+ *
+ * @param formatHolder Holds the new format.
+ * @throws ExoPlaybackException If an error occurs reinitializing the {@link MediaCodec}.
+ */
+ private void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException {
+ MediaFormat oldFormat = format;
+ format = formatHolder.format;
+ drmInitData = formatHolder.drmInitData;
+ if (codec != null && canReconfigureCodec(codec, codecIsAdaptive, oldFormat, format)) {
+ codecReconfigured = true;
+ codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
+ } else {
+ releaseCodec();
+ maybeInitCodec();
+ }
+ }
+
+ /**
+ * Invoked when the output format of the {@link MediaCodec} changes.
+ *
+ * The default implementation is a no-op.
+ *
+ * @param format The new output format.
+ */
+ protected void onOutputFormatChanged(android.media.MediaFormat format) {
+ // Do nothing.
+ }
+
+ /**
+ * Determines whether the existing {@link MediaCodec} should be reconfigured for a new format by
+ * sending codec specific initialization data at the start of the next input buffer. If true is
+ * returned then the {@link MediaCodec} instance will be reconfigured in this way. If false is
+ * returned then the instance will be released, and a new instance will be created for the new
+ * format.
+ *
+ * The default implementation returns false.
+ *
+ * @param codec The existing {@link MediaCodec} instance.
+ * @param codecIsAdaptive Whether the codec is adaptive.
+ * @param oldFormat The format for which the existing instance is configured.
+ * @param newFormat The new format.
+ * @return True if the existing instance can be reconfigured. False otherwise.
+ */
+ @SuppressWarnings("unused")
+ protected boolean canReconfigureCodec(MediaCodec codec, boolean codecIsAdaptive,
+ MediaFormat oldFormat, MediaFormat newFormat) {
+ return false;
+ }
+
+ @Override
+ protected boolean isEnded() {
+ return outputStreamEnded;
+ }
+
+ @Override
+ protected boolean isReady() {
+ return format != null && !waitingForKeys
+ && ((codec == null && !shouldInitCodec()) // We don't want the codec
+ || outputIndex >= 0 // Or we have an output buffer ready to release
+ || inputIndex < 0 // Or we don't have any input buffers to write to
+ || isWithinHotswapPeriod()); // Or the codec is being hotswapped
+ }
+
+ private boolean isWithinHotswapPeriod() {
+ return SystemClock.elapsedRealtime() < codecHotswapTimeMs + MAX_CODEC_HOTSWAP_TIME_MS;
+ }
+
+ /**
+ * @return True if it may be possible to drain more output data. False otherwise.
+ * @throws ExoPlaybackException If an error occurs draining the output buffer.
+ */
+ private boolean drainOutputBuffer(long timeUs) throws ExoPlaybackException {
+ if (outputStreamEnded) {
+ return false;
+ }
+
+ if (outputIndex < 0) {
+ outputIndex = codec.dequeueOutputBuffer(outputBufferInfo, 0);
+ }
+
+ if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
+ onOutputFormatChanged(codec.getOutputFormat());
+ codecCounters.outputFormatChangedCount++;
+ return true;
+ } else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
+ outputBuffers = codec.getOutputBuffers();
+ codecCounters.outputBuffersChangedCount++;
+ return true;
+ } else if (outputIndex < 0) {
+ return false;
+ }
+
+ if ((outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
+ outputStreamEnded = true;
+ return false;
+ }
+
+ if (decodeOnlyPresentationTimestamps.remove(outputBufferInfo.presentationTimeUs)) {
+ codec.releaseOutputBuffer(outputIndex, false);
+ outputIndex = -1;
+ return true;
+ }
+
+ if (processOutputBuffer(timeUs, codec, outputBuffers[outputIndex], outputBufferInfo,
+ outputIndex)) {
+ currentPositionUs = outputBufferInfo.presentationTimeUs;
+ outputIndex = -1;
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Processes the provided output buffer.
+ *
+ * @return True if the output buffer was processed (e.g. rendered or discarded) and hence is no
+ * longer required. False otherwise.
+ * @throws ExoPlaybackException If an error occurs processing the output buffer.
+ */
+ protected abstract boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer,
+ MediaCodec.BufferInfo bufferInfo, int bufferIndex) throws ExoPlaybackException;
+
+ /**
+ * Returns the name of the secure variant of a given decoder.
+ */
+ private static String getSecureDecoderName(String rawDecoderName) {
+ return rawDecoderName + ".secure";
+ }
+
+ private void notifyDecoderInitializationError(final DecoderInitializationException e) {
+ if (eventHandler != null && eventListener != null) {
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ eventListener.onDecoderInitializationError(e);
+ }
+ });
+ }
+ }
+
+ private void notifyCryptoError(final CryptoException e) {
+ if (eventHandler != null && eventListener != null) {
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ eventListener.onCryptoError(e);
+ }
+ });
+ }
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java
new file mode 100644
index 00000000000..31ec2ae9f15
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java
@@ -0,0 +1,181 @@
+/*
+ * 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;
+
+import com.google.android.exoplayer.util.MimeTypes;
+import com.google.android.exoplayer.util.Util;
+
+import android.annotation.TargetApi;
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaCodecInfo.CodecProfileLevel;
+import android.media.MediaCodecList;
+import android.util.Pair;
+
+import java.util.HashMap;
+
+/**
+ * A utility class for querying the available codecs.
+ */
+@TargetApi(16)
+public class MediaCodecUtil {
+
+ private static final HashMap> codecs =
+ new HashMap>();
+
+ /**
+ * Get information about the decoder that will be used for a given mime type. If no decoder
+ * exists for the mime type then null is returned.
+ *
+ * @param mimeType The mime type.
+ * @return Information about the decoder that will be used, or null if no decoder exists.
+ */
+ public static DecoderInfo getDecoderInfo(String mimeType) {
+ Pair info = getMediaCodecInfo(mimeType);
+ if (info == null) {
+ return null;
+ }
+ return new DecoderInfo(info.first.getName(), isAdaptive(info.second));
+ }
+
+ /**
+ * Optional call to warm the codec cache. Call from any appropriate
+ * place to hide latency.
+ */
+ public static synchronized void warmCodecs(String[] mimeTypes) {
+ for (int i = 0; i < mimeTypes.length; i++) {
+ getMediaCodecInfo(mimeTypes[i]);
+ }
+ }
+
+ /**
+ * Returns the best decoder and its capabilities for the given mimeType. If there's no decoder
+ * returns null.
+ */
+ private static synchronized Pair getMediaCodecInfo(
+ String mimeType) {
+ Pair result = codecs.get(mimeType);
+ if (result != null) {
+ return result;
+ }
+ int numberOfCodecs = MediaCodecList.getCodecCount();
+ // Note: MediaCodecList is sorted by the framework such that the best decoders come first.
+ for (int i = 0; i < numberOfCodecs; i++) {
+ MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
+ String codecName = info.getName();
+ if (!info.isEncoder() && isOmxCodec(codecName)) {
+ String[] supportedTypes = info.getSupportedTypes();
+ for (int j = 0; j < supportedTypes.length; j++) {
+ String supportedType = supportedTypes[j];
+ if (supportedType.equalsIgnoreCase(mimeType)) {
+ result = Pair.create(info, info.getCapabilitiesForType(supportedType));
+ codecs.put(mimeType, result);
+ return result;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ private static boolean isOmxCodec(String name) {
+ return name.startsWith("OMX.");
+ }
+
+ private static boolean isAdaptive(CodecCapabilities capabilities) {
+ if (Util.SDK_INT >= 19) {
+ return isAdaptiveV19(capabilities);
+ } else {
+ return false;
+ }
+ }
+
+ @TargetApi(19)
+ private static boolean isAdaptiveV19(CodecCapabilities capabilities) {
+ return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback);
+ }
+
+ /**
+ * @param profile An AVC profile constant from {@link CodecProfileLevel}.
+ * @param level An AVC profile level from {@link CodecProfileLevel}.
+ * @return Whether the specified profile is supported at the specified level.
+ */
+ public static boolean isH264ProfileSupported(int profile, int level) {
+ Pair info = getMediaCodecInfo(MimeTypes.VIDEO_H264);
+ if (info == null) {
+ return false;
+ }
+
+ CodecCapabilities capabilities = info.second;
+ for (int i = 0; i < capabilities.profileLevels.length; i++) {
+ CodecProfileLevel profileLevel = capabilities.profileLevels[i];
+ if (profileLevel.profile == profile && profileLevel.level >= level) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @return the maximum frame size for an H264 stream that can be decoded on the device.
+ */
+ public static int maxH264DecodableFrameSize() {
+ Pair info = getMediaCodecInfo(MimeTypes.VIDEO_H264);
+ if (info == null) {
+ return 0;
+ }
+
+ int maxH264DecodableFrameSize = 0;
+ CodecCapabilities capabilities = info.second;
+ for (int i = 0; i < capabilities.profileLevels.length; i++) {
+ CodecProfileLevel profileLevel = capabilities.profileLevels[i];
+ maxH264DecodableFrameSize = Math.max(
+ avcLevelToMaxFrameSize(profileLevel.level), maxH264DecodableFrameSize);
+ }
+
+ return maxH264DecodableFrameSize;
+ }
+
+ /**
+ * Conversion values taken from: https://en.wikipedia.org/wiki/H.264/MPEG-4_AVC.
+ *
+ * @param avcLevel one of CodecProfileLevel.AVCLevel* constants.
+ * @return maximum frame size that can be decoded by a decoder with the specified avc level
+ * (or {@code -1} if the level is not recognized)
+ */
+ private static int avcLevelToMaxFrameSize(int avcLevel) {
+ switch (avcLevel) {
+ case CodecProfileLevel.AVCLevel1: return 25344;
+ case CodecProfileLevel.AVCLevel1b: return 25344;
+ case CodecProfileLevel.AVCLevel12: return 101376;
+ case CodecProfileLevel.AVCLevel13: return 101376;
+ case CodecProfileLevel.AVCLevel2: return 101376;
+ case CodecProfileLevel.AVCLevel21: return 202752;
+ case CodecProfileLevel.AVCLevel22: return 414720;
+ case CodecProfileLevel.AVCLevel3: return 414720;
+ case CodecProfileLevel.AVCLevel31: return 921600;
+ case CodecProfileLevel.AVCLevel32: return 1310720;
+ case CodecProfileLevel.AVCLevel4: return 2097152;
+ case CodecProfileLevel.AVCLevel41: return 2097152;
+ case CodecProfileLevel.AVCLevel42: return 2228224;
+ case CodecProfileLevel.AVCLevel5: return 5652480;
+ case CodecProfileLevel.AVCLevel51: return 9437184;
+ default: return -1;
+ }
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java
new file mode 100644
index 00000000000..f99c328482d
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java
@@ -0,0 +1,439 @@
+/*
+ * 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;
+
+import com.google.android.exoplayer.drm.DrmSessionManager;
+import com.google.android.exoplayer.util.MimeTypes;
+import com.google.android.exoplayer.util.TraceUtil;
+
+import android.annotation.TargetApi;
+import android.media.MediaCodec;
+import android.media.MediaCrypto;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.view.Surface;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Decodes and renders video using {@MediaCodec}.
+ */
+@TargetApi(16)
+public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
+
+ /**
+ * Interface definition for a callback to be notified of {@link MediaCodecVideoTrackRenderer}
+ * events.
+ */
+ public interface EventListener extends MediaCodecTrackRenderer.EventListener {
+
+ /**
+ * Invoked to report the number of frames dropped by the renderer. Dropped frames are reported
+ * whenever the renderer is stopped having dropped frames, and optionally, whenever the count
+ * reaches a specified threshold whilst the renderer is started.
+ *
+ * @param count The number of dropped frames.
+ * @param elapsed The duration in milliseconds over which the frames were dropped. This
+ * duration is timed from when the renderer was started or from when dropped frames were
+ * last reported (whichever was more recent), and not from when the first of the reported
+ * drops occurred.
+ */
+ void onDroppedFrames(int count, long elapsed);
+
+ /**
+ * Invoked each time there's a change in the size of the video being rendered.
+ *
+ * @param width The video width in pixels.
+ * @param height The video height in pixels.
+ */
+ void onVideoSizeChanged(int width, int height);
+
+ /**
+ * Invoked when a frame is rendered to a surface for the first time following that surface
+ * having been set as the target for the renderer.
+ *
+ * @param surface The surface to which a first frame has been rendered.
+ */
+ void onDrawnToSurface(Surface surface);
+
+ }
+
+ // TODO: Use MediaFormat constants if these get exposed through the API. See [redacted].
+ private static final String KEY_CROP_LEFT = "crop-left";
+ private static final String KEY_CROP_RIGHT = "crop-right";
+ private static final String KEY_CROP_BOTTOM = "crop-bottom";
+ private static final String KEY_CROP_TOP = "crop-top";
+
+ /**
+ * The type of a message that can be passed to an instance of this class via
+ * {@link ExoPlayer#sendMessage} or {@link ExoPlayer#blockingSendMessage}. The message object
+ * should be the target {@link Surface}, or null.
+ */
+ public static final int MSG_SET_SURFACE = 1;
+
+ private final EventListener eventListener;
+ private final long allowedJoiningTimeUs;
+ private final int videoScalingMode;
+ private final int maxDroppedFrameCountToNotify;
+
+ private Surface surface;
+ private boolean drawnToSurface;
+ private boolean renderedFirstFrame;
+ private long joiningDeadlineUs;
+ private long droppedFrameAccumulationStartTimeMs;
+ private int droppedFrameCount;
+
+ private int currentWidth;
+ private int currentHeight;
+ private int lastReportedWidth;
+ private int lastReportedHeight;
+
+ /**
+ * @param source The upstream source from which the renderer obtains samples.
+ * @param videoScalingMode The scaling mode to pass to
+ * {@link MediaCodec#setVideoScalingMode(int)}.
+ */
+ public MediaCodecVideoTrackRenderer(SampleSource source, int videoScalingMode) {
+ this(source, null, true, videoScalingMode);
+ }
+
+ /**
+ * @param source The upstream source from which the renderer obtains samples.
+ * @param drmSessionManager For use with encrypted content. May be null if support for encrypted
+ * content is not required.
+ * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
+ * For example a media file may start with a short clear region so as to allow playback to
+ * begin in parallel with key acquisision. This parameter specifies whether the renderer is
+ * permitted to play clear regions of encrypted media files before {@code drmSessionManager}
+ * has obtained the keys necessary to decrypt encrypted regions of the media.
+ * @param videoScalingMode The scaling mode to pass to
+ * {@link MediaCodec#setVideoScalingMode(int)}.
+ */
+ public MediaCodecVideoTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager,
+ boolean playClearSamplesWithoutKeys, int videoScalingMode) {
+ this(source, drmSessionManager, playClearSamplesWithoutKeys, videoScalingMode, 0);
+ }
+
+ /**
+ * @param source The upstream source from which the renderer obtains samples.
+ * @param videoScalingMode The scaling mode to pass to
+ * {@link MediaCodec#setVideoScalingMode(int)}.
+ * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
+ * can attempt to seamlessly join an ongoing playback.
+ */
+ public MediaCodecVideoTrackRenderer(SampleSource source, int videoScalingMode,
+ long allowedJoiningTimeMs) {
+ this(source, null, true, videoScalingMode, allowedJoiningTimeMs);
+ }
+
+ /**
+ * @param source The upstream source from which the renderer obtains samples.
+ * @param drmSessionManager For use with encrypted content. May be null if support for encrypted
+ * content is not required.
+ * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
+ * For example a media file may start with a short clear region so as to allow playback to
+ * begin in parallel with key acquisision. This parameter specifies whether the renderer is
+ * permitted to play clear regions of encrypted media files before {@code drmSessionManager}
+ * has obtained the keys necessary to decrypt encrypted regions of the media.
+ * @param videoScalingMode The scaling mode to pass to
+ * {@link MediaCodec#setVideoScalingMode(int)}.
+ * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
+ * can attempt to seamlessly join an ongoing playback.
+ */
+ public MediaCodecVideoTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager,
+ boolean playClearSamplesWithoutKeys, int videoScalingMode, long allowedJoiningTimeMs) {
+ this(source, drmSessionManager, playClearSamplesWithoutKeys, videoScalingMode,
+ allowedJoiningTimeMs, null, null, -1);
+ }
+
+ /**
+ * @param source The upstream source from which the renderer obtains samples.
+ * @param videoScalingMode The scaling mode to pass to
+ * {@link MediaCodec#setVideoScalingMode(int)}.
+ * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
+ * can attempt to seamlessly join an ongoing playback.
+ * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+ * null if delivery of events is not required.
+ * @param eventListener A listener of events. May be null if delivery of events is not required.
+ * @param maxDroppedFrameCountToNotify The maximum number of frames that can be dropped between
+ * invocations of {@link EventListener#onDroppedFrames(int, long)}.
+ */
+ public MediaCodecVideoTrackRenderer(SampleSource source, int videoScalingMode,
+ long allowedJoiningTimeMs, Handler eventHandler, EventListener eventListener,
+ int maxDroppedFrameCountToNotify) {
+ this(source, null, true, videoScalingMode, allowedJoiningTimeMs, eventHandler, eventListener,
+ maxDroppedFrameCountToNotify);
+ }
+
+ /**
+ * @param source The upstream source from which the renderer obtains samples.
+ * @param drmSessionManager For use with encrypted content. May be null if support for encrypted
+ * content is not required.
+ * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
+ * For example a media file may start with a short clear region so as to allow playback to
+ * begin in parallel with key acquisision. This parameter specifies whether the renderer is
+ * permitted to play clear regions of encrypted media files before {@code drmSessionManager}
+ * has obtained the keys necessary to decrypt encrypted regions of the media.
+ * @param videoScalingMode The scaling mode to pass to
+ * {@link MediaCodec#setVideoScalingMode(int)}.
+ * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
+ * can attempt to seamlessly join an ongoing playback.
+ * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+ * null if delivery of events is not required.
+ * @param eventListener A listener of events. May be null if delivery of events is not required.
+ * @param maxDroppedFrameCountToNotify The maximum number of frames that can be dropped between
+ * invocations of {@link EventListener#onDroppedFrames(int, long)}.
+ */
+ public MediaCodecVideoTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager,
+ boolean playClearSamplesWithoutKeys, int videoScalingMode, long allowedJoiningTimeMs,
+ Handler eventHandler, EventListener eventListener, int maxDroppedFrameCountToNotify) {
+ super(source, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, eventListener);
+ this.videoScalingMode = videoScalingMode;
+ this.allowedJoiningTimeUs = allowedJoiningTimeMs * 1000;
+ this.eventListener = eventListener;
+ this.maxDroppedFrameCountToNotify = maxDroppedFrameCountToNotify;
+ joiningDeadlineUs = -1;
+ currentWidth = -1;
+ currentHeight = -1;
+ lastReportedWidth = -1;
+ lastReportedHeight = -1;
+ }
+
+ @Override
+ protected boolean handlesMimeType(String mimeType) {
+ return MimeTypes.isVideo(mimeType) && super.handlesMimeType(mimeType);
+ }
+
+ @Override
+ protected void onEnabled(long startTimeUs, boolean joining) {
+ super.onEnabled(startTimeUs, joining);
+ renderedFirstFrame = false;
+ if (joining && allowedJoiningTimeUs > 0) {
+ joiningDeadlineUs = SystemClock.elapsedRealtime() * 1000L + allowedJoiningTimeUs;
+ }
+ }
+
+ @Override
+ protected void seekTo(long timeUs) throws ExoPlaybackException {
+ super.seekTo(timeUs);
+ renderedFirstFrame = false;
+ joiningDeadlineUs = -1;
+ }
+
+ @Override
+ protected boolean isReady() {
+ if (super.isReady() && (renderedFirstFrame || !codecInitialized())) {
+ // Ready. If we were joining then we've now joined, so clear the joining deadline.
+ joiningDeadlineUs = -1;
+ return true;
+ } else if (joiningDeadlineUs == -1) {
+ // Not joining.
+ return false;
+ } else if (SystemClock.elapsedRealtime() * 1000 < joiningDeadlineUs) {
+ // Joining and still within the joining deadline.
+ return true;
+ } else {
+ // The joining deadline has been exceeded. Give up and clear the deadline.
+ joiningDeadlineUs = -1;
+ return false;
+ }
+ }
+
+ @Override
+ protected void onStarted() {
+ super.onStarted();
+ droppedFrameCount = 0;
+ droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime();
+ }
+
+ @Override
+ protected void onStopped() {
+ super.onStopped();
+ joiningDeadlineUs = -1;
+ notifyAndResetDroppedFrameCount();
+ }
+
+ @Override
+ public void onDisabled() {
+ super.onDisabled();
+ currentWidth = -1;
+ currentHeight = -1;
+ lastReportedWidth = -1;
+ lastReportedHeight = -1;
+ }
+
+ @Override
+ public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
+ if (messageType == MSG_SET_SURFACE) {
+ setSurface((Surface) message);
+ } else {
+ super.handleMessage(messageType, message);
+ }
+ }
+
+ /**
+ * @param surface The surface to set.
+ * @throws ExoPlaybackException
+ */
+ private void setSurface(Surface surface) throws ExoPlaybackException {
+ if (this.surface == surface) {
+ return;
+ }
+ this.surface = surface;
+ this.drawnToSurface = false;
+ int state = getState();
+ if (state == TrackRenderer.STATE_ENABLED || state == TrackRenderer.STATE_STARTED) {
+ releaseCodec();
+ maybeInitCodec();
+ }
+ }
+
+ @Override
+ protected boolean shouldInitCodec() {
+ return super.shouldInitCodec() && surface != null;
+ }
+
+ // Override configureCodec to provide the surface.
+ @Override
+ protected void configureCodec(MediaCodec codec, android.media.MediaFormat format,
+ MediaCrypto crypto) {
+ codec.configure(format, surface, crypto, 0);
+ codec.setVideoScalingMode(videoScalingMode);
+ }
+
+ @Override
+ protected void onOutputFormatChanged(android.media.MediaFormat format) {
+ boolean hasCrop = format.containsKey(KEY_CROP_RIGHT) && format.containsKey(KEY_CROP_LEFT)
+ && format.containsKey(KEY_CROP_BOTTOM) && format.containsKey(KEY_CROP_TOP);
+ currentWidth = hasCrop
+ ? format.getInteger(KEY_CROP_RIGHT) - format.getInteger(KEY_CROP_LEFT) + 1
+ : format.getInteger(android.media.MediaFormat.KEY_WIDTH);
+ currentHeight = hasCrop
+ ? format.getInteger(KEY_CROP_BOTTOM) - format.getInteger(KEY_CROP_TOP) + 1
+ : format.getInteger(android.media.MediaFormat.KEY_HEIGHT);
+ }
+
+ @Override
+ protected boolean canReconfigureCodec(MediaCodec codec, boolean codecIsAdaptive,
+ MediaFormat oldFormat, MediaFormat newFormat) {
+ // TODO: Relax this check to also allow non-H264 adaptive decoders.
+ return newFormat.mimeType.equals(MimeTypes.VIDEO_H264)
+ && oldFormat.mimeType.equals(MimeTypes.VIDEO_H264)
+ && codecIsAdaptive
+ || (oldFormat.width == newFormat.width && oldFormat.height == newFormat.height);
+ }
+
+ @Override
+ protected boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer,
+ MediaCodec.BufferInfo bufferInfo, int bufferIndex) {
+ long earlyUs = bufferInfo.presentationTimeUs - timeUs;
+ if (earlyUs < -30000) {
+ // We're more than 30ms late rendering the frame.
+ dropOutputBuffer(codec, bufferIndex);
+ return true;
+ }
+
+ if (!renderedFirstFrame) {
+ renderOutputBuffer(codec, bufferIndex);
+ renderedFirstFrame = true;
+ return true;
+ }
+
+ if (getState() == TrackRenderer.STATE_STARTED && earlyUs < 30000) {
+ if (earlyUs > 11000) {
+ // We're a little too early to render the frame. Sleep until the frame can be rendered.
+ // Note: The 11ms threshold was chosen fairly arbitrarily.
+ try {
+ // Subtracting 10000 rather than 11000 ensures that the sleep time will be at least 1ms.
+ Thread.sleep((earlyUs - 10000) / 1000);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ renderOutputBuffer(codec, bufferIndex);
+ return true;
+ }
+
+ // We're either not playing, or it's not time to render the frame yet.
+ return false;
+ }
+
+ private void dropOutputBuffer(MediaCodec codec, int bufferIndex) {
+ TraceUtil.beginSection("dropVideoBuffer");
+ codec.releaseOutputBuffer(bufferIndex, false);
+ TraceUtil.endSection();
+ codecCounters.droppedOutputBufferCount++;
+ droppedFrameCount++;
+ if (droppedFrameCount == maxDroppedFrameCountToNotify) {
+ notifyAndResetDroppedFrameCount();
+ }
+ }
+
+ private void renderOutputBuffer(MediaCodec codec, int bufferIndex) {
+ if (lastReportedWidth != currentWidth || lastReportedHeight != currentHeight) {
+ lastReportedWidth = currentWidth;
+ lastReportedHeight = currentHeight;
+ notifyVideoSizeChanged(currentWidth, currentHeight);
+ }
+ TraceUtil.beginSection("renderVideoBuffer");
+ codec.releaseOutputBuffer(bufferIndex, true);
+ TraceUtil.endSection();
+ codecCounters.renderedOutputBufferCount++;
+ if (!drawnToSurface) {
+ drawnToSurface = true;
+ notifyDrawnToSurface(surface);
+ }
+ }
+
+ private void notifyVideoSizeChanged(final int width, final int height) {
+ if (eventHandler != null && eventListener != null) {
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ eventListener.onVideoSizeChanged(width, height);
+ }
+ });
+ }
+ }
+
+ private void notifyDrawnToSurface(final Surface surface) {
+ if (eventHandler != null && eventListener != null) {
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ eventListener.onDrawnToSurface(surface);
+ }
+ });
+ }
+ }
+
+ private void notifyAndResetDroppedFrameCount() {
+ if (eventHandler != null && eventListener != null && droppedFrameCount > 0) {
+ long now = SystemClock.elapsedRealtime();
+ final int countToNotify = droppedFrameCount;
+ final long elapsedToNotify = now - droppedFrameAccumulationStartTimeMs;
+ droppedFrameCount = 0;
+ droppedFrameAccumulationStartTimeMs = now;
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ eventListener.onDroppedFrames(countToNotify, elapsedToNotify);
+ }
+ });
+ }
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java
new file mode 100644
index 00000000000..f53defdf933
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java
@@ -0,0 +1,216 @@
+/*
+ * 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;
+
+import com.google.android.exoplayer.util.Util;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Encapsulates the information describing the format of media data, be it audio or video.
+ */
+public class MediaFormat {
+
+ public static final int NO_VALUE = -1;
+
+ public final String mimeType;
+ public final int maxInputSize;
+
+ public final int width;
+ public final int height;
+
+ public final int channelCount;
+ public final int sampleRate;
+
+ private int maxWidth;
+ private int maxHeight;
+
+ public final List initializationData;
+
+ // Lazy-initialized hashcode.
+ private int hashCode;
+ // Possibly-lazy-initialized framework media format.
+ private android.media.MediaFormat frameworkMediaFormat;
+
+ @TargetApi(16)
+ public static MediaFormat createFromFrameworkMediaFormatV16(android.media.MediaFormat format) {
+ return new MediaFormat(format);
+ }
+
+ public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, int width,
+ int height, List initializationData) {
+ return new MediaFormat(mimeType, maxInputSize, width, height, NO_VALUE, NO_VALUE,
+ initializationData);
+ }
+
+ public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, int channelCount,
+ int sampleRate, List initializationData) {
+ return new MediaFormat(mimeType, maxInputSize, NO_VALUE, NO_VALUE, channelCount, sampleRate,
+ initializationData);
+ }
+
+ @TargetApi(16)
+ private MediaFormat(android.media.MediaFormat format) {
+ this.frameworkMediaFormat = format;
+ mimeType = format.getString(android.media.MediaFormat.KEY_MIME);
+ maxInputSize = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_MAX_INPUT_SIZE);
+ width = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_WIDTH);
+ height = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_HEIGHT);
+ channelCount = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_CHANNEL_COUNT);
+ sampleRate = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_SAMPLE_RATE);
+ initializationData = new ArrayList();
+ for (int i = 0; format.containsKey("csd-" + i); i++) {
+ ByteBuffer buffer = format.getByteBuffer("csd-" + i);
+ byte[] data = new byte[buffer.limit()];
+ buffer.get(data);
+ initializationData.add(data);
+ buffer.flip();
+ }
+ maxWidth = NO_VALUE;
+ maxHeight = NO_VALUE;
+ }
+
+ private MediaFormat(String mimeType, int maxInputSize, int width, int height, int channelCount,
+ int sampleRate, List initializationData) {
+ this.mimeType = mimeType;
+ this.maxInputSize = maxInputSize;
+ this.width = width;
+ this.height = height;
+ this.channelCount = channelCount;
+ this.sampleRate = sampleRate;
+ this.initializationData = initializationData == null ? Collections.emptyList()
+ : initializationData;
+ maxWidth = NO_VALUE;
+ maxHeight = NO_VALUE;
+ }
+
+ public void setMaxVideoDimensions(int maxWidth, int maxHeight) {
+ this.maxWidth = maxWidth;
+ this.maxHeight = maxHeight;
+ if (frameworkMediaFormat != null) {
+ maybeSetMaxDimensionsV16(frameworkMediaFormat);
+ }
+ }
+
+ public int getMaxVideoWidth() {
+ return maxWidth;
+ }
+
+ public int getMaxVideoHeight() {
+ return maxHeight;
+ }
+
+ @Override
+ public int hashCode() {
+ if (hashCode == 0) {
+ int result = 17;
+ result = 31 * result + mimeType == null ? 0 : mimeType.hashCode();
+ result = 31 * result + maxInputSize;
+ result = 31 * result + width;
+ result = 31 * result + height;
+ result = 31 * result + maxWidth;
+ result = 31 * result + maxHeight;
+ result = 31 * result + channelCount;
+ result = 31 * result + sampleRate;
+ for (int i = 0; i < initializationData.size(); i++) {
+ result = 31 * result + Arrays.hashCode(initializationData.get(i));
+ }
+ hashCode = result;
+ }
+ return hashCode;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ MediaFormat other = (MediaFormat) obj;
+ if (maxInputSize != other.maxInputSize || width != other.width || height != other.height ||
+ maxWidth != other.maxWidth || maxHeight != other.maxHeight ||
+ channelCount != other.channelCount || sampleRate != other.sampleRate ||
+ !Util.areEqual(mimeType, other.mimeType) ||
+ initializationData.size() != other.initializationData.size()) {
+ return false;
+ }
+ for (int i = 0; i < initializationData.size(); i++) {
+ if (!Arrays.equals(initializationData.get(i), other.initializationData.get(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "MediaFormat(" + mimeType + ", " + maxInputSize + ", " + width + ", " + height + ", " +
+ channelCount + ", " + sampleRate + ", " + maxWidth + ", " + maxHeight + ")";
+ }
+
+ /**
+ * @return A {@link MediaFormat} representation of this format.
+ */
+ @TargetApi(16)
+ public final android.media.MediaFormat getFrameworkMediaFormatV16() {
+ if (frameworkMediaFormat == null) {
+ android.media.MediaFormat format = new android.media.MediaFormat();
+ format.setString(android.media.MediaFormat.KEY_MIME, mimeType);
+ maybeSetIntegerV16(format, android.media.MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize);
+ maybeSetIntegerV16(format, android.media.MediaFormat.KEY_WIDTH, width);
+ maybeSetIntegerV16(format, android.media.MediaFormat.KEY_HEIGHT, height);
+ maybeSetIntegerV16(format, android.media.MediaFormat.KEY_CHANNEL_COUNT, channelCount);
+ maybeSetIntegerV16(format, android.media.MediaFormat.KEY_SAMPLE_RATE, sampleRate);
+ for (int i = 0; i < initializationData.size(); i++) {
+ format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i)));
+ }
+ maybeSetMaxDimensionsV16(format);
+ frameworkMediaFormat = format;
+ }
+ return frameworkMediaFormat;
+ }
+
+ @SuppressLint("InlinedApi")
+ @TargetApi(16)
+ private final void maybeSetMaxDimensionsV16(android.media.MediaFormat format) {
+ maybeSetIntegerV16(format, android.media.MediaFormat.KEY_MAX_WIDTH, maxWidth);
+ maybeSetIntegerV16(format, android.media.MediaFormat.KEY_MAX_HEIGHT, maxHeight);
+ }
+
+ @TargetApi(16)
+ private static final void maybeSetIntegerV16(android.media.MediaFormat format, String key,
+ int value) {
+ if (value != NO_VALUE) {
+ format.setInteger(key, value);
+ }
+ }
+
+ @TargetApi(16)
+ private static final int getOptionalIntegerV16(android.media.MediaFormat format,
+ String key) {
+ return format.containsKey(key) ? format.getInteger(key) : NO_VALUE;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/ParserException.java b/library/src/main/java/com/google/android/exoplayer/ParserException.java
new file mode 100644
index 00000000000..f3830bcba71
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/ParserException.java
@@ -0,0 +1,33 @@
+/*
+ * 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;
+
+import java.io.IOException;
+
+/**
+ * Thrown when an error occurs parsing media data.
+ */
+public class ParserException extends IOException {
+
+ public ParserException(String message) {
+ super(message);
+ }
+
+ public ParserException(Exception cause) {
+ super(cause);
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/SampleHolder.java b/library/src/main/java/com/google/android/exoplayer/SampleHolder.java
new file mode 100644
index 00000000000..6518b06ac5f
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/SampleHolder.java
@@ -0,0 +1,68 @@
+/*
+ * 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;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Holds sample data and corresponding metadata.
+ */
+public final class SampleHolder {
+
+ /**
+ * Whether a {@link SampleSource} is permitted to replace {@link #data} if its current value is
+ * null or of insufficient size to hold the sample.
+ */
+ public final boolean allowDataBufferReplacement;
+
+ public final CryptoInfo cryptoInfo;
+
+ /**
+ * A buffer holding the sample data.
+ */
+ public ByteBuffer data;
+
+ /**
+ * The size of the sample in bytes.
+ */
+ public int size;
+
+ /**
+ * Flags that accompany the sample. A combination of
+ * {@link android.media.MediaExtractor#SAMPLE_FLAG_SYNC} and
+ * {@link android.media.MediaExtractor#SAMPLE_FLAG_ENCRYPTED}
+ */
+ public int flags;
+
+ /**
+ * The time at which the sample should be presented.
+ */
+ public long timeUs;
+
+ /**
+ * If true then the sample should be decoded, but should not be presented.
+ */
+ public boolean decodeOnly;
+
+ /**
+ * @param allowDataBufferReplacement See {@link #allowDataBufferReplacement}.
+ */
+ public SampleHolder(boolean allowDataBufferReplacement) {
+ this.cryptoInfo = new CryptoInfo();
+ this.allowDataBufferReplacement = allowDataBufferReplacement;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/SampleSource.java b/library/src/main/java/com/google/android/exoplayer/SampleSource.java
new file mode 100644
index 00000000000..8e2afc32d6c
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/SampleSource.java
@@ -0,0 +1,157 @@
+/*
+ * 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;
+
+import java.io.IOException;
+
+/**
+ * A source of media samples.
+ *
+ * A {@link SampleSource} may expose one or multiple tracks. The number of tracks and information
+ * about each can be queried using {@link #getTrackCount()} and {@link #getTrackInfo(int)}
+ * respectively.
+ */
+public interface SampleSource {
+
+ /**
+ * The end of stream has been reached.
+ */
+ public static final int END_OF_STREAM = -1;
+ /**
+ * Neither a sample nor a format was read in full. This may be because insufficient data is
+ * buffered upstream. If multiple tracks are enabled, this return value may indicate that the
+ * next piece of data to be returned from the {@link SampleSource} corresponds to a different
+ * track than the one for which data was requested.
+ */
+ public static final int NOTHING_READ = -2;
+ /**
+ * A sample was read.
+ */
+ public static final int SAMPLE_READ = -3;
+ /**
+ * A format was read.
+ */
+ public static final int FORMAT_READ = -4;
+ /**
+ * A discontinuity in the sample stream.
+ */
+ public static final int DISCONTINUITY_READ = -5;
+
+ /**
+ * Prepares the source.
+ *
+ * Preparation may require reading from the data source (e.g. to determine the available tracks
+ * and formats). If insufficient data is available then the call will return rather than block.
+ * The method can be called repeatedly until the return value indicates success.
+ *
+ * @return True if the source was prepared successfully, false otherwise.
+ * @throws IOException If an error occurred preparing the source.
+ */
+ public boolean prepare() throws IOException;
+
+ /**
+ * Returns the number of tracks exposed by the source.
+ *
+ * @return The number of tracks.
+ */
+ public int getTrackCount();
+
+ /**
+ * Returns information about the specified track.
+ *
+ * This method should not be called until after the source has been successfully prepared.
+ *
+ * @return Information about the specified track.
+ */
+ public TrackInfo getTrackInfo(int track);
+
+ /**
+ * Enable the specified track. This allows the track's format and samples to be read from
+ * {@link #readData(int, long, FormatHolder, SampleHolder, boolean)}.
+ *
+ * This method should not be called until after the source has been successfully prepared.
+ *
+ * @param track The track to enable.
+ * @param timeUs The player's current playback position.
+ */
+ public void enable(int track, long timeUs);
+
+ /**
+ * Disable the specified track.
+ *
+ * This method should not be called until after the source has been successfully prepared.
+ *
+ * @param track The track to disable.
+ */
+ public void disable(int track);
+
+ /**
+ * Indicates to the source that it should still be buffering data.
+ *
+ * @param playbackPositionUs The current playback position.
+ */
+ public void continueBuffering(long playbackPositionUs);
+
+ /**
+ * Attempts to read either a sample, a new format or or a discontinuity from the source.
+ *
+ * This method should not be called until after the source has been successfully prepared.
+ *
+ * Note that where multiple tracks are enabled, {@link #NOTHING_READ} may be returned if the
+ * next piece of data to be read from the {@link SampleSource} corresponds to a different track
+ * than the one for which data was requested.
+ *
+ * @param track The track from which to read.
+ * @param playbackPositionUs The current playback position.
+ * @param formatHolder A {@link FormatHolder} object to populate in the case of a new format.
+ * @param sampleHolder A {@link SampleHolder} object to populate in the case of a new sample. If
+ * the caller requires the sample data then it must ensure that {@link SampleHolder#data}
+ * references a valid output buffer.
+ * @param onlyReadDiscontinuity Whether to only read a discontinuity. If true, only
+ * {@link #DISCONTINUITY_READ} or {@link #NOTHING_READ} can be returned.
+ * @return The result, which can be {@link #SAMPLE_READ}, {@link #FORMAT_READ},
+ * {@link #DISCONTINUITY_READ}, {@link #NOTHING_READ} or {@link #END_OF_STREAM}.
+ * @throws IOException If an error occurred reading from the source.
+ */
+ public int readData(int track, long playbackPositionUs, FormatHolder formatHolder,
+ SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException;
+
+ /**
+ * Seeks to the specified time in microseconds.
+ *
+ * This method should not be called until after the source has been successfully prepared.
+ *
+ * @param timeUs The seek position in microseconds.
+ */
+ public void seekToUs(long timeUs);
+
+ /**
+ * Returns an estimate of the position up to which data is buffered.
+ *
+ * This method should not be called until after the source has been successfully prepared.
+ *
+ * @return An estimate of the absolute position in micro-seconds up to which data is buffered,
+ * or {@link TrackRenderer#END_OF_TRACK} if data is buffered to the end of the stream, or
+ * {@link TrackRenderer#UNKNOWN_TIME} if no estimate is available.
+ */
+ public long getBufferedPositionUs();
+
+ /**
+ * Releases the {@link SampleSource}.
+ */
+ public void release();
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/TrackInfo.java b/library/src/main/java/com/google/android/exoplayer/TrackInfo.java
new file mode 100644
index 00000000000..e6c1b0c977d
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/TrackInfo.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer;
+
+/**
+ * Holds high level information about a media track.
+ */
+public final class TrackInfo {
+
+ public final String mimeType;
+ public final long durationUs;
+
+ public TrackInfo(String mimeType, long durationUs) {
+ this.mimeType = mimeType;
+ this.durationUs = durationUs;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java
new file mode 100644
index 00000000000..a8c08cf4f03
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java
@@ -0,0 +1,348 @@
+/*
+ * 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;
+
+import com.google.android.exoplayer.ExoPlayer.ExoPlayerComponent;
+import com.google.android.exoplayer.util.Assertions;
+
+/**
+ * Renders a single component of media.
+ *
+ *
Internally, a renderer's lifecycle is managed by the owning {@link ExoPlayer}. The player
+ * will transition its renderers through various states as the overall playback state changes. The
+ * valid state transitions are shown below, annotated with the methods that are invoked during each
+ * transition.
+ *
+ */
+public abstract class TrackRenderer implements ExoPlayerComponent {
+
+ /**
+ * The renderer has been released and should not be used.
+ */
+ protected static final int STATE_RELEASED = -2;
+ /**
+ * The renderer should be ignored by the player.
+ */
+ protected static final int STATE_IGNORE = -1;
+ /**
+ * The renderer has not yet been prepared.
+ */
+ protected static final int STATE_UNPREPARED = 0;
+ /**
+ * The renderer has completed necessary preparation. Preparation may include, for example,
+ * reading the header of a media file to determine the track format and duration.
+ *
+ * The renderer should not hold scarce or expensive system resources (e.g. media decoders) and
+ * should not be actively buffering media data when in this state.
+ */
+ protected static final int STATE_PREPARED = 1;
+ /**
+ * The renderer is enabled. It should either be ready to be started, or be actively working
+ * towards this state (e.g. a renderer in this state will typically hold any resources that it
+ * requires, such as media decoders, and will have buffered or be buffering any media data that
+ * is required to start playback).
+ */
+ protected static final int STATE_ENABLED = 2;
+ /**
+ * The renderer is started. Calls to {@link #doSomeWork(long)} should cause the media to be
+ * rendered.
+ */
+ protected static final int STATE_STARTED = 3;
+
+ /**
+ * Represents an unknown time or duration.
+ */
+ public static final long UNKNOWN_TIME = -1;
+ /**
+ * Represents a time or duration that should match the duration of the longest track whose
+ * duration is known.
+ */
+ public static final long MATCH_LONGEST = -2;
+ /**
+ * Represents the time of the end of the track.
+ */
+ public static final long END_OF_TRACK = -3;
+
+ private int state;
+
+ /**
+ * A time source renderer is a renderer that, when started, advances its own playback position.
+ * This means that {@link #getCurrentPositionUs()} will return increasing positions independently
+ * to increasing values being passed to {@link #doSomeWork(long)}. A player may have at most one
+ * time source renderer. If provided, the player will use such a renderer as its source of time
+ * during playback.
+ *
+ * This method may be called when the renderer is in any state.
+ *
+ * @return True if the renderer should be considered a time source. False otherwise.
+ */
+ protected boolean isTimeSource() {
+ return false;
+ }
+
+ /**
+ * Returns the current state of the renderer.
+ *
+ * @return The current state (one of the STATE_* constants).
+ */
+ protected final int getState() {
+ return state;
+ }
+
+ /**
+ * Prepares the renderer. This method is non-blocking, and hence it may be necessary to call it
+ * more than once in order to transition the renderer into the prepared state.
+ *
+ * @return The current state (one of the STATE_* constants), for convenience.
+ */
+ @SuppressWarnings("unused")
+ /* package */ final int prepare() throws ExoPlaybackException {
+ Assertions.checkState(state == TrackRenderer.STATE_UNPREPARED);
+ state = doPrepare();
+ Assertions.checkState(state == TrackRenderer.STATE_UNPREPARED ||
+ state == TrackRenderer.STATE_PREPARED ||
+ state == TrackRenderer.STATE_IGNORE);
+ return state;
+ }
+
+ /**
+ * Invoked to make progress when the renderer is in the {@link #STATE_UNPREPARED} state. This
+ * method will be called repeatedly until a value other than {@link #STATE_UNPREPARED} is
+ * returned.
+ *
+ * This method should return quickly, and should not block if the renderer is currently unable to
+ * make any useful progress.
+ *
+ * @return The new state of the renderer. One of {@link #STATE_UNPREPARED},
+ * {@link #STATE_PREPARED} and {@link #STATE_IGNORE}.
+ * @throws ExoPlaybackException If an error occurs.
+ */
+ protected abstract int doPrepare() throws ExoPlaybackException;
+
+ /**
+ * Enable the renderer.
+ *
+ * @param timeUs The player's current position.
+ * @param joining Whether this renderer is being enabled to join an ongoing playback. If true
+ * then {@link #start} must be called immediately after this method returns (unless a
+ * {@link ExoPlaybackException} is thrown).
+ */
+ /* package */ final void enable(long timeUs, boolean joining) throws ExoPlaybackException {
+ Assertions.checkState(state == TrackRenderer.STATE_PREPARED);
+ state = TrackRenderer.STATE_ENABLED;
+ onEnabled(timeUs, joining);
+ }
+
+ /**
+ * Called when the renderer is enabled.
+ *
+ * The default implementation is a no-op.
+ *
+ * @param timeUs The player's current position.
+ * @param joining Whether this renderer is being enabled to join an ongoing playback. If true
+ * then {@link #onStarted} is guaranteed to be called immediately after this method returns
+ * (unless a {@link ExoPlaybackException} is thrown).
+ * @throws ExoPlaybackException If an error occurs.
+ */
+ protected void onEnabled(long timeUs, boolean joining) throws ExoPlaybackException {
+ // Do nothing.
+ }
+
+ /**
+ * Starts the renderer, meaning that calls to {@link #doSomeWork(long)} will cause the
+ * track to be rendered.
+ */
+ /* package */ final void start() throws ExoPlaybackException {
+ Assertions.checkState(state == TrackRenderer.STATE_ENABLED);
+ state = TrackRenderer.STATE_STARTED;
+ onStarted();
+ }
+
+ /**
+ * Called when the renderer is started.
+ *
+ * The default implementation is a no-op.
+ *
+ * @throws ExoPlaybackException If an error occurs.
+ */
+ protected void onStarted() throws ExoPlaybackException {
+ // Do nothing.
+ }
+
+ /**
+ * Stops the renderer.
+ */
+ /* package */ final void stop() throws ExoPlaybackException {
+ Assertions.checkState(state == TrackRenderer.STATE_STARTED);
+ state = TrackRenderer.STATE_ENABLED;
+ onStopped();
+ }
+
+ /**
+ * Called when the renderer is stopped.
+ *
+ * The default implementation is a no-op.
+ *
+ * @throws ExoPlaybackException If an error occurs.
+ */
+ protected void onStopped() throws ExoPlaybackException {
+ // Do nothing.
+ }
+
+ /**
+ * Disable the renderer.
+ */
+ /* package */ final void disable() throws ExoPlaybackException {
+ Assertions.checkState(state == TrackRenderer.STATE_ENABLED);
+ state = TrackRenderer.STATE_PREPARED;
+ onDisabled();
+ }
+
+ /**
+ * Called when the renderer is disabled.
+ *
+ * The default implementation is a no-op.
+ *
+ * @throws ExoPlaybackException If an error occurs.
+ */
+ protected void onDisabled() throws ExoPlaybackException {
+ // Do nothing.
+ }
+
+ /**
+ * Releases the renderer.
+ */
+ /* package */ final void release() throws ExoPlaybackException {
+ Assertions.checkState(state != TrackRenderer.STATE_ENABLED
+ && state != TrackRenderer.STATE_STARTED
+ && state != TrackRenderer.STATE_RELEASED);
+ state = TrackRenderer.STATE_RELEASED;
+ onReleased();
+ }
+
+ /**
+ * Called when the renderer is released.
+ *
+ * The default implementation is a no-op.
+ *
+ * @throws ExoPlaybackException If an error occurs.
+ */
+ protected void onReleased() throws ExoPlaybackException {
+ // Do nothing.
+ }
+
+ /**
+ * Whether the renderer is ready for the {@link ExoPlayer} instance to transition to
+ * {@link ExoPlayer#STATE_ENDED}. The player will make this transition as soon as {@code true} is
+ * returned by all of its {@link TrackRenderer}s.
+ *
+ * This method may be called when the renderer is in the following states:
+ * {@link #STATE_ENABLED}, {@link #STATE_STARTED}
+ *
+ * @return Whether the renderer is ready for the player to transition to the ended state.
+ */
+ protected abstract boolean isEnded();
+
+ /**
+ * Whether the renderer is able to immediately render media from the current position.
+ *
+ * If the renderer is in the {@link #STATE_STARTED} state then returning true indicates that the
+ * renderer has everything that it needs to continue playback. Returning false indicates that
+ * the player should pause until the renderer is ready.
+ *
+ * If the renderer is in the {@link #STATE_ENABLED} state then returning true indicates that the
+ * renderer is ready for playback to be started. Returning false indicates that it is not.
+ *
+ * This method may be called when the renderer is in the following states:
+ * {@link #STATE_ENABLED}, {@link #STATE_STARTED}
+ *
+ * @return True if the renderer is ready to render media. False otherwise.
+ */
+ protected abstract boolean isReady();
+
+ /**
+ * Invoked to make progress when the renderer is in the {@link #STATE_ENABLED} or
+ * {@link #STATE_STARTED} states.
+ *
+ * If the renderer's state is {@link #STATE_STARTED}, then repeated calls to this method should
+ * cause the media track to be rendered. If the state is {@link #STATE_ENABLED}, then repeated
+ * calls should make progress towards getting the renderer into a position where it is ready to
+ * render the track.
+ *
+ * This method should return quickly, and should not block if the renderer is currently unable to
+ * make any useful progress.
+ *
+ * This method may be called when the renderer is in the following states:
+ * {@link #STATE_ENABLED}, {@link #STATE_STARTED}
+ *
+ * @param timeUs The current playback time.
+ * @throws ExoPlaybackException If an error occurs.
+ */
+ protected abstract void doSomeWork(long timeUs) throws ExoPlaybackException;
+
+ /**
+ * Returns the duration of the media being rendered.
+ *
+ * This method may be called when the renderer is in the following states:
+ * {@link #STATE_PREPARED}, {@link #STATE_ENABLED}, {@link #STATE_STARTED}
+ *
+ * @return The duration of the track in micro-seconds, or {@link #MATCH_LONGEST} if
+ * the track's duration should match that of the longest track whose duration is known, or
+ * or {@link #UNKNOWN_TIME} if the duration is not known.
+ */
+ protected abstract long getDurationUs();
+
+ /**
+ * Returns the current playback position.
+ *
+ * This method may be called when the renderer is in the following states:
+ * {@link #STATE_ENABLED}, {@link #STATE_STARTED}
+ *
+ * @return The current playback position in micro-seconds.
+ */
+ protected abstract long getCurrentPositionUs();
+
+ /**
+ * Returns an estimate of the absolute position in micro-seconds up to which data is buffered.
+ *
+ * This method may be called when the renderer is in the following states:
+ * {@link #STATE_ENABLED}, {@link #STATE_STARTED}
+ *
+ * @return An estimate of the absolute position in micro-seconds up to which data is buffered,
+ * or {@link #END_OF_TRACK} if the track is fully buffered, or {@link #UNKNOWN_TIME} if no
+ * estimate is available.
+ */
+ protected abstract long getBufferedPositionUs();
+
+ /**
+ * Seeks to a specified time in the track.
+ *
+ * This method may be called when the renderer is in the following states:
+ * {@link #STATE_ENABLED}
+ *
+ * @param timeUs The desired time in micro-seconds.
+ * @throws ExoPlaybackException If an error occurs.
+ */
+ protected abstract void seekTo(long timeUs) throws ExoPlaybackException;
+
+ @Override
+ public void handleMessage(int what, Object object) throws ExoPlaybackException {
+ // Do nothing.
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/VideoSurfaceView.java b/library/src/main/java/com/google/android/exoplayer/VideoSurfaceView.java
new file mode 100644
index 00000000000..9923e2ae5d1
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/VideoSurfaceView.java
@@ -0,0 +1,77 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.SurfaceView;
+
+/**
+ * A SurfaceView that resizes itself to match a specified aspect ratio.
+ */
+public class VideoSurfaceView extends SurfaceView {
+
+ /**
+ * The surface view will not resize itself if the fractional difference between its default
+ * aspect ratio and the aspect ratio of the video falls below this threshold.
+ *
+ * This tolerance is useful for fullscreen playbacks, since it ensures that the surface will
+ * occupy the whole of the screen when playing content that has the same (or virtually the same)
+ * aspect ratio as the device. This typically reduces the number of view layers that need to be
+ * composited by the underlying system, which can help to reduce power consumption.
+ */
+ private static final float MAX_ASPECT_RATIO_DEFORMATION_PERCENT = 0.01f;
+
+ private float videoAspectRatio;
+
+ public VideoSurfaceView(Context context) {
+ super(context);
+ }
+
+ public VideoSurfaceView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ /**
+ * Set the aspect ratio that this {@link VideoSurfaceView} should satisfy.
+ *
+ * @param widthHeightRatio The width to height ratio.
+ */
+ public void setVideoWidthHeightRatio(float widthHeightRatio) {
+ if (this.videoAspectRatio != widthHeightRatio) {
+ this.videoAspectRatio = widthHeightRatio;
+ requestLayout();
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ int width = getMeasuredWidth();
+ int height = getMeasuredHeight();
+ if (videoAspectRatio != 0) {
+ float viewAspectRatio = (float) width / height;
+ float aspectDeformation = videoAspectRatio / viewAspectRatio - 1;
+ if (aspectDeformation > MAX_ASPECT_RATIO_DEFORMATION_PERCENT) {
+ height = (int) (width / videoAspectRatio);
+ } else if (aspectDeformation < -MAX_ASPECT_RATIO_DEFORMATION_PERCENT) {
+ width = (int) (height * videoAspectRatio);
+ }
+ }
+ setMeasuredDimension(width, height);
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/Chunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/Chunk.java
new file mode 100644
index 00000000000..72a95232c55
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/chunk/Chunk.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer.chunk;
+
+import com.google.android.exoplayer.upstream.Allocation;
+import com.google.android.exoplayer.upstream.Allocator;
+import com.google.android.exoplayer.upstream.DataSource;
+import com.google.android.exoplayer.upstream.DataSourceStream;
+import com.google.android.exoplayer.upstream.DataSpec;
+import com.google.android.exoplayer.upstream.Loader.Loadable;
+import com.google.android.exoplayer.upstream.NonBlockingInputStream;
+import com.google.android.exoplayer.util.Assertions;
+
+import java.io.IOException;
+
+/**
+ * An abstract base class for {@link Loadable} implementations that load chunks of data required
+ * for the playback of streams.
+ */
+public abstract class Chunk implements Loadable {
+
+ /**
+ * The format associated with the data being loaded.
+ */
+ // TODO: Consider removing this and pushing it down into MediaChunk instead.
+ public final Format format;
+ /**
+ * The reason for a {@link ChunkSource} having generated this chunk. For reporting only. Possible
+ * values for this variable are defined by the specific {@link ChunkSource} implementations.
+ */
+ public final int trigger;
+
+ private final DataSource dataSource;
+ private final DataSpec dataSpec;
+
+ private DataSourceStream dataSourceStream;
+
+ /**
+ * @param dataSource The source from which the data should be loaded.
+ * @param dataSpec Defines the data to be loaded. {@code dataSpec.length} must not exceed
+ * {@link Integer#MAX_VALUE}. If {@code dataSpec.length == DataSpec.LENGTH_UNBOUNDED} then
+ * the length resolved by {@code dataSource.open(dataSpec)} must not exceed
+ * {@link Integer#MAX_VALUE}.
+ * @param format See {@link #format}.
+ * @param trigger See {@link #trigger}.
+ */
+ public Chunk(DataSource dataSource, DataSpec dataSpec, Format format, int trigger) {
+ Assertions.checkState(dataSpec.length <= Integer.MAX_VALUE);
+ this.dataSource = Assertions.checkNotNull(dataSource);
+ this.dataSpec = Assertions.checkNotNull(dataSpec);
+ this.format = Assertions.checkNotNull(format);
+ this.trigger = trigger;
+ }
+
+ /**
+ * Initializes the {@link Chunk}.
+ *
+ * @param allocator An {@link Allocator} from which the {@link Allocation} needed to contain the
+ * data can be obtained.
+ */
+ public final void init(Allocator allocator) {
+ Assertions.checkState(dataSourceStream == null);
+ dataSourceStream = new DataSourceStream(dataSource, dataSpec, allocator);
+ }
+
+ /**
+ * Releases the {@link Chunk}, releasing any backing {@link Allocation}s.
+ */
+ public final void release() {
+ if (dataSourceStream != null) {
+ dataSourceStream.close();
+ dataSourceStream = null;
+ }
+ }
+
+ /**
+ * Gets the length of the chunk in bytes.
+ *
+ * @return The length of the chunk in bytes, or {@value DataSpec#LENGTH_UNBOUNDED} if the length
+ * has yet to be determined.
+ */
+ public final long getLength() {
+ return dataSourceStream.getLength();
+ }
+
+ /**
+ * Whether the whole of the data has been consumed.
+ *
+ * @return True if the whole of the data has been consumed. False otherwise.
+ */
+ public final boolean isReadFinished() {
+ return dataSourceStream.isEndOfStream();
+ }
+
+ /**
+ * Whether the whole of the chunk has been loaded.
+ *
+ * @return True if the whole of the chunk has been loaded. False otherwise.
+ */
+ public final boolean isLoadFinished() {
+ return dataSourceStream.isLoadFinished();
+ }
+
+ /**
+ * Gets the number of bytes that have been loaded.
+ *
+ * @return The number of bytes that have been loaded.
+ */
+ public final long bytesLoaded() {
+ return dataSourceStream.getLoadPosition();
+ }
+
+ /**
+ * Causes loaded data to be consumed.
+ *
+ * @throws IOException If an error occurs consuming the loaded data.
+ */
+ public final void consume() throws IOException {
+ Assertions.checkState(dataSourceStream != null);
+ consumeStream(dataSourceStream);
+ }
+
+ /**
+ * Returns a byte array containing the loaded data. If the chunk is partially loaded, this
+ * method returns the data that has been loaded so far. If nothing has been loaded, null is
+ * returned.
+ *
+ * @return The loaded data or null.
+ */
+ public final byte[] getLoadedData() {
+ Assertions.checkState(dataSourceStream != null);
+ return dataSourceStream.getLoadedData();
+ }
+
+ /**
+ * Invoked by {@link #consume()}. Implementations may override this method if they wish to
+ * consume the loaded data at this point.
+ *
+ * The default implementation is a no-op.
+ *
+ * @param stream The stream of loaded data.
+ * @throws IOException If an error occurs consuming the loaded data.
+ */
+ protected void consumeStream(NonBlockingInputStream stream) throws IOException {
+ // Do nothing.
+ }
+
+ protected final NonBlockingInputStream getNonBlockingInputStream() {
+ return dataSourceStream;
+ }
+
+ protected final void resetReadPosition() {
+ if (dataSourceStream != null) {
+ dataSourceStream.resetReadPosition();
+ } else {
+ // We haven't been initialized yet, so the read position must already be 0.
+ }
+ }
+
+ // Loadable implementation
+
+ @Override
+ public final void cancelLoad() {
+ dataSourceStream.cancelLoad();
+ }
+
+ @Override
+ public final boolean isLoadCanceled() {
+ return dataSourceStream.isLoadCanceled();
+ }
+
+ @Override
+ public final void load() throws IOException, InterruptedException {
+ dataSourceStream.load();
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkOperationHolder.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkOperationHolder.java
new file mode 100644
index 00000000000..c59bce97331
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkOperationHolder.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer.chunk;
+
+/**
+ * Holds a chunk operation, which consists of a {@link Chunk} to load together with the number of
+ * {@link MediaChunk}s that should be retained on the queue.
+ */
+public final class ChunkOperationHolder {
+
+ /**
+ * The number of {@link MediaChunk}s to retain in a queue.
+ */
+ public int queueSize;
+
+ /**
+ * The chunk.
+ */
+ public Chunk chunk;
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java
new file mode 100644
index 00000000000..5800acca266
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java
@@ -0,0 +1,753 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer.chunk;
+
+import com.google.android.exoplayer.FormatHolder;
+import com.google.android.exoplayer.LoadControl;
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.SampleHolder;
+import com.google.android.exoplayer.SampleSource;
+import com.google.android.exoplayer.TrackInfo;
+import com.google.android.exoplayer.TrackRenderer;
+import com.google.android.exoplayer.upstream.DataSpec;
+import com.google.android.exoplayer.upstream.Loader;
+import com.google.android.exoplayer.util.Assertions;
+
+import android.os.Handler;
+import android.os.SystemClock;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * A {@link SampleSource} that loads media in {@link Chunk}s, which are themselves obtained from a
+ * {@link ChunkSource}.
+ */
+public class ChunkSampleSource implements SampleSource, Loader.Listener {
+
+ /**
+ * Interface definition for a callback to be notified of {@link ChunkSampleSource} events.
+ */
+ public interface EventListener {
+
+ /**
+ * Invoked when an upstream load is started.
+ *
+ * @param sourceId The id of the reporting {@link SampleSource}.
+ * @param formatId The format id.
+ * @param trigger A trigger for the format selection, as specified by the {@link ChunkSource}.
+ * @param isInitialization Whether the load is for format initialization data.
+ * @param mediaStartTimeMs The media time of the start of the data being loaded, or -1 if this
+ * load is for initialization data.
+ * @param mediaEndTimeMs The media time of the end of the data being loaded, or -1 if this
+ * load is for initialization data.
+ * @param totalBytes The length of the data being loaded in bytes.
+ */
+ void onLoadStarted(int sourceId, int formatId, int trigger, boolean isInitialization,
+ int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes);
+
+ /**
+ * Invoked when the current load operation completes.
+ *
+ * @param sourceId The id of the reporting {@link SampleSource}.
+ */
+ void onLoadCompleted(int sourceId);
+
+ /**
+ * Invoked when the current upstream load operation is canceled.
+ *
+ * @param sourceId The id of the reporting {@link SampleSource}.
+ */
+ void onLoadCanceled(int sourceId);
+
+ /**
+ * Invoked when data is removed from the back of the buffer, typically so that it can be
+ * re-buffered using a different representation.
+ *
+ * @param sourceId The id of the reporting {@link SampleSource}.
+ * @param mediaStartTimeMs The media time of the start of the discarded data.
+ * @param mediaEndTimeMs The media time of the end of the discarded data.
+ * @param totalBytes The length of the data being discarded in bytes.
+ */
+ void onUpstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs,
+ long totalBytes);
+
+ /**
+ * Invoked when an error occurs loading media data.
+ *
+ * @param sourceId The id of the reporting {@link SampleSource}.
+ * @param e The cause of the failure.
+ */
+ void onUpstreamError(int sourceId, IOException e);
+
+ /**
+ * Invoked when an error occurs consuming loaded data.
+ *
+ * @param sourceId The id of the reporting {@link SampleSource}.
+ * @param e The cause of the failure.
+ */
+ void onConsumptionError(int sourceId, IOException e);
+
+ /**
+ * Invoked when data is removed from the front of the buffer, typically due to a seek or
+ * because the data has been consumed.
+ *
+ * @param sourceId The id of the reporting {@link SampleSource}.
+ * @param mediaStartTimeMs The media time of the start of the discarded data.
+ * @param mediaEndTimeMs The media time of the end of the discarded data.
+ * @param totalBytes The length of the data being discarded in bytes.
+ */
+ void onDownstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs,
+ long totalBytes);
+
+ /**
+ * Invoked when the downstream format changes (i.e. when the format being supplied to the
+ * caller of {@link SampleSource#readData} changes).
+ *
+ * @param sourceId The id of the reporting {@link SampleSource}.
+ * @param formatId The format id.
+ * @param trigger The trigger specified in the corresponding upstream load, as specified by the
+ * {@link ChunkSource}.
+ * @param mediaTimeMs The media time at which the change occurred.
+ */
+ void onDownstreamFormatChanged(int sourceId, int formatId, int trigger, int mediaTimeMs);
+
+ }
+
+ private static final int STATE_UNPREPARED = 0;
+ private static final int STATE_PREPARED = 1;
+ private static final int STATE_ENABLED = 2;
+
+ private static final int NO_RESET_PENDING = -1;
+
+ private final int eventSourceId;
+ private final LoadControl loadControl;
+ private final ChunkSource chunkSource;
+ private final ChunkOperationHolder currentLoadableHolder;
+ private final LinkedList mediaChunks;
+ private final List readOnlyMediaChunks;
+ private final int bufferSizeContribution;
+ private final boolean frameAccurateSeeking;
+ private final Handler eventHandler;
+ private final EventListener eventListener;
+
+ private int state;
+ private long downstreamPositionUs;
+ private long lastSeekPositionUs;
+ private long pendingResetTime;
+ private long lastPerformedBufferOperation;
+ private boolean pendingDiscontinuity;
+
+ private Loader loader;
+ private IOException currentLoadableException;
+ private boolean currentLoadableExceptionFatal;
+ private int currentLoadableExceptionCount;
+ private long currentLoadableExceptionTimestamp;
+
+ private volatile Format downstreamFormat;
+
+ public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl,
+ int bufferSizeContribution, boolean frameAccurateSeeking) {
+ this(chunkSource, loadControl, bufferSizeContribution, frameAccurateSeeking, null, null, 0);
+ }
+
+ public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl,
+ int bufferSizeContribution, boolean frameAccurateSeeking, Handler eventHandler,
+ EventListener eventListener, int eventSourceId) {
+ this.chunkSource = chunkSource;
+ this.loadControl = loadControl;
+ this.bufferSizeContribution = bufferSizeContribution;
+ this.frameAccurateSeeking = frameAccurateSeeking;
+ this.eventHandler = eventHandler;
+ this.eventListener = eventListener;
+ this.eventSourceId = eventSourceId;
+ currentLoadableHolder = new ChunkOperationHolder();
+ mediaChunks = new LinkedList();
+ readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks);
+ state = STATE_UNPREPARED;
+ }
+
+ /**
+ * Exposes the current downstream format for debugging purposes. Can be called from any thread.
+ *
+ * @return The current downstream format.
+ */
+ public Format getFormat() {
+ return downstreamFormat;
+ }
+
+ @Override
+ public boolean prepare() {
+ Assertions.checkState(state == STATE_UNPREPARED);
+ loader = new Loader("Loader:" + chunkSource.getTrackInfo().mimeType, this);
+ state = STATE_PREPARED;
+ return true;
+ }
+
+ @Override
+ public int getTrackCount() {
+ Assertions.checkState(state != STATE_UNPREPARED);
+ return 1;
+ }
+
+ @Override
+ public TrackInfo getTrackInfo(int track) {
+ Assertions.checkState(state != STATE_UNPREPARED);
+ Assertions.checkState(track == 0);
+ return chunkSource.getTrackInfo();
+ }
+
+ @Override
+ public void enable(int track, long timeUs) {
+ Assertions.checkState(state == STATE_PREPARED);
+ Assertions.checkState(track == 0);
+ state = STATE_ENABLED;
+ chunkSource.enable();
+ loadControl.register(this, bufferSizeContribution);
+ downstreamFormat = null;
+ downstreamPositionUs = timeUs;
+ lastSeekPositionUs = timeUs;
+ restartFrom(timeUs);
+ }
+
+ @Override
+ public void disable(int track) {
+ Assertions.checkState(state == STATE_ENABLED);
+ Assertions.checkState(track == 0);
+ pendingDiscontinuity = false;
+ state = STATE_PREPARED;
+ loadControl.unregister(this);
+ chunkSource.disable(mediaChunks);
+ if (loader.isLoading()) {
+ loader.cancelLoading();
+ } else {
+ clearMediaChunks();
+ clearCurrentLoadable();
+ loadControl.trimAllocator();
+ }
+ }
+
+ @Override
+ public void continueBuffering(long playbackPositionUs) {
+ Assertions.checkState(state == STATE_ENABLED);
+ downstreamPositionUs = playbackPositionUs;
+ chunkSource.continueBuffering(playbackPositionUs);
+ updateLoadControl();
+ }
+
+ @Override
+ public int readData(int track, long playbackPositionUs, FormatHolder formatHolder,
+ SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException {
+ Assertions.checkState(state == STATE_ENABLED);
+ Assertions.checkState(track == 0);
+
+ if (pendingDiscontinuity) {
+ pendingDiscontinuity = false;
+ return DISCONTINUITY_READ;
+ }
+
+ if (onlyReadDiscontinuity) {
+ return NOTHING_READ;
+ }
+
+ downstreamPositionUs = playbackPositionUs;
+ if (isPendingReset()) {
+ if (currentLoadableException != null) {
+ throw currentLoadableException;
+ }
+ IOException chunkSourceException = chunkSource.getError();
+ if (chunkSourceException != null) {
+ throw chunkSourceException;
+ }
+ return NOTHING_READ;
+ }
+
+ MediaChunk mediaChunk = mediaChunks.getFirst();
+ if (mediaChunk.isReadFinished()) {
+ // We've read all of the samples from the current media chunk.
+ if (mediaChunks.size() > 1) {
+ discardDownstreamMediaChunk();
+ mediaChunk = mediaChunks.getFirst();
+ mediaChunk.seekToStart();
+ return readData(track, playbackPositionUs, formatHolder, sampleHolder, false);
+ } else if (mediaChunk.isLastChunk()) {
+ return END_OF_STREAM;
+ } else {
+ IOException chunkSourceException = chunkSource.getError();
+ if (chunkSourceException != null) {
+ throw chunkSourceException;
+ }
+ return NOTHING_READ;
+ }
+ } else if (downstreamFormat == null || downstreamFormat.id != mediaChunk.format.id) {
+ notifyDownstreamFormatChanged(mediaChunk.format.id, mediaChunk.trigger,
+ mediaChunk.startTimeUs);
+ MediaFormat format = mediaChunk.getMediaFormat();
+ chunkSource.getMaxVideoDimensions(format);
+ formatHolder.format = format;
+ formatHolder.drmInitData = mediaChunk.getPsshInfo();
+ downstreamFormat = mediaChunk.format;
+ return FORMAT_READ;
+ }
+
+ if (mediaChunk.read(sampleHolder)) {
+ sampleHolder.decodeOnly = frameAccurateSeeking && sampleHolder.timeUs < lastSeekPositionUs;
+ onSampleRead(mediaChunk, sampleHolder);
+ return SAMPLE_READ;
+ } else {
+ if (currentLoadableException != null) {
+ throw currentLoadableException;
+ }
+ return NOTHING_READ;
+ }
+ }
+
+ @Override
+ public void seekToUs(long timeUs) {
+ Assertions.checkState(state == STATE_ENABLED);
+ downstreamPositionUs = timeUs;
+ lastSeekPositionUs = timeUs;
+ if (pendingResetTime == timeUs) {
+ return;
+ }
+
+ MediaChunk mediaChunk = getMediaChunk(timeUs);
+ if (mediaChunk == null) {
+ restartFrom(timeUs);
+ pendingDiscontinuity = true;
+ } else {
+ pendingDiscontinuity |= mediaChunk.seekTo(timeUs, mediaChunk == mediaChunks.getFirst());
+ discardDownstreamMediaChunks(mediaChunk);
+ updateLoadControl();
+ }
+ }
+
+ private MediaChunk getMediaChunk(long timeUs) {
+ Iterator mediaChunkIterator = mediaChunks.iterator();
+ while (mediaChunkIterator.hasNext()) {
+ MediaChunk mediaChunk = mediaChunkIterator.next();
+ if (timeUs < mediaChunk.startTimeUs) {
+ return null;
+ } else if (mediaChunk.isLastChunk() || timeUs < mediaChunk.endTimeUs) {
+ return mediaChunk;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public long getBufferedPositionUs() {
+ Assertions.checkState(state == STATE_ENABLED);
+ if (isPendingReset()) {
+ return pendingResetTime;
+ }
+ MediaChunk mediaChunk = mediaChunks.getLast();
+ Chunk currentLoadable = currentLoadableHolder.chunk;
+ if (currentLoadable != null && mediaChunk == currentLoadable) {
+ // Linearly interpolate partially-fetched chunk times.
+ long chunkLength = mediaChunk.getLength();
+ if (chunkLength != DataSpec.LENGTH_UNBOUNDED) {
+ return mediaChunk.startTimeUs + ((mediaChunk.endTimeUs - mediaChunk.startTimeUs) *
+ mediaChunk.bytesLoaded()) / chunkLength;
+ } else {
+ return mediaChunk.startTimeUs;
+ }
+ } else if (mediaChunk.isLastChunk()) {
+ return TrackRenderer.END_OF_TRACK;
+ } else {
+ return mediaChunk.endTimeUs;
+ }
+ }
+
+ @Override
+ public void release() {
+ Assertions.checkState(state != STATE_ENABLED);
+ if (loader != null) {
+ loader.release();
+ loader = null;
+ }
+ state = STATE_UNPREPARED;
+ }
+
+ @Override
+ public void onLoaded() {
+ Chunk currentLoadable = currentLoadableHolder.chunk;
+ try {
+ currentLoadable.consume();
+ } catch (IOException e) {
+ currentLoadableException = e;
+ currentLoadableExceptionCount++;
+ currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
+ currentLoadableExceptionFatal = true;
+ notifyConsumptionError(e);
+ } finally {
+ if (!isMediaChunk(currentLoadable)) {
+ currentLoadable.release();
+ }
+ if (!currentLoadableExceptionFatal) {
+ clearCurrentLoadable();
+ }
+ notifyLoadCompleted();
+ updateLoadControl();
+ }
+ }
+
+ @Override
+ public void onCanceled() {
+ Chunk currentLoadable = currentLoadableHolder.chunk;
+ if (!isMediaChunk(currentLoadable)) {
+ currentLoadable.release();
+ }
+ clearCurrentLoadable();
+ notifyLoadCanceled();
+ if (state == STATE_ENABLED) {
+ restartFrom(pendingResetTime);
+ } else {
+ clearMediaChunks();
+ loadControl.trimAllocator();
+ }
+ }
+
+ @Override
+ public void onError(IOException e) {
+ currentLoadableException = e;
+ currentLoadableExceptionCount++;
+ currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
+ notifyUpstreamError(e);
+ updateLoadControl();
+ }
+
+ /**
+ * Called when a sample has been read from a {@link MediaChunk}. Can be used to perform any
+ * modifications necessary before the sample is returned.
+ *
+ * @param mediaChunk The MediaChunk the sample was ready from.
+ * @param sampleHolder The sample that has just been read.
+ */
+ protected void onSampleRead(MediaChunk mediaChunk, SampleHolder sampleHolder) {
+ // no-op
+ }
+
+ private void restartFrom(long timeUs) {
+ pendingResetTime = timeUs;
+ if (loader.isLoading()) {
+ loader.cancelLoading();
+ } else {
+ clearMediaChunks();
+ clearCurrentLoadable();
+ updateLoadControl();
+ }
+ }
+
+ private void clearMediaChunks() {
+ discardDownstreamMediaChunks(null);
+ }
+
+ private void clearCurrentLoadable() {
+ currentLoadableHolder.chunk = null;
+ currentLoadableException = null;
+ currentLoadableExceptionCount = 0;
+ currentLoadableExceptionFatal = false;
+ }
+
+ private void updateLoadControl() {
+ long loadPositionUs;
+ if (isPendingReset()) {
+ loadPositionUs = pendingResetTime;
+ } else {
+ MediaChunk lastMediaChunk = mediaChunks.getLast();
+ loadPositionUs = lastMediaChunk.nextChunkIndex == -1 ? -1 : lastMediaChunk.endTimeUs;
+ }
+
+ boolean isBackedOff = currentLoadableException != null && !currentLoadableExceptionFatal;
+ boolean nextLoader = loadControl.update(this, downstreamPositionUs, loadPositionUs,
+ isBackedOff || loader.isLoading(), currentLoadableExceptionFatal);
+
+ if (currentLoadableExceptionFatal) {
+ return;
+ }
+
+ long now = SystemClock.elapsedRealtime();
+
+ if (isBackedOff) {
+ long elapsedMillis = now - currentLoadableExceptionTimestamp;
+ if (elapsedMillis >= getRetryDelayMillis(currentLoadableExceptionCount)) {
+ resumeFromBackOff();
+ }
+ return;
+ }
+
+ if (!loader.isLoading()) {
+ if (currentLoadableHolder.chunk == null || now - lastPerformedBufferOperation > 1000) {
+ lastPerformedBufferOperation = now;
+ currentLoadableHolder.queueSize = readOnlyMediaChunks.size();
+ chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetTime, downstreamPositionUs,
+ currentLoadableHolder);
+ discardUpstreamMediaChunks(currentLoadableHolder.queueSize);
+ }
+ if (nextLoader) {
+ maybeStartLoading();
+ }
+ }
+ }
+
+ /**
+ * Resumes loading.
+ *
+ * If the {@link ChunkSource} returns a chunk equivalent to the backed off chunk B, then the
+ * loading of B will be resumed. In all other cases B will be discarded and the new chunk will
+ * be loaded.
+ */
+ private void resumeFromBackOff() {
+ currentLoadableException = null;
+
+ Chunk backedOffChunk = currentLoadableHolder.chunk;
+ if (!isMediaChunk(backedOffChunk)) {
+ currentLoadableHolder.queueSize = readOnlyMediaChunks.size();
+ chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetTime, downstreamPositionUs,
+ currentLoadableHolder);
+ discardUpstreamMediaChunks(currentLoadableHolder.queueSize);
+ if (currentLoadableHolder.chunk == backedOffChunk) {
+ // Chunk was unchanged. Resume loading.
+ loader.startLoading(backedOffChunk);
+ } else {
+ backedOffChunk.release();
+ maybeStartLoading();
+ }
+ return;
+ }
+
+ if (backedOffChunk == mediaChunks.getFirst()) {
+ // We're not able to clear the first media chunk, so we have no choice but to continue
+ // loading it.
+ loader.startLoading(backedOffChunk);
+ return;
+ }
+
+ // The current loadable is the last media chunk. Remove it before we invoke the chunk source,
+ // and add it back again afterwards.
+ MediaChunk removedChunk = mediaChunks.removeLast();
+ Assertions.checkState(backedOffChunk == removedChunk);
+ currentLoadableHolder.queueSize = readOnlyMediaChunks.size();
+ chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetTime, downstreamPositionUs,
+ currentLoadableHolder);
+ mediaChunks.add(removedChunk);
+
+ if (currentLoadableHolder.chunk == backedOffChunk) {
+ // Chunk was unchanged. Resume loading.
+ loader.startLoading(backedOffChunk);
+ } else {
+ // This call will remove and release at least one chunk from the end of mediaChunks. Since
+ // the current loadable is the last media chunk, it is guaranteed to be removed.
+ discardUpstreamMediaChunks(currentLoadableHolder.queueSize);
+ clearCurrentLoadable();
+ maybeStartLoading();
+ }
+ }
+
+ private void maybeStartLoading() {
+ Chunk currentLoadable = currentLoadableHolder.chunk;
+ if (currentLoadable == null) {
+ // Nothing to load.
+ return;
+ }
+ currentLoadable.init(loadControl.getAllocator());
+ if (isMediaChunk(currentLoadable)) {
+ MediaChunk mediaChunk = (MediaChunk) currentLoadable;
+ if (isPendingReset()) {
+ mediaChunk.seekTo(pendingResetTime, false);
+ pendingResetTime = NO_RESET_PENDING;
+ }
+ mediaChunks.add(mediaChunk);
+ notifyLoadStarted(mediaChunk.format.id, mediaChunk.trigger, false,
+ mediaChunk.startTimeUs, mediaChunk.endTimeUs, mediaChunk.getLength());
+ } else {
+ notifyLoadStarted(currentLoadable.format.id, currentLoadable.trigger, true, -1, -1,
+ currentLoadable.getLength());
+ }
+ loader.startLoading(currentLoadable);
+ }
+
+ /**
+ * Discards downstream media chunks until {@code untilChunk} if found. {@code untilChunk} is not
+ * itself discarded. Null can be passed to discard all media chunks.
+ *
+ * @param untilChunk The first media chunk to keep, or null to discard all media chunks.
+ */
+ private void discardDownstreamMediaChunks(MediaChunk untilChunk) {
+ if (mediaChunks.isEmpty() || untilChunk == mediaChunks.getFirst()) {
+ return;
+ }
+ long totalBytes = 0;
+ long startTimeUs = mediaChunks.getFirst().startTimeUs;
+ long endTimeUs = 0;
+ while (!mediaChunks.isEmpty() && untilChunk != mediaChunks.getFirst()) {
+ MediaChunk removed = mediaChunks.removeFirst();
+ totalBytes += removed.bytesLoaded();
+ endTimeUs = removed.endTimeUs;
+ removed.release();
+ }
+ notifyDownstreamDiscarded(startTimeUs, endTimeUs, totalBytes);
+ }
+
+ /**
+ * Discards the first downstream media chunk.
+ */
+ private void discardDownstreamMediaChunk() {
+ MediaChunk removed = mediaChunks.removeFirst();
+ long totalBytes = removed.bytesLoaded();
+ removed.release();
+ notifyDownstreamDiscarded(removed.startTimeUs, removed.endTimeUs, totalBytes);
+ }
+
+ /**
+ * Discard upstream media chunks until the queue length is equal to the length specified.
+ *
+ * @param queueLength The desired length of the queue.
+ */
+ private void discardUpstreamMediaChunks(int queueLength) {
+ if (mediaChunks.size() <= queueLength) {
+ return;
+ }
+ long totalBytes = 0;
+ long startTimeUs = 0;
+ long endTimeUs = mediaChunks.getLast().endTimeUs;
+ while (mediaChunks.size() > queueLength) {
+ MediaChunk removed = mediaChunks.removeLast();
+ totalBytes += removed.bytesLoaded();
+ startTimeUs = removed.startTimeUs;
+ removed.release();
+ }
+ notifyUpstreamDiscarded(startTimeUs, endTimeUs, totalBytes);
+ }
+
+ private boolean isMediaChunk(Chunk chunk) {
+ return chunk instanceof MediaChunk;
+ }
+
+ private boolean isPendingReset() {
+ return pendingResetTime != NO_RESET_PENDING;
+ }
+
+ private long getRetryDelayMillis(long errorCount) {
+ return Math.min((errorCount - 1) * 1000, 5000);
+ }
+
+ protected final int usToMs(long timeUs) {
+ return (int) (timeUs / 1000);
+ }
+
+ private void notifyLoadStarted(final int formatId, final int trigger,
+ final boolean isInitialization, final long mediaStartTimeUs, final long mediaEndTimeUs,
+ final long totalBytes) {
+ if (eventHandler != null && eventListener != null) {
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ eventListener.onLoadStarted(eventSourceId, formatId, trigger, isInitialization,
+ usToMs(mediaStartTimeUs), usToMs(mediaEndTimeUs), totalBytes);
+ }
+ });
+ }
+ }
+
+ private void notifyLoadCompleted() {
+ if (eventHandler != null && eventListener != null) {
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ eventListener.onLoadCompleted(eventSourceId);
+ }
+ });
+ }
+ }
+
+ private void notifyLoadCanceled() {
+ if (eventHandler != null && eventListener != null) {
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ eventListener.onLoadCanceled(eventSourceId);
+ }
+ });
+ }
+ }
+
+ private void notifyUpstreamError(final IOException e) {
+ if (eventHandler != null && eventListener != null) {
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ eventListener.onUpstreamError(eventSourceId, e);
+ }
+ });
+ }
+ }
+
+ private void notifyConsumptionError(final IOException e) {
+ if (eventHandler != null && eventListener != null) {
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ eventListener.onConsumptionError(eventSourceId, e);
+ }
+ });
+ }
+ }
+
+ private void notifyUpstreamDiscarded(final long mediaStartTimeUs, final long mediaEndTimeUs,
+ final long totalBytes) {
+ if (eventHandler != null && eventListener != null) {
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ eventListener.onUpstreamDiscarded(eventSourceId, usToMs(mediaStartTimeUs),
+ usToMs(mediaEndTimeUs), totalBytes);
+ }
+ });
+ }
+ }
+
+ private void notifyDownstreamFormatChanged(final int formatId, final int trigger,
+ final long mediaTimeUs) {
+ if (eventHandler != null && eventListener != null) {
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ eventListener.onDownstreamFormatChanged(eventSourceId, formatId, trigger,
+ usToMs(mediaTimeUs));
+ }
+ });
+ }
+ }
+
+ private void notifyDownstreamDiscarded(final long mediaStartTimeUs, final long mediaEndTimeUs,
+ final long totalBytes) {
+ if (eventHandler != null && eventListener != null) {
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ eventListener.onDownstreamDiscarded(eventSourceId, usToMs(mediaStartTimeUs),
+ usToMs(mediaEndTimeUs), totalBytes);
+ }
+ });
+ }
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSource.java
new file mode 100644
index 00000000000..a68d81fab8d
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSource.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer.chunk;
+
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.TrackInfo;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * A provider of {@link Chunk}s for a {@link ChunkSampleSource} to load.
+ */
+/*
+ * TODO: Share more state between this interface and {@link ChunkSampleSource}. In particular
+ * implementations of this class needs to know about errors, and should be more tightly integrated
+ * into the process of resuming loading of a chunk after an error occurs.
+ */
+public interface ChunkSource {
+
+ /**
+ * Gets information about the track for which this instance provides {@link Chunk}s.
+ *
+ * May be called when the source is disabled or enabled.
+ *
+ * @return Information about the track.
+ */
+ TrackInfo getTrackInfo();
+
+ /**
+ * Adaptive video {@link ChunkSource} implementations must set the maximum video dimensions on
+ * the supplied {@link MediaFormat}. Other implementations do nothing.
+ *
+ * Only called when the source is enabled.
+ */
+ void getMaxVideoDimensions(MediaFormat out);
+
+ /**
+ * Called when the source is enabled.
+ */
+ void enable();
+
+ /**
+ * Called when the source is disabled.
+ *
+ * @param queue A representation of the currently buffered {@link MediaChunk}s.
+ */
+ void disable(List queue);
+
+ /**
+ * Indicates to the source that it should still be checking for updates to the stream.
+ *
+ * @param playbackPositionUs The current playback position.
+ */
+ void continueBuffering(long playbackPositionUs);
+
+ /**
+ * Updates the provided {@link ChunkOperationHolder} to contain the next operation that should
+ * be performed by the calling {@link ChunkSampleSource}.
+ *
+ * The next operation comprises of a possibly shortened queue length (shortened if the
+ * implementation wishes for the caller to discard {@link MediaChunk}s from the queue), together
+ * with the next {@link Chunk} to load. The next chunk may be a {@link MediaChunk} to be added to
+ * the queue, or another {@link Chunk} type (e.g. to load initialization data), or null if the
+ * source is not able to provide a chunk in its current state.
+ *
+ * @param queue A representation of the currently buffered {@link MediaChunk}s.
+ * @param seekPositionUs If the queue is empty, this parameter must specify the seek position. If
+ * the queue is non-empty then this parameter is ignored.
+ * @param playbackPositionUs The current playback position.
+ * @param out A holder for the next operation, whose {@link ChunkOperationHolder#queueSize} is
+ * initially equal to the length of the queue, and whose {@link ChunkOperationHolder#chunk} is
+ * initially equal to null or a {@link Chunk} previously supplied by the {@link ChunkSource}
+ * that the caller has not yet finished loading. In the latter case the chunk can either be
+ * replaced or left unchanged. Note that leaving the chunk unchanged is both preferred and
+ * more efficient than replacing it with a new but identical chunk.
+ */
+ void getChunkOperation(List extends MediaChunk> queue, long seekPositionUs,
+ long playbackPositionUs, ChunkOperationHolder out);
+
+ /**
+ * If the {@link ChunkSource} is currently unable to provide chunks through
+ * {@link ChunkSource#getChunkOperation}, then this method returns the underlying cause. Returns
+ * null otherwise.
+ *
+ * @return An {@link IOException}, or null.
+ */
+ IOException getError();
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/Format.java b/library/src/main/java/com/google/android/exoplayer/chunk/Format.java
new file mode 100644
index 00000000000..d7d301404dc
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/chunk/Format.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer.chunk;
+
+import java.util.Comparator;
+
+/**
+ * A format definition for streams.
+ */
+public final class Format {
+
+ /**
+ * Sorts {@link Format} objects in order of decreasing bandwidth.
+ */
+ public static final class DecreasingBandwidthComparator implements Comparator {
+
+ @Override
+ public int compare(Format a, Format b) {
+ return b.bandwidth - a.bandwidth;
+ }
+
+ }
+
+ /**
+ * An identifier for the format.
+ */
+ public final int id;
+
+ /**
+ * The mime type of the format.
+ */
+ public final String mimeType;
+
+ /**
+ * The width of the video in pixels, or -1 for non-video formats.
+ */
+ public final int width;
+
+ /**
+ * The height of the video in pixels, or -1 for non-video formats.
+ */
+ public final int height;
+
+ /**
+ * The number of audio channels, or -1 for non-audio formats.
+ */
+ public final int numChannels;
+
+ /**
+ * The audio sampling rate in Hz, or -1 for non-audio formats.
+ */
+ public final int audioSamplingRate;
+
+ /**
+ * The average bandwidth in bytes per second.
+ */
+ public final int bandwidth;
+
+ /**
+ * @param id The format identifier.
+ * @param mimeType The format mime type.
+ * @param width The width 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 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.
+ */
+ public Format(int id, String mimeType, int width, int height, int numChannels,
+ int audioSamplingRate, int bandwidth) {
+ this.id = id;
+ this.mimeType = mimeType;
+ this.width = width;
+ this.height = height;
+ this.numChannels = numChannels;
+ this.audioSamplingRate = audioSamplingRate;
+ this.bandwidth = bandwidth;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/FormatEvaluator.java b/library/src/main/java/com/google/android/exoplayer/chunk/FormatEvaluator.java
new file mode 100644
index 00000000000..7998c5ebde3
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/chunk/FormatEvaluator.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer.chunk;
+
+import com.google.android.exoplayer.upstream.BandwidthMeter;
+
+import java.util.List;
+import java.util.Random;
+
+/**
+ * Selects from a number of available formats during playback.
+ */
+public interface FormatEvaluator {
+
+ /**
+ * The trigger for the initial format selection.
+ */
+ static final int TRIGGER_INITIAL = 0;
+ /**
+ * The trigger for a format selection that was triggered by the user.
+ */
+ static final int TRIGGER_MANUAL = 1;
+ /**
+ * The trigger for an adaptive format selection.
+ */
+ static final int TRIGGER_ADAPTIVE = 2;
+ /**
+ * Implementations may define custom trigger codes greater than or equal to this value.
+ */
+ static final int TRIGGER_CUSTOM_BASE = 10000;
+
+ /**
+ * Enables the evaluator.
+ */
+ void enable();
+
+ /**
+ * Disables the evaluator.
+ */
+ void disable();
+
+ /**
+ * Update the supplied evaluation.
+ *
+ * When the method is invoked, {@code evaluation} will contain the currently selected
+ * format (null for the first evaluation), the most recent trigger (TRIGGER_INITIAL for the
+ * first evaluation) and the current queue size. The implementation should update these
+ * fields as necessary.
+ *
+ * The trigger should be considered "sticky" for as long as a given representation is selected,
+ * and so should only be changed if the representation is also changed.
+ *
+ * @param queue A read only representation of the currently buffered {@link MediaChunk}s.
+ * @param playbackPositionUs The current playback position.
+ * @param formats The formats from which to select, ordered by decreasing bandwidth.
+ * @param evaluation The evaluation.
+ */
+ // TODO: Pass more useful information into this method, and finalize the interface.
+ void evaluate(List extends MediaChunk> queue, long playbackPositionUs, Format[] formats,
+ Evaluation evaluation);
+
+ /**
+ * A format evaluation.
+ */
+ public static final class Evaluation {
+
+ /**
+ * The desired size of the queue.
+ */
+ public int queueSize;
+
+ /**
+ * The sticky reason for the format selection.
+ */
+ public int trigger;
+
+ /**
+ * The selected format.
+ */
+ public Format format;
+
+ public Evaluation() {
+ trigger = TRIGGER_INITIAL;
+ }
+
+ }
+
+ /**
+ * Always selects the first format.
+ */
+ public static class FixedEvaluator implements FormatEvaluator {
+
+ @Override
+ public void enable() {
+ // Do nothing.
+ }
+
+ @Override
+ public void disable() {
+ // Do nothing.
+ }
+
+ @Override
+ public void evaluate(List extends MediaChunk> queue, long playbackPositionUs,
+ Format[] formats, Evaluation evaluation) {
+ evaluation.format = formats[0];
+ }
+
+ }
+
+ /**
+ * Selects randomly between the available formats.
+ */
+ public static class RandomEvaluator implements FormatEvaluator {
+
+ private final Random random;
+
+ public RandomEvaluator() {
+ this.random = new Random();
+ }
+
+ @Override
+ public void enable() {
+ // Do nothing.
+ }
+
+ @Override
+ public void disable() {
+ // Do nothing.
+ }
+
+ @Override
+ public void evaluate(List extends MediaChunk> queue, long playbackPositionUs,
+ Format[] formats, Evaluation evaluation) {
+ Format newFormat = formats[random.nextInt(formats.length)];
+ if (evaluation.format != null && evaluation.format.id != newFormat.id) {
+ evaluation.trigger = TRIGGER_ADAPTIVE;
+ }
+ evaluation.format = newFormat;
+ }
+
+ }
+
+ /**
+ * An adaptive evaluator for video formats, which attempts to select the best quality possible
+ * given the current network conditions and state of the buffer.
+ *
+ * This implementation should be used for video only, and should not be used for audio. It is a
+ * reference implementation only. It is recommended that application developers implement their
+ * own adaptive evaluator to more precisely suit their use case.
+ */
+ public static class AdaptiveEvaluator implements FormatEvaluator {
+
+ public static final int DEFAULT_MAX_INITIAL_BYTE_RATE = 100000;
+
+ public static final int DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS = 10000;
+ public static final int DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS = 25000;
+ public static final int DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS = 25000;
+ public static final float DEFAULT_BANDWIDTH_FRACTION = 0.75f;
+
+ private final BandwidthMeter bandwidthMeter;
+
+ private final int maxInitialByteRate;
+ private final long minDurationForQualityIncreaseUs;
+ private final long maxDurationForQualityDecreaseUs;
+ private final long minDurationToRetainAfterDiscardUs;
+ private final float bandwidthFraction;
+
+ /**
+ * @param bandwidthMeter Provides an estimate of the currently available bandwidth.
+ */
+ public AdaptiveEvaluator(BandwidthMeter bandwidthMeter) {
+ this (bandwidthMeter, DEFAULT_MAX_INITIAL_BYTE_RATE,
+ DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,
+ DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
+ DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, DEFAULT_BANDWIDTH_FRACTION);
+ }
+
+ /**
+ * @param bandwidthMeter Provides an estimate of the currently available bandwidth.
+ * @param maxInitialByteRate The maximum bandwidth in bytes per second that should be assumed
+ * when bandwidthMeter cannot provide an estimate due to playback having only just started.
+ * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for
+ * the evaluator to consider switching to a higher quality format.
+ * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for
+ * the evaluator to consider switching to a lower quality format.
+ * @param minDurationToRetainAfterDiscardMs When switching to a significantly higher quality
+ * format, the evaluator may discard some of the media that it has already buffered at the
+ * lower quality, so as to switch up to the higher quality faster. This is the minimum
+ * duration of media that must be retained at the lower quality.
+ * @param bandwidthFraction The fraction of the available bandwidth that the evaluator should
+ * consider available for use. Setting to a value less than 1 is recommended to account
+ * for inaccuracies in the bandwidth estimator.
+ */
+ public AdaptiveEvaluator(BandwidthMeter bandwidthMeter,
+ int maxInitialByteRate,
+ int minDurationForQualityIncreaseMs,
+ int maxDurationForQualityDecreaseMs,
+ int minDurationToRetainAfterDiscardMs,
+ float bandwidthFraction) {
+ this.bandwidthMeter = bandwidthMeter;
+ this.maxInitialByteRate = maxInitialByteRate;
+ this.minDurationForQualityIncreaseUs = minDurationForQualityIncreaseMs * 1000L;
+ this.maxDurationForQualityDecreaseUs = maxDurationForQualityDecreaseMs * 1000L;
+ this.minDurationToRetainAfterDiscardUs = minDurationToRetainAfterDiscardMs * 1000L;
+ this.bandwidthFraction = bandwidthFraction;
+ }
+
+ @Override
+ public void enable() {
+ // Do nothing.
+ }
+
+ @Override
+ public void disable() {
+ // Do nothing.
+ }
+
+ @Override
+ public void evaluate(List extends MediaChunk> queue, long playbackPositionUs,
+ Format[] formats, Evaluation evaluation) {
+ long bufferedDurationUs = queue.isEmpty() ? 0
+ : queue.get(queue.size() - 1).endTimeUs - playbackPositionUs;
+ Format current = evaluation.format;
+ Format ideal = determineIdealFormat(formats, bandwidthMeter.getEstimate());
+ boolean isHigher = ideal != null && current != null && ideal.bandwidth > current.bandwidth;
+ boolean isLower = ideal != null && current != null && ideal.bandwidth < current.bandwidth;
+ if (isHigher) {
+ if (bufferedDurationUs < minDurationForQualityIncreaseUs) {
+ // The ideal format is a higher quality, but we have insufficient buffer to
+ // safely switch up. Defer switching up for now.
+ ideal = current;
+ } else if (bufferedDurationUs >= minDurationToRetainAfterDiscardUs) {
+ // We're switching from an SD stream to a stream of higher resolution. Consider
+ // discarding already buffered media chunks. Specifically, discard media chunks starting
+ // from the first one that is of lower bandwidth, lower resolution and that is not HD.
+ for (int i = 0; i < queue.size(); i++) {
+ MediaChunk thisChunk = queue.get(i);
+ long durationBeforeThisSegmentUs = thisChunk.startTimeUs - playbackPositionUs;
+ if (durationBeforeThisSegmentUs >= minDurationToRetainAfterDiscardUs
+ && thisChunk.format.bandwidth < ideal.bandwidth
+ && thisChunk.format.height < ideal.height
+ && thisChunk.format.height < 720
+ && thisChunk.format.width < 1280) {
+ // Discard chunks from this one onwards.
+ evaluation.queueSize = i;
+ break;
+ }
+ }
+ }
+ } else if (isLower && current != null
+ && bufferedDurationUs >= maxDurationForQualityDecreaseUs) {
+ // The ideal format is a lower quality, but we have sufficient buffer to defer switching
+ // down for now.
+ ideal = current;
+ }
+ if (current != null && ideal != current) {
+ evaluation.trigger = FormatEvaluator.TRIGGER_ADAPTIVE;
+ }
+ evaluation.format = ideal;
+ }
+
+ /**
+ * Compute the ideal format ignoring buffer health.
+ */
+ protected Format determineIdealFormat(Format[] formats, long bandwidthEstimate) {
+ long effectiveBandwidth = computeEffectiveBandwidthEstimate(bandwidthEstimate);
+ for (int i = 0; i < formats.length; i++) {
+ Format format = formats[i];
+ if (format.bandwidth <= effectiveBandwidth) {
+ return format;
+ }
+ }
+ // We didn't manage to calculate a suitable format. Return the lowest quality format.
+ return formats[formats.length - 1];
+ }
+
+ /**
+ * Apply overhead factor, or default value in absence of estimate.
+ */
+ protected long computeEffectiveBandwidthEstimate(long bandwidthEstimate) {
+ return bandwidthEstimate == BandwidthMeter.NO_ESTIMATE
+ ? maxInitialByteRate : (long) (bandwidthEstimate * bandwidthFraction);
+ }
+
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/MediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/MediaChunk.java
new file mode 100644
index 00000000000..ad22645be65
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/chunk/MediaChunk.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer.chunk;
+
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.ParserException;
+import com.google.android.exoplayer.SampleHolder;
+import com.google.android.exoplayer.upstream.DataSource;
+import com.google.android.exoplayer.upstream.DataSpec;
+
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * An abstract base class for {@link Chunk}s that contain media samples.
+ */
+public abstract class MediaChunk extends Chunk {
+
+ /**
+ * The start time of the media contained by the chunk.
+ */
+ public final long startTimeUs;
+ /**
+ * The end time of the media contained by the chunk.
+ */
+ public final long endTimeUs;
+ /**
+ * The index of the next media chunk, or -1 if this is the last media chunk in the stream.
+ */
+ public final int nextChunkIndex;
+
+ /**
+ * Constructor for a chunk of media samples.
+ *
+ * @param dataSource A {@link DataSource} for loading the data.
+ * @param dataSpec Defines the data to be loaded.
+ * @param format The format of the stream to which this chunk belongs.
+ * @param trigger The reason for this chunk being selected.
+ * @param startTimeUs The start time of the media contained by the chunk, in microseconds.
+ * @param endTimeUs The end time of the media contained by the chunk, in microseconds.
+ * @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
+ */
+ public MediaChunk(DataSource dataSource, DataSpec dataSpec, Format format, int trigger,
+ long startTimeUs, long endTimeUs, int nextChunkIndex) {
+ super(dataSource, dataSpec, format, trigger);
+ this.startTimeUs = startTimeUs;
+ this.endTimeUs = endTimeUs;
+ this.nextChunkIndex = nextChunkIndex;
+ }
+
+ /**
+ * Whether this is the last chunk in the stream.
+ *
+ * @return True if this is the last chunk in the stream. False otherwise.
+ */
+ public final boolean isLastChunk() {
+ return nextChunkIndex == -1;
+ }
+
+ /**
+ * Seeks to the beginning of the chunk.
+ */
+ public final void seekToStart() {
+ seekTo(startTimeUs, false);
+ }
+
+ /**
+ * Seeks to the specified position within the chunk.
+ *
+ * @param positionUs The desired seek time in microseconds.
+ * @param allowNoop True if the seek is allowed to do nothing if the result is more accurate than
+ * seeking to a key frame. Always pass false if it is required that the next sample be a key
+ * frame.
+ * @return True if the seek results in a discontinuity in the sequence of samples returned by
+ * {@link #read(SampleHolder)}. False otherwise.
+ */
+ public abstract boolean seekTo(long positionUs, boolean allowNoop);
+
+ /**
+ * Reads the next media sample from the chunk.
+ *
+ * @param holder A holder to store the read sample.
+ * @return True if a sample was read. False if more data is still required.
+ * @throws ParserException If an error occurs parsing the media data.
+ * @throws IllegalStateException If called before {@link #init}, or after {@link #release}
+ */
+ public abstract boolean read(SampleHolder holder) throws ParserException;
+
+ /**
+ * Returns the media format of the samples contained within this chunk.
+ *
+ * @return The sample media format.
+ */
+ public abstract MediaFormat getMediaFormat();
+
+ /**
+ * Returns the pssh information associated with the chunk.
+ *
+ * @return The pssh information.
+ */
+ public abstract Map getPsshInfo();
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/Mp4MediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/Mp4MediaChunk.java
new file mode 100644
index 00000000000..0b1e22b6437
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/chunk/Mp4MediaChunk.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer.chunk;
+
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.ParserException;
+import com.google.android.exoplayer.SampleHolder;
+import com.google.android.exoplayer.parser.mp4.FragmentedMp4Extractor;
+import com.google.android.exoplayer.upstream.DataSource;
+import com.google.android.exoplayer.upstream.DataSpec;
+import com.google.android.exoplayer.upstream.NonBlockingInputStream;
+import com.google.android.exoplayer.util.Assertions;
+
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * An Mp4 {@link MediaChunk}.
+ */
+public final class Mp4MediaChunk extends MediaChunk {
+
+ private final FragmentedMp4Extractor extractor;
+ private final long sampleOffsetUs;
+
+ /**
+ * @param dataSource A {@link DataSource} for loading the data.
+ * @param dataSpec Defines the data to be loaded.
+ * @param format The format of the stream to which this chunk belongs.
+ * @param extractor The extractor that will be used to extract the samples.
+ * @param trigger The reason for this chunk being selected.
+ * @param startTimeUs The start time of the media contained by the chunk, in microseconds.
+ * @param endTimeUs The end time of the media contained by the chunk, in microseconds.
+ * @param sampleOffsetUs An offset to subtract from the sample timestamps parsed by the extractor.
+ * @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
+ */
+ public Mp4MediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
+ int trigger, FragmentedMp4Extractor extractor, long startTimeUs, long endTimeUs,
+ long sampleOffsetUs, int nextChunkIndex) {
+ super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex);
+ this.extractor = extractor;
+ this.sampleOffsetUs = sampleOffsetUs;
+ }
+
+ @Override
+ public boolean seekTo(long positionUs, boolean allowNoop) {
+ long seekTimeUs = positionUs + sampleOffsetUs;
+ boolean isDiscontinuous = extractor.seekTo(seekTimeUs, allowNoop);
+ if (isDiscontinuous) {
+ resetReadPosition();
+ }
+ return isDiscontinuous;
+ }
+
+ @Override
+ public boolean read(SampleHolder holder) throws ParserException {
+ NonBlockingInputStream inputStream = getNonBlockingInputStream();
+ Assertions.checkState(inputStream != null);
+ int result = extractor.read(inputStream, holder);
+ boolean sampleRead = (result & FragmentedMp4Extractor.RESULT_READ_SAMPLE_FULL) != 0;
+ if (sampleRead) {
+ holder.timeUs -= sampleOffsetUs;
+ }
+ return sampleRead;
+ }
+
+ @Override
+ public MediaFormat getMediaFormat() {
+ return extractor.getFormat();
+ }
+
+ @Override
+ public Map getPsshInfo() {
+ return extractor.getPsshInfo();
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/MultiTrackChunkSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/MultiTrackChunkSource.java
new file mode 100644
index 00000000000..98721cce212
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/chunk/MultiTrackChunkSource.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer.chunk;
+
+import com.google.android.exoplayer.ExoPlaybackException;
+import com.google.android.exoplayer.ExoPlayer.ExoPlayerComponent;
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.TrackInfo;
+import com.google.android.exoplayer.util.Assertions;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * A {@link ChunkSource} providing the ability to switch between multiple other {@link ChunkSource}
+ * instances.
+ */
+public class MultiTrackChunkSource implements ChunkSource, ExoPlayerComponent {
+
+ /**
+ * A message to indicate a source selection. Source selection can only be performed when the
+ * source is disabled.
+ */
+ public static final int MSG_SELECT_TRACK = 1;
+
+ private final ChunkSource[] allSources;
+
+ private ChunkSource selectedSource;
+ private boolean enabled;
+
+ public MultiTrackChunkSource(ChunkSource... sources) {
+ this.allSources = sources;
+ this.selectedSource = sources[0];
+ }
+
+ /**
+ * Gets the number of tracks that this source can switch between. May be called safely from any
+ * thread.
+ *
+ * @return The number of tracks.
+ */
+ public int getTrackCount() {
+ return allSources.length;
+ }
+
+ @Override
+ public TrackInfo getTrackInfo() {
+ return selectedSource.getTrackInfo();
+ }
+
+ @Override
+ public void enable() {
+ selectedSource.enable();
+ enabled = true;
+ }
+
+ @Override
+ public void disable(List queue) {
+ selectedSource.disable(queue);
+ enabled = false;
+ }
+
+ @Override
+ public void continueBuffering(long playbackPositionUs) {
+ selectedSource.continueBuffering(playbackPositionUs);
+ }
+
+ @Override
+ public void getChunkOperation(List extends MediaChunk> queue, long seekPositionUs,
+ long playbackPositionUs, ChunkOperationHolder out) {
+ selectedSource.getChunkOperation(queue, seekPositionUs, playbackPositionUs, out);
+ }
+
+ @Override
+ public IOException getError() {
+ return null;
+ }
+
+ @Override
+ public void getMaxVideoDimensions(MediaFormat out) {
+ selectedSource.getMaxVideoDimensions(out);
+ }
+
+ @Override
+ public void handleMessage(int what, Object msg) throws ExoPlaybackException {
+ Assertions.checkState(!enabled);
+ if (what == MSG_SELECT_TRACK) {
+ selectedSource = allSources[(Integer) msg];
+ }
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleMediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleMediaChunk.java
new file mode 100644
index 00000000000..893e28b5076
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleMediaChunk.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer.chunk;
+
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.SampleHolder;
+import com.google.android.exoplayer.upstream.DataSource;
+import com.google.android.exoplayer.upstream.DataSpec;
+import com.google.android.exoplayer.upstream.NonBlockingInputStream;
+import com.google.android.exoplayer.util.Assertions;
+
+import java.nio.ByteBuffer;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * A {@link MediaChunk} containing a single sample.
+ */
+public class SingleSampleMediaChunk extends MediaChunk {
+
+ /**
+ * The sample header data. May be null.
+ */
+ public final byte[] headerData;
+
+ private final MediaFormat sampleFormat;
+
+ /**
+ * @param dataSource A {@link DataSource} for loading the data.
+ * @param dataSpec Defines the data to be loaded.
+ * @param format The format of the stream to which this chunk belongs.
+ * @param trigger The reason for this chunk being selected.
+ * @param startTimeUs The start time of the media contained by the chunk, in microseconds.
+ * @param endTimeUs The end time of the media contained by the chunk, in microseconds.
+ * @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
+ * @param sampleFormat The format of the media contained by the chunk.
+ */
+ public SingleSampleMediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
+ int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex, MediaFormat sampleFormat) {
+ this(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex,
+ sampleFormat, null);
+ }
+
+ /**
+ * @param dataSource A {@link DataSource} for loading the data.
+ * @param dataSpec Defines the data to be loaded.
+ * @param format The format of the stream to which this chunk belongs.
+ * @param trigger The reason for this chunk being selected.
+ * @param startTimeUs The start time of the media contained by the chunk, in microseconds.
+ * @param endTimeUs The end time of the media contained by the chunk, in microseconds.
+ * @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
+ * @param sampleFormat The format of the media contained by the chunk.
+ * @param headerData Custom header data for the sample. May be null. If set, the header data is
+ * prepended to the sample data returned when {@link #read(SampleHolder)} is called. It is
+ * however not considered part of the loaded data, and so is not prepended to the data
+ * returned by {@link #getLoadedData()}. It is also not reflected in the values returned by
+ * {@link #bytesLoaded()} and {@link #getLength()}.
+ */
+ public SingleSampleMediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
+ int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex, MediaFormat sampleFormat,
+ byte[] headerData) {
+ super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex);
+ this.sampleFormat = sampleFormat;
+ this.headerData = headerData;
+ }
+
+ @Override
+ public boolean read(SampleHolder holder) {
+ NonBlockingInputStream inputStream = getNonBlockingInputStream();
+ Assertions.checkState(inputStream != null);
+ if (!isLoadFinished()) {
+ return false;
+ }
+ int bytesLoaded = (int) bytesLoaded();
+ int sampleSize = bytesLoaded;
+ if (headerData != null) {
+ sampleSize += headerData.length;
+ }
+ if (holder.allowDataBufferReplacement &&
+ (holder.data == null || holder.data.capacity() < sampleSize)) {
+ holder.data = ByteBuffer.allocate(sampleSize);
+ }
+ int bytesRead;
+ if (holder.data != null) {
+ if (headerData != null) {
+ holder.data.put(headerData);
+ }
+ bytesRead = inputStream.read(holder.data, bytesLoaded);
+ holder.size = sampleSize;
+ } else {
+ bytesRead = inputStream.skip(bytesLoaded);
+ holder.size = 0;
+ }
+ Assertions.checkState(bytesRead == bytesLoaded);
+ holder.timeUs = startTimeUs;
+ return true;
+ }
+
+ @Override
+ public boolean seekTo(long positionUs, boolean allowNoop) {
+ resetReadPosition();
+ return true;
+ }
+
+ @Override
+ public MediaFormat getMediaFormat() {
+ return sampleFormat;
+ }
+
+ @Override
+ public Map getPsshInfo() {
+ return null;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/WebmMediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/WebmMediaChunk.java
new file mode 100644
index 00000000000..1c86c23865a
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/chunk/WebmMediaChunk.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer.chunk;
+
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.SampleHolder;
+import com.google.android.exoplayer.parser.webm.WebmExtractor;
+import com.google.android.exoplayer.upstream.DataSource;
+import com.google.android.exoplayer.upstream.DataSpec;
+import com.google.android.exoplayer.upstream.NonBlockingInputStream;
+import com.google.android.exoplayer.util.Assertions;
+
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * A WebM {@link MediaChunk}.
+ */
+public final class WebmMediaChunk extends MediaChunk {
+
+ private final WebmExtractor extractor;
+
+ /**
+ * @param dataSource A {@link DataSource} for loading the data.
+ * @param dataSpec Defines the data to be loaded.
+ * @param format The format of the stream to which this chunk belongs.
+ * @param extractor The extractor that will be used to extract the samples.
+ * @param trigger The reason for this chunk being selected.
+ * @param startTimeUs The start time of the media contained by the chunk, in microseconds.
+ * @param endTimeUs The end time of the media contained by the chunk, in microseconds.
+ * @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
+ */
+ public WebmMediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
+ int trigger, WebmExtractor extractor, long startTimeUs, long endTimeUs,
+ int nextChunkIndex) {
+ super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex);
+ this.extractor = extractor;
+ }
+
+ @Override
+ public boolean seekTo(long positionUs, boolean allowNoop) {
+ boolean isDiscontinuous = extractor.seekTo(positionUs, allowNoop);
+ if (isDiscontinuous) {
+ resetReadPosition();
+ }
+ return isDiscontinuous;
+ }
+
+ @Override
+ public boolean read(SampleHolder holder) {
+ NonBlockingInputStream inputStream = getNonBlockingInputStream();
+ Assertions.checkState(inputStream != null);
+ return extractor.read(inputStream, holder);
+ }
+
+ @Override
+ public MediaFormat getMediaFormat() {
+ return extractor.getFormat();
+ }
+
+ @Override
+ public Map getPsshInfo() {
+ // TODO: Add support for Pssh to WebmExtractor
+ return null;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashMp4ChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashMp4ChunkSource.java
new file mode 100644
index 00000000000..0bea6f09a8b
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/dash/DashMp4ChunkSource.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer.dash;
+
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.ParserException;
+import com.google.android.exoplayer.TrackInfo;
+import com.google.android.exoplayer.chunk.Chunk;
+import com.google.android.exoplayer.chunk.ChunkOperationHolder;
+import com.google.android.exoplayer.chunk.ChunkSource;
+import com.google.android.exoplayer.chunk.Format;
+import com.google.android.exoplayer.chunk.Format.DecreasingBandwidthComparator;
+import com.google.android.exoplayer.chunk.FormatEvaluator;
+import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation;
+import com.google.android.exoplayer.chunk.MediaChunk;
+import com.google.android.exoplayer.chunk.Mp4MediaChunk;
+import com.google.android.exoplayer.dash.mpd.Representation;
+import com.google.android.exoplayer.parser.SegmentIndex;
+import com.google.android.exoplayer.parser.mp4.FragmentedMp4Extractor;
+import com.google.android.exoplayer.upstream.DataSource;
+import com.google.android.exoplayer.upstream.DataSpec;
+import com.google.android.exoplayer.upstream.NonBlockingInputStream;
+
+import android.util.Log;
+import android.util.SparseArray;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * An {@link ChunkSource} for Mp4 DASH streams.
+ */
+public class DashMp4ChunkSource implements ChunkSource {
+
+ public static final int DEFAULT_NUM_SEGMENTS_PER_CHUNK = 1;
+
+ private static final int EXPECTED_INITIALIZATION_RESULT =
+ FragmentedMp4Extractor.RESULT_END_OF_STREAM
+ | FragmentedMp4Extractor.RESULT_READ_MOOV
+ | FragmentedMp4Extractor.RESULT_READ_SIDX;
+
+ private static final String TAG = "DashMp4ChunkSource";
+
+ private final TrackInfo trackInfo;
+ private final DataSource dataSource;
+ private final FormatEvaluator evaluator;
+ private final Evaluation evaluation;
+ private final int maxWidth;
+ private final int maxHeight;
+ private final int numSegmentsPerChunk;
+
+ private final Format[] formats;
+ private final SparseArray representations;
+ private final SparseArray extractors;
+
+ private boolean lastChunkWasInitialization;
+
+ /**
+ * @param dataSource A {@link DataSource} suitable for loading the media data.
+ * @param evaluator Selects from the available formats.
+ * @param representations The representations to be considered by the source.
+ */
+ public DashMp4ChunkSource(DataSource dataSource, FormatEvaluator evaluator,
+ Representation... representations) {
+ this(dataSource, evaluator, DEFAULT_NUM_SEGMENTS_PER_CHUNK, representations);
+ }
+
+ /**
+ * @param dataSource A {@link DataSource} suitable for loading the media data.
+ * @param evaluator Selects from the available formats.
+ * @param numSegmentsPerChunk The number of segments (as defined in the stream's segment index)
+ * that should be grouped into a single chunk.
+ * @param representations The representations to be considered by the source.
+ */
+ public DashMp4ChunkSource(DataSource dataSource, FormatEvaluator evaluator,
+ int numSegmentsPerChunk, Representation... representations) {
+ this.dataSource = dataSource;
+ this.evaluator = evaluator;
+ this.numSegmentsPerChunk = numSegmentsPerChunk;
+ this.formats = new Format[representations.length];
+ this.extractors = new SparseArray();
+ this.representations = new SparseArray();
+ this.trackInfo = new TrackInfo(representations[0].format.mimeType,
+ representations[0].periodDuration * 1000);
+ this.evaluation = new Evaluation();
+ int maxWidth = 0;
+ int maxHeight = 0;
+ for (int i = 0; i < representations.length; i++) {
+ formats[i] = representations[i].format;
+ maxWidth = Math.max(formats[i].width, maxWidth);
+ maxHeight = Math.max(formats[i].height, maxHeight);
+ extractors.append(formats[i].id, new FragmentedMp4Extractor());
+ this.representations.put(formats[i].id, representations[i]);
+ }
+ this.maxWidth = maxWidth;
+ this.maxHeight = maxHeight;
+ Arrays.sort(formats, new DecreasingBandwidthComparator());
+ }
+
+ @Override
+ public final void getMaxVideoDimensions(MediaFormat out) {
+ if (trackInfo.mimeType.startsWith("video")) {
+ out.setMaxVideoDimensions(maxWidth, maxHeight);
+ }
+ }
+
+ @Override
+ public final TrackInfo getTrackInfo() {
+ return trackInfo;
+ }
+
+ @Override
+ public void enable() {
+ evaluator.enable();
+ }
+
+ @Override
+ public void disable(List queue) {
+ evaluator.disable();
+ }
+
+ @Override
+ public void continueBuffering(long playbackPositionUs) {
+ // Do nothing
+ }
+
+ @Override
+ public final void getChunkOperation(List extends MediaChunk> queue, long seekPositionUs,
+ long playbackPositionUs, ChunkOperationHolder out) {
+ evaluation.queueSize = queue.size();
+ if (evaluation.format == null || !lastChunkWasInitialization) {
+ evaluator.evaluate(queue, playbackPositionUs, formats, evaluation);
+ }
+ Format selectedFormat = evaluation.format;
+ out.queueSize = evaluation.queueSize;
+
+ if (selectedFormat == null) {
+ out.chunk = null;
+ return;
+ } else if (out.queueSize == queue.size() && out.chunk != null
+ && out.chunk.format.id == selectedFormat.id) {
+ // We already have a chunk, and the evaluation hasn't changed either the format or the size
+ // of the queue. Leave unchanged.
+ return;
+ }
+
+ Representation selectedRepresentation = representations.get(selectedFormat.id);
+ FragmentedMp4Extractor extractor = extractors.get(selectedRepresentation.format.id);
+ if (extractor.getTrack() == null) {
+ Chunk initializationChunk = newInitializationChunk(selectedRepresentation, extractor,
+ dataSource, evaluation.trigger);
+ lastChunkWasInitialization = true;
+ out.chunk = initializationChunk;
+ return;
+ }
+
+ int nextIndex;
+ if (queue.isEmpty()) {
+ nextIndex = Arrays.binarySearch(extractor.getSegmentIndex().timesUs, seekPositionUs);
+ nextIndex = nextIndex < 0 ? -nextIndex - 2 : nextIndex;
+ } else {
+ nextIndex = queue.get(out.queueSize - 1).nextChunkIndex;
+ }
+
+ if (nextIndex == -1) {
+ out.chunk = null;
+ return;
+ }
+
+ Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, extractor, dataSource,
+ extractor.getSegmentIndex(), nextIndex, evaluation.trigger, numSegmentsPerChunk);
+ lastChunkWasInitialization = false;
+ out.chunk = nextMediaChunk;
+ }
+
+ @Override
+ public IOException getError() {
+ return null;
+ }
+
+ private static Chunk newInitializationChunk(Representation representation,
+ FragmentedMp4Extractor extractor, DataSource dataSource, int trigger) {
+ DataSpec dataSpec = new DataSpec(representation.uri, 0, representation.indexEnd + 1,
+ representation.getCacheKey());
+ return new InitializationMp4Loadable(dataSource, dataSpec, trigger, extractor, representation);
+ }
+
+ private static Chunk newMediaChunk(Representation representation,
+ FragmentedMp4Extractor extractor, DataSource dataSource, SegmentIndex sidx, int index,
+ int trigger, int numSegmentsPerChunk) {
+
+ // Computes the segments to included in the next fetch.
+ int numSegmentsToFetch = Math.min(numSegmentsPerChunk, sidx.length - index);
+ int lastSegmentInChunk = index + numSegmentsToFetch - 1;
+ int nextIndex = lastSegmentInChunk == sidx.length - 1 ? -1 : lastSegmentInChunk + 1;
+
+ long startTimeUs = sidx.timesUs[index];
+
+ // Compute the end time, prefer to use next segment start time if there is a next segment.
+ long endTimeUs = nextIndex == -1 ?
+ sidx.timesUs[lastSegmentInChunk] + sidx.durationsUs[lastSegmentInChunk] :
+ sidx.timesUs[nextIndex];
+
+ long offset = (int) representation.indexEnd + 1 + sidx.offsets[index];
+
+ // Compute combined segments byte length.
+ long size = 0;
+ for (int i = index; i <= lastSegmentInChunk; i++) {
+ size += sidx.sizes[i];
+ }
+
+ DataSpec dataSpec = new DataSpec(representation.uri, offset, size,
+ representation.getCacheKey());
+ return new Mp4MediaChunk(dataSource, dataSpec, representation.format, trigger, extractor,
+ startTimeUs, endTimeUs, 0, nextIndex);
+ }
+
+ private static class InitializationMp4Loadable extends Chunk {
+
+ private final Representation representation;
+ private final FragmentedMp4Extractor extractor;
+
+ public InitializationMp4Loadable(DataSource dataSource, DataSpec dataSpec, int trigger,
+ FragmentedMp4Extractor extractor, Representation representation) {
+ super(dataSource, dataSpec, representation.format, trigger);
+ this.extractor = extractor;
+ this.representation = representation;
+ }
+
+ @Override
+ protected void consumeStream(NonBlockingInputStream stream) throws IOException {
+ int result = extractor.read(stream, null);
+ if (result != EXPECTED_INITIALIZATION_RESULT) {
+ throw new ParserException("Invalid initialization data");
+ }
+ validateSegmentIndex(extractor.getSegmentIndex());
+ }
+
+ private void validateSegmentIndex(SegmentIndex segmentIndex) {
+ long expectedIndexLen = representation.indexEnd - representation.indexStart + 1;
+ if (segmentIndex.sizeBytes != expectedIndexLen) {
+ Log.w(TAG, "Sidx length mismatch: sidxLen = " + segmentIndex.sizeBytes +
+ ", ExpectedLen = " + expectedIndexLen);
+ }
+ long sidxContentLength = segmentIndex.offsets[segmentIndex.length - 1] +
+ segmentIndex.sizes[segmentIndex.length - 1] + representation.indexEnd + 1;
+ if (sidxContentLength != representation.contentLength) {
+ Log.w(TAG, "ContentLength mismatch: Actual = " + sidxContentLength +
+ ", Expected = " + representation.contentLength);
+ }
+ }
+
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashWebmChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashWebmChunkSource.java
new file mode 100644
index 00000000000..7f518723e90
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/dash/DashWebmChunkSource.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer.dash;
+
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.ParserException;
+import com.google.android.exoplayer.TrackInfo;
+import com.google.android.exoplayer.chunk.Chunk;
+import com.google.android.exoplayer.chunk.ChunkOperationHolder;
+import com.google.android.exoplayer.chunk.ChunkSource;
+import com.google.android.exoplayer.chunk.Format;
+import com.google.android.exoplayer.chunk.Format.DecreasingBandwidthComparator;
+import com.google.android.exoplayer.chunk.FormatEvaluator;
+import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation;
+import com.google.android.exoplayer.chunk.MediaChunk;
+import com.google.android.exoplayer.chunk.WebmMediaChunk;
+import com.google.android.exoplayer.dash.mpd.Representation;
+import com.google.android.exoplayer.parser.SegmentIndex;
+import com.google.android.exoplayer.parser.webm.WebmExtractor;
+import com.google.android.exoplayer.upstream.DataSource;
+import com.google.android.exoplayer.upstream.DataSpec;
+import com.google.android.exoplayer.upstream.NonBlockingInputStream;
+
+import android.util.Log;
+import android.util.SparseArray;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * An {@link ChunkSource} for WebM DASH streams.
+ */
+public class DashWebmChunkSource implements ChunkSource {
+
+ private static final String TAG = "DashWebmChunkSource";
+
+ private final TrackInfo trackInfo;
+ private final DataSource dataSource;
+ private final FormatEvaluator evaluator;
+ private final Evaluation evaluation;
+ private final int maxWidth;
+ private final int maxHeight;
+ private final int numSegmentsPerChunk;
+
+ private final Format[] formats;
+ private final SparseArray representations;
+ private final SparseArray extractors;
+
+ private boolean lastChunkWasInitialization;
+
+ public DashWebmChunkSource(
+ DataSource dataSource, FormatEvaluator evaluator, Representation... representations) {
+ this(dataSource, evaluator, 1, representations);
+ }
+
+ public DashWebmChunkSource(DataSource dataSource, FormatEvaluator evaluator,
+ int numSegmentsPerChunk, Representation... representations) {
+ this.dataSource = dataSource;
+ this.evaluator = evaluator;
+ this.numSegmentsPerChunk = numSegmentsPerChunk;
+ this.formats = new Format[representations.length];
+ this.extractors = new SparseArray();
+ this.representations = new SparseArray();
+ this.trackInfo = new TrackInfo(
+ representations[0].format.mimeType, representations[0].periodDuration * 1000);
+ this.evaluation = new Evaluation();
+ int maxWidth = 0;
+ int maxHeight = 0;
+ for (int i = 0; i < representations.length; i++) {
+ formats[i] = representations[i].format;
+ maxWidth = Math.max(formats[i].width, maxWidth);
+ maxHeight = Math.max(formats[i].height, maxHeight);
+ extractors.append(formats[i].id, new WebmExtractor());
+ this.representations.put(formats[i].id, representations[i]);
+ }
+ this.maxWidth = maxWidth;
+ this.maxHeight = maxHeight;
+ Arrays.sort(formats, new DecreasingBandwidthComparator());
+ }
+
+ @Override
+ public final void getMaxVideoDimensions(MediaFormat out) {
+ if (trackInfo.mimeType.startsWith("video")) {
+ out.setMaxVideoDimensions(maxWidth, maxHeight);
+ }
+ }
+
+ @Override
+ public final TrackInfo getTrackInfo() {
+ return trackInfo;
+ }
+
+ @Override
+ public void enable() {
+ evaluator.enable();
+ }
+
+ @Override
+ public void disable(List queue) {
+ evaluator.disable();
+ }
+
+ @Override
+ public void continueBuffering(long playbackPositionUs) {
+ // Do nothing
+ }
+
+ @Override
+ public final void getChunkOperation(List extends MediaChunk> queue, long seekPositionUs,
+ long playbackPositionUs, ChunkOperationHolder out) {
+ evaluation.queueSize = queue.size();
+ if (evaluation.format == null || !lastChunkWasInitialization) {
+ evaluator.evaluate(queue, playbackPositionUs, formats, evaluation);
+ }
+ Format selectedFormat = evaluation.format;
+ out.queueSize = evaluation.queueSize;
+
+ if (selectedFormat == null) {
+ out.chunk = null;
+ return;
+ } else if (out.queueSize == queue.size() && out.chunk != null
+ && out.chunk.format.id == selectedFormat.id) {
+ // We already have a chunk, and the evaluation hasn't changed either the format or the size
+ // of the queue. Leave unchanged.
+ return;
+ }
+
+ Representation selectedRepresentation = representations.get(selectedFormat.id);
+ WebmExtractor extractor = extractors.get(selectedRepresentation.format.id);
+ if (!extractor.isPrepared()) {
+ Chunk initializationChunk = newInitializationChunk(selectedRepresentation, extractor,
+ dataSource, evaluation.trigger);
+ lastChunkWasInitialization = true;
+ out.chunk = initializationChunk;
+ return;
+ }
+
+ int nextIndex;
+ if (queue.isEmpty()) {
+ nextIndex = Arrays.binarySearch(extractor.getCues().timesUs, seekPositionUs);
+ nextIndex = nextIndex < 0 ? -nextIndex - 2 : nextIndex;
+ } else {
+ nextIndex = queue.get(out.queueSize - 1).nextChunkIndex;
+ }
+
+ if (nextIndex == -1) {
+ out.chunk = null;
+ return;
+ }
+
+ Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, extractor, dataSource,
+ extractor.getCues(), nextIndex, evaluation.trigger, numSegmentsPerChunk);
+ lastChunkWasInitialization = false;
+ out.chunk = nextMediaChunk;
+ }
+
+ @Override
+ public IOException getError() {
+ return null;
+ }
+
+ private static Chunk newInitializationChunk(Representation representation,
+ WebmExtractor extractor, DataSource dataSource, int trigger) {
+ DataSpec dataSpec = new DataSpec(representation.uri, 0, representation.indexEnd + 1,
+ representation.getCacheKey());
+ return new InitializationWebmLoadable(dataSource, dataSpec, trigger, extractor, representation);
+ }
+
+ private static Chunk newMediaChunk(Representation representation,
+ WebmExtractor extractor, DataSource dataSource, SegmentIndex cues, int index,
+ int trigger, int numSegmentsPerChunk) {
+
+ // Computes the segments to included in the next fetch.
+ int numSegmentsToFetch = Math.min(numSegmentsPerChunk, cues.length - index);
+ int lastSegmentInChunk = index + numSegmentsToFetch - 1;
+ int nextIndex = lastSegmentInChunk == cues.length - 1 ? -1 : lastSegmentInChunk + 1;
+
+ long startTimeUs = cues.timesUs[index];
+
+ // Compute the end time, prefer to use next segment start time if there is a next segment.
+ long endTimeUs = nextIndex == -1 ?
+ cues.timesUs[lastSegmentInChunk] + cues.durationsUs[lastSegmentInChunk] :
+ cues.timesUs[nextIndex];
+
+ long offset = cues.offsets[index];
+
+ // Compute combined segments byte length.
+ long size = 0;
+ for (int i = index; i <= lastSegmentInChunk; i++) {
+ size += cues.sizes[i];
+ }
+
+ DataSpec dataSpec = new DataSpec(representation.uri, offset, size,
+ representation.getCacheKey());
+ return new WebmMediaChunk(dataSource, dataSpec, representation.format, trigger, extractor,
+ startTimeUs, endTimeUs, nextIndex);
+ }
+
+ private static class InitializationWebmLoadable extends Chunk {
+
+ private final Representation representation;
+ private final WebmExtractor extractor;
+
+ public InitializationWebmLoadable(DataSource dataSource, DataSpec dataSpec, int trigger,
+ WebmExtractor extractor, Representation representation) {
+ super(dataSource, dataSpec, representation.format, trigger);
+ this.extractor = extractor;
+ this.representation = representation;
+ }
+
+ @Override
+ protected void consumeStream(NonBlockingInputStream stream) throws IOException {
+ extractor.read(stream, null);
+ if (!extractor.isPrepared()) {
+ throw new ParserException("Invalid initialization data");
+ }
+ validateCues(extractor.getCues());
+ }
+
+ private void validateCues(SegmentIndex cues) {
+ long expectedSizeBytes = representation.indexEnd - representation.indexStart + 1;
+ if (cues.sizeBytes != expectedSizeBytes) {
+ Log.w(TAG, "Cues length mismatch: got " + cues.sizeBytes +
+ " but expected " + expectedSizeBytes);
+ }
+ long expectedContentLength = cues.offsets[cues.length - 1] +
+ cues.sizes[cues.length - 1] + representation.indexEnd + 1;
+ if (representation.contentLength > 0
+ && expectedContentLength != representation.contentLength) {
+ Log.w(TAG, "ContentLength mismatch: got " + expectedContentLength +
+ " but expected " + representation.contentLength);
+ }
+ }
+
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/AdaptationSet.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/AdaptationSet.java
new file mode 100644
index 00000000000..db68df9e13c
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/AdaptationSet.java
@@ -0,0 +1,58 @@
+/*
+ * 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 java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents a set of interchangeable encoded versions of a media content component.
+ */
+public final class AdaptationSet {
+
+ public static final int TYPE_UNKNOWN = -1;
+ public static final int TYPE_VIDEO = 0;
+ public static final int TYPE_AUDIO = 1;
+ public static final int TYPE_TEXT = 2;
+
+ public final int id;
+
+ public final int type;
+
+ public final List representations;
+ public final List contentProtections;
+
+ public AdaptationSet(int id, int type, List representations,
+ List contentProtections) {
+ this.id = id;
+ this.type = type;
+ this.representations = Collections.unmodifiableList(representations);
+ if (contentProtections == null) {
+ this.contentProtections = Collections.emptyList();
+ } else {
+ this.contentProtections = Collections.unmodifiableList(contentProtections);
+ }
+ }
+
+ public AdaptationSet(int id, int type, List representations) {
+ this(id, type, representations, null);
+ }
+
+ public boolean hasContentProtection() {
+ return !contentProtections.isEmpty();
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/ContentProtection.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/ContentProtection.java
new file mode 100644
index 00000000000..1232f804f33
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/ContentProtection.java
@@ -0,0 +1,50 @@
+/*
+ * 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 java.util.Collections;
+import java.util.Map;
+
+/**
+ * Represents a ContentProtection tag in an AdaptationSet. Holds arbitrary data for various DRM
+ * schemes.
+ */
+public final class ContentProtection {
+
+ /**
+ * Identifies the content protection scheme.
+ */
+ public final String schemeUriId;
+ /**
+ * Protection scheme specific data.
+ */
+ public final Map keyedData;
+
+ /**
+ * @param schemeUriId Identifies the content protection scheme.
+ * @param keyedData Data specific to the scheme.
+ */
+ public ContentProtection(String schemeUriId, Map keyedData) {
+ this.schemeUriId = schemeUriId;
+ if (keyedData != null) {
+ this.keyedData = Collections.unmodifiableMap(keyedData);
+ } else {
+ this.keyedData = Collections.emptyMap();
+ }
+
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescription.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescription.java
new file mode 100644
index 00000000000..6d7b35a4501
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescription.java
@@ -0,0 +1,45 @@
+/*
+ * 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 java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents a DASH media presentation description (mpd).
+ */
+public final class MediaPresentationDescription {
+
+ public final long duration;
+
+ public final long minBufferTime;
+
+ public final boolean dynamic;
+
+ public final long minUpdatePeriod;
+
+ public final List periods;
+
+ public MediaPresentationDescription(long duration, long minBufferTime, boolean dynamic,
+ long minUpdatePeriod, List periods) {
+ this.duration = duration;
+ this.minBufferTime = minBufferTime;
+ this.dynamic = dynamic;
+ this.minUpdatePeriod = minUpdatePeriod;
+ this.periods = Collections.unmodifiableList(periods);
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionFetcher.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionFetcher.java
new file mode 100644
index 00000000000..82e91c52dfb
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionFetcher.java
@@ -0,0 +1,68 @@
+/*
+ * 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.ParserException;
+import com.google.android.exoplayer.util.ManifestFetcher;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A concrete implementation of {@link ManifestFetcher} for loading DASH manifests.
+ *
+ * This class is provided for convenience, however it is expected that most applications will
+ * contain their own mechanisms for making asynchronous network requests and parsing the response.
+ * In such cases it is recommended that application developers use their existing solution rather
+ * than this one.
+ */
+public final class MediaPresentationDescriptionFetcher extends
+ ManifestFetcher {
+
+ private final MediaPresentationDescriptionParser parser;
+
+ /**
+ * @param callback The callback to provide with the parsed manifest (or error).
+ */
+ public MediaPresentationDescriptionFetcher(
+ ManifestCallback callback) {
+ super(callback);
+ parser = new MediaPresentationDescriptionParser();
+ }
+
+ /**
+ * @param callback The callback to provide with the parsed manifest (or error).
+ * @param timeoutMillis The timeout in milliseconds for the connection used to load the data.
+ */
+ public MediaPresentationDescriptionFetcher(
+ ManifestCallback callback, int timeoutMillis) {
+ super(callback, timeoutMillis);
+ parser = new MediaPresentationDescriptionParser();
+ }
+
+ @Override
+ protected MediaPresentationDescription parse(InputStream stream, String inputEncoding,
+ String contentId) throws IOException, ParserException {
+ try {
+ return parser.parseMediaPresentationDescription(stream, inputEncoding, contentId);
+ } catch (XmlPullParserException e) {
+ throw new ParserException(e);
+ }
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java
new file mode 100644
index 00000000000..9b0df777614
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java
@@ -0,0 +1,353 @@
+/*
+ * 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.ParserException;
+import com.google.android.exoplayer.chunk.Format;
+import com.google.android.exoplayer.upstream.DataSpec;
+import com.google.android.exoplayer.util.MimeTypes;
+
+import android.net.Uri;
+import android.util.Log;
+
+import org.xml.sax.helpers.DefaultHandler;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A parser of media presentation description files.
+ */
+/*
+ * TODO: Parse representation base attributes at multiple levels, and normalize the resulting
+ * datastructure.
+ * TODO: Decide how best to represent missing integer/double/long attributes.
+ */
+public class MediaPresentationDescriptionParser extends DefaultHandler {
+
+ private static final String TAG = "MediaPresentationDescriptionParser";
+
+ // Note: Does not support the date part of ISO 8601
+ private static final Pattern DURATION =
+ Pattern.compile("^PT(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?$");
+
+ private final XmlPullParserFactory xmlParserFactory;
+
+ public MediaPresentationDescriptionParser() {
+ try {
+ xmlParserFactory = XmlPullParserFactory.newInstance();
+ } catch (XmlPullParserException e) {
+ throw new RuntimeException("Couldn't create XmlPullParserFactory instance", e);
+ }
+ }
+
+ /**
+ * Parses a manifest from the provided {@link InputStream}.
+ *
+ * @param inputStream The stream from which to parse the manifest.
+ * @param inputEncoding The encoding of the input.
+ * @param contentId The content id of the media.
+ * @return The parsed manifest.
+ * @throws IOException If a problem occurred reading from the stream.
+ * @throws XmlPullParserException If a problem occurred parsing the stream as xml.
+ * @throws ParserException If a problem occurred parsing the xml as a DASH mpd.
+ */
+ public MediaPresentationDescription parseMediaPresentationDescription(InputStream inputStream,
+ String inputEncoding, String contentId) throws XmlPullParserException, IOException,
+ ParserException {
+ XmlPullParser xpp = xmlParserFactory.newPullParser();
+ xpp.setInput(inputStream, inputEncoding);
+ int eventType = xpp.next();
+ if (eventType != XmlPullParser.START_TAG || !"MPD".equals(xpp.getName())) {
+ throw new ParserException(
+ "inputStream does not contain a valid media presentation description");
+ }
+ return parseMediaPresentationDescription(xpp, contentId);
+ }
+
+ private MediaPresentationDescription parseMediaPresentationDescription(XmlPullParser xpp,
+ String contentId) throws XmlPullParserException, IOException {
+ long duration = parseDurationMs(xpp, "mediaPresentationDuration");
+ long minBufferTime = parseDurationMs(xpp, "minBufferTime");
+ String typeString = xpp.getAttributeValue(null, "type");
+ boolean dynamic = (typeString != null) ? typeString.equals("dynamic") : false;
+ long minUpdateTime = (dynamic) ? parseDurationMs(xpp, "minimumUpdatePeriod", -1) : -1;
+
+ List periods = new ArrayList();
+ do {
+ xpp.next();
+ if (isStartTag(xpp, "Period")) {
+ periods.add(parsePeriod(xpp, contentId, duration));
+ }
+ } while (!isEndTag(xpp, "MPD"));
+
+ return new MediaPresentationDescription(duration, minBufferTime, dynamic, minUpdateTime,
+ periods);
+ }
+
+ private Period parsePeriod(XmlPullParser xpp, String contentId, long mediaPresentationDuration)
+ throws XmlPullParserException, IOException {
+ int id = parseInt(xpp, "id");
+ long start = parseDurationMs(xpp, "start", 0);
+ long duration = parseDurationMs(xpp, "duration", mediaPresentationDuration);
+
+ List adaptationSets = new ArrayList();
+ List segmentTimelineList = null;
+ int segmentStartNumber = 0;
+ int segmentTimescale = 0;
+ long presentationTimeOffset = 0;
+ do {
+ xpp.next();
+ if (isStartTag(xpp, "AdaptationSet")) {
+ adaptationSets.add(parseAdaptationSet(xpp, contentId, start, duration,
+ segmentTimelineList));
+ } else if (isStartTag(xpp, "SegmentList")) {
+ segmentStartNumber = parseInt(xpp, "startNumber");
+ segmentTimescale = parseInt(xpp, "timescale");
+ presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", 0);
+ segmentTimelineList = parsePeriodSegmentList(xpp, segmentStartNumber);
+ }
+ } while (!isEndTag(xpp, "Period"));
+
+ return new Period(id, start, duration, adaptationSets, segmentTimelineList,
+ segmentStartNumber, segmentTimescale, presentationTimeOffset);
+ }
+
+ private List parsePeriodSegmentList(
+ XmlPullParser xpp, long segmentStartNumber) throws XmlPullParserException, IOException {
+ List segmentTimelineList = new ArrayList();
+
+ do {
+ xpp.next();
+ if (isStartTag(xpp, "SegmentTimeline")) {
+ do {
+ xpp.next();
+ if (isStartTag(xpp, "S")) {
+ long duration = parseLong(xpp, "d");
+ segmentTimelineList.add(new Segment.Timeline(segmentStartNumber, duration));
+ segmentStartNumber++;
+ }
+ } while (!isEndTag(xpp, "SegmentTimeline"));
+ }
+ } while (!isEndTag(xpp, "SegmentList"));
+
+ return segmentTimelineList;
+ }
+
+ private AdaptationSet parseAdaptationSet(XmlPullParser xpp, String contentId, long periodStart,
+ long periodDuration, List segmentTimelineList)
+ throws XmlPullParserException, IOException {
+ int id = -1;
+ int contentType = AdaptationSet.TYPE_UNKNOWN;
+
+ // TODO: Correctly handle other common attributes and elements. See 23009-1 Table 9.
+ String mimeType = xpp.getAttributeValue(null, "mimeType");
+ if (mimeType != null) {
+ if (MimeTypes.isAudio(mimeType)) {
+ contentType = AdaptationSet.TYPE_AUDIO;
+ } else if (MimeTypes.isVideo(mimeType)) {
+ contentType = AdaptationSet.TYPE_VIDEO;
+ } else if (MimeTypes.isText(mimeType)
+ || mimeType.equalsIgnoreCase(MimeTypes.APPLICATION_TTML)) {
+ contentType = AdaptationSet.TYPE_TEXT;
+ }
+ }
+
+ List contentProtections = null;
+ List representations = new ArrayList();
+ do {
+ xpp.next();
+ if (contentType != AdaptationSet.TYPE_UNKNOWN) {
+ if (isStartTag(xpp, "ContentProtection")) {
+ if (contentProtections == null) {
+ contentProtections = new ArrayList();
+ }
+ contentProtections.add(parseContentProtection(xpp));
+ } else if (isStartTag(xpp, "ContentComponent")) {
+ id = Integer.parseInt(xpp.getAttributeValue(null, "id"));
+ String contentTypeString = xpp.getAttributeValue(null, "contentType");
+ contentType = "video".equals(contentTypeString) ? AdaptationSet.TYPE_VIDEO
+ : "audio".equals(contentTypeString) ? AdaptationSet.TYPE_AUDIO
+ : AdaptationSet.TYPE_UNKNOWN;
+ } else if (isStartTag(xpp, "Representation")) {
+ representations.add(parseRepresentation(xpp, contentId, periodStart, periodDuration,
+ mimeType, segmentTimelineList));
+ }
+ }
+ } while (!isEndTag(xpp, "AdaptationSet"));
+
+ return new AdaptationSet(id, contentType, representations, contentProtections);
+ }
+
+ /**
+ * Parses a ContentProtection element.
+ *
+ * @throws XmlPullParserException If an error occurs parsing the element.
+ * @throws IOException If an error occurs reading the element.
+ **/
+ protected ContentProtection parseContentProtection(XmlPullParser xpp)
+ throws XmlPullParserException, IOException {
+ String schemeUriId = xpp.getAttributeValue(null, "schemeUriId");
+ return new ContentProtection(schemeUriId, null);
+ }
+
+ private Representation parseRepresentation(XmlPullParser xpp, String contentId, long periodStart,
+ long periodDuration, String parentMimeType, List segmentTimelineList)
+ throws XmlPullParserException, IOException {
+ int id;
+ try {
+ id = parseInt(xpp, "id");
+ } catch (NumberFormatException nfe) {
+ Log.d(TAG, "Unable to parse id; " + nfe.getMessage());
+ // TODO: need a way to generate a unique and stable id; use hashCode for now
+ id = xpp.getAttributeValue(null, "id").hashCode();
+ }
+ int bandwidth = parseInt(xpp, "bandwidth") / 8;
+ int audioSamplingRate = parseInt(xpp, "audioSamplingRate");
+ int width = parseInt(xpp, "width");
+ int height = parseInt(xpp, "height");
+
+ String mimeType = xpp.getAttributeValue(null, "mimeType");
+ if (mimeType == null) {
+ mimeType = parentMimeType;
+ }
+
+ String representationUrl = null;
+ long indexStart = -1;
+ long indexEnd = -1;
+ long initializationStart = -1;
+ long initializationEnd = -1;
+ int numChannels = -1;
+ List segmentList = null;
+ do {
+ xpp.next();
+ if (isStartTag(xpp, "BaseURL")) {
+ xpp.next();
+ representationUrl = xpp.getText();
+ } else if (isStartTag(xpp, "AudioChannelConfiguration")) {
+ numChannels = Integer.parseInt(xpp.getAttributeValue(null, "value"));
+ } else if (isStartTag(xpp, "SegmentBase")) {
+ String[] indexRange = xpp.getAttributeValue(null, "indexRange").split("-");
+ indexStart = Long.parseLong(indexRange[0]);
+ indexEnd = Long.parseLong(indexRange[1]);
+ } else if (isStartTag(xpp, "SegmentList")) {
+ segmentList = parseRepresentationSegmentList(xpp, segmentTimelineList);
+ } else if (isStartTag(xpp, "Initialization")) {
+ String[] indexRange = xpp.getAttributeValue(null, "range").split("-");
+ initializationStart = Long.parseLong(indexRange[0]);
+ initializationEnd = Long.parseLong(indexRange[1]);
+ }
+ } while (!isEndTag(xpp, "Representation"));
+
+ Uri uri = Uri.parse(representationUrl);
+ Format format = new Format(id, mimeType, width, height, numChannels, audioSamplingRate,
+ bandwidth);
+ if (segmentList == null) {
+ return new Representation(contentId, -1, format, uri, DataSpec.LENGTH_UNBOUNDED,
+ initializationStart, initializationEnd, indexStart, indexEnd, periodStart,
+ periodDuration);
+ } else {
+ return new SegmentedRepresentation(contentId, format, uri, initializationStart,
+ initializationEnd, indexStart, indexEnd, periodStart, periodDuration, segmentList);
+ }
+ }
+
+ private List parseRepresentationSegmentList(XmlPullParser xpp,
+ List segmentTimelineList) throws XmlPullParserException, IOException {
+ List segmentList = new ArrayList();
+ int i = 0;
+
+ do {
+ xpp.next();
+ if (isStartTag(xpp, "Initialization")) {
+ String url = xpp.getAttributeValue(null, "sourceURL");
+ String[] indexRange = xpp.getAttributeValue(null, "range").split("-");
+ long initializationStart = Long.parseLong(indexRange[0]);
+ long initializationEnd = Long.parseLong(indexRange[1]);
+ segmentList.add(new Segment.Initialization(url, initializationStart, initializationEnd));
+ } else if (isStartTag(xpp, "SegmentURL")) {
+ String url = xpp.getAttributeValue(null, "media");
+ String mediaRange = xpp.getAttributeValue(null, "mediaRange");
+ long sequenceNumber = segmentTimelineList.get(i).sequenceNumber;
+ long duration = segmentTimelineList.get(i).duration;
+ i++;
+ if (mediaRange != null) {
+ String[] mediaRangeArray = xpp.getAttributeValue(null, "mediaRange").split("-");
+ long mediaStart = Long.parseLong(mediaRangeArray[0]);
+ segmentList.add(new Segment.Media(url, mediaStart, sequenceNumber, duration));
+ } else {
+ segmentList.add(new Segment.Media(url, sequenceNumber, duration));
+ }
+ }
+ } while (!isEndTag(xpp, "SegmentList"));
+
+ return segmentList;
+ }
+
+ protected static boolean isEndTag(XmlPullParser xpp, String name) throws XmlPullParserException {
+ return xpp.getEventType() == XmlPullParser.END_TAG && name.equals(xpp.getName());
+ }
+
+ protected static boolean isStartTag(XmlPullParser xpp, String name)
+ throws XmlPullParserException {
+ return xpp.getEventType() == XmlPullParser.START_TAG && name.equals(xpp.getName());
+ }
+
+ protected static int parseInt(XmlPullParser xpp, String name) {
+ String value = xpp.getAttributeValue(null, name);
+ return value == null ? -1 : Integer.parseInt(value);
+ }
+
+ protected static long parseLong(XmlPullParser xpp, String name) {
+ return parseLong(xpp, name, -1);
+ }
+
+ protected static long parseLong(XmlPullParser xpp, String name, long defaultValue) {
+ String value = xpp.getAttributeValue(null, name);
+ return value == null ? defaultValue : Long.parseLong(value);
+ }
+
+ private long parseDurationMs(XmlPullParser xpp, String name) {
+ return parseDurationMs(xpp, name, -1);
+ }
+
+ private long parseDurationMs(XmlPullParser xpp, String name, long defaultValue) {
+ String value = xpp.getAttributeValue(null, name);
+ if (value != null) {
+ Matcher matcher = DURATION.matcher(value);
+ if (matcher.matches()) {
+ String hours = matcher.group(2);
+ double durationSeconds = (hours != null) ? Double.parseDouble(hours) * 3600 : 0;
+ String minutes = matcher.group(4);
+ durationSeconds += (minutes != null) ? Double.parseDouble(minutes) * 60 : 0;
+ String seconds = matcher.group(6);
+ durationSeconds += (seconds != null) ? Double.parseDouble(seconds) : 0;
+ return (long) (durationSeconds * 1000);
+ } else {
+ return (long) (Double.parseDouble(value) * 3600 * 1000);
+ }
+ }
+ return defaultValue;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/Period.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/Period.java
new file mode 100644
index 00000000000..4b33161acb4
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/Period.java
@@ -0,0 +1,68 @@
+/*
+ * 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 java.util.Collections;
+import java.util.List;
+
+/**
+ * Encapsulates media content components over a contiguous period of time.
+ */
+public final class Period {
+
+ public final int id;
+
+ public final long start;
+
+ public final long duration;
+
+ public final List adaptationSets;
+
+ public final List segmentList;
+
+ public final int segmentStartNumber;
+
+ public final int segmentTimescale;
+
+ public final long presentationTimeOffset;
+
+ public Period(int id, long start, long duration, List adaptationSets) {
+ this(id, start, duration, adaptationSets, null, 0, 0, 0);
+ }
+
+ public Period(int id, long start, long duration, List adaptationSets,
+ List segmentList, int segmentStartNumber, int segmentTimescale) {
+ this(id, start, duration, adaptationSets, segmentList, segmentStartNumber, segmentTimescale, 0);
+ }
+
+ public Period(int id, long start, long duration, List adaptationSets,
+ List segmentList, int segmentStartNumber, int segmentTimescale,
+ long presentationTimeOffset) {
+ this.id = id;
+ this.start = start;
+ this.duration = duration;
+ this.adaptationSets = Collections.unmodifiableList(adaptationSets);
+ if (segmentList != null) {
+ this.segmentList = Collections.unmodifiableList(segmentList);
+ } else {
+ this.segmentList = null;
+ }
+ this.segmentStartNumber = segmentStartNumber;
+ this.segmentTimescale = segmentTimescale;
+ this.presentationTimeOffset = presentationTimeOffset;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/Representation.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/Representation.java
new file mode 100644
index 00000000000..e5b11e94ca1
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/Representation.java
@@ -0,0 +1,92 @@
+/*
+ * 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 android.net.Uri;
+
+/**
+ * A flat version of a DASH representation.
+ */
+public class Representation {
+
+ /**
+ * Identifies the piece of content to which this {@link Representation} belongs.
+ *
+ * For example, all {@link Representation}s belonging to a video should have the same
+ * {@link #contentId}, which should uniquely identify that video.
+ */
+ public final String contentId;
+
+ /**
+ * Identifies the revision of the {@link Representation}.
+ *
+ * If the media for a given ({@link #contentId} can change over time without a change to the
+ * {@link #format}'s {@link Format#id} (e.g. as a result of re-encoding the media with an
+ * updated encoder), then this identifier must uniquely identify the revision of the media. The
+ * timestamp at which the media was encoded is often a suitable.
+ */
+ public final long revisionId;
+
+ /**
+ * The format in which the {@link Representation} is encoded.
+ */
+ public final Format format;
+
+ public final long contentLength;
+
+ public final long initializationStart;
+
+ public final long initializationEnd;
+
+ public final long indexStart;
+
+ public final long indexEnd;
+
+ public final long periodStart;
+
+ public final long periodDuration;
+
+ public final Uri uri;
+
+ public Representation(String contentId, long revisionId, Format format, Uri uri,
+ long contentLength, long initializationStart, long initializationEnd, long indexStart,
+ long indexEnd, long periodStart, long periodDuration) {
+ this.contentId = contentId;
+ this.revisionId = revisionId;
+ this.format = format;
+ this.contentLength = contentLength;
+ this.initializationStart = initializationStart;
+ this.initializationEnd = initializationEnd;
+ this.indexStart = indexStart;
+ this.indexEnd = indexEnd;
+ this.periodStart = periodStart;
+ this.periodDuration = periodDuration;
+ this.uri = uri;
+ }
+
+ /**
+ * Generates a cache key for the {@link Representation}, in the format
+ * {@link #contentId}.{@link #format.id}.{@link #revisionId}.
+ *
+ * @return A cache key.
+ */
+ public String getCacheKey() {
+ return contentId + "." + format.id + "." + revisionId;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/Segment.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/Segment.java
new file mode 100644
index 00000000000..681c7aae121
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/Segment.java
@@ -0,0 +1,81 @@
+/*
+ * 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;
+ }
+
+ }
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentedRepresentation.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentedRepresentation.java
new file mode 100644
index 00000000000..53f14c38527
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentedRepresentation.java
@@ -0,0 +1,49 @@
+/*
+ * 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 segmentList;
+
+ public SegmentedRepresentation(String contentId, Format format, Uri uri, long initializationStart,
+ long initializationEnd, long indexStart, long indexEnd, long periodStart, long periodDuration,
+ List 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);
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/drm/DrmSessionManager.java b/library/src/main/java/com/google/android/exoplayer/drm/DrmSessionManager.java
new file mode 100644
index 00000000000..3bdfae9d121
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/drm/DrmSessionManager.java
@@ -0,0 +1,109 @@
+/*
+ * 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.drm;
+
+import android.annotation.TargetApi;
+import android.media.MediaCrypto;
+
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * Manages a DRM session.
+ */
+@TargetApi(16)
+public interface DrmSessionManager {
+
+ /**
+ * The error state. {@link #getError()} can be used to retrieve the cause.
+ */
+ public static final int STATE_ERROR = 0;
+ /**
+ * The session is closed.
+ */
+ public static final int STATE_CLOSED = 1;
+ /**
+ * The session is being opened (i.e. {@link #open(Map, String)} has been called, but the session
+ * is not yet open).
+ */
+ public static final int STATE_OPENING = 2;
+ /**
+ * The session is open, but does not yet have the keys required for decryption.
+ */
+ public static final int STATE_OPENED = 3;
+ /**
+ * The session is open and has the keys required for decryption.
+ */
+ public static final int STATE_OPENED_WITH_KEYS = 4;
+
+ /**
+ * Opens the session, possibly asynchronously.
+ *
+ * @param drmInitData Initialization data for the drm schemes supported by the media, keyed by
+ * scheme UUID.
+ * @param mimeType The mimeType of the media.
+ */
+ void open(Map drmInitData, String mimeType);
+
+ /**
+ * Closes the session.
+ */
+ void close();
+
+ /**
+ * Gets the current state of the session.
+ *
+ * @return One of {@link #STATE_ERROR}, {@link #STATE_CLOSED}, {@link #STATE_OPENING},
+ * {@link #STATE_OPENED} and {@link #STATE_OPENED_WITH_KEYS}.
+ */
+ int getState();
+
+ /**
+ * Gets a {@link MediaCrypto} for the open session.
+ *
+ * This method may be called when the manager is in the following states:
+ * {@link #STATE_OPENED}, {@link #STATE_OPENED_WITH_KEYS}
+ *
+ * @return A {@link MediaCrypto} for the open session.
+ * @throws IllegalStateException If called when a session isn't opened.
+ */
+ MediaCrypto getMediaCrypto();
+
+ /**
+ * Whether the session requires a secure decoder for the specified mime type.
+ *
+ * Normally this method should return {@link MediaCrypto#requiresSecureDecoderComponent(String)},
+ * however in some cases implementations may wish to modify the return value (i.e. to force a
+ * secure decoder even when one is not required).
+ *
+ * This method may be called when the manager is in the following states:
+ * {@link #STATE_OPENED}, {@link #STATE_OPENED_WITH_KEYS}
+ *
+ * @return Whether the open session requires a secure decoder for the specified mime type.
+ * @throws IllegalStateException If called when a session isn't opened.
+ */
+ boolean requiresSecureDecoderComponent(String mimeType);
+
+ /**
+ * Gets the cause of the error state.
+ *
+ * This method may be called when the manager is in any state.
+ *
+ * @return An exception if the state is {@link #STATE_ERROR}. Null otherwise.
+ */
+ Exception getError();
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/drm/MediaDrmCallback.java b/library/src/main/java/com/google/android/exoplayer/drm/MediaDrmCallback.java
new file mode 100644
index 00000000000..30757e75b45
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/drm/MediaDrmCallback.java
@@ -0,0 +1,49 @@
+/*
+ * 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.drm;
+
+import android.annotation.TargetApi;
+import android.media.MediaDrm;
+
+import java.util.UUID;
+
+/**
+ * Performs {@link MediaDrm} key and provisioning requests.
+ */
+@TargetApi(18)
+public interface MediaDrmCallback {
+
+ /**
+ * Executes a provisioning request.
+ *
+ * @param uuid The UUID of the content protection scheme.
+ * @param request The request.
+ * @return The response data.
+ * @throws Exception If an error occurred executing the request.
+ */
+ byte[] executeProvisionRequest(UUID uuid, MediaDrm.ProvisionRequest request) throws Exception;
+
+ /**
+ * Executes a key request.
+ *
+ * @param uuid The UUID of the content protection scheme.
+ * @param request The request.
+ * @return The response data.
+ * @throws Exception If an error occurred executing the request.
+ */
+ byte[] executeKeyRequest(UUID uuid, MediaDrm.KeyRequest request) throws Exception;
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java b/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java
new file mode 100644
index 00000000000..b01955ebaa5
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java
@@ -0,0 +1,383 @@
+/*
+ * 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.drm;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.media.DeniedByServerException;
+import android.media.MediaCrypto;
+import android.media.MediaDrm;
+import android.media.MediaDrm.KeyRequest;
+import android.media.MediaDrm.OnEventListener;
+import android.media.MediaDrm.ProvisionRequest;
+import android.media.NotProvisionedException;
+import android.media.UnsupportedSchemeException;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * A base class for {@link DrmSessionManager} implementations that support streaming playbacks
+ * using {@link MediaDrm}.
+ */
+@TargetApi(18)
+public class StreamingDrmSessionManager implements DrmSessionManager {
+
+ /**
+ * Interface definition for a callback to be notified of {@link StreamingDrmSessionManager}
+ * events.
+ */
+ public interface EventListener {
+
+ /**
+ * Invoked when a drm error occurs.
+ *
+ * @param e The corresponding exception.
+ */
+ void onDrmSessionManagerError(Exception e);
+
+ }
+
+ private static final int MSG_PROVISION = 0;
+ private static final int MSG_KEYS = 1;
+
+ private final Handler eventHandler;
+ private final EventListener eventListener;
+ private final MediaDrm mediaDrm;
+
+ /* package */ final MediaDrmHandler mediaDrmHandler;
+ /* package */ final MediaDrmCallback callback;
+ /* package */ final PostResponseHandler postResponseHandler;
+ /* package */ final UUID uuid;
+
+ private HandlerThread requestHandlerThread;
+ private Handler postRequestHandler;
+
+ private int openCount;
+ private int state;
+ private MediaCrypto mediaCrypto;
+ private Exception lastException;
+ private String mimeType;
+ private byte[] schemePsshData;
+ private byte[] sessionId;
+
+ /**
+ * @param uuid The UUID of the drm scheme.
+ * @param playbackLooper The looper associated with the media playback thread. Should usually be
+ * obtained using {@link com.google.android.exoplayer.ExoPlayer#getPlaybackLooper()}.
+ * @param callback Performs key and provisioning requests.
+ * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+ * null if delivery of events is not required.
+ * @param eventListener A listener of events. May be null if delivery of events is not required.
+ * @throws UnsupportedSchemeException If the specified DRM scheme is not supported.
+ */
+ public StreamingDrmSessionManager(UUID uuid, Looper playbackLooper, MediaDrmCallback callback,
+ Handler eventHandler, EventListener eventListener) throws UnsupportedSchemeException {
+ this.uuid = uuid;
+ this.callback = callback;
+ this.eventHandler = eventHandler;
+ this.eventListener = eventListener;
+ mediaDrm = new MediaDrm(uuid);
+ mediaDrm.setOnEventListener(new MediaDrmEventListener());
+ mediaDrmHandler = new MediaDrmHandler(playbackLooper);
+ postResponseHandler = new PostResponseHandler(playbackLooper);
+ state = STATE_CLOSED;
+ }
+
+ @Override
+ public int getState() {
+ return state;
+ }
+
+ @Override
+ public MediaCrypto getMediaCrypto() {
+ if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
+ throw new IllegalStateException();
+ }
+ return mediaCrypto;
+ }
+
+ @Override
+ public boolean requiresSecureDecoderComponent(String mimeType) {
+ if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
+ throw new IllegalStateException();
+ }
+ return mediaCrypto.requiresSecureDecoderComponent(mimeType);
+ }
+
+ @Override
+ public Exception getError() {
+ return state == STATE_ERROR ? lastException : null;
+ }
+
+ /**
+ * Provides access to {@link MediaDrm#getPropertyString(String)}.
+ *
+ * This method may be called when the manager is in any state.
+ *
+ * @param key The key to request.
+ * @return The retrieved property.
+ */
+ public final String getPropertyString(String key) {
+ return mediaDrm.getPropertyString(key);
+ }
+
+ /**
+ * Provides access to {@link MediaDrm#getPropertyByteArray(String)}.
+ *
+ * This method may be called when the manager is in any state.
+ *
+ * @param key The key to request.
+ * @return The retrieved property.
+ */
+ public final byte[] getPropertyByteArray(String key) {
+ return mediaDrm.getPropertyByteArray(key);
+ }
+
+ @Override
+ public void open(Map psshData, String mimeType) {
+ if (++openCount != 1) {
+ return;
+ }
+ if (postRequestHandler == null) {
+ requestHandlerThread = new HandlerThread("DrmRequestHandler");
+ requestHandlerThread.start();
+ postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper());
+ }
+ if (this.schemePsshData == null) {
+ this.mimeType = mimeType;
+ schemePsshData = psshData.get(uuid);
+ if (schemePsshData == null) {
+ onError(new IllegalStateException("Media does not support uuid: " + uuid));
+ return;
+ }
+ }
+ state = STATE_OPENING;
+ openInternal(true);
+ }
+
+ @Override
+ public void close() {
+ if (--openCount != 0) {
+ return;
+ }
+ state = STATE_CLOSED;
+ mediaDrmHandler.removeCallbacksAndMessages(null);
+ postResponseHandler.removeCallbacksAndMessages(null);
+ postRequestHandler.removeCallbacksAndMessages(null);
+ postRequestHandler = null;
+ requestHandlerThread.quit();
+ requestHandlerThread = null;
+ schemePsshData = null;
+ mediaCrypto = null;
+ lastException = null;
+ if (sessionId != null) {
+ mediaDrm.closeSession(sessionId);
+ sessionId = null;
+ }
+ }
+
+ private void openInternal(boolean allowProvisioning) {
+ try {
+ sessionId = mediaDrm.openSession();
+ mediaCrypto = new MediaCrypto(uuid, sessionId);
+ state = STATE_OPENED;
+ postKeyRequest();
+ } catch (NotProvisionedException e) {
+ if (allowProvisioning) {
+ postProvisionRequest();
+ } else {
+ onError(e);
+ }
+ } catch (Exception e) {
+ onError(e);
+ }
+ }
+
+ private void postProvisionRequest() {
+ ProvisionRequest request = mediaDrm.getProvisionRequest();
+ postRequestHandler.obtainMessage(MSG_PROVISION, request).sendToTarget();
+ }
+
+ private void onProvisionResponse(Object response) {
+ if (state != STATE_OPENING && state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
+ // This event is stale.
+ return;
+ }
+
+ if (response instanceof Exception) {
+ onError((Exception) response);
+ return;
+ }
+
+ try {
+ mediaDrm.provideProvisionResponse((byte[]) response);
+ if (state == STATE_OPENING) {
+ openInternal(false);
+ } else {
+ postKeyRequest();
+ }
+ } catch (DeniedByServerException e) {
+ onError(e);
+ }
+ }
+
+ private void postKeyRequest() {
+ KeyRequest keyRequest;
+ try {
+ keyRequest = mediaDrm.getKeyRequest(sessionId, schemePsshData, mimeType,
+ MediaDrm.KEY_TYPE_STREAMING, null);
+ postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget();
+ } catch (NotProvisionedException e) {
+ onKeysError(e);
+ }
+ }
+
+ private void onKeyResponse(Object response) {
+ if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
+ // This event is stale.
+ return;
+ }
+
+ if (response instanceof Exception) {
+ onKeysError((Exception) response);
+ return;
+ }
+
+ try {
+ mediaDrm.provideKeyResponse(sessionId, (byte[]) response);
+ state = STATE_OPENED_WITH_KEYS;
+ } catch (Exception e) {
+ onKeysError(e);
+ }
+ }
+
+ private void onKeysError(Exception e) {
+ if (e instanceof NotProvisionedException) {
+ postProvisionRequest();
+ } else {
+ onError(e);
+ }
+ }
+
+ private void onError(Exception e) {
+ lastException = e;
+ if (eventHandler != null && eventListener != null) {
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ eventListener.onDrmSessionManagerError(lastException);
+ }
+ });
+ }
+ if (state != STATE_OPENED_WITH_KEYS) {
+ state = STATE_ERROR;
+ }
+ }
+
+ @SuppressLint("HandlerLeak")
+ private class MediaDrmHandler extends Handler {
+
+ public MediaDrmHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (openCount == 0 || (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS)) {
+ return;
+ }
+ switch (msg.what) {
+ case MediaDrm.EVENT_KEY_REQUIRED:
+ postKeyRequest();
+ return;
+ case MediaDrm.EVENT_KEY_EXPIRED:
+ state = STATE_OPENED;
+ postKeyRequest();
+ return;
+ case MediaDrm.EVENT_PROVISION_REQUIRED:
+ state = STATE_OPENED;
+ postProvisionRequest();
+ return;
+ }
+ }
+
+ }
+
+ private class MediaDrmEventListener implements OnEventListener {
+
+ @Override
+ public void onEvent(MediaDrm md, byte[] sessionId, int event, int extra, byte[] data) {
+ mediaDrmHandler.sendEmptyMessage(event);
+ }
+
+ }
+
+ @SuppressLint("HandlerLeak")
+ private class PostResponseHandler extends Handler {
+
+ public PostResponseHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_PROVISION:
+ onProvisionResponse(msg.obj);
+ return;
+ case MSG_KEYS:
+ onKeyResponse(msg.obj);
+ return;
+ }
+ }
+
+ }
+
+ @SuppressLint("HandlerLeak")
+ private class PostRequestHandler extends Handler {
+
+ public PostRequestHandler(Looper backgroundLooper) {
+ super(backgroundLooper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ Object response;
+ try {
+ switch (msg.what) {
+ case MSG_PROVISION:
+ response = callback.executeProvisionRequest(uuid, (ProvisionRequest) msg.obj);
+ break;
+ case MSG_KEYS:
+ response = callback.executeKeyRequest(uuid, (KeyRequest) msg.obj);
+ break;
+ default:
+ throw new RuntimeException();
+ }
+ } catch (Exception e) {
+ response = e;
+ }
+ postResponseHandler.obtainMessage(msg.what, response).sendToTarget();
+ }
+
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/parser/SegmentIndex.java b/library/src/main/java/com/google/android/exoplayer/parser/SegmentIndex.java
new file mode 100644
index 00000000000..614453c1ef8
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/parser/SegmentIndex.java
@@ -0,0 +1,70 @@
+/*
+ * 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;
+
+/**
+ * Defines segments within a media stream.
+ */
+public final class SegmentIndex {
+
+ /**
+ * The size in bytes of the segment index as it exists in the stream.
+ */
+ public final int sizeBytes;
+
+ /**
+ * The number of segments.
+ */
+ public final int length;
+
+ /**
+ * The segment sizes, in bytes.
+ */
+ public final int[] sizes;
+
+ /**
+ * The segment byte offsets.
+ */
+ public final long[] offsets;
+
+ /**
+ * The segment durations, in microseconds.
+ */
+ public final long[] durationsUs;
+
+ /**
+ * The start time of each segment, in microseconds.
+ */
+ public final long[] timesUs;
+
+ /**
+ * @param sizeBytes The size in bytes of the segment index as it exists in the stream.
+ * @param sizes The segment sizes, in bytes.
+ * @param offsets The segment byte offsets.
+ * @param durationsUs The segment durations, in microseconds.
+ * @param timesUs The start time of each segment, in microseconds.
+ */
+ public SegmentIndex(int sizeBytes, int[] sizes, long[] offsets, long[] durationsUs,
+ long[] timesUs) {
+ this.sizeBytes = sizeBytes;
+ this.length = sizes.length;
+ this.sizes = sizes;
+ this.offsets = offsets;
+ this.durationsUs = durationsUs;
+ this.timesUs = timesUs;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/Atom.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/Atom.java
new file mode 100644
index 00000000000..643dbd8205d
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/Atom.java
@@ -0,0 +1,117 @@
+/*
+ * 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.mp4;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/* package */ abstract class Atom {
+
+ public static final int TYPE_avc1 = 0x61766331;
+ public static final int TYPE_esds = 0x65736473;
+ public static final int TYPE_mdat = 0x6D646174;
+ public static final int TYPE_mfhd = 0x6D666864;
+ public static final int TYPE_mp4a = 0x6D703461;
+ public static final int TYPE_tfdt = 0x74666474;
+ public static final int TYPE_tfhd = 0x74666864;
+ public static final int TYPE_trex = 0x74726578;
+ public static final int TYPE_trun = 0x7472756E;
+ public static final int TYPE_sidx = 0x73696478;
+ public static final int TYPE_moov = 0x6D6F6F76;
+ public static final int TYPE_trak = 0x7472616B;
+ public static final int TYPE_mdia = 0x6D646961;
+ public static final int TYPE_minf = 0x6D696E66;
+ public static final int TYPE_stbl = 0x7374626C;
+ public static final int TYPE_avcC = 0x61766343;
+ public static final int TYPE_moof = 0x6D6F6F66;
+ public static final int TYPE_traf = 0x74726166;
+ public static final int TYPE_mvex = 0x6D766578;
+ public static final int TYPE_tkhd = 0x746B6864;
+ public static final int TYPE_mdhd = 0x6D646864;
+ public static final int TYPE_hdlr = 0x68646C72;
+ public static final int TYPE_stsd = 0x73747364;
+ public static final int TYPE_pssh = 0x70737368;
+ public static final int TYPE_sinf = 0x73696E66;
+ public static final int TYPE_schm = 0x7363686D;
+ public static final int TYPE_schi = 0x73636869;
+ public static final int TYPE_tenc = 0x74656E63;
+ public static final int TYPE_encv = 0x656E6376;
+ public static final int TYPE_enca = 0x656E6361;
+ public static final int TYPE_frma = 0x66726D61;
+ public static final int TYPE_saiz = 0x7361697A;
+ public static final int TYPE_uuid = 0x75756964;
+
+ public final int type;
+
+ Atom(int type) {
+ this.type = type;
+ }
+
+ public final static class LeafAtom extends Atom {
+
+ private final ParsableByteArray data;
+
+ public LeafAtom(int type, ParsableByteArray data) {
+ super(type);
+ this.data = data;
+ }
+
+ public ParsableByteArray getData() {
+ return data;
+ }
+
+ }
+
+ public final static class ContainerAtom extends Atom {
+
+ public final ArrayList children;
+
+ public ContainerAtom(int type) {
+ super(type);
+ children = new ArrayList();
+ }
+
+ public void add(Atom atom) {
+ children.add(atom);
+ }
+
+ public LeafAtom getLeafAtomOfType(int type) {
+ for (int i = 0; i < children.size(); i++) {
+ Atom atom = children.get(i);
+ if (atom.type == type) {
+ return (LeafAtom) atom;
+ }
+ }
+ return null;
+ }
+
+ public ContainerAtom getContainerAtomOfType(int type) {
+ for (int i = 0; i < children.size(); i++) {
+ Atom atom = children.get(i);
+ if (atom.type == type) {
+ return (ContainerAtom) atom;
+ }
+ }
+ return null;
+ }
+
+ public List getChildren() {
+ return children;
+ }
+
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/CodecSpecificDataUtil.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/CodecSpecificDataUtil.java
new file mode 100644
index 00000000000..e441670cf6b
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/CodecSpecificDataUtil.java
@@ -0,0 +1,252 @@
+/*
+ * 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.mp4;
+
+import com.google.android.exoplayer.util.Assertions;
+
+import android.annotation.SuppressLint;
+import android.media.MediaCodecInfo.CodecProfileLevel;
+import android.util.Pair;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Provides static utility methods for manipulating various types of codec specific data.
+ */
+public class CodecSpecificDataUtil {
+
+ private static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1};
+
+ private static final int[] AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE = new int[] {
+ 96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350
+ };
+
+ private static final int SPS_NAL_UNIT_TYPE = 7;
+
+ private CodecSpecificDataUtil() {}
+
+ /**
+ * Parses an AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1
+ *
+ * @param audioSpecificConfig
+ * @return A pair consisting of the sample rate in Hz and the channel count.
+ */
+ public static Pair parseAudioSpecificConfig(byte[] audioSpecificConfig) {
+ int audioObjectType = (audioSpecificConfig[0] >> 3) & 0x1F;
+ int byteOffset = audioObjectType == 5 || audioObjectType == 29 ? 1 : 0;
+ int frequencyIndex = (audioSpecificConfig[byteOffset] & 0x7) << 1
+ | ((audioSpecificConfig[byteOffset + 1] >> 7) & 0x1);
+ Assertions.checkState(frequencyIndex < 13);
+ int sampleRate = AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE[frequencyIndex];
+ int channelCount = (audioSpecificConfig[byteOffset + 1] >> 3) & 0xF;
+ return Pair.create(sampleRate, channelCount);
+ }
+
+ /**
+ * Builds a simple HE-AAC LC AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1
+ *
+ * @param sampleRate The sample rate in Hz.
+ * @param numChannels The number of channels
+ * @return The AudioSpecificConfig.
+ */
+ public static byte[] buildAudioSpecificConfig(int sampleRate, int numChannels) {
+ int sampleRateIndex = -1;
+ for (int i = 0; i < AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE.length; ++i) {
+ if (sampleRate == AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE[i]) {
+ sampleRateIndex = i;
+ }
+ }
+ // The full specification for AudioSpecificConfig is stated in ISO 14496-3 Section 1.6.2.1
+ byte[] csd = new byte[2];
+ csd[0] = (byte) ((2 /* AAC LC */ << 3) | (sampleRateIndex >> 1));
+ csd[1] = (byte) (((sampleRateIndex & 0x1) << 7) | (numChannels << 3));
+ return csd;
+ }
+
+ /**
+ * Constructs a NAL unit consisting of the NAL start code followed by the specified data.
+ *
+ * @param data An array containing the data that should follow the NAL start code.
+ * @param offset The start offset into {@code data}.
+ * @param length The number of bytes to copy from {@code data}
+ * @return The constructed NAL unit.
+ */
+ public static byte[] buildNalUnit(byte[] data, int offset, int length) {
+ byte[] nalUnit = new byte[length + NAL_START_CODE.length];
+ System.arraycopy(NAL_START_CODE, 0, nalUnit, 0, NAL_START_CODE.length);
+ System.arraycopy(data, offset, nalUnit, NAL_START_CODE.length, length);
+ return nalUnit;
+ }
+
+ /**
+ * Splits an array of NAL units.
+ *
+ * If the input consists of NAL start code delimited units, then the returned array consists of
+ * the split NAL units, each of which is still prefixed with the NAL start code. For any other
+ * input, null is returned.
+ *
+ * @param data An array of data.
+ * @return The individual NAL units, or null if the input did not consist of NAL start code
+ * delimited units.
+ */
+ public static byte[][] splitNalUnits(byte[] data) {
+ if (!isNalStartCode(data, 0)) {
+ // data does not consist of NAL start code delimited units.
+ return null;
+ }
+ List starts = new ArrayList();
+ int nalUnitIndex = 0;
+ do {
+ starts.add(nalUnitIndex);
+ nalUnitIndex = findNalStartCode(data, nalUnitIndex + NAL_START_CODE.length);
+ } while (nalUnitIndex != -1);
+ byte[][] split = new byte[starts.size()][];
+ for (int i = 0; i < starts.size(); i++) {
+ int startIndex = starts.get(i);
+ int endIndex = i < starts.size() - 1 ? starts.get(i + 1) : data.length;
+ byte[] nal = new byte[endIndex - startIndex];
+ System.arraycopy(data, startIndex, nal, 0, nal.length);
+ split[i] = nal;
+ }
+ return split;
+ }
+
+ /**
+ * Finds the next occurrence of the NAL start code from a given index.
+ *
+ * @param data The data in which to search.
+ * @param index The first index to test.
+ * @return The index of the first byte of the found start code, or -1.
+ */
+ private static int findNalStartCode(byte[] data, int index) {
+ int endIndex = data.length - NAL_START_CODE.length;
+ for (int i = index; i <= endIndex; i++) {
+ if (isNalStartCode(data, i)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Tests whether there exists a NAL start code at a given index.
+ *
+ * @param data The data.
+ * @param index The index to test.
+ * @return Whether there exists a start code that begins at {@code index}.
+ */
+ private static boolean isNalStartCode(byte[] data, int index) {
+ if (data.length - index <= NAL_START_CODE.length) {
+ return false;
+ }
+ for (int j = 0; j < NAL_START_CODE.length; j++) {
+ if (data[index + j] != NAL_START_CODE[j]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Parses an SPS NAL unit.
+ *
+ * @param spsNalUnit The NAL unit.
+ * @return A pair consisting of AVC profile and level constants, as defined in
+ * {@link CodecProfileLevel}. Null if the input data was not an SPS NAL unit.
+ */
+ public static Pair parseSpsNalUnit(byte[] spsNalUnit) {
+ // SPS NAL unit:
+ // - Start prefix (4 bytes)
+ // - Forbidden zero bit (1 bit)
+ // - NAL ref idx (2 bits)
+ // - NAL unit type (5 bits)
+ // - Profile idc (8 bits)
+ // - Constraint bits (3 bits)
+ // - Reserved bits (5 bits)
+ // - Level idx (8 bits)
+ if (isNalStartCode(spsNalUnit, 0) && spsNalUnit.length == 8
+ && (spsNalUnit[5] & 0x1F) == SPS_NAL_UNIT_TYPE) {
+ return Pair.create(parseAvcProfile(spsNalUnit), parseAvcLevel(spsNalUnit));
+ }
+ return null;
+ }
+
+ @SuppressLint("InlinedApi")
+ private static int parseAvcProfile(byte[] data) {
+ int profileIdc = data[6] & 0xFF;
+ switch (profileIdc) {
+ case 0x42:
+ return CodecProfileLevel.AVCProfileBaseline;
+ case 0x4d:
+ return CodecProfileLevel.AVCProfileMain;
+ case 0x58:
+ return CodecProfileLevel.AVCProfileExtended;
+ case 0x64:
+ return CodecProfileLevel.AVCProfileHigh;
+ case 0x6e:
+ return CodecProfileLevel.AVCProfileHigh10;
+ case 0x7a:
+ return CodecProfileLevel.AVCProfileHigh422;
+ case 0xf4:
+ return CodecProfileLevel.AVCProfileHigh444;
+ default:
+ return 0;
+ }
+ }
+
+ @SuppressLint("InlinedApi")
+ private static int parseAvcLevel(byte[] data) {
+ int levelIdc = data[8] & 0xFF;
+ switch (levelIdc) {
+ case 9:
+ return CodecProfileLevel.AVCLevel1b;
+ case 10:
+ return CodecProfileLevel.AVCLevel1;
+ case 11:
+ return CodecProfileLevel.AVCLevel11;
+ case 12:
+ return CodecProfileLevel.AVCLevel12;
+ case 13:
+ return CodecProfileLevel.AVCLevel13;
+ case 20:
+ return CodecProfileLevel.AVCLevel2;
+ case 21:
+ return CodecProfileLevel.AVCLevel21;
+ case 22:
+ return CodecProfileLevel.AVCLevel22;
+ case 30:
+ return CodecProfileLevel.AVCLevel3;
+ case 31:
+ return CodecProfileLevel.AVCLevel31;
+ case 32:
+ return CodecProfileLevel.AVCLevel32;
+ case 40:
+ return CodecProfileLevel.AVCLevel4;
+ case 41:
+ return CodecProfileLevel.AVCLevel41;
+ case 42:
+ return CodecProfileLevel.AVCLevel42;
+ case 50:
+ return CodecProfileLevel.AVCLevel5;
+ case 51:
+ return CodecProfileLevel.AVCLevel51;
+ default:
+ return 0;
+ }
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/DefaultSampleValues.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/DefaultSampleValues.java
new file mode 100644
index 00000000000..5e2e0733305
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/DefaultSampleValues.java
@@ -0,0 +1,32 @@
+/*
+ * 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.mp4;
+
+/* package */ final class DefaultSampleValues {
+
+ public final int sampleDescriptionIndex;
+ public final int duration;
+ public final int size;
+ public final int flags;
+
+ public DefaultSampleValues(int sampleDescriptionIndex, int duration, int size, int flags) {
+ this.sampleDescriptionIndex = sampleDescriptionIndex;
+ this.duration = duration;
+ this.size = size;
+ this.flags = flags;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java
new file mode 100644
index 00000000000..2c46b5a93b5
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java
@@ -0,0 +1,1269 @@
+/*
+ * 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.mp4;
+
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.ParserException;
+import com.google.android.exoplayer.SampleHolder;
+import com.google.android.exoplayer.parser.SegmentIndex;
+import com.google.android.exoplayer.parser.mp4.Atom.ContainerAtom;
+import com.google.android.exoplayer.parser.mp4.Atom.LeafAtom;
+import com.google.android.exoplayer.upstream.NonBlockingInputStream;
+import com.google.android.exoplayer.util.Assertions;
+import com.google.android.exoplayer.util.MimeTypes;
+
+import android.annotation.SuppressLint;
+import android.media.MediaCodec;
+import android.media.MediaExtractor;
+import android.util.Pair;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Stack;
+import java.util.UUID;
+
+/**
+ * Facilitates the extraction of data from the fragmented mp4 container format.
+ *
+ * This implementation only supports de-muxed (i.e. single track) streams.
+ */
+public final class FragmentedMp4Extractor {
+
+ /**
+ * An attempt to read from the input stream returned 0 bytes of data.
+ */
+ public static final int RESULT_NEED_MORE_DATA = 1;
+ /**
+ * The end of the input stream was reached.
+ */
+ public static final int RESULT_END_OF_STREAM = 2;
+ /**
+ * A media sample was read.
+ */
+ public static final int RESULT_READ_SAMPLE_FULL = 4;
+ /**
+ * A media sample was partially read.
+ */
+ public static final int RESULT_READ_SAMPLE_PARTIAL = 8;
+ /**
+ * A moov atom was read. The parsed data can be read using {@link #getTrack()},
+ * {@link #getFormat()} and {@link #getPsshInfo}.
+ */
+ public static final int RESULT_READ_MOOV = 16;
+ /**
+ * A sidx atom was read. The parsed data can be read using {@link #getSegmentIndex()}.
+ */
+ public static final int RESULT_READ_SIDX = 32;
+
+ private static final int READ_TERMINATING_RESULTS = RESULT_NEED_MORE_DATA | RESULT_END_OF_STREAM
+ | RESULT_READ_SAMPLE_FULL;
+ private static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1};
+ private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE =
+ new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12};
+
+ // Parser states
+ private static final int STATE_READING_ATOM_HEADER = 0;
+ private static final int STATE_READING_ATOM_PAYLOAD = 1;
+ private static final int STATE_READING_CENC_AUXILIARY_DATA = 2;
+ private static final int STATE_READING_SAMPLE_START = 3;
+ private static final int STATE_READING_SAMPLE_INCREMENTAL = 4;
+
+ // Atom data offsets
+ private static final int ATOM_HEADER_SIZE = 8;
+ private static final int FULL_ATOM_HEADER_SIZE = 12;
+
+ // Atoms that the parser cares about
+ private static final Set PARSED_ATOMS;
+ static {
+ HashSet parsedAtoms = new HashSet();
+ parsedAtoms.add(Atom.TYPE_avc1);
+ parsedAtoms.add(Atom.TYPE_esds);
+ parsedAtoms.add(Atom.TYPE_hdlr);
+ parsedAtoms.add(Atom.TYPE_mdat);
+ parsedAtoms.add(Atom.TYPE_mdhd);
+ parsedAtoms.add(Atom.TYPE_mfhd);
+ parsedAtoms.add(Atom.TYPE_moof);
+ parsedAtoms.add(Atom.TYPE_moov);
+ parsedAtoms.add(Atom.TYPE_mp4a);
+ parsedAtoms.add(Atom.TYPE_sidx);
+ parsedAtoms.add(Atom.TYPE_stsd);
+ parsedAtoms.add(Atom.TYPE_tfdt);
+ parsedAtoms.add(Atom.TYPE_tfhd);
+ parsedAtoms.add(Atom.TYPE_tkhd);
+ parsedAtoms.add(Atom.TYPE_traf);
+ parsedAtoms.add(Atom.TYPE_trak);
+ parsedAtoms.add(Atom.TYPE_trex);
+ parsedAtoms.add(Atom.TYPE_trun);
+ parsedAtoms.add(Atom.TYPE_mvex);
+ parsedAtoms.add(Atom.TYPE_mdia);
+ parsedAtoms.add(Atom.TYPE_minf);
+ parsedAtoms.add(Atom.TYPE_stbl);
+ parsedAtoms.add(Atom.TYPE_pssh);
+ parsedAtoms.add(Atom.TYPE_saiz);
+ parsedAtoms.add(Atom.TYPE_uuid);
+ PARSED_ATOMS = Collections.unmodifiableSet(parsedAtoms);
+ }
+
+ // Atoms that the parser considers to be containers
+ private static final Set CONTAINER_TYPES;
+ static {
+ HashSet atomContainerTypes = new HashSet();
+ atomContainerTypes.add(Atom.TYPE_moov);
+ atomContainerTypes.add(Atom.TYPE_trak);
+ atomContainerTypes.add(Atom.TYPE_mdia);
+ atomContainerTypes.add(Atom.TYPE_minf);
+ atomContainerTypes.add(Atom.TYPE_stbl);
+ atomContainerTypes.add(Atom.TYPE_avcC);
+ atomContainerTypes.add(Atom.TYPE_moof);
+ atomContainerTypes.add(Atom.TYPE_traf);
+ atomContainerTypes.add(Atom.TYPE_mvex);
+ CONTAINER_TYPES = Collections.unmodifiableSet(atomContainerTypes);
+ }
+
+ private final boolean enableSmoothStreamingWorkarounds;
+
+ // Parser state
+ private final ParsableByteArray atomHeader;
+ private final Stack containerAtoms;
+ private final Stack containerAtomEndPoints;
+
+ private int parserState;
+ private int atomBytesRead;
+ private int rootAtomBytesRead;
+ private int atomType;
+ private int atomSize;
+ private ParsableByteArray atomData;
+ private ParsableByteArray cencAuxiliaryData;
+ private int cencAuxiliaryBytesRead;
+ private int sampleBytesRead;
+
+ private int pendingSeekTimeMs;
+ private int sampleIndex;
+ private int pendingSeekSyncSampleIndex;
+ private int lastSyncSampleIndex;
+
+ // Data parsed from moov and sidx atoms
+ private final HashMap psshData;
+ private SegmentIndex segmentIndex;
+ private Track track;
+ private DefaultSampleValues extendsDefaults;
+
+ // Data parsed from the most recent moof atom
+ private TrackFragment fragmentRun;
+
+ public FragmentedMp4Extractor() {
+ this(false);
+ }
+
+ /**
+ * @param enableSmoothStreamingWorkarounds Set to true if this extractor will be used to parse
+ * SmoothStreaming streams. This will enable workarounds for SmoothStreaming violations of
+ * the ISO base media file format (ISO 14496-12). Set to false otherwise.
+ */
+ public FragmentedMp4Extractor(boolean enableSmoothStreamingWorkarounds) {
+ this.enableSmoothStreamingWorkarounds = enableSmoothStreamingWorkarounds;
+ parserState = STATE_READING_ATOM_HEADER;
+ atomHeader = new ParsableByteArray(ATOM_HEADER_SIZE);
+ containerAtoms = new Stack();
+ containerAtomEndPoints = new Stack();
+ psshData = new HashMap();
+ }
+
+ /**
+ * Returns the segment index parsed from the stream.
+ *
+ * @return The segment index, or null if a SIDX atom has yet to be parsed.
+ */
+ public SegmentIndex getSegmentIndex() {
+ return segmentIndex;
+ }
+
+ /**
+ * Returns the pssh information parsed from the stream.
+ *
+ * @return The pssh information. May be null if the MOOV atom has yet to be parsed of if it did
+ * not contain any pssh information.
+ */
+ public Map getPsshInfo() {
+ return psshData.isEmpty() ? null : psshData;
+ }
+
+ /**
+ * Sideloads pssh information into the extractor, so that it can be read through
+ * {@link #getPsshInfo()}.
+ *
+ * @param uuid The UUID of the scheme for which information is being sideloaded.
+ * @param data The corresponding data.
+ */
+ public void putPsshInfo(UUID uuid, byte[] data) {
+ // TODO: This is for SmoothStreaming. Consider using something other than
+ // FragmentedMp4Extractor.getPsshInfo to obtain the pssh data for that use case, so that we can
+ // remove this method.
+ psshData.put(uuid, data);
+ }
+
+ /**
+ * Returns the format of the samples contained within the media stream.
+ *
+ * @return The sample media format, or null if a MOOV atom has yet to be parsed.
+ */
+ public MediaFormat getFormat() {
+ return track == null ? null : track.mediaFormat;
+ }
+
+ /**
+ * Returns the track information parsed from the stream.
+ *
+ * @return The track, or null if a MOOV atom has yet to be parsed.
+ */
+ public Track getTrack() {
+ return track;
+ }
+
+ /**
+ * Sideloads track information into the extractor, so that it can be read through
+ * {@link #getTrack()}.
+ *
+ * @param track The track to sideload.
+ */
+ public void setTrack(Track track) {
+ this.extendsDefaults = new DefaultSampleValues(0, 0, 0, 0);
+ this.track = track;
+ }
+
+ /**
+ * Consumes data from a {@link NonBlockingInputStream}.
+ *
+ * The read terminates if the end of the input stream is reached, if an attempt to read from the
+ * input stream returned 0 bytes of data, or if a sample is read. The returned flags indicate
+ * both the reason for termination and data that was parsed during the read.
+ *
+ * If the returned flags include {@link #RESULT_READ_SAMPLE_PARTIAL} then the sample has been
+ * partially read into {@code out}. Hence the same {@link SampleHolder} instance must be passed
+ * in subsequent calls until the whole sample has been read.
+ *
+ * @param inputStream The input stream from which data should be read.
+ * @param out A {@link SampleHolder} into which the sample should be read.
+ * @return One or more of the {@code RESULT_*} flags defined in this class.
+ * @throws ParserException If an error occurs parsing the media data.
+ */
+ public int read(NonBlockingInputStream inputStream, SampleHolder out)
+ throws ParserException {
+ try {
+ int results = 0;
+ while ((results & READ_TERMINATING_RESULTS) == 0) {
+ switch (parserState) {
+ case STATE_READING_ATOM_HEADER:
+ results |= readAtomHeader(inputStream);
+ break;
+ case STATE_READING_ATOM_PAYLOAD:
+ results |= readAtomPayload(inputStream);
+ break;
+ case STATE_READING_CENC_AUXILIARY_DATA:
+ results |= readCencAuxiliaryData(inputStream);
+ break;
+ default:
+ results |= readOrSkipSample(inputStream, out);
+ break;
+ }
+ }
+ return results;
+ } catch (Exception e) {
+ throw new ParserException(e);
+ }
+ }
+
+ /**
+ * Seeks to a position before or equal to the requested time.
+ *
+ * @param seekTimeUs The desired seek time in microseconds.
+ * @param allowNoop Allow the seek operation to do nothing if the seek time is in the current
+ * fragment run, is equal to or greater than the time of the current sample, and if there
+ * does not exist a sync frame between these two times.
+ * @return True if the operation resulted in a change of state. False if it was a no-op.
+ */
+ public boolean seekTo(long seekTimeUs, boolean allowNoop) {
+ pendingSeekTimeMs = (int) (seekTimeUs / 1000);
+ if (allowNoop && fragmentRun != null
+ && pendingSeekTimeMs >= fragmentRun.getSamplePresentationTime(0)
+ && pendingSeekTimeMs <= fragmentRun.getSamplePresentationTime(fragmentRun.length - 1)) {
+ int sampleIndexFound = 0;
+ int syncSampleIndexFound = 0;
+ for (int i = 0; i < fragmentRun.length; i++) {
+ if (fragmentRun.getSamplePresentationTime(i) <= pendingSeekTimeMs) {
+ if (fragmentRun.sampleIsSyncFrameTable[i]) {
+ syncSampleIndexFound = i;
+ }
+ sampleIndexFound = i;
+ }
+ }
+ if (syncSampleIndexFound == lastSyncSampleIndex && sampleIndexFound >= sampleIndex) {
+ pendingSeekTimeMs = 0;
+ return false;
+ }
+ }
+ containerAtoms.clear();
+ containerAtomEndPoints.clear();
+ enterState(STATE_READING_ATOM_HEADER);
+ return true;
+ }
+
+ private void enterState(int state) {
+ switch (state) {
+ case STATE_READING_ATOM_HEADER:
+ atomBytesRead = 0;
+ if (containerAtomEndPoints.isEmpty()) {
+ rootAtomBytesRead = 0;
+ }
+ break;
+ case STATE_READING_CENC_AUXILIARY_DATA:
+ cencAuxiliaryBytesRead = 0;
+ break;
+ case STATE_READING_SAMPLE_START:
+ sampleBytesRead = 0;
+ break;
+ }
+ parserState = state;
+ }
+
+ private int readAtomHeader(NonBlockingInputStream inputStream) {
+ int remainingBytes = ATOM_HEADER_SIZE - atomBytesRead;
+ int bytesRead = inputStream.read(atomHeader.getData(), atomBytesRead, remainingBytes);
+ if (bytesRead == -1) {
+ return RESULT_END_OF_STREAM;
+ }
+ rootAtomBytesRead += bytesRead;
+ atomBytesRead += bytesRead;
+ if (atomBytesRead != ATOM_HEADER_SIZE) {
+ return RESULT_NEED_MORE_DATA;
+ }
+
+ atomHeader.setPosition(0);
+ atomSize = atomHeader.readInt();
+ atomType = atomHeader.readInt();
+
+ if (atomType == Atom.TYPE_mdat) {
+ int cencAuxSize = fragmentRun.auxiliarySampleInfoTotalSize;
+ if (cencAuxSize > 0) {
+ cencAuxiliaryData = new ParsableByteArray(cencAuxSize);
+ enterState(STATE_READING_CENC_AUXILIARY_DATA);
+ } else {
+ cencAuxiliaryData = null;
+ enterState(STATE_READING_SAMPLE_START);
+ }
+ return 0;
+ }
+
+ if (PARSED_ATOMS.contains(atomType)) {
+ if (CONTAINER_TYPES.contains(atomType)) {
+ enterState(STATE_READING_ATOM_HEADER);
+ containerAtoms.add(new ContainerAtom(atomType));
+ containerAtomEndPoints.add(rootAtomBytesRead + atomSize - ATOM_HEADER_SIZE);
+ } else {
+ atomData = new ParsableByteArray(atomSize);
+ System.arraycopy(atomHeader.getData(), 0, atomData.getData(), 0, ATOM_HEADER_SIZE);
+ enterState(STATE_READING_ATOM_PAYLOAD);
+ }
+ } else {
+ atomData = null;
+ enterState(STATE_READING_ATOM_PAYLOAD);
+ }
+
+ return 0;
+ }
+
+ private int readAtomPayload(NonBlockingInputStream inputStream) {
+ int bytesRead;
+ if (atomData != null) {
+ bytesRead = inputStream.read(atomData.getData(), atomBytesRead, atomSize - atomBytesRead);
+ } else {
+ bytesRead = inputStream.skip(atomSize - atomBytesRead);
+ }
+ if (bytesRead == -1) {
+ return RESULT_END_OF_STREAM;
+ }
+ rootAtomBytesRead += bytesRead;
+ atomBytesRead += bytesRead;
+ if (atomBytesRead != atomSize) {
+ return RESULT_NEED_MORE_DATA;
+ }
+
+ int results = 0;
+ if (atomData != null) {
+ results |= onLeafAtomRead(new LeafAtom(atomType, atomData));
+ }
+
+ while (!containerAtomEndPoints.isEmpty()
+ && containerAtomEndPoints.peek() == rootAtomBytesRead) {
+ containerAtomEndPoints.pop();
+ results |= onContainerAtomRead(containerAtoms.pop());
+ }
+
+ enterState(STATE_READING_ATOM_HEADER);
+ return results;
+ }
+
+ private int onLeafAtomRead(LeafAtom leaf) {
+ if (!containerAtoms.isEmpty()) {
+ containerAtoms.peek().add(leaf);
+ } else if (leaf.type == Atom.TYPE_sidx) {
+ segmentIndex = parseSidx(leaf.getData());
+ return RESULT_READ_SIDX;
+ }
+ return 0;
+ }
+
+ private int onContainerAtomRead(ContainerAtom container) {
+ if (container.type == Atom.TYPE_moov) {
+ onMoovContainerAtomRead(container);
+ return RESULT_READ_MOOV;
+ } else if (container.type == Atom.TYPE_moof) {
+ onMoofContainerAtomRead(container);
+ } else if (!containerAtoms.isEmpty()) {
+ containerAtoms.peek().add(container);
+ }
+ return 0;
+ }
+
+ private void onMoovContainerAtomRead(ContainerAtom moov) {
+ List moovChildren = moov.getChildren();
+ for (int i = 0; i < moovChildren.size(); i++) {
+ Atom child = moovChildren.get(i);
+ if (child.type == Atom.TYPE_pssh) {
+ ParsableByteArray psshAtom = ((LeafAtom) child).getData();
+ psshAtom.setPosition(FULL_ATOM_HEADER_SIZE);
+ UUID uuid = new UUID(psshAtom.readLong(), psshAtom.readLong());
+ int dataSize = psshAtom.readInt();
+ byte[] data = new byte[dataSize];
+ psshAtom.readBytes(data, 0, dataSize);
+ psshData.put(uuid, data);
+ }
+ }
+ ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex);
+ extendsDefaults = parseTrex(mvex.getLeafAtomOfType(Atom.TYPE_trex).getData());
+ track = parseTrak(moov.getContainerAtomOfType(Atom.TYPE_trak));
+ }
+
+ private void onMoofContainerAtomRead(ContainerAtom moof) {
+ fragmentRun = new TrackFragment();
+ parseMoof(track, extendsDefaults, moof, fragmentRun, enableSmoothStreamingWorkarounds);
+ sampleIndex = 0;
+ lastSyncSampleIndex = 0;
+ pendingSeekSyncSampleIndex = 0;
+ if (pendingSeekTimeMs != 0) {
+ for (int i = 0; i < fragmentRun.length; i++) {
+ if (fragmentRun.sampleIsSyncFrameTable[i]) {
+ if (fragmentRun.getSamplePresentationTime(i) <= pendingSeekTimeMs) {
+ pendingSeekSyncSampleIndex = i;
+ }
+ }
+ }
+ pendingSeekTimeMs = 0;
+ }
+ }
+
+ /**
+ * Parses a trex atom (defined in 14496-12).
+ */
+ private static DefaultSampleValues parseTrex(ParsableByteArray trex) {
+ trex.setPosition(FULL_ATOM_HEADER_SIZE + 4);
+ int defaultSampleDescriptionIndex = trex.readUnsignedIntToInt() - 1;
+ int defaultSampleDuration = trex.readUnsignedIntToInt();
+ int defaultSampleSize = trex.readUnsignedIntToInt();
+ int defaultSampleFlags = trex.readInt();
+ return new DefaultSampleValues(defaultSampleDescriptionIndex, defaultSampleDuration,
+ defaultSampleSize, defaultSampleFlags);
+ }
+
+ /**
+ * Parses a trak atom (defined in 14496-12).
+ */
+ private static Track parseTrak(ContainerAtom trak) {
+ ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia);
+ int trackType = parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).getData());
+ Assertions.checkState(trackType == Track.TYPE_AUDIO || trackType == Track.TYPE_VIDEO);
+
+ Pair header = parseTkhd(trak.getLeafAtomOfType(Atom.TYPE_tkhd).getData());
+ int id = header.first;
+ // TODO: This value should be used to set a duration field on the Track object
+ // instantiated below, however we've found examples where the value is 0. Revisit whether we
+ // should set it anyway (and just have it be wrong for bad media streams).
+ // long duration = header.second;
+ long timescale = parseMdhd(mdia.getLeafAtomOfType(Atom.TYPE_mdhd).getData());
+ ContainerAtom stbl = mdia.getContainerAtomOfType(Atom.TYPE_minf)
+ .getContainerAtomOfType(Atom.TYPE_stbl);
+
+ Pair sampleDescriptions =
+ parseStsd(stbl.getLeafAtomOfType(Atom.TYPE_stsd).getData());
+ return new Track(id, trackType, timescale, sampleDescriptions.first, sampleDescriptions.second);
+ }
+
+ /**
+ * Parses a tkhd atom (defined in 14496-12).
+ *
+ * @return A {@link Pair} consisting of the track id and duration.
+ */
+ private static Pair parseTkhd(ParsableByteArray tkhd) {
+ tkhd.setPosition(ATOM_HEADER_SIZE);
+ int fullAtom = tkhd.readInt();
+ int version = parseFullAtomVersion(fullAtom);
+
+ tkhd.skip(version == 0 ? 8 : 16);
+
+ int trackId = tkhd.readInt();
+ tkhd.skip(4);
+ long duration = version == 0 ? tkhd.readUnsignedInt() : tkhd.readUnsignedLongToLong();
+
+ return Pair.create(trackId, duration);
+ }
+
+ /**
+ * Parses an hdlr atom (defined in 14496-12).
+ *
+ * @param hdlr The hdlr atom to parse.
+ * @return The track type.
+ */
+ private static int parseHdlr(ParsableByteArray hdlr) {
+ hdlr.setPosition(FULL_ATOM_HEADER_SIZE + 4);
+ return hdlr.readInt();
+ }
+
+ /**
+ * Parses an mdhd atom (defined in 14496-12).
+ *
+ * @param mdhd The mdhd atom to parse.
+ * @return The media timescale, defined as the number of time units that pass in one second.
+ */
+ private static long parseMdhd(ParsableByteArray mdhd) {
+ mdhd.setPosition(ATOM_HEADER_SIZE);
+ int fullAtom = mdhd.readInt();
+ int version = parseFullAtomVersion(fullAtom);
+
+ mdhd.skip(version == 0 ? 8 : 16);
+ return mdhd.readUnsignedInt();
+ }
+
+ private static Pair parseStsd(ParsableByteArray stsd) {
+ stsd.setPosition(FULL_ATOM_HEADER_SIZE);
+ int numberOfEntries = stsd.readInt();
+ MediaFormat mediaFormat = null;
+ TrackEncryptionBox[] trackEncryptionBoxes = new TrackEncryptionBox[numberOfEntries];
+ for (int i = 0; i < numberOfEntries; i++) {
+ int childStartPosition = stsd.getPosition();
+ int childAtomSize = stsd.readInt();
+ int childAtomType = stsd.readInt();
+ if (childAtomType == Atom.TYPE_avc1 || childAtomType == Atom.TYPE_encv) {
+ Pair avc1 =
+ parseAvc1FromParent(stsd, childStartPosition, childAtomSize);
+ mediaFormat = avc1.first;
+ trackEncryptionBoxes[i] = avc1.second;
+ } else if (childAtomType == Atom.TYPE_mp4a || childAtomType == Atom.TYPE_enca) {
+ Pair mp4a =
+ parseMp4aFromParent(stsd, childStartPosition, childAtomSize);
+ mediaFormat = mp4a.first;
+ trackEncryptionBoxes[i] = mp4a.second;
+ }
+ stsd.setPosition(childStartPosition + childAtomSize);
+ }
+ return Pair.create(mediaFormat, trackEncryptionBoxes);
+ }
+
+ private static Pair parseAvc1FromParent(ParsableByteArray parent,
+ int position, int size) {
+ parent.setPosition(position + ATOM_HEADER_SIZE);
+
+ parent.skip(24);
+ int width = parent.readUnsignedShort();
+ int height = parent.readUnsignedShort();
+ parent.skip(50);
+
+ List initializationData = null;
+ TrackEncryptionBox trackEncryptionBox = null;
+ int childPosition = parent.getPosition();
+ while (childPosition - position < size) {
+ parent.setPosition(childPosition);
+ int childStartPosition = parent.getPosition();
+ int childAtomSize = parent.readInt();
+ int childAtomType = parent.readInt();
+ if (childAtomType == Atom.TYPE_avcC) {
+ initializationData = parseAvcCFromParent(parent, childStartPosition);
+ } else if (childAtomType == Atom.TYPE_sinf) {
+ trackEncryptionBox = parseSinfFromParent(parent, childStartPosition, childAtomSize);
+ }
+ childPosition += childAtomSize;
+ }
+
+ MediaFormat format = MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE,
+ width, height, initializationData);
+ return Pair.create(format, trackEncryptionBox);
+ }
+
+ private static Pair parseMp4aFromParent(ParsableByteArray parent,
+ int position, int size) {
+ parent.setPosition(position + ATOM_HEADER_SIZE);
+ // Start of the mp4a atom (defined in 14496-14)
+ parent.skip(16);
+ int channelCount = parent.readUnsignedShort();
+ int sampleSize = parent.readUnsignedShort();
+ parent.skip(4);
+ int sampleRate = parent.readUnsignedFixedPoint1616();
+
+ byte[] initializationData = null;
+ TrackEncryptionBox trackEncryptionBox = null;
+ int childPosition = parent.getPosition();
+ while (childPosition - position < size) {
+ parent.setPosition(childPosition);
+ int childStartPosition = parent.getPosition();
+ int childAtomSize = parent.readInt();
+ int childAtomType = parent.readInt();
+ if (childAtomType == Atom.TYPE_esds) {
+ initializationData = parseEsdsFromParent(parent, childStartPosition);
+ // TODO: Do we really need to do this? See [redacted]
+ // Update sampleRate and sampleRate from the AudioSpecificConfig initialization data.
+ Pair audioSpecificConfig =
+ CodecSpecificDataUtil.parseAudioSpecificConfig(initializationData);
+ sampleRate = audioSpecificConfig.first;
+ channelCount = audioSpecificConfig.second;
+ } else if (childAtomType == Atom.TYPE_sinf) {
+ trackEncryptionBox = parseSinfFromParent(parent, childStartPosition, childAtomSize);
+ }
+ childPosition += childAtomSize;
+ }
+
+ MediaFormat format = MediaFormat.createAudioFormat("audio/mp4a-latm", sampleSize, channelCount,
+ sampleRate, Collections.singletonList(initializationData));
+ return Pair.create(format, trackEncryptionBox);
+ }
+
+ private static List parseAvcCFromParent(ParsableByteArray parent, int position) {
+ parent.setPosition(position + ATOM_HEADER_SIZE + 4);
+ // Start of the AVCDecoderConfigurationRecord (defined in 14496-15)
+ int nalUnitLength = (parent.readUnsignedByte() & 0x3) + 1;
+ if (nalUnitLength != 4) {
+ // readSample currently relies on a nalUnitLength of 4.
+ // TODO: Consider handling the case where it isn't.
+ throw new IllegalStateException();
+ }
+ List initializationData = new ArrayList();
+ // TODO: We should try and parse these using CodecSpecificDataUtil.parseSpsNalUnit, and
+ // expose the AVC profile and level somewhere useful; Most likely in MediaFormat.
+ int numSequenceParameterSets = parent.readUnsignedByte() & 0x1F;
+ for (int j = 0; j < numSequenceParameterSets; j++) {
+ initializationData.add(parseChildNalUnit(parent));
+ }
+ int numPictureParamterSets = parent.readUnsignedByte();
+ for (int j = 0; j < numPictureParamterSets; j++) {
+ initializationData.add(parseChildNalUnit(parent));
+ }
+ return initializationData;
+ }
+
+ private static byte[] parseChildNalUnit(ParsableByteArray atom) {
+ int length = atom.readUnsignedShort();
+ int offset = atom.getPosition();
+ atom.skip(length);
+ return CodecSpecificDataUtil.buildNalUnit(atom.getData(), offset, length);
+ }
+
+ private static TrackEncryptionBox parseSinfFromParent(ParsableByteArray parent, int position,
+ int size) {
+ int childPosition = position + ATOM_HEADER_SIZE;
+
+ TrackEncryptionBox trackEncryptionBox = null;
+ while (childPosition - position < size) {
+ parent.setPosition(childPosition);
+ int childAtomSize = parent.readInt();
+ int childAtomType = parent.readInt();
+ if (childAtomType == Atom.TYPE_frma) {
+ parent.readInt(); // dataFormat. Expect TYPE_avc1 (video) or TYPE_mp4a (audio).
+ } else if (childAtomType == Atom.TYPE_schm) {
+ parent.skip(4);
+ parent.readInt(); // schemeType. Expect cenc
+ parent.readInt(); // schemeVersion. Expect 0x00010000
+ } else if (childAtomType == Atom.TYPE_schi) {
+ trackEncryptionBox = parseSchiFromParent(parent, childPosition, childAtomSize);
+ }
+ childPosition += childAtomSize;
+ }
+
+ return trackEncryptionBox;
+ }
+
+ private static TrackEncryptionBox parseSchiFromParent(ParsableByteArray parent, int position,
+ int size) {
+ int childPosition = position + ATOM_HEADER_SIZE;
+ while (childPosition - position < size) {
+ parent.setPosition(childPosition);
+ int childAtomSize = parent.readInt();
+ int childAtomType = parent.readInt();
+ if (childAtomType == Atom.TYPE_tenc) {
+ parent.skip(4);
+ int firstInt = parent.readInt();
+ boolean defaultIsEncrypted = (firstInt >> 8) == 1;
+ int defaultInitVectorSize = firstInt & 0xFF;
+ byte[] defaultKeyId = new byte[16];
+ parent.readBytes(defaultKeyId, 0, defaultKeyId.length);
+ return new TrackEncryptionBox(defaultIsEncrypted, defaultInitVectorSize, defaultKeyId);
+ }
+ childPosition += childAtomSize;
+ }
+ return null;
+ }
+
+ private static byte[] parseEsdsFromParent(ParsableByteArray parent, int position) {
+ parent.setPosition(position + ATOM_HEADER_SIZE + 4);
+ // Start of the ES_Descriptor (defined in 14496-1)
+ parent.skip(1); // ES_Descriptor tag
+ int varIntByte = parent.readUnsignedByte();
+ while (varIntByte > 127) {
+ varIntByte = parent.readUnsignedByte();
+ }
+ parent.skip(2); // ES_ID
+
+ int flags = parent.readUnsignedByte();
+ if ((flags & 0x80 /* streamDependenceFlag */) != 0) {
+ parent.skip(2);
+ }
+ if ((flags & 0x40 /* URL_Flag */) != 0) {
+ parent.skip(parent.readUnsignedShort());
+ }
+ if ((flags & 0x20 /* OCRstreamFlag */) != 0) {
+ parent.skip(2);
+ }
+
+ // Start of the DecoderConfigDescriptor (defined in 14496-1)
+ parent.skip(1); // DecoderConfigDescriptor tag
+ varIntByte = parent.readUnsignedByte();
+ while (varIntByte > 127) {
+ varIntByte = parent.readUnsignedByte();
+ }
+ parent.skip(13);
+
+ // Start of AudioSpecificConfig (defined in 14496-3)
+ parent.skip(1); // AudioSpecificConfig tag
+ varIntByte = parent.readUnsignedByte();
+ int varInt = varIntByte & 0x7F;
+ while (varIntByte > 127) {
+ varIntByte = parent.readUnsignedByte();
+ varInt = varInt << 8;
+ varInt |= varIntByte & 0x7F;
+ }
+ byte[] initializationData = new byte[varInt];
+ parent.readBytes(initializationData, 0, varInt);
+ return initializationData;
+ }
+
+ private static void parseMoof(Track track, DefaultSampleValues extendsDefaults,
+ ContainerAtom moof, TrackFragment out, boolean enableSmoothStreamingWorkarounds) {
+ // TODO: Consider checking that the sequence number returned by parseMfhd is as expected.
+ parseMfhd(moof.getLeafAtomOfType(Atom.TYPE_mfhd).getData());
+ parseTraf(track, extendsDefaults, moof.getContainerAtomOfType(Atom.TYPE_traf),
+ out, enableSmoothStreamingWorkarounds);
+ }
+
+ /**
+ * Parses an mfhd atom (defined in 14496-12).
+ *
+ * @param mfhd The mfhd atom to parse.
+ * @return The sequence number of the fragment.
+ */
+ private static int parseMfhd(ParsableByteArray mfhd) {
+ mfhd.setPosition(FULL_ATOM_HEADER_SIZE);
+ return mfhd.readUnsignedIntToInt();
+ }
+
+ /**
+ * Parses a traf atom (defined in 14496-12).
+ */
+ private static void parseTraf(Track track, DefaultSampleValues extendsDefaults,
+ ContainerAtom traf, TrackFragment out, boolean enableSmoothStreamingWorkarounds) {
+ LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz);
+ if (saiz != null) {
+ parseSaiz(saiz.getData(), out);
+ }
+ LeafAtom tfdtAtom = traf.getLeafAtomOfType(Atom.TYPE_tfdt);
+ long decodeTime = tfdtAtom == null ? 0
+ : parseTfdt(traf.getLeafAtomOfType(Atom.TYPE_tfdt).getData());
+ LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd);
+ DefaultSampleValues fragmentHeader = parseTfhd(extendsDefaults, tfhd.getData());
+ out.setSampleDescriptionIndex(fragmentHeader.sampleDescriptionIndex);
+
+ LeafAtom trun = traf.getLeafAtomOfType(Atom.TYPE_trun);
+ parseTrun(track, fragmentHeader, decodeTime, enableSmoothStreamingWorkarounds, trun.getData(),
+ out);
+ LeafAtom uuid = traf.getLeafAtomOfType(Atom.TYPE_uuid);
+ if (uuid != null) {
+ parseUuid(uuid.getData(), out);
+ }
+ }
+
+ private static void parseSaiz(ParsableByteArray saiz, TrackFragment out) {
+ saiz.setPosition(ATOM_HEADER_SIZE);
+ int fullAtom = saiz.readInt();
+ int flags = parseFullAtomFlags(fullAtom);
+ if ((flags & 0x01) == 1) {
+ saiz.skip(8);
+ }
+ int defaultSampleInfoSize = saiz.readUnsignedByte();
+ int sampleCount = saiz.readUnsignedIntToInt();
+ int totalSize = 0;
+ int[] sampleInfoSizes = new int[sampleCount];
+ if (defaultSampleInfoSize == 0) {
+ for (int i = 0; i < sampleCount; i++) {
+ sampleInfoSizes[i] = saiz.readUnsignedByte();
+ totalSize += sampleInfoSizes[i];
+ }
+ } else {
+ for (int i = 0; i < sampleCount; i++) {
+ sampleInfoSizes[i] = defaultSampleInfoSize;
+ totalSize += defaultSampleInfoSize;
+ }
+ }
+ out.setAuxiliarySampleInfoTables(totalSize, sampleInfoSizes);
+ }
+
+ /**
+ * Parses a tfhd atom (defined in 14496-12).
+ *
+ * @param extendsDefaults Default sample values from the trex atom.
+ * @return The parsed default sample values.
+ */
+ private static DefaultSampleValues parseTfhd(DefaultSampleValues extendsDefaults,
+ ParsableByteArray tfhd) {
+ tfhd.setPosition(ATOM_HEADER_SIZE);
+ int fullAtom = tfhd.readInt();
+ int flags = parseFullAtomFlags(fullAtom);
+
+ tfhd.skip(4); // trackId
+ if ((flags & 0x01 /* base_data_offset_present */) != 0) {
+ tfhd.skip(8);
+ }
+
+ int defaultSampleDescriptionIndex =
+ ((flags & 0x02 /* default_sample_description_index_present */) != 0) ?
+ tfhd.readUnsignedIntToInt() - 1 : extendsDefaults.sampleDescriptionIndex;
+ int defaultSampleDuration = ((flags & 0x08 /* default_sample_duration_present */) != 0) ?
+ tfhd.readUnsignedIntToInt() : extendsDefaults.duration;
+ int defaultSampleSize = ((flags & 0x10 /* default_sample_size_present */) != 0) ?
+ tfhd.readUnsignedIntToInt() : extendsDefaults.size;
+ int defaultSampleFlags = ((flags & 0x20 /* default_sample_flags_present */) != 0) ?
+ tfhd.readUnsignedIntToInt() : extendsDefaults.flags;
+ return new DefaultSampleValues(defaultSampleDescriptionIndex, defaultSampleDuration,
+ defaultSampleSize, defaultSampleFlags);
+ }
+
+ /**
+ * Parses a tfdt atom (defined in 14496-12).
+ *
+ * @return baseMediaDecodeTime. The sum of the decode durations of all earlier samples in the
+ * media, expressed in the media's timescale.
+ */
+ private static long parseTfdt(ParsableByteArray tfdt) {
+ tfdt.setPosition(ATOM_HEADER_SIZE);
+ int fullAtom = tfdt.readInt();
+ int version = parseFullAtomVersion(fullAtom);
+ return version == 1 ? tfdt.readUnsignedLongToLong() : tfdt.readUnsignedInt();
+ }
+
+ /**
+ * Parses a trun atom (defined in 14496-12).
+ *
+ * @param track The corresponding track.
+ * @param defaultSampleValues Default sample values.
+ * @param decodeTime The decode time.
+ * @param trun The trun atom to parse.
+ * @param out The {@TrackFragment} into which parsed data should be placed.
+ */
+ private static void parseTrun(Track track, DefaultSampleValues defaultSampleValues,
+ long decodeTime, boolean enableSmoothStreamingWorkarounds, ParsableByteArray trun,
+ TrackFragment out) {
+ trun.setPosition(ATOM_HEADER_SIZE);
+ int fullAtom = trun.readInt();
+ int version = parseFullAtomVersion(fullAtom);
+ int flags = parseFullAtomFlags(fullAtom);
+
+ int numberOfEntries = trun.readUnsignedIntToInt();
+ if ((flags & 0x01 /* data_offset_present */) != 0) {
+ trun.skip(4);
+ }
+
+ boolean firstSampleFlagsPresent = (flags & 0x04 /* first_sample_flags_present */) != 0;
+ int firstSampleFlags = defaultSampleValues.flags;
+ if (firstSampleFlagsPresent) {
+ firstSampleFlags = trun.readUnsignedIntToInt();
+ }
+
+ boolean sampleDurationsPresent = (flags & 0x100 /* sample_duration_present */) != 0;
+ boolean sampleSizesPresent = (flags & 0x200 /* sample_size_present */) != 0;
+ boolean sampleFlagsPresent = (flags & 0x400 /* sample_flags_present */) != 0;
+ boolean sampleCompositionTimeOffsetsPresent =
+ (flags & 0x800 /* sample_composition_time_offsets_present */) != 0;
+
+ int[] sampleSizeTable = new int[numberOfEntries];
+ int[] sampleDecodingTimeTable = new int[numberOfEntries];
+ int[] sampleCompositionTimeOffsetTable = new int[numberOfEntries];
+ boolean[] sampleIsSyncFrameTable = new boolean[numberOfEntries];
+
+ long timescale = track.timescale;
+ long cumulativeTime = decodeTime;
+ for (int i = 0; i < numberOfEntries; i++) {
+ // Use trun values if present, otherwise tfhd, otherwise trex.
+ int sampleDuration = sampleDurationsPresent ? trun.readUnsignedIntToInt()
+ : defaultSampleValues.duration;
+ int sampleSize = sampleSizesPresent ? trun.readUnsignedIntToInt() : defaultSampleValues.size;
+ int sampleFlags = (i == 0 && firstSampleFlagsPresent) ? firstSampleFlags
+ : sampleFlagsPresent ? trun.readInt() : defaultSampleValues.flags;
+ if (sampleCompositionTimeOffsetsPresent) {
+ // Fragmented mp4 streams packaged for smooth streaming violate the BMFF spec by specifying
+ // the sample offset as a signed integer in conjunction with a box version of 0.
+ int sampleOffset;
+ if (version == 0 && !enableSmoothStreamingWorkarounds) {
+ sampleOffset = trun.readUnsignedIntToInt();
+ } else {
+ sampleOffset = trun.readInt();
+ }
+ sampleCompositionTimeOffsetTable[i] = (int) ((sampleOffset * 1000) / timescale);
+ }
+ sampleDecodingTimeTable[i] = (int) ((cumulativeTime * 1000) / timescale);
+ sampleSizeTable[i] = sampleSize;
+ boolean isSync = ((sampleFlags >> 16) & 0x1) == 0;
+ if (track.type == Track.TYPE_VIDEO && enableSmoothStreamingWorkarounds && i != 0) {
+ // Fragmented mp4 streams packaged for smooth streaming violate the BMFF spec by indicating
+ // that every sample is a sync frame, when this is not actually the case.
+ isSync = false;
+ }
+ if (isSync) {
+ sampleIsSyncFrameTable[i] = true;
+ }
+ cumulativeTime += sampleDuration;
+ }
+
+ out.setSampleTables(sampleSizeTable, sampleDecodingTimeTable, sampleCompositionTimeOffsetTable,
+ sampleIsSyncFrameTable);
+ }
+
+ private static void parseUuid(ParsableByteArray uuid, TrackFragment out) {
+ uuid.setPosition(ATOM_HEADER_SIZE);
+ byte[] extendedType = new byte[16];
+ uuid.readBytes(extendedType, 0, 16);
+
+ // Currently this parser only supports Microsoft's PIFF SampleEncryptionBox.
+ if (!Arrays.equals(extendedType, PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE)) {
+ return;
+ }
+
+ // See "Portable encoding of audio-video objects: The Protected Interoperable File Format
+ // (PIFF), John A. Bocharov et al, Section 5.3.2.1."
+ int fullAtom = uuid.readInt();
+ int flags = parseFullAtomFlags(fullAtom);
+
+ if ((flags & 0x01 /* override_track_encryption_box_parameters */) != 0) {
+ // TODO: Implement this.
+ throw new IllegalStateException("Overriding TrackEncryptionBox parameters is unsupported");
+ }
+
+ boolean subsampleEncryption = (flags & 0x02 /* use_subsample_encryption */) != 0;
+ int numberOfEntries = uuid.readUnsignedIntToInt();
+ if (numberOfEntries != out.length) {
+ throw new IllegalStateException("Length mismatch: " + numberOfEntries + ", " + out.length);
+ }
+
+ int sampleEncryptionDataLength = uuid.length() - uuid.getPosition();
+ ParsableByteArray sampleEncryptionData = new ParsableByteArray(sampleEncryptionDataLength);
+ uuid.readBytes(sampleEncryptionData.getData(), 0, sampleEncryptionData.length());
+ out.setSmoothStreamingSampleEncryptionData(sampleEncryptionData, subsampleEncryption);
+ }
+
+ /**
+ * Parses a sidx atom (defined in 14496-12).
+ */
+ private static SegmentIndex parseSidx(ParsableByteArray atom) {
+ atom.setPosition(ATOM_HEADER_SIZE);
+ int fullAtom = atom.readInt();
+ int version = parseFullAtomVersion(fullAtom);
+
+ atom.skip(4);
+ long timescale = atom.readUnsignedInt();
+ long earliestPresentationTime;
+ long firstOffset;
+ if (version == 0) {
+ earliestPresentationTime = atom.readUnsignedInt();
+ firstOffset = atom.readUnsignedInt();
+ } else {
+ earliestPresentationTime = atom.readUnsignedLongToLong();
+ firstOffset = atom.readUnsignedLongToLong();
+ }
+
+ atom.skip(2);
+
+ int referenceCount = atom.readUnsignedShort();
+ int[] sizes = new int[referenceCount];
+ long[] offsets = new long[referenceCount];
+ long[] durationsUs = new long[referenceCount];
+ long[] timesUs = new long[referenceCount];
+
+ long offset = firstOffset;
+ long time = earliestPresentationTime;
+ for (int i = 0; i < referenceCount; i++) {
+ int firstInt = atom.readInt();
+
+ int type = 0x80000000 & firstInt;
+ if (type != 0) {
+ throw new IllegalStateException("Unhandled indirect reference");
+ }
+ long referenceDuration = atom.readUnsignedInt();
+
+ sizes[i] = 0x7fffffff & firstInt;
+ offsets[i] = offset;
+
+ // Calculate time and duration values such that any rounding errors are consistent. i.e. That
+ // timesUs[i] + durationsUs[i] == timesUs[i + 1].
+ timesUs[i] = (time * 1000000L) / timescale;
+ long nextTimeUs = ((time + referenceDuration) * 1000000L) / timescale;
+ durationsUs[i] = nextTimeUs - timesUs[i];
+ time += referenceDuration;
+
+ atom.skip(4);
+ offset += sizes[i];
+ }
+
+ return new SegmentIndex(atom.length(), sizes, offsets, durationsUs, timesUs);
+ }
+
+ private int readCencAuxiliaryData(NonBlockingInputStream inputStream) {
+ int length = cencAuxiliaryData.length();
+ int bytesRead = inputStream.read(cencAuxiliaryData.getData(), cencAuxiliaryBytesRead,
+ length - cencAuxiliaryBytesRead);
+ if (bytesRead == -1) {
+ return RESULT_END_OF_STREAM;
+ }
+ cencAuxiliaryBytesRead += bytesRead;
+ if (cencAuxiliaryBytesRead < length) {
+ return RESULT_NEED_MORE_DATA;
+ }
+ enterState(STATE_READING_SAMPLE_START);
+ return 0;
+ }
+
+ /**
+ * Attempts to read or skip the next sample in the current mdat atom.
+ *
+ * If there are no more samples in the current mdat atom then the parser state is transitioned
+ * to {@link #STATE_READING_ATOM_HEADER} and 0 is returned.
+ *
+ * If there's a pending seek to a sync frame, and if the next sample is before that frame, then
+ * the sample is skipped. Otherwise it is read.
+ *
+ * It is possible for a sample to be read or skipped in part if there is insufficent data
+ * available from the {@link NonBlockingInputStream}. In this case the remainder of the sample
+ * can be read in a subsequent call passing the same {@link SampleHolder}.
+ *
+ * @param inputStream The stream from which to read the sample.
+ * @param out The holder into which to write the sample.
+ * @return A combination of RESULT_* flags indicating the result of the call.
+ */
+ private int readOrSkipSample(NonBlockingInputStream inputStream, SampleHolder out) {
+ if (sampleIndex >= fragmentRun.length) {
+ // We've run out of samples in the current mdat atom.
+ enterState(STATE_READING_ATOM_HEADER);
+ return 0;
+ }
+ if (sampleIndex < pendingSeekSyncSampleIndex) {
+ return skipSample(inputStream);
+ }
+ return readSample(inputStream, out);
+ }
+
+ private int skipSample(NonBlockingInputStream inputStream) {
+ if (parserState == STATE_READING_SAMPLE_START) {
+ ParsableByteArray sampleEncryptionData = cencAuxiliaryData != null ? cencAuxiliaryData
+ : fragmentRun.smoothStreamingSampleEncryptionData;
+ if (sampleEncryptionData != null) {
+ TrackEncryptionBox encryptionBox =
+ track.sampleDescriptionEncryptionBoxes[fragmentRun.sampleDescriptionIndex];
+ int vectorSize = encryptionBox.initializationVectorSize;
+ boolean subsampleEncryption = cencAuxiliaryData != null
+ ? fragmentRun.auxiliarySampleInfoSizeTable[sampleIndex] > vectorSize
+ : fragmentRun.smoothStreamingUsesSubsampleEncryption;
+ sampleEncryptionData.skip(vectorSize);
+ int subsampleCount = subsampleEncryption ? sampleEncryptionData.readUnsignedShort() : 1;
+ if (subsampleEncryption) {
+ sampleEncryptionData.skip((2 + 4) * subsampleCount);
+ }
+ }
+ }
+
+ int sampleSize = fragmentRun.sampleSizeTable[sampleIndex];
+ int bytesRead = inputStream.skip(sampleSize - sampleBytesRead);
+ if (bytesRead == -1) {
+ return RESULT_END_OF_STREAM;
+ }
+ sampleBytesRead += bytesRead;
+ if (sampleSize != sampleBytesRead) {
+ enterState(STATE_READING_SAMPLE_INCREMENTAL);
+ return RESULT_NEED_MORE_DATA;
+ }
+ sampleIndex++;
+ enterState(STATE_READING_SAMPLE_START);
+ return 0;
+ }
+
+ @SuppressLint("InlinedApi")
+ private int readSample(NonBlockingInputStream inputStream, SampleHolder out) {
+ int sampleSize = fragmentRun.sampleSizeTable[sampleIndex];
+ ByteBuffer outputData = out.data;
+ if (parserState == STATE_READING_SAMPLE_START) {
+ out.timeUs = fragmentRun.getSamplePresentationTime(sampleIndex) * 1000L;
+ out.flags = 0;
+ if (fragmentRun.sampleIsSyncFrameTable[sampleIndex]) {
+ out.flags |= MediaExtractor.SAMPLE_FLAG_SYNC;
+ lastSyncSampleIndex = sampleIndex;
+ }
+ if (out.allowDataBufferReplacement
+ && (out.data == null || out.data.capacity() < sampleSize)) {
+ outputData = ByteBuffer.allocate(sampleSize);
+ out.data = outputData;
+ }
+ ParsableByteArray sampleEncryptionData = cencAuxiliaryData != null ? cencAuxiliaryData
+ : fragmentRun.smoothStreamingSampleEncryptionData;
+ if (sampleEncryptionData != null) {
+ readSampleEncryptionData(sampleEncryptionData, out);
+ }
+ }
+
+ int bytesRead;
+ if (outputData == null) {
+ bytesRead = inputStream.skip(sampleSize - sampleBytesRead);
+ } else {
+ bytesRead = inputStream.read(outputData, sampleSize - sampleBytesRead);
+ }
+ if (bytesRead == -1) {
+ return RESULT_END_OF_STREAM;
+ }
+ sampleBytesRead += bytesRead;
+
+ if (sampleSize != sampleBytesRead) {
+ enterState(STATE_READING_SAMPLE_INCREMENTAL);
+ return RESULT_NEED_MORE_DATA | RESULT_READ_SAMPLE_PARTIAL;
+ }
+
+ if (outputData != null) {
+ if (track.type == Track.TYPE_VIDEO) {
+ // The mp4 file contains length-prefixed NAL units, but the decoder wants start code
+ // delimited content. Replace length prefixes with start codes.
+ int sampleOffset = outputData.position() - sampleSize;
+ int position = sampleOffset;
+ while (position < sampleOffset + sampleSize) {
+ outputData.position(position);
+ int length = readUnsignedIntToInt(outputData);
+ outputData.position(position);
+ outputData.put(NAL_START_CODE);
+ position += length + 4;
+ }
+ outputData.position(sampleOffset + sampleSize);
+ }
+ out.size = sampleSize;
+ } else {
+ out.size = 0;
+ }
+
+ sampleIndex++;
+ enterState(STATE_READING_SAMPLE_START);
+ return RESULT_READ_SAMPLE_FULL;
+ }
+
+ @SuppressLint("InlinedApi")
+ private void readSampleEncryptionData(ParsableByteArray sampleEncryptionData, SampleHolder out) {
+ TrackEncryptionBox encryptionBox =
+ track.sampleDescriptionEncryptionBoxes[fragmentRun.sampleDescriptionIndex];
+ byte[] keyId = encryptionBox.keyId;
+ boolean isEncrypted = encryptionBox.isEncrypted;
+ int vectorSize = encryptionBox.initializationVectorSize;
+ boolean subsampleEncryption = cencAuxiliaryData != null
+ ? fragmentRun.auxiliarySampleInfoSizeTable[sampleIndex] > vectorSize
+ : fragmentRun.smoothStreamingUsesSubsampleEncryption;
+
+ byte[] vector = out.cryptoInfo.iv;
+ if (vector == null || vector.length != 16) {
+ vector = new byte[16];
+ }
+ sampleEncryptionData.readBytes(vector, 0, vectorSize);
+
+ int subsampleCount = subsampleEncryption ? sampleEncryptionData.readUnsignedShort() : 1;
+ int[] clearDataSizes = out.cryptoInfo.numBytesOfClearData;
+ if (clearDataSizes == null || clearDataSizes.length < subsampleCount) {
+ clearDataSizes = new int[subsampleCount];
+ }
+ int[] encryptedDataSizes = out.cryptoInfo.numBytesOfEncryptedData;
+ if (encryptedDataSizes == null || encryptedDataSizes.length < subsampleCount) {
+ encryptedDataSizes = new int[subsampleCount];
+ }
+ if (subsampleEncryption) {
+ for (int i = 0; i < subsampleCount; i++) {
+ clearDataSizes[i] = sampleEncryptionData.readUnsignedShort();
+ encryptedDataSizes[i] = sampleEncryptionData.readUnsignedIntToInt();
+ }
+ } else {
+ clearDataSizes[0] = 0;
+ encryptedDataSizes[0] = fragmentRun.sampleSizeTable[sampleIndex];
+ }
+ out.cryptoInfo.set(subsampleCount, clearDataSizes, encryptedDataSizes, keyId, vector,
+ isEncrypted ? MediaCodec.CRYPTO_MODE_AES_CTR : MediaCodec.CRYPTO_MODE_UNENCRYPTED);
+ if (isEncrypted) {
+ out.flags |= MediaExtractor.SAMPLE_FLAG_ENCRYPTED;
+ }
+ }
+
+ /**
+ * Parses the version number out of the additional integer component of a full atom.
+ */
+ private static int parseFullAtomVersion(int fullAtomInt) {
+ return 0x000000FF & (fullAtomInt >> 24);
+ }
+
+ /**
+ * Parses the atom flags out of the additional integer component of a full atom.
+ */
+ private static int parseFullAtomFlags(int fullAtomInt) {
+ return 0x00FFFFFF & fullAtomInt;
+ }
+
+ /**
+ * Reads an unsigned integer into an integer. This method is suitable for use when it can be
+ * assumed that the top bit will always be set to zero.
+ *
+ * @throws IllegalArgumentException If the top bit of the input data is set.
+ */
+ private static int readUnsignedIntToInt(ByteBuffer data) {
+ int result = 0xFF & data.get();
+ for (int i = 1; i < 4; i++) {
+ result <<= 8;
+ result |= 0xFF & data.get();
+ }
+ if (result < 0) {
+ throw new IllegalArgumentException("Top bit not zero: " + result);
+ }
+ return result;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/ParsableByteArray.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/ParsableByteArray.java
new file mode 100644
index 00000000000..13027f01745
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/ParsableByteArray.java
@@ -0,0 +1,154 @@
+/*
+ * 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.mp4;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Wraps a byte array, providing a set of methods for parsing data from it. Numerical values are
+ * parsed with the assumption that their constituent bytes are in big endian order.
+ */
+/* package */ final class ParsableByteArray {
+
+ private final byte[] data;
+ private int position;
+
+ public ParsableByteArray(int length) {
+ this.data = new byte[length];
+ }
+
+ public byte[] getData() {
+ return data;
+ }
+
+ public int length() {
+ return data.length;
+ }
+
+ public int getPosition() {
+ return position;
+ }
+
+ public void setPosition(int position) {
+ this.position = position;
+ }
+
+ public void skip(int bytes) {
+ position += bytes;
+ }
+
+ public void rewind(int bytes) {
+ position -= bytes;
+ }
+
+ public void readBytes(byte[] buffer, int offset, int length) {
+ System.arraycopy(data, position, buffer, offset, length);
+ position += length;
+ }
+
+ public void readBytes(ByteBuffer buffer, int length) {
+ buffer.put(data, position, length);
+ position += length;
+ }
+
+ public int readUnsignedByte() {
+ int result = shiftIntoInt(data, position, 1);
+ position += 1;
+ return result;
+ }
+
+ public int readUnsignedShort() {
+ int result = shiftIntoInt(data, position, 2);
+ position += 2;
+ return result;
+ }
+
+ public long readUnsignedInt() {
+ long result = shiftIntoLong(data, position, 4);
+ position += 4;
+ return result;
+ }
+
+ public int readInt() {
+ int result = shiftIntoInt(data, position, 4);
+ position += 4;
+ return result;
+ }
+
+ public long readLong() {
+ long result = shiftIntoLong(data, position, 8);
+ position += 8;
+ return result;
+ }
+
+ /**
+ * @return The integer portion of a fixed point 16.16.
+ */
+ public int readUnsignedFixedPoint1616() {
+ int result = shiftIntoInt(data, position, 2);
+ position += 4;
+ return result;
+ }
+
+ /**
+ * Reads an unsigned integer into an integer. This method is suitable for use when it can be
+ * assumed that the top bit will always be set to zero.
+ *
+ * @throws IllegalArgumentException If the top bit of the input data is set.
+ */
+ public int readUnsignedIntToInt() {
+ int result = shiftIntoInt(data, position, 4);
+ position += 4;
+ if (result < 0) {
+ throw new IllegalArgumentException("Top bit not zero: " + result);
+ }
+ return result;
+ }
+
+ /**
+ * Reads an unsigned long into a long. This method is suitable for use when it can be
+ * assumed that the top bit will always be set to zero.
+ *
+ * @throws IllegalArgumentException If the top bit of the input data is set.
+ */
+ public long readUnsignedLongToLong() {
+ long result = shiftIntoLong(data, position, 8);
+ position += 8;
+ if (result < 0) {
+ throw new IllegalArgumentException("Top bit not zero: " + result);
+ }
+ return result;
+ }
+
+ private static int shiftIntoInt(byte[] bytes, int offset, int length) {
+ int result = 0xFF & bytes[offset];
+ for (int i = offset + 1; i < offset + length; i++) {
+ result <<= 8;
+ result |= 0xFF & bytes[i];
+ }
+ return result;
+ }
+
+ private static long shiftIntoLong(byte[] bytes, int offset, int length) {
+ long result = 0xFF & bytes[offset];
+ for (int i = offset + 1; i < offset + length; i++) {
+ result <<= 8;
+ result |= 0xFF & bytes[i];
+ }
+ return result;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/Track.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/Track.java
new file mode 100644
index 00000000000..710626bc2e4
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/Track.java
@@ -0,0 +1,76 @@
+/*
+ * 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.mp4;
+
+import com.google.android.exoplayer.MediaFormat;
+
+/**
+ * Encapsulates information describing an MP4 track.
+ */
+public final class Track {
+
+ /**
+ * Type of a video track.
+ */
+ public static final int TYPE_VIDEO = 0x76696465;
+ /**
+ * Type of an audio track.
+ */
+ public static final int TYPE_AUDIO = 0x736F756E;
+ /**
+ * Type of a hint track.
+ */
+ public static final int TYPE_HINT = 0x68696E74;
+ /**
+ * Type of a meta track.
+ */
+ public static final int TYPE_META = 0x6D657461;
+
+ /**
+ * The track identifier.
+ */
+ public final int id;
+
+ /**
+ * One of {@link #TYPE_VIDEO}, {@link #TYPE_AUDIO}, {@link #TYPE_HINT} and {@link #TYPE_META}.
+ */
+ public final int type;
+
+ /**
+ * The track timescale, defined as the number of time units that pass in one second.
+ */
+ public final long timescale;
+
+ /**
+ * The format if {@link #type} is {@link #TYPE_VIDEO} or {@link #TYPE_AUDIO}. Null otherwise.
+ */
+ public final MediaFormat mediaFormat;
+
+ /**
+ * Track encryption boxes for the different track sample descriptions. Entries may be null.
+ */
+ public final TrackEncryptionBox[] sampleDescriptionEncryptionBoxes;
+
+ public Track(int id, int type, long timescale, MediaFormat mediaFormat,
+ TrackEncryptionBox[] sampleDescriptionEncryptionBoxes) {
+ this.id = id;
+ this.type = type;
+ this.timescale = timescale;
+ this.mediaFormat = mediaFormat;
+ this.sampleDescriptionEncryptionBoxes = sampleDescriptionEncryptionBoxes;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/TrackEncryptionBox.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/TrackEncryptionBox.java
new file mode 100644
index 00000000000..2cc899ef713
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/TrackEncryptionBox.java
@@ -0,0 +1,50 @@
+/*
+ * 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.mp4;
+
+/**
+ * Encapsulates information parsed from a track encryption (tenc) box in an MP4 stream.
+ */
+public class TrackEncryptionBox {
+
+ /**
+ * Indicates the encryption state of the samples in the sample group.
+ */
+ public final boolean isEncrypted;
+
+ /**
+ * The initialization vector size in bytes for the samples in the corresponding sample group.
+ */
+ public final int initializationVectorSize;
+
+ /**
+ * The key identifier for the samples in the corresponding sample group.
+ */
+ public final byte[] keyId;
+
+ /**
+ * @param isEncrypted Indicates the encryption state of the samples in the sample group.
+ * @param initializationVectorSize The initialization vector size in bytes for the samples in the
+ * corresponding sample group.
+ * @param keyId The key identifier for the samples in the corresponding sample group.
+ */
+ public TrackEncryptionBox(boolean isEncrypted, int initializationVectorSize, byte[] keyId) {
+ this.isEncrypted = isEncrypted;
+ this.initializationVectorSize = initializationVectorSize;
+ this.keyId = keyId;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/TrackFragment.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/TrackFragment.java
new file mode 100644
index 00000000000..98f33968ca6
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/TrackFragment.java
@@ -0,0 +1,66 @@
+/*
+ * 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.mp4;
+
+/**
+ * A holder for information corresponding to a single fragment of an mp4 file.
+ */
+/* package */ class TrackFragment {
+
+ public int sampleDescriptionIndex;
+
+ public int length;
+ public int[] sampleSizeTable;
+ public int[] sampleDecodingTimeTable;
+ public int[] sampleCompositionTimeOffsetTable;
+ public boolean[] sampleIsSyncFrameTable;
+
+ public int auxiliarySampleInfoTotalSize;
+ public int[] auxiliarySampleInfoSizeTable;
+
+ public boolean smoothStreamingUsesSubsampleEncryption;
+ public ParsableByteArray smoothStreamingSampleEncryptionData;
+
+ public void setSampleDescriptionIndex(int sampleDescriptionIndex) {
+ this.sampleDescriptionIndex = sampleDescriptionIndex;
+ }
+
+ public void setSampleTables(int[] sampleSizeTable, int[] sampleDecodingTimeTable,
+ int[] sampleCompositionTimeOffsetTable, boolean[] sampleIsSyncFrameTable) {
+ this.sampleSizeTable = sampleSizeTable;
+ this.sampleDecodingTimeTable = sampleDecodingTimeTable;
+ this.sampleCompositionTimeOffsetTable = sampleCompositionTimeOffsetTable;
+ this.sampleIsSyncFrameTable = sampleIsSyncFrameTable;
+ this.length = sampleSizeTable.length;
+ }
+
+ public void setAuxiliarySampleInfoTables(int totalAuxiliarySampleInfoSize,
+ int[] auxiliarySampleInfoSizeTable) {
+ this.auxiliarySampleInfoTotalSize = totalAuxiliarySampleInfoSize;
+ this.auxiliarySampleInfoSizeTable = auxiliarySampleInfoSizeTable;
+ }
+
+ public void setSmoothStreamingSampleEncryptionData(ParsableByteArray data,
+ boolean usesSubsampleEncryption) {
+ this.smoothStreamingSampleEncryptionData = data;
+ this.smoothStreamingUsesSubsampleEncryption = usesSubsampleEncryption;
+ }
+
+ public int getSamplePresentationTime(int index) {
+ return sampleDecodingTimeTable[index] + sampleCompositionTimeOffsetTable[index];
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/parser/webm/EbmlReader.java b/library/src/main/java/com/google/android/exoplayer/parser/webm/EbmlReader.java
new file mode 100644
index 00000000000..6dbc744e14f
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/parser/webm/EbmlReader.java
@@ -0,0 +1,543 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer.parser.webm;
+
+import com.google.android.exoplayer.upstream.NonBlockingInputStream;
+import com.google.android.exoplayer.util.Assertions;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.util.Stack;
+
+/**
+ * An event-driven incremental EBML reader base class.
+ *
+ *
EBML can be summarized as a binary XML format somewhat similar to Protocol Buffers.
+ * It was originally designed for the Matroska container format. More information about EBML and
+ * Matroska is available here .
+ */
+public abstract class EbmlReader {
+
+ // Element Types
+ protected static final int TYPE_UNKNOWN = 0; // Undefined element.
+ protected static final int TYPE_MASTER = 1; // Contains child elements.
+ protected static final int TYPE_UNSIGNED_INT = 2;
+ protected static final int TYPE_STRING = 3;
+ protected static final int TYPE_BINARY = 4;
+ protected static final int TYPE_FLOAT = 5;
+
+ // Return values for methods read, readElementId, readElementSize, readVarintBytes, and readBytes.
+ protected static final int RESULT_CONTINUE = 0;
+ protected static final int RESULT_NEED_MORE_DATA = 1;
+ protected static final int RESULT_END_OF_FILE = 2;
+
+ // State values used in variables state, elementIdState, elementContentSizeState, and
+ // varintBytesState.
+ private static final int STATE_BEGIN_READING = 0;
+ private static final int STATE_READ_CONTENTS = 1;
+ private static final int STATE_FINISHED_READING = 2;
+
+ /**
+ * The first byte of a variable-length integer (varint) will have one of these bit masks
+ * indicating the total length in bytes. {@code 0x80} is a one-byte integer,
+ * {@code 0x40} is two bytes, and so on up to eight bytes.
+ */
+ private static final int[] VARINT_LENGTH_MASKS = new int[] {
+ 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01
+ };
+
+ private final Stack masterElementsStack = new Stack();
+ private final byte[] tempByteArray = new byte[8];
+
+ private int state;
+ private long bytesRead;
+ private long elementOffset;
+ private int elementId;
+ private int elementIdState;
+ private long elementContentSize;
+ private int elementContentSizeState;
+ private int varintBytesState;
+ private int varintBytesLength;
+ private int bytesState;
+ private byte[] stringBytes;
+
+ /**
+ * Called to retrieve the type of an element ID. If {@link #TYPE_UNKNOWN} is returned then
+ * the element is skipped. Note that all children of a skipped master element are also skipped.
+ *
+ * @param id The integer ID of this element.
+ * @return One of the {@code TYPE_} constants defined in this class.
+ */
+ protected abstract int getElementType(int id);
+
+ /**
+ * Called when a master element is encountered in the {@link NonBlockingInputStream}.
+ * Following events should be considered as taking place "within" this element until a
+ * matching call to {@link #onMasterElementEnd(int)} is made. Note that it
+ * is possible for the same master element to be nested within itself.
+ *
+ * @param id The integer ID of this element.
+ * @param elementOffset The byte offset where this element starts.
+ * @param headerSize The byte length of this element's ID and size header.
+ * @param contentsSize The byte length of this element's children.
+ * @return {@code true} if parsing should continue or {@code false} if it should stop right away.
+ */
+ protected abstract boolean onMasterElementStart(
+ int id, long elementOffset, int headerSize, int contentsSize);
+
+ /**
+ * Called when a master element has finished reading in all of its children from the
+ * {@link NonBlockingInputStream}.
+ *
+ * @param id The integer ID of this element.
+ * @return {@code true} if parsing should continue or {@code false} if it should stop right away.
+ */
+ protected abstract boolean onMasterElementEnd(int id);
+
+ /**
+ * Called when an integer element is encountered in the {@link NonBlockingInputStream}.
+ *
+ * @param id The integer ID of this element.
+ * @param value The integer value this element contains.
+ * @return {@code true} if parsing should continue or {@code false} if it should stop right away.
+ */
+ protected abstract boolean onIntegerElement(int id, long value);
+
+ /**
+ * Called when a float element is encountered in the {@link NonBlockingInputStream}.
+ *
+ * @param id The integer ID of this element.
+ * @param value The float value this element contains.
+ * @return {@code true} if parsing should continue or {@code false} if it should stop right away.
+ */
+ protected abstract boolean onFloatElement(int id, double value);
+
+ /**
+ * Called when a string element is encountered in the {@link NonBlockingInputStream}.
+ *
+ * @param id The integer ID of this element.
+ * @param value The string value this element contains.
+ * @return {@code true} if parsing should continue or {@code false} if it should stop right away.
+ */
+ protected abstract boolean onStringElement(int id, String value);
+
+ /**
+ * Called when a binary element is encountered in the {@link NonBlockingInputStream}.
+ * The element header (containing element ID and content size) will already have been read.
+ * Subclasses must exactly read the entire contents of the element, which is {@code contentsSize}
+ * bytes in length. It's guaranteed that the full element contents will be immediately available
+ * from {@code inputStream}.
+ *
+ * Several methods are available for reading the contents of a binary element:
+ *
+ * {@link #readVarint(NonBlockingInputStream)}.
+ * {@link #readBytes(NonBlockingInputStream, byte[], int)}.
+ * {@link #readBytes(NonBlockingInputStream, ByteBuffer, int)}.
+ * {@link #skipBytes(NonBlockingInputStream, int)}.
+ * {@link #getBytesRead()}.
+ *
+ * @param inputStream The {@link NonBlockingInputStream} from which this
+ * element's contents should be read.
+ * @param id The integer ID of this element.
+ * @param elementOffset The byte offset where this element starts.
+ * @param headerSize The byte length of this element's ID and size header.
+ * @param contentsSize The byte length of this element's contents.
+ * @return {@code true} if parsing should continue or {@code false} if it should stop right away.
+ */
+ protected abstract boolean onBinaryElement(NonBlockingInputStream inputStream,
+ int id, long elementOffset, int headerSize, int contentsSize);
+
+ /**
+ * Reads from a {@link NonBlockingInputStream} and calls event callbacks as needed.
+ *
+ * @param inputStream The input stream from which data should be read.
+ * @return One of the {@code RESULT_*} flags defined in this class.
+ */
+ protected final int read(NonBlockingInputStream inputStream) {
+ while (true) {
+ while (masterElementsStack.size() > 0
+ && bytesRead >= masterElementsStack.peek().elementEndOffset) {
+ if (!onMasterElementEnd(masterElementsStack.pop().elementId)) {
+ return RESULT_CONTINUE;
+ }
+ }
+
+ if (state == STATE_BEGIN_READING) {
+ final int resultId = readElementId(inputStream);
+ if (resultId != RESULT_CONTINUE) {
+ return resultId;
+ }
+ final int resultSize = readElementContentSize(inputStream);
+ if (resultSize != RESULT_CONTINUE) {
+ return resultSize;
+ }
+ state = STATE_READ_CONTENTS;
+ bytesState = 0;
+ }
+
+ final int type = getElementType(elementId);
+ switch (type) {
+
+ case TYPE_MASTER:
+ final int masterHeaderSize = (int) (bytesRead - elementOffset);
+ masterElementsStack.add(new MasterElement(elementId, bytesRead + elementContentSize));
+ if (!onMasterElementStart(
+ elementId, elementOffset, masterHeaderSize, (int) elementContentSize)) {
+ prepareForNextElement();
+ return RESULT_CONTINUE;
+ }
+ break;
+
+ case TYPE_UNSIGNED_INT:
+ Assertions.checkState(elementContentSize <= 8);
+ final int resultInt =
+ readBytes(inputStream, null, tempByteArray, (int) elementContentSize);
+ if (resultInt != RESULT_CONTINUE) {
+ return resultInt;
+ }
+ final long intValue = parseTempByteArray((int) elementContentSize, false);
+ if (!onIntegerElement(elementId, intValue)) {
+ prepareForNextElement();
+ return RESULT_CONTINUE;
+ }
+ break;
+
+ case TYPE_FLOAT:
+ Assertions.checkState(elementContentSize == 4 || elementContentSize == 8);
+ final int resultFloat =
+ readBytes(inputStream, null, tempByteArray, (int) elementContentSize);
+ if (resultFloat != RESULT_CONTINUE) {
+ return resultFloat;
+ }
+ final long valueBits = parseTempByteArray((int) elementContentSize, false);
+ final double floatValue;
+ if (elementContentSize == 4) {
+ floatValue = Float.intBitsToFloat((int) valueBits);
+ } else {
+ floatValue = Double.longBitsToDouble(valueBits);
+ }
+ if (!onFloatElement(elementId, floatValue)) {
+ prepareForNextElement();
+ return RESULT_CONTINUE;
+ }
+ break;
+
+ case TYPE_STRING:
+ if (stringBytes == null) {
+ stringBytes = new byte[(int) elementContentSize];
+ }
+ final int resultString =
+ readBytes(inputStream, null, stringBytes, (int) elementContentSize);
+ if (resultString != RESULT_CONTINUE) {
+ return resultString;
+ }
+ final String stringValue = new String(stringBytes, Charset.forName("UTF-8"));
+ stringBytes = null;
+ if (!onStringElement(elementId, stringValue)) {
+ prepareForNextElement();
+ return RESULT_CONTINUE;
+ }
+ break;
+
+ case TYPE_BINARY:
+ if (inputStream.getAvailableByteCount() < elementContentSize) {
+ return RESULT_NEED_MORE_DATA;
+ }
+ final int binaryHeaderSize = (int) (bytesRead - elementOffset);
+ final boolean keepGoing = onBinaryElement(
+ inputStream, elementId, elementOffset, binaryHeaderSize, (int) elementContentSize);
+ Assertions.checkState(elementOffset + binaryHeaderSize + elementContentSize == bytesRead);
+ if (!keepGoing) {
+ prepareForNextElement();
+ return RESULT_CONTINUE;
+ }
+ break;
+
+ case TYPE_UNKNOWN:
+ // Unknown elements should be skipped.
+ Assertions.checkState(
+ readBytes(inputStream, null, null, (int) elementContentSize) == RESULT_CONTINUE);
+ break;
+
+ default:
+ throw new IllegalStateException("Invalid element type " + type);
+
+ }
+ prepareForNextElement();
+ }
+ }
+
+ /**
+ * @return The total number of bytes consumed by the reader since first created
+ * or last {@link #reset()}.
+ */
+ protected final long getBytesRead() {
+ return bytesRead;
+ }
+
+ /**
+ * Resets the entire state of the reader so that it will read a new EBML structure from scratch.
+ * This includes resetting {@link #bytesRead} back to 0 and discarding all pending
+ * {@link #onMasterElementEnd(int)} events.
+ */
+ protected final void reset() {
+ prepareForNextElement();
+ masterElementsStack.clear();
+ bytesRead = 0;
+ }
+
+ /**
+ * Reads, parses, and returns an EBML variable-length integer (varint) from the contents
+ * of a binary element.
+ *
+ * @param inputStream The input stream from which data should be read.
+ * @return The varint value at the current position of the contents of a binary element.
+ */
+ protected final long readVarint(NonBlockingInputStream inputStream) {
+ varintBytesState = STATE_BEGIN_READING;
+ Assertions.checkState(readVarintBytes(inputStream) == RESULT_CONTINUE);
+ return parseTempByteArray(varintBytesLength, true);
+ }
+
+ /**
+ * Reads a fixed number of bytes from the contents of a binary element into a {@link ByteBuffer}.
+ *
+ * @param inputStream The input stream from which data should be read.
+ * @param byteBuffer The {@link ByteBuffer} to which data should be written.
+ * @param totalBytes The fixed number of bytes to be read and written.
+ */
+ protected final void readBytes(
+ NonBlockingInputStream inputStream, ByteBuffer byteBuffer, int totalBytes) {
+ bytesState = 0;
+ Assertions.checkState(readBytes(inputStream, byteBuffer, null, totalBytes) == RESULT_CONTINUE);
+ }
+
+ /**
+ * Reads a fixed number of bytes from the contents of a binary element into a {@code byte[]}.
+ *
+ * @param inputStream The input stream from which data should be read.
+ * @param byteArray The byte array to which data should be written.
+ * @param totalBytes The fixed number of bytes to be read and written.
+ */
+ protected final void readBytes(
+ NonBlockingInputStream inputStream, byte[] byteArray, int totalBytes) {
+ bytesState = 0;
+ Assertions.checkState(readBytes(inputStream, null, byteArray, totalBytes) == RESULT_CONTINUE);
+ }
+
+ /**
+ * Skips a fixed number of bytes from the contents of a binary element.
+ *
+ * @param inputStream The input stream from which data should be skipped.
+ * @param totalBytes The fixed number of bytes to be skipped.
+ */
+ protected final void skipBytes(NonBlockingInputStream inputStream, int totalBytes) {
+ bytesState = 0;
+ Assertions.checkState(readBytes(inputStream, null, null, totalBytes) == RESULT_CONTINUE);
+ }
+
+ /**
+ * Resets the internal state of {@link #read(NonBlockingInputStream)} so that it can start
+ * reading a new element from scratch.
+ */
+ private final void prepareForNextElement() {
+ state = STATE_BEGIN_READING;
+ elementIdState = STATE_BEGIN_READING;
+ elementContentSizeState = STATE_BEGIN_READING;
+ elementOffset = bytesRead;
+ }
+
+ /**
+ * Reads an element ID such that reading can be stopped and started again in a later call
+ * if not enough bytes are available. Returns {@link #RESULT_CONTINUE} if a full element ID
+ * has been read into {@link #elementId}. Reset {@link #elementIdState} to
+ * {@link #STATE_BEGIN_READING} before calling to indicate a new element ID should be read.
+ *
+ * @param inputStream The input stream from which an element ID should be read.
+ * @return One of the {@code RESULT_*} flags defined in this class.
+ */
+ private int readElementId(NonBlockingInputStream inputStream) {
+ if (elementIdState == STATE_FINISHED_READING) {
+ return RESULT_CONTINUE;
+ }
+ if (elementIdState == STATE_BEGIN_READING) {
+ varintBytesState = STATE_BEGIN_READING;
+ elementIdState = STATE_READ_CONTENTS;
+ }
+ final int result = readVarintBytes(inputStream);
+ if (result != RESULT_CONTINUE) {
+ return result;
+ }
+ elementId = (int) parseTempByteArray(varintBytesLength, false);
+ elementIdState = STATE_FINISHED_READING;
+ return RESULT_CONTINUE;
+ }
+
+ /**
+ * Reads an element's content size such that reading can be stopped and started again in a later
+ * call if not enough bytes are available. Returns {@link #RESULT_CONTINUE} if an entire element
+ * size has been read into {@link #elementContentSize}. Reset {@link #elementContentSizeState} to
+ * {@link #STATE_BEGIN_READING} before calling to indicate a new element size should be read.
+ *
+ * @param inputStream The input stream from which an element size should be read.
+ * @return One of the {@code RESULT_*} flags defined in this class.
+ */
+ private int readElementContentSize(NonBlockingInputStream inputStream) {
+ if (elementContentSizeState == STATE_FINISHED_READING) {
+ return RESULT_CONTINUE;
+ }
+ if (elementContentSizeState == STATE_BEGIN_READING) {
+ varintBytesState = STATE_BEGIN_READING;
+ elementContentSizeState = STATE_READ_CONTENTS;
+ }
+ final int result = readVarintBytes(inputStream);
+ if (result != RESULT_CONTINUE) {
+ return result;
+ }
+ elementContentSize = parseTempByteArray(varintBytesLength, true);
+ elementContentSizeState = STATE_FINISHED_READING;
+ return RESULT_CONTINUE;
+ }
+
+ /**
+ * Reads an EBML variable-length integer (varint) such that reading can be stopped and started
+ * again in a later call if not enough bytes are available. Returns {@link #RESULT_CONTINUE} if
+ * an entire varint has been read into {@link #tempByteArray} and the length of the varint is in
+ * {@link #varintBytesLength}. Reset {@link #varintBytesState} to {@link #STATE_BEGIN_READING}
+ * before calling to indicate a new varint should be read.
+ *
+ * @param inputStream The input stream from which a varint should be read.
+ * @return One of the {@code RESULT_*} flags defined in this class.
+ */
+ private int readVarintBytes(NonBlockingInputStream inputStream) {
+ if (varintBytesState == STATE_FINISHED_READING) {
+ return RESULT_CONTINUE;
+ }
+
+ // Read first byte to get length.
+ if (varintBytesState == STATE_BEGIN_READING) {
+ bytesState = 0;
+ final int result = readBytes(inputStream, null, tempByteArray, 1);
+ if (result != RESULT_CONTINUE) {
+ return result;
+ }
+ varintBytesState = STATE_READ_CONTENTS;
+
+ final int firstByte = tempByteArray[0] & 0xff;
+ varintBytesLength = -1;
+ for (int i = 0; i < VARINT_LENGTH_MASKS.length; i++) {
+ if ((VARINT_LENGTH_MASKS[i] & firstByte) != 0) {
+ varintBytesLength = i + 1;
+ break;
+ }
+ }
+ if (varintBytesLength == -1) {
+ throw new IllegalStateException(
+ "No valid varint length mask found at bytesRead = " + bytesRead);
+ }
+ }
+
+ // Read remaining bytes.
+ final int result = readBytes(inputStream, null, tempByteArray, varintBytesLength);
+ if (result != RESULT_CONTINUE) {
+ return result;
+ }
+
+ // All bytes have been read.
+ return RESULT_CONTINUE;
+ }
+
+ /**
+ * Reads a set amount of bytes into a {@link ByteBuffer}, {@code byte[]}, or nowhere (skipping
+ * the bytes) such that reading can be stopped and started again later if not enough bytes are
+ * available. Returns {@link #RESULT_CONTINUE} if all bytes have been read. Reset
+ * {@link #bytesState} to {@code 0} before calling to indicate a new set of bytes should be read.
+ *
+ * If both {@code byteBuffer} and {@code byteArray} are not null then bytes are only read
+ * into {@code byteBuffer}.
+ *
+ * @param inputStream The input stream from which bytes should be read.
+ * @param byteBuffer The optional {@link ByteBuffer} into which bytes should be read.
+ * @param byteArray The optional {@code byte[]} into which bytes should be read.
+ * @param totalBytes The total size of bytes to be read or skipped.
+ * @return One of the {@code RESULT_*} flags defined in this class.
+ */
+ private int readBytes(
+ NonBlockingInputStream inputStream, ByteBuffer byteBuffer, byte[] byteArray, int totalBytes) {
+ if (bytesState == STATE_BEGIN_READING
+ && ((byteBuffer != null && totalBytes > byteBuffer.capacity())
+ || (byteArray != null && totalBytes > byteArray.length))) {
+ throw new IllegalStateException("Byte destination not large enough");
+ }
+ if (bytesState < totalBytes) {
+ final int remainingBytes = totalBytes - bytesState;
+ final int result;
+ if (byteBuffer != null) {
+ result = inputStream.read(byteBuffer, remainingBytes);
+ } else if (byteArray != null) {
+ result = inputStream.read(byteArray, bytesState, remainingBytes);
+ } else {
+ result = inputStream.skip(remainingBytes);
+ }
+ if (result == -1) {
+ return RESULT_END_OF_FILE;
+ }
+ bytesState += result;
+ bytesRead += result;
+ if (bytesState < totalBytes) {
+ return RESULT_NEED_MORE_DATA;
+ }
+ }
+ return RESULT_CONTINUE;
+ }
+
+ /**
+ * Parses and returns the integer value currently read into the first {@code byteLength} bytes
+ * of {@link #tempByteArray}. EBML varint length masks can optionally be removed.
+ *
+ * @param byteLength The number of bytes to parse from {@link #tempByteArray}.
+ * @param removeLengthMask Removes the variable-length integer length mask from the value.
+ * @return The resulting integer value. This value could be up to 8-bytes so a Java long is used.
+ */
+ private long parseTempByteArray(int byteLength, boolean removeLengthMask) {
+ if (removeLengthMask) {
+ tempByteArray[0] &= ~VARINT_LENGTH_MASKS[varintBytesLength - 1];
+ }
+ long varint = 0;
+ for (int i = 0; i < byteLength; i++) {
+ // Shift all existing bits up one byte and add the next byte at the bottom.
+ varint = (varint << 8) | (tempByteArray[i] & 0xff);
+ }
+ return varint;
+ }
+
+ /**
+ * Used in {@link #masterElementsStack} to track when the current master element ends so that
+ * {@link #onMasterElementEnd(int)} is called.
+ */
+ private static final class MasterElement {
+
+ private final int elementId;
+ private final long elementEndOffset;
+
+ private MasterElement(int elementId, long elementEndOffset) {
+ this.elementId = elementId;
+ this.elementEndOffset = elementEndOffset;
+ }
+
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/parser/webm/WebmExtractor.java b/library/src/main/java/com/google/android/exoplayer/parser/webm/WebmExtractor.java
new file mode 100644
index 00000000000..49b82f4a160
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/parser/webm/WebmExtractor.java
@@ -0,0 +1,406 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer.parser.webm;
+
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.SampleHolder;
+import com.google.android.exoplayer.parser.SegmentIndex;
+import com.google.android.exoplayer.upstream.NonBlockingInputStream;
+import com.google.android.exoplayer.util.LongArray;
+import com.google.android.exoplayer.util.MimeTypes;
+
+import android.annotation.TargetApi;
+import android.media.MediaExtractor;
+
+import java.util.Arrays;
+
+/**
+ * Facilitates the extraction of data from the WebM container format with a
+ * non-blocking, incremental parser based on {@link EbmlReader}.
+ *
+ *
WebM is a subset of the EBML elements defined for Matroska. More information about EBML and
+ * Matroska is available here .
+ * More info about WebM is here .
+ */
+@TargetApi(16)
+public final class WebmExtractor extends EbmlReader {
+
+ private static final String DOC_TYPE_WEBM = "webm";
+ private static final String CODEC_ID_VP9 = "V_VP9";
+ private static final int UNKNOWN = -1;
+
+ // Element IDs
+ private static final int ID_EBML = 0x1A45DFA3;
+ private static final int ID_EBML_READ_VERSION = 0x42F7;
+ private static final int ID_DOC_TYPE = 0x4282;
+ private static final int ID_DOC_TYPE_READ_VERSION = 0x4285;
+
+ private static final int ID_SEGMENT = 0x18538067;
+
+ private static final int ID_INFO = 0x1549A966;
+ private static final int ID_TIMECODE_SCALE = 0x2AD7B1;
+ private static final int ID_DURATION = 0x4489;
+
+ private static final int ID_CLUSTER = 0x1F43B675;
+ private static final int ID_TIME_CODE = 0xE7;
+ private static final int ID_SIMPLE_BLOCK = 0xA3;
+
+ private static final int ID_TRACKS = 0x1654AE6B;
+ private static final int ID_TRACK_ENTRY = 0xAE;
+ private static final int ID_CODEC_ID = 0x86;
+ private static final int ID_VIDEO = 0xE0;
+ private static final int ID_PIXEL_WIDTH = 0xB0;
+ private static final int ID_PIXEL_HEIGHT = 0xBA;
+
+ private static final int ID_CUES = 0x1C53BB6B;
+ private static final int ID_CUE_POINT = 0xBB;
+ private static final int ID_CUE_TIME = 0xB3;
+ private static final int ID_CUE_TRACK_POSITIONS = 0xB7;
+ private static final int ID_CUE_CLUSTER_POSITION = 0xF1;
+
+ // SimpleBlock Lacing Values
+ private static final int LACING_NONE = 0;
+ private static final int LACING_XIPH = 1;
+ private static final int LACING_FIXED = 2;
+ private static final int LACING_EBML = 3;
+
+ private final byte[] simpleBlockTimecodeAndFlags = new byte[3];
+
+ private SampleHolder tempSampleHolder;
+ private boolean sampleRead;
+
+ private boolean prepared = false;
+ private long segmentStartPosition = UNKNOWN;
+ private long segmentEndPosition = UNKNOWN;
+ private long timecodeScale = 1000000L;
+ private long durationUs = UNKNOWN;
+ private int pixelWidth = UNKNOWN;
+ private int pixelHeight = UNKNOWN;
+ private int cuesByteSize = UNKNOWN;
+ private long clusterTimecodeUs = UNKNOWN;
+ private long simpleBlockTimecodeUs = UNKNOWN;
+ private MediaFormat format;
+ private SegmentIndex cues;
+ private LongArray cueTimesUs;
+ private LongArray cueClusterPositions;
+
+ public WebmExtractor() {
+ cueTimesUs = new LongArray();
+ cueClusterPositions = new LongArray();
+ }
+
+ /**
+ * Whether the has parsed the cues and sample format from the stream.
+ *
+ * @return True if the extractor is prepared. False otherwise.
+ */
+ public boolean isPrepared() {
+ return prepared;
+ }
+
+ /**
+ * Consumes data from a {@link NonBlockingInputStream}.
+ *
+ *
If the return value is {@code false}, then a sample may have been partially read into
+ * {@code sampleHolder}. Hence the same {@link SampleHolder} instance must be passed
+ * in subsequent calls until the whole sample has been read.
+ *
+ * @param inputStream The input stream from which data should be read.
+ * @param sampleHolder A {@link SampleHolder} into which the sample should be read.
+ * @return {@code true} if a sample has been read into the sample holder, otherwise {@code false}.
+ */
+ public boolean read(NonBlockingInputStream inputStream, SampleHolder sampleHolder) {
+ tempSampleHolder = sampleHolder;
+ sampleRead = false;
+ super.read(inputStream);
+ tempSampleHolder = null;
+ return sampleRead;
+ }
+
+ /**
+ * Seeks to a position before or equal to the requested time.
+ *
+ * @param seekTimeUs The desired seek time in microseconds.
+ * @param allowNoop Allow the seek operation to do nothing if the seek time is in the current
+ * segment, is equal to or greater than the time of the current sample, and if there does not
+ * exist a sync frame between these two times.
+ * @return True if the operation resulted in a change of state. False if it was a no-op.
+ */
+ public boolean seekTo(long seekTimeUs, boolean allowNoop) {
+ checkPrepared();
+ if (allowNoop && simpleBlockTimecodeUs != UNKNOWN && seekTimeUs >= simpleBlockTimecodeUs) {
+ final int clusterIndex = Arrays.binarySearch(cues.timesUs, clusterTimecodeUs);
+ if (clusterIndex >= 0 && seekTimeUs < clusterTimecodeUs + cues.durationsUs[clusterIndex]) {
+ return false;
+ }
+ }
+ reset();
+ return true;
+ }
+
+ /**
+ * Returns the cues for the media stream.
+ *
+ * @return The cues in the form of a {@link SegmentIndex}, or null if the extractor is not yet
+ * prepared.
+ */
+ public SegmentIndex getCues() {
+ checkPrepared();
+ return cues;
+ }
+
+ /**
+ * Returns the format of the samples contained within the media stream.
+ *
+ * @return The sample media format, or null if the extracted is not yet prepared.
+ */
+ public MediaFormat getFormat() {
+ checkPrepared();
+ return format;
+ }
+
+ @Override
+ protected int getElementType(int id) {
+ switch (id) {
+ case ID_EBML:
+ case ID_SEGMENT:
+ case ID_INFO:
+ case ID_CLUSTER:
+ case ID_TRACKS:
+ case ID_TRACK_ENTRY:
+ case ID_VIDEO:
+ case ID_CUES:
+ case ID_CUE_POINT:
+ case ID_CUE_TRACK_POSITIONS:
+ return EbmlReader.TYPE_MASTER;
+ case ID_EBML_READ_VERSION:
+ case ID_DOC_TYPE_READ_VERSION:
+ case ID_TIMECODE_SCALE:
+ case ID_TIME_CODE:
+ case ID_PIXEL_WIDTH:
+ case ID_PIXEL_HEIGHT:
+ case ID_CUE_TIME:
+ case ID_CUE_CLUSTER_POSITION:
+ return EbmlReader.TYPE_UNSIGNED_INT;
+ case ID_DOC_TYPE:
+ case ID_CODEC_ID:
+ return EbmlReader.TYPE_STRING;
+ case ID_SIMPLE_BLOCK:
+ return EbmlReader.TYPE_BINARY;
+ case ID_DURATION:
+ return EbmlReader.TYPE_FLOAT;
+ default:
+ return EbmlReader.TYPE_UNKNOWN;
+ }
+ }
+
+ @Override
+ protected boolean onMasterElementStart(
+ int id, long elementOffset, int headerSize, int contentsSize) {
+ switch (id) {
+ case ID_SEGMENT:
+ if (segmentStartPosition != UNKNOWN || segmentEndPosition != UNKNOWN) {
+ throw new IllegalStateException("Multiple Segment elements not supported");
+ }
+ segmentStartPosition = elementOffset + headerSize;
+ segmentEndPosition = elementOffset + headerSize + contentsSize;
+ break;
+ case ID_CUES:
+ cuesByteSize = headerSize + contentsSize;
+ break;
+ }
+ return true;
+ }
+
+ @Override
+ protected boolean onMasterElementEnd(int id) {
+ switch (id) {
+ case ID_CUES:
+ finishPreparing();
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ protected boolean onIntegerElement(int id, long value) {
+ switch (id) {
+ case ID_EBML_READ_VERSION:
+ // Validate that EBMLReadVersion is supported. This extractor only supports v1.
+ if (value != 1) {
+ throw new IllegalStateException("EBMLReadVersion " + value + " not supported");
+ }
+ break;
+ case ID_DOC_TYPE_READ_VERSION:
+ // Validate that DocTypeReadVersion is supported. This extractor only supports up to v2.
+ if (value < 1 || value > 2) {
+ throw new IllegalStateException("DocTypeReadVersion " + value + " not supported");
+ }
+ break;
+ case ID_TIMECODE_SCALE:
+ timecodeScale = value;
+ break;
+ case ID_PIXEL_WIDTH:
+ pixelWidth = (int) value;
+ break;
+ case ID_PIXEL_HEIGHT:
+ pixelHeight = (int) value;
+ break;
+ case ID_CUE_TIME:
+ cueTimesUs.add(scaleTimecodeToUs(value));
+ break;
+ case ID_CUE_CLUSTER_POSITION:
+ cueClusterPositions.add(value);
+ break;
+ case ID_TIME_CODE:
+ clusterTimecodeUs = scaleTimecodeToUs(value);
+ break;
+ }
+ return true;
+ }
+
+ @Override
+ protected boolean onFloatElement(int id, double value) {
+ switch (id) {
+ case ID_DURATION:
+ durationUs = scaleTimecodeToUs(value);
+ break;
+ }
+ return true;
+ }
+
+ @Override
+ protected boolean onStringElement(int id, String value) {
+ switch (id) {
+ case ID_DOC_TYPE:
+ // Validate that DocType is supported. This extractor only supports "webm".
+ if (!DOC_TYPE_WEBM.equals(value)) {
+ throw new IllegalStateException("DocType " + value + " not supported");
+ }
+ break;
+ case ID_CODEC_ID:
+ // Validate that CodecID is supported. This extractor only supports "V_VP9".
+ if (!CODEC_ID_VP9.equals(value)) {
+ throw new IllegalStateException("CodecID " + value + " not supported");
+ }
+ break;
+ }
+ return true;
+ }
+
+ @Override
+ protected boolean onBinaryElement(NonBlockingInputStream inputStream,
+ int id, long elementOffset, int headerSize, int contentsSize) {
+ switch (id) {
+ case ID_SIMPLE_BLOCK:
+ // Please refer to http://www.matroska.org/technical/specs/index.html#simpleblock_structure
+ // for info about how data is organized in a SimpleBlock element.
+
+ // Value of trackNumber is not used but needs to be read.
+ readVarint(inputStream);
+
+ // Next three bytes have timecode and flags.
+ readBytes(inputStream, simpleBlockTimecodeAndFlags, 3);
+
+ // First two bytes of the three are the relative timecode.
+ final int timecode =
+ (simpleBlockTimecodeAndFlags[0] << 8) | (simpleBlockTimecodeAndFlags[1] & 0xff);
+ final long timecodeUs = scaleTimecodeToUs(timecode);
+
+ // Last byte of the three has some flags and the lacing value.
+ final boolean keyframe = (simpleBlockTimecodeAndFlags[2] & 0x80) == 0x80;
+ final boolean invisible = (simpleBlockTimecodeAndFlags[2] & 0x08) == 0x08;
+ final int lacing = (simpleBlockTimecodeAndFlags[2] & 0x06) >> 1;
+ //final boolean discardable = (simpleBlockTimecodeAndFlags[2] & 0x01) == 0x01; // Not used.
+
+ // Validate lacing and set info into sample holder.
+ switch (lacing) {
+ case LACING_NONE:
+ final long elementEndOffset = elementOffset + headerSize + contentsSize;
+ simpleBlockTimecodeUs = clusterTimecodeUs + timecodeUs;
+ tempSampleHolder.flags = keyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0;
+ tempSampleHolder.decodeOnly = invisible;
+ tempSampleHolder.timeUs = clusterTimecodeUs + timecodeUs;
+ tempSampleHolder.size = (int) (elementEndOffset - getBytesRead());
+ break;
+ case LACING_EBML:
+ case LACING_FIXED:
+ case LACING_XIPH:
+ default:
+ throw new IllegalStateException("Lacing mode " + lacing + " not supported");
+ }
+
+ // Read video data into sample holder.
+ readBytes(inputStream, tempSampleHolder.data, tempSampleHolder.size);
+ sampleRead = true;
+ return false;
+ default:
+ skipBytes(inputStream, contentsSize);
+ }
+ return true;
+ }
+
+ private long scaleTimecodeToUs(long unscaledTimecode) {
+ return (unscaledTimecode * timecodeScale) / 1000L;
+ }
+
+ private long scaleTimecodeToUs(double unscaledTimecode) {
+ return (long) ((unscaledTimecode * timecodeScale) / 1000.0);
+ }
+
+ private void checkPrepared() {
+ if (!prepared) {
+ throw new IllegalStateException("Parser not yet prepared");
+ }
+ }
+
+ private void finishPreparing() {
+ if (prepared
+ || segmentStartPosition == UNKNOWN || segmentEndPosition == UNKNOWN
+ || durationUs == UNKNOWN
+ || pixelWidth == UNKNOWN || pixelHeight == UNKNOWN
+ || cuesByteSize == UNKNOWN
+ || cueTimesUs.size() == 0 || cueTimesUs.size() != cueClusterPositions.size()) {
+ throw new IllegalStateException("Incorrect state in finishPreparing()");
+ }
+
+ format = MediaFormat.createVideoFormat(MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, pixelWidth,
+ pixelHeight, null);
+
+ final int cuePointsSize = cueTimesUs.size();
+ final int sizeBytes = cuesByteSize;
+ final int[] sizes = new int[cuePointsSize];
+ final long[] offsets = new long[cuePointsSize];
+ final long[] durationsUs = new long[cuePointsSize];
+ final long[] timesUs = new long[cuePointsSize];
+ for (int i = 0; i < cuePointsSize; i++) {
+ timesUs[i] = cueTimesUs.get(i);
+ offsets[i] = segmentStartPosition + cueClusterPositions.get(i);
+ }
+ for (int i = 0; i < cuePointsSize - 1; i++) {
+ sizes[i] = (int) (offsets[i + 1] - offsets[i]);
+ durationsUs[i] = timesUs[i + 1] - timesUs[i];
+ }
+ sizes[cuePointsSize - 1] = (int) (segmentEndPosition - offsets[cuePointsSize - 1]);
+ durationsUs[cuePointsSize - 1] = durationUs - timesUs[cuePointsSize - 1];
+ cues = new SegmentIndex(sizeBytes, sizes, offsets, durationsUs, timesUs);
+ cueTimesUs = null;
+ cueClusterPositions = null;
+
+ prepared = true;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java
new file mode 100644
index 00000000000..ed8c1030fe7
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java
@@ -0,0 +1,257 @@
+/*
+ * 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.smoothstreaming;
+
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.TrackInfo;
+import com.google.android.exoplayer.chunk.Chunk;
+import com.google.android.exoplayer.chunk.ChunkOperationHolder;
+import com.google.android.exoplayer.chunk.ChunkSource;
+import com.google.android.exoplayer.chunk.Format;
+import com.google.android.exoplayer.chunk.Format.DecreasingBandwidthComparator;
+import com.google.android.exoplayer.chunk.FormatEvaluator;
+import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation;
+import com.google.android.exoplayer.chunk.MediaChunk;
+import com.google.android.exoplayer.chunk.Mp4MediaChunk;
+import com.google.android.exoplayer.parser.mp4.CodecSpecificDataUtil;
+import com.google.android.exoplayer.parser.mp4.FragmentedMp4Extractor;
+import com.google.android.exoplayer.parser.mp4.Track;
+import com.google.android.exoplayer.parser.mp4.TrackEncryptionBox;
+import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.ProtectionElement;
+import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement;
+import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement;
+import com.google.android.exoplayer.upstream.DataSource;
+import com.google.android.exoplayer.upstream.DataSpec;
+
+import android.net.Uri;
+import android.util.Base64;
+import android.util.SparseArray;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * An {@link ChunkSource} for SmoothStreaming.
+ */
+public class SmoothStreamingChunkSource implements ChunkSource {
+
+ private static final int INITIALIZATION_VECTOR_SIZE = 8;
+
+ private final String baseUrl;
+ private final StreamElement streamElement;
+ private final TrackInfo trackInfo;
+ private final DataSource dataSource;
+ private final FormatEvaluator formatEvaluator;
+ private final Evaluation evaluation;
+
+ private final int maxWidth;
+ private final int maxHeight;
+
+ private final SparseArray extractors;
+ private final Format[] formats;
+
+ /**
+ * @param baseUrl The base URL for the streams.
+ * @param manifest The manifest parsed from {@code baseUrl + "/Manifest"}.
+ * @param streamElementIndex The index of the stream element in the manifest to be provided by
+ * the source.
+ * @param trackIndices The indices of the tracks within the stream element to be considered by
+ * the source. May be null if all tracks within the element should be considered.
+ * @param dataSource A {@link DataSource} suitable for loading the media data.
+ * @param formatEvaluator Selects from the available formats.
+ */
+ public SmoothStreamingChunkSource(String baseUrl, SmoothStreamingManifest manifest,
+ int streamElementIndex, int[] trackIndices, DataSource dataSource,
+ FormatEvaluator formatEvaluator) {
+ this.baseUrl = baseUrl;
+ this.streamElement = manifest.streamElements[streamElementIndex];
+ this.trackInfo = new TrackInfo(streamElement.tracks[0].mimeType, manifest.getDurationUs());
+ this.dataSource = dataSource;
+ this.formatEvaluator = formatEvaluator;
+ this.evaluation = new Evaluation();
+
+ TrackEncryptionBox[] trackEncryptionBoxes = null;
+ ProtectionElement protectionElement = manifest.protectionElement;
+ if (protectionElement != null) {
+ byte[] keyId = getKeyId(protectionElement.data);
+ trackEncryptionBoxes = new TrackEncryptionBox[1];
+ trackEncryptionBoxes[0] = new TrackEncryptionBox(true, INITIALIZATION_VECTOR_SIZE, keyId);
+ }
+
+ int trackCount = trackIndices != null ? trackIndices.length : streamElement.tracks.length;
+ formats = new Format[trackCount];
+ extractors = new SparseArray();
+ int maxWidth = 0;
+ int maxHeight = 0;
+ for (int i = 0; i < trackCount; i++) {
+ int trackIndex = trackIndices != null ? trackIndices[i] : i;
+ TrackElement trackElement = streamElement.tracks[trackIndex];
+ formats[i] = new Format(trackIndex, trackElement.mimeType, trackElement.maxWidth,
+ trackElement.maxHeight, trackElement.numChannels, trackElement.sampleRate,
+ trackElement.bitrate / 8);
+ maxWidth = Math.max(maxWidth, trackElement.maxWidth);
+ maxHeight = Math.max(maxHeight, trackElement.maxHeight);
+
+ MediaFormat mediaFormat = getMediaFormat(streamElement, trackIndex);
+ int trackType = streamElement.type == StreamElement.TYPE_VIDEO ? Track.TYPE_VIDEO
+ : Track.TYPE_AUDIO;
+ FragmentedMp4Extractor extractor = new FragmentedMp4Extractor(true);
+ extractor.setTrack(new Track(trackIndex, trackType, streamElement.timeScale, mediaFormat,
+ trackEncryptionBoxes));
+ if (protectionElement != null) {
+ extractor.putPsshInfo(protectionElement.uuid, protectionElement.data);
+ }
+ extractors.put(trackIndex, extractor);
+ }
+ this.maxHeight = maxHeight;
+ this.maxWidth = maxWidth;
+ Arrays.sort(formats, new DecreasingBandwidthComparator());
+ }
+
+ @Override
+ public final void getMaxVideoDimensions(MediaFormat out) {
+ if (trackInfo.mimeType.startsWith("video")) {
+ out.setMaxVideoDimensions(maxWidth, maxHeight);
+ }
+ }
+
+ @Override
+ public final TrackInfo getTrackInfo() {
+ return trackInfo;
+ }
+
+ @Override
+ public void enable() {
+ // Do nothing.
+ }
+
+ @Override
+ public void disable(List queue) {
+ // Do nothing.
+ }
+
+ @Override
+ public void continueBuffering(long playbackPositionUs) {
+ // Do nothing
+ }
+
+ @Override
+ public final void getChunkOperation(List extends MediaChunk> queue, long seekPositionUs,
+ long playbackPositionUs, ChunkOperationHolder out) {
+ evaluation.queueSize = queue.size();
+ formatEvaluator.evaluate(queue, playbackPositionUs, formats, evaluation);
+ Format selectedFormat = evaluation.format;
+ out.queueSize = evaluation.queueSize;
+
+ if (selectedFormat == null) {
+ out.chunk = null;
+ return;
+ } else if (out.queueSize == queue.size() && out.chunk != null
+ && out.chunk.format.id == evaluation.format.id) {
+ // We already have a chunk, and the evaluation hasn't changed either the format or the size
+ // of the queue. Do nothing.
+ return;
+ }
+
+ int nextChunkIndex;
+ if (queue.isEmpty()) {
+ nextChunkIndex = streamElement.getChunkIndex(seekPositionUs);
+ } else {
+ nextChunkIndex = queue.get(out.queueSize - 1).nextChunkIndex;
+ }
+
+ if (nextChunkIndex == -1) {
+ out.chunk = null;
+ return;
+ }
+
+ boolean isLastChunk = nextChunkIndex == streamElement.chunkCount - 1;
+ String requestUrl = streamElement.buildRequestUrl(selectedFormat.id, nextChunkIndex);
+ Uri uri = Uri.parse(baseUrl + '/' + requestUrl);
+ Chunk mediaChunk = newMediaChunk(selectedFormat, uri, null,
+ extractors.get(selectedFormat.id), dataSource, nextChunkIndex, isLastChunk,
+ streamElement.getStartTimeUs(nextChunkIndex),
+ isLastChunk ? -1 : streamElement.getStartTimeUs(nextChunkIndex + 1), 0);
+ out.chunk = mediaChunk;
+ }
+
+ @Override
+ public IOException getError() {
+ return null;
+ }
+
+ private static MediaFormat getMediaFormat(StreamElement streamElement, int trackIndex) {
+ TrackElement trackElement = streamElement.tracks[trackIndex];
+ String mimeType = trackElement.mimeType;
+ if (streamElement.type == StreamElement.TYPE_VIDEO) {
+ MediaFormat format = MediaFormat.createVideoFormat(mimeType, -1, trackElement.maxWidth,
+ trackElement.maxHeight, Arrays.asList(trackElement.csd));
+ format.setMaxVideoDimensions(streamElement.maxWidth, streamElement.maxHeight);
+ return format;
+ } else if (streamElement.type == StreamElement.TYPE_AUDIO) {
+ List csd;
+ if (trackElement.csd != null) {
+ csd = Arrays.asList(trackElement.csd);
+ } else {
+ csd = Collections.singletonList(CodecSpecificDataUtil.buildAudioSpecificConfig(
+ trackElement.sampleRate, trackElement.numChannels));
+ }
+ MediaFormat format = MediaFormat.createAudioFormat(mimeType, -1, trackElement.numChannels,
+ trackElement.sampleRate, csd);
+ return format;
+ }
+ // TODO: Do subtitles need a format? MediaFormat supports KEY_LANGUAGE.
+ return null;
+ }
+
+ private static MediaChunk newMediaChunk(Format formatInfo, Uri uri, String cacheKey,
+ FragmentedMp4Extractor extractor, DataSource dataSource, int chunkIndex,
+ boolean isLast, long chunkStartTimeUs, long nextChunkStartTimeUs, int trigger) {
+ int nextChunkIndex = isLast ? -1 : chunkIndex + 1;
+ long nextStartTimeUs = isLast ? -1 : nextChunkStartTimeUs;
+ long offset = 0;
+ DataSpec dataSpec = new DataSpec(uri, offset, -1, cacheKey);
+ // In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk.
+ // To convert them the absolute timestamps, we need to set sampleOffsetUs to -chunkStartTimeUs.
+ return new Mp4MediaChunk(dataSource, dataSpec, formatInfo, trigger, extractor,
+ chunkStartTimeUs, nextStartTimeUs, -chunkStartTimeUs, nextChunkIndex);
+ }
+
+ private static byte[] getKeyId(byte[] initData) {
+ StringBuilder initDataStringBuilder = new StringBuilder();
+ for (int i = 0; i < initData.length; i += 2) {
+ initDataStringBuilder.append((char) initData[i]);
+ }
+ String initDataString = initDataStringBuilder.toString();
+ String keyIdString = initDataString.substring(
+ initDataString.indexOf("") + 5, initDataString.indexOf(" "));
+ byte[] keyId = Base64.decode(keyIdString, Base64.DEFAULT);
+ swap(keyId, 0, 3);
+ swap(keyId, 1, 2);
+ swap(keyId, 4, 5);
+ swap(keyId, 6, 7);
+ return keyId;
+ }
+
+ private static void swap(byte[] data, int firstPosition, int secondPosition) {
+ byte temp = data[firstPosition];
+ data[firstPosition] = data[secondPosition];
+ data[secondPosition] = temp;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java
new file mode 100644
index 00000000000..185171bfe69
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java
@@ -0,0 +1,230 @@
+/*
+ * 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.smoothstreaming;
+
+import com.google.android.exoplayer.util.MimeTypes;
+
+import java.util.Arrays;
+import java.util.UUID;
+
+/**
+ * Represents a SmoothStreaming manifest.
+ *
+ * @see
+ * IIS Smooth Streaming Client Manifest Format
+ */
+public class SmoothStreamingManifest {
+
+ public final int majorVersion;
+ public final int minorVersion;
+ public final long timeScale;
+ public final int lookAheadCount;
+ public final ProtectionElement protectionElement;
+ public final StreamElement[] streamElements;
+
+ private final long duration;
+
+ public SmoothStreamingManifest(int majorVersion, int minorVersion, long timeScale, long duration,
+ int lookAheadCount, ProtectionElement protectionElement, StreamElement[] streamElements) {
+ this.majorVersion = majorVersion;
+ this.minorVersion = minorVersion;
+ this.timeScale = timeScale;
+ this.duration = duration;
+ this.lookAheadCount = lookAheadCount;
+ this.protectionElement = protectionElement;
+ this.streamElements = streamElements;
+ }
+
+ /**
+ * Gets the duration of the media.
+ *
+ *
+ * @return The duration of the media, in microseconds.
+ */
+ public long getDurationUs() {
+ return (duration * 1000000L) / timeScale;
+ }
+
+ /**
+ * Represents a protection element containing a single header.
+ */
+ public static class ProtectionElement {
+
+ public final UUID uuid;
+ public final byte[] data;
+
+ public ProtectionElement(UUID uuid, byte[] data) {
+ this.uuid = uuid;
+ this.data = data;
+ }
+
+ }
+
+ /**
+ * Represents a QualityLevel element.
+ */
+ public static class TrackElement {
+
+ // Required for all
+ public final int index;
+ public final int bitrate;
+
+ // Audio-video
+ public final String fourCC;
+ public final byte[][] csd;
+ public final int profile;
+ public final int level;
+
+ // Audio-video (derived)
+ public final String mimeType;
+
+ // Video-only
+ public final int maxWidth;
+ public final int maxHeight;
+
+ // Audio-only
+ public final int sampleRate;
+ public final int numChannels;
+ public final int packetSize;
+ public final int audioTag;
+ public final int bitPerSample;
+
+ public final int nalUnitLengthField;
+ public final String content;
+
+ public TrackElement(int index, int bitrate, String fourCC, byte[][] csd, int profile, int level,
+ int maxWidth, int maxHeight, int sampleRate, int channels, int packetSize, int audioTag,
+ int bitPerSample, int nalUnitLengthField, String content) {
+ this.index = index;
+ this.bitrate = bitrate;
+ this.fourCC = fourCC;
+ this.csd = csd;
+ this.profile = profile;
+ this.level = level;
+ this.maxWidth = maxWidth;
+ this.maxHeight = maxHeight;
+ this.sampleRate = sampleRate;
+ this.numChannels = channels;
+ this.packetSize = packetSize;
+ this.audioTag = audioTag;
+ this.bitPerSample = bitPerSample;
+ this.nalUnitLengthField = nalUnitLengthField;
+ this.content = content;
+ this.mimeType = fourCCToMimeType(fourCC);
+ }
+
+ private static String fourCCToMimeType(String fourCC) {
+ if (fourCC.equalsIgnoreCase("H264") || fourCC.equalsIgnoreCase("AVC1")
+ || fourCC.equalsIgnoreCase("DAVC")) {
+ return MimeTypes.VIDEO_H264;
+ } else if (fourCC.equalsIgnoreCase("AACL") || fourCC.equalsIgnoreCase("AACH")) {
+ return MimeTypes.AUDIO_AAC;
+ } else if (fourCC.equalsIgnoreCase("TTML")) {
+ return MimeTypes.APPLICATION_TTML;
+ }
+ return null;
+ }
+
+ }
+
+ /**
+ * Represents a StreamIndex element.
+ */
+ public static class StreamElement {
+
+ public static final int TYPE_UNKNOWN = -1;
+ public static final int TYPE_AUDIO = 0;
+ public static final int TYPE_VIDEO = 1;
+ public static final int TYPE_TEXT = 2;
+
+ private static final String URL_PLACEHOLDER_START_TIME = "{start time}";
+ private static final String URL_PLACEHOLDER_BITRATE = "{bitrate}";
+
+ public final int type;
+ public final String subType;
+ public final long timeScale;
+ public final String name;
+ public final int qualityLevels;
+ public final String url;
+ public final int maxWidth;
+ public final int maxHeight;
+ public final int displayWidth;
+ public final int displayHeight;
+ public final String language;
+ public final TrackElement[] tracks;
+ public final int chunkCount;
+
+ private final long[] chunkStartTimes;
+
+ public StreamElement(int type, String subType, long timeScale, String name,
+ int qualityLevels, String url, int maxWidth, int maxHeight, int displayWidth,
+ int displayHeight, String language, TrackElement[] tracks, long[] chunkStartTimes) {
+ this.type = type;
+ this.subType = subType;
+ this.timeScale = timeScale;
+ this.name = name;
+ this.qualityLevels = qualityLevels;
+ this.url = url;
+ this.maxWidth = maxWidth;
+ this.maxHeight = maxHeight;
+ this.displayWidth = displayWidth;
+ this.displayHeight = displayHeight;
+ this.language = language;
+ this.tracks = tracks;
+ this.chunkCount = chunkStartTimes.length;
+ this.chunkStartTimes = chunkStartTimes;
+ }
+
+ /**
+ * Gets the index of the chunk that contains the specified time.
+ *
+ * @param timeUs The time in microseconds.
+ * @return The index of the corresponding chunk.
+ */
+ public int getChunkIndex(long timeUs) {
+ long time = (timeUs * timeScale) / 1000000L;
+ int chunkIndex = Arrays.binarySearch(chunkStartTimes, time);
+ return chunkIndex < 0 ? -chunkIndex - 2 : chunkIndex;
+ }
+
+ /**
+ * Gets the start time of the specified chunk.
+ *
+ * @param chunkIndex The index of the chunk.
+ * @return The start time of the chunk, in microseconds.
+ */
+ public long getStartTimeUs(int chunkIndex) {
+ return (chunkStartTimes[chunkIndex] * 1000000L) / timeScale;
+ }
+
+ /**
+ * Builds a URL for requesting the specified chunk of the specified track.
+ *
+ * @param track The index of the track for which to build the URL.
+ * @param chunkIndex The index of the chunk for which to build the URL.
+ * @return The request URL.
+ */
+ public String buildRequestUrl(int track, int chunkIndex) {
+ assert (tracks != null);
+ assert (chunkStartTimes != null);
+ assert (chunkIndex < chunkStartTimes.length);
+ return url.replace(URL_PLACEHOLDER_BITRATE, Integer.toString(tracks[track].bitrate))
+ .replace(URL_PLACEHOLDER_START_TIME, Long.toString(chunkStartTimes[chunkIndex]));
+ }
+
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestFetcher.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestFetcher.java
new file mode 100644
index 00000000000..192020f3e9f
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestFetcher.java
@@ -0,0 +1,67 @@
+/*
+ * 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.smoothstreaming;
+
+import com.google.android.exoplayer.ParserException;
+import com.google.android.exoplayer.util.ManifestFetcher;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A concrete implementation of {@link ManifestFetcher} for loading SmoothStreaming
+ * manifests.
+ *
+ * This class is provided for convenience, however it is expected that most applications will
+ * contain their own mechanisms for making asynchronous network requests and parsing the response.
+ * In such cases it is recommended that application developers use their existing solution rather
+ * than this one.
+ */
+public final class SmoothStreamingManifestFetcher extends ManifestFetcher {
+
+ private final SmoothStreamingManifestParser parser;
+
+ /**
+ * @param callback The callback to provide with the parsed manifest (or error).
+ */
+ public SmoothStreamingManifestFetcher(ManifestCallback callback) {
+ super(callback);
+ parser = new SmoothStreamingManifestParser();
+ }
+
+ /**
+ * @param callback The callback to provide with the parsed manifest (or error).
+ * @param timeoutMillis The timeout in milliseconds for the connection used to load the data.
+ */
+ public SmoothStreamingManifestFetcher(ManifestCallback callback,
+ int timeoutMillis) {
+ super(callback, timeoutMillis);
+ parser = new SmoothStreamingManifestParser();
+ }
+
+ @Override
+ protected SmoothStreamingManifest parse(InputStream stream, String inputEncoding,
+ String contentId) throws IOException, ParserException {
+ try {
+ return parser.parse(stream, inputEncoding);
+ } catch (XmlPullParserException e) {
+ throw new ParserException(e);
+ }
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java
new file mode 100644
index 00000000000..d8834f1dd41
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java
@@ -0,0 +1,651 @@
+/*
+ * 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.smoothstreaming;
+
+import com.google.android.exoplayer.ParserException;
+import com.google.android.exoplayer.parser.mp4.CodecSpecificDataUtil;
+import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.ProtectionElement;
+import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement;
+import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement;
+import com.google.android.exoplayer.util.Assertions;
+
+import android.util.Base64;
+import android.util.Pair;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Parses SmoothStreaming client manifests.
+ *
+ * @see
+ * IIS Smooth Streaming Client Manifest Format
+ */
+public class SmoothStreamingManifestParser {
+
+ private final XmlPullParserFactory xmlParserFactory;
+
+ public SmoothStreamingManifestParser() {
+ try {
+ xmlParserFactory = XmlPullParserFactory.newInstance();
+ } catch (XmlPullParserException e) {
+ throw new RuntimeException("Couldn't create XmlPullParserFactory instance", e);
+ }
+ }
+
+ /**
+ * Parses a manifest from the provided {@link InputStream}.
+ *
+ * @param inputStream The stream from which to parse the manifest.
+ * @param inputEncoding The encoding of the input.
+ * @return The parsed manifest.
+ * @throws IOException If a problem occurred reading from the stream.
+ * @throws XmlPullParserException If a problem occurred parsing the stream as xml.
+ * @throws ParserException If a problem occurred parsing the xml as a smooth streaming manifest.
+ */
+ public SmoothStreamingManifest parse(InputStream inputStream, String inputEncoding) throws
+ XmlPullParserException, IOException, ParserException {
+ XmlPullParser xmlParser = xmlParserFactory.newPullParser();
+ xmlParser.setInput(inputStream, inputEncoding);
+ SmoothStreamMediaParser smoothStreamMediaParser = new SmoothStreamMediaParser(null);
+ return (SmoothStreamingManifest) smoothStreamMediaParser.parse(xmlParser);
+ }
+
+ /**
+ * Thrown if a required field is missing.
+ */
+ public static class MissingFieldException extends ParserException {
+
+ public MissingFieldException(String fieldName) {
+ super("Missing required field: " + fieldName);
+ }
+
+ }
+
+ /**
+ * A base class for parsers that parse components of a smooth streaming manifest.
+ */
+ private static abstract class ElementParser {
+
+ private final String tag;
+
+ private final ElementParser parent;
+ private final List> normalizedAttributes;
+
+ public ElementParser(String tag, ElementParser parent) {
+ this.tag = tag;
+ this.parent = parent;
+ this.normalizedAttributes = new LinkedList>();
+ }
+
+ public final Object parse(XmlPullParser xmlParser) throws XmlPullParserException, IOException,
+ ParserException {
+ String tagName;
+ boolean foundStartTag = false;
+ while (true) {
+ int eventType = xmlParser.getEventType();
+ switch (eventType) {
+ case XmlPullParser.START_TAG:
+ tagName = xmlParser.getName();
+ if (tag.equals(tagName)) {
+ foundStartTag = true;
+ parseStartTag(xmlParser);
+ } else if (foundStartTag) {
+ if (handleChildInline(tagName)) {
+ parseStartTag(xmlParser);
+ } else {
+ addChild(newChildParser(this, tagName).parse(xmlParser));
+ }
+ }
+ break;
+ case XmlPullParser.TEXT:
+ if (foundStartTag) {
+ parseText(xmlParser);
+ }
+ break;
+ case XmlPullParser.END_TAG:
+ if (foundStartTag) {
+ tagName = xmlParser.getName();
+ parseEndTag(xmlParser);
+ if (!handleChildInline(tagName)) {
+ return build();
+ }
+ }
+ break;
+ case XmlPullParser.END_DOCUMENT:
+ return null;
+ default:
+ // Do nothing.
+ break;
+ }
+ xmlParser.next();
+ }
+ }
+
+ private ElementParser newChildParser(ElementParser parent, String name) {
+ if (TrackElementParser.TAG.equals(name)) {
+ return new TrackElementParser(parent);
+ } else if (ProtectionElementParser.TAG.equals(name)) {
+ return new ProtectionElementParser(parent);
+ } else if (StreamElementParser.TAG.equals(name)) {
+ return new StreamElementParser(parent);
+ }
+ return null;
+ }
+
+ /**
+ * Stash an attribute that may be normalized at this level. In other words, an attribute that
+ * may have been pulled up from the child elements because its value was the same in all
+ * children.
+ *
+ * Stashing an attribute allows child element parsers to retrieve the values of normalized
+ * attributes using {@link #getNormalizedAttribute(String)}.
+ *
+ * @param key The name of the attribute.
+ * @param value The value of the attribute.
+ */
+ protected final void putNormalizedAttribute(String key, Object value) {
+ normalizedAttributes.add(Pair.create(key, value));
+ }
+
+ /**
+ * Attempt to retrieve a stashed normalized attribute. If there is no stashed attribute with
+ * the provided name, the parent element parser will be queried, and so on up the chain.
+ *
+ * @param key The name of the attribute.
+ * @return The stashed value, or null if the attribute was not be found.
+ */
+ protected final Object getNormalizedAttribute(String key) {
+ for (int i = 0; i < normalizedAttributes.size(); i++) {
+ Pair pair = normalizedAttributes.get(i);
+ if (pair.first.equals(key)) {
+ return pair.second;
+ }
+ }
+ return parent == null ? null : parent.getNormalizedAttribute(key);
+ }
+
+ /**
+ * Whether this {@link ElementParser} parses a child element inline.
+ *
+ * @param tagName The name of the child element.
+ * @return Whether the child is parsed inline.
+ */
+ protected boolean handleChildInline(String tagName) {
+ return false;
+ }
+
+ /**
+ * @param xmlParser The underlying {@link XmlPullParser}
+ * @throws ParserException
+ */
+ protected void parseStartTag(XmlPullParser xmlParser) throws ParserException {
+ // Do nothing.
+ }
+
+ /**
+ * @param xmlParser The underlying {@link XmlPullParser}
+ * @throws ParserException
+ */
+ protected void parseText(XmlPullParser xmlParser) throws ParserException {
+ // Do nothing.
+ }
+
+ /**
+ * @param xmlParser The underlying {@link XmlPullParser}
+ * @throws ParserException
+ */
+ protected void parseEndTag(XmlPullParser xmlParser) throws ParserException {
+ // Do nothing.
+ }
+
+ /**
+ * @param parsedChild A parsed child object.
+ */
+ protected void addChild(Object parsedChild) {
+ // Do nothing.
+ }
+
+ protected abstract Object build();
+
+ protected final String parseRequiredString(XmlPullParser parser, String key)
+ throws MissingFieldException {
+ String value = parser.getAttributeValue(null, key);
+ if (value != null) {
+ return value;
+ } else {
+ throw new MissingFieldException(key);
+ }
+ }
+
+ protected final int parseInt(XmlPullParser parser, String key, int defaultValue)
+ throws ParserException {
+ String value = parser.getAttributeValue(null, key);
+ if (value != null) {
+ try {
+ return Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ throw new ParserException(e);
+ }
+ } else {
+ return defaultValue;
+ }
+ }
+
+ protected final int parseRequiredInt(XmlPullParser parser, String key) throws ParserException {
+ String value = parser.getAttributeValue(null, key);
+ if (value != null) {
+ try {
+ return Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ throw new ParserException(e);
+ }
+ } else {
+ throw new MissingFieldException(key);
+ }
+ }
+
+ protected final long parseLong(XmlPullParser parser, String key, long defaultValue)
+ throws ParserException {
+ String value = parser.getAttributeValue(null, key);
+ if (value != null) {
+ try {
+ return Long.parseLong(value);
+ } catch (NumberFormatException e) {
+ throw new ParserException(e);
+ }
+ } else {
+ return defaultValue;
+ }
+ }
+
+ protected final long parseRequiredLong(XmlPullParser parser, String key)
+ throws ParserException {
+ String value = parser.getAttributeValue(null, key);
+ if (value != null) {
+ try {
+ return Long.parseLong(value);
+ } catch (NumberFormatException e) {
+ throw new ParserException(e);
+ }
+ } else {
+ throw new MissingFieldException(key);
+ }
+ }
+
+ }
+
+ private static class SmoothStreamMediaParser extends ElementParser {
+
+ public static final String TAG = "SmoothStreamingMedia";
+
+ private static final String KEY_MAJOR_VERSION = "MajorVersion";
+ private static final String KEY_MINOR_VERSION = "MinorVersion";
+ private static final String KEY_TIME_SCALE = "TimeScale";
+ private static final String KEY_DURATION = "Duration";
+ private static final String KEY_LOOKAHEAD_COUNT = "LookaheadCount";
+
+ private int majorVersion;
+ private int minorVersion;
+ private long timeScale;
+ private long duration;
+ private int lookAheadCount;
+ private ProtectionElement protectionElement;
+ private List streamElements;
+
+ public SmoothStreamMediaParser(ElementParser parent) {
+ super(TAG, parent);
+ lookAheadCount = -1;
+ protectionElement = null;
+ streamElements = new LinkedList();
+ }
+
+ @Override
+ public void parseStartTag(XmlPullParser parser) throws ParserException {
+ majorVersion = parseRequiredInt(parser, KEY_MAJOR_VERSION);
+ minorVersion = parseRequiredInt(parser, KEY_MINOR_VERSION);
+ timeScale = parseLong(parser, KEY_TIME_SCALE, 10000000L);
+ duration = parseRequiredLong(parser, KEY_DURATION);
+ lookAheadCount = parseInt(parser, KEY_LOOKAHEAD_COUNT, -1);
+ putNormalizedAttribute(KEY_TIME_SCALE, timeScale);
+ }
+
+ @Override
+ public void addChild(Object child) {
+ if (child instanceof StreamElement) {
+ streamElements.add((StreamElement) child);
+ } else if (child instanceof ProtectionElement) {
+ Assertions.checkState(protectionElement == null);
+ protectionElement = (ProtectionElement) child;
+ }
+ }
+
+ @Override
+ public Object build() {
+ StreamElement[] streamElementArray = new StreamElement[streamElements.size()];
+ streamElements.toArray(streamElementArray);
+ return new SmoothStreamingManifest(majorVersion, minorVersion, timeScale, duration,
+ lookAheadCount, protectionElement, streamElementArray);
+ }
+
+ }
+
+ private static class ProtectionElementParser extends ElementParser {
+
+ public static final String TAG = "Protection";
+ public static final String TAG_PROTECTION_HEADER = "ProtectionHeader";
+
+ public static final String KEY_SYSTEM_ID = "SystemID";
+
+ private UUID uuid;
+ private byte[] initData;
+
+ public ProtectionElementParser(ElementParser parent) {
+ super(TAG, parent);
+ }
+
+ @Override
+ public boolean handleChildInline(String tag) {
+ return TAG_PROTECTION_HEADER.equals(tag);
+ }
+
+ @Override
+ public void parseStartTag(XmlPullParser parser) {
+ if (!TAG_PROTECTION_HEADER.equals(parser.getName())) {
+ return;
+ }
+ String uuidString = parser.getAttributeValue(null, KEY_SYSTEM_ID);
+ uuid = UUID.fromString(uuidString);
+ }
+
+ @Override
+ public void parseText(XmlPullParser parser) {
+ initData = Base64.decode(parser.getText(), Base64.DEFAULT);
+ }
+
+ @Override
+ public Object build() {
+ return new ProtectionElement(uuid, initData);
+ }
+
+ }
+
+ private static class StreamElementParser extends ElementParser {
+
+ public static final String TAG = "StreamIndex";
+ private static final String TAG_STREAM_FRAGMENT = "c";
+
+ private static final String KEY_TYPE = "Type";
+ private static final String KEY_TYPE_AUDIO = "audio";
+ private static final String KEY_TYPE_VIDEO = "video";
+ private static final String KEY_TYPE_TEXT = "text";
+ private static final String KEY_SUB_TYPE = "Subtype";
+ private static final String KEY_NAME = "Name";
+ private static final String KEY_CHUNKS = "Chunks";
+ private static final String KEY_QUALITY_LEVELS = "QualityLevels";
+ private static final String KEY_URL = "Url";
+ private static final String KEY_MAX_WIDTH = "MaxWidth";
+ private static final String KEY_MAX_HEIGHT = "MaxHeight";
+ private static final String KEY_DISPLAY_WIDTH = "DisplayWidth";
+ private static final String KEY_DISPLAY_HEIGHT = "DisplayHeight";
+ private static final String KEY_LANGUAGE = "Language";
+ private static final String KEY_TIME_SCALE = "TimeScale";
+
+ private static final String KEY_FRAGMENT_DURATION = "d";
+ private static final String KEY_FRAGMENT_START_TIME = "t";
+
+ private final List tracks;
+
+ private int type;
+ private String subType;
+ private long timeScale;
+ private String name;
+ private int qualityLevels;
+ private String url;
+ private int maxWidth;
+ private int maxHeight;
+ private int displayWidth;
+ private int displayHeight;
+ private String language;
+ private long[] startTimes;
+
+ private int chunkIndex;
+ private long previousChunkDuration;
+
+ public StreamElementParser(ElementParser parent) {
+ super(TAG, parent);
+ tracks = new LinkedList();
+ }
+
+ @Override
+ public boolean handleChildInline(String tag) {
+ return TAG_STREAM_FRAGMENT.equals(tag);
+ }
+
+ @Override
+ public void parseStartTag(XmlPullParser parser) throws ParserException {
+ if (TAG_STREAM_FRAGMENT.equals(parser.getName())) {
+ parseStreamFragmentStartTag(parser);
+ } else {
+ parseStreamElementStartTag(parser);
+ }
+ }
+
+ private void parseStreamFragmentStartTag(XmlPullParser parser) throws ParserException {
+ startTimes[chunkIndex] = parseLong(parser, KEY_FRAGMENT_START_TIME, -1L);
+ if (startTimes[chunkIndex] == -1L) {
+ if (chunkIndex == 0) {
+ // Assume the track starts at t = 0.
+ startTimes[chunkIndex] = 0;
+ } else if (previousChunkDuration != -1L) {
+ // Infer the start time from the previous chunk's start time and duration.
+ startTimes[chunkIndex] = startTimes[chunkIndex - 1] + previousChunkDuration;
+ } else {
+ // We don't have the start time, and we're unable to infer it.
+ throw new ParserException("Unable to infer start time");
+ }
+ }
+ previousChunkDuration = parseLong(parser, KEY_FRAGMENT_DURATION, -1L);
+ chunkIndex++;
+ }
+
+ private void parseStreamElementStartTag(XmlPullParser parser) throws ParserException {
+ type = parseType(parser);
+ putNormalizedAttribute(KEY_TYPE, type);
+ if (type == StreamElement.TYPE_TEXT) {
+ subType = parseRequiredString(parser, KEY_SUB_TYPE);
+ } else {
+ subType = parser.getAttributeValue(null, KEY_SUB_TYPE);
+ }
+ name = parser.getAttributeValue(null, KEY_NAME);
+ qualityLevels = parseInt(parser, KEY_QUALITY_LEVELS, -1);
+ url = parseRequiredString(parser, KEY_URL);
+ maxWidth = parseInt(parser, KEY_MAX_WIDTH, -1);
+ maxHeight = parseInt(parser, KEY_MAX_HEIGHT, -1);
+ displayWidth = parseInt(parser, KEY_DISPLAY_WIDTH, -1);
+ displayHeight = parseInt(parser, KEY_DISPLAY_HEIGHT, -1);
+ language = parser.getAttributeValue(null, KEY_LANGUAGE);
+ timeScale = parseInt(parser, KEY_TIME_SCALE, -1);
+ if (timeScale == -1) {
+ timeScale = (Long) getNormalizedAttribute(KEY_TIME_SCALE);
+ }
+ startTimes = new long[parseRequiredInt(parser, KEY_CHUNKS)];
+ }
+
+ private int parseType(XmlPullParser parser) throws ParserException {
+ String value = parser.getAttributeValue(null, KEY_TYPE);
+ if (value != null) {
+ if (KEY_TYPE_AUDIO.equalsIgnoreCase(value)) {
+ return StreamElement.TYPE_AUDIO;
+ } else if (KEY_TYPE_VIDEO.equalsIgnoreCase(value)) {
+ return StreamElement.TYPE_VIDEO;
+ } else if (KEY_TYPE_TEXT.equalsIgnoreCase(value)) {
+ return StreamElement.TYPE_TEXT;
+ } else {
+ throw new ParserException("Invalid key value[" + value + "]");
+ }
+ }
+ throw new MissingFieldException(KEY_TYPE);
+ }
+
+ @Override
+ public void addChild(Object child) {
+ if (child instanceof TrackElement) {
+ tracks.add((TrackElement) child);
+ }
+ }
+
+ @Override
+ public Object build() {
+ TrackElement[] trackElements = new TrackElement[tracks.size()];
+ tracks.toArray(trackElements);
+ return new StreamElement(type, subType, timeScale, name, qualityLevels, url, maxWidth,
+ maxHeight, displayWidth, displayHeight, language, trackElements, startTimes);
+ }
+
+ }
+
+ private static class TrackElementParser extends ElementParser {
+
+ public static final String TAG = "QualityLevel";
+
+ private static final String KEY_INDEX = "Index";
+ private static final String KEY_BITRATE = "Bitrate";
+ private static final String KEY_CODEC_PRIVATE_DATA = "CodecPrivateData";
+ private static final String KEY_SAMPLING_RATE = "SamplingRate";
+ private static final String KEY_CHANNELS = "Channels";
+ private static final String KEY_BITS_PER_SAMPLE = "BitsPerSample";
+ private static final String KEY_PACKET_SIZE = "PacketSize";
+ private static final String KEY_AUDIO_TAG = "AudioTag";
+ private static final String KEY_FOUR_CC = "FourCC";
+ private static final String KEY_NAL_UNIT_LENGTH_FIELD = "NALUnitLengthField";
+ private static final String KEY_TYPE = "Type";
+ private static final String KEY_MAX_WIDTH = "MaxWidth";
+ private static final String KEY_MAX_HEIGHT = "MaxHeight";
+
+ private final List csd;
+
+ private int index;
+ private int bitrate;
+ private String fourCC;
+ private int profile;
+ private int level;
+ private int maxWidth;
+ private int maxHeight;
+ private int samplingRate;
+ private int channels;
+ private int packetSize;
+ private int audioTag;
+ private int bitPerSample;
+
+ private int nalUnitLengthField;
+ private String content;
+
+ public TrackElementParser(ElementParser parent) {
+ super(TAG, parent);
+ this.csd = new LinkedList();
+ }
+
+ @Override
+ public void parseStartTag(XmlPullParser parser) throws ParserException {
+ int type = (Integer) getNormalizedAttribute(KEY_TYPE);
+ content = null;
+ String value;
+
+ index = parseInt(parser, KEY_INDEX, -1);
+ bitrate = parseRequiredInt(parser, KEY_BITRATE);
+ nalUnitLengthField = parseInt(parser, KEY_NAL_UNIT_LENGTH_FIELD, 4);
+
+ if (type == StreamElement.TYPE_VIDEO) {
+ maxHeight = parseRequiredInt(parser, KEY_MAX_HEIGHT);
+ maxWidth = parseRequiredInt(parser, KEY_MAX_WIDTH);
+ } else {
+ maxHeight = -1;
+ maxWidth = -1;
+ }
+
+ if (type == StreamElement.TYPE_AUDIO) {
+ samplingRate = parseRequiredInt(parser, KEY_SAMPLING_RATE);
+ channels = parseRequiredInt(parser, KEY_CHANNELS);
+ bitPerSample = parseRequiredInt(parser, KEY_BITS_PER_SAMPLE);
+ packetSize = parseRequiredInt(parser, KEY_PACKET_SIZE);
+ audioTag = parseRequiredInt(parser, KEY_AUDIO_TAG);
+ fourCC = parseRequiredString(parser, KEY_FOUR_CC);
+ } else {
+ samplingRate = -1;
+ channels = -1;
+ bitPerSample = -1;
+ packetSize = -1;
+ audioTag = -1;
+ fourCC = parser.getAttributeValue(null, KEY_FOUR_CC);
+ }
+
+ value = parser.getAttributeValue(null, KEY_CODEC_PRIVATE_DATA);
+ if (value != null && value.length() > 0) {
+ byte[] codecPrivateData = hexStringToByteArray(value);
+ byte[][] split = CodecSpecificDataUtil.splitNalUnits(codecPrivateData);
+ if (split == null) {
+ csd.add(codecPrivateData);
+ } else {
+ for (int i = 0; i < split.length; i++) {
+ Pair spsParameters = CodecSpecificDataUtil.parseSpsNalUnit(split[i]);
+ if (spsParameters != null) {
+ profile = spsParameters.first;
+ level = spsParameters.second;
+ }
+ csd.add(split[i]);
+ }
+ }
+ }
+ }
+
+ private byte[] hexStringToByteArray(String hexString) {
+ int length = hexString.length();
+ byte[] data = new byte[length / 2];
+ for (int i = 0; i < data.length; i++) {
+ int stringOffset = i * 2;
+ data[i] = (byte) ((Character.digit(hexString.charAt(stringOffset), 16) << 4)
+ + Character.digit(hexString.charAt(stringOffset + 1), 16));
+ }
+ return data;
+ }
+
+ @Override
+ public void parseText(XmlPullParser parser) {
+ content = parser.getText();
+ }
+
+ @Override
+ public Object build() {
+ byte[][] csdArray = null;
+ if (!csd.isEmpty()) {
+ csdArray = new byte[csd.size()][];
+ csd.toArray(csdArray);
+ }
+ return new TrackElement(index, bitrate, fourCC, csdArray, profile, level, maxWidth, maxHeight,
+ samplingRate, channels, packetSize, audioTag, bitPerSample, nalUnitLengthField, content);
+ }
+
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/text/Subtitle.java b/library/src/main/java/com/google/android/exoplayer/text/Subtitle.java
new file mode 100644
index 00000000000..ff542cfb966
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/text/Subtitle.java
@@ -0,0 +1,72 @@
+/*
+ * 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.text;
+
+/**
+ * A subtitle that contains textual data associated with time indices.
+ */
+public interface Subtitle {
+
+ /**
+ * Gets the start time of the subtitle.
+ *
+ * Note that the value returned may be less than {@code getEventTime(0)}, since a subtitle may
+ * begin prior to the time of the first event.
+ *
+ * @return The start time of the subtitle in microseconds.
+ */
+ public long getStartTime();
+
+ /**
+ * Gets the index of the first event that occurs after a given time (exclusive).
+ *
+ * @param timeUs The time in microseconds.
+ * @return The index of the next event, or -1 if there are no events after the specified time.
+ */
+ public int getNextEventTimeIndex(long timeUs);
+
+ /**
+ * Gets the number of event times, where events are defined as points in time at which the text
+ * returned by {@link #getText(long)} changes.
+ *
+ * @return The number of event times.
+ */
+ public int getEventTimeCount();
+
+ /**
+ * Gets the event time at a specified index.
+ *
+ * @param index The index of the event time to obtain.
+ * @return The event time in microseconds.
+ */
+ public long getEventTime(int index);
+
+ /**
+ * Convenience method for obtaining the last event time.
+ *
+ * @return The time of the last event in microseconds, or -1 if {@code getEventTimeCount() == 0}.
+ */
+ public long getLastEventTime();
+
+ /**
+ * Retrieve the subtitle text that should be displayed at a given time.
+ *
+ * @param timeUs The time in microseconds.
+ * @return The text that should be displayed, or null.
+ */
+ public String getText(long timeUs);
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/text/SubtitleParser.java b/library/src/main/java/com/google/android/exoplayer/text/SubtitleParser.java
new file mode 100644
index 00000000000..e041373d6e8
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/text/SubtitleParser.java
@@ -0,0 +1,46 @@
+/*
+ * 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.text;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Parses {@link Subtitle}s from {@link InputStream}s.
+ */
+public interface SubtitleParser {
+
+ /**
+ * Checks whether the parser supports a given subtitle mime type.
+ *
+ * @param mimeType A subtitle mime type.
+ * @return Whether the mime type is supported.
+ */
+ public boolean canParse(String mimeType);
+
+ /**
+ * Parses a {@link Subtitle} from the provided {@link InputStream}.
+ *
+ * @param inputStream The stream from which to parse the subtitle.
+ * @param inputEncoding The encoding of the input stream.
+ * @param startTimeUs The start time of the subtitle.
+ * @return A parsed representation of the subtitle.
+ * @throws IOException If a problem occurred reading from the stream.
+ */
+ public Subtitle parse(InputStream inputStream, String inputEncoding, long startTimeUs)
+ throws IOException;
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java
new file mode 100644
index 00000000000..3e671d23029
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java
@@ -0,0 +1,299 @@
+/*
+ * 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.text;
+
+import com.google.android.exoplayer.ExoPlaybackException;
+import com.google.android.exoplayer.FormatHolder;
+import com.google.android.exoplayer.SampleHolder;
+import com.google.android.exoplayer.SampleSource;
+import com.google.android.exoplayer.TrackRenderer;
+import com.google.android.exoplayer.dash.mpd.AdaptationSet;
+import com.google.android.exoplayer.util.Assertions;
+import com.google.android.exoplayer.util.VerboseLogUtil;
+
+import android.annotation.TargetApi;
+import android.os.Handler;
+import android.os.Handler.Callback;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A {@link TrackRenderer} for textual subtitles. The actual rendering of each line of text to a
+ * suitable output (e.g. the display) is delegated to a {@link TextRenderer}.
+ */
+@TargetApi(16)
+public class TextTrackRenderer extends TrackRenderer implements Callback {
+
+ /**
+ * An interface for components that render text.
+ */
+ public interface TextRenderer {
+
+ /**
+ * Invoked each time there is a change in the text to be rendered.
+ *
+ * @param text The text to render, or null if no text is to be rendered.
+ */
+ void onText(String text);
+
+ }
+
+ private static final String TAG = "TextTrackRenderer";
+
+ private static final int MSG_UPDATE_OVERLAY = 0;
+
+ private final Handler textRendererHandler;
+ private final TextRenderer textRenderer;
+ private final SampleSource source;
+ private final SampleHolder sampleHolder;
+ private final FormatHolder formatHolder;
+ private final SubtitleParser subtitleParser;
+
+ private int trackIndex;
+
+ private long currentPositionUs;
+ private boolean inputStreamEnded;
+
+ private Subtitle subtitle;
+ private int nextSubtitleEventIndex;
+ private boolean textRendererNeedsUpdate;
+
+ /**
+ * @param source A source from which samples containing subtitle data can be read.
+ * @param subtitleParser A subtitle parser that will parse Subtitle objects from the source.
+ * @param textRenderer The text renderer.
+ * @param textRendererLooper The looper associated with the thread on which textRenderer should be
+ * invoked. If the renderer makes use of standard Android UI components, then this should
+ * normally be the looper associated with the applications' main thread, which can be
+ * obtained using {@link android.app.Activity#getMainLooper()}. Null may be passed if the
+ * renderer should be invoked directly on the player's internal rendering thread.
+ */
+ public TextTrackRenderer(SampleSource source, SubtitleParser subtitleParser,
+ TextRenderer textRenderer, Looper textRendererLooper) {
+ this.source = Assertions.checkNotNull(source);
+ this.subtitleParser = Assertions.checkNotNull(subtitleParser);
+ this.textRenderer = Assertions.checkNotNull(textRenderer);
+ this.textRendererHandler = textRendererLooper == null ? null : new Handler(textRendererLooper,
+ this);
+ formatHolder = new FormatHolder();
+ sampleHolder = new SampleHolder(true);
+ }
+
+ @Override
+ protected int doPrepare() throws ExoPlaybackException {
+ try {
+ boolean sourcePrepared = source.prepare();
+ if (!sourcePrepared) {
+ return TrackRenderer.STATE_UNPREPARED;
+ }
+ } catch (IOException e) {
+ throw new ExoPlaybackException(e);
+ }
+ for (int i = 0; i < source.getTrackCount(); i++) {
+ if (subtitleParser.canParse(source.getTrackInfo(i).mimeType)) {
+ trackIndex = i;
+ return TrackRenderer.STATE_PREPARED;
+ }
+ }
+ return TrackRenderer.STATE_IGNORE;
+ }
+
+ @Override
+ protected void onEnabled(long timeUs, boolean joining) {
+ source.enable(trackIndex, timeUs);
+ seekToInternal(timeUs);
+ }
+
+ @Override
+ protected void seekTo(long timeUs) {
+ source.seekToUs(timeUs);
+ seekToInternal(timeUs);
+ }
+
+ private void seekToInternal(long timeUs) {
+ inputStreamEnded = false;
+ currentPositionUs = timeUs;
+ source.seekToUs(timeUs);
+ if (subtitle != null && (timeUs < subtitle.getStartTime()
+ || subtitle.getLastEventTime() <= timeUs)) {
+ subtitle = null;
+ }
+ resetSampleData();
+ clearTextRenderer();
+ syncNextEventIndex(timeUs);
+ textRendererNeedsUpdate = subtitle != null;
+ }
+
+ @Override
+ protected void doSomeWork(long timeUs) throws ExoPlaybackException {
+ source.continueBuffering(timeUs);
+ currentPositionUs = timeUs;
+
+ // We're iterating through the events in a subtitle. Set textRendererNeedsUpdate if we advance
+ // to the next event.
+ if (subtitle != null) {
+ long nextEventTimeUs = getNextEventTime();
+ while (nextEventTimeUs <= timeUs) {
+ nextSubtitleEventIndex++;
+ nextEventTimeUs = getNextEventTime();
+ textRendererNeedsUpdate = true;
+ }
+ if (nextEventTimeUs == Long.MAX_VALUE) {
+ // We've finished processing the subtitle.
+ subtitle = null;
+ }
+ }
+
+ // We don't have a subtitle. Try and read the next one from the source, and if we succeed then
+ // sync and set textRendererNeedsUpdate.
+ if (subtitle == null) {
+ boolean resetSampleHolder = false;
+ try {
+ int result = source.readData(trackIndex, timeUs, formatHolder, sampleHolder, false);
+ if (result == SampleSource.SAMPLE_READ) {
+ resetSampleHolder = true;
+ InputStream subtitleInputStream =
+ new ByteArrayInputStream(sampleHolder.data.array(), 0, sampleHolder.size);
+ subtitle = subtitleParser.parse(subtitleInputStream, "UTF-8", sampleHolder.timeUs);
+ syncNextEventIndex(timeUs);
+ textRendererNeedsUpdate = true;
+ } else if (result == SampleSource.END_OF_STREAM) {
+ inputStreamEnded = true;
+ }
+ } catch (IOException e) {
+ resetSampleHolder = true;
+ throw new ExoPlaybackException(e);
+ } finally {
+ if (resetSampleHolder) {
+ resetSampleData();
+ }
+ }
+ }
+
+ // Update the text renderer if we're both playing and textRendererNeedsUpdate is set.
+ if (textRendererNeedsUpdate && getState() == TrackRenderer.STATE_STARTED) {
+ textRendererNeedsUpdate = false;
+ if (subtitle == null) {
+ clearTextRenderer();
+ } else {
+ updateTextRenderer(timeUs);
+ }
+ }
+ }
+
+ @Override
+ protected void onDisabled() {
+ source.disable(trackIndex);
+ subtitle = null;
+ resetSampleData();
+ clearTextRenderer();
+ }
+
+ @Override
+ protected void onReleased() {
+ source.release();
+ }
+
+ @Override
+ protected long getCurrentPositionUs() {
+ return currentPositionUs;
+ }
+
+ @Override
+ protected long getDurationUs() {
+ return source.getTrackInfo(trackIndex).durationUs;
+ }
+
+ @Override
+ protected long getBufferedPositionUs() {
+ // Don't block playback whilst subtitles are loading.
+ return END_OF_TRACK;
+ }
+
+ @Override
+ protected boolean isEnded() {
+ return inputStreamEnded && subtitle == null;
+ }
+
+ @Override
+ protected boolean isReady() {
+ // Don't block playback whilst subtitles are loading.
+ // Note: To change this behavior, it will be necessary to consider [redacted].
+ return true;
+ }
+
+ private void syncNextEventIndex(long timeUs) {
+ nextSubtitleEventIndex = subtitle == null ? -1 : subtitle.getNextEventTimeIndex(timeUs);
+ }
+
+ private long getNextEventTime() {
+ return ((nextSubtitleEventIndex == -1)
+ || (nextSubtitleEventIndex >= subtitle.getEventTimeCount())) ? Long.MAX_VALUE
+ : (subtitle.getEventTime(nextSubtitleEventIndex));
+ }
+
+ private void resetSampleData() {
+ if (sampleHolder.data != null) {
+ sampleHolder.data.position(0);
+ }
+ }
+
+ private void updateTextRenderer(long timeUs) {
+ String text = subtitle.getText(timeUs);
+ log("updateTextRenderer; text=: " + text);
+ if (textRendererHandler != null) {
+ textRendererHandler.obtainMessage(MSG_UPDATE_OVERLAY, text).sendToTarget();
+ } else {
+ invokeTextRenderer(text);
+ }
+ }
+
+ private void clearTextRenderer() {
+ log("clearTextRenderer");
+ if (textRendererHandler != null) {
+ textRendererHandler.obtainMessage(MSG_UPDATE_OVERLAY, null).sendToTarget();
+ } else {
+ invokeTextRenderer(null);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_UPDATE_OVERLAY:
+ invokeTextRenderer((String) msg.obj);
+ return true;
+ }
+ return false;
+ }
+
+ private void invokeTextRenderer(String text) {
+ textRenderer.onText(text);
+ }
+
+ private void log(String logMessage) {
+ if (VerboseLogUtil.isTagEnabled(TAG)) {
+ Log.v(TAG, "type=" + AdaptationSet.TYPE_TEXT + ", " + logMessage);
+ }
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlNode.java b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlNode.java
new file mode 100644
index 00000000000..4ee1726b913
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlNode.java
@@ -0,0 +1,172 @@
+/*
+ * 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.text.ttml;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.TreeSet;
+
+/**
+ * A package internal representation of TTML node.
+ */
+/* package */ final class TtmlNode {
+
+ public static final long UNDEFINED_TIME = -1;
+
+ public static final String TAG_TT = "tt";
+ public static final String TAG_HEAD = "head";
+ public static final String TAG_BODY = "body";
+ public static final String TAG_DIV = "div";
+ public static final String TAG_P = "p";
+ public static final String TAG_SPAN = "span";
+ public static final String TAG_BR = "br";
+ public static final String TAG_STYLE = "style";
+ public static final String TAG_STYLING = "styling";
+ public static final String TAG_LAYOUT = "layout";
+ public static final String TAG_REGION = "region";
+ public static final String TAG_METADATA = "metadata";
+ public static final String TAG_SMPTE_IMAGE = "smpte:image";
+ public static final String TAG_SMPTE_DATA = "smpte:data";
+ public static final String TAG_SMPTE_INFORMATION = "smpte:information";
+
+ public final String tag;
+ public final boolean isTextNode;
+ public final String text;
+ public final long startTimeUs;
+ public final long endTimeUs;
+
+ private List children;
+
+ public static TtmlNode buildTextNode(String text) {
+ return new TtmlNode(null, applySpacePolicy(text, true), UNDEFINED_TIME, UNDEFINED_TIME);
+ }
+
+ public static TtmlNode buildNode(String tag, long startTimeUs, long endTimeUs) {
+ return new TtmlNode(tag, null, startTimeUs, endTimeUs);
+ }
+
+ private TtmlNode(String tag, String text, long startTimeUs, long endTimeUs) {
+ this.tag = tag;
+ this.text = text;
+ this.isTextNode = text != null;
+ this.startTimeUs = startTimeUs;
+ this.endTimeUs = endTimeUs;
+ }
+
+ public boolean isActive(long timeUs) {
+ return (startTimeUs == UNDEFINED_TIME && endTimeUs == UNDEFINED_TIME)
+ || (startTimeUs <= timeUs && endTimeUs == UNDEFINED_TIME)
+ || (startTimeUs == UNDEFINED_TIME && timeUs < endTimeUs)
+ || (startTimeUs <= timeUs && timeUs < endTimeUs);
+ }
+
+ public void addChild(TtmlNode child) {
+ if (children == null) {
+ children = new ArrayList();
+ }
+ children.add(child);
+ }
+
+ public TtmlNode getChild(int index) {
+ if (children == null) {
+ throw new IndexOutOfBoundsException();
+ }
+ return children.get(index);
+ }
+
+ public int getChildCount() {
+ return children == null ? 0 : children.size();
+ }
+
+ public long[] getEventTimesUs() {
+ TreeSet eventTimeSet = new TreeSet();
+ getEventTimes(eventTimeSet, false);
+ long[] eventTimes = new long[eventTimeSet.size()];
+ Iterator eventTimeIterator = eventTimeSet.iterator();
+ int i = 0;
+ while (eventTimeIterator.hasNext()) {
+ long eventTimeUs = eventTimeIterator.next();
+ eventTimes[i++] = eventTimeUs;
+ }
+ return eventTimes;
+ }
+
+ private void getEventTimes(TreeSet out, boolean descendsPNode) {
+ boolean isPNode = TAG_P.equals(tag);
+ if (descendsPNode || isPNode) {
+ if (startTimeUs != UNDEFINED_TIME) {
+ out.add(startTimeUs);
+ }
+ if (endTimeUs != UNDEFINED_TIME) {
+ out.add(endTimeUs);
+ }
+ }
+ if (children == null) {
+ return;
+ }
+ for (int i = 0; i < children.size(); i++) {
+ children.get(i).getEventTimes(out, descendsPNode || isPNode);
+ }
+ }
+
+ public String getText(long timeUs) {
+ StringBuilder builder = new StringBuilder();
+ getText(timeUs, builder, false);
+ return applySpacePolicy(builder.toString().replaceAll("\n$", ""), false);
+ }
+
+ private void getText(long timeUs, StringBuilder builder, boolean descendsPNode) {
+ if (isTextNode && descendsPNode) {
+ builder.append(text);
+ } else if (TAG_BR.equals(tag) && descendsPNode) {
+ builder.append("\n");
+ } else if (TAG_METADATA.equals(tag)) {
+ // Do nothing.
+ } else if (isActive(timeUs)) {
+ boolean isPNode = TAG_P.equals(tag);
+ int length = builder.length();
+ for (int i = 0; i < getChildCount(); ++i) {
+ getChild(i).getText(timeUs, builder, descendsPNode || isPNode);
+ }
+ if (isPNode && length != builder.length()) {
+ builder.append("\n");
+ }
+ }
+ }
+
+ /**
+ * Applies the space policy to the given string. See:
+ * The default space
+ * policy
+ *
+ * @param in A string to apply the policy.
+ * @param treatLineFeedAsSpace Whether to convert line feeds to spaces.
+ */
+ private static String applySpacePolicy(String in, boolean treatLineFeedAsSpace) {
+ // Removes carriage return followed by line feed. See: http://www.w3.org/TR/xml/#sec-line-ends
+ String out = in.replaceAll("\r\n", "\n");
+ // Apply suppress-at-line-break="auto" and
+ // white-space-treatment="ignore-if-surrounding-linefeed"
+ out = out.replaceAll(" *\n *", "\n");
+ // Apply linefeed-treatment="treat-as-space"
+ out = treatLineFeedAsSpace ? out.replaceAll("\n", " ") : out;
+ // Apply white-space-collapse="true"
+ out = out.replaceAll("[ \t\\x0B\f\r]+", " ");
+ return out;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java
new file mode 100644
index 00000000000..6fdf7d546f5
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java
@@ -0,0 +1,256 @@
+/*
+ * 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.text.ttml;
+
+import com.google.android.exoplayer.text.Subtitle;
+import com.google.android.exoplayer.text.SubtitleParser;
+import com.google.android.exoplayer.util.MimeTypes;
+
+import android.util.Log;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.LinkedList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A simple TTML parser that supports DFXP presentation profile.
+ *
+ * Supported features in this parser are:
+ *
+ * content
+ * core
+ * presentation
+ * profile
+ * structure
+ * time-offset
+ * timing
+ * tickRate
+ * time-clock-with-frames
+ * time-clock
+ * time-offset-with-frames
+ * time-offset-with-ticks
+ *
+ *
+ * @see TTML specification
+ */
+public class TtmlParser implements SubtitleParser {
+
+ private static final String TAG = "TtmlParser";
+
+ private static final String ATTR_BEGIN = "begin";
+ private static final String ATTR_DURATION = "dur";
+ private static final String ATTR_END = "end";
+
+ private static final Pattern CLOCK_TIME =
+ Pattern.compile("^([0-9][0-9]+):([0-9][0-9]):([0-9][0-9])"
+ + "(?:(\\.[0-9]+)|:([0-9][0-9])(?:\\.([0-9]+))?)?$");
+ private static final Pattern OFFSET_TIME =
+ Pattern.compile("^([0-9]+(?:\\.[0-9]+)?)(h|m|s|ms|f|t)$");
+
+ // TODO: read and apply the following attributes if specified.
+ private static final int DEFAULT_FRAMERATE = 30;
+ private static final int DEFAULT_SUBFRAMERATE = 1;
+ private static final int DEFAULT_TICKRATE = 1;
+
+ private final XmlPullParserFactory xmlParserFactory;
+
+ public TtmlParser() {
+ try {
+ xmlParserFactory = XmlPullParserFactory.newInstance();
+ } catch (XmlPullParserException e) {
+ throw new RuntimeException("Couldn't create XmlPullParserFactory instance", e);
+ }
+ }
+
+ @Override
+ public Subtitle parse(InputStream inputStream, String inputEncoding, long startTimeUs)
+ throws IOException {
+ try {
+ XmlPullParser xmlParser = xmlParserFactory.newPullParser();
+ xmlParser.setInput(inputStream, inputEncoding);
+ TtmlSubtitle ttmlSubtitle = null;
+ LinkedList nodeStack = new LinkedList();
+ int unsupportedTagDepth = 0;
+ int eventType = xmlParser.getEventType();
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ TtmlNode parent = nodeStack.peekLast();
+ if (unsupportedTagDepth == 0) {
+ String name = xmlParser.getName();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (!isSupportedTag(name)) {
+ Log.w(TAG, "Ignoring unsupported tag: " + xmlParser.getName());
+ unsupportedTagDepth++;
+ } else {
+ TtmlNode node = parseNode(xmlParser, parent);
+ nodeStack.addLast(node);
+ if (parent != null) {
+ parent.addChild(node);
+ }
+ }
+ } else if (eventType == XmlPullParser.TEXT) {
+ parent.addChild(TtmlNode.buildTextNode(xmlParser.getText()));
+ } else if (eventType == XmlPullParser.END_TAG) {
+ if (xmlParser.getName().equals(TtmlNode.TAG_TT)) {
+ ttmlSubtitle = new TtmlSubtitle(nodeStack.getLast(), startTimeUs);
+ }
+ nodeStack.removeLast();
+ }
+ } else {
+ if (eventType == XmlPullParser.START_TAG) {
+ unsupportedTagDepth++;
+ } else if (eventType == XmlPullParser.END_TAG) {
+ unsupportedTagDepth--;
+ }
+ }
+ xmlParser.next();
+ eventType = xmlParser.getEventType();
+ }
+ return ttmlSubtitle;
+ } catch (XmlPullParserException xppe) {
+ throw new IOException("Unable to parse source", xppe);
+ }
+ }
+
+ @Override
+ public boolean canParse(String mimeType) {
+ return MimeTypes.APPLICATION_TTML.equals(mimeType);
+ }
+
+ private TtmlNode parseNode(XmlPullParser parser, TtmlNode parent) {
+ long duration = 0;
+ long startTime = TtmlNode.UNDEFINED_TIME;
+ long endTime = TtmlNode.UNDEFINED_TIME;
+ int attributeCount = parser.getAttributeCount();
+ for (int i = 0; i < attributeCount; i++) {
+ // TODO: check if it's safe to ignore the namespace of attributes as follows.
+ String attr = parser.getAttributeName(i).replaceFirst("^.*:", "");
+ String value = parser.getAttributeValue(i);
+ if (attr.equals(ATTR_BEGIN)) {
+ startTime = parseTimeExpression(value,
+ DEFAULT_FRAMERATE, DEFAULT_SUBFRAMERATE, DEFAULT_TICKRATE);
+ } else if (attr.equals(ATTR_END)) {
+ endTime = parseTimeExpression(value,
+ DEFAULT_FRAMERATE, DEFAULT_SUBFRAMERATE, DEFAULT_TICKRATE);
+ } else if (attr.equals(ATTR_DURATION)) {
+ duration = parseTimeExpression(value,
+ DEFAULT_FRAMERATE, DEFAULT_SUBFRAMERATE, DEFAULT_TICKRATE);
+ } else {
+ // Do nothing.
+ }
+ }
+ if (parent != null && parent.startTimeUs != TtmlNode.UNDEFINED_TIME) {
+ if (startTime != TtmlNode.UNDEFINED_TIME) {
+ startTime += parent.startTimeUs;
+ }
+ if (endTime != TtmlNode.UNDEFINED_TIME) {
+ endTime += parent.startTimeUs;
+ }
+ }
+ if (endTime == TtmlNode.UNDEFINED_TIME) {
+ if (duration > 0) {
+ // Infer the end time from the duration.
+ endTime = startTime + duration;
+ } else if (parent != null && parent.endTimeUs != TtmlNode.UNDEFINED_TIME) {
+ // If the end time remains unspecified, then it should be inherited from the parent.
+ endTime = parent.endTimeUs;
+ }
+ }
+ return TtmlNode.buildNode(parser.getName(), startTime, endTime);
+ }
+
+ private static boolean isSupportedTag(String tag) {
+ if (tag.equals(TtmlNode.TAG_TT)
+ || tag.equals(TtmlNode.TAG_HEAD)
+ || tag.equals(TtmlNode.TAG_BODY)
+ || tag.equals(TtmlNode.TAG_DIV)
+ || tag.equals(TtmlNode.TAG_P)
+ || tag.equals(TtmlNode.TAG_SPAN)
+ || tag.equals(TtmlNode.TAG_BR)
+ || tag.equals(TtmlNode.TAG_STYLE)
+ || tag.equals(TtmlNode.TAG_STYLING)
+ || tag.equals(TtmlNode.TAG_LAYOUT)
+ || tag.equals(TtmlNode.TAG_REGION)
+ || tag.equals(TtmlNode.TAG_METADATA)
+ || tag.equals(TtmlNode.TAG_SMPTE_IMAGE)
+ || tag.equals(TtmlNode.TAG_SMPTE_DATA)
+ || tag.equals(TtmlNode.TAG_SMPTE_INFORMATION)) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Parses a time expression, returning the parsed timestamp.
+ *
+ * For the format of a time expression, see:
+ * timeExpression
+ *
+ * @param time A string that includes the time expression.
+ * @param frameRate The framerate of the stream.
+ * @param subframeRate The sub-framerate of the stream
+ * @param tickRate The tick rate of the stream.
+ * @return The parsed timestamp in microseconds.
+ * @throws NumberFormatException If the given string does not contain a valid time expression.
+ */
+ private static long parseTimeExpression(String time, int frameRate, int subframeRate,
+ int tickRate) {
+ Matcher matcher = CLOCK_TIME.matcher(time);
+ if (matcher.matches()) {
+ String hours = matcher.group(1);
+ double durationSeconds = Long.parseLong(hours) * 3600;
+ String minutes = matcher.group(2);
+ durationSeconds += Long.parseLong(minutes) * 60;
+ String seconds = matcher.group(3);
+ durationSeconds += Long.parseLong(seconds);
+ String fraction = matcher.group(4);
+ durationSeconds += (fraction != null) ? Double.parseDouble(fraction) : 0;
+ String frames = matcher.group(5);
+ durationSeconds += (frames != null) ? ((double) Long.parseLong(frames)) / frameRate : 0;
+ String subframes = matcher.group(6);
+ durationSeconds += (subframes != null) ?
+ ((double) Long.parseLong(subframes)) / subframeRate / frameRate : 0;
+ return (long) (durationSeconds * 1000000);
+ }
+ matcher = OFFSET_TIME.matcher(time);
+ if (matcher.matches()) {
+ String timeValue = matcher.group(1);
+ double value = Double.parseDouble(timeValue);
+ String unit = matcher.group(2);
+ if (unit.equals("h")) {
+ value *= 3600L * 1000000L;
+ } else if (unit.equals("m")) {
+ value *= 60 * 1000000;
+ } else if (unit.equals("s")) {
+ value *= 1000000;
+ } else if (unit.equals("ms")) {
+ value *= 1000;
+ } else if (unit.equals("f")) {
+ value = value / frameRate * 1000000;
+ } else if (unit.equals("t")) {
+ value = value / tickRate * 1000000;
+ }
+ return (long) value;
+ }
+ throw new NumberFormatException("Malformed time expression: " + time);
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlSubtitle.java b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlSubtitle.java
new file mode 100644
index 00000000000..9e7299a8d26
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlSubtitle.java
@@ -0,0 +1,69 @@
+/*
+ * 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.text.ttml;
+
+import com.google.android.exoplayer.text.Subtitle;
+
+import java.util.Arrays;
+
+/**
+ * A representation of a TTML subtitle.
+ */
+public final class TtmlSubtitle implements Subtitle {
+
+ private final TtmlNode root;
+ private final long startTimeUs;
+ private final long[] eventTimesUs;
+
+ public TtmlSubtitle(TtmlNode root, long startTimeUs) {
+ this.root = root;
+ this.startTimeUs = startTimeUs;
+ this.eventTimesUs = root.getEventTimesUs();
+ }
+
+ @Override
+ public long getStartTime() {
+ return startTimeUs;
+ }
+
+ @Override
+ public int getNextEventTimeIndex(long timeUs) {
+ int index = Arrays.binarySearch(eventTimesUs, timeUs - startTimeUs);
+ index = index >= 0 ? index + 1 : ~index;
+ return index < eventTimesUs.length ? index : -1;
+ }
+
+ @Override
+ public int getEventTimeCount() {
+ return eventTimesUs.length;
+ }
+
+ @Override
+ public long getEventTime(int index) {
+ return eventTimesUs[index] + startTimeUs;
+ }
+
+ @Override
+ public long getLastEventTime() {
+ return (eventTimesUs.length == 0 ? -1 : eventTimesUs[eventTimesUs.length - 1]) + startTimeUs;
+ }
+
+ @Override
+ public String getText(long timeUs) {
+ return root.getText(timeUs - startTimeUs);
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/Allocation.java b/library/src/main/java/com/google/android/exoplayer/upstream/Allocation.java
new file mode 100644
index 00000000000..2b7619595b3
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/upstream/Allocation.java
@@ -0,0 +1,55 @@
+/*
+ * 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.upstream;
+
+/**
+ * An {@link Allocation}, defined to consist of a set of fragments of underlying byte arrays.
+ *
+ * The byte arrays in which the fragments are located are obtained by {@link #getBuffers}. For
+ * each, the offset and length of the fragment within the byte array are obtained using
+ * {@link #getFragmentOffset} and {@link #getFragmentLength} respectively.
+ */
+public interface Allocation {
+
+ /**
+ * Gets the buffers in which the fragments are allocated.
+ *
+ * @return The buffers in which the fragments are allocated.
+ */
+ public byte[][] getBuffers();
+
+ /**
+ * The offset of the fragment in the buffer at the specified index.
+ *
+ * @param index The index of the buffer.
+ * @return The offset of the fragment in the buffer.
+ */
+ public int getFragmentOffset(int index);
+
+ /**
+ * The length of the fragment in the buffer at the specified index.
+ *
+ * @param index The index of the buffer.
+ * @return The length of the fragment in the buffer.
+ */
+ public int getFragmentLength(int index);
+
+ /**
+ * Releases the allocation.
+ */
+ public void release();
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/Allocator.java b/library/src/main/java/com/google/android/exoplayer/upstream/Allocator.java
new file mode 100644
index 00000000000..9d07265cfc6
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/upstream/Allocator.java
@@ -0,0 +1,47 @@
+/*
+ * 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.upstream;
+
+/**
+ * A source of {@link Allocation}s.
+ */
+public interface Allocator {
+
+ /**
+ * Obtains an allocation of at least the specified size.
+ *
+ * @param size The size of the required allocation, in bytes.
+ * @return The allocation.
+ */
+ public Allocation allocate(int size);
+
+ /**
+ * Hints to the {@link Allocator} that it should make a best effort to release any memory that it
+ * has allocated for the purpose of backing {@link Allocation}s, beyond the specified target
+ * number of bytes.
+ *
+ * @param targetSize The target size in bytes.
+ */
+ public void trim(int targetSize);
+
+ /**
+ * Returns the number of bytes currently allocated in the form of {@link Allocation}s.
+ *
+ * @return The number of allocated bytes.
+ */
+ public int getAllocatedSize();
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/BandwidthMeter.java b/library/src/main/java/com/google/android/exoplayer/upstream/BandwidthMeter.java
new file mode 100644
index 00000000000..0007597e681
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/upstream/BandwidthMeter.java
@@ -0,0 +1,35 @@
+/*
+ * 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.upstream;
+
+/**
+ * Provides estimates of the currently available bandwidth.
+ */
+public interface BandwidthMeter {
+
+ /**
+ * Indicates no bandwidth estimate is available.
+ */
+ final long NO_ESTIMATE = -1;
+
+ /**
+ * Gets the estimated bandwidth.
+ *
+ * @return Estimated bandwidth in bytes/sec, or {@link #NO_ESTIMATE} if no estimate is available.
+ */
+ long getEstimate();
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/BufferPool.java b/library/src/main/java/com/google/android/exoplayer/upstream/BufferPool.java
new file mode 100644
index 00000000000..979dc39e463
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/upstream/BufferPool.java
@@ -0,0 +1,140 @@
+/*
+ * 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.upstream;
+
+import com.google.android.exoplayer.util.Assertions;
+
+import java.util.Arrays;
+
+/**
+ * An {@link Allocator} that maintains a pool of fixed length byte arrays (buffers).
+ *
+ * An {@link Allocation} obtained from a {@link BufferPool} consists of the whole number of these
+ * buffers. When an {@link Allocation} is released, the underlying buffers are returned to the pool
+ * for re-use.
+ */
+public final class BufferPool implements Allocator {
+
+ private static final int INITIAL_RECYCLED_BUFFERS_CAPACITY = 100;
+
+ /**
+ * The length in bytes of each individual buffer in the pool.
+ */
+ public final int bufferLength;
+
+ private int allocatedBufferCount;
+ private int recycledBufferCount;
+ private byte[][] recycledBuffers;
+
+ /**
+ * Constructs an empty pool.
+ *
+ * @param bufferLength The length of each buffer in the pool.
+ */
+ public BufferPool(int bufferLength) {
+ Assertions.checkArgument(bufferLength > 0);
+ this.bufferLength = bufferLength;
+ this.recycledBuffers = new byte[INITIAL_RECYCLED_BUFFERS_CAPACITY][];
+ }
+
+ @Override
+ public synchronized int getAllocatedSize() {
+ return allocatedBufferCount * bufferLength;
+ }
+
+ @Override
+ public synchronized void trim(int targetSize) {
+ int targetBufferCount = (targetSize + bufferLength - 1) / bufferLength;
+ int targetRecycledBufferCount = Math.max(0, targetBufferCount - allocatedBufferCount);
+ if (targetRecycledBufferCount < recycledBufferCount) {
+ Arrays.fill(recycledBuffers, targetRecycledBufferCount, recycledBufferCount, null);
+ recycledBufferCount = targetRecycledBufferCount;
+ }
+ }
+
+ @Override
+ public synchronized Allocation allocate(int size) {
+ int requiredBufferCount = requiredBufferCount(size);
+ allocatedBufferCount += requiredBufferCount;
+ byte[][] buffers = new byte[requiredBufferCount][];
+ for (int i = 0; i < requiredBufferCount; i++) {
+ // Use a recycled buffer if one is available. Else instantiate a new one.
+ buffers[i] = recycledBufferCount > 0 ? recycledBuffers[--recycledBufferCount] :
+ new byte[bufferLength];
+ }
+ return new AllocationImpl(buffers);
+ }
+
+ /**
+ * Returns the buffers belonging to an allocation to the pool.
+ *
+ * @param allocation The allocation to return.
+ */
+ /* package */ synchronized void release(AllocationImpl allocation) {
+ byte[][] buffers = allocation.getBuffers();
+ allocatedBufferCount -= buffers.length;
+
+ int newRecycledBufferCount = recycledBufferCount + buffers.length;
+ if (recycledBuffers.length < newRecycledBufferCount) {
+ // Expand the capacity of the recycled buffers array.
+ byte[][] newRecycledBuffers = new byte[newRecycledBufferCount * 2][];
+ if (recycledBufferCount > 0) {
+ System.arraycopy(recycledBuffers, 0, newRecycledBuffers, 0, recycledBufferCount);
+ }
+ recycledBuffers = newRecycledBuffers;
+ }
+ System.arraycopy(buffers, 0, recycledBuffers, recycledBufferCount, buffers.length);
+ recycledBufferCount = newRecycledBufferCount;
+ }
+
+ private int requiredBufferCount(long size) {
+ return (int) ((size + bufferLength - 1) / bufferLength);
+ }
+
+ private class AllocationImpl implements Allocation {
+
+ private byte[][] buffers;
+
+ public AllocationImpl(byte[][] buffers) {
+ this.buffers = buffers;
+ }
+
+ @Override
+ public byte[][] getBuffers() {
+ return buffers;
+ }
+
+ @Override
+ public int getFragmentOffset(int index) {
+ return 0;
+ }
+
+ @Override
+ public int getFragmentLength(int index) {
+ return bufferLength;
+ }
+
+ @Override
+ public void release() {
+ if (buffers != null) {
+ BufferPool.this.release(this);
+ buffers = null;
+ }
+ }
+
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/ByteArrayDataSink.java b/library/src/main/java/com/google/android/exoplayer/upstream/ByteArrayDataSink.java
new file mode 100644
index 00000000000..18b79ea5476
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/upstream/ByteArrayDataSink.java
@@ -0,0 +1,60 @@
+/*
+ * 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.upstream;
+
+import com.google.android.exoplayer.util.Assertions;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+/**
+ * A {@link DataSink} for writing to a byte array.
+ */
+public class ByteArrayDataSink implements DataSink {
+
+ private ByteArrayOutputStream stream;
+
+ @Override
+ public DataSink open(DataSpec dataSpec) throws IOException {
+ if (dataSpec.length == DataSpec.LENGTH_UNBOUNDED) {
+ stream = new ByteArrayOutputStream();
+ } else {
+ Assertions.checkArgument(dataSpec.length <= Integer.MAX_VALUE);
+ stream = new ByteArrayOutputStream((int) dataSpec.length);
+ }
+ return this;
+ }
+
+ @Override
+ public void close() throws IOException {
+ stream.close();
+ }
+
+ @Override
+ public void write(byte[] buffer, int offset, int length) throws IOException {
+ stream.write(buffer, offset, length);
+ }
+
+ /**
+ * Returns the data written to the sink since the last call to {@link #open(DataSpec)}.
+ *
+ * @return The data, or null if {@link #open(DataSpec)} has never been called.
+ */
+ public byte[] getData() {
+ return stream == null ? null : stream.toByteArray();
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/ByteArrayDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/ByteArrayDataSource.java
new file mode 100644
index 00000000000..937c9bc229b
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/upstream/ByteArrayDataSource.java
@@ -0,0 +1,61 @@
+/*
+ * 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.upstream;
+
+import com.google.android.exoplayer.util.Assertions;
+
+import java.io.IOException;
+
+/**
+ * A {@link DataSource} for reading from a byte array.
+ */
+public class ByteArrayDataSource implements DataSource {
+
+ private final byte[] data;
+ private int readPosition;
+
+ /**
+ * @param data The data to be read.
+ */
+ public ByteArrayDataSource(byte[] data) {
+ this.data = Assertions.checkNotNull(data);
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ if (dataSpec.length == DataSpec.LENGTH_UNBOUNDED) {
+ Assertions.checkArgument(dataSpec.position < data.length);
+ } else {
+ Assertions.checkArgument(dataSpec.position + dataSpec.length <= data.length);
+ }
+ readPosition = (int) dataSpec.position;
+ return (dataSpec.length == DataSpec.LENGTH_UNBOUNDED)
+ ? (data.length - dataSpec.position) : dataSpec.length;
+ }
+
+ @Override
+ public void close() throws IOException {
+ // Do nothing.
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int length) throws IOException {
+ System.arraycopy(data, readPosition, buffer, offset, length);
+ readPosition += length;
+ return length;
+ }
+}
+
diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/ByteArrayNonBlockingInputStream.java b/library/src/main/java/com/google/android/exoplayer/upstream/ByteArrayNonBlockingInputStream.java
new file mode 100644
index 00000000000..b73a21a4bd4
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/upstream/ByteArrayNonBlockingInputStream.java
@@ -0,0 +1,83 @@
+/*
+ * 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.upstream;
+
+import com.google.android.exoplayer.util.Assertions;
+
+import java.nio.ByteBuffer;
+
+/**
+ * An implementation of {@link NonBlockingInputStream} for reading data from a byte array.
+ */
+public final class ByteArrayNonBlockingInputStream implements NonBlockingInputStream {
+
+ private final byte[] data;
+
+ private int position;
+
+ public ByteArrayNonBlockingInputStream(byte[] data) {
+ this.data = Assertions.checkNotNull(data);
+ }
+
+ @Override
+ public int skip(int length) {
+ int skipLength = getReadLength(length);
+ position += skipLength;
+ return skipLength;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int length) {
+ if (isEndOfStream()) {
+ return -1;
+ }
+ int readLength = getReadLength(length);
+ System.arraycopy(data, position, buffer, offset, readLength);
+ position += readLength;
+ return readLength;
+ }
+
+ @Override
+ public int read(ByteBuffer buffer, int length) {
+ if (isEndOfStream()) {
+ return -1;
+ }
+ int readLength = getReadLength(length);
+ buffer.put(data, position, readLength);
+ position += readLength;
+ return readLength;
+ }
+
+ @Override
+ public long getAvailableByteCount() {
+ return data.length - position;
+ }
+
+ @Override
+ public boolean isEndOfStream() {
+ return position == data.length;
+ }
+
+ @Override
+ public void close() {
+ // Do nothing.
+ }
+
+ private int getReadLength(int requestedLength) {
+ return Math.min(requestedLength, data.length - position);
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/DataSink.java b/library/src/main/java/com/google/android/exoplayer/upstream/DataSink.java
new file mode 100644
index 00000000000..7fb160fe1e2
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/upstream/DataSink.java
@@ -0,0 +1,52 @@
+/*
+ * 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.upstream;
+
+import java.io.IOException;
+
+/**
+ * A component that consumes media data.
+ */
+public interface DataSink {
+
+ /**
+ * Opens the {@link DataSink} to consume the specified data. Calls to {@link #open(DataSpec)} and
+ * {@link #close()} must be balanced.
+ *
+ * @param dataSpec Defines the data to be consumed.
+ * @return This {@link DataSink}, for convenience.
+ * @throws IOException
+ */
+ public DataSink open(DataSpec dataSpec) throws IOException;
+
+ /**
+ * Closes the {@link DataSink}.
+ *
+ * @throws IOException
+ */
+ public void close() throws IOException;
+
+ /**
+ * Consumes the provided data.
+ *
+ * @param buffer The buffer from which data should be consumed.
+ * @param offset The offset of the data to consume in {@code buffer}.
+ * @param length The length of the data to consume, in bytes.
+ * @throws IOException
+ */
+ public void write(byte[] buffer, int offset, int length) throws IOException;
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/DataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/DataSource.java
new file mode 100644
index 00000000000..596bf764c32
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/upstream/DataSource.java
@@ -0,0 +1,66 @@
+/*
+ * 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.upstream;
+
+import java.io.IOException;
+
+/**
+ * A component that provides media data.
+ */
+public interface DataSource {
+
+ /**
+ * Opens the {@link DataSource} to read the specified data. Calls to {@link #open(DataSpec)} and
+ * {@link #close()} must be balanced.
+ *
+ * Note: If {@link #open(DataSpec)} throws an {@link IOException}, callers must still call
+ * {@link #close()} to ensure that any partial effects of the {@link #open(DataSpec)} invocation
+ * are cleaned up. Implementations of this class can assume that callers will call
+ * {@link #close()} in this case.
+ *
+ * @param dataSpec Defines the data to be read.
+ * @throws IOException If an error occurs opening the source.
+ * @return The number of bytes that can be read from the opened source. For unbounded requests
+ * (i.e. requests where {@link DataSpec#length} equals {@link DataSpec#LENGTH_UNBOUNDED})
+ * this value is the resolved length of the request. For all other requests, the value
+ * returned will be equal to the request's {@link DataSpec#length}.
+ */
+ public long open(DataSpec dataSpec) throws IOException;
+
+ /**
+ * Closes the {@link DataSource}.
+ *
+ * Note: This method will be called even if the corresponding call to {@link #open(DataSpec)}
+ * threw an {@link IOException}. See {@link #open(DataSpec)} for more details.
+ *
+ * @throws IOException If an error occurs closing the source.
+ */
+ public void close() throws IOException;
+
+ /**
+ * Reads up to {@code length} bytes of data and stores them into {@code buffer}, starting at
+ * index {@code offset}. This method blocks until at least one byte of data can be read, the end
+ * of the opened range is detected, or an exception is thrown.
+ *
+ * @param buffer The buffer into which the read data should be stored.
+ * @param offset The start offset into {@code buffer} at which data should be written.
+ * @param readLength The maximum number of bytes to read.
+ * @return The actual number of bytes read, or -1 if the end of the opened range is reached.
+ * @throws IOException If an error occurs reading from the source.
+ */
+ public int read(byte[] buffer, int offset, int readLength) throws IOException;
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/DataSourceInputStream.java b/library/src/main/java/com/google/android/exoplayer/upstream/DataSourceInputStream.java
new file mode 100644
index 00000000000..8ed35c3ea89
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/upstream/DataSourceInputStream.java
@@ -0,0 +1,86 @@
+/*
+ * 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.upstream;
+
+import com.google.android.exoplayer.util.Assertions;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Allows data corresponding to a given {@link DataSpec} to be read from a {@link DataSource} and
+ * consumed as an {@link InputStream}.
+ */
+public class DataSourceInputStream extends InputStream {
+
+ private final DataSource dataSource;
+ private final DataSpec dataSpec;
+ private final byte[] singleByteArray;
+
+ private boolean opened = false;
+ private boolean closed = false;
+
+ /**
+ * @param dataSource The {@link DataSource} from which the data should be read.
+ * @param dataSpec The {@link DataSpec} defining the data to be read from {@code dataSource}.
+ */
+ public DataSourceInputStream(DataSource dataSource, DataSpec dataSpec) {
+ this.dataSource = dataSource;
+ this.dataSpec = dataSpec;
+ singleByteArray = new byte[1];
+ }
+
+ @Override
+ public int read() throws IOException {
+ read(singleByteArray);
+ return singleByteArray[0];
+ }
+
+ @Override
+ public int read(byte[] buffer) throws IOException {
+ return read(buffer, 0, buffer.length);
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int length) throws IOException {
+ Assertions.checkState(!closed);
+ checkOpened();
+ return dataSource.read(buffer, offset, length);
+ }
+
+ @Override
+ public long skip(long byteCount) throws IOException {
+ Assertions.checkState(!closed);
+ checkOpened();
+ return super.skip(byteCount);
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (!closed) {
+ dataSource.close();
+ closed = true;
+ }
+ }
+
+ private void checkOpened() throws IOException {
+ if (!opened) {
+ dataSource.open(dataSpec);
+ opened = true;
+ }
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/DataSourceStream.java b/library/src/main/java/com/google/android/exoplayer/upstream/DataSourceStream.java
new file mode 100644
index 00000000000..3dc52d20547
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/upstream/DataSourceStream.java
@@ -0,0 +1,301 @@
+/*
+ * 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.upstream;
+
+import com.google.android.exoplayer.upstream.Loader.Loadable;
+import com.google.android.exoplayer.util.Assertions;
+import com.google.android.exoplayer.util.Util;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * Loads data from a {@link DataSource} into an in-memory {@link Allocation}. The loaded data
+ * can be consumed by treating the instance as a non-blocking {@link NonBlockingInputStream}.
+ */
+public final class DataSourceStream implements Loadable, NonBlockingInputStream {
+
+ /**
+ * Thrown when an error is encountered trying to load data into a {@link DataSourceStream}.
+ */
+ public static class DataSourceStreamLoadException extends IOException {
+
+ public DataSourceStreamLoadException(IOException cause) {
+ super(cause);
+ }
+
+ }
+
+ private final DataSource dataSource;
+ private final DataSpec dataSpec;
+ private final Allocator allocator;
+ private final ReadHead readHead;
+
+ private Allocation allocation;
+
+ private volatile boolean loadCanceled;
+ private volatile long loadPosition;
+ private volatile long resolvedLength;
+
+ private int writeFragmentIndex;
+ private int writeFragmentOffset;
+ private int writeFragmentRemainingLength;
+
+ /**
+ * @param dataSource The source from which the data should be loaded.
+ * @param dataSpec Defines the data to be loaded. {@code dataSpec.length} must not exceed
+ * {@link Integer#MAX_VALUE}. If {@code dataSpec.length == DataSpec.LENGTH_UNBOUNDED} then
+ * the length resolved by {@code dataSource.open(dataSpec)} must not exceed
+ * {@link Integer#MAX_VALUE}.
+ * @param allocator Used to obtain an {@link Allocation} for holding the data.
+ */
+ public DataSourceStream(DataSource dataSource, DataSpec dataSpec, Allocator allocator) {
+ Assertions.checkState(dataSpec.length <= Integer.MAX_VALUE);
+ this.dataSource = dataSource;
+ this.dataSpec = dataSpec;
+ this.allocator = allocator;
+ resolvedLength = DataSpec.LENGTH_UNBOUNDED;
+ readHead = new ReadHead();
+ }
+
+ /**
+ * Resets the read position to the start of the data.
+ */
+ public void resetReadPosition() {
+ readHead.reset();
+ }
+
+ /**
+ * Returns the current read position for data being read out of the source.
+ *
+ * @return The current read position.
+ */
+ public long getReadPosition() {
+ return readHead.position;
+ }
+
+ /**
+ * Returns the number of bytes of data that have been loaded.
+ *
+ * @return The number of bytes of data that have been loaded.
+ */
+ public long getLoadPosition() {
+ return loadPosition;
+ }
+
+ /**
+ * Returns the length of the streamin bytes.
+ *
+ * @return The length of the stream in bytes, or {@value DataSpec#LENGTH_UNBOUNDED} if the length
+ * has yet to be determined.
+ */
+ public long getLength() {
+ return resolvedLength != DataSpec.LENGTH_UNBOUNDED ? resolvedLength : dataSpec.length;
+ }
+
+ /**
+ * Whether the stream has finished loading.
+ *
+ * @return True if the stream has finished loading. False otherwise.
+ */
+ public boolean isLoadFinished() {
+ return resolvedLength != DataSpec.LENGTH_UNBOUNDED && loadPosition == resolvedLength;
+ }
+
+ /**
+ * Returns a byte array containing the loaded data. If the data is partially loaded, this method
+ * returns the portion of the data that has been loaded so far. If nothing has been loaded, null
+ * is returned. This method does not use or update the current read position.
+ *
+ * Note: The read methods provide a more efficient way of consuming the loaded data. Use this
+ * method only when a freshly allocated byte[] containing all of the loaded data is required.
+ *
+ * @return The loaded data or null.
+ */
+ public final byte[] getLoadedData() {
+ if (loadPosition == 0) {
+ return null;
+ }
+
+ byte[] rawData = new byte[(int) loadPosition];
+ read(null, rawData, 0, new ReadHead(), rawData.length);
+ return rawData;
+ }
+
+ // {@link NonBlockingInputStream} implementation.
+
+ @Override
+ public long getAvailableByteCount() {
+ return loadPosition - readHead.position;
+ }
+
+ @Override
+ public boolean isEndOfStream() {
+ return resolvedLength != DataSpec.LENGTH_UNBOUNDED && readHead.position == resolvedLength;
+ }
+
+ @Override
+ public void close() {
+ if (allocation != null) {
+ allocation.release();
+ allocation = null;
+ }
+ }
+
+ @Override
+ public int skip(int skipLength) {
+ return read(null, null, 0, readHead, skipLength);
+ }
+
+ @Override
+ public int read(ByteBuffer target1, int readLength) {
+ return read(target1, null, 0, readHead, readLength);
+ }
+
+ @Override
+ public int read(byte[] target, int offset, int readLength) {
+ return read(null, target, offset, readHead, readLength);
+ }
+
+ /**
+ * Reads data to either a target {@link ByteBuffer}, or to a target byte array at a specified
+ * offset. The {@code readHead} is updated to reflect the read that was performed.
+ */
+ private int read(ByteBuffer target, byte[] targetArray, int targetArrayOffset,
+ ReadHead readHead, int readLength) {
+ if (readHead.position == dataSpec.length) {
+ return -1;
+ }
+ int bytesToRead = (int) Math.min(loadPosition - readHead.position, readLength);
+ if (bytesToRead == 0) {
+ return 0;
+ }
+ if (readHead.position == 0) {
+ readHead.fragmentIndex = 0;
+ readHead.fragmentOffset = allocation.getFragmentOffset(0);
+ readHead.fragmentRemaining = allocation.getFragmentLength(0);
+ }
+ int bytesRead = 0;
+ byte[][] buffers = allocation.getBuffers();
+ while (bytesRead < bytesToRead) {
+ int bufferReadLength = Math.min(readHead.fragmentRemaining, bytesToRead - bytesRead);
+ if (target != null) {
+ target.put(buffers[readHead.fragmentIndex], readHead.fragmentOffset, bufferReadLength);
+ } else if (targetArray != null) {
+ System.arraycopy(buffers[readHead.fragmentIndex], readHead.fragmentOffset, targetArray,
+ targetArrayOffset, bufferReadLength);
+ targetArrayOffset += bufferReadLength;
+ }
+ readHead.position += bufferReadLength;
+ bytesRead += bufferReadLength;
+ readHead.fragmentOffset += bufferReadLength;
+ readHead.fragmentRemaining -= bufferReadLength;
+ if (readHead.fragmentRemaining == 0 && readHead.position < resolvedLength) {
+ readHead.fragmentIndex++;
+ readHead.fragmentOffset = allocation.getFragmentOffset(readHead.fragmentIndex);
+ readHead.fragmentRemaining = allocation.getFragmentLength(readHead.fragmentIndex);
+ }
+ }
+
+ return bytesRead;
+ }
+
+ // {@link Loadable} implementation.
+
+ @Override
+ public void cancelLoad() {
+ loadCanceled = true;
+ }
+
+ @Override
+ public boolean isLoadCanceled() {
+ return loadCanceled;
+ }
+
+ @Override
+ public void load() throws IOException, InterruptedException {
+ if (loadCanceled || isLoadFinished()) {
+ // The load was canceled, or is already complete.
+ return;
+ }
+ try {
+ DataSpec loadDataSpec;
+ if (resolvedLength == DataSpec.LENGTH_UNBOUNDED) {
+ loadDataSpec = dataSpec;
+ resolvedLength = dataSource.open(loadDataSpec);
+ if (resolvedLength > Integer.MAX_VALUE) {
+ throw new DataSourceStreamLoadException(
+ new UnexpectedLengthException(dataSpec.length, resolvedLength));
+ }
+ } else {
+ loadDataSpec = new DataSpec(dataSpec.uri, dataSpec.position + loadPosition,
+ resolvedLength - loadPosition, dataSpec.key);
+ dataSource.open(loadDataSpec);
+ }
+ if (allocation == null) {
+ allocation = allocator.allocate((int) resolvedLength);
+ }
+ if (loadPosition == 0) {
+ writeFragmentIndex = 0;
+ writeFragmentOffset = allocation.getFragmentOffset(0);
+ writeFragmentRemainingLength = allocation.getFragmentLength(0);
+ }
+
+ int read = Integer.MAX_VALUE;
+ byte[][] buffers = allocation.getBuffers();
+ while (!loadCanceled && loadPosition < resolvedLength && read > 0) {
+ if (Thread.interrupted()) {
+ throw new InterruptedException();
+ }
+ int writeLength = (int) Math.min(writeFragmentRemainingLength,
+ resolvedLength - loadPosition);
+ read = dataSource.read(buffers[writeFragmentIndex], writeFragmentOffset, writeLength);
+ if (read > 0) {
+ loadPosition += read;
+ writeFragmentOffset += read;
+ writeFragmentRemainingLength -= read;
+ if (writeFragmentRemainingLength == 0 && loadPosition < resolvedLength) {
+ writeFragmentIndex++;
+ writeFragmentOffset = allocation.getFragmentOffset(writeFragmentIndex);
+ writeFragmentRemainingLength = allocation.getFragmentLength(writeFragmentIndex);
+ }
+ } else if (resolvedLength != loadPosition) {
+ throw new DataSourceStreamLoadException(
+ new UnexpectedLengthException(resolvedLength, loadPosition));
+ }
+ }
+ } finally {
+ Util.closeQuietly(dataSource);
+ }
+ }
+
+ private static class ReadHead {
+
+ private int position;
+ private int fragmentIndex;
+ private int fragmentOffset;
+ private int fragmentRemaining;
+
+ public void reset() {
+ position = 0;
+ fragmentIndex = 0;
+ fragmentOffset = 0;
+ fragmentRemaining = 0;
+ }
+
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/DataSpec.java b/library/src/main/java/com/google/android/exoplayer/upstream/DataSpec.java
new file mode 100644
index 00000000000..d9f5030d196
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/upstream/DataSpec.java
@@ -0,0 +1,117 @@
+/*
+ * 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.upstream;
+
+import com.google.android.exoplayer.util.Assertions;
+
+import android.net.Uri;
+
+/**
+ * Defines a region of media data.
+ */
+public final class DataSpec {
+
+ /**
+ * A permitted value of {@link #length}. A {@link DataSpec} defined with this length represents
+ * the region of media data that starts at its {@link #position} and extends to the end of the
+ * data whose location is defined by its {@link #uri}.
+ */
+ public static final int LENGTH_UNBOUNDED = -1;
+
+ /**
+ * Identifies the source from which data should be read.
+ */
+ public final Uri uri;
+ /**
+ * True if the data at {@link #uri} is the full stream. False otherwise. An example where this
+ * may be false is if {@link #uri} defines the location of a cached part of the stream.
+ */
+ public final boolean uriIsFullStream;
+ /**
+ * The absolute position of the data in the full stream.
+ */
+ public final long absoluteStreamPosition;
+ /**
+ * The position of the data when read from {@link #uri}. Always equal to
+ * {@link #absoluteStreamPosition} if {@link #uriIsFullStream}.
+ */
+ public final long position;
+ /**
+ * The length of the data. Greater than zero, or equal to {@link #LENGTH_UNBOUNDED}.
+ */
+ public final long length;
+ /**
+ * A key that uniquely identifies the original stream. Used for cache indexing. May be null if the
+ * {@link DataSpec} is not intended to be used in conjunction with a cache.
+ */
+ public final String key;
+
+ /**
+ * Construct a {@link DataSpec} for which {@link #uriIsFullStream} is true.
+ *
+ * @param uri {@link #uri}.
+ * @param absoluteStreamPosition {@link #absoluteStreamPosition}, equal to {@link #position}.
+ * @param length {@link #length}.
+ * @param key {@link #key}.
+ */
+ public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key) {
+ this(uri, absoluteStreamPosition, length, key, absoluteStreamPosition, true);
+ }
+
+ /**
+ * Construct a {@link DataSpec} for which {@link #uriIsFullStream} is false.
+ *
+ * @param uri {@link #uri}.
+ * @param absoluteStreamPosition {@link #absoluteStreamPosition}.
+ * @param length {@link #length}.
+ * @param key {@link #key}.
+ * @param position {@link #position}.
+ */
+ public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key, long position) {
+ this(uri, absoluteStreamPosition, length, key, position, false);
+ }
+
+ /**
+ * Construct a {@link DataSpec}.
+ *
+ * @param uri {@link #uri}.
+ * @param absoluteStreamPosition {@link #absoluteStreamPosition}.
+ * @param length {@link #length}.
+ * @param key {@link #key}.
+ * @param position {@link #position}.
+ * @param uriIsFullStream {@link #uriIsFullStream}.
+ */
+ public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key, long position,
+ boolean uriIsFullStream) {
+ Assertions.checkArgument(absoluteStreamPosition >= 0);
+ Assertions.checkArgument(position >= 0);
+ Assertions.checkArgument(length > 0 || length == LENGTH_UNBOUNDED);
+ Assertions.checkArgument(absoluteStreamPosition == position || !uriIsFullStream);
+ this.uri = uri;
+ this.uriIsFullStream = uriIsFullStream;
+ this.absoluteStreamPosition = absoluteStreamPosition;
+ this.position = position;
+ this.length = length;
+ this.key = key;
+ }
+
+ @Override
+ public String toString() {
+ return "DataSpec[" + uri + ", " + uriIsFullStream + ", " + absoluteStreamPosition + ", " +
+ position + ", " + length + ", " + key + "]";
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/DefaultBandwidthMeter.java b/library/src/main/java/com/google/android/exoplayer/upstream/DefaultBandwidthMeter.java
new file mode 100644
index 00000000000..9fa124a2721
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/upstream/DefaultBandwidthMeter.java
@@ -0,0 +1,146 @@
+/*
+ * 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.upstream;
+
+import com.google.android.exoplayer.util.Assertions;
+import com.google.android.exoplayer.util.Clock;
+import com.google.android.exoplayer.util.SlidingPercentile;
+import com.google.android.exoplayer.util.SystemClock;
+
+import android.os.Handler;
+
+/**
+ * Counts transferred bytes while transfers are open and creates a bandwidth sample and updated
+ * bandwidth estimate each time a transfer ends.
+ */
+public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener {
+
+ /**
+ * Interface definition for a callback to be notified of {@link DefaultBandwidthMeter} events.
+ */
+ public interface EventListener {
+
+ /**
+ * Invoked periodically to indicate that bytes have been transferred.
+ *
+ * @param elapsedMs The time taken to transfer the bytes, in milliseconds.
+ * @param bytes The number of bytes transferred.
+ * @param bandwidthEstimate The estimated bandwidth in bytes/sec, or {@link #NO_ESTIMATE} if no
+ * estimate is available. Note that this estimate is typically derived from more information
+ * than {@code bytes} and {@code elapsedMs}.
+ */
+ void onBandwidthSample(int elapsedMs, long bytes, long bandwidthEstimate);
+
+ }
+
+ private static final int DEFAULT_MAX_WEIGHT = 2000;
+
+ private final Handler eventHandler;
+ private final EventListener eventListener;
+ private final Clock clock;
+ private final SlidingPercentile slidingPercentile;
+
+ private long accumulator;
+ private long startTimeMs;
+ private long bandwidthEstimate;
+ private int streamCount;
+
+ public DefaultBandwidthMeter() {
+ this(null, null);
+ }
+
+ public DefaultBandwidthMeter(Handler eventHandler, EventListener eventListener) {
+ this(eventHandler, eventListener, new SystemClock());
+ }
+
+ public DefaultBandwidthMeter(Handler eventHandler, EventListener eventListener, Clock clock) {
+ this(eventHandler, eventListener, clock, DEFAULT_MAX_WEIGHT);
+ }
+
+ public DefaultBandwidthMeter(Handler eventHandler, EventListener eventListener, int maxWeight) {
+ this(eventHandler, eventListener, new SystemClock(), maxWeight);
+ }
+
+ public DefaultBandwidthMeter(Handler eventHandler, EventListener eventListener, Clock clock,
+ int maxWeight) {
+ this.eventHandler = eventHandler;
+ this.eventListener = eventListener;
+ this.clock = clock;
+ this.slidingPercentile = new SlidingPercentile(maxWeight);
+ bandwidthEstimate = NO_ESTIMATE;
+ }
+
+ /**
+ * Gets the estimated bandwidth.
+ *
+ * @return Estimated bandwidth in bytes/sec, or {@link #NO_ESTIMATE} if no estimate is available.
+ */
+ @Override
+ public synchronized long getEstimate() {
+ return bandwidthEstimate;
+ }
+
+ @Override
+ public synchronized void onTransferStart() {
+ if (streamCount == 0) {
+ startTimeMs = clock.elapsedRealtime();
+ }
+ streamCount++;
+ }
+
+ @Override
+ public synchronized void onBytesTransferred(int bytes) {
+ accumulator += bytes;
+ }
+
+ @Override
+ public synchronized void onTransferEnd() {
+ Assertions.checkState(streamCount > 0);
+ long nowMs = clock.elapsedRealtime();
+ int elapsedMs = (int) (nowMs - startTimeMs);
+ if (elapsedMs > 0) {
+ float bytesPerSecond = accumulator * 1000 / elapsedMs;
+ slidingPercentile.addSample(computeWeight(accumulator), bytesPerSecond);
+ float bandwidthEstimateFloat = slidingPercentile.getPercentile(0.5f);
+ bandwidthEstimate = bandwidthEstimateFloat == Float.NaN
+ ? NO_ESTIMATE : (long) bandwidthEstimateFloat;
+ notifyBandwidthSample(elapsedMs, accumulator, bandwidthEstimate);
+ }
+ streamCount--;
+ if (streamCount > 0) {
+ startTimeMs = nowMs;
+ }
+ accumulator = 0;
+ }
+
+ // TODO: Use media time (bytes / mediaRate) as weight.
+ private int computeWeight(long mediaBytes) {
+ return (int) Math.sqrt(mediaBytes);
+ }
+
+ private void notifyBandwidthSample(final int elapsedMs, final long bytes,
+ final long bandwidthEstimate) {
+ if (eventHandler != null && eventListener != null) {
+ eventHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ eventListener.onBandwidthSample(elapsedMs, bytes, bandwidthEstimate);
+ }
+ });
+ }
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/FileDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/FileDataSource.java
new file mode 100644
index 00000000000..0ca8d3ea80e
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/upstream/FileDataSource.java
@@ -0,0 +1,82 @@
+/*
+ * 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.upstream;
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+
+/**
+ * A local file {@link DataSource}.
+ */
+public final class FileDataSource implements DataSource {
+
+ /**
+ * Thrown when IOException is encountered during local file read operation.
+ */
+ public static class FileDataSourceException extends IOException {
+
+ public FileDataSourceException(IOException cause) {
+ super(cause);
+ }
+
+ }
+
+ private RandomAccessFile file;
+ private long bytesRemaining;
+
+ @Override
+ public long open(DataSpec dataSpec) throws FileDataSourceException {
+ try {
+ file = new RandomAccessFile(dataSpec.uri.getPath(), "r");
+ file.seek(dataSpec.position);
+ bytesRemaining = dataSpec.length == DataSpec.LENGTH_UNBOUNDED
+ ? file.length() - dataSpec.position
+ : dataSpec.length;
+ return bytesRemaining;
+ } catch (IOException e) {
+ throw new FileDataSourceException(e);
+ }
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws FileDataSourceException {
+ if (bytesRemaining == 0) {
+ return -1;
+ } else {
+ int bytesRead = 0;
+ try {
+ bytesRead = file.read(buffer, offset, (int) Math.min(bytesRemaining, readLength));
+ } catch (IOException e) {
+ throw new FileDataSourceException(e);
+ }
+ bytesRemaining -= bytesRead;
+ return bytesRead;
+ }
+ }
+
+ @Override
+ public void close() throws FileDataSourceException {
+ if (file != null) {
+ try {
+ file.close();
+ } catch (IOException e) {
+ throw new FileDataSourceException(e);
+ }
+ file = null;
+ }
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/HttpDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/HttpDataSource.java
new file mode 100644
index 00000000000..374a648efab
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/upstream/HttpDataSource.java
@@ -0,0 +1,445 @@
+/*
+ * 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.upstream;
+
+import com.google.android.exoplayer.util.Assertions;
+import com.google.android.exoplayer.util.Predicate;
+import com.google.android.exoplayer.util.Util;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * An http {@link DataSource}.
+ */
+public class HttpDataSource implements DataSource {
+
+ /**
+ * A {@link Predicate} that rejects content types often used for pay-walls.
+ */
+ public static final Predicate REJECT_PAYWALL_TYPES = new Predicate() {
+
+ @Override
+ public boolean evaluate(String contentType) {
+ contentType = Util.toLowerInvariant(contentType);
+ return !TextUtils.isEmpty(contentType)
+ && (!contentType.contains("text") || contentType.contains("text/vtt"))
+ && !contentType.contains("html") && !contentType.contains("xml");
+ }
+
+ };
+
+ /**
+ * Thrown when an error is encountered when trying to read from HTTP data source.
+ */
+ public static class HttpDataSourceException extends IOException {
+
+ /*
+ * The {@link DataSpec} associated with the current connection.
+ */
+ public final DataSpec dataSpec;
+
+ public HttpDataSourceException(DataSpec dataSpec) {
+ super();
+ this.dataSpec = dataSpec;
+ }
+
+ public HttpDataSourceException(String message, DataSpec dataSpec) {
+ super(message);
+ this.dataSpec = dataSpec;
+ }
+
+ public HttpDataSourceException(IOException cause, DataSpec dataSpec) {
+ super(cause);
+ this.dataSpec = dataSpec;
+ }
+
+ public HttpDataSourceException(String message, IOException cause, DataSpec dataSpec) {
+ super(message, cause);
+ this.dataSpec = dataSpec;
+ }
+
+ }
+
+ /**
+ * Thrown when the content type is invalid.
+ */
+ public static final class InvalidContentTypeException extends HttpDataSourceException {
+
+ public final String contentType;
+
+ public InvalidContentTypeException(String contentType, DataSpec dataSpec) {
+ super("Invalid content type: " + contentType, dataSpec);
+ this.contentType = contentType;
+ }
+
+ }
+
+ /**
+ * Thrown when an attempt to open a connection results in a response code not in the 2xx range.
+ */
+ public static final class InvalidResponseCodeException extends HttpDataSourceException {
+
+ /**
+ * The response code that was outside of the 2xx range.
+ */
+ public final int responseCode;
+
+ /**
+ * An unmodifiable map of the response header fields and values.
+ */
+ public final Map> headerFields;
+
+ public InvalidResponseCodeException(int responseCode, Map> headerFields,
+ DataSpec dataSpec) {
+ super("Response code: " + responseCode, dataSpec);
+ this.responseCode = responseCode;
+ this.headerFields = headerFields;
+ }
+
+ }
+
+ public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 8 * 1000;
+ public static final int DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000;
+
+ private static final String TAG = "HttpDataSource";
+ private static final Pattern CONTENT_RANGE_HEADER =
+ Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$");
+
+ private final int connectTimeoutMillis;
+ private final int readTimeoutMillis;
+ private final String userAgent;
+ private final Predicate contentTypePredicate;
+ private final HashMap requestProperties;
+ private final TransferListener listener;
+
+ private DataSpec dataSpec;
+ private HttpURLConnection connection;
+ private InputStream inputStream;
+ private boolean opened;
+
+ private long dataLength;
+ private long bytesRead;
+
+ /**
+ * @param userAgent The User-Agent string that should be used.
+ * @param contentTypePredicate An optional {@link Predicate}. If a content type is
+ * rejected by the predicate then a {@link InvalidContentTypeException} is thrown from
+ * {@link #open(DataSpec)}.
+ */
+ public HttpDataSource(String userAgent, Predicate contentTypePredicate) {
+ this(userAgent, contentTypePredicate, null);
+ }
+
+ /**
+ * @param userAgent The User-Agent string that should be used.
+ * @param contentTypePredicate An optional {@link Predicate}. If a content type is
+ * rejected by the predicate then a {@link InvalidContentTypeException} is thrown from
+ * {@link #open(DataSpec)}.
+ * @param listener An optional listener.
+ */
+ public HttpDataSource(String userAgent, Predicate contentTypePredicate,
+ TransferListener listener) {
+ this(userAgent, contentTypePredicate, listener, DEFAULT_CONNECT_TIMEOUT_MILLIS,
+ DEFAULT_READ_TIMEOUT_MILLIS);
+ }
+
+ /**
+ * @param userAgent The User-Agent string that should be used.
+ * @param contentTypePredicate An optional {@link Predicate}. If a content type is
+ * rejected by the predicate then a {@link InvalidContentTypeException} is thrown from
+ * {@link #open(DataSpec)}.
+ * @param listener An optional listener.
+ * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is
+ * interpreted as an infinite timeout.
+ * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted
+ * as an infinite timeout.
+ */
+ public HttpDataSource(String userAgent, Predicate contentTypePredicate,
+ TransferListener listener, int connectTimeoutMillis, int readTimeoutMillis) {
+ this.userAgent = Assertions.checkNotEmpty(userAgent);
+ this.contentTypePredicate = contentTypePredicate;
+ this.listener = listener;
+ this.requestProperties = new HashMap();
+ this.connectTimeoutMillis = connectTimeoutMillis;
+ this.readTimeoutMillis = readTimeoutMillis;
+ }
+
+ /**
+ * Sets the value of a request header field. The value will be used for subsequent connections
+ * established by the source.
+ *
+ * @param name The name of the header field.
+ * @param value The value of the field.
+ */
+ public void setRequestProperty(String name, String value) {
+ Assertions.checkNotNull(name);
+ Assertions.checkNotNull(value);
+ synchronized (requestProperties) {
+ requestProperties.put(name, value);
+ }
+ }
+
+ /**
+ * Clears the value of a request header field. The change will apply to subsequent connections
+ * established by the source.
+ *
+ * @param name The name of the header field.
+ */
+ public void clearRequestProperty(String name) {
+ Assertions.checkNotNull(name);
+ synchronized (requestProperties) {
+ requestProperties.remove(name);
+ }
+ }
+
+ /**
+ * Clears all request header fields that were set by {@link #setRequestProperty(String, String)}.
+ */
+ public void clearAllRequestProperties() {
+ synchronized (requestProperties) {
+ requestProperties.clear();
+ }
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws HttpDataSourceException {
+ this.dataSpec = dataSpec;
+ this.bytesRead = 0;
+ try {
+ connection = makeConnection(dataSpec);
+ } catch (IOException e) {
+ throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e,
+ dataSpec);
+ }
+
+ // Check for a valid response code.
+ int responseCode;
+ try {
+ responseCode = connection.getResponseCode();
+ } catch (IOException e) {
+ throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e,
+ dataSpec);
+ }
+ if (responseCode < 200 || responseCode > 299) {
+ Map> headers = connection.getHeaderFields();
+ closeConnection();
+ throw new InvalidResponseCodeException(responseCode, headers, dataSpec);
+ }
+
+ // Check for a valid content type.
+ String contentType = connection.getContentType();
+ if (contentTypePredicate != null && !contentTypePredicate.evaluate(contentType)) {
+ closeConnection();
+ throw new InvalidContentTypeException(contentType, dataSpec);
+ }
+
+ long contentLength = getContentLength(connection);
+ dataLength = dataSpec.length == DataSpec.LENGTH_UNBOUNDED ? contentLength : dataSpec.length;
+ if (dataLength == DataSpec.LENGTH_UNBOUNDED) {
+ // The DataSpec specified unbounded length and we failed to resolve a length from the
+ // response headers.
+ throw new HttpDataSourceException(
+ new UnexpectedLengthException(DataSpec.LENGTH_UNBOUNDED, DataSpec.LENGTH_UNBOUNDED),
+ dataSpec);
+ }
+
+ if (dataSpec.length != DataSpec.LENGTH_UNBOUNDED && contentLength != DataSpec.LENGTH_UNBOUNDED
+ && contentLength != dataSpec.length) {
+ // The DataSpec specified a length and we resolved a length from the response headers, but
+ // the two lengths do not match.
+ closeConnection();
+ throw new HttpDataSourceException(
+ new UnexpectedLengthException(dataSpec.length, contentLength), dataSpec);
+ }
+
+ try {
+ inputStream = connection.getInputStream();
+ } catch (IOException e) {
+ closeConnection();
+ throw new HttpDataSourceException(e, dataSpec);
+ }
+
+ opened = true;
+ if (listener != null) {
+ listener.onTransferStart();
+ }
+
+ return dataLength;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException {
+ int read = 0;
+ try {
+ read = inputStream.read(buffer, offset, readLength);
+ } catch (IOException e) {
+ throw new HttpDataSourceException(e, dataSpec);
+ }
+
+ if (read > 0) {
+ bytesRead += read;
+ if (listener != null) {
+ listener.onBytesTransferred(read);
+ }
+ } else if (dataLength != bytesRead) {
+ // Check for cases where the server closed the connection having not sent the correct amount
+ // of data.
+ throw new HttpDataSourceException(new UnexpectedLengthException(dataLength, bytesRead),
+ dataSpec);
+ }
+
+ return read;
+ }
+
+ @Override
+ public void close() throws HttpDataSourceException {
+ try {
+ if (inputStream != null) {
+ try {
+ inputStream.close();
+ } catch (IOException e) {
+ throw new HttpDataSourceException(e, dataSpec);
+ }
+ inputStream = null;
+ }
+ } finally {
+ if (opened) {
+ opened = false;
+ if (listener != null) {
+ listener.onTransferEnd();
+ }
+ closeConnection();
+ }
+ }
+ }
+
+ private void closeConnection() {
+ if (connection != null) {
+ connection.disconnect();
+ connection = null;
+ }
+ }
+
+ /**
+ * Returns the current connection, or null if the source is not currently opened.
+ *
+ * @return The current open connection, or null.
+ */
+ protected final HttpURLConnection getConnection() {
+ return connection;
+ }
+
+ /**
+ * Returns the number of bytes that have been read since the most recent call to
+ * {@link #open(DataSpec)}.
+ *
+ * @return The number of bytes read.
+ */
+ protected final long bytesRead() {
+ return bytesRead;
+ }
+
+ /**
+ * Returns the number of bytes that are still to be read for the current {@link DataSpec}. This
+ * value is equivalent to {@code dataSpec.length - bytesRead()}, where dataSpec is the
+ * {@link DataSpec} that was passed to the most recent call of {@link #open(DataSpec)}.
+ *
+ * @return The number of bytes remaining.
+ */
+ protected final long bytesRemaining() {
+ return dataLength - bytesRead;
+ }
+
+ private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException {
+ URL url = new URL(dataSpec.uri.toString());
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setConnectTimeout(connectTimeoutMillis);
+ connection.setReadTimeout(readTimeoutMillis);
+ connection.setDoOutput(false);
+ synchronized (requestProperties) {
+ for (HashMap.Entry property : requestProperties.entrySet()) {
+ connection.setRequestProperty(property.getKey(), property.getValue());
+ }
+ }
+ connection.setRequestProperty("Accept-Encoding", "deflate");
+ connection.setRequestProperty("User-Agent", userAgent);
+ connection.setRequestProperty("Range", buildRangeHeader(dataSpec));
+ connection.connect();
+ return connection;
+ }
+
+ private String buildRangeHeader(DataSpec dataSpec) {
+ String rangeRequest = "bytes=" + dataSpec.position + "-";
+ if (dataSpec.length != DataSpec.LENGTH_UNBOUNDED) {
+ rangeRequest += (dataSpec.position + dataSpec.length - 1);
+ }
+ return rangeRequest;
+ }
+
+ private long getContentLength(HttpURLConnection connection) {
+ long contentLength = DataSpec.LENGTH_UNBOUNDED;
+ String contentLengthHeader = connection.getHeaderField("Content-Length");
+ if (!TextUtils.isEmpty(contentLengthHeader)) {
+ try {
+ contentLength = Long.parseLong(contentLengthHeader);
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "Unexpected Content-Length [" + contentLengthHeader + "]");
+ }
+ }
+ String contentRangeHeader = connection.getHeaderField("Content-Range");
+ if (!TextUtils.isEmpty(contentRangeHeader)) {
+ Matcher matcher = CONTENT_RANGE_HEADER.matcher(contentRangeHeader);
+ if (matcher.find()) {
+ try {
+ long contentLengthFromRange =
+ Long.parseLong(matcher.group(2)) - Long.parseLong(matcher.group(1)) + 1;
+ if (contentLength < 0) {
+ // Some proxy servers strip the Content-Length header. Fall back to the length
+ // calculated here in this case.
+ contentLength = contentLengthFromRange;
+ } else if (contentLength != contentLengthFromRange) {
+ // If there is a discrepancy between the Content-Length and Content-Range headers,
+ // assume the one with the larger value is correct. We have seen cases where carrier
+ // change one of them to reduce the size of a request, but it is unlikely anybody would
+ // increase it.
+ Log.w(TAG, "Inconsistent headers [" + contentLengthHeader + "] [" + contentRangeHeader +
+ "]");
+ contentLength = Math.max(contentLength, contentLengthFromRange);
+ }
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "Unexpected Content-Range [" + contentRangeHeader + "]");
+ }
+ }
+ }
+ if (contentLength == DataSpec.LENGTH_UNBOUNDED) {
+ Log.w(TAG, "Unable to parse content length [" + contentLengthHeader + "] [" +
+ contentRangeHeader + "]");
+ }
+ return contentLength;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/Loader.java b/library/src/main/java/com/google/android/exoplayer/upstream/Loader.java
new file mode 100644
index 00000000000..fc232d328d2
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/upstream/Loader.java
@@ -0,0 +1,223 @@
+/*
+ * 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.upstream;
+
+import com.google.android.exoplayer.util.Assertions;
+import com.google.android.exoplayer.util.Util;
+
+import android.annotation.SuppressLint;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+import java.io.IOException;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * Manages the background loading of {@link Loadable}s.
+ */
+public final class Loader {
+
+ /**
+ * Thrown when an unexpected exception is encountered during loading.
+ */
+ public static final class UnexpectedLoaderException extends IOException {
+
+ public UnexpectedLoaderException(Exception cause) {
+ super("Unexpected " + cause.getClass().getSimpleName() + ": " + cause.getMessage(), cause);
+ }
+
+ }
+
+ /**
+ * Interface definition of an object that can be loaded using a {@link Loader}.
+ */
+ public interface Loadable {
+
+ /**
+ * Cancels the load.
+ */
+ void cancelLoad();
+
+ /**
+ * Whether the load has been canceled.
+ *
+ * @return True if the load has been canceled. False otherwise.
+ */
+ boolean isLoadCanceled();
+
+ /**
+ * Performs the load, returning on completion or cancelation.
+ *
+ * @throws IOException
+ * @throws InterruptedException
+ */
+ void load() throws IOException, InterruptedException;
+
+ }
+
+ /**
+ * Interface definition for a callback to be notified of {@link Loader} events.
+ */
+ public interface Listener {
+
+ /**
+ * Invoked when loading has been canceled.
+ */
+ void onCanceled();
+
+ /**
+ * Invoked when the data source has been fully loaded.
+ */
+ void onLoaded();
+
+ /**
+ * Invoked when the data source is stopped due to an error.
+ */
+ void onError(IOException exception);
+
+ }
+
+ private static final int MSG_END_OF_SOURCE = 0;
+ private static final int MSG_ERROR = 1;
+
+ private final ExecutorService downloadExecutorService;
+ private final Listener listener;
+
+ private LoadTask currentTask;
+ private boolean loading;
+
+ /**
+ * @param threadName A name for the loader's thread.
+ * @param listener A listener to invoke when state changes occur.
+ */
+ public Loader(String threadName, Listener listener) {
+ this.downloadExecutorService = Util.newSingleThreadExecutor(threadName);
+ this.listener = listener;
+ }
+
+ /**
+ * Start loading a {@link Loadable}.
+ *
+ * A {@link Loader} instance can only load one {@link Loadable} at a time, and so this method
+ * must not be called when another load is in progress.
+ *
+ * @param loadable The {@link Loadable} to load.
+ */
+ public void startLoading(Loadable loadable) {
+ Assertions.checkState(!loading);
+ loading = true;
+ currentTask = new LoadTask(loadable);
+ downloadExecutorService.submit(currentTask);
+ }
+
+ /**
+ * Whether the {@link Loader} is currently loading a {@link Loadable}.
+ *
+ * @return Whether the {@link Loader} is currently loading a {@link Loadable}.
+ */
+ public boolean isLoading() {
+ return loading;
+ }
+
+ /**
+ * Cancels the current load.
+ *
+ * This method should only be called when a load is in progress.
+ */
+ public void cancelLoading() {
+ Assertions.checkState(loading);
+ currentTask.quit();
+ }
+
+ /**
+ * Releases the {@link Loader}.
+ *
+ * This method should be called when the {@link Loader} is no longer required.
+ */
+ public void release() {
+ if (loading) {
+ cancelLoading();
+ }
+ downloadExecutorService.shutdown();
+ }
+
+ @SuppressLint("HandlerLeak")
+ private final class LoadTask extends Handler implements Runnable {
+
+ private static final String TAG = "LoadTask";
+
+ private final Loadable loadable;
+
+ private volatile Thread executorThread;
+
+ public LoadTask(Loadable loadable) {
+ this.loadable = loadable;
+ }
+
+ public void quit() {
+ loadable.cancelLoad();
+ if (executorThread != null) {
+ executorThread.interrupt();
+ }
+ }
+
+ @Override
+ public void run() {
+ try {
+ executorThread = Thread.currentThread();
+ if (!loadable.isLoadCanceled()) {
+ loadable.load();
+ }
+ sendEmptyMessage(MSG_END_OF_SOURCE);
+ } catch (IOException e) {
+ obtainMessage(MSG_ERROR, e).sendToTarget();
+ } catch (InterruptedException e) {
+ // The load was canceled.
+ Assertions.checkState(loadable.isLoadCanceled());
+ sendEmptyMessage(MSG_END_OF_SOURCE);
+ } catch (Exception e) {
+ // This should never happen, but handle it anyway.
+ Log.e(TAG, "Unexpected error loading stream", e);
+ obtainMessage(MSG_ERROR, new UnexpectedLoaderException(e)).sendToTarget();
+ }
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ onFinished();
+ if (loadable.isLoadCanceled()) {
+ listener.onCanceled();
+ return;
+ }
+ switch (msg.what) {
+ case MSG_END_OF_SOURCE:
+ listener.onLoaded();
+ break;
+ case MSG_ERROR:
+ listener.onError((IOException) msg.obj);
+ break;
+ }
+ }
+
+ private void onFinished() {
+ loading = false;
+ currentTask = null;
+ }
+
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/NetworkLock.java b/library/src/main/java/com/google/android/exoplayer/upstream/NetworkLock.java
new file mode 100644
index 00000000000..26df7db5c44
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/upstream/NetworkLock.java
@@ -0,0 +1,117 @@
+/*
+ * 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.upstream;
+
+import java.io.IOException;
+import java.util.PriorityQueue;
+
+/**
+ * A network task prioritization mechanism.
+ *
+ * Manages different priority network tasks. A network task that wishes to have its priority
+ * respected, and respect the priority of other tasks, should register itself with the lock prior
+ * to making network requests. It should then call one of the lock's proceed methods frequently
+ * during execution, so as to ensure that it continues only if it is the highest (or equally
+ * highest) priority task.
+ *
+ * Note that lower integer values correspond to higher priorities.
+ */
+public final class NetworkLock {
+
+ /**
+ * Thrown when a task is attempts to proceed when it does not have the highest priority.
+ */
+ public static class PriorityTooLowException extends IOException {
+
+ public PriorityTooLowException(int priority, int highestPriority) {
+ super("Priority too low [priority=" + priority + ", highest=" + highestPriority + "]");
+ }
+
+ }
+
+ public static final NetworkLock instance = new NetworkLock();
+
+ /**
+ * Priority for network tasks associated with media streaming.
+ */
+ public static final int STREAMING_PRIORITY = 0;
+ /**
+ * Priority for network tasks associated with background downloads.
+ */
+ public static final int DOWNLOAD_PRIORITY = 10;
+
+ private final PriorityQueue queue;
+
+ private NetworkLock() {
+ queue = new PriorityQueue();
+ }
+
+ /**
+ * Blocks until the passed priority is the lowest one (i.e. highest priority).
+ *
+ * @param priority The priority of the task that would like to proceed.
+ */
+ public synchronized void proceed(int priority) throws InterruptedException {
+ while (queue.peek() < priority) {
+ wait();
+ }
+ }
+
+ /**
+ * A non-blocking variant of {@link #proceed(int)}.
+ *
+ * @param priority The priority of the task that would like to proceed.
+ * @return Whether the passed priority is allowed to proceed.
+ */
+ public synchronized boolean proceedNonBlocking(int priority) {
+ return queue.peek() >= priority;
+ }
+
+ /**
+ * A throwing variant of {@link #proceed(int)}.
+ *
+ * @param priority The priority of the task that would like to proceed.
+ * @throws PriorityTooLowException If the passed priority is not high enough to proceed.
+ */
+ public synchronized void proceedOrThrow(int priority) throws PriorityTooLowException {
+ int highestPriority = queue.peek();
+ if (highestPriority < priority) {
+ throw new PriorityTooLowException(priority, highestPriority);
+ }
+ }
+
+ /**
+ * Register a new task.
+ *
+ * The task must call {@link #remove(int)} when done.
+ *
+ * @param priority The priority of the task.
+ */
+ public synchronized void add(int priority) {
+ queue.add(priority);
+ }
+
+ /**
+ * Unregister a task.
+ *
+ * @param priority The priority of the task.
+ */
+ public synchronized void remove(int priority) {
+ queue.remove(priority);
+ notifyAll();
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/NonBlockingInputStream.java b/library/src/main/java/com/google/android/exoplayer/upstream/NonBlockingInputStream.java
new file mode 100644
index 00000000000..f9e1dffe2d1
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/upstream/NonBlockingInputStream.java
@@ -0,0 +1,79 @@
+/*
+ * 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.upstream;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Represents a source of bytes that can be consumed by downstream components.
+ *
+ * The read and skip methods are non-blocking, and hence return 0 (indicating that no data has
+ * been read) in the case that data is not yet available to be consumed.
+ */
+public interface NonBlockingInputStream {
+
+ /**
+ * Skips over and discards up to {@code length} bytes of data. This method may skip over some
+ * smaller number of bytes, possibly 0.
+ *
+ * @param length The maximum number of bytes to skip.
+ * @return The actual number of bytes skipped, or -1 if the end of the data is reached.
+ */
+ int skip(int length);
+
+ /**
+ * Reads up to {@code length} bytes of data and stores them into {@code buffer}, starting at
+ * index {@code offset}. This method may read fewer bytes, possibly 0.
+ *
+ * @param buffer The buffer into which the read data should be stored.
+ * @param offset The start offset into {@code buffer} at which data should be written.
+ * @param length The maximum number of bytes to read.
+ * @return The actual number of bytes read, or -1 if the end of the data is reached.
+ */
+ int read(byte[] buffer, int offset, int length);
+
+ /**
+ * Reads up to {@code length} bytes of data and stores them into {@code buffer}. This method may
+ * read fewer bytes, possibly 0.
+ *
+ * @param buffer The buffer into which the read data should be stored.
+ * @param length The maximum number of bytes to read.
+ * @return The actual number of bytes read, or -1 if the end of the data is reached.
+ */
+ int read(ByteBuffer buffer, int length);
+
+ /**
+ * Returns the number of bytes currently available for reading or skipping. Calls to the read()
+ * and skip() methods are guaranteed to be satisfied in full if they request less than or
+ * equal to the value returned.
+ *
+ * @return The number of bytes currently available.
+ */
+ long getAvailableByteCount();
+
+ /**
+ * Whether the end of the data has been reached.
+ *
+ * @return True if the end of the data has been reached, false otherwise.
+ */
+ boolean isEndOfStream();
+
+ /**
+ * Closes the input stream.
+ */
+ void close();
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/PriorityDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/PriorityDataSource.java
new file mode 100644
index 00000000000..34ed14794c1
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/upstream/PriorityDataSource.java
@@ -0,0 +1,58 @@
+/*
+ * 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.upstream;
+
+import com.google.android.exoplayer.util.Assertions;
+
+import java.io.IOException;
+
+/**
+ * Allows {@link #open(DataSpec)} and {@link #read(byte[], int, int)} calls only if the specified
+ * priority is the highest priority of any task. {@link NetworkLock.PriorityTooLowException} is
+ * thrown when this condition does not hold.
+ */
+public class PriorityDataSource implements DataSource {
+
+ private final DataSource upstream;
+ private final int priority;
+
+ /**
+ * @param priority The priority of the source.
+ * @param upstream The upstream {@link DataSource}.
+ */
+ public PriorityDataSource(int priority, DataSource upstream) {
+ this.priority = priority;
+ this.upstream = Assertions.checkNotNull(upstream);
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ NetworkLock.instance.proceedOrThrow(priority);
+ return upstream.open(dataSpec);
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int max) throws IOException {
+ NetworkLock.instance.proceedOrThrow(priority);
+ return upstream.read(buffer, offset, max);
+ }
+
+ @Override
+ public void close() throws IOException {
+ upstream.close();
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/TeeDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/TeeDataSource.java
new file mode 100644
index 00000000000..b54db198436
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/upstream/TeeDataSource.java
@@ -0,0 +1,67 @@
+/*
+ * 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.upstream;
+
+import com.google.android.exoplayer.util.Assertions;
+
+import java.io.IOException;
+
+/**
+ * Tees data into a {@link DataSink} as the data is read.
+ */
+public final class TeeDataSource implements DataSource {
+
+ private final DataSource upstream;
+ private final DataSink dataSink;
+
+ /**
+ * @param upstream The upstream {@link DataSource}.
+ * @param dataSink The {@link DataSink} into which data is written.
+ */
+ public TeeDataSource(DataSource upstream, DataSink dataSink) {
+ this.upstream = Assertions.checkNotNull(upstream);
+ this.dataSink = Assertions.checkNotNull(dataSink);
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ long dataLength = upstream.open(dataSpec);
+ if (dataSpec.length == DataSpec.LENGTH_UNBOUNDED) {
+ // Reconstruct dataSpec in order to provide the resolved length to the sink.
+ dataSpec = new DataSpec(dataSpec.uri, dataSpec.absoluteStreamPosition, dataLength,
+ dataSpec.key, dataSpec.position, dataSpec.uriIsFullStream);
+ }
+ dataSink.open(dataSpec);
+ return dataLength;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int max) throws IOException {
+ int num = upstream.read(buffer, offset, max);
+ if (num > 0) {
+ // TODO: Consider continuing even if disk writes fail.
+ dataSink.write(buffer, offset, num);
+ }
+ return num;
+ }
+
+ @Override
+ public void close() throws IOException {
+ upstream.close();
+ dataSink.close();
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/TransferListener.java b/library/src/main/java/com/google/android/exoplayer/upstream/TransferListener.java
new file mode 100644
index 00000000000..f2b6d1f8d4d
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/upstream/TransferListener.java
@@ -0,0 +1,41 @@
+/*
+ * 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.upstream;
+
+/**
+ * Interface definition for a callback to be notified of data transfer events.
+ */
+public interface TransferListener {
+
+ /**
+ * Invoked when a transfer starts.
+ */
+ void onTransferStart();
+
+ /**
+ * Called incrementally during a transfer.
+ *
+ * @param bytesTransferred The number of bytes transferred since the previous call to this
+ * method (or if the first call, since the transfer was started).
+ */
+ void onBytesTransferred(int bytesTransferred);
+
+ /**
+ * Invoked when a transfer ends.
+ */
+ void onTransferEnd();
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/UnexpectedLengthException.java b/library/src/main/java/com/google/android/exoplayer/upstream/UnexpectedLengthException.java
new file mode 100644
index 00000000000..c7bc6c303d6
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/upstream/UnexpectedLengthException.java
@@ -0,0 +1,45 @@
+/*
+ * 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.upstream;
+
+import java.io.IOException;
+
+/**
+ * Thrown when the length of some data does not match an expected length.
+ */
+public final class UnexpectedLengthException extends IOException {
+
+ /**
+ * The length that was expected, in bytes.
+ */
+ public final long expectedLength;
+
+ /**
+ * The actual length encountered, in bytes.
+ */
+ public final long actualLength;
+
+ /**
+ * @param expectedLength The length that was expected, in bytes.
+ * @param actualLength The actual length encountered, in bytes.
+ */
+ public UnexpectedLengthException(long expectedLength, long actualLength) {
+ super("Expected: " + expectedLength + ", got: " + actualLength);
+ this.expectedLength = expectedLength;
+ this.actualLength = actualLength;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/cache/Cache.java b/library/src/main/java/com/google/android/exoplayer/upstream/cache/Cache.java
new file mode 100644
index 00000000000..1c482e9fd39
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/upstream/cache/Cache.java
@@ -0,0 +1,176 @@
+/*
+ * 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.upstream.cache;
+
+import java.io.File;
+import java.util.NavigableSet;
+import java.util.Set;
+
+/**
+ * An interface for cache.
+ */
+public interface Cache {
+
+ /**
+ * Interface definition for a callback to be notified of {@link Cache} events.
+ */
+ public interface Listener {
+
+ /**
+ * Invoked when a {@link CacheSpan} is added to the cache.
+ *
+ * @param cache The source of the event.
+ * @param span The added {@link CacheSpan}.
+ */
+ void onSpanAdded(Cache cache, CacheSpan span);
+
+ /**
+ * Invoked when a {@link CacheSpan} is removed from the cache.
+ *
+ * @param cache The source of the event.
+ * @param span The removed {@link CacheSpan}.
+ */
+ void onSpanRemoved(Cache cache, CacheSpan span);
+
+ /**
+ * Invoked when an existing {@link CacheSpan} is accessed, causing it to be replaced. The new
+ * {@link CacheSpan} is guaranteed to represent the same data as the one it replaces, however
+ * {@link CacheSpan#file} and {@link CacheSpan#lastAccessTimestamp} may have changed.
+ *
+ * Note that for span replacement, {@link #onSpanAdded(Cache, CacheSpan)} and
+ * {@link #onSpanRemoved(Cache, CacheSpan)} are not invoked in addition to this method.
+ *
+ * @param cache The source of the event.
+ * @param oldSpan The old {@link CacheSpan}, which has been removed from the cache.
+ * @param newSpan The new {@link CacheSpan}, which has been added to the cache.
+ */
+ void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan);
+
+ }
+
+ /**
+ * Registers a listener to listen for changes to a given key.
+ *
+ * No guarantees are made about the thread or threads on which the listener is invoked, but it
+ * is guaranteed that listener methods will be invoked in a serial fashion (i.e. one at a time)
+ * and in the same order as events occurred.
+ *
+ * @param key The key to listen to.
+ * @param listener The listener to add.
+ * @return The current spans for the key.
+ */
+ NavigableSet addListener(String key, Listener listener);
+
+ /**
+ * Unregisters a listener.
+ *
+ * @param key The key to stop listening to.
+ * @param listener The listener to remove.
+ */
+ void removeListener(String key, Listener listener);
+
+ /**
+ * Returns the cached spans for a given cache key.
+ *
+ * @param key The key for which spans should be returned.
+ * @return The spans for the key. May be null if there are no such spans.
+ */
+ NavigableSet getCachedSpans(String key);
+
+ /**
+ * Returns all keys in the cache.
+ *
+ * @return All the keys in the cache.
+ */
+ Set getKeys();
+
+ /**
+ * Returns the total disk space in bytes used by the cache.
+ *
+ * @return The total disk space in bytes.
+ */
+ long getCacheSpace();
+
+ /**
+ * A caller should invoke this method when they require data from a given position for a given
+ * key.
+ *
+ * If there is a cache entry that overlaps the position, then the returned {@link CacheSpan}
+ * defines the file in which the data is stored. {@link CacheSpan#isCached} is true. The caller
+ * may read from the cache file, but does not acquire any locks.
+ *
+ * If there is no cache entry overlapping {@code offset}, then the returned {@link CacheSpan}
+ * defines a hole in the cache starting at {@code position} into which the caller may write as it
+ * obtains the data from some other source. The returned {@link CacheSpan} serves as a lock.
+ * Whilst the caller holds the lock it may write data into the hole. It may split data into
+ * multiple files. When the caller has finished writing a file it should commit it to the cache
+ * by calling {@link #commitFile(File)}. When the caller has finished writing, it must release
+ * the lock by calling {@link #releaseHoleSpan}.
+ *
+ * @param key The key of the data being requested.
+ * @param position The position of the data being requested.
+ * @return The {@link CacheSpan}.
+ * @throws InterruptedException
+ */
+ CacheSpan startReadWrite(String key, long position) throws InterruptedException;
+
+ /**
+ * Same as {@link #startReadWrite(String, long)}. However, if the cache entry is locked, then
+ * instead of blocking, this method will return null as the {@link CacheSpan}.
+ *
+ * @param key The key of the data being requested.
+ * @param position The position of the data being requested.
+ * @return The {@link CacheSpan}. Or null if the cache entry is locked.
+ * @throws InterruptedException
+ */
+ CacheSpan startReadWriteNonBlocking(String key, long position) throws InterruptedException;
+
+ /**
+ * Obtains a cache file into which data can be written. Must only be called when holding a
+ * corresponding hole {@link CacheSpan} obtained from {@link #startReadWrite(String, long)}.
+ *
+ * @param key The cache key for the data.
+ * @param position The starting position of the data.
+ * @param length The length of the data to be written. Used only to ensure that there is enough
+ * space in the cache.
+ * @return The file into which data should be written.
+ */
+ File startFile(String key, long position, long length);
+
+ /**
+ * Commits a file into the cache. Must only be called when holding a corresponding hole
+ * {@link CacheSpan} obtained from {@link #startReadWrite(String, long)}
+ *
+ * @param file A newly written cache file.
+ */
+ void commitFile(File file);
+
+ /**
+ * Releases a {@link CacheSpan} obtained from {@link #startReadWrite(String, long)} which
+ * corresponded to a hole in the cache.
+ *
+ * @param holeSpan The {@link CacheSpan} being released.
+ */
+ void releaseHoleSpan(CacheSpan holeSpan);
+
+ /**
+ * Removes a cached {@link CacheSpan} from the cache, deleting the underlying file.
+ *
+ * @param span The {@link CacheSpan} to remove.
+ */
+ void removeSpan(CacheSpan span);
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSink.java b/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSink.java
new file mode 100644
index 00000000000..02177f7d930
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSink.java
@@ -0,0 +1,123 @@
+/*
+ * 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.upstream.cache;
+
+import com.google.android.exoplayer.upstream.DataSink;
+import com.google.android.exoplayer.upstream.DataSpec;
+import com.google.android.exoplayer.util.Assertions;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/**
+ * Writes data into a cache.
+ */
+public class CacheDataSink implements DataSink {
+
+ private final Cache cache;
+ private final long maxCacheFileSize;
+
+ private DataSpec dataSpec;
+ private File file;
+ private FileOutputStream outputStream;
+ private long outputStreamBytesWritten;
+ private long dataSpecBytesWritten;
+
+ /**
+ * Thrown when IOException is encountered when writing data into sink.
+ */
+ public static class CacheDataSinkException extends IOException {
+
+ public CacheDataSinkException(IOException cause) {
+ super(cause);
+ }
+
+ }
+
+
+ /**
+ * @param cache The cache into which data should be written.
+ * @param maxCacheFileSize The maximum size of a cache file, in bytes. If the sink is opened for
+ * a {@link DataSpec} whose size exceeds this value, then the data will be fragmented into
+ * multiple cache files.
+ */
+ public CacheDataSink(Cache cache, long maxCacheFileSize) {
+ this.cache = Assertions.checkNotNull(cache);
+ this.maxCacheFileSize = maxCacheFileSize;
+ }
+
+ @Override
+ public DataSink open(DataSpec dataSpec) throws CacheDataSinkException {
+ try {
+ this.dataSpec = dataSpec;
+ dataSpecBytesWritten = 0;
+ openNextOutputStream();
+ return this;
+ } catch (FileNotFoundException e) {
+ throw new CacheDataSinkException(e);
+ }
+ }
+
+ @Override
+ public void write(byte[] buffer, int offset, int length) throws CacheDataSinkException {
+ try {
+ int bytesWritten = 0;
+ while (bytesWritten < length) {
+ if (outputStreamBytesWritten == maxCacheFileSize) {
+ closeCurrentOutputStream();
+ openNextOutputStream();
+ }
+ int bytesToWrite = (int) Math.min(length - bytesWritten,
+ maxCacheFileSize - outputStreamBytesWritten);
+ outputStream.write(buffer, offset + bytesWritten, bytesToWrite);
+ bytesWritten += bytesToWrite;
+ outputStreamBytesWritten += bytesToWrite;
+ dataSpecBytesWritten += bytesToWrite;
+ }
+ } catch (IOException e) {
+ throw new CacheDataSinkException(e);
+ }
+ }
+
+ @Override
+ public void close() throws CacheDataSinkException {
+ try {
+ closeCurrentOutputStream();
+ } catch (IOException e) {
+ throw new CacheDataSinkException(e);
+ }
+ }
+
+ private void openNextOutputStream() throws FileNotFoundException {
+ file = cache.startFile(dataSpec.key, dataSpec.absoluteStreamPosition + dataSpecBytesWritten,
+ Math.min(dataSpec.length - dataSpecBytesWritten, maxCacheFileSize));
+ outputStream = new FileOutputStream(file);
+ outputStreamBytesWritten = 0;
+ }
+
+ private void closeCurrentOutputStream() throws IOException {
+ if (outputStream != null) {
+ outputStream.flush();
+ outputStream.close();
+ outputStream = null;
+ cache.commitFile(file);
+ file = null;
+ }
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSource.java
new file mode 100644
index 00000000000..1a0ebf3e194
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSource.java
@@ -0,0 +1,218 @@
+/*
+ * 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.upstream.cache;
+
+import com.google.android.exoplayer.upstream.DataSink;
+import com.google.android.exoplayer.upstream.DataSource;
+import com.google.android.exoplayer.upstream.DataSpec;
+import com.google.android.exoplayer.upstream.FileDataSource;
+import com.google.android.exoplayer.upstream.TeeDataSource;
+import com.google.android.exoplayer.upstream.cache.CacheDataSink.CacheDataSinkException;
+import com.google.android.exoplayer.util.Assertions;
+
+import android.net.Uri;
+
+import java.io.IOException;
+
+/**
+ * A {@link DataSource} that reads and writes a {@link Cache}. Requests are fulfilled from the cache
+ * when possible. When data is not cached it is requested from an upstream {@link DataSource} and
+ * written into the cache.
+ */
+public final class CacheDataSource implements DataSource {
+
+ private final Cache cache;
+ private final DataSource cacheReadDataSource;
+ private final DataSource cacheWriteDataSource;
+ private final DataSource upstreamDataSource;
+
+ private final boolean blockOnCache;
+ private final boolean ignoreCacheOnError;
+
+ private DataSource currentDataSource;
+ private Uri uri;
+ private String key;
+ private long readPosition;
+ private long bytesRemaining;
+ private CacheSpan lockedSpan;
+ private boolean ignoreCache;
+
+ /**
+ * Constructs an instance with default {@link DataSource} and {@link DataSink} instances for
+ * reading and writing the cache.
+ */
+ public CacheDataSource(Cache cache, DataSource upstream, boolean blockOnCache,
+ boolean ignoreCacheOnError) {
+ this(cache, upstream, blockOnCache, ignoreCacheOnError, Long.MAX_VALUE);
+ }
+
+ /**
+ * Constructs an instance with default {@link DataSource} and {@link DataSink} instances for
+ * reading and writing the cache. The sink is configured to fragment data such that no single
+ * cache file is greater than maxCacheFileSize bytes.
+ */
+ public CacheDataSource(Cache cache, DataSource upstream, boolean blockOnCache,
+ boolean ignoreCacheOnError, long maxCacheFileSize) {
+ this(cache, upstream, new FileDataSource(), new CacheDataSink(cache, maxCacheFileSize),
+ blockOnCache, ignoreCacheOnError);
+ }
+
+ /**
+ * Constructs an instance with arbitrary {@link DataSource} and {@link DataSink} instances for
+ * reading and writing the cache. One use of this constructor is to allow data to be transformed
+ * before it is written to disk.
+ *
+ * @param cache The cache.
+ * @param upstream A {@link DataSource} for reading data not in the cache.
+ * @param cacheReadDataSource A {@link DataSource} for reading data from the cache.
+ * @param cacheWriteDataSink A {@link DataSink} for writing data to the cache.
+ * @param blockOnCache A flag indicating whether we will block reads if the cache key is locked.
+ * If this flag is false, then we will read from upstream if the cache key is locked.
+ * @param ignoreCacheOnError Whether the cache is bypassed following any cache related error. If
+ * true, then cache related exceptions may be thrown for one cycle of open, read and close
+ * calls. Subsequent cycles of these calls will then bypass the cache.
+ */
+ public CacheDataSource(Cache cache, DataSource upstream, DataSource cacheReadDataSource,
+ DataSink cacheWriteDataSink, boolean blockOnCache, boolean ignoreCacheOnError) {
+ this.cache = cache;
+ this.cacheReadDataSource = cacheReadDataSource;
+ this.blockOnCache = blockOnCache;
+ this.ignoreCacheOnError = ignoreCacheOnError;
+ this.upstreamDataSource = upstream;
+ if (cacheWriteDataSink != null) {
+ this.cacheWriteDataSource = new TeeDataSource(upstream, cacheWriteDataSink);
+ } else {
+ this.cacheWriteDataSource = null;
+ }
+ }
+
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ Assertions.checkState(dataSpec.uriIsFullStream);
+ // TODO: Support caching for unbounded requests. This requires storing the source length
+ // into the cache (the simplest approach is to incorporate it into each cache file's name).
+ Assertions.checkState(dataSpec.length != DataSpec.LENGTH_UNBOUNDED);
+ try {
+ uri = dataSpec.uri;
+ key = dataSpec.key;
+ readPosition = dataSpec.position;
+ bytesRemaining = dataSpec.length;
+ openNextSource();
+ return dataSpec.length;
+ } catch (IOException e) {
+ handleBeforeThrow(e);
+ throw e;
+ }
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int max) throws IOException {
+ try {
+ int num = currentDataSource.read(buffer, offset, max);
+ if (num >= 0) {
+ readPosition += num;
+ bytesRemaining -= num;
+ } else {
+ closeCurrentSource();
+ if (bytesRemaining > 0) {
+ openNextSource();
+ return read(buffer, offset, max);
+ }
+ }
+ return num;
+ } catch (IOException e) {
+ handleBeforeThrow(e);
+ throw e;
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ try {
+ closeCurrentSource();
+ } catch (IOException e) {
+ handleBeforeThrow(e);
+ throw e;
+ }
+ }
+
+ /**
+ * Opens the next source. If the cache contains data spanning the current read position then
+ * {@link #cacheReadDataSource} is opened to read from it. Else {@link #upstreamDataSource} is
+ * opened to read from the upstream source and write into the cache.
+ */
+ private void openNextSource() throws IOException {
+ try {
+ DataSpec dataSpec;
+ CacheSpan span;
+ if (ignoreCache) {
+ span = null;
+ } else if (blockOnCache) {
+ span = cache.startReadWrite(key, readPosition);
+ } else {
+ span = cache.startReadWriteNonBlocking(key, readPosition);
+ }
+ if (span == null) {
+ // The data is locked in the cache, or we're ignoring the cache. Bypass the cache and read
+ // from upstream.
+ currentDataSource = upstreamDataSource;
+ dataSpec = new DataSpec(uri, readPosition, bytesRemaining, key);
+ } else if (span.isCached) {
+ // Data is cached, read from cache.
+ Uri fileUri = Uri.fromFile(span.file);
+ long filePosition = readPosition - span.position;
+ long length = Math.min(span.length - filePosition, bytesRemaining);
+ dataSpec = new DataSpec(fileUri, readPosition, length, key, filePosition);
+ currentDataSource = cacheReadDataSource;
+ } else {
+ // Data is not cached, and data is not locked, read from upstream with cache backing.
+ lockedSpan = span;
+ long length = span.isOpenEnded() ? bytesRemaining : Math.min(span.length, bytesRemaining);
+ dataSpec = new DataSpec(uri, readPosition, length, key);
+ currentDataSource = cacheWriteDataSource != null ? cacheWriteDataSource
+ : upstreamDataSource;
+ }
+ currentDataSource.open(dataSpec);
+ } catch (InterruptedException e) {
+ // Should never happen.
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void closeCurrentSource() throws IOException {
+ if (currentDataSource == null) {
+ return;
+ }
+ try {
+ currentDataSource.close();
+ currentDataSource = null;
+ } finally {
+ if (lockedSpan != null) {
+ cache.releaseHoleSpan(lockedSpan);
+ lockedSpan = null;
+ }
+ }
+ }
+
+ private void handleBeforeThrow(IOException exception) {
+ if (ignoreCacheOnError && (currentDataSource == cacheReadDataSource
+ || exception instanceof CacheDataSinkException)) {
+ // Ignore the cache from now on.
+ ignoreCache = true;
+ }
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheEvictor.java b/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheEvictor.java
new file mode 100644
index 00000000000..5b4231ad710
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheEvictor.java
@@ -0,0 +1,34 @@
+/*
+ * 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.upstream.cache;
+
+/**
+ * Evicts data from a {@link Cache}. Implementations should call {@link Cache#removeSpan(CacheSpan)}
+ * to evict cache entries based on their eviction policies.
+ */
+public interface CacheEvictor extends Cache.Listener {
+
+ /**
+ * Invoked when a writer starts writing to the cache.
+ *
+ * @param cache The source of the event.
+ * @param key The key being written.
+ * @param position The starting position of the data being written.
+ * @param length The maximum length of the data being written.
+ */
+ void onStartFile(Cache cache, String key, long position, long length);
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheSpan.java b/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheSpan.java
new file mode 100644
index 00000000000..4d2ce48afbb
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheSpan.java
@@ -0,0 +1,133 @@
+/*
+ * 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.upstream.cache;
+
+
+import java.io.File;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Defines a span of data that may or may not be cached (as indicated by {@link #isCached}).
+ */
+public final class CacheSpan implements Comparable {
+
+ private static final String SUFFIX = ".v1.exo";
+ private static final String SUFFIX_ESCAPED = "\\.v1\\.exo";
+ private static final Pattern cacheFilePattern =
+ Pattern.compile("^(.+)\\.(\\d+)\\.(\\d+)(" + SUFFIX_ESCAPED + ")$");
+
+ /**
+ * The cache key that uniquely identifies the original stream.
+ */
+ public final String key;
+ /**
+ * The position of the {@link CacheSpan} in the original stream.
+ */
+ public final long position;
+ /**
+ * The length of the {@link CacheSpan}, or -1 if this is an open-ended hole.
+ */
+ public final long length;
+ /**
+ * Whether the {@link CacheSpan} is cached.
+ */
+ public final boolean isCached;
+ /**
+ * The file corresponding to this {@link CacheSpan}, or null if {@link #isCached} is false.
+ */
+ public final File file;
+ /**
+ * The last access timestamp, or -1 if {@link #isCached} is false.
+ */
+ public final long lastAccessTimestamp;
+
+ public static File getCacheFileName(File cacheDir, String key, long offset,
+ long lastAccessTimestamp) {
+ return new File(cacheDir, key + "." + offset + "." + lastAccessTimestamp + SUFFIX);
+ }
+
+ public static CacheSpan createLookup(String key, long position) {
+ return new CacheSpan(key, position, -1, false, -1, null);
+ }
+
+ public static CacheSpan createOpenHole(String key, long position) {
+ return new CacheSpan(key, position, -1, false, -1, null);
+ }
+
+ public static CacheSpan createClosedHole(String key, long position, long length) {
+ return new CacheSpan(key, position, length, false, -1, null);
+ }
+
+ /**
+ * Creates a cache span from an underlying cache file.
+ *
+ * @param file The cache file.
+ * @return The span, or null if the file name is not correctly formatted.
+ */
+ public static CacheSpan createCacheEntry(File file) {
+ Matcher matcher = cacheFilePattern.matcher(file.getName());
+ if (!matcher.matches()) {
+ return null;
+ }
+ return CacheSpan.createCacheEntry(matcher.group(1), Long.parseLong(matcher.group(2)),
+ Long.parseLong(matcher.group(3)), file);
+ }
+
+ private static CacheSpan createCacheEntry(String key, long position, long lastAccessTimestamp,
+ File file) {
+ return new CacheSpan(key, position, file.length(), true, lastAccessTimestamp, file);
+ }
+
+ private CacheSpan(String key, long position, long length, boolean isCached,
+ long lastAccessTimestamp, File file) {
+ this.key = key;
+ this.position = position;
+ this.length = length;
+ this.isCached = isCached;
+ this.file = file;
+ this.lastAccessTimestamp = lastAccessTimestamp;
+ }
+
+ /**
+ * @return True if this is an open-ended {@link CacheSpan}. False otherwise.
+ */
+ public boolean isOpenEnded() {
+ return length == -1;
+ }
+
+ /**
+ * Renames the file underlying this cache span to update its last access time.
+ *
+ * @return A {@link CacheSpan} representing the updated cache file.
+ */
+ public CacheSpan touch() {
+ long now = System.currentTimeMillis();
+ File newCacheFile = getCacheFileName(file.getParentFile(), key, position, now);
+ file.renameTo(newCacheFile);
+ return CacheSpan.createCacheEntry(key, position, now, newCacheFile);
+ }
+
+ @Override
+ public int compareTo(CacheSpan another) {
+ if (!key.equals(another.key)) {
+ return key.compareTo(another.key);
+ }
+ long startOffsetDiff = position - another.position;
+ return startOffsetDiff == 0 ? 0 : ((startOffsetDiff < 0) ? -1 : 1);
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/cache/LeastRecentlyUsedCacheEvictor.java b/library/src/main/java/com/google/android/exoplayer/upstream/cache/LeastRecentlyUsedCacheEvictor.java
new file mode 100644
index 00000000000..0c34154a597
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/upstream/cache/LeastRecentlyUsedCacheEvictor.java
@@ -0,0 +1,76 @@
+/*
+ * 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.upstream.cache;
+
+import java.util.Comparator;
+import java.util.TreeSet;
+
+/**
+ * Evicts least recently used cache files first.
+ */
+public class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Comparator {
+
+ private final long maxBytes;
+ private final TreeSet leastRecentlyUsed;
+
+ private long currentSize;
+
+ public LeastRecentlyUsedCacheEvictor(long maxBytes) {
+ this.maxBytes = maxBytes;
+ this.leastRecentlyUsed = new TreeSet(this);
+ }
+
+ @Override
+ public void onStartFile(Cache cache, String key, long position, long length) {
+ evictCache(cache, length);
+ }
+
+ @Override
+ public void onSpanAdded(Cache cache, CacheSpan span) {
+ leastRecentlyUsed.add(span);
+ currentSize += span.length;
+ evictCache(cache, 0);
+ }
+
+ @Override
+ public void onSpanRemoved(Cache cache, CacheSpan span) {
+ leastRecentlyUsed.remove(span);
+ currentSize -= span.length;
+ }
+
+ @Override
+ public void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan) {
+ onSpanRemoved(cache, oldSpan);
+ onSpanAdded(cache, newSpan);
+ }
+
+ @Override
+ public int compare(CacheSpan lhs, CacheSpan rhs) {
+ long lastAccessTimestampDelta = lhs.lastAccessTimestamp - rhs.lastAccessTimestamp;
+ if (lastAccessTimestampDelta == 0) {
+ // Use the standard compareTo method as a tie-break.
+ return lhs.compareTo(rhs);
+ }
+ return lhs.lastAccessTimestamp < rhs.lastAccessTimestamp ? -1 : 1;
+ }
+
+ private void evictCache(Cache cache, long requiredSpace) {
+ while (currentSize + requiredSpace > maxBytes) {
+ cache.removeSpan(leastRecentlyUsed.first());
+ }
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/cache/NoOpCacheEvictor.java b/library/src/main/java/com/google/android/exoplayer/upstream/cache/NoOpCacheEvictor.java
new file mode 100644
index 00000000000..ea050c28dd2
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/upstream/cache/NoOpCacheEvictor.java
@@ -0,0 +1,47 @@
+/*
+ * 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.upstream.cache;
+
+
+/**
+ * Evictor that doesn't ever evict cache files.
+ *
+ * Warning: Using this evictor might have unforeseeable consequences if cache
+ * size is not managed elsewhere.
+ */
+public class NoOpCacheEvictor implements CacheEvictor {
+
+ @Override
+ public void onStartFile(Cache cache, String key, long position, long length) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onSpanAdded(Cache cache, CacheSpan span) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onSpanRemoved(Cache cache, CacheSpan span) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan) {
+ // Do nothing.
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/cache/SimpleCache.java b/library/src/main/java/com/google/android/exoplayer/upstream/cache/SimpleCache.java
new file mode 100644
index 00000000000..4a44407c3da
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/upstream/cache/SimpleCache.java
@@ -0,0 +1,333 @@
+/*
+ * 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.upstream.cache;
+
+import com.google.android.exoplayer.util.Assertions;
+
+import android.os.ConditionVariable;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map.Entry;
+import java.util.NavigableSet;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * A {@link Cache} implementation that maintains an in-memory representation.
+ */
+public class SimpleCache implements Cache {
+
+ private final File cacheDir;
+ private final CacheEvictor evictor;
+ private final HashMap lockedSpans;
+ private final HashMap> cachedSpans;
+ private final HashMap> listeners;
+ private long totalSpace = 0;
+
+ /**
+ * Constructs the cache. The cache will delete any unrecognized files from the directory. Hence
+ * the directory cannot be used to store other files.
+ *
+ * @param cacheDir A dedicated cache directory.
+ */
+ public SimpleCache(File cacheDir, CacheEvictor evictor) {
+ this.cacheDir = cacheDir;
+ this.evictor = evictor;
+ this.lockedSpans = new HashMap();
+ this.cachedSpans = new HashMap>();
+ this.listeners = new HashMap>();
+ // Start cache initialization.
+ final ConditionVariable conditionVariable = new ConditionVariable();
+ new Thread() {
+ @Override
+ public void run() {
+ synchronized (SimpleCache.this) {
+ conditionVariable.open();
+ initialize();
+ }
+ }
+ }.start();
+ conditionVariable.block();
+ }
+
+ @Override
+ public synchronized NavigableSet addListener(String key, Listener listener) {
+ ArrayList listenersForKey = listeners.get(key);
+ if (listenersForKey == null) {
+ listenersForKey = new ArrayList();
+ listeners.put(key, listenersForKey);
+ }
+ listenersForKey.add(listener);
+ return getCachedSpans(key);
+ }
+
+ @Override
+ public synchronized void removeListener(String key, Listener listener) {
+ ArrayList listenersForKey = listeners.get(key);
+ if (listenersForKey != null) {
+ listenersForKey.remove(listener);
+ if (listenersForKey.isEmpty()) {
+ listeners.remove(key);
+ }
+ }
+ }
+
+ @Override
+ public synchronized NavigableSet getCachedSpans(String key) {
+ TreeSet spansForKey = cachedSpans.get(key);
+ return spansForKey == null ? null : new TreeSet(spansForKey);
+ }
+
+ @Override
+ public synchronized Set getKeys() {
+ return new HashSet(cachedSpans.keySet());
+ }
+
+ @Override
+ public synchronized long getCacheSpace() {
+ return totalSpace;
+ }
+
+ @Override
+ public synchronized CacheSpan startReadWrite(String key, long position)
+ throws InterruptedException {
+ CacheSpan lookupSpan = CacheSpan.createLookup(key, position);
+ // Wait until no-one holds a lock for the key.
+ while (lockedSpans.containsKey(key)) {
+ wait();
+ }
+ return getSpanningRegion(key, lookupSpan);
+ }
+
+ @Override
+ public synchronized CacheSpan startReadWriteNonBlocking(String key, long position)
+ throws InterruptedException {
+ CacheSpan lookupSpan = CacheSpan.createLookup(key, position);
+ // Return null if key is locked
+ if (lockedSpans.containsKey(key)) {
+ return null;
+ }
+ return getSpanningRegion(key, lookupSpan);
+ }
+
+ private CacheSpan getSpanningRegion(String key, CacheSpan lookupSpan) {
+ CacheSpan spanningRegion = getSpan(lookupSpan);
+ if (spanningRegion.isCached) {
+ CacheSpan oldCacheSpan = spanningRegion;
+ // Remove the old span from the in-memory representation.
+ TreeSet spansForKey = cachedSpans.get(oldCacheSpan.key);
+ Assertions.checkState(spansForKey.remove(oldCacheSpan));
+ // Obtain a new span with updated last access timestamp.
+ spanningRegion = oldCacheSpan.touch();
+ // Add the updated span back into the in-memory representation.
+ spansForKey.add(spanningRegion);
+ notifySpanTouched(oldCacheSpan, spanningRegion);
+ } else {
+ lockedSpans.put(key, spanningRegion);
+ }
+ return spanningRegion;
+ }
+
+ @Override
+ public synchronized File startFile(String key, long position, long length) {
+ Assertions.checkState(lockedSpans.containsKey(key));
+ if (!cacheDir.exists()) {
+ // For some reason the cache directory doesn't exist. Make a best effort to create it.
+ removeStaleSpans();
+ cacheDir.mkdirs();
+ }
+ evictor.onStartFile(this, key, position, length);
+ return CacheSpan.getCacheFileName(cacheDir, key, position, System.currentTimeMillis());
+ }
+
+ @Override
+ public synchronized void commitFile(File file) {
+ CacheSpan span = CacheSpan.createCacheEntry(file);
+ Assertions.checkState(span != null);
+ Assertions.checkState(lockedSpans.containsKey(span.key));
+ // If the file doesn't exist, don't add it to the in-memory representation.
+ if (!file.exists()) {
+ return;
+ }
+ // If the file has length 0, delete it and don't add it to the in-memory representation.
+ long length = file.length();
+ if (length == 0) {
+ file.delete();
+ return;
+ }
+ addSpan(span);
+ }
+
+ @Override
+ public synchronized void releaseHoleSpan(CacheSpan holeSpan) {
+ Assertions.checkState(holeSpan == lockedSpans.remove(holeSpan.key));
+ notifyAll();
+ }
+
+ /**
+ * Returns the cache {@link CacheSpan} corresponding to the provided lookup {@link CacheSpan}.
+ *
+ * If the lookup position is contained by an existing entry in the cache, then the returned
+ * {@link CacheSpan} defines the file in which the data is stored. If the lookup position is not
+ * contained by an existing entry, then the returned {@link CacheSpan} defines the maximum extents
+ * of the hole in the cache.
+ *
+ * @param lookupSpan A lookup {@link CacheSpan} specifying a key and position.
+ * @return The corresponding cache {@link CacheSpan}.
+ */
+ private CacheSpan getSpan(CacheSpan lookupSpan) {
+ String key = lookupSpan.key;
+ long offset = lookupSpan.position;
+ TreeSet entries = cachedSpans.get(key);
+ if (entries == null) {
+ return CacheSpan.createOpenHole(key, lookupSpan.position);
+ }
+ CacheSpan floorSpan = entries.floor(lookupSpan);
+ if (floorSpan != null &&
+ floorSpan.position <= offset && offset < floorSpan.position + floorSpan.length) {
+ // The lookup position is contained within floorSpan.
+ if (floorSpan.file.exists()) {
+ return floorSpan;
+ } else {
+ // The file has been deleted from under us. It's likely that other files will have been
+ // deleted too, so scan the whole in-memory representation.
+ removeStaleSpans();
+ return getSpan(lookupSpan);
+ }
+ }
+ CacheSpan ceilEntry = entries.ceiling(lookupSpan);
+ return ceilEntry == null ? CacheSpan.createOpenHole(key, lookupSpan.position) :
+ CacheSpan.createClosedHole(key, lookupSpan.position,
+ ceilEntry.position - lookupSpan.position);
+ }
+
+ /**
+ * Ensures that the cache's in-memory representation has been initialized.
+ */
+ private void initialize() {
+ if (!cacheDir.exists()) {
+ cacheDir.mkdirs();
+ }
+ File[] files = cacheDir.listFiles();
+ if (files == null) {
+ return;
+ }
+ for (int i = 0; i < files.length; i++) {
+ File file = files[i];
+ if (file.length() == 0) {
+ file.delete();
+ } else {
+ CacheSpan span = CacheSpan.createCacheEntry(file);
+ if (span == null) {
+ file.delete();
+ } else {
+ addSpan(span);
+ }
+ }
+ }
+ }
+
+ /**
+ * Adds a cached span to the in-memory representation.
+ *
+ * @param span The span to be added.
+ */
+ private void addSpan(CacheSpan span) {
+ TreeSet spansForKey = cachedSpans.get(span.key);
+ if (spansForKey == null) {
+ spansForKey = new TreeSet();
+ cachedSpans.put(span.key, spansForKey);
+ }
+ spansForKey.add(span);
+ totalSpace += span.length;
+ notifySpanAdded(span);
+ }
+
+ @Override
+ public synchronized void removeSpan(CacheSpan span) {
+ TreeSet spansForKey = cachedSpans.get(span.key);
+ totalSpace -= span.length;
+ Assertions.checkState(spansForKey.remove(span));
+ span.file.delete();
+ if (spansForKey.isEmpty()) {
+ cachedSpans.remove(span.key);
+ }
+ notifySpanRemoved(span);
+ }
+
+ /**
+ * Scans all of the cached spans in the in-memory representation, removing any for which files
+ * no longer exist.
+ */
+ private void removeStaleSpans() {
+ Iterator>> iterator = cachedSpans.entrySet().iterator();
+ while (iterator.hasNext()) {
+ Entry> next = iterator.next();
+ Iterator spanIterator = next.getValue().iterator();
+ boolean isEmpty = true;
+ while (spanIterator.hasNext()) {
+ CacheSpan span = spanIterator.next();
+ if (!span.file.exists()) {
+ spanIterator.remove();
+ if (span.isCached) {
+ totalSpace -= span.length;
+ }
+ notifySpanRemoved(span);
+ } else {
+ isEmpty = false;
+ }
+ }
+ if (isEmpty) {
+ iterator.remove();
+ }
+ }
+ }
+
+ private void notifySpanRemoved(CacheSpan span) {
+ ArrayList keyListeners = listeners.get(span.key);
+ if (keyListeners != null) {
+ for (int i = keyListeners.size() - 1; i >= 0; i--) {
+ keyListeners.get(i).onSpanRemoved(this, span);
+ }
+ }
+ evictor.onSpanRemoved(this, span);
+ }
+
+ private void notifySpanAdded(CacheSpan span) {
+ ArrayList keyListeners = listeners.get(span.key);
+ if (keyListeners != null) {
+ for (int i = keyListeners.size() - 1; i >= 0; i--) {
+ keyListeners.get(i).onSpanAdded(this, span);
+ }
+ }
+ evictor.onSpanAdded(this, span);
+ }
+
+ private void notifySpanTouched(CacheSpan oldSpan, CacheSpan newSpan) {
+ ArrayList keyListeners = listeners.get(oldSpan.key);
+ if (keyListeners != null) {
+ for (int i = keyListeners.size() - 1; i >= 0; i--) {
+ keyListeners.get(i).onSpanTouched(this, oldSpan, newSpan);
+ }
+ }
+ evictor.onSpanTouched(this, oldSpan, newSpan);
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/util/Assertions.java b/library/src/main/java/com/google/android/exoplayer/util/Assertions.java
new file mode 100644
index 00000000000..90a7162c128
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/util/Assertions.java
@@ -0,0 +1,155 @@
+/*
+ * 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.util;
+
+import com.google.android.exoplayer.ExoPlayerLibraryInfo;
+
+import android.os.Looper;
+import android.text.TextUtils;
+
+/**
+ * Provides methods for asserting the truth of expressions and properties.
+ */
+public final class Assertions {
+
+ private Assertions() {}
+
+ /**
+ * Ensures the truth of an expression involving one or more arguments passed to the calling
+ * method.
+ *
+ * @param expression A boolean expression.
+ * @throws IllegalArgumentException If {@code expression} is false.
+ */
+ public static void checkArgument(boolean expression) {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && !expression) {
+ throw new IllegalArgumentException();
+ }
+ }
+
+ /**
+ * Ensures the truth of an expression involving one or more arguments passed to the calling
+ * method.
+ *
+ * @param expression A boolean expression.
+ * @param errorMessage The exception message to use if the check fails. The message is converted
+ * to a {@link String} using {@link String#valueOf(Object)}.
+ * @throws IllegalArgumentException If {@code expression} is false.
+ */
+ public static void checkArgument(boolean expression, Object errorMessage) {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && !expression) {
+ throw new IllegalArgumentException(String.valueOf(errorMessage));
+ }
+ }
+
+ /**
+ * Ensures the truth of an expression involving the state of the calling instance.
+ *
+ * @param expression A boolean expression.
+ * @throws IllegalStateException If {@code expression} is false.
+ */
+ public static void checkState(boolean expression) {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && !expression) {
+ throw new IllegalStateException();
+ }
+ }
+
+ /**
+ * Ensures the truth of an expression involving the state of the calling instance.
+ *
+ * @param expression A boolean expression.
+ * @param errorMessage The exception message to use if the check fails. The message is converted
+ * to a string using {@link String#valueOf(Object)}.
+ * @throws IllegalStateException If {@code expression} is false.
+ */
+ public static void checkState(boolean expression, Object errorMessage) {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && !expression) {
+ throw new IllegalStateException(String.valueOf(errorMessage));
+ }
+ }
+
+ /**
+ * Ensures that an object reference is not null.
+ *
+ * @param reference An object reference.
+ * @return The non-null reference that was validated.
+ * @throws NullPointerException If {@code reference} is null.
+ */
+ public static T checkNotNull(T reference) {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && reference == null) {
+ throw new NullPointerException();
+ }
+ return reference;
+ }
+
+ /**
+ * Ensures that an object reference is not null.
+ *
+ * @param reference An object reference.
+ * @param errorMessage The exception message to use if the check fails. The message is converted
+ * to a string using {@link String#valueOf(Object)}.
+ * @return The non-null reference that was validated.
+ * @throws NullPointerException If {@code reference} is null.
+ */
+ public static T checkNotNull(T reference, Object errorMessage) {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && reference == null) {
+ throw new NullPointerException(String.valueOf(errorMessage));
+ }
+ return reference;
+ }
+
+ /**
+ * Ensures that a string passed as an argument to the calling method is not null or 0-length.
+ *
+ * @param string A string.
+ * @return The non-null, non-empty string that was validated.
+ * @throws IllegalArgumentException If {@code string} is null or 0-length.
+ */
+ public static String checkNotEmpty(String string) {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && TextUtils.isEmpty(string)) {
+ throw new IllegalArgumentException();
+ }
+ return string;
+ }
+
+ /**
+ * Ensures that a string passed as an argument to the calling method is not null or 0-length.
+ *
+ * @param string A string.
+ * @param errorMessage The exception message to use if the check fails. The message is converted
+ * to a string using {@link String#valueOf(Object)}.
+ * @return The non-null, non-empty string that was validated.
+ * @throws IllegalArgumentException If {@code string} is null or 0-length.
+ */
+ public static String checkNotEmpty(String string, Object errorMessage) {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && TextUtils.isEmpty(string)) {
+ throw new IllegalArgumentException(String.valueOf(errorMessage));
+ }
+ return string;
+ }
+
+ /**
+ * Ensures that the calling thread is the application's main thread.
+ *
+ * @throws IllegalStateException If the calling thread is not the application's main thread.
+ */
+ public static void checkMainThread() {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && Looper.myLooper() != Looper.getMainLooper()) {
+ throw new IllegalStateException("Not in applications main thread");
+ }
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/util/Clock.java b/library/src/main/java/com/google/android/exoplayer/util/Clock.java
new file mode 100644
index 00000000000..af56e45ca31
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/util/Clock.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer.util;
+
+/**
+ * An interface through which system clocks can be read. The {@link SystemClock} implementation
+ * must be used for all non-test cases.
+ */
+public interface Clock {
+
+ /**
+ * Returns {@link android.os.SystemClock#elapsedRealtime}.
+ *
+ * @return Elapsed milliseconds since boot.
+ */
+ long elapsedRealtime();
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/util/LongArray.java b/library/src/main/java/com/google/android/exoplayer/util/LongArray.java
new file mode 100644
index 00000000000..a88b2f3e6d4
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/util/LongArray.java
@@ -0,0 +1,77 @@
+/*
+ * 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.util;
+
+import java.util.Arrays;
+
+/**
+ * An append-only, auto-growing {@code long[]}.
+ */
+public class LongArray {
+
+ private static final int DEFAULT_INITIAL_CAPACITY = 32;
+
+ private int size;
+ private long[] values;
+
+ public LongArray() {
+ this(DEFAULT_INITIAL_CAPACITY);
+ }
+
+ /**
+ * @param initialCapacity The initial capacity of the array.
+ */
+ public LongArray(int initialCapacity) {
+ values = new long[initialCapacity];
+ }
+
+ /**
+ * Appends a value.
+ *
+ * @param value The value to append.
+ */
+ public void add(long value) {
+ if (size == values.length) {
+ values = Arrays.copyOf(values, size * 2);
+ }
+ values[size++] = value;
+ }
+
+ /**
+ * Gets a value.
+ *
+ * @param index The index.
+ * @return The corresponding value.
+ * @throws IndexOutOfBoundsException If the index is less than zero, or greater than or equal to
+ * {@link #size()}
+ */
+ public long get(int index) {
+ if (index < 0 || index >= size) {
+ throw new IndexOutOfBoundsException("Invalid size " + index + ", size is " + size);
+ }
+ return values[index];
+ }
+
+ /**
+ * Gets the current size of the array.
+ *
+ * @return The current size of the array.
+ */
+ public int size() {
+ return size;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java b/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java
new file mode 100644
index 00000000000..bcf19890afa
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java
@@ -0,0 +1,137 @@
+/*
+ * 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.util;
+
+import com.google.android.exoplayer.ParserException;
+
+import android.os.AsyncTask;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+/**
+ * An {@link AsyncTask} for loading and parsing media manifests.
+ *
+ * @param The type of the manifest being parsed.
+ */
+public abstract class ManifestFetcher extends AsyncTask