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
17package com.googlecode.mp4parser;
18
19import com.coremedia.iso.BoxParser;
20import com.coremedia.iso.ChannelHelper;
21import com.coremedia.iso.Hex;
22import com.coremedia.iso.IsoFile;
23import com.coremedia.iso.IsoTypeWriter;
24import com.coremedia.iso.boxes.Box;
25import com.coremedia.iso.boxes.ContainerBox;
26import com.coremedia.iso.boxes.UserBox;
27import com.googlecode.mp4parser.annotations.DoNotParseDetail;
28
29import java.io.IOException;
30import java.nio.ByteBuffer;
31import java.nio.channels.FileChannel;
32import java.nio.channels.ReadableByteChannel;
33import java.nio.channels.WritableByteChannel;
34import java.util.logging.Logger;
35
36import 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 */
48public 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