/* * Copyright (C) 2016 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 android.support.v17.leanback.media; import android.content.Context; import android.graphics.drawable.Drawable; import android.media.AudioManager; import android.media.MediaPlayer; import android.net.Uri; import android.os.Handler; import android.support.v17.leanback.widget.Action; import android.support.v17.leanback.widget.ArrayObjectAdapter; import android.support.v17.leanback.widget.OnItemViewSelectedListener; import android.support.v17.leanback.widget.PlaybackControlsRow; import android.support.v17.leanback.widget.Presenter; import android.support.v17.leanback.widget.Row; import android.support.v17.leanback.widget.RowPresenter; import android.view.KeyEvent; import android.view.SurfaceHolder; import android.view.View; import java.io.IOException; /** * This glue extends the {@link android.support.v17.leanback.media.PlaybackControlGlue} with a * {@link MediaPlayer} synchronization. It supports 7 actions: * * <ul> * <li>{@link android.support.v17.leanback.widget.PlaybackControlsRow.FastForwardAction}</li> * <li>{@link android.support.v17.leanback.widget.PlaybackControlsRow.RewindAction}</li> * <li>{@link android.support.v17.leanback.widget.PlaybackControlsRow.PlayPauseAction}</li> * <li>{@link android.support.v17.leanback.widget.PlaybackControlsRow.RepeatAction}</li> * <li>{@link android.support.v17.leanback.widget.PlaybackControlsRow.ThumbsDownAction}</li> * <li>{@link android.support.v17.leanback.widget.PlaybackControlsRow.ThumbsUpAction}</li> * </ul> * * @hide */ public class MediaPlayerGlue extends PlaybackControlGlue implements OnItemViewSelectedListener { public static final int NO_REPEAT = 0; public static final int REPEAT_ONE = 1; public static final int REPEAT_ALL = 2; public static final int FAST_FORWARD_REWIND_STEP = 10 * 1000; // in milliseconds public static final int FAST_FORWARD_REWIND_REPEAT_DELAY = 200; // in milliseconds private static final String TAG = "MediaPlayerGlue"; protected final PlaybackControlsRow.ThumbsDownAction mThumbsDownAction; protected final PlaybackControlsRow.ThumbsUpAction mThumbsUpAction; MediaPlayer mPlayer = new MediaPlayer(); private final PlaybackControlsRow.RepeatAction mRepeatAction; private Runnable mRunnable; private Handler mHandler = new Handler(); private boolean mInitialized = false; // true when the MediaPlayer is prepared/initialized private Action mSelectedAction; // the action which is currently selected by the user private long mLastKeyDownEvent = 0L; // timestamp when the last DPAD_CENTER KEY_DOWN occurred private Uri mMediaSourceUri = null; private String mMediaSourcePath = null; private PlayerCallback mPlayerCallback; private MediaPlayer.OnCompletionListener mOnCompletionListener; private String mArtist; private String mTitle; private Drawable mCover; /** * Sets the drawable representing cover image. */ public void setCover(Drawable cover) { this.mCover = cover; } /** * Sets the artist name. */ public void setArtist(String artist) { this.mArtist = artist; } /** * Sets the media title. */ public void setTitle(String title) { this.mTitle = title; } /** * Sets the url for the video. */ public void setVideoUrl(String videoUrl) { setMediaSource(videoUrl); onMetadataChanged(); } /** * Constructor. */ public MediaPlayerGlue(Context context) { this(context, new int[]{1}, new int[]{1}); } /** * Constructor. */ public MediaPlayerGlue( Context context, int[] fastForwardSpeeds, int[] rewindSpeeds) { super(context, fastForwardSpeeds, rewindSpeeds); // Instantiate secondary actions mRepeatAction = new PlaybackControlsRow.RepeatAction(getContext()); mThumbsDownAction = new PlaybackControlsRow.ThumbsDownAction(getContext()); mThumbsUpAction = new PlaybackControlsRow.ThumbsUpAction(getContext()); mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsAction.OUTLINE); mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsAction.OUTLINE); } @Override protected void onAttachedToHost(PlaybackGlueHost host) { super.onAttachedToHost(host); if (host instanceof SurfaceHolderGlueHost) { ((SurfaceHolderGlueHost) host).setSurfaceHolderCallback( new VideoPlayerSurfaceHolderCallback()); } } /** * Sets the callback, which would tell the listener that video is ready to be played. */ public void setPlayerCallback(PlayerCallback callback) { this.mPlayerCallback = callback; } /** * Will reset the {@link MediaPlayer} and the glue such that a new file can be played. You are * not required to call this method before playing the first file. However you have to call it * before playing a second one. */ public void reset() { mInitialized = false; mPlayer.reset(); } /** * Release internal MediaPlayer. Should not use the object after call release(). */ public void release() { mInitialized = false; mPlayer.release(); } @Override protected void onDetachedFromHost() { if (getHost() instanceof SurfaceHolderGlueHost) { ((SurfaceHolderGlueHost) getHost()).setSurfaceHolderCallback(null); } reset(); release(); super.onDetachedFromHost(); } @Override protected void onCreateSecondaryActions(ArrayObjectAdapter secondaryActionsAdapter) { secondaryActionsAdapter.add(mRepeatAction); secondaryActionsAdapter.add(mThumbsDownAction); secondaryActionsAdapter.add(mThumbsUpAction); } /** * @see MediaPlayer#setDisplay(SurfaceHolder) */ public void setDisplay(SurfaceHolder surfaceHolder) { mPlayer.setDisplay(surfaceHolder); } @Override public void enableProgressUpdating(final boolean enabled) { if (mRunnable != null) mHandler.removeCallbacks(mRunnable); if (!enabled) { return; } if (mRunnable == null) { mRunnable = new Runnable() { @Override public void run() { updateProgress(); mHandler.postDelayed(this, getUpdatePeriod()); } }; } mHandler.postDelayed(mRunnable, getUpdatePeriod()); } @Override public void onActionClicked(Action action) { // If either 'Shuffle' or 'Repeat' has been clicked we need to make sure the actions index // is incremented and the UI updated such that we can display the new state. super.onActionClicked(action); if (action instanceof PlaybackControlsRow.RepeatAction) { mRepeatAction.nextIndex(); } else if (action instanceof PlaybackControlsRow.ThumbsUpAction) { if (mThumbsUpAction.getIndex() == PlaybackControlsRow.ThumbsAction.SOLID) { mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsAction.OUTLINE); } else { mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsAction.SOLID); mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsAction.OUTLINE); } } else if (action instanceof PlaybackControlsRow.ThumbsDownAction) { if (mThumbsDownAction.getIndex() == PlaybackControlsRow.ThumbsAction.SOLID) { mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsAction.OUTLINE); } else { mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsAction.SOLID); mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsAction.OUTLINE); } } onMetadataChanged(); } @Override public boolean onKey(View v, int keyCode, KeyEvent event) { // This method is overridden in order to make implement fast forwarding and rewinding when // the user keeps the corresponding action pressed. // We only consume DPAD_CENTER Action_DOWN events on the Fast-Forward and Rewind action and // only if it has not been pressed in the last X milliseconds. boolean consume = mSelectedAction instanceof PlaybackControlsRow.RewindAction; consume = consume || mSelectedAction instanceof PlaybackControlsRow.FastForwardAction; consume = consume && mInitialized; consume = consume && event.getKeyCode() == KeyEvent.KEYCODE_DPAD_CENTER; consume = consume && event.getAction() == KeyEvent.ACTION_DOWN; consume = consume && System .currentTimeMillis() - mLastKeyDownEvent > FAST_FORWARD_REWIND_REPEAT_DELAY; if (consume) { mLastKeyDownEvent = System.currentTimeMillis(); int newPosition = getCurrentPosition() + FAST_FORWARD_REWIND_STEP; if (mSelectedAction instanceof PlaybackControlsRow.RewindAction) { newPosition = getCurrentPosition() - FAST_FORWARD_REWIND_STEP; } // Make sure the new calculated duration is in the range 0 >= X >= MediaDuration if (newPosition < 0) newPosition = 0; if (newPosition > getMediaDuration()) newPosition = getMediaDuration(); seekTo(newPosition); return true; } return super.onKey(v, keyCode, event); } @Override public boolean hasValidMedia() { return mTitle != null && (mMediaSourcePath != null || mMediaSourceUri != null); } @Override public boolean isMediaPlaying() { return mInitialized && mPlayer.isPlaying(); } @Override public CharSequence getMediaTitle() { return mTitle != null ? mTitle : "N/a"; } @Override public CharSequence getMediaSubtitle() { return mArtist != null ? mArtist : "N/a"; } @Override public int getMediaDuration() { return mInitialized ? mPlayer.getDuration() : 0; } @Override public Drawable getMediaArt() { return mCover; } @Override public long getSupportedActions() { return PlaybackControlGlue.ACTION_PLAY_PAUSE | PlaybackControlGlue.ACTION_FAST_FORWARD | PlaybackControlGlue.ACTION_REWIND; } @Override public int getCurrentSpeedId() { // 0 = Pause, 1 = Normal Playback Speed return isMediaPlaying() ? 1 : 0; } @Override public int getCurrentPosition() { return mInitialized ? mPlayer.getCurrentPosition() : 0; } @Override public void play(int speed) { if (!mInitialized || mPlayer.isPlaying()) { return; } mPlayer.start(); onMetadataChanged(); onStateChanged(); updateProgress(); } @Override public void pause() { if (isMediaPlaying()) { mPlayer.pause(); onStateChanged(); } } /** * Sets the playback mode. It currently support no repeat, repeat once and infinite * loop mode. */ public void setMode(int mode) { switch(mode) { case NO_REPEAT: mOnCompletionListener = null; break; case REPEAT_ONE: mOnCompletionListener = new MediaPlayer.OnCompletionListener() { public boolean mFirstRepeat; @Override public void onCompletion(MediaPlayer mediaPlayer) { if (!mFirstRepeat) { mFirstRepeat = true; mediaPlayer.setOnCompletionListener(null); } play(); } }; break; case REPEAT_ALL: mOnCompletionListener = new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mediaPlayer) { play(); } }; break; } } /** * Called whenever the user presses fast-forward/rewind or when the user keeps the * corresponding action pressed. * * @param newPosition The new position of the media track in milliseconds. */ protected void seekTo(int newPosition) { if (!mInitialized) { return; } mPlayer.seekTo(newPosition); } /** * Sets the media source of the player witha given URI. * * @return Returns <code>true</code> if uri represents a new media; <code>false</code> * otherwise. * @see MediaPlayer#setDataSource(String) */ public boolean setMediaSource(Uri uri) { if (mMediaSourceUri != null && mMediaSourceUri.equals(uri)) { return false; } if (mMediaSourceUri != null || mMediaSourcePath != null) { mMediaSourceUri = uri; mMediaSourcePath = null; prepareMediaForPlaying(); } else { mMediaSourceUri = uri; prepareMediaForPlaying(); } return true; } /** * Sets the media source of the player with a String path URL. * * @return Returns <code>true</code> if path represents a new media; <code>false</code> * otherwise. * @see MediaPlayer#setDataSource(String) */ public boolean setMediaSource(String path) { if (mMediaSourcePath != null && mMediaSourcePath.equals(mMediaSourcePath)) { return false; } if (mMediaSourceUri != null || mMediaSourcePath != null) { mMediaSourceUri = null; mMediaSourcePath = path; prepareMediaForPlaying(); } else { mMediaSourcePath = path; prepareMediaForPlaying(); } return true; } private void prepareMediaForPlaying() { reset(); try { if (mMediaSourceUri != null) { mPlayer.setDataSource(getContext(), mMediaSourceUri); } else if (mMediaSourcePath != null) { mPlayer.setDataSource(mMediaSourcePath); } else { return; } } catch (IOException e) { e.printStackTrace(); throw new RuntimeException(e); } mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); mPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { mInitialized = true; if (mPlayerCallback != null) { mPlayerCallback.onReadyForPlayback(); } } }); if (mOnCompletionListener != null) { mPlayer.setOnCompletionListener(mOnCompletionListener); } mPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() { @Override public void onBufferingUpdate(MediaPlayer mp, int percent) { if (getControlsRow() == null) { return; } getControlsRow().setBufferedProgress((int) (mp.getDuration() * (percent / 100f))); } }); mPlayer.prepareAsync(); onStateChanged(); } /** * This is a listener implementation for the {@link OnItemViewSelectedListener}. * This implementation is required in order to detect KEY_DOWN events * on the {@link android.support.v17.leanback.widget.PlaybackControlsRow.FastForwardAction} and * {@link android.support.v17.leanback.widget.PlaybackControlsRow.RewindAction}. Thus you * should <u>NOT</u> set another {@link OnItemViewSelectedListener} on your * Fragment. Instead, override this method and call its super (this) * implementation. * * @see OnItemViewSelectedListener#onItemSelected( *Presenter.ViewHolder, Object, RowPresenter.ViewHolder, Object) */ @Override public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) { if (item instanceof Action) { mSelectedAction = (Action) item; } else { mSelectedAction = null; } } @Override public boolean isReadyForPlayback() { return mInitialized; } /** * Implements {@link SurfaceHolder.Callback} that can then be set on the * {@link PlaybackGlueHost}. */ class VideoPlayerSurfaceHolderCallback implements SurfaceHolder.Callback { @Override public void surfaceCreated(SurfaceHolder surfaceHolder) { setDisplay(surfaceHolder); } @Override public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) { } @Override public void surfaceDestroyed(SurfaceHolder surfaceHolder) { setDisplay(null); } } }