diff options
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.java | 391 |
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); + } + } + } +} |