diff options
Diffstat (limited to 'src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java')
-rw-r--r-- | src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java | 644 |
1 files changed, 644 insertions, 0 deletions
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java b/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java new file mode 100644 index 00000000..eb596e93 --- /dev/null +++ b/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java @@ -0,0 +1,644 @@ +/* + * Copyright (C) 2015 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.exoplayer.buffer; + +import android.media.MediaFormat; +import android.os.ConditionVariable; +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; +import android.util.ArrayMap; +import android.util.Log; +import android.util.Pair; + +import com.google.android.exoplayer.SampleHolder; +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.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * Manages {@link SampleChunk} objects. + * <p> + * The buffer manager can be disabled, while running, if the write throughput to the associated + * external storage is detected to be lower than a threshold {@code MINIMUM_DISK_WRITE_SPEED_MBPS}". + * This leads to restarting playback flow. + */ +public class BufferManager { + private static final String TAG = "BufferManager"; + private static final boolean DEBUG = false; + + // Constants for the disk write speed checking + private static final long MINIMUM_WRITE_SIZE_FOR_SPEED_CHECK = + 10L * 1024 * 1024; // Checks for every 10M disk write + private static final int MINIMUM_SAMPLE_SIZE_FOR_SPEED_CHECK = 15 * 1024; + private static final int MAXIMUM_SPEED_CHECK_COUNT = 5; // Checks only 5 times + private static final int MINIMUM_DISK_WRITE_SPEED_MBPS = 3; // 3 Megabytes per second + + 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, Long> mStartPositionMap = new ArrayMap<>(); + private final Map<String, ChunkEvictedListener> mEvictListeners = new ArrayMap<>(); + private final StorageManager mStorageManager; + private long mBufferSize = 0; + private final EvictChunkQueueMap mPendingDelete = new EvictChunkQueueMap(); + private final SampleChunk.ChunkCallback mChunkCallback = new SampleChunk.ChunkCallback() { + @Override + public void onChunkWrite(SampleChunk chunk) { + mBufferSize += chunk.getSize(); + } + + @Override + public void onChunkDelete(SampleChunk chunk) { + mBufferSize -= chunk.getSize(); + } + }; + + 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); + } + /** + * Handles I/O + * between BufferManager and {@link SampleExtractor}. + */ + public interface SampleBuffer { + + /** + * Initializes SampleBuffer. + * @param Ids track identifiers for storage read/write. + * @param mediaFormats meta-data for each track. + * @throws IOException + */ + void init(@NonNull List<String> Ids, + @NonNull List<com.google.android.exoplayer.MediaFormat> mediaFormats) + throws IOException; + + /** + * Selects the track {@code index} for reading sample data. + */ + void selectTrack(int index); + + /** + * Deselects the track at {@code index}, + * so that no more samples will be read from the track. + */ + void deselectTrack(int index); + + /** + * Writes sample to storage. + * + * @param index track index + * @param sample sample to write at storage + * @param conditionVariable notifies the completion of writing sample. + * @throws IOException + */ + void writeSample(int index, SampleHolder sample, ConditionVariable conditionVariable) + throws IOException; + + /** + * Checks whether storage write speed is slow. + */ + boolean isWriteSpeedSlow(int sampleSize, long writeDurationNs); + + /** + * Handles when write speed is slow. + * @throws IOException + */ + void handleWriteSpeedSlow() throws IOException; + + /** + * Sets the flag when EoS was reached. + */ + void setEos(); + + /** + * Reads the next sample in the track at index {@code track} into {@code sampleHolder}, + * returning {@link com.google.android.exoplayer.SampleSource#SAMPLE_READ} + * if it is available. + * If the next sample is not available, + * returns {@link com.google.android.exoplayer.SampleSource#NOTHING_READ}. + */ + int readSample(int index, SampleHolder outSample); + + /** + * Seeks to the specified time in microseconds. + */ + void seekTo(long positionUs); + + /** + * Returns an estimate of the position up to which data is buffered. + */ + long getBufferedPositionUs(); + + /** + * Returns whether there is buffered data. + */ + boolean continueBuffering(long positionUs); + + /** + * Cleans up and releases everything. + * @throws IOException + */ + void release() throws IOException; + } + + /** + * Storage configuration and policy manager for {@link BufferManager} + */ + public interface StorageManager { + + /** + * Provides eligible storage directory for {@link BufferManager}. + * + * @return a directory to save buffer(chunks) and meta files + */ + 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 + */ + boolean isPersistent(); + + /** + * Informs whether the storage usage exceeds pre-determined size. + * + * @param bufferSize the current total usage of Storage in bytes. + * @param pendingDelete the current storage usage which will be deleted in near future by + * bytes + * @return {@code true} if it reached pre-determined max size + */ + boolean reachedStorageMax(long bufferSize, long pendingDelete); + + /** + * Informs whether the storage has enough remained space. + * + * @param pendingDelete the current storage usage which will be deleted in near future by + * bytes + * @return {@code true} if it has enough space + */ + boolean hasEnoughBuffer(long pendingDelete); + + /** + * 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 + */ + Pair<String, MediaFormat> readTrackInfoFile(boolean isAudio) throws IOException; + + /** + * Reads sample indexes 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; + + /** + * Writes track information to storage. + * + * @param trackId track name + * @param format {@link android.media.MediaFormat} of the track + * @param isAudio {@code true} if it is for audio track + * @throws IOException + */ + void writeTrackInfoFile(String trackId, MediaFormat format, boolean isAudio) + throws IOException; + + /** + * Writes index file to storage. + * + * @param trackName track name + * @param index {@link SampleChunk} container + * @throws IOException + */ + void writeIndexFile(String trackName, SortedMap<Long, SampleChunk> index) + throws IOException; + } + + private static class EvictChunkQueueMap { + private final Map<String, LinkedList<SampleChunk>> mEvictMap = new ArrayMap<>(); + private long mSize; + + private void init(String key) { + mEvictMap.put(key, new LinkedList<>()); + } + + private void add(String key, SampleChunk chunk) { + LinkedList<SampleChunk> queue = mEvictMap.get(key); + if (queue != null) { + mSize += chunk.getSize(); + queue.add(chunk); + } + } + + private SampleChunk poll(String key, long startPositionUs) { + LinkedList<SampleChunk> queue = mEvictMap.get(key); + if (queue != null) { + SampleChunk chunk = queue.peek(); + if (chunk != null && chunk.getStartPositionUs() < startPositionUs) { + mSize -= chunk.getSize(); + return queue.poll(); + } + } + return null; + } + + private long getSize() { + return mSize; + } + + private void release() { + for (Map.Entry<String, LinkedList<SampleChunk>> entry : mEvictMap.entrySet()) { + for (SampleChunk chunk : entry.getValue()) { + SampleChunk.IoState.release(chunk, true); + } + } + mEvictMap.clear(); + mSize = 0; + } + } + + public BufferManager(StorageManager storageManager) { + this(storageManager, new SampleChunk.SampleChunkCreator()); + } + + public BufferManager(StorageManager storageManager, + SampleChunk.SampleChunkCreator sampleChunkCreator) { + mStorageManager = storageManager; + mSampleChunkCreator = sampleChunkCreator; + clearBuffer(true); + } + + public void registerChunkEvictedListener(String id, ChunkEvictedListener listener) { + mEvictListeners.put(id, listener); + } + + public void unregisterChunkEvictedListener(String id) { + 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. + * + * @param id the name of the track + * @param positionUs starting position of the {@link SampleChunk} in micro seconds. + * @param samplePool {@link SamplePool} for the fast creation of samples. + * @return returns the created {@link SampleChunk}. + * @throws IOException + */ + public SampleChunk createNewWriteFile(String id, long positionUs, + SamplePool samplePool) throws IOException { + if (!maybeEvictChunk()) { + throw new IOException("Not enough storage space"); + } + SortedMap<Long, SampleChunk> 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; + } + + /** + * Loads a track using {@link BufferManager.StorageManager}. + * + * @param trackId the name of the track. + * @param samplePool {@link SamplePool} for the fast creation of samples. + * @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; + + SortedMap<Long, SampleChunk> map = mChunkMap.get(trackId); + if (map == null) { + map = new TreeMap<>(); + mChunkMap.put(trackId, map); + mStartPositionMap.put(trackId, startPositionUs); + 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); + } + } + + /** + * Finds a {@link SampleChunk} for the specified track name and the position. + * + * @param id the name of the track. + * @param positionUs the position. + * @return returns the found {@link SampleChunk}. + */ + public SampleChunk getReadFile(String id, long positionUs) { + SortedMap<Long, SampleChunk> map = mChunkMap.get(id); + if (map == null) { + return null; + } + SampleChunk sampleChunk; + SortedMap<Long, SampleChunk> headMap = map.headMap(positionUs + 1); + if (!headMap.isEmpty()) { + sampleChunk = headMap.get(headMap.lastKey()); + } else { + sampleChunk = map.get(map.firstKey()); + } + return sampleChunk; + } + + /** + * Evicts chunks which are ready to be evicted for the specified track + * + * @param id the specified track + * @param earlierThanPositionUs the start position of the {@link SampleChunk} + * should be earlier than + */ + public void evictChunks(String id, long earlierThanPositionUs) { + SampleChunk chunk = null; + while ((chunk = mPendingDelete.poll(id, earlierThanPositionUs)) != null) { + SampleChunk.IoState.release(chunk, !mStorageManager.isPersistent()) ; + } + } + + /** + * Returns the start position of the specified track in micro seconds. + * + * @param id the specified track + */ + public long getStartPositionUs(String id) { + Long ret = mStartPositionMap.get(id); + return ret == null ? 0 : ret; + } + + private boolean maybeEvictChunk() { + long pendingDelete = mPendingDelete.getSize(); + while (mStorageManager.reachedStorageMax(mBufferSize, pendingDelete) + || !mStorageManager.hasEnoughBuffer(pendingDelete)) { + if (mStorageManager.isPersistent()) { + // Since chunks are persistent, we cannot evict chunks. + return false; + } + SortedMap<Long, SampleChunk> earliestChunkMap = null; + SampleChunk earliestChunk = null; + String earliestChunkId = null; + for (Map.Entry<String, SortedMap<Long, SampleChunk>> entry : mChunkMap.entrySet()) { + SortedMap<Long, SampleChunk> map = entry.getValue(); + if (map.isEmpty()) { + continue; + } + SampleChunk chunk = map.get(map.firstKey()); + if (earliestChunk == null + || chunk.getCreatedTimeMs() < earliestChunk.getCreatedTimeMs()) { + earliestChunkMap = map; + earliestChunk = chunk; + earliestChunkId = entry.getKey(); + } + } + if (earliestChunk == null) { + break; + } + mPendingDelete.add(earliestChunkId, earliestChunk); + earliestChunkMap.remove(earliestChunk.getStartPositionUs()); + if (DEBUG) { + Log.d(TAG, String.format("bufferSize = %d; pendingDelete = %b; " + + "earliestChunk size = %d; %s@%d (%s)", + mBufferSize, pendingDelete, earliestChunk.getSize(), earliestChunkId, + earliestChunk.getStartPositionUs(), + Utils.toIsoDateTimeString(earliestChunk.getCreatedTimeMs()))); + } + ChunkEvictedListener listener = mEvictListeners.get(earliestChunkId); + if (listener != null) { + listener.onChunkEvicted(earliestChunkId, earliestChunk.getCreatedTimeMs()); + } + pendingDelete = mPendingDelete.getSize(); + } + for (Map.Entry<String, SortedMap<Long, SampleChunk>> entry : mChunkMap.entrySet()) { + SortedMap<Long, SampleChunk> map = entry.getValue(); + if (map.isEmpty()) { + continue; + } + mStartPositionMap.put(entry.getKey(), map.firstKey()); + } + return true; + } + + /** + * Reads track information which includes {@link MediaFormat}. + * + * @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. + } + try { + trackInfos.add(mStorageManager.readTrackInfoFile(true)); + } catch (FileNotFoundException e) { + // See above catch block. + } + return trackInfos; + } + + /** + * Writes track information and index information for all tracks. + * + * @param audio audio information. + * @param video video information. + * @throws IOException + */ + public void writeMetaFiles(Pair<String, MediaFormat> audio, Pair<String, MediaFormat> video) + 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"); + } + 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"); + } + 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()); + } + } + mChunkMap.clear(); + if (mClosed) { + clearBuffer(!mStorageManager.isPersistent()); + } + } + + private void resetWriteStat(float writeBandwidth) { + mWriteBandwidth = writeBandwidth; + mTotalWriteSize = 0; + mTotalWriteTimeNs = 0; + } + + /** + * Adds a disk write sample size to calculate the average disk write bandwidth. + */ + public void addWriteStat(long size, long timeNs) { + if (size >= mMinSampleSizeForSpeedCheck) { + mTotalWriteSize += size; + mTotalWriteTimeNs += timeNs; + } + } + + /** + * Returns if the average disk write bandwidth is slower than + * threshold {@code MINIMUM_DISK_WRITE_SPEED_MBPS}. + */ + public boolean isWriteSlow() { + if (mTotalWriteSize < MINIMUM_WRITE_SIZE_FOR_SPEED_CHECK) { + return false; + } + + // Checks write speed for only MAXIMUM_SPEED_CHECK_COUNT times to ignore outliers + // by temporary system overloading during the playback. + if (mSpeedCheckCount > MAXIMUM_SPEED_CHECK_COUNT) { + return false; + } + mSpeedCheckCount++; + float megabytePerSecond = calculateWriteBandwidth(); + resetWriteStat(megabytePerSecond); + if (DEBUG) { + Log.d(TAG, "Measured disk write performance: " + megabytePerSecond + "MBps"); + } + return megabytePerSecond < MINIMUM_DISK_WRITE_SPEED_MBPS; + } + + /** + * Returns recent write bandwidth in MBps. If recent bandwidth is not available, + * returns {float -1.0f}. + */ + public float getWriteBandwidth() { + return mWriteBandwidth == 0.0f ? -1.0f : mWriteBandwidth; + } + + private float calculateWriteBandwidth() { + if (mTotalWriteTimeNs == 0) { + return -1; + } + return ((float) mTotalWriteSize * 1000 / mTotalWriteTimeNs); + } + + /** + * 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. + */ + @VisibleForTesting + public boolean hasSpeedCheckDone() { + return mSpeedCheckCount > 0; + } + + /** + * Sets minimum sample size for write speed check. + * @param sampleSize minimum sample size for write speed check. + */ + @VisibleForTesting + public void setMinimumSampleSizeForSpeedCheck(int sampleSize) { + mMinSampleSizeForSpeedCheck = sampleSize; + } +} |