/* * 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.mobilyzer.util.video.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 com.google.android.exoplayer.chunk.FormatEvaluator.BufferBasedAdaptiveEvaluator; import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator; 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, BufferBasedAdaptiveEvaluator.EventListener, AdaptiveEvaluator.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. * <p> * 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(String formatId, int trigger, int mediaTimeMs); void onAudioFormatEnabled(String formatId, int trigger, int mediaTimeMs); void onDroppedFrames(int count, long elapsed); // void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate); void onBandwidthSample(String label, long startTime, long endTime, int elapsedMs, long bytes, long bitrateEstimate); void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization, int mediaStartTimeMs, int mediaEndTimeMs, long length); void onLoadCompleted(int sourceId, long bytesLoaded); void onSwitchToSteadyState(long elapsedMs); void onAllChunksDownloaded(long totalBytes); } /** * 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<Listener> 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<Listener>(); 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); // silent the player player.sendMessage(renderers[TYPE_AUDIO], MediaCodecAudioTrackRenderer.MSG_SET_VOLUME, (float) 0.0); } /* 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 bitrateEstimate) { public void onBandwidthSample(String label, long startTime, long endTime, int elapsedMs, long bytes, long bitrateEstimate) { if (infoListener != null) { infoListener.onBandwidthSample(label, startTime, endTime, elapsedMs, bytes, bitrateEstimate); } } @Override public void onDownstreamFormatChanged(int sourceId, String 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, String formatId, int trigger, boolean isInitialization, int mediaStartTimeMs, int mediaEndTimeMs, long length) { if (infoListener != null) { infoListener.onLoadStarted(sourceId, formatId, trigger, isInitialization, mediaStartTimeMs, mediaEndTimeMs, length); } } @Override public void onLoadCompleted(int sourceId, long bytesLoaded) { if (infoListener != null) { infoListener.onLoadCompleted(sourceId, bytesLoaded); } } @Override public void onLoadCanceled(int sourceId, long bytesLoaded) { // Do nothing. } @Override public void onUpstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs, long bytesDiscarded) { // Do nothing. } @Override public void onDownstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs, long bytesDiscarded) { // 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); } } } @Override public void onSwitchToSteadyState(long elapsedMs) { if (infoListener != null) { infoListener.onSwitchToSteadyState(elapsedMs); } } @Override public void onAllChunksDownloaded(long totalBytes) { if (infoListener != null) { infoListener.onAllChunksDownloaded(totalBytes); } } }