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.
- package/dist/index.cjs +345 -264
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +46 -4
- package/dist/index.d.ts +46 -4
- package/dist/index.js +344 -265
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +64 -8
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +54 -10
- package/dist/react/index.d.ts +54 -10
- package/dist/react/index.js +64 -9
- package/dist/react/index.js.map +1 -1
- package/dist/worker/render-worker.js +256 -0
- package/package.json +1 -1
- package/dist/render-worker.js +0 -177
- package/dist/render-worker.js.map +0 -1
package/dist/react/index.d.ts
CHANGED
|
@@ -75,7 +75,32 @@ interface RenderOptions {
|
|
|
75
75
|
/** AbortSignal to cancel rendering */
|
|
76
76
|
signal?: AbortSignal;
|
|
77
77
|
}
|
|
78
|
-
|
|
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<
|
|
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<
|
|
129
|
+
stitchToUrl(clips: ClipInput[], options?: StitchOptions): Promise<{
|
|
130
|
+
url: string;
|
|
131
|
+
metrics: RenderMetrics;
|
|
132
|
+
}>;
|
|
100
133
|
}
|
|
101
134
|
|
|
102
|
-
interface
|
|
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
|
|
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
|
|
115
|
-
declare function
|
|
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:
|
|
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
|
|
176
|
+
export { type UseClipRenderActions, type UseClipRenderResult, type UseClipRenderState, type UseRenderResult, type UseStitchActions, type UseStitchResult, type UseStitchState, useClipRender, useRender, useStitch };
|
package/dist/react/index.js
CHANGED
|
@@ -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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
package/dist/react/index.js.map
CHANGED
|
@@ -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