@smoove/renderer 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +57 -0
- package/dist/audio-mix.d.ts +14 -0
- package/dist/audio-mix.d.ts.map +1 -0
- package/dist/audio-mix.js +127 -0
- package/dist/audio-mix.js.map +1 -0
- package/dist/audio-source-null.d.ts +25 -0
- package/dist/audio-source-null.d.ts.map +1 -0
- package/dist/audio-source-null.js +26 -0
- package/dist/audio-source-null.js.map +1 -0
- package/dist/audio-track.d.ts +17 -0
- package/dist/audio-track.d.ts.map +1 -0
- package/dist/audio-track.js +91 -0
- package/dist/audio-track.js.map +1 -0
- package/dist/encode.d.ts +45 -0
- package/dist/encode.d.ts.map +1 -0
- package/dist/encode.js +105 -0
- package/dist/encode.js.map +1 -0
- package/dist/font-loader.d.ts +11 -0
- package/dist/font-loader.d.ts.map +1 -0
- package/dist/font-loader.js +52 -0
- package/dist/font-loader.js.map +1 -0
- package/dist/gl-node.d.ts +10 -0
- package/dist/gl-node.d.ts.map +1 -0
- package/dist/gl-node.js +61 -0
- package/dist/gl-node.js.map +1 -0
- package/dist/gl.d.ts +12 -0
- package/dist/gl.d.ts.map +1 -0
- package/dist/gl.js +28 -0
- package/dist/gl.js.map +1 -0
- package/dist/image-loader.d.ts +9 -0
- package/dist/image-loader.d.ts.map +1 -0
- package/dist/image-loader.js +20 -0
- package/dist/image-loader.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/media-input-source.d.ts +8 -0
- package/dist/media-input-source.d.ts.map +1 -0
- package/dist/media-input-source.js +22 -0
- package/dist/media-input-source.js.map +1 -0
- package/dist/media-server.d.ts +7 -0
- package/dist/media-server.d.ts.map +1 -0
- package/dist/media-server.js +14 -0
- package/dist/media-server.js.map +1 -0
- package/dist/probe.d.ts +5 -0
- package/dist/probe.d.ts.map +1 -0
- package/dist/probe.js +13 -0
- package/dist/probe.js.map +1 -0
- package/dist/register.d.ts +2 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +6 -0
- package/dist/register.js.map +1 -0
- package/dist/render.d.ts +18 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +190 -0
- package/dist/render.js.map +1 -0
- package/dist/setup.d.ts +12 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +46 -0
- package/dist/setup.js.map +1 -0
- package/dist/skia.d.ts +8 -0
- package/dist/skia.d.ts.map +1 -0
- package/dist/skia.js +15 -0
- package/dist/skia.js.map +1 -0
- package/dist/types.d.ts +153 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/video-source-mediabunny.d.ts +63 -0
- package/dist/video-source-mediabunny.d.ts.map +1 -0
- package/dist/video-source-mediabunny.js +239 -0
- package/dist/video-source-mediabunny.js.map +1 -0
- package/package.json +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 shemi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @smoove/renderer
|
|
2
|
+
|
|
3
|
+
Headless video renderer for [smoove](https://smoove.dev) — turn a
|
|
4
|
+
`Composition` into an MP4 or WebM file from Node.
|
|
5
|
+
|
|
6
|
+
`renderComposition` walks the frame clock the same way the player does,
|
|
7
|
+
rasterizes each frame with [skia-canvas](https://skia-canvas.org), and
|
|
8
|
+
encodes the result with [Mediabunny](https://mediabunny.dev) in a single
|
|
9
|
+
in-process pass: no ffmpeg binary, no temp files. The composition you
|
|
10
|
+
preview in the browser is the file you ship.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
pnpm add konva @smoove/core @smoove/renderer
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`konva` and `@smoove/core` are peer dependencies
|
|
19
|
+
(`@smoove/transitions` is an optional peer). Node-only.
|
|
20
|
+
|
|
21
|
+
## Quick example
|
|
22
|
+
|
|
23
|
+
Import the `register` entry first — it installs the skia backend and the
|
|
24
|
+
Node media factories. Do this before you build the composition:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import "@smoove/renderer/register";
|
|
28
|
+
import { Composition, interpolate, Rect, Sequence } from "@smoove/core";
|
|
29
|
+
import { renderComposition } from "@smoove/renderer";
|
|
30
|
+
|
|
31
|
+
const comp = new Composition({ id: "out", fps: 60, durationInFrames: 120, width: 1280, height: 720 });
|
|
32
|
+
|
|
33
|
+
const main = new Sequence({ from: 0, durationInFrames: 120 });
|
|
34
|
+
const box = new Rect({ x: 0, y: 320, width: 80, height: 80, fill: "#ffd166" });
|
|
35
|
+
main.add(box);
|
|
36
|
+
main.register((f) => box.x(interpolate(f, [0, 119], [0, 1200])));
|
|
37
|
+
comp.add(main);
|
|
38
|
+
|
|
39
|
+
await renderComposition(comp, { output: "out.mp4" });
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Only `output` is required; everything else falls back to the composition's
|
|
43
|
+
own settings. Options include `resolution`, `fit`, `quality` presets,
|
|
44
|
+
`fps`, a frame `range`, `format` (`"mp4"` or `"webm"`), `mute`, `fonts`,
|
|
45
|
+
`onProgress`, and an `AbortSignal`. Single frames and raw frame streams are
|
|
46
|
+
also supported.
|
|
47
|
+
|
|
48
|
+
To render WebGL shader transitions headlessly, import `@smoove/renderer/gl`
|
|
49
|
+
before building the composition.
|
|
50
|
+
|
|
51
|
+
## Docs
|
|
52
|
+
|
|
53
|
+
Full documentation lives at [smoove.dev](https://smoove.dev).
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AudioClip } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Decode every {@link AudioClip} and mix it into one interleaved stereo f32 PCM
|
|
4
|
+
* timeline at {@link AUDIO_SAMPLE_RATE}. Replaces the ffmpeg
|
|
5
|
+
* `atrim/atempo/volume/adelay/amix` filtergraph: each clip is decoded with a
|
|
6
|
+
* Mediabunny {@link AudioSampleSink}, resampled for its playback rate + source
|
|
7
|
+
* rate, gain-shaped by its volume envelope, delayed to its start frame, and
|
|
8
|
+
* summed. The result is fed to the encoder as {@link AudioSample}s.
|
|
9
|
+
*
|
|
10
|
+
* Returns `null` when nothing decodes to audible PCM (so the caller can skip the
|
|
11
|
+
* audio track entirely).
|
|
12
|
+
*/
|
|
13
|
+
export declare function mixAudio(clips: AudioClip[], fps: number, totalSeconds: number, fromFrame?: number): Promise<Float32Array | null>;
|
|
14
|
+
//# sourceMappingURL=audio-mix.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audio-mix.d.ts","sourceRoot":"","sources":["../src/audio-mix.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAkB,MAAM,YAAY,CAAC;AAE5D;;;;;;;;;;GAUG;AACH,wBAAsB,QAAQ,CAC5B,KAAK,EAAE,SAAS,EAAE,EAClB,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,MAAM,EACpB,SAAS,SAAI,GACZ,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAiB9B"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { ALL_FORMATS, AudioSampleSink, Input } from "mediabunny";
|
|
2
|
+
import { AUDIO_CHANNELS, AUDIO_SAMPLE_RATE } from "./encode.js";
|
|
3
|
+
import { makeInputSource } from "./media-input-source.js";
|
|
4
|
+
/**
|
|
5
|
+
* Decode every {@link AudioClip} and mix it into one interleaved stereo f32 PCM
|
|
6
|
+
* timeline at {@link AUDIO_SAMPLE_RATE}. Replaces the ffmpeg
|
|
7
|
+
* `atrim/atempo/volume/adelay/amix` filtergraph: each clip is decoded with a
|
|
8
|
+
* Mediabunny {@link AudioSampleSink}, resampled for its playback rate + source
|
|
9
|
+
* rate, gain-shaped by its volume envelope, delayed to its start frame, and
|
|
10
|
+
* summed. The result is fed to the encoder as {@link AudioSample}s.
|
|
11
|
+
*
|
|
12
|
+
* Returns `null` when nothing decodes to audible PCM (so the caller can skip the
|
|
13
|
+
* audio track entirely).
|
|
14
|
+
*/
|
|
15
|
+
export async function mixAudio(clips, fps, totalSeconds, fromFrame = 0) {
|
|
16
|
+
if (clips.length === 0)
|
|
17
|
+
return null;
|
|
18
|
+
const totalFrames = Math.ceil(totalSeconds * AUDIO_SAMPLE_RATE);
|
|
19
|
+
const mix = new Float32Array(totalFrames * AUDIO_CHANNELS);
|
|
20
|
+
let mixed = 0;
|
|
21
|
+
for (const clip of clips) {
|
|
22
|
+
mixed += await mixClip(mix, totalFrames, clip, fps, fromFrame);
|
|
23
|
+
}
|
|
24
|
+
if (mixed === 0)
|
|
25
|
+
return null;
|
|
26
|
+
// Sum can exceed unity when clips overlap; clamp so the encoder gets valid PCM.
|
|
27
|
+
for (let i = 0; i < mix.length; i++) {
|
|
28
|
+
const v = mix[i];
|
|
29
|
+
mix[i] = v > 1 ? 1 : v < -1 ? -1 : v;
|
|
30
|
+
}
|
|
31
|
+
return mix;
|
|
32
|
+
}
|
|
33
|
+
/** Decode one clip and add it into `mix`. Returns the number of frames written. */
|
|
34
|
+
async function mixClip(mix, totalFrames, clip, fps, fromFrame) {
|
|
35
|
+
const compDurSec = (clip.endFrame - clip.startFrame + 1) / fps;
|
|
36
|
+
const srcDurSec = compDurSec * clip.playbackRate;
|
|
37
|
+
const startSec = (clip.startFrame - fromFrame) / fps;
|
|
38
|
+
const input = new Input({ formats: ALL_FORMATS, source: makeInputSource(clip.src) });
|
|
39
|
+
try {
|
|
40
|
+
const track = await input.getPrimaryAudioTrack();
|
|
41
|
+
if (!track || !(await track.canDecode()))
|
|
42
|
+
return 0;
|
|
43
|
+
const sink = new AudioSampleSink(track);
|
|
44
|
+
// Decode the consumed source region into contiguous per-channel buffers.
|
|
45
|
+
const parts = [];
|
|
46
|
+
let srcChannels = 0;
|
|
47
|
+
let srcRate = 0;
|
|
48
|
+
for await (const s of sink.samples(clip.mediaInSeconds, clip.mediaInSeconds + srcDurSec)) {
|
|
49
|
+
srcChannels = s.numberOfChannels;
|
|
50
|
+
srcRate = s.sampleRate;
|
|
51
|
+
const buf = new Float32Array(s.allocationSize({ planeIndex: 0, format: "f32" }) / 4);
|
|
52
|
+
s.copyTo(buf, { planeIndex: 0, format: "f32" });
|
|
53
|
+
parts.push({ ts: s.timestamp, data: buf, frames: s.numberOfFrames });
|
|
54
|
+
s.close();
|
|
55
|
+
}
|
|
56
|
+
if (parts.length === 0 || srcChannels === 0)
|
|
57
|
+
return 0;
|
|
58
|
+
const totalSrc = parts.reduce((n, p) => n + p.frames, 0);
|
|
59
|
+
const chans = Array.from({ length: srcChannels }, () => new Float32Array(totalSrc));
|
|
60
|
+
let w = 0;
|
|
61
|
+
for (const p of parts) {
|
|
62
|
+
for (let i = 0; i < p.frames; i++) {
|
|
63
|
+
const base = i * srcChannels;
|
|
64
|
+
for (let c = 0; c < srcChannels; c++)
|
|
65
|
+
chans[c][w + i] = p.data[base + c];
|
|
66
|
+
}
|
|
67
|
+
w += p.frames;
|
|
68
|
+
}
|
|
69
|
+
const origin = parts[0].ts;
|
|
70
|
+
const left = chans[0];
|
|
71
|
+
const right = (srcChannels > 1 ? chans[1] : chans[0]);
|
|
72
|
+
const outStart = Math.round(startSec * AUDIO_SAMPLE_RATE);
|
|
73
|
+
const outLen = Math.round(compDurSec * AUDIO_SAMPLE_RATE);
|
|
74
|
+
let written = 0;
|
|
75
|
+
for (let i = 0; i < outLen; i++) {
|
|
76
|
+
const outIdx = outStart + i;
|
|
77
|
+
if (outIdx < 0 || outIdx >= totalFrames)
|
|
78
|
+
continue;
|
|
79
|
+
const compRel = i / AUDIO_SAMPLE_RATE;
|
|
80
|
+
const srcAbs = clip.mediaInSeconds + compRel * clip.playbackRate;
|
|
81
|
+
const pos = (srcAbs - origin) * srcRate;
|
|
82
|
+
const i0 = Math.floor(pos);
|
|
83
|
+
if (i0 < 0 || i0 >= totalSrc)
|
|
84
|
+
continue;
|
|
85
|
+
const frac = pos - i0;
|
|
86
|
+
const vol = evalVolume(clip.volume, compRel);
|
|
87
|
+
const li = outIdx * 2;
|
|
88
|
+
mix[li] = mix[li] + sampleAt(left, i0, frac, totalSrc) * vol;
|
|
89
|
+
mix[li + 1] = mix[li + 1] + sampleAt(right, i0, frac, totalSrc) * vol;
|
|
90
|
+
written++;
|
|
91
|
+
}
|
|
92
|
+
return written;
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
input.dispose();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Linear interpolation between frame `i0` and `i0+1`, holding the last frame. */
|
|
99
|
+
function sampleAt(buf, i0, frac, len) {
|
|
100
|
+
const a = buf[i0];
|
|
101
|
+
if (i0 + 1 >= len || frac === 0)
|
|
102
|
+
return a;
|
|
103
|
+
const b = buf[i0 + 1];
|
|
104
|
+
return a + (b - a) * frac;
|
|
105
|
+
}
|
|
106
|
+
/** Evaluate a constant level or piecewise-linear envelope at clip-relative time `t` (seconds). */
|
|
107
|
+
function evalVolume(volume, t) {
|
|
108
|
+
if (typeof volume === "number")
|
|
109
|
+
return volume;
|
|
110
|
+
if (volume.length === 0)
|
|
111
|
+
return 1;
|
|
112
|
+
if (volume.length === 1)
|
|
113
|
+
return volume[0].volume;
|
|
114
|
+
const first = volume[0];
|
|
115
|
+
if (t <= first.time)
|
|
116
|
+
return first.volume;
|
|
117
|
+
for (let i = 1; i < volume.length; i++) {
|
|
118
|
+
const b = volume[i];
|
|
119
|
+
if (t <= b.time) {
|
|
120
|
+
const a = volume[i - 1];
|
|
121
|
+
const span = b.time - a.time || 1;
|
|
122
|
+
return a.volume + ((b.volume - a.volume) * (t - a.time)) / span;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return volume[volume.length - 1].volume;
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=audio-mix.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audio-mix.js","sourceRoot":"","sources":["../src/audio-mix.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { AudioSource, AudioSourceFactory } from "@smoove/core";
|
|
2
|
+
/**
|
|
3
|
+
* No-op {@link AudioSource} for server rendering. Audio is not decoded by the
|
|
4
|
+
* node during frame rendering — `RenderingAudioDriver` only records timing/volume
|
|
5
|
+
* metadata per frame (collected via `comp.getAudioAssets()`), which the
|
|
6
|
+
* {@link mixAudio} pass later decodes (via Mediabunny) and mixes into the muxed
|
|
7
|
+
* track. This source exists only so the `Audio` node can be constructed in Node
|
|
8
|
+
* without touching `document`.
|
|
9
|
+
*/
|
|
10
|
+
export declare class NullAudioSource implements AudioSource {
|
|
11
|
+
readonly duration = 0;
|
|
12
|
+
readonly currentTime = 0;
|
|
13
|
+
readonly isReady = true;
|
|
14
|
+
load(): Promise<void>;
|
|
15
|
+
seek(): Promise<void>;
|
|
16
|
+
play(): Promise<void>;
|
|
17
|
+
pause(): void;
|
|
18
|
+
setMuted(): void;
|
|
19
|
+
setVolume(): void;
|
|
20
|
+
setPlaybackRate(): void;
|
|
21
|
+
onReady(): () => void;
|
|
22
|
+
destroy(): void;
|
|
23
|
+
}
|
|
24
|
+
export declare const nullAudioSourceFactory: AudioSourceFactory;
|
|
25
|
+
//# sourceMappingURL=audio-source-null.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audio-source-null.d.ts","sourceRoot":"","sources":["../src/audio-source-null.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEpE;;;;;;;GAOG;AACH,qBAAa,eAAgB,YAAW,WAAW;IACjD,QAAQ,CAAC,QAAQ,KAAK;IACtB,QAAQ,CAAC,WAAW,KAAK;IACzB,QAAQ,CAAC,OAAO,QAAQ;IAElB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IACrB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IACrB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAC3B,KAAK,IAAI,IAAI;IACb,QAAQ,IAAI,IAAI;IAChB,SAAS,IAAI,IAAI;IACjB,eAAe,IAAI,IAAI;IACvB,OAAO,IAAI,MAAM,IAAI;IAGrB,OAAO,IAAI,IAAI;CAChB;AAED,eAAO,MAAM,sBAAsB,EAAE,kBAAgD,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* No-op {@link AudioSource} for server rendering. Audio is not decoded by the
|
|
3
|
+
* node during frame rendering — `RenderingAudioDriver` only records timing/volume
|
|
4
|
+
* metadata per frame (collected via `comp.getAudioAssets()`), which the
|
|
5
|
+
* {@link mixAudio} pass later decodes (via Mediabunny) and mixes into the muxed
|
|
6
|
+
* track. This source exists only so the `Audio` node can be constructed in Node
|
|
7
|
+
* without touching `document`.
|
|
8
|
+
*/
|
|
9
|
+
export class NullAudioSource {
|
|
10
|
+
duration = 0;
|
|
11
|
+
currentTime = 0;
|
|
12
|
+
isReady = true;
|
|
13
|
+
async load() { }
|
|
14
|
+
async seek() { }
|
|
15
|
+
async play() { }
|
|
16
|
+
pause() { }
|
|
17
|
+
setMuted() { }
|
|
18
|
+
setVolume() { }
|
|
19
|
+
setPlaybackRate() { }
|
|
20
|
+
onReady() {
|
|
21
|
+
return () => { };
|
|
22
|
+
}
|
|
23
|
+
destroy() { }
|
|
24
|
+
}
|
|
25
|
+
export const nullAudioSourceFactory = () => new NullAudioSource();
|
|
26
|
+
//# sourceMappingURL=audio-source-null.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audio-source-null.js","sourceRoot":"","sources":["../src/audio-source-null.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,MAAM,OAAO,eAAe;IACjB,QAAQ,GAAG,CAAC,CAAC;IACb,WAAW,GAAG,CAAC,CAAC;IAChB,OAAO,GAAG,IAAI,CAAC;IAExB,KAAK,CAAC,IAAI,KAAmB,CAAC;IAC9B,KAAK,CAAC,IAAI,KAAmB,CAAC;IAC9B,KAAK,CAAC,IAAI,KAAmB,CAAC;IAC9B,KAAK,KAAU,CAAC;IAChB,QAAQ,KAAU,CAAC;IACnB,SAAS,KAAU,CAAC;IACpB,eAAe,KAAU,CAAC;IAC1B,OAAO;QACL,OAAO,GAAG,EAAE,GAAE,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,KAAU,CAAC;CACnB;AAED,MAAM,CAAC,MAAM,sBAAsB,GAAuB,GAAG,EAAE,CAAC,IAAI,eAAe,EAAE,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Composition } from "@smoove/core";
|
|
2
|
+
import type { AudioClip, VolumeKeyframe } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Coalesce the per-frame audio samples collected during a render
|
|
5
|
+
* (`comp.getAudioAssets()`) into clips. Samples are grouped by source id,
|
|
6
|
+
* muted/silent ones dropped, then split into runs of consecutive frames — each
|
|
7
|
+
* run becomes one {@link AudioClip} the {@link mixAudio} pass decodes and mixes.
|
|
8
|
+
*/
|
|
9
|
+
export declare function collectAudioTrack(comp: Composition, fps: number): AudioClip[];
|
|
10
|
+
/**
|
|
11
|
+
* Reduce a dense per-frame volume curve to its breakpoints (Ramer–Douglas–
|
|
12
|
+
* Peucker on vertical error). A linear ramp collapses to two points, a constant
|
|
13
|
+
* run to two — keeping the volume envelope evaluated by the mixer compact while
|
|
14
|
+
* preserving the shape. `eps` is the max allowed volume error (0..1).
|
|
15
|
+
*/
|
|
16
|
+
export declare function simplifyEnvelope(kfs: VolumeKeyframe[], eps?: number): VolumeKeyframe[];
|
|
17
|
+
//# sourceMappingURL=audio-track.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audio-track.d.ts","sourceRoot":"","sources":["../src/audio-track.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAc,WAAW,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE5D;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,CA4B7E;AAwBD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,cAAc,EAAE,EAAE,GAAG,SAAQ,GAAG,cAAc,EAAE,CA4BrF"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coalesce the per-frame audio samples collected during a render
|
|
3
|
+
* (`comp.getAudioAssets()`) into clips. Samples are grouped by source id,
|
|
4
|
+
* muted/silent ones dropped, then split into runs of consecutive frames — each
|
|
5
|
+
* run becomes one {@link AudioClip} the {@link mixAudio} pass decodes and mixes.
|
|
6
|
+
*/
|
|
7
|
+
export function collectAudioTrack(comp, fps) {
|
|
8
|
+
const audible = comp.getAudioAssets().filter((a) => !a.muted && a.volume > 0);
|
|
9
|
+
const byId = new Map();
|
|
10
|
+
for (const a of audible) {
|
|
11
|
+
const arr = byId.get(a.id);
|
|
12
|
+
if (arr)
|
|
13
|
+
arr.push(a);
|
|
14
|
+
else
|
|
15
|
+
byId.set(a.id, [a]);
|
|
16
|
+
}
|
|
17
|
+
const clips = [];
|
|
18
|
+
for (const [id, group] of byId) {
|
|
19
|
+
group.sort((x, y) => x.frame - y.frame);
|
|
20
|
+
let run = [];
|
|
21
|
+
let prevFrame = Number.NaN;
|
|
22
|
+
for (const a of group) {
|
|
23
|
+
if (run.length > 0 && a.frame !== prevFrame + 1) {
|
|
24
|
+
clips.push(makeClip(id, run, fps));
|
|
25
|
+
run = [];
|
|
26
|
+
}
|
|
27
|
+
run.push(a);
|
|
28
|
+
prevFrame = a.frame;
|
|
29
|
+
}
|
|
30
|
+
if (run.length > 0)
|
|
31
|
+
clips.push(makeClip(id, run, fps));
|
|
32
|
+
}
|
|
33
|
+
clips.sort((a, b) => a.startFrame - b.startFrame);
|
|
34
|
+
return clips;
|
|
35
|
+
}
|
|
36
|
+
function makeClip(id, run, fps) {
|
|
37
|
+
const first = run[0];
|
|
38
|
+
const last = run[run.length - 1];
|
|
39
|
+
const startFrame = first.frame;
|
|
40
|
+
const baseVolume = first.volume;
|
|
41
|
+
const varies = run.some((r) => Math.abs(r.volume - baseVolume) > 1e-6);
|
|
42
|
+
const volume = varies
|
|
43
|
+
? simplifyEnvelope(run.map((r) => ({ time: (r.frame - startFrame) / fps, volume: r.volume })))
|
|
44
|
+
: baseVolume;
|
|
45
|
+
return {
|
|
46
|
+
id,
|
|
47
|
+
src: first.src,
|
|
48
|
+
startFrame,
|
|
49
|
+
endFrame: last.frame,
|
|
50
|
+
mediaInSeconds: first.mediaTime,
|
|
51
|
+
playbackRate: first.playbackRate,
|
|
52
|
+
volume,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Reduce a dense per-frame volume curve to its breakpoints (Ramer–Douglas–
|
|
57
|
+
* Peucker on vertical error). A linear ramp collapses to two points, a constant
|
|
58
|
+
* run to two — keeping the volume envelope evaluated by the mixer compact while
|
|
59
|
+
* preserving the shape. `eps` is the max allowed volume error (0..1).
|
|
60
|
+
*/
|
|
61
|
+
export function simplifyEnvelope(kfs, eps = 0.015) {
|
|
62
|
+
if (kfs.length <= 2)
|
|
63
|
+
return kfs;
|
|
64
|
+
const keep = new Uint8Array(kfs.length);
|
|
65
|
+
keep[0] = 1;
|
|
66
|
+
keep[kfs.length - 1] = 1;
|
|
67
|
+
const stack = [[0, kfs.length - 1]];
|
|
68
|
+
while (stack.length > 0) {
|
|
69
|
+
const [lo, hi] = stack.pop();
|
|
70
|
+
const a = kfs[lo];
|
|
71
|
+
const b = kfs[hi];
|
|
72
|
+
const span = b.time - a.time || 1;
|
|
73
|
+
let maxErr = -1;
|
|
74
|
+
let idx = -1;
|
|
75
|
+
for (let i = lo + 1; i < hi; i++) {
|
|
76
|
+
const p = kfs[i];
|
|
77
|
+
const lineV = a.volume + ((b.volume - a.volume) * (p.time - a.time)) / span;
|
|
78
|
+
const err = Math.abs(p.volume - lineV);
|
|
79
|
+
if (err > maxErr) {
|
|
80
|
+
maxErr = err;
|
|
81
|
+
idx = i;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (maxErr > eps && idx >= 0) {
|
|
85
|
+
keep[idx] = 1;
|
|
86
|
+
stack.push([lo, idx], [idx, hi]);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return kfs.filter((_, i) => keep[i] === 1);
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=audio-track.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audio-track.js","sourceRoot":"","sources":["../src/audio-track.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAiB,EAAE,GAAW;IAC9D,MAAM,OAAO,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAE9E,MAAM,IAAI,GAAG,IAAI,GAAG,EAAwB,CAAC;IAC7C,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC3B,IAAI,GAAG;YAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;;YAChB,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3B,CAAC;IAED,MAAM,KAAK,GAAgB,EAAE,CAAC;IAC9B,KAAK,MAAM,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,IAAI,EAAE,CAAC;QAC/B,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;QACxC,IAAI,GAAG,GAAiB,EAAE,CAAC;QAC3B,IAAI,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC;QAC3B,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;YACtB,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC,KAAK,KAAK,SAAS,GAAG,CAAC,EAAE,CAAC;gBAChD,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;gBACnC,GAAG,GAAG,EAAE,CAAC;YACX,CAAC;YACD,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACZ,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC;QACtB,CAAC;QACD,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;IACzD,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC;IAClD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,QAAQ,CAAC,EAAU,EAAE,GAAiB,EAAE,GAAW;IAC1D,MAAM,KAAK,GAAG,GAAG,CAAC,CAAC,CAAe,CAAC;IACnC,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAe,CAAC;IAC/C,MAAM,UAAU,GAAG,KAAK,CAAC,KAAK,CAAC;IAC/B,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC;IAChC,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,GAAG,UAAU,CAAC,GAAG,IAAI,CAAC,CAAC;IAEvE,MAAM,MAAM,GAA8B,MAAM;QAC9C,CAAC,CAAC,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,UAAU,CAAC,GAAG,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QAC9F,CAAC,CAAC,UAAU,CAAC;IAEf,OAAO;QACL,EAAE;QACF,GAAG,EAAE,KAAK,CAAC,GAAG;QACd,UAAU;QACV,QAAQ,EAAE,IAAI,CAAC,KAAK;QACpB,cAAc,EAAE,KAAK,CAAC,SAAS;QAC/B,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,MAAM;KACP,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAqB,EAAE,GAAG,GAAG,KAAK;IACjE,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC;QAAE,OAAO,GAAG,CAAC;IAChC,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACxC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACZ,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;IACzB,MAAM,KAAK,GAA4B,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;IAC7D,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,GAAG,EAAsB,CAAC;QACjD,MAAM,CAAC,GAAG,GAAG,CAAC,EAAE,CAAmB,CAAC;QACpC,MAAM,CAAC,GAAG,GAAG,CAAC,EAAE,CAAmB,CAAC;QACpC,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC;QAClC,IAAI,MAAM,GAAG,CAAC,CAAC,CAAC;QAChB,IAAI,GAAG,GAAG,CAAC,CAAC,CAAC;QACb,KAAK,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;YACjC,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,CAAmB,CAAC;YACnC,MAAM,KAAK,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC;YAC5E,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC;YACvC,IAAI,GAAG,GAAG,MAAM,EAAE,CAAC;gBACjB,MAAM,GAAG,GAAG,CAAC;gBACb,GAAG,GAAG,CAAC,CAAC;YACV,CAAC;QACH,CAAC;QACD,IAAI,MAAM,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC;YAC7B,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACd,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC;QACnC,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;AAC7C,CAAC"}
|
package/dist/encode.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Writable } from "node:stream";
|
|
2
|
+
import type { QualityConfig } from "./types.js";
|
|
3
|
+
/** Output PCM format the audio mixer produces and the encoder consumes. */
|
|
4
|
+
export declare const AUDIO_SAMPLE_RATE = 48000;
|
|
5
|
+
export declare const AUDIO_CHANNELS = 2;
|
|
6
|
+
export type EncodeTarget = {
|
|
7
|
+
kind: "file";
|
|
8
|
+
path: string;
|
|
9
|
+
}
|
|
10
|
+
/** A node stream to pipe a fragmented container into (for `renderToStream`). */
|
|
11
|
+
| {
|
|
12
|
+
kind: "stream";
|
|
13
|
+
writable: Writable;
|
|
14
|
+
};
|
|
15
|
+
export interface EncoderOptions {
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
fps: number;
|
|
19
|
+
format: "mp4" | "webm";
|
|
20
|
+
quality: QualityConfig;
|
|
21
|
+
hasAudio: boolean;
|
|
22
|
+
target: EncodeTarget;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Single-pass video+audio encoder built on Mediabunny's {@link Output}. Replaces
|
|
26
|
+
* the old two-pass ffmpeg flow (raw-RGBA pipe → temp file → mux): video frames
|
|
27
|
+
* are added as {@link VideoSample}s, mixed audio as {@link AudioSample}s, and one
|
|
28
|
+
* `finalize()` muxes the container — no temp files, no child processes.
|
|
29
|
+
*/
|
|
30
|
+
export declare class MediabunnyEncoder {
|
|
31
|
+
private readonly _output;
|
|
32
|
+
private readonly _video;
|
|
33
|
+
private readonly _audio;
|
|
34
|
+
private readonly _width;
|
|
35
|
+
private readonly _height;
|
|
36
|
+
constructor(opts: EncoderOptions);
|
|
37
|
+
start(): Promise<void>;
|
|
38
|
+
/** Add one RGBA frame (`width*height*4` bytes) at `timestamp` seconds. */
|
|
39
|
+
addFrame(rgba: Uint8Array, timestamp: number, duration: number): Promise<void>;
|
|
40
|
+
/** Add a chunk of interleaved f32 PCM ({@link AUDIO_CHANNELS} @ {@link AUDIO_SAMPLE_RATE}). */
|
|
41
|
+
addAudioChunk(data: Float32Array, timestamp: number): Promise<void>;
|
|
42
|
+
finalize(): Promise<void>;
|
|
43
|
+
cancel(): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=encode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encode.d.ts","sourceRoot":"","sources":["../src/encode.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAa5C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,2EAA2E;AAC3E,eAAO,MAAM,iBAAiB,QAAS,CAAC;AACxC,eAAO,MAAM,cAAc,IAAI,CAAC;AAEhC,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AAChC,gFAAgF;GAC9E;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;AAE3C,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC;IACvB,OAAO,EAAE,aAAa,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,YAAY,CAAC;CACtB;AAED;;;;;GAKG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoB;IAC3C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA2B;IAClD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAErB,IAAI,EAAE,cAAc;IAmChC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAItB,0EAA0E;IACpE,QAAQ,CAAC,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAepF,+FAA+F;IACzF,aAAa,CAAC,IAAI,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBzE,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAIzB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;CAGxB"}
|
package/dist/encode.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { AppendOnlyStreamTarget, AudioSample, AudioSampleSource, FilePathTarget, Mp4OutputFormat, Output, VideoSample, VideoSampleSource, WebMOutputFormat, } from "mediabunny";
|
|
2
|
+
/** Output PCM format the audio mixer produces and the encoder consumes. */
|
|
3
|
+
export const AUDIO_SAMPLE_RATE = 48_000;
|
|
4
|
+
export const AUDIO_CHANNELS = 2;
|
|
5
|
+
/**
|
|
6
|
+
* Single-pass video+audio encoder built on Mediabunny's {@link Output}. Replaces
|
|
7
|
+
* the old two-pass ffmpeg flow (raw-RGBA pipe → temp file → mux): video frames
|
|
8
|
+
* are added as {@link VideoSample}s, mixed audio as {@link AudioSample}s, and one
|
|
9
|
+
* `finalize()` muxes the container — no temp files, no child processes.
|
|
10
|
+
*/
|
|
11
|
+
export class MediabunnyEncoder {
|
|
12
|
+
_output;
|
|
13
|
+
_video;
|
|
14
|
+
_audio;
|
|
15
|
+
_width;
|
|
16
|
+
_height;
|
|
17
|
+
constructor(opts) {
|
|
18
|
+
this._width = opts.width;
|
|
19
|
+
this._height = opts.height;
|
|
20
|
+
const streaming = opts.target.kind === "stream";
|
|
21
|
+
const target = opts.target.kind === "file"
|
|
22
|
+
? new FilePathTarget(opts.target.path)
|
|
23
|
+
: new AppendOnlyStreamTarget(nodeWritableToWeb(opts.target.writable));
|
|
24
|
+
// Streaming wants a sequential, fragmented container; files get an
|
|
25
|
+
// in-memory fast-start so the moov atom lands up front (web-playable).
|
|
26
|
+
const format = opts.format === "webm"
|
|
27
|
+
? new WebMOutputFormat()
|
|
28
|
+
: new Mp4OutputFormat({ fastStart: streaming ? "fragmented" : "in-memory" });
|
|
29
|
+
this._output = new Output({ format, target });
|
|
30
|
+
const videoCodec = opts.format === "webm" ? "vp9" : "avc";
|
|
31
|
+
this._video = new VideoSampleSource({ codec: videoCodec, bitrate: opts.quality.videoBitrate });
|
|
32
|
+
this._output.addVideoTrack(this._video, { frameRate: opts.fps });
|
|
33
|
+
if (opts.hasAudio) {
|
|
34
|
+
const audioCodec = opts.format === "webm" ? "opus" : "aac";
|
|
35
|
+
this._audio = new AudioSampleSource({
|
|
36
|
+
codec: audioCodec,
|
|
37
|
+
bitrate: opts.quality.audioBitrate,
|
|
38
|
+
});
|
|
39
|
+
this._output.addAudioTrack(this._audio);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
this._audio = null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
start() {
|
|
46
|
+
return this._output.start();
|
|
47
|
+
}
|
|
48
|
+
/** Add one RGBA frame (`width*height*4` bytes) at `timestamp` seconds. */
|
|
49
|
+
async addFrame(rgba, timestamp, duration) {
|
|
50
|
+
const sample = new VideoSample(rgba, {
|
|
51
|
+
format: "RGBA",
|
|
52
|
+
codedWidth: this._width,
|
|
53
|
+
codedHeight: this._height,
|
|
54
|
+
timestamp,
|
|
55
|
+
duration,
|
|
56
|
+
});
|
|
57
|
+
try {
|
|
58
|
+
await this._video.add(sample);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
sample.close();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Add a chunk of interleaved f32 PCM ({@link AUDIO_CHANNELS} @ {@link AUDIO_SAMPLE_RATE}). */
|
|
65
|
+
async addAudioChunk(data, timestamp) {
|
|
66
|
+
if (!this._audio)
|
|
67
|
+
return;
|
|
68
|
+
const sample = new AudioSample({
|
|
69
|
+
data,
|
|
70
|
+
format: "f32",
|
|
71
|
+
numberOfChannels: AUDIO_CHANNELS,
|
|
72
|
+
sampleRate: AUDIO_SAMPLE_RATE,
|
|
73
|
+
timestamp,
|
|
74
|
+
});
|
|
75
|
+
try {
|
|
76
|
+
await this._audio.add(sample);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
sample.close();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
finalize() {
|
|
83
|
+
return this._output.finalize();
|
|
84
|
+
}
|
|
85
|
+
cancel() {
|
|
86
|
+
return this._output.cancel();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Bridge a node {@link Writable} to a web {@link WritableStream} for streaming targets. */
|
|
90
|
+
function nodeWritableToWeb(node) {
|
|
91
|
+
return new WritableStream({
|
|
92
|
+
write(chunk) {
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
node.write(chunk, (err) => (err ? reject(err) : resolve()));
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
close() {
|
|
98
|
+
node.end();
|
|
99
|
+
},
|
|
100
|
+
abort(reason) {
|
|
101
|
+
node.destroy(reason instanceof Error ? reason : new Error(String(reason)));
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=encode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encode.js","sourceRoot":"","sources":["../src/encode.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { FontLoader } from "@smoove/core";
|
|
2
|
+
/** Default on-disk cache for fonts fetched from remote URLs. */
|
|
3
|
+
export declare const DEFAULT_FONT_CACHE_DIR: string;
|
|
4
|
+
/**
|
|
5
|
+
* Build a {@link FontLoader} backed by skia-canvas `FontLibrary`. Downloads
|
|
6
|
+
* remote font URLs into `cacheDir` (default {@link DEFAULT_FONT_CACHE_DIR}) and
|
|
7
|
+
* registers each face once. Installed by `setupServerRendering` so scene-declared
|
|
8
|
+
* `Font` nodes work headlessly without a DOM `@font-face`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function makeSkiaFontLoader(cacheDir?: string): FontLoader;
|
|
11
|
+
//# sourceMappingURL=font-loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"font-loader.d.ts","sourceRoot":"","sources":["../src/font-loader.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAsB,UAAU,EAAE,MAAM,cAAc,CAAC;AAGnE,gEAAgE;AAChE,eAAO,MAAM,sBAAsB,QAAsC,CAAC;AA6B1E;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,GAAE,MAA+B,GAAG,UAAU,CAQxF"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { FontLibrary } from "skia-canvas";
|
|
7
|
+
/** Default on-disk cache for fonts fetched from remote URLs. */
|
|
8
|
+
export const DEFAULT_FONT_CACHE_DIR = path.join(tmpdir(), "smoove-fonts");
|
|
9
|
+
// Process-level dedup of FontLibrary.use() calls. skia accumulates faces into a
|
|
10
|
+
// family (and is first-wins per weight/style slot), so re-registering the same
|
|
11
|
+
// face is wasted work — track what we've already installed.
|
|
12
|
+
const registered = new Set();
|
|
13
|
+
function isRemote(src) {
|
|
14
|
+
return /^https?:\/\//i.test(src);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Resolve a face `src` to a local file path skia-canvas can load. Local paths
|
|
18
|
+
* pass through; remote URLs are downloaded once into `cacheDir` (keyed by a hash
|
|
19
|
+
* of the URL) and reused on subsequent frames and process runs.
|
|
20
|
+
*/
|
|
21
|
+
async function resolveToLocalPath(src, cacheDir) {
|
|
22
|
+
if (!isRemote(src))
|
|
23
|
+
return src;
|
|
24
|
+
const ext = path.extname(new URL(src).pathname) || ".font";
|
|
25
|
+
const file = path.join(cacheDir, `${createHash("sha256").update(src).digest("hex")}${ext}`);
|
|
26
|
+
if (existsSync(file))
|
|
27
|
+
return file;
|
|
28
|
+
const res = await fetch(src);
|
|
29
|
+
if (!res.ok)
|
|
30
|
+
throw new Error(`font fetch failed (${res.status}) for ${src}`);
|
|
31
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
32
|
+
await mkdir(cacheDir, { recursive: true });
|
|
33
|
+
await writeFile(file, buf);
|
|
34
|
+
return file;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Build a {@link FontLoader} backed by skia-canvas `FontLibrary`. Downloads
|
|
38
|
+
* remote font URLs into `cacheDir` (default {@link DEFAULT_FONT_CACHE_DIR}) and
|
|
39
|
+
* registers each face once. Installed by `setupServerRendering` so scene-declared
|
|
40
|
+
* `Font` nodes work headlessly without a DOM `@font-face`.
|
|
41
|
+
*/
|
|
42
|
+
export function makeSkiaFontLoader(cacheDir = DEFAULT_FONT_CACHE_DIR) {
|
|
43
|
+
return async (family, face) => {
|
|
44
|
+
const localPath = await resolveToLocalPath(face.src, cacheDir);
|
|
45
|
+
const key = `${family}|${face.weight}|${face.style}|${localPath}`;
|
|
46
|
+
if (registered.has(key))
|
|
47
|
+
return;
|
|
48
|
+
registered.add(key);
|
|
49
|
+
FontLibrary.use(family, [localPath]);
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=font-loader.js.map
|