aboutsummaryrefslogtreecommitdiff
path: root/tuner/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java
diff options
context:
space:
mode:
Diffstat (limited to 'tuner/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java')
-rw-r--r--tuner/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java391
1 files changed, 391 insertions, 0 deletions
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java b/tuner/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java
new file mode 100644
index 00000000..2a58ffcf
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java
@@ -0,0 +1,391 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.exoplayer.buffer;
+
+import android.media.MediaFormat;
+import android.util.Log;
+import android.util.Pair;
+import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.google.protobuf.nano.MessageNano;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedMap;
+
+/** Manages DVR storage. */
+public class DvrStorageManager implements BufferManager.StorageManager {
+ private static final String TAG = "DvrStorageManager";
+
+ // TODO: make serializable classes and use protobuf after internal data structure is finalized.
+ private static final String KEY_PIXEL_WIDTH_HEIGHT_RATIO =
+ "com.google.android.videos.pixelWidthHeightRatio";
+ private static final String META_FILE_TYPE_AUDIO = "audio";
+ private static final String META_FILE_TYPE_VIDEO = "video";
+ private static final String META_FILE_TYPE_CAPTION = "caption";
+ private static final String META_FILE_SUFFIX = ".meta";
+ private static final String IDX_FILE_SUFFIX = ".idx";
+ private static final String IDX_FILE_SUFFIX_V2 = IDX_FILE_SUFFIX + "2";
+
+ // Size of minimum reserved storage buffer which will be used to save meta files
+ // and index files after actual recording finished.
+ private static final long MIN_BUFFER_BYTES = 256L * 1024 * 1024;
+ private static final int NO_VALUE = -1;
+ private static final long NO_VALUE_LONG = -1L;
+
+ private final File mBufferDir;
+
+ // {@code true} when this is for recording, {@code false} when this is for replaying.
+ private final boolean mIsRecording;
+
+ public DvrStorageManager(File file, boolean isRecording) {
+ mBufferDir = file;
+ mBufferDir.mkdirs();
+ mIsRecording = isRecording;
+ }
+
+ @Override
+ public File getBufferDir() {
+ return mBufferDir;
+ }
+
+ @Override
+ public boolean isPersistent() {
+ return true;
+ }
+
+ @Override
+ public boolean reachedStorageMax(long bufferSize, long pendingDelete) {
+ return false;
+ }
+
+ @Override
+ public boolean hasEnoughBuffer(long pendingDelete) {
+ return !mIsRecording || mBufferDir.getUsableSpace() >= MIN_BUFFER_BYTES;
+ }
+
+ private void readFormatInt(DataInputStream in, MediaFormat format, String key)
+ throws IOException {
+ int val = in.readInt();
+ if (val != NO_VALUE) {
+ format.setInteger(key, val);
+ }
+ }
+
+ private void readFormatLong(DataInputStream in, MediaFormat format, String key)
+ throws IOException {
+ long val = in.readLong();
+ if (val != NO_VALUE_LONG) {
+ format.setLong(key, val);
+ }
+ }
+
+ private void readFormatFloat(DataInputStream in, MediaFormat format, String key)
+ throws IOException {
+ float val = in.readFloat();
+ if (val != NO_VALUE) {
+ format.setFloat(key, val);
+ }
+ }
+
+ private String readString(DataInputStream in) throws IOException {
+ int len = in.readInt();
+ if (len <= 0) {
+ return null;
+ }
+ byte[] strBytes = new byte[len];
+ in.readFully(strBytes);
+ return new String(strBytes, StandardCharsets.UTF_8);
+ }
+
+ private void readFormatString(DataInputStream in, MediaFormat format, String key)
+ throws IOException {
+ String str = readString(in);
+ if (str != null) {
+ format.setString(key, str);
+ }
+ }
+
+ private void readFormatStringOptional(DataInputStream in, MediaFormat format, String key) {
+ try {
+ String str = readString(in);
+ if (str != null) {
+ format.setString(key, str);
+ }
+ } catch (IOException e) {
+ // Since we are reading optional field, ignore the exception.
+ }
+ }
+
+ private ByteBuffer readByteBuffer(DataInputStream in) throws IOException {
+ int len = in.readInt();
+ if (len <= 0) {
+ return null;
+ }
+ byte[] bytes = new byte[len];
+ in.readFully(bytes);
+ ByteBuffer buffer = ByteBuffer.allocate(len);
+ buffer.put(bytes);
+ buffer.flip();
+
+ return buffer;
+ }
+
+ private void readFormatByteBuffer(DataInputStream in, MediaFormat format, String key)
+ throws IOException {
+ ByteBuffer buffer = readByteBuffer(in);
+ if (buffer != null) {
+ format.setByteBuffer(key, buffer);
+ }
+ }
+
+ @Override
+ public List<BufferManager.TrackFormat> readTrackInfoFiles(boolean isAudio) {
+ List<BufferManager.TrackFormat> trackFormatList = new ArrayList<>();
+ int index = 0;
+ boolean trackNotFound = false;
+ do {
+ String fileName =
+ (isAudio ? META_FILE_TYPE_AUDIO : META_FILE_TYPE_VIDEO)
+ + ((index == 0) ? META_FILE_SUFFIX : (index + META_FILE_SUFFIX));
+ File file = new File(getBufferDir(), fileName);
+ try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
+ String name = readString(in);
+ MediaFormat format = new MediaFormat();
+ readFormatString(in, format, MediaFormat.KEY_MIME);
+ readFormatInt(in, format, MediaFormat.KEY_MAX_INPUT_SIZE);
+ readFormatInt(in, format, MediaFormat.KEY_WIDTH);
+ readFormatInt(in, format, MediaFormat.KEY_HEIGHT);
+ readFormatInt(in, format, MediaFormat.KEY_CHANNEL_COUNT);
+ readFormatInt(in, format, MediaFormat.KEY_SAMPLE_RATE);
+ readFormatFloat(in, format, KEY_PIXEL_WIDTH_HEIGHT_RATIO);
+ for (int i = 0; i < 3; ++i) {
+ readFormatByteBuffer(in, format, "csd-" + i);
+ }
+ readFormatLong(in, format, MediaFormat.KEY_DURATION);
+
+ // This is optional since language field is added later.
+ readFormatStringOptional(in, format, MediaFormat.KEY_LANGUAGE);
+ trackFormatList.add(new BufferManager.TrackFormat(name, format));
+ } catch (IOException e) {
+ trackNotFound = true;
+ }
+ index++;
+ } while (!trackNotFound);
+ return trackFormatList;
+ }
+
+ /**
+ * Reads caption information from files.
+ *
+ * @return a list of {@link AtscCaptionTrack} objects which store caption information.
+ */
+ public List<AtscCaptionTrack> readCaptionInfoFiles() {
+ List<AtscCaptionTrack> tracks = new ArrayList<>();
+ int index = 0;
+ boolean trackNotFound = false;
+ do {
+ String fileName =
+ META_FILE_TYPE_CAPTION
+ + ((index == 0) ? META_FILE_SUFFIX : (index + META_FILE_SUFFIX));
+ File file = new File(getBufferDir(), fileName);
+ try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
+ byte[] data = new byte[(int) file.length()];
+ in.read(data);
+ tracks.add(AtscCaptionTrack.parseFrom(data));
+ } catch (IOException e) {
+ trackNotFound = true;
+ }
+ index++;
+ } while (!trackNotFound);
+ return tracks;
+ }
+
+ private ArrayList<BufferManager.PositionHolder> readOldIndexFile(File indexFile)
+ throws IOException {
+ ArrayList<BufferManager.PositionHolder> indices = new ArrayList<>();
+ try (DataInputStream in = new DataInputStream(new FileInputStream(indexFile))) {
+ long count = in.readLong();
+ for (long i = 0; i < count; ++i) {
+ long positionUs = in.readLong();
+ indices.add(new BufferManager.PositionHolder(positionUs, positionUs, 0));
+ }
+ return indices;
+ }
+ }
+
+ private ArrayList<BufferManager.PositionHolder> readNewIndexFile(File indexFile)
+ throws IOException {
+ ArrayList<BufferManager.PositionHolder> indices = new ArrayList<>();
+ try (DataInputStream in = new DataInputStream(new FileInputStream(indexFile))) {
+ long count = in.readLong();
+ for (long i = 0; i < count; ++i) {
+ long positionUs = in.readLong();
+ long basePositionUs = in.readLong();
+ int offset = in.readInt();
+ indices.add(new BufferManager.PositionHolder(positionUs, basePositionUs, offset));
+ }
+ return indices;
+ }
+ }
+
+ @Override
+ public ArrayList<BufferManager.PositionHolder> readIndexFile(String trackId)
+ throws IOException {
+ File file = new File(getBufferDir(), trackId + IDX_FILE_SUFFIX_V2);
+ if (file.exists()) {
+ return readNewIndexFile(file);
+ } else {
+ return readOldIndexFile(new File(getBufferDir(), trackId + IDX_FILE_SUFFIX));
+ }
+ }
+
+ private void writeFormatInt(DataOutputStream out, MediaFormat format, String key)
+ throws IOException {
+ if (format.containsKey(key)) {
+ out.writeInt(format.getInteger(key));
+ } else {
+ out.writeInt(NO_VALUE);
+ }
+ }
+
+ private void writeFormatLong(DataOutputStream out, MediaFormat format, String key)
+ throws IOException {
+ if (format.containsKey(key)) {
+ out.writeLong(format.getLong(key));
+ } else {
+ out.writeLong(NO_VALUE_LONG);
+ }
+ }
+
+ private void writeFormatFloat(DataOutputStream out, MediaFormat format, String key)
+ throws IOException {
+ if (format.containsKey(key)) {
+ out.writeFloat(format.getFloat(key));
+ } else {
+ out.writeFloat(NO_VALUE);
+ }
+ }
+
+ private void writeString(DataOutputStream out, String str) throws IOException {
+ byte[] data = str.getBytes(StandardCharsets.UTF_8);
+ out.writeInt(data.length);
+ if (data.length > 0) {
+ out.write(data);
+ }
+ }
+
+ private void writeFormatString(DataOutputStream out, MediaFormat format, String key)
+ throws IOException {
+ if (format.containsKey(key)) {
+ writeString(out, format.getString(key));
+ } else {
+ out.writeInt(0);
+ }
+ }
+
+ private void writeByteBuffer(DataOutputStream out, ByteBuffer buffer) throws IOException {
+ byte[] data = new byte[buffer.limit()];
+ buffer.get(data);
+ buffer.flip();
+ out.writeInt(data.length);
+ if (data.length > 0) {
+ out.write(data);
+ } else {
+ out.writeInt(0);
+ }
+ }
+
+ private void writeFormatByteBuffer(DataOutputStream out, MediaFormat format, String key)
+ throws IOException {
+ if (format.containsKey(key)) {
+ writeByteBuffer(out, format.getByteBuffer(key));
+ } else {
+ out.writeInt(0);
+ }
+ }
+
+ @Override
+ public void writeTrackInfoFiles(List<BufferManager.TrackFormat> formatList, boolean isAudio)
+ throws IOException {
+ for (int i = 0; i < formatList.size(); ++i) {
+ BufferManager.TrackFormat trackFormat = formatList.get(i);
+ String fileName =
+ (isAudio ? META_FILE_TYPE_AUDIO : META_FILE_TYPE_VIDEO)
+ + ((i == 0) ? META_FILE_SUFFIX : (i + META_FILE_SUFFIX));
+ File file = new File(getBufferDir(), fileName);
+ try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) {
+ writeString(out, trackFormat.trackId);
+ writeFormatString(out, trackFormat.format, MediaFormat.KEY_MIME);
+ writeFormatInt(out, trackFormat.format, MediaFormat.KEY_MAX_INPUT_SIZE);
+ writeFormatInt(out, trackFormat.format, MediaFormat.KEY_WIDTH);
+ writeFormatInt(out, trackFormat.format, MediaFormat.KEY_HEIGHT);
+ writeFormatInt(out, trackFormat.format, MediaFormat.KEY_CHANNEL_COUNT);
+ writeFormatInt(out, trackFormat.format, MediaFormat.KEY_SAMPLE_RATE);
+ writeFormatFloat(out, trackFormat.format, KEY_PIXEL_WIDTH_HEIGHT_RATIO);
+ for (int j = 0; j < 3; ++j) {
+ writeFormatByteBuffer(out, trackFormat.format, "csd-" + j);
+ }
+ writeFormatLong(out, trackFormat.format, MediaFormat.KEY_DURATION);
+ writeFormatString(out, trackFormat.format, MediaFormat.KEY_LANGUAGE);
+ }
+ }
+ }
+
+ /**
+ * Writes caption information to files.
+ *
+ * @param tracks a list of {@link AtscCaptionTrack} objects which store caption information.
+ */
+ public void writeCaptionInfoFiles(List<AtscCaptionTrack> tracks) {
+ if (tracks == null || tracks.isEmpty()) {
+ return;
+ }
+ for (int i = 0; i < tracks.size(); i++) {
+ AtscCaptionTrack track = tracks.get(i);
+ String fileName =
+ META_FILE_TYPE_CAPTION + ((i == 0) ? META_FILE_SUFFIX : (i + META_FILE_SUFFIX));
+ File file = new File(getBufferDir(), fileName);
+ try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) {
+ out.write(MessageNano.toByteArray(track));
+ } catch (Exception e) {
+ Log.e(TAG, "Fail to write caption info to files", e);
+ }
+ }
+ }
+
+ @Override
+ public void writeIndexFile(String trackName, SortedMap<Long, Pair<SampleChunk, Integer>> index)
+ throws IOException {
+ File indexFile = new File(getBufferDir(), trackName + IDX_FILE_SUFFIX_V2);
+ try (DataOutputStream out = new DataOutputStream(new FileOutputStream(indexFile))) {
+ out.writeLong(index.size());
+ for (Map.Entry<Long, Pair<SampleChunk, Integer>> entry : index.entrySet()) {
+ out.writeLong(entry.getKey());
+ out.writeLong(entry.getValue().first.getStartPositionUs());
+ out.writeInt(entry.getValue().second);
+ }
+ }
+ }
+}