Home | History | Annotate | Download | only in mdat
      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 
     17 package com.coremedia.iso.boxes.mdat;
     18 
     19 import com.coremedia.iso.BoxParser;
     20 import com.coremedia.iso.ChannelHelper;
     21 import com.coremedia.iso.boxes.Box;
     22 import com.coremedia.iso.boxes.ContainerBox;
     23 import com.googlecode.mp4parser.AbstractBox;
     24 
     25 import java.io.IOException;
     26 import java.lang.ref.Reference;
     27 import java.lang.ref.SoftReference;
     28 import java.nio.ByteBuffer;
     29 import java.nio.channels.FileChannel;
     30 import java.nio.channels.ReadableByteChannel;
     31 import java.nio.channels.WritableByteChannel;
     32 import java.util.HashMap;
     33 import java.util.Map;
     34 import java.util.logging.Logger;
     35 
     36 import 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  */
     49 public 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