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