• 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 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          */
onChunkWrite(SampleChunk chunk)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          */
onChunkDelete(SampleChunk chunk)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
createSampleChunk( SamplePool samplePool, File file, long startPositionUs, ChunkCallback chunkCallback)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          */
loadSampleChunkFromFile( SamplePool samplePool, File bufferDir, String filename, long startPositionUs, ChunkCallback chunkCallback, SampleChunk prev)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 
equals(SampleChunk chunk, long offset)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. */
isReadFinished()135         boolean isReadFinished() {
136             return mChunk == null;
137         }
138 
139         /** Returns the start position of the current SampleChunk */
getStartPositionUs()140         long getStartPositionUs() {
141             return mChunk == null ? 0 : mChunk.getStartPositionUs();
142         }
143 
reset(@ullable SampleChunk chunk)144         private void reset(@Nullable SampleChunk chunk) {
145             mChunk = chunk;
146             mCurrentOffset = 0;
147         }
148 
reset(SampleChunk chunk, long offset)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          */
openRead(SampleChunk chunk, long offset)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          */
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, otherwise
215          *     close current SampleChunk and writes at this
216          * @throws IOException
217          */
write(SampleHolder sample, SampleChunk nextChunk)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          */
closeWrite()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. */
getChunk()246         SampleChunk getChunk() {
247             return mChunk;
248         }
249 
250         /** Returns the current offset of the current SampleChunk for subsequent I/O operation. */
getOffset()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          */
release(SampleChunk chunk, boolean delete)262         static void release(SampleChunk chunk, boolean delete) {
263             chunk.release(delete);
264         }
265     }
266 
267     @VisibleForTesting
SampleChunk( SamplePool samplePool, File file, long startPositionUs, long createdTimeMs, ChunkCallback chunkCallback)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.
SampleChunk( SamplePool samplePool, File file, long startPositionUs, ChunkCallback chunkCallback)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 
openRead()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 
openWrite()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 
CloseAccessFileIfNeeded()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 
closeRead()333     private void closeRead() throws IOException {
334         if (mIsReading) {
335             mIsReading = false;
336             CloseAccessFileIfNeeded();
337         }
338     }
339 
closeWrite(SampleChunk nextChunk)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 
isReadFinished(IoState state)349     private boolean isReadFinished(IoState state) {
350         return mWriteFinished && state.equals(this, mWriteOffset);
351     }
352 
read(IoState state)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
write(SampleHolder sample, IoState state)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 
release(boolean delete)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. */
getStartPositionUs()420     public long getStartPositionUs() {
421         return mStartPositionUs;
422     }
423 
424     /** Returns the creation time. */
getCreatedTimeMs()425     public long getCreatedTimeMs() {
426         return mCreatedTimeMs;
427     }
428 
429     /** Returns the current size. */
getSize()430     public long getSize() {
431         return mWriteOffset;
432     }
433 }
434