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 */ 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 */ 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 */ 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 */ 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 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 */ 137 boolean isReadFinished() { 138 return mChunk == null; 139 } 140 141 /** 142 * Returns the start position of the current SampleChunk 143 */ 144 long getStartPositionUs() { 145 return mChunk == null ? 0 : mChunk.getStartPositionUs(); 146 } 147 148 private void reset(@Nullable SampleChunk chunk) { 149 mChunk = chunk; 150 mCurrentOffset = 0; 151 } 152 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 250 SampleChunk getChunk() { 251 return mChunk; 252 } 253 254 /** 255 * Returns the current offset of the current SampleChunk for subsequent I/O operation. 256 */ 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 */ 268 static void release(SampleChunk chunk, boolean delete) { 269 chunk.release(delete); 270 } 271 } 272 273 @VisibleForTesting 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. 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 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 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 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 334 private void closeRead() throws IOException{ 335 if (mIsReading) { 336 mIsReading = false; 337 CloseAccessFileIfNeeded(); 338 } 339 } 340 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 351 private boolean isReadFinished(IoState state) { 352 return mWriteFinished && state.equals(this, mWriteOffset); 353 } 354 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 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 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 */ 420 public long getStartPositionUs() { 421 return mStartPositionUs; 422 } 423 424 /** 425 * Returns the creation time. 426 */ 427 public long getCreatedTimeMs() { 428 return mCreatedTimeMs; 429 } 430 431 /** 432 * Returns the current size. 433 */ 434 public long getSize() { 435 return mWriteOffset; 436 } 437 } 438