homebridge-plugin-utils 1.34.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 +128 -0
- package/dist/backpressure.js +273 -0
- package/dist/backpressure.js.map +1 -0
- 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 -257
- 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 +70 -4
- package/dist/ffmpeg/fmp4.js +100 -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 +143 -91
- package/dist/ffmpeg/process.js +406 -265
- package/dist/ffmpeg/process.js.map +1 -1
- package/dist/ffmpeg/record.d.ts +302 -188
- package/dist/ffmpeg/record.js +408 -560
- 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 -244
- package/dist/ffmpeg/rtp.js.map +1 -1
- package/dist/ffmpeg/settings.d.ts +5 -1
- package/dist/ffmpeg/settings.js +20 -2
- package/dist/ffmpeg/settings.js.map +1 -1
- package/dist/ffmpeg/stream.d.ts +57 -107
- package/dist/ffmpeg/stream.js +121 -148
- 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 -5
- package/dist/index.js +8 -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 +597 -45
- package/dist/util.js +787 -68
- 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/codecs.js
CHANGED
|
@@ -23,32 +23,190 @@
|
|
|
23
23
|
* @module
|
|
24
24
|
*/
|
|
25
25
|
import { EOL, cpus } from "node:os";
|
|
26
|
-
import { execFile } from "node:child_process";
|
|
27
26
|
import { env, platform } from "node:process";
|
|
27
|
+
import { composeSignals } from "../util.js";
|
|
28
|
+
import { execFile } from "node:child_process";
|
|
28
29
|
import { promisify } from "node:util";
|
|
29
30
|
import { readFileSync } from "node:fs";
|
|
30
31
|
// Promisified execFile, created once at module level rather than per-invocation.
|
|
31
32
|
const execFileAsync = promisify(execFile);
|
|
33
|
+
// Default watchdog timeout, in milliseconds, applied to every probe invocation so a slow or hung FFmpeg binary cannot stall the host plugin indefinitely. Thirty
|
|
34
|
+
// seconds comfortably exceeds the realistic worst-case probe (the hardware-acceleration validation on slow Raspberry Pi hosts) while keeping a crashed binary from
|
|
35
|
+
// parking a plugin forever.
|
|
36
|
+
const PROBE_DEFAULT_TIMEOUT_MS = 30_000;
|
|
37
|
+
// Codec-line regexes for `parseFfmpegCodecs`. Compiled once at module scope to match the convention used for `VALID_HOMEKIT_NAME`, `NON_PRINTABLE_CHARS`, and the other
|
|
38
|
+
// hot-path patterns in this package - regex construction per call is wasted work, even on the cold probe path, when one regex serves every line of every probe.
|
|
39
|
+
const FFMPEG_CODECS_DECODERS_REGEX = /\S+\s+(\S+).+\(decoders: (.*?)\s*\)/;
|
|
40
|
+
const FFMPEG_CODECS_ENCODERS_REGEX = /\S+\s+(\S+).+\(encoders: (.*?)\s*\)/;
|
|
32
41
|
/**
|
|
33
|
-
*
|
|
42
|
+
* Parse the stdout of `ffmpeg -version` and return the version string. Returns `"unknown"` when the expected `"ffmpeg version X Copyright..."` line is not found -
|
|
43
|
+
* matching the behavior of the class-level probe, which records the literal `"unknown"` when it cannot identify the binary.
|
|
44
|
+
*
|
|
45
|
+
* Exposed as a module-scope helper (rather than remaining a closure inside the class) so the parse logic is unit-testable against fixture strings without spinning up
|
|
46
|
+
* the probe plumbing.
|
|
47
|
+
*
|
|
48
|
+
* @param stdout - Captured stdout from `ffmpeg -hide_banner -version`.
|
|
49
|
+
*
|
|
50
|
+
* @returns The version string, or `"unknown"` if the version line is absent.
|
|
51
|
+
*
|
|
52
|
+
* @category FFmpeg
|
|
53
|
+
*/
|
|
54
|
+
export function parseFfmpegVersion(stdout) {
|
|
55
|
+
const versionRegex = /^ffmpeg version (.*) Copyright.*$/m;
|
|
56
|
+
const versionMatch = versionRegex.exec(stdout);
|
|
57
|
+
return versionMatch ? (versionMatch[1] ?? "unknown") : "unknown";
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Parse an FFmpeg version string into its numeric triple. Splits on `.` and `-` so real-world version strings parse correctly across release tarballs ("8.1.1"),
|
|
61
|
+
* distributor suffixes ("8.1.1-tessus"), distro packages ("4.4.2-0ubuntu0.22.04.1"), and git snapshots ("N-123456-gabcdef"). Any non-numeric segment yields `0` via the
|
|
62
|
+
* `|| 0` fallback, which gives a safe, conservative result: an unknown build parses as `{ major: 0, minor: 0, patch: 0 }` and fails every {@link ffmpegVersionAtLeast}
|
|
63
|
+
* check where the requested major is >= 1.
|
|
64
|
+
*
|
|
65
|
+
* @param version - An FFmpeg version string as produced by {@link parseFfmpegVersion} or by `FfmpegCodecs.ffmpegVersion`.
|
|
34
66
|
*
|
|
35
|
-
*
|
|
36
|
-
* GPU memory (on Raspberry Pi). Intended for plugin authors or advanced users needing to assess FFmpeg capabilities dynamically.
|
|
67
|
+
* @returns The parsed numeric triple.
|
|
37
68
|
*
|
|
38
69
|
* @example
|
|
39
70
|
*
|
|
40
71
|
* ```ts
|
|
41
|
-
*
|
|
72
|
+
* parseFfmpegVersionParts("8.1.1"); // { major: 8, minor: 1, patch: 1 }
|
|
73
|
+
* parseFfmpegVersionParts("8.1.1-tessus"); // { major: 8, minor: 1, patch: 1 }
|
|
74
|
+
* parseFfmpegVersionParts("N-123456-gabcdef"); // { major: 0, minor: 123456, patch: 0 } - git snapshot, effectively "unknown"
|
|
75
|
+
* parseFfmpegVersionParts("unknown"); // { major: 0, minor: 0, patch: 0 }
|
|
76
|
+
* ```
|
|
77
|
+
*
|
|
78
|
+
* @category FFmpeg
|
|
79
|
+
*/
|
|
80
|
+
export function parseFfmpegVersionParts(version) {
|
|
81
|
+
const parts = version.split(/[.-]/);
|
|
82
|
+
return {
|
|
83
|
+
major: Number.parseInt(parts[0] ?? "", 10) || 0,
|
|
84
|
+
minor: Number.parseInt(parts[1] ?? "", 10) || 0,
|
|
85
|
+
patch: Number.parseInt(parts[2] ?? "", 10) || 0
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Return `true` when `parts` represents an FFmpeg version at least as new as the requested major/minor/patch. Compares major, then minor, then patch - the canonical
|
|
90
|
+
* semver ordering. This is the single source of truth for version-gating comparisons across the library; both the `FfmpegCodecs.ffmpegAtLeast` instance method and the
|
|
91
|
+
* test-side fixtures delegate here so the boundary logic lives in one implementation.
|
|
92
|
+
*
|
|
93
|
+
* @param parts - A parsed version triple as produced by {@link parseFfmpegVersionParts}.
|
|
94
|
+
* @param major - The minimum major version to accept.
|
|
95
|
+
* @param minor - The minimum minor version, when major is equal. Defaults to `0`.
|
|
96
|
+
* @param patch - The minimum patch version, when major and minor are equal. Defaults to `0`.
|
|
97
|
+
*
|
|
98
|
+
* @returns `true` if `parts` is >= the requested version; `false` otherwise.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
*
|
|
102
|
+
* ```ts
|
|
103
|
+
* const parts = parseFfmpegVersionParts("8.1.2");
|
|
104
|
+
*
|
|
105
|
+
* ffmpegVersionAtLeast(parts, 8); // true - 8.1.2 >= 8.0.0
|
|
106
|
+
* ffmpegVersionAtLeast(parts, 8, 1, 3); // false - 8.1.2 < 8.1.3
|
|
107
|
+
* ffmpegVersionAtLeast(parts, 9); // false - 8.1.2 < 9.0.0
|
|
108
|
+
* ```
|
|
109
|
+
*
|
|
110
|
+
* @category FFmpeg
|
|
111
|
+
*/
|
|
112
|
+
export function ffmpegVersionAtLeast(parts, major, minor = 0, patch = 0) {
|
|
113
|
+
if (parts.major !== major) {
|
|
114
|
+
return parts.major > major;
|
|
115
|
+
}
|
|
116
|
+
if (parts.minor !== minor) {
|
|
117
|
+
return parts.minor > minor;
|
|
118
|
+
}
|
|
119
|
+
return parts.patch >= patch;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Parse the stdout of `ffmpeg -hwaccels` and return the list of hardware-acceleration method names in lowercase, in the order they appeared. Skips blank lines and
|
|
123
|
+
* the leading `"Hardware acceleration methods:"` banner.
|
|
124
|
+
*
|
|
125
|
+
* @param stdout - Captured stdout from `ffmpeg -hide_banner -hwaccels`.
|
|
126
|
+
*
|
|
127
|
+
* @returns Lowercased acceleration method names, one per entry.
|
|
128
|
+
*
|
|
129
|
+
* @category FFmpeg
|
|
130
|
+
*/
|
|
131
|
+
export function parseFfmpegHwAccels(stdout) {
|
|
132
|
+
const result = [];
|
|
133
|
+
for (const line of stdout.split(EOL)) {
|
|
134
|
+
if (!line.length) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (line === "Hardware acceleration methods:") {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
result.push(line.toLowerCase());
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Parse the stdout of `ffmpeg -codecs` into a format-keyed index of decoders and encoders. Both sets are lowercased for case-insensitive `hasDecoder` /
|
|
146
|
+
* `hasEncoder` lookups. Formats with only decoders or only encoders still produce a full entry (the opposite set is simply empty).
|
|
147
|
+
*
|
|
148
|
+
* @param stdout - Captured stdout from `ffmpeg -hide_banner -codecs`.
|
|
149
|
+
*
|
|
150
|
+
* @returns A record keyed by codec format, each entry carrying the decoders and encoders reported for that format.
|
|
42
151
|
*
|
|
43
|
-
*
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
152
|
+
* @category FFmpeg
|
|
153
|
+
*/
|
|
154
|
+
export function parseFfmpegCodecs(stdout) {
|
|
155
|
+
const result = {};
|
|
156
|
+
// Capture each codec's decoder and encoder lists separately because FFmpeg reports them in independent parenthesized segments on the same line. The regexes are
|
|
157
|
+
// hoisted to module scope above so they are compiled once rather than once per call.
|
|
158
|
+
for (const codecLine of stdout.split(EOL)) {
|
|
159
|
+
const decodersMatch = FFMPEG_CODECS_DECODERS_REGEX.exec(codecLine);
|
|
160
|
+
const encodersMatch = FFMPEG_CODECS_ENCODERS_REGEX.exec(codecLine);
|
|
161
|
+
if (decodersMatch) {
|
|
162
|
+
const [, format, decoders] = decodersMatch;
|
|
163
|
+
if ((format !== undefined) && (decoders !== undefined)) {
|
|
164
|
+
const entry = (result[format] ??= { decoders: new Set(), encoders: new Set() });
|
|
165
|
+
for (const decoder of decoders.split(" ")) {
|
|
166
|
+
entry.decoders.add(decoder.toLowerCase());
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (encodersMatch) {
|
|
171
|
+
const [, format, encoders] = encodersMatch;
|
|
172
|
+
if ((format !== undefined) && (encoders !== undefined)) {
|
|
173
|
+
const entry = (result[format] ??= { decoders: new Set(), encoders: new Set() });
|
|
174
|
+
for (const encoder of encoders.split(" ")) {
|
|
175
|
+
entry.encoders.add(encoder.toLowerCase());
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Parse the stdout of `vcgencmd get_mem gpu` into a megabyte value. Returns `0` when the expected `gpu=<N>M` shape is absent or the captured digits fail integer
|
|
184
|
+
* parsing - matching the class-level fallback.
|
|
185
|
+
*
|
|
186
|
+
* @param stdout - Captured stdout from `vcgencmd get_mem gpu`.
|
|
47
187
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
188
|
+
* @returns The reported GPU memory size in megabytes, or `0` when the value could not be read.
|
|
189
|
+
*
|
|
190
|
+
* @category FFmpeg
|
|
191
|
+
*/
|
|
192
|
+
export function parseRpiGpuMem(stdout) {
|
|
193
|
+
const gpuRegex = /^gpu=(.*)M\n$/;
|
|
194
|
+
const gpuMatch = gpuRegex.exec(stdout);
|
|
195
|
+
return Number.parseInt(gpuMatch?.[1] ?? "", 10) || 0;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Probe FFmpeg capabilities and codecs on the host system.
|
|
199
|
+
*
|
|
200
|
+
* Construct via the static factory {@link FfmpegCodecs.probe} to run the live probe pipeline, or via {@link FfmpegCodecs.fromState} to inject pre-assembled state
|
|
201
|
+
* (tests, cached capability data). Instances are immutable value objects - every getter and predicate reads from a frozen {@link FfmpegCodecsState} snapshot
|
|
202
|
+
* assembled at construction, so callers holding a reference know its state cannot shift underneath them.
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
*
|
|
206
|
+
* ```ts
|
|
207
|
+
* const codecs = await FfmpegCodecs.probe({ ffmpegExec: "ffmpeg", log: console, verbose: true });
|
|
50
208
|
*
|
|
51
|
-
* if(
|
|
209
|
+
* if(codecs) {
|
|
52
210
|
*
|
|
53
211
|
* console.log("Available FFmpeg version:", codecs.ffmpegVersion);
|
|
54
212
|
*
|
|
@@ -62,86 +220,159 @@ const execFileAsync = promisify(execFile);
|
|
|
62
220
|
* @category FFmpeg
|
|
63
221
|
*/
|
|
64
222
|
export class FfmpegCodecs {
|
|
223
|
+
#state;
|
|
224
|
+
// Private constructor - the only paths in are the static factories below. This makes "a live `FfmpegCodecs` has complete state" a type-level fact rather than a
|
|
225
|
+
// convention callers must remember to honor by running a separate async init step after construction.
|
|
226
|
+
constructor(state) {
|
|
227
|
+
this.#state = state;
|
|
228
|
+
}
|
|
65
229
|
/**
|
|
66
|
-
*
|
|
230
|
+
* Async factory. Runs the full probe pipeline (host-system detection, optional Raspberry Pi GPU memory, FFmpeg version + codec inventory + hardware-accel inventory
|
|
231
|
+
* with per-accel capability validation) and returns a populated instance on success, or `null` when any required probe fails. Every inner probe runs under a
|
|
232
|
+
* watchdog timeout so a slow or hung FFmpeg binary cannot stall the caller indefinitely; the optional `init.signal` composes with that timeout so callers can cancel
|
|
233
|
+
* probing from outside (for example, during plugin shutdown).
|
|
234
|
+
*
|
|
235
|
+
* @param options - Options used to configure the probe (FFmpeg executable, logger, verbose flag).
|
|
236
|
+
* @param init - Optional probe options. `signal` cancels in-flight probes; the per-call watchdog still applies.
|
|
237
|
+
*
|
|
238
|
+
* @returns A promise that resolves to a populated `FfmpegCodecs` instance, or `null` if probing failed.
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
*
|
|
242
|
+
* ```ts
|
|
243
|
+
* const codecs = await FfmpegCodecs.probe({ log: plugin.log }, { signal: shutdown.signal });
|
|
244
|
+
*
|
|
245
|
+
* if(!codecs) {
|
|
246
|
+
*
|
|
247
|
+
* plugin.log.error("FFmpeg probing failed.");
|
|
248
|
+
* }
|
|
249
|
+
* ```
|
|
67
250
|
*/
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
_cpuGeneration;
|
|
73
|
-
log;
|
|
74
|
-
ffmpegCodecs;
|
|
75
|
-
ffmpegHwAccels;
|
|
251
|
+
static async probe(options, init = {}) {
|
|
252
|
+
const state = await probeFfmpegCapabilities(options, init.signal);
|
|
253
|
+
return state ? new FfmpegCodecs(state) : null;
|
|
254
|
+
}
|
|
76
255
|
/**
|
|
77
|
-
*
|
|
256
|
+
* Sync factory. Wraps a pre-assembled {@link FfmpegCodecsState} in a `FfmpegCodecs` instance without running any probes. Intended for tests that build a stand-in
|
|
257
|
+
* capability snapshot and for plugins that cache probe results across restarts and want to rehydrate the class without re-probing.
|
|
258
|
+
*
|
|
259
|
+
* @param state - Fully-assembled capability snapshot.
|
|
260
|
+
*
|
|
261
|
+
* @returns A populated `FfmpegCodecs` instance backed by the supplied state.
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
*
|
|
265
|
+
* ```ts
|
|
266
|
+
* const codecs = FfmpegCodecs.fromState({
|
|
267
|
+
*
|
|
268
|
+
* codecs: {},
|
|
269
|
+
* cpuGeneration: 0,
|
|
270
|
+
* ffmpegExec: "ffmpeg",
|
|
271
|
+
* ffmpegVersion: "8.0",
|
|
272
|
+
* ffmpegVersionParts: parseFfmpegVersionParts("8.0"),
|
|
273
|
+
* gpuMem: 0,
|
|
274
|
+
* hostSystem: "macOS.Apple",
|
|
275
|
+
* hwAccels: new Set([ "videotoolbox" ]),
|
|
276
|
+
* verbose: false
|
|
277
|
+
* });
|
|
278
|
+
* ```
|
|
78
279
|
*/
|
|
79
|
-
|
|
280
|
+
static fromState(state) {
|
|
281
|
+
return new FfmpegCodecs(state);
|
|
282
|
+
}
|
|
80
283
|
/**
|
|
81
|
-
*
|
|
284
|
+
* The path or command name used to invoke FFmpeg.
|
|
285
|
+
*/
|
|
286
|
+
get ffmpegExec() {
|
|
287
|
+
return this.#state.ffmpegExec;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Indicates whether verbose logging is enabled for FFmpeg probing and downstream consumers.
|
|
291
|
+
*/
|
|
292
|
+
get verbose() {
|
|
293
|
+
return this.#state.verbose;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Returns the amount of GPU memory available on the host system, in megabytes.
|
|
82
297
|
*
|
|
83
|
-
* @
|
|
298
|
+
* @remarks Always returns `0` on non-Raspberry Pi systems.
|
|
299
|
+
*/
|
|
300
|
+
get gpuMem() {
|
|
301
|
+
return this.#state.gpuMem;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Returns the detected FFmpeg version string. `"unknown"` when the probe ran but the `ffmpeg version X Copyright...` line was not found in stdout. Any other value
|
|
305
|
+
* is the literal version string reported by the binary and may carry suffixes (`"8.1.1-tessus"`, `"4.4.2-0ubuntu0.22.04.1"`, etc.). For version-comparison decisions,
|
|
306
|
+
* prefer {@link ffmpegAtLeast} over parsing this string directly.
|
|
84
307
|
*/
|
|
85
|
-
|
|
86
|
-
this.
|
|
87
|
-
this.ffmpegCodecs = {};
|
|
88
|
-
this.ffmpegHwAccels = new Set();
|
|
89
|
-
this.log = options.log;
|
|
90
|
-
this.verbose = options.verbose ?? false;
|
|
91
|
-
// Detect our host system type.
|
|
92
|
-
this.probeHwOs();
|
|
308
|
+
get ffmpegVersion() {
|
|
309
|
+
return this.#state.ffmpegVersion;
|
|
93
310
|
}
|
|
94
311
|
/**
|
|
95
|
-
*
|
|
312
|
+
* Returns the detected FFmpeg major version as a number, or `0` when detection failed or the version string doesn't begin with an integer. Useful for display
|
|
313
|
+
* ("Running FFmpeg 8") and for callers that need the raw major number. Use {@link ffmpegAtLeast} for version-gating comparisons so all version logic flows through
|
|
314
|
+
* a single boundary-correct comparison primitive.
|
|
96
315
|
*
|
|
97
|
-
*
|
|
316
|
+
* @returns The major version number (e.g., `6`, `7`, `8`, `10`), or `0` if the version is unknown or non-numeric.
|
|
317
|
+
*/
|
|
318
|
+
get ffmpegMajorVersion() {
|
|
319
|
+
return this.#state.ffmpegVersionParts.major;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Return `true` when the detected FFmpeg build is at least the requested version. Compares major, then minor, then patch - the canonical semver ordering. This is
|
|
323
|
+
* the single source of truth for version-gating decisions across the library; callers prefer this over hand-rolled comparisons so the boundary logic (major-equal
|
|
324
|
+
* but minor-greater means "newer", etc.) lives in one place and stays consistent.
|
|
98
325
|
*
|
|
99
|
-
* @
|
|
326
|
+
* @param major - The minimum major version to accept.
|
|
327
|
+
* @param minor - The minimum minor version, when major is equal. Defaults to `0`.
|
|
328
|
+
* @param patch - The minimum patch version, when major and minor are equal. Defaults to `0`.
|
|
329
|
+
*
|
|
330
|
+
* @returns `true` if the detected version is >= the requested version; `false` otherwise. Returns `false` for unknown or unparseable version strings (major = 0).
|
|
100
331
|
*
|
|
101
332
|
* @example
|
|
102
333
|
*
|
|
103
334
|
* ```ts
|
|
335
|
+
* if(codecs.ffmpegAtLeast(8)) {
|
|
104
336
|
*
|
|
105
|
-
*
|
|
337
|
+
* // FFmpeg 8.0.0 or later.
|
|
338
|
+
* }
|
|
106
339
|
*
|
|
107
|
-
* if(
|
|
340
|
+
* if(codecs.ffmpegAtLeast(8, 1)) {
|
|
108
341
|
*
|
|
109
|
-
*
|
|
342
|
+
* // FFmpeg 8.1.0 or later.
|
|
110
343
|
* }
|
|
111
344
|
* ```
|
|
112
345
|
*/
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return true;
|
|
346
|
+
ffmpegAtLeast(major, minor = 0, patch = 0) {
|
|
347
|
+
return ffmpegVersionAtLeast(this.#state.ffmpegVersionParts, major, minor, patch);
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Returns the host system type we are running on as one of `"generic"`, `"macOS.Apple"`, `"macOS.Intel"`, or `"raspbian"`.
|
|
351
|
+
*
|
|
352
|
+
* @remarks We are only trying to detect host capabilities to the extent they impact which FFmpeg options we are going to use.
|
|
353
|
+
*/
|
|
354
|
+
get hostSystem() {
|
|
355
|
+
return this.#state.hostSystem;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Returns the CPU generation if we're on Linux and have an Intel processor or on macOS and have an Apple Silicon processor.
|
|
359
|
+
*
|
|
360
|
+
* @returns Returns the CPU generation or 0 if it can't be detected or an invalid platform.
|
|
361
|
+
*/
|
|
362
|
+
get cpuGeneration() {
|
|
363
|
+
return this.#state.cpuGeneration;
|
|
132
364
|
}
|
|
133
365
|
/**
|
|
134
366
|
* Checks whether a specific decoder is available for a given codec.
|
|
135
367
|
*
|
|
136
|
-
* @param codec
|
|
137
|
-
* @param decoder - The decoder name to check for, e.g., "h264_qsv"
|
|
368
|
+
* @param codec - The codec name, e.g., `"h264"`.
|
|
369
|
+
* @param decoder - The decoder name to check for, e.g., `"h264_qsv"`.
|
|
138
370
|
*
|
|
139
371
|
* @returns `true` if the decoder is available for the codec, `false` otherwise.
|
|
140
372
|
*
|
|
141
373
|
* @example
|
|
142
374
|
*
|
|
143
375
|
* ```ts
|
|
144
|
-
*
|
|
145
376
|
* if(codecs.hasDecoder("h264", "h264_qsv")) {
|
|
146
377
|
*
|
|
147
378
|
* // Use hardware decoding.
|
|
@@ -149,24 +380,19 @@ export class FfmpegCodecs {
|
|
|
149
380
|
* ```
|
|
150
381
|
*/
|
|
151
382
|
hasDecoder(codec, decoder) {
|
|
152
|
-
|
|
153
|
-
codec = codec.toLowerCase();
|
|
154
|
-
decoder = decoder.toLowerCase();
|
|
155
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
156
|
-
return this.ffmpegCodecs[codec]?.decoders.has(decoder) ?? false;
|
|
383
|
+
return this.#state.codecs[codec.toLowerCase()]?.decoders.has(decoder.toLowerCase()) ?? false;
|
|
157
384
|
}
|
|
158
385
|
/**
|
|
159
386
|
* Checks whether a specific encoder is available for a given codec.
|
|
160
387
|
*
|
|
161
|
-
* @param codec
|
|
162
|
-
* @param encoder - The encoder name to check for, e.g., "h264_videotoolbox"
|
|
388
|
+
* @param codec - The codec name, e.g., `"h264"`.
|
|
389
|
+
* @param encoder - The encoder name to check for, e.g., `"h264_videotoolbox"`.
|
|
163
390
|
*
|
|
164
391
|
* @returns `true` if the encoder is available for the codec, `false` otherwise.
|
|
165
392
|
*
|
|
166
393
|
* @example
|
|
167
394
|
*
|
|
168
395
|
* ```ts
|
|
169
|
-
*
|
|
170
396
|
* if(codecs.hasEncoder("h264", "h264_videotoolbox")) {
|
|
171
397
|
*
|
|
172
398
|
* // Use hardware encoding.
|
|
@@ -174,16 +400,13 @@ export class FfmpegCodecs {
|
|
|
174
400
|
* ```
|
|
175
401
|
*/
|
|
176
402
|
hasEncoder(codec, encoder) {
|
|
177
|
-
|
|
178
|
-
codec = codec.toLowerCase();
|
|
179
|
-
encoder = encoder.toLowerCase();
|
|
180
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
181
|
-
return this.ffmpegCodecs[codec]?.encoders.has(encoder) ?? false;
|
|
403
|
+
return this.#state.codecs[codec.toLowerCase()]?.encoders.has(encoder.toLowerCase()) ?? false;
|
|
182
404
|
}
|
|
183
405
|
/**
|
|
184
|
-
* Checks whether a given hardware acceleration method is available and validated on the host, as provided by the output of `ffmpeg -hwaccels
|
|
406
|
+
* Checks whether a given hardware acceleration method is available and validated on the host, as provided by the output of `ffmpeg -hwaccels` and the per-accel
|
|
407
|
+
* capability probe.
|
|
185
408
|
*
|
|
186
|
-
* @param accel - The hardware acceleration method name, e.g., "videotoolbox"
|
|
409
|
+
* @param accel - The hardware acceleration method name, e.g., `"videotoolbox"`.
|
|
187
410
|
*
|
|
188
411
|
* @returns `true` if the hardware acceleration method is available, `false` otherwise.
|
|
189
412
|
*
|
|
@@ -197,207 +420,204 @@ export class FfmpegCodecs {
|
|
|
197
420
|
* ```
|
|
198
421
|
*/
|
|
199
422
|
hasHwAccel(accel) {
|
|
200
|
-
return this.
|
|
201
|
-
}
|
|
202
|
-
/**
|
|
203
|
-
* Returns the amount of GPU memory available on the host system, in megabytes.
|
|
204
|
-
*
|
|
205
|
-
* @remarks Always returns `0` on non-Raspberry Pi systems.
|
|
206
|
-
*/
|
|
207
|
-
get gpuMem() {
|
|
208
|
-
return this._gpuMem ?? 0;
|
|
423
|
+
return this.#state.hwAccels.has(accel.toLowerCase());
|
|
209
424
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
425
|
+
}
|
|
426
|
+
// Probe pipeline. Module-scope functions rather than class methods because the probe assembles state BEFORE a `FfmpegCodecs` instance exists - putting the
|
|
427
|
+
// orchestration inside the class would conflate "how do I populate state?" with "how do I hold and serve state?" The factory at `FfmpegCodecs.probe` glues the two
|
|
428
|
+
// together.
|
|
429
|
+
// Orchestrate the full probe pipeline. Returns a populated state snapshot on success, or `null` when any required probe fails.
|
|
430
|
+
async function probeFfmpegCapabilities(options, signal) {
|
|
431
|
+
const ffmpegExec = options.ffmpegExec ?? "ffmpeg";
|
|
432
|
+
const verbose = options.verbose ?? false;
|
|
433
|
+
const { log } = options;
|
|
434
|
+
// Sync host-system / CPU-generation detection.
|
|
435
|
+
const { hostSystem, cpuGeneration } = probeHwOs();
|
|
436
|
+
// Raspberry Pi: probe GPU memory via vcgencmd. Non-fatal if the command fails (0 propagates; the configureHwAccel gate treats 0 as insufficient anyway).
|
|
437
|
+
let gpuMem = 0;
|
|
438
|
+
if (hostSystem === "raspbian") {
|
|
439
|
+
gpuMem = await probeRpiGpuMem(log, signal);
|
|
215
440
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
*/
|
|
221
|
-
get hostSystem() {
|
|
222
|
-
return this._hostSystem ?? "generic";
|
|
441
|
+
// FFmpeg version is the first required probe; failure here means the FFmpeg binary isn't usable.
|
|
442
|
+
const ffmpegVersion = await probeFfmpegVersion(ffmpegExec, log, signal);
|
|
443
|
+
if (ffmpegVersion === null) {
|
|
444
|
+
return null;
|
|
223
445
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
get cpuGeneration() {
|
|
230
|
-
return this._cpuGeneration ?? 0;
|
|
446
|
+
log.info("Using FFmpeg version: %s.", ffmpegVersion);
|
|
447
|
+
// FFmpeg codec inventory.
|
|
448
|
+
const codecs = await probeFfmpegCodecs(ffmpegExec, log, signal);
|
|
449
|
+
if (codecs === null) {
|
|
450
|
+
return null;
|
|
231
451
|
}
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const versionRegex = /^ffmpeg version (.*) Copyright.*$/m;
|
|
237
|
-
// Parse out the version string.
|
|
238
|
-
const versionMatch = versionRegex.exec(stdout);
|
|
239
|
-
// If we have a version string, let's save it. Otherwise, we're blind.
|
|
240
|
-
this._ffmpegVersion = versionMatch ? versionMatch[1] : "unknown";
|
|
241
|
-
this.log.info("Using FFmpeg version: %s.", this.ffmpegVersion);
|
|
242
|
-
});
|
|
452
|
+
// FFmpeg hwaccels inventory, with per-accel capability validation.
|
|
453
|
+
const hwAccels = await probeFfmpegHwAccels(ffmpegExec, verbose, log, signal);
|
|
454
|
+
if (hwAccels === null) {
|
|
455
|
+
return null;
|
|
243
456
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
457
|
+
return {
|
|
458
|
+
codecs,
|
|
459
|
+
cpuGeneration,
|
|
460
|
+
ffmpegExec,
|
|
461
|
+
ffmpegVersion,
|
|
462
|
+
ffmpegVersionParts: parseFfmpegVersionParts(ffmpegVersion),
|
|
463
|
+
gpuMem,
|
|
464
|
+
hostSystem,
|
|
465
|
+
hwAccels,
|
|
466
|
+
verbose
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
// Detect hardware / OS characteristics synchronously from Node built-ins. No subprocesses; cheap and deterministic.
|
|
470
|
+
function probeHwOs() {
|
|
471
|
+
// Retrieve the CPU model string once to avoid repeated allocations from cpus(). A machine always has at least one CPU entry in practice; the empty-string fallback
|
|
472
|
+
// keeps the lookup total for `noUncheckedIndexedAccess` without changing behavior in any real environment.
|
|
473
|
+
const cpuModelString = cpus()[0]?.model ?? "";
|
|
474
|
+
let hostSystem = "generic";
|
|
475
|
+
let cpuGeneration = 0;
|
|
476
|
+
// Take a look at the platform we're on for an initial hint of what we are.
|
|
477
|
+
switch (platform) {
|
|
478
|
+
// The beloved macOS.
|
|
479
|
+
case "darwin":
|
|
480
|
+
hostSystem = "macOS." + (cpuModelString.includes("Apple") ? "Apple" : "Intel");
|
|
481
|
+
// Identify what generation of Apple Silicon we have.
|
|
482
|
+
if (cpuModelString.includes("Apple")) {
|
|
483
|
+
// Extract the CPU model.
|
|
484
|
+
const cpuModel = /Apple M(\d+) .*/i.exec(cpuModelString);
|
|
485
|
+
if (cpuModel?.[1]) {
|
|
486
|
+
cpuGeneration = Number(cpuModel[1]);
|
|
256
487
|
}
|
|
257
|
-
// We've found a hardware acceleration method, let's add it.
|
|
258
|
-
this.ffmpegHwAccels.add(accel.toLowerCase());
|
|
259
488
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
this.ffmpegHwAccels.delete(accel);
|
|
272
|
-
if (this.verbose) {
|
|
273
|
-
this.log.error("Hardware-accelerated decoding and encoding using %s will be unavailable: unable to successfully validate capabilities.", accel);
|
|
489
|
+
break;
|
|
490
|
+
// The indomitable Linux.
|
|
491
|
+
case "linux":
|
|
492
|
+
// Check /sys/firmware for the Raspberry Pi 4 device-tree model string. Failure is non-fatal: we fall through to the generic-Linux path with no Pi-specific
|
|
493
|
+
// configuration.
|
|
494
|
+
try {
|
|
495
|
+
// As of the 4.9 kernel, Raspberry Pi prefers to be identified using this method and has deprecated cpuinfo.
|
|
496
|
+
const systemId = readFileSync("/sys/firmware/devicetree/base/model", { encoding: "utf8" });
|
|
497
|
+
// Is it a Pi 4?
|
|
498
|
+
if (/Raspberry Pi (Compute Module )?4/.test(systemId)) {
|
|
499
|
+
hostSystem = "raspbian";
|
|
274
500
|
}
|
|
275
501
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
279
|
-
// Probe our video processor's encoding and decoding capabilities.
|
|
280
|
-
async probeFfmpegCodecs() {
|
|
281
|
-
return this.probeCmd(this.ffmpegExec, ["-hide_banner", "-codecs"], (stdout) => {
|
|
282
|
-
// A regular expression to parse out the codec and its supported decoders.
|
|
283
|
-
const decodersRegex = /\S+\s+(\S+).+\(decoders: (.*?)\s*\)/;
|
|
284
|
-
// A regular expression to parse out the codec and its supported encoders.
|
|
285
|
-
const encodersRegex = /\S+\s+(\S+).+\(encoders: (.*?)\s*\)/;
|
|
286
|
-
// Iterate through each line, and a build a list of encoders.
|
|
287
|
-
for (const codecLine of stdout.split(EOL)) {
|
|
288
|
-
// Let's see if we have decoders.
|
|
289
|
-
const decodersMatch = decodersRegex.exec(codecLine);
|
|
290
|
-
// Let's see if we have encoders.
|
|
291
|
-
const encodersMatch = encodersRegex.exec(codecLine);
|
|
292
|
-
// If we found decoders, add them to our list of supported decoders for this format.
|
|
293
|
-
if (decodersMatch) {
|
|
294
|
-
this.ffmpegCodecs[decodersMatch[1]] ??= { decoders: new Set(), encoders: new Set() };
|
|
295
|
-
this.ffmpegCodecs[decodersMatch[1]].decoders = new Set(decodersMatch[2].split(" ").map(x => x.toLowerCase()));
|
|
296
|
-
}
|
|
297
|
-
// If we found encoders, add them to our list of supported encoders for this format.
|
|
298
|
-
if (encodersMatch) {
|
|
299
|
-
this.ffmpegCodecs[encodersMatch[1]] ??= { decoders: new Set(), encoders: new Set() };
|
|
300
|
-
this.ffmpegCodecs[encodersMatch[1]].encoders = new Set(encodersMatch[2].split(" ").map(x => x.toLowerCase()));
|
|
301
|
-
}
|
|
502
|
+
catch {
|
|
503
|
+
// We aren't especially concerned with errors here, given we're just trying to ascertain the system information through hints.
|
|
302
504
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
if (cpuModelString.includes("Apple")) {
|
|
316
|
-
// Extract the CPU model.
|
|
317
|
-
const cpuModel = /Apple M(\d+) .*/i.exec(cpuModelString);
|
|
318
|
-
this._cpuGeneration = 0;
|
|
319
|
-
if (cpuModel?.[1]) {
|
|
320
|
-
this._cpuGeneration = Number(cpuModel[1]);
|
|
505
|
+
// Identify what generation of Intel CPU we have if we're on Intel.
|
|
506
|
+
if (cpuModelString.includes("Intel")) {
|
|
507
|
+
// Extract the CPU model.
|
|
508
|
+
const cpuModel = /Intel.*Core.*i\d+-(\d{3,5})/i.exec(cpuModelString);
|
|
509
|
+
if (cpuModel?.[1]) {
|
|
510
|
+
// Grab the individual SKU as both a number and string.
|
|
511
|
+
const skuStr = cpuModel[1];
|
|
512
|
+
const skuNum = Number(skuStr);
|
|
513
|
+
// Now deduce the CPU generation.
|
|
514
|
+
if (skuNum < 1000) {
|
|
515
|
+
// First generation CPUs are three digit SKUs.
|
|
516
|
+
cpuGeneration = 1;
|
|
321
517
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
case "linux":
|
|
326
|
-
// Let's further see if we're a small, but scrappy, Raspberry Pi.
|
|
327
|
-
try {
|
|
328
|
-
// As of the 4.9 kernel, Raspberry Pi prefers to be identified using this method and has deprecated cpuinfo.
|
|
329
|
-
const systemId = readFileSync("/sys/firmware/devicetree/base/model", { encoding: "utf8" });
|
|
330
|
-
// Is it a Pi 4?
|
|
331
|
-
if (/Raspberry Pi (Compute Module )?4/.test(systemId)) {
|
|
332
|
-
this._hostSystem = "raspbian";
|
|
518
|
+
else if (skuStr.length > 4) {
|
|
519
|
+
// For five-digit SKUs, the generation are the leading digits before the last three.
|
|
520
|
+
cpuGeneration = Number(skuStr.slice(0, skuStr.length - 3));
|
|
333
521
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
// Identify what generation of Intel CPU we have if we're on Intel.
|
|
339
|
-
if (cpuModelString.includes("Intel")) {
|
|
340
|
-
// Extract the CPU model.
|
|
341
|
-
const cpuModel = /Intel.*Core.*i\d+-(\d{3,5})/i.exec(cpuModelString);
|
|
342
|
-
this._cpuGeneration = 0;
|
|
343
|
-
if (cpuModel?.[1]) {
|
|
344
|
-
// Grab the individual SKU as both a number and string.
|
|
345
|
-
const skuStr = cpuModel[1];
|
|
346
|
-
const skuNum = Number(skuStr);
|
|
347
|
-
// Now deduce the CPU generation.
|
|
348
|
-
if (skuNum < 1000) {
|
|
349
|
-
// First generation CPUs are three digit SKUs.
|
|
350
|
-
this._cpuGeneration = 1;
|
|
351
|
-
}
|
|
352
|
-
else if (skuStr.length > 4) {
|
|
353
|
-
// For five-digit SKUs, the generation are the leading digits before the last three.
|
|
354
|
-
this._cpuGeneration = Number(skuStr.slice(0, skuStr.length - 3));
|
|
355
|
-
}
|
|
356
|
-
else {
|
|
357
|
-
// Finally, for four-digit SKUs, the generation is the first digit.
|
|
358
|
-
this._cpuGeneration = Number(skuStr.charAt(0));
|
|
359
|
-
}
|
|
522
|
+
else {
|
|
523
|
+
// Finally, for four-digit SKUs, the generation is the first digit.
|
|
524
|
+
cpuGeneration = Number(skuStr.charAt(0));
|
|
360
525
|
}
|
|
361
526
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
527
|
+
}
|
|
528
|
+
break;
|
|
529
|
+
default:
|
|
530
|
+
// We aren't trying to solve for every system type.
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
return { cpuGeneration, hostSystem };
|
|
534
|
+
}
|
|
535
|
+
// Probe the FFmpeg version string via `ffmpeg -hide_banner -version`. Returns the parsed version string, or `null` on probe failure.
|
|
536
|
+
async function probeFfmpegVersion(ffmpegExec, log, signal) {
|
|
537
|
+
let version = null;
|
|
538
|
+
const ok = await probeCmd(ffmpegExec, ["-hide_banner", "-version"], (stdout) => {
|
|
539
|
+
version = parseFfmpegVersion(stdout);
|
|
540
|
+
}, { log, signal });
|
|
541
|
+
return ok ? version : null;
|
|
542
|
+
}
|
|
543
|
+
// Probe the FFmpeg codec inventory via `ffmpeg -hide_banner -codecs`. Returns a format-keyed index, or `null` on probe failure.
|
|
544
|
+
async function probeFfmpegCodecs(ffmpegExec, log, signal) {
|
|
545
|
+
let parsed = null;
|
|
546
|
+
const ok = await probeCmd(ffmpegExec, ["-hide_banner", "-codecs"], (stdout) => {
|
|
547
|
+
parsed = parseFfmpegCodecs(stdout);
|
|
548
|
+
}, { log, signal });
|
|
549
|
+
return ok ? parsed : null;
|
|
550
|
+
}
|
|
551
|
+
// Probe the FFmpeg hardware-accelerator inventory via `ffmpeg -hide_banner -hwaccels`, then validate each advertised accel by running a one-second synthetic transcode
|
|
552
|
+
// that initializes the hardware-acceleration context. The validation catches the case where a build advertises an accel the host hardware cannot actually use;
|
|
553
|
+
// discarding those accelerators here means callers don't have to re-validate downstream.
|
|
554
|
+
async function probeFfmpegHwAccels(ffmpegExec, verbose, log, signal) {
|
|
555
|
+
const hwAccels = new Set();
|
|
556
|
+
const ok = await probeCmd(ffmpegExec, ["-hide_banner", "-hwaccels"], (stdout) => {
|
|
557
|
+
for (const accel of parseFfmpegHwAccels(stdout)) {
|
|
558
|
+
hwAccels.add(accel);
|
|
366
559
|
}
|
|
560
|
+
}, { log, signal });
|
|
561
|
+
if (!ok) {
|
|
562
|
+
return null;
|
|
367
563
|
}
|
|
368
|
-
//
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
564
|
+
// Capability validation per accel. Hardware / driver availability can diverge from FFmpeg's advertised build capabilities (e.g., `videotoolbox` is always present in
|
|
565
|
+
// macOS builds but unusable on Intel Macs running a pre-VT OS). A quick synthetic transcode against each accel tells us which ones actually work on this host.
|
|
566
|
+
for (const accel of hwAccels) {
|
|
567
|
+
// Validate one accel at a time rather than in parallel: each synthetic transcode initializes the same underlying hardware-acceleration context (GPU or codec
|
|
568
|
+
// engine), so concurrent probes would contend for that shared resource and risk a false negative - an accel reported as unusable because of contention with a
|
|
569
|
+
// sibling probe rather than a genuine host incapability. This is a one-time startup probe, not a throughput-sensitive workload, so serializing it here costs nothing
|
|
570
|
+
// that matters.
|
|
571
|
+
// eslint-disable-next-line no-await-in-loop
|
|
572
|
+
const accelOk = await probeCmd(ffmpegExec, [
|
|
573
|
+
"-hide_banner", "-hwaccel", accel, "-v", "quiet", "-t", "1", "-f", "lavfi", "-i", "color=black:1920x1080", "-c:v", "libx264", "-f", "null", "-"
|
|
574
|
+
], () => { }, { log, quietRunErrors: true, signal });
|
|
575
|
+
if (!accelOk) {
|
|
576
|
+
hwAccels.delete(accel);
|
|
577
|
+
if (verbose) {
|
|
578
|
+
log.error("Hardware-accelerated decoding and encoding using %s will be unavailable: unable to successfully validate capabilities.", accel);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return hwAccels;
|
|
583
|
+
}
|
|
584
|
+
// Probe Raspberry Pi GPU memory via `vcgencmd get_mem gpu`. Returns the megabyte value, or `0` on probe failure (the caller's configureHwAccel gate treats `0` as
|
|
585
|
+
// insufficient anyway, so the return-value distinction is cosmetic but keeps the null handling consistent with the other probes).
|
|
586
|
+
async function probeRpiGpuMem(log, signal) {
|
|
587
|
+
let gpuMem = 0;
|
|
588
|
+
await probeCmd("vcgencmd", ["get_mem", "gpu"], (stdout) => {
|
|
589
|
+
gpuMem = parseRpiGpuMem(stdout);
|
|
590
|
+
}, { log, signal });
|
|
591
|
+
return gpuMem;
|
|
592
|
+
}
|
|
593
|
+
// Utility to run a probe subcommand under a composed signal (caller's signal + a per-probe watchdog timeout). The caller's processOutput handler receives the stdout on
|
|
594
|
+
// success; a returned `true` indicates the command completed and the output was handed off. An ENOENT failure (the binary could not be found) always logs regardless of
|
|
595
|
+
// `quietRunErrors`, since a missing binary is worth surfacing no matter the caller's intent; other failures log unless `quietRunErrors` is set - that flag exists for
|
|
596
|
+
// the per-accel validation loop where failures are expected and logged at a higher level by the caller.
|
|
597
|
+
async function probeCmd(command, commandLineArgs, processOutput, options) {
|
|
598
|
+
const { log, quietRunErrors = false, signal: callerSignal } = options;
|
|
599
|
+
// Always compose with a per-probe watchdog timeout so the worst-case outcome is bounded. When the caller also supplies a signal, `composeSignals` falls through to
|
|
600
|
+
// `AbortSignal.any()`; when no caller signal is supplied, the timeout alone governs (composeSignals returns it unwrapped).
|
|
601
|
+
const composed = composeSignals(callerSignal, AbortSignal.timeout(PROBE_DEFAULT_TIMEOUT_MS));
|
|
602
|
+
try {
|
|
603
|
+
const { stdout } = await execFileAsync(command, commandLineArgs, { signal: composed });
|
|
604
|
+
processOutput(stdout);
|
|
605
|
+
return true;
|
|
378
606
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
processOutput(stdout);
|
|
385
|
-
return true;
|
|
607
|
+
catch (error) {
|
|
608
|
+
const execError = error;
|
|
609
|
+
if (execError.code === "ENOENT") {
|
|
610
|
+
log.error("Unable to find '%s' in path: '%s'.", command, env["PATH"]);
|
|
611
|
+
return false;
|
|
386
612
|
}
|
|
387
|
-
|
|
388
|
-
const execError = error;
|
|
389
|
-
if (execError.code === "ENOENT") {
|
|
390
|
-
this.log.error("Unable to find '%s' in path: '%s'.", command, env.PATH);
|
|
391
|
-
}
|
|
392
|
-
else if (quietRunErrors) {
|
|
393
|
-
return false;
|
|
394
|
-
}
|
|
395
|
-
else {
|
|
396
|
-
this.log.error("Error running %s: %s", command, execError.message);
|
|
397
|
-
}
|
|
398
|
-
this.log.error("Unable to probe the capabilities of your Homebridge host without access to '%s'. Ensure that it is available in your path and correctly working.", command);
|
|
613
|
+
else if (quietRunErrors) {
|
|
399
614
|
return false;
|
|
400
615
|
}
|
|
616
|
+
else {
|
|
617
|
+
log.error("Error running %s: %s.", command, execError.message.replace(/\.$/, ""));
|
|
618
|
+
}
|
|
619
|
+
log.error("Unable to probe the capabilities of your Homebridge host without access to '%s'. Ensure that it is available in your path and correctly working.", command);
|
|
620
|
+
return false;
|
|
401
621
|
}
|
|
402
622
|
}
|
|
403
623
|
//# sourceMappingURL=codecs.js.map
|