/* * Copyright (C) 2016 - 2018 ExoMedia Contributors * * 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.devbrackets.android.exomedia.core.video.mp; import android.content.Context; import android.media.AudioManager; import android.media.MediaPlayer; import android.media.PlaybackParams; import android.net.Uri; import android.os.Build; import android.support.annotation.FloatRange; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; import android.view.Surface; import com.devbrackets.android.exomedia.core.ListenerMux; import com.devbrackets.android.exomedia.core.exoplayer.WindowInfo; import com.devbrackets.android.exomedia.core.video.ClearableSurface; import java.io.IOException; import java.util.Map; import static android.content.ContentValues.TAG; /** * A delegated object used to handle the majority of the * functionality for the "Native" video view implementation * to simplify support for both the {@link android.view.TextureView} * and {@link android.view.SurfaceView} implementations */ @SuppressWarnings("WeakerAccess") public class NativeVideoDelegate { public interface Callback { void videoSizeChanged(int width, int height); } public enum State { ERROR, IDLE, PREPARING, PREPARED, PLAYING, PAUSED, COMPLETED } protected Map<String, String> headers; protected State currentState = State.IDLE; protected Context context; protected Callback callback; protected ClearableSurface clearableSurface; protected MediaPlayer mediaPlayer; protected boolean playRequested = false; protected long requestedSeek; protected int currentBufferPercent; @FloatRange(from = 0.0, to = 1.0) protected float requestedVolume = 1.0f; protected ListenerMux listenerMux; @NonNull protected InternalListeners internalListeners = new InternalListeners(); @Nullable protected MediaPlayer.OnCompletionListener onCompletionListener; @Nullable protected MediaPlayer.OnPreparedListener onPreparedListener; @Nullable protected MediaPlayer.OnBufferingUpdateListener onBufferingUpdateListener; @Nullable protected MediaPlayer.OnSeekCompleteListener onSeekCompleteListener; @Nullable protected MediaPlayer.OnErrorListener onErrorListener; @Nullable protected MediaPlayer.OnInfoListener onInfoListener; public NativeVideoDelegate(@NonNull Context context, @NonNull Callback callback, @NonNull ClearableSurface clearableSurface) { this.context = context; this.callback = callback; this.clearableSurface = clearableSurface; initMediaPlayer(); currentState = State.IDLE; } public void start() { if (isReady()) { mediaPlayer.start(); currentState = State.PLAYING; } playRequested = true; listenerMux.setNotifiedCompleted(false); } public void pause() { if (isReady() && mediaPlayer.isPlaying()) { mediaPlayer.pause(); currentState = State.PAUSED; } playRequested = false; } public long getDuration() { if (!listenerMux.isPrepared() || !isReady()) { return 0; } return mediaPlayer.getDuration(); } public long getCurrentPosition() { if (!listenerMux.isPrepared() || !isReady()) { return 0; } return mediaPlayer.getCurrentPosition(); } @FloatRange(from = 0.0, to = 1.0) public float getVolume() { return requestedVolume; } public boolean setVolume(@FloatRange(from = 0.0, to = 1.0) float volume) { requestedVolume = volume; mediaPlayer.setVolume(volume, volume); return true; } public void seekTo(long milliseconds) { if (isReady()) { mediaPlayer.seekTo((int) milliseconds); requestedSeek = 0; } else { requestedSeek = milliseconds; } } public boolean isPlaying() { return isReady() && mediaPlayer.isPlaying(); } public int getBufferPercentage() { if (mediaPlayer != null) { return currentBufferPercent; } return 0; } @Nullable public WindowInfo getWindowInfo() { return null; } public boolean setPlaybackSpeed(float speed) { // Marshmallow+ support setting the playback speed natively if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { PlaybackParams params = new PlaybackParams(); params.setSpeed(speed); mediaPlayer.setPlaybackParams(params); return true; } return false; } public float getPlaybackSpeed() { // Marshmallow+ support setting the playback speed natively if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { return mediaPlayer.getPlaybackParams().getSpeed(); } return 1F; } /** * Performs the functionality to stop the video in playback * * @param clearSurface <code>true</code> if the surface should be cleared */ public void stopPlayback(boolean clearSurface) { currentState = State.IDLE; if (isReady()) { try { mediaPlayer.stop(); } catch (Exception e) { Log.d(TAG, "stopPlayback: error calling mediaPlayer.stop()", e); } } playRequested = false; if (clearSurface) { listenerMux.clearSurfaceWhenReady(clearableSurface); } } /** * Cleans up the resources being held. This should only be called when * destroying the video view */ public void suspend() { currentState = State.IDLE; try { mediaPlayer.reset(); mediaPlayer.release(); } catch (Exception e) { Log.d(TAG, "stopPlayback: error calling mediaPlayer.reset() or mediaPlayer.release()", e); } playRequested = false; } public boolean restart() { if (currentState != State.COMPLETED) { return false; } seekTo(0); start(); //Makes sure the listeners get the onPrepared callback listenerMux.setNotifiedPrepared(false); listenerMux.setNotifiedCompleted(false); return true; } /** * Sets video URI using specific headers. * * @param uri The Uri for the video to play * @param headers The headers for the URI request. * Note that the cross domain redirection is allowed by default, but that can be * changed with key/value pairs through the headers parameter with * "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value * to disallow or allow cross domain redirection. */ public void setVideoURI(Uri uri, @Nullable Map<String, String> headers) { this.headers = headers; requestedSeek = 0; playRequested = false; openVideo(uri); } public void setListenerMux(ListenerMux listenerMux) { this.listenerMux = listenerMux; setOnCompletionListener(listenerMux); setOnPreparedListener(listenerMux); setOnBufferingUpdateListener(listenerMux); setOnSeekCompleteListener(listenerMux); setOnErrorListener(listenerMux); } /** * Register a callback to be invoked when the media file * is loaded and ready to go. * * @param listener The callback that will be run */ public void setOnPreparedListener(@Nullable MediaPlayer.OnPreparedListener listener) { onPreparedListener = listener; } /** * Register a callback to be invoked when the end of a media file * has been reached during playback. * * @param listener The callback that will be run */ public void setOnCompletionListener(@Nullable MediaPlayer.OnCompletionListener listener) { onCompletionListener = listener; } /** * Register a callback to be invoked when the status of a network * stream's buffer has changed. * * @param listener the callback that will be run. */ public void setOnBufferingUpdateListener(@Nullable MediaPlayer.OnBufferingUpdateListener listener) { onBufferingUpdateListener = listener; } /** * Register a callback to be invoked when a seek operation has been * completed. * * @param listener the callback that will be run */ public void setOnSeekCompleteListener(@Nullable MediaPlayer.OnSeekCompleteListener listener) { onSeekCompleteListener = listener; } /** * Register a callback to be invoked when an error occurs * during playback or setup. If no listener is specified, * or if the listener returned false, TextureVideoView will inform * the user of any errors. * * @param listener The callback that will be run */ public void setOnErrorListener(@Nullable MediaPlayer.OnErrorListener listener) { onErrorListener = listener; } /** * Register a callback to be invoked when an informational event * occurs during playback or setup. * * @param listener The callback that will be run */ public void setOnInfoListener(@Nullable MediaPlayer.OnInfoListener listener) { onInfoListener = listener; } public void onSurfaceSizeChanged(int width, int height) { if (mediaPlayer == null || width <= 0 || height <= 0) { return; } if (requestedSeek != 0) { seekTo(requestedSeek); } if (playRequested) { start(); } } public void onSurfaceReady(Surface surface) { mediaPlayer.setSurface(surface); if (playRequested) { start(); } } protected void initMediaPlayer() { mediaPlayer = new MediaPlayer(); mediaPlayer.setOnInfoListener(internalListeners); mediaPlayer.setOnErrorListener(internalListeners); mediaPlayer.setOnPreparedListener(internalListeners); mediaPlayer.setOnCompletionListener(internalListeners); mediaPlayer.setOnSeekCompleteListener(internalListeners); mediaPlayer.setOnBufferingUpdateListener(internalListeners); mediaPlayer.setOnVideoSizeChangedListener(internalListeners); mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); mediaPlayer.setScreenOnWhilePlaying(true); } protected boolean isReady() { return currentState != State.ERROR && currentState != State.IDLE && currentState != State.PREPARING; } protected void openVideo(@Nullable Uri uri) { if (uri == null) { return; } currentBufferPercent = 0; try { mediaPlayer.reset(); mediaPlayer.setDataSource(context.getApplicationContext(), uri, headers); mediaPlayer.prepareAsync(); currentState = State.PREPARING; } catch (IOException | IllegalArgumentException ex) { Log.w(TAG, "Unable to open content: " + uri, ex); currentState = State.ERROR; internalListeners.onError(mediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); } } public class InternalListeners implements MediaPlayer.OnBufferingUpdateListener, MediaPlayer.OnErrorListener, MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnSeekCompleteListener, MediaPlayer.OnInfoListener, MediaPlayer.OnVideoSizeChangedListener { @Override public void onBufferingUpdate(MediaPlayer mp, int percent) { currentBufferPercent = percent; if (onBufferingUpdateListener != null) { onBufferingUpdateListener.onBufferingUpdate(mp, percent); } } @Override public void onCompletion(MediaPlayer mp) { currentState = State.COMPLETED; if (onCompletionListener != null) { onCompletionListener.onCompletion(mediaPlayer); } } @Override public void onSeekComplete(MediaPlayer mp) { if (onSeekCompleteListener != null) { onSeekCompleteListener.onSeekComplete(mp); } } @Override public boolean onError(MediaPlayer mp, int what, int extra) { Log.d(TAG, "Error: " + what + "," + extra); currentState = State.ERROR; return onErrorListener == null || onErrorListener.onError(mediaPlayer, what, extra); } @Override public void onPrepared(MediaPlayer mp) { currentState = State.PREPARED; if (onPreparedListener != null) { onPreparedListener.onPrepared(mediaPlayer); } callback.videoSizeChanged(mp.getVideoWidth(), mp.getVideoHeight()); if (requestedSeek != 0) { seekTo(requestedSeek); } if (playRequested) { start(); } } @Override public boolean onInfo(MediaPlayer mp, int what, int extra) { return onInfoListener == null || onInfoListener.onInfo(mp, what, extra); } @Override public void onVideoSizeChanged(MediaPlayer mp, int width, int height) { callback.videoSizeChanged(mp.getVideoWidth(), mp.getVideoHeight()); } } }