1 /* 2 * Copyright 2008 CoreMedia AG, Hamburg 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.coremedia.iso.boxes.mdat; 18 19 import com.coremedia.iso.BoxParser; 20 import com.coremedia.iso.ChannelHelper; 21 import com.coremedia.iso.boxes.Box; 22 import com.coremedia.iso.boxes.ContainerBox; 23 import com.googlecode.mp4parser.AbstractBox; 24 25 import java.io.IOException; 26 import java.lang.ref.Reference; 27 import java.lang.ref.SoftReference; 28 import java.nio.ByteBuffer; 29 import java.nio.channels.FileChannel; 30 import java.nio.channels.ReadableByteChannel; 31 import java.nio.channels.WritableByteChannel; 32 import java.util.HashMap; 33 import java.util.Map; 34 import java.util.logging.Logger; 35 36 import static com.googlecode.mp4parser.util.CastUtils.l2i; 37 38 /** 39 * This box contains the media data. In video tracks, this box would contain video frames. A presentation may 40 * contain zero or more Media Data Boxes. The actual media data follows the type field; its structure is described 41 * by the metadata (see {@link com.coremedia.iso.boxes.SampleTableBox}).<br> 42 * In large presentations, it may be desirable to have more data in this box than a 32-bit size would permit. In this 43 * case, the large variant of the size field is used.<br> 44 * There may be any number of these boxes in the file (including zero, if all the media data is in other files). The 45 * metadata refers to media data by its absolute offset within the file (see {@link com.coremedia.iso.boxes.StaticChunkOffsetBox}); 46 * so Media Data Box headers and free space may easily be skipped, and files without any box structure may 47 * also be referenced and used. 48 */ 49 public final class MediaDataBox implements Box { 50 private static Logger LOG = Logger.getLogger(MediaDataBox.class.getName()); 51 52 public static final String TYPE = "mdat"; 53 public static final int BUFFER_SIZE = 10 * 1024 * 1024; 54 ContainerBox parent; 55 56 ByteBuffer header; 57 58 // These fields are for the special case of a FileChannel as input. 59 private FileChannel fileChannel; 60 private long startPosition; 61 private long contentSize; 62 63 64 private Map<Long, Reference<ByteBuffer>> cache = new HashMap<Long, Reference<ByteBuffer>>(); 65 66 67 /** 68 * If the whole content is just in one mapped buffer keep a strong reference to it so it is 69 * not evicted from the cache. 70 */ 71 private ByteBuffer content; 72 getParent()73 public ContainerBox getParent() { 74 return parent; 75 } 76 setParent(ContainerBox parent)77 public void setParent(ContainerBox parent) { 78 this.parent = parent; 79 } 80 getType()81 public String getType() { 82 return TYPE; 83 } 84 transfer(FileChannel from, long position, long count, WritableByteChannel to)85 private static void transfer(FileChannel from, long position, long count, WritableByteChannel to) throws IOException { 86 long maxCount = (64 * 1024 * 1024) - (32 * 1024); 87 // Transfer data in chunks a bit less than 64MB 88 // People state that this is a kind of magic number on Windows. 89 // I don't care. The size seems reasonable. 90 long offset = 0; 91 while (offset < count) { 92 offset += from.transferTo(position + offset, Math.min(maxCount, count - offset), to); 93 } 94 } 95 getBox(WritableByteChannel writableByteChannel)96 public void getBox(WritableByteChannel writableByteChannel) throws IOException { 97 if (fileChannel != null) { 98 assert checkStillOk(); 99 transfer(fileChannel, startPosition - header.limit(), contentSize + header.limit(), writableByteChannel); 100 } else { 101 header.rewind(); 102 writableByteChannel.write(header); 103 writableByteChannel.write(content); 104 } 105 } 106 107 /** 108 * If someone use the same file as source and sink it could the case that 109 * inserting a few bytes before the mdat results in overwrting data we still 110 * need to write this mdat here. This method just makes sure that we haven't already 111 * overwritten the mdat contents. 112 * 113 * @return true if ok 114 */ checkStillOk()115 private boolean checkStillOk() { 116 try { 117 fileChannel.position(startPosition - header.limit()); 118 ByteBuffer h2 = ByteBuffer.allocate(header.limit()); 119 fileChannel.read(h2); 120 header.rewind(); 121 h2.rewind(); 122 assert h2.equals(header) : "It seems that the content I want to read has already been overwritten."; 123 return true; 124 } catch (IOException e) { 125 e.printStackTrace(); 126 return false; 127 } 128 129 } 130 131 getSize()132 public long getSize() { 133 long size = header.limit(); 134 size += contentSize; 135 return size; 136 } 137 parse(ReadableByteChannel readableByteChannel, ByteBuffer header, long contentSize, BoxParser boxParser)138 public void parse(ReadableByteChannel readableByteChannel, ByteBuffer header, long contentSize, BoxParser boxParser) throws IOException { 139 this.header = header; 140 this.contentSize = contentSize; 141 142 if (readableByteChannel instanceof FileChannel && (contentSize > AbstractBox.MEM_MAP_THRESHOLD)) { 143 this.fileChannel = ((FileChannel) readableByteChannel); 144 this.startPosition = ((FileChannel) readableByteChannel).position(); 145 ((FileChannel) readableByteChannel).position(((FileChannel) readableByteChannel).position() + contentSize); 146 } else { 147 content = ChannelHelper.readFully(readableByteChannel, l2i(contentSize)); 148 cache.put(0l, new SoftReference<ByteBuffer>(content)); 149 } 150 } 151 getContent(long offset, int length)152 public synchronized ByteBuffer getContent(long offset, int length) { 153 154 for (Long chacheEntryOffset : cache.keySet()) { 155 if (chacheEntryOffset <= offset && offset <= chacheEntryOffset + BUFFER_SIZE) { 156 ByteBuffer cacheEntry = cache.get(chacheEntryOffset).get(); 157 if ((cacheEntry != null) && ((chacheEntryOffset + cacheEntry.limit()) >= (offset + length))) { 158 // CACHE HIT 159 cacheEntry.position((int) (offset - chacheEntryOffset)); 160 ByteBuffer cachedSample = cacheEntry.slice(); 161 cachedSample.limit(length); 162 return cachedSample; 163 } 164 } 165 } 166 // CACHE MISS 167 ByteBuffer cacheEntry; 168 try { 169 // Just mapping 10MB at a time. Seems reasonable. 170 cacheEntry = fileChannel.map(FileChannel.MapMode.READ_ONLY, startPosition + offset, Math.min(BUFFER_SIZE, contentSize - offset)); 171 } catch (IOException e1) { 172 LOG.fine("Even mapping just 10MB of the source file into the memory failed. " + e1); 173 throw new RuntimeException( 174 "Delayed reading of mdat content failed. Make sure not to close " + 175 "the FileChannel that has been used to create the IsoFile!", e1); 176 } 177 cache.put(offset, new SoftReference<ByteBuffer>(cacheEntry)); 178 cacheEntry.position(0); 179 ByteBuffer cachedSample = cacheEntry.slice(); 180 cachedSample.limit(length); 181 return cachedSample; 182 } 183 184 getHeader()185 public ByteBuffer getHeader() { 186 return header; 187 } 188 189 } 190