noVNC/core/ast2100/ast2100.js

505 lines
21 KiB
JavaScript

"use strict";
/*global DCTSIZE, DCTSIZE2, JpegHuffmanTable, BitStream, AST2100IDCT, AAN_IDCT_SCALING_FACTORS, ZIGZAG_ORDER */
/*global ATEN_QT_LUMA, ATEN_QT_CHROMA, TABLE_CLASS_AC, TABLE_CLASS_DC */
/*global BITS_AC_LUMA, BITS_AC_CHROMA, BITS_DC_LUMA, BITS_DC_CHROMA, HUFFVAL_AC_LUMA, HUFFVAL_AC_CHROMA, HUFFVAL_DC_LUMA, HUFFVAL_DC_CHROMA */
/*global inRangeIncl, isEmpty */
/*global fmt_u8a, fmt_u8, fmt_u16 */
/*
(c) Copyright 2015-2017 Kevin Kelley <kelleyk@kelleyk.net>.
This implementation is the product of clean-room reverse engineering (that is, I
am not and have never been subject to enondisclosure agreements, nor have I had
access to proprietary information, related to the subject matter of this
project).
*/
var verboseDebug = false;
var verboseMcuCount = false;
var traceUpdates = false;
var verboseStats = false;
var verboseVideoSettings = true;
var Ast2100Decoder;
(function () {
"use strict";
Ast2100Decoder = function (defaults) {
this._blitCallback = defaults.blitCallback;
this._videoSettingsChangedCallback = defaults.videoSettingsChangedCallback;
this._frame_width = defaults.width;
this._frame_height = defaults.height;
if (!this._frame_width || !this._frame_height)
throw 'Missing required parameter: width, height';
// Either 444u or 422u (though the "422" mode is really 4:2:0, where
// chroma is subsampled by a factor of two in each direction).. Applies
// only to JPEG-ish blocks, not to VQ blocks; but VQ blocks seem to only
// appear in 4:4:4 color mode.
this.subsamplingMode = -1;
this.quantTables = [new Int32Array(64), new Int32Array(64)];
this._loadedQuantTables = [-1, -1];
this.huffTables = [
[new JpegHuffmanTable({bits: BITS_DC_LUMA, huffval: HUFFVAL_DC_LUMA}),
new JpegHuffmanTable({bits: BITS_DC_CHROMA, huffval: HUFFVAL_DC_CHROMA})],
[new JpegHuffmanTable({bits: BITS_AC_LUMA, huffval: HUFFVAL_AC_LUMA}),
new JpegHuffmanTable({bits: BITS_AC_CHROMA, huffval: HUFFVAL_AC_CHROMA})],
];
this._scan_components = [
{huffTableSelectorDC: 0, huffTableSelectorAC: 0}, // Y
{huffTableSelectorDC: 1, huffTableSelectorAC: 1}, // Cb
{huffTableSelectorDC: 1, huffTableSelectorAC: 1} // Cr
];
this._scan_prev_dc = [0, 0, 0];
this._mcuPosX = 0;
this._mcuPosY = 0;
this._initializeVq();
// Allocate memory here once and then re-use it. These buffers store
// data after entropy decoding (Huffman, zigzag) and before we dequant
// and apply the IDCT.
this._tmpBufY = [
new Int16Array(DCTSIZE2),
new Int16Array(DCTSIZE2),
new Int16Array(DCTSIZE2),
new Int16Array(DCTSIZE2)];
this._tmpBufCb = new Int16Array(DCTSIZE2);
this._tmpBufCr = new Int16Array(DCTSIZE2);
// These buffers store IDCT output.
this._componentBufY = [
new Uint8Array(DCTSIZE2),
new Uint8Array(DCTSIZE2),
new Uint8Array(DCTSIZE2),
new Uint8Array(DCTSIZE2)];
this._componentBufCb = new Uint8Array(DCTSIZE2);
this._componentBufCr = new Uint8Array(DCTSIZE2);
// The final RGB output goes here; noVNC expects each group of four
// elements to represent one RGBA pixel. this._outputBuf = new
// Uint8Array(DCTSIZE2 * 4);
this._outputBuf = new Uint8Array(DCTSIZE2 * 4);
};
Ast2100Decoder.prototype = {
// TODO: Could/should remove this indirection.
_idct: function (quant_table, data_unit, dstBuf) {
if (!quant_table)
throw 'Required argument missing: quant_table';
if (!data_unit)
throw 'Required argument missing: data_unit';
if (!dstBuf)
throw 'Required argument missing: dstBuf';
return AST2100IDCT.idct_fixed_aan(quant_table, data_unit, dstBuf);
},
_getMcuSize: function () {
return {444: 8, 422: 16}[this.subsamplingMode];
},
// Bakes in C(u)*C(v) and the cosine terms (from the IDCT formula).
_loadQuantTable: function (slot, srcTable) {
for (var y = 0; y < 8; ++y) {
for (var x = 0; x < 8; ++x) {
this.quantTables[slot][y*8+x] = ~~(srcTable[y*8+x] * AAN_IDCT_SCALING_FACTORS[x] * AAN_IDCT_SCALING_FACTORS[y] * 65536.0);
}
}
},
_initializeVq: function () {
// These colors are in YCbCr, so they're just black, white, and two
// shades of grey.
this._vqCodewordLookup = [0, 1, 2, 3];
this._vqCodebook = [
[0x00, 0x80, 0x80],
[0xFF, 0x80, 0x80],
[0x80, 0x80, 0x80],
[0xC0, 0x80, 0x80]
];
},
// Update our position variables to point at the next MCU.
_advancePosition: function () {
var mcuSize = this._getMcuSize();
var widthInMcus = ~~(this._frame_width / mcuSize);
if (this._frame_width % mcuSize != 0)
widthInMcus += 1;
var heightInMcus = ~~(this._frame_height / mcuSize);
if (this._frame_height % mcuSize != 0)
heightInMcus += 1;
this._mcuPosX += 1;
if (this._mcuPosX >= widthInMcus) {
this._mcuPosX = 0;
this._mcuPosY += 1;
}
if (this._mcuPosY >= heightInMcus) {
this._mcuPosY = 0;
}
},
// Change the frame's size.
setSize: function (width, height) {
if (this._frame_width != width || this._frame_height != height)
Util.Debug('Ast2100Decoder: frame height changed to '+width+'x'+height);
this._frame_width = width;
this._frame_height = height;
},
// Each quant table selector is between 0x0 (lowest quality) and 0xB (highest quality). The ATEN client shows a
// single quality slider, which changes both values in tandem. The server sends all three values with each
// FramebufferUpdate message, so these values are updated with every call to decode(). They will be -1 before
// the first frame is decoded.
getVideoSettings: function () {
return {
quantTableSelectorLuma: this._loadedQuantTables[0],
quantTableSelectorChroma: this._loadedQuantTables[1],
subsamplingMode: this.subsamplingMode
};
},
decode: function (data) {
var mcuIdx = 0;
if (verboseStats) {
var blockTypeCounter = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; // length 16
}
// Reset state that must be reset between frames.
this._scan_prev_dc = [0, 0, 0];
this._mcuPosX = 0;
this._mcuPosY = 0;
// First four bytes.
var quantTableSelectorLuma = data[0]; // 0 <= x <= 0xB
var quantTableSelectorChroma = data[1]; // 0 <= x <= 0xB
var subsamplingMode = (data[2] << 8) | data[3]; // 422u or 444u
var changedSettings = false;
if (this.subsamplingMode != subsamplingMode) {
if (verboseVideoSettings)
console.log('decode(): new subsampling mode: '+subsamplingMode);
this.subsamplingMode = subsamplingMode;
changedSettings = true;
}
// The remainder of the stream is byte-swapped in four-byte chunks.
// BitStream takes care of this.
this._stream = new BitStream({data: data});
this._stream.skip(16);
this._stream.skip(16); // do this in two parts because bits must be < 32; thanks JavaScript!
if (quantTableSelectorLuma != this._loadedQuantTables[0]) {
if (!inRangeIncl(quantTableSelectorLuma, 0, 0xB))
throw 'Out-of-range selector for luma quant table: ' + quantTableSelectorLuma.toString(16);
if (verboseVideoSettings)
console.log('decode(): loading new luma quant table: '+fmt_u8(quantTableSelectorLuma));
this._loadQuantTable(0, ATEN_QT_LUMA[quantTableSelectorLuma]);
this._loadedQuantTables[0] = quantTableSelectorLuma;
changedSettings = true;
}
if (quantTableSelectorChroma != this._loadedQuantTables[1]) {
if (!inRangeIncl(quantTableSelectorChroma, 0, 0xB))
throw 'Out-of-range selector for chroma quant table: ' + quantTableSelectorChroma.toString(16);
if (verboseVideoSettings)
console.log('decode(): loading new chroma quant table: '+fmt_u8(quantTableSelectorChroma));
this._loadQuantTable(1, ATEN_QT_CHROMA[quantTableSelectorChroma]);
this._loadedQuantTables[1] = quantTableSelectorChroma;
changedSettings = true;
}
if (this.subsamplingMode != 422 && this.subsamplingMode != 444)
throw 'Unexpected value for subsamplingMode: 0x' + fmt_u16(this.subsamplingMode);
if (changedSettings && this._videoSettingsChangedCallback)
this._videoSettingsChangedCallback(this.getVideoSettings());
// The remainder of the stream is byte-swapped in four-byte chunks. BitStream takes care of this.
this._stream = new BitStream({data: data});
this._stream.skip(16);
this._stream.skip(16); // do this in two parts because bits must be < 32; thanks JavaScript!
while (true) {
var controlFlag = this._stream.read(4); // uint4
if (verboseStats)
++blockTypeCounter[controlFlag];
if (verboseMcuCount) {
console.log('MCU #' + mcuIdx + '- Control flag: ' + controlFlag.toString(16));
console.log(' stream pos = ' + this._stream.getPos());
}
if (controlFlag == 0 || controlFlag == 4 || controlFlag == 8 || controlFlag == 0xC) {
// JPEG-ish (DCT-compressed) data.
if (controlFlag == 8 || controlFlag == 0xC) {
this._mcuPosX = this._stream.read(8); // uint8
this._mcuPosY = this._stream.read(8); // uint8
if (traceUpdates)
console.log("decode(): read new MCU pos: (0x"+fmt_u8(this._mcuPosX)+",0x"+fmt_u8(this._mcuPosY)+")");
}
if (controlFlag == 4 || controlFlag == 0xC) {
// Haven't seen traffic where this feature is used yet.
throw 'Unexpected control flag: alternate quant table';
}
// Since we always have 4:2:2 chroma subsampling on, we'll read 6 blocks (4 Y, 1 Cr, 1 Cb) and
// produce a block of 16x16 pixels.
this._parseMcu();
} else if (inRangeIncl(controlFlag, 5, 7) || inRangeIncl(controlFlag, 0xD, 0xF)) {
// VQ-compressed data.
if (controlFlag >= 0xD) {
this._mcuPosX = this._stream.read(8); // uint8
this._mcuPosY = this._stream.read(8); // uint8
if (traceUpdates)
console.log("decode(): read new MCU pos: (0x"+fmt_u8(this._mcuPosX)+",0x"+fmt_u8(this._mcuPosY)+")");
}
var codewordSize = (controlFlag & 7) - 5; // 0 <= codewordSize <= 2
this._parseVqBlock(codewordSize);
} else if (controlFlag == 9) {
// Done with frame!
break;
} else {
throw 'Unexpected control flag: unknown value 0x'+fmt_u8(controlFlag);
}
++mcuIdx;
}
// The 0x9 "end-of-frame" block is included in this count.
if (traceUpdates)
console.log("decode(): finished after "+mcuIdx+" blocks");
if (verboseStats) {
var counts = {};
for (var i = 0; i < 16; ++i)
if (i != 9 && blockTypeCounter[i] > 0)
counts[i] = blockTypeCounter[i];
if (!isEmpty(counts))
console.log(counts);
}
},
// - Always 8x8, since chroma subsampling does not apply to
// VQ-compressed data. (As a consequence, VQ blocks only seem to
// appear when DCT chroma subsampling is disabled; that is, in "444"
// mode.)
// - Reads 64 * codewordSize bits from the input stream.
_parseVqBlock: function (codewordSize) {
var mcuSize = this._getMcuSize();
if (mcuSize != 8)
throw 'Unexpected MCU size for VQ block!';
if (!inRangeIncl(codewordSize, 0, 2))
throw 'Out-of-range codewordSize!';
var i;
var y_buf = this._componentBufY[0];
var cb_buf = this._componentBufCb;
var cr_buf = this._componentBufCr;
var that = this;
var setColor = function (j, codeword) {
var color = that._vqCodebook[that._vqCodewordLookup[codeword]];
y_buf[j] = color[0];
cb_buf[j] = color[1];
cr_buf[j] = color[2];
};
// Read new codebook data. (That is: new colors for each of our
// codewords (the values we'll read from the input data) to map to.)
for (i = 0; i < (1 << codewordSize); ++i) {
// Read 1b flag and 2b codebook slot number. if flag is set,
// read 24b RGB value and set colors[slot #]. Regardless (?),
// set the ith codeowrd to map to this slot.
var hasNewColor = this._stream.read(1);
var codebookSlotIdx = this._stream.read(2);
if (hasNewColor) {
var color = [this._stream.read(8), this._stream.read(8), this._stream.read(8)]; // Y, Cb, Cr
this._vqCodebook[codebookSlotIdx] = color;
}
this._vqCodewordLookup[i] = codebookSlotIdx;
}
// Read a block of image data.
if (codewordSize == 0) {
// Act as though we've got a single-entry codebook.
for (i = 0; i < 64; ++i)
setColor(i, 0);
} else {
for (i = 0; i < 64; ++i)
setColor(i, this._stream.read(codewordSize));
}
// Perform colorspace conversion and copy into destination image buffer.
for (var j = 0; j < 64; ++j)
this._ycbcrToRgb(this._outputBuf, j, this._componentBufY[0][j], this._componentBufCb[j], this._componentBufCr[j]);
this._blitCallback(8 * this._mcuPosX, 8 * this._mcuPosY, 8, 8, this._outputBuf);
this._advancePosition();
},
_parseMcu: function () {
var qtLuma = this.quantTables[0];
var qtChroma = this.quantTables[1];
this._parseDataUnit(0, this._tmpBufY[0]);
this._idct(qtLuma, this._tmpBufY[0], this._componentBufY[0]);
if (this.subsamplingMode != 444) {
this._parseDataUnit(0, this._tmpBufY[1]);
this._idct(qtLuma, this._tmpBufY[1], this._componentBufY[1]);
this._parseDataUnit(0, this._tmpBufY[2]);
this._idct(qtLuma, this._tmpBufY[2], this._componentBufY[2]);
this._parseDataUnit(0, this._tmpBufY[3]);
this._idct(qtLuma, this._tmpBufY[3], this._componentBufY[3]);
}
this._parseDataUnit(1, this._tmpBufCb);
this._idct(qtChroma, this._tmpBufCb, this._componentBufCb);
this._parseDataUnit(2, this._tmpBufCr);
this._idct(qtChroma, this._tmpBufCr, this._componentBufCr);
if (this.subsamplingMode != 444) {
// 4:2:0 subsampling (x2 in each direction), or what ATEN calls "422" (even though it's not).
for (var dy = 0; dy < 2; ++ dy) {
for (var dx = 0; dx < 2; ++dx) {
// for each of the four blocks in this MCU
var componentBufY = this._componentBufY[dx*2+dy];
for (var y = 0; y < 8; ++y) {
for (var x = 0; x < 8; ++x) {
var hy = ~~((8*dx+y)/2);
var hx = ~~((8*dy+x)/2);
this._ycbcrToRgb(this._outputBuf, y*8+x, componentBufY[y*8+x], this._componentBufCb[hy*8+hx], this._componentBufCr[hy*8+hx]);
}
}
this._blitCallback(16 * this._mcuPosX + 8 * dy, 16 * this._mcuPosY + 8 * dx, 8, 8, this._outputBuf);
}
}
} else {
// No subsampling.
for (var j = 0; j < 64; ++j)
this._ycbcrToRgb(this._outputBuf, j, this._componentBufY[0][j], this._componentBufCb[j], this._componentBufCr[j]);
this._blitCallback(8 * this._mcuPosX, 8 * this._mcuPosY, 8, 8, this._outputBuf);
}
this._advancePosition();
},
_parseDataUnit: function (componentIdx, buf) {
var scanComponent = this._scan_components[componentIdx];
var dc_hufftable = this.huffTables[TABLE_CLASS_DC][scanComponent.huffTableSelectorDC];
var ac_hufftable = this.huffTables[TABLE_CLASS_AC][scanComponent.huffTableSelectorAC];
var setValue = function (i, val) {
buf[ZIGZAG_ORDER[i]] = val;
};
// First element is the DC component, followed by 63 AC components.
// The DC component is encoded slightly differently than the AC
// components: it is stored as the delta between the value and the
// last DC component from the same component.
var dc_delta = this._readEncodedValueDC(dc_hufftable);
this._scan_prev_dc[componentIdx] += dc_delta;
buf[0] = this._scan_prev_dc[componentIdx];
// Read the AC components.
for (var i = 1; i < 64;) {
var x = ac_hufftable.readCode(this._stream);
// N.B.(kelleyk): Renamed r, s to runlen, size.
// See ITU T.81 p89 (e.g. Fig F.1).
// r is runlength of zeroes; if s==0, r==0 means EOB and r==15
// means ZRL. s is number of bits required to represent the
// amplitude that follows the codeword.
var runlen = x >>> 4;
var size = x & 0x0F;
if (size == 0) {
if (runlen == 0) { // special EOB (end-of-block) codeword; fill remainder with zeroes
while (i < 64) {
setValue(i, 0);
++i;
}
break;
} else if (runlen == 0xF) { // special ZLE (zero-length-encode) codeword; emit sixteen zeroes
for (var j = 0; j < 16; ++j)
setValue(i + j, 0);
i += 16;
continue;
}
}
// Emit runlen zero entries.
for (var j = 0; j < runlen; ++j)
setValue(i + j, 0);
i += runlen;
setValue(i, this._readEncodedValueAC(size)); // category=size
i += 1;
}
return buf;
},
_readEncodedValueDC: function (huffTable) {
var category = huffTable.readCode(this._stream);
return this._readEncodedValueAC(category);
},
_readEncodedValueAC: function (category) {
if (category == 0)
return 0;
var value; // sint32
var val_sign = this._stream.read(1);
if (val_sign == 0) {
// Negative (unlike the more common two's-complement
// representation).
value = -(1 << category) + 1;
} else {
value = (1 << category - 1);
}
if (category > 1) {
var more_bits = this._stream.read(category - 1); // uint
value += more_bits;
}
return value;
},
_ycbcrToRgb: function (outputBuf, outputOffset, y, cb, cr) {
outputOffset *= 4;
outputBuf[outputOffset + 0] = clamp(YUVTORGB_Y_TABLE[y] + YUVTORGB_CR_R_TABLE[cr]);
outputBuf[outputOffset + 1] = clamp(YUVTORGB_Y_TABLE[y] + YUVTORGB_CR_G_TABLE[cr] + YUVTORGB_CB_G_TABLE[cb]);
outputBuf[outputOffset + 2] = clamp(YUVTORGB_Y_TABLE[y] + YUVTORGB_CB_B_TABLE[cb]);
outputBuf[outputOffset + 3] = 0xFF; // noVNC expects alpha
}
};
})();