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