@camstack/ui-library 1.1.16 → 1.1.17
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/dist/composites/cap-settings/CoverageTrack.d.ts +12 -1
- package/dist/contexts/recorded-playback.d.ts +23 -0
- package/dist/index.cjs +514 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.js +502 -9
- package/dist/lib/index.d.ts +4 -0
- package/dist/lib/recorded-control-protocol.d.ts +54 -0
- package/dist/lib/scrub-controller.d.ts +115 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -7710,6 +7710,240 @@ function ensureMfHostInit() {
|
|
|
7710
7710
|
});
|
|
7711
7711
|
}
|
|
7712
7712
|
//#endregion
|
|
7713
|
+
//#region src/lib/recorded-control-protocol.ts
|
|
7714
|
+
/** Serialize a command for `RTCDataChannel.send` (matches the server schema). */
|
|
7715
|
+
function serializeRecordedCommand(cmd) {
|
|
7716
|
+
return JSON.stringify(cmd);
|
|
7717
|
+
}
|
|
7718
|
+
/**
|
|
7719
|
+
* Playback mode reported by the server session. `live` = the shared live feed
|
|
7720
|
+
* owns the track; every other value describes the recorded feeder.
|
|
7721
|
+
*/
|
|
7722
|
+
var RECORDED_PLAYBACK_MODES = [
|
|
7723
|
+
"live",
|
|
7724
|
+
"loading",
|
|
7725
|
+
"playing",
|
|
7726
|
+
"paused",
|
|
7727
|
+
"gap",
|
|
7728
|
+
"ended",
|
|
7729
|
+
"error"
|
|
7730
|
+
];
|
|
7731
|
+
function isRecordedPlaybackMode(value) {
|
|
7732
|
+
return typeof value === "string" && RECORDED_PLAYBACK_MODES.includes(value);
|
|
7733
|
+
}
|
|
7734
|
+
/**
|
|
7735
|
+
* Parse a raw server→client control frame. Returns `null` on any malformed
|
|
7736
|
+
* payload (bad JSON, unknown discriminator, wrong field types).
|
|
7737
|
+
*/
|
|
7738
|
+
function parseRecordedServerMessage(raw) {
|
|
7739
|
+
let parsed;
|
|
7740
|
+
try {
|
|
7741
|
+
parsed = JSON.parse(raw);
|
|
7742
|
+
} catch {
|
|
7743
|
+
return null;
|
|
7744
|
+
}
|
|
7745
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
7746
|
+
const obj = { ...parsed };
|
|
7747
|
+
if (obj.t === "state" && isRecordedPlaybackMode(obj.state)) return {
|
|
7748
|
+
kind: "state",
|
|
7749
|
+
state: obj.state
|
|
7750
|
+
};
|
|
7751
|
+
if (obj.t === "position" && typeof obj.epochMs === "number") return {
|
|
7752
|
+
kind: "position",
|
|
7753
|
+
epochMs: obj.epochMs
|
|
7754
|
+
};
|
|
7755
|
+
return null;
|
|
7756
|
+
}
|
|
7757
|
+
//#endregion
|
|
7758
|
+
//#region src/lib/scrub-controller.ts
|
|
7759
|
+
/**
|
|
7760
|
+
* scrub-controller — shared scrub-position state for timeline surfaces (R9).
|
|
7761
|
+
*
|
|
7762
|
+
* Ported from the proven srcV2 viewer implementation
|
|
7763
|
+
* (camstack/srcV2/recording/scrub-controller.ts): the same throttle,
|
|
7764
|
+
* commit-dedupe, and live→recorded-bypass semantics, now driving the
|
|
7765
|
+
* admin-ui `RecordingTimeline` through the `camstack-control` datachannel.
|
|
7766
|
+
*
|
|
7767
|
+
* Two layers:
|
|
7768
|
+
* 1. Pure reducer + helpers (unit-tested, no React deps):
|
|
7769
|
+
* scrubReducer, shouldEmit, shouldCommit, cursorFractionFor,
|
|
7770
|
+
* makeScrubBridge
|
|
7771
|
+
* 2. useScrubController hook: thin React glue that bridges the reducer
|
|
7772
|
+
* to a PlaybackControlSink (e.g. the recorded-playback datachannel).
|
|
7773
|
+
*/
|
|
7774
|
+
var initialScrubState = {
|
|
7775
|
+
scrubbing: false,
|
|
7776
|
+
scrubEpoch: null,
|
|
7777
|
+
playheadEpoch: null,
|
|
7778
|
+
lastSource: null
|
|
7779
|
+
};
|
|
7780
|
+
function scrubReducer(state, action) {
|
|
7781
|
+
switch (action.type) {
|
|
7782
|
+
case "begin": return {
|
|
7783
|
+
...state,
|
|
7784
|
+
scrubbing: true,
|
|
7785
|
+
scrubEpoch: null,
|
|
7786
|
+
lastSource: action.source
|
|
7787
|
+
};
|
|
7788
|
+
case "update": return {
|
|
7789
|
+
...state,
|
|
7790
|
+
scrubbing: true,
|
|
7791
|
+
scrubEpoch: action.epoch,
|
|
7792
|
+
lastSource: action.source
|
|
7793
|
+
};
|
|
7794
|
+
case "end": return {
|
|
7795
|
+
...state,
|
|
7796
|
+
scrubbing: false,
|
|
7797
|
+
scrubEpoch: null,
|
|
7798
|
+
playheadEpoch: action.epoch,
|
|
7799
|
+
lastSource: action.source
|
|
7800
|
+
};
|
|
7801
|
+
case "seek": return {
|
|
7802
|
+
...state,
|
|
7803
|
+
scrubbing: false,
|
|
7804
|
+
scrubEpoch: null,
|
|
7805
|
+
playheadEpoch: action.epoch,
|
|
7806
|
+
lastSource: action.source
|
|
7807
|
+
};
|
|
7808
|
+
case "playhead":
|
|
7809
|
+
if (state.scrubbing) return state;
|
|
7810
|
+
return {
|
|
7811
|
+
...state,
|
|
7812
|
+
playheadEpoch: action.epoch
|
|
7813
|
+
};
|
|
7814
|
+
case "reset": return initialScrubState;
|
|
7815
|
+
default: return state;
|
|
7816
|
+
}
|
|
7817
|
+
}
|
|
7818
|
+
/**
|
|
7819
|
+
* Throttle decision for continuous `update` emits (default ~70 ms).
|
|
7820
|
+
* Returns true when enough time has elapsed since the last emit.
|
|
7821
|
+
*/
|
|
7822
|
+
function shouldEmit(lastEmitMs, now, throttleMs = 70) {
|
|
7823
|
+
return now - lastEmitMs >= throttleMs;
|
|
7824
|
+
}
|
|
7825
|
+
/** Epochs within this many ms of the last commit count as the SAME target. */
|
|
7826
|
+
var COMMIT_DEDUPE_TOLERANCE_MS = 750;
|
|
7827
|
+
/** Only dedupe against a commit made within this recent window. */
|
|
7828
|
+
var COMMIT_DEDUPE_WINDOW_MS = 2e3;
|
|
7829
|
+
/**
|
|
7830
|
+
* Decide whether a `seek`/`scrubCommit` should actually hit the playback sink.
|
|
7831
|
+
* Each commit reloads the server feeder (loading→playing) and visibly stutters
|
|
7832
|
+
* playback; firing the same (or a near-identical) target twice in quick
|
|
7833
|
+
* succession produces a redundant reload. Drop a commit only when it targets
|
|
7834
|
+
* essentially the SAME epoch as the last commit AND that commit was recent — a
|
|
7835
|
+
* genuinely different target, or a re-seek to the same spot after the window,
|
|
7836
|
+
* always commits.
|
|
7837
|
+
*/
|
|
7838
|
+
function shouldCommit(last, epoch, now, toleranceMs = 750, windowMs = COMMIT_DEDUPE_WINDOW_MS) {
|
|
7839
|
+
if (last === null) return true;
|
|
7840
|
+
const sameTarget = Math.abs(epoch - last.epoch) <= toleranceMs;
|
|
7841
|
+
const recent = now - last.at <= windowMs;
|
|
7842
|
+
return !(sameTarget && recent);
|
|
7843
|
+
}
|
|
7844
|
+
/**
|
|
7845
|
+
* The active cursor position (scrub target while scrubbing, else playhead)
|
|
7846
|
+
* expressed as a 0..1 fraction across the day window.
|
|
7847
|
+
* Returns null when no position is known or the day window is degenerate.
|
|
7848
|
+
*/
|
|
7849
|
+
function cursorFractionFor(state, day) {
|
|
7850
|
+
const epoch = state.scrubbing ? state.scrubEpoch : state.playheadEpoch;
|
|
7851
|
+
if (epoch === null) return null;
|
|
7852
|
+
const span = day.to - day.from;
|
|
7853
|
+
if (span <= 0) return null;
|
|
7854
|
+
return Math.min(1, Math.max(0, (epoch - day.from) / span));
|
|
7855
|
+
}
|
|
7856
|
+
/**
|
|
7857
|
+
* Builds the always-active bridge callbacks from a sink, a dispatch, and the
|
|
7858
|
+
* throttle ref. Pure (no React) so it can be unit-tested directly: the hook
|
|
7859
|
+
* just wraps the result in `useMemo`.
|
|
7860
|
+
*
|
|
7861
|
+
* Every gesture drives the sink immediately — there is no VOD-active gate.
|
|
7862
|
+
*/
|
|
7863
|
+
function makeScrubBridge(sink, dispatch, lastEmitRef, lastCommitRef = { current: null }, isPlayerLive = () => false) {
|
|
7864
|
+
const commit = (epoch, fire) => {
|
|
7865
|
+
const now = Date.now();
|
|
7866
|
+
if (isPlayerLive()) {
|
|
7867
|
+
lastCommitRef.current = {
|
|
7868
|
+
epoch,
|
|
7869
|
+
at: now
|
|
7870
|
+
};
|
|
7871
|
+
fire(epoch);
|
|
7872
|
+
return;
|
|
7873
|
+
}
|
|
7874
|
+
if (!shouldCommit(lastCommitRef.current, epoch, now)) return;
|
|
7875
|
+
lastCommitRef.current = {
|
|
7876
|
+
epoch,
|
|
7877
|
+
at: now
|
|
7878
|
+
};
|
|
7879
|
+
fire(epoch);
|
|
7880
|
+
};
|
|
7881
|
+
return {
|
|
7882
|
+
beginScrub: (source) => {
|
|
7883
|
+
dispatch({
|
|
7884
|
+
type: "begin",
|
|
7885
|
+
source
|
|
7886
|
+
});
|
|
7887
|
+
sink.scrubStart();
|
|
7888
|
+
},
|
|
7889
|
+
updateScrub: (epoch, source) => {
|
|
7890
|
+
dispatch({
|
|
7891
|
+
type: "update",
|
|
7892
|
+
epoch,
|
|
7893
|
+
source
|
|
7894
|
+
});
|
|
7895
|
+
const now = Date.now();
|
|
7896
|
+
if (!shouldEmit(lastEmitRef.current, now)) return;
|
|
7897
|
+
lastEmitRef.current = now;
|
|
7898
|
+
sink.scrubTo(epoch);
|
|
7899
|
+
},
|
|
7900
|
+
endScrub: (epoch, source) => {
|
|
7901
|
+
dispatch({
|
|
7902
|
+
type: "end",
|
|
7903
|
+
epoch,
|
|
7904
|
+
source
|
|
7905
|
+
});
|
|
7906
|
+
commit(epoch, sink.scrubCommit);
|
|
7907
|
+
},
|
|
7908
|
+
seek: (epoch, source) => {
|
|
7909
|
+
dispatch({
|
|
7910
|
+
type: "seek",
|
|
7911
|
+
epoch,
|
|
7912
|
+
source
|
|
7913
|
+
});
|
|
7914
|
+
commit(epoch, sink.seek);
|
|
7915
|
+
},
|
|
7916
|
+
setPlayhead: (epoch) => dispatch({
|
|
7917
|
+
type: "playhead",
|
|
7918
|
+
epoch
|
|
7919
|
+
}),
|
|
7920
|
+
reset: () => dispatch({ type: "reset" })
|
|
7921
|
+
};
|
|
7922
|
+
}
|
|
7923
|
+
/**
|
|
7924
|
+
* Owns the shared scrub position and bridges it to a playback control sink.
|
|
7925
|
+
*
|
|
7926
|
+
* The bridge is always active: any gesture (drag or tap) drives the sink
|
|
7927
|
+
* immediately. There is no VOD-active gate — the timeline always drives
|
|
7928
|
+
* playback while frame-push mode is on.
|
|
7929
|
+
*
|
|
7930
|
+
* @param sink Always-active playback control surface (e.g. the datachannel)
|
|
7931
|
+
* @param isPlayerLive Synchronous getter for "is the player on the live
|
|
7932
|
+
* edge?" — lets a live→recorded commit bypass dedupe so the seek always
|
|
7933
|
+
* reaches the server.
|
|
7934
|
+
*/
|
|
7935
|
+
function useScrubController(sink, isPlayerLive = () => false) {
|
|
7936
|
+
const [state, dispatch] = (0, react$1.useReducer)(scrubReducer, initialScrubState);
|
|
7937
|
+
const lastEmitRef = (0, react$1.useRef)(0);
|
|
7938
|
+
const lastCommitRef = (0, react$1.useRef)(null);
|
|
7939
|
+
const isPlayerLiveRef = (0, react$1.useRef)(isPlayerLive);
|
|
7940
|
+
isPlayerLiveRef.current = isPlayerLive;
|
|
7941
|
+
return {
|
|
7942
|
+
state,
|
|
7943
|
+
...(0, react$1.useMemo)(() => makeScrubBridge(sink, dispatch, lastEmitRef, lastCommitRef, () => isPlayerLiveRef.current()), [sink])
|
|
7944
|
+
};
|
|
7945
|
+
}
|
|
7946
|
+
//#endregion
|
|
7713
7947
|
//#region node_modules/lucide-react/dist/esm/shared/src/utils/mergeClasses.js
|
|
7714
7948
|
/**
|
|
7715
7949
|
* @license lucide-react v0.576.0 - ISC
|
|
@@ -32548,6 +32782,100 @@ function useVodPlayback() {
|
|
|
32548
32782
|
return (0, react$1.useContext)(VodPlaybackContext);
|
|
32549
32783
|
}
|
|
32550
32784
|
//#endregion
|
|
32785
|
+
//#region src/contexts/recorded-playback.tsx
|
|
32786
|
+
/**
|
|
32787
|
+
* RecordedPlayback — shared context wiring the hero player's
|
|
32788
|
+
* `camstack-control` RTCDataChannel to the Recording timeline (R9).
|
|
32789
|
+
*
|
|
32790
|
+
* The hero `StreamPanel` (host side) BINDS the channel that
|
|
32791
|
+
* `CameraStreamPlayer` opens on the live WebRTC session; the Recording
|
|
32792
|
+
* widget's `RecordingTimeline` (controller side) SENDS frame-push playback
|
|
32793
|
+
* commands over it (seek / scrubCommit / setRate / goLive — the broker's
|
|
32794
|
+
* `TimelineSession` drives the `RecordedFeeder` on the SAME session, so
|
|
32795
|
+
* recorded frames arrive on the same `<video>` element) and OBSERVES the
|
|
32796
|
+
* server's playback state + ~1 Hz position reports.
|
|
32797
|
+
*
|
|
32798
|
+
* This is the admin-ui port of the shipped viewer's default recorded path
|
|
32799
|
+
* (srcV2 → embed → `camstack-control`), replacing "manifest-window HLS only".
|
|
32800
|
+
* The HLS VOD path (VodPlayback context) remains available as a fallback.
|
|
32801
|
+
*
|
|
32802
|
+
* Built on `createSharedContext` (NOT a plain `createContext`) so it crosses
|
|
32803
|
+
* the Module-Federation boundary — same mechanism as `vod-playback`.
|
|
32804
|
+
* Consumers outside a provider get the inert default: `bindControlChannel`
|
|
32805
|
+
* is null (StreamPanel then opens NO datachannel — plain live player) and
|
|
32806
|
+
* `channelOpen` is false (RecordingTimeline stays on the HLS path).
|
|
32807
|
+
*/
|
|
32808
|
+
var RecordedPlaybackContext = createSharedContext("camstack:recorded-playback", {
|
|
32809
|
+
bindControlChannel: null,
|
|
32810
|
+
channelOpen: false,
|
|
32811
|
+
mode: "live",
|
|
32812
|
+
positionMs: null,
|
|
32813
|
+
send: () => false
|
|
32814
|
+
});
|
|
32815
|
+
function RecordedPlaybackProvider({ children }) {
|
|
32816
|
+
const channelRef = (0, react$1.useRef)(null);
|
|
32817
|
+
const [channelOpen, setChannelOpen] = (0, react$1.useState)(false);
|
|
32818
|
+
const [mode, setMode] = (0, react$1.useState)("live");
|
|
32819
|
+
const [positionMs, setPositionMs] = (0, react$1.useState)(null);
|
|
32820
|
+
const bindControlChannel = (0, react$1.useCallback)((channel) => {
|
|
32821
|
+
channelRef.current = channel;
|
|
32822
|
+
const markClosed = () => {
|
|
32823
|
+
if (channelRef.current !== channel) return;
|
|
32824
|
+
channelRef.current = null;
|
|
32825
|
+
setChannelOpen(false);
|
|
32826
|
+
setMode("live");
|
|
32827
|
+
setPositionMs(null);
|
|
32828
|
+
};
|
|
32829
|
+
channel.onopen = () => {
|
|
32830
|
+
if (channelRef.current === channel) setChannelOpen(true);
|
|
32831
|
+
};
|
|
32832
|
+
channel.onclose = markClosed;
|
|
32833
|
+
channel.onerror = markClosed;
|
|
32834
|
+
channel.onmessage = (e) => {
|
|
32835
|
+
if (channelRef.current !== channel) return;
|
|
32836
|
+
const data = e.data;
|
|
32837
|
+
if (typeof data !== "string") return;
|
|
32838
|
+
const msg = parseRecordedServerMessage(data);
|
|
32839
|
+
if (!msg) return;
|
|
32840
|
+
if (msg.kind === "position") setPositionMs(msg.epochMs);
|
|
32841
|
+
else {
|
|
32842
|
+
setMode(msg.state);
|
|
32843
|
+
if (msg.state === "live") setPositionMs(null);
|
|
32844
|
+
}
|
|
32845
|
+
};
|
|
32846
|
+
if (channel.readyState === "open") setChannelOpen(true);
|
|
32847
|
+
else if (channel.readyState === "closed" || channel.readyState === "closing") markClosed();
|
|
32848
|
+
else setChannelOpen(false);
|
|
32849
|
+
}, []);
|
|
32850
|
+
const send = (0, react$1.useCallback)((cmd) => {
|
|
32851
|
+
const channel = channelRef.current;
|
|
32852
|
+
if (!channel || channel.readyState !== "open") return false;
|
|
32853
|
+
channel.send(serializeRecordedCommand(cmd));
|
|
32854
|
+
return true;
|
|
32855
|
+
}, []);
|
|
32856
|
+
const value = (0, react$1.useMemo)(() => ({
|
|
32857
|
+
bindControlChannel,
|
|
32858
|
+
channelOpen,
|
|
32859
|
+
mode,
|
|
32860
|
+
positionMs,
|
|
32861
|
+
send
|
|
32862
|
+
}), [
|
|
32863
|
+
bindControlChannel,
|
|
32864
|
+
channelOpen,
|
|
32865
|
+
mode,
|
|
32866
|
+
positionMs,
|
|
32867
|
+
send
|
|
32868
|
+
]);
|
|
32869
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RecordedPlaybackContext.Provider, {
|
|
32870
|
+
value,
|
|
32871
|
+
children
|
|
32872
|
+
});
|
|
32873
|
+
}
|
|
32874
|
+
/** @returns the recorded-playback control surface (inert outside a provider). */
|
|
32875
|
+
function useRecordedPlayback() {
|
|
32876
|
+
return (0, react$1.useContext)(RecordedPlaybackContext);
|
|
32877
|
+
}
|
|
32878
|
+
//#endregion
|
|
32551
32879
|
//#region src/composites/cap-settings/recording-spans.ts
|
|
32552
32880
|
/**
|
|
32553
32881
|
* Merge availability ranges (across every profile) into the union of distinct
|
|
@@ -32793,8 +33121,12 @@ function axisTicks(fromMs, toMs, count) {
|
|
|
32793
33121
|
}
|
|
32794
33122
|
//#endregion
|
|
32795
33123
|
//#region src/composites/cap-settings/CoverageTrack.tsx
|
|
32796
|
-
function CoverageTrack({ windowFrom, windowTo, spans, playheadMs, onSeek }) {
|
|
33124
|
+
function CoverageTrack({ windowFrom, windowTo, spans, playheadMs, onSeek, onScrubStart, onScrubMove, onScrubEnd }) {
|
|
32797
33125
|
const [hoverInfo, setHoverInfo] = (0, react$1.useState)(null);
|
|
33126
|
+
const scrubEnabled = Boolean(onScrubEnd);
|
|
33127
|
+
const [dragging, setDragging] = (0, react$1.useState)(false);
|
|
33128
|
+
/** Last epoch reported during the drag — commit target on cancel/release. */
|
|
33129
|
+
const lastDragMsRef = (0, react$1.useRef)(null);
|
|
32798
33130
|
const msFromEvent = (e) => {
|
|
32799
33131
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
32800
33132
|
if (rect.width <= 0) return null;
|
|
@@ -32802,10 +33134,43 @@ function CoverageTrack({ windowFrom, windowTo, spans, playheadMs, onSeek }) {
|
|
|
32802
33134
|
return windowFrom + Math.min(1, Math.max(0, ratio)) * (windowTo - windowFrom);
|
|
32803
33135
|
};
|
|
32804
33136
|
const handleClick = (e) => {
|
|
33137
|
+
if (scrubEnabled) return;
|
|
32805
33138
|
const ms = msFromEvent(e);
|
|
32806
33139
|
if (ms === null) return;
|
|
32807
33140
|
onSeek(ms);
|
|
32808
33141
|
};
|
|
33142
|
+
const handlePointerDown = (e) => {
|
|
33143
|
+
if (!scrubEnabled) return;
|
|
33144
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
33145
|
+
setDragging(true);
|
|
33146
|
+
onScrubStart?.();
|
|
33147
|
+
const ms = msFromEvent(e);
|
|
33148
|
+
if (ms !== null) {
|
|
33149
|
+
lastDragMsRef.current = ms;
|
|
33150
|
+
onScrubMove?.(ms);
|
|
33151
|
+
}
|
|
33152
|
+
};
|
|
33153
|
+
const handlePointerMove = (e) => {
|
|
33154
|
+
if (!scrubEnabled || !dragging) return;
|
|
33155
|
+
const ms = msFromEvent(e);
|
|
33156
|
+
if (ms === null) return;
|
|
33157
|
+
lastDragMsRef.current = ms;
|
|
33158
|
+
onScrubMove?.(ms);
|
|
33159
|
+
};
|
|
33160
|
+
const handlePointerUp = (e) => {
|
|
33161
|
+
if (!scrubEnabled || !dragging) return;
|
|
33162
|
+
setDragging(false);
|
|
33163
|
+
const ms = msFromEvent(e) ?? lastDragMsRef.current;
|
|
33164
|
+
lastDragMsRef.current = null;
|
|
33165
|
+
if (ms !== null) onScrubEnd?.(ms);
|
|
33166
|
+
};
|
|
33167
|
+
const handlePointerCancel = () => {
|
|
33168
|
+
if (!dragging) return;
|
|
33169
|
+
setDragging(false);
|
|
33170
|
+
const ms = lastDragMsRef.current;
|
|
33171
|
+
lastDragMsRef.current = null;
|
|
33172
|
+
if (ms !== null) onScrubEnd?.(ms);
|
|
33173
|
+
};
|
|
32809
33174
|
const handleMouseMove = (e) => {
|
|
32810
33175
|
const ms = msFromEvent(e);
|
|
32811
33176
|
if (ms === null) return;
|
|
@@ -32829,10 +33194,15 @@ function CoverageTrack({ windowFrom, windowTo, spans, playheadMs, onSeek }) {
|
|
|
32829
33194
|
className: "relative",
|
|
32830
33195
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
32831
33196
|
"data-testid": "coverage-track",
|
|
33197
|
+
"data-scrubbing": dragging ? "true" : "false",
|
|
32832
33198
|
onClick: handleClick,
|
|
33199
|
+
onPointerDown: handlePointerDown,
|
|
33200
|
+
onPointerMove: handlePointerMove,
|
|
33201
|
+
onPointerUp: handlePointerUp,
|
|
33202
|
+
onPointerCancel: handlePointerCancel,
|
|
32833
33203
|
onMouseMove: handleMouseMove,
|
|
32834
33204
|
onMouseLeave: handleMouseLeave,
|
|
32835
|
-
className:
|
|
33205
|
+
className: `relative h-5 overflow-hidden rounded bg-background/40 ${scrubEnabled ? "cursor-ew-resize touch-none select-none" : "cursor-pointer"}`,
|
|
32836
33206
|
role: "group",
|
|
32837
33207
|
"aria-label": "Recorded coverage timeline",
|
|
32838
33208
|
children: [spans.map((s) => {
|
|
@@ -33131,6 +33501,15 @@ var PAN_MS_PER_PX = 18e3 / 100;
|
|
|
33131
33501
|
var EVENT_PRE_MS = 5e3;
|
|
33132
33502
|
var EVENT_POST_MS = 1e4;
|
|
33133
33503
|
var EMPTY_RANGES = [];
|
|
33504
|
+
var MODE_LABELS = {
|
|
33505
|
+
live: "Live",
|
|
33506
|
+
loading: "Loading…",
|
|
33507
|
+
playing: "Playing",
|
|
33508
|
+
paused: "Paused",
|
|
33509
|
+
gap: "No footage",
|
|
33510
|
+
ended: "Ended",
|
|
33511
|
+
error: "Error"
|
|
33512
|
+
};
|
|
33134
33513
|
function RecordingTimeline({ deviceId }) {
|
|
33135
33514
|
const system = useSystem();
|
|
33136
33515
|
const vod = useVodPlayback();
|
|
@@ -33138,6 +33517,51 @@ function RecordingTimeline({ deviceId }) {
|
|
|
33138
33517
|
const [playheadMs, setPlayheadMs] = (0, react$1.useState)(null);
|
|
33139
33518
|
const [busyKey, setBusyKey] = (0, react$1.useState)(null);
|
|
33140
33519
|
const [playbackError, setPlaybackError] = (0, react$1.useState)(null);
|
|
33520
|
+
const recorded = useRecordedPlayback();
|
|
33521
|
+
const { send: sendControl, mode: recordedMode, positionMs: recordedPositionMs } = recorded;
|
|
33522
|
+
const [forceHls, setForceHls] = (0, react$1.useState)(false);
|
|
33523
|
+
const framePush = recorded.channelOpen && !forceHls;
|
|
33524
|
+
const framePushRef = (0, react$1.useRef)(framePush);
|
|
33525
|
+
framePushRef.current = framePush;
|
|
33526
|
+
const sink = (0, react$1.useMemo)(() => ({
|
|
33527
|
+
scrubStart: () => {},
|
|
33528
|
+
scrubTo: () => {},
|
|
33529
|
+
scrubCommit: (epochMs) => {
|
|
33530
|
+
sendControl({
|
|
33531
|
+
t: "scrubCommit",
|
|
33532
|
+
epoch: epochMs
|
|
33533
|
+
});
|
|
33534
|
+
},
|
|
33535
|
+
seek: (epochMs) => {
|
|
33536
|
+
sendControl({
|
|
33537
|
+
t: "seek",
|
|
33538
|
+
epoch: epochMs
|
|
33539
|
+
});
|
|
33540
|
+
}
|
|
33541
|
+
}), [sendControl]);
|
|
33542
|
+
const recordedModeRef = (0, react$1.useRef)(recordedMode);
|
|
33543
|
+
recordedModeRef.current = recordedMode;
|
|
33544
|
+
const { state: scrubState, beginScrub, updateScrub, endScrub, seek: scrubSeek, setPlayhead, reset: resetScrub } = useScrubController(sink, (0, react$1.useCallback)(() => recordedModeRef.current === "live", []));
|
|
33545
|
+
(0, react$1.useEffect)(() => {
|
|
33546
|
+
if (recordedPositionMs !== null) setPlayhead(recordedPositionMs);
|
|
33547
|
+
}, [recordedPositionMs, setPlayhead]);
|
|
33548
|
+
(0, react$1.useEffect)(() => {
|
|
33549
|
+
if (!recorded.channelOpen || recordedMode === "live") resetScrub();
|
|
33550
|
+
}, [
|
|
33551
|
+
recorded.channelOpen,
|
|
33552
|
+
recordedMode,
|
|
33553
|
+
resetScrub
|
|
33554
|
+
]);
|
|
33555
|
+
const handleGoLive = (0, react$1.useCallback)(() => {
|
|
33556
|
+
sendControl({ t: "goLive" });
|
|
33557
|
+
resetScrub();
|
|
33558
|
+
}, [sendControl, resetScrub]);
|
|
33559
|
+
const handleRateToggle = (0, react$1.useCallback)(() => {
|
|
33560
|
+
sendControl({
|
|
33561
|
+
t: "setRate",
|
|
33562
|
+
rate: recordedModeRef.current === "paused" ? 1 : 0
|
|
33563
|
+
});
|
|
33564
|
+
}, [sendControl]);
|
|
33141
33565
|
const [enabledKinds, setEnabledKinds] = (0, react$1.useState)(() => new Set(ALL_KINDS));
|
|
33142
33566
|
const toggleKind = (0, react$1.useCallback)((kind) => {
|
|
33143
33567
|
setEnabledKinds((current) => {
|
|
@@ -33291,6 +33715,10 @@ function RecordingTimeline({ deviceId }) {
|
|
|
33291
33715
|
const handleSeek = (0, react$1.useCallback)((clickMs) => {
|
|
33292
33716
|
const target = seekTargetForSpans(clickMs, spans);
|
|
33293
33717
|
if (!target) return;
|
|
33718
|
+
if (framePushRef.current) {
|
|
33719
|
+
scrubSeek(target.fromMs, "timeline");
|
|
33720
|
+
return;
|
|
33721
|
+
}
|
|
33294
33722
|
const label = `Recording · ${new Date(target.fromMs).toLocaleString(void 0, {
|
|
33295
33723
|
month: "short",
|
|
33296
33724
|
day: "numeric",
|
|
@@ -33298,14 +33726,31 @@ function RecordingTimeline({ deviceId }) {
|
|
|
33298
33726
|
minute: "2-digit"
|
|
33299
33727
|
})}`;
|
|
33300
33728
|
playWindow(`seek:${target.fromMs}`, target.fromMs, target.toMs, label, target.fromMs);
|
|
33301
|
-
}, [
|
|
33729
|
+
}, [
|
|
33730
|
+
spans,
|
|
33731
|
+
playWindow,
|
|
33732
|
+
scrubSeek
|
|
33733
|
+
]);
|
|
33734
|
+
const handleScrubStart = (0, react$1.useCallback)(() => {
|
|
33735
|
+
beginScrub("timeline");
|
|
33736
|
+
}, [beginScrub]);
|
|
33737
|
+
const handleScrubMove = (0, react$1.useCallback)((ms) => {
|
|
33738
|
+
updateScrub(ms, "timeline");
|
|
33739
|
+
}, [updateScrub]);
|
|
33740
|
+
const handleScrubEnd = (0, react$1.useCallback)((ms) => {
|
|
33741
|
+
endScrub(seekTargetForSpans(ms, spans)?.fromMs ?? ms, "timeline");
|
|
33742
|
+
}, [spans, endScrub]);
|
|
33302
33743
|
const handlePlayClip = (0, react$1.useCallback)((clip) => {
|
|
33744
|
+
if (framePushRef.current) {
|
|
33745
|
+
scrubSeek(clip.timestamp - EVENT_PRE_MS, "timeline");
|
|
33746
|
+
return;
|
|
33747
|
+
}
|
|
33303
33748
|
const label = `${clip.label} · ${new Date(clip.timestamp).toLocaleTimeString(void 0, {
|
|
33304
33749
|
hour: "2-digit",
|
|
33305
33750
|
minute: "2-digit"
|
|
33306
33751
|
})}`;
|
|
33307
33752
|
playWindow(`event:${clip.id}`, clip.timestamp - EVENT_PRE_MS, clip.timestamp + EVENT_POST_MS, label, clip.timestamp);
|
|
33308
|
-
}, [playWindow]);
|
|
33753
|
+
}, [playWindow, scrubSeek]);
|
|
33309
33754
|
const handleSelectBucket = (0, react$1.useCallback)((index) => {
|
|
33310
33755
|
setSelectedBucket((cur) => cur === index ? null : index);
|
|
33311
33756
|
}, []);
|
|
@@ -33313,6 +33758,8 @@ function RecordingTimeline({ deviceId }) {
|
|
|
33313
33758
|
const densityLoading = eventDensityQuery.isLoading;
|
|
33314
33759
|
const drillLoading = (objectDrill.isLoading || motionDrill.isLoading || audioDrill.isLoading) && drillEnabled;
|
|
33315
33760
|
const ticks = (0, react$1.useMemo)(() => axisTicks(view.fromMs, view.toMs, 5), [view.fromMs, view.toMs]);
|
|
33761
|
+
const displayPlayheadMs = framePush ? scrubState.scrubbing ? scrubState.scrubEpoch : scrubState.playheadEpoch : playheadMs;
|
|
33762
|
+
const showRateToggle = framePush && (recordedMode === "playing" || recordedMode === "paused");
|
|
33316
33763
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
33317
33764
|
className: "space-y-3",
|
|
33318
33765
|
children: [
|
|
@@ -33331,6 +33778,45 @@ function RecordingTimeline({ deviceId }) {
|
|
|
33331
33778
|
dayBounds: computedDayBounds,
|
|
33332
33779
|
onViewChange: handleViewChange
|
|
33333
33780
|
}),
|
|
33781
|
+
recorded.channelOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
33782
|
+
"data-testid": "frame-push-status-row",
|
|
33783
|
+
className: "flex flex-wrap items-center gap-2 text-[10px]",
|
|
33784
|
+
children: [
|
|
33785
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
33786
|
+
"data-testid": "frame-push-mode-chip",
|
|
33787
|
+
className: `inline-flex items-center gap-1 rounded px-1.5 py-0.5 font-semibold uppercase tracking-wider ${framePush && recordedMode !== "live" ? recordedMode === "error" ? "bg-rose-500/15 text-rose-400" : "bg-primary/15 text-primary" : "bg-background/40 text-foreground-subtle"}`,
|
|
33788
|
+
children: framePush ? MODE_LABELS[recordedMode] : "HLS mode"
|
|
33789
|
+
}),
|
|
33790
|
+
framePush && recordedPositionMs !== null && !scrubState.scrubbing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
33791
|
+
className: "tabular-nums text-foreground-subtle",
|
|
33792
|
+
children: new Date(recordedPositionMs).toLocaleTimeString()
|
|
33793
|
+
}) : null,
|
|
33794
|
+
showRateToggle ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
33795
|
+
type: "button",
|
|
33796
|
+
"data-testid": "frame-push-rate-toggle",
|
|
33797
|
+
onClick: handleRateToggle,
|
|
33798
|
+
className: "inline-flex items-center gap-1 rounded px-1.5 py-0.5 font-medium text-foreground-subtle transition-colors hover:bg-surface-hover hover:text-foreground",
|
|
33799
|
+
title: recordedMode === "paused" ? "Resume playback" : "Pause playback",
|
|
33800
|
+
children: [recordedMode === "paused" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Play, { className: "h-3 w-3" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pause, { className: "h-3 w-3" }), recordedMode === "paused" ? "Resume" : "Pause"]
|
|
33801
|
+
}) : null,
|
|
33802
|
+
framePush && recordedMode !== "live" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
33803
|
+
type: "button",
|
|
33804
|
+
"data-testid": "frame-push-go-live",
|
|
33805
|
+
onClick: handleGoLive,
|
|
33806
|
+
className: "inline-flex items-center gap-1 rounded bg-danger/10 px-1.5 py-0.5 font-medium text-danger transition-colors hover:bg-danger/20",
|
|
33807
|
+
title: "Return to the live stream",
|
|
33808
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Radio, { className: "h-3 w-3" }), "Go live"]
|
|
33809
|
+
}) : null,
|
|
33810
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
33811
|
+
type: "button",
|
|
33812
|
+
"data-testid": "frame-push-hls-toggle",
|
|
33813
|
+
onClick: () => setForceHls((v) => !v),
|
|
33814
|
+
className: `ml-auto inline-flex items-center gap-1 rounded px-1.5 py-0.5 font-medium transition-colors ${forceHls ? "bg-primary/15 text-primary" : "text-foreground-subtle hover:bg-surface-hover hover:text-foreground"}`,
|
|
33815
|
+
title: "Play recordings through the HLS player instead of the live WebRTC session",
|
|
33816
|
+
children: "HLS"
|
|
33817
|
+
})
|
|
33818
|
+
]
|
|
33819
|
+
}) : null,
|
|
33334
33820
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
33335
33821
|
ref: trackAreaRef,
|
|
33336
33822
|
onWheel: handleWheel,
|
|
@@ -33348,8 +33834,13 @@ function RecordingTimeline({ deviceId }) {
|
|
|
33348
33834
|
windowFrom: view.fromMs,
|
|
33349
33835
|
windowTo: view.toMs,
|
|
33350
33836
|
spans,
|
|
33351
|
-
playheadMs,
|
|
33352
|
-
onSeek: handleSeek
|
|
33837
|
+
playheadMs: displayPlayheadMs,
|
|
33838
|
+
onSeek: handleSeek,
|
|
33839
|
+
...framePush ? {
|
|
33840
|
+
onScrubStart: handleScrubStart,
|
|
33841
|
+
onScrubMove: handleScrubMove,
|
|
33842
|
+
onScrubEnd: handleScrubEnd
|
|
33843
|
+
} : {}
|
|
33353
33844
|
})]
|
|
33354
33845
|
}),
|
|
33355
33846
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -33386,7 +33877,7 @@ function RecordingTimeline({ deviceId }) {
|
|
|
33386
33877
|
onLoadMore: handleLoadMore
|
|
33387
33878
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
33388
33879
|
className: "text-[11px] text-foreground-subtle",
|
|
33389
|
-
children: "Click the coverage bar to play from any point, or a density bar to browse that window
|
|
33880
|
+
children: framePush ? "Click or drag the coverage bar to play from any point (frames play in the main player above), or click a density bar to browse that window’s events." : "Click the coverage bar to play from any point, or a density bar to browse that window’s events."
|
|
33390
33881
|
})
|
|
33391
33882
|
]
|
|
33392
33883
|
});
|
|
@@ -38674,6 +39165,7 @@ function ImageOffIcon({ className }) {
|
|
|
38674
39165
|
}
|
|
38675
39166
|
function StreamPanel({ serverUrl, createSession, sendAnswer, handleOffer, getIceServers, addIceCandidate, getIceCandidates, closeSession, getSessionState, playbackUrl, playbackAuthToken, onExitPlayback, posterUrl, streams, activeStreamId: controlledStreamId, onStreamChange, deviceName, phase, pipelineMetrics, detections, defaultShowDetections = true, defaultShowMotion = false, streamStats, autoPlay = false, showPlayStop = false, snapshotSrc, snapshotLoading = false, onRefreshSnapshot, showStreamStats = false, children, extraOverlay, ptzAvailable = false, ptzShown = false, ptzOverlay, onPtzToggle, intercomAvailable = false, intercomShown = false, onIntercomToggle, chromeless = false, className }) {
|
|
38676
39167
|
const registeredButtons = usePlayerToolbarButtons();
|
|
39168
|
+
const recordedPlayback = useRecordedPlayback();
|
|
38677
39169
|
const [isPlaying, setIsPlaying] = (0, react$1.useState)(autoPlay);
|
|
38678
39170
|
const [showDetections, setShowDetections] = (0, react$1.useState)(defaultShowDetections);
|
|
38679
39171
|
const [showMotion, setShowMotion] = (0, react$1.useState)(defaultShowMotion);
|
|
@@ -38979,6 +39471,7 @@ function StreamPanel({ serverUrl, createSession, sendAnswer, handleOffer, getIce
|
|
|
38979
39471
|
addIceCandidate,
|
|
38980
39472
|
getIceCandidates,
|
|
38981
39473
|
closeSession,
|
|
39474
|
+
...recordedPlayback.bindControlChannel ? { onControlChannel: recordedPlayback.bindControlChannel } : {},
|
|
38982
39475
|
...posterUrl ? { posterUrl } : {},
|
|
38983
39476
|
overlay: anyOverlay || extraOverlay || ptzOverlayVisible ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
38984
39477
|
anyOverlay && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DetectionOverlay, {
|
|
@@ -43622,6 +44115,8 @@ exports.CHIP_INACTIVE = CHIP_INACTIVE;
|
|
|
43622
44115
|
exports.CLASS_COLORS = CLASS_COLORS;
|
|
43623
44116
|
exports.COLUMN_BREAKPOINT_CLASS = COLUMN_BREAKPOINT_CLASS;
|
|
43624
44117
|
exports.COLUMN_PRIORITY = COLUMN_PRIORITY;
|
|
44118
|
+
exports.COMMIT_DEDUPE_TOLERANCE_MS = COMMIT_DEDUPE_TOLERANCE_MS;
|
|
44119
|
+
exports.COMMIT_DEDUPE_WINDOW_MS = COMMIT_DEDUPE_WINDOW_MS;
|
|
43625
44120
|
exports.CONTROL_CAP_NAMES = CONTROL_CAP_NAMES;
|
|
43626
44121
|
exports.CONTROL_FILLS = CONTROL_FILLS;
|
|
43627
44122
|
exports.CameraStreamPlayer = CameraStreamPlayer;
|
|
@@ -43756,9 +44251,11 @@ exports.PrivacyMaskSettings = PrivacyMaskSettings;
|
|
|
43756
44251
|
exports.ProviderBadge = ProviderBadge;
|
|
43757
44252
|
exports.PtzPanel = PtzPanel;
|
|
43758
44253
|
exports.QrCode = QrCode;
|
|
44254
|
+
exports.RECORDED_PLAYBACK_MODES = RECORDED_PLAYBACK_MODES;
|
|
43759
44255
|
exports.RIGHT = RIGHT;
|
|
43760
44256
|
exports.ROLE_DESCRIPTOR = ROLE_DESCRIPTOR;
|
|
43761
44257
|
exports.RadialGauge = RadialGauge;
|
|
44258
|
+
exports.RecordedPlaybackProvider = RecordedPlaybackProvider;
|
|
43762
44259
|
exports.RecordingPanel = RecordingPanel;
|
|
43763
44260
|
exports.ResponseLog = ResponseLog;
|
|
43764
44261
|
exports.SECTION_BODY = SECTION_BODY;
|
|
@@ -43842,6 +44339,7 @@ exports.coverHighlight = coverHighlight;
|
|
|
43842
44339
|
exports.createLucideIcon = createLucideIcon;
|
|
43843
44340
|
exports.createSharedContext = createSharedContext;
|
|
43844
44341
|
exports.createTheme = require_theme_index.createTheme$1;
|
|
44342
|
+
exports.cursorFractionFor = cursorFractionFor;
|
|
43845
44343
|
exports.darkColors = require_theme_index.darkColors$1;
|
|
43846
44344
|
exports.defaultTheme = require_theme_index.defaultTheme$1;
|
|
43847
44345
|
exports.deriveDeviceKind = deriveDeviceKind;
|
|
@@ -43859,12 +44357,14 @@ exports.getPhaseVisual = getPhaseVisual;
|
|
|
43859
44357
|
exports.groupChildrenByLayout = groupChildrenByLayout;
|
|
43860
44358
|
exports.hardwareLabel = hardwareLabel;
|
|
43861
44359
|
exports.humidifierTint = humidifierTint;
|
|
44360
|
+
exports.initialScrubState = initialScrubState;
|
|
43862
44361
|
exports.isAbsentProvider = isAbsentProvider;
|
|
43863
44362
|
exports.isAbsentProvider$1 = isAbsentProvider;
|
|
43864
44363
|
exports.isFieldVisible = isFieldVisible;
|
|
43865
44364
|
exports.lawnMowerActivityMeta = lawnMowerActivityMeta;
|
|
43866
44365
|
exports.lightColors = require_theme_index.lightColors$1;
|
|
43867
44366
|
exports.loadRemoteBundle = loadRemoteBundle;
|
|
44367
|
+
exports.makeScrubBridge = makeScrubBridge;
|
|
43868
44368
|
exports.metadataEntries = metadataEntries;
|
|
43869
44369
|
exports.metadataString = metadataString;
|
|
43870
44370
|
exports.mirror = mirror;
|
|
@@ -43872,11 +44372,16 @@ exports.mountAddonPage = mountAddonPage;
|
|
|
43872
44372
|
exports.nextSort = nextSort;
|
|
43873
44373
|
exports.normalizeForSearch = normalizeForSearch;
|
|
43874
44374
|
exports.overrideEntityIdFromLink = overrideEntityIdFromLink;
|
|
44375
|
+
exports.parseRecordedServerMessage = parseRecordedServerMessage;
|
|
43875
44376
|
exports.providerIcons = providerIcons;
|
|
43876
44377
|
exports.resolveContainerPrimary = resolveContainerPrimary;
|
|
43877
44378
|
exports.resolveControlAlign = resolveControlAlign;
|
|
43878
44379
|
exports.resolveDeviceControl = resolveDeviceControl;
|
|
43879
44380
|
exports.resolvePrimaryChild = resolvePrimaryChild;
|
|
44381
|
+
exports.scrubReducer = scrubReducer;
|
|
44382
|
+
exports.serializeRecordedCommand = serializeRecordedCommand;
|
|
44383
|
+
exports.shouldCommit = shouldCommit;
|
|
44384
|
+
exports.shouldEmit = shouldEmit;
|
|
43880
44385
|
exports.sortRows = sortRows;
|
|
43881
44386
|
exports.statusIcons = statusIcons;
|
|
43882
44387
|
exports.tankAlert = tankAlert;
|
|
@@ -44482,6 +44987,7 @@ exports.usePtzSavePreset = usePtzSavePreset;
|
|
|
44482
44987
|
exports.usePtzSetAutofocus = usePtzSetAutofocus;
|
|
44483
44988
|
exports.usePtzStop = usePtzStop;
|
|
44484
44989
|
exports.useRebootReboot = useRebootReboot;
|
|
44990
|
+
exports.useRecordedPlayback = useRecordedPlayback;
|
|
44485
44991
|
exports.useRecordingApplyDeviceSettingsPatch = useRecordingApplyDeviceSettingsPatch;
|
|
44486
44992
|
exports.useRecordingGetAvailability = useRecordingGetAvailability;
|
|
44487
44993
|
exports.useRecordingGetDaysWithRecordings = useRecordingGetDaysWithRecordings;
|
|
@@ -44500,6 +45006,7 @@ exports.useRemoteComponent = useRemoteComponent;
|
|
|
44500
45006
|
exports.useScriptRunnerGetStatus = useScriptRunnerGetStatus;
|
|
44501
45007
|
exports.useScriptRunnerRun = useScriptRunnerRun;
|
|
44502
45008
|
exports.useScriptRunnerStop = useScriptRunnerStop;
|
|
45009
|
+
exports.useScrubController = useScrubController;
|
|
44503
45010
|
exports.useSettingsStoreCount = useSettingsStoreCount;
|
|
44504
45011
|
exports.useSettingsStoreDeclareCollection = useSettingsStoreDeclareCollection;
|
|
44505
45012
|
exports.useSettingsStoreDelete = useSettingsStoreDelete;
|