diff options
Diffstat (limited to 'src/com/android/tv/tuner/exoplayer')
24 files changed, 1992 insertions, 477 deletions
diff --git a/src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java b/src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java index 5e839223..5f536708 100644 --- a/src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java +++ b/src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java @@ -40,6 +40,7 @@ public class Cea708TextTrackRenderer extends TrackRenderer implements private static final boolean DEBUG = false; public static final int MSG_SERVICE_NUMBER = 1; + public static final int MSG_ENABLE_CLOSED_CAPTION = 2; // According to CEA-708B, the maximum value of closed caption bandwidth is 9600bps. private static final int DEFAULT_INPUT_BUFFER_SIZE = 9600 / 8; @@ -52,11 +53,13 @@ public class Cea708TextTrackRenderer extends TrackRenderer implements private long mCurrentPositionUs; private long mPresentationTimeUs; private int mTrackIndex; + private boolean mRenderingDisabled; private Cea708Parser mCea708Parser; private CcListener mCcListener; public interface CcListener { void emitEvent(CaptionEvent captionEvent); + void clearCaption(); void discoverServiceNumber(int serviceNumber); } @@ -204,7 +207,7 @@ public class Cea708TextTrackRenderer extends TrackRenderer implements } case SampleSource.SAMPLE_READ: { mSampleHolder.data.flip(); - if (mCea708Parser != null) { + if (mCea708Parser != null && !mRenderingDisabled) { mCea708Parser.parseClosedCaption(mSampleHolder.data, mSampleHolder.timeUs); } return true; @@ -274,10 +277,26 @@ public class Cea708TextTrackRenderer extends TrackRenderer implements @Override public void handleMessage(int messageType, Object message) throws ExoPlaybackException { - if (messageType == MSG_SERVICE_NUMBER) { - setServiceNumber((int) message); - } else { - super.handleMessage(messageType, message); + switch (messageType) { + case MSG_SERVICE_NUMBER: + setServiceNumber((int) message); + break; + case MSG_ENABLE_CLOSED_CAPTION: + boolean renderingDisabled = (Boolean) message == false; + if (mRenderingDisabled != renderingDisabled) { + mRenderingDisabled = renderingDisabled; + if (mRenderingDisabled) { + if (mCea708Parser != null) { + mCea708Parser.clear(); + } + if (mCcListener != null) { + mCcListener.clearCaption(); + } + } + } + break; + default: + super.handleMessage(messageType, message); } } } diff --git a/src/com/android/tv/tuner/exoplayer/ExoPlayerExtractorsFactory.java b/src/com/android/tv/tuner/exoplayer/ExoPlayerExtractorsFactory.java new file mode 100644 index 00000000..0ab6d8c4 --- /dev/null +++ b/src/com/android/tv/tuner/exoplayer/ExoPlayerExtractorsFactory.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2017 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 com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.ExtractorsFactory; +import com.google.android.exoplayer2.extractor.TimestampAdjuster; +import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory; +import com.google.android.exoplayer2.extractor.ts.TsExtractor; + +import java.util.ArrayList; +import java.util.List; + +/** + * Extractor factory, mainly aim at create TsExtractor with FLAG_ALLOW_NON_IDR_KEYFRAMES flags for + * H.264 stream + */ +public final class ExoPlayerExtractorsFactory implements ExtractorsFactory { + @Override + public Extractor[] createExtractors() { + // Only create TsExtractor since we only target MPEG2TS stream. + Extractor[] extractors = { + new TsExtractor(new TimestampAdjuster(0), new DefaultTsPayloadReaderFactory( + DefaultTsPayloadReaderFactory.FLAG_ALLOW_NON_IDR_KEYFRAMES), false) }; + return extractors; + } +} 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; } diff --git a/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java b/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java index ec7b4b16..b7e42a7c 100644 --- a/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java +++ b/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java @@ -25,7 +25,6 @@ import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer; import com.android.tv.tuner.tvinput.PlaybackBufferListener; import android.os.Handler; -import android.util.Pair; import java.io.IOException; import java.util.ArrayList; @@ -61,18 +60,17 @@ public class FileSampleExtractor implements SampleExtractor{ @Override public boolean prepare() throws IOException { - ArrayList<Pair<String, android.media.MediaFormat>> trackInfos = - mBufferManager.readTrackInfoFiles(); - if (trackInfos == null || trackInfos.isEmpty()) { + List<BufferManager.TrackFormat> trackFormatList = mBufferManager.readTrackInfoFiles(); + if (trackFormatList == null || trackFormatList.isEmpty()) { throw new IOException("Cannot find meta files for the recording."); } - mTrackCount = trackInfos.size(); + mTrackCount = trackFormatList.size(); List<String> ids = new ArrayList<>(); mTrackFormats.clear(); for (int i = 0; i < mTrackCount; ++i) { - Pair<String, android.media.MediaFormat> pair = trackInfos.get(i); - ids.add(pair.first); - mTrackFormats.add(MediaFormatUtil.createMediaFormat(pair.second)); + BufferManager.TrackFormat trackFormat = trackFormatList.get(i); + ids.add(trackFormat.trackId); + mTrackFormats.add(MediaFormatUtil.createMediaFormat(trackFormat.format)); } mSampleBuffer = new RecordingSampleBuffer(mBufferManager, mBufferListener, true, RecordingSampleBuffer.BUFFER_REASON_RECORDED_PLAYBACK); diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java b/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java index 381b22e9..2694298a 100644 --- a/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java +++ b/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java @@ -39,20 +39,22 @@ import com.android.tv.common.SoftPreconditions; import com.android.tv.tuner.data.Cea708Data; import com.android.tv.tuner.data.Cea708Data.CaptionEvent; import com.android.tv.tuner.data.TunerChannel; -import com.android.tv.tuner.exoplayer.ac3.Ac3PassthroughTrackRenderer; -import com.android.tv.tuner.exoplayer.ac3.Ac3TrackRenderer; +import com.android.tv.tuner.exoplayer.audio.MpegTsDefaultAudioTrackRenderer; +import com.android.tv.tuner.exoplayer.audio.MpegTsMediaCodecAudioTrackRenderer; import com.android.tv.tuner.source.TsDataSource; import com.android.tv.tuner.source.TsDataSourceManager; import com.android.tv.tuner.tvinput.EventDetector; +import com.android.tv.tuner.tvinput.TunerDebug; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -/** - * MPEG-2 TS stream player implementation using ExoPlayer. - */ -public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRenderer.EventListener, - Ac3PassthroughTrackRenderer.EventListener, Ac3TrackRenderer.Ac3EventListener { +/** MPEG-2 TS stream player implementation using ExoPlayer. */ +public class MpegTsPlayer + implements ExoPlayer.Listener, + MediaCodecVideoTrackRenderer.EventListener, + MpegTsDefaultAudioTrackRenderer.EventListener, + MpegTsMediaCodecAudioTrackRenderer.Ac3EventListener { private int mCaptionServiceNumber = Cea708Data.EMPTY_SERVICE_NUMBER; /** @@ -60,7 +62,7 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen */ public interface RendererBuilder { void buildRenderers(MpegTsPlayer mpegTsPlayer, DataSource dataSource, - RendererBuilderCallback callback); + boolean hasSoftwareAudioDecoder, RendererBuilderCallback callback); } /** @@ -94,6 +96,11 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen void onEmitCaptionEvent(CaptionEvent event); /** + * Notifies clearing up whole closed caption event. + */ + void onClearCaptionEvent(); + + /** * Notifies the discovered caption service number. */ void onDiscoverCaptionServiceNumber(int serviceNumber); @@ -215,10 +222,11 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen * Creates renderers and {@link DataSource} and initializes player. * @param context a {@link Context} instance * @param channel to play + * @param hasSoftwareAudioDecoder {@code true} if there is connected software decoder * @param eventListener for program information which will be scanned from MPEG2-TS stream * @return true when everything is created and initialized well, false otherwise */ - public boolean prepare(Context context, TunerChannel channel, + public boolean prepare(Context context, TunerChannel channel, boolean hasSoftwareAudioDecoder, EventDetector.EventListener eventListener) { TsDataSource source = null; if (channel != null) { @@ -236,7 +244,7 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen } mRendererBuildingState = RENDERER_BUILDING_STATE_BUILDING; mBuilderCallback = new InternalRendererBuilderCallback(); - mRendererBuilder.buildRenderers(this, source, mBuilderCallback); + mRendererBuilder.buildRenderers(this, source, hasSoftwareAudioDecoder, mBuilderCallback); return true; } @@ -304,8 +312,10 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen SoftPreconditions.checkState(supportSmoothTrickPlay(playbackParams.getSpeed())); mPlayer.setPlayWhenReady(true); mTrickplayRunning = true; - if (mAudioRenderer instanceof Ac3PassthroughTrackRenderer) { - mPlayer.sendMessage(mAudioRenderer, Ac3PassthroughTrackRenderer.MSG_SET_PLAYBACK_SPEED, + if (mAudioRenderer instanceof MpegTsDefaultAudioTrackRenderer) { + mPlayer.sendMessage( + mAudioRenderer, + MpegTsDefaultAudioTrackRenderer.MSG_SET_PLAYBACK_SPEED, playbackParams.getSpeed()); } else { mPlayer.sendMessage(mAudioRenderer, @@ -317,9 +327,9 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen private void stopSmoothTrickplay(boolean calledBySeek) { if (mTrickplayRunning) { mTrickplayRunning = false; - if (mAudioRenderer instanceof Ac3PassthroughTrackRenderer) { - mPlayer.sendMessage(mAudioRenderer, - Ac3PassthroughTrackRenderer.MSG_SET_PLAYBACK_SPEED, + if (mAudioRenderer instanceof MpegTsDefaultAudioTrackRenderer) { + mPlayer.sendMessage( + mAudioRenderer, MpegTsDefaultAudioTrackRenderer.MSG_SET_PLAYBACK_SPEED, 1.0f); } else { mPlayer.sendMessage(mAudioRenderer, @@ -423,8 +433,9 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen */ public void setVolume(float volume) { mVolume = volume; - if (mAudioRenderer instanceof Ac3PassthroughTrackRenderer) { - mPlayer.sendMessage(mAudioRenderer, Ac3PassthroughTrackRenderer.MSG_SET_VOLUME, volume); + if (mAudioRenderer instanceof MpegTsDefaultAudioTrackRenderer) { + mPlayer.sendMessage(mAudioRenderer, MpegTsDefaultAudioTrackRenderer.MSG_SET_VOLUME, + volume); } else { mPlayer.sendMessage(mAudioRenderer, MediaCodecAudioTrackRenderer.MSG_SET_VOLUME, volume); @@ -432,18 +443,20 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen } /** - * Enables or disables audio. + * Enables or disables audio and closed caption. * - * @param enable enables the audio when {@code true}, disables otherwise. + * @param enable enables the audio and closed caption when {@code true}, disables otherwise. */ - public void setAudioTrack(boolean enable) { - if (mAudioRenderer instanceof Ac3PassthroughTrackRenderer) { - mPlayer.sendMessage(mAudioRenderer, Ac3PassthroughTrackRenderer.MSG_SET_AUDIO_TRACK, + public void setAudioTrackAndClosedCaption(boolean enable) { + if (mAudioRenderer instanceof MpegTsDefaultAudioTrackRenderer) { + mPlayer.sendMessage(mAudioRenderer, MpegTsDefaultAudioTrackRenderer.MSG_SET_AUDIO_TRACK, enable ? 1 : 0); } else { mPlayer.sendMessage(mAudioRenderer, MediaCodecAudioTrackRenderer.MSG_SET_VOLUME, enable ? mVolume : 0.0f); } + mPlayer.sendMessage(mTextRenderer, Cea708TextTrackRenderer.MSG_ENABLE_CLOSED_CAPTION, + enable); } /** @@ -495,6 +508,28 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen } /** + * Returns the index of the currently selected track for the specified renderer. + * + * @param rendererIndex The index of the renderer. + * @return The selected track. A negative value or a value greater than or equal to the renderer's + * track count indicates that the renderer is disabled. + */ + public int getSelectedTrack(int rendererIndex) { + return mPlayer.getSelectedTrack(rendererIndex); + } + + /** + * Returns the format of a track. + * + * @param rendererIndex The index of the renderer. + * @param trackIndex The index of the track. + * @return The format of the track. + */ + public MediaFormat getTrackFormat(int rendererIndex, int trackIndex) { + return mPlayer.getTrackFormat(rendererIndex, trackIndex); + } + + /** * Gets the main handler of the player. */ /* package */ Handler getMainHandler() { @@ -579,6 +614,7 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen @Override public void onDroppedFrames(int count, long elapsed) { + TunerDebug.notifyVideoFrameDrop(count, elapsed); if (mTrickplayRunning && mListener != null) { mListener.onSmoothTrickplayForceStopped(); } @@ -622,6 +658,13 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen } @Override + public void clearCaption() { + if (mVideoEventListener != null) { + mVideoEventListener.onClearCaptionEvent(); + } + } + + @Override public void discoverServiceNumber(int serviceNumber) { if (mVideoEventListener != null) { mVideoEventListener.onDiscoverCaptionServiceNumber(serviceNumber); @@ -650,4 +693,4 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen } } } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java b/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java index 0e46c9cf..006ccac2 100644 --- a/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java +++ b/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java @@ -18,12 +18,14 @@ package com.android.tv.tuner.exoplayer; import android.content.Context; +import com.google.android.exoplayer.MediaCodecSelector; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.upstream.DataSource; +import com.android.tv.Features; import com.android.tv.tuner.exoplayer.MpegTsPlayer.RendererBuilder; import com.android.tv.tuner.exoplayer.MpegTsPlayer.RendererBuilderCallback; -import com.android.tv.tuner.exoplayer.ac3.Ac3PassthroughTrackRenderer; +import com.android.tv.tuner.exoplayer.audio.MpegTsDefaultAudioTrackRenderer; import com.android.tv.tuner.exoplayer.buffer.BufferManager; import com.android.tv.tuner.tvinput.PlaybackBufferListener; @@ -44,7 +46,7 @@ public class MpegTsRendererBuilder implements RendererBuilder { @Override public void buildRenderers(MpegTsPlayer mpegTsPlayer, DataSource dataSource, - RendererBuilderCallback callback) { + boolean mHasSoftwareAudioDecoder, RendererBuilderCallback callback) { // Build the video and audio renderers. SampleExtractor extractor = dataSource == null ? new MpegTsSampleExtractor(mBufferManager, mBufferListener) : @@ -52,10 +54,16 @@ public class MpegTsRendererBuilder implements RendererBuilder { SampleSource sampleSource = new MpegTsSampleSource(extractor); MpegTsVideoTrackRenderer videoRenderer = new MpegTsVideoTrackRenderer(mContext, sampleSource, mpegTsPlayer.getMainHandler(), mpegTsPlayer); - // TODO: Only using Ac3PassthroughTrackRenderer for A/V sync issue. We will use - // {@link Ac3TrackRenderer} when we use ExoPlayer's extractor. - TrackRenderer audioRenderer = new Ac3PassthroughTrackRenderer(sampleSource, - mpegTsPlayer.getMainHandler(), mpegTsPlayer); + // TODO: Only using MpegTsDefaultAudioTrackRenderer for A/V sync issue. We will use + // {@link MpegTsMediaCodecAudioTrackRenderer} when we use ExoPlayer's extractor. + TrackRenderer audioRenderer = + new MpegTsDefaultAudioTrackRenderer( + sampleSource, + MediaCodecSelector.DEFAULT, + mpegTsPlayer.getMainHandler(), + mpegTsPlayer, + mHasSoftwareAudioDecoder, + !Features.AC3_SOFTWARE_DECODE.isEnabled(mContext)); Cea708TextTrackRenderer textRenderer = new Cea708TextTrackRenderer(sampleSource); TrackRenderer[] renderers = new TrackRenderer[MpegTsPlayer.RENDERER_COUNT]; diff --git a/src/com/android/tv/tuner/exoplayer/ac3/AudioClock.java b/src/com/android/tv/tuner/exoplayer/audio/AudioClock.java index 600c2c88..5666c5b9 100644 --- a/src/com/android/tv/tuner/exoplayer/ac3/AudioClock.java +++ b/src/com/android/tv/tuner/exoplayer/audio/AudioClock.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.tuner.exoplayer.ac3; +package com.android.tv.tuner.exoplayer.audio; import com.android.tv.common.SoftPreconditions; diff --git a/src/com/android/tv/tuner/exoplayer/audio/AudioDecoder.java b/src/com/android/tv/tuner/exoplayer/audio/AudioDecoder.java new file mode 100644 index 00000000..e581092a --- /dev/null +++ b/src/com/android/tv/tuner/exoplayer/audio/AudioDecoder.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2017 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.audio; + +import com.google.android.exoplayer.ExoPlaybackException; +import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.SampleHolder; + +import java.nio.ByteBuffer; + +/** A base class for audio decoders. */ +public abstract class AudioDecoder { + + /** + * Decodes an audio sample. + * + * @param sampleHolder a holder that contains the sample data and corresponding metadata + */ + public abstract void decode(SampleHolder sampleHolder); + + /** Returns a decoded sample from decoder. */ + public abstract ByteBuffer getDecodedSample(); + + /** Returns the presentation time for the decoded sample. */ + public abstract long getDecodedTimeUs(); + + /** + * Clear previous decode state if any. Prepares to decode samples of the specified encoding. + * This method should be called before using decode. + * + * @param mime audio encoding + */ + public abstract void resetDecoderState(String mimeType); + + /** Releases all the resource. */ + public abstract void release(); + + /** + * Init decoder if needed. + * + * @param format the format used to initialize decoder + */ + public void maybeInitDecoder(MediaFormat format) throws ExoPlaybackException { + // Do nothing. + } + + /** Returns input buffer that will be used in decoder. */ + public ByteBuffer getInputBuffer() { + return null; + } + + /** Returns the output format. */ + public android.media.MediaFormat getOutputFormat() { + return null; + } +} diff --git a/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackMonitor.java b/src/com/android/tv/tuner/exoplayer/audio/AudioTrackMonitor.java index bfdf08ac..ec616b13 100644 --- a/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackMonitor.java +++ b/src/com/android/tv/tuner/exoplayer/audio/AudioTrackMonitor.java @@ -14,12 +14,13 @@ * limitations under the License. */ -package com.android.tv.tuner.exoplayer.ac3; +package com.android.tv.tuner.exoplayer.audio; import android.os.SystemClock; import android.util.Log; import android.util.Pair; +import com.google.android.exoplayer.util.MimeTypes; import java.util.ArrayList; import java.util.HashSet; import java.util.Set; @@ -35,7 +36,7 @@ public class AudioTrackMonitor { private final ArrayList<Pair<Long, Integer>> mPtsList = new ArrayList<>(); private final Set<Integer> mSampleSize = new HashSet<>(); private final Set<Integer> mCurSampleSize = new HashSet<>(); - private final Set<Integer> mAc3Header = new HashSet<>(); + private final Set<Integer> mHeader = new HashSet<>(); private long mExpireMs; private long mDuration; @@ -43,6 +44,8 @@ public class AudioTrackMonitor { private long mTotalCount; private long mStartMs; + private boolean mIsMp2; + private void flush() { mExpireMs += mDuration; mSampleCount = 0; @@ -61,10 +64,14 @@ public class AudioTrackMonitor { mTotalCount = 0; mStartMs = 0; mSampleSize.clear(); - mAc3Header.clear(); + mHeader.clear(); flush(); } + public void setEncoding(String mime) { + mIsMp2 = MimeTypes.AUDIO_MPEG_L2.equalsIgnoreCase(mime); + } + /** * Adds an audio sample information for monitoring. * @@ -76,7 +83,7 @@ public class AudioTrackMonitor { mTotalCount++; mSampleCount++; mSampleSize.add(sampleSize); - mAc3Header.add(header); + mHeader.add(header); mCurSampleSize.add(sampleSize); if (mTotalCount == 1) { mStartMs = SystemClock.elapsedRealtime(); @@ -98,8 +105,9 @@ public class AudioTrackMonitor { long now = SystemClock.elapsedRealtime(); if (mExpireMs != 0 && now >= mExpireMs) { if (DEBUG) { - long sampleDuration = (mTotalCount - 1) * - Ac3PassthroughTrackRenderer.AC3_SAMPLE_DURATION_US / 1000; + long unitDuration = mIsMp2 ? MpegTsDefaultAudioTrackRenderer.MP2_SAMPLE_DURATION_US + : MpegTsDefaultAudioTrackRenderer.AC3_SAMPLE_DURATION_US; + long sampleDuration = (mTotalCount - 1) * unitDuration / 1000; long totalDuration = now - mStartMs; StringBuilder ptsBuilder = new StringBuilder(); ptsBuilder.append("PTS received ").append(mSampleCount).append(", ") @@ -113,7 +121,7 @@ public class AudioTrackMonitor { } if (DEBUG || mCurSampleSize.size() > 1) { Log.d(TAG, "PTS received sample size: " - + String.valueOf(mSampleSize) + mCurSampleSize + mAc3Header); + + String.valueOf(mSampleSize) + mCurSampleSize + mHeader); } flush(); } diff --git a/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackWrapper.java b/src/com/android/tv/tuner/exoplayer/audio/AudioTrackWrapper.java index bc3c5d00..953c9fc4 100644 --- a/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackWrapper.java +++ b/src/com/android/tv/tuner/exoplayer/audio/AudioTrackWrapper.java @@ -14,10 +14,11 @@ * limitations under the License. */ -package com.android.tv.tuner.exoplayer.ac3; +package com.android.tv.tuner.exoplayer.audio; import android.media.MediaFormat; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.audio.AudioTrack; import java.nio.ByteBuffer; @@ -28,6 +29,10 @@ import java.nio.ByteBuffer; * This wrapper class will do nothing in disabled status for those operations. */ public class AudioTrackWrapper { + private static final int PCM16_FRAME_BYTES = 2; + private static final int AC3_FRAMES_IN_ONE_SAMPLE = 1536; + private static final int BUFFERED_SAMPLES_IN_AUDIOTRACK = + MpegTsDefaultAudioTrackRenderer.BUFFERED_SAMPLES_IN_AUDIOTRACK; private final AudioTrack mAudioTrack = new AudioTrack(); private int mAudioSessionID; private boolean mIsEnabled; @@ -106,7 +111,7 @@ public class AudioTrackWrapper { mAudioTrack.setVolume(volume); } - public void reconfigure(MediaFormat format) { + public void reconfigure(MediaFormat format, int audioBufferSize) { if (!mIsEnabled || format == null) { return; } @@ -117,9 +122,9 @@ public class AudioTrackWrapper { try { pcmEncoding = format.getInteger(MediaFormat.KEY_PCM_ENCODING); } catch (Exception e) { - pcmEncoding = com.google.android.exoplayer.MediaFormat.NO_VALUE; + pcmEncoding = C.ENCODING_PCM_16BIT; } - // TODO: Handle non-AC3 or non-passthrough audio. + // TODO: Handle non-AC3. if (MediaFormat.MIMETYPE_AUDIO_AC3.equalsIgnoreCase(mimeType) && channelCount != 2) { // Workarounds b/25955476. // Since all devices and platforms does not support passthrough for non-stereo AC3, @@ -127,7 +132,14 @@ public class AudioTrackWrapper { // In other words, the channel count should be always 2. channelCount = 2; } - mAudioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding); + if (MediaFormat.MIMETYPE_AUDIO_RAW.equalsIgnoreCase(mimeType)) { + audioBufferSize = + channelCount + * PCM16_FRAME_BYTES + * AC3_FRAMES_IN_ONE_SAMPLE + * BUFFERED_SAMPLES_IN_AUDIOTRACK; + } + mAudioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, audioBufferSize); } public void handleDiscontinuity() { diff --git a/src/com/android/tv/tuner/exoplayer/audio/MediaCodecAudioDecoder.java b/src/com/android/tv/tuner/exoplayer/audio/MediaCodecAudioDecoder.java new file mode 100644 index 00000000..72bc68b6 --- /dev/null +++ b/src/com/android/tv/tuner/exoplayer/audio/MediaCodecAudioDecoder.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2017 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.audio; + +import android.media.MediaCodec; +import android.util.Log; + +import com.google.android.exoplayer.CodecCounters; +import com.google.android.exoplayer.DecoderInfo; +import com.google.android.exoplayer.ExoPlaybackException; +import com.google.android.exoplayer.MediaCodecSelector; +import com.google.android.exoplayer.MediaCodecUtil; +import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.SampleHolder; + +import java.nio.ByteBuffer; +import java.util.ArrayList; + +/** A decoder to use MediaCodec for decoding audio stream. */ +public class MediaCodecAudioDecoder extends AudioDecoder { + private static final String TAG = "MediaCodecAudioDecoder"; + + public static final int INDEX_INVALID = -1; + + private final CodecCounters mCodecCounters; + private final MediaCodecSelector mSelector; + + private MediaCodec mCodec; + private MediaCodec.BufferInfo mOutputBufferInfo; + private ByteBuffer mMediaCodecOutputBuffer; + private ArrayList<Long> mDecodeOnlyPresentationTimestamps; + private boolean mWaitingForFirstSyncFrame; + private boolean mIsNewIndex; + private int mInputIndex; + private int mOutputIndex; + + /** Creates a MediaCodec based audio decoder. */ + public MediaCodecAudioDecoder(MediaCodecSelector selector) { + mSelector = selector; + mOutputBufferInfo = new MediaCodec.BufferInfo(); + mCodecCounters = new CodecCounters(); + mDecodeOnlyPresentationTimestamps = new ArrayList<>(); + } + + /** Returns {@code true} if there is decoder for {@code mimeType}. */ + public static boolean supportMimeType(MediaCodecSelector selector, String mimeType) { + if (selector == null) { + return false; + } + return getDecoderInfo(selector, mimeType) != null; + } + + private static DecoderInfo getDecoderInfo(MediaCodecSelector selector, String mimeType) { + try { + return selector.getDecoderInfo(mimeType, false); + } catch (MediaCodecUtil.DecoderQueryException e) { + Log.e(TAG, "Select decoder error:" + e); + return null; + } + } + + private boolean shouldInitCodec(MediaFormat format) { + return format != null && mCodec == null; + } + + @Override + public void maybeInitDecoder(MediaFormat format) throws ExoPlaybackException { + if (!shouldInitCodec(format)) { + return; + } + + String mimeType = format.mimeType; + DecoderInfo decoderInfo = getDecoderInfo(mSelector, mimeType); + if (decoderInfo == null) { + Log.i(TAG, "There is not decoder found for " + mimeType); + return; + } + + String codecName = decoderInfo.name; + try { + mCodec = MediaCodec.createByCodecName(codecName); + mCodec.configure(format.getFrameworkMediaFormatV16(), null, null, 0); + mCodec.start(); + } catch (Exception e) { + Log.e(TAG, "Failed when configure or start codec:" + e); + throw new ExoPlaybackException(e); + } + mInputIndex = INDEX_INVALID; + mOutputIndex = INDEX_INVALID; + mWaitingForFirstSyncFrame = true; + mCodecCounters.codecInitCount++; + } + + @Override + public void resetDecoderState(String mimeType) { + if (mCodec == null) { + return; + } + mInputIndex = INDEX_INVALID; + mOutputIndex = INDEX_INVALID; + mDecodeOnlyPresentationTimestamps.clear(); + mCodec.flush(); + mWaitingForFirstSyncFrame = true; + } + + @Override + public void release() { + if (mCodec != null) { + mDecodeOnlyPresentationTimestamps.clear(); + mInputIndex = INDEX_INVALID; + mOutputIndex = INDEX_INVALID; + mCodecCounters.codecReleaseCount++; + try { + mCodec.stop(); + } finally { + try { + mCodec.release(); + } finally { + mCodec = null; + } + } + } + } + + /** Returns the index of input buffer which is ready for using. */ + public int getInputIndex() { + return mInputIndex; + } + + @Override + public ByteBuffer getInputBuffer() { + if (mInputIndex < 0) { + mInputIndex = mCodec.dequeueInputBuffer(0); + if (mInputIndex < 0) { + return null; + } + return mCodec.getInputBuffer(mInputIndex); + } + return mCodec.getInputBuffer(mInputIndex); + } + + @Override + public void decode(SampleHolder sampleHolder) { + if (mWaitingForFirstSyncFrame) { + if (!sampleHolder.isSyncFrame()) { + sampleHolder.clearData(); + return; + } + mWaitingForFirstSyncFrame = false; + } + long presentationTimeUs = sampleHolder.timeUs; + if (sampleHolder.isDecodeOnly()) { + mDecodeOnlyPresentationTimestamps.add(presentationTimeUs); + } + mCodec.queueInputBuffer(mInputIndex, 0, sampleHolder.data.limit(), presentationTimeUs, 0); + mInputIndex = INDEX_INVALID; + mCodecCounters.inputBufferCount++; + } + + private int getDecodeOnlyIndex(long presentationTimeUs) { + final int size = mDecodeOnlyPresentationTimestamps.size(); + for (int i = 0; i < size; i++) { + if (mDecodeOnlyPresentationTimestamps.get(i).longValue() == presentationTimeUs) { + return i; + } + } + return INDEX_INVALID; + } + + /** Returns the index of output buffer which is ready for using. */ + public int getOutputIndex() { + if (mOutputIndex < 0) { + mOutputIndex = mCodec.dequeueOutputBuffer(mOutputBufferInfo, 0); + mIsNewIndex = true; + } else { + mIsNewIndex = false; + } + return mOutputIndex; + } + + @Override + public android.media.MediaFormat getOutputFormat() { + return mCodec.getOutputFormat(); + } + + /** Returns {@code true} if the output is only for decoding but not for rendering. */ + public boolean maybeDecodeOnlyIndex() { + int decodeOnlyIndex = getDecodeOnlyIndex(mOutputBufferInfo.presentationTimeUs); + if (decodeOnlyIndex != INDEX_INVALID) { + mCodec.releaseOutputBuffer(mOutputIndex, false); + mCodecCounters.skippedOutputBufferCount++; + mDecodeOnlyPresentationTimestamps.remove(decodeOnlyIndex); + mOutputIndex = INDEX_INVALID; + return true; + } + return false; + } + + @Override + public ByteBuffer getDecodedSample() { + if (maybeDecodeOnlyIndex() || mOutputIndex < 0) { + return null; + } + if (mIsNewIndex) { + mMediaCodecOutputBuffer = mCodec.getOutputBuffer(mOutputIndex); + } + return mMediaCodecOutputBuffer; + } + + @Override + public long getDecodedTimeUs() { + return mOutputBufferInfo.presentationTimeUs; + } + + /** Releases the output buffer after rendering. */ + public void releaseOutputBuffer() { + mCodecCounters.renderedOutputBufferCount++; + mCodec.releaseOutputBuffer(mOutputIndex, false); + mOutputIndex = INDEX_INVALID; + } +} diff --git a/src/com/android/tv/tuner/exoplayer/ac3/Ac3PassthroughTrackRenderer.java b/src/com/android/tv/tuner/exoplayer/audio/MpegTsDefaultAudioTrackRenderer.java index 9dae2e34..77170419 100644 --- a/src/com/android/tv/tuner/exoplayer/ac3/Ac3PassthroughTrackRenderer.java +++ b/src/com/android/tv/tuner/exoplayer/audio/MpegTsDefaultAudioTrackRenderer.java @@ -14,8 +14,10 @@ * limitations under the License. */ -package com.android.tv.tuner.exoplayer.ac3; +package com.android.tv.tuner.exoplayer.audio; +import android.media.MediaCodec; +import android.os.Build; import android.os.Handler; import android.os.SystemClock; import android.util.Log; @@ -23,16 +25,16 @@ import android.util.Log; import com.google.android.exoplayer.CodecCounters; import com.google.android.exoplayer.ExoPlaybackException; import com.google.android.exoplayer.MediaClock; -import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; +import com.google.android.exoplayer.MediaCodecSelector; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormatHolder; -import com.google.android.exoplayer.MediaFormatUtil; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.audio.AudioTrack; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.MimeTypes; +import com.android.tv.tuner.exoplayer.ffmpeg.FfmpegDecoderClient; import com.android.tv.tuner.tvinput.TunerDebug; import java.io.IOException; @@ -40,9 +42,10 @@ import java.nio.ByteBuffer; import java.util.ArrayList; /** - * Decodes and renders AC3 audio. + * Decodes and renders DTV audio. Supports MediaCodec based decoding, passthrough playback and + * ffmpeg based software decoding (AC3, MP2). */ -public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaClock { +public class MpegTsDefaultAudioTrackRenderer extends TrackRenderer implements MediaClock { public static final int MSG_SET_VOLUME = 10000; public static final int MSG_SET_AUDIO_TRACK = MSG_SET_VOLUME + 1; public static final int MSG_SET_PLAYBACK_SPEED = MSG_SET_VOLUME + 2; @@ -51,7 +54,19 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC // One AC3 sample has 1536 frames, and its duration is 32ms. public static final long AC3_SAMPLE_DURATION_US = 32000; - private static final String TAG = "Ac3PassthroughTrackRenderer"; + // TODO: Check whether DVB broadcasting uses sample rate other than 48Khz. + // MPEG-1 audio Layer II and III has 1152 frames per sample. + // 1152 frames duration is 24ms when sample rate is 48Khz. + static final long MP2_SAMPLE_DURATION_US = 24000; + + // This is around 150ms, 150ms is big enough not to under-run AudioTrack, + // and 150ms is also small enough to fill the buffer rapidly. + static int BUFFERED_SAMPLES_IN_AUDIOTRACK = 5; + public static final long INITIAL_AUDIO_BUFFERING_TIME_US = + BUFFERED_SAMPLES_IN_AUDIOTRACK * AC3_SAMPLE_DURATION_US; + + + private static final String TAG = "MpegTsDefaultAudioTrac"; private static final boolean DEBUG = false; /** @@ -67,6 +82,8 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC private static final int DEFAULT_OUTPUT_BUFFER_SIZE = 1024*1024; private static final int MONITOR_DURATION_MS = 1000; private static final int AC3_HEADER_BITRATE_OFFSET = 4; + private static final int MP2_HEADER_BITRATE_OFFSET = 2; + private static final int MP2_HEADER_BITRATE_MASK = 0xfc; // Keep this as static in order to prevent new framework AudioTrack creation // while old AudioTrack is being released. @@ -83,17 +100,25 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC // PTS interpolated time should be delayed reasonably when AudioTrack is not used. private static final long ESTIMATED_TRACK_RENDERING_DELAY_US = 500000; + private final MediaCodecSelector mSelector; + private final CodecCounters mCodecCounters; private final SampleSource.SampleSourceReader mSource; - private final SampleHolder mSampleHolder; private final MediaFormatHolder mFormatHolder; private final EventListener mEventListener; private final Handler mEventHandler; private final AudioTrackMonitor mMonitor; private final AudioClock mAudioClock; + private final boolean mAc3Passthrough; + private final boolean mSoftwareDecoderAvailable; private MediaFormat mFormat; + private SampleHolder mSampleHolder; + private String mDecodingMime; + private boolean mFormatConfigured; + private int mSampleSize; private final ByteBuffer mOutputBuffer; + private AudioDecoder mAudioDecoder; private boolean mOutputReady; private int mTrackIndex; private boolean mSourceStateReady; @@ -106,16 +131,23 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC private long mInterpolatedTimeUs; private long mPreviousPositionUs; private boolean mIsStopped; + private boolean mEnabled = true; + private boolean mIsMuted; private ArrayList<Integer> mTracksIndex; - - public Ac3PassthroughTrackRenderer(SampleSource source, Handler eventHandler, - EventListener listener) { + private boolean mUseFrameworkDecoder; + + public MpegTsDefaultAudioTrackRenderer( + SampleSource source, + MediaCodecSelector selector, + Handler eventHandler, + EventListener listener, + boolean hasSoftwareAudioDecoder, + boolean usePassthrough) { mSource = source.register(); + mSelector = selector; mEventHandler = eventHandler; mEventListener = listener; mTrackIndex = -1; - mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DIRECT); - mSampleHolder.ensureSpaceForWrite(DEFAULT_INPUT_BUFFER_SIZE); mOutputBuffer = ByteBuffer.allocate(DEFAULT_OUTPUT_BUFFER_SIZE); mFormatHolder = new MediaFormatHolder(); AUDIO_TRACK.restart(); @@ -123,6 +155,8 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC mMonitor = new AudioTrackMonitor(); mAudioClock = new AudioClock(); mTracksIndex = new ArrayList<>(); + mAc3Passthrough = usePassthrough; + mSoftwareDecoderAvailable = hasSoftwareAudioDecoder && FfmpegDecoderClient.isAvailable(); } @Override @@ -130,8 +164,11 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC return this; } - private static boolean handlesMimeType(String mimeType) { - return mimeType.equals(MimeTypes.AUDIO_AC3) || mimeType.equals(MimeTypes.AUDIO_E_AC3); + private boolean handlesMimeType(String mimeType) { + return mimeType.equals(MimeTypes.AUDIO_AC3) + || mimeType.equals(MimeTypes.AUDIO_E_AC3) + || mimeType.equals(MimeTypes.AUDIO_MPEG_L2) + || MediaCodecAudioDecoder.supportMimeType(mSelector, mimeType); } @Override @@ -141,7 +178,8 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC return false; } for (int i = 0; i < mSource.getTrackCount(); i++) { - if (handlesMimeType(mSource.getFormat(i).mimeType)) { + String mimeType = mSource.getFormat(i).mimeType; + if (MimeTypes.isAudio(mimeType) && handlesMimeType(mimeType)) { if (mTrackIndex < 0) { mTrackIndex = i; } @@ -174,7 +212,9 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC @Override protected void onDisabled() { - AUDIO_TRACK.resetSessionId(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + AUDIO_TRACK.resetSessionId(); + } clearDecodeState(); mFormat = null; mSource.disable(mTrackIndex); @@ -182,6 +222,7 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC @Override protected void onReleased() { + releaseDecoder(); AUDIO_TRACK.release(); mSource.release(); } @@ -213,9 +254,12 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC protected void seekTo(long positionUs) { mSource.seekToUs(positionUs); AUDIO_TRACK.reset(); - // resetSessionId() will create a new framework AudioTrack instead of reusing old one. - AUDIO_TRACK.resetSessionId(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + // resetSessionId() will create a new framework AudioTrack instead of reusing old one. + AUDIO_TRACK.resetSessionId(); + } seekToInternal(positionUs); + clearDecodeState(); } @Override @@ -274,7 +318,10 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC return; } - // Process only one sample at a time for doSomeWork() + if (mAudioDecoder != null) { + mAudioDecoder.maybeInitDecoder(mFormat); + } + // Process only one sample at a time for doSomeWork() when using FFmpeg decoder. if (processOutput()) { if (!mOutputReady) { while (feedInputBuffer()) { @@ -314,9 +361,18 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC private void clearDecodeState() { mOutputReady = false; + if (mAudioDecoder != null) { + mAudioDecoder.resetDecoderState(mDecodingMime); + } AUDIO_TRACK.reset(); } + private void releaseDecoder() { + if (mAudioDecoder != null) { + mAudioDecoder.release(); + } + } + private void readFormat() throws IOException, ExoPlaybackException { int result = mSource.readData(mTrackIndex, mCurrentPositionUs, mFormatHolder, mSampleHolder); @@ -325,14 +381,69 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC } } + private MediaFormat convertMediaFormatToRaw(MediaFormat format) { + return MediaFormat.createAudioFormat( + format.trackId, + MimeTypes.AUDIO_RAW, + format.bitrate, + format.maxInputSize, + format.durationUs, + format.channelCount, + format.sampleRate, + format.initializationData, + format.language); + } + private void onInputFormatChanged(MediaFormatHolder formatHolder) throws ExoPlaybackException { - mFormat = formatHolder.format; - if (DEBUG) { + String mimeType = formatHolder.format.mimeType; + mUseFrameworkDecoder = MediaCodecAudioDecoder.supportMimeType(mSelector, mimeType); + if (mUseFrameworkDecoder) { + mAudioDecoder = new MediaCodecAudioDecoder(mSelector); + mFormat = formatHolder.format; + mAudioDecoder.maybeInitDecoder(mFormat); + mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DISABLED); + } else if (mSoftwareDecoderAvailable + && (MimeTypes.AUDIO_MPEG_L2.equalsIgnoreCase(mimeType) + || MimeTypes.AUDIO_AC3.equalsIgnoreCase(mimeType) && !mAc3Passthrough)) { + releaseDecoder(); + mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DIRECT); + mSampleHolder.ensureSpaceForWrite(DEFAULT_INPUT_BUFFER_SIZE); + mAudioDecoder = FfmpegDecoderClient.getInstance(); + mDecodingMime = mimeType; + mFormat = convertMediaFormatToRaw(formatHolder.format); + } else { + mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DIRECT); + mSampleHolder.ensureSpaceForWrite(DEFAULT_INPUT_BUFFER_SIZE); + mFormat = formatHolder.format; + releaseDecoder(); + } + mFormatConfigured = true; + mMonitor.setEncoding(mimeType); + if (DEBUG && !mUseFrameworkDecoder) { Log.d(TAG, "AudioTrack was configured to FORMAT: " + mFormat.toString()); } clearDecodeState(); - AUDIO_TRACK.reconfigure(mFormat.getFrameworkMediaFormatV16()); + if (!mUseFrameworkDecoder) { + AUDIO_TRACK.reconfigure(mFormat.getFrameworkMediaFormatV16(), 0); + } + } + + private void onSampleSizeChanged(int sampleSize) { + if (DEBUG) { + Log.d(TAG, "Sample size was changed to : " + sampleSize); + } + clearDecodeState(); + int audioBufferSize = sampleSize * BUFFERED_SAMPLES_IN_AUDIOTRACK; + mSampleSize = sampleSize; + AUDIO_TRACK.reconfigure(mFormat.getFrameworkMediaFormatV16(), audioBufferSize); + } + + private void onOutputFormatChanged(android.media.MediaFormat format) { + if (DEBUG) { + Log.d(TAG, "AudioTrack was configured to FORMAT: " + format.toString()); + } + AUDIO_TRACK.reconfigure(format, 0); } private boolean feedInputBuffer() throws IOException, ExoPlaybackException { @@ -340,10 +451,24 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC return false; } - mSampleHolder.data.clear(); - mSampleHolder.size = 0; - int result = mSource.readData(mTrackIndex, mPresentationTimeUs, mFormatHolder, - mSampleHolder); + if (mUseFrameworkDecoder) { + boolean indexChanged = + ((MediaCodecAudioDecoder) mAudioDecoder).getInputIndex() + == MediaCodecAudioDecoder.INDEX_INVALID; + if (indexChanged) { + mSampleHolder.data = mAudioDecoder.getInputBuffer(); + if (mSampleHolder.data != null) { + mSampleHolder.clearData(); + } else { + return false; + } + } + } else { + mSampleHolder.data.clear(); + mSampleHolder.size = 0; + } + int result = + mSource.readData(mTrackIndex, mPresentationTimeUs, mFormatHolder, mSampleHolder); switch (result) { case SampleSource.NOTHING_READ: { return false; @@ -359,8 +484,48 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC return false; } default: { + if (mSampleHolder.size != mSampleSize + && mFormatConfigured + && !mUseFrameworkDecoder) { + onSampleSizeChanged(mSampleHolder.size); + } mSampleHolder.data.flip(); - decodeDone(mSampleHolder.data, mSampleHolder.timeUs); + if (!mUseFrameworkDecoder) { + if (MimeTypes.AUDIO_MPEG_L2.equalsIgnoreCase(mDecodingMime)) { + mMonitor.addPts( + mSampleHolder.timeUs, + mOutputBuffer.position(), + mSampleHolder.data.get(MP2_HEADER_BITRATE_OFFSET) + & MP2_HEADER_BITRATE_MASK); + } else { + mMonitor.addPts( + mSampleHolder.timeUs, + mOutputBuffer.position(), + mSampleHolder.data.get(AC3_HEADER_BITRATE_OFFSET) & 0xff); + } + } + if (mAudioDecoder != null) { + mAudioDecoder.decode(mSampleHolder); + if (mUseFrameworkDecoder) { + int outputIndex = + ((MediaCodecAudioDecoder) mAudioDecoder).getOutputIndex(); + if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + onOutputFormatChanged(mAudioDecoder.getOutputFormat()); + return true; + } else if (outputIndex < 0) { + return true; + } + if (((MediaCodecAudioDecoder) mAudioDecoder).maybeDecodeOnlyIndex()) { + AUDIO_TRACK.handleDiscontinuity(); + return true; + } + } + ByteBuffer outputBuffer = mAudioDecoder.getDecodedSample(); + long presentationTimeUs = mAudioDecoder.getDecodedTimeUs(); + decodeDone(outputBuffer, presentationTimeUs); + } else { + decodeDone(mSampleHolder.data, mSampleHolder.timeUs); + } return true; } } @@ -383,15 +548,22 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC int handleBufferResult; try { // To reduce discontinuity, interpolate presentation time. - mInterpolatedTimeUs = mPresentationTimeUs + if (MimeTypes.AUDIO_MPEG_L2.equalsIgnoreCase(mDecodingMime)) { + mInterpolatedTimeUs = mPresentationTimeUs + + mPresentationCount * MP2_SAMPLE_DURATION_US; + } else if (!mUseFrameworkDecoder) { + mInterpolatedTimeUs = mPresentationTimeUs + mPresentationCount * AC3_SAMPLE_DURATION_US; - handleBufferResult = AUDIO_TRACK.handleBuffer(mOutputBuffer, - 0, mOutputBuffer.limit(), mInterpolatedTimeUs); + } else { + mInterpolatedTimeUs = mPresentationTimeUs; + } + handleBufferResult = + AUDIO_TRACK.handleBuffer( + mOutputBuffer, 0, mOutputBuffer.limit(), mInterpolatedTimeUs); } catch (AudioTrack.WriteException e) { notifyAudioTrackWriteError(e); throw new ExoPlaybackException(e); } - if ((handleBufferResult & AudioTrack.RESULT_POSITION_DISCONTINUITY) != 0) { Log.i(TAG, "Play discontinuity happened"); mCurrentPositionUs = Long.MIN_VALUE; @@ -399,6 +571,9 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC if ((handleBufferResult & AudioTrack.RESULT_BUFFER_CONSUMED) != 0) { mCodecCounters.renderedOutputBufferCount++; mOutputReady = false; + if (mUseFrameworkDecoder) { + ((MediaCodecAudioDecoder) mAudioDecoder).releaseOutputBuffer(); + } return true; } return false; @@ -421,7 +596,7 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC if (!AUDIO_TRACK.isInitialized()) { return mAudioClock.getPositionUs(); } else if (!AUDIO_TRACK.isEnabled()) { - if (mInterpolatedTimeUs > 0) { + if (mInterpolatedTimeUs > 0 && !mUseFrameworkDecoder) { return mInterpolatedTimeUs - ESTIMATED_TRACK_RENDERING_DELAY_US; } return mPresentationTimeUs; @@ -471,8 +646,6 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC Assertions.checkState(mOutputBuffer.remaining() >= outputBuffer.limit()); mOutputBuffer.put(outputBuffer); - mMonitor.addPts(presentationTimeUs, mOutputBuffer.position(), - mOutputBuffer.get(AC3_HEADER_BITRATE_OFFSET)); if (presentationTimeUs == mPresentationTimeUs) { mPresentationCount++; } else { @@ -511,24 +684,29 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC public void handleMessage(int messageType, Object message) throws ExoPlaybackException { switch (messageType) { case MSG_SET_VOLUME: - AUDIO_TRACK.setVolume((Float) message); + float volume = (Float) message; + // Workaround: we cannot mute the audio track by setting the volume to 0, we need to + // disable the AUDIO_TRACK for this intent. However, enabling/disabling audio track + // whenever volume is being set might cause side effects, therefore we only handle + // "explicit mute operations", i.e., only after certain non-zero volume has been + // set, the subsequent volume setting operations will be consider as mute/un-mute + // operations and thus enable/disable the audio track. + if (mIsMuted && volume > 0) { + mIsMuted = false; + if (mEnabled) { + setStatus(true); + } + } else if (!mIsMuted && volume == 0) { + mIsMuted = true; + if (mEnabled) { + setStatus(false); + } + } + AUDIO_TRACK.setVolume(volume); break; case MSG_SET_AUDIO_TRACK: - boolean enabled = (Integer) message == 1; - if (enabled == AUDIO_TRACK.isEnabled()) { - return; - } - if (!enabled) { - // mAudioClock can be different from getPositionUs. In order to sync them, - // we set mAudioClock. - mAudioClock.setPositionUs(getPositionUs()); - } - AUDIO_TRACK.setStatus(enabled); - if (enabled) { - // When AUDIO_TRACK is enabled, we need to clear AUDIO_TRACK and seek to - // the current position. If not, AUDIO_TRACK has the obsolete data. - seekTo(mAudioClock.getPositionUs()); - } + mEnabled = (Integer) message == 1; + setStatus(mEnabled); break; case MSG_SET_PLAYBACK_SPEED: mAudioClock.setPlaybackSpeed((Float) message); @@ -537,4 +715,21 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC super.handleMessage(messageType, message); } } + + private void setStatus(boolean enabled) { + if (enabled == AUDIO_TRACK.isEnabled()) { + return; + } + if (!enabled) { + // mAudioClock can be different from getPositionUs. In order to sync them, + // we set mAudioClock. + mAudioClock.setPositionUs(getPositionUs()); + } + AUDIO_TRACK.setStatus(enabled); + if (enabled) { + // When AUDIO_TRACK is enabled, we need to clear AUDIO_TRACK and seek to + // the current position. If not, AUDIO_TRACK has the obsolete data. + seekTo(mAudioClock.getPositionUs()); + } + } } diff --git a/src/com/android/tv/tuner/exoplayer/ac3/Ac3TrackRenderer.java b/src/com/android/tv/tuner/exoplayer/audio/MpegTsMediaCodecAudioTrackRenderer.java index 2bf86b5a..142aa9b2 100644 --- a/src/com/android/tv/tuner/exoplayer/ac3/Ac3TrackRenderer.java +++ b/src/com/android/tv/tuner/exoplayer/audio/MpegTsMediaCodecAudioTrackRenderer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.tuner.exoplayer.ac3; +package com.android.tv.tuner.exoplayer.audio; import android.os.Handler; @@ -25,16 +25,13 @@ import com.google.android.exoplayer.SampleSource; /** * MPEG-2 TS audio track renderer. - * <p>Since the audio output from {@link android.media.MediaExtractor} contains extra samples at - * the beginning, using original {@link MediaCodecAudioTrackRenderer} as audio renderer causes - * asynchronous Audio/Video outputs. - * This class calculates the offset of audio data and adjust the presentation times to avoid the - * asynchronous Audio/Video problem. + * + * <p>Since the audio output from {@link android.media.MediaExtractor} contains extra samples at the + * beginning, using original {@link MediaCodecAudioTrackRenderer} as audio renderer causes + * asynchronous Audio/Video outputs. This class calculates the offset of audio data and adjust the + * presentation times to avoid the asynchronous Audio/Video problem. */ -public class Ac3TrackRenderer extends MediaCodecAudioTrackRenderer { - private final String TAG = "Ac3TrackRenderer"; - private final boolean DEBUG = false; - +public class MpegTsMediaCodecAudioTrackRenderer extends MediaCodecAudioTrackRenderer { private final Ac3EventListener mListener; public interface Ac3EventListener extends EventListener { @@ -47,8 +44,11 @@ public class Ac3TrackRenderer extends MediaCodecAudioTrackRenderer { void onAudioTrackSetPlaybackParamsError(IllegalArgumentException e); } - public Ac3TrackRenderer(SampleSource source, MediaCodecSelector mediaCodecSelector, - Handler eventHandler, EventListener eventListener) { + public MpegTsMediaCodecAudioTrackRenderer( + SampleSource source, + MediaCodecSelector mediaCodecSelector, + Handler eventHandler, + EventListener eventListener) { super(source, mediaCodecSelector, eventHandler, eventListener); mListener = (Ac3EventListener) eventListener; } diff --git a/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java b/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java index eb596e93..112e9dc4 100644 --- a/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java +++ b/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java @@ -25,13 +25,14 @@ import android.util.Log; import android.util.Pair; import com.google.android.exoplayer.SampleHolder; +import com.android.tv.common.SoftPreconditions; import com.android.tv.tuner.exoplayer.SampleExtractor; import com.android.tv.util.Utils; import java.io.File; -import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; +import java.util.ConcurrentModificationException; import java.util.LinkedList; import java.util.List; import java.util.Locale; @@ -59,7 +60,8 @@ public class BufferManager { private final SampleChunk.SampleChunkCreator mSampleChunkCreator; // Maps from track name to a map which maps from starting position to {@link SampleChunk}. - private final Map<String, SortedMap<Long, SampleChunk>> mChunkMap = new ArrayMap<>(); + private final Map<String, SortedMap<Long, Pair<SampleChunk, Integer>>> mChunkMap = + new ArrayMap<>(); private final Map<String, Long> mStartPositionMap = new ArrayMap<>(); private final Map<String, ChunkEvictedListener> mEvictListeners = new ArrayMap<>(); private final StorageManager mStorageManager; @@ -77,13 +79,11 @@ public class BufferManager { } }; - private volatile boolean mClosed = false; private int mMinSampleSizeForSpeedCheck = MINIMUM_SAMPLE_SIZE_FOR_SPEED_CHECK; private long mTotalWriteSize; private long mTotalWriteTimeNs; private float mWriteBandwidth = 0.0f; private volatile int mSpeedCheckCount; - private boolean mDisabled = false; public interface ChunkEvictedListener { void onChunkEvicted(String id, long createdTimeMs); @@ -174,6 +174,66 @@ public class BufferManager { } /** + * A Track format which will be loaded and saved from the permanent storage for recordings. + */ + public static class TrackFormat { + + /** + * The track id for the specified track. The track id will be used as a track identifier + * for recordings. + */ + public final String trackId; + + /** + * The {@link MediaFormat} for the specified track. + */ + public final MediaFormat format; + + /** + * Creates TrackFormat. + * @param trackId + * @param format + */ + public TrackFormat(String trackId, MediaFormat format) { + this.trackId = trackId; + this.format = format; + } + } + + /** + * A Holder for a sample position which will be loaded from the index file for recordings. + */ + public static class PositionHolder { + + /** + * The current sample position in microseconds. + * The position is identical to the PTS(presentation time stamp) of the sample. + */ + public final long positionUs; + + /** + * Base sample position for the current {@link SampleChunk}. + */ + public final long basePositionUs; + + /** + * The file offset for the current sample in the current {@link SampleChunk}. + */ + public final int offset; + + /** + * Creates a holder for a specific position in the recording. + * @param positionUs + * @param offset + */ + public PositionHolder(long positionUs, long basePositionUs, int offset) { + this.positionUs = positionUs; + this.basePositionUs = basePositionUs; + this.offset = offset; + } + } + + /** * Storage configuration and policy manager for {@link BufferManager} */ public interface StorageManager { @@ -186,11 +246,6 @@ public class BufferManager { File getBufferDir(); /** - * Cleans up storage. - */ - void clearStorage(); - - /** * Informs whether the storage is used for persistent use. (eg. dvr recording/play) * * @return {@code true} if stored files are persistent @@ -220,29 +275,27 @@ public class BufferManager { * Reads track name & {@link MediaFormat} from storage. * * @param isAudio {@code true} if it is for audio track - * @return {@link Pair} of track name & {@link MediaFormat} - * @throws IOException + * @return {@link List} of TrackFormat */ - Pair<String, MediaFormat> readTrackInfoFile(boolean isAudio) throws IOException; + List<TrackFormat> readTrackInfoFiles(boolean isAudio); /** - * Reads sample indexes for each written sample from storage. + * Reads key sample positions for each written sample from storage. * * @param trackId track name * @return indexes of the specified track * @throws IOException */ - ArrayList<Long> readIndexFile(String trackId) throws IOException; + ArrayList<PositionHolder> readIndexFile(String trackId) throws IOException; /** * Writes track information to storage. * - * @param trackId track name - * @param format {@link android.media.MediaFormat} of the track + * @param formatList {@list List} of TrackFormat * @param isAudio {@code true} if it is for audio track * @throws IOException */ - void writeTrackInfoFile(String trackId, MediaFormat format, boolean isAudio) + void writeTrackInfoFiles(List<TrackFormat> formatList, boolean isAudio) throws IOException; /** @@ -252,7 +305,7 @@ public class BufferManager { * @param index {@link SampleChunk} container * @throws IOException */ - void writeIndexFile(String trackName, SortedMap<Long, SampleChunk> index) + void writeIndexFile(String trackName, SortedMap<Long, Pair<SampleChunk, Integer>> index) throws IOException; } @@ -307,7 +360,6 @@ public class BufferManager { SampleChunk.SampleChunkCreator sampleChunkCreator) { mStorageManager = storageManager; mSampleChunkCreator = sampleChunkCreator; - clearBuffer(true); } public void registerChunkEvictedListener(String id, ChunkEvictedListener listener) { @@ -318,44 +370,44 @@ public class BufferManager { mEvictListeners.remove(id); } - private void clearBuffer(boolean deleteFiles) { - mChunkMap.clear(); - if (deleteFiles) { - mStorageManager.clearStorage(); - } - mBufferSize = 0; - } - private static String getFileName(String id, long positionUs) { return String.format(Locale.ENGLISH, "%s_%016x.chunk", id, positionUs); } /** - * Creates a new {@link SampleChunk} for caching samples. + * Creates a new {@link SampleChunk} for caching samples if it is needed. * * @param id the name of the track - * @param positionUs starting position of the {@link SampleChunk} in micro seconds. + * @param positionUs current position to write a sample in micro seconds. * @param samplePool {@link SamplePool} for the fast creation of samples. + * @param currentChunk the current {@link SampleChunk} to write, {@code null} when to create + * a new {@link SampleChunk}. + * @param currentOffset the current offset to write. * @return returns the created {@link SampleChunk}. * @throws IOException */ - public SampleChunk createNewWriteFile(String id, long positionUs, - SamplePool samplePool) throws IOException { + public SampleChunk createNewWriteFileIfNeeded(String id, long positionUs, SamplePool samplePool, + SampleChunk currentChunk, int currentOffset) throws IOException { if (!maybeEvictChunk()) { throw new IOException("Not enough storage space"); } - SortedMap<Long, SampleChunk> map = mChunkMap.get(id); + SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(id); if (map == null) { map = new TreeMap<>(); mChunkMap.put(id, map); mStartPositionMap.put(id, positionUs); mPendingDelete.init(id); } - File file = new File(mStorageManager.getBufferDir(), getFileName(id, positionUs)); - SampleChunk sampleChunk = mSampleChunkCreator.createSampleChunk(samplePool, file, - positionUs, mChunkCallback); - map.put(positionUs, sampleChunk); - return sampleChunk; + if (currentChunk == null) { + File file = new File(mStorageManager.getBufferDir(), getFileName(id, positionUs)); + SampleChunk sampleChunk = mSampleChunkCreator + .createSampleChunk(samplePool, file, positionUs, mChunkCallback); + map.put(positionUs, new Pair(sampleChunk, 0)); + return sampleChunk; + } else { + map.put(positionUs, new Pair(currentChunk, currentOffset)); + return null; + } } /** @@ -366,10 +418,10 @@ public class BufferManager { * @throws IOException */ public void loadTrackFromStorage(String trackId, SamplePool samplePool) throws IOException { - ArrayList<Long> keyPositions = mStorageManager.readIndexFile(trackId); - long startPositionUs = keyPositions.size() > 0 ? keyPositions.get(0) : 0; + ArrayList<PositionHolder> keyPositions = mStorageManager.readIndexFile(trackId); + long startPositionUs = keyPositions.size() > 0 ? keyPositions.get(0).positionUs : 0; - SortedMap<Long, SampleChunk> map = mChunkMap.get(trackId); + SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(trackId); if (map == null) { map = new TreeMap<>(); mChunkMap.put(trackId, map); @@ -377,11 +429,15 @@ public class BufferManager { mPendingDelete.init(trackId); } SampleChunk chunk = null; - for (long positionUs: keyPositions) { - chunk = mSampleChunkCreator.loadSampleChunkFromFile(samplePool, - mStorageManager.getBufferDir(), getFileName(trackId, positionUs), positionUs, - mChunkCallback, chunk); - map.put(positionUs, chunk); + long basePositionUs = -1; + for (PositionHolder position: keyPositions) { + if (position.basePositionUs != basePositionUs) { + chunk = mSampleChunkCreator.loadSampleChunkFromFile(samplePool, + mStorageManager.getBufferDir(), getFileName(trackId, position.positionUs), + position.positionUs, mChunkCallback, chunk); + basePositionUs = position.basePositionUs; + } + map.put(position.positionUs, new Pair(chunk, position.offset)); } } @@ -392,19 +448,19 @@ public class BufferManager { * @param positionUs the position. * @return returns the found {@link SampleChunk}. */ - public SampleChunk getReadFile(String id, long positionUs) { - SortedMap<Long, SampleChunk> map = mChunkMap.get(id); + public Pair<SampleChunk, Integer> getReadFile(String id, long positionUs) { + SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(id); if (map == null) { return null; } - SampleChunk sampleChunk; - SortedMap<Long, SampleChunk> headMap = map.headMap(positionUs + 1); + Pair<SampleChunk, Integer> ret; + SortedMap<Long, Pair<SampleChunk, Integer>> headMap = map.headMap(positionUs + 1); if (!headMap.isEmpty()) { - sampleChunk = headMap.get(headMap.lastKey()); + ret = headMap.get(headMap.lastKey()); } else { - sampleChunk = map.get(map.firstKey()); + ret = map.get(map.firstKey()); } - return sampleChunk; + return ret; } /** @@ -439,15 +495,16 @@ public class BufferManager { // Since chunks are persistent, we cannot evict chunks. return false; } - SortedMap<Long, SampleChunk> earliestChunkMap = null; + SortedMap<Long, Pair<SampleChunk, Integer>> earliestChunkMap = null; SampleChunk earliestChunk = null; String earliestChunkId = null; - for (Map.Entry<String, SortedMap<Long, SampleChunk>> entry : mChunkMap.entrySet()) { - SortedMap<Long, SampleChunk> map = entry.getValue(); + for (Map.Entry<String, SortedMap<Long, Pair<SampleChunk, Integer>>> entry : + mChunkMap.entrySet()) { + SortedMap<Long, Pair<SampleChunk, Integer>> map = entry.getValue(); if (map.isEmpty()) { continue; } - SampleChunk chunk = map.get(map.firstKey()); + SampleChunk chunk = map.get(map.firstKey()).first; if (earliestChunk == null || chunk.getCreatedTimeMs() < earliestChunk.getCreatedTimeMs()) { earliestChunkMap = map; @@ -473,8 +530,9 @@ public class BufferManager { } pendingDelete = mPendingDelete.getSize(); } - for (Map.Entry<String, SortedMap<Long, SampleChunk>> entry : mChunkMap.entrySet()) { - SortedMap<Long, SampleChunk> map = entry.getValue(); + for (Map.Entry<String, SortedMap<Long, Pair<SampleChunk, Integer>>> entry : + mChunkMap.entrySet()) { + SortedMap<Long, Pair<SampleChunk, Integer>> map = entry.getValue(); if (map.isEmpty()) { continue; } @@ -489,70 +547,74 @@ public class BufferManager { * @return returns all track information which is found by {@link BufferManager.StorageManager}. * @throws IOException */ - public ArrayList<Pair<String, MediaFormat>> readTrackInfoFiles() throws IOException { - ArrayList<Pair<String, MediaFormat>> trackInfos = new ArrayList<>(); - try { - trackInfos.add(mStorageManager.readTrackInfoFile(false)); - } catch (FileNotFoundException e) { - // There can be a single track only recording. (eg. audio-only, video-only) - // So the exception should not stop the read. + public List<TrackFormat> readTrackInfoFiles() throws IOException { + List<TrackFormat> trackFormatList = new ArrayList<>(); + trackFormatList.addAll(mStorageManager.readTrackInfoFiles(false)); + trackFormatList.addAll(mStorageManager.readTrackInfoFiles(true)); + if (trackFormatList.isEmpty()) { + throw new IOException("No track information to load"); } - try { - trackInfos.add(mStorageManager.readTrackInfoFile(true)); - } catch (FileNotFoundException e) { - // See above catch block. - } - return trackInfos; + return trackFormatList; } /** * Writes track information and index information for all tracks. * - * @param audio audio information. - * @param video video information. + * @param audios list of audio track information + * @param videos list of audio track information * @throws IOException */ - public void writeMetaFiles(Pair<String, MediaFormat> audio, Pair<String, MediaFormat> video) + public void writeMetaFiles(List<TrackFormat> audios, List<TrackFormat> videos) throws IOException { - if (audio != null) { - mStorageManager.writeTrackInfoFile(audio.first, audio.second, true); - SortedMap<Long, SampleChunk> map = mChunkMap.get(audio.first); - if (map == null) { - throw new IOException("Audio track index missing"); + if (audios.isEmpty() && videos.isEmpty()) { + throw new IOException("No track information to save"); + } + if (!audios.isEmpty()) { + mStorageManager.writeTrackInfoFiles(audios, true); + for (TrackFormat trackFormat : audios) { + SortedMap<Long, Pair<SampleChunk, Integer>> map = + mChunkMap.get(trackFormat.trackId); + if (map == null) { + throw new IOException("Audio track index missing"); + } + mStorageManager.writeIndexFile(trackFormat.trackId, map); } - mStorageManager.writeIndexFile(audio.first, map); } - if (video != null) { - mStorageManager.writeTrackInfoFile(video.first, video.second, false); - SortedMap<Long, SampleChunk> map = mChunkMap.get(video.first); - if (map == null) { - throw new IOException("Video track index missing"); + if (!videos.isEmpty()) { + mStorageManager.writeTrackInfoFiles(videos, false); + for (TrackFormat trackFormat : videos) { + SortedMap<Long, Pair<SampleChunk, Integer>> map = + mChunkMap.get(trackFormat.trackId); + if (map == null) { + throw new IOException("Video track index missing"); + } + mStorageManager.writeIndexFile(trackFormat.trackId, map); } - mStorageManager.writeIndexFile(video.first, map); } } /** - * Marks it is closed and it is not used anymore. - */ - public void close() { - // Clean-up may happen after this is called. - mClosed = true; - } - - /** * Releases all the resources. */ public void release() { - mPendingDelete.release(); - for (Map.Entry<String, SortedMap<Long, SampleChunk>> entry : mChunkMap.entrySet()) { - for (SampleChunk chunk : entry.getValue().values()) { - SampleChunk.IoState.release(chunk, !mStorageManager.isPersistent()); + try { + mPendingDelete.release(); + for (Map.Entry<String, SortedMap<Long, Pair<SampleChunk, Integer>>> entry : + mChunkMap.entrySet()) { + SampleChunk toRelease = null; + for (Pair<SampleChunk, Integer> positions : entry.getValue().values()) { + if (toRelease != positions.first) { + toRelease = positions.first; + SampleChunk.IoState.release(toRelease, !mStorageManager.isPersistent()); + } + } } - } - mChunkMap.clear(); - if (mClosed) { - clearBuffer(!mStorageManager.isPersistent()); + mChunkMap.clear(); + } catch (ConcurrentModificationException | NullPointerException e) { + // TODO: remove this after it it confirmed that race condition issues are resolved. + // b/32492258, b/32373376 + SoftPreconditions.checkState(false, "Exception on BufferManager#release: ", + e.toString()); } } @@ -611,20 +673,6 @@ public class BufferManager { } /** - * Marks {@link BufferManager} object disabled to prevent it from the future use. - */ - public void disable() { - mDisabled = true; - } - - /** - * Returns if {@link BufferManager} object is disabled. - */ - public boolean isDisabled() { - return mDisabled; - } - - /** * Returns if {@link BufferManager} has checked the write speed, * which is suitable for Trickplay. */ diff --git a/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java b/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java index 6a0502a7..6a09016c 100644 --- a/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java +++ b/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java @@ -17,8 +17,12 @@ package com.android.tv.tuner.exoplayer.buffer; import android.media.MediaFormat; +import android.util.Log; import android.util.Pair; +import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack; +import com.google.protobuf.nano.MessageNano; + import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; @@ -28,18 +32,25 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.List; +import java.util.Map; import java.util.SortedMap; /** * Manages DVR storage. */ public class DvrStorageManager implements BufferManager.StorageManager { + private static final String TAG = "DvrStorageManager"; // TODO: make serializable classes and use protobuf after internal data structure is finalized. private static final String KEY_PIXEL_WIDTH_HEIGHT_RATIO = "com.google.android.videos.pixelWidthHeightRatio"; + private static final String META_FILE_TYPE_AUDIO = "audio"; + private static final String META_FILE_TYPE_VIDEO = "video"; + private static final String META_FILE_TYPE_CAPTION = "caption"; private static final String META_FILE_SUFFIX = ".meta"; private static final String IDX_FILE_SUFFIX = ".idx"; + private static final String IDX_FILE_SUFFIX_V2 = IDX_FILE_SUFFIX + "2"; // Size of minimum reserved storage buffer which will be used to save meta files // and index files after actual recording finished. @@ -59,18 +70,6 @@ public class DvrStorageManager implements BufferManager.StorageManager { } @Override - public void clearStorage() { - if (mIsRecording) { - File[] files = mBufferDir.listFiles(); - if (files != null && files.length > 0) { - for (File file : files) { - file.delete(); - } - } - } - } - - @Override public File getBufferDir() { return mBufferDir; } @@ -132,6 +131,17 @@ public class DvrStorageManager implements BufferManager.StorageManager { } } + private void readFormatStringOptional(DataInputStream in, MediaFormat format, String key) { + try { + String str = readString(in); + if (str != null) { + format.setString(key, str); + } + } catch (IOException e) { + // Since we are reading optional field, ignore the exception. + } + } + private ByteBuffer readByteBuffer(DataInputStream in) throws IOException { int len = in.readInt(); if (len <= 0) { @@ -155,39 +165,104 @@ public class DvrStorageManager implements BufferManager.StorageManager { } @Override - public Pair<String, MediaFormat> readTrackInfoFile(boolean isAudio) throws IOException { - File file = new File(getBufferDir(), (isAudio ? "audio" : "video") + META_FILE_SUFFIX); - try (DataInputStream in = new DataInputStream(new FileInputStream(file))) { - String name = readString(in); - MediaFormat format = new MediaFormat(); - readFormatString(in, format, MediaFormat.KEY_MIME); - readFormatInt(in, format, MediaFormat.KEY_MAX_INPUT_SIZE); - readFormatInt(in, format, MediaFormat.KEY_WIDTH); - readFormatInt(in, format, MediaFormat.KEY_HEIGHT); - readFormatInt(in, format, MediaFormat.KEY_CHANNEL_COUNT); - readFormatInt(in, format, MediaFormat.KEY_SAMPLE_RATE); - readFormatFloat(in, format, KEY_PIXEL_WIDTH_HEIGHT_RATIO); - for (int i = 0; i < 3; ++i) { - readFormatByteBuffer(in, format, "csd-" + i); + public List<BufferManager.TrackFormat> readTrackInfoFiles(boolean isAudio) { + List<BufferManager.TrackFormat> trackFormatList = new ArrayList<>(); + int index = 0; + boolean trackNotFound = false; + do { + String fileName = (isAudio ? META_FILE_TYPE_AUDIO : META_FILE_TYPE_VIDEO) + + ((index == 0) ? META_FILE_SUFFIX : (index + META_FILE_SUFFIX)); + File file = new File(getBufferDir(), fileName); + try (DataInputStream in = new DataInputStream(new FileInputStream(file))) { + String name = readString(in); + MediaFormat format = new MediaFormat(); + readFormatString(in, format, MediaFormat.KEY_MIME); + readFormatInt(in, format, MediaFormat.KEY_MAX_INPUT_SIZE); + readFormatInt(in, format, MediaFormat.KEY_WIDTH); + readFormatInt(in, format, MediaFormat.KEY_HEIGHT); + readFormatInt(in, format, MediaFormat.KEY_CHANNEL_COUNT); + readFormatInt(in, format, MediaFormat.KEY_SAMPLE_RATE); + readFormatFloat(in, format, KEY_PIXEL_WIDTH_HEIGHT_RATIO); + for (int i = 0; i < 3; ++i) { + readFormatByteBuffer(in, format, "csd-" + i); + } + readFormatLong(in, format, MediaFormat.KEY_DURATION); + + // This is optional since language field is added later. + readFormatStringOptional(in, format, MediaFormat.KEY_LANGUAGE); + trackFormatList.add(new BufferManager.TrackFormat(name, format)); + } catch (IOException e) { + trackNotFound = true; } - readFormatLong(in, format, MediaFormat.KEY_DURATION); - return new Pair<>(name, format); + index++; + } while(!trackNotFound); + return trackFormatList; + } + + /** + * Reads caption information from files. + * + * @return a list of {@link AtscCaptionTrack} objects which store caption information. + */ + public List<AtscCaptionTrack> readCaptionInfoFiles() { + List<AtscCaptionTrack> tracks = new ArrayList<>(); + int index = 0; + boolean trackNotFound = false; + do { + String fileName = META_FILE_TYPE_CAPTION + + ((index == 0) ? META_FILE_SUFFIX : (index + META_FILE_SUFFIX)); + File file = new File(getBufferDir(), fileName); + try (DataInputStream in = new DataInputStream(new FileInputStream(file))) { + byte[] data = new byte[(int) file.length()]; + in.read(data); + tracks.add(AtscCaptionTrack.parseFrom(data)); + } catch (IOException e) { + trackNotFound = true; + } + index++; + } while(!trackNotFound); + return tracks; + } + + private ArrayList<BufferManager.PositionHolder> readOldIndexFile(File indexFile) + throws IOException { + ArrayList<BufferManager.PositionHolder> indices = new ArrayList<>(); + try (DataInputStream in = new DataInputStream(new FileInputStream(indexFile))) { + long count = in.readLong(); + for (long i = 0; i < count; ++i) { + long positionUs = in.readLong(); + indices.add(new BufferManager.PositionHolder(positionUs, positionUs, 0)); + } + return indices; } } - @Override - public ArrayList<Long> readIndexFile(String trackId) throws IOException { - ArrayList<Long> indices = new ArrayList<>(); - File file = new File(getBufferDir(), trackId + IDX_FILE_SUFFIX); - try (DataInputStream in = new DataInputStream(new FileInputStream(file))) { + private ArrayList<BufferManager.PositionHolder> readNewIndexFile(File indexFile) + throws IOException { + ArrayList<BufferManager.PositionHolder> indices = new ArrayList<>(); + try (DataInputStream in = new DataInputStream(new FileInputStream(indexFile))) { long count = in.readLong(); for (long i = 0; i < count; ++i) { - indices.add(in.readLong()); + long positionUs = in.readLong(); + long basePositionUs = in.readLong(); + int offset = in.readInt(); + indices.add(new BufferManager.PositionHolder(positionUs, basePositionUs, offset)); } return indices; } } + @Override + public ArrayList<BufferManager.PositionHolder> readIndexFile(String trackId) + throws IOException { + File file = new File(getBufferDir(), trackId + IDX_FILE_SUFFIX_V2); + if (file.exists()) { + return readNewIndexFile(file); + } else { + return readOldIndexFile(new File(getBufferDir(),trackId + IDX_FILE_SUFFIX)); + } + } + private void writeFormatInt(DataOutputStream out, MediaFormat format, String key) throws IOException { if (format.containsKey(key)) { @@ -254,33 +329,63 @@ public class DvrStorageManager implements BufferManager.StorageManager { } @Override - public void writeTrackInfoFile(String trackId, MediaFormat format, boolean isAudio) + public void writeTrackInfoFiles(List<BufferManager.TrackFormat> formatList, boolean isAudio) throws IOException { - File file = new File(getBufferDir(), (isAudio ? "audio" : "video") + META_FILE_SUFFIX); - try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) { - writeString(out, trackId); - writeFormatString(out, format, MediaFormat.KEY_MIME); - writeFormatInt(out, format, MediaFormat.KEY_MAX_INPUT_SIZE); - writeFormatInt(out, format, MediaFormat.KEY_WIDTH); - writeFormatInt(out, format, MediaFormat.KEY_HEIGHT); - writeFormatInt(out, format, MediaFormat.KEY_CHANNEL_COUNT); - writeFormatInt(out, format, MediaFormat.KEY_SAMPLE_RATE); - writeFormatFloat(out, format, KEY_PIXEL_WIDTH_HEIGHT_RATIO); - for (int i = 0; i < 3; ++i) { - writeFormatByteBuffer(out, format, "csd-" + i); + for (int i = 0; i < formatList.size() ; ++i) { + BufferManager.TrackFormat trackFormat = formatList.get(i); + String fileName = (isAudio ? META_FILE_TYPE_AUDIO : META_FILE_TYPE_VIDEO) + + ((i == 0) ? META_FILE_SUFFIX : (i + META_FILE_SUFFIX)); + File file = new File(getBufferDir(), fileName); + try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) { + writeString(out, trackFormat.trackId); + writeFormatString(out, trackFormat.format, MediaFormat.KEY_MIME); + writeFormatInt(out, trackFormat.format, MediaFormat.KEY_MAX_INPUT_SIZE); + writeFormatInt(out, trackFormat.format, MediaFormat.KEY_WIDTH); + writeFormatInt(out, trackFormat.format, MediaFormat.KEY_HEIGHT); + writeFormatInt(out, trackFormat.format, MediaFormat.KEY_CHANNEL_COUNT); + writeFormatInt(out, trackFormat.format, MediaFormat.KEY_SAMPLE_RATE); + writeFormatFloat(out, trackFormat.format, KEY_PIXEL_WIDTH_HEIGHT_RATIO); + for (int j = 0; j < 3; ++j) { + writeFormatByteBuffer(out, trackFormat.format, "csd-" + j); + } + writeFormatLong(out, trackFormat.format, MediaFormat.KEY_DURATION); + writeFormatString(out, trackFormat.format, MediaFormat.KEY_LANGUAGE); + } + } + } + + /** + * Writes caption information to files. + * + * @param tracks a list of {@link AtscCaptionTrack} objects which store caption information. + */ + public void writeCaptionInfoFiles(List<AtscCaptionTrack> tracks) { + if (tracks == null || tracks.isEmpty()) { + return; + } + for (int i = 0; i < tracks.size(); i++) { + AtscCaptionTrack track = tracks.get(i); + String fileName = META_FILE_TYPE_CAPTION + + ((i == 0) ? META_FILE_SUFFIX : (i + META_FILE_SUFFIX)); + File file = new File(getBufferDir(), fileName); + try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) { + out.write(MessageNano.toByteArray(track)); + } catch (Exception e) { + Log.e(TAG, "Fail to write caption info to files", e); } - writeFormatLong(out, format, MediaFormat.KEY_DURATION); } } @Override - public void writeIndexFile(String trackName, SortedMap<Long, SampleChunk> index) + public void writeIndexFile(String trackName, SortedMap<Long, Pair<SampleChunk, Integer>> index) throws IOException { - File indexFile = new File(getBufferDir(), trackName + IDX_FILE_SUFFIX); + File indexFile = new File(getBufferDir(), trackName + IDX_FILE_SUFFIX_V2); try (DataOutputStream out = new DataOutputStream(new FileOutputStream(indexFile))) { out.writeLong(index.size()); - for (Long key : index.keySet()) { - out.writeLong(key); + for (Map.Entry<Long, Pair<SampleChunk, Integer>> entry : index.entrySet()) { + out.writeLong(entry.getKey()); + out.writeLong(entry.getValue().first.getStartPositionUs()); + out.writeInt(entry.getValue().second); } } } diff --git a/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java b/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java index 4869b49f..af0c3f0d 100644 --- a/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java +++ b/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java @@ -66,9 +66,14 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer, public static final int BUFFER_REASON_RECORDING = 2; /** - * The duration of a chunk of samples, {@link SampleChunk}. + * The minimum duration to support seek in Trickplay. */ - static final long CHUNK_DURATION_US = TimeUnit.MILLISECONDS.toMicros(500); + static final long MIN_SEEK_DURATION_US = TimeUnit.MILLISECONDS.toMicros(500); + + /** + * The duration of a {@link SampleChunk} for recordings. + */ + static final long RECORDING_CHUNK_DURATION_US = MIN_SEEK_DURATION_US * 1200; // 10 minutes private static final long BUFFER_WRITE_TIMEOUT_MS = 10 * 1000; // 10 seconds private static final long BUFFER_NEEDED_US = 1000L * Math.max(MpegTsPlayer.MIN_BUFFER_MS, MpegTsPlayer.MIN_REBUFFER_MS); @@ -79,7 +84,6 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer, private int mTrackCount; private boolean[] mTrackSelected; - private List<String> mIds; private List<SampleQueue> mReadSampleQueues; private final SamplePool mSamplePool = new SamplePool(); private long mLastBufferedPositionUs = C.UNKNOWN_TIME_US; @@ -130,7 +134,6 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer, if (mTrackCount <= 0) { throw new IOException("No tracks to initialize"); } - mIds = ids; mTrackSelected = new boolean[mTrackCount]; mReadSampleQueues = new ArrayList<>(); mSampleChunkIoHelper = new SampleChunkIoHelper(ids, mediaFormats, mBufferReason, @@ -139,6 +142,9 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer, mReadSampleQueues.add(i, new SampleQueue(mSamplePool)); } mSampleChunkIoHelper.init(); + for (int i = 0; i < mTrackCount; ++i) { + mBufferManager.registerChunkEvictedListener(ids.get(i), RecordingSampleBuffer.this); + } } @Override @@ -146,8 +152,6 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer, if (!mTrackSelected[index]) { mTrackSelected[index] = true; mReadSampleQueues.get(index).clear(); - mBufferManager.registerChunkEvictedListener(mIds.get(index), - RecordingSampleBuffer.this); mSampleChunkIoHelper.openRead(index, mCurrentPlaybackPositionUs); } } @@ -157,7 +161,7 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer, if (mTrackSelected[index]) { mTrackSelected[index] = false; mReadSampleQueues.get(index).clear(); - mBufferManager.unregisterChunkEvictedListener(mIds.get(index)); + mSampleChunkIoHelper.closeRead(index); } } @@ -193,7 +197,6 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer, } // Disables buffering samples afterwards, and notifies the disk speed is slow. Log.w(TAG, "Disk is too slow for trickplay"); - mBufferManager.disable(); mBufferListener.onDiskTooSlow(); } @@ -205,7 +208,7 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer, private boolean maybeReadSample(SampleQueue queue, int index) { if (queue.getLastQueuedPositionUs() != null && queue.getLastQueuedPositionUs() > mCurrentPlaybackPositionUs + BUFFER_NEEDED_US - && queue.isDurationGreaterThan(CHUNK_DURATION_US)) { + && queue.isDurationGreaterThan(MIN_SEEK_DURATION_US)) { // The speed of queuing samples can be higher than the playback speed. // If the duration of the samples in the queue is not limited, // samples can be accumulated and there can be out-of-memory issues. @@ -300,7 +303,7 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer, public void onChunkEvicted(String id, long createdTimeMs) { if (mBufferListener != null) { mBufferListener.onBufferStartTimeChanged( - createdTimeMs + TimeUnit.MICROSECONDS.toMillis(CHUNK_DURATION_US)); + createdTimeMs + TimeUnit.MICROSECONDS.toMillis(MIN_SEEK_DURATION_US)); } } } diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java index 552caaef..04b5a071 100644 --- a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java +++ b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java @@ -78,7 +78,6 @@ public class SampleChunk { /** * A class for SampleChunk creation. */ - @VisibleForTesting public static class SampleChunkCreator { /** @@ -151,18 +150,23 @@ public class SampleChunk { mCurrentOffset = 0; } + private void reset(SampleChunk chunk, long offset) { + mChunk = chunk; + mCurrentOffset = offset; + } + /** * Prepares for read I/O operation from a new SampleChunk. * * @param chunk the new SampleChunk to read from * @throws IOException */ - void openRead(SampleChunk chunk) throws IOException { + void openRead(SampleChunk chunk, long offset) throws IOException { if (mChunk != null) { mChunk.closeRead(); } chunk.openRead(); - reset(chunk); + reset(chunk, offset); } /** @@ -241,6 +245,20 @@ public class SampleChunk { } /** + * Returns the current SampleChunk for subsequent I/O operation. + */ + SampleChunk getChunk() { + return mChunk; + } + + /** + * Returns the current offset of the current SampleChunk for subsequent I/O operation. + */ + long getOffset() { + return mCurrentOffset; + } + + /** * Releases SampleChunk. the SampleChunk will not be used anymore. * * @param chunk to release diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java index 37ae4022..ca97a91a 100644 --- a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java +++ b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java @@ -21,6 +21,7 @@ import android.os.ConditionVariable; import android.os.Handler; import android.os.HandlerThread; import android.os.Message; +import android.util.ArraySet; import android.util.Log; import android.util.Pair; @@ -31,7 +32,9 @@ import com.android.tv.common.SoftPreconditions; import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer.BufferReason; import java.io.IOException; +import java.util.LinkedList; import java.util.List; +import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; /** @@ -46,11 +49,13 @@ public class SampleChunkIoHelper implements Handler.Callback { private static final int MSG_OPEN_READ = 1; private static final int MSG_OPEN_WRITE = 2; - private static final int MSG_CLOSE_WRITE = 3; - private static final int MSG_READ = 4; - private static final int MSG_WRITE = 5; - private static final int MSG_RELEASE = 6; + private static final int MSG_CLOSE_READ = 3; + private static final int MSG_CLOSE_WRITE = 4; + private static final int MSG_READ = 5; + private static final int MSG_WRITE = 6; + private static final int MSG_RELEASE = 7; + private final long mSampleChunkDurationUs; private final int mTrackCount; private final List<String> mIds; private final List<MediaFormat> mMediaFormats; @@ -62,9 +67,11 @@ public class SampleChunkIoHelper implements Handler.Callback { private Handler mIoHandler; private final ConcurrentLinkedQueue<SampleHolder> mReadSampleBuffers[]; private final ConcurrentLinkedQueue<SampleHolder> mHandlerReadSampleBuffers[]; - private final long[] mWriteEndPositionUs; + private final long[] mWriteIndexEndPositionUs; + private final long[] mWriteChunkEndPositionUs; private final SampleChunk.IoState[] mReadIoStates; private final SampleChunk.IoState[] mWriteIoStates; + private final Set<Integer> mSelectedTracks = new ArraySet<>(); private long mBufferDurationUs = 0; private boolean mWriteEnded; private boolean mErrorNotified; @@ -129,11 +136,20 @@ public class SampleChunkIoHelper implements Handler.Callback { mReadSampleBuffers = new ConcurrentLinkedQueue[mTrackCount]; mHandlerReadSampleBuffers = new ConcurrentLinkedQueue[mTrackCount]; - mWriteEndPositionUs = new long[mTrackCount]; + mWriteIndexEndPositionUs = new long[mTrackCount]; + mWriteChunkEndPositionUs = new long[mTrackCount]; mReadIoStates = new SampleChunk.IoState[mTrackCount]; mWriteIoStates = new SampleChunk.IoState[mTrackCount]; + + // Small chunk duration for live playback will give more fine grained storage usage + // and eviction handling for trickplay. + mSampleChunkDurationUs = + bufferReason == RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK ? + RecordingSampleBuffer.MIN_SEEK_DURATION_US : + RecordingSampleBuffer.RECORDING_CHUNK_DURATION_US; for (int i = 0; i < mTrackCount; ++i) { - mWriteEndPositionUs[i] = RecordingSampleBuffer.CHUNK_DURATION_US; + mWriteIndexEndPositionUs[i] = RecordingSampleBuffer.MIN_SEEK_DURATION_US; + mWriteChunkEndPositionUs[i] = mSampleChunkDurationUs; mReadIoStates[i] = new SampleChunk.IoState(); mWriteIoStates[i] = new SampleChunk.IoState(); } @@ -204,6 +220,15 @@ public class SampleChunkIoHelper implements Handler.Callback { } /** + * Closes read from the specified track. + * + * @param index track index + */ + public void closeRead(int index) { + mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_CLOSE_READ, index)); + } + + /** * Notifies writes are finished. */ public void closeWrite() { @@ -229,21 +254,19 @@ public class SampleChunkIoHelper implements Handler.Callback { try { if (mBufferReason == RecordingSampleBuffer.BUFFER_REASON_RECORDING && mTrackCount > 0) { // Saves meta information for recording. - Pair<String, android.media.MediaFormat> audio = null, video = null; + List<BufferManager.TrackFormat> audios = new LinkedList<>(); + List<BufferManager.TrackFormat> videos = new LinkedList<>(); for (int i = 0; i < mTrackCount; ++i) { android.media.MediaFormat format = mMediaFormats.get(i).getFrameworkMediaFormatV16(); format.setLong(android.media.MediaFormat.KEY_DURATION, mBufferDurationUs); - if (audio == null && MimeTypes.isAudio(mMediaFormats.get(i).mimeType)) { - audio = new Pair<>(mIds.get(i), format); - } else if (video == null && MimeTypes.isVideo(mMediaFormats.get(i).mimeType)) { - video = new Pair<>(mIds.get(i), format); - } - if (audio != null && video != null) { - break; + if (MimeTypes.isAudio(mMediaFormats.get(i).mimeType)) { + audios.add(new BufferManager.TrackFormat(mIds.get(i), format)); + } else if (MimeTypes.isVideo(mMediaFormats.get(i).mimeType)) { + videos.add(new BufferManager.TrackFormat(mIds.get(i), format)); } } - mBufferManager.writeMetaFiles(audio, video); + mBufferManager.writeMetaFiles(audios, videos); } } finally { mBufferManager.release(); @@ -265,6 +288,9 @@ public class SampleChunkIoHelper implements Handler.Callback { case MSG_OPEN_WRITE: doOpenWrite((int) message.obj); return true; + case MSG_CLOSE_READ: + doCloseRead((int) message.obj); + return true; case MSG_CLOSE_WRITE: doCloseWrite(); return true; @@ -291,14 +317,16 @@ public class SampleChunkIoHelper implements Handler.Callback { private void doOpenRead(IoParams params) throws IOException { int index = params.index; mIoHandler.removeMessages(MSG_READ, index); - SampleChunk chunk = mBufferManager.getReadFile(mIds.get(index), params.positionUs); - if (chunk == null) { + Pair<SampleChunk, Integer> readPosition = + mBufferManager.getReadFile(mIds.get(index), params.positionUs); + if (readPosition == null) { String errorMessage = "Chunk ID:" + mIds.get(index) + " pos:" + params.positionUs + "is not found"; - SoftPreconditions.checkNotNull(chunk, TAG, errorMessage); + SoftPreconditions.checkNotNull(readPosition, TAG, errorMessage); throw new IOException(errorMessage); } - mReadIoStates[index].openRead(chunk); + mSelectedTracks.add(index); + mReadIoStates[index].openRead(readPosition.first, (long) readPosition.second); if (mHandlerReadSampleBuffers[index] != null) { SampleHolder sample; while ((sample = mHandlerReadSampleBuffers[index].poll()) != null) { @@ -310,10 +338,22 @@ public class SampleChunkIoHelper implements Handler.Callback { } private void doOpenWrite(int index) throws IOException { - SampleChunk chunk = mBufferManager.createNewWriteFile(mIds.get(index), 0, mSamplePool); + SampleChunk chunk = mBufferManager.createNewWriteFileIfNeeded(mIds.get(index), 0, + mSamplePool, null, 0); mWriteIoStates[index].openWrite(chunk); } + private void doCloseRead(int index) { + mSelectedTracks.remove(index); + if (mHandlerReadSampleBuffers[index] != null) { + SampleHolder sample; + while ((sample = mHandlerReadSampleBuffers[index].poll()) != null) { + mSamplePool.releaseSample(sample); + } + } + mIoHandler.removeMessages(MSG_READ, index); + } + private void doRead(int index) throws IOException { mIoHandler.removeMessages(MSG_READ, index); if (mHandlerReadSampleBuffers[index].size() >= MAX_READ_BUFFER_SAMPLES) { @@ -357,13 +397,21 @@ public class SampleChunkIoHelper implements Handler.Callback { if (sample.timeUs > mBufferDurationUs) { mBufferDurationUs = sample.timeUs; } - - if (sample.timeUs >= mWriteEndPositionUs[index]) { - nextChunk = mBufferManager.createNewWriteFile(mIds.get(index), - mWriteEndPositionUs[index], mSamplePool); - mWriteEndPositionUs[index] = - ((sample.timeUs / RecordingSampleBuffer.CHUNK_DURATION_US) + 1) * - RecordingSampleBuffer.CHUNK_DURATION_US; + if (sample.timeUs >= mWriteIndexEndPositionUs[index]) { + SampleChunk currentChunk = sample.timeUs >= mWriteChunkEndPositionUs[index] ? + null : mWriteIoStates[params.index].getChunk(); + int currentOffset = (int) mWriteIoStates[params.index].getOffset(); + nextChunk = mBufferManager.createNewWriteFileIfNeeded( + mIds.get(index), mWriteIndexEndPositionUs[index], mSamplePool, + currentChunk, currentOffset); + mWriteIndexEndPositionUs[index] = + ((sample.timeUs / RecordingSampleBuffer.MIN_SEEK_DURATION_US) + 1) * + RecordingSampleBuffer.MIN_SEEK_DURATION_US; + if (nextChunk != null) { + mWriteChunkEndPositionUs[index] = + ((sample.timeUs / mSampleChunkDurationUs) + 1) + * mSampleChunkDurationUs; + } } } mWriteIoStates[params.index].write(params.sample, nextChunk); @@ -391,15 +439,22 @@ public class SampleChunkIoHelper implements Handler.Callback { mIoHandler.removeCallbacksAndMessages(null); mFinished = true; conditionVariable.open(); + mSelectedTracks.clear(); } private void releaseEvictedChunks() { - if (mBufferReason != RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK) { + if (mBufferReason != RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK + || mSelectedTracks.isEmpty()) { return; } + long currentStartPositionUs = Long.MAX_VALUE; + for (int trackIndex : mSelectedTracks) { + currentStartPositionUs = Math.min(currentStartPositionUs, + mReadIoStates[trackIndex].getStartPositionUs()); + } for (int i = 0; i < mTrackCount; ++i) { long evictEndPositionUs = Math.min(mBufferManager.getStartPositionUs(mIds.get(i)), - mReadIoStates[i].getStartPositionUs()); + currentStartPositionUs); mBufferManager.evictChunks(mIds.get(i), evictEndPositionUs); } } diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java b/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java index 7b098f40..75eac5a2 100644 --- a/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java +++ b/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java @@ -43,6 +43,7 @@ public class SampleQueue { if (sampleFromQueue == null) { return SampleSource.NOTHING_READ; } + sample.ensureSpaceForWrite(sampleFromQueue.size); sample.size = sampleFromQueue.size; sample.flags = sampleFromQueue.flags; sample.timeUs = sampleFromQueue.timeUs; diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java b/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java index 40c4ef95..159fde18 100644 --- a/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java +++ b/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java @@ -19,18 +19,18 @@ package com.android.tv.tuner.exoplayer.buffer; import android.os.ConditionVariable; import android.support.annotation.NonNull; + import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleSource; +import com.android.tv.common.SoftPreconditions; import com.android.tv.tuner.tvinput.PlaybackBufferListener; import com.android.tv.tuner.exoplayer.SampleExtractor; import java.io.IOException; import java.util.List; -import junit.framework.Assert; - /** * Handles I/O for {@link SampleExtractor} when * physical storage based buffer is not used. Trickplay is disabled. @@ -115,8 +115,8 @@ public class SimpleSampleBuffer implements BufferManager.SampleBuffer { @Override public synchronized int readSample(int track, SampleHolder sampleHolder) { SampleQueue queue = mPlayingSampleQueues[track]; - Assert.assertNotNull(queue); - int result = queue.dequeueSample(sampleHolder); + SoftPreconditions.checkNotNull(queue); + int result = queue == null ? SampleSource.NOTHING_READ : queue.dequeueSample(sampleHolder); if (result != SampleSource.SAMPLE_READ && reachedEos()) { return SampleSource.END_OF_STREAM; } diff --git a/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java b/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java index 258a5cd0..9fe921b8 100644 --- a/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java +++ b/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java @@ -17,20 +17,23 @@ package com.android.tv.tuner.exoplayer.buffer; import android.content.Context; -import android.media.MediaFormat; import android.os.AsyncTask; -import android.os.Looper; import android.provider.Settings; +import android.support.annotation.NonNull; import android.util.Pair; +import com.android.tv.common.SoftPreconditions; + import java.io.File; import java.util.ArrayList; +import java.util.List; import java.util.SortedMap; /** * Manages Trickplay storage. */ public class TrickplayStorageManager implements BufferManager.StorageManager { + // TODO: Support multi-sessions. private static final String BUFFER_DIR = "timeshift"; // Copied from android.provider.Settings.Global (hidden fields) @@ -43,53 +46,68 @@ public class TrickplayStorageManager implements BufferManager.StorageManager { private static final int DEFAULT_THRESHOLD_PERCENTAGE = 10; private static final long DEFAULT_THRESHOLD_MAX_BYTES = 500L * 1024 * 1024; - private final File mBufferDir; + private static AsyncTask<Void, Void, Void> sLastCacheCleanUpTask; + private static File sBufferDir; + private static long sStorageBufferBytes; + private final long mMaxBufferSize; - private final long mStorageBufferBytes; - private static long getStorageBufferBytes(Context context, File path) { + private static void initParamsIfNeeded(Context context, @NonNull File path) { + // TODO: Support multi-sessions. + SoftPreconditions.checkState( + sBufferDir == null || sBufferDir.equals(path)); + if (path.equals(sBufferDir)) { + return; + } + sBufferDir = path; long lowPercentage = Settings.Global.getInt(context.getContentResolver(), SYS_STORAGE_THRESHOLD_PERCENTAGE, DEFAULT_THRESHOLD_PERCENTAGE); - long lowBytes = path.getTotalSpace() * lowPercentage / 100; + long lowPercentageToBytes = path.getTotalSpace() * lowPercentage / 100; long maxLowBytes = Settings.Global.getLong(context.getContentResolver(), SYS_STORAGE_THRESHOLD_MAX_BYTES, DEFAULT_THRESHOLD_MAX_BYTES); - return Math.min(lowBytes, maxLowBytes); + sStorageBufferBytes = Math.min(lowPercentageToBytes, maxLowBytes); } - public TrickplayStorageManager(Context context, File baseDir, long maxBufferSize) { - mBufferDir = new File(baseDir, BUFFER_DIR); - mBufferDir.mkdirs(); + public TrickplayStorageManager(Context context, @NonNull File baseDir, long maxBufferSize) { + initParamsIfNeeded(context, new File(baseDir, BUFFER_DIR)); + sBufferDir.mkdirs(); mMaxBufferSize = maxBufferSize; clearStorage(); - mStorageBufferBytes = getStorageBufferBytes(context, mBufferDir); } - @Override - public void clearStorage() { - File files[] = mBufferDir.listFiles(); - if (files == null || files.length == 0) { - return; + private void clearStorage() { + long now = System.currentTimeMillis(); + if (sLastCacheCleanUpTask != null) { + sLastCacheCleanUpTask.cancel(true); } - if (Looper.myLooper() == Looper.getMainLooper()) { - new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void... params) { - for (File file : files) { + sLastCacheCleanUpTask = new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + if (isCancelled()) { + return null; + } + File files[] = sBufferDir.listFiles(); + if (files == null || files.length == 0) { + return null; + } + for (File file : files) { + if (isCancelled()) { + break; + } + long lastModified = file.lastModified(); + if (lastModified != 0 && lastModified < now) { file.delete(); } - return null; } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } else { - for (File file : files) { - file.delete(); + return null; } - } + }; + sLastCacheCleanUpTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } @Override public File getBufferDir() { - return mBufferDir; + return sBufferDir; } @Override @@ -104,25 +122,26 @@ public class TrickplayStorageManager implements BufferManager.StorageManager { @Override public boolean hasEnoughBuffer(long pendingDelete) { - return mBufferDir.getUsableSpace() + pendingDelete >= mStorageBufferBytes; + return sBufferDir.getUsableSpace() + pendingDelete >= sStorageBufferBytes; } @Override - public Pair<String, MediaFormat> readTrackInfoFile(boolean isAudio) { + public List<BufferManager.TrackFormat> readTrackInfoFiles(boolean isAudio) { return null; } @Override - public ArrayList<Long> readIndexFile(String trackId) { + public ArrayList<BufferManager.PositionHolder> readIndexFile(String trackId) { return null; } @Override - public void writeTrackInfoFile(String trackId, MediaFormat format, boolean isAudio) { + public void writeTrackInfoFiles(List<BufferManager.TrackFormat> formatList, boolean isAudio) { } @Override - public void writeIndexFile(String trackName, SortedMap<Long, SampleChunk> index) { + public void writeIndexFile(String trackName, + SortedMap<Long, Pair<SampleChunk, Integer>> index) { } } diff --git a/src/com/android/tv/tuner/exoplayer/ffmpeg/FfmpegDecoderClient.java b/src/com/android/tv/tuner/exoplayer/ffmpeg/FfmpegDecoderClient.java new file mode 100644 index 00000000..356636cc --- /dev/null +++ b/src/com/android/tv/tuner/exoplayer/ffmpeg/FfmpegDecoderClient.java @@ -0,0 +1,249 @@ +/* + * Copyright (C) 2017 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.ffmpeg; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.os.RemoteException; + +import android.support.annotation.MainThread; +import android.support.annotation.WorkerThread; +import android.support.annotation.VisibleForTesting; +import com.google.android.exoplayer.SampleHolder; +import com.android.tv.Features; +import com.android.tv.tuner.exoplayer.audio.AudioDecoder; + +import java.nio.ByteBuffer; + +/** + * The class connects {@link FfmpegDecoderService} to decode audio samples. + * In order to sandbox ffmpeg based decoder, {@link FfmpegDecoderService} is an isolated process + * without any permission and connected by binder. + */ +public class FfmpegDecoderClient extends AudioDecoder { + private static FfmpegDecoderClient sInstance; + + private IFfmpegDecoder mService; + private Boolean mIsAvailable; + + private static final String FFMPEG_DECODER_SERVICE_FILTER = + "com.android.tv.tuner.exoplayer.ffmpeg.IFfmpegDecoder"; + private static final long FFMPEG_SERVICE_CONNECT_TIMEOUT_MS = 500; + + private final ServiceConnection mConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName className, IBinder service) { + mService = IFfmpegDecoder.Stub.asInterface(service); + synchronized (FfmpegDecoderClient.this) { + try { + mIsAvailable = mService.isAvailable(); + } catch (RemoteException e) { + } + FfmpegDecoderClient.this.notify(); + } + } + + @Override + public void onServiceDisconnected(ComponentName className) { + synchronized (FfmpegDecoderClient.this) { + sInstance.releaseLocked(); + mIsAvailable = false; + mService = null; + } + } + }; + + /** + * Connects to the decoder service for future uses. + * @param context + * @return {@code true} when decoder service is connected. + */ + @MainThread + public synchronized static boolean connect(Context context) { + if (Features.AC3_SOFTWARE_DECODE.isEnabled(context)) { + if (sInstance == null) { + sInstance = new FfmpegDecoderClient(); + Intent intent = + new Intent(FFMPEG_DECODER_SERVICE_FILTER) + .setComponent( + new ComponentName(context, FfmpegDecoderService.class)); + if (context.bindService(intent, sInstance.mConnection, Context.BIND_AUTO_CREATE)) { + return true; + } else { + sInstance = null; + } + } + } + return false; + } + + /** + * Disconnects from the decoder service and release resources. + * @param context + */ + @MainThread + public synchronized static void disconnect(Context context) { + if (sInstance != null) { + synchronized (sInstance) { + sInstance.releaseLocked(); + if (sInstance.mIsAvailable != null && sInstance.mIsAvailable) { + context.unbindService(sInstance.mConnection); + } + sInstance.mIsAvailable = false; + sInstance.mService = null; + } + sInstance = null; + } + } + + /** + * Returns whether service is available or not. + * Before using client, this should be used to check availability. + */ + @WorkerThread + public synchronized static boolean isAvailable() { + if (sInstance != null) { + return sInstance.available(); + } + return false; + } + + /** + * Returns an client instance. + */ + public synchronized static FfmpegDecoderClient getInstance() { + if (sInstance != null) { + sInstance.createDecoder(); + } + return sInstance; + } + + private FfmpegDecoderClient() { + } + + private synchronized boolean available() { + if (mIsAvailable == null) { + try { + this.wait(FFMPEG_SERVICE_CONNECT_TIMEOUT_MS); + } catch (InterruptedException e) { + } + } + return mIsAvailable != null && mIsAvailable == true; + } + + private synchronized void createDecoder() { + if (mIsAvailable == null || mIsAvailable == false) { + return; + } + try { + mService.create(); + } catch (RemoteException e) { + } + } + + private void releaseLocked() { + if (mIsAvailable == null || mIsAvailable == false) { + return; + } + try { + mService.release(); + } catch (RemoteException e) { + } + } + + @Override + public synchronized void release() { + releaseLocked(); + } + + @Override + public synchronized void decode(SampleHolder sampleHolder) { + if (mIsAvailable == null || mIsAvailable == false) { + return; + } + byte[] sampleBytes = new byte [sampleHolder.data.limit()]; + sampleHolder.data.get(sampleBytes, 0, sampleBytes.length); + try { + mService.decode(sampleHolder.timeUs, sampleBytes); + } catch (RemoteException e) { + } + } + + @Override + public synchronized void resetDecoderState(String mimeType) { + if (mIsAvailable == null || mIsAvailable == false) { + return; + } + try { + mService.resetDecoderState(mimeType); + } catch (RemoteException e) { + } + } + + @Override + public synchronized ByteBuffer getDecodedSample() { + if (mIsAvailable == null || mIsAvailable == false) { + return null; + } + try { + byte[] outputBytes = mService.getDecodedSample(); + if (outputBytes != null && outputBytes.length > 0) { + return ByteBuffer.wrap(outputBytes); + } + } catch (RemoteException e) { + } + return null; + } + + @Override + public synchronized long getDecodedTimeUs() { + if (mIsAvailable == null || mIsAvailable == false) { + return 0; + } + try { + return mService.getDecodedTimeUs(); + } catch (RemoteException e) { + } + return 0; + } + + @VisibleForTesting + public boolean testSandboxIsolatedProcess() { + // When testing isolated process, we will check the permission in FfmpegDecoderService. + // If the service have any permission, an exception will be thrown. + try { + mService.testSandboxIsolatedProcess(); + } catch (RemoteException e) { + return false; + } + return true; + } + + @VisibleForTesting + public void testSandboxMinijail() { + // When testing minijail, we will call a system call which is blocked by minijail. In that + // case, the FfmpegDecoderService will be disconnected, we can check the connection status + // to make sure if the minijail works or not. + try { + mService.testSandboxMinijail(); + } catch (RemoteException e) { + } + } +} diff --git a/src/com/android/tv/tuner/exoplayer/ffmpeg/FfmpegDecoderService.java b/src/com/android/tv/tuner/exoplayer/ffmpeg/FfmpegDecoderService.java new file mode 100644 index 00000000..3ebdd381 --- /dev/null +++ b/src/com/android/tv/tuner/exoplayer/ffmpeg/FfmpegDecoderService.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2017 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.ffmpeg; + +import android.app.Service; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.res.AssetFileDescriptor; +import android.os.AsyncTask; +import android.os.IBinder; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioDecoder; + +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Ffmpeg based audio decoder service. + * It should be isolatedProcess due to security reason. + */ +public class FfmpegDecoderService extends Service { + private static final String TAG = "FfmpegDecoderService"; + private static final boolean DEBUG = false; + + private static final String POLICY_FILE = "whitelist.policy"; + + private static final long MINIJAIL_SETUP_WAIT_TIMEOUT_MS = 5000; + + private static boolean sLibraryLoaded = true; + + static { + try { + System.loadLibrary("minijail_jni"); + } catch (Exception | Error e) { + Log.e(TAG, "Load minijail failed:", e); + sLibraryLoaded = false; + } + } + + private FfmpegDecoder mBinder = new FfmpegDecoder(); + private volatile Object mMinijailSetupMonitor = new Object(); + //@GuardedBy("mMinijailSetupMonitor") + private volatile Boolean mMinijailSetup; + + @Override + public void onCreate() { + if (sLibraryLoaded) { + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + synchronized (mMinijailSetupMonitor) { + int pipeFd = getPolicyPipeFd(); + if (pipeFd <= 0) { + Log.e(TAG, "fail to open policy file"); + mMinijailSetup = false; + } else { + nativeSetupMinijail(pipeFd); + mMinijailSetup = true; + if (DEBUG) Log.d(TAG, "Minijail setup successfully"); + } + mMinijailSetupMonitor.notify(); + } + return null; + } + }.execute(); + } else { + synchronized (mMinijailSetupMonitor) { + mMinijailSetup = false; + mMinijailSetupMonitor.notify(); + } + } + super.onCreate(); + } + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + private int getPolicyPipeFd() { + try { + ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); + final ParcelFileDescriptor.AutoCloseOutputStream outputStream = + new ParcelFileDescriptor.AutoCloseOutputStream(pipe[1]); + final AssetFileDescriptor policyFile = getAssets().openFd("whitelist.policy"); + final byte[] buffer = new byte[2048]; + final FileInputStream policyStream = policyFile.createInputStream(); + while (true) { + int bytesRead = policyStream.read(buffer); + if (bytesRead == -1) break; + outputStream.write(buffer, 0, bytesRead); + } + policyStream.close(); + outputStream.close(); + return pipe[0].detachFd(); + } catch (IOException e) { + Log.e(TAG, "Policy file not found:" + e); + } + return -1; + } + + private final class FfmpegDecoder extends IFfmpegDecoder.Stub { + FfmpegAudioDecoder mDecoder; + @Override + public boolean isAvailable() { + return isMinijailSetupDone() && FfmpegAudioDecoder.isAvailable(); + } + + @Override + public void create() { + mDecoder = new FfmpegAudioDecoder(FfmpegDecoderService.this); + } + + @Override + public void release() { + if (mDecoder != null) { + mDecoder.release(); + mDecoder = null; + } + } + + @Override + public void decode(long timeUs, byte[] sample) { + if (!isMinijailSetupDone()) { + // If minijail is not setup, we don't run decode for better security. + return; + } + mDecoder.decode(timeUs, sample); + } + + @Override + public void resetDecoderState(String mimetype) { + mDecoder.resetDecoderState(mimetype); + } + + @Override + public byte[] getDecodedSample() { + ByteBuffer decodedBuffer = mDecoder.getDecodedSample(); + byte[] ret = new byte[decodedBuffer.limit()]; + decodedBuffer.get(ret, 0, ret.length); + return ret; + } + + @Override + public long getDecodedTimeUs() { + return mDecoder.getDecodedTimeUs(); + } + + private boolean isMinijailSetupDone() { + synchronized (mMinijailSetupMonitor) { + if (DEBUG) Log.d(TAG, "mMinijailSetup in isAvailable(): " + mMinijailSetup); + if (mMinijailSetup == null) { + try { + if (DEBUG) Log.d(TAG, "Wait till Minijail setup is done"); + mMinijailSetupMonitor.wait(MINIJAIL_SETUP_WAIT_TIMEOUT_MS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + return mMinijailSetup != null && mMinijailSetup; + } + } + + @Override + public void testSandboxIsolatedProcess() { + if (!isMinijailSetupDone()) { + // If minijail is not setup, we return directly to make the test fail. + return; + } + if (FfmpegDecoderService.this.checkSelfPermission("android.permission.INTERNET") + == PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Shouldn't have the permission of internet"); + } + } + + @Override + public void testSandboxMinijail() { + if (!isMinijailSetupDone()) { + // If minijail is not setup, we return directly to make the test fail. + return; + } + nativeTestMinijail(); + } + } + + private native void nativeSetupMinijail(int policyFd); + private native void nativeTestMinijail(); +} diff --git a/src/com/android/tv/tuner/exoplayer/ffmpeg/IFfmpegDecoder.aidl b/src/com/android/tv/tuner/exoplayer/ffmpeg/IFfmpegDecoder.aidl new file mode 100644 index 00000000..ed053790 --- /dev/null +++ b/src/com/android/tv/tuner/exoplayer/ffmpeg/IFfmpegDecoder.aidl @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2017 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.ffmpeg; + +interface IFfmpegDecoder { + boolean isAvailable(); + void create(); + void release(); + void resetDecoderState(String mimetype); + void decode(long timeUs, in byte[] sample); + byte[] getDecodedSample(); + long getDecodedTimeUs(); + void testSandboxIsolatedProcess(); + void testSandboxMinijail(); +}
\ No newline at end of file |