• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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          */
onPlaybackPositionChanged(long positionMs)78         public void onPlaybackPositionChanged(long positionMs) { }
79         /**
80          * Called when the playback state or the playback speed is changed.
81          */
onPlaybackStateChanged(int playbackState, int playbackSpeed)82         public void onPlaybackStateChanged(int playbackState, int playbackSpeed) { }
83         /**
84          * Called when the playback toward the end.
85          */
onPlaybackEnded()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          */
onAspectRatioChanged(float videoAspectRatio)96         void onAspectRatioChanged(float videoAspectRatio);
97     }
98 
99     public interface OnContentBlockedListener {
100         /**
101          * Called when the Video's aspect ratio is changed.
102          */
onContentBlocked(TvContentRating rating)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          */
onTracksAvailabilityChanged(boolean hasClosedCaption, boolean hasMultiAudio)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          */
onTrackSelected(String selectedTrackId)117         void onTrackSelected(String selectedTrackId);
118     }
119 
DvrPlayer(TvView tvView)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      */
prepare(boolean doPlay)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      */
play()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      */
pause()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      */
fastForward(int speed)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      */
rewind(int speed)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      */
seekTo(long positionMs)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      */
reset()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      */
setCallback(DvrPlayerCallback callback)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      */
setOnAspectRatioChangedListener(OnAspectRatioChangedListener listener)286     public void setOnAspectRatioChangedListener(OnAspectRatioChangedListener listener) {
287         mOnAspectRatioChangedListener = listener;
288     }
289 
290     /**
291      * Sets the listener to content blocking.
292      */
setOnContentBlockedListener(OnContentBlockedListener listener)293     public void setOnContentBlockedListener(OnContentBlockedListener listener) {
294         mOnContentBlockedListener = listener;
295     }
296 
297     /**
298      * Sets the listener to tracks changing.
299      */
setOnTracksAvailabilityChangedListener( OnTracksAvailabilityChangedListener listener)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      */
setOnTrackSelectedListener(int trackType, OnTrackSelectedListener listener)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      */
getOnTrackSelectedListener(int trackType)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      */
setProgram(RecordedProgram program, long initialSeekPositionMs)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      */
getProgram()348     public RecordedProgram getProgram() {
349         return mProgram;
350     }
351 
352     /**
353      * Returns the currrent playback posistion in msecs.
354      */
getPlaybackPosition()355     public long getPlaybackPosition() {
356         return mTimeShiftCurrentPositionMs;
357     }
358 
359     /**
360      * Returns the playback speed currently used.
361      */
getPlaybackSpeed()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      */
getPlaybackState()369     public int getPlaybackState() {
370         return mPlaybackState;
371     }
372 
373     /**
374      * Returns the subtitle tracks of the current playback.
375      */
getSubtitleTracks()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      */
getAudioTracks()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      */
getSelectedTrackId(int trackType)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      */
isPlaybackPrepared()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      */
selectTrack(int trackType, TvTrackInfo selectedTrack)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 
setSelectedTrackId(int trackType, String trackId)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 
setPlaybackSpeed(int speed)442     private void setPlaybackSpeed(int speed) {
443         mPlaybackParams.setSpeed(speed);
444         mTvView.timeShiftSetPlaybackParams(mPlaybackParams);
445     }
446 
getRealSeekPosition(long seekPositionMs, long endMarginMs)447     private long getRealSeekPosition(long seekPositionMs, long endMarginMs) {
448         return Math.max(0, Math.min(seekPositionMs, mProgram.getDurationMillis() - endMarginMs));
449     }
450 
setTvViewCallbacks()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 
resumeToWatchedPositionIfNeeded()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 }