1 /*
2  * Copyright (C) 2019 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.exoplayer2.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.exoplayer2.C;
24 import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
25 
26 import java.io.File;
27 import java.io.IOException;
28 import java.io.RandomAccessFile;
29 import java.nio.channels.FileChannel;
30 
31 /**
32  * {@link SampleChunk} stores samples into file and makes them available for read. Stored file = {
33  * Header, Sample } * N Header = sample size : int, sample flag : int, sample PTS in micro second :
34  * long
35  */
36 public class SampleChunk {
37     private static final String TAG = "SampleChunk";
38     private static final boolean DEBUG = false;
39 
40     // The flag values should not be changed.
41     /**
42      * This indicates that the (encoded) buffer marked as such contains
43      * the data for a key frame.
44      */
45     private static final int BUFFER_FLAG_KEY_FRAME = 1;
46     /** Indicates that a buffer should be decoded but not rendered. */
47     private static final int BUFFER_FLAG_DECODE_ONLY = 1 << 31; // 0x80000000
48     /** Indicates that a buffer is (at least partially) encrypted. */
49     private static final int BUFFER_FLAG_ENCRYPTED = 1 << 30; // 0x40000000
50 
51     private final long mCreatedTimeMs;
52     private final long mStartPositionUs;
53     private SampleChunk mNextChunk;
54 
55     // Header = sample size : int, sample flag : int, sample PTS in micro second : long
56     private static final int SAMPLE_HEADER_LENGTH = 16;
57 
58     private final File mFile;
59     private final ChunkCallback mChunkCallback;
60     private final InputBufferPool mInputBufferPool;
61     private RandomAccessFile mAccessFile;
62     private long mWriteOffset;
63     private boolean mWriteFinished;
64     private boolean mIsReading;
65     private boolean mIsWriting;
66 
67     /** A callback for chunks being committed to permanent storage. */
68     public abstract static class ChunkCallback {
69 
70         /**
71          * Notifies when writing a SampleChunk is completed.
72          *
73          * @param chunk SampleChunk which is written completely
74          */
onChunkWrite(SampleChunk chunk)75         public void onChunkWrite(SampleChunk chunk) {}
76 
77         /**
78          * Notifies when a SampleChunk is deleted.
79          *
80          * @param chunk SampleChunk which is deleted from storage
81          */
onChunkDelete(SampleChunk chunk)82         public void onChunkDelete(SampleChunk chunk) {}
83     }
84 
85     /** A class for SampleChunk creation. */
86     public static class SampleChunkCreator {
87 
88         /**
89          * Returns a newly created SampleChunk to read & write samples.
90          *
91          * @param inputBufferPool sample allocator
92          * @param file filename which will be created newly
93          * @param startPositionUs the start position of the earliest sample to be stored
94          * @param chunkCallback for total storage usage change notification
95          */
96         @VisibleForTesting
createSampleChunk( InputBufferPool inputBufferPool, File file, long startPositionUs, ChunkCallback chunkCallback)97         SampleChunk createSampleChunk(
98                 InputBufferPool inputBufferPool,
99                 File file,
100                 long startPositionUs,
101                 ChunkCallback chunkCallback) {
102             return new SampleChunk(
103                     inputBufferPool,
104                     file,
105                     startPositionUs,
106                     System.currentTimeMillis(),
107                     chunkCallback);
108         }
109 
110         /**
111          * Returns a newly created SampleChunk which is backed by an existing file. Created
112          * SampleChunk is read-only.
113          *
114          * @param inputBufferPool sample allocator
115          * @param bufferDir the directory where the file to read is located
116          * @param filename the filename which will be read afterwards
117          * @param startPositionUs the start position of the earliest sample in the file
118          * @param chunkCallback for total storage usage change notification
119          * @param prev the previous SampleChunk just before the newly created SampleChunk
120          */
loadSampleChunkFromFile( InputBufferPool inputBufferPool, File bufferDir, String filename, long startPositionUs, ChunkCallback chunkCallback, SampleChunk prev)121         SampleChunk loadSampleChunkFromFile(
122                 InputBufferPool inputBufferPool,
123                 File bufferDir,
124                 String filename,
125                 long startPositionUs,
126                 ChunkCallback chunkCallback,
127                 SampleChunk prev) {
128             File file = new File(bufferDir, filename);
129             SampleChunk chunk =
130                     new SampleChunk(inputBufferPool, file, startPositionUs, chunkCallback);
131             if (prev != null) {
132                 prev.mNextChunk = chunk;
133             }
134             return chunk;
135         }
136     }
137 
138     /**
139      * Handles I/O for SampleChunk. Maintains current SampleChunk and the current offset for next
140      * I/O operation.
141      */
142     @VisibleForTesting
143     static class IoState {
144         private SampleChunk mChunk;
145         private long mCurrentOffset;
146 
equals(SampleChunk chunk, long offset)147         private boolean equals(SampleChunk chunk, long offset) {
148             return chunk == mChunk && mCurrentOffset == offset;
149         }
150 
151         /** Returns whether read I/O operation is finished. */
isReadFinished()152         boolean isReadFinished() {
153             return mChunk == null;
154         }
155 
156         /** Returns the start position of the current SampleChunk */
getStartPositionUs()157         long getStartPositionUs() {
158             return mChunk == null ? 0 : mChunk.getStartPositionUs();
159         }
160 
reset(@ullable SampleChunk chunk)161         private void reset(@Nullable SampleChunk chunk) {
162             mChunk = chunk;
163             mCurrentOffset = 0;
164         }
165 
reset(SampleChunk chunk, long offset)166         private void reset(SampleChunk chunk, long offset) {
167             mChunk = chunk;
168             mCurrentOffset = offset;
169         }
170 
171         /**
172          * Prepares for read I/O operation from a new SampleChunk.
173          *
174          * @param chunk the new SampleChunk to read from
175          * @throws IOException if an I/O error occurs.
176          */
openRead(SampleChunk chunk, long offset)177         void openRead(SampleChunk chunk, long offset) throws IOException {
178             if (mChunk != null) {
179                 mChunk.closeRead();
180             }
181             chunk.openRead();
182             reset(chunk, offset);
183         }
184 
185         /**
186          * Prepares for write I/O operation to a new SampleChunk.
187          *
188          * @param chunk the new SampleChunk to write samples afterwards
189          * @throws IOException if an I/O error occurs.
190          */
openWrite(SampleChunk chunk)191         void openWrite(SampleChunk chunk) throws IOException {
192             if (mChunk != null) {
193                 mChunk.closeWrite(chunk);
194             }
195             chunk.openWrite();
196             reset(chunk);
197         }
198 
199         /**
200          * Reads a sample if it is available.
201          *
202          * @return Returns a sample if it is available, null otherwise.
203          * @throws IOException if an I/O error occurs.
204          */
read()205         DecoderInputBuffer read() throws IOException {
206             if (mChunk != null && mChunk.isReadFinished(this)) {
207                 SampleChunk next = mChunk.mNextChunk;
208                 mChunk.closeRead();
209                 if (next != null) {
210                     next.openRead();
211                 }
212                 reset(next);
213             }
214             if (mChunk != null) {
215                 try {
216                     return mChunk.read(this);
217                 } catch (IllegalStateException e) {
218                     // Write is finished and there is no additional buffer to read.
219                     Log.w(TAG, "Tried to read sample over EOS.");
220                     return null;
221                 }
222             } else {
223                 return null;
224             }
225         }
226 
227         /**
228          * Writes a sample.
229          *
230          * @param sample to write
231          * @param nextChunk if this is {@code null} writes at the current SampleChunk, otherwise
232          *     close current SampleChunk and writes at this
233          * @throws IOException if an I/O error occurs.
234          */
write(DecoderInputBuffer sample, SampleChunk nextChunk)235         void write(DecoderInputBuffer sample, SampleChunk nextChunk) throws IOException {
236             if (mChunk == null) {
237                 throw new IOException("mChunk should not be null");
238             }
239             if (nextChunk != null) {
240                 if (mChunk.mNextChunk != null) {
241                     throw new IllegalStateException("Requested write for wrong SampleChunk");
242                 }
243                 mChunk.closeWrite(nextChunk);
244                 mChunk.mChunkCallback.onChunkWrite(mChunk);
245                 nextChunk.openWrite();
246                 reset(nextChunk);
247             }
248             mChunk.write(sample, this);
249         }
250 
251         /**
252          * Finishes write I/O operation.
253          *
254          * @throws IOException if an I/O error occurs.
255          */
closeWrite()256         void closeWrite() throws IOException {
257             if (mChunk != null) {
258                 mChunk.closeWrite(null);
259             }
260         }
261 
262         /** Returns the current SampleChunk for subsequent I/O operation. */
getChunk()263         SampleChunk getChunk() {
264             return mChunk;
265         }
266 
267         /** Returns the current offset of the current SampleChunk for subsequent I/O operation. */
getOffset()268         long getOffset() {
269             return mCurrentOffset;
270         }
271 
272         /**
273          * Releases SampleChunk. the SampleChunk will not be used anymore.
274          *
275          * @param chunk to release
276          * @param delete {@code true} when the backed file needs to be deleted, {@code false}
277          *     otherwise.
278          */
release(SampleChunk chunk, boolean delete)279         static void release(SampleChunk chunk, boolean delete) {
280             chunk.release(delete);
281         }
282     }
283 
284     @VisibleForTesting
SampleChunk( InputBufferPool inputBufferPool, File file, long startPositionUs, long createdTimeMs, ChunkCallback chunkCallback)285     SampleChunk(
286             InputBufferPool inputBufferPool,
287             File file,
288             long startPositionUs,
289             long createdTimeMs,
290             ChunkCallback chunkCallback) {
291         mStartPositionUs = startPositionUs;
292         mCreatedTimeMs = createdTimeMs;
293         mInputBufferPool = inputBufferPool;
294         mFile = file;
295         mChunkCallback = chunkCallback;
296     }
297 
298     // Constructor of SampleChunk which is backed by the given existing file.
SampleChunk( InputBufferPool inputBufferPool, File file, long startPositionUs, ChunkCallback chunkCallback)299     private SampleChunk(
300             InputBufferPool inputBufferPool,
301             File file,
302             long startPositionUs,
303             ChunkCallback chunkCallback) {
304         mStartPositionUs = startPositionUs;
305         mCreatedTimeMs = mStartPositionUs / 1000;
306         mInputBufferPool = inputBufferPool;
307         mFile = file;
308         mChunkCallback = chunkCallback;
309         mWriteFinished = true;
310     }
311 
openRead()312     private void openRead() throws IOException {
313         if (!mIsReading) {
314             if (mAccessFile == null) {
315                 mAccessFile = new RandomAccessFile(mFile, "r");
316             }
317             if (mWriteFinished && mWriteOffset == 0) {
318                 // Lazy loading of write offset, in order not to load
319                 // all SampleChunk's write offset at start time of recorded playback.
320                 mWriteOffset = mAccessFile.length();
321             }
322             mIsReading = true;
323         }
324     }
325 
openWrite()326     private void openWrite() throws IOException {
327         if (mWriteFinished) {
328             throw new IllegalStateException("Opened for write though write is already finished");
329         }
330         if (!mIsWriting) {
331             if (mIsReading) {
332                 throw new IllegalStateException(
333                         "Write is requested for " + "an already opened SampleChunk");
334             }
335             mAccessFile = new RandomAccessFile(mFile, "rw");
336             mIsWriting = true;
337         }
338     }
339 
CloseAccessFileIfNeeded()340     private void CloseAccessFileIfNeeded() throws IOException {
341         if (!mIsReading && !mIsWriting) {
342             try {
343                 if (mAccessFile != null) {
344                     mAccessFile.close();
345                 }
346             } finally {
347                 mAccessFile = null;
348             }
349         }
350     }
351 
closeRead()352     private void closeRead() throws IOException {
353         if (mIsReading) {
354             mIsReading = false;
355             CloseAccessFileIfNeeded();
356         }
357     }
358 
closeWrite(SampleChunk nextChunk)359     private void closeWrite(SampleChunk nextChunk) throws IOException {
360         if (mIsWriting) {
361             mNextChunk = nextChunk;
362             mIsWriting = false;
363             mWriteFinished = true;
364             CloseAccessFileIfNeeded();
365         }
366     }
367 
isReadFinished(IoState state)368     private boolean isReadFinished(IoState state) {
369         return mWriteFinished && state.equals(this, mWriteOffset);
370     }
371 
read(IoState state)372     private DecoderInputBuffer read(IoState state) throws IOException {
373         if (mAccessFile == null || state.mChunk != this) {
374             throw new IllegalStateException("Requested read for wrong SampleChunk");
375         }
376         long offset = state.mCurrentOffset;
377         if (offset >= mWriteOffset) {
378             if (mWriteFinished) {
379                 throw new IllegalStateException("Requested read for wrong range");
380             } else {
381                 if (offset != mWriteOffset) {
382                     Log.e(TAG, "This should not happen!");
383                 }
384                 return null;
385             }
386         }
387         mAccessFile.seek(offset);
388         int size = mAccessFile.readInt();
389         DecoderInputBuffer sample = mInputBufferPool.acquireSample(size);
390         int flags = mAccessFile.readInt();
391         flags = (isKeyFrame(flags) ? C.BUFFER_FLAG_KEY_FRAME : 0)
392                 | (isDecodeOnly(flags) ? C.BUFFER_FLAG_DECODE_ONLY : 0)
393                 | (isEncrypted(flags) ? C.BUFFER_FLAG_ENCRYPTED : 0);
394         sample.setFlags(flags);
395         sample.timeUs = mAccessFile.readLong();
396         sample.data.clear();
397         sample.data.put(
398                 mAccessFile
399                         .getChannel()
400                         .map(
401                                 FileChannel.MapMode.READ_ONLY,
402                                 offset + SAMPLE_HEADER_LENGTH,
403                                 size));
404         offset += size + SAMPLE_HEADER_LENGTH;
405         state.mCurrentOffset = offset;
406         return sample;
407     }
408 
isKeyFrame(int flag)409     private boolean isKeyFrame(int flag) {
410         return (flag & BUFFER_FLAG_KEY_FRAME) == BUFFER_FLAG_KEY_FRAME;
411     }
412 
isDecodeOnly(int flag)413     private boolean isDecodeOnly(int flag) {
414         return (flag & BUFFER_FLAG_DECODE_ONLY) == BUFFER_FLAG_DECODE_ONLY;
415     }
416 
isEncrypted(int flag)417     private boolean isEncrypted(int flag) {
418         return (flag & BUFFER_FLAG_ENCRYPTED) == BUFFER_FLAG_ENCRYPTED;
419     }
420 
421     @VisibleForTesting
write(DecoderInputBuffer sample, IoState state)422     void write(DecoderInputBuffer sample, IoState state) throws IOException {
423         if (mAccessFile == null || mNextChunk != null || !state.equals(this, mWriteOffset)) {
424             throw new IllegalStateException("Requested write for wrong SampleChunk");
425         }
426 
427         mAccessFile.seek(mWriteOffset);
428         int size = sample.data.position();
429         mAccessFile.writeInt(size);
430         int flags = (sample.isKeyFrame() ? BUFFER_FLAG_KEY_FRAME : 0)
431                 | (sample.isDecodeOnly() ? BUFFER_FLAG_DECODE_ONLY : 0)
432                 | (sample.isEncrypted() ? BUFFER_FLAG_ENCRYPTED : 0);
433         mAccessFile.writeInt(flags);
434         mAccessFile.writeLong(sample.timeUs);
435         sample.data.position(0).limit(size);
436         mAccessFile.getChannel().position(mWriteOffset + SAMPLE_HEADER_LENGTH).write(sample.data);
437         mWriteOffset += size + SAMPLE_HEADER_LENGTH;
438         state.mCurrentOffset = mWriteOffset;
439     }
440 
release(boolean delete)441     private void release(boolean delete) {
442         mWriteFinished = true;
443         mIsReading = mIsWriting = false;
444         try {
445             if (mAccessFile != null) {
446                 mAccessFile.close();
447             }
448         } catch (IOException e) {
449             // Since the SampleChunk will not be reused, ignore exception.
450         }
451         if (delete) {
452             mFile.delete();
453             mChunkCallback.onChunkDelete(this);
454         }
455     }
456 
457     /** Returns the start position. */
getStartPositionUs()458     public long getStartPositionUs() {
459         return mStartPositionUs;
460     }
461 
462     /** Returns the creation time. */
getCreatedTimeMs()463     public long getCreatedTimeMs() {
464         return mCreatedTimeMs;
465     }
466 
467     /** Returns the current size. */
getSize()468     public long getSize() {
469         return mWriteOffset;
470     }
471 }
472