homebridge-plugin-utils 1.35.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/README.md +130 -2
  2. package/build/eslint-plugin/README.md +164 -0
  3. package/build/eslint-plugin/config.mjs +308 -0
  4. package/build/eslint-plugin/index.mjs +7 -0
  5. package/build/eslint-plugin/plugin.mjs +46 -0
  6. package/build/eslint-plugin/rules/blank-line-after-open-brace.mjs +137 -0
  7. package/build/eslint-plugin/rules/blank-line-after-open-brace.test.mjs +112 -0
  8. package/build/eslint-plugin/rules/comment-style.mjs +190 -0
  9. package/build/eslint-plugin/rules/comment-style.test.mjs +190 -0
  10. package/build/eslint-plugin/rules/enforce-node-protocol.mjs +114 -0
  11. package/build/eslint-plugin/rules/enforce-node-protocol.test.mjs +116 -0
  12. package/build/eslint-plugin/rules/paren-comparisons-in-logical.mjs +94 -0
  13. package/build/eslint-plugin/rules/paren-comparisons-in-logical.test.mjs +107 -0
  14. package/build/eslint-plugin/rules/split-type-imports.mjs +354 -0
  15. package/build/eslint-plugin/rules/split-type-imports.test.mjs +268 -0
  16. package/build/eslint-plugin/test-setup.mjs +21 -0
  17. package/build/fs-ops.mjs +184 -0
  18. package/build/tsconfig.json +18 -3
  19. package/dist/backpressure.d.ts +94 -45
  20. package/dist/backpressure.js +229 -93
  21. package/dist/backpressure.js.map +1 -1
  22. package/dist/cli/index.d.ts +95 -0
  23. package/dist/cli/index.js +355 -0
  24. package/dist/cli/index.js.map +1 -0
  25. package/dist/clock-double.d.ts +93 -0
  26. package/dist/clock-double.js +141 -0
  27. package/dist/clock-double.js.map +1 -0
  28. package/dist/clock.d.ts +39 -0
  29. package/dist/clock.js +34 -0
  30. package/dist/clock.js.map +1 -0
  31. package/dist/eslint-plugin/config.d.mts +193 -0
  32. package/dist/eslint-plugin/index.d.mts +2 -0
  33. package/dist/eslint-plugin/plugin.d.mts +87 -0
  34. package/dist/eslint-plugin/rules/blank-line-after-open-brace.d.mts +19 -0
  35. package/dist/eslint-plugin/rules/comment-style.d.mts +15 -0
  36. package/dist/eslint-plugin/rules/enforce-node-protocol.d.mts +18 -0
  37. package/dist/eslint-plugin/rules/paren-comparisons-in-logical.d.mts +15 -0
  38. package/dist/eslint-plugin/rules/split-type-imports.d.mts +16 -0
  39. package/dist/featureOptions-docs.d.ts +96 -0
  40. package/dist/featureOptions-docs.js +193 -0
  41. package/dist/featureOptions-docs.js.map +1 -0
  42. package/dist/featureOptions.d.ts +674 -0
  43. package/dist/featureOptions.js +870 -0
  44. package/dist/featureOptions.js.map +1 -0
  45. package/dist/ffmpeg/codecs.d.ts +256 -72
  46. package/dist/ffmpeg/codecs.js +477 -262
  47. package/dist/ffmpeg/codecs.js.map +1 -1
  48. package/dist/ffmpeg/dgram-util.d.ts +46 -0
  49. package/dist/ffmpeg/dgram-util.js +38 -0
  50. package/dist/ffmpeg/dgram-util.js.map +1 -0
  51. package/dist/ffmpeg/exec.d.ts +83 -64
  52. package/dist/ffmpeg/exec.js +77 -86
  53. package/dist/ffmpeg/exec.js.map +1 -1
  54. package/dist/ffmpeg/fmp4.d.ts +49 -2
  55. package/dist/ffmpeg/fmp4.js +47 -11
  56. package/dist/ffmpeg/fmp4.js.map +1 -1
  57. package/dist/ffmpeg/hap-enums.d.ts +214 -0
  58. package/dist/ffmpeg/hap-enums.js +92 -0
  59. package/dist/ffmpeg/hap-enums.js.map +1 -0
  60. package/dist/ffmpeg/index.d.ts +16 -9
  61. package/dist/ffmpeg/index.js +6 -0
  62. package/dist/ffmpeg/index.js.map +1 -1
  63. package/dist/ffmpeg/mp4-assembler.d.ts +120 -0
  64. package/dist/ffmpeg/mp4-assembler.js +387 -0
  65. package/dist/ffmpeg/mp4-assembler.js.map +1 -0
  66. package/dist/ffmpeg/mp4-parser.d.ts +94 -0
  67. package/dist/ffmpeg/mp4-parser.js +130 -0
  68. package/dist/ffmpeg/mp4-parser.js.map +1 -0
  69. package/dist/ffmpeg/options.d.ts +62 -149
  70. package/dist/ffmpeg/options.js +608 -499
  71. package/dist/ffmpeg/options.js.map +1 -1
  72. package/dist/ffmpeg/process.d.ts +142 -96
  73. package/dist/ffmpeg/process.js +406 -278
  74. package/dist/ffmpeg/process.js.map +1 -1
  75. package/dist/ffmpeg/record.d.ts +302 -188
  76. package/dist/ffmpeg/record.js +408 -567
  77. package/dist/ffmpeg/record.js.map +1 -1
  78. package/dist/ffmpeg/recording-process-double.d.ts +144 -0
  79. package/dist/ffmpeg/recording-process-double.js +168 -0
  80. package/dist/ffmpeg/recording-process-double.js.map +1 -0
  81. package/dist/ffmpeg/rtp-parser.d.ts +70 -0
  82. package/dist/ffmpeg/rtp-parser.js +77 -0
  83. package/dist/ffmpeg/rtp-parser.js.map +1 -0
  84. package/dist/ffmpeg/rtp.d.ts +198 -141
  85. package/dist/ffmpeg/rtp.js +473 -251
  86. package/dist/ffmpeg/rtp.js.map +1 -1
  87. package/dist/ffmpeg/settings.d.ts +5 -2
  88. package/dist/ffmpeg/settings.js +20 -5
  89. package/dist/ffmpeg/settings.js.map +1 -1
  90. package/dist/ffmpeg/stream.d.ts +57 -107
  91. package/dist/ffmpeg/stream.js +121 -150
  92. package/dist/ffmpeg/stream.js.map +1 -1
  93. package/dist/formatters.d.ts +106 -0
  94. package/dist/formatters.js +174 -0
  95. package/dist/formatters.js.map +1 -0
  96. package/dist/homebridge-enums.d.ts +30 -0
  97. package/dist/homebridge-enums.js +17 -0
  98. package/dist/homebridge-enums.js.map +1 -0
  99. package/dist/index.d.ts +12 -6
  100. package/dist/index.js +7 -2
  101. package/dist/index.js.map +1 -1
  102. package/dist/logclient/auth.d.ts +114 -0
  103. package/dist/logclient/auth.js +199 -0
  104. package/dist/logclient/auth.js.map +1 -0
  105. package/dist/logclient/cli-run.d.ts +76 -0
  106. package/dist/logclient/cli-run.js +639 -0
  107. package/dist/logclient/cli-run.js.map +1 -0
  108. package/dist/logclient/cli.d.ts +3 -0
  109. package/dist/logclient/cli.js +97 -0
  110. package/dist/logclient/cli.js.map +1 -0
  111. package/dist/logclient/client.d.ts +145 -0
  112. package/dist/logclient/client.js +600 -0
  113. package/dist/logclient/client.js.map +1 -0
  114. package/dist/logclient/config.d.ts +173 -0
  115. package/dist/logclient/config.js +199 -0
  116. package/dist/logclient/config.js.map +1 -0
  117. package/dist/logclient/endpoints.d.ts +54 -0
  118. package/dist/logclient/endpoints.js +73 -0
  119. package/dist/logclient/endpoints.js.map +1 -0
  120. package/dist/logclient/filter.d.ts +45 -0
  121. package/dist/logclient/filter.js +51 -0
  122. package/dist/logclient/filter.js.map +1 -0
  123. package/dist/logclient/frame.d.ts +93 -0
  124. package/dist/logclient/frame.js +203 -0
  125. package/dist/logclient/frame.js.map +1 -0
  126. package/dist/logclient/index.d.ts +31 -0
  127. package/dist/logclient/index.js +12 -0
  128. package/dist/logclient/index.js.map +1 -0
  129. package/dist/logclient/parser.d.ts +211 -0
  130. package/dist/logclient/parser.js +393 -0
  131. package/dist/logclient/parser.js.map +1 -0
  132. package/dist/logclient/rest.d.ts +41 -0
  133. package/dist/logclient/rest.js +111 -0
  134. package/dist/logclient/rest.js.map +1 -0
  135. package/dist/logclient/settings.d.ts +15 -0
  136. package/dist/logclient/settings.js +64 -0
  137. package/dist/logclient/settings.js.map +1 -0
  138. package/dist/logclient/socket-double.d.ts +201 -0
  139. package/dist/logclient/socket-double.js +384 -0
  140. package/dist/logclient/socket-double.js.map +1 -0
  141. package/dist/logclient/socket.d.ts +257 -0
  142. package/dist/logclient/socket.js +620 -0
  143. package/dist/logclient/socket.js.map +1 -0
  144. package/dist/logclient/stitch.d.ts +83 -0
  145. package/dist/logclient/stitch.js +146 -0
  146. package/dist/logclient/stitch.js.map +1 -0
  147. package/dist/logclient/time-expression.d.ts +42 -0
  148. package/dist/logclient/time-expression.js +181 -0
  149. package/dist/logclient/time-expression.js.map +1 -0
  150. package/dist/logclient/time-window.d.ts +38 -0
  151. package/dist/logclient/time-window.js +53 -0
  152. package/dist/logclient/time-window.js.map +1 -0
  153. package/dist/logclient/types.d.ts +107 -0
  154. package/dist/logclient/types.js +6 -0
  155. package/dist/logclient/types.js.map +1 -0
  156. package/dist/mqttClient.d.ts +287 -0
  157. package/dist/mqttClient.js +433 -0
  158. package/dist/mqttClient.js.map +1 -0
  159. package/dist/service.d.ts +64 -15
  160. package/dist/service.js +93 -66
  161. package/dist/service.js.map +1 -1
  162. package/dist/ui/featureOptions.js +870 -0
  163. package/dist/ui/featureOptions.js.map +1 -0
  164. package/dist/ui/formatters.js +174 -0
  165. package/dist/ui/formatters.js.map +1 -0
  166. package/dist/ui/pluginConfigSession.mjs +141 -0
  167. package/dist/ui/webUi-featureOptions/categoryState.mjs +135 -0
  168. package/dist/ui/webUi-featureOptions/effects/keyboard.mjs +61 -0
  169. package/dist/ui/webUi-featureOptions/effects/persist.mjs +226 -0
  170. package/dist/ui/webUi-featureOptions/effects/theme.mjs +398 -0
  171. package/dist/ui/webUi-featureOptions/effects/tokens.mjs +152 -0
  172. package/dist/ui/webUi-featureOptions/rendering.mjs +431 -0
  173. package/dist/ui/webUi-featureOptions/selectors.mjs +360 -0
  174. package/dist/ui/webUi-featureOptions/state.mjs +319 -0
  175. package/dist/ui/webUi-featureOptions/store.mjs +181 -0
  176. package/dist/ui/webUi-featureOptions/utils.mjs +200 -0
  177. package/dist/ui/webUi-featureOptions/views/connectionError.mjs +152 -0
  178. package/dist/ui/webUi-featureOptions/views/deviceInfo.mjs +80 -0
  179. package/dist/ui/webUi-featureOptions/views/header.mjs +77 -0
  180. package/dist/ui/webUi-featureOptions/views/nav.mjs +327 -0
  181. package/dist/ui/webUi-featureOptions/views/options.mjs +521 -0
  182. package/dist/ui/webUi-featureOptions/views/search.mjs +395 -0
  183. package/dist/ui/webUi-featureOptions.mjs +677 -0
  184. package/dist/ui/webUi.mjs +192 -87
  185. package/dist/util.d.ts +602 -45
  186. package/dist/util.js +783 -78
  187. package/dist/util.js.map +1 -1
  188. package/package.json +33 -15
  189. package/build/eslint-rules.mjs +0 -511
  190. package/dist/featureoptions.d.ts +0 -264
  191. package/dist/featureoptions.js +0 -480
  192. package/dist/featureoptions.js.map +0 -1
  193. package/dist/mqttclient.d.ts +0 -178
  194. package/dist/mqttclient.js +0 -310
  195. package/dist/mqttclient.js.map +0 -1
  196. package/dist/ui/featureoptions.js +0 -480
  197. package/dist/ui/featureoptions.js.map +0 -1
  198. package/dist/ui/webUi-featureoptions.mjs +0 -3765
@@ -2,7 +2,66 @@
2
2
  *
3
3
  * ffmpeg/options.ts: FFmpeg decoder and encoder options with hardware-accelerated codec support where available.
4
4
  */
5
- import { HOMEKIT_STREAMING_HEADROOM, RPI_GPU_MINIMUM } from "./settings.js";
5
+ import { HOMEKIT_STREAMING_HEADROOM, RPI4_GPU_MINIMUM, RPI4_HW_TRANSCODE_MAX_PIXELS } from "./settings.js";
6
+ import { AudioRecordingCodecType } from "./hap-enums.js";
7
+ // HAP protocol const enum values mirrored locally for the H264 enums that are unique to this module. `verbatimModuleSyntax` disallows value imports of ambient const
8
+ // enums, so we preserve the canonical names in code by mirroring the numeric contract from hap-nodejs. Values MUST stay in lockstep with the upstream definitions in
9
+ // `hap-nodejs/.../RTPStreamManagement.d.ts`. `AudioRecordingCodecType` is hoisted to `hap-enums.ts` so this module and `record.ts` share one mirror.
10
+ const H264Level = { LEVEL3_1: 0, LEVEL3_2: 1, LEVEL4_0: 2 };
11
+ const H264Profile = { BASELINE: 0, HIGH: 2, MAIN: 1 };
12
+ // Translation tables for the H264 enum values that shape FFmpeg's `-level:v` and `-profile:v` arguments. `as const satisfies Record<...>` pins each map to exhaustively
13
+ // cover every enum member at compile time - a new enum value added upstream forces the build to fail until the table is updated, so encoder-argument emission cannot
14
+ // silently default to a wrong value when consumers extend the upstream enum. The two-key payload (`numeric`/`string`) matches FFmpeg's two emission shapes per argument.
15
+ const H264_LEVEL_NAMES = {
16
+ [H264Level.LEVEL3_1]: { numeric: "31", string: "3.1" },
17
+ [H264Level.LEVEL3_2]: { numeric: "32", string: "3.2" },
18
+ [H264Level.LEVEL4_0]: { numeric: "40", string: "4.0" }
19
+ };
20
+ const H264_PROFILE_NAMES = {
21
+ [H264Profile.BASELINE]: { numeric: "66", string: "baseline" },
22
+ [H264Profile.HIGH]: { numeric: "100", string: "high" },
23
+ [H264Profile.MAIN]: { numeric: "77", string: "main" }
24
+ };
25
+ // Pure derivations that every encoder path - software livestream, software record, and each per-platform hardware handler - uses identically. Centralizing them in
26
+ // module-scope functions prevents bitrate / buffer / keyframe math from drifting across platforms and keeps each caller a one-line interpolation rather than an inline
27
+ // arithmetic expression.
28
+ // `-g:v` value. HomeKit's idrInterval is expressed in seconds, so the GOP length in frames is (fps * idrInterval).
29
+ function gopArg(options) {
30
+ return (options.fps * options.idrInterval).toString();
31
+ }
32
+ // `-bufsize` value, in kbps. A rate-control buffer sized at twice the target bitrate is the convention we apply uniformly across encoders.
33
+ function bufsizeArg(options) {
34
+ return (2 * options.bitrate).toString() + "k";
35
+ }
36
+ // `-maxrate` value, in kbps. Adds the streaming headroom only when smart-quality is enabled; otherwise caps strictly at the requested bitrate.
37
+ function maxrateArg(options) {
38
+ return (options.bitrate + (options.smartQuality ? HOMEKIT_STREAMING_HEADROOM : 0)).toString() + "k";
39
+ }
40
+ // `-b:v` value, in kbps. Emitted on the libx264 / macOS.Intel / raspbian / pre-FFmpeg-8.x fallback paths and on the smartQuality-off branches of every encoder that
41
+ // has a quality-constrained mode. Extracted here so every `-b:v` emission across the class flows through a single formatter, matching the gop/bufsize/maxrate pattern.
42
+ function bitrateArg(options) {
43
+ return options.bitrate.toString() + "k";
44
+ }
45
+ // Canonicalize a caller-supplied codec string into our `SupportedDecodeCodec` surface, or `undefined` when the codec is unsupported. A single frozen table encodes
46
+ // both the case-insensitive normalization (all keys are lowercase; `canonicalDecodeCodec` lowercases its input before lookup) and the `h265 -> hevc` alias, so callers
47
+ // never have to branch on either separately and the mapping documents the full accepted input surface in one place. Symmetric with `QSV_DECODER_BY_CODEC`: both are
48
+ // frozen Records, TypeScript-readonly at the type level and runtime-immutable via `Object.freeze`.
49
+ const DECODE_CODEC_ALIASES = Object.freeze({
50
+ "av1": "av1",
51
+ "h264": "h264",
52
+ "h265": "hevc",
53
+ "hevc": "hevc"
54
+ });
55
+ function canonicalDecodeCodec(codec) {
56
+ return DECODE_CODEC_ALIASES[codec.toLowerCase()];
57
+ }
58
+ // Intel QSV decoder naming: each canonical codec maps to its `<codec>_qsv` variant. Typed against `SupportedDecodeCodec` (not `string`) so the lookup is total at the
59
+ // type level and handlers don't need runtime fallbacks. Frozen so the mapping is effectively immutable at the module boundary.
60
+ const QSV_DECODER_BY_CODEC = Object.freeze({
61
+ "av1": "av1_qsv",
62
+ "h264": "h264_qsv",
63
+ "hevc": "hevc_qsv"
64
+ });
6
65
  /**
7
66
  * Provides Homebridge FFmpeg transcoding, decoding, and encoding options, selecting codecs, pixel formats, and hardware acceleration for the host system.
8
67
  *
@@ -43,26 +102,11 @@ import { HOMEKIT_STREAMING_HEADROOM, RPI_GPU_MINIMUM } from "./settings.js";
43
102
  */
44
103
  export class FfmpegOptions {
45
104
  /**
46
- * FFmpeg codec and hardware capabilities for the current host.
47
- *
48
- */
49
- codecSupport;
50
- /**
51
- * The configuration options used to initialize this instance.
105
+ * The configuration options used to initialize this instance. This is the single stored state on `FfmpegOptions`: every other public field on this class is either
106
+ * a getter that forwards to `this.config`, or a fixed constant independent of it (`audioDecoder`), so external callers have exactly one canonical path to each
107
+ * config-backed value and internal code never has to keep a parallel field in sync with `config` at construction time.
52
108
  */
53
109
  config;
54
- /**
55
- * Indicates if debug logging is enabled.
56
- */
57
- debug;
58
- /**
59
- * Logging interface for output and errors.
60
- */
61
- log;
62
- /**
63
- * Function returning the name for this options instance to be used for logging.
64
- */
65
- name;
66
110
  /**
67
111
  * Creates an instance of Homebridge FFmpeg encoding and decoding options.
68
112
  *
@@ -75,13 +119,34 @@ export class FfmpegOptions {
75
119
  * ```
76
120
  */
77
121
  constructor(options) {
78
- this.codecSupport = options.codecSupport;
79
122
  this.config = options;
80
- this.debug = options.debug ?? false;
81
- this.log = options.log;
82
- this.name = options.name;
83
123
  // Configure our hardware acceleration support.
84
- this.configureHwAccel();
124
+ this.#configureHwAccel();
125
+ }
126
+ /**
127
+ * Indicates if debug logging is enabled. Normalizes `undefined` to `false` so callers always see a definite boolean regardless of whether the config object set
128
+ * the field explicitly.
129
+ */
130
+ get debug() {
131
+ return this.config.debug ?? false;
132
+ }
133
+ /**
134
+ * Logging interface for output and errors.
135
+ */
136
+ get log() {
137
+ return this.config.log;
138
+ }
139
+ /**
140
+ * Function returning the name for this options instance to be used for logging.
141
+ */
142
+ get name() {
143
+ return this.config.name;
144
+ }
145
+ // Internal alias for the config's codec capabilities, so class-internal code reads `this.#codecSupport.X` rather than the longer `this.config.codecSupport.X`. No
146
+ // public exposure - external callers use `ffmpegOpts.config.codecSupport` as the single canonical path. The getter avoids duplicating state at construction: the
147
+ // config object is the source of truth, and this accessor is a one-line forward.
148
+ get #codecSupport() {
149
+ return this.config.codecSupport;
85
150
  }
86
151
  /**
87
152
  * Determines and configures hardware-accelerated video decoding and transcoding for the host system.
@@ -92,8 +157,6 @@ export class FfmpegOptions {
92
157
  *
93
158
  * This method is called automatically by the `FfmpegOptions` constructor and is not intended to be called directly.
94
159
  *
95
- * @returns `true` if hardware-accelerated transcoding is enabled after configuration, otherwise `false`.
96
- *
97
160
  * @example
98
161
  *
99
162
  * ```ts
@@ -107,115 +170,142 @@ export class FfmpegOptions {
107
170
  * @see FfmpegCodecs
108
171
  * @see FfmpegOptions
109
172
  */
110
- configureHwAccel() {
111
- let logMessage = "";
112
- // Utility to return which hardware acceleration features are currently available to us.
113
- const accelCategories = () => {
114
- const categories = [];
115
- if (this.config.hardwareDecoding) {
116
- categories.push("decoding");
117
- }
118
- if (this.config.hardwareTranscoding) {
119
- categories.push("\u26ED\uFE0E transcoding");
173
+ #configureHwAccel() {
174
+ // Dispatch to the per-platform handler. Each handler captures its platform's complete hardware-setup story - capability validation, force-disable overrides,
175
+ // platform-specific autopromotion - in one contiguous block, so interdependencies (raspbian's GPU-mem gate affecting both capabilities, QSV's transcoding ->
176
+ // decoding autopromote) read locally rather than scattered across shared decoding / transcoding switches. The returned platform label decorates the final
177
+ // "enabled" log line so readers can distinguish the variant at a glance without re-deriving it from the host system.
178
+ let platformLabel = "";
179
+ switch (this.#codecSupport.hostSystem) {
180
+ case "macOS.Apple":
181
+ case "macOS.Intel":
182
+ platformLabel = this.#configureMacOSHwAccel();
183
+ break;
184
+ case "raspbian":
185
+ platformLabel = this.#configureRaspbianHwAccel();
186
+ break;
187
+ default:
188
+ platformLabel = this.#configureQsvHwAccel();
189
+ break;
190
+ }
191
+ // Inform the user. Only emits when some hardware capability ended up enabled - if every capability validation disabled both flags, we stay silent rather than
192
+ // logging "enabled: (nothing)."
193
+ if (this.config.hardwareDecoding || this.config.hardwareTranscoding) {
194
+ this.log.info("\u26A1\uFE0F Hardware-accelerated " + this.#accelCategoriesLabel() + " enabled" + (platformLabel.length ? ": " + platformLabel : "") + ".");
195
+ }
196
+ }
197
+ /**
198
+ * Configure hardware acceleration for macOS (Apple Silicon and Intel Mac).
199
+ *
200
+ * Validates the VideoToolbox hardware accelerator when decoding is requested and the `h264_videotoolbox` encoder when transcoding is requested; disables the
201
+ * corresponding flag if the validation fails. Additionally, when transcoding is requested, checks for the native macOS `aac_at` AAC encoder and warns (non-fatal) if
202
+ * it is missing, since `audioEncoder` will fall back to `libfdk_aac` in that case.
203
+ *
204
+ * @returns An empty string - macOS has no platform-specific label for the "enabled" log line.
205
+ */
206
+ #configureMacOSHwAccel() {
207
+ if (this.config.hardwareDecoding) {
208
+ this.#validateHwAccel("videotoolbox");
209
+ }
210
+ if (this.config.hardwareTranscoding) {
211
+ this.#validateEncoder("h264_videotoolbox");
212
+ if (!this.#codecSupport.hasEncoder("aac", "aac_at")) {
213
+ this.log.error("Your video processor does not have support for the native macOS AAC encoder, aac_at. Will attempt to use libfdk_aac instead.");
120
214
  }
121
- return categories.join(" and ");
122
- };
123
- // Hardware-accelerated decoding is enabled by default, where supported. Let's select the decoder options accordingly where supported.
215
+ }
216
+ return "";
217
+ }
218
+ /**
219
+ * Configure hardware acceleration for Raspberry Pi.
220
+ *
221
+ * GPU memory is the umbrella gate, but only when hardware decoding is requested: if the advertised GPU-memory allocation is below `RPI4_GPU_MINIMUM`, both decoding
222
+ * and transcoding are disabled with an info-level diagnostic (the Pi's hardware codec driver won't perform reliably below that threshold). A transcoding-only
223
+ * request bypasses this check by design and does not consult GPU memory at all. When memory is sufficient, hardware decoding is still force-disabled because of an
224
+ * unresolved FFmpeg 7 regression in the `h264_v4l2m2m` decoder, which fails to initialize on Raspberry Pi with current FFmpeg builds. Hardware transcoding is
225
+ * validated against the `h264_v4l2m2m` encoder.
226
+ *
227
+ * @returns A descriptive label for the "enabled" log line when hardware transcoding was requested, noting that HKSV recordings still use software transcoding even
228
+ * when livestream transcoding runs on the hardware encoder. Empty string otherwise.
229
+ */
230
+ #configureRaspbianHwAccel() {
124
231
  if (this.config.hardwareDecoding) {
125
- // Utility function to check that we have a specific decoder codec available to us.
126
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
127
- const validateDecoder = (codec, pixelFormat) => {
128
- if (!this.config.codecSupport.hasDecoder("h264", codec)) {
129
- this.log.error("Unable to enable hardware-accelerated decoding. Your video processor does not have support for the " + codec + " decoder. " +
130
- "Using software decoding instead.");
131
- this.config.hardwareDecoding = false;
132
- return false;
133
- }
134
- return true;
135
- };
136
- // Utility function to check that we have a specific hardware accelerator available to us.
137
- const validateHwAccel = (accel) => {
138
- if (!this.config.codecSupport.hasHwAccel(accel)) {
139
- this.log.error("Unable to enable hardware-accelerated decoding. Your video processor does not have support for the " + accel + " hardware accelerator. " +
140
- "Using software decoding instead.");
141
- this.config.hardwareDecoding = false;
142
- return false;
143
- }
144
- return true;
145
- };
146
- switch (this.codecSupport.hostSystem) {
147
- case "macOS.Apple":
148
- case "macOS.Intel":
149
- // Verify that we have hardware-accelerated decoding available to us.
150
- validateHwAccel("videotoolbox");
151
- break;
152
- case "raspbian":
153
- // If it's less than the minimum hardware GPU memory we need on an Raspberry Pi, we revert back to our default decoder.
154
- if (this.config.codecSupport.gpuMem < RPI_GPU_MINIMUM) {
155
- this.log.info("Disabling hardware-accelerated %s. Adjust the GPU memory configuration on your Raspberry Pi to at least %s MB to enable it.", accelCategories(), RPI_GPU_MINIMUM);
156
- this.config.hardwareDecoding = false;
157
- this.config.hardwareTranscoding = false;
158
- return false;
159
- }
160
- // Verify that we have the hardware decoder available to us. Unfortunately, as of FFmpeg 7, it seems that hardware decoding is flaky, at best, on Raspberry Pi.
161
- // validateDecoder("h264_v4l2m2m", [ "yuv420p" ]);
162
- this.config.hardwareDecoding = false;
163
- break;
164
- default:
165
- // Back to software decoding unless we're on a known system that always supports hardware decoding.
166
- this.config.hardwareDecoding = false;
167
- break;
232
+ // GPU-mem gate: insufficient memory disables every hardware capability. We run this only inside the decoding branch to preserve the legacy behavior where a
233
+ // transcoding-only request on raspbian skipped the GPU-mem check; moving the gate to always-fire would change observable behavior for that configuration.
234
+ if (this.#codecSupport.gpuMem < RPI4_GPU_MINIMUM) {
235
+ this.log.info("Disabling hardware-accelerated %s. Adjust the GPU memory configuration on your Raspberry Pi to at least %s MB to enable it.", this.#accelCategoriesLabel(), RPI4_GPU_MINIMUM);
236
+ this.config.hardwareDecoding = false;
237
+ this.config.hardwareTranscoding = false;
238
+ return "";
168
239
  }
240
+ // FFmpeg 7+ introduced a regression in h264_v4l2m2m decoding on Raspberry Pi, so we force software decoding here until upstream resolves it. When the
241
+ // FFmpeg regression is fixed, re-enable by validating the decoder availability and clearing this override.
242
+ this.config.hardwareDecoding = false;
169
243
  }
170
- // If we've enabled hardware-accelerated transcoding, let's select the encoder options accordingly where supported.
171
244
  if (this.config.hardwareTranscoding) {
172
- // Utility function to check that we have a specific encoder codec available to us.
173
- const validateEncoder = (codec) => {
174
- if (!this.config.codecSupport.hasEncoder("h264", codec)) {
175
- this.log.error("Unable to enable hardware-accelerated transcoding. Your video processor does not have support for the " + codec + " encoder. " +
176
- "Using software transcoding instead.");
177
- this.config.hardwareTranscoding = false;
178
- return false;
179
- }
180
- return true;
181
- };
182
- switch (this.codecSupport.hostSystem) {
183
- case "macOS.Apple":
184
- case "macOS.Intel":
185
- // Verify that we have the hardware encoder available to us.
186
- validateEncoder("h264_videotoolbox");
187
- // Validate that we have access to the AudioToolbox AAC encoder.
188
- if (!this.config.codecSupport.hasEncoder("aac", "aac_at")) {
189
- this.log.error("Your video processor does not have support for the native macOS AAC encoder, aac_at. Will attempt to use libfdk_aac instead.");
190
- }
191
- break;
192
- case "raspbian":
193
- // Verify that we have the hardware encoder available to us.
194
- validateEncoder("h264_v4l2m2m");
195
- logMessage = "Raspberry Pi hardware acceleration will be used for livestreaming. " +
196
- "HomeKit Secure Video recordings are not supported by the hardware encoder and will use software transcoding instead";
197
- break;
198
- default:
199
- // Let's see if we have Intel QuickSync hardware decoding available to us.
200
- if (this.config.codecSupport.hasHwAccel("qsv") &&
201
- this.config.codecSupport.hasDecoder("h264", "h264_qsv") && this.config.codecSupport.hasEncoder("h264", "h264_qsv") &&
202
- this.config.codecSupport.hasDecoder("hevc", "hevc_qsv")) {
203
- this.config.hardwareDecoding = true;
204
- logMessage = "Intel Quick Sync Video";
205
- }
206
- else {
207
- // Back to software encoding.
208
- this.config.hardwareDecoding = false;
209
- this.config.hardwareTranscoding = false;
210
- }
211
- break;
245
+ this.#validateEncoder("h264_v4l2m2m");
246
+ return "Raspberry Pi hardware acceleration will be used for livestreaming. " +
247
+ "HomeKit Secure Video recordings are not supported by the hardware encoder and will use software transcoding instead";
248
+ }
249
+ return "";
250
+ }
251
+ /**
252
+ * Configure hardware acceleration for Intel QSV (Quick Sync Video) and other generic hosts.
253
+ *
254
+ * Hardware decoding is never enabled by default on generic hosts - only when the transcoding path below autopromotes it on detection of the full QSV capability set
255
+ * (`qsv` hwaccel + `h264_qsv` encoder + `h264_qsv` decoder + `hevc_qsv` decoder). If transcoding is requested but QSV isn't available, both flags fall back to
256
+ * software.
257
+ *
258
+ * @returns `"Intel Quick Sync Video"` when QSV was autopromoted, an empty string otherwise.
259
+ */
260
+ #configureQsvHwAccel() {
261
+ // Generic hosts never decode-on-demand - decoding is only enabled as a side effect of transcoding autopromoting it below.
262
+ this.config.hardwareDecoding = false;
263
+ if (this.config.hardwareTranscoding) {
264
+ const qsvReady = this.#codecSupport.hasHwAccel("qsv") &&
265
+ this.#codecSupport.hasDecoder("h264", "h264_qsv") && this.#codecSupport.hasEncoder("h264", "h264_qsv") &&
266
+ this.#codecSupport.hasDecoder("hevc", "hevc_qsv");
267
+ if (qsvReady) {
268
+ this.config.hardwareDecoding = true;
269
+ return "Intel Quick Sync Video";
212
270
  }
271
+ // QSV not advertised - fall back to software for both capabilities so the final log stays silent about this instance.
272
+ this.config.hardwareTranscoding = false;
213
273
  }
214
- // Inform the user.
215
- if (this.config.hardwareDecoding || this.config.hardwareTranscoding) {
216
- this.log.info("\u26A1\uFE0F Hardware-accelerated " + accelCategories() + " enabled" + (logMessage.length ? ": " + logMessage : "") + ".");
274
+ return "";
275
+ }
276
+ // Shared validator: the requested hardware accelerator is advertised by the probe. Used only when hardware decoding is requested. On failure, logs the miss at error
277
+ // level and flips `hardwareDecoding` to false so downstream encoders transparently fall back to software.
278
+ #validateHwAccel(accel) {
279
+ if (this.#codecSupport.hasHwAccel(accel)) {
280
+ return true;
281
+ }
282
+ this.log.error("Unable to enable hardware-accelerated decoding. Your video processor does not have support for the " + accel + " hardware accelerator. " +
283
+ "Using software decoding instead.");
284
+ this.config.hardwareDecoding = false;
285
+ return false;
286
+ }
287
+ // Shared validator: the requested H.264 encoder is advertised by the probe. Used when hardware transcoding is requested. On failure, logs the miss at error level
288
+ // and flips `hardwareTranscoding` to false so streamEncoder and friends transparently fall back to libx264.
289
+ #validateEncoder(codec) {
290
+ if (this.#codecSupport.hasEncoder("h264", codec)) {
291
+ return true;
292
+ }
293
+ this.log.error("Unable to enable hardware-accelerated transcoding. Your video processor does not have support for the " + codec + " encoder. " +
294
+ "Using software transcoding instead.");
295
+ this.config.hardwareTranscoding = false;
296
+ return false;
297
+ }
298
+ // Human-readable "X and Y" label for the capabilities currently enabled - used in the final "enabled" log line and the raspbian GPU-mem warning. Consults the live
299
+ // `this.config.hardwareDecoding` / `hardwareTranscoding` values so callers can invoke it before, during, or after the per-platform configuration reshuffles them.
300
+ #accelCategoriesLabel() {
301
+ const categories = [];
302
+ if (this.config.hardwareDecoding) {
303
+ categories.push("decoding");
304
+ }
305
+ if (this.config.hardwareTranscoding) {
306
+ categories.push("\u26ED\uFE0E transcoding");
217
307
  }
218
- return this.config.hardwareTranscoding;
308
+ return categories.join(" and ");
219
309
  }
220
310
  /**
221
311
  * Determines the required hardware transfer filters based on the decoding and encoding configuration.
@@ -223,10 +313,11 @@ export class FfmpegOptions {
223
313
  * This method manages the transition between software and hardware processing contexts. When video data needs to move between the CPU and GPU for processing, we
224
314
  * provide the appropriate FFmpeg filters to handle that transfer efficiently.
225
315
  *
226
- * @param options - Video encoder options including hardware decoding and transcoding state.
316
+ * @param options - The hardware-state view with the decoding and transcoding flags. Callers pass either a full `VideoEncoderOptions` (structurally assignable) or a
317
+ * purpose-built object exposing the same flags - whichever is natural at the call site.
227
318
  * @returns Array of filter strings for hardware upload or download operations.
228
319
  */
229
- getHardwareTransferFilters(options) {
320
+ #getHardwareTransferFilters(options) {
230
321
  const filters = [];
231
322
  // We need to handle four possible state transitions between decoding and encoding.
232
323
  //
@@ -238,11 +329,11 @@ export class FfmpegOptions {
238
329
  const needsDownload = options.hardwareDecoding && !options.hardwareTranscoding;
239
330
  if (needsUpload) {
240
331
  // We need to upload frames from system memory to the GPU for hardware encoding.
241
- switch (this.codecSupport.hostSystem) {
332
+ switch (this.#codecSupport.hostSystem) {
242
333
  case "macOS.Apple":
243
334
  case "macOS.Intel":
244
335
  // FFmpeg 8.x on macOS requires explicit upload when moving from software decoding to VideoToolbox encoding.
245
- if (this.config.codecSupport.ffmpegVersion.startsWith("8.")) {
336
+ if (this.#codecSupport.ffmpegAtLeast(8)) {
246
337
  filters.push("hwupload");
247
338
  }
248
339
  break;
@@ -259,11 +350,11 @@ export class FfmpegOptions {
259
350
  }
260
351
  if (needsDownload) {
261
352
  // We need to download frames from the GPU to system memory for software encoding.
262
- switch (this.codecSupport.hostSystem) {
353
+ switch (this.#codecSupport.hostSystem) {
263
354
  case "macOS.Apple":
264
355
  case "macOS.Intel":
265
356
  // FFmpeg 8.x on macOS requires explicit download and format conversion when moving from VideoToolbox to software.
266
- if (this.config.codecSupport.ffmpegVersion.startsWith("8.")) {
357
+ if (this.#codecSupport.ffmpegAtLeast(8)) {
267
358
  filters.push("hwdownload", "format=nv12");
268
359
  }
269
360
  break;
@@ -285,18 +376,19 @@ export class FfmpegOptions {
285
376
  * When we're using hardware encoding without hardware decoding, we need to initialize the hardware device context explicitly. This method provides the
286
377
  * platform-specific initialization arguments required by FFmpeg.
287
378
  *
288
- * @param options - Video encoder options.
379
+ * @param options - The hardware-state view with the decoding and transcoding flags. Only those flags are read, so the helper accepts either a full
380
+ * `VideoEncoderOptions` or a purpose-built object exposing the same flags.
289
381
  * @returns Array of FFmpeg arguments for hardware device initialization.
290
382
  */
291
- getHardwareDeviceInit(options) {
383
+ #getHardwareDeviceInit(options) {
292
384
  // Only initialize hardware device if we're encoding with hardware but not decoding with it. When decoding with hardware, the device context is already initialized
293
385
  // by the decoder.
294
386
  if (!options.hardwareDecoding && options.hardwareTranscoding) {
295
- switch (this.codecSupport.hostSystem) {
387
+ switch (this.#codecSupport.hostSystem) {
296
388
  case "macOS.Apple":
297
389
  case "macOS.Intel":
298
390
  // Unfortunately, versions of FFmpeg prior to 8.0 don't properly support VideoToolbox use cases like this.
299
- if (!this.config.codecSupport.ffmpegVersion.startsWith("8.")) {
391
+ if (!this.#codecSupport.ffmpegAtLeast(8)) {
300
392
  break;
301
393
  }
302
394
  // Initialize VideoToolbox hardware context and assign it a name for use in filter chains.
@@ -330,61 +422,66 @@ export class FfmpegOptions {
330
422
  * ```
331
423
  */
332
424
  audioEncoder(options = {}) {
333
- // Default our codec to AAC_ELD unless specified.
334
- options = { codec: 1 /* AudioRecordingCodecType.AAC_ELD */, ...options };
335
- // If we don't have libfdk_aac available to us, we're essentially dead in the water.
336
- let encoderOptions = [];
337
- // Utility function to return a default audio encoder codec.
338
- const defaultAudioEncoderOptions = () => {
339
- const audioOptions = [];
340
- if (this.config.codecSupport.hasEncoder("aac", "libfdk_aac")) {
341
- // Default to libfdk_aac since FFmpeg doesn't natively support AAC-ELD. We use the following options by default:
342
- //
343
- // -codec:a libfdk_aac Use the libfdk_aac encoder.
344
- // -afterburner 1 Increases audio quality at the expense of needing a little bit more computational power in libfdk_aac.
345
- audioOptions.push("-codec:a", "libfdk_aac", "-afterburner", "1");
346
- switch (options.codec) {
347
- case 1 /* AudioRecordingCodecType.AAC_ELD */:
348
- break;
349
- case 0 /* AudioRecordingCodecType.AAC_LC */:
350
- default:
351
- audioOptions.push("-vbr", "4");
352
- break;
353
- }
354
- }
355
- return audioOptions;
356
- };
357
- switch (this.codecSupport.hostSystem) {
425
+ // Resolve the codec default here so every handler sees the same canonicalized shape. Matching the pattern used by `streamEncoder` and `videoDecoder`: the public
426
+ // method is pure normalization plus dispatch; each per-platform handler owns its own arg-emission story.
427
+ const resolved = { codec: AudioRecordingCodecType.AAC_ELD, ...options };
428
+ switch (this.#codecSupport.hostSystem) {
358
429
  case "macOS.Apple":
359
430
  case "macOS.Intel":
360
- // If we don't have audiotoolbox available, let's default back to libfdk_aac.
361
- if (!this.config.codecSupport.hasEncoder("aac", "aac_at")) {
362
- encoderOptions = defaultAudioEncoderOptions();
363
- break;
364
- }
365
- // aac_at is the macOS audio encoder API. We use the following options:
366
- //
367
- // -codec:a aac_at Use the aac_at encoder on macOS.
368
- // -aac_at_mode cvbr Use the constrained variable bitrate setting to allow the encoder to optimize audio within the requested bitrates.
369
- encoderOptions = [
370
- "-codec:a", "aac_at"
371
- ];
372
- switch (options.codec) {
373
- case 1 /* AudioRecordingCodecType.AAC_ELD */:
374
- encoderOptions.push("-aac_at_mode", "cbr");
375
- break;
376
- case 0 /* AudioRecordingCodecType.AAC_LC */:
377
- default:
378
- encoderOptions.push("-aac_at_mode", "vbr");
379
- encoderOptions.push("-q:a", "2");
380
- break;
381
- }
431
+ return this.#macOSAudioEncoderArgs(resolved);
432
+ default:
433
+ return this.#defaultAudioEncoderArgs(resolved);
434
+ }
435
+ }
436
+ /**
437
+ * Emit audio-encode args for macOS, preferring the AudioToolbox native encoder (`aac_at`) when advertised. Falls back to `defaultAudioEncoderArgs` when `aac_at` is
438
+ * unavailable, which typically means using libfdk_aac. Matches the per-platform handler pattern used throughout the class.
439
+ *
440
+ * @param options - Resolved audio encoder options with the codec field defaulted.
441
+ * @returns The macOS-appropriate audio-encoder args, or the default fallback when `aac_at` is missing.
442
+ */
443
+ #macOSAudioEncoderArgs(options) {
444
+ if (!this.#codecSupport.hasEncoder("aac", "aac_at")) {
445
+ return this.#defaultAudioEncoderArgs(options);
446
+ }
447
+ // aac_at is the macOS audio encoder API.
448
+ //
449
+ // -codec:a aac_at Use the aac_at encoder on macOS.
450
+ // -aac_at_mode cbr Constant-bitrate mode for AAC_ELD - HomeKit event recording is strict about rate, so a steady bitrate is what it wants.
451
+ // -aac_at_mode vbr + -q:a 2 Variable-bitrate mode for AAC_LC, letting the encoder optimize audio within the requested bitrate envelope.
452
+ const args = ["-codec:a", "aac_at"];
453
+ switch (options.codec) {
454
+ case AudioRecordingCodecType.AAC_ELD:
455
+ args.push("-aac_at_mode", "cbr");
382
456
  break;
457
+ case AudioRecordingCodecType.AAC_LC:
383
458
  default:
384
- encoderOptions = defaultAudioEncoderOptions();
459
+ args.push("-aac_at_mode", "vbr", "-q:a", "2");
385
460
  break;
386
461
  }
387
- return encoderOptions;
462
+ return args;
463
+ }
464
+ /**
465
+ * Emit audio-encode args for the default (non-macOS) path using `libfdk_aac`. Returns `[]` when libfdk_aac is not advertised, which is effectively fatal for audio -
466
+ * the caller will have no audio encoder to feed FFmpeg. This is the "essentially dead in the water" outcome we document at the call sites.
467
+ *
468
+ * @param options - Resolved audio encoder options with the codec field defaulted.
469
+ * @returns The libfdk_aac-based audio-encoder args, or `[]` when libfdk_aac is unavailable.
470
+ */
471
+ #defaultAudioEncoderArgs(options) {
472
+ if (!this.#codecSupport.hasEncoder("aac", "libfdk_aac")) {
473
+ return [];
474
+ }
475
+ // FFmpeg doesn't natively support AAC-ELD, so libfdk_aac is our cross-platform choice.
476
+ //
477
+ // -codec:a libfdk_aac Use the libfdk_aac encoder.
478
+ // -afterburner 1 Increases audio quality at the expense of a small CPU overhead in libfdk_aac.
479
+ // -vbr 4 Variable-bitrate mode 4, added only for AAC_LC. AAC_ELD stays at the library default (CBR).
480
+ const args = ["-codec:a", "libfdk_aac", "-afterburner", "1"];
481
+ if (options.codec !== AudioRecordingCodecType.AAC_ELD) {
482
+ args.push("-vbr", "4");
483
+ }
484
+ return args;
388
485
  }
389
486
  /**
390
487
  * Returns the audio decoder to use when decoding.
@@ -395,7 +492,8 @@ export class FfmpegOptions {
395
492
  /**
396
493
  * Returns the video decoder arguments to use for decoding video.
397
494
  *
398
- * @param codec - Optional. Codec to decode (`"av1"`, `"h264"` (default), or `"hevc"`).
495
+ * @param codec - Optional. Codec to decode (`"av1"`, `"h264"` (default), or `"hevc"`; `"h265"` is accepted as an
496
+ * alias for `"hevc"`, and codec matching is case-insensitive).
399
497
  * @returns Array of FFmpeg command-line arguments for video decoding or an empty array if the codec isn't supported.
400
498
  *
401
499
  * @example
@@ -405,66 +503,89 @@ export class FfmpegOptions {
405
503
  * ```
406
504
  */
407
505
  videoDecoder(codec = "h264") {
408
- switch (codec.toLowerCase()) {
409
- case "av1":
410
- codec = "av1";
411
- break;
412
- case "h264":
413
- codec = "h264";
414
- break;
415
- case "h265":
416
- case "hevc":
417
- codec = "hevc";
418
- break;
506
+ // Normalize the caller-supplied codec into our `SupportedDecodeCodec` surface via a single table-driven lookup. `canonicalDecodeCodec` handles both case folding
507
+ // and the `h265 -> hevc` alias; bogus codecs fall out as `undefined` and short-circuit before any hardware dispatch so typos can never leak into argv.
508
+ const normalized = canonicalDecodeCodec(codec);
509
+ if (!normalized) {
510
+ return [];
511
+ }
512
+ // Hardware decoding is an all-or-nothing gate at the plugin level. When the caller has opted out (or configureHwAccel disabled it on this host) we emit no decoder
513
+ // args and FFmpeg falls through to software decoding transparently.
514
+ if (!this.config.hardwareDecoding) {
515
+ return [];
516
+ }
517
+ // Dispatch to the platform handler. Each handler owns its platform's full hardware-decode story: capability gates (AV1 silicon-generation caveats), arg shape, and
518
+ // codec-specific mappings. The single switch here is the only place that knows "which handler for which platform."
519
+ switch (this.#codecSupport.hostSystem) {
520
+ case "macOS.Apple":
521
+ case "macOS.Intel":
522
+ return this.#macOSHardwareDecodeArgs(normalized);
523
+ case "raspbian":
524
+ return this.#raspbianHardwareDecodeArgs();
419
525
  default:
420
- // If it's unknown to us, we bail out.
421
- return [];
422
- }
423
- // Intel QSV decoder to codec mapping.
424
- const qsvDecoder = {
425
- "av1": "av1_qsv",
426
- "h264": "h264_qsv",
427
- "hevc": "hevc_qsv"
428
- };
429
- // Default to no special decoder options for inbound streams.
430
- let decoderOptions = [];
431
- // If we've enabled hardware-accelerated transcoding, let's select decoder options accordingly where supported.
432
- if (this.config.hardwareDecoding) {
433
- switch (this.codecSupport.hostSystem) {
434
- case "macOS.Apple":
435
- case "macOS.Intel":
436
- // h264_videotoolbox is the macOS hardware decoder and encoder API. We use the following options for decoding video:
437
- //
438
- // -hwaccel videotoolbox Select Video Toolbox for hardware-accelerated H.264 decoding.
439
- decoderOptions = [
440
- "-hwaccel", "videotoolbox",
441
- ...(this.config.codecSupport.ffmpegVersion.startsWith("8.") ? ["-hwaccel_output_format", "videotoolbox_vld"] : [])
442
- ];
443
- break;
444
- case "raspbian":
445
- // h264_v4l2m2m is the preferred Raspberry Pi hardware decoder codec. We use the following options for decoding video:
446
- //
447
- // -codec:v h264_v4l2m2m Select the h264_v4l2m2m codec for hardware-accelerated H.264 processing.
448
- decoderOptions = [
449
- // The decoder is broken in FFmpeg 7, unfortunately.
450
- // "-codec:v", "h264_v4l2m2m"
451
- ];
452
- break;
453
- default:
454
- // h264_qsv is the Intel Quick Sync Video hardware encoder and decoder.
455
- //
456
- // -hwaccel qsv Select Quick Sync Video to enable hardware-accelerated H.264 decoding.
457
- // -codec:v X_qsv Select the Quick Sync Video codec for hardware-accelerated AV1, H.264, or HEVC processing. AV1 decoding isn't available
458
- // before 11th generation Intel CPUs.
459
- decoderOptions = ((codec === "av1") && (this.codecSupport.cpuGeneration < 11)) ? [] : [
460
- "-hwaccel", "qsv",
461
- "-hwaccel_output_format", "qsv",
462
- "-codec:v", qsvDecoder[codec]
463
- ];
464
- break;
465
- }
526
+ return this.#qsvHardwareDecodeArgs(normalized);
527
+ }
528
+ }
529
+ /**
530
+ * Emit hardware-decode args for the macOS VideoToolbox path, applying the AV1 capability gate.
531
+ *
532
+ * AV1 hardware decode via VideoToolbox requires all three of:
533
+ *
534
+ * 1. **Apple Silicon M3 or newer** (`cpuGeneration >= 3`) - the first Apple silicon generation to ship AV1 hardware decode. Intel Macs never supported it.
535
+ * 2. **FFmpeg 8.0 or newer** - FFmpeg added the VideoToolbox AV1 decoder in the 8.0 release; earlier FFmpeg builds lack the decoder entirely.
536
+ * 3. macOS Sonoma 14.4 or newer at the OS level (not represented in our capability model; assumed when the above two hold and the user is running current software).
537
+ *
538
+ * When any of the three is missing, we return `[]` and FFmpeg falls back to software AV1 decode transparently. H.264 and HEVC paths are unaffected - they work on
539
+ * every supported macOS configuration.
540
+ *
541
+ * @param codec - The canonicalized codec, already narrowed by `videoDecoder` to one of the supported decode codecs.
542
+ * @returns The VideoToolbox decoder args, or `[]` when the host cannot hardware-decode the requested codec.
543
+ */
544
+ #macOSHardwareDecodeArgs(codec) {
545
+ if ((codec === "av1") && ((this.#codecSupport.hostSystem === "macOS.Intel") || (this.#codecSupport.cpuGeneration < 3) ||
546
+ (!this.#codecSupport.ffmpegAtLeast(8)))) {
547
+ return [];
548
+ }
549
+ // -hwaccel videotoolbox Select VideoToolbox for hardware-accelerated decoding on macOS.
550
+ // -hwaccel_output_format ... Explicit output format on FFmpeg 8.x; pre-8.x lets FFmpeg choose and the flag is omitted.
551
+ return [
552
+ "-hwaccel", "videotoolbox",
553
+ ...(this.#codecSupport.ffmpegAtLeast(8) ? ["-hwaccel_output_format", "videotoolbox_vld"] : [])
554
+ ];
555
+ }
556
+ /**
557
+ * Emit hardware-decode args for the Intel QSV path, applying the silicon-generation gate for AV1.
558
+ *
559
+ * AV1 hardware decode via Intel QSV requires 11th-generation silicon or newer (cpuGeneration >= 11). Older chips return `[]` and FFmpeg handles software decode
560
+ * transparently. H.264 and HEVC paths always emit QSV decoder args on any advertised generation.
561
+ *
562
+ * @param codec - The canonicalized codec, already narrowed by `videoDecoder` to one of the supported decode codecs.
563
+ * @returns The QSV decoder args, or `[]` when the silicon cannot hardware-decode the requested codec.
564
+ */
565
+ #qsvHardwareDecodeArgs(codec) {
566
+ if ((codec === "av1") && (this.#codecSupport.cpuGeneration < 11)) {
567
+ return [];
466
568
  }
467
- return decoderOptions;
569
+ // -hwaccel qsv Select Quick Sync Video for hardware-accelerated decoding.
570
+ // -hwaccel_output_format qsv Keep frames on the GPU across the filter chain to avoid a needless download+upload round trip.
571
+ // -codec:v <codec>_qsv Select the specific QSV decoder for the requested codec. The `SupportedDecodeCodec` narrowing makes this lookup total at the
572
+ // type level, so no runtime fallback is required.
573
+ return [
574
+ "-hwaccel", "qsv",
575
+ "-hwaccel_output_format", "qsv",
576
+ "-codec:v", QSV_DECODER_BY_CODEC[codec]
577
+ ];
578
+ }
579
+ /**
580
+ * Emit hardware-decode args for the Raspberry Pi V4L2 path.
581
+ *
582
+ * Currently returns `[]` unconditionally. The h264_v4l2m2m decoder is broken on FFmpeg 7+ and remains disabled pending an upstream fix; FFmpeg then falls back to
583
+ * software decode. When upstream resolves the decoder regression, this method is the single place to lift the block.
584
+ *
585
+ * @returns An empty array.
586
+ */
587
+ #raspbianHardwareDecodeArgs() {
588
+ return [];
468
589
  }
469
590
  /**
470
591
  * Returns the platform-appropriate FFmpeg video filters needed to transfer hardware-decoded frames to system memory. When hardware decoding is active, decoded frames
@@ -474,7 +595,7 @@ export class FfmpegOptions {
474
595
  * @returns An array of FFmpeg filter strings to prepend to a video filter chain, or an empty array if no transfer is needed.
475
596
  */
476
597
  get hardwareDownloadFilters() {
477
- return this.getHardwareTransferFilters({ hardwareDecoding: this.config.hardwareDecoding, hardwareTranscoding: false });
598
+ return this.#getHardwareTransferFilters({ hardwareDecoding: this.config.hardwareDecoding, hardwareTranscoding: false });
478
599
  }
479
600
  /**
480
601
  * Returns the FFmpeg crop filter string, or a default no-op filter if cropping is disabled.
@@ -494,27 +615,34 @@ export class FfmpegOptions {
494
615
  "y=ih*" + this.config.crop.y.toString()
495
616
  ].join(":");
496
617
  }
618
+ // Conditional splice of the crop filter into a larger filter chain: an array with the filter when cropping is enabled, an empty array otherwise. Both the software
619
+ // (`defaultVideoEncoderOptions`) and hardware (`hardwareStreamContext`) paths inline this splice - centralizing it here keeps "do we emit a crop filter" a one-line
620
+ // decision. Returning a readonly array so callers can only splice, not mutate.
621
+ get #cropFilterSegment() {
622
+ return this.config.crop ? [this.cropFilter] : [];
623
+ }
497
624
  /**
498
625
  * Generate the appropriate scale filter for the current platform. This method returns platform-specific scale filters to leverage hardware acceleration capabilities
499
626
  * where available.
500
627
  */
501
- getScaleFilter(options) {
628
+ #getScaleFilter(options) {
502
629
  // Determine the target dimensions for our scale operation. We maintain aspect ratio while ensuring the output doesn't exceed the requested height.
503
630
  const targetHeight = options.height.toString();
504
631
  const filters = [];
505
632
  // Our default software scaler.
506
633
  const swScale = "scale=-2:min(ih\\, " + targetHeight + ")" + ":in_range=auto:out_range=auto";
507
634
  // Add any required hardware transfer filters first. This ensures we're in the correct memory context before scaling.
508
- filters.push(...this.getHardwareTransferFilters(options));
635
+ filters.push(...this.#getHardwareTransferFilters(options));
509
636
  // Set our FFmpeg scale filter based on the platform and available hardware acceleration.
510
637
  //
511
638
  // scale=-2:min(ih\,height) Scale the video to the size that's being requested while respecting aspect ratios and ensuring our final dimensions are
512
- // a power of two. For macOS, we use the accelerated version, scale_vt. For Intel QSV, we use vpp_qsv.
513
- // format= Set the pixel formats we want to target for output, when needed.
514
- switch (this.codecSupport.hostSystem) {
639
+ // divisible by two. For macOS, we use the accelerated version, scale_vt. For Intel QSV, we use vpp_qsv.
640
+ // in_range/out_range=auto On the software path, let FFmpeg infer the input and output color ranges rather than forcing a conversion. The QSV path
641
+ // instead carries a format=same sub-parameter inside vpp_qsv to keep frames in their existing GPU pixel format.
642
+ switch (this.#codecSupport.hostSystem) {
515
643
  case "macOS.Apple":
516
644
  case "macOS.Intel":
517
- if (this.config.codecSupport.ffmpegVersion.startsWith("8.") && options.hardwareTranscoding) {
645
+ if (this.#codecSupport.ffmpegAtLeast(8) && options.hardwareTranscoding) {
518
646
  // On macOS with FFmpeg 8.x, we can use the VideoToolbox scaler (scale_vt) which provides hardware-accelerated scaling. This is significantly more efficient
519
647
  // than software scaling and can handle higher throughput with lower CPU usage. Prior to FFmpeg 8.0, this would break under a variety of scenarios and was
520
648
  // unreliable.
@@ -548,51 +676,30 @@ export class FfmpegOptions {
548
676
  }
549
677
  return filters;
550
678
  }
551
- /**
552
- * Generates the default set of FFmpeg video encoder arguments for software transcoding using libx264.
553
- *
554
- * This method builds command-line options for the FFmpeg libx264 encoder based on the provided encoder options, including bitrate, H.264 profile and level, pixel
555
- * format, frame rate, buffer size, and optional smart quality settings. It is used internally when hardware-accelerated transcoding is not enabled or supported.
556
- *
557
- * @param options - The encoder options to use for generating FFmpeg arguments.
558
- *
559
- * @returns An array of FFmpeg command-line arguments for software video encoding.
560
- *
561
- * @example
562
- *
563
- * ```ts
564
- * const encoderOptions: VideoEncoderOptions = {
565
- *
566
- * bitrate: 2000,
567
- * fps: 30,
568
- * height: 720,
569
- * idrInterval: 2,
570
- * inputFps: 30,
571
- * level: H264Level.LEVEL3_1,
572
- * profile: H264Profile.MAIN,
573
- * smartQuality: true,
574
- * width: 1280
575
- * };
576
- *
577
- * const args = ffmpegOpts['defaultVideoEncoderOptions'](encoderOptions);
578
- * ```
579
- *
580
- * @see VideoEncoderOptions
581
- */
582
- defaultVideoEncoderOptions(options) {
679
+ // Generates the default set of FFmpeg video encoder arguments for software transcoding using libx264. Builds command-line options based on the provided encoder
680
+ // options - bitrate, H.264 profile and level, pixel format, frame rate, buffer size, and optional smart quality settings. Used internally when hardware-accelerated
681
+ // transcoding is not enabled or supported; reachable from outside only through the public `streamEncoder` / `recordEncoder` dispatchers.
682
+ #defaultVideoEncoderOptions(options) {
583
683
  const videoFilters = [];
584
684
  // fps= Use the fps filter to provide the frame rate requested by HomeKit. We only need to apply this filter if our input and output
585
685
  // frame rates aren't already identical.
586
686
  const fpsFilter = ["fps=" + options.fps.toString()];
587
- // Build our pixel-level filters. We need to handle potential hardware downloads and format conversions.
588
- const pixelFilters = [];
589
- // Add any required hardware transfer filters. This handles downloading from GPU if we were hardware decoding.
590
- pixelFilters.push(...this.getHardwareTransferFilters(options));
591
- // Set our FFmpeg pixel-level filters:
687
+ // Build our pixel-level filter chain. The universal invariant across every encoder path in this class is that `crop` is a CPU-side filter and must operate on
688
+ // frames in system memory. On this software-encode path the only transfer that ever appears is a download (GPU->CPU, when hardware decoding is paired with
689
+ // software encoding), so crop sits *after* the transfer - immediately on the CPU side of the GPU boundary. The hardware-encode path in `streamEncoder` expresses
690
+ // the same invariant with the opposite ordering, since its transfer is always an upload (CPU->GPU) and crop must precede it.
592
691
  //
593
- // scale=-2:min(ih\,height) Scale the video to the size that's being requested while respecting aspect ratios and ensuring our final dimensions are
594
- // a power of two.
595
- pixelFilters.push("scale=-2:min(ih\\, " + options.height.toString() + "):in_range=auto:out_range=auto");
692
+ // hwdownload / format=nv12 Optional hardware transfer (emitted only when hardware decoding is paired with software encoding on FFmpeg 8.x macOS, or
693
+ // when downloading from a GPU accelerator on a generic QSV-like host). Brings GPU-resident frames into system memory so the
694
+ // downstream crop / scale filters can operate on them.
695
+ // crop=... Applied when `config.crop` is set. Uses relative iw/ih multipliers so the crop rectangle scales with source resolution;
696
+ // sized against the source frame and feeds the cropped region into the scaler so aspect-ratio math reflects the crop.
697
+ // scale=-2:min(ih\,height) Scale to HomeKit's requested dimensions while respecting aspect ratios and ensuring final dimensions divisible by two.
698
+ const pixelFilters = [
699
+ ...this.#getHardwareTransferFilters(options),
700
+ ...this.#cropFilterSegment,
701
+ "scale=-2:min(ih\\, " + options.height.toString() + "):in_range=auto:out_range=auto"
702
+ ];
596
703
  // Let's assemble our filter collection. If we're reducing our framerate, we want to frontload the fps filter so the downstream filters need to do less work. If we're
597
704
  // increasing our framerate, we want to do pixel operations on the minimal set of source frames that we need, since we're just going to duplicate them.
598
705
  if (options.fps < options.inputFps) {
@@ -610,7 +717,7 @@ export class FfmpegOptions {
610
717
  // -noautoscale Don't attempt to scale the video stream automatically.
611
718
  // -bf 0 Disable B-frames when encoding to increase compatibility against occasionally finicky HomeKit clients.
612
719
  // -filter:v Set the pixel format and scale the video to the size we want while respecting aspect ratios and ensuring our final
613
- // dimensions are a power of two.
720
+ // dimensions are divisible by two.
614
721
  // -g:v Set the group of pictures to the number of frames per second * the interval in between keyframes to ensure a solid
615
722
  // livestreaming experience.
616
723
  // -bufsize size This is the decoder buffer size, which drives the variability / quality of the output bitrate.
@@ -619,14 +726,14 @@ export class FfmpegOptions {
619
726
  const encoderOptions = [
620
727
  "-codec:v", "libx264",
621
728
  "-preset", "veryfast",
622
- "-profile:v", this.getH264Profile(options.profile),
623
- "-level:v", this.getH264Level(options.level),
729
+ "-profile:v", this.#getH264Profile(options.profile),
730
+ "-level:v", this.#getH264Level(options.level),
624
731
  "-noautoscale",
625
732
  "-bf", "0",
626
733
  "-filter:v", videoFilters.join(", "),
627
- "-g:v", (options.fps * options.idrInterval).toString(),
628
- "-bufsize", (2 * options.bitrate).toString() + "k",
629
- "-maxrate", (options.bitrate + (options.smartQuality ? HOMEKIT_STREAMING_HEADROOM : 0)).toString() + "k"
734
+ "-g:v", gopArg(options),
735
+ "-bufsize", bufsizeArg(options),
736
+ "-maxrate", maxrateArg(options)
630
737
  ];
631
738
  // Using libx264's constant rate factor mode produces generally better results across the board. We use a capped CRF approach, allowing libx264 to
632
739
  // make intelligent choices about how to adjust bitrate to achieve a certain quality level depending on the complexity of the scene being encoded, but
@@ -642,7 +749,7 @@ export class FfmpegOptions {
642
749
  // HKSV typically requests bitrates of around 2000kbps, which results in a reasonably high quality recording, as opposed to the typical 2-300kbps
643
750
  // that livestreaming from the Home app itself generates. Those lower bitrates in livestreaming really benefit from the magic that using a good CRF value
644
751
  // can produce in libx264.
645
- encoderOptions.push("-b:v", options.bitrate.toString() + "k");
752
+ encoderOptions.push("-b:v", bitrateArg(options));
646
753
  }
647
754
  return encoderOptions;
648
755
  }
@@ -653,21 +760,18 @@ export class FfmpegOptions {
653
760
  * @returns Array of FFmpeg command-line arguments for video encoding.
654
761
  */
655
762
  recordEncoder(options) {
656
- // We always disable smart quality when recording due to HomeKit's strict requirements here.
657
- options.smartQuality = false;
658
- // Generally, we default to using the same encoding options we use to transcode livestreams, unless we have platform-specific quirks we need to address,
659
- // such as where we can have hardware-accelerated transcoded livestreaming, but not hardware-accelerated HKSV event recording. The other noteworthy
660
- // aspect here is that HKSV is quite specific in what it wants, and isn't very tolerant of creative license in how you may choose to alter bitrate to
661
- // address quality. When we call our encoders, we also let them know we don't want any additional quality optimizations when transcoding HKSV events.
662
- switch (this.codecSupport.hostSystem) {
663
- case "raspbian":
664
- // Raspberry Pi struggles with hardware-accelerated HKSV event recording due to issues in the FFmpeg codec driver, currently. We hope this improves
665
- // over time and can offer it to Pi users, or develop a workaround. For now, we default to libx264.
666
- return this.defaultVideoEncoderOptions(options);
667
- default:
668
- // By default, we use the same options for HKSV and streaming.
669
- return this.streamEncoder(options);
763
+ // HKSV is strict about bitrates and format, so smart quality is always disabled for recordings. Every other concern - default merging, clamping hardware flags
764
+ // against the resolved class config, dispatch to the per-platform handler - is identical to the livestream path. The shape below reflects that: `recordEncoder`
765
+ // overrides smartQuality, then flows through the same machinery as `streamEncoder`. Raspberry Pi is the one platform-specific divergence - its hardware v4l2m2m
766
+ // encoder is unreliable for HKSV event recording, so we fall back to libx264 regardless of the resolved `hardwareTranscoding` flag. On every other platform the
767
+ // streaming dispatcher handles the rest.
768
+ const recordingInput = { ...options, smartQuality: false };
769
+ // Recording falls back to software wherever it can't use the hardware encoder (Raspberry Pi today). This is the same predicate maxSourcePixels("record") consults, so
770
+ // the encoder choice and the source ceiling are guaranteed consistent and evolve together when the upstream v4l2m2m regression is fixed.
771
+ if (!this.#hardwareEncodes("record")) {
772
+ return this.#defaultVideoEncoderOptions(this.#resolveEncoderOptions(recordingInput));
670
773
  }
774
+ return this.streamEncoder(recordingInput);
671
775
  }
672
776
  /**
673
777
  * Returns the video encoder options to use when transcoding for livestreaming.
@@ -682,215 +786,220 @@ export class FfmpegOptions {
682
786
  * ```
683
787
  */
684
788
  streamEncoder(options) {
685
- // Default hardware decoding and smart quality to true unless specified.
686
- options = { hardwareDecoding: true, hardwareTranscoding: this.config.hardwareTranscoding, smartQuality: true, ...options };
687
- // Disable hardware acceleration if we haven't detected it.
688
- if (!this.config.hardwareDecoding) {
689
- options.hardwareDecoding = false;
690
- }
691
- if (!this.config.hardwareTranscoding) {
692
- options.hardwareTranscoding = false;
693
- }
694
- // If we aren't hardware-accelerated, we default to libx264.
695
- if (!options.hardwareTranscoding) {
696
- return this.defaultVideoEncoderOptions(options);
789
+ const resolved = this.#resolveEncoderOptions(options);
790
+ // Software-only path. libx264 has its own filter-chain story (downloads-if-any, crop, scale) and is self-contained in defaultVideoEncoderOptions.
791
+ if (!resolved.hardwareTranscoding) {
792
+ return this.#defaultVideoEncoderOptions(resolved);
697
793
  }
698
- // If we've enabled hardware-accelerated transcoding, let's select encoder options accordingly.
699
- //
700
- // We begin by adjusting the maximum bitrate tolerance used with -bufsize. This provides an upper bound on bitrate, with a little bit extra to allow encoders some
701
- // variation in order to maximize quality while honoring bandwidth constraints.
702
- const adjustedMaxBitrate = options.bitrate + (options.smartQuality ? HOMEKIT_STREAMING_HEADROOM : 0);
703
- // Initialize our options. We'll add hardware device initialization first if needed.
704
- const encoderOptions = [...this.getHardwareDeviceInit(options)];
705
- const videoFilters = [];
706
- // Build our pixel filter chain. We conditionally include the crop filter if configured, then apply platform-specific scaling which handles any necessary hardware
707
- // transfers internally.
708
- //
709
- // crop Crop filter options, if requested.
710
- // scale=... Scale the video to the size that's being requested while respecting aspect ratios and ensuring our final dimensions are
711
- // a power of two. This also handles hardware transfers as needed.
712
- videoFilters.push(...(this.config.crop ? [this.cropFilter] : []), ...this.getScaleFilter(options));
713
- switch (this.codecSupport.hostSystem) {
794
+ // Hardware path. Pre-compute shared state once and dispatch to the per-platform handler. Each handler owns its platform's full encoder story: codec selection,
795
+ // profile encoding (string vs. numeric), level override, bitrate / quality-mode fork (-q:v / -b:v / -global_quality), and any platform-specific flags like
796
+ // -allow_sw / -realtime / -reset_timestamps. The dispatcher prepends the init-device args so that concern lives in one place.
797
+ const context = this.#hardwareStreamContext(resolved);
798
+ switch (this.#codecSupport.hostSystem) {
714
799
  case "macOS.Apple":
715
- // h264_videotoolbox is the macOS hardware encoder API. We use the following options on Apple Silicon:
716
- //
717
- // -codec:v Specify the macOS hardware encoder, h264_videotoolbox.
718
- // -allow_sw 1 Allow the use of the software encoder if the hardware encoder is occupied or unavailable.
719
- // This allows us to scale when we get multiple streaming requests simultaneously and consume all the available encode engines.
720
- // -realtime 1 We prefer speed over quality - if the encoder has to make a choice, sacrifice one for the other.
721
- // -profile:v Use the H.264 profile that HomeKit is requesting when encoding.
722
- // -level:v 0 We override what HomeKit requests for the H.264 profile level on macOS when we're using hardware-accelerated transcoding because
723
- // the hardware encoder is particular about how to use levels. Setting it to 0 allows the encoder to decide for itself.
724
- // -bf 0 Disable B-frames when encoding to increase compatibility against occasionally finicky HomeKit clients.
725
- // -noautoscale Don't attempt to scale the video stream automatically.
726
- // -filter:v Set the pixel format, adjust the frame rate if needed, and scale the video to the size we want while respecting aspect ratios and
727
- // ensuring our final dimensions are a power of two.
728
- // -g:v Set the group of pictures to the number of frames per second * the interval in between keyframes to ensure a solid
729
- // livestreaming experience.
730
- // -bufsize size This is the decoder buffer size, which drives the variability / quality of the output bitrate.
731
- // -maxrate bitrate The maximum bitrate tolerance used in concert with -bufsize to constrain the maximum bitrate permitted.
732
- // -r framerate Set the output framerate. We use this to bypass doing this in filters so we can maximize the use of our hardware pipeline.
733
- encoderOptions.push("-codec:v", "h264_videotoolbox", "-allow_sw", "1", "-realtime", "1", "-profile:v", this.getH264Profile(options.profile), "-level:v", "0", "-bf", "0", "-noautoscale", "-filter:v", videoFilters.join(", "), "-g:v", (options.fps * options.idrInterval).toString(), "-bufsize", (2 * options.bitrate).toString() + "k", "-maxrate", adjustedMaxBitrate.toString() + "k", ...((options.fps !== options.inputFps) ? ["-r", options.fps.toString()] : []));
734
- if (options.smartQuality) {
735
- // -q:v 90 Use a fixed quality scale of 90, to allow videotoolbox the ability to vary bitrates to achieve the visual quality we want,
736
- // constrained by our maximum bitrate. This is an Apple Silicon-specific feature.
737
- encoderOptions.push("-q:v", "90");
738
- }
739
- else {
740
- // -b:v Average bitrate that's being requested by HomeKit.
741
- encoderOptions.push("-b:v", options.bitrate.toString() + "k");
742
- }
743
- return encoderOptions;
800
+ return [...context.init, ...this.#macOSAppleStreamEncoderArgs(resolved, context)];
744
801
  case "macOS.Intel":
745
- // h264_videotoolbox is the macOS hardware encoder API. We use the following options on Intel-based Macs:
746
- //
747
- // -codec:v Specify the macOS hardware encoder, h264_videotoolbox.
748
- // -allow_sw 1 Allow the use of the software encoder if the hardware encoder is occupied or unavailable.
749
- // This allows us to scale when we get multiple streaming requests simultaneously that can consume all the available encode engines.
750
- // -realtime 1 We prefer speed over quality - if the encoder has to make a choice, sacrifice one for the other.
751
- // -profile:v Use the H.264 profile that HomeKit is requesting when encoding.
752
- // -level:v 0 We override what HomeKit requests for the H.264 profile level on macOS when we're using hardware-accelerated transcoding because
753
- // the hardware encoder is particular about how to use levels. Setting it to 0 allows the encoder to decide for itself.
754
- // -bf 0 Disable B-frames when encoding to increase compatibility against occasionally finicky HomeKit clients.
755
- // -noautoscale Don't attempt to scale the video stream automatically.
756
- // -filter:v Set the pixel format, adjust the frame rate if needed, and scale the video to the size we want while respecting aspect ratios and
757
- // ensuring our final dimensions are a power of two.
758
- // -b:v Average bitrate that's being requested by HomeKit. We can't use a quality constraint and allow for more optimization of the
759
- // bitrate on Intel-based Macs due to hardware / API limitations.
760
- // -g:v Set the group of pictures to the number of frames per second * the interval in between keyframes to ensure a solid
761
- // livestreaming experience.
762
- // -bufsize size This is the decoder buffer size, which drives the variability / quality of the output bitrate.
763
- // -maxrate bitrate The maximum bitrate tolerance used in concert with -bufsize to constrain the maximum bitrate permitted.
764
- // -r framerate Set the output framerate. We use this to bypass doing this in filters so we can maximize the use of our hardware pipeline.
765
- encoderOptions.push("-codec:v", "h264_videotoolbox", "-allow_sw", "1", "-realtime", "1", "-profile:v", this.getH264Profile(options.profile), "-level:v", "0", "-bf", "0", "-noautoscale", "-filter:v", videoFilters.join(", "), "-b:v", options.bitrate.toString() + "k", "-g:v", (options.fps * options.idrInterval).toString(), "-bufsize", (2 * options.bitrate).toString() + "k", "-maxrate", adjustedMaxBitrate.toString() + "k", ...((options.fps !== options.inputFps) ? ["-r", options.fps.toString()] : []));
766
- return encoderOptions;
802
+ return [...context.init, ...this.#macOSIntelStreamEncoderArgs(resolved, context)];
767
803
  case "raspbian":
768
- // h264_v4l2m2m is the preferred interface to the Raspberry Pi hardware encoder API. We use the following options:
769
- //
770
- // -codec:v Specify the Raspberry Pi hardware encoder, h264_v4l2m2m.
771
- // -noautoscale Don't attempt to scale the video stream automatically.
772
- // -filter:v Set the pixel format, adjust the frame rate if needed, and scale the video to the size we want while respecting aspect ratios and
773
- // ensuring our final dimensions are a power of two.
774
- // -b:v Average bitrate that's being requested by HomeKit. We can't use a quality constraint and allow for more optimization of the
775
- // bitrate due to v4l2m2m limitations.
776
- // -g:v Set the group of pictures to the number of frames per second * the interval in between keyframes to ensure a solid
777
- // livestreaming experience.
778
- // -bufsize size This is the decoder buffer size, which drives the variability / quality of the output bitrate.
779
- // -maxrate bitrate The maximum bitrate tolerance used in concert with -bufsize to constrain the maximum bitrate permitted.
780
- // -r framerate Set the output framerate. We use this to bypass doing this in filters so we can maximize the use of our hardware pipeline.
781
- encoderOptions.push("-codec:v", "h264_v4l2m2m", "-profile:v", this.getH264Profile(options.profile, true), "-bf", "0", "-noautoscale", "-reset_timestamps", "1", "-filter:v", videoFilters.join(", "), "-b:v", options.bitrate.toString() + "k", "-g:v", (options.fps * options.idrInterval).toString(), "-bufsize", (2 * options.bitrate).toString() + "k", "-maxrate", adjustedMaxBitrate.toString() + "k", ...((options.fps !== options.inputFps) ? ["-r", options.fps.toString()] : []));
782
- return encoderOptions;
804
+ return [...context.init, ...this.#raspbianStreamEncoderArgs(resolved, context)];
783
805
  default:
784
- // h264_qsv is the Intel Quick Sync Video hardware encoder API. We use the following options:
785
- //
786
- // -codec:v Specify the Intel Quick Sync Video hardware encoder, h264_qsv.
787
- // -profile:v Use the H.264 profile that HomeKit is requesting when encoding.
788
- // -level:v 0 We override what HomeKit requests for the H.264 profile level when we're using hardware-accelerated transcoding because
789
- // the hardware encoder will determine which levels to use. Setting it to 0 allows the encoder to decide for itself.
790
- // -bf 0 Disable B-frames when encoding to increase compatibility against occasionally finicky HomeKit clients.
791
- // -noautoscale Don't attempt to scale the video stream automatically.
792
- // -filter:v Set the pixel format, adjust the frame rate if needed, and scale the video to the size we want while respecting aspect ratios and
793
- // ensuring our final dimensions are a power of two.
794
- // -g:v Set the group of pictures to the number of frames per second * the interval in between keyframes to ensure a solid
795
- // livestreaming experience.
796
- // -bufsize size This is the decoder buffer size, which drives the variability / quality of the output bitrate.
797
- // -maxrate bitrate The maximum bitrate tolerance used in concert with -bufsize to constrain the maximum bitrate permitted.
798
- // -r framerate Set the output framerate. We use this to bypass doing this in filters so we can maximize the use of our hardware pipeline.
799
- encoderOptions.push("-codec:v", "h264_qsv", "-profile:v", this.getH264Profile(options.profile), "-level:v", "0", "-bf", "0", "-noautoscale", "-filter:v", videoFilters.join(", "), "-g:v", (options.fps * options.idrInterval).toString(), "-bufsize", (2 * options.bitrate).toString() + "k", "-maxrate", adjustedMaxBitrate.toString() + "k", ...((options.fps !== options.inputFps) ? ["-r", options.fps.toString()] : []));
800
- if (options.smartQuality) {
801
- // -global_quality 20 Use a global quality setting of 20, to allow QSV the ability to vary bitrates to achieve the visual quality we want,
802
- // constrained by our maximum bitrate. This leverages a QSV-specific feature known as intelligent constant quality.
803
- encoderOptions.push("-global_quality", "20");
804
- }
805
- else {
806
- // -b:v Average bitrate that's being requested by HomeKit.
807
- encoderOptions.push("-b:v", options.bitrate.toString() + "k");
808
- }
809
- return encoderOptions;
810
- }
811
- }
812
- /**
813
- * Returns the maximum pixel count supported by a specific hardware encoder on the host system, or `Infinity` if not limited.
814
- *
815
- * @returns Maximum supported pixel count.
816
- */
817
- get hostSystemMaxPixels() {
818
- if (this.config.hardwareTranscoding) {
819
- switch (this.codecSupport.hostSystem) {
820
- case "raspbian":
821
- // For constrained environments like Raspberry Pi, when hardware transcoding has been selected for a camera, we limit the available source streams to no more
822
- // than 1080p. In practice, that means that devices like the G4 Pro can't use their highest quality stream for transcoding due to the limitations of the
823
- // Raspberry Pi GPU that cannot support higher pixel counts.
824
- return 1920 * 1080;
825
- default:
826
- break;
827
- }
806
+ return [...context.init, ...this.#qsvStreamEncoderArgs(resolved, context)];
828
807
  }
829
- return Infinity;
830
808
  }
831
809
  /**
832
- * Converts a HomeKit H.264 level enum value to the corresponding FFmpeg string or numeric representation.
833
- *
834
- * This helper is used to translate between HomeKit's `H264Level` enum and the string or numeric format expected by FFmpeg's `-level:v` argument.
810
+ * Merge the caller's encoder options with our defaults and clamp the hardware flags against the resolved class config. This is the single source of truth for "what
811
+ * hardware state does this call run in" - every public encoder method flows its input through here before dispatching, so no downstream handler ever sees unresolved
812
+ * or unclamped values. The helper is idempotent under repeat application: a resolved options object passed back through this helper produces an identical result,
813
+ * which makes safe the `recordEncoder` -> `streamEncoder` delegation chain.
835
814
  *
836
- * @param level - The H.264 level to translate.
837
- * @param numeric - Optional. If `true`, returns the numeric representation (e.g., "31"). If `false` or omitted, returns the standard string format (e.g., "3.1").
815
+ * Each resolver-guaranteed field is computed with one formula:
838
816
  *
839
- * @returns The FFmpeg-compatible H.264 level string or numeric value.
840
- *
841
- * @example
817
+ * `resolved = (caller ?? classDefault) && clamp`
842
818
  *
843
- * ```ts
844
- * ffmpegOpts['getH264Level'](H264Level.LEVEL3_1); // "3.1"
819
+ * `??` coalesces undefined (whether from omission or explicit-undefined spread) to the class default; `&&` then clamps against the resolved class capability so a
820
+ * caller can only ever downgrade a flag, never upgrade one. `smartQuality` has no class-level counterpart and no clamp - it defaults to `true` when the caller omits
821
+ * it and passes through otherwise. The result satisfies `Required<Pick<...>>` at the type level *and* at runtime because the formula produces a concrete boolean on
822
+ * every branch; there is no code path that can leave a resolver-guaranteed field undefined.
845
823
  *
846
- * ffmpegOpts['getH264Level'](H264Level.LEVEL4_0, true); // "40"
847
- * ```
824
+ * @param options - The caller-supplied encoder options.
825
+ * @returns A resolved options object with defaults filled in and hardware flags clamped against the resolved class config. The three resolver-guaranteed fields
826
+ * (`hardwareDecoding`, `hardwareTranscoding`, `smartQuality`) are narrowed to `Required` in the return type so downstream handlers see definite booleans.
827
+ */
828
+ #resolveEncoderOptions(options) {
829
+ return {
830
+ ...options,
831
+ hardwareDecoding: (options.hardwareDecoding ?? this.config.hardwareDecoding) && this.config.hardwareDecoding,
832
+ hardwareTranscoding: (options.hardwareTranscoding ?? this.config.hardwareTranscoding) && this.config.hardwareTranscoding,
833
+ smartQuality: options.smartQuality ?? true
834
+ };
835
+ }
836
+ /**
837
+ * Pre-compute the shared state every hardware-stream encoder path consumes. Having this in a dedicated helper keeps each per-platform handler a pure function of
838
+ * `(options, context)`, with the shared computation centralized in exactly one place rather than duplicated inline at the start of every branch.
839
+ *
840
+ * - `bufsize`, `gop`, `maxrate` are the pre-formatted rate-control strings every hardware handler splices directly into its `-bufsize` / `-g:v` / `-maxrate` args.
841
+ * Sourced from the module-scope `bufsizeArg` / `gopArg` / `maxrateArg` helpers so the software and hardware paths share one derivation of each.
842
+ * - `filterChain` is the comma-joined `-filter:v` value. Crop sits first when configured (CPU-side filter), then the platform-specific scaler (which may prepend its
843
+ * own transfer filters; see `getScaleFilter`).
844
+ * - `frameRateArg` materializes the `...(fps !== inputFps ? ["-r", fps] : [])` conditional once so every handler can splat it directly.
845
+ * - `init` is the platform-specific hardware-device init args (`-init_hw_device` / `-filter_hw_device`, emitted only for SW-decode + HW-encode on FFmpeg 8.x macOS
846
+ * and on generic QSV hosts). Prepended by the dispatcher before the handler's output.
847
+ */
848
+ #hardwareStreamContext(options) {
849
+ const videoFilters = [
850
+ ...this.#cropFilterSegment,
851
+ ...this.#getScaleFilter(options)
852
+ ];
853
+ return {
854
+ bufsize: bufsizeArg(options),
855
+ filterChain: videoFilters.join(", "),
856
+ frameRateArg: (options.fps !== options.inputFps) ? ["-r", options.fps.toString()] : [],
857
+ gop: gopArg(options),
858
+ init: this.#getHardwareDeviceInit(options),
859
+ maxrate: maxrateArg(options)
860
+ };
861
+ }
862
+ /**
863
+ * Emit hardware-encode args for macOS Apple Silicon using `h264_videotoolbox`.
848
864
  *
849
- * @see H264Level
865
+ * Apple Silicon supports a quality-constraint mode via `-q:v`, which lets VideoToolbox vary bitrate to achieve a target visual quality bounded by `-maxrate`. When
866
+ * smart quality is off, falls back to a fixed average bitrate via `-b:v`. `-level:v 0` lets the hardware encoder decide the level itself (the API is particular
867
+ * about explicit level values).
850
868
  */
851
- getH264Level(level, numeric = false) {
852
- switch (level) {
853
- case 0 /* H264Level.LEVEL3_1 */:
854
- return numeric ? "31" : "3.1";
855
- case 1 /* H264Level.LEVEL3_2 */:
856
- return numeric ? "32" : "3.2";
857
- case 2 /* H264Level.LEVEL4_0 */:
858
- return numeric ? "40" : "4.0";
859
- default:
860
- return numeric ? "31" : "3.1";
869
+ #macOSAppleStreamEncoderArgs(options, context) {
870
+ const args = [
871
+ "-codec:v", "h264_videotoolbox",
872
+ "-allow_sw", "1",
873
+ "-realtime", "1",
874
+ "-profile:v", this.#getH264Profile(options.profile),
875
+ "-level:v", "0",
876
+ "-bf", "0",
877
+ "-noautoscale",
878
+ "-filter:v", context.filterChain,
879
+ "-g:v", context.gop,
880
+ "-bufsize", context.bufsize,
881
+ "-maxrate", context.maxrate,
882
+ ...context.frameRateArg
883
+ ];
884
+ if (options.smartQuality) {
885
+ // -q:v 90 lets VideoToolbox vary bitrate to achieve target visual quality, capped by -maxrate. Apple Silicon-specific; pairs with the hardware encoder's ICQ mode.
886
+ args.push("-q:v", "90");
861
887
  }
888
+ else {
889
+ args.push("-b:v", bitrateArg(options));
890
+ }
891
+ return args;
862
892
  }
863
893
  /**
864
- * Converts a HomeKit H.264 profile enum value to the corresponding FFmpeg string or numeric representation.
865
- *
866
- * This helper is used to translate between HomeKit's `H264Profile` enum and the string or numeric format expected by FFmpeg's `-profile:v` argument.
867
- *
868
- * @param profile - The H.264 profile to translate.
869
- * @param numeric - Optional. If `true`, returns the numeric representation (e.g., "100"). If `false` or omitted, returns the standard string format (e.g., "high").
894
+ * Emit hardware-encode args for Intel-based Macs using `h264_videotoolbox`.
870
895
  *
871
- * @returns The FFmpeg-compatible H.264 profile string or numeric value.
896
+ * The Intel VideoToolbox encoder lacks a quality-constraint mode - hardware and API limitations only support a fixed average bitrate via `-b:v`, regardless of the
897
+ * caller's smart-quality preference. smartQuality still influences `-maxrate` via `context.maxrate` (headroom added on top), but no `-q:v` emission exists on this
898
+ * path. The arg shape otherwise matches the Apple Silicon handler.
899
+ */
900
+ #macOSIntelStreamEncoderArgs(options, context) {
901
+ return [
902
+ "-codec:v", "h264_videotoolbox",
903
+ "-allow_sw", "1",
904
+ "-realtime", "1",
905
+ "-profile:v", this.#getH264Profile(options.profile),
906
+ "-level:v", "0",
907
+ "-bf", "0",
908
+ "-noautoscale",
909
+ "-filter:v", context.filterChain,
910
+ "-b:v", bitrateArg(options),
911
+ "-g:v", context.gop,
912
+ "-bufsize", context.bufsize,
913
+ "-maxrate", context.maxrate,
914
+ ...context.frameRateArg
915
+ ];
916
+ }
917
+ /**
918
+ * Emit hardware-encode args for Raspberry Pi using `h264_v4l2m2m`.
872
919
  *
873
- * @example
920
+ * v4l2m2m wants the numeric H.264 profile encoding (66 / 77 / 100), not the canonical string form - see `getH264Profile`'s `numeric` argument. Like Intel Mac, the
921
+ * v4l2m2m encoder has no quality-constraint mode; `-b:v` is always emitted. `-reset_timestamps 1` is required for the Pi encoder to produce usable output. No
922
+ * `-level:v` is emitted - v4l2m2m manages levels internally.
923
+ */
924
+ #raspbianStreamEncoderArgs(options, context) {
925
+ return [
926
+ "-codec:v", "h264_v4l2m2m",
927
+ "-profile:v", this.#getH264Profile(options.profile, true),
928
+ "-bf", "0",
929
+ "-noautoscale",
930
+ "-reset_timestamps", "1",
931
+ "-filter:v", context.filterChain,
932
+ "-b:v", bitrateArg(options),
933
+ "-g:v", context.gop,
934
+ "-bufsize", context.bufsize,
935
+ "-maxrate", context.maxrate,
936
+ ...context.frameRateArg
937
+ ];
938
+ }
939
+ /**
940
+ * Emit hardware-encode args for Intel QSV using `h264_qsv`.
874
941
  *
875
- * ```ts
876
- * ffmpegOpts['getH264Profile'](H264Profile.HIGH); // "high"
942
+ * QSV supports its own quality-constraint mode via `-global_quality` (intelligent constant quality / ICQ), analogous to VideoToolbox's `-q:v` on Apple Silicon. When
943
+ * smart quality is off, falls back to `-b:v`. `-level:v 0` lets the QSV encoder pick its own level - the hardware encoder handles this better than honoring an
944
+ * explicit HomeKit-requested level.
945
+ */
946
+ #qsvStreamEncoderArgs(options, context) {
947
+ const args = [
948
+ "-codec:v", "h264_qsv",
949
+ "-profile:v", this.#getH264Profile(options.profile),
950
+ "-level:v", "0",
951
+ "-bf", "0",
952
+ "-noautoscale",
953
+ "-filter:v", context.filterChain,
954
+ "-g:v", context.gop,
955
+ "-bufsize", context.bufsize,
956
+ "-maxrate", context.maxrate,
957
+ ...context.frameRateArg
958
+ ];
959
+ if (options.smartQuality) {
960
+ // -global_quality 20 is QSV's intelligent-constant-quality mode, analogous to -q:v 90 on Apple VT but scaled differently.
961
+ args.push("-global_quality", "20");
962
+ }
963
+ else {
964
+ args.push("-b:v", bitrateArg(options));
965
+ }
966
+ return args;
967
+ }
968
+ // The single source of truth for "does this transcode context run on the hardware encoder on THIS host, in the resolved class config?". Live streaming uses the
969
+ // hardware encoder whenever transcoding is enabled; HKSV recording additionally excludes Raspberry Pi, whose h264_v4l2m2m encoder is unreliable for event recording
970
+ // for reasons separate from the FFmpeg-7+ h264_v4l2m2m decoder regression noted in #configureRaspbianHwAccel, which affects decoding only. Both recordEncoder and
971
+ // maxSourcePixels consult this, so the encoder choice and the source ceiling can never disagree - if the encoder is ever validated as reliable for event recording,
972
+ // relaxing the raspbian exclusion here flips both together with no consumer change. Reads the resolved class config (set in the constructor by #configureHwAccel,
973
+ // before any encoder method is callable), matching the class-level decision recordEncoder has always made.
974
+ #hardwareEncodes(context) {
975
+ if (!this.config.hardwareTranscoding) {
976
+ return false;
977
+ }
978
+ return (context === "stream") || (this.#codecSupport.hostSystem !== "raspbian");
979
+ }
980
+ /**
981
+ * Returns the maximum source pixel count the host's hardware transcode pipeline can ingest for the given encoding context, or `Infinity` when unconstrained.
877
982
  *
878
- * ffmpegOpts['getH264Profile'](H264Profile.BASELINE, true); // "66"
879
- * ```
983
+ * Only Raspberry Pi's GPU imposes a real limit; every other host is unconstrained. A context is capped only when it actually runs on that hardware path - so today live
984
+ * streaming on a Pi returns the RPi ceiling while recording on a Pi returns `Infinity` (it software-encodes). Consumers apply this value blindly; the "why" lives here.
880
985
  *
881
- * @see H264Profile
986
+ * @param context - The encoding context whose ceiling is requested.
987
+ * @returns Maximum supported source pixel count for `context`.
882
988
  */
883
- getH264Profile(profile, numeric = false) {
884
- switch (profile) {
885
- case 0 /* H264Profile.BASELINE */:
886
- return numeric ? "66" : "baseline";
887
- case 2 /* H264Profile.HIGH */:
888
- return numeric ? "100" : "high";
889
- case 1 /* H264Profile.MAIN */:
890
- return numeric ? "77" : "main";
891
- default:
892
- return numeric ? "77" : "main";
893
- }
989
+ maxSourcePixels(context) {
990
+ return (this.#hardwareEncodes(context) && (this.#codecSupport.hostSystem === "raspbian")) ? RPI4_HW_TRANSCODE_MAX_PIXELS : Infinity;
991
+ }
992
+ // Translates HomeKit's `H264Level` enum into the string or numeric form FFmpeg's `-level:v` accepts. `numeric=true` returns the v4l2m2m form (e.g. "31"); the default
993
+ // returns the canonical string form (e.g. "3.1"). Indexed lookup against `H264_LEVEL_NAMES`, declared `as const satisfies Record<H264Level, ...>` so a new enum member
994
+ // upstream is a compile-time failure here.
995
+ #getH264Level(level, numeric = false) {
996
+ return H264_LEVEL_NAMES[level][numeric ? "numeric" : "string"];
997
+ }
998
+ // Translates HomeKit's `H264Profile` enum into the string or numeric form FFmpeg's `-profile:v` accepts. `numeric=true` returns the v4l2m2m form (e.g. "100"); the
999
+ // default returns the canonical string form (e.g. "high"). Indexed lookup against `H264_PROFILE_NAMES`, declared `as const satisfies Record<H264Profile, ...>` so a
1000
+ // new enum member upstream is a compile-time failure here.
1001
+ #getH264Profile(profile, numeric = false) {
1002
+ return H264_PROFILE_NAMES[profile][numeric ? "numeric" : "string"];
894
1003
  }
895
1004
  }
896
1005
  //# sourceMappingURL=options.js.map