aboutsummaryrefslogtreecommitdiff
path: root/usbtuner/src/com/android/usbtuner/tvinput/TvInputSessionImplInternal.java
diff options
context:
space:
mode:
Diffstat (limited to 'usbtuner/src/com/android/usbtuner/tvinput/TvInputSessionImplInternal.java')
-rw-r--r--usbtuner/src/com/android/usbtuner/tvinput/TvInputSessionImplInternal.java1468
1 files changed, 1468 insertions, 0 deletions
diff --git a/usbtuner/src/com/android/usbtuner/tvinput/TvInputSessionImplInternal.java b/usbtuner/src/com/android/usbtuner/tvinput/TvInputSessionImplInternal.java
new file mode 100644
index 00000000..c49d0833
--- /dev/null
+++ b/usbtuner/src/com/android/usbtuner/tvinput/TvInputSessionImplInternal.java
@@ -0,0 +1,1468 @@
+/*
+ * Copyright (C) 2015 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.usbtuner.tvinput;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.media.MediaDataSource;
+import android.media.MediaFormat;
+import android.media.PlaybackParams;
+import android.media.tv.TvContentRating;
+import android.media.tv.TvInputManager;
+import android.media.tv.TvTrackInfo;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.os.SystemClock;
+import android.text.Html;
+import android.util.Log;
+import android.util.Pair;
+import android.util.Size;
+import android.util.SparseArray;
+import android.view.Surface;
+import android.view.accessibility.CaptioningManager;
+
+import com.google.android.exoplayer.audio.AudioCapabilities;
+import com.android.tv.common.TvContentRatingCache;
+import com.android.usbtuner.FileDataSource;
+import com.android.usbtuner.InputStreamSource;
+import com.android.usbtuner.TunerHal;
+import com.android.usbtuner.UsbTunerDataSource;
+import com.android.usbtuner.data.Cea708Data;
+import com.android.usbtuner.data.Channel;
+import com.android.usbtuner.data.PsipData.EitItem;
+import com.android.usbtuner.data.PsipData.TvTracksInterface;
+import com.android.usbtuner.data.Track.AtscAudioTrack;
+import com.android.usbtuner.data.Track.AtscCaptionTrack;
+import com.android.usbtuner.data.TunerChannel;
+import com.android.usbtuner.exoplayer.CacheManager;
+import com.android.usbtuner.exoplayer.DvrStorageManager;
+import com.android.usbtuner.exoplayer.MpegTsPassthroughAc3RendererBuilder;
+import com.android.usbtuner.exoplayer.MpegTsPlayer;
+import com.android.usbtuner.util.IsoUtils;
+import com.android.usbtuner.util.StatusTextUtils;
+
+import junit.framework.Assert;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * {@link TvInputSessionImplInternal} implements a handler thread which processes TV input jobs
+ * such as handling {@link ExoPlayer}, managing a tuner device, trickplay, and so on.
+ */
+public class TvInputSessionImplInternal implements PlaybackCacheListener,
+ MpegTsPlayer.VideoEventListener, MpegTsPlayer.Listener, EventDetector.EventListener,
+ ChannelDataManager.ProgramInfoListener, Handler.Callback {
+ private static final String TAG = "TvInputSessionInternal";
+ private static final boolean DEBUG = false;
+ private static final boolean ENABLE_PROFILER = true;
+ private static final String PLAY_FROM_CHANNEL = "channel";
+ private static final String PLAY_FROM_RECORDING = "record";
+
+ // Public messages
+ public static final int MSG_SELECT_TRACK = 1;
+ public static final int MSG_SET_CAPTION_ENABLED = 2;
+ public static final int MSG_SET_SURFACE = 3;
+ public static final int MSG_SET_STREAM_VOLUME = 4;
+ public static final int MSG_TIMESHIFT_PAUSE = 5;
+ public static final int MSG_TIMESHIFT_RESUME = 6;
+ public static final int MSG_TIMESHIFT_SEEK_TO = 7;
+ public static final int MSG_TIMESHIFT_SET_PLAYBACKPARAMS = 8;
+ public static final int MSG_AUDIO_CAPABILITIES_CHANGED = 9;
+ public static final int MSG_UNBLOCKED_RATING = 10;
+
+ // Private messages
+ private static final int MSG_TUNE = 1000;
+ private static final int MSG_RELEASE = 1001;
+ private static final int MSG_RETRY_PLAYBACK = 1002;
+ private static final int MSG_START_PLAYBACK = 1003;
+ private static final int MSG_PLAYBACK_STATE_CHANGED = 1004;
+ private static final int MSG_PLAYBACK_ERROR = 1005;
+ private static final int MSG_PLAYBACK_VIDEO_SIZE_CHANGED = 1006;
+ private static final int MSG_AUDIO_UNPLAYABLE = 1007;
+ private static final int MSG_UPDATE_PROGRAM = 1008;
+ private static final int MSG_SCHEDULE_OF_PROGRAMS = 1009;
+ private static final int MSG_UPDATE_CHANNEL_INFO = 1010;
+ private static final int MSG_TRICKPLAY = 1011;
+ private static final int MSG_DRAWN_TO_SURFACE = 1012;
+ private static final int MSG_PARENTAL_CONTROLS = 1013;
+ private static final int MSG_RESCHEDULE_PROGRAMS = 1014;
+ private static final int MSG_CACHE_START_TIME_CHANGED = 1015;
+ private static final int MSG_CHECK_SIGNAL = 1016;
+ private static final int MSG_DISCOVER_CAPTION_SERVICE_NUMBER = 1017;
+ private static final int MSG_RECOVER_STOPPED_PLAYBACK = 1018;
+ private static final int MSG_CACHE_STATE_CHANGED = 1019;
+ private static final int MSG_PROGRAM_DATA_RESULT = 1020;
+ private static final int MSG_STOP_TUNE = 1021;
+
+ private static final int TS_PACKET_SIZE = 188;
+ private static final int CHECK_NO_SIGNAL_INITIAL_DELAY_MS = 4000;
+ private static final int CHECK_NO_SIGNAL_PERIOD_MS = 500;
+ private static final int RECOVER_STOPPED_PLAYBACK_PERIOD_MS = 2500;
+ private static final int PARENTAL_CONTROLS_INTERVAL_MS = 5000;
+ private static final int RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS = 4000;
+ private static final int RESCHEDULE_PROGRAMS_INTERVAL_MS = 10000;
+ private static final int RESCHEDULE_PROGRAMS_TOLERANCE_MS = 2000;
+ private static final int MAX_RETRY_COUNT = 2;
+
+ // Some examples of the track ids of the audio tracks, "a0", "a1", "a2".
+ // The number after prefix is being used for indicating a index of the given audio track.
+ private static final String AUDIO_TRACK_PREFIX = "a";
+
+ // Some examples of the tracks id of the caption tracks, "s1", "s2", "s3".
+ // The number after prefix is being used for indicating a index of a caption service number
+ // of the given caption track.
+ private static final String SUBTITLE_TRACK_PREFIX = "s";
+ private static final int TRACK_PREFIX_SIZE = 1;
+ private static final String VIDEO_TRACK_ID = "v";
+ private static final long CACHE_UNDERFLOW_BUFFER_MS = 5000;
+
+ // Actual interval would be divided by the speed.
+ private static final int TRICKPLAY_SEEK_INTERVAL_MS = 2000;
+ private static final int MIN_TRICKPLAY_SEEK_INTERVAL_MS = 500;
+
+ private final Context mContext;
+ private final ChannelDataManager mChannelDataManager;
+ private final TunerHal mTunerHal;
+ private UsbTunerDataSource mTunerSource;
+ private FileDataSource mFileSource;
+ private InputStreamSource mSource;
+ private Surface mSurface;
+ private int mPlayerGeneration;
+ private int mPreparingGeneration;
+ private int mEndedGeneration;
+ private volatile MpegTsPlayer mPlayer;
+ private volatile TunerChannel mChannel;
+ private String mRecordingId;
+ private volatile Long mRecordingDuration;
+ private final Handler mHandler;
+ private final HandlerThread mHandlerThread;
+ private int mRetryCount;
+ private float mVolume;
+ private final ArrayList<TvTrackInfo> mTvTracks;
+ private SparseArray<AtscAudioTrack> mAudioTrackMap;
+ private SparseArray<AtscCaptionTrack> mCaptionTrackMap;
+ private AtscCaptionTrack mCaptionTrack;
+ private boolean mCaptionEnabled;
+ private volatile long mRecordStartTimeMs;
+ private volatile long mCacheStartTimeMs;
+ private PlaybackParams mPlaybackParams = new PlaybackParams();
+ private boolean mPlayerStarted = false;
+ private boolean mReportedDrawnToSurface = false;
+ private boolean mReportedSignalAvailable = false;
+ private EitItem mProgram;
+ private List<EitItem> mPrograms;
+ private TvInputManager mTvInputManager;
+ private boolean mChannelBlocked;
+ private TvContentRating mUnblockedContentRating;
+ private long mLastPositionMs;
+ private AudioCapabilities mAudioCapabilities;
+ private final CountDownLatch mReleaseLatch = new CountDownLatch(1);
+ private long mLastLimitInBytes = 0L;
+ private long mLastPositionInBytes = 0L;
+ private final CacheManager mCacheManager;
+ private final TvContentRatingCache mTvContentRatingCache = TvContentRatingCache.getInstance();
+
+ public TvInputSessionImplInternal(Context context, ChannelDataManager channelDataManager,
+ CacheManager cacheManager) {
+ mContext = context;
+ mTunerHal = TunerHal.getInstance(context);
+ if (mTunerHal == null) {
+ throw new RuntimeException("Failed to open a DVB device");
+ }
+
+ // HandlerThread should be set up before it is registered as a listener in the all other
+ // components.
+ mHandlerThread = new HandlerThread(TAG);
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper(), this);
+ mChannelDataManager = channelDataManager;
+ mChannelDataManager.setListener(this);
+ mChannelDataManager.checkDataVersion(mContext);
+ mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE);
+ mTunerSource = new UsbTunerDataSource(mTunerHal, this);
+ mFileSource = new FileDataSource(this);
+ mVolume = 1.0f;
+ mTvTracks = new ArrayList<>();
+ mAudioTrackMap = new SparseArray<>();
+ mCaptionTrackMap = new SparseArray<>();
+ CaptioningManager captioningManager =
+ (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
+ mCaptionEnabled = captioningManager.isEnabled();
+ mPlaybackParams.setSpeed(1.0f);
+ mCacheManager = cacheManager;
+ }
+
+ // Public methods
+ public void tune(Uri channelUri) {
+ if (mSurface != null) { // To avoid removing MSG_SET_SURFACE
+ mHandler.removeCallbacksAndMessages(null);
+ }
+ sendMessage(MSG_TUNE, channelUri);
+ }
+
+ public void stopTune() {
+ mHandler.removeCallbacksAndMessages(null);
+ sendMessage(MSG_STOP_TUNE);
+ }
+
+ public TunerChannel getCurrentChannel() {
+ return mChannel;
+ }
+
+ public long getStartPosition() {
+ return mCacheStartTimeMs;
+ }
+
+
+ private String getRecordingPath() {
+ return mContext.getCacheDir().getAbsolutePath() + "/recording" + mRecordingId;
+ }
+
+ public Long getDurationForRecording() {
+ return mRecordingDuration;
+ }
+
+ private Long getDurationForRecording(String recordingId) {
+ try {
+ DvrStorageManager storageManager =
+ new DvrStorageManager(new File(getRecordingPath()), false);
+ Pair<String, MediaFormat> trackInfo = null;
+ try {
+ trackInfo = storageManager.readTrackInfoFile(false);
+ } catch (FileNotFoundException e) {
+ }
+ if (trackInfo == null) {
+ trackInfo = storageManager.readTrackInfoFile(true);
+ }
+ Long durationUs = trackInfo.second.getLong(MediaFormat.KEY_DURATION);
+ // we need duration by milli for trickplay notification.
+ return durationUs != null ? durationUs / 1000 : null;
+ } catch (IOException e) {
+ Log.e(TAG, "meta file for recording was not found: " + recordingId);
+ return null;
+ }
+ }
+
+ public long getCurrentPosition() {
+ // TODO: More precise time may be necessary.
+ MpegTsPlayer mpegTsPlayer = mPlayer;
+ long currentTime = mpegTsPlayer != null
+ ? mRecordStartTimeMs + mpegTsPlayer.getCurrentPosition() : mRecordStartTimeMs;
+ if (DEBUG) {
+ long systemCurrentTime = System.currentTimeMillis();
+ Log.d(TAG, "currentTime = " + currentTime
+ + " ; System.currentTimeMillis() = " + systemCurrentTime
+ + " ; diff = " + (currentTime - systemCurrentTime));
+ }
+ return currentTime;
+ }
+
+ public void sendMessage(int messageType) {
+ mHandler.sendEmptyMessage(messageType);
+ }
+
+ public void sendMessage(int messageType, Object object) {
+ mHandler.obtainMessage(messageType, object).sendToTarget();
+ }
+
+ public void sendMessage(int messageType, int arg1, int arg2, Object object) {
+ mHandler.obtainMessage(messageType, arg1, arg2, object).sendToTarget();
+ }
+
+ public void release() {
+ mHandler.removeCallbacksAndMessages(null);
+ mHandler.sendEmptyMessage(MSG_RELEASE);
+ try {
+ mReleaseLatch.await();
+ } catch (InterruptedException e) {
+ Log.e(TAG, "Couldn't wait for finish of MSG_RELEASE", e);
+ } finally {
+ mHandlerThread.quitSafely();
+ }
+ }
+
+ // MpegTsPlayer.Listener
+ @Override
+ public void onStateChanged(int generation, boolean playWhenReady, int playbackState) {
+ sendMessage(MSG_PLAYBACK_STATE_CHANGED, generation, playbackState, playWhenReady);
+ }
+
+ @Override
+ public void onError(int generation, Exception e) {
+ sendMessage(MSG_PLAYBACK_ERROR, generation, 0, e);
+ }
+
+ @Override
+ public void onVideoSizeChanged(int generation, int width, int height, float pixelWidthHeight) {
+ sendMessage(MSG_PLAYBACK_VIDEO_SIZE_CHANGED, generation, 0, new Size(width, height));
+ }
+
+ @Override
+ public void onDrawnToSurface(MpegTsPlayer player, Surface surface) {
+ sendMessage(MSG_DRAWN_TO_SURFACE, player);
+ }
+
+ @Override
+ public void onAudioUnplayable(int generation) {
+ sendMessage(MSG_AUDIO_UNPLAYABLE, generation);
+ }
+
+ // MpegTsPlayer.VideoEventListener
+ @Override
+ public void onEmitCaptionEvent(Cea708Data.CaptionEvent event) {
+ mInternalListener.sendUiMessage(TvInputSessionImpl.MSG_UI_PROCESS_CAPTION_TRACK, event);
+ }
+
+ @Override
+ public void onDiscoverCaptionServiceNumber(int serviceNumber) {
+ sendMessage(MSG_DISCOVER_CAPTION_SERVICE_NUMBER, serviceNumber);
+ }
+
+ // ChannelDataManager.ProgramInfoListener
+ @Override
+ public void onProgramsArrived(TunerChannel channel, List<EitItem> programs) {
+ sendMessage(MSG_SCHEDULE_OF_PROGRAMS, new Pair<>(channel, programs));
+ }
+
+ @Override
+ public void onChannelArrived(TunerChannel channel) {
+ sendMessage(MSG_UPDATE_CHANNEL_INFO, channel);
+ }
+
+ @Override
+ public void onRescanNeeded() {
+ mInternalListener.sendUiMessage(TvInputSessionImpl.MSG_UI_TOAST_RESCAN_NEEDED);
+ }
+
+ @Override
+ public void onRequestProgramsResponse(TunerChannel channel, List<EitItem> programs) {
+ sendMessage(MSG_PROGRAM_DATA_RESULT, new Pair<>(channel, programs));
+ }
+
+ // PlaybackCacheListener
+ @Override
+ public void onCacheStartTimeChanged(long startTimeMs) {
+ sendMessage(MSG_CACHE_START_TIME_CHANGED, startTimeMs);
+ }
+
+ @Override
+ public void onCacheStateChanged(boolean available) {
+ sendMessage(MSG_CACHE_STATE_CHANGED, available);
+ }
+
+ @Override
+ public void onDiskTooSlow() {
+ sendMessage(MSG_RETRY_PLAYBACK, mPlayer);
+ }
+
+ // EventDetector.EventListener
+ @Override
+ public void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) {
+ mChannelDataManager.notifyChannelDetected(channel, channelArrivedAtFirstTime);
+ }
+
+ @Override
+ public void onEventDetected(TunerChannel channel, List<EitItem> items) {
+ mChannelDataManager.notifyEventDetected(channel, items);
+ }
+
+ // InternalListener
+ public interface InternalListener {
+ void sendUiMessage(int message);
+ void sendUiMessage(int message, Object object);
+ void sendUiMessage(int message, int arg1, int arg2, Object object);
+ void notifyVideoAvailable();
+ void notifyVideoUnavailable(int reason);
+ void notifyTimeShiftStatusChanged(int status);
+ void notifyContentBlocked(TvContentRating tvContentRating);
+ void notifyContentAllowed();
+ void notifyTracksChanged(ArrayList<TvTrackInfo> tvTracks);
+ void notifyTrackSelected(int type, String trackId);
+ }
+
+ public void setInternalListener(TvInputSessionImpl internalListener) {
+ mInternalListener = internalListener;
+ }
+
+ private InternalListener mInternalListener = new InternalListener() {
+ @Override
+ public void sendUiMessage(int message) {
+ // do nothing.
+ }
+
+ @Override
+ public void sendUiMessage(int message, Object object) {
+ // do nothing.
+ }
+
+ @Override
+ public void sendUiMessage(int message, int arg1, int arg2, Object object) {
+ // do nothing.
+ }
+
+ @Override
+ public void notifyVideoAvailable() {
+ // do nothing.
+ }
+
+ @Override
+ public void notifyVideoUnavailable(int reason) {
+ // do nothing.
+ }
+
+ @Override
+ public void notifyTimeShiftStatusChanged(int status) {
+ // do nothing.
+ }
+
+ @Override
+ public void notifyContentBlocked(TvContentRating tvContentRating) {
+ // do nothing.
+ }
+
+ @Override
+ public void notifyContentAllowed() {
+ // do nothing.
+ }
+
+ @Override
+ public void notifyTracksChanged(ArrayList<TvTrackInfo> tvTracks) {
+ // do nothing.
+ }
+
+ @Override
+ public void notifyTrackSelected(int type, String trackId) {
+ // do nothing.
+ }
+ };
+
+ private long parseChannel(Uri uri) {
+ try {
+ List<String> paths = uri.getPathSegments();
+ if (paths.size() > 1 && paths.get(0).equals(PLAY_FROM_CHANNEL)) {
+ return ContentUris.parseId(uri);
+ }
+ } catch (UnsupportedOperationException | NumberFormatException e) {
+ }
+ return -1;
+ }
+
+ private String parseRecording(Uri uri) {
+ if (uri.getScheme().equals(PLAY_FROM_RECORDING)) {
+ return uri.getPath();
+ }
+ return null;
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_TUNE: {
+ if (DEBUG) Log.d(TAG, "MSG_TUNE");
+
+ // When sequential tuning messages arrived, it skips middle tuning messages in order
+ // to change to the last requested channel quickly.
+ if (mHandler.hasMessages(MSG_TUNE)) {
+ return true;
+ }
+ Uri channelUri = (Uri) msg.obj;
+ String recording = null;
+ long channelId = parseChannel(channelUri);
+ TunerChannel channel = (channelId == -1) ? null
+ : mChannelDataManager.getChannel(channelId);
+ if (channelId == -1) {
+ recording = parseRecording(channelUri);
+ }
+ if (channel == null && recording == null) {
+ Log.w(TAG, "onTune() is failed. Can't find channel for " + channelUri);
+ stopTune();
+ mInternalListener.notifyVideoUnavailable(
+ TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
+ return true;
+ }
+ mHandler.removeCallbacksAndMessages(null);
+ if (channel != null) {
+ mChannelDataManager.requestProgramsData(channel);
+ }
+ prepareTune(channel, recording);
+ mInternalListener.notifyContentAllowed();
+ resetPlayback();
+ resetTvTracks();
+ mHandler.sendEmptyMessageDelayed(MSG_RESCHEDULE_PROGRAMS,
+ RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS);
+ mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL,
+ CHECK_NO_SIGNAL_INITIAL_DELAY_MS);
+ return true;
+ }
+ case MSG_STOP_TUNE: {
+ if (DEBUG) {
+ Log.d(TAG, "MSG_STOP_TUNE");
+ }
+ mChannel = null;
+ stopPlayback();
+ stopCaptionTrack();
+ resetTvTracks();
+ mTunerHal.stopTune();
+ mSource = null;
+ mInternalListener.notifyVideoUnavailable(
+ TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
+ return true;
+ }
+ case MSG_RELEASE: {
+ if (DEBUG) {
+ Log.d(TAG, "MSG_RELEASE");
+ }
+ mHandler.removeCallbacksAndMessages(null);
+ stopPlayback();
+ stopCaptionTrack();
+ try {
+ mTunerHal.close();
+ } catch (Exception ex) {
+ Log.e(TAG, "Error on closing tuner HAL.", ex);
+ }
+ mSource = null;
+ mReleaseLatch.countDown();
+ return true;
+ }
+ case MSG_RETRY_PLAYBACK: {
+ if (mPlayer == msg.obj) {
+ mHandler.removeMessages(MSG_RETRY_PLAYBACK);
+ mRetryCount++;
+ if (DEBUG) {
+ Log.d(TAG, "MSG_RETRY_PLAYBACK " + mRetryCount);
+ }
+ if (mRetryCount <= MAX_RETRY_COUNT) {
+ resetPlayback();
+ } else {
+ // When it reaches this point, it may be due to an error that occurred in
+ // the tuner device. Calling stopPlayback() and TunerHal.stopTune()
+ // resets the tuner device to recover from the error.
+ stopPlayback();
+ stopCaptionTrack();
+ mTunerHal.stopTune();
+
+ mInternalListener.notifyVideoUnavailable(
+ TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
+
+ // After MAX_RETRY_COUNT, give some delay of an empirically chosen value
+ // before recovering the playback.
+ mHandler.sendEmptyMessageDelayed(MSG_RECOVER_STOPPED_PLAYBACK,
+ RECOVER_STOPPED_PLAYBACK_PERIOD_MS);
+ }
+ }
+ return true;
+ }
+ case MSG_RECOVER_STOPPED_PLAYBACK: {
+ if (DEBUG) {
+ Log.d(TAG, "MSG_RECOVER_STOPPED_PLAYBACK");
+ }
+ resetPlayback();
+ return true;
+ }
+ case MSG_START_PLAYBACK: {
+ if (DEBUG) {
+ Log.d(TAG, "MSG_START_PLAYBACK");
+ }
+ if (mChannel != null || mRecordingId != null) {
+ startPlayback(msg.obj);
+ }
+ return true;
+ }
+ case MSG_PLAYBACK_STATE_CHANGED: {
+ int generation = msg.arg1;
+ int playbackState = msg.arg2;
+ boolean playWhenReady = (boolean) msg.obj;
+ if (DEBUG) {
+ Log.d(TAG, "ExoPlayer state change: " + generation + " "
+ + playbackState + " " + playWhenReady);
+ }
+
+ // Generation starts from 1 not 0.
+ if (playbackState == MpegTsPlayer.STATE_READY
+ && mPreparingGeneration == mPlayerGeneration) {
+ if (DEBUG) {
+ Log.d(TAG, "ExoPlayer ready: " + mPlayerGeneration);
+ }
+
+ // mPreparingGeneration was set to mPlayerGeneration in order to indicate that
+ // ExoPlayer is in its preparing status when MpegTsPlayer::prepare() was called.
+ // Now MpegTsPlayer::prepare() is finished. Clear preparing state in order to
+ // ensure another DO_START_PLAYBACK will not be sent for same generation.
+ mPreparingGeneration = 0;
+ sendMessage(MSG_START_PLAYBACK, mPlayer);
+ } else if (playbackState == MpegTsPlayer.STATE_ENDED
+ && mEndedGeneration != generation) {
+ // Final status
+ // notification of STATE_ENDED from MpegTsPlayer will be ignored afterwards.
+ mEndedGeneration = generation;
+ Log.i(TAG, "Player ended: end of stream " + generation);
+ sendMessage(MSG_RETRY_PLAYBACK, mPlayer);
+ }
+ return true;
+ }
+ case MSG_PLAYBACK_ERROR: {
+ int generation = msg.arg1;
+ Exception exception = (Exception) msg.obj;
+ Log.i(TAG, "ExoPlayer Error: " + generation + " " + mPlayerGeneration);
+ if (generation != mPlayerGeneration) {
+ return true;
+ }
+ mHandler.obtainMessage(MSG_RETRY_PLAYBACK, mPlayer).sendToTarget();
+ return true;
+ }
+ case MSG_PLAYBACK_VIDEO_SIZE_CHANGED: {
+ int generation = msg.arg1;
+ Size size = (Size) msg.obj;
+ if (generation != mPlayerGeneration) {
+ return true;
+ }
+ if (mChannel != null && mChannel.hasVideo()) {
+ updateVideoTrack(size.getWidth(), size.getHeight());
+ }
+ return true;
+ }
+ case MSG_AUDIO_UNPLAYABLE: {
+ int generation = (int) msg.obj;
+ if (mPlayer == null || generation != mPlayerGeneration) {
+ return true;
+ }
+ Log.i(TAG, "AC3 audio cannot be played due to device limitation");
+ mInternalListener.sendUiMessage(
+ TvInputSessionImpl.MSG_UI_SHOW_AUDIO_UNPLAYABLE);
+ return true;
+ }
+ case MSG_UPDATE_PROGRAM: {
+ if (mChannel != null) {
+ EitItem program = (EitItem) msg.obj;
+ updateTvTracks(program);
+ mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS);
+ }
+ return true;
+ }
+ case MSG_SCHEDULE_OF_PROGRAMS: {
+ mHandler.removeMessages(MSG_UPDATE_PROGRAM);
+ Pair<TunerChannel, List<EitItem>> pair =
+ (Pair<TunerChannel, List<EitItem>>) msg.obj;
+ TunerChannel channel = pair.first;
+ if (mChannel == null) {
+ return true;
+ }
+ if (mChannel != null && mChannel.compareTo(channel) != 0) {
+ return true;
+ }
+ mPrograms = pair.second;
+ EitItem currentProgram = getCurrentProgram();
+ if (currentProgram == null) {
+ mProgram = null;
+ }
+ long currentTimeMs = getCurrentPosition();
+ if (mPrograms != null) {
+ for (EitItem item : mPrograms) {
+ if (currentProgram != null && currentProgram.compareTo(item) == 0) {
+ if (DEBUG) {
+ Log.d(TAG, "Update current TvTracks " + item);
+ }
+ if (mProgram != null && mProgram.compareTo(item) == 0) {
+ continue;
+ }
+ mProgram = item;
+ updateTvTracks(item);
+ } else if (item.getStartTimeUtcMillis() > currentTimeMs) {
+ if (DEBUG) {
+ Log.d(TAG, "Update next TvTracks " + item + " "
+ + (item.getStartTimeUtcMillis() - currentTimeMs));
+ }
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(MSG_UPDATE_PROGRAM, item),
+ item.getStartTimeUtcMillis() - currentTimeMs);
+ }
+ }
+ }
+ mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS);
+ return true;
+ }
+ case MSG_UPDATE_CHANNEL_INFO: {
+ TunerChannel channel = (TunerChannel) msg.obj;
+ if (mChannel != null && mChannel.compareTo(channel) == 0) {
+ updateChannelInfo(channel);
+ }
+ return true;
+ }
+ case MSG_PROGRAM_DATA_RESULT: {
+ TunerChannel channel = (TunerChannel) ((Pair) msg.obj).first;
+
+ // If there already exists, skip it since real-time data is a top priority,
+ if (mChannel != null && mChannel.compareTo(channel) == 0
+ && mPrograms == null && mProgram == null) {
+ sendMessage(MSG_SCHEDULE_OF_PROGRAMS, msg.obj);
+ }
+ return true;
+ }
+ case MSG_DRAWN_TO_SURFACE: {
+ if (mPlayer == msg.obj && mSurface != null && mPlayerStarted) {
+ if (DEBUG) {
+ Log.d(TAG, "MSG_DRAWN_TO_SURFACE");
+ }
+ mCacheStartTimeMs = mRecordStartTimeMs =
+ (mRecordingId != null) ? 0 : System.currentTimeMillis();
+ mInternalListener.notifyVideoAvailable();
+ mReportedDrawnToSurface = true;
+
+ // If surface is drawn successfully, it means that the playback was brought back
+ // to normal and therefore, the playback recovery status will be reset through
+ // setting a zero value to the retry count.
+ // TODO: Consider audio only channels for detecting playback status changes to
+ // be normal.
+ mRetryCount = 0;
+ if (mCaptionEnabled && mCaptionTrack != null) {
+ startCaptionTrack();
+ } else {
+ stopCaptionTrack();
+ }
+ }
+ return true;
+ }
+ case MSG_TRICKPLAY: {
+ doTrickplay(msg.arg1);
+ return true;
+ }
+ case MSG_RESCHEDULE_PROGRAMS: {
+ doReschedulePrograms();
+ return true;
+ }
+ case MSG_PARENTAL_CONTROLS: {
+ doParentalControls();
+ mHandler.removeMessages(MSG_PARENTAL_CONTROLS);
+ mHandler.sendEmptyMessageDelayed(MSG_PARENTAL_CONTROLS,
+ PARENTAL_CONTROLS_INTERVAL_MS);
+ return true;
+ }
+ case MSG_UNBLOCKED_RATING: {
+ mUnblockedContentRating = (TvContentRating) msg.obj;
+ doParentalControls();
+ mHandler.removeMessages(MSG_PARENTAL_CONTROLS);
+ mHandler.sendEmptyMessageDelayed(MSG_PARENTAL_CONTROLS,
+ PARENTAL_CONTROLS_INTERVAL_MS);
+ return true;
+ }
+ case MSG_DISCOVER_CAPTION_SERVICE_NUMBER: {
+ int serviceNumber = (int) msg.obj;
+ doDiscoverCaptionServiceNumber(serviceNumber);
+ return true;
+ }
+ case MSG_SELECT_TRACK: {
+ // TODO : mChannel == null && mRecordingId != null
+ if (mChannel != null) {
+ doSelectTrack(msg.arg1, (String) msg.obj);
+ }
+ return true;
+ }
+ case MSG_SET_CAPTION_ENABLED: {
+ mCaptionEnabled = (boolean) msg.obj;
+ if (mCaptionEnabled) {
+ startCaptionTrack();
+ } else {
+ stopCaptionTrack();
+ }
+ return true;
+ }
+ case MSG_TIMESHIFT_PAUSE: {
+ doTimeShiftPause();
+ return true;
+ }
+ case MSG_TIMESHIFT_RESUME: {
+ doTimeShiftResume();
+ return true;
+ }
+ case MSG_TIMESHIFT_SEEK_TO: {
+ doTimeShiftSeekTo((long) msg.obj);
+ return true;
+ }
+ case MSG_TIMESHIFT_SET_PLAYBACKPARAMS: {
+ doTimeShiftSetPlaybackParams((PlaybackParams) msg.obj);
+ return true;
+ }
+ case MSG_AUDIO_CAPABILITIES_CHANGED: {
+ AudioCapabilities capabilities = (AudioCapabilities) msg.obj;
+ if (DEBUG) {
+ Log.d(TAG, "MSG_AUDIO_CAPABILITIES_CHANGED " + capabilities);
+ }
+ if (capabilities == null) {
+ return true;
+ }
+ if (!capabilities.equals(mAudioCapabilities)) {
+ // HDMI supported encodings are changed. restart player.
+ mAudioCapabilities = capabilities;
+ resetPlayback();
+ }
+ return true;
+ }
+ case MSG_SET_SURFACE: {
+ Surface surface = (Surface) msg.obj;
+ if (DEBUG) {
+ Log.d(TAG, "MSG_SET_SURFACE " + surface);
+ }
+ if (surface != null && !surface.isValid()) {
+ Log.w(TAG, "Ignoring invalid surface.");
+ return true;
+ }
+ mSurface = surface;
+ resetPlayback();
+ return true;
+ }
+ case MSG_SET_STREAM_VOLUME: {
+ mVolume = (float) msg.obj;
+ if (mPlayer != null && mPlayer.isPlaying()) {
+ mPlayer.setVolume(mVolume);
+ }
+ return true;
+ }
+ case MSG_CACHE_START_TIME_CHANGED: {
+ if (mPlayer == null) {
+ return true;
+ }
+ mCacheStartTimeMs = (long) msg.obj;
+ if (!hasEnoughBackwardCache()
+ && (!mPlayer.isPlaying() || mPlaybackParams.getSpeed() < 1.0f)) {
+ mPlayer.setPlayWhenReady(true);
+ mPlayer.setAudioTrack(true);
+ mPlaybackParams.setSpeed(1.0f);
+ }
+ return true;
+ }
+ case MSG_CACHE_STATE_CHANGED: {
+ boolean available = (boolean) msg.obj;
+ mInternalListener.notifyTimeShiftStatusChanged(available
+ ? TvInputManager.TIME_SHIFT_STATUS_AVAILABLE
+ : TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE);
+ return true;
+ }
+ case MSG_CHECK_SIGNAL: {
+ if (mChannel == null) {
+ return true;
+ }
+ long limitInBytes = mSource != null ? mSource.getLimit() : 0L;
+ long positionInBytes = mSource != null ? mSource.getPosition() : 0L;
+ if (UsbTunerDebug.ENABLED) {
+ UsbTunerDebug.calculateDiff();
+ mInternalListener.sendUiMessage(TvInputSessionImpl.MSG_UI_SET_STATUS_TEXT,
+ Html.fromHtml(
+ StatusTextUtils.getStatusWarningInHTML(
+ (limitInBytes - mLastLimitInBytes)
+ / TS_PACKET_SIZE,
+ UsbTunerDebug.getVideoFrameDrop(),
+ UsbTunerDebug.getBytesInQueue(),
+ UsbTunerDebug.getAudioPositionUs(),
+ UsbTunerDebug.getAudioPositionUsRate(),
+ UsbTunerDebug.getAudioPtsUs(),
+ UsbTunerDebug.getAudioPtsUsRate(),
+ UsbTunerDebug.getVideoPtsUs(),
+ UsbTunerDebug.getVideoPtsUsRate()
+ )));
+ }
+ if (DEBUG) {
+ Log.d(TAG, String.format("MSG_CHECK_SIGNAL position: %d, limit: %d",
+ positionInBytes, limitInBytes));
+ }
+ mInternalListener.sendUiMessage(TvInputSessionImpl.MSG_UI_HIDE_MESSAGE);
+ if (mSource != null && mChannel.getType() == Channel.TYPE_TUNER
+ && positionInBytes == mLastPositionInBytes
+ && limitInBytes == mLastLimitInBytes) {
+ mInternalListener.notifyVideoUnavailable(
+ TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
+
+ mReportedSignalAvailable = false;
+ } else {
+ if (mReportedDrawnToSurface && !mReportedSignalAvailable) {
+ mInternalListener.notifyVideoAvailable();
+ mReportedSignalAvailable = true;
+ }
+ }
+ mLastLimitInBytes = limitInBytes;
+ mLastPositionInBytes = positionInBytes;
+ mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL,
+ CHECK_NO_SIGNAL_PERIOD_MS);
+ return true;
+ }
+ default: {
+ Log.w(TAG, "Unhandled message code: " + msg.what);
+ return false;
+ }
+ }
+ }
+
+ // Private methods
+ private void doSelectTrack(int type, String trackId) {
+ int numTrackId = trackId != null
+ ? Integer.parseInt(trackId.substring(TRACK_PREFIX_SIZE)) : -1;
+ if (type == TvTrackInfo.TYPE_AUDIO) {
+ if (trackId == null) {
+ return;
+ }
+ AtscAudioTrack audioTrack = mAudioTrackMap.get(numTrackId);
+ if (audioTrack == null) {
+ return;
+ }
+ int oldAudioPid = mChannel.getAudioPid();
+ mChannel.selectAudioTrack(audioTrack.index);
+ int newAudioPid = mChannel.getAudioPid();
+ if (oldAudioPid != newAudioPid) {
+ // TODO: Implement a switching between tracks more smoothly.
+ resetPlayback();
+ }
+ mInternalListener.notifyTrackSelected(type, trackId);
+ } else if (type == TvTrackInfo.TYPE_SUBTITLE) {
+ if (trackId == null) {
+ mInternalListener.notifyTrackSelected(type, null);
+ mCaptionTrack = null;
+ stopCaptionTrack();
+ return;
+ }
+ for (TvTrackInfo track : mTvTracks) {
+ if (track.getId().equals(trackId)) {
+ // The service number of the caption service is used for track id of a
+ // subtitle track. Passes the following track id on to TsParser.
+ mInternalListener.notifyTrackSelected(type, trackId);
+ mCaptionTrack = mCaptionTrackMap.get(numTrackId);
+ startCaptionTrack();
+ return;
+ }
+ }
+ }
+ }
+
+ private MpegTsPlayer createPlayer(AudioCapabilities capabilities, CacheManager cacheManager) {
+ if (capabilities == null) {
+ Log.w(TAG, "No Audio Capabilities");
+ }
+ ++mPlayerGeneration;
+
+ MpegTsPlayer player = new MpegTsPlayer(mPlayerGeneration,
+ new MpegTsPassthroughAc3RendererBuilder(cacheManager, this),
+ mHandler, capabilities);
+ Log.i(TAG, "Passthrough AC3 renderer");
+ if (DEBUG) {
+ Log.d(TAG, "ExoPlayer created: " + mPlayerGeneration);
+ }
+ return player;
+ }
+
+ private void startCaptionTrack() {
+ if (mCaptionEnabled && mCaptionTrack != null) {
+ mInternalListener.sendUiMessage(
+ TvInputSessionImpl.MSG_UI_START_CAPTION_TRACK, mCaptionTrack);
+ if (mPlayer != null) {
+ mPlayer.setCaptionServiceNumber(mCaptionTrack.serviceNumber);
+ }
+ }
+ }
+
+ private void stopCaptionTrack() {
+ if (mPlayer != null) {
+ mPlayer.setCaptionServiceNumber(Cea708Data.EMPTY_SERVICE_NUMBER);
+ }
+ mInternalListener.sendUiMessage(TvInputSessionImpl.MSG_UI_STOP_CAPTION_TRACK);
+ }
+
+ private void resetTvTracks() {
+ mTvTracks.clear();
+ mAudioTrackMap.clear();
+ mCaptionTrackMap.clear();
+ mInternalListener.sendUiMessage(TvInputSessionImpl.MSG_UI_RESET_CAPTION_TRACK);
+ mInternalListener.notifyTracksChanged(mTvTracks);
+ }
+
+ private void updateTvTracks(TvTracksInterface tvTracksInterface) {
+ if (DEBUG) {
+ Log.d(TAG, "UpdateTvTracks " + tvTracksInterface);
+ }
+ List<AtscAudioTrack> audioTracks = tvTracksInterface.getAudioTracks();
+ List<AtscCaptionTrack> captionTracks = tvTracksInterface.getCaptionTracks();
+ if (audioTracks != null && !audioTracks.isEmpty()) {
+ updateAudioTracks(audioTracks);
+ }
+ if (captionTracks == null || captionTracks.isEmpty()) {
+ if (tvTracksInterface.hasCaptionTrack()) {
+ updateCaptionTracks(captionTracks);
+ }
+ } else {
+ updateCaptionTracks(captionTracks);
+ }
+ }
+
+ private void removeTvTracks(int trackType) {
+ Iterator<TvTrackInfo> iterator = mTvTracks.iterator();
+ while (iterator.hasNext()) {
+ TvTrackInfo tvTrackInfo = iterator.next();
+ if (tvTrackInfo.getType() == trackType) {
+ iterator.remove();
+ }
+ }
+ }
+
+ private void updateVideoTrack(int width, int height) {
+ removeTvTracks(TvTrackInfo.TYPE_VIDEO);
+ mTvTracks.add(new TvTrackInfo.Builder(TvTrackInfo.TYPE_VIDEO, VIDEO_TRACK_ID)
+ .setVideoWidth(width).setVideoHeight(height).build());
+ mInternalListener.notifyTracksChanged(mTvTracks);
+ mInternalListener.notifyTrackSelected(TvTrackInfo.TYPE_VIDEO, VIDEO_TRACK_ID);
+ }
+
+ private void updateAudioTracks(List<AtscAudioTrack> audioTracks) {
+ if (DEBUG) {
+ Log.d(TAG, "Update AudioTracks " + audioTracks);
+ }
+ removeTvTracks(TvTrackInfo.TYPE_AUDIO);
+ mAudioTrackMap.clear();
+ if (audioTracks != null) {
+ int index = 0;
+ for (AtscAudioTrack audioTrack : audioTracks) {
+ String language = audioTrack.language;
+ if (language == null && mChannel.getAudioTracks() != null
+ && mChannel.getAudioTracks().size() == audioTracks.size()) {
+ // If a language is not present, use a language field in PMT section parsed.
+ language = mChannel.getAudioTracks().get(index).language;
+ }
+
+ // Save the index to the audio track.
+ // Later, when a audio track is selected, Both an audio pid and its audio stream
+ // type reside in the selected index position of the tuner channel's audio data.
+ audioTrack.index = index;
+ TvTrackInfo.Builder builder = new TvTrackInfo.Builder(
+ TvTrackInfo.TYPE_AUDIO, AUDIO_TRACK_PREFIX + index);
+ if (IsoUtils.isValidIso3Language(language)) {
+ builder.setLanguage(language);
+ }
+ if (audioTrack.channelCount != 0) {
+ builder.setAudioChannelCount(audioTrack.channelCount);
+ }
+ if (audioTrack.sampleRate != 0) {
+ builder.setAudioSampleRate(audioTrack.sampleRate);
+ }
+ TvTrackInfo track = builder.build();
+ mTvTracks.add(track);
+ mAudioTrackMap.put(index, audioTrack);
+ ++index;
+ }
+ }
+ mInternalListener.notifyTracksChanged(mTvTracks);
+ }
+
+ private void updateCaptionTracks(List<AtscCaptionTrack> captionTracks) {
+ if (DEBUG) {
+ Log.d(TAG, "Update CaptionTrack " + captionTracks);
+ }
+ removeTvTracks(TvTrackInfo.TYPE_SUBTITLE);
+ mCaptionTrackMap.clear();
+ if (captionTracks != null) {
+ for (AtscCaptionTrack captionTrack : captionTracks) {
+ if (mCaptionTrackMap.indexOfKey(captionTrack.serviceNumber) >= 0) {
+ continue;
+ }
+ String language = captionTrack.language;
+
+ // The service number of the caption service is used for track id of a subtitle.
+ // Later, when a subtitle is chosen, track id will be passed on to TsParser.
+ TvTrackInfo.Builder builder =
+ new TvTrackInfo.Builder(TvTrackInfo.TYPE_SUBTITLE,
+ SUBTITLE_TRACK_PREFIX + captionTrack.serviceNumber);
+ if (IsoUtils.isValidIso3Language(language)) {
+ builder.setLanguage(language);
+ }
+ mTvTracks.add(builder.build());
+ mCaptionTrackMap.put(captionTrack.serviceNumber, captionTrack);
+ }
+ }
+ mInternalListener.notifyTracksChanged(mTvTracks);
+ }
+
+ private void updateChannelInfo(TunerChannel channel) {
+ if (DEBUG) {
+ Log.d(TAG, String.format("Channel Info (old) videoPid: %d audioPid: %d " +
+ "audioSize: %d", mChannel.getVideoPid(), mChannel.getAudioPid(),
+ mChannel.getAudioPids().size()));
+ }
+
+ // The list of the audio tracks resided in a channel is often changed depending on a
+ // program being on the air. So, we should update the streaming PIDs and types of the
+ // tuned channel according to the newly received channel data.
+ int oldVideoPid = mChannel.getVideoPid();
+ int oldAudioPid = mChannel.getAudioPid();
+ List<Integer> audioPids = channel.getAudioPids();
+ List<Integer> audioStreamTypes = channel.getAudioStreamTypes();
+ int size = audioPids.size();
+ mChannel.setVideoPid(channel.getVideoPid());
+ mChannel.setAudioPids(audioPids);
+ mChannel.setAudioStreamTypes(audioStreamTypes);
+ updateTvTracks(mChannel);
+ int index = audioPids.isEmpty() ? -1 : 0;
+ for (int i = 0; i < size; ++i) {
+ if (audioPids.get(i) == oldAudioPid) {
+ index = i;
+ break;
+ }
+ }
+ mChannel.selectAudioTrack(index);
+ mInternalListener.notifyTrackSelected(TvTrackInfo.TYPE_AUDIO,
+ index == -1 ? null : AUDIO_TRACK_PREFIX + index);
+
+ // Reset playback if there is a change in the listening streaming PIDs.
+ if (oldVideoPid != mChannel.getVideoPid()
+ || oldAudioPid != mChannel.getAudioPid()) {
+ // TODO: Implement a switching between tracks more smoothly.
+ resetPlayback();
+ }
+ if (DEBUG) {
+ Log.d(TAG, String.format("Channel Info (new) videoPid: %d audioPid: %d " +
+ " audioSize: %d", mChannel.getVideoPid(), mChannel.getAudioPid(),
+ mChannel.getAudioPids().size()));
+ }
+ }
+
+ private void stopPlayback() {
+ if (mPlayer != null) {
+ if (mSource != null) {
+ mSource.stopStream();
+ }
+ mPlayer.setPlayWhenReady(false);
+ mPlayer.release();
+ mPlayer = null;
+ mPlaybackParams.setSpeed(1.0f);
+ mPlayerStarted = false;
+ mReportedDrawnToSurface = false;
+ mReportedSignalAvailable = false;
+ mInternalListener.sendUiMessage(TvInputSessionImpl.MSG_UI_HIDE_AUDIO_UNPLAYABLE);
+ }
+ }
+
+ private void startPlayback(Object playerObj) {
+ // TODO: provide hasAudio()/hasVideo() for play recordings.
+ if (mPlayer == null || mPlayer != playerObj) {
+ return;
+ }
+ if (mChannel != null && !mChannel.hasAudio()) {
+ // A channel needs to have a audio stream at least to play in exoPlayer.
+ mInternalListener.notifyVideoUnavailable(
+ TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
+ return;
+ }
+ if (mSurface != null && !mPlayerStarted) {
+ mPlayer.setSurface(mSurface);
+ mPlayer.setPlayWhenReady(true);
+ mPlayer.setVolume(mVolume);
+ if (mChannel != null && !mChannel.hasVideo() && mChannel.hasAudio()) {
+ mInternalListener.notifyVideoUnavailable(
+ TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY);
+ } else {
+ mInternalListener.notifyVideoUnavailable(
+ TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING);
+ }
+ mInternalListener.sendUiMessage(TvInputSessionImpl.MSG_UI_HIDE_MESSAGE);
+ mPlayerStarted = true;
+ }
+ }
+
+ private void playFromChannel(long timestamp) {
+ long oldTimestamp;
+ mSource = getDataSource(mChannel.getType());
+ Assert.assertNotNull(mSource);
+ if (mSource.tuneToChannel(mChannel)) {
+ if (ENABLE_PROFILER) {
+ oldTimestamp = timestamp;
+ timestamp = SystemClock.elapsedRealtime();
+ Log.i(TAG, "[Profiler] tuneToChannel() takes " + (timestamp - oldTimestamp)
+ + " ms");
+ }
+ mSource.startStream();
+ mPlayer = createPlayer(mAudioCapabilities, mCacheManager);
+ mPlayer.setCaptionServiceNumber(Cea708Data.EMPTY_SERVICE_NUMBER);
+ mPlayer.addListener(this);
+ mPlayer.setVideoEventListener(this);
+ mPlayer.setCaptionServiceNumber(mCaptionTrack != null ?
+ mCaptionTrack.serviceNumber : Cea708Data.EMPTY_SERVICE_NUMBER);
+ mPreparingGeneration = mPlayerGeneration;
+ mPlayer.prepare((MediaDataSource) mSource);
+ mPlayerStarted = false;
+ } else {
+ // Close TunerHal when tune fails.
+ mTunerHal.stopTune();
+ mInternalListener.notifyVideoUnavailable(
+ TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
+ }
+ }
+
+ private void playFromRecording() {
+ // TODO: Handle errors.
+ CacheManager cacheManager =
+ new CacheManager(new DvrStorageManager(new File(getRecordingPath()), false));
+ mSource = null;
+ mPlayer = createPlayer(mAudioCapabilities, cacheManager);
+ mPlayer.setCaptionServiceNumber(Cea708Data.EMPTY_SERVICE_NUMBER);
+ mPlayer.addListener(this);
+ mPlayer.setVideoEventListener(this);
+ mPlayer.setCaptionServiceNumber(mCaptionTrack != null ?
+ mCaptionTrack.serviceNumber : Cea708Data.EMPTY_SERVICE_NUMBER);
+ mPreparingGeneration = mPlayerGeneration;
+ mPlayer.prepare(null);
+ mPlayerStarted = false;
+ }
+
+ private void resetPlayback() {
+ long timestamp, oldTimestamp;
+ timestamp = SystemClock.elapsedRealtime();
+ stopPlayback();
+ stopCaptionTrack();
+ if (ENABLE_PROFILER) {
+ oldTimestamp = timestamp;
+ timestamp = SystemClock.elapsedRealtime();
+ Log.i(TAG, "[Profiler] stopPlayback() takes " + (timestamp - oldTimestamp) + " ms");
+ }
+ if (!mChannelBlocked && mSurface != null) {
+ mInternalListener.notifyVideoUnavailable(
+ TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING);
+ if (mChannel != null) {
+ playFromChannel(timestamp);
+ } else if (mRecordingId != null){
+ playFromRecording();
+ }
+ }
+ }
+
+ private InputStreamSource getDataSource(int type) {
+ switch (type) {
+ case Channel.TYPE_TUNER:
+ return mTunerSource;
+ case Channel.TYPE_FILE:
+ return mFileSource;
+ default:
+ return null;
+ }
+ }
+
+ private void prepareTune(TunerChannel channel, String recording) {
+ mChannelBlocked = false;
+ mUnblockedContentRating = null;
+ mRetryCount = 0;
+ mChannel = channel;
+ mRecordingId = recording;
+ mRecordingDuration = recording != null ? getDurationForRecording(recording) : null;
+ mProgram = null;
+ mPrograms = null;
+ mCacheStartTimeMs = mRecordStartTimeMs =
+ (mRecordingId != null) ? 0 : System.currentTimeMillis();
+ mLastPositionMs = 0;
+ mCaptionTrack = null;
+ mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS);
+ }
+
+ private void doReschedulePrograms() {
+ long currentPositionMs = getCurrentPosition();
+ long forwardDifference = Math.abs(currentPositionMs - mLastPositionMs
+ - RESCHEDULE_PROGRAMS_INTERVAL_MS);
+ mLastPositionMs = currentPositionMs;
+
+ // A gap is measured as the time difference between previous and next current position
+ // periodically. If the gap has a significant difference with an interval of a period,
+ // this means that there is a change of playback status and the programs of the current
+ // channel should be rescheduled to new playback timeline.
+ if (forwardDifference > RESCHEDULE_PROGRAMS_TOLERANCE_MS) {
+ if (DEBUG) {
+ Log.d(TAG, "reschedule programs size:"
+ + (mPrograms != null ? mPrograms.size() : 0) + " current program: "
+ + getCurrentProgram());
+ }
+ mHandler.obtainMessage(MSG_SCHEDULE_OF_PROGRAMS, new Pair<>(mChannel, mPrograms))
+ .sendToTarget();
+ }
+ mHandler.removeMessages(MSG_RESCHEDULE_PROGRAMS);
+ mHandler.sendEmptyMessageDelayed(MSG_RESCHEDULE_PROGRAMS,
+ RESCHEDULE_PROGRAMS_INTERVAL_MS);
+ }
+
+ private int getTrickPlaySeekIntervalMs() {
+ return Math.max(MIN_TRICKPLAY_SEEK_INTERVAL_MS,
+ (int) Math.abs(TRICKPLAY_SEEK_INTERVAL_MS / mPlaybackParams.getSpeed()));
+ }
+
+ private void doTrickplay(int seekPositionMs) {
+ mHandler.removeMessages(MSG_TRICKPLAY);
+ if (mPlaybackParams.getSpeed() == 1.0f || !mPlayer.isPlaying()) {
+ return;
+ }
+ if (seekPositionMs < mCacheStartTimeMs - mRecordStartTimeMs) {
+ mPlayer.seekTo(mCacheStartTimeMs - mRecordStartTimeMs);
+ mPlaybackParams.setSpeed(1.0f);
+ mPlayer.setAudioTrack(true);
+ return;
+ } else if (seekPositionMs > System.currentTimeMillis() - mRecordStartTimeMs) {
+ mPlayer.seekTo(System.currentTimeMillis() - mRecordStartTimeMs);
+ mPlaybackParams.setSpeed(1.0f);
+ mPlayer.setAudioTrack(true);
+ return;
+ }
+
+ if (!mPlayer.isBuffering()) {
+ mPlayer.seekTo(seekPositionMs);
+ }
+ seekPositionMs += mPlaybackParams.getSpeed() * getTrickPlaySeekIntervalMs();
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_TRICKPLAY, seekPositionMs, 0),
+ getTrickPlaySeekIntervalMs());
+ }
+
+ private void doTimeShiftPause() {
+ if (!hasEnoughBackwardCache()) {
+ return;
+ }
+ mPlaybackParams.setSpeed(1.0f);
+ mPlayer.setPlayWhenReady(false);
+ mPlayer.setAudioTrack(true);
+ }
+
+ private void doTimeShiftResume() {
+ mPlaybackParams.setSpeed(1.0f);
+ mPlayer.setPlayWhenReady(true);
+ mPlayer.setAudioTrack(true);
+ }
+
+ private void doTimeShiftSeekTo(long timeMs) {
+ mPlayer.seekTo((int) (timeMs - mRecordStartTimeMs));
+ }
+
+ private void doTimeShiftSetPlaybackParams(PlaybackParams params) {
+ if (!hasEnoughBackwardCache() && params.getSpeed() < 1.0f) {
+ return;
+ }
+ mPlaybackParams = params;
+ if (!mHandler.hasMessages(MSG_TRICKPLAY)) {
+ // Initiate trickplay
+ float rate = mPlaybackParams.getSpeed();
+ if (rate != 1.0f) {
+ mPlayer.setAudioTrack(false);
+ mPlayer.setPlayWhenReady(true);
+ }
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_TRICKPLAY,
+ (int) (mPlayer.getCurrentPosition() + rate * getTrickPlaySeekIntervalMs()), 0));
+ }
+ }
+
+ private EitItem getCurrentProgram() {
+ if (mPrograms == null) {
+ return null;
+ }
+ long currentTimeMs = getCurrentPosition();
+ for (EitItem item : mPrograms) {
+ if (item.getStartTimeUtcMillis() <= currentTimeMs
+ && item.getEndTimeUtcMillis() >= currentTimeMs) {
+ return item;
+ }
+ }
+ return null;
+ }
+
+ private void doParentalControls() {
+ boolean isParentalControlsEnabled = mTvInputManager.isParentalControlsEnabled();
+ if (isParentalControlsEnabled) {
+ TvContentRating blockContentRating = getContentRatingOfCurrentProgramBlocked();
+ if (DEBUG) {
+ if (blockContentRating != null) {
+ Log.d(TAG, "Check parental controls: blocked by content rating - "
+ + blockContentRating);
+ } else {
+ Log.d(TAG, "Check parental controls: available");
+ }
+ }
+ updateChannelBlockStatus(blockContentRating != null, blockContentRating);
+ } else {
+ if (DEBUG) {
+ Log.d(TAG, "Check parental controls: available");
+ }
+ updateChannelBlockStatus(false, null);
+ }
+ }
+
+ private void doDiscoverCaptionServiceNumber(int serviceNumber) {
+ int index = mCaptionTrackMap.indexOfKey(serviceNumber);
+ if (index < 0) {
+ AtscCaptionTrack captionTrack = new AtscCaptionTrack();
+ captionTrack.serviceNumber = serviceNumber;
+ captionTrack.wideAspectRatio = false;
+ captionTrack.easyReader = false;
+ mCaptionTrackMap.put(serviceNumber, captionTrack);
+ mTvTracks.add(new TvTrackInfo.Builder(TvTrackInfo.TYPE_SUBTITLE,
+ SUBTITLE_TRACK_PREFIX + serviceNumber).build());
+ mInternalListener.notifyTracksChanged(mTvTracks);
+ }
+ }
+
+ private TvContentRating getContentRatingOfCurrentProgramBlocked() {
+ EitItem currentProgram = getCurrentProgram();
+ if (currentProgram == null) {
+ return null;
+ }
+ TvContentRating[] ratings = mTvContentRatingCache
+ .getRatings(currentProgram.getContentRating());
+ if (ratings == null) {
+ return null;
+ }
+ for (TvContentRating rating : ratings) {
+ if (!Objects.equals(mUnblockedContentRating, rating) && mTvInputManager
+ .isRatingBlocked(rating)) {
+ return rating;
+ }
+ }
+ return null;
+ }
+
+ private void updateChannelBlockStatus(boolean channelBlocked,
+ TvContentRating contentRating) {
+ if (mChannelBlocked == channelBlocked) {
+ return;
+ }
+ mChannelBlocked = channelBlocked;
+ if (mChannelBlocked) {
+ mHandler.removeCallbacksAndMessages(null);
+ mTunerHal.stopTune();
+ stopPlayback();
+ resetTvTracks();
+ if (contentRating != null) {
+ mInternalListener.notifyContentBlocked(contentRating);
+ }
+ mHandler.sendEmptyMessageDelayed(MSG_PARENTAL_CONTROLS, PARENTAL_CONTROLS_INTERVAL_MS);
+ } else {
+ mHandler.removeCallbacksAndMessages(null);
+ resetPlayback();
+ mInternalListener.notifyContentAllowed();
+ mHandler.sendEmptyMessageDelayed(MSG_RESCHEDULE_PROGRAMS,
+ RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS);
+ mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_INITIAL_DELAY_MS);
+ }
+ }
+
+ private boolean hasEnoughBackwardCache() {
+ return mPlayer.getCurrentPosition() + CACHE_UNDERFLOW_BUFFER_MS
+ >= mCacheStartTimeMs - mRecordStartTimeMs;
+ }
+}