"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 . 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._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; }, 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 if (this.subsamplingMode != subsamplingMode) { if (verboseVideoSettings) console.log('decode(): new subsampling mode: '+subsamplingMode); this.subsamplingMode = subsamplingMode; } // 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; } 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; } if (this.subsamplingMode != 422 && this.subsamplingMode != 444) throw 'Unexpected value for subsamplingMode: 0x' + fmt_u16(this.subsamplingMode); // 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 } }; })();