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.usbtuner.exoplayer.cache;
18 
19 import android.media.MediaFormat;
20 import android.os.ConditionVariable;
21 import android.os.HandlerThread;
22 import android.support.annotation.VisibleForTesting;
23 import android.support.annotation.NonNull;
24 import android.support.annotation.Nullable;
25 import android.util.ArrayMap;
26 import android.util.ArraySet;
27 import android.util.Log;
28 import android.util.Pair;
29 
30 import com.google.android.exoplayer.SampleHolder;
31 
32 import java.io.File;
33 import java.io.FileNotFoundException;
34 import java.io.IOException;
35 import java.text.SimpleDateFormat;
36 import java.util.ArrayList;
37 import java.util.Date;
38 import java.util.List;
39 import java.util.Locale;
40 import java.util.Map;
41 import java.util.Set;
42 import java.util.SortedMap;
43 import java.util.TreeMap;
44 
45 /**
46  * Manages {@link SampleCache} objects.
47  * <p>
48  * The cache manager can be disabled, while running, if the write throughput to the associated
49  * external storage is detected to be lower than a threshold {@code MINIMUM_DISK_WRITE_SPEED_MBPS}".
50  * This leads to restarting playback flow.
51  */
52 public class CacheManager {
53     private static final String TAG = "CacheManager";
54     private static final boolean DEBUG = false;
55 
56     // Constants for the disk write speed checking
57     private static final long MINIMUM_WRITE_SIZE_FOR_SPEED_CHECK =
58             10L * 1024 * 1024;  // Checks for every 10M disk write
59     private static final int MINIMUM_SAMPLE_SIZE_FOR_SPEED_CHECK = 15 * 1024;
60     private static final int MAXIMUM_SPEED_CHECK_COUNT = 5;  // Checks only 5 times
61     private static final int MINIMUM_DISK_WRITE_SPEED_MBPS = 3;  // 3 Megabytes per second
62 
63     private final SampleCache.SampleCacheFactory mSampleCacheFactory;
64     private final Map<String, SortedMap<Long, SampleCache>> mCacheMap = new ArrayMap<>();
65     private final Map<String, EvictListener> mEvictListeners = new ArrayMap<>();
66     private final StorageManager mStorageManager;
67     private final HandlerThread mIoHandlerThread = new HandlerThread(TAG);
68     private long mCacheSize = 0;
69     private final CacheSet mPendingDelete = new CacheSet();
70     private final CacheListener mCacheListener = new CacheListener() {
71         @Override
72         public void onWrite(SampleCache cache) {
73             mCacheSize += cache.getSize();
74         }
75 
76         @Override
77         public void onDelete(SampleCache cache) {
78             mPendingDelete.remove(cache);
79             mCacheSize -= cache.getSize();
80         }
81     };
82 
83     private volatile boolean mClosed = false;
84     private int mMinSampleSizeForSpeedCheck = MINIMUM_SAMPLE_SIZE_FOR_SPEED_CHECK;
85     private long mTotalWriteSize;
86     private long mTotalWriteTimeNs;
87     private volatile int mSpeedCheckCount;
88     private boolean mDisabled = false;
89 
90     public interface CacheListener {
onWrite(SampleCache cache)91         void onWrite(SampleCache cache);
onDelete(SampleCache cache)92         void onDelete(SampleCache cache);
93     }
94 
95     public interface EvictListener {
onCacheEvicted(String id, long createdTimeMs)96         void onCacheEvicted(String id, long createdTimeMs);
97     }
98 
99     /**
100      * Handles I/O
101      * between CacheManager and {@link com.android.usbtuner.exoplayer.SampleExtractor}.
102      */
103     public interface SampleBuffer {
104 
105         /**
106          * Initializes SampleBuffer.
107          * @param Ids track identifiers for storage read/write.
108          * @param mediaFormats meta-data for each track, this will be saved to storage in recording.
109          * @throws IOException
110          */
init(@onNull List<String> Ids, @Nullable List<MediaFormat> mediaFormats)111         void init(@NonNull List<String> Ids, @Nullable List<MediaFormat> mediaFormats)
112                 throws IOException;
113 
114         /**
115          * Selects the track {@code index} for reading sample data.
116          */
selectTrack(int index)117         void selectTrack(int index);
118 
119         /**
120          * Deselects the track at {@code index},
121          * so that no more samples will be read from the track.
122          */
deselectTrack(int index)123         void deselectTrack(int index);
124 
125         /**
126          * Writes sample to storage.
127          *
128          * @param index track index
129          * @param sample sample to write at storage
130          * @param conditionVariable notifies the completion of writing sample.
131          * @throws IOException
132          */
writeSample(int index, SampleHolder sample, ConditionVariable conditionVariable)133         void writeSample(int index, SampleHolder sample, ConditionVariable conditionVariable)
134                 throws IOException;
135 
136         /**
137          * Checks whether storage write speed is slow.
138          */
isWriteSpeedSlow(int sampleSize, long writeDurationNs)139         boolean isWriteSpeedSlow(int sampleSize, long writeDurationNs);
140 
141         /**
142          * Handles when write speed is slow.
143          */
handleWriteSpeedSlow()144         void handleWriteSpeedSlow();
145 
146         /**
147          * Sets the flag when EoS was met.
148          */
setEos()149         void setEos();
150 
151         /**
152          * Reads the next sample in the track at index {@code track} into {@code sampleHolder},
153          * returning {@link com.google.android.exoplayer.SampleSource#SAMPLE_READ}
154          * if it is available.
155          * If the next sample is not available,
156          * returns {@link com.google.android.exoplayer.SampleSource#NOTHING_READ}.
157          */
readSample(int index, SampleHolder outSample)158         int readSample(int index, SampleHolder outSample);
159 
160         /**
161          * Seeks to the specified time in microseconds.
162          */
seekTo(long positionUs)163         void seekTo(long positionUs);
164 
165         /**
166          * Returns an estimate of the position up to which data is buffered.
167          */
getBufferedPositionUs()168         long getBufferedPositionUs();
169 
170         /**
171          * Returns whether there is buffered data.
172          */
continueBuffering(long positionUs)173         boolean continueBuffering(long positionUs);
174 
175         /**
176          * Cleans up and releases everything.
177          */
release()178         void release();
179     }
180 
181     /**
182      * Storage configuration and policy manager for {@link CacheManager}
183      */
184     public interface StorageManager {
185 
186         /**
187          * Provides eligible storage directory for {@link CacheManager}.
188          *
189          * @return a directory to save cache chunks and meta files
190          */
getCacheDir()191         File getCacheDir();
192 
193         /**
194          * Cleans up storage.
195          */
clearStorage()196         void clearStorage();
197 
198         /**
199          * Informs whether the storage is used for persistent use. (eg. dvr recording/play)
200          *
201          * @return {@code true} if stored files are persistent
202          */
isPersistent()203         boolean isPersistent();
204 
205         /**
206          * Informs whether the storage usage exceeds pre-determined size.
207          *
208          * @param cacheSize the current total usage of Storage in bytes.
209          * @param pendingDelete the current storage usage which will be deleted in near future by
210          *                      bytes
211          * @return {@code true} if it reached pre-determined max size
212          */
reachedStorageMax(long cacheSize, long pendingDelete)213         boolean reachedStorageMax(long cacheSize, long pendingDelete);
214 
215         /**
216          * Informs whether the storage has enough remained space.
217          *
218          * @param pendingDelete the current storage usage which will be deleted in near future by
219          *                      bytes
220          * @return {@code true} if it has enough space
221          */
hasEnoughBuffer(long pendingDelete)222         boolean hasEnoughBuffer(long pendingDelete);
223 
224         /**
225          * Reads track name & {@link MediaFormat} from storage.
226          *
227          * @param isAudio {@code true} if it is for audio track
228          * @return {@link Pair} of track name & {@link MediaFormat}
229          * @throws {@link java.io.IOException}
230          */
readTrackInfoFile(boolean isAudio)231         Pair<String, MediaFormat> readTrackInfoFile(boolean isAudio) throws IOException;
232 
233         /**
234          * Reads sample indexes for each written sample from storage.
235          *
236          * @param trackId track name
237          * @return
238          * @throws {@link java.io.IOException}
239          */
readIndexFile(String trackId)240         ArrayList<Long> readIndexFile(String trackId) throws IOException;
241 
242         /**
243          * Writes track information to storage.
244          *
245          * @param trackId track name
246          * @param format {@link android.media.MediaFormat} of the track
247          * @param isAudio {@code true} if it is for audio track
248          * @throws {@link java.io.IOException}
249          */
writeTrackInfoFile(String trackId, MediaFormat format, boolean isAudio)250         void writeTrackInfoFile(String trackId, MediaFormat format, boolean isAudio)
251                 throws IOException;
252 
253         /**
254          * Writes index file to storage.
255          *
256          * @param trackName track name
257          * @param index {@link SampleCache} container
258          * @throws {@link java.io.IOException}
259          */
writeIndexFile(String trackName, SortedMap<Long, SampleCache> index)260         void writeIndexFile(String trackName, SortedMap<Long, SampleCache> index)
261                 throws IOException;
262     }
263 
264     private static class CacheSet {
265         private final Set<SampleCache> mCaches = new ArraySet<>();
266 
add(SampleCache cache)267         public synchronized void add(SampleCache cache) {
268             mCaches.add(cache);
269         }
270 
remove(SampleCache cache)271         public synchronized void remove(SampleCache cache) {
272             mCaches.remove(cache);
273         }
274 
getSize()275         public synchronized long getSize() {
276             long size = 0;
277             for (SampleCache cache : mCaches) {
278                 size += cache.getSize();
279             }
280             return size;
281         }
282     }
283 
CacheManager(StorageManager storageManager)284     public CacheManager(StorageManager storageManager) {
285         this(storageManager, new SampleCache.SampleCacheFactory());
286     }
287 
CacheManager(StorageManager storageManager, SampleCache.SampleCacheFactory sampleCacheFactory)288     public CacheManager(StorageManager storageManager,
289             SampleCache.SampleCacheFactory sampleCacheFactory) {
290         mStorageManager = storageManager;
291         mSampleCacheFactory = sampleCacheFactory;
292         clearCache(true);
293         mIoHandlerThread.start();
294     }
295 
registerEvictListener(String id, EvictListener evictListener)296     public void registerEvictListener(String id, EvictListener evictListener) {
297         mEvictListeners.put(id, evictListener);
298     }
299 
unregisterEvictListener(String id)300     public void unregisterEvictListener(String id) {
301         mEvictListeners.remove(id);
302     }
303 
clearCache(boolean deleteFiles)304     private void clearCache(boolean deleteFiles) {
305         mCacheMap.clear();
306         if (deleteFiles) {
307             mStorageManager.clearStorage();
308         }
309         mCacheSize = 0;
310     }
311 
getFileName(String id, long positionUs)312     private static String getFileName(String id, long positionUs) {
313         return String.format(Locale.ENGLISH, "%s_%016x.cache", id, positionUs);
314     }
315 
316     /**
317      * Creates a new {@link SampleCache} for caching samples.
318      *
319      * @param id the name of the track
320      * @param positionUs starting position of the {@link SampleCache} in micro seconds.
321      * @param samplePool {@link SamplePool} for the fast creation of samples.
322      * @return returns the created {@link SampleCache}.
323      * @throws {@link java.io.IOException}
324      */
createNewWriteFile(String id, long positionUs, SamplePool samplePool)325     public SampleCache createNewWriteFile(String id, long positionUs, SamplePool samplePool)
326             throws IOException {
327         if (!maybeEvictCache()) {
328             throw new IOException("Not enough storage space");
329         }
330         SortedMap<Long, SampleCache> map = mCacheMap.get(id);
331         if (map == null) {
332             map = new TreeMap<>();
333             mCacheMap.put(id, map);
334         }
335         File file = new File(mStorageManager.getCacheDir(), getFileName(id, positionUs));
336         SampleCache sampleCache = mSampleCacheFactory.createSampleCache(samplePool, file,
337                 positionUs, mCacheListener, mIoHandlerThread.getLooper());
338         map.put(positionUs, sampleCache);
339         return sampleCache;
340     }
341 
342     /**
343      * Loads a track using {@link CacheManager.StorageManager}.
344      *
345      * @param trackId the name of the track.
346      * @param samplePool {@link SamplePool} for the fast creation of samples.
347      * @throws {@link java.io.IOException}
348      */
loadTrackFormStorage(String trackId, SamplePool samplePool)349     public void loadTrackFormStorage(String trackId, SamplePool samplePool) throws IOException {
350         ArrayList<Long> keyPositions = mStorageManager.readIndexFile(trackId);
351 
352         // TODO: notify the end position
353         SortedMap<Long, SampleCache> map = mCacheMap.get(trackId);
354         if (map == null) {
355             map = new TreeMap<>();
356             mCacheMap.put(trackId, map);
357         }
358         SampleCache cache = null;
359         for (long positionUs: keyPositions) {
360             cache = mSampleCacheFactory.createSampleCacheFromFile(samplePool,
361                     mStorageManager.getCacheDir(), getFileName(trackId, positionUs), positionUs,
362                     mCacheListener, mIoHandlerThread.getLooper(), cache);
363             map.put(positionUs, cache);
364         }
365     }
366 
367     /**
368      * Finds a {@link SampleCache} for the specified track name and the position.
369      *
370      * @param id the name of the track.
371      * @param positionUs the position.
372      * @return returns the found {@link SampleCache}.
373      */
getReadFile(String id, long positionUs)374     public SampleCache getReadFile(String id, long positionUs) {
375         SortedMap<Long, SampleCache> map = mCacheMap.get(id);
376         if (map == null) {
377             return null;
378         }
379         SampleCache sampleCache;
380         SortedMap<Long, SampleCache> headMap = map.headMap(positionUs + 1);
381         if (!headMap.isEmpty()) {
382             sampleCache = headMap.get(headMap.lastKey());
383         } else {
384             sampleCache = map.get(map.firstKey());
385         }
386         return sampleCache;
387     }
388 
maybeEvictCache()389     private boolean maybeEvictCache() {
390         long pendingDelete = mPendingDelete.getSize();
391         while (mStorageManager.reachedStorageMax(mCacheSize, pendingDelete)
392                 || !mStorageManager.hasEnoughBuffer(pendingDelete)) {
393             if (mStorageManager.isPersistent()) {
394                 // Since cache is persistent, we cannot evict caches.
395                 return false;
396             }
397             SortedMap<Long, SampleCache> earliestCacheMap = null;
398             SampleCache earliestCache = null;
399             String earliestCacheId = null;
400             for (Map.Entry<String, SortedMap<Long, SampleCache>> entry : mCacheMap.entrySet()) {
401                 SortedMap<Long, SampleCache> map = entry.getValue();
402                 if (map.isEmpty()) {
403                     continue;
404                 }
405                 SampleCache cache = map.get(map.firstKey());
406                 if (earliestCache == null
407                         || cache.getCreatedTimeMs() < earliestCache.getCreatedTimeMs()) {
408                     earliestCacheMap = map;
409                     earliestCache = cache;
410                     earliestCacheId = entry.getKey();
411                 }
412             }
413             if (earliestCache == null) {
414                 break;
415             }
416             mPendingDelete.add(earliestCache);
417             earliestCache.delete();
418             earliestCacheMap.remove(earliestCache.getStartPositionUs());
419             if (DEBUG) {
420                 Log.d(TAG, String.format("cacheSize = %d; pendingDelete = %b; "
421                                 + "earliestCache size = %d; %s@%d (%s)",
422                         mCacheSize, pendingDelete, earliestCache.getSize(), earliestCacheId,
423                         earliestCache.getStartPositionUs(),
424                         new SimpleDateFormat().format(new Date(earliestCache.getCreatedTimeMs()))));
425             }
426             EvictListener listener = mEvictListeners.get(earliestCacheId);
427             if (listener != null) {
428                 listener.onCacheEvicted(earliestCacheId, earliestCache.getCreatedTimeMs());
429             }
430             pendingDelete = mPendingDelete.getSize();
431         }
432         return true;
433     }
434 
435     /**
436      * Reads track information which includes {@link MediaFormat}.
437      *
438      * @return returns all track information which is found by {@link CacheManager.StorageManager}.
439      * @throws {@link java.io.IOException}
440      */
readTrackInfoFiles()441     public ArrayList<Pair<String, MediaFormat>> readTrackInfoFiles() throws IOException {
442         ArrayList<Pair<String, MediaFormat>> trackInfos = new ArrayList<>();
443         try {
444             trackInfos.add(mStorageManager.readTrackInfoFile(false));
445         } catch (FileNotFoundException e) {
446             // There can be a single track only recording. (eg. audio-only, video-only)
447             // So the exception should not stop the read.
448         }
449         try {
450             trackInfos.add(mStorageManager.readTrackInfoFile(true));
451         } catch (FileNotFoundException e) {
452             // See above catch block.
453         }
454         return trackInfos;
455     }
456 
457     /**
458      * Writes track information and index information for all tracks.
459      *
460      * @param audio audio information.
461      * @param video video information.
462      */
writeMetaFiles(Pair<String, MediaFormat> audio, Pair<String, MediaFormat> video)463     public void writeMetaFiles(Pair<String, MediaFormat> audio, Pair<String, MediaFormat> video) {
464         try {
465             if (audio != null) {
466                 mStorageManager.writeTrackInfoFile(audio.first, audio.second, true);
467                 SortedMap<Long, SampleCache> map = mCacheMap.get(audio.first);
468                 if (map == null) {
469                     throw new IOException("Audio track index missing");
470                 }
471                 mStorageManager.writeIndexFile(audio.first, map);
472             }
473             if (video != null) {
474                 mStorageManager.writeTrackInfoFile(video.first, video.second, false);
475                 SortedMap<Long, SampleCache> map = mCacheMap.get(video.first);
476                 if (map == null) {
477                     throw new IOException("Video track index missing");
478                 }
479                 mStorageManager.writeIndexFile(video.first, map);
480             }
481         } catch (IOException e) {
482             // TODO: throw exception and notify this failure properly.
483         }
484     }
485 
486     /**
487      * Marks it is closed and it is not used anymore.
488      */
close()489     public void close() {
490         // Clean-up may happen after this is called.
491         mClosed = true;
492     }
493 
494     /**
495      * Cleans up the specified track.
496      *
497      * @param trackId the name of the track.
498      */
clearTrack(String trackId)499     public void clearTrack(String trackId) {
500         SortedMap<Long, SampleCache> map = mCacheMap.get(trackId);
501         if (map == null) {
502             Log.w(TAG, "Cache with specified ID (" + trackId + ") not found");
503             return;
504         }
505         for (SampleCache cache : map.values()) {
506             cache.clear();
507             cache.close();
508             if (!mStorageManager.isPersistent()) {
509                 cache.delete();
510             }
511         }
512         mCacheMap.remove(trackId);
513         if (mCacheMap.isEmpty() && mClosed) {
514             mIoHandlerThread.quitSafely();
515             clearCache(!mStorageManager.isPersistent());
516         }
517     }
518 
resetWriteStat()519     private void resetWriteStat() {
520         mTotalWriteSize = 0;
521         mTotalWriteTimeNs = 0;
522     }
523 
524     /**
525      * Adds a disk write sample size to calculate the average disk write bandwidth.
526      */
addWriteStat(long size, long timeNs)527     public void addWriteStat(long size, long timeNs) {
528         if (size >= mMinSampleSizeForSpeedCheck) {
529             mTotalWriteSize += size;
530             mTotalWriteTimeNs += timeNs;
531         }
532     }
533 
534     /**
535      * Returns if the average disk write bandwidth is slower than
536      * threshold {@code MINIMUM_DISK_WRITE_SPEED_MBPS}.
537      */
isWriteSlow()538     public boolean isWriteSlow() {
539         if (mTotalWriteSize < MINIMUM_WRITE_SIZE_FOR_SPEED_CHECK) {
540             return false;
541         }
542 
543         // Checks write speed for only MAXIMUM_SPEED_CHECK_COUNT times to ignore outliers
544         // by temporary system overloading during the playback.
545         if (mSpeedCheckCount > MAXIMUM_SPEED_CHECK_COUNT) {
546             return false;
547         }
548         mSpeedCheckCount++;
549         float megabytePerSecond = getWriteBandwidth();
550         resetWriteStat();
551         if (DEBUG) {
552             Log.d(TAG, "Measured disk write performance: " + megabytePerSecond + "MBps");
553         }
554         return megabytePerSecond < MINIMUM_DISK_WRITE_SPEED_MBPS;
555     }
556 
557     /**
558      * Returns the disk write speed in megabytes per second.
559      */
getWriteBandwidth()560     private float getWriteBandwidth() {
561         if (mTotalWriteTimeNs == 0) {
562             return -1;
563         }
564         return ((float) mTotalWriteSize * 1000 / mTotalWriteTimeNs);
565     }
566 
567     /**
568      * Marks {@link CacheManger} object disabled to prevent it from the future use.
569      */
disable()570     public void disable() {
571         mDisabled = true;
572     }
573 
574     /**
575      * Returns if {@link CacheManger} object is disabled.
576      */
isDisabled()577     public boolean isDisabled() {
578         return mDisabled;
579     }
580 
581     /**
582      * Returns if {@link CacheManager} has checked the write speed, which is suitable for Trickplay.
583      */
584     @VisibleForTesting
hasSpeedCheckDone()585     public boolean hasSpeedCheckDone() {
586         return mSpeedCheckCount > 0;
587     }
588 
589     /**
590      * Sets minimum sample size for write speed check.
591      * @param sampleSize minimum sample size for write speed check.
592      */
593     @VisibleForTesting
setMinimumSampleSizeForSpeedCheck(int sampleSize)594     public void setMinimumSampleSizeForSpeedCheck(int sampleSize) {
595         mMinSampleSizeForSpeedCheck = sampleSize;
596     }
597 }
598