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.
Files changed (183) hide show
  1. package/README.md +40 -1
  2. package/build/client/_app/immutable/chunks/{BEbRZpHw.js → BJF4m1Bj.js} +1 -1
  3. package/build/client/_app/immutable/chunks/BJF4m1Bj.js.br +0 -0
  4. package/build/client/_app/immutable/chunks/BJF4m1Bj.js.gz +0 -0
  5. package/build/client/_app/immutable/chunks/{BVDR6adL.js → C3HGm51f.js} +1 -1
  6. package/build/client/_app/immutable/chunks/C3HGm51f.js.br +0 -0
  7. package/build/client/_app/immutable/chunks/C3HGm51f.js.gz +0 -0
  8. package/build/client/_app/immutable/chunks/{4FCfXqUa.js → C7OUKWnm.js} +1 -1
  9. package/build/client/_app/immutable/chunks/C7OUKWnm.js.br +0 -0
  10. package/build/client/_app/immutable/chunks/C7OUKWnm.js.gz +0 -0
  11. package/build/client/_app/immutable/chunks/CrhgzsNe.js +1 -0
  12. package/build/client/_app/immutable/chunks/CrhgzsNe.js.br +0 -0
  13. package/build/client/_app/immutable/chunks/CrhgzsNe.js.gz +0 -0
  14. package/build/client/_app/immutable/chunks/DSWdnTh9.js +108 -0
  15. package/build/client/_app/immutable/chunks/DSWdnTh9.js.br +0 -0
  16. package/build/client/_app/immutable/chunks/DSWdnTh9.js.gz +0 -0
  17. package/build/client/_app/immutable/entry/{app.CH6AvWYr.js → app.BSLYJbsx.js} +2 -2
  18. package/build/client/_app/immutable/entry/app.BSLYJbsx.js.br +0 -0
  19. package/build/client/_app/immutable/entry/app.BSLYJbsx.js.gz +0 -0
  20. package/build/client/_app/immutable/entry/start.BvNZNOOV.js +1 -0
  21. package/build/client/_app/immutable/entry/start.BvNZNOOV.js.br +0 -0
  22. package/build/client/_app/immutable/entry/start.BvNZNOOV.js.gz +0 -0
  23. package/build/client/_app/immutable/nodes/{0.bYfuwnNW.js → 0.cLtfz2y2.js} +1 -1
  24. package/build/client/_app/immutable/nodes/0.cLtfz2y2.js.br +0 -0
  25. package/build/client/_app/immutable/nodes/0.cLtfz2y2.js.gz +0 -0
  26. package/build/client/_app/immutable/nodes/{1.kv-uSGjV.js → 1.tMVPJLEm.js} +1 -1
  27. package/build/client/_app/immutable/nodes/1.tMVPJLEm.js.br +3 -0
  28. package/build/client/_app/immutable/nodes/1.tMVPJLEm.js.gz +0 -0
  29. package/build/client/_app/immutable/nodes/2.TBMdq9ab.js +3 -0
  30. package/build/client/_app/immutable/nodes/2.TBMdq9ab.js.br +0 -0
  31. package/build/client/_app/immutable/nodes/2.TBMdq9ab.js.gz +0 -0
  32. package/build/client/_app/immutable/nodes/3.DDoGfLgu.js +1 -0
  33. package/build/client/_app/immutable/nodes/3.DDoGfLgu.js.br +0 -0
  34. package/build/client/_app/immutable/nodes/3.DDoGfLgu.js.gz +0 -0
  35. package/build/client/_app/immutable/nodes/{4.DXitiaKH.js → 4.UrhL5n-g.js} +1 -1
  36. package/build/client/_app/immutable/nodes/4.UrhL5n-g.js.br +0 -0
  37. package/build/client/_app/immutable/nodes/4.UrhL5n-g.js.gz +0 -0
  38. package/build/client/_app/version.json +1 -1
  39. package/build/client/_app/version.json.br +0 -0
  40. package/build/client/_app/version.json.gz +0 -0
  41. package/build/handler.js +7 -7
  42. package/build/index.js +7 -7
  43. package/build/server/chunks/chunks/{context.js-CPdlcRhV.js → context.js-mXbNUDBD.js} +13 -6
  44. package/build/server/chunks/chunks/context.js-mXbNUDBD.js.map +1 -0
  45. package/build/server/chunks/chunks/{contract.js-nwG99Zzj.js → contract.js-BCXeXDmU.js} +29 -13
  46. package/build/server/chunks/chunks/contract.js-BCXeXDmU.js.map +1 -0
  47. package/build/server/chunks/chunks/{core.js-DNIuZ9y-.js → core.js-Cj7hjmtV.js} +49 -2
  48. package/build/server/chunks/chunks/core.js-Cj7hjmtV.js.map +1 -0
  49. package/build/server/chunks/chunks/{exports.js-Ci6jkE-z.js → exports.js-DFO7C1N3.js} +2 -2
  50. package/build/server/chunks/chunks/{exports.js-Ci6jkE-z.js.map → exports.js-DFO7C1N3.js.map} +1 -1
  51. package/build/server/chunks/chunks/{http.js-DmUA2gRk.js → http.js-BHghHRmI.js} +2 -2
  52. package/build/server/chunks/chunks/{http.js-DmUA2gRk.js.map → http.js-BHghHRmI.js.map} +1 -1
  53. package/build/server/chunks/chunks/{internal.js-DWu3OCGw.js → internal.js-Du8xenh_.js} +3 -3
  54. package/build/server/chunks/chunks/{internal.js-DWu3OCGw.js.map → internal.js-Du8xenh_.js.map} +1 -1
  55. package/build/server/chunks/chunks/internal2.js-BlXt2rfE.js.map +1 -1
  56. package/build/server/chunks/chunks/{manifest-membership.js-CJnQXrL4.js → manifest-membership.js-vIb-KkQi.js} +2 -2
  57. package/build/server/chunks/chunks/{manifest-membership.js-CJnQXrL4.js.map → manifest-membership.js-vIb-KkQi.js.map} +1 -1
  58. package/build/server/chunks/chunks/{raw-reconciliation.js-_D9lZ_sc.js → raw-reconciliation.js-CeIOAYCA.js} +3 -3
  59. package/build/server/chunks/chunks/raw-reconciliation.js-CeIOAYCA.js.map +1 -0
  60. package/build/server/chunks/chunks/{raw-reconciliation2.js-C2No1QIL.js → raw-reconciliation2.js-5mUCDiJk.js} +2 -2
  61. package/build/server/chunks/chunks/{raw-reconciliation2.js-C2No1QIL.js.map → raw-reconciliation2.js-5mUCDiJk.js.map} +1 -1
  62. package/build/server/chunks/chunks/{shared.js-BIggwEH0.js → shared.js-CNSYTfB6.js} +2 -2
  63. package/build/server/chunks/chunks/{shared.js-BIggwEH0.js.map → shared.js-CNSYTfB6.js.map} +1 -1
  64. package/build/server/chunks/chunks/{utils.js-Dxvg75XM.js → utils.js-ATk6U-az.js} +3 -3
  65. package/build/server/chunks/chunks/{utils.js-Dxvg75XM.js.map → utils.js-ATk6U-az.js.map} +1 -1
  66. package/build/server/chunks/entries/endpoints/api/_...path_/{_server.ts.js-wQD5tBuh.js → _server.ts.js-CN1hcfLb.js} +6 -6
  67. package/build/server/chunks/entries/endpoints/api/_...path_/{_server.ts.js-wQD5tBuh.js.map → _server.ts.js-CN1hcfLb.js.map} +1 -1
  68. package/build/server/chunks/entries/endpoints/api/assets/_id_/{_server.ts.js-CLCx81uK.js → _server.ts.js-D_LU4nrh.js} +7 -7
  69. package/build/server/chunks/entries/endpoints/api/assets/_id_/{_server.ts.js-CLCx81uK.js.map → _server.ts.js-D_LU4nrh.js.map} +1 -1
  70. package/build/server/chunks/entries/endpoints/api/assets/_id_/raw/{_server.ts.js-DkvELcZA.js → _server.ts.js-BY9NgKmC.js} +7 -7
  71. package/build/server/chunks/entries/endpoints/api/assets/_id_/raw/{_server.ts.js-DkvELcZA.js.map → _server.ts.js-BY9NgKmC.js.map} +1 -1
  72. package/build/server/chunks/entries/endpoints/api/assets/_id_/reimport/{_server.ts.js-CCNcWQxH.js → _server.ts.js-D2uPmOzD.js} +7 -7
  73. package/build/server/chunks/entries/endpoints/api/assets/_id_/reimport/{_server.ts.js-CCNcWQxH.js.map → _server.ts.js-D2uPmOzD.js.map} +1 -1
  74. package/build/server/chunks/entries/endpoints/api/assets/{_server.ts.js-DUK4Rnpu.js → _server.ts.js-CCsQkuSA.js} +7 -7
  75. package/build/server/chunks/entries/endpoints/api/assets/{_server.ts.js-DUK4Rnpu.js.map → _server.ts.js-CCsQkuSA.js.map} +1 -1
  76. package/build/server/chunks/entries/endpoints/api/assets/delete/{_server.ts.js-DNgHnoga.js → _server.ts.js-CFVRCJTe.js} +7 -7
  77. package/build/server/chunks/entries/endpoints/api/assets/delete/{_server.ts.js-DNgHnoga.js.map → _server.ts.js-CFVRCJTe.js.map} +1 -1
  78. package/build/server/chunks/entries/endpoints/api/assets/move/{_server.ts.js-gdXko55o.js → _server.ts.js-DTDSQlpi.js} +7 -7
  79. package/build/server/chunks/entries/endpoints/api/assets/move/{_server.ts.js-gdXko55o.js.map → _server.ts.js-DTDSQlpi.js.map} +1 -1
  80. package/build/server/chunks/entries/endpoints/api/folders/_path_/{_server.ts.js-DcsBk_jx.js → _server.ts.js-DrunH7kn.js} +7 -7
  81. package/build/server/chunks/entries/endpoints/api/folders/_path_/{_server.ts.js-DcsBk_jx.js.map → _server.ts.js-DrunH7kn.js.map} +1 -1
  82. package/build/server/chunks/entries/endpoints/api/folders/{_server.ts.js-BhTg6GgC.js → _server.ts.js-jskTD8nZ.js} +7 -7
  83. package/build/server/chunks/entries/endpoints/api/folders/{_server.ts.js-BhTg6GgC.js.map → _server.ts.js-jskTD8nZ.js.map} +1 -1
  84. package/build/server/chunks/entries/endpoints/api/imports/{_server.ts.js-CKjnRFXQ.js → _server.ts.js-DEXkS6pQ.js} +7 -7
  85. package/build/server/chunks/entries/endpoints/api/imports/{_server.ts.js-CKjnRFXQ.js.map → _server.ts.js-DEXkS6pQ.js.map} +1 -1
  86. package/build/server/chunks/entries/endpoints/api/manifests/_name_/{_server.ts.js-C9rdzmoj.js → _server.ts.js-baLtBCf4.js} +7 -7
  87. package/build/server/chunks/entries/endpoints/api/manifests/_name_/{_server.ts.js-C9rdzmoj.js.map → _server.ts.js-baLtBCf4.js.map} +1 -1
  88. package/build/server/chunks/entries/endpoints/api/manifests/{_server.ts.js-BdZb4d69.js → _server.ts.js-1Cx7sbfQ.js} +7 -7
  89. package/build/server/chunks/entries/endpoints/api/manifests/{_server.ts.js-BdZb4d69.js.map → _server.ts.js-1Cx7sbfQ.js.map} +1 -1
  90. package/build/server/chunks/entries/endpoints/api/raw/adopt/{_server.ts.js-DaTgBGGt.js → _server.ts.js-Xc7glEPl.js} +7 -7
  91. package/build/server/chunks/entries/endpoints/api/raw/adopt/{_server.ts.js-DaTgBGGt.js.map → _server.ts.js-Xc7glEPl.js.map} +1 -1
  92. package/build/server/chunks/entries/endpoints/api/raw/edited/accept/{_server.ts.js-Btohjiz4.js → _server.ts.js-C7IxpaSj.js} +7 -7
  93. package/build/server/chunks/entries/endpoints/api/raw/edited/accept/{_server.ts.js-Btohjiz4.js.map → _server.ts.js-C7IxpaSj.js.map} +1 -1
  94. package/build/server/chunks/entries/endpoints/api/raw/edited/dismiss/{_server.ts.js-D0alWeVb.js → _server.ts.js-DLZkdOrS.js} +7 -7
  95. package/build/server/chunks/entries/endpoints/api/raw/edited/dismiss/{_server.ts.js-D0alWeVb.js.map → _server.ts.js-DLZkdOrS.js.map} +1 -1
  96. package/build/server/chunks/entries/endpoints/api/reconcile/{_server.ts.js-CRNi0bmU.js → _server.ts.js-CSS2DDb1.js} +7 -7
  97. package/build/server/chunks/entries/endpoints/api/reconcile/{_server.ts.js-CRNi0bmU.js.map → _server.ts.js-CSS2DDb1.js.map} +1 -1
  98. package/build/server/chunks/entries/endpoints/api/reveal/{_server.ts.js-B6C1DDpq.js → _server.ts.js-CrXh2YL-.js} +7 -7
  99. package/build/server/chunks/entries/endpoints/api/reveal/{_server.ts.js-B6C1DDpq.js.map → _server.ts.js-CrXh2YL-.js.map} +1 -1
  100. package/build/server/chunks/entries/endpoints/api/state/{_server.ts.js-qPKxHQ-e.js → _server.ts.js-yT-0jHAT.js} +7 -7
  101. package/build/server/chunks/entries/endpoints/api/state/{_server.ts.js-qPKxHQ-e.js.map → _server.ts.js-yT-0jHAT.js.map} +1 -1
  102. package/build/server/chunks/entries/endpoints/files/_...path_/{_server.ts.js-BjioCWTI.js → _server.ts.js-DTZAlq3j.js} +6 -6
  103. package/build/server/chunks/entries/endpoints/files/_...path_/{_server.ts.js-BjioCWTI.js.map → _server.ts.js-DTZAlq3j.js.map} +1 -1
  104. package/build/server/chunks/entries/endpoints/files/proxy/_...path_/{_server.ts.js-BzUFg_ur.js → _server.ts.js-Cux6Dnfu.js} +7 -7
  105. package/build/server/chunks/entries/endpoints/files/proxy/_...path_/{_server.ts.js-BzUFg_ur.js.map → _server.ts.js-Cux6Dnfu.js.map} +1 -1
  106. package/build/server/chunks/entries/endpoints/files/raw/_...path_/{_server.ts.js-CrjUXxJT.js → _server.ts.js-BjcPFOHi.js} +7 -7
  107. package/build/server/chunks/entries/endpoints/files/raw/_...path_/{_server.ts.js-CrjUXxJT.js.map → _server.ts.js-BjcPFOHi.js.map} +1 -1
  108. package/build/server/chunks/entries/{hooks.server.js-DQ8kZ3N2.js → hooks.server.js-e6ugpMpo.js} +6 -6
  109. package/build/server/chunks/entries/{hooks.server.js-DQ8kZ3N2.js.map → hooks.server.js-e6ugpMpo.js.map} +1 -1
  110. package/build/server/chunks/{handler-CJ8lsr6W.js → handler-BbW2vgaL.js} +5 -5
  111. package/build/server/chunks/{handler-CJ8lsr6W.js.map → handler-BbW2vgaL.js.map} +1 -1
  112. package/build/server/chunks/{index.js-Ph6LJ5Na.js → index.js-CMR9GUhE.js} +5 -5
  113. package/build/server/chunks/{index.js-Ph6LJ5Na.js.map → index.js-CMR9GUhE.js.map} +1 -1
  114. package/build/server/chunks/{manifest.js-CWjy0GQ9.js → manifest.js-Chbh3YcD.js} +28 -28
  115. package/build/server/chunks/{manifest.js-CWjy0GQ9.js.map → manifest.js-Chbh3YcD.js.map} +1 -1
  116. package/build/server/chunks/nodes/{0.js-s3yR2Abv.js → 0.js-DH9RX0ts.js} +2 -2
  117. package/build/server/chunks/nodes/{0.js-s3yR2Abv.js.map → 0.js-DH9RX0ts.js.map} +1 -1
  118. package/build/server/chunks/nodes/1.js-CZRl5Ija.js +7 -0
  119. package/build/server/chunks/nodes/{1.js-BKDWaQXi.js.map → 1.js-CZRl5Ija.js.map} +1 -1
  120. package/build/server/chunks/nodes/2.js-Dh13OPjS.js +7 -0
  121. package/build/server/chunks/nodes/2.js-Dh13OPjS.js.map +1 -0
  122. package/build/server/chunks/nodes/3.js-CnTWmFQd.js +7 -0
  123. package/build/server/chunks/nodes/{3.js-CxLEr5zC.js.map → 3.js-CnTWmFQd.js.map} +1 -1
  124. package/build/server/chunks/nodes/4.js-hm5WP4MD.js +7 -0
  125. package/build/server/chunks/nodes/{4.js-BoG4R0fU.js.map → 4.js-hm5WP4MD.js.map} +1 -1
  126. package/dist/core/api-types.d.ts +7 -0
  127. package/dist/core/index.d.ts +3 -3
  128. package/dist/core/index.js +1 -1
  129. package/dist/core/plugins/video.d.ts +38 -3
  130. package/dist/core/plugins/video.js +62 -0
  131. package/dist/runtime/chroma-key-gl.d.ts +2 -0
  132. package/dist/runtime/chroma-key-gl.js +6 -0
  133. package/dist/runtime/chroma-key-video-lifecycle.d.ts +22 -0
  134. package/dist/runtime/chroma-key-video-lifecycle.js +324 -0
  135. package/dist/runtime/components/ChromaKeyVideo.svelte +35 -127
  136. package/dist/server/config-loader.d.ts +2 -2
  137. package/dist/server/operations.js +6 -1
  138. package/dist/server/plugins/chroma-key-video.d.ts +2 -2
  139. package/dist/server/plugins/chroma-key-video.js +5 -4
  140. package/dist/server/plugins/contract.d.ts +3 -1
  141. package/dist/server/plugins/video.d.ts +4 -2
  142. package/dist/server/plugins/video.js +10 -2
  143. package/dist/server/processing/video-processing.d.ts +3 -3
  144. package/dist/server/processing/video-processing.js +19 -7
  145. package/package.json +1 -1
  146. package/build/client/_app/immutable/chunks/4FCfXqUa.js.br +0 -0
  147. package/build/client/_app/immutable/chunks/4FCfXqUa.js.gz +0 -0
  148. package/build/client/_app/immutable/chunks/B6BtXxR0.js +0 -1
  149. package/build/client/_app/immutable/chunks/B6BtXxR0.js.br +0 -0
  150. package/build/client/_app/immutable/chunks/B6BtXxR0.js.gz +0 -0
  151. package/build/client/_app/immutable/chunks/BEbRZpHw.js.br +0 -1
  152. package/build/client/_app/immutable/chunks/BEbRZpHw.js.gz +0 -0
  153. package/build/client/_app/immutable/chunks/BVDR6adL.js.br +0 -0
  154. package/build/client/_app/immutable/chunks/BVDR6adL.js.gz +0 -0
  155. package/build/client/_app/immutable/chunks/BX8lJS3k.js +0 -108
  156. package/build/client/_app/immutable/chunks/BX8lJS3k.js.br +0 -0
  157. package/build/client/_app/immutable/chunks/BX8lJS3k.js.gz +0 -0
  158. package/build/client/_app/immutable/entry/app.CH6AvWYr.js.br +0 -0
  159. package/build/client/_app/immutable/entry/app.CH6AvWYr.js.gz +0 -0
  160. package/build/client/_app/immutable/entry/start.v9Hgefer.js +0 -1
  161. package/build/client/_app/immutable/entry/start.v9Hgefer.js.br +0 -0
  162. package/build/client/_app/immutable/entry/start.v9Hgefer.js.gz +0 -0
  163. package/build/client/_app/immutable/nodes/0.bYfuwnNW.js.br +0 -0
  164. package/build/client/_app/immutable/nodes/0.bYfuwnNW.js.gz +0 -0
  165. package/build/client/_app/immutable/nodes/1.kv-uSGjV.js.br +0 -3
  166. package/build/client/_app/immutable/nodes/1.kv-uSGjV.js.gz +0 -0
  167. package/build/client/_app/immutable/nodes/2.B0CNEieI.js +0 -3
  168. package/build/client/_app/immutable/nodes/2.B0CNEieI.js.br +0 -0
  169. package/build/client/_app/immutable/nodes/2.B0CNEieI.js.gz +0 -0
  170. package/build/client/_app/immutable/nodes/3.K5eLxWD7.js +0 -1
  171. package/build/client/_app/immutable/nodes/3.K5eLxWD7.js.br +0 -0
  172. package/build/client/_app/immutable/nodes/3.K5eLxWD7.js.gz +0 -0
  173. package/build/client/_app/immutable/nodes/4.DXitiaKH.js.br +0 -0
  174. package/build/client/_app/immutable/nodes/4.DXitiaKH.js.gz +0 -0
  175. package/build/server/chunks/chunks/context.js-CPdlcRhV.js.map +0 -1
  176. package/build/server/chunks/chunks/contract.js-nwG99Zzj.js.map +0 -1
  177. package/build/server/chunks/chunks/core.js-DNIuZ9y-.js.map +0 -1
  178. package/build/server/chunks/chunks/raw-reconciliation.js-_D9lZ_sc.js.map +0 -1
  179. package/build/server/chunks/nodes/1.js-BKDWaQXi.js +0 -7
  180. package/build/server/chunks/nodes/2.js-BuHtNZN4.js +0 -7
  181. package/build/server/chunks/nodes/2.js-BuHtNZN4.js.map +0 -1
  182. package/build/server/chunks/nodes/3.js-CxLEr5zC.js +0 -7
  183. 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
- // Render loop: requestVideoFrameCallback fires once per *presented* video frame
43
- // (and never while paused), so upload + draw happen only when there is a new
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
- const maskSource = src.mask?.src;
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
- stopLoop();
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 video = videoElement;
129
- if (!video) return;
130
- function onPlay() {
131
- startLoop();
132
- }
133
- function onStop() {
134
- stopLoop();
135
- drawCurrentFrame();
136
- }
137
- function onSeeked() {
138
- if (!loopRunning) drawCurrentFrame();
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
- if (webglUnavailable) return;
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
- // Touch every keying field so the effect re-runs on any of them.
168
- void [
169
- key.keyColor,
170
- key.colorCutoff,
171
- key.colorFeathering,
172
- key.maskFeathering,
173
- key.sharpening,
174
- key.despill,
175
- key.despillLuminanceAdd
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, VideoPreset } from '../core/index.js';
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, VideoPreset>;
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
- presetNames[type] = Object.keys(plugin.presets(this.#config)).sort();
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 VideoPreset } from '../../core/index.js';
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, VideoPreset>;
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, resolveNamedPreset } from './contract.js';
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
- resolvePreset: (parameters, config) => resolveNamedPreset('video', parameters.preset, config.plugins.video.presets),
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 VideoPreset } from '../../core/index.js';
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 const videoServerPlugin: ServerAssetTypePlugin<VideoParameters, VideoPreset>;
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
- resolvePreset: (parameters, config) => resolveNamedPreset('video', parameters.preset, config.plugins.video.presets),
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,