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 17 package com.android.tv.tuner.exoplayer.buffer; 18 19 import android.media.MediaFormat; 20 import android.util.Pair; 21 22 import java.io.DataInputStream; 23 import java.io.DataOutputStream; 24 import java.io.File; 25 import java.io.FileInputStream; 26 import java.io.FileOutputStream; 27 import java.io.IOException; 28 import java.nio.ByteBuffer; 29 import java.nio.charset.StandardCharsets; 30 import java.util.ArrayList; 31 import java.util.SortedMap; 32 33 /** 34 * Manages DVR storage. 35 */ 36 public class DvrStorageManager implements BufferManager.StorageManager { 37 38 // TODO: make serializable classes and use protobuf after internal data structure is finalized. 39 private static final String KEY_PIXEL_WIDTH_HEIGHT_RATIO = 40 "com.google.android.videos.pixelWidthHeightRatio"; 41 private static final String META_FILE_SUFFIX = ".meta"; 42 private static final String IDX_FILE_SUFFIX = ".idx"; 43 44 // Size of minimum reserved storage buffer which will be used to save meta files 45 // and index files after actual recording finished. 46 private static final long MIN_BUFFER_BYTES = 256L * 1024 * 1024; 47 private static final int NO_VALUE = -1; 48 private static final long NO_VALUE_LONG = -1L; 49 50 private final File mBufferDir; 51 52 // {@code true} when this is for recording, {@code false} when this is for replaying. 53 private final boolean mIsRecording; 54 55 public DvrStorageManager(File file, boolean isRecording) { 56 mBufferDir = file; 57 mBufferDir.mkdirs(); 58 mIsRecording = isRecording; 59 } 60 61 @Override 62 public void clearStorage() { 63 if (mIsRecording) { 64 File[] files = mBufferDir.listFiles(); 65 if (files != null && files.length > 0) { 66 for (File file : files) { 67 file.delete(); 68 } 69 } 70 } 71 } 72 73 @Override 74 public File getBufferDir() { 75 return mBufferDir; 76 } 77 78 @Override 79 public boolean isPersistent() { 80 return true; 81 } 82 83 @Override 84 public boolean reachedStorageMax(long bufferSize, long pendingDelete) { 85 return false; 86 } 87 88 @Override 89 public boolean hasEnoughBuffer(long pendingDelete) { 90 return !mIsRecording || mBufferDir.getUsableSpace() >= MIN_BUFFER_BYTES; 91 } 92 93 private void readFormatInt(DataInputStream in, MediaFormat format, String key) 94 throws IOException { 95 int val = in.readInt(); 96 if (val != NO_VALUE) { 97 format.setInteger(key, val); 98 } 99 } 100 101 private void readFormatLong(DataInputStream in, MediaFormat format, String key) 102 throws IOException { 103 long val = in.readLong(); 104 if (val != NO_VALUE_LONG) { 105 format.setLong(key, val); 106 } 107 } 108 109 private void readFormatFloat(DataInputStream in, MediaFormat format, String key) 110 throws IOException { 111 float val = in.readFloat(); 112 if (val != NO_VALUE) { 113 format.setFloat(key, val); 114 } 115 } 116 117 private String readString(DataInputStream in) throws IOException { 118 int len = in.readInt(); 119 if (len <= 0) { 120 return null; 121 } 122 byte [] strBytes = new byte[len]; 123 in.readFully(strBytes); 124 return new String(strBytes, StandardCharsets.UTF_8); 125 } 126 127 private void readFormatString(DataInputStream in, MediaFormat format, String key) 128 throws IOException { 129 String str = readString(in); 130 if (str != null) { 131 format.setString(key, str); 132 } 133 } 134 135 private ByteBuffer readByteBuffer(DataInputStream in) throws IOException { 136 int len = in.readInt(); 137 if (len <= 0) { 138 return null; 139 } 140 byte [] bytes = new byte[len]; 141 in.readFully(bytes); 142 ByteBuffer buffer = ByteBuffer.allocate(len); 143 buffer.put(bytes); 144 buffer.flip(); 145 146 return buffer; 147 } 148 149 private void readFormatByteBuffer(DataInputStream in, MediaFormat format, String key) 150 throws IOException { 151 ByteBuffer buffer = readByteBuffer(in); 152 if (buffer != null) { 153 format.setByteBuffer(key, buffer); 154 } 155 } 156 157 @Override 158 public Pair<String, MediaFormat> readTrackInfoFile(boolean isAudio) throws IOException { 159 File file = new File(getBufferDir(), (isAudio ? "audio" : "video") + META_FILE_SUFFIX); 160 try (DataInputStream in = new DataInputStream(new FileInputStream(file))) { 161 String name = readString(in); 162 MediaFormat format = new MediaFormat(); 163 readFormatString(in, format, MediaFormat.KEY_MIME); 164 readFormatInt(in, format, MediaFormat.KEY_MAX_INPUT_SIZE); 165 readFormatInt(in, format, MediaFormat.KEY_WIDTH); 166 readFormatInt(in, format, MediaFormat.KEY_HEIGHT); 167 readFormatInt(in, format, MediaFormat.KEY_CHANNEL_COUNT); 168 readFormatInt(in, format, MediaFormat.KEY_SAMPLE_RATE); 169 readFormatFloat(in, format, KEY_PIXEL_WIDTH_HEIGHT_RATIO); 170 for (int i = 0; i < 3; ++i) { 171 readFormatByteBuffer(in, format, "csd-" + i); 172 } 173 readFormatLong(in, format, MediaFormat.KEY_DURATION); 174 return new Pair<>(name, format); 175 } 176 } 177 178 @Override 179 public ArrayList<Long> readIndexFile(String trackId) throws IOException { 180 ArrayList<Long> indices = new ArrayList<>(); 181 File file = new File(getBufferDir(), trackId + IDX_FILE_SUFFIX); 182 try (DataInputStream in = new DataInputStream(new FileInputStream(file))) { 183 long count = in.readLong(); 184 for (long i = 0; i < count; ++i) { 185 indices.add(in.readLong()); 186 } 187 return indices; 188 } 189 } 190 191 private void writeFormatInt(DataOutputStream out, MediaFormat format, String key) 192 throws IOException { 193 if (format.containsKey(key)) { 194 out.writeInt(format.getInteger(key)); 195 } else { 196 out.writeInt(NO_VALUE); 197 } 198 } 199 200 private void writeFormatLong(DataOutputStream out, MediaFormat format, String key) 201 throws IOException { 202 if (format.containsKey(key)) { 203 out.writeLong(format.getLong(key)); 204 } else { 205 out.writeLong(NO_VALUE_LONG); 206 } 207 } 208 209 private void writeFormatFloat(DataOutputStream out, MediaFormat format, String key) 210 throws IOException { 211 if (format.containsKey(key)) { 212 out.writeFloat(format.getFloat(key)); 213 } else { 214 out.writeFloat(NO_VALUE); 215 } 216 } 217 218 private void writeString(DataOutputStream out, String str) throws IOException { 219 byte [] data = str.getBytes(StandardCharsets.UTF_8); 220 out.writeInt(data.length); 221 if (data.length > 0) { 222 out.write(data); 223 } 224 } 225 226 private void writeFormatString(DataOutputStream out, MediaFormat format, String key) 227 throws IOException { 228 if (format.containsKey(key)) { 229 writeString(out, format.getString(key)); 230 } else { 231 out.writeInt(0); 232 } 233 } 234 235 private void writeByteBuffer(DataOutputStream out, ByteBuffer buffer) throws IOException { 236 byte [] data = new byte[buffer.limit()]; 237 buffer.get(data); 238 buffer.flip(); 239 out.writeInt(data.length); 240 if (data.length > 0) { 241 out.write(data); 242 } else { 243 out.writeInt(0); 244 } 245 } 246 247 private void writeFormatByteBuffer(DataOutputStream out, MediaFormat format, String key) 248 throws IOException { 249 if (format.containsKey(key)) { 250 writeByteBuffer(out, format.getByteBuffer(key)); 251 } else { 252 out.writeInt(0); 253 } 254 } 255 256 @Override 257 public void writeTrackInfoFile(String trackId, MediaFormat format, boolean isAudio) 258 throws IOException { 259 File file = new File(getBufferDir(), (isAudio ? "audio" : "video") + META_FILE_SUFFIX); 260 try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) { 261 writeString(out, trackId); 262 writeFormatString(out, format, MediaFormat.KEY_MIME); 263 writeFormatInt(out, format, MediaFormat.KEY_MAX_INPUT_SIZE); 264 writeFormatInt(out, format, MediaFormat.KEY_WIDTH); 265 writeFormatInt(out, format, MediaFormat.KEY_HEIGHT); 266 writeFormatInt(out, format, MediaFormat.KEY_CHANNEL_COUNT); 267 writeFormatInt(out, format, MediaFormat.KEY_SAMPLE_RATE); 268 writeFormatFloat(out, format, KEY_PIXEL_WIDTH_HEIGHT_RATIO); 269 for (int i = 0; i < 3; ++i) { 270 writeFormatByteBuffer(out, format, "csd-" + i); 271 } 272 writeFormatLong(out, format, MediaFormat.KEY_DURATION); 273 } 274 } 275 276 @Override 277 public void writeIndexFile(String trackName, SortedMap<Long, SampleChunk> index) 278 throws IOException { 279 File indexFile = new File(getBufferDir(), trackName + IDX_FILE_SUFFIX); 280 try (DataOutputStream out = new DataOutputStream(new FileOutputStream(indexFile))) { 281 out.writeLong(index.size()); 282 for (Long key : index.keySet()) { 283 out.writeLong(key); 284 } 285 } 286 } 287 } 288