Home | History | Annotate | Download | only in playback
      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 
     28 import com.android.tv.dvr.data.RecordedProgram;
     29 
     30 import java.util.ArrayList;
     31 import java.util.List;
     32 import java.util.concurrent.TimeUnit;
     33 
     34 class 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 }