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 com.android.tv.dvr.ui.playback; 18 19 import android.media.PlaybackParams; 20 import android.media.session.PlaybackState; 21 import android.media.tv.TvContentRating; 22 import android.media.tv.TvInputManager; 23 import android.media.tv.TvTrackInfo; 24 import android.media.tv.TvView; 25 import android.text.TextUtils; 26 import android.util.Log; 27 import com.android.tv.dvr.data.RecordedProgram; 28 import java.util.ArrayList; 29 import java.util.List; 30 import java.util.concurrent.TimeUnit; 31 32 class DvrPlayer { 33 private static final String TAG = "DvrPlayer"; 34 private static final boolean DEBUG = false; 35 36 /** The max rewinding speed supported by DVR player. */ 37 public static final int MAX_REWIND_SPEED = 256; 38 /** The max fast-forwarding speed supported by DVR player. */ 39 public static final int MAX_FAST_FORWARD_SPEED = 256; 40 41 private static final long SEEK_POSITION_MARGIN_MS = TimeUnit.SECONDS.toMillis(2); 42 private static final long REWIND_POSITION_MARGIN_MS = 32; // Workaround value. b/29994826 43 44 private RecordedProgram mProgram; 45 private long mInitialSeekPositionMs; 46 private final TvView mTvView; 47 private DvrPlayerCallback mCallback; 48 private OnAspectRatioChangedListener mOnAspectRatioChangedListener; 49 private OnContentBlockedListener mOnContentBlockedListener; 50 private OnTracksAvailabilityChangedListener mOnTracksAvailabilityChangedListener; 51 private OnTrackSelectedListener mOnAudioTrackSelectedListener; 52 private OnTrackSelectedListener mOnSubtitleTrackSelectedListener; 53 private String mSelectedAudioTrackId; 54 private String mSelectedSubtitleTrackId; 55 private float mAspectRatio = Float.NaN; 56 private int mPlaybackState = PlaybackState.STATE_NONE; 57 private long mTimeShiftCurrentPositionMs; 58 private boolean mPauseOnPrepared; 59 private boolean mHasClosedCaption; 60 private boolean mHasMultiAudio; 61 private final PlaybackParams mPlaybackParams = new PlaybackParams(); 62 private final DvrPlayerCallback mEmptyCallback = new DvrPlayerCallback(); 63 private long mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; 64 private boolean mTimeShiftPlayAvailable; 65 66 public static class DvrPlayerCallback { 67 /** 68 * Called when the playback position is changed. The normal updating frequency is around 1 69 * sec., which is restricted to the implementation of {@link 70 * android.media.tv.TvInputService}. 71 */ 72 public void onPlaybackPositionChanged(long positionMs) {} 73 /** Called when the playback state or the playback speed is changed. */ 74 public void onPlaybackStateChanged(int playbackState, int playbackSpeed) {} 75 /** Called when the playback toward the end. */ 76 public void onPlaybackEnded() {} 77 } 78 79 public interface OnAspectRatioChangedListener { 80 /** 81 * Called when the Video's aspect ratio is changed. 82 * 83 * @param videoAspectRatio The aspect ratio of video. 0 stands for unknown ratios. Listeners 84 * should handle it carefully. 85 */ 86 void onAspectRatioChanged(float videoAspectRatio); 87 } 88 89 public interface OnContentBlockedListener { 90 /** Called when the Video's aspect ratio is changed. */ 91 void onContentBlocked(TvContentRating rating); 92 } 93 94 public interface OnTracksAvailabilityChangedListener { 95 /** Called when the Video's subtitle or audio tracks are changed. */ 96 void onTracksAvailabilityChanged(boolean hasClosedCaption, boolean hasMultiAudio); 97 } 98 99 public interface OnTrackSelectedListener { 100 /** Called when certain subtitle or audio track is selected. */ 101 void onTrackSelected(String selectedTrackId); 102 } 103 104 public DvrPlayer(TvView tvView) { 105 mTvView = tvView; 106 mTvView.setCaptionEnabled(true); 107 mPlaybackParams.setSpeed(1.0f); 108 setTvViewCallbacks(); 109 setCallback(null); 110 } 111 112 /** 113 * Prepares playback. 114 * 115 * @param doPlay indicates DVR player do or do not start playback after media is prepared. 116 */ 117 public void prepare(boolean doPlay) throws IllegalStateException { 118 if (DEBUG) Log.d(TAG, "prepare()"); 119 if (mProgram == null) { 120 throw new IllegalStateException("Recorded program not set"); 121 } else if (mPlaybackState != PlaybackState.STATE_NONE) { 122 throw new IllegalStateException("Playback is already prepared"); 123 } 124 mTvView.timeShiftPlay(mProgram.getInputId(), mProgram.getUri()); 125 mPlaybackState = PlaybackState.STATE_CONNECTING; 126 mPauseOnPrepared = !doPlay; 127 mCallback.onPlaybackStateChanged(mPlaybackState, 1); 128 } 129 130 /** Resumes playback. */ 131 public void play() throws IllegalStateException { 132 if (DEBUG) Log.d(TAG, "play()"); 133 if (!isPlaybackPrepared()) { 134 throw new IllegalStateException("Recorded program not set or video not ready yet"); 135 } 136 switch (mPlaybackState) { 137 case PlaybackState.STATE_FAST_FORWARDING: 138 case PlaybackState.STATE_REWINDING: 139 setPlaybackSpeed(1); 140 break; 141 default: 142 mTvView.timeShiftResume(); 143 } 144 mPlaybackState = PlaybackState.STATE_PLAYING; 145 mCallback.onPlaybackStateChanged(mPlaybackState, 1); 146 } 147 148 /** Pauses playback. */ 149 public void pause() throws IllegalStateException { 150 if (DEBUG) Log.d(TAG, "pause()"); 151 if (!isPlaybackPrepared()) { 152 throw new IllegalStateException("Recorded program not set or playback not started yet"); 153 } 154 switch (mPlaybackState) { 155 case PlaybackState.STATE_FAST_FORWARDING: 156 case PlaybackState.STATE_REWINDING: 157 setPlaybackSpeed(1); 158 // falls through 159 case PlaybackState.STATE_PLAYING: 160 mTvView.timeShiftPause(); 161 mPlaybackState = PlaybackState.STATE_PAUSED; 162 break; 163 default: 164 break; 165 } 166 mCallback.onPlaybackStateChanged(mPlaybackState, 1); 167 } 168 169 /** 170 * Fast-forwards playback with the given speed. If the given speed is larger than {@value 171 * #MAX_FAST_FORWARD_SPEED}, uses {@value #MAX_FAST_FORWARD_SPEED}. 172 */ 173 public void fastForward(int speed) throws IllegalStateException { 174 if (DEBUG) Log.d(TAG, "fastForward()"); 175 if (!isPlaybackPrepared()) { 176 throw new IllegalStateException("Recorded program not set or playback not started yet"); 177 } 178 if (speed <= 0) { 179 throw new IllegalArgumentException("Speed cannot be negative or 0"); 180 } 181 if (mTimeShiftCurrentPositionMs >= mProgram.getDurationMillis() - SEEK_POSITION_MARGIN_MS) { 182 return; 183 } 184 speed = Math.min(speed, MAX_FAST_FORWARD_SPEED); 185 if (DEBUG) Log.d(TAG, "Let's play with speed: " + speed); 186 setPlaybackSpeed(speed); 187 mPlaybackState = PlaybackState.STATE_FAST_FORWARDING; 188 mCallback.onPlaybackStateChanged(mPlaybackState, speed); 189 } 190 191 /** 192 * Rewinds playback with the given speed. If the given speed is larger than {@value 193 * #MAX_REWIND_SPEED}, uses {@value #MAX_REWIND_SPEED}. 194 */ 195 public void rewind(int speed) throws IllegalStateException { 196 if (DEBUG) Log.d(TAG, "rewind()"); 197 if (!isPlaybackPrepared()) { 198 throw new IllegalStateException("Recorded program not set or playback not started yet"); 199 } 200 if (speed <= 0) { 201 throw new IllegalArgumentException("Speed cannot be negative or 0"); 202 } 203 if (mTimeShiftCurrentPositionMs <= REWIND_POSITION_MARGIN_MS) { 204 return; 205 } 206 speed = Math.min(speed, MAX_REWIND_SPEED); 207 if (DEBUG) Log.d(TAG, "Let's play with speed: " + speed); 208 setPlaybackSpeed(-speed); 209 mPlaybackState = PlaybackState.STATE_REWINDING; 210 mCallback.onPlaybackStateChanged(mPlaybackState, speed); 211 } 212 213 /** Seeks playback to the specified position. */ 214 public void seekTo(long positionMs) throws IllegalStateException { 215 if (DEBUG) Log.d(TAG, "seekTo()"); 216 if (!isPlaybackPrepared()) { 217 throw new IllegalStateException("Recorded program not set or playback not started yet"); 218 } 219 if (mProgram == null || mPlaybackState == PlaybackState.STATE_NONE) { 220 return; 221 } 222 positionMs = getRealSeekPosition(positionMs, SEEK_POSITION_MARGIN_MS); 223 if (DEBUG) Log.d(TAG, "Now: " + getPlaybackPosition() + ", shift to: " + positionMs); 224 mTvView.timeShiftSeekTo(positionMs + mStartPositionMs); 225 if (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING 226 || mPlaybackState == PlaybackState.STATE_REWINDING) { 227 mPlaybackState = PlaybackState.STATE_PLAYING; 228 mTvView.timeShiftResume(); 229 mCallback.onPlaybackStateChanged(mPlaybackState, 1); 230 } 231 } 232 233 /** Resets playback. */ 234 public void reset() { 235 if (DEBUG) Log.d(TAG, "reset()"); 236 mCallback.onPlaybackStateChanged(PlaybackState.STATE_NONE, 1); 237 mPlaybackState = PlaybackState.STATE_NONE; 238 mTvView.reset(); 239 mTimeShiftPlayAvailable = false; 240 mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; 241 mTimeShiftCurrentPositionMs = 0; 242 mPlaybackParams.setSpeed(1.0f); 243 mProgram = null; 244 mSelectedAudioTrackId = null; 245 mSelectedSubtitleTrackId = null; 246 } 247 248 /** Sets callbacks for playback. */ 249 public void setCallback(DvrPlayerCallback callback) { 250 if (callback != null) { 251 mCallback = callback; 252 } else { 253 mCallback = mEmptyCallback; 254 } 255 } 256 257 /** Sets the listener to aspect ratio changing. */ 258 public void setOnAspectRatioChangedListener(OnAspectRatioChangedListener listener) { 259 mOnAspectRatioChangedListener = listener; 260 } 261 262 /** Sets the listener to content blocking. */ 263 public void setOnContentBlockedListener(OnContentBlockedListener listener) { 264 mOnContentBlockedListener = listener; 265 } 266 267 /** Sets the listener to tracks changing. */ 268 public void setOnTracksAvailabilityChangedListener( 269 OnTracksAvailabilityChangedListener listener) { 270 mOnTracksAvailabilityChangedListener = listener; 271 } 272 273 /** 274 * Sets the listener to tracks of the given type being selected. 275 * 276 * @param trackType should be either {@link TvTrackInfo#TYPE_AUDIO} or {@link 277 * TvTrackInfo#TYPE_SUBTITLE}. 278 */ 279 public void setOnTrackSelectedListener(int trackType, OnTrackSelectedListener listener) { 280 if (trackType == TvTrackInfo.TYPE_AUDIO) { 281 mOnAudioTrackSelectedListener = listener; 282 } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { 283 mOnSubtitleTrackSelectedListener = listener; 284 } 285 } 286 287 /** Gets the listener to tracks of the given type being selected. */ 288 public OnTrackSelectedListener getOnTrackSelectedListener(int trackType) { 289 if (trackType == TvTrackInfo.TYPE_AUDIO) { 290 return mOnAudioTrackSelectedListener; 291 } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { 292 return mOnSubtitleTrackSelectedListener; 293 } 294 return null; 295 } 296 297 /** Sets recorded programs for playback. If the player is playing another program, stops it. */ 298 public void setProgram(RecordedProgram program, long initialSeekPositionMs) { 299 if (mProgram != null && mProgram.equals(program)) { 300 return; 301 } 302 if (mPlaybackState != PlaybackState.STATE_NONE) { 303 reset(); 304 } 305 mInitialSeekPositionMs = initialSeekPositionMs; 306 mProgram = program; 307 } 308 309 /** Returns the recorded program now playing. */ 310 public RecordedProgram getProgram() { 311 return mProgram; 312 } 313 314 /** Returns the currrent playback posistion in msecs. */ 315 public long getPlaybackPosition() { 316 return mTimeShiftCurrentPositionMs; 317 } 318 319 /** Returns the playback speed currently used. */ 320 public int getPlaybackSpeed() { 321 return (int) mPlaybackParams.getSpeed(); 322 } 323 324 /** Returns the playback state defined in {@link android.media.session.PlaybackState}. */ 325 public int getPlaybackState() { 326 return mPlaybackState; 327 } 328 329 /** Returns the subtitle tracks of the current playback. */ 330 public ArrayList<TvTrackInfo> getSubtitleTracks() { 331 return new ArrayList<>(mTvView.getTracks(TvTrackInfo.TYPE_SUBTITLE)); 332 } 333 334 /** Returns the audio tracks of the current playback. */ 335 public ArrayList<TvTrackInfo> getAudioTracks() { 336 return new ArrayList<>(mTvView.getTracks(TvTrackInfo.TYPE_AUDIO)); 337 } 338 339 /** Returns the ID of the selected track of the given type. */ 340 public String getSelectedTrackId(int trackType) { 341 if (trackType == TvTrackInfo.TYPE_AUDIO) { 342 return mSelectedAudioTrackId; 343 } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { 344 return mSelectedSubtitleTrackId; 345 } 346 return null; 347 } 348 349 /** Returns if playback of the recorded program is started. */ 350 public boolean isPlaybackPrepared() { 351 return mPlaybackState != PlaybackState.STATE_NONE 352 && mPlaybackState != PlaybackState.STATE_CONNECTING; 353 } 354 355 /** 356 * Selects the given track. 357 * 358 * @return ID of the selected track. 359 */ 360 String selectTrack(int trackType, TvTrackInfo selectedTrack) { 361 String oldSelectedTrackId = getSelectedTrackId(trackType); 362 String newSelectedTrackId = selectedTrack == null ? null : selectedTrack.getId(); 363 if (!TextUtils.equals(oldSelectedTrackId, newSelectedTrackId)) { 364 if (selectedTrack == null) { 365 mTvView.selectTrack(trackType, null); 366 return null; 367 } else { 368 List<TvTrackInfo> tracks = mTvView.getTracks(trackType); 369 if (tracks != null && tracks.contains(selectedTrack)) { 370 mTvView.selectTrack(trackType, newSelectedTrackId); 371 return newSelectedTrackId; 372 } else if (trackType == TvTrackInfo.TYPE_SUBTITLE && oldSelectedTrackId != null) { 373 // Track not found, disabled closed caption. 374 mTvView.selectTrack(trackType, null); 375 return null; 376 } 377 } 378 } 379 return oldSelectedTrackId; 380 } 381 382 private void setSelectedTrackId(int trackType, String trackId) { 383 if (trackType == TvTrackInfo.TYPE_AUDIO) { 384 mSelectedAudioTrackId = trackId; 385 } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { 386 mSelectedSubtitleTrackId = trackId; 387 } 388 } 389 390 private void setPlaybackSpeed(int speed) { 391 mPlaybackParams.setSpeed(speed); 392 mTvView.timeShiftSetPlaybackParams(mPlaybackParams); 393 } 394 395 private long getRealSeekPosition(long seekPositionMs, long endMarginMs) { 396 return Math.max(0, Math.min(seekPositionMs, mProgram.getDurationMillis() - endMarginMs)); 397 } 398 399 private void setTvViewCallbacks() { 400 mTvView.setTimeShiftPositionCallback( 401 new TvView.TimeShiftPositionCallback() { 402 @Override 403 public void onTimeShiftStartPositionChanged(String inputId, long timeMs) { 404 if (DEBUG) Log.d(TAG, "onTimeShiftStartPositionChanged:" + timeMs); 405 mStartPositionMs = timeMs; 406 if (mTimeShiftPlayAvailable) { 407 resumeToWatchedPositionIfNeeded(); 408 } 409 } 410 411 @Override 412 public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) { 413 if (DEBUG) Log.d(TAG, "onTimeShiftCurrentPositionChanged: " + timeMs); 414 if (!mTimeShiftPlayAvailable) { 415 // Workaround of b/31436263 416 return; 417 } 418 // Workaround of b/32211561, TIF won't report start position when TIS report 419 // its start position as 0. In that case, we have to do the prework of 420 // playback 421 // on the first time we get current position, and the start position should 422 // be 0 423 // at that time. 424 if (mStartPositionMs == TvInputManager.TIME_SHIFT_INVALID_TIME) { 425 mStartPositionMs = 0; 426 resumeToWatchedPositionIfNeeded(); 427 } 428 timeMs -= mStartPositionMs; 429 if (mPlaybackState == PlaybackState.STATE_REWINDING 430 && timeMs <= REWIND_POSITION_MARGIN_MS) { 431 play(); 432 } else { 433 mTimeShiftCurrentPositionMs = getRealSeekPosition(timeMs, 0); 434 mCallback.onPlaybackPositionChanged(mTimeShiftCurrentPositionMs); 435 if (timeMs >= mProgram.getDurationMillis()) { 436 pause(); 437 mCallback.onPlaybackEnded(); 438 } 439 } 440 } 441 }); 442 mTvView.setCallback( 443 new TvView.TvInputCallback() { 444 @Override 445 public void onTimeShiftStatusChanged(String inputId, int status) { 446 if (DEBUG) Log.d(TAG, "onTimeShiftStatusChanged:" + status); 447 if (status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE 448 && mPlaybackState == PlaybackState.STATE_CONNECTING) { 449 mTimeShiftPlayAvailable = true; 450 if (mStartPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { 451 // onTimeShiftStatusChanged is sometimes called after 452 // onTimeShiftStartPositionChanged is called. In this case, 453 // resumeToWatchedPositionIfNeeded needs to be called here. 454 resumeToWatchedPositionIfNeeded(); 455 } 456 } 457 } 458 459 @Override 460 public void onTracksChanged(String inputId, List<TvTrackInfo> tracks) { 461 boolean hasClosedCaption = 462 !mTvView.getTracks(TvTrackInfo.TYPE_SUBTITLE).isEmpty(); 463 boolean hasMultiAudio = 464 mTvView.getTracks(TvTrackInfo.TYPE_AUDIO).size() > 1; 465 if ((hasClosedCaption != mHasClosedCaption 466 || hasMultiAudio != mHasMultiAudio) 467 && mOnTracksAvailabilityChangedListener != null) { 468 mOnTracksAvailabilityChangedListener.onTracksAvailabilityChanged( 469 hasClosedCaption, hasMultiAudio); 470 } 471 mHasClosedCaption = hasClosedCaption; 472 mHasMultiAudio = hasMultiAudio; 473 } 474 475 @Override 476 public void onTrackSelected(String inputId, int type, String trackId) { 477 if (type == TvTrackInfo.TYPE_AUDIO || type == TvTrackInfo.TYPE_SUBTITLE) { 478 setSelectedTrackId(type, trackId); 479 OnTrackSelectedListener listener = getOnTrackSelectedListener(type); 480 if (listener != null) { 481 listener.onTrackSelected(trackId); 482 } 483 } else if (type == TvTrackInfo.TYPE_VIDEO 484 && trackId != null 485 && mOnAspectRatioChangedListener != null) { 486 List<TvTrackInfo> trackInfos = 487 mTvView.getTracks(TvTrackInfo.TYPE_VIDEO); 488 if (trackInfos != null) { 489 for (TvTrackInfo trackInfo : trackInfos) { 490 if (trackInfo.getId().equals(trackId)) { 491 float videoAspectRatio; 492 int videoWidth = trackInfo.getVideoWidth(); 493 int videoHeight = trackInfo.getVideoHeight(); 494 if (videoWidth > 0 && videoHeight > 0) { 495 videoAspectRatio = 496 trackInfo.getVideoPixelAspectRatio() 497 * trackInfo.getVideoWidth() 498 / trackInfo.getVideoHeight(); 499 } else { 500 // Aspect ratio is unknown. Pass the message to 501 // listeners. 502 videoAspectRatio = 0; 503 } 504 if (DEBUG) Log.d(TAG, "Aspect Ratio: " + videoAspectRatio); 505 if (mAspectRatio != videoAspectRatio 506 || videoAspectRatio == 0) { 507 mOnAspectRatioChangedListener.onAspectRatioChanged( 508 videoAspectRatio); 509 mAspectRatio = videoAspectRatio; 510 return; 511 } 512 } 513 } 514 } 515 } 516 } 517 518 @Override 519 public void onContentBlocked(String inputId, TvContentRating rating) { 520 if (mOnContentBlockedListener != null) { 521 mOnContentBlockedListener.onContentBlocked(rating); 522 } 523 } 524 }); 525 } 526 527 private void resumeToWatchedPositionIfNeeded() { 528 if (mInitialSeekPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { 529 mTvView.timeShiftSeekTo( 530 getRealSeekPosition(mInitialSeekPositionMs, SEEK_POSITION_MARGIN_MS) 531 + mStartPositionMs); 532 mInitialSeekPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; 533 } 534 if (mPauseOnPrepared) { 535 mTvView.timeShiftPause(); 536 mPlaybackState = PlaybackState.STATE_PAUSED; 537 mPauseOnPrepared = false; 538 } else { 539 mTvView.timeShiftResume(); 540 mPlaybackState = PlaybackState.STATE_PLAYING; 541 } 542 mCallback.onPlaybackStateChanged(mPlaybackState, 1); 543 } 544 } 545