Home | History | Annotate | Download | only in mp4parser
      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 
     60     protected AbstractBox(String type) {
     61         this.type = type;
     62     }
     63 
     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      */
     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      */
     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      */
     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
    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 {
    133             content.rewind();
    134             bb.put(content);
    135         }
    136         bb.rewind();
    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      */
    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      */
    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      */
    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
    184     public String getType() {
    185         return type;
    186     }
    187 
    188     @DoNotParseDetail
    189     public byte[] getUserType() {
    190         return userType;
    191     }
    192 
    193     @DoNotParseDetail
    194     public ContainerBox getParent() {
    195         return parent;
    196     }
    197 
    198     @DoNotParseDetail
    199     public void setParent(ContainerBox parent) {
    200         this.parent = parent;
    201     }
    202 
    203     @DoNotParseDetail
    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      */
    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      */
    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 
    260     private boolean isSmallBox() {
    261         return (content == null ? (getContentSize() + (deadBytes != null ? deadBytes.limit() : 0) + 8) : content.limit()) < 1L << 32;
    262     }
    263 
    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