Skip to content

Commit 5b5185d

Browse files
authored
Merge pull request #13 from diffusionstudio/konstantin/feature/opus-wasm-encoder
Konstantin/feature/opus wasm encoder
2 parents 087154a + 29cecd4 commit 5b5185d

32 files changed

+1008
-563
lines changed

README.md

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,9 @@ const composition = new core.Composition(); // 1920x1080
5959
await composition.add(video); // convenience function for
6060
await composition.add(text); // clip -> track -> composition
6161

62-
// export video using webcodecs at 25 FPS
63-
new core.WebcodecsEncoder(composition, { fps: 25 })
64-
.export(); // use resolution = 2 to render at 4k
62+
// render video using webcodecs at 25 FPS
63+
// use resolution: 2 to render at 4k
64+
new core.Encoder(composition, { fps: 25 }).render();
6565
```
6666

6767
This may look familiar to some. That is because the API is heavily inspired by **Moviepy** and Swift UI. It models the structure of popular video editing applications such as Adobe Premiere or CapCut. The current state can be visualized as follows:
@@ -89,9 +89,9 @@ https://github.com/user-attachments/assets/7a943407-e916-4d9f-b46a-3163dbff44c3
8989

9090
**Remotion** is a React-based video creation tool that transforms the entire DOM into videos. It's particularly suited for beginners, as web developers can start creating videos using the skills they already have.
9191

92-
**Motion Canvas** uses a Canvas 2D implementation for rendering. It is intended as a standalone editor for creating production-quality animations. It features a unique imperative API that adds elements to the timeline procedurally, rather than relying on keyframes like traditional video editing tools. This makes Motion Canvas ideal for crafting detailed, animated videos.
92+
**Motion Canvas** is intended as a standalone editor for creating production-quality animations. It features a unique imperative API that adds elements to the timeline procedurally, rather than relying on keyframes like traditional video editing tools. This makes Motion Canvas ideal for crafting detailed, animated videos.
9393

94-
In contrast, **Diffusion Studio** is not a framework with a visual editing interface but a video editing library that can be integrated into existing projects. It operates entirely on the client-side, eliminating the need for additional backend infrastructure. Diffusion Studio is also dedicated to supporting the latest rendering technologies, including WebGPU, WebGL, and WebCodecs. If a feature you need isn't available, you can easily extend it using [Pixi.js](https://github.com/pixijs/pixijs).
94+
In contrast, **Diffusion Studio** is not a framework with a visual editing interface but a **video editing library** that can be integrated into existing projects. It operates entirely on the **client-side**, eliminating the need for additional backend infrastructure. Diffusion Studio is also dedicated to supporting the latest rendering technologies, including WebGPU, WebGL, and WebCodecs. If a feature you need isn't available, you can **easily extend** it using [Pixi.js](https://github.com/pixijs/pixijs).
9595

9696
## Current features
9797
* **Video/Audio** trim and offset
@@ -134,50 +134,49 @@ I’m excited to be part of the next generation of video editing technology.
134134
## Compatability
135135

136136
✅ Supported
137-
⏰ Not yet supported
138-
❌ Not planned
139-
🔬 Not tested
137+
🧪 Experimental
138+
❌ Not supported
140139

141140
### Desktop
142141

143142
| Browser | | Operating System | |
144143
| ----------------- | -- | ----------------- | -- |
145144
| Chrome || Windows ||
146145
| Edge || Macos ||
147-
| Firefox | | Linux ||
148-
| Safari | |
149-
| Opera | 🔬 |
146+
| Firefox | 🧪 | Linux ||
147+
| Safari | |
148+
| Opera | |
150149
| Brave ||
151-
| Vivaldi | 🔬 |
150+
| Vivaldi | |
152151

153152

154153
### Mobile
155154

156155
| Browser | | Operating System | |
157156
| ----------------- | -- | ----------------- | -- |
158-
| Brave Android | 🔬 | Android | 🔬 |
159-
| Chrome Android | 🔬 | iOS | |
160-
| Firefox Android | |
161-
| Opera Android | 🔬 |
162-
| Safari iOS | |
157+
| Brave Android | | Android | |
158+
| Chrome Android | | iOS | |
159+
| Firefox Android | 🧪 |
160+
| Opera Android | |
161+
| Safari iOS | |
163162

164163

165164
| | Demultiplexing | Multiplexing |
166165
| ----------- | -------------- | -------------|
167166
| Mp4 |||
168-
| Webm || |
167+
| Webm || |
169168
| Mov |||
170-
| Mkv | ||
171-
| Avi | ||
169+
| Mkv | ||
170+
| Avi | ||
172171

173172
| | Decoding | Encoding |
174173
| ----------- | -------- | ----------------- |
175174
| Avc1 |||
176-
| Hevc |||
177-
| VP9 |||
178-
| VP8 |||
179-
| AAC || ✅ (except Linux) |
175+
| AAC || ✅ (Chromium only)|
180176
| Opus |||
177+
| Wav |||
178+
| Hevc |||
179+
| VP9 |||
180+
| VP8 |||
181181
| Mp3 |||
182182
| Ogg |||
183-
| Wav || N/A |

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@diffusionstudio/core",
33
"private": false,
4-
"version": "1.0.0-rc.4",
4+
"version": "1.0.0-rc.5",
55
"type": "module",
66
"description": "Build bleeding edge video processing applications",
77
"files": [
@@ -61,21 +61,29 @@
6161
"url": "[email protected]:diffusionstudio/core.git"
6262
},
6363
"keywords": [
64-
"edit videos",
65-
"programmatic video editing",
66-
"video editing",
64+
"mp4",
65+
"web",
66+
"aac",
67+
"h264",
68+
"opus",
69+
"edit",
70+
"webgl",
71+
"video",
72+
"audio",
73+
"record",
74+
"canvas",
75+
"tiktok",
76+
"webgpu",
77+
"encode",
78+
"editor",
79+
"decode",
80+
"editing",
81+
"youtube",
82+
"recorder",
6783
"automate",
68-
"web-based",
69-
"browser-based",
70-
"automation",
71-
"ai video editor",
72-
"video apps",
73-
"video applications",
74-
"create videos",
75-
"automate workflows",
76-
"automated video editing",
77-
"free video editor",
78-
"editor for youtube",
79-
"online video editor"
84+
"webcodecs",
85+
"client-side",
86+
"programmatic",
87+
"browser-based"
8088
]
8189
}

playground/controls.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import * as core from '../src';
2-
import { exportComposition } from './export';
2+
import { render } from './render';
33

44
export function setupControls(composition: core.Composition) {
55
const handlePlay = () => composition.play();
66
const handlePause = () => composition.pause();
77
const handleBack = () => composition.seek(0);
88
const handleForward = () => composition.seek(composition.duration.frames);
9-
const handleExport = () => exportComposition(composition);
9+
const handleExport = () => render(composition);
1010

1111
playButton.addEventListener('click', handlePlay);
1212
pauseButton.addEventListener('click', handlePause);

playground/main.ts

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -104,24 +104,21 @@ await composition.add(
104104
);
105105

106106
await composition.add(
107-
new core.ImageClip(
108-
await core.ImageSource.from('/dvd_logo.svg'),
109-
{
110-
stop: composition.duration,
111-
x(this: core.TextClip, time: core.Timestamp) {
112-
const width = typeof this.width == 'number' ? this.width : 0;
113-
const range = composition.width - width;
114-
const x = (time.seconds * 500) % (range * 2);
107+
new core.ImageClip(await core.ImageSource.from('/dvd_logo.svg'), {
108+
stop: composition.duration,
109+
x(this: core.TextClip, time: core.Timestamp) {
110+
const width = typeof this.width == 'number' ? this.width : 0;
111+
const range = composition.width - width;
112+
const x = (time.seconds * 500) % (range * 2);
115113

116-
return x > range ? range * 2 - x : x;
117-
},
118-
y(this: core.TextClip, time: core.Timestamp) {
119-
const height = typeof this.height == 'number' ? this.height : 0;
120-
const range = composition.height - height;
121-
const y = (time.seconds * 200) % (range * 2);
114+
return x > range ? range * 2 - x : x;
115+
},
116+
y(this: core.TextClip, time: core.Timestamp) {
117+
const height = typeof this.height == 'number' ? this.height : 0;
118+
const range = composition.height - height;
119+
const y = (time.seconds * 200) % (range * 2);
122120

123-
return y > range ? range * 2 - y : y;
124-
},
125-
}
126-
)
121+
return y > range ? range * 2 - y : y;
122+
},
123+
})
127124
);

playground/export.ts renamed to playground/render.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ import * as core from '../src';
33

44
let fps = 30;
55

6-
export async function exportComposition(composition: core.Composition) {
6+
export async function render(composition: core.Composition) {
77
if (loader.style.display != 'none') return;
88

99
try {
10-
const encoder = new core.WebcodecsEncoder(composition, { debug: true, fps });
10+
const encoder = new core.Encoder(composition, { debug: true, fps });
1111

1212
encoder.on('render', (event) => {
1313
const { progress, total } = event.detail;
@@ -24,15 +24,17 @@ export async function exportComposition(composition: core.Composition) {
2424
},
2525
],
2626
});
27+
2728
loader.style.display = 'block';
28-
await encoder.export(fileHandle);
29+
await encoder.render(fileHandle);
2930
} catch (e) {
3031
if (e instanceof DOMException) {
32+
console.log(e)
3133
// user canceled file picker
32-
} else if (e instanceof core.ExportError) {
34+
} else if (e instanceof core.EncoderError) {
3335
alert(e.message);
3436
} else {
35-
alert(String(e))
37+
alert(String(e));
3638
}
3739
} finally {
3840
loader.style.display = 'none';
@@ -52,3 +54,7 @@ fpsButton.addEventListener('click', () => {
5254

5355
if (!Number.isNaN(value)) fps = value
5456
});
57+
58+
if (!('showSaveFilePicker' in window)) {
59+
Object.assign(window, { showSaveFilePicker: async () => undefined });
60+
}

src/clips/video/video.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ export class VideoClip extends VisualMixin(MediaClip<VideoClipProps>) {
4747
public constructor(source?: File | VideoSource, props: VideoClipProps = {}) {
4848
super();
4949

50-
(this.textrues.html5.source as any).playsInline = true;
50+
this.element.controls = false;
51+
this.element.playsInline = true;
52+
this.element.style.display = 'hidden';
53+
5154
(this.textrues.html5.source as any).autoPlay = false;
5255
(this.textrues.html5.source as any).loop = false;
5356
this.sprite.texture = this.textrues.html5;
@@ -96,11 +99,11 @@ export class VideoClip extends VisualMixin(MediaClip<VideoClipProps>) {
9699
}
97100

98101
public async connect(track: Track<VideoClip>): Promise<void> {
102+
super.connect(track);
103+
99104
// without seeking the first frame a black frame will be rendered
100105
const frame = track.composition?.frame ?? 0;
101106
await this.seek(Timestamp.fromFrames(frame));
102-
103-
super.connect(track);
104107
}
105108

106109
@visualize

src/composition/composition.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ export class Composition extends EventEmitterMixin<CompositionEvents, typeof Ser
402402
this.trigger('pause', this.frame);
403403
}
404404

405-
public async audio(numberOfChannels = 2, sampleRate = 44100): Promise<AudioBuffer> {
405+
public async audio(numberOfChannels = 2, sampleRate = 48000): Promise<AudioBuffer> {
406406
const length = this.duration.seconds * sampleRate;
407407
const context = new OfflineAudioContext({
408408
sampleRate,
@@ -445,7 +445,7 @@ export class Composition extends EventEmitterMixin<CompositionEvents, typeof Ser
445445
output.getChannelData(i).set(outputData);
446446
}
447447
} catch (e) {
448-
console.warn('could not get channel data', e);
448+
console.error('could not get channel data', e);
449449
}
450450

451451
clip.source.audioBuffer = undefined;

0 commit comments

Comments
 (0)