/* * 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 static abstract 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. */ @VisibleForTesting 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 */ 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. */ 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; } /** * Prepares for read I/O operation from a new SampleChunk. * * @param chunk the new SampleChunk to read from * @throws IOException */ void openRead(SampleChunk chunk) throws IOException { if (mChunk != null) { mChunk.closeRead(); } chunk.openRead(); reset(chunk); } /** * 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 (nextChunk != null) { if (mChunk == null || 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); } } /** * 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; } }