1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.tv.tuner.tvinput; 18 19 import android.content.ContentResolver; 20 import android.content.ContentUris; 21 import android.content.Context; 22 import android.database.Cursor; 23 import android.media.MediaFormat; 24 import android.media.PlaybackParams; 25 import android.media.tv.TvContentRating; 26 import android.media.tv.TvContract; 27 import android.media.tv.TvInputManager; 28 import android.media.tv.TvTrackInfo; 29 import android.net.Uri; 30 import android.os.Environment; 31 import android.os.Handler; 32 import android.os.HandlerThread; 33 import android.os.Message; 34 import android.os.SystemClock; 35 import android.support.annotation.AnyThread; 36 import android.support.annotation.MainThread; 37 import android.support.annotation.WorkerThread; 38 import android.text.Html; 39 import android.text.TextUtils; 40 import android.util.Log; 41 import android.util.Pair; 42 import android.util.SparseArray; 43 import android.view.Surface; 44 import android.view.accessibility.CaptioningManager; 45 import com.android.tv.common.CommonPreferences.TrickplaySetting; 46 import com.android.tv.common.SoftPreconditions; 47 import com.android.tv.common.TvContentRatingCache; 48 import com.android.tv.common.customization.CustomizationManager; 49 import com.android.tv.common.customization.CustomizationManager.TRICKPLAY_MODE; 50 import com.android.tv.common.util.SystemPropertiesProxy; 51 import com.android.tv.tuner.TunerPreferences; 52 import com.android.tv.tuner.data.Cea708Data; 53 import com.android.tv.tuner.data.PsipData.EitItem; 54 import com.android.tv.tuner.data.PsipData.TvTracksInterface; 55 import com.android.tv.tuner.data.TunerChannel; 56 import com.android.tv.tuner.data.nano.Channel; 57 import com.android.tv.tuner.data.nano.Track.AtscAudioTrack; 58 import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack; 59 import com.android.tv.tuner.exoplayer.MpegTsPlayer; 60 import com.android.tv.tuner.exoplayer.MpegTsRendererBuilder; 61 import com.android.tv.tuner.exoplayer.buffer.BufferManager; 62 import com.android.tv.tuner.exoplayer.buffer.BufferManager.StorageManager; 63 import com.android.tv.tuner.exoplayer.buffer.DvrStorageManager; 64 import com.android.tv.tuner.exoplayer.buffer.TrickplayStorageManager; 65 import com.android.tv.tuner.source.TsDataSource; 66 import com.android.tv.tuner.source.TsDataSourceManager; 67 import com.android.tv.tuner.util.StatusTextUtils; 68 import com.google.android.exoplayer.ExoPlayer; 69 import com.google.android.exoplayer.audio.AudioCapabilities; 70 import java.io.File; 71 import java.util.ArrayList; 72 import java.util.Iterator; 73 import java.util.List; 74 import java.util.Objects; 75 import java.util.concurrent.Semaphore; 76 import java.util.concurrent.TimeUnit; 77 78 /** 79 * {@link TunerSessionWorker} implements a handler thread which processes TV input jobs such as 80 * handling {@link ExoPlayer}, managing a tuner device, trickplay, and so on. 81 */ 82 @WorkerThread 83 public class TunerSessionWorker 84 implements PlaybackBufferListener, 85 MpegTsPlayer.VideoEventListener, 86 MpegTsPlayer.Listener, 87 EventDetector.EventListener, 88 ChannelDataManager.ProgramInfoListener, 89 Handler.Callback { 90 private static final String TAG = "TunerSessionWorker"; 91 private static final boolean DEBUG = false; 92 private static final boolean ENABLE_PROFILER = true; 93 private static final String PLAY_FROM_CHANNEL = "channel"; 94 private static final String MAX_BUFFER_SIZE_KEY = "tv.tuner.buffersize_mbytes"; 95 private static final int MAX_BUFFER_SIZE_DEF = 2 * 1024; // 2GB 96 private static final int MIN_BUFFER_SIZE_DEF = 256; // 256MB 97 98 // Public messages 99 public static final int MSG_SELECT_TRACK = 1; 100 public static final int MSG_UPDATE_CAPTION_TRACK = 2; 101 public static final int MSG_SET_STREAM_VOLUME = 3; 102 public static final int MSG_TIMESHIFT_PAUSE = 4; 103 public static final int MSG_TIMESHIFT_RESUME = 5; 104 public static final int MSG_TIMESHIFT_SEEK_TO = 6; 105 public static final int MSG_TIMESHIFT_SET_PLAYBACKPARAMS = 7; 106 public static final int MSG_AUDIO_CAPABILITIES_CHANGED = 8; 107 public static final int MSG_UNBLOCKED_RATING = 9; 108 public static final int MSG_TUNER_PREFERENCES_CHANGED = 10; 109 110 // Private messages 111 private static final int MSG_TUNE = 1000; 112 private static final int MSG_RELEASE = 1001; 113 private static final int MSG_RETRY_PLAYBACK = 1002; 114 private static final int MSG_START_PLAYBACK = 1003; 115 private static final int MSG_UPDATE_PROGRAM = 1008; 116 private static final int MSG_SCHEDULE_OF_PROGRAMS = 1009; 117 private static final int MSG_UPDATE_CHANNEL_INFO = 1010; 118 private static final int MSG_TRICKPLAY_BY_SEEK = 1011; 119 private static final int MSG_SMOOTH_TRICKPLAY_MONITOR = 1012; 120 private static final int MSG_PARENTAL_CONTROLS = 1015; 121 private static final int MSG_RESCHEDULE_PROGRAMS = 1016; 122 private static final int MSG_BUFFER_START_TIME_CHANGED = 1017; 123 private static final int MSG_CHECK_SIGNAL = 1018; 124 private static final int MSG_DISCOVER_CAPTION_SERVICE_NUMBER = 1019; 125 private static final int MSG_RESET_PLAYBACK = 1020; 126 private static final int MSG_BUFFER_STATE_CHANGED = 1021; 127 private static final int MSG_PROGRAM_DATA_RESULT = 1022; 128 private static final int MSG_STOP_TUNE = 1023; 129 private static final int MSG_SET_SURFACE = 1024; 130 private static final int MSG_NOTIFY_AUDIO_TRACK_UPDATED = 1025; 131 132 private static final int TS_PACKET_SIZE = 188; 133 private static final int CHECK_NO_SIGNAL_INITIAL_DELAY_MS = 4000; 134 private static final int CHECK_NO_SIGNAL_PERIOD_MS = 500; 135 private static final int RECOVER_STOPPED_PLAYBACK_PERIOD_MS = 2500; 136 private static final int PARENTAL_CONTROLS_INTERVAL_MS = 5000; 137 private static final int RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS = 4000; 138 private static final int RESCHEDULE_PROGRAMS_INTERVAL_MS = 10000; 139 private static final int RESCHEDULE_PROGRAMS_TOLERANCE_MS = 2000; 140 // The following 3s is defined empirically. This should be larger than 2s considering video 141 // key frame interval in the TS stream. 142 private static final int PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS = 3000; 143 private static final int PLAYBACK_RETRY_DELAY_MS = 5000; 144 private static final int MAX_IMMEDIATE_RETRY_COUNT = 5; 145 private static final long INVALID_TIME = -1; 146 147 // Some examples of the track ids of the audio tracks, "a0", "a1", "a2". 148 // The number after prefix is being used for indicating a index of the given audio track. 149 private static final String AUDIO_TRACK_PREFIX = "a"; 150 151 // Some examples of the tracks id of the caption tracks, "s1", "s2", "s3". 152 // The number after prefix is being used for indicating a index of a caption service number 153 // of the given caption track. 154 private static final String SUBTITLE_TRACK_PREFIX = "s"; 155 private static final int TRACK_PREFIX_SIZE = 1; 156 private static final String VIDEO_TRACK_ID = "v"; 157 private static final long BUFFER_UNDERFLOW_BUFFER_MS = 5000; 158 159 // Actual interval would be divided by the speed. 160 private static final int EXPECTED_KEY_FRAME_INTERVAL_MS = 500; 161 private static final int MIN_TRICKPLAY_SEEK_INTERVAL_MS = 20; 162 private static final int TRICKPLAY_MONITOR_INTERVAL_MS = 250; 163 private static final int RELEASE_WAIT_INTERVAL_MS = 50; 164 private static final long TRICKPLAY_OFF_DURATION_MS = TimeUnit.DAYS.toMillis(14); 165 166 // Since release() is done asynchronously, synchronization between multiple TunerSessionWorker 167 // creation/release is required. 168 // This is used to guarantee that at most one active TunerSessionWorker exists at any give time. 169 private static Semaphore sActiveSessionSemaphore = new Semaphore(1); 170 171 private final Context mContext; 172 private final ChannelDataManager mChannelDataManager; 173 private final TsDataSourceManager mSourceManager; 174 private final int mMaxTrickplayBufferSizeMb; 175 private final File mTrickplayBufferDir; 176 private final @TRICKPLAY_MODE int mTrickplayModeCustomization; 177 private volatile Surface mSurface; 178 private volatile float mVolume = 1.0f; 179 private volatile boolean mCaptionEnabled; 180 private volatile MpegTsPlayer mPlayer; 181 private volatile TunerChannel mChannel; 182 private volatile Long mRecordingDuration; 183 private volatile long mRecordStartTimeMs; 184 private volatile long mBufferStartTimeMs; 185 private volatile boolean mTrickplayDisabledByStorageIssue; 186 private @TrickplaySetting int mTrickplaySetting; 187 private long mTrickplayExpiredMs; 188 private String mRecordingId; 189 private final Handler mHandler; 190 private int mRetryCount; 191 private final ArrayList<TvTrackInfo> mTvTracks; 192 private final SparseArray<AtscAudioTrack> mAudioTrackMap; 193 private final SparseArray<AtscCaptionTrack> mCaptionTrackMap; 194 private AtscCaptionTrack mCaptionTrack; 195 private PlaybackParams mPlaybackParams = new PlaybackParams(); 196 private boolean mPlayerStarted = false; 197 private boolean mReportedDrawnToSurface = false; 198 private boolean mReportedWeakSignal = false; 199 private EitItem mProgram; 200 private List<EitItem> mPrograms; 201 private final TvInputManager mTvInputManager; 202 private boolean mChannelBlocked; 203 private TvContentRating mUnblockedContentRating; 204 private long mLastPositionMs; 205 private AudioCapabilities mAudioCapabilities; 206 private long mLastLimitInBytes; 207 private final TvContentRatingCache mTvContentRatingCache = TvContentRatingCache.getInstance(); 208 private final TunerSession mSession; 209 private final boolean mHasSoftwareAudioDecoder; 210 private int mPlayerState = ExoPlayer.STATE_IDLE; 211 private long mPreparingStartTimeMs; 212 private long mBufferingStartTimeMs; 213 private long mReadyStartTimeMs; 214 private boolean mIsActiveSession; 215 private boolean mReleaseRequested; // Guarded by mReleaseLock 216 private final Object mReleaseLock = new Object(); 217 218 public TunerSessionWorker( 219 Context context, ChannelDataManager channelDataManager, TunerSession tunerSession) { 220 if (DEBUG) Log.d(TAG, "TunerSessionWorker created"); 221 mContext = context; 222 223 // HandlerThread should be set up before it is registered as a listener in the all other 224 // components. 225 HandlerThread handlerThread = new HandlerThread(TAG); 226 handlerThread.start(); 227 mHandler = new Handler(handlerThread.getLooper(), this); 228 mSession = tunerSession; 229 mChannelDataManager = channelDataManager; 230 mChannelDataManager.setListener(this); 231 mChannelDataManager.checkDataVersion(mContext); 232 mSourceManager = TsDataSourceManager.createSourceManager(false); 233 mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE); 234 mTvTracks = new ArrayList<>(); 235 mAudioTrackMap = new SparseArray<>(); 236 mCaptionTrackMap = new SparseArray<>(); 237 CaptioningManager captioningManager = 238 (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE); 239 mCaptionEnabled = captioningManager.isEnabled(); 240 mPlaybackParams.setSpeed(1.0f); 241 mMaxTrickplayBufferSizeMb = 242 SystemPropertiesProxy.getInt(MAX_BUFFER_SIZE_KEY, MAX_BUFFER_SIZE_DEF); 243 mTrickplayModeCustomization = CustomizationManager.getTrickplayMode(context); 244 if (mTrickplayModeCustomization 245 == CustomizationManager.TRICKPLAY_MODE_USE_EXTERNAL_STORAGE) { 246 boolean useExternalStorage = 247 Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) 248 && Environment.isExternalStorageRemovable(); 249 mTrickplayBufferDir = useExternalStorage ? context.getExternalCacheDir() : null; 250 } else if (mTrickplayModeCustomization == CustomizationManager.TRICKPLAY_MODE_ENABLED) { 251 mTrickplayBufferDir = context.getCacheDir(); 252 } else { 253 mTrickplayBufferDir = null; 254 } 255 mTrickplayDisabledByStorageIssue = mTrickplayBufferDir == null; 256 mTrickplaySetting = TunerPreferences.getTrickplaySetting(context); 257 if (mTrickplaySetting != TunerPreferences.TRICKPLAY_SETTING_NOT_SET 258 && mTrickplayModeCustomization 259 == CustomizationManager.TRICKPLAY_MODE_USE_EXTERNAL_STORAGE) { 260 // Consider the case of Customization package updates the value of trickplay mode 261 // to TRICKPLAY_MODE_USE_EXTERNAL_STORAGE after install. 262 mTrickplaySetting = TunerPreferences.TRICKPLAY_SETTING_NOT_SET; 263 TunerPreferences.setTrickplaySetting(context, mTrickplaySetting); 264 TunerPreferences.setTrickplayExpiredMs(context, 0); 265 } 266 mTrickplayExpiredMs = TunerPreferences.getTrickplayExpiredMs(context); 267 mPreparingStartTimeMs = INVALID_TIME; 268 mBufferingStartTimeMs = INVALID_TIME; 269 mReadyStartTimeMs = INVALID_TIME; 270 // NOTE: We assume that TunerSessionWorker instance will be at most one. 271 // Only one TunerSessionWorker can be connected to FfmpegDecoderClient at any given time. 272 // connect() will return false, if there is a connected TunerSessionWorker already. 273 mHasSoftwareAudioDecoder = false; // TODO reimplement ffmpeg for google3 274 // TODO connect the ffmpeg client and report if available. 275 } 276 277 // Public methods 278 @MainThread 279 public void tune(Uri channelUri) { 280 mHandler.removeCallbacksAndMessages(null); 281 mSourceManager.setHasPendingTune(); 282 sendMessage(MSG_TUNE, channelUri); 283 } 284 285 @MainThread 286 public void stopTune() { 287 mHandler.removeCallbacksAndMessages(null); 288 sendMessage(MSG_STOP_TUNE); 289 } 290 291 /** Sets {@link Surface}. */ 292 @MainThread 293 public void setSurface(Surface surface) { 294 if (surface != null && !surface.isValid()) { 295 Log.w(TAG, "Ignoring invalid surface."); 296 return; 297 } 298 // mSurface is kept even when tune is called right after. But, messages can be deleted by 299 // tune or updateChannelBlockStatus. So mSurface should be stored here, not through message. 300 mSurface = surface; 301 mHandler.sendEmptyMessage(MSG_SET_SURFACE); 302 } 303 304 /** Sets volume. */ 305 @MainThread 306 public void setStreamVolume(float volume) { 307 // mVolume is kept even when tune is called right after. But, messages can be deleted by 308 // tune or updateChannelBlockStatus. So mVolume is stored here and mPlayer.setVolume will be 309 // called in MSG_SET_STREAM_VOLUME. 310 mVolume = volume; 311 mHandler.sendEmptyMessage(MSG_SET_STREAM_VOLUME); 312 } 313 314 /** Sets if caption is enabled or disabled. */ 315 @MainThread 316 public void setCaptionEnabled(boolean captionEnabled) { 317 // mCaptionEnabled is kept even when tune is called right after. But, messages can be 318 // deleted by tune or updateChannelBlockStatus. So mCaptionEnabled is stored here and 319 // start/stopCaptionTrack will be called in MSG_UPDATE_CAPTION_STATUS. 320 mCaptionEnabled = captionEnabled; 321 mHandler.sendEmptyMessage(MSG_UPDATE_CAPTION_TRACK); 322 } 323 324 public TunerChannel getCurrentChannel() { 325 return mChannel; 326 } 327 328 @MainThread 329 public long getStartPosition() { 330 return mBufferStartTimeMs; 331 } 332 333 private String getRecordingPath() { 334 return Uri.parse(mRecordingId).getPath(); 335 } 336 337 private Long getDurationForRecording(String recordingId) { 338 DvrStorageManager storageManager = 339 new DvrStorageManager(new File(getRecordingPath()), false); 340 List<BufferManager.TrackFormat> trackFormatList = storageManager.readTrackInfoFiles(false); 341 if (trackFormatList.isEmpty()) { 342 trackFormatList = storageManager.readTrackInfoFiles(true); 343 } 344 if (!trackFormatList.isEmpty()) { 345 BufferManager.TrackFormat trackFormat = trackFormatList.get(0); 346 Long durationUs = trackFormat.format.getLong(MediaFormat.KEY_DURATION); 347 // we need duration by milli for trickplay notification. 348 return durationUs != null ? durationUs / 1000 : null; 349 } 350 Log.e(TAG, "meta file for recording was not found: " + recordingId); 351 return null; 352 } 353 354 @MainThread 355 public long getCurrentPosition() { 356 // TODO: More precise time may be necessary. 357 MpegTsPlayer mpegTsPlayer = mPlayer; 358 long currentTime = 359 mpegTsPlayer != null 360 ? mRecordStartTimeMs + mpegTsPlayer.getCurrentPosition() 361 : mRecordStartTimeMs; 362 if (mChannel == null && mPlayerState == ExoPlayer.STATE_ENDED) { 363 currentTime = mRecordingDuration + mRecordStartTimeMs; 364 } 365 if (DEBUG) { 366 long systemCurrentTime = System.currentTimeMillis(); 367 Log.d( 368 TAG, 369 "currentTime = " 370 + currentTime 371 + " ; System.currentTimeMillis() = " 372 + systemCurrentTime 373 + " ; diff = " 374 + (currentTime - systemCurrentTime)); 375 } 376 return currentTime; 377 } 378 379 @AnyThread 380 public void sendMessage(int messageType) { 381 mHandler.sendEmptyMessage(messageType); 382 } 383 384 @AnyThread 385 public void sendMessage(int messageType, Object object) { 386 mHandler.obtainMessage(messageType, object).sendToTarget(); 387 } 388 389 @AnyThread 390 public void sendMessage(int messageType, int arg1, int arg2, Object object) { 391 mHandler.obtainMessage(messageType, arg1, arg2, object).sendToTarget(); 392 } 393 394 @MainThread 395 public void release() { 396 if (DEBUG) Log.d(TAG, "release()"); 397 synchronized (mReleaseLock) { 398 mReleaseRequested = true; 399 } 400 if (mHasSoftwareAudioDecoder) { 401 // TODO reimplement for google3 402 // Here disconnect ffmpeg 403 } 404 mChannelDataManager.setListener(null); 405 mHandler.removeCallbacksAndMessages(null); 406 mHandler.sendEmptyMessage(MSG_RELEASE); 407 } 408 409 // MpegTsPlayer.Listener 410 // Called in the same thread as mHandler. 411 @Override 412 public void onStateChanged(boolean playWhenReady, int playbackState) { 413 if (DEBUG) Log.d(TAG, "ExoPlayer state change: " + playbackState + " " + playWhenReady); 414 if (playbackState == mPlayerState) { 415 return; 416 } 417 mReadyStartTimeMs = INVALID_TIME; 418 mPreparingStartTimeMs = INVALID_TIME; 419 mBufferingStartTimeMs = INVALID_TIME; 420 if (playbackState == ExoPlayer.STATE_READY) { 421 if (DEBUG) Log.d(TAG, "ExoPlayer ready"); 422 if (!mPlayerStarted) { 423 sendMessage(MSG_START_PLAYBACK, System.identityHashCode(mPlayer)); 424 } 425 mReadyStartTimeMs = SystemClock.elapsedRealtime(); 426 } else if (playbackState == ExoPlayer.STATE_PREPARING) { 427 mPreparingStartTimeMs = SystemClock.elapsedRealtime(); 428 } else if (playbackState == ExoPlayer.STATE_BUFFERING) { 429 mBufferingStartTimeMs = SystemClock.elapsedRealtime(); 430 } else if (playbackState == ExoPlayer.STATE_ENDED) { 431 // Final status 432 // notification of STATE_ENDED from MpegTsPlayer will be ignored afterwards. 433 Log.i(TAG, "Player ended: end of stream"); 434 if (mChannel != null) { 435 sendMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)); 436 } 437 } 438 mPlayerState = playbackState; 439 } 440 441 @Override 442 public void onError(Exception e) { 443 if (TunerPreferences.getStoreTsStream(mContext)) { 444 // Crash intentionally to capture the error causing TS file. 445 Log.e( 446 TAG, 447 "Crash intentionally to capture the error causing TS file. " + e.getMessage()); 448 SoftPreconditions.checkState(false); 449 } 450 // There maybe some errors that finally raise ExoPlaybackException and will be handled here. 451 // If we are playing live stream, retrying playback maybe helpful. But for recorded stream, 452 // retrying playback is not helpful. 453 if (mChannel != null) { 454 mHandler.obtainMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)) 455 .sendToTarget(); 456 } 457 } 458 459 @Override 460 public void onVideoSizeChanged(int width, int height, float pixelWidthHeight) { 461 if (mChannel != null && mChannel.hasVideo()) { 462 updateVideoTrack(width, height); 463 } 464 if (mRecordingId != null) { 465 updateVideoTrack(width, height); 466 } 467 } 468 469 @Override 470 public void onDrawnToSurface(MpegTsPlayer player, Surface surface) { 471 if (mSurface != null && mPlayerStarted) { 472 if (DEBUG) Log.d(TAG, "MSG_DRAWN_TO_SURFACE"); 473 if (mRecordingId != null) { 474 // Workaround of b/33298048: set it to 1 instead of 0. 475 mBufferStartTimeMs = mRecordStartTimeMs = 1; 476 } else { 477 mBufferStartTimeMs = mRecordStartTimeMs = System.currentTimeMillis(); 478 } 479 notifyVideoAvailable(); 480 mReportedDrawnToSurface = true; 481 482 // If surface is drawn successfully, it means that the playback was brought back 483 // to normal and therefore, the playback recovery status will be reset through 484 // setting a zero value to the retry count. 485 // TODO: Consider audio only channels for detecting playback status changes to 486 // be normal. 487 mRetryCount = 0; 488 if (mCaptionEnabled && mCaptionTrack != null) { 489 startCaptionTrack(); 490 } else { 491 stopCaptionTrack(); 492 } 493 mHandler.sendEmptyMessage(MSG_NOTIFY_AUDIO_TRACK_UPDATED); 494 } 495 } 496 497 @Override 498 public void onSmoothTrickplayForceStopped() { 499 if (mPlayer == null || !mHandler.hasMessages(MSG_SMOOTH_TRICKPLAY_MONITOR)) { 500 return; 501 } 502 mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR); 503 doTrickplayBySeek((int) mPlayer.getCurrentPosition()); 504 } 505 506 @Override 507 public void onAudioUnplayable() { 508 if (mPlayer == null) { 509 return; 510 } 511 Log.i(TAG, "AC3 audio cannot be played due to device limitation"); 512 mSession.sendUiMessage(TunerSession.MSG_UI_SHOW_AUDIO_UNPLAYABLE); 513 } 514 515 // MpegTsPlayer.VideoEventListener 516 @Override 517 public void onEmitCaptionEvent(Cea708Data.CaptionEvent event) { 518 mSession.sendUiMessage(TunerSession.MSG_UI_PROCESS_CAPTION_TRACK, event); 519 } 520 521 @Override 522 public void onClearCaptionEvent() { 523 mSession.sendUiMessage(TunerSession.MSG_UI_CLEAR_CAPTION_RENDERER); 524 } 525 526 @Override 527 public void onDiscoverCaptionServiceNumber(int serviceNumber) { 528 sendMessage(MSG_DISCOVER_CAPTION_SERVICE_NUMBER, serviceNumber); 529 } 530 531 // ChannelDataManager.ProgramInfoListener 532 @Override 533 public void onProgramsArrived(TunerChannel channel, List<EitItem> programs) { 534 sendMessage(MSG_SCHEDULE_OF_PROGRAMS, new Pair<>(channel, programs)); 535 } 536 537 @Override 538 public void onChannelArrived(TunerChannel channel) { 539 sendMessage(MSG_UPDATE_CHANNEL_INFO, channel); 540 } 541 542 @Override 543 public void onRescanNeeded() { 544 mSession.sendUiMessage(TunerSession.MSG_UI_TOAST_RESCAN_NEEDED); 545 } 546 547 @Override 548 public void onRequestProgramsResponse(TunerChannel channel, List<EitItem> programs) { 549 sendMessage(MSG_PROGRAM_DATA_RESULT, new Pair<>(channel, programs)); 550 } 551 552 // PlaybackBufferListener 553 @Override 554 public void onBufferStartTimeChanged(long startTimeMs) { 555 sendMessage(MSG_BUFFER_START_TIME_CHANGED, startTimeMs); 556 } 557 558 @Override 559 public void onBufferStateChanged(boolean available) { 560 sendMessage(MSG_BUFFER_STATE_CHANGED, available); 561 } 562 563 @Override 564 public void onDiskTooSlow() { 565 mTrickplayDisabledByStorageIssue = true; 566 sendMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)); 567 } 568 569 // EventDetector.EventListener 570 @Override 571 public void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) { 572 mChannelDataManager.notifyChannelDetected(channel, channelArrivedAtFirstTime); 573 } 574 575 @Override 576 public void onEventDetected(TunerChannel channel, List<EitItem> items) { 577 mChannelDataManager.notifyEventDetected(channel, items); 578 } 579 580 @Override 581 public void onChannelScanDone() { 582 // do nothing. 583 } 584 585 private long parseChannel(Uri uri) { 586 try { 587 List<String> paths = uri.getPathSegments(); 588 if (paths.size() > 1 && paths.get(0).equals(PLAY_FROM_CHANNEL)) { 589 return ContentUris.parseId(uri); 590 } 591 } catch (UnsupportedOperationException | NumberFormatException e) { 592 } 593 return -1; 594 } 595 596 private static class RecordedProgram { 597 // private final long mChannelId; 598 private final String mDataUri; 599 600 private static final String[] PROJECTION = { 601 TvContract.Programs.COLUMN_CHANNEL_ID, 602 TvContract.RecordedPrograms.COLUMN_RECORDING_DATA_URI, 603 }; 604 605 public RecordedProgram(Cursor cursor) { 606 int index = 0; 607 // mChannelId = cursor.getLong(index++); 608 index++; 609 mDataUri = cursor.getString(index++); 610 } 611 612 public RecordedProgram(long channelId, String dataUri) { 613 // mChannelId = channelId; 614 mDataUri = dataUri; 615 } 616 617 public static RecordedProgram onQuery(Cursor c) { 618 RecordedProgram recording = null; 619 if (c != null && c.moveToNext()) { 620 recording = new RecordedProgram(c); 621 } 622 return recording; 623 } 624 625 public String getDataUri() { 626 return mDataUri; 627 } 628 } 629 630 private RecordedProgram getRecordedProgram(Uri recordedUri) { 631 ContentResolver resolver = mContext.getContentResolver(); 632 try (Cursor c = resolver.query(recordedUri, RecordedProgram.PROJECTION, null, null, null)) { 633 if (c != null) { 634 RecordedProgram result = RecordedProgram.onQuery(c); 635 if (DEBUG) { 636 Log.d(TAG, "Finished query for " + this); 637 } 638 return result; 639 } else { 640 if (c == null) { 641 Log.e(TAG, "Unknown query error for " + this); 642 } else { 643 if (DEBUG) Log.d(TAG, "Canceled query for " + this); 644 } 645 return null; 646 } 647 } 648 } 649 650 private String parseRecording(Uri uri) { 651 RecordedProgram recording = getRecordedProgram(uri); 652 if (recording != null) { 653 return recording.getDataUri(); 654 } 655 return null; 656 } 657 658 @Override 659 public boolean handleMessage(Message msg) { 660 switch (msg.what) { 661 case MSG_TUNE: 662 { 663 if (DEBUG) Log.d(TAG, "MSG_TUNE"); 664 665 // When sequential tuning messages arrived, it skips middle tuning messages in 666 // order 667 // to change to the last requested channel quickly. 668 if (mHandler.hasMessages(MSG_TUNE)) { 669 return true; 670 } 671 notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING); 672 if (!mIsActiveSession) { 673 // Wait until release is finished if there is a pending release. 674 try { 675 while (!sActiveSessionSemaphore.tryAcquire( 676 RELEASE_WAIT_INTERVAL_MS, TimeUnit.MILLISECONDS)) { 677 synchronized (mReleaseLock) { 678 if (mReleaseRequested) { 679 return true; 680 } 681 } 682 } 683 } catch (InterruptedException e) { 684 Thread.currentThread().interrupt(); 685 } 686 synchronized (mReleaseLock) { 687 if (mReleaseRequested) { 688 sActiveSessionSemaphore.release(); 689 return true; 690 } 691 } 692 mIsActiveSession = true; 693 } 694 Uri channelUri = (Uri) msg.obj; 695 String recording = null; 696 long channelId = parseChannel(channelUri); 697 TunerChannel channel = 698 (channelId == -1) ? null : mChannelDataManager.getChannel(channelId); 699 if (channelId == -1) { 700 recording = parseRecording(channelUri); 701 } 702 if (channel == null && recording == null) { 703 Log.w(TAG, "onTune() is failed. Can't find channel for " + channelUri); 704 stopTune(); 705 notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); 706 return true; 707 } 708 clearCallbacksAndMessagesSafely(); 709 mChannelDataManager.removeAllCallbacksAndMessages(); 710 if (channel != null) { 711 if (mTvInputManager.isParentalControlsEnabled() && channel.isLocked()) { 712 Log.i(TAG, "onTune() is failed. Channel is blocked" + channel); 713 mSession.notifyContentBlocked(TvContentRating.UNRATED); 714 return true; 715 } 716 mChannelDataManager.requestProgramsData(channel); 717 } 718 prepareTune(channel, recording); 719 // TODO: Need to refactor. notifyContentAllowed() should not be called if 720 // parental 721 // control is turned on. 722 mSession.notifyContentAllowed(); 723 resetTvTracks(); 724 resetPlayback(); 725 mHandler.sendEmptyMessageDelayed( 726 MSG_RESCHEDULE_PROGRAMS, RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS); 727 return true; 728 } 729 case MSG_STOP_TUNE: 730 { 731 if (DEBUG) Log.d(TAG, "MSG_STOP_TUNE"); 732 mChannel = null; 733 stopPlayback(true); 734 stopCaptionTrack(); 735 resetTvTracks(); 736 notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); 737 return true; 738 } 739 case MSG_RELEASE: 740 { 741 if (DEBUG) Log.d(TAG, "MSG_RELEASE"); 742 mHandler.removeCallbacksAndMessages(null); 743 stopPlayback(true); 744 stopCaptionTrack(); 745 mSourceManager.release(); 746 mHandler.getLooper().quitSafely(); 747 if (mIsActiveSession) { 748 sActiveSessionSemaphore.release(); 749 } 750 return true; 751 } 752 case MSG_RETRY_PLAYBACK: 753 { 754 if (System.identityHashCode(mPlayer) == (int) msg.obj) { 755 Log.i(TAG, "Retrying the playback for channel: " + mChannel); 756 mHandler.removeMessages(MSG_RETRY_PLAYBACK); 757 // When there is a request of retrying playback, don't reuse TunerHal. 758 mSourceManager.setKeepTuneStatus(false); 759 mRetryCount++; 760 if (DEBUG) { 761 Log.d(TAG, "MSG_RETRY_PLAYBACK " + mRetryCount); 762 } 763 mChannelDataManager.removeAllCallbacksAndMessages(); 764 if (mRetryCount <= MAX_IMMEDIATE_RETRY_COUNT) { 765 resetPlayback(); 766 } else { 767 // When it reaches this point, it may be due to an error that occurred 768 // in 769 // the tuner device. Calling stopPlayback() resets the tuner device 770 // to recover from the error. 771 stopPlayback(false); 772 stopCaptionTrack(); 773 774 notifyVideoUnavailable( 775 TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL); 776 Log.i(TAG, "Notify weak signal since fail to retry playback"); 777 778 // After MAX_IMMEDIATE_RETRY_COUNT, give some delay of an empirically 779 // chosen 780 // value before recovering the playback. 781 mHandler.sendEmptyMessageDelayed( 782 MSG_RESET_PLAYBACK, RECOVER_STOPPED_PLAYBACK_PERIOD_MS); 783 } 784 } 785 return true; 786 } 787 case MSG_RESET_PLAYBACK: 788 { 789 if (DEBUG) Log.d(TAG, "MSG_RESET_PLAYBACK"); 790 mChannelDataManager.removeAllCallbacksAndMessages(); 791 resetPlayback(); 792 return true; 793 } 794 case MSG_START_PLAYBACK: 795 { 796 if (DEBUG) Log.d(TAG, "MSG_START_PLAYBACK"); 797 if (mChannel != null || mRecordingId != null) { 798 startPlayback((int) msg.obj); 799 } 800 return true; 801 } 802 case MSG_UPDATE_PROGRAM: 803 { 804 if (mChannel != null) { 805 EitItem program = (EitItem) msg.obj; 806 updateTvTracks(program, false); 807 mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS); 808 } 809 return true; 810 } 811 case MSG_SCHEDULE_OF_PROGRAMS: 812 { 813 mHandler.removeMessages(MSG_UPDATE_PROGRAM); 814 Pair<TunerChannel, List<EitItem>> pair = 815 (Pair<TunerChannel, List<EitItem>>) msg.obj; 816 TunerChannel channel = pair.first; 817 if (mChannel == null) { 818 return true; 819 } 820 if (mChannel != null && mChannel.compareTo(channel) != 0) { 821 return true; 822 } 823 mPrograms = pair.second; 824 EitItem currentProgram = getCurrentProgram(); 825 if (currentProgram == null) { 826 mProgram = null; 827 } 828 long currentTimeMs = getCurrentPosition(); 829 if (mPrograms != null) { 830 for (EitItem item : mPrograms) { 831 if (currentProgram != null && currentProgram.compareTo(item) == 0) { 832 if (DEBUG) { 833 Log.d(TAG, "Update current TvTracks " + item); 834 } 835 if (mProgram != null && mProgram.compareTo(item) == 0) { 836 continue; 837 } 838 mProgram = item; 839 updateTvTracks(item, false); 840 } else if (item.getStartTimeUtcMillis() > currentTimeMs) { 841 if (DEBUG) { 842 Log.d( 843 TAG, 844 "Update next TvTracks " 845 + item 846 + " " 847 + (item.getStartTimeUtcMillis() 848 - currentTimeMs)); 849 } 850 mHandler.sendMessageDelayed( 851 mHandler.obtainMessage(MSG_UPDATE_PROGRAM, item), 852 item.getStartTimeUtcMillis() - currentTimeMs); 853 } 854 } 855 } 856 mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS); 857 return true; 858 } 859 case MSG_UPDATE_CHANNEL_INFO: 860 { 861 TunerChannel channel = (TunerChannel) msg.obj; 862 if (mChannel != null && mChannel.compareTo(channel) == 0) { 863 updateChannelInfo(channel); 864 } 865 return true; 866 } 867 case MSG_PROGRAM_DATA_RESULT: 868 { 869 TunerChannel channel = (TunerChannel) ((Pair) msg.obj).first; 870 871 // If there already exists, skip it since real-time data is a top priority, 872 if (mChannel != null 873 && mChannel.compareTo(channel) == 0 874 && mPrograms == null 875 && mProgram == null) { 876 sendMessage(MSG_SCHEDULE_OF_PROGRAMS, msg.obj); 877 } 878 return true; 879 } 880 case MSG_TRICKPLAY_BY_SEEK: 881 { 882 if (mPlayer == null) { 883 return true; 884 } 885 doTrickplayBySeek(msg.arg1); 886 return true; 887 } 888 case MSG_SMOOTH_TRICKPLAY_MONITOR: 889 { 890 if (mPlayer == null) { 891 return true; 892 } 893 long systemCurrentTime = System.currentTimeMillis(); 894 long position = getCurrentPosition(); 895 if (mRecordingId == null) { 896 // Checks if the position exceeds the upper bound when forwarding, 897 // or exceed the lower bound when rewinding. 898 // If the direction is not checked, there can be some issues. 899 // (See b/29939781 for more details.) 900 if ((position > systemCurrentTime && mPlaybackParams.getSpeed() > 0L) 901 || (position < mBufferStartTimeMs 902 && mPlaybackParams.getSpeed() < 0L)) { 903 doTimeShiftResume(); 904 return true; 905 } 906 } else { 907 if (position > mRecordingDuration || position < 0) { 908 doTimeShiftPause(); 909 return true; 910 } 911 } 912 mHandler.sendEmptyMessageDelayed( 913 MSG_SMOOTH_TRICKPLAY_MONITOR, TRICKPLAY_MONITOR_INTERVAL_MS); 914 return true; 915 } 916 case MSG_RESCHEDULE_PROGRAMS: 917 { 918 if (mHandler.hasMessages(MSG_SCHEDULE_OF_PROGRAMS)) { 919 mHandler.sendEmptyMessage(MSG_RESCHEDULE_PROGRAMS); 920 } else { 921 doReschedulePrograms(); 922 } 923 return true; 924 } 925 case MSG_PARENTAL_CONTROLS: 926 { 927 doParentalControls(); 928 mHandler.removeMessages(MSG_PARENTAL_CONTROLS); 929 mHandler.sendEmptyMessageDelayed( 930 MSG_PARENTAL_CONTROLS, PARENTAL_CONTROLS_INTERVAL_MS); 931 return true; 932 } 933 case MSG_UNBLOCKED_RATING: 934 { 935 mUnblockedContentRating = (TvContentRating) msg.obj; 936 doParentalControls(); 937 mHandler.removeMessages(MSG_PARENTAL_CONTROLS); 938 mHandler.sendEmptyMessageDelayed( 939 MSG_PARENTAL_CONTROLS, PARENTAL_CONTROLS_INTERVAL_MS); 940 return true; 941 } 942 case MSG_DISCOVER_CAPTION_SERVICE_NUMBER: 943 { 944 int serviceNumber = (int) msg.obj; 945 doDiscoverCaptionServiceNumber(serviceNumber); 946 return true; 947 } 948 case MSG_SELECT_TRACK: 949 { 950 if (mPlayer == null) { 951 Log.w(TAG, "mPlayer is null when doselectTrack is called"); 952 return false; 953 } 954 if (mChannel != null || mRecordingId != null) { 955 doSelectTrack(msg.arg1, (String) msg.obj); 956 } 957 return true; 958 } 959 case MSG_UPDATE_CAPTION_TRACK: 960 { 961 if (mCaptionEnabled) { 962 startCaptionTrack(); 963 } else { 964 stopCaptionTrack(); 965 } 966 return true; 967 } 968 case MSG_TIMESHIFT_PAUSE: 969 { 970 if (DEBUG) Log.d(TAG, "MSG_TIMESHIFT_PAUSE"); 971 if (mPlayer == null) { 972 return true; 973 } 974 setTrickplayEnabledIfNeeded(); 975 doTimeShiftPause(); 976 return true; 977 } 978 case MSG_TIMESHIFT_RESUME: 979 { 980 if (DEBUG) Log.d(TAG, "MSG_TIMESHIFT_RESUME"); 981 if (mPlayer == null) { 982 return true; 983 } 984 setTrickplayEnabledIfNeeded(); 985 doTimeShiftResume(); 986 return true; 987 } 988 case MSG_TIMESHIFT_SEEK_TO: 989 { 990 long position = (long) msg.obj; 991 if (DEBUG) Log.d(TAG, "MSG_TIMESHIFT_SEEK_TO (position=" + position + ")"); 992 if (mPlayer == null) { 993 return true; 994 } 995 setTrickplayEnabledIfNeeded(); 996 doTimeShiftSeekTo(position); 997 return true; 998 } 999 case MSG_TIMESHIFT_SET_PLAYBACKPARAMS: 1000 { 1001 if (mPlayer == null) { 1002 return true; 1003 } 1004 setTrickplayEnabledIfNeeded(); 1005 doTimeShiftSetPlaybackParams((PlaybackParams) msg.obj); 1006 return true; 1007 } 1008 case MSG_AUDIO_CAPABILITIES_CHANGED: 1009 { 1010 AudioCapabilities capabilities = (AudioCapabilities) msg.obj; 1011 if (DEBUG) { 1012 Log.d(TAG, "MSG_AUDIO_CAPABILITIES_CHANGED " + capabilities); 1013 } 1014 if (capabilities == null) { 1015 return true; 1016 } 1017 if (!capabilities.equals(mAudioCapabilities)) { 1018 // HDMI supported encodings are changed. restart player. 1019 mAudioCapabilities = capabilities; 1020 resetPlayback(); 1021 } 1022 return true; 1023 } 1024 case MSG_SET_STREAM_VOLUME: 1025 { 1026 if (mPlayer != null && mPlayer.isPlaying()) { 1027 mPlayer.setVolume(mVolume); 1028 } 1029 return true; 1030 } 1031 case MSG_TUNER_PREFERENCES_CHANGED: 1032 { 1033 mHandler.removeMessages(MSG_TUNER_PREFERENCES_CHANGED); 1034 @TrickplaySetting 1035 int trickplaySetting = TunerPreferences.getTrickplaySetting(mContext); 1036 if (trickplaySetting != mTrickplaySetting) { 1037 boolean wasTrcikplayEnabled = 1038 mTrickplaySetting != TunerPreferences.TRICKPLAY_SETTING_DISABLED; 1039 boolean isTrickplayEnabled = 1040 trickplaySetting != TunerPreferences.TRICKPLAY_SETTING_DISABLED; 1041 mTrickplaySetting = trickplaySetting; 1042 if (isTrickplayEnabled != wasTrcikplayEnabled) { 1043 sendMessage(MSG_RESET_PLAYBACK, System.identityHashCode(mPlayer)); 1044 } 1045 } 1046 return true; 1047 } 1048 case MSG_BUFFER_START_TIME_CHANGED: 1049 { 1050 if (mPlayer == null) { 1051 return true; 1052 } 1053 mBufferStartTimeMs = (long) msg.obj; 1054 if (!hasEnoughBackwardBuffer() 1055 && (!mPlayer.isPlaying() || mPlaybackParams.getSpeed() < 1.0f)) { 1056 mPlayer.setPlayWhenReady(true); 1057 mPlayer.setAudioTrackAndClosedCaption(true); 1058 mPlaybackParams.setSpeed(1.0f); 1059 } 1060 return true; 1061 } 1062 case MSG_BUFFER_STATE_CHANGED: 1063 { 1064 boolean available = (boolean) msg.obj; 1065 mSession.notifyTimeShiftStatusChanged( 1066 available 1067 ? TvInputManager.TIME_SHIFT_STATUS_AVAILABLE 1068 : TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE); 1069 return true; 1070 } 1071 case MSG_CHECK_SIGNAL: 1072 if (mChannel == null || mPlayer == null) { 1073 return true; 1074 } 1075 TsDataSource source = mPlayer.getDataSource(); 1076 long limitInBytes = source != null ? source.getBufferedPosition() : 0L; 1077 if (TunerDebug.ENABLED) { 1078 TunerDebug.calculateDiff(); 1079 mSession.sendUiMessage( 1080 TunerSession.MSG_UI_SET_STATUS_TEXT, 1081 Html.fromHtml( 1082 StatusTextUtils.getStatusWarningInHTML( 1083 (limitInBytes - mLastLimitInBytes) / TS_PACKET_SIZE, 1084 TunerDebug.getVideoFrameDrop(), 1085 TunerDebug.getBytesInQueue(), 1086 TunerDebug.getAudioPositionUs(), 1087 TunerDebug.getAudioPositionUsRate(), 1088 TunerDebug.getAudioPtsUs(), 1089 TunerDebug.getAudioPtsUsRate(), 1090 TunerDebug.getVideoPtsUs(), 1091 TunerDebug.getVideoPtsUsRate()))); 1092 } 1093 mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_MESSAGE); 1094 long currentTime = SystemClock.elapsedRealtime(); 1095 long bufferingTimeMs = 1096 mBufferingStartTimeMs != INVALID_TIME 1097 ? currentTime - mBufferingStartTimeMs 1098 : mBufferingStartTimeMs; 1099 long preparingTimeMs = 1100 mPreparingStartTimeMs != INVALID_TIME 1101 ? currentTime - mPreparingStartTimeMs 1102 : mPreparingStartTimeMs; 1103 boolean isBufferingTooLong = 1104 bufferingTimeMs > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS; 1105 boolean isPreparingTooLong = 1106 preparingTimeMs > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS; 1107 boolean isWeakSignal = 1108 source != null 1109 && mChannel.getType() != Channel.TunerType.TYPE_FILE 1110 && (isBufferingTooLong || isPreparingTooLong); 1111 if (isWeakSignal && !mReportedWeakSignal) { 1112 if (!mHandler.hasMessages(MSG_RETRY_PLAYBACK)) { 1113 mHandler.sendMessageDelayed( 1114 mHandler.obtainMessage( 1115 MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)), 1116 PLAYBACK_RETRY_DELAY_MS); 1117 } 1118 if (mPlayer != null) { 1119 mPlayer.setAudioTrackAndClosedCaption(false); 1120 } 1121 notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL); 1122 Log.i( 1123 TAG, 1124 "Notify weak signal due to signal check, " 1125 + String.format( 1126 "packetsPerSec:%d, bufferingTimeMs:%d, preparingTimeMs:%d, " 1127 + "videoFrameDrop:%d", 1128 (limitInBytes - mLastLimitInBytes) / TS_PACKET_SIZE, 1129 bufferingTimeMs, 1130 preparingTimeMs, 1131 TunerDebug.getVideoFrameDrop())); 1132 } else if (!isWeakSignal && mReportedWeakSignal) { 1133 boolean isPlaybackStable = 1134 mReadyStartTimeMs != INVALID_TIME 1135 && currentTime - mReadyStartTimeMs 1136 > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS; 1137 if (!isPlaybackStable) { 1138 // Wait until playback becomes stable. 1139 } else if (mReportedDrawnToSurface) { 1140 mHandler.removeMessages(MSG_RETRY_PLAYBACK); 1141 notifyVideoAvailable(); 1142 mPlayer.setAudioTrackAndClosedCaption(true); 1143 } 1144 } 1145 mLastLimitInBytes = limitInBytes; 1146 mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_PERIOD_MS); 1147 return true; 1148 case MSG_SET_SURFACE: 1149 { 1150 if (mPlayer != null) { 1151 mPlayer.setSurface(mSurface); 1152 } else { 1153 // TODO: Since surface is dynamically set, we can remove the dependency of 1154 // playback start on mSurface nullity. 1155 resetPlayback(); 1156 } 1157 return true; 1158 } 1159 case MSG_NOTIFY_AUDIO_TRACK_UPDATED: 1160 { 1161 notifyAudioTracksUpdated(); 1162 return true; 1163 } 1164 default: 1165 { 1166 Log.w(TAG, "Unhandled message code: " + msg.what); 1167 return false; 1168 } 1169 } 1170 } 1171 1172 // Private methods 1173 private void doSelectTrack(int type, String trackId) { 1174 int numTrackId = 1175 trackId != null ? Integer.parseInt(trackId.substring(TRACK_PREFIX_SIZE)) : -1; 1176 if (type == TvTrackInfo.TYPE_AUDIO) { 1177 if (trackId == null) { 1178 return; 1179 } 1180 if (numTrackId != mPlayer.getSelectedTrack(MpegTsPlayer.TRACK_TYPE_AUDIO)) { 1181 mPlayer.setSelectedTrack(MpegTsPlayer.TRACK_TYPE_AUDIO, numTrackId); 1182 } 1183 mSession.notifyTrackSelected(type, trackId); 1184 } else if (type == TvTrackInfo.TYPE_SUBTITLE) { 1185 if (trackId == null) { 1186 mSession.notifyTrackSelected(type, null); 1187 mCaptionTrack = null; 1188 stopCaptionTrack(); 1189 return; 1190 } 1191 for (TvTrackInfo track : mTvTracks) { 1192 if (track.getId().equals(trackId)) { 1193 // The service number of the caption service is used for track id of a 1194 // subtitle track. Passes the following track id on to TsParser. 1195 mSession.notifyTrackSelected(type, trackId); 1196 mCaptionTrack = mCaptionTrackMap.get(numTrackId); 1197 startCaptionTrack(); 1198 return; 1199 } 1200 } 1201 } 1202 } 1203 1204 private void setTrickplayEnabledIfNeeded() { 1205 if (mChannel == null 1206 || mTrickplayModeCustomization != CustomizationManager.TRICKPLAY_MODE_ENABLED) { 1207 return; 1208 } 1209 if (mTrickplaySetting == TunerPreferences.TRICKPLAY_SETTING_NOT_SET) { 1210 mTrickplaySetting = TunerPreferences.TRICKPLAY_SETTING_ENABLED; 1211 TunerPreferences.setTrickplaySetting(mContext, mTrickplaySetting); 1212 } 1213 } 1214 1215 private MpegTsPlayer createPlayer(AudioCapabilities capabilities) { 1216 if (capabilities == null) { 1217 Log.w(TAG, "No Audio Capabilities"); 1218 } 1219 long now = System.currentTimeMillis(); 1220 if (mTrickplayModeCustomization == CustomizationManager.TRICKPLAY_MODE_ENABLED 1221 && mTrickplaySetting == TunerPreferences.TRICKPLAY_SETTING_NOT_SET) { 1222 if (mTrickplayExpiredMs == 0) { 1223 mTrickplayExpiredMs = now + TRICKPLAY_OFF_DURATION_MS; 1224 TunerPreferences.setTrickplayExpiredMs(mContext, mTrickplayExpiredMs); 1225 } else { 1226 if (mTrickplayExpiredMs < now) { 1227 mTrickplaySetting = TunerPreferences.TRICKPLAY_SETTING_DISABLED; 1228 TunerPreferences.setTrickplaySetting(mContext, mTrickplaySetting); 1229 } 1230 } 1231 } 1232 BufferManager bufferManager = null; 1233 if (mRecordingId != null) { 1234 StorageManager storageManager = 1235 new DvrStorageManager(new File(getRecordingPath()), false); 1236 bufferManager = new BufferManager(storageManager); 1237 updateCaptionTracks(((DvrStorageManager) storageManager).readCaptionInfoFiles()); 1238 } else if (!mTrickplayDisabledByStorageIssue 1239 && mTrickplaySetting != TunerPreferences.TRICKPLAY_SETTING_DISABLED 1240 && mMaxTrickplayBufferSizeMb >= MIN_BUFFER_SIZE_DEF) { 1241 bufferManager = 1242 new BufferManager( 1243 new TrickplayStorageManager( 1244 mContext, 1245 mTrickplayBufferDir, 1246 1024L * 1024 * mMaxTrickplayBufferSizeMb)); 1247 } else { 1248 Log.w(TAG, "Trickplay is disabled."); 1249 } 1250 MpegTsPlayer player = 1251 new MpegTsPlayer( 1252 new MpegTsRendererBuilder(mContext, bufferManager, this), 1253 mHandler, 1254 mSourceManager, 1255 capabilities, 1256 this); 1257 Log.i(TAG, "Passthrough AC3 renderer"); 1258 if (DEBUG) Log.d(TAG, "ExoPlayer created"); 1259 return player; 1260 } 1261 1262 private void startCaptionTrack() { 1263 if (mCaptionEnabled && mCaptionTrack != null) { 1264 mSession.sendUiMessage(TunerSession.MSG_UI_START_CAPTION_TRACK, mCaptionTrack); 1265 if (mPlayer != null) { 1266 mPlayer.setCaptionServiceNumber(mCaptionTrack.serviceNumber); 1267 } 1268 } 1269 } 1270 1271 private void stopCaptionTrack() { 1272 if (mPlayer != null) { 1273 mPlayer.setCaptionServiceNumber(Cea708Data.EMPTY_SERVICE_NUMBER); 1274 } 1275 mSession.sendUiMessage(TunerSession.MSG_UI_STOP_CAPTION_TRACK); 1276 } 1277 1278 private void resetTvTracks() { 1279 mTvTracks.clear(); 1280 mAudioTrackMap.clear(); 1281 mCaptionTrackMap.clear(); 1282 mSession.sendUiMessage(TunerSession.MSG_UI_RESET_CAPTION_TRACK); 1283 mSession.notifyTracksChanged(mTvTracks); 1284 } 1285 1286 private void updateTvTracks(TvTracksInterface tvTracksInterface, boolean fromPmt) { 1287 synchronized (tvTracksInterface) { 1288 if (DEBUG) { 1289 Log.d(TAG, "UpdateTvTracks " + tvTracksInterface); 1290 } 1291 List<AtscAudioTrack> audioTracks = tvTracksInterface.getAudioTracks(); 1292 List<AtscCaptionTrack> captionTracks = tvTracksInterface.getCaptionTracks(); 1293 // According to ATSC A/69 chapter 6.9, both PMT and EIT should have descriptors for 1294 // audio 1295 // tracks, but in real world, we see some bogus audio track info in EIT, so, we trust 1296 // audio 1297 // track info in PMT more and use info in EIT only when we have nothing. 1298 if (audioTracks != null 1299 && !audioTracks.isEmpty() 1300 && (mChannel == null || mChannel.getAudioTracks() == null || fromPmt)) { 1301 updateAudioTracks(audioTracks); 1302 } 1303 if (captionTracks == null || captionTracks.isEmpty()) { 1304 if (tvTracksInterface.hasCaptionTrack()) { 1305 updateCaptionTracks(captionTracks); 1306 } 1307 } else { 1308 updateCaptionTracks(captionTracks); 1309 } 1310 } 1311 } 1312 1313 private void removeTvTracks(int trackType) { 1314 Iterator<TvTrackInfo> iterator = mTvTracks.iterator(); 1315 while (iterator.hasNext()) { 1316 TvTrackInfo tvTrackInfo = iterator.next(); 1317 if (tvTrackInfo.getType() == trackType) { 1318 iterator.remove(); 1319 } 1320 } 1321 } 1322 1323 private void updateVideoTrack(int width, int height) { 1324 removeTvTracks(TvTrackInfo.TYPE_VIDEO); 1325 mTvTracks.add( 1326 new TvTrackInfo.Builder(TvTrackInfo.TYPE_VIDEO, VIDEO_TRACK_ID) 1327 .setVideoWidth(width) 1328 .setVideoHeight(height) 1329 .build()); 1330 mSession.notifyTracksChanged(mTvTracks); 1331 mSession.notifyTrackSelected(TvTrackInfo.TYPE_VIDEO, VIDEO_TRACK_ID); 1332 } 1333 1334 private void updateAudioTracks(List<AtscAudioTrack> audioTracks) { 1335 if (DEBUG) { 1336 Log.d(TAG, "Update AudioTracks " + audioTracks); 1337 } 1338 mAudioTrackMap.clear(); 1339 if (audioTracks != null) { 1340 int index = 0; 1341 for (AtscAudioTrack audioTrack : audioTracks) { 1342 audioTrack.index = index; 1343 mAudioTrackMap.put(index, audioTrack); 1344 ++index; 1345 } 1346 } 1347 mHandler.sendEmptyMessage(MSG_NOTIFY_AUDIO_TRACK_UPDATED); 1348 } 1349 1350 private void notifyAudioTracksUpdated() { 1351 if (mPlayer == null) { 1352 // Audio tracks will be updated later once player initialization is done. 1353 return; 1354 } 1355 int audioTrackCount = mPlayer.getTrackCount(MpegTsPlayer.TRACK_TYPE_AUDIO); 1356 removeTvTracks(TvTrackInfo.TYPE_AUDIO); 1357 for (int i = 0; i < audioTrackCount; i++) { 1358 // We use language information from EIT/VCT only when the player does not provide 1359 // languages. 1360 com.google.android.exoplayer.MediaFormat infoFromPlayer = 1361 mPlayer.getTrackFormat(MpegTsPlayer.TRACK_TYPE_AUDIO, i); 1362 AtscAudioTrack infoFromEit = mAudioTrackMap.get(i); 1363 AtscAudioTrack infoFromVct = 1364 (mChannel != null 1365 && mChannel.getAudioTracks().size() == mAudioTrackMap.size() 1366 && i < mChannel.getAudioTracks().size()) 1367 ? mChannel.getAudioTracks().get(i) 1368 : null; 1369 String language = 1370 !TextUtils.isEmpty(infoFromPlayer.language) 1371 ? infoFromPlayer.language 1372 : (infoFromEit != null && infoFromEit.language != null) 1373 ? infoFromEit.language 1374 : (infoFromVct != null && infoFromVct.language != null) 1375 ? infoFromVct.language 1376 : null; 1377 TvTrackInfo.Builder builder = 1378 new TvTrackInfo.Builder(TvTrackInfo.TYPE_AUDIO, AUDIO_TRACK_PREFIX + i); 1379 builder.setLanguage(language); 1380 builder.setAudioChannelCount(infoFromPlayer.channelCount); 1381 builder.setAudioSampleRate(infoFromPlayer.sampleRate); 1382 TvTrackInfo track = builder.build(); 1383 mTvTracks.add(track); 1384 } 1385 mSession.notifyTracksChanged(mTvTracks); 1386 } 1387 1388 private void updateCaptionTracks(List<AtscCaptionTrack> captionTracks) { 1389 if (DEBUG) { 1390 Log.d(TAG, "Update CaptionTrack " + captionTracks); 1391 } 1392 removeTvTracks(TvTrackInfo.TYPE_SUBTITLE); 1393 mCaptionTrackMap.clear(); 1394 if (captionTracks != null) { 1395 for (AtscCaptionTrack captionTrack : captionTracks) { 1396 if (mCaptionTrackMap.indexOfKey(captionTrack.serviceNumber) >= 0) { 1397 continue; 1398 } 1399 String language = captionTrack.language; 1400 1401 // The service number of the caption service is used for track id of a subtitle. 1402 // Later, when a subtitle is chosen, track id will be passed on to TsParser. 1403 TvTrackInfo.Builder builder = 1404 new TvTrackInfo.Builder( 1405 TvTrackInfo.TYPE_SUBTITLE, 1406 SUBTITLE_TRACK_PREFIX + captionTrack.serviceNumber); 1407 builder.setLanguage(language); 1408 mTvTracks.add(builder.build()); 1409 mCaptionTrackMap.put(captionTrack.serviceNumber, captionTrack); 1410 } 1411 } 1412 mSession.notifyTracksChanged(mTvTracks); 1413 } 1414 1415 private void updateChannelInfo(TunerChannel channel) { 1416 if (DEBUG) { 1417 Log.d( 1418 TAG, 1419 String.format( 1420 "Channel Info (old) videoPid: %d audioPid: %d " + "audioSize: %d", 1421 mChannel.getVideoPid(), 1422 mChannel.getAudioPid(), 1423 mChannel.getAudioPids().size())); 1424 } 1425 1426 // The list of the audio tracks resided in a channel is often changed depending on a 1427 // program being on the air. So, we should update the streaming PIDs and types of the 1428 // tuned channel according to the newly received channel data. 1429 int oldVideoPid = mChannel.getVideoPid(); 1430 int oldAudioPid = mChannel.getAudioPid(); 1431 List<Integer> audioPids = channel.getAudioPids(); 1432 List<Integer> audioStreamTypes = channel.getAudioStreamTypes(); 1433 int size = audioPids.size(); 1434 mChannel.setVideoPid(channel.getVideoPid()); 1435 mChannel.setAudioPids(audioPids); 1436 mChannel.setAudioStreamTypes(audioStreamTypes); 1437 updateTvTracks(channel, true); 1438 int index = audioPids.isEmpty() ? -1 : 0; 1439 for (int i = 0; i < size; ++i) { 1440 if (audioPids.get(i) == oldAudioPid) { 1441 index = i; 1442 break; 1443 } 1444 } 1445 mChannel.selectAudioTrack(index); 1446 mSession.notifyTrackSelected( 1447 TvTrackInfo.TYPE_AUDIO, index == -1 ? null : AUDIO_TRACK_PREFIX + index); 1448 1449 // Reset playback if there is a change in the listening streaming PIDs. 1450 if (oldVideoPid != mChannel.getVideoPid() || oldAudioPid != mChannel.getAudioPid()) { 1451 // TODO: Implement a switching between tracks more smoothly. 1452 resetPlayback(); 1453 } 1454 if (DEBUG) { 1455 Log.d( 1456 TAG, 1457 String.format( 1458 "Channel Info (new) videoPid: %d audioPid: %d " + " audioSize: %d", 1459 mChannel.getVideoPid(), 1460 mChannel.getAudioPid(), 1461 mChannel.getAudioPids().size())); 1462 } 1463 } 1464 1465 private void stopPlayback(boolean removeChannelDataCallbacks) { 1466 if (removeChannelDataCallbacks) { 1467 mChannelDataManager.removeAllCallbacksAndMessages(); 1468 } 1469 if (mPlayer != null) { 1470 mPlayer.setPlayWhenReady(false); 1471 mPlayer.release(); 1472 mPlayer = null; 1473 mPlayerState = ExoPlayer.STATE_IDLE; 1474 mPlaybackParams.setSpeed(1.0f); 1475 mPlayerStarted = false; 1476 mReportedDrawnToSurface = false; 1477 mPreparingStartTimeMs = INVALID_TIME; 1478 mBufferingStartTimeMs = INVALID_TIME; 1479 mReadyStartTimeMs = INVALID_TIME; 1480 mLastLimitInBytes = 0L; 1481 mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_AUDIO_UNPLAYABLE); 1482 mSession.notifyTimeShiftStatusChanged(TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE); 1483 } 1484 } 1485 1486 private void startPlayback(int playerHashCode) { 1487 // TODO: provide hasAudio()/hasVideo() for play recordings. 1488 if (mPlayer == null || System.identityHashCode(mPlayer) != playerHashCode) { 1489 return; 1490 } 1491 if (mChannel != null && !mChannel.hasAudio()) { 1492 if (DEBUG) Log.d(TAG, "Channel " + mChannel + " does not have audio."); 1493 // Playbacks with video-only stream have not been tested yet. 1494 // No video-only channel has been found. 1495 notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); 1496 return; 1497 } 1498 if (mChannel != null 1499 && ((mChannel.hasAudio() && !mPlayer.hasAudio()) 1500 || (mChannel.hasVideo() && !mPlayer.hasVideo())) 1501 && mChannel.getType() != Channel.TunerType.TYPE_NETWORK) { 1502 // If the channel is from network, skip this part since the video and audio tracks 1503 // information for channels from network are more reliable in the extractor. Otherwise, 1504 // tracks haven't been detected in the extractor. Try again. 1505 sendMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)); 1506 return; 1507 } 1508 // Since mSurface is volatile, we define a local variable surface to keep the same value 1509 // inside this method. 1510 Surface surface = mSurface; 1511 if (surface != null && !mPlayerStarted) { 1512 mPlayer.setSurface(surface); 1513 mPlayer.setPlayWhenReady(true); 1514 mPlayer.setVolume(mVolume); 1515 if (mChannel != null && mPlayer.hasAudio() && !mPlayer.hasVideo()) { 1516 notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY); 1517 } else if (!mReportedWeakSignal) { 1518 // Doesn't show buffering during weak signal. 1519 notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING); 1520 } 1521 mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_MESSAGE); 1522 mPlayerStarted = true; 1523 } 1524 } 1525 1526 private void preparePlayback() { 1527 SoftPreconditions.checkState(mPlayer == null); 1528 if (mChannel == null && mRecordingId == null) { 1529 return; 1530 } 1531 mSourceManager.setKeepTuneStatus(true); 1532 MpegTsPlayer player = createPlayer(mAudioCapabilities); 1533 player.setCaptionServiceNumber(Cea708Data.EMPTY_SERVICE_NUMBER); 1534 player.setVideoEventListener(this); 1535 player.setCaptionServiceNumber( 1536 mCaptionTrack != null 1537 ? mCaptionTrack.serviceNumber 1538 : Cea708Data.EMPTY_SERVICE_NUMBER); 1539 if (!player.prepare(mContext, mChannel, mHasSoftwareAudioDecoder, this)) { 1540 mSourceManager.setKeepTuneStatus(false); 1541 player.release(); 1542 if (!mHandler.hasMessages(MSG_TUNE)) { 1543 // When prepare failed, there may be some errors related to hardware. In that 1544 // case, retry playback immediately may not help. 1545 notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL); 1546 Log.i(TAG, "Notify weak signal due to player preparation failure"); 1547 mHandler.sendMessageDelayed( 1548 mHandler.obtainMessage( 1549 MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)), 1550 PLAYBACK_RETRY_DELAY_MS); 1551 } 1552 } else { 1553 mPlayer = player; 1554 mPlayerStarted = false; 1555 mHandler.removeMessages(MSG_CHECK_SIGNAL); 1556 mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_INITIAL_DELAY_MS); 1557 } 1558 } 1559 1560 private void resetPlayback() { 1561 long timestamp; 1562 long oldTimestamp; 1563 timestamp = SystemClock.elapsedRealtime(); 1564 stopPlayback(false); 1565 stopCaptionTrack(); 1566 if (ENABLE_PROFILER) { 1567 oldTimestamp = timestamp; 1568 timestamp = SystemClock.elapsedRealtime(); 1569 Log.i(TAG, "[Profiler] stopPlayback() takes " + (timestamp - oldTimestamp) + " ms"); 1570 } 1571 if (mChannelBlocked || mSurface == null) { 1572 return; 1573 } 1574 preparePlayback(); 1575 } 1576 1577 private void prepareTune(TunerChannel channel, String recording) { 1578 mChannelBlocked = false; 1579 mUnblockedContentRating = null; 1580 mRetryCount = 0; 1581 mChannel = channel; 1582 mRecordingId = recording; 1583 mRecordingDuration = recording != null ? getDurationForRecording(recording) : null; 1584 mProgram = null; 1585 mPrograms = null; 1586 if (mRecordingId != null) { 1587 // Workaround of b/33298048: set it to 1 instead of 0. 1588 mBufferStartTimeMs = mRecordStartTimeMs = 1; 1589 } else { 1590 mBufferStartTimeMs = mRecordStartTimeMs = System.currentTimeMillis(); 1591 } 1592 mLastPositionMs = 0; 1593 mCaptionTrack = null; 1594 mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS); 1595 } 1596 1597 private void doReschedulePrograms() { 1598 long currentPositionMs = getCurrentPosition(); 1599 long forwardDifference = 1600 Math.abs(currentPositionMs - mLastPositionMs - RESCHEDULE_PROGRAMS_INTERVAL_MS); 1601 mLastPositionMs = currentPositionMs; 1602 1603 // A gap is measured as the time difference between previous and next current position 1604 // periodically. If the gap has a significant difference with an interval of a period, 1605 // this means that there is a change of playback status and the programs of the current 1606 // channel should be rescheduled to new playback timeline. 1607 if (forwardDifference > RESCHEDULE_PROGRAMS_TOLERANCE_MS) { 1608 if (DEBUG) { 1609 Log.d( 1610 TAG, 1611 "reschedule programs size:" 1612 + (mPrograms != null ? mPrograms.size() : 0) 1613 + " current program: " 1614 + getCurrentProgram()); 1615 } 1616 mHandler.obtainMessage(MSG_SCHEDULE_OF_PROGRAMS, new Pair<>(mChannel, mPrograms)) 1617 .sendToTarget(); 1618 } 1619 mHandler.removeMessages(MSG_RESCHEDULE_PROGRAMS); 1620 mHandler.sendEmptyMessageDelayed(MSG_RESCHEDULE_PROGRAMS, RESCHEDULE_PROGRAMS_INTERVAL_MS); 1621 } 1622 1623 private int getTrickPlaySeekIntervalMs() { 1624 return Math.max( 1625 EXPECTED_KEY_FRAME_INTERVAL_MS / (int) Math.abs(mPlaybackParams.getSpeed()), 1626 MIN_TRICKPLAY_SEEK_INTERVAL_MS); 1627 } 1628 1629 @SuppressWarnings("NarrowingCompoundAssignment") 1630 private void doTrickplayBySeek(int seekPositionMs) { 1631 mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK); 1632 if (mPlaybackParams.getSpeed() == 1.0f || !mPlayer.isPrepared()) { 1633 return; 1634 } 1635 if (seekPositionMs < mBufferStartTimeMs - mRecordStartTimeMs) { 1636 if (mPlaybackParams.getSpeed() > 1.0f) { 1637 // If fast forwarding, the seekPositionMs can be out of the buffered range 1638 // because of chuck evictions. 1639 seekPositionMs = (int) (mBufferStartTimeMs - mRecordStartTimeMs); 1640 } else { 1641 mPlayer.seekTo(mBufferStartTimeMs - mRecordStartTimeMs); 1642 mPlaybackParams.setSpeed(1.0f); 1643 mPlayer.setAudioTrackAndClosedCaption(true); 1644 return; 1645 } 1646 } else if (seekPositionMs > System.currentTimeMillis() - mRecordStartTimeMs) { 1647 // Stops trickplay when FF requested the position later than current position. 1648 // If RW trickplay requested the position later than current position, 1649 // continue trickplay. 1650 if (mPlaybackParams.getSpeed() > 0.0f) { 1651 mPlayer.seekTo(System.currentTimeMillis() - mRecordStartTimeMs); 1652 mPlaybackParams.setSpeed(1.0f); 1653 mPlayer.setAudioTrackAndClosedCaption(true); 1654 return; 1655 } 1656 } 1657 1658 long delayForNextSeek = getTrickPlaySeekIntervalMs(); 1659 if (!mPlayer.isBuffering()) { 1660 mPlayer.seekTo(seekPositionMs); 1661 } else { 1662 delayForNextSeek = MIN_TRICKPLAY_SEEK_INTERVAL_MS; 1663 } 1664 seekPositionMs += mPlaybackParams.getSpeed() * delayForNextSeek; 1665 mHandler.sendMessageDelayed( 1666 mHandler.obtainMessage(MSG_TRICKPLAY_BY_SEEK, seekPositionMs, 0), delayForNextSeek); 1667 } 1668 1669 private void doTimeShiftPause() { 1670 mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR); 1671 mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK); 1672 if (!hasEnoughBackwardBuffer()) { 1673 return; 1674 } 1675 mPlaybackParams.setSpeed(1.0f); 1676 mPlayer.setPlayWhenReady(false); 1677 mPlayer.setAudioTrackAndClosedCaption(true); 1678 } 1679 1680 private void doTimeShiftResume() { 1681 mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR); 1682 mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK); 1683 mPlaybackParams.setSpeed(1.0f); 1684 mPlayer.setPlayWhenReady(true); 1685 mPlayer.setAudioTrackAndClosedCaption(true); 1686 } 1687 1688 private void doTimeShiftSeekTo(long timeMs) { 1689 mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR); 1690 mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK); 1691 mPlayer.seekTo((int) (timeMs - mRecordStartTimeMs)); 1692 } 1693 1694 private void doTimeShiftSetPlaybackParams(PlaybackParams params) { 1695 if (!hasEnoughBackwardBuffer() && params.getSpeed() < 1.0f) { 1696 return; 1697 } 1698 mPlaybackParams = params; 1699 float speed = mPlaybackParams.getSpeed(); 1700 if (speed == 1.0f) { 1701 mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR); 1702 mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK); 1703 doTimeShiftResume(); 1704 } else if (mPlayer.supportSmoothTrickPlay(speed)) { 1705 mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK); 1706 mPlayer.setAudioTrackAndClosedCaption(false); 1707 mPlayer.startSmoothTrickplay(mPlaybackParams); 1708 mHandler.sendEmptyMessageDelayed( 1709 MSG_SMOOTH_TRICKPLAY_MONITOR, TRICKPLAY_MONITOR_INTERVAL_MS); 1710 } else { 1711 mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR); 1712 if (!mHandler.hasMessages(MSG_TRICKPLAY_BY_SEEK)) { 1713 mPlayer.setAudioTrackAndClosedCaption(false); 1714 mPlayer.setPlayWhenReady(false); 1715 // Initiate trickplay 1716 mHandler.sendMessage( 1717 mHandler.obtainMessage( 1718 MSG_TRICKPLAY_BY_SEEK, 1719 (int) 1720 (mPlayer.getCurrentPosition() 1721 + speed * getTrickPlaySeekIntervalMs()), 1722 0)); 1723 } 1724 } 1725 } 1726 1727 private EitItem getCurrentProgram() { 1728 if (mPrograms == null || mPrograms.isEmpty()) { 1729 return null; 1730 } 1731 if (mChannel.getType() == Channel.TunerType.TYPE_FILE) { 1732 // For the playback from the local file, we use the first one from the given program. 1733 EitItem first = mPrograms.get(0); 1734 if (first != null 1735 && (mProgram == null 1736 || first.getStartTimeUtcMillis() < mProgram.getStartTimeUtcMillis())) { 1737 return first; 1738 } 1739 return null; 1740 } 1741 long currentTimeMs = getCurrentPosition(); 1742 for (EitItem item : mPrograms) { 1743 if (item.getStartTimeUtcMillis() <= currentTimeMs 1744 && item.getEndTimeUtcMillis() >= currentTimeMs) { 1745 return item; 1746 } 1747 } 1748 return null; 1749 } 1750 1751 private void doParentalControls() { 1752 boolean isParentalControlsEnabled = mTvInputManager.isParentalControlsEnabled(); 1753 if (isParentalControlsEnabled) { 1754 TvContentRating blockContentRating = getContentRatingOfCurrentProgramBlocked(); 1755 if (DEBUG) { 1756 if (blockContentRating != null) { 1757 Log.d( 1758 TAG, 1759 "Check parental controls: blocked by content rating - " 1760 + blockContentRating); 1761 } else { 1762 Log.d(TAG, "Check parental controls: available"); 1763 } 1764 } 1765 updateChannelBlockStatus(blockContentRating != null, blockContentRating); 1766 } else { 1767 if (DEBUG) { 1768 Log.d(TAG, "Check parental controls: available"); 1769 } 1770 updateChannelBlockStatus(false, null); 1771 } 1772 } 1773 1774 private void doDiscoverCaptionServiceNumber(int serviceNumber) { 1775 int index = mCaptionTrackMap.indexOfKey(serviceNumber); 1776 if (index < 0) { 1777 AtscCaptionTrack captionTrack = new AtscCaptionTrack(); 1778 captionTrack.serviceNumber = serviceNumber; 1779 captionTrack.wideAspectRatio = false; 1780 captionTrack.easyReader = false; 1781 mCaptionTrackMap.put(serviceNumber, captionTrack); 1782 mTvTracks.add( 1783 new TvTrackInfo.Builder( 1784 TvTrackInfo.TYPE_SUBTITLE, 1785 SUBTITLE_TRACK_PREFIX + serviceNumber) 1786 .build()); 1787 mSession.notifyTracksChanged(mTvTracks); 1788 } 1789 } 1790 1791 private TvContentRating getContentRatingOfCurrentProgramBlocked() { 1792 EitItem currentProgram = getCurrentProgram(); 1793 if (currentProgram == null) { 1794 return null; 1795 } 1796 TvContentRating[] ratings = 1797 mTvContentRatingCache.getRatings(currentProgram.getContentRating()); 1798 if (ratings == null || ratings.length == 0) { 1799 ratings = new TvContentRating[] {TvContentRating.UNRATED}; 1800 } 1801 for (TvContentRating rating : ratings) { 1802 if (!Objects.equals(mUnblockedContentRating, rating) 1803 && mTvInputManager.isRatingBlocked(rating)) { 1804 return rating; 1805 } 1806 } 1807 return null; 1808 } 1809 1810 private void updateChannelBlockStatus(boolean channelBlocked, TvContentRating contentRating) { 1811 if (mChannelBlocked == channelBlocked) { 1812 return; 1813 } 1814 mChannelBlocked = channelBlocked; 1815 if (mChannelBlocked) { 1816 clearCallbacksAndMessagesSafely(); 1817 stopPlayback(true); 1818 resetTvTracks(); 1819 if (contentRating != null) { 1820 mSession.notifyContentBlocked(contentRating); 1821 } 1822 mHandler.sendEmptyMessageDelayed(MSG_PARENTAL_CONTROLS, PARENTAL_CONTROLS_INTERVAL_MS); 1823 } else { 1824 clearCallbacksAndMessagesSafely(); 1825 resetPlayback(); 1826 mSession.notifyContentAllowed(); 1827 mHandler.sendEmptyMessageDelayed( 1828 MSG_RESCHEDULE_PROGRAMS, RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS); 1829 mHandler.removeMessages(MSG_CHECK_SIGNAL); 1830 mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_INITIAL_DELAY_MS); 1831 } 1832 } 1833 1834 @WorkerThread 1835 private void clearCallbacksAndMessagesSafely() { 1836 // If MSG_RELEASE is removed, TunerSessionWorker will hang forever. 1837 // Do not remove messages, after release is requested from MainThread. 1838 synchronized (mReleaseLock) { 1839 if (!mReleaseRequested) { 1840 mHandler.removeCallbacksAndMessages(null); 1841 } 1842 } 1843 } 1844 1845 private boolean hasEnoughBackwardBuffer() { 1846 return mPlayer.getCurrentPosition() + BUFFER_UNDERFLOW_BUFFER_MS 1847 >= mBufferStartTimeMs - mRecordStartTimeMs; 1848 } 1849 1850 private void notifyVideoUnavailable(final int reason) { 1851 mReportedWeakSignal = (reason == TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL); 1852 if (mSession != null) { 1853 mSession.notifyVideoUnavailable(reason); 1854 } 1855 } 1856 1857 private void notifyVideoAvailable() { 1858 mReportedWeakSignal = false; 1859 if (mSession != null) { 1860 mSession.notifyVideoAvailable(); 1861 } 1862 } 1863 } 1864