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.
- package/README.md +130 -2
- package/build/eslint-plugin/README.md +164 -0
- package/build/eslint-plugin/config.mjs +308 -0
- package/build/eslint-plugin/index.mjs +7 -0
- package/build/eslint-plugin/plugin.mjs +46 -0
- package/build/eslint-plugin/rules/blank-line-after-open-brace.mjs +137 -0
- package/build/eslint-plugin/rules/blank-line-after-open-brace.test.mjs +112 -0
- package/build/eslint-plugin/rules/comment-style.mjs +190 -0
- package/build/eslint-plugin/rules/comment-style.test.mjs +190 -0
- package/build/eslint-plugin/rules/enforce-node-protocol.mjs +114 -0
- package/build/eslint-plugin/rules/enforce-node-protocol.test.mjs +116 -0
- package/build/eslint-plugin/rules/paren-comparisons-in-logical.mjs +94 -0
- package/build/eslint-plugin/rules/paren-comparisons-in-logical.test.mjs +107 -0
- package/build/eslint-plugin/rules/split-type-imports.mjs +354 -0
- package/build/eslint-plugin/rules/split-type-imports.test.mjs +268 -0
- package/build/eslint-plugin/test-setup.mjs +21 -0
- package/build/fs-ops.mjs +184 -0
- package/build/tsconfig.json +18 -3
- package/dist/backpressure.d.ts +94 -45
- package/dist/backpressure.js +229 -93
- package/dist/backpressure.js.map +1 -1
- package/dist/cli/index.d.ts +95 -0
- package/dist/cli/index.js +355 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/clock-double.d.ts +93 -0
- package/dist/clock-double.js +141 -0
- package/dist/clock-double.js.map +1 -0
- package/dist/clock.d.ts +39 -0
- package/dist/clock.js +34 -0
- package/dist/clock.js.map +1 -0
- package/dist/eslint-plugin/config.d.mts +193 -0
- package/dist/eslint-plugin/index.d.mts +2 -0
- package/dist/eslint-plugin/plugin.d.mts +87 -0
- package/dist/eslint-plugin/rules/blank-line-after-open-brace.d.mts +19 -0
- package/dist/eslint-plugin/rules/comment-style.d.mts +15 -0
- package/dist/eslint-plugin/rules/enforce-node-protocol.d.mts +18 -0
- package/dist/eslint-plugin/rules/paren-comparisons-in-logical.d.mts +15 -0
- package/dist/eslint-plugin/rules/split-type-imports.d.mts +16 -0
- package/dist/featureOptions-docs.d.ts +96 -0
- package/dist/featureOptions-docs.js +193 -0
- package/dist/featureOptions-docs.js.map +1 -0
- package/dist/featureOptions.d.ts +674 -0
- package/dist/featureOptions.js +870 -0
- package/dist/featureOptions.js.map +1 -0
- package/dist/ffmpeg/codecs.d.ts +256 -72
- package/dist/ffmpeg/codecs.js +477 -262
- package/dist/ffmpeg/codecs.js.map +1 -1
- package/dist/ffmpeg/dgram-util.d.ts +46 -0
- package/dist/ffmpeg/dgram-util.js +38 -0
- package/dist/ffmpeg/dgram-util.js.map +1 -0
- package/dist/ffmpeg/exec.d.ts +83 -64
- package/dist/ffmpeg/exec.js +77 -86
- package/dist/ffmpeg/exec.js.map +1 -1
- package/dist/ffmpeg/fmp4.d.ts +49 -2
- package/dist/ffmpeg/fmp4.js +47 -11
- package/dist/ffmpeg/fmp4.js.map +1 -1
- package/dist/ffmpeg/hap-enums.d.ts +214 -0
- package/dist/ffmpeg/hap-enums.js +92 -0
- package/dist/ffmpeg/hap-enums.js.map +1 -0
- package/dist/ffmpeg/index.d.ts +16 -9
- package/dist/ffmpeg/index.js +6 -0
- package/dist/ffmpeg/index.js.map +1 -1
- package/dist/ffmpeg/mp4-assembler.d.ts +120 -0
- package/dist/ffmpeg/mp4-assembler.js +387 -0
- package/dist/ffmpeg/mp4-assembler.js.map +1 -0
- package/dist/ffmpeg/mp4-parser.d.ts +94 -0
- package/dist/ffmpeg/mp4-parser.js +130 -0
- package/dist/ffmpeg/mp4-parser.js.map +1 -0
- package/dist/ffmpeg/options.d.ts +62 -149
- package/dist/ffmpeg/options.js +608 -499
- package/dist/ffmpeg/options.js.map +1 -1
- package/dist/ffmpeg/process.d.ts +142 -96
- package/dist/ffmpeg/process.js +406 -278
- package/dist/ffmpeg/process.js.map +1 -1
- package/dist/ffmpeg/record.d.ts +302 -188
- package/dist/ffmpeg/record.js +408 -567
- package/dist/ffmpeg/record.js.map +1 -1
- package/dist/ffmpeg/recording-process-double.d.ts +144 -0
- package/dist/ffmpeg/recording-process-double.js +168 -0
- package/dist/ffmpeg/recording-process-double.js.map +1 -0
- package/dist/ffmpeg/rtp-parser.d.ts +70 -0
- package/dist/ffmpeg/rtp-parser.js +77 -0
- package/dist/ffmpeg/rtp-parser.js.map +1 -0
- package/dist/ffmpeg/rtp.d.ts +198 -141
- package/dist/ffmpeg/rtp.js +473 -251
- package/dist/ffmpeg/rtp.js.map +1 -1
- package/dist/ffmpeg/settings.d.ts +5 -2
- package/dist/ffmpeg/settings.js +20 -5
- package/dist/ffmpeg/settings.js.map +1 -1
- package/dist/ffmpeg/stream.d.ts +57 -107
- package/dist/ffmpeg/stream.js +121 -150
- package/dist/ffmpeg/stream.js.map +1 -1
- package/dist/formatters.d.ts +106 -0
- package/dist/formatters.js +174 -0
- package/dist/formatters.js.map +1 -0
- package/dist/homebridge-enums.d.ts +30 -0
- package/dist/homebridge-enums.js +17 -0
- package/dist/homebridge-enums.js.map +1 -0
- package/dist/index.d.ts +12 -6
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/logclient/auth.d.ts +114 -0
- package/dist/logclient/auth.js +199 -0
- package/dist/logclient/auth.js.map +1 -0
- package/dist/logclient/cli-run.d.ts +76 -0
- package/dist/logclient/cli-run.js +639 -0
- package/dist/logclient/cli-run.js.map +1 -0
- package/dist/logclient/cli.d.ts +3 -0
- package/dist/logclient/cli.js +97 -0
- package/dist/logclient/cli.js.map +1 -0
- package/dist/logclient/client.d.ts +145 -0
- package/dist/logclient/client.js +600 -0
- package/dist/logclient/client.js.map +1 -0
- package/dist/logclient/config.d.ts +173 -0
- package/dist/logclient/config.js +199 -0
- package/dist/logclient/config.js.map +1 -0
- package/dist/logclient/endpoints.d.ts +54 -0
- package/dist/logclient/endpoints.js +73 -0
- package/dist/logclient/endpoints.js.map +1 -0
- package/dist/logclient/filter.d.ts +45 -0
- package/dist/logclient/filter.js +51 -0
- package/dist/logclient/filter.js.map +1 -0
- package/dist/logclient/frame.d.ts +93 -0
- package/dist/logclient/frame.js +203 -0
- package/dist/logclient/frame.js.map +1 -0
- package/dist/logclient/index.d.ts +31 -0
- package/dist/logclient/index.js +12 -0
- package/dist/logclient/index.js.map +1 -0
- package/dist/logclient/parser.d.ts +211 -0
- package/dist/logclient/parser.js +393 -0
- package/dist/logclient/parser.js.map +1 -0
- package/dist/logclient/rest.d.ts +41 -0
- package/dist/logclient/rest.js +111 -0
- package/dist/logclient/rest.js.map +1 -0
- package/dist/logclient/settings.d.ts +15 -0
- package/dist/logclient/settings.js +64 -0
- package/dist/logclient/settings.js.map +1 -0
- package/dist/logclient/socket-double.d.ts +201 -0
- package/dist/logclient/socket-double.js +384 -0
- package/dist/logclient/socket-double.js.map +1 -0
- package/dist/logclient/socket.d.ts +257 -0
- package/dist/logclient/socket.js +620 -0
- package/dist/logclient/socket.js.map +1 -0
- package/dist/logclient/stitch.d.ts +83 -0
- package/dist/logclient/stitch.js +146 -0
- package/dist/logclient/stitch.js.map +1 -0
- package/dist/logclient/time-expression.d.ts +42 -0
- package/dist/logclient/time-expression.js +181 -0
- package/dist/logclient/time-expression.js.map +1 -0
- package/dist/logclient/time-window.d.ts +38 -0
- package/dist/logclient/time-window.js +53 -0
- package/dist/logclient/time-window.js.map +1 -0
- package/dist/logclient/types.d.ts +107 -0
- package/dist/logclient/types.js +6 -0
- package/dist/logclient/types.js.map +1 -0
- package/dist/mqttClient.d.ts +287 -0
- package/dist/mqttClient.js +433 -0
- package/dist/mqttClient.js.map +1 -0
- package/dist/service.d.ts +64 -15
- package/dist/service.js +93 -66
- package/dist/service.js.map +1 -1
- package/dist/ui/featureOptions.js +870 -0
- package/dist/ui/featureOptions.js.map +1 -0
- package/dist/ui/formatters.js +174 -0
- package/dist/ui/formatters.js.map +1 -0
- package/dist/ui/pluginConfigSession.mjs +141 -0
- package/dist/ui/webUi-featureOptions/categoryState.mjs +135 -0
- package/dist/ui/webUi-featureOptions/effects/keyboard.mjs +61 -0
- package/dist/ui/webUi-featureOptions/effects/persist.mjs +226 -0
- package/dist/ui/webUi-featureOptions/effects/theme.mjs +398 -0
- package/dist/ui/webUi-featureOptions/effects/tokens.mjs +152 -0
- package/dist/ui/webUi-featureOptions/rendering.mjs +431 -0
- package/dist/ui/webUi-featureOptions/selectors.mjs +360 -0
- package/dist/ui/webUi-featureOptions/state.mjs +319 -0
- package/dist/ui/webUi-featureOptions/store.mjs +181 -0
- package/dist/ui/webUi-featureOptions/utils.mjs +200 -0
- package/dist/ui/webUi-featureOptions/views/connectionError.mjs +152 -0
- package/dist/ui/webUi-featureOptions/views/deviceInfo.mjs +80 -0
- package/dist/ui/webUi-featureOptions/views/header.mjs +77 -0
- package/dist/ui/webUi-featureOptions/views/nav.mjs +327 -0
- package/dist/ui/webUi-featureOptions/views/options.mjs +521 -0
- package/dist/ui/webUi-featureOptions/views/search.mjs +395 -0
- package/dist/ui/webUi-featureOptions.mjs +677 -0
- package/dist/ui/webUi.mjs +192 -87
- package/dist/util.d.ts +602 -45
- package/dist/util.js +783 -78
- package/dist/util.js.map +1 -1
- package/package.json +33 -15
- package/build/eslint-rules.mjs +0 -511
- package/dist/featureoptions.d.ts +0 -264
- package/dist/featureoptions.js +0 -480
- package/dist/featureoptions.js.map +0 -1
- package/dist/mqttclient.d.ts +0 -178
- package/dist/mqttclient.js +0 -310
- package/dist/mqttclient.js.map +0 -1
- package/dist/ui/featureoptions.js +0 -480
- package/dist/ui/featureoptions.js.map +0 -1
- package/dist/ui/webUi-featureoptions.mjs +0 -3765
package/dist/ffmpeg/options.js
CHANGED
|
@@ -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,
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
//
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
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 -
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 -
|
|
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
|
|
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
|
|
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
|
-
//
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
459
|
+
args.push("-aac_at_mode", "vbr", "-q:a", "2");
|
|
385
460
|
break;
|
|
386
461
|
}
|
|
387
|
-
return
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
513
|
-
//
|
|
514
|
-
|
|
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
|
|
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
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
|
588
|
-
|
|
589
|
-
//
|
|
590
|
-
|
|
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
|
-
//
|
|
594
|
-
// a
|
|
595
|
-
|
|
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
|
|
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
|
|
623
|
-
"-level:v", this
|
|
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
|
|
628
|
-
"-bufsize", (
|
|
629
|
-
"-maxrate", (options
|
|
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
|
|
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
|
-
//
|
|
657
|
-
|
|
658
|
-
//
|
|
659
|
-
//
|
|
660
|
-
//
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
-
//
|
|
699
|
-
//
|
|
700
|
-
//
|
|
701
|
-
|
|
702
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
833
|
-
*
|
|
834
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
840
|
-
*
|
|
841
|
-
* @example
|
|
817
|
+
* `resolved = (caller ?? classDefault) && clamp`
|
|
842
818
|
*
|
|
843
|
-
*
|
|
844
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
876
|
-
*
|
|
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
|
-
*
|
|
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
|
-
* @
|
|
986
|
+
* @param context - The encoding context whose ceiling is requested.
|
|
987
|
+
* @returns Maximum supported source pixel count for `context`.
|
|
882
988
|
*/
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
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
|