egaki 0.5.0 → 0.7.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 (280) hide show
  1. package/CHANGELOG.md +188 -0
  2. package/dist/cli/cache-utils.d.ts +33 -0
  3. package/dist/cli/cache-utils.d.ts.map +1 -0
  4. package/dist/cli/cache-utils.js +157 -0
  5. package/dist/cli/cached-generate.d.ts +105 -0
  6. package/dist/cli/cached-generate.d.ts.map +1 -0
  7. package/dist/cli/cached-generate.js +208 -0
  8. package/dist/cli/cached-generate.test.d.ts +11 -0
  9. package/dist/cli/cached-generate.test.d.ts.map +1 -0
  10. package/dist/cli/cached-generate.test.js +431 -0
  11. package/dist/cli/cartesia-provider.d.ts +16 -0
  12. package/dist/cli/cartesia-provider.d.ts.map +1 -0
  13. package/dist/cli/cartesia-provider.js +184 -0
  14. package/dist/cli/chatgpt-auth.d.ts.map +1 -0
  15. package/dist/cli/cli.d.ts +4 -0
  16. package/dist/cli/cli.d.ts.map +1 -0
  17. package/dist/cli/cli.js +1329 -0
  18. package/dist/cli/clone-generate.d.ts +26 -0
  19. package/dist/cli/clone-generate.d.ts.map +1 -0
  20. package/dist/cli/clone-generate.js +148 -0
  21. package/dist/{credentials.d.ts → cli/credentials.d.ts} +11 -2
  22. package/dist/cli/credentials.d.ts.map +1 -0
  23. package/dist/{credentials.js → cli/credentials.js} +88 -5
  24. package/dist/cli/elevenlabs-speech-provider.d.ts +26 -0
  25. package/dist/cli/elevenlabs-speech-provider.d.ts.map +1 -0
  26. package/dist/cli/elevenlabs-speech-provider.js +199 -0
  27. package/dist/cli/generate.d.ts +140 -0
  28. package/dist/cli/generate.d.ts.map +1 -0
  29. package/dist/cli/generate.js +604 -0
  30. package/dist/cli/generate.test.d.ts +2 -0
  31. package/dist/cli/generate.test.d.ts.map +1 -0
  32. package/dist/cli/generate.test.js +131 -0
  33. package/dist/cli/login.d.ts.map +1 -0
  34. package/dist/{login.js → cli/login.js} +45 -4
  35. package/dist/cli/main.d.ts +3 -0
  36. package/dist/cli/main.d.ts.map +1 -0
  37. package/dist/cli/main.js +7 -0
  38. package/dist/cli/model-catalog.d.ts +141 -0
  39. package/dist/cli/model-catalog.d.ts.map +1 -0
  40. package/dist/cli/model-catalog.js +2135 -0
  41. package/dist/{models.d.ts → cli/models.d.ts} +16 -9
  42. package/dist/cli/models.d.ts.map +1 -0
  43. package/dist/cli/models.js +313 -0
  44. package/dist/cli/open-browser.d.ts.map +1 -0
  45. package/dist/cli/openai-speech-provider.d.ts +3 -0
  46. package/dist/cli/openai-speech-provider.d.ts.map +1 -0
  47. package/dist/cli/openai-speech-provider.js +28 -0
  48. package/dist/cli/separation-generate.d.ts +34 -0
  49. package/dist/cli/separation-generate.d.ts.map +1 -0
  50. package/dist/cli/separation-generate.js +117 -0
  51. package/dist/cli/speech-catalog.d.ts +35 -0
  52. package/dist/cli/speech-catalog.d.ts.map +1 -0
  53. package/dist/cli/speech-catalog.js +195 -0
  54. package/dist/cli/speech-generate.d.ts +86 -0
  55. package/dist/cli/speech-generate.d.ts.map +1 -0
  56. package/dist/cli/speech-generate.js +156 -0
  57. package/dist/cli/speech-models.d.ts +8 -0
  58. package/dist/cli/speech-models.d.ts.map +1 -0
  59. package/dist/cli/speech-models.js +109 -0
  60. package/dist/cli/speech-provider.test.d.ts +2 -0
  61. package/dist/cli/speech-provider.test.d.ts.map +1 -0
  62. package/dist/cli/speech-provider.test.js +197 -0
  63. package/dist/cli/subscription.d.ts.map +1 -0
  64. package/dist/cli/transcription-catalog.d.ts +35 -0
  65. package/dist/cli/transcription-catalog.d.ts.map +1 -0
  66. package/dist/cli/transcription-catalog.js +192 -0
  67. package/dist/cli/transcription-generate.d.ts +62 -0
  68. package/dist/cli/transcription-generate.d.ts.map +1 -0
  69. package/dist/cli/transcription-generate.js +144 -0
  70. package/dist/cli/transcription-models.d.ts +9 -0
  71. package/dist/cli/transcription-models.d.ts.map +1 -0
  72. package/dist/cli/transcription-models.js +100 -0
  73. package/dist/cli/transcription-providers.d.ts +22 -0
  74. package/dist/cli/transcription-providers.d.ts.map +1 -0
  75. package/dist/cli/transcription-providers.js +290 -0
  76. package/dist/cli/video-model-catalog.d.ts +2 -0
  77. package/dist/cli/video-model-catalog.d.ts.map +1 -0
  78. package/dist/cli/video-model-catalog.js +5 -0
  79. package/dist/cli/xai-auth.d.ts +29 -0
  80. package/dist/cli/xai-auth.d.ts.map +1 -0
  81. package/dist/cli/xai-auth.js +325 -0
  82. package/dist/vite/angled-screen.d.ts +57 -0
  83. package/dist/vite/angled-screen.d.ts.map +1 -0
  84. package/dist/vite/angled-screen.js +115 -0
  85. package/dist/vite/app.d.ts +51 -0
  86. package/dist/vite/app.d.ts.map +1 -0
  87. package/dist/vite/app.js +150 -0
  88. package/dist/vite/bands-shader.d.ts +47 -0
  89. package/dist/vite/bands-shader.d.ts.map +1 -0
  90. package/dist/vite/bands-shader.js +267 -0
  91. package/dist/vite/code-block.d.ts +107 -0
  92. package/dist/vite/code-block.d.ts.map +1 -0
  93. package/dist/vite/code-block.js +641 -0
  94. package/dist/vite/components.d.ts +36 -0
  95. package/dist/vite/components.d.ts.map +1 -0
  96. package/dist/vite/components.js +113 -0
  97. package/dist/vite/dispersion-rings-shader.d.ts +44 -0
  98. package/dist/vite/dispersion-rings-shader.d.ts.map +1 -0
  99. package/dist/vite/dispersion-rings-shader.js +282 -0
  100. package/dist/vite/easing-curves.d.ts +129 -0
  101. package/dist/vite/easing-curves.d.ts.map +1 -0
  102. package/dist/vite/easing-curves.js +584 -0
  103. package/dist/vite/easing-curves.test.d.ts +2 -0
  104. package/dist/vite/easing-curves.test.d.ts.map +1 -0
  105. package/dist/vite/easing-curves.test.js +200 -0
  106. package/dist/vite/generated-media-client.d.ts +35 -0
  107. package/dist/vite/generated-media-client.d.ts.map +1 -0
  108. package/dist/vite/generated-media-client.js +76 -0
  109. package/dist/vite/keyframes.d.ts +129 -0
  110. package/dist/vite/keyframes.d.ts.map +1 -0
  111. package/dist/vite/keyframes.js +194 -0
  112. package/dist/vite/layout-editor.d.ts +45 -0
  113. package/dist/vite/layout-editor.d.ts.map +1 -0
  114. package/dist/vite/layout-editor.js +587 -0
  115. package/dist/vite/layout-transition.d.ts +136 -0
  116. package/dist/vite/layout-transition.d.ts.map +1 -0
  117. package/dist/vite/layout-transition.js +581 -0
  118. package/dist/vite/liquid-gradient-shader.d.ts +62 -0
  119. package/dist/vite/liquid-gradient-shader.d.ts.map +1 -0
  120. package/dist/vite/liquid-gradient-shader.js +413 -0
  121. package/dist/vite/mdx-client.d.ts +15 -0
  122. package/dist/vite/mdx-client.d.ts.map +1 -0
  123. package/dist/vite/mdx-client.js +265 -0
  124. package/dist/vite/mdx-parse.d.ts +102 -0
  125. package/dist/vite/mdx-parse.d.ts.map +1 -0
  126. package/dist/vite/mdx-parse.js +251 -0
  127. package/dist/vite/mdx-provided-components.d.ts +18 -0
  128. package/dist/vite/mdx-provided-components.d.ts.map +1 -0
  129. package/dist/vite/mdx-provided-components.js +14 -0
  130. package/dist/vite/mdx-video.d.ts +327 -0
  131. package/dist/vite/mdx-video.d.ts.map +1 -0
  132. package/dist/vite/mdx-video.js +596 -0
  133. package/dist/vite/mdx-video.test.d.ts +9 -0
  134. package/dist/vite/mdx-video.test.d.ts.map +1 -0
  135. package/dist/vite/mdx-video.test.js +1945 -0
  136. package/dist/vite/media-components.d.ts +45 -0
  137. package/dist/vite/media-components.d.ts.map +1 -0
  138. package/dist/vite/media-components.js +355 -0
  139. package/dist/vite/media-duration-store.d.ts +66 -0
  140. package/dist/vite/media-duration-store.d.ts.map +1 -0
  141. package/dist/vite/media-duration-store.js +204 -0
  142. package/dist/vite/player-page.d.ts +35 -0
  143. package/dist/vite/player-page.d.ts.map +1 -0
  144. package/dist/vite/player-page.js +720 -0
  145. package/dist/vite/render-client.d.ts +18 -0
  146. package/dist/vite/render-client.d.ts.map +1 -0
  147. package/dist/vite/render-client.js +67 -0
  148. package/dist/vite/sdk.d.ts +221 -0
  149. package/dist/vite/sdk.d.ts.map +1 -0
  150. package/dist/vite/sdk.js +355 -0
  151. package/dist/vite/server-components.d.ts +55 -0
  152. package/dist/vite/server-components.d.ts.map +1 -0
  153. package/dist/vite/server-components.js +156 -0
  154. package/dist/vite/server-mdx.d.ts +74 -0
  155. package/dist/vite/server-mdx.d.ts.map +1 -0
  156. package/dist/vite/server-mdx.js +259 -0
  157. package/dist/vite/shader-renderer.d.ts +48 -0
  158. package/dist/vite/shader-renderer.d.ts.map +1 -0
  159. package/dist/vite/shader-renderer.js +411 -0
  160. package/dist/vite/store-hooks.d.ts +12 -0
  161. package/dist/vite/store-hooks.d.ts.map +1 -0
  162. package/dist/vite/store-hooks.js +93 -0
  163. package/dist/vite/store.d.ts +75 -0
  164. package/dist/vite/store.d.ts.map +1 -0
  165. package/dist/vite/store.js +52 -0
  166. package/dist/vite/store.test.d.ts +6 -0
  167. package/dist/vite/store.test.d.ts.map +1 -0
  168. package/dist/vite/store.test.js +172 -0
  169. package/dist/vite/tweakpane-hook.d.ts +94 -0
  170. package/dist/vite/tweakpane-hook.d.ts.map +1 -0
  171. package/dist/vite/tweakpane-hook.js +450 -0
  172. package/dist/vite/vite-plugin.d.ts +28 -0
  173. package/dist/vite/vite-plugin.d.ts.map +1 -0
  174. package/dist/vite/vite-plugin.js +538 -0
  175. package/dist/vite/wave-gradient-shader.d.ts +41 -0
  176. package/dist/vite/wave-gradient-shader.d.ts.map +1 -0
  177. package/dist/vite/wave-gradient-shader.js +207 -0
  178. package/package.json +103 -25
  179. package/skills/egaki/SKILL.md +162 -1
  180. package/src/cli/cache-utils.ts +169 -0
  181. package/src/cli/cached-generate.test.ts +531 -0
  182. package/src/cli/cached-generate.ts +348 -0
  183. package/src/cli/cartesia-provider.ts +249 -0
  184. package/src/cli/cli.ts +1818 -0
  185. package/src/cli/clone-generate.ts +202 -0
  186. package/src/{credentials.ts → cli/credentials.ts} +90 -7
  187. package/src/cli/elevenlabs-speech-provider.ts +243 -0
  188. package/src/cli/generate.test.ts +166 -0
  189. package/src/cli/generate.ts +912 -0
  190. package/src/{login.ts → cli/login.ts} +48 -3
  191. package/src/cli/main.ts +8 -0
  192. package/src/cli/model-catalog.ts +2336 -0
  193. package/src/cli/models.ts +408 -0
  194. package/src/cli/openai-speech-provider.ts +34 -0
  195. package/src/cli/separation-generate.ts +187 -0
  196. package/src/cli/speech-catalog.ts +253 -0
  197. package/src/cli/speech-generate.ts +275 -0
  198. package/src/cli/speech-models.ts +146 -0
  199. package/src/cli/speech-provider.test.ts +226 -0
  200. package/src/cli/transcription-catalog.ts +250 -0
  201. package/src/cli/transcription-generate.ts +214 -0
  202. package/src/cli/transcription-models.ts +136 -0
  203. package/src/cli/transcription-providers.ts +427 -0
  204. package/src/cli/video-model-catalog.ts +14 -0
  205. package/src/cli/xai-auth.ts +396 -0
  206. package/src/vite/angled-screen.tsx +214 -0
  207. package/src/vite/app.tsx +197 -0
  208. package/src/vite/bands-shader.tsx +319 -0
  209. package/src/vite/code-block.tsx +939 -0
  210. package/src/vite/components.tsx +278 -0
  211. package/src/vite/dispersion-rings-shader.tsx +331 -0
  212. package/src/vite/easing-curves.test.ts +232 -0
  213. package/src/vite/easing-curves.ts +743 -0
  214. package/src/vite/generated-media-client.tsx +121 -0
  215. package/src/vite/keyframes.tsx +347 -0
  216. package/src/vite/layout-editor.tsx +709 -0
  217. package/src/vite/layout-transition.tsx +706 -0
  218. package/src/vite/liquid-gradient-shader.tsx +474 -0
  219. package/src/vite/mdx-client.tsx +420 -0
  220. package/src/vite/mdx-parse.ts +343 -0
  221. package/src/vite/mdx-provided-components.ts +19 -0
  222. package/src/vite/mdx-video.test.tsx +2219 -0
  223. package/src/vite/mdx-video.tsx +976 -0
  224. package/src/vite/media-components.tsx +473 -0
  225. package/src/vite/media-duration-store.ts +225 -0
  226. package/src/vite/motion-timing.ts +194 -0
  227. package/src/vite/player-page.tsx +1107 -0
  228. package/src/vite/render-client.ts +84 -0
  229. package/src/vite/sdk.ts +598 -0
  230. package/src/vite/server-components.tsx +241 -0
  231. package/src/vite/server-mdx.ts +285 -0
  232. package/src/vite/shader-renderer.tsx +535 -0
  233. package/src/vite/store-hooks.ts +117 -0
  234. package/src/vite/store.test.ts +202 -0
  235. package/src/vite/store.ts +115 -0
  236. package/src/vite/styles.css +21 -0
  237. package/src/vite/tweakpane-hook.tsx +593 -0
  238. package/src/vite/virtual-modules.d.ts +30 -0
  239. package/src/vite/vite-plugin.ts +584 -0
  240. package/src/vite/wave-gradient-shader.tsx +253 -0
  241. package/README.md +0 -266
  242. package/dist/antigravity-auth.d.ts +0 -15
  243. package/dist/antigravity-auth.d.ts.map +0 -1
  244. package/dist/antigravity-auth.js +0 -544
  245. package/dist/antigravity-image.d.ts +0 -11
  246. package/dist/antigravity-image.d.ts.map +0 -1
  247. package/dist/antigravity-image.js +0 -285
  248. package/dist/chatgpt-auth.d.ts.map +0 -1
  249. package/dist/cli.d.ts +0 -3
  250. package/dist/cli.d.ts.map +0 -1
  251. package/dist/cli.js +0 -968
  252. package/dist/credentials.d.ts.map +0 -1
  253. package/dist/gateway-model-candidates.generated.d.ts +0 -11
  254. package/dist/gateway-model-candidates.generated.d.ts.map +0 -1
  255. package/dist/gateway-model-candidates.generated.js +0 -64
  256. package/dist/login.d.ts.map +0 -1
  257. package/dist/model-catalog.d.ts +0 -42
  258. package/dist/model-catalog.d.ts.map +0 -1
  259. package/dist/model-catalog.js +0 -1001
  260. package/dist/models.d.ts.map +0 -1
  261. package/dist/models.js +0 -246
  262. package/dist/open-browser.d.ts.map +0 -1
  263. package/dist/subscription.d.ts.map +0 -1
  264. package/dist/video-model-catalog.d.ts +0 -52
  265. package/dist/video-model-catalog.d.ts.map +0 -1
  266. package/dist/video-model-catalog.js +0 -306
  267. package/src/cli.ts +0 -1378
  268. package/src/model-catalog.ts +0 -1077
  269. package/src/models.ts +0 -309
  270. package/src/video-model-catalog.ts +0 -363
  271. /package/dist/{chatgpt-auth.d.ts → cli/chatgpt-auth.d.ts} +0 -0
  272. /package/dist/{chatgpt-auth.js → cli/chatgpt-auth.js} +0 -0
  273. /package/dist/{login.d.ts → cli/login.d.ts} +0 -0
  274. /package/dist/{open-browser.d.ts → cli/open-browser.d.ts} +0 -0
  275. /package/dist/{open-browser.js → cli/open-browser.js} +0 -0
  276. /package/dist/{subscription.d.ts → cli/subscription.d.ts} +0 -0
  277. /package/dist/{subscription.js → cli/subscription.js} +0 -0
  278. /package/src/{chatgpt-auth.ts → cli/chatgpt-auth.ts} +0 -0
  279. /package/src/{open-browser.ts → cli/open-browser.ts} +0 -0
  280. /package/src/{subscription.ts → cli/subscription.ts} +0 -0
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Ghost-aware media components for egaki video.
3
+ *
4
+ * Wraps Remotion's Img and @remotion/media's Audio/Video with:
5
+ * - LayoutTransition ghost neutralization (Audio renders nothing in ghost,
6
+ * Video renders a lightweight div placeholder for FLIP measurement)
7
+ * - Promise<string> src support via Suspense + React 19 use()
8
+ * - Auto duration reporting to the per-section duration store
9
+ * - Tweakpane trim controls for interactive Video editing
10
+ * - Export-aware delayRender for frame capture blocking
11
+ *
12
+ * Also defines ExportContext for detecting export mode (renderMediaOnWeb).
13
+ */
14
+ import { type ComponentProps, type ReactNode } from 'react';
15
+ import { Audio as MediaAudio, Video as MediaVideo } from '@remotion/media';
16
+ export declare const ExportContext: import("react").Context<boolean>;
17
+ /** Returns true when the component is rendering inside an export (renderMediaOnWeb). */
18
+ export declare function useIsExporting(): boolean;
19
+ type ImgProps = Omit<React.ImgHTMLAttributes<HTMLImageElement>, 'src'> & {
20
+ src?: string | Promise<string>;
21
+ };
22
+ export declare function Img(props: ImgProps): import("react").JSX.Element | null;
23
+ /** Extra props accepted by egaki's Audio and Video wrappers. */
24
+ type MediaOffsetProps = {
25
+ /**
26
+ * Frame offset for when the media starts playing.
27
+ *
28
+ * Positive or zero: delays playback from section start.
29
+ * `startInFrames={1 * FPS}` waits 1 second then starts.
30
+ *
31
+ * Negative: offset from section end.
32
+ * `startInFrames={-2 * FPS}` starts playing 2 seconds before the section ends.
33
+ */
34
+ startInFrames?: number;
35
+ };
36
+ type AudioProps = Omit<ComponentProps<typeof MediaAudio>, 'src'> & MediaOffsetProps & {
37
+ src?: string | Promise<string>;
38
+ };
39
+ export declare function Audio(props: AudioProps): import("react").JSX.Element | null;
40
+ type VideoProps = Omit<ComponentProps<typeof MediaVideo>, 'src'> & MediaOffsetProps & {
41
+ src?: string | Promise<string>;
42
+ };
43
+ export declare function Video(props: VideoProps): string | number | bigint | boolean | import("react").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | null | undefined;
44
+ export {};
45
+ //# sourceMappingURL=media-components.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-components.d.ts","sourceRoot":"","sources":["../../src/vite/media-components.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAUL,KAAK,cAAc,EACnB,KAAK,SAAS,EACf,MAAM,OAAO,CAAA;AAQd,OAAO,EAAE,KAAK,IAAI,UAAU,EAAE,KAAK,IAAI,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAe1E,eAAO,MAAM,aAAa,kCAAuB,CAAA;AAEjD,wFAAwF;AACxF,wBAAgB,cAAc,IAAI,OAAO,CAExC;AA6JD,KAAK,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,iBAAiB,CAAC,gBAAgB,CAAC,EAAE,KAAK,CAAC,GAAG;IACvE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CAC/B,CAAA;AAOD,wBAAgB,GAAG,CAAC,KAAK,EAAE,QAAQ,sCAYlC;AAED,gEAAgE;AAChE,KAAK,gBAAgB,GAAG;IACtB;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,CAAA;AAED,KAAK,UAAU,GAAG,IAAI,CAAC,cAAc,CAAC,OAAO,UAAU,CAAC,EAAE,KAAK,CAAC,GAAG,gBAAgB,GAAG;IACpF,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CAC/B,CAAA;AAoBD,wBAAgB,KAAK,CAAC,KAAK,EAAE,UAAU,sCAwBtC;AAED,KAAK,UAAU,GAAG,IAAI,CAAC,cAAc,CAAC,OAAO,UAAU,CAAC,EAAE,KAAK,CAAC,GAAG,gBAAgB,GAAG;IACpF,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CAC/B,CAAA;AAOD,wBAAgB,KAAK,CAAC,KAAK,EAAE,UAAU,+TA0CtC"}
@@ -0,0 +1,355 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ /**
4
+ * Ghost-aware media components for egaki video.
5
+ *
6
+ * Wraps Remotion's Img and @remotion/media's Audio/Video with:
7
+ * - LayoutTransition ghost neutralization (Audio renders nothing in ghost,
8
+ * Video renders a lightweight div placeholder for FLIP measurement)
9
+ * - Promise<string> src support via Suspense + React 19 use()
10
+ * - Auto duration reporting to the per-section duration store
11
+ * - Tweakpane trim controls for interactive Video editing
12
+ * - Export-aware delayRender for frame capture blocking
13
+ *
14
+ * Also defines ExportContext for detecting export mode (renderMediaOnWeb).
15
+ */
16
+ import { createContext, Suspense, use, useContext, useEffect, useId, useLayoutEffect, useRef, useState, } from 'react';
17
+ import { Img as RemotionImg, Sequence, useCurrentFrame, useDelayRender, useVideoConfig, } from 'remotion';
18
+ import { Audio as MediaAudio, Video as MediaVideo } from '@remotion/media';
19
+ import { LayoutContainerContext } from "./layout-transition.js";
20
+ import { getCachedRawDuration, cacheRawDuration, computeEffectiveDuration, reportSectionDuration, useSectionIndex, } from "./media-duration-store.js";
21
+ import { useTweakpane } from "./tweakpane-hook.js";
22
+ // ---------------------------------------------------------------------------
23
+ // Export context — lets components detect when they're inside a render export
24
+ // ---------------------------------------------------------------------------
25
+ export const ExportContext = createContext(false);
26
+ /** Returns true when the component is rendering inside an export (renderMediaOnWeb). */
27
+ export function useIsExporting() {
28
+ return useContext(ExportContext);
29
+ }
30
+ // ---------------------------------------------------------------------------
31
+ // useReportMediaDuration
32
+ //
33
+ // Shared hook for Audio/Video: fetches raw media duration, computes
34
+ // effective playback duration (accounting for trim + playbackRate), and
35
+ // reports to the per-section duration store.
36
+ //
37
+ // Priority:
38
+ // 1. Both trimBefore + trimAfter set → compute from props, no fetch.
39
+ // 2. Raw src cached → compute effective from cached raw + trim + rate.
40
+ // 3. Cache miss → delayRender, fetch via mediabunny, cache raw, compute
41
+ // effective, report, continueRender.
42
+ //
43
+ // Returns the RAW source duration (for tweakpane trim controls). null until
44
+ // known. Reports the EFFECTIVE duration to the section store.
45
+ //
46
+ // Uses delayRender to block export rendering until the duration is known.
47
+ //
48
+ // Reports are NOT cleared on unmount. Remotion's Series.Sequence unmounts
49
+ // inactive sections during normal seek/playback; clearing on unmount would
50
+ // cause an infinite loop (report → duration changes → remount → report).
51
+ // Reports persist for the lifetime of the composition and are reset when
52
+ // the MDX/modules change (see resetSectionDurations in mdx-client.tsx).
53
+ // ---------------------------------------------------------------------------
54
+ function useReportMediaDuration(props, skip) {
55
+ const instanceId = useId();
56
+ const sectionIndex = useSectionIndex();
57
+ const { fps } = useVideoConfig();
58
+ const { delayRender, continueRender } = useDelayRender();
59
+ const isExporting = useIsExporting();
60
+ const [rawDuration, setRawDuration] = useState(null);
61
+ // useLayoutEffect ensures delayRender is registered before paint,
62
+ // preventing the renderer from capturing a frame before the handle exists.
63
+ useLayoutEffect(() => {
64
+ if (!props.src || skip)
65
+ return;
66
+ let delayHandle = null;
67
+ let disposed = false;
68
+ const reportEffective = (raw) => {
69
+ const effective = computeEffectiveDuration({
70
+ rawSeconds: raw,
71
+ fps,
72
+ trimBefore: props.trimBefore,
73
+ trimAfter: props.trimAfter,
74
+ playbackRate: props.playbackRate,
75
+ startInFrames: props.startInFrames,
76
+ });
77
+ if (effective != null && effective > 0) {
78
+ reportSectionDuration(sectionIndex, instanceId, effective);
79
+ }
80
+ };
81
+ // Fast path: both trim bounds set → effective duration fully determined.
82
+ const effectiveFromProps = computeEffectiveDuration({
83
+ fps,
84
+ trimBefore: props.trimBefore,
85
+ trimAfter: props.trimAfter,
86
+ playbackRate: props.playbackRate,
87
+ startInFrames: props.startInFrames,
88
+ });
89
+ if (effectiveFromProps != null) {
90
+ reportSectionDuration(sectionIndex, instanceId, effectiveFromProps);
91
+ return;
92
+ }
93
+ // Check raw src cache
94
+ const cachedRaw = getCachedRawDuration(props.src);
95
+ if (cachedRaw !== undefined) {
96
+ setRawDuration(cachedRaw);
97
+ reportEffective(cachedRaw);
98
+ return;
99
+ }
100
+ // Cache miss: during export, block rendering until metadata is known.
101
+ // In the interactive Player, delayRender still creates global handles even
102
+ // though playback buffering is handled by Remotion's separate BufferState.
103
+ // Keeping it export-only avoids stale render-ready state during trim seeks.
104
+ if (isExporting) {
105
+ delayHandle = delayRender('Fetching media duration for ' + props.src);
106
+ }
107
+ void (async () => {
108
+ try {
109
+ const { Input, UrlSource, ALL_FORMATS } = await import('mediabunny');
110
+ if (disposed)
111
+ return;
112
+ const input = new Input({
113
+ formats: ALL_FORMATS,
114
+ source: new UrlSource(props.src),
115
+ });
116
+ const duration = (await input.getDurationFromMetadata()) ??
117
+ (await input.computeDuration());
118
+ input.dispose();
119
+ if (!disposed && isFinite(duration) && duration > 0) {
120
+ cacheRawDuration(props.src, duration);
121
+ setRawDuration(duration);
122
+ reportEffective(duration);
123
+ }
124
+ }
125
+ catch {
126
+ // Source unreadable or unsupported format; skip
127
+ }
128
+ finally {
129
+ if (delayHandle != null) {
130
+ continueRender(delayHandle);
131
+ delayHandle = null;
132
+ }
133
+ }
134
+ })();
135
+ return () => {
136
+ disposed = true;
137
+ if (delayHandle != null) {
138
+ continueRender(delayHandle);
139
+ delayHandle = null;
140
+ }
141
+ };
142
+ }, [props.src, skip, sectionIndex, instanceId, fps, props.trimBefore, props.trimAfter, props.playbackRate, props.startInFrames, isExporting]);
143
+ return rawDuration;
144
+ }
145
+ // ---------------------------------------------------------------------------
146
+ // ExportDelayFallback — Suspense fallback that blocks Remotion frame capture
147
+ // during export when a promise src is still pending. In the interactive Player,
148
+ // renders nothing (no blocking needed).
149
+ // ---------------------------------------------------------------------------
150
+ function ExportDelayFallback() {
151
+ const isExporting = useIsExporting();
152
+ const { delayRender, continueRender } = useDelayRender();
153
+ useLayoutEffect(() => {
154
+ if (!isExporting)
155
+ return;
156
+ const handle = delayRender('Waiting for async media src');
157
+ return () => continueRender(handle);
158
+ }, [isExporting, delayRender, continueRender]);
159
+ return null;
160
+ }
161
+ function ResolvedImg({ srcPromise, ...rest }) {
162
+ const src = use(srcPromise);
163
+ return _jsx(Img, { src: src, ...rest });
164
+ }
165
+ export function Img(props) {
166
+ const { style, src, ...rest } = props;
167
+ if (!src)
168
+ return null;
169
+ if (src instanceof Promise) {
170
+ return (_jsx(Suspense, { fallback: _jsx(ExportDelayFallback, {}), children: _jsx(ResolvedImg, { srcPromise: src, style: style, ...rest }) }));
171
+ }
172
+ // eslint-disable-next-line jsx-a11y/alt-text
173
+ return _jsx(RemotionImg, { src: src, style: { display: 'block', ...style }, ...rest });
174
+ }
175
+ function ResolvedAudio({ srcPromise, ...rest }) {
176
+ const src = use(srcPromise);
177
+ return _jsx(Audio, { src: src, ...rest });
178
+ }
179
+ /**
180
+ * Resolve startInFrames to a Sequence `from` value.
181
+ * Positive: use directly. Negative: offset from section end (can resolve to
182
+ * a negative `from`, which Remotion handles as pre-roll — the media starts
183
+ * mid-playback at the section's first frame).
184
+ * Returns 0 (no wrapping needed) when startInFrames is 0 or undefined.
185
+ */
186
+ function resolveMediaStartFrame(startInFrames, durationInFrames) {
187
+ if (startInFrames == null || startInFrames === 0)
188
+ return 0;
189
+ if (startInFrames < 0)
190
+ return durationInFrames + startInFrames;
191
+ return startInFrames;
192
+ }
193
+ export function Audio(props) {
194
+ const { startInFrames, src, ...mediaProps } = props;
195
+ const container = useContext(LayoutContainerContext);
196
+ const { durationInFrames } = useVideoConfig();
197
+ // Skip duration reporting when src is a promise (not yet resolved)
198
+ useReportMediaDuration({ ...props, src: typeof src === 'string' ? src : undefined }, container === 'ghost' || src instanceof Promise);
199
+ if (container === 'ghost')
200
+ return null;
201
+ if (src instanceof Promise) {
202
+ return (_jsx(Suspense, { fallback: _jsx(ExportDelayFallback, {}), children: _jsx(ResolvedAudio, { srcPromise: src, startInFrames: startInFrames, ...mediaProps }) }));
203
+ }
204
+ const resolvedFrom = resolveMediaStartFrame(startInFrames, durationInFrames);
205
+ if (resolvedFrom) {
206
+ return _jsx(Sequence, { from: resolvedFrom, layout: "none", children: _jsx(MediaAudio, { src: src, ...mediaProps }) });
207
+ }
208
+ return _jsx(MediaAudio, { src: src, ...mediaProps });
209
+ }
210
+ function ResolvedVideo({ srcPromise, ...rest }) {
211
+ const src = use(srcPromise);
212
+ return _jsx(Video, { src: src, ...rest });
213
+ }
214
+ export function Video(props) {
215
+ const { startInFrames, src, ...mediaProps } = props;
216
+ const container = useContext(LayoutContainerContext);
217
+ const isExporting = useIsExporting();
218
+ const { durationInFrames } = useVideoConfig();
219
+ // Promise src: wrap in Suspense, resolve, then re-render with string src
220
+ if (src instanceof Promise) {
221
+ return (_jsx(Suspense, { fallback: _jsx(ExportDelayFallback, {}), children: _jsx(ResolvedVideo, { srcPromise: src, startInFrames: startInFrames, ...mediaProps }) }));
222
+ }
223
+ // Default to filling the container. Remotion's canvas renders at the
224
+ // video's native resolution without explicit width/height, which causes
225
+ // the video to appear tiny in flex layouts. safe-mdx also drops the
226
+ // style prop when a component is passed as an expression prop (e.g.
227
+ // background={<Video style={{...}} />}), so explicit user styles are
228
+ // unreliable in MDX. Defaulting to 100% matches how users expect a
229
+ // video to behave in a layout system. An explicit style prop overrides.
230
+ const filledProps = {
231
+ ...mediaProps,
232
+ src,
233
+ style: { width: '100%', height: '100%', ...mediaProps.style },
234
+ };
235
+ const resolvedFrom = resolveMediaStartFrame(startInFrames, durationInFrames);
236
+ const wrapWithOffset = (el) => resolvedFrom ? _jsx(Sequence, { from: resolvedFrom, layout: "none", children: el }) : el;
237
+ if (container === 'ghost') {
238
+ return wrapWithOffset(_jsx("div", { style: filledProps.style }));
239
+ }
240
+ // During export, report duration but skip tweakpane UI
241
+ if (isExporting) {
242
+ return wrapWithOffset(_jsx(VideoExportDuration, { ...filledProps, startInFrames: startInFrames }));
243
+ }
244
+ return wrapWithOffset(_jsx(VideoWithTweakpane, { ...filledProps, startInFrames: startInFrames }));
245
+ }
246
+ /** Export mode: reports duration to section store, renders plain MediaVideo. */
247
+ function VideoExportDuration(props) {
248
+ const { startInFrames, ...mediaProps } = props;
249
+ useReportMediaDuration(props);
250
+ return _jsx(MediaVideo, { ...mediaProps });
251
+ }
252
+ /**
253
+ * Loads the source video's duration via the shared hook, then delegates
254
+ * to VideoTrimControls for tweakpane trim sliders. Until duration is known,
255
+ * renders the video without trim controls.
256
+ */
257
+ function VideoWithTweakpane(props) {
258
+ const rawDuration = useReportMediaDuration(props);
259
+ const { startInFrames, ...mediaProps } = props;
260
+ if (rawDuration === null) {
261
+ return _jsx(MediaVideo, { ...mediaProps });
262
+ }
263
+ return _jsx(VideoTrimControls, { ...mediaProps, mediaDuration: rawDuration });
264
+ }
265
+ /**
266
+ * Registers tweakpane start/end sliders (in seconds) and converts
267
+ * them to Remotion's trimBefore/trimAfter frame props. The folder label
268
+ * is the video filename extracted from src.
269
+ *
270
+ * When the user drags a slider, the player pauses and seeks to the
271
+ * corresponding frame so the user can see exactly where the cut lands.
272
+ * Seeks are debounced to 50ms to avoid flooding the player during
273
+ * continuous dragging.
274
+ *
275
+ * Section offset computation: this component lives inside a Remotion
276
+ * Series.Sequence. useCurrentFrame() returns the frame relative to the
277
+ * sequence, while egakiSDK.getCurrentFrame() returns the absolute
278
+ * composition frame. The difference (absolute - relative) gives the
279
+ * section's start frame in the composition, cached on first render.
280
+ */
281
+ function VideoTrimControls(props) {
282
+ const { mediaDuration, ...videoProps } = props;
283
+ const { fps, durationInFrames: sectionDuration } = useVideoConfig();
284
+ const relativeFrame = useCurrentFrame();
285
+ // Compute the absolute frame offset of this section once on first render.
286
+ // useCurrentFrame() = relative, egakiSDK.getCurrentFrame() = absolute.
287
+ const sectionOffsetRef = useRef(null);
288
+ if (sectionOffsetRef.current === null) {
289
+ try {
290
+ const absoluteFrame = window.egakiSDK?.getCurrentFrame() ?? 0;
291
+ sectionOffsetRef.current = absoluteFrame - relativeFrame;
292
+ }
293
+ catch {
294
+ sectionOffsetRef.current = 0;
295
+ }
296
+ }
297
+ // Use the filename from src as the tweakpane folder label
298
+ const src = typeof props.src === 'string' ? props.src : 'Video';
299
+ const label = src.split('/').pop()?.split('?')[0] || 'Video';
300
+ // Convert any existing trim props (in frames) to seconds for defaults
301
+ const defaultStart = props.trimBefore != null ? props.trimBefore / fps : 0;
302
+ const defaultEnd = props.trimAfter != null ? props.trimAfter / fps : mediaDuration;
303
+ const tp = useTweakpane(label, {
304
+ trim: { type: 'range', value: { min: defaultStart, max: defaultEnd }, min: 0, max: mediaDuration, step: 0.1 },
305
+ });
306
+ // Debounced seek: pause the player and seek to the trim point so the
307
+ // user sees the exact frame they're cutting to. Debounce at 50ms so
308
+ // continuous slider dragging doesn't flood the player with seeks.
309
+ const seekTimerRef = useRef(null);
310
+ const prevTrimStartRef = useRef(tp.trim.min);
311
+ const prevTrimEndRef = useRef(tp.trim.max);
312
+ useEffect(() => {
313
+ const sdk = window.egakiSDK;
314
+ if (!sdk)
315
+ return;
316
+ const offset = sectionOffsetRef.current ?? 0;
317
+ let targetFrame = null;
318
+ if (tp.trim.min !== prevTrimStartRef.current) {
319
+ // start changed → seek to section start (where source shows start)
320
+ targetFrame = offset;
321
+ prevTrimStartRef.current = tp.trim.min;
322
+ }
323
+ else if (tp.trim.max !== prevTrimEndRef.current) {
324
+ // end changed → seek to the section-relative frame where source
325
+ // shows the end point: F = (end - start) * fps - 1
326
+ // Seek away from the cut. Browser/media decoders are often
327
+ // flaky at the exact final decodable frame of a trimmed range, and this
328
+ // seek is only preview feedback; trimAfter itself remains exact.
329
+ const endRelative = Math.round((tp.trim.max - tp.trim.min) * fps) - Math.round(fps * 0.25);
330
+ targetFrame = offset + Math.max(0, Math.min(endRelative, sectionDuration - 1));
331
+ prevTrimEndRef.current = tp.trim.max;
332
+ }
333
+ if (targetFrame === null)
334
+ return;
335
+ if (seekTimerRef.current)
336
+ clearTimeout(seekTimerRef.current);
337
+ const frame = targetFrame;
338
+ seekTimerRef.current = setTimeout(() => {
339
+ try {
340
+ sdk.seekTo(Math.max(0, frame));
341
+ }
342
+ catch {
343
+ // SDK not ready, ignore
344
+ }
345
+ }, 50);
346
+ return () => {
347
+ if (seekTimerRef.current)
348
+ clearTimeout(seekTimerRef.current);
349
+ };
350
+ }, [tp.trim.min, tp.trim.max, fps, sectionDuration]);
351
+ // Convert seconds back to frames for Remotion
352
+ const trimBefore = tp.trim.min > 0 ? Math.round(tp.trim.min * fps) : undefined;
353
+ const trimAfter = tp.trim.max < mediaDuration ? Math.round(tp.trim.max * fps) : undefined;
354
+ return _jsx(MediaVideo, { ...videoProps, trimBefore: trimBefore, trimAfter: trimAfter });
355
+ }
@@ -0,0 +1,66 @@
1
+ import { useMediaDurations } from './store-hooks.ts';
2
+ export declare const SectionIndexContext: import("react").Context<number>;
3
+ export declare function useSectionIndex(): number;
4
+ /**
5
+ * Read a cached RAW source duration by src URL.
6
+ * Returns undefined on cache miss. Does NOT trigger any fetch.
7
+ */
8
+ export declare function getCachedRawDuration(src: string): number | undefined;
9
+ /**
10
+ * Cache a RAW source duration. Only updates if the new value is larger
11
+ * (metadata reads should be deterministic, but multiple formats may
12
+ * report slightly different values).
13
+ */
14
+ export declare function cacheRawDuration(src: string, seconds: number): void;
15
+ /**
16
+ * Compute effective media playback duration in seconds from Remotion trim,
17
+ * speed, and gap props.
18
+ *
19
+ * `rawSeconds` is the full source duration (from cache or mediabunny).
20
+ * When both trimBefore and trimAfter are set, rawSeconds is not needed.
21
+ *
22
+ * trimBefore/trimAfter are in FRAMES (Remotion convention).
23
+ * startInFrames offsets playback start: positive delays from section start,
24
+ * negative counts from section end. Only positive values add to auto-duration
25
+ * (negative values anchor to section end, so the section must have an explicit
26
+ * duration or another media element determining length).
27
+ * playbackRate defaults to 1.
28
+ *
29
+ * Returns null when duration cannot be determined (missing bounds and
30
+ * no rawSeconds).
31
+ */
32
+ export declare function computeEffectiveDuration({ rawSeconds, fps, trimBefore, trimAfter, playbackRate, startInFrames, }: {
33
+ rawSeconds?: number;
34
+ fps: number;
35
+ trimBefore?: number;
36
+ trimAfter?: number;
37
+ playbackRate?: number;
38
+ startInFrames?: number;
39
+ }): number | null;
40
+ export { useMediaDurations };
41
+ /**
42
+ * Report an effective media duration for a section from a specific
43
+ * component instance. The per-section max is derived from all active
44
+ * reports via selectMediaDurations. Called by the useReportMediaDuration hook.
45
+ */
46
+ export declare function reportSectionDuration(sectionIndex: number, instanceId: string, seconds: number): void;
47
+ /**
48
+ * Clear a component instance's report. Called on unmount so the
49
+ * per-section max shrinks when media elements are removed.
50
+ */
51
+ export declare function clearSectionDuration(sectionIndex: number, instanceId: string): void;
52
+ /**
53
+ * Reset all section duration reports. Called when the MDX composition
54
+ * changes (HMR, MDX edit, module update) so stale reports don't persist.
55
+ * Does NOT clear the raw src cache (localStorage).
56
+ */
57
+ export declare function resetSectionDurations(): void;
58
+ /**
59
+ * Returns the count of null-duration sections that have no reports yet.
60
+ * Used by the export button to block export until all media durations
61
+ * are resolved.
62
+ */
63
+ export declare function countUnresolvedSections(sections: {
64
+ durationInFrames: number | null;
65
+ }[]): number;
66
+ //# sourceMappingURL=media-duration-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-duration-store.d.ts","sourceRoot":"","sources":["../../src/vite/media-duration-store.ts"],"names":[],"mappings":"AA0BA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAUpD,eAAO,MAAM,mBAAmB,iCAA4B,CAAA;AAE5D,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAyCD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEpE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,QAO5D;AAMD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,wBAAwB,CAAC,EACvC,UAAU,EACV,GAAG,EACH,UAAU,EACV,SAAS,EACT,YAAY,EACZ,aAAa,GACd,EAAE;IACD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,GAAG,EAAE,MAAM,CAAA;IACX,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,GAAG,MAAM,GAAG,IAAI,CAchB;AAMD,OAAO,EAAE,iBAAiB,EAAE,CAAA;AAE5B;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,QAW9F;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,QAc5E;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,SAGpC;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE;IAAE,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EAAE,GAC9C,MAAM,CASR"}
@@ -0,0 +1,204 @@
1
+ 'use client';
2
+ /**
3
+ * Media duration cache and section duration state.
4
+ *
5
+ * Two layers:
6
+ *
7
+ * 1. **Raw src cache** (persistent) — `Map<string, number>` keyed by media
8
+ * src URL → RAW source duration in seconds (before trim/playbackRate).
9
+ * Backed by localStorage. Never stores effective/trimmed durations so
10
+ * the same src used with different trim ranges doesn't get poisoned.
11
+ *
12
+ * 2. **Per-instance section reports** (ephemeral) — each mounted Audio/Video
13
+ * component reports its EFFECTIVE playback duration (after trim + speed)
14
+ * keyed by `(sectionIndex, instanceId)`. Per-section max is derived from
15
+ * active reports. When a component unmounts, its report is cleared and
16
+ * the max recomputes — durations shrink correctly when media is removed,
17
+ * src changes, or trim bounds change.
18
+ *
19
+ * `useMediaDurations()` returns per-section maxes as
20
+ * `Record<string, number>` (section-index-as-string → seconds) for
21
+ * `resolveAutoDurations`. Subscribes via the centralized zustand store.
22
+ */
23
+ import { createContext, useContext } from 'react';
24
+ import { egakiStore } from "./store.js";
25
+ import { useMediaDurations } from "./store-hooks.js";
26
+ // ---------------------------------------------------------------------------
27
+ // Section index context
28
+ //
29
+ // Provided by player-page.tsx around each section's content. Audio/Video
30
+ // components read this to know which section they belong to, then report
31
+ // their effective media duration grouped by section index.
32
+ // ---------------------------------------------------------------------------
33
+ export const SectionIndexContext = createContext(-1);
34
+ export function useSectionIndex() {
35
+ return useContext(SectionIndexContext);
36
+ }
37
+ // ---------------------------------------------------------------------------
38
+ // Persistent raw src cache (localStorage)
39
+ //
40
+ // Stores RAW source duration only — the full media length before any trim
41
+ // or speed adjustments. This prevents cache poisoning when the same src
42
+ // is used with different trim ranges in different sections.
43
+ // ---------------------------------------------------------------------------
44
+ const STORAGE_KEY = 'egaki:media-src-durations';
45
+ /** src URL → RAW duration in seconds (before trim/speed) */
46
+ const rawSrcCache = new Map();
47
+ function loadSrcCache() {
48
+ try {
49
+ const raw = localStorage.getItem(STORAGE_KEY);
50
+ if (!raw)
51
+ return;
52
+ const parsed = JSON.parse(raw);
53
+ for (const [src, dur] of Object.entries(parsed)) {
54
+ if (typeof dur === 'number' && dur > 0)
55
+ rawSrcCache.set(src, dur);
56
+ }
57
+ }
58
+ catch {
59
+ // localStorage unavailable or corrupted; start fresh
60
+ }
61
+ }
62
+ function persistSrcCache() {
63
+ try {
64
+ const obj = {};
65
+ for (const [src, dur] of rawSrcCache)
66
+ obj[src] = dur;
67
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(obj));
68
+ }
69
+ catch {
70
+ // localStorage full or unavailable, ignore
71
+ }
72
+ }
73
+ // Load on module init (runs once in the browser)
74
+ if (typeof localStorage !== 'undefined')
75
+ loadSrcCache();
76
+ /**
77
+ * Read a cached RAW source duration by src URL.
78
+ * Returns undefined on cache miss. Does NOT trigger any fetch.
79
+ */
80
+ export function getCachedRawDuration(src) {
81
+ return rawSrcCache.get(src);
82
+ }
83
+ /**
84
+ * Cache a RAW source duration. Only updates if the new value is larger
85
+ * (metadata reads should be deterministic, but multiple formats may
86
+ * report slightly different values).
87
+ */
88
+ export function cacheRawDuration(src, seconds) {
89
+ if (!isFinite(seconds) || seconds <= 0)
90
+ return;
91
+ const prev = rawSrcCache.get(src);
92
+ if (prev === undefined || prev < seconds) {
93
+ rawSrcCache.set(src, seconds);
94
+ persistSrcCache();
95
+ }
96
+ }
97
+ // ---------------------------------------------------------------------------
98
+ // Effective duration: derive from raw + trim + playbackRate
99
+ // ---------------------------------------------------------------------------
100
+ /**
101
+ * Compute effective media playback duration in seconds from Remotion trim,
102
+ * speed, and gap props.
103
+ *
104
+ * `rawSeconds` is the full source duration (from cache or mediabunny).
105
+ * When both trimBefore and trimAfter are set, rawSeconds is not needed.
106
+ *
107
+ * trimBefore/trimAfter are in FRAMES (Remotion convention).
108
+ * startInFrames offsets playback start: positive delays from section start,
109
+ * negative counts from section end. Only positive values add to auto-duration
110
+ * (negative values anchor to section end, so the section must have an explicit
111
+ * duration or another media element determining length).
112
+ * playbackRate defaults to 1.
113
+ *
114
+ * Returns null when duration cannot be determined (missing bounds and
115
+ * no rawSeconds).
116
+ */
117
+ export function computeEffectiveDuration({ rawSeconds, fps, trimBefore, trimAfter, playbackRate, startInFrames, }) {
118
+ const rate = playbackRate ?? 1;
119
+ if (rate <= 0)
120
+ return null;
121
+ const startFrame = trimBefore ?? 0;
122
+ const endFrame = trimAfter ?? (rawSeconds != null ? Math.round(rawSeconds * fps) : null);
123
+ if (endFrame == null)
124
+ return null;
125
+ const mediaFrames = endFrame - startFrame;
126
+ if (mediaFrames <= 0)
127
+ return null;
128
+ // Only positive startInFrames adds to effective duration (delay before media).
129
+ // Negative startInFrames anchors to section end and doesn't extend auto-duration.
130
+ // The delay is timeline-fixed (not affected by playbackRate), so add it separately.
131
+ const positiveOffsetFrames = startInFrames != null && startInFrames > 0 ? startInFrames : 0;
132
+ return mediaFrames / fps / rate + positiveOffsetFrames / fps;
133
+ }
134
+ // ---------------------------------------------------------------------------
135
+ // Per-instance section reports — stored in the centralized zustand store.
136
+ // ---------------------------------------------------------------------------
137
+ export { useMediaDurations };
138
+ /**
139
+ * Report an effective media duration for a section from a specific
140
+ * component instance. The per-section max is derived from all active
141
+ * reports via selectMediaDurations. Called by the useReportMediaDuration hook.
142
+ */
143
+ export function reportSectionDuration(sectionIndex, instanceId, seconds) {
144
+ if (sectionIndex < 0 || !isFinite(seconds) || seconds <= 0)
145
+ return;
146
+ egakiStore.setState((state) => {
147
+ const prev = state.sectionReports.get(sectionIndex)?.get(instanceId);
148
+ if (prev === seconds)
149
+ return state;
150
+ const newReports = new Map(state.sectionReports);
151
+ const sectionMap = new Map(newReports.get(sectionIndex) ?? []);
152
+ sectionMap.set(instanceId, seconds);
153
+ newReports.set(sectionIndex, sectionMap);
154
+ return { sectionReports: newReports };
155
+ });
156
+ }
157
+ /**
158
+ * Clear a component instance's report. Called on unmount so the
159
+ * per-section max shrinks when media elements are removed.
160
+ */
161
+ export function clearSectionDuration(sectionIndex, instanceId) {
162
+ egakiStore.setState((state) => {
163
+ const existing = state.sectionReports.get(sectionIndex);
164
+ if (!existing || !existing.has(instanceId))
165
+ return state;
166
+ const newReports = new Map(state.sectionReports);
167
+ const sectionMap = new Map(existing);
168
+ sectionMap.delete(instanceId);
169
+ if (sectionMap.size === 0) {
170
+ newReports.delete(sectionIndex);
171
+ }
172
+ else {
173
+ newReports.set(sectionIndex, sectionMap);
174
+ }
175
+ return { sectionReports: newReports };
176
+ });
177
+ }
178
+ /**
179
+ * Reset all section duration reports. Called when the MDX composition
180
+ * changes (HMR, MDX edit, module update) so stale reports don't persist.
181
+ * Does NOT clear the raw src cache (localStorage).
182
+ */
183
+ export function resetSectionDurations() {
184
+ if (egakiStore.getState().sectionReports.size === 0)
185
+ return;
186
+ egakiStore.setState({ sectionReports: new Map() });
187
+ }
188
+ /**
189
+ * Returns the count of null-duration sections that have no reports yet.
190
+ * Used by the export button to block export until all media durations
191
+ * are resolved.
192
+ */
193
+ export function countUnresolvedSections(sections) {
194
+ const { sectionReports } = egakiStore.getState();
195
+ let count = 0;
196
+ for (let i = 0; i < sections.length; i++) {
197
+ if (sections[i].durationInFrames !== null)
198
+ continue;
199
+ const reports = sectionReports.get(i);
200
+ if (!reports || reports.size === 0)
201
+ count++;
202
+ }
203
+ return count;
204
+ }