framewebworker 0.1.2 → 0.1.4

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.
@@ -75,7 +75,32 @@ interface RenderOptions {
75
75
  /** AbortSignal to cancel rendering */
76
76
  signal?: AbortSignal;
77
77
  }
78
- type ClipStatus = 'queued' | 'rendering' | 'done' | 'failed';
78
+ interface ClipMetrics {
79
+ clipId: string;
80
+ extractionMs: number;
81
+ encodingMs: number;
82
+ totalMs: number;
83
+ framesExtracted: number;
84
+ }
85
+ interface RenderMetrics {
86
+ totalMs: number;
87
+ extractionMs: number;
88
+ encodingMs: number;
89
+ stitchMs: number;
90
+ clips: ClipMetrics[];
91
+ framesPerSecond: number;
92
+ }
93
+ interface Segment {
94
+ start: number;
95
+ end: number;
96
+ captions?: CaptionSegment[];
97
+ }
98
+ /** Options for the render()/renderToUrl() single-video API */
99
+ interface SingleVideoRenderOptions extends Omit<StitchOptions, 'onProgress' | 'onComplete'> {
100
+ onProgress?: (progress: RichProgress) => void;
101
+ onComplete?: (metrics: RenderMetrics) => void;
102
+ }
103
+ type ClipStatus = 'pending' | 'rendering' | 'encoding' | 'done' | 'error';
79
104
  interface ClipProgress {
80
105
  index: number;
81
106
  status: ClipStatus;
@@ -85,8 +110,10 @@ interface RichProgress {
85
110
  overall: number;
86
111
  clips: ClipProgress[];
87
112
  }
113
+ /** Extends RenderOptions with rich per-clip progress reporting and completion metrics */
88
114
  interface StitchOptions extends Omit<RenderOptions, 'onProgress'> {
89
115
  onProgress?: (progress: RichProgress) => void;
116
+ onComplete?: (metrics: RenderMetrics) => void;
90
117
  }
91
118
  interface FrameWorker {
92
119
  /** Render a single clip to a Blob */
@@ -94,39 +121,56 @@ interface FrameWorker {
94
121
  /** Render a single clip and return an object URL */
95
122
  renderToUrl(clip: ClipInput, options?: RenderOptions): Promise<string>;
96
123
  /** Stitch multiple clips into one Blob */
97
- stitch(clips: ClipInput[], options?: StitchOptions): Promise<Blob>;
124
+ stitch(clips: ClipInput[], options?: StitchOptions): Promise<{
125
+ blob: Blob;
126
+ metrics: RenderMetrics;
127
+ }>;
98
128
  /** Stitch multiple clips and return an object URL */
99
- stitchToUrl(clips: ClipInput[], options?: StitchOptions): Promise<string>;
129
+ stitchToUrl(clips: ClipInput[], options?: StitchOptions): Promise<{
130
+ url: string;
131
+ metrics: RenderMetrics;
132
+ }>;
100
133
  }
101
134
 
102
- interface UseRenderState {
135
+ interface UseClipRenderState {
103
136
  progress: number;
104
137
  isRendering: boolean;
105
138
  error: Error | null;
106
139
  blob: Blob | null;
107
140
  url: string | null;
108
141
  }
109
- interface UseRenderActions {
142
+ interface UseClipRenderActions {
110
143
  render: (clip: ClipInput, options?: Omit<RenderOptions, 'onProgress' | 'signal'>) => Promise<Blob | null>;
111
144
  cancel: () => void;
112
145
  reset: () => void;
113
146
  }
114
- type UseRenderResult = UseRenderState & UseRenderActions;
115
- declare function useRender(frameWorker: FrameWorker): UseRenderResult;
147
+ type UseClipRenderResult = UseClipRenderState & UseClipRenderActions;
148
+ declare function useClipRender(frameWorker: FrameWorker): UseClipRenderResult;
149
+ interface UseRenderResult {
150
+ start: () => void;
151
+ cancel: () => void;
152
+ progress: RichProgress | null;
153
+ metrics: RenderMetrics | null;
154
+ url: string | null;
155
+ error: Error | null;
156
+ isRendering: boolean;
157
+ }
158
+ declare function useRender(videoUrl: string | null, segments: Segment[], options?: Omit<SingleVideoRenderOptions, 'onProgress' | 'onComplete' | 'signal'>): UseRenderResult;
116
159
 
117
160
  interface UseStitchState {
118
- progress: number;
161
+ progress: RichProgress;
119
162
  isRendering: boolean;
120
163
  error: Error | null;
121
164
  blob: Blob | null;
122
165
  url: string | null;
166
+ metrics: RenderMetrics | null;
123
167
  }
124
168
  interface UseStitchActions {
125
- stitch: (clips: ClipInput[], options?: Omit<StitchOptions, 'onProgress' | 'signal'>) => Promise<Blob | null>;
169
+ stitch: (clips: ClipInput[], options?: Omit<StitchOptions, 'onProgress' | 'onComplete' | 'signal'>) => Promise<Blob | null>;
126
170
  cancel: () => void;
127
171
  reset: () => void;
128
172
  }
129
173
  type UseStitchResult = UseStitchState & UseStitchActions;
130
174
  declare function useStitch(frameWorker: FrameWorker): UseStitchResult;
131
175
 
132
- export { type UseRenderActions, type UseRenderResult, type UseRenderState, type UseStitchActions, type UseStitchResult, type UseStitchState, useRender, useStitch };
176
+ export { type UseClipRenderActions, type UseClipRenderResult, type UseClipRenderState, type UseRenderResult, type UseStitchActions, type UseStitchResult, type UseStitchState, useClipRender, useRender, useStitch };
@@ -1,7 +1,8 @@
1
1
  import { useState, useRef, useCallback } from 'react';
2
+ import { render } from '../render.js';
2
3
 
3
4
  // src/react/useRender.ts
4
- function useRender(frameWorker) {
5
+ function useClipRender(frameWorker) {
5
6
  const [state, setState] = useState({
6
7
  progress: 0,
7
8
  isRendering: false,
@@ -56,13 +57,60 @@ function useRender(frameWorker) {
56
57
  );
57
58
  return { ...state, render, cancel, reset };
58
59
  }
60
+ function useRender(videoUrl, segments, options) {
61
+ const [isRendering, setIsRendering] = useState(false);
62
+ const [progress, setProgress] = useState(null);
63
+ const [metrics, setMetrics] = useState(null);
64
+ const [url, setUrl] = useState(null);
65
+ const [error, setError] = useState(null);
66
+ const abortRef = useRef(null);
67
+ const urlRef = useRef(null);
68
+ const cancel = useCallback(() => {
69
+ abortRef.current?.abort();
70
+ }, []);
71
+ const start = useCallback(() => {
72
+ if (!videoUrl || isRendering) return;
73
+ if (urlRef.current) {
74
+ URL.revokeObjectURL(urlRef.current);
75
+ urlRef.current = null;
76
+ }
77
+ const controller = new AbortController();
78
+ abortRef.current = controller;
79
+ setIsRendering(true);
80
+ setProgress(null);
81
+ setMetrics(null);
82
+ setUrl(null);
83
+ setError(null);
84
+ render(videoUrl, segments, {
85
+ ...options,
86
+ signal: controller.signal,
87
+ onProgress: (p) => setProgress(p),
88
+ onComplete: (m) => setMetrics(m)
89
+ }).then(({ blob }) => {
90
+ const objectUrl = URL.createObjectURL(blob);
91
+ urlRef.current = objectUrl;
92
+ setUrl(objectUrl);
93
+ setIsRendering(false);
94
+ }).catch((err) => {
95
+ if (err instanceof DOMException && err.name === "AbortError") {
96
+ setIsRendering(false);
97
+ return;
98
+ }
99
+ setError(err instanceof Error ? err : new Error(String(err)));
100
+ setIsRendering(false);
101
+ });
102
+ }, [videoUrl, segments, options, isRendering]);
103
+ return { start, cancel, progress, metrics, url, error, isRendering };
104
+ }
105
+ var INITIAL_PROGRESS = { overall: 0, clips: [] };
59
106
  function useStitch(frameWorker) {
60
107
  const [state, setState] = useState({
61
- progress: 0,
108
+ progress: INITIAL_PROGRESS,
62
109
  isRendering: false,
63
110
  error: null,
64
111
  blob: null,
65
- url: null
112
+ url: null,
113
+ metrics: null
66
114
  });
67
115
  const abortRef = useRef(null);
68
116
  const urlRef = useRef(null);
@@ -74,7 +122,7 @@ function useStitch(frameWorker) {
74
122
  URL.revokeObjectURL(urlRef.current);
75
123
  urlRef.current = null;
76
124
  }
77
- setState({ progress: 0, isRendering: false, error: null, blob: null, url: null });
125
+ setState({ progress: INITIAL_PROGRESS, isRendering: false, error: null, blob: null, url: null, metrics: null });
78
126
  }, []);
79
127
  const stitch = useCallback(
80
128
  async (clips, options) => {
@@ -84,18 +132,25 @@ function useStitch(frameWorker) {
84
132
  }
85
133
  const controller = new AbortController();
86
134
  abortRef.current = controller;
87
- setState({ progress: 0, isRendering: true, error: null, blob: null, url: null });
135
+ setState({ progress: INITIAL_PROGRESS, isRendering: true, error: null, blob: null, url: null, metrics: null });
88
136
  try {
89
- const blob = await frameWorker.stitch(clips, {
137
+ const { blob, metrics } = await frameWorker.stitch(clips, {
90
138
  ...options,
91
139
  signal: controller.signal,
92
140
  onProgress: (p) => {
93
- setState((prev) => ({ ...prev, progress: p.overall }));
141
+ setState((prev) => ({ ...prev, progress: p }));
142
+ },
143
+ onComplete: (m) => {
144
+ setState((prev) => ({ ...prev, metrics: m }));
94
145
  }
95
146
  });
96
147
  const url = URL.createObjectURL(blob);
97
148
  urlRef.current = url;
98
- setState({ progress: 1, isRendering: false, error: null, blob, url });
149
+ const doneProgress = {
150
+ overall: 1,
151
+ clips: clips.map((_, i) => ({ index: i, status: "done", progress: 1 }))
152
+ };
153
+ setState((prev) => ({ ...prev, progress: doneProgress, isRendering: false, blob, url }));
99
154
  return blob;
100
155
  } catch (err) {
101
156
  if (err instanceof DOMException && err.name === "AbortError") {
@@ -112,6 +167,6 @@ function useStitch(frameWorker) {
112
167
  return { ...state, stitch, cancel, reset };
113
168
  }
114
169
 
115
- export { useRender, useStitch };
170
+ export { useClipRender, useRender, useStitch };
116
171
  //# sourceMappingURL=index.js.map
117
172
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/react/useRender.ts","../../src/react/useStitch.ts"],"names":["useState","useRef","useCallback"],"mappings":"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","file":"index.js","sourcesContent":["'use client';\n\nimport { useState, useCallback, useRef } from 'react';\nimport type { ClipInput, RenderOptions, FrameWorker } from '../types.js';\n\nexport interface UseRenderState {\n progress: number;\n isRendering: boolean;\n error: Error | null;\n blob: Blob | null;\n url: string | null;\n}\n\nexport interface UseRenderActions {\n render: (clip: ClipInput, options?: Omit<RenderOptions, 'onProgress' | 'signal'>) => Promise<Blob | null>;\n cancel: () => void;\n reset: () => void;\n}\n\nexport type UseRenderResult = UseRenderState & UseRenderActions;\n\nexport function useRender(frameWorker: FrameWorker): UseRenderResult {\n const [state, setState] = useState<UseRenderState>({\n progress: 0,\n isRendering: false,\n error: null,\n blob: null,\n url: null,\n });\n\n const abortRef = useRef<AbortController | null>(null);\n const urlRef = useRef<string | null>(null);\n\n const cancel = useCallback(() => {\n abortRef.current?.abort();\n }, []);\n\n const reset = useCallback(() => {\n if (urlRef.current) {\n URL.revokeObjectURL(urlRef.current);\n urlRef.current = null;\n }\n setState({ progress: 0, isRendering: false, error: null, blob: null, url: null });\n }, []);\n\n const render = useCallback(\n async (\n clip: ClipInput,\n options?: Omit<RenderOptions, 'onProgress' | 'signal'>\n ): Promise<Blob | null> => {\n if (urlRef.current) {\n URL.revokeObjectURL(urlRef.current);\n urlRef.current = null;\n }\n\n const controller = new AbortController();\n abortRef.current = controller;\n\n setState({ progress: 0, isRendering: true, error: null, blob: null, url: null });\n\n try {\n const blob = await frameWorker.render(clip, {\n ...options,\n signal: controller.signal,\n onProgress: (p) => {\n setState((prev) => ({ ...prev, progress: p }));\n },\n });\n\n const url = URL.createObjectURL(blob);\n urlRef.current = url;\n setState({ progress: 1, isRendering: false, error: null, blob, url });\n return blob;\n } catch (err) {\n if (err instanceof DOMException && err.name === 'AbortError') {\n setState((prev) => ({ ...prev, isRendering: false, error: null }));\n return null;\n }\n const error = err instanceof Error ? err : new Error(String(err));\n setState((prev) => ({ ...prev, isRendering: false, error }));\n return null;\n }\n },\n [frameWorker]\n );\n\n return { ...state, render, cancel, reset };\n}\n","'use client';\n\nimport { useState, useCallback, useRef } from 'react';\nimport type { ClipInput, StitchOptions, FrameWorker } from '../types.js';\n\nexport interface UseStitchState {\n progress: number;\n isRendering: boolean;\n error: Error | null;\n blob: Blob | null;\n url: string | null;\n}\n\nexport interface UseStitchActions {\n stitch: (clips: ClipInput[], options?: Omit<StitchOptions, 'onProgress' | 'signal'>) => Promise<Blob | null>;\n cancel: () => void;\n reset: () => void;\n}\n\nexport type UseStitchResult = UseStitchState & UseStitchActions;\n\nexport function useStitch(frameWorker: FrameWorker): UseStitchResult {\n const [state, setState] = useState<UseStitchState>({\n progress: 0,\n isRendering: false,\n error: null,\n blob: null,\n url: null,\n });\n\n const abortRef = useRef<AbortController | null>(null);\n const urlRef = useRef<string | null>(null);\n\n const cancel = useCallback(() => {\n abortRef.current?.abort();\n }, []);\n\n const reset = useCallback(() => {\n if (urlRef.current) {\n URL.revokeObjectURL(urlRef.current);\n urlRef.current = null;\n }\n setState({ progress: 0, isRendering: false, error: null, blob: null, url: null });\n }, []);\n\n const stitch = useCallback(\n async (\n clips: ClipInput[],\n options?: Omit<StitchOptions, 'onProgress' | 'signal'>\n ): Promise<Blob | null> => {\n if (urlRef.current) {\n URL.revokeObjectURL(urlRef.current);\n urlRef.current = null;\n }\n\n const controller = new AbortController();\n abortRef.current = controller;\n\n setState({ progress: 0, isRendering: true, error: null, blob: null, url: null });\n\n try {\n const blob = await frameWorker.stitch(clips, {\n ...options,\n signal: controller.signal,\n onProgress: (p) => {\n setState((prev) => ({ ...prev, progress: p.overall }));\n },\n });\n\n const url = URL.createObjectURL(blob);\n urlRef.current = url;\n setState({ progress: 1, isRendering: false, error: null, blob, url });\n return blob;\n } catch (err) {\n if (err instanceof DOMException && err.name === 'AbortError') {\n setState((prev) => ({ ...prev, isRendering: false, error: null }));\n return null;\n }\n const error = err instanceof Error ? err : new Error(String(err));\n setState((prev) => ({ ...prev, isRendering: false, error }));\n return null;\n }\n },\n [frameWorker]\n );\n\n return { ...state, stitch, cancel, reset };\n}\n"]}
1
+ {"version":3,"sources":["../../src/react/useRender.ts","../../src/react/useStitch.ts"],"names":["renderSegments","useState","useRef","useCallback"],"mappings":"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","file":"index.js","sourcesContent":["'use client';\n\nimport { useState, useCallback, useRef } from 'react';\nimport type { ClipInput, RenderOptions, FrameWorker } from '../types.js';\nimport type { Segment, SingleVideoRenderOptions, RichProgress, RenderMetrics } from '../types.js';\nimport { render as renderSegments } from '../render.js';\n\n// ── useClipRender — wraps FrameWorker.render(clip) ───────────────────────────\n\nexport interface UseClipRenderState {\n progress: number;\n isRendering: boolean;\n error: Error | null;\n blob: Blob | null;\n url: string | null;\n}\n\nexport interface UseClipRenderActions {\n render: (clip: ClipInput, options?: Omit<RenderOptions, 'onProgress' | 'signal'>) => Promise<Blob | null>;\n cancel: () => void;\n reset: () => void;\n}\n\nexport type UseClipRenderResult = UseClipRenderState & UseClipRenderActions;\n\nexport function useClipRender(frameWorker: FrameWorker): UseClipRenderResult {\n const [state, setState] = useState<UseClipRenderState>({\n progress: 0,\n isRendering: false,\n error: null,\n blob: null,\n url: null,\n });\n\n const abortRef = useRef<AbortController | null>(null);\n const urlRef = useRef<string | null>(null);\n\n const cancel = useCallback(() => {\n abortRef.current?.abort();\n }, []);\n\n const reset = useCallback(() => {\n if (urlRef.current) {\n URL.revokeObjectURL(urlRef.current);\n urlRef.current = null;\n }\n setState({ progress: 0, isRendering: false, error: null, blob: null, url: null });\n }, []);\n\n const render = useCallback(\n async (\n clip: ClipInput,\n options?: Omit<RenderOptions, 'onProgress' | 'signal'>\n ): Promise<Blob | null> => {\n if (urlRef.current) {\n URL.revokeObjectURL(urlRef.current);\n urlRef.current = null;\n }\n\n const controller = new AbortController();\n abortRef.current = controller;\n\n setState({ progress: 0, isRendering: true, error: null, blob: null, url: null });\n\n try {\n const blob = await frameWorker.render(clip, {\n ...options,\n signal: controller.signal,\n onProgress: (p) => {\n setState((prev) => ({ ...prev, progress: p }));\n },\n });\n\n const url = URL.createObjectURL(blob);\n urlRef.current = url;\n setState({ progress: 1, isRendering: false, error: null, blob, url });\n return blob;\n } catch (err) {\n if (err instanceof DOMException && err.name === 'AbortError') {\n setState((prev) => ({ ...prev, isRendering: false, error: null }));\n return null;\n }\n const error = err instanceof Error ? err : new Error(String(err));\n setState((prev) => ({ ...prev, isRendering: false, error }));\n return null;\n }\n },\n [frameWorker]\n );\n\n return { ...state, render, cancel, reset };\n}\n\n// ── useRender — single-video multi-segment API ────────────────────────────────\n\nexport interface UseRenderResult {\n start: () => void;\n cancel: () => void;\n progress: RichProgress | null;\n metrics: RenderMetrics | null;\n url: string | null;\n error: Error | null;\n isRendering: boolean;\n}\n\nexport function useRender(\n videoUrl: string | null,\n segments: Segment[],\n options?: Omit<SingleVideoRenderOptions, 'onProgress' | 'onComplete' | 'signal'>\n): UseRenderResult {\n const [isRendering, setIsRendering] = useState(false);\n const [progress, setProgress] = useState<RichProgress | null>(null);\n const [metrics, setMetrics] = useState<RenderMetrics | null>(null);\n const [url, setUrl] = useState<string | null>(null);\n const [error, setError] = useState<Error | null>(null);\n\n const abortRef = useRef<AbortController | null>(null);\n const urlRef = useRef<string | null>(null);\n\n const cancel = useCallback(() => {\n abortRef.current?.abort();\n }, []);\n\n const start = useCallback(() => {\n if (!videoUrl || isRendering) return;\n\n if (urlRef.current) {\n URL.revokeObjectURL(urlRef.current);\n urlRef.current = null;\n }\n\n const controller = new AbortController();\n abortRef.current = controller;\n\n setIsRendering(true);\n setProgress(null);\n setMetrics(null);\n setUrl(null);\n setError(null);\n\n renderSegments(videoUrl, segments, {\n ...options,\n signal: controller.signal,\n onProgress: (p) => setProgress(p),\n onComplete: (m) => setMetrics(m),\n }).then(({ blob }) => {\n const objectUrl = URL.createObjectURL(blob);\n urlRef.current = objectUrl;\n setUrl(objectUrl);\n setIsRendering(false);\n }).catch((err) => {\n if (err instanceof DOMException && err.name === 'AbortError') {\n setIsRendering(false);\n return;\n }\n setError(err instanceof Error ? err : new Error(String(err)));\n setIsRendering(false);\n });\n }, [videoUrl, segments, options, isRendering]);\n\n return { start, cancel, progress, metrics, url, error, isRendering };\n}\n","'use client';\n\nimport { useState, useCallback, useRef } from 'react';\nimport type { ClipInput, StitchOptions, RichProgress, RenderMetrics, FrameWorker } from '../types.js';\n\nexport interface UseStitchState {\n progress: RichProgress;\n isRendering: boolean;\n error: Error | null;\n blob: Blob | null;\n url: string | null;\n metrics: RenderMetrics | null;\n}\n\nexport interface UseStitchActions {\n stitch: (clips: ClipInput[], options?: Omit<StitchOptions, 'onProgress' | 'onComplete' | 'signal'>) => Promise<Blob | null>;\n cancel: () => void;\n reset: () => void;\n}\n\nexport type UseStitchResult = UseStitchState & UseStitchActions;\n\nconst INITIAL_PROGRESS: RichProgress = { overall: 0, clips: [] };\n\nexport function useStitch(frameWorker: FrameWorker): UseStitchResult {\n const [state, setState] = useState<UseStitchState>({\n progress: INITIAL_PROGRESS,\n isRendering: false,\n error: null,\n blob: null,\n url: null,\n metrics: null,\n });\n\n const abortRef = useRef<AbortController | null>(null);\n const urlRef = useRef<string | null>(null);\n\n const cancel = useCallback(() => {\n abortRef.current?.abort();\n }, []);\n\n const reset = useCallback(() => {\n if (urlRef.current) {\n URL.revokeObjectURL(urlRef.current);\n urlRef.current = null;\n }\n setState({ progress: INITIAL_PROGRESS, isRendering: false, error: null, blob: null, url: null, metrics: null });\n }, []);\n\n const stitch = useCallback(\n async (\n clips: ClipInput[],\n options?: Omit<StitchOptions, 'onProgress' | 'onComplete' | 'signal'>\n ): Promise<Blob | null> => {\n if (urlRef.current) {\n URL.revokeObjectURL(urlRef.current);\n urlRef.current = null;\n }\n\n const controller = new AbortController();\n abortRef.current = controller;\n\n setState({ progress: INITIAL_PROGRESS, isRendering: true, error: null, blob: null, url: null, metrics: null });\n\n try {\n const { blob, metrics } = await frameWorker.stitch(clips, {\n ...options,\n signal: controller.signal,\n onProgress: (p) => {\n setState((prev) => ({ ...prev, progress: p }));\n },\n onComplete: (m) => {\n setState((prev) => ({ ...prev, metrics: m }));\n },\n });\n\n const url = URL.createObjectURL(blob);\n urlRef.current = url;\n const doneProgress: RichProgress = {\n overall: 1,\n clips: clips.map((_, i) => ({ index: i, status: 'done', progress: 1 })),\n };\n setState((prev) => ({ ...prev, progress: doneProgress, isRendering: false, blob, url }));\n return blob;\n } catch (err) {\n if (err instanceof DOMException && err.name === 'AbortError') {\n setState((prev) => ({ ...prev, isRendering: false, error: null }));\n return null;\n }\n const error = err instanceof Error ? err : new Error(String(err));\n setState((prev) => ({ ...prev, isRendering: false, error }));\n return null;\n }\n },\n [frameWorker]\n );\n\n return { ...state, stitch, cancel, reset };\n}\n"]}
@@ -0,0 +1,256 @@
1
+ // src/captions.ts
2
+ var STYLE_PRESETS = {
3
+ hormozi: {
4
+ preset: "hormozi",
5
+ fontFamily: 'Impact, "Arial Black", sans-serif',
6
+ fontSize: 64,
7
+ fontWeight: "900",
8
+ color: "#FFFFFF",
9
+ strokeColor: "#000000",
10
+ strokeWidth: 4,
11
+ backgroundColor: "transparent",
12
+ backgroundPadding: 0,
13
+ backgroundRadius: 0,
14
+ position: "bottom",
15
+ textAlign: "center",
16
+ lineHeight: 1.1,
17
+ maxWidth: 0.9,
18
+ shadow: true,
19
+ shadowColor: "rgba(0,0,0,0.9)",
20
+ shadowBlur: 6,
21
+ shadowOffsetX: 2,
22
+ shadowOffsetY: 2,
23
+ uppercase: true,
24
+ wordHighlight: true,
25
+ wordHighlightColor: "#FFD700",
26
+ wordHighlightTextColor: "#000000"
27
+ },
28
+ modern: {
29
+ preset: "modern",
30
+ fontFamily: '"Inter", "Helvetica Neue", Arial, sans-serif',
31
+ fontSize: 42,
32
+ fontWeight: "700",
33
+ color: "#FFFFFF",
34
+ strokeColor: "transparent",
35
+ strokeWidth: 0,
36
+ backgroundColor: "rgba(0,0,0,0.65)",
37
+ backgroundPadding: 12,
38
+ backgroundRadius: 8,
39
+ position: "bottom",
40
+ textAlign: "center",
41
+ lineHeight: 1.3,
42
+ maxWidth: 0.85,
43
+ shadow: false,
44
+ shadowColor: "transparent",
45
+ shadowBlur: 0,
46
+ shadowOffsetX: 0,
47
+ shadowOffsetY: 0,
48
+ uppercase: false,
49
+ wordHighlight: false,
50
+ wordHighlightColor: "#3B82F6",
51
+ wordHighlightTextColor: "#FFFFFF"
52
+ },
53
+ minimal: {
54
+ preset: "minimal",
55
+ fontFamily: '"Helvetica Neue", Arial, sans-serif',
56
+ fontSize: 36,
57
+ fontWeight: "400",
58
+ color: "#FFFFFF",
59
+ strokeColor: "transparent",
60
+ strokeWidth: 0,
61
+ backgroundColor: "transparent",
62
+ backgroundPadding: 0,
63
+ backgroundRadius: 0,
64
+ position: "bottom",
65
+ textAlign: "center",
66
+ lineHeight: 1.4,
67
+ maxWidth: 0.8,
68
+ shadow: true,
69
+ shadowColor: "rgba(0,0,0,0.8)",
70
+ shadowBlur: 8,
71
+ shadowOffsetX: 0,
72
+ shadowOffsetY: 2,
73
+ uppercase: false,
74
+ wordHighlight: false,
75
+ wordHighlightColor: "#FFFFFF",
76
+ wordHighlightTextColor: "#000000"
77
+ },
78
+ bold: {
79
+ preset: "bold",
80
+ fontFamily: '"Arial Black", "Helvetica Neue", Arial, sans-serif',
81
+ fontSize: 56,
82
+ fontWeight: "900",
83
+ color: "#FFFF00",
84
+ strokeColor: "#000000",
85
+ strokeWidth: 5,
86
+ backgroundColor: "transparent",
87
+ backgroundPadding: 0,
88
+ backgroundRadius: 0,
89
+ position: "center",
90
+ textAlign: "center",
91
+ lineHeight: 1.2,
92
+ maxWidth: 0.88,
93
+ shadow: true,
94
+ shadowColor: "rgba(0,0,0,1)",
95
+ shadowBlur: 4,
96
+ shadowOffsetX: 3,
97
+ shadowOffsetY: 3,
98
+ uppercase: true,
99
+ wordHighlight: false,
100
+ wordHighlightColor: "#FF0000",
101
+ wordHighlightTextColor: "#FFFFFF"
102
+ }
103
+ };
104
+ function mergeStyle(base, overrides) {
105
+ return overrides ? { ...base, ...overrides } : base;
106
+ }
107
+ function getActiveCaptions(segments, currentTime) {
108
+ return segments.filter(
109
+ (seg) => currentTime >= seg.startTime && currentTime < seg.endTime
110
+ );
111
+ }
112
+ function wrapText(ctx2, text, maxWidth) {
113
+ const words = text.split(" ");
114
+ const lines = [];
115
+ let current = "";
116
+ for (const word of words) {
117
+ const test = current ? `${current} ${word}` : word;
118
+ if (ctx2.measureText(test).width > maxWidth && current) {
119
+ lines.push(current);
120
+ current = word;
121
+ } else {
122
+ current = test;
123
+ }
124
+ }
125
+ if (current) lines.push(current);
126
+ return lines;
127
+ }
128
+ function renderCaption(ctx2, segment, resolvedStyle, canvasWidth, canvasHeight) {
129
+ const style = resolvedStyle;
130
+ const text = style.uppercase ? segment.text.toUpperCase() : segment.text;
131
+ ctx2.save();
132
+ const scaledFontSize = style.fontSize / 1080 * canvasHeight;
133
+ ctx2.font = `${style.fontWeight} ${scaledFontSize}px ${style.fontFamily}`;
134
+ ctx2.textAlign = style.textAlign;
135
+ ctx2.textBaseline = "bottom";
136
+ const maxPx = style.maxWidth * canvasWidth;
137
+ const lines = wrapText(ctx2, text, maxPx);
138
+ const lineH = scaledFontSize * style.lineHeight;
139
+ const totalH = lines.length * lineH;
140
+ let baseY;
141
+ if (style.position === "top") {
142
+ baseY = scaledFontSize * 1.5;
143
+ } else if (style.position === "center") {
144
+ baseY = canvasHeight / 2 - totalH / 2 + lineH;
145
+ } else {
146
+ baseY = canvasHeight - scaledFontSize * 1.2;
147
+ }
148
+ const cx = canvasWidth / 2;
149
+ lines.forEach((line, i) => {
150
+ const y = baseY + i * lineH;
151
+ if (style.backgroundColor && style.backgroundColor !== "transparent") {
152
+ const metrics = ctx2.measureText(line);
153
+ const bw = metrics.width + style.backgroundPadding * 2;
154
+ const bh = lineH + style.backgroundPadding;
155
+ const bx = cx - bw / 2;
156
+ const by = y - lineH;
157
+ ctx2.fillStyle = style.backgroundColor;
158
+ if (style.backgroundRadius > 0) {
159
+ roundRect(ctx2, bx, by, bw, bh, style.backgroundRadius);
160
+ ctx2.fill();
161
+ } else {
162
+ ctx2.fillRect(bx, by, bw, bh);
163
+ }
164
+ }
165
+ if (style.shadow) {
166
+ ctx2.shadowColor = style.shadowColor;
167
+ ctx2.shadowBlur = style.shadowBlur;
168
+ ctx2.shadowOffsetX = style.shadowOffsetX;
169
+ ctx2.shadowOffsetY = style.shadowOffsetY;
170
+ }
171
+ if (style.strokeWidth > 0 && style.strokeColor !== "transparent") {
172
+ ctx2.lineWidth = style.strokeWidth;
173
+ ctx2.strokeStyle = style.strokeColor;
174
+ ctx2.strokeText(line, cx, y);
175
+ }
176
+ ctx2.shadowColor = "transparent";
177
+ ctx2.shadowBlur = 0;
178
+ ctx2.fillStyle = style.color;
179
+ ctx2.fillText(line, cx, y);
180
+ });
181
+ ctx2.restore();
182
+ }
183
+ function roundRect(ctx2, x, y, w, h, r) {
184
+ ctx2.beginPath();
185
+ ctx2.moveTo(x + r, y);
186
+ ctx2.lineTo(x + w - r, y);
187
+ ctx2.quadraticCurveTo(x + w, y, x + w, y + r);
188
+ ctx2.lineTo(x + w, y + h - r);
189
+ ctx2.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
190
+ ctx2.lineTo(x + r, y + h);
191
+ ctx2.quadraticCurveTo(x, y + h, x, y + h - r);
192
+ ctx2.lineTo(x, y + r);
193
+ ctx2.quadraticCurveTo(x, y, x + r, y);
194
+ ctx2.closePath();
195
+ }
196
+
197
+ // src/worker/render-worker.ts
198
+ var meta = null;
199
+ var canvas = null;
200
+ var ctx = null;
201
+ var collected = [];
202
+ self.onmessage = (event) => {
203
+ const msg = event.data;
204
+ switch (msg.type) {
205
+ case "init": {
206
+ meta = msg.meta;
207
+ canvas = new OffscreenCanvas(meta.width, meta.height);
208
+ ctx = canvas.getContext("2d");
209
+ collected.length = 0;
210
+ break;
211
+ }
212
+ case "frame": {
213
+ if (!meta || !ctx) return;
214
+ const { bitmap, timestamp, index } = msg;
215
+ ctx.clearRect(0, 0, meta.width, meta.height);
216
+ ctx.drawImage(bitmap, 0, 0, meta.width, meta.height);
217
+ bitmap.close();
218
+ if (meta.captions?.segments?.length) {
219
+ const baseStylePreset = meta.captions.style?.preset ?? "modern";
220
+ const baseStyle = mergeStyle(STYLE_PRESETS[baseStylePreset], meta.captions.style);
221
+ const active = getActiveCaptions(meta.captions.segments, timestamp);
222
+ for (const seg of active) {
223
+ const segStyle = mergeStyle(baseStyle, seg.style);
224
+ renderCaption(
225
+ ctx,
226
+ seg,
227
+ segStyle,
228
+ meta.width,
229
+ meta.height
230
+ );
231
+ }
232
+ }
233
+ const imageData = ctx.getImageData(0, 0, meta.width, meta.height);
234
+ const buffer = imageData.data.buffer.slice(
235
+ imageData.data.byteOffset,
236
+ imageData.data.byteOffset + imageData.data.byteLength
237
+ );
238
+ collected.push({ buffer, timestamp, width: meta.width, height: meta.height });
239
+ const progress = { type: "progress", value: (index + 1) / meta.totalFrames };
240
+ self.postMessage(progress);
241
+ break;
242
+ }
243
+ case "end": {
244
+ const frames = collected.slice();
245
+ const transferables = frames.map((f) => f.buffer);
246
+ const done = { type: "done", frames };
247
+ self.postMessage(done, transferables);
248
+ collected.length = 0;
249
+ break;
250
+ }
251
+ case "abort": {
252
+ collected.length = 0;
253
+ break;
254
+ }
255
+ }
256
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "framewebworker",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Browser-native video rendering and clip export library. Trim, caption, and export MP4 Blobs in the browser — no server needed.",
5
5
  "keywords": [
6
6
  "video",