@invintusmedia/tomp4 1.3.1 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tomp4.js +2 -2
- package/package.json +1 -1
- package/src/codecs/REFERENCE.md +885 -0
- package/src/codecs/h264-cabac-init.js +546 -0
- package/src/codecs/h264-cabac.js +322 -0
- package/src/codecs/h264-cavlc-tables.js +628 -0
- package/src/codecs/h264-decoder.js +940 -0
- package/src/codecs/h264-encoder.js +502 -0
- package/src/codecs/h264-intra.js +292 -0
- package/src/codecs/h264-sps-pps.js +483 -0
- package/src/codecs/h264-tables.js +217 -0
- package/src/codecs/h264-transform.js +268 -0
- package/src/codecs/smart-render.js +169 -0
- package/src/hls-clip.js +50 -22
- package/src/index.js +1 -1
|
@@ -0,0 +1,940 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* H.264 Decoder
|
|
3
|
+
*
|
|
4
|
+
* Decodes H.264 High profile CABAC streams to YUV pixel data.
|
|
5
|
+
* Used by the HLS clipper for smart-rendering: decode a GOP from
|
|
6
|
+
* keyframe to clip start, extract pixels, re-encode as I-frame.
|
|
7
|
+
*
|
|
8
|
+
* Reference: ITU-T H.264, FFmpeg libavcodec/h264_cabac.c
|
|
9
|
+
*
|
|
10
|
+
* @module codecs/h264-decoder
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { CabacDecoder, removeEmulationPrevention } from './h264-cabac.js';
|
|
14
|
+
import { cabacInitI, cabacInitPB } from './h264-cabac-init.js';
|
|
15
|
+
import { parseSPSFull, parsePPSFull, parseSliceHeader } from './h264-sps-pps.js';
|
|
16
|
+
import { inverseDCT4x4, inverseHadamard4x4, inverseHadamard2x2, dequantize4x4, clip255 } from './h264-transform.js';
|
|
17
|
+
import { intra4x4Predict, intra16x16Predict, intraChromaPredict } from './h264-intra.js';
|
|
18
|
+
import { i16x16TypeMap } from './h264-tables.js';
|
|
19
|
+
|
|
20
|
+
// ── Context index offsets (Table 9-11) ────────────────────
|
|
21
|
+
|
|
22
|
+
const CTX_MB_TYPE_SI = 0;
|
|
23
|
+
const CTX_MB_TYPE_I = 3;
|
|
24
|
+
const CTX_MB_SKIP_P = 11;
|
|
25
|
+
const CTX_MB_TYPE_P = 14;
|
|
26
|
+
const CTX_SUB_MB_P = 21;
|
|
27
|
+
const CTX_MB_SKIP_B = 24;
|
|
28
|
+
const CTX_MB_TYPE_B = 27;
|
|
29
|
+
const CTX_SUB_MB_B = 36;
|
|
30
|
+
const CTX_MVD_X = 40;
|
|
31
|
+
const CTX_MVD_Y = 47;
|
|
32
|
+
const CTX_REF_IDX = 54;
|
|
33
|
+
const CTX_QP_DELTA = 60;
|
|
34
|
+
const CTX_CHROMA_PRED = 64;
|
|
35
|
+
const CTX_INTRA_PRED_FLAG = 68;
|
|
36
|
+
const CTX_INTRA_PRED_REM = 69;
|
|
37
|
+
const CTX_CBP_LUMA = 73;
|
|
38
|
+
const CTX_CBP_CHROMA = 77;
|
|
39
|
+
|
|
40
|
+
// coded_block_flag bases per category
|
|
41
|
+
const CBF_BASE = [85, 89, 93, 97, 101, 1012];
|
|
42
|
+
|
|
43
|
+
// significant_coeff / last_significant context offsets (frame mode)
|
|
44
|
+
const SIG_OFF = [105, 120, 134, 149, 152, 402];
|
|
45
|
+
const LAST_OFF = [166, 181, 195, 210, 213, 417];
|
|
46
|
+
|
|
47
|
+
// coeff_abs_level_minus1 context offsets per category
|
|
48
|
+
const ABS_LEVEL_BASE = [227, 237, 247, 257, 266, 952];
|
|
49
|
+
|
|
50
|
+
// coeff_abs_level context state machine (from REFERENCE.md)
|
|
51
|
+
const LEVEL1_CTX = [1, 2, 3, 4, 0, 0, 0, 0];
|
|
52
|
+
const LEVELGT1_CTX = [5, 5, 5, 5, 6, 7, 8, 9];
|
|
53
|
+
const TRANS_ON_1 = [1, 2, 3, 3, 4, 5, 6, 7];
|
|
54
|
+
const TRANS_ON_GT1 = [4, 4, 4, 4, 5, 6, 7, 7];
|
|
55
|
+
|
|
56
|
+
// ── YUV Frame Buffer ──────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
class YUVFrame {
|
|
59
|
+
constructor(width, height) {
|
|
60
|
+
this.width = width;
|
|
61
|
+
this.height = height;
|
|
62
|
+
this.strideY = width;
|
|
63
|
+
this.strideC = width >> 1;
|
|
64
|
+
this.Y = new Uint8Array(width * height);
|
|
65
|
+
this.U = new Uint8Array((width >> 1) * (height >> 1));
|
|
66
|
+
this.V = new Uint8Array((width >> 1) * (height >> 1));
|
|
67
|
+
this.poc = 0;
|
|
68
|
+
this.frameNum = 0;
|
|
69
|
+
this.isReference = false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getY(x, y) {
|
|
73
|
+
x = Math.max(0, Math.min(this.width - 1, x));
|
|
74
|
+
y = Math.max(0, Math.min(this.height - 1, y));
|
|
75
|
+
return this.Y[y * this.strideY + x];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getU(x, y) {
|
|
79
|
+
x = Math.max(0, Math.min((this.width >> 1) - 1, x));
|
|
80
|
+
y = Math.max(0, Math.min((this.height >> 1) - 1, y));
|
|
81
|
+
return this.U[y * this.strideC + x];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
getV(x, y) {
|
|
85
|
+
x = Math.max(0, Math.min((this.width >> 1) - 1, x));
|
|
86
|
+
y = Math.max(0, Math.min((this.height >> 1) - 1, y));
|
|
87
|
+
return this.V[y * this.strideC + x];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
clone() {
|
|
91
|
+
const f = new YUVFrame(this.width, this.height);
|
|
92
|
+
f.Y.set(this.Y); f.U.set(this.U); f.V.set(this.V);
|
|
93
|
+
f.poc = this.poc; f.frameNum = this.frameNum;
|
|
94
|
+
f.isReference = this.isReference;
|
|
95
|
+
return f;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── H.264 Decoder ─────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
export class H264Decoder {
|
|
102
|
+
constructor() {
|
|
103
|
+
this.spsMap = new Map();
|
|
104
|
+
this.ppsMap = new Map();
|
|
105
|
+
this.dpb = [];
|
|
106
|
+
this.frame = null;
|
|
107
|
+
this.sps = null;
|
|
108
|
+
this.pps = null;
|
|
109
|
+
|
|
110
|
+
// Per-frame MB state
|
|
111
|
+
this.mbW = 0;
|
|
112
|
+
this.mbH = 0;
|
|
113
|
+
this.mbType = null; // Int32Array[numMBs]
|
|
114
|
+
this.mbCbpLuma = null; // Uint8Array[numMBs]
|
|
115
|
+
this.mbCbpChroma = null; // Uint8Array[numMBs]
|
|
116
|
+
this.nzCoeff = null; // non-zero coeff counts for coded_block_flag
|
|
117
|
+
this.mbIntraChromaMode = null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Feed a NAL unit to the decoder.
|
|
122
|
+
* @returns {YUVFrame|null}
|
|
123
|
+
*/
|
|
124
|
+
feedNAL(nalUnit) {
|
|
125
|
+
const nalType = nalUnit[0] & 0x1F;
|
|
126
|
+
if (nalType === 7) {
|
|
127
|
+
const sps = parseSPSFull(nalUnit);
|
|
128
|
+
this.spsMap.set(sps.seq_parameter_set_id, sps);
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
if (nalType === 8) {
|
|
132
|
+
const sps = this.spsMap.values().next().value;
|
|
133
|
+
const pps = parsePPSFull(nalUnit, sps);
|
|
134
|
+
this.ppsMap.set(pps.pic_parameter_set_id, pps);
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
if (nalType === 5 || nalType === 1) {
|
|
138
|
+
return this._decodeSlice(nalUnit);
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
decodeAccessUnit(nalUnits) {
|
|
144
|
+
let frame = null;
|
|
145
|
+
for (const nal of nalUnits) {
|
|
146
|
+
const result = this.feedNAL(nal);
|
|
147
|
+
if (result) frame = result;
|
|
148
|
+
}
|
|
149
|
+
return frame;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── Slice Decoding ──────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
_decodeSlice(nalUnit) {
|
|
155
|
+
if (this.spsMap.size === 0 || this.ppsMap.size === 0) return null;
|
|
156
|
+
this.sps = this.spsMap.values().next().value;
|
|
157
|
+
this.pps = this.ppsMap.values().next().value;
|
|
158
|
+
|
|
159
|
+
const { sps, pps } = this;
|
|
160
|
+
const sh = parseSliceHeader(nalUnit, sps, pps);
|
|
161
|
+
|
|
162
|
+
if (sh.isIDR) this.dpb = [];
|
|
163
|
+
|
|
164
|
+
const fw = sps.PicWidthInMbs * 16;
|
|
165
|
+
const fh = sps.PicHeightInMbs * 16;
|
|
166
|
+
this.frame = new YUVFrame(fw, fh);
|
|
167
|
+
this.frame.frameNum = sh.frame_num;
|
|
168
|
+
this.frame.poc = sh.pic_order_cnt_lsb;
|
|
169
|
+
this.frame.isReference = sh.nal_ref_idc !== 0;
|
|
170
|
+
|
|
171
|
+
this.mbW = sps.PicWidthInMbs;
|
|
172
|
+
this.mbH = sps.PicHeightInMbs;
|
|
173
|
+
const numMBs = sps.PicSizeInMbs;
|
|
174
|
+
this.mbType = new Int32Array(numMBs).fill(-1);
|
|
175
|
+
this.mbCbpLuma = new Uint8Array(numMBs);
|
|
176
|
+
this.mbCbpChroma = new Uint8Array(numMBs);
|
|
177
|
+
this.mbIntraChromaMode = new Uint8Array(numMBs);
|
|
178
|
+
// 4x4 block non-zero coeff counts: 24 per MB (16 luma + 4 Cb + 4 Cr)
|
|
179
|
+
this.nzCoeff = new Uint8Array(numMBs * 24);
|
|
180
|
+
|
|
181
|
+
const refL0 = this._buildRefList(sh, 0);
|
|
182
|
+
const refL1 = sh.isB ? this._buildRefList(sh, 1) : [];
|
|
183
|
+
|
|
184
|
+
// Init CABAC
|
|
185
|
+
const cabac = new CabacDecoder(sh._rbsp, sh.headerBitLength);
|
|
186
|
+
const initTable = sh.isI ? cabacInitI : cabacInitPB[sh.cabac_init_idc];
|
|
187
|
+
cabac.initContexts(sh.slice_type_mod5, sh.SliceQPY, sh.cabac_init_idc, initTable);
|
|
188
|
+
|
|
189
|
+
let qp = sh.SliceQPY;
|
|
190
|
+
let prevQPDelta = 0;
|
|
191
|
+
let mbIdx = sh.first_mb_in_slice;
|
|
192
|
+
|
|
193
|
+
while (mbIdx < numMBs) {
|
|
194
|
+
const mbX = mbIdx % this.mbW;
|
|
195
|
+
const mbY = (mbIdx / this.mbW) | 0;
|
|
196
|
+
|
|
197
|
+
// Skip flag (P/B slices only)
|
|
198
|
+
let skipped = false;
|
|
199
|
+
if (!sh.isI) {
|
|
200
|
+
skipped = this._decodeMbSkip(cabac, sh, mbIdx, mbX, mbY);
|
|
201
|
+
if (skipped) {
|
|
202
|
+
this.mbType[mbIdx] = -1; // skip
|
|
203
|
+
this._reconstructSkip(mbX, mbY, refL0, sh);
|
|
204
|
+
const endOfSlice = cabac.decodeTerminate();
|
|
205
|
+
if (endOfSlice) break;
|
|
206
|
+
mbIdx++;
|
|
207
|
+
prevQPDelta = 0;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Decode mb_type
|
|
213
|
+
let mt;
|
|
214
|
+
if (sh.isI) {
|
|
215
|
+
mt = this._decodeMbTypeI(cabac, mbIdx);
|
|
216
|
+
} else if (sh.isP) {
|
|
217
|
+
mt = this._decodeMbTypeP(cabac, mbIdx);
|
|
218
|
+
} else {
|
|
219
|
+
mt = this._decodeMbTypeB(cabac, mbIdx);
|
|
220
|
+
}
|
|
221
|
+
this.mbType[mbIdx] = mt;
|
|
222
|
+
|
|
223
|
+
// I_PCM
|
|
224
|
+
if (this._isPCM(mt, sh)) {
|
|
225
|
+
this._decodeIPCM(cabac, mbX, mbY);
|
|
226
|
+
qp = sh.SliceQPY;
|
|
227
|
+
prevQPDelta = 0;
|
|
228
|
+
const endOfSlice = cabac.decodeTerminate();
|
|
229
|
+
if (endOfSlice) break;
|
|
230
|
+
mbIdx++;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const isIntra = this._isIntra(mt, sh);
|
|
235
|
+
const isI16 = this._isI16x16(mt, sh);
|
|
236
|
+
const isINxN = this._isINxN(mt, sh);
|
|
237
|
+
|
|
238
|
+
// Intra 4x4 prediction modes
|
|
239
|
+
if (isINxN) {
|
|
240
|
+
this._decodeIntra4x4Modes(cabac, mbX, mbY, mbIdx);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Intra chroma prediction mode
|
|
244
|
+
let chromaMode = 0;
|
|
245
|
+
if (isIntra) {
|
|
246
|
+
chromaMode = this._decodeChromaPredMode(cabac, mbIdx);
|
|
247
|
+
this.mbIntraChromaMode[mbIdx] = chromaMode;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Inter prediction
|
|
251
|
+
if (!isIntra) {
|
|
252
|
+
this._decodeInterPred(cabac, sh, mt, mbIdx, mbX, mbY, refL0, refL1);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// CBP
|
|
256
|
+
let cbpL, cbpC;
|
|
257
|
+
if (isI16) {
|
|
258
|
+
const typeIdx = this._i16idx(mt, sh);
|
|
259
|
+
cbpL = i16x16TypeMap[typeIdx][1];
|
|
260
|
+
cbpC = i16x16TypeMap[typeIdx][2];
|
|
261
|
+
} else {
|
|
262
|
+
const cbp = this._decodeCBP(cabac, mbIdx, isIntra);
|
|
263
|
+
cbpL = cbp & 0xF;
|
|
264
|
+
cbpC = (cbp >> 4) & 0x3;
|
|
265
|
+
}
|
|
266
|
+
this.mbCbpLuma[mbIdx] = cbpL;
|
|
267
|
+
this.mbCbpChroma[mbIdx] = cbpC;
|
|
268
|
+
|
|
269
|
+
// QP delta
|
|
270
|
+
let qpDelta = 0;
|
|
271
|
+
if (cbpL > 0 || cbpC > 0 || isI16) {
|
|
272
|
+
qpDelta = this._decodeQPDelta(cabac, prevQPDelta);
|
|
273
|
+
prevQPDelta = qpDelta;
|
|
274
|
+
} else {
|
|
275
|
+
prevQPDelta = 0;
|
|
276
|
+
}
|
|
277
|
+
qp = ((qp + qpDelta + 52 + 52) % 52);
|
|
278
|
+
|
|
279
|
+
// Reconstruct
|
|
280
|
+
if (isI16) {
|
|
281
|
+
this._reconI16x16(cabac, mbX, mbY, mt, sh, qp, cbpL, cbpC);
|
|
282
|
+
} else if (isINxN) {
|
|
283
|
+
this._reconINxN(cabac, mbX, mbY, qp, cbpL, cbpC);
|
|
284
|
+
} else {
|
|
285
|
+
this._reconInter(cabac, mbX, mbY, qp, cbpL, cbpC);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Chroma
|
|
289
|
+
this._reconChroma(cabac, mbX, mbY, qp, cbpC, isIntra, chromaMode);
|
|
290
|
+
|
|
291
|
+
const endOfSlice = cabac.decodeTerminate();
|
|
292
|
+
if (endOfSlice) break;
|
|
293
|
+
mbIdx++;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Deblocking
|
|
297
|
+
if (sh.disable_deblocking_filter_idc !== 1) {
|
|
298
|
+
this._deblock(sh);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Store reference
|
|
302
|
+
if (this.frame.isReference) {
|
|
303
|
+
this.dpb.push(this.frame.clone());
|
|
304
|
+
if (this.dpb.length > 16) this.dpb.shift();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return this.frame;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ── mb_skip (P/B) ──────────────────────────────────────
|
|
311
|
+
|
|
312
|
+
_decodeMbSkip(cabac, sh, mbIdx, mbX, mbY) {
|
|
313
|
+
const ctxBase = sh.isP ? CTX_MB_SKIP_P : CTX_MB_SKIP_B;
|
|
314
|
+
const leftSkip = mbX > 0 && this.mbType[mbIdx - 1] === -1 ? 0 : 1;
|
|
315
|
+
const topSkip = mbY > 0 && this.mbType[mbIdx - this.mbW] === -1 ? 0 : 1;
|
|
316
|
+
// Wait — skip context is: left NOT skip + top NOT skip
|
|
317
|
+
// Actually from FFmpeg: ctxInc = (left_type != SKIP) + (top_type != SKIP)
|
|
318
|
+
const ctxInc = (mbX > 0 && this.mbType[mbIdx - 1] !== -1 ? 1 : 0) +
|
|
319
|
+
(mbY > 0 && this.mbType[mbIdx - this.mbW] !== -1 ? 1 : 0);
|
|
320
|
+
return cabac.decodeBin(ctxBase + ctxInc) === 1;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ── mb_type decoders (FFmpeg patterns from REFERENCE.md) ─
|
|
324
|
+
|
|
325
|
+
_decodeMbTypeI(cabac, mbIdx) {
|
|
326
|
+
// I-slice: ctx base = 3
|
|
327
|
+
const ctxInc = this._i16Neighbor(mbIdx);
|
|
328
|
+
const bin0 = cabac.decodeBin(CTX_MB_TYPE_I + ctxInc);
|
|
329
|
+
if (bin0 === 0) return 0; // I_NxN
|
|
330
|
+
|
|
331
|
+
const term = cabac.decodeTerminate();
|
|
332
|
+
if (term) return 25; // I_PCM
|
|
333
|
+
|
|
334
|
+
// I_16x16 subtype
|
|
335
|
+
let mt = 1;
|
|
336
|
+
mt += 12 * cabac.decodeBin(CTX_MB_TYPE_I + 3); // cbp_luma != 0
|
|
337
|
+
if (cabac.decodeBin(CTX_MB_TYPE_I + 4)) // cbp_chroma > 0
|
|
338
|
+
mt += 4 + 4 * cabac.decodeBin(CTX_MB_TYPE_I + 5); // cbp_chroma == 2
|
|
339
|
+
mt += 2 * cabac.decodeBin(CTX_MB_TYPE_I + 6); // pred_mode bit 1
|
|
340
|
+
mt += cabac.decodeBin(CTX_MB_TYPE_I + 7); // pred_mode bit 0
|
|
341
|
+
return mt;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
_i16Neighbor(mbIdx) {
|
|
345
|
+
const mbX = mbIdx % this.mbW;
|
|
346
|
+
const mbY = (mbIdx / this.mbW) | 0;
|
|
347
|
+
const left = mbX > 0 ? this.mbType[mbIdx - 1] : -1;
|
|
348
|
+
const top = mbY > 0 ? this.mbType[mbIdx - this.mbW] : -1;
|
|
349
|
+
// ctxInc = (left is I_16x16 or I_PCM) + (top is I_16x16 or I_PCM)
|
|
350
|
+
return (left >= 1 ? 1 : 0) + (top >= 1 ? 1 : 0);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
_decodeMbTypeP(cabac, mbIdx) {
|
|
354
|
+
if (cabac.decodeBin(CTX_MB_TYPE_P + 0) === 0) {
|
|
355
|
+
if (cabac.decodeBin(CTX_MB_TYPE_P + 1) === 0)
|
|
356
|
+
return 3 * cabac.decodeBin(CTX_MB_TYPE_P + 2); // 0=P_L0_16x16, 3=P_8x8
|
|
357
|
+
return 2 - cabac.decodeBin(CTX_MB_TYPE_P + 3); // 1=P_L0_16x8, 2=P_L0_8x16
|
|
358
|
+
}
|
|
359
|
+
// Intra in P-slice: decode I mb_type and add 5
|
|
360
|
+
return 5 + this._decodeMbTypeIinPB(cabac, CTX_MB_TYPE_P + 3);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
_decodeMbTypeB(cabac, mbIdx) {
|
|
364
|
+
const ctxInc = this._bTypeNeighborCtx(mbIdx);
|
|
365
|
+
if (cabac.decodeBin(CTX_MB_TYPE_B + ctxInc) === 0) return 0; // B_Direct_16x16
|
|
366
|
+
|
|
367
|
+
if (cabac.decodeBin(CTX_MB_TYPE_B + 3) === 0)
|
|
368
|
+
return 1 + cabac.decodeBin(CTX_MB_TYPE_B + 5); // B_L0_16x16 or B_L1_16x16
|
|
369
|
+
|
|
370
|
+
if (cabac.decodeBin(CTX_MB_TYPE_B + 4) === 0) {
|
|
371
|
+
return 3 + ((cabac.decodeBin(CTX_MB_TYPE_B + 5) << 1) |
|
|
372
|
+
cabac.decodeBin(CTX_MB_TYPE_B + 5)); // 3-6
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (cabac.decodeBin(CTX_MB_TYPE_B + 5) === 0) {
|
|
376
|
+
return 7 + ((cabac.decodeBin(CTX_MB_TYPE_B + 5) << 1) |
|
|
377
|
+
cabac.decodeBin(CTX_MB_TYPE_B + 5)); // 7-10
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (cabac.decodeBin(CTX_MB_TYPE_B + 5) === 0) {
|
|
381
|
+
return 11 + ((cabac.decodeBin(CTX_MB_TYPE_B + 5) << 1) |
|
|
382
|
+
cabac.decodeBin(CTX_MB_TYPE_B + 5)); // 11-14
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (cabac.decodeBin(CTX_MB_TYPE_B + 5) === 0) {
|
|
386
|
+
return 15 + ((cabac.decodeBin(CTX_MB_TYPE_B + 5) << 1) |
|
|
387
|
+
cabac.decodeBin(CTX_MB_TYPE_B + 5)); // 15-18
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (cabac.decodeBin(CTX_MB_TYPE_B + 5) === 0) {
|
|
391
|
+
return 19 + ((cabac.decodeBin(CTX_MB_TYPE_B + 5) << 1) |
|
|
392
|
+
cabac.decodeBin(CTX_MB_TYPE_B + 5)); // 19-22
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Intra in B-slice
|
|
396
|
+
return 23 + this._decodeMbTypeIinPB(cabac, CTX_MB_TYPE_B + 8);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
_bTypeNeighborCtx(mbIdx) {
|
|
400
|
+
const mbX = mbIdx % this.mbW;
|
|
401
|
+
const mbY = (mbIdx / this.mbW) | 0;
|
|
402
|
+
const left = mbX > 0 ? this.mbType[mbIdx - 1] : -1;
|
|
403
|
+
const top = mbY > 0 ? this.mbType[mbIdx - this.mbW] : -1;
|
|
404
|
+
return (left > 0 ? 1 : 0) + (top > 0 ? 1 : 0);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** Decode I mb_type when embedded in P/B slice (different ctx base) */
|
|
408
|
+
_decodeMbTypeIinPB(cabac, ctxBase) {
|
|
409
|
+
const bin0 = cabac.decodeBin(ctxBase);
|
|
410
|
+
if (bin0 === 0) return 0; // I_NxN
|
|
411
|
+
const term = cabac.decodeTerminate();
|
|
412
|
+
if (term) return 25; // I_PCM
|
|
413
|
+
let mt = 1;
|
|
414
|
+
mt += 12 * cabac.decodeBin(ctxBase + 1);
|
|
415
|
+
if (cabac.decodeBin(ctxBase + 2))
|
|
416
|
+
mt += 4 + 4 * cabac.decodeBin(ctxBase + 2);
|
|
417
|
+
mt += 2 * cabac.decodeBin(ctxBase + 3);
|
|
418
|
+
mt += cabac.decodeBin(ctxBase + 3);
|
|
419
|
+
return mt;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ── Type predicates ─────────────────────────────────────
|
|
423
|
+
|
|
424
|
+
_isPCM(mt, sh) { const b = sh.isI ? 0 : sh.isP ? 5 : 23; return mt - b === 25; }
|
|
425
|
+
_isIntra(mt, sh) { if (sh.isI) return true; return mt >= (sh.isP ? 5 : 23); }
|
|
426
|
+
_isI16x16(mt, sh) { const b = sh.isI ? 0 : sh.isP ? 5 : 23; const a = mt - b; return a >= 1 && a <= 24; }
|
|
427
|
+
_isINxN(mt, sh) { const b = sh.isI ? 0 : sh.isP ? 5 : 23; return mt - b === 0; }
|
|
428
|
+
_i16idx(mt, sh) { const b = sh.isI ? 0 : sh.isP ? 5 : 23; return mt - b - 1; }
|
|
429
|
+
|
|
430
|
+
// ── Chroma prediction mode ──────────────────────────────
|
|
431
|
+
|
|
432
|
+
_decodeChromaPredMode(cabac, mbIdx) {
|
|
433
|
+
const mbX = mbIdx % this.mbW;
|
|
434
|
+
const mbY = (mbIdx / this.mbW) | 0;
|
|
435
|
+
const leftMode = mbX > 0 ? this.mbIntraChromaMode[mbIdx - 1] : 0;
|
|
436
|
+
const topMode = mbY > 0 ? this.mbIntraChromaMode[mbIdx - this.mbW] : 0;
|
|
437
|
+
const ctxInc = (leftMode > 0 ? 1 : 0) + (topMode > 0 ? 1 : 0);
|
|
438
|
+
|
|
439
|
+
if (cabac.decodeBin(CTX_CHROMA_PRED + ctxInc) === 0) return 0;
|
|
440
|
+
if (cabac.decodeBin(CTX_CHROMA_PRED + 3) === 0) return 1;
|
|
441
|
+
return 2 + cabac.decodeBin(CTX_CHROMA_PRED + 3);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ── Intra 4x4 prediction modes ─────────────────────────
|
|
445
|
+
|
|
446
|
+
_decodeIntra4x4Modes(cabac, mbX, mbY, mbIdx) {
|
|
447
|
+
// 16 4x4 blocks, decode prev_intra4x4_pred_mode_flag + rem
|
|
448
|
+
for (let blk = 0; blk < 16; blk++) {
|
|
449
|
+
const flag = cabac.decodeBin(CTX_INTRA_PRED_FLAG);
|
|
450
|
+
if (flag) {
|
|
451
|
+
// Use most probable mode (skip decoding rem)
|
|
452
|
+
} else {
|
|
453
|
+
// Read 3 fixed-context bins for rem_intra4x4_pred_mode
|
|
454
|
+
const b0 = cabac.decodeBin(CTX_INTRA_PRED_REM);
|
|
455
|
+
const b1 = cabac.decodeBin(CTX_INTRA_PRED_REM);
|
|
456
|
+
const b2 = cabac.decodeBin(CTX_INTRA_PRED_REM);
|
|
457
|
+
// rem = b0 | (b1<<1) | (b2<<2)
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ── CBP decoding (FFmpeg pattern from REFERENCE.md) ─────
|
|
463
|
+
|
|
464
|
+
_decodeCBP(cabac, mbIdx, isIntra) {
|
|
465
|
+
const mbX = mbIdx % this.mbW;
|
|
466
|
+
const mbY = (mbIdx / this.mbW) | 0;
|
|
467
|
+
|
|
468
|
+
// Neighbor CBP for context derivation
|
|
469
|
+
const cbpA = mbX > 0 ? this.mbCbpLuma[mbIdx - 1] : 0;
|
|
470
|
+
const cbpB = mbY > 0 ? this.mbCbpLuma[mbIdx - this.mbW] : 0;
|
|
471
|
+
const cbpCA = mbX > 0 ? this.mbCbpChroma[mbIdx - 1] : 0;
|
|
472
|
+
const cbpCB = mbY > 0 ? this.mbCbpChroma[mbIdx - this.mbW] : 0;
|
|
473
|
+
|
|
474
|
+
// Luma CBP: 4 bins conditioned on left/top
|
|
475
|
+
let cbpL = 0;
|
|
476
|
+
// bit 0: left=A's bit1, top=B's bit2
|
|
477
|
+
let ctx = (!(cbpA & 0x02) ? 1 : 0) + 2 * (!(cbpB & 0x04) ? 1 : 0);
|
|
478
|
+
cbpL |= cabac.decodeBin(CTX_CBP_LUMA + ctx);
|
|
479
|
+
// bit 1: left=current bit0, top=B's bit3
|
|
480
|
+
ctx = (!(cbpL & 0x01) ? 1 : 0) + 2 * (!(cbpB & 0x08) ? 1 : 0);
|
|
481
|
+
cbpL |= cabac.decodeBin(CTX_CBP_LUMA + ctx) << 1;
|
|
482
|
+
// bit 2: left=A's bit3, top=current bit0
|
|
483
|
+
ctx = (!(cbpA & 0x08) ? 1 : 0) + 2 * (!(cbpL & 0x01) ? 1 : 0);
|
|
484
|
+
cbpL |= cabac.decodeBin(CTX_CBP_LUMA + ctx) << 2;
|
|
485
|
+
// bit 3: left=current bit2, top=current bit1
|
|
486
|
+
ctx = (!(cbpL & 0x04) ? 1 : 0) + 2 * (!(cbpL & 0x02) ? 1 : 0);
|
|
487
|
+
cbpL |= cabac.decodeBin(CTX_CBP_LUMA + ctx) << 3;
|
|
488
|
+
|
|
489
|
+
// Chroma CBP
|
|
490
|
+
let cbpC = 0;
|
|
491
|
+
if (this.sps.ChromaArrayType !== 0) {
|
|
492
|
+
ctx = (cbpCA > 0 ? 1 : 0) + 2 * (cbpCB > 0 ? 1 : 0);
|
|
493
|
+
if (cabac.decodeBin(CTX_CBP_CHROMA + ctx)) {
|
|
494
|
+
ctx = 4 + (cbpCA === 2 ? 1 : 0) + 2 * (cbpCB === 2 ? 1 : 0);
|
|
495
|
+
cbpC = 1 + cabac.decodeBin(CTX_CBP_CHROMA + ctx);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return cbpL | (cbpC << 4);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ── QP delta ────────────────────────────────────────────
|
|
503
|
+
|
|
504
|
+
_decodeQPDelta(cabac, prevQPDelta) {
|
|
505
|
+
const ctxInc0 = prevQPDelta !== 0 ? 1 : 0;
|
|
506
|
+
if (cabac.decodeBin(CTX_QP_DELTA + ctxInc0) === 0) return 0;
|
|
507
|
+
|
|
508
|
+
let abs = 1;
|
|
509
|
+
while (abs < 52 && cabac.decodeBin(CTX_QP_DELTA + Math.min(abs + 1, 2))) {
|
|
510
|
+
abs++;
|
|
511
|
+
}
|
|
512
|
+
const sign = cabac.decodeBypass();
|
|
513
|
+
return sign ? -abs : abs;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ── Residual block decoding (CABAC) ─────────────────────
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Decode a 4x4 residual block using CABAC.
|
|
520
|
+
* @param {number} cat - Block category (0=DC16x16, 1=AC16x16, 2=Luma4x4, 3=ChromaDC, 4=ChromaAC, 5=Luma8x8)
|
|
521
|
+
* @param {number} mbIdx - Macroblock index
|
|
522
|
+
* @param {number} blkIdx - Block index within MB (for nzCoeff tracking)
|
|
523
|
+
* @returns {Int32Array} Coefficients in scan order
|
|
524
|
+
*/
|
|
525
|
+
_decodeResidualBlock(cabac, cat, mbIdx, blkIdx) {
|
|
526
|
+
const maxCoeff = (cat === 0 || cat === 3) ? 16 : (cat === 1 || cat === 4) ? 15 : 16;
|
|
527
|
+
|
|
528
|
+
// coded_block_flag
|
|
529
|
+
const cbfBase = CBF_BASE[cat] || 85;
|
|
530
|
+
const nzLeft = 0; // simplified: would check left neighbor's nzCoeff
|
|
531
|
+
const nzTop = 0; // simplified: would check top neighbor's nzCoeff
|
|
532
|
+
const cbfCtx = cbfBase + nzLeft + 2 * nzTop;
|
|
533
|
+
|
|
534
|
+
if (cabac.decodeBin(cbfCtx) === 0) return new Int32Array(maxCoeff);
|
|
535
|
+
|
|
536
|
+
// significant_coeff_flag + last_significant_coeff_flag
|
|
537
|
+
const sigBase = SIG_OFF[cat] || 105;
|
|
538
|
+
const lastBase = LAST_OFF[cat] || 166;
|
|
539
|
+
const significantPositions = [];
|
|
540
|
+
|
|
541
|
+
for (let i = 0; i < maxCoeff - 1; i++) {
|
|
542
|
+
if (cabac.decodeBin(sigBase + Math.min(i, 14))) {
|
|
543
|
+
significantPositions.push(i);
|
|
544
|
+
if (cabac.decodeBin(lastBase + Math.min(i, 14))) break;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (significantPositions.length === 0 ||
|
|
548
|
+
significantPositions[significantPositions.length - 1] !== maxCoeff - 1) {
|
|
549
|
+
// Last position is implicitly significant if we didn't hit "last" flag
|
|
550
|
+
significantPositions.push(maxCoeff - 1);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// coeff_abs_level_minus1 + sign (node-based state machine)
|
|
554
|
+
const absBase = ABS_LEVEL_BASE[cat] || 227;
|
|
555
|
+
const coeffs = new Int32Array(maxCoeff);
|
|
556
|
+
let nodeCtx = 0;
|
|
557
|
+
|
|
558
|
+
for (let i = significantPositions.length - 1; i >= 0; i--) {
|
|
559
|
+
const pos = significantPositions[i];
|
|
560
|
+
let level;
|
|
561
|
+
|
|
562
|
+
const ctx1 = absBase + LEVEL1_CTX[nodeCtx];
|
|
563
|
+
if (cabac.decodeBin(ctx1) === 0) {
|
|
564
|
+
level = 1;
|
|
565
|
+
nodeCtx = TRANS_ON_1[nodeCtx];
|
|
566
|
+
} else {
|
|
567
|
+
const ctxGt1 = absBase + LEVELGT1_CTX[nodeCtx];
|
|
568
|
+
nodeCtx = TRANS_ON_GT1[nodeCtx];
|
|
569
|
+
level = 2;
|
|
570
|
+
while (level < 15 && cabac.decodeBin(ctxGt1)) level++;
|
|
571
|
+
if (level >= 15) {
|
|
572
|
+
// Exp-Golomb k=0 suffix
|
|
573
|
+
let k = 0;
|
|
574
|
+
while (cabac.decodeBypass()) { level += 1 << k; k++; }
|
|
575
|
+
while (k > 0) { k--; level += cabac.decodeBypass() << k; }
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const sign = cabac.decodeBypass();
|
|
580
|
+
coeffs[pos] = sign ? -level : level;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Track non-zero coefficients
|
|
584
|
+
if (blkIdx >= 0 && mbIdx >= 0) {
|
|
585
|
+
this.nzCoeff[mbIdx * 24 + blkIdx] = significantPositions.length;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return coeffs;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// ── Reconstruction: I_16x16 ─────────────────────────────
|
|
592
|
+
|
|
593
|
+
_reconI16x16(cabac, mbX, mbY, mt, sh, qp, cbpL, cbpC) {
|
|
594
|
+
const predMode = i16x16TypeMap[this._i16idx(mt, sh)][0];
|
|
595
|
+
const { above, left, aboveLeft, hasAbove, hasLeft } = this._neighbors16(mbX, mbY);
|
|
596
|
+
const pred = intra16x16Predict(predMode, above, left, aboveLeft, hasAbove, hasLeft);
|
|
597
|
+
|
|
598
|
+
// Decode luma DC (Hadamard)
|
|
599
|
+
const mbIdx = mbY * this.mbW + mbX;
|
|
600
|
+
const dcCoeffs = this._decodeResidualBlock(cabac, 0, mbIdx, -1);
|
|
601
|
+
const dcTrans = inverseHadamard4x4(dcCoeffs);
|
|
602
|
+
|
|
603
|
+
// Decode luma AC + reconstruct each 4x4 block
|
|
604
|
+
for (let blk = 0; blk < 16; blk++) {
|
|
605
|
+
const bx = (blk & 3) * 4;
|
|
606
|
+
const by = (blk >> 2) * 4;
|
|
607
|
+
|
|
608
|
+
// 8x8 block index for CBP
|
|
609
|
+
const cbpIdx = ((by >> 3) << 1) | (bx >> 3);
|
|
610
|
+
|
|
611
|
+
const residual = new Int32Array(16);
|
|
612
|
+
// DC from Hadamard (the qp scaling for DC is different)
|
|
613
|
+
const qpDiv6 = (qp / 6) | 0;
|
|
614
|
+
if (qpDiv6 >= 2) {
|
|
615
|
+
residual[0] = (dcTrans[blk] * this._levelScale(qp, 0)) << (qpDiv6 - 2);
|
|
616
|
+
} else {
|
|
617
|
+
residual[0] = (dcTrans[blk] * this._levelScale(qp, 0) + (1 << (1 - qpDiv6))) >> (2 - qpDiv6);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (cbpL & (1 << cbpIdx)) {
|
|
621
|
+
const ac = this._decodeResidualBlock(cabac, 1, mbIdx, blk);
|
|
622
|
+
// Dequantize AC (positions 1-15)
|
|
623
|
+
const qpMod6 = qp % 6;
|
|
624
|
+
const ls = [10, 11, 13, 14, 16, 18][qpMod6];
|
|
625
|
+
for (let i = 1; i < 16; i++) {
|
|
626
|
+
if (ac[i] !== 0) {
|
|
627
|
+
const scale = this._levelScale(qp, i);
|
|
628
|
+
if (qpDiv6 >= 4) {
|
|
629
|
+
residual[i] = (ac[i] * scale) << (qpDiv6 - 4);
|
|
630
|
+
} else {
|
|
631
|
+
residual[i] = (ac[i] * scale + (1 << (3 - qpDiv6))) >> (4 - qpDiv6);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const decoded = inverseDCT4x4(residual);
|
|
638
|
+
|
|
639
|
+
for (let y = 0; y < 4; y++) {
|
|
640
|
+
for (let x = 0; x < 4; x++) {
|
|
641
|
+
const px = mbX * 16 + bx + x;
|
|
642
|
+
const py = mbY * 16 + by + y;
|
|
643
|
+
this.frame.Y[py * this.frame.strideY + px] =
|
|
644
|
+
clip255(pred[(by + y) * 16 + bx + x] + decoded[y * 4 + x]);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
_levelScale(qp, scanPos) {
|
|
651
|
+
const scales = [
|
|
652
|
+
[10, 13, 10, 13, 13, 16, 13, 16, 10, 13, 10, 13, 13, 16, 13, 16],
|
|
653
|
+
[11, 14, 11, 14, 14, 18, 14, 18, 11, 14, 11, 14, 14, 18, 14, 18],
|
|
654
|
+
[13, 16, 13, 16, 16, 20, 16, 20, 13, 16, 13, 16, 16, 20, 16, 20],
|
|
655
|
+
[14, 18, 14, 18, 18, 23, 18, 23, 14, 18, 14, 18, 18, 23, 18, 23],
|
|
656
|
+
[16, 20, 16, 20, 20, 25, 20, 25, 16, 20, 16, 20, 20, 25, 20, 25],
|
|
657
|
+
[18, 23, 18, 23, 23, 29, 23, 29, 18, 23, 18, 23, 23, 29, 23, 29],
|
|
658
|
+
];
|
|
659
|
+
return scales[qp % 6][scanPos % 16];
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// ── Reconstruction: I_NxN ───────────────────────────────
|
|
663
|
+
|
|
664
|
+
_reconINxN(cabac, mbX, mbY, qp, cbpL, cbpC) {
|
|
665
|
+
const mbIdx = mbY * this.mbW + mbX;
|
|
666
|
+
for (let blk = 0; blk < 16; blk++) {
|
|
667
|
+
const bx = (blk & 3) * 4;
|
|
668
|
+
const by = (blk >> 2) * 4;
|
|
669
|
+
|
|
670
|
+
// Get neighboring samples for this 4x4 block
|
|
671
|
+
const above = new Int32Array(8);
|
|
672
|
+
const left = new Int32Array(4);
|
|
673
|
+
let aL = 128;
|
|
674
|
+
const hA = mbY > 0 || by > 0;
|
|
675
|
+
const hL = mbX > 0 || bx > 0;
|
|
676
|
+
|
|
677
|
+
if (hA) for (let i = 0; i < 8; i++) above[i] = this.frame.getY(mbX * 16 + bx + i, mbY * 16 + by - 1);
|
|
678
|
+
if (hL) for (let i = 0; i < 4; i++) left[i] = this.frame.getY(mbX * 16 + bx - 1, mbY * 16 + by + i);
|
|
679
|
+
if (hA && hL) aL = this.frame.getY(mbX * 16 + bx - 1, mbY * 16 + by - 1);
|
|
680
|
+
|
|
681
|
+
const pred = intra4x4Predict(2, above, left, aL, hA, hL, hA); // DC mode as default
|
|
682
|
+
|
|
683
|
+
const cbpIdx = ((by >> 3) << 1) | (bx >> 3);
|
|
684
|
+
let decoded = null;
|
|
685
|
+
if (cbpL & (1 << cbpIdx)) {
|
|
686
|
+
const coeffs = this._decodeResidualBlock(cabac, 2, mbIdx, blk);
|
|
687
|
+
const dequant = dequantize4x4(coeffs, qp, true);
|
|
688
|
+
decoded = inverseDCT4x4(dequant);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
for (let y = 0; y < 4; y++) {
|
|
692
|
+
for (let x = 0; x < 4; x++) {
|
|
693
|
+
const px = mbX * 16 + bx + x;
|
|
694
|
+
const py = mbY * 16 + by + y;
|
|
695
|
+
const val = pred[y * 4 + x] + (decoded ? decoded[y * 4 + x] : 0);
|
|
696
|
+
this.frame.Y[py * this.frame.strideY + px] = clip255(val);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// ── Reconstruction: Inter ───────────────────────────────
|
|
703
|
+
|
|
704
|
+
_reconInter(cabac, mbX, mbY, qp, cbpL, cbpC) {
|
|
705
|
+
// Residual over motion-compensated prediction (already written by _decodeInterPred)
|
|
706
|
+
const mbIdx = mbY * this.mbW + mbX;
|
|
707
|
+
if (cbpL > 0) {
|
|
708
|
+
for (let blk = 0; blk < 16; blk++) {
|
|
709
|
+
const bx = (blk & 3) * 4;
|
|
710
|
+
const by = (blk >> 2) * 4;
|
|
711
|
+
const cbpIdx = ((by >> 3) << 1) | (bx >> 3);
|
|
712
|
+
if (cbpL & (1 << cbpIdx)) {
|
|
713
|
+
const coeffs = this._decodeResidualBlock(cabac, 2, mbIdx, blk);
|
|
714
|
+
const dequant = dequantize4x4(coeffs, qp, false);
|
|
715
|
+
const decoded = inverseDCT4x4(dequant);
|
|
716
|
+
for (let y = 0; y < 4; y++) {
|
|
717
|
+
for (let x = 0; x < 4; x++) {
|
|
718
|
+
const px = mbX * 16 + bx + x;
|
|
719
|
+
const py = mbY * 16 + by + y;
|
|
720
|
+
const idx = py * this.frame.strideY + px;
|
|
721
|
+
this.frame.Y[idx] = clip255(this.frame.Y[idx] + decoded[y * 4 + x]);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// ── Chroma reconstruction ───────────────────────────────
|
|
730
|
+
|
|
731
|
+
_reconChroma(cabac, mbX, mbY, qp, cbpC, isIntra, chromaMode) {
|
|
732
|
+
const mbIdx = mbY * this.mbW + mbX;
|
|
733
|
+
const qpC = this._chromaQP(qp + this.pps.chroma_qp_index_offset);
|
|
734
|
+
|
|
735
|
+
// Intra chroma prediction
|
|
736
|
+
if (isIntra) {
|
|
737
|
+
for (let comp = 0; comp < 2; comp++) {
|
|
738
|
+
const plane = comp === 0 ? this.frame.U : this.frame.V;
|
|
739
|
+
const getC = comp === 0 ?
|
|
740
|
+
(x, y) => this.frame.getU(x, y) :
|
|
741
|
+
(x, y) => this.frame.getV(x, y);
|
|
742
|
+
|
|
743
|
+
const above = new Uint8Array(8);
|
|
744
|
+
const left = new Uint8Array(8);
|
|
745
|
+
let aL = 128;
|
|
746
|
+
const hA = mbY > 0;
|
|
747
|
+
const hL = mbX > 0;
|
|
748
|
+
if (hA) for (let i = 0; i < 8; i++) above[i] = getC(mbX * 8 + i, mbY * 8 - 1);
|
|
749
|
+
if (hL) for (let i = 0; i < 8; i++) left[i] = getC(mbX * 8 - 1, mbY * 8 + i);
|
|
750
|
+
if (hA && hL) aL = getC(mbX * 8 - 1, mbY * 8 - 1);
|
|
751
|
+
|
|
752
|
+
const pred = intraChromaPredict(chromaMode, above, left, aL, hA, hL);
|
|
753
|
+
for (let y = 0; y < 8; y++)
|
|
754
|
+
for (let x = 0; x < 8; x++)
|
|
755
|
+
plane[(mbY * 8 + y) * this.frame.strideC + mbX * 8 + x] = clip255(pred[y * 8 + x]);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
if (cbpC === 0) return;
|
|
760
|
+
|
|
761
|
+
// Chroma DC + AC
|
|
762
|
+
for (let comp = 0; comp < 2; comp++) {
|
|
763
|
+
const plane = comp === 0 ? this.frame.U : this.frame.V;
|
|
764
|
+
const dc = this._decodeResidualBlock(cabac, 3, mbIdx, 16 + comp * 4);
|
|
765
|
+
const dcT = inverseHadamard2x2(dc);
|
|
766
|
+
|
|
767
|
+
for (let blk = 0; blk < 4; blk++) {
|
|
768
|
+
const bx = (blk & 1) * 4;
|
|
769
|
+
const by = (blk >> 1) * 4;
|
|
770
|
+
|
|
771
|
+
const residual = new Int32Array(16);
|
|
772
|
+
// DC from Hadamard with chroma QP scaling
|
|
773
|
+
const qpDiv6 = (qpC / 6) | 0;
|
|
774
|
+
const qpMod6 = qpC % 6;
|
|
775
|
+
const dcScale = [10, 11, 13, 14, 16, 18][qpMod6];
|
|
776
|
+
if (qpDiv6 >= 1) {
|
|
777
|
+
residual[0] = (dcT[blk] * dcScale) << (qpDiv6 - 1);
|
|
778
|
+
} else {
|
|
779
|
+
residual[0] = (dcT[blk] * dcScale) >> 1;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
if (cbpC === 2) {
|
|
783
|
+
const ac = this._decodeResidualBlock(cabac, 4, mbIdx, 16 + comp * 4 + blk);
|
|
784
|
+
for (let i = 1; i < 16; i++) {
|
|
785
|
+
if (ac[i] !== 0) {
|
|
786
|
+
const scale = this._levelScale(qpC, i);
|
|
787
|
+
if (qpDiv6 >= 4) {
|
|
788
|
+
residual[i] = (ac[i] * scale) << (qpDiv6 - 4);
|
|
789
|
+
} else {
|
|
790
|
+
residual[i] = (ac[i] * scale + (1 << (3 - qpDiv6))) >> (4 - qpDiv6);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const decoded = inverseDCT4x4(residual);
|
|
797
|
+
for (let y = 0; y < 4; y++) {
|
|
798
|
+
for (let x = 0; x < 4; x++) {
|
|
799
|
+
const px = mbX * 8 + bx + x;
|
|
800
|
+
const py = mbY * 8 + by + y;
|
|
801
|
+
const idx = py * this.frame.strideC + px;
|
|
802
|
+
plane[idx] = clip255(plane[idx] + decoded[y * 4 + x]);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// ── Inter prediction stubs ──────────────────────────────
|
|
810
|
+
|
|
811
|
+
_decodeInterPred(cabac, sh, mt, mbIdx, mbX, mbY, refL0, refL1) {
|
|
812
|
+
// For now: decode the CABAC syntax elements (ref_idx, mvd) to keep
|
|
813
|
+
// the CABAC state correct, then use zero-motion from refL0[0].
|
|
814
|
+
|
|
815
|
+
const isP = sh.isP;
|
|
816
|
+
const base = isP ? 0 : 23;
|
|
817
|
+
const adjMt = isP ? mt : mt;
|
|
818
|
+
|
|
819
|
+
// Copy reference frame block with zero motion
|
|
820
|
+
const ref = refL0.length > 0 ? refL0[0] : null;
|
|
821
|
+
if (ref) {
|
|
822
|
+
for (let y = 0; y < 16; y++)
|
|
823
|
+
for (let x = 0; x < 16; x++)
|
|
824
|
+
this.frame.Y[(mbY * 16 + y) * this.frame.strideY + mbX * 16 + x] = ref.getY(mbX * 16 + x, mbY * 16 + y);
|
|
825
|
+
for (let y = 0; y < 8; y++)
|
|
826
|
+
for (let x = 0; x < 8; x++) {
|
|
827
|
+
this.frame.U[(mbY * 8 + y) * this.frame.strideC + mbX * 8 + x] = ref.getU(mbX * 8 + x, mbY * 8 + y);
|
|
828
|
+
this.frame.V[(mbY * 8 + y) * this.frame.strideC + mbX * 8 + x] = ref.getV(mbX * 8 + x, mbY * 8 + y);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// TODO: properly decode ref_idx and mvd to keep CABAC in sync
|
|
833
|
+
// For P_L0_16x16: 1 ref_idx + 2 mvd components
|
|
834
|
+
// For B types: varies
|
|
835
|
+
// Without proper inter CABAC syntax parsing, the CABAC state will
|
|
836
|
+
// desync on P/B frames. This is acceptable for IDR-only testing.
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
_reconstructSkip(mbX, mbY, refL0, sh) {
|
|
840
|
+
const ref = refL0.length > 0 ? refL0[0] : null;
|
|
841
|
+
if (ref) {
|
|
842
|
+
for (let y = 0; y < 16; y++)
|
|
843
|
+
for (let x = 0; x < 16; x++)
|
|
844
|
+
this.frame.Y[(mbY * 16 + y) * this.frame.strideY + mbX * 16 + x] = ref.getY(mbX * 16 + x, mbY * 16 + y);
|
|
845
|
+
for (let y = 0; y < 8; y++)
|
|
846
|
+
for (let x = 0; x < 8; x++) {
|
|
847
|
+
this.frame.U[(mbY * 8 + y) * this.frame.strideC + mbX * 8 + x] = ref.getU(mbX * 8 + x, mbY * 8 + y);
|
|
848
|
+
this.frame.V[(mbY * 8 + y) * this.frame.strideC + mbX * 8 + x] = ref.getV(mbX * 8 + x, mbY * 8 + y);
|
|
849
|
+
}
|
|
850
|
+
} else {
|
|
851
|
+
this._fillGray(mbX, mbY);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// ── Deblocking filter (simplified) ──────────────────────
|
|
856
|
+
|
|
857
|
+
_deblock(sh) {
|
|
858
|
+
// Simplified deblocking: apply mild smoothing at MB boundaries
|
|
859
|
+
// TODO: Full H.264 deblocking per Section 8.7
|
|
860
|
+
const f = this.frame;
|
|
861
|
+
for (let mbY = 0; mbY < this.mbH; mbY++) {
|
|
862
|
+
for (let mbX = 0; mbX < this.mbW; mbX++) {
|
|
863
|
+
// Vertical edge at left MB boundary
|
|
864
|
+
if (mbX > 0) {
|
|
865
|
+
const x = mbX * 16;
|
|
866
|
+
for (let y = 0; y < 16; y++) {
|
|
867
|
+
const py = mbY * 16 + y;
|
|
868
|
+
const p0 = f.Y[py * f.strideY + x - 1];
|
|
869
|
+
const q0 = f.Y[py * f.strideY + x];
|
|
870
|
+
const d = ((q0 - p0 + 2) >> 2);
|
|
871
|
+
if (Math.abs(d) < 4) {
|
|
872
|
+
f.Y[py * f.strideY + x - 1] = clip255(p0 + d);
|
|
873
|
+
f.Y[py * f.strideY + x] = clip255(q0 - d);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
// Horizontal edge at top MB boundary
|
|
878
|
+
if (mbY > 0) {
|
|
879
|
+
const y = mbY * 16;
|
|
880
|
+
for (let x = 0; x < 16; x++) {
|
|
881
|
+
const px = mbX * 16 + x;
|
|
882
|
+
const p0 = f.Y[(y - 1) * f.strideY + px];
|
|
883
|
+
const q0 = f.Y[y * f.strideY + px];
|
|
884
|
+
const d = ((q0 - p0 + 2) >> 2);
|
|
885
|
+
if (Math.abs(d) < 4) {
|
|
886
|
+
f.Y[(y - 1) * f.strideY + px] = clip255(p0 + d);
|
|
887
|
+
f.Y[y * f.strideY + px] = clip255(q0 - d);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// ── Utilities ───────────────────────────────────────────
|
|
896
|
+
|
|
897
|
+
_neighbors16(mbX, mbY) {
|
|
898
|
+
const f = this.frame;
|
|
899
|
+
const hA = mbY > 0, hL = mbX > 0;
|
|
900
|
+
const above = new Uint8Array(16);
|
|
901
|
+
const left = new Uint8Array(16);
|
|
902
|
+
let aL = 128;
|
|
903
|
+
if (hA) for (let x = 0; x < 16; x++) above[x] = f.Y[(mbY * 16 - 1) * f.strideY + mbX * 16 + x];
|
|
904
|
+
if (hL) for (let y = 0; y < 16; y++) left[y] = f.Y[(mbY * 16 + y) * f.strideY + mbX * 16 - 1];
|
|
905
|
+
if (hA && hL) aL = f.Y[(mbY * 16 - 1) * f.strideY + mbX * 16 - 1];
|
|
906
|
+
return { above, left, aboveLeft: aL, hasAbove: hA, hasLeft: hL };
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
_buildRefList(sh, listIdx) {
|
|
910
|
+
if (listIdx === 0) return [...this.dpb].sort((a, b) => b.frameNum - a.frameNum);
|
|
911
|
+
return [...this.dpb].sort((a, b) => a.poc - b.poc);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
_fillGray(mbX, mbY) {
|
|
915
|
+
for (let y = 0; y < 16; y++)
|
|
916
|
+
for (let x = 0; x < 16; x++)
|
|
917
|
+
this.frame.Y[(mbY * 16 + y) * this.frame.strideY + mbX * 16 + x] = 128;
|
|
918
|
+
for (let y = 0; y < 8; y++)
|
|
919
|
+
for (let x = 0; x < 8; x++) {
|
|
920
|
+
this.frame.U[(mbY * 8 + y) * this.frame.strideC + mbX * 8 + x] = 128;
|
|
921
|
+
this.frame.V[(mbY * 8 + y) * this.frame.strideC + mbX * 8 + x] = 128;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
_chromaQP(qpI) {
|
|
926
|
+
const t = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,
|
|
927
|
+
28,29,29,30,31,32,32,33,33,34,34,35,35,36,36,37,37,37,38,38,38,39,39,39,39];
|
|
928
|
+
return t[Math.max(0, Math.min(51, qpI))];
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
_decodeIPCM(cabac, mbX, mbY) {
|
|
932
|
+
// I_PCM: raw pixel data follows (after byte alignment)
|
|
933
|
+
// The CABAC state is reset after I_PCM
|
|
934
|
+
// For simplicity, fill with mid-gray
|
|
935
|
+
this._fillGray(mbX, mbY);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
export { YUVFrame };
|
|
940
|
+
export default H264Decoder;
|