@norskvideo/norsk-studio-built-ins 1.27.0-2026-01-10-23683704 → 1.27.0-2026-01-14-d7f304fc

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.
Files changed (31) hide show
  1. package/client/info.js +158 -90
  2. package/lib/input.srt-listener/_gen/types.d.ts +1 -0
  3. package/lib/input.srt-listener/_gen/yaml-docs.js +3 -0
  4. package/lib/input.srt-listener/_gen/yaml-docs.js.map +1 -1
  5. package/lib/input.srt-listener/_gen/zod.js +1 -0
  6. package/lib/input.srt-listener/_gen/zod.js.map +1 -1
  7. package/lib/input.srt-listener/info.d.ts +1 -0
  8. package/lib/input.srt-listener/info.js +9 -0
  9. package/lib/input.srt-listener/info.js.map +1 -1
  10. package/lib/input.srt-listener/runtime.d.ts +1 -0
  11. package/lib/input.srt-listener/runtime.js +7 -0
  12. package/lib/input.srt-listener/runtime.js.map +1 -1
  13. package/lib/input.srt-listener/types.yaml +2 -0
  14. package/lib/processor.browserOverlay/runtime.js +1 -1
  15. package/lib/processor.browserOverlay/runtime.js.map +1 -1
  16. package/lib/processor.onscreenGraphic/runtime.js +1 -1
  17. package/lib/processor.onscreenGraphic/runtime.js.map +1 -1
  18. package/lib/processor.smartSourceSwitch/inline-view.d.ts +3 -5
  19. package/lib/processor.smartSourceSwitch/inline-view.js +16 -4
  20. package/lib/processor.smartSourceSwitch/inline-view.js.map +1 -1
  21. package/lib/processor.videoCompose/fullscreen.js +28 -10
  22. package/lib/processor.videoCompose/fullscreen.js.map +1 -1
  23. package/lib/processor.videoCompose/preset-transition-panel.js +37 -8
  24. package/lib/processor.videoCompose/preset-transition-panel.js.map +1 -1
  25. package/lib/processor.videoCompose/runtime.js +42 -42
  26. package/lib/processor.videoCompose/runtime.js.map +1 -1
  27. package/lib/processor.videoCompose/visual-preview.js +3 -1
  28. package/lib/processor.videoCompose/visual-preview.js.map +1 -1
  29. package/lib/processor.zoomTo/runtime.js +1 -1
  30. package/lib/processor.zoomTo/runtime.js.map +1 -1
  31. package/package.json +3 -3
package/client/info.js CHANGED
@@ -25992,7 +25992,7 @@ var require_dist6 = __commonJS({
25992
25992
  unstable_createCollection: () => createCollection2
25993
25993
  });
25994
25994
  module.exports = __toCommonJS2(index_exports);
25995
- var import_react33 = __toESM2(require_react());
25995
+ var import_react34 = __toESM2(require_react());
25996
25996
  var import_react_context = require_dist3();
25997
25997
  var import_react_compose_refs = require_dist4();
25998
25998
  var import_react_slot = require_dist5();
@@ -26006,14 +26006,14 @@ var require_dist6 = __commonJS({
26006
26006
  );
26007
26007
  const CollectionProvider = (props) => {
26008
26008
  const { scope, children } = props;
26009
- const ref = import_react33.default.useRef(null);
26010
- const itemMap = import_react33.default.useRef(/* @__PURE__ */ new Map()).current;
26009
+ const ref = import_react34.default.useRef(null);
26010
+ const itemMap = import_react34.default.useRef(/* @__PURE__ */ new Map()).current;
26011
26011
  return /* @__PURE__ */ (0, import_jsx_runtime75.jsx)(CollectionProviderImpl, { scope, itemMap, collectionRef: ref, children });
26012
26012
  };
26013
26013
  CollectionProvider.displayName = PROVIDER_NAME;
26014
26014
  const COLLECTION_SLOT_NAME = name + "CollectionSlot";
26015
26015
  const CollectionSlotImpl = (0, import_react_slot.createSlot)(COLLECTION_SLOT_NAME);
26016
- const CollectionSlot = import_react33.default.forwardRef(
26016
+ const CollectionSlot = import_react34.default.forwardRef(
26017
26017
  (props, forwardedRef) => {
26018
26018
  const { scope, children } = props;
26019
26019
  const context = useCollectionContext(COLLECTION_SLOT_NAME, scope);
@@ -26025,13 +26025,13 @@ var require_dist6 = __commonJS({
26025
26025
  const ITEM_SLOT_NAME = name + "CollectionItemSlot";
26026
26026
  const ITEM_DATA_ATTR = "data-radix-collection-item";
26027
26027
  const CollectionItemSlotImpl = (0, import_react_slot.createSlot)(ITEM_SLOT_NAME);
26028
- const CollectionItemSlot = import_react33.default.forwardRef(
26028
+ const CollectionItemSlot = import_react34.default.forwardRef(
26029
26029
  (props, forwardedRef) => {
26030
26030
  const { scope, children, ...itemData } = props;
26031
- const ref = import_react33.default.useRef(null);
26031
+ const ref = import_react34.default.useRef(null);
26032
26032
  const composedRefs = (0, import_react_compose_refs.useComposedRefs)(forwardedRef, ref);
26033
26033
  const context = useCollectionContext(ITEM_SLOT_NAME, scope);
26034
- import_react33.default.useEffect(() => {
26034
+ import_react34.default.useEffect(() => {
26035
26035
  context.itemMap.set(ref, { ref, ...itemData });
26036
26036
  return () => void context.itemMap.delete(ref);
26037
26037
  });
@@ -26041,7 +26041,7 @@ var require_dist6 = __commonJS({
26041
26041
  CollectionItemSlot.displayName = ITEM_SLOT_NAME;
26042
26042
  function useCollection(scope) {
26043
26043
  const context = useCollectionContext(name + "CollectionConsumer", scope);
26044
- const getItems = import_react33.default.useCallback(() => {
26044
+ const getItems = import_react34.default.useCallback(() => {
26045
26045
  const collectionNode = context.collectionRef.current;
26046
26046
  if (!collectionNode) return [];
26047
26047
  const orderedNodes = Array.from(collectionNode.querySelectorAll(`[${ITEM_DATA_ATTR}]`));
@@ -62393,6 +62393,15 @@ function info_default6({ defineComponent, mappingsToStreams: mappingsToStreams2,
62393
62393
  global: unique("sourceName")
62394
62394
  }
62395
62395
  },
62396
+ burstProtection: {
62397
+ advanced: true,
62398
+ help: "Drop data until input is completely stable",
62399
+ hint: {
62400
+ defaultValue: false,
62401
+ type: "boolean",
62402
+ optional: true
62403
+ }
62404
+ },
62396
62405
  decodeOutputs: (0, import_client_types3.DecodeOutputsForm)(),
62397
62406
  streamMappings: StreamMappingForm(defaultStreamMapping3, {
62398
62407
  sourceNames: (cfg) => cfg.streamIds ?? []
@@ -73377,13 +73386,17 @@ var import_jsx_runtime55 = __toESM(require_jsx_runtime());
73377
73386
  var import_FaTimes = __toESM(require_FaTimes());
73378
73387
  var import_FaPlay2 = __toESM(require_FaPlay());
73379
73388
  var import_FaCircle = __toESM(require_FaCircle());
73380
- function InlineView18({ state, config }) {
73389
+ var import_react25 = __toESM(require_react());
73390
+ function InlineView18({ state, config, sendCommand }) {
73381
73391
  const priorityOrder = state.sourcePriority || config.sources;
73382
- return (0, import_jsx_runtime55.jsxs)(import_jsx_runtime55.Fragment, { children: [(0, import_jsx_runtime55.jsx)("h5", { className: "text-gray-900 dark:text-white font-medium", children: "Sources:" }), (0, import_jsx_runtime55.jsxs)("ul", { className: "space-y-2 mt-2", children: [config.sources.map((s, i) => {
73392
+ const handlePriorityChange = (0, import_react25.useCallback)((newOrder) => {
73393
+ sendCommand({ type: "set-priority-order", sources: newOrder });
73394
+ }, [sendCommand]);
73395
+ return (0, import_jsx_runtime55.jsxs)(import_jsx_runtime55.Fragment, { children: [(0, import_jsx_runtime55.jsx)("h5", { className: "text-gray-900 dark:text-white font-medium", children: "Sources:" }), (0, import_jsx_runtime55.jsxs)("ul", { className: "space-y-2 mt-2", children: [priorityOrder.map((s, index3) => {
73383
73396
  const isActive = state.activeSource === s;
73384
73397
  const isAvailable = state.availableSources.includes(s);
73385
73398
  const isOffline = !isActive && !isAvailable;
73386
- const priorityRank = priorityOrder.indexOf(s) + 1;
73399
+ const priorityRank = index3 + 1;
73387
73400
  return (0, import_jsx_runtime55.jsxs)("li", { className: "flex items-center gap-2 w-full", children: [(0, import_jsx_runtime55.jsx)("div", { className: "relative w-4 h-4 flex items-center justify-center flex-shrink-0", children: isOffline ? (
73388
73401
  // Red X for offline sources
73389
73402
  (0, import_jsx_runtime55.jsx)(import_FaTimes.FaTimes, { className: "w-4 h-4 text-red-500" })
@@ -73393,7 +73406,15 @@ function InlineView18({ state, config }) {
73393
73406
  ) : (
73394
73407
  // Green circle for available but not active
73395
73408
  (0, import_jsx_runtime55.jsx)(import_FaCircle.FaCircle, { className: "w-2 h-2 text-green-500" })
73396
- ) }), (0, import_jsx_runtime55.jsx)("span", { className: "text-gray-900 dark:text-white flex-1", children: s }), (0, import_jsx_runtime55.jsx)("span", { className: "text-gray-500 dark:text-gray-400 text-sm", children: priorityRank })] }, i);
73409
+ ) }), (0, import_jsx_runtime55.jsx)("span", { className: "text-gray-900 dark:text-white flex-1", children: s }), (0, import_jsx_runtime55.jsxs)("div", { className: "flex items-center gap-1", children: [index3 > 0 && (0, import_jsx_runtime55.jsx)("button", { onClick: () => {
73410
+ const newOrder = [...priorityOrder];
73411
+ [newOrder[index3], newOrder[index3 - 1]] = [newOrder[index3 - 1], newOrder[index3]];
73412
+ handlePriorityChange(newOrder);
73413
+ }, className: "w-4 h-4 flex items-center justify-center text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200", title: "Move up", children: (0, import_jsx_runtime55.jsx)("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", strokeWidth: 2, stroke: "currentColor", className: "w-3 h-3", children: (0, import_jsx_runtime55.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M4.5 15.75l7.5-7.5 7.5 7.5" }) }) }), index3 < priorityOrder.length - 1 && (0, import_jsx_runtime55.jsx)("button", { onClick: () => {
73414
+ const newOrder = [...priorityOrder];
73415
+ [newOrder[index3], newOrder[index3 + 1]] = [newOrder[index3 + 1], newOrder[index3]];
73416
+ handlePriorityChange(newOrder);
73417
+ }, className: "w-4 h-4 flex items-center justify-center text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200", title: "Move down", children: (0, import_jsx_runtime55.jsx)("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", strokeWidth: 2, stroke: "currentColor", className: "w-3 h-3", children: (0, import_jsx_runtime55.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19.5 8.25l-7.5 7.5-7.5-7.5" }) }) }), (0, import_jsx_runtime55.jsx)("span", { className: "text-gray-500 dark:text-gray-400 text-sm w-4 text-center", children: priorityRank })] })] }, s);
73397
73418
  }), (0, import_jsx_runtime55.jsxs)("li", { className: "flex items-center gap-2", children: [(0, import_jsx_runtime55.jsx)("div", { className: "relative w-4 h-4 flex items-center justify-center", children: state.activeSource === "fallback" ? (
73398
73419
  // Green play button for active fallback
73399
73420
  (0, import_jsx_runtime55.jsxs)(import_jsx_runtime55.Fragment, { children: [(0, import_jsx_runtime55.jsx)(import_FaPlay2.FaPlay, { className: "w-3 h-3 text-green-500" }), (0, import_jsx_runtime55.jsx)("div", { className: "absolute inset-0 flex items-center justify-center opacity-30", children: (0, import_jsx_runtime55.jsx)("div", { className: "w-4 h-4 bg-green-500 animate-ping duration-150" }) })] })
@@ -73406,12 +73427,12 @@ var inline_view_default18 = InlineView18;
73406
73427
 
73407
73428
  // build/client/processor.smartSourceSwitch/fullscreen.js
73408
73429
  var import_jsx_runtime56 = __toESM(require_jsx_runtime());
73409
- var import_react25 = __toESM(require_react());
73430
+ var import_react26 = __toESM(require_react());
73410
73431
  function FullscreenView2({ state, config, sendCommand }) {
73411
- const handleMakeActive = (0, import_react25.useCallback)((source) => {
73432
+ const handleMakeActive = (0, import_react26.useCallback)((source) => {
73412
73433
  sendCommand({ type: "make-source-active", source });
73413
73434
  }, [sendCommand]);
73414
- const handlePriorityChange = (0, import_react25.useCallback)((newOrder) => {
73435
+ const handlePriorityChange = (0, import_react26.useCallback)((newOrder) => {
73415
73436
  sendCommand({ type: "set-priority-order", sources: newOrder });
73416
73437
  }, [sendCommand]);
73417
73438
  const displaySources = config.sources;
@@ -73776,9 +73797,9 @@ var import_config11 = __toESM(require_config2());
73776
73797
 
73777
73798
  // build/client/processor.syncExternalAudio/inline-view.js
73778
73799
  var import_jsx_runtime58 = __toESM(require_jsx_runtime());
73779
- var import_react26 = __toESM(require_react());
73800
+ var import_react27 = __toESM(require_react());
73780
73801
  function InlineView19({ state, config, raise }) {
73781
- (0, import_react26.useEffect)(() => {
73802
+ (0, import_react27.useEffect)(() => {
73782
73803
  if (raise) {
73783
73804
  raise();
73784
73805
  }
@@ -73790,19 +73811,19 @@ var inline_view_default19 = InlineView19;
73790
73811
 
73791
73812
  // build/client/processor.syncExternalAudio/fullscreen-view.js
73792
73813
  var import_jsx_runtime59 = __toESM(require_jsx_runtime());
73793
- var import_react27 = __toESM(require_react());
73814
+ var import_react28 = __toESM(require_react());
73794
73815
  function SyncExternalAudioFullscreenView({ state, config, sendCommand }) {
73795
- const videoContainerRef = (0, import_react27.useRef)(null);
73796
- const whepClientRef = (0, import_react27.useRef)(null);
73816
+ const videoContainerRef = (0, import_react28.useRef)(null);
73817
+ const whepClientRef = (0, import_react28.useRef)(null);
73797
73818
  const commentaries = Object.entries(state.commentaries || {});
73798
- const [selectedCommentary, setSelectedCommentary] = (0, import_react27.useState)(commentaries[0]?.[0] || "");
73799
- (0, import_react27.useEffect)(() => {
73819
+ const [selectedCommentary, setSelectedCommentary] = (0, import_react28.useState)(commentaries[0]?.[0] || "");
73820
+ (0, import_react28.useEffect)(() => {
73800
73821
  if (commentaries.length > 0 && !state.commentaries[selectedCommentary]) {
73801
73822
  setSelectedCommentary(commentaries[0][0]);
73802
73823
  }
73803
73824
  }, [commentaries, selectedCommentary, state.commentaries]);
73804
73825
  const commentary = state.commentaries[selectedCommentary];
73805
- (0, import_react27.useEffect)(() => {
73826
+ (0, import_react28.useEffect)(() => {
73806
73827
  if (commentary?.whepUrl && videoContainerRef.current) {
73807
73828
  if (whepClientRef.current) {
73808
73829
  whepClientRef.current = null;
@@ -74038,11 +74059,11 @@ var summary_view_default11 = SummaryView11;
74038
74059
 
74039
74060
  // build/client/processor.videoCompose/fullscreen.js
74040
74061
  var import_jsx_runtime66 = __toESM(require_jsx_runtime());
74041
- var import_react30 = __toESM(require_react());
74062
+ var import_react31 = __toESM(require_react());
74042
74063
 
74043
74064
  // build/client/processor.videoCompose/preset-transition-panel.js
74044
74065
  var import_jsx_runtime63 = __toESM(require_jsx_runtime());
74045
- var import_react28 = __toESM(require_react());
74066
+ var import_react29 = __toESM(require_react());
74046
74067
 
74047
74068
  // build/client/processor.videoCompose/presets-metadata.js
74048
74069
  var PRESET_METADATA = {
@@ -76651,16 +76672,16 @@ function PresetVisualPreview({ layers, size: size4 = "small", referenceResolutio
76651
76672
 
76652
76673
  // build/client/processor.videoCompose/preset-transition-panel.js
76653
76674
  function PresetTransitionPanel({ state, sendCommand, selectedPreset, onSelectPreset, sourceSelection, onSourceSelectionChange, presetConfig, onPresetConfigChange }) {
76654
- const [transition, setTransition] = (0, import_react28.useState)({
76675
+ const [transition, setTransition] = (0, import_react29.useState)({
76655
76676
  durationMs: 1e3,
76656
76677
  easing: "ease_in_out"
76657
76678
  });
76658
- const lastPresetRef = (0, import_react28.useRef)(null);
76659
- const loadingSceneRef = (0, import_react28.useRef)(false);
76679
+ const lastPresetRef = (0, import_react29.useRef)(null);
76680
+ const loadingSceneRef = (0, import_react29.useRef)(false);
76660
76681
  const metadata = selectedPreset ? PRESET_METADATA[selectedPreset] : null;
76661
76682
  const reference = state.sources.find((s) => s.sourceName === state.reference);
76662
- const onlineSourceNames = (0, import_react28.useMemo)(() => state.sources.filter((s) => s.status === "online").map((s) => s.sourceName).join(","), [state.sources]);
76663
- (0, import_react28.useEffect)(() => {
76683
+ const onlineSourceNames = (0, import_react29.useMemo)(() => state.sources.filter((s) => s.status === "online").map((s) => s.sourceName).join(","), [state.sources]);
76684
+ (0, import_react29.useEffect)(() => {
76664
76685
  if (!selectedPreset || !metadata)
76665
76686
  return;
76666
76687
  if (lastPresetRef.current === selectedPreset)
@@ -76693,13 +76714,13 @@ function PresetTransitionPanel({ state, sendCommand, selectedPreset, onSelectPre
76693
76714
  onPresetConfigChange(defaults2);
76694
76715
  }
76695
76716
  }, [metadata, onPresetConfigChange, onSourceSelectionChange, onlineSourceNames, reference?.resolution, selectedPreset, state.sources]);
76696
- (0, import_react28.useEffect)(() => {
76717
+ (0, import_react29.useEffect)(() => {
76697
76718
  if (!selectedPreset || sourceSelection.length === 0)
76698
76719
  return;
76699
76720
  const storageKey = `videoCompose.preset.${selectedPreset}.sources`;
76700
76721
  localStorage.setItem(storageKey, JSON.stringify(sourceSelection));
76701
76722
  }, [selectedPreset, sourceSelection]);
76702
- const handlePreview = (0, import_react28.useCallback)(() => {
76723
+ const handlePreview = (0, import_react29.useCallback)(() => {
76703
76724
  if (!selectedPreset || !metadata)
76704
76725
  return;
76705
76726
  const layout = {
@@ -76713,11 +76734,20 @@ function PresetTransitionPanel({ state, sendCommand, selectedPreset, onSelectPre
76713
76734
  layout
76714
76735
  });
76715
76736
  }, [selectedPreset, metadata, sourceSelection, presetConfig, sendCommand]);
76716
- const handleCancelPreview = (0, import_react28.useCallback)(() => {
76737
+ const handleCancelPreview = (0, import_react29.useCallback)(() => {
76717
76738
  sendCommand({ type: "disable-preview" });
76718
76739
  }, [sendCommand]);
76719
- const handleApplyPreview = (0, import_react28.useCallback)(() => {
76720
- const hasOrchestration = selectedPreset && metadata && PRESETS[selectedPreset]?.(sourceSelection, presetConfig, { width: 1920, height: 1080 })?.orchestrations;
76740
+ const hasOrchestration = (0, import_react29.useMemo)(() => {
76741
+ if (!selectedPreset)
76742
+ return false;
76743
+ try {
76744
+ const presetDef = PRESETS[selectedPreset]?.(sourceSelection, presetConfig, { width: 1920, height: 1080 });
76745
+ return !!presetDef?.orchestrations;
76746
+ } catch {
76747
+ return false;
76748
+ }
76749
+ }, [selectedPreset, sourceSelection, presetConfig]);
76750
+ const handleApplyPreview = (0, import_react29.useCallback)(() => {
76721
76751
  sendCommand({
76722
76752
  type: "apply-preview",
76723
76753
  // Only pass transition if there's no orchestration
@@ -76728,8 +76758,8 @@ function PresetTransitionPanel({ state, sendCommand, selectedPreset, onSelectPre
76728
76758
  }
76729
76759
  }
76730
76760
  });
76731
- }, [sendCommand, transition.durationMs, transition.easing, selectedPreset, metadata, sourceSelection, presetConfig]);
76732
- const handleApplyDirect = (0, import_react28.useCallback)(() => {
76761
+ }, [sendCommand, transition.durationMs, transition.easing, hasOrchestration]);
76762
+ const handleApplyDirect = (0, import_react29.useCallback)(() => {
76733
76763
  if (!selectedPreset || !metadata)
76734
76764
  return;
76735
76765
  const layout = {
@@ -76738,7 +76768,6 @@ function PresetTransitionPanel({ state, sendCommand, selectedPreset, onSelectPre
76738
76768
  sourceSelection: sourceSelection.length > 0 ? sourceSelection : void 0,
76739
76769
  ...metadata.hasConfig ? { config: presetConfig } : {}
76740
76770
  };
76741
- const hasOrchestration = PRESETS[selectedPreset]?.(sourceSelection, presetConfig, { width: 1920, height: 1080 })?.orchestrations;
76742
76771
  sendCommand({
76743
76772
  type: "update-layout",
76744
76773
  layout,
@@ -76750,8 +76779,8 @@ function PresetTransitionPanel({ state, sendCommand, selectedPreset, onSelectPre
76750
76779
  }
76751
76780
  }
76752
76781
  });
76753
- }, [selectedPreset, metadata, sourceSelection, presetConfig, sendCommand, transition]);
76754
- const referenceResolution = (0, import_react28.useMemo)(() => {
76782
+ }, [selectedPreset, metadata, sourceSelection, presetConfig, sendCommand, transition, hasOrchestration]);
76783
+ const referenceResolution = (0, import_react29.useMemo)(() => {
76755
76784
  const reference2 = state.sources.find((s) => s.sourceName === state.reference);
76756
76785
  return reference2?.resolution;
76757
76786
  }, [state.sources, state.reference]);
@@ -76776,7 +76805,7 @@ function SceneCard({ scene, isActive, onSelect, onDelete }) {
76776
76805
  }, className: "absolute top-1 right-1 p-1 rounded bg-red-600 text-white hover:bg-red-700\n opacity-0 group-hover:opacity-100 transition-opacity", title: "Delete scene", children: (0, import_jsx_runtime63.jsx)("svg", { className: "w-3 h-3", fill: "currentColor", viewBox: "0 0 20 20", children: (0, import_jsx_runtime63.jsx)("path", { fillRule: "evenodd", d: "M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z", clipRule: "evenodd" }) }) })] });
76777
76806
  }
76778
76807
  function PresetCard({ presetKey, metadata, onClick, referenceResolution }) {
76779
- const previewLayout = (0, import_react28.useMemo)(() => {
76808
+ const previewLayout = (0, import_react29.useMemo)(() => {
76780
76809
  if (!referenceResolution)
76781
76810
  return null;
76782
76811
  const generator = PRESETS[presetKey];
@@ -76794,7 +76823,7 @@ function PresetCard({ presetKey, metadata, onClick, referenceResolution }) {
76794
76823
  return (0, import_jsx_runtime63.jsxs)("button", { className: "p-1.5 rounded border border-gray-300 dark:border-gray-600\n hover:border-blue-500 dark:hover:border-blue-400\n hover:bg-blue-50 dark:hover:bg-blue-900/20\n bg-white dark:bg-gray-800\n text-left transition-colors flex flex-col gap-1.5", onClick, children: [(0, import_jsx_runtime63.jsx)("div", { className: "w-full flex justify-center", children: previewLayout && referenceResolution ? (0, import_jsx_runtime63.jsx)(PresetVisualPreview, { layers: previewLayout.layers, size: "small", referenceResolution }) : (0, import_jsx_runtime63.jsx)("div", { className: "w-16 h-9 bg-gray-700 dark:bg-gray-900 rounded flex items-center justify-center", children: (0, import_jsx_runtime63.jsx)("span", { className: "text-xs text-gray-400", children: "?" }) }) }), (0, import_jsx_runtime63.jsxs)("div", { className: "w-full", children: [(0, import_jsx_runtime63.jsx)("h3", { className: "font-medium text-xs text-gray-900 dark:text-white truncate text-center", children: metadata.name }), (0, import_jsx_runtime63.jsxs)("div", { className: "flex items-center justify-center gap-1 mt-0.5", children: [(0, import_jsx_runtime63.jsx)("span", { className: "text-xs text-gray-500 dark:text-gray-400", children: metadata.minSources === metadata.maxSources ? `${metadata.minSources} src${metadata.minSources > 1 ? "s" : ""}` : `${metadata.minSources}-${metadata.maxSources} srcs` }), metadata.hasConfig && (0, import_jsx_runtime63.jsx)("span", { className: "text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300\n px-1 py-0.5 rounded", children: "Config" })] })] })] });
76795
76824
  }
76796
76825
  function SaveSceneForm({ onSave, onCancel }) {
76797
- const [name, setName] = (0, import_react28.useState)("");
76826
+ const [name, setName] = (0, import_react29.useState)("");
76798
76827
  return (0, import_jsx_runtime63.jsxs)("div", { className: "space-y-2 p-3 bg-gray-50 dark:bg-gray-900 rounded", children: [(0, import_jsx_runtime63.jsx)("label", { className: "block text-sm font-medium text-gray-700 dark:text-gray-300", children: "Scene Name" }), (0, import_jsx_runtime63.jsx)("input", { type: "text", value: name, onChange: (e) => setName(e.target.value), placeholder: "Enter scene name...", className: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600\n rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-white", autoFocus: true }), (0, import_jsx_runtime63.jsxs)("div", { className: "flex gap-2", children: [(0, import_jsx_runtime63.jsx)("button", { onClick: () => {
76799
76828
  if (name.trim()) {
76800
76829
  onSave(name.trim());
@@ -76807,9 +76836,29 @@ function SaveSceneForm({ onSave, onCancel }) {
76807
76836
  }
76808
76837
  function PresetConfiguration(props) {
76809
76838
  const { metadata, onPreview, onCancelPreview, onApplyPreview, onApplyDirect, onCancel, isTransitioning, isPreviewActive, sourceSelection, state: _state, sendCommand } = props;
76810
- const [activeTab, setActiveTab] = (0, import_react28.useState)("sources");
76811
- const [showSaveForm, setShowSaveForm] = (0, import_react28.useState)(false);
76839
+ const [activeTab, setActiveTab] = (0, import_react29.useState)("sources");
76840
+ const [showSaveForm, setShowSaveForm] = (0, import_react29.useState)(false);
76841
+ const [isPending, setIsPending] = (0, import_react29.useState)(false);
76842
+ (0, import_react29.useEffect)(() => {
76843
+ if (isTransitioning) {
76844
+ setIsPending(false);
76845
+ }
76846
+ }, [isTransitioning]);
76847
+ (0, import_react29.useEffect)(() => {
76848
+ if (isPending) {
76849
+ const timeout = setTimeout(() => setIsPending(false), 1e4);
76850
+ return () => clearTimeout(timeout);
76851
+ }
76852
+ }, [isPending]);
76812
76853
  const canPreview = sourceSelection.length >= metadata.minSources;
76854
+ const handleApplyPreviewWithPending = (0, import_react29.useCallback)(() => {
76855
+ setIsPending(true);
76856
+ onApplyPreview();
76857
+ }, [onApplyPreview]);
76858
+ const handleApplyDirectWithPending = (0, import_react29.useCallback)(() => {
76859
+ setIsPending(true);
76860
+ onApplyDirect();
76861
+ }, [onApplyDirect]);
76813
76862
  const handleSaveScene = (name) => {
76814
76863
  const layout = {
76815
76864
  type: "preset",
@@ -76820,7 +76869,7 @@ function PresetConfiguration(props) {
76820
76869
  sendCommand({ type: "create-scene", name, layout });
76821
76870
  setShowSaveForm(false);
76822
76871
  };
76823
- return (0, import_jsx_runtime63.jsxs)("div", { className: "h-full flex flex-col", children: [(0, import_jsx_runtime63.jsxs)("div", { className: "p-6 border-b border-gray-200 dark:border-gray-700", children: [(0, import_jsx_runtime63.jsx)("button", { onClick: onCancel, className: "text-sm text-blue-600 dark:text-blue-400 hover:text-blue-700\n dark:hover:text-blue-300 mb-2", children: "\u2190 Back to Presets" }), (0, import_jsx_runtime63.jsx)("h2", { className: "text-2xl font-bold text-gray-900 dark:text-white", children: metadata.name }), (0, import_jsx_runtime63.jsx)("p", { className: "text-sm text-gray-600 dark:text-gray-400 mt-1", children: metadata.description })] }), (0, import_jsx_runtime63.jsx)("div", { className: "flex-1 overflow-y-auto", children: metadata.hasConfig ? (0, import_jsx_runtime63.jsxs)("div", { children: [(0, import_jsx_runtime63.jsx)("div", { className: "border-b border-gray-200 dark:border-gray-700 px-6", children: (0, import_jsx_runtime63.jsxs)("div", { className: "flex gap-6", children: [(0, import_jsx_runtime63.jsx)("button", { onClick: () => setActiveTab("sources"), className: `py-3 border-b-2 font-medium text-sm transition-colors ${activeTab === "sources" ? "border-blue-600 text-blue-600 dark:text-blue-400" : "border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"}`, children: "Source Ordering" }), (0, import_jsx_runtime63.jsx)("button", { onClick: () => setActiveTab("config"), className: `py-3 border-b-2 font-medium text-sm transition-colors ${activeTab === "config" ? "border-blue-600 text-blue-600 dark:text-blue-400" : "border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"}`, children: "Configuration" })] }) }), (0, import_jsx_runtime63.jsx)("div", { className: "p-6", children: activeTab === "sources" ? (0, import_jsx_runtime63.jsx)(SourceOrderingPanel, { ...props }) : (0, import_jsx_runtime63.jsx)(ConfigPanel, { presetKey: props.presetKey, config: props.config, onConfigChange: props.onConfigChange }) })] }) : (0, import_jsx_runtime63.jsx)("div", { className: "p-6", children: (0, import_jsx_runtime63.jsx)(SourceOrderingPanel, { ...props }) }) }), (0, import_jsx_runtime63.jsxs)("div", { className: "border-t border-gray-200 dark:border-gray-700 p-6 space-y-4", children: [showSaveForm ? (0, import_jsx_runtime63.jsx)(SaveSceneForm, { onSave: handleSaveScene, onCancel: () => setShowSaveForm(false) }) : (0, import_jsx_runtime63.jsx)("button", { onClick: () => setShowSaveForm(true), disabled: !canPreview, className: "w-full px-4 py-2 text-sm rounded border-2 border-dashed\n border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400\n hover:border-green-500 hover:text-green-600 dark:hover:border-green-400\n dark:hover:text-green-400 transition-colors\n disabled:opacity-50 disabled:cursor-not-allowed", children: "Save as Scene" }), (0, import_jsx_runtime63.jsx)(PreviewControls, { transition: props.transition, onTransitionChange: props.onTransitionChange, onPreview, onCancelPreview, onApplyPreview, onApplyDirect, isTransitioning, isPreviewActive, canPreview, presetKey: props.presetKey, sourceSelection, config: props.config })] })] });
76872
+ return (0, import_jsx_runtime63.jsxs)("div", { className: "h-full flex flex-col", children: [(0, import_jsx_runtime63.jsxs)("div", { className: "p-6 border-b border-gray-200 dark:border-gray-700", children: [(0, import_jsx_runtime63.jsx)("button", { onClick: onCancel, className: "text-sm text-blue-600 dark:text-blue-400 hover:text-blue-700\n dark:hover:text-blue-300 mb-2", children: "\u2190 Back to Presets" }), (0, import_jsx_runtime63.jsx)("h2", { className: "text-2xl font-bold text-gray-900 dark:text-white", children: metadata.name }), (0, import_jsx_runtime63.jsx)("p", { className: "text-sm text-gray-600 dark:text-gray-400 mt-1", children: metadata.description })] }), (0, import_jsx_runtime63.jsx)("div", { className: "flex-1 overflow-y-auto", children: metadata.hasConfig ? (0, import_jsx_runtime63.jsxs)("div", { children: [(0, import_jsx_runtime63.jsx)("div", { className: "border-b border-gray-200 dark:border-gray-700 px-6", children: (0, import_jsx_runtime63.jsxs)("div", { className: "flex gap-6", children: [(0, import_jsx_runtime63.jsx)("button", { onClick: () => setActiveTab("sources"), className: `py-3 border-b-2 font-medium text-sm transition-colors ${activeTab === "sources" ? "border-blue-600 text-blue-600 dark:text-blue-400" : "border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"}`, children: "Source Ordering" }), (0, import_jsx_runtime63.jsx)("button", { onClick: () => setActiveTab("config"), className: `py-3 border-b-2 font-medium text-sm transition-colors ${activeTab === "config" ? "border-blue-600 text-blue-600 dark:text-blue-400" : "border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"}`, children: "Configuration" })] }) }), (0, import_jsx_runtime63.jsx)("div", { className: "p-6", children: activeTab === "sources" ? (0, import_jsx_runtime63.jsx)(SourceOrderingPanel, { ...props }) : (0, import_jsx_runtime63.jsx)(ConfigPanel, { presetKey: props.presetKey, config: props.config, onConfigChange: props.onConfigChange }) })] }) : (0, import_jsx_runtime63.jsx)("div", { className: "p-6", children: (0, import_jsx_runtime63.jsx)(SourceOrderingPanel, { ...props }) }) }), (0, import_jsx_runtime63.jsxs)("div", { className: "border-t border-gray-200 dark:border-gray-700 p-6 space-y-4", children: [showSaveForm ? (0, import_jsx_runtime63.jsx)(SaveSceneForm, { onSave: handleSaveScene, onCancel: () => setShowSaveForm(false) }) : (0, import_jsx_runtime63.jsx)("button", { onClick: () => setShowSaveForm(true), disabled: !canPreview, className: "w-full px-4 py-2 text-sm rounded border-2 border-dashed\n border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400\n hover:border-green-500 hover:text-green-600 dark:hover:border-green-400\n dark:hover:text-green-400 transition-colors\n disabled:opacity-50 disabled:cursor-not-allowed", children: "Save as Scene" }), (0, import_jsx_runtime63.jsx)(PreviewControls, { transition: props.transition, onTransitionChange: props.onTransitionChange, onPreview, onCancelPreview, onApplyPreview: handleApplyPreviewWithPending, onApplyDirect: handleApplyDirectWithPending, isTransitioning, isPreviewActive, canPreview, presetKey: props.presetKey, sourceSelection, config: props.config, isPending })] })] });
76824
76873
  }
76825
76874
  function SourceOrderingPanel({ metadata, sourceSelection, onSourceSelectionChange, availableSources }) {
76826
76875
  const usedSourceNames = new Set(sourceSelection);
@@ -76878,8 +76927,9 @@ function ConfigPanel({ presetKey, config, onConfigChange }) {
76878
76927
  }
76879
76928
  return null;
76880
76929
  }
76881
- function PreviewControls({ transition, onTransitionChange, onPreview, onCancelPreview, onApplyPreview, onApplyDirect, isTransitioning, isPreviewActive, canPreview, presetKey, sourceSelection, config }) {
76882
- const hasOrchestration = (0, import_react28.useMemo)(() => {
76930
+ function PreviewControls({ transition, onTransitionChange, onPreview, onCancelPreview, onApplyPreview, onApplyDirect, isTransitioning, isPreviewActive, canPreview, presetKey, sourceSelection, config, isPending }) {
76931
+ const isBusy = isPending || isTransitioning;
76932
+ const hasOrchestration = (0, import_react29.useMemo)(() => {
76883
76933
  try {
76884
76934
  const presetDef = PRESETS[presetKey]?.(sourceSelection, config, { width: 1920, height: 1080 });
76885
76935
  return !!presetDef?.orchestrations;
@@ -76893,22 +76943,22 @@ function PreviewControls({ transition, onTransitionChange, onPreview, onCancelPr
76893
76943
  }), className: "w-full accent-blue-600" }), (0, import_jsx_runtime63.jsxs)("div", { className: "flex justify-between text-xs text-gray-500", children: [(0, import_jsx_runtime63.jsx)("span", { children: "Instant" }), (0, import_jsx_runtime63.jsx)("span", { children: "3s" })] })] }), (0, import_jsx_runtime63.jsxs)("div", { className: "space-y-2", children: [(0, import_jsx_runtime63.jsx)("label", { className: "block text-sm font-medium text-gray-700 dark:text-gray-300", children: "Easing" }), (0, import_jsx_runtime63.jsxs)("select", { value: transition.easing, onChange: (e) => onTransitionChange({
76894
76944
  ...transition,
76895
76945
  easing: e.target.value
76896
- }), className: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600\n rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-white", children: [(0, import_jsx_runtime63.jsx)("option", { value: "linear", children: "Linear" }), (0, import_jsx_runtime63.jsx)("option", { value: "ease_in", children: "Ease In" }), (0, import_jsx_runtime63.jsx)("option", { value: "ease_out", children: "Ease Out" }), (0, import_jsx_runtime63.jsx)("option", { value: "ease_in_out", children: "Ease In Out" })] })] })] }), (0, import_jsx_runtime63.jsxs)("div", { className: "space-y-2", children: [(0, import_jsx_runtime63.jsx)("button", { className: "w-full px-4 py-3 rounded-lg font-medium\n bg-green-600 text-white hover:bg-green-700\n disabled:opacity-50 disabled:cursor-not-allowed\n transition-colors", onClick: isPreviewActive ? onApplyPreview : onApplyDirect, disabled: !canPreview || isTransitioning, children: isTransitioning ? "Transitioning..." : "Take to Live" }), isPreviewActive ? (0, import_jsx_runtime63.jsx)("button", { className: "w-full px-4 py-2 rounded-lg font-medium\n bg-red-600 text-white hover:bg-red-700\n transition-colors", onClick: onCancelPreview, children: "Cancel Preview" }) : (0, import_jsx_runtime63.jsx)("button", { className: "w-full px-4 py-2 rounded-lg font-medium\n border border-blue-600 text-blue-600 dark:text-blue-400\n hover:bg-blue-50 dark:hover:bg-blue-900/20\n disabled:opacity-50 disabled:cursor-not-allowed\n transition-colors", onClick: onPreview, disabled: !canPreview || isTransitioning, children: "Preview Video" })] })] });
76946
+ }), className: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600\n rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-white", children: [(0, import_jsx_runtime63.jsx)("option", { value: "linear", children: "Linear" }), (0, import_jsx_runtime63.jsx)("option", { value: "ease_in", children: "Ease In" }), (0, import_jsx_runtime63.jsx)("option", { value: "ease_out", children: "Ease Out" }), (0, import_jsx_runtime63.jsx)("option", { value: "ease_in_out", children: "Ease In Out" })] })] })] }), (0, import_jsx_runtime63.jsxs)("div", { className: "space-y-2", children: [(0, import_jsx_runtime63.jsx)("button", { className: "w-full px-4 py-3 rounded-lg font-medium\n bg-green-600 text-white hover:bg-green-700\n disabled:opacity-50 disabled:cursor-not-allowed\n transition-colors", onClick: isPreviewActive ? onApplyPreview : onApplyDirect, disabled: !canPreview || isBusy, children: isBusy ? "Transitioning..." : "Take to Live" }), isPreviewActive ? (0, import_jsx_runtime63.jsx)("button", { className: "w-full px-4 py-2 rounded-lg font-medium\n bg-red-600 text-white hover:bg-red-700\n transition-colors", onClick: onCancelPreview, children: "Cancel Preview" }) : (0, import_jsx_runtime63.jsx)("button", { className: "w-full px-4 py-2 rounded-lg font-medium\n border border-blue-600 text-blue-600 dark:text-blue-400\n hover:bg-blue-50 dark:hover:bg-blue-900/20\n disabled:opacity-50 disabled:cursor-not-allowed\n transition-colors", onClick: onPreview, disabled: !canPreview || isBusy, children: "Preview Video" })] })] });
76897
76947
  }
76898
76948
 
76899
76949
  // build/client/processor.videoCompose/webrtc-preview.js
76900
76950
  var import_jsx_runtime64 = __toESM(require_jsx_runtime());
76901
- var import_react29 = __toESM(require_react());
76951
+ var import_react30 = __toESM(require_react());
76902
76952
  var import_webrtc_client2 = __toESM(require_webrtc_client());
76903
76953
  function WebRTCPreview({ url, id, onPlaying }) {
76904
- const [_client, setClient] = (0, import_react29.useState)(null);
76905
- const [error, setError] = (0, import_react29.useState)(null);
76906
- const [connecting, setConnecting] = (0, import_react29.useState)(false);
76907
- const onPlayingRef = (0, import_react29.useRef)(onPlaying);
76908
- (0, import_react29.useEffect)(() => {
76954
+ const [_client, setClient] = (0, import_react30.useState)(null);
76955
+ const [error, setError] = (0, import_react30.useState)(null);
76956
+ const [connecting, setConnecting] = (0, import_react30.useState)(false);
76957
+ const onPlayingRef = (0, import_react30.useRef)(onPlaying);
76958
+ (0, import_react30.useEffect)(() => {
76909
76959
  onPlayingRef.current = onPlaying;
76910
76960
  }, [onPlaying]);
76911
- const createClient = (0, import_react29.useCallback)((url2) => {
76961
+ const createClient = (0, import_react30.useCallback)((url2) => {
76912
76962
  console.log(`[WebRTCPreview ${id}] Creating WHEP client for URL:`, url2);
76913
76963
  setConnecting(true);
76914
76964
  const client = new import_webrtc_client2.WhepClient({
@@ -76949,7 +76999,7 @@ function WebRTCPreview({ url, id, onPlaying }) {
76949
76999
  });
76950
77000
  return client;
76951
77001
  }, [id]);
76952
- const cleanupClient = (0, import_react29.useCallback)(() => {
77002
+ const cleanupClient = (0, import_react30.useCallback)(() => {
76953
77003
  setClient((client) => {
76954
77004
  if (!client)
76955
77005
  return null;
@@ -76966,7 +77016,7 @@ function WebRTCPreview({ url, id, onPlaying }) {
76966
77016
  setError(null);
76967
77017
  setConnecting(false);
76968
77018
  }, []);
76969
- (0, import_react29.useEffect)(() => {
77019
+ (0, import_react30.useEffect)(() => {
76970
77020
  if (url) {
76971
77021
  setError(null);
76972
77022
  const client = createClient(url);
@@ -77012,7 +77062,9 @@ function VisualPreview({ layout, sourceJpegUrls, reference, title }) {
77012
77062
  height: `${heightPercent}%`,
77013
77063
  opacity: layer.opacity,
77014
77064
  zIndex: layer.zIndex
77015
- }, children: [jpegUrl ? (0, import_jsx_runtime65.jsx)("img", { src: jpegUrl, alt: layer.sourceName, className: "w-full h-full object-cover", onError: (e) => {
77065
+ }, children: [jpegUrl ? (0, import_jsx_runtime65.jsx)("img", { src: jpegUrl, alt: layer.sourceName, className: "w-full h-full object-cover", loading: "lazy", decoding: "async", onLoad: (e) => {
77066
+ e.currentTarget.style.display = "";
77067
+ }, onError: (e) => {
77016
77068
  e.currentTarget.style.display = "none";
77017
77069
  } }) : (0, import_jsx_runtime65.jsx)("div", { className: "w-full h-full bg-gray-700 flex items-center justify-center", children: (0, import_jsx_runtime65.jsxs)("div", { className: "text-center p-2", children: [(0, import_jsx_runtime65.jsx)("div", { className: "text-xs text-gray-400 truncate", children: layer.sourceName }), (0, import_jsx_runtime65.jsx)("div", { className: "text-xs text-gray-500 mt-1", children: "No preview" })] }) }), (0, import_jsx_runtime65.jsx)("div", { className: "absolute top-1 left-1 px-1.5 py-0.5\n bg-black/60 rounded text-xs text-white\n font-mono", children: layer.sourceName }), (0, import_jsx_runtime65.jsx)("div", { className: "absolute top-1 right-1 w-5 h-5\n bg-blue-600 rounded-full flex items-center justify-center\n text-xs text-white font-semibold", children: layer.zIndex }), layer.opacity < 1 && (0, import_jsx_runtime65.jsxs)("div", { className: "absolute bottom-1 right-1 px-1.5 py-0.5\n bg-black/60 rounded text-xs text-white", children: [(layer.opacity * 100).toFixed(0), "%"] })] }, idx);
77018
77070
  }) })] });
@@ -77020,19 +77072,34 @@ function VisualPreview({ layout, sourceJpegUrls, reference, title }) {
77020
77072
 
77021
77073
  // build/client/processor.videoCompose/fullscreen.js
77022
77074
  function FullscreenView3({ state, config: _config, sendCommand }) {
77023
- const [showVideoPreview, setShowVideoPreview] = (0, import_react30.useState)(false);
77024
- const [pendingPreviewCommand, setPendingPreviewCommand] = (0, import_react30.useState)(null);
77025
- const [selectedPreset, setSelectedPreset] = (0, import_react30.useState)(null);
77026
- const [sourceSelection, setSourceSelection] = (0, import_react30.useState)([]);
77027
- const [presetConfig, setPresetConfig] = (0, import_react30.useState)({});
77028
- const prevConfigRef = (0, import_react30.useRef)({ selectedPreset, sourceSelection, presetConfig });
77029
- const handleSourceSelectionChange = (0, import_react30.useCallback)((sources) => {
77075
+ const [showVideoPreview, setShowVideoPreview] = (0, import_react31.useState)(false);
77076
+ const [pendingPreviewCommand, setPendingPreviewCommand] = (0, import_react31.useState)(null);
77077
+ const [selectedPreset, setSelectedPreset] = (0, import_react31.useState)(null);
77078
+ const [sourceSelection, setSourceSelection] = (0, import_react31.useState)([]);
77079
+ const [presetConfig, setPresetConfig] = (0, import_react31.useState)({});
77080
+ const prevConfigRef = (0, import_react31.useRef)({ selectedPreset, sourceSelection, presetConfig });
77081
+ const [throttledJpegUrls, setThrottledJpegUrls] = (0, import_react31.useState)({});
77082
+ const lastJpegUpdateRef = (0, import_react31.useRef)(0);
77083
+ (0, import_react31.useEffect)(() => {
77084
+ const now = Date.now();
77085
+ if (now - lastJpegUpdateRef.current > 2e3) {
77086
+ lastJpegUpdateRef.current = now;
77087
+ setThrottledJpegUrls(state.sourceJpegUrls ?? {});
77088
+ }
77089
+ }, [state.sourceJpegUrls]);
77090
+ const [imagesReady, setImagesReady] = (0, import_react31.useState)(false);
77091
+ (0, import_react31.useEffect)(() => {
77092
+ const timer = setTimeout(() => setImagesReady(true), 100);
77093
+ return () => clearTimeout(timer);
77094
+ }, []);
77095
+ const onlineSourceCount = (0, import_react31.useMemo)(() => state.sources.filter((s) => s.status === "online").length, [state.sources]);
77096
+ const handleSourceSelectionChange = (0, import_react31.useCallback)((sources) => {
77030
77097
  setSourceSelection(sources);
77031
77098
  }, []);
77032
- const handlePresetConfigChange = (0, import_react30.useCallback)((config) => {
77099
+ const handlePresetConfigChange = (0, import_react31.useCallback)((config) => {
77033
77100
  setPresetConfig(config);
77034
77101
  }, []);
77035
- (0, import_react30.useEffect)(() => {
77102
+ (0, import_react31.useEffect)(() => {
77036
77103
  const prev = prevConfigRef.current;
77037
77104
  const configChanged = prev.selectedPreset !== selectedPreset || JSON.stringify(prev.sourceSelection) !== JSON.stringify(sourceSelection) || JSON.stringify(prev.presetConfig) !== JSON.stringify(presetConfig);
77038
77105
  if (configChanged) {
@@ -77044,23 +77111,20 @@ function FullscreenView3({ state, config: _config, sendCommand }) {
77044
77111
  prevConfigRef.current = { selectedPreset, sourceSelection, presetConfig };
77045
77112
  }
77046
77113
  }, [selectedPreset, sourceSelection, presetConfig, state.previewMode, sendCommand]);
77047
- const handlePreviewPlaying = (0, import_react30.useCallback)(() => {
77114
+ const handlePreviewPlaying = (0, import_react31.useCallback)(() => {
77048
77115
  console.log("[FullscreenView] Preview video started playing, sending pending command");
77049
77116
  if (pendingPreviewCommand) {
77050
77117
  sendCommand(pendingPreviewCommand);
77051
77118
  setPendingPreviewCommand(null);
77052
77119
  }
77053
77120
  }, [pendingPreviewCommand, sendCommand]);
77054
- const handleSendCommand = (0, import_react30.useCallback)((cmd) => {
77121
+ const handleSendCommand = (0, import_react31.useCallback)((cmd) => {
77055
77122
  if (cmd.type === "enable-preview") {
77056
- console.log("[FullscreenView] Deferring enable-preview until video starts playing");
77057
77123
  setShowVideoPreview(true);
77058
77124
  setPendingPreviewCommand(cmd);
77059
77125
  setTimeout(() => {
77060
- console.log("[FullscreenView] Timeout reached, checking if command still pending");
77061
77126
  setPendingPreviewCommand((pending) => {
77062
77127
  if (pending) {
77063
- console.log("[FullscreenView] Sending command due to timeout");
77064
77128
  sendCommand(pending);
77065
77129
  return null;
77066
77130
  }
@@ -77075,22 +77139,26 @@ function FullscreenView3({ state, config: _config, sendCommand }) {
77075
77139
  sendCommand(cmd);
77076
77140
  }
77077
77141
  }, [sendCommand]);
77078
- const previewLayout = (() => {
77142
+ const referenceResolution = (0, import_react31.useMemo)(() => {
77143
+ const ref = state.sources.find((s) => s.sourceName === state.reference);
77144
+ return ref?.resolution;
77145
+ }, [state.sources, state.reference]);
77146
+ const referenceResolutionKey = referenceResolution ? `${referenceResolution.width}x${referenceResolution.height}` : null;
77147
+ const previewLayout = (0, import_react31.useMemo)(() => {
77079
77148
  if (!selectedPreset || sourceSelection.length === 0)
77080
77149
  return void 0;
77081
- const reference = state.sources.find((s) => s.sourceName === state.reference);
77082
- if (!reference?.resolution)
77150
+ if (!referenceResolution)
77083
77151
  return void 0;
77084
77152
  const generator = PRESETS[selectedPreset];
77085
77153
  if (!generator)
77086
77154
  return void 0;
77087
77155
  try {
77088
- const presetDef = generator(sourceSelection, presetConfig, { width: reference.resolution.width, height: reference.resolution.height });
77156
+ const presetDef = generator(sourceSelection, presetConfig, { width: referenceResolution.width, height: referenceResolution.height });
77089
77157
  return presetDef.finalConfig;
77090
77158
  } catch (_e) {
77091
77159
  return void 0;
77092
77160
  }
77093
- })();
77161
+ }, [selectedPreset, sourceSelection, presetConfig, referenceResolutionKey]);
77094
77162
  if (!state) {
77095
77163
  return (0, import_jsx_runtime66.jsx)("div", { className: "flex h-full items-center justify-center bg-gray-50 dark:bg-gray-900", children: (0, import_jsx_runtime66.jsxs)("div", { className: "text-center", children: [(0, import_jsx_runtime66.jsx)("div", { className: "text-xl font-semibold text-gray-900 dark:text-white mb-2", children: "Loading Video Compose..." }), (0, import_jsx_runtime66.jsx)("div", { className: "text-sm text-gray-500 dark:text-gray-400", children: "Connecting to component state..." })] }) });
77096
77164
  }
@@ -77101,7 +77169,7 @@ function FullscreenView3({ state, config: _config, sendCommand }) {
77101
77169
  }, sendCommand: handleSendCommand, selectedPreset, onSelectPreset: setSelectedPreset, sourceSelection, onSourceSelectionChange: handleSourceSelectionChange, presetConfig, onPresetConfigChange: handlePresetConfigChange }) }), (0, import_jsx_runtime66.jsxs)("div", { className: "flex-1 flex flex-col", children: [(0, import_jsx_runtime66.jsxs)("div", { className: "flex-1 flex gap-6 p-6 min-h-0", children: [(0, import_jsx_runtime66.jsxs)("div", { className: "flex-1 flex flex-col min-w-0", children: [(0, import_jsx_runtime66.jsx)("div", { className: "flex items-center justify-between mb-4", children: (0, import_jsx_runtime66.jsxs)("div", { className: "flex items-center gap-3", children: [(0, import_jsx_runtime66.jsx)("h2", { className: "text-xl font-bold text-gray-900 dark:text-white", children: showVideoPreview ? "Preview Transition" : "Preview Output" }), state.previewTransitioning && (0, import_jsx_runtime66.jsx)("span", { className: "text-sm text-blue-600 dark:text-blue-400 font-medium", children: "Transitioning..." })] }) }), showVideoPreview ? (0, import_jsx_runtime66.jsx)(WebRTCPreview, { url: state.previewPreviewUrl, id: `${_config.id}-preview`, onPlaying: handlePreviewPlaying }, state.previewPreviewUrl) : (0, import_jsx_runtime66.jsx)("div", { className: "flex-1 flex items-center justify-center bg-gray-900 rounded-lg overflow-hidden", children: (0, import_jsx_runtime66.jsx)("div", { className: "w-full h-full flex items-center justify-center p-4", children: (0, import_jsx_runtime66.jsx)("div", { className: "w-full max-w-full max-h-full", children: (0, import_jsx_runtime66.jsx)(VisualPreview, { layout: (
77102
77170
  // Show preset being built, or preview composition, or current layout
77103
77171
  previewLayout ? previewLayout : state.previewMode && state.previewResolvedLayout ? state.previewResolvedLayout : state.resolvedLayout
77104
- ), sourceJpegUrls: state.sourceJpegUrls, reference: state.sources.find((s) => s.sourceName === state.reference)?.resolution, title: previewLayout ? "Preset Preview (Building...)" : state.previewMode ? "Preview Composition" : "Current Layout" }) }) }) })] }), (0, import_jsx_runtime66.jsxs)("div", { className: "flex-1 flex flex-col min-w-0", children: [(0, import_jsx_runtime66.jsxs)("div", { className: "flex items-center justify-between mb-4", children: [(0, import_jsx_runtime66.jsx)("h2", { className: "text-xl font-bold text-gray-900 dark:text-white", children: "Live Output" }), state.transitioning && (0, import_jsx_runtime66.jsx)("span", { className: "text-sm text-blue-600 dark:text-blue-400 font-medium", children: "Transitioning..." }), !state.previewUrl && !state.referenceAvailable && (0, import_jsx_runtime66.jsx)("span", { className: "text-xs text-gray-500 dark:text-gray-400", children: "Waiting for reference stream..." })] }), (0, import_jsx_runtime66.jsx)(WebRTCPreview, { url: state.previewUrl, id: `${_config.id}-live` })] })] }), (0, import_jsx_runtime66.jsxs)("div", { className: "border-t border-gray-200 dark:border-gray-700\n bg-white dark:bg-gray-800 h-64 flex flex-col", children: [(0, import_jsx_runtime66.jsx)("div", { className: "p-4 border-b border-gray-200 dark:border-gray-700", children: (0, import_jsx_runtime66.jsx)("h2", { className: "text-lg font-bold text-gray-900 dark:text-white", children: "Status" }) }), (0, import_jsx_runtime66.jsxs)("div", { className: "flex-1 overflow-y-auto p-4 space-y-4", children: [(0, import_jsx_runtime66.jsxs)("div", { className: "space-y-2", children: [(0, import_jsx_runtime66.jsxs)("div", { className: "flex items-center justify-between", children: [(0, import_jsx_runtime66.jsx)("h3", { className: "text-sm font-semibold text-gray-900 dark:text-white", children: "Sources" }), (0, import_jsx_runtime66.jsxs)("span", { className: "text-xs text-gray-500 dark:text-gray-400", children: [state.sources.filter((s) => s.status === "online").length, " / ", state.sources.length, " online"] })] }), (0, import_jsx_runtime66.jsx)("div", { className: "space-y-1", children: state.sources.map((source, i) => (0, import_jsx_runtime66.jsxs)("div", { className: "flex items-center gap-2 p-1.5 rounded bg-gray-50 dark:bg-gray-900", children: [(0, import_jsx_runtime66.jsx)("div", { className: `w-1.5 h-1.5 rounded-full flex-shrink-0 ${source.status === "online" ? "bg-live" : "bg-offline"}` }), (0, import_jsx_runtime66.jsxs)("div", { className: "flex-1 min-w-0", children: [(0, import_jsx_runtime66.jsxs)("div", { className: `text-xs font-medium ${source.sourceName === state.reference ? "text-blue-600 dark:text-blue-400" : "text-gray-900 dark:text-white"}`, children: [source.sourceName, source.sourceName === state.reference && " (ref)"] }), source.resolution && (0, import_jsx_runtime66.jsxs)("div", { className: "text-xs text-gray-500 dark:text-gray-400", children: [source.resolution.width, "\xD7", source.resolution.height, source.frameRate && ` @ ${source.frameRate.frames}/${source.frameRate.seconds}fps`] })] })] }, i)) })] }), state.resolvedLayout && state.resolvedLayout.layers.length > 0 && (0, import_jsx_runtime66.jsxs)("div", { className: "space-y-2", children: [(0, import_jsx_runtime66.jsxs)("div", { children: [(0, import_jsx_runtime66.jsx)("h3", { className: "text-sm font-semibold text-gray-900 dark:text-white", children: "Active Layout" }), (0, import_jsx_runtime66.jsxs)("div", { className: "text-xs text-gray-600 dark:text-gray-400 mt-0.5", children: [state.layout?.type === "preset" ? state.layout.preset : "Custom", " \u2022 ", state.resolvedLayout.layers.length, " ", state.resolvedLayout.layers.length === 1 ? "layer" : "layers"] })] }), (0, import_jsx_runtime66.jsx)("div", { className: "space-y-1", children: state.resolvedLayout.layers.map((layer, i) => (0, import_jsx_runtime66.jsxs)("div", { className: "text-xs p-1.5 rounded bg-gray-50 dark:bg-gray-900", children: [(0, import_jsx_runtime66.jsxs)("div", { className: "font-medium text-gray-900 dark:text-white", children: ["L", layer.zIndex, ": ", layer.sourceName] }), (0, import_jsx_runtime66.jsxs)("div", { className: "text-xs text-gray-500 dark:text-gray-400", children: [(layer.opacity * 100).toFixed(0), "%", layer.destRect && (0, import_jsx_runtime66.jsxs)(import_jsx_runtime66.Fragment, { children: [" \u2022 (", layer.destRect.x.toFixed(0), ", ", layer.destRect.y.toFixed(0), ")"] })] })] }, i)) })] }), state.transitioning && (0, import_jsx_runtime66.jsxs)("div", { className: "space-y-1", children: [(0, import_jsx_runtime66.jsx)("h3", { className: "text-sm font-semibold text-gray-900 dark:text-white", children: "Transition" }), (0, import_jsx_runtime66.jsx)("div", { className: "text-xs text-gray-600 dark:text-gray-400", children: "In progress..." })] })] })] })] })] });
77172
+ ), sourceJpegUrls: imagesReady ? throttledJpegUrls : {}, reference: referenceResolution, title: previewLayout ? "Preset Preview (Building...)" : state.previewMode ? "Preview Composition" : "Current Layout" }) }) }) })] }), (0, import_jsx_runtime66.jsxs)("div", { className: "flex-1 flex flex-col min-w-0", children: [(0, import_jsx_runtime66.jsxs)("div", { className: "flex items-center justify-between mb-4", children: [(0, import_jsx_runtime66.jsx)("h2", { className: "text-xl font-bold text-gray-900 dark:text-white", children: "Live Output" }), state.transitioning && (0, import_jsx_runtime66.jsx)("span", { className: "text-sm text-blue-600 dark:text-blue-400 font-medium", children: "Transitioning..." }), !state.previewUrl && !state.referenceAvailable && (0, import_jsx_runtime66.jsx)("span", { className: "text-xs text-gray-500 dark:text-gray-400", children: "Waiting for reference stream..." })] }), (0, import_jsx_runtime66.jsx)(WebRTCPreview, { url: state.previewUrl, id: `${_config.id}-live` })] })] }), (0, import_jsx_runtime66.jsxs)("div", { className: "border-t border-gray-200 dark:border-gray-700\n bg-white dark:bg-gray-800 h-64 flex flex-col", children: [(0, import_jsx_runtime66.jsx)("div", { className: "p-4 border-b border-gray-200 dark:border-gray-700", children: (0, import_jsx_runtime66.jsx)("h2", { className: "text-lg font-bold text-gray-900 dark:text-white", children: "Status" }) }), (0, import_jsx_runtime66.jsxs)("div", { className: "flex-1 overflow-y-auto p-4 space-y-4", children: [(0, import_jsx_runtime66.jsxs)("div", { className: "space-y-2", children: [(0, import_jsx_runtime66.jsxs)("div", { className: "flex items-center justify-between", children: [(0, import_jsx_runtime66.jsx)("h3", { className: "text-sm font-semibold text-gray-900 dark:text-white", children: "Sources" }), (0, import_jsx_runtime66.jsxs)("span", { className: "text-xs text-gray-500 dark:text-gray-400", children: [onlineSourceCount, " / ", state.sources.length, " online"] })] }), (0, import_jsx_runtime66.jsx)("div", { className: "space-y-1", children: state.sources.map((source, i) => (0, import_jsx_runtime66.jsxs)("div", { className: "flex items-center gap-2 p-1.5 rounded bg-gray-50 dark:bg-gray-900", children: [(0, import_jsx_runtime66.jsx)("div", { className: `w-1.5 h-1.5 rounded-full flex-shrink-0 ${source.status === "online" ? "bg-live" : "bg-offline"}` }), (0, import_jsx_runtime66.jsxs)("div", { className: "flex-1 min-w-0", children: [(0, import_jsx_runtime66.jsxs)("div", { className: `text-xs font-medium ${source.sourceName === state.reference ? "text-blue-600 dark:text-blue-400" : "text-gray-900 dark:text-white"}`, children: [source.sourceName, source.sourceName === state.reference && " (ref)"] }), source.resolution && (0, import_jsx_runtime66.jsxs)("div", { className: "text-xs text-gray-500 dark:text-gray-400", children: [source.resolution.width, "\xD7", source.resolution.height, source.frameRate && ` @ ${source.frameRate.frames}/${source.frameRate.seconds}fps`] })] })] }, i)) })] }), state.resolvedLayout && state.resolvedLayout.layers.length > 0 && (0, import_jsx_runtime66.jsxs)("div", { className: "space-y-2", children: [(0, import_jsx_runtime66.jsxs)("div", { children: [(0, import_jsx_runtime66.jsx)("h3", { className: "text-sm font-semibold text-gray-900 dark:text-white", children: "Active Layout" }), (0, import_jsx_runtime66.jsxs)("div", { className: "text-xs text-gray-600 dark:text-gray-400 mt-0.5", children: [state.layout?.type === "preset" ? state.layout.preset : "Custom", " \u2022 ", state.resolvedLayout.layers.length, " ", state.resolvedLayout.layers.length === 1 ? "layer" : "layers"] })] }), (0, import_jsx_runtime66.jsx)("div", { className: "space-y-1", children: state.resolvedLayout.layers.map((layer, i) => (0, import_jsx_runtime66.jsxs)("div", { className: "text-xs p-1.5 rounded bg-gray-50 dark:bg-gray-900", children: [(0, import_jsx_runtime66.jsxs)("div", { className: "font-medium text-gray-900 dark:text-white", children: ["L", layer.zIndex, ": ", layer.sourceName] }), (0, import_jsx_runtime66.jsxs)("div", { className: "text-xs text-gray-500 dark:text-gray-400", children: [(layer.opacity * 100).toFixed(0), "%", layer.destRect && (0, import_jsx_runtime66.jsxs)(import_jsx_runtime66.Fragment, { children: [" \u2022 (", layer.destRect.x.toFixed(0), ", ", layer.destRect.y.toFixed(0), ")"] })] })] }, i)) })] }), state.transitioning && (0, import_jsx_runtime66.jsxs)("div", { className: "space-y-1", children: [(0, import_jsx_runtime66.jsx)("h3", { className: "text-sm font-semibold text-gray-900 dark:text-white", children: "Transition" }), (0, import_jsx_runtime66.jsx)("div", { className: "text-xs text-gray-600 dark:text-gray-400", children: "In progress..." })] })] })] })] })] });
77105
77173
  }
77106
77174
  var fullscreen_default2 = FullscreenView3;
77107
77175
 
@@ -77733,7 +77801,7 @@ var import_util15 = __toESM(require_util());
77733
77801
 
77734
77802
  // build/client/processor.webRtcDuplex/fullscreen.js
77735
77803
  var import_jsx_runtime71 = __toESM(require_jsx_runtime());
77736
- var import_react31 = __toESM(require_react());
77804
+ var import_react32 = __toESM(require_react());
77737
77805
  var import_webrtc_client3 = __toESM(require_webrtc_client());
77738
77806
  var DuplexWhepClient = class extends NorskWhepClient {
77739
77807
  showVideo;
@@ -77774,11 +77842,11 @@ var DuplexWhipClient = class extends import_webrtc_client3.WhipClient {
77774
77842
  }
77775
77843
  };
77776
77844
  function FullscreenView4({ state, config }) {
77777
- const [client, setClient] = (0, import_react31.useState)(void 0);
77778
- const [sender, setSender] = (0, import_react31.useState)(void 0);
77779
- const container = (0, import_react31.useRef)(null);
77780
- const button = (0, import_react31.useRef)(null);
77781
- const go = (0, import_react31.useCallback)(async () => {
77845
+ const [client, setClient] = (0, import_react32.useState)(void 0);
77846
+ const [sender, setSender] = (0, import_react32.useState)(void 0);
77847
+ const container = (0, import_react32.useRef)(null);
77848
+ const button = (0, import_react32.useRef)(null);
77849
+ const go = (0, import_react32.useCallback)(async () => {
77782
77850
  if (!state.publishUrl)
77783
77851
  return;
77784
77852
  if (!client && state.outputUrl && container.current) {
@@ -78070,7 +78138,7 @@ function assertUnreachable30(_) {
78070
78138
 
78071
78139
  // build/client/util.stats.latency/inline-view.js
78072
78140
  var import_jsx_runtime73 = __toESM(require_jsx_runtime());
78073
- var import_react32 = __toESM(require_react());
78141
+ var import_react33 = __toESM(require_react());
78074
78142
 
78075
78143
  // ../../node_modules/chart.js/auto/auto.js
78076
78144
  Chart.register(...registerables);
@@ -78078,8 +78146,8 @@ var auto_default = Chart;
78078
78146
 
78079
78147
  // build/client/util.stats.latency/inline-view.js
78080
78148
  function InlineView22({ state, config: _2 }) {
78081
- const chartContainer = (0, import_react32.useRef)(null);
78082
- const [chartControl, setChartControl] = (0, import_react32.useState)(void 0);
78149
+ const chartContainer = (0, import_react33.useRef)(null);
78150
+ const [chartControl, setChartControl] = (0, import_react33.useState)(void 0);
78083
78151
  function makeDataSet(key, color2, values) {
78084
78152
  return {
78085
78153
  label: key,
@@ -78090,13 +78158,13 @@ function InlineView22({ state, config: _2 }) {
78090
78158
  data: values
78091
78159
  };
78092
78160
  }
78093
- const makeData = (0, import_react32.useCallback)((state2) => {
78161
+ const makeData = (0, import_react33.useCallback)((state2) => {
78094
78162
  return {
78095
78163
  labels: new Array(state2.values.length).fill(0).map((_, i) => i).map((_) => ""),
78096
78164
  datasets: [makeDataSet("latency", "rgba(255, 0, 0, 255)", state2.values)]
78097
78165
  };
78098
78166
  }, []);
78099
- (0, import_react32.useEffect)(() => {
78167
+ (0, import_react33.useEffect)(() => {
78100
78168
  if (!chartContainer.current)
78101
78169
  return;
78102
78170
  auto_default.defaults.color = "#FFF";
@@ -78134,7 +78202,7 @@ function InlineView22({ state, config: _2 }) {
78134
78202
  chart.update();
78135
78203
  }, 100);
78136
78204
  }, [chartContainer, makeData, state]);
78137
- (0, import_react32.useEffect)(() => {
78205
+ (0, import_react33.useEffect)(() => {
78138
78206
  if (!chartControl)
78139
78207
  return;
78140
78208
  chartControl.data = makeData(state);