fundus 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -1
- package/build/client/_app/immutable/chunks/{BEbRZpHw.js → BJF4m1Bj.js} +1 -1
- package/build/client/_app/immutable/chunks/BJF4m1Bj.js.br +0 -0
- package/build/client/_app/immutable/chunks/BJF4m1Bj.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{BVDR6adL.js → C3HGm51f.js} +1 -1
- package/build/client/_app/immutable/chunks/C3HGm51f.js.br +0 -0
- package/build/client/_app/immutable/chunks/C3HGm51f.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{4FCfXqUa.js → C7OUKWnm.js} +1 -1
- package/build/client/_app/immutable/chunks/C7OUKWnm.js.br +0 -0
- package/build/client/_app/immutable/chunks/C7OUKWnm.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CrhgzsNe.js +1 -0
- package/build/client/_app/immutable/chunks/CrhgzsNe.js.br +0 -0
- package/build/client/_app/immutable/chunks/CrhgzsNe.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DSWdnTh9.js +108 -0
- package/build/client/_app/immutable/chunks/DSWdnTh9.js.br +0 -0
- package/build/client/_app/immutable/chunks/DSWdnTh9.js.gz +0 -0
- package/build/client/_app/immutable/entry/{app.CH6AvWYr.js → app.BSLYJbsx.js} +2 -2
- package/build/client/_app/immutable/entry/app.BSLYJbsx.js.br +0 -0
- package/build/client/_app/immutable/entry/app.BSLYJbsx.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.BvNZNOOV.js +1 -0
- package/build/client/_app/immutable/entry/start.BvNZNOOV.js.br +0 -0
- package/build/client/_app/immutable/entry/start.BvNZNOOV.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{0.bYfuwnNW.js → 0.cLtfz2y2.js} +1 -1
- package/build/client/_app/immutable/nodes/0.cLtfz2y2.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.cLtfz2y2.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{1.kv-uSGjV.js → 1.tMVPJLEm.js} +1 -1
- package/build/client/_app/immutable/nodes/1.tMVPJLEm.js.br +3 -0
- package/build/client/_app/immutable/nodes/1.tMVPJLEm.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.TBMdq9ab.js +3 -0
- package/build/client/_app/immutable/nodes/2.TBMdq9ab.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.TBMdq9ab.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.DDoGfLgu.js +1 -0
- package/build/client/_app/immutable/nodes/3.DDoGfLgu.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.DDoGfLgu.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{4.DXitiaKH.js → 4.UrhL5n-g.js} +1 -1
- package/build/client/_app/immutable/nodes/4.UrhL5n-g.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.UrhL5n-g.js.gz +0 -0
- package/build/client/_app/version.json +1 -1
- package/build/client/_app/version.json.br +0 -0
- package/build/client/_app/version.json.gz +0 -0
- package/build/handler.js +7 -7
- package/build/index.js +7 -7
- package/build/server/chunks/chunks/{context.js-CPdlcRhV.js → context.js-mXbNUDBD.js} +13 -6
- package/build/server/chunks/chunks/context.js-mXbNUDBD.js.map +1 -0
- package/build/server/chunks/chunks/{contract.js-nwG99Zzj.js → contract.js-BCXeXDmU.js} +29 -13
- package/build/server/chunks/chunks/contract.js-BCXeXDmU.js.map +1 -0
- package/build/server/chunks/chunks/{core.js-DNIuZ9y-.js → core.js-Cj7hjmtV.js} +49 -2
- package/build/server/chunks/chunks/core.js-Cj7hjmtV.js.map +1 -0
- package/build/server/chunks/chunks/{exports.js-Ci6jkE-z.js → exports.js-DFO7C1N3.js} +2 -2
- package/build/server/chunks/chunks/{exports.js-Ci6jkE-z.js.map → exports.js-DFO7C1N3.js.map} +1 -1
- package/build/server/chunks/chunks/{http.js-DmUA2gRk.js → http.js-BHghHRmI.js} +2 -2
- package/build/server/chunks/chunks/{http.js-DmUA2gRk.js.map → http.js-BHghHRmI.js.map} +1 -1
- package/build/server/chunks/chunks/{internal.js-DWu3OCGw.js → internal.js-Du8xenh_.js} +3 -3
- package/build/server/chunks/chunks/{internal.js-DWu3OCGw.js.map → internal.js-Du8xenh_.js.map} +1 -1
- package/build/server/chunks/chunks/internal2.js-BlXt2rfE.js.map +1 -1
- package/build/server/chunks/chunks/{manifest-membership.js-CJnQXrL4.js → manifest-membership.js-vIb-KkQi.js} +2 -2
- package/build/server/chunks/chunks/{manifest-membership.js-CJnQXrL4.js.map → manifest-membership.js-vIb-KkQi.js.map} +1 -1
- package/build/server/chunks/chunks/{raw-reconciliation.js-_D9lZ_sc.js → raw-reconciliation.js-CeIOAYCA.js} +3 -3
- package/build/server/chunks/chunks/raw-reconciliation.js-CeIOAYCA.js.map +1 -0
- package/build/server/chunks/chunks/{raw-reconciliation2.js-C2No1QIL.js → raw-reconciliation2.js-5mUCDiJk.js} +2 -2
- package/build/server/chunks/chunks/{raw-reconciliation2.js-C2No1QIL.js.map → raw-reconciliation2.js-5mUCDiJk.js.map} +1 -1
- package/build/server/chunks/chunks/{shared.js-BIggwEH0.js → shared.js-CNSYTfB6.js} +2 -2
- package/build/server/chunks/chunks/{shared.js-BIggwEH0.js.map → shared.js-CNSYTfB6.js.map} +1 -1
- package/build/server/chunks/chunks/{utils.js-Dxvg75XM.js → utils.js-ATk6U-az.js} +3 -3
- package/build/server/chunks/chunks/{utils.js-Dxvg75XM.js.map → utils.js-ATk6U-az.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/_...path_/{_server.ts.js-wQD5tBuh.js → _server.ts.js-CN1hcfLb.js} +6 -6
- package/build/server/chunks/entries/endpoints/api/_...path_/{_server.ts.js-wQD5tBuh.js.map → _server.ts.js-CN1hcfLb.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/assets/_id_/{_server.ts.js-CLCx81uK.js → _server.ts.js-D_LU4nrh.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/assets/_id_/{_server.ts.js-CLCx81uK.js.map → _server.ts.js-D_LU4nrh.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/assets/_id_/raw/{_server.ts.js-DkvELcZA.js → _server.ts.js-BY9NgKmC.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/assets/_id_/raw/{_server.ts.js-DkvELcZA.js.map → _server.ts.js-BY9NgKmC.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/assets/_id_/reimport/{_server.ts.js-CCNcWQxH.js → _server.ts.js-D2uPmOzD.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/assets/_id_/reimport/{_server.ts.js-CCNcWQxH.js.map → _server.ts.js-D2uPmOzD.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/assets/{_server.ts.js-DUK4Rnpu.js → _server.ts.js-CCsQkuSA.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/assets/{_server.ts.js-DUK4Rnpu.js.map → _server.ts.js-CCsQkuSA.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/assets/delete/{_server.ts.js-DNgHnoga.js → _server.ts.js-CFVRCJTe.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/assets/delete/{_server.ts.js-DNgHnoga.js.map → _server.ts.js-CFVRCJTe.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/assets/move/{_server.ts.js-gdXko55o.js → _server.ts.js-DTDSQlpi.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/assets/move/{_server.ts.js-gdXko55o.js.map → _server.ts.js-DTDSQlpi.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/folders/_path_/{_server.ts.js-DcsBk_jx.js → _server.ts.js-DrunH7kn.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/folders/_path_/{_server.ts.js-DcsBk_jx.js.map → _server.ts.js-DrunH7kn.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/folders/{_server.ts.js-BhTg6GgC.js → _server.ts.js-jskTD8nZ.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/folders/{_server.ts.js-BhTg6GgC.js.map → _server.ts.js-jskTD8nZ.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/imports/{_server.ts.js-CKjnRFXQ.js → _server.ts.js-DEXkS6pQ.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/imports/{_server.ts.js-CKjnRFXQ.js.map → _server.ts.js-DEXkS6pQ.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/manifests/_name_/{_server.ts.js-C9rdzmoj.js → _server.ts.js-baLtBCf4.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/manifests/_name_/{_server.ts.js-C9rdzmoj.js.map → _server.ts.js-baLtBCf4.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/manifests/{_server.ts.js-BdZb4d69.js → _server.ts.js-1Cx7sbfQ.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/manifests/{_server.ts.js-BdZb4d69.js.map → _server.ts.js-1Cx7sbfQ.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/raw/adopt/{_server.ts.js-DaTgBGGt.js → _server.ts.js-Xc7glEPl.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/raw/adopt/{_server.ts.js-DaTgBGGt.js.map → _server.ts.js-Xc7glEPl.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/raw/edited/accept/{_server.ts.js-Btohjiz4.js → _server.ts.js-C7IxpaSj.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/raw/edited/accept/{_server.ts.js-Btohjiz4.js.map → _server.ts.js-C7IxpaSj.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/raw/edited/dismiss/{_server.ts.js-D0alWeVb.js → _server.ts.js-DLZkdOrS.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/raw/edited/dismiss/{_server.ts.js-D0alWeVb.js.map → _server.ts.js-DLZkdOrS.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/reconcile/{_server.ts.js-CRNi0bmU.js → _server.ts.js-CSS2DDb1.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/reconcile/{_server.ts.js-CRNi0bmU.js.map → _server.ts.js-CSS2DDb1.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/reveal/{_server.ts.js-B6C1DDpq.js → _server.ts.js-CrXh2YL-.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/reveal/{_server.ts.js-B6C1DDpq.js.map → _server.ts.js-CrXh2YL-.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/state/{_server.ts.js-qPKxHQ-e.js → _server.ts.js-yT-0jHAT.js} +7 -7
- package/build/server/chunks/entries/endpoints/api/state/{_server.ts.js-qPKxHQ-e.js.map → _server.ts.js-yT-0jHAT.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/files/_...path_/{_server.ts.js-BjioCWTI.js → _server.ts.js-DTZAlq3j.js} +6 -6
- package/build/server/chunks/entries/endpoints/files/_...path_/{_server.ts.js-BjioCWTI.js.map → _server.ts.js-DTZAlq3j.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/files/proxy/_...path_/{_server.ts.js-BzUFg_ur.js → _server.ts.js-Cux6Dnfu.js} +7 -7
- package/build/server/chunks/entries/endpoints/files/proxy/_...path_/{_server.ts.js-BzUFg_ur.js.map → _server.ts.js-Cux6Dnfu.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/files/raw/_...path_/{_server.ts.js-CrjUXxJT.js → _server.ts.js-BjcPFOHi.js} +7 -7
- package/build/server/chunks/entries/endpoints/files/raw/_...path_/{_server.ts.js-CrjUXxJT.js.map → _server.ts.js-BjcPFOHi.js.map} +1 -1
- package/build/server/chunks/entries/{hooks.server.js-DQ8kZ3N2.js → hooks.server.js-e6ugpMpo.js} +6 -6
- package/build/server/chunks/entries/{hooks.server.js-DQ8kZ3N2.js.map → hooks.server.js-e6ugpMpo.js.map} +1 -1
- package/build/server/chunks/{handler-CJ8lsr6W.js → handler-BbW2vgaL.js} +5 -5
- package/build/server/chunks/{handler-CJ8lsr6W.js.map → handler-BbW2vgaL.js.map} +1 -1
- package/build/server/chunks/{index.js-Ph6LJ5Na.js → index.js-CMR9GUhE.js} +5 -5
- package/build/server/chunks/{index.js-Ph6LJ5Na.js.map → index.js-CMR9GUhE.js.map} +1 -1
- package/build/server/chunks/{manifest.js-CWjy0GQ9.js → manifest.js-Chbh3YcD.js} +28 -28
- package/build/server/chunks/{manifest.js-CWjy0GQ9.js.map → manifest.js-Chbh3YcD.js.map} +1 -1
- package/build/server/chunks/nodes/{0.js-s3yR2Abv.js → 0.js-DH9RX0ts.js} +2 -2
- package/build/server/chunks/nodes/{0.js-s3yR2Abv.js.map → 0.js-DH9RX0ts.js.map} +1 -1
- package/build/server/chunks/nodes/1.js-CZRl5Ija.js +7 -0
- package/build/server/chunks/nodes/{1.js-BKDWaQXi.js.map → 1.js-CZRl5Ija.js.map} +1 -1
- package/build/server/chunks/nodes/2.js-Dh13OPjS.js +7 -0
- package/build/server/chunks/nodes/2.js-Dh13OPjS.js.map +1 -0
- package/build/server/chunks/nodes/3.js-CnTWmFQd.js +7 -0
- package/build/server/chunks/nodes/{3.js-CxLEr5zC.js.map → 3.js-CnTWmFQd.js.map} +1 -1
- package/build/server/chunks/nodes/4.js-hm5WP4MD.js +7 -0
- package/build/server/chunks/nodes/{4.js-BoG4R0fU.js.map → 4.js-hm5WP4MD.js.map} +1 -1
- package/dist/core/api-types.d.ts +7 -0
- package/dist/core/index.d.ts +3 -3
- package/dist/core/index.js +1 -1
- package/dist/core/plugins/video.d.ts +38 -3
- package/dist/core/plugins/video.js +62 -0
- package/dist/runtime/chroma-key-gl.d.ts +2 -0
- package/dist/runtime/chroma-key-gl.js +6 -0
- package/dist/runtime/chroma-key-video-lifecycle.d.ts +22 -0
- package/dist/runtime/chroma-key-video-lifecycle.js +324 -0
- package/dist/runtime/components/ChromaKeyVideo.svelte +35 -127
- package/dist/server/config-loader.d.ts +2 -2
- package/dist/server/operations.js +6 -1
- package/dist/server/plugins/chroma-key-video.d.ts +2 -2
- package/dist/server/plugins/chroma-key-video.js +5 -4
- package/dist/server/plugins/contract.d.ts +3 -1
- package/dist/server/plugins/video.d.ts +4 -2
- package/dist/server/plugins/video.js +10 -2
- package/dist/server/processing/video-processing.d.ts +3 -3
- package/dist/server/processing/video-processing.js +19 -7
- package/package.json +1 -1
- package/build/client/_app/immutable/chunks/4FCfXqUa.js.br +0 -0
- package/build/client/_app/immutable/chunks/4FCfXqUa.js.gz +0 -0
- package/build/client/_app/immutable/chunks/B6BtXxR0.js +0 -1
- package/build/client/_app/immutable/chunks/B6BtXxR0.js.br +0 -0
- package/build/client/_app/immutable/chunks/B6BtXxR0.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BEbRZpHw.js.br +0 -1
- package/build/client/_app/immutable/chunks/BEbRZpHw.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BVDR6adL.js.br +0 -0
- package/build/client/_app/immutable/chunks/BVDR6adL.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BX8lJS3k.js +0 -108
- package/build/client/_app/immutable/chunks/BX8lJS3k.js.br +0 -0
- package/build/client/_app/immutable/chunks/BX8lJS3k.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.CH6AvWYr.js.br +0 -0
- package/build/client/_app/immutable/entry/app.CH6AvWYr.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.v9Hgefer.js +0 -1
- package/build/client/_app/immutable/entry/start.v9Hgefer.js.br +0 -0
- package/build/client/_app/immutable/entry/start.v9Hgefer.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.bYfuwnNW.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.bYfuwnNW.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.kv-uSGjV.js.br +0 -3
- package/build/client/_app/immutable/nodes/1.kv-uSGjV.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.B0CNEieI.js +0 -3
- package/build/client/_app/immutable/nodes/2.B0CNEieI.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.B0CNEieI.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.K5eLxWD7.js +0 -1
- package/build/client/_app/immutable/nodes/3.K5eLxWD7.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.K5eLxWD7.js.gz +0 -0
- package/build/client/_app/immutable/nodes/4.DXitiaKH.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.DXitiaKH.js.gz +0 -0
- package/build/server/chunks/chunks/context.js-CPdlcRhV.js.map +0 -1
- package/build/server/chunks/chunks/contract.js-nwG99Zzj.js.map +0 -1
- package/build/server/chunks/chunks/core.js-DNIuZ9y-.js.map +0 -1
- package/build/server/chunks/chunks/raw-reconciliation.js-_D9lZ_sc.js.map +0 -1
- package/build/server/chunks/nodes/1.js-BKDWaQXi.js +0 -7
- package/build/server/chunks/nodes/2.js-BuHtNZN4.js +0 -7
- package/build/server/chunks/nodes/2.js-BuHtNZN4.js.map +0 -1
- package/build/server/chunks/nodes/3.js-CxLEr5zC.js +0 -7
- package/build/server/chunks/nodes/4.js-BoG4R0fU.js +0 -7
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
const HAVE_CURRENT_DATA = 2;
|
|
2
|
+
function copyChromaKey(parameters) {
|
|
3
|
+
return {
|
|
4
|
+
...parameters,
|
|
5
|
+
keyColor: [...parameters.keyColor]
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
/** Internal media state machine for a single, stable video element. */
|
|
9
|
+
export class ChromaKeyVideoLifecycle {
|
|
10
|
+
#video;
|
|
11
|
+
#renderer;
|
|
12
|
+
#active = null;
|
|
13
|
+
#sourceGeneration = 0;
|
|
14
|
+
#playing = false;
|
|
15
|
+
#disposed = false;
|
|
16
|
+
#loopGeneration = 0;
|
|
17
|
+
#loopRunning = false;
|
|
18
|
+
#pendingFrame = null;
|
|
19
|
+
constructor(video, renderer) {
|
|
20
|
+
this.#video = video;
|
|
21
|
+
this.#renderer = renderer;
|
|
22
|
+
video.addEventListener('play', this.#handlePlay);
|
|
23
|
+
video.addEventListener('pause', this.#handleStop);
|
|
24
|
+
video.addEventListener('ended', this.#handleStop);
|
|
25
|
+
video.addEventListener('seeked', this.#handleSeeked);
|
|
26
|
+
video.addEventListener('loadeddata', this.#handleLoadedData);
|
|
27
|
+
video.addEventListener('error', this.#handleError);
|
|
28
|
+
}
|
|
29
|
+
switchSource(snapshot) {
|
|
30
|
+
if (this.#disposed)
|
|
31
|
+
return;
|
|
32
|
+
const previous = this.#active;
|
|
33
|
+
if (previous &&
|
|
34
|
+
(previous.status === 'loading' || previous.status === 'settled') &&
|
|
35
|
+
previous.rawVideoUrl === snapshot.rawVideoUrl &&
|
|
36
|
+
previous.maskUrl === snapshot.maskUrl) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
this.#stopLoop();
|
|
40
|
+
this.#invalidatePlayRequest(previous);
|
|
41
|
+
const active = {
|
|
42
|
+
generation: ++this.#sourceGeneration,
|
|
43
|
+
status: 'loading',
|
|
44
|
+
rawVideoUrl: snapshot.rawVideoUrl,
|
|
45
|
+
normalizedVideoUrl: snapshot.resolvedVideoUrl,
|
|
46
|
+
maskUrl: snapshot.maskUrl,
|
|
47
|
+
chromaKey: copyChromaKey(snapshot.chromaKey),
|
|
48
|
+
mediaSettled: false,
|
|
49
|
+
maskSettled: !snapshot.maskUrl,
|
|
50
|
+
playRequestToken: 0,
|
|
51
|
+
playRequestPending: false,
|
|
52
|
+
playRejectedForCurrentIntent: false
|
|
53
|
+
};
|
|
54
|
+
this.#active = active;
|
|
55
|
+
this.#video.pause();
|
|
56
|
+
this.#renderer.setMask(null);
|
|
57
|
+
this.#renderer.clear();
|
|
58
|
+
this.#video.src = snapshot.resolvedVideoUrl;
|
|
59
|
+
// Reading the reflected property gives the browser-normalized absolute URL.
|
|
60
|
+
active.normalizedVideoUrl = this.#video.src;
|
|
61
|
+
this.#video.load();
|
|
62
|
+
this.#primeMedia();
|
|
63
|
+
if (snapshot.maskUrl)
|
|
64
|
+
this.#loadMask(active, snapshot.maskUrl);
|
|
65
|
+
}
|
|
66
|
+
setPlaying(playing) {
|
|
67
|
+
if (this.#disposed)
|
|
68
|
+
return;
|
|
69
|
+
const changed = this.#playing !== playing;
|
|
70
|
+
this.#playing = playing;
|
|
71
|
+
const active = this.#active;
|
|
72
|
+
if (!playing) {
|
|
73
|
+
this.#invalidatePlayRequest(active);
|
|
74
|
+
this.#stopLoop();
|
|
75
|
+
// Keep the muted loading warm-up alive until the browser has produced a
|
|
76
|
+
// decodable frame. Some engines treat preload="auto" as metadata-only and
|
|
77
|
+
// do not reach loadeddata until playback has started.
|
|
78
|
+
if (!active || active.status !== 'loading' || active.mediaSettled) {
|
|
79
|
+
this.#video.pause();
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (changed && active)
|
|
84
|
+
active.playRejectedForCurrentIntent = false;
|
|
85
|
+
this.#requestPlayback(active);
|
|
86
|
+
}
|
|
87
|
+
setChromaKey(parameters) {
|
|
88
|
+
if (this.#disposed)
|
|
89
|
+
return;
|
|
90
|
+
const active = this.#active;
|
|
91
|
+
if (!active || active.status === 'failed' || active.status === 'disposed')
|
|
92
|
+
return;
|
|
93
|
+
active.chromaKey = copyChromaKey(parameters);
|
|
94
|
+
if (active.status === 'settled' && this.#video.paused && this.#isCurrentResource(active)) {
|
|
95
|
+
this.#draw(active);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
dispose() {
|
|
99
|
+
if (this.#disposed)
|
|
100
|
+
return;
|
|
101
|
+
this.#disposed = true;
|
|
102
|
+
this.#sourceGeneration++;
|
|
103
|
+
const active = this.#active;
|
|
104
|
+
this.#invalidatePlayRequest(active);
|
|
105
|
+
this.#stopLoop();
|
|
106
|
+
if (active)
|
|
107
|
+
active.status = 'disposed';
|
|
108
|
+
this.#active = null;
|
|
109
|
+
this.#video.removeEventListener('play', this.#handlePlay);
|
|
110
|
+
this.#video.removeEventListener('pause', this.#handleStop);
|
|
111
|
+
this.#video.removeEventListener('ended', this.#handleStop);
|
|
112
|
+
this.#video.removeEventListener('seeked', this.#handleSeeked);
|
|
113
|
+
this.#video.removeEventListener('loadeddata', this.#handleLoadedData);
|
|
114
|
+
this.#video.removeEventListener('error', this.#handleError);
|
|
115
|
+
this.#video.pause();
|
|
116
|
+
}
|
|
117
|
+
#loadMask(active, maskUrl) {
|
|
118
|
+
const image = new Image();
|
|
119
|
+
image.src = maskUrl;
|
|
120
|
+
image
|
|
121
|
+
.decode()
|
|
122
|
+
.then(() => {
|
|
123
|
+
if (!this.#isActive(active) || active.status !== 'loading')
|
|
124
|
+
return;
|
|
125
|
+
this.#renderer.setMask(image);
|
|
126
|
+
active.maskSettled = true;
|
|
127
|
+
this.#settleIfReady(active);
|
|
128
|
+
})
|
|
129
|
+
.catch(() => {
|
|
130
|
+
if (!this.#isActive(active) || active.status !== 'loading')
|
|
131
|
+
return;
|
|
132
|
+
console.warn(`Failed to load chroma-key mask "${maskUrl}".`);
|
|
133
|
+
active.maskSettled = true;
|
|
134
|
+
this.#settleIfReady(active);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
#primeMedia() {
|
|
138
|
+
let request;
|
|
139
|
+
try {
|
|
140
|
+
request = this.#video.play();
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
Promise.resolve(request).catch(() => {
|
|
146
|
+
// Muted warm-up is best-effort. If preload still reaches loadeddata, the
|
|
147
|
+
// normal settled playback request below gets its own retry state.
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
#handleLoadedData = () => {
|
|
151
|
+
const active = this.#active;
|
|
152
|
+
if (!active || active.status !== 'loading')
|
|
153
|
+
return;
|
|
154
|
+
if (!this.#isCurrentResource(active))
|
|
155
|
+
return;
|
|
156
|
+
active.mediaSettled = true;
|
|
157
|
+
if (!this.#playing)
|
|
158
|
+
this.#video.pause();
|
|
159
|
+
this.#settleIfReady(active);
|
|
160
|
+
};
|
|
161
|
+
#handleError = () => {
|
|
162
|
+
const active = this.#active;
|
|
163
|
+
if (!active || active.status === 'failed' || active.status === 'disposed')
|
|
164
|
+
return;
|
|
165
|
+
active.status = 'failed';
|
|
166
|
+
this.#invalidatePlayRequest(active);
|
|
167
|
+
this.#stopLoop();
|
|
168
|
+
this.#video.pause();
|
|
169
|
+
this.#renderer.clear();
|
|
170
|
+
};
|
|
171
|
+
#handlePlay = () => {
|
|
172
|
+
const active = this.#active;
|
|
173
|
+
// Loading playback only warms the hidden media element up to loadeddata.
|
|
174
|
+
// Drawing and the frame loop remain gated on video + mask settlement.
|
|
175
|
+
if (active?.status === 'loading')
|
|
176
|
+
return;
|
|
177
|
+
if (!active || active.status !== 'settled') {
|
|
178
|
+
this.#stopLoop();
|
|
179
|
+
this.#video.pause();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
active.playRejectedForCurrentIntent = false;
|
|
183
|
+
if (!this.#video.paused)
|
|
184
|
+
this.#startLoop(active);
|
|
185
|
+
};
|
|
186
|
+
#handleStop = () => {
|
|
187
|
+
const active = this.#active;
|
|
188
|
+
this.#stopLoop();
|
|
189
|
+
if (active?.status === 'settled')
|
|
190
|
+
this.#draw(active);
|
|
191
|
+
};
|
|
192
|
+
#handleSeeked = () => {
|
|
193
|
+
const active = this.#active;
|
|
194
|
+
if (!active || active.status !== 'settled' || !this.#video.paused)
|
|
195
|
+
return;
|
|
196
|
+
this.#draw(active);
|
|
197
|
+
};
|
|
198
|
+
#settleIfReady(active) {
|
|
199
|
+
if (!this.#isActive(active) ||
|
|
200
|
+
active.status !== 'loading' ||
|
|
201
|
+
!active.mediaSettled ||
|
|
202
|
+
!active.maskSettled) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (!this.#isCurrentResource(active)) {
|
|
206
|
+
active.mediaSettled = false;
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (!this.#playing && !this.#video.paused)
|
|
210
|
+
this.#video.pause();
|
|
211
|
+
active.status = 'settled';
|
|
212
|
+
this.#draw(active);
|
|
213
|
+
if (this.#playing && !this.#video.paused)
|
|
214
|
+
this.#startLoop(active);
|
|
215
|
+
else
|
|
216
|
+
this.#requestPlayback(active);
|
|
217
|
+
}
|
|
218
|
+
#draw(active) {
|
|
219
|
+
if (!this.#isActive(active) ||
|
|
220
|
+
active.status !== 'settled' ||
|
|
221
|
+
!this.#isCurrentResource(active)) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
this.#renderer.draw(this.#video, active.chromaKey);
|
|
225
|
+
}
|
|
226
|
+
#isCurrentResource(active) {
|
|
227
|
+
return (this.#video.readyState >= HAVE_CURRENT_DATA &&
|
|
228
|
+
this.#video.currentSrc === active.normalizedVideoUrl);
|
|
229
|
+
}
|
|
230
|
+
#isActive(active) {
|
|
231
|
+
return (!this.#disposed && this.#active === active && active.generation === this.#sourceGeneration);
|
|
232
|
+
}
|
|
233
|
+
#requestPlayback(active) {
|
|
234
|
+
if (!active ||
|
|
235
|
+
!this.#isActive(active) ||
|
|
236
|
+
active.status !== 'settled' ||
|
|
237
|
+
!this.#playing ||
|
|
238
|
+
!this.#video.paused ||
|
|
239
|
+
active.playRequestPending ||
|
|
240
|
+
active.playRejectedForCurrentIntent) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const requestToken = ++active.playRequestToken;
|
|
244
|
+
active.playRequestPending = true;
|
|
245
|
+
let request;
|
|
246
|
+
try {
|
|
247
|
+
request = this.#video.play();
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
this.#finishPlayRequest(active, requestToken, false);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
Promise.resolve(request).then(() => this.#finishPlayRequest(active, requestToken, true), () => this.#finishPlayRequest(active, requestToken, false));
|
|
254
|
+
}
|
|
255
|
+
#finishPlayRequest(active, requestToken, succeeded) {
|
|
256
|
+
if (!this.#isActive(active) || active.playRequestToken !== requestToken)
|
|
257
|
+
return;
|
|
258
|
+
active.playRequestPending = false;
|
|
259
|
+
active.playRejectedForCurrentIntent = !succeeded;
|
|
260
|
+
}
|
|
261
|
+
#invalidatePlayRequest(active) {
|
|
262
|
+
if (!active)
|
|
263
|
+
return;
|
|
264
|
+
active.playRequestToken++;
|
|
265
|
+
active.playRequestPending = false;
|
|
266
|
+
}
|
|
267
|
+
#startLoop(active) {
|
|
268
|
+
if (this.#loopRunning ||
|
|
269
|
+
!this.#isActive(active) ||
|
|
270
|
+
active.status !== 'settled' ||
|
|
271
|
+
this.#video.paused) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
this.#loopRunning = true;
|
|
275
|
+
this.#loopGeneration++;
|
|
276
|
+
this.#scheduleNextFrame(active, this.#loopGeneration);
|
|
277
|
+
}
|
|
278
|
+
#scheduleNextFrame(active, loopGeneration) {
|
|
279
|
+
const sourceGeneration = active.generation;
|
|
280
|
+
let handle = 0;
|
|
281
|
+
const callback = () => {
|
|
282
|
+
const pending = this.#pendingFrame;
|
|
283
|
+
if (!pending ||
|
|
284
|
+
pending.generation !== loopGeneration ||
|
|
285
|
+
pending.handle !== handle ||
|
|
286
|
+
this.#loopGeneration !== loopGeneration ||
|
|
287
|
+
!this.#loopRunning ||
|
|
288
|
+
!this.#isActive(active) ||
|
|
289
|
+
active.generation !== sourceGeneration ||
|
|
290
|
+
active.status !== 'settled' ||
|
|
291
|
+
this.#video.paused) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
this.#pendingFrame = null;
|
|
295
|
+
this.#draw(active);
|
|
296
|
+
if (this.#loopRunning &&
|
|
297
|
+
this.#loopGeneration === loopGeneration &&
|
|
298
|
+
this.#isActive(active) &&
|
|
299
|
+
!this.#video.paused) {
|
|
300
|
+
this.#scheduleNextFrame(active, loopGeneration);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
if (this.#video.requestVideoFrameCallback) {
|
|
304
|
+
handle = this.#video.requestVideoFrameCallback(callback);
|
|
305
|
+
this.#pendingFrame = { generation: loopGeneration, kind: 'rvfc', handle };
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
handle = requestAnimationFrame(callback);
|
|
309
|
+
this.#pendingFrame = { generation: loopGeneration, kind: 'raf', handle };
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
#stopLoop() {
|
|
313
|
+
this.#loopGeneration++;
|
|
314
|
+
this.#loopRunning = false;
|
|
315
|
+
const pending = this.#pendingFrame;
|
|
316
|
+
this.#pendingFrame = null;
|
|
317
|
+
if (!pending)
|
|
318
|
+
return;
|
|
319
|
+
if (pending.kind === 'rvfc')
|
|
320
|
+
this.#video.cancelVideoFrameCallback?.(pending.handle);
|
|
321
|
+
else
|
|
322
|
+
cancelAnimationFrame(pending.handle);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
Plays a greenscreen video keyed onto a transparent canvas. Video and mask
|
|
3
|
-
sources are read at mount; keying parameters are read for every frame.
|
|
4
|
-
-->
|
|
1
|
+
<!-- Plays a greenscreen video whose source, mask, playback and keying can change in place. -->
|
|
5
2
|
<script lang="ts">
|
|
6
|
-
import { onMount, type Snippet } from 'svelte';
|
|
7
|
-
import type { ChromaKeyVideoManifestEntry } from '../../core/index.js';
|
|
3
|
+
import { onMount, untrack, type Snippet } from 'svelte';
|
|
4
|
+
import type { ChromaKeyParameters, ChromaKeyVideoManifestEntry } from '../../core/index.js';
|
|
8
5
|
import { ChromaKeyGl } from '../chroma-key-gl.js';
|
|
6
|
+
import { ChromaKeyVideoLifecycle } from '../chroma-key-video-lifecycle.js';
|
|
9
7
|
import { resolveRetainedSource } from '../preload-registry.js';
|
|
10
8
|
|
|
11
9
|
interface Props {
|
|
@@ -38,57 +36,10 @@
|
|
|
38
36
|
let webglUnavailable = $state(false);
|
|
39
37
|
|
|
40
38
|
let renderer: ChromaKeyGl | null = null;
|
|
39
|
+
let lifecycle = $state.raw<ChromaKeyVideoLifecycle | null>(null);
|
|
41
40
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
// frame — rAF is the fallback for browsers without it.
|
|
45
|
-
let frameHandle = 0;
|
|
46
|
-
let loopRunning = false;
|
|
47
|
-
|
|
48
|
-
function drawCurrentFrame() {
|
|
49
|
-
const video = videoElement;
|
|
50
|
-
if (renderer && video && video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
|
|
51
|
-
renderer.draw(video, src.chromaKey);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function onFrame() {
|
|
56
|
-
drawCurrentFrame();
|
|
57
|
-
if (loopRunning) scheduleNextFrame();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function scheduleNextFrame() {
|
|
61
|
-
const video = videoElement!;
|
|
62
|
-
frameHandle = video.requestVideoFrameCallback
|
|
63
|
-
? video.requestVideoFrameCallback(onFrame)
|
|
64
|
-
: requestAnimationFrame(onFrame);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function startLoop() {
|
|
68
|
-
if (loopRunning) return;
|
|
69
|
-
loopRunning = true;
|
|
70
|
-
scheduleNextFrame();
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function stopLoop() {
|
|
74
|
-
if (!loopRunning) return;
|
|
75
|
-
loopRunning = false;
|
|
76
|
-
const video = videoElement;
|
|
77
|
-
if (video?.cancelVideoFrameCallback) video.cancelVideoFrameCallback(frameHandle);
|
|
78
|
-
else cancelAnimationFrame(frameHandle);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Playback control only starts/stops the element; the render loop follows the
|
|
82
|
-
// element's actual `play`/`pause` events (see the listener effect below), so it
|
|
83
|
-
// stops correctly no matter who pauses — the `playing` prop OR a host driving
|
|
84
|
-
// the bound element directly (e.g. the editor's PlaybackControls scrubber).
|
|
85
|
-
function resumePlayback() {
|
|
86
|
-
if (!playing || webglUnavailable) return;
|
|
87
|
-
videoElement?.play().catch(() => {});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function pausePlayback() {
|
|
91
|
-
videoElement?.pause();
|
|
41
|
+
function snapshotChromaKey(key: ChromaKeyParameters): ChromaKeyParameters {
|
|
42
|
+
return { ...key, keyColor: [...key.keyColor] };
|
|
92
43
|
}
|
|
93
44
|
|
|
94
45
|
onMount(() => {
|
|
@@ -98,83 +49,47 @@
|
|
|
98
49
|
return;
|
|
99
50
|
}
|
|
100
51
|
const activeRenderer = renderer;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (maskSource) {
|
|
104
|
-
const image = new Image();
|
|
105
|
-
image.src = maskSource;
|
|
106
|
-
image
|
|
107
|
-
.decode()
|
|
108
|
-
.then(() => {
|
|
109
|
-
if (renderer === activeRenderer) activeRenderer.setMask(image);
|
|
110
|
-
})
|
|
111
|
-
.catch(() => {
|
|
112
|
-
console.warn(`Failed to load chroma-key mask "${maskSource}".`);
|
|
113
|
-
});
|
|
114
|
-
}
|
|
52
|
+
const activeLifecycle = new ChromaKeyVideoLifecycle(videoElement!, activeRenderer);
|
|
53
|
+
lifecycle = activeLifecycle;
|
|
115
54
|
|
|
116
55
|
return () => {
|
|
117
|
-
|
|
56
|
+
activeLifecycle.dispose();
|
|
57
|
+
if (lifecycle === activeLifecycle) lifecycle = null;
|
|
118
58
|
activeRenderer.dispose();
|
|
119
|
-
renderer = null;
|
|
59
|
+
if (renderer === activeRenderer) renderer = null;
|
|
120
60
|
};
|
|
121
61
|
});
|
|
122
62
|
|
|
123
|
-
// The render loop tracks the element's real playback state: play starts it,
|
|
124
|
-
// pause/ended stop it (so a paused instance costs nothing, even on the rAF
|
|
125
|
-
// fallback that fires regardless of playback). A seek while paused repaints the
|
|
126
|
-
// single new frame so scrubbing updates the canvas without running the loop.
|
|
127
63
|
$effect(() => {
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
video.addEventListener('play', onPlay);
|
|
141
|
-
video.addEventListener('pause', onStop);
|
|
142
|
-
video.addEventListener('ended', onStop);
|
|
143
|
-
video.addEventListener('seeked', onSeeked);
|
|
144
|
-
return () => {
|
|
145
|
-
video.removeEventListener('play', onPlay);
|
|
146
|
-
video.removeEventListener('pause', onStop);
|
|
147
|
-
video.removeEventListener('ended', onStop);
|
|
148
|
-
video.removeEventListener('seeked', onSeeked);
|
|
149
|
-
};
|
|
64
|
+
const activeLifecycle = lifecycle;
|
|
65
|
+
const entry = src;
|
|
66
|
+
const rawVideoUrl = entry.src;
|
|
67
|
+
const maskUrl = entry.mask?.src;
|
|
68
|
+
if (!activeLifecycle) return;
|
|
69
|
+
activeLifecycle.switchSource({
|
|
70
|
+
rawVideoUrl,
|
|
71
|
+
resolvedVideoUrl: resolveRetainedSource(rawVideoUrl),
|
|
72
|
+
maskUrl,
|
|
73
|
+
chromaKey: untrack(() => snapshotChromaKey(entry.chromaKey))
|
|
74
|
+
});
|
|
150
75
|
});
|
|
151
76
|
|
|
152
|
-
// Drive playback from `playing` (runs after onMount, so the renderer exists —
|
|
153
|
-
// or WebGL is known to be unavailable and nothing starts).
|
|
154
77
|
$effect(() => {
|
|
155
|
-
|
|
156
|
-
if (playing) resumePlayback();
|
|
157
|
-
else pausePlayback();
|
|
78
|
+
lifecycle?.setPlaying(playing);
|
|
158
79
|
});
|
|
159
80
|
|
|
160
|
-
// Retune a still frame live: the draw loop only runs during playback, so a
|
|
161
|
-
// keying change while paused (the editor tuning a bubble on a still frame)
|
|
162
|
-
// would otherwise leave the canvas stale until the next play/seek. Reading the
|
|
163
|
-
// keying fields here makes this a reactive dependency; when the loop is already
|
|
164
|
-
// running its per-frame draw handles it, so only repaint when it isn't.
|
|
165
81
|
$effect(() => {
|
|
166
82
|
const key = src.chromaKey;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
key.
|
|
170
|
-
key.
|
|
171
|
-
key.
|
|
172
|
-
key.
|
|
173
|
-
key.
|
|
174
|
-
key.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (!loopRunning) drawCurrentFrame();
|
|
83
|
+
const snapshot: ChromaKeyParameters = {
|
|
84
|
+
keyColor: [key.keyColor[0], key.keyColor[1], key.keyColor[2]],
|
|
85
|
+
colorCutoff: key.colorCutoff,
|
|
86
|
+
colorFeathering: key.colorFeathering,
|
|
87
|
+
maskFeathering: key.maskFeathering,
|
|
88
|
+
sharpening: key.sharpening,
|
|
89
|
+
despill: key.despill,
|
|
90
|
+
despillLuminanceAdd: key.despillLuminanceAdd
|
|
91
|
+
};
|
|
92
|
+
lifecycle?.setChromaKey(snapshot);
|
|
178
93
|
});
|
|
179
94
|
</script>
|
|
180
95
|
|
|
@@ -187,14 +102,7 @@
|
|
|
187
102
|
{/if}
|
|
188
103
|
{:else}
|
|
189
104
|
<canvas bind:this={canvasElement}></canvas>
|
|
190
|
-
<video
|
|
191
|
-
bind:this={videoElement}
|
|
192
|
-
src={resolveRetainedSource(src.src)}
|
|
193
|
-
{loop}
|
|
194
|
-
muted
|
|
195
|
-
playsinline
|
|
196
|
-
preload="auto"
|
|
197
|
-
></video>
|
|
105
|
+
<video bind:this={videoElement} {loop} muted playsinline preload="auto"></video>
|
|
198
106
|
{/if}
|
|
199
107
|
</div>
|
|
200
108
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { AudioPreset, FigmaImportScale, ImagePreset,
|
|
1
|
+
import type { AudioPreset, FigmaImportScale, ImagePreset, ResolvedVideoPreset } from '../core/index.js';
|
|
2
2
|
import type { ServerPluginRegistry } from './plugins/contract.js';
|
|
3
3
|
export interface ResolvedPresetPluginConfig {
|
|
4
4
|
presets: Record<string, ImagePreset>;
|
|
5
5
|
}
|
|
6
6
|
export interface ResolvedVideoPluginConfig {
|
|
7
|
-
presets: Record<string,
|
|
7
|
+
presets: Record<string, ResolvedVideoPreset>;
|
|
8
8
|
/** Absolute path of the ffmpeg binary to use; null = the bundled `ffmpeg-static`. */
|
|
9
9
|
ffmpegPath: string | null;
|
|
10
10
|
/** Absolute path of the ffprobe binary to use; null = the bundled `ffprobe-static`. */
|
|
@@ -767,8 +767,12 @@ export class FundusOperations {
|
|
|
767
767
|
// Per plugin type, the available preset names — straight off the registry
|
|
768
768
|
// Chroma-key-video shares the video preset namespace via its plugin.
|
|
769
769
|
const presetNames = {};
|
|
770
|
+
const presetCapabilities = {};
|
|
770
771
|
for (const [type, plugin] of Object.entries(this.#registry)) {
|
|
771
|
-
|
|
772
|
+
const presets = plugin.presets(this.#config);
|
|
773
|
+
const names = Object.keys(presets).sort();
|
|
774
|
+
presetNames[type] = names;
|
|
775
|
+
presetCapabilities[type] = Object.fromEntries(names.map((name) => [name, plugin.presetCapabilities?.(presets[name]) ?? {}]));
|
|
772
776
|
}
|
|
773
777
|
const rawIngestionQueue = this.#buildRawIngestionQueue(library, classification);
|
|
774
778
|
return {
|
|
@@ -778,6 +782,7 @@ export class FundusOperations {
|
|
|
778
782
|
importDefaults: { figma: { scale: this.#config.import.figma.defaults.scale } },
|
|
779
783
|
issues,
|
|
780
784
|
presetNames,
|
|
785
|
+
presetCapabilities,
|
|
781
786
|
manifestMemberships,
|
|
782
787
|
manifestDeliveredBytes,
|
|
783
788
|
rawIngestionQueue
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
* mask/fallback references never reach ffmpeg, they are entry-only data
|
|
5
5
|
* (the descriptor's `processingParameters` keeps them out of the job hash).
|
|
6
6
|
*/
|
|
7
|
-
import { type ChromaKeyVideoParameters, type
|
|
7
|
+
import { type ChromaKeyVideoParameters, type ResolvedVideoPreset } from '../../core/index.js';
|
|
8
8
|
import { type ServerAssetTypePlugin } from './contract.js';
|
|
9
|
-
export declare const chromaKeyVideoServerPlugin: ServerAssetTypePlugin<ChromaKeyVideoParameters,
|
|
9
|
+
export declare const chromaKeyVideoServerPlugin: ServerAssetTypePlugin<ChromaKeyVideoParameters, ResolvedVideoPreset>;
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
* mask/fallback references never reach ffmpeg, they are entry-only data
|
|
5
5
|
* (the descriptor's `processingParameters` keeps them out of the job hash).
|
|
6
6
|
*/
|
|
7
|
-
import { chromaKeyVideoPlugin } from '../../core/index.js';
|
|
7
|
+
import { chromaKeyVideoPlugin, isCustomFfmpegVideoPreset } from '../../core/index.js';
|
|
8
8
|
import { probeMedia } from '../processing/media-probe.js';
|
|
9
9
|
import { processVideoFile } from '../processing/video-processing.js';
|
|
10
|
-
import { requireProxyField
|
|
11
|
-
import { videoConfigDefinition } from './video.js';
|
|
10
|
+
import { requireProxyField } from './contract.js';
|
|
11
|
+
import { resolveVideoPreset, videoConfigDefinition } from './video.js';
|
|
12
12
|
export const chromaKeyVideoServerPlugin = {
|
|
13
13
|
descriptor: chromaKeyVideoPlugin,
|
|
14
14
|
configDefinitions: [videoConfigDefinition],
|
|
@@ -16,7 +16,8 @@ export const chromaKeyVideoServerPlugin = {
|
|
|
16
16
|
// the preset map, and the resolution label are video's.
|
|
17
17
|
pluginConfig: (config) => config.plugins.video,
|
|
18
18
|
presets: (config) => config.plugins.video.presets,
|
|
19
|
-
|
|
19
|
+
presetCapabilities: (preset) => ({ maxWidth: !isCustomFfmpegVideoPreset(preset) }),
|
|
20
|
+
resolvePreset: resolveVideoPreset,
|
|
20
21
|
proxyExtension: (preset) => preset.container,
|
|
21
22
|
process: (job) => processVideoFile({
|
|
22
23
|
sourcePath: job.sourcePath,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* (processor, codegen, check, store, operations) are type-agnostic and consult
|
|
7
7
|
* only the projected server registry (plugins.ts).
|
|
8
8
|
*/
|
|
9
|
-
import type { AssetTypePluginDescriptor, ManifestEntryData } from '../../core/index.js';
|
|
9
|
+
import type { AssetTypePluginDescriptor, ManifestEntryData, PresetCapabilitiesDto } from '../../core/index.js';
|
|
10
10
|
import type { ResolvedFundusConfig } from '../config-loader.js';
|
|
11
11
|
/** Output metadata of one processed proxy file (absent fields don't apply). */
|
|
12
12
|
export interface ProxyOutputInfo {
|
|
@@ -72,6 +72,8 @@ export interface ServerAssetTypePlugin<TParameters = unknown, TPreset = unknown>
|
|
|
72
72
|
pluginConfig(config: ResolvedFundusConfig): unknown;
|
|
73
73
|
/** The merged (built-in + host) named presets available to this type. */
|
|
74
74
|
presets(config: ResolvedFundusConfig): Record<string, TPreset>;
|
|
75
|
+
/** Optional editor behavior exposed for each resolved preset. */
|
|
76
|
+
presetCapabilities?(preset: TPreset): PresetCapabilitiesDto;
|
|
75
77
|
/**
|
|
76
78
|
* The resolved settings of the preset the validated parameters name —
|
|
77
79
|
* these feed the job hash (a preset edit under an unchanged name
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Video plugin (server part): one ffmpeg transcode per preset.
|
|
3
3
|
*/
|
|
4
|
-
import { type VideoParameters, type
|
|
4
|
+
import { type VideoParameters, type ResolvedVideoPreset } from '../../core/index.js';
|
|
5
|
+
import type { ResolvedFundusConfig } from '../config-loader.js';
|
|
5
6
|
import { type ServerAssetTypePlugin, type ServerPluginConfigDefinition } from './contract.js';
|
|
6
7
|
export declare const videoConfigDefinition: ServerPluginConfigDefinition;
|
|
7
|
-
export declare
|
|
8
|
+
export declare function resolveVideoPreset(parameters: VideoParameters, config: ResolvedFundusConfig): ResolvedVideoPreset;
|
|
9
|
+
export declare const videoServerPlugin: ServerAssetTypePlugin<VideoParameters, ResolvedVideoPreset>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Video plugin (server part): one ffmpeg transcode per preset.
|
|
3
3
|
*/
|
|
4
|
-
import { VIDEO_BUILT_IN_PRESETS, validateVideoPreset, videoPlugin } from '../../core/index.js';
|
|
4
|
+
import { VIDEO_BUILT_IN_PRESETS, isCustomFfmpegVideoPreset, validateVideoPreset, videoPlugin } from '../../core/index.js';
|
|
5
5
|
import { probeMedia } from '../processing/media-probe.js';
|
|
6
6
|
import { processVideoFile } from '../processing/video-processing.js';
|
|
7
7
|
import { binaryPathOverride, mergePresets } from './config.js';
|
|
@@ -29,12 +29,20 @@ export const videoConfigDefinition = {
|
|
|
29
29
|
})
|
|
30
30
|
})
|
|
31
31
|
};
|
|
32
|
+
export function resolveVideoPreset(parameters, config) {
|
|
33
|
+
const preset = resolveNamedPreset('video', parameters.preset, config.plugins.video.presets);
|
|
34
|
+
if (isCustomFfmpegVideoPreset(preset) && parameters.maxWidth !== undefined) {
|
|
35
|
+
throw new Error(`Video preset "${parameters.preset}" uses "ffmpegArgs" and cannot be combined with "maxWidth".`);
|
|
36
|
+
}
|
|
37
|
+
return preset;
|
|
38
|
+
}
|
|
32
39
|
export const videoServerPlugin = {
|
|
33
40
|
descriptor: videoPlugin,
|
|
34
41
|
configDefinitions: [videoConfigDefinition],
|
|
35
42
|
pluginConfig: (config) => config.plugins.video,
|
|
36
43
|
presets: (config) => config.plugins.video.presets,
|
|
37
|
-
|
|
44
|
+
presetCapabilities: (preset) => ({ maxWidth: !isCustomFfmpegVideoPreset(preset) }),
|
|
45
|
+
resolvePreset: resolveVideoPreset,
|
|
38
46
|
proxyExtension: (preset) => preset.container,
|
|
39
47
|
process: (job) => processVideoFile({
|
|
40
48
|
sourcePath: job.sourcePath,
|