Home | History | Annotate | Download | only in buffer
      1 /*
      2  * Copyright (C) 2015 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 
     17 package com.android.tv.tuner.exoplayer.buffer;
     18 
     19 import android.media.MediaFormat;
     20 import android.os.ConditionVariable;
     21 import android.support.annotation.NonNull;
     22 import android.support.annotation.VisibleForTesting;
     23 import android.util.ArrayMap;
     24 import android.util.Log;
     25 import android.util.Pair;
     26 
     27 import com.google.android.exoplayer.SampleHolder;
     28 import com.android.tv.common.SoftPreconditions;
     29 import com.android.tv.tuner.exoplayer.SampleExtractor;
     30 import com.android.tv.util.Utils;
     31 
     32 import java.io.File;
     33 import java.io.IOException;
     34 import java.util.ArrayList;
     35 import java.util.ConcurrentModificationException;
     36 import java.util.LinkedList;
     37 import java.util.List;
     38 import java.util.Locale;
     39 import java.util.Map;
     40 import java.util.SortedMap;
     41 import java.util.TreeMap;
     42 
     43 /**
     44  * Manages {@link SampleChunk} objects.
     45  * <p>
     46  * The buffer manager can be disabled, while running, if the write throughput to the associated
     47  * external storage is detected to be lower than a threshold {@code MINIMUM_DISK_WRITE_SPEED_MBPS}".
     48  * This leads to restarting playback flow.
     49  */
     50 public class BufferManager {
     51     private static final String TAG = "BufferManager";
     52     private static final boolean DEBUG = false;
     53 
     54     // Constants for the disk write speed checking
     55     private static final long MINIMUM_WRITE_SIZE_FOR_SPEED_CHECK =
     56             10L * 1024 * 1024;  // Checks for every 10M disk write
     57     private static final int MINIMUM_SAMPLE_SIZE_FOR_SPEED_CHECK = 15 * 1024;
     58     private static final int MAXIMUM_SPEED_CHECK_COUNT = 5;  // Checks only 5 times
     59     private static final int MINIMUM_DISK_WRITE_SPEED_MBPS = 3;  // 3 Megabytes per second
     60 
     61     private final SampleChunk.SampleChunkCreator mSampleChunkCreator;
     62     // Maps from track name to a map which maps from starting position to {@link SampleChunk}.
     63     private final Map<String, SortedMap<Long, Pair<SampleChunk, Integer>>> mChunkMap =
     64             new ArrayMap<>();
     65     private final Map<String, Long> mStartPositionMap = new ArrayMap<>();
     66     private final Map<String, ChunkEvictedListener> mEvictListeners = new ArrayMap<>();
     67     private final StorageManager mStorageManager;
     68     private long mBufferSize = 0;
     69     private final EvictChunkQueueMap mPendingDelete = new EvictChunkQueueMap();
     70     private final SampleChunk.ChunkCallback mChunkCallback = new SampleChunk.ChunkCallback() {
     71         @Override
     72         public void onChunkWrite(SampleChunk chunk) {
     73             mBufferSize += chunk.getSize();
     74         }
     75 
     76         @Override
     77         public void onChunkDelete(SampleChunk chunk) {
     78             mBufferSize -= chunk.getSize();
     79         }
     80     };
     81 
     82     private int mMinSampleSizeForSpeedCheck = MINIMUM_SAMPLE_SIZE_FOR_SPEED_CHECK;
     83     private long mTotalWriteSize;
     84     private long mTotalWriteTimeNs;
     85     private float mWriteBandwidth = 0.0f;
     86     private volatile int mSpeedCheckCount;
     87 
     88     public interface ChunkEvictedListener {
     89         void onChunkEvicted(String id, long createdTimeMs);
     90     }
     91     /**
     92      * Handles I/O
     93      * between BufferManager and {@link SampleExtractor}.
     94      */
     95     public interface SampleBuffer {
     96 
     97         /**
     98          * Initializes SampleBuffer.
     99          * @param Ids track identifiers for storage read/write.
    100          * @param mediaFormats meta-data for each track.
    101          * @throws IOException
    102          */
    103         void init(@NonNull List<String> Ids,
    104                   @NonNull List<com.google.android.exoplayer.MediaFormat> mediaFormats)
    105                 throws IOException;
    106 
    107         /**
    108          * Selects the track {@code index} for reading sample data.
    109          */
    110         void selectTrack(int index);
    111 
    112         /**
    113          * Deselects the track at {@code index},
    114          * so that no more samples will be read from the track.
    115          */
    116         void deselectTrack(int index);
    117 
    118         /**
    119          * Writes sample to storage.
    120          *
    121          * @param index track index
    122          * @param sample sample to write at storage
    123          * @param conditionVariable notifies the completion of writing sample.
    124          * @throws IOException
    125          */
    126         void writeSample(int index, SampleHolder sample, ConditionVariable conditionVariable)
    127                 throws IOException;
    128 
    129         /**
    130          * Checks whether storage write speed is slow.
    131          */
    132         boolean isWriteSpeedSlow(int sampleSize, long writeDurationNs);
    133 
    134         /**
    135          * Handles when write speed is slow.
    136          * @throws IOException
    137          */
    138         void handleWriteSpeedSlow() throws IOException;
    139 
    140         /**
    141          * Sets the flag when EoS was reached.
    142          */
    143         void setEos();
    144 
    145         /**
    146          * Reads the next sample in the track at index {@code track} into {@code sampleHolder},
    147          * returning {@link com.google.android.exoplayer.SampleSource#SAMPLE_READ}
    148          * if it is available.
    149          * If the next sample is not available,
    150          * returns {@link com.google.android.exoplayer.SampleSource#NOTHING_READ}.
    151          */
    152         int readSample(int index, SampleHolder outSample);
    153 
    154         /**
    155          * Seeks to the specified time in microseconds.
    156          */
    157         void seekTo(long positionUs);
    158 
    159         /**
    160          * Returns an estimate of the position up to which data is buffered.
    161          */
    162         long getBufferedPositionUs();
    163 
    164         /**
    165          * Returns whether there is buffered data.
    166          */
    167         boolean continueBuffering(long positionUs);
    168 
    169         /**
    170          * Cleans up and releases everything.
    171          * @throws IOException
    172          */
    173         void release() throws IOException;
    174     }
    175 
    176     /**
    177      * A Track format which will be loaded and saved from the permanent storage for recordings.
    178      */
    179     public static class TrackFormat {
    180 
    181         /**
    182          * The track id for the specified track. The track id will be used as a track identifier
    183          * for recordings.
    184          */
    185         public final String trackId;
    186 
    187         /**
    188          * The {@link MediaFormat} for the specified track.
    189          */
    190         public final MediaFormat format;
    191 
    192         /**
    193          * Creates TrackFormat.
    194          * @param trackId
    195          * @param format
    196          */
    197         public TrackFormat(String trackId, MediaFormat format) {
    198             this.trackId = trackId;
    199             this.format = format;
    200         }
    201     }
    202 
    203     /**
    204      * A Holder for a sample position which will be loaded from the index file for recordings.
    205      */
    206     public static class PositionHolder {
    207 
    208         /**
    209          * The current sample position in microseconds.
    210          * The position is identical to the PTS(presentation time stamp) of the sample.
    211          */
    212         public final long positionUs;
    213 
    214         /**
    215          * Base sample position for the current {@link SampleChunk}.
    216          */
    217         public final long basePositionUs;
    218 
    219         /**
    220          * The file offset for the current sample in the current {@link SampleChunk}.
    221          */
    222         public final int offset;
    223 
    224         /**
    225          * Creates a holder for a specific position in the recording.
    226          * @param positionUs
    227          * @param offset
    228          */
    229         public PositionHolder(long positionUs, long basePositionUs, int offset) {
    230             this.positionUs = positionUs;
    231             this.basePositionUs = basePositionUs;
    232             this.offset = offset;
    233         }
    234     }
    235 
    236     /**
    237      * Storage configuration and policy manager for {@link BufferManager}
    238      */
    239     public interface StorageManager {
    240 
    241         /**
    242          * Provides eligible storage directory for {@link BufferManager}.
    243          *
    244          * @return a directory to save buffer(chunks) and meta files
    245          */
    246         File getBufferDir();
    247 
    248         /**
    249          * Informs whether the storage is used for persistent use. (eg. dvr recording/play)
    250          *
    251          * @return {@code true} if stored files are persistent
    252          */
    253         boolean isPersistent();
    254 
    255         /**
    256          * Informs whether the storage usage exceeds pre-determined size.
    257          *
    258          * @param bufferSize the current total usage of Storage in bytes.
    259          * @param pendingDelete the current storage usage which will be deleted in near future by
    260          *                      bytes
    261          * @return {@code true} if it reached pre-determined max size
    262          */
    263         boolean reachedStorageMax(long bufferSize, long pendingDelete);
    264 
    265         /**
    266          * Informs whether the storage has enough remained space.
    267          *
    268          * @param pendingDelete the current storage usage which will be deleted in near future by
    269          *                      bytes
    270          * @return {@code true} if it has enough space
    271          */
    272         boolean hasEnoughBuffer(long pendingDelete);
    273 
    274         /**
    275          * Reads track name & {@link MediaFormat} from storage.
    276          *
    277          * @param isAudio {@code true} if it is for audio track
    278          * @return {@link List} of TrackFormat
    279          */
    280         List<TrackFormat> readTrackInfoFiles(boolean isAudio);
    281 
    282         /**
    283          * Reads key sample positions for each written sample from storage.
    284          *
    285          * @param trackId track name
    286          * @return indexes of the specified track
    287          * @throws IOException
    288          */
    289         ArrayList<PositionHolder> readIndexFile(String trackId) throws IOException;
    290 
    291         /**
    292          * Writes track information to storage.
    293          *
    294          * @param formatList {@list List} of TrackFormat
    295          * @param isAudio {@code true} if it is for audio track
    296          * @throws IOException
    297          */
    298         void writeTrackInfoFiles(List<TrackFormat> formatList, boolean isAudio)
    299                 throws IOException;
    300 
    301         /**
    302          * Writes index file to storage.
    303          *
    304          * @param trackName track name
    305          * @param index {@link SampleChunk} container
    306          * @throws IOException
    307          */
    308         void writeIndexFile(String trackName, SortedMap<Long, Pair<SampleChunk, Integer>> index)
    309                 throws IOException;
    310     }
    311 
    312     private static class EvictChunkQueueMap {
    313         private final Map<String, LinkedList<SampleChunk>> mEvictMap = new ArrayMap<>();
    314         private long mSize;
    315 
    316         private void init(String key) {
    317             mEvictMap.put(key, new LinkedList<>());
    318         }
    319 
    320         private void add(String key, SampleChunk chunk) {
    321             LinkedList<SampleChunk> queue = mEvictMap.get(key);
    322             if (queue != null) {
    323                 mSize += chunk.getSize();
    324                 queue.add(chunk);
    325             }
    326         }
    327 
    328         private SampleChunk poll(String key, long startPositionUs) {
    329             LinkedList<SampleChunk> queue = mEvictMap.get(key);
    330             if (queue != null) {
    331                 SampleChunk chunk = queue.peek();
    332                 if (chunk != null && chunk.getStartPositionUs() < startPositionUs) {
    333                     mSize -= chunk.getSize();
    334                     return queue.poll();
    335                 }
    336             }
    337             return null;
    338         }
    339 
    340         private long getSize() {
    341             return mSize;
    342         }
    343 
    344         private void release() {
    345             for (Map.Entry<String, LinkedList<SampleChunk>> entry : mEvictMap.entrySet()) {
    346                 for (SampleChunk chunk : entry.getValue()) {
    347                     SampleChunk.IoState.release(chunk, true);
    348                 }
    349             }
    350             mEvictMap.clear();
    351             mSize = 0;
    352         }
    353     }
    354 
    355     public BufferManager(StorageManager storageManager) {
    356         this(storageManager, new SampleChunk.SampleChunkCreator());
    357     }
    358 
    359     public BufferManager(StorageManager storageManager,
    360             SampleChunk.SampleChunkCreator sampleChunkCreator) {
    361         mStorageManager = storageManager;
    362         mSampleChunkCreator = sampleChunkCreator;
    363     }
    364 
    365     public void registerChunkEvictedListener(String id, ChunkEvictedListener listener) {
    366         mEvictListeners.put(id, listener);
    367     }
    368 
    369     public void unregisterChunkEvictedListener(String id) {
    370         mEvictListeners.remove(id);
    371     }
    372 
    373     private static String getFileName(String id, long positionUs) {
    374         return String.format(Locale.ENGLISH, "%s_%016x.chunk", id, positionUs);
    375     }
    376 
    377     /**
    378      * Creates a new {@link SampleChunk} for caching samples if it is needed.
    379      *
    380      * @param id the name of the track
    381      * @param positionUs current position to write a sample in micro seconds.
    382      * @param samplePool {@link SamplePool} for the fast creation of samples.
    383      * @param currentChunk the current {@link SampleChunk} to write, {@code null} when to create
    384      *                     a new {@link SampleChunk}.
    385      * @param currentOffset the current offset to write.
    386      * @return returns the created {@link SampleChunk}.
    387      * @throws IOException
    388      */
    389     public SampleChunk createNewWriteFileIfNeeded(String id, long positionUs, SamplePool samplePool,
    390             SampleChunk currentChunk, int currentOffset) throws IOException {
    391         if (!maybeEvictChunk()) {
    392             throw new IOException("Not enough storage space");
    393         }
    394         SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(id);
    395         if (map == null) {
    396             map = new TreeMap<>();
    397             mChunkMap.put(id, map);
    398             mStartPositionMap.put(id, positionUs);
    399             mPendingDelete.init(id);
    400         }
    401         if (currentChunk == null) {
    402             File file = new File(mStorageManager.getBufferDir(), getFileName(id, positionUs));
    403             SampleChunk sampleChunk = mSampleChunkCreator
    404                     .createSampleChunk(samplePool, file, positionUs, mChunkCallback);
    405             map.put(positionUs, new Pair(sampleChunk, 0));
    406             return sampleChunk;
    407         } else {
    408             map.put(positionUs, new Pair(currentChunk, currentOffset));
    409             return null;
    410         }
    411     }
    412 
    413     /**
    414      * Loads a track using {@link BufferManager.StorageManager}.
    415      *
    416      * @param trackId the name of the track.
    417      * @param samplePool {@link SamplePool} for the fast creation of samples.
    418      * @throws IOException
    419      */
    420     public void loadTrackFromStorage(String trackId, SamplePool samplePool) throws IOException {
    421         ArrayList<PositionHolder> keyPositions = mStorageManager.readIndexFile(trackId);
    422         long startPositionUs = keyPositions.size() > 0 ? keyPositions.get(0).positionUs : 0;
    423 
    424         SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(trackId);
    425         if (map == null) {
    426             map = new TreeMap<>();
    427             mChunkMap.put(trackId, map);
    428             mStartPositionMap.put(trackId, startPositionUs);
    429             mPendingDelete.init(trackId);
    430         }
    431         SampleChunk chunk = null;
    432         long basePositionUs = -1;
    433         for (PositionHolder position: keyPositions) {
    434             if (position.basePositionUs != basePositionUs) {
    435                 chunk = mSampleChunkCreator.loadSampleChunkFromFile(samplePool,
    436                         mStorageManager.getBufferDir(), getFileName(trackId, position.positionUs),
    437                         position.positionUs, mChunkCallback, chunk);
    438                 basePositionUs = position.basePositionUs;
    439             }
    440             map.put(position.positionUs, new Pair(chunk, position.offset));
    441         }
    442     }
    443 
    444     /**
    445      * Finds a {@link SampleChunk} for the specified track name and the position.
    446      *
    447      * @param id the name of the track.
    448      * @param positionUs the position.
    449      * @return returns the found {@link SampleChunk}.
    450      */
    451     public Pair<SampleChunk, Integer> getReadFile(String id, long positionUs) {
    452         SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(id);
    453         if (map == null) {
    454             return null;
    455         }
    456         Pair<SampleChunk, Integer> ret;
    457         SortedMap<Long, Pair<SampleChunk, Integer>> headMap = map.headMap(positionUs + 1);
    458         if (!headMap.isEmpty()) {
    459             ret = headMap.get(headMap.lastKey());
    460         } else {
    461             ret = map.get(map.firstKey());
    462         }
    463         return ret;
    464     }
    465 
    466     /**
    467      * Evicts chunks which are ready to be evicted for the specified track
    468      *
    469      * @param id the specified track
    470      * @param earlierThanPositionUs the start position of the {@link SampleChunk}
    471      *                   should be earlier than
    472      */
    473     public void evictChunks(String id, long earlierThanPositionUs) {
    474         SampleChunk chunk = null;
    475         while ((chunk = mPendingDelete.poll(id, earlierThanPositionUs)) != null) {
    476             SampleChunk.IoState.release(chunk, !mStorageManager.isPersistent())  ;
    477         }
    478     }
    479 
    480     /**
    481      * Returns the start position of the specified track in micro seconds.
    482      *
    483      * @param id the specified track
    484      */
    485     public long getStartPositionUs(String id) {
    486         Long ret = mStartPositionMap.get(id);
    487         return ret == null ? 0 : ret;
    488     }
    489 
    490     private boolean maybeEvictChunk() {
    491         long pendingDelete = mPendingDelete.getSize();
    492         while (mStorageManager.reachedStorageMax(mBufferSize, pendingDelete)
    493                 || !mStorageManager.hasEnoughBuffer(pendingDelete)) {
    494             if (mStorageManager.isPersistent()) {
    495                 // Since chunks are persistent, we cannot evict chunks.
    496                 return false;
    497             }
    498             SortedMap<Long, Pair<SampleChunk, Integer>> earliestChunkMap = null;
    499             SampleChunk earliestChunk = null;
    500             String earliestChunkId = null;
    501             for (Map.Entry<String, SortedMap<Long, Pair<SampleChunk, Integer>>> entry :
    502                     mChunkMap.entrySet()) {
    503                 SortedMap<Long, Pair<SampleChunk, Integer>> map = entry.getValue();
    504                 if (map.isEmpty()) {
    505                     continue;
    506                 }
    507                 SampleChunk chunk = map.get(map.firstKey()).first;
    508                 if (earliestChunk == null
    509                         || chunk.getCreatedTimeMs() < earliestChunk.getCreatedTimeMs()) {
    510                     earliestChunkMap = map;
    511                     earliestChunk = chunk;
    512                     earliestChunkId = entry.getKey();
    513                 }
    514             }
    515             if (earliestChunk == null) {
    516                 break;
    517             }
    518             mPendingDelete.add(earliestChunkId, earliestChunk);
    519             earliestChunkMap.remove(earliestChunk.getStartPositionUs());
    520             if (DEBUG) {
    521                 Log.d(TAG, String.format("bufferSize = %d; pendingDelete = %b; "
    522                                 + "earliestChunk size = %d; %s@%d (%s)",
    523                         mBufferSize, pendingDelete, earliestChunk.getSize(), earliestChunkId,
    524                         earliestChunk.getStartPositionUs(),
    525                         Utils.toIsoDateTimeString(earliestChunk.getCreatedTimeMs())));
    526             }
    527             ChunkEvictedListener listener = mEvictListeners.get(earliestChunkId);
    528             if (listener != null) {
    529                 listener.onChunkEvicted(earliestChunkId, earliestChunk.getCreatedTimeMs());
    530             }
    531             pendingDelete = mPendingDelete.getSize();
    532         }
    533         for (Map.Entry<String, SortedMap<Long, Pair<SampleChunk, Integer>>> entry :
    534                 mChunkMap.entrySet()) {
    535             SortedMap<Long, Pair<SampleChunk, Integer>> map = entry.getValue();
    536             if (map.isEmpty()) {
    537                 continue;
    538             }
    539             mStartPositionMap.put(entry.getKey(), map.firstKey());
    540         }
    541         return true;
    542     }
    543 
    544     /**
    545      * Reads track information which includes {@link MediaFormat}.
    546      *
    547      * @return returns all track information which is found by {@link BufferManager.StorageManager}.
    548      * @throws IOException
    549      */
    550     public List<TrackFormat> readTrackInfoFiles() throws IOException {
    551         List<TrackFormat> trackFormatList = new ArrayList<>();
    552         trackFormatList.addAll(mStorageManager.readTrackInfoFiles(false));
    553         trackFormatList.addAll(mStorageManager.readTrackInfoFiles(true));
    554         if (trackFormatList.isEmpty()) {
    555             throw new IOException("No track information to load");
    556         }
    557         return trackFormatList;
    558     }
    559 
    560     /**
    561      * Writes track information and index information for all tracks.
    562      *
    563      * @param audios list of audio track information
    564      * @param videos list of audio track information
    565      * @throws IOException
    566      */
    567     public void writeMetaFiles(List<TrackFormat> audios, List<TrackFormat> videos)
    568             throws IOException {
    569         if (audios.isEmpty() && videos.isEmpty()) {
    570             throw new IOException("No track information to save");
    571         }
    572         if (!audios.isEmpty()) {
    573             mStorageManager.writeTrackInfoFiles(audios, true);
    574             for (TrackFormat trackFormat : audios) {
    575                 SortedMap<Long, Pair<SampleChunk, Integer>> map =
    576                         mChunkMap.get(trackFormat.trackId);
    577                 if (map == null) {
    578                     throw new IOException("Audio track index missing");
    579                 }
    580                 mStorageManager.writeIndexFile(trackFormat.trackId, map);
    581             }
    582         }
    583         if (!videos.isEmpty()) {
    584             mStorageManager.writeTrackInfoFiles(videos, false);
    585             for (TrackFormat trackFormat : videos) {
    586                 SortedMap<Long, Pair<SampleChunk, Integer>> map =
    587                         mChunkMap.get(trackFormat.trackId);
    588                 if (map == null) {
    589                     throw new IOException("Video track index missing");
    590                 }
    591                 mStorageManager.writeIndexFile(trackFormat.trackId, map);
    592             }
    593         }
    594     }
    595 
    596     /**
    597      * Releases all the resources.
    598      */
    599     public void release() {
    600         try {
    601             mPendingDelete.release();
    602             for (Map.Entry<String, SortedMap<Long, Pair<SampleChunk, Integer>>> entry :
    603                     mChunkMap.entrySet()) {
    604                 SampleChunk toRelease = null;
    605                 for (Pair<SampleChunk, Integer> positions : entry.getValue().values()) {
    606                     if (toRelease != positions.first) {
    607                         toRelease = positions.first;
    608                         SampleChunk.IoState.release(toRelease, !mStorageManager.isPersistent());
    609                     }
    610                 }
    611             }
    612             mChunkMap.clear();
    613         } catch (ConcurrentModificationException | NullPointerException e) {
    614             // TODO: remove this after it it confirmed that race condition issues are resolved.
    615             // b/32492258, b/32373376
    616             SoftPreconditions.checkState(false, "Exception on BufferManager#release: ",
    617                     e.toString());
    618         }
    619     }
    620 
    621     private void resetWriteStat(float writeBandwidth) {
    622         mWriteBandwidth = writeBandwidth;
    623         mTotalWriteSize = 0;
    624         mTotalWriteTimeNs = 0;
    625     }
    626 
    627     /**
    628      * Adds a disk write sample size to calculate the average disk write bandwidth.
    629      */
    630     public void addWriteStat(long size, long timeNs) {
    631         if (size >= mMinSampleSizeForSpeedCheck) {
    632             mTotalWriteSize += size;
    633             mTotalWriteTimeNs += timeNs;
    634         }
    635     }
    636 
    637     /**
    638      * Returns if the average disk write bandwidth is slower than
    639      * threshold {@code MINIMUM_DISK_WRITE_SPEED_MBPS}.
    640      */
    641     public boolean isWriteSlow() {
    642         if (mTotalWriteSize < MINIMUM_WRITE_SIZE_FOR_SPEED_CHECK) {
    643             return false;
    644         }
    645 
    646         // Checks write speed for only MAXIMUM_SPEED_CHECK_COUNT times to ignore outliers
    647         // by temporary system overloading during the playback.
    648         if (mSpeedCheckCount > MAXIMUM_SPEED_CHECK_COUNT) {
    649             return false;
    650         }
    651         mSpeedCheckCount++;
    652         float megabytePerSecond = calculateWriteBandwidth();
    653         resetWriteStat(megabytePerSecond);
    654         if (DEBUG) {
    655             Log.d(TAG, "Measured disk write performance: " + megabytePerSecond + "MBps");
    656         }
    657         return megabytePerSecond < MINIMUM_DISK_WRITE_SPEED_MBPS;
    658     }
    659 
    660     /**
    661      * Returns recent write bandwidth in MBps. If recent bandwidth is not available,
    662      * returns {float -1.0f}.
    663      */
    664     public float getWriteBandwidth() {
    665         return mWriteBandwidth == 0.0f ? -1.0f : mWriteBandwidth;
    666     }
    667 
    668     private float calculateWriteBandwidth() {
    669         if (mTotalWriteTimeNs == 0) {
    670             return -1;
    671         }
    672         return ((float) mTotalWriteSize * 1000 / mTotalWriteTimeNs);
    673     }
    674 
    675     /**
    676      * Returns if {@link BufferManager} has checked the write speed,
    677      * which is suitable for Trickplay.
    678      */
    679     @VisibleForTesting
    680     public boolean hasSpeedCheckDone() {
    681         return mSpeedCheckCount > 0;
    682     }
    683 
    684     /**
    685      * Sets minimum sample size for write speed check.
    686      * @param sampleSize minimum sample size for write speed check.
    687      */
    688     @VisibleForTesting
    689     public void setMinimumSampleSizeForSpeedCheck(int sampleSize) {
    690         mMinSampleSizeForSpeedCheck = sampleSize;
    691     }
    692 }
    693