package com.coremedia.iso.boxes.mdat;
import com.coremedia.iso.IsoFile;
import com.coremedia.iso.boxes.Box;
import com.coremedia.iso.boxes.ChunkOffsetBox;
import com.coremedia.iso.boxes.SampleSizeBox;
import com.coremedia.iso.boxes.SampleToChunkBox;
import com.coremedia.iso.boxes.TrackBox;
import com.coremedia.iso.boxes.fragment.MovieExtendsBox;
import com.coremedia.iso.boxes.fragment.MovieFragmentBox;
import com.coremedia.iso.boxes.fragment.TrackExtendsBox;
import com.coremedia.iso.boxes.fragment.TrackFragmentBox;
import com.coremedia.iso.boxes.fragment.TrackRunBox;
import java.nio.ByteBuffer;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.googlecode.mp4parser.util.CastUtils.l2i;
/**
* Creates a list of <code>ByteBuffer</code>s that represent the samples of a given track.
*/
public class SampleList extends AbstractList<ByteBuffer> {
Map<Long, Long> offsets2Sizes;
List<Long> offsetKeys = null;
IsoFile isoFile;
HashMap<MediaDataBox, Long> mdatStartCache = new HashMap<MediaDataBox, Long>();
HashMap<MediaDataBox, Long> mdatEndCache = new HashMap<MediaDataBox, Long>();
ArrayList<MediaDataBox> mdats = new ArrayList<MediaDataBox>(1);
/**
* Gets a sorted random access optimized list of all sample offsets.
* Basically it is a map from sample number to sample offset.
*
* @return the sorted list of sample offsets
*/
public List<Long> getOffsetKeys() {
if (offsetKeys == null) {
List<Long> offsetKeys = new ArrayList<Long>(offsets2Sizes.size());
for (Long aLong : offsets2Sizes.keySet()) {
offsetKeys.add(aLong);
}
Collections.sort(offsetKeys);
this.offsetKeys = offsetKeys;
}
return offsetKeys;
}
public SampleList(TrackBox trackBox) {
this.isoFile = trackBox.getIsoFile(); // where are we?
offsets2Sizes = new HashMap<Long, Long>();
// find all mdats first to be able to use them later with explicitly looking them up
long currentOffset = 0;
for (Box b : isoFile.getBoxes()) {
long currentSize = b.getSize();
if ("mdat".equals(b.getType())) {
if (b instanceof MediaDataBox) {
long contentOffset = currentOffset + ((MediaDataBox) b).getHeader().limit();
mdatStartCache.put((MediaDataBox) b, contentOffset);
mdatEndCache.put((MediaDataBox) b, contentOffset + currentSize);
mdats.add((MediaDataBox) b);
} else {
throw new RuntimeException("Sample need to be in mdats and mdats need to be instanceof MediaDataBox");
}
}
currentOffset += currentSize;
}
// first we get all sample from the 'normal' MP4 part.
// if there are none - no problem.
SampleSizeBox sampleSizeBox = trackBox.getSampleTableBox().getSampleSizeBox();
ChunkOffsetBox chunkOffsetBox = trackBox.getSampleTableBox().getChunkOffsetBox();
SampleToChunkBox sampleToChunkBox = trackBox.getSampleTableBox().getSampleToChunkBox();
if (sampleToChunkBox != null && sampleToChunkBox.getEntries().size() > 0 && chunkOffsetBox != null &&
chunkOffsetBox.getChunkOffsets().length > 0 && sampleSizeBox != null && sampleSizeBox.getSampleCount() > 0) {
long[] numberOfSamplesInChunk = sampleToChunkBox.blowup(chunkOffsetBox.getChunkOffsets().length);
if (sampleSizeBox.getSampleSize() > 0) {
// Every sample has the same size!
// no need to store each size separately
// this happens when people use raw audio formats in MP4 (are you stupid guys???)
offsets2Sizes = new DummyMap<Long, Long>(sampleSizeBox.getSampleSize());
long sampleSize = sampleSizeBox.getSampleSize();
for (int i = 0; i < numberOfSamplesInChunk.length; i++) {
long thisChunksNumberOfSamples = numberOfSamplesInChunk[i];
long sampleOffset = chunkOffsetBox.getChunkOffsets()[i];
for (int j = 0; j < thisChunksNumberOfSamples; j++) {
offsets2Sizes.put(sampleOffset, sampleSize);
sampleOffset += sampleSize;
}
}
} else {
// the normal case where all samples have different sizes
int sampleIndex = 0;
long sampleSizes[] = sampleSizeBox.getSampleSizes();
for (int i = 0; i < numberOfSamplesInChunk.length; i++) {
long thisChunksNumberOfSamples = numberOfSamplesInChunk[i];
long sampleOffset = chunkOffsetBox.getChunkOffsets()[i];
for (int j = 0; j < thisChunksNumberOfSamples; j++) {
long sampleSize = sampleSizes[sampleIndex];
offsets2Sizes.put(sampleOffset, sampleSize);
sampleOffset += sampleSize;
sampleIndex++;
}
}
}
}
// Next we add all samples from the fragments
// in most cases - I've never seen it different it's either normal or fragmented.
List<MovieExtendsBox> movieExtendsBoxes = trackBox.getParent().getBoxes(MovieExtendsBox.class);
if (movieExtendsBoxes.size() > 0) {
List<TrackExtendsBox> trackExtendsBoxes = movieExtendsBoxes.get(0).getBoxes(TrackExtendsBox.class);
for (TrackExtendsBox trackExtendsBox : trackExtendsBoxes) {
if (trackExtendsBox.getTrackId() == trackBox.getTrackHeaderBox().getTrackId()) {
for (MovieFragmentBox movieFragmentBox : trackBox.getIsoFile().getBoxes(MovieFragmentBox.class)) {
offsets2Sizes.putAll(getOffsets(movieFragmentBox, trackBox.getTrackHeaderBox().getTrackId()));
}
}
}
}
// We have now a map from all sample offsets to their sizes
}
@Override
public int size() {
return offsets2Sizes.size();
}
@Override
public ByteBuffer get(int index) {
// it is a two stage lookup: from index to offset to size
Long offset = getOffsetKeys().get(index);
int sampleSize = l2i(offsets2Sizes.get(offset));
for (MediaDataBox mediaDataBox : mdats) {
long start = mdatStartCache.get(mediaDataBox);
long end = mdatEndCache.get(mediaDataBox);
if ((start <= offset) && (offset + sampleSize <= end)) {
ByteBuffer bb = mediaDataBox.getContent();
bb.position(l2i(offset - start));
ByteBuffer sample = bb.slice();
sample.limit(sampleSize);
return sample;
}
}
throw new RuntimeException("The sample with offset " + offset + " and size " + sampleSize + " is NOT located within an mdat");
}
Map<Long, Long> getOffsets(MovieFragmentBox moof, long trackId) {
Map<Long, Long> offsets2Sizes = new HashMap<Long, Long>();
List<TrackFragmentBox> traf = moof.getBoxes(TrackFragmentBox.class);
for (TrackFragmentBox trackFragmentBox : traf) {
if (trackFragmentBox.getTrackFragmentHeaderBox().getTrackId() == trackId) {
long baseDataOffset;
if (trackFragmentBox.getTrackFragmentHeaderBox().hasBaseDataOffset()) {
baseDataOffset = trackFragmentBox.getTrackFragmentHeaderBox().getBaseDataOffset();
} else {
baseDataOffset = moof.getOffset();
}
for (TrackRunBox trun : trackFragmentBox.getBoxes(TrackRunBox.class)) {
long sampleBaseOffset = baseDataOffset + trun.getDataOffset();
long[] sampleOffsets = trun.getSampleOffsets();
long[] sampleSizes = trun.getSampleSizes();
for (int i = 0; i < sampleSizes.length; i++) {
offsets2Sizes.put(sampleOffsets[i] + sampleBaseOffset, sampleSizes[i]);
}
}
}
}
return offsets2Sizes;
}
}