@pixerate/editor-react 0.60.0 → 0.62.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.
@@ -77,6 +77,7 @@ interface VideoTransportProps {
77
77
  /**
78
78
  * Play / pause, a frame back and forward, the playhead and total length as timecode, and the
79
79
  * edits that work on the playhead and the selection: split, delete, ripple delete, undo and redo.
80
+ * When any asset has proxies, a preview-size control picks what the preview plays (a view setting).
80
81
  */
81
82
  declare function VideoTransport({ controller, editable, className }: VideoTransportProps): React__default.JSX.Element;
82
83
 
@@ -92,17 +93,6 @@ interface VideoTimelineProps {
92
93
  waveforms?: VideoWaveformMode;
93
94
  className?: string;
94
95
  }
95
- /**
96
- * The tracks of a `VideoEditorController` laid out in time: a ruler with markers (click or drag it
97
- * to scrub), a playhead, and one row per track with its toggles (mute, hide, lock) and its clips.
98
- * Click a clip to select it (Shift / Cmd to add or remove it), drag it to move it (onto another
99
- * track of the same kind too; hold Alt to insert, pushing later material along on every sync-locked
100
- * track, or Shift to overwrite what is underneath), drag an edge to trim it (Alt to ripple). Moves snap to clip edges,
101
- * markers, the playhead and frames. Every edit is one undo step; refused edits show why.
102
- *
103
- * Keyboard (when focused): Space plays, ←/→ step a frame, S splits at the playhead, Delete removes
104
- * the selection (Shift to ripple), Cmd/Ctrl+Z undoes, Shift+Cmd/Ctrl+Z redoes.
105
- */
106
96
  declare function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable, waveforms, className }: VideoTimelineProps): React__default.JSX.Element;
107
97
 
108
98
  interface ClipInspectorProps {
@@ -145,7 +145,7 @@ function VideoScopes({ controller, scopes = VIDEO_SCOPE_KINDS, width = 256, heig
145
145
  }
146
146
 
147
147
  // src/video-editor/VideoTransport.tsx
148
- import { formatVideoTimecode } from "@pixerate/editor/video-editor";
148
+ import { formatVideoTimecode, parseVideoPreviewScale, VIDEO_PREVIEW_SCALE_OPTIONS, videoProjectHasProxies } from "@pixerate/editor/video-editor";
149
149
  import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
150
150
  var btn = "rounded border border-border px-2 py-0.5 text-xs disabled:opacity-40";
151
151
  function VideoTransport({ controller, editable = true, className = "" }) {
@@ -161,6 +161,18 @@ function VideoTransport({ controller, editable = true, className = "" }) {
161
161
  " / ",
162
162
  formatVideoTimecode(duration, project.fps)
163
163
  ] }),
164
+ videoProjectHasProxies(project) && /* @__PURE__ */ jsx3(
165
+ "select",
166
+ {
167
+ className: "rounded border border-border bg-transparent px-1 py-0.5 text-xs",
168
+ value: String(state.previewScale),
169
+ onChange: (e) => controller.setPreviewScale(parseVideoPreviewScale(e.target.value)),
170
+ "aria-label": "Preview size",
171
+ title: "Play proxies in the preview (the export always uses the originals)",
172
+ "data-preview-scale": "",
173
+ children: VIDEO_PREVIEW_SCALE_OPTIONS.map((o) => /* @__PURE__ */ jsx3("option", { value: o.value, children: o.label }, o.value))
174
+ }
175
+ ),
164
176
  editable && /* @__PURE__ */ jsxs2(Fragment, { children: [
165
177
  /* @__PURE__ */ jsx3("span", { className: "mx-1 h-4 w-px bg-border" }),
166
178
  /* @__PURE__ */ jsx3("button", { type: "button", className: btn, onClick: () => controller.splitAtPlayhead(), disabled: !canEdit, title: "Split at the playhead (S)", "data-split": "", children: "Split" }),
@@ -189,6 +201,7 @@ import {
189
201
  planVideoClipDrag,
190
202
  videoDragLabel,
191
203
  videoAssetMap,
204
+ videoSequenceMap,
192
205
  videoClipLabel,
193
206
  videoClipShowsWaveform,
194
207
  videoClipWaveformPath,
@@ -220,6 +233,27 @@ function ClipWaveform({ controller, clip, asset, width, height }) {
220
233
  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 }) });
221
234
  }
222
235
  var isField = (el) => el instanceof HTMLElement && /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName);
236
+ function useCachedRanges(controller, project, px) {
237
+ const [ranges, setRanges] = useState2([]);
238
+ useEffect4(() => {
239
+ const cache = controller.frameCache;
240
+ if (!cache) return setRanges([]);
241
+ let timer = null;
242
+ const read = () => {
243
+ timer = null;
244
+ setRanges(cache.cachedRanges(project, { step: 2e3 / px }));
245
+ };
246
+ read();
247
+ const off = cache.subscribe(() => {
248
+ timer ??= setTimeout(read, 250);
249
+ });
250
+ return () => {
251
+ off();
252
+ if (timer) clearTimeout(timer);
253
+ };
254
+ }, [controller, project, px]);
255
+ return ranges;
256
+ }
223
257
  function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable = true, waveforms = "audio", className = "" }) {
224
258
  const state = useVideoEditorState(controller);
225
259
  const { project, duration, time, selection, readOnly, error } = state;
@@ -231,10 +265,12 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
231
265
  const rows = useRef4(/* @__PURE__ */ new Map());
232
266
  const ruler = useRef4(null);
233
267
  const assets = videoAssetMap(project);
268
+ const sequences = videoSequenceMap(project);
234
269
  const msToPx = (ms) => ms * px / 1e3;
235
270
  const span = Math.max(duration + 5e3, 1e4);
236
271
  const width = msToPx(span);
237
272
  const ticks = videoRulerTicks(span, px, project.fps);
273
+ const cached = useCachedRanges(controller, project, px);
238
274
  const seekAt = (clientX) => {
239
275
  const box = ruler.current.getBoundingClientRect();
240
276
  controller.seek(Math.max(0, (clientX - box.left) * 1e3 / px));
@@ -396,6 +432,7 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
396
432
  },
397
433
  children: [
398
434
  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)),
435
+ cached.map((r) => /* @__PURE__ */ jsx4("div", { className: "pointer-events-none absolute bottom-0 h-[3px] bg-emerald-500/70", style: { left: msToPx(r.start), width: Math.max(1, msToPx(r.end - r.start)) }, title: "Frames cached: plays and scrubs without waiting", "data-cached-range": "" }, r.start)),
399
436
  (project.markers ?? []).map((m) => /* @__PURE__ */ jsx4(
400
437
  "div",
401
438
  {
@@ -438,7 +475,7 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
438
475
  children: clips.map(({ clip, home, start, duration: length, dragging }) => {
439
476
  const asset = clip.type === "media" ? assets.get(clip.asset) : void 0;
440
477
  const selected = selection.includes(clip.id);
441
- const label = videoClipLabel(clip, asset);
478
+ const label = videoClipLabel(clip, asset, clip.type === "sequence" ? sequences.get(clip.sequence) : void 0);
442
479
  return /* @__PURE__ */ jsxs3(
443
480
  "div",
444
481
  {
@@ -453,6 +490,7 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
453
490
  },
454
491
  title: `${label} \xB7 ${formatVideoSeconds(start)}\u2013${formatVideoSeconds(start + length)}`,
455
492
  "data-clip": clip.id,
493
+ "data-compound": clip.type === "sequence" ? clip.sequence : void 0,
456
494
  "data-selected": selected ? "" : void 0,
457
495
  "data-dragging": dragging ? "" : void 0,
458
496
  "aria-selected": selected,
@@ -629,7 +667,7 @@ function ClipInspector({ controller, className = "" }) {
629
667
  const transition = clip.transition;
630
668
  return /* @__PURE__ */ jsxs4("div", { className: `flex flex-col gap-1.5 ${className}`, "data-clip-inspector": "", "data-clip-id": clip.id, children: [
631
669
  /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2 text-xs", children: [
632
- /* @__PURE__ */ jsx5("span", { className: "font-medium", children: clip.type === "media" ? "Media clip" : clip.type === "text" ? "Title" : "Colour clip" }),
670
+ /* @__PURE__ */ jsx5("span", { className: "font-medium", children: clip.type === "media" ? "Media clip" : clip.type === "text" ? "Title" : clip.type === "sequence" ? "Compound clip" : "Colour clip" }),
633
671
  /* @__PURE__ */ jsx5("code", { className: "text-muted-foreground", children: clip.id }),
634
672
  at.track.locked && /* @__PURE__ */ jsx5("span", { className: "text-amber-500", children: "locked track" })
635
673
  ] }),
@@ -777,6 +815,14 @@ function ClipInspector({ controller, className = "" }) {
777
815
  /* @__PURE__ */ jsx5(Row, { label: "Font size", name: "fontSize", children: number("fontSize", "Font size", { min: 1, step: 2 }) }),
778
816
  /* @__PURE__ */ jsx5(Row, { label: "Background", name: "background", children: text("background", "Background") })
779
817
  ] }),
818
+ clip.type === "sequence" && /* @__PURE__ */ jsxs4(Fragment3, { children: [
819
+ /* @__PURE__ */ jsx5(Row, { label: "Sequence", name: "sequence", children: /* @__PURE__ */ jsxs4("span", { className: "truncate", title: "A compound clip plays its sequence as one picture and sound; its clips cannot be edited here yet", children: [
820
+ project.sequences?.find((q) => q.id === clip.sequence)?.name ?? clip.sequence,
821
+ " \xB7 from ",
822
+ formatVideoSeconds2(clip.in ?? 0)
823
+ ] }) }),
824
+ /* @__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") }) })
825
+ ] }),
780
826
  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") }) }),
781
827
  /* @__PURE__ */ jsx5(Row, { label: "Transition in", name: "transition", children: /* @__PURE__ */ jsxs4(
782
828
  "select",
@@ -930,6 +976,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMe
930
976
  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: [
931
977
  /* @__PURE__ */ jsx6("span", { className: "rounded bg-muted px-1 text-[10px] uppercase text-muted-foreground", "data-asset-kind": "", children: asset.kind }),
932
978
  /* @__PURE__ */ jsx6("span", { className: "min-w-0 flex-1 truncate", title: asset.src, children: asset.name ?? asset.id }),
979
+ asset.proxies?.length ? /* @__PURE__ */ jsx6("span", { className: "rounded bg-muted px-1 text-[10px] text-muted-foreground", title: `Proxies: ${asset.proxies.map((p) => `${p.scale * 100}%`).join(", ")} (the preview can play these; exports use the original)`, "data-asset-proxies": "", children: "proxy" }) : null,
933
980
  asset.duration !== void 0 && /* @__PURE__ */ jsx6("span", { className: "tabular-nums text-muted-foreground", children: formatVideoSeconds3(asset.duration) }),
934
981
  editable && /* @__PURE__ */ jsx6("button", { type: "button", className: btn3, disabled: !canEdit, onClick: () => addToTimeline(asset), "data-add-to-timeline": "", children: "Add to timeline" })
935
982
  ] }, asset.id)) }) : /* @__PURE__ */ jsxs5("p", { className: "text-xs text-muted-foreground", children: [