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.
Files changed (198) hide show
  1. package/README.md +130 -2
  2. package/build/eslint-plugin/README.md +164 -0
  3. package/build/eslint-plugin/config.mjs +308 -0
  4. package/build/eslint-plugin/index.mjs +7 -0
  5. package/build/eslint-plugin/plugin.mjs +46 -0
  6. package/build/eslint-plugin/rules/blank-line-after-open-brace.mjs +137 -0
  7. package/build/eslint-plugin/rules/blank-line-after-open-brace.test.mjs +112 -0
  8. package/build/eslint-plugin/rules/comment-style.mjs +190 -0
  9. package/build/eslint-plugin/rules/comment-style.test.mjs +190 -0
  10. package/build/eslint-plugin/rules/enforce-node-protocol.mjs +114 -0
  11. package/build/eslint-plugin/rules/enforce-node-protocol.test.mjs +116 -0
  12. package/build/eslint-plugin/rules/paren-comparisons-in-logical.mjs +94 -0
  13. package/build/eslint-plugin/rules/paren-comparisons-in-logical.test.mjs +107 -0
  14. package/build/eslint-plugin/rules/split-type-imports.mjs +354 -0
  15. package/build/eslint-plugin/rules/split-type-imports.test.mjs +268 -0
  16. package/build/eslint-plugin/test-setup.mjs +21 -0
  17. package/build/fs-ops.mjs +184 -0
  18. package/build/tsconfig.json +18 -3
  19. package/dist/backpressure.d.ts +128 -0
  20. package/dist/backpressure.js +273 -0
  21. package/dist/backpressure.js.map +1 -0
  22. package/dist/cli/index.d.ts +95 -0
  23. package/dist/cli/index.js +355 -0
  24. package/dist/cli/index.js.map +1 -0
  25. package/dist/clock-double.d.ts +93 -0
  26. package/dist/clock-double.js +141 -0
  27. package/dist/clock-double.js.map +1 -0
  28. package/dist/clock.d.ts +39 -0
  29. package/dist/clock.js +34 -0
  30. package/dist/clock.js.map +1 -0
  31. package/dist/eslint-plugin/config.d.mts +193 -0
  32. package/dist/eslint-plugin/index.d.mts +2 -0
  33. package/dist/eslint-plugin/plugin.d.mts +87 -0
  34. package/dist/eslint-plugin/rules/blank-line-after-open-brace.d.mts +19 -0
  35. package/dist/eslint-plugin/rules/comment-style.d.mts +15 -0
  36. package/dist/eslint-plugin/rules/enforce-node-protocol.d.mts +18 -0
  37. package/dist/eslint-plugin/rules/paren-comparisons-in-logical.d.mts +15 -0
  38. package/dist/eslint-plugin/rules/split-type-imports.d.mts +16 -0
  39. package/dist/featureOptions-docs.d.ts +96 -0
  40. package/dist/featureOptions-docs.js +193 -0
  41. package/dist/featureOptions-docs.js.map +1 -0
  42. package/dist/featureOptions.d.ts +674 -0
  43. package/dist/featureOptions.js +870 -0
  44. package/dist/featureOptions.js.map +1 -0
  45. package/dist/ffmpeg/codecs.d.ts +256 -72
  46. package/dist/ffmpeg/codecs.js +477 -257
  47. package/dist/ffmpeg/codecs.js.map +1 -1
  48. package/dist/ffmpeg/dgram-util.d.ts +46 -0
  49. package/dist/ffmpeg/dgram-util.js +38 -0
  50. package/dist/ffmpeg/dgram-util.js.map +1 -0
  51. package/dist/ffmpeg/exec.d.ts +83 -64
  52. package/dist/ffmpeg/exec.js +77 -86
  53. package/dist/ffmpeg/exec.js.map +1 -1
  54. package/dist/ffmpeg/fmp4.d.ts +70 -4
  55. package/dist/ffmpeg/fmp4.js +100 -11
  56. package/dist/ffmpeg/fmp4.js.map +1 -1
  57. package/dist/ffmpeg/hap-enums.d.ts +214 -0
  58. package/dist/ffmpeg/hap-enums.js +92 -0
  59. package/dist/ffmpeg/hap-enums.js.map +1 -0
  60. package/dist/ffmpeg/index.d.ts +16 -9
  61. package/dist/ffmpeg/index.js +6 -0
  62. package/dist/ffmpeg/index.js.map +1 -1
  63. package/dist/ffmpeg/mp4-assembler.d.ts +120 -0
  64. package/dist/ffmpeg/mp4-assembler.js +387 -0
  65. package/dist/ffmpeg/mp4-assembler.js.map +1 -0
  66. package/dist/ffmpeg/mp4-parser.d.ts +94 -0
  67. package/dist/ffmpeg/mp4-parser.js +130 -0
  68. package/dist/ffmpeg/mp4-parser.js.map +1 -0
  69. package/dist/ffmpeg/options.d.ts +62 -149
  70. package/dist/ffmpeg/options.js +608 -499
  71. package/dist/ffmpeg/options.js.map +1 -1
  72. package/dist/ffmpeg/process.d.ts +143 -91
  73. package/dist/ffmpeg/process.js +406 -265
  74. package/dist/ffmpeg/process.js.map +1 -1
  75. package/dist/ffmpeg/record.d.ts +302 -188
  76. package/dist/ffmpeg/record.js +408 -560
  77. package/dist/ffmpeg/record.js.map +1 -1
  78. package/dist/ffmpeg/recording-process-double.d.ts +144 -0
  79. package/dist/ffmpeg/recording-process-double.js +168 -0
  80. package/dist/ffmpeg/recording-process-double.js.map +1 -0
  81. package/dist/ffmpeg/rtp-parser.d.ts +70 -0
  82. package/dist/ffmpeg/rtp-parser.js +77 -0
  83. package/dist/ffmpeg/rtp-parser.js.map +1 -0
  84. package/dist/ffmpeg/rtp.d.ts +198 -141
  85. package/dist/ffmpeg/rtp.js +473 -244
  86. package/dist/ffmpeg/rtp.js.map +1 -1
  87. package/dist/ffmpeg/settings.d.ts +5 -1
  88. package/dist/ffmpeg/settings.js +20 -2
  89. package/dist/ffmpeg/settings.js.map +1 -1
  90. package/dist/ffmpeg/stream.d.ts +57 -107
  91. package/dist/ffmpeg/stream.js +121 -148
  92. package/dist/ffmpeg/stream.js.map +1 -1
  93. package/dist/formatters.d.ts +106 -0
  94. package/dist/formatters.js +174 -0
  95. package/dist/formatters.js.map +1 -0
  96. package/dist/homebridge-enums.d.ts +30 -0
  97. package/dist/homebridge-enums.js +17 -0
  98. package/dist/homebridge-enums.js.map +1 -0
  99. package/dist/index.d.ts +12 -5
  100. package/dist/index.js +8 -2
  101. package/dist/index.js.map +1 -1
  102. package/dist/logclient/auth.d.ts +114 -0
  103. package/dist/logclient/auth.js +199 -0
  104. package/dist/logclient/auth.js.map +1 -0
  105. package/dist/logclient/cli-run.d.ts +76 -0
  106. package/dist/logclient/cli-run.js +639 -0
  107. package/dist/logclient/cli-run.js.map +1 -0
  108. package/dist/logclient/cli.d.ts +3 -0
  109. package/dist/logclient/cli.js +97 -0
  110. package/dist/logclient/cli.js.map +1 -0
  111. package/dist/logclient/client.d.ts +145 -0
  112. package/dist/logclient/client.js +600 -0
  113. package/dist/logclient/client.js.map +1 -0
  114. package/dist/logclient/config.d.ts +173 -0
  115. package/dist/logclient/config.js +199 -0
  116. package/dist/logclient/config.js.map +1 -0
  117. package/dist/logclient/endpoints.d.ts +54 -0
  118. package/dist/logclient/endpoints.js +73 -0
  119. package/dist/logclient/endpoints.js.map +1 -0
  120. package/dist/logclient/filter.d.ts +45 -0
  121. package/dist/logclient/filter.js +51 -0
  122. package/dist/logclient/filter.js.map +1 -0
  123. package/dist/logclient/frame.d.ts +93 -0
  124. package/dist/logclient/frame.js +203 -0
  125. package/dist/logclient/frame.js.map +1 -0
  126. package/dist/logclient/index.d.ts +31 -0
  127. package/dist/logclient/index.js +12 -0
  128. package/dist/logclient/index.js.map +1 -0
  129. package/dist/logclient/parser.d.ts +211 -0
  130. package/dist/logclient/parser.js +393 -0
  131. package/dist/logclient/parser.js.map +1 -0
  132. package/dist/logclient/rest.d.ts +41 -0
  133. package/dist/logclient/rest.js +111 -0
  134. package/dist/logclient/rest.js.map +1 -0
  135. package/dist/logclient/settings.d.ts +15 -0
  136. package/dist/logclient/settings.js +64 -0
  137. package/dist/logclient/settings.js.map +1 -0
  138. package/dist/logclient/socket-double.d.ts +201 -0
  139. package/dist/logclient/socket-double.js +384 -0
  140. package/dist/logclient/socket-double.js.map +1 -0
  141. package/dist/logclient/socket.d.ts +257 -0
  142. package/dist/logclient/socket.js +620 -0
  143. package/dist/logclient/socket.js.map +1 -0
  144. package/dist/logclient/stitch.d.ts +83 -0
  145. package/dist/logclient/stitch.js +146 -0
  146. package/dist/logclient/stitch.js.map +1 -0
  147. package/dist/logclient/time-expression.d.ts +42 -0
  148. package/dist/logclient/time-expression.js +181 -0
  149. package/dist/logclient/time-expression.js.map +1 -0
  150. package/dist/logclient/time-window.d.ts +38 -0
  151. package/dist/logclient/time-window.js +53 -0
  152. package/dist/logclient/time-window.js.map +1 -0
  153. package/dist/logclient/types.d.ts +107 -0
  154. package/dist/logclient/types.js +6 -0
  155. package/dist/logclient/types.js.map +1 -0
  156. package/dist/mqttClient.d.ts +287 -0
  157. package/dist/mqttClient.js +433 -0
  158. package/dist/mqttClient.js.map +1 -0
  159. package/dist/service.d.ts +64 -15
  160. package/dist/service.js +93 -66
  161. package/dist/service.js.map +1 -1
  162. package/dist/ui/featureOptions.js +870 -0
  163. package/dist/ui/featureOptions.js.map +1 -0
  164. package/dist/ui/formatters.js +174 -0
  165. package/dist/ui/formatters.js.map +1 -0
  166. package/dist/ui/pluginConfigSession.mjs +141 -0
  167. package/dist/ui/webUi-featureOptions/categoryState.mjs +135 -0
  168. package/dist/ui/webUi-featureOptions/effects/keyboard.mjs +61 -0
  169. package/dist/ui/webUi-featureOptions/effects/persist.mjs +226 -0
  170. package/dist/ui/webUi-featureOptions/effects/theme.mjs +398 -0
  171. package/dist/ui/webUi-featureOptions/effects/tokens.mjs +152 -0
  172. package/dist/ui/webUi-featureOptions/rendering.mjs +431 -0
  173. package/dist/ui/webUi-featureOptions/selectors.mjs +360 -0
  174. package/dist/ui/webUi-featureOptions/state.mjs +319 -0
  175. package/dist/ui/webUi-featureOptions/store.mjs +181 -0
  176. package/dist/ui/webUi-featureOptions/utils.mjs +200 -0
  177. package/dist/ui/webUi-featureOptions/views/connectionError.mjs +152 -0
  178. package/dist/ui/webUi-featureOptions/views/deviceInfo.mjs +80 -0
  179. package/dist/ui/webUi-featureOptions/views/header.mjs +77 -0
  180. package/dist/ui/webUi-featureOptions/views/nav.mjs +327 -0
  181. package/dist/ui/webUi-featureOptions/views/options.mjs +521 -0
  182. package/dist/ui/webUi-featureOptions/views/search.mjs +395 -0
  183. package/dist/ui/webUi-featureOptions.mjs +677 -0
  184. package/dist/ui/webUi.mjs +192 -87
  185. package/dist/util.d.ts +597 -45
  186. package/dist/util.js +787 -68
  187. package/dist/util.js.map +1 -1
  188. package/package.json +33 -15
  189. package/build/eslint-rules.mjs +0 -511
  190. package/dist/featureoptions.d.ts +0 -264
  191. package/dist/featureoptions.js +0 -480
  192. package/dist/featureoptions.js.map +0 -1
  193. package/dist/mqttclient.d.ts +0 -178
  194. package/dist/mqttclient.js +0 -310
  195. package/dist/mqttclient.js.map +0 -1
  196. package/dist/ui/featureoptions.js +0 -480
  197. package/dist/ui/featureoptions.js.map +0 -1
  198. package/dist/ui/webUi-featureoptions.mjs +0 -3765
@@ -1,185 +1,158 @@
1
1
  /* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
2
2
  *
3
- * ffmpeg/stream.ts: Provide FFmpeg process control to support HomeKit livestreaming.
3
+ * ffmpeg/stream.ts: FFmpeg process control for HomeKit livestreaming with signal-driven stream-health monitoring.
4
4
  */
5
+ /**
6
+ * HomeKit livestreaming FFmpeg process with a signal-driven internal stream-health monitor.
7
+ *
8
+ * This module defines `FfmpegStreamingProcess`, the specialization of {@link FfmpegProcess} for HomeKit live video sessions. The subclass extends the base directly and
9
+ * composes an internal UDP socket that watches the return port for inbound packets - the HomeKit client's RTCP receiver reports, which flow only once the client is
10
+ * receiving FFmpeg's output. The watchdog arms on the first such packet; if that return traffic then stops for longer than the configured window the socket aborts the
11
+ * process with `HbpuAbortError("timeout")` and the base class teardown handles the rest. There is no separate "delegate" surface, no error callbacks,
12
+ * and no reach-through to the raw ChildProcess - every external interaction is through the inherited `signal`, `ready`, `exited`, `stdin`, `stderr`, `stderrLog`,
13
+ * `abort()`, and `[Symbol.asyncDispose]`.
14
+ *
15
+ * `stdout` stays externally readable on this subclass because HBUP's two-way-audio talkback path forwards stdout bytes to the WebSocket that carries audio from the
16
+ * camera back to the HomeKit client. Unlike the fMP4 subclasses, there is no internal consumer of the stdout stream that would race with an external reader.
17
+ *
18
+ * The stream-health socket is intentionally simpler than {@link ffmpeg/rtp!RtpDemuxer | RtpDemuxer}: its sole job is to detect liveness on a known return port. It
19
+ * does not classify RTP vs. RTCP or forward packets. Two-way-audio demuxing is `RtpDemuxer`'s responsibility and lives in its own class.
20
+ *
21
+ * @module
22
+ */
23
+ import { HbpuAbortError, Watchdog, onAbort } from "../util.js";
24
+ import { createDgramSocket, loopbackAddress } from "./dgram-util.js";
5
25
  import { FfmpegProcess } from "./process.js";
6
- import { createSocket } from "node:dgram";
26
+ import { STREAM_HEALTH_TIMEOUT } from "./settings.js";
7
27
  /**
8
- * Provides FFmpeg process management and socket handling to support HomeKit livestreaming sessions.
28
+ * FFmpeg process specialization for HomeKit livestreaming. Extends {@link FfmpegProcess} directly and composes an internal stream-health UDP socket when a return port
29
+ * is configured.
9
30
  *
10
- * This class extends `FfmpegProcess` to create, monitor, and terminate HomeKit-compatible video streams. Additionally, it invokes delegate hooks for error processing and
11
- * stream lifecycle management.
31
+ * Lifecycle is entirely signal-driven: construction spawns the child and (optionally) binds the health socket; the socket watches for inbound packets and aborts the
32
+ * process with `"timeout"` if the window lapses; the inherited teardown path closes the socket and clears the watchdog timer as part of its signal-abort listener
33
+ * fan-out. The subclass adds no new public verbs beyond what {@link FfmpegProcess} provides.
12
34
  *
13
35
  * @example
14
36
  *
15
37
  * ```ts
16
- * const streamingDelegate: HomebridgeStreamingDelegate = {
38
+ * await using proc = new FfmpegStreamingProcess(ffmpegOptions, {
17
39
  *
18
- * controller,
19
- * stopStream: (sessionId) => { ... } // End-of-session cleanup code.
20
- * };
40
+ * args: commandLineArgs,
41
+ * returnPort: { ipFamily: "ipv4", port: 50000 },
42
+ * signal: session.controller.signal
43
+ * });
21
44
  *
22
- * const process = new FfmpegStreamingProcess(
45
+ * await proc.ready;
23
46
  *
24
- * streamingDelegate,
25
- * sessionId,
26
- * ffmpegOptions,
27
- * commandLineArgs,
28
- * { addressVersion: "ipv4", port: 5000 }
29
- * );
47
+ * // Observe the process from the session's own control flow. When the health socket detects a stall, proc.signal fires
48
+ * // with reason "timeout" and proc.exited resolves with the kill-driven exit context. Surface crashes via the owning
49
+ * // session's error path.
50
+ * proc.exited.catch((error) => session.onStreamingError(error));
30
51
  * ```
31
52
  *
32
- * @see HomebridgeStreamingDelegate
33
53
  * @see FfmpegProcess
34
54
  *
35
55
  * @category FFmpeg
36
56
  */
37
57
  export class FfmpegStreamingProcess extends FfmpegProcess {
38
- /*
39
- * The streaming delegate instance responsible for handling stream events and errors.
40
- */
41
- delegate;
58
+ // The return-port descriptor requested at construction (verbatim from `init.returnPort`). Undefined when no return port was configured. Read through the
59
+ // {@link FfmpegStreamingProcess.returnPort} getter, which projects the kernel-assigned port over this descriptor once the bind settles.
60
+ #requestedReturnPort;
61
+ // The port the kernel actually bound the health socket to, captured from `socket.address().port` once the `"listening"` event fires. Undefined until then. For
62
+ // specific-port binds this duplicates {@link #requestedReturnPort}'s port field but the assignment keeps the post-bind read path uniform across both construction
63
+ // modes (specific port and `port: 0` ephemeral).
64
+ #assignedReturnPort;
42
65
  /**
43
- * The unique session identifier for this streaming process.
44
- */
45
- sessionId;
46
- /**
47
- * The timeout reference used to monitor UDP stream health.
48
- */
49
- streamTimeout;
50
- /**
51
- * Constructs a new FFmpeg streaming process for a HomeKit session.
52
- *
53
- * Sets up required delegate hooks, creates UDP return sockets if needed, and starts the FFmpeg process. Automatically handles FFmpeg process errors and cleans up on
54
- * failures.
55
- *
56
- * @param delegate - The Homebridge streaming delegate for this session.
57
- * @param sessionId - The HomeKit session identifier for this stream.
58
- * @param ffmpegOptions - The FFmpeg configuration options.
59
- * @param commandLineArgs - FFmpeg command-line arguments.
60
- * @param returnPort - Optional. UDP port info for talkback support (used for two-way audio in HomeKit for cameras that support it).
61
- * @param callback - Optional. Callback invoked when the stream is ready or errors occur.
66
+ * Construct and spawn a new streaming FFmpeg process.
62
67
  *
63
- * @example
68
+ * Spawning happens synchronously as part of construction. When `init.returnPort` is supplied, the subclass binds a UDP socket and enforces the liveness watchdog; the
69
+ * socket closes and the watchdog clears as part of the inherited teardown when the signal aborts for any reason.
64
70
  *
65
- * ```ts
66
- * const process = new FfmpegStreamingProcess(delegate, sessionId, ffmpegOptions, commandLineArgs, { addressVersion: "ipv6", port: 6000 });
67
- * ```
71
+ * @param options - Shared {@link FfmpegOptions} configuration (codec support, logger, debug flag, name).
72
+ * @param init - Optional init options. See {@link FfmpegStreamingInit}.
68
73
  */
69
- constructor(delegate, sessionId, ffmpegOptions, commandLineArgs, returnPort, callback) {
70
- // Initialize our parent.
71
- super(ffmpegOptions);
72
- this.delegate = delegate;
73
- this.delegate.adjustProbeSize ??= () => { };
74
- this.delegate.ffmpegErrorCheck ??= () => undefined;
75
- this.delegate.stopStream ??= () => { };
76
- this.sessionId = sessionId;
77
- // Create the return port for FFmpeg, if requested to do so. The only time we don't do this is when we're standing up
78
- // a two-way audio stream - in that case, the audio work is done through RtpSplitter and not here.
79
- if (returnPort) {
80
- this.createSocket(returnPort);
74
+ constructor(options, init = {}) {
75
+ super(options, init);
76
+ this.#requestedReturnPort = init.returnPort;
77
+ if (init.returnPort) {
78
+ this.#startHealthMonitor(init.returnPort, init.healthTimeout ?? STREAM_HEALTH_TIMEOUT);
81
79
  }
82
- // Start it up, with appropriate error handling.
83
- this.start(commandLineArgs, callback, (errorMessage) => {
84
- // Stop the stream.
85
- this.delegate.stopStream?.(this.sessionId);
86
- // Let homebridge know what happened and stop the stream if we've already started.
87
- if (!this.isStarted && this.callback) {
88
- this.callback(new Error(errorMessage));
89
- this.callback = null;
90
- return;
91
- }
92
- // Tell Homebridge to forcibly stop the streaming session.
93
- this.delegate.controller.forceStopStreamingSession(this.sessionId);
94
- this.delegate.stopStream?.(this.sessionId);
95
- });
96
80
  }
97
81
  /**
98
- * Creates and binds a UDP socket for monitoring the health of the outgoing video stream.
82
+ * The UDP return-port descriptor the health socket is bound to, or `undefined` when no return port was configured. For a specific-port construction
83
+ * (`init.returnPort.port` non-zero), this descriptor equals `init.returnPort` from the moment the constructor returns; for an ephemeral construction
84
+ * (`init.returnPort.port === 0`), the `port` field is `0` until the kernel completes the bind, then the kernel-assigned port. Consumers that need the assigned
85
+ * ephemeral port `await proc.ready` before reading it; in practice the health-socket bind completes well before the FFmpeg child reaches the `ready` signal, so a
86
+ * post-`ready` read always observes the kernel's pick.
99
87
  *
100
- * Listens for UDP "message" events, sets and clears timeouts, and handles error/cleanup scenarios. If no messages are received within 5 seconds, forcibly stops the
101
- * stream and informs the delegate.
88
+ * Returns a fresh descriptor on every read; callers must treat the result as read-only. The `ipFamily` field is the verbatim value passed at construction; the
89
+ * `port` field reads from the live socket-address projection once captured (specific and ephemeral binds converge on a single read path).
102
90
  *
103
- * @param portInfo - Object containing the address version ("ipv4" or "ipv6") and port number.
91
+ * @returns The bound return-port descriptor, or `undefined` when no return port was configured.
104
92
  */
105
- createSocket(portInfo) {
106
- let errorListener;
107
- let messageListener;
108
- const socket = createSocket(portInfo.addressVersion === "ipv6" ? "udp6" : "udp4");
109
- // Cleanup after ourselves when the socket closes.
110
- socket.once("close", () => {
111
- if (this.streamTimeout) {
112
- clearTimeout(this.streamTimeout);
113
- }
114
- socket.off("error", errorListener);
115
- socket.off("message", messageListener);
93
+ get returnPort() {
94
+ if (this.#requestedReturnPort === undefined) {
95
+ return undefined;
96
+ }
97
+ return {
98
+ ipFamily: this.#requestedReturnPort.ipFamily,
99
+ port: this.#assignedReturnPort ?? this.#requestedReturnPort.port
100
+ };
101
+ }
102
+ // Bind a UDP socket to the return port and enforce a re-armed inactivity watchdog. Called from the constructor when a return-port descriptor is provided. The
103
+ // pre-aborted-signal guard is load-bearing: if the caller passed an already-aborted parent signal, the base class's teardown runs synchronously during `super()` and
104
+ // `this.signal` is aborted by the time we get here. Registering an `"abort"` listener on an already-aborted signal does NOT re-dispatch, so the socket and watchdog
105
+ // timer would leak if we proceeded. Short-circuiting leaves no resources allocated.
106
+ #startHealthMonitor(returnPort, timeoutMs) {
107
+ if (this.aborted) {
108
+ return;
109
+ }
110
+ const socket = createDgramSocket(returnPort.ipFamily);
111
+ // Capture the kernel-assigned port as soon as the bind succeeds. For specific-port binds this duplicates `returnPort.port`; for ephemeral binds (port: 0) this
112
+ // is where the kernel's pick becomes observable via {@link returnPort}. Wired as `once("listening", ...)` so it fires exactly when the bind transitions to the
113
+ // listening state - the same moment after which message events can arrive. The handler captures `socket` by closure to avoid `this.#socket`-style reads from
114
+ // an async event handler.
115
+ socket.once("listening", () => {
116
+ this.#assignedReturnPort = socket.address().port;
116
117
  });
117
- // Handle potential network errors.
118
- socket.on("error", errorListener = (error) => {
119
- this.log.error("Socket error: %s.", error.name);
120
- this.delegate.stopStream?.(this.sessionId);
118
+ // Inactivity watchdog: if no packets arrive within `timeoutMs`, abort the process with `"timeout"`. Self-cleans when `this.signal` aborts for any other reason,
119
+ // so the teardown listener below only has to close the socket.
120
+ const watchdog = new Watchdog({
121
+ onFire: () => {
122
+ this.log.debug("Streaming process inactivity watchdog fired after %d ms with no inbound packets.", timeoutMs);
123
+ this.abort(new HbpuAbortError("timeout"));
124
+ },
125
+ signal: this.signal,
126
+ timeoutMs
121
127
  });
122
- // Manage our video streams in case we haven't received a stop request, but we're in fact dead zombies.
123
- socket.on("message", messageListener = () => {
124
- // Clear our last canary.
125
- if (this.streamTimeout) {
126
- clearTimeout(this.streamTimeout);
128
+ // Inbound-packet handler and the SOLE arm site for the watchdog. The first inbound packet performs the initial arm; every subsequent packet re-arms it. We do
129
+ // not inspect the payload - liveness is the only signal this socket cares about.
130
+ socket.on("message", () => { watchdog.arm(); });
131
+ // Socket errors on the health port are treated as fatal - if the kernel cannot deliver us packets, we cannot tell the stream is alive. Abort with `"failed"` and
132
+ // surface the underlying error via `cause` so downstream diagnostics carry the root cause.
133
+ socket.on("error", (error) => {
134
+ this.log.error("Streaming process return-port socket error: %s.", error.message);
135
+ if (!this.aborted) {
136
+ this.abort(new HbpuAbortError("failed", { cause: error }));
127
137
  }
128
- // Set our new canary.
129
- this.streamTimeout = setTimeout(() => {
130
- this.log.debug("Video stream appears to be inactive for 5 seconds. Stopping stream.");
131
- this.delegate.controller.forceStopStreamingSession(this.sessionId);
132
- this.delegate.stopStream?.(this.sessionId);
133
- }, 5000);
134
138
  });
135
- // Bind to the port we're opening.
136
- socket.bind(portInfo.port, (portInfo.addressVersion === "ipv6") ? "::1" : "127.0.0.1");
137
- }
138
- /**
139
- * Returns the underlying FFmpeg child process, or null if the process is not running.
140
- *
141
- * @returns The current FFmpeg process, or `null` if not running.
142
- *
143
- * @example
144
- *
145
- * ```ts
146
- * const ffmpeg = process.ffmpegProcess;
147
- *
148
- * if(ffmpeg) {
149
- *
150
- * // Interact directly with the child process if necessary.
151
- * }
152
- * ```
153
- */
154
- get ffmpegProcess() {
155
- return this.process;
156
- }
157
- /**
158
- * Handle and logs FFmpeg process errors.
159
- *
160
- * If a known error condition is detected by the delegate, logs the custom message and returns. For "not enough frames to estimate rate; consider increasing probesize"
161
- * errors, invokes the delegate's `adjustProbeSize` hook for automatic tuning. Otherwise, falls back to the parent class's logging.
162
- *
163
- * @param exitCode - The exit code from FFmpeg.
164
- * @param signal - The signal, if any, used to terminate the process.
165
- */
166
- logFfmpegError(exitCode, signal) {
167
- // We want to process known streaming-related errors due to the performance and latency tweaks we've made to the FFmpeg command line. In some cases we may inform the
168
- // user and take no action, in others, we tune our own internal parameters.
169
- // Process any specific errors our caller is interested in.
170
- const errTest = this.delegate.ffmpegErrorCheck?.(this.stderrLog);
171
- if (errTest) {
172
- this.log.error(errTest);
173
- return;
174
- }
175
- // Test for probesize errors.
176
- if (this.stderrLog.some(logEntry => logEntry.includes("not enough frames to estimate rate; consider increasing probesize"))) {
177
- // Let the streaming delegate know to adjust its parameters for the next run and inform the user.
178
- this.delegate.adjustProbeSize?.();
179
- return;
180
- }
181
- // Otherwise, revert to our default logging in our parent.
182
- super.logFfmpegError(exitCode, signal);
139
+ // Teardown convergence point: when `this.signal` aborts for any reason, close the socket. The watchdog's timer is handled by its own self-clean listener on the
140
+ // same signal, so nothing else is owed here. `onAbort` is preferred over bare `addEventListener` even though the early `aborted` short-circuit above already
141
+ // guards the pre-aborted case for this allocation path - resource-class teardown handlers throughout HBPU go through `onAbort` so no constructor site has to
142
+ // re-implement the AbortSignal pre-aborted-listener workaround.
143
+ onAbort(this.signal, () => socket.close());
144
+ // Bind. Binding may fail asynchronously with an `"error"` event, which the listener above catches.
145
+ socket.bind(returnPort.port, loopbackAddress(returnPort.ipFamily));
146
+ // This is an inactivity watchdog, and "inactivity" presupposes prior activity: it watches one thing - a *flowing* return stream that dried up - so it arms only
147
+ // on the first inbound packet (the message handler above), then re-arms on every subsequent one. There is deliberately NO arm at construction or on `ready`,
148
+ // because the watchdog must not police startup or establishment. Those are separate concerns owned elsewhere: "did the child process start" is the base-class
149
+ // optional `startupTimeout` (documented in process.ts), and "did the return stream ever go live" is the consumer's - the HomeKit session lifecycle and the
150
+ // consumer's establishment gate - which is the only thing that bounds a stream that never delivers a first packet. We do not fold that bound in here, and on the
151
+ // one-way live-view path that bound stays external to this class, owned by the consumer rather than this watchdog. Arming on `ready` was wrong precisely because a
152
+ // ready-armed clock starts before any media has round-tripped: on a cold start the first return packet cannot arrive within the window (prime, transcode, SRTP
153
+ // egress, client decode, client RTCP back all have to complete first), so the clock counts down through that legitimate first-packet latency and false-positives
154
+ // on a perfectly healthy stream. Arming on the packet itself is immune by construction. Note the message handler arms regardless of `ready`: any datagram that
155
+ // arrives before stderr does still starts the inactivity clock, so a stream that is already alive never waits on the spawn signal to be protected.
183
156
  }
184
157
  }
185
158
  //# sourceMappingURL=stream.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"stream.js","sourceRoot":"","sources":["../../src/ffmpeg/stream.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"stream.js","sourceRoot":"","sources":["../../src/ffmpeg/stream.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Format a bitrate value into a human-readable form as bps, kbps, or Mbps.
3
+ *
4
+ * @param value - The bitrate value to convert, in bits per second.
5
+ *
6
+ * @returns Returns the value as a human-readable string.
7
+ *
8
+ * @example
9
+ *
10
+ * ```ts
11
+ * formatBps(500); // "500 bps".
12
+ * formatBps(2000); // "2 kbps".
13
+ * formatBps(15000); // "15 kbps".
14
+ * formatBps(2560); // "2.6 kbps".
15
+ * formatBps(1000000); // "1 Mbps".
16
+ * formatBps(2560000); // "2.6 Mbps".
17
+ * ```
18
+ *
19
+ * @category Utilities
20
+ */
21
+ export declare function formatBps(value: number): string;
22
+ /**
23
+ * Format a byte count into a human-readable form as bytes, KB, MB, GB, or TB. Uses 1024-based thresholds matching the convention every operating system uses for
24
+ * displaying file and buffer sizes.
25
+ *
26
+ * @param value - The byte count to convert.
27
+ *
28
+ * @returns Returns the value as a human-readable string.
29
+ *
30
+ * @example
31
+ *
32
+ * ```ts
33
+ * formatBytes(512); // "512 bytes".
34
+ * formatBytes(2048); // "2 KB".
35
+ * formatBytes(1536); // "1.5 KB".
36
+ * formatBytes(1_048_576); // "1 MB".
37
+ * formatBytes(2_621_440); // "2.5 MB".
38
+ * formatBytes(1_073_741_824); // "1 GB".
39
+ * formatBytes(1_099_511_627_776); // "1 TB".
40
+ * ```
41
+ *
42
+ * @category Utilities
43
+ */
44
+ export declare function formatBytes(value: number): string;
45
+ /**
46
+ * Format a millisecond duration into a human-readable form as ms, s, min, or hr. Tiered thresholds match how operators naturally read elapsed time: sub-second
47
+ * values stay in milliseconds for precision, longer durations promote to seconds, minutes, and hours.
48
+ *
49
+ * @param value - The duration to convert, in milliseconds.
50
+ *
51
+ * @returns Returns the value as a human-readable string.
52
+ *
53
+ * @example
54
+ *
55
+ * ```ts
56
+ * formatMs(250); // "250 ms".
57
+ * formatMs(1500); // "1.5 s".
58
+ * formatMs(15000); // "15 s".
59
+ * formatMs(90000); // "1.5 min".
60
+ * formatMs(5_400_000); // "1.5 hr".
61
+ * ```
62
+ *
63
+ * @category Utilities
64
+ */
65
+ export declare function formatMs(value: number): string;
66
+ /**
67
+ * Format a numeric percentage value into a human-readable form with a trailing percent sign. Applies the same precision policy as the magnitude-based formatters
68
+ * via the shared internal helper: whole numbers render without a trailing decimal, fractional numbers render to one decimal place.
69
+ *
70
+ * @param value - The percentage value to convert. Treated as already-scaled into percent units (50 means 50%, not 0.5).
71
+ *
72
+ * @returns Returns the value as a human-readable string ending in `%`.
73
+ *
74
+ * @example
75
+ *
76
+ * ```ts
77
+ * formatPercent(0); // "0%".
78
+ * formatPercent(50); // "50%".
79
+ * formatPercent(100); // "100%".
80
+ * formatPercent(33.333); // "33.3%".
81
+ * ```
82
+ *
83
+ * @category Utilities
84
+ */
85
+ export declare function formatPercent(value: number): string;
86
+ /**
87
+ * Format a second-resolution duration into a human-readable form as s, min, or hr. Same tier semantics as {@link formatMs}, scaled for inputs that arrive already
88
+ * in seconds rather than milliseconds.
89
+ *
90
+ * @param value - The duration to convert, in seconds.
91
+ *
92
+ * @returns Returns the value as a human-readable string.
93
+ *
94
+ * @example
95
+ *
96
+ * ```ts
97
+ * formatSeconds(45); // "45 s".
98
+ * formatSeconds(90); // "1.5 min".
99
+ * formatSeconds(1800); // "30 min".
100
+ * formatSeconds(5400); // "1.5 hr".
101
+ * ```
102
+ *
103
+ * @category Utilities
104
+ */
105
+ export declare function formatSeconds(value: number): string;
106
+ //# sourceMappingURL=formatters.d.ts.map
@@ -0,0 +1,174 @@
1
+ /* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
2
+ *
3
+ * formatters.ts: Browser-safe magnitude and percentage formatters shared between the server-side `util.ts` surface and the browser-shipped `featureOptions.ts`
4
+ * model.
5
+ */
6
+ /**
7
+ * **Why this file exists.** `featureOptions.ts` ships into `dist/ui/` for the browser to load (via the `copyFeatureOptions` build step). The catalog's built-in
8
+ * formatter registry needs `formatBps`, `formatBytes`, `formatMs`, `formatPercent`, and `formatSeconds` at runtime - and pulling them from `util.ts` would drag in
9
+ * `util.ts`'s `node:timers/promises` import, which the browser cannot resolve. This module is the SSOT for the magnitude-rendering policy. It has zero runtime
10
+ * imports of any kind, so shipping it alongside `featureOptions.js` is safe in any runtime that can execute ES2024+ JavaScript.
11
+ *
12
+ * **Precision policy.** Whole numbers render without a trailing decimal place ("5" not "5.0"); fractional numbers render to one decimal place. Centralizing the
13
+ * precision policy in `formatMagnitude` means tightening it later - more precision, a thousands separator, locale-aware formatting - is a single-line change
14
+ * rather than a sweep across every format helper.
15
+ *
16
+ * **Consumers.** `util.ts` re-exports these for the server-side surface; `featureOptions.ts` imports directly from here to keep its browser-runnable dependency
17
+ * graph free of `util.ts`. Both consumers share one implementation - the file is the join point.
18
+ *
19
+ * @module
20
+ */
21
+ // Shared magnitude-rendering helper used by every magnitude-based formatter, applying the module's precision policy (see the module doc): whole numbers render with
22
+ // no trailing decimal place, fractional numbers to one decimal place.
23
+ function formatMagnitude(value) {
24
+ return ((value % 1) === 0 ? value.toFixed(0) : value.toFixed(1));
25
+ }
26
+ /**
27
+ * Format a bitrate value into a human-readable form as bps, kbps, or Mbps.
28
+ *
29
+ * @param value - The bitrate value to convert, in bits per second.
30
+ *
31
+ * @returns Returns the value as a human-readable string.
32
+ *
33
+ * @example
34
+ *
35
+ * ```ts
36
+ * formatBps(500); // "500 bps".
37
+ * formatBps(2000); // "2 kbps".
38
+ * formatBps(15000); // "15 kbps".
39
+ * formatBps(2560); // "2.6 kbps".
40
+ * formatBps(1000000); // "1 Mbps".
41
+ * formatBps(2560000); // "2.6 Mbps".
42
+ * ```
43
+ *
44
+ * @category Utilities
45
+ */
46
+ export function formatBps(value) {
47
+ if (value < 1_000) {
48
+ return value.toString() + " bps";
49
+ }
50
+ if (value < 1_000_000) {
51
+ return formatMagnitude(value / 1_000) + " kbps";
52
+ }
53
+ return formatMagnitude(value / 1_000_000) + " Mbps";
54
+ }
55
+ /**
56
+ * Format a byte count into a human-readable form as bytes, KB, MB, GB, or TB. Uses 1024-based thresholds matching the convention every operating system uses for
57
+ * displaying file and buffer sizes.
58
+ *
59
+ * @param value - The byte count to convert.
60
+ *
61
+ * @returns Returns the value as a human-readable string.
62
+ *
63
+ * @example
64
+ *
65
+ * ```ts
66
+ * formatBytes(512); // "512 bytes".
67
+ * formatBytes(2048); // "2 KB".
68
+ * formatBytes(1536); // "1.5 KB".
69
+ * formatBytes(1_048_576); // "1 MB".
70
+ * formatBytes(2_621_440); // "2.5 MB".
71
+ * formatBytes(1_073_741_824); // "1 GB".
72
+ * formatBytes(1_099_511_627_776); // "1 TB".
73
+ * ```
74
+ *
75
+ * @category Utilities
76
+ */
77
+ export function formatBytes(value) {
78
+ if (value < 1_024) {
79
+ return value.toString() + " bytes";
80
+ }
81
+ if (value < 1_048_576) {
82
+ return formatMagnitude(value / 1_024) + " KB";
83
+ }
84
+ if (value < 1_073_741_824) {
85
+ return formatMagnitude(value / 1_048_576) + " MB";
86
+ }
87
+ if (value < 1_099_511_627_776) {
88
+ return formatMagnitude(value / 1_073_741_824) + " GB";
89
+ }
90
+ return formatMagnitude(value / 1_099_511_627_776) + " TB";
91
+ }
92
+ /**
93
+ * Format a millisecond duration into a human-readable form as ms, s, min, or hr. Tiered thresholds match how operators naturally read elapsed time: sub-second
94
+ * values stay in milliseconds for precision, longer durations promote to seconds, minutes, and hours.
95
+ *
96
+ * @param value - The duration to convert, in milliseconds.
97
+ *
98
+ * @returns Returns the value as a human-readable string.
99
+ *
100
+ * @example
101
+ *
102
+ * ```ts
103
+ * formatMs(250); // "250 ms".
104
+ * formatMs(1500); // "1.5 s".
105
+ * formatMs(15000); // "15 s".
106
+ * formatMs(90000); // "1.5 min".
107
+ * formatMs(5_400_000); // "1.5 hr".
108
+ * ```
109
+ *
110
+ * @category Utilities
111
+ */
112
+ export function formatMs(value) {
113
+ if (value < 1_000) {
114
+ return value.toString() + " ms";
115
+ }
116
+ if (value < 60_000) {
117
+ return formatMagnitude(value / 1_000) + " s";
118
+ }
119
+ if (value < 3_600_000) {
120
+ return formatMagnitude(value / 60_000) + " min";
121
+ }
122
+ return formatMagnitude(value / 3_600_000) + " hr";
123
+ }
124
+ /**
125
+ * Format a numeric percentage value into a human-readable form with a trailing percent sign. Applies the same precision policy as the magnitude-based formatters
126
+ * via the shared internal helper: whole numbers render without a trailing decimal, fractional numbers render to one decimal place.
127
+ *
128
+ * @param value - The percentage value to convert. Treated as already-scaled into percent units (50 means 50%, not 0.5).
129
+ *
130
+ * @returns Returns the value as a human-readable string ending in `%`.
131
+ *
132
+ * @example
133
+ *
134
+ * ```ts
135
+ * formatPercent(0); // "0%".
136
+ * formatPercent(50); // "50%".
137
+ * formatPercent(100); // "100%".
138
+ * formatPercent(33.333); // "33.3%".
139
+ * ```
140
+ *
141
+ * @category Utilities
142
+ */
143
+ export function formatPercent(value) {
144
+ return formatMagnitude(value) + "%";
145
+ }
146
+ /**
147
+ * Format a second-resolution duration into a human-readable form as s, min, or hr. Same tier semantics as {@link formatMs}, scaled for inputs that arrive already
148
+ * in seconds rather than milliseconds.
149
+ *
150
+ * @param value - The duration to convert, in seconds.
151
+ *
152
+ * @returns Returns the value as a human-readable string.
153
+ *
154
+ * @example
155
+ *
156
+ * ```ts
157
+ * formatSeconds(45); // "45 s".
158
+ * formatSeconds(90); // "1.5 min".
159
+ * formatSeconds(1800); // "30 min".
160
+ * formatSeconds(5400); // "1.5 hr".
161
+ * ```
162
+ *
163
+ * @category Utilities
164
+ */
165
+ export function formatSeconds(value) {
166
+ if (value < 60) {
167
+ return value.toString() + " s";
168
+ }
169
+ if (value < 3_600) {
170
+ return formatMagnitude(value / 60) + " min";
171
+ }
172
+ return formatMagnitude(value / 3_600) + " hr";
173
+ }
174
+ //# sourceMappingURL=formatters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formatters.js","sourceRoot":"","sources":["../src/formatters.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;;;;;;;;;;GAcG;AAEH,oKAAoK;AACpK,sEAAsE;AACtE,SAAS,eAAe,CAAC,KAAa;IAEpC,OAAO,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;AACnE,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,SAAS,CAAC,KAAa;IAErC,IAAG,KAAK,GAAG,KAAK,EAAE,CAAC;QAEjB,OAAO,KAAK,CAAC,QAAQ,EAAE,GAAG,MAAM,CAAC;IACnC,CAAC;IAED,IAAG,KAAK,GAAG,SAAS,EAAE,CAAC;QAErB,OAAO,eAAe,CAAC,KAAK,GAAG,KAAK,CAAC,GAAG,OAAO,CAAC;IAClD,CAAC;IAED,OAAO,eAAe,CAAC,KAAK,GAAG,SAAS,CAAC,GAAG,OAAO,CAAC;AACtD,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,WAAW,CAAC,KAAa;IAEvC,IAAG,KAAK,GAAG,KAAK,EAAE,CAAC;QAEjB,OAAO,KAAK,CAAC,QAAQ,EAAE,GAAG,QAAQ,CAAC;IACrC,CAAC;IAED,IAAG,KAAK,GAAG,SAAS,EAAE,CAAC;QAErB,OAAO,eAAe,CAAC,KAAK,GAAG,KAAK,CAAC,GAAG,KAAK,CAAC;IAChD,CAAC;IAED,IAAG,KAAK,GAAG,aAAa,EAAE,CAAC;QAEzB,OAAO,eAAe,CAAC,KAAK,GAAG,SAAS,CAAC,GAAG,KAAK,CAAC;IACpD,CAAC;IAED,IAAG,KAAK,GAAG,iBAAiB,EAAE,CAAC;QAE7B,OAAO,eAAe,CAAC,KAAK,GAAG,aAAa,CAAC,GAAG,KAAK,CAAC;IACxD,CAAC;IAED,OAAO,eAAe,CAAC,KAAK,GAAG,iBAAiB,CAAC,GAAG,KAAK,CAAC;AAC5D,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAa;IAEpC,IAAG,KAAK,GAAG,KAAK,EAAE,CAAC;QAEjB,OAAO,KAAK,CAAC,QAAQ,EAAE,GAAG,KAAK,CAAC;IAClC,CAAC;IAED,IAAG,KAAK,GAAG,MAAM,EAAE,CAAC;QAElB,OAAO,eAAe,CAAC,KAAK,GAAG,KAAK,CAAC,GAAG,IAAI,CAAC;IAC/C,CAAC;IAED,IAAG,KAAK,GAAG,SAAS,EAAE,CAAC;QAErB,OAAO,eAAe,CAAC,KAAK,GAAG,MAAM,CAAC,GAAG,MAAM,CAAC;IAClD,CAAC;IAED,OAAO,eAAe,CAAC,KAAK,GAAG,SAAS,CAAC,GAAG,KAAK,CAAC;AACpD,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,aAAa,CAAC,KAAa;IAEzC,OAAO,eAAe,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC;AACtC,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,aAAa,CAAC,KAAa;IAEzC,IAAG,KAAK,GAAG,EAAE,EAAE,CAAC;QAEd,OAAO,KAAK,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC;IACjC,CAAC;IAED,IAAG,KAAK,GAAG,KAAK,EAAE,CAAC;QAEjB,OAAO,eAAe,CAAC,KAAK,GAAG,EAAE,CAAC,GAAG,MAAM,CAAC;IAC9C,CAAC;IAED,OAAO,eAAe,CAAC,KAAK,GAAG,KAAK,CAAC,GAAG,KAAK,CAAC;AAChD,CAAC"}