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