Skip to content

Commit c1bba97

Browse files
committed
Add unit tests for H.264 decoder
1 parent d106b7a commit c1bba97

2 files changed

Lines changed: 266 additions & 2 deletions

File tree

core/decoders/h264.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import * as Log from '../util/logging.js';
1111

12-
class H264Parser {
12+
export class H264Parser {
1313
constructor(data) {
1414
this._data = data;
1515
this._index = 0;
@@ -109,7 +109,7 @@ class H264Parser {
109109
}
110110
}
111111

112-
class H264Context {
112+
export class H264Context {
113113
constructor(width, height) {
114114
this.lastUsed = 0;
115115
this._width = width;

tests/test.h264.js

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
import Websock from '../core/websock.js';
2+
import Display from '../core/display.js';
3+
4+
import { H264Parser } from '../core/decoders/h264.js';
5+
import H264Decoder from '../core/decoders/h264.js';
6+
import Base64 from '../core/base64.js';
7+
8+
import FakeWebSocket from './fake.websocket.js';
9+
10+
/* This is a 3 frame 16x16 video where the first frame is solid red, the second
11+
* is solid green and the third is solid blue.
12+
*
13+
* The colour space is BT.709. It is encoded into the stream.
14+
*/
15+
const redGreenBlue16x16Video = new Uint8Array(Base64.decode(
16+
'AAAAAWdCwBTZnpuAgICgAAADACAAAAZB4oVNAAAAAWjJYyyAAAABBgX//4HcRem95tlIt5Ys' +
17+
'2CDZI+7veDI2NCAtIGNvcmUgMTY0IHIzMTA4IDMxZTE5ZjkgLSBILjI2NC9NUEVHLTQgQVZD' +
18+
'IGNvZGVjIC0gQ29weWxlZnQgMjAwMy0yMDIzIC0gaHR0cDovL3d3dy52aWRlb2xhbi5vcmcv' +
19+
'eDI2NC5odG1sIC0gb3B0aW9uczogY2FiYWM9MCByZWY9NSBkZWJsb2NrPTE6MDowIGFuYWx5' +
20+
'c2U9MHgxOjB4MTExIG1lPWhleCBzdWJtZT04IHBzeT0xIHBzeV9yZD0xLjAwOjAuMDAgbWl4' +
21+
'ZWRfcmVmPTEgbWVfcmFuZ2U9MTYgY2hyb21hX21lPTEgdHJlbGxpcz0yIDh4OGRjdD0wIGNx' +
22+
'bT0wIGRlYWR6b25lPTIxLDExIGZhc3RfcHNraXA9MSBjaHJvbWFfcXBfb2Zmc2V0PS0yIHRo' +
23+
'cmVhZHM9MSBsb29rYWhlYWRfdGhyZWFkcz0xIHNsaWNlZF90aHJlYWRzPTAgbnI9MCBkZWNp' +
24+
'bWF0ZT0xIGludGVybGFjZWQ9MCBibHVyYXlfY29tcGF0PTAgY29uc3RyYWluZWRfaW50cmE9' +
25+
'MCBiZnJhbWVzPTAgd2VpZ2h0cD0wIGtleWludD1pbmZpbml0ZSBrZXlpbnRfbWluPTI1IHNj' +
26+
'ZW5lY3V0PTQwIGludHJhX3JlZnJlc2g9MCByY19sb29rYWhlYWQ9NTAgcmM9YWJyIG1idHJl' +
27+
'ZT0xIGJpdHJhdGU9NDAwIHJhdGV0b2w9MS4wIHFjb21wPTAuNjAgcXBtaW49MCBxcG1heD02' +
28+
'OSBxcHN0ZXA9NCBpcF9yYXRpbz0xLjQwIGFxPTE6MS4wMACAAAABZYiEBrxmKAAPVccAAS04' +
29+
'4AA5DRJMnkycJk4TPwAAAAFBiIga8RigADVVHAAGaGOAANtuAAAAAUGIkBr///wRRQABVf8c' +
30+
'AAcho4AAiD4='));
31+
32+
let _haveH264Decode = null;
33+
34+
async function haveH264Decode() {
35+
if (_haveH264Decode !== null) {
36+
return _haveH264Decode;
37+
}
38+
39+
if (!('VideoDecoder' in window)) {
40+
_haveH264Decode = false;
41+
return false;
42+
}
43+
44+
// We'll need to make do with some placeholders here
45+
const config = {
46+
codec: 'avc1.42401f',
47+
codedWidth: 1920,
48+
codedHeight: 1080,
49+
optimizeForLatency: true,
50+
};
51+
52+
_haveH264Decode = await VideoDecoder.isConfigSupported(config);
53+
return _haveH264Decode;
54+
}
55+
56+
function createSolidColorFrameBuffer(color, width, height) {
57+
const r = (color >> 24) & 0xff;
58+
const g = (color >> 16) & 0xff;
59+
const b = (color >> 8) & 0xff;
60+
const a = (color >> 0) & 0xff;
61+
62+
const size = width * height * 4;
63+
let array = new Uint8ClampedArray(size);
64+
65+
for (let i = 0; i < size / 4; ++i) {
66+
array[i * 4 + 0] = r;
67+
array[i * 4 + 1] = g;
68+
array[i * 4 + 2] = b;
69+
array[i * 4 + 3] = a;
70+
}
71+
72+
return array;
73+
}
74+
75+
function makeMessageHeader(length, resetContext, resetAllContexts) {
76+
let flags = 0;
77+
if (resetContext) {
78+
flags |= 1;
79+
}
80+
if (resetAllContexts) {
81+
flags |= 2;
82+
}
83+
84+
let header = new Uint8Array(8);
85+
let i = 0;
86+
87+
let appendU32 = (v) => {
88+
header[i++] = (v >> 24) & 0xff;
89+
header[i++] = (v >> 16) & 0xff;
90+
header[i++] = (v >> 8) & 0xff;
91+
header[i++] = v & 0xff;
92+
};
93+
94+
appendU32(length);
95+
appendU32(flags);
96+
97+
return header;
98+
}
99+
100+
function wrapRectData(data, resetContext, resetAllContexts) {
101+
let header = makeMessageHeader(data.length, resetContext, resetAllContexts);
102+
return Array.from(header).concat(Array.from(data));
103+
}
104+
105+
function testDecodeRect(decoder, x, y, width, height, data, display, depth) {
106+
let sock;
107+
let done = false;
108+
109+
sock = new Websock;
110+
sock.open("ws://example.com");
111+
112+
sock.on('message', () => {
113+
done = decoder.decodeRect(x, y, width, height, sock, display, depth);
114+
});
115+
116+
// Empty messages are filtered at multiple layers, so we need to
117+
// do a direct call
118+
if (data.length === 0) {
119+
done = decoder.decodeRect(x, y, width, height, sock, display, depth);
120+
} else {
121+
sock._websocket._receiveData(new Uint8Array(data));
122+
}
123+
124+
display.flip();
125+
126+
return done;
127+
}
128+
129+
function almost(a, b) {
130+
let diff = Math.abs(a - b);
131+
return diff < 5;
132+
}
133+
134+
describe('H.264 Parser', function () {
135+
it('should parse constrained baseline video', function () {
136+
let parser = new H264Parser(redGreenBlue16x16Video);
137+
138+
let frame = parser.parse();
139+
expect(frame).to.have.property('key', true);
140+
141+
expect(parser).to.have.property('profileIdc', 66);
142+
expect(parser).to.have.property('constraintSet', 192);
143+
expect(parser).to.have.property('levelIdc', 20);
144+
145+
frame = parser.parse();
146+
expect(frame).to.have.property('key', false);
147+
148+
frame = parser.parse();
149+
expect(frame).to.have.property('key', false);
150+
151+
frame = parser.parse();
152+
expect(frame).to.be.null;
153+
});
154+
});
155+
156+
describe('H.264 Decoder Unit Test', function () {
157+
let decoder;
158+
159+
beforeEach(async function () {
160+
if (!await haveH264Decode()) {
161+
this.skip();
162+
return;
163+
}
164+
decoder = new H264Decoder();
165+
});
166+
167+
it('creates and resets context', function () {
168+
let context = decoder._getContext(1, 2, 3, 4);
169+
expect(context._width).to.equal(3);
170+
expect(context._height).to.equal(4);
171+
expect(decoder._contexts).to.not.be.empty;
172+
decoder._resetContext(1, 2, 3, 4);
173+
expect(decoder._contexts).to.be.empty;
174+
});
175+
176+
it('resets all contexts', function () {
177+
decoder._getContext(0, 0, 1, 1);
178+
decoder._getContext(2, 2, 1, 1);
179+
expect(decoder._contexts).to.not.be.empty;
180+
decoder._resetAllContexts();
181+
expect(decoder._contexts).to.be.empty;
182+
});
183+
184+
it('caches contexts', function () {
185+
let c1 = decoder._getContext(1, 2, 3, 4);
186+
c1.lastUsed = 1;
187+
let c2 = decoder._getContext(1, 2, 3, 4);
188+
c2.lastUsed = 2;
189+
expect(Object.keys(decoder._contexts).length).to.equal(1);
190+
expect(c1.lastUsed).to.equal(c2.lastUsed);
191+
});
192+
193+
it('deletes oldest context', function () {
194+
for (let i = 0; i < 65; ++i) {
195+
let context = decoder._getContext(i, 0, 1, 1);
196+
context.lastUsed = i;
197+
}
198+
199+
expect(decoder._findOldestContextId()).to.equal('1,0,1,1');
200+
expect(decoder._contexts[decoder._contextId(0, 0, 1, 1)]).to.be.undefined;
201+
expect(decoder._contexts[decoder._contextId(1, 0, 1, 1)]).to.not.be.null;
202+
expect(decoder._contexts[decoder._contextId(63, 0, 1, 1)]).to.not.be.null;
203+
expect(decoder._contexts[decoder._contextId(64, 0, 1, 1)]).to.not.be.null;
204+
});
205+
});
206+
207+
describe('H.264 Decoder Functional Test', function () {
208+
let decoder;
209+
let display;
210+
211+
before(FakeWebSocket.replace);
212+
after(FakeWebSocket.restore);
213+
214+
beforeEach(async function () {
215+
if (!await haveH264Decode()) {
216+
this.skip();
217+
return;
218+
}
219+
decoder = new H264Decoder();
220+
display = new Display(document.createElement('canvas'));
221+
display.resize(16, 16);
222+
});
223+
224+
it('should handle H.264 rect', async function () {
225+
let data = wrapRectData(redGreenBlue16x16Video, false, false);
226+
let done = testDecodeRect(decoder, 0, 0, 16, 16, data, display, 24);
227+
expect(done).to.be.true;
228+
await display.flush();
229+
let targetData = createSolidColorFrameBuffer(0x0000ffff, 16, 16);
230+
expect(display).to.have.displayed(targetData, almost);
231+
});
232+
233+
it('should handle specific context reset', async function () {
234+
let data = wrapRectData(redGreenBlue16x16Video, false, false);
235+
let done = testDecodeRect(decoder, 0, 0, 16, 16, data, display, 24);
236+
expect(done).to.be.true;
237+
await display.flush();
238+
let targetData = createSolidColorFrameBuffer(0x0000ffff, 16, 16);
239+
expect(display).to.have.displayed(targetData, almost);
240+
241+
data = wrapRectData([], true, false);
242+
done = testDecodeRect(decoder, 0, 0, 16, 16, data, display, 24);
243+
expect(done).to.be.true;
244+
await display.flush();
245+
246+
expect(decoder._contexts[decoder._contextId(0, 0, 16, 16)]._decoder).to.be.null;
247+
});
248+
249+
it('should handle global context reset', async function () {
250+
let data = wrapRectData(redGreenBlue16x16Video, false, false);
251+
let done = testDecodeRect(decoder, 0, 0, 16, 16, data, display, 24);
252+
expect(done).to.be.true;
253+
await display.flush();
254+
let targetData = createSolidColorFrameBuffer(0x0000ffff, 16, 16);
255+
expect(display).to.have.displayed(targetData, almost);
256+
257+
data = wrapRectData([], false, true);
258+
done = testDecodeRect(decoder, 0, 0, 16, 16, data, display, 24);
259+
expect(done).to.be.true;
260+
await display.flush();
261+
262+
expect(decoder._contexts[decoder._contextId(0, 0, 16, 16)]._decoder).to.be.null;
263+
});
264+
});

0 commit comments

Comments
 (0)