@camstack/ui-library 1.2.11 → 1.2.13

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",
@@ -18162,6 +18162,12 @@ var useBackupListArchives = trpc.backup.listArchives.useQuery;
18162
18162
  var useBackupUpsertDestinationPolicy = trpc.backup.upsertDestinationPolicy.useMutation;
18163
18163
  /** Generated alias around `trpc.backup.previewSchedule.useQuery`. */
18164
18164
  var useBackupPreviewSchedule = trpc.backup.previewSchedule.useQuery;
18165
+ /** Generated alias around `trpc.backup.listSchedules.useQuery`. */
18166
+ var useBackupListSchedules = trpc.backup.listSchedules.useQuery;
18167
+ /** Generated alias around `trpc.backup.upsertSchedule.useMutation`. */
18168
+ var useBackupUpsertSchedule = trpc.backup.upsertSchedule.useMutation;
18169
+ /** Generated alias around `trpc.backup.deleteSchedule.useMutation`. */
18170
+ var useBackupDeleteSchedule = trpc.backup.deleteSchedule.useMutation;
18165
18171
  /** Generated alias around `trpc.battery.wakeForStream.useMutation`. */
18166
18172
  var useBatteryWakeForStream = trpc.battery.wakeForStream.useMutation;
18167
18173
  /** Generated alias around `trpc.battery.getStatus.useQuery`. */
@@ -18722,6 +18728,10 @@ var useLocalNetworkList = trpc.localNetwork.list.useQuery;
18722
18728
  var useLocalNetworkGetPreferred = trpc.localNetwork.getPreferred.useQuery;
18723
18729
  /** Generated alias around `trpc.localNetwork.getConnectionEndpoints.useQuery`. */
18724
18730
  var useLocalNetworkGetConnectionEndpoints = trpc.localNetwork.getConnectionEndpoints.useQuery;
18731
+ /** Generated alias around `trpc.localNetwork.getNotificationEndpoint.useQuery`. */
18732
+ var useLocalNetworkGetNotificationEndpoint = trpc.localNetwork.getNotificationEndpoint.useQuery;
18733
+ /** Generated alias around `trpc.localNetwork.setNotificationEndpoint.useMutation`. */
18734
+ var useLocalNetworkSetNotificationEndpoint = trpc.localNetwork.setNotificationEndpoint.useMutation;
18725
18735
  /** Generated alias around `trpc.localNetwork.getAllowedAddresses.useQuery`. */
18726
18736
  var useLocalNetworkGetAllowedAddresses = trpc.localNetwork.getAllowedAddresses.useQuery;
18727
18737
  /** Generated alias around `trpc.localNetwork.setAllowedAddresses.useMutation`. */
@@ -18994,6 +19004,12 @@ var usePipelineAnalyticsGetEventStoreFootprint = trpc.pipelineAnalytics.getEvent
18994
19004
  var usePipelineAnalyticsPruneEvents = trpc.pipelineAnalytics.pruneEvents.useMutation;
18995
19005
  /** Generated alias around `trpc.pipelineAnalytics.deleteDeviceEvents.useMutation`. */
18996
19006
  var usePipelineAnalyticsDeleteDeviceEvents = trpc.pipelineAnalytics.deleteDeviceEvents.useMutation;
19007
+ /** Generated alias around `trpc.pipelineAnalytics.relocateMedia.useMutation`. */
19008
+ var usePipelineAnalyticsRelocateMedia = trpc.pipelineAnalytics.relocateMedia.useMutation;
19009
+ /** Generated alias around `trpc.pipelineAnalytics.getMediaRelocateStatus.useQuery`. */
19010
+ var usePipelineAnalyticsGetMediaRelocateStatus = trpc.pipelineAnalytics.getMediaRelocateStatus.useQuery;
19011
+ /** Generated alias around `trpc.pipelineAnalytics.cancelMediaRelocate.useMutation`. */
19012
+ var usePipelineAnalyticsCancelMediaRelocate = trpc.pipelineAnalytics.cancelMediaRelocate.useMutation;
18997
19013
  /** Generated alias around `trpc.pipelineAnalytics.listOpsLog.useQuery`. */
18998
19014
  var usePipelineAnalyticsListOpsLog = trpc.pipelineAnalytics.listOpsLog.useQuery;
18999
19015
  /** Generated alias around `trpc.pipelineAnalytics.getEventMedia.useQuery`. */
@@ -19290,6 +19306,16 @@ var useRecordingPruneFootage = trpc.recording.pruneFootage.useMutation;
19290
19306
  var useRecordingDeleteFootprint = trpc.recording.deleteFootprint.useMutation;
19291
19307
  /** Generated alias around `trpc.recording.listOpsLog.useQuery`. */
19292
19308
  var useRecordingListOpsLog = trpc.recording.listOpsLog.useQuery;
19309
+ /** Generated alias around `trpc.recording.renderGif.useMutation`. */
19310
+ var useRecordingRenderGif = trpc.recording.renderGif.useMutation;
19311
+ /** Generated alias around `trpc.recording.renderClip.useMutation`. */
19312
+ var useRecordingRenderClip = trpc.recording.renderClip.useMutation;
19313
+ /** Generated alias around `trpc.recording.relocateFootage.useMutation`. */
19314
+ var useRecordingRelocateFootage = trpc.recording.relocateFootage.useMutation;
19315
+ /** Generated alias around `trpc.recording.getRelocateStatus.useQuery`. */
19316
+ var useRecordingGetRelocateStatus = trpc.recording.getRelocateStatus.useQuery;
19317
+ /** Generated alias around `trpc.recording.cancelRelocate.useMutation`. */
19318
+ var useRecordingCancelRelocate = trpc.recording.cancelRelocate.useMutation;
19293
19319
  /** Generated alias around `trpc.recording.getStatus.useQuery`. */
19294
19320
  var useRecordingGetStatus = trpc.recording.getStatus.useQuery;
19295
19321
  /** Generated alias around `trpc.recording.getDeviceSettingsContribution.useQuery`. */
@@ -19430,6 +19456,8 @@ var useStreamBrokerRetractCameraStream = trpc.streamBroker.retractCameraStream.u
19430
19456
  var useStreamBrokerAssignProfile = trpc.streamBroker.assignProfile.useMutation;
19431
19457
  /** Generated alias around `trpc.streamBroker.unassignProfile.useMutation`. */
19432
19458
  var useStreamBrokerUnassignProfile = trpc.streamBroker.unassignProfile.useMutation;
19459
+ /** Generated alias around `trpc.streamBroker.renderPreBufferClip.useMutation`. */
19460
+ var useStreamBrokerRenderPreBufferClip = trpc.streamBroker.renderPreBufferClip.useMutation;
19433
19461
  /** Generated alias around `trpc.streamBroker.listAllCameraStreams.useQuery`. */
19434
19462
  var useStreamBrokerListAllCameraStreams = trpc.streamBroker.listAllCameraStreams.useQuery;
19435
19463
  /** Generated alias around `trpc.streamBroker.listAllProfileSlots.useQuery`. */
@@ -34729,315 +34757,108 @@ function AutotrackSection({ deviceId }) {
34729
34757
  }
34730
34758
  //#endregion
34731
34759
  //#region src/composites/cap-settings/recording-config-form.ts
34732
- function emptyRule() {
34760
+ /** Every weekday — the `days` of a band that covers the whole week. */
34761
+ var ALL_WEEKDAYS = [
34762
+ 0,
34763
+ 1,
34764
+ 2,
34765
+ 3,
34766
+ 4,
34767
+ 5,
34768
+ 6
34769
+ ];
34770
+ /** The full-day span of an always-on band (`HH:MM`, 23:59 = end of day). */
34771
+ var ALL_DAY_START = "00:00";
34772
+ var ALL_DAY_END = "23:59";
34773
+ /** A band covering every day, all day, in `mode` — what the Base tab authors. */
34774
+ function alwaysBand(mode, rest) {
34733
34775
  return {
34734
- schedule: { kind: "always" },
34735
- mode: "continuous",
34736
- preBufferSec: 0,
34737
- postBufferSec: 0,
34738
- resetTimeoutOnNewEvent: true
34776
+ days: [...ALL_WEEKDAYS],
34777
+ start: ALL_DAY_START,
34778
+ end: ALL_DAY_END,
34779
+ mode,
34780
+ ...rest
34739
34781
  };
34740
34782
  }
34741
- function isTriggerMode(mode) {
34742
- return mode === "onMotion" || mode === "onAudioThreshold";
34783
+ /** A fresh row for the Advanced band table: every day, office hours, continuous. */
34784
+ function emptyBand() {
34785
+ return {
34786
+ days: [...ALL_WEEKDAYS],
34787
+ start: "09:00",
34788
+ end: "18:00",
34789
+ mode: "continuous"
34790
+ };
34743
34791
  }
34744
- function withRuleAt(rules, index, patch) {
34745
- return rules.map((r, i) => i === index ? {
34746
- ...r,
34792
+ function withBandAt(bands, index, patch) {
34793
+ return bands.map((band, i) => i === index ? {
34794
+ ...band,
34747
34795
  ...patch
34748
- } : r);
34796
+ } : band);
34797
+ }
34798
+ function removeBandAt(bands, index) {
34799
+ return bands.filter((_, i) => i !== index);
34749
34800
  }
34750
- function removeRuleAt(rules, index) {
34751
- return rules.filter((_, i) => i !== index);
34801
+ /** Does this band cover the whole week, all day? (the Base-tab shape) */
34802
+ function isAlwaysBand(band) {
34803
+ return (band.days.length === 0 || ALL_WEEKDAYS.every((d) => band.days.includes(d))) && band.start === "00:00" && band.end === "23:59";
34752
34804
  }
34753
- /** Recording bands normalized: explicit `schedules[]` wins, the deprecated
34754
- * singular `schedule` is lifted, absent = empty (= always). */
34755
- function bandsOf(cfg) {
34756
- if (cfg.schedules && cfg.schedules.length > 0) return [...cfg.schedules];
34757
- if (cfg.schedule) return [cfg.schedule];
34758
- return [];
34805
+ /**
34806
+ * A config is "advanced" when its bands cannot be expressed by the Base tab —
34807
+ * i.e. more than one band, or a single band restricted in time or weekday. Such
34808
+ * a config opens directly in the Advanced band table so nothing is silently
34809
+ * flattened by the simpler form.
34810
+ */
34811
+ function isAdvancedConfig(cfg) {
34812
+ if (cfg.bands.length > 1) return true;
34813
+ const [only] = cfg.bands;
34814
+ return only !== void 0 && !isAlwaysBand(only);
34759
34815
  }
34760
- /** Read a persisted/migrated config into the Basic form state. `mode` is read
34761
- * directly; a legacy mode-less config falls back to continuous/off by `enabled`. */
34816
+ /**
34817
+ * Read a persisted config into the Basic form state. `mode` is DERIVED from the
34818
+ * bands by the same function the recorder stamps `config.mode` with; the
34819
+ * `events` parameters come from the first events band, so re-opening the form
34820
+ * shows what is actually recorded.
34821
+ */
34762
34822
  function formStateFromConfig(cfg) {
34823
+ const eventsBand = cfg.bands.find((band) => band.mode === "events");
34763
34824
  return {
34764
- mode: cfg.mode ?? (cfg.enabled ? "continuous" : "off"),
34825
+ mode: (0, _camstack_types.deriveRecordingMode)(cfg),
34765
34826
  profiles: cfg.profiles,
34766
34827
  segmentSeconds: cfg.segmentSeconds,
34767
- schedules: bandsOf(cfg),
34768
- triggers: cfg.triggers ?? {},
34769
- preBufferSec: cfg.preBufferSec,
34770
- postBufferSec: cfg.postBufferSec,
34828
+ triggers: eventsBand?.triggers ?? {},
34829
+ preBufferSec: eventsBand?.preBufferSec,
34830
+ postBufferSec: eventsBand?.postBufferSec,
34771
34831
  retention: cfg.retention,
34772
34832
  scrubThumbnails: cfg.scrubThumbnails
34773
34833
  };
34774
34834
  }
34775
- /** Serialize the Basic form state back to the wire config. `enabled` is derived
34776
- * from `mode`; `rules`/singular `schedule` are never emitted. Schedules,
34777
- * triggers and buffers are preserved even when off so they survive a re-enable. */
34835
+ /**
34836
+ * Serialize the Basic form state to the wire config: one always-on band of the
34837
+ * chosen mode, none when `off`. `enabled` follows the mode; `mode` itself is
34838
+ * NOT sent — the recorder stamps it as the derived summary of the bands.
34839
+ */
34778
34840
  function configFromFormState(state) {
34779
34841
  return {
34780
34842
  enabled: state.mode !== "off",
34781
- mode: state.mode,
34843
+ bands: bandsFromFormState(state),
34782
34844
  profiles: state.profiles ? [...state.profiles] : void 0,
34783
34845
  segmentSeconds: state.segmentSeconds,
34784
- schedules: state.schedules.length > 0 ? [...state.schedules] : void 0,
34785
- triggers: Object.keys(state.triggers).length > 0 ? state.triggers : void 0,
34786
- preBufferSec: state.preBufferSec,
34787
- postBufferSec: state.postBufferSec,
34788
34846
  retention: state.retention,
34789
34847
  scrubThumbnails: state.scrubThumbnails
34790
34848
  };
34791
34849
  }
34792
- /**
34793
- * A config is "advanced" when it carries `rules[]` that the backend could NOT
34794
- * cleanly collapse to a `mode` (the non-destructive `migrateRulesToMode` leaves
34795
- * `mode` absent). The settings UI opens such a config in the Advanced rules tab.
34796
- */
34797
- function isAdvancedConfig(cfg) {
34798
- return cfg.mode == null && (cfg.rules?.length ?? 0) > 0;
34799
- }
34800
- //#endregion
34801
- //#region src/composites/cap-settings/RecordingRulesEditor.tsx
34802
- var DAYS = [
34803
- {
34804
- d: 0,
34805
- label: "S"
34806
- },
34807
- {
34808
- d: 1,
34809
- label: "M"
34810
- },
34811
- {
34812
- d: 2,
34813
- label: "T"
34814
- },
34815
- {
34816
- d: 3,
34817
- label: "W"
34818
- },
34819
- {
34820
- d: 4,
34821
- label: "T"
34822
- },
34823
- {
34824
- d: 5,
34825
- label: "F"
34826
- },
34827
- {
34828
- d: 6,
34829
- label: "S"
34830
- }
34831
- ];
34832
- /** Converts a select string value to RecordingMode without a cast. */
34833
- function toMode(v) {
34834
- return v === "onMotion" || v === "onAudioThreshold" ? v : "continuous";
34835
- }
34836
- /**
34837
- * The rule patch to apply when the mode changes, normalising the fields that
34838
- * only apply to certain modes so a saved rule never carries stale values:
34839
- * `continuous` drops the pre/post buffers + threshold; trigger modes drop the
34840
- * threshold unless it's `onAudioThreshold`, which is seeded to -30 dBFS when
34841
- * unset so the displayed default is the value actually persisted.
34842
- */
34843
- function modePatch(rule, mode) {
34844
- if (mode === "onAudioThreshold") return {
34845
- mode,
34846
- thresholdDbfs: rule.thresholdDbfs ?? -30
34847
- };
34848
- if (mode === "onMotion") return {
34849
- mode,
34850
- thresholdDbfs: void 0
34851
- };
34852
- return {
34853
- mode,
34854
- preBufferSec: 0,
34855
- postBufferSec: 0,
34856
- thresholdDbfs: void 0
34857
- };
34858
- }
34859
- function TimeOfDayRow({ schedule, onChange }) {
34860
- const toggleDay = (d) => {
34861
- const cur = schedule.days ?? [
34862
- 0,
34863
- 1,
34864
- 2,
34865
- 3,
34866
- 4,
34867
- 5,
34868
- 6
34869
- ];
34870
- const days = cur.includes(d) ? cur.filter((x) => x !== d) : [...cur, d].toSorted((a, b) => a - b);
34871
- onChange({
34872
- ...schedule,
34873
- days
34874
- });
34875
- };
34876
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
34877
- className: "flex flex-wrap items-center gap-2 text-xs",
34878
- children: [
34879
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
34880
- type: "time",
34881
- value: schedule.start,
34882
- onChange: (e) => onChange({
34883
- ...schedule,
34884
- start: e.target.value
34885
- }),
34886
- className: "rounded-md border border-border bg-background px-2 py-1 text-xs"
34887
- }),
34888
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "–" }),
34889
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
34890
- type: "time",
34891
- value: schedule.end,
34892
- onChange: (e) => onChange({
34893
- ...schedule,
34894
- end: e.target.value
34895
- }),
34896
- className: "rounded-md border border-border bg-background px-2 py-1 text-xs"
34897
- }),
34898
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
34899
- className: "flex gap-1",
34900
- children: DAYS.map(({ d, label }) => {
34901
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
34902
- type: "button",
34903
- onClick: () => toggleDay(d),
34904
- 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"}`,
34905
- children: label
34906
- }, d);
34907
- })
34908
- })
34909
- ]
34910
- });
34911
- }
34912
- function RecordingRulesEditor({ rules, onChange }) {
34913
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
34914
- className: "space-y-2",
34915
- children: [
34916
- rules.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
34917
- className: "text-xs text-foreground-subtle",
34918
- children: "No rules — recording is continuous (always) when enabled."
34919
- }) : null,
34920
- rules.map((rule, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
34921
- className: "space-y-2 rounded-lg border border-border bg-surface p-3",
34922
- children: [
34923
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
34924
- className: "flex items-center justify-between gap-2",
34925
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
34926
- className: "rounded-md border border-border bg-background px-2 py-1 text-xs",
34927
- value: rule.schedule.kind,
34928
- onChange: (e) => {
34929
- onChange(withRuleAt(rules, i, { schedule: e.target.value === "timeOfDay" ? {
34930
- kind: "timeOfDay",
34931
- start: "00:00",
34932
- end: "23:59"
34933
- } : { kind: "always" } }));
34934
- },
34935
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
34936
- value: "always",
34937
- children: "Always"
34938
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
34939
- value: "timeOfDay",
34940
- children: "Time of day"
34941
- })]
34942
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
34943
- type: "button",
34944
- onClick: () => onChange(removeRuleAt(rules, i)),
34945
- className: "text-foreground-subtle hover:text-red-500",
34946
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(X, { className: "h-4 w-4" })
34947
- })]
34948
- }),
34949
- rule.schedule.kind === "timeOfDay" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TimeOfDayRow, {
34950
- schedule: rule.schedule,
34951
- onChange: (schedule) => onChange(withRuleAt(rules, i, { schedule }))
34952
- }) : null,
34953
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
34954
- className: "flex flex-wrap items-center gap-2 text-xs",
34955
- children: [
34956
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
34957
- className: "rounded-md border border-border bg-background px-2 py-1 text-xs",
34958
- value: rule.mode,
34959
- onChange: (e) => onChange(withRuleAt(rules, i, modePatch(rule, toMode(e.target.value)))),
34960
- children: [
34961
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
34962
- value: "continuous",
34963
- children: "Continuous"
34964
- }),
34965
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
34966
- value: "onMotion",
34967
- children: "On motion"
34968
- }),
34969
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
34970
- value: "onAudioThreshold",
34971
- children: "On audio"
34972
- })
34973
- ]
34974
- }),
34975
- isTriggerMode(rule.mode) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
34976
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
34977
- className: "flex items-center gap-1",
34978
- children: [
34979
- "pre",
34980
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
34981
- type: "number",
34982
- min: 0,
34983
- value: rule.preBufferSec,
34984
- onChange: (e) => onChange(withRuleAt(rules, i, { preBufferSec: Number(e.target.value) })),
34985
- className: "w-14 rounded-md border border-border bg-background px-1 py-1 text-xs"
34986
- }),
34987
- "s"
34988
- ]
34989
- }),
34990
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
34991
- className: "flex items-center gap-1",
34992
- children: [
34993
- "post",
34994
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
34995
- type: "number",
34996
- min: 0,
34997
- value: rule.postBufferSec,
34998
- onChange: (e) => onChange(withRuleAt(rules, i, { postBufferSec: Number(e.target.value) })),
34999
- className: "w-14 rounded-md border border-border bg-background px-1 py-1 text-xs"
35000
- }),
35001
- "s"
35002
- ]
35003
- }),
35004
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
35005
- className: "flex items-center gap-1",
35006
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
35007
- type: "checkbox",
35008
- checked: rule.resetTimeoutOnNewEvent,
35009
- onChange: (e) => onChange(withRuleAt(rules, i, { resetTimeoutOnNewEvent: e.target.checked }))
35010
- }), "reset"]
35011
- })
35012
- ] }) : null,
35013
- rule.mode === "onAudioThreshold" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
35014
- className: "flex items-center gap-1",
35015
- children: [
35016
- "thr",
35017
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
35018
- type: "number",
35019
- value: rule.thresholdDbfs ?? -30,
35020
- onChange: (e) => onChange(withRuleAt(rules, i, { thresholdDbfs: Number(e.target.value) })),
35021
- className: "w-16 rounded-md border border-border bg-background px-1 py-1 text-xs"
35022
- }),
35023
- "dBFS"
35024
- ]
35025
- }) : null
35026
- ]
35027
- })
35028
- ]
35029
- }, i)),
35030
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
35031
- type: "button",
35032
- onClick: () => onChange([...rules, emptyRule()]),
35033
- 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",
35034
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Plus, { className: "h-3.5 w-3.5" }), " Add rule"]
35035
- })
35036
- ]
35037
- });
34850
+ /** The Base tab's bands: one always-on band of the chosen mode (none = off). */
34851
+ function bandsFromFormState(state) {
34852
+ if (state.mode === "off") return [];
34853
+ if (state.mode === "continuous") return [alwaysBand("continuous")];
34854
+ return [alwaysBand("events", {
34855
+ ...Object.keys(state.triggers).length > 0 ? { triggers: state.triggers } : {},
34856
+ ...state.preBufferSec !== void 0 ? { preBufferSec: state.preBufferSec } : {},
34857
+ ...state.postBufferSec !== void 0 ? { postBufferSec: state.postBufferSec } : {}
34858
+ })];
35038
34859
  }
35039
34860
  //#endregion
35040
- //#region src/composites/cap-settings/ScheduleBandsEditor.tsx
34861
+ //#region src/composites/cap-settings/RecordingBandsEditor.tsx
35041
34862
  /** Weekday chips in display order (Mon→Sun); value is `Date.getDay()` (0=Sun). */
35042
34863
  var WEEKDAYS = [
35043
34864
  {
@@ -35069,115 +34890,177 @@ var WEEKDAYS = [
35069
34890
  label: "Sun"
35070
34891
  }
35071
34892
  ];
35072
- function newBand() {
35073
- return {
35074
- kind: "timeOfDay",
35075
- start: "09:00",
35076
- end: "18:00"
35077
- };
35078
- }
35079
- function ScheduleBandsEditor({ schedules, onChange }) {
35080
- const replaceAt = (i, band) => onChange(schedules.map((s, idx) => idx === i ? band : s));
35081
- const removeAt = (i) => onChange(schedules.filter((_, idx) => idx !== i));
35082
- const addBand = () => onChange([...schedules, newBand()]);
34893
+ var BAND_MODES = [{
34894
+ value: "continuous",
34895
+ label: "Continuous"
34896
+ }, {
34897
+ value: "events",
34898
+ label: "Events"
34899
+ }];
34900
+ /**
34901
+ * The band table the ONLY authoring surface for recording intent since the
34902
+ * legacy mode/schedules/triggers/rules model was retired (2026-07-30).
34903
+ *
34904
+ * Each row is one `RecordingBand`: when it applies (weekdays + `HH:MM` span,
34905
+ * `end <= start` wraps past midnight) and how it records (continuous, or events
34906
+ * with its own triggers and pre/post buffers). No band covering *now* = nothing
34907
+ * is recorded then; "off" is the absence of a band, never a band value.
34908
+ */
34909
+ function RecordingBandsEditor({ bands, onChange }) {
35083
34910
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35084
34911
  className: "space-y-2",
35085
- children: [schedules.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
34912
+ children: [bands.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
35086
34913
  className: "text-[11px] text-foreground-subtle",
35087
- children: "Always (24/7)add a band to restrict hours."
35088
- }) : schedules.map((band, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BandRow, {
34914
+ children: "No bandsnothing is recorded. Add one to start."
34915
+ }) : bands.map((band, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BandRow, {
35089
34916
  band,
35090
- onChange: (b) => replaceAt(i, b),
35091
- onRemove: () => removeAt(i)
34917
+ onChange: (patch) => onChange(withBandAt(bands, i, patch)),
34918
+ onRemove: () => onChange(removeBandAt(bands, i))
35092
34919
  }, i)), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
35093
34920
  type: "button",
35094
- onClick: addBand,
34921
+ onClick: () => onChange([...bands, emptyBand()]),
35095
34922
  className: "flex items-center gap-1 rounded-md bg-surface-hover px-2 py-1 text-[11px] text-foreground-subtle hover:text-foreground",
35096
34923
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Plus, { className: "h-3 w-3" }), " Add band"]
35097
34924
  })]
35098
34925
  });
35099
34926
  }
34927
+ function numOrUndef(value) {
34928
+ return value === "" ? void 0 : Number(value);
34929
+ }
35100
34930
  function BandRow({ band, onChange, onRemove }) {
35101
- if (band.kind === "always") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35102
- className: "flex items-center gap-2 text-xs",
35103
- children: [
35104
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35105
- className: "flex-1",
35106
- children: "Always"
35107
- }),
35108
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
35109
- type: "button",
35110
- onClick: () => onChange(newBand()),
35111
- className: "text-foreground-subtle hover:text-foreground underline",
35112
- children: "Set band"
35113
- }),
35114
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
35115
- type: "button",
35116
- onClick: onRemove,
35117
- "aria-label": "Remove band",
35118
- className: "text-foreground-subtle hover:text-red-500",
35119
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Trash2, { className: "h-3.5 w-3.5" })
35120
- })
35121
- ]
35122
- });
35123
- const days = band.days ?? [];
35124
34931
  const toggleDay = (value) => {
35125
- const next = days.includes(value) ? days.filter((d) => d !== value) : [...days, value];
35126
- onChange({
35127
- ...band,
35128
- days: next.length > 0 ? next : void 0
35129
- });
34932
+ const current = band.days.length === 0 ? WEEKDAYS.map((d) => d.value) : band.days;
34933
+ onChange({ days: current.includes(value) ? current.filter((d) => d !== value) : [...current, value] });
35130
34934
  };
34935
+ const setTriggers = (patch) => onChange({ triggers: {
34936
+ ...band.triggers,
34937
+ ...patch
34938
+ } });
35131
34939
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35132
34940
  className: "space-y-1.5 rounded-md border border-border p-2",
35133
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35134
- className: "flex items-center gap-2 text-xs",
35135
- children: [
35136
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
35137
- type: "time",
35138
- value: band.start,
35139
- onChange: (e) => onChange({
35140
- ...band,
35141
- start: e.target.value
34941
+ children: [
34942
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
34943
+ className: "flex flex-wrap items-center gap-2 text-xs",
34944
+ children: [
34945
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
34946
+ type: "time",
34947
+ "aria-label": "Band start",
34948
+ value: band.start,
34949
+ onChange: (e) => onChange({ start: e.target.value }),
34950
+ className: "rounded-md border border-border bg-background px-1 py-0.5"
35142
34951
  }),
35143
- className: "rounded-md border border-border bg-background px-1 py-0.5"
35144
- }),
35145
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35146
- className: "text-foreground-subtle",
35147
- children: "→"
35148
- }),
35149
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
35150
- type: "time",
35151
- value: band.end,
35152
- onChange: (e) => onChange({
35153
- ...band,
35154
- end: e.target.value
34952
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
34953
+ className: "text-foreground-subtle",
34954
+ children: ""
35155
34955
  }),
35156
- className: "rounded-md border border-border bg-background px-1 py-0.5"
35157
- }),
35158
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "flex-1" }),
35159
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
35160
- type: "button",
35161
- onClick: onRemove,
35162
- "aria-label": "Remove band",
35163
- className: "text-foreground-subtle hover:text-red-500",
35164
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Trash2, { className: "h-3.5 w-3.5" })
35165
- })
35166
- ]
35167
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35168
- className: "flex flex-wrap gap-1",
35169
- children: [WEEKDAYS.map((d) => {
35170
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
35171
- type: "button",
35172
- onClick: () => toggleDay(d.value),
35173
- 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"}`,
35174
- children: d.label
35175
- }, d.value);
35176
- }), days.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35177
- className: "self-center text-[10px] text-foreground-subtle",
35178
- children: "every day"
35179
- }) : null]
35180
- })]
34956
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
34957
+ type: "time",
34958
+ "aria-label": "Band end",
34959
+ value: band.end,
34960
+ onChange: (e) => onChange({ end: e.target.value }),
34961
+ className: "rounded-md border border-border bg-background px-1 py-0.5"
34962
+ }),
34963
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
34964
+ className: "flex gap-1",
34965
+ children: BAND_MODES.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
34966
+ type: "button",
34967
+ onClick: () => onChange({ mode: m.value }),
34968
+ className: `rounded px-2 py-0.5 ${band.mode === m.value ? "bg-primary text-primary-foreground" : "bg-surface-hover text-foreground-subtle"}`,
34969
+ children: m.label
34970
+ }, m.value))
34971
+ }),
34972
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "flex-1" }),
34973
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
34974
+ type: "button",
34975
+ onClick: onRemove,
34976
+ "aria-label": "Remove band",
34977
+ className: "text-foreground-subtle hover:text-red-500",
34978
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Trash2, { className: "h-3.5 w-3.5" })
34979
+ })
34980
+ ]
34981
+ }),
34982
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
34983
+ className: "flex flex-wrap gap-1",
34984
+ children: [WEEKDAYS.map((d) => {
34985
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
34986
+ type: "button",
34987
+ onClick: () => toggleDay(d.value),
34988
+ 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"}`,
34989
+ children: d.label
34990
+ }, d.value);
34991
+ }), band.days.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
34992
+ className: "self-center text-[10px] text-foreground-subtle",
34993
+ children: "every day"
34994
+ }) : null]
34995
+ }),
34996
+ band.mode === "events" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
34997
+ className: "space-y-1.5 border-t border-border pt-1.5",
34998
+ children: [
34999
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
35000
+ className: "flex items-center gap-2 text-xs",
35001
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
35002
+ type: "checkbox",
35003
+ checked: band.triggers?.motion ?? false,
35004
+ onChange: (e) => setTriggers({ motion: e.target.checked ? true : void 0 })
35005
+ }), "Motion"]
35006
+ }),
35007
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
35008
+ className: "flex items-center gap-2 text-xs",
35009
+ children: [
35010
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
35011
+ type: "checkbox",
35012
+ checked: band.triggers?.audioThresholdDbfs != null,
35013
+ onChange: (e) => setTriggers({ audioThresholdDbfs: e.target.checked ? -30 : void 0 })
35014
+ }),
35015
+ "Audio above threshold",
35016
+ band.triggers?.audioThresholdDbfs != null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
35017
+ type: "number",
35018
+ "aria-label": "Audio threshold dBFS",
35019
+ value: band.triggers.audioThresholdDbfs,
35020
+ onChange: (e) => setTriggers({ audioThresholdDbfs: numOrUndef(e.target.value) }),
35021
+ className: "w-16 rounded-md border border-border bg-background px-1 py-0.5 text-xs"
35022
+ }) : null,
35023
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35024
+ className: "text-foreground-subtle",
35025
+ children: "dBFS"
35026
+ })
35027
+ ]
35028
+ }),
35029
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35030
+ className: "flex flex-wrap items-center gap-3 text-xs",
35031
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
35032
+ className: "flex items-center gap-1",
35033
+ children: [
35034
+ "Pre-buffer",
35035
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
35036
+ type: "number",
35037
+ min: 0,
35038
+ value: band.preBufferSec ?? "",
35039
+ placeholder: "0",
35040
+ onChange: (e) => onChange({ preBufferSec: numOrUndef(e.target.value) }),
35041
+ className: "w-16 rounded-md border border-border bg-background px-1 py-1 text-xs"
35042
+ }),
35043
+ "s"
35044
+ ]
35045
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
35046
+ className: "flex items-center gap-1",
35047
+ children: [
35048
+ "Post-buffer",
35049
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
35050
+ type: "number",
35051
+ min: 0,
35052
+ value: band.postBufferSec ?? "",
35053
+ placeholder: "0",
35054
+ onChange: (e) => onChange({ postBufferSec: numOrUndef(e.target.value) }),
35055
+ className: "w-16 rounded-md border border-border bg-background px-1 py-1 text-xs"
35056
+ }),
35057
+ "s"
35058
+ ]
35059
+ })]
35060
+ })
35061
+ ]
35062
+ }) : null
35063
+ ]
35181
35064
  });
35182
35065
  }
35183
35066
  //#endregion
@@ -35201,16 +35084,15 @@ var MODES = [
35201
35084
  label: "Continuous"
35202
35085
  }
35203
35086
  ];
35204
- function RecordingSettings({ initial, saving, onSave }) {
35087
+ function RecordingSettings({ initial, saving, onSave, onRegenerateStrips, regeneratingStrips }) {
35205
35088
  const [tab, setTab] = (0, react$1.useState)(isAdvancedConfig(initial) ? "advanced" : "base");
35206
35089
  const [base, setBase] = (0, react$1.useState)(formStateFromConfig(initial));
35207
- const [advEnabled, setAdvEnabled] = (0, react$1.useState)(initial.enabled);
35208
- const [rules, setRules] = (0, react$1.useState)(initial.rules ? [...initial.rules] : []);
35090
+ const [bands, setBands] = (0, react$1.useState)([...initial.bands]);
35209
35091
  const [common, setCommon] = (0, react$1.useState)({
35210
35092
  profiles: initial.profiles,
35211
35093
  segmentSeconds: initial.segmentSeconds,
35212
35094
  retention: initial.retention,
35213
- scrubThumbnails: initial.scrubThumbnails
35095
+ stripsEnabled: initial.stripsEnabled
35214
35096
  });
35215
35097
  const ret = common.retention ?? {};
35216
35098
  const setRet = (patch) => setCommon({
@@ -35221,10 +35103,6 @@ function RecordingSettings({ initial, saving, onSave }) {
35221
35103
  }
35222
35104
  });
35223
35105
  const numOrUndef = (v) => v === "" ? void 0 : Number(v);
35224
- const parsePreset = (v) => {
35225
- const parsed = _camstack_types.ScrubThumbnailPresetSchema.safeParse(v);
35226
- return parsed.success ? parsed.data : _camstack_types.DEFAULT_SCRUB_THUMBNAIL_PRESET;
35227
- };
35228
35106
  const toggleProfile = (p) => {
35229
35107
  const cur = common.profiles ?? [...PROFILES];
35230
35108
  const next = cur.includes(p) ? cur.filter((x) => x !== p) : [...cur, p];
@@ -35241,24 +35119,31 @@ function RecordingSettings({ initial, saving, onSave }) {
35241
35119
  }
35242
35120
  });
35243
35121
  const switchTab = (next) => {
35244
- if (next === "advanced" && rules.length === 0) setRules([emptyRule()]);
35122
+ if (next === "advanced") setBands(bandsFromFormState(base));
35123
+ else setBase(formStateFromConfig({
35124
+ enabled: bands.length > 0,
35125
+ bands
35126
+ }));
35245
35127
  setTab(next);
35246
35128
  };
35247
35129
  const handleSave = () => {
35248
- if (tab === "base") onSave(configFromFormState({
35249
- ...base,
35250
- profiles: common.profiles,
35251
- segmentSeconds: common.segmentSeconds,
35252
- retention: common.retention,
35253
- scrubThumbnails: common.scrubThumbnails
35254
- }));
35255
- else onSave({
35256
- enabled: advEnabled,
35257
- rules,
35130
+ const shared = {
35258
35131
  profiles: common.profiles ? [...common.profiles] : void 0,
35259
35132
  segmentSeconds: common.segmentSeconds,
35260
35133
  retention: common.retention,
35261
- scrubThumbnails: common.scrubThumbnails
35134
+ ...common.stripsEnabled === void 0 ? {} : { stripsEnabled: common.stripsEnabled }
35135
+ };
35136
+ if (tab === "base") onSave({
35137
+ ...configFromFormState({
35138
+ ...base,
35139
+ ...shared
35140
+ }),
35141
+ ...shared
35142
+ });
35143
+ else onSave({
35144
+ enabled: bands.length > 0,
35145
+ bands,
35146
+ ...shared
35262
35147
  });
35263
35148
  };
35264
35149
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -35270,7 +35155,7 @@ function RecordingSettings({ initial, saving, onSave }) {
35270
35155
  type: "button",
35271
35156
  onClick: () => switchTab(t),
35272
35157
  className: `rounded px-2.5 py-1 ${tab === t ? "bg-primary text-primary-foreground" : "bg-surface-hover text-foreground-subtle"}`,
35273
- children: t === "base" ? "Base" : "Advanced (rules)"
35158
+ children: t === "base" ? "Base" : "Advanced (bands)"
35274
35159
  }, t))
35275
35160
  }),
35276
35161
  tab === "base" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -35291,16 +35176,17 @@ function RecordingSettings({ initial, saving, onSave }) {
35291
35176
  children: m.label
35292
35177
  }, m.value))]
35293
35178
  }),
35294
- base.mode !== "off" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
35295
- className: "mb-1.5 text-[11px] font-semibold uppercase tracking-wider text-foreground-subtle",
35296
- children: "Time bands"
35297
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScheduleBandsEditor, {
35298
- schedules: base.schedules,
35299
- onChange: (schedules) => setBase({
35300
- ...base,
35301
- schedules
35302
- })
35303
- })] }) : null,
35179
+ base.mode !== "off" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
35180
+ className: "text-[11px] text-foreground-subtle",
35181
+ children: [
35182
+ "Records 24/7. Use ",
35183
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35184
+ className: "font-medium",
35185
+ children: "Advanced (bands)"
35186
+ }),
35187
+ " to restrict hours or weekdays, or to mix continuous and event windows."
35188
+ ]
35189
+ }) : null,
35304
35190
  base.mode === "events" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35305
35191
  className: "space-y-2",
35306
35192
  children: [
@@ -35378,22 +35264,22 @@ function RecordingSettings({ initial, saving, onSave }) {
35378
35264
  ]
35379
35265
  }) : null
35380
35266
  ]
35381
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35267
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
35382
35268
  className: "space-y-4",
35383
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
35384
- className: "flex items-center gap-2 text-xs",
35385
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
35386
- type: "checkbox",
35387
- checked: advEnabled,
35388
- onChange: (e) => setAdvEnabled(e.target.checked)
35389
- }), "Record this camera"]
35390
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
35391
- className: "mb-1.5 text-[11px] font-semibold uppercase tracking-wider text-foreground-subtle",
35392
- children: "Rules"
35393
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RecordingRulesEditor, {
35394
- rules,
35395
- onChange: setRules
35396
- })] })]
35269
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
35270
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
35271
+ className: "mb-1.5 text-[11px] font-semibold uppercase tracking-wider text-foreground-subtle",
35272
+ children: "Recording bands"
35273
+ }),
35274
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RecordingBandsEditor, {
35275
+ bands,
35276
+ onChange: setBands
35277
+ }),
35278
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
35279
+ className: "mt-1.5 text-[11px] text-foreground-subtle",
35280
+ children: "A band says WHEN to record and HOW. Nothing is recorded outside every band — an empty table means this camera is off."
35281
+ })
35282
+ ] })
35397
35283
  }),
35398
35284
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
35399
35285
  className: "flex flex-wrap items-center gap-4 text-xs",
@@ -35432,27 +35318,30 @@ function RecordingSettings({ initial, saving, onSave }) {
35432
35318
  }),
35433
35319
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
35434
35320
  className: "flex items-center gap-1",
35435
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35436
- className: "text-foreground-subtle",
35437
- children: "Scrub preview quality"
35438
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
35439
- value: common.scrubThumbnails ?? _camstack_types.DEFAULT_SCRUB_THUMBNAIL_PRESET,
35321
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
35322
+ type: "checkbox",
35323
+ checked: common.stripsEnabled === true,
35440
35324
  onChange: (e) => setCommon({
35441
35325
  ...common,
35442
- scrubThumbnails: parsePreset(e.target.value)
35443
- }),
35444
- className: "rounded-md border border-border bg-background px-1 py-1 text-xs",
35445
- children: _camstack_types.SCRUB_THUMBNAIL_PRESET_ORDER.map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
35446
- value: p,
35447
- children: _camstack_types.SCRUB_THUMBNAIL_PRESET_LABELS[p]
35448
- }, p))
35326
+ stripsEnabled: e.target.checked
35327
+ })
35328
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35329
+ className: "text-foreground-subtle",
35330
+ children: "Scrub thumbnail strips"
35449
35331
  })]
35450
- })
35332
+ }),
35333
+ onRegenerateStrips ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
35334
+ type: "button",
35335
+ onClick: onRegenerateStrips,
35336
+ disabled: regeneratingStrips === true,
35337
+ 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",
35338
+ children: regeneratingStrips === true ? "Regenerating…" : "Regenerate strips"
35339
+ }) : null
35451
35340
  ]
35452
35341
  }),
35453
35342
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
35454
35343
  className: "text-[11px] text-foreground-subtle",
35455
- 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."
35344
+ 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."
35456
35345
  }),
35457
35346
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
35458
35347
  className: "mb-1.5 text-[11px] font-semibold uppercase tracking-wider text-foreground-subtle",
@@ -36701,6 +36590,14 @@ function RecordingPanel({ deviceId }) {
36701
36590
  const configQuery = useRecordingGetDeviceConfig({ deviceId });
36702
36591
  const setConfig = useRecordingSetDeviceConfig();
36703
36592
  const rescanStorage = useRecordingRescanStorage();
36593
+ const customAction = useAddonsCustom();
36594
+ const regenerateStrips = (0, react$1.useCallback)(() => {
36595
+ customAction.mutate({
36596
+ addonId: "recorder",
36597
+ action: "regenerateStrips",
36598
+ input: { deviceId }
36599
+ });
36600
+ }, [customAction, deviceId]);
36704
36601
  const saveConfig = (config) => {
36705
36602
  setConfig.mutate({
36706
36603
  deviceId,
@@ -36742,7 +36639,9 @@ function RecordingPanel({ deviceId }) {
36742
36639
  children: resolvedConfig ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RecordingSettings, {
36743
36640
  initial: resolvedConfig,
36744
36641
  saving: setConfig.isPending,
36745
- onSave: saveConfig
36642
+ onSave: saveConfig,
36643
+ onRegenerateStrips: regenerateStrips,
36644
+ regeneratingStrips: customAction.isPending
36746
36645
  }, JSON.stringify(resolvedConfig)) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
36747
36646
  className: "px-1 py-2 text-xs text-foreground-subtle",
36748
36647
  children: "Loading settings…"
@@ -40308,6 +40207,21 @@ function DetectionBoxes({ detections, frameWidth, frameHeight }) {
40308
40207
  }
40309
40208
  //#endregion
40310
40209
  //#region src/composites/camera-stream-player.tsx
40210
+ /**
40211
+ * Silence (or restore) a live WebRTC stream FOR REAL.
40212
+ *
40213
+ * `video.muted` alone is not enough: iOS WebKit keeps playing a
40214
+ * `MediaStream`'s audio track through a muted element, which is why live audio
40215
+ * kept sounding under recorded playback on the viewer. Gating the audio TRACK
40216
+ * (`track.enabled`) is what actually stops it, so both are set together — and
40217
+ * re-applied at every stream swap, because a freshly-attached stream arrives
40218
+ * with its tracks enabled whatever the element says.
40219
+ */
40220
+ function applyStreamMute(video, muted) {
40221
+ video.muted = muted;
40222
+ const stream = video.srcObject;
40223
+ if (stream instanceof MediaStream) for (const track of stream.getAudioTracks()) track.enabled = !muted;
40224
+ }
40311
40225
  function isTouchDevice() {
40312
40226
  if (typeof window === "undefined") return false;
40313
40227
  return window.matchMedia("(hover: none) and (pointer: coarse)").matches || typeof navigator !== "undefined" && navigator.maxTouchPoints > 0 || "ontouchstart" in window;
@@ -40390,6 +40304,11 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
40390
40304
  const [state, setState] = (0, react$1.useState)("idle");
40391
40305
  const [errorMessage, setErrorMessage] = (0, react$1.useState)("");
40392
40306
  const [isMuted, setIsMuted] = (0, react$1.useState)(initialMuted);
40307
+ /** Read by the async stream-swap closures: a freshly-promoted stream must
40308
+ * arrive already respecting the CURRENT mute, not the one captured at
40309
+ * connect time. */
40310
+ const isMutedRef = (0, react$1.useRef)(isMuted);
40311
+ isMutedRef.current = isMuted;
40393
40312
  const [isFullscreen, setIsFullscreen] = (0, react$1.useState)(false);
40394
40313
  const [isPipSupported, setIsPipSupported] = (0, react$1.useState)(false);
40395
40314
  const isTouch = isTouchDevice();
@@ -40519,6 +40438,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
40519
40438
  pcRef.current = pc;
40520
40439
  if (videoRef.current) {
40521
40440
  videoRef.current.srcObject = combinedStream;
40441
+ applyStreamMute(videoRef.current, isMutedRef.current);
40522
40442
  videoRef.current.play().catch(() => {});
40523
40443
  }
40524
40444
  if (oldPc && oldPc !== pc) try {
@@ -40538,6 +40458,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
40538
40458
  }
40539
40459
  if (videoRef.current) {
40540
40460
  videoRef.current.srcObject = combinedStream;
40461
+ applyStreamMute(videoRef.current, isMutedRef.current);
40541
40462
  videoRef.current.play().catch(() => {});
40542
40463
  }
40543
40464
  };
@@ -40624,6 +40545,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
40624
40545
  whepStream.addTrack(event.track);
40625
40546
  if (videoRef.current) {
40626
40547
  videoRef.current.srcObject = whepStream;
40548
+ applyStreamMute(videoRef.current, isMutedRef.current);
40627
40549
  videoRef.current.play().catch(() => {});
40628
40550
  }
40629
40551
  };
@@ -40696,6 +40618,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
40696
40618
  pcRef.current = pc;
40697
40619
  if (videoRef.current) {
40698
40620
  videoRef.current.srcObject = combinedStream;
40621
+ applyStreamMute(videoRef.current, isMutedRef.current);
40699
40622
  videoRef.current.play().catch(() => {});
40700
40623
  }
40701
40624
  if (oldPc && oldPc !== pc) try {
@@ -40716,6 +40639,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
40716
40639
  }
40717
40640
  if (videoRef.current) {
40718
40641
  videoRef.current.srcObject = combinedStream;
40642
+ applyStreamMute(videoRef.current, isMutedRef.current);
40719
40643
  videoRef.current.play().catch(() => {});
40720
40644
  }
40721
40645
  };
@@ -41040,7 +40964,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
41040
40964
  mountedRef.current = false;
41041
40965
  cleanup();
41042
40966
  };
41043
- }, [serverUrl, streamKey]);
40967
+ }, []);
41044
40968
  const hasSignaledRef = (0, react$1.useRef)(false);
41045
40969
  (0, react$1.useEffect)(() => {
41046
40970
  if (!autoPlay) return;
@@ -41078,6 +41002,23 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
41078
41002
  }
41079
41003
  connect();
41080
41004
  };
41005
+ const prevServerUrlRef = (0, react$1.useRef)(serverUrl);
41006
+ const prevStreamKeyRef = (0, react$1.useRef)(streamKey);
41007
+ (0, react$1.useEffect)(() => {
41008
+ const serverChanged = prevServerUrlRef.current !== serverUrl;
41009
+ const keyChanged = prevStreamKeyRef.current !== streamKey;
41010
+ prevServerUrlRef.current = serverUrl;
41011
+ prevStreamKeyRef.current = streamKey;
41012
+ if (!serverChanged && !keyChanged) return;
41013
+ if (!mountedRef.current) return;
41014
+ reconnectAttemptsRef.current = 0;
41015
+ if (serverChanged) {
41016
+ cleanup();
41017
+ connect();
41018
+ return;
41019
+ }
41020
+ handleSeamlessReconnect();
41021
+ }, [serverUrl, streamKey]);
41081
41022
  const prevReconnectSignalRef = (0, react$1.useRef)(reconnectSignal);
41082
41023
  (0, react$1.useEffect)(() => {
41083
41024
  if (prevReconnectSignalRef.current === reconnectSignal) return;
@@ -41087,8 +41028,9 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
41087
41028
  }, [reconnectSignal]);
41088
41029
  const toggleMute = () => {
41089
41030
  if (videoRef.current) {
41090
- videoRef.current.muted = !videoRef.current.muted;
41091
- setIsMuted(videoRef.current.muted);
41031
+ const next = !videoRef.current.muted;
41032
+ applyStreamMute(videoRef.current, next);
41033
+ setIsMuted(next);
41092
41034
  }
41093
41035
  };
41094
41036
  const toggleFullscreen = async () => {
@@ -41316,7 +41258,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
41316
41258
  setIsMuted(initialMuted);
41317
41259
  const video = videoRef.current;
41318
41260
  if (!video) return;
41319
- video.muted = initialMuted;
41261
+ applyStreamMute(video, initialMuted);
41320
41262
  if (!initialMuted) video.play().catch(() => {});
41321
41263
  }, [initialMuted]);
41322
41264
  const showToolbarBriefly = () => {
@@ -42428,7 +42370,7 @@ function StreamPanel({ serverUrl, createSession, sendAnswer, handleOffer, getIce
42428
42370
  extraOverlay,
42429
42371
  ptzOverlayVisible && ptzOverlay
42430
42372
  ] }) : void 0
42431
- }, effectiveStreamId)
42373
+ })
42432
42374
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SnapshotPreview, {
42433
42375
  src: snapshotSrc ?? null,
42434
42376
  isLoading: snapshotLoading,
@@ -46809,6 +46751,15 @@ function useDeviceDetections(trpc, deviceId) {
46809
46751
  * @param deviceId - numeric device ID (null = disabled)
46810
46752
  * @param pollIntervalMs - how often to refresh profile slots (default: 5000)
46811
46753
  */
46754
+ /** TTL for the TURN/STUN credential cache. Short enough that rotated creds
46755
+ * are picked up promptly, long enough to cover a connect + several profile
46756
+ * switches without re-paying the fetch. */
46757
+ var TURN_CACHE_TTL_MS = 5 * 6e4;
46758
+ /** Keyed on the caller's trpc client object (stable per connected system —
46759
+ * NOT on `trpc.turnProvider`, which is a proxy minted fresh on every property
46760
+ * access), so two admin tabs pointed at different hubs never cross-serve
46761
+ * credentials. An unstable key degrades to a cache miss, never to wrong creds. */
46762
+ var turnServersCache = /* @__PURE__ */ new WeakMap();
46812
46763
  function useDeviceWebrtc(trpc, deviceId, pollIntervalMs = 5e3) {
46813
46764
  const [remoteStreams, setRemoteStreams] = (0, react$1.useState)([]);
46814
46765
  (0, react$1.useEffect)(() => {
@@ -46878,18 +46829,28 @@ function useDeviceWebrtc(trpc, deviceId, pollIntervalMs = 5e3) {
46878
46829
  }, [deviceId, remoteStreams]);
46879
46830
  const getIceServers = (0, react$1.useCallback)(async () => {
46880
46831
  if (!trpc.turnProvider) return void 0;
46832
+ const cached = turnServersCache.get(trpc);
46833
+ if (cached && Date.now() - cached.fetchedAt < TURN_CACHE_TTL_MS) return cached.servers;
46881
46834
  try {
46882
- return (await trpc.turnProvider.getTurnServers.query()).map((s) => {
46835
+ const mapped = (await trpc.turnProvider.getTurnServers.query()).map((s) => {
46883
46836
  return {
46884
46837
  urls: typeof s.urls === "string" ? s.urls : [...s.urls],
46885
46838
  ...s.username !== void 0 ? { username: s.username } : {},
46886
46839
  ...s.credential !== void 0 ? { credential: s.credential } : {}
46887
46840
  };
46888
46841
  });
46842
+ turnServersCache.set(trpc, {
46843
+ servers: mapped,
46844
+ fetchedAt: Date.now()
46845
+ });
46846
+ return mapped;
46889
46847
  } catch {
46890
- return;
46848
+ return cached?.servers;
46891
46849
  }
46892
46850
  }, [trpc]);
46851
+ (0, react$1.useEffect)(() => {
46852
+ getIceServers();
46853
+ }, [getIceServers]);
46893
46854
  const createSession = (0, react$1.useCallback)(async (target, hints) => {
46894
46855
  if (deviceId === null) throw new Error("No device selected");
46895
46856
  const res = await trpc.webrtcSession.createSession.mutate({
@@ -47666,15 +47627,18 @@ exports.useAutomationControlEnable = useAutomationControlEnable;
47666
47627
  exports.useAutomationControlGetStatus = useAutomationControlGetStatus;
47667
47628
  exports.useAutomationControlTrigger = useAutomationControlTrigger;
47668
47629
  exports.useBackupDelete = useBackupDelete;
47630
+ exports.useBackupDeleteSchedule = useBackupDeleteSchedule;
47669
47631
  exports.useBackupGetEntries = useBackupGetEntries;
47670
47632
  exports.useBackupList = useBackupList;
47671
47633
  exports.useBackupListArchives = useBackupListArchives;
47672
47634
  exports.useBackupListDestinations = useBackupListDestinations;
47673
47635
  exports.useBackupListLocations = useBackupListLocations;
47636
+ exports.useBackupListSchedules = useBackupListSchedules;
47674
47637
  exports.useBackupPreviewSchedule = useBackupPreviewSchedule;
47675
47638
  exports.useBackupRestore = useBackupRestore;
47676
47639
  exports.useBackupTrigger = useBackupTrigger;
47677
47640
  exports.useBackupUpsertDestinationPolicy = useBackupUpsertDestinationPolicy;
47641
+ exports.useBackupUpsertSchedule = useBackupUpsertSchedule;
47678
47642
  exports.useBatteryGetStatus = useBatteryGetStatus;
47679
47643
  exports.useBatteryWakeForStream = useBatteryWakeForStream;
47680
47644
  exports.useBinaryGetStatus = useBinaryGetStatus;
@@ -47984,10 +47948,12 @@ exports.useLlmTestProfile = useLlmTestProfile;
47984
47948
  exports.useLlmUpsertProfile = useLlmUpsertProfile;
47985
47949
  exports.useLocalNetworkGetAllowedAddresses = useLocalNetworkGetAllowedAddresses;
47986
47950
  exports.useLocalNetworkGetConnectionEndpoints = useLocalNetworkGetConnectionEndpoints;
47951
+ exports.useLocalNetworkGetNotificationEndpoint = useLocalNetworkGetNotificationEndpoint;
47987
47952
  exports.useLocalNetworkGetPreferred = useLocalNetworkGetPreferred;
47988
47953
  exports.useLocalNetworkList = useLocalNetworkList;
47989
47954
  exports.useLocalNetworkResetAllowlistToBestMatch = useLocalNetworkResetAllowlistToBestMatch;
47990
47955
  exports.useLocalNetworkSetAllowedAddresses = useLocalNetworkSetAllowedAddresses;
47956
+ exports.useLocalNetworkSetNotificationEndpoint = useLocalNetworkSetNotificationEndpoint;
47991
47957
  exports.useLockControlGetStatus = useLockControlGetStatus;
47992
47958
  exports.useLockControlLock = useLockControlLock;
47993
47959
  exports.useLockControlOpen = useLockControlOpen;
@@ -48107,6 +48073,7 @@ exports.usePetFeederSetFeedSound = usePetFeederSetFeedSound;
48107
48073
  exports.usePetFeederSetIndicatorLight = usePetFeederSetIndicatorLight;
48108
48074
  exports.usePetFeederSetVolume = usePetFeederSetVolume;
48109
48075
  exports.usePipelineAnalyticsApplyDeviceSettingsPatch = usePipelineAnalyticsApplyDeviceSettingsPatch;
48076
+ exports.usePipelineAnalyticsCancelMediaRelocate = usePipelineAnalyticsCancelMediaRelocate;
48110
48077
  exports.usePipelineAnalyticsClearTracks = usePipelineAnalyticsClearTracks;
48111
48078
  exports.usePipelineAnalyticsDeleteDeviceEvents = usePipelineAnalyticsDeleteDeviceEvents;
48112
48079
  exports.usePipelineAnalyticsDeleteTracks = usePipelineAnalyticsDeleteTracks;
@@ -48118,6 +48085,7 @@ exports.usePipelineAnalyticsGetEventDensity = usePipelineAnalyticsGetEventDensit
48118
48085
  exports.usePipelineAnalyticsGetEventMedia = usePipelineAnalyticsGetEventMedia;
48119
48086
  exports.usePipelineAnalyticsGetEventStoreFootprint = usePipelineAnalyticsGetEventStoreFootprint;
48120
48087
  exports.usePipelineAnalyticsGetKeyEvents = usePipelineAnalyticsGetKeyEvents;
48088
+ exports.usePipelineAnalyticsGetMediaRelocateStatus = usePipelineAnalyticsGetMediaRelocateStatus;
48121
48089
  exports.usePipelineAnalyticsGetMotionEvents = usePipelineAnalyticsGetMotionEvents;
48122
48090
  exports.usePipelineAnalyticsGetObjectEvents = usePipelineAnalyticsGetObjectEvents;
48123
48091
  exports.usePipelineAnalyticsGetSensorEvents = usePipelineAnalyticsGetSensorEvents;
@@ -48130,6 +48098,7 @@ exports.usePipelineAnalyticsListTracks = usePipelineAnalyticsListTracks;
48130
48098
  exports.usePipelineAnalyticsPruneEvents = usePipelineAnalyticsPruneEvents;
48131
48099
  exports.usePipelineAnalyticsPruneEventsBefore = usePipelineAnalyticsPruneEventsBefore;
48132
48100
  exports.usePipelineAnalyticsPruneTracksBefore = usePipelineAnalyticsPruneTracksBefore;
48101
+ exports.usePipelineAnalyticsRelocateMedia = usePipelineAnalyticsRelocateMedia;
48133
48102
  exports.usePipelineAnalyticsSearchObjectEvents = usePipelineAnalyticsSearchObjectEvents;
48134
48103
  exports.usePipelineAnalyticsWipeAllAnalytics = usePipelineAnalyticsWipeAllAnalytics;
48135
48104
  exports.usePipelineExecutorCacheFrameInPool = usePipelineExecutorCacheFrameInPool;
@@ -48267,6 +48236,7 @@ exports.usePtzStop = usePtzStop;
48267
48236
  exports.useRebootReboot = useRebootReboot;
48268
48237
  exports.useRecordedPlayback = useRecordedPlayback;
48269
48238
  exports.useRecordingApplyDeviceSettingsPatch = useRecordingApplyDeviceSettingsPatch;
48239
+ exports.useRecordingCancelRelocate = useRecordingCancelRelocate;
48270
48240
  exports.useRecordingDeleteFootprint = useRecordingDeleteFootprint;
48271
48241
  exports.useRecordingExportCancelExport = useRecordingExportCancelExport;
48272
48242
  exports.useRecordingExportCreateExport = useRecordingExportCreateExport;
@@ -48280,12 +48250,16 @@ exports.useRecordingGetDeviceConfig = useRecordingGetDeviceConfig;
48280
48250
  exports.useRecordingGetDeviceLiveContribution = useRecordingGetDeviceLiveContribution;
48281
48251
  exports.useRecordingGetDeviceSettingsContribution = useRecordingGetDeviceSettingsContribution;
48282
48252
  exports.useRecordingGetPlaybackManifest = useRecordingGetPlaybackManifest;
48253
+ exports.useRecordingGetRelocateStatus = useRecordingGetRelocateStatus;
48283
48254
  exports.useRecordingGetStatus = useRecordingGetStatus;
48284
48255
  exports.useRecordingGetStorageUsage = useRecordingGetStorageUsage;
48285
48256
  exports.useRecordingListOpsLog = useRecordingListOpsLog;
48286
48257
  exports.useRecordingLocateSegment = useRecordingLocateSegment;
48287
48258
  exports.useRecordingPruneFootage = useRecordingPruneFootage;
48288
48259
  exports.useRecordingReadSegmentBytes = useRecordingReadSegmentBytes;
48260
+ exports.useRecordingRelocateFootage = useRecordingRelocateFootage;
48261
+ exports.useRecordingRenderClip = useRecordingRenderClip;
48262
+ exports.useRecordingRenderGif = useRecordingRenderGif;
48289
48263
  exports.useRecordingRescanStorage = useRecordingRescanStorage;
48290
48264
  exports.useRecordingSetDeviceConfig = useRecordingSetDeviceConfig;
48291
48265
  exports.useRemoteComponent = useRemoteComponent;
@@ -48368,6 +48342,7 @@ exports.useStreamBrokerPullAudioChunks = useStreamBrokerPullAudioChunks;
48368
48342
  exports.useStreamBrokerPullFrameHandles = useStreamBrokerPullFrameHandles;
48369
48343
  exports.useStreamBrokerRegenerateRtspToken = useStreamBrokerRegenerateRtspToken;
48370
48344
  exports.useStreamBrokerReleaseStreamWithCodec = useStreamBrokerReleaseStreamWithCodec;
48345
+ exports.useStreamBrokerRenderPreBufferClip = useStreamBrokerRenderPreBufferClip;
48371
48346
  exports.useStreamBrokerRestartProfile = useStreamBrokerRestartProfile;
48372
48347
  exports.useStreamBrokerRetractCameraStream = useStreamBrokerRetractCameraStream;
48373
48348
  exports.useStreamBrokerSetPreBufferDuration = useStreamBrokerSetPreBufferDuration;