Home | History | Annotate | Download | only in media
      1 /*
      2  * Copyright (C) 2016 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package androidx.leanback.media;
     18 
     19 import android.content.Context;
     20 import android.graphics.drawable.Drawable;
     21 import android.media.AudioManager;
     22 import android.media.MediaPlayer;
     23 import android.net.Uri;
     24 import android.os.Handler;
     25 import android.view.KeyEvent;
     26 import android.view.SurfaceHolder;
     27 import android.view.View;
     28 
     29 import androidx.annotation.RestrictTo;
     30 import androidx.leanback.widget.Action;
     31 import androidx.leanback.widget.ArrayObjectAdapter;
     32 import androidx.leanback.widget.OnItemViewSelectedListener;
     33 import androidx.leanback.widget.PlaybackControlsRow;
     34 import androidx.leanback.widget.Presenter;
     35 import androidx.leanback.widget.Row;
     36 import androidx.leanback.widget.RowPresenter;
     37 
     38 import java.io.IOException;
     39 import java.util.List;
     40 
     41 /**
     42  * This glue extends the {@link androidx.leanback.media.PlaybackControlGlue} with a
     43  * {@link MediaPlayer} synchronization. It supports 7 actions:
     44  *
     45  * <ul>
     46  * <li>{@link androidx.leanback.widget.PlaybackControlsRow.FastForwardAction}</li>
     47  * <li>{@link androidx.leanback.widget.PlaybackControlsRow.RewindAction}</li>
     48  * <li>{@link  androidx.leanback.widget.PlaybackControlsRow.PlayPauseAction}</li>
     49  * <li>{@link androidx.leanback.widget.PlaybackControlsRow.RepeatAction}</li>
     50  * <li>{@link androidx.leanback.widget.PlaybackControlsRow.ThumbsDownAction}</li>
     51  * <li>{@link androidx.leanback.widget.PlaybackControlsRow.ThumbsUpAction}</li>
     52  * </ul>
     53  *
     54  * @hide
     55  * @deprecated Use {@link MediaPlayerAdapter} with {@link PlaybackTransportControlGlue} or
     56  *             {@link PlaybackBannerControlGlue}.
     57  */
     58 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     59 @Deprecated
     60 public class MediaPlayerGlue extends PlaybackControlGlue implements
     61         OnItemViewSelectedListener {
     62 
     63     public static final int NO_REPEAT = 0;
     64     public static final int REPEAT_ONE = 1;
     65     public static final int REPEAT_ALL = 2;
     66 
     67     public static final int FAST_FORWARD_REWIND_STEP = 10 * 1000; // in milliseconds
     68     public static final int FAST_FORWARD_REWIND_REPEAT_DELAY = 200; // in milliseconds
     69     private static final String TAG = "MediaPlayerGlue";
     70     protected final PlaybackControlsRow.ThumbsDownAction mThumbsDownAction;
     71     protected final PlaybackControlsRow.ThumbsUpAction mThumbsUpAction;
     72     MediaPlayer mPlayer = new MediaPlayer();
     73     private final PlaybackControlsRow.RepeatAction mRepeatAction;
     74     private Runnable mRunnable;
     75     private Handler mHandler = new Handler();
     76     private boolean mInitialized = false; // true when the MediaPlayer is prepared/initialized
     77     private Action mSelectedAction; // the action which is currently selected by the user
     78     private long mLastKeyDownEvent = 0L; // timestamp when the last DPAD_CENTER KEY_DOWN occurred
     79     private Uri mMediaSourceUri = null;
     80     private String mMediaSourcePath = null;
     81     private MediaPlayer.OnCompletionListener mOnCompletionListener;
     82     private String mArtist;
     83     private String mTitle;
     84     private Drawable mCover;
     85 
     86     /**
     87      * Sets the drawable representing cover image.
     88      */
     89     public void setCover(Drawable cover) {
     90         this.mCover = cover;
     91     }
     92 
     93     /**
     94      * Sets the artist name.
     95      */
     96     public void setArtist(String artist) {
     97         this.mArtist = artist;
     98     }
     99 
    100     /**
    101      * Sets the media title.
    102      */
    103     public void setTitle(String title) {
    104         this.mTitle = title;
    105     }
    106 
    107     /**
    108      * Sets the url for the video.
    109      */
    110     public void setVideoUrl(String videoUrl) {
    111         setMediaSource(videoUrl);
    112         onMetadataChanged();
    113     }
    114 
    115     /**
    116      * Constructor.
    117      */
    118     public MediaPlayerGlue(Context context) {
    119         this(context, new int[]{1}, new int[]{1});
    120     }
    121 
    122     /**
    123      * Constructor.
    124      */
    125     public MediaPlayerGlue(
    126             Context context, int[] fastForwardSpeeds, int[] rewindSpeeds) {
    127         super(context, fastForwardSpeeds, rewindSpeeds);
    128 
    129         // Instantiate secondary actions
    130         mRepeatAction = new PlaybackControlsRow.RepeatAction(getContext());
    131         mThumbsDownAction = new PlaybackControlsRow.ThumbsDownAction(getContext());
    132         mThumbsUpAction = new PlaybackControlsRow.ThumbsUpAction(getContext());
    133         mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsAction.INDEX_OUTLINE);
    134         mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsAction.INDEX_OUTLINE);
    135     }
    136 
    137     @Override
    138     protected void onAttachedToHost(PlaybackGlueHost host) {
    139         super.onAttachedToHost(host);
    140         if (host instanceof SurfaceHolderGlueHost) {
    141             ((SurfaceHolderGlueHost) host).setSurfaceHolderCallback(
    142                     new VideoPlayerSurfaceHolderCallback());
    143         }
    144     }
    145 
    146     /**
    147      * Will reset the {@link MediaPlayer} and the glue such that a new file can be played. You are
    148      * not required to call this method before playing the first file. However you have to call it
    149      * before playing a second one.
    150      */
    151     public void reset() {
    152         changeToUnitialized();
    153         mPlayer.reset();
    154     }
    155 
    156     void changeToUnitialized() {
    157         if (mInitialized) {
    158             mInitialized = false;
    159             List<PlayerCallback> callbacks = getPlayerCallbacks();
    160             if (callbacks != null) {
    161                 for (PlayerCallback callback: callbacks) {
    162                     callback.onPreparedStateChanged(MediaPlayerGlue.this);
    163                 }
    164             }
    165         }
    166     }
    167 
    168     /**
    169      * Release internal MediaPlayer. Should not use the object after call release().
    170      */
    171     public void release() {
    172         changeToUnitialized();
    173         mPlayer.release();
    174     }
    175 
    176     @Override
    177     protected void onDetachedFromHost() {
    178         if (getHost() instanceof SurfaceHolderGlueHost) {
    179             ((SurfaceHolderGlueHost) getHost()).setSurfaceHolderCallback(null);
    180         }
    181         reset();
    182         release();
    183         super.onDetachedFromHost();
    184     }
    185 
    186     @Override
    187     protected void onCreateSecondaryActions(ArrayObjectAdapter secondaryActionsAdapter) {
    188         secondaryActionsAdapter.add(mRepeatAction);
    189         secondaryActionsAdapter.add(mThumbsDownAction);
    190         secondaryActionsAdapter.add(mThumbsUpAction);
    191     }
    192 
    193     /**
    194      * @see MediaPlayer#setDisplay(SurfaceHolder)
    195      */
    196     public void setDisplay(SurfaceHolder surfaceHolder) {
    197         mPlayer.setDisplay(surfaceHolder);
    198     }
    199 
    200     @Override
    201     public void enableProgressUpdating(final boolean enabled) {
    202         if (mRunnable != null) mHandler.removeCallbacks(mRunnable);
    203         if (!enabled) {
    204             return;
    205         }
    206         if (mRunnable == null) {
    207             mRunnable = new Runnable() {
    208                 @Override
    209                 public void run() {
    210                     updateProgress();
    211                     mHandler.postDelayed(this, getUpdatePeriod());
    212                 }
    213             };
    214         }
    215         mHandler.postDelayed(mRunnable, getUpdatePeriod());
    216     }
    217 
    218     @Override
    219     public void onActionClicked(Action action) {
    220         // If either 'Shuffle' or 'Repeat' has been clicked we need to make sure the actions index
    221         // is incremented and the UI updated such that we can display the new state.
    222         super.onActionClicked(action);
    223         if (action instanceof PlaybackControlsRow.RepeatAction) {
    224             ((PlaybackControlsRow.RepeatAction) action).nextIndex();
    225         } else if (action == mThumbsUpAction) {
    226             if (mThumbsUpAction.getIndex() == PlaybackControlsRow.ThumbsAction.INDEX_SOLID) {
    227                 mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsAction.INDEX_OUTLINE);
    228             } else {
    229                 mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsAction.INDEX_SOLID);
    230                 mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsAction.INDEX_OUTLINE);
    231             }
    232         } else if (action == mThumbsDownAction) {
    233             if (mThumbsDownAction.getIndex() == PlaybackControlsRow.ThumbsAction.INDEX_SOLID) {
    234                 mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsAction.INDEX_OUTLINE);
    235             } else {
    236                 mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsAction.INDEX_SOLID);
    237                 mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsAction.INDEX_OUTLINE);
    238             }
    239         }
    240         onMetadataChanged();
    241     }
    242 
    243     @Override
    244     public boolean onKey(View v, int keyCode, KeyEvent event) {
    245         // This method is overridden in order to make implement fast forwarding and rewinding when
    246         // the user keeps the corresponding action pressed.
    247         // We only consume DPAD_CENTER Action_DOWN events on the Fast-Forward and Rewind action and
    248         // only if it has not been pressed in the last X milliseconds.
    249         boolean consume = mSelectedAction instanceof PlaybackControlsRow.RewindAction;
    250         consume = consume || mSelectedAction instanceof PlaybackControlsRow.FastForwardAction;
    251         consume = consume && mInitialized;
    252         consume = consume && event.getKeyCode() == KeyEvent.KEYCODE_DPAD_CENTER;
    253         consume = consume && event.getAction() == KeyEvent.ACTION_DOWN;
    254         consume = consume && System
    255                 .currentTimeMillis() - mLastKeyDownEvent > FAST_FORWARD_REWIND_REPEAT_DELAY;
    256 
    257         if (consume) {
    258             mLastKeyDownEvent = System.currentTimeMillis();
    259             int newPosition = getCurrentPosition() + FAST_FORWARD_REWIND_STEP;
    260             if (mSelectedAction instanceof PlaybackControlsRow.RewindAction) {
    261                 newPosition = getCurrentPosition() - FAST_FORWARD_REWIND_STEP;
    262             }
    263             // Make sure the new calculated duration is in the range 0 >= X >= MediaDuration
    264             if (newPosition < 0) newPosition = 0;
    265             if (newPosition > getMediaDuration()) newPosition = getMediaDuration();
    266             seekTo(newPosition);
    267             return true;
    268         }
    269 
    270         return super.onKey(v, keyCode, event);
    271     }
    272 
    273     @Override
    274     public boolean hasValidMedia() {
    275         return mTitle != null && (mMediaSourcePath != null || mMediaSourceUri != null);
    276     }
    277 
    278     @Override
    279     public boolean isMediaPlaying() {
    280         return mInitialized && mPlayer.isPlaying();
    281     }
    282 
    283     @Override
    284     public boolean isPlaying() {
    285         return isMediaPlaying();
    286     }
    287 
    288     @Override
    289     public CharSequence getMediaTitle() {
    290         return mTitle != null ? mTitle : "N/a";
    291     }
    292 
    293     @Override
    294     public CharSequence getMediaSubtitle() {
    295         return mArtist != null ? mArtist : "N/a";
    296     }
    297 
    298     @Override
    299     public int getMediaDuration() {
    300         return mInitialized ? mPlayer.getDuration() : 0;
    301     }
    302 
    303     @Override
    304     public Drawable getMediaArt() {
    305         return mCover;
    306     }
    307 
    308     @Override
    309     public long getSupportedActions() {
    310         return PlaybackControlGlue.ACTION_PLAY_PAUSE
    311                 | PlaybackControlGlue.ACTION_FAST_FORWARD
    312                 | PlaybackControlGlue.ACTION_REWIND;
    313     }
    314 
    315     @Override
    316     public int getCurrentSpeedId() {
    317         // 0 = Pause, 1 = Normal Playback Speed
    318         return isMediaPlaying() ? 1 : 0;
    319     }
    320 
    321     @Override
    322     public int getCurrentPosition() {
    323         return mInitialized ? mPlayer.getCurrentPosition() : 0;
    324     }
    325 
    326     @Override
    327     public void play(int speed) {
    328         if (!mInitialized || mPlayer.isPlaying()) {
    329             return;
    330         }
    331         mPlayer.start();
    332         onMetadataChanged();
    333         onStateChanged();
    334         updateProgress();
    335     }
    336 
    337     @Override
    338     public void pause() {
    339         if (isMediaPlaying()) {
    340             mPlayer.pause();
    341             onStateChanged();
    342         }
    343     }
    344 
    345     /**
    346      * Sets the playback mode. It currently support no repeat, repeat once and infinite
    347      * loop mode.
    348      */
    349     public void setMode(int mode) {
    350         switch(mode) {
    351             case NO_REPEAT:
    352                 mOnCompletionListener = null;
    353                 break;
    354             case REPEAT_ONE:
    355                 mOnCompletionListener = new MediaPlayer.OnCompletionListener() {
    356                     public boolean mFirstRepeat;
    357 
    358                     @Override
    359                     public void onCompletion(MediaPlayer mediaPlayer) {
    360                         if (!mFirstRepeat) {
    361                             mFirstRepeat = true;
    362                             mediaPlayer.setOnCompletionListener(null);
    363                         }
    364                         play();
    365                     }
    366                 };
    367                 break;
    368             case REPEAT_ALL:
    369                 mOnCompletionListener = new MediaPlayer.OnCompletionListener() {
    370                     @Override
    371                     public void onCompletion(MediaPlayer mediaPlayer) {
    372                         play();
    373                     }
    374                 };
    375                 break;
    376         }
    377     }
    378 
    379     /**
    380      * Called whenever the user presses fast-forward/rewind or when the user keeps the
    381      * corresponding action pressed.
    382      *
    383      * @param newPosition The new position of the media track in milliseconds.
    384      */
    385     protected void seekTo(int newPosition) {
    386         if (!mInitialized) {
    387             return;
    388         }
    389         mPlayer.seekTo(newPosition);
    390     }
    391 
    392     /**
    393      * Sets the media source of the player witha given URI.
    394      *
    395      * @return Returns <code>true</code> if uri represents a new media; <code>false</code>
    396      * otherwise.
    397      * @see MediaPlayer#setDataSource(String)
    398      */
    399     public boolean setMediaSource(Uri uri) {
    400         if (mMediaSourceUri != null ? mMediaSourceUri.equals(uri) : uri == null) {
    401             return false;
    402         }
    403         mMediaSourceUri = uri;
    404         mMediaSourcePath = null;
    405         prepareMediaForPlaying();
    406         return true;
    407     }
    408 
    409     /**
    410      * Sets the media source of the player with a String path URL.
    411      *
    412      * @return Returns <code>true</code> if path represents a new media; <code>false</code>
    413      * otherwise.
    414      * @see MediaPlayer#setDataSource(String)
    415      */
    416     public boolean setMediaSource(String path) {
    417         if (mMediaSourcePath != null ? mMediaSourcePath.equals(path) : path == null) {
    418             return false;
    419         }
    420         mMediaSourceUri = null;
    421         mMediaSourcePath = path;
    422         prepareMediaForPlaying();
    423         return true;
    424     }
    425 
    426     private void prepareMediaForPlaying() {
    427         reset();
    428         try {
    429             if (mMediaSourceUri != null) {
    430                 mPlayer.setDataSource(getContext(), mMediaSourceUri);
    431             } else if (mMediaSourcePath != null) {
    432                 mPlayer.setDataSource(mMediaSourcePath);
    433             } else {
    434                 return;
    435             }
    436         } catch (IOException e) {
    437             e.printStackTrace();
    438             throw new RuntimeException(e);
    439         }
    440         mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
    441         mPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
    442             @Override
    443             public void onPrepared(MediaPlayer mp) {
    444                 mInitialized = true;
    445                 List<PlayerCallback> callbacks = getPlayerCallbacks();
    446                 if (callbacks != null) {
    447                     for (PlayerCallback callback: callbacks) {
    448                         callback.onPreparedStateChanged(MediaPlayerGlue.this);
    449                     }
    450                 }
    451             }
    452         });
    453 
    454         if (mOnCompletionListener != null) {
    455             mPlayer.setOnCompletionListener(mOnCompletionListener);
    456         }
    457 
    458         mPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
    459             @Override
    460             public void onBufferingUpdate(MediaPlayer mp, int percent) {
    461                 if (getControlsRow() == null) {
    462                     return;
    463                 }
    464                 getControlsRow().setBufferedProgress((int) (mp.getDuration() * (percent / 100f)));
    465             }
    466         });
    467         mPlayer.prepareAsync();
    468         onStateChanged();
    469     }
    470 
    471     /**
    472      * This is a listener implementation for the {@link OnItemViewSelectedListener}.
    473      * This implementation is required in order to detect KEY_DOWN events
    474      * on the {@link androidx.leanback.widget.PlaybackControlsRow.FastForwardAction} and
    475      * {@link androidx.leanback.widget.PlaybackControlsRow.RewindAction}. Thus you
    476      * should <u>NOT</u> set another {@link OnItemViewSelectedListener} on your
    477      * Fragment. Instead, override this method and call its super (this)
    478      * implementation.
    479      *
    480      * @see OnItemViewSelectedListener#onItemSelected(
    481      *Presenter.ViewHolder, Object, RowPresenter.ViewHolder, Object)
    482      */
    483     @Override
    484     public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
    485                                RowPresenter.ViewHolder rowViewHolder, Row row) {
    486         if (item instanceof Action) {
    487             mSelectedAction = (Action) item;
    488         } else {
    489             mSelectedAction = null;
    490         }
    491     }
    492 
    493     @Override
    494     public boolean isPrepared() {
    495         return mInitialized;
    496     }
    497 
    498     /**
    499      * Implements {@link SurfaceHolder.Callback} that can then be set on the
    500      * {@link PlaybackGlueHost}.
    501      */
    502     class VideoPlayerSurfaceHolderCallback implements SurfaceHolder.Callback {
    503         @Override
    504         public void surfaceCreated(SurfaceHolder surfaceHolder) {
    505             setDisplay(surfaceHolder);
    506         }
    507 
    508         @Override
    509         public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
    510         }
    511 
    512         @Override
    513         public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
    514             setDisplay(null);
    515         }
    516     }
    517 }
    518