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