Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
689 changes: 273 additions & 416 deletions package-lock.json

Large diffs are not rendered by default.

28 changes: 14 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@diffusionstudio/core",
"private": false,
"version": "1.0.0-rc.7",
"version": "1.0.0-rc.8",
"type": "module",
"description": "Build bleeding edge video processing applications",
"files": [
Expand All @@ -27,26 +27,26 @@
"docs": "typedoc src/index.ts --plugin typedoc-plugin-markdown --out ./docs"
},
"devDependencies": {
"@biomejs/biome": "1.8.3",
"@biomejs/biome": "1.9.2",
"@types/dom-webcodecs": "^0.1.11",
"@types/node": "^22.5.2",
"@types/node": "^22.5.5",
"@types/wicg-file-system-access": "^2023.10.5",
"@vitest/coverage-v8": "^2.0.5",
"@vitest/web-worker": "^2.0.5",
"@webgpu/types": "^0.1.44",
"jsdom": "^25.0.0",
"@vitest/coverage-v8": "^2.1.1",
"@vitest/web-worker": "^2.1.1",
"@webgpu/types": "^0.1.46",
"jsdom": "^25.0.1",
"rollup-plugin-node-externals": "^7.1.3",
"typedoc": "^0.26.6",
"typedoc-plugin-markdown": "^4.2.6",
"typescript": "^5.5.4",
"typedoc": "^0.26.7",
"typedoc-plugin-markdown": "^4.2.8",
"typescript": "^5.6.2",
"user-agent-data-types": "^0.4.2",
"vite": "^5.4.2",
"vite-plugin-dts": "^4.1.0",
"vitest": "^2.0.5",
"vite": "^5.4.7",
"vite-plugin-dts": "^4.2.1",
"vitest": "^2.1.1",
"vitest-canvas-mock": "^0.3.3"
},
"dependencies": {
"mp4-muxer": "^5.1.1"
"mp4-muxer": "^5.1.3"
},
"peerDependencies": {
"pixi-filters": ">=6.0.0",
Expand Down
46 changes: 46 additions & 0 deletions src/clips/clip/clip.deserializer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { describe, it, expect } from 'vitest';
import { ClipDeserializer } from './clip.desierializer';
import {
AudioClip,
VideoClip,
HtmlClip,
ImageClip,
TextClip,
ComplexTextClip,
Clip
} from '..';
import { AudioSource, HtmlSource, ImageSource, VideoSource } from '../../sources';
import type { Source } from '../../sources';

describe('ClipDeserializer', () => {
it('should return correct clip based on type', () => {
expect(ClipDeserializer.fromType({ type: 'video' })).toBeInstanceOf(VideoClip);
expect(ClipDeserializer.fromType({ type: 'audio' })).toBeInstanceOf(AudioClip);
expect(ClipDeserializer.fromType({ type: 'html' })).toBeInstanceOf(HtmlClip);
expect(ClipDeserializer.fromType({ type: 'image' })).toBeInstanceOf(ImageClip);
expect(ClipDeserializer.fromType({ type: 'text' })).toBeInstanceOf(TextClip);
expect(ClipDeserializer.fromType({ type: 'complex_text' })).toBeInstanceOf(ComplexTextClip);
expect(ClipDeserializer.fromType({ type: 'unknown' as any })).toBeInstanceOf(Clip); // Default case
});

it('should return correct clip based on source', () => {
// Mock instances for different source types
const audioSource = new AudioSource();
const videoSource = new VideoSource();
const imageSource = new ImageSource();
const htmlSource = new HtmlSource();

const res = ClipDeserializer.fromSource(audioSource)

// Ensure proper class instantiation based on source type
expect(res).toBeInstanceOf(AudioClip);
expect(ClipDeserializer.fromSource(videoSource)).toBeInstanceOf(VideoClip);
expect(ClipDeserializer.fromSource(imageSource)).toBeInstanceOf(ImageClip);
expect(ClipDeserializer.fromSource(htmlSource)).toBeInstanceOf(HtmlClip);
});

it('should return undefined if source type does not match', () => {
const invalidSourceMock = { type: 'unknown' } as any as Source;
expect(ClipDeserializer.fromSource(invalidSourceMock)).toBeUndefined();
});
});
101 changes: 101 additions & 0 deletions src/clips/video/buffer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* Copyright (c) 2024 The Diffusion Studio Authors
*
* This Source Code Form is subject to the terms of the Mozilla
* Public License, v. 2.0 that can be found in the LICENSE file.
*/

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { FrameBuffer } from './buffer';

describe('FrameBuffer', () => {
let frameBuffer: FrameBuffer;
let mockVideoFrame: any;

beforeEach(() => {
// Mock VideoFrame
mockVideoFrame = {
close: vi.fn(),
};

frameBuffer = new FrameBuffer();
});

it('should enqueue frames and trigger onenqueue callback', () => {
const mockOnEnqueue = vi.fn();
frameBuffer.onenqueue = mockOnEnqueue;

frameBuffer.enqueue(mockVideoFrame);

expect(frameBuffer['buffer'].length).toBe(1);
expect(frameBuffer['buffer'][0]).toBe(mockVideoFrame);
expect(mockOnEnqueue).toHaveBeenCalled();
});

it('should dequeue frames in FIFO order', async () => {
const frame1 = { ...mockVideoFrame };
const frame2 = { ...mockVideoFrame };

frameBuffer.enqueue(frame1);
frameBuffer.enqueue(frame2);

const dequeuedFrame1 = await frameBuffer.dequeue();
const dequeuedFrame2 = await frameBuffer.dequeue();

expect(dequeuedFrame1).toBe(frame1);
expect(dequeuedFrame2).toBe(frame2);
expect(frameBuffer['buffer'].length).toBe(0);
});

it('should wait for a frame to be enqueued if buffer is empty and state is active', async () => {
const mockOnEnqueue = vi.fn();
const mockWaitFor = vi.spyOn(frameBuffer as any, 'waitFor');

frameBuffer.onenqueue = mockOnEnqueue;
const dequeuePromise = frameBuffer.dequeue();

// Simulate enqueuing a frame after some delay
setTimeout(() => {
frameBuffer.enqueue(mockVideoFrame);
}, 100);

const result = await dequeuePromise;

expect(result).toBe(mockVideoFrame);
expect(mockWaitFor).toHaveBeenCalledWith(20000); // 20s timeout
});

it('should resolve immediately if buffer is closed and empty', async () => {
frameBuffer.close();

const result = await frameBuffer.dequeue();
expect(result).toBeUndefined();
});

it('should call onclose callback when buffer is closed', () => {
const mockOnClose = vi.fn();
frameBuffer.onclose = mockOnClose;

frameBuffer.close();

expect(frameBuffer['state']).toBe('closed');
expect(mockOnClose).toHaveBeenCalled();
});

it('should close all frames when terminate is called', () => {
const frame1 = { ...mockVideoFrame, close: vi.fn() };
const frame2 = { ...mockVideoFrame, close: vi.fn() };

frameBuffer.enqueue(frame1);
frameBuffer.enqueue(frame2);

frameBuffer.terminate();

expect(frame1.close).toHaveBeenCalled();
expect(frame2.close).toHaveBeenCalled();
});

it('should reject after timeout if no enqueue or close happens', async () => {
await expect((frameBuffer as any).waitFor(50)).rejects.toThrow('Promise timed out after 50 ms');
});
});
99 changes: 99 additions & 0 deletions src/clips/video/decoder.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* Copyright (c) 2024 The Diffusion Studio Authors
*
* This Source Code Form is subject to the terms of the Mozilla
* Public License, v. 2.0 that can be found in the LICENSE file.
*/

import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
import { Decoder } from './decoder';

describe('Decoder', () => {
let mockPostMessage: Mock<any>
let mockVideoDecoder: Mock<any>
let mockVideoFrame: any;
let decoder: Decoder;

beforeEach(() => {
// Mock postMessage for 'self'
mockPostMessage = vi.fn();
(global as any).self = {
postMessage: mockPostMessage,
close: vi.fn(),
};

// Mock VideoDecoder
mockVideoDecoder = vi.fn().mockImplementation(({ output, error }) => {
return {
output,
error,
decode: vi.fn(),
close: vi.fn(),
};
});
(global as any).VideoDecoder = mockVideoDecoder;

// Mock VideoFrame
mockVideoFrame = {
timestamp: 0,
duration: 1000000, // 1 second in nanoseconds
close: vi.fn(),
};
});

it('should initialize with correct properties', () => {
const range = [0, 5] satisfies [number, number]; // 5 seconds range
const fps = 30;

decoder = new Decoder(range, fps);

expect(decoder.video).toBeDefined();
expect(mockVideoDecoder).toHaveBeenCalled();
expect(decoder['currentTime']).toBe(range[0] * 1e6);
expect(decoder['firstTimestamp']).toBe(range[0] * 1e6);
expect(decoder['totalFrames']).toBe(((range[1] - range[0]) * fps) + 1);
expect(decoder['fps']).toBe(fps);
});

it('should post a frame and update current time and count', () => {
const range = [0, 5] satisfies [number, number];
const fps = 30;

decoder = new Decoder(range, fps);

decoder['postFrame'](mockVideoFrame);

expect(mockPostMessage).toHaveBeenCalledWith({ type: 'frame', frame: mockVideoFrame });
expect(decoder['currentTime']).toBeGreaterThan(range[0] * 1e6); // Time should increase
expect(decoder['currentFrames']).toBe(1);
});

it('should handle frame output within range and post frames', () => {
const range = [0, 5] satisfies [number, number];
const fps = 30;

decoder = new Decoder(range, fps);
mockVideoFrame.timestamp = range[0] * 1e6; // Start time

decoder['handleFrameOutput'](mockVideoFrame);

expect(mockPostMessage).toHaveBeenCalledWith({ type: 'frame', frame: mockVideoFrame });
expect(mockVideoFrame.close).toHaveBeenCalled();
});

it('should handle errors and post error messages', () => {
const range = [0, 5] satisfies [number, number];
const fps = 30;
const mockError = new DOMException('Test Error');

decoder = new Decoder(range, fps);

decoder['handleError'](mockError);

expect(mockPostMessage).toHaveBeenCalledWith({
type: 'error',
message: 'Test Error',
});
expect(self.close).toHaveBeenCalled();
});
});
1 change: 1 addition & 0 deletions src/clips/video/demuxer/ffmpeg.worker.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* c8 ignore start */
import type { WebAVPacket, WebAVStream } from './types';

let Module: any; // TODO: rm any
Expand Down
1 change: 1 addition & 0 deletions src/clips/video/demuxer/types/avutil.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* c8 ignore start */
/**
* sync with ffmpeg libavutil/avutil.h
*/
Expand Down
1 change: 1 addition & 0 deletions src/clips/video/demuxer/types/demuxer.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* c8 ignore start */
/**
* sync with web-demuxer.h
*/
Expand Down
1 change: 1 addition & 0 deletions src/clips/video/demuxer/types/ffmpeg-worker-message.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* c8 ignore start */
import type { AVMediaType } from './avutil';

export enum FFMpegWorkerMessageType {
Expand Down
1 change: 1 addition & 0 deletions src/clips/video/demuxer/web-demuxer.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* c8 ignore start */
import { AVMediaType, FFMpegWorkerMessageType } from './types';
import FFmpegWorker from './ffmpeg.worker.ts?worker&inline';

Expand Down
Loading