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