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 */ 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 */ 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 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 */ 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 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. */ 135 boolean isReadFinished() { 136 return mChunk == null; 137 } 138 139 /** Returns the start position of the current SampleChunk */ 140 long getStartPositionUs() { 141 return mChunk == null ? 0 : mChunk.getStartPositionUs(); 142 } 143 144 private void reset(@Nullable SampleChunk chunk) { 145 mChunk = chunk; 146 mCurrentOffset = 0; 147 } 148 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 */ 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 */ 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 */ 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 */ 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 */ 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. */ 246 SampleChunk getChunk() { 247 return mChunk; 248 } 249 250 /** Returns the current offset of the current SampleChunk for subsequent I/O operation. */ 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 */ 262 static void release(SampleChunk chunk, boolean delete) { 263 chunk.release(delete); 264 } 265 } 266 267 @VisibleForTesting 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. 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 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 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 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 333 private void closeRead() throws IOException { 334 if (mIsReading) { 335 mIsReading = false; 336 CloseAccessFileIfNeeded(); 337 } 338 } 339 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 349 private boolean isReadFinished(IoState state) { 350 return mWriteFinished && state.equals(this, mWriteOffset); 351 } 352 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 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 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. */ 420 public long getStartPositionUs() { 421 return mStartPositionUs; 422 } 423 424 /** Returns the creation time. */ 425 public long getCreatedTimeMs() { 426 return mCreatedTimeMs; 427 } 428 429 /** Returns the current size. */ 430 public long getSize() { 431 return mWriteOffset; 432 } 433 } 434