package org.jcodec.containers.mp4.muxer; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Date; import java.util.List; import org.jcodec.common.Assert; import org.jcodec.common.IntArrayList; import org.jcodec.common.LongArrayList; import org.jcodec.common.SeekableByteChannel; import org.jcodec.common.model.Rational; import org.jcodec.common.model.Size; import org.jcodec.common.model.Unit; import org.jcodec.containers.mp4.MP4Packet; import org.jcodec.containers.mp4.TrackType; import org.jcodec.containers.mp4.boxes.Box; import org.jcodec.containers.mp4.boxes.ChunkOffsets64Box; import org.jcodec.containers.mp4.boxes.CompositionOffsetsBox; import org.jcodec.containers.mp4.boxes.Edit; import org.jcodec.containers.mp4.boxes.HandlerBox; import org.jcodec.containers.mp4.boxes.Header; import org.jcodec.containers.mp4.boxes.MediaBox; import org.jcodec.containers.mp4.boxes.MediaHeaderBox; import org.jcodec.containers.mp4.boxes.MediaInfoBox; import org.jcodec.containers.mp4.boxes.MovieHeaderBox; import org.jcodec.containers.mp4.boxes.NodeBox; import org.jcodec.containers.mp4.boxes.SampleDescriptionBox; import org.jcodec.containers.mp4.boxes.SampleEntry; import org.jcodec.containers.mp4.boxes.SampleSizesBox; import org.jcodec.containers.mp4.boxes.SampleToChunkBox; import org.jcodec.containers.mp4.boxes.SyncSamplesBox; import org.jcodec.containers.mp4.boxes.TimeToSampleBox; import org.jcodec.containers.mp4.boxes.TrackHeaderBox; import org.jcodec.containers.mp4.boxes.TrakBox; import org.jcodec.containers.mp4.boxes.CompositionOffsetsBox.Entry; import org.jcodec.containers.mp4.boxes.SampleToChunkBox.SampleToChunkEntry; import org.jcodec.containers.mp4.boxes.TimeToSampleBox.TimeToSampleEntry; /** * This class is part of JCodec ( www.jcodec.org ) This software is distributed * under FreeBSD License * * @author The JCodec project * */ public class FramesMP4MuxerTrack extends AbstractMP4MuxerTrack { private List<TimeToSampleEntry> sampleDurations = new ArrayList<TimeToSampleEntry>(); private long sameDurCount = 0; private long curDuration = -1; private LongArrayList chunkOffsets = new LongArrayList(); private IntArrayList sampleSizes = new IntArrayList(); private IntArrayList iframes = new IntArrayList(); private List<Entry> compositionOffsets = new ArrayList<Entry>(); private int lastCompositionOffset = 0; private int lastCompositionSamples = 0; private long ptsEstimate = 0; private int lastEntry = -1; private long trackTotalDuration; private int curFrame; private boolean allIframes = true; private TimecodeMP4MuxerTrack timecodeTrack; private SeekableByteChannel out; public FramesMP4MuxerTrack(SeekableByteChannel out, int trackId, TrackType type, int timescale) { super(trackId, type, timescale); this.out = out; setTgtChunkDuration(new Rational(1, 1), Unit.FRAME); } public void addFrame(MP4Packet pkt) throws IOException { if (finished) throw new IllegalStateException("The muxer track has finished muxing"); int entryNo = pkt.getEntryNo() + 1; int compositionOffset = (int) (pkt.getPts() - ptsEstimate); if (compositionOffset != lastCompositionOffset) { if (lastCompositionSamples > 0) compositionOffsets.add(new Entry(lastCompositionSamples, lastCompositionOffset)); lastCompositionOffset = compositionOffset; lastCompositionSamples = 0; } lastCompositionSamples++; ptsEstimate += pkt.getDuration(); if (lastEntry != -1 && lastEntry != entryNo) { outChunk(lastEntry); samplesInLastChunk = -1; } curChunk.add(pkt.getData()); if (pkt.isKeyFrame()) iframes.add(curFrame + 1); else allIframes = false; curFrame++; chunkDuration += pkt.getDuration(); if (curDuration != -1 && pkt.getDuration() != curDuration) { sampleDurations.add(new TimeToSampleEntry((int) sameDurCount, (int) curDuration)); sameDurCount = 0; } curDuration = pkt.getDuration(); sameDurCount++; trackTotalDuration += pkt.getDuration(); outChunkIfNeeded(entryNo); processTimecode(pkt); lastEntry = entryNo; } private void processTimecode(MP4Packet pkt) throws IOException { if (timecodeTrack != null) timecodeTrack.addTimecode(pkt); } private void outChunkIfNeeded(int entryNo) throws IOException { Assert.assertTrue(tgtChunkDurationUnit == Unit.FRAME || tgtChunkDurationUnit == Unit.SEC); if (tgtChunkDurationUnit == Unit.FRAME && curChunk.size() * tgtChunkDuration.getDen() == tgtChunkDuration.getNum()) { outChunk(entryNo); } else if (tgtChunkDurationUnit == Unit.SEC && chunkDuration > 0 && chunkDuration * tgtChunkDuration.getDen() >= tgtChunkDuration.getNum() * timescale) { outChunk(entryNo); } } void outChunk(int entryNo) throws IOException { if (curChunk.size() == 0) return; chunkOffsets.add(out.position()); for (ByteBuffer bs : curChunk) { sampleSizes.add(bs.remaining()); out.write(bs); } if (samplesInLastChunk == -1 || samplesInLastChunk != curChunk.size()) { samplesInChunks.add(new SampleToChunkEntry(chunkNo + 1, curChunk.size(), entryNo)); } samplesInLastChunk = curChunk.size(); chunkNo++; chunkDuration = 0; curChunk.clear(); } protected Box finish(MovieHeaderBox mvhd) throws IOException { if (finished) throw new IllegalStateException("The muxer track has finished muxing"); outChunk(lastEntry); if (sameDurCount > 0) { sampleDurations.add(new TimeToSampleEntry((int) sameDurCount, (int) curDuration)); } finished = true; TrakBox trak = new TrakBox(); Size dd = getDisplayDimensions(); TrackHeaderBox tkhd = new TrackHeaderBox(trackId, ((long) mvhd.getTimescale() * trackTotalDuration) / timescale, dd.getWidth(), dd.getHeight(), new Date().getTime(), new Date().getTime(), 1.0f, (short) 0, 0, new int[] { 0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x40000000 }); tkhd.setFlags(0xf); trak.add(tkhd); tapt(trak); MediaBox media = new MediaBox(); trak.add(media); media.add(new MediaHeaderBox(timescale, trackTotalDuration, 0, new Date().getTime(), new Date().getTime(), 0)); HandlerBox hdlr = new HandlerBox("mhlr", type.getHandler(), "appl", 0, 0); media.add(hdlr); MediaInfoBox minf = new MediaInfoBox(); media.add(minf); mediaHeader(minf, type); minf.add(new HandlerBox("dhlr", "url ", "appl", 0, 0)); addDref(minf); NodeBox stbl = new NodeBox(new Header("stbl")); minf.add(stbl); putCompositionOffsets(stbl); putEdits(trak); putName(trak); stbl.add(new SampleDescriptionBox(sampleEntries.toArray(new SampleEntry[0]))); stbl.add(new SampleToChunkBox(samplesInChunks.toArray(new SampleToChunkEntry[0]))); stbl.add(new SampleSizesBox(sampleSizes.toArray())); stbl.add(new TimeToSampleBox(sampleDurations.toArray(new TimeToSampleEntry[] {}))); stbl.add(new ChunkOffsets64Box(chunkOffsets.toArray())); if (!allIframes && iframes.size() > 0) stbl.add(new SyncSamplesBox(iframes.toArray())); return trak; } private void putCompositionOffsets(NodeBox stbl) { if (compositionOffsets.size() > 0) { compositionOffsets.add(new Entry(lastCompositionSamples, lastCompositionOffset)); int min = minOffset(compositionOffsets); if (min > 0) { for (Entry entry : compositionOffsets) { entry.offset -= min; } } Entry first = compositionOffsets.get(0); if (first.getOffset() > 0) { if (edits == null) { edits = new ArrayList<Edit>(); edits.add(new Edit(trackTotalDuration, first.getOffset(), 1.0f)); } else { for (Edit edit : edits) { edit.setMediaTime(edit.getMediaTime() + first.getOffset()); } } } stbl.add(new CompositionOffsetsBox(compositionOffsets.toArray(new Entry[0]))); } } public static int minOffset(List<Entry> offs) { int min = Integer.MAX_VALUE; for (Entry entry : offs) { if (entry.getOffset() < min) min = entry.getOffset(); } return min; } public long getTrackTotalDuration() { return trackTotalDuration; } public void addSampleEntries(SampleEntry[] sampleEntries) { for (SampleEntry se : sampleEntries) { addSampleEntry(se); } } public TimecodeMP4MuxerTrack getTimecodeTrack() { return timecodeTrack; } public void setTimecode(TimecodeMP4MuxerTrack timecodeTrack) { this.timecodeTrack = timecodeTrack; } }