@camstack/addon-export-hap 1.2.132 → 1.2.139

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.
@@ -90670,6 +90670,153 @@ function createClipThumbnailMinter(input) {
90670
90670
  };
90671
90671
  }
90672
90672
  //#endregion
90673
+ //#region src/hksv/recording-active.ts
90674
+ /**
90675
+ * `true`/`false` as HomeKit holds it, or `null` when this accessory does not
90676
+ * offer recording at all.
90677
+ */
90678
+ function readHomekitRecordingActive(accessories) {
90679
+ for (const accessory of accessories) {
90680
+ const service = accessory.getService(import_dist.Service.CameraRecordingManagement);
90681
+ if (service === void 0) continue;
90682
+ const value = service.getCharacteristic(import_dist.Characteristic.Active).value;
90683
+ if (typeof value === "number") return value === 1;
90684
+ if (typeof value === "boolean") return value;
90685
+ return null;
90686
+ }
90687
+ return null;
90688
+ }
90689
+ /**
90690
+ * One sentence for the settings page: what HomeKit says, or why it was never
90691
+ * asked.
90692
+ *
90693
+ * The cost stays in the text even though the switch is gone. It is still the
90694
+ * operator's to understand — a camera recording here runs a permanent ffmpeg —
90695
+ * and a status line that reported only the state would have quietly dropped
90696
+ * the one number that decides whether to turn it on.
90697
+ */
90698
+ function describeHomekitRecording(active, lastBuild) {
90699
+ if (active === true) return "HomeKit is recording this camera. Turn it off in the Home app (Camera settings → “Stream”), not here — iOS owns this switch and keeps it across reboots. While it is on, this camera runs a permanent prebuffer (~0.7% of one CPU core on the copy branch).";
90700
+ if (active === false) return "This camera offers recording and HomeKit has it off. Turn it on in the Home app (Camera settings → “Stream and Allow Recording”); it needs iCloud+ and a home hub. Nothing runs here until you do.";
90701
+ if (lastBuild !== null && !lastBuild.advertised) return `Recording is not offered for this camera: ${lastBuild.detail ?? "the accessory could not advertise it"}.`;
90702
+ return "Not known yet — this camera’s accessory has not published its recording service in this process. It appears once the accessory is built.";
90703
+ }
90704
+ //#endregion
90705
+ //#region src/hksv/recording-source.ts
90706
+ /** The tallest frame the recording path will hold in its prebuffer. */
90707
+ var MAX_RECORDING_HEIGHT = 1080;
90708
+ /** HKSV takes H.264 only — AAC audio and H.264 video, no negotiation. */
90709
+ var RECORDABLE_CODEC = "h264";
90710
+ /**
90711
+ * Pick the slot the recording child pulls.
90712
+ *
90713
+ * Deliberately NOT `pickPreferredRtspEntry`: that picker resolves the operator's
90714
+ * LIVE preference and, on `auto`, steers by the resolution iOS negotiated for a
90715
+ * live session — neither is a fact about recording, and on 615 it selects `mid`,
90716
+ * a 10 fps slot. Recording has one criterion, applied here and nowhere else:
90717
+ * the largest copyable frame that does not exceed {@link MAX_RECORDING_HEIGHT}.
90718
+ */
90719
+ function pickRecordingSource(entries, options = {}) {
90720
+ const enabled = entries.filter((e) => e.enabled);
90721
+ if (enabled.length === 0) return {
90722
+ ok: false,
90723
+ refusal: "no-enabled-stream"
90724
+ };
90725
+ const h264 = enabled.filter((e) => normaliseCodec(e.codec) === RECORDABLE_CODEC);
90726
+ const pool = h264.length > 0 ? h264 : options.allowTranscode === true ? enabled : [];
90727
+ if (pool.length === 0) return {
90728
+ ok: false,
90729
+ refusal: "no-h264-stream"
90730
+ };
90731
+ const transcode = h264.length === 0;
90732
+ const sized = pool.filter(hasUsableResolution);
90733
+ if (sized.length === 0) return {
90734
+ ok: false,
90735
+ refusal: "no-resolution"
90736
+ };
90737
+ const withinCeiling = sized.filter((e) => height(e) <= MAX_RECORDING_HEIGHT);
90738
+ const within = withinCeiling.length > 0;
90739
+ const best = [...within ? withinCeiling : sized].sort((a, b) => within ? height(b) - height(a) : height(a) - height(b))[0];
90740
+ if (best === void 0 || best.resolution === void 0) return {
90741
+ ok: false,
90742
+ refusal: "no-resolution"
90743
+ };
90744
+ return {
90745
+ ok: true,
90746
+ source: {
90747
+ brokerId: best.brokerId,
90748
+ profile: best.profile ?? best.brokerId,
90749
+ transcode,
90750
+ url: best.url,
90751
+ mutedUrl: best.mutedUrl,
90752
+ width: best.resolution.width,
90753
+ height: best.resolution.height
90754
+ }
90755
+ };
90756
+ }
90757
+ /** A one-line reason for the log — silence about a withdrawn service reads as a bug. */
90758
+ function refusalReason(refusal) {
90759
+ switch (refusal) {
90760
+ case "no-enabled-stream": return "the camera has no enabled RTSP profile";
90761
+ case "no-h264-stream": return "every enabled profile is H.265 — HKSV takes H.264 only, and a permanent transcode costs 128x a copy";
90762
+ case "no-resolution": return "no enabled profile declares a resolution, so nothing honest could be advertised";
90763
+ }
90764
+ }
90765
+ function height(entry) {
90766
+ return entry.resolution?.height ?? 0;
90767
+ }
90768
+ function hasUsableResolution(entry) {
90769
+ const r = entry.resolution;
90770
+ return r !== void 0 && r.width > 0 && r.height > 0;
90771
+ }
90772
+ /** Publishers spell H.265 four ways; the same normalisation the broker uses. */
90773
+ function normaliseCodec(codec) {
90774
+ return (codec ?? "").toLowerCase().replace(/[.\s-]/g, "");
90775
+ }
90776
+ //#endregion
90777
+ //#region src/hksv/recordability-token.ts
90778
+ /** No slot is recordable. A real answer, and a fingerprint-moving one. */
90779
+ var RECORDABILITY_NONE = "rec:none";
90780
+ /**
90781
+ * The slot HKSV would record from, as a fingerprint token.
90782
+ *
90783
+ * `null` when `entries` is null — the profiles could not be read, which is not
90784
+ * a claim about the camera.
90785
+ */
90786
+ function recordabilityToken(entries, options = {}) {
90787
+ if (entries === null) return null;
90788
+ const choice = pickRecordingSource(entries, options);
90789
+ return choice.ok ? `rec:${choice.source.profile}${choice.source.transcode ? "+x" : ""}` : RECORDABILITY_NONE;
90790
+ }
90791
+ var TOKEN_SUFFIX = /\|(rec:[^|]*)$/;
90792
+ /**
90793
+ * Attach the recording token to a device-shape fingerprint.
90794
+ *
90795
+ * On an unreadable read the persisted token is carried forward — but only onto
90796
+ * the base it was persisted against. If the device's SHAPE also changed, that
90797
+ * change is a rebuild in its own right and the token has nothing to say about
90798
+ * it; inventing one there would advertise a recording slot nobody read.
90799
+ */
90800
+ function withRecordabilityToken(input) {
90801
+ if (input.token !== null) return `${input.base}|${input.token}`;
90802
+ const carried = input.persisted?.match(TOKEN_SUFFIX);
90803
+ if (carried === null || carried === void 0) return input.base;
90804
+ return carried.input?.slice(0, carried.index) === input.base ? `${input.base}|${carried[1]}` : input.base;
90805
+ }
90806
+ /**
90807
+ * What this camera could record, as one fingerprint token.
90808
+ *
90809
+ * There used to be an `rec:off` here, for a per-camera switch of ours. That
90810
+ * switch is gone (D589): whether HomeKit RECORDS is HomeKit's own persisted
90811
+ * characteristic, it does not change the accessory's shape, and a rebuild is
90812
+ * about shape. What still belongs here is the CAPABILITY — which slot, and
90813
+ * whether serving it means running an encoder — because those are two
90814
+ * different accessories to build.
90815
+ */
90816
+ function recordabilityTokenFor(input) {
90817
+ return recordabilityToken(input.entries, { allowTranscode: input.allowTranscode !== false });
90818
+ }
90819
+ //#endregion
90673
90820
  //#region src/hksv/videoclips-plane.ts
90674
90821
  /**
90675
90822
  * The two data-plane routes behind the HomeKit clip source.
@@ -90932,8 +91079,9 @@ function createHksvClipPlanes(deps) {
90932
91079
  *
90933
91080
  * ## The source EXISTS because of HomeKit, and is not a switch
90934
91081
  *
90935
- * A camera has this source because it is exported to HomeKit AND HomeKit
90936
- * recording is on for it; turn either off and the row is gone (D550, D569 § 5).
91082
+ * A camera has this source because it is exported to HomeKit and HomeKit has
91083
+ * not said no to recording it; un-export it, or switch recording off in the
91084
+ * Home app, and the row is gone (D550, D569 § 5, D589).
90937
91085
  * There is deliberately no enable control of its own — a knob over somebody
90938
91086
  * else's decision is D62's defect. `keepClips` is NOT that gate either: it
90939
91087
  * governs the TEE, and the clips already kept remain the truth about this
@@ -90980,7 +91128,7 @@ function createHksvVideoclipsProvider(deps) {
90980
91128
  */
90981
91129
  async function measure(deviceId) {
90982
91130
  const facts = deps.describeCamera(deviceId);
90983
- if (!facts.exported || !facts.recording) return null;
91131
+ if (!facts.exported || facts.homekitRecording === false) return null;
90984
91132
  const catalog = await deps.readCatalog(deviceId);
90985
91133
  return {
90986
91134
  availability: availabilityFor(facts, catalog),
@@ -106335,6 +106483,72 @@ function buildRecordingOptions(input) {
106335
106483
  };
106336
106484
  }
106337
106485
  //#endregion
106486
+ //#region src/hksv/recording-invocation.ts
106487
+ /**
106488
+ * The resolution the accessory may advertise, which is the one it can deliver.
106489
+ *
106490
+ * A copy is advertised at its source size whatever that is — there is no
106491
+ * encoder to resize with, and advertising a size we cannot produce is the
106492
+ * "stream that does not match its own advertisement" defect in miniature.
106493
+ * A transcode above the ceiling is fitted down, because with an encoder in the
106494
+ * pipeline the 6.35 MB 4K fragment is a choice rather than a fact, and iOS
106495
+ * renders HKSV clips at 1080p regardless.
106496
+ */
106497
+ function recordingOutput(source) {
106498
+ if (!source.transcode || source.height <= 1080) return {
106499
+ width: source.width,
106500
+ height: source.height,
106501
+ scaled: false
106502
+ };
106503
+ return {
106504
+ width: Math.max(2, Math.round(source.width * MAX_RECORDING_HEIGHT / source.height / 2) * 2),
106505
+ height: MAX_RECORDING_HEIGHT,
106506
+ scaled: true
106507
+ };
106508
+ }
106509
+ /** The invocation, minus the sink and sidecar the child owns. */
106510
+ function buildRecordingInvocation(input) {
106511
+ const { source } = input;
106512
+ const audio = input.audioActive ? {
106513
+ kind: "encode",
106514
+ codec: "aac",
106515
+ bitrateKbps: 32,
106516
+ sampleRateHz: HKSV_AUDIO_SAMPLE_RATE_HZ,
106517
+ channels: 1
106518
+ } : { kind: "none" };
106519
+ const out = recordingOutput(source);
106520
+ const video = source.transcode ? {
106521
+ kind: "encode",
106522
+ encoder: "libx264",
106523
+ preset: "veryfast",
106524
+ profile: "main",
106525
+ level: "4.0",
106526
+ pixelFormat: "yuv420p",
106527
+ forceKeyFramesSeconds: Math.max(1, Math.round(input.fragmentMs / 1e3)),
106528
+ bf: 0,
106529
+ ...out.scaled ? { scale: {
106530
+ mode: "fit",
106531
+ width: out.width,
106532
+ height: out.height
106533
+ } } : {}
106534
+ } : { kind: "copy" };
106535
+ return {
106536
+ logLevel: "error",
106537
+ decodeHwAccel: source.transcode ? input.decodeHwAccel : null,
106538
+ input: {
106539
+ url: input.audioActive ? source.url : source.mutedUrl,
106540
+ rtspTransport: "tcp",
106541
+ ...source.transcode && input.decodeInputArgs !== void 0 && input.decodeInputArgs.length > 0 ? { extraArgs: input.decodeInputArgs } : {},
106542
+ analyzeDurationUs: 1e6,
106543
+ probeSizeBytes: 1e6
106544
+ },
106545
+ video,
106546
+ audio,
106547
+ threadCount: 0,
106548
+ outputArgs: []
106549
+ };
106550
+ }
106551
+ //#endregion
106338
106552
  //#region src/hksv/fragment-source.ts
106339
106553
  /**
106340
106554
  * How far back the prebuffer ring reaches.
@@ -106502,6 +106716,7 @@ var HksvFragmentSource = class {
106502
106716
  meta: {
106503
106717
  brokerId: this.input.source.brokerId,
106504
106718
  resolution: `${this.input.source.width}x${this.input.source.height}`,
106719
+ branch: this.input.source.transcode ? "transcode" : "copy",
106505
106720
  fragmentMs: this.input.fragmentMs,
106506
106721
  audioActive: this.audioActive,
106507
106722
  windowMs: PREBUFFER_WINDOW_MS
@@ -106566,27 +106781,13 @@ var HksvFragmentSource = class {
106566
106781
  * takes AAC only — which the same measurement priced at 0.4 % of a core.
106567
106782
  */
106568
106783
  buildInvocation() {
106569
- const audio = this.audioActive ? {
106570
- kind: "encode",
106571
- codec: "aac",
106572
- bitrateKbps: 32,
106573
- sampleRateHz: HKSV_AUDIO_SAMPLE_RATE_HZ,
106574
- channels: 1
106575
- } : { kind: "none" };
106576
- return {
106577
- logLevel: "error",
106578
- decodeHwAccel: null,
106579
- input: {
106580
- url: this.audioActive ? this.input.source.url : this.input.source.mutedUrl,
106581
- rtspTransport: "tcp",
106582
- analyzeDurationUs: 1e6,
106583
- probeSizeBytes: 1e6
106584
- },
106585
- video: { kind: "copy" },
106586
- audio,
106587
- threadCount: 0,
106588
- outputArgs: []
106589
- };
106784
+ return buildRecordingInvocation({
106785
+ source: this.input.source,
106786
+ audioActive: this.audioActive,
106787
+ fragmentMs: this.input.fragmentMs,
106788
+ decodeHwAccel: this.input.decodeHwAccel ?? null,
106789
+ ...this.input.decodeInputArgs === void 0 ? {} : { decodeInputArgs: this.input.decodeInputArgs }
106790
+ });
106590
106791
  }
106591
106792
  };
106592
106793
  //#endregion
@@ -106946,74 +107147,6 @@ var HksvRecordingDelegate = class {
106946
107147
  }
106947
107148
  };
106948
107149
  //#endregion
106949
- //#region src/hksv/recording-source.ts
106950
- /** The tallest frame the recording path will hold in its prebuffer. */
106951
- var MAX_RECORDING_HEIGHT = 1080;
106952
- /** HKSV takes H.264 only — AAC audio and H.264 video, no negotiation. */
106953
- var RECORDABLE_CODEC = "h264";
106954
- /**
106955
- * Pick the slot the recording child pulls.
106956
- *
106957
- * Deliberately NOT `pickPreferredRtspEntry`: that picker resolves the operator's
106958
- * LIVE preference and, on `auto`, steers by the resolution iOS negotiated for a
106959
- * live session — neither is a fact about recording, and on 615 it selects `mid`,
106960
- * a 10 fps slot. Recording has one criterion, applied here and nowhere else:
106961
- * the largest copyable frame that does not exceed {@link MAX_RECORDING_HEIGHT}.
106962
- */
106963
- function pickRecordingSource(entries) {
106964
- const enabled = entries.filter((e) => e.enabled);
106965
- if (enabled.length === 0) return {
106966
- ok: false,
106967
- refusal: "no-enabled-stream"
106968
- };
106969
- const h264 = enabled.filter((e) => normaliseCodec(e.codec) === RECORDABLE_CODEC);
106970
- if (h264.length === 0) return {
106971
- ok: false,
106972
- refusal: "no-h264-stream"
106973
- };
106974
- const sized = h264.filter(hasUsableResolution);
106975
- if (sized.length === 0) return {
106976
- ok: false,
106977
- refusal: "no-resolution"
106978
- };
106979
- const withinCeiling = sized.filter((e) => height(e) <= MAX_RECORDING_HEIGHT);
106980
- const best = [...withinCeiling.length > 0 ? withinCeiling : sized].sort((a, b) => withinCeiling.length > 0 ? height(b) - height(a) : height(a) - height(b))[0];
106981
- if (best === void 0 || best.resolution === void 0) return {
106982
- ok: false,
106983
- refusal: "no-resolution"
106984
- };
106985
- return {
106986
- ok: true,
106987
- source: {
106988
- brokerId: best.brokerId,
106989
- profile: best.profile ?? best.brokerId,
106990
- url: best.url,
106991
- mutedUrl: best.mutedUrl,
106992
- width: best.resolution.width,
106993
- height: best.resolution.height
106994
- }
106995
- };
106996
- }
106997
- /** A one-line reason for the log — silence about a withdrawn service reads as a bug. */
106998
- function refusalReason(refusal) {
106999
- switch (refusal) {
107000
- case "no-enabled-stream": return "the camera has no enabled RTSP profile";
107001
- case "no-h264-stream": return "every enabled profile is H.265 — HKSV takes H.264 only, and a permanent transcode costs 128x a copy";
107002
- case "no-resolution": return "no enabled profile declares a resolution, so nothing honest could be advertised";
107003
- }
107004
- }
107005
- function height(entry) {
107006
- return entry.resolution?.height ?? 0;
107007
- }
107008
- function hasUsableResolution(entry) {
107009
- const r = entry.resolution;
107010
- return r !== void 0 && r.width > 0 && r.height > 0;
107011
- }
107012
- /** Publishers spell H.265 four ways; the same normalisation the broker uses. */
107013
- function normaliseCodec(codec) {
107014
- return (codec ?? "").toLowerCase().replace(/[.\s-]/g, "");
107015
- }
107016
- //#endregion
107017
107150
  //#region src/hksv/build-recording.ts
107018
107151
  /**
107019
107152
  * Assemble HomeKit Secure Video for one camera — the ADVERTISEMENT and the
@@ -107065,7 +107198,7 @@ async function buildHksvRecording(input) {
107065
107198
  note("profiles-unreadable", "cameraStreams.getProfileRtspEntries could not be read");
107066
107199
  return null;
107067
107200
  }
107068
- const choice = pickRecordingSource(entries);
107201
+ const choice = pickRecordingSource(entries, { allowTranscode: bctx.options.hapDeviceSettings?.allowTranscode !== false });
107069
107202
  if (!choice.ok) {
107070
107203
  log.info("export-hap: HKSV withheld — no recordable stream", { meta: {
107071
107204
  refusal: choice.refusal,
@@ -107075,9 +107208,10 @@ async function buildHksvRecording(input) {
107075
107208
  return null;
107076
107209
  }
107077
107210
  const source = choice.source;
107211
+ const decodeHwAccel = source.transcode ? await resolveDecodeHwAccel(bctx, log) : null;
107078
107212
  const gopMs = await readSourceGopMs(bctx, source.width, source.height);
107079
- const fragmentLengthMs = deriveFragmentLengthMs(gopMs);
107080
- if (fragmentLengthMs === null) {
107213
+ const fragmentLengthMs = source.transcode ? HKSV_FRAGMENT_LENGTHS_MS[0] : deriveFragmentLengthMs(gopMs);
107214
+ if (fragmentLengthMs === void 0 || fragmentLengthMs === null) {
107081
107215
  log.warn("export-hap: HKSV withheld — the camera key-frame interval is longer than any fragment length we advertise", { meta: {
107082
107216
  gopMs,
107083
107217
  brokerId: source.brokerId
@@ -107088,9 +107222,10 @@ async function buildHksvRecording(input) {
107088
107222
  const parsedProfile = CamProfileSchema.safeParse(source.profile);
107089
107223
  const recordedProfile = parsedProfile.success ? parsedProfile.data : null;
107090
107224
  const fps = resolveFps(input.fpsByProfile, source.profile);
107225
+ const out = recordingOutput(source);
107091
107226
  const options = buildRecordingOptions({
107092
- width: source.width,
107093
- height: source.height,
107227
+ width: out.width,
107228
+ height: out.height,
107094
107229
  fps,
107095
107230
  fragmentLengthMs
107096
107231
  });
@@ -107100,8 +107235,8 @@ async function buildHksvRecording(input) {
107100
107235
  logger: log,
107101
107236
  deviceId: numericDeviceId,
107102
107237
  store: clipStore,
107103
- width: source.width,
107104
- height: source.height,
107238
+ width: out.width,
107239
+ height: out.height,
107105
107240
  fragmentMs: fragmentLengthMs,
107106
107241
  ...recordedProfile === null ? {} : { profile: recordedProfile }
107107
107242
  });
@@ -107120,14 +107255,21 @@ async function buildHksvRecording(input) {
107120
107255
  spawnFn: node_child_process.spawn,
107121
107256
  source,
107122
107257
  fragmentMs,
107123
- audioActive
107258
+ audioActive,
107259
+ decodeHwAccel: decodeHwAccel?.hwaccel ?? null,
107260
+ ...decodeHwAccel === null || decodeHwAccel.extraInputArgs.length === 0 ? {} : { decodeInputArgs: decodeHwAccel.extraInputArgs }
107124
107261
  })
107125
107262
  });
107126
107263
  const advertisedResolution = options.video.resolutions[0];
107127
107264
  log.info("export-hap: HKSV ADVERTISED — recording is offered for this camera", { meta: {
107128
107265
  brokerId: source.brokerId,
107129
107266
  profile: source.profile,
107130
- resolution: `${source.width}x${source.height}`,
107267
+ resolution: `${out.width}x${out.height}`,
107268
+ branch: source.transcode ? "transcode" : "copy",
107269
+ ...source.transcode ? {
107270
+ sourceResolution: `${source.width}x${source.height}`,
107271
+ decodeHwAccel: decodeHwAccel?.hwaccel ?? "software"
107272
+ } : {},
107131
107273
  measuredFps: fps,
107132
107274
  advertisedFps: advertisedResolution?.[2] ?? null,
107133
107275
  fragmentLengthMs,
@@ -107142,6 +107284,27 @@ async function buildHksvRecording(input) {
107142
107284
  dispose: () => delegate.dispose()
107143
107285
  };
107144
107286
  }
107287
+ /**
107288
+ * The `-hwaccel` value for a transcoding prebuffer, and the flags that travel
107289
+ * with it.
107290
+ *
107291
+ * Software is a legitimate answer and is always NAMED: a camera that quietly
107292
+ * stopped using the GPU and one that never had it look identical in a CPU
107293
+ * graph, and on this branch the difference is most of a core, permanently.
107294
+ */
107295
+ async function resolveDecodeHwAccel(bctx, log) {
107296
+ const decision = selectHwDecode({
107297
+ transcode: true,
107298
+ reading: await probeDecoderHwaccel({
107299
+ source: decoderInfoSourceFromContext(bctx.ctx),
107300
+ log,
107301
+ memo: bctx.options.decodeMemos.reading
107302
+ }),
107303
+ platform: process.platform
107304
+ });
107305
+ if (decision.kind === "software") log.warn("export-hap: HKSV will transcode in SOFTWARE — this costs most of a core, permanently", { meta: { reason: decision.reason } });
107306
+ return decision;
107307
+ }
107145
107308
  /** `cameraStreams.getProfileRtspEntries`, or `null` when the cap is unreachable. */
107146
107309
  async function readProfileEntries(bctx) {
107147
107310
  try {
@@ -108137,13 +108300,12 @@ async function buildCameraAccessory(input) {
108137
108300
  bctx,
108138
108301
  streamingOptions: streams.streamingOptions
108139
108302
  }));
108140
- const recordingEnabled = options.hapDeviceSettings.hksvRecording === true;
108141
108303
  let recordingAudioActive = true;
108142
- const recording = recordingEnabled ? await buildHksvRecording({
108304
+ const recording = await buildHksvRecording({
108143
108305
  bctx,
108144
108306
  fpsByProfile: advertisedVideo.fpsByProfile,
108145
108307
  isAudioActive: () => recordingAudioActive
108146
- }) : null;
108308
+ });
108147
108309
  const controller = new (isDoorbell ? import_dist.DoorbellController : import_dist.CameraController)({
108148
108310
  delegate: streams.delegate,
108149
108311
  streamingOptions: streams.streamingOptions,
@@ -108386,23 +108548,13 @@ function syncStateToJson(map) {
108386
108548
  * capabilities bound to it — restricting coverage by type is precisely what
108387
108549
  * confined this exporter to cameras for its first three rounds.
108388
108550
  */
108389
- var DEFAULT_DEVICE_SETTINGS = {
108390
- streamPreference: "auto",
108391
- hksvRecording: true
108392
- };
108393
108551
  /**
108394
- * ON unless explicitly switched off — operator decision 2026-08-08 (flipped
108395
- * from the launch default of off). ABSENT must resolve to ON or the flip is a
108396
- * lie for every entry persisted before the field existed, so every read goes
108397
- * through this one resolver (`!== false`), never a scattered `=== true`. The
108398
- * cost that made off-by-default look prudent is measured and small on the only
108399
- * branch the recorder accepts (copy: 0.7 % of a core / ~30 MB RSS, D84), and a
108400
- * camera the recorder cannot copy refuses recording with a logged reason
108401
- * rather than paying for a transcode.
108552
+ * What an entry with no stored settings is read as — and what a camera
108553
+ * exported TODAY is built from. Exported so a spec can assert the one thing
108554
+ * that matters about it: it must resolve to NOT recording, or "off by default"
108555
+ * is true only for cameras exposed before the flip.
108402
108556
  */
108403
- function resolveHksvRecording(settings) {
108404
- return settings?.hksvRecording !== false;
108405
- }
108557
+ var DEFAULT_DEVICE_SETTINGS = { streamPreference: "auto" };
108406
108558
  /**
108407
108559
  * Whether this camera offers Apple's NEW camera service alongside the classic
108408
108560
  * one — `camera-multi-tier-rtp-stream-management` (0x8031), where HEVC and 4K
@@ -108445,11 +108597,11 @@ var EXPORT_HAP_ADDON_ID = "export-hap";
108445
108597
  * stays and says the copies are off, which is D62's other half: an off switch
108446
108598
  * is REPORTED off, never made to look like a broken camera.
108447
108599
  */
108448
- function describeHksvCamera(entry, lastBuild) {
108600
+ function describeHksvCamera(entry, lastBuild, homekitRecording) {
108449
108601
  const isCamera = entry !== null && entry.mapperKind === "camera";
108450
108602
  return {
108451
108603
  exported: isCamera,
108452
- recording: isCamera && resolveHksvRecording(entry.settings),
108604
+ homekitRecording: isCamera ? homekitRecording : null,
108453
108605
  keepingClips: isCamera && resolveKeepHomekitClips(entry.settings),
108454
108606
  lastBuild
108455
108607
  };
@@ -108693,7 +108845,7 @@ var ExportHapAddon = class extends BaseAddon {
108693
108845
  const clipsProvider = createHksvVideoclipsProvider({
108694
108846
  logger: this.ctx.logger.child("hksv-clips"),
108695
108847
  addonId: EXPORT_HAP_ADDON_ID,
108696
- describeCamera: (deviceId) => describeHksvCamera(this.findEntry(deviceId), this.hksvBuilds.lastFor(deviceId)),
108848
+ describeCamera: (deviceId) => describeHksvCamera(this.findEntry(deviceId), this.hksvBuilds.lastFor(deviceId), readHomekitRecordingActive(this.exposed.get(String(deviceId))?.accessories ?? [])),
108697
108849
  readCatalog: async (deviceId) => await this.hksvClipStore?.listCatalog(deviceId) ?? { kind: "no-root" },
108698
108850
  locationRefusal: () => this.hksvClipLocation?.lastRefusal ?? null,
108699
108851
  mediaUrl: (deviceId, stem) => this.hksvClipUrl("media", deviceId, stem),
@@ -108849,7 +109001,6 @@ var ExportHapAddon = class extends BaseAddon {
108849
109001
  },
108850
109002
  hapDeviceSettings: {
108851
109003
  streamPreference: entrySettings.streamPreference ?? "auto",
108852
- hksvRecording: resolveHksvRecording(entrySettings),
108853
109004
  keepHomekitClips: resolveKeepHomekitClips(entrySettings),
108854
109005
  multiTierService: resolveMultiTierService(entrySettings),
108855
109006
  allowTranscode: entrySettings.allowTranscode !== false
@@ -108930,15 +109081,40 @@ var ExportHapAddon = class extends BaseAddon {
108930
109081
  features: device.features
108931
109082
  });
108932
109083
  const persisted = syncStateFromJson(this.config.exportSyncState).get(deviceId)?.fingerprint;
109084
+ const withToken = withRecordabilityToken({
109085
+ base: fresh,
109086
+ token: await this.readRecordabilityToken(deviceId, device.type),
109087
+ persisted
109088
+ });
108933
109089
  return resolveExportFingerprint({
108934
109090
  ready: device.probed ?? false,
108935
- fresh,
109091
+ fresh: withToken,
108936
109092
  persisted
108937
109093
  });
108938
109094
  } catch {
108939
109095
  return null;
108940
109096
  }
108941
109097
  }
109098
+ /**
109099
+ * The slot HKSV would record this camera from, for the fingerprint.
109100
+ *
109101
+ * `null` — do not move the fingerprint — for anything that is not a camera
109102
+ * (a switch has no recording decision, and stamping a constant on it would
109103
+ * republish every non-camera accessory once for nothing) and for a camera
109104
+ * whose profiles could not be read right now.
109105
+ */
109106
+ async readRecordabilityToken(deviceId, type) {
109107
+ if (pickMapperKind(type) !== "camera") return null;
109108
+ try {
109109
+ const entries = await (await this.ctx.fetchDevice(deviceId)).cameraStreams?.getProfileRtspEntries({});
109110
+ return recordabilityTokenFor({
109111
+ allowTranscode: this.findEntry(deviceId)?.settings?.allowTranscode !== false,
109112
+ entries: entries ?? null
109113
+ });
109114
+ } catch {
109115
+ return null;
109116
+ }
109117
+ }
108942
109118
  /** Persist a device's current export fingerprint into `exportSyncState`
108943
109119
  * (best-effort; called after a successful attach). */
108944
109120
  async persistFingerprint(deviceId, fingerprint) {
@@ -109204,7 +109380,6 @@ var ExportHapAddon = class extends BaseAddon {
109204
109380
  const enabled = entry !== null;
109205
109381
  const enabledKey = `hap:${deviceId}:enabled`;
109206
109382
  const streamPreferenceKey = `hap:${deviceId}:streamPreference`;
109207
- const hksvKey = `hap:${deviceId}:hksvRecording`;
109208
109383
  const multiTierKey = `hap:${deviceId}:multiTierService`;
109209
109384
  const useBridgeKey = `hap:${deviceId}:useBridge`;
109210
109385
  const allowTranscodeKey = `hap:${deviceId}:allowTranscode`;
@@ -109255,17 +109430,14 @@ var ExportHapAddon = class extends BaseAddon {
109255
109430
  immediate: true
109256
109431
  },
109257
109432
  {
109258
- type: "boolean",
109259
- key: hksvKey,
109433
+ type: "info",
109434
+ key: `hap:${deviceId}:hksvRecordingStatus`,
109260
109435
  label: "HomeKit recording (Secure Video)",
109261
- description: "Offer “Stream and Allow Recording” in iOS Home. Requires iCloud+ and a home hub. Keeps a continuous 8s prebuffer for this camera (~0.7% of one CPU core, H.264 sources only).",
109262
- style: "switch",
109263
- value: resolveHksvRecording(settings),
109436
+ content: describeHomekitRecording(readHomekitRecordingActive(this.exposed.get(String(deviceId))?.accessories ?? []), this.hksvBuilds.lastFor(deviceId)),
109264
109437
  showWhen: {
109265
109438
  field: enabledKey,
109266
109439
  equals: true
109267
- },
109268
- immediate: true
109440
+ }
109269
109441
  },
109270
109442
  {
109271
109443
  type: "boolean",
@@ -109288,7 +109460,7 @@ var ExportHapAddon = class extends BaseAddon {
109288
109460
  style: "switch",
109289
109461
  value: resolveKeepHomekitClips(settings),
109290
109462
  showWhen: {
109291
- field: hksvKey,
109463
+ field: enabledKey,
109292
109464
  equals: true
109293
109465
  },
109294
109466
  immediate: true
@@ -109388,7 +109560,6 @@ var ExportHapAddon = class extends BaseAddon {
109388
109560
  const wasEnabled = this.exposed.has(deviceIdStr);
109389
109561
  const enabledKey = `hap:${deviceId}:enabled`;
109390
109562
  const streamPreferenceKey = `hap:${deviceId}:streamPreference`;
109391
- const hksvKey = `hap:${deviceId}:hksvRecording`;
109392
109563
  const useBridgeKey = `hap:${deviceId}:useBridge`;
109393
109564
  const allowTranscodeKey = `hap:${deviceId}:allowTranscode`;
109394
109565
  const multiTierKey = `hap:${deviceId}:multiTierService`;
@@ -109397,7 +109568,6 @@ var ExportHapAddon = class extends BaseAddon {
109397
109568
  const enabledValue = enabledKey in patch ? Boolean(patch[enabledKey]) : wasEnabled;
109398
109569
  const streamPreferenceRaw = streamPreferenceKey in patch ? patch[streamPreferenceKey] : current?.settings?.streamPreference;
109399
109570
  const streamPreference = typeof streamPreferenceRaw === "string" && streamPreferenceRaw.trim().length > 0 ? streamPreferenceRaw : "auto";
109400
- const hksvRecording = hksvKey in patch ? Boolean(patch[hksvKey]) : resolveHksvRecording(current?.settings);
109401
109571
  const useBridge = useBridgeKey in patch ? Boolean(patch[useBridgeKey]) : current?.settings?.useBridge !== false;
109402
109572
  const allowTranscode = allowTranscodeKey in patch ? Boolean(patch[allowTranscodeKey]) : current?.settings?.allowTranscode !== false;
109403
109573
  const multiTierService = multiTierKey in patch ? Boolean(patch[multiTierKey]) : resolveMultiTierService(current?.settings);
@@ -109409,7 +109579,6 @@ var ExportHapAddon = class extends BaseAddon {
109409
109579
  const nextSettings = {
109410
109580
  ...current?.settings ?? DEFAULT_DEVICE_SETTINGS,
109411
109581
  streamPreference,
109412
- hksvRecording,
109413
109582
  useBridge,
109414
109583
  allowTranscode,
109415
109584
  multiTierService,
@@ -109426,7 +109595,6 @@ var ExportHapAddon = class extends BaseAddon {
109426
109595
  return { success: true };
109427
109596
  }
109428
109597
  const currentPref = current?.settings?.streamPreference ?? "auto";
109429
- const currentHksv = resolveHksvRecording(current?.settings);
109430
109598
  const currentKeepClips = resolveKeepHomekitClips(current?.settings);
109431
109599
  const mapperKind = current?.mapperKind ?? "generic";
109432
109600
  const wasBridged = shouldBridge({
@@ -109438,16 +109606,12 @@ var ExportHapAddon = class extends BaseAddon {
109438
109606
  useBridge
109439
109607
  });
109440
109608
  await this.updateEntrySettings(deviceIdStr, nextSettings);
109441
- if (currentPref !== streamPreference || currentHksv !== hksvRecording || currentKeepClips !== keepClips || wasBridged !== willBridge) {
109609
+ if (currentPref !== streamPreference || currentKeepClips !== keepClips || wasBridged !== willBridge) {
109442
109610
  log.info("export-hap: per-camera export settings changed — refreshing accessory", { meta: {
109443
109611
  streamPreference: {
109444
109612
  from: currentPref,
109445
109613
  to: streamPreference
109446
109614
  },
109447
- hksvRecording: {
109448
- from: currentHksv,
109449
- to: hksvRecording
109450
- },
109451
109615
  keepClips: {
109452
109616
  from: currentKeepClips,
109453
109617
  to: keepClips
@@ -109534,6 +109698,7 @@ function errMsg(err) {
109534
109698
  }
109535
109699
  //#endregion
109536
109700
  exports.DEFAULT_CLIP_RETENTION_DAYS = DEFAULT_CLIP_RETENTION_DAYS;
109701
+ exports.DEFAULT_DEVICE_SETTINGS = DEFAULT_DEVICE_SETTINGS;
109537
109702
  exports.EXPORT_HAP_ADDON_ID = EXPORT_HAP_ADDON_ID;
109538
109703
  exports.ExportHapAddon = ExportHapAddon;
109539
109704
  exports.default = ExportHapAddon;
@@ -109543,7 +109708,6 @@ exports.describeHksvCamera = describeHksvCamera;
109543
109708
  exports.initHapStorage = initHapStorage;
109544
109709
  exports.publishStandalone = publishStandalone;
109545
109710
  exports.resolveClipRetentionDays = resolveClipRetentionDays;
109546
- exports.resolveHksvRecording = resolveHksvRecording;
109547
109711
  exports.resolveKeepHomekitClips = resolveKeepHomekitClips;
109548
109712
  exports.resolveMultiTierService = resolveMultiTierService;
109549
109713
  exports.unpublishAccessory = unpublishAccessory;
@@ -90660,6 +90660,153 @@ function createClipThumbnailMinter(input) {
90660
90660
  };
90661
90661
  }
90662
90662
  //#endregion
90663
+ //#region src/hksv/recording-active.ts
90664
+ /**
90665
+ * `true`/`false` as HomeKit holds it, or `null` when this accessory does not
90666
+ * offer recording at all.
90667
+ */
90668
+ function readHomekitRecordingActive(accessories) {
90669
+ for (const accessory of accessories) {
90670
+ const service = accessory.getService(import_dist.Service.CameraRecordingManagement);
90671
+ if (service === void 0) continue;
90672
+ const value = service.getCharacteristic(import_dist.Characteristic.Active).value;
90673
+ if (typeof value === "number") return value === 1;
90674
+ if (typeof value === "boolean") return value;
90675
+ return null;
90676
+ }
90677
+ return null;
90678
+ }
90679
+ /**
90680
+ * One sentence for the settings page: what HomeKit says, or why it was never
90681
+ * asked.
90682
+ *
90683
+ * The cost stays in the text even though the switch is gone. It is still the
90684
+ * operator's to understand — a camera recording here runs a permanent ffmpeg —
90685
+ * and a status line that reported only the state would have quietly dropped
90686
+ * the one number that decides whether to turn it on.
90687
+ */
90688
+ function describeHomekitRecording(active, lastBuild) {
90689
+ if (active === true) return "HomeKit is recording this camera. Turn it off in the Home app (Camera settings → “Stream”), not here — iOS owns this switch and keeps it across reboots. While it is on, this camera runs a permanent prebuffer (~0.7% of one CPU core on the copy branch).";
90690
+ if (active === false) return "This camera offers recording and HomeKit has it off. Turn it on in the Home app (Camera settings → “Stream and Allow Recording”); it needs iCloud+ and a home hub. Nothing runs here until you do.";
90691
+ if (lastBuild !== null && !lastBuild.advertised) return `Recording is not offered for this camera: ${lastBuild.detail ?? "the accessory could not advertise it"}.`;
90692
+ return "Not known yet — this camera’s accessory has not published its recording service in this process. It appears once the accessory is built.";
90693
+ }
90694
+ //#endregion
90695
+ //#region src/hksv/recording-source.ts
90696
+ /** The tallest frame the recording path will hold in its prebuffer. */
90697
+ var MAX_RECORDING_HEIGHT = 1080;
90698
+ /** HKSV takes H.264 only — AAC audio and H.264 video, no negotiation. */
90699
+ var RECORDABLE_CODEC = "h264";
90700
+ /**
90701
+ * Pick the slot the recording child pulls.
90702
+ *
90703
+ * Deliberately NOT `pickPreferredRtspEntry`: that picker resolves the operator's
90704
+ * LIVE preference and, on `auto`, steers by the resolution iOS negotiated for a
90705
+ * live session — neither is a fact about recording, and on 615 it selects `mid`,
90706
+ * a 10 fps slot. Recording has one criterion, applied here and nowhere else:
90707
+ * the largest copyable frame that does not exceed {@link MAX_RECORDING_HEIGHT}.
90708
+ */
90709
+ function pickRecordingSource(entries, options = {}) {
90710
+ const enabled = entries.filter((e) => e.enabled);
90711
+ if (enabled.length === 0) return {
90712
+ ok: false,
90713
+ refusal: "no-enabled-stream"
90714
+ };
90715
+ const h264 = enabled.filter((e) => normaliseCodec(e.codec) === RECORDABLE_CODEC);
90716
+ const pool = h264.length > 0 ? h264 : options.allowTranscode === true ? enabled : [];
90717
+ if (pool.length === 0) return {
90718
+ ok: false,
90719
+ refusal: "no-h264-stream"
90720
+ };
90721
+ const transcode = h264.length === 0;
90722
+ const sized = pool.filter(hasUsableResolution);
90723
+ if (sized.length === 0) return {
90724
+ ok: false,
90725
+ refusal: "no-resolution"
90726
+ };
90727
+ const withinCeiling = sized.filter((e) => height(e) <= MAX_RECORDING_HEIGHT);
90728
+ const within = withinCeiling.length > 0;
90729
+ const best = [...within ? withinCeiling : sized].sort((a, b) => within ? height(b) - height(a) : height(a) - height(b))[0];
90730
+ if (best === void 0 || best.resolution === void 0) return {
90731
+ ok: false,
90732
+ refusal: "no-resolution"
90733
+ };
90734
+ return {
90735
+ ok: true,
90736
+ source: {
90737
+ brokerId: best.brokerId,
90738
+ profile: best.profile ?? best.brokerId,
90739
+ transcode,
90740
+ url: best.url,
90741
+ mutedUrl: best.mutedUrl,
90742
+ width: best.resolution.width,
90743
+ height: best.resolution.height
90744
+ }
90745
+ };
90746
+ }
90747
+ /** A one-line reason for the log — silence about a withdrawn service reads as a bug. */
90748
+ function refusalReason(refusal) {
90749
+ switch (refusal) {
90750
+ case "no-enabled-stream": return "the camera has no enabled RTSP profile";
90751
+ case "no-h264-stream": return "every enabled profile is H.265 — HKSV takes H.264 only, and a permanent transcode costs 128x a copy";
90752
+ case "no-resolution": return "no enabled profile declares a resolution, so nothing honest could be advertised";
90753
+ }
90754
+ }
90755
+ function height(entry) {
90756
+ return entry.resolution?.height ?? 0;
90757
+ }
90758
+ function hasUsableResolution(entry) {
90759
+ const r = entry.resolution;
90760
+ return r !== void 0 && r.width > 0 && r.height > 0;
90761
+ }
90762
+ /** Publishers spell H.265 four ways; the same normalisation the broker uses. */
90763
+ function normaliseCodec(codec) {
90764
+ return (codec ?? "").toLowerCase().replace(/[.\s-]/g, "");
90765
+ }
90766
+ //#endregion
90767
+ //#region src/hksv/recordability-token.ts
90768
+ /** No slot is recordable. A real answer, and a fingerprint-moving one. */
90769
+ var RECORDABILITY_NONE = "rec:none";
90770
+ /**
90771
+ * The slot HKSV would record from, as a fingerprint token.
90772
+ *
90773
+ * `null` when `entries` is null — the profiles could not be read, which is not
90774
+ * a claim about the camera.
90775
+ */
90776
+ function recordabilityToken(entries, options = {}) {
90777
+ if (entries === null) return null;
90778
+ const choice = pickRecordingSource(entries, options);
90779
+ return choice.ok ? `rec:${choice.source.profile}${choice.source.transcode ? "+x" : ""}` : RECORDABILITY_NONE;
90780
+ }
90781
+ var TOKEN_SUFFIX = /\|(rec:[^|]*)$/;
90782
+ /**
90783
+ * Attach the recording token to a device-shape fingerprint.
90784
+ *
90785
+ * On an unreadable read the persisted token is carried forward — but only onto
90786
+ * the base it was persisted against. If the device's SHAPE also changed, that
90787
+ * change is a rebuild in its own right and the token has nothing to say about
90788
+ * it; inventing one there would advertise a recording slot nobody read.
90789
+ */
90790
+ function withRecordabilityToken(input) {
90791
+ if (input.token !== null) return `${input.base}|${input.token}`;
90792
+ const carried = input.persisted?.match(TOKEN_SUFFIX);
90793
+ if (carried === null || carried === void 0) return input.base;
90794
+ return carried.input?.slice(0, carried.index) === input.base ? `${input.base}|${carried[1]}` : input.base;
90795
+ }
90796
+ /**
90797
+ * What this camera could record, as one fingerprint token.
90798
+ *
90799
+ * There used to be an `rec:off` here, for a per-camera switch of ours. That
90800
+ * switch is gone (D589): whether HomeKit RECORDS is HomeKit's own persisted
90801
+ * characteristic, it does not change the accessory's shape, and a rebuild is
90802
+ * about shape. What still belongs here is the CAPABILITY — which slot, and
90803
+ * whether serving it means running an encoder — because those are two
90804
+ * different accessories to build.
90805
+ */
90806
+ function recordabilityTokenFor(input) {
90807
+ return recordabilityToken(input.entries, { allowTranscode: input.allowTranscode !== false });
90808
+ }
90809
+ //#endregion
90663
90810
  //#region src/hksv/videoclips-plane.ts
90664
90811
  /**
90665
90812
  * The two data-plane routes behind the HomeKit clip source.
@@ -90922,8 +91069,9 @@ function createHksvClipPlanes(deps) {
90922
91069
  *
90923
91070
  * ## The source EXISTS because of HomeKit, and is not a switch
90924
91071
  *
90925
- * A camera has this source because it is exported to HomeKit AND HomeKit
90926
- * recording is on for it; turn either off and the row is gone (D550, D569 § 5).
91072
+ * A camera has this source because it is exported to HomeKit and HomeKit has
91073
+ * not said no to recording it; un-export it, or switch recording off in the
91074
+ * Home app, and the row is gone (D550, D569 § 5, D589).
90927
91075
  * There is deliberately no enable control of its own — a knob over somebody
90928
91076
  * else's decision is D62's defect. `keepClips` is NOT that gate either: it
90929
91077
  * governs the TEE, and the clips already kept remain the truth about this
@@ -90970,7 +91118,7 @@ function createHksvVideoclipsProvider(deps) {
90970
91118
  */
90971
91119
  async function measure(deviceId) {
90972
91120
  const facts = deps.describeCamera(deviceId);
90973
- if (!facts.exported || !facts.recording) return null;
91121
+ if (!facts.exported || facts.homekitRecording === false) return null;
90974
91122
  const catalog = await deps.readCatalog(deviceId);
90975
91123
  return {
90976
91124
  availability: availabilityFor(facts, catalog),
@@ -106325,6 +106473,72 @@ function buildRecordingOptions(input) {
106325
106473
  };
106326
106474
  }
106327
106475
  //#endregion
106476
+ //#region src/hksv/recording-invocation.ts
106477
+ /**
106478
+ * The resolution the accessory may advertise, which is the one it can deliver.
106479
+ *
106480
+ * A copy is advertised at its source size whatever that is — there is no
106481
+ * encoder to resize with, and advertising a size we cannot produce is the
106482
+ * "stream that does not match its own advertisement" defect in miniature.
106483
+ * A transcode above the ceiling is fitted down, because with an encoder in the
106484
+ * pipeline the 6.35 MB 4K fragment is a choice rather than a fact, and iOS
106485
+ * renders HKSV clips at 1080p regardless.
106486
+ */
106487
+ function recordingOutput(source) {
106488
+ if (!source.transcode || source.height <= 1080) return {
106489
+ width: source.width,
106490
+ height: source.height,
106491
+ scaled: false
106492
+ };
106493
+ return {
106494
+ width: Math.max(2, Math.round(source.width * MAX_RECORDING_HEIGHT / source.height / 2) * 2),
106495
+ height: MAX_RECORDING_HEIGHT,
106496
+ scaled: true
106497
+ };
106498
+ }
106499
+ /** The invocation, minus the sink and sidecar the child owns. */
106500
+ function buildRecordingInvocation(input) {
106501
+ const { source } = input;
106502
+ const audio = input.audioActive ? {
106503
+ kind: "encode",
106504
+ codec: "aac",
106505
+ bitrateKbps: 32,
106506
+ sampleRateHz: HKSV_AUDIO_SAMPLE_RATE_HZ,
106507
+ channels: 1
106508
+ } : { kind: "none" };
106509
+ const out = recordingOutput(source);
106510
+ const video = source.transcode ? {
106511
+ kind: "encode",
106512
+ encoder: "libx264",
106513
+ preset: "veryfast",
106514
+ profile: "main",
106515
+ level: "4.0",
106516
+ pixelFormat: "yuv420p",
106517
+ forceKeyFramesSeconds: Math.max(1, Math.round(input.fragmentMs / 1e3)),
106518
+ bf: 0,
106519
+ ...out.scaled ? { scale: {
106520
+ mode: "fit",
106521
+ width: out.width,
106522
+ height: out.height
106523
+ } } : {}
106524
+ } : { kind: "copy" };
106525
+ return {
106526
+ logLevel: "error",
106527
+ decodeHwAccel: source.transcode ? input.decodeHwAccel : null,
106528
+ input: {
106529
+ url: input.audioActive ? source.url : source.mutedUrl,
106530
+ rtspTransport: "tcp",
106531
+ ...source.transcode && input.decodeInputArgs !== void 0 && input.decodeInputArgs.length > 0 ? { extraArgs: input.decodeInputArgs } : {},
106532
+ analyzeDurationUs: 1e6,
106533
+ probeSizeBytes: 1e6
106534
+ },
106535
+ video,
106536
+ audio,
106537
+ threadCount: 0,
106538
+ outputArgs: []
106539
+ };
106540
+ }
106541
+ //#endregion
106328
106542
  //#region src/hksv/fragment-source.ts
106329
106543
  /**
106330
106544
  * How far back the prebuffer ring reaches.
@@ -106492,6 +106706,7 @@ var HksvFragmentSource = class {
106492
106706
  meta: {
106493
106707
  brokerId: this.input.source.brokerId,
106494
106708
  resolution: `${this.input.source.width}x${this.input.source.height}`,
106709
+ branch: this.input.source.transcode ? "transcode" : "copy",
106495
106710
  fragmentMs: this.input.fragmentMs,
106496
106711
  audioActive: this.audioActive,
106497
106712
  windowMs: PREBUFFER_WINDOW_MS
@@ -106556,27 +106771,13 @@ var HksvFragmentSource = class {
106556
106771
  * takes AAC only — which the same measurement priced at 0.4 % of a core.
106557
106772
  */
106558
106773
  buildInvocation() {
106559
- const audio = this.audioActive ? {
106560
- kind: "encode",
106561
- codec: "aac",
106562
- bitrateKbps: 32,
106563
- sampleRateHz: HKSV_AUDIO_SAMPLE_RATE_HZ,
106564
- channels: 1
106565
- } : { kind: "none" };
106566
- return {
106567
- logLevel: "error",
106568
- decodeHwAccel: null,
106569
- input: {
106570
- url: this.audioActive ? this.input.source.url : this.input.source.mutedUrl,
106571
- rtspTransport: "tcp",
106572
- analyzeDurationUs: 1e6,
106573
- probeSizeBytes: 1e6
106574
- },
106575
- video: { kind: "copy" },
106576
- audio,
106577
- threadCount: 0,
106578
- outputArgs: []
106579
- };
106774
+ return buildRecordingInvocation({
106775
+ source: this.input.source,
106776
+ audioActive: this.audioActive,
106777
+ fragmentMs: this.input.fragmentMs,
106778
+ decodeHwAccel: this.input.decodeHwAccel ?? null,
106779
+ ...this.input.decodeInputArgs === void 0 ? {} : { decodeInputArgs: this.input.decodeInputArgs }
106780
+ });
106580
106781
  }
106581
106782
  };
106582
106783
  //#endregion
@@ -106936,74 +107137,6 @@ var HksvRecordingDelegate = class {
106936
107137
  }
106937
107138
  };
106938
107139
  //#endregion
106939
- //#region src/hksv/recording-source.ts
106940
- /** The tallest frame the recording path will hold in its prebuffer. */
106941
- var MAX_RECORDING_HEIGHT = 1080;
106942
- /** HKSV takes H.264 only — AAC audio and H.264 video, no negotiation. */
106943
- var RECORDABLE_CODEC = "h264";
106944
- /**
106945
- * Pick the slot the recording child pulls.
106946
- *
106947
- * Deliberately NOT `pickPreferredRtspEntry`: that picker resolves the operator's
106948
- * LIVE preference and, on `auto`, steers by the resolution iOS negotiated for a
106949
- * live session — neither is a fact about recording, and on 615 it selects `mid`,
106950
- * a 10 fps slot. Recording has one criterion, applied here and nowhere else:
106951
- * the largest copyable frame that does not exceed {@link MAX_RECORDING_HEIGHT}.
106952
- */
106953
- function pickRecordingSource(entries) {
106954
- const enabled = entries.filter((e) => e.enabled);
106955
- if (enabled.length === 0) return {
106956
- ok: false,
106957
- refusal: "no-enabled-stream"
106958
- };
106959
- const h264 = enabled.filter((e) => normaliseCodec(e.codec) === RECORDABLE_CODEC);
106960
- if (h264.length === 0) return {
106961
- ok: false,
106962
- refusal: "no-h264-stream"
106963
- };
106964
- const sized = h264.filter(hasUsableResolution);
106965
- if (sized.length === 0) return {
106966
- ok: false,
106967
- refusal: "no-resolution"
106968
- };
106969
- const withinCeiling = sized.filter((e) => height(e) <= MAX_RECORDING_HEIGHT);
106970
- const best = [...withinCeiling.length > 0 ? withinCeiling : sized].sort((a, b) => withinCeiling.length > 0 ? height(b) - height(a) : height(a) - height(b))[0];
106971
- if (best === void 0 || best.resolution === void 0) return {
106972
- ok: false,
106973
- refusal: "no-resolution"
106974
- };
106975
- return {
106976
- ok: true,
106977
- source: {
106978
- brokerId: best.brokerId,
106979
- profile: best.profile ?? best.brokerId,
106980
- url: best.url,
106981
- mutedUrl: best.mutedUrl,
106982
- width: best.resolution.width,
106983
- height: best.resolution.height
106984
- }
106985
- };
106986
- }
106987
- /** A one-line reason for the log — silence about a withdrawn service reads as a bug. */
106988
- function refusalReason(refusal) {
106989
- switch (refusal) {
106990
- case "no-enabled-stream": return "the camera has no enabled RTSP profile";
106991
- case "no-h264-stream": return "every enabled profile is H.265 — HKSV takes H.264 only, and a permanent transcode costs 128x a copy";
106992
- case "no-resolution": return "no enabled profile declares a resolution, so nothing honest could be advertised";
106993
- }
106994
- }
106995
- function height(entry) {
106996
- return entry.resolution?.height ?? 0;
106997
- }
106998
- function hasUsableResolution(entry) {
106999
- const r = entry.resolution;
107000
- return r !== void 0 && r.width > 0 && r.height > 0;
107001
- }
107002
- /** Publishers spell H.265 four ways; the same normalisation the broker uses. */
107003
- function normaliseCodec(codec) {
107004
- return (codec ?? "").toLowerCase().replace(/[.\s-]/g, "");
107005
- }
107006
- //#endregion
107007
107140
  //#region src/hksv/build-recording.ts
107008
107141
  /**
107009
107142
  * Assemble HomeKit Secure Video for one camera — the ADVERTISEMENT and the
@@ -107055,7 +107188,7 @@ async function buildHksvRecording(input) {
107055
107188
  note("profiles-unreadable", "cameraStreams.getProfileRtspEntries could not be read");
107056
107189
  return null;
107057
107190
  }
107058
- const choice = pickRecordingSource(entries);
107191
+ const choice = pickRecordingSource(entries, { allowTranscode: bctx.options.hapDeviceSettings?.allowTranscode !== false });
107059
107192
  if (!choice.ok) {
107060
107193
  log.info("export-hap: HKSV withheld — no recordable stream", { meta: {
107061
107194
  refusal: choice.refusal,
@@ -107065,9 +107198,10 @@ async function buildHksvRecording(input) {
107065
107198
  return null;
107066
107199
  }
107067
107200
  const source = choice.source;
107201
+ const decodeHwAccel = source.transcode ? await resolveDecodeHwAccel(bctx, log) : null;
107068
107202
  const gopMs = await readSourceGopMs(bctx, source.width, source.height);
107069
- const fragmentLengthMs = deriveFragmentLengthMs(gopMs);
107070
- if (fragmentLengthMs === null) {
107203
+ const fragmentLengthMs = source.transcode ? HKSV_FRAGMENT_LENGTHS_MS[0] : deriveFragmentLengthMs(gopMs);
107204
+ if (fragmentLengthMs === void 0 || fragmentLengthMs === null) {
107071
107205
  log.warn("export-hap: HKSV withheld — the camera key-frame interval is longer than any fragment length we advertise", { meta: {
107072
107206
  gopMs,
107073
107207
  brokerId: source.brokerId
@@ -107078,9 +107212,10 @@ async function buildHksvRecording(input) {
107078
107212
  const parsedProfile = CamProfileSchema.safeParse(source.profile);
107079
107213
  const recordedProfile = parsedProfile.success ? parsedProfile.data : null;
107080
107214
  const fps = resolveFps(input.fpsByProfile, source.profile);
107215
+ const out = recordingOutput(source);
107081
107216
  const options = buildRecordingOptions({
107082
- width: source.width,
107083
- height: source.height,
107217
+ width: out.width,
107218
+ height: out.height,
107084
107219
  fps,
107085
107220
  fragmentLengthMs
107086
107221
  });
@@ -107090,8 +107225,8 @@ async function buildHksvRecording(input) {
107090
107225
  logger: log,
107091
107226
  deviceId: numericDeviceId,
107092
107227
  store: clipStore,
107093
- width: source.width,
107094
- height: source.height,
107228
+ width: out.width,
107229
+ height: out.height,
107095
107230
  fragmentMs: fragmentLengthMs,
107096
107231
  ...recordedProfile === null ? {} : { profile: recordedProfile }
107097
107232
  });
@@ -107110,14 +107245,21 @@ async function buildHksvRecording(input) {
107110
107245
  spawnFn: spawn,
107111
107246
  source,
107112
107247
  fragmentMs,
107113
- audioActive
107248
+ audioActive,
107249
+ decodeHwAccel: decodeHwAccel?.hwaccel ?? null,
107250
+ ...decodeHwAccel === null || decodeHwAccel.extraInputArgs.length === 0 ? {} : { decodeInputArgs: decodeHwAccel.extraInputArgs }
107114
107251
  })
107115
107252
  });
107116
107253
  const advertisedResolution = options.video.resolutions[0];
107117
107254
  log.info("export-hap: HKSV ADVERTISED — recording is offered for this camera", { meta: {
107118
107255
  brokerId: source.brokerId,
107119
107256
  profile: source.profile,
107120
- resolution: `${source.width}x${source.height}`,
107257
+ resolution: `${out.width}x${out.height}`,
107258
+ branch: source.transcode ? "transcode" : "copy",
107259
+ ...source.transcode ? {
107260
+ sourceResolution: `${source.width}x${source.height}`,
107261
+ decodeHwAccel: decodeHwAccel?.hwaccel ?? "software"
107262
+ } : {},
107121
107263
  measuredFps: fps,
107122
107264
  advertisedFps: advertisedResolution?.[2] ?? null,
107123
107265
  fragmentLengthMs,
@@ -107132,6 +107274,27 @@ async function buildHksvRecording(input) {
107132
107274
  dispose: () => delegate.dispose()
107133
107275
  };
107134
107276
  }
107277
+ /**
107278
+ * The `-hwaccel` value for a transcoding prebuffer, and the flags that travel
107279
+ * with it.
107280
+ *
107281
+ * Software is a legitimate answer and is always NAMED: a camera that quietly
107282
+ * stopped using the GPU and one that never had it look identical in a CPU
107283
+ * graph, and on this branch the difference is most of a core, permanently.
107284
+ */
107285
+ async function resolveDecodeHwAccel(bctx, log) {
107286
+ const decision = selectHwDecode({
107287
+ transcode: true,
107288
+ reading: await probeDecoderHwaccel({
107289
+ source: decoderInfoSourceFromContext(bctx.ctx),
107290
+ log,
107291
+ memo: bctx.options.decodeMemos.reading
107292
+ }),
107293
+ platform: process.platform
107294
+ });
107295
+ if (decision.kind === "software") log.warn("export-hap: HKSV will transcode in SOFTWARE — this costs most of a core, permanently", { meta: { reason: decision.reason } });
107296
+ return decision;
107297
+ }
107135
107298
  /** `cameraStreams.getProfileRtspEntries`, or `null` when the cap is unreachable. */
107136
107299
  async function readProfileEntries(bctx) {
107137
107300
  try {
@@ -108127,13 +108290,12 @@ async function buildCameraAccessory(input) {
108127
108290
  bctx,
108128
108291
  streamingOptions: streams.streamingOptions
108129
108292
  }));
108130
- const recordingEnabled = options.hapDeviceSettings.hksvRecording === true;
108131
108293
  let recordingAudioActive = true;
108132
- const recording = recordingEnabled ? await buildHksvRecording({
108294
+ const recording = await buildHksvRecording({
108133
108295
  bctx,
108134
108296
  fpsByProfile: advertisedVideo.fpsByProfile,
108135
108297
  isAudioActive: () => recordingAudioActive
108136
- }) : null;
108298
+ });
108137
108299
  const controller = new (isDoorbell ? import_dist.DoorbellController : import_dist.CameraController)({
108138
108300
  delegate: streams.delegate,
108139
108301
  streamingOptions: streams.streamingOptions,
@@ -108376,23 +108538,13 @@ function syncStateToJson(map) {
108376
108538
  * capabilities bound to it — restricting coverage by type is precisely what
108377
108539
  * confined this exporter to cameras for its first three rounds.
108378
108540
  */
108379
- var DEFAULT_DEVICE_SETTINGS = {
108380
- streamPreference: "auto",
108381
- hksvRecording: true
108382
- };
108383
108541
  /**
108384
- * ON unless explicitly switched off — operator decision 2026-08-08 (flipped
108385
- * from the launch default of off). ABSENT must resolve to ON or the flip is a
108386
- * lie for every entry persisted before the field existed, so every read goes
108387
- * through this one resolver (`!== false`), never a scattered `=== true`. The
108388
- * cost that made off-by-default look prudent is measured and small on the only
108389
- * branch the recorder accepts (copy: 0.7 % of a core / ~30 MB RSS, D84), and a
108390
- * camera the recorder cannot copy refuses recording with a logged reason
108391
- * rather than paying for a transcode.
108542
+ * What an entry with no stored settings is read as — and what a camera
108543
+ * exported TODAY is built from. Exported so a spec can assert the one thing
108544
+ * that matters about it: it must resolve to NOT recording, or "off by default"
108545
+ * is true only for cameras exposed before the flip.
108392
108546
  */
108393
- function resolveHksvRecording(settings) {
108394
- return settings?.hksvRecording !== false;
108395
- }
108547
+ var DEFAULT_DEVICE_SETTINGS = { streamPreference: "auto" };
108396
108548
  /**
108397
108549
  * Whether this camera offers Apple's NEW camera service alongside the classic
108398
108550
  * one — `camera-multi-tier-rtp-stream-management` (0x8031), where HEVC and 4K
@@ -108435,11 +108587,11 @@ var EXPORT_HAP_ADDON_ID = "export-hap";
108435
108587
  * stays and says the copies are off, which is D62's other half: an off switch
108436
108588
  * is REPORTED off, never made to look like a broken camera.
108437
108589
  */
108438
- function describeHksvCamera(entry, lastBuild) {
108590
+ function describeHksvCamera(entry, lastBuild, homekitRecording) {
108439
108591
  const isCamera = entry !== null && entry.mapperKind === "camera";
108440
108592
  return {
108441
108593
  exported: isCamera,
108442
- recording: isCamera && resolveHksvRecording(entry.settings),
108594
+ homekitRecording: isCamera ? homekitRecording : null,
108443
108595
  keepingClips: isCamera && resolveKeepHomekitClips(entry.settings),
108444
108596
  lastBuild
108445
108597
  };
@@ -108683,7 +108835,7 @@ var ExportHapAddon = class extends BaseAddon {
108683
108835
  const clipsProvider = createHksvVideoclipsProvider({
108684
108836
  logger: this.ctx.logger.child("hksv-clips"),
108685
108837
  addonId: EXPORT_HAP_ADDON_ID,
108686
- describeCamera: (deviceId) => describeHksvCamera(this.findEntry(deviceId), this.hksvBuilds.lastFor(deviceId)),
108838
+ describeCamera: (deviceId) => describeHksvCamera(this.findEntry(deviceId), this.hksvBuilds.lastFor(deviceId), readHomekitRecordingActive(this.exposed.get(String(deviceId))?.accessories ?? [])),
108687
108839
  readCatalog: async (deviceId) => await this.hksvClipStore?.listCatalog(deviceId) ?? { kind: "no-root" },
108688
108840
  locationRefusal: () => this.hksvClipLocation?.lastRefusal ?? null,
108689
108841
  mediaUrl: (deviceId, stem) => this.hksvClipUrl("media", deviceId, stem),
@@ -108839,7 +108991,6 @@ var ExportHapAddon = class extends BaseAddon {
108839
108991
  },
108840
108992
  hapDeviceSettings: {
108841
108993
  streamPreference: entrySettings.streamPreference ?? "auto",
108842
- hksvRecording: resolveHksvRecording(entrySettings),
108843
108994
  keepHomekitClips: resolveKeepHomekitClips(entrySettings),
108844
108995
  multiTierService: resolveMultiTierService(entrySettings),
108845
108996
  allowTranscode: entrySettings.allowTranscode !== false
@@ -108920,15 +109071,40 @@ var ExportHapAddon = class extends BaseAddon {
108920
109071
  features: device.features
108921
109072
  });
108922
109073
  const persisted = syncStateFromJson(this.config.exportSyncState).get(deviceId)?.fingerprint;
109074
+ const withToken = withRecordabilityToken({
109075
+ base: fresh,
109076
+ token: await this.readRecordabilityToken(deviceId, device.type),
109077
+ persisted
109078
+ });
108923
109079
  return resolveExportFingerprint({
108924
109080
  ready: device.probed ?? false,
108925
- fresh,
109081
+ fresh: withToken,
108926
109082
  persisted
108927
109083
  });
108928
109084
  } catch {
108929
109085
  return null;
108930
109086
  }
108931
109087
  }
109088
+ /**
109089
+ * The slot HKSV would record this camera from, for the fingerprint.
109090
+ *
109091
+ * `null` — do not move the fingerprint — for anything that is not a camera
109092
+ * (a switch has no recording decision, and stamping a constant on it would
109093
+ * republish every non-camera accessory once for nothing) and for a camera
109094
+ * whose profiles could not be read right now.
109095
+ */
109096
+ async readRecordabilityToken(deviceId, type) {
109097
+ if (pickMapperKind(type) !== "camera") return null;
109098
+ try {
109099
+ const entries = await (await this.ctx.fetchDevice(deviceId)).cameraStreams?.getProfileRtspEntries({});
109100
+ return recordabilityTokenFor({
109101
+ allowTranscode: this.findEntry(deviceId)?.settings?.allowTranscode !== false,
109102
+ entries: entries ?? null
109103
+ });
109104
+ } catch {
109105
+ return null;
109106
+ }
109107
+ }
108932
109108
  /** Persist a device's current export fingerprint into `exportSyncState`
108933
109109
  * (best-effort; called after a successful attach). */
108934
109110
  async persistFingerprint(deviceId, fingerprint) {
@@ -109194,7 +109370,6 @@ var ExportHapAddon = class extends BaseAddon {
109194
109370
  const enabled = entry !== null;
109195
109371
  const enabledKey = `hap:${deviceId}:enabled`;
109196
109372
  const streamPreferenceKey = `hap:${deviceId}:streamPreference`;
109197
- const hksvKey = `hap:${deviceId}:hksvRecording`;
109198
109373
  const multiTierKey = `hap:${deviceId}:multiTierService`;
109199
109374
  const useBridgeKey = `hap:${deviceId}:useBridge`;
109200
109375
  const allowTranscodeKey = `hap:${deviceId}:allowTranscode`;
@@ -109245,17 +109420,14 @@ var ExportHapAddon = class extends BaseAddon {
109245
109420
  immediate: true
109246
109421
  },
109247
109422
  {
109248
- type: "boolean",
109249
- key: hksvKey,
109423
+ type: "info",
109424
+ key: `hap:${deviceId}:hksvRecordingStatus`,
109250
109425
  label: "HomeKit recording (Secure Video)",
109251
- description: "Offer “Stream and Allow Recording” in iOS Home. Requires iCloud+ and a home hub. Keeps a continuous 8s prebuffer for this camera (~0.7% of one CPU core, H.264 sources only).",
109252
- style: "switch",
109253
- value: resolveHksvRecording(settings),
109426
+ content: describeHomekitRecording(readHomekitRecordingActive(this.exposed.get(String(deviceId))?.accessories ?? []), this.hksvBuilds.lastFor(deviceId)),
109254
109427
  showWhen: {
109255
109428
  field: enabledKey,
109256
109429
  equals: true
109257
- },
109258
- immediate: true
109430
+ }
109259
109431
  },
109260
109432
  {
109261
109433
  type: "boolean",
@@ -109278,7 +109450,7 @@ var ExportHapAddon = class extends BaseAddon {
109278
109450
  style: "switch",
109279
109451
  value: resolveKeepHomekitClips(settings),
109280
109452
  showWhen: {
109281
- field: hksvKey,
109453
+ field: enabledKey,
109282
109454
  equals: true
109283
109455
  },
109284
109456
  immediate: true
@@ -109378,7 +109550,6 @@ var ExportHapAddon = class extends BaseAddon {
109378
109550
  const wasEnabled = this.exposed.has(deviceIdStr);
109379
109551
  const enabledKey = `hap:${deviceId}:enabled`;
109380
109552
  const streamPreferenceKey = `hap:${deviceId}:streamPreference`;
109381
- const hksvKey = `hap:${deviceId}:hksvRecording`;
109382
109553
  const useBridgeKey = `hap:${deviceId}:useBridge`;
109383
109554
  const allowTranscodeKey = `hap:${deviceId}:allowTranscode`;
109384
109555
  const multiTierKey = `hap:${deviceId}:multiTierService`;
@@ -109387,7 +109558,6 @@ var ExportHapAddon = class extends BaseAddon {
109387
109558
  const enabledValue = enabledKey in patch ? Boolean(patch[enabledKey]) : wasEnabled;
109388
109559
  const streamPreferenceRaw = streamPreferenceKey in patch ? patch[streamPreferenceKey] : current?.settings?.streamPreference;
109389
109560
  const streamPreference = typeof streamPreferenceRaw === "string" && streamPreferenceRaw.trim().length > 0 ? streamPreferenceRaw : "auto";
109390
- const hksvRecording = hksvKey in patch ? Boolean(patch[hksvKey]) : resolveHksvRecording(current?.settings);
109391
109561
  const useBridge = useBridgeKey in patch ? Boolean(patch[useBridgeKey]) : current?.settings?.useBridge !== false;
109392
109562
  const allowTranscode = allowTranscodeKey in patch ? Boolean(patch[allowTranscodeKey]) : current?.settings?.allowTranscode !== false;
109393
109563
  const multiTierService = multiTierKey in patch ? Boolean(patch[multiTierKey]) : resolveMultiTierService(current?.settings);
@@ -109399,7 +109569,6 @@ var ExportHapAddon = class extends BaseAddon {
109399
109569
  const nextSettings = {
109400
109570
  ...current?.settings ?? DEFAULT_DEVICE_SETTINGS,
109401
109571
  streamPreference,
109402
- hksvRecording,
109403
109572
  useBridge,
109404
109573
  allowTranscode,
109405
109574
  multiTierService,
@@ -109416,7 +109585,6 @@ var ExportHapAddon = class extends BaseAddon {
109416
109585
  return { success: true };
109417
109586
  }
109418
109587
  const currentPref = current?.settings?.streamPreference ?? "auto";
109419
- const currentHksv = resolveHksvRecording(current?.settings);
109420
109588
  const currentKeepClips = resolveKeepHomekitClips(current?.settings);
109421
109589
  const mapperKind = current?.mapperKind ?? "generic";
109422
109590
  const wasBridged = shouldBridge({
@@ -109428,16 +109596,12 @@ var ExportHapAddon = class extends BaseAddon {
109428
109596
  useBridge
109429
109597
  });
109430
109598
  await this.updateEntrySettings(deviceIdStr, nextSettings);
109431
- if (currentPref !== streamPreference || currentHksv !== hksvRecording || currentKeepClips !== keepClips || wasBridged !== willBridge) {
109599
+ if (currentPref !== streamPreference || currentKeepClips !== keepClips || wasBridged !== willBridge) {
109432
109600
  log.info("export-hap: per-camera export settings changed — refreshing accessory", { meta: {
109433
109601
  streamPreference: {
109434
109602
  from: currentPref,
109435
109603
  to: streamPreference
109436
109604
  },
109437
- hksvRecording: {
109438
- from: currentHksv,
109439
- to: hksvRecording
109440
- },
109441
109605
  keepClips: {
109442
109606
  from: currentKeepClips,
109443
109607
  to: keepClips
@@ -109523,4 +109687,4 @@ function errMsg(err) {
109523
109687
  return err instanceof Error ? err.message : String(err);
109524
109688
  }
109525
109689
  //#endregion
109526
- export { DEFAULT_CLIP_RETENTION_DAYS, EXPORT_HAP_ADDON_ID, ExportHapAddon, ExportHapAddon as default, describeHksvCamera, unpublishAccessory as i, initHapStorage as n, publishStandalone as r, resolveClipRetentionDays, resolveHksvRecording, resolveKeepHomekitClips, resolveMultiTierService, deriveUsername as t };
109690
+ export { DEFAULT_CLIP_RETENTION_DAYS, DEFAULT_DEVICE_SETTINGS, EXPORT_HAP_ADDON_ID, ExportHapAddon, ExportHapAddon as default, describeHksvCamera, unpublishAccessory as i, initHapStorage as n, publishStandalone as r, resolveClipRetentionDays, resolveKeepHomekitClips, resolveMultiTierService, deriveUsername as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-export-hap",
3
- "version": "1.2.132",
3
+ "version": "1.2.139",
4
4
  "description": "HomeKit (HAP) exporter for CamStack devices. Publishes each exposed device as its own HomeKit accessory: cameras and doorbells with SRTP streaming, HomeKit Secure Video, motion, two-way audio, PTZ and battery; switches, lights, locks and sensors through a capability→service table.",
5
5
  "keywords": [
6
6
  "camstack",