blob: e8325e1fd373535b95387dcdf2af186ea9613a26 [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.content.Context;
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.common.compat.TvViewCompat.TvInputCallbackCompat;
import com.android.tv.dvr.DvrTvView;
import com.android.tv.dvr.data.RecordedProgram;
import com.android.tv.ui.AppLayerTvView;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/** Player for recorded programs. */
public 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 static final long FORWARD_POSITION_MARGIN_MS = TimeUnit.SECONDS.toMillis(5);
private RecordedProgram mProgram;
private long mInitialSeekPositionMs;
private final DvrTvView 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;
/** Callback of DVR player. */
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() {}
/** Called when the playback is resumed to live position. */
public void onPlaybackResume() {}
}
/** Listener for aspect ratio changed events. */
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);
}
/** Listener for content blocked events. */
public interface OnContentBlockedListener {
/** Called when the Video's aspect ratio is changed. */
void onContentBlocked(TvContentRating rating);
}
/** Listener for tracks availability changed events */
public interface OnTracksAvailabilityChangedListener {
/** Called when the Video's subtitle or audio tracks are changed. */
void onTracksAvailabilityChanged(boolean hasClosedCaption, boolean hasMultiAudio);
}
/** Listener for track selected events */
public interface OnTrackSelectedListener {
/** Called when certain subtitle or audio track is selected. */
void onTrackSelected(String selectedTrackId);
}
/** Constructor of DvrPlayer. */
public DvrPlayer(AppLayerTvView tvView, Context context) {
mTvView = new DvrTvView(context, tvView, this);
mTvView.setCaptionEnabled(true);
mPlaybackParams.setSpeed(1.0f);
setTvViewCallbacks();
setCallback(null);
mTvView.init();
}
/**
* 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 DVR tv view. */
public DvrTvView getView() {
return mTvView;
}
/** 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() {
List<TvTrackInfo> tracks = mTvView.getTracks(TvTrackInfo.TYPE_AUDIO);
return tracks == null ? new ArrayList<>() : new ArrayList<>(tracks);
}
/** 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;
}
public void release() {
mTvView.release();
}
/**
* 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;
long bufferedTimeMs =
System.currentTimeMillis()
- mProgram.getStartTimeUtcMillis()
- FORWARD_POSITION_MARGIN_MS;
if ((mPlaybackState == PlaybackState.STATE_REWINDING
&& timeMs <= REWIND_POSITION_MARGIN_MS)
|| (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING
&& timeMs > bufferedTimeMs)) {
play();
mCallback.onPlaybackResume();
} else {
mTimeShiftCurrentPositionMs = getRealSeekPosition(timeMs, 0);
mCallback.onPlaybackPositionChanged(mTimeShiftCurrentPositionMs);
if (timeMs >= mProgram.getDurationMillis()) {
pause();
mCallback.onPlaybackEnded();
}
}
}
});
mTvView.setCallback(
new TvInputCallbackCompat() {
@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);
}
}