/* * 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.usbtuner.exoplayer.cache; import android.media.MediaCodec; import android.media.MediaFormat; import android.os.ConditionVariable; import android.support.annotation.IntDef; import android.util.Log; import android.util.Pair; import com.google.android.exoplayer.C; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.util.MimeTypes; import com.android.usbtuner.tvinput.PlaybackCacheListener; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; import java.util.concurrent.TimeUnit; import junit.framework.Assert; /** * Handles I/O between {@link com.android.usbtuner.exoplayer.SampleExtractor} and * {@link CacheManager}.Reads & writes samples from/to {@link SampleCache} which is backed * by physical storage. */ public class RecordingSampleBuffer implements CacheManager.SampleBuffer, CacheManager.EvictListener { private static final String TAG = "RecordingSampleBuffer"; private static final boolean DEBUG = false; @IntDef({CACHE_REASON_LIVE_PLAYBACK, CACHE_REASON_RECORDED_PLAYBACK, CACHE_REASON_RECORDING}) @Retention(RetentionPolicy.SOURCE) public @interface CacheReason {} /** * A cache reason for live-stream playback. */ public static final int CACHE_REASON_LIVE_PLAYBACK = 0; /** * A cache reason for playback of a recorded program. */ public static final int CACHE_REASON_RECORDED_PLAYBACK = 1; /** * A cache reason for recording a program. */ public static final int CACHE_REASON_RECORDING = 2; private static final long CACHE_WRITE_TIMEOUT_MS = 10 * 1000; // 10 seconds private static final long CHUNK_DURATION_US = TimeUnit.MILLISECONDS.toMicros(500); private static final long LIVE_THRESHOLD_US = TimeUnit.SECONDS.toMicros(1); private final CacheManager mCacheManager; private final PlaybackCacheListener mCacheListener; private final int mCacheReason; private int mTrackCount; private List mIds; private List mMediaFormats; private volatile long mCacheDurationUs = 0; private long[] mCacheEndPositionUs; // SampleCache to append the latest live sample. private SampleCache[] mSampleCaches; private CachedSampleQueue[] mPlayingSampleQueues; private final SamplePool mSamplePool = new SamplePool(); private long mLastBufferedPositionUs = C.UNKNOWN_TIME_US; private long mCurrentPlaybackPositionUs = 0; private boolean mEos = false; private class CachedSampleQueue extends SampleQueue { private SampleCache mCache = null; public CachedSampleQueue(SamplePool samplePool) { super(samplePool); } public void setSource(SampleCache newCache) { for (SampleCache cache = mCache; cache != null; cache = cache.getNext()) { cache.clear(); cache.close(); } mCache = newCache; for (SampleCache cache = mCache; cache != null; cache = cache.getNext()) { cache.resetRead(); } } public boolean maybeReadSample() { if (isDurationGreaterThan(CHUNK_DURATION_US)) { return false; } SampleHolder sample = mCache.maybeReadSample(); if (sample == null) { if (!mCache.canReadMore() && mCache.getNext() != null) { mCache.clear(); mCache.close(); mCache = mCache.getNext(); mCache.resetRead(); return maybeReadSample(); } else { if (mCacheReason == CACHE_REASON_RECORDED_PLAYBACK && !mCache.canReadMore() && mCache.getNext() == null) { // At the end of the recorded playback. setEos(); } return false; } } else { queueSample(sample); return true; } } public int dequeueSample(SampleHolder sample) { maybeReadSample(); return super.dequeueSample(sample); } @Override public void clear() { super.clear(); for (SampleCache cache = mCache; cache != null; cache = cache.getNext()) { cache.clear(); cache.close(); } mCache = null; } public long getSourceStartPositionUs() { return mCache == null ? -1 : mCache.getStartPositionUs(); } } /** * Creates {@link com.android.usbtuner.exoplayer.cache.CacheManager.SampleBuffer} with * cached I/O backed by physical storage (e.g. trickplay,recording,recorded-playback). * * @param cacheManager * @param cacheListener * @param enableTrickplay {@code true} when trickplay should be enabled * @param cacheReason the reason for caching samples {@link RecordingSampleBuffer.CacheReason} */ public RecordingSampleBuffer(CacheManager cacheManager, PlaybackCacheListener cacheListener, boolean enableTrickplay, @CacheReason int cacheReason) { mCacheManager = cacheManager; mCacheListener = cacheListener; if (cacheListener != null) { cacheListener.onCacheStateChanged(enableTrickplay); } mCacheReason = cacheReason; } private String getTrackId(int index) { return mIds.get(index); } @Override public synchronized void init(List ids, List mediaFormats) throws IOException { mTrackCount = ids.size(); if (mTrackCount <= 0) { throw new IOException("No tracks to initialize"); } mIds = ids; if (mCacheReason == CACHE_REASON_RECORDING && mediaFormats == null) { throw new IOException("MediaFormat is not provided."); } mMediaFormats = mediaFormats; mSampleCaches = new SampleCache[mTrackCount]; mPlayingSampleQueues = new CachedSampleQueue[mTrackCount]; mCacheEndPositionUs = new long[mTrackCount]; for (int i = 0; i < mTrackCount; i++) { if (mCacheReason != CACHE_REASON_RECORDED_PLAYBACK) { mSampleCaches[i] = mCacheManager.createNewWriteFile(getTrackId(i), 0, mSamplePool); mPlayingSampleQueues[i] = null; mCacheEndPositionUs[i] = CHUNK_DURATION_US; } else { mCacheManager.loadTrackFormStorage(mIds.get(i), mSamplePool); } } } private boolean isLiveLocked(long positionUs) { Long livePositionUs = null; for (SampleCache cache : mSampleCaches) { if (livePositionUs == null || livePositionUs < cache.getEndPositionUs()) { livePositionUs = cache.getEndPositionUs(); } } return (livePositionUs == null || Math.abs(livePositionUs - positionUs) < LIVE_THRESHOLD_US); } private void seekIndividualTrackLocked(int index, long positionUs, boolean isLive) { CachedSampleQueue queue = mPlayingSampleQueues[index]; if (queue == null) { return; } queue.clear(); if (isLive) { queue.setSource(mSampleCaches[index]); } else { queue.setSource(mCacheManager.getReadFile(getTrackId(index), positionUs)); } queue.maybeReadSample(); } @Override public synchronized void selectTrack(int index) { if (mPlayingSampleQueues[index] == null) { String trackId = getTrackId(index); mPlayingSampleQueues[index] = new CachedSampleQueue(mSamplePool); mCacheManager.registerEvictListener(trackId, this); seekIndividualTrackLocked(index, mCurrentPlaybackPositionUs, mCacheReason != CACHE_REASON_RECORDED_PLAYBACK && isLiveLocked( mCurrentPlaybackPositionUs)); mPlayingSampleQueues[index].maybeReadSample(); } } @Override public synchronized void deselectTrack(int index) { if (mPlayingSampleQueues[index] != null) { mPlayingSampleQueues[index].clear(); mPlayingSampleQueues[index] = null; mCacheManager.unregisterEvictListener(getTrackId(index)); } } @Override public void writeSample(int index, SampleHolder sample, ConditionVariable conditionVariable) throws IOException { synchronized (this) { SampleCache cache = mSampleCaches[index]; if ((sample.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) { if (sample.timeUs > mCacheDurationUs) { mCacheDurationUs = sample.timeUs; } if (sample.timeUs >= mCacheEndPositionUs[index]) { try { SampleCache nextCache = mCacheManager.createNewWriteFile( getTrackId(index), mCacheEndPositionUs[index], mSamplePool); cache.finishWrite(nextCache); mSampleCaches[index] = cache = nextCache; mCacheEndPositionUs[index] = ((sample.timeUs / CHUNK_DURATION_US) + 1) * CHUNK_DURATION_US; } catch (IOException e) { cache.finishWrite(null); throw e; } } } cache.writeSample(sample, conditionVariable); } if (!conditionVariable.block(CACHE_WRITE_TIMEOUT_MS)) { Log.e(TAG, "Error: Serious delay on writing cache"); conditionVariable.block(); } } @Override public boolean isWriteSpeedSlow(int sampleSize, long writeDurationNs) { if (mCacheReason == CACHE_REASON_RECORDED_PLAYBACK) { return false; } mCacheManager.addWriteStat(sampleSize, writeDurationNs); return mCacheManager.isWriteSlow(); } @Override public void handleWriteSpeedSlow() { Log.w(TAG, "Disk is too slow for trickplay. Disable trickplay."); mCacheManager.disable(); mCacheListener.onDiskTooSlow(); } @Override public synchronized void setEos() { mEos = true; } private synchronized boolean reachedEos() { return mEos; } @Override public synchronized int readSample(int track, SampleHolder sampleHolder) { CachedSampleQueue queue = mPlayingSampleQueues[track]; Assert.assertNotNull(queue); queue.maybeReadSample(); int result = queue.dequeueSample(sampleHolder); if (result != SampleSource.SAMPLE_READ && reachedEos()) { return SampleSource.END_OF_STREAM; } return result; } @Override public synchronized void seekTo(long positionUs) { boolean isLive = mCacheReason != CACHE_REASON_RECORDED_PLAYBACK && isLiveLocked(positionUs); // Seek video track first for (int i = 0; i < mPlayingSampleQueues.length; ++i) { CachedSampleQueue queue = mPlayingSampleQueues[i]; if (queue == null) { continue; } seekIndividualTrackLocked(i, positionUs, isLive); if (DEBUG) { Log.d(TAG, "start time = " + queue.getSourceStartPositionUs()); } } mLastBufferedPositionUs = positionUs; } @Override public synchronized long getBufferedPositionUs() { Long result = null; for (CachedSampleQueue queue : mPlayingSampleQueues) { if (queue == null) { continue; } Long bufferedPositionUs = queue.getEndPositionUs(); if (bufferedPositionUs == null) { continue; } if (result == null || result > bufferedPositionUs) { result = bufferedPositionUs; } } if (result == null) { return mLastBufferedPositionUs; } else { return (mLastBufferedPositionUs = result); } } @Override public synchronized boolean continueBuffering(long positionUs) { boolean hasSamples = true; mCurrentPlaybackPositionUs = positionUs; for (CachedSampleQueue queue : mPlayingSampleQueues) { if (queue == null) { continue; } queue.maybeReadSample(); if (queue.isEmpty()) { hasSamples = false; } } return hasSamples; } @Override public synchronized void release() { if (mSampleCaches == null) { return; } if (mCacheReason == CACHE_REASON_RECORDED_PLAYBACK) { mCacheManager.close(); } for (int i = 0; i < mTrackCount; ++i) { if (mCacheReason != CACHE_REASON_RECORDED_PLAYBACK) { mSampleCaches[i].finishWrite(null); } mCacheManager.unregisterEvictListener(getTrackId(i)); } if (mCacheReason == CACHE_REASON_RECORDING && mTrackCount > 0) { // Saves meta information for recording. Pair audio = null, video = null; for (int i = 0; i < mTrackCount; ++i) { MediaFormat mediaFormat = mMediaFormats.get(i); String mime = mediaFormat.getString(MediaFormat.KEY_MIME); mediaFormat.setLong(android.media.MediaFormat.KEY_DURATION, mCacheDurationUs); if (MimeTypes.isAudio(mime)) { audio = new Pair<>(getTrackId(i), mediaFormat); } else if (MimeTypes.isVideo(mime)) { video = new Pair<>(getTrackId(i), mediaFormat); } } mCacheManager.writeMetaFiles(audio, video); } for (int i = 0; i < mTrackCount; ++i) { mCacheManager.clearTrack(getTrackId(i)); } } // CacheEvictListener @Override public void onCacheEvicted(String id, long createdTimeMs) { if (mCacheListener != null) { mCacheListener.onCacheStartTimeChanged( createdTimeMs + TimeUnit.MICROSECONDS.toMillis(CHUNK_DURATION_US)); } } }