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