1/* 2 * Copyright (C) 2016 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 17package com.android.usbtuner.exoplayer.cache; 18 19import android.media.MediaCodec; 20import android.media.MediaFormat; 21import android.os.ConditionVariable; 22import android.support.annotation.IntDef; 23import android.util.Log; 24import android.util.Pair; 25 26import com.google.android.exoplayer.C; 27import com.google.android.exoplayer.SampleHolder; 28import com.google.android.exoplayer.SampleSource; 29import com.google.android.exoplayer.util.MimeTypes; 30import com.android.usbtuner.tvinput.PlaybackCacheListener; 31 32import java.io.IOException; 33import java.lang.annotation.Retention; 34import java.lang.annotation.RetentionPolicy; 35import java.util.List; 36import java.util.concurrent.TimeUnit; 37 38import junit.framework.Assert; 39 40/** 41 * Handles I/O between {@link com.android.usbtuner.exoplayer.SampleExtractor} and 42 * {@link CacheManager}.Reads & writes samples from/to {@link SampleCache} which is backed 43 * by physical storage. 44 */ 45public class RecordingSampleBuffer implements CacheManager.SampleBuffer, 46 CacheManager.EvictListener { 47 private static final String TAG = "RecordingSampleBuffer"; 48 private static final boolean DEBUG = false; 49 50 @IntDef({CACHE_REASON_LIVE_PLAYBACK, CACHE_REASON_RECORDED_PLAYBACK, CACHE_REASON_RECORDING}) 51 @Retention(RetentionPolicy.SOURCE) 52 public @interface CacheReason {} 53 54 /** 55 * A cache reason for live-stream playback. 56 */ 57 public static final int CACHE_REASON_LIVE_PLAYBACK = 0; 58 59 /** 60 * A cache reason for playback of a recorded program. 61 */ 62 public static final int CACHE_REASON_RECORDED_PLAYBACK = 1; 63 64 /** 65 * A cache reason for recording a program. 66 */ 67 public static final int CACHE_REASON_RECORDING = 2; 68 69 private static final long CACHE_WRITE_TIMEOUT_MS = 10 * 1000; // 10 seconds 70 private static final long CHUNK_DURATION_US = TimeUnit.MILLISECONDS.toMicros(500); 71 private static final long LIVE_THRESHOLD_US = TimeUnit.SECONDS.toMicros(1); 72 73 private final CacheManager mCacheManager; 74 private final PlaybackCacheListener mCacheListener; 75 private final int mCacheReason; 76 77 private int mTrackCount; 78 private List<String> mIds; 79 private List<MediaFormat> mMediaFormats; 80 private volatile long mCacheDurationUs = 0; 81 private long[] mCacheEndPositionUs; 82 // SampleCache to append the latest live sample. 83 private SampleCache[] mSampleCaches; 84 private CachedSampleQueue[] mPlayingSampleQueues; 85 private final SamplePool mSamplePool = new SamplePool(); 86 private long mLastBufferedPositionUs = C.UNKNOWN_TIME_US; 87 private long mCurrentPlaybackPositionUs = 0; 88 private boolean mEos = false; 89 90 private class CachedSampleQueue extends SampleQueue { 91 private SampleCache mCache = null; 92 93 public CachedSampleQueue(SamplePool samplePool) { 94 super(samplePool); 95 } 96 97 public void setSource(SampleCache newCache) { 98 for (SampleCache cache = mCache; cache != null; cache = cache.getNext()) { 99 cache.clear(); 100 cache.close(); 101 } 102 mCache = newCache; 103 for (SampleCache cache = mCache; cache != null; cache = cache.getNext()) { 104 cache.resetRead(); 105 } 106 } 107 108 public boolean maybeReadSample() { 109 if (isDurationGreaterThan(CHUNK_DURATION_US)) { 110 return false; 111 } 112 SampleHolder sample = mCache.maybeReadSample(); 113 if (sample == null) { 114 if (!mCache.canReadMore() && mCache.getNext() != null) { 115 mCache.clear(); 116 mCache.close(); 117 mCache = mCache.getNext(); 118 mCache.resetRead(); 119 return maybeReadSample(); 120 } else { 121 if (mCacheReason == CACHE_REASON_RECORDED_PLAYBACK 122 && !mCache.canReadMore() && mCache.getNext() == null) { 123 // At the end of the recorded playback. 124 setEos(); 125 } 126 return false; 127 } 128 } else { 129 queueSample(sample); 130 return true; 131 } 132 } 133 134 public int dequeueSample(SampleHolder sample) { 135 maybeReadSample(); 136 return super.dequeueSample(sample); 137 } 138 139 @Override 140 public void clear() { 141 super.clear(); 142 for (SampleCache cache = mCache; cache != null; cache = cache.getNext()) { 143 cache.clear(); 144 cache.close(); 145 } 146 mCache = null; 147 } 148 149 public long getSourceStartPositionUs() { 150 return mCache == null ? -1 : mCache.getStartPositionUs(); 151 } 152 } 153 154 /** 155 * Creates {@link com.android.usbtuner.exoplayer.cache.CacheManager.SampleBuffer} with 156 * cached I/O backed by physical storage (e.g. trickplay,recording,recorded-playback). 157 * 158 * @param cacheManager 159 * @param cacheListener 160 * @param enableTrickplay {@code true} when trickplay should be enabled 161 * @param cacheReason the reason for caching samples {@link RecordingSampleBuffer.CacheReason} 162 */ 163 public RecordingSampleBuffer(CacheManager cacheManager, PlaybackCacheListener cacheListener, 164 boolean enableTrickplay, @CacheReason int cacheReason) { 165 mCacheManager = cacheManager; 166 mCacheListener = cacheListener; 167 if (cacheListener != null) { 168 cacheListener.onCacheStateChanged(enableTrickplay); 169 } 170 mCacheReason = cacheReason; 171 } 172 173 private String getTrackId(int index) { 174 return mIds.get(index); 175 } 176 177 @Override 178 public synchronized void init(List<String> ids, List<MediaFormat> mediaFormats) 179 throws IOException { 180 mTrackCount = ids.size(); 181 if (mTrackCount <= 0) { 182 throw new IOException("No tracks to initialize"); 183 } 184 mIds = ids; 185 if (mCacheReason == CACHE_REASON_RECORDING && mediaFormats == null) { 186 throw new IOException("MediaFormat is not provided."); 187 } 188 mMediaFormats = mediaFormats; 189 mSampleCaches = new SampleCache[mTrackCount]; 190 mPlayingSampleQueues = new CachedSampleQueue[mTrackCount]; 191 mCacheEndPositionUs = new long[mTrackCount]; 192 for (int i = 0; i < mTrackCount; i++) { 193 if (mCacheReason != CACHE_REASON_RECORDED_PLAYBACK) { 194 mSampleCaches[i] = mCacheManager.createNewWriteFile(getTrackId(i), 0, mSamplePool); 195 mPlayingSampleQueues[i] = null; 196 mCacheEndPositionUs[i] = CHUNK_DURATION_US; 197 } else { 198 mCacheManager.loadTrackFormStorage(mIds.get(i), mSamplePool); 199 } 200 } 201 } 202 203 private boolean isLiveLocked(long positionUs) { 204 Long livePositionUs = null; 205 for (SampleCache cache : mSampleCaches) { 206 if (livePositionUs == null || livePositionUs < cache.getEndPositionUs()) { 207 livePositionUs = cache.getEndPositionUs(); 208 } 209 } 210 return (livePositionUs == null 211 || Math.abs(livePositionUs - positionUs) < LIVE_THRESHOLD_US); 212 } 213 214 private void seekIndividualTrackLocked(int index, long positionUs, boolean isLive) { 215 CachedSampleQueue queue = mPlayingSampleQueues[index]; 216 if (queue == null) { 217 return; 218 } 219 queue.clear(); 220 if (isLive) { 221 queue.setSource(mSampleCaches[index]); 222 } else { 223 queue.setSource(mCacheManager.getReadFile(getTrackId(index), positionUs)); 224 } 225 queue.maybeReadSample(); 226 } 227 228 @Override 229 public synchronized void selectTrack(int index) { 230 if (mPlayingSampleQueues[index] == null) { 231 String trackId = getTrackId(index); 232 mPlayingSampleQueues[index] = new CachedSampleQueue(mSamplePool); 233 mCacheManager.registerEvictListener(trackId, this); 234 seekIndividualTrackLocked(index, mCurrentPlaybackPositionUs, 235 mCacheReason != CACHE_REASON_RECORDED_PLAYBACK && isLiveLocked( 236 mCurrentPlaybackPositionUs)); 237 mPlayingSampleQueues[index].maybeReadSample(); 238 } 239 } 240 241 @Override 242 public synchronized void deselectTrack(int index) { 243 if (mPlayingSampleQueues[index] != null) { 244 mPlayingSampleQueues[index].clear(); 245 mPlayingSampleQueues[index] = null; 246 mCacheManager.unregisterEvictListener(getTrackId(index)); 247 } 248 } 249 250 @Override 251 public void writeSample(int index, SampleHolder sample, 252 ConditionVariable conditionVariable) throws IOException { 253 synchronized (this) { 254 SampleCache cache = mSampleCaches[index]; 255 if ((sample.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) { 256 if (sample.timeUs > mCacheDurationUs) { 257 mCacheDurationUs = sample.timeUs; 258 } 259 if (sample.timeUs >= mCacheEndPositionUs[index]) { 260 try { 261 SampleCache nextCache = mCacheManager.createNewWriteFile( 262 getTrackId(index), mCacheEndPositionUs[index], mSamplePool); 263 cache.finishWrite(nextCache); 264 mSampleCaches[index] = cache = nextCache; 265 mCacheEndPositionUs[index] = 266 ((sample.timeUs / CHUNK_DURATION_US) + 1) * CHUNK_DURATION_US; 267 } catch (IOException e) { 268 cache.finishWrite(null); 269 throw e; 270 } 271 } 272 } 273 cache.writeSample(sample, conditionVariable); 274 } 275 276 if (!conditionVariable.block(CACHE_WRITE_TIMEOUT_MS)) { 277 Log.e(TAG, "Error: Serious delay on writing cache"); 278 conditionVariable.block(); 279 } 280 } 281 282 @Override 283 public boolean isWriteSpeedSlow(int sampleSize, long writeDurationNs) { 284 if (mCacheReason == CACHE_REASON_RECORDED_PLAYBACK) { 285 return false; 286 } 287 mCacheManager.addWriteStat(sampleSize, writeDurationNs); 288 return mCacheManager.isWriteSlow(); 289 } 290 291 @Override 292 public void handleWriteSpeedSlow() { 293 Log.w(TAG, "Disk is too slow for trickplay. Disable trickplay."); 294 mCacheManager.disable(); 295 mCacheListener.onDiskTooSlow(); 296 } 297 298 @Override 299 public synchronized void setEos() { 300 mEos = true; 301 } 302 303 private synchronized boolean reachedEos() { 304 return mEos; 305 } 306 307 @Override 308 public synchronized int readSample(int track, SampleHolder sampleHolder) { 309 CachedSampleQueue queue = mPlayingSampleQueues[track]; 310 Assert.assertNotNull(queue); 311 queue.maybeReadSample(); 312 int result = queue.dequeueSample(sampleHolder); 313 if (result != SampleSource.SAMPLE_READ && reachedEos()) { 314 return SampleSource.END_OF_STREAM; 315 } 316 return result; 317 } 318 319 @Override 320 public synchronized void seekTo(long positionUs) { 321 boolean isLive = mCacheReason != CACHE_REASON_RECORDED_PLAYBACK && isLiveLocked(positionUs); 322 323 // Seek video track first 324 for (int i = 0; i < mPlayingSampleQueues.length; ++i) { 325 CachedSampleQueue queue = mPlayingSampleQueues[i]; 326 if (queue == null) { 327 continue; 328 } 329 seekIndividualTrackLocked(i, positionUs, isLive); 330 if (DEBUG) { 331 Log.d(TAG, "start time = " + queue.getSourceStartPositionUs()); 332 } 333 } 334 mLastBufferedPositionUs = positionUs; 335 } 336 337 @Override 338 public synchronized long getBufferedPositionUs() { 339 Long result = null; 340 for (CachedSampleQueue queue : mPlayingSampleQueues) { 341 if (queue == null) { 342 continue; 343 } 344 Long bufferedPositionUs = queue.getEndPositionUs(); 345 if (bufferedPositionUs == null) { 346 continue; 347 } 348 if (result == null || result > bufferedPositionUs) { 349 result = bufferedPositionUs; 350 } 351 } 352 if (result == null) { 353 return mLastBufferedPositionUs; 354 } else { 355 return (mLastBufferedPositionUs = result); 356 } 357 } 358 359 @Override 360 public synchronized boolean continueBuffering(long positionUs) { 361 boolean hasSamples = true; 362 mCurrentPlaybackPositionUs = positionUs; 363 for (CachedSampleQueue queue : mPlayingSampleQueues) { 364 if (queue == null) { 365 continue; 366 } 367 queue.maybeReadSample(); 368 if (queue.isEmpty()) { 369 hasSamples = false; 370 } 371 } 372 return hasSamples; 373 } 374 375 @Override 376 public synchronized void release() { 377 if (mSampleCaches == null) { 378 return; 379 } 380 if (mCacheReason == CACHE_REASON_RECORDED_PLAYBACK) { 381 mCacheManager.close(); 382 } 383 for (int i = 0; i < mTrackCount; ++i) { 384 if (mCacheReason != CACHE_REASON_RECORDED_PLAYBACK) { 385 mSampleCaches[i].finishWrite(null); 386 } 387 mCacheManager.unregisterEvictListener(getTrackId(i)); 388 } 389 if (mCacheReason == CACHE_REASON_RECORDING && mTrackCount > 0) { 390 // Saves meta information for recording. 391 Pair<String, android.media.MediaFormat> audio = null, video = null; 392 for (int i = 0; i < mTrackCount; ++i) { 393 MediaFormat mediaFormat = mMediaFormats.get(i); 394 String mime = mediaFormat.getString(MediaFormat.KEY_MIME); 395 mediaFormat.setLong(android.media.MediaFormat.KEY_DURATION, mCacheDurationUs); 396 if (MimeTypes.isAudio(mime)) { 397 audio = new Pair<>(getTrackId(i), mediaFormat); 398 } 399 else if (MimeTypes.isVideo(mime)) { 400 video = new Pair<>(getTrackId(i), mediaFormat); 401 } 402 } 403 mCacheManager.writeMetaFiles(audio, video); 404 } 405 406 for (int i = 0; i < mTrackCount; ++i) { 407 mCacheManager.clearTrack(getTrackId(i)); 408 } 409 } 410 411 // CacheEvictListener 412 @Override 413 public void onCacheEvicted(String id, long createdTimeMs) { 414 if (mCacheListener != null) { 415 mCacheListener.onCacheStartTimeChanged( 416 createdTimeMs + TimeUnit.MICROSECONDS.toMillis(CHUNK_DURATION_US)); 417 } 418 } 419} 420