diff options
Diffstat (limited to 'tuner/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java')
-rw-r--r-- | tuner/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java | 433 |
1 files changed, 433 insertions, 0 deletions
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java b/tuner/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java new file mode 100644 index 00000000..bf77a6eb --- /dev/null +++ b/tuner/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java @@ -0,0 +1,433 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.tuner.exoplayer.buffer; + +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.util.Log; +import com.google.android.exoplayer.SampleHolder; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.channels.FileChannel; + +/** + * {@link SampleChunk} stores samples into file and makes them available for read. Stored file = { + * Header, Sample } * N Header = sample size : int, sample flag : int, sample PTS in micro second : + * long + */ +public class SampleChunk { + private static final String TAG = "SampleChunk"; + private static final boolean DEBUG = false; + + private final long mCreatedTimeMs; + private final long mStartPositionUs; + private SampleChunk mNextChunk; + + // Header = sample size : int, sample flag : int, sample PTS in micro second : long + private static final int SAMPLE_HEADER_LENGTH = 16; + + private final File mFile; + private final ChunkCallback mChunkCallback; + private final SamplePool mSamplePool; + private RandomAccessFile mAccessFile; + private long mWriteOffset; + private boolean mWriteFinished; + private boolean mIsReading; + private boolean mIsWriting; + + /** A callback for chunks being committed to permanent storage. */ + public abstract static class ChunkCallback { + + /** + * Notifies when writing a SampleChunk is completed. + * + * @param chunk SampleChunk which is written completely + */ + public void onChunkWrite(SampleChunk chunk) {} + + /** + * Notifies when a SampleChunk is deleted. + * + * @param chunk SampleChunk which is deleted from storage + */ + public void onChunkDelete(SampleChunk chunk) {} + } + + /** A class for SampleChunk creation. */ + public static class SampleChunkCreator { + + /** + * Returns a newly created SampleChunk to read & write samples. + * + * @param samplePool sample allocator + * @param file filename which will be created newly + * @param startPositionUs the start position of the earliest sample to be stored + * @param chunkCallback for total storage usage change notification + */ + @VisibleForTesting + public SampleChunk createSampleChunk( + SamplePool samplePool, + File file, + long startPositionUs, + ChunkCallback chunkCallback) { + return new SampleChunk( + samplePool, file, startPositionUs, System.currentTimeMillis(), chunkCallback); + } + + /** + * Returns a newly created SampleChunk which is backed by an existing file. Created + * SampleChunk is read-only. + * + * @param samplePool sample allocator + * @param bufferDir the directory where the file to read is located + * @param filename the filename which will be read afterwards + * @param startPositionUs the start position of the earliest sample in the file + * @param chunkCallback for total storage usage change notification + * @param prev the previous SampleChunk just before the newly created SampleChunk + * @throws IOException + */ + SampleChunk loadSampleChunkFromFile( + SamplePool samplePool, + File bufferDir, + String filename, + long startPositionUs, + ChunkCallback chunkCallback, + SampleChunk prev) + throws IOException { + File file = new File(bufferDir, filename); + SampleChunk chunk = new SampleChunk(samplePool, file, startPositionUs, chunkCallback); + if (prev != null) { + prev.mNextChunk = chunk; + } + return chunk; + } + } + + /** + * Handles I/O for SampleChunk. Maintains current SampleChunk and the current offset for next + * I/O operation. + */ + @VisibleForTesting + public static class IoState { + private SampleChunk mChunk; + private long mCurrentOffset; + + private boolean equals(SampleChunk chunk, long offset) { + return chunk == mChunk && mCurrentOffset == offset; + } + + /** Returns whether read I/O operation is finished. */ + boolean isReadFinished() { + return mChunk == null; + } + + /** Returns the start position of the current SampleChunk */ + long getStartPositionUs() { + return mChunk == null ? 0 : mChunk.getStartPositionUs(); + } + + private void reset(@Nullable SampleChunk chunk) { + mChunk = chunk; + mCurrentOffset = 0; + } + + private void reset(SampleChunk chunk, long offset) { + mChunk = chunk; + mCurrentOffset = offset; + } + + /** + * Prepares for read I/O operation from a new SampleChunk. + * + * @param chunk the new SampleChunk to read from + * @throws IOException + */ + void openRead(SampleChunk chunk, long offset) throws IOException { + if (mChunk != null) { + mChunk.closeRead(); + } + chunk.openRead(); + reset(chunk, offset); + } + + /** + * Prepares for write I/O operation to a new SampleChunk. + * + * @param chunk the new SampleChunk to write samples afterwards + * @throws IOException + */ + void openWrite(SampleChunk chunk) throws IOException { + if (mChunk != null) { + mChunk.closeWrite(chunk); + } + chunk.openWrite(); + reset(chunk); + } + + /** + * Reads a sample if it is available. + * + * @return Returns a sample if it is available, null otherwise. + * @throws IOException + */ + SampleHolder read() throws IOException { + if (mChunk != null && mChunk.isReadFinished(this)) { + SampleChunk next = mChunk.mNextChunk; + mChunk.closeRead(); + if (next != null) { + next.openRead(); + } + reset(next); + } + if (mChunk != null) { + try { + return mChunk.read(this); + } catch (IllegalStateException e) { + // Write is finished and there is no additional buffer to read. + Log.w(TAG, "Tried to read sample over EOS."); + return null; + } + } else { + return null; + } + } + + /** + * Writes a sample. + * + * @param sample to write + * @param nextChunk if this is {@code null} writes at the current SampleChunk, otherwise + * close current SampleChunk and writes at this + * @throws IOException + */ + void write(SampleHolder sample, SampleChunk nextChunk) throws IOException { + if (mChunk == null) { + throw new IOException("mChunk should not be null"); + } + if (nextChunk != null) { + if (mChunk.mNextChunk != null) { + throw new IllegalStateException("Requested write for wrong SampleChunk"); + } + mChunk.closeWrite(nextChunk); + mChunk.mChunkCallback.onChunkWrite(mChunk); + nextChunk.openWrite(); + reset(nextChunk); + } + mChunk.write(sample, this); + } + + /** + * Finishes write I/O operation. + * + * @throws IOException + */ + void closeWrite() throws IOException { + if (mChunk != null) { + mChunk.closeWrite(null); + } + } + + /** Returns the current SampleChunk for subsequent I/O operation. */ + SampleChunk getChunk() { + return mChunk; + } + + /** Returns the current offset of the current SampleChunk for subsequent I/O operation. */ + long getOffset() { + return mCurrentOffset; + } + + /** + * Releases SampleChunk. the SampleChunk will not be used anymore. + * + * @param chunk to release + * @param delete {@code true} when the backed file needs to be deleted, {@code false} + * otherwise. + */ + static void release(SampleChunk chunk, boolean delete) { + chunk.release(delete); + } + } + + @VisibleForTesting + protected SampleChunk( + SamplePool samplePool, + File file, + long startPositionUs, + long createdTimeMs, + ChunkCallback chunkCallback) { + mStartPositionUs = startPositionUs; + mCreatedTimeMs = createdTimeMs; + mSamplePool = samplePool; + mFile = file; + mChunkCallback = chunkCallback; + } + + // Constructor of SampleChunk which is backed by the given existing file. + private SampleChunk( + SamplePool samplePool, File file, long startPositionUs, ChunkCallback chunkCallback) + throws IOException { + mStartPositionUs = startPositionUs; + mCreatedTimeMs = mStartPositionUs / 1000; + mSamplePool = samplePool; + mFile = file; + mChunkCallback = chunkCallback; + mWriteFinished = true; + } + + private void openRead() throws IOException { + if (!mIsReading) { + if (mAccessFile == null) { + mAccessFile = new RandomAccessFile(mFile, "r"); + } + if (mWriteFinished && mWriteOffset == 0) { + // Lazy loading of write offset, in order not to load + // all SampleChunk's write offset at start time of recorded playback. + mWriteOffset = mAccessFile.length(); + } + mIsReading = true; + } + } + + private void openWrite() throws IOException { + if (mWriteFinished) { + throw new IllegalStateException("Opened for write though write is already finished"); + } + if (!mIsWriting) { + if (mIsReading) { + throw new IllegalStateException( + "Write is requested for " + "an already opened SampleChunk"); + } + mAccessFile = new RandomAccessFile(mFile, "rw"); + mIsWriting = true; + } + } + + private void CloseAccessFileIfNeeded() throws IOException { + if (!mIsReading && !mIsWriting) { + try { + if (mAccessFile != null) { + mAccessFile.close(); + } + } finally { + mAccessFile = null; + } + } + } + + private void closeRead() throws IOException { + if (mIsReading) { + mIsReading = false; + CloseAccessFileIfNeeded(); + } + } + + private void closeWrite(SampleChunk nextChunk) throws IOException { + if (mIsWriting) { + mNextChunk = nextChunk; + mIsWriting = false; + mWriteFinished = true; + CloseAccessFileIfNeeded(); + } + } + + private boolean isReadFinished(IoState state) { + return mWriteFinished && state.equals(this, mWriteOffset); + } + + private SampleHolder read(IoState state) throws IOException { + if (mAccessFile == null || state.mChunk != this) { + throw new IllegalStateException("Requested read for wrong SampleChunk"); + } + long offset = state.mCurrentOffset; + if (offset >= mWriteOffset) { + if (mWriteFinished) { + throw new IllegalStateException("Requested read for wrong range"); + } else { + if (offset != mWriteOffset) { + Log.e(TAG, "This should not happen!"); + } + return null; + } + } + mAccessFile.seek(offset); + int size = mAccessFile.readInt(); + SampleHolder sample = mSamplePool.acquireSample(size); + sample.size = size; + sample.flags = mAccessFile.readInt(); + sample.timeUs = mAccessFile.readLong(); + sample.clearData(); + sample.data.put( + mAccessFile + .getChannel() + .map( + FileChannel.MapMode.READ_ONLY, + offset + SAMPLE_HEADER_LENGTH, + sample.size)); + offset += sample.size + SAMPLE_HEADER_LENGTH; + state.mCurrentOffset = offset; + return sample; + } + + @VisibleForTesting + protected void write(SampleHolder sample, IoState state) throws IOException { + if (mAccessFile == null || mNextChunk != null || !state.equals(this, mWriteOffset)) { + throw new IllegalStateException("Requested write for wrong SampleChunk"); + } + + mAccessFile.seek(mWriteOffset); + mAccessFile.writeInt(sample.size); + mAccessFile.writeInt(sample.flags); + mAccessFile.writeLong(sample.timeUs); + sample.data.position(0).limit(sample.size); + mAccessFile.getChannel().position(mWriteOffset + SAMPLE_HEADER_LENGTH).write(sample.data); + mWriteOffset += sample.size + SAMPLE_HEADER_LENGTH; + state.mCurrentOffset = mWriteOffset; + } + + private void release(boolean delete) { + mWriteFinished = true; + mIsReading = mIsWriting = false; + try { + if (mAccessFile != null) { + mAccessFile.close(); + } + } catch (IOException e) { + // Since the SampleChunk will not be reused, ignore exception. + } + if (delete) { + mFile.delete(); + mChunkCallback.onChunkDelete(this); + } + } + + /** Returns the start position. */ + public long getStartPositionUs() { + return mStartPositionUs; + } + + /** Returns the creation time. */ + public long getCreatedTimeMs() { + return mCreatedTimeMs; + } + + /** Returns the current size. */ + public long getSize() { + return mWriteOffset; + } +} |