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