aboutsummaryrefslogtreecommitdiff
path: root/src/com/android/tv/dvr/ui/playback/DvrPlayer.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/tv/dvr/ui/playback/DvrPlayer.java')
-rw-r--r--src/com/android/tv/dvr/ui/playback/DvrPlayer.java583
1 files changed, 583 insertions, 0 deletions
diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlayer.java b/src/com/android/tv/dvr/ui/playback/DvrPlayer.java
new file mode 100644
index 00000000..7226c666
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlayer.java
@@ -0,0 +1,583 @@
+/*
+ * 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);
+ }
+} \ No newline at end of file