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