@ksp-gonogo/kerbcast-react 1.6.2 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,91 @@
1
+ import { useEffect, useId } from "react";
2
+ import { useKerbcastDisplaySizes } from "../context";
3
+
4
+ /*
5
+ * Debounce + bucket for self-measured display-size reporting. The first
6
+ * measurement after mount reports promptly (so a feed never sits at a stale
7
+ * resolution until it is resized); later churn is debounced. Sizes are ceiled
8
+ * to a BUCKET_PX multiple: that meets-the-minimum-need (never under-serve),
9
+ * yields an even value (H.264-friendly), and gives a material-change threshold
10
+ * so a feed re-reports only when a resize crosses a bucket boundary.
11
+ */
12
+ const DEBOUNCE_MS = 200;
13
+ const BUCKET_PX = 16;
14
+
15
+ function bucket(px: number): number {
16
+ const b = Math.ceil(px / BUCKET_PX) * BUCKET_PX;
17
+ return b < BUCKET_PX ? BUCKET_PX : b;
18
+ }
19
+
20
+ export interface ReportDisplaySizeOptions {
21
+ /**
22
+ * Report while mounted. Default true. Set false for a fixed-resolution feed
23
+ * that should not drive auto-resolution. This is a RESOLUTION concern only,
24
+ * independent of whether the feed shows its action UI.
25
+ */
26
+ enabled?: boolean;
27
+ /**
28
+ * Report a square (side x side, the min edge) rather than the raw w x h.
29
+ * Face cams are square; part cams are not.
30
+ */
31
+ square?: boolean;
32
+ }
33
+
34
+ /**
35
+ * Self-measure a feed element and drive the client's per-consumer
36
+ * `reportDisplaySize` (via the context display-size registry, which collapses
37
+ * multiple feeds of the same camera to one MAX report). Attach `ref` to the
38
+ * element whose rendered pixel box is the display size. Reporting follows the
39
+ * mount lifecycle: on unmount / `flightId` change / `enabled` flip it
40
+ * deregisters, and the sidecar's clear-on-departure (unsubscribe) does the
41
+ * server-side clear.
42
+ */
43
+ export function useReportDisplaySize(
44
+ flightId: number | null,
45
+ ref: React.RefObject<Element | null>,
46
+ { enabled = true, square = false }: ReportDisplaySizeOptions = {},
47
+ ): void {
48
+ const displaySizes = useKerbcastDisplaySizes();
49
+ const instanceId = useId();
50
+
51
+ useEffect(() => {
52
+ if (flightId === null || !enabled) return;
53
+ const el = ref.current;
54
+ if (!el) return;
55
+
56
+ let timer: ReturnType<typeof setTimeout> | null = null;
57
+ let lastW = -1;
58
+ let lastH = -1;
59
+ let firstReported = false;
60
+
61
+ const report = (width: number, height: number) => {
62
+ const side = square ? Math.min(width, height) : 0;
63
+ const bw = bucket(square ? side : width);
64
+ const bh = bucket(square ? side : height);
65
+ if (bw === lastW && bh === lastH) return;
66
+ lastW = bw;
67
+ lastH = bh;
68
+ displaySizes.report(flightId, instanceId, bw, bh);
69
+ };
70
+
71
+ const observer = new ResizeObserver((entries) => {
72
+ const entry = entries[0];
73
+ if (!entry) return;
74
+ const { width, height } = entry.contentRect;
75
+ if (!firstReported) {
76
+ firstReported = true;
77
+ report(width, height);
78
+ return;
79
+ }
80
+ if (timer !== null) clearTimeout(timer);
81
+ timer = setTimeout(() => report(width, height), DEBOUNCE_MS);
82
+ });
83
+ observer.observe(el);
84
+
85
+ return () => {
86
+ observer.disconnect();
87
+ if (timer !== null) clearTimeout(timer);
88
+ displaySizes.clear(flightId, instanceId);
89
+ };
90
+ }, [displaySizes, flightId, ref, enabled, square, instanceId]);
91
+ }
package/src/index.ts CHANGED
@@ -10,16 +10,23 @@
10
10
  export {
11
11
  KerbcastProvider,
12
12
  createClientSubscriptions,
13
+ createClientDisplaySizes,
13
14
  useKerbcastClient,
14
15
  useKerbcastSubscriptions,
16
+ useKerbcastDisplaySizes,
17
+ } from "./context";
18
+ export type {
19
+ KerbcastProviderProps,
20
+ KerbcastSubscriptions,
21
+ KerbcastDisplaySizes,
15
22
  } from "./context";
16
- export type { KerbcastProviderProps, KerbcastSubscriptions } from "./context";
17
23
 
18
24
  // Hooks
19
25
  export { useKerbcastCameras } from "./hooks/useKerbcastCameras";
20
26
  export { useKerbcastStream } from "./hooks/useKerbcastStream";
21
27
  export { useKerbcastClock } from "./hooks/useKerbcastClock";
22
28
  export { useKerbcastInFlight } from "./hooks/useKerbcastInFlight";
29
+ export { useReportDisplaySize } from "./hooks/useReportDisplaySize";
23
30
 
24
31
  // Shared standby glyph (used by CameraFeed + downstream dashboards)
25
32
  export { StandbyIcon } from "./StandbyIcon";
@@ -40,3 +47,7 @@ export type {
40
47
  CameraStreamHook,
41
48
  FeedAction,
42
49
  } from "./CameraFeed";
50
+
51
+ // KerbalFaceFeed — shared single-kerbal-face primitive
52
+ export { KerbalFaceFeed } from "./KerbalFaceFeed";
53
+ export type { KerbalFaceFeedProps, KerbalFeedState } from "./KerbalFaceFeed";