Home | History | Annotate | Download | only in exoplayer
      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