blob: 6b71fcf69ba6d2e59f0e8beba79e014705abf1a2 [file] [log] [blame]
/*
* 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.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.media.MediaDataSource;
import android.media.MediaFormat;
import android.media.PlaybackParams;
import android.media.tv.TvContentRating;
import android.media.tv.TvContract;
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.MpegTsPassthroughAc3RendererBuilder;
import com.android.usbtuner.exoplayer.MpegTsPlayer;
import com.android.usbtuner.exoplayer.cache.CacheManager;
import com.android.usbtuner.exoplayer.cache.DvrStorageManager;
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 TunerSessionWorker} implements a handler thread which processes TV input jobs
* such as handling {@link ExoPlayer}, managing a tuner device, trickplay, and so on.
*/
public class TunerSessionWorker implements PlaybackCacheListener,
MpegTsPlayer.VideoEventListener, MpegTsPlayer.Listener, EventDetector.EventListener,
ChannelDataManager.ProgramInfoListener, Handler.Callback {
private static final String TAG = "TunerSessionWorker";
private static final boolean DEBUG = false;
private static final boolean ENABLE_PROFILER = true;
private static final String PLAY_FROM_CHANNEL = "channel";
// 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 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();
private final TunerSession mSession;
public TunerSessionWorker(Context context, ChannelDataManager channelDataManager,
CacheManager cacheManager, TunerSession tunerSession) {
mContext = context;
mTunerHal = TunerHal.createInstance(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.
HandlerThread handlerThread = new HandlerThread(TAG);
handlerThread.start();
mHandler = new Handler(handlerThread.getLooper(), this);
mSession = tunerSession;
mChannelDataManager = channelDataManager;
// TODO: need to refactor it for multi-tuner support.
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 Uri.parse(mRecordingId).getPath();
}
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 {
mHandler.getLooper().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) {
mSession.sendUiMessage(TunerSession.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() {
mSession.sendUiMessage(TunerSession.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);
}
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 static class RecordedProgram {
private long mChannelId;
private String mDataUri;
private static final String[] PROJECTION = {
TvContract.Programs.COLUMN_CHANNEL_ID,
TvContract.RecordedPrograms.COLUMN_RECORDING_DATA_URI,
};
public RecordedProgram(Cursor cursor) {
int index = 0;
mChannelId = cursor.getLong(index++);
mDataUri = cursor.getString(index++);
}
public RecordedProgram(long channelId, String dataUri) {
mChannelId = channelId;
mDataUri = dataUri;
}
public static RecordedProgram onQuery(Cursor c) {
RecordedProgram recording = null;
if (c != null && c.moveToNext()) {
recording = new RecordedProgram(c);
}
return recording;
}
public String getDataUri() {
return mDataUri;
}
}
private RecordedProgram getRecordedProgram(Uri recordedUri) {
ContentResolver resolver = mContext.getContentResolver();
try(Cursor c = resolver.query(recordedUri, RecordedProgram.PROJECTION, null, null, null)) {
if (c != null) {
RecordedProgram result = RecordedProgram.onQuery(c);
if (DEBUG) {
Log.d(TAG, "Finished query for " + this);
}
return result;
} else {
if (c == null) {
Log.e(TAG, "Unknown query error for " + this);
} else {
if (DEBUG) {
Log.d(TAG, "Canceled query for " + this);
}
}
return null;
}
}
}
private String parseRecording(Uri uri) {
RecordedProgram recording = getRecordedProgram(uri);
if (recording != null) {
return recording.getDataUri();
}
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();
mSession.notifyVideoUnavailable(
TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
return true;
}
mHandler.removeCallbacksAndMessages(null);
if (channel != null) {
mChannelDataManager.requestProgramsData(channel);
}
prepareTune(channel, recording);
// TODO: Need to refactor. notifyContentAllowed() should not be called if parental
// control is turned on.
mSession.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;
mSession.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();
mSession.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());
}
if (mRecordingId != null) {
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");
mSession.sendUiMessage(
TunerSession.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();
mSession.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: {
if (mChannel != null) {
doSelectTrack(msg.arg1, (String) msg.obj);
} else if (mRecordingId != null) {
// TODO : mChannel == null && mRecordingId != null
Log.d(TAG, "track selected for recording");
}
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;
mSession.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();
mSession.sendUiMessage(TunerSession.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));
}
mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_MESSAGE);
if (mSource != null && mChannel.getType() == Channel.TYPE_TUNER
&& positionInBytes == mLastPositionInBytes
&& limitInBytes == mLastLimitInBytes) {
mSession.notifyVideoUnavailable(
TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
mReportedSignalAvailable = false;
} else {
if (mReportedDrawnToSurface && !mReportedSignalAvailable) {
mSession.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();
}
mSession.notifyTrackSelected(type, trackId);
} else if (type == TvTrackInfo.TYPE_SUBTITLE) {
if (trackId == null) {
mSession.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.
mSession.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(mContext, cacheManager, this),
mHandler, capabilities, this);
Log.i(TAG, "Passthrough AC3 renderer");
if (DEBUG) Log.d(TAG, "ExoPlayer created: " + mPlayerGeneration);
return player;
}
private void startCaptionTrack() {
if (mCaptionEnabled && mCaptionTrack != null) {
mSession.sendUiMessage(
TunerSession.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);
}
mSession.sendUiMessage(TunerSession.MSG_UI_STOP_CAPTION_TRACK);
}
private void resetTvTracks() {
mTvTracks.clear();
mAudioTrackMap.clear();
mCaptionTrackMap.clear();
mSession.sendUiMessage(TunerSession.MSG_UI_RESET_CAPTION_TRACK);
mSession.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());
mSession.notifyTracksChanged(mTvTracks);
mSession.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;
}
}
mSession.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);
}
}
mSession.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);
mSession.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;
mSession.sendUiMessage(TunerSession.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.
mSession.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()) {
mSession.notifyVideoUnavailable(
TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY);
} else {
mSession.notifyVideoUnavailable(
TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING);
}
mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_MESSAGE);
mPlayerStarted = true;
}
}
private void playFromChannel(long timestamp) {
long oldTimestamp;
mSource = null;
if (mChannel.getType() == Channel.TYPE_TUNER) {
mSource = mTunerSource;
} else if (mChannel.getType() == Channel.TYPE_FILE) {
mSource = mFileSource;
}
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.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();
mSession.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.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) {
mSession.notifyVideoUnavailable(
TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING);
if (mChannel != null) {
playFromChannel(timestamp);
} else if (mRecordingId != null){
playFromRecording();
}
}
}
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());
mSession.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) {
mSession.notifyContentBlocked(contentRating);
}
mHandler.sendEmptyMessageDelayed(MSG_PARENTAL_CONTROLS, PARENTAL_CONTROLS_INTERVAL_MS);
} else {
mHandler.removeCallbacksAndMessages(null);
resetPlayback();
mSession.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;
}
}