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.exoplayer; 18 19 import android.net.Uri; 20 import android.os.ConditionVariable; 21 import android.os.Handler; 22 import android.os.HandlerThread; 23 import android.os.Looper; 24 import android.os.Message; 25 import android.os.SystemClock; 26 import android.support.annotation.VisibleForTesting; 27 import android.util.Pair; 28 import com.android.tv.tuner.exoplayer.audio.MpegTsDefaultAudioTrackRenderer; 29 import com.android.tv.tuner.exoplayer.buffer.BufferManager; 30 import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer; 31 import com.android.tv.tuner.exoplayer.buffer.SimpleSampleBuffer; 32 import com.android.tv.tuner.tvinput.PlaybackBufferListener; 33 import com.google.android.exoplayer.MediaFormat; 34 import com.google.android.exoplayer.MediaFormatHolder; 35 import com.google.android.exoplayer.SampleHolder; 36 import com.google.android.exoplayer.upstream.DataSource; 37 import com.google.android.exoplayer2.C; 38 import com.google.android.exoplayer2.Format; 39 import com.google.android.exoplayer2.FormatHolder; 40 import com.google.android.exoplayer2.Timeline; 41 import com.google.android.exoplayer2.decoder.DecoderInputBuffer; 42 import com.google.android.exoplayer2.source.ExtractorMediaSource; 43 import com.google.android.exoplayer2.source.ExtractorMediaSource.EventListener; 44 import com.google.android.exoplayer2.source.MediaPeriod; 45 import com.google.android.exoplayer2.source.MediaSource; 46 import com.google.android.exoplayer2.source.SampleStream; 47 import com.google.android.exoplayer2.source.TrackGroupArray; 48 import com.google.android.exoplayer2.trackselection.FixedTrackSelection; 49 import com.google.android.exoplayer2.trackselection.TrackSelection; 50 import com.google.android.exoplayer2.upstream.DataSpec; 51 import com.google.android.exoplayer2.upstream.DefaultAllocator; 52 import java.io.IOException; 53 import java.util.ArrayList; 54 import java.util.HashMap; 55 import java.util.List; 56 import java.util.Locale; 57 import java.util.Map; 58 import java.util.concurrent.atomic.AtomicBoolean; 59 60 /** 61 * A class that extracts samples from a live broadcast stream while storing the sample on the disk. 62 * For demux, this class relies on {@link com.google.android.exoplayer.extractor.ts.TsExtractor}. 63 */ 64 public class ExoPlayerSampleExtractor implements SampleExtractor { 65 private static final String TAG = "ExoPlayerSampleExtracto"; 66 67 private static final int INVALID_TRACK_INDEX = -1; 68 private final HandlerThread mSourceReaderThread; 69 private final long mId; 70 71 private final Handler.Callback mSourceReaderWorker; 72 73 private BufferManager.SampleBuffer mSampleBuffer; 74 private Handler mSourceReaderHandler; 75 private volatile boolean mPrepared; 76 private AtomicBoolean mOnCompletionCalled = new AtomicBoolean(); 77 private IOException mExceptionOnPrepare; 78 private List<MediaFormat> mTrackFormats; 79 private int mVideoTrackIndex = INVALID_TRACK_INDEX; 80 private boolean mVideoTrackMet; 81 private long mBaseSamplePts = Long.MIN_VALUE; 82 private HashMap<Integer, Long> mLastExtractedPositionUsMap = new HashMap<>(); 83 private final List<Pair<Integer, SampleHolder>> mPendingSamples = new ArrayList<>(); 84 private OnCompletionListener mOnCompletionListener; 85 private Handler mOnCompletionListenerHandler; 86 private IOException mError; 87 88 public ExoPlayerSampleExtractor( 89 Uri uri, 90 final DataSource source, 91 BufferManager bufferManager, 92 PlaybackBufferListener bufferListener, 93 boolean isRecording) { 94 this( 95 uri, 96 source, 97 bufferManager, 98 bufferListener, 99 isRecording, 100 Looper.myLooper(), 101 new HandlerThread("SourceReaderThread")); 102 } 103 104 @VisibleForTesting 105 public ExoPlayerSampleExtractor( 106 Uri uri, 107 DataSource source, 108 BufferManager bufferManager, 109 PlaybackBufferListener bufferListener, 110 boolean isRecording, 111 Looper workerLooper, 112 HandlerThread sourceReaderThread) { 113 // It'll be used as a timeshift file chunk name's prefix. 114 mId = System.currentTimeMillis(); 115 116 EventListener eventListener = 117 new EventListener() { 118 @Override 119 public void onLoadError(IOException error) { 120 mError = error; 121 } 122 }; 123 124 mSourceReaderThread = sourceReaderThread; 125 mSourceReaderWorker = 126 new SourceReaderWorker( 127 new ExtractorMediaSource( 128 uri, 129 new com.google.android.exoplayer2.upstream.DataSource.Factory() { 130 @Override 131 public com.google.android.exoplayer2.upstream.DataSource 132 createDataSource() { 133 // Returns an adapter implementation for ExoPlayer V2 134 // DataSource interface. 135 return new com.google.android.exoplayer2.upstream 136 .DataSource() { 137 @Override 138 public long open(DataSpec dataSpec) throws IOException { 139 return source.open( 140 new com.google.android.exoplayer.upstream 141 .DataSpec( 142 dataSpec.uri, 143 dataSpec.postBody, 144 dataSpec.absoluteStreamPosition, 145 dataSpec.position, 146 dataSpec.length, 147 dataSpec.key, 148 dataSpec.flags)); 149 } 150 151 @Override 152 public int read( 153 byte[] buffer, int offset, int readLength) 154 throws IOException { 155 return source.read(buffer, offset, readLength); 156 } 157 158 @Override 159 public Uri getUri() { 160 return null; 161 } 162 163 @Override 164 public void close() throws IOException { 165 source.close(); 166 } 167 }; 168 } 169 }, 170 new ExoPlayerExtractorsFactory(), 171 new Handler(workerLooper), 172 eventListener)); 173 if (isRecording) { 174 mSampleBuffer = 175 new RecordingSampleBuffer( 176 bufferManager, 177 bufferListener, 178 false, 179 RecordingSampleBuffer.BUFFER_REASON_RECORDING); 180 } else { 181 if (bufferManager == null) { 182 mSampleBuffer = new SimpleSampleBuffer(bufferListener); 183 } else { 184 mSampleBuffer = 185 new RecordingSampleBuffer( 186 bufferManager, 187 bufferListener, 188 true, 189 RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK); 190 } 191 } 192 } 193 194 @Override 195 public void setOnCompletionListener(OnCompletionListener listener, Handler handler) { 196 mOnCompletionListener = listener; 197 mOnCompletionListenerHandler = handler; 198 } 199 200 private class SourceReaderWorker implements Handler.Callback, MediaPeriod.Callback { 201 public static final int MSG_PREPARE = 1; 202 public static final int MSG_FETCH_SAMPLES = 2; 203 public static final int MSG_RELEASE = 3; 204 private static final int RETRY_INTERVAL_MS = 50; 205 206 private final MediaSource mSampleSource; 207 private MediaPeriod mMediaPeriod; 208 private SampleStream[] mStreams; 209 private boolean[] mTrackMetEos; 210 private boolean mMetEos = false; 211 private long mCurrentPosition; 212 private DecoderInputBuffer mDecoderInputBuffer; 213 private SampleHolder mSampleHolder; 214 private boolean mPrepareRequested; 215 216 public SourceReaderWorker(MediaSource sampleSource) { 217 mSampleSource = sampleSource; 218 mSampleSource.prepareSource( 219 null, 220 false, 221 new MediaSource.Listener() { 222 @Override 223 public void onSourceInfoRefreshed( 224 MediaSource source, Timeline timeline, Object manifest) { 225 // Dynamic stream change is not supported yet. b/28169263 226 // For now, this will cause EOS and playback reset. 227 } 228 }); 229 mDecoderInputBuffer = 230 new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); 231 mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); 232 } 233 234 MediaFormat convertFormat(Format format) { 235 if (format.sampleMimeType.startsWith("audio/")) { 236 return MediaFormat.createAudioFormat( 237 format.id, 238 format.sampleMimeType, 239 format.bitrate, 240 format.maxInputSize, 241 com.google.android.exoplayer.C.UNKNOWN_TIME_US, 242 format.channelCount, 243 format.sampleRate, 244 format.initializationData, 245 format.language, 246 format.pcmEncoding); 247 } else if (format.sampleMimeType.startsWith("video/")) { 248 return MediaFormat.createVideoFormat( 249 format.id, 250 format.sampleMimeType, 251 format.bitrate, 252 format.maxInputSize, 253 com.google.android.exoplayer.C.UNKNOWN_TIME_US, 254 format.width, 255 format.height, 256 format.initializationData, 257 format.rotationDegrees, 258 format.pixelWidthHeightRatio, 259 format.projectionData, 260 format.stereoMode, 261 null // colorInfo 262 ); 263 } else if (format.sampleMimeType.endsWith("/cea-608") 264 || format.sampleMimeType.startsWith("text/")) { 265 return MediaFormat.createTextFormat( 266 format.id, 267 format.sampleMimeType, 268 format.bitrate, 269 com.google.android.exoplayer.C.UNKNOWN_TIME_US, 270 format.language); 271 } else { 272 return MediaFormat.createFormatForMimeType( 273 format.id, 274 format.sampleMimeType, 275 format.bitrate, 276 com.google.android.exoplayer.C.UNKNOWN_TIME_US); 277 } 278 } 279 280 @Override 281 public void onPrepared(MediaPeriod mediaPeriod) { 282 if (mMediaPeriod == null) { 283 // This instance is already released while the extractor is preparing. 284 return; 285 } 286 TrackSelection.Factory selectionFactory = new FixedTrackSelection.Factory(); 287 TrackGroupArray trackGroupArray = mMediaPeriod.getTrackGroups(); 288 TrackSelection[] selections = new TrackSelection[trackGroupArray.length]; 289 for (int i = 0; i < selections.length; ++i) { 290 selections[i] = selectionFactory.createTrackSelection(trackGroupArray.get(i), 0); 291 } 292 boolean[] retain = new boolean[trackGroupArray.length]; 293 boolean[] reset = new boolean[trackGroupArray.length]; 294 mStreams = new SampleStream[trackGroupArray.length]; 295 mMediaPeriod.selectTracks(selections, retain, mStreams, reset, 0); 296 if (mTrackFormats == null) { 297 int trackCount = trackGroupArray.length; 298 mTrackMetEos = new boolean[trackCount]; 299 List<MediaFormat> trackFormats = new ArrayList<>(); 300 int videoTrackCount = 0; 301 for (int i = 0; i < trackCount; i++) { 302 Format format = trackGroupArray.get(i).getFormat(0); 303 if (format.sampleMimeType.startsWith("video/")) { 304 videoTrackCount++; 305 mVideoTrackIndex = i; 306 } 307 trackFormats.add(convertFormat(format)); 308 } 309 if (videoTrackCount > 1) { 310 // Disable dropping samples when there are multiple video tracks. 311 mVideoTrackIndex = INVALID_TRACK_INDEX; 312 } 313 mTrackFormats = trackFormats; 314 List<String> ids = new ArrayList<>(); 315 for (int i = 0; i < mTrackFormats.size(); i++) { 316 ids.add(String.format(Locale.ENGLISH, "%s_%x", Long.toHexString(mId), i)); 317 } 318 try { 319 mSampleBuffer.init(ids, mTrackFormats); 320 } catch (IOException e) { 321 // In this case, we will not schedule any further operation. 322 // mExceptionOnPrepare will be notified to ExoPlayer, and ExoPlayer will 323 // call release() eventually. 324 mExceptionOnPrepare = e; 325 return; 326 } 327 mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES); 328 mPrepared = true; 329 } 330 } 331 332 @Override 333 public void onContinueLoadingRequested(MediaPeriod source) { 334 source.continueLoading(mCurrentPosition); 335 } 336 337 @Override 338 public boolean handleMessage(Message message) { 339 switch (message.what) { 340 case MSG_PREPARE: 341 if (!mPrepareRequested) { 342 mPrepareRequested = true; 343 mMediaPeriod = 344 mSampleSource.createPeriod( 345 new MediaSource.MediaPeriodId(0), 346 new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE)); 347 mMediaPeriod.prepare(this, 0); 348 try { 349 mMediaPeriod.maybeThrowPrepareError(); 350 } catch (IOException e) { 351 mError = e; 352 } 353 } 354 return true; 355 case MSG_FETCH_SAMPLES: 356 boolean didSomething = false; 357 ConditionVariable conditionVariable = new ConditionVariable(); 358 int trackCount = mStreams.length; 359 for (int i = 0; i < trackCount; ++i) { 360 if (!mTrackMetEos[i] 361 && C.RESULT_NOTHING_READ != fetchSample(i, conditionVariable)) { 362 if (mMetEos) { 363 // If mMetEos was on during fetchSample() due to an error, 364 // fetching from other tracks is not necessary. 365 break; 366 } 367 didSomething = true; 368 } 369 } 370 mMediaPeriod.continueLoading(mCurrentPosition); 371 if (!mMetEos) { 372 if (didSomething) { 373 mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES); 374 } else { 375 mSourceReaderHandler.sendEmptyMessageDelayed( 376 MSG_FETCH_SAMPLES, RETRY_INTERVAL_MS); 377 } 378 } else { 379 notifyCompletionIfNeeded(false); 380 } 381 return true; 382 case MSG_RELEASE: 383 if (mMediaPeriod != null) { 384 mSampleSource.releasePeriod(mMediaPeriod); 385 mSampleSource.releaseSource(); 386 mMediaPeriod = null; 387 } 388 cleanUp(); 389 mSourceReaderHandler.removeCallbacksAndMessages(null); 390 return true; 391 default: // fall out 392 } 393 return false; 394 } 395 396 private int fetchSample(int track, ConditionVariable conditionVariable) { 397 FormatHolder dummyFormatHolder = new FormatHolder(); 398 mDecoderInputBuffer.clear(); 399 int ret = mStreams[track].readData(dummyFormatHolder, mDecoderInputBuffer, false); 400 if (ret == C.RESULT_BUFFER_READ 401 // Double-check if the extractor provided the data to prevent NPE. b/33758354 402 && mDecoderInputBuffer.data != null) { 403 if (mCurrentPosition < mDecoderInputBuffer.timeUs) { 404 mCurrentPosition = mDecoderInputBuffer.timeUs; 405 } 406 if (mMediaPeriod != null) { 407 mMediaPeriod.discardBuffer(mCurrentPosition, false); 408 } 409 try { 410 Long lastExtractedPositionUs = mLastExtractedPositionUsMap.get(track); 411 if (lastExtractedPositionUs == null) { 412 mLastExtractedPositionUsMap.put(track, mDecoderInputBuffer.timeUs); 413 } else { 414 mLastExtractedPositionUsMap.put( 415 track, 416 Math.max(lastExtractedPositionUs, mDecoderInputBuffer.timeUs)); 417 } 418 queueSample(track, conditionVariable); 419 } catch (IOException e) { 420 mLastExtractedPositionUsMap.clear(); 421 mMetEos = true; 422 mSampleBuffer.setEos(); 423 } 424 } else if (ret == C.RESULT_END_OF_INPUT) { 425 mTrackMetEos[track] = true; 426 for (int i = 0; i < mTrackMetEos.length; ++i) { 427 if (!mTrackMetEos[i]) { 428 break; 429 } 430 if (i == mTrackMetEos.length - 1) { 431 mMetEos = true; 432 mSampleBuffer.setEos(); 433 } 434 } 435 } 436 // TODO: Handle C.RESULT_FORMAT_READ for dynamic resolution change. b/28169263 437 return ret; 438 } 439 440 private void queueSample(int index, ConditionVariable conditionVariable) 441 throws IOException { 442 if (mVideoTrackIndex != INVALID_TRACK_INDEX) { 443 if (!mVideoTrackMet) { 444 if (index != mVideoTrackIndex) { 445 SampleHolder sample = 446 new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); 447 mSampleHolder.flags = 448 (mDecoderInputBuffer.isKeyFrame() 449 ? com.google.android.exoplayer.C.SAMPLE_FLAG_SYNC 450 : 0) 451 | (mDecoderInputBuffer.isDecodeOnly() 452 ? com.google 453 .android 454 .exoplayer 455 .C 456 .SAMPLE_FLAG_DECODE_ONLY 457 : 0); 458 sample.timeUs = mDecoderInputBuffer.timeUs; 459 sample.size = mDecoderInputBuffer.data.position(); 460 sample.ensureSpaceForWrite(sample.size); 461 mDecoderInputBuffer.flip(); 462 sample.data.position(0); 463 sample.data.put(mDecoderInputBuffer.data); 464 sample.data.flip(); 465 mPendingSamples.add(new Pair<>(index, sample)); 466 return; 467 } 468 mVideoTrackMet = true; 469 mBaseSamplePts = 470 mDecoderInputBuffer.timeUs 471 - MpegTsDefaultAudioTrackRenderer 472 .INITIAL_AUDIO_BUFFERING_TIME_US; 473 for (Pair<Integer, SampleHolder> pair : mPendingSamples) { 474 if (pair.second.timeUs >= mBaseSamplePts) { 475 mSampleBuffer.writeSample(pair.first, pair.second, conditionVariable); 476 } 477 } 478 mPendingSamples.clear(); 479 } else { 480 if (mDecoderInputBuffer.timeUs < mBaseSamplePts && mVideoTrackIndex != index) { 481 return; 482 } 483 } 484 } 485 // Copy the decoder input to the sample holder. 486 mSampleHolder.clearData(); 487 mSampleHolder.flags = 488 (mDecoderInputBuffer.isKeyFrame() 489 ? com.google.android.exoplayer.C.SAMPLE_FLAG_SYNC 490 : 0) 491 | (mDecoderInputBuffer.isDecodeOnly() 492 ? com.google.android.exoplayer.C.SAMPLE_FLAG_DECODE_ONLY 493 : 0); 494 mSampleHolder.timeUs = mDecoderInputBuffer.timeUs; 495 mSampleHolder.size = mDecoderInputBuffer.data.position(); 496 mSampleHolder.ensureSpaceForWrite(mSampleHolder.size); 497 mDecoderInputBuffer.flip(); 498 mSampleHolder.data.position(0); 499 mSampleHolder.data.put(mDecoderInputBuffer.data); 500 mSampleHolder.data.flip(); 501 long writeStartTimeNs = SystemClock.elapsedRealtimeNanos(); 502 mSampleBuffer.writeSample(index, mSampleHolder, conditionVariable); 503 504 // Checks whether the storage has enough bandwidth for recording samples. 505 if (mSampleBuffer.isWriteSpeedSlow( 506 mSampleHolder.size, SystemClock.elapsedRealtimeNanos() - writeStartTimeNs)) { 507 mSampleBuffer.handleWriteSpeedSlow(); 508 } 509 } 510 } 511 512 @Override 513 public void maybeThrowError() throws IOException { 514 if (mError != null) { 515 IOException e = mError; 516 mError = null; 517 throw e; 518 } 519 } 520 521 @Override 522 public boolean prepare() throws IOException { 523 if (!mSourceReaderThread.isAlive()) { 524 mSourceReaderThread.start(); 525 mSourceReaderHandler = 526 new Handler(mSourceReaderThread.getLooper(), mSourceReaderWorker); 527 mSourceReaderHandler.sendEmptyMessage(SourceReaderWorker.MSG_PREPARE); 528 } 529 if (mExceptionOnPrepare != null) { 530 throw mExceptionOnPrepare; 531 } 532 return mPrepared; 533 } 534 535 @Override 536 public List<MediaFormat> getTrackFormats() { 537 return mTrackFormats; 538 } 539 540 @Override 541 public void getTrackMediaFormat(int track, MediaFormatHolder outMediaFormatHolder) { 542 outMediaFormatHolder.format = mTrackFormats.get(track); 543 outMediaFormatHolder.drmInitData = null; 544 } 545 546 @Override 547 public void selectTrack(int index) { 548 mSampleBuffer.selectTrack(index); 549 } 550 551 @Override 552 public void deselectTrack(int index) { 553 mSampleBuffer.deselectTrack(index); 554 } 555 556 @Override 557 public long getBufferedPositionUs() { 558 return mSampleBuffer.getBufferedPositionUs(); 559 } 560 561 @Override 562 public boolean continueBuffering(long positionUs) { 563 return mSampleBuffer.continueBuffering(positionUs); 564 } 565 566 @Override 567 public void seekTo(long positionUs) { 568 mSampleBuffer.seekTo(positionUs); 569 } 570 571 @Override 572 public int readSample(int track, SampleHolder sampleHolder) { 573 return mSampleBuffer.readSample(track, sampleHolder); 574 } 575 576 @Override 577 public void release() { 578 if (mSourceReaderThread.isAlive()) { 579 mSourceReaderHandler.removeCallbacksAndMessages(null); 580 mSourceReaderHandler.sendEmptyMessage(SourceReaderWorker.MSG_RELEASE); 581 mSourceReaderThread.quitSafely(); 582 // Return early in this case so that session worker can start working on the next 583 // request as early as it can. The clean up will be done in the reader thread while 584 // handling MSG_RELEASE. 585 } else { 586 cleanUp(); 587 } 588 } 589 590 private void cleanUp() { 591 boolean result = true; 592 try { 593 if (mSampleBuffer != null) { 594 mSampleBuffer.release(); 595 mSampleBuffer = null; 596 } 597 } catch (IOException e) { 598 result = false; 599 } 600 notifyCompletionIfNeeded(result); 601 setOnCompletionListener(null, null); 602 } 603 604 private void notifyCompletionIfNeeded(final boolean result) { 605 if (!mOnCompletionCalled.getAndSet(true)) { 606 final OnCompletionListener listener = mOnCompletionListener; 607 final long lastExtractedPositionUs = getLastExtractedPositionUs(); 608 if (mOnCompletionListenerHandler != null && mOnCompletionListener != null) { 609 mOnCompletionListenerHandler.post( 610 new Runnable() { 611 @Override 612 public void run() { 613 listener.onCompletion(result, lastExtractedPositionUs); 614 } 615 }); 616 } 617 } 618 } 619 620 private long getLastExtractedPositionUs() { 621 long lastExtractedPositionUs = Long.MIN_VALUE; 622 for (Map.Entry<Integer, Long> entry : mLastExtractedPositionUsMap.entrySet()) { 623 if (mVideoTrackIndex != entry.getKey()) { 624 lastExtractedPositionUs = Math.max(lastExtractedPositionUs, entry.getValue()); 625 } 626 } 627 if (lastExtractedPositionUs == Long.MIN_VALUE) { 628 lastExtractedPositionUs = com.google.android.exoplayer.C.UNKNOWN_TIME_US; 629 } 630 return lastExtractedPositionUs; 631 } 632 } 633