package org.jcodec.containers.mkv; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.jcodec.common.LongArrayList; import org.jcodec.common.NIOUtils; import org.jcodec.common.model.Packet; import org.jcodec.common.model.TapeTimecode; import org.jcodec.containers.mkv.ebml.BinaryElement; import org.jcodec.containers.mkv.ebml.Element; 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; import org.jcodec.containers.mkv.elements.TrackEntryElement; import org.jcodec.containers.mkv.elements.Tracks; /** * This class is part of JCodec ( www.jcodec.org ) This software is distributed * under FreeBSD License * * @author The JCodec project * */ public class MKVDemuxer { private List<MKVDemuxer.MKVDemuxerTrack> tracks = new ArrayList<MKVDemuxer.MKVDemuxerTrack>(); private FileChannel input; private UnsignedIntegerElement scale; public static MKVDemuxer getDemuxer(FileChannel fc) throws IOException{ SimpleEBMLParser par = new SimpleEBMLParser(fc); par.parse(); return new MKVDemuxer(par.getTree(), fc); } public static Map<Long, List<BlockElement>> demuxClusters(Cluster[] clusters){ Map<Long, List<BlockElement>> perTrackBlocs = new HashMap<Long, List<BlockElement>>(); for (Cluster c : clusters){ UnsignedIntegerElement uie = (UnsignedIntegerElement) Type.findFirst(c, Type.Cluster, Type.Timecode); long timecode = uie.get(); for(Element e : c.children){ BlockElement be = null; if (e instanceof BlockElement){ be = (BlockElement) e; } else if (e.type.equals(Type.BlockGroup)) { be = (BlockElement) Type.findFirst(e, Type.BlockGroup, Type.Block); } if (be == null) continue; be.absoluteTimecode = timecode + be.timecode; Long no = Long.valueOf(be.trackNumber); if (!perTrackBlocs.containsKey(no)){ perTrackBlocs.put(no, new ArrayList<BlockElement>()); } perTrackBlocs.get(no).add(be); } } return perTrackBlocs; } public MKVDemuxer(List<MasterElement> tree, FileChannel input) { this.input = input; Tracks tracs = Type.findFirst(tree, Type.Segment, Type.Tracks); Cluster[] clusters = Type.findAll(tree, Cluster.class, Type.Segment, Type.Cluster); scale = Type.findFirst(tree, Type.Segment, Type.Info, Type.TimecodeScale); Map<Long, List<BlockElement>> p = demuxClusters(clusters); for(TrackEntryElement track : Type.findAll(tracs, TrackEntryElement.class, Type.Tracks, Type.TrackEntry)){ MasterElement video = (MasterElement) Type.findFirst(track, Type.TrackEntry, Type.Video); MasterElement audio = (MasterElement) Type.findFirst(track, Type.TrackEntry, Type.Audio); UnsignedIntegerElement trackNo = (UnsignedIntegerElement) Type.findFirst(track, Type.TrackEntry, Type.TrackNumber); long no = trackNo.get(); List<BlockElement> bes = p.get(Long.valueOf(no)); MKVDemuxer.MKVDemuxerTrack t = null; if (video != null){ t = new VideoTrack(track, no, bes); } else if (audio != null){ t = new AudioTrack(track, no, bes); } else { t = new OtherTrack(track, no, bes); } tracks.add(t); } } public VideoTrack getVideoTrack() { for (MKVDemuxer.MKVDemuxerTrack t : tracks) if (t instanceof VideoTrack) return (VideoTrack) t; return null; } public MKVDemuxer.MKVDemuxerTrack getTrack(int no) { return tracks.get(no); } public List<AudioTrack> getAudioTracks() { List<AudioTrack> audioTracks = new ArrayList<AudioTrack>(); for (MKVDemuxer.MKVDemuxerTrack t : tracks) if (t instanceof AudioTrack) audioTracks.add((AudioTrack)t); return audioTracks; } public MKVDemuxer.MKVDemuxerTrack[] getTracks() { return tracks.toArray(new MKVDemuxer.MKVDemuxerTrack[]{}); } public static interface MKVDemuxerTrack { public Packet getFrames(ByteBuffer buffer, int n) throws IOException; public Packet getFrames(int n) throws IOException; public void seekPointer(int frameNo); public int getFrameCount(); public String getCodecID(); public long getNo(); } public class VideoTrack implements MKVDemuxerTrack { private long no; private List<BlockElement> bes; private int pointer = 0; public long defaultDuration; public long pixelWidth = -1; public long pixelHeight = -1; public long displayWidth = -1; public long displayHeight = -1; public long displayUnit = 0; public ByteBuffer codecState; public String codec; public VideoTrack(TrackEntryElement track, long no, List<BlockElement> bes) { this.no = no; this.bes = bes; UnsignedIntegerElement uie = (UnsignedIntegerElement) Type.findFirst(track, Type.TrackEntry, Type.DefaultDuration); if (uie != null) this.defaultDuration = uie.get(); UnsignedIntegerElement width = (UnsignedIntegerElement) Type.findFirst(track, Type.TrackEntry, Type.Video, Type.PixelWidth); if (width != null) this.pixelWidth = width.get(); UnsignedIntegerElement height = (UnsignedIntegerElement) Type.findFirst(track, Type.TrackEntry, Type.Video, Type.PixelHeight); if (height != null) this.pixelHeight = height.get(); UnsignedIntegerElement dwidth = (UnsignedIntegerElement) Type.findFirst(track, Type.TrackEntry, Type.Video, Type.DisplayWidth); if (dwidth != null) this.displayWidth = dwidth.get(); UnsignedIntegerElement dheight = (UnsignedIntegerElement) Type.findFirst(track, Type.TrackEntry, Type.Video, Type.DisplayHeight); if (dheight != null) this.displayHeight = dheight.get(); UnsignedIntegerElement dunit = (UnsignedIntegerElement) Type.findFirst(track, Type.TrackEntry, Type.Video, Type.DisplayUnit); if (dunit != null) this.displayUnit = dunit.get(); BinaryElement cprivate = (BinaryElement) Type.findFirst(track, Type.TrackEntry, Type.CodecPrivate); if (cprivate != null) this.codecState = ByteBuffer.wrap(cprivate.getData()); StringElement codecelement = (StringElement) Type.findFirst(track, Type.TrackEntry, Type.CodecID); if (codecelement != null) this.codec = codecelement.get(); } public Packet getFrames(ByteBuffer buffer, int n) throws IOException { if (n != 1) throw new IllegalArgumentException("Frames should be = 1 for this track"); if (pointer >= bes.size()) return null; BlockElement block = bes.get(pointer); int size = (int) block.frameSizes[0]; if (buffer.remaining() < size) throw new IllegalArgumentException("Buffer size is not enough to fit a packet"); Packet packet = readFrame(buffer, size, block); this.pointer++; return packet; } public Packet getFrames(int n) throws IOException { if (n != 1) throw new IllegalArgumentException("Frames should be = 1 for this track"); if (pointer >= bes.size()) return null; BlockElement be = bes.get(pointer); Packet packet = readFrame(ByteBuffer.allocate((int) be.frameSizes[0]), n, be); this.pointer++; return packet; } private Packet readFrame(ByteBuffer buffer, int size, BlockElement block) throws IOException { input.position(block.frameOffsets[0]); if (NIOUtils.read(input, buffer) < size) return null; buffer.flip(); long timecale = scale != null ? scale.get() : 1L; return new Packet(buffer, 0L, timecale, 0L, pointer, block.keyFrame, new TapeTimecode((short)0, (byte)0, (byte)0, (byte)0, false)); } public void seekPointer(int frameNo) { pointer = frameNo; } public int getFrameCount() { return bes.size(); } @Override public long getNo() { return no; } @Override public String getCodecID() { return codec; } } public class AudioTrack implements MKVDemuxer.MKVDemuxerTrack { private long no; long[] sampleOffsets; private long[] sampleSizes; int currentSampleNo = 0; private String codec; public AudioTrack(TrackEntryElement track, long no, List<BlockElement> bes) { this.no = no; LongArrayList offsets = new LongArrayList(); LongArrayList sizes = new LongArrayList(); for(BlockElement be : bes){ offsets.addAll(be.frameOffsets); sizes.addAll(be.frameSizes); } sampleOffsets = offsets.toArray(); sampleSizes = sizes.toArray(); StringElement codecelement = (StringElement) Type.findFirst(track, Type.TrackEntry, Type.CodecID); if (codecelement != null) this.codec = codecelement.get(); } public Packet getFrames(ByteBuffer buffer, int n) throws IOException { if (n<=0 || (currentSampleNo+n-1) > sampleOffsets.length) return null; int size = 0; for(int i=0;i<n;i++) size += sampleSizes[currentSampleNo+i]; if (buffer.remaining() < size) throw new IllegalArgumentException("Buffer size is not enough to fit a packet"); return readFrames(buffer, size, n); } public Packet getFrames(int n) throws IOException { if (n<=0 || (currentSampleNo+n-1) >= sampleOffsets.length) return null; int size = 0; for(int i=0;i<n;i++) size += sampleSizes[currentSampleNo+i]; return readFrames(ByteBuffer.allocate(size), size, n); } private Packet readFrames(ByteBuffer buffer, int size, int n) throws IOException { for (int i = 0; i < n; i++) { input.position(sampleOffsets[currentSampleNo + i]); if (NIOUtils.read(input, buffer, (int) sampleSizes[currentSampleNo + i]) < sampleSizes[currentSampleNo + i]) return null; } buffer.flip(); long timecale = scale != null ? scale.get() : 1L; return new Packet(buffer, 0L, timecale, 0L, currentSampleNo, true, new TapeTimecode((short)0, (byte)0, (byte)0, (byte)0, false)); } public void seekPointer(int sampleNo) { this.currentSampleNo = sampleNo; } public int getFrameCount() { return sampleOffsets.length; } @Override public long getNo() { return no; } @Override public String getCodecID() { return codec; } } public class OtherTrack implements MKVDemuxerTrack { private long no; private List<BlockElement> bes; public OtherTrack(TrackEntryElement track, long no, List<BlockElement> bes) { this.no = no; this.bes = bes; } @Override public Packet getFrames(ByteBuffer buffer, int n) throws IOException { // TODO Auto-generated method stub return null; } @Override public Packet getFrames(int n) throws IOException { // TODO Auto-generated method stub return null; } @Override public void seekPointer(int frameNo) { // TODO Auto-generated method stub } @Override public int getFrameCount() { // TODO Auto-generated method stub return 0; } @Override public long getNo() { return no; } @Override public String getCodecID() { // TODO Auto-generated method stub return null; } } }