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();
}
}