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