Skip to content

Commit fe455a1

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

File tree

2 files changed

+266
-2
lines changed

2 files changed

+266
-2
lines changed

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, H264Context } 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+
'AAAAAWdCwBTZnpqAgICgAAADACAAAAZB4oVNAAAAAWjJYyyAAAABBgX//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+
function createSolidColorFrameBuffer(color, width, height) {
33+
const r = (color >> 24) & 0xff;
34+
const g = (color >> 16) & 0xff;
35+
const b = (color >> 8) & 0xff;
36+
const a = (color >> 0) & 0xff;
37+
38+
const size = width * height * 4;
39+
let array = new Uint8ClampedArray(size);
40+
41+
for (let i = 0; i < size / 4; ++i) {
42+
array[i * 4 + 0] = r;
43+
array[i * 4 + 1] = g;
44+
array[i * 4 + 2] = b;
45+
array[i * 4 + 3] = a;
46+
}
47+
48+
return array;
49+
}
50+
51+
function makeMessageHeader(length, resetContext, resetAllContexts) {
52+
let flags = 0;
53+
if (resetContext) {
54+
flags |= 1;
55+
}
56+
if (resetAllContexts) {
57+
flags |= 2;
58+
}
59+
60+
let header = new Uint8Array(8);
61+
let i = 0;
62+
63+
let appendU32 = (v) => {
64+
header[i++] = (v >> 24) & 0xff;
65+
header[i++] = (v >> 16) & 0xff;
66+
header[i++] = (v >> 8) & 0xff;
67+
header[i++] = v & 0xff;
68+
};
69+
70+
appendU32(length);
71+
appendU32(flags);
72+
73+
return header;
74+
}
75+
76+
function wrapRectData(data, resetContext, resetAllContexts) {
77+
let header = makeMessageHeader(data.length, resetContext, resetAllContexts);
78+
return Array.from(header).concat(Array.from(data));
79+
}
80+
81+
function testDecodeRect(decoder, x, y, width, height, data, display, depth) {
82+
let sock;
83+
let done = false;
84+
85+
sock = new Websock;
86+
sock.open("ws://example.com");
87+
88+
sock.on('message', () => {
89+
done = decoder.decodeRect(x, y, width, height, sock, display, depth);
90+
});
91+
92+
// Empty messages are filtered at multiple layers, so we need to
93+
// do a direct call
94+
if (data.length === 0) {
95+
done = decoder.decodeRect(x, y, width, height, sock, display, depth);
96+
} else {
97+
sock._websocket._receiveData(new Uint8Array(data));
98+
}
99+
100+
display.flip();
101+
102+
return done;
103+
}
104+
105+
function frameBufferFromCanvasContext(ctx) {
106+
let imageData = ctx.getImageData(0, 0, 16, 16);
107+
let buffer = imageData.data.buffer;
108+
return new Uint8ClampedArray(buffer);
109+
}
110+
111+
describe('H.264 Parser', function () {
112+
it('should parse constrained baseline video', function () {
113+
let parser = new H264Parser(redGreenBlue16x16Video);
114+
115+
let frame = parser.parse();
116+
expect(frame).to.have.property('key', true);
117+
118+
expect(parser).to.have.property('profileIdc', 66);
119+
expect(parser).to.have.property('constraintSet', 192);
120+
expect(parser).to.have.property('levelIdc', 20);
121+
122+
frame = parser.parse();
123+
expect(frame).to.have.property('key', false);
124+
125+
frame = parser.parse();
126+
expect(frame).to.have.property('key', false);
127+
128+
frame = parser.parse();
129+
expect(frame).to.be.null;
130+
});
131+
});
132+
133+
describe('H.264 Context', function () {
134+
it('should decode constrained baseline video chunk', async function () {
135+
let context = new H264Context(16, 16);
136+
let pendingFrame = context.decode(redGreenBlue16x16Video);
137+
138+
expect(pendingFrame).to.have.property('keep', true);
139+
140+
if (!pendingFrame.ready) {
141+
await pendingFrame.promise;
142+
}
143+
expect(pendingFrame.ready).to.be.true;
144+
145+
let frame = pendingFrame.frame;
146+
147+
expect(frame.visibleRect.width).to.equal(16);
148+
expect(frame.visibleRect.height).to.equal(16);
149+
150+
// Note: VideoFrame.copyTo() doesn't work at all on Firefox and it won't
151+
// do the RGBA conversion on Chrome.
152+
153+
let canvas = document.createElement('canvas');
154+
let ctx = canvas.getContext('2d');
155+
156+
ctx.drawImage(frame, 0, 0);
157+
let framebuffer = frameBufferFromCanvasContext(ctx);
158+
159+
const solidBlue = createSolidColorFrameBuffer(0x0000ffff, 16, 16);
160+
expect(framebuffer).to.eql(solidBlue);
161+
});
162+
});
163+
164+
describe('H.264 Decoder Unit Test', function () {
165+
let decoder;
166+
167+
beforeEach(function () {
168+
decoder = new H264Decoder();
169+
});
170+
171+
it('creates and resets context', function () {
172+
let context = decoder._getContext(1, 2, 3, 4);
173+
expect(context._width).to.equal(3);
174+
expect(context._height).to.equal(4);
175+
expect(decoder._contexts).to.not.be.empty;
176+
decoder._resetContext(1, 2, 3, 4);
177+
expect(decoder._contexts).to.be.empty;
178+
});
179+
180+
it('resets all contexts', function () {
181+
decoder._getContext(0, 0, 1, 1);
182+
decoder._getContext(2, 2, 1, 1);
183+
expect(decoder._contexts).to.not.be.empty;
184+
decoder._resetAllContexts();
185+
expect(decoder._contexts).to.be.empty;
186+
});
187+
188+
it('caches contexts', function () {
189+
let c1 = decoder._getContext(1, 2, 3, 4);
190+
c1.lastUsed = 1;
191+
let c2 = decoder._getContext(1, 2, 3, 4);
192+
c2.lastUsed = 2;
193+
expect(Object.keys(decoder._contexts).length).to.equal(1);
194+
expect(c1.lastUsed).to.equal(c2.lastUsed);
195+
});
196+
197+
it('deletes oldest context', function () {
198+
for (let i = 0; i < 65; ++i) {
199+
let context = decoder._getContext(i, 0, 1, 1);
200+
context.lastUsed = i;
201+
}
202+
203+
expect(decoder._findOldestContextId()).to.equal('1,0,1,1');
204+
expect(decoder._contexts[decoder._contextId(0, 0, 1, 1)]).to.be.undefined;
205+
expect(decoder._contexts[decoder._contextId(1, 0, 1, 1)]).to.not.be.null;
206+
expect(decoder._contexts[decoder._contextId(63, 0, 1, 1)]).to.not.be.null;
207+
expect(decoder._contexts[decoder._contextId(64, 0, 1, 1)]).to.not.be.null;
208+
});
209+
});
210+
211+
describe('H.264 Decoder Functional Test', function () {
212+
let decoder;
213+
let display;
214+
215+
before(FakeWebSocket.replace);
216+
after(FakeWebSocket.restore);
217+
218+
beforeEach(function () {
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);
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);
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);
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)