/* * 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; 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.util.Log; import java.util.List; import java.util.concurrent.TimeUnit; 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 RecordedProgram mProgram; private long mInitialSeekPositionMs; private final TvView mTvView; private DvrPlayerCallback mCallback; private AspectRatioChangedListener mAspectRatioChangedListener; private ContentBlockedListener mContentBlockedListener; private float mAspectRatio = Float.NaN; private int mPlaybackState = PlaybackState.STATE_NONE; private long mTimeShiftCurrentPositionMs; private boolean mPauseOnPrepared; 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 AspectRatioChangedListener { /** * Called when the Video's aspect ratio is changed. */ void onAspectRatioChanged(float videoAspectRatio); } public interface ContentBlockedListener { /** * Called when the Video's aspect ratio is changed. */ void onContentBlocked(TvContentRating rating); } public DvrPlayer(TvView tvView) { mTvView = tvView; 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; } /** * Sets callbacks for playback. */ public void setCallback(DvrPlayerCallback callback) { if (callback != null) { mCallback = callback; } else { mCallback = mEmptyCallback; } } /** * Sets listener to aspect ratio changing. */ public void setAspectRatioChangedListener(AspectRatioChangedListener listener) { mAspectRatioChangedListener = listener; } /** * Sets listener to content blocking. */ public void setContentBlockedListener(ContentBlockedListener listener) { mContentBlockedListener = listener; } /** * 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 if playback of the recorded program is started. */ public boolean isPlaybackPrepared() { return mPlaybackState != PlaybackState.STATE_NONE && mPlaybackState != PlaybackState.STATE_CONNECTING; } 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; } } @Override public void onTrackSelected(String inputId, int type, String trackId) { if (trackId == null || type != TvTrackInfo.TYPE_VIDEO || mAspectRatioChangedListener == null) { return; } List trackInfos = mTvView.getTracks(TvTrackInfo.TYPE_VIDEO); if (trackInfos != null) { for (TvTrackInfo trackInfo : trackInfos) { if (trackInfo.getId().equals(trackId)) { float videoAspectRatio = trackInfo.getVideoPixelAspectRatio() * trackInfo.getVideoWidth() / trackInfo.getVideoHeight(); if (DEBUG) Log.d(TAG, "Aspect Ratio: " + videoAspectRatio); if (!Float.isNaN(videoAspectRatio) && mAspectRatio != videoAspectRatio) { mAspectRatioChangedListener .onAspectRatioChanged(videoAspectRatio); mAspectRatio = videoAspectRatio; return; } } } } } @Override public void onContentBlocked(String inputId, TvContentRating rating) { if (mContentBlockedListener != null) { mContentBlockedListener.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); } }