package org.jcodec.codecs.vpx;
import static org.jcodec.codecs.vpx.VP8Util.MAX_MODE_LF_DELTAS;
import static org.jcodec.codecs.vpx.VP8Util.MAX_REF_LF_DELTAS;
import static org.jcodec.codecs.vpx.VP8Util.getBitInBytes;
import static org.jcodec.codecs.vpx.VP8Util.getBitsInBytes;
import static org.jcodec.codecs.vpx.VP8Util.getDefaultCoefProbs;
import static org.jcodec.codecs.vpx.VP8Util.getMacroblockCount;
import static org.jcodec.codecs.vpx.VP8Util.keyFrameYModeProb;
import static org.jcodec.codecs.vpx.VP8Util.keyFrameYModeTree;
import static org.jcodec.codecs.vpx.VP8Util.vp8CoefUpdateProbs;
import java.nio.ByteBuffer;
import org.jcodec.api.NotSupportedException;
import org.jcodec.codecs.vpx.Macroblock.Subblock;
import org.jcodec.codecs.vpx.VP8Util.QuantizationParams;
import org.jcodec.codecs.vpx.VP8Util.SubblockConstants;
import org.jcodec.common.Assert;
import org.jcodec.common.VideoCodecMeta;
import org.jcodec.common.VideoDecoder;
import org.jcodec.common.io.NIOUtils;
import org.jcodec.common.model.ColorSpace;
import org.jcodec.common.model.Picture8Bit;
import org.jcodec.common.model.Size;
import org.jcodec.common.tools.MathUtil;
/**
* This class is part of JCodec ( www.jcodec.org ) This software is distributed
* under FreeBSD License
*
* @author The JCodec project
*
*/
public class VP8Decoder extends VideoDecoder {
private byte[][] segmentationMap;
private int[] refLoopFilterDeltas = new int[MAX_REF_LF_DELTAS];
private int[] modeLoopFilterDeltas = new int[MAX_MODE_LF_DELTAS];
@Override
public Picture8Bit decodeFrame8Bit(ByteBuffer frame, byte[][] buffer) {
byte[] firstThree = new byte[3];
frame.get(firstThree);
boolean keyFrame = getBitInBytes(firstThree, 0) == 0;
if(!keyFrame)
return null;
int version = getBitsInBytes(firstThree, 1, 3);
boolean showFrame = getBitInBytes(firstThree, 4) > 0;
int partitionSize = getBitsInBytes(firstThree, 5, 19);
String threeByteToken = printHexByte(frame.get()) + " " + printHexByte(frame.get()) + " "
+ printHexByte(frame.get());
int twoBytesWidth = (frame.get() & 0xFF) | (frame.get() & 0xFF) << 8;
int twoBytesHeight = (frame.get() & 0xFF) | (frame.get() & 0xFF) << 8;
int width = (twoBytesWidth & 0x3fff);
int height = (twoBytesHeight & 0x3fff);
int numberOfMBRows = getMacroblockCount(height);
int numberOfMBCols = getMacroblockCount(width);
/** Init macroblocks and subblocks */
if(segmentationMap == null)
segmentationMap = new byte[numberOfMBRows][numberOfMBCols];
Macroblock[][] mbs = new Macroblock[numberOfMBRows + 2][numberOfMBCols + 2];
for (int row = 0; row < numberOfMBRows + 2; row++) {
for (int col = 0; col < numberOfMBCols + 2; col++) {
mbs[row][col] = new Macroblock(row, col);
}
}
int headerOffset = frame.position();
VPXBooleanDecoder headerDecoder = new VPXBooleanDecoder(frame, 0);
boolean isYUVColorSpace = (headerDecoder.decodeBit() == 0);
boolean clampingRequired = headerDecoder.decodeBit() == 0;
int segmentation = headerDecoder.decodeBit();
SegmentBasedAdjustments segmentBased = null;
if(segmentation != 0) {
segmentBased = updateSegmentation(headerDecoder);
// Segmentation map persists between frames
for (int row = 0; row < numberOfMBRows; row++) {
for (int col = 0; col < numberOfMBCols; col++) {
mbs[row + 1][col + 1].segment = segmentationMap[row][col];
}
}
}
int simpleFilter = headerDecoder.decodeBit();
int filterLevel = headerDecoder.decodeInt(6);
int filterType = (filterLevel == 0) ? 0 : (simpleFilter > 0) ? 1 : 2;
int sharpnessLevel = headerDecoder.decodeInt(3);
int loopFilterDeltaFlag = headerDecoder.decodeBit();
if(loopFilterDeltaFlag == 1) {
int loopFilterDeltaUpdate = headerDecoder.decodeBit();
if (loopFilterDeltaUpdate == 1) {
for (int i = 0; i < MAX_REF_LF_DELTAS; i++) {
if (headerDecoder.decodeBit() > 0) {
refLoopFilterDeltas[i] = headerDecoder.decodeInt(6);
;
if (headerDecoder.decodeBit() > 0) // Apply sign
refLoopFilterDeltas[i] = refLoopFilterDeltas[i] * -1;
}
}
for (int i = 0; i < MAX_MODE_LF_DELTAS; i++) {
if (headerDecoder.decodeBit() > 0) {
modeLoopFilterDeltas[i] = headerDecoder.decodeInt(6);
if (headerDecoder.decodeBit() > 0) // Apply sign
modeLoopFilterDeltas[i] = modeLoopFilterDeltas[i] * -1;
}
}
}
}
int log2OfPartCnt = headerDecoder.decodeInt(2);
Assert.assertEquals(0, log2OfPartCnt);
int partitionsCount = 1;
long runningSize = 0;
long zSize = frame.limit() - (partitionSize + headerOffset);
ByteBuffer tokenBuffer = frame.duplicate();
tokenBuffer.position(partitionSize + headerOffset);
VPXBooleanDecoder decoder = new VPXBooleanDecoder(tokenBuffer, 0);
int yacIndex = headerDecoder.decodeInt(7);
int ydcDelta = ((headerDecoder.decodeBit() > 0) ? VP8Util.delta(headerDecoder) : 0);
int y2dcDelta = ((headerDecoder.decodeBit() > 0) ? VP8Util.delta(headerDecoder) : 0);
int y2acDelta = ((headerDecoder.decodeBit() > 0) ? VP8Util.delta(headerDecoder) : 0);
int chromaDCDelta = ((headerDecoder.decodeBit() > 0) ? VP8Util.delta(headerDecoder) : 0);
int chromaACDelta = ((headerDecoder.decodeBit() > 0) ? VP8Util.delta(headerDecoder) : 0);
boolean refreshProbs = headerDecoder.decodeBit() == 0;
QuantizationParams quants = new QuantizationParams(yacIndex, ydcDelta, y2dcDelta, y2acDelta, chromaDCDelta,
chromaACDelta);
int[][][][] coefProbs = getDefaultCoefProbs();
for (int i = 0; i < VP8Util.BLOCK_TYPES; i++)
for (int j = 0; j < VP8Util.COEF_BANDS; j++)
for (int k = 0; k < VP8Util.PREV_COEF_CONTEXTS; k++)
for (int l = 0; l < VP8Util.MAX_ENTROPY_TOKENS - 1; l++) {
if (headerDecoder.decodeBool(vp8CoefUpdateProbs[i][j][k][l]) > 0) {
int newp = headerDecoder.decodeInt(8);
coefProbs[i][j][k][l] = newp;
}
}
int macroBlockNoCoeffSkip = (int) headerDecoder.decodeBit();
Assert.assertEquals(1, macroBlockNoCoeffSkip);
int probSkipFalse = headerDecoder.decodeInt(8);
for (int mbRow = 0; mbRow < numberOfMBRows; mbRow++) {
for (int mbCol = 0; mbCol < numberOfMBCols; mbCol++) {
Macroblock mb = mbs[mbRow + 1][mbCol + 1];
if (segmentation != 0 && segmentBased.segmentProbs != null) {
// if segmentation is on and if segment map is updated
mb.segment = headerDecoder.readTree(VP8Util.segmentTree, segmentBased.segmentProbs);
segmentationMap[mbRow][mbCol] = (byte) mb.segment;
}
if (segmentation != 0 && segmentBased.qp != null) {
int qIndex = yacIndex;
if (segmentBased.abs != 0)
qIndex = segmentBased.qp[mb.segment];
else
qIndex += segmentBased.qp[mb.segment];
quants = new QuantizationParams(qIndex, ydcDelta, y2dcDelta, y2acDelta, chromaDCDelta,
chromaACDelta);
}
mb.quants = quants;
// Ref based filter level adjustment
if (loopFilterDeltaFlag != 0) {
int level = filterLevel;
level = level + refLoopFilterDeltas[0];
level = MathUtil.clip(level, 0, 63);
mb.filterLevel = level;
} else {
mb.filterLevel = filterLevel;
}
// Segment based filter level adjustment
if (segmentation != 0 && segmentBased.lf != null) {
if (segmentBased.abs != 0) {
mb.filterLevel = segmentBased.lf[mb.segment];
} else {
mb.filterLevel += segmentBased.lf[mb.segment];
mb.filterLevel = MathUtil.clip(mb.filterLevel, 0, 63);
}
}
if (macroBlockNoCoeffSkip > 0)
mb.skipCoeff = headerDecoder.decodeBool(probSkipFalse);
mb.lumaMode = headerDecoder.readTree(keyFrameYModeTree, keyFrameYModeProb);
// 1 is added to account for non-displayed framing macroblocks,
// which are used for prediction only.
if (mb.lumaMode == SubblockConstants.B_PRED) {
for (int sbRow = 0; sbRow < 4; sbRow++) {
for (int sbCol = 0; sbCol < 4; sbCol++) {
Subblock sb = mb.ySubblocks[sbRow][sbCol];
Subblock A = sb.getAbove(VP8Util.PLANE.Y1, mbs);
Subblock L = sb.getLeft(VP8Util.PLANE.Y1, mbs);
sb.mode = headerDecoder.readTree(SubblockConstants.subblockModeTree,
SubblockConstants.keyFrameSubblockModeProb[A.mode][L.mode]);
}
}
} else {
int fixedMode;
switch (mb.lumaMode) {
case SubblockConstants.DC_PRED:
fixedMode = SubblockConstants.B_DC_PRED;
break;
case SubblockConstants.V_PRED:
fixedMode = SubblockConstants.B_VE_PRED;
break;
case SubblockConstants.H_PRED:
fixedMode = SubblockConstants.B_HE_PRED;
break;
case SubblockConstants.TM_PRED:
fixedMode = SubblockConstants.B_TM_PRED;
break;
default:
fixedMode = SubblockConstants.B_DC_PRED;
break;
}
mb.lumaMode = edgeEmu(mb.lumaMode, mbCol, mbRow);
for (int x = 0; x < 4; x++)
for (int y = 0; y < 4; y++)
mb.ySubblocks[y][x].mode = fixedMode;
}
mb.chromaMode = headerDecoder.readTree(VP8Util.vp8UVModeTree, VP8Util.vp8KeyFrameUVModeProb);
}
}
for (int mbRow = 0; mbRow < numberOfMBRows; mbRow++) {
for (int mbCol = 0; mbCol < numberOfMBCols; mbCol++) {
Macroblock mb = mbs[mbRow + 1][mbCol + 1];
mb.decodeMacroBlock(mbs, decoder, coefProbs);
mb.dequantMacroBlock(mbs);
}
}
if (filterType > 0 && filterLevel != 0) {
if (filterType == 2) {
FilterUtil.loopFilterUV(mbs, sharpnessLevel, keyFrame);
FilterUtil.loopFilterY(mbs, sharpnessLevel, keyFrame);
} else if (filterType == 1) {
// loopFilterSimple(frame);
}
}
Picture8Bit p = Picture8Bit.createPicture8Bit(width, height, buffer, ColorSpace.YUV420);
int mbWidth = getMacroblockCount(width);
int mbHeight = getMacroblockCount(height);
for (int mbRow = 0; mbRow < mbHeight; mbRow++) {
for (int mbCol = 0; mbCol < mbWidth; mbCol++) {
Macroblock mb = mbs[mbRow + 1][mbCol + 1];
mb.put(mbRow, mbCol, p);
}
}
return p;
}
private int edgeEmu(int mode, int mbCol, int mbRow) {
switch (mode) {
case SubblockConstants.V_PRED:
return mbRow == 0 ? SubblockConstants.DC_PRED : mode;
case SubblockConstants.H_PRED:
return mbCol == 0 ? SubblockConstants.DC_PRED : mode;
case SubblockConstants.TM_PRED:
return edgeEmuTm(mode, mbCol, mbRow);
default:
return mode;
}
}
private int edgeEmuTm(int mode, int mbCol, int mbRow) {
if (mbCol == 0)
return mbRow != 0 ? SubblockConstants.V_PRED : SubblockConstants.DC_PRED;
else
return mbRow != 0 ? mode : SubblockConstants.H_PRED;
}
private static class SegmentBasedAdjustments {
private int[] segmentProbs;
private int[] qp;
private int[] lf;
private int abs;
public SegmentBasedAdjustments(int[] segmentProbs, int[] qp, int[] lf, int abs) {
this.segmentProbs = segmentProbs;
this.qp = qp;
this.lf = lf;
this.abs = abs;
}
}
private SegmentBasedAdjustments updateSegmentation(VPXBooleanDecoder headerDecoder) {
int updateMBSegmentationMap = headerDecoder.decodeBit();
int updateSegmentFeatureData = headerDecoder.decodeBit();
int[] qp = null;
int[] lf = null;
int abs = 0;
if (updateSegmentFeatureData != 0) {
qp = new int[4];
lf = new int[4];
abs = headerDecoder.decodeBit();
for (int i = 0; i < 4; i++) {
int quantizerUpdate = headerDecoder.decodeBit();
if (quantizerUpdate != 0) {
qp[i] = headerDecoder.decodeInt(7);
qp[i] = headerDecoder.decodeBit() != 0 ? -qp[i] : qp[i];
}
}
for (int i = 0; i < 4; i++) {
int loopFilterUpdate = headerDecoder.decodeBit();
if (loopFilterUpdate != 0) {
lf[i] = headerDecoder.decodeInt(6);
lf[i] = headerDecoder.decodeBit() != 0 ? -lf[i] : lf[i];
}
}
}
int[] segmentProbs = new int[3];
if (updateMBSegmentationMap != 0) {
for (int i = 0; i < 3; i++) {
int segmentProbUpdate = headerDecoder.decodeBit();
if (segmentProbUpdate != 0)
segmentProbs[i] = headerDecoder.decodeInt(8);
else
segmentProbs[i] = 255;
}
}
return new SegmentBasedAdjustments(segmentProbs, qp, lf, abs);
}
public static int probe(ByteBuffer data) {
if ((data.get(3) & 0xff) == 0x9d && (data.get(4) & 0xff) == 0x1 && (data.get(5) & 0xff) == 0x2a)
return 100;
return 0;
}
public static String printHexByte(byte b) {
return "0x" + Integer.toHexString(b & 0xFF);
}
@Override
public VideoCodecMeta getCodecMeta(ByteBuffer frame) {
NIOUtils.skip(frame, 6);
int twoBytesWidth = (frame.get() & 0xFF) | (frame.get() & 0xFF) << 8;
int twoBytesHeight = (frame.get() & 0xFF) | (frame.get() & 0xFF) << 8;
int width = (twoBytesWidth & 0x3fff);
int height = (twoBytesHeight & 0x3fff);
return new VideoCodecMeta(new Size(width, height), ColorSpace.YUV420);
}
}