@pixerate/editor-react 0.60.0 → 0.61.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" }),
@@ -220,6 +232,27 @@ function ClipWaveform({ controller, clip, asset, width, height }) {
220
232
  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
233
  }
222
234
  var isField = (el) => el instanceof HTMLElement && /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName);
235
+ function useCachedRanges(controller, project, px) {
236
+ const [ranges, setRanges] = useState2([]);
237
+ useEffect4(() => {
238
+ const cache = controller.frameCache;
239
+ if (!cache) return setRanges([]);
240
+ let timer = null;
241
+ const read = () => {
242
+ timer = null;
243
+ setRanges(cache.cachedRanges(project, { step: 2e3 / px }));
244
+ };
245
+ read();
246
+ const off = cache.subscribe(() => {
247
+ timer ??= setTimeout(read, 250);
248
+ });
249
+ return () => {
250
+ off();
251
+ if (timer) clearTimeout(timer);
252
+ };
253
+ }, [controller, project, px]);
254
+ return ranges;
255
+ }
223
256
  function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable = true, waveforms = "audio", className = "" }) {
224
257
  const state = useVideoEditorState(controller);
225
258
  const { project, duration, time, selection, readOnly, error } = state;
@@ -235,6 +268,7 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
235
268
  const span = Math.max(duration + 5e3, 1e4);
236
269
  const width = msToPx(span);
237
270
  const ticks = videoRulerTicks(span, px, project.fps);
271
+ const cached = useCachedRanges(controller, project, px);
238
272
  const seekAt = (clientX) => {
239
273
  const box = ruler.current.getBoundingClientRect();
240
274
  controller.seek(Math.max(0, (clientX - box.left) * 1e3 / px));
@@ -396,6 +430,7 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
396
430
  },
397
431
  children: [
398
432
  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)),
433
+ 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
434
  (project.markers ?? []).map((m) => /* @__PURE__ */ jsx4(
400
435
  "div",
401
436
  {
@@ -930,6 +965,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMe
930
965
  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
966
  /* @__PURE__ */ jsx6("span", { className: "rounded bg-muted px-1 text-[10px] uppercase text-muted-foreground", "data-asset-kind": "", children: asset.kind }),
932
967
  /* @__PURE__ */ jsx6("span", { className: "min-w-0 flex-1 truncate", title: asset.src, children: asset.name ?? asset.id }),
968
+ 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
969
  asset.duration !== void 0 && /* @__PURE__ */ jsx6("span", { className: "tabular-nums text-muted-foreground", children: formatVideoSeconds3(asset.duration) }),
934
970
  editable && /* @__PURE__ */ jsx6("button", { type: "button", className: btn3, disabled: !canEdit, onClick: () => addToTimeline(asset), "data-add-to-timeline": "", children: "Add to timeline" })
935
971
  ] }, asset.id)) }) : /* @__PURE__ */ jsxs5("p", { className: "text-xs text-muted-foreground", children: [