@camstack/ui-library 1.2.12 → 1.2.14

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/index.cjs CHANGED
@@ -12644,7 +12644,7 @@ var buttonVariants = cva("inline-flex items-center justify-center rounded-md fon
12644
12644
  secondary: "bg-surface text-foreground hover:bg-surface-hover",
12645
12645
  ghost: "hover:bg-surface-hover text-foreground-muted",
12646
12646
  danger: "bg-danger text-white hover:bg-danger/90",
12647
- outline: "border border-border bg-transparent hover:bg-surface-hover"
12647
+ outline: "border border-border bg-transparent text-foreground hover:bg-surface-hover"
12648
12648
  },
12649
12649
  size: {
12650
12650
  sm: "h-7 px-2.5 text-xs gap-1.5",
@@ -12949,7 +12949,7 @@ function DialogTrigger({ children, ...props }) {
12949
12949
  * `w-full` (which would overflow/clip a centered dialog); the per-variant
12950
12950
  * `max-w-*` keeps small dialogs small while the cap prevents overflow.
12951
12951
  */
12952
- var contentVariants = cva("fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 m-0 w-[calc(100vw-2rem)] bg-background-elevated border border-border rounded-lg p-4 backdrop:bg-black/50 backdrop:backdrop-blur-sm max-h-[calc(100dvh-2rem)] overflow-y-auto [&:not([open])]:hidden", {
12952
+ var contentVariants = cva("fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 m-0 w-[calc(100vw-2rem)] bg-background-elevated text-foreground border border-border rounded-lg p-4 backdrop:bg-black/50 backdrop:backdrop-blur-sm max-h-[calc(100dvh-2rem)] overflow-y-auto [&:not([open])]:hidden", {
12953
12953
  variants: {
12954
12954
  width: {
12955
12955
  sm: "max-w-sm",
@@ -18504,6 +18504,8 @@ var useDeviceManagerProviderDiscoveryParamsSchema = trpc.deviceManager.providerD
18504
18504
  var useDeviceManagerTestField = trpc.deviceManager.testField.useMutation;
18505
18505
  /** Generated alias around `trpc.deviceManager.getDeviceStatusAggregate.useQuery`. */
18506
18506
  var useDeviceManagerGetDeviceStatusAggregate = trpc.deviceManager.getDeviceStatusAggregate.useQuery;
18507
+ /** Generated alias around `trpc.deviceManager.getDeviceStatusAggregateBatch.useQuery`. */
18508
+ var useDeviceManagerGetDeviceStatusAggregateBatch = trpc.deviceManager.getDeviceStatusAggregateBatch.useQuery;
18507
18509
  /** Generated alias around `trpc.deviceManager.getDeviceSettingsContribution.useQuery`. */
18508
18510
  var useDeviceManagerGetDeviceSettingsContribution = trpc.deviceManager.getDeviceSettingsContribution.useQuery;
18509
18511
  /** Generated alias around `trpc.deviceManager.getDeviceLiveContribution.useQuery`. */
@@ -18728,6 +18730,10 @@ var useLocalNetworkList = trpc.localNetwork.list.useQuery;
18728
18730
  var useLocalNetworkGetPreferred = trpc.localNetwork.getPreferred.useQuery;
18729
18731
  /** Generated alias around `trpc.localNetwork.getConnectionEndpoints.useQuery`. */
18730
18732
  var useLocalNetworkGetConnectionEndpoints = trpc.localNetwork.getConnectionEndpoints.useQuery;
18733
+ /** Generated alias around `trpc.localNetwork.getNotificationEndpoint.useQuery`. */
18734
+ var useLocalNetworkGetNotificationEndpoint = trpc.localNetwork.getNotificationEndpoint.useQuery;
18735
+ /** Generated alias around `trpc.localNetwork.setNotificationEndpoint.useMutation`. */
18736
+ var useLocalNetworkSetNotificationEndpoint = trpc.localNetwork.setNotificationEndpoint.useMutation;
18731
18737
  /** Generated alias around `trpc.localNetwork.getAllowedAddresses.useQuery`. */
18732
18738
  var useLocalNetworkGetAllowedAddresses = trpc.localNetwork.getAllowedAddresses.useQuery;
18733
18739
  /** Generated alias around `trpc.localNetwork.setAllowedAddresses.useMutation`. */
@@ -19000,6 +19006,12 @@ var usePipelineAnalyticsGetEventStoreFootprint = trpc.pipelineAnalytics.getEvent
19000
19006
  var usePipelineAnalyticsPruneEvents = trpc.pipelineAnalytics.pruneEvents.useMutation;
19001
19007
  /** Generated alias around `trpc.pipelineAnalytics.deleteDeviceEvents.useMutation`. */
19002
19008
  var usePipelineAnalyticsDeleteDeviceEvents = trpc.pipelineAnalytics.deleteDeviceEvents.useMutation;
19009
+ /** Generated alias around `trpc.pipelineAnalytics.relocateMedia.useMutation`. */
19010
+ var usePipelineAnalyticsRelocateMedia = trpc.pipelineAnalytics.relocateMedia.useMutation;
19011
+ /** Generated alias around `trpc.pipelineAnalytics.getMediaRelocateStatus.useQuery`. */
19012
+ var usePipelineAnalyticsGetMediaRelocateStatus = trpc.pipelineAnalytics.getMediaRelocateStatus.useQuery;
19013
+ /** Generated alias around `trpc.pipelineAnalytics.cancelMediaRelocate.useMutation`. */
19014
+ var usePipelineAnalyticsCancelMediaRelocate = trpc.pipelineAnalytics.cancelMediaRelocate.useMutation;
19003
19015
  /** Generated alias around `trpc.pipelineAnalytics.listOpsLog.useQuery`. */
19004
19016
  var usePipelineAnalyticsListOpsLog = trpc.pipelineAnalytics.listOpsLog.useQuery;
19005
19017
  /** Generated alias around `trpc.pipelineAnalytics.getEventMedia.useQuery`. */
@@ -19296,6 +19308,16 @@ var useRecordingPruneFootage = trpc.recording.pruneFootage.useMutation;
19296
19308
  var useRecordingDeleteFootprint = trpc.recording.deleteFootprint.useMutation;
19297
19309
  /** Generated alias around `trpc.recording.listOpsLog.useQuery`. */
19298
19310
  var useRecordingListOpsLog = trpc.recording.listOpsLog.useQuery;
19311
+ /** Generated alias around `trpc.recording.renderGif.useMutation`. */
19312
+ var useRecordingRenderGif = trpc.recording.renderGif.useMutation;
19313
+ /** Generated alias around `trpc.recording.renderClip.useMutation`. */
19314
+ var useRecordingRenderClip = trpc.recording.renderClip.useMutation;
19315
+ /** Generated alias around `trpc.recording.relocateFootage.useMutation`. */
19316
+ var useRecordingRelocateFootage = trpc.recording.relocateFootage.useMutation;
19317
+ /** Generated alias around `trpc.recording.getRelocateStatus.useQuery`. */
19318
+ var useRecordingGetRelocateStatus = trpc.recording.getRelocateStatus.useQuery;
19319
+ /** Generated alias around `trpc.recording.cancelRelocate.useMutation`. */
19320
+ var useRecordingCancelRelocate = trpc.recording.cancelRelocate.useMutation;
19299
19321
  /** Generated alias around `trpc.recording.getStatus.useQuery`. */
19300
19322
  var useRecordingGetStatus = trpc.recording.getStatus.useQuery;
19301
19323
  /** Generated alias around `trpc.recording.getDeviceSettingsContribution.useQuery`. */
@@ -19436,6 +19458,8 @@ var useStreamBrokerRetractCameraStream = trpc.streamBroker.retractCameraStream.u
19436
19458
  var useStreamBrokerAssignProfile = trpc.streamBroker.assignProfile.useMutation;
19437
19459
  /** Generated alias around `trpc.streamBroker.unassignProfile.useMutation`. */
19438
19460
  var useStreamBrokerUnassignProfile = trpc.streamBroker.unassignProfile.useMutation;
19461
+ /** Generated alias around `trpc.streamBroker.renderPreBufferClip.useMutation`. */
19462
+ var useStreamBrokerRenderPreBufferClip = trpc.streamBroker.renderPreBufferClip.useMutation;
19439
19463
  /** Generated alias around `trpc.streamBroker.listAllCameraStreams.useQuery`. */
19440
19464
  var useStreamBrokerListAllCameraStreams = trpc.streamBroker.listAllCameraStreams.useQuery;
19441
19465
  /** Generated alias around `trpc.streamBroker.listAllProfileSlots.useQuery`. */
@@ -34735,315 +34759,108 @@ function AutotrackSection({ deviceId }) {
34735
34759
  }
34736
34760
  //#endregion
34737
34761
  //#region src/composites/cap-settings/recording-config-form.ts
34738
- function emptyRule() {
34762
+ /** Every weekday — the `days` of a band that covers the whole week. */
34763
+ var ALL_WEEKDAYS = [
34764
+ 0,
34765
+ 1,
34766
+ 2,
34767
+ 3,
34768
+ 4,
34769
+ 5,
34770
+ 6
34771
+ ];
34772
+ /** The full-day span of an always-on band (`HH:MM`, 23:59 = end of day). */
34773
+ var ALL_DAY_START = "00:00";
34774
+ var ALL_DAY_END = "23:59";
34775
+ /** A band covering every day, all day, in `mode` — what the Base tab authors. */
34776
+ function alwaysBand(mode, rest) {
34739
34777
  return {
34740
- schedule: { kind: "always" },
34741
- mode: "continuous",
34742
- preBufferSec: 0,
34743
- postBufferSec: 0,
34744
- resetTimeoutOnNewEvent: true
34778
+ days: [...ALL_WEEKDAYS],
34779
+ start: ALL_DAY_START,
34780
+ end: ALL_DAY_END,
34781
+ mode,
34782
+ ...rest
34745
34783
  };
34746
34784
  }
34747
- function isTriggerMode(mode) {
34748
- return mode === "onMotion" || mode === "onAudioThreshold";
34785
+ /** A fresh row for the Advanced band table: every day, office hours, continuous. */
34786
+ function emptyBand() {
34787
+ return {
34788
+ days: [...ALL_WEEKDAYS],
34789
+ start: "09:00",
34790
+ end: "18:00",
34791
+ mode: "continuous"
34792
+ };
34749
34793
  }
34750
- function withRuleAt(rules, index, patch) {
34751
- return rules.map((r, i) => i === index ? {
34752
- ...r,
34794
+ function withBandAt(bands, index, patch) {
34795
+ return bands.map((band, i) => i === index ? {
34796
+ ...band,
34753
34797
  ...patch
34754
- } : r);
34798
+ } : band);
34799
+ }
34800
+ function removeBandAt(bands, index) {
34801
+ return bands.filter((_, i) => i !== index);
34755
34802
  }
34756
- function removeRuleAt(rules, index) {
34757
- return rules.filter((_, i) => i !== index);
34803
+ /** Does this band cover the whole week, all day? (the Base-tab shape) */
34804
+ function isAlwaysBand(band) {
34805
+ return (band.days.length === 0 || ALL_WEEKDAYS.every((d) => band.days.includes(d))) && band.start === "00:00" && band.end === "23:59";
34758
34806
  }
34759
- /** Recording bands normalized: explicit `schedules[]` wins, the deprecated
34760
- * singular `schedule` is lifted, absent = empty (= always). */
34761
- function bandsOf(cfg) {
34762
- if (cfg.schedules && cfg.schedules.length > 0) return [...cfg.schedules];
34763
- if (cfg.schedule) return [cfg.schedule];
34764
- return [];
34807
+ /**
34808
+ * A config is "advanced" when its bands cannot be expressed by the Base tab —
34809
+ * i.e. more than one band, or a single band restricted in time or weekday. Such
34810
+ * a config opens directly in the Advanced band table so nothing is silently
34811
+ * flattened by the simpler form.
34812
+ */
34813
+ function isAdvancedConfig(cfg) {
34814
+ if (cfg.bands.length > 1) return true;
34815
+ const [only] = cfg.bands;
34816
+ return only !== void 0 && !isAlwaysBand(only);
34765
34817
  }
34766
- /** Read a persisted/migrated config into the Basic form state. `mode` is read
34767
- * directly; a legacy mode-less config falls back to continuous/off by `enabled`. */
34818
+ /**
34819
+ * Read a persisted config into the Basic form state. `mode` is DERIVED from the
34820
+ * bands by the same function the recorder stamps `config.mode` with; the
34821
+ * `events` parameters come from the first events band, so re-opening the form
34822
+ * shows what is actually recorded.
34823
+ */
34768
34824
  function formStateFromConfig(cfg) {
34825
+ const eventsBand = cfg.bands.find((band) => band.mode === "events");
34769
34826
  return {
34770
- mode: cfg.mode ?? (cfg.enabled ? "continuous" : "off"),
34827
+ mode: (0, _camstack_types.deriveRecordingMode)(cfg),
34771
34828
  profiles: cfg.profiles,
34772
34829
  segmentSeconds: cfg.segmentSeconds,
34773
- schedules: bandsOf(cfg),
34774
- triggers: cfg.triggers ?? {},
34775
- preBufferSec: cfg.preBufferSec,
34776
- postBufferSec: cfg.postBufferSec,
34830
+ triggers: eventsBand?.triggers ?? {},
34831
+ preBufferSec: eventsBand?.preBufferSec,
34832
+ postBufferSec: eventsBand?.postBufferSec,
34777
34833
  retention: cfg.retention,
34778
34834
  scrubThumbnails: cfg.scrubThumbnails
34779
34835
  };
34780
34836
  }
34781
- /** Serialize the Basic form state back to the wire config. `enabled` is derived
34782
- * from `mode`; `rules`/singular `schedule` are never emitted. Schedules,
34783
- * triggers and buffers are preserved even when off so they survive a re-enable. */
34837
+ /**
34838
+ * Serialize the Basic form state to the wire config: one always-on band of the
34839
+ * chosen mode, none when `off`. `enabled` follows the mode; `mode` itself is
34840
+ * NOT sent — the recorder stamps it as the derived summary of the bands.
34841
+ */
34784
34842
  function configFromFormState(state) {
34785
34843
  return {
34786
34844
  enabled: state.mode !== "off",
34787
- mode: state.mode,
34845
+ bands: bandsFromFormState(state),
34788
34846
  profiles: state.profiles ? [...state.profiles] : void 0,
34789
34847
  segmentSeconds: state.segmentSeconds,
34790
- schedules: state.schedules.length > 0 ? [...state.schedules] : void 0,
34791
- triggers: Object.keys(state.triggers).length > 0 ? state.triggers : void 0,
34792
- preBufferSec: state.preBufferSec,
34793
- postBufferSec: state.postBufferSec,
34794
34848
  retention: state.retention,
34795
34849
  scrubThumbnails: state.scrubThumbnails
34796
34850
  };
34797
34851
  }
34798
- /**
34799
- * A config is "advanced" when it carries `rules[]` that the backend could NOT
34800
- * cleanly collapse to a `mode` (the non-destructive `migrateRulesToMode` leaves
34801
- * `mode` absent). The settings UI opens such a config in the Advanced rules tab.
34802
- */
34803
- function isAdvancedConfig(cfg) {
34804
- return cfg.mode == null && (cfg.rules?.length ?? 0) > 0;
34805
- }
34806
- //#endregion
34807
- //#region src/composites/cap-settings/RecordingRulesEditor.tsx
34808
- var DAYS = [
34809
- {
34810
- d: 0,
34811
- label: "S"
34812
- },
34813
- {
34814
- d: 1,
34815
- label: "M"
34816
- },
34817
- {
34818
- d: 2,
34819
- label: "T"
34820
- },
34821
- {
34822
- d: 3,
34823
- label: "W"
34824
- },
34825
- {
34826
- d: 4,
34827
- label: "T"
34828
- },
34829
- {
34830
- d: 5,
34831
- label: "F"
34832
- },
34833
- {
34834
- d: 6,
34835
- label: "S"
34836
- }
34837
- ];
34838
- /** Converts a select string value to RecordingMode without a cast. */
34839
- function toMode(v) {
34840
- return v === "onMotion" || v === "onAudioThreshold" ? v : "continuous";
34841
- }
34842
- /**
34843
- * The rule patch to apply when the mode changes, normalising the fields that
34844
- * only apply to certain modes so a saved rule never carries stale values:
34845
- * `continuous` drops the pre/post buffers + threshold; trigger modes drop the
34846
- * threshold unless it's `onAudioThreshold`, which is seeded to -30 dBFS when
34847
- * unset so the displayed default is the value actually persisted.
34848
- */
34849
- function modePatch(rule, mode) {
34850
- if (mode === "onAudioThreshold") return {
34851
- mode,
34852
- thresholdDbfs: rule.thresholdDbfs ?? -30
34853
- };
34854
- if (mode === "onMotion") return {
34855
- mode,
34856
- thresholdDbfs: void 0
34857
- };
34858
- return {
34859
- mode,
34860
- preBufferSec: 0,
34861
- postBufferSec: 0,
34862
- thresholdDbfs: void 0
34863
- };
34864
- }
34865
- function TimeOfDayRow({ schedule, onChange }) {
34866
- const toggleDay = (d) => {
34867
- const cur = schedule.days ?? [
34868
- 0,
34869
- 1,
34870
- 2,
34871
- 3,
34872
- 4,
34873
- 5,
34874
- 6
34875
- ];
34876
- const days = cur.includes(d) ? cur.filter((x) => x !== d) : [...cur, d].toSorted((a, b) => a - b);
34877
- onChange({
34878
- ...schedule,
34879
- days
34880
- });
34881
- };
34882
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
34883
- className: "flex flex-wrap items-center gap-2 text-xs",
34884
- children: [
34885
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
34886
- type: "time",
34887
- value: schedule.start,
34888
- onChange: (e) => onChange({
34889
- ...schedule,
34890
- start: e.target.value
34891
- }),
34892
- className: "rounded-md border border-border bg-background px-2 py-1 text-xs"
34893
- }),
34894
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "–" }),
34895
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
34896
- type: "time",
34897
- value: schedule.end,
34898
- onChange: (e) => onChange({
34899
- ...schedule,
34900
- end: e.target.value
34901
- }),
34902
- className: "rounded-md border border-border bg-background px-2 py-1 text-xs"
34903
- }),
34904
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
34905
- className: "flex gap-1",
34906
- children: DAYS.map(({ d, label }) => {
34907
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
34908
- type: "button",
34909
- onClick: () => toggleDay(d),
34910
- className: `h-6 w-6 rounded text-[10px] ${!schedule.days || schedule.days.includes(d) ? "bg-primary text-primary-foreground" : "bg-surface-hover text-foreground-subtle"}`,
34911
- children: label
34912
- }, d);
34913
- })
34914
- })
34915
- ]
34916
- });
34917
- }
34918
- function RecordingRulesEditor({ rules, onChange }) {
34919
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
34920
- className: "space-y-2",
34921
- children: [
34922
- rules.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
34923
- className: "text-xs text-foreground-subtle",
34924
- children: "No rules — recording is continuous (always) when enabled."
34925
- }) : null,
34926
- rules.map((rule, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
34927
- className: "space-y-2 rounded-lg border border-border bg-surface p-3",
34928
- children: [
34929
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
34930
- className: "flex items-center justify-between gap-2",
34931
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
34932
- className: "rounded-md border border-border bg-background px-2 py-1 text-xs",
34933
- value: rule.schedule.kind,
34934
- onChange: (e) => {
34935
- onChange(withRuleAt(rules, i, { schedule: e.target.value === "timeOfDay" ? {
34936
- kind: "timeOfDay",
34937
- start: "00:00",
34938
- end: "23:59"
34939
- } : { kind: "always" } }));
34940
- },
34941
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
34942
- value: "always",
34943
- children: "Always"
34944
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
34945
- value: "timeOfDay",
34946
- children: "Time of day"
34947
- })]
34948
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
34949
- type: "button",
34950
- onClick: () => onChange(removeRuleAt(rules, i)),
34951
- className: "text-foreground-subtle hover:text-red-500",
34952
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(X, { className: "h-4 w-4" })
34953
- })]
34954
- }),
34955
- rule.schedule.kind === "timeOfDay" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TimeOfDayRow, {
34956
- schedule: rule.schedule,
34957
- onChange: (schedule) => onChange(withRuleAt(rules, i, { schedule }))
34958
- }) : null,
34959
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
34960
- className: "flex flex-wrap items-center gap-2 text-xs",
34961
- children: [
34962
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
34963
- className: "rounded-md border border-border bg-background px-2 py-1 text-xs",
34964
- value: rule.mode,
34965
- onChange: (e) => onChange(withRuleAt(rules, i, modePatch(rule, toMode(e.target.value)))),
34966
- children: [
34967
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
34968
- value: "continuous",
34969
- children: "Continuous"
34970
- }),
34971
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
34972
- value: "onMotion",
34973
- children: "On motion"
34974
- }),
34975
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
34976
- value: "onAudioThreshold",
34977
- children: "On audio"
34978
- })
34979
- ]
34980
- }),
34981
- isTriggerMode(rule.mode) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
34982
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
34983
- className: "flex items-center gap-1",
34984
- children: [
34985
- "pre",
34986
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
34987
- type: "number",
34988
- min: 0,
34989
- value: rule.preBufferSec,
34990
- onChange: (e) => onChange(withRuleAt(rules, i, { preBufferSec: Number(e.target.value) })),
34991
- className: "w-14 rounded-md border border-border bg-background px-1 py-1 text-xs"
34992
- }),
34993
- "s"
34994
- ]
34995
- }),
34996
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
34997
- className: "flex items-center gap-1",
34998
- children: [
34999
- "post",
35000
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
35001
- type: "number",
35002
- min: 0,
35003
- value: rule.postBufferSec,
35004
- onChange: (e) => onChange(withRuleAt(rules, i, { postBufferSec: Number(e.target.value) })),
35005
- className: "w-14 rounded-md border border-border bg-background px-1 py-1 text-xs"
35006
- }),
35007
- "s"
35008
- ]
35009
- }),
35010
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
35011
- className: "flex items-center gap-1",
35012
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
35013
- type: "checkbox",
35014
- checked: rule.resetTimeoutOnNewEvent,
35015
- onChange: (e) => onChange(withRuleAt(rules, i, { resetTimeoutOnNewEvent: e.target.checked }))
35016
- }), "reset"]
35017
- })
35018
- ] }) : null,
35019
- rule.mode === "onAudioThreshold" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
35020
- className: "flex items-center gap-1",
35021
- children: [
35022
- "thr",
35023
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
35024
- type: "number",
35025
- value: rule.thresholdDbfs ?? -30,
35026
- onChange: (e) => onChange(withRuleAt(rules, i, { thresholdDbfs: Number(e.target.value) })),
35027
- className: "w-16 rounded-md border border-border bg-background px-1 py-1 text-xs"
35028
- }),
35029
- "dBFS"
35030
- ]
35031
- }) : null
35032
- ]
35033
- })
35034
- ]
35035
- }, i)),
35036
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
35037
- type: "button",
35038
- onClick: () => onChange([...rules, emptyRule()]),
35039
- className: "flex items-center gap-1 rounded-md border border-dashed border-border px-3 py-1.5 text-xs text-foreground-subtle hover:border-primary hover:text-foreground",
35040
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Plus, { className: "h-3.5 w-3.5" }), " Add rule"]
35041
- })
35042
- ]
35043
- });
34852
+ /** The Base tab's bands: one always-on band of the chosen mode (none = off). */
34853
+ function bandsFromFormState(state) {
34854
+ if (state.mode === "off") return [];
34855
+ if (state.mode === "continuous") return [alwaysBand("continuous")];
34856
+ return [alwaysBand("events", {
34857
+ ...Object.keys(state.triggers).length > 0 ? { triggers: state.triggers } : {},
34858
+ ...state.preBufferSec !== void 0 ? { preBufferSec: state.preBufferSec } : {},
34859
+ ...state.postBufferSec !== void 0 ? { postBufferSec: state.postBufferSec } : {}
34860
+ })];
35044
34861
  }
35045
34862
  //#endregion
35046
- //#region src/composites/cap-settings/ScheduleBandsEditor.tsx
34863
+ //#region src/composites/cap-settings/RecordingBandsEditor.tsx
35047
34864
  /** Weekday chips in display order (Mon→Sun); value is `Date.getDay()` (0=Sun). */
35048
34865
  var WEEKDAYS = [
35049
34866
  {
@@ -35075,115 +34892,177 @@ var WEEKDAYS = [
35075
34892
  label: "Sun"
35076
34893
  }
35077
34894
  ];
35078
- function newBand() {
35079
- return {
35080
- kind: "timeOfDay",
35081
- start: "09:00",
35082
- end: "18:00"
35083
- };
35084
- }
35085
- function ScheduleBandsEditor({ schedules, onChange }) {
35086
- const replaceAt = (i, band) => onChange(schedules.map((s, idx) => idx === i ? band : s));
35087
- const removeAt = (i) => onChange(schedules.filter((_, idx) => idx !== i));
35088
- const addBand = () => onChange([...schedules, newBand()]);
34895
+ var BAND_MODES = [{
34896
+ value: "continuous",
34897
+ label: "Continuous"
34898
+ }, {
34899
+ value: "events",
34900
+ label: "Events"
34901
+ }];
34902
+ /**
34903
+ * The band table the ONLY authoring surface for recording intent since the
34904
+ * legacy mode/schedules/triggers/rules model was retired (2026-07-30).
34905
+ *
34906
+ * Each row is one `RecordingBand`: when it applies (weekdays + `HH:MM` span,
34907
+ * `end <= start` wraps past midnight) and how it records (continuous, or events
34908
+ * with its own triggers and pre/post buffers). No band covering *now* = nothing
34909
+ * is recorded then; "off" is the absence of a band, never a band value.
34910
+ */
34911
+ function RecordingBandsEditor({ bands, onChange }) {
35089
34912
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35090
34913
  className: "space-y-2",
35091
- children: [schedules.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
34914
+ children: [bands.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
35092
34915
  className: "text-[11px] text-foreground-subtle",
35093
- children: "Always (24/7)add a band to restrict hours."
35094
- }) : schedules.map((band, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BandRow, {
34916
+ children: "No bandsnothing is recorded. Add one to start."
34917
+ }) : bands.map((band, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BandRow, {
35095
34918
  band,
35096
- onChange: (b) => replaceAt(i, b),
35097
- onRemove: () => removeAt(i)
34919
+ onChange: (patch) => onChange(withBandAt(bands, i, patch)),
34920
+ onRemove: () => onChange(removeBandAt(bands, i))
35098
34921
  }, i)), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
35099
34922
  type: "button",
35100
- onClick: addBand,
34923
+ onClick: () => onChange([...bands, emptyBand()]),
35101
34924
  className: "flex items-center gap-1 rounded-md bg-surface-hover px-2 py-1 text-[11px] text-foreground-subtle hover:text-foreground",
35102
34925
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Plus, { className: "h-3 w-3" }), " Add band"]
35103
34926
  })]
35104
34927
  });
35105
34928
  }
34929
+ function numOrUndef(value) {
34930
+ return value === "" ? void 0 : Number(value);
34931
+ }
35106
34932
  function BandRow({ band, onChange, onRemove }) {
35107
- if (band.kind === "always") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35108
- className: "flex items-center gap-2 text-xs",
35109
- children: [
35110
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35111
- className: "flex-1",
35112
- children: "Always"
35113
- }),
35114
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
35115
- type: "button",
35116
- onClick: () => onChange(newBand()),
35117
- className: "text-foreground-subtle hover:text-foreground underline",
35118
- children: "Set band"
35119
- }),
35120
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
35121
- type: "button",
35122
- onClick: onRemove,
35123
- "aria-label": "Remove band",
35124
- className: "text-foreground-subtle hover:text-red-500",
35125
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Trash2, { className: "h-3.5 w-3.5" })
35126
- })
35127
- ]
35128
- });
35129
- const days = band.days ?? [];
35130
34933
  const toggleDay = (value) => {
35131
- const next = days.includes(value) ? days.filter((d) => d !== value) : [...days, value];
35132
- onChange({
35133
- ...band,
35134
- days: next.length > 0 ? next : void 0
35135
- });
34934
+ const current = band.days.length === 0 ? WEEKDAYS.map((d) => d.value) : band.days;
34935
+ onChange({ days: current.includes(value) ? current.filter((d) => d !== value) : [...current, value] });
35136
34936
  };
34937
+ const setTriggers = (patch) => onChange({ triggers: {
34938
+ ...band.triggers,
34939
+ ...patch
34940
+ } });
35137
34941
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35138
34942
  className: "space-y-1.5 rounded-md border border-border p-2",
35139
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35140
- className: "flex items-center gap-2 text-xs",
35141
- children: [
35142
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
35143
- type: "time",
35144
- value: band.start,
35145
- onChange: (e) => onChange({
35146
- ...band,
35147
- start: e.target.value
34943
+ children: [
34944
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
34945
+ className: "flex flex-wrap items-center gap-2 text-xs",
34946
+ children: [
34947
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
34948
+ type: "time",
34949
+ "aria-label": "Band start",
34950
+ value: band.start,
34951
+ onChange: (e) => onChange({ start: e.target.value }),
34952
+ className: "rounded-md border border-border bg-background px-1 py-0.5"
35148
34953
  }),
35149
- className: "rounded-md border border-border bg-background px-1 py-0.5"
35150
- }),
35151
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35152
- className: "text-foreground-subtle",
35153
- children: "→"
35154
- }),
35155
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
35156
- type: "time",
35157
- value: band.end,
35158
- onChange: (e) => onChange({
35159
- ...band,
35160
- end: e.target.value
34954
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
34955
+ className: "text-foreground-subtle",
34956
+ children: ""
35161
34957
  }),
35162
- className: "rounded-md border border-border bg-background px-1 py-0.5"
35163
- }),
35164
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "flex-1" }),
35165
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
35166
- type: "button",
35167
- onClick: onRemove,
35168
- "aria-label": "Remove band",
35169
- className: "text-foreground-subtle hover:text-red-500",
35170
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Trash2, { className: "h-3.5 w-3.5" })
35171
- })
35172
- ]
35173
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35174
- className: "flex flex-wrap gap-1",
35175
- children: [WEEKDAYS.map((d) => {
35176
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
35177
- type: "button",
35178
- onClick: () => toggleDay(d.value),
35179
- className: `rounded px-1.5 py-0.5 text-[10px] ${days.includes(d.value) ? "bg-primary text-primary-foreground" : "bg-surface-hover text-foreground-subtle"}`,
35180
- children: d.label
35181
- }, d.value);
35182
- }), days.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35183
- className: "self-center text-[10px] text-foreground-subtle",
35184
- children: "every day"
35185
- }) : null]
35186
- })]
34958
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
34959
+ type: "time",
34960
+ "aria-label": "Band end",
34961
+ value: band.end,
34962
+ onChange: (e) => onChange({ end: e.target.value }),
34963
+ className: "rounded-md border border-border bg-background px-1 py-0.5"
34964
+ }),
34965
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
34966
+ className: "flex gap-1",
34967
+ children: BAND_MODES.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
34968
+ type: "button",
34969
+ onClick: () => onChange({ mode: m.value }),
34970
+ className: `rounded px-2 py-0.5 ${band.mode === m.value ? "bg-primary text-primary-foreground" : "bg-surface-hover text-foreground-subtle"}`,
34971
+ children: m.label
34972
+ }, m.value))
34973
+ }),
34974
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "flex-1" }),
34975
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
34976
+ type: "button",
34977
+ onClick: onRemove,
34978
+ "aria-label": "Remove band",
34979
+ className: "text-foreground-subtle hover:text-red-500",
34980
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Trash2, { className: "h-3.5 w-3.5" })
34981
+ })
34982
+ ]
34983
+ }),
34984
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
34985
+ className: "flex flex-wrap gap-1",
34986
+ children: [WEEKDAYS.map((d) => {
34987
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
34988
+ type: "button",
34989
+ onClick: () => toggleDay(d.value),
34990
+ className: `rounded px-1.5 py-0.5 text-[10px] ${band.days.length === 0 || band.days.includes(d.value) ? "bg-primary text-primary-foreground" : "bg-surface-hover text-foreground-subtle"}`,
34991
+ children: d.label
34992
+ }, d.value);
34993
+ }), band.days.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
34994
+ className: "self-center text-[10px] text-foreground-subtle",
34995
+ children: "every day"
34996
+ }) : null]
34997
+ }),
34998
+ band.mode === "events" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
34999
+ className: "space-y-1.5 border-t border-border pt-1.5",
35000
+ children: [
35001
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
35002
+ className: "flex items-center gap-2 text-xs",
35003
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
35004
+ type: "checkbox",
35005
+ checked: band.triggers?.motion ?? false,
35006
+ onChange: (e) => setTriggers({ motion: e.target.checked ? true : void 0 })
35007
+ }), "Motion"]
35008
+ }),
35009
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
35010
+ className: "flex items-center gap-2 text-xs",
35011
+ children: [
35012
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
35013
+ type: "checkbox",
35014
+ checked: band.triggers?.audioThresholdDbfs != null,
35015
+ onChange: (e) => setTriggers({ audioThresholdDbfs: e.target.checked ? -30 : void 0 })
35016
+ }),
35017
+ "Audio above threshold",
35018
+ band.triggers?.audioThresholdDbfs != null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
35019
+ type: "number",
35020
+ "aria-label": "Audio threshold dBFS",
35021
+ value: band.triggers.audioThresholdDbfs,
35022
+ onChange: (e) => setTriggers({ audioThresholdDbfs: numOrUndef(e.target.value) }),
35023
+ className: "w-16 rounded-md border border-border bg-background px-1 py-0.5 text-xs"
35024
+ }) : null,
35025
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35026
+ className: "text-foreground-subtle",
35027
+ children: "dBFS"
35028
+ })
35029
+ ]
35030
+ }),
35031
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35032
+ className: "flex flex-wrap items-center gap-3 text-xs",
35033
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
35034
+ className: "flex items-center gap-1",
35035
+ children: [
35036
+ "Pre-buffer",
35037
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
35038
+ type: "number",
35039
+ min: 0,
35040
+ value: band.preBufferSec ?? "",
35041
+ placeholder: "0",
35042
+ onChange: (e) => onChange({ preBufferSec: numOrUndef(e.target.value) }),
35043
+ className: "w-16 rounded-md border border-border bg-background px-1 py-1 text-xs"
35044
+ }),
35045
+ "s"
35046
+ ]
35047
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
35048
+ className: "flex items-center gap-1",
35049
+ children: [
35050
+ "Post-buffer",
35051
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
35052
+ type: "number",
35053
+ min: 0,
35054
+ value: band.postBufferSec ?? "",
35055
+ placeholder: "0",
35056
+ onChange: (e) => onChange({ postBufferSec: numOrUndef(e.target.value) }),
35057
+ className: "w-16 rounded-md border border-border bg-background px-1 py-1 text-xs"
35058
+ }),
35059
+ "s"
35060
+ ]
35061
+ })]
35062
+ })
35063
+ ]
35064
+ }) : null
35065
+ ]
35187
35066
  });
35188
35067
  }
35189
35068
  //#endregion
@@ -35207,16 +35086,15 @@ var MODES = [
35207
35086
  label: "Continuous"
35208
35087
  }
35209
35088
  ];
35210
- function RecordingSettings({ initial, saving, onSave }) {
35089
+ function RecordingSettings({ initial, saving, onSave, onRegenerateStrips, regeneratingStrips }) {
35211
35090
  const [tab, setTab] = (0, react$1.useState)(isAdvancedConfig(initial) ? "advanced" : "base");
35212
35091
  const [base, setBase] = (0, react$1.useState)(formStateFromConfig(initial));
35213
- const [advEnabled, setAdvEnabled] = (0, react$1.useState)(initial.enabled);
35214
- const [rules, setRules] = (0, react$1.useState)(initial.rules ? [...initial.rules] : []);
35092
+ const [bands, setBands] = (0, react$1.useState)([...initial.bands]);
35215
35093
  const [common, setCommon] = (0, react$1.useState)({
35216
35094
  profiles: initial.profiles,
35217
35095
  segmentSeconds: initial.segmentSeconds,
35218
35096
  retention: initial.retention,
35219
- scrubThumbnails: initial.scrubThumbnails
35097
+ stripsEnabled: initial.stripsEnabled
35220
35098
  });
35221
35099
  const ret = common.retention ?? {};
35222
35100
  const setRet = (patch) => setCommon({
@@ -35227,10 +35105,6 @@ function RecordingSettings({ initial, saving, onSave }) {
35227
35105
  }
35228
35106
  });
35229
35107
  const numOrUndef = (v) => v === "" ? void 0 : Number(v);
35230
- const parsePreset = (v) => {
35231
- const parsed = _camstack_types.ScrubThumbnailPresetSchema.safeParse(v);
35232
- return parsed.success ? parsed.data : _camstack_types.DEFAULT_SCRUB_THUMBNAIL_PRESET;
35233
- };
35234
35108
  const toggleProfile = (p) => {
35235
35109
  const cur = common.profiles ?? [...PROFILES];
35236
35110
  const next = cur.includes(p) ? cur.filter((x) => x !== p) : [...cur, p];
@@ -35247,24 +35121,31 @@ function RecordingSettings({ initial, saving, onSave }) {
35247
35121
  }
35248
35122
  });
35249
35123
  const switchTab = (next) => {
35250
- if (next === "advanced" && rules.length === 0) setRules([emptyRule()]);
35124
+ if (next === "advanced") setBands(bandsFromFormState(base));
35125
+ else setBase(formStateFromConfig({
35126
+ enabled: bands.length > 0,
35127
+ bands
35128
+ }));
35251
35129
  setTab(next);
35252
35130
  };
35253
35131
  const handleSave = () => {
35254
- if (tab === "base") onSave(configFromFormState({
35255
- ...base,
35256
- profiles: common.profiles,
35257
- segmentSeconds: common.segmentSeconds,
35258
- retention: common.retention,
35259
- scrubThumbnails: common.scrubThumbnails
35260
- }));
35261
- else onSave({
35262
- enabled: advEnabled,
35263
- rules,
35132
+ const shared = {
35264
35133
  profiles: common.profiles ? [...common.profiles] : void 0,
35265
35134
  segmentSeconds: common.segmentSeconds,
35266
35135
  retention: common.retention,
35267
- scrubThumbnails: common.scrubThumbnails
35136
+ ...common.stripsEnabled === void 0 ? {} : { stripsEnabled: common.stripsEnabled }
35137
+ };
35138
+ if (tab === "base") onSave({
35139
+ ...configFromFormState({
35140
+ ...base,
35141
+ ...shared
35142
+ }),
35143
+ ...shared
35144
+ });
35145
+ else onSave({
35146
+ enabled: bands.length > 0,
35147
+ bands,
35148
+ ...shared
35268
35149
  });
35269
35150
  };
35270
35151
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -35276,7 +35157,7 @@ function RecordingSettings({ initial, saving, onSave }) {
35276
35157
  type: "button",
35277
35158
  onClick: () => switchTab(t),
35278
35159
  className: `rounded px-2.5 py-1 ${tab === t ? "bg-primary text-primary-foreground" : "bg-surface-hover text-foreground-subtle"}`,
35279
- children: t === "base" ? "Base" : "Advanced (rules)"
35160
+ children: t === "base" ? "Base" : "Advanced (bands)"
35280
35161
  }, t))
35281
35162
  }),
35282
35163
  tab === "base" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -35297,16 +35178,17 @@ function RecordingSettings({ initial, saving, onSave }) {
35297
35178
  children: m.label
35298
35179
  }, m.value))]
35299
35180
  }),
35300
- base.mode !== "off" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
35301
- className: "mb-1.5 text-[11px] font-semibold uppercase tracking-wider text-foreground-subtle",
35302
- children: "Time bands"
35303
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScheduleBandsEditor, {
35304
- schedules: base.schedules,
35305
- onChange: (schedules) => setBase({
35306
- ...base,
35307
- schedules
35308
- })
35309
- })] }) : null,
35181
+ base.mode !== "off" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
35182
+ className: "text-[11px] text-foreground-subtle",
35183
+ children: [
35184
+ "Records 24/7. Use ",
35185
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35186
+ className: "font-medium",
35187
+ children: "Advanced (bands)"
35188
+ }),
35189
+ " to restrict hours or weekdays, or to mix continuous and event windows."
35190
+ ]
35191
+ }) : null,
35310
35192
  base.mode === "events" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35311
35193
  className: "space-y-2",
35312
35194
  children: [
@@ -35384,22 +35266,22 @@ function RecordingSettings({ initial, saving, onSave }) {
35384
35266
  ]
35385
35267
  }) : null
35386
35268
  ]
35387
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35269
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
35388
35270
  className: "space-y-4",
35389
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
35390
- className: "flex items-center gap-2 text-xs",
35391
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
35392
- type: "checkbox",
35393
- checked: advEnabled,
35394
- onChange: (e) => setAdvEnabled(e.target.checked)
35395
- }), "Record this camera"]
35396
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
35397
- className: "mb-1.5 text-[11px] font-semibold uppercase tracking-wider text-foreground-subtle",
35398
- children: "Rules"
35399
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RecordingRulesEditor, {
35400
- rules,
35401
- onChange: setRules
35402
- })] })]
35271
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
35272
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
35273
+ className: "mb-1.5 text-[11px] font-semibold uppercase tracking-wider text-foreground-subtle",
35274
+ children: "Recording bands"
35275
+ }),
35276
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RecordingBandsEditor, {
35277
+ bands,
35278
+ onChange: setBands
35279
+ }),
35280
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
35281
+ className: "mt-1.5 text-[11px] text-foreground-subtle",
35282
+ children: "A band says WHEN to record and HOW. Nothing is recorded outside every band — an empty table means this camera is off."
35283
+ })
35284
+ ] })
35403
35285
  }),
35404
35286
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35405
35287
  className: "flex flex-wrap items-center gap-4 text-xs",
@@ -35438,27 +35320,30 @@ function RecordingSettings({ initial, saving, onSave }) {
35438
35320
  }),
35439
35321
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
35440
35322
  className: "flex items-center gap-1",
35441
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35442
- className: "text-foreground-subtle",
35443
- children: "Scrub preview quality"
35444
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
35445
- value: common.scrubThumbnails ?? _camstack_types.DEFAULT_SCRUB_THUMBNAIL_PRESET,
35323
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
35324
+ type: "checkbox",
35325
+ checked: common.stripsEnabled === true,
35446
35326
  onChange: (e) => setCommon({
35447
35327
  ...common,
35448
- scrubThumbnails: parsePreset(e.target.value)
35449
- }),
35450
- className: "rounded-md border border-border bg-background px-1 py-1 text-xs",
35451
- children: _camstack_types.SCRUB_THUMBNAIL_PRESET_ORDER.map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
35452
- value: p,
35453
- children: _camstack_types.SCRUB_THUMBNAIL_PRESET_LABELS[p]
35454
- }, p))
35328
+ stripsEnabled: e.target.checked
35329
+ })
35330
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35331
+ className: "text-foreground-subtle",
35332
+ children: "Scrub thumbnail strips"
35455
35333
  })]
35456
- })
35334
+ }),
35335
+ onRegenerateStrips ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
35336
+ type: "button",
35337
+ onClick: onRegenerateStrips,
35338
+ disabled: regeneratingStrips === true,
35339
+ className: "rounded-md border border-border bg-surface-hover px-2 py-1 text-xs text-foreground-subtle transition-colors hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50",
35340
+ children: regeneratingStrips === true ? "Regenerating…" : "Regenerate strips"
35341
+ }) : null
35457
35342
  ]
35458
35343
  }),
35459
35344
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
35460
35345
  className: "text-[11px] text-foreground-subtle",
35461
- children: "Timeline scrub-preview resolution + quality. Higher = crisper previews but larger sprite sheets. Applies to NEW recordings only existing preview sheets keep their original resolution."
35346
+ children: "OPT-IN: save every keyframe of the low recording as a JPEG strip for fluid fast scrubbing. Costs disk (a derived cache, reclaimed with the footage). Regenerate clears and rebuilds the recent days from the segments already on disk."
35462
35347
  }),
35463
35348
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
35464
35349
  className: "mb-1.5 text-[11px] font-semibold uppercase tracking-wider text-foreground-subtle",
@@ -36707,6 +36592,14 @@ function RecordingPanel({ deviceId }) {
36707
36592
  const configQuery = useRecordingGetDeviceConfig({ deviceId });
36708
36593
  const setConfig = useRecordingSetDeviceConfig();
36709
36594
  const rescanStorage = useRecordingRescanStorage();
36595
+ const customAction = useAddonsCustom();
36596
+ const regenerateStrips = (0, react$1.useCallback)(() => {
36597
+ customAction.mutate({
36598
+ addonId: "recorder",
36599
+ action: "regenerateStrips",
36600
+ input: { deviceId }
36601
+ });
36602
+ }, [customAction, deviceId]);
36710
36603
  const saveConfig = (config) => {
36711
36604
  setConfig.mutate({
36712
36605
  deviceId,
@@ -36748,7 +36641,9 @@ function RecordingPanel({ deviceId }) {
36748
36641
  children: resolvedConfig ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RecordingSettings, {
36749
36642
  initial: resolvedConfig,
36750
36643
  saving: setConfig.isPending,
36751
- onSave: saveConfig
36644
+ onSave: saveConfig,
36645
+ onRegenerateStrips: regenerateStrips,
36646
+ regeneratingStrips: customAction.isPending
36752
36647
  }, JSON.stringify(resolvedConfig)) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
36753
36648
  className: "px-1 py-2 text-xs text-foreground-subtle",
36754
36649
  children: "Loading settings…"
@@ -40314,6 +40209,21 @@ function DetectionBoxes({ detections, frameWidth, frameHeight }) {
40314
40209
  }
40315
40210
  //#endregion
40316
40211
  //#region src/composites/camera-stream-player.tsx
40212
+ /**
40213
+ * Silence (or restore) a live WebRTC stream FOR REAL.
40214
+ *
40215
+ * `video.muted` alone is not enough: iOS WebKit keeps playing a
40216
+ * `MediaStream`'s audio track through a muted element, which is why live audio
40217
+ * kept sounding under recorded playback on the viewer. Gating the audio TRACK
40218
+ * (`track.enabled`) is what actually stops it, so both are set together — and
40219
+ * re-applied at every stream swap, because a freshly-attached stream arrives
40220
+ * with its tracks enabled whatever the element says.
40221
+ */
40222
+ function applyStreamMute(video, muted) {
40223
+ video.muted = muted;
40224
+ const stream = video.srcObject;
40225
+ if (stream instanceof MediaStream) for (const track of stream.getAudioTracks()) track.enabled = !muted;
40226
+ }
40317
40227
  function isTouchDevice() {
40318
40228
  if (typeof window === "undefined") return false;
40319
40229
  return window.matchMedia("(hover: none) and (pointer: coarse)").matches || typeof navigator !== "undefined" && navigator.maxTouchPoints > 0 || "ontouchstart" in window;
@@ -40396,6 +40306,11 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
40396
40306
  const [state, setState] = (0, react$1.useState)("idle");
40397
40307
  const [errorMessage, setErrorMessage] = (0, react$1.useState)("");
40398
40308
  const [isMuted, setIsMuted] = (0, react$1.useState)(initialMuted);
40309
+ /** Read by the async stream-swap closures: a freshly-promoted stream must
40310
+ * arrive already respecting the CURRENT mute, not the one captured at
40311
+ * connect time. */
40312
+ const isMutedRef = (0, react$1.useRef)(isMuted);
40313
+ isMutedRef.current = isMuted;
40399
40314
  const [isFullscreen, setIsFullscreen] = (0, react$1.useState)(false);
40400
40315
  const [isPipSupported, setIsPipSupported] = (0, react$1.useState)(false);
40401
40316
  const isTouch = isTouchDevice();
@@ -40525,6 +40440,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
40525
40440
  pcRef.current = pc;
40526
40441
  if (videoRef.current) {
40527
40442
  videoRef.current.srcObject = combinedStream;
40443
+ applyStreamMute(videoRef.current, isMutedRef.current);
40528
40444
  videoRef.current.play().catch(() => {});
40529
40445
  }
40530
40446
  if (oldPc && oldPc !== pc) try {
@@ -40544,6 +40460,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
40544
40460
  }
40545
40461
  if (videoRef.current) {
40546
40462
  videoRef.current.srcObject = combinedStream;
40463
+ applyStreamMute(videoRef.current, isMutedRef.current);
40547
40464
  videoRef.current.play().catch(() => {});
40548
40465
  }
40549
40466
  };
@@ -40630,6 +40547,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
40630
40547
  whepStream.addTrack(event.track);
40631
40548
  if (videoRef.current) {
40632
40549
  videoRef.current.srcObject = whepStream;
40550
+ applyStreamMute(videoRef.current, isMutedRef.current);
40633
40551
  videoRef.current.play().catch(() => {});
40634
40552
  }
40635
40553
  };
@@ -40702,6 +40620,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
40702
40620
  pcRef.current = pc;
40703
40621
  if (videoRef.current) {
40704
40622
  videoRef.current.srcObject = combinedStream;
40623
+ applyStreamMute(videoRef.current, isMutedRef.current);
40705
40624
  videoRef.current.play().catch(() => {});
40706
40625
  }
40707
40626
  if (oldPc && oldPc !== pc) try {
@@ -40722,6 +40641,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
40722
40641
  }
40723
40642
  if (videoRef.current) {
40724
40643
  videoRef.current.srcObject = combinedStream;
40644
+ applyStreamMute(videoRef.current, isMutedRef.current);
40725
40645
  videoRef.current.play().catch(() => {});
40726
40646
  }
40727
40647
  };
@@ -41046,7 +40966,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
41046
40966
  mountedRef.current = false;
41047
40967
  cleanup();
41048
40968
  };
41049
- }, [serverUrl, streamKey]);
40969
+ }, []);
41050
40970
  const hasSignaledRef = (0, react$1.useRef)(false);
41051
40971
  (0, react$1.useEffect)(() => {
41052
40972
  if (!autoPlay) return;
@@ -41084,6 +41004,23 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
41084
41004
  }
41085
41005
  connect();
41086
41006
  };
41007
+ const prevServerUrlRef = (0, react$1.useRef)(serverUrl);
41008
+ const prevStreamKeyRef = (0, react$1.useRef)(streamKey);
41009
+ (0, react$1.useEffect)(() => {
41010
+ const serverChanged = prevServerUrlRef.current !== serverUrl;
41011
+ const keyChanged = prevStreamKeyRef.current !== streamKey;
41012
+ prevServerUrlRef.current = serverUrl;
41013
+ prevStreamKeyRef.current = streamKey;
41014
+ if (!serverChanged && !keyChanged) return;
41015
+ if (!mountedRef.current) return;
41016
+ reconnectAttemptsRef.current = 0;
41017
+ if (serverChanged) {
41018
+ cleanup();
41019
+ connect();
41020
+ return;
41021
+ }
41022
+ handleSeamlessReconnect();
41023
+ }, [serverUrl, streamKey]);
41087
41024
  const prevReconnectSignalRef = (0, react$1.useRef)(reconnectSignal);
41088
41025
  (0, react$1.useEffect)(() => {
41089
41026
  if (prevReconnectSignalRef.current === reconnectSignal) return;
@@ -41093,8 +41030,9 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
41093
41030
  }, [reconnectSignal]);
41094
41031
  const toggleMute = () => {
41095
41032
  if (videoRef.current) {
41096
- videoRef.current.muted = !videoRef.current.muted;
41097
- setIsMuted(videoRef.current.muted);
41033
+ const next = !videoRef.current.muted;
41034
+ applyStreamMute(videoRef.current, next);
41035
+ setIsMuted(next);
41098
41036
  }
41099
41037
  };
41100
41038
  const toggleFullscreen = async () => {
@@ -41322,7 +41260,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
41322
41260
  setIsMuted(initialMuted);
41323
41261
  const video = videoRef.current;
41324
41262
  if (!video) return;
41325
- video.muted = initialMuted;
41263
+ applyStreamMute(video, initialMuted);
41326
41264
  if (!initialMuted) video.play().catch(() => {});
41327
41265
  }, [initialMuted]);
41328
41266
  const showToolbarBriefly = () => {
@@ -42434,7 +42372,7 @@ function StreamPanel({ serverUrl, createSession, sendAnswer, handleOffer, getIce
42434
42372
  extraOverlay,
42435
42373
  ptzOverlayVisible && ptzOverlay
42436
42374
  ] }) : void 0
42437
- }, effectiveStreamId)
42375
+ })
42438
42376
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SnapshotPreview, {
42439
42377
  src: snapshotSrc ?? null,
42440
42378
  isLoading: snapshotLoading,
@@ -46815,6 +46753,15 @@ function useDeviceDetections(trpc, deviceId) {
46815
46753
  * @param deviceId - numeric device ID (null = disabled)
46816
46754
  * @param pollIntervalMs - how often to refresh profile slots (default: 5000)
46817
46755
  */
46756
+ /** TTL for the TURN/STUN credential cache. Short enough that rotated creds
46757
+ * are picked up promptly, long enough to cover a connect + several profile
46758
+ * switches without re-paying the fetch. */
46759
+ var TURN_CACHE_TTL_MS = 5 * 6e4;
46760
+ /** Keyed on the caller's trpc client object (stable per connected system —
46761
+ * NOT on `trpc.turnProvider`, which is a proxy minted fresh on every property
46762
+ * access), so two admin tabs pointed at different hubs never cross-serve
46763
+ * credentials. An unstable key degrades to a cache miss, never to wrong creds. */
46764
+ var turnServersCache = /* @__PURE__ */ new WeakMap();
46818
46765
  function useDeviceWebrtc(trpc, deviceId, pollIntervalMs = 5e3) {
46819
46766
  const [remoteStreams, setRemoteStreams] = (0, react$1.useState)([]);
46820
46767
  (0, react$1.useEffect)(() => {
@@ -46884,18 +46831,28 @@ function useDeviceWebrtc(trpc, deviceId, pollIntervalMs = 5e3) {
46884
46831
  }, [deviceId, remoteStreams]);
46885
46832
  const getIceServers = (0, react$1.useCallback)(async () => {
46886
46833
  if (!trpc.turnProvider) return void 0;
46834
+ const cached = turnServersCache.get(trpc);
46835
+ if (cached && Date.now() - cached.fetchedAt < TURN_CACHE_TTL_MS) return cached.servers;
46887
46836
  try {
46888
- return (await trpc.turnProvider.getTurnServers.query()).map((s) => {
46837
+ const mapped = (await trpc.turnProvider.getTurnServers.query()).map((s) => {
46889
46838
  return {
46890
46839
  urls: typeof s.urls === "string" ? s.urls : [...s.urls],
46891
46840
  ...s.username !== void 0 ? { username: s.username } : {},
46892
46841
  ...s.credential !== void 0 ? { credential: s.credential } : {}
46893
46842
  };
46894
46843
  });
46844
+ turnServersCache.set(trpc, {
46845
+ servers: mapped,
46846
+ fetchedAt: Date.now()
46847
+ });
46848
+ return mapped;
46895
46849
  } catch {
46896
- return;
46850
+ return cached?.servers;
46897
46851
  }
46898
46852
  }, [trpc]);
46853
+ (0, react$1.useEffect)(() => {
46854
+ getIceServers();
46855
+ }, [getIceServers]);
46899
46856
  const createSession = (0, react$1.useCallback)(async (target, hints) => {
46900
46857
  if (deviceId === null) throw new Error("No device selected");
46901
46858
  const res = await trpc.webrtcSession.createSession.mutate({
@@ -47825,6 +47782,7 @@ exports.useDeviceManagerGetDeviceLiveInfoAggregate = useDeviceManagerGetDeviceLi
47825
47782
  exports.useDeviceManagerGetDeviceSettingsAggregate = useDeviceManagerGetDeviceSettingsAggregate;
47826
47783
  exports.useDeviceManagerGetDeviceSettingsContribution = useDeviceManagerGetDeviceSettingsContribution;
47827
47784
  exports.useDeviceManagerGetDeviceStatusAggregate = useDeviceManagerGetDeviceStatusAggregate;
47785
+ exports.useDeviceManagerGetDeviceStatusAggregateBatch = useDeviceManagerGetDeviceStatusAggregateBatch;
47828
47786
  exports.useDeviceManagerGetLinkedDevices = useDeviceManagerGetLinkedDevices;
47829
47787
  exports.useDeviceManagerGetRoleDisplayDefaults = useDeviceManagerGetRoleDisplayDefaults;
47830
47788
  exports.useDeviceManagerGetSettingsSchema = useDeviceManagerGetSettingsSchema;
@@ -47993,10 +47951,12 @@ exports.useLlmTestProfile = useLlmTestProfile;
47993
47951
  exports.useLlmUpsertProfile = useLlmUpsertProfile;
47994
47952
  exports.useLocalNetworkGetAllowedAddresses = useLocalNetworkGetAllowedAddresses;
47995
47953
  exports.useLocalNetworkGetConnectionEndpoints = useLocalNetworkGetConnectionEndpoints;
47954
+ exports.useLocalNetworkGetNotificationEndpoint = useLocalNetworkGetNotificationEndpoint;
47996
47955
  exports.useLocalNetworkGetPreferred = useLocalNetworkGetPreferred;
47997
47956
  exports.useLocalNetworkList = useLocalNetworkList;
47998
47957
  exports.useLocalNetworkResetAllowlistToBestMatch = useLocalNetworkResetAllowlistToBestMatch;
47999
47958
  exports.useLocalNetworkSetAllowedAddresses = useLocalNetworkSetAllowedAddresses;
47959
+ exports.useLocalNetworkSetNotificationEndpoint = useLocalNetworkSetNotificationEndpoint;
48000
47960
  exports.useLockControlGetStatus = useLockControlGetStatus;
48001
47961
  exports.useLockControlLock = useLockControlLock;
48002
47962
  exports.useLockControlOpen = useLockControlOpen;
@@ -48116,6 +48076,7 @@ exports.usePetFeederSetFeedSound = usePetFeederSetFeedSound;
48116
48076
  exports.usePetFeederSetIndicatorLight = usePetFeederSetIndicatorLight;
48117
48077
  exports.usePetFeederSetVolume = usePetFeederSetVolume;
48118
48078
  exports.usePipelineAnalyticsApplyDeviceSettingsPatch = usePipelineAnalyticsApplyDeviceSettingsPatch;
48079
+ exports.usePipelineAnalyticsCancelMediaRelocate = usePipelineAnalyticsCancelMediaRelocate;
48119
48080
  exports.usePipelineAnalyticsClearTracks = usePipelineAnalyticsClearTracks;
48120
48081
  exports.usePipelineAnalyticsDeleteDeviceEvents = usePipelineAnalyticsDeleteDeviceEvents;
48121
48082
  exports.usePipelineAnalyticsDeleteTracks = usePipelineAnalyticsDeleteTracks;
@@ -48127,6 +48088,7 @@ exports.usePipelineAnalyticsGetEventDensity = usePipelineAnalyticsGetEventDensit
48127
48088
  exports.usePipelineAnalyticsGetEventMedia = usePipelineAnalyticsGetEventMedia;
48128
48089
  exports.usePipelineAnalyticsGetEventStoreFootprint = usePipelineAnalyticsGetEventStoreFootprint;
48129
48090
  exports.usePipelineAnalyticsGetKeyEvents = usePipelineAnalyticsGetKeyEvents;
48091
+ exports.usePipelineAnalyticsGetMediaRelocateStatus = usePipelineAnalyticsGetMediaRelocateStatus;
48130
48092
  exports.usePipelineAnalyticsGetMotionEvents = usePipelineAnalyticsGetMotionEvents;
48131
48093
  exports.usePipelineAnalyticsGetObjectEvents = usePipelineAnalyticsGetObjectEvents;
48132
48094
  exports.usePipelineAnalyticsGetSensorEvents = usePipelineAnalyticsGetSensorEvents;
@@ -48139,6 +48101,7 @@ exports.usePipelineAnalyticsListTracks = usePipelineAnalyticsListTracks;
48139
48101
  exports.usePipelineAnalyticsPruneEvents = usePipelineAnalyticsPruneEvents;
48140
48102
  exports.usePipelineAnalyticsPruneEventsBefore = usePipelineAnalyticsPruneEventsBefore;
48141
48103
  exports.usePipelineAnalyticsPruneTracksBefore = usePipelineAnalyticsPruneTracksBefore;
48104
+ exports.usePipelineAnalyticsRelocateMedia = usePipelineAnalyticsRelocateMedia;
48142
48105
  exports.usePipelineAnalyticsSearchObjectEvents = usePipelineAnalyticsSearchObjectEvents;
48143
48106
  exports.usePipelineAnalyticsWipeAllAnalytics = usePipelineAnalyticsWipeAllAnalytics;
48144
48107
  exports.usePipelineExecutorCacheFrameInPool = usePipelineExecutorCacheFrameInPool;
@@ -48276,6 +48239,7 @@ exports.usePtzStop = usePtzStop;
48276
48239
  exports.useRebootReboot = useRebootReboot;
48277
48240
  exports.useRecordedPlayback = useRecordedPlayback;
48278
48241
  exports.useRecordingApplyDeviceSettingsPatch = useRecordingApplyDeviceSettingsPatch;
48242
+ exports.useRecordingCancelRelocate = useRecordingCancelRelocate;
48279
48243
  exports.useRecordingDeleteFootprint = useRecordingDeleteFootprint;
48280
48244
  exports.useRecordingExportCancelExport = useRecordingExportCancelExport;
48281
48245
  exports.useRecordingExportCreateExport = useRecordingExportCreateExport;
@@ -48289,12 +48253,16 @@ exports.useRecordingGetDeviceConfig = useRecordingGetDeviceConfig;
48289
48253
  exports.useRecordingGetDeviceLiveContribution = useRecordingGetDeviceLiveContribution;
48290
48254
  exports.useRecordingGetDeviceSettingsContribution = useRecordingGetDeviceSettingsContribution;
48291
48255
  exports.useRecordingGetPlaybackManifest = useRecordingGetPlaybackManifest;
48256
+ exports.useRecordingGetRelocateStatus = useRecordingGetRelocateStatus;
48292
48257
  exports.useRecordingGetStatus = useRecordingGetStatus;
48293
48258
  exports.useRecordingGetStorageUsage = useRecordingGetStorageUsage;
48294
48259
  exports.useRecordingListOpsLog = useRecordingListOpsLog;
48295
48260
  exports.useRecordingLocateSegment = useRecordingLocateSegment;
48296
48261
  exports.useRecordingPruneFootage = useRecordingPruneFootage;
48297
48262
  exports.useRecordingReadSegmentBytes = useRecordingReadSegmentBytes;
48263
+ exports.useRecordingRelocateFootage = useRecordingRelocateFootage;
48264
+ exports.useRecordingRenderClip = useRecordingRenderClip;
48265
+ exports.useRecordingRenderGif = useRecordingRenderGif;
48298
48266
  exports.useRecordingRescanStorage = useRecordingRescanStorage;
48299
48267
  exports.useRecordingSetDeviceConfig = useRecordingSetDeviceConfig;
48300
48268
  exports.useRemoteComponent = useRemoteComponent;
@@ -48377,6 +48345,7 @@ exports.useStreamBrokerPullAudioChunks = useStreamBrokerPullAudioChunks;
48377
48345
  exports.useStreamBrokerPullFrameHandles = useStreamBrokerPullFrameHandles;
48378
48346
  exports.useStreamBrokerRegenerateRtspToken = useStreamBrokerRegenerateRtspToken;
48379
48347
  exports.useStreamBrokerReleaseStreamWithCodec = useStreamBrokerReleaseStreamWithCodec;
48348
+ exports.useStreamBrokerRenderPreBufferClip = useStreamBrokerRenderPreBufferClip;
48380
48349
  exports.useStreamBrokerRestartProfile = useStreamBrokerRestartProfile;
48381
48350
  exports.useStreamBrokerRetractCameraStream = useStreamBrokerRetractCameraStream;
48382
48351
  exports.useStreamBrokerSetPreBufferDuration = useStreamBrokerSetPreBufferDuration;