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