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