1 package com.android.tv.samples.sampletunertvinput; 2 3 import static android.media.tv.TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN; 4 5 import android.content.Context; 6 import android.media.MediaCodec; 7 import android.media.MediaCodec.BufferInfo; 8 import android.media.MediaCodec.LinearBlock; 9 import android.media.MediaFormat; 10 import android.media.tv.tuner.dvr.DvrPlayback; 11 import android.media.tv.tuner.dvr.DvrSettings; 12 import android.media.tv.tuner.filter.AvSettings; 13 import android.media.tv.tuner.filter.Filter; 14 import android.media.tv.tuner.filter.FilterCallback; 15 import android.media.tv.tuner.filter.FilterEvent; 16 import android.media.tv.tuner.filter.MediaEvent; 17 import android.media.tv.tuner.filter.TsFilterConfiguration; 18 import android.media.tv.tuner.frontend.AtscFrontendSettings; 19 import android.media.tv.tuner.frontend.DvbtFrontendSettings; 20 import android.media.tv.tuner.frontend.FrontendSettings; 21 import android.media.tv.tuner.frontend.OnTuneEventListener; 22 import android.media.tv.tuner.Tuner; 23 import android.media.tv.TvInputService; 24 import android.net.Uri; 25 import android.os.Handler; 26 import android.os.HandlerExecutor; 27 import android.os.ParcelFileDescriptor; 28 import android.util.Log; 29 import android.view.Surface; 30 import java.io.File; 31 import java.io.FileNotFoundException; 32 import java.io.IOException; 33 import java.nio.ByteBuffer; 34 import java.util.ArrayDeque; 35 import java.util.ArrayList; 36 import java.util.Deque; 37 import java.util.List; 38 39 40 /** SampleTunerTvInputService */ 41 public class SampleTunerTvInputService extends TvInputService { 42 private static final String TAG = "SampleTunerTvInput"; 43 private static final boolean DEBUG = true; 44 45 private static final int AUDIO_TPID = 257; 46 private static final int VIDEO_TPID = 256; 47 private static final int STATUS_MASK = 0xf; 48 private static final int LOW_THRESHOLD = 0x1000; 49 private static final int HIGH_THRESHOLD = 0x07fff; 50 private static final int FREQUENCY = 578000; 51 private static final int FILTER_BUFFER_SIZE = 16000000; 52 private static final int DVR_BUFFER_SIZE = 4000000; 53 private static final int INPUT_FILE_MAX_SIZE = 700000; 54 private static final int PACKET_SIZE = 188; 55 56 private static final int TIMEOUT_US = 100000; 57 private static final boolean SAVE_DATA = false; 58 private static final String ES_FILE_NAME = "test.es"; 59 private static final MediaFormat VIDEO_FORMAT; 60 61 static { 62 // format extracted for the specific input file 63 VIDEO_FORMAT = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, 320, 240); VIDEO_FORMAT.setInteger(MediaFormat.KEY_TRACK_ID, 1)64 VIDEO_FORMAT.setInteger(MediaFormat.KEY_TRACK_ID, 1); VIDEO_FORMAT.setLong(MediaFormat.KEY_DURATION, 9933333)65 VIDEO_FORMAT.setLong(MediaFormat.KEY_DURATION, 9933333); VIDEO_FORMAT.setInteger(MediaFormat.KEY_LEVEL, 32)66 VIDEO_FORMAT.setInteger(MediaFormat.KEY_LEVEL, 32); VIDEO_FORMAT.setInteger(MediaFormat.KEY_PROFILE, 65536)67 VIDEO_FORMAT.setInteger(MediaFormat.KEY_PROFILE, 65536); 68 ByteBuffer csd = ByteBuffer.wrap( 69 new byte[] {0, 0, 0, 1, 103, 66, -64, 20, -38, 5, 7, -24, 64, 0, 0, 3, 0, 64, 0, 70 0, 15, 35, -59, 10, -88}); 71 VIDEO_FORMAT.setByteBuffer("csd-0", csd); 72 csd = ByteBuffer.wrap(new byte[] {0, 0, 0, 1, 104, -50, 60, -128}); 73 VIDEO_FORMAT.setByteBuffer("csd-1", csd); 74 } 75 76 public static final String INPUT_ID = 77 "com.android.tv.samples.sampletunertvinput/.SampleTunerTvInputService"; 78 private String mSessionId; 79 80 @Override onCreateSession(String inputId, String sessionId)81 public TvInputSessionImpl onCreateSession(String inputId, String sessionId) { 82 TvInputSessionImpl session = new TvInputSessionImpl(this); 83 if (DEBUG) { 84 Log.d(TAG, "onCreateSession(inputId=" + inputId + ", sessionId=" + sessionId + ")"); 85 } 86 mSessionId = sessionId; 87 return session; 88 } 89 90 @Override onCreateSession(String inputId)91 public TvInputSessionImpl onCreateSession(String inputId) { 92 return new TvInputSessionImpl(this); 93 } 94 95 class TvInputSessionImpl extends Session { 96 97 private final Context mContext; 98 private Handler mHandler; 99 100 private Surface mSurface; 101 private Filter mAudioFilter; 102 private Filter mVideoFilter; 103 private DvrPlayback mDvr; 104 private Tuner mTuner; 105 private MediaCodec mMediaCodec; 106 private Thread mDecoderThread; 107 private Deque<MediaEvent> mDataQueue; 108 private List<MediaEvent> mSavedData; 109 private boolean mDataReady = false; 110 111 TvInputSessionImpl(Context context)112 public TvInputSessionImpl(Context context) { 113 super(context); 114 mContext = context; 115 } 116 117 @Override onRelease()118 public void onRelease() { 119 if (DEBUG) { 120 Log.d(TAG, "onRelease"); 121 } 122 if (mDecoderThread != null) { 123 mDecoderThread.interrupt(); 124 mDecoderThread = null; 125 } 126 if (mMediaCodec != null) { 127 mMediaCodec.release(); 128 mMediaCodec = null; 129 } 130 if (mAudioFilter != null) { 131 mAudioFilter.close(); 132 } 133 if (mVideoFilter != null) { 134 mVideoFilter.close(); 135 } 136 if (mDvr != null) { 137 mDvr.close(); 138 mDvr = null; 139 } 140 if (mTuner != null) { 141 mTuner.close(); 142 mTuner = null; 143 } 144 mDataQueue = null; 145 mSavedData = null; 146 } 147 148 @Override onSetSurface(Surface surface)149 public boolean onSetSurface(Surface surface) { 150 if (DEBUG) { 151 Log.d(TAG, "onSetSurface"); 152 } 153 this.mSurface = surface; 154 return true; 155 } 156 157 @Override onSetStreamVolume(float v)158 public void onSetStreamVolume(float v) { 159 if (DEBUG) { 160 Log.d(TAG, "onSetStreamVolume " + v); 161 } 162 } 163 164 @Override onTune(Uri uri)165 public boolean onTune(Uri uri) { 166 if (DEBUG) { 167 Log.d(TAG, "onTune " + uri); 168 } 169 if (!initCodec()) { 170 Log.e(TAG, "null codec!"); 171 return false; 172 } 173 mHandler = new Handler(); 174 mDecoderThread = 175 new Thread( 176 this::decodeInternal, 177 "sample-tuner-tis-decoder-thread"); 178 mDecoderThread.start(); 179 return true; 180 } 181 182 @Override onSetCaptionEnabled(boolean b)183 public void onSetCaptionEnabled(boolean b) { 184 if (DEBUG) { 185 Log.d(TAG, "onSetCaptionEnabled " + b); 186 } 187 } 188 audioFilter()189 private Filter audioFilter() { 190 Filter audioFilter = mTuner.openFilter(Filter.TYPE_TS, Filter.SUBTYPE_AUDIO, 191 FILTER_BUFFER_SIZE, new HandlerExecutor(mHandler), 192 new FilterCallback() { 193 @Override 194 public void onFilterEvent(Filter filter, FilterEvent[] events) { 195 if (DEBUG) { 196 Log.d(TAG, "onFilterEvent audio, 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 } 204 } 205 206 @Override 207 public void onFilterStatusChanged(Filter filter, int status) { 208 if (DEBUG) { 209 Log.d(TAG, "onFilterEvent audio, status=" + status); 210 } 211 } 212 }); 213 AvSettings settings = 214 AvSettings.builder(Filter.TYPE_TS, true).setPassthrough(false).build(); 215 audioFilter.configure( 216 TsFilterConfiguration.builder().setTpid(AUDIO_TPID) 217 .setSettings(settings).build()); 218 return audioFilter; 219 } 220 videoFilter()221 private Filter videoFilter() { 222 Filter videoFilter = mTuner.openFilter(Filter.TYPE_TS, Filter.SUBTYPE_VIDEO, 223 FILTER_BUFFER_SIZE, new HandlerExecutor(mHandler), 224 new FilterCallback() { 225 @Override 226 public void onFilterEvent(Filter filter, FilterEvent[] events) { 227 if (DEBUG) { 228 Log.d(TAG, "onFilterEvent video, size=" + events.length); 229 } 230 for (int i = 0; i < events.length; i++) { 231 if (DEBUG) { 232 Log.d(TAG, "events[" + i + "] is " 233 + events[i].getClass().getSimpleName()); 234 } 235 if (events[i] instanceof MediaEvent) { 236 MediaEvent me = (MediaEvent) events[i]; 237 mDataQueue.add(me); 238 if (SAVE_DATA) { 239 mSavedData.add(me); 240 } 241 } 242 } 243 } 244 245 @Override 246 public void onFilterStatusChanged(Filter filter, int status) { 247 if (DEBUG) { 248 Log.d(TAG, "onFilterEvent video, status=" + status); 249 } 250 if (status == Filter.STATUS_DATA_READY) { 251 mDataReady = true; 252 } 253 } 254 }); 255 AvSettings settings = 256 AvSettings.builder(Filter.TYPE_TS, false).setPassthrough(false).build(); 257 videoFilter.configure( 258 TsFilterConfiguration.builder().setTpid(VIDEO_TPID) 259 .setSettings(settings).build()); 260 return videoFilter; 261 } 262 dvrPlayback()263 private DvrPlayback dvrPlayback() { 264 DvrPlayback dvr = mTuner.openDvrPlayback(DVR_BUFFER_SIZE, new HandlerExecutor(mHandler), 265 status -> { 266 if (DEBUG) { 267 Log.d(TAG, "onPlaybackStatusChanged status=" + status); 268 } 269 }); 270 int res = dvr.configure( 271 DvrSettings.builder() 272 .setStatusMask(STATUS_MASK) 273 .setLowThreshold(LOW_THRESHOLD) 274 .setHighThreshold(HIGH_THRESHOLD) 275 .setDataFormat(DvrSettings.DATA_FORMAT_ES) 276 .setPacketSize(PACKET_SIZE) 277 .build()); 278 if (DEBUG) { 279 Log.d(TAG, "config res=" + res); 280 } 281 String testFile = mContext.getFilesDir().getAbsolutePath() + "/" + ES_FILE_NAME; 282 File file = new File(testFile); 283 if (file.exists()) { 284 try { 285 dvr.setFileDescriptor( 286 ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE)); 287 } catch (FileNotFoundException e) { 288 Log.e(TAG, "Failed to create FD"); 289 } 290 } else { 291 Log.w(TAG, "File not existing"); 292 } 293 return dvr; 294 } 295 tune()296 private void tune() { 297 DvbtFrontendSettings feSettings = DvbtFrontendSettings.builder() 298 .setFrequency(FREQUENCY) 299 .setTransmissionMode(DvbtFrontendSettings.TRANSMISSION_MODE_AUTO) 300 .setBandwidth(DvbtFrontendSettings.BANDWIDTH_8MHZ) 301 .setConstellation(DvbtFrontendSettings.CONSTELLATION_AUTO) 302 .setHierarchy(DvbtFrontendSettings.HIERARCHY_AUTO) 303 .setHighPriorityCodeRate(DvbtFrontendSettings.CODERATE_AUTO) 304 .setLowPriorityCodeRate(DvbtFrontendSettings.CODERATE_AUTO) 305 .setGuardInterval(DvbtFrontendSettings.GUARD_INTERVAL_AUTO) 306 .setHighPriority(true) 307 .setStandard(DvbtFrontendSettings.STANDARD_T) 308 .build(); 309 mTuner.setOnTuneEventListener(new HandlerExecutor(mHandler), new OnTuneEventListener() { 310 @Override 311 public void onTuneEvent(int tuneEvent) { 312 if (DEBUG) { 313 Log.d(TAG, "onTuneEvent " + tuneEvent); 314 } 315 long read = mDvr.read(INPUT_FILE_MAX_SIZE); 316 if (DEBUG) { 317 Log.d(TAG, "read=" + read); 318 } 319 } 320 }); 321 mTuner.tune(feSettings); 322 } 323 initCodec()324 private boolean initCodec() { 325 if (mMediaCodec != null) { 326 mMediaCodec.release(); 327 mMediaCodec = null; 328 } 329 try { 330 mMediaCodec = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_AVC); 331 mMediaCodec.configure(VIDEO_FORMAT, mSurface, null, 0); 332 } catch (IOException e) { 333 Log.e(TAG, "Error in initCodec: " + e.getMessage()); 334 } 335 336 if (mMediaCodec == null) { 337 Log.e(TAG, "null codec!"); 338 notifyVideoUnavailable(VIDEO_UNAVAILABLE_REASON_UNKNOWN); 339 return false; 340 } 341 return true; 342 } 343 decodeInternal()344 private void decodeInternal() { 345 mDataQueue = new ArrayDeque<>(); 346 mSavedData = new ArrayList<>(); 347 mTuner = new Tuner(mContext, mSessionId, 348 TvInputService.PRIORITY_HINT_USE_CASE_TYPE_LIVE); 349 350 mAudioFilter = audioFilter(); 351 mVideoFilter = videoFilter(); 352 mAudioFilter.start(); 353 mVideoFilter.start(); 354 // use dvr playback to feed the data on platform without physical tuner 355 mDvr = dvrPlayback(); 356 tune(); 357 mDvr.start(); 358 mMediaCodec.start(); 359 360 try { 361 while (!Thread.interrupted()) { 362 if (!mDataReady) { 363 Thread.sleep(100); 364 continue; 365 } 366 if (!mDataQueue.isEmpty()) { 367 if (handleDataBuffer(mDataQueue.getFirst())) { 368 // data consumed, remove. 369 mDataQueue.pollFirst(); 370 } 371 } 372 if (SAVE_DATA) { 373 mDataQueue.addAll(mSavedData); 374 } 375 } 376 } catch (Exception e) { 377 Log.e(TAG, "Error in decodeInternal: " + e.getMessage()); 378 } 379 } 380 handleDataBuffer(MediaEvent mediaEvent)381 private boolean handleDataBuffer(MediaEvent mediaEvent) { 382 if (mediaEvent.getLinearBlock() == null) { 383 if (DEBUG) Log.d(TAG, "getLinearBlock() == null"); 384 return true; 385 } 386 boolean success = false; 387 LinearBlock block = mediaEvent.getLinearBlock(); 388 if (queueCodecInputBuffer(block, mediaEvent.getDataLength(), mediaEvent.getOffset(), 389 mediaEvent.getPts())) { 390 releaseCodecOutputBuffer(); 391 success = true; 392 } 393 mediaEvent.release(); 394 return success; 395 } 396 queueCodecInputBuffer(LinearBlock block, long sampleSize, long offset, long pts)397 private boolean queueCodecInputBuffer(LinearBlock block, long sampleSize, 398 long offset, long pts) { 399 int res = mMediaCodec.dequeueInputBuffer(TIMEOUT_US); 400 if (res >= 0) { 401 ByteBuffer buffer = mMediaCodec.getInputBuffer(res); 402 if (buffer == null) { 403 throw new RuntimeException("Null decoder input buffer"); 404 } 405 406 ByteBuffer data = block.map(); 407 if (offset > 0 && offset < data.limit()) { 408 data.position((int) offset); 409 } else { 410 data.position(0); 411 } 412 413 if (DEBUG) { 414 Log.d( 415 TAG, 416 "Decoder: Send data to decoder." 417 + " Sample size=" 418 + sampleSize 419 + " pts=" 420 + pts 421 + " limit=" 422 + data.limit() 423 + " pos=" 424 + data.position() 425 + " size=" 426 + (data.limit() - data.position())); 427 } 428 // fill codec input buffer 429 int size = sampleSize > data.limit() ? data.limit() : (int) sampleSize; 430 if (DEBUG) Log.d(TAG, "limit " + data.limit() + " sampleSize " + sampleSize); 431 if (data.hasArray()) { 432 Log.d(TAG, "hasArray"); 433 buffer.put(data.array(), 0, size); 434 } else { 435 byte[] array = new byte[size]; 436 data.get(array, 0, size); 437 buffer.put(array, 0, size); 438 } 439 440 mMediaCodec.queueInputBuffer(res, 0, (int) sampleSize, pts, 0); 441 } else { 442 if (DEBUG) Log.d(TAG, "queueCodecInputBuffer res=" + res); 443 return false; 444 } 445 return true; 446 } 447 releaseCodecOutputBuffer()448 private void releaseCodecOutputBuffer() { 449 // play frames 450 BufferInfo bufferInfo = new BufferInfo(); 451 int res = mMediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_US); 452 if (res >= 0) { 453 mMediaCodec.releaseOutputBuffer(res, true); 454 notifyVideoAvailable(); 455 if (DEBUG) { 456 Log.d(TAG, "notifyVideoAvailable"); 457 } 458 } else if (res == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { 459 MediaFormat format = mMediaCodec.getOutputFormat(); 460 if (DEBUG) { 461 Log.d(TAG, "releaseCodecOutputBuffer: Output format changed:" + format); 462 } 463 } else if (res == MediaCodec.INFO_TRY_AGAIN_LATER) { 464 if (DEBUG) { 465 Log.d(TAG, "releaseCodecOutputBuffer: timeout"); 466 } 467 } else { 468 if (DEBUG) { 469 Log.d(TAG, "Return value of releaseCodecOutputBuffer:" + res); 470 } 471 } 472 } 473 474 } 475 } 476