aboutsummaryrefslogtreecommitdiff
path: root/src/com/android/tv/tuner/exoplayer
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/tv/tuner/exoplayer')
-rw-r--r--src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java378
-rw-r--r--src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java14
-rw-r--r--src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java62
-rw-r--r--src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java13
-rw-r--r--src/com/android/tv/tuner/exoplayer/ac3/Ac3DefaultTrackRenderer.java (renamed from src/com/android/tv/tuner/exoplayer/ac3/Ac3PassthroughTrackRenderer.java)114
-rw-r--r--src/com/android/tv/tuner/exoplayer/ac3/Ac3MediaCodecTrackRenderer.java (renamed from src/com/android/tv/tuner/exoplayer/ac3/Ac3TrackRenderer.java)21
-rw-r--r--src/com/android/tv/tuner/exoplayer/ac3/AudioTrackMonitor.java4
-rw-r--r--src/com/android/tv/tuner/exoplayer/ac3/AudioTrackWrapper.java20
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java280
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java209
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java23
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java23
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java115
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java1
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java1
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java85
16 files changed, 935 insertions, 428 deletions
diff --git a/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java b/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java
index c105e222..89641530 100644
--- a/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java
+++ b/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java
@@ -23,17 +23,29 @@ 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.extractor.DefaultExtractorsFactory;
+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.ac3.Ac3DefaultTrackRenderer;
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 +54,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 +67,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 +79,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 DefaultExtractorsFactory(),
// 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 +156,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 +299,7 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
didSomething = true;
}
}
+ mMediaPeriod.continueLoading(mCurrentPosition);
if (!mMetEos) {
if (didSomething) {
mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES);
@@ -171,17 +312,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 +324,109 @@ 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
+ - Ac3DefaultTrackRenderer.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..ba0edf20 100644
--- a/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java
+++ b/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java
@@ -39,8 +39,8 @@ 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.ac3.Ac3DefaultTrackRenderer;
+import com.android.tv.tuner.exoplayer.ac3.Ac3MediaCodecTrackRenderer;
import com.android.tv.tuner.source.TsDataSource;
import com.android.tv.tuner.source.TsDataSourceManager;
import com.android.tv.tuner.tvinput.EventDetector;
@@ -48,11 +48,12 @@ import com.android.tv.tuner.tvinput.EventDetector;
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,
+ Ac3DefaultTrackRenderer.EventListener,
+ Ac3MediaCodecTrackRenderer.Ac3EventListener {
private int mCaptionServiceNumber = Cea708Data.EMPTY_SERVICE_NUMBER;
/**
@@ -304,8 +305,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 Ac3DefaultTrackRenderer) {
+ mPlayer.sendMessage(
+ mAudioRenderer,
+ Ac3DefaultTrackRenderer.MSG_SET_PLAYBACK_SPEED,
playbackParams.getSpeed());
} else {
mPlayer.sendMessage(mAudioRenderer,
@@ -317,10 +320,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,
- 1.0f);
+ if (mAudioRenderer instanceof Ac3DefaultTrackRenderer) {
+ mPlayer.sendMessage(
+ mAudioRenderer, Ac3DefaultTrackRenderer.MSG_SET_PLAYBACK_SPEED, 1.0f);
} else {
mPlayer.sendMessage(mAudioRenderer,
MediaCodecAudioTrackRenderer.MSG_SET_PLAYBACK_PARAMS,
@@ -423,8 +425,8 @@ 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 Ac3DefaultTrackRenderer) {
+ mPlayer.sendMessage(mAudioRenderer, Ac3DefaultTrackRenderer.MSG_SET_VOLUME, volume);
} else {
mPlayer.sendMessage(mAudioRenderer, MediaCodecAudioTrackRenderer.MSG_SET_VOLUME,
volume);
@@ -437,9 +439,9 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen
* @param enable enables the audio when {@code true}, disables otherwise.
*/
public void setAudioTrack(boolean enable) {
- if (mAudioRenderer instanceof Ac3PassthroughTrackRenderer) {
- mPlayer.sendMessage(mAudioRenderer, Ac3PassthroughTrackRenderer.MSG_SET_AUDIO_TRACK,
- enable ? 1 : 0);
+ if (mAudioRenderer instanceof Ac3DefaultTrackRenderer) {
+ mPlayer.sendMessage(
+ mAudioRenderer, Ac3DefaultTrackRenderer.MSG_SET_AUDIO_TRACK, enable ? 1 : 0);
} else {
mPlayer.sendMessage(mAudioRenderer, MediaCodecAudioTrackRenderer.MSG_SET_VOLUME,
enable ? mVolume : 0.0f);
@@ -495,6 +497,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() {
@@ -650,4 +674,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..a1a97d3d 100644
--- a/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java
+++ b/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java
@@ -21,9 +21,10 @@ import android.content.Context;
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.ac3.Ac3DefaultTrackRenderer;
import com.android.tv.tuner.exoplayer.buffer.BufferManager;
import com.android.tv.tuner.tvinput.PlaybackBufferListener;
@@ -52,10 +53,12 @@ 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 Ac3DefaultTrackRenderer for A/V sync issue. We will use
+ // {@link Ac3MediaCodecTrackRenderer} when we use ExoPlayer's extractor.
+ TrackRenderer audioRenderer =
+ new Ac3DefaultTrackRenderer(
+ sampleSource, mpegTsPlayer.getMainHandler(), mpegTsPlayer,
+ !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/Ac3PassthroughTrackRenderer.java b/src/com/android/tv/tuner/exoplayer/ac3/Ac3DefaultTrackRenderer.java
index 9dae2e34..d442fde8 100644
--- a/src/com/android/tv/tuner/exoplayer/ac3/Ac3PassthroughTrackRenderer.java
+++ b/src/com/android/tv/tuner/exoplayer/ac3/Ac3DefaultTrackRenderer.java
@@ -23,16 +23,15 @@ 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.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.google.android.exoplayer2.decoder.SimpleOutputBuffer;
import com.android.tv.tuner.tvinput.TunerDebug;
import java.io.IOException;
@@ -40,9 +39,9 @@ import java.nio.ByteBuffer;
import java.util.ArrayList;
/**
- * Decodes and renders AC3 audio.
+ * Decodes and renders AC3 audio. Supports passthrough playback and ffmpeg based software decoding.
*/
-public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaClock {
+public class Ac3DefaultTrackRenderer 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 +50,14 @@ 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";
+ // 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 = "Ac3DefaultTrackRenderer";
private static final boolean DEBUG = false;
/**
@@ -93,6 +99,8 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
private final AudioClock mAudioClock;
private MediaFormat mFormat;
+ private boolean mFormatConfigured;
+ private int mSampleSize;
private final ByteBuffer mOutputBuffer;
private boolean mOutputReady;
private int mTrackIndex;
@@ -106,10 +114,15 @@ 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) {
+ public Ac3DefaultTrackRenderer(
+ SampleSource source,
+ Handler eventHandler,
+ EventListener listener,
+ boolean usePassthrough) {
mSource = source.register();
mEventHandler = eventHandler;
mEventListener = listener;
@@ -325,14 +338,38 @@ 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;
+ mFormat = formatHolder.format;
+ mFormatConfigured = true;
if (DEBUG) {
Log.d(TAG, "AudioTrack was configured to FORMAT: " + mFormat.toString());
}
clearDecodeState();
- AUDIO_TRACK.reconfigure(mFormat.getFrameworkMediaFormatV16());
+ 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 boolean feedInputBuffer() throws IOException, ExoPlaybackException {
@@ -359,8 +396,11 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
return false;
}
default: {
+ if (mSampleHolder.size != mSampleSize && mFormatConfigured) {
+ onSampleSizeChanged(mSampleHolder.size);
+ }
mSampleHolder.data.flip();
- decodeDone(mSampleHolder.data, mSampleHolder.timeUs);
+ decodeDone(mSampleHolder.data, mSampleHolder.timeUs);
return true;
}
}
@@ -511,24 +551,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 +582,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/ac3/Ac3MediaCodecTrackRenderer.java
index 2bf86b5a..604959d1 100644
--- a/src/com/android/tv/tuner/exoplayer/ac3/Ac3TrackRenderer.java
+++ b/src/com/android/tv/tuner/exoplayer/ac3/Ac3MediaCodecTrackRenderer.java
@@ -25,14 +25,14 @@ 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";
+public class Ac3MediaCodecTrackRenderer extends MediaCodecAudioTrackRenderer {
+ private final String TAG = "Ac3MediaCodecTrackRenderer";
private final boolean DEBUG = false;
private final Ac3EventListener mListener;
@@ -47,8 +47,11 @@ public class Ac3TrackRenderer extends MediaCodecAudioTrackRenderer {
void onAudioTrackSetPlaybackParamsError(IllegalArgumentException e);
}
- public Ac3TrackRenderer(SampleSource source, MediaCodecSelector mediaCodecSelector,
- Handler eventHandler, EventListener eventListener) {
+ public Ac3MediaCodecTrackRenderer(
+ 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/ac3/AudioTrackMonitor.java b/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackMonitor.java
index bfdf08ac..6f152490 100644
--- a/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackMonitor.java
+++ b/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackMonitor.java
@@ -98,8 +98,8 @@ 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 sampleDuration =
+ (mTotalCount - 1) * Ac3DefaultTrackRenderer.AC3_SAMPLE_DURATION_US / 1000;
long totalDuration = now - mStartMs;
StringBuilder ptsBuilder = new StringBuilder();
ptsBuilder.append("PTS received ").append(mSampleCount).append(", ")
diff --git a/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackWrapper.java b/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackWrapper.java
index bc3c5d00..393e12c3 100644
--- a/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackWrapper.java
+++ b/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackWrapper.java
@@ -18,6 +18,7 @@ package com.android.tv.tuner.exoplayer.ac3;
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 =
+ Ac3DefaultTrackRenderer.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/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..bea3defd 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.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..ab6d1a75 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java
@@ -151,18 +151,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 +246,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..0b219b41 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java
@@ -19,6 +19,7 @@ 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;
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) {
}
}