Skip to content

Commit a4d26e8

Browse files
committed
added encoder unit tests
1 parent 676d545 commit a4d26e8

12 files changed

+683
-3
lines changed

src/encoders/canvas.spec.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/**
2+
* Copyright (c) 2024 The Diffusion Studio Authors
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla
5+
* Public License, v. 2.0 that can be found in the LICENSE file.
6+
*/
7+
8+
import { beforeEach, describe, expect, it, vi } from 'vitest';
9+
import { OpusEncoder } from './opus';
10+
import { CanvasEncoder } from './canvas';
11+
12+
const buffer = {
13+
sampleRate: 8000,
14+
duration: 1,
15+
numberOfChannels: 1,
16+
length: 8000,
17+
getChannelData: vi.fn(() => new Float32Array(8000)),
18+
} as unknown as AudioBuffer
19+
20+
describe('The CanvasEncoder', () => {
21+
const canvas = document.createElement('canvas');
22+
canvas.height = 640;
23+
canvas.width = 480;
24+
25+
let encoder: CanvasEncoder;
26+
27+
const configureSpy = vi.spyOn(VideoEncoder.prototype, 'configure').mockImplementation(vi.fn());
28+
const encodeSpy = vi.spyOn(VideoEncoder.prototype, 'encode').mockImplementation(vi.fn());
29+
30+
beforeEach(() => {
31+
encoder = new CanvasEncoder(canvas, {
32+
fps: 60,
33+
gpuBatchSize: 4,
34+
numberOfChannels: 1,
35+
sampleRate: 2000,
36+
videoBitrate: 1e6,
37+
});
38+
39+
expect(encoder.fps).toBe(60);
40+
expect(encoder.height).toBe(640);
41+
expect(encoder.width).toBe(480);
42+
expect(encoder.sampleRate).toBe(8000);
43+
expect(encoder.videoBitrate).toBe(1e6);
44+
45+
configureSpy.mockClear();
46+
encodeSpy.mockClear();
47+
});
48+
49+
it('should encode video', async () => {
50+
expect(encoder.frame).toBe(0);
51+
expect(configureSpy).toBeCalledTimes(0);
52+
53+
await encoder.encodeVideo();
54+
55+
expect(encoder.frame).toBe(1);
56+
expect(configureSpy).toBeCalledTimes(1);
57+
expect(encodeSpy).toBeCalledTimes(1);
58+
expect(encodeSpy.mock.calls[0][1]?.keyFrame).toBe(true);
59+
60+
await encoder.encodeVideo();
61+
62+
expect(encoder.frame).toBe(2);
63+
expect(configureSpy).toBeCalledTimes(1);
64+
expect(encodeSpy).toBeCalledTimes(2);
65+
expect(encodeSpy.mock.calls[1][1]?.keyFrame).toBe(false);
66+
});
67+
68+
it('should encode audio', async () => {
69+
encoder = new CanvasEncoder(canvas, {
70+
numberOfChannels: 1,
71+
sampleRate: 2000,
72+
audio: true,
73+
});
74+
75+
expect(encoder.sampleRate).toBe(8000)
76+
77+
const configureSpy = vi.spyOn(OpusEncoder.prototype, 'configure').mockImplementation(vi.fn());
78+
const encodeSpy = vi.spyOn(OpusEncoder.prototype, 'encode').mockImplementation(vi.fn());
79+
80+
await encoder.encodeAudio(buffer);
81+
82+
expect(configureSpy).toBeCalledTimes(1);
83+
expect(encodeSpy).toBeCalledTimes(1);
84+
85+
expect(configureSpy.mock.calls[0][0]).toStrictEqual({ numberOfChannels: 1, sampleRate: 8000 });
86+
expect(encodeSpy.mock.calls[0][0].data.length).toBe(8000);
87+
expect(encodeSpy.mock.calls[0][0].numberOfFrames).toBe(8000);
88+
});
89+
90+
it("should not encode audio when it's not enabled", async () => {
91+
await expect(encoder.encodeAudio(buffer)).rejects.toThrowError();
92+
});
93+
94+
it('should create a blob', async () => {
95+
await encoder.encodeVideo();
96+
97+
const blob = await encoder.blob();
98+
99+
expect(blob).toBeInstanceOf(Blob);
100+
});
101+
102+
it('should not create a blob if the buffer is not defined', async () => {
103+
await expect(() => encoder.blob()).rejects.toThrowError();
104+
});
105+
});

src/encoders/encoder.spec.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/**
2+
* Copyright (c) 2024 The Diffusion Studio Authors
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla
5+
* Public License, v. 2.0 that can be found in the LICENSE file.
6+
*/
7+
8+
import { beforeEach, describe, expect, it, vi } from 'vitest';
9+
import { Composition } from '../composition';
10+
import { Encoder } from './encoder';
11+
import { Clip } from '../clips';
12+
13+
14+
describe('The Encoder', () => {
15+
let composition: Composition;
16+
let encoder: Encoder;
17+
18+
const configureSpy = vi.spyOn(VideoEncoder.prototype, 'configure').mockImplementation(vi.fn());
19+
const encodeSpy = vi.spyOn(VideoEncoder.prototype, 'encode').mockImplementation(vi.fn());
20+
21+
beforeEach(() => {
22+
composition = new Composition();
23+
24+
encoder = new Encoder(composition, {
25+
audio: false,
26+
debug: false,
27+
fps: 60,
28+
gpuBatchSize: 4,
29+
numberOfChannels: 1,
30+
resolution: 0.5,
31+
sampleRate: 8000,
32+
videoBitrate: 1e6,
33+
});
34+
35+
configureSpy.mockClear();
36+
encodeSpy.mockClear();
37+
38+
expect(encoder.audio).toBe(false);
39+
expect(encoder.debug).toBe(false);
40+
expect(encoder.fps).toBe(60);
41+
});
42+
43+
it('should render the compostion', async () => {
44+
await composition.add(new Clip({ stop: 10 }));
45+
46+
const pauseSpy = vi.spyOn(composition, 'pause').mockImplementation(async () => undefined);
47+
const seekSpy = vi.spyOn(composition, 'seek').mockImplementation(async () => undefined);
48+
49+
await encoder.render();
50+
51+
expect(pauseSpy).toBeCalledTimes(1);
52+
// before and after
53+
expect(seekSpy).toBeCalledTimes(2);
54+
});
55+
56+
it('should not render when the composition renderer is not defined', async () => {
57+
delete composition.renderer;
58+
59+
await expect(() => encoder.render()).rejects.toThrowError();
60+
});
61+
62+
it('should debug the render process', async () => {
63+
encoder.debug = true;
64+
65+
await composition.add(new Clip({ stop: 10 }));
66+
67+
const logSpy = vi.spyOn(console, 'info');
68+
69+
await encoder.render();
70+
71+
expect(logSpy).toBeCalledTimes(3);
72+
});
73+
});

src/encoders/encoder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export class Encoder extends WebcodecsVideoEncoder {
5050
const [videoConfig, audioConfig] = await this.getConfigs();
5151

5252
if (this.debug) {
53-
console.log('Hardware Preference', videoConfig.hardwareAcceleration);
53+
console.info('Hardware Preference', videoConfig.hardwareAcceleration);
5454
}
5555

5656
const now = performance.now();
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* Copyright (c) 2024 The Diffusion Studio Authors
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla
5+
* Public License, v. 2.0 that can be found in the LICENSE file.
6+
*/
7+
8+
import { describe, it, expect } from 'vitest';
9+
import { createOpusHead } from './opus.utils';
10+
11+
describe('createOpusHead', () => {
12+
it('should generate a correct Opus header', () => {
13+
const sampleRate = 48000;
14+
const numberOfChannels = 2;
15+
const result = createOpusHead(sampleRate, numberOfChannels);
16+
17+
// Check that the result is a Uint8Array of length 19
18+
expect(result).toBeInstanceOf(Uint8Array);
19+
expect(result.length).toBe(19);
20+
21+
// Check magic signature "OpusHead"
22+
expect(result[0]).toBe('O'.charCodeAt(0));
23+
expect(result[1]).toBe('p'.charCodeAt(0));
24+
expect(result[2]).toBe('u'.charCodeAt(0));
25+
expect(result[3]).toBe('s'.charCodeAt(0));
26+
expect(result[4]).toBe('H'.charCodeAt(0));
27+
expect(result[5]).toBe('e'.charCodeAt(0));
28+
expect(result[6]).toBe('a'.charCodeAt(0));
29+
expect(result[7]).toBe('d'.charCodeAt(0));
30+
31+
// Check version is set to 1
32+
expect(result[8]).toBe(1);
33+
34+
// Check number of channels
35+
expect(result[9]).toBe(numberOfChannels);
36+
37+
// Check pre-skip is 0 (bytes 10 and 11)
38+
expect(result[10]).toBe(0);
39+
expect(result[11]).toBe(0);
40+
41+
// Check sample rate is correctly encoded (bytes 12-15)
42+
expect(result[12]).toBe(sampleRate & 0xFF);
43+
expect(result[13]).toBe((sampleRate >> 8) & 0xFF);
44+
expect(result[14]).toBe((sampleRate >> 16) & 0xFF);
45+
expect(result[15]).toBe((sampleRate >> 24) & 0xFF);
46+
47+
// Check gain is 0 (bytes 16 and 17)
48+
expect(result[16]).toBe(0);
49+
expect(result[17]).toBe(0);
50+
51+
// Check channel mapping is 0
52+
expect(result[18]).toBe(0);
53+
});
54+
55+
it('should correctly encode a different sample rate and number of channels', () => {
56+
const sampleRate = 44100;
57+
const numberOfChannels = 1;
58+
const result = createOpusHead(sampleRate, numberOfChannels);
59+
60+
// Check number of channels
61+
expect(result[9]).toBe(numberOfChannels);
62+
63+
// Check sample rate is correctly encoded (bytes 12-15)
64+
expect(result[12]).toBe(sampleRate & 0xFF);
65+
expect(result[13]).toBe((sampleRate >> 8) & 0xFF);
66+
expect(result[14]).toBe((sampleRate >> 16) & 0xFF);
67+
expect(result[15]).toBe((sampleRate >> 24) & 0xFF);
68+
});
69+
});

src/encoders/utils.spec.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/**
2+
* Copyright (c) 2024 The Diffusion Studio Authors
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla
5+
* Public License, v. 2.0 that can be found in the LICENSE file.
6+
*/
7+
8+
import { describe, expect, it, vi } from 'vitest';
9+
10+
import { audioClipFilter, createRenderEventDetail, createStreamTarget, toOpusSampleRate, withError } from './utils';
11+
import { ArrayBufferTarget, FileSystemWritableFileStreamTarget } from 'mp4-muxer';
12+
import { Clip, MediaClip } from '../clips';
13+
14+
15+
describe('createStreamTarget', () => {
16+
it('should create a downloadable stream target', async () => {
17+
const res = await createStreamTarget('test.mp4');
18+
19+
expect(res.target).toBeInstanceOf(ArrayBufferTarget);
20+
expect(res.fastStart).toBe('in-memory');
21+
22+
const a = document.createElement('a');
23+
24+
const clickSpy = vi.spyOn(a, 'click');
25+
const createSpy = vi.spyOn(document, 'createElement').mockReturnValue(a);
26+
27+
await res.close(true);
28+
29+
expect(clickSpy).toBeCalledTimes(1);
30+
expect(createSpy).toBeCalledTimes(1);
31+
expect(a.download).toBe('test.mp4');
32+
});
33+
34+
it('should upload the file if the target is an http address', async () => {
35+
const res = await createStreamTarget('https://s3.com/test.mp4');
36+
37+
expect(res.target).toBeInstanceOf(ArrayBufferTarget);
38+
expect(res.fastStart).toBe('in-memory');
39+
40+
const fetchSpy = vi.spyOn(global, 'fetch');
41+
42+
await res.close(true);
43+
44+
expect(fetchSpy).toBeCalledTimes(1);
45+
expect(fetchSpy.mock.calls[0][0]).toBe('https://s3.com/test.mp4');
46+
expect(fetchSpy.mock.calls[0][1]?.method).toBe('PUT');
47+
});
48+
49+
it('should throw an error when using http upload and the response is not ok', async () => {
50+
const res = await createStreamTarget('https://s3.com/test.mp4');
51+
52+
const fetchSpy = vi.spyOn(global, 'fetch').mockReturnValue({
53+
ok: false,
54+
} as any)
55+
56+
await expect(() => res.close(true)).rejects.toThrowError();
57+
58+
fetchSpy.mockRestore();
59+
});
60+
61+
it('should handle the file system access', async () => {
62+
const handle = new FileSystemFileHandle();
63+
const res = await createStreamTarget(handle);
64+
65+
expect(res.target).toBeInstanceOf(FileSystemWritableFileStreamTarget);
66+
expect(res.fastStart).toBe(false);
67+
68+
const closeSpy = vi.spyOn(FileSystemWritableFileStream.prototype, 'close');
69+
70+
await res.close(true);
71+
72+
expect(closeSpy).toBeCalledTimes(1);
73+
});
74+
});
75+
76+
describe('withError', () => {
77+
it('should reject promise all errors', async () => {
78+
const promise1 = Promise.resolve(3);
79+
const promise2 = new Promise((_, reject) =>
80+
setTimeout(reject, 100, 'foo'),
81+
);
82+
83+
const promise = withError(Promise.allSettled([promise1, promise2]));
84+
85+
await expect(promise).rejects.toThrowError();
86+
})
87+
});
88+
89+
describe('createRenderEventDetail', () => {
90+
it('should should calculate the remaining time', async () => {
91+
const date = new Date();
92+
date.setSeconds(date.getSeconds() - 10);
93+
const detail = createRenderEventDetail(50, 100, date.getTime());
94+
expect(detail.progress).toBe(50);
95+
expect(detail.total).toBe(100);
96+
// it took 10 secs for 50% there should be 10 seconds remaining
97+
expect(detail.remaining.getSeconds()).toBe(10);
98+
});
99+
});
100+
101+
describe('audioClipFilter', () => {
102+
it('should filter clips', async () => {
103+
const clips = [new Clip(), new Clip(), new MediaClip({ disabled: true }), new MediaClip()];
104+
105+
expect(clips.filter(audioClipFilter).length).toBe(1);
106+
});
107+
});
108+
109+
describe('toOpusSampleRate', () => {
110+
it('should find the closes available opus sample rate', async () => {
111+
expect(toOpusSampleRate(0)).toBe(8000);
112+
expect(toOpusSampleRate(10000)).toBe(8000);
113+
expect(toOpusSampleRate(10001)).toBe(12000);
114+
expect(toOpusSampleRate(50000)).toBe(48000);
115+
});
116+
});

0 commit comments

Comments
 (0)