package org.jcodec.containers.mkv.boxes; import static java.lang.System.arraycopy; import static org.jcodec.containers.mkv.boxes.EbmlSint.convertToBytes; import static org.jcodec.containers.mkv.boxes.EbmlSint.signedComplement; import org.jcodec.common.ByteArrayList; import org.jcodec.common.io.SeekableByteChannel; import org.jcodec.containers.mkv.util.EbmlUtil; import org.jcodec.platform.Platform; import java.io.IOException; import java.lang.IllegalArgumentException; import java.lang.StringBuilder; import java.lang.System; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.util.Arrays; /** * This class is part of JCodec ( www.jcodec.org ) This software is distributed * under FreeBSD License * * @author The JCodec project * */ public class MkvBlock extends EbmlBin { private static final String XIPH = "Xiph"; private static final String EBML = "EBML"; private static final String FIXED = "Fixed"; private static final int MAX_BLOCK_HEADER_SIZE = 512; public int[] frameOffsets; public int[] frameSizes; public long trackNumber; public int timecode; public long absoluteTimecode; public boolean _keyFrame; public int headerSize; public String lacing; public boolean discardable; public boolean lacingPresent; public ByteBuffer[] frames; public static final byte[] BLOCK_ID = new byte[]{(byte)0xA1}; public static final byte[] SIMPLEBLOCK_ID = new byte[]{(byte)0xA3}; public static MkvBlock copy(MkvBlock old) { MkvBlock be = new MkvBlock(old.id); be.trackNumber = old.trackNumber; be.timecode = old.timecode; be.absoluteTimecode = old.absoluteTimecode; be._keyFrame = old._keyFrame; be.headerSize = old.headerSize; be.lacing = old.lacing; be.discardable = old.discardable; be.lacingPresent = old.lacingPresent; be.frameOffsets = new int[old.frameOffsets.length]; be.frameSizes = new int[old.frameSizes.length]; be.dataOffset = old.dataOffset; be.offset = old.offset; be.type = old.type; arraycopy(old.frameOffsets, 0, be.frameOffsets, 0, be.frameOffsets.length); arraycopy(old.frameSizes, 0, be.frameSizes, 0, be.frameSizes.length); return be; } public static MkvBlock keyFrame(long trackNumber, int timecode, ByteBuffer frame) { MkvBlock be = new MkvBlock(SIMPLEBLOCK_ID); be.frames = new ByteBuffer[] { frame }; be.frameSizes = new int[] { frame.limit() }; be._keyFrame = true; be.trackNumber = trackNumber; be.timecode = timecode; return be; } public MkvBlock(byte[] type) { super(type); if (!Platform.arrayEqualsByte(SIMPLEBLOCK_ID, type) && !Platform.arrayEqualsByte(BLOCK_ID, type)) throw new IllegalArgumentException("Block initiated with invalid id: " + EbmlUtil.toHexString(type)); } @Override public void readChannel(SeekableByteChannel is) throws IOException { ByteBuffer bb = ByteBuffer.allocate((int) 100); is.read(bb); bb.flip(); this.read(bb); is.setPosition(this.dataOffset+this.dataLen); } @Override public void read(ByteBuffer source) { ByteBuffer bb = source.slice(); trackNumber = MkvBlock.ebmlDecode(bb); int tcPart1 = bb.get() & 0xFF; int tcPart2 = bb.get() & 0xFF; timecode = (short) (((short) tcPart1 << 8) | (short) tcPart2); int flags = bb.get() & 0xFF; _keyFrame = (flags & 0x80) > 0; discardable = (flags & 0x01) > 0; int laceFlags = flags & 0x06; lacingPresent = laceFlags != 0x00; if (lacingPresent) { int lacesCount = bb.get() & 0xFF; frameSizes = new int[lacesCount + 1]; if (laceFlags == 0x02) { /* Xiph */ lacing = XIPH; headerSize = readXiphLaceSizes(bb, frameSizes, (int) this.dataLen, bb.position()); } else if (laceFlags == 0x06) { /* EBML */ lacing = EBML; headerSize = readEBMLLaceSizes(bb, frameSizes, (int) this.dataLen, bb.position()); } else if (laceFlags == 0x04) { /* Fixed Size Lacing */ this.lacing = FIXED; this.headerSize = bb.position(); int aLaceSize = (int) ((this.dataLen - this.headerSize) / (lacesCount + 1)); Arrays.fill(frameSizes, aLaceSize); } else { throw new RuntimeException("Unsupported lacing type flag."); } turnSizesToFrameOffsets(frameSizes); } else { this.lacing = ""; int frameOffset = bb.position(); frameOffsets = new int[1]; frameOffsets[0] = frameOffset; headerSize = bb.position(); frameSizes = new int[1]; frameSizes[0] = (int) (this.dataLen - headerSize); } } private void turnSizesToFrameOffsets(int[] sizes) { frameOffsets = new int[sizes.length]; frameOffsets[0] = headerSize; for (int i = 1; i < sizes.length; i++) frameOffsets[i] = frameOffsets[i - 1] + sizes[i - 1]; } public static int readXiphLaceSizes(ByteBuffer bb, int[] sizes, int size, int preLacingHeaderSize) { int startPos = bb.position(); int lastIndex = sizes.length - 1; sizes[lastIndex] = size; for (int l = 0; l < lastIndex; l++) { int laceSize = 255; while (laceSize == 255) { laceSize = bb.get() & 0xFF; sizes[l] += laceSize; } // Update the size of the last block sizes[lastIndex] -= sizes[l]; } int headerSize = (bb.position() - startPos) + preLacingHeaderSize; sizes[lastIndex] -= headerSize; return headerSize; } public static int readEBMLLaceSizes(ByteBuffer source, int[] sizes, int size, int preLacingHeaderSize) { int lastIndex = sizes.length - 1; sizes[lastIndex] = size; int startPos = source.position(); sizes[0] = (int) MkvBlock.ebmlDecode(source); sizes[lastIndex] -= sizes[0]; int laceSize = sizes[0]; long laceSizeDiff = 0; for (int l = 1; l < lastIndex; l++) { laceSizeDiff = MkvBlock.ebmlDecodeSigned(source); laceSize += laceSizeDiff; sizes[l] = laceSize; // Update the size of the last block sizes[lastIndex] -= sizes[l]; } int headerSize = (source.position() - startPos) + preLacingHeaderSize; sizes[lastIndex] -= headerSize; return headerSize; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("{dataOffset: ").append(dataOffset); sb.append(", trackNumber: ").append(trackNumber); sb.append(", timecode: ").append(timecode); sb.append(", keyFrame: ").append(_keyFrame); sb.append(", headerSize: ").append(headerSize); sb.append(", lacing: ").append(lacing); for (int i = 0; i < frameSizes.length; i++) sb.append(", frame[").append(i).append("] offset ").append(frameOffsets[i]).append(" size ").append(frameSizes[i]); sb.append(" }"); return sb.toString(); } public ByteBuffer[] getFrames(ByteBuffer source) throws IOException { ByteBuffer[] frames = new ByteBuffer[frameSizes.length]; for (int i = 0; i < frameSizes.length; i++) { if (frameOffsets[i] > source.limit()) System.err.println("frame offset: " + frameOffsets[i] + " limit: " + source.limit()); source.position(frameOffsets[i]); ByteBuffer bb = source.slice(); bb.limit(frameSizes[i]); frames[i] = bb; } return frames; } public void readFrames(ByteBuffer source) throws IOException { this.frames = getFrames(source); } // @Override public ByteBuffer getData() { int dataSize = (int) getDataSize(); ByteBuffer bb = ByteBuffer.allocate(dataSize + EbmlUtil.ebmlLength(dataSize) + id.length); bb.put(id); bb.put(EbmlUtil.ebmlEncode(dataSize)); bb.put(EbmlUtil.ebmlEncode(trackNumber)); bb.put((byte) ((timecode >>> 8) & 0xFF)); bb.put((byte) (timecode & 0xFF)); byte flags = 0x00; if (XIPH.equals(lacing)) { flags = 0x02; } else if (EBML.equals(lacing)) { flags = 0x06; } else if (FIXED.equals(lacing)) { flags = 0x04; } if (discardable) flags |= 0x01; if (_keyFrame) flags |= 0x80; bb.put(flags); if ((flags & 0x06) != 0) { bb.put((byte) ((frames.length - 1) & 0xFF)); bb.put(muxLacingInfo()); } for (int i = 0; i < frames.length; i++) { ByteBuffer frame = frames[i]; bb.put(frame); } bb.flip(); return bb; } public void seekAndReadContent(FileChannel source) throws IOException { data = ByteBuffer.allocate((int) dataLen); source.position(dataOffset); source.read(data); this.data.flip(); } /** * Get the total size of this element */ @Override public long size() { long size = getDataSize(); size += EbmlUtil.ebmlLength(size); size += id.length; return size; } public int getDataSize() { int size = 0; // TODO: one can do same calculation with for(byte[] aFrame : this.frames) size += aFrame.length; for (long fsize : frameSizes) size += fsize; if (lacingPresent) { size += muxLacingInfo().length; size += 1; // int8 laces count, a.k.a. frame_count-1 } size += 3; // int8 - flags; sint16 - timecode size += EbmlUtil.ebmlLength(trackNumber); return size; } private byte[] muxLacingInfo() { if (EBML.equals(lacing)) return muxEbmlLacing(frameSizes); if (XIPH.equals(lacing)) return muxXiphLacing(frameSizes); if (FIXED.equals(lacing)) return new byte[0]; return null; } static public long ebmlDecode(ByteBuffer bb) { byte firstByte = bb.get(); int length = EbmlUtil.computeLength(firstByte); if (length == 0) throw new RuntimeException("Invalid ebml integer size."); long value = firstByte & (0xFF >>> length); length--; while (length > 0) { value = (value << 8) | (bb.get() & 0xff); length--; } return value; } static public long ebmlDecodeSigned(ByteBuffer source) { byte firstByte = source.get(); int size = EbmlUtil.computeLength(firstByte); if (size == 0) throw new RuntimeException("Invalid ebml integer size."); long value = firstByte & (0xFF >>> size); int remaining = size-1; while (remaining > 0){ value = (value << 8) | (source.get() & 0xff); remaining--; } return value - signedComplement[size]; } public static long[] calcEbmlLacingDiffs(int[] laceSizes) { int lacesCount = laceSizes.length - 1; long[] out = new long[lacesCount]; out[0] = (int) laceSizes[0]; for (int i = 1; i < lacesCount; i++) { out[i] = laceSizes[i] - laceSizes[i - 1]; } return out; } public static byte[] muxEbmlLacing(int[] laceSizes) { ByteArrayList bytes = ByteArrayList.createByteArrayList(); long[] laceSizeDiffs = calcEbmlLacingDiffs(laceSizes); bytes.addAll(EbmlUtil.ebmlEncode(laceSizeDiffs[0])); for (int i = 1; i < laceSizeDiffs.length; i++) { bytes.addAll(convertToBytes(laceSizeDiffs[i])); } return bytes.toArray(); } public static byte[] muxXiphLacing(int[] laceSizes) { ByteArrayList bytes = ByteArrayList.createByteArrayList(); for (int i = 0; i < laceSizes.length - 1; i++) { long laceSize = laceSizes[i]; while (laceSize >= 255) { bytes.add((byte) 255); laceSize -= 255; } bytes.add((byte) laceSize); } return bytes.toArray(); } }