@pixerate/editor-react 0.56.0 → 0.57.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.
|
@@ -41,11 +41,11 @@ function useVideoEditor(controllerOrOptions = {}) {
|
|
|
41
41
|
return { controller, state, ...actions };
|
|
42
42
|
}
|
|
43
43
|
function useVideoEditorTools(registry, controller, options = {}) {
|
|
44
|
-
const { id, namespace, readOnly, rasterize } = options;
|
|
44
|
+
const { id, namespace, readOnly, rasterize, framePixels, framePixelsTimeout } = options;
|
|
45
45
|
useEffect(() => {
|
|
46
46
|
if (!registry) return;
|
|
47
|
-
return registry.register(createVideoEditorToolProvider(controller, { id, namespace, readOnly, rasterize }));
|
|
48
|
-
}, [registry, controller, id, namespace, readOnly, rasterize]);
|
|
47
|
+
return registry.register(createVideoEditorToolProvider(controller, { id, namespace, readOnly, rasterize, framePixels, framePixelsTimeout }));
|
|
48
|
+
}, [registry, controller, id, namespace, readOnly, rasterize, framePixels, framePixelsTimeout]);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
// src/video-editor/VideoPreview.tsx
|
|
@@ -76,28 +76,96 @@ function VideoPreview({ controller, resolution, onFrame, className = "", style }
|
|
|
76
76
|
);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
// src/video-editor/VideoScopes.tsx
|
|
80
|
+
import { useEffect as useEffect3, useRef as useRef3, useState } from "react";
|
|
81
|
+
import { readVideoScopesPanel, VIDEO_SCOPE_KINDS } from "@pixerate/editor/video-editor";
|
|
82
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
83
|
+
var LABELS = { histogram: "Histogram", waveform: "Luma waveform", vectorscope: "Vectorscope" };
|
|
84
|
+
var pct = (v) => `${Math.round(v * 1e3) / 10}%`;
|
|
85
|
+
function VideoScopes({ controller, scopes = VIDEO_SCOPE_KINDS, width = 256, height = 128, className = "" }) {
|
|
86
|
+
const canvases = useRef3({});
|
|
87
|
+
const [reading, setReading] = useState(() => ({ ok: false, message: "Show the preview to see its scopes." }));
|
|
88
|
+
const key = scopes.join(",");
|
|
89
|
+
useEffect3(() => {
|
|
90
|
+
const feed = controller.previewPixels;
|
|
91
|
+
let frame = 0;
|
|
92
|
+
const raf = typeof requestAnimationFrame === "function" ? requestAnimationFrame : (fn) => setTimeout(fn, 16);
|
|
93
|
+
const caf = typeof cancelAnimationFrame === "function" ? cancelAnimationFrame : (id) => clearTimeout(id);
|
|
94
|
+
const draw = () => {
|
|
95
|
+
frame = 0;
|
|
96
|
+
const next = readVideoScopesPanel(feed, scopes, { width, height });
|
|
97
|
+
setReading(next);
|
|
98
|
+
if (!next.ok) return;
|
|
99
|
+
for (const kind of scopes) {
|
|
100
|
+
const img = next.images[kind];
|
|
101
|
+
const ctx = canvases.current[kind]?.getContext("2d");
|
|
102
|
+
if (img && ctx && typeof ImageData !== "undefined") ctx.putImageData(new ImageData(img.data, img.width, img.height), 0, 0);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const schedule = () => {
|
|
106
|
+
if (!frame) frame = raf(draw);
|
|
107
|
+
};
|
|
108
|
+
schedule();
|
|
109
|
+
const off = feed.subscribe(schedule);
|
|
110
|
+
return () => {
|
|
111
|
+
off();
|
|
112
|
+
if (frame) caf(frame);
|
|
113
|
+
};
|
|
114
|
+
}, [controller, key, width, height]);
|
|
115
|
+
return /* @__PURE__ */ jsxs("div", { className: `flex flex-col gap-2 text-xs ${className}`, "data-video-scopes": "", children: [
|
|
116
|
+
/* @__PURE__ */ jsx2("div", { className: "flex flex-wrap gap-2", children: scopes.map((kind) => {
|
|
117
|
+
const w = kind === "vectorscope" ? height : width;
|
|
118
|
+
return /* @__PURE__ */ jsxs("figure", { className: "m-0 flex min-w-0 max-w-full flex-col gap-1", children: [
|
|
119
|
+
/* @__PURE__ */ jsx2("canvas", { ref: (el) => void (canvases.current[kind] = el), width: w, height, className: "rounded border border-border bg-black", style: { width: w, maxWidth: "100%", height: "auto", aspectRatio: `${w} / ${height}` }, "aria-label": LABELS[kind], "data-scope": kind }),
|
|
120
|
+
/* @__PURE__ */ jsx2("figcaption", { className: "text-[11px] text-muted-foreground", children: LABELS[kind] })
|
|
121
|
+
] }, kind);
|
|
122
|
+
}) }),
|
|
123
|
+
reading.ok ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5", "data-scope-stats": "", children: [
|
|
124
|
+
/* @__PURE__ */ jsxs("div", { className: "tabular-nums", children: [
|
|
125
|
+
"Luma ",
|
|
126
|
+
reading.stats.luma.mean.toFixed(2),
|
|
127
|
+
" mean \xB7 ",
|
|
128
|
+
reading.stats.luma.p1.toFixed(2),
|
|
129
|
+
"\u2013",
|
|
130
|
+
reading.stats.luma.p99.toFixed(2),
|
|
131
|
+
" \xB7 crushed ",
|
|
132
|
+
pct(reading.stats.clipped.shadows),
|
|
133
|
+
" \xB7 blown ",
|
|
134
|
+
pct(reading.stats.clipped.highlights),
|
|
135
|
+
" \xB7 average",
|
|
136
|
+
" ",
|
|
137
|
+
/* @__PURE__ */ jsx2("span", { className: "inline-block h-2.5 w-2.5 rounded-sm align-middle", style: { background: reading.stats.average } }),
|
|
138
|
+
" ",
|
|
139
|
+
reading.stats.average,
|
|
140
|
+
!reading.complete && /* @__PURE__ */ jsx2("span", { className: "text-muted-foreground", children: " \xB7 loading" })
|
|
141
|
+
] }),
|
|
142
|
+
reading.notes.map((n) => /* @__PURE__ */ jsx2("div", { className: "text-amber-500", children: n }, n))
|
|
143
|
+
] }) : /* @__PURE__ */ jsx2("div", { className: "text-muted-foreground", "data-scope-message": "", children: reading.message })
|
|
144
|
+
] });
|
|
145
|
+
}
|
|
146
|
+
|
|
79
147
|
// src/video-editor/VideoTransport.tsx
|
|
80
148
|
import { formatVideoTimecode } from "@pixerate/editor/video-editor";
|
|
81
|
-
import { Fragment, jsx as
|
|
149
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
82
150
|
var btn = "rounded border border-border px-2 py-0.5 text-xs disabled:opacity-40";
|
|
83
151
|
function VideoTransport({ controller, editable = true, className = "" }) {
|
|
84
152
|
const state = useVideoEditorState(controller);
|
|
85
153
|
const { time, duration, playing, project, selection, readOnly } = state;
|
|
86
154
|
const canEdit = editable && !readOnly;
|
|
87
|
-
return /* @__PURE__ */
|
|
88
|
-
/* @__PURE__ */
|
|
89
|
-
/* @__PURE__ */
|
|
90
|
-
/* @__PURE__ */
|
|
91
|
-
/* @__PURE__ */
|
|
155
|
+
return /* @__PURE__ */ jsxs2("div", { className: `flex flex-wrap items-center gap-1.5 text-xs ${className}`, "data-video-transport": "", children: [
|
|
156
|
+
/* @__PURE__ */ jsx3("button", { type: "button", className: btn, onClick: () => controller.step(-1), "aria-label": "Previous frame", title: "Previous frame (\u2190)", "data-step-back": "", children: "\u25C0|" }),
|
|
157
|
+
/* @__PURE__ */ jsx3("button", { type: "button", className: btn, onClick: () => controller.toggle(), "aria-label": playing ? "Pause" : "Play", title: "Play / pause (Space)", disabled: duration <= 0, "data-play": "", children: playing ? "Pause" : "Play" }),
|
|
158
|
+
/* @__PURE__ */ jsx3("button", { type: "button", className: btn, onClick: () => controller.step(1), "aria-label": "Next frame", title: "Next frame (\u2192)", "data-step-forward": "", children: "|\u25B6" }),
|
|
159
|
+
/* @__PURE__ */ jsxs2("span", { className: "px-1 tabular-nums text-muted-foreground", "data-timecode": "", children: [
|
|
92
160
|
formatVideoTimecode(time, project.fps),
|
|
93
161
|
" / ",
|
|
94
162
|
formatVideoTimecode(duration, project.fps)
|
|
95
163
|
] }),
|
|
96
|
-
editable && /* @__PURE__ */
|
|
97
|
-
/* @__PURE__ */
|
|
98
|
-
/* @__PURE__ */
|
|
99
|
-
/* @__PURE__ */
|
|
100
|
-
/* @__PURE__ */
|
|
164
|
+
editable && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
165
|
+
/* @__PURE__ */ jsx3("span", { className: "mx-1 h-4 w-px bg-border" }),
|
|
166
|
+
/* @__PURE__ */ jsx3("button", { type: "button", className: btn, onClick: () => controller.splitAtPlayhead(), disabled: !canEdit, title: "Split at the playhead (S)", "data-split": "", children: "Split" }),
|
|
167
|
+
/* @__PURE__ */ jsx3("button", { type: "button", className: btn, onClick: () => controller.deleteSelection(), disabled: !canEdit || !selection.length, title: "Delete the selected clips (Delete)", "data-delete": "", children: "Delete" }),
|
|
168
|
+
/* @__PURE__ */ jsx3(
|
|
101
169
|
"button",
|
|
102
170
|
{
|
|
103
171
|
type: "button",
|
|
@@ -109,14 +177,14 @@ function VideoTransport({ controller, editable = true, className = "" }) {
|
|
|
109
177
|
children: "Ripple delete"
|
|
110
178
|
}
|
|
111
179
|
),
|
|
112
|
-
/* @__PURE__ */
|
|
113
|
-
/* @__PURE__ */
|
|
180
|
+
/* @__PURE__ */ jsx3("button", { type: "button", className: btn, onClick: () => controller.undo(), disabled: !canEdit || !state.canUndo, title: state.canUndo ? `Undo ${state.undoLabel}` : "Nothing to undo", "data-undo": "", children: "Undo" }),
|
|
181
|
+
/* @__PURE__ */ jsx3("button", { type: "button", className: btn, onClick: () => controller.redo(), disabled: !canEdit || !state.canRedo, title: state.canRedo ? `Redo ${state.redoLabel}` : "Nothing to redo", "data-redo": "", children: "Redo" })
|
|
114
182
|
] })
|
|
115
183
|
] });
|
|
116
184
|
}
|
|
117
185
|
|
|
118
186
|
// src/video-editor/VideoTimeline.tsx
|
|
119
|
-
import { useEffect as
|
|
187
|
+
import { useEffect as useEffect4, useMemo as useMemo2, useRef as useRef4, useState as useState2, useSyncExternalStore as useSyncExternalStore2 } from "react";
|
|
120
188
|
import {
|
|
121
189
|
planVideoClipDrag,
|
|
122
190
|
videoDragLabel,
|
|
@@ -129,7 +197,7 @@ import {
|
|
|
129
197
|
videoRulerTicks,
|
|
130
198
|
formatVideoSeconds
|
|
131
199
|
} from "@pixerate/editor/video-editor";
|
|
132
|
-
import { Fragment as Fragment2, jsx as
|
|
200
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
133
201
|
var HEADER = 150;
|
|
134
202
|
var RULER = 24;
|
|
135
203
|
var ROW = 44;
|
|
@@ -143,25 +211,25 @@ function ClipWaveform({ controller, clip, asset, width, height }) {
|
|
|
143
211
|
() => controller.audioPeaks.peek(asset),
|
|
144
212
|
() => void 0
|
|
145
213
|
);
|
|
146
|
-
|
|
214
|
+
useEffect4(() => {
|
|
147
215
|
controller.audioPeaks.load(asset).catch(() => {
|
|
148
216
|
});
|
|
149
217
|
}, [controller, asset]);
|
|
150
218
|
const d = useMemo2(() => peaks ? videoClipWaveformPath(clip, peaks, width, height) : "", [peaks, clip, width, height]);
|
|
151
219
|
if (!d) return null;
|
|
152
|
-
return /* @__PURE__ */
|
|
220
|
+
return /* @__PURE__ */ jsx4("svg", { className: "pointer-events-none absolute inset-0", width, height, "aria-hidden": "true", "data-waveform": clip.id, children: /* @__PURE__ */ jsx4("path", { d, fill: "currentColor", fillOpacity: 0.45 }) });
|
|
153
221
|
}
|
|
154
222
|
var isField = (el) => el instanceof HTMLElement && /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName);
|
|
155
223
|
function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable = true, waveforms = "audio", className = "" }) {
|
|
156
224
|
const state = useVideoEditorState(controller);
|
|
157
225
|
const { project, duration, time, selection, readOnly, error } = state;
|
|
158
|
-
const [ownZoom, setOwnZoom] =
|
|
226
|
+
const [ownZoom, setOwnZoom] = useState2(pxPerSecond ?? 100);
|
|
159
227
|
const px = onPxPerSecondChange ? pxPerSecond ?? 100 : ownZoom;
|
|
160
228
|
const canEdit = editable && !readOnly;
|
|
161
|
-
const [drag, setDrag] =
|
|
162
|
-
const dragRef =
|
|
163
|
-
const rows =
|
|
164
|
-
const ruler =
|
|
229
|
+
const [drag, setDrag] = useState2(null);
|
|
230
|
+
const dragRef = useRef4(null);
|
|
231
|
+
const rows = useRef4(/* @__PURE__ */ new Map());
|
|
232
|
+
const ruler = useRef4(null);
|
|
165
233
|
const assets = videoAssetMap(project);
|
|
166
234
|
const msToPx = (ms) => ms * px / 1e3;
|
|
167
235
|
const span = Math.max(duration + 5e3, 1e4);
|
|
@@ -192,10 +260,10 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
|
|
|
192
260
|
dragRef.current = next;
|
|
193
261
|
setDrag(next);
|
|
194
262
|
};
|
|
195
|
-
const live =
|
|
263
|
+
const live = useRef4({ planFor, selection });
|
|
196
264
|
live.current = { planFor, selection };
|
|
197
|
-
const stopDrag =
|
|
198
|
-
|
|
265
|
+
const stopDrag = useRef4(null);
|
|
266
|
+
useEffect4(() => () => stopDrag.current?.(), []);
|
|
199
267
|
const onClipPointerDown = (e, clip, track) => {
|
|
200
268
|
if (e.button !== void 0 && e.button !== 0) return;
|
|
201
269
|
e.stopPropagation();
|
|
@@ -273,7 +341,7 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
|
|
|
273
341
|
}).filter((x) => x !== null)
|
|
274
342
|
)
|
|
275
343
|
}));
|
|
276
|
-
const toggle = (track, field2, label) => /* @__PURE__ */
|
|
344
|
+
const toggle = (track, field2, label) => /* @__PURE__ */ jsx4(
|
|
277
345
|
"button",
|
|
278
346
|
{
|
|
279
347
|
type: "button",
|
|
@@ -287,12 +355,12 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
|
|
|
287
355
|
children: field2 === "muted" ? "M" : field2 === "hidden" ? "H" : "L"
|
|
288
356
|
}
|
|
289
357
|
);
|
|
290
|
-
return /* @__PURE__ */
|
|
291
|
-
!onPxPerSecondChange && /* @__PURE__ */
|
|
358
|
+
return /* @__PURE__ */ jsxs3("div", { className: `flex flex-col gap-1 ${className}`, "data-video-timeline-wrapper": "", children: [
|
|
359
|
+
!onPxPerSecondChange && /* @__PURE__ */ jsxs3("label", { className: "flex items-center gap-2 self-end text-xs text-muted-foreground", children: [
|
|
292
360
|
"Zoom",
|
|
293
|
-
/* @__PURE__ */
|
|
361
|
+
/* @__PURE__ */ jsx4("input", { type: "range", min: MIN_ZOOM, max: MAX_ZOOM, step: 5, value: ownZoom, "aria-label": "Timeline zoom", onChange: (e) => setOwnZoom(Number(e.target.value)), "data-zoom": "" })
|
|
294
362
|
] }),
|
|
295
|
-
/* @__PURE__ */
|
|
363
|
+
/* @__PURE__ */ jsx4(
|
|
296
364
|
"div",
|
|
297
365
|
{
|
|
298
366
|
className: "relative overflow-x-auto rounded border border-border outline-none focus-visible:ring-2 focus-visible:ring-rose-500/50",
|
|
@@ -301,10 +369,10 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
|
|
|
301
369
|
"aria-label": "Video timeline",
|
|
302
370
|
onKeyDown,
|
|
303
371
|
"data-video-timeline": "",
|
|
304
|
-
children: /* @__PURE__ */
|
|
305
|
-
/* @__PURE__ */
|
|
306
|
-
/* @__PURE__ */
|
|
307
|
-
/* @__PURE__ */
|
|
372
|
+
children: /* @__PURE__ */ jsxs3("div", { className: "relative", style: { width: HEADER + width }, children: [
|
|
373
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex", style: { height: RULER }, children: [
|
|
374
|
+
/* @__PURE__ */ jsx4("div", { className: "sticky left-0 z-20 shrink-0 border-b border-r border-border bg-background", style: { width: HEADER } }),
|
|
375
|
+
/* @__PURE__ */ jsxs3(
|
|
308
376
|
"div",
|
|
309
377
|
{
|
|
310
378
|
ref: ruler,
|
|
@@ -327,8 +395,8 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
|
|
|
327
395
|
if (e.buttons & 1) seekAt(e.clientX);
|
|
328
396
|
},
|
|
329
397
|
children: [
|
|
330
|
-
ticks.map((t) => /* @__PURE__ */
|
|
331
|
-
(project.markers ?? []).map((m) => /* @__PURE__ */
|
|
398
|
+
ticks.map((t) => /* @__PURE__ */ jsx4("div", { className: `absolute bottom-0 w-px ${t.major ? "h-2.5 bg-muted-foreground/70" : "h-1 bg-muted-foreground/40"}`, style: { left: t.x }, "data-tick": t.time, children: t.label && /* @__PURE__ */ jsx4("span", { className: "absolute -top-3.5 left-1 whitespace-nowrap tabular-nums", children: t.label }) }, t.time)),
|
|
399
|
+
(project.markers ?? []).map((m) => /* @__PURE__ */ jsx4(
|
|
332
400
|
"div",
|
|
333
401
|
{
|
|
334
402
|
className: "absolute top-0 h-2.5 w-2.5 -translate-x-1/2 rotate-45",
|
|
@@ -342,19 +410,19 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
|
|
|
342
410
|
}
|
|
343
411
|
)
|
|
344
412
|
] }),
|
|
345
|
-
placed.map(({ track, clips }) => /* @__PURE__ */
|
|
346
|
-
/* @__PURE__ */
|
|
347
|
-
/* @__PURE__ */
|
|
348
|
-
/* @__PURE__ */
|
|
349
|
-
/* @__PURE__ */
|
|
413
|
+
placed.map(({ track, clips }) => /* @__PURE__ */ jsxs3("div", { className: "flex border-b border-border last:border-b-0", style: { height: ROW }, "data-track": track.id, children: [
|
|
414
|
+
/* @__PURE__ */ jsxs3("div", { className: "sticky left-0 z-20 flex shrink-0 flex-col justify-center gap-0.5 border-r border-border bg-background px-2 text-xs", style: { width: HEADER }, "data-track-header": "", children: [
|
|
415
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-1", children: [
|
|
416
|
+
/* @__PURE__ */ jsx4("span", { className: "truncate font-medium", children: track.name ?? track.id }),
|
|
417
|
+
/* @__PURE__ */ jsx4("span", { className: "rounded bg-muted px-1 text-[10px] uppercase text-muted-foreground", "data-track-kind": "", children: track.kind })
|
|
350
418
|
] }),
|
|
351
|
-
editable && /* @__PURE__ */
|
|
419
|
+
editable && /* @__PURE__ */ jsxs3("div", { className: "flex gap-1", children: [
|
|
352
420
|
toggle(track, "muted", "Mute"),
|
|
353
421
|
track.kind === "video" && toggle(track, "hidden", "Hide"),
|
|
354
422
|
toggle(track, "locked", "Lock")
|
|
355
423
|
] })
|
|
356
424
|
] }),
|
|
357
|
-
/* @__PURE__ */
|
|
425
|
+
/* @__PURE__ */ jsx4(
|
|
358
426
|
"div",
|
|
359
427
|
{
|
|
360
428
|
ref: (el) => {
|
|
@@ -371,7 +439,7 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
|
|
|
371
439
|
const asset = clip.type === "media" ? assets.get(clip.asset) : void 0;
|
|
372
440
|
const selected = selection.includes(clip.id);
|
|
373
441
|
const label = videoClipLabel(clip, asset);
|
|
374
|
-
return /* @__PURE__ */
|
|
442
|
+
return /* @__PURE__ */ jsxs3(
|
|
375
443
|
"div",
|
|
376
444
|
{
|
|
377
445
|
className: `absolute top-1 bottom-1 select-none touch-none overflow-hidden rounded text-[11px] text-white ${canEdit && !home.locked ? "cursor-grab" : "cursor-default"} ${dragging ? "opacity-80 shadow-lg" : ""}`,
|
|
@@ -390,15 +458,15 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
|
|
|
390
458
|
"aria-selected": selected,
|
|
391
459
|
onPointerDown: (e) => onClipPointerDown(e, clip, home),
|
|
392
460
|
children: [
|
|
393
|
-
videoClipShowsWaveform(clip, asset, waveforms) && /* @__PURE__ */
|
|
394
|
-
/* @__PURE__ */
|
|
395
|
-
clip.transition && /* @__PURE__ */
|
|
461
|
+
videoClipShowsWaveform(clip, asset, waveforms) && /* @__PURE__ */ jsx4(ClipWaveform, { controller, clip, asset, width: Math.max(2, msToPx(clip.duration)), height: ROW - 8 }),
|
|
462
|
+
/* @__PURE__ */ jsx4("span", { className: "pointer-events-none relative block truncate px-1.5 py-0.5 leading-4", children: label }),
|
|
463
|
+
clip.transition && /* @__PURE__ */ jsxs3("span", { className: "pointer-events-none absolute left-1 bottom-0.5 rounded bg-black/40 px-1 text-[9px] leading-3", title: `${clip.transition.type} ${clip.transition.duration} ms`, "data-transition": clip.transition.type, children: [
|
|
396
464
|
"\u29D3 ",
|
|
397
465
|
clip.transition.type
|
|
398
466
|
] }),
|
|
399
|
-
canEdit && !home.locked && /* @__PURE__ */
|
|
400
|
-
/* @__PURE__ */
|
|
401
|
-
/* @__PURE__ */
|
|
467
|
+
canEdit && !home.locked && /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
468
|
+
/* @__PURE__ */ jsx4("div", { className: "absolute inset-y-0 left-0 cursor-ew-resize bg-white/0 hover:bg-white/40", style: { width: HANDLE }, "data-handle": "start", "aria-hidden": "true" }),
|
|
469
|
+
/* @__PURE__ */ jsx4("div", { className: "absolute inset-y-0 right-0 cursor-ew-resize bg-white/0 hover:bg-white/40", style: { width: HANDLE }, "data-handle": "end", "aria-hidden": "true" })
|
|
402
470
|
] })
|
|
403
471
|
]
|
|
404
472
|
},
|
|
@@ -408,11 +476,11 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
|
|
|
408
476
|
}
|
|
409
477
|
)
|
|
410
478
|
] }, track.id)),
|
|
411
|
-
/* @__PURE__ */
|
|
479
|
+
/* @__PURE__ */ jsx4("div", { className: "pointer-events-none absolute top-0 bottom-0 w-0.5 -translate-x-1/2 bg-rose-500", style: { left: HEADER + msToPx(time), zIndex: 15 }, "data-playhead": "" })
|
|
412
480
|
] })
|
|
413
481
|
}
|
|
414
482
|
),
|
|
415
|
-
error && /* @__PURE__ */
|
|
483
|
+
error && /* @__PURE__ */ jsx4("p", { className: "text-xs text-red-500", role: "alert", "data-edit-error": "", children: error })
|
|
416
484
|
] });
|
|
417
485
|
}
|
|
418
486
|
|
|
@@ -442,7 +510,7 @@ import {
|
|
|
442
510
|
videoKeyframeToggleEdit,
|
|
443
511
|
videoKeyframeValueEdit
|
|
444
512
|
} from "@pixerate/editor/video-editor";
|
|
445
|
-
import { Fragment as Fragment3, jsx as
|
|
513
|
+
import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
446
514
|
var btn2 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
|
|
447
515
|
var field = "w-full rounded border border-border bg-transparent px-1 py-0.5 text-xs disabled:opacity-50";
|
|
448
516
|
var DEFAULT_TRANSITION = 500;
|
|
@@ -456,7 +524,7 @@ function Commit({
|
|
|
456
524
|
if (raw !== value) onCommit(raw);
|
|
457
525
|
};
|
|
458
526
|
if (multiline)
|
|
459
|
-
return /* @__PURE__ */
|
|
527
|
+
return /* @__PURE__ */ jsx5(
|
|
460
528
|
"textarea",
|
|
461
529
|
{
|
|
462
530
|
className: field,
|
|
@@ -468,11 +536,11 @@ function Commit({
|
|
|
468
536
|
},
|
|
469
537
|
value
|
|
470
538
|
);
|
|
471
|
-
return /* @__PURE__ */
|
|
539
|
+
return /* @__PURE__ */ jsx5("input", { className: field, ...rest, defaultValue: value, onKeyDown: (e) => e.key === "Enter" && e.currentTarget.blur(), onBlur: (e) => commit(e.currentTarget.value) }, value);
|
|
472
540
|
}
|
|
473
541
|
function Row({ label, name, children }) {
|
|
474
|
-
return /* @__PURE__ */
|
|
475
|
-
/* @__PURE__ */
|
|
542
|
+
return /* @__PURE__ */ jsxs4("label", { className: "grid grid-cols-[5.5rem_minmax(0,1fr)] items-center gap-2 text-xs", "data-field": name, children: [
|
|
543
|
+
/* @__PURE__ */ jsx5("span", { className: "text-muted-foreground", children: label }),
|
|
476
544
|
children
|
|
477
545
|
] });
|
|
478
546
|
}
|
|
@@ -480,7 +548,7 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
480
548
|
const state = useVideoEditorState(controller);
|
|
481
549
|
const { project, selection, readOnly, time } = state;
|
|
482
550
|
if (selection.length !== 1)
|
|
483
|
-
return /* @__PURE__ */
|
|
551
|
+
return /* @__PURE__ */ jsx5("p", { className: `text-xs text-muted-foreground ${className}`, "data-clip-inspector": "", children: selection.length ? `${selection.length} clips selected. Select one clip to edit it.` : "Select a clip on the timeline to edit it." });
|
|
484
552
|
const at = findVideoClip(project, selection[0]);
|
|
485
553
|
if (!at) return null;
|
|
486
554
|
const clip = at.clip;
|
|
@@ -497,24 +565,24 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
497
565
|
};
|
|
498
566
|
const text = (key, label) => {
|
|
499
567
|
const value = clip[key];
|
|
500
|
-
return /* @__PURE__ */
|
|
568
|
+
return /* @__PURE__ */ jsx5(Commit, { value: value === void 0 ? "" : String(value), "aria-label": label ?? key, disabled, onCommit: (raw) => set(key, raw.trim() || void 0, `Change ${label ?? key}`) });
|
|
501
569
|
};
|
|
502
570
|
const number = (key, label, attrs = {}) => {
|
|
503
571
|
const value = clip[key];
|
|
504
|
-
return /* @__PURE__ */
|
|
572
|
+
return /* @__PURE__ */ jsx5(Commit, { type: "number", value: value === void 0 ? "" : String(value), "aria-label": label, disabled, onCommit: (raw) => num(key, raw, `Change ${label.toLowerCase()}`), ...attrs });
|
|
505
573
|
};
|
|
506
574
|
const transition = clip.transition;
|
|
507
|
-
return /* @__PURE__ */
|
|
508
|
-
/* @__PURE__ */
|
|
509
|
-
/* @__PURE__ */
|
|
510
|
-
/* @__PURE__ */
|
|
511
|
-
at.track.locked && /* @__PURE__ */
|
|
575
|
+
return /* @__PURE__ */ jsxs4("div", { className: `flex flex-col gap-1.5 ${className}`, "data-clip-inspector": "", "data-clip-id": clip.id, children: [
|
|
576
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2 text-xs", children: [
|
|
577
|
+
/* @__PURE__ */ jsx5("span", { className: "font-medium", children: clip.type === "media" ? "Media clip" : clip.type === "text" ? "Title" : "Colour clip" }),
|
|
578
|
+
/* @__PURE__ */ jsx5("code", { className: "text-muted-foreground", children: clip.id }),
|
|
579
|
+
at.track.locked && /* @__PURE__ */ jsx5("span", { className: "text-amber-500", children: "locked track" })
|
|
512
580
|
] }),
|
|
513
|
-
/* @__PURE__ */
|
|
514
|
-
/* @__PURE__ */
|
|
515
|
-
/* @__PURE__ */
|
|
516
|
-
videoKeyframeRows(project, clip.id, time).map((row) => /* @__PURE__ */
|
|
517
|
-
/* @__PURE__ */
|
|
581
|
+
/* @__PURE__ */ jsx5(Row, { label: "Name", name: "name", children: text("name", "Name") }),
|
|
582
|
+
/* @__PURE__ */ jsx5(Row, { label: "Start", name: "start", children: /* @__PURE__ */ jsx5("span", { className: "tabular-nums text-xs", children: formatVideoSeconds2(clip.start) }) }),
|
|
583
|
+
/* @__PURE__ */ jsx5(Row, { label: "Duration", name: "duration", children: /* @__PURE__ */ jsx5("span", { className: "tabular-nums text-xs", children: formatVideoSeconds2(clip.duration) }) }),
|
|
584
|
+
videoKeyframeRows(project, clip.id, time).map((row) => /* @__PURE__ */ jsx5(Row, { label: row.label, name: row.property, children: /* @__PURE__ */ jsxs4("span", { className: "flex min-w-0 items-center gap-1", "data-keyframe-row": row.property, children: [
|
|
585
|
+
/* @__PURE__ */ jsx5(
|
|
518
586
|
Commit,
|
|
519
587
|
{
|
|
520
588
|
type: "number",
|
|
@@ -533,7 +601,7 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
533
601
|
}
|
|
534
602
|
}
|
|
535
603
|
),
|
|
536
|
-
/* @__PURE__ */
|
|
604
|
+
/* @__PURE__ */ jsx5(
|
|
537
605
|
"button",
|
|
538
606
|
{
|
|
539
607
|
type: "button",
|
|
@@ -550,11 +618,11 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
550
618
|
children: row.onKey ? "\u25C6" : "\u25C7"
|
|
551
619
|
}
|
|
552
620
|
),
|
|
553
|
-
row.keyed && /* @__PURE__ */
|
|
621
|
+
row.keyed && /* @__PURE__ */ jsx5("span", { className: "shrink-0 text-[10px] tabular-nums text-muted-foreground", "data-keyframe-count": row.property, children: row.keys })
|
|
554
622
|
] }) }, row.property)),
|
|
555
|
-
at.track.kind === "video" && /* @__PURE__ */
|
|
556
|
-
at.track.kind === "video" && /* @__PURE__ */
|
|
557
|
-
/* @__PURE__ */
|
|
623
|
+
at.track.kind === "video" && /* @__PURE__ */ jsx5(Row, { label: "Blend", name: "blend", children: /* @__PURE__ */ jsx5("select", { className: field, value: clip.blend ?? "normal", disabled, "aria-label": "Blend mode", onChange: (e) => set("blend", e.target.value === "normal" ? void 0 : e.target.value, "Change blend mode"), children: VIDEO_BLEND_MODES.map((m) => /* @__PURE__ */ jsx5("option", { value: m, children: VIDEO_BLEND_MODE_LABELS[m] }, m)) }) }),
|
|
624
|
+
at.track.kind === "video" && /* @__PURE__ */ jsxs4(Fragment3, { children: [
|
|
625
|
+
/* @__PURE__ */ jsx5(Row, { label: "Look", name: "gradePreset", children: /* @__PURE__ */ jsxs4(
|
|
558
626
|
"select",
|
|
559
627
|
{
|
|
560
628
|
className: field,
|
|
@@ -566,12 +634,12 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
566
634
|
if (grade) controller.apply({ op: "update_clip", clip: clip.id, set: { grade } }, `Apply ${e.target.value} look`);
|
|
567
635
|
},
|
|
568
636
|
children: [
|
|
569
|
-
/* @__PURE__ */
|
|
570
|
-
VIDEO_GRADE_PRESET_NAMES.map((name) => /* @__PURE__ */
|
|
637
|
+
/* @__PURE__ */ jsx5("option", { value: "", children: clip.grade ? "Graded" : "None" }),
|
|
638
|
+
VIDEO_GRADE_PRESET_NAMES.map((name) => /* @__PURE__ */ jsx5("option", { value: name, children: name.replace(/-/g, " ") }, name))
|
|
571
639
|
]
|
|
572
640
|
}
|
|
573
641
|
) }),
|
|
574
|
-
VIDEO_GRADE_CONTROLS.map((c) => /* @__PURE__ */
|
|
642
|
+
VIDEO_GRADE_CONTROLS.map((c) => /* @__PURE__ */ jsx5(Row, { label: c.label, name: `grade.${c.key}`, children: /* @__PURE__ */ jsx5(
|
|
575
643
|
Commit,
|
|
576
644
|
{
|
|
577
645
|
type: "number",
|
|
@@ -591,7 +659,7 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
591
659
|
}
|
|
592
660
|
) }, c.key))
|
|
593
661
|
] }),
|
|
594
|
-
at.track.kind === "video" && ["animateIn", "animateOut"].map((key) => /* @__PURE__ */
|
|
662
|
+
at.track.kind === "video" && ["animateIn", "animateOut"].map((key) => /* @__PURE__ */ jsx5(Row, { label: key === "animateIn" ? "Animate in" : "Animate out", name: key, children: /* @__PURE__ */ jsxs4(
|
|
595
663
|
"select",
|
|
596
664
|
{
|
|
597
665
|
className: field,
|
|
@@ -603,20 +671,20 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
603
671
|
set(key, effect ? { ...clip[key], effect } : void 0, key === "animateIn" ? "Change entrance" : "Change exit");
|
|
604
672
|
},
|
|
605
673
|
children: [
|
|
606
|
-
/* @__PURE__ */
|
|
607
|
-
VIDEO_CLIP_ANIMATION_EFFECTS.map((effect) => /* @__PURE__ */
|
|
674
|
+
/* @__PURE__ */ jsx5("option", { value: "", children: "None" }),
|
|
675
|
+
VIDEO_CLIP_ANIMATION_EFFECTS.map((effect) => /* @__PURE__ */ jsx5("option", { value: effect, children: VIDEO_CLIP_ANIMATION_LABELS[effect] }, effect))
|
|
608
676
|
]
|
|
609
677
|
}
|
|
610
678
|
) }, key)),
|
|
611
|
-
/* @__PURE__ */
|
|
612
|
-
/* @__PURE__ */
|
|
613
|
-
clip.type === "media" && /* @__PURE__ */
|
|
614
|
-
/* @__PURE__ */
|
|
615
|
-
/* @__PURE__ */
|
|
616
|
-
/* @__PURE__ */
|
|
679
|
+
/* @__PURE__ */ jsx5(Row, { label: "Fade in (ms)", name: "fadeIn", children: number("fadeIn", "Fade in", { min: 0, step: 100 }) }),
|
|
680
|
+
/* @__PURE__ */ jsx5(Row, { label: "Fade out (ms)", name: "fadeOut", children: number("fadeOut", "Fade out", { min: 0, step: 100 }) }),
|
|
681
|
+
clip.type === "media" && /* @__PURE__ */ jsxs4(Fragment3, { children: [
|
|
682
|
+
/* @__PURE__ */ jsx5(Row, { label: "Speed", name: "speed", children: clip.timeRemap ? /* @__PURE__ */ jsxs4("span", { className: "flex items-center gap-1", "data-ramped": "", children: [
|
|
683
|
+
/* @__PURE__ */ jsx5("span", { className: "text-muted-foreground", children: "ramped" }),
|
|
684
|
+
/* @__PURE__ */ jsx5("button", { type: "button", className: btn2, disabled, onClick: () => controller.apply({ op: "set_time_remap", clip: clip.id, keys: null, ripple: true }, "Remove speed ramp"), children: "Remove ramp" })
|
|
617
685
|
] }) : number("speed", "Speed", { min: 0.1, step: 0.25 }) }),
|
|
618
|
-
project.assets.find((a) => a.id === clip.asset)?.kind !== "image" && /* @__PURE__ */
|
|
619
|
-
/* @__PURE__ */
|
|
686
|
+
project.assets.find((a) => a.id === clip.asset)?.kind !== "image" && /* @__PURE__ */ jsx5(Row, { label: "Retime", name: "retime", children: /* @__PURE__ */ jsxs4("span", { className: "flex flex-wrap gap-1", children: [
|
|
687
|
+
/* @__PURE__ */ jsx5(
|
|
620
688
|
"button",
|
|
621
689
|
{
|
|
622
690
|
type: "button",
|
|
@@ -627,11 +695,11 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
627
695
|
children: "Freeze 1 s here"
|
|
628
696
|
}
|
|
629
697
|
),
|
|
630
|
-
/* @__PURE__ */
|
|
698
|
+
/* @__PURE__ */ jsx5("button", { type: "button", className: btn2, disabled, title: "Normal speed, then a glide into 0.3\xD7 slow motion", onClick: () => controller.apply(videoSpeedRampEdit(clip, videoSlowMotionRamp(clip.duration)), "Speed ramp"), children: "Slow-mo ramp" })
|
|
631
699
|
] }) }),
|
|
632
|
-
/* @__PURE__ */
|
|
700
|
+
/* @__PURE__ */ jsx5(Row, { label: "Muted", name: "muted", children: /* @__PURE__ */ jsx5("input", { type: "checkbox", className: "justify-self-start", checked: !!clip.muted, disabled, "aria-label": "Muted", onChange: (e) => set("muted", e.target.checked || void 0, e.target.checked ? "Mute clip" : "Unmute clip") }) }),
|
|
633
701
|
["fadeInCurve", "fadeOutCurve"].map(
|
|
634
|
-
(key) => (key === "fadeInCurve" ? clip.fadeIn : clip.fadeOut) ? /* @__PURE__ */
|
|
702
|
+
(key) => (key === "fadeInCurve" ? clip.fadeIn : clip.fadeOut) ? /* @__PURE__ */ jsx5(Row, { label: key === "fadeInCurve" ? "Fade in sound" : "Fade out sound", name: key, children: /* @__PURE__ */ jsx5(
|
|
635
703
|
"select",
|
|
636
704
|
{
|
|
637
705
|
className: field,
|
|
@@ -642,19 +710,19 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
642
710
|
const curve = e.target.value;
|
|
643
711
|
set(key, curve === "linear" ? void 0 : curve, key === "fadeInCurve" ? "Change fade in curve" : "Change fade out curve");
|
|
644
712
|
},
|
|
645
|
-
children: VIDEO_FADE_CURVES.map((c) => /* @__PURE__ */
|
|
713
|
+
children: VIDEO_FADE_CURVES.map((c) => /* @__PURE__ */ jsx5("option", { value: c, children: VIDEO_FADE_CURVE_LABELS[c] }, c))
|
|
646
714
|
}
|
|
647
715
|
) }, key) : null
|
|
648
716
|
)
|
|
649
717
|
] }),
|
|
650
|
-
clip.type === "text" && /* @__PURE__ */
|
|
651
|
-
/* @__PURE__ */
|
|
652
|
-
/* @__PURE__ */
|
|
653
|
-
/* @__PURE__ */
|
|
654
|
-
/* @__PURE__ */
|
|
718
|
+
clip.type === "text" && /* @__PURE__ */ jsxs4(Fragment3, { children: [
|
|
719
|
+
/* @__PURE__ */ jsx5(Row, { label: "Text", name: "text", children: /* @__PURE__ */ jsx5(Commit, { multiline: true, value: clip.text, "aria-label": "Text", disabled, onCommit: (raw) => set("text", raw, "Change text") }) }),
|
|
720
|
+
/* @__PURE__ */ jsx5(Row, { label: "Colour", name: "color", children: text("color", "Colour") }),
|
|
721
|
+
/* @__PURE__ */ jsx5(Row, { label: "Font size", name: "fontSize", children: number("fontSize", "Font size", { min: 1, step: 2 }) }),
|
|
722
|
+
/* @__PURE__ */ jsx5(Row, { label: "Background", name: "background", children: text("background", "Background") })
|
|
655
723
|
] }),
|
|
656
|
-
clip.type === "color" && /* @__PURE__ */
|
|
657
|
-
/* @__PURE__ */
|
|
724
|
+
clip.type === "color" && /* @__PURE__ */ jsx5(Row, { label: "Colour", name: "color", children: /* @__PURE__ */ jsx5(Commit, { value: clip.color, "aria-label": "Colour", disabled, onCommit: (raw) => raw.trim() && set("color", raw.trim(), "Change colour") }) }),
|
|
725
|
+
/* @__PURE__ */ jsx5(Row, { label: "Transition in", name: "transition", children: /* @__PURE__ */ jsxs4(
|
|
658
726
|
"select",
|
|
659
727
|
{
|
|
660
728
|
className: field,
|
|
@@ -669,12 +737,12 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
669
737
|
);
|
|
670
738
|
},
|
|
671
739
|
children: [
|
|
672
|
-
/* @__PURE__ */
|
|
673
|
-
VIDEO_TRANSITION_TYPES.map((t) => /* @__PURE__ */
|
|
740
|
+
/* @__PURE__ */ jsx5("option", { value: "", children: "None" }),
|
|
741
|
+
VIDEO_TRANSITION_TYPES.map((t) => /* @__PURE__ */ jsx5("option", { value: t, children: VIDEO_TRANSITION_LABELS[t] }, t))
|
|
674
742
|
]
|
|
675
743
|
}
|
|
676
744
|
) }),
|
|
677
|
-
transition && /* @__PURE__ */
|
|
745
|
+
transition && /* @__PURE__ */ jsx5(Row, { label: "Length (ms)", name: "transitionDuration", children: /* @__PURE__ */ jsx5(
|
|
678
746
|
Commit,
|
|
679
747
|
{
|
|
680
748
|
type: "number",
|
|
@@ -689,7 +757,7 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
689
757
|
}
|
|
690
758
|
}
|
|
691
759
|
) }),
|
|
692
|
-
transition && videoTransitionHasDirection(transition.type) && /* @__PURE__ */
|
|
760
|
+
transition && videoTransitionHasDirection(transition.type) && /* @__PURE__ */ jsx5(Row, { label: "Direction", name: "transitionDirection", children: /* @__PURE__ */ jsx5(
|
|
693
761
|
"select",
|
|
694
762
|
{
|
|
695
763
|
className: field,
|
|
@@ -697,10 +765,10 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
697
765
|
disabled,
|
|
698
766
|
"aria-label": "Transition direction",
|
|
699
767
|
onChange: (e) => controller.apply({ op: "set_transition", clip: clip.id, transition: { ...transition, direction: e.target.value } }, "Change transition direction"),
|
|
700
|
-
children: VIDEO_WIPE_DIRECTIONS.map((d) => /* @__PURE__ */
|
|
768
|
+
children: VIDEO_WIPE_DIRECTIONS.map((d) => /* @__PURE__ */ jsx5("option", { value: d, children: d }, d))
|
|
701
769
|
}
|
|
702
770
|
) }),
|
|
703
|
-
transition && /* @__PURE__ */
|
|
771
|
+
transition && /* @__PURE__ */ jsx5(Row, { label: "Transition sound", name: "transitionAudioCurve", children: /* @__PURE__ */ jsx5(
|
|
704
772
|
"select",
|
|
705
773
|
{
|
|
706
774
|
className: field,
|
|
@@ -713,30 +781,30 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
713
781
|
const next = curve === videoTransitionAudioCurve(rest) ? rest : { ...rest, audioCurve: curve };
|
|
714
782
|
controller.apply({ op: "set_transition", clip: clip.id, transition: next }, "Change transition sound");
|
|
715
783
|
},
|
|
716
|
-
children: VIDEO_FADE_CURVES.map((c) => /* @__PURE__ */
|
|
784
|
+
children: VIDEO_FADE_CURVES.map((c) => /* @__PURE__ */ jsx5("option", { value: c, children: VIDEO_FADE_CURVE_LABELS[c] }, c))
|
|
717
785
|
}
|
|
718
786
|
) })
|
|
719
787
|
] });
|
|
720
788
|
}
|
|
721
789
|
|
|
722
790
|
// src/video-editor/MediaBin.tsx
|
|
723
|
-
import { useState as
|
|
791
|
+
import { useState as useState3 } from "react";
|
|
724
792
|
import {
|
|
725
793
|
formatVideoSeconds as formatVideoSeconds3,
|
|
726
794
|
probeVideoMedia,
|
|
727
795
|
videoAssetDropEdit,
|
|
728
796
|
videoAssetKindOf
|
|
729
797
|
} from "@pixerate/editor/video-editor";
|
|
730
|
-
import { Fragment as Fragment4, jsx as
|
|
798
|
+
import { Fragment as Fragment4, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
731
799
|
var btn3 = "rounded border border-border px-2 py-0.5 text-xs disabled:opacity-40";
|
|
732
800
|
function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMedia, className = "" }) {
|
|
733
801
|
const { project, readOnly } = useVideoEditorState(controller);
|
|
734
802
|
const canEdit = editable && !readOnly;
|
|
735
|
-
const [url, setUrl] =
|
|
736
|
-
const [urlKind, setUrlKind] =
|
|
737
|
-
const [busy, setBusy] =
|
|
738
|
-
const [problem, setProblem] =
|
|
739
|
-
const [over, setOver] =
|
|
803
|
+
const [url, setUrl] = useState3("");
|
|
804
|
+
const [urlKind, setUrlKind] = useState3("");
|
|
805
|
+
const [busy, setBusy] = useState3(0);
|
|
806
|
+
const [problem, setProblem] = useState3(null);
|
|
807
|
+
const [over, setOver] = useState3(false);
|
|
740
808
|
const addSource = async (src, kind, name) => {
|
|
741
809
|
setBusy((n) => n + 1);
|
|
742
810
|
try {
|
|
@@ -784,7 +852,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMe
|
|
|
784
852
|
if (added) controller.select([added]);
|
|
785
853
|
}
|
|
786
854
|
};
|
|
787
|
-
return /* @__PURE__ */
|
|
855
|
+
return /* @__PURE__ */ jsxs5(
|
|
788
856
|
"div",
|
|
789
857
|
{
|
|
790
858
|
className: `flex flex-col gap-2 rounded ${over ? "ring-2 ring-rose-500/60" : ""} ${className}`,
|
|
@@ -802,20 +870,20 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMe
|
|
|
802
870
|
void addFiles(e.dataTransfer.files);
|
|
803
871
|
},
|
|
804
872
|
children: [
|
|
805
|
-
/* @__PURE__ */
|
|
806
|
-
project.assets.length ? /* @__PURE__ */
|
|
807
|
-
/* @__PURE__ */
|
|
808
|
-
/* @__PURE__ */
|
|
809
|
-
asset.duration !== void 0 && /* @__PURE__ */
|
|
810
|
-
editable && /* @__PURE__ */
|
|
811
|
-
] }, asset.id)) }) : /* @__PURE__ */
|
|
873
|
+
/* @__PURE__ */ jsx6("div", { className: "text-xs font-medium", children: "Media" }),
|
|
874
|
+
project.assets.length ? /* @__PURE__ */ jsx6("ul", { className: "flex flex-col gap-1", "aria-label": "Media assets", children: project.assets.map((asset) => /* @__PURE__ */ jsxs5("li", { className: "flex items-center gap-2 text-xs", "data-asset": asset.id, children: [
|
|
875
|
+
/* @__PURE__ */ jsx6("span", { className: "rounded bg-muted px-1 text-[10px] uppercase text-muted-foreground", "data-asset-kind": "", children: asset.kind }),
|
|
876
|
+
/* @__PURE__ */ jsx6("span", { className: "min-w-0 flex-1 truncate", title: asset.src, children: asset.name ?? asset.id }),
|
|
877
|
+
asset.duration !== void 0 && /* @__PURE__ */ jsx6("span", { className: "tabular-nums text-muted-foreground", children: formatVideoSeconds3(asset.duration) }),
|
|
878
|
+
editable && /* @__PURE__ */ jsx6("button", { type: "button", className: btn3, disabled: !canEdit, onClick: () => addToTimeline(asset), "data-add-to-timeline": "", children: "Add to timeline" })
|
|
879
|
+
] }, asset.id)) }) : /* @__PURE__ */ jsxs5("p", { className: "text-xs text-muted-foreground", children: [
|
|
812
880
|
"No media yet.",
|
|
813
881
|
editable ? " Choose or drop files, or add a URL." : ""
|
|
814
882
|
] }),
|
|
815
|
-
editable && /* @__PURE__ */
|
|
816
|
-
/* @__PURE__ */
|
|
883
|
+
editable && /* @__PURE__ */ jsxs5(Fragment4, { children: [
|
|
884
|
+
/* @__PURE__ */ jsxs5("label", { className: "flex flex-col gap-1 rounded border border-dashed border-border p-2 text-xs text-muted-foreground", children: [
|
|
817
885
|
"Choose or drop video, audio or image files",
|
|
818
|
-
/* @__PURE__ */
|
|
886
|
+
/* @__PURE__ */ jsx6(
|
|
819
887
|
"input",
|
|
820
888
|
{
|
|
821
889
|
type: "file",
|
|
@@ -832,7 +900,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMe
|
|
|
832
900
|
}
|
|
833
901
|
)
|
|
834
902
|
] }),
|
|
835
|
-
/* @__PURE__ */
|
|
903
|
+
/* @__PURE__ */ jsxs5(
|
|
836
904
|
"form",
|
|
837
905
|
{
|
|
838
906
|
className: "flex items-center gap-1",
|
|
@@ -841,7 +909,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMe
|
|
|
841
909
|
void addUrl();
|
|
842
910
|
},
|
|
843
911
|
children: [
|
|
844
|
-
/* @__PURE__ */
|
|
912
|
+
/* @__PURE__ */ jsx6(
|
|
845
913
|
"input",
|
|
846
914
|
{
|
|
847
915
|
className: "min-w-0 flex-1 rounded border border-border bg-transparent px-1 py-0.5 text-xs",
|
|
@@ -852,26 +920,26 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMe
|
|
|
852
920
|
onChange: (e) => setUrl(e.target.value)
|
|
853
921
|
}
|
|
854
922
|
),
|
|
855
|
-
/* @__PURE__ */
|
|
856
|
-
/* @__PURE__ */
|
|
857
|
-
/* @__PURE__ */
|
|
858
|
-
/* @__PURE__ */
|
|
859
|
-
/* @__PURE__ */
|
|
923
|
+
/* @__PURE__ */ jsxs5("select", { className: "rounded border border-border bg-transparent px-1 py-0.5 text-xs", value: urlKind, disabled: !canEdit, "aria-label": "Media kind", onChange: (e) => setUrlKind(e.target.value), children: [
|
|
924
|
+
/* @__PURE__ */ jsx6("option", { value: "", children: "Auto" }),
|
|
925
|
+
/* @__PURE__ */ jsx6("option", { value: "video", children: "Video" }),
|
|
926
|
+
/* @__PURE__ */ jsx6("option", { value: "audio", children: "Audio" }),
|
|
927
|
+
/* @__PURE__ */ jsx6("option", { value: "image", children: "Image" })
|
|
860
928
|
] }),
|
|
861
|
-
/* @__PURE__ */
|
|
929
|
+
/* @__PURE__ */ jsx6("button", { type: "submit", className: btn3, disabled: !canEdit || !url.trim(), "data-add-url": "", children: "Add" })
|
|
862
930
|
]
|
|
863
931
|
}
|
|
864
932
|
)
|
|
865
933
|
] }),
|
|
866
|
-
busy > 0 && /* @__PURE__ */
|
|
867
|
-
problem && /* @__PURE__ */
|
|
934
|
+
busy > 0 && /* @__PURE__ */ jsx6("p", { className: "text-xs text-muted-foreground", children: "Reading media\u2026" }),
|
|
935
|
+
problem && /* @__PURE__ */ jsx6("p", { className: "text-xs text-red-500", role: "alert", "data-media-error": "", children: problem })
|
|
868
936
|
]
|
|
869
937
|
}
|
|
870
938
|
);
|
|
871
939
|
}
|
|
872
940
|
|
|
873
941
|
// src/video-editor/TranscriptPanel.tsx
|
|
874
|
-
import
|
|
942
|
+
import React5, { useMemo as useMemo3, useState as useState4 } from "react";
|
|
875
943
|
import {
|
|
876
944
|
findVideoFillers,
|
|
877
945
|
findVideoTranscriptPauses,
|
|
@@ -881,15 +949,15 @@ import {
|
|
|
881
949
|
videoWordAt,
|
|
882
950
|
videoWordsRange
|
|
883
951
|
} from "@pixerate/editor/video-editor";
|
|
884
|
-
import { Fragment as Fragment5, jsx as
|
|
952
|
+
import { Fragment as Fragment5, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
885
953
|
var btn4 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
|
|
886
954
|
function TranscriptPanel({ controller, track, editable = true, className = "" }) {
|
|
887
955
|
const state = useVideoEditorState(controller);
|
|
888
956
|
const { project, time, readOnly } = state;
|
|
889
957
|
const canEdit = editable && !readOnly;
|
|
890
|
-
const [anchor, setAnchor] =
|
|
891
|
-
const [focus, setFocus] =
|
|
892
|
-
const [message, setMessage] =
|
|
958
|
+
const [anchor, setAnchor] = useState4(null);
|
|
959
|
+
const [focus, setFocus] = useState4(null);
|
|
960
|
+
const [message, setMessage] = useState4(null);
|
|
893
961
|
const words = useMemo3(() => {
|
|
894
962
|
try {
|
|
895
963
|
return videoTimelineTranscript(project, { track });
|
|
@@ -916,9 +984,9 @@ function TranscriptPanel({ controller, track, editable = true, className = "" })
|
|
|
916
984
|
setMessage(e instanceof Error ? e.message : String(e));
|
|
917
985
|
}
|
|
918
986
|
};
|
|
919
|
-
return /* @__PURE__ */
|
|
920
|
-
words.length > 0 ? /* @__PURE__ */
|
|
921
|
-
/* @__PURE__ */
|
|
987
|
+
return /* @__PURE__ */ jsxs6("div", { className: `flex flex-col gap-2 text-xs ${className}`, "data-transcript-panel": "", children: [
|
|
988
|
+
words.length > 0 ? /* @__PURE__ */ jsxs6(Fragment5, { children: [
|
|
989
|
+
/* @__PURE__ */ jsx7(
|
|
922
990
|
"div",
|
|
923
991
|
{
|
|
924
992
|
className: "max-h-48 overflow-y-auto leading-6",
|
|
@@ -934,8 +1002,8 @@ function TranscriptPanel({ controller, track, editable = true, className = "" })
|
|
|
934
1002
|
},
|
|
935
1003
|
children: words.map((w, i) => {
|
|
936
1004
|
const inSelection = selected !== null && i >= selected[0] && i <= selected[1];
|
|
937
|
-
return /* @__PURE__ */
|
|
938
|
-
/* @__PURE__ */
|
|
1005
|
+
return /* @__PURE__ */ jsxs6(React5.Fragment, { children: [
|
|
1006
|
+
/* @__PURE__ */ jsx7(
|
|
939
1007
|
"span",
|
|
940
1008
|
{
|
|
941
1009
|
role: "option",
|
|
@@ -961,51 +1029,52 @@ function TranscriptPanel({ controller, track, editable = true, className = "" })
|
|
|
961
1029
|
})
|
|
962
1030
|
}
|
|
963
1031
|
),
|
|
964
|
-
canEdit && /* @__PURE__ */
|
|
965
|
-
/* @__PURE__ */
|
|
966
|
-
/* @__PURE__ */
|
|
967
|
-
/* @__PURE__ */
|
|
1032
|
+
canEdit && /* @__PURE__ */ jsxs6("div", { className: "flex flex-wrap gap-1", children: [
|
|
1033
|
+
/* @__PURE__ */ jsx7("button", { type: "button", className: btn4, disabled: !selected, onClick: () => selected && cut([videoWordsRange(words, selected[0], selected[1])], "Cut words", "Select words to cut."), children: "Delete selected words" }),
|
|
1034
|
+
/* @__PURE__ */ jsx7("button", { type: "button", className: btn4, onClick: () => cut(findVideoFillers(words), "Remove fillers", "No filler words found."), children: "Remove fillers" }),
|
|
1035
|
+
/* @__PURE__ */ jsx7("button", { type: "button", className: btn4, onClick: () => cut(findVideoTranscriptPauses(words, { fps: project.fps }), "Tighten pauses", "No long pauses found."), children: "Tighten pauses" })
|
|
968
1036
|
] })
|
|
969
|
-
] }) : /* @__PURE__ */
|
|
970
|
-
canEdit && sound.length > 0 && /* @__PURE__ */
|
|
971
|
-
/* @__PURE__ */
|
|
972
|
-
/* @__PURE__ */
|
|
973
|
-
/* @__PURE__ */
|
|
1037
|
+
] }) : /* @__PURE__ */ jsx7("p", { className: "text-muted-foreground", "data-transcript-empty": "", children: "No transcribed media on the timeline. Load a transcript (Whisper JSON, .srt or .vtt) for a video or audio asset to edit by text." }),
|
|
1038
|
+
canEdit && sound.length > 0 && /* @__PURE__ */ jsxs6("details", { children: [
|
|
1039
|
+
/* @__PURE__ */ jsx7("summary", { className: "cursor-pointer text-muted-foreground", children: "Transcripts" }),
|
|
1040
|
+
/* @__PURE__ */ jsx7("ul", { className: "mt-1 space-y-1", children: sound.map((a) => /* @__PURE__ */ jsxs6("li", { className: "flex items-center gap-2", children: [
|
|
1041
|
+
/* @__PURE__ */ jsxs6("span", { className: "min-w-0 flex-1 truncate", children: [
|
|
974
1042
|
a.name ?? a.id,
|
|
975
1043
|
" ",
|
|
976
1044
|
a.transcript ? `\xB7 ${a.transcript.length} words` : ""
|
|
977
1045
|
] }),
|
|
978
|
-
/* @__PURE__ */
|
|
1046
|
+
/* @__PURE__ */ jsx7("input", { type: "file", accept: ".json,.srt,.vtt,application/json,text/vtt", "aria-label": `Load a transcript for ${a.name ?? a.id}`, className: "w-40 text-[10px]", onChange: (e) => void load(a.id, e.target.files?.[0]) })
|
|
979
1047
|
] }, a.id)) })
|
|
980
1048
|
] }),
|
|
981
|
-
message && /* @__PURE__ */
|
|
1049
|
+
message && /* @__PURE__ */ jsx7("p", { className: "text-red-500", role: "alert", "data-transcript-error": "", children: message })
|
|
982
1050
|
] });
|
|
983
1051
|
}
|
|
984
1052
|
|
|
985
1053
|
// src/video-editor/VideoEditor.tsx
|
|
986
|
-
import { useEffect as
|
|
987
|
-
import { jsx as
|
|
988
|
-
function VideoEditor({ controller: given, options, editable = true, pxPerSecond, onPxPerSecondChange, resolution, showIssues = true, showTranscript = true, className = "" }) {
|
|
1054
|
+
import { useEffect as useEffect5 } from "react";
|
|
1055
|
+
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1056
|
+
function VideoEditor({ controller: given, options, editable = true, pxPerSecond, onPxPerSecondChange, resolution, showIssues = true, showTranscript = true, showScopes = false, className = "" }) {
|
|
989
1057
|
const { controller, state } = useVideoEditor(given ?? options ?? {});
|
|
990
|
-
|
|
1058
|
+
useEffect5(() => {
|
|
991
1059
|
if (editable) return;
|
|
992
1060
|
const was = controller.getState().readOnly;
|
|
993
1061
|
controller.setReadOnly(true);
|
|
994
1062
|
return () => controller.setReadOnly(was);
|
|
995
1063
|
}, [controller, editable]);
|
|
996
1064
|
const { issues } = state;
|
|
997
|
-
const issueList = showIssues && issues.length > 0 && /* @__PURE__ */
|
|
998
|
-
return /* @__PURE__ */
|
|
999
|
-
/* @__PURE__ */
|
|
1000
|
-
editable && /* @__PURE__ */
|
|
1001
|
-
/* @__PURE__ */
|
|
1002
|
-
/* @__PURE__ */
|
|
1003
|
-
/* @__PURE__ */
|
|
1065
|
+
const issueList = showIssues && issues.length > 0 && /* @__PURE__ */ jsx8("ul", { className: "space-y-0.5 text-xs", "aria-label": "Video issues", "data-video-issues": "", children: issues.map((issue, i) => /* @__PURE__ */ jsx8("li", { className: issue.severity === "error" ? "text-red-500" : issue.severity === "warning" ? "text-amber-500" : "text-muted-foreground", "data-issue": issue.code, children: issue.message }, i)) });
|
|
1066
|
+
return /* @__PURE__ */ jsxs7("div", { className: `flex flex-col gap-3 ${className}`, "data-video-editor": "", children: [
|
|
1067
|
+
/* @__PURE__ */ jsxs7("div", { className: `grid gap-3 ${editable ? "lg:grid-cols-[16rem_minmax(0,1fr)_18rem]" : ""}`, children: [
|
|
1068
|
+
editable && /* @__PURE__ */ jsx8(MediaBin, { controller, className: "min-w-0" }),
|
|
1069
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex min-w-0 flex-col gap-2", children: [
|
|
1070
|
+
/* @__PURE__ */ jsx8(VideoPreview, { controller, resolution, className: "rounded-md border border-border" }),
|
|
1071
|
+
/* @__PURE__ */ jsx8(VideoTransport, { controller, editable }),
|
|
1072
|
+
showScopes && /* @__PURE__ */ jsx8(VideoScopes, { controller })
|
|
1004
1073
|
] }),
|
|
1005
|
-
editable && /* @__PURE__ */
|
|
1074
|
+
editable && /* @__PURE__ */ jsx8(ClipInspector, { controller, className: "min-w-0" })
|
|
1006
1075
|
] }),
|
|
1007
|
-
/* @__PURE__ */
|
|
1008
|
-
showTranscript && /* @__PURE__ */
|
|
1076
|
+
/* @__PURE__ */ jsx8(VideoTimeline, { controller, editable, pxPerSecond, onPxPerSecondChange }),
|
|
1077
|
+
showTranscript && /* @__PURE__ */ jsx8(TranscriptPanel, { controller, editable }),
|
|
1009
1078
|
issueList
|
|
1010
1079
|
] });
|
|
1011
1080
|
}
|
|
@@ -1015,6 +1084,7 @@ export {
|
|
|
1015
1084
|
TranscriptPanel,
|
|
1016
1085
|
VideoEditor,
|
|
1017
1086
|
VideoPreview,
|
|
1087
|
+
VideoScopes,
|
|
1018
1088
|
VideoTimeline,
|
|
1019
1089
|
VideoTransport,
|
|
1020
1090
|
useVideoEditor,
|