@motionscript/browser 0.1.0-alpha.7 → 0.1.0-alpha.8
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/NOTICE +15 -14
- package/README.md +66 -43
- package/dist/asset-bytes.d.ts +7 -4
- package/dist/asset-bytes.d.ts.map +1 -1
- package/dist/asset-bytes.js +9 -4
- package/dist/asset-bytes.js.map +1 -1
- package/dist/audio/bus-graph.d.ts.map +1 -1
- package/dist/audio/bus-graph.js +15 -6
- package/dist/audio/bus-graph.js.map +1 -1
- package/dist/audio/effect-graph.d.ts +62 -0
- package/dist/audio/effect-graph.d.ts.map +1 -0
- package/dist/audio/effect-graph.js +234 -0
- package/dist/audio/effect-graph.js.map +1 -0
- package/dist/audio/index.d.ts.map +1 -1
- package/dist/audio/index.js +2 -3
- package/dist/audio/index.js.map +1 -1
- package/dist/audio/mixer.d.ts +4 -4
- package/dist/audio/mixer.d.ts.map +1 -1
- package/dist/audio/mixer.js +18 -38
- package/dist/audio/mixer.js.map +1 -1
- package/dist/audio/player.d.ts +12 -6
- package/dist/audio/player.d.ts.map +1 -1
- package/dist/audio/player.js +49 -62
- package/dist/audio/player.js.map +1 -1
- package/dist/browser/THIRD_PARTY_LICENSES.txt +379 -27
- package/dist/browser/audio.js +1 -1
- package/dist/browser/chunks/chunk-IILVN4SK.js +2 -0
- package/dist/browser/chunks/chunk-IILVN4SK.js.map +7 -0
- package/dist/browser/chunks/chunk-KLXMRKTL.js +2 -0
- package/dist/browser/chunks/chunk-L5UFQACH.js +163 -0
- package/dist/browser/chunks/chunk-L5UFQACH.js.map +7 -0
- package/dist/browser/chunks/mediabunny-mp3-encoder-ZCIHVVTJ.js +14 -0
- package/dist/browser/chunks/mediabunny-mp3-encoder-ZCIHVVTJ.js.map +7 -0
- package/dist/browser/index.js +51 -1343
- package/dist/browser/index.js.map +4 -4
- package/dist/browser/manifest.json +8 -8
- package/dist/browser/ms_renderer_bg.wasm +0 -0
- package/dist/canvaskit-renderer.d.ts +8 -0
- package/dist/canvaskit-renderer.d.ts.map +1 -0
- package/dist/canvaskit-renderer.js +23 -0
- package/dist/canvaskit-renderer.js.map +1 -0
- package/dist/concat.d.ts +33 -7
- package/dist/concat.d.ts.map +1 -1
- package/dist/concat.js +117 -72
- package/dist/concat.js.map +1 -1
- package/dist/deliver.d.ts +10 -0
- package/dist/deliver.d.ts.map +1 -0
- package/dist/deliver.js +22 -0
- package/dist/deliver.js.map +1 -0
- package/dist/dynamic-nodes.d.ts.map +1 -1
- package/dist/dynamic-nodes.js +2 -3
- package/dist/dynamic-nodes.js.map +1 -1
- package/dist/engine.d.ts +37 -9
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +41 -32
- package/dist/engine.js.map +1 -1
- package/dist/export-audio.d.ts +36 -0
- package/dist/export-audio.d.ts.map +1 -0
- package/dist/export-audio.js +56 -0
- package/dist/export-audio.js.map +1 -0
- package/dist/exporter.d.ts +125 -14
- package/dist/exporter.d.ts.map +1 -1
- package/dist/exporter.js +133 -89
- package/dist/exporter.js.map +1 -1
- package/dist/formats.d.ts +36 -0
- package/dist/formats.d.ts.map +1 -0
- package/dist/formats.js +83 -0
- package/dist/formats.js.map +1 -0
- package/dist/frame-loop.d.ts +190 -0
- package/dist/frame-loop.d.ts.map +1 -0
- package/dist/frame-loop.js +304 -0
- package/dist/frame-loop.js.map +1 -0
- package/dist/getter.d.ts +4 -1
- package/dist/getter.d.ts.map +1 -1
- package/dist/getter.js +7 -6
- package/dist/getter.js.map +1 -1
- package/dist/index.d.ts +12 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -21
- package/dist/index.js.map +1 -1
- package/dist/mix-timeline-audio.d.ts +5 -5
- package/dist/mix-timeline-audio.d.ts.map +1 -1
- package/dist/mix-timeline-audio.js +10 -12
- package/dist/mix-timeline-audio.js.map +1 -1
- package/dist/parallel/export-parallel.d.ts +32 -0
- package/dist/parallel/export-parallel.d.ts.map +1 -0
- package/dist/parallel/export-parallel.js +114 -0
- package/dist/parallel/export-parallel.js.map +1 -0
- package/dist/parallel/governor.d.ts +57 -0
- package/dist/parallel/governor.d.ts.map +1 -0
- package/dist/parallel/governor.js +79 -0
- package/dist/parallel/governor.js.map +1 -0
- package/dist/parallel/lane.d.ts +45 -0
- package/dist/parallel/lane.d.ts.map +1 -0
- package/dist/parallel/lane.js +2 -0
- package/dist/parallel/lane.js.map +1 -0
- package/dist/parallel/lanes.d.ts +20 -0
- package/dist/parallel/lanes.d.ts.map +1 -0
- package/dist/parallel/lanes.js +152 -0
- package/dist/parallel/lanes.js.map +1 -0
- package/dist/parallel/plan.d.ts +23 -0
- package/dist/parallel/plan.d.ts.map +1 -0
- package/dist/parallel/plan.js +30 -0
- package/dist/parallel/plan.js.map +1 -0
- package/dist/parallel/run-lanes.d.ts +38 -0
- package/dist/parallel/run-lanes.d.ts.map +1 -0
- package/dist/parallel/run-lanes.js +164 -0
- package/dist/parallel/run-lanes.js.map +1 -0
- package/dist/parallel/session.d.ts +9 -0
- package/dist/parallel/session.d.ts.map +1 -0
- package/dist/parallel/session.js +79 -0
- package/dist/parallel/session.js.map +1 -0
- package/dist/render-context.d.ts +2 -0
- package/dist/render-context.d.ts.map +1 -1
- package/dist/render-context.js +8 -4
- package/dist/render-context.js.map +1 -1
- package/dist/renderer.d.ts +22 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +23 -0
- package/dist/renderer.js.map +1 -0
- package/dist/screenshot.d.ts +4 -4
- package/dist/screenshot.d.ts.map +1 -1
- package/dist/screenshot.js +3 -3
- package/dist/screenshot.js.map +1 -1
- package/dist/still.d.ts +17 -19
- package/dist/still.d.ts.map +1 -1
- package/dist/still.js +27 -34
- package/dist/still.js.map +1 -1
- package/dist/ticker.d.ts +22 -8
- package/dist/ticker.d.ts.map +1 -1
- package/dist/ticker.js +55 -20
- package/dist/ticker.js.map +1 -1
- package/package.json +9 -10
- package/dist/browser/chunks/GLTFLoader-PGXD4V5B.js +0 -2
- package/dist/browser/chunks/GLTFLoader-PGXD4V5B.js.map +0 -7
- package/dist/browser/chunks/SkeletonUtils-XYDMTGTM.js +0 -2
- package/dist/browser/chunks/chunk-2MAFJWBZ.js +0 -2
- package/dist/browser/chunks/chunk-2MAFJWBZ.js.map +0 -7
- package/dist/browser/chunks/chunk-CTLBXZSU.js +0 -4119
- package/dist/browser/chunks/chunk-CTLBXZSU.js.map +0 -7
- package/dist/browser/chunks/chunk-FQ32PDIU.js +0 -2
- package/dist/browser/chunks/chunk-FQ32PDIU.js.map +0 -7
- package/dist/browser/chunks/chunk-FY3BTKY4.js +0 -2
- package/dist/browser/chunks/chunk-FY3BTKY4.js.map +0 -7
- package/dist/browser/chunks/three.module-Z4EOVUWF.js +0 -2
- package/dist/browser/chunks/three.module-Z4EOVUWF.js.map +0 -7
- /package/dist/browser/chunks/{SkeletonUtils-XYDMTGTM.js.map → chunk-KLXMRKTL.js.map} +0 -0
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@motionscript/browser",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.8",
|
|
4
4
|
"exports": {
|
|
5
5
|
".": "dist/browser/index.js",
|
|
6
6
|
"./audio": "dist/browser/audio.js"
|
|
7
7
|
},
|
|
8
8
|
"peers": [
|
|
9
|
-
"@motionscript/canvaskit",
|
|
10
9
|
"@motionscript/core"
|
|
11
10
|
],
|
|
12
11
|
"assets": {
|
|
13
|
-
"esbuild.wasm": "dist/browser/esbuild.wasm"
|
|
12
|
+
"esbuild.wasm": "dist/browser/esbuild.wasm",
|
|
13
|
+
"ms_renderer_bg.wasm": "dist/browser/ms_renderer_bg.wasm"
|
|
14
14
|
},
|
|
15
15
|
"thirdParty": [
|
|
16
|
+
{
|
|
17
|
+
"name": "@mediabunny/mp3-encoder",
|
|
18
|
+
"version": "1.62.1",
|
|
19
|
+
"license": "MPL-2.0"
|
|
20
|
+
},
|
|
16
21
|
{
|
|
17
22
|
"name": "esbuild-wasm",
|
|
18
23
|
"version": "0.28.2",
|
|
@@ -22,11 +27,6 @@
|
|
|
22
27
|
"name": "mediabunny",
|
|
23
28
|
"version": "1.49.0",
|
|
24
29
|
"license": "MPL-2.0"
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
"name": "three",
|
|
28
|
-
"version": "0.185.1",
|
|
29
|
-
"license": "MIT"
|
|
30
30
|
}
|
|
31
31
|
]
|
|
32
32
|
}
|
|
Binary file
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BrowserRenderer } from "./engine";
|
|
2
|
+
import { getCanvasKit } from "./getter";
|
|
3
|
+
import { WebStorageAdapter } from "./storage-adapter";
|
|
4
|
+
/** The default backend: CanvasKit over WebGL. */
|
|
5
|
+
export declare function canvasKitRenderer(canvasKit: Awaited<ReturnType<typeof getCanvasKit>>): BrowserRenderer<WebStorageAdapter>;
|
|
6
|
+
/** `renderer` when a host gave one; else CanvasKit, loaded only now. */
|
|
7
|
+
export declare function resolveRenderer(renderer: BrowserRenderer | undefined, wasmUrl?: string): Promise<BrowserRenderer>;
|
|
8
|
+
//# sourceMappingURL=canvaskit-renderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canvaskit-renderer.d.ts","sourceRoot":"","sources":["../src/canvaskit-renderer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAGxC,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAEtD,iDAAiD;AACjD,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,GAAG,eAAe,CAAC,iBAAiB,CAAC,CAYzH;AAED,wEAAwE;AACxE,wBAAsB,eAAe,CAAC,QAAQ,EAAE,eAAe,GAAG,SAAS,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAEvH"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { getCanvasKit } from "./getter";
|
|
2
|
+
import { WebMeasureContext } from "./measure-context";
|
|
3
|
+
import { WebRenderContext } from "./render-context";
|
|
4
|
+
import { WebStorageAdapter } from "./storage-adapter";
|
|
5
|
+
/** The default backend: CanvasKit over WebGL. */
|
|
6
|
+
export function canvasKitRenderer(canvasKit) {
|
|
7
|
+
return {
|
|
8
|
+
createStorageAdapter({ catalog, viewport, fps }) {
|
|
9
|
+
return new WebStorageAdapter(canvasKit, catalog, viewport, fps);
|
|
10
|
+
},
|
|
11
|
+
createRenderContext(storage) {
|
|
12
|
+
return new WebRenderContext(canvasKit, storage);
|
|
13
|
+
},
|
|
14
|
+
createMeasureContext(storage) {
|
|
15
|
+
return new WebMeasureContext(storage);
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** `renderer` when a host gave one; else CanvasKit, loaded only now. */
|
|
20
|
+
export async function resolveRenderer(renderer, wasmUrl) {
|
|
21
|
+
return renderer ?? canvasKitRenderer(await getCanvasKit(wasmUrl));
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=canvaskit-renderer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canvaskit-renderer.js","sourceRoot":"","sources":["../src/canvaskit-renderer.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAEtD,iDAAiD;AACjD,MAAM,UAAU,iBAAiB,CAAC,SAAmD;IACjF,OAAO;QACH,oBAAoB,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAgB;YACzD,OAAO,IAAI,iBAAiB,CAAC,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAC;QACpE,CAAC;QACD,mBAAmB,CAAC,OAA0B;YAC1C,OAAO,IAAI,gBAAgB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QACpD,CAAC;QACD,oBAAoB,CAAC,OAA0B;YAC3C,OAAO,IAAI,iBAAiB,CAAC,OAAO,CAAC,CAAC;QAC1C,CAAC;KACJ,CAAC;AACN,CAAC;AAED,wEAAwE;AACxE,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,QAAqC,EAAE,OAAgB;IACzF,OAAO,QAAQ,IAAI,iBAAiB,CAAC,MAAM,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC;AACtE,CAAC","sourcesContent":["import type { MeasureContext2D, PlatformDeps } from \"@motionscript/core\";\r\nimport type { BrowserRenderer } from \"./engine\";\r\nimport { getCanvasKit } from \"./getter\";\r\nimport { WebMeasureContext } from \"./measure-context\";\r\nimport { WebRenderContext } from \"./render-context\";\r\nimport { WebStorageAdapter } from \"./storage-adapter\";\r\n\r\n/** The default backend: CanvasKit over WebGL. */\r\nexport function canvasKitRenderer(canvasKit: Awaited<ReturnType<typeof getCanvasKit>>): BrowserRenderer<WebStorageAdapter> {\r\n return {\r\n createStorageAdapter({ catalog, viewport, fps }: PlatformDeps): WebStorageAdapter {\r\n return new WebStorageAdapter(canvasKit, catalog, viewport, fps);\r\n },\r\n createRenderContext(storage: WebStorageAdapter): WebRenderContext {\r\n return new WebRenderContext(canvasKit, storage);\r\n },\r\n createMeasureContext(storage: WebStorageAdapter): MeasureContext2D {\r\n return new WebMeasureContext(storage);\r\n },\r\n };\r\n}\r\n\r\n/** `renderer` when a host gave one; else CanvasKit, loaded only now. */\r\nexport async function resolveRenderer(renderer: BrowserRenderer | undefined, wasmUrl?: string): Promise<BrowserRenderer> {\r\n return renderer ?? canvasKitRenderer(await getCanvasKit(wasmUrl));\r\n}\r\n"]}
|
package/dist/concat.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Join encoded MP4 segments end to end, without re-encoding.
|
|
2
|
+
* Join encoded MP4 or WebM segments end to end, without re-encoding.
|
|
3
3
|
*
|
|
4
4
|
* The point of this is what it *doesn't* do: no decode, no encode, no pixels.
|
|
5
5
|
* Each segment is demuxed to its encoded packets, the packets are re-stamped onto
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
*
|
|
16
16
|
* ## What the caller has to guarantee
|
|
17
17
|
*
|
|
18
|
-
* **Every segment must be encoded with an identical configuration** — same
|
|
19
|
-
*
|
|
18
|
+
* **Every segment must be encoded with an identical configuration** — same
|
|
19
|
+
* container, codec, resolution and bitrate settings. A container carries exactly one decoder
|
|
20
20
|
* configuration for a track, so a segment whose parameter sets differ from the
|
|
21
21
|
* first would be described by a configuration that isn't its own, and decode as
|
|
22
22
|
* garbage from that point on. That is not a theoretical concern: hardware encoders
|
|
@@ -56,7 +56,7 @@ export interface ConcatOptions {
|
|
|
56
56
|
* one. One buffer in, one encode, no seams.
|
|
57
57
|
*/
|
|
58
58
|
audio?: AudioBuffer | null;
|
|
59
|
-
/**
|
|
59
|
+
/** Bitrate for that track: AAC in an MP4, Opus in a WebM. Defaults to 192 kbps. */
|
|
60
60
|
audioBitrate?: number;
|
|
61
61
|
/**
|
|
62
62
|
* Write the result out as it is muxed instead of returning it.
|
|
@@ -81,13 +81,39 @@ export interface ConcatOptions {
|
|
|
81
81
|
position: number;
|
|
82
82
|
}>;
|
|
83
83
|
}
|
|
84
|
+
/** Joins segments one at a time, in timeline order, as each becomes available. */
|
|
85
|
+
export interface SegmentJoiner {
|
|
86
|
+
/**
|
|
87
|
+
* Mux the next segment after the ones already appended. Appends must not
|
|
88
|
+
* overlap: await each before starting the next.
|
|
89
|
+
*
|
|
90
|
+
* @throws {ConcatMismatchError} as {@link concatVideoSegments} does.
|
|
91
|
+
*/
|
|
92
|
+
append(segment: Uint8Array): Promise<void>;
|
|
93
|
+
/** Finish the container. The bytes, or an empty array when they went to `stream`. */
|
|
94
|
+
finish(): Promise<Uint8Array>;
|
|
95
|
+
/** Abandon the join; nothing more is written. */
|
|
96
|
+
cancel(): Promise<void>;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* {@link concatVideoSegments} one segment at a time, so a split export can write
|
|
100
|
+
* each piece the moment it and every piece before it exist, instead of holding
|
|
101
|
+
* every segment until the last one lands.
|
|
102
|
+
*
|
|
103
|
+
* The audio, when given, is encoded as soon as the first segment fixes the
|
|
104
|
+
* container, ahead of the video: the muxer holds the (small) audio packets and
|
|
105
|
+
* interleaves them as the video catches up.
|
|
106
|
+
*/
|
|
107
|
+
export declare function createSegmentJoiner(options?: ConcatOptions): SegmentJoiner;
|
|
84
108
|
/**
|
|
85
|
-
* Concatenate `segments` (each a complete MP4) into one
|
|
109
|
+
* Concatenate `segments` (each a complete MP4, or each a complete WebM) into one
|
|
110
|
+
* file of the same container.
|
|
86
111
|
*
|
|
87
112
|
* Segments are joined in array order. Returns the muxed bytes.
|
|
88
113
|
*
|
|
89
|
-
* @throws {ConcatMismatchError} if a segment carries no video track,
|
|
90
|
-
*
|
|
114
|
+
* @throws {ConcatMismatchError} if a segment carries no video track, is in another
|
|
115
|
+
* container than the first, or has a decoder configuration that differs
|
|
116
|
+
* from the first segment's. Callers that can
|
|
91
117
|
* fall back to a single-threaded export should catch this and do so — the
|
|
92
118
|
* alternative is shipping a file that decodes into garbage halfway through.
|
|
93
119
|
*/
|
package/dist/concat.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"concat.d.ts","sourceRoot":"","sources":["../src/concat.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"concat.d.ts","sourceRoot":"","sources":["../src/concat.ts"],"names":[],"mappings":"AAgBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,wFAAwF;AACxF,qBAAa,mBAAoB,SAAQ,KAAK;IAEtC,yDAAyD;IACzD,QAAQ,CAAC,OAAO,EAAE,MAAM;;IADxB,yDAAyD;IAChD,OAAO,EAAE,MAAM,EACxB,OAAO,EAAE,MAAM;CAKtB;AA2CD,MAAM,WAAW,aAAa;IAC1B;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAC3B,mFAAmF;IACnF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,EAAE,cAAc,CAAC;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,UAAU,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClF;AAED,kFAAkF;AAClF,MAAM,WAAW,aAAa;IAC1B;;;;;OAKG;IACH,MAAM,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,qFAAqF;IACrF,MAAM,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC;IAC9B,iDAAiD;IACjD,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,aAAkB,GAAG,aAAa,CAoE9E;AAuBD;;;;;;;;;;;GAWG;AACH,wBAAsB,mBAAmB,CACrC,QAAQ,EAAE,UAAU,EAAE,EACtB,OAAO,GAAE,aAAkB,GAC5B,OAAO,CAAC,UAAU,CAAC,CAcrB"}
|
package/dist/concat.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BufferSource, BufferTarget, EncodedPacketSink, EncodedVideoPacketSource, Input, MP4, Output, StreamTarget, WEBM, } from "mediabunny";
|
|
2
|
+
import { audioTrackSource, formatSpec } from "./formats";
|
|
2
3
|
/**
|
|
3
|
-
* Join encoded MP4 segments end to end, without re-encoding.
|
|
4
|
+
* Join encoded MP4 or WebM segments end to end, without re-encoding.
|
|
4
5
|
*
|
|
5
6
|
* The point of this is what it *doesn't* do: no decode, no encode, no pixels.
|
|
6
7
|
* Each segment is demuxed to its encoded packets, the packets are re-stamped onto
|
|
@@ -16,8 +17,8 @@ import { AudioBufferSource, BufferSource, BufferTarget, EncodedPacketSink, Encod
|
|
|
16
17
|
*
|
|
17
18
|
* ## What the caller has to guarantee
|
|
18
19
|
*
|
|
19
|
-
* **Every segment must be encoded with an identical configuration** — same
|
|
20
|
-
*
|
|
20
|
+
* **Every segment must be encoded with an identical configuration** — same
|
|
21
|
+
* container, codec, resolution and bitrate settings. A container carries exactly one decoder
|
|
21
22
|
* configuration for a track, so a segment whose parameter sets differ from the
|
|
22
23
|
* first would be described by a configuration that isn't its own, and decode as
|
|
23
24
|
* garbage from that point on. That is not a theoretical concern: hardware encoders
|
|
@@ -86,14 +87,114 @@ function toBytes(description) {
|
|
|
86
87
|
}
|
|
87
88
|
return new Uint8Array(description);
|
|
88
89
|
}
|
|
89
|
-
const DEFAULT_AUDIO_BITRATE = 192_000;
|
|
90
90
|
/**
|
|
91
|
-
*
|
|
91
|
+
* {@link concatVideoSegments} one segment at a time, so a split export can write
|
|
92
|
+
* each piece the moment it and every piece before it exist, instead of holding
|
|
93
|
+
* every segment until the last one lands.
|
|
94
|
+
*
|
|
95
|
+
* The audio, when given, is encoded as soon as the first segment fixes the
|
|
96
|
+
* container, ahead of the video: the muxer holds the (small) audio packets and
|
|
97
|
+
* interleaves them as the video catches up.
|
|
98
|
+
*/
|
|
99
|
+
export function createSegmentJoiner(options = {}) {
|
|
100
|
+
let first = null;
|
|
101
|
+
let output = null;
|
|
102
|
+
let source = null;
|
|
103
|
+
let audioSource = null;
|
|
104
|
+
let target = null;
|
|
105
|
+
let index = 0;
|
|
106
|
+
let offset = 0;
|
|
107
|
+
const start = async (segment) => {
|
|
108
|
+
target = options.stream
|
|
109
|
+
? new StreamTarget(options.stream)
|
|
110
|
+
: new BufferTarget();
|
|
111
|
+
output = new Output({ format: formatSpec(segment.format).container(), target });
|
|
112
|
+
// `codec` comes off the track rather than the decoder config: the latter's
|
|
113
|
+
// `codec` is the full codec *string* (`avc1.64001f`), while the source wants
|
|
114
|
+
// the family.
|
|
115
|
+
source = new EncodedVideoPacketSource(segment.track.codec);
|
|
116
|
+
output.addVideoTrack(source);
|
|
117
|
+
// Declared before `start()`: a muxer fixes its track list up front.
|
|
118
|
+
audioSource = options.audio ? audioTrackSource(segment.format, options.audioBitrate) : null;
|
|
119
|
+
if (audioSource)
|
|
120
|
+
output.addAudioTrack(audioSource);
|
|
121
|
+
await output.start();
|
|
122
|
+
if (audioSource && options.audio)
|
|
123
|
+
await audioSource.add(options.audio);
|
|
124
|
+
};
|
|
125
|
+
return {
|
|
126
|
+
async append(bytes) {
|
|
127
|
+
const i = index++;
|
|
128
|
+
const input = new Input({ source: new BufferSource(bytes), formats: [MP4, WEBM] });
|
|
129
|
+
try {
|
|
130
|
+
const segment = await openSegment(input, i, first);
|
|
131
|
+
if (!first) {
|
|
132
|
+
first = segment;
|
|
133
|
+
await start(segment);
|
|
134
|
+
}
|
|
135
|
+
// The decoder config is handed over with each segment's first packet.
|
|
136
|
+
// Only the first can take effect (a container holds one per track), but
|
|
137
|
+
// every one has been checked equal to it.
|
|
138
|
+
let leading = true;
|
|
139
|
+
for await (const packet of new EncodedPacketSink(segment.track).packets()) {
|
|
140
|
+
await source.add(packet.clone({ timestamp: packet.timestamp + offset }), leading ? { decoderConfig: segment.config } : undefined);
|
|
141
|
+
leading = false;
|
|
142
|
+
}
|
|
143
|
+
// The segment's own duration rather than its last packet's end: summing
|
|
144
|
+
// packet durations accumulates rounding into a drift across many segments.
|
|
145
|
+
offset += segment.duration;
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
input.dispose();
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
async finish() {
|
|
152
|
+
if (!output)
|
|
153
|
+
throw new Error("concatVideoSegments: no segments");
|
|
154
|
+
source.close();
|
|
155
|
+
audioSource?.close();
|
|
156
|
+
await output.finalize();
|
|
157
|
+
if (options.stream)
|
|
158
|
+
return new Uint8Array(0);
|
|
159
|
+
const buffer = target.buffer;
|
|
160
|
+
if (!buffer)
|
|
161
|
+
throw new Error("concatVideoSegments produced no data");
|
|
162
|
+
return new Uint8Array(buffer);
|
|
163
|
+
},
|
|
164
|
+
async cancel() {
|
|
165
|
+
if (output && output.state !== "finalized" && output.state !== "canceled")
|
|
166
|
+
await output.cancel();
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/** Demux one segment and check it can follow `first`. */
|
|
171
|
+
async function openSegment(input, i, first) {
|
|
172
|
+
const format = (await input.getFormat()) === WEBM ? "webm" : "mp4";
|
|
173
|
+
if (first && format !== first.format) {
|
|
174
|
+
throw new ConcatMismatchError(i, `Segment ${i} is ${format} and segment 0 is ${first.format}.`);
|
|
175
|
+
}
|
|
176
|
+
const track = await input.getPrimaryVideoTrack();
|
|
177
|
+
if (!track)
|
|
178
|
+
throw new ConcatMismatchError(i, `Segment ${i} has no video track.`);
|
|
179
|
+
const config = await track.getDecoderConfig();
|
|
180
|
+
if (!config)
|
|
181
|
+
throw new ConcatMismatchError(i, `Segment ${i} has no decoder configuration.`);
|
|
182
|
+
if (first && !sameDecoderConfig(first.config, config)) {
|
|
183
|
+
throw new ConcatMismatchError(i, `Segment ${i} was encoded differently from segment 0 (${first.config.codec} `
|
|
184
|
+
+ `${first.config.codedWidth}x${first.config.codedHeight} vs ${config.codec} `
|
|
185
|
+
+ `${config.codedWidth}x${config.codedHeight}); joining them would decode as garbage.`);
|
|
186
|
+
}
|
|
187
|
+
return { format, track, config, duration: await track.computeDuration() };
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Concatenate `segments` (each a complete MP4, or each a complete WebM) into one
|
|
191
|
+
* file of the same container.
|
|
92
192
|
*
|
|
93
193
|
* Segments are joined in array order. Returns the muxed bytes.
|
|
94
194
|
*
|
|
95
|
-
* @throws {ConcatMismatchError} if a segment carries no video track,
|
|
96
|
-
*
|
|
195
|
+
* @throws {ConcatMismatchError} if a segment carries no video track, is in another
|
|
196
|
+
* container than the first, or has a decoder configuration that differs
|
|
197
|
+
* from the first segment's. Callers that can
|
|
97
198
|
* fall back to a single-threaded export should catch this and do so — the
|
|
98
199
|
* alternative is shipping a file that decodes into garbage halfway through.
|
|
99
200
|
*/
|
|
@@ -104,71 +205,15 @@ export async function concatVideoSegments(segments, options = {}) {
|
|
|
104
205
|
// to it, or it has to go to a sink, either of which means muxing after all.
|
|
105
206
|
if (segments.length === 1 && !options.audio && !options.stream)
|
|
106
207
|
return segments[0];
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
throw new ConcatMismatchError(i, `Segment ${i} has no video track.`);
|
|
113
|
-
const config = await track.getDecoderConfig();
|
|
114
|
-
if (!config)
|
|
115
|
-
throw new ConcatMismatchError(i, `Segment ${i} has no decoder configuration.`);
|
|
116
|
-
if (opened.length > 0 && !sameDecoderConfig(opened[0].config, config)) {
|
|
117
|
-
throw new ConcatMismatchError(i, `Segment ${i} was encoded differently from segment 0 (${opened[0].config.codec} `
|
|
118
|
-
+ `${opened[0].config.codedWidth}x${opened[0].config.codedHeight} vs ${config.codec} `
|
|
119
|
-
+ `${config.codedWidth}x${config.codedHeight}); joining them would decode as garbage.`);
|
|
120
|
-
}
|
|
121
|
-
opened.push({ track, config, duration: await track.computeDuration() });
|
|
208
|
+
const joiner = createSegmentJoiner(options);
|
|
209
|
+
try {
|
|
210
|
+
for (const segment of segments)
|
|
211
|
+
await joiner.append(segment);
|
|
212
|
+
return await joiner.finish();
|
|
122
213
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const output = new Output({ format: new Mp4OutputFormat(), target });
|
|
127
|
-
// `codec` comes off the track rather than the decoder config: the latter's
|
|
128
|
-
// `codec` is the full codec *string* (`avc1.64001f`), while the source wants
|
|
129
|
-
// the family.
|
|
130
|
-
const source = new EncodedVideoPacketSource(opened[0].track.codec);
|
|
131
|
-
output.addVideoTrack(source);
|
|
132
|
-
// Declared before `start()` — a muxer fixes its track list up front, so an
|
|
133
|
-
// audio track added later would have nowhere to go.
|
|
134
|
-
const audioSource = options.audio
|
|
135
|
-
? new AudioBufferSource({
|
|
136
|
-
codec: "aac",
|
|
137
|
-
bitrate: options.audioBitrate ?? DEFAULT_AUDIO_BITRATE,
|
|
138
|
-
})
|
|
139
|
-
: null;
|
|
140
|
-
if (audioSource)
|
|
141
|
-
output.addAudioTrack(audioSource);
|
|
142
|
-
await output.start();
|
|
143
|
-
let offset = 0;
|
|
144
|
-
for (const segment of opened) {
|
|
145
|
-
// The decoder config is handed over with the first packet of each segment.
|
|
146
|
-
// Only the first one can take effect — a container holds one per track —
|
|
147
|
-
// but they have all been checked equal above, so restating it is harmless
|
|
148
|
-
// and keeps the muxer from having to infer anything.
|
|
149
|
-
let first = true;
|
|
150
|
-
for await (const packet of new EncodedPacketSink(segment.track).packets()) {
|
|
151
|
-
await source.add(packet.clone({ timestamp: packet.timestamp + offset }), first ? { decoderConfig: segment.config } : undefined);
|
|
152
|
-
first = false;
|
|
153
|
-
}
|
|
154
|
-
// Advance by the segment's own duration rather than by the last packet's
|
|
155
|
-
// end: a packet's `duration` is what the container recorded, and summing
|
|
156
|
-
// those would accumulate rounding into a drift across many segments.
|
|
157
|
-
offset += segment.duration;
|
|
214
|
+
catch (error) {
|
|
215
|
+
await joiner.cancel().catch(() => { });
|
|
216
|
+
throw error;
|
|
158
217
|
}
|
|
159
|
-
// After the video packets, so the encode overlaps nothing it has to wait on.
|
|
160
|
-
if (audioSource && options.audio)
|
|
161
|
-
await audioSource.add(options.audio);
|
|
162
|
-
source.close();
|
|
163
|
-
audioSource?.close();
|
|
164
|
-
await output.finalize();
|
|
165
|
-
// A streamed result has already gone to the sink; there is nothing to hand
|
|
166
|
-
// back, and materialising it here would undo the point of streaming.
|
|
167
|
-
if (options.stream)
|
|
168
|
-
return new Uint8Array(0);
|
|
169
|
-
const buffer = target.buffer;
|
|
170
|
-
if (!buffer)
|
|
171
|
-
throw new Error("concatVideoSegments produced no data");
|
|
172
|
-
return new Uint8Array(buffer);
|
|
173
218
|
}
|
|
174
219
|
//# sourceMappingURL=concat.js.map
|
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 AudioBufferSource,\r\n BufferSource,\r\n BufferTarget,\r\n EncodedPacketSink,\r\n EncodedVideoPacketSource,\r\n Input,\r\n MP4,\r\n Mp4OutputFormat,\r\n Output,\r\n StreamTarget,\r\n type InputVideoTrack,\r\n type VideoCodec,\r\n} from \"mediabunny\";\r\n\r\n/**\r\n * Join encoded MP4 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 codec,\r\n * same resolution, same 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 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 /** AAC bitrate for that track. 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\nconst DEFAULT_AUDIO_BITRATE = 192_000;\r\n\r\n/**\r\n * Concatenate `segments` (each a complete MP4) into one MP4.\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, or one whose\r\n * decoder configuration differs 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 opened: Segment[] = [];\r\n for (let i = 0; i < segments.length; i++) {\r\n const input = new Input({ source: new BufferSource(segments[i]), formats: [MP4] });\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 (opened.length > 0 && !sameDecoderConfig(opened[0].config, config)) {\r\n throw new ConcatMismatchError(\r\n i,\r\n `Segment ${i} was encoded differently from segment 0 (${opened[0].config.codec} `\r\n + `${opened[0].config.codedWidth}x${opened[0].config.codedHeight} vs ${config.codec} `\r\n + `${config.codedWidth}x${config.codedHeight}); joining them would decode as garbage.`,\r\n );\r\n }\r\n opened.push({ track, config, duration: await track.computeDuration() });\r\n }\r\n\r\n const target = options.stream\r\n ? new StreamTarget(options.stream as ConstructorParameters<typeof StreamTarget>[0])\r\n : new BufferTarget();\r\n const output = new Output({ format: new Mp4OutputFormat(), 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 const source = new EncodedVideoPacketSource(opened[0].track.codec as VideoCodec);\r\n output.addVideoTrack(source);\r\n // Declared before `start()` — a muxer fixes its track list up front, so an\r\n // audio track added later would have nowhere to go.\r\n const audioSource = options.audio\r\n ? new AudioBufferSource({\r\n codec: \"aac\",\r\n bitrate: options.audioBitrate ?? DEFAULT_AUDIO_BITRATE,\r\n })\r\n : null;\r\n if (audioSource) output.addAudioTrack(audioSource);\r\n await output.start();\r\n\r\n let offset = 0;\r\n for (const segment of opened) {\r\n // The decoder config is handed over with the first packet of each segment.\r\n // Only the first one can take effect — a container holds one per track —\r\n // but they have all been checked equal above, so restating it is harmless\r\n // and keeps the muxer from having to infer anything.\r\n let first = 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 first ? { decoderConfig: segment.config } : undefined,\r\n );\r\n first = false;\r\n }\r\n // Advance by the segment's own duration rather than by the last packet's\r\n // end: a packet's `duration` is what the container recorded, and summing\r\n // those would accumulate rounding into a drift across many segments.\r\n offset += segment.duration;\r\n }\r\n\r\n // After the video packets, so the encode overlaps nothing it has to wait on.\r\n if (audioSource && options.audio) await audioSource.add(options.audio);\r\n\r\n source.close();\r\n audioSource?.close();\r\n await output.finalize();\r\n\r\n // A streamed result has already gone to the sink; there is nothing to hand\r\n // back, and materialising it here would undo the point of streaming.\r\n if (options.stream) return new Uint8Array(0);\r\n\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"]}
|
|
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"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand an export's bytes back, or download them through an `<a download>`.
|
|
3
|
+
* Headless callers capture the bytes and write the file themselves.
|
|
4
|
+
*/
|
|
5
|
+
export declare function deliver(bytes: Uint8Array, options: {
|
|
6
|
+
returnBytes: boolean;
|
|
7
|
+
filename: string;
|
|
8
|
+
mimeType: string;
|
|
9
|
+
}): Uint8Array | undefined;
|
|
10
|
+
//# sourceMappingURL=deliver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deliver.d.ts","sourceRoot":"","sources":["../src/deliver.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,OAAO,CACnB,KAAK,EAAE,UAAU,EACjB,OAAO,EAAE;IAAE,WAAW,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACtE,UAAU,GAAG,SAAS,CAgBxB"}
|
package/dist/deliver.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand an export's bytes back, or download them through an `<a download>`.
|
|
3
|
+
* Headless callers capture the bytes and write the file themselves.
|
|
4
|
+
*/
|
|
5
|
+
export function deliver(bytes, options) {
|
|
6
|
+
if (options.returnBytes)
|
|
7
|
+
return bytes;
|
|
8
|
+
// Every encoder here builds its Uint8Array over a whole, plain ArrayBuffer, so
|
|
9
|
+
// the cast is sound — TS only balks because the generic parameter admits a
|
|
10
|
+
// SharedArrayBuffer, which a BlobPart may not be.
|
|
11
|
+
const blob = new Blob([bytes.buffer], { type: options.mimeType });
|
|
12
|
+
const url = URL.createObjectURL(blob);
|
|
13
|
+
const link = document.createElement("a");
|
|
14
|
+
link.href = url;
|
|
15
|
+
link.download = options.filename;
|
|
16
|
+
document.body.appendChild(link);
|
|
17
|
+
link.click();
|
|
18
|
+
document.body.removeChild(link);
|
|
19
|
+
URL.revokeObjectURL(url);
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=deliver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deliver.js","sourceRoot":"","sources":["../src/deliver.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,UAAU,OAAO,CACnB,KAAiB,EACjB,OAAqE;IAErE,IAAI,OAAO,CAAC,WAAW;QAAE,OAAO,KAAK,CAAC;IAEtC,+EAA+E;IAC/E,2EAA2E;IAC3E,kDAAkD;IAClD,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,CAAC,KAAK,CAAC,MAAqB,CAAC,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;IACjF,MAAM,GAAG,GAAG,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;IACtC,MAAM,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;IACzC,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC;IAChB,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;IACjC,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,CAAC,KAAK,EAAE,CAAC;IACb,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;IAChC,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;IACzB,OAAO,SAAS,CAAC;AACrB,CAAC","sourcesContent":["/**\n * Hand an export's bytes back, or download them through an `<a download>`.\n * Headless callers capture the bytes and write the file themselves.\n */\nexport function deliver(\n bytes: Uint8Array,\n options: { returnBytes: boolean; filename: string; mimeType: string },\n): Uint8Array | undefined {\n if (options.returnBytes) return bytes;\n\n // Every encoder here builds its Uint8Array over a whole, plain ArrayBuffer, so\n // the cast is sound — TS only balks because the generic parameter admits a\n // SharedArrayBuffer, which a BlobPart may not be.\n const blob = new Blob([bytes.buffer as ArrayBuffer], { type: options.mimeType });\n const url = URL.createObjectURL(blob);\n const link = document.createElement(\"a\");\n link.href = url;\n link.download = options.filename;\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n URL.revokeObjectURL(url);\n return undefined;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-nodes.d.ts","sourceRoot":"","sources":["../src/dynamic-nodes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"dynamic-nodes.d.ts","sourceRoot":"","sources":["../src/dynamic-nodes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AA6C5D;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAC/B,OAAO,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAAG,SAAS,EAC/C,eAAe,CAAC,EAAE,MAAM,GACzB,iBAAiB,CAMnB"}
|
package/dist/dynamic-nodes.js
CHANGED
|
@@ -11,19 +11,18 @@ import * as esbuild from "esbuild-wasm";
|
|
|
11
11
|
* `@motionscript/sdk` barrel (that package depends on `core`, so the reverse
|
|
12
12
|
* would be circular) — `browser` is where this concern actually belongs: the
|
|
13
13
|
* package whose entire job is "what running in an actual browser tab looks
|
|
14
|
-
* like" already owns `
|
|
14
|
+
* like" already owns the renderer, `WebAudioSink` and `WebTicker` on exactly that
|
|
15
15
|
* basis.
|
|
16
16
|
*/
|
|
17
17
|
// Default to the wasm shipped by esbuild-wasm, resolved lazily — `import.meta.url`
|
|
18
18
|
// is empty in a non-ESM output (an IIFE bundle, which is what the headless page
|
|
19
19
|
// loads), and `new URL("…", "")` throws on import, before a caller has had the
|
|
20
20
|
// chance to pass an explicit URL that would have made the default irrelevant.
|
|
21
|
-
// Mirrors `getCanvasKit`'s own `defaultWasmUrl` in `getter.ts`.
|
|
22
21
|
function defaultCompilerWasmUrl() {
|
|
23
22
|
return new URL("esbuild-wasm/esbuild.wasm", import.meta.url).href;
|
|
24
23
|
}
|
|
25
24
|
let compilerPromise;
|
|
26
|
-
/** Loaded once, memoized process-wide
|
|
25
|
+
/** Loaded once, memoized process-wide. */
|
|
27
26
|
function defaultCompiler(wasmUrl) {
|
|
28
27
|
return (compilerPromise ??= (async () => {
|
|
29
28
|
await esbuild.initialize({ wasmURL: wasmUrl ?? defaultCompilerWasmUrl() });
|
|
@@ -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,
|
|
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\";\r\nimport * as sdk from \"@motionscript/core/sdk\";\r\nimport * as esbuild from \"esbuild-wasm\";\r\n\r\n/**\r\n * `browser`'s real, working default for `Registry.addNodeFromSource` — the\r\n * `sdk` half is cheap (re-exports, no WASM) and imported eagerly; the compiler\r\n * is the expensive half, and stays behind a lazy `import()` so an app that\r\n * never registers a source-string node never pays for it, however long the\r\n * engine has been running.\r\n *\r\n * `core` has no compiler dependency of its own and cannot import the published\r\n * `@motionscript/sdk` barrel (that package depends on `core`, so the reverse\r\n * would be circular) — `browser` is where this concern actually belongs: the\r\n * package whose entire job is \"what running in an actual browser tab looks\r\n * like\" already owns the renderer, `WebAudioSink` and `WebTicker` on exactly that\r\n * basis.\r\n */\r\n\r\n// Default to the wasm shipped by esbuild-wasm, resolved lazily — `import.meta.url`\r\n// is empty in a non-ESM output (an IIFE bundle, which is what the headless page\r\n// loads), and `new URL(\"…\", \"\")` throws on import, before a caller has had the\r\n// chance to pass an explicit URL that would have made the default irrelevant.\r\nfunction defaultCompilerWasmUrl(): string {\r\n return new URL(\"esbuild-wasm/esbuild.wasm\", import.meta.url).href;\r\n}\r\n\r\nlet compilerPromise: Promise<(source: string) => Promise<string>> | undefined;\r\n\r\n/** Loaded once, memoized process-wide. */\r\nfunction defaultCompiler(wasmUrl?: string): Promise<(source: string) => Promise<string>> {\r\n return (compilerPromise ??= (async () => {\r\n await esbuild.initialize({ wasmURL: wasmUrl ?? defaultCompilerWasmUrl() });\r\n return async (source: string) => {\r\n const { code } = await esbuild.transform(source, {\r\n // A component is written with calls: there is no JSX to compile.\r\n loader: \"ts\",\r\n format: \"cjs\",\r\n target: \"es2020\",\r\n });\r\n return code;\r\n };\r\n })());\r\n}\r\n\r\n/**\r\n * Resolves a full `DynamicNodeConfig`, filling in whatever the caller left\r\n * unset with `browser`'s own default. Synchronous — the WASM import happens\r\n * on `compile`'s first actual call, not here, so attaching this to every\r\n * engine costs nothing until a source string is genuinely registered.\r\n */\r\nexport function resolveDynamicNodes(\r\n partial: Partial<DynamicNodeConfig> | undefined,\r\n compilerWasmUrl?: string,\r\n): DynamicNodeConfig {\r\n return {\r\n sdk: partial?.sdk ?? sdk,\r\n modules: partial?.modules,\r\n compile: partial?.compile ?? (async (source) => (await defaultCompiler(compilerWasmUrl))(source)),\r\n };\r\n}\r\n"]}
|