blob: fe2bb464c7bd35ac76c1e039120c82198851b3a5 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tv.tuner.exoplayer2.buffer;
import android.os.ConditionVariable;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.util.Log;
import com.android.tv.tuner.exoplayer.MpegTsPlayer;
import com.android.tv.tuner.exoplayer.SampleExtractor;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.util.Assertions;
import com.google.auto.factory.AutoFactory;
import com.google.auto.factory.Provided;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Handles I/O between {@link SampleExtractor} and {@link BufferManager}.Reads & writes samples
* from/to {@link SampleChunk} which is backed by physical storage.
*/
public class RecordingSampleBuffer
implements BufferManager.SampleBuffer, BufferManager.ChunkEvictedListener {
private static final String TAG = "RecordingSampleBuffer";
@IntDef({BUFFER_REASON_LIVE_PLAYBACK, BUFFER_REASON_RECORDED_PLAYBACK, BUFFER_REASON_RECORDING})
@Retention(RetentionPolicy.SOURCE)
public @interface BufferReason {}
/** A buffer reason for live-stream playback. */
public static final int BUFFER_REASON_LIVE_PLAYBACK = 0;
/** A buffer reason for playback of a recorded program. */
public static final int BUFFER_REASON_RECORDED_PLAYBACK = 1;
/** A buffer reason for recording a program. */
public static final int BUFFER_REASON_RECORDING = 2;
/** The minimum duration to support seek in Trickplay. */
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);
private final BufferManager mBufferManager;
private final PlaybackBufferListener mBufferListener;
private final @BufferReason int mBufferReason;
private final SampleChunkIoHelper.Factory mSampleChunkIoHelperFactory;
private int mTrackCount;
private boolean[] mTrackSelected;
private List<SampleQueue> mReadSampleQueues;
private final SamplePool mSamplePool = new SamplePool();
private long mLastBufferedPositionUs = C.UNKNOWN_TIME_US;
private long mCurrentPlaybackPositionUs = 0;
// An error in I/O thread of {@link SampleChunkIoHelper} will be notified.
private volatile boolean mError;
// Eos was reached in I/O thread of {@link SampleChunkIoHelper}.
private volatile boolean mEos;
private SampleChunkIoHelper mSampleChunkIoHelper;
private final SampleChunkIoHelper.IoCallback mIoCallback =
new SampleChunkIoHelper.IoCallback() {
@Override
public void onIoReachedEos() {
mEos = true;
}
@Override
public void onIoError() {
mError = true;
}
};
/**
* Factory for {@link RecordingSampleBuffer}.
*
* <p>This wrapper class keeps other classes from needing to reference the {@link AutoFactory}
* generated class.
*/
public interface Factory {
public RecordingSampleBuffer create(
BufferManager bufferManager,
PlaybackBufferListener bufferListener,
boolean enableTrickplay,
@BufferReason int bufferReason);
}
/**
* Creates {@link BufferManager.SampleBuffer} with cached I/O backed by physical storage (e.g.
* trickplay,recording,recorded-playback).
*
* @param bufferManager the manager of {@link SampleChunk}
* @param bufferListener the listener for buffer I/O event
* @param enableTrickplay {@code true} when trickplay should be enabled
* @param bufferReason the reason for caching samples {@link BufferReason}
*/
@AutoFactory(implementing = Factory.class)
public RecordingSampleBuffer(
BufferManager bufferManager,
PlaybackBufferListener bufferListener,
boolean enableTrickplay,
@BufferReason int bufferReason,
@Provided SampleChunkIoHelper.Factory sampleChunkIoHelperFactory) {
mBufferManager = bufferManager;
mBufferListener = bufferListener;
if (bufferListener != null) {
bufferListener.onBufferStateChanged(enableTrickplay);
}
mBufferReason = bufferReason;
mSampleChunkIoHelperFactory = sampleChunkIoHelperFactory;
}
@Override
public void init(@NonNull List<String> ids, @NonNull List<MediaFormat> mediaFormats)
throws IOException {
mTrackCount = ids.size();
if (mTrackCount <= 0) {
throw new IOException("No tracks to initialize");
}
mTrackSelected = new boolean[mTrackCount];
mReadSampleQueues = new ArrayList<>();
mSampleChunkIoHelper =
mSampleChunkIoHelperFactory.create(
ids, mediaFormats, mBufferReason, mBufferManager, mSamplePool, mIoCallback);
for (int i = 0; i < mTrackCount; ++i) {
mReadSampleQueues.add(i, new SampleQueue(mSamplePool));
}
mSampleChunkIoHelper.init();
for (int i = 0; i < mTrackCount; ++i) {
mBufferManager.registerChunkEvictedListener(ids.get(i), RecordingSampleBuffer.this);
}
}
@Override
public void selectTrack(int index) {
if (!mTrackSelected[index]) {
mTrackSelected[index] = true;
mReadSampleQueues.get(index).clear();
mSampleChunkIoHelper.openRead(index, mCurrentPlaybackPositionUs);
}
}
@Override
public void deselectTrack(int index) {
if (mTrackSelected[index]) {
mTrackSelected[index] = false;
mReadSampleQueues.get(index).clear();
mSampleChunkIoHelper.closeRead(index);
}
}
@Override
public void writeSample(int index, SampleHolder sample, ConditionVariable conditionVariable)
throws IOException {
mSampleChunkIoHelper.writeSample(index, sample, conditionVariable);
if (!conditionVariable.block(BUFFER_WRITE_TIMEOUT_MS)) {
Log.e(TAG, "Error: Serious delay on writing buffer");
conditionVariable.block();
}
}
@Override
public boolean isWriteSpeedSlow(int sampleSize, long writeDurationNs) {
if (mBufferReason == BUFFER_REASON_RECORDED_PLAYBACK) {
return false;
}
mBufferManager.addWriteStat(sampleSize, writeDurationNs);
return mBufferManager.isWriteSlow();
}
@Override
public void handleWriteSpeedSlow() throws IOException {
if (mBufferReason == BUFFER_REASON_RECORDING) {
// Recording does not need to stop because I/O speed is slow temporarily.
// If fixed size buffer of TsStreamer overflows, TsDataSource will reach EoS.
// Reaching EoS will stop recording eventually.
Log.w(
TAG,
"Disk I/O speed is slow for recording temporarily: "
+ mBufferManager.getWriteBandwidth()
+ "MBps");
return;
}
// Disables buffering samples afterwards, and notifies the disk speed is slow.
Log.w(TAG, "Disk is too slow for trickplay");
mBufferListener.onDiskTooSlow();
}
@Override
public void setEos() {
mSampleChunkIoHelper.closeWrite();
}
private boolean maybeReadSample(SampleQueue queue, int index) {
if (queue.getLastQueuedPositionUs() != null
&& queue.getLastQueuedPositionUs() > mCurrentPlaybackPositionUs + BUFFER_NEEDED_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.
// But, the throttling should provide enough samples for the player to
// finish the buffering state.
return false;
}
SampleHolder sample = mSampleChunkIoHelper.readSample(index);
if (sample != null) {
queue.queueSample(sample);
return true;
}
return false;
}
@Override
public int readSample(int track, SampleHolder outSample) {
Assertions.checkState(mTrackSelected[track]);
maybeReadSample(mReadSampleQueues.get(track), track);
int result = mReadSampleQueues.get(track).dequeueSample(outSample);
if ((result != SampleSource.SAMPLE_READ && mEos) || mError) {
return SampleSource.END_OF_STREAM;
}
return result;
}
@Override
public void seekTo(long positionUs) {
for (int i = 0; i < mTrackCount; ++i) {
if (mTrackSelected[i]) {
mReadSampleQueues.get(i).clear();
mSampleChunkIoHelper.openRead(i, positionUs);
}
}
mLastBufferedPositionUs = positionUs;
}
@Override
public long getBufferedPositionUs() {
Long result = null;
for (int i = 0; i < mTrackCount; ++i) {
if (!mTrackSelected[i]) {
continue;
}
Long lastQueuedSamplePositionUs = mReadSampleQueues.get(i).getLastQueuedPositionUs();
if (lastQueuedSamplePositionUs == null) {
// No sample has been queued.
result = mLastBufferedPositionUs;
continue;
}
if (result == null || result > lastQueuedSamplePositionUs) {
result = lastQueuedSamplePositionUs;
}
}
if (result == null) {
return mLastBufferedPositionUs;
}
return (mLastBufferedPositionUs = result);
}
@Override
public boolean continueBuffering(long positionUs) {
mCurrentPlaybackPositionUs = positionUs;
for (int i = 0; i < mTrackCount; ++i) {
if (!mTrackSelected[i]) {
continue;
}
SampleQueue queue = mReadSampleQueues.get(i);
maybeReadSample(queue, i);
if (queue.getLastQueuedPositionUs() == null
|| positionUs > queue.getLastQueuedPositionUs()) {
// No more buffered data.
return false;
}
}
return true;
}
@Override
public void release() throws IOException {
if (mTrackCount <= 0) {
return;
}
if (mSampleChunkIoHelper != null) {
mSampleChunkIoHelper.release();
}
}
// onChunkEvictedListener
@Override
public void onChunkEvicted(String id, long createdTimeMs) {
if (mBufferListener != null) {
mBufferListener.onBufferStartTimeChanged(
createdTimeMs + TimeUnit.MICROSECONDS.toMillis(MIN_SEEK_DURATION_US));
}
}
}