aboutsummaryrefslogtreecommitdiff
path: root/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java')
-rw-r--r--src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java378
1 files changed, 266 insertions, 112 deletions
diff --git a/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java b/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java
index c105e222..0b648400 100644
--- a/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java
+++ b/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java
@@ -23,17 +23,28 @@ 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.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder;
-import com.google.android.exoplayer.SampleSource;
-import com.google.android.exoplayer.extractor.ExtractorSampleSource;
-import com.google.android.exoplayer.extractor.ExtractorSampleSource.EventListener;
-import com.google.android.exoplayer.upstream.Allocator;
import com.google.android.exoplayer.upstream.DataSource;
-import com.google.android.exoplayer.upstream.DefaultAllocator;
+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;
@@ -42,10 +53,11 @@ 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;
-import java.util.concurrent.atomic.AtomicLong;
/**
* A class that extracts samples from a live broadcast stream while storing the sample on the disk.
@@ -54,11 +66,7 @@ import java.util.concurrent.atomic.AtomicLong;
public class ExoPlayerSampleExtractor implements SampleExtractor {
private static final String TAG = "ExoPlayerSampleExtracto";
- // Buffer segment size for memory allocator. Copied from demo implementation of ExoPlayer.
- private static final int BUFFER_SEGMENT_SIZE_IN_BYTES = 64 * 1024;
- // Buffer segment count for sample source. Copied from demo implementation of ExoPlayer.
- private static final int BUFFER_SEGMENT_COUNT = 256;
-
+ private static final int INVALID_TRACK_INDEX = -1;
private final HandlerThread mSourceReaderThread;
private final long mId;
@@ -70,36 +78,69 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
private AtomicBoolean mOnCompletionCalled = new AtomicBoolean();
private IOException mExceptionOnPrepare;
private List<MediaFormat> mTrackFormats;
+ private int mVideoTrackIndex = INVALID_TRACK_INDEX;
+ private boolean mVideoTrackMet;
+ private long mBaseSamplePts = Long.MIN_VALUE;
private HashMap<Integer, Long> mLastExtractedPositionUsMap = new HashMap<>();
+ private final List<Pair<Integer, SampleHolder>> mPendingSamples = new LinkedList<>();
private OnCompletionListener mOnCompletionListener;
private Handler mOnCompletionListenerHandler;
private IOException mError;
- public ExoPlayerSampleExtractor(Uri uri, DataSource source, BufferManager bufferManager,
+ 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();
- Allocator allocator = new DefaultAllocator(BUFFER_SEGMENT_SIZE_IN_BYTES);
EventListener eventListener = new EventListener() {
-
@Override
- public void onLoadError(int sourceId, IOException e) {
- mError = e;
+ public void onLoadError(IOException error) {
+ mError = error;
}
};
mSourceReaderThread = new HandlerThread("SourceReaderThread");
- mSourceReaderWorker = new SourceReaderWorker(new ExtractorSampleSource(uri, source,
- allocator, BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE_IN_BYTES,
+ 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, 0));
+ Looper.myLooper() != null ? new Handler() : null, eventListener));
if (isRecording) {
mSampleBuffer = new RecordingSampleBuffer(bufferManager, bufferListener, false,
RecordingSampleBuffer.BUFFER_REASON_RECORDING);
} else {
- if (bufferManager == null || bufferManager.isDisabled()) {
+ if (bufferManager == null) {
mSampleBuffer = new SimpleSampleBuffer(bufferListener);
} else {
mSampleBuffer = new RecordingSampleBuffer(bufferManager, bufferListener, true,
@@ -114,43 +155,141 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
mOnCompletionListenerHandler = handler;
}
- private class SourceReaderWorker implements Handler.Callback {
+ 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 SampleSource mSampleSource;
- private SampleSource.SampleSourceReader mSampleSourceReader;
+ 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(SampleSource sampleSource) {
+ 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<MediaFormat> 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<String> 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:
- mPrepared = prepare();
- if (!mPrepared && mExceptionOnPrepare == null) {
- mSourceReaderHandler
- .sendEmptyMessageDelayed(MSG_PREPARE, RETRY_INTERVAL_MS);
- } else{
- mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES);
+ 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;
- SampleHolder sample = new SampleHolder(
- SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
ConditionVariable conditionVariable = new ConditionVariable();
- int trackCount = mSampleSourceReader.getTrackCount();
+ int trackCount = mStreams.length;
for (int i = 0; i < trackCount; ++i) {
- if (!mTrackMetEos[i] && SampleSource.NOTHING_READ
- != fetchSample(i, sample, conditionVariable)) {
+ 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.
@@ -159,6 +298,7 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
didSomething = true;
}
}
+ mMediaPeriod.continueLoading(mCurrentPosition);
if (!mMetEos) {
if (didSomething) {
mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES);
@@ -171,17 +311,10 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
}
return true;
case MSG_RELEASE:
- if (mSampleSourceReader != null) {
- if (mPrepared) {
- // ExtractorSampleSource expects all the tracks should be disabled
- // before releasing.
- int count = mSampleSourceReader.getTrackCount();
- for (int i = 0; i < count; ++i) {
- mSampleSourceReader.disable(i);
- }
- }
- mSampleSourceReader.release();
- mSampleSourceReader = null;
+ if (mMediaPeriod != null) {
+ mSampleSource.releasePeriod(mMediaPeriod);
+ mSampleSource.releaseSource();
+ mMediaPeriod = null;
}
cleanUp();
mSourceReaderHandler.removeCallbacksAndMessages(null);
@@ -190,91 +323,110 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
return false;
}
- private boolean prepare() {
- if (mSampleSourceReader == null) {
- mSampleSourceReader = mSampleSource.register();
- }
- if(!mSampleSourceReader.prepare(0)) {
- return false;
- }
- if (mTrackFormats == null) {
- int trackCount = mSampleSourceReader.getTrackCount();
- mTrackMetEos = new boolean[trackCount];
- List<MediaFormat> trackFormats = new ArrayList<>();
- for (int i = 0; i < trackCount; i++) {
- trackFormats.add(mSampleSourceReader.getFormat(i));
- mSampleSourceReader.enable(i, 0);
-
- }
- mTrackFormats = trackFormats;
- List<String> 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 false;
- }
- }
- return true;
- }
-
private int fetchSample(int track, SampleHolder sample,
ConditionVariable conditionVariable) {
- mSampleSourceReader.continueBuffering(track, mCurrentPosition);
-
- MediaFormatHolder formatHolder = new MediaFormatHolder();
- sample.clearData();
- int ret = mSampleSourceReader.readData(track, mCurrentPosition, formatHolder, sample);
- if (ret == SampleSource.SAMPLE_READ) {
- if (mCurrentPosition < sample.timeUs) {
- mCurrentPosition = sample.timeUs;
+ 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, sample.timeUs);
+ mLastExtractedPositionUsMap.put(track, mDecoderInputBuffer.timeUs);
} else {
mLastExtractedPositionUsMap.put(track,
- Math.max(lastExtractedPositionUs, sample.timeUs));
+ Math.max(lastExtractedPositionUs, mDecoderInputBuffer.timeUs));
}
- queueSample(track, sample, conditionVariable);
+ queueSample(track, conditionVariable);
} catch (IOException e) {
mLastExtractedPositionUsMap.clear();
mMetEos = true;
mSampleBuffer.setEos();
}
- } else if (ret == SampleSource.END_OF_STREAM) {
+ } 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) {
+ if (i == mTrackMetEos.length - 1) {
mMetEos = true;
mSampleBuffer.setEos();
}
}
}
- // TODO: Handle SampleSource.FORMAT_READ for dynamic resolution change. b/28169263
+ // TODO: Handle C.RESULT_FORMAT_READ for dynamic resolution change. b/28169263
return ret;
}
- }
-
- private void queueSample(int index, SampleHolder sample, ConditionVariable conditionVariable)
- throws IOException {
- long writeStartTimeNs = SystemClock.elapsedRealtimeNanos();
- mSampleBuffer.writeSample(index, sample, conditionVariable);
- // Checks whether the storage has enough bandwidth for recording samples.
- if (mSampleBuffer.isWriteSpeedSlow(sample.size,
- SystemClock.elapsedRealtimeNanos() - writeStartTimeNs)) {
- mSampleBuffer.handleWriteSpeedSlow();
+ 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<Integer, SampleHolder> 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();
+ }
}
}
@@ -328,7 +480,7 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
}
@Override
- public boolean continueBuffering(long positionUs) {
+ public boolean continueBuffering(long positionUs) {
return mSampleBuffer.continueBuffering(positionUs);
}
@@ -386,12 +538,14 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
}
private long getLastExtractedPositionUs() {
- long lastExtractedPositionUs = Long.MAX_VALUE;
- for (long value : mLastExtractedPositionUsMap.values()) {
- lastExtractedPositionUs = Math.min(lastExtractedPositionUs, value);
+ long lastExtractedPositionUs = Long.MIN_VALUE;
+ for (Map.Entry<Integer, Long> entry : mLastExtractedPositionUsMap.entrySet()) {
+ if (mVideoTrackIndex != entry.getKey()) {
+ lastExtractedPositionUs = Math.max(lastExtractedPositionUs, entry.getValue());
+ }
}
- if (lastExtractedPositionUs == Long.MAX_VALUE) {
- lastExtractedPositionUs = C.UNKNOWN_TIME_US;
+ if (lastExtractedPositionUs == Long.MIN_VALUE) {
+ lastExtractedPositionUs = com.google.android.exoplayer.C.UNKNOWN_TIME_US;
}
return lastExtractedPositionUs;
}