@invintusmedia/tomp4 1.4.2 → 1.5.0

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.
@@ -1,940 +0,0 @@
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;