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