@norskvideo/ctl-sdk 0.1.16 → 0.1.18
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.
package/components/ShotBar.d.ts
CHANGED
|
@@ -18,6 +18,16 @@ export interface ShotBarProps {
|
|
|
18
18
|
graphicBaseUrl?: string;
|
|
19
19
|
/** Default transition applied to takes. */
|
|
20
20
|
transition?: Transition;
|
|
21
|
+
/**
|
|
22
|
+
* Compose output resolution the shot `config` rects are expressed in — the
|
|
23
|
+
* denominator for the thumbnail geometry. Pass the live reference so a 720p
|
|
24
|
+
* compose draws the L-bar/PiP box where the graphic frames it; defaults to
|
|
25
|
+
* 1920x1080.
|
|
26
|
+
*/
|
|
27
|
+
reference?: {
|
|
28
|
+
width: number;
|
|
29
|
+
height: number;
|
|
30
|
+
};
|
|
21
31
|
/** Show the editable graphic-URL field on graphic-bearing cards (default true). */
|
|
22
32
|
editableGraphics?: boolean;
|
|
23
33
|
/** Send the assembled commands to air. The consumer owns the WebSocket. */
|
|
@@ -43,4 +53,4 @@ export interface ShotBarProps {
|
|
|
43
53
|
*/
|
|
44
54
|
renderPreviewMonitor?: (selected: ResolvedShot | null) => ReactNode;
|
|
45
55
|
}
|
|
46
|
-
export declare function ShotBar({ shots, pgmShotId, pvwShotId, graphicBaseUrl, transition, editableGraphics, onTake, onPreview, previewing, onCancelPreview, renderProgramMonitor, renderPreviewMonitor, }: ShotBarProps): import("react").JSX.Element;
|
|
56
|
+
export declare function ShotBar({ shots, pgmShotId, pvwShotId, graphicBaseUrl, transition, reference, editableGraphics, onTake, onPreview, previewing, onCancelPreview, renderProgramMonitor, renderPreviewMonitor, }: ShotBarProps): import("react").JSX.Element;
|
package/components/ShotBar.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useState } from "react";
|
|
3
|
-
import { graphicResolvable, hideReasonLabel, prepareTake } from "./shot-bar-view-model.js";
|
|
3
|
+
import { graphicResolvable, hideReasonLabel, prepareTake, shotThumbnailBox } from "./shot-bar-view-model.js";
|
|
4
4
|
import { useUIPrimitives } from "./ui-primitives.js";
|
|
5
5
|
const FAMILY_BADGE = { geometry: "Geometry", graphic: "Graphic" };
|
|
6
|
-
export function ShotBar({ shots, pgmShotId, pvwShotId, graphicBaseUrl, transition, editableGraphics = true, onTake, onPreview, previewing = false, onCancelPreview, renderProgramMonitor, renderPreviewMonitor, }) {
|
|
6
|
+
export function ShotBar({ shots, pgmShotId, pvwShotId, graphicBaseUrl, transition, reference = { width: 1920, height: 1080 }, editableGraphics = true, onTake, onPreview, previewing = false, onCancelPreview, renderProgramMonitor, renderPreviewMonitor, }) {
|
|
7
7
|
const { Button, Input } = useUIPrimitives();
|
|
8
8
|
const [selectedId, setSelectedId] = useState(null);
|
|
9
9
|
const [editedUrls, setEditedUrls] = useState({});
|
|
@@ -28,7 +28,7 @@ export function ShotBar({ shots, pgmShotId, pvwShotId, graphicBaseUrl, transitio
|
|
|
28
28
|
// preview take handler or a preview monitor renderer). Otherwise it's a dead
|
|
29
29
|
// pane — a direct-take product has no PVW feed — so give On Air the full row.
|
|
30
30
|
const showPreview = Boolean(onPreview || renderPreviewMonitor);
|
|
31
|
-
return (_jsxs("div", { className: "flex flex-col gap-4 text-zinc-100", children: [_jsxs("div", { className: "grid gap-3", style: { gridTemplateColumns: showPreview ? "minmax(0, 1fr) minmax(0, 1fr)" : "minmax(0, 1fr)" }, children: [showPreview ? (_jsx(Monitor, { label: "Preview", tone: "pvw", shot: findLabel(shots, pvwShotId), render: renderPreviewMonitor ? () => renderPreviewMonitor(selected ?? null) : undefined })) : null, _jsx(Monitor, { label: "Live", tone: "pgm", shot: findLabel(shots, pgmShotId), render: renderProgramMonitor })] }), _jsx("div", { className: "flex gap-3 overflow-x-auto pb-1", children: shots.map((shot) => (_jsx(ShotCard, { shot: shot, selected: shot.id === selectedId, onAir: shot.id === pgmShotId, config: shot.status === "ready" ? shot.config : undefined, onSelect: () => shot.status === "ready" && setSelectedId(shot.id) }, shot.id))) }), selected ? (_jsxs("div", { className: "flex flex-col gap-3 rounded-lg border border-zinc-800 bg-zinc-900/60 p-3", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("div", { className: "flex items-center gap-2 text-sm font-medium", children: [selected.label, previewing ? (_jsx("span", { className: "rounded bg-emerald-600/20 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-emerald-400", children: "Previewing" })) : null] }), _jsxs("div", { className: "flex gap-2", children: [previewing ? (onCancelPreview ? (_jsx(Button, { variant: "ghost", size: "sm", onClick: onCancelPreview, children: "Cancel" })) : null) : onPreview ? (_jsx(Button, { variant: "ghost", size: "sm", disabled: !takeableGraphic, onClick: () => onPreview(commands, selected), children: "Preview" })) : null, _jsx(Button, { variant: "primary", size: "sm", disabled: !takeableGraphic, onClick: () => onTake(commands, selected), children: "Take" })] })] }), !takeableGraphic ? (_jsx("p", { className: "text-xs text-amber-500/90", children: "This shot's graphic page is a relative path but no engine-reachable graphic base is configured, so it can't be loaded into the overlay. Set an absolute page URL, or configure the overlay base." })) : null, editableGraphics && selected.graphicUrl !== undefined ? (_jsxs("div", { className: "flex flex-col gap-1 text-xs text-zinc-400", children: [_jsx("label", { htmlFor: "shotbar-graphic-url", children: "Graphic page" }), _jsx(Input, { id: "shotbar-graphic-url", type: "text", value: editedUrls[selected.id] ?? selected.graphicUrl, placeholder: selected.graphicUrl, onChange: (e) => setEditedUrls((prev) => ({ ...prev, [selected.id]: e.target.value })) })] })) : null] })) : (_jsx("div", { className: "rounded-lg border border-dashed border-zinc-800 p-4 text-center text-sm text-zinc-500", children: "No shot selected \u2014 pick an available shot above." }))] }));
|
|
31
|
+
return (_jsxs("div", { className: "flex flex-col gap-4 text-zinc-100", children: [_jsxs("div", { className: "grid gap-3", style: { gridTemplateColumns: showPreview ? "minmax(0, 1fr) minmax(0, 1fr)" : "minmax(0, 1fr)" }, children: [showPreview ? (_jsx(Monitor, { label: "Preview", tone: "pvw", shot: findLabel(shots, pvwShotId), render: renderPreviewMonitor ? () => renderPreviewMonitor(selected ?? null) : undefined })) : null, _jsx(Monitor, { label: "Live", tone: "pgm", shot: findLabel(shots, pgmShotId), render: renderProgramMonitor })] }), _jsx("div", { className: "flex gap-3 overflow-x-auto pb-1", children: shots.map((shot) => (_jsx(ShotCard, { shot: shot, selected: shot.id === selectedId, onAir: shot.id === pgmShotId, config: shot.status === "ready" ? shot.config : undefined, reference: reference, onSelect: () => shot.status === "ready" && setSelectedId(shot.id) }, shot.id))) }), selected ? (_jsxs("div", { className: "flex flex-col gap-3 rounded-lg border border-zinc-800 bg-zinc-900/60 p-3", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("div", { className: "flex items-center gap-2 text-sm font-medium", children: [selected.label, previewing ? (_jsx("span", { className: "rounded bg-emerald-600/20 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-emerald-400", children: "Previewing" })) : null] }), _jsxs("div", { className: "flex gap-2", children: [previewing ? (onCancelPreview ? (_jsx(Button, { variant: "ghost", size: "sm", onClick: onCancelPreview, children: "Cancel" })) : null) : onPreview ? (_jsx(Button, { variant: "ghost", size: "sm", disabled: !takeableGraphic, onClick: () => onPreview(commands, selected), children: "Preview" })) : null, _jsx(Button, { variant: "primary", size: "sm", disabled: !takeableGraphic, onClick: () => onTake(commands, selected), children: "Take" })] })] }), !takeableGraphic ? (_jsx("p", { className: "text-xs text-amber-500/90", children: "This shot's graphic page is a relative path but no engine-reachable graphic base is configured, so it can't be loaded into the overlay. Set an absolute page URL, or configure the overlay base." })) : null, editableGraphics && selected.graphicUrl !== undefined ? (_jsxs("div", { className: "flex flex-col gap-1 text-xs text-zinc-400", children: [_jsx("label", { htmlFor: "shotbar-graphic-url", children: "Graphic page" }), _jsx(Input, { id: "shotbar-graphic-url", type: "text", value: editedUrls[selected.id] ?? selected.graphicUrl, placeholder: selected.graphicUrl, onChange: (e) => setEditedUrls((prev) => ({ ...prev, [selected.id]: e.target.value })) })] })) : null] })) : (_jsx("div", { className: "rounded-lg border border-dashed border-zinc-800 p-4 text-center text-sm text-zinc-500", children: "No shot selected \u2014 pick an available shot above." }))] }));
|
|
32
32
|
}
|
|
33
33
|
function findLabel(shots, id) {
|
|
34
34
|
if (!id)
|
|
@@ -40,28 +40,25 @@ function Monitor({ label, tone, shot, render, }) {
|
|
|
40
40
|
const chip = tone === "pgm" ? "bg-red-600 text-white" : "bg-emerald-600 text-white";
|
|
41
41
|
return (_jsxs("div", { className: `relative overflow-hidden rounded-lg border-2 ${ring} bg-black`, children: [_jsx("span", { className: `absolute left-2 top-2 z-10 rounded px-1.5 py-0.5 text-[10px] font-semibold ${chip}`, children: label }), _jsx("div", { className: "flex aspect-video items-center justify-center", children: render ? render() : _jsx("span", { className: "text-xs text-zinc-500", children: shot ?? "—" }) })] }));
|
|
42
42
|
}
|
|
43
|
-
function ShotCard({ shot, selected, onAir, config, onSelect, }) {
|
|
43
|
+
function ShotCard({ shot, selected, onAir, config, reference, onSelect, }) {
|
|
44
44
|
const hidden = shot.status === "hidden";
|
|
45
45
|
const border = onAir ? "border-red-600" : selected ? "border-sky-500" : "border-zinc-800 hover:border-zinc-600";
|
|
46
|
-
return (_jsxs("button", { type: "button", onClick: onSelect, disabled: hidden, title: hidden ? hideReasonLabel(shot.reason) : shot.label, className: `flex w-40 shrink-0 flex-col gap-2 rounded-lg border ${border} bg-zinc-900 p-2 text-left transition-colors disabled:cursor-not-allowed disabled:opacity-40`, children: [_jsx(ShotThumbnail, { preset: shot.status === "ready" ? shot.preset : "fullscreen", config: config }), _jsxs("div", { className: "flex items-center justify-between gap-1", children: [_jsx("span", { className: "truncate text-xs font-medium text-zinc-100", children: shot.label }), shot.status === "ready" ? (_jsx("span", { className: "shrink-0 rounded bg-zinc-800 px-1 text-[9px] uppercase tracking-wide text-zinc-400", children: FAMILY_BADGE[shot.family] })) : null] }), hidden ? (_jsx("span", { className: "text-[10px] leading-tight text-amber-500/80", children: hideReasonLabel(shot.reason) })) : null] }));
|
|
46
|
+
return (_jsxs("button", { type: "button", onClick: onSelect, disabled: hidden, title: hidden ? hideReasonLabel(shot.reason) : shot.label, className: `flex w-40 shrink-0 flex-col gap-2 rounded-lg border ${border} bg-zinc-900 p-2 text-left transition-colors disabled:cursor-not-allowed disabled:opacity-40`, children: [_jsx(ShotThumbnail, { preset: shot.status === "ready" ? shot.preset : "fullscreen", config: config, reference: reference }), _jsxs("div", { className: "flex items-center justify-between gap-1", children: [_jsx("span", { className: "truncate text-xs font-medium text-zinc-100", children: shot.label }), shot.status === "ready" ? (_jsx("span", { className: "shrink-0 rounded bg-zinc-800 px-1 text-[9px] uppercase tracking-wide text-zinc-400", children: FAMILY_BADGE[shot.family] })) : null] }), hidden ? (_jsx("span", { className: "text-[10px] leading-tight text-amber-500/80", children: hideReasonLabel(shot.reason) })) : null] }));
|
|
47
47
|
}
|
|
48
48
|
// A truthful 16:9 schematic of what the preset does — programme fill plus the
|
|
49
|
-
// preset's overlay/box geometry, read from config where it's positional.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
// preset's overlay/box geometry, read from config where it's positional. The
|
|
50
|
+
// positional box is scaled against `reference` (the compose output space the
|
|
51
|
+
// config px live in), not a fixed canvas — see shotThumbnailBox.
|
|
52
|
+
function ShotThumbnail({ preset, config, reference, }) {
|
|
53
|
+
const box = shotThumbnailBox(preset, config, reference);
|
|
54
|
+
const p = (n) => (n == null ? undefined : `${n}%`);
|
|
53
55
|
const prog = _jsx("div", { className: "absolute inset-0 bg-sky-700/50" });
|
|
54
56
|
let overlay = null;
|
|
55
|
-
if (preset === "pip") {
|
|
56
|
-
overlay = (_jsx("div", { className: "absolute rounded-sm bg-amber-400/80", style: { right:
|
|
57
|
+
if (preset === "pip" && box) {
|
|
58
|
+
overlay = (_jsx("div", { className: "absolute rounded-sm bg-amber-400/80", style: { right: p(box.right), bottom: p(box.bottom), width: p(box.width), height: p(box.height) } }));
|
|
57
59
|
}
|
|
58
|
-
else if (preset === "lbar") {
|
|
59
|
-
overlay = (_jsx("div", { className: "absolute rounded-sm bg-sky-700/70 ring-1 ring-sky-300/40", style: {
|
|
60
|
-
left: pct(c.videoX ?? 480, 1920),
|
|
61
|
-
top: pct(c.videoY ?? 0, 1080),
|
|
62
|
-
width: pct(c.videoWidth ?? 1440, 1920),
|
|
63
|
-
height: pct(c.videoHeight ?? 810, 1080),
|
|
64
|
-
} }));
|
|
60
|
+
else if (preset === "lbar" && box) {
|
|
61
|
+
overlay = (_jsx("div", { className: "absolute rounded-sm bg-sky-700/70 ring-1 ring-sky-300/40", style: { left: p(box.left), top: p(box.top), width: p(box.width), height: p(box.height) } }));
|
|
65
62
|
}
|
|
66
63
|
else if (preset === "side-by-side") {
|
|
67
64
|
return (_jsxs("div", { className: "relative aspect-video w-full overflow-hidden rounded bg-zinc-950", children: [_jsx("div", { className: "absolute inset-y-0 left-0 w-1/2 bg-sky-700/50" }), _jsx("div", { className: "absolute inset-y-0 right-0 w-1/2 bg-emerald-700/50" })] }));
|
|
@@ -27,6 +27,33 @@ export declare function resolveGraphicUrl(url: string, base: string | undefined)
|
|
|
27
27
|
* rather than fire a `change-url` the media container can't fetch.
|
|
28
28
|
*/
|
|
29
29
|
export declare function graphicResolvable(graphicUrl: string | undefined, base: string | undefined): boolean;
|
|
30
|
+
/** The overlay box a positional preset draws in the ShotCard thumbnail, as
|
|
31
|
+
* percentages (0..100) of the 16:9 card. Corner-anchored presets use
|
|
32
|
+
* right/bottom; rect presets use left/top. */
|
|
33
|
+
export interface ThumbBox {
|
|
34
|
+
left?: number;
|
|
35
|
+
top?: number;
|
|
36
|
+
right?: number;
|
|
37
|
+
bottom?: number;
|
|
38
|
+
width: number;
|
|
39
|
+
height: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The overlay box for a shot's thumbnail schematic, or `null` for presets whose
|
|
43
|
+
* thumbnail is a fixed schematic with no config-driven geometry (side-by-side,
|
|
44
|
+
* quarters, overlay, lower-third, fullscreen).
|
|
45
|
+
*
|
|
46
|
+
* A shot's `config` rect is absolute px in the compose OUTPUT space, so the box
|
|
47
|
+
* MUST be divided by that same `reference` resolution — NOT a fixed 1080p canvas.
|
|
48
|
+
* A 720p compose (config scaled to 1280x720) drew the L-bar window at 16.7%/50%
|
|
49
|
+
* against a hardcoded 1920x1080, landing it off the top-left of where the
|
|
50
|
+
* graphic frames it. Absent config fields fall back to the shipped demo geometry
|
|
51
|
+
* as fixed percentages, so a bare deployment is unchanged at any resolution.
|
|
52
|
+
*/
|
|
53
|
+
export declare function shotThumbnailBox(preset: string, config: Record<string, unknown> | undefined, reference: {
|
|
54
|
+
width: number;
|
|
55
|
+
height: number;
|
|
56
|
+
}): ThumbBox | null;
|
|
30
57
|
export interface PrepareTakeOptions {
|
|
31
58
|
/**
|
|
32
59
|
* Engine-reachable base to resolve a relative graphic path against — the
|
|
@@ -69,6 +69,46 @@ export function graphicResolvable(graphicUrl, base) {
|
|
|
69
69
|
return true;
|
|
70
70
|
return resolveGraphicUrl(graphicUrl, base) !== null;
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* The overlay box for a shot's thumbnail schematic, or `null` for presets whose
|
|
74
|
+
* thumbnail is a fixed schematic with no config-driven geometry (side-by-side,
|
|
75
|
+
* quarters, overlay, lower-third, fullscreen).
|
|
76
|
+
*
|
|
77
|
+
* A shot's `config` rect is absolute px in the compose OUTPUT space, so the box
|
|
78
|
+
* MUST be divided by that same `reference` resolution — NOT a fixed 1080p canvas.
|
|
79
|
+
* A 720p compose (config scaled to 1280x720) drew the L-bar window at 16.7%/50%
|
|
80
|
+
* against a hardcoded 1920x1080, landing it off the top-left of where the
|
|
81
|
+
* graphic frames it. Absent config fields fall back to the shipped demo geometry
|
|
82
|
+
* as fixed percentages, so a bare deployment is unchanged at any resolution.
|
|
83
|
+
*/
|
|
84
|
+
export function shotThumbnailBox(preset, config, reference) {
|
|
85
|
+
const c = config ?? {};
|
|
86
|
+
const num = (k) => (typeof c[k] === "number" ? c[k] : undefined);
|
|
87
|
+
const pct = (v, of) => Math.max(0, Math.min(100, (v / of) * 100));
|
|
88
|
+
if (preset === "lbar") {
|
|
89
|
+
const vx = num("videoX");
|
|
90
|
+
const vy = num("videoY");
|
|
91
|
+
const vw = num("videoWidth");
|
|
92
|
+
const vh = num("videoHeight");
|
|
93
|
+
return {
|
|
94
|
+
left: vx != null ? pct(vx, reference.width) : 25,
|
|
95
|
+
top: vy != null ? pct(vy, reference.height) : 0,
|
|
96
|
+
width: vw != null ? pct(vw, reference.width) : 75,
|
|
97
|
+
height: vh != null ? pct(vh, reference.height) : 75,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (preset === "pip") {
|
|
101
|
+
const w = num("width");
|
|
102
|
+
const h = num("height");
|
|
103
|
+
return {
|
|
104
|
+
right: 8,
|
|
105
|
+
bottom: 10,
|
|
106
|
+
width: w != null ? pct(w, reference.width) : 20,
|
|
107
|
+
height: h != null ? pct(h, reference.height) : 25,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
72
112
|
/**
|
|
73
113
|
* Assemble the exact `TakeCommand[]` for a shot: pick the graphic URL (edited
|
|
74
114
|
* over manifest), resolve it against the engine-reachable base, and hand
|
package/package.json
CHANGED
|
@@ -49,6 +49,10 @@ export function parseProductTemplatesFile(raw, path) {
|
|
|
49
49
|
rec.sha256 = entry.sha256;
|
|
50
50
|
if (isRecord(entry.launchDefaults))
|
|
51
51
|
rec.launchDefaults = entry.launchDefaults;
|
|
52
|
+
if (isRecord(entry.parameterDefaults) &&
|
|
53
|
+
Object.values(entry.parameterDefaults).every((v) => typeof v === "string")) {
|
|
54
|
+
rec.parameterDefaults = entry.parameterDefaults;
|
|
55
|
+
}
|
|
52
56
|
if (Array.isArray(entry.suggestedSecurityGroups) &&
|
|
53
57
|
entry.suggestedSecurityGroups.every((l) => typeof l === "string")) {
|
|
54
58
|
rec.suggestedSecurityGroups = entry.suggestedSecurityGroups;
|
|
@@ -49,6 +49,14 @@ export interface ProductTemplateRecord {
|
|
|
49
49
|
* authoritatively by the worker at launch. Rides launches as
|
|
50
50
|
* template_launch_defaults_json. */
|
|
51
51
|
launchDefaults?: Record<string, unknown>;
|
|
52
|
+
/** Template-author parameter tier (env-var authoring): name → default
|
|
53
|
+
* value. A name the product declared overrides the manifest default;
|
|
54
|
+
* an undeclared name ADDS a template-defined variable. Fused into every
|
|
55
|
+
* launch's parameter map beneath the operator's instance values —
|
|
56
|
+
* instance wins, template default wins over manifest default. Names
|
|
57
|
+
* must be env-var shaped and never `NORSK_`-prefixed (that namespace is
|
|
58
|
+
* infra-injected: NORSK_RUNTIME_ROLE, NORSK_MATCHED_CAPABILITIES, …). */
|
|
59
|
+
parameterDefaults?: Record<string, string>;
|
|
52
60
|
/** Template-author security-group suggestions, by discovery-tag LABEL
|
|
53
61
|
* (portable across deployments; ids are not). The launch UI pre-ticks
|
|
54
62
|
* discovered groups whose label matches — a suggestion the operator
|