| /* |
| * Copyright 2012 Sebastian Annies, Hamburg |
| * |
| * 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.googlecode.mp4parser; |
| |
| import com.coremedia.iso.BoxParser; |
| import com.coremedia.iso.ChannelHelper; |
| import com.coremedia.iso.Hex; |
| import com.coremedia.iso.IsoFile; |
| import com.coremedia.iso.IsoTypeWriter; |
| import com.coremedia.iso.boxes.Box; |
| import com.coremedia.iso.boxes.ContainerBox; |
| import com.coremedia.iso.boxes.UserBox; |
| import com.googlecode.mp4parser.annotations.DoNotParseDetail; |
| |
| import java.io.IOException; |
| import java.nio.ByteBuffer; |
| import java.nio.channels.FileChannel; |
| import java.nio.channels.ReadableByteChannel; |
| import java.nio.channels.WritableByteChannel; |
| import java.util.logging.Logger; |
| |
| import static com.googlecode.mp4parser.util.CastUtils.l2i; |
| |
| /** |
| * A basic on-demand parsing box. Requires the implementation of three methods to become a fully working box: |
| * <ol> |
| * <li>{@link #_parseDetails(java.nio.ByteBuffer)}</li> |
| * <li>{@link #getContent(java.nio.ByteBuffer)}</li> |
| * <li>{@link #getContentSize()}</li> |
| * </ol> |
| * additionally this new box has to be put into the <code>isoparser-default.properties</code> file so that |
| * it is accessible by the <code>PropertyBoxParserImpl</code> |
| */ |
| public abstract class AbstractBox implements Box { |
| public static int MEM_MAP_THRESHOLD = 100 * 1024; |
| private static Logger LOG = Logger.getLogger(AbstractBox.class.getName()); |
| |
| protected String type; |
| private byte[] userType; |
| private ContainerBox parent; |
| |
| private ByteBuffer content; |
| private ByteBuffer deadBytes = null; |
| |
| |
| protected AbstractBox(String type) { |
| this.type = type; |
| } |
| |
| protected AbstractBox(String type, byte[] userType) { |
| this.type = type; |
| this.userType = userType; |
| } |
| |
| /** |
| * Get the box's content size without its header. This must be the exact number of bytes |
| * that <code>getContent(ByteBuffer)</code> writes. |
| * |
| * @return Gets the box's content size in bytes |
| * @see #getContent(java.nio.ByteBuffer) |
| */ |
| protected abstract long getContentSize(); |
| |
| /** |
| * Write the box's content into the given <code>ByteBuffer</code>. This must include flags |
| * and version in case of a full box. <code>byteBuffer</code> has been initialized with |
| * <code>getSize()</code> bytes. |
| * |
| * @param byteBuffer the sink for the box's content |
| */ |
| protected abstract void getContent(ByteBuffer byteBuffer); |
| |
| /** |
| * Parse the box's fields and child boxes if any. |
| * |
| * @param content the box's raw content beginning after the 4-cc field. |
| */ |
| protected abstract void _parseDetails(ByteBuffer content); |
| |
| /** |
| * Read the box's content from a byte channel without parsing it. Parsing is done on-demand. |
| * |
| * @param readableByteChannel the (part of the) iso file to parse |
| * @param contentSize expected contentSize of the box |
| * @param boxParser creates inner boxes |
| * @throws IOException in case of an I/O error. |
| */ |
| @DoNotParseDetail |
| public void parse(ReadableByteChannel readableByteChannel, ByteBuffer header, long contentSize, BoxParser boxParser) throws IOException { |
| if (readableByteChannel instanceof FileChannel && contentSize > MEM_MAP_THRESHOLD) { |
| // todo: if I map this here delayed I could use transferFrom/transferTo in the getBox method |
| // todo: potentially this could speed up writing. |
| // |
| // It's quite expensive to map a file into the memory. Just do it when the box is larger than a MB. |
| content = ((FileChannel) readableByteChannel).map(FileChannel.MapMode.READ_ONLY, ((FileChannel) readableByteChannel).position(), contentSize); |
| ((FileChannel) readableByteChannel).position(((FileChannel) readableByteChannel).position() + contentSize); |
| } else { |
| assert contentSize < Integer.MAX_VALUE; |
| content = ChannelHelper.readFully(readableByteChannel, contentSize); |
| } |
| } |
| |
| public void getBox(WritableByteChannel os) throws IOException { |
| ByteBuffer bb = ByteBuffer.allocate(l2i(getSize())); |
| getHeader(bb); |
| if (content == null) { |
| getContent(bb); |
| if (deadBytes != null) { |
| deadBytes.rewind(); |
| while (deadBytes.remaining() > 0) { |
| bb.put(deadBytes); |
| } |
| } |
| } else { |
| content.rewind(); |
| bb.put(content); |
| } |
| bb.rewind(); |
| os.write(bb); |
| } |
| |
| |
| /** |
| * Parses the raw content of the box. It surrounds the actual parsing |
| * which is done |
| */ |
| synchronized final void parseDetails() { |
| if (content != null) { |
| ByteBuffer content = this.content; |
| this.content = null; |
| content.rewind(); |
| _parseDetails(content); |
| if (content.remaining() > 0) { |
| deadBytes = content.slice(); |
| } |
| assert verify(content); |
| } |
| } |
| |
| /** |
| * Sets the 'dead' bytes. These bytes are left if the content of the box |
| * has been parsed but not all bytes have been used up. |
| * |
| * @param newDeadBytes the unused bytes with no meaning but required for bytewise reconstruction |
| */ |
| protected void setDeadBytes(ByteBuffer newDeadBytes) { |
| deadBytes = newDeadBytes; |
| } |
| |
| |
| /** |
| * Gets the full size of the box including header and content. |
| * |
| * @return the box's size |
| */ |
| public long getSize() { |
| long size = (content == null ? getContentSize() : content.limit()); |
| size += (8 + // size|type |
| (size >= ((1L << 32) - 8) ? 8 : 0) + // 32bit - 8 byte size and type |
| (UserBox.TYPE.equals(getType()) ? 16 : 0)); |
| size += (deadBytes == null ? 0 : deadBytes.limit()); |
| return size; |
| } |
| |
| @DoNotParseDetail |
| public String getType() { |
| return type; |
| } |
| |
| @DoNotParseDetail |
| public byte[] getUserType() { |
| return userType; |
| } |
| |
| @DoNotParseDetail |
| public ContainerBox getParent() { |
| return parent; |
| } |
| |
| @DoNotParseDetail |
| public void setParent(ContainerBox parent) { |
| this.parent = parent; |
| } |
| |
| @DoNotParseDetail |
| public IsoFile getIsoFile() { |
| return parent.getIsoFile(); |
| } |
| |
| /** |
| * Check if details are parsed. |
| * |
| * @return <code>true</code> whenever the content <code>ByteBuffer</code> is not <code>null</code> |
| */ |
| public boolean isParsed() { |
| return content == null; |
| } |
| |
| |
| /** |
| * Verifies that a box can be reconstructed byte-exact after parsing. |
| * |
| * @param content the raw content of the box |
| * @return <code>true</code> if raw content exactly matches the reconstructed content |
| */ |
| private boolean verify(ByteBuffer content) { |
| ByteBuffer bb = ByteBuffer.allocate(l2i(getContentSize() + (deadBytes != null ? deadBytes.limit() : 0))); |
| getContent(bb); |
| if (deadBytes != null) { |
| deadBytes.rewind(); |
| while (deadBytes.remaining() > 0) { |
| bb.put(deadBytes); |
| } |
| } |
| content.rewind(); |
| bb.rewind(); |
| |
| |
| if (content.remaining() != bb.remaining()) { |
| LOG.severe(this.getType() + ": remaining differs " + content.remaining() + " vs. " + bb.remaining()); |
| return false; |
| } |
| int p = content.position(); |
| for (int i = content.limit() - 1, j = bb.limit() - 1; i >= p; i--, j--) { |
| byte v1 = content.get(i); |
| byte v2 = bb.get(j); |
| if (v1 != v2) { |
| LOG.severe(String.format("%s: buffers differ at %d: %2X/%2X", this.getType(), i, v1, v2)); |
| byte[] b1 = new byte[content.remaining()]; |
| byte[] b2 = new byte[bb.remaining()]; |
| content.get(b1); |
| bb.get(b2); |
| System.err.println("original : " + Hex.encodeHex(b1, 4)); |
| System.err.println("reconstructed : " + Hex.encodeHex(b2, 4)); |
| return false; |
| } |
| } |
| return true; |
| |
| } |
| |
| private boolean isSmallBox() { |
| return (content == null ? (getContentSize() + (deadBytes != null ? deadBytes.limit() : 0) + 8) : content.limit()) < 1L << 32; |
| } |
| |
| private void getHeader(ByteBuffer byteBuffer) { |
| if (isSmallBox()) { |
| IsoTypeWriter.writeUInt32(byteBuffer, this.getSize()); |
| byteBuffer.put(IsoFile.fourCCtoBytes(getType())); |
| } else { |
| IsoTypeWriter.writeUInt32(byteBuffer, 1); |
| byteBuffer.put(IsoFile.fourCCtoBytes(getType())); |
| IsoTypeWriter.writeUInt64(byteBuffer, getSize()); |
| } |
| if (UserBox.TYPE.equals(getType())) { |
| byteBuffer.put(getUserType()); |
| } |
| |
| |
| } |
| } |