Home | History | Annotate | Download | only in dvr
      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          */
     67         public void onPlaybackPositionChanged(long positionMs) { }
     68         /**
     69          * Called when the playback state or the playback speed is changed.
     70          */
     71         public void onPlaybackStateChanged(int playbackState, int playbackSpeed) { }
     72         /**
     73          * Called when the playback toward the end.
     74          */
     75         public void onPlaybackEnded() { }
     76     }
     77 
     78     public interface AspectRatioChangedListener {
     79         /**
     80          * Called when the Video's aspect ratio is changed.
     81          */
     82         void onAspectRatioChanged(float videoAspectRatio);
     83     }
     84 
     85     public interface ContentBlockedListener {
     86         /**
     87          * Called when the Video's aspect ratio is changed.
     88          */
     89         void onContentBlocked(TvContentRating rating);
     90     }
     91 
     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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    255     public void setAspectRatioChangedListener(AspectRatioChangedListener listener) {
    256         mAspectRatioChangedListener = listener;
    257     }
    258 
    259     /**
    260      * Sets listener to content blocking.
    261      */
    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      */
    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      */
    283     public RecordedProgram getProgram() {
    284         return mProgram;
    285     }
    286 
    287     /**
    288      * Returns the currrent playback posistion in msecs.
    289      */
    290     public long getPlaybackPosition() {
    291         return mTimeShiftCurrentPositionMs;
    292     }
    293 
    294     /**
    295      * Returns the playback speed currently used.
    296      */
    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      */
    304     public int getPlaybackState() {
    305         return mPlaybackState;
    306     }
    307 
    308     /**
    309      * Returns if playback of the recorded program is started.
    310      */
    311     public boolean isPlaybackPrepared() {
    312         return mPlaybackState != PlaybackState.STATE_NONE
    313                 && mPlaybackState != PlaybackState.STATE_CONNECTING;
    314     }
    315 
    316     private void setPlaybackSpeed(int speed) {
    317         mPlaybackParams.setSpeed(speed);
    318         mTvView.timeShiftSetPlaybackParams(mPlaybackParams);
    319     }
    320 
    321     private long getRealSeekPosition(long seekPositionMs, long endMarginMs) {
    322         return Math.max(0, Math.min(seekPositionMs, mProgram.getDurationMillis() - endMarginMs));
    323     }
    324 
    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 
    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 }