package org.jcodec.containers.mp4.muxer; import static org.jcodec.containers.mp4.MP4TrackType.SOUND; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.Date; import java.util.List; import org.jcodec.common.AudioCodecMeta; import org.jcodec.common.AudioFormat; import org.jcodec.common.Codec; import org.jcodec.common.Muxer; import org.jcodec.common.MuxerTrack; import org.jcodec.common.VideoCodecMeta; import org.jcodec.common.io.NIOUtils; import org.jcodec.common.io.SeekableByteChannel; import org.jcodec.common.model.Size; import org.jcodec.containers.mp4.Brand; import org.jcodec.containers.mp4.MP4TrackType; import org.jcodec.containers.mp4.MP4Util; import org.jcodec.containers.mp4.boxes.AudioSampleEntry; import org.jcodec.containers.mp4.boxes.Box; import org.jcodec.containers.mp4.boxes.Box.LeafBox; import org.jcodec.containers.mp4.boxes.EndianBox; import org.jcodec.containers.mp4.boxes.FileTypeBox; import org.jcodec.containers.mp4.boxes.FormatBox; import org.jcodec.containers.mp4.boxes.Header; import org.jcodec.containers.mp4.boxes.MovieBox; import org.jcodec.containers.mp4.boxes.MovieHeaderBox; import org.jcodec.containers.mp4.boxes.NodeBox; import org.jcodec.containers.mp4.boxes.VideoSampleEntry; import org.jcodec.platform.Platform; /** * This class is part of JCodec ( www.jcodec.org ) This software is distributed * under FreeBSD License * * Creates MP4 file out of a set of samples * * @author The JCodec project * */ public class MP4Muxer implements Muxer { private List<AbstractMP4MuxerTrack> tracks; protected long mdatOffset; private int nextTrackId = 1; protected SeekableByteChannel out; public static MP4Muxer createMP4MuxerToChannel(SeekableByteChannel output) throws IOException { return new MP4Muxer(output, Brand.MP4.getFileTypeBox()); } public static MP4Muxer createMP4Muxer(SeekableByteChannel output, Brand brand) throws IOException { return new MP4Muxer(output, brand.getFileTypeBox()); } public MP4Muxer(SeekableByteChannel output, FileTypeBox ftyp) throws IOException { this.tracks = new ArrayList<AbstractMP4MuxerTrack>(); this.out = output; ByteBuffer buf = ByteBuffer.allocate(1024); ftyp.write(buf); Header.createHeader("wide", 8).write(buf); Header.createHeader("mdat", 1).write(buf); mdatOffset = buf.position(); buf.putLong(0); buf.flip(); output.write(buf); } public static VideoSampleEntry videoSampleEntry(String fourcc, Size size, String encoderName) { return VideoSampleEntry.createVideoSampleEntry(new Header(fourcc), (short) 0, (short) 0, "jcod", 0, 768, (short) size.getWidth(), (short) size.getHeight(), 72, 72, (short) 1, encoderName != null ? encoderName : "jcodec", (short) 24, (short) 1, (short) -1); } public static AudioSampleEntry audioSampleEntry(String fourcc, int drefId, int sampleSize, int channels, int sampleRate, ByteOrder endian) { AudioSampleEntry ase = AudioSampleEntry.createAudioSampleEntry(Header.createHeader(fourcc, 0), (short) drefId, (short) channels, (short) 16, sampleRate, (short) 0, 0, 65535, 0, 1, sampleSize, channels * sampleSize, sampleSize, (short) 1); NodeBox wave = new NodeBox(new Header("wave")); ase.add(wave); wave.add(FormatBox.createFormatBox(fourcc)); wave.add(EndianBox.createEndianBox(endian)); wave.add(terminatorAtom()); // ase.add(new ChannelBox(atom)); return ase; } public static LeafBox terminatorAtom() { return LeafBox.createLeafBox(new Header(Platform.stringFromBytes(new byte[4])), ByteBuffer.allocate(0)); } public TimecodeMP4MuxerTrack addTimecodeTrack() { TimecodeMP4MuxerTrack track = new TimecodeMP4MuxerTrack(out, nextTrackId++); tracks.add(track); return track; } private FramesMP4MuxerTrack addTrack(MP4TrackType type, Codec codec) { FramesMP4MuxerTrack track = new FramesMP4MuxerTrack(out, nextTrackId++, type, codec); tracks.add(track); return track; } public List<AbstractMP4MuxerTrack> getTracks() { return tracks; } @Override public void finish() throws IOException { if(tracks.size() == 0) throw new RuntimeException("Can not save header with 0 tracks."); MovieBox movie = finalizeHeader(); storeHeader(movie); } public void storeHeader(MovieBox movie) throws IOException { long mdatSize = out.position() - mdatOffset + 8; MP4Util.writeMovie(out, movie); out.setPosition(mdatOffset); NIOUtils.writeLong(out, mdatSize); } public MovieBox finalizeHeader() throws IOException { MovieBox movie = MovieBox.createMovieBox(); MovieHeaderBox mvhd = movieHeader(movie); movie.addFirst(mvhd); for (AbstractMP4MuxerTrack track : tracks) { Box trak = track.finish(mvhd); if (trak != null) movie.add(trak); } return movie; } public AbstractMP4MuxerTrack getVideoTrack() { for (AbstractMP4MuxerTrack frameMuxer : tracks) { if (frameMuxer.isVideo()) { return frameMuxer; } } return null; } public AbstractMP4MuxerTrack getTimecodeTrack() { for (AbstractMP4MuxerTrack frameMuxer : tracks) { if (frameMuxer.isTimecode()) { return frameMuxer; } } return null; } public List<AbstractMP4MuxerTrack> getAudioTracks() { ArrayList<AbstractMP4MuxerTrack> result = new ArrayList<AbstractMP4MuxerTrack>(); for (AbstractMP4MuxerTrack frameMuxer : tracks) { if (frameMuxer.isAudio()) { result.add(frameMuxer); } } return result; } private MovieHeaderBox movieHeader(NodeBox movie) { int timescale = tracks.get(0).getTimescale(); long duration = tracks.get(0).getTrackTotalDuration(); AbstractMP4MuxerTrack videoTrack = getVideoTrack(); if (videoTrack != null) { timescale = videoTrack.getTimescale(); duration = videoTrack.getTrackTotalDuration(); } return MovieHeaderBox.createMovieHeaderBox(timescale, duration, 1.0f, 1.0f, new Date().getTime(), new Date().getTime(), new int[] { 0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x40000000 }, nextTrackId); } public PCMMP4MuxerTrack addPCMAudioTrack(AudioFormat format) { PCMMP4MuxerTrack track = new PCMMP4MuxerTrack(out, nextTrackId++, format); tracks.add(track); return track; } public FramesMP4MuxerTrack addCompressedAudioTrack(Codec codec, AudioFormat format) { FramesMP4MuxerTrack track = addTrack(SOUND, codec); track.addAudioSampleEntry(format); return track; } @Override public MuxerTrack addVideoTrack(Codec codec, VideoCodecMeta meta) { FramesMP4MuxerTrack track = addTrack(MP4TrackType.VIDEO, codec); if (meta == null && codec != Codec.H264) { throw new RuntimeException("VideoCodecMeta is required upfront for all codecs but H.264"); } track.addVideoSampleEntry(meta); return track; } @Override public MuxerTrack addAudioTrack(Codec codec, AudioCodecMeta meta) { AudioFormat format = meta.getFormat(); if (codec == Codec.PCM) { return addPCMAudioTrack(format); } else { return addCompressedAudioTrack(codec, format); } } }