1 /* 2 * Copyright 2012 Sebastian Annies, 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.googlecode.mp4parser; 18 19 import com.coremedia.iso.BoxParser; 20 import com.coremedia.iso.ChannelHelper; 21 import com.coremedia.iso.Hex; 22 import com.coremedia.iso.IsoFile; 23 import com.coremedia.iso.IsoTypeWriter; 24 import com.coremedia.iso.boxes.Box; 25 import com.coremedia.iso.boxes.ContainerBox; 26 import com.coremedia.iso.boxes.UserBox; 27 import com.googlecode.mp4parser.annotations.DoNotParseDetail; 28 29 import java.io.IOException; 30 import java.nio.ByteBuffer; 31 import java.nio.channels.FileChannel; 32 import java.nio.channels.ReadableByteChannel; 33 import java.nio.channels.WritableByteChannel; 34 import java.util.logging.Logger; 35 36 import static com.googlecode.mp4parser.util.CastUtils.l2i; 37 38 /** 39 * A basic on-demand parsing box. Requires the implementation of three methods to become a fully working box: 40 * <ol> 41 * <li>{@link #_parseDetails(java.nio.ByteBuffer)}</li> 42 * <li>{@link #getContent(java.nio.ByteBuffer)}</li> 43 * <li>{@link #getContentSize()}</li> 44 * </ol> 45 * additionally this new box has to be put into the <code>isoparser-default.properties</code> file so that 46 * it is accessible by the <code>PropertyBoxParserImpl</code> 47 */ 48 public abstract class AbstractBox implements Box { 49 public static int MEM_MAP_THRESHOLD = 100 * 1024; 50 private static Logger LOG = Logger.getLogger(AbstractBox.class.getName()); 51 52 protected String type; 53 private byte[] userType; 54 private ContainerBox parent; 55 56 private ByteBuffer content; 57 private ByteBuffer deadBytes = null; 58 59 AbstractBox(String type)60 protected AbstractBox(String type) { 61 this.type = type; 62 } 63 AbstractBox(String type, byte[] userType)64 protected AbstractBox(String type, byte[] userType) { 65 this.type = type; 66 this.userType = userType; 67 } 68 69 /** 70 * Get the box's content size without its header. This must be the exact number of bytes 71 * that <code>getContent(ByteBuffer)</code> writes. 72 * 73 * @return Gets the box's content size in bytes 74 * @see #getContent(java.nio.ByteBuffer) 75 */ getContentSize()76 protected abstract long getContentSize(); 77 78 /** 79 * Write the box's content into the given <code>ByteBuffer</code>. This must include flags 80 * and version in case of a full box. <code>byteBuffer</code> has been initialized with 81 * <code>getSize()</code> bytes. 82 * 83 * @param byteBuffer the sink for the box's content 84 */ getContent(ByteBuffer byteBuffer)85 protected abstract void getContent(ByteBuffer byteBuffer); 86 87 /** 88 * Parse the box's fields and child boxes if any. 89 * 90 * @param content the box's raw content beginning after the 4-cc field. 91 */ _parseDetails(ByteBuffer content)92 protected abstract void _parseDetails(ByteBuffer content); 93 94 /** 95 * Read the box's content from a byte channel without parsing it. Parsing is done on-demand. 96 * 97 * @param readableByteChannel the (part of the) iso file to parse 98 * @param contentSize expected contentSize of the box 99 * @param boxParser creates inner boxes 100 * @throws IOException in case of an I/O error. 101 */ 102 @DoNotParseDetail parse(ReadableByteChannel readableByteChannel, ByteBuffer header, long contentSize, BoxParser boxParser)103 public void parse(ReadableByteChannel readableByteChannel, ByteBuffer header, long contentSize, BoxParser boxParser) throws IOException { 104 if (readableByteChannel instanceof FileChannel && contentSize > MEM_MAP_THRESHOLD) { 105 // todo: if I map this here delayed I could use transferFrom/transferTo in the getBox method 106 // todo: potentially this could speed up writing. 107 // 108 // It's quite expensive to map a file into the memory. Just do it when the box is larger than a MB. 109 content = ((FileChannel) readableByteChannel).map(FileChannel.MapMode.READ_ONLY, ((FileChannel) readableByteChannel).position(), contentSize); 110 ((FileChannel) readableByteChannel).position(((FileChannel) readableByteChannel).position() + contentSize); 111 } else { 112 assert contentSize < Integer.MAX_VALUE; 113 content = ChannelHelper.readFully(readableByteChannel, contentSize); 114 } 115 if (isParsed() == false) { 116 parseDetails(); 117 } 118 119 } 120 121 public void getBox(WritableByteChannel os) throws IOException { 122 ByteBuffer bb = ByteBuffer.allocate(l2i(getSize())); 123 getHeader(bb); 124 if (content == null) { 125 getContent(bb); 126 if (deadBytes != null) { 127 deadBytes.rewind(); 128 while (deadBytes.remaining() > 0) { 129 bb.put(deadBytes); 130 } 131 } 132 } else { content.rewind()133 content.rewind(); 134 bb.put(content); 135 } bb.rewind()136 bb.rewind(); os.write(bb)137 os.write(bb); 138 } 139 140 141 /** 142 * Parses the raw content of the box. It surrounds the actual parsing 143 * which is done 144 */ parseDetails()145 synchronized final void parseDetails() { 146 if (content != null) { 147 ByteBuffer content = this.content; 148 this.content = null; 149 content.rewind(); 150 _parseDetails(content); 151 if (content.remaining() > 0) { 152 deadBytes = content.slice(); 153 } 154 assert verify(content); 155 } 156 } 157 158 /** 159 * Sets the 'dead' bytes. These bytes are left if the content of the box 160 * has been parsed but not all bytes have been used up. 161 * 162 * @param newDeadBytes the unused bytes with no meaning but required for bytewise reconstruction 163 */ setDeadBytes(ByteBuffer newDeadBytes)164 protected void setDeadBytes(ByteBuffer newDeadBytes) { 165 deadBytes = newDeadBytes; 166 } 167 168 169 /** 170 * Gets the full size of the box including header and content. 171 * 172 * @return the box's size 173 */ getSize()174 public long getSize() { 175 long size = (content == null ? getContentSize() : content.limit()); 176 size += (8 + // size|type 177 (size >= ((1L << 32) - 8) ? 8 : 0) + // 32bit - 8 byte size and type 178 (UserBox.TYPE.equals(getType()) ? 16 : 0)); 179 size += (deadBytes == null ? 0 : deadBytes.limit()); 180 return size; 181 } 182 183 @DoNotParseDetail getType()184 public String getType() { 185 return type; 186 } 187 188 @DoNotParseDetail getUserType()189 public byte[] getUserType() { 190 return userType; 191 } 192 193 @DoNotParseDetail getParent()194 public ContainerBox getParent() { 195 return parent; 196 } 197 198 @DoNotParseDetail setParent(ContainerBox parent)199 public void setParent(ContainerBox parent) { 200 this.parent = parent; 201 } 202 203 @DoNotParseDetail getIsoFile()204 public IsoFile getIsoFile() { 205 return parent.getIsoFile(); 206 } 207 208 /** 209 * Check if details are parsed. 210 * 211 * @return <code>true</code> whenever the content <code>ByteBuffer</code> is not <code>null</code> 212 */ isParsed()213 public boolean isParsed() { 214 return content == null; 215 } 216 217 218 /** 219 * Verifies that a box can be reconstructed byte-exact after parsing. 220 * 221 * @param content the raw content of the box 222 * @return <code>true</code> if raw content exactly matches the reconstructed content 223 */ verify(ByteBuffer content)224 private boolean verify(ByteBuffer content) { 225 ByteBuffer bb = ByteBuffer.allocate(l2i(getContentSize() + (deadBytes != null ? deadBytes.limit() : 0))); 226 getContent(bb); 227 if (deadBytes != null) { 228 deadBytes.rewind(); 229 while (deadBytes.remaining() > 0) { 230 bb.put(deadBytes); 231 } 232 } 233 content.rewind(); 234 bb.rewind(); 235 236 237 if (content.remaining() != bb.remaining()) { 238 LOG.severe(this.getType() + ": remaining differs " + content.remaining() + " vs. " + bb.remaining()); 239 return false; 240 } 241 int p = content.position(); 242 for (int i = content.limit() - 1, j = bb.limit() - 1; i >= p; i--, j--) { 243 byte v1 = content.get(i); 244 byte v2 = bb.get(j); 245 if (v1 != v2) { 246 LOG.severe(String.format("%s: buffers differ at %d: %2X/%2X", this.getType(), i, v1, v2)); 247 byte[] b1 = new byte[content.remaining()]; 248 byte[] b2 = new byte[bb.remaining()]; 249 content.get(b1); 250 bb.get(b2); 251 System.err.println("original : " + Hex.encodeHex(b1, 4)); 252 System.err.println("reconstructed : " + Hex.encodeHex(b2, 4)); 253 return false; 254 } 255 } 256 return true; 257 258 } 259 isSmallBox()260 private boolean isSmallBox() { 261 return (content == null ? (getContentSize() + (deadBytes != null ? deadBytes.limit() : 0) + 8) : content.limit()) < 1L << 32; 262 } 263 getHeader(ByteBuffer byteBuffer)264 private void getHeader(ByteBuffer byteBuffer) { 265 if (isSmallBox()) { 266 IsoTypeWriter.writeUInt32(byteBuffer, this.getSize()); 267 byteBuffer.put(IsoFile.fourCCtoBytes(getType())); 268 } else { 269 IsoTypeWriter.writeUInt32(byteBuffer, 1); 270 byteBuffer.put(IsoFile.fourCCtoBytes(getType())); 271 IsoTypeWriter.writeUInt64(byteBuffer, getSize()); 272 } 273 if (UserBox.TYPE.equals(getType())) { 274 byteBuffer.put(getUserType()); 275 } 276 277 278 } 279 } 280