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; 18 19 import android.media.PlaybackParams; 20 import android.media.tv.TvContentRating; 21 import android.media.tv.TvInputManager; 22 import android.media.tv.TvTrackInfo; 23 import android.media.tv.TvView; 24 import android.media.session.PlaybackState; 25 import android.util.Log; 26 27 import java.util.List; 28 import java.util.concurrent.TimeUnit; 29 30 public class DvrPlayer { 31 private static final String TAG = "DvrPlayer"; 32 private static final boolean DEBUG = false; 33 34 /** 35 * The max rewinding speed supported by DVR player. 36 */ 37 public static final int MAX_REWIND_SPEED = 256; 38 /** 39 * The max fast-forwarding speed supported by DVR player. 40 */ 41 public static final int MAX_FAST_FORWARD_SPEED = 256; 42 43 private static final long SEEK_POSITION_MARGIN_MS = TimeUnit.SECONDS.toMillis(2); 44 private static final long REWIND_POSITION_MARGIN_MS = 32; // Workaround value. b/29994826 45 46 private RecordedProgram mProgram; 47 private long mInitialSeekPositionMs; 48 private final TvView mTvView; 49 private DvrPlayerCallback mCallback; 50 private AspectRatioChangedListener mAspectRatioChangedListener; 51 private ContentBlockedListener mContentBlockedListener; 52 private float mAspectRatio = Float.NaN; 53 private int mPlaybackState = PlaybackState.STATE_NONE; 54 private long mTimeShiftCurrentPositionMs; 55 private boolean mPauseOnPrepared; 56 private final PlaybackParams mPlaybackParams = new PlaybackParams(); 57 private final DvrPlayerCallback mEmptyCallback = new DvrPlayerCallback(); 58 private long mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; 59 private boolean mTimeShiftPlayAvailable; 60 61 public static class DvrPlayerCallback { 62 /** 63 * Called when the playback position is changed. The normal updating frequency is 64 * around 1 sec., which is restricted to the implementation of 65 * {@link android.media.tv.TvInputService}. 66 */ onPlaybackPositionChanged(long positionMs)67 public void onPlaybackPositionChanged(long positionMs) { } 68 /** 69 * Called when the playback state or the playback speed is changed. 70 */ onPlaybackStateChanged(int playbackState, int playbackSpeed)71 public void onPlaybackStateChanged(int playbackState, int playbackSpeed) { } 72 /** 73 * Called when the playback toward the end. 74 */ onPlaybackEnded()75 public void onPlaybackEnded() { } 76 } 77 78 public interface AspectRatioChangedListener { 79 /** 80 * Called when the Video's aspect ratio is changed. 81 */ onAspectRatioChanged(float videoAspectRatio)82 void onAspectRatioChanged(float videoAspectRatio); 83 } 84 85 public interface ContentBlockedListener { 86 /** 87 * Called when the Video's aspect ratio is changed. 88 */ onContentBlocked(TvContentRating rating)89 void onContentBlocked(TvContentRating rating); 90 } 91 DvrPlayer(TvView tvView)92 public DvrPlayer(TvView tvView) { 93 mTvView = tvView; 94 mPlaybackParams.setSpeed(1.0f); 95 setTvViewCallbacks(); 96 setCallback(null); 97 } 98 99 /** 100 * Prepares playback. 101 * 102 * @param doPlay indicates DVR player do or do not start playback after media is prepared. 103 */ prepare(boolean doPlay)104 public void prepare(boolean doPlay) throws IllegalStateException { 105 if (DEBUG) Log.d(TAG, "prepare()"); 106 if (mProgram == null) { 107 throw new IllegalStateException("Recorded program not set"); 108 } else if (mPlaybackState != PlaybackState.STATE_NONE) { 109 throw new IllegalStateException("Playback is already prepared"); 110 } 111 mTvView.timeShiftPlay(mProgram.getInputId(), mProgram.getUri()); 112 mPlaybackState = PlaybackState.STATE_CONNECTING; 113 mPauseOnPrepared = !doPlay; 114 mCallback.onPlaybackStateChanged(mPlaybackState, 1); 115 } 116 117 /** 118 * Resumes playback. 119 */ play()120 public void play() throws IllegalStateException { 121 if (DEBUG) Log.d(TAG, "play()"); 122 if (!isPlaybackPrepared()) { 123 throw new IllegalStateException("Recorded program not set or video not ready yet"); 124 } 125 switch (mPlaybackState) { 126 case PlaybackState.STATE_FAST_FORWARDING: 127 case PlaybackState.STATE_REWINDING: 128 setPlaybackSpeed(1); 129 break; 130 default: 131 mTvView.timeShiftResume(); 132 } 133 mPlaybackState = PlaybackState.STATE_PLAYING; 134 mCallback.onPlaybackStateChanged(mPlaybackState, 1); 135 } 136 137 /** 138 * Pauses playback. 139 */ pause()140 public void pause() throws IllegalStateException { 141 if (DEBUG) Log.d(TAG, "pause()"); 142 if (!isPlaybackPrepared()) { 143 throw new IllegalStateException("Recorded program not set or playback not started yet"); 144 } 145 switch (mPlaybackState) { 146 case PlaybackState.STATE_FAST_FORWARDING: 147 case PlaybackState.STATE_REWINDING: 148 setPlaybackSpeed(1); 149 // falls through 150 case PlaybackState.STATE_PLAYING: 151 mTvView.timeShiftPause(); 152 mPlaybackState = PlaybackState.STATE_PAUSED; 153 break; 154 default: 155 break; 156 } 157 mCallback.onPlaybackStateChanged(mPlaybackState, 1); 158 } 159 160 /** 161 * Fast-forwards playback with the given speed. If the given speed is larger than 162 * {@value #MAX_FAST_FORWARD_SPEED}, uses {@value #MAX_FAST_FORWARD_SPEED}. 163 */ fastForward(int speed)164 public void fastForward(int speed) throws IllegalStateException { 165 if (DEBUG) Log.d(TAG, "fastForward()"); 166 if (!isPlaybackPrepared()) { 167 throw new IllegalStateException("Recorded program not set or playback not started yet"); 168 } 169 if (speed <= 0) { 170 throw new IllegalArgumentException("Speed cannot be negative or 0"); 171 } 172 if (mTimeShiftCurrentPositionMs >= mProgram.getDurationMillis() - SEEK_POSITION_MARGIN_MS) { 173 return; 174 } 175 speed = Math.min(speed, MAX_FAST_FORWARD_SPEED); 176 if (DEBUG) Log.d(TAG, "Let's play with speed: " + speed); 177 setPlaybackSpeed(speed); 178 mPlaybackState = PlaybackState.STATE_FAST_FORWARDING; 179 mCallback.onPlaybackStateChanged(mPlaybackState, speed); 180 } 181 182 /** 183 * Rewinds playback with the given speed. If the given speed is larger than 184 * {@value #MAX_REWIND_SPEED}, uses {@value #MAX_REWIND_SPEED}. 185 */ rewind(int speed)186 public void rewind(int speed) throws IllegalStateException { 187 if (DEBUG) Log.d(TAG, "rewind()"); 188 if (!isPlaybackPrepared()) { 189 throw new IllegalStateException("Recorded program not set or playback not started yet"); 190 } 191 if (speed <= 0) { 192 throw new IllegalArgumentException("Speed cannot be negative or 0"); 193 } 194 if (mTimeShiftCurrentPositionMs <= REWIND_POSITION_MARGIN_MS) { 195 return; 196 } 197 speed = Math.min(speed, MAX_REWIND_SPEED); 198 if (DEBUG) Log.d(TAG, "Let's play with speed: " + speed); 199 setPlaybackSpeed(-speed); 200 mPlaybackState = PlaybackState.STATE_REWINDING; 201 mCallback.onPlaybackStateChanged(mPlaybackState, speed); 202 } 203 204 /** 205 * Seeks playback to the specified position. 206 */ seekTo(long positionMs)207 public void seekTo(long positionMs) throws IllegalStateException { 208 if (DEBUG) Log.d(TAG, "seekTo()"); 209 if (!isPlaybackPrepared()) { 210 throw new IllegalStateException("Recorded program not set or playback not started yet"); 211 } 212 if (mProgram == null || mPlaybackState == PlaybackState.STATE_NONE) { 213 return; 214 } 215 positionMs = getRealSeekPosition(positionMs, SEEK_POSITION_MARGIN_MS); 216 if (DEBUG) Log.d(TAG, "Now: " + getPlaybackPosition() + ", shift to: " + positionMs); 217 mTvView.timeShiftSeekTo(positionMs + mStartPositionMs); 218 if (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING || 219 mPlaybackState == PlaybackState.STATE_REWINDING) { 220 mPlaybackState = PlaybackState.STATE_PLAYING; 221 mTvView.timeShiftResume(); 222 mCallback.onPlaybackStateChanged(mPlaybackState, 1); 223 } 224 } 225 226 /** 227 * Resets playback. 228 */ reset()229 public void reset() { 230 if (DEBUG) Log.d(TAG, "reset()"); 231 mCallback.onPlaybackStateChanged(PlaybackState.STATE_NONE, 1); 232 mPlaybackState = PlaybackState.STATE_NONE; 233 mTvView.reset(); 234 mTimeShiftPlayAvailable = false; 235 mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; 236 mTimeShiftCurrentPositionMs = 0; 237 mPlaybackParams.setSpeed(1.0f); 238 mProgram = null; 239 } 240 241 /** 242 * Sets callbacks for playback. 243 */ setCallback(DvrPlayerCallback callback)244 public void setCallback(DvrPlayerCallback callback) { 245 if (callback != null) { 246 mCallback = callback; 247 } else { 248 mCallback = mEmptyCallback; 249 } 250 } 251 252 /** 253 * Sets listener to aspect ratio changing. 254 */ setAspectRatioChangedListener(AspectRatioChangedListener listener)255 public void setAspectRatioChangedListener(AspectRatioChangedListener listener) { 256 mAspectRatioChangedListener = listener; 257 } 258 259 /** 260 * Sets listener to content blocking. 261 */ setContentBlockedListener(ContentBlockedListener listener)262 public void setContentBlockedListener(ContentBlockedListener listener) { 263 mContentBlockedListener = listener; 264 } 265 266 /** 267 * Sets recorded programs for playback. If the player is playing another program, stops it. 268 */ setProgram(RecordedProgram program, long initialSeekPositionMs)269 public void setProgram(RecordedProgram program, long initialSeekPositionMs) { 270 if (mProgram != null && mProgram.equals(program)) { 271 return; 272 } 273 if (mPlaybackState != PlaybackState.STATE_NONE) { 274 reset(); 275 } 276 mInitialSeekPositionMs = initialSeekPositionMs; 277 mProgram = program; 278 } 279 280 /** 281 * Returns the recorded program now playing. 282 */ getProgram()283 public RecordedProgram getProgram() { 284 return mProgram; 285 } 286 287 /** 288 * Returns the currrent playback posistion in msecs. 289 */ getPlaybackPosition()290 public long getPlaybackPosition() { 291 return mTimeShiftCurrentPositionMs; 292 } 293 294 /** 295 * Returns the playback speed currently used. 296 */ getPlaybackSpeed()297 public int getPlaybackSpeed() { 298 return (int) mPlaybackParams.getSpeed(); 299 } 300 301 /** 302 * Returns the playback state defined in {@link android.media.session.PlaybackState}. 303 */ getPlaybackState()304 public int getPlaybackState() { 305 return mPlaybackState; 306 } 307 308 /** 309 * Returns if playback of the recorded program is started. 310 */ isPlaybackPrepared()311 public boolean isPlaybackPrepared() { 312 return mPlaybackState != PlaybackState.STATE_NONE 313 && mPlaybackState != PlaybackState.STATE_CONNECTING; 314 } 315 setPlaybackSpeed(int speed)316 private void setPlaybackSpeed(int speed) { 317 mPlaybackParams.setSpeed(speed); 318 mTvView.timeShiftSetPlaybackParams(mPlaybackParams); 319 } 320 getRealSeekPosition(long seekPositionMs, long endMarginMs)321 private long getRealSeekPosition(long seekPositionMs, long endMarginMs) { 322 return Math.max(0, Math.min(seekPositionMs, mProgram.getDurationMillis() - endMarginMs)); 323 } 324 setTvViewCallbacks()325 private void setTvViewCallbacks() { 326 mTvView.setTimeShiftPositionCallback(new TvView.TimeShiftPositionCallback() { 327 @Override 328 public void onTimeShiftStartPositionChanged(String inputId, long timeMs) { 329 if (DEBUG) Log.d(TAG, "onTimeShiftStartPositionChanged:" + timeMs); 330 mStartPositionMs = timeMs; 331 if (mTimeShiftPlayAvailable) { 332 resumeToWatchedPositionIfNeeded(); 333 } 334 } 335 336 @Override 337 public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) { 338 if (DEBUG) Log.d(TAG, "onTimeShiftCurrentPositionChanged: " + timeMs); 339 if (!mTimeShiftPlayAvailable) { 340 // Workaround of b/31436263 341 return; 342 } 343 // Workaround of b/32211561, TIF won't report start position when TIS report 344 // its start position as 0. In that case, we have to do the prework of playback 345 // on the first time we get current position, and the start position should be 0 346 // at that time. 347 if (mStartPositionMs == TvInputManager.TIME_SHIFT_INVALID_TIME) { 348 mStartPositionMs = 0; 349 resumeToWatchedPositionIfNeeded(); 350 } 351 timeMs -= mStartPositionMs; 352 if (mPlaybackState == PlaybackState.STATE_REWINDING 353 && timeMs <= REWIND_POSITION_MARGIN_MS) { 354 play(); 355 } else { 356 mTimeShiftCurrentPositionMs = getRealSeekPosition(timeMs, 0); 357 mCallback.onPlaybackPositionChanged(mTimeShiftCurrentPositionMs); 358 if (timeMs >= mProgram.getDurationMillis()) { 359 pause(); 360 mCallback.onPlaybackEnded(); 361 } 362 } 363 } 364 }); 365 mTvView.setCallback(new TvView.TvInputCallback() { 366 @Override 367 public void onTimeShiftStatusChanged(String inputId, int status) { 368 if (DEBUG) Log.d(TAG, "onTimeShiftStatusChanged:" + status); 369 if (status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE 370 && mPlaybackState == PlaybackState.STATE_CONNECTING) { 371 mTimeShiftPlayAvailable = true; 372 } 373 } 374 375 @Override 376 public void onTrackSelected(String inputId, int type, String trackId) { 377 if (trackId == null || type != TvTrackInfo.TYPE_VIDEO 378 || mAspectRatioChangedListener == null) { 379 return; 380 } 381 List<TvTrackInfo> trackInfos = mTvView.getTracks(TvTrackInfo.TYPE_VIDEO); 382 if (trackInfos != null) { 383 for (TvTrackInfo trackInfo : trackInfos) { 384 if (trackInfo.getId().equals(trackId)) { 385 float videoAspectRatio = trackInfo.getVideoPixelAspectRatio() 386 * trackInfo.getVideoWidth() / trackInfo.getVideoHeight(); 387 if (DEBUG) Log.d(TAG, "Aspect Ratio: " + videoAspectRatio); 388 if (!Float.isNaN(videoAspectRatio) 389 && mAspectRatio != videoAspectRatio) { 390 mAspectRatioChangedListener 391 .onAspectRatioChanged(videoAspectRatio); 392 mAspectRatio = videoAspectRatio; 393 return; 394 } 395 } 396 } 397 } 398 } 399 400 @Override 401 public void onContentBlocked(String inputId, TvContentRating rating) { 402 if (mContentBlockedListener != null) { 403 mContentBlockedListener.onContentBlocked(rating); 404 } 405 } 406 }); 407 } 408 resumeToWatchedPositionIfNeeded()409 private void resumeToWatchedPositionIfNeeded() { 410 if (mInitialSeekPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { 411 mTvView.timeShiftSeekTo(getRealSeekPosition(mInitialSeekPositionMs, 412 SEEK_POSITION_MARGIN_MS) + mStartPositionMs); 413 mInitialSeekPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; 414 } 415 if (mPauseOnPrepared) { 416 mTvView.timeShiftPause(); 417 mPlaybackState = PlaybackState.STATE_PAUSED; 418 mPauseOnPrepared = false; 419 } else { 420 mTvView.timeShiftResume(); 421 mPlaybackState = PlaybackState.STATE_PLAYING; 422 } 423 mCallback.onPlaybackStateChanged(mPlaybackState, 1); 424 } 425 }