Home | History | Annotate | Download | only in buffer
      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 import com.google.android.exoplayer.SampleHolder;
     23 import java.io.File;
     24 import java.io.IOException;
     25 import java.io.RandomAccessFile;
     26 import java.nio.channels.FileChannel;
     27 
     28 /**
     29  * {@link SampleChunk} stores samples into file and makes them available for read. Stored file = {
     30  * Header, Sample } * N Header = sample size : int, sample flag : int, sample PTS in micro second :
     31  * long
     32  */
     33 public class SampleChunk {
     34     private static final String TAG = "SampleChunk";
     35     private static final boolean DEBUG = false;
     36 
     37     private final long mCreatedTimeMs;
     38     private final long mStartPositionUs;
     39     private SampleChunk mNextChunk;
     40 
     41     // Header = sample size : int, sample flag : int, sample PTS in micro second : long
     42     private static final int SAMPLE_HEADER_LENGTH = 16;
     43 
     44     private final File mFile;
     45     private final ChunkCallback mChunkCallback;
     46     private final SamplePool mSamplePool;
     47     private RandomAccessFile mAccessFile;
     48     private long mWriteOffset;
     49     private boolean mWriteFinished;
     50     private boolean mIsReading;
     51     private boolean mIsWriting;
     52 
     53     /** A callback for chunks being committed to permanent storage. */
     54     public abstract static class ChunkCallback {
     55 
     56         /**
     57          * Notifies when writing a SampleChunk is completed.
     58          *
     59          * @param chunk SampleChunk which is written completely
     60          */
     61         public void onChunkWrite(SampleChunk chunk) {}
     62 
     63         /**
     64          * Notifies when a SampleChunk is deleted.
     65          *
     66          * @param chunk SampleChunk which is deleted from storage
     67          */
     68         public void onChunkDelete(SampleChunk chunk) {}
     69     }
     70 
     71     /** A class for SampleChunk creation. */
     72     public static class SampleChunkCreator {
     73 
     74         /**
     75          * Returns a newly created SampleChunk to read & write samples.
     76          *
     77          * @param samplePool sample allocator
     78          * @param file filename which will be created newly
     79          * @param startPositionUs the start position of the earliest sample to be stored
     80          * @param chunkCallback for total storage usage change notification
     81          */
     82         @VisibleForTesting
     83         public SampleChunk createSampleChunk(
     84                 SamplePool samplePool,
     85                 File file,
     86                 long startPositionUs,
     87                 ChunkCallback chunkCallback) {
     88             return new SampleChunk(
     89                     samplePool, file, startPositionUs, System.currentTimeMillis(), chunkCallback);
     90         }
     91 
     92         /**
     93          * Returns a newly created SampleChunk which is backed by an existing file. Created
     94          * SampleChunk is read-only.
     95          *
     96          * @param samplePool sample allocator
     97          * @param bufferDir the directory where the file to read is located
     98          * @param filename the filename which will be read afterwards
     99          * @param startPositionUs the start position of the earliest sample in the file
    100          * @param chunkCallback for total storage usage change notification
    101          * @param prev the previous SampleChunk just before the newly created SampleChunk
    102          * @throws IOException
    103          */
    104         SampleChunk loadSampleChunkFromFile(
    105                 SamplePool samplePool,
    106                 File bufferDir,
    107                 String filename,
    108                 long startPositionUs,
    109                 ChunkCallback chunkCallback,
    110                 SampleChunk prev)
    111                 throws IOException {
    112             File file = new File(bufferDir, filename);
    113             SampleChunk chunk = new SampleChunk(samplePool, file, startPositionUs, chunkCallback);
    114             if (prev != null) {
    115                 prev.mNextChunk = chunk;
    116             }
    117             return chunk;
    118         }
    119     }
    120 
    121     /**
    122      * Handles I/O for SampleChunk. Maintains current SampleChunk and the current offset for next
    123      * I/O operation.
    124      */
    125     @VisibleForTesting
    126     public 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         /** Returns whether read I/O operation is finished. */
    135         boolean isReadFinished() {
    136             return mChunk == null;
    137         }
    138 
    139         /** Returns the start position of the current SampleChunk */
    140         long getStartPositionUs() {
    141             return mChunk == null ? 0 : mChunk.getStartPositionUs();
    142         }
    143 
    144         private void reset(@Nullable SampleChunk chunk) {
    145             mChunk = chunk;
    146             mCurrentOffset = 0;
    147         }
    148 
    149         private void reset(SampleChunk chunk, long offset) {
    150             mChunk = chunk;
    151             mCurrentOffset = offset;
    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          */
    160         void openRead(SampleChunk chunk, long offset) throws IOException {
    161             if (mChunk != null) {
    162                 mChunk.closeRead();
    163             }
    164             chunk.openRead();
    165             reset(chunk, offset);
    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          */
    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          */
    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, otherwise
    215          *     close current SampleChunk and writes at this
    216          * @throws IOException
    217          */
    218         void write(SampleHolder sample, SampleChunk nextChunk) throws IOException {
    219             if (mChunk == null) {
    220                 throw new IOException("mChunk should not be null");
    221             }
    222             if (nextChunk != null) {
    223                 if (mChunk.mNextChunk != null) {
    224                     throw new IllegalStateException("Requested write for wrong SampleChunk");
    225                 }
    226                 mChunk.closeWrite(nextChunk);
    227                 mChunk.mChunkCallback.onChunkWrite(mChunk);
    228                 nextChunk.openWrite();
    229                 reset(nextChunk);
    230             }
    231             mChunk.write(sample, this);
    232         }
    233 
    234         /**
    235          * Finishes write I/O operation.
    236          *
    237          * @throws IOException
    238          */
    239         void closeWrite() throws IOException {
    240             if (mChunk != null) {
    241                 mChunk.closeWrite(null);
    242             }
    243         }
    244 
    245         /** Returns the current SampleChunk for subsequent I/O operation. */
    246         SampleChunk getChunk() {
    247             return mChunk;
    248         }
    249 
    250         /** Returns the current offset of the current SampleChunk for subsequent I/O operation. */
    251         long getOffset() {
    252             return mCurrentOffset;
    253         }
    254 
    255         /**
    256          * Releases SampleChunk. the SampleChunk will not be used anymore.
    257          *
    258          * @param chunk to release
    259          * @param delete {@code true} when the backed file needs to be deleted, {@code false}
    260          *     otherwise.
    261          */
    262         static void release(SampleChunk chunk, boolean delete) {
    263             chunk.release(delete);
    264         }
    265     }
    266 
    267     @VisibleForTesting
    268     protected SampleChunk(
    269             SamplePool samplePool,
    270             File file,
    271             long startPositionUs,
    272             long createdTimeMs,
    273             ChunkCallback chunkCallback) {
    274         mStartPositionUs = startPositionUs;
    275         mCreatedTimeMs = createdTimeMs;
    276         mSamplePool = samplePool;
    277         mFile = file;
    278         mChunkCallback = chunkCallback;
    279     }
    280 
    281     // Constructor of SampleChunk which is backed by the given existing file.
    282     private SampleChunk(
    283             SamplePool samplePool, File file, long startPositionUs, ChunkCallback chunkCallback)
    284             throws IOException {
    285         mStartPositionUs = startPositionUs;
    286         mCreatedTimeMs = mStartPositionUs / 1000;
    287         mSamplePool = samplePool;
    288         mFile = file;
    289         mChunkCallback = chunkCallback;
    290         mWriteFinished = true;
    291     }
    292 
    293     private void openRead() throws IOException {
    294         if (!mIsReading) {
    295             if (mAccessFile == null) {
    296                 mAccessFile = new RandomAccessFile(mFile, "r");
    297             }
    298             if (mWriteFinished && mWriteOffset == 0) {
    299                 // Lazy loading of write offset, in order not to load
    300                 // all SampleChunk's write offset at start time of recorded playback.
    301                 mWriteOffset = mAccessFile.length();
    302             }
    303             mIsReading = true;
    304         }
    305     }
    306 
    307     private void openWrite() throws IOException {
    308         if (mWriteFinished) {
    309             throw new IllegalStateException("Opened for write though write is already finished");
    310         }
    311         if (!mIsWriting) {
    312             if (mIsReading) {
    313                 throw new IllegalStateException(
    314                         "Write is requested for " + "an already opened SampleChunk");
    315             }
    316             mAccessFile = new RandomAccessFile(mFile, "rw");
    317             mIsWriting = true;
    318         }
    319     }
    320 
    321     private void CloseAccessFileIfNeeded() throws IOException {
    322         if (!mIsReading && !mIsWriting) {
    323             try {
    324                 if (mAccessFile != null) {
    325                     mAccessFile.close();
    326                 }
    327             } finally {
    328                 mAccessFile = null;
    329             }
    330         }
    331     }
    332 
    333     private void closeRead() throws IOException {
    334         if (mIsReading) {
    335             mIsReading = false;
    336             CloseAccessFileIfNeeded();
    337         }
    338     }
    339 
    340     private void closeWrite(SampleChunk nextChunk) throws IOException {
    341         if (mIsWriting) {
    342             mNextChunk = nextChunk;
    343             mIsWriting = false;
    344             mWriteFinished = true;
    345             CloseAccessFileIfNeeded();
    346         }
    347     }
    348 
    349     private boolean isReadFinished(IoState state) {
    350         return mWriteFinished && state.equals(this, mWriteOffset);
    351     }
    352 
    353     private SampleHolder read(IoState state) throws IOException {
    354         if (mAccessFile == null || state.mChunk != this) {
    355             throw new IllegalStateException("Requested read for wrong SampleChunk");
    356         }
    357         long offset = state.mCurrentOffset;
    358         if (offset >= mWriteOffset) {
    359             if (mWriteFinished) {
    360                 throw new IllegalStateException("Requested read for wrong range");
    361             } else {
    362                 if (offset != mWriteOffset) {
    363                     Log.e(TAG, "This should not happen!");
    364                 }
    365                 return null;
    366             }
    367         }
    368         mAccessFile.seek(offset);
    369         int size = mAccessFile.readInt();
    370         SampleHolder sample = mSamplePool.acquireSample(size);
    371         sample.size = size;
    372         sample.flags = mAccessFile.readInt();
    373         sample.timeUs = mAccessFile.readLong();
    374         sample.clearData();
    375         sample.data.put(
    376                 mAccessFile
    377                         .getChannel()
    378                         .map(
    379                                 FileChannel.MapMode.READ_ONLY,
    380                                 offset + SAMPLE_HEADER_LENGTH,
    381                                 sample.size));
    382         offset += sample.size + SAMPLE_HEADER_LENGTH;
    383         state.mCurrentOffset = offset;
    384         return sample;
    385     }
    386 
    387     @VisibleForTesting
    388     protected void write(SampleHolder sample, IoState state) throws IOException {
    389         if (mAccessFile == null || mNextChunk != null || !state.equals(this, mWriteOffset)) {
    390             throw new IllegalStateException("Requested write for wrong SampleChunk");
    391         }
    392 
    393         mAccessFile.seek(mWriteOffset);
    394         mAccessFile.writeInt(sample.size);
    395         mAccessFile.writeInt(sample.flags);
    396         mAccessFile.writeLong(sample.timeUs);
    397         sample.data.position(0).limit(sample.size);
    398         mAccessFile.getChannel().position(mWriteOffset + SAMPLE_HEADER_LENGTH).write(sample.data);
    399         mWriteOffset += sample.size + SAMPLE_HEADER_LENGTH;
    400         state.mCurrentOffset = mWriteOffset;
    401     }
    402 
    403     private void release(boolean delete) {
    404         mWriteFinished = true;
    405         mIsReading = mIsWriting = false;
    406         try {
    407             if (mAccessFile != null) {
    408                 mAccessFile.close();
    409             }
    410         } catch (IOException e) {
    411             // Since the SampleChunk will not be reused, ignore exception.
    412         }
    413         if (delete) {
    414             mFile.delete();
    415             mChunkCallback.onChunkDelete(this);
    416         }
    417     }
    418 
    419     /** Returns the start position. */
    420     public long getStartPositionUs() {
    421         return mStartPositionUs;
    422     }
    423 
    424     /** Returns the creation time. */
    425     public long getCreatedTimeMs() {
    426         return mCreatedTimeMs;
    427     }
    428 
    429     /** Returns the current size. */
    430     public long getSize() {
    431         return mWriteOffset;
    432     }
    433 }
    434