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 */ setCover(Drawable cover)89 public void setCover(Drawable cover) { 90 this.mCover = cover; 91 } 92 93 /** 94 * Sets the artist name. 95 */ setArtist(String artist)96 public void setArtist(String artist) { 97 this.mArtist = artist; 98 } 99 100 /** 101 * Sets the media title. 102 */ setTitle(String title)103 public void setTitle(String title) { 104 this.mTitle = title; 105 } 106 107 /** 108 * Sets the url for the video. 109 */ setVideoUrl(String videoUrl)110 public void setVideoUrl(String videoUrl) { 111 setMediaSource(videoUrl); 112 onMetadataChanged(); 113 } 114 115 /** 116 * Constructor. 117 */ MediaPlayerGlue(Context context)118 public MediaPlayerGlue(Context context) { 119 this(context, new int[]{1}, new int[]{1}); 120 } 121 122 /** 123 * Constructor. 124 */ MediaPlayerGlue( Context context, int[] fastForwardSpeeds, int[] rewindSpeeds)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 onAttachedToHost(PlaybackGlueHost host)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 */ reset()151 public void reset() { 152 changeToUnitialized(); 153 mPlayer.reset(); 154 } 155 changeToUnitialized()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 */ release()171 public void release() { 172 changeToUnitialized(); 173 mPlayer.release(); 174 } 175 176 @Override onDetachedFromHost()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 onCreateSecondaryActions(ArrayObjectAdapter secondaryActionsAdapter)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 */ setDisplay(SurfaceHolder surfaceHolder)196 public void setDisplay(SurfaceHolder surfaceHolder) { 197 mPlayer.setDisplay(surfaceHolder); 198 } 199 200 @Override enableProgressUpdating(final boolean enabled)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 onActionClicked(Action action)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 onKey(View v, int keyCode, KeyEvent event)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 hasValidMedia()274 public boolean hasValidMedia() { 275 return mTitle != null && (mMediaSourcePath != null || mMediaSourceUri != null); 276 } 277 278 @Override isMediaPlaying()279 public boolean isMediaPlaying() { 280 return mInitialized && mPlayer.isPlaying(); 281 } 282 283 @Override isPlaying()284 public boolean isPlaying() { 285 return isMediaPlaying(); 286 } 287 288 @Override getMediaTitle()289 public CharSequence getMediaTitle() { 290 return mTitle != null ? mTitle : "N/a"; 291 } 292 293 @Override getMediaSubtitle()294 public CharSequence getMediaSubtitle() { 295 return mArtist != null ? mArtist : "N/a"; 296 } 297 298 @Override getMediaDuration()299 public int getMediaDuration() { 300 return mInitialized ? mPlayer.getDuration() : 0; 301 } 302 303 @Override getMediaArt()304 public Drawable getMediaArt() { 305 return mCover; 306 } 307 308 @Override getSupportedActions()309 public long getSupportedActions() { 310 return PlaybackControlGlue.ACTION_PLAY_PAUSE 311 | PlaybackControlGlue.ACTION_FAST_FORWARD 312 | PlaybackControlGlue.ACTION_REWIND; 313 } 314 315 @Override getCurrentSpeedId()316 public int getCurrentSpeedId() { 317 // 0 = Pause, 1 = Normal Playback Speed 318 return isMediaPlaying() ? 1 : 0; 319 } 320 321 @Override getCurrentPosition()322 public int getCurrentPosition() { 323 return mInitialized ? mPlayer.getCurrentPosition() : 0; 324 } 325 326 @Override play(int speed)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 pause()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 */ setMode(int mode)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 */ seekTo(int newPosition)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 */ setMediaSource(Uri uri)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 */ setMediaSource(String path)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 prepareMediaForPlaying()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 onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row)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 isPrepared()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 surfaceCreated(SurfaceHolder surfaceHolder)504 public void surfaceCreated(SurfaceHolder surfaceHolder) { 505 setDisplay(surfaceHolder); 506 } 507 508 @Override surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2)509 public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) { 510 } 511 512 @Override surfaceDestroyed(SurfaceHolder surfaceHolder)513 public void surfaceDestroyed(SurfaceHolder surfaceHolder) { 514 setDisplay(null); 515 } 516 } 517 } 518