package org.jcodec.containers.mkv.muxer;
import static org.jcodec.containers.mkv.MKVType.CodecID;
import static org.jcodec.containers.mkv.MKVType.Cues;
import static org.jcodec.containers.mkv.MKVType.DateUTC;
import static org.jcodec.containers.mkv.MKVType.DocType;
import static org.jcodec.containers.mkv.MKVType.DocTypeReadVersion;
import static org.jcodec.containers.mkv.MKVType.DocTypeVersion;
import static org.jcodec.containers.mkv.MKVType.EBML;
import static org.jcodec.containers.mkv.MKVType.EBMLMaxIDLength;
import static org.jcodec.containers.mkv.MKVType.EBMLMaxSizeLength;
import static org.jcodec.containers.mkv.MKVType.EBMLReadVersion;
import static org.jcodec.containers.mkv.MKVType.EBMLVersion;
import static org.jcodec.containers.mkv.MKVType.Info;
import static org.jcodec.containers.mkv.MKVType.MuxingApp;
import static org.jcodec.containers.mkv.MKVType.Name;
import static org.jcodec.containers.mkv.MKVType.PixelHeight;
import static org.jcodec.containers.mkv.MKVType.PixelWidth;
import static org.jcodec.containers.mkv.MKVType.Segment;
import static org.jcodec.containers.mkv.MKVType.TimecodeScale;
import static org.jcodec.containers.mkv.MKVType.TrackEntry;
import static org.jcodec.containers.mkv.MKVType.TrackNumber;
import static org.jcodec.containers.mkv.MKVType.TrackType;
import static org.jcodec.containers.mkv.MKVType.TrackUID;
import static org.jcodec.containers.mkv.MKVType.Tracks;
import static org.jcodec.containers.mkv.MKVType.Video;
import static org.jcodec.containers.mkv.MKVType.WritingApp;
import static org.jcodec.containers.mkv.MKVType.createByType;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.jcodec.common.AudioCodecMeta;
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.SeekableByteChannel;
import org.jcodec.containers.mkv.CuesFactory;
import org.jcodec.containers.mkv.MKVType;
import org.jcodec.containers.mkv.SeekHeadFactory;
import org.jcodec.containers.mkv.boxes.EbmlBase;
import org.jcodec.containers.mkv.boxes.EbmlBin;
import org.jcodec.containers.mkv.boxes.EbmlDate;
import org.jcodec.containers.mkv.boxes.EbmlFloat;
import org.jcodec.containers.mkv.boxes.EbmlMaster;
import org.jcodec.containers.mkv.boxes.EbmlString;
import org.jcodec.containers.mkv.boxes.EbmlUint;
import org.jcodec.containers.mkv.boxes.MkvBlock;
import org.jcodec.containers.mkv.muxer.MKVMuxerTrack.MKVMuxerTrackType;
/**
* This class is part of JCodec ( www.jcodec.org ) This software is distributed
* under FreeBSD License
*
* @author The JCodec project
*
*/
public class MKVMuxer implements Muxer {
private List<MKVMuxerTrack> tracks;
private MKVMuxerTrack audioTrack;
private MKVMuxerTrack videoTrack;
private EbmlMaster mkvInfo;
private EbmlMaster mkvTracks;
private EbmlMaster mkvCues;
private EbmlMaster mkvSeekHead;
private List<EbmlMaster> clusterList;
private SeekableByteChannel sink;
private static Map<Codec, String> codec2mkv = new HashMap<Codec, String>();
static {
codec2mkv.put(Codec.H264, "V_MPEG4/ISO/AVC");
codec2mkv.put(Codec.VP8, "V_VP8");
codec2mkv.put(Codec.VP9, "V_VP9");
}
public MKVMuxer(SeekableByteChannel s) {
this.sink = s;
this.tracks = new ArrayList<MKVMuxerTrack>();
this.clusterList = new LinkedList<EbmlMaster>();
}
public MKVMuxerTrack createVideoTrack(VideoCodecMeta meta, String codecId) {
if (videoTrack == null) {
videoTrack = new MKVMuxerTrack();
tracks.add(videoTrack);
videoTrack.codecId = codecId;
videoTrack.videoMeta = meta;
videoTrack.trackNo = tracks.size();
}
return videoTrack;
}
public void finish() throws IOException {
List<EbmlMaster> mkvFile = new ArrayList<EbmlMaster>();
EbmlMaster ebmlHeader = defaultEbmlHeader();
mkvFile.add(ebmlHeader);
EbmlMaster segmentElem = (EbmlMaster) createByType(Segment);
mkvInfo = muxInfo();
mkvTracks = muxTracks();
mkvCues = (EbmlMaster) createByType(Cues);
mkvSeekHead = muxSeekHead();
muxCues();
segmentElem.add(mkvSeekHead);
segmentElem.add(mkvInfo);
segmentElem.add(mkvTracks);
segmentElem.add(mkvCues);
for (EbmlMaster aCluster : clusterList)
segmentElem.add(aCluster);
mkvFile.add(segmentElem);
for (EbmlMaster el : mkvFile)
el.mux(sink);
}
private EbmlMaster defaultEbmlHeader() {
EbmlMaster master = (EbmlMaster) createByType(EBML);
createLong(master, EBMLVersion, 1);
createLong(master, EBMLReadVersion, 1);
createLong(master, EBMLMaxIDLength, 4);
createLong(master, EBMLMaxSizeLength, 8);
createString(master, DocType, "webm");
createLong(master, DocTypeVersion, 2);
createLong(master, DocTypeReadVersion, 2);
return master;
}
private EbmlMaster muxInfo() {
EbmlMaster master = (EbmlMaster) createByType(Info);
int frameDurationInNanoseconds = MKVMuxerTrack.NANOSECONDS_IN_A_MILISECOND * 40;
createLong(master, TimecodeScale, frameDurationInNanoseconds);
createString(master, WritingApp, "JCodec");
createString(master, MuxingApp, "JCodec");
List<MKVMuxerTrack> tracks2 = tracks;
long max = 0;
for (MKVMuxerTrack track : tracks2) {
MkvBlock lastBlock = track.trackBlocks.get(track.trackBlocks.size() - 1);
if (lastBlock.absoluteTimecode > max)
max = lastBlock.absoluteTimecode;
}
createDouble(master, MKVType.Duration, (max + 1) * frameDurationInNanoseconds * 1.0);
createDate(master, DateUTC, new Date());
return master;
}
private EbmlMaster muxTracks() {
EbmlMaster master = (EbmlMaster) createByType(Tracks);
for (int i = 0; i < tracks.size(); i++) {
MKVMuxerTrack track = tracks.get(i);
EbmlMaster trackEntryElem = (EbmlMaster) createByType(TrackEntry);
createLong(trackEntryElem, TrackNumber, track.trackNo);
createLong(trackEntryElem, TrackUID, track.trackNo);
if (MKVMuxerTrackType.VIDEO.equals(track.type)) {
createLong(trackEntryElem, TrackType, (byte) 0x01);
createString(trackEntryElem, Name, "Track " + (i + 1) + " Video");
createString(trackEntryElem, CodecID, track.codecId);
// createChild(trackEntryElem, CodecPrivate, codecMeta.getCodecPrivate());
// VideoCodecMeta vcm = (VideoCodecMeta) codecMeta;
EbmlMaster trackVideoElem = (EbmlMaster) createByType(Video);
createLong(trackVideoElem, PixelWidth, track.videoMeta.getSize().getWidth());
createLong(trackVideoElem, PixelHeight, track.videoMeta.getSize().getHeight());
trackEntryElem.add(trackVideoElem);
} else {
createLong(trackEntryElem, TrackType, (byte) 0x02);
createString(trackEntryElem, Name, "Track " + (i + 1) + " Audio");
createString(trackEntryElem, CodecID, track.codecId);
// createChild(trackEntryElem, CodecPrivate, codecMeta.getCodecPrivate());
}
master.add(trackEntryElem);
}
return master;
}
private void muxCues() {
CuesFactory cf = new CuesFactory(mkvSeekHead.size() + mkvInfo.size() + mkvTracks.size(),
videoTrack.trackNo);
for (MkvBlock aBlock : videoTrack.trackBlocks) {
EbmlMaster mkvCluster = singleBlockedCluster(aBlock);
clusterList.add(mkvCluster);
cf.add(CuesFactory.CuePointMock.make(mkvCluster));
}
EbmlMaster indexedCues = cf.createCues();
for (EbmlBase aCuePoint : indexedCues.children)
mkvCues.add(aCuePoint);
}
private EbmlMaster singleBlockedCluster(MkvBlock aBlock) {
EbmlMaster mkvCluster = createByType(MKVType.Cluster);
createLong(mkvCluster, MKVType.Timecode, aBlock.absoluteTimecode - aBlock.timecode);
mkvCluster.add(aBlock);
return mkvCluster;
}
private EbmlMaster muxSeekHead() {
SeekHeadFactory shi = new SeekHeadFactory();
shi.add(mkvInfo);
shi.add(mkvTracks);
shi.add(mkvCues);
return shi.indexSeekHead();
}
public static void createLong(EbmlMaster parent, MKVType type, long value) {
EbmlUint se = (EbmlUint) createByType(type);
se.setUint(value);
parent.add(se);
}
public static void createString(EbmlMaster parent, MKVType type, String value) {
EbmlString se = (EbmlString) createByType(type);
se.setString(value);
parent.add(se);
}
public static void createDate(EbmlMaster parent, MKVType type, Date value) {
EbmlDate se = (EbmlDate) createByType(type);
se.setDate(value);
parent.add(se);
}
public static void createBuffer(EbmlMaster parent, MKVType type, ByteBuffer value) {
EbmlBin se = (EbmlBin) createByType(type);
se.setBuf(value);
parent.add(se);
}
public static void createDouble(EbmlMaster parent, MKVType type, double value) {
try {
EbmlFloat se = (EbmlFloat) createByType(type);
se.setDouble(value);
parent.add(se);
} catch (ClassCastException cce) {
throw new RuntimeException("Element of type " + type + " can't be cast to EbmlFloat", cce);
}
}
@Override
public MuxerTrack addVideoTrack(Codec codec, VideoCodecMeta meta) {
return createVideoTrack(meta, codec2mkv.get(codec));
}
@Override
public MuxerTrack addAudioTrack(Codec codec, AudioCodecMeta meta) {
audioTrack = new MKVMuxerTrack();
tracks.add(audioTrack);
audioTrack.codecId = codec2mkv.get(codec);
audioTrack.trackNo = tracks.size();
return audioTrack;
}
}