Skip to content

Commit 258fcf7

Browse files
committed
added clip test cases
1 parent 4267646 commit 258fcf7

File tree

8 files changed

+323
-12
lines changed

8 files changed

+323
-12
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { ClipDeserializer } from './clip.desierializer';
3+
import {
4+
AudioClip,
5+
VideoClip,
6+
HtmlClip,
7+
ImageClip,
8+
TextClip,
9+
ComplexTextClip,
10+
Clip
11+
} from '..';
12+
import { AudioSource, HtmlSource, ImageSource, VideoSource } from '../../sources';
13+
import type { Source } from '../../sources';
14+
15+
describe('ClipDeserializer', () => {
16+
it('should return correct clip based on type', () => {
17+
expect(ClipDeserializer.fromType({ type: 'video' })).toBeInstanceOf(VideoClip);
18+
expect(ClipDeserializer.fromType({ type: 'audio' })).toBeInstanceOf(AudioClip);
19+
expect(ClipDeserializer.fromType({ type: 'html' })).toBeInstanceOf(HtmlClip);
20+
expect(ClipDeserializer.fromType({ type: 'image' })).toBeInstanceOf(ImageClip);
21+
expect(ClipDeserializer.fromType({ type: 'text' })).toBeInstanceOf(TextClip);
22+
expect(ClipDeserializer.fromType({ type: 'complex_text' })).toBeInstanceOf(ComplexTextClip);
23+
expect(ClipDeserializer.fromType({ type: 'unknown' as any })).toBeInstanceOf(Clip); // Default case
24+
});
25+
26+
it('should return correct clip based on source', () => {
27+
// Mock instances for different source types
28+
const audioSource = new AudioSource();
29+
const videoSource = new VideoSource();
30+
const imageSource = new ImageSource();
31+
const htmlSource = new HtmlSource();
32+
33+
const res = ClipDeserializer.fromSource(audioSource)
34+
35+
// Ensure proper class instantiation based on source type
36+
expect(res).toBeInstanceOf(AudioClip);
37+
expect(ClipDeserializer.fromSource(videoSource)).toBeInstanceOf(VideoClip);
38+
expect(ClipDeserializer.fromSource(imageSource)).toBeInstanceOf(ImageClip);
39+
expect(ClipDeserializer.fromSource(htmlSource)).toBeInstanceOf(HtmlClip);
40+
});
41+
42+
it('should return undefined if source type does not match', () => {
43+
const invalidSourceMock = { type: 'unknown' } as any as Source;
44+
expect(ClipDeserializer.fromSource(invalidSourceMock)).toBeUndefined();
45+
});
46+
});

src/clips/video/buffer.spec.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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, vi, beforeEach } from 'vitest';
9+
import { FrameBuffer } from './buffer';
10+
11+
describe('FrameBuffer', () => {
12+
let frameBuffer: FrameBuffer;
13+
let mockVideoFrame: any;
14+
15+
beforeEach(() => {
16+
// Mock VideoFrame
17+
mockVideoFrame = {
18+
close: vi.fn(),
19+
};
20+
21+
frameBuffer = new FrameBuffer();
22+
});
23+
24+
it('should enqueue frames and trigger onenqueue callback', () => {
25+
const mockOnEnqueue = vi.fn();
26+
frameBuffer.onenqueue = mockOnEnqueue;
27+
28+
frameBuffer.enqueue(mockVideoFrame);
29+
30+
expect(frameBuffer['buffer'].length).toBe(1);
31+
expect(frameBuffer['buffer'][0]).toBe(mockVideoFrame);
32+
expect(mockOnEnqueue).toHaveBeenCalled();
33+
});
34+
35+
it('should dequeue frames in FIFO order', async () => {
36+
const frame1 = { ...mockVideoFrame };
37+
const frame2 = { ...mockVideoFrame };
38+
39+
frameBuffer.enqueue(frame1);
40+
frameBuffer.enqueue(frame2);
41+
42+
const dequeuedFrame1 = await frameBuffer.dequeue();
43+
const dequeuedFrame2 = await frameBuffer.dequeue();
44+
45+
expect(dequeuedFrame1).toBe(frame1);
46+
expect(dequeuedFrame2).toBe(frame2);
47+
expect(frameBuffer['buffer'].length).toBe(0);
48+
});
49+
50+
it('should wait for a frame to be enqueued if buffer is empty and state is active', async () => {
51+
const mockOnEnqueue = vi.fn();
52+
const mockWaitFor = vi.spyOn(frameBuffer as any, 'waitFor');
53+
54+
frameBuffer.onenqueue = mockOnEnqueue;
55+
const dequeuePromise = frameBuffer.dequeue();
56+
57+
// Simulate enqueuing a frame after some delay
58+
setTimeout(() => {
59+
frameBuffer.enqueue(mockVideoFrame);
60+
}, 100);
61+
62+
const result = await dequeuePromise;
63+
64+
expect(result).toBe(mockVideoFrame);
65+
expect(mockWaitFor).toHaveBeenCalledWith(20000); // 20s timeout
66+
});
67+
68+
it('should resolve immediately if buffer is closed and empty', async () => {
69+
frameBuffer.close();
70+
71+
const result = await frameBuffer.dequeue();
72+
expect(result).toBeUndefined();
73+
});
74+
75+
it('should call onclose callback when buffer is closed', () => {
76+
const mockOnClose = vi.fn();
77+
frameBuffer.onclose = mockOnClose;
78+
79+
frameBuffer.close();
80+
81+
expect(frameBuffer['state']).toBe('closed');
82+
expect(mockOnClose).toHaveBeenCalled();
83+
});
84+
85+
it('should close all frames when terminate is called', () => {
86+
const frame1 = { ...mockVideoFrame, close: vi.fn() };
87+
const frame2 = { ...mockVideoFrame, close: vi.fn() };
88+
89+
frameBuffer.enqueue(frame1);
90+
frameBuffer.enqueue(frame2);
91+
92+
frameBuffer.terminate();
93+
94+
expect(frame1.close).toHaveBeenCalled();
95+
expect(frame2.close).toHaveBeenCalled();
96+
});
97+
98+
it('should reject after timeout if no enqueue or close happens', async () => {
99+
await expect((frameBuffer as any).waitFor(50)).rejects.toThrow('Promise timed out after 50 ms');
100+
});
101+
});

src/clips/video/decoder.spec.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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, vi, beforeEach, Mock } from 'vitest';
9+
import { Decoder } from './decoder';
10+
11+
describe('Decoder', () => {
12+
let mockPostMessage: Mock<any>
13+
let mockVideoDecoder: Mock<any>
14+
let mockVideoFrame: any;
15+
let decoder: Decoder;
16+
17+
beforeEach(() => {
18+
// Mock postMessage for 'self'
19+
mockPostMessage = vi.fn();
20+
(global as any).self = {
21+
postMessage: mockPostMessage,
22+
close: vi.fn(),
23+
};
24+
25+
// Mock VideoDecoder
26+
mockVideoDecoder = vi.fn().mockImplementation(({ output, error }) => {
27+
return {
28+
output,
29+
error,
30+
decode: vi.fn(),
31+
close: vi.fn(),
32+
};
33+
});
34+
(global as any).VideoDecoder = mockVideoDecoder;
35+
36+
// Mock VideoFrame
37+
mockVideoFrame = {
38+
timestamp: 0,
39+
duration: 1000000, // 1 second in nanoseconds
40+
close: vi.fn(),
41+
};
42+
});
43+
44+
it('should initialize with correct properties', () => {
45+
const range = [0, 5] satisfies [number, number]; // 5 seconds range
46+
const fps = 30;
47+
48+
decoder = new Decoder(range, fps);
49+
50+
expect(decoder.video).toBeDefined();
51+
expect(mockVideoDecoder).toHaveBeenCalled();
52+
expect(decoder['currentTime']).toBe(range[0] * 1e6);
53+
expect(decoder['firstTimestamp']).toBe(range[0] * 1e6);
54+
expect(decoder['totalFrames']).toBe(((range[1] - range[0]) * fps) + 1);
55+
expect(decoder['fps']).toBe(fps);
56+
});
57+
58+
it('should post a frame and update current time and count', () => {
59+
const range = [0, 5] satisfies [number, number];
60+
const fps = 30;
61+
62+
decoder = new Decoder(range, fps);
63+
64+
decoder['postFrame'](mockVideoFrame);
65+
66+
expect(mockPostMessage).toHaveBeenCalledWith({ type: 'frame', frame: mockVideoFrame });
67+
expect(decoder['currentTime']).toBeGreaterThan(range[0] * 1e6); // Time should increase
68+
expect(decoder['currentFrames']).toBe(1);
69+
});
70+
71+
it('should handle frame output within range and post frames', () => {
72+
const range = [0, 5] satisfies [number, number];
73+
const fps = 30;
74+
75+
decoder = new Decoder(range, fps);
76+
mockVideoFrame.timestamp = range[0] * 1e6; // Start time
77+
78+
decoder['handleFrameOutput'](mockVideoFrame);
79+
80+
expect(mockPostMessage).toHaveBeenCalledWith({ type: 'frame', frame: mockVideoFrame });
81+
expect(mockVideoFrame.close).toHaveBeenCalled();
82+
});
83+
84+
it('should handle errors and post error messages', () => {
85+
const range = [0, 5] satisfies [number, number];
86+
const fps = 30;
87+
const mockError = new DOMException('Test Error');
88+
89+
decoder = new Decoder(range, fps);
90+
91+
decoder['handleError'](mockError);
92+
93+
expect(mockPostMessage).toHaveBeenCalledWith({
94+
type: 'error',
95+
message: 'Test Error',
96+
});
97+
expect(self.close).toHaveBeenCalled();
98+
});
99+
});

src/clips/video/video.spec.ts

Lines changed: 71 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { Source, VideoSource } from '../../sources';
1111
import { VideoClip } from './video';
1212
import { Composition } from '../../composition';
1313
import { Keyframe, Timestamp } from '../../models';
14+
import { sleep } from '../../utils';
15+
import { FrameBuffer } from './buffer';
1416

1517
import type { MockInstance } from 'vitest';
1618

@@ -24,7 +26,7 @@ describe('The Video Clip', () => {
2426

2527
let playFn: MockInstance<() => Promise<void>>;
2628
let pauseFn: MockInstance<() => Promise<void>>;
27-
let seekFn: MockInstance<(time: Timestamp) => Promise<void>>;
29+
let seekFn: MockInstance<(arg: number) => void>;
2830

2931
const createObjectUrlSpy = vi.spyOn(Source.prototype as any, 'createObjectURL');
3032

@@ -95,8 +97,8 @@ describe('The Video Clip', () => {
9597

9698
it('should connect to a track', async () => {
9799
clip
98-
.set({ offset: 10, height: '100%' })
99-
.subclip(10, 80);
100+
.set({ offset: 6, height: '100%' })
101+
.subclip(6, 80);
100102

101103
const composition = new Composition();
102104
const track = composition.createTrack('video');
@@ -106,17 +108,18 @@ describe('The Video Clip', () => {
106108
await track.add(clip);
107109

108110
expect(seekFn).toHaveBeenCalledTimes(1);
109-
expect(seekFn.mock.calls[0][0].seconds).toBe(1);
111+
// composition.frame - offset = 24; 24 / 30fps = 0.8
112+
expect(seekFn.mock.calls[0][0]).toBe(0.8);
110113

111114
expect(clip.state).toBe('ATTACHED');
112-
expect(clip.offset.frames).toBe(10);
115+
expect(clip.offset.frames).toBe(6);
113116
expect(clip.duration.seconds).toBe(30);
114117
expect(clip.height).toBe('100%');
115-
expect(clip.range[0].frames).toBe(10);
118+
expect(clip.range[0].frames).toBe(6);
116119
expect(clip.range[1].frames).toBe(80);
117120

118-
expect(clip.start.frames).toBe(20);
119-
expect(clip.stop.frames).toBe(90);
121+
expect(clip.start.frames).toBe(12);
122+
expect(clip.stop.frames).toBe(86);
120123

121124
expect(track.clips.findIndex((n) => n.id == clip.id)).toBe(0);
122125
expect(attachFn).toBeCalledTimes(1);
@@ -151,6 +154,7 @@ describe('The Video Clip', () => {
151154
expect(updateSpy).toHaveBeenCalledTimes(1);
152155
expect(exitSpy).toHaveBeenCalledTimes(0);
153156

157+
pauseFn.mockClear();
154158
composition.state = 'IDLE';
155159
composition.frame = 60;
156160
composition.computeFrame();
@@ -255,6 +259,60 @@ describe('The Video Clip', () => {
255259

256260
expect(clip.sprite.texture.uid).toBe(clip.textrues.html5.uid);
257261
});
262+
263+
it('should start decoding the video when the seek method is called and the composition is rendering', async () => {
264+
const composition = new Composition();
265+
await composition.add(clip);
266+
267+
const buffer = new FrameBuffer();
268+
269+
Object.defineProperty(buffer, 'onenqueue', {
270+
set: (fn: () => void) => fn()
271+
});
272+
273+
//@ts-ignore
274+
const decodeSpy = vi.spyOn(clip, 'decodeVideo').mockReturnValueOnce(buffer);
275+
composition.state = 'RENDER';
276+
277+
await clip.seek(new Timestamp());
278+
279+
expect(decodeSpy).toBeCalledTimes(1);
280+
expect(seekFn.mock.calls[0][0]).toBe(0);
281+
});
282+
283+
it('should calculate the correct demux range', async () => {
284+
const composition = new Composition();
285+
await composition.add(clip);
286+
287+
clip.subclip(6, 63);
288+
289+
let [start, stop] = (clip as any).demuxRange;
290+
expect(start).toBe(0.2);
291+
expect(stop).toBe(2.1);
292+
293+
clip.offsetBy(-12)
294+
295+
composition.duration = 30;
296+
297+
[start, stop] = (clip as any).demuxRange;
298+
expect(start).toBe(0.4);
299+
expect(stop).toBe(1.4);
300+
});
301+
302+
it('should be able to cancel decoding', async () => {
303+
const workerSpy = vi.fn();
304+
const bufferSpy = vi.fn();
305+
306+
// @ts-ignore
307+
clip.worker = { terminate: workerSpy };
308+
// @ts-ignore
309+
clip.buffer = { terminate: bufferSpy };
310+
311+
clip.cancelDecoding();
312+
313+
expect(workerSpy).toBeCalledTimes(1);
314+
expect(bufferSpy).toBeCalledTimes(1);
315+
});
258316
});
259317

260318
// Blend of different test files
@@ -382,5 +440,9 @@ function mockDimensions(clip: VideoClip, width = 540, height = 680) {
382440
}
383441

384442
function mockSeek(clip: VideoClip) {
385-
return vi.spyOn(clip, 'seek').mockImplementation(async () => { });
443+
return vi.spyOn(clip.element, 'currentTime', 'set')
444+
.mockImplementation(async function (this: HTMLVideoElement) {
445+
await sleep(1);
446+
this.dispatchEvent(new Event('seeked'));
447+
});
386448
}

src/sources/audio.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type { ClipType } from '../clips';
1111
import type { ArgumentTypes } from '../types';
1212

1313
export class AudioSource extends Source {
14-
public readonly type: ClipType = 'base';
14+
public readonly type: ClipType = 'audio';
1515
public audioBuffer?: AudioBuffer;
1616

1717
public async decode(

src/sources/html.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { documentToSvgImageUrl } from './html.utils';
1111
import type { ClipType } from '../clips';
1212

1313
export class HtmlSource extends Source {
14-
public readonly type: ClipType = 'base';
14+
public readonly type: ClipType = 'html';
1515
/**
1616
* Access to the iframe that is required
1717
* for extracting the html's dimensions

0 commit comments

Comments
 (0)