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.tv.tuner.exoplayer.buffer; 18 19import android.os.ConditionVariable; 20import android.support.annotation.IntDef; 21import android.support.annotation.NonNull; 22import android.util.Log; 23 24import com.google.android.exoplayer.C; 25import com.google.android.exoplayer.MediaFormat; 26import com.google.android.exoplayer.SampleHolder; 27import com.google.android.exoplayer.SampleSource; 28import com.google.android.exoplayer.util.Assertions; 29import com.android.tv.tuner.exoplayer.MpegTsPlayer; 30import com.android.tv.tuner.tvinput.PlaybackBufferListener; 31import com.android.tv.tuner.exoplayer.SampleExtractor; 32 33import java.io.IOException; 34import java.lang.annotation.Retention; 35import java.lang.annotation.RetentionPolicy; 36import java.util.ArrayList; 37import java.util.List; 38import java.util.concurrent.TimeUnit; 39 40/** 41 * Handles I/O between {@link SampleExtractor} and 42 * {@link BufferManager}.Reads & writes samples from/to {@link SampleChunk} which is backed 43 * by physical storage. 44 */ 45public class RecordingSampleBuffer implements BufferManager.SampleBuffer, 46 BufferManager.ChunkEvictedListener { 47 private static final String TAG = "RecordingSampleBuffer"; 48 49 @IntDef({BUFFER_REASON_LIVE_PLAYBACK, BUFFER_REASON_RECORDED_PLAYBACK, BUFFER_REASON_RECORDING}) 50 @Retention(RetentionPolicy.SOURCE) 51 public @interface BufferReason {} 52 53 /** 54 * A buffer reason for live-stream playback. 55 */ 56 public static final int BUFFER_REASON_LIVE_PLAYBACK = 0; 57 58 /** 59 * A buffer reason for playback of a recorded program. 60 */ 61 public static final int BUFFER_REASON_RECORDED_PLAYBACK = 1; 62 63 /** 64 * A buffer reason for recording a program. 65 */ 66 public static final int BUFFER_REASON_RECORDING = 2; 67 68 /** 69 * The minimum duration to support seek in Trickplay. 70 */ 71 static final long MIN_SEEK_DURATION_US = TimeUnit.MILLISECONDS.toMicros(500); 72 73 /** 74 * The duration of a {@link SampleChunk} for recordings. 75 */ 76 static final long RECORDING_CHUNK_DURATION_US = MIN_SEEK_DURATION_US * 1200; // 10 minutes 77 private static final long BUFFER_WRITE_TIMEOUT_MS = 10 * 1000; // 10 seconds 78 private static final long BUFFER_NEEDED_US = 79 1000L * Math.max(MpegTsPlayer.MIN_BUFFER_MS, MpegTsPlayer.MIN_REBUFFER_MS); 80 81 private final BufferManager mBufferManager; 82 private final PlaybackBufferListener mBufferListener; 83 private final @BufferReason int mBufferReason; 84 85 private int mTrackCount; 86 private boolean[] mTrackSelected; 87 private List<SampleQueue> mReadSampleQueues; 88 private final SamplePool mSamplePool = new SamplePool(); 89 private long mLastBufferedPositionUs = C.UNKNOWN_TIME_US; 90 private long mCurrentPlaybackPositionUs = 0; 91 92 // An error in I/O thread of {@link SampleChunkIoHelper} will be notified. 93 private volatile boolean mError; 94 95 // Eos was reached in I/O thread of {@link SampleChunkIoHelper}. 96 private volatile boolean mEos; 97 private SampleChunkIoHelper mSampleChunkIoHelper; 98 private final SampleChunkIoHelper.IoCallback mIoCallback = 99 new SampleChunkIoHelper.IoCallback() { 100 @Override 101 public void onIoReachedEos() { 102 mEos = true; 103 } 104 105 @Override 106 public void onIoError() { 107 mError = true; 108 } 109 }; 110 111 /** 112 * Creates {@link BufferManager.SampleBuffer} with 113 * cached I/O backed by physical storage (e.g. trickplay,recording,recorded-playback). 114 * 115 * @param bufferManager the manager of {@link SampleChunk} 116 * @param bufferListener the listener for buffer I/O event 117 * @param enableTrickplay {@code true} when trickplay should be enabled 118 * @param bufferReason the reason for caching samples {@link RecordingSampleBuffer.BufferReason} 119 */ 120 public RecordingSampleBuffer(BufferManager bufferManager, PlaybackBufferListener bufferListener, 121 boolean enableTrickplay, @BufferReason int bufferReason) { 122 mBufferManager = bufferManager; 123 mBufferListener = bufferListener; 124 if (bufferListener != null) { 125 bufferListener.onBufferStateChanged(enableTrickplay); 126 } 127 mBufferReason = bufferReason; 128 } 129 130 @Override 131 public void init(@NonNull List<String> ids, @NonNull List<MediaFormat> mediaFormats) 132 throws IOException { 133 mTrackCount = ids.size(); 134 if (mTrackCount <= 0) { 135 throw new IOException("No tracks to initialize"); 136 } 137 mTrackSelected = new boolean[mTrackCount]; 138 mReadSampleQueues = new ArrayList<>(); 139 mSampleChunkIoHelper = new SampleChunkIoHelper(ids, mediaFormats, mBufferReason, 140 mBufferManager, mSamplePool, mIoCallback); 141 for (int i = 0; i < mTrackCount; ++i) { 142 mReadSampleQueues.add(i, new SampleQueue(mSamplePool)); 143 } 144 mSampleChunkIoHelper.init(); 145 for (int i = 0; i < mTrackCount; ++i) { 146 mBufferManager.registerChunkEvictedListener(ids.get(i), RecordingSampleBuffer.this); 147 } 148 } 149 150 @Override 151 public void selectTrack(int index) { 152 if (!mTrackSelected[index]) { 153 mTrackSelected[index] = true; 154 mReadSampleQueues.get(index).clear(); 155 mSampleChunkIoHelper.openRead(index, mCurrentPlaybackPositionUs); 156 } 157 } 158 159 @Override 160 public void deselectTrack(int index) { 161 if (mTrackSelected[index]) { 162 mTrackSelected[index] = false; 163 mReadSampleQueues.get(index).clear(); 164 mSampleChunkIoHelper.closeRead(index); 165 } 166 } 167 168 @Override 169 public void writeSample(int index, SampleHolder sample, ConditionVariable conditionVariable) 170 throws IOException { 171 mSampleChunkIoHelper.writeSample(index, sample, conditionVariable); 172 173 if (!conditionVariable.block(BUFFER_WRITE_TIMEOUT_MS)) { 174 Log.e(TAG, "Error: Serious delay on writing buffer"); 175 conditionVariable.block(); 176 } 177 } 178 179 @Override 180 public boolean isWriteSpeedSlow(int sampleSize, long writeDurationNs) { 181 if (mBufferReason == BUFFER_REASON_RECORDED_PLAYBACK) { 182 return false; 183 } 184 mBufferManager.addWriteStat(sampleSize, writeDurationNs); 185 return mBufferManager.isWriteSlow(); 186 } 187 188 @Override 189 public void handleWriteSpeedSlow() throws IOException{ 190 if (mBufferReason == BUFFER_REASON_RECORDING) { 191 // Recording does not need to stop because I/O speed is slow temporarily. 192 // If fixed size buffer of TsStreamer overflows, TsDataSource will reach EoS. 193 // Reaching EoS will stop recording eventually. 194 Log.w(TAG, "Disk I/O speed is slow for recording temporarily: " 195 + mBufferManager.getWriteBandwidth() + "MBps"); 196 return; 197 } 198 // Disables buffering samples afterwards, and notifies the disk speed is slow. 199 Log.w(TAG, "Disk is too slow for trickplay"); 200 mBufferListener.onDiskTooSlow(); 201 } 202 203 @Override 204 public void setEos() { 205 mSampleChunkIoHelper.closeWrite(); 206 } 207 208 private boolean maybeReadSample(SampleQueue queue, int index) { 209 if (queue.getLastQueuedPositionUs() != null 210 && queue.getLastQueuedPositionUs() > mCurrentPlaybackPositionUs + BUFFER_NEEDED_US 211 && queue.isDurationGreaterThan(MIN_SEEK_DURATION_US)) { 212 // The speed of queuing samples can be higher than the playback speed. 213 // If the duration of the samples in the queue is not limited, 214 // samples can be accumulated and there can be out-of-memory issues. 215 // But, the throttling should provide enough samples for the player to 216 // finish the buffering state. 217 return false; 218 } 219 SampleHolder sample = mSampleChunkIoHelper.readSample(index); 220 if (sample != null) { 221 queue.queueSample(sample); 222 return true; 223 } 224 return false; 225 } 226 227 @Override 228 public int readSample(int track, SampleHolder outSample) { 229 Assertions.checkState(mTrackSelected[track]); 230 maybeReadSample(mReadSampleQueues.get(track), track); 231 int result = mReadSampleQueues.get(track).dequeueSample(outSample); 232 if ((result != SampleSource.SAMPLE_READ && mEos) || mError) { 233 return SampleSource.END_OF_STREAM; 234 } 235 return result; 236 } 237 238 @Override 239 public void seekTo(long positionUs) { 240 for (int i = 0; i < mTrackCount; ++i) { 241 if (mTrackSelected[i]) { 242 mReadSampleQueues.get(i).clear(); 243 mSampleChunkIoHelper.openRead(i, positionUs); 244 } 245 } 246 mLastBufferedPositionUs = positionUs; 247 } 248 249 @Override 250 public long getBufferedPositionUs() { 251 Long result = null; 252 for (int i = 0; i < mTrackCount; ++i) { 253 if (!mTrackSelected[i]) { 254 continue; 255 } 256 Long lastQueuedSamplePositionUs = 257 mReadSampleQueues.get(i).getLastQueuedPositionUs(); 258 if (lastQueuedSamplePositionUs == null) { 259 // No sample has been queued. 260 result = mLastBufferedPositionUs; 261 continue; 262 } 263 if (result == null || result > lastQueuedSamplePositionUs) { 264 result = lastQueuedSamplePositionUs; 265 } 266 } 267 if (result == null) { 268 return mLastBufferedPositionUs; 269 } 270 return (mLastBufferedPositionUs = result); 271 } 272 273 @Override 274 public boolean continueBuffering(long positionUs) { 275 mCurrentPlaybackPositionUs = positionUs; 276 for (int i = 0; i < mTrackCount; ++i) { 277 if (!mTrackSelected[i]) { 278 continue; 279 } 280 SampleQueue queue = mReadSampleQueues.get(i); 281 maybeReadSample(queue, i); 282 if (queue.getLastQueuedPositionUs() == null 283 || positionUs > queue.getLastQueuedPositionUs()) { 284 // No more buffered data. 285 return false; 286 } 287 } 288 return true; 289 } 290 291 @Override 292 public void release() throws IOException { 293 if (mTrackCount <= 0) { 294 return; 295 } 296 if (mSampleChunkIoHelper != null) { 297 mSampleChunkIoHelper.release(); 298 } 299 } 300 301 // onChunkEvictedListener 302 @Override 303 public void onChunkEvicted(String id, long createdTimeMs) { 304 if (mBufferListener != null) { 305 mBufferListener.onBufferStartTimeChanged( 306 createdTimeMs + TimeUnit.MICROSECONDS.toMillis(MIN_SEEK_DURATION_US)); 307 } 308 } 309} 310