@pixerate/editor-react 0.55.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
|
|
|
@@ -434,12 +502,16 @@ import {
|
|
|
434
502
|
VIDEO_TRANSITION_TYPES,
|
|
435
503
|
VIDEO_WIPE_DIRECTIONS,
|
|
436
504
|
videoTransitionHasDirection,
|
|
505
|
+
videoFreezeFrameEdit,
|
|
437
506
|
videoKeyframeRows,
|
|
507
|
+
videoSlowMotionRamp,
|
|
508
|
+
videoSpeedRampEdit,
|
|
438
509
|
videoTransitionAudioCurve,
|
|
439
510
|
videoKeyframeToggleEdit,
|
|
440
511
|
videoKeyframeValueEdit
|
|
441
512
|
} from "@pixerate/editor/video-editor";
|
|
442
|
-
import { Fragment as Fragment3, jsx as
|
|
513
|
+
import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
514
|
+
var btn2 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
|
|
443
515
|
var field = "w-full rounded border border-border bg-transparent px-1 py-0.5 text-xs disabled:opacity-50";
|
|
444
516
|
var DEFAULT_TRANSITION = 500;
|
|
445
517
|
function Commit({
|
|
@@ -452,7 +524,7 @@ function Commit({
|
|
|
452
524
|
if (raw !== value) onCommit(raw);
|
|
453
525
|
};
|
|
454
526
|
if (multiline)
|
|
455
|
-
return /* @__PURE__ */
|
|
527
|
+
return /* @__PURE__ */ jsx5(
|
|
456
528
|
"textarea",
|
|
457
529
|
{
|
|
458
530
|
className: field,
|
|
@@ -464,11 +536,11 @@ function Commit({
|
|
|
464
536
|
},
|
|
465
537
|
value
|
|
466
538
|
);
|
|
467
|
-
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);
|
|
468
540
|
}
|
|
469
541
|
function Row({ label, name, children }) {
|
|
470
|
-
return /* @__PURE__ */
|
|
471
|
-
/* @__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 }),
|
|
472
544
|
children
|
|
473
545
|
] });
|
|
474
546
|
}
|
|
@@ -476,7 +548,7 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
476
548
|
const state = useVideoEditorState(controller);
|
|
477
549
|
const { project, selection, readOnly, time } = state;
|
|
478
550
|
if (selection.length !== 1)
|
|
479
|
-
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." });
|
|
480
552
|
const at = findVideoClip(project, selection[0]);
|
|
481
553
|
if (!at) return null;
|
|
482
554
|
const clip = at.clip;
|
|
@@ -493,24 +565,24 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
493
565
|
};
|
|
494
566
|
const text = (key, label) => {
|
|
495
567
|
const value = clip[key];
|
|
496
|
-
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}`) });
|
|
497
569
|
};
|
|
498
570
|
const number = (key, label, attrs = {}) => {
|
|
499
571
|
const value = clip[key];
|
|
500
|
-
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 });
|
|
501
573
|
};
|
|
502
574
|
const transition = clip.transition;
|
|
503
|
-
return /* @__PURE__ */
|
|
504
|
-
/* @__PURE__ */
|
|
505
|
-
/* @__PURE__ */
|
|
506
|
-
/* @__PURE__ */
|
|
507
|
-
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" })
|
|
508
580
|
] }),
|
|
509
|
-
/* @__PURE__ */
|
|
510
|
-
/* @__PURE__ */
|
|
511
|
-
/* @__PURE__ */
|
|
512
|
-
videoKeyframeRows(project, clip.id, time).map((row) => /* @__PURE__ */
|
|
513
|
-
/* @__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(
|
|
514
586
|
Commit,
|
|
515
587
|
{
|
|
516
588
|
type: "number",
|
|
@@ -529,7 +601,7 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
529
601
|
}
|
|
530
602
|
}
|
|
531
603
|
),
|
|
532
|
-
/* @__PURE__ */
|
|
604
|
+
/* @__PURE__ */ jsx5(
|
|
533
605
|
"button",
|
|
534
606
|
{
|
|
535
607
|
type: "button",
|
|
@@ -546,11 +618,11 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
546
618
|
children: row.onKey ? "\u25C6" : "\u25C7"
|
|
547
619
|
}
|
|
548
620
|
),
|
|
549
|
-
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 })
|
|
550
622
|
] }) }, row.property)),
|
|
551
|
-
at.track.kind === "video" && /* @__PURE__ */
|
|
552
|
-
at.track.kind === "video" && /* @__PURE__ */
|
|
553
|
-
/* @__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(
|
|
554
626
|
"select",
|
|
555
627
|
{
|
|
556
628
|
className: field,
|
|
@@ -562,12 +634,12 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
562
634
|
if (grade) controller.apply({ op: "update_clip", clip: clip.id, set: { grade } }, `Apply ${e.target.value} look`);
|
|
563
635
|
},
|
|
564
636
|
children: [
|
|
565
|
-
/* @__PURE__ */
|
|
566
|
-
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))
|
|
567
639
|
]
|
|
568
640
|
}
|
|
569
641
|
) }),
|
|
570
|
-
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(
|
|
571
643
|
Commit,
|
|
572
644
|
{
|
|
573
645
|
type: "number",
|
|
@@ -587,7 +659,7 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
587
659
|
}
|
|
588
660
|
) }, c.key))
|
|
589
661
|
] }),
|
|
590
|
-
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(
|
|
591
663
|
"select",
|
|
592
664
|
{
|
|
593
665
|
className: field,
|
|
@@ -599,18 +671,35 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
599
671
|
set(key, effect ? { ...clip[key], effect } : void 0, key === "animateIn" ? "Change entrance" : "Change exit");
|
|
600
672
|
},
|
|
601
673
|
children: [
|
|
602
|
-
/* @__PURE__ */
|
|
603
|
-
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))
|
|
604
676
|
]
|
|
605
677
|
}
|
|
606
678
|
) }, key)),
|
|
607
|
-
/* @__PURE__ */
|
|
608
|
-
/* @__PURE__ */
|
|
609
|
-
clip.type === "media" && /* @__PURE__ */
|
|
610
|
-
/* @__PURE__ */
|
|
611
|
-
|
|
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" })
|
|
685
|
+
] }) : number("speed", "Speed", { min: 0.1, step: 0.25 }) }),
|
|
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(
|
|
688
|
+
"button",
|
|
689
|
+
{
|
|
690
|
+
type: "button",
|
|
691
|
+
className: btn2,
|
|
692
|
+
disabled: disabled || time < clip.start || time > clip.start + clip.duration,
|
|
693
|
+
title: "Hold the frame at the playhead for a second, pushing later clips along",
|
|
694
|
+
onClick: () => controller.apply(videoFreezeFrameEdit(clip, time - clip.start, 1e3), "Freeze frame"),
|
|
695
|
+
children: "Freeze 1 s here"
|
|
696
|
+
}
|
|
697
|
+
),
|
|
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" })
|
|
699
|
+
] }) }),
|
|
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") }) }),
|
|
612
701
|
["fadeInCurve", "fadeOutCurve"].map(
|
|
613
|
-
(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(
|
|
614
703
|
"select",
|
|
615
704
|
{
|
|
616
705
|
className: field,
|
|
@@ -621,19 +710,19 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
621
710
|
const curve = e.target.value;
|
|
622
711
|
set(key, curve === "linear" ? void 0 : curve, key === "fadeInCurve" ? "Change fade in curve" : "Change fade out curve");
|
|
623
712
|
},
|
|
624
|
-
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))
|
|
625
714
|
}
|
|
626
715
|
) }, key) : null
|
|
627
716
|
)
|
|
628
717
|
] }),
|
|
629
|
-
clip.type === "text" && /* @__PURE__ */
|
|
630
|
-
/* @__PURE__ */
|
|
631
|
-
/* @__PURE__ */
|
|
632
|
-
/* @__PURE__ */
|
|
633
|
-
/* @__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") })
|
|
634
723
|
] }),
|
|
635
|
-
clip.type === "color" && /* @__PURE__ */
|
|
636
|
-
/* @__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(
|
|
637
726
|
"select",
|
|
638
727
|
{
|
|
639
728
|
className: field,
|
|
@@ -648,12 +737,12 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
648
737
|
);
|
|
649
738
|
},
|
|
650
739
|
children: [
|
|
651
|
-
/* @__PURE__ */
|
|
652
|
-
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))
|
|
653
742
|
]
|
|
654
743
|
}
|
|
655
744
|
) }),
|
|
656
|
-
transition && /* @__PURE__ */
|
|
745
|
+
transition && /* @__PURE__ */ jsx5(Row, { label: "Length (ms)", name: "transitionDuration", children: /* @__PURE__ */ jsx5(
|
|
657
746
|
Commit,
|
|
658
747
|
{
|
|
659
748
|
type: "number",
|
|
@@ -668,7 +757,7 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
668
757
|
}
|
|
669
758
|
}
|
|
670
759
|
) }),
|
|
671
|
-
transition && videoTransitionHasDirection(transition.type) && /* @__PURE__ */
|
|
760
|
+
transition && videoTransitionHasDirection(transition.type) && /* @__PURE__ */ jsx5(Row, { label: "Direction", name: "transitionDirection", children: /* @__PURE__ */ jsx5(
|
|
672
761
|
"select",
|
|
673
762
|
{
|
|
674
763
|
className: field,
|
|
@@ -676,10 +765,10 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
676
765
|
disabled,
|
|
677
766
|
"aria-label": "Transition direction",
|
|
678
767
|
onChange: (e) => controller.apply({ op: "set_transition", clip: clip.id, transition: { ...transition, direction: e.target.value } }, "Change transition direction"),
|
|
679
|
-
children: VIDEO_WIPE_DIRECTIONS.map((d) => /* @__PURE__ */
|
|
768
|
+
children: VIDEO_WIPE_DIRECTIONS.map((d) => /* @__PURE__ */ jsx5("option", { value: d, children: d }, d))
|
|
680
769
|
}
|
|
681
770
|
) }),
|
|
682
|
-
transition && /* @__PURE__ */
|
|
771
|
+
transition && /* @__PURE__ */ jsx5(Row, { label: "Transition sound", name: "transitionAudioCurve", children: /* @__PURE__ */ jsx5(
|
|
683
772
|
"select",
|
|
684
773
|
{
|
|
685
774
|
className: field,
|
|
@@ -692,30 +781,30 @@ function ClipInspector({ controller, className = "" }) {
|
|
|
692
781
|
const next = curve === videoTransitionAudioCurve(rest) ? rest : { ...rest, audioCurve: curve };
|
|
693
782
|
controller.apply({ op: "set_transition", clip: clip.id, transition: next }, "Change transition sound");
|
|
694
783
|
},
|
|
695
|
-
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))
|
|
696
785
|
}
|
|
697
786
|
) })
|
|
698
787
|
] });
|
|
699
788
|
}
|
|
700
789
|
|
|
701
790
|
// src/video-editor/MediaBin.tsx
|
|
702
|
-
import { useState as
|
|
791
|
+
import { useState as useState3 } from "react";
|
|
703
792
|
import {
|
|
704
793
|
formatVideoSeconds as formatVideoSeconds3,
|
|
705
794
|
probeVideoMedia,
|
|
706
795
|
videoAssetDropEdit,
|
|
707
796
|
videoAssetKindOf
|
|
708
797
|
} from "@pixerate/editor/video-editor";
|
|
709
|
-
import { Fragment as Fragment4, jsx as
|
|
710
|
-
var
|
|
798
|
+
import { Fragment as Fragment4, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
799
|
+
var btn3 = "rounded border border-border px-2 py-0.5 text-xs disabled:opacity-40";
|
|
711
800
|
function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMedia, className = "" }) {
|
|
712
801
|
const { project, readOnly } = useVideoEditorState(controller);
|
|
713
802
|
const canEdit = editable && !readOnly;
|
|
714
|
-
const [url, setUrl] =
|
|
715
|
-
const [urlKind, setUrlKind] =
|
|
716
|
-
const [busy, setBusy] =
|
|
717
|
-
const [problem, setProblem] =
|
|
718
|
-
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);
|
|
719
808
|
const addSource = async (src, kind, name) => {
|
|
720
809
|
setBusy((n) => n + 1);
|
|
721
810
|
try {
|
|
@@ -763,7 +852,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMe
|
|
|
763
852
|
if (added) controller.select([added]);
|
|
764
853
|
}
|
|
765
854
|
};
|
|
766
|
-
return /* @__PURE__ */
|
|
855
|
+
return /* @__PURE__ */ jsxs5(
|
|
767
856
|
"div",
|
|
768
857
|
{
|
|
769
858
|
className: `flex flex-col gap-2 rounded ${over ? "ring-2 ring-rose-500/60" : ""} ${className}`,
|
|
@@ -781,20 +870,20 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMe
|
|
|
781
870
|
void addFiles(e.dataTransfer.files);
|
|
782
871
|
},
|
|
783
872
|
children: [
|
|
784
|
-
/* @__PURE__ */
|
|
785
|
-
project.assets.length ? /* @__PURE__ */
|
|
786
|
-
/* @__PURE__ */
|
|
787
|
-
/* @__PURE__ */
|
|
788
|
-
asset.duration !== void 0 && /* @__PURE__ */
|
|
789
|
-
editable && /* @__PURE__ */
|
|
790
|
-
] }, 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: [
|
|
791
880
|
"No media yet.",
|
|
792
881
|
editable ? " Choose or drop files, or add a URL." : ""
|
|
793
882
|
] }),
|
|
794
|
-
editable && /* @__PURE__ */
|
|
795
|
-
/* @__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: [
|
|
796
885
|
"Choose or drop video, audio or image files",
|
|
797
|
-
/* @__PURE__ */
|
|
886
|
+
/* @__PURE__ */ jsx6(
|
|
798
887
|
"input",
|
|
799
888
|
{
|
|
800
889
|
type: "file",
|
|
@@ -811,7 +900,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMe
|
|
|
811
900
|
}
|
|
812
901
|
)
|
|
813
902
|
] }),
|
|
814
|
-
/* @__PURE__ */
|
|
903
|
+
/* @__PURE__ */ jsxs5(
|
|
815
904
|
"form",
|
|
816
905
|
{
|
|
817
906
|
className: "flex items-center gap-1",
|
|
@@ -820,7 +909,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMe
|
|
|
820
909
|
void addUrl();
|
|
821
910
|
},
|
|
822
911
|
children: [
|
|
823
|
-
/* @__PURE__ */
|
|
912
|
+
/* @__PURE__ */ jsx6(
|
|
824
913
|
"input",
|
|
825
914
|
{
|
|
826
915
|
className: "min-w-0 flex-1 rounded border border-border bg-transparent px-1 py-0.5 text-xs",
|
|
@@ -831,26 +920,26 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMe
|
|
|
831
920
|
onChange: (e) => setUrl(e.target.value)
|
|
832
921
|
}
|
|
833
922
|
),
|
|
834
|
-
/* @__PURE__ */
|
|
835
|
-
/* @__PURE__ */
|
|
836
|
-
/* @__PURE__ */
|
|
837
|
-
/* @__PURE__ */
|
|
838
|
-
/* @__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" })
|
|
839
928
|
] }),
|
|
840
|
-
/* @__PURE__ */
|
|
929
|
+
/* @__PURE__ */ jsx6("button", { type: "submit", className: btn3, disabled: !canEdit || !url.trim(), "data-add-url": "", children: "Add" })
|
|
841
930
|
]
|
|
842
931
|
}
|
|
843
932
|
)
|
|
844
933
|
] }),
|
|
845
|
-
busy > 0 && /* @__PURE__ */
|
|
846
|
-
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 })
|
|
847
936
|
]
|
|
848
937
|
}
|
|
849
938
|
);
|
|
850
939
|
}
|
|
851
940
|
|
|
852
941
|
// src/video-editor/TranscriptPanel.tsx
|
|
853
|
-
import
|
|
942
|
+
import React5, { useMemo as useMemo3, useState as useState4 } from "react";
|
|
854
943
|
import {
|
|
855
944
|
findVideoFillers,
|
|
856
945
|
findVideoTranscriptPauses,
|
|
@@ -860,15 +949,15 @@ import {
|
|
|
860
949
|
videoWordAt,
|
|
861
950
|
videoWordsRange
|
|
862
951
|
} from "@pixerate/editor/video-editor";
|
|
863
|
-
import { Fragment as Fragment5, jsx as
|
|
864
|
-
var
|
|
952
|
+
import { Fragment as Fragment5, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
953
|
+
var btn4 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
|
|
865
954
|
function TranscriptPanel({ controller, track, editable = true, className = "" }) {
|
|
866
955
|
const state = useVideoEditorState(controller);
|
|
867
956
|
const { project, time, readOnly } = state;
|
|
868
957
|
const canEdit = editable && !readOnly;
|
|
869
|
-
const [anchor, setAnchor] =
|
|
870
|
-
const [focus, setFocus] =
|
|
871
|
-
const [message, setMessage] =
|
|
958
|
+
const [anchor, setAnchor] = useState4(null);
|
|
959
|
+
const [focus, setFocus] = useState4(null);
|
|
960
|
+
const [message, setMessage] = useState4(null);
|
|
872
961
|
const words = useMemo3(() => {
|
|
873
962
|
try {
|
|
874
963
|
return videoTimelineTranscript(project, { track });
|
|
@@ -895,9 +984,9 @@ function TranscriptPanel({ controller, track, editable = true, className = "" })
|
|
|
895
984
|
setMessage(e instanceof Error ? e.message : String(e));
|
|
896
985
|
}
|
|
897
986
|
};
|
|
898
|
-
return /* @__PURE__ */
|
|
899
|
-
words.length > 0 ? /* @__PURE__ */
|
|
900
|
-
/* @__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(
|
|
901
990
|
"div",
|
|
902
991
|
{
|
|
903
992
|
className: "max-h-48 overflow-y-auto leading-6",
|
|
@@ -913,8 +1002,8 @@ function TranscriptPanel({ controller, track, editable = true, className = "" })
|
|
|
913
1002
|
},
|
|
914
1003
|
children: words.map((w, i) => {
|
|
915
1004
|
const inSelection = selected !== null && i >= selected[0] && i <= selected[1];
|
|
916
|
-
return /* @__PURE__ */
|
|
917
|
-
/* @__PURE__ */
|
|
1005
|
+
return /* @__PURE__ */ jsxs6(React5.Fragment, { children: [
|
|
1006
|
+
/* @__PURE__ */ jsx7(
|
|
918
1007
|
"span",
|
|
919
1008
|
{
|
|
920
1009
|
role: "option",
|
|
@@ -940,51 +1029,52 @@ function TranscriptPanel({ controller, track, editable = true, className = "" })
|
|
|
940
1029
|
})
|
|
941
1030
|
}
|
|
942
1031
|
),
|
|
943
|
-
canEdit && /* @__PURE__ */
|
|
944
|
-
/* @__PURE__ */
|
|
945
|
-
/* @__PURE__ */
|
|
946
|
-
/* @__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" })
|
|
947
1036
|
] })
|
|
948
|
-
] }) : /* @__PURE__ */
|
|
949
|
-
canEdit && sound.length > 0 && /* @__PURE__ */
|
|
950
|
-
/* @__PURE__ */
|
|
951
|
-
/* @__PURE__ */
|
|
952
|
-
/* @__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: [
|
|
953
1042
|
a.name ?? a.id,
|
|
954
1043
|
" ",
|
|
955
1044
|
a.transcript ? `\xB7 ${a.transcript.length} words` : ""
|
|
956
1045
|
] }),
|
|
957
|
-
/* @__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]) })
|
|
958
1047
|
] }, a.id)) })
|
|
959
1048
|
] }),
|
|
960
|
-
message && /* @__PURE__ */
|
|
1049
|
+
message && /* @__PURE__ */ jsx7("p", { className: "text-red-500", role: "alert", "data-transcript-error": "", children: message })
|
|
961
1050
|
] });
|
|
962
1051
|
}
|
|
963
1052
|
|
|
964
1053
|
// src/video-editor/VideoEditor.tsx
|
|
965
|
-
import { useEffect as
|
|
966
|
-
import { jsx as
|
|
967
|
-
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 = "" }) {
|
|
968
1057
|
const { controller, state } = useVideoEditor(given ?? options ?? {});
|
|
969
|
-
|
|
1058
|
+
useEffect5(() => {
|
|
970
1059
|
if (editable) return;
|
|
971
1060
|
const was = controller.getState().readOnly;
|
|
972
1061
|
controller.setReadOnly(true);
|
|
973
1062
|
return () => controller.setReadOnly(was);
|
|
974
1063
|
}, [controller, editable]);
|
|
975
1064
|
const { issues } = state;
|
|
976
|
-
const issueList = showIssues && issues.length > 0 && /* @__PURE__ */
|
|
977
|
-
return /* @__PURE__ */
|
|
978
|
-
/* @__PURE__ */
|
|
979
|
-
editable && /* @__PURE__ */
|
|
980
|
-
/* @__PURE__ */
|
|
981
|
-
/* @__PURE__ */
|
|
982
|
-
/* @__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 })
|
|
983
1073
|
] }),
|
|
984
|
-
editable && /* @__PURE__ */
|
|
1074
|
+
editable && /* @__PURE__ */ jsx8(ClipInspector, { controller, className: "min-w-0" })
|
|
985
1075
|
] }),
|
|
986
|
-
/* @__PURE__ */
|
|
987
|
-
showTranscript && /* @__PURE__ */
|
|
1076
|
+
/* @__PURE__ */ jsx8(VideoTimeline, { controller, editable, pxPerSecond, onPxPerSecondChange }),
|
|
1077
|
+
showTranscript && /* @__PURE__ */ jsx8(TranscriptPanel, { controller, editable }),
|
|
988
1078
|
issueList
|
|
989
1079
|
] });
|
|
990
1080
|
}
|
|
@@ -994,6 +1084,7 @@ export {
|
|
|
994
1084
|
TranscriptPanel,
|
|
995
1085
|
VideoEditor,
|
|
996
1086
|
VideoPreview,
|
|
1087
|
+
VideoScopes,
|
|
997
1088
|
VideoTimeline,
|
|
998
1089
|
VideoTransport,
|
|
999
1090
|
useVideoEditor,
|