package org.jcodec.containers.mkv.elements; import com.chiorichan.Loader; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.util.Arrays; import org.jcodec.common.ByteArrayList; import org.jcodec.containers.mkv.Reader; import org.jcodec.containers.mkv.Type; import org.jcodec.containers.mkv.ebml.BinaryElement; import org.jcodec.containers.mkv.ebml.Element; import static org.jcodec.containers.mkv.ebml.SignedIntegerElement.convertToBytes; public class BlockElement extends BinaryElement { 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 long[] frameOffsets; public long[] 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; private byte[][] frames; public static BlockElement copy( BlockElement old ) { BlockElement be = new BlockElement( 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 long[old.frameOffsets.length]; be.frameSizes = new long[old.frameSizes.length]; be.dataOffset = old.dataOffset; be.offset = old.offset; be.type = old.type; System.arraycopy( old.frameOffsets, 0, be.frameOffsets, 0, be.frameOffsets.length ); System.arraycopy( old.frameSizes, 0, be.frameSizes, 0, be.frameSizes.length ); return be; } public static BlockElement keyFrame( long trackNumber, int timecode, byte[] frame ) { BlockElement be = new BlockElement( Type.SimpleBlock.id ); be.frames = new byte[][] { frame }; be.frameSizes = new long[] { frame.length }; be.keyFrame = true; be.trackNumber = trackNumber; be.timecode = timecode; return be; } public BlockElement( byte[] type ) { super( type ); if ( !Arrays.equals( Type.SimpleBlock.id, type ) && !Arrays.equals( Type.Block.id, type ) ) throw new IllegalArgumentException( "Block initiated with invalid id: " + Reader.printAsHex( type ) ); } @Override public void readData( FileChannel source ) throws IOException { // dataOffset = source.position(); // Read enough for the header to fit in ByteBuffer bb = ByteBuffer.allocate( (int) (MAX_BLOCK_HEADER_SIZE > size ? size : MAX_BLOCK_HEADER_SIZE) ); source.read( bb ); // Skip the rest of the block source.position( dataOffset + size ); bb.flip(); trackNumber = Reader.getEbmlVInt( bb ); // Read timecode int blockTimecode1 = bb.get() & 0xFF; int blockTimecode2 = bb.get() & 0xFF; short tc = (short) (((short) blockTimecode1 << 8) | (short) blockTimecode2); timecode = tc;// (blockTimecode1 << 8) | blockTimecode2; int flags = bb.get() & 0xFF; keyFrame = (flags & 0x80) > 0; discardable = (flags & 0x01) > 0; int laceFlags = flags & 0x06; // Increase the HeaderSize by the number of bytes we have read lacingPresent = laceFlags != 0x00; if ( lacingPresent ) { // We have lacing int lacesCount = bb.get() & 0xFF; frameSizes = new long[lacesCount + 1]; // HeaderSize += 1; if ( laceFlags == 0x02 ) { // Xiph Lacing lacing = XIPH; headerSize = readXiphLaceSizes( bb, frameSizes, (int) this.size, bb.position() ); } else if ( laceFlags == 0x06 ) { // EBML Lacing lacing = EBML; headerSize = readEBMLLaceSizes( bb, frameSizes, (int) this.size, bb.position() ); } else if ( laceFlags == 0x04 ) { // Fixed Size Lacing this.lacing = FIXED; this.headerSize = bb.position(); int aLaceSize = (int) ((this.size - this.headerSize) / (lacesCount + 1)); Arrays.fill( frameSizes, aLaceSize ); } else throw new RuntimeException( "Unsupported lacing type flag." ); turnSizesToFrameOffsets( frameSizes ); } else { this.lacing = ""; long frameOffset = dataOffset + bb.position(); frameOffsets = new long[1]; frameOffsets[0] = frameOffset; headerSize = bb.position(); // (int) (frameOffset - dataOffset); frameSizes = new long[1]; frameSizes[0] = this.size - headerSize; } } private void turnSizesToFrameOffsets( long[] sizes ) { frameOffsets = new long[sizes.length]; frameOffsets[0] = dataOffset + headerSize; for ( int i = 1; i < sizes.length; i++ ) { frameOffsets[i] = frameOffsets[i - 1] + sizes[i - 1]; } } public static int readXiphLaceSizes( ByteBuffer bb, long[] 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, long[] sizes, int size, int preLacingHeaderSize ) { int lastIndex = sizes.length - 1; sizes[lastIndex] = size; int startPos = source.position(); sizes[0] = Reader.getEbmlVInt( source ); sizes[lastIndex] -= sizes[0]; long laceSize = sizes[0]; long laceSizeDiff = 0; for ( int l = 1; l < lastIndex; l++ ) { laceSizeDiff = Reader.getSignedEbmlVInt( 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 byte[][] getFrames( FileChannel source ) throws IOException { byte[][] frames = new byte[frameSizes.length][]; for ( int i = 0; i < frameSizes.length; i++ ) { ByteBuffer bb = ByteBuffer.allocate( (int) frameSizes[i] ); source.position( frameOffsets[i] ); source.read( bb ); bb.flip(); frames[i] = bb.array(); } return frames; } public void readFrames( FileChannel source ) throws IOException { this.frames = getFrames( source ); } @Override public ByteBuffer mux() { int dataSize = (int) getDataSize(); ByteBuffer bb = ByteBuffer.allocate( dataSize + getEbmlSize( dataSize ) + id.length ); bb.put( id ); bb.put( ebmlBytes( dataSize ) ); bb.put( ebmlBytes( 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 ( byte[] frame : frames ) { bb.put( frame ); } bb.flip(); //muxTesting(); return bb; } public ByteBuffer muxTesting() { int dataSize = (int) getDataSize(); ByteBuffer bb = ByteBuffer.allocate( dataSize + getEbmlSize( dataSize ) + id.length + 24 ); bb.put( id ); bb.put( ebmlBytes( dataSize ) ); bb.put( ebmlBytes( trackNumber ) ); int bits = Float.floatToIntBits( timecode ); Loader.getLogger().info( "Float: " + timecode + ", Bits: " + bits + ", " + (bits & 0xFF) + " " + ((bits >>> 8) & 0xFF) + " " + ((bits >>> 16) & 0xFF) + " " + ((bits >>> 24) & 0xFF) ); bb.put( (byte) ((bits >>> 24) & 0xFF) ); bb.put( (byte) ((bits >>> 16) & 0xFF) ); bb.put( (byte) ((bits >>> 8) & 0xFF) ); bb.put( (byte) (bits & 0xFF) ); // TODO: This might be a problem! 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 ( byte[] frame : frames ) { bb.put( frame ); } bb.flip(); return bb; } private static final String HEX_DIGITS = "0123456789abcdef"; public static String toHex( byte[] data ) { StringBuffer buf = new StringBuffer(); for ( int i = 0; i != data.length; i++ ) { int v = data[i] & 0xff; buf.append( HEX_DIGITS.charAt( v >> 4 ) ); buf.append( HEX_DIGITS.charAt( v & 0xf ) ); buf.append( " " ); } return buf.toString(); } public void seekAndReadContent( FileChannel source ) throws IOException { data = ByteBuffer.allocate( (int) size ); source.position( dataOffset ); source.read( data ); this.data.flip(); } /** * Get the total size of this element */ @Override public long getSize() { long size = getDataSize(); size += getEbmlSize( size ); size += id.length; return size; } public long getDataSize() { long 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 += getEbmlSize( 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; } public static long[] calcEbmlLacingDiffs( long[] 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( long[] laceSizes ) { ByteArrayList bytes = new ByteArrayList(); long[] laceSizeDiffs = calcEbmlLacingDiffs( laceSizes ); bytes.addAll( Element.ebmlBytes( laceSizeDiffs[0] ) ); for ( int i = 1; i < laceSizeDiffs.length; i++ ) { bytes.addAll( convertToBytes( laceSizeDiffs[i] ) ); } return bytes.toArray(); } public static byte[] muxXiphLacing( long[] laceSizes ) { ByteArrayList bytes = new ByteArrayList(); 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(); } }