@camstack/addon-pipeline 1.2.75 → 1.2.77

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.
Files changed (31) hide show
  1. package/dist/{addon-utils-a_36HH_N.js → addon-utils-bZcwLaVW.js} +1 -1
  2. package/dist/audio-analyzer/index.js +2 -2
  3. package/dist/audio-analyzer/index.mjs +1 -1
  4. package/dist/detection-pipeline/index.js +4 -4
  5. package/dist/detection-pipeline/index.mjs +2 -2
  6. package/dist/{dist-6ZJCV0jY.js → dist-DQZzL0Ri.js} +452 -42
  7. package/dist/{dist-T_8jMw7d.mjs → dist-HhcoV-Ky.mjs} +452 -42
  8. package/dist/{event-loop-stall-monitor-Cjof8vA1.js → event-loop-stall-monitor-CBOo1oh_.js} +1 -1
  9. package/dist/{event-loop-stall-monitor-XeGmL4xr.mjs → event-loop-stall-monitor-CqBEWGSZ.mjs} +1 -1
  10. package/dist/{lazy-sharp-DvwPafeP.js → lazy-sharp-DIC1rpea.js} +1 -1
  11. package/dist/motion-wasm/index.js +2 -2
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/pipeline-runner/index.js +278 -15
  14. package/dist/pipeline-runner/index.mjs +277 -14
  15. package/dist/recorder/index.js +368 -18
  16. package/dist/recorder/index.mjs +367 -17
  17. package/dist/session-decode/decode-worker-child.js +2 -2
  18. package/dist/session-decode/decode-worker-child.mjs +1 -1
  19. package/dist/stream-broker/_stub.js +2 -2
  20. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-H2OTmgR6.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-De_xzvjU.mjs} +3 -3
  21. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DHJSxHH1.mjs +26 -0
  22. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Dg08SxUW.mjs +26 -0
  23. package/dist/stream-broker/{hostInit-4XDN_Nt0.mjs → hostInit-CDS5frmk.mjs} +3 -3
  24. package/dist/stream-broker/index.js +306 -71
  25. package/dist/stream-broker/index.mjs +306 -71
  26. package/dist/stream-broker/remoteEntry.js +1 -1
  27. package/dist/{worker-protocol-SqED7Y9i.mjs → worker-protocol-BNlLGLBV.mjs} +1 -1
  28. package/dist/{worker-protocol-B0cbTXyw.js → worker-protocol-DZVGVAPU.js} +1 -1
  29. package/package.json +1 -1
  30. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-RYTOS6B-.mjs +0 -26
  31. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DfsLsn2e.mjs +0 -26
@@ -1,6 +1,6 @@
1
- const require_dist = require("../dist-6ZJCV0jY.js");
1
+ const require_dist = require("../dist-DQZzL0Ri.js");
2
2
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
3
- const require_addon_utils = require("../addon-utils-a_36HH_N.js");
3
+ const require_addon_utils = require("../addon-utils-bZcwLaVW.js");
4
4
  let node_crypto = require("node:crypto");
5
5
  let node_child_process = require("node:child_process");
6
6
  let node_path = require("node:path");
@@ -1710,7 +1710,7 @@ async function availabilityProfile(root, deviceId) {
1710
1710
  * NOT profiles (`events/`, plus the `strips/` trees the retired filmstrip tier
1711
1711
  * left on disk); a walker that trusts readdir would offer them as playable
1712
1712
  * profiles. */
1713
- var KNOWN_PROFILES = new Set([
1713
+ var KNOWN_PROFILES$1 = new Set([
1714
1714
  "high",
1715
1715
  "mid",
1716
1716
  "low"
@@ -1718,7 +1718,7 @@ var KNOWN_PROFILES = new Set([
1718
1718
  /** `<root>/<deviceId>/` PROFILE subdirectories. Empty when absent. */
1719
1719
  async function profilesOf(root, deviceId) {
1720
1720
  try {
1721
- return (await node_fs.promises.readdir(node_path.default.join(root, String(deviceId)))).filter((e) => KNOWN_PROFILES.has(e));
1721
+ return (await node_fs.promises.readdir(node_path.default.join(root, String(deviceId)))).filter((e) => KNOWN_PROFILES$1.has(e));
1722
1722
  } catch {
1723
1723
  return [];
1724
1724
  }
@@ -5665,26 +5665,29 @@ var PENDING_GRACE_MS = 3e4;
5665
5665
  * segment of every recording would be classified as a previous run's leftover
5666
5666
  * and dropped.
5667
5667
  */
5668
- var PREVIOUS_RUN_SKEW_MS = 1e3;
5668
+ var PREVIOUS_RUN_SKEW_MS$1 = 1e3;
5669
5669
  /**
5670
5670
  * Decide whether a missing flat file is a late flush or a previous run's entry.
5671
5671
  *
5672
- * ffmpeg's `-segment_list` file is not cleared between runs and a fresh watcher
5673
- * starts at `processed = 0`, so the first passes after every attach replay the
5674
- * PREVIOUS run's playlist. Those entries were relocated into the bucket tree
5675
- * long ago; their absence is the system working. Over 2026-08-10..13 they were
5676
- * 83% of all retries (median age 1 615 s, max 17.4 h) and produced 68 of the
5677
- * 122 `never landed` warnings a warning that reads like data loss for a file
5678
- * that was correctly filed hours earlier.
5672
+ * ffmpeg opens `-segment_list` in TRUNCATE mode when it spawns, but not
5673
+ * instantly: a fresh watcher starts at `processed = 0` and, in the window
5674
+ * between spawn and that truncation, reads the PREVIOUS run's playlist body.
5675
+ * Those entries were relocated into the bucket tree long ago; their absence is
5676
+ * the system working. Over 2026-08-10..13 they were 83% of all retries (median
5677
+ * age 1 615 s, max 17.4 h) and produced 68 of the 122 `never landed` warnings —
5678
+ * a warning that reads like data loss for a file that was correctly filed hours
5679
+ * earlier.
5679
5680
  *
5680
- * Note what this deliberately does NOT do: it does not skip a previous run's
5681
- * entry whose file is STILL THERE. That case is real crash recovery — an
5682
- * unclean stop leaves un-relocated segments in `.rec-tmp` and the playlist is
5683
- * the only record of their durations so the caller stats first and only asks
5684
- * this question once the file is known to be absent.
5681
+ * What this is NOT is crash recovery, and an earlier version of this comment
5682
+ * claimed it was. Once ffmpeg truncates, the previous run's un-relocated
5683
+ * segments are named by NOTHING, so no amount of care in this function can
5684
+ * reach them on 2026-08-14 that had stranded 179 GB across the fleet. They
5685
+ * are recovered from the DIRECTORY instead, by `staging-reconcile.ts`, which
5686
+ * the addon runs once at boot. This function only has to be right about the
5687
+ * entries a playlist does name.
5685
5688
  */
5686
5689
  function classifyMissingEntry(input) {
5687
- return input.segmentStartMs < input.writerStartedMs - PREVIOUS_RUN_SKEW_MS ? "stale-previous-run" : "pending";
5690
+ return input.segmentStartMs < input.writerStartedMs - PREVIOUS_RUN_SKEW_MS$1 ? "stale-previous-run" : "pending";
5688
5691
  }
5689
5692
  /** Whether a pending entry has outlived its grace. Pure — the caller clocks it. */
5690
5693
  function shouldGiveUpOnPending(input) {
@@ -6979,6 +6982,312 @@ var RetentionSweeper = class {
6979
6982
  }
6980
6983
  };
6981
6984
  //#endregion
6985
+ //#region src/recorder/addon/staging-reconcile.ts
6986
+ /**
6987
+ * Recover `.rec-tmp` segments that a writer restart orphaned.
6988
+ *
6989
+ * WHY THIS EXISTS
6990
+ * ───────────────
6991
+ * The segment watcher's ONLY record of what it still owes is ffmpeg's
6992
+ * `-segment_list live.m3u8`. ffmpeg opens that file in TRUNCATE mode on every
6993
+ * spawn, so a writer respawn destroys it. Every segment written but not yet
6994
+ * relocated at that instant becomes permanently unreachable:
6995
+ *
6996
+ * - no playlist names it, so no watcher will ever finalize it;
6997
+ * - `parseSegmentPath` cannot see it — staging is flat and dot-prefixed, kept
6998
+ * deliberately out of the hydrate walk — so retention never counts it;
6999
+ * - the one `.rec-tmp` sweep that exists (`sweepUnassignedStaging`) touches
7000
+ * only locations with ZERO placement assignments, never the live one.
7001
+ *
7002
+ * So the footage is not deleted and not kept: it is stranded, and it accrues
7003
+ * for as long as the install runs.
7004
+ *
7005
+ * `segment-watcher.ts` carries a comment asserting the opposite — that the
7006
+ * playlist "is NOT cleared between runs", which is what its `stale-previous-run`
7007
+ * classification and its documented "that is the crash recovery" path are built
7008
+ * on. The live filesystem refutes it, and a leftover that describes the right
7009
+ * design reads as verification: on 2026-08-14 `/recordings/.rec-tmp/590/high/`
7010
+ * held a `live.m3u8` of 50 entries, every one named at or after that writer's
7011
+ * 20:28:31 start, beside **5 863 older `.m4s` files referenced by nobody** —
7012
+ * the oldest from 2026-07-04. Fleet total: 179 GB, on an array at 90% full.
7013
+ *
7014
+ * WHAT THIS DOES NOT DO
7015
+ * ─────────────────────
7016
+ * It never deletes. Recovery is the same `SegmentStore.onFinalized` the live
7017
+ * path uses — stat, relocate into the UTC bucket, index — so a recovered
7018
+ * segment is indistinguishable from one that was never orphaned, and a segment
7019
+ * it cannot place stays exactly where it is for the next attempt.
7020
+ */
7021
+ /**
7022
+ * How far before the writer's own start a segment may be named and still count
7023
+ * as belonging to THIS run.
7024
+ *
7025
+ * Mirrors `segment-watcher.PREVIOUS_RUN_SKEW_MS`, and for the same reason:
7026
+ * ffmpeg names segments with whole-second `%s`, so the first segment of a run
7027
+ * carries the second already in progress when the process spawned. Without the
7028
+ * tolerance a recovery pass would reach for the live writer's opening segment —
7029
+ * the one file in the directory that is still being written.
7030
+ */
7031
+ var PREVIOUS_RUN_SKEW_MS = 1e3;
7032
+ /**
7033
+ * How much longer than nominal an inter-segment gap may be and still be read as
7034
+ * one segment's duration.
7035
+ *
7036
+ * ffmpeg cuts on keyframes, so a real segment overshoots `-segment_time` by up
7037
+ * to a GOP; a gap of twice nominal is still plausibly one segment. Anything
7038
+ * wider is a RUN BOUNDARY — the writer was down in between — and the honest
7039
+ * duration is the nominal one. Trusting the gap there would paint a solid
7040
+ * timeline bar across hours of footage that does not exist, which is a worse
7041
+ * failure than under-reporting by a few seconds.
7042
+ */
7043
+ var MAX_GAP_FACTOR = 2;
7044
+ /**
7045
+ * Decide which staged files belong to a previous run, and how long each ran.
7046
+ *
7047
+ * Pure — the caller does the I/O. The gate is the file's own name against the
7048
+ * writer's start: a segment named before this ffmpeg spawned cannot be one this
7049
+ * ffmpeg is writing, whatever the playlist says.
7050
+ */
7051
+ function planStagingRecovery(input) {
7052
+ const nominalMs = input.segmentSeconds * 1e3;
7053
+ const cutoffMs = input.writerStartedMs - PREVIOUS_RUN_SKEW_MS;
7054
+ const owned = input.playlistNames;
7055
+ const candidates = [];
7056
+ for (const name of input.names) {
7057
+ const startMs = parseEpochStartMs(name);
7058
+ if (startMs === null) continue;
7059
+ if (startMs >= cutoffMs) continue;
7060
+ if (owned?.has(name) === true) continue;
7061
+ candidates.push({
7062
+ name,
7063
+ startMs
7064
+ });
7065
+ }
7066
+ candidates.sort((a, b) => a.startMs - b.startMs);
7067
+ const maxGapMs = nominalMs * MAX_GAP_FACTOR;
7068
+ return candidates.map((c, i) => {
7069
+ const next = candidates[i + 1];
7070
+ const gapMs = next === void 0 ? 0 : next.startMs - c.startMs;
7071
+ const durMs = gapMs > 0 && gapMs <= maxGapMs ? gapMs : nominalMs;
7072
+ return {
7073
+ name: c.name,
7074
+ startMs: c.startMs,
7075
+ durMs
7076
+ };
7077
+ });
7078
+ }
7079
+ /** The real filesystem. The only production value of {@link StagingIo}. */
7080
+ var nodeStagingIo = {
7081
+ listDir: (dir) => node_fs.promises.readdir(dir),
7082
+ readPlaylist: (dir) => node_fs.promises.readFile(node_path.default.join(dir, "live.m3u8"), "utf8").catch(() => null)
7083
+ };
7084
+ /** The only `<deviceId>/` subdirectories of staging that hold segments. */
7085
+ var KNOWN_PROFILES = new Set([
7086
+ "high",
7087
+ "mid",
7088
+ "low"
7089
+ ]);
7090
+ /** Default orphans per chunk. 590/high alone had 5 863 of them. */
7091
+ var DEFAULT_CHUNK_SIZE = 100;
7092
+ var macrotask = () => new Promise((resolve) => {
7093
+ setTimeout(resolve, 0);
7094
+ });
7095
+ /**
7096
+ * Relocate every previous-run segment left in one staging dir.
7097
+ *
7098
+ * Chunked with a yield between batches: this runs alongside a live recorder and
7099
+ * a backlog is measured in thousands of files over shfs/FUSE — a single
7100
+ * uninterrupted loop is exactly the event-loop stall the recorder has already
7101
+ * paid for once (D128).
7102
+ *
7103
+ * A failure is per-segment and never aborts the pass: the file stays in staging
7104
+ * and the next attach retries it. Every drop is logged with the segment name —
7105
+ * silence reads as "never happened", and this whole class of loss went
7106
+ * unnoticed for six weeks precisely because nothing said anything.
7107
+ */
7108
+ async function recoverStagingOrphans(dir, input, deps) {
7109
+ let names;
7110
+ try {
7111
+ names = await deps.listDir(dir);
7112
+ } catch (err) {
7113
+ deps.logger.warn("recorder: staging recovery could not list the staging dir", { meta: {
7114
+ dir,
7115
+ error: err instanceof Error ? err.message : String(err)
7116
+ } });
7117
+ return {
7118
+ found: 0,
7119
+ recovered: 0,
7120
+ failed: 0,
7121
+ aborted: false
7122
+ };
7123
+ }
7124
+ let playlistNames;
7125
+ const body = await deps.readPlaylist(dir).catch(() => null);
7126
+ if (body !== null) playlistNames = new Set(parseLivePlaylist(body).map((e) => e.segPath.replace(/^.*\//, "")));
7127
+ const plan = planStagingRecovery({
7128
+ ...input,
7129
+ names,
7130
+ playlistNames
7131
+ });
7132
+ if (plan.length === 0) return {
7133
+ found: 0,
7134
+ recovered: 0,
7135
+ failed: 0,
7136
+ aborted: false
7137
+ };
7138
+ const chunkSize = deps.chunkSize ?? DEFAULT_CHUNK_SIZE;
7139
+ const yieldBetween = deps.yieldBetween ?? macrotask;
7140
+ let recovered = 0;
7141
+ let failed = 0;
7142
+ let aborted = false;
7143
+ for (let i = 0; i < plan.length; i++) {
7144
+ const orphan = plan[i];
7145
+ if (deps.shouldStop?.() === true) {
7146
+ aborted = true;
7147
+ deps.logger.warn("recorder: staging recovery stopped short by shutdown", { meta: {
7148
+ dir,
7149
+ recovered,
7150
+ failed,
7151
+ remaining: plan.length - i
7152
+ } });
7153
+ break;
7154
+ }
7155
+ try {
7156
+ await deps.finalize(orphan, `${dir}/${orphan.name}`);
7157
+ recovered += 1;
7158
+ } catch (err) {
7159
+ failed += 1;
7160
+ deps.logger.warn("recorder: staging recovery could not relocate an orphaned segment", { meta: {
7161
+ dir,
7162
+ seg: orphan.name,
7163
+ startMs: orphan.startMs,
7164
+ durMs: orphan.durMs,
7165
+ error: err instanceof Error ? err.message : String(err)
7166
+ } });
7167
+ }
7168
+ if ((i + 1) % chunkSize === 0 && i + 1 < plan.length) await yieldBetween();
7169
+ }
7170
+ deps.logger.info("recorder: recovered segments a writer restart had orphaned in staging", { meta: {
7171
+ dir,
7172
+ found: plan.length,
7173
+ recovered,
7174
+ failed,
7175
+ aborted,
7176
+ oldestMs: plan[0]?.startMs,
7177
+ newestMs: plan[plan.length - 1]?.startMs
7178
+ } });
7179
+ return {
7180
+ found: plan.length,
7181
+ recovered,
7182
+ failed,
7183
+ aborted
7184
+ };
7185
+ }
7186
+ /**
7187
+ * Boot reconcile: relocate every previous-run segment stranded in staging, on
7188
+ * every location, for every camera — attached or not.
7189
+ *
7190
+ * WHY BOOT, AND NOT ATTACH
7191
+ * ────────────────────────
7192
+ * Orphans are created by a writer restart, so by construction they all predate
7193
+ * the runner that finds them; boot is when the whole backlog is visible at
7194
+ * once. Attach looked like the natural hook and is the wrong one twice over: it
7195
+ * only ever reaches cameras that are currently recording (device 1438 had a
7196
+ * staging backlog while attached to nothing), and it puts unbounded filesystem
7197
+ * work on the path that must start recording NOW — measured, it kept microtasks
7198
+ * in flight across `RecordingController.stop()` and made the runner's 5 s
7199
+ * SIGTERM-grace test fail under load.
7200
+ *
7201
+ * `bootMs` is the cutoff, and it is what makes this safe to run while writers
7202
+ * are spawning: a live writer's segments are all named after boot, so the plan
7203
+ * cannot select one. Nothing is deleted, and a segment that cannot be placed is
7204
+ * left exactly where it is for the next boot.
7205
+ */
7206
+ async function recoverAllStagedOrphans(deps) {
7207
+ let dirs = 0;
7208
+ let found = 0;
7209
+ let recovered = 0;
7210
+ let failed = 0;
7211
+ let aborted = false;
7212
+ for (const location of deps.locations()) {
7213
+ const stagingRoot = `${location.root}/${STAGING_DIR_NAME}`;
7214
+ let deviceDirs;
7215
+ try {
7216
+ deviceDirs = await deps.listDir(stagingRoot);
7217
+ } catch {
7218
+ continue;
7219
+ }
7220
+ for (const deviceDir of deviceDirs) {
7221
+ if (aborted) break;
7222
+ const deviceId = Number(deviceDir);
7223
+ if (!Number.isInteger(deviceId) || deviceId <= 0) continue;
7224
+ let profileDirs;
7225
+ try {
7226
+ profileDirs = await deps.listDir(`${stagingRoot}/${deviceDir}`);
7227
+ } catch {
7228
+ continue;
7229
+ }
7230
+ const segmentSeconds = await deps.segmentSecondsFor(deviceId);
7231
+ for (const profile of profileDirs) {
7232
+ if (aborted) break;
7233
+ if (!KNOWN_PROFILES.has(profile)) continue;
7234
+ const dir = `${stagingRoot}/${deviceDir}/${profile}`;
7235
+ dirs += 1;
7236
+ const report = await recoverStagingOrphans(dir, {
7237
+ writerStartedMs: deps.bootMs,
7238
+ segmentSeconds
7239
+ }, {
7240
+ listDir: deps.listDir,
7241
+ readPlaylist: deps.readPlaylist,
7242
+ finalize: (orphan, flatAbsPath) => deps.finalize({
7243
+ deviceId,
7244
+ profile,
7245
+ locationId: location.id,
7246
+ locationRoot: location.root,
7247
+ flatAbsPath,
7248
+ startMs: orphan.startMs,
7249
+ durMs: orphan.durMs
7250
+ }),
7251
+ logger: withDeviceTag(deps.logger, deviceId),
7252
+ chunkSize: deps.chunkSize,
7253
+ yieldBetween: deps.yieldBetween,
7254
+ shouldStop: deps.shouldStop
7255
+ });
7256
+ found += report.found;
7257
+ recovered += report.recovered;
7258
+ failed += report.failed;
7259
+ if (report.aborted) aborted = true;
7260
+ }
7261
+ }
7262
+ }
7263
+ if (found > 0 || failed > 0) deps.logger.info("recorder: staging boot reconcile complete", { meta: {
7264
+ dirs,
7265
+ found,
7266
+ recovered,
7267
+ failed,
7268
+ aborted
7269
+ } });
7270
+ return {
7271
+ dirs,
7272
+ found,
7273
+ recovered,
7274
+ failed,
7275
+ aborted
7276
+ };
7277
+ }
7278
+ /** Bind `tags: { deviceId }` onto every line a per-dir pass writes. */
7279
+ function withDeviceTag(logger, deviceId) {
7280
+ const tag = (extras) => ({
7281
+ ...typeof extras === "object" && extras !== null ? extras : {},
7282
+ tags: { deviceId }
7283
+ });
7284
+ return {
7285
+ info: (message, extras) => logger.info(message, tag(extras)),
7286
+ warn: (message, extras) => logger.warn(message, tag(extras)),
7287
+ debug: (message, extras) => logger.debug(message, tag(extras))
7288
+ };
7289
+ }
7290
+ //#endregion
6982
7291
  //#region src/recorder/addon/index.ts
6983
7292
  /**
6984
7293
  * recorder addon SHELL — recording + storage-evictable providers on the
@@ -7516,11 +7825,13 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
7516
7825
  force: true
7517
7826
  }).catch(() => {});
7518
7827
  await this.sweepUnassignedStaging();
7828
+ const stagingBootMs = Date.now();
7519
7829
  try {
7520
7830
  await this.controller?.start();
7521
7831
  } catch (err) {
7522
7832
  this.ctx.logger.warn("recorder: controller start failed", { meta: { error: require_dist.errMsg(err) } });
7523
7833
  }
7834
+ this.recoverStagedOrphans(stagingBootMs);
7524
7835
  if (this.eventUnsub === null) this.eventUnsub = subscribeEventCapture({
7525
7836
  eventBus: this.ctx.eventBus,
7526
7837
  map: this.eventMap,
@@ -7825,6 +8136,45 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
7825
8136
  }
7826
8137
  }
7827
8138
  /**
8139
+ * Boot reconcile for `.rec-tmp`: relocate every segment a previous runner's
8140
+ * writers left stranded, on every location, for every camera.
8141
+ *
8142
+ * The counterpart to {@link sweepUnassignedStaging} above, which only ever
8143
+ * looks at locations nothing is assigned to — so the LIVE root, the one that
8144
+ * actually accumulates, had no pass at all (D148). Nothing here deletes: each
8145
+ * orphan goes through the same `SegmentStore.onFinalized` a live segment
8146
+ * does, and one it cannot place is left on disk for the next boot.
8147
+ */
8148
+ async recoverStagedOrphans(bootMs) {
8149
+ try {
8150
+ await recoverAllStagedOrphans({
8151
+ locations: () => this.resolvedLocations.map((l) => ({
8152
+ id: l.id,
8153
+ root: l.root
8154
+ })),
8155
+ listDir: nodeStagingIo.listDir,
8156
+ readPlaylist: nodeStagingIo.readPlaylist,
8157
+ finalize: async (input) => {
8158
+ const store = this.segmentStore;
8159
+ if (store === null) throw new Error("recorder is shutting down");
8160
+ await store.onFinalized(input);
8161
+ },
8162
+ logger: this.ctx.logger,
8163
+ bootMs,
8164
+ segmentSecondsFor: async (deviceId) => {
8165
+ try {
8166
+ return (await loadDeviceConfig(this.configStore(), deviceId)).segmentSeconds ?? this.config.segmentSeconds;
8167
+ } catch {
8168
+ return this.config.segmentSeconds;
8169
+ }
8170
+ },
8171
+ shouldStop: () => this.segmentStore === null
8172
+ });
8173
+ } catch (err) {
8174
+ this.ctx.logger.warn("recorder: staging boot reconcile failed", { meta: { error: require_dist.errMsg(err) } });
8175
+ }
8176
+ }
8177
+ /**
7828
8178
  * Surface a storage-placement fault on the liveness/alert surface. STRICTLY
7829
8179
  * best-effort: the alerts cap is a nice-to-have and an unmounted disk must
7830
8180
  * still be shouted about in the log even when it is unreachable.