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 17 package com.android.tv.tuner.exoplayer.buffer; 18 19 import android.support.annotation.Nullable; 20 import android.support.annotation.VisibleForTesting; 21 import android.util.Log; 22 import com.google.android.exoplayer.SampleHolder; 23 import java.io.File; 24 import java.io.IOException; 25 import java.io.RandomAccessFile; 26 import java.nio.channels.FileChannel; 27 28 /** 29 * {@link SampleChunk} stores samples into file and makes them available for read. Stored file = { 30 * Header, Sample } * N Header = sample size : int, sample flag : int, sample PTS in micro second : 31 * long 32 */ 33 public class SampleChunk { 34 private static final String TAG = "SampleChunk"; 35 private static final boolean DEBUG = false; 36 37 private final long mCreatedTimeMs; 38 private final long mStartPositionUs; 39 private SampleChunk mNextChunk; 40 41 // Header = sample size : int, sample flag : int, sample PTS in micro second : long 42 private static final int SAMPLE_HEADER_LENGTH = 16; 43 44 private final File mFile; 45 private final ChunkCallback mChunkCallback; 46 private final SamplePool mSamplePool; 47 private RandomAccessFile mAccessFile; 48 private long mWriteOffset; 49 private boolean mWriteFinished; 50 private boolean mIsReading; 51 private boolean mIsWriting; 52 53 /** A callback for chunks being committed to permanent storage. */ 54 public abstract static class ChunkCallback { 55 56 /** 57 * Notifies when writing a SampleChunk is completed. 58 * 59 * @param chunk SampleChunk which is written completely 60 */ onChunkWrite(SampleChunk chunk)61 public void onChunkWrite(SampleChunk chunk) {} 62 63 /** 64 * Notifies when a SampleChunk is deleted. 65 * 66 * @param chunk SampleChunk which is deleted from storage 67 */ onChunkDelete(SampleChunk chunk)68 public void onChunkDelete(SampleChunk chunk) {} 69 } 70 71 /** A class for SampleChunk creation. */ 72 public static class SampleChunkCreator { 73 74 /** 75 * Returns a newly created SampleChunk to read & write samples. 76 * 77 * @param samplePool sample allocator 78 * @param file filename which will be created newly 79 * @param startPositionUs the start position of the earliest sample to be stored 80 * @param chunkCallback for total storage usage change notification 81 */ 82 @VisibleForTesting createSampleChunk( SamplePool samplePool, File file, long startPositionUs, ChunkCallback chunkCallback)83 public SampleChunk createSampleChunk( 84 SamplePool samplePool, 85 File file, 86 long startPositionUs, 87 ChunkCallback chunkCallback) { 88 return new SampleChunk( 89 samplePool, file, startPositionUs, System.currentTimeMillis(), chunkCallback); 90 } 91 92 /** 93 * Returns a newly created SampleChunk which is backed by an existing file. Created 94 * SampleChunk is read-only. 95 * 96 * @param samplePool sample allocator 97 * @param bufferDir the directory where the file to read is located 98 * @param filename the filename which will be read afterwards 99 * @param startPositionUs the start position of the earliest sample in the file 100 * @param chunkCallback for total storage usage change notification 101 * @param prev the previous SampleChunk just before the newly created SampleChunk 102 * @throws IOException 103 */ loadSampleChunkFromFile( SamplePool samplePool, File bufferDir, String filename, long startPositionUs, ChunkCallback chunkCallback, SampleChunk prev)104 SampleChunk loadSampleChunkFromFile( 105 SamplePool samplePool, 106 File bufferDir, 107 String filename, 108 long startPositionUs, 109 ChunkCallback chunkCallback, 110 SampleChunk prev) 111 throws IOException { 112 File file = new File(bufferDir, filename); 113 SampleChunk chunk = new SampleChunk(samplePool, file, startPositionUs, chunkCallback); 114 if (prev != null) { 115 prev.mNextChunk = chunk; 116 } 117 return chunk; 118 } 119 } 120 121 /** 122 * Handles I/O for SampleChunk. Maintains current SampleChunk and the current offset for next 123 * I/O operation. 124 */ 125 @VisibleForTesting 126 public static class IoState { 127 private SampleChunk mChunk; 128 private long mCurrentOffset; 129 equals(SampleChunk chunk, long offset)130 private boolean equals(SampleChunk chunk, long offset) { 131 return chunk == mChunk && mCurrentOffset == offset; 132 } 133 134 /** Returns whether read I/O operation is finished. */ isReadFinished()135 boolean isReadFinished() { 136 return mChunk == null; 137 } 138 139 /** Returns the start position of the current SampleChunk */ getStartPositionUs()140 long getStartPositionUs() { 141 return mChunk == null ? 0 : mChunk.getStartPositionUs(); 142 } 143 reset(@ullable SampleChunk chunk)144 private void reset(@Nullable SampleChunk chunk) { 145 mChunk = chunk; 146 mCurrentOffset = 0; 147 } 148 reset(SampleChunk chunk, long offset)149 private void reset(SampleChunk chunk, long offset) { 150 mChunk = chunk; 151 mCurrentOffset = offset; 152 } 153 154 /** 155 * Prepares for read I/O operation from a new SampleChunk. 156 * 157 * @param chunk the new SampleChunk to read from 158 * @throws IOException 159 */ openRead(SampleChunk chunk, long offset)160 void openRead(SampleChunk chunk, long offset) throws IOException { 161 if (mChunk != null) { 162 mChunk.closeRead(); 163 } 164 chunk.openRead(); 165 reset(chunk, offset); 166 } 167 168 /** 169 * Prepares for write I/O operation to a new SampleChunk. 170 * 171 * @param chunk the new SampleChunk to write samples afterwards 172 * @throws IOException 173 */ openWrite(SampleChunk chunk)174 void openWrite(SampleChunk chunk) throws IOException { 175 if (mChunk != null) { 176 mChunk.closeWrite(chunk); 177 } 178 chunk.openWrite(); 179 reset(chunk); 180 } 181 182 /** 183 * Reads a sample if it is available. 184 * 185 * @return Returns a sample if it is available, null otherwise. 186 * @throws IOException 187 */ read()188 SampleHolder read() throws IOException { 189 if (mChunk != null && mChunk.isReadFinished(this)) { 190 SampleChunk next = mChunk.mNextChunk; 191 mChunk.closeRead(); 192 if (next != null) { 193 next.openRead(); 194 } 195 reset(next); 196 } 197 if (mChunk != null) { 198 try { 199 return mChunk.read(this); 200 } catch (IllegalStateException e) { 201 // Write is finished and there is no additional buffer to read. 202 Log.w(TAG, "Tried to read sample over EOS."); 203 return null; 204 } 205 } else { 206 return null; 207 } 208 } 209 210 /** 211 * Writes a sample. 212 * 213 * @param sample to write 214 * @param nextChunk if this is {@code null} writes at the current SampleChunk, otherwise 215 * close current SampleChunk and writes at this 216 * @throws IOException 217 */ write(SampleHolder sample, SampleChunk nextChunk)218 void write(SampleHolder sample, SampleChunk nextChunk) throws IOException { 219 if (mChunk == null) { 220 throw new IOException("mChunk should not be null"); 221 } 222 if (nextChunk != null) { 223 if (mChunk.mNextChunk != null) { 224 throw new IllegalStateException("Requested write for wrong SampleChunk"); 225 } 226 mChunk.closeWrite(nextChunk); 227 mChunk.mChunkCallback.onChunkWrite(mChunk); 228 nextChunk.openWrite(); 229 reset(nextChunk); 230 } 231 mChunk.write(sample, this); 232 } 233 234 /** 235 * Finishes write I/O operation. 236 * 237 * @throws IOException 238 */ closeWrite()239 void closeWrite() throws IOException { 240 if (mChunk != null) { 241 mChunk.closeWrite(null); 242 } 243 } 244 245 /** Returns the current SampleChunk for subsequent I/O operation. */ getChunk()246 SampleChunk getChunk() { 247 return mChunk; 248 } 249 250 /** Returns the current offset of the current SampleChunk for subsequent I/O operation. */ getOffset()251 long getOffset() { 252 return mCurrentOffset; 253 } 254 255 /** 256 * Releases SampleChunk. the SampleChunk will not be used anymore. 257 * 258 * @param chunk to release 259 * @param delete {@code true} when the backed file needs to be deleted, {@code false} 260 * otherwise. 261 */ release(SampleChunk chunk, boolean delete)262 static void release(SampleChunk chunk, boolean delete) { 263 chunk.release(delete); 264 } 265 } 266 267 @VisibleForTesting SampleChunk( SamplePool samplePool, File file, long startPositionUs, long createdTimeMs, ChunkCallback chunkCallback)268 protected SampleChunk( 269 SamplePool samplePool, 270 File file, 271 long startPositionUs, 272 long createdTimeMs, 273 ChunkCallback chunkCallback) { 274 mStartPositionUs = startPositionUs; 275 mCreatedTimeMs = createdTimeMs; 276 mSamplePool = samplePool; 277 mFile = file; 278 mChunkCallback = chunkCallback; 279 } 280 281 // Constructor of SampleChunk which is backed by the given existing file. SampleChunk( SamplePool samplePool, File file, long startPositionUs, ChunkCallback chunkCallback)282 private SampleChunk( 283 SamplePool samplePool, File file, long startPositionUs, ChunkCallback chunkCallback) 284 throws IOException { 285 mStartPositionUs = startPositionUs; 286 mCreatedTimeMs = mStartPositionUs / 1000; 287 mSamplePool = samplePool; 288 mFile = file; 289 mChunkCallback = chunkCallback; 290 mWriteFinished = true; 291 } 292 openRead()293 private void openRead() throws IOException { 294 if (!mIsReading) { 295 if (mAccessFile == null) { 296 mAccessFile = new RandomAccessFile(mFile, "r"); 297 } 298 if (mWriteFinished && mWriteOffset == 0) { 299 // Lazy loading of write offset, in order not to load 300 // all SampleChunk's write offset at start time of recorded playback. 301 mWriteOffset = mAccessFile.length(); 302 } 303 mIsReading = true; 304 } 305 } 306 openWrite()307 private void openWrite() throws IOException { 308 if (mWriteFinished) { 309 throw new IllegalStateException("Opened for write though write is already finished"); 310 } 311 if (!mIsWriting) { 312 if (mIsReading) { 313 throw new IllegalStateException( 314 "Write is requested for " + "an already opened SampleChunk"); 315 } 316 mAccessFile = new RandomAccessFile(mFile, "rw"); 317 mIsWriting = true; 318 } 319 } 320 CloseAccessFileIfNeeded()321 private void CloseAccessFileIfNeeded() throws IOException { 322 if (!mIsReading && !mIsWriting) { 323 try { 324 if (mAccessFile != null) { 325 mAccessFile.close(); 326 } 327 } finally { 328 mAccessFile = null; 329 } 330 } 331 } 332 closeRead()333 private void closeRead() throws IOException { 334 if (mIsReading) { 335 mIsReading = false; 336 CloseAccessFileIfNeeded(); 337 } 338 } 339 closeWrite(SampleChunk nextChunk)340 private void closeWrite(SampleChunk nextChunk) throws IOException { 341 if (mIsWriting) { 342 mNextChunk = nextChunk; 343 mIsWriting = false; 344 mWriteFinished = true; 345 CloseAccessFileIfNeeded(); 346 } 347 } 348 isReadFinished(IoState state)349 private boolean isReadFinished(IoState state) { 350 return mWriteFinished && state.equals(this, mWriteOffset); 351 } 352 read(IoState state)353 private SampleHolder read(IoState state) throws IOException { 354 if (mAccessFile == null || state.mChunk != this) { 355 throw new IllegalStateException("Requested read for wrong SampleChunk"); 356 } 357 long offset = state.mCurrentOffset; 358 if (offset >= mWriteOffset) { 359 if (mWriteFinished) { 360 throw new IllegalStateException("Requested read for wrong range"); 361 } else { 362 if (offset != mWriteOffset) { 363 Log.e(TAG, "This should not happen!"); 364 } 365 return null; 366 } 367 } 368 mAccessFile.seek(offset); 369 int size = mAccessFile.readInt(); 370 SampleHolder sample = mSamplePool.acquireSample(size); 371 sample.size = size; 372 sample.flags = mAccessFile.readInt(); 373 sample.timeUs = mAccessFile.readLong(); 374 sample.clearData(); 375 sample.data.put( 376 mAccessFile 377 .getChannel() 378 .map( 379 FileChannel.MapMode.READ_ONLY, 380 offset + SAMPLE_HEADER_LENGTH, 381 sample.size)); 382 offset += sample.size + SAMPLE_HEADER_LENGTH; 383 state.mCurrentOffset = offset; 384 return sample; 385 } 386 387 @VisibleForTesting write(SampleHolder sample, IoState state)388 protected void write(SampleHolder sample, IoState state) throws IOException { 389 if (mAccessFile == null || mNextChunk != null || !state.equals(this, mWriteOffset)) { 390 throw new IllegalStateException("Requested write for wrong SampleChunk"); 391 } 392 393 mAccessFile.seek(mWriteOffset); 394 mAccessFile.writeInt(sample.size); 395 mAccessFile.writeInt(sample.flags); 396 mAccessFile.writeLong(sample.timeUs); 397 sample.data.position(0).limit(sample.size); 398 mAccessFile.getChannel().position(mWriteOffset + SAMPLE_HEADER_LENGTH).write(sample.data); 399 mWriteOffset += sample.size + SAMPLE_HEADER_LENGTH; 400 state.mCurrentOffset = mWriteOffset; 401 } 402 release(boolean delete)403 private void release(boolean delete) { 404 mWriteFinished = true; 405 mIsReading = mIsWriting = false; 406 try { 407 if (mAccessFile != null) { 408 mAccessFile.close(); 409 } 410 } catch (IOException e) { 411 // Since the SampleChunk will not be reused, ignore exception. 412 } 413 if (delete) { 414 mFile.delete(); 415 mChunkCallback.onChunkDelete(this); 416 } 417 } 418 419 /** Returns the start position. */ getStartPositionUs()420 public long getStartPositionUs() { 421 return mStartPositionUs; 422 } 423 424 /** Returns the creation time. */ getCreatedTimeMs()425 public long getCreatedTimeMs() { 426 return mCreatedTimeMs; 427 } 428 429 /** Returns the current size. */ getSize()430 public long getSize() { 431 return mWriteOffset; 432 } 433 } 434