1/*
2 * Copyright 2008 CoreMedia AG, 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.coremedia.iso.boxes.mdat;
18
19import com.coremedia.iso.BoxParser;
20import com.coremedia.iso.ChannelHelper;
21import com.coremedia.iso.boxes.Box;
22import com.coremedia.iso.boxes.ContainerBox;
23import com.googlecode.mp4parser.AbstractBox;
24
25import java.io.IOException;
26import java.lang.ref.Reference;
27import java.lang.ref.SoftReference;
28import java.nio.ByteBuffer;
29import java.nio.channels.FileChannel;
30import java.nio.channels.ReadableByteChannel;
31import java.nio.channels.WritableByteChannel;
32import java.util.HashMap;
33import java.util.Map;
34import java.util.logging.Logger;
35
36import static com.googlecode.mp4parser.util.CastUtils.l2i;
37
38/**
39 * This box contains the media data. In video tracks, this box would contain video frames. A presentation may
40 * contain zero or more Media Data Boxes. The actual media data follows the type field; its structure is described
41 * by the metadata (see {@link com.coremedia.iso.boxes.SampleTableBox}).<br>
42 * In large presentations, it may be desirable to have more data in this box than a 32-bit size would permit. In this
43 * case, the large variant of the size field is used.<br>
44 * There may be any number of these boxes in the file (including zero, if all the media data is in other files). The
45 * metadata refers to media data by its absolute offset within the file (see {@link com.coremedia.iso.boxes.StaticChunkOffsetBox});
46 * so Media Data Box headers and free space may easily be skipped, and files without any box structure may
47 * also be referenced and used.
48 */
49public final class MediaDataBox implements Box {
50    private static Logger LOG = Logger.getLogger(MediaDataBox.class.getName());
51
52    public static final String TYPE = "mdat";
53    public static final int BUFFER_SIZE = 10 * 1024 * 1024;
54    ContainerBox parent;
55
56    ByteBuffer header;
57
58    // These fields are for the special case of a FileChannel as input.
59    private FileChannel fileChannel;
60    private long startPosition;
61    private long contentSize;
62
63
64    private Map<Long, Reference<ByteBuffer>> cache = new HashMap<Long, Reference<ByteBuffer>>();
65
66
67    /**
68     * If the whole content is just in one mapped buffer keep a strong reference to it so it is
69     * not evicted from the cache.
70     */
71    private ByteBuffer content;
72
73    public ContainerBox getParent() {
74        return parent;
75    }
76
77    public void setParent(ContainerBox parent) {
78        this.parent = parent;
79    }
80
81    public String getType() {
82        return TYPE;
83    }
84
85    private static void transfer(FileChannel from, long position, long count, WritableByteChannel to) throws IOException {
86        long maxCount = (64 * 1024 * 1024) - (32 * 1024);
87        // Transfer data in chunks a bit less than 64MB
88        // People state that this is a kind of magic number on Windows.
89        // I don't care. The size seems reasonable.
90        long offset = 0;
91        while (offset < count) {
92            offset += from.transferTo(position + offset, Math.min(maxCount, count - offset), to);
93        }
94    }
95
96    public void getBox(WritableByteChannel writableByteChannel) throws IOException {
97        if (fileChannel != null) {
98            assert checkStillOk();
99            transfer(fileChannel, startPosition - header.limit(), contentSize + header.limit(), writableByteChannel);
100        } else {
101            header.rewind();
102            writableByteChannel.write(header);
103            writableByteChannel.write(content);
104        }
105    }
106
107    /**
108     * If someone use the same file as source and sink it could the case that
109     * inserting a few bytes before the mdat results in overwrting data we still
110     * need to write this mdat here. This method just makes sure that we haven't already
111     * overwritten the mdat contents.
112     *
113     * @return true if ok
114     */
115    private boolean checkStillOk() {
116        try {
117            fileChannel.position(startPosition - header.limit());
118            ByteBuffer h2 = ByteBuffer.allocate(header.limit());
119            fileChannel.read(h2);
120            header.rewind();
121            h2.rewind();
122            assert h2.equals(header) : "It seems that the content I want to read has already been overwritten.";
123            return true;
124        } catch (IOException e) {
125            e.printStackTrace();
126            return false;
127        }
128
129    }
130
131
132    public long getSize() {
133        long size = header.limit();
134        size += contentSize;
135        return size;
136    }
137
138    public void parse(ReadableByteChannel readableByteChannel, ByteBuffer header, long contentSize, BoxParser boxParser) throws IOException {
139        this.header = header;
140        this.contentSize = contentSize;
141
142        if (readableByteChannel instanceof FileChannel && (contentSize > AbstractBox.MEM_MAP_THRESHOLD)) {
143            this.fileChannel = ((FileChannel) readableByteChannel);
144            this.startPosition = ((FileChannel) readableByteChannel).position();
145            ((FileChannel) readableByteChannel).position(((FileChannel) readableByteChannel).position() + contentSize);
146        } else {
147            content = ChannelHelper.readFully(readableByteChannel, l2i(contentSize));
148            cache.put(0l, new SoftReference<ByteBuffer>(content));
149        }
150    }
151
152    public synchronized ByteBuffer getContent(long offset, int length) {
153
154        for (Long chacheEntryOffset : cache.keySet()) {
155            if (chacheEntryOffset <= offset && offset <= chacheEntryOffset + BUFFER_SIZE) {
156                ByteBuffer cacheEntry = cache.get(chacheEntryOffset).get();
157                if ((cacheEntry != null) && ((chacheEntryOffset + cacheEntry.limit()) >= (offset + length))) {
158                    // CACHE HIT
159                    cacheEntry.position((int) (offset - chacheEntryOffset));
160                    ByteBuffer cachedSample = cacheEntry.slice();
161                    cachedSample.limit(length);
162                    return cachedSample;
163                }
164            }
165        }
166        // CACHE MISS
167        ByteBuffer cacheEntry;
168        try {
169            // Just mapping 10MB at a time. Seems reasonable.
170            cacheEntry = fileChannel.map(FileChannel.MapMode.READ_ONLY, startPosition + offset, Math.min(BUFFER_SIZE, contentSize - offset));
171        } catch (IOException e1) {
172            LOG.fine("Even mapping just 10MB of the source file into the memory failed. " + e1);
173            throw new RuntimeException(
174                    "Delayed reading of mdat content failed. Make sure not to close " +
175                            "the FileChannel that has been used to create the IsoFile!", e1);
176        }
177        cache.put(offset, new SoftReference<ByteBuffer>(cacheEntry));
178        cacheEntry.position(0);
179        ByteBuffer cachedSample = cacheEntry.slice();
180        cachedSample.limit(length);
181        return cachedSample;
182    }
183
184
185    public ByteBuffer getHeader() {
186        return header;
187    }
188
189}
190