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 }