| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License |
| */ |
| |
| package com.android.tv.dvr.ui.playback; |
| |
| import android.media.PlaybackParams; |
| import android.media.session.PlaybackState; |
| import android.media.tv.TvContentRating; |
| import android.media.tv.TvInputManager; |
| import android.media.tv.TvTrackInfo; |
| import android.media.tv.TvView; |
| import android.text.TextUtils; |
| import android.util.Log; |
| |
| import com.android.tv.dvr.data.RecordedProgram; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.concurrent.TimeUnit; |
| |
| class DvrPlayer { |
| private static final String TAG = "DvrPlayer"; |
| private static final boolean DEBUG = false; |
| |
| /** |
| * The max rewinding speed supported by DVR player. |
| */ |
| public static final int MAX_REWIND_SPEED = 256; |
| /** |
| * The max fast-forwarding speed supported by DVR player. |
| */ |
| public static final int MAX_FAST_FORWARD_SPEED = 256; |
| |
| private static final long SEEK_POSITION_MARGIN_MS = TimeUnit.SECONDS.toMillis(2); |
| private static final long REWIND_POSITION_MARGIN_MS = 32; // Workaround value. b/29994826 |
| |
| private RecordedProgram mProgram; |
| private long mInitialSeekPositionMs; |
| private final TvView mTvView; |
| private DvrPlayerCallback mCallback; |
| private OnAspectRatioChangedListener mOnAspectRatioChangedListener; |
| private OnContentBlockedListener mOnContentBlockedListener; |
| private OnTracksAvailabilityChangedListener mOnTracksAvailabilityChangedListener; |
| private OnTrackSelectedListener mOnAudioTrackSelectedListener; |
| private OnTrackSelectedListener mOnSubtitleTrackSelectedListener; |
| private String mSelectedAudioTrackId; |
| private String mSelectedSubtitleTrackId; |
| private float mAspectRatio = Float.NaN; |
| private int mPlaybackState = PlaybackState.STATE_NONE; |
| private long mTimeShiftCurrentPositionMs; |
| private boolean mPauseOnPrepared; |
| private boolean mHasClosedCaption; |
| private boolean mHasMultiAudio; |
| private final PlaybackParams mPlaybackParams = new PlaybackParams(); |
| private final DvrPlayerCallback mEmptyCallback = new DvrPlayerCallback(); |
| private long mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; |
| private boolean mTimeShiftPlayAvailable; |
| |
| public static class DvrPlayerCallback { |
| /** |
| * Called when the playback position is changed. The normal updating frequency is |
| * around 1 sec., which is restricted to the implementation of |
| * {@link android.media.tv.TvInputService}. |
| */ |
| public void onPlaybackPositionChanged(long positionMs) { } |
| /** |
| * Called when the playback state or the playback speed is changed. |
| */ |
| public void onPlaybackStateChanged(int playbackState, int playbackSpeed) { } |
| /** |
| * Called when the playback toward the end. |
| */ |
| public void onPlaybackEnded() { } |
| } |
| |
| public interface OnAspectRatioChangedListener { |
| /** |
| * Called when the Video's aspect ratio is changed. |
| * |
| * @param videoAspectRatio The aspect ratio of video. 0 stands for unknown ratios. |
| * Listeners should handle it carefully. |
| */ |
| void onAspectRatioChanged(float videoAspectRatio); |
| } |
| |
| public interface OnContentBlockedListener { |
| /** |
| * Called when the Video's aspect ratio is changed. |
| */ |
| void onContentBlocked(TvContentRating rating); |
| } |
| |
| public interface OnTracksAvailabilityChangedListener { |
| /** |
| * Called when the Video's subtitle or audio tracks are changed. |
| */ |
| void onTracksAvailabilityChanged(boolean hasClosedCaption, boolean hasMultiAudio); |
| } |
| |
| public interface OnTrackSelectedListener { |
| /** |
| * Called when certain subtitle or audio track is selected. |
| */ |
| void onTrackSelected(String selectedTrackId); |
| } |
| |
| public DvrPlayer(TvView tvView) { |
| mTvView = tvView; |
| mTvView.setCaptionEnabled(true); |
| mPlaybackParams.setSpeed(1.0f); |
| setTvViewCallbacks(); |
| setCallback(null); |
| } |
| |
| /** |
| * Prepares playback. |
| * |
| * @param doPlay indicates DVR player do or do not start playback after media is prepared. |
| */ |
| public void prepare(boolean doPlay) throws IllegalStateException { |
| if (DEBUG) Log.d(TAG, "prepare()"); |
| if (mProgram == null) { |
| throw new IllegalStateException("Recorded program not set"); |
| } else if (mPlaybackState != PlaybackState.STATE_NONE) { |
| throw new IllegalStateException("Playback is already prepared"); |
| } |
| mTvView.timeShiftPlay(mProgram.getInputId(), mProgram.getUri()); |
| mPlaybackState = PlaybackState.STATE_CONNECTING; |
| mPauseOnPrepared = !doPlay; |
| mCallback.onPlaybackStateChanged(mPlaybackState, 1); |
| } |
| |
| /** |
| * Resumes playback. |
| */ |
| public void play() throws IllegalStateException { |
| if (DEBUG) Log.d(TAG, "play()"); |
| if (!isPlaybackPrepared()) { |
| throw new IllegalStateException("Recorded program not set or video not ready yet"); |
| } |
| switch (mPlaybackState) { |
| case PlaybackState.STATE_FAST_FORWARDING: |
| case PlaybackState.STATE_REWINDING: |
| setPlaybackSpeed(1); |
| break; |
| default: |
| mTvView.timeShiftResume(); |
| } |
| mPlaybackState = PlaybackState.STATE_PLAYING; |
| mCallback.onPlaybackStateChanged(mPlaybackState, 1); |
| } |
| |
| /** |
| * Pauses playback. |
| */ |
| public void pause() throws IllegalStateException { |
| if (DEBUG) Log.d(TAG, "pause()"); |
| if (!isPlaybackPrepared()) { |
| throw new IllegalStateException("Recorded program not set or playback not started yet"); |
| } |
| switch (mPlaybackState) { |
| case PlaybackState.STATE_FAST_FORWARDING: |
| case PlaybackState.STATE_REWINDING: |
| setPlaybackSpeed(1); |
| // falls through |
| case PlaybackState.STATE_PLAYING: |
| mTvView.timeShiftPause(); |
| mPlaybackState = PlaybackState.STATE_PAUSED; |
| break; |
| default: |
| break; |
| } |
| mCallback.onPlaybackStateChanged(mPlaybackState, 1); |
| } |
| |
| /** |
| * Fast-forwards playback with the given speed. If the given speed is larger than |
| * {@value #MAX_FAST_FORWARD_SPEED}, uses {@value #MAX_FAST_FORWARD_SPEED}. |
| */ |
| public void fastForward(int speed) throws IllegalStateException { |
| if (DEBUG) Log.d(TAG, "fastForward()"); |
| if (!isPlaybackPrepared()) { |
| throw new IllegalStateException("Recorded program not set or playback not started yet"); |
| } |
| if (speed <= 0) { |
| throw new IllegalArgumentException("Speed cannot be negative or 0"); |
| } |
| if (mTimeShiftCurrentPositionMs >= mProgram.getDurationMillis() - SEEK_POSITION_MARGIN_MS) { |
| return; |
| } |
| speed = Math.min(speed, MAX_FAST_FORWARD_SPEED); |
| if (DEBUG) Log.d(TAG, "Let's play with speed: " + speed); |
| setPlaybackSpeed(speed); |
| mPlaybackState = PlaybackState.STATE_FAST_FORWARDING; |
| mCallback.onPlaybackStateChanged(mPlaybackState, speed); |
| } |
| |
| /** |
| * Rewinds playback with the given speed. If the given speed is larger than |
| * {@value #MAX_REWIND_SPEED}, uses {@value #MAX_REWIND_SPEED}. |
| */ |
| public void rewind(int speed) throws IllegalStateException { |
| if (DEBUG) Log.d(TAG, "rewind()"); |
| if (!isPlaybackPrepared()) { |
| throw new IllegalStateException("Recorded program not set or playback not started yet"); |
| } |
| if (speed <= 0) { |
| throw new IllegalArgumentException("Speed cannot be negative or 0"); |
| } |
| if (mTimeShiftCurrentPositionMs <= REWIND_POSITION_MARGIN_MS) { |
| return; |
| } |
| speed = Math.min(speed, MAX_REWIND_SPEED); |
| if (DEBUG) Log.d(TAG, "Let's play with speed: " + speed); |
| setPlaybackSpeed(-speed); |
| mPlaybackState = PlaybackState.STATE_REWINDING; |
| mCallback.onPlaybackStateChanged(mPlaybackState, speed); |
| } |
| |
| /** |
| * Seeks playback to the specified position. |
| */ |
| public void seekTo(long positionMs) throws IllegalStateException { |
| if (DEBUG) Log.d(TAG, "seekTo()"); |
| if (!isPlaybackPrepared()) { |
| throw new IllegalStateException("Recorded program not set or playback not started yet"); |
| } |
| if (mProgram == null || mPlaybackState == PlaybackState.STATE_NONE) { |
| return; |
| } |
| positionMs = getRealSeekPosition(positionMs, SEEK_POSITION_MARGIN_MS); |
| if (DEBUG) Log.d(TAG, "Now: " + getPlaybackPosition() + ", shift to: " + positionMs); |
| mTvView.timeShiftSeekTo(positionMs + mStartPositionMs); |
| if (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING || |
| mPlaybackState == PlaybackState.STATE_REWINDING) { |
| mPlaybackState = PlaybackState.STATE_PLAYING; |
| mTvView.timeShiftResume(); |
| mCallback.onPlaybackStateChanged(mPlaybackState, 1); |
| } |
| } |
| |
| /** |
| * Resets playback. |
| */ |
| public void reset() { |
| if (DEBUG) Log.d(TAG, "reset()"); |
| mCallback.onPlaybackStateChanged(PlaybackState.STATE_NONE, 1); |
| mPlaybackState = PlaybackState.STATE_NONE; |
| mTvView.reset(); |
| mTimeShiftPlayAvailable = false; |
| mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; |
| mTimeShiftCurrentPositionMs = 0; |
| mPlaybackParams.setSpeed(1.0f); |
| mProgram = null; |
| mSelectedAudioTrackId = null; |
| mSelectedSubtitleTrackId = null; |
| } |
| |
| /** |
| * Sets callbacks for playback. |
| */ |
| public void setCallback(DvrPlayerCallback callback) { |
| if (callback != null) { |
| mCallback = callback; |
| } else { |
| mCallback = mEmptyCallback; |
| } |
| } |
| |
| /** |
| * Sets the listener to aspect ratio changing. |
| */ |
| public void setOnAspectRatioChangedListener(OnAspectRatioChangedListener listener) { |
| mOnAspectRatioChangedListener = listener; |
| } |
| |
| /** |
| * Sets the listener to content blocking. |
| */ |
| public void setOnContentBlockedListener(OnContentBlockedListener listener) { |
| mOnContentBlockedListener = listener; |
| } |
| |
| /** |
| * Sets the listener to tracks changing. |
| */ |
| public void setOnTracksAvailabilityChangedListener( |
| OnTracksAvailabilityChangedListener listener) { |
| mOnTracksAvailabilityChangedListener = listener; |
| } |
| |
| /** |
| * Sets the listener to tracks of the given type being selected. |
| * |
| * @param trackType should be either {@link TvTrackInfo#TYPE_AUDIO} |
| * or {@link TvTrackInfo#TYPE_SUBTITLE}. |
| */ |
| public void setOnTrackSelectedListener(int trackType, OnTrackSelectedListener listener) { |
| if (trackType == TvTrackInfo.TYPE_AUDIO) { |
| mOnAudioTrackSelectedListener = listener; |
| } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { |
| mOnSubtitleTrackSelectedListener = listener; |
| } |
| } |
| |
| /** |
| * Gets the listener to tracks of the given type being selected. |
| */ |
| public OnTrackSelectedListener getOnTrackSelectedListener(int trackType) { |
| if (trackType == TvTrackInfo.TYPE_AUDIO) { |
| return mOnAudioTrackSelectedListener; |
| } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { |
| return mOnSubtitleTrackSelectedListener; |
| } |
| return null; |
| } |
| |
| /** |
| * Sets recorded programs for playback. If the player is playing another program, stops it. |
| */ |
| public void setProgram(RecordedProgram program, long initialSeekPositionMs) { |
| if (mProgram != null && mProgram.equals(program)) { |
| return; |
| } |
| if (mPlaybackState != PlaybackState.STATE_NONE) { |
| reset(); |
| } |
| mInitialSeekPositionMs = initialSeekPositionMs; |
| mProgram = program; |
| } |
| |
| /** |
| * Returns the recorded program now playing. |
| */ |
| public RecordedProgram getProgram() { |
| return mProgram; |
| } |
| |
| /** |
| * Returns the currrent playback posistion in msecs. |
| */ |
| public long getPlaybackPosition() { |
| return mTimeShiftCurrentPositionMs; |
| } |
| |
| /** |
| * Returns the playback speed currently used. |
| */ |
| public int getPlaybackSpeed() { |
| return (int) mPlaybackParams.getSpeed(); |
| } |
| |
| /** |
| * Returns the playback state defined in {@link android.media.session.PlaybackState}. |
| */ |
| public int getPlaybackState() { |
| return mPlaybackState; |
| } |
| |
| /** |
| * Returns the subtitle tracks of the current playback. |
| */ |
| public ArrayList<TvTrackInfo> getSubtitleTracks() { |
| return new ArrayList<>(mTvView.getTracks(TvTrackInfo.TYPE_SUBTITLE)); |
| } |
| |
| /** |
| * Returns the audio tracks of the current playback. |
| */ |
| public ArrayList<TvTrackInfo> getAudioTracks() { |
| return new ArrayList<>(mTvView.getTracks(TvTrackInfo.TYPE_AUDIO)); |
| } |
| |
| /** |
| * Returns the ID of the selected track of the given type. |
| */ |
| public String getSelectedTrackId(int trackType) { |
| if (trackType == TvTrackInfo.TYPE_AUDIO) { |
| return mSelectedAudioTrackId; |
| } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { |
| return mSelectedSubtitleTrackId; |
| } |
| return null; |
| } |
| |
| /** |
| * Returns if playback of the recorded program is started. |
| */ |
| public boolean isPlaybackPrepared() { |
| return mPlaybackState != PlaybackState.STATE_NONE |
| && mPlaybackState != PlaybackState.STATE_CONNECTING; |
| } |
| |
| /** |
| * Selects the given track. |
| * |
| * @return ID of the selected track. |
| */ |
| String selectTrack(int trackType, TvTrackInfo selectedTrack) { |
| String oldSelectedTrackId = getSelectedTrackId(trackType); |
| String newSelectedTrackId = selectedTrack == null ? null : selectedTrack.getId(); |
| if (!TextUtils.equals(oldSelectedTrackId, newSelectedTrackId)) { |
| if (selectedTrack == null) { |
| mTvView.selectTrack(trackType, null); |
| return null; |
| } else { |
| List<TvTrackInfo> tracks = mTvView.getTracks(trackType); |
| if (tracks != null && tracks.contains(selectedTrack)) { |
| mTvView.selectTrack(trackType, newSelectedTrackId); |
| return newSelectedTrackId; |
| } else if (trackType == TvTrackInfo.TYPE_SUBTITLE && oldSelectedTrackId != null) { |
| // Track not found, disabled closed caption. |
| mTvView.selectTrack(trackType, null); |
| return null; |
| } |
| } |
| } |
| return oldSelectedTrackId; |
| } |
| |
| private void setSelectedTrackId(int trackType, String trackId) { |
| if (trackType == TvTrackInfo.TYPE_AUDIO) { |
| mSelectedAudioTrackId = trackId; |
| } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { |
| mSelectedSubtitleTrackId = trackId; |
| } |
| } |
| |
| private void setPlaybackSpeed(int speed) { |
| mPlaybackParams.setSpeed(speed); |
| mTvView.timeShiftSetPlaybackParams(mPlaybackParams); |
| } |
| |
| private long getRealSeekPosition(long seekPositionMs, long endMarginMs) { |
| return Math.max(0, Math.min(seekPositionMs, mProgram.getDurationMillis() - endMarginMs)); |
| } |
| |
| private void setTvViewCallbacks() { |
| mTvView.setTimeShiftPositionCallback(new TvView.TimeShiftPositionCallback() { |
| @Override |
| public void onTimeShiftStartPositionChanged(String inputId, long timeMs) { |
| if (DEBUG) Log.d(TAG, "onTimeShiftStartPositionChanged:" + timeMs); |
| mStartPositionMs = timeMs; |
| if (mTimeShiftPlayAvailable) { |
| resumeToWatchedPositionIfNeeded(); |
| } |
| } |
| |
| @Override |
| public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) { |
| if (DEBUG) Log.d(TAG, "onTimeShiftCurrentPositionChanged: " + timeMs); |
| if (!mTimeShiftPlayAvailable) { |
| // Workaround of b/31436263 |
| return; |
| } |
| // Workaround of b/32211561, TIF won't report start position when TIS report |
| // its start position as 0. In that case, we have to do the prework of playback |
| // on the first time we get current position, and the start position should be 0 |
| // at that time. |
| if (mStartPositionMs == TvInputManager.TIME_SHIFT_INVALID_TIME) { |
| mStartPositionMs = 0; |
| resumeToWatchedPositionIfNeeded(); |
| } |
| timeMs -= mStartPositionMs; |
| if (mPlaybackState == PlaybackState.STATE_REWINDING |
| && timeMs <= REWIND_POSITION_MARGIN_MS) { |
| play(); |
| } else { |
| mTimeShiftCurrentPositionMs = getRealSeekPosition(timeMs, 0); |
| mCallback.onPlaybackPositionChanged(mTimeShiftCurrentPositionMs); |
| if (timeMs >= mProgram.getDurationMillis()) { |
| pause(); |
| mCallback.onPlaybackEnded(); |
| } |
| } |
| } |
| }); |
| mTvView.setCallback(new TvView.TvInputCallback() { |
| @Override |
| public void onTimeShiftStatusChanged(String inputId, int status) { |
| if (DEBUG) Log.d(TAG, "onTimeShiftStatusChanged:" + status); |
| if (status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE |
| && mPlaybackState == PlaybackState.STATE_CONNECTING) { |
| mTimeShiftPlayAvailable = true; |
| if (mStartPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { |
| // onTimeShiftStatusChanged is sometimes called after |
| // onTimeShiftStartPositionChanged is called. In this case, |
| // resumeToWatchedPositionIfNeeded needs to be called here. |
| resumeToWatchedPositionIfNeeded(); |
| } |
| } |
| } |
| |
| @Override |
| public void onTracksChanged(String inputId, List<TvTrackInfo> tracks) { |
| boolean hasClosedCaption = |
| !mTvView.getTracks(TvTrackInfo.TYPE_SUBTITLE).isEmpty(); |
| boolean hasMultiAudio = mTvView.getTracks(TvTrackInfo.TYPE_AUDIO).size() > 1; |
| if ((hasClosedCaption != mHasClosedCaption || hasMultiAudio != mHasMultiAudio) |
| && mOnTracksAvailabilityChangedListener != null) { |
| mOnTracksAvailabilityChangedListener |
| .onTracksAvailabilityChanged(hasClosedCaption, hasMultiAudio); |
| } |
| mHasClosedCaption = hasClosedCaption; |
| mHasMultiAudio = hasMultiAudio; |
| } |
| |
| @Override |
| public void onTrackSelected(String inputId, int type, String trackId) { |
| if (type == TvTrackInfo.TYPE_AUDIO || type == TvTrackInfo.TYPE_SUBTITLE) { |
| setSelectedTrackId(type, trackId); |
| OnTrackSelectedListener listener = getOnTrackSelectedListener(type); |
| if (listener != null) { |
| listener.onTrackSelected(trackId); |
| } |
| } else if (type == TvTrackInfo.TYPE_VIDEO && trackId != null |
| && mOnAspectRatioChangedListener != null) { |
| List<TvTrackInfo> trackInfos = mTvView.getTracks(TvTrackInfo.TYPE_VIDEO); |
| if (trackInfos != null) { |
| for (TvTrackInfo trackInfo : trackInfos) { |
| if (trackInfo.getId().equals(trackId)) { |
| float videoAspectRatio; |
| int videoWidth = trackInfo.getVideoWidth(); |
| int videoHeight = trackInfo.getVideoHeight(); |
| if (videoWidth > 0 && videoHeight > 0) { |
| videoAspectRatio = trackInfo.getVideoPixelAspectRatio() |
| * trackInfo.getVideoWidth() / trackInfo.getVideoHeight(); |
| } else { |
| // Aspect ratio is unknown. Pass the message to listeners. |
| videoAspectRatio = 0; |
| } |
| if (DEBUG) Log.d(TAG, "Aspect Ratio: " + videoAspectRatio); |
| if (mAspectRatio != videoAspectRatio || videoAspectRatio == 0) { |
| mOnAspectRatioChangedListener |
| .onAspectRatioChanged(videoAspectRatio); |
| mAspectRatio = videoAspectRatio; |
| return; |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| @Override |
| public void onContentBlocked(String inputId, TvContentRating rating) { |
| if (mOnContentBlockedListener != null) { |
| mOnContentBlockedListener.onContentBlocked(rating); |
| } |
| } |
| }); |
| } |
| |
| private void resumeToWatchedPositionIfNeeded() { |
| if (mInitialSeekPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { |
| mTvView.timeShiftSeekTo(getRealSeekPosition(mInitialSeekPositionMs, |
| SEEK_POSITION_MARGIN_MS) + mStartPositionMs); |
| mInitialSeekPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; |
| } |
| if (mPauseOnPrepared) { |
| mTvView.timeShiftPause(); |
| mPlaybackState = PlaybackState.STATE_PAUSED; |
| mPauseOnPrepared = false; |
| } else { |
| mTvView.timeShiftResume(); |
| mPlaybackState = PlaybackState.STATE_PLAYING; |
| } |
| mCallback.onPlaybackStateChanged(mPlaybackState, 1); |
| } |
| } |