/* * 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 getSubtitleTracks() { return new ArrayList<>(mTvView.getTracks(TvTrackInfo.TYPE_SUBTITLE)); } /** * Returns the audio tracks of the current playback. */ public ArrayList 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 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 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 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); } }