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 @VisibleForTesting 82 public static class SampleChunkCreator { 83 84 /** 85 * Returns a newly created SampleChunk to read & write samples. 86 * 87 * @param samplePool sample allocator 88 * @param file filename which will be created newly 89 * @param startPositionUs the start position of the earliest sample to be stored 90 * @param chunkCallback for total storage usage change notification 91 */ createSampleChunk(SamplePool samplePool, File file, long startPositionUs, ChunkCallback chunkCallback)92 SampleChunk createSampleChunk(SamplePool samplePool, File file, 93 long startPositionUs, ChunkCallback chunkCallback) { 94 return new SampleChunk(samplePool, file, startPositionUs, System.currentTimeMillis(), 95 chunkCallback); 96 } 97 98 /** 99 * Returns a newly created SampleChunk which is backed by an existing file. 100 * Created SampleChunk is read-only. 101 * 102 * @param samplePool sample allocator 103 * @param bufferDir the directory where the file to read is located 104 * @param filename the filename which will be read afterwards 105 * @param startPositionUs the start position of the earliest sample in the file 106 * @param chunkCallback for total storage usage change notification 107 * @param prev the previous SampleChunk just before the newly created SampleChunk 108 * @throws IOException 109 */ loadSampleChunkFromFile(SamplePool samplePool, File bufferDir, String filename, long startPositionUs, ChunkCallback chunkCallback, SampleChunk prev)110 SampleChunk loadSampleChunkFromFile(SamplePool samplePool, File bufferDir, 111 String filename, long startPositionUs, ChunkCallback chunkCallback, 112 SampleChunk prev) throws IOException { 113 File file = new File(bufferDir, filename); 114 SampleChunk chunk = 115 new SampleChunk(samplePool, file, startPositionUs, chunkCallback); 116 if (prev != null) { 117 prev.mNextChunk = chunk; 118 } 119 return chunk; 120 } 121 } 122 123 /** 124 * Handles I/O for SampleChunk. 125 * Maintains current SampleChunk and the current offset for next I/O operation. 126 */ 127 static class IoState { 128 private SampleChunk mChunk; 129 private long mCurrentOffset; 130 equals(SampleChunk chunk, long offset)131 private boolean equals(SampleChunk chunk, long offset) { 132 return chunk == mChunk && mCurrentOffset == offset; 133 } 134 135 /** 136 * Returns whether read I/O operation is finished. 137 */ isReadFinished()138 boolean isReadFinished() { 139 return mChunk == null; 140 } 141 142 /** 143 * Returns the start position of the current SampleChunk 144 */ getStartPositionUs()145 long getStartPositionUs() { 146 return mChunk == null ? 0 : mChunk.getStartPositionUs(); 147 } 148 reset(@ullable SampleChunk chunk)149 private void reset(@Nullable SampleChunk chunk) { 150 mChunk = chunk; 151 mCurrentOffset = 0; 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)160 void openRead(SampleChunk chunk) throws IOException { 161 if (mChunk != null) { 162 mChunk.closeRead(); 163 } 164 chunk.openRead(); 165 reset(chunk); 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, 215 * otherwise close current SampleChunk and writes at this 216 * @throws IOException 217 */ write(SampleHolder sample, SampleChunk nextChunk)218 void write(SampleHolder sample, SampleChunk nextChunk) 219 throws IOException { 220 if (nextChunk != null) { 221 if (mChunk == null || mChunk.mNextChunk != null) { 222 throw new IllegalStateException("Requested write for wrong SampleChunk"); 223 } 224 mChunk.closeWrite(nextChunk); 225 mChunk.mChunkCallback.onChunkWrite(mChunk); 226 nextChunk.openWrite(); 227 reset(nextChunk); 228 } 229 mChunk.write(sample, this); 230 } 231 232 /** 233 * Finishes write I/O operation. 234 * 235 * @throws IOException 236 */ closeWrite()237 void closeWrite() throws IOException { 238 if (mChunk != null) { 239 mChunk.closeWrite(null); 240 } 241 } 242 243 /** 244 * Releases SampleChunk. the SampleChunk will not be used anymore. 245 * 246 * @param chunk to release 247 * @param delete {@code true} when the backed file needs to be deleted, 248 * {@code false} otherwise. 249 */ release(SampleChunk chunk, boolean delete)250 static void release(SampleChunk chunk, boolean delete) { 251 chunk.release(delete); 252 } 253 } 254 255 @VisibleForTesting SampleChunk(SamplePool samplePool, File file, long startPositionUs, long createdTimeMs, ChunkCallback chunkCallback)256 protected SampleChunk(SamplePool samplePool, File file, long startPositionUs, 257 long createdTimeMs, ChunkCallback chunkCallback) { 258 mStartPositionUs = startPositionUs; 259 mCreatedTimeMs = createdTimeMs; 260 mSamplePool = samplePool; 261 mFile = file; 262 mChunkCallback = chunkCallback; 263 } 264 265 // Constructor of SampleChunk which is backed by the given existing file. SampleChunk(SamplePool samplePool, File file, long startPositionUs, ChunkCallback chunkCallback)266 private SampleChunk(SamplePool samplePool, File file, long startPositionUs, 267 ChunkCallback chunkCallback) throws IOException { 268 mStartPositionUs = startPositionUs; 269 mCreatedTimeMs = mStartPositionUs / 1000; 270 mSamplePool = samplePool; 271 mFile = file; 272 mChunkCallback = chunkCallback; 273 mWriteFinished = true; 274 } 275 openRead()276 private void openRead() throws IOException { 277 if (!mIsReading) { 278 if (mAccessFile == null) { 279 mAccessFile = new RandomAccessFile(mFile, "r"); 280 } 281 if (mWriteFinished && mWriteOffset == 0) { 282 // Lazy loading of write offset, in order not to load 283 // all SampleChunk's write offset at start time of recorded playback. 284 mWriteOffset = mAccessFile.length(); 285 } 286 mIsReading = true; 287 } 288 } 289 openWrite()290 private void openWrite() throws IOException { 291 if (mWriteFinished) { 292 throw new IllegalStateException("Opened for write though write is already finished"); 293 } 294 if (!mIsWriting) { 295 if (mIsReading) { 296 throw new IllegalStateException("Write is requested for " 297 + "an already opened SampleChunk"); 298 } 299 mAccessFile = new RandomAccessFile(mFile, "rw"); 300 mIsWriting = true; 301 } 302 } 303 CloseAccessFileIfNeeded()304 private void CloseAccessFileIfNeeded() throws IOException { 305 if (!mIsReading && !mIsWriting) { 306 try { 307 if (mAccessFile != null) { 308 mAccessFile.close(); 309 } 310 } finally { 311 mAccessFile = null; 312 } 313 } 314 } 315 closeRead()316 private void closeRead() throws IOException{ 317 if (mIsReading) { 318 mIsReading = false; 319 CloseAccessFileIfNeeded(); 320 } 321 } 322 closeWrite(SampleChunk nextChunk)323 private void closeWrite(SampleChunk nextChunk) 324 throws IOException { 325 if (mIsWriting) { 326 mNextChunk = nextChunk; 327 mIsWriting = false; 328 mWriteFinished = true; 329 CloseAccessFileIfNeeded(); 330 } 331 } 332 isReadFinished(IoState state)333 private boolean isReadFinished(IoState state) { 334 return mWriteFinished && state.equals(this, mWriteOffset); 335 } 336 read(IoState state)337 private SampleHolder read(IoState state) throws IOException { 338 if (mAccessFile == null || state.mChunk != this) { 339 throw new IllegalStateException("Requested read for wrong SampleChunk"); 340 } 341 long offset = state.mCurrentOffset; 342 if (offset >= mWriteOffset) { 343 if (mWriteFinished) { 344 throw new IllegalStateException("Requested read for wrong range"); 345 } else { 346 if (offset != mWriteOffset) { 347 Log.e(TAG, "This should not happen!"); 348 } 349 return null; 350 } 351 } 352 mAccessFile.seek(offset); 353 int size = mAccessFile.readInt(); 354 SampleHolder sample = mSamplePool.acquireSample(size); 355 sample.size = size; 356 sample.flags = mAccessFile.readInt(); 357 sample.timeUs = mAccessFile.readLong(); 358 sample.clearData(); 359 sample.data.put(mAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 360 offset + SAMPLE_HEADER_LENGTH, sample.size)); 361 offset += sample.size + SAMPLE_HEADER_LENGTH; 362 state.mCurrentOffset = offset; 363 return sample; 364 } 365 366 @VisibleForTesting write(SampleHolder sample, IoState state)367 protected void write(SampleHolder sample, IoState state) 368 throws IOException { 369 if (mAccessFile == null || mNextChunk != null || !state.equals(this, mWriteOffset)) { 370 throw new IllegalStateException("Requested write for wrong SampleChunk"); 371 } 372 373 mAccessFile.seek(mWriteOffset); 374 mAccessFile.writeInt(sample.size); 375 mAccessFile.writeInt(sample.flags); 376 mAccessFile.writeLong(sample.timeUs); 377 sample.data.position(0).limit(sample.size); 378 mAccessFile.getChannel().position(mWriteOffset + SAMPLE_HEADER_LENGTH).write(sample.data); 379 mWriteOffset += sample.size + SAMPLE_HEADER_LENGTH; 380 state.mCurrentOffset = mWriteOffset; 381 } 382 release(boolean delete)383 private void release(boolean delete) { 384 mWriteFinished = true; 385 mIsReading = mIsWriting = false; 386 try { 387 if (mAccessFile != null) { 388 mAccessFile.close(); 389 } 390 } catch (IOException e) { 391 // Since the SampleChunk will not be reused, ignore exception. 392 } 393 if (delete) { 394 mFile.delete(); 395 mChunkCallback.onChunkDelete(this); 396 } 397 } 398 399 /** 400 * Returns the start position. 401 */ getStartPositionUs()402 public long getStartPositionUs() { 403 return mStartPositionUs; 404 } 405 406 /** 407 * Returns the creation time. 408 */ getCreatedTimeMs()409 public long getCreatedTimeMs() { 410 return mCreatedTimeMs; 411 } 412 413 /** 414 * Returns the current size. 415 */ getSize()416 public long getSize() { 417 return mWriteOffset; 418 } 419 } 420