package org.jcodec.containers.mp4.muxer; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.jcodec.codecs.aac.ADTSParser; import org.jcodec.codecs.h264.H264Utils; import org.jcodec.codecs.h264.io.model.SeqParameterSet; import org.jcodec.codecs.mpeg4.mp4.EsdsBox; import org.jcodec.common.Assert; import org.jcodec.common.AudioFormat; import org.jcodec.common.Codec; import org.jcodec.common.IntArrayList; import org.jcodec.common.LongArrayList; import org.jcodec.common.VideoCodecMeta; import org.jcodec.common.io.NIOUtils; import org.jcodec.common.io.SeekableByteChannel; import org.jcodec.common.model.ColorSpace; import org.jcodec.common.model.Packet; import org.jcodec.common.model.Packet.FrameType; import org.jcodec.common.model.Rational; import org.jcodec.common.model.Size; import org.jcodec.common.model.Unit; import org.jcodec.containers.mp4.MP4TrackType; import org.jcodec.containers.mp4.boxes.AudioSampleEntry; 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.CompositionOffsetsBox.Entry; 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.PixelAspectExt; 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.SampleToChunkBox.SampleToChunkEntry; import org.jcodec.containers.mp4.boxes.SyncSamplesBox; import org.jcodec.containers.mp4.boxes.TimeToSampleBox; import org.jcodec.containers.mp4.boxes.TimeToSampleBox.TimeToSampleEntry; import org.jcodec.containers.mp4.boxes.TrackHeaderBox; import org.jcodec.containers.mp4.boxes.TrakBox; /** * 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 static Map<Codec, String> codec2fourcc = new HashMap<Codec, String>(); static { codec2fourcc.put(Codec.H264, "avc1"); codec2fourcc.put(Codec.AAC, "mp4a"); codec2fourcc.put(Codec.PRORES, "apch"); codec2fourcc.put(Codec.JPEG, "mjpg"); codec2fourcc.put(Codec.PNG, "png "); codec2fourcc.put(Codec.V210, "v210"); } private List<TimeToSampleEntry> sampleDurations; private long sameDurCount = 0; private long curDuration = -1; private LongArrayList chunkOffsets; private IntArrayList sampleSizes; private IntArrayList iframes; private List<Entry> compositionOffsets; 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; private Codec codec; // SPS/PPS lists when h.264 is stored, otherwise these lists are not used. private List<ByteBuffer> spsList = new ArrayList<ByteBuffer>(); private List<ByteBuffer> ppsList = new ArrayList<ByteBuffer>(); // ADTS header used to construct audio sample entry for AAC private ADTSParser.Header adtsHeader; public FramesMP4MuxerTrack(SeekableByteChannel out, int trackId, MP4TrackType type, Codec codec) { super(trackId, type); this.sampleDurations = new ArrayList<TimeToSampleEntry>(); this.chunkOffsets = LongArrayList.createLongArrayList(); this.sampleSizes = IntArrayList.createIntArrayList(); this.iframes = IntArrayList.createIntArrayList(); this.compositionOffsets = new ArrayList<Entry>(); this.out = out; this.codec = codec; setTgtChunkDuration(new Rational(1, 1), Unit.FRAME); } public void addFrame(Packet pkt) throws IOException { if (codec == Codec.H264) { ByteBuffer result = pkt.getData(); if (pkt.frameType == FrameType.UNKOWN) { pkt.setFrameType(H264Utils.isByteBufferIDRSlice(result) ? FrameType.KEY : FrameType.INTER); } H264Utils.wipePSinplace(result, spsList, ppsList); result = H264Utils.encodeMOVPacket(result); pkt = Packet.createPacketWithData(pkt, result); } else if (codec == Codec.AAC) { ByteBuffer result = pkt.getData(); adtsHeader = ADTSParser.read(result); System.out.println(String.format("crc_absent: %d, num_aac_frames: %d, size: %d, remaining: %d, %d, %d, %d", adtsHeader.getCrcAbsent(), adtsHeader.getNumAACFrames(), adtsHeader.getSize(), result.remaining(), adtsHeader.getObjectType(), adtsHeader.getSamplingIndex(), adtsHeader.getChanConfig())); pkt = Packet.createPacketWithData(pkt, result); } addFrameInternal(pkt, 1); processTimecode(pkt); } public void addFrameInternal(Packet pkt, int entryNo) throws IOException { if (finished) throw new IllegalStateException("The muxer track has finished muxing"); if (_timescale == NO_TIMESCALE_SET) { if (adtsHeader != null) { _timescale = adtsHeader.getSampleRate(); } else { _timescale = pkt.getTimescale(); } } if (_timescale != pkt.getTimescale()) { pkt.setPts((pkt.getPts() * _timescale) / pkt.getTimescale()); pkt.setDuration((pkt.getPts() * _timescale) / pkt.getDuration()); } if (adtsHeader != null) { pkt.setDuration(1024); } if(type == MP4TrackType.VIDEO) { 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); lastEntry = entryNo; } private void processTimecode(Packet 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"); if (getEntries().isEmpty()) { if (codec == Codec.H264) { SeqParameterSet sps = SeqParameterSet.read(spsList.get(0).duplicate()); Size size = H264Utils.getPicSize(sps); VideoCodecMeta meta = new VideoCodecMeta(size, ColorSpace.YUV420); addVideoSampleEntry(meta); } else { throw new RuntimeException("Sample entry missing not supported for anything other then H.264"); } } setCodecPrivateIfNeeded(); outChunk(lastEntry); if (sameDurCount > 0) { sampleDurations.add(new TimeToSampleEntry((int) sameDurCount, (int) curDuration)); } finished = true; TrakBox trak = TrakBox.createTrakBox(); Size dd = getDisplayDimensions(); TrackHeaderBox tkhd = TrackHeaderBox.createTrackHeaderBox(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 = MediaBox.createMediaBox(); trak.add(media); media.add(MediaHeaderBox.createMediaHeaderBox(_timescale, trackTotalDuration, 0, new Date().getTime(), new Date().getTime(), 0)); HandlerBox hdlr = HandlerBox.createHandlerBox("mhlr", type.getHandler(), "appl", 0, 0); media.add(hdlr); MediaInfoBox minf = MediaInfoBox.createMediaInfoBox(); media.add(minf); mediaHeader(minf, type); minf.add(HandlerBox.createHandlerBox("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(SampleDescriptionBox.createSampleDescriptionBox(sampleEntries.toArray(new SampleEntry[0]))); stbl.add(SampleToChunkBox.createSampleToChunkBox(samplesInChunks.toArray(new SampleToChunkEntry[0]))); stbl.add(SampleSizesBox.createSampleSizesBox2(sampleSizes.toArray())); stbl.add(TimeToSampleBox.createTimeToSampleBox(sampleDurations.toArray(new TimeToSampleEntry[] {}))); stbl.add(ChunkOffsets64Box.createChunkOffsets64Box(chunkOffsets.toArray())); if (!allIframes && iframes.size() > 0) stbl.add(SyncSamplesBox.createSyncSamplesBox(iframes.toArray())); return trak; } void addVideoSampleEntry(VideoCodecMeta meta) { SampleEntry se = MP4Muxer.videoSampleEntry(codec2fourcc.get(codec), meta.getSize(), "JCodec"); if (meta.getPixelAspectRatio() != null) se.add(PixelAspectExt.createPixelAspectExt(meta.getPixelAspectRatio())); addSampleEntry(se); } 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(CompositionOffsetsBox.createCompositionOffsetsBox(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 (int i = 0; i < sampleEntries.length; i++) { SampleEntry se = sampleEntries[i]; addSampleEntry(se); } } public TimecodeMP4MuxerTrack getTimecodeTrack() { return timecodeTrack; } public void setTimecode(TimecodeMP4MuxerTrack timecodeTrack) { this.timecodeTrack = timecodeTrack; } public void setCodecPrivateIfNeeded() { if (codec == Codec.H264) { getEntries().get(0).add(H264Utils.createAvcCFromPS(selectUnique(spsList), selectUnique(ppsList), 4)); } else if (codec == Codec.AAC) { getEntries().get(0).add(EsdsBox.fromADTS(adtsHeader)); } } private static class ByteArrayWrapper { private byte[] bytes; public ByteArrayWrapper(ByteBuffer bytes) { this.bytes = NIOUtils.toArray(bytes); } public ByteBuffer get() { return ByteBuffer.wrap(bytes); } @Override public boolean equals(Object obj) { if (!(obj instanceof ByteArrayWrapper)) return false; return Arrays.equals(bytes, ((ByteArrayWrapper) obj).bytes); } @Override public int hashCode() { return Arrays.hashCode(bytes); } } private List<ByteBuffer> selectUnique(List<ByteBuffer> bblist) { Set<ByteArrayWrapper> all = new HashSet<ByteArrayWrapper>(); for (ByteBuffer byteBuffer : bblist) { all.add(new ByteArrayWrapper(byteBuffer)); } List<ByteBuffer> result = new ArrayList<ByteBuffer>(); for (ByteArrayWrapper bs : all) { result.add(bs.get()); } return result; } public static AudioSampleEntry compressedAudioSampleEntry(String fourcc, int drefId, int sampleSize, int channels, int sampleRate, int samplesPerPacket, int bytesPerPacket, int bytesPerFrame) { AudioSampleEntry ase = AudioSampleEntry.createAudioSampleEntry(Header.createHeader(fourcc, 0), (short) drefId, (short) channels, (short) 16, sampleRate, (short) 0, 0, 65534, 0, samplesPerPacket, bytesPerPacket, bytesPerFrame, 16 / 8, (short) 0); return ase; } void addAudioSampleEntry(AudioFormat format) { AudioSampleEntry ase = compressedAudioSampleEntry(codec2fourcc.get(codec), (short) 1, (short) 16, format.getChannels(), format.getSampleRate(), 0, 0, 0); addSampleEntry(ase); } }