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.usbtuner.exoplayer.cache;
18
19import android.media.MediaCodec;
20import android.media.MediaFormat;
21import android.os.ConditionVariable;
22import android.support.annotation.IntDef;
23import android.util.Log;
24import android.util.Pair;
25
26import com.google.android.exoplayer.C;
27import com.google.android.exoplayer.SampleHolder;
28import com.google.android.exoplayer.SampleSource;
29import com.google.android.exoplayer.util.MimeTypes;
30import com.android.usbtuner.tvinput.PlaybackCacheListener;
31
32import java.io.IOException;
33import java.lang.annotation.Retention;
34import java.lang.annotation.RetentionPolicy;
35import java.util.List;
36import java.util.concurrent.TimeUnit;
37
38import junit.framework.Assert;
39
40/**
41 * Handles I/O between {@link com.android.usbtuner.exoplayer.SampleExtractor} and
42 * {@link CacheManager}.Reads & writes samples from/to {@link SampleCache} which is backed
43 * by physical storage.
44 */
45public class RecordingSampleBuffer implements CacheManager.SampleBuffer,
46        CacheManager.EvictListener {
47    private static final String TAG = "RecordingSampleBuffer";
48    private static final boolean DEBUG = false;
49
50    @IntDef({CACHE_REASON_LIVE_PLAYBACK, CACHE_REASON_RECORDED_PLAYBACK, CACHE_REASON_RECORDING})
51    @Retention(RetentionPolicy.SOURCE)
52    public @interface CacheReason {}
53
54    /**
55     * A cache reason for live-stream playback.
56     */
57    public static final int CACHE_REASON_LIVE_PLAYBACK = 0;
58
59    /**
60     * A cache reason for playback of a recorded program.
61     */
62    public static final int CACHE_REASON_RECORDED_PLAYBACK = 1;
63
64    /**
65     * A cache reason for recording a program.
66     */
67    public static final int CACHE_REASON_RECORDING = 2;
68
69    private static final long CACHE_WRITE_TIMEOUT_MS = 10 * 1000;  // 10 seconds
70    private static final long CHUNK_DURATION_US = TimeUnit.MILLISECONDS.toMicros(500);
71    private static final long LIVE_THRESHOLD_US = TimeUnit.SECONDS.toMicros(1);
72
73    private final CacheManager mCacheManager;
74    private final PlaybackCacheListener mCacheListener;
75    private final int mCacheReason;
76
77    private int mTrackCount;
78    private List<String> mIds;
79    private List<MediaFormat> mMediaFormats;
80    private volatile long mCacheDurationUs = 0;
81    private long[] mCacheEndPositionUs;
82    // SampleCache to append the latest live sample.
83    private SampleCache[] mSampleCaches;
84    private CachedSampleQueue[] mPlayingSampleQueues;
85    private final SamplePool mSamplePool = new SamplePool();
86    private long mLastBufferedPositionUs = C.UNKNOWN_TIME_US;
87    private long mCurrentPlaybackPositionUs = 0;
88    private boolean mEos = false;
89
90    private class CachedSampleQueue extends SampleQueue {
91        private SampleCache mCache = null;
92
93        public CachedSampleQueue(SamplePool samplePool) {
94            super(samplePool);
95        }
96
97        public void setSource(SampleCache newCache) {
98            for (SampleCache cache = mCache; cache != null; cache = cache.getNext()) {
99                cache.clear();
100                cache.close();
101            }
102            mCache = newCache;
103            for (SampleCache cache = mCache; cache != null; cache = cache.getNext()) {
104                cache.resetRead();
105            }
106        }
107
108        public boolean maybeReadSample() {
109            if (isDurationGreaterThan(CHUNK_DURATION_US)) {
110                return false;
111            }
112            SampleHolder sample = mCache.maybeReadSample();
113            if (sample == null) {
114                if (!mCache.canReadMore() && mCache.getNext() != null) {
115                    mCache.clear();
116                    mCache.close();
117                    mCache = mCache.getNext();
118                    mCache.resetRead();
119                    return maybeReadSample();
120                } else {
121                    if (mCacheReason == CACHE_REASON_RECORDED_PLAYBACK
122                            && !mCache.canReadMore() && mCache.getNext() == null) {
123                        // At the end of the recorded playback.
124                        setEos();
125                    }
126                    return false;
127                }
128            } else {
129                queueSample(sample);
130                return true;
131            }
132        }
133
134        public int dequeueSample(SampleHolder sample) {
135            maybeReadSample();
136            return super.dequeueSample(sample);
137        }
138
139        @Override
140        public void clear() {
141            super.clear();
142            for (SampleCache cache = mCache; cache != null; cache = cache.getNext()) {
143                cache.clear();
144                cache.close();
145            }
146            mCache = null;
147        }
148
149        public long getSourceStartPositionUs() {
150            return mCache == null ? -1 : mCache.getStartPositionUs();
151        }
152    }
153
154    /**
155     * Creates {@link com.android.usbtuner.exoplayer.cache.CacheManager.SampleBuffer} with
156     * cached I/O backed by physical storage (e.g. trickplay,recording,recorded-playback).
157     *
158     * @param cacheManager
159     * @param cacheListener
160     * @param enableTrickplay {@code true} when trickplay should be enabled
161     * @param cacheReason the reason for caching samples {@link RecordingSampleBuffer.CacheReason}
162     */
163    public RecordingSampleBuffer(CacheManager cacheManager, PlaybackCacheListener cacheListener,
164            boolean enableTrickplay, @CacheReason int cacheReason) {
165        mCacheManager = cacheManager;
166        mCacheListener = cacheListener;
167        if (cacheListener != null) {
168            cacheListener.onCacheStateChanged(enableTrickplay);
169        }
170        mCacheReason = cacheReason;
171    }
172
173    private String getTrackId(int index) {
174        return mIds.get(index);
175    }
176
177    @Override
178    public synchronized void init(List<String> ids, List<MediaFormat> mediaFormats)
179            throws IOException {
180        mTrackCount = ids.size();
181        if (mTrackCount <= 0) {
182            throw new IOException("No tracks to initialize");
183        }
184        mIds = ids;
185        if (mCacheReason == CACHE_REASON_RECORDING && mediaFormats == null) {
186            throw new IOException("MediaFormat is not provided.");
187        }
188        mMediaFormats = mediaFormats;
189        mSampleCaches = new SampleCache[mTrackCount];
190        mPlayingSampleQueues = new CachedSampleQueue[mTrackCount];
191        mCacheEndPositionUs = new long[mTrackCount];
192        for (int i = 0; i < mTrackCount; i++) {
193            if (mCacheReason != CACHE_REASON_RECORDED_PLAYBACK) {
194                mSampleCaches[i] = mCacheManager.createNewWriteFile(getTrackId(i), 0, mSamplePool);
195                mPlayingSampleQueues[i] = null;
196                mCacheEndPositionUs[i] = CHUNK_DURATION_US;
197            } else {
198                mCacheManager.loadTrackFormStorage(mIds.get(i), mSamplePool);
199            }
200        }
201    }
202
203    private boolean isLiveLocked(long positionUs) {
204        Long livePositionUs = null;
205        for (SampleCache cache : mSampleCaches) {
206            if (livePositionUs == null || livePositionUs < cache.getEndPositionUs()) {
207                livePositionUs = cache.getEndPositionUs();
208            }
209        }
210        return (livePositionUs == null
211                || Math.abs(livePositionUs - positionUs) < LIVE_THRESHOLD_US);
212    }
213
214    private void seekIndividualTrackLocked(int index, long positionUs, boolean isLive) {
215        CachedSampleQueue queue = mPlayingSampleQueues[index];
216        if (queue == null) {
217            return;
218        }
219        queue.clear();
220        if (isLive) {
221            queue.setSource(mSampleCaches[index]);
222        } else {
223            queue.setSource(mCacheManager.getReadFile(getTrackId(index), positionUs));
224        }
225        queue.maybeReadSample();
226    }
227
228    @Override
229    public synchronized void selectTrack(int index) {
230        if (mPlayingSampleQueues[index] == null) {
231            String trackId = getTrackId(index);
232            mPlayingSampleQueues[index] = new CachedSampleQueue(mSamplePool);
233            mCacheManager.registerEvictListener(trackId, this);
234            seekIndividualTrackLocked(index, mCurrentPlaybackPositionUs,
235                    mCacheReason != CACHE_REASON_RECORDED_PLAYBACK && isLiveLocked(
236                            mCurrentPlaybackPositionUs));
237            mPlayingSampleQueues[index].maybeReadSample();
238        }
239    }
240
241    @Override
242    public synchronized void deselectTrack(int index) {
243        if (mPlayingSampleQueues[index] != null) {
244            mPlayingSampleQueues[index].clear();
245            mPlayingSampleQueues[index] = null;
246            mCacheManager.unregisterEvictListener(getTrackId(index));
247        }
248    }
249
250    @Override
251    public void writeSample(int index, SampleHolder sample,
252            ConditionVariable conditionVariable) throws IOException {
253        synchronized (this) {
254            SampleCache cache = mSampleCaches[index];
255            if ((sample.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) {
256                if (sample.timeUs > mCacheDurationUs) {
257                    mCacheDurationUs = sample.timeUs;
258                }
259                if (sample.timeUs >= mCacheEndPositionUs[index]) {
260                    try {
261                        SampleCache nextCache = mCacheManager.createNewWriteFile(
262                                getTrackId(index), mCacheEndPositionUs[index], mSamplePool);
263                        cache.finishWrite(nextCache);
264                        mSampleCaches[index] = cache = nextCache;
265                        mCacheEndPositionUs[index] =
266                                ((sample.timeUs / CHUNK_DURATION_US) + 1) * CHUNK_DURATION_US;
267                    } catch (IOException e) {
268                        cache.finishWrite(null);
269                        throw e;
270                    }
271                }
272            }
273            cache.writeSample(sample, conditionVariable);
274        }
275
276        if (!conditionVariable.block(CACHE_WRITE_TIMEOUT_MS)) {
277            Log.e(TAG, "Error: Serious delay on writing cache");
278            conditionVariable.block();
279        }
280    }
281
282    @Override
283    public boolean isWriteSpeedSlow(int sampleSize, long writeDurationNs) {
284        if (mCacheReason == CACHE_REASON_RECORDED_PLAYBACK) {
285            return false;
286        }
287        mCacheManager.addWriteStat(sampleSize, writeDurationNs);
288        return mCacheManager.isWriteSlow();
289    }
290
291    @Override
292    public void handleWriteSpeedSlow() {
293        Log.w(TAG, "Disk is too slow for trickplay. Disable trickplay.");
294        mCacheManager.disable();
295        mCacheListener.onDiskTooSlow();
296    }
297
298    @Override
299    public synchronized void setEos() {
300        mEos = true;
301    }
302
303    private synchronized boolean reachedEos() {
304        return mEos;
305    }
306
307    @Override
308    public synchronized int readSample(int track, SampleHolder sampleHolder) {
309        CachedSampleQueue queue = mPlayingSampleQueues[track];
310        Assert.assertNotNull(queue);
311        queue.maybeReadSample();
312        int result = queue.dequeueSample(sampleHolder);
313        if (result != SampleSource.SAMPLE_READ && reachedEos()) {
314            return SampleSource.END_OF_STREAM;
315        }
316        return result;
317    }
318
319    @Override
320    public synchronized void seekTo(long positionUs) {
321        boolean isLive = mCacheReason != CACHE_REASON_RECORDED_PLAYBACK && isLiveLocked(positionUs);
322
323        // Seek video track first
324        for (int i = 0; i < mPlayingSampleQueues.length; ++i) {
325            CachedSampleQueue queue = mPlayingSampleQueues[i];
326            if (queue == null) {
327                continue;
328            }
329            seekIndividualTrackLocked(i, positionUs, isLive);
330            if (DEBUG) {
331                Log.d(TAG, "start time = " + queue.getSourceStartPositionUs());
332            }
333        }
334        mLastBufferedPositionUs = positionUs;
335    }
336
337    @Override
338    public synchronized long getBufferedPositionUs() {
339        Long result = null;
340        for (CachedSampleQueue queue : mPlayingSampleQueues) {
341            if (queue == null) {
342                continue;
343            }
344            Long bufferedPositionUs = queue.getEndPositionUs();
345            if (bufferedPositionUs == null) {
346                continue;
347            }
348            if (result == null || result > bufferedPositionUs) {
349                result = bufferedPositionUs;
350            }
351        }
352        if (result == null) {
353            return mLastBufferedPositionUs;
354        } else {
355            return (mLastBufferedPositionUs = result);
356        }
357    }
358
359    @Override
360    public synchronized boolean continueBuffering(long positionUs) {
361        boolean hasSamples = true;
362        mCurrentPlaybackPositionUs = positionUs;
363        for (CachedSampleQueue queue : mPlayingSampleQueues) {
364            if (queue == null) {
365                continue;
366            }
367            queue.maybeReadSample();
368            if (queue.isEmpty()) {
369                hasSamples = false;
370            }
371        }
372        return hasSamples;
373    }
374
375    @Override
376    public synchronized void release() {
377        if (mSampleCaches == null) {
378            return;
379        }
380        if (mCacheReason == CACHE_REASON_RECORDED_PLAYBACK) {
381            mCacheManager.close();
382        }
383        for (int i = 0; i < mTrackCount; ++i) {
384            if (mCacheReason != CACHE_REASON_RECORDED_PLAYBACK) {
385                mSampleCaches[i].finishWrite(null);
386            }
387            mCacheManager.unregisterEvictListener(getTrackId(i));
388        }
389        if (mCacheReason == CACHE_REASON_RECORDING && mTrackCount > 0) {
390            // Saves meta information for recording.
391            Pair<String, android.media.MediaFormat> audio = null, video = null;
392            for (int i = 0; i < mTrackCount; ++i) {
393                MediaFormat mediaFormat = mMediaFormats.get(i);
394                String mime = mediaFormat.getString(MediaFormat.KEY_MIME);
395                mediaFormat.setLong(android.media.MediaFormat.KEY_DURATION, mCacheDurationUs);
396                if (MimeTypes.isAudio(mime)) {
397                    audio = new Pair<>(getTrackId(i), mediaFormat);
398                }
399                else if (MimeTypes.isVideo(mime)) {
400                    video = new Pair<>(getTrackId(i), mediaFormat);
401                }
402            }
403            mCacheManager.writeMetaFiles(audio, video);
404        }
405
406        for (int i = 0; i < mTrackCount; ++i) {
407            mCacheManager.clearTrack(getTrackId(i));
408        }
409    }
410
411    // CacheEvictListener
412    @Override
413    public void onCacheEvicted(String id, long createdTimeMs) {
414        if (mCacheListener != null) {
415            mCacheListener.onCacheStartTimeChanged(
416                    createdTimeMs + TimeUnit.MICROSECONDS.toMillis(CHUNK_DURATION_US));
417        }
418    }
419}
420