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