@camstack/addon-export-hap 1.2.134 → 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,
@@ -108392,38 +108554,7 @@ function syncStateToJson(map) {
108392
108554
  * that matters about it: it must resolve to NOT recording, or "off by default"
108393
108555
  * is true only for cameras exposed before the flip.
108394
108556
  */
108395
- var DEFAULT_DEVICE_SETTINGS = {
108396
- streamPreference: "auto",
108397
- hksvRecording: false
108398
- };
108399
- /**
108400
- * OFF unless explicitly switched on — operator decision 2026-09-21, reversing
108401
- * the 2026-08-08 flip:
108402
- *
108403
- * > *"il recording deve essere off di default, non deve essere attivo quando
108404
- * > si esporta una camera su HKSV"*
108405
- *
108406
- * What changed is not the cost the earlier record argued about — recording on
108407
- * the copy branch really is 0.7 % of a core and ~30 MB (D84), and cheap it
108408
- * remains. What changed is that on-by-default made the switch **say something
108409
- * the system was not doing**. Measured on 2026-09-21: 592, 587, 618 and 640
108410
- * all reported HomeKit recording ON while the advertisement was WITHHELD
108411
- * (`no-enabled-stream` — no enabled RTSP profile), so HomeKit never recorded
108412
- * them and the tee had nothing to keep. The operator was being told four
108413
- * cameras were recording because nobody had turned them off.
108414
- *
108415
- * Exposing a camera to HomeKit is a decision about EXPOSURE. Recording is a
108416
- * second decision and now has to be made.
108417
- *
108418
- * ABSENT resolves OFF — the load-bearing case, for the same reason it was
108419
- * before and with the opposite answer: a camera whose settings predate the
108420
- * field, or that was exposed before today, was never asked about. Every read
108421
- * goes through this one resolver (`=== true`), never a scattered `!== false`,
108422
- * because those two disagree about exactly that case.
108423
- */
108424
- function resolveHksvRecording(settings) {
108425
- return settings?.hksvRecording === true;
108426
- }
108557
+ var DEFAULT_DEVICE_SETTINGS = { streamPreference: "auto" };
108427
108558
  /**
108428
108559
  * Whether this camera offers Apple's NEW camera service alongside the classic
108429
108560
  * one — `camera-multi-tier-rtp-stream-management` (0x8031), where HEVC and 4K
@@ -108466,11 +108597,11 @@ var EXPORT_HAP_ADDON_ID = "export-hap";
108466
108597
  * stays and says the copies are off, which is D62's other half: an off switch
108467
108598
  * is REPORTED off, never made to look like a broken camera.
108468
108599
  */
108469
- function describeHksvCamera(entry, lastBuild) {
108600
+ function describeHksvCamera(entry, lastBuild, homekitRecording) {
108470
108601
  const isCamera = entry !== null && entry.mapperKind === "camera";
108471
108602
  return {
108472
108603
  exported: isCamera,
108473
- recording: isCamera && resolveHksvRecording(entry.settings),
108604
+ homekitRecording: isCamera ? homekitRecording : null,
108474
108605
  keepingClips: isCamera && resolveKeepHomekitClips(entry.settings),
108475
108606
  lastBuild
108476
108607
  };
@@ -108714,7 +108845,7 @@ var ExportHapAddon = class extends BaseAddon {
108714
108845
  const clipsProvider = createHksvVideoclipsProvider({
108715
108846
  logger: this.ctx.logger.child("hksv-clips"),
108716
108847
  addonId: EXPORT_HAP_ADDON_ID,
108717
- 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 ?? [])),
108718
108849
  readCatalog: async (deviceId) => await this.hksvClipStore?.listCatalog(deviceId) ?? { kind: "no-root" },
108719
108850
  locationRefusal: () => this.hksvClipLocation?.lastRefusal ?? null,
108720
108851
  mediaUrl: (deviceId, stem) => this.hksvClipUrl("media", deviceId, stem),
@@ -108870,7 +109001,6 @@ var ExportHapAddon = class extends BaseAddon {
108870
109001
  },
108871
109002
  hapDeviceSettings: {
108872
109003
  streamPreference: entrySettings.streamPreference ?? "auto",
108873
- hksvRecording: resolveHksvRecording(entrySettings),
108874
109004
  keepHomekitClips: resolveKeepHomekitClips(entrySettings),
108875
109005
  multiTierService: resolveMultiTierService(entrySettings),
108876
109006
  allowTranscode: entrySettings.allowTranscode !== false
@@ -108951,15 +109081,40 @@ var ExportHapAddon = class extends BaseAddon {
108951
109081
  features: device.features
108952
109082
  });
108953
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
+ });
108954
109089
  return resolveExportFingerprint({
108955
109090
  ready: device.probed ?? false,
108956
- fresh,
109091
+ fresh: withToken,
108957
109092
  persisted
108958
109093
  });
108959
109094
  } catch {
108960
109095
  return null;
108961
109096
  }
108962
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
+ }
108963
109118
  /** Persist a device's current export fingerprint into `exportSyncState`
108964
109119
  * (best-effort; called after a successful attach). */
108965
109120
  async persistFingerprint(deviceId, fingerprint) {
@@ -109225,7 +109380,6 @@ var ExportHapAddon = class extends BaseAddon {
109225
109380
  const enabled = entry !== null;
109226
109381
  const enabledKey = `hap:${deviceId}:enabled`;
109227
109382
  const streamPreferenceKey = `hap:${deviceId}:streamPreference`;
109228
- const hksvKey = `hap:${deviceId}:hksvRecording`;
109229
109383
  const multiTierKey = `hap:${deviceId}:multiTierService`;
109230
109384
  const useBridgeKey = `hap:${deviceId}:useBridge`;
109231
109385
  const allowTranscodeKey = `hap:${deviceId}:allowTranscode`;
@@ -109276,17 +109430,14 @@ var ExportHapAddon = class extends BaseAddon {
109276
109430
  immediate: true
109277
109431
  },
109278
109432
  {
109279
- type: "boolean",
109280
- key: hksvKey,
109433
+ type: "info",
109434
+ key: `hap:${deviceId}:hksvRecordingStatus`,
109281
109435
  label: "HomeKit recording (Secure Video)",
109282
- 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).",
109283
- style: "switch",
109284
- value: resolveHksvRecording(settings),
109436
+ content: describeHomekitRecording(readHomekitRecordingActive(this.exposed.get(String(deviceId))?.accessories ?? []), this.hksvBuilds.lastFor(deviceId)),
109285
109437
  showWhen: {
109286
109438
  field: enabledKey,
109287
109439
  equals: true
109288
- },
109289
- immediate: true
109440
+ }
109290
109441
  },
109291
109442
  {
109292
109443
  type: "boolean",
@@ -109309,7 +109460,7 @@ var ExportHapAddon = class extends BaseAddon {
109309
109460
  style: "switch",
109310
109461
  value: resolveKeepHomekitClips(settings),
109311
109462
  showWhen: {
109312
- field: hksvKey,
109463
+ field: enabledKey,
109313
109464
  equals: true
109314
109465
  },
109315
109466
  immediate: true
@@ -109409,7 +109560,6 @@ var ExportHapAddon = class extends BaseAddon {
109409
109560
  const wasEnabled = this.exposed.has(deviceIdStr);
109410
109561
  const enabledKey = `hap:${deviceId}:enabled`;
109411
109562
  const streamPreferenceKey = `hap:${deviceId}:streamPreference`;
109412
- const hksvKey = `hap:${deviceId}:hksvRecording`;
109413
109563
  const useBridgeKey = `hap:${deviceId}:useBridge`;
109414
109564
  const allowTranscodeKey = `hap:${deviceId}:allowTranscode`;
109415
109565
  const multiTierKey = `hap:${deviceId}:multiTierService`;
@@ -109418,7 +109568,6 @@ var ExportHapAddon = class extends BaseAddon {
109418
109568
  const enabledValue = enabledKey in patch ? Boolean(patch[enabledKey]) : wasEnabled;
109419
109569
  const streamPreferenceRaw = streamPreferenceKey in patch ? patch[streamPreferenceKey] : current?.settings?.streamPreference;
109420
109570
  const streamPreference = typeof streamPreferenceRaw === "string" && streamPreferenceRaw.trim().length > 0 ? streamPreferenceRaw : "auto";
109421
- const hksvRecording = hksvKey in patch ? Boolean(patch[hksvKey]) : resolveHksvRecording(current?.settings);
109422
109571
  const useBridge = useBridgeKey in patch ? Boolean(patch[useBridgeKey]) : current?.settings?.useBridge !== false;
109423
109572
  const allowTranscode = allowTranscodeKey in patch ? Boolean(patch[allowTranscodeKey]) : current?.settings?.allowTranscode !== false;
109424
109573
  const multiTierService = multiTierKey in patch ? Boolean(patch[multiTierKey]) : resolveMultiTierService(current?.settings);
@@ -109430,7 +109579,6 @@ var ExportHapAddon = class extends BaseAddon {
109430
109579
  const nextSettings = {
109431
109580
  ...current?.settings ?? DEFAULT_DEVICE_SETTINGS,
109432
109581
  streamPreference,
109433
- hksvRecording,
109434
109582
  useBridge,
109435
109583
  allowTranscode,
109436
109584
  multiTierService,
@@ -109447,7 +109595,6 @@ var ExportHapAddon = class extends BaseAddon {
109447
109595
  return { success: true };
109448
109596
  }
109449
109597
  const currentPref = current?.settings?.streamPreference ?? "auto";
109450
- const currentHksv = resolveHksvRecording(current?.settings);
109451
109598
  const currentKeepClips = resolveKeepHomekitClips(current?.settings);
109452
109599
  const mapperKind = current?.mapperKind ?? "generic";
109453
109600
  const wasBridged = shouldBridge({
@@ -109459,16 +109606,12 @@ var ExportHapAddon = class extends BaseAddon {
109459
109606
  useBridge
109460
109607
  });
109461
109608
  await this.updateEntrySettings(deviceIdStr, nextSettings);
109462
- if (currentPref !== streamPreference || currentHksv !== hksvRecording || currentKeepClips !== keepClips || wasBridged !== willBridge) {
109609
+ if (currentPref !== streamPreference || currentKeepClips !== keepClips || wasBridged !== willBridge) {
109463
109610
  log.info("export-hap: per-camera export settings changed — refreshing accessory", { meta: {
109464
109611
  streamPreference: {
109465
109612
  from: currentPref,
109466
109613
  to: streamPreference
109467
109614
  },
109468
- hksvRecording: {
109469
- from: currentHksv,
109470
- to: hksvRecording
109471
- },
109472
109615
  keepClips: {
109473
109616
  from: currentKeepClips,
109474
109617
  to: keepClips
@@ -109565,7 +109708,6 @@ exports.describeHksvCamera = describeHksvCamera;
109565
109708
  exports.initHapStorage = initHapStorage;
109566
109709
  exports.publishStandalone = publishStandalone;
109567
109710
  exports.resolveClipRetentionDays = resolveClipRetentionDays;
109568
- exports.resolveHksvRecording = resolveHksvRecording;
109569
109711
  exports.resolveKeepHomekitClips = resolveKeepHomekitClips;
109570
109712
  exports.resolveMultiTierService = resolveMultiTierService;
109571
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,
@@ -108382,38 +108544,7 @@ function syncStateToJson(map) {
108382
108544
  * that matters about it: it must resolve to NOT recording, or "off by default"
108383
108545
  * is true only for cameras exposed before the flip.
108384
108546
  */
108385
- var DEFAULT_DEVICE_SETTINGS = {
108386
- streamPreference: "auto",
108387
- hksvRecording: false
108388
- };
108389
- /**
108390
- * OFF unless explicitly switched on — operator decision 2026-09-21, reversing
108391
- * the 2026-08-08 flip:
108392
- *
108393
- * > *"il recording deve essere off di default, non deve essere attivo quando
108394
- * > si esporta una camera su HKSV"*
108395
- *
108396
- * What changed is not the cost the earlier record argued about — recording on
108397
- * the copy branch really is 0.7 % of a core and ~30 MB (D84), and cheap it
108398
- * remains. What changed is that on-by-default made the switch **say something
108399
- * the system was not doing**. Measured on 2026-09-21: 592, 587, 618 and 640
108400
- * all reported HomeKit recording ON while the advertisement was WITHHELD
108401
- * (`no-enabled-stream` — no enabled RTSP profile), so HomeKit never recorded
108402
- * them and the tee had nothing to keep. The operator was being told four
108403
- * cameras were recording because nobody had turned them off.
108404
- *
108405
- * Exposing a camera to HomeKit is a decision about EXPOSURE. Recording is a
108406
- * second decision and now has to be made.
108407
- *
108408
- * ABSENT resolves OFF — the load-bearing case, for the same reason it was
108409
- * before and with the opposite answer: a camera whose settings predate the
108410
- * field, or that was exposed before today, was never asked about. Every read
108411
- * goes through this one resolver (`=== true`), never a scattered `!== false`,
108412
- * because those two disagree about exactly that case.
108413
- */
108414
- function resolveHksvRecording(settings) {
108415
- return settings?.hksvRecording === true;
108416
- }
108547
+ var DEFAULT_DEVICE_SETTINGS = { streamPreference: "auto" };
108417
108548
  /**
108418
108549
  * Whether this camera offers Apple's NEW camera service alongside the classic
108419
108550
  * one — `camera-multi-tier-rtp-stream-management` (0x8031), where HEVC and 4K
@@ -108456,11 +108587,11 @@ var EXPORT_HAP_ADDON_ID = "export-hap";
108456
108587
  * stays and says the copies are off, which is D62's other half: an off switch
108457
108588
  * is REPORTED off, never made to look like a broken camera.
108458
108589
  */
108459
- function describeHksvCamera(entry, lastBuild) {
108590
+ function describeHksvCamera(entry, lastBuild, homekitRecording) {
108460
108591
  const isCamera = entry !== null && entry.mapperKind === "camera";
108461
108592
  return {
108462
108593
  exported: isCamera,
108463
- recording: isCamera && resolveHksvRecording(entry.settings),
108594
+ homekitRecording: isCamera ? homekitRecording : null,
108464
108595
  keepingClips: isCamera && resolveKeepHomekitClips(entry.settings),
108465
108596
  lastBuild
108466
108597
  };
@@ -108704,7 +108835,7 @@ var ExportHapAddon = class extends BaseAddon {
108704
108835
  const clipsProvider = createHksvVideoclipsProvider({
108705
108836
  logger: this.ctx.logger.child("hksv-clips"),
108706
108837
  addonId: EXPORT_HAP_ADDON_ID,
108707
- 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 ?? [])),
108708
108839
  readCatalog: async (deviceId) => await this.hksvClipStore?.listCatalog(deviceId) ?? { kind: "no-root" },
108709
108840
  locationRefusal: () => this.hksvClipLocation?.lastRefusal ?? null,
108710
108841
  mediaUrl: (deviceId, stem) => this.hksvClipUrl("media", deviceId, stem),
@@ -108860,7 +108991,6 @@ var ExportHapAddon = class extends BaseAddon {
108860
108991
  },
108861
108992
  hapDeviceSettings: {
108862
108993
  streamPreference: entrySettings.streamPreference ?? "auto",
108863
- hksvRecording: resolveHksvRecording(entrySettings),
108864
108994
  keepHomekitClips: resolveKeepHomekitClips(entrySettings),
108865
108995
  multiTierService: resolveMultiTierService(entrySettings),
108866
108996
  allowTranscode: entrySettings.allowTranscode !== false
@@ -108941,15 +109071,40 @@ var ExportHapAddon = class extends BaseAddon {
108941
109071
  features: device.features
108942
109072
  });
108943
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
+ });
108944
109079
  return resolveExportFingerprint({
108945
109080
  ready: device.probed ?? false,
108946
- fresh,
109081
+ fresh: withToken,
108947
109082
  persisted
108948
109083
  });
108949
109084
  } catch {
108950
109085
  return null;
108951
109086
  }
108952
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
+ }
108953
109108
  /** Persist a device's current export fingerprint into `exportSyncState`
108954
109109
  * (best-effort; called after a successful attach). */
108955
109110
  async persistFingerprint(deviceId, fingerprint) {
@@ -109215,7 +109370,6 @@ var ExportHapAddon = class extends BaseAddon {
109215
109370
  const enabled = entry !== null;
109216
109371
  const enabledKey = `hap:${deviceId}:enabled`;
109217
109372
  const streamPreferenceKey = `hap:${deviceId}:streamPreference`;
109218
- const hksvKey = `hap:${deviceId}:hksvRecording`;
109219
109373
  const multiTierKey = `hap:${deviceId}:multiTierService`;
109220
109374
  const useBridgeKey = `hap:${deviceId}:useBridge`;
109221
109375
  const allowTranscodeKey = `hap:${deviceId}:allowTranscode`;
@@ -109266,17 +109420,14 @@ var ExportHapAddon = class extends BaseAddon {
109266
109420
  immediate: true
109267
109421
  },
109268
109422
  {
109269
- type: "boolean",
109270
- key: hksvKey,
109423
+ type: "info",
109424
+ key: `hap:${deviceId}:hksvRecordingStatus`,
109271
109425
  label: "HomeKit recording (Secure Video)",
109272
- 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).",
109273
- style: "switch",
109274
- value: resolveHksvRecording(settings),
109426
+ content: describeHomekitRecording(readHomekitRecordingActive(this.exposed.get(String(deviceId))?.accessories ?? []), this.hksvBuilds.lastFor(deviceId)),
109275
109427
  showWhen: {
109276
109428
  field: enabledKey,
109277
109429
  equals: true
109278
- },
109279
- immediate: true
109430
+ }
109280
109431
  },
109281
109432
  {
109282
109433
  type: "boolean",
@@ -109299,7 +109450,7 @@ var ExportHapAddon = class extends BaseAddon {
109299
109450
  style: "switch",
109300
109451
  value: resolveKeepHomekitClips(settings),
109301
109452
  showWhen: {
109302
- field: hksvKey,
109453
+ field: enabledKey,
109303
109454
  equals: true
109304
109455
  },
109305
109456
  immediate: true
@@ -109399,7 +109550,6 @@ var ExportHapAddon = class extends BaseAddon {
109399
109550
  const wasEnabled = this.exposed.has(deviceIdStr);
109400
109551
  const enabledKey = `hap:${deviceId}:enabled`;
109401
109552
  const streamPreferenceKey = `hap:${deviceId}:streamPreference`;
109402
- const hksvKey = `hap:${deviceId}:hksvRecording`;
109403
109553
  const useBridgeKey = `hap:${deviceId}:useBridge`;
109404
109554
  const allowTranscodeKey = `hap:${deviceId}:allowTranscode`;
109405
109555
  const multiTierKey = `hap:${deviceId}:multiTierService`;
@@ -109408,7 +109558,6 @@ var ExportHapAddon = class extends BaseAddon {
109408
109558
  const enabledValue = enabledKey in patch ? Boolean(patch[enabledKey]) : wasEnabled;
109409
109559
  const streamPreferenceRaw = streamPreferenceKey in patch ? patch[streamPreferenceKey] : current?.settings?.streamPreference;
109410
109560
  const streamPreference = typeof streamPreferenceRaw === "string" && streamPreferenceRaw.trim().length > 0 ? streamPreferenceRaw : "auto";
109411
- const hksvRecording = hksvKey in patch ? Boolean(patch[hksvKey]) : resolveHksvRecording(current?.settings);
109412
109561
  const useBridge = useBridgeKey in patch ? Boolean(patch[useBridgeKey]) : current?.settings?.useBridge !== false;
109413
109562
  const allowTranscode = allowTranscodeKey in patch ? Boolean(patch[allowTranscodeKey]) : current?.settings?.allowTranscode !== false;
109414
109563
  const multiTierService = multiTierKey in patch ? Boolean(patch[multiTierKey]) : resolveMultiTierService(current?.settings);
@@ -109420,7 +109569,6 @@ var ExportHapAddon = class extends BaseAddon {
109420
109569
  const nextSettings = {
109421
109570
  ...current?.settings ?? DEFAULT_DEVICE_SETTINGS,
109422
109571
  streamPreference,
109423
- hksvRecording,
109424
109572
  useBridge,
109425
109573
  allowTranscode,
109426
109574
  multiTierService,
@@ -109437,7 +109585,6 @@ var ExportHapAddon = class extends BaseAddon {
109437
109585
  return { success: true };
109438
109586
  }
109439
109587
  const currentPref = current?.settings?.streamPreference ?? "auto";
109440
- const currentHksv = resolveHksvRecording(current?.settings);
109441
109588
  const currentKeepClips = resolveKeepHomekitClips(current?.settings);
109442
109589
  const mapperKind = current?.mapperKind ?? "generic";
109443
109590
  const wasBridged = shouldBridge({
@@ -109449,16 +109596,12 @@ var ExportHapAddon = class extends BaseAddon {
109449
109596
  useBridge
109450
109597
  });
109451
109598
  await this.updateEntrySettings(deviceIdStr, nextSettings);
109452
- if (currentPref !== streamPreference || currentHksv !== hksvRecording || currentKeepClips !== keepClips || wasBridged !== willBridge) {
109599
+ if (currentPref !== streamPreference || currentKeepClips !== keepClips || wasBridged !== willBridge) {
109453
109600
  log.info("export-hap: per-camera export settings changed — refreshing accessory", { meta: {
109454
109601
  streamPreference: {
109455
109602
  from: currentPref,
109456
109603
  to: streamPreference
109457
109604
  },
109458
- hksvRecording: {
109459
- from: currentHksv,
109460
- to: hksvRecording
109461
- },
109462
109605
  keepClips: {
109463
109606
  from: currentKeepClips,
109464
109607
  to: keepClips
@@ -109544,4 +109687,4 @@ function errMsg(err) {
109544
109687
  return err instanceof Error ? err.message : String(err);
109545
109688
  }
109546
109689
  //#endregion
109547
- 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, 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.134",
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",