package org.jcodec.containers.mkv; import static org.jcodec.containers.mkv.CuesIndexer.CuePointMock.make; import static org.jcodec.containers.mkv.Type.Audio; import static org.jcodec.containers.mkv.Type.BitDepth; import static org.jcodec.containers.mkv.Type.Channels; import static org.jcodec.containers.mkv.Type.CodecID; import static org.jcodec.containers.mkv.Type.Cues; import static org.jcodec.containers.mkv.Type.Name; import static org.jcodec.containers.mkv.Type.OutputSamplingFrequency; import static org.jcodec.containers.mkv.Type.PixelHeight; import static org.jcodec.containers.mkv.Type.PixelWidth; import static org.jcodec.containers.mkv.Type.SamplingFrequency; import static org.jcodec.containers.mkv.Type.Segment; import static org.jcodec.containers.mkv.Type.TrackEntry; import static org.jcodec.containers.mkv.Type.TrackNumber; import static org.jcodec.containers.mkv.Type.TrackType; import static org.jcodec.containers.mkv.Type.TrackUID; import static org.jcodec.containers.mkv.Type.Tracks; import java.io.IOException; import java.nio.channels.FileChannel; import java.util.ArrayList; import java.util.Date; import java.util.LinkedList; import java.util.List; import org.jcodec.common.SeekableByteChannel; import org.jcodec.common.model.Rational; import org.jcodec.common.model.Size; import org.jcodec.common.model.Unit; import org.jcodec.containers.mkv.ebml.BinaryElement; import org.jcodec.containers.mkv.ebml.DateElement; import org.jcodec.containers.mkv.ebml.Element; import org.jcodec.containers.mkv.ebml.FloatElement; import org.jcodec.containers.mkv.ebml.MasterElement; import org.jcodec.containers.mkv.ebml.StringElement; import org.jcodec.containers.mkv.ebml.UnsignedIntegerElement; import org.jcodec.containers.mkv.elements.BlockElement; import org.jcodec.containers.mkv.elements.Cluster; public class MKVMuxer { List<MKVMuxerTrack> tracks = new ArrayList<MKVMuxerTrack>(); private SeekableByteChannel out; private MasterElement mkvInfo; private MasterElement mkvTracks; private MasterElement mkvCues; private MasterElement mkvSeekHead; private MasterElement segmentElem; private LinkedList<Cluster> mkvClusters = new LinkedList<Cluster>(); public MKVMuxer(SeekableByteChannel out) { this.out = out; } public MKVMuxerTrack addVideoTrack(Size dimentions, String encoder) { MKVMuxerTrack video = new MKVMuxerTrack(tracks.size()+1); video.dimentions = dimentions; video.encoder = encoder; video.ttype = TType.VIDEO; tracks.add(video); return video; } MKVMuxerTrack addVideoTrack(Size dimentions, String encoder, int timescale) { MKVMuxerTrack video = new MKVMuxerTrack(tracks.size(), timescale); video.dimentions = dimentions; video.encoder = encoder; video.ttype = TType.VIDEO; tracks.add(video); return video; } MKVMuxerTrack addAudioTrack(Size dimentions, String encoder, int timescale, int sampleDuration, int sampleSize) { MKVMuxerTrack audio = new MKVMuxerTrack(tracks.size(), timescale); audio.encoder = encoder; audio.sampleDuration = sampleDuration; audio.sampleSize = sampleSize; audio.ttype = TType.AUDIO; tracks.add(audio); return audio; } void writeHeader() throws IOException { muxEbmlHeader(); muxSegmentHeader(); } public void mux() throws IOException { /** * In order to write Cues, one has to know the sized of Clusters fist. * thus blocks are organized into clusters before writing header. * */ getVideoTrack().clusterBlocks(); // EBML // SeekHead // Info // Tracks // Cues writeHeader(); // Clusters muxClusters(); segmentElem.mux(out); // TODO: Chapters // TODO: Attachments // TODO: Tags } private void muxSegmentHeader() { // # Segment segmentElem = (MasterElement) Type.createElementByType(Segment); // # Meta Seek // muxSeeks(segmentElem); muxInfo(); muxTracks(); muxSeekHead(); muxCues(); // Tracks Info segmentElem.addChildElement(mkvSeekHead); segmentElem.addChildElement(mkvInfo); segmentElem.addChildElement(mkvTracks); segmentElem.addChildElement(mkvCues); } private void muxCues() { CuesIndexer ci = new CuesIndexer(cuesOffsset(), 1); for (Cluster aCluster : mkvClusters) ci.add(make(aCluster)); MasterElement indexedCues = ci.createCues(); for (Element aCuePoint : indexedCues.children) mkvCues.addChildElement(aCuePoint); System.out.println("cues size: " + mkvCues.getSize()); } private void muxSeekHead() { SeekHeadIndexer shi = new SeekHeadIndexer(); mkvCues = (MasterElement) Type.createElementByType(Cues); shi.add(mkvInfo); shi.add(mkvTracks); shi.add(mkvCues); mkvSeekHead = shi.indexSeekHead(); } private long cuesOffsset() { return mkvSeekHead.getSize() + mkvInfo.getSize() + mkvTracks.getSize(); } private void muxEbmlHeader() throws IOException { MasterElement ebmlHeaderElem = (MasterElement) Type.createElementByType(Type.EBML); StringElement docTypeElem = (StringElement) Type.createElementByType(Type.DocType); docTypeElem.set("matroska"); UnsignedIntegerElement docTypeVersionElem = (UnsignedIntegerElement) Type.createElementByType(Type.DocTypeVersion); docTypeVersionElem.set(2); UnsignedIntegerElement docTypeReadVersionElem = (UnsignedIntegerElement) Type.createElementByType(Type.DocTypeReadVersion); docTypeReadVersionElem.set(2); ebmlHeaderElem.addChildElement(docTypeElem); ebmlHeaderElem.addChildElement(docTypeVersionElem); ebmlHeaderElem.addChildElement(docTypeReadVersionElem); ebmlHeaderElem.mux(out); } private void muxClusters() { for (Cluster cluster : mkvClusters) segmentElem.addChildElement(cluster); } private void muxTracks() { mkvTracks = (MasterElement) Type.createElementByType(Tracks); for (MKVMuxerTrack track : tracks) { MasterElement trackEntryElem = (MasterElement) Type.createElementByType(TrackEntry); createAndAddElement(trackEntryElem, TrackNumber, track.trackId); createAndAddElement(trackEntryElem, TrackUID, track.trackId); createAndAddElement(trackEntryElem, TrackType, track.getMkvType()); createAndAddElement(trackEntryElem, Name, track.getName()); // trackEntryElem.addChildElement(findFirst(track, TrackEntry, Language)); createAndAddElement(trackEntryElem, CodecID, track.encoder); // trackEntryElem.addChildElement(findFirst(track, TrackEntry, CodecPrivate)); // trackEntryElem.addChildElement(findFirst(track, TrackEntry, DefaultDuration)); // Now we add the audio/video dependant sub-elements if (track.isVideo()) { MasterElement trackVideoElem = (MasterElement) Type.createElementByType(Type.Video); createAndAddElement(trackVideoElem, PixelWidth, track.dimentions.getWidth()); createAndAddElement(trackVideoElem, PixelHeight, track.dimentions.getHeight()); trackEntryElem.addChildElement(trackVideoElem); } else if (track.isAudio()) { MasterElement trackAudioElem = (MasterElement) Type.createElementByType(Audio); createAndAddElement(trackAudioElem, Channels, track.channels); createAndAddElement(trackAudioElem, BitDepth, track.bitdepth); createAndAddElement(trackAudioElem, SamplingFrequency, track.samplingFrequency); createAndAddElement(trackAudioElem, OutputSamplingFrequency, track.outputSamplingFrequency); trackEntryElem.addChildElement(trackAudioElem); } mkvTracks.addChildElement(trackEntryElem); } } private void muxInfo() { // # Segment Info mkvInfo = (MasterElement) Type.createElementByType(Type.Info); // Add timecode scale UnsignedIntegerElement timecodescaleElem = (UnsignedIntegerElement) Type.createElementByType(Type.TimecodeScale); timecodescaleElem.set(getVideoTrack().getTimescale()); mkvInfo.addChildElement(timecodescaleElem); FloatElement durationElem = (FloatElement) Type.createElementByType(Type.Duration); durationElem.set(getVideoTrack().getTrackTotalDuration()); mkvInfo.addChildElement(durationElem); DateElement dateElem = (DateElement) Type.createElementByType(Type.DateUTC); dateElem.setDate(new Date()); mkvInfo.addChildElement(dateElem); StringElement writingAppElem = (StringElement) Type.createElementByType(Type.WritingApp); writingAppElem.set("JCodec v0.1.0"); mkvInfo.addChildElement(writingAppElem); StringElement muxingAppElem = (StringElement) Type.createElementByType(Type.MuxingApp); muxingAppElem.set("JCodec MKVMuxer v0.1a"); mkvInfo.addChildElement(muxingAppElem); } MKVMuxerTrack getVideoTrack() { for (MKVMuxerTrack track : tracks) if (track.isVideo()) return track; return null; } MKVMuxerTrack getTimecodeTrack() { for (MKVMuxerTrack track : tracks) if (track.isTimecode()) return track; return null; } List<MKVMuxerTrack> getAudioTracks() { List<MKVMuxerTrack> audio = new ArrayList<MKVMuxerTrack>(); for (MKVMuxerTrack t : tracks) if (t.isAudio()) audio.add(t); return audio; } public static void createAndAddElement(MasterElement parent, Type type, byte[] value) { BinaryElement se = (BinaryElement) Type.createElementByType(type); se.setData(value); parent.addChildElement(se); } public static void createAndAddElement(MasterElement parent, Type type, double value) { FloatElement se = (FloatElement) Type.createElementByType(type); se.set(value); parent.addChildElement(se); } public static void createAndAddElement(MasterElement parent, Type type, long value) { UnsignedIntegerElement se = (UnsignedIntegerElement) Type.createElementByType(type); se.set(value); parent.addChildElement(se); } public static void createAndAddElement(MasterElement parent, Type type, String value) { StringElement se = (StringElement) Type.createElementByType(type); se.set(value); parent.addChildElement(se); } public enum TType { VIDEO, AUDIO, TIMECODE; } // MuxerTrack // UncompressedTrack // TimecodeTrack // CompressedTrack public class MKVMuxerTrack { private static final int NANOSECONDS_IN_A_SECOND = 1000000000; public int bitdepth; public int channels; public double outputSamplingFrequency; public double samplingFrequency; public int sampleSize; public int sampleDuration; public long chunkDuration; public String encoder; public Size dimentions; public int trackId; private int timescale = 40000000; public int currentBlock = 0; public List<BlockElement> blocks = new ArrayList<BlockElement>(); MKVMuxer.TType ttype = TType.VIDEO; MKVMuxerTrack(int trackId) { this.trackId = trackId; } MKVMuxerTrack(int trackId, int timescale) { this.trackId = trackId; this.timescale = timescale; } public String getName() { String name = ""; if (isVideo()) name = "Video"; if (isAudio()) name = "Audio"; return name+trackId; } public byte getMkvType() { //A set of track types coded on 8 bits (1: video, 2: audio, 3: complex, 0x10: logo, 0x11: subtitle, 0x12: buttons, 0x20: control). if (isVideo()) return 0x01; if (isAudio()) return 0x02; return 0x03; } public void setTgtChunkDuration(Rational duration, Unit unit) { } long getTrackTotalDuration() { return 0; } int getTimescale() { return timescale; } boolean isVideo() { return TType.VIDEO.equals(this.ttype); } boolean isTimecode() { return false; } boolean isAudio() { return TType.AUDIO.equals(this.ttype); } Size getDisplayDimensions() { return null; } public void addSampleEntry(BlockElement se) { blocks.add(se); } public void clusterBlocks() { int framesPerCluster = NANOSECONDS_IN_A_SECOND/timescale; long i=0; for (BlockElement be : blocks){ if (i%framesPerCluster == 0) { Cluster c = Type.createElementByType(Type.Cluster); createAndAddElement(c, Type.Timecode, i); c.timecode = i; if (!mkvClusters.isEmpty()){ long prevSize = mkvClusters.getLast().getSize(); createAndAddElement(c, Type.PrevSize, prevSize); c.prevsize = prevSize; } mkvClusters.add(c); } Cluster c = mkvClusters.getLast(); be.timecode = (int)(i - c.timecode); c.addChildElement(be); i++; } } // private void tracksToClusters() { // mkvClusters = new ArrayList<Cluster>(); // // long timecodeBase = 0; // int frameRate = 25; // 1000000000/Segment.Info.TimecodeScale // Cluster cluster = Type.createElementByType(Type.Cluster); // List<Element> blocks = new ArrayList<Element>(); // // for (MKVMuxerTrack aTrack : tracks) { // aTrack.getTimescale(); // // createAndAddElement(cluster, Timecode, timecodeBase); // createAndAddElement(cluster, PrevSize, mkvClusters.get(mkvClusters.size()-1).getSize()); // // for (Element child : aCluster.children) { // if (child.type.equals(Type.SimpleBlock)) { // BlockElement aBlock = (BlockElement) child; // BlockElement be = copy(aBlock); // be.readFrames(source); // blocks.add(be); // } else if (child.type.equals(Type.BlockGroup)) { // MasterElement aBlockGroup = (MasterElement) child; // MasterElement bg = new MasterElement(Type.BlockGroup.id); // bg.type = Type.BlockGroup; // BlockElement aBlock = (BlockElement) Type.findFirst(aBlockGroup, Type.BlockGroup, Type.Block); // BlockElement be = BlockElement.copy(aBlock); // be.readFrames(source); // bg.addChildElement(be); // bg.addChildElement(Type.findFirst(aBlockGroup, Type.BlockGroup, Type.BlockDuration)); // bg.addChildElement(Type.findFirst(aBlockGroup, Type.BlockGroup, Type.ReferenceBlock)); // blocks.add(bg); // } // } // } // for (Element e : blocks) // cluster.addChildElement(e); // // mkvClusters.add(cluster); // // } } }