1/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.tv.tuner.exoplayer.buffer;
18
19import android.os.ConditionVariable;
20import android.support.annotation.IntDef;
21import android.support.annotation.NonNull;
22import android.util.Log;
23
24import com.google.android.exoplayer.C;
25import com.google.android.exoplayer.MediaFormat;
26import com.google.android.exoplayer.SampleHolder;
27import com.google.android.exoplayer.SampleSource;
28import com.google.android.exoplayer.util.Assertions;
29import com.android.tv.tuner.exoplayer.MpegTsPlayer;
30import com.android.tv.tuner.tvinput.PlaybackBufferListener;
31import com.android.tv.tuner.exoplayer.SampleExtractor;
32
33import java.io.IOException;
34import java.lang.annotation.Retention;
35import java.lang.annotation.RetentionPolicy;
36import java.util.ArrayList;
37import java.util.List;
38import java.util.concurrent.TimeUnit;
39
40/**
41 * Handles I/O between {@link SampleExtractor} and
42 * {@link BufferManager}.Reads & writes samples from/to {@link SampleChunk} which is backed
43 * by physical storage.
44 */
45public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
46        BufferManager.ChunkEvictedListener {
47    private static final String TAG = "RecordingSampleBuffer";
48
49    @IntDef({BUFFER_REASON_LIVE_PLAYBACK, BUFFER_REASON_RECORDED_PLAYBACK, BUFFER_REASON_RECORDING})
50    @Retention(RetentionPolicy.SOURCE)
51    public @interface BufferReason {}
52
53    /**
54     * A buffer reason for live-stream playback.
55     */
56    public static final int BUFFER_REASON_LIVE_PLAYBACK = 0;
57
58    /**
59     * A buffer reason for playback of a recorded program.
60     */
61    public static final int BUFFER_REASON_RECORDED_PLAYBACK = 1;
62
63    /**
64     * A buffer reason for recording a program.
65     */
66    public static final int BUFFER_REASON_RECORDING = 2;
67
68    /**
69     * The minimum duration to support seek in Trickplay.
70     */
71    static final long MIN_SEEK_DURATION_US = TimeUnit.MILLISECONDS.toMicros(500);
72
73    /**
74     * The duration of a {@link SampleChunk} for recordings.
75     */
76    static final long RECORDING_CHUNK_DURATION_US = MIN_SEEK_DURATION_US * 1200; // 10 minutes
77    private static final long BUFFER_WRITE_TIMEOUT_MS = 10 * 1000;  // 10 seconds
78    private static final long BUFFER_NEEDED_US =
79            1000L * Math.max(MpegTsPlayer.MIN_BUFFER_MS, MpegTsPlayer.MIN_REBUFFER_MS);
80
81    private final BufferManager mBufferManager;
82    private final PlaybackBufferListener mBufferListener;
83    private final @BufferReason int mBufferReason;
84
85    private int mTrackCount;
86    private boolean[] mTrackSelected;
87    private List<SampleQueue> mReadSampleQueues;
88    private final SamplePool mSamplePool = new SamplePool();
89    private long mLastBufferedPositionUs = C.UNKNOWN_TIME_US;
90    private long mCurrentPlaybackPositionUs = 0;
91
92    // An error in I/O thread of {@link SampleChunkIoHelper} will be notified.
93    private volatile boolean mError;
94
95    // Eos was reached in I/O thread of {@link SampleChunkIoHelper}.
96    private volatile boolean mEos;
97    private SampleChunkIoHelper mSampleChunkIoHelper;
98    private final SampleChunkIoHelper.IoCallback mIoCallback =
99            new SampleChunkIoHelper.IoCallback() {
100        @Override
101        public void onIoReachedEos() {
102            mEos = true;
103        }
104
105        @Override
106        public void onIoError() {
107            mError = true;
108        }
109    };
110
111    /**
112     * Creates {@link BufferManager.SampleBuffer} with
113     * cached I/O backed by physical storage (e.g. trickplay,recording,recorded-playback).
114     *
115     * @param bufferManager the manager of {@link SampleChunk}
116     * @param bufferListener the listener for buffer I/O event
117     * @param enableTrickplay {@code true} when trickplay should be enabled
118     * @param bufferReason the reason for caching samples {@link RecordingSampleBuffer.BufferReason}
119     */
120    public RecordingSampleBuffer(BufferManager bufferManager, PlaybackBufferListener bufferListener,
121            boolean enableTrickplay, @BufferReason int bufferReason) {
122        mBufferManager = bufferManager;
123        mBufferListener = bufferListener;
124        if (bufferListener != null) {
125            bufferListener.onBufferStateChanged(enableTrickplay);
126        }
127        mBufferReason = bufferReason;
128    }
129
130    @Override
131    public void init(@NonNull List<String> ids, @NonNull List<MediaFormat> mediaFormats)
132            throws IOException {
133        mTrackCount = ids.size();
134        if (mTrackCount <= 0) {
135            throw new IOException("No tracks to initialize");
136        }
137        mTrackSelected = new boolean[mTrackCount];
138        mReadSampleQueues = new ArrayList<>();
139        mSampleChunkIoHelper = new SampleChunkIoHelper(ids, mediaFormats, mBufferReason,
140                mBufferManager, mSamplePool, mIoCallback);
141        for (int i = 0; i < mTrackCount; ++i) {
142            mReadSampleQueues.add(i, new SampleQueue(mSamplePool));
143        }
144        mSampleChunkIoHelper.init();
145        for (int i = 0; i < mTrackCount; ++i) {
146            mBufferManager.registerChunkEvictedListener(ids.get(i), RecordingSampleBuffer.this);
147        }
148    }
149
150    @Override
151    public void selectTrack(int index) {
152        if (!mTrackSelected[index]) {
153            mTrackSelected[index] = true;
154            mReadSampleQueues.get(index).clear();
155            mSampleChunkIoHelper.openRead(index, mCurrentPlaybackPositionUs);
156        }
157    }
158
159    @Override
160    public void deselectTrack(int index) {
161        if (mTrackSelected[index]) {
162            mTrackSelected[index] = false;
163            mReadSampleQueues.get(index).clear();
164            mSampleChunkIoHelper.closeRead(index);
165        }
166    }
167
168    @Override
169    public void writeSample(int index, SampleHolder sample, ConditionVariable conditionVariable)
170            throws IOException {
171        mSampleChunkIoHelper.writeSample(index, sample, conditionVariable);
172
173        if (!conditionVariable.block(BUFFER_WRITE_TIMEOUT_MS)) {
174            Log.e(TAG, "Error: Serious delay on writing buffer");
175            conditionVariable.block();
176        }
177    }
178
179    @Override
180    public boolean isWriteSpeedSlow(int sampleSize, long writeDurationNs) {
181        if (mBufferReason == BUFFER_REASON_RECORDED_PLAYBACK) {
182            return false;
183        }
184        mBufferManager.addWriteStat(sampleSize, writeDurationNs);
185        return mBufferManager.isWriteSlow();
186    }
187
188    @Override
189    public void handleWriteSpeedSlow() throws IOException{
190        if (mBufferReason == BUFFER_REASON_RECORDING) {
191            // Recording does not need to stop because I/O speed is slow temporarily.
192            // If fixed size buffer of TsStreamer overflows, TsDataSource will reach EoS.
193            // Reaching EoS will stop recording eventually.
194            Log.w(TAG, "Disk I/O speed is slow for recording temporarily: "
195                    + mBufferManager.getWriteBandwidth() + "MBps");
196            return;
197        }
198        // Disables buffering samples afterwards, and notifies the disk speed is slow.
199        Log.w(TAG, "Disk is too slow for trickplay");
200        mBufferListener.onDiskTooSlow();
201    }
202
203    @Override
204    public void setEos() {
205        mSampleChunkIoHelper.closeWrite();
206    }
207
208    private boolean maybeReadSample(SampleQueue queue, int index) {
209        if (queue.getLastQueuedPositionUs() != null
210                && queue.getLastQueuedPositionUs() > mCurrentPlaybackPositionUs + BUFFER_NEEDED_US
211                && queue.isDurationGreaterThan(MIN_SEEK_DURATION_US)) {
212            // The speed of queuing samples can be higher than the playback speed.
213            // If the duration of the samples in the queue is not limited,
214            // samples can be accumulated and there can be out-of-memory issues.
215            // But, the throttling should provide enough samples for the player to
216            // finish the buffering state.
217            return false;
218        }
219        SampleHolder sample = mSampleChunkIoHelper.readSample(index);
220        if (sample != null) {
221            queue.queueSample(sample);
222            return true;
223        }
224        return false;
225    }
226
227    @Override
228    public int readSample(int track, SampleHolder outSample) {
229        Assertions.checkState(mTrackSelected[track]);
230        maybeReadSample(mReadSampleQueues.get(track), track);
231        int result = mReadSampleQueues.get(track).dequeueSample(outSample);
232        if ((result != SampleSource.SAMPLE_READ && mEos) || mError) {
233            return SampleSource.END_OF_STREAM;
234        }
235        return result;
236    }
237
238    @Override
239    public void seekTo(long positionUs) {
240        for (int i = 0; i < mTrackCount; ++i) {
241            if (mTrackSelected[i]) {
242                mReadSampleQueues.get(i).clear();
243                mSampleChunkIoHelper.openRead(i, positionUs);
244            }
245        }
246        mLastBufferedPositionUs = positionUs;
247    }
248
249    @Override
250    public long getBufferedPositionUs() {
251        Long result = null;
252        for (int i = 0; i < mTrackCount; ++i) {
253            if (!mTrackSelected[i]) {
254                continue;
255            }
256            Long lastQueuedSamplePositionUs =
257                    mReadSampleQueues.get(i).getLastQueuedPositionUs();
258            if (lastQueuedSamplePositionUs == null) {
259                // No sample has been queued.
260                result = mLastBufferedPositionUs;
261                continue;
262            }
263            if (result == null || result > lastQueuedSamplePositionUs) {
264                result = lastQueuedSamplePositionUs;
265            }
266        }
267        if (result == null) {
268            return mLastBufferedPositionUs;
269        }
270        return (mLastBufferedPositionUs = result);
271    }
272
273    @Override
274    public boolean continueBuffering(long positionUs) {
275        mCurrentPlaybackPositionUs = positionUs;
276        for (int i = 0; i < mTrackCount; ++i) {
277            if (!mTrackSelected[i]) {
278                continue;
279            }
280            SampleQueue queue = mReadSampleQueues.get(i);
281            maybeReadSample(queue, i);
282            if (queue.getLastQueuedPositionUs() == null
283                    || positionUs > queue.getLastQueuedPositionUs()) {
284                // No more buffered data.
285                return false;
286            }
287        }
288        return true;
289    }
290
291    @Override
292    public void release() throws IOException {
293        if (mTrackCount <= 0) {
294            return;
295        }
296        if (mSampleChunkIoHelper != null) {
297            mSampleChunkIoHelper.release();
298        }
299    }
300
301    // onChunkEvictedListener
302    @Override
303    public void onChunkEvicted(String id, long createdTimeMs) {
304        if (mBufferListener != null) {
305            mBufferListener.onBufferStartTimeChanged(
306                    createdTimeMs + TimeUnit.MICROSECONDS.toMillis(MIN_SEEK_DURATION_US));
307        }
308    }
309}
310