@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 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
+ ```
@@ -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 };