• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
onChunkEvicted(String id, long createdTimeMs)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          */
init( @onNull List<String> Ids, @NonNull List<com.google.android.exoplayer.MediaFormat> mediaFormats)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. */
selectTrack(int index)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          */
deselectTrack(int index)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          */
writeSample(int index, SampleHolder sample, ConditionVariable conditionVariable)123         void writeSample(int index, SampleHolder sample, ConditionVariable conditionVariable)
124                 throws IOException;
125 
126         /** Checks whether storage write speed is slow. */
isWriteSpeedSlow(int sampleSize, long writeDurationNs)127         boolean isWriteSpeedSlow(int sampleSize, long writeDurationNs);
128 
129         /**
130          * Handles when write speed is slow.
131          *
132          * @throws IOException
133          */
handleWriteSpeedSlow()134         void handleWriteSpeedSlow() throws IOException;
135 
136         /** Sets the flag when EoS was reached. */
setEos()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          */
readSample(int index, SampleHolder outSample)145         int readSample(int index, SampleHolder outSample);
146 
147         /** Seeks to the specified time in microseconds. */
seekTo(long positionUs)148         void seekTo(long positionUs);
149 
150         /** Returns an estimate of the position up to which data is buffered. */
getBufferedPositionUs()151         long getBufferedPositionUs();
152 
153         /** Returns whether there is buffered data. */
continueBuffering(long positionUs)154         boolean continueBuffering(long positionUs);
155 
156         /**
157          * Cleans up and releases everything.
158          *
159          * @throws IOException
160          */
release()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          */
TrackFormat(String trackId, MediaFormat format)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          */
PositionHolder(long positionUs, long basePositionUs, int offset)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          */
getBufferDir()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          */
isPersistent()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          */
reachedStorageMax(long bufferSize, long pendingDelete)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          */
hasEnoughBuffer(long pendingDelete)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          */
readTrackInfoFiles(boolean isAudio)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          */
readIndexFile(String trackId)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          */
writeTrackInfoFiles(List<TrackFormat> formatList, boolean isAudio)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          */
writeIndexFile(String trackName, SortedMap<Long, Pair<SampleChunk, Integer>> index)285         void writeIndexFile(String trackName, SortedMap<Long, Pair<SampleChunk, Integer>> index)
286                 throws IOException;
287 
288         /**
289          * Writes to index file to storage.
290          *
291          * @param trackName track name
292          * @param size size of sample
293          * @param position position in micro seconds
294          * @param sampleChunk {@link SampleChunk} chunk to be added
295          * @param offset offset
296          * @throws IOException
297          */
updateIndexFile( String trackName, int size, long position, SampleChunk sampleChunk, int offset)298         void updateIndexFile(
299                 String trackName, int size, long position, SampleChunk sampleChunk, int offset)
300                 throws IOException;
301     }
302 
303     private static class EvictChunkQueueMap {
304         private final Map<String, LinkedList<SampleChunk>> mEvictMap = new ArrayMap<>();
305         private long mSize;
306 
init(String key)307         private void init(String key) {
308             mEvictMap.put(key, new LinkedList<>());
309         }
310 
add(String key, SampleChunk chunk)311         private void add(String key, SampleChunk chunk) {
312             LinkedList<SampleChunk> queue = mEvictMap.get(key);
313             if (queue != null) {
314                 mSize += chunk.getSize();
315                 queue.add(chunk);
316             }
317         }
318 
poll(String key, long startPositionUs)319         private SampleChunk poll(String key, long startPositionUs) {
320             LinkedList<SampleChunk> queue = mEvictMap.get(key);
321             if (queue != null) {
322                 SampleChunk chunk = queue.peek();
323                 if (chunk != null && chunk.getStartPositionUs() < startPositionUs) {
324                     mSize -= chunk.getSize();
325                     return queue.poll();
326                 }
327             }
328             return null;
329         }
330 
getSize()331         private long getSize() {
332             return mSize;
333         }
334 
release()335         private void release() {
336             for (Map.Entry<String, LinkedList<SampleChunk>> entry : mEvictMap.entrySet()) {
337                 for (SampleChunk chunk : entry.getValue()) {
338                     SampleChunk.IoState.release(chunk, true);
339                 }
340             }
341             mEvictMap.clear();
342             mSize = 0;
343         }
344     }
345 
BufferManager(StorageManager storageManager)346     public BufferManager(StorageManager storageManager) {
347         this(storageManager, new SampleChunk.SampleChunkCreator());
348     }
349 
BufferManager( StorageManager storageManager, SampleChunk.SampleChunkCreator sampleChunkCreator)350     public BufferManager(
351             StorageManager storageManager, SampleChunk.SampleChunkCreator sampleChunkCreator) {
352         mStorageManager = storageManager;
353         mSampleChunkCreator = sampleChunkCreator;
354     }
355 
registerChunkEvictedListener(String id, ChunkEvictedListener listener)356     public void registerChunkEvictedListener(String id, ChunkEvictedListener listener) {
357         mEvictListeners.put(id, listener);
358     }
359 
unregisterChunkEvictedListener(String id)360     public void unregisterChunkEvictedListener(String id) {
361         mEvictListeners.remove(id);
362     }
363 
getFileName(String id, long positionUs)364     private static String getFileName(String id, long positionUs) {
365         return String.format(Locale.ENGLISH, "%s_%016x.chunk", id, positionUs);
366     }
367 
368     /**
369      * Creates a new {@link SampleChunk} for caching samples if it is needed.
370      *
371      * @param id the name of the track
372      * @param positionUs current position to write a sample in micro seconds.
373      * @param samplePool {@link SamplePool} for the fast creation of samples.
374      * @param currentChunk the current {@link SampleChunk} to write, {@code null} when to create a
375      *     new {@link SampleChunk}.
376      * @param currentOffset the current offset to write.
377      * @return returns the created {@link SampleChunk}.
378      * @throws IOException
379      */
createNewWriteFileIfNeeded( String id, long positionUs, SamplePool samplePool, SampleChunk currentChunk, int currentOffset, boolean updateIndexFile)380     public SampleChunk createNewWriteFileIfNeeded(
381             String id,
382             long positionUs,
383             SamplePool samplePool,
384             SampleChunk currentChunk,
385             int currentOffset,
386             boolean updateIndexFile)
387             throws IOException {
388         if (!maybeEvictChunk()) {
389             throw new IOException("Not enough storage space");
390         }
391         SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(id);
392         if (map == null) {
393             map = new TreeMap<>();
394             mChunkMap.put(id, map);
395             mStartPositionMap.put(id, positionUs);
396             mPendingDelete.init(id);
397         }
398         if (currentChunk == null) {
399             File file = new File(mStorageManager.getBufferDir(), getFileName(id, positionUs));
400             SampleChunk sampleChunk =
401                     mSampleChunkCreator.createSampleChunk(
402                             samplePool, file, positionUs, mChunkCallback);
403             map.put(positionUs, new Pair(sampleChunk, 0));
404             if (updateIndexFile) {
405                 mStorageManager.updateIndexFile(id, map.size(), positionUs, sampleChunk, 0);
406             }
407             return sampleChunk;
408         } else {
409             map.put(positionUs, new Pair(currentChunk, currentOffset));
410             if (updateIndexFile) {
411                 mStorageManager.updateIndexFile(
412                         id, map.size(), positionUs, currentChunk, currentOffset);
413             }
414             return null;
415         }
416     }
417 
418     /**
419      * Loads a track using {@link BufferManager.StorageManager}.
420      *
421      * @param trackId the name of the track.
422      * @param samplePool {@link SamplePool} for the fast creation of samples.
423      * @throws IOException
424      */
loadTrackFromStorage(String trackId, SamplePool samplePool)425     public void loadTrackFromStorage(String trackId, SamplePool samplePool) throws IOException {
426         ArrayList<PositionHolder> keyPositions = mStorageManager.readIndexFile(trackId);
427         long startPositionUs = keyPositions.size() > 0 ? keyPositions.get(0).positionUs : 0;
428 
429         SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(trackId);
430         if (map == null) {
431             map = new TreeMap<>();
432             mChunkMap.put(trackId, map);
433             mStartPositionMap.put(trackId, startPositionUs);
434             mPendingDelete.init(trackId);
435         }
436         SampleChunk chunk = null;
437         long basePositionUs = -1;
438         for (PositionHolder position : keyPositions) {
439             if (position.basePositionUs != basePositionUs) {
440                 chunk =
441                         mSampleChunkCreator.loadSampleChunkFromFile(
442                                 samplePool,
443                                 mStorageManager.getBufferDir(),
444                                 getFileName(trackId, position.positionUs),
445                                 position.positionUs,
446                                 mChunkCallback,
447                                 chunk);
448                 basePositionUs = position.basePositionUs;
449             }
450             map.put(position.positionUs, new Pair(chunk, position.offset));
451         }
452     }
453 
454     /**
455      * Finds a {@link SampleChunk} for the specified track name and the position.
456      *
457      * @param id the name of the track.
458      * @param positionUs the position.
459      * @return returns the found {@link SampleChunk}.
460      */
getReadFile(String id, long positionUs)461     public Pair<SampleChunk, Integer> getReadFile(String id, long positionUs) {
462         SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(id);
463         if (map == null) {
464             return null;
465         }
466         Pair<SampleChunk, Integer> ret;
467         SortedMap<Long, Pair<SampleChunk, Integer>> headMap = map.headMap(positionUs + 1);
468         if (!headMap.isEmpty()) {
469             ret = headMap.get(headMap.lastKey());
470         } else {
471             ret = map.get(map.firstKey());
472         }
473         return ret;
474     }
475 
476     /**
477      * Evicts chunks which are ready to be evicted for the specified track
478      *
479      * @param id the specified track
480      * @param earlierThanPositionUs the start position of the {@link SampleChunk} should be earlier
481      *     than
482      */
evictChunks(String id, long earlierThanPositionUs)483     public void evictChunks(String id, long earlierThanPositionUs) {
484         SampleChunk chunk = null;
485         while ((chunk = mPendingDelete.poll(id, earlierThanPositionUs)) != null) {
486             SampleChunk.IoState.release(chunk, !mStorageManager.isPersistent());
487         }
488     }
489 
490     /**
491      * Returns the start position of the specified track in micro seconds.
492      *
493      * @param id the specified track
494      */
getStartPositionUs(String id)495     public long getStartPositionUs(String id) {
496         Long ret = mStartPositionMap.get(id);
497         return ret == null ? 0 : ret;
498     }
499 
maybeEvictChunk()500     private boolean maybeEvictChunk() {
501         long pendingDelete = mPendingDelete.getSize();
502         while (mStorageManager.reachedStorageMax(mBufferSize, pendingDelete)
503                 || !mStorageManager.hasEnoughBuffer(pendingDelete)) {
504             if (mStorageManager.isPersistent()) {
505                 // Since chunks are persistent, we cannot evict chunks.
506                 return false;
507             }
508             SortedMap<Long, Pair<SampleChunk, Integer>> earliestChunkMap = null;
509             SampleChunk earliestChunk = null;
510             String earliestChunkId = null;
511             for (Map.Entry<String, SortedMap<Long, Pair<SampleChunk, Integer>>> entry :
512                     mChunkMap.entrySet()) {
513                 SortedMap<Long, Pair<SampleChunk, Integer>> map = entry.getValue();
514                 if (map.isEmpty()) {
515                     continue;
516                 }
517                 SampleChunk chunk = map.get(map.firstKey()).first;
518                 if (earliestChunk == null
519                         || chunk.getCreatedTimeMs() < earliestChunk.getCreatedTimeMs()) {
520                     earliestChunkMap = map;
521                     earliestChunk = chunk;
522                     earliestChunkId = entry.getKey();
523                 }
524             }
525             if (earliestChunk == null) {
526                 break;
527             }
528             mPendingDelete.add(earliestChunkId, earliestChunk);
529             earliestChunkMap.remove(earliestChunk.getStartPositionUs());
530             if (DEBUG) {
531                 Log.d(
532                         TAG,
533                         String.format(
534                                 "bufferSize = %d; pendingDelete = %b; "
535                                         + "earliestChunk size = %d; %s@%d (%s)",
536                                 mBufferSize,
537                                 pendingDelete,
538                                 earliestChunk.getSize(),
539                                 earliestChunkId,
540                                 earliestChunk.getStartPositionUs(),
541                                 CommonUtils.toIsoDateTimeString(earliestChunk.getCreatedTimeMs())));
542             }
543             ChunkEvictedListener listener = mEvictListeners.get(earliestChunkId);
544             if (listener != null) {
545                 listener.onChunkEvicted(earliestChunkId, earliestChunk.getCreatedTimeMs());
546             }
547             pendingDelete = mPendingDelete.getSize();
548         }
549         for (Map.Entry<String, SortedMap<Long, Pair<SampleChunk, Integer>>> entry :
550                 mChunkMap.entrySet()) {
551             SortedMap<Long, Pair<SampleChunk, Integer>> map = entry.getValue();
552             if (map.isEmpty()) {
553                 continue;
554             }
555             mStartPositionMap.put(entry.getKey(), map.firstKey());
556         }
557         return true;
558     }
559 
560     /**
561      * Reads track information which includes {@link MediaFormat}.
562      *
563      * @return returns all track information which is found by {@link BufferManager.StorageManager}.
564      * @throws IOException
565      */
readTrackInfoFiles()566     public List<TrackFormat> readTrackInfoFiles() throws IOException {
567         List<TrackFormat> trackFormatList = new ArrayList<>();
568         trackFormatList.addAll(mStorageManager.readTrackInfoFiles(false));
569         trackFormatList.addAll(mStorageManager.readTrackInfoFiles(true));
570         if (trackFormatList.isEmpty()) {
571             throw new IOException("No track information to load");
572         }
573         return trackFormatList;
574     }
575 
576     /**
577      * Writes track information and index information for all tracks.
578      *
579      * @param audios list of audio track information
580      * @param videos list of audio track information
581      * @throws IOException
582      */
writeMetaFiles(List<TrackFormat> audios, List<TrackFormat> videos)583     public void writeMetaFiles(List<TrackFormat> audios, List<TrackFormat> videos)
584             throws IOException {
585         if (audios.isEmpty() && videos.isEmpty()) {
586             throw new IOException("No track information to save");
587         }
588         if (!audios.isEmpty()) {
589             mStorageManager.writeTrackInfoFiles(audios, true);
590             for (TrackFormat trackFormat : audios) {
591                 SortedMap<Long, Pair<SampleChunk, Integer>> map =
592                         mChunkMap.get(trackFormat.trackId);
593                 if (map == null) {
594                     throw new IOException("Audio track index missing");
595                 }
596                 mStorageManager.writeIndexFile(trackFormat.trackId, map);
597             }
598         }
599         if (!videos.isEmpty()) {
600             mStorageManager.writeTrackInfoFiles(videos, false);
601             for (TrackFormat trackFormat : videos) {
602                 SortedMap<Long, Pair<SampleChunk, Integer>> map =
603                         mChunkMap.get(trackFormat.trackId);
604                 if (map == null) {
605                     throw new IOException("Video track index missing");
606                 }
607                 mStorageManager.writeIndexFile(trackFormat.trackId, map);
608             }
609         }
610     }
611 
612     /**
613      * Writes track information for all tracks.
614      *
615      * @param audios list of audio track information
616      * @param videos list of audio track information
617      * @throws IOException
618      */
writeMetaFilesOnly(List<TrackFormat> audios, List<TrackFormat> videos)619     public void writeMetaFilesOnly(List<TrackFormat> audios, List<TrackFormat> videos)
620             throws IOException {
621         if (audios.isEmpty() && videos.isEmpty()) {
622             throw new IOException("No track information to save");
623         }
624         if (!audios.isEmpty()) {
625             mStorageManager.writeTrackInfoFiles(audios, true);
626         }
627         if (!videos.isEmpty()) {
628             mStorageManager.writeTrackInfoFiles(videos, false);
629         }
630     }
631 
632     /** Releases all the resources. */
release()633     public void release() {
634         try {
635             mPendingDelete.release();
636             for (Map.Entry<String, SortedMap<Long, Pair<SampleChunk, Integer>>> entry :
637                     mChunkMap.entrySet()) {
638                 SampleChunk toRelease = null;
639                 for (Pair<SampleChunk, Integer> positions : entry.getValue().values()) {
640                     if (toRelease != positions.first) {
641                         toRelease = positions.first;
642                         SampleChunk.IoState.release(toRelease, !mStorageManager.isPersistent());
643                     }
644                 }
645             }
646             mChunkMap.clear();
647         } catch (ConcurrentModificationException | NullPointerException e) {
648             // TODO: remove this after it it confirmed that race condition issues are resolved.
649             // b/32492258, b/32373376
650             SoftPreconditions.checkState(
651                     false, "Exception on BufferManager#release: ", e.toString());
652         }
653     }
654 
resetWriteStat(float writeBandwidth)655     private void resetWriteStat(float writeBandwidth) {
656         mWriteBandwidth = writeBandwidth;
657         mTotalWriteSize = 0;
658         mTotalWriteTimeNs = 0;
659     }
660 
661     /** Adds a disk write sample size to calculate the average disk write bandwidth. */
addWriteStat(long size, long timeNs)662     public void addWriteStat(long size, long timeNs) {
663         if (size >= mMinSampleSizeForSpeedCheck) {
664             mTotalWriteSize += size;
665             mTotalWriteTimeNs += timeNs;
666         }
667     }
668 
669     /**
670      * Returns if the average disk write bandwidth is slower than threshold {@code
671      * MINIMUM_DISK_WRITE_SPEED_MBPS}.
672      */
isWriteSlow()673     public boolean isWriteSlow() {
674         if (mTotalWriteSize < MINIMUM_WRITE_SIZE_FOR_SPEED_CHECK) {
675             return false;
676         }
677 
678         // Checks write speed for only MAXIMUM_SPEED_CHECK_COUNT times to ignore outliers
679         // by temporary system overloading during the playback.
680         if (mSpeedCheckCount.get() > MAXIMUM_SPEED_CHECK_COUNT) {
681             return false;
682         }
683         mSpeedCheckCount.incrementAndGet();
684         float megabytePerSecond = calculateWriteBandwidth();
685         resetWriteStat(megabytePerSecond);
686         if (DEBUG) {
687             Log.d(TAG, "Measured disk write performance: " + megabytePerSecond + "MBps");
688         }
689         return megabytePerSecond < MINIMUM_DISK_WRITE_SPEED_MBPS;
690     }
691 
692     /**
693      * Returns recent write bandwidth in MBps. If recent bandwidth is not available, returns {float
694      * -1.0f}.
695      */
getWriteBandwidth()696     public float getWriteBandwidth() {
697         return mWriteBandwidth == 0.0f ? -1.0f : mWriteBandwidth;
698     }
699 
calculateWriteBandwidth()700     private float calculateWriteBandwidth() {
701         if (mTotalWriteTimeNs == 0) {
702             return -1;
703         }
704         return ((float) mTotalWriteSize * 1000 / mTotalWriteTimeNs);
705     }
706 
707     /**
708      * Returns if {@link BufferManager} has checked the write speed, which is suitable for
709      * Trickplay.
710      */
711     @VisibleForTesting
hasSpeedCheckDone()712     public boolean hasSpeedCheckDone() {
713         return mSpeedCheckCount.get() > 0;
714     }
715 
716     /**
717      * Sets minimum sample size for write speed check.
718      *
719      * @param sampleSize minimum sample size for write speed check.
720      */
721     @VisibleForTesting
setMinimumSampleSizeForSpeedCheck(int sampleSize)722     public void setMinimumSampleSizeForSpeedCheck(int sampleSize) {
723         mMinSampleSizeForSpeedCheck = sampleSize;
724     }
725 }
726