@motionscript/browser 0.1.0-alpha.8 → 0.1.0-alpha.9
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/CHANGELOG.md +6 -0
- package/LICENSE +201 -201
- package/NOTICE +49 -49
- package/README.md +141 -141
- package/dist/asset-bytes.js.map +1 -1
- package/dist/audio/bus-graph.js.map +1 -1
- package/dist/audio/effect-graph.js.map +1 -1
- package/dist/audio/index.js.map +1 -1
- package/dist/audio/loop-region.js.map +1 -1
- package/dist/audio/mixer.js.map +1 -1
- package/dist/audio/player.js.map +1 -1
- package/dist/browser/audio.js +1 -1
- package/dist/browser/chunks/{chunk-IILVN4SK.js → chunk-F3AZKPHU.js} +1 -1
- package/dist/browser/chunks/chunk-F3AZKPHU.js.map +7 -0
- package/dist/browser/index.js +2 -2
- package/dist/browser/index.js.map +3 -3
- package/dist/browser/ms_renderer_bg.wasm +0 -0
- package/dist/concat.js.map +1 -1
- package/dist/dynamic-nodes.js.map +1 -1
- package/dist/engine.js.map +1 -1
- package/dist/export-audio.js.map +1 -1
- package/dist/exporter.js.map +1 -1
- package/dist/frame-loop.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mix-timeline-audio.js.map +1 -1
- package/dist/parallel/export-parallel.js.map +1 -1
- package/dist/parallel/lane.js.map +1 -1
- package/dist/parallel/run-lanes.js.map +1 -1
- package/dist/parallel/session.js.map +1 -1
- package/dist/renderer.js.map +1 -1
- package/dist/screenshot.js.map +1 -1
- package/dist/still.js.map +1 -1
- package/dist/ticker.js.map +1 -1
- package/package.json +4 -4
- package/dist/audio/filter-graph.d.ts +0 -59
- package/dist/audio/filter-graph.d.ts.map +0 -1
- package/dist/audio/filter-graph.js +0 -241
- package/dist/audio/filter-graph.js.map +0 -1
- package/dist/browser/chunks/chunk-IILVN4SK.js.map +0 -7
- package/dist/canvaskit-renderer.d.ts +0 -8
- package/dist/canvaskit-renderer.d.ts.map +0 -1
- package/dist/canvaskit-renderer.js +0 -23
- package/dist/canvaskit-renderer.js.map +0 -1
- package/dist/decode/image-levels.d.ts +0 -137
- package/dist/decode/image-levels.d.ts.map +0 -1
- package/dist/decode/image-levels.js +0 -477
- package/dist/decode/image-levels.js.map +0 -1
- package/dist/decode/levels.d.ts +0 -37
- package/dist/decode/levels.d.ts.map +0 -1
- package/dist/decode/levels.js +0 -68
- package/dist/decode/levels.js.map +0 -1
- package/dist/decode/video-levels.d.ts +0 -37
- package/dist/decode/video-levels.d.ts.map +0 -1
- package/dist/decode/video-levels.js +0 -95
- package/dist/decode/video-levels.js.map +0 -1
- package/dist/getter.d.ts +0 -13
- package/dist/getter.d.ts.map +0 -1
- package/dist/getter.js +0 -39
- package/dist/getter.js.map +0 -1
- package/dist/measure-context.d.ts +0 -12
- package/dist/measure-context.d.ts.map +0 -1
- package/dist/measure-context.js +0 -12
- package/dist/measure-context.js.map +0 -1
- package/dist/render-context.d.ts +0 -34
- package/dist/render-context.d.ts.map +0 -1
- package/dist/render-context.js +0 -63
- package/dist/render-context.js.map +0 -1
- package/dist/storage-adapter.d.ts +0 -367
- package/dist/storage-adapter.d.ts.map +0 -1
- package/dist/storage-adapter.js +0 -1280
- package/dist/storage-adapter.js.map +0 -1
- package/dist/three/renderer.d.ts +0 -47
- package/dist/three/renderer.d.ts.map +0 -1
- package/dist/three/renderer.js +0 -182
- package/dist/three/renderer.js.map +0 -1
|
Binary file
|
package/dist/concat.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"concat.js","sourceRoot":"","sources":["../src/concat.ts"],"names":[],"mappings":"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","sourcesContent":["import {\r\n BufferSource,\r\n BufferTarget,\r\n EncodedPacketSink,\r\n EncodedVideoPacketSource,\r\n Input,\r\n MP4,\r\n Output,\r\n StreamTarget,\r\n WEBM,\r\n type InputVideoTrack,\r\n type VideoCodec,\r\n} from \"mediabunny\";\r\n\r\nimport { audioTrackSource, formatSpec, type VideoFormat } from \"./formats\";\r\n\r\n/**\r\n * Join encoded MP4 or WebM segments end to end, without re-encoding.\r\n *\r\n * The point of this is what it *doesn't* do: no decode, no encode, no pixels.\r\n * Each segment is demuxed to its encoded packets, the packets are re-stamped onto\r\n * one continuous timeline, and a single muxer writes them into one container. The\r\n * bytes of every frame survive untouched, so the result is bit-identical to the\r\n * segments and the whole thing runs at I/O speed rather than encode speed.\r\n *\r\n * It exists so an export can be **split across workers**. Rendering a timeline is\r\n * embarrassingly parallel per scene — each scene's generator is independent — but\r\n * the output has to be one file, and re-encoding the parts to join them would give\r\n * back everything the parallelism won (and lose a generation of quality). Joining\r\n * at the packet level is what makes splitting worth doing.\r\n *\r\n * ## What the caller has to guarantee\r\n *\r\n * **Every segment must be encoded with an identical configuration** — same\r\n * container, codec, resolution and bitrate settings. A container carries exactly one decoder\r\n * configuration for a track, so a segment whose parameter sets differ from the\r\n * first would be described by a configuration that isn't its own, and decode as\r\n * garbage from that point on. That is not a theoretical concern: hardware encoders\r\n * are free to pick different parameter sets for the same nominal settings. This is\r\n * checked rather than assumed — see {@link ConcatMismatchError} — because the\r\n * failure is silent corruption rather than an exception.\r\n *\r\n * **Every segment must begin with a key frame.** Otherwise its first packets refer\r\n * back to frames that lived in the previous segment's GOP and no longer precede\r\n * them. Encoders start with a key frame by default; a caller choosing an explicit\r\n * `keyFrameInterval` needs it to divide the segment length.\r\n *\r\n * Audio is *muxed* here but never *joined*: the segments' own audio is ignored\r\n * entirely, and a single already-mixed buffer is encoded once across the whole\r\n * result (see {@link ConcatOptions.audio}). Concatenating separately-encoded AAC\r\n * would put the encoder's priming delay at every join — a click at each boundary\r\n * and a track that drifts further out of sync with every one — so the timeline's\r\n * audio is mixed in one pass by whoever owns the timeline, and arrives here as\r\n * one buffer.\r\n */\r\n\r\n/** Thrown when segments can't be joined losslessly. Carries which segment disagreed. */\r\nexport class ConcatMismatchError extends Error {\r\n constructor(\r\n /** Index of the offending segment in the input array. */\r\n readonly segment: number,\r\n message: string,\r\n ) {\r\n super(message);\r\n this.name = \"ConcatMismatchError\";\r\n }\r\n}\r\n\r\n/** One segment's demuxed video track, plus the config that describes it. */\r\ninterface Segment {\r\n format: VideoFormat;\r\n track: InputVideoTrack;\r\n config: VideoDecoderConfig;\r\n duration: number;\r\n}\r\n\r\n/**\r\n * Two decoder configurations describe the same bitstream.\r\n *\r\n * `description` holds the codec's parameter sets (the `avcC` box for H.264) and is\r\n * the field that actually decides decodability — a differing one is exactly the\r\n * silent-corruption case. Compared bytewise; the rest are compared by value.\r\n */\r\nfunction sameDecoderConfig(a: VideoDecoderConfig, b: VideoDecoderConfig): boolean {\r\n if (a.codec !== b.codec) return false;\r\n if (a.codedWidth !== b.codedWidth) return false;\r\n if (a.codedHeight !== b.codedHeight) return false;\r\n\r\n const da = toBytes(a.description);\r\n const db = toBytes(b.description);\r\n if (da === null || db === null) return da === db;\r\n if (da.length !== db.length) return false;\r\n for (let i = 0; i < da.length; i++) {\r\n if (da[i] !== db[i]) return false;\r\n }\r\n return true;\r\n}\r\n\r\nfunction toBytes(description: VideoDecoderConfig[\"description\"]): Uint8Array | null {\r\n if (!description) return null;\r\n // `description` is a BufferSource: an ArrayBuffer, a SharedArrayBuffer, or any\r\n // view over one. Views carry an offset into a larger buffer, so they must be\r\n // narrowed rather than wrapped whole.\r\n if (ArrayBuffer.isView(description)) {\r\n return new Uint8Array(description.buffer, description.byteOffset, description.byteLength);\r\n }\r\n return new Uint8Array(description);\r\n}\r\n\r\nexport interface ConcatOptions {\r\n /**\r\n * One continuous audio track for the whole joined timeline.\r\n *\r\n * Mixed once by the caller (see `mixTimelineAudio`) rather than per segment,\r\n * and that is not a convenience: separately-encoded AAC carries an encoder\r\n * priming delay at the head of each piece, so joining them puts a click at\r\n * every scene boundary and drifts the track further out of sync with each\r\n * one. One buffer in, one encode, no seams.\r\n */\r\n audio?: AudioBuffer | null;\r\n /** Bitrate for that track: AAC in an MP4, Opus in a WebM. Defaults to 192 kbps. */\r\n audioBitrate?: number;\r\n /**\r\n * Write the result out as it is muxed instead of returning it.\r\n *\r\n * With this the joined file never exists in memory as one buffer, which is\r\n * the difference between an export the process can hold and one it can't:\r\n * a 4× 60fps render is hundreds of megabytes, and the default path costs\r\n * roughly twice that at the tail once the `Blob` copy is counted.\r\n *\r\n * **Chunks carry a byte `position` and do not arrive in file order** — a\r\n * muxer seeks back to patch atom sizes. A sink that appends will produce a\r\n * file of exactly the right length whose size fields belong to a different\r\n * one: unplayable, and indistinguishable from success until it is opened.\r\n * Honour `position`.\r\n *\r\n * `concatVideoSegments` resolves to an empty array when this is used; the\r\n * bytes went to the sink.\r\n */\r\n stream?: WritableStream<{ type: \"write\"; data: Uint8Array; position: number }>;\r\n}\r\n\r\n/** Joins segments one at a time, in timeline order, as each becomes available. */\r\nexport interface SegmentJoiner {\r\n /**\r\n * Mux the next segment after the ones already appended. Appends must not\r\n * overlap: await each before starting the next.\r\n *\r\n * @throws {ConcatMismatchError} as {@link concatVideoSegments} does.\r\n */\r\n append(segment: Uint8Array): Promise<void>;\r\n /** Finish the container. The bytes, or an empty array when they went to `stream`. */\r\n finish(): Promise<Uint8Array>;\r\n /** Abandon the join; nothing more is written. */\r\n cancel(): Promise<void>;\r\n}\r\n\r\n/**\r\n * {@link concatVideoSegments} one segment at a time, so a split export can write\r\n * each piece the moment it and every piece before it exist, instead of holding\r\n * every segment until the last one lands.\r\n *\r\n * The audio, when given, is encoded as soon as the first segment fixes the\r\n * container, ahead of the video: the muxer holds the (small) audio packets and\r\n * interleaves them as the video catches up.\r\n */\r\nexport function createSegmentJoiner(options: ConcatOptions = {}): SegmentJoiner {\r\n let first: Segment | null = null;\r\n let output: Output | null = null;\r\n let source: EncodedVideoPacketSource | null = null;\r\n let audioSource: ReturnType<typeof audioTrackSource> | null = null;\r\n let target: StreamTarget | BufferTarget | null = null;\r\n let index = 0;\r\n let offset = 0;\r\n\r\n const start = async (segment: Segment) => {\r\n target = options.stream\r\n ? new StreamTarget(options.stream as ConstructorParameters<typeof StreamTarget>[0])\r\n : new BufferTarget();\r\n output = new Output({ format: formatSpec(segment.format).container(), target });\r\n // `codec` comes off the track rather than the decoder config: the latter's\r\n // `codec` is the full codec *string* (`avc1.64001f`), while the source wants\r\n // the family.\r\n source = new EncodedVideoPacketSource(segment.track.codec as VideoCodec);\r\n output.addVideoTrack(source);\r\n // Declared before `start()`: a muxer fixes its track list up front.\r\n audioSource = options.audio ? audioTrackSource(segment.format, options.audioBitrate) : null;\r\n if (audioSource) output.addAudioTrack(audioSource);\r\n await output.start();\r\n if (audioSource && options.audio) await audioSource.add(options.audio);\r\n };\r\n\r\n return {\r\n async append(bytes) {\r\n const i = index++;\r\n const input = new Input({ source: new BufferSource(bytes), formats: [MP4, WEBM] });\r\n try {\r\n const segment = await openSegment(input, i, first);\r\n if (!first) {\r\n first = segment;\r\n await start(segment);\r\n }\r\n // The decoder config is handed over with each segment's first packet.\r\n // Only the first can take effect (a container holds one per track), but\r\n // every one has been checked equal to it.\r\n let leading = true;\r\n for await (const packet of new EncodedPacketSink(segment.track).packets()) {\r\n await source!.add(\r\n packet.clone({ timestamp: packet.timestamp + offset }),\r\n leading ? { decoderConfig: segment.config } : undefined,\r\n );\r\n leading = false;\r\n }\r\n // The segment's own duration rather than its last packet's end: summing\r\n // packet durations accumulates rounding into a drift across many segments.\r\n offset += segment.duration;\r\n } finally {\r\n input.dispose();\r\n }\r\n },\r\n async finish() {\r\n if (!output) throw new Error(\"concatVideoSegments: no segments\");\r\n source!.close();\r\n audioSource?.close();\r\n await output.finalize();\r\n if (options.stream) return new Uint8Array(0);\r\n const buffer = (target as BufferTarget).buffer;\r\n if (!buffer) throw new Error(\"concatVideoSegments produced no data\");\r\n return new Uint8Array(buffer);\r\n },\r\n async cancel() {\r\n if (output && output.state !== \"finalized\" && output.state !== \"canceled\") await output.cancel();\r\n },\r\n };\r\n}\r\n\r\n/** Demux one segment and check it can follow `first`. */\r\nasync function openSegment(input: Input, i: number, first: Segment | null): Promise<Segment> {\r\n const format: VideoFormat = (await input.getFormat()) === WEBM ? \"webm\" : \"mp4\";\r\n if (first && format !== first.format) {\r\n throw new ConcatMismatchError(i, `Segment ${i} is ${format} and segment 0 is ${first.format}.`);\r\n }\r\n const track = await input.getPrimaryVideoTrack();\r\n if (!track) throw new ConcatMismatchError(i, `Segment ${i} has no video track.`);\r\n const config = await track.getDecoderConfig();\r\n if (!config) throw new ConcatMismatchError(i, `Segment ${i} has no decoder configuration.`);\r\n if (first && !sameDecoderConfig(first.config, config)) {\r\n throw new ConcatMismatchError(\r\n i,\r\n `Segment ${i} was encoded differently from segment 0 (${first.config.codec} `\r\n + `${first.config.codedWidth}x${first.config.codedHeight} vs ${config.codec} `\r\n + `${config.codedWidth}x${config.codedHeight}); joining them would decode as garbage.`,\r\n );\r\n }\r\n return { format, track, config, duration: await track.computeDuration() };\r\n}\r\n\r\n/**\r\n * Concatenate `segments` (each a complete MP4, or each a complete WebM) into one\r\n * file of the same container.\r\n *\r\n * Segments are joined in array order. Returns the muxed bytes.\r\n *\r\n * @throws {ConcatMismatchError} if a segment carries no video track, is in another\r\n * container than the first, or has a decoder configuration that differs\r\n * from the first segment's. Callers that can\r\n * fall back to a single-threaded export should catch this and do so — the\r\n * alternative is shipping a file that decodes into garbage halfway through.\r\n */\r\nexport async function concatVideoSegments(\r\n segments: Uint8Array[],\r\n options: ConcatOptions = {},\r\n): Promise<Uint8Array> {\r\n if (segments.length === 0) throw new Error(\"concatVideoSegments: no segments\");\r\n // A lone segment is already the finished file — unless a track has to be added\r\n // to it, or it has to go to a sink, either of which means muxing after all.\r\n if (segments.length === 1 && !options.audio && !options.stream) return segments[0];\r\n\r\n const joiner = createSegmentJoiner(options);\r\n try {\r\n for (const segment of segments) await joiner.append(segment);\r\n return await joiner.finish();\r\n } catch (error) {\r\n await joiner.cancel().catch(() => {});\r\n throw error;\r\n }\r\n}\r\n"]}
|
|
1
|
+
{"version":3,"file":"concat.js","sourceRoot":"","sources":["../src/concat.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n BufferSource,\n BufferTarget,\n EncodedPacketSink,\n EncodedVideoPacketSource,\n Input,\n MP4,\n Output,\n StreamTarget,\n WEBM,\n type InputVideoTrack,\n type VideoCodec,\n} from \"mediabunny\";\n\nimport { audioTrackSource, formatSpec, type VideoFormat } from \"./formats\";\n\n/**\n * Join encoded MP4 or WebM segments end to end, without re-encoding.\n *\n * The point of this is what it *doesn't* do: no decode, no encode, no pixels.\n * Each segment is demuxed to its encoded packets, the packets are re-stamped onto\n * one continuous timeline, and a single muxer writes them into one container. The\n * bytes of every frame survive untouched, so the result is bit-identical to the\n * segments and the whole thing runs at I/O speed rather than encode speed.\n *\n * It exists so an export can be **split across workers**. Rendering a timeline is\n * embarrassingly parallel per scene — each scene's generator is independent — but\n * the output has to be one file, and re-encoding the parts to join them would give\n * back everything the parallelism won (and lose a generation of quality). Joining\n * at the packet level is what makes splitting worth doing.\n *\n * ## What the caller has to guarantee\n *\n * **Every segment must be encoded with an identical configuration** — same\n * container, codec, resolution and bitrate settings. A container carries exactly one decoder\n * configuration for a track, so a segment whose parameter sets differ from the\n * first would be described by a configuration that isn't its own, and decode as\n * garbage from that point on. That is not a theoretical concern: hardware encoders\n * are free to pick different parameter sets for the same nominal settings. This is\n * checked rather than assumed — see {@link ConcatMismatchError} — because the\n * failure is silent corruption rather than an exception.\n *\n * **Every segment must begin with a key frame.** Otherwise its first packets refer\n * back to frames that lived in the previous segment's GOP and no longer precede\n * them. Encoders start with a key frame by default; a caller choosing an explicit\n * `keyFrameInterval` needs it to divide the segment length.\n *\n * Audio is *muxed* here but never *joined*: the segments' own audio is ignored\n * entirely, and a single already-mixed buffer is encoded once across the whole\n * result (see {@link ConcatOptions.audio}). Concatenating separately-encoded AAC\n * would put the encoder's priming delay at every join — a click at each boundary\n * and a track that drifts further out of sync with every one — so the timeline's\n * audio is mixed in one pass by whoever owns the timeline, and arrives here as\n * one buffer.\n */\n\n/** Thrown when segments can't be joined losslessly. Carries which segment disagreed. */\nexport class ConcatMismatchError extends Error {\n constructor(\n /** Index of the offending segment in the input array. */\n readonly segment: number,\n message: string,\n ) {\n super(message);\n this.name = \"ConcatMismatchError\";\n }\n}\n\n/** One segment's demuxed video track, plus the config that describes it. */\ninterface Segment {\n format: VideoFormat;\n track: InputVideoTrack;\n config: VideoDecoderConfig;\n duration: number;\n}\n\n/**\n * Two decoder configurations describe the same bitstream.\n *\n * `description` holds the codec's parameter sets (the `avcC` box for H.264) and is\n * the field that actually decides decodability — a differing one is exactly the\n * silent-corruption case. Compared bytewise; the rest are compared by value.\n */\nfunction sameDecoderConfig(a: VideoDecoderConfig, b: VideoDecoderConfig): boolean {\n if (a.codec !== b.codec) return false;\n if (a.codedWidth !== b.codedWidth) return false;\n if (a.codedHeight !== b.codedHeight) return false;\n\n const da = toBytes(a.description);\n const db = toBytes(b.description);\n if (da === null || db === null) return da === db;\n if (da.length !== db.length) return false;\n for (let i = 0; i < da.length; i++) {\n if (da[i] !== db[i]) return false;\n }\n return true;\n}\n\nfunction toBytes(description: VideoDecoderConfig[\"description\"]): Uint8Array | null {\n if (!description) return null;\n // `description` is a BufferSource: an ArrayBuffer, a SharedArrayBuffer, or any\n // view over one. Views carry an offset into a larger buffer, so they must be\n // narrowed rather than wrapped whole.\n if (ArrayBuffer.isView(description)) {\n return new Uint8Array(description.buffer, description.byteOffset, description.byteLength);\n }\n return new Uint8Array(description);\n}\n\nexport interface ConcatOptions {\n /**\n * One continuous audio track for the whole joined timeline.\n *\n * Mixed once by the caller (see `mixTimelineAudio`) rather than per segment,\n * and that is not a convenience: separately-encoded AAC carries an encoder\n * priming delay at the head of each piece, so joining them puts a click at\n * every scene boundary and drifts the track further out of sync with each\n * one. One buffer in, one encode, no seams.\n */\n audio?: AudioBuffer | null;\n /** Bitrate for that track: AAC in an MP4, Opus in a WebM. Defaults to 192 kbps. */\n audioBitrate?: number;\n /**\n * Write the result out as it is muxed instead of returning it.\n *\n * With this the joined file never exists in memory as one buffer, which is\n * the difference between an export the process can hold and one it can't:\n * a 4× 60fps render is hundreds of megabytes, and the default path costs\n * roughly twice that at the tail once the `Blob` copy is counted.\n *\n * **Chunks carry a byte `position` and do not arrive in file order** — a\n * muxer seeks back to patch atom sizes. A sink that appends will produce a\n * file of exactly the right length whose size fields belong to a different\n * one: unplayable, and indistinguishable from success until it is opened.\n * Honour `position`.\n *\n * `concatVideoSegments` resolves to an empty array when this is used; the\n * bytes went to the sink.\n */\n stream?: WritableStream<{ type: \"write\"; data: Uint8Array; position: number }>;\n}\n\n/** Joins segments one at a time, in timeline order, as each becomes available. */\nexport interface SegmentJoiner {\n /**\n * Mux the next segment after the ones already appended. Appends must not\n * overlap: await each before starting the next.\n *\n * @throws {ConcatMismatchError} as {@link concatVideoSegments} does.\n */\n append(segment: Uint8Array): Promise<void>;\n /** Finish the container. The bytes, or an empty array when they went to `stream`. */\n finish(): Promise<Uint8Array>;\n /** Abandon the join; nothing more is written. */\n cancel(): Promise<void>;\n}\n\n/**\n * {@link concatVideoSegments} one segment at a time, so a split export can write\n * each piece the moment it and every piece before it exist, instead of holding\n * every segment until the last one lands.\n *\n * The audio, when given, is encoded as soon as the first segment fixes the\n * container, ahead of the video: the muxer holds the (small) audio packets and\n * interleaves them as the video catches up.\n */\nexport function createSegmentJoiner(options: ConcatOptions = {}): SegmentJoiner {\n let first: Segment | null = null;\n let output: Output | null = null;\n let source: EncodedVideoPacketSource | null = null;\n let audioSource: ReturnType<typeof audioTrackSource> | null = null;\n let target: StreamTarget | BufferTarget | null = null;\n let index = 0;\n let offset = 0;\n\n const start = async (segment: Segment) => {\n target = options.stream\n ? new StreamTarget(options.stream as ConstructorParameters<typeof StreamTarget>[0])\n : new BufferTarget();\n output = new Output({ format: formatSpec(segment.format).container(), target });\n // `codec` comes off the track rather than the decoder config: the latter's\n // `codec` is the full codec *string* (`avc1.64001f`), while the source wants\n // the family.\n source = new EncodedVideoPacketSource(segment.track.codec as VideoCodec);\n output.addVideoTrack(source);\n // Declared before `start()`: a muxer fixes its track list up front.\n audioSource = options.audio ? audioTrackSource(segment.format, options.audioBitrate) : null;\n if (audioSource) output.addAudioTrack(audioSource);\n await output.start();\n if (audioSource && options.audio) await audioSource.add(options.audio);\n };\n\n return {\n async append(bytes) {\n const i = index++;\n const input = new Input({ source: new BufferSource(bytes), formats: [MP4, WEBM] });\n try {\n const segment = await openSegment(input, i, first);\n if (!first) {\n first = segment;\n await start(segment);\n }\n // The decoder config is handed over with each segment's first packet.\n // Only the first can take effect (a container holds one per track), but\n // every one has been checked equal to it.\n let leading = true;\n for await (const packet of new EncodedPacketSink(segment.track).packets()) {\n await source!.add(\n packet.clone({ timestamp: packet.timestamp + offset }),\n leading ? { decoderConfig: segment.config } : undefined,\n );\n leading = false;\n }\n // The segment's own duration rather than its last packet's end: summing\n // packet durations accumulates rounding into a drift across many segments.\n offset += segment.duration;\n } finally {\n input.dispose();\n }\n },\n async finish() {\n if (!output) throw new Error(\"concatVideoSegments: no segments\");\n source!.close();\n audioSource?.close();\n await output.finalize();\n if (options.stream) return new Uint8Array(0);\n const buffer = (target as BufferTarget).buffer;\n if (!buffer) throw new Error(\"concatVideoSegments produced no data\");\n return new Uint8Array(buffer);\n },\n async cancel() {\n if (output && output.state !== \"finalized\" && output.state !== \"canceled\") await output.cancel();\n },\n };\n}\n\n/** Demux one segment and check it can follow `first`. */\nasync function openSegment(input: Input, i: number, first: Segment | null): Promise<Segment> {\n const format: VideoFormat = (await input.getFormat()) === WEBM ? \"webm\" : \"mp4\";\n if (first && format !== first.format) {\n throw new ConcatMismatchError(i, `Segment ${i} is ${format} and segment 0 is ${first.format}.`);\n }\n const track = await input.getPrimaryVideoTrack();\n if (!track) throw new ConcatMismatchError(i, `Segment ${i} has no video track.`);\n const config = await track.getDecoderConfig();\n if (!config) throw new ConcatMismatchError(i, `Segment ${i} has no decoder configuration.`);\n if (first && !sameDecoderConfig(first.config, config)) {\n throw new ConcatMismatchError(\n i,\n `Segment ${i} was encoded differently from segment 0 (${first.config.codec} `\n + `${first.config.codedWidth}x${first.config.codedHeight} vs ${config.codec} `\n + `${config.codedWidth}x${config.codedHeight}); joining them would decode as garbage.`,\n );\n }\n return { format, track, config, duration: await track.computeDuration() };\n}\n\n/**\n * Concatenate `segments` (each a complete MP4, or each a complete WebM) into one\n * file of the same container.\n *\n * Segments are joined in array order. Returns the muxed bytes.\n *\n * @throws {ConcatMismatchError} if a segment carries no video track, is in another\n * container than the first, or has a decoder configuration that differs\n * from the first segment's. Callers that can\n * fall back to a single-threaded export should catch this and do so — the\n * alternative is shipping a file that decodes into garbage halfway through.\n */\nexport async function concatVideoSegments(\n segments: Uint8Array[],\n options: ConcatOptions = {},\n): Promise<Uint8Array> {\n if (segments.length === 0) throw new Error(\"concatVideoSegments: no segments\");\n // A lone segment is already the finished file — unless a track has to be added\n // to it, or it has to go to a sink, either of which means muxing after all.\n if (segments.length === 1 && !options.audio && !options.stream) return segments[0];\n\n const joiner = createSegmentJoiner(options);\n try {\n for (const segment of segments) await joiner.append(segment);\n return await joiner.finish();\n } catch (error) {\n await joiner.cancel().catch(() => {});\n throw error;\n }\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-nodes.js","sourceRoot":"","sources":["../src/dynamic-nodes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,GAAG,MAAM,wBAAwB,CAAC;AAC9C,OAAO,KAAK,OAAO,MAAM,cAAc,CAAC;AAExC;;;;;;;;;;;;;GAaG;AAEH,mFAAmF;AACnF,gFAAgF;AAChF,+EAA+E;AAC/E,8EAA8E;AAC9E,SAAS,sBAAsB;IAC3B,OAAO,IAAI,GAAG,CAAC,2BAA2B,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;AACtE,CAAC;AAED,IAAI,eAAyE,CAAC;AAE9E,0CAA0C;AAC1C,SAAS,eAAe,CAAC,OAAgB;IACrC,OAAO,CAAC,eAAe,KAAK,CAAC,KAAK,IAAI,EAAE;QACpC,MAAM,OAAO,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,OAAO,IAAI,sBAAsB,EAAE,EAAE,CAAC,CAAC;QAC3E,OAAO,KAAK,EAAE,MAAc,EAAE,EAAE;YAC5B,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,OAAO,CAAC,SAAS,CAAC,MAAM,EAAE;gBAC7C,iEAAiE;gBACjE,MAAM,EAAE,IAAI;gBACZ,MAAM,EAAE,KAAK;gBACb,MAAM,EAAE,QAAQ;aACnB,CAAC,CAAC;YACH,OAAO,IAAI,CAAC;QAChB,CAAC,CAAC;IACN,CAAC,CAAC,EAAE,CAAC,CAAC;AACV,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAC/B,OAA+C,EAC/C,eAAwB;IAExB,OAAO;QACH,GAAG,EAAE,OAAO,EAAE,GAAG,IAAI,GAAG;QACxB,OAAO,EAAE,OAAO,EAAE,OAAO;QACzB,OAAO,EAAE,OAAO,EAAE,OAAO,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,eAAe,CAAC,eAAe,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;KACpG,CAAC;AACN,CAAC","sourcesContent":["import type { DynamicNodeConfig } from \"@motionscript/core\";\
|
|
1
|
+
{"version":3,"file":"dynamic-nodes.js","sourceRoot":"","sources":["../src/dynamic-nodes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,GAAG,MAAM,wBAAwB,CAAC;AAC9C,OAAO,KAAK,OAAO,MAAM,cAAc,CAAC;AAExC;;;;;;;;;;;;;GAaG;AAEH,mFAAmF;AACnF,gFAAgF;AAChF,+EAA+E;AAC/E,8EAA8E;AAC9E,SAAS,sBAAsB;IAC3B,OAAO,IAAI,GAAG,CAAC,2BAA2B,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;AACtE,CAAC;AAED,IAAI,eAAyE,CAAC;AAE9E,0CAA0C;AAC1C,SAAS,eAAe,CAAC,OAAgB;IACrC,OAAO,CAAC,eAAe,KAAK,CAAC,KAAK,IAAI,EAAE;QACpC,MAAM,OAAO,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,OAAO,IAAI,sBAAsB,EAAE,EAAE,CAAC,CAAC;QAC3E,OAAO,KAAK,EAAE,MAAc,EAAE,EAAE;YAC5B,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,OAAO,CAAC,SAAS,CAAC,MAAM,EAAE;gBAC7C,iEAAiE;gBACjE,MAAM,EAAE,IAAI;gBACZ,MAAM,EAAE,KAAK;gBACb,MAAM,EAAE,QAAQ;aACnB,CAAC,CAAC;YACH,OAAO,IAAI,CAAC;QAChB,CAAC,CAAC;IACN,CAAC,CAAC,EAAE,CAAC,CAAC;AACV,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAC/B,OAA+C,EAC/C,eAAwB;IAExB,OAAO;QACH,GAAG,EAAE,OAAO,EAAE,GAAG,IAAI,GAAG;QACxB,OAAO,EAAE,OAAO,EAAE,OAAO;QACzB,OAAO,EAAE,OAAO,EAAE,OAAO,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,eAAe,CAAC,eAAe,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;KACpG,CAAC;AACN,CAAC","sourcesContent":["import type { DynamicNodeConfig } from \"@motionscript/core\";\nimport * as sdk from \"@motionscript/core/sdk\";\nimport * as esbuild from \"esbuild-wasm\";\n\n/**\n * `browser`'s real, working default for `Registry.addNodeFromSource` — the\n * `sdk` half is cheap (re-exports, no WASM) and imported eagerly; the compiler\n * is the expensive half, and stays behind a lazy `import()` so an app that\n * never registers a source-string node never pays for it, however long the\n * engine has been running.\n *\n * `core` has no compiler dependency of its own and cannot import the published\n * `@motionscript/sdk` barrel (that package depends on `core`, so the reverse\n * would be circular) — `browser` is where this concern actually belongs: the\n * package whose entire job is \"what running in an actual browser tab looks\n * like\" already owns the renderer, `WebAudioSink` and `WebTicker` on exactly that\n * basis.\n */\n\n// Default to the wasm shipped by esbuild-wasm, resolved lazily — `import.meta.url`\n// is empty in a non-ESM output (an IIFE bundle, which is what the headless page\n// loads), and `new URL(\"…\", \"\")` throws on import, before a caller has had the\n// chance to pass an explicit URL that would have made the default irrelevant.\nfunction defaultCompilerWasmUrl(): string {\n return new URL(\"esbuild-wasm/esbuild.wasm\", import.meta.url).href;\n}\n\nlet compilerPromise: Promise<(source: string) => Promise<string>> | undefined;\n\n/** Loaded once, memoized process-wide. */\nfunction defaultCompiler(wasmUrl?: string): Promise<(source: string) => Promise<string>> {\n return (compilerPromise ??= (async () => {\n await esbuild.initialize({ wasmURL: wasmUrl ?? defaultCompilerWasmUrl() });\n return async (source: string) => {\n const { code } = await esbuild.transform(source, {\n // A component is written with calls: there is no JSX to compile.\n loader: \"ts\",\n format: \"cjs\",\n target: \"es2020\",\n });\n return code;\n };\n })());\n}\n\n/**\n * Resolves a full `DynamicNodeConfig`, filling in whatever the caller left\n * unset with `browser`'s own default. Synchronous — the WASM import happens\n * on `compile`'s first actual call, not here, so attaching this to every\n * engine costs nothing until a source string is genuinely registered.\n */\nexport function resolveDynamicNodes(\n partial: Partial<DynamicNodeConfig> | undefined,\n compilerWasmUrl?: string,\n): DynamicNodeConfig {\n return {\n sdk: partial?.sdk ?? sdk,\n modules: partial?.modules,\n compile: partial?.compile ?? (async (source) => (await defaultCompiler(compilerWasmUrl))(source)),\n };\n}\n"]}
|
package/dist/engine.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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","sourcesContent":["import {\r\n AudioSink,\r\n Engine,\r\n EngineOptions,\r\n EnginePlatform,\r\n EngineRenderContext,\r\n Exporter,\r\n ExportStillRequest,\r\n ExportVideoRequest,\r\n ExportedStill,\r\n Registry,\r\n Ticker,\r\n MeasureContext2D,\r\n PlatformDeps,\r\n StorageAdapter,\r\n type DynamicNodeConfig,\r\n type NodeConstructor,\r\n type NodeTypeMap,\r\n} from \"@motionscript/core\";\r\n\r\nimport type { WgpuStorageAdapter } from \"@motionscript/renderer\";\r\nimport { loadRenderer, type RendererWasmOption } from \"./renderer\";\r\nimport { resolveDynamicNodes } from \"./dynamic-nodes\";\r\nimport { WebAudioSink } from \"./audio/player\";\r\nimport { WebTicker } from \"./ticker\";\r\nimport { exportVideo as exportVideoFile, type AudioEncoderOptions, type VideoEncoderOptions } from \"./exporter\";\r\nimport { exportAudio } from \"./export-audio\";\r\nimport { isAudioFormat, type ExportFormat } from \"./formats\";\r\nimport { StillRenderer } from \"./still\";\r\n\r\nexport interface BrowserEngineOptions extends Omit<EngineOptions, \"nodes\" | \"dynamicNodes\">, RendererWasmOption {\r\n /**\r\n * Node types any document opened here may name — components as an array\r\n * or a record naming each key outright, same as `EngineOptions`, plus a\r\n * TS source string in the **array** form only. A string default-exports a\r\n * component, whose `key` is its key — the record form takes no string, so\r\n * there is exactly one place a dynamic node's key comes from, not two that\r\n * could disagree.\r\n */\r\n nodes?: readonly (NodeConstructor | string)[] | NodeTypeMap;\r\n /**\r\n * Enables a source string in `nodes`, and `engine.registerNode(source)`\r\n * at any point afterward. Any field left unset falls back to `browser`'s\r\n * own default — `@motionscript/core/sdk` for `sdk`, a lazily-loaded\r\n * `esbuild-wasm` for `compile` — so this never needs to be passed at all\r\n * for the common case. `modules` has no default: only the host knows\r\n * which loaded packages a script it compiles may reach.\r\n */\r\n dynamicNodes?: Partial<DynamicNodeConfig>;\r\n /**\r\n * Where `esbuild-wasm`'s own `.wasm` binary is served — only read if the\r\n * default compiler is actually used.\r\n */\r\n compilerWasmUrl?: string;\r\n /** Default video encoder settings for `session.export()`. Overridable per call. */\r\n video?: VideoEncoderOptions;\r\n /** Default audio encoder settings for `session.export()`. Overridable per call. */\r\n audio?: AudioEncoderOptions;\r\n}\r\n\r\n/** What a session's frames are drawn with, and the storage and measurement it reads from. */\r\nexport interface BrowserRenderer<TStorage extends StorageAdapter = StorageAdapter> {\r\n createStorageAdapter(deps: PlatformDeps): TStorage;\r\n createRenderContext(storage: TStorage): BrowserRenderContext;\r\n createMeasureContext(storage: TStorage): MeasureContext2D;\r\n dispose?(): void;\r\n}\r\n\r\n/** A render context as this package drives it: an engine one, plus what exports and stills ask of it. */\r\nexport type BrowserRenderContext = EngineRenderContext & {\r\n /**\r\n * Settles once the surface `mount` began can draw. A backend whose surface\r\n * comes up asynchronously (a GPU device) must have it, or the first frame\r\n * draws onto nothing.\r\n */\r\n whenMounted?(): Promise<void>;\r\n /**\r\n * The frame last drawn, copied into a fresh 2D canvas, or `undefined` before\r\n * one was. What a still encodes; a WebGPU canvas is readable only in the task\r\n * that drew it, so a backend may draw it again here.\r\n */\r\n snapshotCanvas?(): HTMLCanvasElement | undefined;\r\n};\r\n\r\n/** What `session.export({ encode })` accepts in this environment. */\r\nexport interface BrowserEncodeOptions {\r\n /**\r\n * Defaults to `'mp4'`. An audio format (`'wav'`, `'mp3'`, `'m4a'`) renders no\r\n * frames and ignores `scale`, `frames`, `includeAudio`, `video` and `supersample`.\r\n */\r\n format?: ExportFormat;\r\n video?: VideoEncoderOptions;\r\n audio?: AudioEncoderOptions;\r\n supersample?: number;\r\n /** Set false to get the bytes back instead of triggering a browser download. */\r\n download?: boolean;\r\n filename?: string;\r\n}\r\n\r\n/**\r\n * The browser's {@link Exporter}: WebCodecs + mediabunny for video, the\r\n * browser's own image codecs for a still.\r\n *\r\n * **It does not reuse a session's storage adapter, and that is deliberate.** An\r\n * adapter's viewport clamps the resolution images decode at — `StorageAdapter`\r\n * documents that it has no setter for exactly this reason — so handing an export\r\n * the preview's adapter would paint a 2× render from decodes sized for 1×, and\r\n * the result would be soft in a way nothing reports. What is shared is the\r\n * engine's renderer backend, the same one its sessions draw with.\r\n */\r\nclass WebExporter implements Exporter {\r\n private still: StillRenderer | null = null;\r\n private stillRegistry: Registry | null = null;\r\n /** Settles once every still asked for so far has been encoded. */\r\n private stillQueue: Promise<unknown> = Promise.resolve();\r\n\r\n constructor(\r\n private readonly backend: BrowserRenderer,\r\n private readonly defaults: { video?: VideoEncoderOptions; audio?: AudioEncoderOptions } & RendererWasmOption,\r\n ) { }\r\n\r\n async exportVideo(request: ExportVideoRequest): Promise<Uint8Array | void> {\r\n const encode = (request.encode ?? {}) as BrowserEncodeOptions;\r\n if (encode.format && isAudioFormat(encode.format)) {\r\n return exportAudio({\r\n document: request.project,\r\n registry: request.registry,\r\n manifest: request.manifest,\r\n format: encode.format,\r\n audio: encode.audio ?? this.defaults.audio,\r\n filename: encode.filename,\r\n returnBytes: encode.download !== true,\r\n onProgress: request.onProgress,\r\n signal: request.signal,\r\n rendererWasmUrl: this.defaults.rendererWasmUrl,\r\n });\r\n }\r\n return exportVideoFile({\r\n document: request.project,\r\n registry: request.registry,\r\n scale: request.scale,\r\n manifest: request.manifest,\r\n includeAudio: request.includeAudio,\r\n frames: request.frames,\r\n format: encode.format,\r\n video: encode.video ?? this.defaults.video,\r\n audio: encode.audio ?? this.defaults.audio,\r\n supersample: encode.supersample,\r\n filename: encode.filename,\r\n // A `Session` asks for bytes; only an explicit `download: true` takes\r\n // the DOM anchor path this function offers a page.\r\n returnBytes: encode.download !== true,\r\n onProgress: request.onProgress,\r\n onIssue: request.onIssue,\r\n signal: request.signal,\r\n rendererWasmUrl: this.defaults.rendererWasmUrl,\r\n });\r\n }\r\n\r\n /**\r\n * Queued, rather than a renderer per call: the one renderer is what keeps a\r\n * GPU surface and the decode cache warm between stills, and its draw and the\r\n * encode of it are two steps another call's draw would land between.\r\n */\r\n exportStill(request: ExportStillRequest): Promise<ExportedStill> {\r\n const still = this.stillQueue.then(() => this.drawStill(request));\r\n this.stillQueue = still.catch(() => undefined);\r\n return still;\r\n }\r\n\r\n dispose(): void {\r\n this.still?.dispose();\r\n this.still = null;\r\n }\r\n\r\n private async drawStill(request: ExportStillRequest): Promise<ExportedStill> {\r\n const renderer = this.renderer(request);\r\n const drawn = await renderer.render(request.project, {\r\n scale: request.scale,\r\n onIssue: request.onIssue,\r\n ...frameOption(request.frame),\r\n });\r\n const bytes = await renderer.toBytes({\r\n format: request.format,\r\n quality: request.quality,\r\n });\r\n return { bytes, frame: drawn.frame, totalFrames: drawn.totalFrames };\r\n }\r\n\r\n /**\r\n * One `StillRenderer`, reused across calls and re-viewported when the ask\r\n * changes — `setViewport` is a no-op when the size already matches, and a\r\n * full reset when it does not, which is the same trade the class documents.\r\n */\r\n private renderer(request: ExportStillRequest): StillRenderer {\r\n const { project, registry } = request;\r\n if (project.viewport === undefined || project.fps === undefined) {\r\n throw new Error(\"exportStill takes a project whose fps and viewport are resolved — see Session.\");\r\n }\r\n // A renderer compiles against the registry it was built with.\r\n if (this.still && this.stillRegistry !== registry) this.dispose();\r\n this.still ??= new StillRenderer(this.backend, {\r\n viewport: project.viewport,\r\n fps: project.fps,\r\n manifest: request.manifest,\r\n registry,\r\n });\r\n this.stillRegistry = registry;\r\n this.still.setViewport(project.viewport);\r\n if (request.manifest) this.still.setManifest(request.manifest);\r\n return this.still;\r\n }\r\n}\r\n\r\n/** `ExportStillRequest.frame` in the shape `StillRenderer.render` accepts. */\r\nfunction frameOption(frame: ExportStillRequest[\"frame\"]): { frame?: number | \"first\" | \"last\" } {\r\n if (frame.kind === \"frame\") return { frame: frame.frame };\r\n return { frame: frame.kind };\r\n}\r\n\r\n/**\r\n * An engine backed by the browser: WebGPU surfaces, Web Audio, WebCodecs.\r\n * Rejects with `WebGpuUnavailableError` where this browser has no WebGPU adapter.\r\n *\r\n * Async because the renderer is a wasm module and every seam below needs it. Await\r\n * once, at app start, and share the result — the caches underneath (registered\r\n * fonts, decoded images, shaped paragraphs) are the point of keeping one engine\r\n * rather than one per canvas.\r\n *\r\n * ```ts\r\n * const engine = await createBrowserEngine({ nodes: { pulseRing: PulseRing } });\r\n * const session = engine.open({ document, target: canvasEl });\r\n * session.playback!.play();\r\n * ```\r\n */\r\nexport async function createBrowserEngine(\r\n options: BrowserEngineOptions = {},\r\n): Promise<Engine<WgpuStorageAdapter>> {\r\n const rawNodes = options.nodes;\r\n // `Array.isArray` doesn't narrow a `readonly T[] | U` union cleanly against\r\n // its `arg is any[]` (mutable) predicate — the `.filter` predicate below\r\n // already guarantees no string survives, so the cast is just recovering\r\n // what the runtime check already proved.\r\n const classNodes = (\r\n Array.isArray(rawNodes)\r\n ? rawNodes.filter((n): n is NodeConstructor => typeof n !== \"string\")\r\n : rawNodes\r\n ) as NodeTypeMap | readonly NodeConstructor[] | undefined;\r\n const sourceNodes = Array.isArray(rawNodes)\r\n ? rawNodes.filter((n): n is string => typeof n === \"string\")\r\n : [];\r\n // Cheap — see dynamic-nodes.ts: no WASM import happens here, only on\r\n // `compile`'s first actual call. Attached unconditionally so a later\r\n // `engine.registerNode(source)` works even when `nodes` carried no\r\n // string at boot.\r\n const dynamicNodes = resolveDynamicNodes(options.dynamicNodes, options.compilerWasmUrl);\r\n\r\n const renderer: BrowserRenderer<WgpuStorageAdapter> = await loadRenderer(options);\r\n const exporter = new WebExporter(renderer, { video: options.video, audio: options.audio, rendererWasmUrl: options.rendererWasmUrl });\r\n\r\n const platform: EnginePlatform<WgpuStorageAdapter> = {\r\n createStorageAdapter: (deps) => renderer.createStorageAdapter(deps),\r\n createRenderContext: (storage) => renderer.createRenderContext(storage),\r\n createMeasureContext: (storage) => renderer.createMeasureContext(storage),\r\n createAudioSink(): AudioSink {\r\n return new WebAudioSink();\r\n },\r\n createTicker(fps: number, audioDevice: AudioSink): Ticker {\r\n // Shares the device's own `AudioContext`: two independent timebases\r\n // in one session drift against each other, and sound is the one that\r\n // cannot be nudged back. A getter, so neither creates it before it\r\n // is needed (the first play, or the first sound to decode).\r\n const sink = audioDevice as WebAudioSink;\r\n return new WebTicker({ context: () => sink.getContext(), fps });\r\n },\r\n createExporter(): Exporter {\r\n return exporter;\r\n },\r\n dispose(): void {\r\n exporter.dispose();\r\n renderer.dispose?.();\r\n },\r\n };\r\n\r\n const engine = new Engine<WgpuStorageAdapter>(platform, { ...options, nodes: classNodes, dynamicNodes });\r\n for (const source of sourceNodes) {\r\n // Sequential — one script's error shouldn't mask another's, and\r\n // registration order should match array order for determinism.\r\n await engine.registerNode(source);\r\n }\r\n return engine;\r\n}\r\n"]}
|
|
1
|
+
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n AudioSink,\n Engine,\n EngineOptions,\n EnginePlatform,\n EngineRenderContext,\n Exporter,\n ExportStillRequest,\n ExportVideoRequest,\n ExportedStill,\n Registry,\n Ticker,\n MeasureContext2D,\n PlatformDeps,\n StorageAdapter,\n type DynamicNodeConfig,\n type NodeConstructor,\n type NodeTypeMap,\n} from \"@motionscript/core\";\n\nimport type { WgpuStorageAdapter } from \"@motionscript/renderer\";\nimport { loadRenderer, type RendererWasmOption } from \"./renderer\";\nimport { resolveDynamicNodes } from \"./dynamic-nodes\";\nimport { WebAudioSink } from \"./audio/player\";\nimport { WebTicker } from \"./ticker\";\nimport { exportVideo as exportVideoFile, type AudioEncoderOptions, type VideoEncoderOptions } from \"./exporter\";\nimport { exportAudio } from \"./export-audio\";\nimport { isAudioFormat, type ExportFormat } from \"./formats\";\nimport { StillRenderer } from \"./still\";\n\nexport interface BrowserEngineOptions extends Omit<EngineOptions, \"nodes\" | \"dynamicNodes\">, RendererWasmOption {\n /**\n * Node types any document opened here may name — components as an array\n * or a record naming each key outright, same as `EngineOptions`, plus a\n * TS source string in the **array** form only. A string default-exports a\n * component, whose `key` is its key — the record form takes no string, so\n * there is exactly one place a dynamic node's key comes from, not two that\n * could disagree.\n */\n nodes?: readonly (NodeConstructor | string)[] | NodeTypeMap;\n /**\n * Enables a source string in `nodes`, and `engine.registerNode(source)`\n * at any point afterward. Any field left unset falls back to `browser`'s\n * own default — `@motionscript/core/sdk` for `sdk`, a lazily-loaded\n * `esbuild-wasm` for `compile` — so this never needs to be passed at all\n * for the common case. `modules` has no default: only the host knows\n * which loaded packages a script it compiles may reach.\n */\n dynamicNodes?: Partial<DynamicNodeConfig>;\n /**\n * Where `esbuild-wasm`'s own `.wasm` binary is served — only read if the\n * default compiler is actually used.\n */\n compilerWasmUrl?: string;\n /** Default video encoder settings for `session.export()`. Overridable per call. */\n video?: VideoEncoderOptions;\n /** Default audio encoder settings for `session.export()`. Overridable per call. */\n audio?: AudioEncoderOptions;\n}\n\n/** What a session's frames are drawn with, and the storage and measurement it reads from. */\nexport interface BrowserRenderer<TStorage extends StorageAdapter = StorageAdapter> {\n createStorageAdapter(deps: PlatformDeps): TStorage;\n createRenderContext(storage: TStorage): BrowserRenderContext;\n createMeasureContext(storage: TStorage): MeasureContext2D;\n dispose?(): void;\n}\n\n/** A render context as this package drives it: an engine one, plus what exports and stills ask of it. */\nexport type BrowserRenderContext = EngineRenderContext & {\n /**\n * Settles once the surface `mount` began can draw. A backend whose surface\n * comes up asynchronously (a GPU device) must have it, or the first frame\n * draws onto nothing.\n */\n whenMounted?(): Promise<void>;\n /**\n * The frame last drawn, copied into a fresh 2D canvas, or `undefined` before\n * one was. What a still encodes; a WebGPU canvas is readable only in the task\n * that drew it, so a backend may draw it again here.\n */\n snapshotCanvas?(): HTMLCanvasElement | undefined;\n};\n\n/** What `session.export({ encode })` accepts in this environment. */\nexport interface BrowserEncodeOptions {\n /**\n * Defaults to `'mp4'`. An audio format (`'wav'`, `'mp3'`, `'m4a'`) renders no\n * frames and ignores `scale`, `frames`, `includeAudio`, `video` and `supersample`.\n */\n format?: ExportFormat;\n video?: VideoEncoderOptions;\n audio?: AudioEncoderOptions;\n supersample?: number;\n /** Set false to get the bytes back instead of triggering a browser download. */\n download?: boolean;\n filename?: string;\n}\n\n/**\n * The browser's {@link Exporter}: WebCodecs + mediabunny for video, the\n * browser's own image codecs for a still.\n *\n * **It does not reuse a session's storage adapter, and that is deliberate.** An\n * adapter's viewport clamps the resolution images decode at — `StorageAdapter`\n * documents that it has no setter for exactly this reason — so handing an export\n * the preview's adapter would paint a 2× render from decodes sized for 1×, and\n * the result would be soft in a way nothing reports. What is shared is the\n * engine's renderer backend, the same one its sessions draw with.\n */\nclass WebExporter implements Exporter {\n private still: StillRenderer | null = null;\n private stillRegistry: Registry | null = null;\n /** Settles once every still asked for so far has been encoded. */\n private stillQueue: Promise<unknown> = Promise.resolve();\n\n constructor(\n private readonly backend: BrowserRenderer,\n private readonly defaults: { video?: VideoEncoderOptions; audio?: AudioEncoderOptions } & RendererWasmOption,\n ) { }\n\n async exportVideo(request: ExportVideoRequest): Promise<Uint8Array | void> {\n const encode = (request.encode ?? {}) as BrowserEncodeOptions;\n if (encode.format && isAudioFormat(encode.format)) {\n return exportAudio({\n document: request.project,\n registry: request.registry,\n manifest: request.manifest,\n format: encode.format,\n audio: encode.audio ?? this.defaults.audio,\n filename: encode.filename,\n returnBytes: encode.download !== true,\n onProgress: request.onProgress,\n signal: request.signal,\n rendererWasmUrl: this.defaults.rendererWasmUrl,\n });\n }\n return exportVideoFile({\n document: request.project,\n registry: request.registry,\n scale: request.scale,\n manifest: request.manifest,\n includeAudio: request.includeAudio,\n frames: request.frames,\n format: encode.format,\n video: encode.video ?? this.defaults.video,\n audio: encode.audio ?? this.defaults.audio,\n supersample: encode.supersample,\n filename: encode.filename,\n // A `Session` asks for bytes; only an explicit `download: true` takes\n // the DOM anchor path this function offers a page.\n returnBytes: encode.download !== true,\n onProgress: request.onProgress,\n onIssue: request.onIssue,\n signal: request.signal,\n rendererWasmUrl: this.defaults.rendererWasmUrl,\n });\n }\n\n /**\n * Queued, rather than a renderer per call: the one renderer is what keeps a\n * GPU surface and the decode cache warm between stills, and its draw and the\n * encode of it are two steps another call's draw would land between.\n */\n exportStill(request: ExportStillRequest): Promise<ExportedStill> {\n const still = this.stillQueue.then(() => this.drawStill(request));\n this.stillQueue = still.catch(() => undefined);\n return still;\n }\n\n dispose(): void {\n this.still?.dispose();\n this.still = null;\n }\n\n private async drawStill(request: ExportStillRequest): Promise<ExportedStill> {\n const renderer = this.renderer(request);\n const drawn = await renderer.render(request.project, {\n scale: request.scale,\n onIssue: request.onIssue,\n ...frameOption(request.frame),\n });\n const bytes = await renderer.toBytes({\n format: request.format,\n quality: request.quality,\n });\n return { bytes, frame: drawn.frame, totalFrames: drawn.totalFrames };\n }\n\n /**\n * One `StillRenderer`, reused across calls and re-viewported when the ask\n * changes — `setViewport` is a no-op when the size already matches, and a\n * full reset when it does not, which is the same trade the class documents.\n */\n private renderer(request: ExportStillRequest): StillRenderer {\n const { project, registry } = request;\n if (project.viewport === undefined || project.fps === undefined) {\n throw new Error(\"exportStill takes a project whose fps and viewport are resolved — see Session.\");\n }\n // A renderer compiles against the registry it was built with.\n if (this.still && this.stillRegistry !== registry) this.dispose();\n this.still ??= new StillRenderer(this.backend, {\n viewport: project.viewport,\n fps: project.fps,\n manifest: request.manifest,\n registry,\n });\n this.stillRegistry = registry;\n this.still.setViewport(project.viewport);\n if (request.manifest) this.still.setManifest(request.manifest);\n return this.still;\n }\n}\n\n/** `ExportStillRequest.frame` in the shape `StillRenderer.render` accepts. */\nfunction frameOption(frame: ExportStillRequest[\"frame\"]): { frame?: number | \"first\" | \"last\" } {\n if (frame.kind === \"frame\") return { frame: frame.frame };\n return { frame: frame.kind };\n}\n\n/**\n * An engine backed by the browser: WebGPU surfaces, Web Audio, WebCodecs.\n * Rejects with `WebGpuUnavailableError` where this browser has no WebGPU adapter.\n *\n * Async because the renderer is a wasm module and every seam below needs it. Await\n * once, at app start, and share the result — the caches underneath (registered\n * fonts, decoded images, shaped paragraphs) are the point of keeping one engine\n * rather than one per canvas.\n *\n * ```ts\n * const engine = await createBrowserEngine({ nodes: { pulseRing: PulseRing } });\n * const session = engine.open({ document, target: canvasEl });\n * session.playback!.play();\n * ```\n */\nexport async function createBrowserEngine(\n options: BrowserEngineOptions = {},\n): Promise<Engine<WgpuStorageAdapter>> {\n const rawNodes = options.nodes;\n // `Array.isArray` doesn't narrow a `readonly T[] | U` union cleanly against\n // its `arg is any[]` (mutable) predicate — the `.filter` predicate below\n // already guarantees no string survives, so the cast is just recovering\n // what the runtime check already proved.\n const classNodes = (\n Array.isArray(rawNodes)\n ? rawNodes.filter((n): n is NodeConstructor => typeof n !== \"string\")\n : rawNodes\n ) as NodeTypeMap | readonly NodeConstructor[] | undefined;\n const sourceNodes = Array.isArray(rawNodes)\n ? rawNodes.filter((n): n is string => typeof n === \"string\")\n : [];\n // Cheap — see dynamic-nodes.ts: no WASM import happens here, only on\n // `compile`'s first actual call. Attached unconditionally so a later\n // `engine.registerNode(source)` works even when `nodes` carried no\n // string at boot.\n const dynamicNodes = resolveDynamicNodes(options.dynamicNodes, options.compilerWasmUrl);\n\n const renderer: BrowserRenderer<WgpuStorageAdapter> = await loadRenderer(options);\n const exporter = new WebExporter(renderer, { video: options.video, audio: options.audio, rendererWasmUrl: options.rendererWasmUrl });\n\n const platform: EnginePlatform<WgpuStorageAdapter> = {\n createStorageAdapter: (deps) => renderer.createStorageAdapter(deps),\n createRenderContext: (storage) => renderer.createRenderContext(storage),\n createMeasureContext: (storage) => renderer.createMeasureContext(storage),\n createAudioSink(): AudioSink {\n return new WebAudioSink();\n },\n createTicker(fps: number, audioDevice: AudioSink): Ticker {\n // Shares the device's own `AudioContext`: two independent timebases\n // in one session drift against each other, and sound is the one that\n // cannot be nudged back. A getter, so neither creates it before it\n // is needed (the first play, or the first sound to decode).\n const sink = audioDevice as WebAudioSink;\n return new WebTicker({ context: () => sink.getContext(), fps });\n },\n createExporter(): Exporter {\n return exporter;\n },\n dispose(): void {\n exporter.dispose();\n renderer.dispose?.();\n },\n };\n\n const engine = new Engine<WgpuStorageAdapter>(platform, { ...options, nodes: classNodes, dynamicNodes });\n for (const source of sourceNodes) {\n // Sequential — one script's error shouldn't mask another's, and\n // registration order should match array order for determinism.\n await engine.registerNode(source);\n }\n return engine;\n}\n"]}
|
package/dist/export-audio.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"export-audio.js","sourceRoot":"","sources":["../src/export-audio.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAElD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,UAAU,EAAoB,MAAM,WAAW,CAAC;AAC/F,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAsBxD,qFAAqF;AACrF,MAAM,OAAO,YAAa,SAAQ,KAAK;IACjB,IAAI,GAAG,cAAc,CAAC;IAExC;QACI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAClD,CAAC;CACJ;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,MAAyB;IACvD,MAAM,EAAE,MAAM,GAAG,KAAK,EAAE,KAAK,GAAG,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,GAAG,KAAK,EAAE,GAAG,MAAM,CAAC;IACvF,MAAM,IAAI,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;IAChC,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO;QAAE,MAAM,IAAI,UAAU,CAAC,8BAA8B,MAAM,uCAAuC,CAAC,CAAC;IAC7H,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,yEAAyE;IACzE,MAAM,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAEjC,MAAM,KAAK,GAAG,MAAM,gBAAgB,CAAC;QACjC,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,GAAG,EAAE,MAAM,CAAC,GAAG;QACf,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,eAAe,EAAE,MAAM,CAAC,eAAe;KAC1C,CAAC,CAAC;IACH,IAAI,KAAK,KAAK,IAAI;QAAE,MAAM,IAAI,YAAY,EAAE,CAAC;IAC7C,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,UAAU,EAAE,CAAC,GAAG,CAAC,CAAC;IAElB,MAAM,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;IAClC,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,SAAS,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;IAChE,MAAM,MAAM,GAAG,gBAAgB,CAAC,MAAM,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IACvD,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;IAC7B,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IACrB,MAAM,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACxB,MAAM,CAAC,KAAK,EAAE,CAAC;IACf,MAAM,MAAM,CAAC,QAAQ,EAAE,CAAC;IACxB,IAAI,CAAC,MAAM,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC/D,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC;IAEhB,OAAO,OAAO,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE;QAC1C,WAAW;QACX,QAAQ,EAAE,MAAM,CAAC,QAAQ,IAAI,UAAU,IAAI,CAAC,SAAS,EAAE;QACvD,QAAQ,EAAE,IAAI,CAAC,QAAQ;KAC1B,CAAC,CAAC;AACP,CAAC","sourcesContent":["import type { AssetManifest, ProjectDocument, Registry, Size2D, VisualDocument } from \"@motionscript/core\";\
|
|
1
|
+
{"version":3,"file":"export-audio.js","sourceRoot":"","sources":["../src/export-audio.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAElD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,UAAU,EAAoB,MAAM,WAAW,CAAC;AAC/F,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAsBxD,qFAAqF;AACrF,MAAM,OAAO,YAAa,SAAQ,KAAK;IACjB,IAAI,GAAG,cAAc,CAAC;IAExC;QACI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAClD,CAAC;CACJ;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,MAAyB;IACvD,MAAM,EAAE,MAAM,GAAG,KAAK,EAAE,KAAK,GAAG,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,GAAG,KAAK,EAAE,GAAG,MAAM,CAAC;IACvF,MAAM,IAAI,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;IAChC,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO;QAAE,MAAM,IAAI,UAAU,CAAC,8BAA8B,MAAM,uCAAuC,CAAC,CAAC;IAC7H,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,yEAAyE;IACzE,MAAM,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAEjC,MAAM,KAAK,GAAG,MAAM,gBAAgB,CAAC;QACjC,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,GAAG,EAAE,MAAM,CAAC,GAAG;QACf,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,eAAe,EAAE,MAAM,CAAC,eAAe;KAC1C,CAAC,CAAC;IACH,IAAI,KAAK,KAAK,IAAI;QAAE,MAAM,IAAI,YAAY,EAAE,CAAC;IAC7C,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,UAAU,EAAE,CAAC,GAAG,CAAC,CAAC;IAElB,MAAM,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;IAClC,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,SAAS,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;IAChE,MAAM,MAAM,GAAG,gBAAgB,CAAC,MAAM,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IACvD,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;IAC7B,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IACrB,MAAM,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACxB,MAAM,CAAC,KAAK,EAAE,CAAC;IACf,MAAM,MAAM,CAAC,QAAQ,EAAE,CAAC;IACxB,IAAI,CAAC,MAAM,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC/D,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC;IAEhB,OAAO,OAAO,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE;QAC1C,WAAW;QACX,QAAQ,EAAE,MAAM,CAAC,QAAQ,IAAI,UAAU,IAAI,CAAC,SAAS,EAAE;QACvD,QAAQ,EAAE,IAAI,CAAC,QAAQ;KAC1B,CAAC,CAAC;AACP,CAAC","sourcesContent":["import type { AssetManifest, ProjectDocument, Registry, Size2D, VisualDocument } from \"@motionscript/core\";\nimport { BufferTarget, Output } from \"mediabunny\";\n\nimport { deliver } from \"./deliver\";\nimport type { RendererWasmOption } from \"./renderer\";\nimport type { AudioEncoderOptions, ExportProgressCallback } from \"./exporter\";\nimport { audioTrackSource, ensureAudioEncoder, formatSpec, type AudioFormat } from \"./formats\";\nimport { mixTimelineAudio } from \"./mix-timeline-audio\";\n\nexport type ExportAudioParams = RendererWasmOption & {\n /** A project, or a lone still or composition as the one track of one. */\n document: ProjectDocument | VisualDocument;\n /** The vocabulary its clips compile against. Defaults to what the library ships. */\n registry?: Registry;\n /** Discovery lays clips out at this size and rate; 1920×1080 at 60 fps when neither the document nor this says. */\n viewport?: Size2D;\n fps?: number;\n manifest?: AssetManifest;\n /** Defaults to `'wav'`, which needs no encoder. */\n format?: AudioFormat;\n audio?: AudioEncoderOptions;\n /** Defaults to `export.<extension>`. */\n filename?: string;\n onProgress?: ExportProgressCallback;\n signal?: AbortSignal;\n /** When true, the encoded bytes are returned instead of triggering a browser download. */\n returnBytes?: boolean;\n};\n\n/** The timeline schedules no sound, so an audio-only export has nothing to write. */\nexport class NoAudioError extends Error {\n override readonly name = \"NoAudioError\";\n\n constructor() {\n super(\"The timeline has no audio to export.\");\n }\n}\n\n/**\n * Mix the timeline's audio and encode it alone: WAV, MP3 or M4A (AAC). Nothing\n * is drawn, so no frame is rendered and no GPU surface is made.\n *\n * @throws {NoAudioError} when nothing on the timeline plays, rather than writing a silent file.\n */\nexport async function exportAudio(params: ExportAudioParams): Promise<Uint8Array | void> {\n const { format = \"wav\", audio = {}, onProgress, signal, returnBytes = false } = params;\n const spec = formatSpec(format);\n if (spec.kind !== \"audio\") throw new RangeError(`exportAudio writes audio; \"${format}\" is video, which exportVideo writes.`);\n signal?.throwIfAborted();\n // Before the mix, so a browser that cannot encode the format fails fast.\n await ensureAudioEncoder(format);\n\n const mixed = await mixTimelineAudio({\n document: params.document,\n registry: params.registry,\n viewport: params.viewport,\n fps: params.fps,\n manifest: params.manifest,\n sampleRate: audio.sampleRate,\n rendererWasmUrl: params.rendererWasmUrl,\n });\n if (mixed === null) throw new NoAudioError();\n signal?.throwIfAborted();\n onProgress?.(0.5);\n\n const target = new BufferTarget();\n const output = new Output({ format: spec.container(), target });\n const source = audioTrackSource(format, audio.bitrate);\n output.addAudioTrack(source);\n await output.start();\n await source.add(mixed);\n source.close();\n await output.finalize();\n if (!target.buffer) throw new Error(\"Export produced no data\");\n onProgress?.(1);\n\n return deliver(new Uint8Array(target.buffer), {\n returnBytes,\n filename: params.filename ?? `export.${spec.extension}`,\n mimeType: spec.mimeType,\n });\n}\n"]}
|
package/dist/exporter.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"exporter.js","sourceRoot":"","sources":["../src/exporter.ts"],"names":[],"mappings":"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","sourcesContent":["import {\r\n DEFAULT_OUTPUT, ManifestAssetCatalog, Registry, resolveProjectDocument, type AssetManifest,\r\n type FrameRange, type ProjectDocument, type RenderIssue, type VisualDocument, type Size2D, type StorageAdapter, type Theme,\r\n} from \"@motionscript/core\";\r\nimport {\r\n renderTimeline,\r\n type VideoFrameSink,\r\n} from \"./frame-loop\";\r\nimport {\r\n AudioBufferSource,\r\n BufferTarget,\r\n CanvasSource,\r\n canEncodeVideo,\r\n Output,\r\n type Quality,\r\n type VideoCodec,\r\n} from 'mediabunny';\r\nimport type { BrowserRenderContext } from \"./engine\";\r\nimport { loadRenderer, type RendererWasmOption } from \"./renderer\";\r\nimport { WebAudioMixer } from \"./audio/mixer\";\r\nimport { deliver } from \"./deliver\";\r\nimport {\r\n audioTrackSource, DEFAULT_AUDIO_BITRATE, ensureAudioEncoder, formatSpec, type VideoFormat,\r\n} from \"./formats\";\r\n\r\n/** Reports export progress in [0, 1]; video encoding occupies most of the range, audio mixing/finalize the tail. */\r\nexport type ExportProgressCallback = (progress: number) => void;\r\n\r\n/**\r\n * How the video track is encoded.\r\n *\r\n * Every field is a straight pass-through to mediabunny's `VideoEncodingConfig`,\r\n * defaulted here rather than there so an export has one obvious place to look.\r\n */\r\nexport type VideoEncoderOptions = {\r\n /**\r\n * Which codec the video track is encoded with. Defaults to the format's own:\r\n * `'avc'` (H.264) for MP4, which every player, editor and upload target\r\n * accepts, and `'vp9'` for WebM.\r\n *\r\n * `'hevc'` and `'av1'` reconstruct sharp edges far better at the same\r\n * bitrate, which is what a listing of syntax-highlighted code or a hairline\r\n * stroke is made of — worth reaching for when the file is going somewhere\r\n * that decodes them. Both are still 4:2:0, so they narrow the gap rather\r\n * than close it. Availability depends on the platform's encoders; an\r\n * unsupported choice, or one the container cannot hold (`'avc'` in WebM),\r\n * falls back to the format's default rather than failing the export.\r\n */\r\n codec?: 'avc' | 'hevc' | 'av1' | 'vp9' | 'vp8';\r\n /**\r\n * Which encoder implementation WebCodecs should prefer.\r\n *\r\n * Defaults to `'no-preference'`, which lets each platform use its own\r\n * hardware encoder (VideoToolbox on macOS, Media Foundation/NVENC on\r\n * Windows, VA-API on Linux). That is typically several times faster than the\r\n * software path *and* it is the only configuration in which this sink's\r\n * zero-readback design actually holds: a software encoder needs the pixels\r\n * on the CPU, so it forces a full GPU→CPU copy of every frame that a\r\n * hardware encoder reads directly off the canvas.\r\n *\r\n * Pass `'prefer-software'` when byte-identical output across machines\r\n * matters more than speed — different hardware encoders tune quality and\r\n * colour differently at the same nominal bitrate, so the same settings do\r\n * not produce the same file.\r\n */\r\n hardwareAcceleration?: 'no-preference' | 'prefer-hardware' | 'prefer-software';\r\n /**\r\n * Target bitrate in bits per second, or one of mediabunny's subjective\r\n * `Quality` levels.\r\n *\r\n * Defaults to {@link motionGraphicsBitrate}, which is deliberately *not*\r\n * mediabunny's `QUALITY_VERY_HIGH`: that level is frame-rate blind, so a\r\n * 60 fps export got the same ~12 Mbps at 1080p as a 30 fps one and paid for\r\n * it with ringing along every glyph edge and banding across every gradient.\r\n * See that function for the reasoning.\r\n */\r\n bitrate?: number | Quality;\r\n /** Defaults to mediabunny's `'variable'`. `'constant'` is the reproducible-output choice. */\r\n bitrateMode?: 'constant' | 'variable';\r\n /**\r\n * Seconds between key frames. Defaults to 1.\r\n *\r\n * Shorter than mediabunny's 2, because a frame's error is carried by every\r\n * inter frame that references it: on synthetic content, where a flat\r\n * background makes a shimmer obvious, a keyframe twice as often visibly\r\n * resets the drift. It also halves the seek granularity, which matters for a\r\n * clip an editor is going to scrub.\r\n */\r\n keyFrameInterval?: number;\r\n /**\r\n * WebCodecs performance/quality trade-off. Defaults to `'quality'`, which\r\n * also guarantees no frame is dropped — the only correct setting for a file\r\n * being written rather than streamed.\r\n */\r\n latencyMode?: 'quality' | 'realtime';\r\n /**\r\n * A hint about what the frames contain, per\r\n * [mst-content-hint](https://w3c.github.io/mst-content-hint/#video-content-hints).\r\n *\r\n * Defaults to `'detail'`: encoders that honour it bias towards preserving\r\n * sharp edges and fine texture over smoothing motion, which is what motion\r\n * graphics are. Ignored where it is not implemented.\r\n */\r\n contentHint?: string;\r\n};\r\n\r\n/**\r\n * Bits per pixel per frame the default bitrate is built from.\r\n *\r\n * Rendered motion graphics are the hard case for a codec tuned on camera\r\n * footage: hard glyph edges, large flat fields, and gradients with no sensor\r\n * noise to hide the quantisation in. They compress *worse* than live action at\r\n * the same perceived quality, not better, so the figure here is well above what\r\n * a streaming preset would use — this is a master being rendered once, not a\r\n * bitstream being pushed down a pipe.\r\n */\r\nconst BITS_PER_PIXEL = 0.2;\r\n/** Floor, so a tiny viewport still gets enough bits to be legible. */\r\nconst MIN_VIDEO_BITRATE = 2_000_000;\r\n/**\r\n * Ceiling. Past this the returns are gone and the levels start excluding\r\n * hardware decoders — 4K60 lands here, and AVC level 5.1 (which every consumer\r\n * device implements) tops out at 240 Mbps, well clear of it.\r\n */\r\nconst MAX_VIDEO_BITRATE = 100_000_000;\r\n\r\n/**\r\n * The default video bitrate for an export of this size and frame rate.\r\n *\r\n * Scaling with **both** axes is the point. mediabunny's `Quality` levels scale\r\n * with resolution only, so doubling the frame rate halved the bits available per\r\n * frame while leaving the number unchanged — which is exactly the configuration\r\n * that produced clean stills and a mushy video from the very same renderer.\r\n */\r\nexport function motionGraphicsBitrate(width: number, height: number, fps: number): number {\r\n const raw = width * height * fps * BITS_PER_PIXEL;\r\n return Math.round(Math.min(MAX_VIDEO_BITRATE, Math.max(MIN_VIDEO_BITRATE, raw)));\r\n}\r\n\r\n/** How the mixed audio track is encoded. */\r\nexport type AudioEncoderOptions = {\r\n /** Bitrate in bits per second for AAC, Opus and MP3; WAV ignores it. Defaults to 192 kbps. */\r\n bitrate?: number;\r\n /** Mixdown sample rate in Hz. Defaults to the mixer's own (44100); Opus is resampled to 48 kHz. */\r\n sampleRate?: number;\r\n};\r\n\r\n/** See {@link VideoEncoderOptions.keyFrameInterval}. */\r\nconst DEFAULT_KEY_FRAME_INTERVAL = 1;\r\n/** See {@link ExportParams.supersample}. */\r\nconst DEFAULT_SUPERSAMPLE = 2;\r\n\r\nexport type ExportParams = RendererWasmOption & {\r\n /** A project, or a lone still or composition as the one track of one. */\r\n document: ProjectDocument | VisualDocument;\r\n /** The vocabulary its clips compile against. Defaults to what the library ships. */\r\n registry?: Registry;\r\n /** Win over the document's own; 1920×1080 at 60 fps when neither says. */\r\n viewport?: Size2D;\r\n fps?: number;\r\n scale?: number;\r\n /** The container, and with it the default codecs: H.264 and AAC for `'mp4'` (the default), VP9 and Opus for `'webm'`. */\r\n format?: VideoFormat;\r\n /** Defaults to `export.<extension>`. */\r\n filename?: string;\r\n manifest?: AssetManifest;\r\n /** Theme for a document that carries none of its own. */\r\n theme?: Theme;\r\n /**\r\n * Set false to produce a silent file — no audio track is added to the\r\n * container and the mix is skipped entirely. Defaults to true.\r\n */\r\n includeAudio?: boolean;\r\n /** Video encoder settings; see {@link VideoEncoderOptions} for the defaults. */\r\n video?: VideoEncoderOptions;\r\n /** Audio encoder settings; ignored when `includeAudio` is false. */\r\n audio?: AudioEncoderOptions;\r\n /**\r\n * Encode only these frames (`end` excluded), timestamped from 0 and silent:\r\n * one segment of a split export, for `concatVideoSegments` to join. Mix\r\n * the audio once with `mixTimelineAudio` instead.\r\n */\r\n frames?: FrameRange;\r\n onProgress?: ExportProgressCallback;\r\n signal?: AbortSignal;\r\n /**\r\n * What the renderer drew but not as the document says, once per kind,\r\n * feature and node. A lost GPU device fails the export with a\r\n * `RenderBackendError` instead.\r\n */\r\n onIssue?: (issue: RenderIssue) => void;\r\n /**\r\n * Render each frame at this multiple of the output resolution and downsample\r\n * it before encoding. **Defaults to 2**; pass 1 to turn it off.\r\n *\r\n * The one lever that improves an export at a *fixed* output size. Every\r\n * codec a browser can encode is 4:2:0 — colour is stored at half resolution\r\n * on both axes, and no bitrate buys that back — so the colour fringing along\r\n * a saturated glyph or a hard edge is a property of the format, not of the\r\n * settings. Handing the encoder an already-downsampled frame does not undo\r\n * the subsampling, but it does mean every chroma sample averages four real\r\n * pixels instead of one, which is what removes most of the crawl on fine\r\n * detail and much of the banding on gradients.\r\n *\r\n * Measured against a correctly-sampled reference frame of a syntax-highlighted\r\n * listing, `2` lands 2.7 dB closer than a 1× encode — and 1.3 dB closer than\r\n * an *unencoded* 1× still, which is why it is the default despite costing\r\n * `supersample²` times the render work per frame plus a rescale. Pass 1 when\r\n * turnaround matters more than the file, and note that exporting at\r\n * `scale: 2` is cheaper than supersampling *and* better whenever the extra\r\n * resolution is wanted in the file itself.\r\n *\r\n * Clamped so the render surface stays inside {@link MAX_RENDER_AXIS}, since a\r\n * supersampled 4K export is already drawing 8K frames.\r\n */\r\n supersample?: number;\r\n /**\r\n * When true, the encoded bytes are returned instead of triggering a\r\n * browser download. Used by headless drivers (e.g. the CLI) that capture\r\n * the output themselves and write it to disk; leave unset for the\r\n * interactive player, which downloads via an `<a>` element.\r\n */\r\n returnBytes?: boolean;\r\n}\r\n\r\nconst EMPTY_MANIFEST: AssetManifest = {\r\n image: {},\r\n video: {},\r\n audio: {},\r\n font: {},\r\n};\r\n\r\n// ── Audio mixing ─────────────────────────────────────────────────────────────\r\n//\r\n// The mix itself lives in ./audio/mixer, shared with `mixTimelineAudio` so a\r\n// split export and a whole one cannot mix differently.\r\n\r\n// ── Video sink ───────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * mediabunny/WebCodecs implementation of the frame sink, writing whichever\r\n * container `format` names.\r\n *\r\n * Wraps the very `<canvas>` the renderer draws into, so `addFrame` costs nothing but\r\n * a timestamp — WebCodecs pulls the pixels from the canvas itself, with no\r\n * GPU→CPU readback. That is why the seam signals a frame rather than passing one.\r\n *\r\n * The readback claim holds only while the encoder is a hardware one, which is\r\n * why {@link VideoEncoderOptions.hardwareAcceleration} defaults to letting the\r\n * platform choose: a software encoder has to be handed CPU pixels, so it drags\r\n * a full-frame copy off the GPU on every single frame.\r\n */\r\nexport class MediabunnyVideoSink implements VideoFrameSink<AudioBuffer> {\r\n private readonly target = new BufferTarget();\r\n private readonly output: Output;\r\n private readonly video: CanvasSource;\r\n private audio: AudioBufferSource | null = null;\r\n\r\n constructor(\r\n canvas: HTMLCanvasElement,\r\n /** Encoded output size, which is the canvas' own unless supersampling is on. */\r\n output: { width: number; height: number },\r\n private readonly format: VideoFormat,\r\n encoding: ResolvedVideoEncoding,\r\n private readonly audioOptions: AudioEncoderOptions = {},\r\n ) {\r\n const downsampling = output.width !== canvas.width || output.height !== canvas.height;\r\n this.video = new CanvasSource(canvas, {\r\n codec: encoding.codec,\r\n bitrate: encoding.bitrate,\r\n hardwareAcceleration: encoding.hardwareAcceleration,\r\n keyFrameInterval: encoding.keyFrameInterval,\r\n latencyMode: encoding.latencyMode,\r\n contentHint: encoding.contentHint,\r\n ...(encoding.bitrateMode !== undefined && { bitrateMode: encoding.bitrateMode }),\r\n // `fit` is exact here — the supersampled canvas is a whole multiple\r\n // of the output — but mediabunny requires it whenever both axes are\r\n // given, so it is stated rather than inferred.\r\n ...(downsampling && { transform: { width: output.width, height: output.height, fit: 'fill' as const } }),\r\n });\r\n this.output = new Output({ format: formatSpec(format).container(), target: this.target });\r\n }\r\n\r\n async start(info: { fps: number; hasAudio: boolean }): Promise<void> {\r\n this.output.addVideoTrack(this.video, { frameRate: info.fps });\r\n // Tracks must be declared before the muxer starts, which is why the\r\n // orchestrator reports this rather than leaving it to be inferred.\r\n if (info.hasAudio) {\r\n await ensureAudioEncoder(this.format);\r\n this.audio = audioTrackSource(this.format, this.audioOptions.bitrate ?? DEFAULT_AUDIO_BITRATE);\r\n this.output.addAudioTrack(this.audio);\r\n }\r\n await this.output.start();\r\n }\r\n\r\n async addFrame(timestamp: number, duration: number): Promise<void> {\r\n await this.video.add(timestamp, duration);\r\n }\r\n\r\n async addAudio(mixed: AudioBuffer): Promise<void> {\r\n if (!this.audio) return;\r\n await this.audio.add(mixed);\r\n }\r\n\r\n async finalize(): Promise<Uint8Array> {\r\n this.video.close();\r\n this.audio?.close();\r\n await this.output.finalize();\r\n const buffer = this.target.buffer;\r\n if (!buffer) throw new Error('Export produced no data');\r\n return new Uint8Array(buffer);\r\n }\r\n}\r\n\r\n// ── Main export ──────────────────────────────────────────────────────────────\r\n\r\n/** Round up to the nearest even integer — see the call site. */\r\nfunction evenUp(n: number): number {\r\n const rounded = Math.round(n);\r\n return rounded % 2 === 0 ? rounded : rounded + 1;\r\n}\r\n\r\n/**\r\n * Longest axis the render surface is allowed to reach.\r\n *\r\n * Supersampling multiplies the surface, not the output: a 4K export at\r\n * `supersample: 2` is drawing 8K frames, and the next step up asks for a\r\n * 15360-wide WebGL surface that most drivers will not give and none will give\r\n * cheaply. 8192 is the smallest `MAX_TEXTURE_SIZE` in wide circulation, so a\r\n * surface inside it is one every machine that can run the export at all can\r\n * also allocate.\r\n */\r\nconst MAX_RENDER_AXIS = 8192;\r\n\r\n/**\r\n * How far the frame can actually be supersampled at this output size.\r\n *\r\n * Reduced rather than refused: an export that quietly renders at 1× is a\r\n * slightly softer file, and one that throws because the default was too\r\n * ambitious for a 4K project is no file at all.\r\n */\r\nfunction fittedSupersample(requested: number, output: { width: number; height: number }): number {\r\n const longest = Math.max(output.width, output.height);\r\n let factor = Math.max(1, Math.round(requested));\r\n while (factor > 1 && longest * factor > MAX_RENDER_AXIS) factor--;\r\n return factor;\r\n}\r\n\r\n/**\r\n * The codec to actually encode with: the one asked for if the container holds\r\n * it and this platform can encode it at this size, else the format's default,\r\n * else its fallback (`'avc'` for MP4, `'vp8'` for WebM).\r\n *\r\n * Which encoders a machine has varies by OS, browser and GPU, and an export that\r\n * dies at the muxer for want of an HEVC encoder is worse than one that quietly\r\n * produces H.264. The fallback is not probed: there is nothing to fall back to.\r\n */\r\nasync function resolveCodec(\r\n requested: VideoCodec | undefined,\r\n format: VideoFormat,\r\n size: { width: number; height: number },\r\n fps: number,\r\n): Promise<VideoCodec> {\r\n const { codecs, fallback } = formatSpec(format).video!;\r\n let wanted = requested ?? codecs[0];\r\n if (!codecs.includes(wanted)) {\r\n console.warn(`[motionscript] ${format.toUpperCase()} cannot hold ${wanted}; exporting as ${CODEC_NAMES[codecs[0]]} instead.`);\r\n wanted = codecs[0];\r\n }\r\n for (const codec of new Set([wanted, codecs[0], fallback])) {\r\n if (codec === fallback || await canEncode(codec, size, fps)) {\r\n if (codec !== wanted) {\r\n console.warn(`[motionscript] No ${wanted} encoder available; exporting as ${CODEC_NAMES[codec]} instead.`);\r\n }\r\n return codec;\r\n }\r\n }\r\n return fallback;\r\n}\r\n\r\nconst CODEC_NAMES: Record<VideoCodec, string> = { avc: 'H.264', hevc: 'HEVC', vp9: 'VP9', av1: 'AV1', vp8: 'VP8' };\r\n\r\nasync function canEncode(codec: VideoCodec, size: { width: number; height: number }, fps: number): Promise<boolean> {\r\n try {\r\n return await canEncodeVideo(codec, {\r\n width: size.width,\r\n height: size.height,\r\n bitrate: motionGraphicsBitrate(size.width, size.height, fps),\r\n });\r\n } catch {\r\n // A probe that throws is a probe that answered \"no\".\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Every encoder setting, decided. A split export pins one of these across its\r\n * segments, since `concatVideoSegments` refuses parts encoded differently. With\r\n * a numeric `bitrate` (see {@link plainVideoEncoding}) it is plain data and\r\n * crosses a process boundary.\r\n */\r\nexport interface ResolvedVideoEncoding {\r\n codec: VideoCodec;\r\n /** A `Quality` is left for the encoder to size, as an export always has. */\r\n bitrate: number | Quality;\r\n hardwareAcceleration: NonNullable<VideoEncoderOptions['hardwareAcceleration']>;\r\n keyFrameInterval: number;\r\n latencyMode: NonNullable<VideoEncoderOptions['latencyMode']>;\r\n contentHint: string;\r\n bitrateMode?: VideoEncoderOptions['bitrateMode'];\r\n}\r\n\r\n/** {@link VideoEncoderOptions} with its defaults applied against the encoded size and rate. */\r\nexport async function resolveVideoEncoding(\r\n video: VideoEncoderOptions | undefined,\r\n format: VideoFormat,\r\n output: { width: number; height: number },\r\n fps: number,\r\n): Promise<ResolvedVideoEncoding> {\r\n const codec = await resolveCodec(video?.codec, format, output, fps);\r\n return {\r\n codec,\r\n bitrate: video?.bitrate ?? motionGraphicsBitrate(output.width, output.height, fps),\r\n hardwareAcceleration: video?.hardwareAcceleration ?? 'no-preference',\r\n keyFrameInterval: video?.keyFrameInterval ?? DEFAULT_KEY_FRAME_INTERVAL,\r\n latencyMode: video?.latencyMode ?? 'quality',\r\n contentHint: video?.contentHint ?? 'detail',\r\n ...(video?.bitrateMode !== undefined && { bitrateMode: video.bitrateMode }),\r\n };\r\n}\r\n\r\n/**\r\n * The encoding with a `Quality` turned into the bits per second mediabunny would\r\n * give it at this size, since a class instance cannot cross a `MessagePort`.\r\n */\r\nexport function plainVideoEncoding(\r\n encoding: ResolvedVideoEncoding,\r\n output: { width: number; height: number },\r\n): ResolvedVideoEncoding & { bitrate: number } {\r\n const { bitrate } = encoding;\r\n if (typeof bitrate === 'number') return { ...encoding, bitrate };\r\n const quality = bitrate as unknown as { _toVideoBitrate(c: VideoCodec, w: number, h: number): number };\r\n return { ...encoding, bitrate: quality._toVideoBitrate(encoding.codec, output.width, output.height) };\r\n}\r\n\r\n/** The encoded size, rounded up to even, and how far each frame is supersampled. */\r\nexport function exportGeometry(viewport: Size2D, scale: number, supersample: number) {\r\n // Every block-based codec works in macroblocks, so an odd dimension is\r\n // padded by the encoder with a smeared strip that was never drawn.\r\n const output = { width: evenUp(viewport.width * scale), height: evenUp(viewport.height * scale) };\r\n const oversample = fittedSupersample(supersample, output);\r\n return { output, oversample, renderScale: scale * oversample };\r\n}\r\n\r\n/** A hidden canvas with a mounted render context, reusable across renders of one project. */\r\nexport interface ExportSurface {\r\n canvas: HTMLCanvasElement;\r\n renderContext: BrowserRenderContext;\r\n storageAdapter: StorageAdapter;\r\n assetCatalog: ManifestAssetCatalog;\r\n dispose(): void;\r\n}\r\n\r\n/**\r\n * Open the canvas, renderer, storage and render context an export draws with,\r\n * resolving once the surface can draw (a frame drawn before an asynchronously\r\n * started surface is ready encodes blank). The canvas is a whole multiple of the\r\n * output, so the downsample is exact.\r\n */\r\nexport async function openExportSurface(params: {\r\n canvasSize: { width: number; height: number };\r\n viewport: Size2D;\r\n fps: number;\r\n manifest: AssetManifest;\r\n} & RendererWasmOption): Promise<ExportSurface> {\r\n const canvas = document.createElement('canvas');\r\n canvas.width = params.canvasSize.width;\r\n canvas.height = params.canvasSize.height;\r\n canvas.style.display = 'none';\r\n document.body.appendChild(canvas);\r\n try {\r\n const assetCatalog = new ManifestAssetCatalog(params.manifest);\r\n const renderer = await loadRenderer(params);\r\n const storageAdapter = renderer.createStorageAdapter({ catalog: assetCatalog, viewport: params.viewport, fps: params.fps });\r\n const renderContext = renderer.createRenderContext(storageAdapter);\r\n renderContext.mount(canvas);\r\n await renderContext.whenMounted?.();\r\n return {\r\n canvas, renderContext, storageAdapter, assetCatalog,\r\n dispose: () => {\r\n renderContext.dispose();\r\n canvas.remove();\r\n },\r\n };\r\n } catch (error) {\r\n canvas.remove();\r\n throw error;\r\n }\r\n}\r\n\r\n/**\r\n * Renders a document frame-by-frame to an offscreen canvas and muxes the result\r\n * into an MP4 or WebM via mediabunny.\r\n *\r\n * The frame loop itself — discovery, state evaluation, the warm-and-re-render retry\r\n * that keeps every frame accurate, audio scheduling, progress and cancellation —\r\n * lives in `renderTimeline` (frame-loop.ts). What this function\r\n * supplies is the browser-specific half: a canvas to draw into, a WebCodecs sink,\r\n * a Web Audio mixer, and the `<a download>` delivery.\r\n */\r\nexport async function exportVideo(params: ExportParams): Promise<Uint8Array | void> {\r\n const {\r\n // Not `document`, which would shadow the DOM global this function uses.\r\n document: source,\r\n registry = new Registry(),\r\n scale = 1,\r\n format = 'mp4',\r\n manifest = EMPTY_MANIFEST,\r\n theme,\r\n includeAudio = true,\r\n video,\r\n audio = {},\r\n frames,\r\n onProgress,\r\n signal,\r\n rendererWasmUrl,\r\n supersample = DEFAULT_SUPERSAMPLE,\r\n returnBytes = false,\r\n } = params;\r\n\r\n const spec = formatSpec(format);\r\n if (spec.kind !== 'video') throw new RangeError(`exportVideo writes video; \"${format}\" is audio, which exportAudio writes.`);\r\n const filename = params.filename ?? `export.${spec.extension}`;\r\n const project = resolveProjectDocument(source, params, DEFAULT_OUTPUT);\r\n if (project.tracks.length === 0) return;\r\n signal?.throwIfAborted();\r\n const viewport = project.viewport!;\r\n const fps = project.fps!;\r\n\r\n const { output, oversample, renderScale } = exportGeometry(viewport, scale, supersample);\r\n const surface = await openExportSurface({\r\n canvasSize: { width: output.width * oversample, height: output.height * oversample },\r\n viewport, fps, manifest, rendererWasmUrl,\r\n });\r\n\r\n let bytes: Uint8Array | undefined;\r\n try {\r\n const encoding = await resolveVideoEncoding(video, format, output, fps);\r\n bytes = await renderTimeline<AudioBuffer>({\r\n project, registry, theme, scale: renderScale, includeAudio, frames,\r\n renderContext: surface.renderContext, storageAdapter: surface.storageAdapter, assetCatalog: surface.assetCatalog,\r\n sink: new MediabunnyVideoSink(surface.canvas, output, format, encoding, audio),\r\n mixer: new WebAudioMixer(audio.sampleRate, surface.assetCatalog),\r\n onProgress,\r\n onIssue: params.onIssue,\r\n signal,\r\n }) as Uint8Array | undefined;\r\n } finally {\r\n surface.dispose();\r\n }\r\n\r\n if (!bytes) return;\r\n return deliver(bytes, { returnBytes, filename, mimeType: spec.mimeType });\r\n}\r\n"]}
|
|
1
|
+
{"version":3,"file":"exporter.js","sourceRoot":"","sources":["../src/exporter.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n DEFAULT_OUTPUT, ManifestAssetCatalog, Registry, resolveProjectDocument, type AssetManifest,\n type FrameRange, type ProjectDocument, type RenderIssue, type VisualDocument, type Size2D, type StorageAdapter, type Theme,\n} from \"@motionscript/core\";\nimport {\n renderTimeline,\n type VideoFrameSink,\n} from \"./frame-loop\";\nimport {\n AudioBufferSource,\n BufferTarget,\n CanvasSource,\n canEncodeVideo,\n Output,\n type Quality,\n type VideoCodec,\n} from 'mediabunny';\nimport type { BrowserRenderContext } from \"./engine\";\nimport { loadRenderer, type RendererWasmOption } from \"./renderer\";\nimport { WebAudioMixer } from \"./audio/mixer\";\nimport { deliver } from \"./deliver\";\nimport {\n audioTrackSource, DEFAULT_AUDIO_BITRATE, ensureAudioEncoder, formatSpec, type VideoFormat,\n} from \"./formats\";\n\n/** Reports export progress in [0, 1]; video encoding occupies most of the range, audio mixing/finalize the tail. */\nexport type ExportProgressCallback = (progress: number) => void;\n\n/**\n * How the video track is encoded.\n *\n * Every field is a straight pass-through to mediabunny's `VideoEncodingConfig`,\n * defaulted here rather than there so an export has one obvious place to look.\n */\nexport type VideoEncoderOptions = {\n /**\n * Which codec the video track is encoded with. Defaults to the format's own:\n * `'avc'` (H.264) for MP4, which every player, editor and upload target\n * accepts, and `'vp9'` for WebM.\n *\n * `'hevc'` and `'av1'` reconstruct sharp edges far better at the same\n * bitrate, which is what a listing of syntax-highlighted code or a hairline\n * stroke is made of — worth reaching for when the file is going somewhere\n * that decodes them. Both are still 4:2:0, so they narrow the gap rather\n * than close it. Availability depends on the platform's encoders; an\n * unsupported choice, or one the container cannot hold (`'avc'` in WebM),\n * falls back to the format's default rather than failing the export.\n */\n codec?: 'avc' | 'hevc' | 'av1' | 'vp9' | 'vp8';\n /**\n * Which encoder implementation WebCodecs should prefer.\n *\n * Defaults to `'no-preference'`, which lets each platform use its own\n * hardware encoder (VideoToolbox on macOS, Media Foundation/NVENC on\n * Windows, VA-API on Linux). That is typically several times faster than the\n * software path *and* it is the only configuration in which this sink's\n * zero-readback design actually holds: a software encoder needs the pixels\n * on the CPU, so it forces a full GPU→CPU copy of every frame that a\n * hardware encoder reads directly off the canvas.\n *\n * Pass `'prefer-software'` when byte-identical output across machines\n * matters more than speed — different hardware encoders tune quality and\n * colour differently at the same nominal bitrate, so the same settings do\n * not produce the same file.\n */\n hardwareAcceleration?: 'no-preference' | 'prefer-hardware' | 'prefer-software';\n /**\n * Target bitrate in bits per second, or one of mediabunny's subjective\n * `Quality` levels.\n *\n * Defaults to {@link motionGraphicsBitrate}, which is deliberately *not*\n * mediabunny's `QUALITY_VERY_HIGH`: that level is frame-rate blind, so a\n * 60 fps export got the same ~12 Mbps at 1080p as a 30 fps one and paid for\n * it with ringing along every glyph edge and banding across every gradient.\n * See that function for the reasoning.\n */\n bitrate?: number | Quality;\n /** Defaults to mediabunny's `'variable'`. `'constant'` is the reproducible-output choice. */\n bitrateMode?: 'constant' | 'variable';\n /**\n * Seconds between key frames. Defaults to 1.\n *\n * Shorter than mediabunny's 2, because a frame's error is carried by every\n * inter frame that references it: on synthetic content, where a flat\n * background makes a shimmer obvious, a keyframe twice as often visibly\n * resets the drift. It also halves the seek granularity, which matters for a\n * clip an editor is going to scrub.\n */\n keyFrameInterval?: number;\n /**\n * WebCodecs performance/quality trade-off. Defaults to `'quality'`, which\n * also guarantees no frame is dropped — the only correct setting for a file\n * being written rather than streamed.\n */\n latencyMode?: 'quality' | 'realtime';\n /**\n * A hint about what the frames contain, per\n * [mst-content-hint](https://w3c.github.io/mst-content-hint/#video-content-hints).\n *\n * Defaults to `'detail'`: encoders that honour it bias towards preserving\n * sharp edges and fine texture over smoothing motion, which is what motion\n * graphics are. Ignored where it is not implemented.\n */\n contentHint?: string;\n};\n\n/**\n * Bits per pixel per frame the default bitrate is built from.\n *\n * Rendered motion graphics are the hard case for a codec tuned on camera\n * footage: hard glyph edges, large flat fields, and gradients with no sensor\n * noise to hide the quantisation in. They compress *worse* than live action at\n * the same perceived quality, not better, so the figure here is well above what\n * a streaming preset would use — this is a master being rendered once, not a\n * bitstream being pushed down a pipe.\n */\nconst BITS_PER_PIXEL = 0.2;\n/** Floor, so a tiny viewport still gets enough bits to be legible. */\nconst MIN_VIDEO_BITRATE = 2_000_000;\n/**\n * Ceiling. Past this the returns are gone and the levels start excluding\n * hardware decoders — 4K60 lands here, and AVC level 5.1 (which every consumer\n * device implements) tops out at 240 Mbps, well clear of it.\n */\nconst MAX_VIDEO_BITRATE = 100_000_000;\n\n/**\n * The default video bitrate for an export of this size and frame rate.\n *\n * Scaling with **both** axes is the point. mediabunny's `Quality` levels scale\n * with resolution only, so doubling the frame rate halved the bits available per\n * frame while leaving the number unchanged — which is exactly the configuration\n * that produced clean stills and a mushy video from the very same renderer.\n */\nexport function motionGraphicsBitrate(width: number, height: number, fps: number): number {\n const raw = width * height * fps * BITS_PER_PIXEL;\n return Math.round(Math.min(MAX_VIDEO_BITRATE, Math.max(MIN_VIDEO_BITRATE, raw)));\n}\n\n/** How the mixed audio track is encoded. */\nexport type AudioEncoderOptions = {\n /** Bitrate in bits per second for AAC, Opus and MP3; WAV ignores it. Defaults to 192 kbps. */\n bitrate?: number;\n /** Mixdown sample rate in Hz. Defaults to the mixer's own (44100); Opus is resampled to 48 kHz. */\n sampleRate?: number;\n};\n\n/** See {@link VideoEncoderOptions.keyFrameInterval}. */\nconst DEFAULT_KEY_FRAME_INTERVAL = 1;\n/** See {@link ExportParams.supersample}. */\nconst DEFAULT_SUPERSAMPLE = 2;\n\nexport type ExportParams = RendererWasmOption & {\n /** A project, or a lone still or composition as the one track of one. */\n document: ProjectDocument | VisualDocument;\n /** The vocabulary its clips compile against. Defaults to what the library ships. */\n registry?: Registry;\n /** Win over the document's own; 1920×1080 at 60 fps when neither says. */\n viewport?: Size2D;\n fps?: number;\n scale?: number;\n /** The container, and with it the default codecs: H.264 and AAC for `'mp4'` (the default), VP9 and Opus for `'webm'`. */\n format?: VideoFormat;\n /** Defaults to `export.<extension>`. */\n filename?: string;\n manifest?: AssetManifest;\n /** Theme for a document that carries none of its own. */\n theme?: Theme;\n /**\n * Set false to produce a silent file — no audio track is added to the\n * container and the mix is skipped entirely. Defaults to true.\n */\n includeAudio?: boolean;\n /** Video encoder settings; see {@link VideoEncoderOptions} for the defaults. */\n video?: VideoEncoderOptions;\n /** Audio encoder settings; ignored when `includeAudio` is false. */\n audio?: AudioEncoderOptions;\n /**\n * Encode only these frames (`end` excluded), timestamped from 0 and silent:\n * one segment of a split export, for `concatVideoSegments` to join. Mix\n * the audio once with `mixTimelineAudio` instead.\n */\n frames?: FrameRange;\n onProgress?: ExportProgressCallback;\n signal?: AbortSignal;\n /**\n * What the renderer drew but not as the document says, once per kind,\n * feature and node. A lost GPU device fails the export with a\n * `RenderBackendError` instead.\n */\n onIssue?: (issue: RenderIssue) => void;\n /**\n * Render each frame at this multiple of the output resolution and downsample\n * it before encoding. **Defaults to 2**; pass 1 to turn it off.\n *\n * The one lever that improves an export at a *fixed* output size. Every\n * codec a browser can encode is 4:2:0 — colour is stored at half resolution\n * on both axes, and no bitrate buys that back — so the colour fringing along\n * a saturated glyph or a hard edge is a property of the format, not of the\n * settings. Handing the encoder an already-downsampled frame does not undo\n * the subsampling, but it does mean every chroma sample averages four real\n * pixels instead of one, which is what removes most of the crawl on fine\n * detail and much of the banding on gradients.\n *\n * Measured against a correctly-sampled reference frame of a syntax-highlighted\n * listing, `2` lands 2.7 dB closer than a 1× encode — and 1.3 dB closer than\n * an *unencoded* 1× still, which is why it is the default despite costing\n * `supersample²` times the render work per frame plus a rescale. Pass 1 when\n * turnaround matters more than the file, and note that exporting at\n * `scale: 2` is cheaper than supersampling *and* better whenever the extra\n * resolution is wanted in the file itself.\n *\n * Clamped so the render surface stays inside {@link MAX_RENDER_AXIS}, since a\n * supersampled 4K export is already drawing 8K frames.\n */\n supersample?: number;\n /**\n * When true, the encoded bytes are returned instead of triggering a\n * browser download. Used by headless drivers (e.g. the CLI) that capture\n * the output themselves and write it to disk; leave unset for the\n * interactive player, which downloads via an `<a>` element.\n */\n returnBytes?: boolean;\n}\n\nconst EMPTY_MANIFEST: AssetManifest = {\n image: {},\n video: {},\n audio: {},\n font: {},\n};\n\n// ── Audio mixing ─────────────────────────────────────────────────────────────\n//\n// The mix itself lives in ./audio/mixer, shared with `mixTimelineAudio` so a\n// split export and a whole one cannot mix differently.\n\n// ── Video sink ───────────────────────────────────────────────────────────────\n\n/**\n * mediabunny/WebCodecs implementation of the frame sink, writing whichever\n * container `format` names.\n *\n * Wraps the very `<canvas>` the renderer draws into, so `addFrame` costs nothing but\n * a timestamp — WebCodecs pulls the pixels from the canvas itself, with no\n * GPU→CPU readback. That is why the seam signals a frame rather than passing one.\n *\n * The readback claim holds only while the encoder is a hardware one, which is\n * why {@link VideoEncoderOptions.hardwareAcceleration} defaults to letting the\n * platform choose: a software encoder has to be handed CPU pixels, so it drags\n * a full-frame copy off the GPU on every single frame.\n */\nexport class MediabunnyVideoSink implements VideoFrameSink<AudioBuffer> {\n private readonly target = new BufferTarget();\n private readonly output: Output;\n private readonly video: CanvasSource;\n private audio: AudioBufferSource | null = null;\n\n constructor(\n canvas: HTMLCanvasElement,\n /** Encoded output size, which is the canvas' own unless supersampling is on. */\n output: { width: number; height: number },\n private readonly format: VideoFormat,\n encoding: ResolvedVideoEncoding,\n private readonly audioOptions: AudioEncoderOptions = {},\n ) {\n const downsampling = output.width !== canvas.width || output.height !== canvas.height;\n this.video = new CanvasSource(canvas, {\n codec: encoding.codec,\n bitrate: encoding.bitrate,\n hardwareAcceleration: encoding.hardwareAcceleration,\n keyFrameInterval: encoding.keyFrameInterval,\n latencyMode: encoding.latencyMode,\n contentHint: encoding.contentHint,\n ...(encoding.bitrateMode !== undefined && { bitrateMode: encoding.bitrateMode }),\n // `fit` is exact here — the supersampled canvas is a whole multiple\n // of the output — but mediabunny requires it whenever both axes are\n // given, so it is stated rather than inferred.\n ...(downsampling && { transform: { width: output.width, height: output.height, fit: 'fill' as const } }),\n });\n this.output = new Output({ format: formatSpec(format).container(), target: this.target });\n }\n\n async start(info: { fps: number; hasAudio: boolean }): Promise<void> {\n this.output.addVideoTrack(this.video, { frameRate: info.fps });\n // Tracks must be declared before the muxer starts, which is why the\n // orchestrator reports this rather than leaving it to be inferred.\n if (info.hasAudio) {\n await ensureAudioEncoder(this.format);\n this.audio = audioTrackSource(this.format, this.audioOptions.bitrate ?? DEFAULT_AUDIO_BITRATE);\n this.output.addAudioTrack(this.audio);\n }\n await this.output.start();\n }\n\n async addFrame(timestamp: number, duration: number): Promise<void> {\n await this.video.add(timestamp, duration);\n }\n\n async addAudio(mixed: AudioBuffer): Promise<void> {\n if (!this.audio) return;\n await this.audio.add(mixed);\n }\n\n async finalize(): Promise<Uint8Array> {\n this.video.close();\n this.audio?.close();\n await this.output.finalize();\n const buffer = this.target.buffer;\n if (!buffer) throw new Error('Export produced no data');\n return new Uint8Array(buffer);\n }\n}\n\n// ── Main export ──────────────────────────────────────────────────────────────\n\n/** Round up to the nearest even integer — see the call site. */\nfunction evenUp(n: number): number {\n const rounded = Math.round(n);\n return rounded % 2 === 0 ? rounded : rounded + 1;\n}\n\n/**\n * Longest axis the render surface is allowed to reach.\n *\n * Supersampling multiplies the surface, not the output: a 4K export at\n * `supersample: 2` is drawing 8K frames, and the next step up asks for a\n * 15360-wide WebGL surface that most drivers will not give and none will give\n * cheaply. 8192 is the smallest `MAX_TEXTURE_SIZE` in wide circulation, so a\n * surface inside it is one every machine that can run the export at all can\n * also allocate.\n */\nconst MAX_RENDER_AXIS = 8192;\n\n/**\n * How far the frame can actually be supersampled at this output size.\n *\n * Reduced rather than refused: an export that quietly renders at 1× is a\n * slightly softer file, and one that throws because the default was too\n * ambitious for a 4K project is no file at all.\n */\nfunction fittedSupersample(requested: number, output: { width: number; height: number }): number {\n const longest = Math.max(output.width, output.height);\n let factor = Math.max(1, Math.round(requested));\n while (factor > 1 && longest * factor > MAX_RENDER_AXIS) factor--;\n return factor;\n}\n\n/**\n * The codec to actually encode with: the one asked for if the container holds\n * it and this platform can encode it at this size, else the format's default,\n * else its fallback (`'avc'` for MP4, `'vp8'` for WebM).\n *\n * Which encoders a machine has varies by OS, browser and GPU, and an export that\n * dies at the muxer for want of an HEVC encoder is worse than one that quietly\n * produces H.264. The fallback is not probed: there is nothing to fall back to.\n */\nasync function resolveCodec(\n requested: VideoCodec | undefined,\n format: VideoFormat,\n size: { width: number; height: number },\n fps: number,\n): Promise<VideoCodec> {\n const { codecs, fallback } = formatSpec(format).video!;\n let wanted = requested ?? codecs[0];\n if (!codecs.includes(wanted)) {\n console.warn(`[motionscript] ${format.toUpperCase()} cannot hold ${wanted}; exporting as ${CODEC_NAMES[codecs[0]]} instead.`);\n wanted = codecs[0];\n }\n for (const codec of new Set([wanted, codecs[0], fallback])) {\n if (codec === fallback || await canEncode(codec, size, fps)) {\n if (codec !== wanted) {\n console.warn(`[motionscript] No ${wanted} encoder available; exporting as ${CODEC_NAMES[codec]} instead.`);\n }\n return codec;\n }\n }\n return fallback;\n}\n\nconst CODEC_NAMES: Record<VideoCodec, string> = { avc: 'H.264', hevc: 'HEVC', vp9: 'VP9', av1: 'AV1', vp8: 'VP8' };\n\nasync function canEncode(codec: VideoCodec, size: { width: number; height: number }, fps: number): Promise<boolean> {\n try {\n return await canEncodeVideo(codec, {\n width: size.width,\n height: size.height,\n bitrate: motionGraphicsBitrate(size.width, size.height, fps),\n });\n } catch {\n // A probe that throws is a probe that answered \"no\".\n return false;\n }\n}\n\n/**\n * Every encoder setting, decided. A split export pins one of these across its\n * segments, since `concatVideoSegments` refuses parts encoded differently. With\n * a numeric `bitrate` (see {@link plainVideoEncoding}) it is plain data and\n * crosses a process boundary.\n */\nexport interface ResolvedVideoEncoding {\n codec: VideoCodec;\n /** A `Quality` is left for the encoder to size, as an export always has. */\n bitrate: number | Quality;\n hardwareAcceleration: NonNullable<VideoEncoderOptions['hardwareAcceleration']>;\n keyFrameInterval: number;\n latencyMode: NonNullable<VideoEncoderOptions['latencyMode']>;\n contentHint: string;\n bitrateMode?: VideoEncoderOptions['bitrateMode'];\n}\n\n/** {@link VideoEncoderOptions} with its defaults applied against the encoded size and rate. */\nexport async function resolveVideoEncoding(\n video: VideoEncoderOptions | undefined,\n format: VideoFormat,\n output: { width: number; height: number },\n fps: number,\n): Promise<ResolvedVideoEncoding> {\n const codec = await resolveCodec(video?.codec, format, output, fps);\n return {\n codec,\n bitrate: video?.bitrate ?? motionGraphicsBitrate(output.width, output.height, fps),\n hardwareAcceleration: video?.hardwareAcceleration ?? 'no-preference',\n keyFrameInterval: video?.keyFrameInterval ?? DEFAULT_KEY_FRAME_INTERVAL,\n latencyMode: video?.latencyMode ?? 'quality',\n contentHint: video?.contentHint ?? 'detail',\n ...(video?.bitrateMode !== undefined && { bitrateMode: video.bitrateMode }),\n };\n}\n\n/**\n * The encoding with a `Quality` turned into the bits per second mediabunny would\n * give it at this size, since a class instance cannot cross a `MessagePort`.\n */\nexport function plainVideoEncoding(\n encoding: ResolvedVideoEncoding,\n output: { width: number; height: number },\n): ResolvedVideoEncoding & { bitrate: number } {\n const { bitrate } = encoding;\n if (typeof bitrate === 'number') return { ...encoding, bitrate };\n const quality = bitrate as unknown as { _toVideoBitrate(c: VideoCodec, w: number, h: number): number };\n return { ...encoding, bitrate: quality._toVideoBitrate(encoding.codec, output.width, output.height) };\n}\n\n/** The encoded size, rounded up to even, and how far each frame is supersampled. */\nexport function exportGeometry(viewport: Size2D, scale: number, supersample: number) {\n // Every block-based codec works in macroblocks, so an odd dimension is\n // padded by the encoder with a smeared strip that was never drawn.\n const output = { width: evenUp(viewport.width * scale), height: evenUp(viewport.height * scale) };\n const oversample = fittedSupersample(supersample, output);\n return { output, oversample, renderScale: scale * oversample };\n}\n\n/** A hidden canvas with a mounted render context, reusable across renders of one project. */\nexport interface ExportSurface {\n canvas: HTMLCanvasElement;\n renderContext: BrowserRenderContext;\n storageAdapter: StorageAdapter;\n assetCatalog: ManifestAssetCatalog;\n dispose(): void;\n}\n\n/**\n * Open the canvas, renderer, storage and render context an export draws with,\n * resolving once the surface can draw (a frame drawn before an asynchronously\n * started surface is ready encodes blank). The canvas is a whole multiple of the\n * output, so the downsample is exact.\n */\nexport async function openExportSurface(params: {\n canvasSize: { width: number; height: number };\n viewport: Size2D;\n fps: number;\n manifest: AssetManifest;\n} & RendererWasmOption): Promise<ExportSurface> {\n const canvas = document.createElement('canvas');\n canvas.width = params.canvasSize.width;\n canvas.height = params.canvasSize.height;\n canvas.style.display = 'none';\n document.body.appendChild(canvas);\n try {\n const assetCatalog = new ManifestAssetCatalog(params.manifest);\n const renderer = await loadRenderer(params);\n const storageAdapter = renderer.createStorageAdapter({ catalog: assetCatalog, viewport: params.viewport, fps: params.fps });\n const renderContext = renderer.createRenderContext(storageAdapter);\n renderContext.mount(canvas);\n await renderContext.whenMounted?.();\n return {\n canvas, renderContext, storageAdapter, assetCatalog,\n dispose: () => {\n renderContext.dispose();\n canvas.remove();\n },\n };\n } catch (error) {\n canvas.remove();\n throw error;\n }\n}\n\n/**\n * Renders a document frame-by-frame to an offscreen canvas and muxes the result\n * into an MP4 or WebM via mediabunny.\n *\n * The frame loop itself — discovery, state evaluation, the warm-and-re-render retry\n * that keeps every frame accurate, audio scheduling, progress and cancellation —\n * lives in `renderTimeline` (frame-loop.ts). What this function\n * supplies is the browser-specific half: a canvas to draw into, a WebCodecs sink,\n * a Web Audio mixer, and the `<a download>` delivery.\n */\nexport async function exportVideo(params: ExportParams): Promise<Uint8Array | void> {\n const {\n // Not `document`, which would shadow the DOM global this function uses.\n document: source,\n registry = new Registry(),\n scale = 1,\n format = 'mp4',\n manifest = EMPTY_MANIFEST,\n theme,\n includeAudio = true,\n video,\n audio = {},\n frames,\n onProgress,\n signal,\n rendererWasmUrl,\n supersample = DEFAULT_SUPERSAMPLE,\n returnBytes = false,\n } = params;\n\n const spec = formatSpec(format);\n if (spec.kind !== 'video') throw new RangeError(`exportVideo writes video; \"${format}\" is audio, which exportAudio writes.`);\n const filename = params.filename ?? `export.${spec.extension}`;\n const project = resolveProjectDocument(source, params, DEFAULT_OUTPUT);\n if (project.tracks.length === 0) return;\n signal?.throwIfAborted();\n const viewport = project.viewport!;\n const fps = project.fps!;\n\n const { output, oversample, renderScale } = exportGeometry(viewport, scale, supersample);\n const surface = await openExportSurface({\n canvasSize: { width: output.width * oversample, height: output.height * oversample },\n viewport, fps, manifest, rendererWasmUrl,\n });\n\n let bytes: Uint8Array | undefined;\n try {\n const encoding = await resolveVideoEncoding(video, format, output, fps);\n bytes = await renderTimeline<AudioBuffer>({\n project, registry, theme, scale: renderScale, includeAudio, frames,\n renderContext: surface.renderContext, storageAdapter: surface.storageAdapter, assetCatalog: surface.assetCatalog,\n sink: new MediabunnyVideoSink(surface.canvas, output, format, encoding, audio),\n mixer: new WebAudioMixer(audio.sampleRate, surface.assetCatalog),\n onProgress,\n onIssue: params.onIssue,\n signal,\n }) as Uint8Array | undefined;\n } finally {\n surface.dispose();\n }\n\n if (!bytes) return;\n return deliver(bytes, { returnBytes, filename, mimeType: spec.mimeType });\n}\n"]}
|
package/dist/frame-loop.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"frame-loop.js","sourceRoot":"","sources":["../src/frame-loop.ts"],"names":[],"mappings":"AAAA,OAAO,EACW,YAAY,EAAE,SAAS,EAAkE,SAAS,EAChH,aAAa,EACb,kBAAkB,EAAE,kBAAkB,EAAoB,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,QAAQ,EAAE,aAAa,EAClG,aAAa,EAAE,cAAc,EAAmC,gBAAgB,GAChH,MAAM,oBAAoB,CAAC;AA4E5B;;;;GAIG;AACH,MAAM,OAAO,aAAc,SAAQ,SAAS;IACxC,iFAAiF;IACjF,IAAa,cAAc,KAAc,OAAO,KAAK,CAAC,CAAC,CAAC;IAExD,GAAG,CAAC,IAAY,IAAa,OAAO,IAAI,CAAC,CAAC,CAAC;IAC3C,KAAK,CAAC,MAAM,CAAC,IAAY,EAAE,KAAkB,IAAmB,CAAC;IACjE,MAAM,CAAC,KAA0B,IAAU,CAAC;IAC5C,QAAQ,CAAC,SAAkC,IAAU,CAAC;IACtD,MAAM,CAAC,SAAiB,IAAU,CAAC;IACnC,IAAI,CAAC,KAAa,EAAE,MAAc,EAAE,QAAiB,IAAU,CAAC;IAChE,IAAI,KAAW,CAAC;CACnB;AA+BD;;;GAGG;AACH,SAAS,WAAW,CAAC,aAAoC,EAAE,IAAiB,EAAE,OAAsC;IAChH,KAAK,MAAM,GAAG,IAAI,aAAa,CAAC,gBAAgB,EAAE,EAAE,CAAC;QACjD,MAAM,KAAK,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,kBAAkB,CAAC,KAAK,CAAC;YAAE,MAAM,IAAI,kBAAkB,CAAC,KAAK,CAAC,CAAC;QACnE,MAAM,GAAG,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;QAClC,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,SAAS;QAC5B,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACd,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;AACL,CAAC;AAED,2FAA2F;AAC3F,KAAK,UAAU,eAAe,CAC1B,EAAE,aAAa,EAAE,cAAc,EAAiB,EAChD,IAAgB,EAChB,KAAa;IAEb,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,GAAG,EAAE,CAAC,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,cAAc,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;IACnG,IAAI,OAAO,KAAK,WAAW,EAAE,CAAC;QAC1B,OAAO,CAAC,IAAI,CAAC,kBAAkB,KAAK,oCAAoC,aAAa,gCAAgC,CAAC,CAAC;IAC3H,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,SAAS,YAAY,CAAC,cAA8B;IAChD,MAAM,QAAQ,GAAG,cAAc,CAAC,eAAe,EAAE,CAAC;IAClD,cAAc,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;IACxC,OAAO,GAAG,EAAE,CAAC,cAAc,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;AAC1D,CAAC;AAED,SAAS,QAAQ,CAAC,OAAwB;IACtC,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC;IAClC,IAAI,GAAG,KAAK,SAAS,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC9C,MAAM,IAAI,KAAK,CAAC,8EAA8E,CAAC,CAAC;IACpG,CAAC;IACD,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC;AAC7B,CAAC;AAED,oHAAoH;AACpH,SAAS,YAAY,CAAC,MAA8B,EAAE,WAAmB;IACrE,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,WAAW,EAAE,CAAC;IACnD,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,MAAM,CAAC;IAC9B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI,GAAG,IAAI,KAAK,IAAI,GAAG,GAAG,WAAW,EAAE,CAAC;QACvG,MAAM,IAAI,UAAU,CAAC,UAAU,KAAK,OAAO,GAAG,2CAA2C,WAAW,UAAU,CAAC,CAAC;IACpH,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;AAC1B,CAAC;AAED,8GAA8G;AAC9G,SAAS,OAAO,CAAC,MAAoB;IACjC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,aAAa,EAAE,GAAG,MAAM,CAAC;IAClE,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC5C,yEAAyE;IACzE,2EAA2E;IAC3E,MAAM,UAAU,GAAG,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC;IACjE,UAAU,EAAE,CAAC;IACb,mEAAmE;IACnE,4EAA4E;IAC5E,6EAA6E;IAC7E,yBAAyB;IACzB,MAAM,QAAQ,GAAqB,aAAa,CAAC;IACjD,MAAM,KAAK,GAAG,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC;IAC1E,MAAM,KAAK,GAA+B,EAAE,CAAC;IAC7C,MAAM,KAAK,GAAG,IAAI,SAAS,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC;IACxG,MAAM,SAAS,GAAG,IAAI,aAAa,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IAC/D,MAAM,cAAc,GAAG,IAAI,cAAc,CAAC,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;IACjE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,QAAQ,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC;AAC5F,CAAC;AAED;;;;GAIG;AACH,KAAK,UAAU,iBAAiB,CAAC,KAAgB,EAAE,OAAqB,EAAE,QAA2B;IACjG,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC/G,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IACvD,KAAK,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;AAChC,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAAC,OAA4B;IACrD,MAAM,GAAG,GAA4B,EAAE,CAAC;IACxC,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;QACjC,MAAM,YAAY,GAAG,KAAK,CAAC,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC;QACpD,KAAK,MAAM,OAAO,IAAI,KAAK,CAAC,aAAa;YAAE,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC,CAAC;IACnF,CAAC;IACD,OAAO,GAAG,CAAC;AACf,CAAC;AAuBD;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAChC,MAAoC;IAEpC,MAAM,EAAE,KAAK,GAAG,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,EAAE,GAAG,MAAM,CAAC;IACnF,MAAM,OAAO,GAAkB,MAAM,CAAC;IACtC,MAAM,EAAE,aAAa,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC;IAElD,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAC/C,MAAM,EAAE,cAAc,EAAE,CAAC;IAEzB,aAAa,CAAC,UAAU,GAAG,KAAK,CAAC;IACjC,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,QAAQ,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACzG,MAAM,aAAa,GAAG,YAAY,CAAC,cAAc,CAAC,CAAC;IACnD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,IAAI,CAAC;QACD,yFAAyF;QACzF,aAAa,CAAC,gBAAgB,EAAE,CAAC;QACjC,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,YAAY,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,CAAC,WAAW,CAAC,CAAC;QACnF,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM;YAC1B,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,GAAG,GAAG,IAAI,IAAI,CAAC,QAAQ,IAAI,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YACtG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACrD,IAAI,MAAM,CAAC,MAAM;YAAE,KAAK,MAAM,EAAE,IAAI,QAAQ;gBAAE,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;QACjE,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC;QACrE,MAAM,EAAE,aAAa,EAAE,GAAG,OAAO,CAAC;QAClC,MAAM,UAAU,GAAG,GAAG,GAAG,KAAK,CAAC;QAC/B,+DAA+D;QAC/D,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC;QAC/C,MAAM,YAAY,GAAG,KAAK,CAAC,OAAO,GAAG,IAAI,YAAY,CAAC,OAAO,EAAE,cAAc,EAAE,IAAI,aAAa,EAAE,CAAC,CAAC;QACpG,MAAM,iBAAiB,CAAC,KAAK,EAAE,YAAY,EAAE,QAAQ,CAAC,CAAC;QACvD,MAAM,SAAS,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;QACxC,MAAM,UAAU,GAAG,YAAY,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,KAAK,SAAS,CAAC;QAEjG,MAAM,IAAI,CAAC,KAAK,CAAC;YACb,KAAK,EAAE,QAAQ,CAAC,KAAK,GAAG,KAAK;YAC7B,MAAM,EAAE,QAAQ,CAAC,MAAM,GAAG,KAAK;YAC/B,GAAG;YACH,WAAW,EAAE,UAAU;YACvB,QAAQ,EAAE,UAAU;SACvB,CAAC,CAAC;QAEH,MAAM,aAAa,GAAG,CAAC,GAAG,GAAG,CAAC;QAC9B,IAAI,UAAU,GAAG,CAAC,CAAC;QACnB,6EAA6E;QAC7E,2EAA2E;QAC3E,4EAA4E;QAC5E,4EAA4E;QAC5E,8EAA8E;QAC9E,6EAA6E;QAC7E,mCAAmC;QACnC,EAAE;QACF,+EAA+E;QAC/E,qDAAqD;QACrD,cAAc,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QAChC,IAAI,CAAC;YACD,KAAK,IAAI,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;gBAC/B,MAAM,EAAE,cAAc,EAAE,CAAC;gBAEzB,MAAM,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;gBAC7B,UAAU,EAAE,CAAC;gBACb,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;gBAC1B,cAAc,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;gBAChC,MAAM,eAAe,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,cAAc,CAAC,MAAM,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC,CAAC;gBAC9E,WAAW,CAAC,aAAa,EAAE,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;gBAEjD,MAAM,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;gBAC/C,UAAU,IAAI,aAAa,CAAC;gBAC5B,MAAM,IAAI,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC;gBAC3B,6EAA6E;gBAC7E,UAAU,EAAE,CAAC,CAAC,IAAI,GAAG,UAAU,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBAE5D,wEAAwE;gBACxE,qEAAqE;gBACrE,yEAAyE;gBACzE,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC;oBAAE,MAAM,gBAAgB,EAAE,CAAC;YACpD,CAAC;QACL,CAAC;gBAAS,CAAC;YACP,cAAc,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QACrC,CAAC;QAED,IAAI,UAAU,EAAE,CAAC;YACb,MAAM,KAAK,GAAG,MAAM,KAAM,CAAC,GAAG,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;YACzD,IAAI,KAAK,KAAK,IAAI,IAAI,IAAI,CAAC,QAAQ;gBAAE,MAAM,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;YAChE,UAAU,EAAE,CAAC,IAAI,CAAC,CAAC;QACvB,CAAC;QAED,iEAAiE;QACjE,WAAW,CAAC,aAAa,EAAE,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;QACjD,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;QACpC,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC;QAChB,OAAO,KAAK,CAAC;IACjB,CAAC;YAAS,CAAC;QACP,aAAa,EAAE,CAAC;QAChB,cAAc,CAAC,OAAO,EAAE,CAAC;QACzB,KAAK,CAAC,OAAO,EAAE,CAAC;IACpB,CAAC;AACL,CAAC;AAaD;;;;GAIG;AACH,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAI5B;IAEA;IALK,IAAI,GAAG,oBAAoB,CAAC;IAE9C,YACa,KAAa;IACtB,wCAAwC;IAC/B,QAA+B;QAExC,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,KAAK,OAAO,CAAC,OAAO,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAChG,KAAK,CAAC,SAAS,KAAK,8DAA8D,MAAM,EAAE,CAAC,CAAC;QALnF,UAAK,GAAL,KAAK,CAAQ;QAEb,aAAQ,GAAR,QAAQ,CAAuB;IAI5C,CAAC;CACJ;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,MAAuB;IACrD,MAAM,EAAE,KAAK,GAAG,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC;IAC7C,MAAM,OAAO,GAAkB,MAAM,CAAC;IACtC,MAAM,EAAE,aAAa,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC;IAElD,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAEzF,aAAa,CAAC,UAAU,GAAG,KAAK,CAAC;IACjC,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9E,MAAM,aAAa,GAAG,YAAY,CAAC,cAAc,CAAC,CAAC;IACnD,IAAI,CAAC;QACD,MAAM,EAAE,WAAW,EAAE,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC;QAC3C,MAAM,SAAS,GACX,KAAK,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;YACtB,CAAC,CAAC,KAAK,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,WAAW,GAAG,CAAC;gBACrC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC;QAC1B,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,WAAW,GAAG,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;QAClF,MAAM,OAAO,GAAG,SAAS,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC;QACtD,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC;QAE5C,MAAM,YAAY,GAAG,KAAK,CAAC,OAAO,GAAG,IAAI,YAAY,CAAC,OAAO,EAAE,cAAc,EAAE,IAAI,aAAa,EAAE,CAAC,CAAC;QACpG,MAAM,iBAAiB,CAAC,KAAK,EAAE,YAAY,EAAE,aAAa,CAAC,KAAK,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;QAC7G,MAAM,YAAY,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;QACvC,cAAc,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QACpC,cAAc,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAChC,aAAa,CAAC,gBAAgB,EAAE,CAAC;QACjC,MAAM,eAAe,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,cAAc,CAAC,MAAM,CAAC,aAAa,CAAC,EAAE,WAAW,CAAC,CAAC;QACxF,WAAW,CAAC,aAAa,EAAE,IAAI,GAAG,EAAE,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;QAEtD,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,CAAC;IAC/C,CAAC;YAAS,CAAC;QACP,aAAa,EAAE,CAAC;QAChB,2EAA2E;QAC3E,2EAA2E;QAC3E,sBAAsB;QACtB,cAAc,CAAC,OAAO,EAAE,CAAC;QACzB,KAAK,CAAC,OAAO,EAAE,CAAC;IACpB,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,SAAS,cAAc,CAAC,KAAgB,EAAE,OAA4B,EAAE,KAAa,EAAE,IAAI,GAAG,KAAK;IAC/F,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,WAAW;SACrC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;SAC9C,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAC7C,IAAI,MAAM,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO;IAC9B,KAAK,IAAI,KAAK,GAAG,KAAK,EAAE,KAAK,IAAI,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QAC7C,MAAM,QAAQ,GAAG,aAAa,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QACnD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;YACvE,MAAM,IAAI,kBAAkB,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAE,CAAC,CAAC,CAAC;QACtF,CAAC;IACL,CAAC;AACL,CAAC","sourcesContent":["import {\r\n AssetCatalog, AssetManager, AudioSink, type AudioRequest, type BuildError, type CanvasRenderContext2D, ClipStore, MeasureContext2D,\r\n AssetTimeline, type AssetTimelineResult, type FrameRange, type ProjectDocument, type Registry,\r\n isFatalRenderIssue, RenderBackendError, type RenderIssue, renderIssueKey, renderSettled, SETTLE_PASSES, setTheme, toRenderIssue,\r\n type FrameSpec, type Size2D, spansCovering, StateEvaluator, type StorageAdapter, type Theme, yieldToScheduler,\r\n} from \"@motionscript/core\";\r\n\r\n/**\r\n * Rendering a timeline is portable; *encoding* it is not.\r\n *\r\n * Everything about driving the frames — discovery, state evaluation, asset\r\n * windows, the bounded warm-and-re-render retry that makes a frame accurate,\r\n * audio scheduling, progress, cancellation — is the same work whatever the\r\n * output format. Only the codec differs, so it is injected as a `VideoFrameSink`\r\n * (exporter.ts: mediabunny over WebCodecs).\r\n */\r\n\r\n/** Image formats a still can be encoded to. */\r\nexport type ScreenshotFormat = \"png\" | \"jpeg\";\r\n\r\nexport type { FrameSpec };\r\n\r\n/** One scheduled audio request, with the absolute time its track starts at. */\r\nexport interface ScheduledAudioRequest {\r\n request: AudioRequest;\r\n /**\r\n * Seconds to add to the request's own times, which are clip-relative: its\r\n * track's start. Getting it wrong produces an export that looks right and\r\n * drifts out of sync, which no pixel comparison catches.\r\n */\r\n globalOffset: number;\r\n}\r\n\r\n/**\r\n * Where finished frames go.\r\n *\r\n * `TAudio` ties the sink to its {@link AudioMixer}: what a mixer produces is\r\n * platform-specific (a Web Audio `AudioBuffer`, interleaved PCM, a temp file) and\r\n * only its own sink consumes it, so this module relays it without naming it.\r\n */\r\nexport interface VideoFrameSink<TAudio = unknown> {\r\n /**\r\n * Called once before the first frame, with the resolved output geometry.\r\n *\r\n * `hasAudio` is here rather than left for the sink to infer because a muxer\r\n * generally has to declare its tracks before writing any, and whether the\r\n * timeline schedules any audio is only known after discovery — which happens\r\n * inside the orchestrator, after the sink was constructed.\r\n */\r\n start(info: {\r\n width: number;\r\n height: number;\r\n fps: number;\r\n totalFrames: number;\r\n hasAudio: boolean;\r\n }): Promise<void>;\r\n\r\n /**\r\n * One frame has been drawn and the surface flushed. Times are in seconds.\r\n *\r\n * **Deliberately does not receive pixels.** A browser sink wraps the canvas\r\n * the renderer already drew into and lets WebCodecs pull from it directly, with no\r\n * readback; handing a buffer here instead would force a full GPU→CPU\r\n * `readPixels` on every frame of every export. A sink that *does* need bytes\r\n * (ffmpeg over stdin) reads them itself, because it holds the render target it\r\n * was constructed with. Do not \"simplify\" this signature.\r\n */\r\n addFrame(timestamp: number, duration: number): Promise<void>;\r\n\r\n /** Mixed audio for the whole timeline, if any was produced. */\r\n addAudio?(mixed: TAudio): Promise<void>;\r\n\r\n /** Finish the container. Returns encoded bytes, or nothing if the sink delivered them itself. */\r\n finalize(): Promise<Uint8Array | void>;\r\n}\r\n\r\n/** Mixes every scheduled request into one timeline-length buffer. */\r\nexport interface AudioMixer<TAudio = unknown> {\r\n mix(requests: readonly ScheduledAudioRequest[], totalDuration: number): Promise<TAudio | null>;\r\n}\r\n\r\n/**\r\n * Audio is mixed offline at the end of a run (or irrelevant, for a still), so the\r\n * `AssetManager` just needs an `AudioSink`-shaped sink that does nothing during\r\n * the per-frame pass.\r\n */\r\nexport class NoopAudioSink extends AudioSink {\r\n /** Nothing here records a request, so `AssetManager` can skip the whole sync. */\r\n override get schedulesAudio(): boolean { return false; }\r\n\r\n has(_src: string): boolean { return true; }\r\n async append(_src: string, _data: ArrayBuffer): Promise<void> { }\r\n retain(_keep: ReadonlySet<string>): void { }\r\n schedule(_requests: readonly AudioRequest[]): void { }\r\n syncTo(_clipTime: number): void { }\r\n play(_time: number, _speed: number, _reverse: boolean): void { }\r\n stop(): void { }\r\n}\r\n\r\n/** The platform pieces both entry points need. */\r\n/**\r\n * What the frame loop draws with. Any backend's context and adapter: the loop\r\n * uses only core's base members, so another renderer exports through it too.\r\n */\r\ninterface RenderHarness {\r\n renderContext: CanvasRenderContext2D & { pixelRatio: number };\r\n /** The same adapter the context was built with. */\r\n storageAdapter: StorageAdapter;\r\n assetCatalog: AssetCatalog;\r\n}\r\n\r\n/** Fields shared by the video and still entry points. */\r\ninterface CommonParams extends RenderHarness {\r\n /** Its fps and viewport resolved — see `Session`. */\r\n project: ProjectDocument;\r\n /** The vocabulary its clips compile against. */\r\n registry: Registry;\r\n /** Theme for a project that carries none of its own. */\r\n theme?: Theme;\r\n /**\r\n * What the backend drew but not as the document says (a feature it lacks,\r\n * a shader that does not compile, a GPU error), once per kind, feature and\r\n * node. A lost device or a crashed backend is not reported here: the render\r\n * throws a {@link RenderBackendError} instead.\r\n */\r\n onIssue?: (issue: RenderIssue) => void;\r\n}\r\n\r\n/**\r\n * Hand the context's issues since the last call to `onIssue`, once per key in\r\n * `seen`, and throw on one that means the frames drawn are not to be trusted.\r\n */\r\nfunction drainIssues(renderContext: CanvasRenderContext2D, seen: Set<string>, onIssue?: (issue: RenderIssue) => void): void {\r\n for (const raw of renderContext.takeRenderIssues()) {\r\n const issue = toRenderIssue(raw);\r\n if (isFatalRenderIssue(issue)) throw new RenderBackendError(issue);\r\n const key = renderIssueKey(issue);\r\n if (seen.has(key)) continue;\r\n seen.add(key);\r\n onIssue?.(issue);\r\n }\r\n}\r\n\r\n/** Draw one frame, settling and re-rendering until it is accurate. See `renderSettled`. */\r\nasync function renderWarmFrame(\r\n { renderContext, storageAdapter }: RenderHarness,\r\n draw: () => void,\r\n frame: number,\r\n): Promise<void> {\r\n const outcome = await renderSettled(() => renderContext.execute(draw), storageAdapter, () => true);\r\n if (outcome === \"exhausted\") {\r\n console.warn(`[export] frame ${frame} still had pending decodes after ${SETTLE_PASSES} renders; it may not be exact.`);\r\n }\r\n}\r\n\r\n/**\r\n * Switch the adapter to decoding exactly what each frame draws, so a frame is\r\n * the same pixels whatever was rendered on this adapter before it. Returns the\r\n * restore: an adapter lent by a live preview must not come back pinned to it.\r\n */\r\nfunction exactDecodes(storageAdapter: StorageAdapter): () => void {\r\n const previous = storageAdapter.getDecodePolicy();\r\n storageAdapter.setDecodePolicy(\"exact\");\r\n return () => storageAdapter.setDecodePolicy(previous);\r\n}\r\n\r\nfunction outputOf(project: ProjectDocument): { fps: number; viewport: Size2D } {\r\n const { fps, viewport } = project;\r\n if (fps === undefined || viewport === undefined) {\r\n throw new Error(\"Rendering takes a project whose fps and viewport are resolved — see Session.\");\r\n }\r\n return { fps, viewport };\r\n}\r\n\r\n/** `frames`, or the whole timeline; a range that is empty or leaves the timeline is refused rather than clamped. */\r\nfunction checkedRange(frames: FrameRange | undefined, totalFrames: number): FrameRange {\r\n if (!frames) return { start: 0, end: totalFrames };\r\n const { start, end } = frames;\r\n if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end <= start || end > totalFrames) {\r\n throw new RangeError(`Frames ${start} to ${end} are not a range inside this timeline's ${totalFrames} frames.`);\r\n }\r\n return { start, end };\r\n}\r\n\r\n/** Build the discovery + evaluator pair both entry points drive, over one store so each track builds once. */\r\nfunction prepare(params: CommonParams) {\r\n const { project, registry, assetCatalog, renderContext } = params;\r\n const { fps, viewport } = outputOf(project);\r\n // The theme is process-global, so anything else drawing in this page may\r\n // have replaced it; applied before the first build, which resolves tokens.\r\n const applyTheme = () => setTheme(project.theme ?? params.theme);\r\n applyTheme();\r\n // The render context *is* the measurer — `RenderContext2D` extends\r\n // `MeasureContext2D` so a node measures and draws through one object. Named\r\n // rather than cast so the compiler checks it; this package has no test suite\r\n // to catch it otherwise.\r\n const measurer: MeasureContext2D = renderContext;\r\n const scope = { registry, viewport, fps, assets: assetCatalog, measurer };\r\n const loads: { manager?: AssetManager } = {};\r\n const store = new ClipStore(project, scope, { isReady: (key) => loads.manager?.isReady(key) ?? false });\r\n const precomper = new AssetTimeline(project, scope, { store });\r\n const stateEvaluator = new StateEvaluator(project, scope, store);\r\n return { store, precomper, stateEvaluator, measurer, fps, viewport, loads, applyTheme };\r\n}\r\n\r\n/**\r\n * Load the layout gates of `trackIds`, then rebuild any of them whose compile\r\n * measured a face before it was resident — every build here runs before a\r\n * single asset has loaded.\r\n */\r\nasync function settleLayoutGates(store: ClipStore, manager: AssetManager, trackIds: readonly string[]): Promise<void> {\r\n const gates = trackIds.flatMap((id) => store.layoutGates(id).filter((record) => !manager.isReady(record.src)));\r\n if (gates.length > 0) await manager.loadRecords(gates);\r\n store.settleGates(trackIds);\r\n}\r\n\r\n/**\r\n * Collect every audio request with the absolute offset it plays at. See\r\n * {@link ScheduledAudioRequest.globalOffset}.\r\n *\r\n * Exported because a **split** export has to mix its audio somewhere other than\r\n * alongside its frames: the video is rendered in pieces by separate workers, and\r\n * concatenating separately-encoded AAC would put the encoder's priming delay at\r\n * every join. The coordinator mixes once over the whole timeline instead — and\r\n * must schedule it exactly as this does. A second implementation that drifted by\r\n * a track offset would produce a file that plays perfectly and is out of sync.\r\n */\r\nexport function collectAudio(precomp: AssetTimelineResult): ScheduledAudioRequest[] {\r\n const out: ScheduledAudioRequest[] = [];\r\n for (const track of precomp.tracks) {\r\n const globalOffset = track.startFrame / precomp.fps;\r\n for (const request of track.audioRequests) out.push({ request, globalOffset });\r\n }\r\n return out;\r\n}\r\n\r\nexport interface RenderTimelineParams<TAudio> extends CommonParams {\r\n scale?: number;\r\n sink: VideoFrameSink<TAudio>;\r\n mixer?: AudioMixer<TAudio>;\r\n /**\r\n * Set false to render a silent file: no mix, and no audio track declared on\r\n * the container at all.\r\n *\r\n * Distinct from muting. The audio is still discovered — the pass that finds\r\n * it is the one that finds the frames' assets — it simply is not encoded.\r\n */\r\n includeAudio?: boolean;\r\n /**\r\n * Render only these frames, timestamped from 0, with no audio: one segment of a\r\n * split export. Only the tracks they cover are discovered.\r\n */\r\n frames?: FrameRange;\r\n onProgress?: (progress: number) => void;\r\n signal?: AbortSignal;\r\n}\r\n\r\n/**\r\n * Drive every frame of `project` (or of `frames`) into `sink`, then mix audio and finalize.\r\n *\r\n * The caller owns the render context and its surface — this neither creates nor\r\n * disposes them, because how a surface comes to exist is exactly the thing that\r\n * differs between backends.\r\n */\r\nexport async function renderTimeline<TAudio>(\r\n params: RenderTimelineParams<TAudio>,\r\n): Promise<Uint8Array | void> {\r\n const { scale = 1, sink, mixer, onProgress, signal, includeAudio = true } = params;\r\n const harness: RenderHarness = params;\r\n const { renderContext, storageAdapter } = harness;\r\n\r\n if (params.project.tracks.length === 0) return;\r\n signal?.throwIfAborted();\r\n\r\n renderContext.pixelRatio = scale;\r\n const { store, precomper, stateEvaluator, measurer, fps, viewport, loads, applyTheme } = prepare(params);\r\n const restorePolicy = exactDecodes(storageAdapter);\r\n const seen = new Set<string>();\r\n try {\r\n // Not this render's: a device lost before it started was recovered from before it draws.\r\n renderContext.takeRenderIssues();\r\n const { start, end } = checkedRange(params.frames, precomper.result().totalFrames);\r\n const trackIds = params.frames\r\n ? store.spans.filter((span) => span.startFrame < end && span.endFrame >= start).map((span) => span.id)\r\n : params.project.tracks.map((track) => track.id);\r\n if (params.frames) for (const id of trackIds) store.discover(id);\r\n const precomp = params.frames ? precomper.result() : precomper.run();\r\n const { totalDuration } = precomp;\r\n const frameCount = end - start;\r\n // Before the sink starts, so a refused export encodes nothing.\r\n assertDrawable(store, precomp, start, end - 1);\r\n const assetManager = loads.manager = new AssetManager(precomp, storageAdapter, new NoopAudioSink());\r\n await settleLayoutGates(store, assetManager, trackIds);\r\n const scheduled = collectAudio(precomp);\r\n const wantsAudio = includeAudio && !params.frames && scheduled.length > 0 && mixer !== undefined;\r\n\r\n await sink.start({\r\n width: viewport.width * scale,\r\n height: viewport.height * scale,\r\n fps,\r\n totalFrames: frameCount,\r\n hasAudio: wantsAudio,\r\n });\r\n\r\n const frameDuration = 1 / fps;\r\n let globalTime = 0;\r\n // An export is the most perfectly monotonic forward walk of a timeline there\r\n // is — exactly the access pattern an adapter's look-ahead decode window is\r\n // built for. Left at the default (`false`, i.e. \"paused\") every video frame\r\n // instead takes the random-access branch: a seek + decode the render has to\r\n // wait on, followed by a *second* full render of the frame once it lands (see\r\n // `renderWarmFrame`). Telling the adapter we are playing turns that into one\r\n // sequential decode pass per clip.\r\n //\r\n // Restored in the `finally` so a cancelled export doesn't leave an adapter the\r\n // caller still owns believing a playhead is running.\r\n storageAdapter.setPlaying(true);\r\n try {\r\n for (let f = start; f < end; f++) {\r\n signal?.throwIfAborted();\r\n\r\n await assetManager.loadAt(f);\r\n applyTheme();\r\n stateEvaluator.stateAt(f);\r\n stateEvaluator.layout(measurer);\r\n await renderWarmFrame(harness, () => stateEvaluator.render(renderContext), f);\r\n drainIssues(renderContext, seen, params.onIssue);\r\n\r\n await sink.addFrame(globalTime, frameDuration);\r\n globalTime += frameDuration;\r\n const done = f - start + 1;\r\n // Video occupies most of the range; the audio mix and finalize are the tail.\r\n onProgress?.((done / frameCount) * (wantsAudio ? 0.85 : 1));\r\n\r\n // Hand the host back its event loop periodically so a long export stays\r\n // interruptible. Core's yield is feature-detected and prefers a real\r\n // macrotask over a clamped setTimeout, in the browser and in Node alike.\r\n if ((f + 1) % 4 === 0) await yieldToScheduler();\r\n }\r\n } finally {\r\n storageAdapter.setPlaying(false);\r\n }\r\n\r\n if (wantsAudio) {\r\n const mixed = await mixer!.mix(scheduled, totalDuration);\r\n if (mixed !== null && sink.addAudio) await sink.addAudio(mixed);\r\n onProgress?.(0.97);\r\n }\r\n\r\n // The last frame's device may have been lost after it was drawn.\r\n drainIssues(renderContext, seen, params.onIssue);\r\n const bytes = await sink.finalize();\r\n onProgress?.(1);\r\n return bytes;\r\n } finally {\r\n restorePolicy();\r\n stateEvaluator.dispose();\r\n store.dispose();\r\n }\r\n}\r\n\r\nexport interface DrawFrameParams extends CommonParams {\r\n scale?: number;\r\n frame: FrameSpec;\r\n}\r\n\r\nexport interface DrawnFrame {\r\n /** The frame actually drawn, after clamping the request into range. */\r\n frame: number;\r\n totalFrames: number;\r\n}\r\n\r\n/**\r\n * Every track covering {@link frame} failed to build. Drawn anyway, it would be\r\n * a blank frame — the consequence, with the cause hidden — so a still and an\r\n * export both refuse it.\r\n */\r\nexport class NothingToDrawError extends Error {\r\n override readonly name = \"NothingToDrawError\";\r\n\r\n constructor(\r\n readonly frame: number,\r\n /** One per track covering the frame. */\r\n readonly failures: readonly BuildError[],\r\n ) {\r\n const causes = failures.map((failure) => ` ${failure.trackId}: ${failure.message}`).join(\"\\n\");\r\n super(`Frame ${frame} has nothing to draw — every track on it failed to build:\\n${causes}`);\r\n }\r\n}\r\n\r\n/**\r\n * Draw one frame of `project` into the caller's render context and leave it on\r\n * the surface.\r\n *\r\n * Same discovery/evaluator/warm-retry path as {@link renderTimeline}, so a still\r\n * is the same render the video would produce at that frame — and only the tracks\r\n * on that frame are discovered, since a track's span is known without it.\r\n *\r\n * The drawing is shared while what happens to the frame varies: a still encodes\r\n * it, a live preview leaves it on a visible canvas. The context is the\r\n * caller's throughout — this neither creates, resizes nor disposes it — so a host\r\n * holding one surface can call this repeatedly and keep both the surface and the\r\n * storage adapter's decode cache warm.\r\n */\r\nexport async function drawFrameAt(params: DrawFrameParams): Promise<DrawnFrame> {\r\n const { scale = 1, frame, project } = params;\r\n const harness: RenderHarness = params;\r\n const { renderContext, storageAdapter } = harness;\r\n\r\n if (project.tracks.length === 0) throw new Error(\"The project has no tracks to render.\");\r\n\r\n renderContext.pixelRatio = scale;\r\n const { store, precomper, stateEvaluator, measurer, loads } = prepare(params);\r\n const restorePolicy = exactDecodes(storageAdapter);\r\n try {\r\n const { totalFrames } = precomper.result();\r\n const requested =\r\n frame.kind === \"first\" ? 0\r\n : frame.kind === \"last\" ? totalFrames - 1\r\n : frame.frame;\r\n const targetFrame = Math.max(0, Math.min(totalFrames - 1, Math.floor(requested)));\r\n const precomp = precomper.ensureCovering(targetFrame);\r\n assertDrawable(store, precomp, targetFrame);\r\n\r\n const assetManager = loads.manager = new AssetManager(precomp, storageAdapter, new NoopAudioSink());\r\n await settleLayoutGates(store, assetManager, spansCovering(store.spans, targetFrame).map((span) => span.id));\r\n await assetManager.loadAt(targetFrame);\r\n stateEvaluator.stateAt(targetFrame);\r\n stateEvaluator.layout(measurer);\r\n renderContext.takeRenderIssues();\r\n await renderWarmFrame(harness, () => stateEvaluator.render(renderContext), targetFrame);\r\n drainIssues(renderContext, new Set(), params.onIssue);\r\n\r\n return { frame: targetFrame, totalFrames };\r\n } finally {\r\n restorePolicy();\r\n // The pass flushed the surface before returning, so the pixels survive the\r\n // tree being torn down here. In `finally` so a throw mid-draw doesn't leak\r\n // a live scene graph.\r\n stateEvaluator.dispose();\r\n store.dispose();\r\n }\r\n}\r\n\r\n/**\r\n * Throw a {@link NothingToDrawError} for the first frame in `first..last` whose\r\n * every track failed to build. Only a build that threw counts: `buildErrors` also\r\n * carries a node's failed declaration, and a track with one still draws.\r\n */\r\nfunction assertDrawable(store: ClipStore, precomp: AssetTimelineResult, first: number, last = first): void {\r\n const failed = new Map(precomp.buildErrors\r\n .filter((error) => store.failed(error.trackId))\r\n .map((error) => [error.trackId, error]));\r\n if (failed.size === 0) return;\r\n for (let frame = first; frame <= last; frame++) {\r\n const covering = spansCovering(store.spans, frame);\r\n if (covering.length > 0 && covering.every((span) => failed.has(span.id))) {\r\n throw new NothingToDrawError(frame, covering.map((span) => failed.get(span.id)!));\r\n }\r\n }\r\n}\r\n"]}
|
|
1
|
+
{"version":3,"file":"frame-loop.js","sourceRoot":"","sources":["../src/frame-loop.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n AssetCatalog, AssetManager, AudioSink, type AudioRequest, type BuildError, type CanvasRenderContext2D, ClipStore, MeasureContext2D,\n AssetTimeline, type AssetTimelineResult, type FrameRange, type ProjectDocument, type Registry,\n isFatalRenderIssue, RenderBackendError, type RenderIssue, renderIssueKey, renderSettled, SETTLE_PASSES, setTheme, toRenderIssue,\n type FrameSpec, type Size2D, spansCovering, StateEvaluator, type StorageAdapter, type Theme, yieldToScheduler,\n} from \"@motionscript/core\";\n\n/**\n * Rendering a timeline is portable; *encoding* it is not.\n *\n * Everything about driving the frames — discovery, state evaluation, asset\n * windows, the bounded warm-and-re-render retry that makes a frame accurate,\n * audio scheduling, progress, cancellation — is the same work whatever the\n * output format. Only the codec differs, so it is injected as a `VideoFrameSink`\n * (exporter.ts: mediabunny over WebCodecs).\n */\n\n/** Image formats a still can be encoded to. */\nexport type ScreenshotFormat = \"png\" | \"jpeg\";\n\nexport type { FrameSpec };\n\n/** One scheduled audio request, with the absolute time its track starts at. */\nexport interface ScheduledAudioRequest {\n request: AudioRequest;\n /**\n * Seconds to add to the request's own times, which are clip-relative: its\n * track's start. Getting it wrong produces an export that looks right and\n * drifts out of sync, which no pixel comparison catches.\n */\n globalOffset: number;\n}\n\n/**\n * Where finished frames go.\n *\n * `TAudio` ties the sink to its {@link AudioMixer}: what a mixer produces is\n * platform-specific (a Web Audio `AudioBuffer`, interleaved PCM, a temp file) and\n * only its own sink consumes it, so this module relays it without naming it.\n */\nexport interface VideoFrameSink<TAudio = unknown> {\n /**\n * Called once before the first frame, with the resolved output geometry.\n *\n * `hasAudio` is here rather than left for the sink to infer because a muxer\n * generally has to declare its tracks before writing any, and whether the\n * timeline schedules any audio is only known after discovery — which happens\n * inside the orchestrator, after the sink was constructed.\n */\n start(info: {\n width: number;\n height: number;\n fps: number;\n totalFrames: number;\n hasAudio: boolean;\n }): Promise<void>;\n\n /**\n * One frame has been drawn and the surface flushed. Times are in seconds.\n *\n * **Deliberately does not receive pixels.** A browser sink wraps the canvas\n * the renderer already drew into and lets WebCodecs pull from it directly, with no\n * readback; handing a buffer here instead would force a full GPU→CPU\n * `readPixels` on every frame of every export. A sink that *does* need bytes\n * (ffmpeg over stdin) reads them itself, because it holds the render target it\n * was constructed with. Do not \"simplify\" this signature.\n */\n addFrame(timestamp: number, duration: number): Promise<void>;\n\n /** Mixed audio for the whole timeline, if any was produced. */\n addAudio?(mixed: TAudio): Promise<void>;\n\n /** Finish the container. Returns encoded bytes, or nothing if the sink delivered them itself. */\n finalize(): Promise<Uint8Array | void>;\n}\n\n/** Mixes every scheduled request into one timeline-length buffer. */\nexport interface AudioMixer<TAudio = unknown> {\n mix(requests: readonly ScheduledAudioRequest[], totalDuration: number): Promise<TAudio | null>;\n}\n\n/**\n * Audio is mixed offline at the end of a run (or irrelevant, for a still), so the\n * `AssetManager` just needs an `AudioSink`-shaped sink that does nothing during\n * the per-frame pass.\n */\nexport class NoopAudioSink extends AudioSink {\n /** Nothing here records a request, so `AssetManager` can skip the whole sync. */\n override get schedulesAudio(): boolean { return false; }\n\n has(_src: string): boolean { return true; }\n async append(_src: string, _data: ArrayBuffer): Promise<void> { }\n retain(_keep: ReadonlySet<string>): void { }\n schedule(_requests: readonly AudioRequest[]): void { }\n syncTo(_clipTime: number): void { }\n play(_time: number, _speed: number, _reverse: boolean): void { }\n stop(): void { }\n}\n\n/** The platform pieces both entry points need. */\n/**\n * What the frame loop draws with. Any backend's context and adapter: the loop\n * uses only core's base members, so another renderer exports through it too.\n */\ninterface RenderHarness {\n renderContext: CanvasRenderContext2D & { pixelRatio: number };\n /** The same adapter the context was built with. */\n storageAdapter: StorageAdapter;\n assetCatalog: AssetCatalog;\n}\n\n/** Fields shared by the video and still entry points. */\ninterface CommonParams extends RenderHarness {\n /** Its fps and viewport resolved — see `Session`. */\n project: ProjectDocument;\n /** The vocabulary its clips compile against. */\n registry: Registry;\n /** Theme for a project that carries none of its own. */\n theme?: Theme;\n /**\n * What the backend drew but not as the document says (a feature it lacks,\n * a shader that does not compile, a GPU error), once per kind, feature and\n * node. A lost device or a crashed backend is not reported here: the render\n * throws a {@link RenderBackendError} instead.\n */\n onIssue?: (issue: RenderIssue) => void;\n}\n\n/**\n * Hand the context's issues since the last call to `onIssue`, once per key in\n * `seen`, and throw on one that means the frames drawn are not to be trusted.\n */\nfunction drainIssues(renderContext: CanvasRenderContext2D, seen: Set<string>, onIssue?: (issue: RenderIssue) => void): void {\n for (const raw of renderContext.takeRenderIssues()) {\n const issue = toRenderIssue(raw);\n if (isFatalRenderIssue(issue)) throw new RenderBackendError(issue);\n const key = renderIssueKey(issue);\n if (seen.has(key)) continue;\n seen.add(key);\n onIssue?.(issue);\n }\n}\n\n/** Draw one frame, settling and re-rendering until it is accurate. See `renderSettled`. */\nasync function renderWarmFrame(\n { renderContext, storageAdapter }: RenderHarness,\n draw: () => void,\n frame: number,\n): Promise<void> {\n const outcome = await renderSettled(() => renderContext.execute(draw), storageAdapter, () => true);\n if (outcome === \"exhausted\") {\n console.warn(`[export] frame ${frame} still had pending decodes after ${SETTLE_PASSES} renders; it may not be exact.`);\n }\n}\n\n/**\n * Switch the adapter to decoding exactly what each frame draws, so a frame is\n * the same pixels whatever was rendered on this adapter before it. Returns the\n * restore: an adapter lent by a live preview must not come back pinned to it.\n */\nfunction exactDecodes(storageAdapter: StorageAdapter): () => void {\n const previous = storageAdapter.getDecodePolicy();\n storageAdapter.setDecodePolicy(\"exact\");\n return () => storageAdapter.setDecodePolicy(previous);\n}\n\nfunction outputOf(project: ProjectDocument): { fps: number; viewport: Size2D } {\n const { fps, viewport } = project;\n if (fps === undefined || viewport === undefined) {\n throw new Error(\"Rendering takes a project whose fps and viewport are resolved — see Session.\");\n }\n return { fps, viewport };\n}\n\n/** `frames`, or the whole timeline; a range that is empty or leaves the timeline is refused rather than clamped. */\nfunction checkedRange(frames: FrameRange | undefined, totalFrames: number): FrameRange {\n if (!frames) return { start: 0, end: totalFrames };\n const { start, end } = frames;\n if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end <= start || end > totalFrames) {\n throw new RangeError(`Frames ${start} to ${end} are not a range inside this timeline's ${totalFrames} frames.`);\n }\n return { start, end };\n}\n\n/** Build the discovery + evaluator pair both entry points drive, over one store so each track builds once. */\nfunction prepare(params: CommonParams) {\n const { project, registry, assetCatalog, renderContext } = params;\n const { fps, viewport } = outputOf(project);\n // The theme is process-global, so anything else drawing in this page may\n // have replaced it; applied before the first build, which resolves tokens.\n const applyTheme = () => setTheme(project.theme ?? params.theme);\n applyTheme();\n // The render context *is* the measurer — `RenderContext2D` extends\n // `MeasureContext2D` so a node measures and draws through one object. Named\n // rather than cast so the compiler checks it; this package has no test suite\n // to catch it otherwise.\n const measurer: MeasureContext2D = renderContext;\n const scope = { registry, viewport, fps, assets: assetCatalog, measurer };\n const loads: { manager?: AssetManager } = {};\n const store = new ClipStore(project, scope, { isReady: (key) => loads.manager?.isReady(key) ?? false });\n const precomper = new AssetTimeline(project, scope, { store });\n const stateEvaluator = new StateEvaluator(project, scope, store);\n return { store, precomper, stateEvaluator, measurer, fps, viewport, loads, applyTheme };\n}\n\n/**\n * Load the layout gates of `trackIds`, then rebuild any of them whose compile\n * measured a face before it was resident — every build here runs before a\n * single asset has loaded.\n */\nasync function settleLayoutGates(store: ClipStore, manager: AssetManager, trackIds: readonly string[]): Promise<void> {\n const gates = trackIds.flatMap((id) => store.layoutGates(id).filter((record) => !manager.isReady(record.src)));\n if (gates.length > 0) await manager.loadRecords(gates);\n store.settleGates(trackIds);\n}\n\n/**\n * Collect every audio request with the absolute offset it plays at. See\n * {@link ScheduledAudioRequest.globalOffset}.\n *\n * Exported because a **split** export has to mix its audio somewhere other than\n * alongside its frames: the video is rendered in pieces by separate workers, and\n * concatenating separately-encoded AAC would put the encoder's priming delay at\n * every join. The coordinator mixes once over the whole timeline instead — and\n * must schedule it exactly as this does. A second implementation that drifted by\n * a track offset would produce a file that plays perfectly and is out of sync.\n */\nexport function collectAudio(precomp: AssetTimelineResult): ScheduledAudioRequest[] {\n const out: ScheduledAudioRequest[] = [];\n for (const track of precomp.tracks) {\n const globalOffset = track.startFrame / precomp.fps;\n for (const request of track.audioRequests) out.push({ request, globalOffset });\n }\n return out;\n}\n\nexport interface RenderTimelineParams<TAudio> extends CommonParams {\n scale?: number;\n sink: VideoFrameSink<TAudio>;\n mixer?: AudioMixer<TAudio>;\n /**\n * Set false to render a silent file: no mix, and no audio track declared on\n * the container at all.\n *\n * Distinct from muting. The audio is still discovered — the pass that finds\n * it is the one that finds the frames' assets — it simply is not encoded.\n */\n includeAudio?: boolean;\n /**\n * Render only these frames, timestamped from 0, with no audio: one segment of a\n * split export. Only the tracks they cover are discovered.\n */\n frames?: FrameRange;\n onProgress?: (progress: number) => void;\n signal?: AbortSignal;\n}\n\n/**\n * Drive every frame of `project` (or of `frames`) into `sink`, then mix audio and finalize.\n *\n * The caller owns the render context and its surface — this neither creates nor\n * disposes them, because how a surface comes to exist is exactly the thing that\n * differs between backends.\n */\nexport async function renderTimeline<TAudio>(\n params: RenderTimelineParams<TAudio>,\n): Promise<Uint8Array | void> {\n const { scale = 1, sink, mixer, onProgress, signal, includeAudio = true } = params;\n const harness: RenderHarness = params;\n const { renderContext, storageAdapter } = harness;\n\n if (params.project.tracks.length === 0) return;\n signal?.throwIfAborted();\n\n renderContext.pixelRatio = scale;\n const { store, precomper, stateEvaluator, measurer, fps, viewport, loads, applyTheme } = prepare(params);\n const restorePolicy = exactDecodes(storageAdapter);\n const seen = new Set<string>();\n try {\n // Not this render's: a device lost before it started was recovered from before it draws.\n renderContext.takeRenderIssues();\n const { start, end } = checkedRange(params.frames, precomper.result().totalFrames);\n const trackIds = params.frames\n ? store.spans.filter((span) => span.startFrame < end && span.endFrame >= start).map((span) => span.id)\n : params.project.tracks.map((track) => track.id);\n if (params.frames) for (const id of trackIds) store.discover(id);\n const precomp = params.frames ? precomper.result() : precomper.run();\n const { totalDuration } = precomp;\n const frameCount = end - start;\n // Before the sink starts, so a refused export encodes nothing.\n assertDrawable(store, precomp, start, end - 1);\n const assetManager = loads.manager = new AssetManager(precomp, storageAdapter, new NoopAudioSink());\n await settleLayoutGates(store, assetManager, trackIds);\n const scheduled = collectAudio(precomp);\n const wantsAudio = includeAudio && !params.frames && scheduled.length > 0 && mixer !== undefined;\n\n await sink.start({\n width: viewport.width * scale,\n height: viewport.height * scale,\n fps,\n totalFrames: frameCount,\n hasAudio: wantsAudio,\n });\n\n const frameDuration = 1 / fps;\n let globalTime = 0;\n // An export is the most perfectly monotonic forward walk of a timeline there\n // is — exactly the access pattern an adapter's look-ahead decode window is\n // built for. Left at the default (`false`, i.e. \"paused\") every video frame\n // instead takes the random-access branch: a seek + decode the render has to\n // wait on, followed by a *second* full render of the frame once it lands (see\n // `renderWarmFrame`). Telling the adapter we are playing turns that into one\n // sequential decode pass per clip.\n //\n // Restored in the `finally` so a cancelled export doesn't leave an adapter the\n // caller still owns believing a playhead is running.\n storageAdapter.setPlaying(true);\n try {\n for (let f = start; f < end; f++) {\n signal?.throwIfAborted();\n\n await assetManager.loadAt(f);\n applyTheme();\n stateEvaluator.stateAt(f);\n stateEvaluator.layout(measurer);\n await renderWarmFrame(harness, () => stateEvaluator.render(renderContext), f);\n drainIssues(renderContext, seen, params.onIssue);\n\n await sink.addFrame(globalTime, frameDuration);\n globalTime += frameDuration;\n const done = f - start + 1;\n // Video occupies most of the range; the audio mix and finalize are the tail.\n onProgress?.((done / frameCount) * (wantsAudio ? 0.85 : 1));\n\n // Hand the host back its event loop periodically so a long export stays\n // interruptible. Core's yield is feature-detected and prefers a real\n // macrotask over a clamped setTimeout, in the browser and in Node alike.\n if ((f + 1) % 4 === 0) await yieldToScheduler();\n }\n } finally {\n storageAdapter.setPlaying(false);\n }\n\n if (wantsAudio) {\n const mixed = await mixer!.mix(scheduled, totalDuration);\n if (mixed !== null && sink.addAudio) await sink.addAudio(mixed);\n onProgress?.(0.97);\n }\n\n // The last frame's device may have been lost after it was drawn.\n drainIssues(renderContext, seen, params.onIssue);\n const bytes = await sink.finalize();\n onProgress?.(1);\n return bytes;\n } finally {\n restorePolicy();\n stateEvaluator.dispose();\n store.dispose();\n }\n}\n\nexport interface DrawFrameParams extends CommonParams {\n scale?: number;\n frame: FrameSpec;\n}\n\nexport interface DrawnFrame {\n /** The frame actually drawn, after clamping the request into range. */\n frame: number;\n totalFrames: number;\n}\n\n/**\n * Every track covering {@link frame} failed to build. Drawn anyway, it would be\n * a blank frame — the consequence, with the cause hidden — so a still and an\n * export both refuse it.\n */\nexport class NothingToDrawError extends Error {\n override readonly name = \"NothingToDrawError\";\n\n constructor(\n readonly frame: number,\n /** One per track covering the frame. */\n readonly failures: readonly BuildError[],\n ) {\n const causes = failures.map((failure) => ` ${failure.trackId}: ${failure.message}`).join(\"\\n\");\n super(`Frame ${frame} has nothing to draw — every track on it failed to build:\\n${causes}`);\n }\n}\n\n/**\n * Draw one frame of `project` into the caller's render context and leave it on\n * the surface.\n *\n * Same discovery/evaluator/warm-retry path as {@link renderTimeline}, so a still\n * is the same render the video would produce at that frame — and only the tracks\n * on that frame are discovered, since a track's span is known without it.\n *\n * The drawing is shared while what happens to the frame varies: a still encodes\n * it, a live preview leaves it on a visible canvas. The context is the\n * caller's throughout — this neither creates, resizes nor disposes it — so a host\n * holding one surface can call this repeatedly and keep both the surface and the\n * storage adapter's decode cache warm.\n */\nexport async function drawFrameAt(params: DrawFrameParams): Promise<DrawnFrame> {\n const { scale = 1, frame, project } = params;\n const harness: RenderHarness = params;\n const { renderContext, storageAdapter } = harness;\n\n if (project.tracks.length === 0) throw new Error(\"The project has no tracks to render.\");\n\n renderContext.pixelRatio = scale;\n const { store, precomper, stateEvaluator, measurer, loads } = prepare(params);\n const restorePolicy = exactDecodes(storageAdapter);\n try {\n const { totalFrames } = precomper.result();\n const requested =\n frame.kind === \"first\" ? 0\n : frame.kind === \"last\" ? totalFrames - 1\n : frame.frame;\n const targetFrame = Math.max(0, Math.min(totalFrames - 1, Math.floor(requested)));\n const precomp = precomper.ensureCovering(targetFrame);\n assertDrawable(store, precomp, targetFrame);\n\n const assetManager = loads.manager = new AssetManager(precomp, storageAdapter, new NoopAudioSink());\n await settleLayoutGates(store, assetManager, spansCovering(store.spans, targetFrame).map((span) => span.id));\n await assetManager.loadAt(targetFrame);\n stateEvaluator.stateAt(targetFrame);\n stateEvaluator.layout(measurer);\n renderContext.takeRenderIssues();\n await renderWarmFrame(harness, () => stateEvaluator.render(renderContext), targetFrame);\n drainIssues(renderContext, new Set(), params.onIssue);\n\n return { frame: targetFrame, totalFrames };\n } finally {\n restorePolicy();\n // The pass flushed the surface before returning, so the pixels survive the\n // tree being torn down here. In `finally` so a throw mid-draw doesn't leak\n // a live scene graph.\n stateEvaluator.dispose();\n store.dispose();\n }\n}\n\n/**\n * Throw a {@link NothingToDrawError} for the first frame in `first..last` whose\n * every track failed to build. Only a build that threw counts: `buildErrors` also\n * carries a node's failed declaration, and a track with one still draws.\n */\nfunction assertDrawable(store: ClipStore, precomp: AssetTimelineResult, first: number, last = first): void {\n const failed = new Map(precomp.buildErrors\n .filter((error) => store.failed(error.trackId))\n .map((error) => [error.trackId, error]));\n if (failed.size === 0) return;\n for (let frame = first; frame <= last; frame++) {\n const covering = spansCovering(store.spans, frame);\n if (covering.length > 0 && covering.every((span) => failed.has(span.id))) {\n throw new NothingToDrawError(frame, covering.map((span) => failed.get(span.id)!));\n }\n }\n}\n"]}
|