aboutsummaryrefslogtreecommitdiff
path: root/tuner/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java
diff options
context:
space:
mode:
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.java433
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;
+ }
+}