/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * Copyright 2014, OpenSpace Solutions LLC. All Right Reserved. */ package com.chiorichan.dvr.encoder; import java.io.IOException; 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.CuesIndexer; import static org.jcodec.containers.mkv.CuesIndexer.CuePointMock.make; import org.jcodec.containers.mkv.SeekHeadIndexer; import org.jcodec.containers.mkv.Type; 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 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; /** * * @author Chiori Greene */ public class WebMuxer { List<WebMuxerTrack> tracks = new ArrayList<WebMuxerTrack>(); 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>(); private String writingApp = "JCodec"; private String muxingApp = "JCodec WebMuxer"; public WebMuxer( SeekableByteChannel out ) { this.out = out; } public WebMuxerTrack addVideoTrack( Size dimentions, String encoder ) { WebMuxerTrack video = new WebMuxerTrack( tracks.size() + 1 ); video.dimentions = dimentions; video.encoder = encoder; video.ttype = TType.VIDEO; tracks.add( video ); return video; } public WebMuxerTrack addVideoTrack( Size dimentions, String encoder, int timescale ) { WebMuxerTrack video = new WebMuxerTrack( tracks.size(), timescale ); video.dimentions = dimentions; video.encoder = encoder; video.ttype = TType.VIDEO; tracks.add( video ); return video; } public WebMuxerTrack addAudioTrack( Size dimentions, String encoder, int timescale, int sampleDuration, int sampleSize ) { WebMuxerTrack audio = new WebMuxerTrack( 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( "webm" ); 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 ( WebMuxerTrack track : tracks ) { MasterElement trackEntryElem = (MasterElement) Type.createElementByType( TrackEntry ); createAndAddElement( trackEntryElem, TrackNumber, track.trackId ); createAndAddElement( trackEntryElem, TrackUID, track.trackId ); createAndAddElement( trackEntryElem, TrackType, track.getMkvType() ); if ( track.getName() != null && !track.getName().isEmpty() ) 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 ); } } public void setWritingApp( String name ) { writingApp = name; } public void setMuxingApp( String name ) { muxingApp = name; } 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( writingApp ); mkvInfo.addChildElement( writingAppElem ); StringElement muxingAppElem = (StringElement) Type.createElementByType( Type.MuxingApp ); muxingAppElem.set( muxingApp ); mkvInfo.addChildElement( muxingAppElem ); } WebMuxerTrack getVideoTrack() { for ( WebMuxerTrack track : tracks ) { if ( track.isVideo() ) return track; } return null; } WebMuxerTrack getTimecodeTrack() { for ( WebMuxerTrack track : tracks ) { if ( track.isTimecode() ) return track; } return null; } List<WebMuxerTrack> getAudioTracks() { List<WebMuxerTrack> audio = new ArrayList<WebMuxerTrack>(); for ( WebMuxerTrack 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 WebMuxerTrack { 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 = 1000000; public int currentBlock = 0; public List<BlockElement> blocks = new ArrayList<BlockElement>(); public String trackName = null; WebMuxer.TType ttype = TType.VIDEO; WebMuxerTrack( int trackId ) { this.trackId = trackId; } WebMuxerTrack( int trackId, int timescale ) { this.trackId = trackId; this.timescale = timescale; } public void setName( String name ) { trackName = name; } public String getName() { return trackName; } 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++; } } } }