/* * Copyright 2008 CoreMedia AG, Hamburg * * Licensed under the Apache License, Version 2.0 (the License); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an AS IS BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.coremedia.iso.boxes.mdat; import com.coremedia.iso.BoxParser; import com.coremedia.iso.ChannelHelper; import com.coremedia.iso.boxes.Box; import com.coremedia.iso.boxes.ContainerBox; import com.googlecode.mp4parser.AbstractBox; import java.io.IOException; import java.lang.ref.Reference; import java.lang.ref.SoftReference; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; import java.util.HashMap; import java.util.Map; import java.util.logging.Logger; import static com.googlecode.mp4parser.util.CastUtils.l2i; /** * This box contains the media data. In video tracks, this box would contain video frames. A presentation may * contain zero or more Media Data Boxes. The actual media data follows the type field; its structure is described * by the metadata (see {@link com.coremedia.iso.boxes.SampleTableBox}).
* In large presentations, it may be desirable to have more data in this box than a 32-bit size would permit. In this * case, the large variant of the size field is used.
* There may be any number of these boxes in the file (including zero, if all the media data is in other files). The * metadata refers to media data by its absolute offset within the file (see {@link com.coremedia.iso.boxes.StaticChunkOffsetBox}); * so Media Data Box headers and free space may easily be skipped, and files without any box structure may * also be referenced and used. */ public final class MediaDataBox implements Box { private static Logger LOG = Logger.getLogger(MediaDataBox.class.getName()); public static final String TYPE = "mdat"; public static final int BUFFER_SIZE = 10 * 1024 * 1024; ContainerBox parent; ByteBuffer header; // These fields are for the special case of a FileChannel as input. private FileChannel fileChannel; private long startPosition; private long contentSize; private Map> cache = new HashMap>(); /** * If the whole content is just in one mapped buffer keep a strong reference to it so it is * not evicted from the cache. */ private ByteBuffer content; public ContainerBox getParent() { return parent; } public void setParent(ContainerBox parent) { this.parent = parent; } public String getType() { return TYPE; } private static void transfer(FileChannel from, long position, long count, WritableByteChannel to) throws IOException { long maxCount = (64 * 1024 * 1024) - (32 * 1024); // Transfer data in chunks a bit less than 64MB // People state that this is a kind of magic number on Windows. // I don't care. The size seems reasonable. long offset = 0; while (offset < count) { offset += from.transferTo(position + offset, Math.min(maxCount, count - offset), to); } } public void getBox(WritableByteChannel writableByteChannel) throws IOException { if (fileChannel != null) { assert checkStillOk(); transfer(fileChannel, startPosition - header.limit(), contentSize + header.limit(), writableByteChannel); } else { header.rewind(); writableByteChannel.write(header); writableByteChannel.write(content); } } /** * If someone use the same file as source and sink it could the case that * inserting a few bytes before the mdat results in overwrting data we still * need to write this mdat here. This method just makes sure that we haven't already * overwritten the mdat contents. * * @return true if ok */ private boolean checkStillOk() { try { fileChannel.position(startPosition - header.limit()); ByteBuffer h2 = ByteBuffer.allocate(header.limit()); fileChannel.read(h2); header.rewind(); h2.rewind(); assert h2.equals(header) : "It seems that the content I want to read has already been overwritten."; return true; } catch (IOException e) { e.printStackTrace(); return false; } } public long getSize() { long size = header.limit(); size += contentSize; return size; } public void parse(ReadableByteChannel readableByteChannel, ByteBuffer header, long contentSize, BoxParser boxParser) throws IOException { this.header = header; this.contentSize = contentSize; if (readableByteChannel instanceof FileChannel && (contentSize > AbstractBox.MEM_MAP_THRESHOLD)) { this.fileChannel = ((FileChannel) readableByteChannel); this.startPosition = ((FileChannel) readableByteChannel).position(); ((FileChannel) readableByteChannel).position(((FileChannel) readableByteChannel).position() + contentSize); } else { content = ChannelHelper.readFully(readableByteChannel, l2i(contentSize)); cache.put(0l, new SoftReference(content)); } } public synchronized ByteBuffer getContent(long offset, int length) { for (Long chacheEntryOffset : cache.keySet()) { if (chacheEntryOffset <= offset && offset <= chacheEntryOffset + BUFFER_SIZE) { ByteBuffer cacheEntry = cache.get(chacheEntryOffset).get(); if ((cacheEntry != null) && ((chacheEntryOffset + cacheEntry.limit()) >= (offset + length))) { // CACHE HIT cacheEntry.position((int) (offset - chacheEntryOffset)); ByteBuffer cachedSample = cacheEntry.slice(); cachedSample.limit(length); return cachedSample; } } } // CACHE MISS ByteBuffer cacheEntry; try { // Just mapping 10MB at a time. Seems reasonable. cacheEntry = fileChannel.map(FileChannel.MapMode.READ_ONLY, startPosition + offset, Math.min(BUFFER_SIZE, contentSize - offset)); } catch (IOException e1) { LOG.fine("Even mapping just 10MB of the source file into the memory failed. " + e1); throw new RuntimeException( "Delayed reading of mdat content failed. Make sure not to close " + "the FileChannel that has been used to create the IsoFile!", e1); } cache.put(offset, new SoftReference(cacheEntry)); cacheEntry.position(0); ByteBuffer cachedSample = cacheEntry.slice(); cachedSample.limit(length); return cachedSample; } public ByteBuffer getHeader() { return header; } }