diff --git a/core/decoders/h264.js b/core/decoders/h264.js index 1a34e4056..c965eca70 100644 --- a/core/decoders/h264.js +++ b/core/decoders/h264.js @@ -9,7 +9,7 @@ import * as Log from '../util/logging.js'; -class H264Parser { +export class H264Parser { constructor(data) { this._data = data; this._index = 0; @@ -109,7 +109,7 @@ class H264Parser { } } -class H264Context { +export class H264Context { constructor(width, height) { this.lastUsed = 0; this._width = width; diff --git a/tests/test.h264.js b/tests/test.h264.js new file mode 100644 index 000000000..8256324a0 --- /dev/null +++ b/tests/test.h264.js @@ -0,0 +1,107 @@ +const expect = chai.expect; + +import { H264Parser, H264Context } from '../core/decoders/h264.js'; +import Base64 from '../core/base64.js'; + +/* This video was generated using the following commands: + * magick -size 16x16 xc:#ff0000ff 1.png + * magick -size 16x16 xc:#00ff00ff 2.png + * magick -size 16x16 xc:#0000ffff 3.png + * ffmpeg -pattern_type glob -i '*.png' -c:v libx264 -pix_fmt yuv420p -profile:v baseline video.h264 + * + * It is a 3 frame 16x16 video where the first frame is solid red, the second + * is solid green and the third is solid blue. + */ +const redGreenBlue16x16Video = new Uint8Array(Base64.decode( + 'AAAAAWdCwArZHpqAgQEgAAADACAAAAZB4kTJAAAAAWjLg8sgAAABBgX//23cRem95tlIt5Ys2CDZ' + + 'I+7veDI2NCAtIGNvcmUgMTY0IHIzMTA4IDMxZTE5ZjkgLSBILjI2NC9NUEVHLTQgQVZDIGNvZGVj' + + 'IC0gQ29weWxlZnQgMjAwMy0yMDIzIC0gaHR0cDovL3d3dy52aWRlb2xhbi5vcmcveDI2NC5odG1s' + + 'IC0gb3B0aW9uczogY2FiYWM9MCByZWY9MyBkZWJsb2NrPTE6MDowIGFuYWx5c2U9MHgxOjB4MTEx' + + 'IG1lPWhleCBzdWJtZT03IHBzeT0xIHBzeV9yZD0xLjAwOjAuMDAgbWl4ZWRfcmVmPTEgbWVfcmFu' + + 'Z2U9MTYgY2hyb21hX21lPTEgdHJlbGxpcz0xIDh4OGRjdD0wIGNxbT0wIGRlYWR6b25lPTIxLDEx' + + 'IGZhc3RfcHNraXA9MSBjaHJvbWFfcXBfb2Zmc2V0PS0yIHRocmVhZHM9MSBsb29rYWhlYWRfdGhy' + + 'ZWFkcz0xIHNsaWNlZF90aHJlYWRzPTAgbnI9MCBkZWNpbWF0ZT0xIGludGVybGFjZWQ9MCBibHVy' + + 'YXlfY29tcGF0PTAgY29uc3RyYWluZWRfaW50cmE9MCBiZnJhbWVzPTAgd2VpZ2h0cD0wIGtleWlu' + + 'dD0yNTAga2V5aW50X21pbj0yNSBzY2VuZWN1dD00MCBpbnRyYV9yZWZyZXNoPTAgcmNfbG9va2Fo' + + 'ZWFkPTQwIHJjPWNyZiBtYnRyZWU9MSBjcmY9MjMuMCBxY29tcD0wLjYwIHFwbWluPTAgcXBtYXg9' + + 'NjkgcXBzdGVwPTQgaXBfcmF0aW89MS40MCBhcT0xOjEuMDAAgAAAAWWIhArxGKAAJAMcAAQ644AA' + + 'l8YAAAABQYiIK8RigACFJHAAEeuOAAJPOAAAAAFBiJArxGKAAJ6ccAAS+I4AAgu4')); + +function createSolidColorFrameBuffer(color, width, height) { + const r = (color >> 24) & 0xff; + const g = (color >> 16) & 0xff; + const b = (color >> 8) & 0xff; + const a = (color >> 0) & 0xff; + + const size = width * height * 4; + let array = new Uint8Array(size); + + for (let i = 0; i < size / 4; ++i) { + array[i * 4 + 0] = r; + array[i * 4 + 1] = g; + array[i * 4 + 2] = b; + array[i * 4 + 3] = a; + } + + return array; +} + +function frameBufferFromCanvasContext(ctx) { + let imageData = ctx.getImageData(0, 0, 16, 16); + let buffer = imageData.data.buffer; + return new Uint8Array(buffer); +} + +describe('H.264 Parser', function () { + it('should parse constrained baseline video', function () { + let parser = new H264Parser(redGreenBlue16x16Video); + + let frame = parser.parse(); + expect(frame).to.have.property('key', true); + + expect(parser).to.have.property('profileIdc', 66); + expect(parser).to.have.property('constraintSet', 192); + expect(parser).to.have.property('levelIdc', 10); + + frame = parser.parse(); + expect(frame).to.have.property('key', false); + + frame = parser.parse(); + expect(frame).to.have.property('key', false); + + frame = parser.parse(); + expect(frame).to.be.null; + }); +}); + +describe('H.264 Context', function () { + it('should decode constrained baseline video chunk', async function () { + let context = new H264Context(16, 16); + let pendingFrame = context.decode(redGreenBlue16x16Video); + + expect(pendingFrame).to.have.property('keep', true); + + if (!pendingFrame.ready) { + await pendingFrame.promise; + } + expect(pendingFrame.ready).to.be.true; + + let frame = pendingFrame.frame; + + expect(frame.visibleRect.width).to.equal(16); + expect(frame.visibleRect.height).to.equal(16); + + // Note: VideoFrame.copyTo() doesn't work at all on Firefox and it won't + // do the RGBA conversion on Chrome. + + // TODO: Use something more portable + let canvas = new OffscreenCanvas(16, 16); + let ctx = canvas.getContext('2d'); + + ctx.drawImage(frame, 0, 0); + let framebuffer = frameBufferFromCanvasContext(ctx); + + const solidBlue = createSolidColorFrameBuffer(0x0000ffff, 16, 16); + expect(framebuffer).to.eql(solidBlue); + }); +});