@camstack/ui-library 1.1.16 → 1.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.
@@ -2,6 +2,9 @@ export { cn } from './cn';
2
2
  export { formatLastSeen } from './format-last-seen';
3
3
  export { formatControlDateTime } from './format-control-datetime';
4
4
  export type { ControlDateTimeFormat } from './format-control-datetime';
5
+ export { formatNumeric } from './format-numeric';
6
+ export { resolveSensorDisplay } from './resolve-sensor-display';
7
+ export type { SensorDisplayInput, ResolvedSensorDisplay } from './resolve-sensor-display';
5
8
  export { isAbsentProvider } from './cap-error';
6
9
  export * from './responsive';
7
10
  export { mirror } from './pipeline-mirror';
@@ -10,3 +13,7 @@ export { PHASE_CONFIG, getPhaseVisual } from './phase-config';
10
13
  export type { PhaseVisual } from './phase-config';
11
14
  export { createSharedContext } from './shared-context';
12
15
  export { ensureMfHostInit } from './mf-runtime-init';
16
+ export { serializeRecordedCommand, parseRecordedServerMessage, RECORDED_PLAYBACK_MODES, } from './recorded-control-protocol';
17
+ export type { RecordedControlCommand, RecordedPlaybackMode, RecordedServerMessage, } from './recorded-control-protocol';
18
+ export { scrubReducer, initialScrubState, shouldEmit, shouldCommit, cursorFractionFor, makeScrubBridge, useScrubController, COMMIT_DEDUPE_TOLERANCE_MS, COMMIT_DEDUPE_WINDOW_MS, } from './scrub-controller';
19
+ export type { ScrubSource, ScrubState, ScrubAction, ScrubController, PlaybackControlSink, LastCommit, } from './scrub-controller';
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Client-side wire codec for the `camstack-control` RTCDataChannel (R9).
3
+ *
4
+ * Mirrors the broker's `control-protocol.ts`
5
+ * (packages/addon-pipeline/src/stream-broker/recorded/control-protocol.ts):
6
+ * client→server commands drive the per-viewer `TimelineSession` /
7
+ * `RecordedFeeder` (frame-push recorded playback on the SAME WebRTC session
8
+ * as live video); server→client messages report playback state + a ~1 Hz
9
+ * position. Shapes are re-declared here (not imported) because ui-library
10
+ * cannot depend on an addon package — the embed SPA does the same.
11
+ *
12
+ * Parsing is defensive: a malformed frame returns `null`, never throws, so a
13
+ * bad datachannel message can't take down the player.
14
+ */
15
+ export type RecordedControlCommand = {
16
+ readonly t: 'playRecorded';
17
+ readonly deviceId: number;
18
+ readonly epoch: number;
19
+ readonly profile: string;
20
+ } | {
21
+ readonly t: 'seek';
22
+ readonly epoch: number;
23
+ } | {
24
+ readonly t: 'scrubCommit';
25
+ readonly epoch: number;
26
+ } | {
27
+ readonly t: 'setRate';
28
+ readonly rate: 0 | 1;
29
+ } | {
30
+ readonly t: 'goLive';
31
+ } | {
32
+ readonly t: 'setProfile';
33
+ readonly profile: string;
34
+ };
35
+ /** Serialize a command for `RTCDataChannel.send` (matches the server schema). */
36
+ export declare function serializeRecordedCommand(cmd: RecordedControlCommand): string;
37
+ /**
38
+ * Playback mode reported by the server session. `live` = the shared live feed
39
+ * owns the track; every other value describes the recorded feeder.
40
+ */
41
+ export declare const RECORDED_PLAYBACK_MODES: readonly ["live", "loading", "playing", "paused", "gap", "ended", "error"];
42
+ export type RecordedPlaybackMode = (typeof RECORDED_PLAYBACK_MODES)[number];
43
+ export type RecordedServerMessage = {
44
+ readonly kind: 'state';
45
+ readonly state: RecordedPlaybackMode;
46
+ } | {
47
+ readonly kind: 'position';
48
+ readonly epochMs: number;
49
+ };
50
+ /**
51
+ * Parse a raw server→client control frame. Returns `null` on any malformed
52
+ * payload (bad JSON, unknown discriminator, wrong field types).
53
+ */
54
+ export declare function parseRecordedServerMessage(raw: string): RecordedServerMessage | null;
@@ -0,0 +1,25 @@
1
+ import { DeviceDisplayOverride, RoleDisplayDefault } from '@camstack/types';
2
+ export interface SensorDisplayInput {
3
+ /** Per-device override from the row (`DeviceInfo.display`). */
4
+ readonly display?: DeviceDisplayOverride;
5
+ /** Cap name for `perCap` refinement (e.g. 'numeric-sensor'); undefined skips it. */
6
+ readonly capName?: string;
7
+ /** Role default (`roleDisplayDefaults[device.role]`). */
8
+ readonly roleDefault?: RoleDisplayDefault;
9
+ /** Live slice unit (the unit the raw VALUE is actually in, for generic sensors). */
10
+ readonly sliceUnit?: string;
11
+ /** Live slice precision. */
12
+ readonly slicePrecision?: number;
13
+ /** ROLE_DESCRIPTOR canonical unit fallback (typed roles). */
14
+ readonly canonicalUnit?: string;
15
+ }
16
+ export interface ResolvedSensorDisplay {
17
+ /** Display unit — `''` when none resolves anywhere. */
18
+ readonly unit: string;
19
+ /** Display precision (int 0-10) or undefined for auto-format. */
20
+ readonly precision: number | undefined;
21
+ /** Convert a raw slice value into the display unit. Identity unless an
22
+ * override unit differs from the source unit AND both share a dimension. */
23
+ readonly toDisplayValue: (raw: number) => number;
24
+ }
25
+ export declare function resolveSensorDisplay(input: SensorDisplayInput): ResolvedSensorDisplay;
@@ -0,0 +1,115 @@
1
+ export type ScrubSource = 'timeline' | 'heatmap';
2
+ export interface ScrubState {
3
+ readonly scrubbing: boolean;
4
+ readonly scrubEpoch: number | null;
5
+ readonly playheadEpoch: number | null;
6
+ readonly lastSource: ScrubSource | null;
7
+ }
8
+ export declare const initialScrubState: ScrubState;
9
+ export type ScrubAction = {
10
+ readonly type: 'begin';
11
+ readonly source: ScrubSource;
12
+ } | {
13
+ readonly type: 'update';
14
+ readonly epoch: number;
15
+ readonly source: ScrubSource;
16
+ } | {
17
+ readonly type: 'end';
18
+ readonly epoch: number;
19
+ readonly source: ScrubSource;
20
+ } | {
21
+ readonly type: 'seek';
22
+ readonly epoch: number;
23
+ readonly source: ScrubSource;
24
+ } | {
25
+ readonly type: 'playhead';
26
+ readonly epoch: number;
27
+ } | {
28
+ readonly type: 'reset';
29
+ };
30
+ export declare function scrubReducer(state: ScrubState, action: ScrubAction): ScrubState;
31
+ /**
32
+ * Throttle decision for continuous `update` emits (default ~70 ms).
33
+ * Returns true when enough time has elapsed since the last emit.
34
+ */
35
+ export declare function shouldEmit(lastEmitMs: number, now: number, throttleMs?: number): boolean;
36
+ /** The last committed target (epoch + wall-clock of the commit). */
37
+ export interface LastCommit {
38
+ readonly epoch: number;
39
+ readonly at: number;
40
+ }
41
+ /** Epochs within this many ms of the last commit count as the SAME target. */
42
+ export declare const COMMIT_DEDUPE_TOLERANCE_MS = 750;
43
+ /** Only dedupe against a commit made within this recent window. */
44
+ export declare const COMMIT_DEDUPE_WINDOW_MS = 2000;
45
+ /**
46
+ * Decide whether a `seek`/`scrubCommit` should actually hit the playback sink.
47
+ * Each commit reloads the server feeder (loading→playing) and visibly stutters
48
+ * playback; firing the same (or a near-identical) target twice in quick
49
+ * succession produces a redundant reload. Drop a commit only when it targets
50
+ * essentially the SAME epoch as the last commit AND that commit was recent — a
51
+ * genuinely different target, or a re-seek to the same spot after the window,
52
+ * always commits.
53
+ */
54
+ export declare function shouldCommit(last: LastCommit | null, epoch: number, now: number, toleranceMs?: number, windowMs?: number): boolean;
55
+ /**
56
+ * The active cursor position (scrub target while scrubbing, else playhead)
57
+ * expressed as a 0..1 fraction across the day window.
58
+ * Returns null when no position is known or the day window is degenerate.
59
+ */
60
+ export declare function cursorFractionFor(state: ScrubState, day: {
61
+ readonly from: number;
62
+ readonly to: number;
63
+ }): number | null;
64
+ /**
65
+ * Generic, always-active playback control surface the scrub controller
66
+ * drives. `scrubStart`/`scrubTo` are CLIENT-SIDE hooks (the wire protocol has
67
+ * no per-move scrub message — a future WebCodecs preview consumes them);
68
+ * `scrubCommit`/`seek` reach the server feeder.
69
+ */
70
+ export interface PlaybackControlSink {
71
+ scrubStart(): void;
72
+ scrubTo(epochMs: number): void;
73
+ scrubCommit(epochMs: number): void;
74
+ seek(epochMs: number): void;
75
+ }
76
+ /** A mutable holder, structurally compatible with React's `MutableRefObject`. */
77
+ export interface MutableRef<T> {
78
+ current: T;
79
+ }
80
+ /** The gesture callbacks the hook exposes (everything except `state`). */
81
+ type ScrubBridge = Omit<ScrubController, 'state'>;
82
+ /**
83
+ * Builds the always-active bridge callbacks from a sink, a dispatch, and the
84
+ * throttle ref. Pure (no React) so it can be unit-tested directly: the hook
85
+ * just wraps the result in `useMemo`.
86
+ *
87
+ * Every gesture drives the sink immediately — there is no VOD-active gate.
88
+ */
89
+ export declare function makeScrubBridge(sink: PlaybackControlSink, dispatch: (action: ScrubAction) => void, lastEmitRef: MutableRef<number>, lastCommitRef?: MutableRef<LastCommit | null>,
90
+ /** Reports whether the PLAYER is currently on the live edge (not in recorded
91
+ * mode). Used to bypass commit-dedupe on the live→recorded transition. */
92
+ isPlayerLive?: () => boolean): ScrubBridge;
93
+ export interface ScrubController {
94
+ readonly state: ScrubState;
95
+ readonly beginScrub: (source: ScrubSource) => void;
96
+ readonly updateScrub: (epoch: number, source: ScrubSource) => void;
97
+ readonly endScrub: (epoch: number, source: ScrubSource) => void;
98
+ readonly seek: (epoch: number, source: ScrubSource) => void;
99
+ readonly setPlayhead: (epoch: number) => void;
100
+ readonly reset: () => void;
101
+ }
102
+ /**
103
+ * Owns the shared scrub position and bridges it to a playback control sink.
104
+ *
105
+ * The bridge is always active: any gesture (drag or tap) drives the sink
106
+ * immediately. There is no VOD-active gate — the timeline always drives
107
+ * playback while frame-push mode is on.
108
+ *
109
+ * @param sink Always-active playback control surface (e.g. the datachannel)
110
+ * @param isPlayerLive Synchronous getter for "is the player on the live
111
+ * edge?" — lets a live→recorded commit bypass dedupe so the seek always
112
+ * reaches the server.
113
+ */
114
+ export declare function useScrubController(sink: PlaybackControlSink, isPlayerLive?: () => boolean): ScrubController;
115
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/ui-library",
3
- "version": "1.1.16",
3
+ "version": "1.1.18",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",