/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tv.tuner.exoplayer; import android.net.Uri; import android.os.ConditionVariable; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.os.SystemClock; import android.util.Pair; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource.EventListener; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.FixedTrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DefaultAllocator; import com.android.tv.tuner.exoplayer.audio.MpegTsDefaultAudioTrackRenderer; import com.android.tv.tuner.exoplayer.buffer.BufferManager; import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer; import com.android.tv.tuner.exoplayer.buffer.SimpleSampleBuffer; import com.android.tv.tuner.tvinput.PlaybackBufferListener; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; /** * A class that extracts samples from a live broadcast stream while storing the sample on the disk. * For demux, this class relies on {@link com.google.android.exoplayer.extractor.ts.TsExtractor}. */ public class ExoPlayerSampleExtractor implements SampleExtractor { private static final String TAG = "ExoPlayerSampleExtracto"; private static final int INVALID_TRACK_INDEX = -1; private final HandlerThread mSourceReaderThread; private final long mId; private final Handler.Callback mSourceReaderWorker; private BufferManager.SampleBuffer mSampleBuffer; private Handler mSourceReaderHandler; private volatile boolean mPrepared; private AtomicBoolean mOnCompletionCalled = new AtomicBoolean(); private IOException mExceptionOnPrepare; private List mTrackFormats; private int mVideoTrackIndex = INVALID_TRACK_INDEX; private boolean mVideoTrackMet; private long mBaseSamplePts = Long.MIN_VALUE; private HashMap mLastExtractedPositionUsMap = new HashMap<>(); private final List> mPendingSamples = new LinkedList<>(); private OnCompletionListener mOnCompletionListener; private Handler mOnCompletionListenerHandler; private IOException mError; public ExoPlayerSampleExtractor(Uri uri, final DataSource source, BufferManager bufferManager, PlaybackBufferListener bufferListener, boolean isRecording) { // It'll be used as a timeshift file chunk name's prefix. mId = System.currentTimeMillis(); EventListener eventListener = new EventListener() { @Override public void onLoadError(IOException error) { mError = error; } }; mSourceReaderThread = new HandlerThread("SourceReaderThread"); mSourceReaderWorker = new SourceReaderWorker(new ExtractorMediaSource(uri, new com.google.android.exoplayer2.upstream.DataSource.Factory() { @Override public com.google.android.exoplayer2.upstream.DataSource createDataSource() { // Returns an adapter implementation for ExoPlayer V2 DataSource interface. return new com.google.android.exoplayer2.upstream.DataSource() { @Override public long open(DataSpec dataSpec) throws IOException { return source.open( new com.google.android.exoplayer.upstream.DataSpec( dataSpec.uri, dataSpec.postBody, dataSpec.absoluteStreamPosition, dataSpec.position, dataSpec.length, dataSpec.key, dataSpec.flags)); } @Override public int read(byte[] buffer, int offset, int readLength) throws IOException { return source.read(buffer, offset, readLength); } @Override public Uri getUri() { return null; } @Override public void close() throws IOException { source.close(); } }; } }, new ExoPlayerExtractorsFactory(), // Do not create a handler if we not on a looper. e.g. test. Looper.myLooper() != null ? new Handler() : null, eventListener)); if (isRecording) { mSampleBuffer = new RecordingSampleBuffer(bufferManager, bufferListener, false, RecordingSampleBuffer.BUFFER_REASON_RECORDING); } else { if (bufferManager == null) { mSampleBuffer = new SimpleSampleBuffer(bufferListener); } else { mSampleBuffer = new RecordingSampleBuffer(bufferManager, bufferListener, true, RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK); } } } @Override public void setOnCompletionListener(OnCompletionListener listener, Handler handler) { mOnCompletionListener = listener; mOnCompletionListenerHandler = handler; } private class SourceReaderWorker implements Handler.Callback, MediaPeriod.Callback { public static final int MSG_PREPARE = 1; public static final int MSG_FETCH_SAMPLES = 2; public static final int MSG_RELEASE = 3; private static final int RETRY_INTERVAL_MS = 50; private final MediaSource mSampleSource; private MediaPeriod mMediaPeriod; private SampleStream[] mStreams; private boolean[] mTrackMetEos; private boolean mMetEos = false; private long mCurrentPosition; private DecoderInputBuffer mDecoderInputBuffer; private SampleHolder mSampleHolder; private boolean mPrepareRequested; public SourceReaderWorker(MediaSource sampleSource) { mSampleSource = sampleSource; mSampleSource.prepareSource(null, false, new MediaSource.Listener() { @Override public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { // Dynamic stream change is not supported yet. b/28169263 // For now, this will cause EOS and playback reset. } }); mDecoderInputBuffer = new DecoderInputBuffer( DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); } MediaFormat convertFormat(Format format) { if (format.sampleMimeType.startsWith("audio/")) { return MediaFormat.createAudioFormat(format.id, format.sampleMimeType, format.bitrate, format.maxInputSize, com.google.android.exoplayer.C.UNKNOWN_TIME_US, format.channelCount, format.sampleRate, format.initializationData, format.language, format.pcmEncoding); } else if (format.sampleMimeType.startsWith("video/")) { return MediaFormat.createVideoFormat( format.id, format.sampleMimeType, format.bitrate, format.maxInputSize, com.google.android.exoplayer.C.UNKNOWN_TIME_US, format.width, format.height, format.initializationData, format.rotationDegrees, format.pixelWidthHeightRatio, format.projectionData, format.stereoMode); } else if (format.sampleMimeType.endsWith("/cea-608") || format.sampleMimeType.startsWith("text/")) { return MediaFormat.createTextFormat( format.id, format.sampleMimeType, format.bitrate, com.google.android.exoplayer.C.UNKNOWN_TIME_US, format.language); } else { return MediaFormat.createFormatForMimeType( format.id, format.sampleMimeType, format.bitrate, com.google.android.exoplayer.C.UNKNOWN_TIME_US); } } @Override public void onPrepared(MediaPeriod mediaPeriod) { if (mMediaPeriod == null) { // This instance is already released while the extractor is preparing. return; } TrackSelection.Factory selectionFactory = new FixedTrackSelection.Factory(); TrackGroupArray trackGroupArray = mMediaPeriod.getTrackGroups(); TrackSelection[] selections = new TrackSelection[trackGroupArray.length]; for (int i = 0; i < selections.length; ++i) { selections[i] = selectionFactory.createTrackSelection(trackGroupArray.get(i), 0); } boolean retain[] = new boolean[trackGroupArray.length]; boolean reset[] = new boolean[trackGroupArray.length]; mStreams = new SampleStream[trackGroupArray.length]; mMediaPeriod.selectTracks(selections, retain, mStreams, reset, 0); if (mTrackFormats == null) { int trackCount = trackGroupArray.length; mTrackMetEos = new boolean[trackCount]; List trackFormats = new ArrayList<>(); int videoTrackCount = 0; for (int i = 0; i < trackCount; i++) { Format format = trackGroupArray.get(i).getFormat(0); if (format.sampleMimeType.startsWith("video/")) { videoTrackCount++; mVideoTrackIndex = i; } trackFormats.add(convertFormat(format)); } if (videoTrackCount > 1) { // Disable dropping samples when there are multiple video tracks. mVideoTrackIndex = INVALID_TRACK_INDEX; } mTrackFormats = trackFormats; List ids = new ArrayList<>(); for (int i = 0; i < mTrackFormats.size(); i++) { ids.add(String.format(Locale.ENGLISH, "%s_%x", Long.toHexString(mId), i)); } try { mSampleBuffer.init(ids, mTrackFormats); } catch (IOException e) { // In this case, we will not schedule any further operation. // mExceptionOnPrepare will be notified to ExoPlayer, and ExoPlayer will // call release() eventually. mExceptionOnPrepare = e; return; } mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES); mPrepared = true; } } @Override public void onContinueLoadingRequested(MediaPeriod source) { source.continueLoading(mCurrentPosition); } @Override public boolean handleMessage(Message message) { switch (message.what) { case MSG_PREPARE: if (!mPrepareRequested) { mPrepareRequested = true; mMediaPeriod = mSampleSource.createPeriod(0, new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE), 0); mMediaPeriod.prepare(this); try { mMediaPeriod.maybeThrowPrepareError(); } catch (IOException e) { mError = e; } } return true; case MSG_FETCH_SAMPLES: boolean didSomething = false; ConditionVariable conditionVariable = new ConditionVariable(); int trackCount = mStreams.length; for (int i = 0; i < trackCount; ++i) { if (!mTrackMetEos[i] && C.RESULT_NOTHING_READ != fetchSample(i, mSampleHolder, conditionVariable)) { if (mMetEos) { // If mMetEos was on during fetchSample() due to an error, // fetching from other tracks is not necessary. break; } didSomething = true; } } mMediaPeriod.continueLoading(mCurrentPosition); if (!mMetEos) { if (didSomething) { mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES); } else { mSourceReaderHandler.sendEmptyMessageDelayed(MSG_FETCH_SAMPLES, RETRY_INTERVAL_MS); } } else { notifyCompletionIfNeeded(false); } return true; case MSG_RELEASE: if (mMediaPeriod != null) { mSampleSource.releasePeriod(mMediaPeriod); mSampleSource.releaseSource(); mMediaPeriod = null; } cleanUp(); mSourceReaderHandler.removeCallbacksAndMessages(null); return true; } return false; } private int fetchSample(int track, SampleHolder sample, ConditionVariable conditionVariable) { FormatHolder dummyFormatHolder = new FormatHolder(); mDecoderInputBuffer.clear(); int ret = mStreams[track].readData(dummyFormatHolder, mDecoderInputBuffer); if (ret == C.RESULT_BUFFER_READ // Double-check if the extractor provided the data to prevent NPE. b/33758354 && mDecoderInputBuffer.data != null) { if (mCurrentPosition < mDecoderInputBuffer.timeUs) { mCurrentPosition = mDecoderInputBuffer.timeUs; } try { Long lastExtractedPositionUs = mLastExtractedPositionUsMap.get(track); if (lastExtractedPositionUs == null) { mLastExtractedPositionUsMap.put(track, mDecoderInputBuffer.timeUs); } else { mLastExtractedPositionUsMap.put(track, Math.max(lastExtractedPositionUs, mDecoderInputBuffer.timeUs)); } queueSample(track, conditionVariable); } catch (IOException e) { mLastExtractedPositionUsMap.clear(); mMetEos = true; mSampleBuffer.setEos(); } } else if (ret == C.RESULT_END_OF_INPUT) { mTrackMetEos[track] = true; for (int i = 0; i < mTrackMetEos.length; ++i) { if (!mTrackMetEos[i]) { break; } if (i == mTrackMetEos.length - 1) { mMetEos = true; mSampleBuffer.setEos(); } } } // TODO: Handle C.RESULT_FORMAT_READ for dynamic resolution change. b/28169263 return ret; } private void queueSample(int index, ConditionVariable conditionVariable) throws IOException { if (mVideoTrackIndex != INVALID_TRACK_INDEX) { if (!mVideoTrackMet) { if (index != mVideoTrackIndex) { SampleHolder sample = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); mSampleHolder.flags = (mDecoderInputBuffer.isKeyFrame() ? com.google.android.exoplayer.C.SAMPLE_FLAG_SYNC : 0) | (mDecoderInputBuffer.isDecodeOnly() ? com.google.android.exoplayer.C.SAMPLE_FLAG_DECODE_ONLY : 0); sample.timeUs = mDecoderInputBuffer.timeUs; sample.size = mDecoderInputBuffer.data.position(); sample.ensureSpaceForWrite(sample.size); mDecoderInputBuffer.flip(); sample.data.position(0); sample.data.put(mDecoderInputBuffer.data); sample.data.flip(); mPendingSamples.add(new Pair<>(index, sample)); return; } mVideoTrackMet = true; mBaseSamplePts = mDecoderInputBuffer.timeUs - MpegTsDefaultAudioTrackRenderer .INITIAL_AUDIO_BUFFERING_TIME_US; for (Pair pair : mPendingSamples) { if (pair.second.timeUs >= mBaseSamplePts) { mSampleBuffer.writeSample(pair.first, pair.second, conditionVariable); } } mPendingSamples.clear(); } else { if (mDecoderInputBuffer.timeUs < mBaseSamplePts && mVideoTrackIndex != index) { return; } } } // Copy the decoder input to the sample holder. mSampleHolder.clearData(); mSampleHolder.flags = (mDecoderInputBuffer.isKeyFrame() ? com.google.android.exoplayer.C.SAMPLE_FLAG_SYNC : 0) | (mDecoderInputBuffer.isDecodeOnly() ? com.google.android.exoplayer.C.SAMPLE_FLAG_DECODE_ONLY : 0); mSampleHolder.timeUs = mDecoderInputBuffer.timeUs; mSampleHolder.size = mDecoderInputBuffer.data.position(); mSampleHolder.ensureSpaceForWrite(mSampleHolder.size); mDecoderInputBuffer.flip(); mSampleHolder.data.position(0); mSampleHolder.data.put(mDecoderInputBuffer.data); mSampleHolder.data.flip(); long writeStartTimeNs = SystemClock.elapsedRealtimeNanos(); mSampleBuffer.writeSample(index, mSampleHolder, conditionVariable); // Checks whether the storage has enough bandwidth for recording samples. if (mSampleBuffer.isWriteSpeedSlow(mSampleHolder.size, SystemClock.elapsedRealtimeNanos() - writeStartTimeNs)) { mSampleBuffer.handleWriteSpeedSlow(); } } } @Override public void maybeThrowError() throws IOException { if (mError != null) { IOException e = mError; mError = null; throw e; } } @Override public boolean prepare() throws IOException { if (!mSourceReaderThread.isAlive()) { mSourceReaderThread.start(); mSourceReaderHandler = new Handler(mSourceReaderThread.getLooper(), mSourceReaderWorker); mSourceReaderHandler.sendEmptyMessage(SourceReaderWorker.MSG_PREPARE); } if (mExceptionOnPrepare != null) { throw mExceptionOnPrepare; } return mPrepared; } @Override public List getTrackFormats() { return mTrackFormats; } @Override public void getTrackMediaFormat(int track, MediaFormatHolder outMediaFormatHolder) { outMediaFormatHolder.format = mTrackFormats.get(track); outMediaFormatHolder.drmInitData = null; } @Override public void selectTrack(int index) { mSampleBuffer.selectTrack(index); } @Override public void deselectTrack(int index) { mSampleBuffer.deselectTrack(index); } @Override public long getBufferedPositionUs() { return mSampleBuffer.getBufferedPositionUs(); } @Override public boolean continueBuffering(long positionUs) { return mSampleBuffer.continueBuffering(positionUs); } @Override public void seekTo(long positionUs) { mSampleBuffer.seekTo(positionUs); } @Override public int readSample(int track, SampleHolder sampleHolder) { return mSampleBuffer.readSample(track, sampleHolder); } @Override public void release() { if (mSourceReaderThread.isAlive()) { mSourceReaderHandler.removeCallbacksAndMessages(null); mSourceReaderHandler.sendEmptyMessage(SourceReaderWorker.MSG_RELEASE); mSourceReaderThread.quitSafely(); // Return early in this case so that session worker can start working on the next // request as early as it can. The clean up will be done in the reader thread while // handling MSG_RELEASE. } else { cleanUp(); } } private void cleanUp() { boolean result = true; try { if (mSampleBuffer != null) { mSampleBuffer.release(); mSampleBuffer = null; } } catch (IOException e) { result = false; } notifyCompletionIfNeeded(result); setOnCompletionListener(null, null); } private void notifyCompletionIfNeeded(final boolean result) { if (!mOnCompletionCalled.getAndSet(true)) { final OnCompletionListener listener = mOnCompletionListener; final long lastExtractedPositionUs = getLastExtractedPositionUs(); if (mOnCompletionListenerHandler != null && mOnCompletionListener != null) { mOnCompletionListenerHandler.post(new Runnable() { @Override public void run() { listener.onCompletion(result, lastExtractedPositionUs); } }); } } } private long getLastExtractedPositionUs() { long lastExtractedPositionUs = Long.MIN_VALUE; for (Map.Entry entry : mLastExtractedPositionUsMap.entrySet()) { if (mVideoTrackIndex != entry.getKey()) { lastExtractedPositionUs = Math.max(lastExtractedPositionUs, entry.getValue()); } } if (lastExtractedPositionUs == Long.MIN_VALUE) { lastExtractedPositionUs = com.google.android.exoplayer.C.UNKNOWN_TIME_US; } return lastExtractedPositionUs; } }