1 package com.android.tv.samples.sampletunertvinput;
2 
3 import static android.media.tv.TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING;
4 import static android.media.tv.TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN;
5 
6 import android.content.ContentUris;
7 import android.content.ContentValues;
8 import android.content.Context;
9 import android.media.MediaCodec;
10 import android.media.MediaCodec.BufferInfo;
11 import android.media.MediaFormat;
12 import android.media.tv.TvContract;
13 import android.media.tv.tuner.dvr.DvrPlayback;
14 import android.media.tv.tuner.dvr.DvrSettings;
15 import android.media.tv.tuner.filter.Filter;
16 import android.media.tv.tuner.filter.FilterCallback;
17 import android.media.tv.tuner.filter.FilterEvent;
18 import android.media.tv.tuner.filter.MediaEvent;
19 import android.media.tv.tuner.Tuner;
20 import android.media.tv.TvInputService;
21 import android.media.tv.tuner.filter.SectionEvent;
22 import android.net.Uri;
23 import android.os.Handler;
24 import android.util.Log;
25 import android.view.Surface;
26 
27 import com.android.tv.common.util.Clock;
28 
29 import java.io.IOException;
30 import java.nio.ByteBuffer;
31 import java.util.ArrayDeque;
32 import java.util.ArrayList;
33 import java.util.Deque;
34 import java.util.List;
35 
36 
37 /** SampleTunerTvInputService */
38 public class SampleTunerTvInputService extends TvInputService {
39     private static final String TAG = "SampleTunerTvInput";
40     private static final boolean DEBUG = true;
41 
42     private static final int TIMEOUT_US = 100000;
43     private static final boolean SAVE_DATA = false;
44     private static final boolean USE_DVR = true;
45     private static final String MEDIA_INPUT_FILE_NAME = "media.ts";
46     private static final MediaFormat VIDEO_FORMAT;
47 
48     static {
49         // format extracted for the specific input file
50         VIDEO_FORMAT = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, 480, 360);
VIDEO_FORMAT.setInteger(MediaFormat.KEY_TRACK_ID, 1)51         VIDEO_FORMAT.setInteger(MediaFormat.KEY_TRACK_ID, 1);
VIDEO_FORMAT.setLong(MediaFormat.KEY_DURATION, 10000000)52         VIDEO_FORMAT.setLong(MediaFormat.KEY_DURATION, 10000000);
VIDEO_FORMAT.setInteger(MediaFormat.KEY_LEVEL, 256)53         VIDEO_FORMAT.setInteger(MediaFormat.KEY_LEVEL, 256);
VIDEO_FORMAT.setInteger(MediaFormat.KEY_PROFILE, 65536)54         VIDEO_FORMAT.setInteger(MediaFormat.KEY_PROFILE, 65536);
55         ByteBuffer csd = ByteBuffer.wrap(
56                 new byte[] {0, 0, 0, 1, 103, 66, -64, 30, -39, 1, -32, -65, -27, -64, 68, 0, 0, 3,
57                         0, 4, 0, 0, 3, 0, -16, 60, 88, -71, 32});
58         VIDEO_FORMAT.setByteBuffer("csd-0", csd);
59         csd = ByteBuffer.wrap(new byte[] {0, 0, 0, 1, 104, -53, -125, -53, 32});
60         VIDEO_FORMAT.setByteBuffer("csd-1", csd);
61     }
62 
63     public static final String INPUT_ID =
64             "com.android.tv.samples.sampletunertvinput/.SampleTunerTvInputService";
65     private String mSessionId;
66     private Uri mChannelUri;
67 
68     @Override
onCreateSession(String inputId, String sessionId)69     public TvInputSessionImpl onCreateSession(String inputId, String sessionId) {
70         TvInputSessionImpl session =  new TvInputSessionImpl(this);
71         if (DEBUG) {
72             Log.d(TAG, "onCreateSession(inputId=" + inputId + ", sessionId=" + sessionId + ")");
73         }
74         mSessionId = sessionId;
75         return session;
76     }
77 
78     @Override
onCreateSession(String inputId)79     public TvInputSessionImpl onCreateSession(String inputId) {
80         if (DEBUG) {
81             Log.d(TAG, "onCreateSession(inputId=" + inputId + ")");
82         }
83         return new TvInputSessionImpl(this);
84     }
85 
86     class TvInputSessionImpl extends Session {
87 
88         private final Context mContext;
89         private Handler mHandler;
90 
91         private Surface mSurface;
92         private Filter mAudioFilter;
93         private Filter mVideoFilter;
94         private Filter mSectionFilter;
95         private DvrPlayback mDvr;
96         private Tuner mTuner;
97         private MediaCodec mMediaCodec;
98         private Thread mDecoderThread;
99         private Deque<MediaEventData> mDataQueue;
100         private List<MediaEventData> mSavedData;
101         private long mCurrentLoopStartTimeUs = 0;
102         private long mLastFramePtsUs = 0;
103         private boolean mVideoAvailable;
104         private boolean mDataReady = false;
105 
106 
TvInputSessionImpl(Context context)107         public TvInputSessionImpl(Context context) {
108             super(context);
109             mContext = context;
110         }
111 
112         @Override
onRelease()113         public void onRelease() {
114             if (DEBUG) {
115                 Log.d(TAG, "onRelease");
116             }
117             if (mDecoderThread != null) {
118                 mDecoderThread.interrupt();
119                 mDecoderThread = null;
120             }
121             if (mMediaCodec != null) {
122                 mMediaCodec.release();
123                 mMediaCodec = null;
124             }
125             if (mAudioFilter != null) {
126                 mAudioFilter.close();
127             }
128             if (mVideoFilter != null) {
129                 mVideoFilter.close();
130             }
131             if (mSectionFilter != null) {
132                 mSectionFilter.close();
133             }
134             if (mDvr != null) {
135                 mDvr.close();
136                 mDvr = null;
137             }
138             if (mTuner != null) {
139                 mTuner.close();
140                 mTuner = null;
141             }
142             mDataQueue = null;
143             mSavedData = null;
144         }
145 
146         @Override
onSetSurface(Surface surface)147         public boolean onSetSurface(Surface surface) {
148             if (DEBUG) {
149                 Log.d(TAG, "onSetSurface");
150             }
151             this.mSurface = surface;
152             return true;
153         }
154 
155         @Override
onSetStreamVolume(float v)156         public void onSetStreamVolume(float v) {
157             if (DEBUG) {
158                 Log.d(TAG, "onSetStreamVolume " + v);
159             }
160         }
161 
162         @Override
onTune(Uri uri)163         public boolean onTune(Uri uri) {
164             if (DEBUG) {
165                 Log.d(TAG, "onTune " + uri);
166             }
167             if (!initCodec()) {
168                 Log.e(TAG, "null codec!");
169                 return false;
170             }
171             mChannelUri = uri;
172             mHandler = new Handler();
173             mVideoAvailable = false;
174             notifyVideoUnavailable(VIDEO_UNAVAILABLE_REASON_TUNING);
175 
176             mDecoderThread =
177                     new Thread(
178                             this::decodeInternal,
179                             "sample-tuner-tis-decoder-thread");
180             mDecoderThread.start();
181             return true;
182         }
183 
184         @Override
onSetCaptionEnabled(boolean b)185         public void onSetCaptionEnabled(boolean b) {
186             if (DEBUG) {
187                 Log.d(TAG, "onSetCaptionEnabled " + b);
188             }
189         }
190 
videoFilterCallback()191         private FilterCallback videoFilterCallback() {
192             return new FilterCallback() {
193                 @Override
194                 public void onFilterEvent(Filter filter, FilterEvent[] events) {
195                     if (DEBUG) {
196                         Log.d(TAG, "onFilterEvent video, size=" + events.length);
197                     }
198                     for (int i = 0; i < events.length; i++) {
199                         if (DEBUG) {
200                             Log.d(TAG, "events[" + i + "] is "
201                                     + events[i].getClass().getSimpleName());
202                         }
203                         if (events[i] instanceof MediaEvent) {
204                             MediaEvent me = (MediaEvent) events[i];
205 
206                             MediaEventData storedEvent = MediaEventData.generateEventData(me);
207                             if (storedEvent == null) {
208                                 continue;
209                             }
210                             mDataQueue.add(storedEvent);
211                             if (SAVE_DATA) {
212                                 mSavedData.add(storedEvent);
213                             }
214                         }
215                     }
216                 }
217 
218                 @Override
219                 public void onFilterStatusChanged(Filter filter, int status) {
220                     if (DEBUG) {
221                         Log.d(TAG, "onFilterEvent video, status=" + status);
222                     }
223                     if (status == Filter.STATUS_DATA_READY) {
224                         mDataReady = true;
225                     }
226                 }
227             };
228         }
229 
230         private FilterCallback sectionFilterCallback() {
231             return new FilterCallback() {
232                 @Override
233                 public void onFilterEvent(Filter filter, FilterEvent[] events) {
234                     if (DEBUG) {
235                         Log.d(TAG, "onFilterEvent section, size=" + events.length);
236                     }
237                     for (int i = 0; i < events.length; i++) {
238                         if (DEBUG) {
239                             Log.d(TAG, "events[" + i + "] is "
240                                     + events[i].getClass().getSimpleName());
241                         }
242                         if (events[i] instanceof SectionEvent) {
243                             SectionEvent sectionEvent = (SectionEvent) events[i];
244                             int dataSize = (int)sectionEvent.getDataLengthLong();
245                             if (DEBUG) {
246                                 Log.d(TAG, "section dataSize:" + dataSize);
247                             }
248 
249                             byte[] data = new byte[dataSize];
250                             filter.read(data, 0, dataSize);
251 
252                             handleSection(data);
253                         }
254                     }
255                 }
256 
257                 @Override
258                 public void onFilterStatusChanged(Filter filter, int status) {
259                     if (DEBUG) {
260                         Log.d(TAG, "onFilterStatusChanged section, status=" + status);
261                     }
262                 }
263             };
264         }
265 
266         private boolean initCodec() {
267             if (mMediaCodec != null) {
268                 mMediaCodec.release();
269                 mMediaCodec = null;
270             }
271             try {
272                 mMediaCodec = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
273                 mMediaCodec.configure(VIDEO_FORMAT, mSurface, null, 0);
274             } catch (IOException e) {
275                 Log.e(TAG, "Error in initCodec: " + e.getMessage());
276             }
277 
278             if (mMediaCodec == null) {
279                 Log.e(TAG, "null codec!");
280                 mVideoAvailable = false;
281                 notifyVideoUnavailable(VIDEO_UNAVAILABLE_REASON_UNKNOWN);
282                 return false;
283             }
284             return true;
285         }
286 
287         private void decodeInternal() {
288             mDataQueue = new ArrayDeque<>();
289             mSavedData = new ArrayList<>();
290             mTuner = new Tuner(mContext, mSessionId,
291                     TvInputService.PRIORITY_HINT_USE_CASE_TYPE_LIVE);
292 
293             mAudioFilter = SampleTunerTvInputUtils.createAvFilter(mTuner, mHandler,
294                     SampleTunerTvInputUtils.createDefaultLoggingFilterCallback("audio"), true);
295             mVideoFilter = SampleTunerTvInputUtils.createAvFilter(mTuner, mHandler,
296                     videoFilterCallback(), false);
297             mSectionFilter = SampleTunerTvInputUtils.createSectionFilter(mTuner, mHandler,
298                     sectionFilterCallback());
299             mAudioFilter.start();
300             mVideoFilter.start();
301             mSectionFilter.start();
302 
303             // Dvr Playback can be used to read a file instead of relying on physical tuner
304             if (USE_DVR) {
305                 mDvr = SampleTunerTvInputUtils.configureDvrPlayback(mTuner, mHandler,
306                         DvrSettings.DATA_FORMAT_TS);
307                 SampleTunerTvInputUtils.readFilePlaybackInput(getApplicationContext(), mDvr,
308                         MEDIA_INPUT_FILE_NAME);
309                 mDvr.start();
310             } else {
311                 SampleTunerTvInputUtils.tune(mTuner, mHandler);
312             }
313             mMediaCodec.start();
314 
315             try {
316                 while (!Thread.interrupted()) {
317                     if (!mDataReady) {
318                         Thread.sleep(100);
319                         continue;
320                     }
321                     if (!mDataQueue.isEmpty()) {
322                         if (handleDataBuffer(mDataQueue.getFirst())) {
323                             // data consumed, remove.
324                             mDataQueue.pollFirst();
325                         }
326                     }
327                     else if (SAVE_DATA) {
328                         if (DEBUG) {
329                             Log.d(TAG, "Adding saved data to data queue");
330                         }
331                         mDataQueue.addAll(mSavedData);
332                     }
333                 }
334             } catch (Exception e) {
335                 Log.e(TAG, "Error in decodeInternal: " + e.getMessage());
336             }
337         }
338 
339         private void handleSection(byte[] data) {
340             SampleTunerTvInputSectionParser.EitEventInfo eventInfo =
341                     SampleTunerTvInputSectionParser.parseEitSection(data);
342             if (eventInfo == null) {
343                 Log.e(TAG, "Did not receive event info from parser");
344                 return;
345             }
346 
347             // We assume that our program starts at the current time
348             long startTimeMs = Clock.SYSTEM.currentTimeMillis();
349             long endTimeMs = startTimeMs + ((long)eventInfo.getLengthSeconds() * 1000);
350 
351             // Remove any other programs which conflict with our start and end time
352             Uri conflictsUri =
353                     TvContract.buildProgramsUriForChannel(mChannelUri, startTimeMs, endTimeMs);
354             int programsDeleted = mContext.getContentResolver().delete(conflictsUri, null, null);
355             if (DEBUG) {
356                 Log.d(TAG, "Deleted " + programsDeleted + " conflicting program(s)");
357             }
358 
359             // Insert our new program into the newly opened time slot
360             ContentValues values = new ContentValues();
361             values.put(TvContract.Programs.COLUMN_CHANNEL_ID, ContentUris.parseId(mChannelUri));
362             values.put(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, startTimeMs);
363             values.put(TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, endTimeMs);
364             values.put(TvContract.Programs.COLUMN_TITLE, eventInfo.getEventTitle());
365             values.put(TvContract.Programs.COLUMN_SHORT_DESCRIPTION, "");
366             if (DEBUG) {
367                 Log.d(TAG, "Inserting program with values: " + values);
368             }
369             mContext.getContentResolver().insert(TvContract.Programs.CONTENT_URI, values);
370         }
371 
372         private boolean handleDataBuffer(MediaEventData mediaEventData) {
373             boolean success = false;
374             if (queueCodecInputBuffer(mediaEventData.getData(), mediaEventData.getDataSize(),
375                     mediaEventData.getPts())) {
376                 releaseCodecOutputBuffer();
377                 success = true;
378             }
379             return success;
380         }
381 
382         private boolean queueCodecInputBuffer(byte[] data, int size, long pts) {
383             int res = mMediaCodec.dequeueInputBuffer(TIMEOUT_US);
384             if (res >= 0) {
385                 ByteBuffer buffer = mMediaCodec.getInputBuffer(res);
386                 if (buffer == null) {
387                     throw new RuntimeException("Null decoder input buffer");
388                 }
389 
390                 if (DEBUG) {
391                     Log.d(
392                         TAG,
393                         "Decoder: Send data to decoder."
394                             + " pts="
395                             + pts
396                             + " size="
397                             + size);
398                 }
399                 // fill codec input buffer
400                 buffer.put(data, 0, size);
401 
402                 mMediaCodec.queueInputBuffer(res, 0, size, pts, 0);
403             } else {
404                 if (DEBUG) Log.d(TAG, "queueCodecInputBuffer res=" + res);
405                 return false;
406             }
407             return true;
408         }
409 
410         private void releaseCodecOutputBuffer() {
411             // play frames
412             BufferInfo bufferInfo = new BufferInfo();
413             int res = mMediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_US);
414             if (res >= 0) {
415                 long currentFramePtsUs = bufferInfo.presentationTimeUs;
416 
417                 // We know we are starting a new loop if the loop time is not set or if
418                 // the current frame is before the last frame
419                 if (mCurrentLoopStartTimeUs == 0 || currentFramePtsUs < mLastFramePtsUs) {
420                     mCurrentLoopStartTimeUs = System.nanoTime() / 1000;
421                 }
422                 mLastFramePtsUs = currentFramePtsUs;
423 
424                 long desiredUs = mCurrentLoopStartTimeUs + currentFramePtsUs;
425                 long nowUs = System.nanoTime() / 1000;
426                 long sleepTimeUs = desiredUs - nowUs;
427 
428                 if (DEBUG) {
429                     Log.d(TAG, "currentFramePts: " + currentFramePtsUs
430                             + " sleeping for: " + sleepTimeUs);
431                 }
432                 if (sleepTimeUs > 0) {
433                     try {
434                         Thread.sleep(
435                                 /* millis */ sleepTimeUs / 1000,
436                                 /* nanos */ (int) (sleepTimeUs % 1000) * 1000);
437                     } catch (InterruptedException e) {
438                         Thread.currentThread().interrupt();
439                         if (DEBUG) {
440                             Log.d(TAG, "InterruptedException:\n" + Log.getStackTraceString(e));
441                         }
442                         return;
443                     }
444                 }
445                 mMediaCodec.releaseOutputBuffer(res, true);
446                 if (!mVideoAvailable) {
447                     mVideoAvailable = true;
448                     notifyVideoAvailable();
449                     if (DEBUG) {
450                         Log.d(TAG, "notifyVideoAvailable");
451                     }
452                 }
453             } else if (res == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
454                 MediaFormat format = mMediaCodec.getOutputFormat();
455                 if (DEBUG) {
456                     Log.d(TAG, "releaseCodecOutputBuffer: Output format changed:" + format);
457                 }
458             } else if (res == MediaCodec.INFO_TRY_AGAIN_LATER) {
459                 if (DEBUG) {
460                     Log.d(TAG, "releaseCodecOutputBuffer: timeout");
461                 }
462             } else {
463                 if (DEBUG) {
464                     Log.d(TAG, "Return value of releaseCodecOutputBuffer:" + res);
465                 }
466             }
467         }
468 
469     }
470 
471     /**
472      * MediaEventData is a helper class which is used to hold the data within MediaEvents
473      * locally in our Java code, instead of in the position allocated by our native code
474      */
475     public static class MediaEventData {
476         private final long mPts;
477         private final int mDataSize;
478         private final byte[] mData;
479 
480         public MediaEventData(long pts, int dataSize, byte[] data) {
481             mPts = pts;
482             mDataSize = dataSize;
483             mData = data;
484         }
485 
486         /**
487          * Parses a MediaEvent, including copying its data and freeing the underlying LinearBlock
488          * @return {@code null} if the event has no LinearBlock
489          */
490         public static MediaEventData generateEventData(MediaEvent event) {
491             if(event.getLinearBlock() == null) {
492                 if (DEBUG) {
493                     Log.d(TAG, "MediaEvent had null LinearBlock");
494                 }
495                 return null;
496             }
497 
498             ByteBuffer memoryBlock = event.getLinearBlock().map();
499             int eventOffset = (int)event.getOffset();
500             int eventDataLength = (int)event.getDataLength();
501             if (DEBUG) {
502                 Log.d(TAG, "MediaEvent has length=" + eventDataLength
503                         + " offset=" + eventOffset
504                         + " capacity=" + memoryBlock.capacity()
505                         + " limit=" + memoryBlock.limit());
506             }
507             if (eventOffset < 0 || eventDataLength < 0 || eventOffset >= memoryBlock.limit()) {
508                 if (DEBUG) {
509                     Log.e(TAG, "MediaEvent length or offset was invalid");
510                 }
511                 event.getLinearBlock().recycle();
512                 event.release();
513                 return null;
514             }
515             // We allow the case of eventOffset + eventDataLength > memoryBlock.limit()
516             // When it occurs, we read until memoryBlock.limit
517             int dataSize = Math.min(eventDataLength, memoryBlock.limit() - eventOffset);
518             memoryBlock.position(eventOffset);
519 
520             byte[] memoryData = new byte[dataSize];
521             memoryBlock.get(memoryData, 0, dataSize);
522             MediaEventData eventData = new MediaEventData(event.getPts(), dataSize, memoryData);
523 
524             event.getLinearBlock().recycle();
525             event.release();
526             return eventData;
527         }
528 
529         public long getPts() {
530             return mPts;
531         }
532 
533         public int getDataSize() {
534             return mDataSize;
535         }
536 
537         public byte[] getData() {
538             return mData;
539         }
540     }
541 }
542