package org.jcodec.codecs.prores; import static java.lang.Math.min; import static java.util.Arrays.fill; import static org.jcodec.codecs.prores.ProresConsts.dcCodebooks; import static org.jcodec.codecs.prores.ProresConsts.firstDCCodebook; import static org.jcodec.codecs.prores.ProresConsts.interlaced_scan; import static org.jcodec.codecs.prores.ProresConsts.levCodebooks; import static org.jcodec.codecs.prores.ProresConsts.progressive_scan; import static org.jcodec.codecs.prores.ProresConsts.runCodebooks; import static org.jcodec.common.dct.SimpleIDCT10Bit.idct10; import static org.jcodec.common.tools.MathUtil.log2; import static org.jcodec.common.tools.MathUtil.toSigned; import java.nio.ByteBuffer; import org.jcodec.codecs.prores.ProresConsts.FrameHeader; import org.jcodec.codecs.prores.ProresConsts.PictureHeader; import org.jcodec.common.VideoCodecMeta; import org.jcodec.common.VideoDecoder; import org.jcodec.common.io.BitReader; import org.jcodec.common.io.NIOUtils; import org.jcodec.common.logging.Logger; import org.jcodec.common.model.ColorSpace; import org.jcodec.common.model.Picture8Bit; import org.jcodec.common.model.Rect; import org.jcodec.common.model.Size; import org.jcodec.platform.Platform; /** * * This class is part of JCodec ( www.jcodec.org ) This software is distributed * under FreeBSD License * * Decoder for Apple ProRes format * * As posted at http://git.videolan.org/?p=ffmpeg.git;a=commitdiff;h=5554d * e13b29b9bb812ee5cfd606349873ddf0945 * * @author The JCodec project * */ public class ProresDecoder extends VideoDecoder { public ProresDecoder() { } public static ProresDecoder createProresDecoder(int downscale) { if (2 == downscale) { return new ProresToThumb4x4(); } else if (4 == downscale) { return new ProresToThumb2x2(); } else if (8 == downscale) { return new ProresToThumb(); } else { return new ProresDecoder(); } } static final int[] table = new int[] { 8, 7, 6, 6, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; static final int[] mask = new int[] { 0, 0, 0, 0, 0, 0, 0, 0, -1 }; public static final int nZeros(int check16Bit) { int low = table[check16Bit & 0xff]; check16Bit >>= 8; int high = table[check16Bit]; return high + (mask[high] & low); } public static final int readCodeword(BitReader reader, Codebook codebook) { int q = nZeros(reader.check16Bits()); reader.skipFast(q + 1); if (q > codebook.switchBits) { int bits = codebook.golombBits + q; if (bits > 16) Logger.error("Broken prores slice"); return ((1 << bits) | reader.readFast16(bits)) - codebook.golombOffset; } else if (codebook.riceOrder > 0) return (q << codebook.riceOrder) | reader.readFast16(codebook.riceOrder); else return q; } public static final int golumbToSigned(int val) { return (val >> 1) ^ golumbSign(val); } public static final int golumbSign(int val) { return -(val & 1); } public static final void readDCCoeffs(BitReader bits, int[] qMat, int[] out, int blocksPerSlice, int blkSize) { int c = readCodeword(bits, firstDCCodebook); if (c < 0) { return; // throw new RuntimeException("DC tex damaged"); } int prevDc = golumbToSigned(c); out[0] = 4096 + qScale(qMat, 0, prevDc); int code = 5, sign = 0, idx = blkSize; for (int i = 1; i < blocksPerSlice; i++, idx += blkSize) { code = readCodeword(bits, dcCodebooks[min(code, 6)]); if (code < 0) { return; // throw new RuntimeException("DC tex damaged"); } if (code != 0) sign ^= golumbSign(code); else sign = 0; prevDc += toSigned((code + 1) >> 1, sign); out[idx] = 4096 + qScale(qMat, 0, prevDc); } } protected static final void readACCoeffs(BitReader bits, int[] qMat, int[] out, int blocksPerSlice, int[] scan, int max, int log2blkSize) { int run = 4; int level = 2; int blockMask = blocksPerSlice - 1; // since blocksPerSlice is 1 << n int log2BlocksPerSlice = log2(blocksPerSlice); int maxCoeffs = 64 << log2BlocksPerSlice; int pos = blockMask; while (bits.remaining() > 32 || bits.checkAllBits() != 0) { run = readCodeword(bits, runCodebooks[min(run, 15)]); if (run < 0 || run >= maxCoeffs - pos - 1) { return; // throw new RuntimeException("AC tex damaged, RUN"); } pos += run + 1; level = readCodeword(bits, levCodebooks[min(level, 9)]) + 1; if (level < 0 || level > 65535) { return; // throw new RuntimeException("DC tex damaged, LEV"); } int sign = -bits.read1Bit(); int ind = pos >> log2BlocksPerSlice; if (ind >= max) break; out[((pos & blockMask) << log2blkSize) + scan[ind]] = qScale(qMat, ind, toSigned(level, sign)); } } private static final int qScale(int[] qMat, int ind, int val) { return ((val * qMat[ind]) >> 2); } protected void decodeOnePlane(BitReader bits, int blocksPerSlice, int[] out, int[] qMat, int[] scan, int mbX, int mbY, int plane) { try { readDCCoeffs(bits, qMat, out, blocksPerSlice, 64); readACCoeffs(bits, qMat, out, blocksPerSlice, scan, 64, 6); } catch (RuntimeException e) { System.err.println("Suppressing slice error at [" + mbX + ", " + mbY + "]."); } for (int i = 0; i < blocksPerSlice; i++) { idct10(out, i << 6); } } public Picture8Bit decodeFrame8Bit(ByteBuffer data, byte[][] target) { FrameHeader fh = readFrameHeader(data); int codedWidth = (fh.width + 15) & ~0xf; int codedHeight = (fh.height + 15) & ~0xf; int lumaSize = codedWidth * codedHeight; int chromaSize = lumaSize >> (3 - fh.chromaType); if (target == null || target[0].length < lumaSize || target[1].length < chromaSize || target[2].length < chromaSize) { throw new RuntimeException("Provided output picture won't fit into provided buffer"); } if (fh.frameType == 0) { decodePicture(data, target, fh.width, fh.height, codedWidth >> 4, fh.qMatLuma, fh.qMatChroma, fh.scan, 0, fh.chromaType); } else { decodePicture(data, target, fh.width, fh.height >> 1, codedWidth >> 4, fh.qMatLuma, fh.qMatChroma, fh.scan, fh.topFieldFirst ? 1 : 2, fh.chromaType); decodePicture(data, target, fh.width, fh.height >> 1, codedWidth >> 4, fh.qMatLuma, fh.qMatChroma, fh.scan, fh.topFieldFirst ? 2 : 1, fh.chromaType); } return new Picture8Bit(codedWidth, codedHeight, target, fh.chromaType == 2 ? ColorSpace.YUV422 : ColorSpace.YUV444, new Rect(0, 0, fh.width, fh.height)); } public Picture8Bit[] decodeFields8Bit(ByteBuffer data, byte[][][] target) { FrameHeader fh = readFrameHeader(data); int codedWidth = (fh.width + 15) & ~0xf; int codedHeight = (fh.height + 15) & ~0xf; int lumaSize = codedWidth * codedHeight; int chromaSize = lumaSize >> 1; if (fh.frameType == 0) { if (target == null || target[0][0].length < lumaSize || target[0][1].length < chromaSize || target[0][2].length < chromaSize) { throw new RuntimeException("Provided output picture won't fit into provided buffer"); } decodePicture(data, target[0], fh.width, fh.height, codedWidth >> 4, fh.qMatLuma, fh.qMatChroma, fh.scan, 0, fh.chromaType); return new Picture8Bit[] { Picture8Bit.createPicture8Bit(codedWidth, codedHeight, target[0], ColorSpace.YUV422) }; } else { lumaSize >>= 1; chromaSize >>= 1; if (target == null || target[0][0].length < lumaSize || target[0][1].length < chromaSize || target[0][2].length < chromaSize || target[1][0].length < lumaSize || target[1][1].length < chromaSize || target[1][2].length < chromaSize) { throw new RuntimeException("Provided output picture won't fit into provided buffer"); } decodePicture(data, target[fh.topFieldFirst ? 0 : 1], fh.width, fh.height >> 1, codedWidth >> 4, fh.qMatLuma, fh.qMatChroma, fh.scan, 0, fh.chromaType); decodePicture(data, target[fh.topFieldFirst ? 1 : 0], fh.width, fh.height >> 1, codedWidth >> 4, fh.qMatLuma, fh.qMatChroma, fh.scan, 0, fh.chromaType); return new Picture8Bit[] { Picture8Bit.createPicture8Bit(codedWidth, codedHeight >> 1, target[0], ColorSpace.YUV422), Picture8Bit.createPicture8Bit(codedWidth, codedHeight >> 1, target[1], ColorSpace.YUV422) }; } } public static FrameHeader readFrameHeader(ByteBuffer inp) { int frameSize = inp.getInt(); String sig = readSig(inp); if (!"icpf".equals(sig)) throw new RuntimeException("Not a prores frame"); short hdrSize = inp.getShort(); short version = inp.getShort(); int res1 = inp.getInt(); short width = inp.getShort(); short height = inp.getShort(); int flags1 = inp.get(); int frameType = (flags1 >> 2) & 3; int chromaType = (flags1 >> 6) & 3; int[] scan; boolean topFieldFirst = false; if (frameType == 0) { scan = progressive_scan; } else { scan = interlaced_scan; if (frameType == 1) topFieldFirst = true; } byte res2 = inp.get(); byte prim = inp.get(); byte transFunc = inp.get(); byte matrix = inp.get(); byte pixFmt = inp.get(); byte res3 = inp.get(); int flags2 = inp.get() & 0xff; int[] qMatLuma = new int[64]; int[] qMatChroma = new int[64]; if (hasQMatLuma(flags2)) { readQMat(inp, qMatLuma, scan); } else { fill(qMatLuma, 4); } if (hasQMatChroma(flags2)) { readQMat(inp, qMatChroma, scan); } else { fill(qMatChroma, 4); } inp.position( inp.position() + hdrSize - (20 + (hasQMatLuma(flags2) ? 64 : 0) + (hasQMatChroma(flags2) ? 64 : 0))); return new FrameHeader(frameSize - hdrSize - 8, width, height, frameType, topFieldFirst, scan, qMatLuma, qMatChroma, chromaType); } static final String readSig(ByteBuffer inp) { byte[] sig = new byte[4]; inp.get(sig); return Platform.stringFromBytes(sig); } protected void decodePicture(ByteBuffer data, byte[][] result, int width, int height, int mbWidth, int[] qMatLuma, int[] qMatChroma, int[] scan, int pictureType, int chromaType) { ProresConsts.PictureHeader ph = readPictureHeader(data); int mbX = 0, mbY = 0; int sliceMbCount = 1 << ph.log2SliceMbWidth; for (int i = 0; i < ph.sliceSizes.length; i++) { while (mbWidth - mbX < sliceMbCount) sliceMbCount >>= 1; decodeSlice(NIOUtils.read(data, ph.sliceSizes[i]), qMatLuma, qMatChroma, scan, sliceMbCount, mbX, mbY, ph.sliceSizes[i], result, width, pictureType, chromaType); mbX += sliceMbCount; if (mbX == mbWidth) { sliceMbCount = 1 << ph.log2SliceMbWidth; mbX = 0; mbY++; } } } public static PictureHeader readPictureHeader(ByteBuffer inp) { int hdrSize = (inp.get() & 0xff) >> 3; inp.getInt(); int sliceCount = inp.getShort(); int a = inp.get() & 0xff; int log2SliceMbWidth = a >> 4; short[] sliceSizes = new short[sliceCount]; for (int i = 0; i < sliceCount; i++) { sliceSizes[i] = inp.getShort(); } return new PictureHeader(log2SliceMbWidth, sliceSizes); } private void decodeSlice(ByteBuffer data, int[] qMatLuma, int[] qMatChroma, int[] scan, int sliceMbCount, int mbX, int mbY, short sliceSize, byte[][] result, int lumaStride, int pictureType, int chromaType) { int hdrSize = (data.get() & 0xff) >> 3; int qScale = clip(data.get() & 0xff, 1, 224); qScale = qScale > 128 ? qScale - 96 << 2 : qScale; int yDataSize = data.getShort(); int uDataSize = data.getShort(); int vDataSize = hdrSize > 7 ? data.getShort() : sliceSize - uDataSize - yDataSize - hdrSize; int[] y = new int[sliceMbCount << 8]; decodeOnePlane(bitstream(data, yDataSize), sliceMbCount << 2, y, scaleMat(qMatLuma, qScale), scan, mbX, mbY, 0); int chromaBlkCount = (sliceMbCount << chromaType) >> 1; int[] u = new int[chromaBlkCount << 6]; decodeOnePlane(bitstream(data, uDataSize), chromaBlkCount, u, scaleMat(qMatChroma, qScale), scan, mbX, mbY, 1); int[] v = new int[chromaBlkCount << 6]; decodeOnePlane(bitstream(data, vDataSize), chromaBlkCount, v, scaleMat(qMatChroma, qScale), scan, mbX, mbY, 2); putSlice(result, lumaStride, mbX, mbY, y, u, v, pictureType == 0 ? 0 : 1, pictureType == 2 ? 1 : 0, chromaType, sliceMbCount); } public static final int[] scaleMat(int[] qMatLuma, int qScale) { int[] res = new int[qMatLuma.length]; for (int i = 0; i < qMatLuma.length; i++) res[i] = qMatLuma[i] * qScale; return res; } static final BitReader bitstream(ByteBuffer data, int dataSize) { return BitReader.createBitReader(NIOUtils.read(data, dataSize)); } byte clipTo8Bit(int val, int min, int max) { return (byte) (((val < min ? min : (val > max ? max : val)) >> 2) - 128); } static final int clip(int val, int min, int max) { return val < min ? min : (val > max ? max : val); } protected void putSlice(byte[][] result, int lumaStride, int mbX, int mbY, int[] y, int[] u, int[] v, int dist, int shift, int chromaType, int sliceMbCount) { int chromaStride = lumaStride >> 1; putLuma(result[0], shift * lumaStride, lumaStride << dist, mbX, mbY, y, sliceMbCount, dist, shift); if (chromaType == 2) { putChroma(result[1], shift * chromaStride, chromaStride << dist, mbX, mbY, u, sliceMbCount, dist, shift); putChroma(result[2], shift * chromaStride, chromaStride << dist, mbX, mbY, v, sliceMbCount, dist, shift); } else { putLuma(result[1], shift * lumaStride, lumaStride << dist, mbX, mbY, u, sliceMbCount, dist, shift); putLuma(result[2], shift * lumaStride, lumaStride << dist, mbX, mbY, v, sliceMbCount, dist, shift); } } private void putLuma(byte[] y, int off, int stride, int mbX, int mbY, int[] luma, int mbPerSlice, int dist, int shift) { off += (mbX << 4) + (mbY << 4) * stride; for (int k = 0; k < mbPerSlice; k++) { putBlock(y, off, stride, luma, k << 8, dist, shift); putBlock(y, off + 8, stride, luma, (k << 8) + 64, dist, shift); putBlock(y, off + 8 * stride, stride, luma, (k << 8) + 128, dist, shift); putBlock(y, off + 8 * stride + 8, stride, luma, (k << 8) + 192, dist, shift); off += 16; } } private void putChroma(byte[] y, int off, int stride, int mbX, int mbY, int[] chroma, int mbPerSlice, int dist, int shift) { off += (mbX << 3) + (mbY << 4) * stride; for (int k = 0; k < mbPerSlice; k++) { putBlock(y, off, stride, chroma, k << 7, dist, shift); putBlock(y, off + 8 * stride, stride, chroma, (k << 7) + 64, dist, shift); off += 8; } } private void putBlock(byte[] square, int sqOff, int sqStride, int[] flat, int flOff, int dist, int shift) { for (int i = 0; i < 8; i++) { for (int j = 0; j < 8; j++) square[j + sqOff] = clipTo8Bit(flat[j + flOff], 4, 1019); sqOff += sqStride; flOff += 8; } } static final boolean hasQMatChroma(int flags2) { return (flags2 & 1) != 0; } static final void readQMat(ByteBuffer inp, int[] qMatLuma, int[] scan) { byte[] b = new byte[64]; inp.get(b); for (int i = 0; i < 64; i++) { qMatLuma[i] = b[scan[i]] & 0xff; } } static final boolean hasQMatLuma(int flags2) { return (flags2 & 2) != 0; } public boolean isProgressive(ByteBuffer data) { return (((data.get(20) & 0xff) >> 2) & 3) == 0; } public static int probe(ByteBuffer data) { if (data.get(4) == 'i' && data.get(5) == 'c' && data.get(6) == 'p' && data.get(7) == 'f') return 100; return 0; } @Override public VideoCodecMeta getCodecMeta(ByteBuffer data) { FrameHeader fh = readFrameHeader(data); return new VideoCodecMeta(new Size(fh.width, fh.height), ColorSpace.YUV420); } }