Skip to content

Commit 2ee053b

Browse files
committed
INTRA works!
1 parent 7914bfc commit 2ee053b

7 files changed

Lines changed: 524 additions & 59 deletions

File tree

index.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,32 @@
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
77
<title>h261</title>
88
<style>
9+
#app {
10+
width: 704px;
11+
height: 576px;
12+
border: 1px solid red;
13+
position: relative;
14+
}
15+
916
#output {
1017
width: 704px;
1118
height: 576px;
1219
image-rendering: pixelated;
1320
}
21+
22+
img {
23+
position: absolute;
24+
top: 0;
25+
left: 0;
26+
z-index: 100;
27+
}
1428
</style>
1529
</head>
1630
<body>
1731
<div id="app">
1832
<canvas id="output" width="352" height="288">
1933
</canvas>
34+
<img src="mbgrid.svg" alt="">
2035
</div>
2136
<script type="module" src="/src/main.ts"></script>
2237
</body>

public/badapple-2s.h261

5.17 MB
Binary file not shown.

public/mbgrid.svg

Lines changed: 411 additions & 0 deletions
Loading

readme.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# hate 261
2+
3+
Toy h.261 decoder.
4+
I said this was doable in 1 sitting. I WAS WRONG
5+
6+
I got DCT+Quantization working after ~7 sittings...
7+
8+
## Resources
9+
10+
[H.261](https://www.itu.int/rec/T-REC-H.261) - The actual spec, kinda hard to read for a beginner
11+
12+
[FFMPEG](https://git.ffmpeg.org/ffmpeg.git) - An implementation that actually builds (looking at you maikmerten/p64).
13+
14+
Configuring with the following command will give you a minimal h261 ffplay build.
15+
16+
```sh
17+
./configure --enable-debug=3 --disable-ffmpeg --disable-ffprobe --disable-doc --disable-everything --enable-decoder=h261 --enable-parser=h261 --enable-demuxer=h261 --enable-protocol=file --enable-filter=scale
18+
```
19+
20+
[YCbCr on Wikipedia](https://en.wikipedia.org/wiki/YCbCr#ITU-R_BT.601_conversion) - The color model
21+
22+
[Discrete cosine transform on Wikipedia](https://en.wikipedia.org/wiki/Discrete_cosine_transform) - Basically the most important part of the codec
23+
24+
[JPEG on wikipedia](https://en.wikipedia.org/wiki/JPEG#Discrete_cosine_transform) - More DCT material
25+
26+
[High level overview of h261](https://cgg.mff.cuni.cz/~pepca/lectures/pdf/2d-12-h261.pdf) - I wish this was the first thing I read
27+
28+
[Techniques and standards for image, video, and audio coding](https://archive.org/details/techniquesstanda0000raok) - Useful book (you have to create an archive.org account to borrow)

src/dec.ts

Lines changed: 55 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ type MVector = [number, number];
66

77
interface Block {
88
index: number,
9-
data: Uint8Array,
9+
data: Uint8ClampedArray,
1010
}
1111

1212
interface Macroblock {
@@ -35,6 +35,9 @@ function reconstruct(level: number, quant: number) {
3535
return clip(quant * (2 * level + lp) + qe, -2048, 2047);
3636
}
3737

38+
// @ts-ignore
39+
window.reconstruct = reconstruct;
40+
3841
function reconstructDC(level: number): number {
3942
if (level === 0 || level === 128) {
4043
throw "Invalid DC: 0 and 128 are not allowed";
@@ -46,48 +49,55 @@ function reconstructDC(level: number): number {
4649
}
4750

4851
const { SQRT2, PI } = Math;
52+
const ISQ2 = (1 / SQRT2);
53+
54+
function alpha(x: number) {
55+
return x === 0 ? ISQ2 : 1
56+
}
4957

50-
function idct(block: Uint8Array) {
51-
const result = new Uint8Array(64);
52-
53-
// for (let x = 0; x < 8; x++) {
54-
// for (let y = 0; y < 8; y++) {
55-
// let sum = 0;
56-
// for (let u = 0; u < 8; u++) {
57-
// for (let v = 0; v < 8; v++) {
58-
// const Cu = u === 0 ? 1 / SQRT2 : 1;
59-
// const Cv = v === 0 ? 1 / SQRT2 : 1;
60-
// const dctCoeff = block[u + v * 8];
61-
// sum += Cu * Cv * dctCoeff *
62-
// Math.cos(((2 * x + 1) * u * PI) / 16) *
63-
// Math.cos(((2 * y + 1) * v * PI) / 16);
64-
// }
65-
// }
66-
// result[x + y * 8] = (1 / 4) * sum;
67-
// }
68-
// }
58+
export function idct(block: Int16Array) {
59+
const result = new Uint8ClampedArray(64);
6960

7061
for (let y = 0; y < 8; y++) {
7162
for (let x = 0; x < 8; x++) {
7263
let sum = 0;
73-
for (let u = 0; u < 8; u++) {
74-
for (let v = 0; v < 8; v++) {
75-
const Cu = u === 0 ? 1 / SQRT2 : 1;
76-
const Cv = v === 0 ? 1 / SQRT2 : 1;
77-
const index = u * 8 + v;
78-
const dctCoeff = block[index];
79-
sum += Cu * Cv * dctCoeff *
80-
Math.cos(((2 * x + 1) * u * PI) / 16) *
81-
Math.cos(((2 * y + 1) * v * PI) / 16);
64+
65+
for (let v = 0; v < 8; v++) {
66+
for (let u = 0; u < 8; u++) {
67+
68+
sum += alpha(u) * alpha(v) * block[u + v * 8] *
69+
Math.cos((PI * (2 * x + 1) * u) / 16) *
70+
Math.cos((PI * (2 * y + 1) * v) / 16);
8271
}
8372
}
84-
const alpha = (x === 0 ? 1 / SQRT2 : 1) * (y === 0 ? 1 / SQRT2 : 1);
85-
result[x * 8 + y] = (1 / 4) * alpha * sum;
73+
result[y * 8 + x] = (sum / 4);
8674
}
8775
}
8876
return result;
8977
}
9078

79+
export function dct(block: Uint8Array) {
80+
const result = new Int16Array(64);
81+
for (let v = 0; v < 8; v++) {
82+
for (let u = 0; u < 8; u++) {
83+
let sum = 0;
84+
85+
for (let y = 0; y < 8; y++) {
86+
for (let x = 0; x < 8; x++) {
87+
sum += block[x + y * 8]
88+
* Math.cos((PI * (2 * x + 1) * u) / 16)
89+
* Math.cos((PI * (2 * y + 1) * v) / 16);
90+
}
91+
}
92+
93+
result[u + v * 8] = (sum / 4) * alpha(u) * alpha(v);
94+
}
95+
}
96+
97+
return result;
98+
}
99+
100+
91101
function writeRgb(dst: Uint8ClampedArray, index: number, y: number, u: number, v: number) {
92102
// https://en.wikipedia.org/wiki/YCbCr#ITU-R_BT.601_conversion
93103
dst[index + 0] = (298.082 * y + 408.583 * u) / 256 - 222.921;
@@ -96,15 +106,16 @@ function writeRgb(dst: Uint8ClampedArray, index: number, y: number, u: number, v
96106
dst[index + 3] = 255;
97107
}
98108

99-
function getMacroblockImageDataFromYuv(data: Uint8Array[]) {
100-
const cCb = data[4], cCr = data[5];
109+
function getMacroblockImageDataFromYuv(data: Uint8ClampedArray[]) {
110+
const cCb = data[5], cCr = data[4];
101111

102112
const id = new ImageData(16, 16, { colorSpace: 'srgb' });
103113

104114
for (let i = 0; i < 4; i++) {
105115
const cY = data[i];
106116
const xa = i & 1 ? 8 : 0;
107117
const ya = i & 2 ? 8 : 0;
118+
108119
for (let x = xa; x < xa + 8; x++) {
109120
for (let y = ya; y < ya + 8; y++) {
110121
const cyi = (x & 7) + 8 * (y & 7);
@@ -172,7 +183,7 @@ export class Frame {
172183
}
173184

174185
const mtype = h261.MTYPE[this.br.countLeadingZeroes()];
175-
console.log(this.mba, h261.getMtypeString(mtype));
186+
// console.log(this.mba, h261.getMtypeString(mtype));
176187

177188
const mb: Macroblock = {
178189
address: this.mba,
@@ -195,13 +206,11 @@ export class Frame {
195206
}
196207

197208
const mvd1 = this.br.readVlcOr(vlc.MVD_TREE, -1);
198-
/*
199-
TODO(mbabnik): track motion vectors; and if we should do the diff
200-
thing, since we then read an extra bit?
201-
*/
209+
//TODO(mbabnik): motion vectors
202210
const mvd2 = this.br.readVlcOr(vlc.MVD_TREE, -1);
203211

204-
console.log({ previousMv, mvd1, mvd2 })
212+
void mvd1;
213+
void mvd2;
205214

206215
mb.mvd = [0, 0];
207216
}
@@ -216,7 +225,7 @@ export class Frame {
216225
}
217226
mb.cbp = cbp;
218227

219-
const tmpBlock = new Uint8Array(64);
228+
const tmpBlock = new Int16Array(64);
220229

221230
// for each block (in a macroblock)
222231
block: for (let i = 0; i < 6; i++) {
@@ -226,10 +235,10 @@ export class Frame {
226235
const coded = !!(cbp & (1 << (5 - i)));
227236

228237
if (!(mtype.prediction & h261.INTER_BIT)) { // INTRA
229-
const dcCoefLvl = (this.br.readInt(8) << 24) >> 24; // sign extend i8
238+
const dcCoefLvl = this.br.readInt(8);
230239
tmpBlock[0] = reconstructDC(dcCoefLvl);
231240
j = 1;
232-
} else if (coded) {
241+
} else if (coded) {
233242
const check = this.br.peekInt(2);
234243
if (check & 0x2) {
235244
this.br.readInt(2);
@@ -267,9 +276,9 @@ export class Frame {
267276
reconstruct(level, mb.mquant ?? gquant);
268277
}
269278
}
270-
271279
mb.blocks[i] = { data: idct(tmpBlock), index: i };
272280
}
281+
// throw "time to throw pogchamp";
273282
}
274283
return mb;
275284
}
@@ -289,7 +298,7 @@ export class Frame {
289298
console.log('Discarding extra byte:', this.br.readInt(8))
290299
}
291300

292-
console.groupCollapsed(`GOB ${groupNumber}`)
301+
// console.groupCollapsed(`GOB ${groupNumber}`)
293302

294303
// console.log({ groupNumber, gquant, at: this.br.at.toString(16) })
295304
const macroblocks = [];
@@ -317,7 +326,7 @@ export class Frame {
317326
}
318327

319328
protected read() {
320-
console.groupCollapsed(`frame ${this.frameNumber}`)
329+
// console.groupCollapsed(`frame ${this.frameNumber}`)
321330

322331
while (this.br.peekInt(20) != h261.PSC) {
323332
this.br.readInt(1);
@@ -345,7 +354,7 @@ export class Frame {
345354
this.gobs[i] = this.readGob(i);
346355
}
347356

348-
console.groupEnd();
357+
// console.groupEnd();
349358
}
350359

351360

src/h261def.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,12 @@ export const MTYPE: Record<number, MType> = {
6363
}
6464

6565
export const TCOEFF_REORDER = [
66-
0, 1, 5, 6, 14, 15, 27, 28,
67-
2, 4, 7, 13, 16, 26, 29, 42,
68-
3, 8, 12, 17, 25, 30, 41, 43,
69-
9, 11, 18, 24, 31, 40, 44, 53,
70-
10, 19, 23, 32, 39, 45, 52, 54,
71-
20, 22, 33, 38, 46, 51, 55, 60,
72-
21, 34, 37, 47, 50, 56, 59, 61,
73-
35, 36, 48, 49, 57, 58, 62, 63
74-
];
66+
0, 1, 8, 16, 9, 2, 3, 10,
67+
17, 24, 32, 25, 18, 11, 4, 5,
68+
12, 19, 26, 33, 40, 48, 41, 34,
69+
27, 20, 13, 6, 7, 14, 21, 28,
70+
35, 42, 49, 56, 57, 50, 43, 36,
71+
29, 22, 15, 23, 30, 37, 44, 51,
72+
58, 59, 52, 45, 38, 31, 39, 46,
73+
53, 60, 61, 54, 47, 55, 62, 63
74+
]

src/main.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { BitReader } from "./BitReader";
2-
// import "./treegen"
32
import { Frame } from "./dec";
43

54
const canvas = document.getElementById('output') as HTMLCanvasElement;
@@ -19,16 +18,19 @@ function canvasCheckerboard() {
1918
}
2019

2120
async function main() {
22-
// const res = await fetch("badapple.h261");
21+
// const res = await fetch("badapple-2s.h261");
2322
const res = await fetch("rickroll.h261");
2423
const buf = await res.arrayBuffer();
2524

2625
const br = new BitReader(buf);
27-
26+
2827
let previousFrame: Frame | undefined = undefined;
29-
for (let i = 0; i < 100; i++) {
28+
for (let i = 0; i < 1; i++) {
29+
console.time('frame')
3030
const fr: Frame = new Frame(br, previousFrame, i);
31+
console.log(fr);
3132
fr.paint(g);
33+
console.timeEnd('frame')
3234
previousFrame = fr;
3335
}
3436
}

0 commit comments

Comments
 (0)