package org.jcodec.containers.mp4.muxer;
import static org.jcodec.containers.mp4.TrackType.SOUND;
import static org.jcodec.containers.mp4.TrackType.VIDEO;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.jcodec.common.AudioFormat;
import org.jcodec.common.NIOUtils;
import org.jcodec.common.SeekableByteChannel;
import org.jcodec.common.model.Size;
import org.jcodec.containers.mp4.Brand;
import org.jcodec.containers.mp4.MP4Util;
import org.jcodec.containers.mp4.TrackType;
import org.jcodec.containers.mp4.boxes.AudioSampleEntry;
import org.jcodec.containers.mp4.boxes.Box;
import org.jcodec.containers.mp4.boxes.EndianBox;
import org.jcodec.containers.mp4.boxes.EndianBox.Endian;
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.LeafBox;
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.SampleEntry;
import org.jcodec.containers.mp4.boxes.VideoSampleEntry;
/**
* 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 {
private List<AbstractMP4MuxerTrack> tracks = new ArrayList<AbstractMP4MuxerTrack>();
protected long mdatOffset;
private int nextTrackId = 1;
protected SeekableByteChannel out;
public MP4Muxer(SeekableByteChannel output) throws IOException {
this(output, Brand.MP4);
}
public MP4Muxer(SeekableByteChannel output, Brand brand) throws IOException {
this(output, brand.getFileTypeBox());
}
public MP4Muxer(SeekableByteChannel output, FileTypeBox ftyp) throws IOException {
this.out = output;
ByteBuffer buf = ByteBuffer.allocate(1024);
ftyp.write(buf);
new Header("wide", 8).write(buf);
new Header("mdat", 1).write(buf);
mdatOffset = buf.position();
buf.putLong(0);
buf.flip();
output.write(buf);
}
public FramesMP4MuxerTrack addVideoTrackWithTimecode(String fourcc, Size size, String encoderName, int timescale) {
TimecodeMP4MuxerTrack timecode = addTimecodeTrack(timescale);
FramesMP4MuxerTrack track = addTrack(VIDEO, timescale);
track.addSampleEntry(videoSampleEntry(fourcc, size, encoderName));
track.setTimecode(timecode);
return track;
}
public FramesMP4MuxerTrack addVideoTrack(String fourcc, Size size, String encoderName, int timescale) {
FramesMP4MuxerTrack track = addTrack(VIDEO, timescale);
track.addSampleEntry(videoSampleEntry(fourcc, size, encoderName));
return track;
}
public static VideoSampleEntry videoSampleEntry(String fourcc, Size size, String encoderName) {
return new VideoSampleEntry(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, Endian endian) {
AudioSampleEntry ase = new AudioSampleEntry(new Header(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(new FormatBox(fourcc));
wave.add(new EndianBox(endian));
wave.add(terminatorAtom());
// ase.add(new ChannelBox(atom));
return ase;
}
public static LeafBox terminatorAtom() {
return new LeafBox(new Header(new String(new byte[4])), ByteBuffer.allocate(0));
}
public TimecodeMP4MuxerTrack addTimecodeTrack(int timescale) {
TimecodeMP4MuxerTrack track = new TimecodeMP4MuxerTrack(out, nextTrackId++, timescale);
tracks.add(track);
return track;
}
public FramesMP4MuxerTrack addTrack(TrackType type, int timescale) {
FramesMP4MuxerTrack track = new FramesMP4MuxerTrack(out, nextTrackId++, type, timescale);
tracks.add(track);
return track;
}
public PCMMP4MuxerTrack addPCMTrack(int timescale, int sampleDuration, int sampleSize,
SampleEntry se) {
PCMMP4MuxerTrack track = new PCMMP4MuxerTrack(out, nextTrackId++, TrackType.SOUND, timescale, sampleDuration, sampleSize, se);
tracks.add(track);
return track;
}
public List<AbstractMP4MuxerTrack> getTracks() {
return tracks;
}
public void writeHeader() throws IOException {
MovieBox movie = finalizeHeader();
storeHeader(movie);
}
public void storeHeader(MovieBox movie) throws IOException {
long mdatSize = out.position() - mdatOffset + 8;
MP4Util.writeMovie(out, movie);
out.position(mdatOffset);
NIOUtils.writeLong(out, mdatSize);
}
public MovieBox finalizeHeader() throws IOException {
MovieBox movie = new MovieBox();
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 new MovieHeaderBox(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 static String lookupFourcc(AudioFormat format) {
if (format.getSampleSizeInBits() == 16 && !format.isBigEndian())
return "sowt";
else if (format.getSampleSizeInBits() == 24)
return "in24";
else
throw new IllegalArgumentException("Audio format " + format + " is not supported.");
}
public PCMMP4MuxerTrack addPCMAudioTrack(AudioFormat format) {
return addPCMTrack((int) format.getSampleRate(), 1, (format.getSampleSizeInBits() >> 3)
* format.getChannels(), audioSampleEntry(format));
}
public static AudioSampleEntry audioSampleEntry(AudioFormat format) {
return MP4Muxer.audioSampleEntry(lookupFourcc(format), 1,
format.getSampleSizeInBits() >> 3, format.getChannels(), (int) format.getSampleRate(),
format.isBigEndian() ? Endian.BIG_ENDIAN : Endian.LITTLE_ENDIAN);
}
public FramesMP4MuxerTrack addCompressedAudioTrack(String fourcc, int timescale, int channels, int sampleRate,
int samplesPerPkt, Box... extra) {
FramesMP4MuxerTrack track = addTrack(SOUND, timescale);
AudioSampleEntry ase = new AudioSampleEntry(new Header(fourcc, 0), (short) 1, (short) channels, (short) 16,
sampleRate, (short) 0, 0, 65534, 0, samplesPerPkt, 0, 0, 2, (short) 1);
NodeBox wave = new NodeBox(new Header("wave"));
ase.add(wave);
wave.add(new FormatBox(fourcc));
for (Box box : extra)
wave.add(box);
wave.add(terminatorAtom());
track.addSampleEntry(ase);
return track;
}
}