@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.
@@ -205,6 +205,18 @@ function VideoTransport({ controller, editable = true, className = "" }) {
205
205
  " / ",
206
206
  (0, import_video_editor4.formatVideoTimecode)(duration, project.fps)
207
207
  ] }),
208
+ (0, import_video_editor4.videoProjectHasProxies)(project) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
209
+ "select",
210
+ {
211
+ className: "rounded border border-border bg-transparent px-1 py-0.5 text-xs",
212
+ value: String(state.previewScale),
213
+ onChange: (e) => controller.setPreviewScale((0, import_video_editor4.parseVideoPreviewScale)(e.target.value)),
214
+ "aria-label": "Preview size",
215
+ title: "Play proxies in the preview (the export always uses the originals)",
216
+ "data-preview-scale": "",
217
+ children: import_video_editor4.VIDEO_PREVIEW_SCALE_OPTIONS.map((o) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: o.value, children: o.label }, o.value))
218
+ }
219
+ ),
208
220
  editable && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
209
221
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-1 h-4 w-px bg-border" }),
210
222
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: btn, onClick: () => controller.splitAtPlayhead(), disabled: !canEdit, title: "Split at the playhead (S)", "data-split": "", children: "Split" }),
@@ -253,6 +265,27 @@ function ClipWaveform({ controller, clip, asset, width, height }) {
253
265
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("svg", { className: "pointer-events-none absolute inset-0", width, height, "aria-hidden": "true", "data-waveform": clip.id, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d, fill: "currentColor", fillOpacity: 0.45 }) });
254
266
  }
255
267
  var isField = (el) => el instanceof HTMLElement && /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName);
268
+ function useCachedRanges(controller, project, px) {
269
+ const [ranges, setRanges] = (0, import_react4.useState)([]);
270
+ (0, import_react4.useEffect)(() => {
271
+ const cache = controller.frameCache;
272
+ if (!cache) return setRanges([]);
273
+ let timer = null;
274
+ const read = () => {
275
+ timer = null;
276
+ setRanges(cache.cachedRanges(project, { step: 2e3 / px }));
277
+ };
278
+ read();
279
+ const off = cache.subscribe(() => {
280
+ timer ??= setTimeout(read, 250);
281
+ });
282
+ return () => {
283
+ off();
284
+ if (timer) clearTimeout(timer);
285
+ };
286
+ }, [controller, project, px]);
287
+ return ranges;
288
+ }
256
289
  function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable = true, waveforms = "audio", className = "" }) {
257
290
  const state = useVideoEditorState(controller);
258
291
  const { project, duration, time, selection, readOnly, error } = state;
@@ -264,10 +297,12 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
264
297
  const rows = (0, import_react4.useRef)(/* @__PURE__ */ new Map());
265
298
  const ruler = (0, import_react4.useRef)(null);
266
299
  const assets = (0, import_video_editor5.videoAssetMap)(project);
300
+ const sequences = (0, import_video_editor5.videoSequenceMap)(project);
267
301
  const msToPx = (ms) => ms * px / 1e3;
268
302
  const span = Math.max(duration + 5e3, 1e4);
269
303
  const width = msToPx(span);
270
304
  const ticks = (0, import_video_editor5.videoRulerTicks)(span, px, project.fps);
305
+ const cached = useCachedRanges(controller, project, px);
271
306
  const seekAt = (clientX) => {
272
307
  const box = ruler.current.getBoundingClientRect();
273
308
  controller.seek(Math.max(0, (clientX - box.left) * 1e3 / px));
@@ -429,6 +464,7 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
429
464
  },
430
465
  children: [
431
466
  ticks.map((t) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("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__ */ (0, import_jsx_runtime4.jsx)("span", { className: "absolute -top-3.5 left-1 whitespace-nowrap tabular-nums", children: t.label }) }, t.time)),
467
+ cached.map((r) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("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)),
432
468
  (project.markers ?? []).map((m) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
433
469
  "div",
434
470
  {
@@ -471,7 +507,7 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
471
507
  children: clips.map(({ clip, home, start, duration: length, dragging }) => {
472
508
  const asset = clip.type === "media" ? assets.get(clip.asset) : void 0;
473
509
  const selected = selection.includes(clip.id);
474
- const label = (0, import_video_editor5.videoClipLabel)(clip, asset);
510
+ const label = (0, import_video_editor5.videoClipLabel)(clip, asset, clip.type === "sequence" ? sequences.get(clip.sequence) : void 0);
475
511
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
476
512
  "div",
477
513
  {
@@ -486,6 +522,7 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
486
522
  },
487
523
  title: `${label} \xB7 ${(0, import_video_editor5.formatVideoSeconds)(start)}\u2013${(0, import_video_editor5.formatVideoSeconds)(start + length)}`,
488
524
  "data-clip": clip.id,
525
+ "data-compound": clip.type === "sequence" ? clip.sequence : void 0,
489
526
  "data-selected": selected ? "" : void 0,
490
527
  "data-dragging": dragging ? "" : void 0,
491
528
  "aria-selected": selected,
@@ -637,7 +674,7 @@ function ClipInspector({ controller, className = "" }) {
637
674
  const transition = clip.transition;
638
675
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: `flex flex-col gap-1.5 ${className}`, "data-clip-inspector": "", "data-clip-id": clip.id, children: [
639
676
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center gap-2 text-xs", children: [
640
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "font-medium", children: clip.type === "media" ? "Media clip" : clip.type === "text" ? "Title" : "Colour clip" }),
677
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "font-medium", children: clip.type === "media" ? "Media clip" : clip.type === "text" ? "Title" : clip.type === "sequence" ? "Compound clip" : "Colour clip" }),
641
678
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("code", { className: "text-muted-foreground", children: clip.id }),
642
679
  at.track.locked && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-amber-500", children: "locked track" })
643
680
  ] }),
@@ -785,6 +822,14 @@ function ClipInspector({ controller, className = "" }) {
785
822
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Font size", name: "fontSize", children: number("fontSize", "Font size", { min: 1, step: 2 }) }),
786
823
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Background", name: "background", children: text("background", "Background") })
787
824
  ] }),
825
+ clip.type === "sequence" && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
826
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Sequence", name: "sequence", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "truncate", title: "A compound clip plays its sequence as one picture and sound; its clips cannot be edited here yet", children: [
827
+ project.sequences?.find((q) => q.id === clip.sequence)?.name ?? clip.sequence,
828
+ " \xB7 from ",
829
+ (0, import_video_editor6.formatVideoSeconds)(clip.in ?? 0)
830
+ ] }) }),
831
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Muted", name: "muted", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("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") }) })
832
+ ] }),
788
833
  clip.type === "color" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Colour", name: "color", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Commit, { value: clip.color, "aria-label": "Colour", disabled, onCommit: (raw) => raw.trim() && set("color", raw.trim(), "Change colour") }) }),
789
834
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Transition in", name: "transition", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
790
835
  "select",
@@ -933,6 +978,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
933
978
  project.assets.length ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("ul", { className: "flex flex-col gap-1", "aria-label": "Media assets", children: project.assets.map((asset) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("li", { className: "flex items-center gap-2 text-xs", "data-asset": asset.id, children: [
934
979
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "rounded bg-muted px-1 text-[10px] uppercase text-muted-foreground", "data-asset-kind": "", children: asset.kind }),
935
980
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "min-w-0 flex-1 truncate", title: asset.src, children: asset.name ?? asset.id }),
981
+ asset.proxies?.length ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("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,
936
982
  asset.duration !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "tabular-nums text-muted-foreground", children: (0, import_video_editor7.formatVideoSeconds)(asset.duration) }),
937
983
  editable && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: btn3, disabled: !canEdit, onClick: () => addToTimeline(asset), "data-add-to-timeline": "", children: "Add to timeline" })
938
984
  ] }, asset.id)) }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "text-xs text-muted-foreground", children: [