1/*
2 * Copyright (C) 2015 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
17package com.android.tv.tuner.exoplayer.buffer;
18
19import android.media.MediaFormat;
20import android.util.Log;
21import android.util.Pair;
22
23import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
24import com.google.protobuf.nano.MessageNano;
25
26import java.io.DataInputStream;
27import java.io.DataOutputStream;
28import java.io.File;
29import java.io.FileInputStream;
30import java.io.FileOutputStream;
31import java.io.IOException;
32import java.nio.ByteBuffer;
33import java.nio.charset.StandardCharsets;
34import java.util.ArrayList;
35import java.util.List;
36import java.util.Map;
37import java.util.SortedMap;
38
39/**
40 * Manages DVR storage.
41 */
42public class DvrStorageManager implements BufferManager.StorageManager {
43    private static final String TAG = "DvrStorageManager";
44
45    // TODO: make serializable classes and use protobuf after internal data structure is finalized.
46    private static final String KEY_PIXEL_WIDTH_HEIGHT_RATIO =
47            "com.google.android.videos.pixelWidthHeightRatio";
48    private static final String META_FILE_TYPE_AUDIO = "audio";
49    private static final String META_FILE_TYPE_VIDEO = "video";
50    private static final String META_FILE_TYPE_CAPTION = "caption";
51    private static final String META_FILE_SUFFIX = ".meta";
52    private static final String IDX_FILE_SUFFIX = ".idx";
53    private static final String IDX_FILE_SUFFIX_V2 = IDX_FILE_SUFFIX + "2";
54
55    // Size of minimum reserved storage buffer which will be used to save meta files
56    // and index files after actual recording finished.
57    private static final long MIN_BUFFER_BYTES = 256L * 1024 * 1024;
58    private static final int NO_VALUE = -1;
59    private static final long NO_VALUE_LONG = -1L;
60
61    private final File mBufferDir;
62
63    // {@code true} when this is for recording, {@code false} when this is for replaying.
64    private final boolean mIsRecording;
65
66    public DvrStorageManager(File file, boolean isRecording) {
67        mBufferDir = file;
68        mBufferDir.mkdirs();
69        mIsRecording = isRecording;
70    }
71
72    @Override
73    public File getBufferDir() {
74        return mBufferDir;
75    }
76
77    @Override
78    public boolean isPersistent() {
79        return true;
80    }
81
82    @Override
83    public boolean reachedStorageMax(long bufferSize, long pendingDelete) {
84        return false;
85    }
86
87    @Override
88    public boolean hasEnoughBuffer(long pendingDelete) {
89        return !mIsRecording || mBufferDir.getUsableSpace() >= MIN_BUFFER_BYTES;
90    }
91
92    private void readFormatInt(DataInputStream in, MediaFormat format, String key)
93            throws IOException {
94        int val = in.readInt();
95        if (val != NO_VALUE) {
96            format.setInteger(key, val);
97        }
98    }
99
100    private void readFormatLong(DataInputStream in, MediaFormat format, String key)
101            throws IOException {
102        long val = in.readLong();
103        if (val != NO_VALUE_LONG) {
104            format.setLong(key, val);
105        }
106    }
107
108    private void readFormatFloat(DataInputStream in, MediaFormat format, String key)
109            throws IOException {
110        float val = in.readFloat();
111        if (val != NO_VALUE) {
112            format.setFloat(key, val);
113        }
114    }
115
116    private String readString(DataInputStream in) throws IOException {
117        int len = in.readInt();
118        if (len <= 0) {
119            return null;
120        }
121        byte [] strBytes = new byte[len];
122        in.readFully(strBytes);
123        return new String(strBytes, StandardCharsets.UTF_8);
124    }
125
126    private void readFormatString(DataInputStream in, MediaFormat format, String key)
127            throws IOException {
128        String str = readString(in);
129        if (str != null) {
130            format.setString(key, str);
131        }
132    }
133
134    private void readFormatStringOptional(DataInputStream in, MediaFormat format, String key) {
135        try {
136            String str = readString(in);
137            if (str != null) {
138                format.setString(key, str);
139            }
140        } catch (IOException e) {
141            // Since we are reading optional field, ignore the exception.
142        }
143    }
144
145    private ByteBuffer readByteBuffer(DataInputStream in) throws IOException {
146        int len = in.readInt();
147        if (len <= 0) {
148            return null;
149        }
150        byte [] bytes = new byte[len];
151        in.readFully(bytes);
152        ByteBuffer buffer = ByteBuffer.allocate(len);
153        buffer.put(bytes);
154        buffer.flip();
155
156        return buffer;
157    }
158
159    private void readFormatByteBuffer(DataInputStream in, MediaFormat format, String key)
160            throws IOException {
161        ByteBuffer buffer = readByteBuffer(in);
162        if (buffer != null) {
163            format.setByteBuffer(key, buffer);
164        }
165    }
166
167    @Override
168    public List<BufferManager.TrackFormat> readTrackInfoFiles(boolean isAudio) {
169        List<BufferManager.TrackFormat> trackFormatList = new ArrayList<>();
170        int index = 0;
171        boolean trackNotFound = false;
172        do {
173            String fileName = (isAudio ? META_FILE_TYPE_AUDIO : META_FILE_TYPE_VIDEO)
174                    + ((index == 0) ? META_FILE_SUFFIX : (index + META_FILE_SUFFIX));
175            File file = new File(getBufferDir(), fileName);
176            try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
177                String name = readString(in);
178                MediaFormat format = new MediaFormat();
179                readFormatString(in, format, MediaFormat.KEY_MIME);
180                readFormatInt(in, format, MediaFormat.KEY_MAX_INPUT_SIZE);
181                readFormatInt(in, format, MediaFormat.KEY_WIDTH);
182                readFormatInt(in, format, MediaFormat.KEY_HEIGHT);
183                readFormatInt(in, format, MediaFormat.KEY_CHANNEL_COUNT);
184                readFormatInt(in, format, MediaFormat.KEY_SAMPLE_RATE);
185                readFormatFloat(in, format, KEY_PIXEL_WIDTH_HEIGHT_RATIO);
186                for (int i = 0; i < 3; ++i) {
187                    readFormatByteBuffer(in, format, "csd-" + i);
188                }
189                readFormatLong(in, format, MediaFormat.KEY_DURATION);
190
191                // This is optional since language field is added later.
192                readFormatStringOptional(in, format, MediaFormat.KEY_LANGUAGE);
193                trackFormatList.add(new BufferManager.TrackFormat(name, format));
194            } catch (IOException e) {
195                trackNotFound = true;
196            }
197            index++;
198        } while(!trackNotFound);
199        return trackFormatList;
200    }
201
202    /**
203     * Reads caption information from files.
204     *
205     * @return a list of {@link AtscCaptionTrack} objects which store caption information.
206     */
207    public List<AtscCaptionTrack> readCaptionInfoFiles() {
208        List<AtscCaptionTrack> tracks = new ArrayList<>();
209        int index = 0;
210        boolean trackNotFound = false;
211        do {
212            String fileName = META_FILE_TYPE_CAPTION +
213                    ((index == 0) ? META_FILE_SUFFIX : (index + META_FILE_SUFFIX));
214            File file = new File(getBufferDir(), fileName);
215            try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
216                byte[] data = new byte[(int) file.length()];
217                in.read(data);
218                tracks.add(AtscCaptionTrack.parseFrom(data));
219            } catch (IOException e) {
220                trackNotFound = true;
221            }
222            index++;
223        } while(!trackNotFound);
224        return tracks;
225    }
226
227    private ArrayList<BufferManager.PositionHolder> readOldIndexFile(File indexFile)
228            throws IOException {
229        ArrayList<BufferManager.PositionHolder> indices = new ArrayList<>();
230        try (DataInputStream in = new DataInputStream(new FileInputStream(indexFile))) {
231            long count = in.readLong();
232            for (long i = 0; i < count; ++i) {
233                long positionUs = in.readLong();
234                indices.add(new BufferManager.PositionHolder(positionUs, positionUs, 0));
235            }
236            return indices;
237        }
238    }
239
240    private ArrayList<BufferManager.PositionHolder> readNewIndexFile(File indexFile)
241            throws IOException {
242        ArrayList<BufferManager.PositionHolder> indices = new ArrayList<>();
243        try (DataInputStream in = new DataInputStream(new FileInputStream(indexFile))) {
244            long count = in.readLong();
245            for (long i = 0; i < count; ++i) {
246                long positionUs = in.readLong();
247                long basePositionUs = in.readLong();
248                int offset = in.readInt();
249                indices.add(new BufferManager.PositionHolder(positionUs, basePositionUs, offset));
250            }
251            return indices;
252        }
253    }
254
255    @Override
256    public ArrayList<BufferManager.PositionHolder> readIndexFile(String trackId)
257            throws IOException {
258        File file = new File(getBufferDir(), trackId + IDX_FILE_SUFFIX_V2);
259        if (file.exists()) {
260            return readNewIndexFile(file);
261        } else {
262            return readOldIndexFile(new File(getBufferDir(),trackId + IDX_FILE_SUFFIX));
263        }
264    }
265
266    private void writeFormatInt(DataOutputStream out, MediaFormat format, String key)
267            throws IOException {
268        if (format.containsKey(key)) {
269            out.writeInt(format.getInteger(key));
270        } else {
271            out.writeInt(NO_VALUE);
272        }
273    }
274
275    private void writeFormatLong(DataOutputStream out, MediaFormat format, String key)
276            throws IOException {
277        if (format.containsKey(key)) {
278            out.writeLong(format.getLong(key));
279        } else {
280            out.writeLong(NO_VALUE_LONG);
281        }
282    }
283
284    private void writeFormatFloat(DataOutputStream out, MediaFormat format, String key)
285            throws IOException {
286        if (format.containsKey(key)) {
287            out.writeFloat(format.getFloat(key));
288        } else {
289            out.writeFloat(NO_VALUE);
290        }
291    }
292
293    private void writeString(DataOutputStream out, String str) throws IOException {
294        byte [] data = str.getBytes(StandardCharsets.UTF_8);
295        out.writeInt(data.length);
296        if (data.length > 0) {
297            out.write(data);
298        }
299    }
300
301    private void writeFormatString(DataOutputStream out, MediaFormat format, String key)
302            throws IOException {
303        if (format.containsKey(key)) {
304            writeString(out, format.getString(key));
305        } else {
306            out.writeInt(0);
307        }
308    }
309
310    private void writeByteBuffer(DataOutputStream out, ByteBuffer buffer) throws IOException {
311        byte [] data = new byte[buffer.limit()];
312        buffer.get(data);
313        buffer.flip();
314        out.writeInt(data.length);
315        if (data.length > 0) {
316            out.write(data);
317        } else {
318            out.writeInt(0);
319        }
320    }
321
322    private void writeFormatByteBuffer(DataOutputStream out, MediaFormat format, String key)
323            throws IOException {
324        if (format.containsKey(key)) {
325            writeByteBuffer(out, format.getByteBuffer(key));
326        } else {
327            out.writeInt(0);
328        }
329    }
330
331    @Override
332    public void writeTrackInfoFiles(List<BufferManager.TrackFormat> formatList, boolean isAudio)
333            throws IOException {
334        for (int i = 0; i < formatList.size() ; ++i) {
335            BufferManager.TrackFormat trackFormat = formatList.get(i);
336            String fileName = (isAudio ? META_FILE_TYPE_AUDIO : META_FILE_TYPE_VIDEO)
337                    + ((i == 0) ? META_FILE_SUFFIX : (i + META_FILE_SUFFIX));
338            File file = new File(getBufferDir(), fileName);
339            try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) {
340                writeString(out, trackFormat.trackId);
341                writeFormatString(out, trackFormat.format, MediaFormat.KEY_MIME);
342                writeFormatInt(out, trackFormat.format, MediaFormat.KEY_MAX_INPUT_SIZE);
343                writeFormatInt(out, trackFormat.format, MediaFormat.KEY_WIDTH);
344                writeFormatInt(out, trackFormat.format, MediaFormat.KEY_HEIGHT);
345                writeFormatInt(out, trackFormat.format, MediaFormat.KEY_CHANNEL_COUNT);
346                writeFormatInt(out, trackFormat.format, MediaFormat.KEY_SAMPLE_RATE);
347                writeFormatFloat(out, trackFormat.format, KEY_PIXEL_WIDTH_HEIGHT_RATIO);
348                for (int j = 0; j < 3; ++j) {
349                    writeFormatByteBuffer(out, trackFormat.format, "csd-" + j);
350                }
351                writeFormatLong(out, trackFormat.format, MediaFormat.KEY_DURATION);
352                writeFormatString(out, trackFormat.format, MediaFormat.KEY_LANGUAGE);
353            }
354        }
355    }
356
357    /**
358     * Writes caption information to files.
359     *
360     * @param tracks a list of {@link AtscCaptionTrack} objects which store caption information.
361     */
362    public void writeCaptionInfoFiles(List<AtscCaptionTrack> tracks) {
363        if (tracks == null || tracks.isEmpty()) {
364            return;
365        }
366        for (int i = 0; i < tracks.size(); i++) {
367            AtscCaptionTrack track = tracks.get(i);
368            String fileName = META_FILE_TYPE_CAPTION +
369                    ((i == 0) ? META_FILE_SUFFIX : (i + META_FILE_SUFFIX));
370            File file = new File(getBufferDir(), fileName);
371            try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) {
372                out.write(MessageNano.toByteArray(track));
373            } catch (Exception e) {
374                Log.e(TAG, "Fail to write caption info to files", e);
375            }
376        }
377    }
378
379    @Override
380    public void writeIndexFile(String trackName, SortedMap<Long, Pair<SampleChunk, Integer>> index)
381            throws IOException {
382        File indexFile  = new File(getBufferDir(), trackName + IDX_FILE_SUFFIX_V2);
383        try (DataOutputStream out = new DataOutputStream(new FileOutputStream(indexFile))) {
384            out.writeLong(index.size());
385            for (Map.Entry<Long, Pair<SampleChunk, Integer>> entry : index.entrySet()) {
386                out.writeLong(entry.getKey());
387                out.writeLong(entry.getValue().first.getStartPositionUs());
388                out.writeInt(entry.getValue().second);
389            }
390        }
391    }
392}
393