package com.coremedia.iso.boxes.mdat; import com.coremedia.iso.IsoFile; import com.coremedia.iso.boxes.*; import com.coremedia.iso.boxes.fragment.*; import java.nio.ByteBuffer; import java.util.*; import static com.googlecode.mp4parser.util.CastUtils.l2i; /** * Creates a list of ByteBuffers that represent the samples of a given track. */ public class SampleList extends AbstractList { long[] offsets; long[] sizes; IsoFile isoFile; HashMap mdatStartCache = new HashMap(); HashMap mdatEndCache = new HashMap(); MediaDataBox[] mdats; /** * 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 long[] getOffsetKeys() { return offsets; } public SampleList(TrackBox trackBox) { initIsoFile(trackBox.getIsoFile()); // where are we? // 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(); final long[] chunkOffsets = chunkOffsetBox != null ? chunkOffsetBox.getChunkOffsets() : new long[0]; if (sampleToChunkBox != null && sampleToChunkBox.getEntries().size() > 0 && chunkOffsets.length > 0 && sampleSizeBox != null && sampleSizeBox.getSampleCount() > 0) { long[] numberOfSamplesInChunk = sampleToChunkBox.blowup(chunkOffsets.length); int sampleIndex = 0; if (sampleSizeBox.getSampleSize() > 0) { sizes = new long[l2i(sampleSizeBox.getSampleCount())]; Arrays.fill(sizes, sampleSizeBox.getSampleSize()); } else { sizes = sampleSizeBox.getSampleSizes(); } offsets = new long[sizes.length]; for (int i = 0; i < numberOfSamplesInChunk.length; i++) { long thisChunksNumberOfSamples = numberOfSamplesInChunk[i]; long sampleOffset = chunkOffsets[i]; for (int j = 0; j < thisChunksNumberOfSamples; j++) { long sampleSize = sizes[sampleIndex]; offsets[sampleIndex] = sampleOffset; 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 movieExtendsBoxes = trackBox.getParent().getBoxes(MovieExtendsBox.class); if (movieExtendsBoxes.size() > 0) { Map offsets2Sizes = new HashMap(); List 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(), trackExtendsBox)); } } } if (sizes == null || offsets == null) { sizes = new long[0]; offsets = new long[0]; } splitToArrays(offsets2Sizes); } // We have now a map from all sample offsets to their sizes } private void splitToArrays(Map offsets2Sizes) { List keys = new ArrayList(offsets2Sizes.keySet()); Collections.sort(keys); long[] nuSizes = new long[sizes.length + keys.size()]; System.arraycopy(sizes, 0, nuSizes, 0, sizes.length); long[] nuOffsets = new long[offsets.length + keys.size()]; System.arraycopy(offsets, 0, nuOffsets, 0, offsets.length); for (int i = 0; i < keys.size(); i++) { nuOffsets[i + offsets.length] = keys.get(i); nuSizes[i + sizes.length] = offsets2Sizes.get(keys.get(i)); } sizes = nuSizes; offsets = nuOffsets; } public SampleList(TrackFragmentBox traf) { sizes = new long[0]; offsets = new long[0]; Map offsets2Sizes = new HashMap(); initIsoFile(traf.getIsoFile()); final List movieFragmentBoxList = isoFile.getBoxes(MovieFragmentBox.class); final long trackId = traf.getTrackFragmentHeaderBox().getTrackId(); for (MovieFragmentBox moof : movieFragmentBoxList) { final List trackFragmentHeaderBoxes = moof.getTrackFragmentHeaderBoxes(); for (TrackFragmentHeaderBox tfhd : trackFragmentHeaderBoxes) { if (tfhd.getTrackId() == trackId) { offsets2Sizes.putAll(getOffsets(moof, trackId, null)); } } } splitToArrays(offsets2Sizes); } private void initIsoFile(IsoFile isoFile) { this.isoFile = isoFile; // find all mdats first to be able to use them later with explicitly looking them up long currentOffset = 0; LinkedList mdats = new LinkedList(); for (Box b : this.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; } this.mdats = mdats.toArray(new MediaDataBox[mdats.size()]); } @Override public int size() { return sizes.length; } @Override public ByteBuffer get(int index) { // it is a two stage lookup: from index to offset to size long offset = offsets[index]; int sampleSize = l2i(sizes[index]); for (MediaDataBox mediaDataBox : mdats) { long start = mdatStartCache.get(mediaDataBox); long end = mdatEndCache.get(mediaDataBox); if ((start <= offset) && (offset + sampleSize <= end)) { return mediaDataBox.getContent(offset - start, sampleSize); } } throw new RuntimeException("The sample with offset " + offset + " and size " + sampleSize + " is NOT located within an mdat"); } Map getOffsets(MovieFragmentBox moof, long trackId, TrackExtendsBox trex) { Map offsets2Sizes = new HashMap(); List 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(); final TrackFragmentHeaderBox tfhd = ((TrackFragmentBox) trun.getParent()).getTrackFragmentHeaderBox(); long offset = 0; for (TrackRunBox.Entry entry : trun.getEntries()) { final long sampleSize; if (trun.isSampleSizePresent()) { sampleSize = entry.getSampleSize(); offsets2Sizes.put(offset + sampleBaseOffset, sampleSize); offset += sampleSize; } else { if (tfhd.hasDefaultSampleSize()) { sampleSize = tfhd.getDefaultSampleSize(); offsets2Sizes.put(offset + sampleBaseOffset, sampleSize); offset += sampleSize; } else { if (trex == null) { throw new RuntimeException("File doesn't contain trex box but track fragments aren't fully self contained. Cannot determine sample size."); } sampleSize = trex.getDefaultSampleSize(); offsets2Sizes.put(offset + sampleBaseOffset, sampleSize); offset += sampleSize; } } } } } } return offsets2Sizes; } }