package org.jcodec.containers.mp4.muxer; import org.jcodec.api.NotSupportedException; import org.jcodec.common.Assert; import org.jcodec.common.AudioFormat; import org.jcodec.common.LongArrayList; import org.jcodec.common.io.SeekableByteChannel; import org.jcodec.common.model.Packet; 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.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.SampleToChunkBox.SampleToChunkEntry; 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; import java.io.IOException; import java.lang.IllegalStateException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Date; /** * This class is part of JCodec ( www.jcodec.org ) This software is distributed * under FreeBSD License * * @author The JCodec project * */ public class PCMMP4MuxerTrack extends AbstractMP4MuxerTrack { private int frameDuration; private int frameSize; private int framesInCurChunk; private LongArrayList chunkOffsets; private int totalFrames; private SeekableByteChannel out; public PCMMP4MuxerTrack(SeekableByteChannel out, int trackId, AudioFormat format) { super(trackId, MP4TrackType.SOUND); this.chunkOffsets = LongArrayList.createLongArrayList(); this.out = out; this.frameDuration = 1; this.frameSize = (format.getSampleSizeInBits() >> 3) * format.getChannels(); addSampleEntry(_audioSampleEntry(format)); this._timescale = format.getSampleRate(); setTgtChunkDuration(new Rational(1, 2), Unit.SEC); } public static AudioSampleEntry _audioSampleEntry(AudioFormat format) { return MP4Muxer.audioSampleEntry(lookupFourcc(format), 1, format.getSampleSizeInBits() >> 3, format.getChannels(), (int) format.getSampleRate(), format.isBigEndian() ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN); } public static String lookupFourcc(AudioFormat format) { if (format.getSampleSizeInBits() == 16 && !format.isBigEndian()) return "sowt"; else if (format.getSampleSizeInBits() == 24) return "in24"; else throw new NotSupportedException("Audio format " + format + " is not supported."); } @Override public void addFrame(Packet outPacket) throws IOException { addSamples(outPacket.getData().duplicate()); } public void addSamples(ByteBuffer buffer) throws IOException { curChunk.add(buffer); int frames = buffer.remaining() / frameSize; totalFrames += frames; framesInCurChunk += frames; chunkDuration += frames * frameDuration; outChunkIfNeeded(); } private void outChunkIfNeeded() throws IOException { Assert.assertTrue(tgtChunkDurationUnit == Unit.FRAME || tgtChunkDurationUnit == Unit.SEC); if (tgtChunkDurationUnit == Unit.FRAME && framesInCurChunk * tgtChunkDuration.getDen() == tgtChunkDuration.getNum()) { outChunk(); } else if (tgtChunkDurationUnit == Unit.SEC && chunkDuration > 0 && chunkDuration * tgtChunkDuration.getDen() >= tgtChunkDuration.getNum() * _timescale) { outChunk(); } } private void outChunk() throws IOException { if (framesInCurChunk == 0) return; chunkOffsets.add(out.position()); for (ByteBuffer b : curChunk) { out.write(b); } curChunk.clear(); if (samplesInLastChunk == -1 || framesInCurChunk != samplesInLastChunk) { samplesInChunks.add(new SampleToChunkEntry(chunkNo + 1, framesInCurChunk, 1)); } samplesInLastChunk = framesInCurChunk; chunkNo++; framesInCurChunk = 0; chunkDuration = 0; } protected Box finish(MovieHeaderBox mvhd) throws IOException { if (finished) throw new IllegalStateException("The muxer track has finished muxing"); outChunk(); finished = true; TrakBox trak = TrakBox.createTrakBox(); Size dd = getDisplayDimensions(); TrackHeaderBox tkhd = TrackHeaderBox.createTrackHeaderBox(trackId, ((long) mvhd.getTimescale() * totalFrames * frameDuration) / _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, totalFrames * frameDuration, 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); 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.createSampleSizesBox(frameSize, totalFrames)); stbl.add(TimeToSampleBox .createTimeToSampleBox(new TimeToSampleEntry[] { new TimeToSampleEntry(totalFrames, frameDuration) })); stbl.add(ChunkOffsets64Box.createChunkOffsets64Box(chunkOffsets.toArray())); return trak; } public long getTrackTotalDuration() { return totalFrames * frameDuration; } }