• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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     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          */
createSampleChunk(SamplePool samplePool, File file, long startPositionUs, ChunkCallback chunkCallback)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          */
loadSampleChunkFromFile(SamplePool samplePool, File bufferDir, String filename, long startPositionUs, ChunkCallback chunkCallback, SampleChunk prev)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 
equals(SampleChunk chunk, long offset)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          */
isReadFinished()137         boolean isReadFinished() {
138             return mChunk == null;
139         }
140 
141         /**
142          * Returns the start position of the current SampleChunk
143          */
getStartPositionUs()144         long getStartPositionUs() {
145             return mChunk == null ? 0 : mChunk.getStartPositionUs();
146         }
147 
reset(@ullable SampleChunk chunk)148         private void reset(@Nullable SampleChunk chunk) {
149             mChunk = chunk;
150             mCurrentOffset = 0;
151         }
152 
reset(SampleChunk chunk, long offset)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          */
openRead(SampleChunk chunk, long offset)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          */
openWrite(SampleChunk chunk)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          */
read()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          */
write(SampleHolder sample, SampleChunk nextChunk)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          */
closeWrite()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          */
getChunk()250         SampleChunk getChunk() {
251             return mChunk;
252         }
253 
254         /**
255          * Returns the current offset of the current SampleChunk for subsequent I/O operation.
256          */
getOffset()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          */
release(SampleChunk chunk, boolean delete)268         static void release(SampleChunk chunk, boolean delete) {
269             chunk.release(delete);
270         }
271     }
272 
273     @VisibleForTesting
SampleChunk(SamplePool samplePool, File file, long startPositionUs, long createdTimeMs, ChunkCallback chunkCallback)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.
SampleChunk(SamplePool samplePool, File file, long startPositionUs, ChunkCallback chunkCallback)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 
openRead()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 
openWrite()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 
CloseAccessFileIfNeeded()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 
closeRead()334     private void closeRead() throws IOException{
335         if (mIsReading) {
336             mIsReading = false;
337             CloseAccessFileIfNeeded();
338         }
339     }
340 
closeWrite(SampleChunk nextChunk)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 
isReadFinished(IoState state)351     private boolean isReadFinished(IoState state) {
352         return mWriteFinished && state.equals(this, mWriteOffset);
353     }
354 
read(IoState state)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
write(SampleHolder sample, IoState state)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 
release(boolean delete)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      */
getStartPositionUs()420     public long getStartPositionUs() {
421         return mStartPositionUs;
422     }
423 
424     /**
425      * Returns the creation time.
426      */
getCreatedTimeMs()427     public long getCreatedTimeMs() {
428         return mCreatedTimeMs;
429     }
430 
431     /**
432      * Returns the current size.
433      */
getSize()434     public long getSize() {
435         return mWriteOffset;
436     }
437 }
438