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