• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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