@ksp-gonogo/kerbcast-react 1.10.2 → 1.11.0-rc.10
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/README.md +19 -4
- package/dist/CameraFeed.d.ts +21 -0
- package/dist/CameraFeed.js +172 -33
- package/dist/CommandBrackets.d.ts +110 -0
- package/dist/CommandBrackets.js +167 -0
- package/dist/KerbalFaceFeed.js +2 -2
- package/dist/hooks/useCommandDisplayUt.d.ts +22 -0
- package/dist/hooks/useCommandDisplayUt.js +104 -0
- package/dist/hooks/useDisplayedFrameUt.d.ts +11 -0
- package/dist/hooks/useDisplayedFrameUt.js +22 -0
- package/dist/hooks/useReportDisplaySize.d.ts +49 -3
- package/dist/hooks/useReportDisplaySize.js +187 -21
- package/dist/hooks/useTargetBracketsPreference.d.ts +6 -0
- package/dist/hooks/useTargetBracketsPreference.js +39 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/package.json +2 -2
- package/src/CameraFeed.test.tsx +538 -8
- package/src/CameraFeed.tsx +520 -242
- package/src/CommandBrackets.test.tsx +259 -0
- package/src/CommandBrackets.tsx +341 -0
- package/src/KerbalFaceFeed.tsx +2 -2
- package/src/hooks/useCommandDisplayUt.test.tsx +156 -0
- package/src/hooks/useCommandDisplayUt.ts +135 -0
- package/src/hooks/useDisplayedFrameUt.ts +26 -0
- package/src/hooks/useReportDisplaySize.loop.test.tsx +290 -0
- package/src/hooks/useReportDisplaySize.test.tsx +18 -9
- package/src/hooks/useReportDisplaySize.ts +232 -23
- package/src/hooks/useTargetBracketsPreference.ts +38 -0
- package/src/index.test.ts +2 -1
- package/src/index.ts +15 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { commandReadout, } from "@ksp-gonogo/kerbcast";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import styled from "styled-components";
|
|
5
|
+
const BRACKET_STROKE_PX = 2;
|
|
6
|
+
const BRACKET_MIN_HALF_PX = 14;
|
|
7
|
+
const BRACKET_ARM_FRACTION = 0.3;
|
|
8
|
+
const BRACKET_MIN_ARM_PX = 6;
|
|
9
|
+
const READOUT_GAP_PX = 11;
|
|
10
|
+
/** Default colours; each is overridable through the named CSS custom property. */
|
|
11
|
+
export const COMMAND_BRACKET_COLOURS = {
|
|
12
|
+
clear: "var(--kerbcast-bracket-clear, #4caf50)",
|
|
13
|
+
occluded: "var(--kerbcast-bracket-occluded, #f44336)",
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Where `object-fit: contain` media actually lands inside its box. Falls back
|
|
17
|
+
* to the whole box while the media size is unknown.
|
|
18
|
+
*/
|
|
19
|
+
export function containedContentRect(boxWidth, boxHeight, mediaWidth, mediaHeight) {
|
|
20
|
+
if (mediaWidth <= 0 || mediaHeight <= 0 || boxWidth <= 0 || boxHeight <= 0) {
|
|
21
|
+
return { x: 0, y: 0, width: Math.max(0, boxWidth), height: Math.max(0, boxHeight) };
|
|
22
|
+
}
|
|
23
|
+
const scale = Math.min(boxWidth / mediaWidth, boxHeight / mediaHeight);
|
|
24
|
+
const width = mediaWidth * scale;
|
|
25
|
+
const height = mediaHeight * scale;
|
|
26
|
+
return { x: (boxWidth - width) / 2, y: (boxHeight - height) / 2, width, height };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The bracket box centred on the displayed frame. `bracketHalfPx` is in
|
|
30
|
+
* capture px of a `frameHeightPx`-tall frame, so it scales by the displayed
|
|
31
|
+
* height; a missing half-size draws the minimum box.
|
|
32
|
+
*/
|
|
33
|
+
export function bracketGeometry(rect, bracketHalfPx, frameHeightPx) {
|
|
34
|
+
const scale = frameHeightPx > 0 ? rect.height / frameHeightPx : 1;
|
|
35
|
+
const half = Math.max(BRACKET_MIN_HALF_PX, (bracketHalfPx ?? 0) * scale);
|
|
36
|
+
const arm = Math.max(BRACKET_MIN_ARM_PX, BRACKET_ARM_FRACTION * half * 2);
|
|
37
|
+
const cx = rect.x + rect.width / 2;
|
|
38
|
+
const cy = rect.y + rect.height / 2;
|
|
39
|
+
return {
|
|
40
|
+
cx,
|
|
41
|
+
cy,
|
|
42
|
+
half,
|
|
43
|
+
arm,
|
|
44
|
+
stroke: BRACKET_STROKE_PX,
|
|
45
|
+
topLeft: { x: cx - half, y: cy - half },
|
|
46
|
+
bottomRight: { x: cx + half, y: cy + half },
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function cameraCornerStyle(state) {
|
|
50
|
+
return state.occluded
|
|
51
|
+
? { colour: COMMAND_BRACKET_COLOURS.occluded, dashed: true }
|
|
52
|
+
: { colour: COMMAND_BRACKET_COLOURS.clear, dashed: false };
|
|
53
|
+
}
|
|
54
|
+
function applyCorner(base, override) {
|
|
55
|
+
return {
|
|
56
|
+
colour: override?.colour ?? base.colour,
|
|
57
|
+
dashed: override?.dashed ?? base.dashed,
|
|
58
|
+
hidden: override?.hidden ?? false,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** Tracks the rendered box and intrinsic size of a video element. */
|
|
62
|
+
function useVideoBox(video) {
|
|
63
|
+
const [box, setBox] = useState({ boxW: 0, boxH: 0, mediaW: 0, mediaH: 0 });
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (!video)
|
|
66
|
+
return;
|
|
67
|
+
const measure = () => {
|
|
68
|
+
const next = {
|
|
69
|
+
boxW: video.clientWidth,
|
|
70
|
+
boxH: video.clientHeight,
|
|
71
|
+
mediaW: video.videoWidth,
|
|
72
|
+
mediaH: video.videoHeight,
|
|
73
|
+
};
|
|
74
|
+
setBox((prev) => prev.boxW === next.boxW &&
|
|
75
|
+
prev.boxH === next.boxH &&
|
|
76
|
+
prev.mediaW === next.mediaW &&
|
|
77
|
+
prev.mediaH === next.mediaH
|
|
78
|
+
? prev
|
|
79
|
+
: next);
|
|
80
|
+
};
|
|
81
|
+
measure();
|
|
82
|
+
const observer = new ResizeObserver(measure);
|
|
83
|
+
observer.observe(video);
|
|
84
|
+
video.addEventListener("loadedmetadata", measure);
|
|
85
|
+
video.addEventListener("resize", measure);
|
|
86
|
+
return () => {
|
|
87
|
+
observer.disconnect();
|
|
88
|
+
video.removeEventListener("loadedmetadata", measure);
|
|
89
|
+
video.removeEventListener("resize", measure);
|
|
90
|
+
};
|
|
91
|
+
}, [video]);
|
|
92
|
+
return box;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Target brackets for a command camera feed: the aim point sits at frame
|
|
96
|
+
* centre, so the box is always centred on the displayed video. Both corners
|
|
97
|
+
* show the camera axis by default; an overlay can restyle either and draw
|
|
98
|
+
* its own marks. Without a fix, or over a blanked feed, the default corners
|
|
99
|
+
* are left out but the overlay and readout still run.
|
|
100
|
+
*/
|
|
101
|
+
export function CommandBrackets({ state, displayUt, utSource, video, fallbackSize, overlay, }) {
|
|
102
|
+
const { boxW, boxH, mediaW, mediaH } = useVideoBox(video);
|
|
103
|
+
const rect = containedContentRect(boxW, boxH, mediaW || fallbackSize?.width || 0, mediaH || fallbackSize?.height || 0);
|
|
104
|
+
const g = bracketGeometry(rect, state.bracketHalfPx, state.frameHeightPx || mediaH);
|
|
105
|
+
const ctx = {
|
|
106
|
+
geometry: g,
|
|
107
|
+
width: boxW,
|
|
108
|
+
height: boxH,
|
|
109
|
+
displayUt,
|
|
110
|
+
utSource: utSource ?? null,
|
|
111
|
+
state,
|
|
112
|
+
};
|
|
113
|
+
const overrides = typeof overlay?.corners === "function" ? overlay.corners(ctx) : overlay?.corners;
|
|
114
|
+
const camera = cameraCornerStyle(state);
|
|
115
|
+
const topLeft = applyCorner(camera, overrides?.topLeft);
|
|
116
|
+
const bottomRight = applyCorner(camera, overrides?.bottomRight);
|
|
117
|
+
const { x: left, y: top } = g.topLeft;
|
|
118
|
+
const { x: right, y: bottom } = g.bottomRight;
|
|
119
|
+
const blanked = !!state.blank;
|
|
120
|
+
const drawCorners = state.hasFix && !blanked;
|
|
121
|
+
const defaultReadout = blanked ? "" : commandReadout(state);
|
|
122
|
+
const readout = overlay?.readout ? overlay.readout(defaultReadout, ctx) : defaultReadout;
|
|
123
|
+
return (_jsxs(_Fragment, { children: [(drawCorners || overlay?.render) && (_jsxs(BracketSvg, { "aria-hidden": "true", "data-camera-axis": state.camera, width: boxW, height: boxH, children: [drawCorners && !topLeft.hidden && (_jsx("path", { "data-corner": "topLeft", d: `M${left} ${top + g.arm}L${left} ${top}L${left + g.arm} ${top}`, fill: "none", strokeWidth: g.stroke, strokeDasharray: topLeft.dashed ? "3 2" : undefined, style: { stroke: topLeft.colour } })), drawCorners && !bottomRight.hidden && (_jsx("path", { "data-corner": "bottomRight", d: `M${right} ${bottom - g.arm}L${right} ${bottom}L${right - g.arm} ${bottom}`, fill: "none", strokeWidth: g.stroke, strokeDasharray: bottomRight.dashed ? "3 2" : undefined, style: { stroke: bottomRight.colour } })), overlay?.render && _jsx("g", { "data-bracket-overlay": "", children: overlay.render(ctx) })] })), readout && (_jsx(Readout, { "data-ut-source": utSource ?? undefined, style: { left: g.cx, top: bottom + READOUT_GAP_PX }, children: readout }))] }));
|
|
124
|
+
}
|
|
125
|
+
/** Reason a command feed is blanked, centred on black over the feed. */
|
|
126
|
+
export function CommandBlank({ reason }) {
|
|
127
|
+
return (_jsx(BlankOverlay, { role: "status", "aria-label": "Feed blanked", children: _jsx(BlankText, { children: reason }) }));
|
|
128
|
+
}
|
|
129
|
+
const BracketSvg = styled.svg `
|
|
130
|
+
position: absolute;
|
|
131
|
+
top: 0;
|
|
132
|
+
left: 0;
|
|
133
|
+
z-index: 1;
|
|
134
|
+
overflow: visible;
|
|
135
|
+
pointer-events: none;
|
|
136
|
+
`;
|
|
137
|
+
const Readout = styled.div `
|
|
138
|
+
position: absolute;
|
|
139
|
+
z-index: 1;
|
|
140
|
+
transform: translateX(-50%);
|
|
141
|
+
white-space: nowrap;
|
|
142
|
+
color: #fff;
|
|
143
|
+
font-size: 10px;
|
|
144
|
+
font-weight: 600;
|
|
145
|
+
letter-spacing: 0.12em;
|
|
146
|
+
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.9);
|
|
147
|
+
pointer-events: none;
|
|
148
|
+
`;
|
|
149
|
+
const BlankOverlay = styled.div `
|
|
150
|
+
position: absolute;
|
|
151
|
+
inset: 0;
|
|
152
|
+
z-index: 1;
|
|
153
|
+
display: flex;
|
|
154
|
+
align-items: center;
|
|
155
|
+
justify-content: center;
|
|
156
|
+
padding: 1rem;
|
|
157
|
+
background: #000;
|
|
158
|
+
pointer-events: none;
|
|
159
|
+
`;
|
|
160
|
+
const BlankText = styled.span `
|
|
161
|
+
color: rgba(255, 255, 255, 0.85);
|
|
162
|
+
font-size: 0.85rem;
|
|
163
|
+
font-weight: 600;
|
|
164
|
+
letter-spacing: 0.12em;
|
|
165
|
+
text-align: center;
|
|
166
|
+
text-transform: uppercase;
|
|
167
|
+
`;
|
package/dist/KerbalFaceFeed.js
CHANGED
|
@@ -91,11 +91,11 @@ export function KerbalFaceFeed({ flightId, size, reportSize = true, actions, sho
|
|
|
91
91
|
// Reporting follows the mount lifecycle; the registry collapses multiple
|
|
92
92
|
// feeds of the same kerbal to one MAX report.
|
|
93
93
|
const frameRef = useRef(null);
|
|
94
|
-
|
|
94
|
+
const videoRef = useRef(null);
|
|
95
|
+
useReportDisplaySize(flightId, frameRef, { square: true, enabled: reportSize, videoRef });
|
|
95
96
|
// Bind the stream to the persistent <video> without remounting it — the
|
|
96
97
|
// element stays mounted across every re-layout / prop change, only its
|
|
97
98
|
// srcObject is (re)assigned. A stall (stream -> null) leaves the last frame.
|
|
98
|
-
const videoRef = useRef(null);
|
|
99
99
|
useEffect(() => {
|
|
100
100
|
if (videoRef.current && stream) {
|
|
101
101
|
videoRef.current.srcObject = stream;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type FrameUtSource, type CommandTrackEvent } from "@ksp-gonogo/kerbcast";
|
|
2
|
+
export { MAX_UT_EXTRAPOLATION_MS, extrapolateCaptureUt } from "@ksp-gonogo/kerbcast";
|
|
3
|
+
/** Real milliseconds until the first event after `displayUt`, or `null` when none is ahead. */
|
|
4
|
+
export declare function msUntilNextEvent(events: readonly CommandTrackEvent[] | undefined, displayUt: number, warpRate: number): number | null;
|
|
5
|
+
/** Whether an event's UT lies in `(from, to]`, i.e. moving the display from `from` to `to` changes the state in force. */
|
|
6
|
+
export declare function crossesEvent(events: readonly CommandTrackEvent[] | undefined, from: number, to: number): boolean;
|
|
7
|
+
/** The UT a command feed is showing and where it came from; `"override"` is a host's own value. */
|
|
8
|
+
export interface CommandDisplayUt {
|
|
9
|
+
ut: number;
|
|
10
|
+
source: FrameUtSource | "override";
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The UT a command feed is showing. An explicit `override` (a host with its
|
|
14
|
+
* own delayed playout) wins. Otherwise, given the feed's `flightId` and the
|
|
15
|
+
* `video` element showing it, the presented frame's own capture stamp is
|
|
16
|
+
* used (`source: "exact"`); without one, the client's capture clock is
|
|
17
|
+
* interpolated to now (`"estimated"`). Re-evaluated twice a second while
|
|
18
|
+
* `enabled` so readout ages keep counting, and again the moment the
|
|
19
|
+
* displayed UT reaches the next of `events`, so a state change lands on its
|
|
20
|
+
* frame rather than on the next tick. `null` when nothing is known.
|
|
21
|
+
*/
|
|
22
|
+
export declare function useCommandDisplayUt(enabled: boolean, override?: number, events?: readonly CommandTrackEvent[], flightId?: number | null, video?: HTMLVideoElement | null): CommandDisplayUt | null;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { useEffect, useReducer, useRef } from "react";
|
|
2
|
+
import { extrapolateCaptureUt, } from "@ksp-gonogo/kerbcast";
|
|
3
|
+
import { useKerbcastClient } from "../context";
|
|
4
|
+
import { nowMs } from "../timing";
|
|
5
|
+
export { MAX_UT_EXTRAPOLATION_MS, extrapolateCaptureUt } from "@ksp-gonogo/kerbcast";
|
|
6
|
+
const TICK_MS = 500;
|
|
7
|
+
/** Real milliseconds until the first event after `displayUt`, or `null` when none is ahead. */
|
|
8
|
+
export function msUntilNextEvent(events, displayUt, warpRate) {
|
|
9
|
+
if (!events?.length || !(warpRate > 0))
|
|
10
|
+
return null;
|
|
11
|
+
let next = Infinity;
|
|
12
|
+
for (const e of events)
|
|
13
|
+
if (e.ut > displayUt && e.ut < next)
|
|
14
|
+
next = e.ut;
|
|
15
|
+
return next === Infinity ? null : ((next - displayUt) / warpRate) * 1000;
|
|
16
|
+
}
|
|
17
|
+
/** Whether an event's UT lies in `(from, to]`, i.e. moving the display from `from` to `to` changes the state in force. */
|
|
18
|
+
export function crossesEvent(events, from, to) {
|
|
19
|
+
if (!events?.length)
|
|
20
|
+
return false;
|
|
21
|
+
const lo = Math.min(from, to);
|
|
22
|
+
const hi = Math.max(from, to);
|
|
23
|
+
return events.some((e) => e.ut > lo && e.ut <= hi);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The UT a command feed is showing. An explicit `override` (a host with its
|
|
27
|
+
* own delayed playout) wins. Otherwise, given the feed's `flightId` and the
|
|
28
|
+
* `video` element showing it, the presented frame's own capture stamp is
|
|
29
|
+
* used (`source: "exact"`); without one, the client's capture clock is
|
|
30
|
+
* interpolated to now (`"estimated"`). Re-evaluated twice a second while
|
|
31
|
+
* `enabled` so readout ages keep counting, and again the moment the
|
|
32
|
+
* displayed UT reaches the next of `events`, so a state change lands on its
|
|
33
|
+
* frame rather than on the next tick. `null` when nothing is known.
|
|
34
|
+
*/
|
|
35
|
+
export function useCommandDisplayUt(enabled, override, events, flightId, video) {
|
|
36
|
+
const client = useKerbcastClient();
|
|
37
|
+
const sample = useRef({
|
|
38
|
+
captureUt: client.clock.captureUt,
|
|
39
|
+
warpRate: client.clock.warpRate,
|
|
40
|
+
at: nowMs(),
|
|
41
|
+
});
|
|
42
|
+
const frame = useRef(null);
|
|
43
|
+
const shown = useRef(null);
|
|
44
|
+
const eventsRef = useRef(events);
|
|
45
|
+
eventsRef.current = events;
|
|
46
|
+
const [, tick] = useReducer((c) => c + 1, 0);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const take = () => {
|
|
49
|
+
const { captureUt, warpRate } = client.clock;
|
|
50
|
+
sample.current = { captureUt, warpRate, at: nowMs() };
|
|
51
|
+
};
|
|
52
|
+
take();
|
|
53
|
+
return client.on("settings-change", take);
|
|
54
|
+
}, [client]);
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!enabled)
|
|
57
|
+
return;
|
|
58
|
+
const interval = setInterval(tick, TICK_MS);
|
|
59
|
+
return () => clearInterval(interval);
|
|
60
|
+
}, [enabled]);
|
|
61
|
+
/* Exact frames re-render only when they change what is drawn: a switch of
|
|
62
|
+
source, or a step across an event. Ages catch up on the tick. */
|
|
63
|
+
const overriding = override !== undefined;
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
frame.current = null;
|
|
66
|
+
if (!enabled || overriding || flightId == null || !video)
|
|
67
|
+
return;
|
|
68
|
+
return client.watchDisplayedFrames(flightId, video, (f) => {
|
|
69
|
+
frame.current = f.source === "exact" ? f : null;
|
|
70
|
+
const prev = shown.current;
|
|
71
|
+
if (!frame.current) {
|
|
72
|
+
if (prev?.source === "exact")
|
|
73
|
+
tick();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (prev === null ||
|
|
77
|
+
prev.source !== "exact" ||
|
|
78
|
+
crossesEvent(eventsRef.current, prev.ut, f.ut)) {
|
|
79
|
+
tick();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}, [client, enabled, overriding, flightId, video]);
|
|
83
|
+
const { captureUt, warpRate, at } = sample.current;
|
|
84
|
+
const exact = frame.current;
|
|
85
|
+
const displayUt = overriding
|
|
86
|
+
? { ut: override, source: "override" }
|
|
87
|
+
: exact
|
|
88
|
+
? { ut: exact.ut, source: "exact" }
|
|
89
|
+
: captureUt === null
|
|
90
|
+
? null
|
|
91
|
+
: { ut: extrapolateCaptureUt(captureUt, warpRate, at, nowMs()), source: "estimated" };
|
|
92
|
+
shown.current = displayUt;
|
|
93
|
+
const untilEdge = enabled && displayUt?.source === "estimated"
|
|
94
|
+
? msUntilNextEvent(events, displayUt.ut, warpRate)
|
|
95
|
+
: null;
|
|
96
|
+
/* Edges further out than a tick are caught by a later render's timer. */
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (untilEdge === null || untilEdge > TICK_MS)
|
|
99
|
+
return;
|
|
100
|
+
const timer = setTimeout(tick, Math.max(0, untilEdge) + 1);
|
|
101
|
+
return () => clearTimeout(timer);
|
|
102
|
+
});
|
|
103
|
+
return displayUt;
|
|
104
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DisplayedFrameUt } from "@ksp-gonogo/kerbcast";
|
|
2
|
+
/**
|
|
3
|
+
* Mission time of the frame `video` is showing for camera `flightId`,
|
|
4
|
+
* updated on every presented frame (so the calling component re-renders at
|
|
5
|
+
* the video's frame rate). `source` says whether the UT is the frame's own
|
|
6
|
+
* capture stamp (`"exact"`) or the capture clock extrapolated
|
|
7
|
+
* (`"estimated"`). `null` until the first frame with a known UT, and while
|
|
8
|
+
* either argument is null. For per-frame work without re-rendering, use
|
|
9
|
+
* `client.watchDisplayedFrames` directly.
|
|
10
|
+
*/
|
|
11
|
+
export declare function useDisplayedFrameUt(flightId: number | null, video: HTMLVideoElement | null): DisplayedFrameUt | null;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { useKerbcastClient } from "../context";
|
|
3
|
+
/**
|
|
4
|
+
* Mission time of the frame `video` is showing for camera `flightId`,
|
|
5
|
+
* updated on every presented frame (so the calling component re-renders at
|
|
6
|
+
* the video's frame rate). `source` says whether the UT is the frame's own
|
|
7
|
+
* capture stamp (`"exact"`) or the capture clock extrapolated
|
|
8
|
+
* (`"estimated"`). `null` until the first frame with a known UT, and while
|
|
9
|
+
* either argument is null. For per-frame work without re-rendering, use
|
|
10
|
+
* `client.watchDisplayedFrames` directly.
|
|
11
|
+
*/
|
|
12
|
+
export function useDisplayedFrameUt(flightId, video) {
|
|
13
|
+
const client = useKerbcastClient();
|
|
14
|
+
const [frame, setFrame] = useState(null);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
setFrame(null);
|
|
17
|
+
if (flightId === null || !video)
|
|
18
|
+
return;
|
|
19
|
+
return client.watchDisplayedFrames(flightId, video, setFrame);
|
|
20
|
+
}, [client, flightId, video]);
|
|
21
|
+
return frame;
|
|
22
|
+
}
|
|
@@ -1,3 +1,31 @@
|
|
|
1
|
+
/** Shape of a camera whose configured capture size is not known. */
|
|
2
|
+
export declare const DEFAULT_FEED_ASPECT: number;
|
|
3
|
+
/**
|
|
4
|
+
* Report size for a `width` x `height` box: the `aspect` picture that fits it,
|
|
5
|
+
* width bucketed. Never the box's own shape.
|
|
6
|
+
*/
|
|
7
|
+
export declare function displayReportSize(width: number, height: number, aspect?: number): {
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
};
|
|
11
|
+
export interface Size {
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
}
|
|
15
|
+
/** The stream changing from `from` (null when nothing was known before) to `to`. */
|
|
16
|
+
export interface StreamChange {
|
|
17
|
+
from: Size | null;
|
|
18
|
+
to: Size;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Whether a box going from `prev` to `box` is explained by `change`: the new
|
|
22
|
+
* box has the new stream's aspect, and it grew or shrank no more than the
|
|
23
|
+
* stream's own change in size or aspect accounts for. A layout that fits the
|
|
24
|
+
* stream into a fixed tile changes with the aspect; one that shows the video
|
|
25
|
+
* at its own size changes with the size. A box that jumps further (entering
|
|
26
|
+
* fullscreen at the same aspect) is a layout change.
|
|
27
|
+
*/
|
|
28
|
+
export declare function followsStream(prev: Size, box: Size, change: StreamChange): boolean;
|
|
1
29
|
export interface ReportDisplaySizeOptions {
|
|
2
30
|
/**
|
|
3
31
|
* Report while mounted. Default true. Set false for a fixed-resolution feed
|
|
@@ -6,10 +34,25 @@ export interface ReportDisplaySizeOptions {
|
|
|
6
34
|
*/
|
|
7
35
|
enabled?: boolean;
|
|
8
36
|
/**
|
|
9
|
-
* Report a square (side x side, the min edge) rather than
|
|
10
|
-
* Face cams are square; part cams are not.
|
|
37
|
+
* Report a square (side x side, the min edge) rather than a picture at
|
|
38
|
+
* `aspect`. Face cams are square; part cams are not.
|
|
11
39
|
*/
|
|
12
40
|
square?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Width over height of the picture to ask for, normally the camera's
|
|
43
|
+
* configured capture size. Default 16:9. Ignored when `square` is set.
|
|
44
|
+
*/
|
|
45
|
+
aspect?: number;
|
|
46
|
+
/**
|
|
47
|
+
* The `<video>` playing this camera. Its resolution changes are watched so a
|
|
48
|
+
* box resize that only follows the stream is not reported back.
|
|
49
|
+
*/
|
|
50
|
+
videoRef?: React.RefObject<HTMLVideoElement | null>;
|
|
51
|
+
/**
|
|
52
|
+
* The size the sidecar says it is rendering this camera at, for the same
|
|
53
|
+
* purpose: a layout may size itself from this rather than from the video.
|
|
54
|
+
*/
|
|
55
|
+
streamSize?: Size | null;
|
|
13
56
|
}
|
|
14
57
|
/**
|
|
15
58
|
* Self-measure a feed element and drive the client's per-consumer
|
|
@@ -20,5 +63,8 @@ export interface ReportDisplaySizeOptions {
|
|
|
20
63
|
* mount lifecycle: on unmount / `flightId` change / `enabled` flip it
|
|
21
64
|
* deregisters, and the sidecar's clear-on-departure (unsubscribe) does the
|
|
22
65
|
* server-side clear.
|
|
66
|
+
*
|
|
67
|
+
* A box resize that follows a change in the incoming stream is not reported,
|
|
68
|
+
* so a layout sized from the stream cannot feed it back into its own request.
|
|
23
69
|
*/
|
|
24
|
-
export declare function useReportDisplaySize(flightId: number | null, ref: React.RefObject<Element | null>, { enabled, square }?: ReportDisplaySizeOptions): void;
|
|
70
|
+
export declare function useReportDisplaySize(flightId: number | null, ref: React.RefObject<Element | null>, { enabled, square, aspect, videoRef, streamSize, }?: ReportDisplaySizeOptions): void;
|