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.common.SoftPreconditions; 29 import com.android.tv.tuner.exoplayer.SampleExtractor; 30 import com.android.tv.util.Utils; 31 32 import java.io.File; 33 import java.io.IOException; 34 import java.util.ArrayList; 35 import java.util.ConcurrentModificationException; 36 import java.util.LinkedList; 37 import java.util.List; 38 import java.util.Locale; 39 import java.util.Map; 40 import java.util.SortedMap; 41 import java.util.TreeMap; 42 43 /** 44 * Manages {@link SampleChunk} objects. 45 * <p> 46 * The buffer manager can be disabled, while running, if the write throughput to the associated 47 * external storage is detected to be lower than a threshold {@code MINIMUM_DISK_WRITE_SPEED_MBPS}". 48 * This leads to restarting playback flow. 49 */ 50 public class BufferManager { 51 private static final String TAG = "BufferManager"; 52 private static final boolean DEBUG = false; 53 54 // Constants for the disk write speed checking 55 private static final long MINIMUM_WRITE_SIZE_FOR_SPEED_CHECK = 56 10L * 1024 * 1024; // Checks for every 10M disk write 57 private static final int MINIMUM_SAMPLE_SIZE_FOR_SPEED_CHECK = 15 * 1024; 58 private static final int MAXIMUM_SPEED_CHECK_COUNT = 5; // Checks only 5 times 59 private static final int MINIMUM_DISK_WRITE_SPEED_MBPS = 3; // 3 Megabytes per second 60 61 private final SampleChunk.SampleChunkCreator mSampleChunkCreator; 62 // Maps from track name to a map which maps from starting position to {@link SampleChunk}. 63 private final Map<String, SortedMap<Long, Pair<SampleChunk, Integer>>> mChunkMap = 64 new ArrayMap<>(); 65 private final Map<String, Long> mStartPositionMap = new ArrayMap<>(); 66 private final Map<String, ChunkEvictedListener> mEvictListeners = new ArrayMap<>(); 67 private final StorageManager mStorageManager; 68 private long mBufferSize = 0; 69 private final EvictChunkQueueMap mPendingDelete = new EvictChunkQueueMap(); 70 private final SampleChunk.ChunkCallback mChunkCallback = 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 volatile int mSpeedCheckCount; 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 * A Track format which will be loaded and saved from the permanent storage for recordings. 178 */ 179 public static class TrackFormat { 180 181 /** 182 * The track id for the specified track. The track id will be used as a track identifier 183 * for recordings. 184 */ 185 public final String trackId; 186 187 /** 188 * The {@link MediaFormat} for the specified track. 189 */ 190 public final MediaFormat format; 191 192 /** 193 * Creates TrackFormat. 194 * @param trackId 195 * @param format 196 */ TrackFormat(String trackId, MediaFormat format)197 public TrackFormat(String trackId, MediaFormat format) { 198 this.trackId = trackId; 199 this.format = format; 200 } 201 } 202 203 /** 204 * A Holder for a sample position which will be loaded from the index file for recordings. 205 */ 206 public static class PositionHolder { 207 208 /** 209 * The current sample position in microseconds. 210 * The position is identical to the PTS(presentation time stamp) of the sample. 211 */ 212 public final long positionUs; 213 214 /** 215 * Base sample position for the current {@link SampleChunk}. 216 */ 217 public final long basePositionUs; 218 219 /** 220 * The file offset for the current sample in the current {@link SampleChunk}. 221 */ 222 public final int offset; 223 224 /** 225 * Creates a holder for a specific position in the recording. 226 * @param positionUs 227 * @param offset 228 */ PositionHolder(long positionUs, long basePositionUs, int offset)229 public PositionHolder(long positionUs, long basePositionUs, int offset) { 230 this.positionUs = positionUs; 231 this.basePositionUs = basePositionUs; 232 this.offset = offset; 233 } 234 } 235 236 /** 237 * Storage configuration and policy manager for {@link BufferManager} 238 */ 239 public interface StorageManager { 240 241 /** 242 * Provides eligible storage directory for {@link BufferManager}. 243 * 244 * @return a directory to save buffer(chunks) and meta files 245 */ getBufferDir()246 File getBufferDir(); 247 248 /** 249 * Informs whether the storage is used for persistent use. (eg. dvr recording/play) 250 * 251 * @return {@code true} if stored files are persistent 252 */ isPersistent()253 boolean isPersistent(); 254 255 /** 256 * Informs whether the storage usage exceeds pre-determined size. 257 * 258 * @param bufferSize the current total usage of Storage in bytes. 259 * @param pendingDelete the current storage usage which will be deleted in near future by 260 * bytes 261 * @return {@code true} if it reached pre-determined max size 262 */ reachedStorageMax(long bufferSize, long pendingDelete)263 boolean reachedStorageMax(long bufferSize, long pendingDelete); 264 265 /** 266 * Informs whether the storage has enough remained space. 267 * 268 * @param pendingDelete the current storage usage which will be deleted in near future by 269 * bytes 270 * @return {@code true} if it has enough space 271 */ hasEnoughBuffer(long pendingDelete)272 boolean hasEnoughBuffer(long pendingDelete); 273 274 /** 275 * Reads track name & {@link MediaFormat} from storage. 276 * 277 * @param isAudio {@code true} if it is for audio track 278 * @return {@link List} of TrackFormat 279 */ readTrackInfoFiles(boolean isAudio)280 List<TrackFormat> readTrackInfoFiles(boolean isAudio); 281 282 /** 283 * Reads key sample positions for each written sample from storage. 284 * 285 * @param trackId track name 286 * @return indexes of the specified track 287 * @throws IOException 288 */ readIndexFile(String trackId)289 ArrayList<PositionHolder> readIndexFile(String trackId) throws IOException; 290 291 /** 292 * Writes track information to storage. 293 * 294 * @param formatList {@list List} of TrackFormat 295 * @param isAudio {@code true} if it is for audio track 296 * @throws IOException 297 */ writeTrackInfoFiles(List<TrackFormat> formatList, boolean isAudio)298 void writeTrackInfoFiles(List<TrackFormat> formatList, boolean isAudio) 299 throws IOException; 300 301 /** 302 * Writes index file to storage. 303 * 304 * @param trackName track name 305 * @param index {@link SampleChunk} container 306 * @throws IOException 307 */ writeIndexFile(String trackName, SortedMap<Long, Pair<SampleChunk, Integer>> index)308 void writeIndexFile(String trackName, SortedMap<Long, Pair<SampleChunk, Integer>> index) 309 throws IOException; 310 } 311 312 private static class EvictChunkQueueMap { 313 private final Map<String, LinkedList<SampleChunk>> mEvictMap = new ArrayMap<>(); 314 private long mSize; 315 init(String key)316 private void init(String key) { 317 mEvictMap.put(key, new LinkedList<>()); 318 } 319 add(String key, SampleChunk chunk)320 private void add(String key, SampleChunk chunk) { 321 LinkedList<SampleChunk> queue = mEvictMap.get(key); 322 if (queue != null) { 323 mSize += chunk.getSize(); 324 queue.add(chunk); 325 } 326 } 327 poll(String key, long startPositionUs)328 private SampleChunk poll(String key, long startPositionUs) { 329 LinkedList<SampleChunk> queue = mEvictMap.get(key); 330 if (queue != null) { 331 SampleChunk chunk = queue.peek(); 332 if (chunk != null && chunk.getStartPositionUs() < startPositionUs) { 333 mSize -= chunk.getSize(); 334 return queue.poll(); 335 } 336 } 337 return null; 338 } 339 getSize()340 private long getSize() { 341 return mSize; 342 } 343 release()344 private void release() { 345 for (Map.Entry<String, LinkedList<SampleChunk>> entry : mEvictMap.entrySet()) { 346 for (SampleChunk chunk : entry.getValue()) { 347 SampleChunk.IoState.release(chunk, true); 348 } 349 } 350 mEvictMap.clear(); 351 mSize = 0; 352 } 353 } 354 BufferManager(StorageManager storageManager)355 public BufferManager(StorageManager storageManager) { 356 this(storageManager, new SampleChunk.SampleChunkCreator()); 357 } 358 BufferManager(StorageManager storageManager, SampleChunk.SampleChunkCreator sampleChunkCreator)359 public BufferManager(StorageManager storageManager, 360 SampleChunk.SampleChunkCreator sampleChunkCreator) { 361 mStorageManager = storageManager; 362 mSampleChunkCreator = sampleChunkCreator; 363 } 364 registerChunkEvictedListener(String id, ChunkEvictedListener listener)365 public void registerChunkEvictedListener(String id, ChunkEvictedListener listener) { 366 mEvictListeners.put(id, listener); 367 } 368 unregisterChunkEvictedListener(String id)369 public void unregisterChunkEvictedListener(String id) { 370 mEvictListeners.remove(id); 371 } 372 getFileName(String id, long positionUs)373 private static String getFileName(String id, long positionUs) { 374 return String.format(Locale.ENGLISH, "%s_%016x.chunk", id, positionUs); 375 } 376 377 /** 378 * Creates a new {@link SampleChunk} for caching samples if it is needed. 379 * 380 * @param id the name of the track 381 * @param positionUs current position to write a sample in micro seconds. 382 * @param samplePool {@link SamplePool} for the fast creation of samples. 383 * @param currentChunk the current {@link SampleChunk} to write, {@code null} when to create 384 * a new {@link SampleChunk}. 385 * @param currentOffset the current offset to write. 386 * @return returns the created {@link SampleChunk}. 387 * @throws IOException 388 */ createNewWriteFileIfNeeded(String id, long positionUs, SamplePool samplePool, SampleChunk currentChunk, int currentOffset)389 public SampleChunk createNewWriteFileIfNeeded(String id, long positionUs, SamplePool samplePool, 390 SampleChunk currentChunk, int currentOffset) throws IOException { 391 if (!maybeEvictChunk()) { 392 throw new IOException("Not enough storage space"); 393 } 394 SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(id); 395 if (map == null) { 396 map = new TreeMap<>(); 397 mChunkMap.put(id, map); 398 mStartPositionMap.put(id, positionUs); 399 mPendingDelete.init(id); 400 } 401 if (currentChunk == null) { 402 File file = new File(mStorageManager.getBufferDir(), getFileName(id, positionUs)); 403 SampleChunk sampleChunk = mSampleChunkCreator 404 .createSampleChunk(samplePool, file, positionUs, mChunkCallback); 405 map.put(positionUs, new Pair(sampleChunk, 0)); 406 return sampleChunk; 407 } else { 408 map.put(positionUs, new Pair(currentChunk, currentOffset)); 409 return null; 410 } 411 } 412 413 /** 414 * Loads a track using {@link BufferManager.StorageManager}. 415 * 416 * @param trackId the name of the track. 417 * @param samplePool {@link SamplePool} for the fast creation of samples. 418 * @throws IOException 419 */ loadTrackFromStorage(String trackId, SamplePool samplePool)420 public void loadTrackFromStorage(String trackId, SamplePool samplePool) throws IOException { 421 ArrayList<PositionHolder> keyPositions = mStorageManager.readIndexFile(trackId); 422 long startPositionUs = keyPositions.size() > 0 ? keyPositions.get(0).positionUs : 0; 423 424 SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(trackId); 425 if (map == null) { 426 map = new TreeMap<>(); 427 mChunkMap.put(trackId, map); 428 mStartPositionMap.put(trackId, startPositionUs); 429 mPendingDelete.init(trackId); 430 } 431 SampleChunk chunk = null; 432 long basePositionUs = -1; 433 for (PositionHolder position: keyPositions) { 434 if (position.basePositionUs != basePositionUs) { 435 chunk = mSampleChunkCreator.loadSampleChunkFromFile(samplePool, 436 mStorageManager.getBufferDir(), getFileName(trackId, position.positionUs), 437 position.positionUs, mChunkCallback, chunk); 438 basePositionUs = position.basePositionUs; 439 } 440 map.put(position.positionUs, new Pair(chunk, position.offset)); 441 } 442 } 443 444 /** 445 * Finds a {@link SampleChunk} for the specified track name and the position. 446 * 447 * @param id the name of the track. 448 * @param positionUs the position. 449 * @return returns the found {@link SampleChunk}. 450 */ getReadFile(String id, long positionUs)451 public Pair<SampleChunk, Integer> getReadFile(String id, long positionUs) { 452 SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(id); 453 if (map == null) { 454 return null; 455 } 456 Pair<SampleChunk, Integer> ret; 457 SortedMap<Long, Pair<SampleChunk, Integer>> headMap = map.headMap(positionUs + 1); 458 if (!headMap.isEmpty()) { 459 ret = headMap.get(headMap.lastKey()); 460 } else { 461 ret = map.get(map.firstKey()); 462 } 463 return ret; 464 } 465 466 /** 467 * Evicts chunks which are ready to be evicted for the specified track 468 * 469 * @param id the specified track 470 * @param earlierThanPositionUs the start position of the {@link SampleChunk} 471 * should be earlier than 472 */ evictChunks(String id, long earlierThanPositionUs)473 public void evictChunks(String id, long earlierThanPositionUs) { 474 SampleChunk chunk = null; 475 while ((chunk = mPendingDelete.poll(id, earlierThanPositionUs)) != null) { 476 SampleChunk.IoState.release(chunk, !mStorageManager.isPersistent()) ; 477 } 478 } 479 480 /** 481 * Returns the start position of the specified track in micro seconds. 482 * 483 * @param id the specified track 484 */ getStartPositionUs(String id)485 public long getStartPositionUs(String id) { 486 Long ret = mStartPositionMap.get(id); 487 return ret == null ? 0 : ret; 488 } 489 maybeEvictChunk()490 private boolean maybeEvictChunk() { 491 long pendingDelete = mPendingDelete.getSize(); 492 while (mStorageManager.reachedStorageMax(mBufferSize, pendingDelete) 493 || !mStorageManager.hasEnoughBuffer(pendingDelete)) { 494 if (mStorageManager.isPersistent()) { 495 // Since chunks are persistent, we cannot evict chunks. 496 return false; 497 } 498 SortedMap<Long, Pair<SampleChunk, Integer>> earliestChunkMap = null; 499 SampleChunk earliestChunk = null; 500 String earliestChunkId = null; 501 for (Map.Entry<String, SortedMap<Long, Pair<SampleChunk, Integer>>> entry : 502 mChunkMap.entrySet()) { 503 SortedMap<Long, Pair<SampleChunk, Integer>> map = entry.getValue(); 504 if (map.isEmpty()) { 505 continue; 506 } 507 SampleChunk chunk = map.get(map.firstKey()).first; 508 if (earliestChunk == null 509 || chunk.getCreatedTimeMs() < earliestChunk.getCreatedTimeMs()) { 510 earliestChunkMap = map; 511 earliestChunk = chunk; 512 earliestChunkId = entry.getKey(); 513 } 514 } 515 if (earliestChunk == null) { 516 break; 517 } 518 mPendingDelete.add(earliestChunkId, earliestChunk); 519 earliestChunkMap.remove(earliestChunk.getStartPositionUs()); 520 if (DEBUG) { 521 Log.d(TAG, String.format("bufferSize = %d; pendingDelete = %b; " 522 + "earliestChunk size = %d; %s@%d (%s)", 523 mBufferSize, pendingDelete, earliestChunk.getSize(), earliestChunkId, 524 earliestChunk.getStartPositionUs(), 525 Utils.toIsoDateTimeString(earliestChunk.getCreatedTimeMs()))); 526 } 527 ChunkEvictedListener listener = mEvictListeners.get(earliestChunkId); 528 if (listener != null) { 529 listener.onChunkEvicted(earliestChunkId, earliestChunk.getCreatedTimeMs()); 530 } 531 pendingDelete = mPendingDelete.getSize(); 532 } 533 for (Map.Entry<String, SortedMap<Long, Pair<SampleChunk, Integer>>> entry : 534 mChunkMap.entrySet()) { 535 SortedMap<Long, Pair<SampleChunk, Integer>> map = entry.getValue(); 536 if (map.isEmpty()) { 537 continue; 538 } 539 mStartPositionMap.put(entry.getKey(), map.firstKey()); 540 } 541 return true; 542 } 543 544 /** 545 * Reads track information which includes {@link MediaFormat}. 546 * 547 * @return returns all track information which is found by {@link BufferManager.StorageManager}. 548 * @throws IOException 549 */ readTrackInfoFiles()550 public List<TrackFormat> readTrackInfoFiles() throws IOException { 551 List<TrackFormat> trackFormatList = new ArrayList<>(); 552 trackFormatList.addAll(mStorageManager.readTrackInfoFiles(false)); 553 trackFormatList.addAll(mStorageManager.readTrackInfoFiles(true)); 554 if (trackFormatList.isEmpty()) { 555 throw new IOException("No track information to load"); 556 } 557 return trackFormatList; 558 } 559 560 /** 561 * Writes track information and index information for all tracks. 562 * 563 * @param audios list of audio track information 564 * @param videos list of audio track information 565 * @throws IOException 566 */ writeMetaFiles(List<TrackFormat> audios, List<TrackFormat> videos)567 public void writeMetaFiles(List<TrackFormat> audios, List<TrackFormat> videos) 568 throws IOException { 569 if (audios.isEmpty() && videos.isEmpty()) { 570 throw new IOException("No track information to save"); 571 } 572 if (!audios.isEmpty()) { 573 mStorageManager.writeTrackInfoFiles(audios, true); 574 for (TrackFormat trackFormat : audios) { 575 SortedMap<Long, Pair<SampleChunk, Integer>> map = 576 mChunkMap.get(trackFormat.trackId); 577 if (map == null) { 578 throw new IOException("Audio track index missing"); 579 } 580 mStorageManager.writeIndexFile(trackFormat.trackId, map); 581 } 582 } 583 if (!videos.isEmpty()) { 584 mStorageManager.writeTrackInfoFiles(videos, false); 585 for (TrackFormat trackFormat : videos) { 586 SortedMap<Long, Pair<SampleChunk, Integer>> map = 587 mChunkMap.get(trackFormat.trackId); 588 if (map == null) { 589 throw new IOException("Video track index missing"); 590 } 591 mStorageManager.writeIndexFile(trackFormat.trackId, map); 592 } 593 } 594 } 595 596 /** 597 * Releases all the resources. 598 */ release()599 public void release() { 600 try { 601 mPendingDelete.release(); 602 for (Map.Entry<String, SortedMap<Long, Pair<SampleChunk, Integer>>> entry : 603 mChunkMap.entrySet()) { 604 SampleChunk toRelease = null; 605 for (Pair<SampleChunk, Integer> positions : entry.getValue().values()) { 606 if (toRelease != positions.first) { 607 toRelease = positions.first; 608 SampleChunk.IoState.release(toRelease, !mStorageManager.isPersistent()); 609 } 610 } 611 } 612 mChunkMap.clear(); 613 } catch (ConcurrentModificationException | NullPointerException e) { 614 // TODO: remove this after it it confirmed that race condition issues are resolved. 615 // b/32492258, b/32373376 616 SoftPreconditions.checkState(false, "Exception on BufferManager#release: ", 617 e.toString()); 618 } 619 } 620 resetWriteStat(float writeBandwidth)621 private void resetWriteStat(float writeBandwidth) { 622 mWriteBandwidth = writeBandwidth; 623 mTotalWriteSize = 0; 624 mTotalWriteTimeNs = 0; 625 } 626 627 /** 628 * Adds a disk write sample size to calculate the average disk write bandwidth. 629 */ addWriteStat(long size, long timeNs)630 public void addWriteStat(long size, long timeNs) { 631 if (size >= mMinSampleSizeForSpeedCheck) { 632 mTotalWriteSize += size; 633 mTotalWriteTimeNs += timeNs; 634 } 635 } 636 637 /** 638 * Returns if the average disk write bandwidth is slower than 639 * threshold {@code MINIMUM_DISK_WRITE_SPEED_MBPS}. 640 */ isWriteSlow()641 public boolean isWriteSlow() { 642 if (mTotalWriteSize < MINIMUM_WRITE_SIZE_FOR_SPEED_CHECK) { 643 return false; 644 } 645 646 // Checks write speed for only MAXIMUM_SPEED_CHECK_COUNT times to ignore outliers 647 // by temporary system overloading during the playback. 648 if (mSpeedCheckCount > MAXIMUM_SPEED_CHECK_COUNT) { 649 return false; 650 } 651 mSpeedCheckCount++; 652 float megabytePerSecond = calculateWriteBandwidth(); 653 resetWriteStat(megabytePerSecond); 654 if (DEBUG) { 655 Log.d(TAG, "Measured disk write performance: " + megabytePerSecond + "MBps"); 656 } 657 return megabytePerSecond < MINIMUM_DISK_WRITE_SPEED_MBPS; 658 } 659 660 /** 661 * Returns recent write bandwidth in MBps. If recent bandwidth is not available, 662 * returns {float -1.0f}. 663 */ getWriteBandwidth()664 public float getWriteBandwidth() { 665 return mWriteBandwidth == 0.0f ? -1.0f : mWriteBandwidth; 666 } 667 calculateWriteBandwidth()668 private float calculateWriteBandwidth() { 669 if (mTotalWriteTimeNs == 0) { 670 return -1; 671 } 672 return ((float) mTotalWriteSize * 1000 / mTotalWriteTimeNs); 673 } 674 675 /** 676 * Returns if {@link BufferManager} has checked the write speed, 677 * which is suitable for Trickplay. 678 */ 679 @VisibleForTesting hasSpeedCheckDone()680 public boolean hasSpeedCheckDone() { 681 return mSpeedCheckCount > 0; 682 } 683 684 /** 685 * Sets minimum sample size for write speed check. 686 * @param sampleSize minimum sample size for write speed check. 687 */ 688 @VisibleForTesting setMinimumSampleSizeForSpeedCheck(int sampleSize)689 public void setMinimumSampleSizeForSpeedCheck(int sampleSize) { 690 mMinSampleSizeForSpeedCheck = sampleSize; 691 } 692 } 693