package org.jcodec.containers.mkv.demuxer; import static org.jcodec.containers.mkv.MKVType.Audio; import static org.jcodec.containers.mkv.MKVType.Cluster; import static org.jcodec.containers.mkv.MKVType.CodecPrivate; import static org.jcodec.containers.mkv.MKVType.DisplayHeight; import static org.jcodec.containers.mkv.MKVType.DisplayUnit; import static org.jcodec.containers.mkv.MKVType.DisplayWidth; import static org.jcodec.containers.mkv.MKVType.Info; import static org.jcodec.containers.mkv.MKVType.PixelHeight; import static org.jcodec.containers.mkv.MKVType.PixelWidth; import static org.jcodec.containers.mkv.MKVType.SamplingFrequency; import static org.jcodec.containers.mkv.MKVType.Segment; import static org.jcodec.containers.mkv.MKVType.Timecode; 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.Tracks; import static org.jcodec.containers.mkv.MKVType.Video; import static org.jcodec.containers.mkv.MKVType.findFirst; import static org.jcodec.containers.mkv.MKVType.findList; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.jcodec.codecs.h264.H264Utils; import org.jcodec.codecs.h264.mp4.AvcCBox; import org.jcodec.common.Codec; import org.jcodec.common.Demuxer; import org.jcodec.common.DemuxerTrack; import org.jcodec.common.DemuxerTrackMeta; import org.jcodec.common.SeekableDemuxerTrack; import org.jcodec.common.VideoCodecMeta; import org.jcodec.common.io.SeekableByteChannel; import org.jcodec.common.model.ColorSpace; import org.jcodec.common.model.Packet; import org.jcodec.common.model.Size; import org.jcodec.common.model.TapeTimecode; import org.jcodec.common.model.Packet.FrameType; import org.jcodec.containers.mkv.MKVParser; import org.jcodec.containers.mkv.MKVType; import org.jcodec.containers.mkv.boxes.EbmlBase; import org.jcodec.containers.mkv.boxes.EbmlBin; 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; /** * This class is part of JCodec ( www.jcodec.org ) This software is distributed * under FreeBSD License * * @author The JCodec project * */ public final class MKVDemuxer implements Demuxer { private VideoTrack vTrack = null; private List<DemuxerTrack> aTracks; private List<EbmlMaster> t; private SeekableByteChannel channel; int timescale = 1; int pictureWidth; int pictureHeight; private static Map<String, Codec> codecMapping = new HashMap<String, Codec>(); static { codecMapping.put("V_VP8", Codec.VP8); codecMapping.put("V_VP9", Codec.VP9); codecMapping.put("V_MPEG4/ISO/AVC", Codec.H264); } public MKVDemuxer(SeekableByteChannel fileChannelWrapper) throws IOException { this.channel = fileChannelWrapper; this.aTracks = new ArrayList<DemuxerTrack>(); MKVParser parser = new MKVParser(channel); this.t = parser.parse(); demux(); } private void demux() { MKVType[] path = { Segment, Info, TimecodeScale }; EbmlUint ts = MKVType.findFirstTree(t, path); if (ts != null) timescale = (int) ts.getUint(); MKVType[] path9 = { Segment, Tracks, TrackEntry }; for (EbmlMaster aTrack : findList(t, EbmlMaster.class, path9)) { MKVType[] path1 = { TrackEntry, TrackType }; long type = ((EbmlUint) findFirst(aTrack, path1)).getUint(); MKVType[] path2 = { TrackEntry, TrackNumber }; long id = ((EbmlUint) findFirst(aTrack, path2)).getUint(); if (type == 1) { // video if (vTrack != null) throw new RuntimeException("More then 1 video track, can not compute..."); MKVType[] path3 = { TrackEntry, CodecPrivate }; MKVType[] path10 = { TrackEntry, MKVType.CodecID }; EbmlString codecId = (EbmlString) findFirst(aTrack, path10); Codec codec = codecMapping.get(codecId.getString()); EbmlBin videoCodecState = (EbmlBin) findFirst(aTrack, path3); ByteBuffer state = null; if (videoCodecState != null) state = videoCodecState.data; MKVType[] path4 = { TrackEntry, Video, PixelWidth }; EbmlUint width = (EbmlUint) findFirst(aTrack, path4); MKVType[] path5 = { TrackEntry, Video, PixelHeight }; EbmlUint height = (EbmlUint) findFirst(aTrack, path5); MKVType[] path6 = { TrackEntry, Video, DisplayWidth }; EbmlUint dwidth = (EbmlUint) findFirst(aTrack, path6); MKVType[] path7 = { TrackEntry, Video, DisplayHeight }; EbmlUint dheight = (EbmlUint) findFirst(aTrack, path7); MKVType[] path8 = { TrackEntry, Video, DisplayUnit }; EbmlUint unit = (EbmlUint) findFirst(aTrack, path8); if (width != null && height != null) { pictureWidth = (int) width.getUint(); pictureHeight = (int) height.getUint(); } else if (dwidth != null && dheight != null) { if (unit == null || unit.getUint() == 0) { pictureHeight = (int) dheight.getUint(); pictureWidth = (int) dwidth.getUint(); } else { throw new RuntimeException("DisplayUnits other then 0 are not implemented yet"); } } vTrack = new VideoTrack(this, (int) id, state, codec); } else if (type == 2) { AudioTrack audioTrack = new AudioTrack((int) id, this); MKVType[] path3 = { TrackEntry, Audio, SamplingFrequency }; EbmlFloat sf = (EbmlFloat) findFirst(aTrack, path3); if (sf != null) audioTrack.samplingFrequency = sf.getDouble(); aTracks.add(audioTrack); } } MKVType[] path2 = { Segment, Cluster }; for (EbmlMaster aCluster : findList(t, EbmlMaster.class, path2)) { MKVType[] path1 = { Cluster, Timecode }; long baseTimecode = ((EbmlUint) findFirst(aCluster, path1)).getUint(); for (EbmlBase child : aCluster.children) if (MKVType.SimpleBlock.equals(child.type)) { MkvBlock b = (MkvBlock) child; b.absoluteTimecode = b.timecode + baseTimecode; putIntoRightBasket(b); } else if (MKVType.BlockGroup.equals(child.type)) { EbmlMaster group = (EbmlMaster) child; for (EbmlBase grandChild : group.children) { if (grandChild.type == MKVType.Block) { MkvBlock b = (MkvBlock) child; b.absoluteTimecode = b.timecode + baseTimecode; putIntoRightBasket(b); } } } } } private void putIntoRightBasket(MkvBlock b) { if (b.trackNumber == vTrack.trackNo) { vTrack.blocks.add(b); } else { for (int i = 0; i < aTracks.size(); i++) { AudioTrack audio = (AudioTrack) aTracks.get(i); if (b.trackNumber == audio.trackNo) { audio.blocks.add(IndexedBlock.make(audio.framesCount, b)); audio.framesCount += b.frameSizes.length; } } } } private static final TapeTimecode ZERO_TAPE_TIMECODE = new TapeTimecode((short) 0, (byte) 0, (byte) 0, (byte) 0, false); public static class VideoTrack implements SeekableDemuxerTrack { private ByteBuffer state; public final int trackNo; private int frameIdx = 0; List<MkvBlock> blocks; private MKVDemuxer demuxer; private Codec codec; private AvcCBox avcC; public VideoTrack(MKVDemuxer demuxer, int trackNo, ByteBuffer state, Codec codec) { this.blocks = new ArrayList<MkvBlock>(); this.demuxer = demuxer; this.trackNo = trackNo; this.codec = codec; if (codec == Codec.H264) { avcC = H264Utils.parseAVCCFromBuffer(state); this.state = H264Utils.avcCToAnnexB(avcC); } else { this.state = state; } } @Override public Packet nextFrame() throws IOException { if (frameIdx >= blocks.size()) return null; MkvBlock b = blocks.get(frameIdx); if (b == null) throw new RuntimeException("Something somewhere went wrong."); frameIdx++; /** * This part could be moved withing yet-another inner class, say * MKVPacket to that channel is actually read only when * Packet.getData() is executed. */ demuxer.channel.setPosition(b.dataOffset); ByteBuffer data = ByteBuffer.allocate(b.dataLen); demuxer.channel.read(data); data.flip(); b.readFrames(data.duplicate()); long duration = 1; if (frameIdx < blocks.size()) duration = blocks.get(frameIdx).absoluteTimecode - b.absoluteTimecode; ByteBuffer result = b.frames[0].duplicate(); if (codec == Codec.H264) { result = H264Utils.decodeMOVPacket(result, avcC); } return Packet.createPacket(result, b.absoluteTimecode, demuxer.timescale, duration, frameIdx - 1, b._keyFrame ? FrameType.KEY : FrameType.INTER, ZERO_TAPE_TIMECODE); } @Override public boolean gotoFrame(long i) { if (i > Integer.MAX_VALUE) return false; if (i > blocks.size()) return false; frameIdx = (int) i; return true; } @Override public long getCurFrame() { return frameIdx; } @Override public void seek(double second) { throw new RuntimeException("Not implemented yet"); } public int getFrameCount() { return blocks.size(); } public ByteBuffer getCodecState() { return state; } @Override public DemuxerTrackMeta getMeta() { return new DemuxerTrackMeta(org.jcodec.common.TrackType.VIDEO, codec, 0, null, 0, state, new VideoCodecMeta(new Size(demuxer.pictureWidth, demuxer.pictureHeight), ColorSpace.YUV420), null); } @Override public boolean gotoSyncFrame(long i) { throw new RuntimeException("Unsupported"); } } public static class IndexedBlock { public int firstFrameNo; public MkvBlock block; public static IndexedBlock make(int no, MkvBlock b) { IndexedBlock ib = new IndexedBlock(); ib.firstFrameNo = no; ib.block = b; return ib; } } public static class AudioTrack implements SeekableDemuxerTrack { public double samplingFrequency; public final int trackNo; List<IndexedBlock> blocks; private int framesCount = 0; private int frameIdx = 0; private int blockIdx = 0; private int frameInBlockIdx = 0; private MKVDemuxer demuxer; public AudioTrack(int trackNo, MKVDemuxer demuxer) { this.blocks = new ArrayList<IndexedBlock>(); this.trackNo = trackNo; this.demuxer = demuxer; } @Override public Packet nextFrame() throws IOException { if (frameIdx > blocks.size()) return null; MkvBlock b = blocks.get(blockIdx).block; if (b == null) throw new RuntimeException("Something somewhere went wrong."); if (b.frames == null || b.frames.length == 0) { /** * This part could be moved withing yet-another inner class, say * MKVPacket to that channel is actually rean only when * Packet.getData() is executed. */ demuxer.channel.setPosition(b.dataOffset); ByteBuffer data = ByteBuffer.allocate(b.dataLen); demuxer.channel.read(data); b.readFrames(data); } ByteBuffer data = b.frames[frameInBlockIdx].duplicate(); frameInBlockIdx++; frameIdx++; if (frameInBlockIdx >= b.frames.length) { blockIdx++; frameInBlockIdx = 0; } return Packet.createPacket(data, b.absoluteTimecode, (int) Math.round(samplingFrequency), 1, 0, FrameType.KEY, ZERO_TAPE_TIMECODE); } @Override public boolean gotoFrame(long i) { if (i > Integer.MAX_VALUE) return false; if (i > this.framesCount) return false; int frameBlockIdx = findBlockIndex(i); if (frameBlockIdx == -1) return false; frameIdx = (int) i; blockIdx = frameBlockIdx; frameInBlockIdx = (int) i - blocks.get(blockIdx).firstFrameNo; return true; } private int findBlockIndex(long i) { for (int blockIndex = 0; blockIndex < blocks.size(); blockIndex++) { if (i < blocks.get(blockIndex).block.frameSizes.length) return blockIndex; i -= blocks.get(blockIndex).block.frameSizes.length; } return -1; } @Override public long getCurFrame() { return frameIdx; } @Override public void seek(double second) { throw new RuntimeException("Not implemented yet"); } /** * Get multiple frames * * @param count * @return */ public Packet getFrames(int count) { if (count + frameIdx >= framesCount) return null; List<ByteBuffer> packetFrames = new ArrayList<ByteBuffer>(); MkvBlock firstBlockInAPacket = blocks.get(blockIdx).block; while (count > 0) { MkvBlock b = blocks.get(blockIdx).block; if (b.frames == null || b.frames.length == 0) { /** * This part could be moved withing yet-another inner class, * say MKVPacket to that channel is actually rean only when * Packet.getData() is executed. */ try { demuxer.channel.setPosition(b.dataOffset); ByteBuffer data = ByteBuffer.allocate(b.dataLen); demuxer.channel.read(data); b.readFrames(data); } catch (IOException ioe) { throw new RuntimeException("while reading frames of a Block at offset 0x" + Long.toHexString(b.dataOffset).toUpperCase() + ")", ioe); } } packetFrames.add(b.frames[frameInBlockIdx].duplicate()); frameIdx++; frameInBlockIdx++; if (frameInBlockIdx >= b.frames.length) { frameInBlockIdx = 0; blockIdx++; } count--; } int size = 0; for (ByteBuffer aFrame : packetFrames) size += aFrame.limit(); ByteBuffer data = ByteBuffer.allocate(size); for (ByteBuffer aFrame : packetFrames) data.put(aFrame); return Packet.createPacket(data, firstBlockInAPacket.absoluteTimecode, (int) Math.round(samplingFrequency), packetFrames.size(), 0, FrameType.KEY, ZERO_TAPE_TIMECODE); } @Override public DemuxerTrackMeta getMeta() { return null; } @Override public boolean gotoSyncFrame(long frame) { return gotoFrame(frame); } } public int getPictureWidth() { return pictureWidth; } public int getPictureHeight() { return pictureHeight; } @Override public List<DemuxerTrack> getAudioTracks() { return aTracks; } @Override public List<DemuxerTrack> getTracks() { ArrayList<DemuxerTrack> tracks = new ArrayList<DemuxerTrack>(aTracks); tracks.add(vTrack); return tracks; } @Override public List<DemuxerTrack> getVideoTracks() { ArrayList<DemuxerTrack> tracks = new ArrayList<DemuxerTrack>(); tracks.add(vTrack); return tracks; } public List<? extends EbmlBase> getTree() { return t; } @Override public void close() throws IOException { channel.close(); } }