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