@vosjs/render-core 0.1.0
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 +55 -0
- package/dist/index.d.ts +394 -0
- package/dist/index.js +938 -0
- package/dist/index.js.map +1 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 vosso
|
|
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,55 @@
|
|
|
1
|
+
# @vosso/render-core
|
|
2
|
+
|
|
3
|
+
The orchestration math and Node-side media plumbing for deterministic vos
|
|
4
|
+
renders. Pixels never leave the browser — every frame is encoded in-page via
|
|
5
|
+
WebCodecs + [mediabunny](https://mediabunny.dev). This package owns everything
|
|
6
|
+
around that: how a timeline is sharded into parallel chunks, and how the encoded
|
|
7
|
+
chunks are stitched back into one file without re-encoding.
|
|
8
|
+
|
|
9
|
+
Pure Node, no browser and no WebGL. Consumed in-source by
|
|
10
|
+
[`@vosso/vos-plugin`](../vos-plugin) (bundled via tsup `noExternal`) and by the
|
|
11
|
+
vosso render worker's finalize path. MIT.
|
|
12
|
+
|
|
13
|
+
## Why sharding is correct here
|
|
14
|
+
|
|
15
|
+
A vos render is a pure `seek(t)` function, so a timeline splits cleanly into
|
|
16
|
+
independent frame ranges. Each range is rendered concurrently as its own page,
|
|
17
|
+
encoded with **pinned** encoder params and chunk-local timestamps, then the
|
|
18
|
+
packets are stream-copied into a single container — a plain demux/remux, no
|
|
19
|
+
re-encode, no quality loss. Timestamp offsets come from the plan (frames ÷ fps),
|
|
20
|
+
never from measured durations, so rounding can't drift.
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
planChunks(total, fps, policy) ──▶ [ {startFrame, endFrame}, … ]
|
|
24
|
+
│ render each range concurrently (browser, pinned encoder)
|
|
25
|
+
▼
|
|
26
|
+
concatEncodedVideo([chunk₀, chunk₁, …]) ──▶ one file, packet-identical to a single-pass render
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Audio stays out of the chunks by design and is mixed **once** at finalize, so AAC
|
|
30
|
+
priming seams never exist.
|
|
31
|
+
|
|
32
|
+
## Exports
|
|
33
|
+
|
|
34
|
+
| Export | Purpose |
|
|
35
|
+
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
36
|
+
| `planChunks(totalFrames, fps, policy)` | Split a timeline into balanced frame ranges (floor `DEFAULT_MIN_FRAMES_PER_CHUNK` per chunk). Pure math. |
|
|
37
|
+
| `concatEncodedVideo(chunks, options)` | Stream-copy encoded chunks into one video (mediabunny demux/mux, no re-encode). |
|
|
38
|
+
| `countVideoPackets(bytes)` | Count video packets in an encoded file — used by parity checks. |
|
|
39
|
+
| `buildFinalizeConcatPage(options)` | Build the in-browser finalize page: fetch chunk parts, concat, mux produced audio, upload the result. Runs in a browser page (its memory, not the 128 MB isolate). |
|
|
40
|
+
| `audioProducerCode()` / `dataHasAudio(data)` | Page-JS mirror of the client audio exporter — produces the mixed audio buffer at finalize from the lowered Voila data. |
|
|
41
|
+
|
|
42
|
+
## Contract: the concat mirror
|
|
43
|
+
|
|
44
|
+
`concat.ts` and the concat inside `buildFinalizeConcatPage` must stay
|
|
45
|
+
**packet-identical** — the finalize page runs in a browser, the standalone concat
|
|
46
|
+
runs in Node, and cloud exports fan-in through the page while the CLI uses the Node
|
|
47
|
+
path. `vos-plugin`'s `verify-finalize-page` asserts they produce identical output;
|
|
48
|
+
keep the two in sync when touching either.
|
|
49
|
+
|
|
50
|
+
## Development
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pnpm --filter @vosso/render-core test # vitest
|
|
54
|
+
pnpm --filter @vosso/render-core typecheck
|
|
55
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { VideoCodec } from 'mediabunny';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Chunk planner — timeline sharding for deterministic renders.
|
|
5
|
+
*
|
|
6
|
+
* Because vos evaluation is a pure function of time (seek(t) — the whole
|
|
7
|
+
* point of the sampler tween backend), any frame range can render in
|
|
8
|
+
* isolation. The planner splits [0, totalFrames) into balanced, contiguous
|
|
9
|
+
* ranges; each chunk encodes independently (so it starts on a keyframe by
|
|
10
|
+
* construction) with chunk-LOCAL timestamps starting at 0, and the finalize
|
|
11
|
+
* step (concat.ts) offsets packets back onto the global timeline.
|
|
12
|
+
*
|
|
13
|
+
* Pure math — no I/O — so the policy stays unit-testable and reusable by
|
|
14
|
+
* every harness (CLI, local server, render service).
|
|
15
|
+
*/
|
|
16
|
+
interface ChunkPlanPolicy {
|
|
17
|
+
/** Upper bound on simultaneous chunk renders (browser pages/sessions). */
|
|
18
|
+
maxParallel: number;
|
|
19
|
+
/**
|
|
20
|
+
* Below this many frames per chunk, extra parallelism costs more in
|
|
21
|
+
* per-chunk fixed overhead (page load, module import, first seek) than it
|
|
22
|
+
* saves — stop splitting. (Remotion's floor is 5; ours is higher because
|
|
23
|
+
* chunk startup includes CDN module imports.)
|
|
24
|
+
*/
|
|
25
|
+
minFramesPerChunk?: number;
|
|
26
|
+
}
|
|
27
|
+
declare const DEFAULT_MIN_FRAMES_PER_CHUNK = 24;
|
|
28
|
+
interface RenderChunk {
|
|
29
|
+
/** 0-based chunk index; also the concat order. */
|
|
30
|
+
index: number;
|
|
31
|
+
/** First frame of the chunk (inclusive, global frame numbering). */
|
|
32
|
+
startFrame: number;
|
|
33
|
+
/** End frame (exclusive). */
|
|
34
|
+
endFrame: number;
|
|
35
|
+
frameCount: number;
|
|
36
|
+
/** Global start time in seconds (startFrame / fps). */
|
|
37
|
+
startTime: number;
|
|
38
|
+
/** Exact chunk duration in seconds (frameCount / fps). */
|
|
39
|
+
duration: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Split `totalFrames` into at most `maxParallel` balanced contiguous chunks.
|
|
43
|
+
* Sizes differ by at most one frame; chunk boundaries are exact frame
|
|
44
|
+
* indices so no frame is rendered twice or skipped.
|
|
45
|
+
*/
|
|
46
|
+
declare function planChunks(totalFrames: number, fps: number, policy: ChunkPlanPolicy): RenderChunk[];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Finalize concat — stream-copy chunk videos into one file, no re-encode.
|
|
50
|
+
*
|
|
51
|
+
* Each chunk is an independently encoded file whose timestamps start at 0
|
|
52
|
+
* (chunk-local). Concat demuxes every chunk's video packets and re-muxes
|
|
53
|
+
* them into one output, offsetting timestamps by the chunk's global start
|
|
54
|
+
* time. Because chunks were encoded with identical codec/params (the caller's
|
|
55
|
+
* responsibility — one encoder config per render) and every independently
|
|
56
|
+
* encoded file begins with a keyframe, packet-level concatenation is valid
|
|
57
|
+
* with zero quality loss and near-zero CPU: pure container work, runs in
|
|
58
|
+
* plain Node (mediabunny demux/mux is pure JS; no WebCodecs needed).
|
|
59
|
+
*
|
|
60
|
+
* Audio is deliberately absent here: per the rendering plan, audio renders
|
|
61
|
+
* ONCE centrally and gets muxed against the concatenated video, so encoder
|
|
62
|
+
* priming seams never exist.
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
interface ConcatChunk {
|
|
66
|
+
/** Encoded chunk file bytes (WebM or MP4, matching `format`). */
|
|
67
|
+
data: Uint8Array;
|
|
68
|
+
/**
|
|
69
|
+
* The chunk's exact intended duration in seconds (frameCount / fps, from
|
|
70
|
+
* the chunk plan). Used as the timestamp offset for the NEXT chunk —
|
|
71
|
+
* derived from the plan, not from packet rounding, so drift can't
|
|
72
|
+
* accumulate across chunks.
|
|
73
|
+
*/
|
|
74
|
+
duration: number;
|
|
75
|
+
}
|
|
76
|
+
interface ConcatOptions {
|
|
77
|
+
format: 'webm' | 'mp4';
|
|
78
|
+
/** Stamped into the output track metadata (players use it as a hint). */
|
|
79
|
+
frameRate?: number;
|
|
80
|
+
}
|
|
81
|
+
interface ConcatResult {
|
|
82
|
+
bytes: Uint8Array;
|
|
83
|
+
/** Total video packets written. */
|
|
84
|
+
packetCount: number;
|
|
85
|
+
/** Codec copied through (from the first chunk). */
|
|
86
|
+
codec: VideoCodec;
|
|
87
|
+
}
|
|
88
|
+
interface MuxExportOptions extends ConcatOptions {
|
|
89
|
+
/**
|
|
90
|
+
* Video parts in plan order. An async iterable lets the caller feed parts
|
|
91
|
+
* ONE at a time from storage instead of materializing all of them first
|
|
92
|
+
* (the worker finalize's memory shape).
|
|
93
|
+
*/
|
|
94
|
+
video: AsyncIterable<ConcatChunk> | Iterable<ConcatChunk>;
|
|
95
|
+
/**
|
|
96
|
+
* Optional encoded audio file (any container mediabunny reads — the audio
|
|
97
|
+
* mix page produces Opus in WebM). Its packets are STREAM-COPIED into the
|
|
98
|
+
* output's audio track: no decode, no WebCodecs, pure container work, so
|
|
99
|
+
* this runs in a Worker isolate exactly like the video concat.
|
|
100
|
+
*/
|
|
101
|
+
audio?: Uint8Array;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Mux a chunked export's final artifact: stream-copy the video parts with
|
|
105
|
+
* plan-derived timestamp offsets, and stream-copy the pre-encoded audio
|
|
106
|
+
* track when one is provided. `packetCount` counts VIDEO packets only —
|
|
107
|
+
* that is the frame-parity contract callers assert against the plan.
|
|
108
|
+
*/
|
|
109
|
+
declare function muxEncodedExport(options: MuxExportOptions): Promise<ConcatResult>;
|
|
110
|
+
/**
|
|
111
|
+
* Concatenate independently encoded chunk videos into one stream-copied file.
|
|
112
|
+
* Chunks must share codec and encoder params; the first packet of every
|
|
113
|
+
* chunk must be a keyframe (violations throw — better a loud failure at
|
|
114
|
+
* finalize than a corrupt artifact). Thin wrapper over muxEncodedExport,
|
|
115
|
+
* kept as the CLI's finalize entry point.
|
|
116
|
+
*/
|
|
117
|
+
declare function concatEncodedVideo(chunks: ConcatChunk[], options: ConcatOptions): Promise<ConcatResult>;
|
|
118
|
+
/**
|
|
119
|
+
* Count video packets in an encoded file — cheap integrity check used by
|
|
120
|
+
* parity verification (chunked and single-flight renders of the same
|
|
121
|
+
* composition must contain the same number of frames).
|
|
122
|
+
*/
|
|
123
|
+
declare function countVideoPackets(data: Uint8Array): Promise<number>;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Finalize page — an HTML page that stream-copy concatenates encoded chunk
|
|
127
|
+
* videos IN A BROWSER and PUTs the result to an upload URL.
|
|
128
|
+
*
|
|
129
|
+
* Why a page and not the Worker: concat needs every chunk in memory plus the
|
|
130
|
+
* output buffer, which busts a 128MB isolate on real exports; a browser page
|
|
131
|
+
* (the same Browser Run substrate that rendered the chunks) has gigabytes,
|
|
132
|
+
* mediabunny already proven in it, and gives the later audio-mix step a home
|
|
133
|
+
* (OfflineAudioContext exists only in browsers).
|
|
134
|
+
*
|
|
135
|
+
* The in-page algorithm MIRRORS ./concat.ts (concatEncodedVideo) — same
|
|
136
|
+
* packet walk, same plan-derived timestamp offsets, same keyframe guard.
|
|
137
|
+
* KEEP THEM IN SYNC; scripts/verify-finalize-page.ts (vos-plugin) asserts the
|
|
138
|
+
* two implementations produce packet-identical output.
|
|
139
|
+
*
|
|
140
|
+
* Failure contract: any error (fetch, codec mismatch, upload) lands in
|
|
141
|
+
* `__renderComplete = { success: false, error }` — there is no base64
|
|
142
|
+
* fallback here (finals can be huge); the caller fails the job.
|
|
143
|
+
*/
|
|
144
|
+
interface FinalizePart {
|
|
145
|
+
/** Where the page fetches this chunk's encoded bytes (CORS-accessible). */
|
|
146
|
+
url: string;
|
|
147
|
+
/** The chunk's PLANNED duration in seconds (frameCount / fps) — the
|
|
148
|
+
* timestamp offset source, never demuxed durations (rounding drifts). */
|
|
149
|
+
duration: number;
|
|
150
|
+
}
|
|
151
|
+
interface FinalizeConcatPageOptions {
|
|
152
|
+
parts: FinalizePart[];
|
|
153
|
+
format: 'webm' | 'mp4';
|
|
154
|
+
/** Stamped into the output track metadata. */
|
|
155
|
+
frameRate: number;
|
|
156
|
+
/** The finished file is PUT here (Content-Type set from `format`). */
|
|
157
|
+
uploadUrl: string;
|
|
158
|
+
/**
|
|
159
|
+
* Audio-once-at-finalize (the rendering plan's audio model — chunks are
|
|
160
|
+
* video-only by construction, so encoder priming seams never exist):
|
|
161
|
+
* `producerCode` defines `window.__vosAudioProducer__({ data, duration,
|
|
162
|
+
* sampleRate }) => AudioBuffer | null` (the studio clips' plan rides the
|
|
163
|
+
* code itself, baked by `audioProducerCode({ plan })`, since this
|
|
164
|
+
* page passes none), evaluated before the concat; a
|
|
165
|
+
* returned buffer is muxed as the output's audio track (AAC for mp4 with
|
|
166
|
+
* an Opus fallback, Opus for webm). Null/absent = video-only.
|
|
167
|
+
*/
|
|
168
|
+
audio?: {
|
|
169
|
+
producerCode: string;
|
|
170
|
+
data: unknown;
|
|
171
|
+
/** Total OUTPUT duration in seconds (the mix length). */
|
|
172
|
+
duration: number;
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* Pre-encoded audio: the `audio` ingest part produced by the audio
|
|
176
|
+
* mix page. Its packets are STREAM-COPIED into the output — no decode, no
|
|
177
|
+
* OfflineAudioContext, no producer — so a fallback finalize on this page
|
|
178
|
+
* carries none of the audio-production memory bill. Mutually exclusive
|
|
179
|
+
* with `audio`.
|
|
180
|
+
*/
|
|
181
|
+
audioPart?: {
|
|
182
|
+
url: string;
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
declare function buildFinalizeConcatPage(options: FinalizeConcatPageOptions): string;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The take audio producer — page JavaScript implementing the export audio mix
|
|
189
|
+
* for the take data schema, as a source string for injection into render
|
|
190
|
+
* pages (the engine's `capture.audioProducerCode` seam for single-flight
|
|
191
|
+
* captures, the audio mix page, and the finalize page for chunked exports).
|
|
192
|
+
*
|
|
193
|
+
* MIRRORS the client exporter's audio path (the platform's
|
|
194
|
+
* decodeAudio → spliceAudio → mixExportAudio) — the same
|
|
195
|
+
* algorithm, so a cloud export sounds identical to a client export of the
|
|
196
|
+
* same composition. KEEP THEM IN SYNC when the client mixer changes.
|
|
197
|
+
*
|
|
198
|
+
* The producer is `window.__vosAudioProducer__({ data, plan, duration,
|
|
199
|
+
* sampleRate })`. Inputs read from `data` (all optional):
|
|
200
|
+
* videoSrc recording URL (requires hasAudio; the VOICE on legacy takes,
|
|
201
|
+
* the SYSTEM track on split takes)
|
|
202
|
+
* micSrc mic sidecar URL (the mic/system split) — the voice; when present the
|
|
203
|
+
* recording's own track mixes as system audio under sysGain
|
|
204
|
+
* hasAudio whether the recording carries audio
|
|
205
|
+
* segments [{ in, out, rate? }] SOURCE spans — mic is spliced/resampled
|
|
206
|
+
* so trims and speed changes stay lip-synced (pitch shifts with
|
|
207
|
+
* rate, matching preservesPitch=false preview playback)
|
|
208
|
+
* micGain voice master fader (default 1)
|
|
209
|
+
* sysGain system-audio master fader (default 1; split takes only)
|
|
210
|
+
*
|
|
211
|
+
* The music/SFX clips no longer ride `data`: they live on the
|
|
212
|
+
* studio stack entry (`vosso.studio`), and the host builds an ENGINE audio
|
|
213
|
+
* plan from them ahead of the page (studio-core's `studioAudioPlan`), the
|
|
214
|
+
* shape `@vosjs/core/audio`'s `mixAudio` renders. The page imports that
|
|
215
|
+
* mixer from the CDN and renders the plan to ONE buffer beside the
|
|
216
|
+
* recording's tracks. `plan` reaches the producer either as a call argument
|
|
217
|
+
* (a page's CONFIG) or baked into the code as `window.__vosAudioPlan__`
|
|
218
|
+
* (the engine's capture template calls the producer with `{ data, duration,
|
|
219
|
+
* sampleRate }` only, so single-flight captures bake it); no plan, no clips.
|
|
220
|
+
*/
|
|
221
|
+
/**
|
|
222
|
+
* The `@vosjs/core/audio` build a render page imports for `mixAudio`.
|
|
223
|
+
* Pinned to the version the api installs (`coreAudioCdn.test.ts` there
|
|
224
|
+
* holds it to the installed package): render-core has no engine dependency.
|
|
225
|
+
*/
|
|
226
|
+
declare const CORE_AUDIO_CDN_URL = "https://esm.sh/@vosjs/core@0.23.1/audio?target=es2022";
|
|
227
|
+
/**
|
|
228
|
+
* A sampled audio plan, structurally `@vosjs/core/audio`'s `AudioPlan` (and
|
|
229
|
+
* studio-core's `StudioAudioPlan`), typed here so render-core depends on
|
|
230
|
+
* neither.
|
|
231
|
+
*/
|
|
232
|
+
interface AudioPlanJson {
|
|
233
|
+
duration: number;
|
|
234
|
+
step: number;
|
|
235
|
+
tracks: {
|
|
236
|
+
id: string;
|
|
237
|
+
src: string;
|
|
238
|
+
loop: boolean;
|
|
239
|
+
points: {
|
|
240
|
+
t: number;
|
|
241
|
+
on: boolean;
|
|
242
|
+
pos: number;
|
|
243
|
+
gain: number;
|
|
244
|
+
}[];
|
|
245
|
+
}[];
|
|
246
|
+
}
|
|
247
|
+
interface AudioProducerCodeOptions {
|
|
248
|
+
/**
|
|
249
|
+
* Bake the plan into the code (`window.__vosAudioPlan__`) for callers
|
|
250
|
+
* that cannot pass one at call time (the engine's capture template).
|
|
251
|
+
*/
|
|
252
|
+
plan?: AudioPlanJson | null;
|
|
253
|
+
/** Override the mixer import (tests, a self-hosted engine). */
|
|
254
|
+
coreAudioUrl?: string;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* The studio entry's own data out of a stack in either shape: the STORED
|
|
258
|
+
* config's `stack` array (`[{ id, data, … }]`) or the lowered record keyed
|
|
259
|
+
* by entry id (`{ 'vosso.studio': data }`). Null when there is none.
|
|
260
|
+
*/
|
|
261
|
+
declare function studioEntryData(stack: unknown): Record<string, unknown> | null;
|
|
262
|
+
declare function audioProducerCode(options?: AudioProducerCodeOptions): string;
|
|
263
|
+
/**
|
|
264
|
+
* Does this composition carry anything the audio producer could mix? The
|
|
265
|
+
* voice reads off `data`; the clips off the studio stack entry (`stack` in
|
|
266
|
+
* either shape `studioEntryData` reads) or an already-built plan.
|
|
267
|
+
*/
|
|
268
|
+
declare function dataHasAudio(data: unknown, stack?: unknown, plan?: AudioPlanJson | null): boolean;
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Audio mix page — an HTML page that renders a chunked export's audio track
|
|
272
|
+
* ALONE and PUTs it to the ingest route as the `audio` part.
|
|
273
|
+
*
|
|
274
|
+
* Why a page: the mix needs OfflineAudioContext + WebCodecs AudioEncoder,
|
|
275
|
+
* which exist only in browsers. Why ALONE: the finalize page used to produce
|
|
276
|
+
* audio AND concat video in one context, and the combined memory bill
|
|
277
|
+
* (whole source recording + full-length PCM + all parts + output buffer)
|
|
278
|
+
* is what the fleet kills at peak (job 288257ae). This page's
|
|
279
|
+
* live set stays ~tens of MB by construction:
|
|
280
|
+
*
|
|
281
|
+
* - the source recording is never fetched whole: `__vosStreamSplice__`
|
|
282
|
+
* (installed here, consumed by the shared audio producer) streams-decodes
|
|
283
|
+
* ONLY the needed source spans through mediabunny's UrlSource, whose
|
|
284
|
+
* read cache is bounded (~8MiB) — the asset route serves Range/206.
|
|
285
|
+
* - the spliced voice buffer is capped at the REQUESTED output duration,
|
|
286
|
+
* which is also the duration fix (a duration-capped render must not carry the
|
|
287
|
+
* full take's audio).
|
|
288
|
+
* - the encoded result is ~1MB of Opus in WebM.
|
|
289
|
+
*
|
|
290
|
+
* The whole-file decodeAudioData path stays as the in-page fallback rung
|
|
291
|
+
* (unsupported codec, no Range support): the producer falls back on any
|
|
292
|
+
* stream-splice failure, so a mix is never LOST to the optimization.
|
|
293
|
+
*
|
|
294
|
+
* Runs CONCURRENTLY with the chunk phase — it depends only on the source
|
|
295
|
+
* recording and the doc, never on a part.
|
|
296
|
+
*
|
|
297
|
+
* Failure contract: mirrors the finalize page — any error lands in
|
|
298
|
+
* `__renderComplete = { success: false, error }`; stage markers ride
|
|
299
|
+
* `__finalizeStage` (heap-stamped) so a death localizes itself.
|
|
300
|
+
*/
|
|
301
|
+
|
|
302
|
+
interface AudioMixPageOptions {
|
|
303
|
+
/** The vos's resolved ctx.data (asset URLs absolute, render token baked). */
|
|
304
|
+
data: unknown;
|
|
305
|
+
/**
|
|
306
|
+
* The studio clips as an engine audio plan, built by the host from
|
|
307
|
+
* the stored config's `vosso.studio` stack entry; null when the take has
|
|
308
|
+
* no music/SFX. Rides the page's CONFIG, once.
|
|
309
|
+
*/
|
|
310
|
+
plan?: AudioPlanJson | null;
|
|
311
|
+
/** Requested OUTPUT duration in seconds — the mix length, the trim bound. */
|
|
312
|
+
duration: number;
|
|
313
|
+
/** The finished audio file is PUT here (ingest `?part=audio`). */
|
|
314
|
+
uploadUrl: string;
|
|
315
|
+
}
|
|
316
|
+
declare function buildAudioMixPage(options: AudioMixPageOptions): string;
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Image diff page — computes the RMS difference between two images in a
|
|
320
|
+
* browser page. Workers can't decode images (no canvas/Image APIs), so the
|
|
321
|
+
* golden-frame canary hands both images to a page and reads one number back.
|
|
322
|
+
*
|
|
323
|
+
* Same completion contract as every render page: `__renderComplete` with
|
|
324
|
+
* `{ success, rms, width, height }` or `{ success: false, error }`.
|
|
325
|
+
* RMS is over RGB (alpha ignored) in 0–255 units: WebP re-encode jitter of
|
|
326
|
+
* the same frame lands well under 5; a real engine/fleet drift (missing
|
|
327
|
+
* layer, changed shader, font fallback) lands far above 10.
|
|
328
|
+
*/
|
|
329
|
+
interface ImageDiffPageOptions {
|
|
330
|
+
/** data: URLs — the canary inlines both images (they are ~tens of KB). */
|
|
331
|
+
candidateUrl: string;
|
|
332
|
+
goldenUrl: string;
|
|
333
|
+
}
|
|
334
|
+
declare function buildImageDiffPage(options: ImageDiffPageOptions): string;
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Digest page — the fleet's eyes for a take. One bare page decodes the
|
|
338
|
+
* recording through a <video> and does what the CLI's digest page does
|
|
339
|
+
* locally: per-source-second motion bins (64×36 luma diff, changed-pixel
|
|
340
|
+
* fraction), the scene instants those bins reveal, one FOOTAGE frame plus a
|
|
341
|
+
* crop per moment, and a contact sheet. Every image PUTs itself to the ingest
|
|
342
|
+
* route as a `digest-*` part (the page's bytes never marshal through the
|
|
343
|
+
* browser session), then a manifest with the bins and every image's size.
|
|
344
|
+
*
|
|
345
|
+
* Same completion contract as every render page: `__renderComplete` with
|
|
346
|
+
* `{ success, uploaded: true }` or `{ success: false, error }`.
|
|
347
|
+
*
|
|
348
|
+
* The bins must be byte-comparable with the CLI's (`MOTION_DELTA` 24, one
|
|
349
|
+
* seek per source second at i + 0.5, 64×36): they feed the speed planner.
|
|
350
|
+
*/
|
|
351
|
+
interface DigestShot {
|
|
352
|
+
/** Part stem; the page writes `<name>.full.png` and, with a box, `<name>.crop.png`. */
|
|
353
|
+
name: string;
|
|
354
|
+
/** Source instant, seconds. */
|
|
355
|
+
t: number;
|
|
356
|
+
/** Crop box in FRAME px, or null for a full-only moment. */
|
|
357
|
+
box: {
|
|
358
|
+
x: number;
|
|
359
|
+
y: number;
|
|
360
|
+
w: number;
|
|
361
|
+
h: number;
|
|
362
|
+
} | null;
|
|
363
|
+
/** A short label for the contact sheet. */
|
|
364
|
+
label: string;
|
|
365
|
+
}
|
|
366
|
+
interface DigestPageOptions {
|
|
367
|
+
/** The recording, reachable from the page (a `?rt=` asset URL). */
|
|
368
|
+
videoUrl: string;
|
|
369
|
+
/** Source duration, seconds. */
|
|
370
|
+
durationS: number;
|
|
371
|
+
/** The region of the frame the doc renders (a window take's crop, or all). */
|
|
372
|
+
region: {
|
|
373
|
+
x: number;
|
|
374
|
+
y: number;
|
|
375
|
+
w: number;
|
|
376
|
+
h: number;
|
|
377
|
+
};
|
|
378
|
+
shots: DigestShot[];
|
|
379
|
+
/** Long edges (px) of the emitted images. */
|
|
380
|
+
fullMax: number;
|
|
381
|
+
cropMax: number;
|
|
382
|
+
/** Changed-pixel luma threshold for the bins. */
|
|
383
|
+
motionDelta: number;
|
|
384
|
+
/** Scene rule: a bin at/above `motion` after one at/below `quiet`. */
|
|
385
|
+
scene: {
|
|
386
|
+
motion: number;
|
|
387
|
+
quiet: number;
|
|
388
|
+
};
|
|
389
|
+
/** `…/api/render/ingest/{jobId}?token=…` — parts append `&part=digest-<name>`. */
|
|
390
|
+
uploadUrl: string;
|
|
391
|
+
}
|
|
392
|
+
declare function buildDigestPage(options: DigestPageOptions): string;
|
|
393
|
+
|
|
394
|
+
export { type AudioMixPageOptions, type AudioPlanJson, type AudioProducerCodeOptions, CORE_AUDIO_CDN_URL, type ChunkPlanPolicy, type ConcatChunk, type ConcatOptions, type ConcatResult, DEFAULT_MIN_FRAMES_PER_CHUNK, type DigestPageOptions, type DigestShot, type FinalizeConcatPageOptions, type FinalizePart, type ImageDiffPageOptions, type MuxExportOptions, type RenderChunk, audioProducerCode, buildAudioMixPage, buildDigestPage, buildFinalizeConcatPage, buildImageDiffPage, concatEncodedVideo, countVideoPackets, dataHasAudio, muxEncodedExport, planChunks, studioEntryData };
|