blob: 780bfb2f94ea736c1882acf855da31a7d2b4db10 [file] [log] [blame]
/*
* 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.tv.TvContentRating;
import android.media.tv.TvInputManager;
import android.media.tv.TvTrackInfo;
import android.media.tv.TvView;
import android.media.session.PlaybackState;
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);
}
}