homebridge-plugin-utils 1.35.0 → 2.1.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 (204) hide show
  1. package/README.md +130 -2
  2. package/build/eslint-plugin/README.md +164 -0
  3. package/build/eslint-plugin/config.mjs +308 -0
  4. package/build/eslint-plugin/index.mjs +7 -0
  5. package/build/eslint-plugin/plugin.mjs +46 -0
  6. package/build/eslint-plugin/rules/blank-line-after-open-brace.mjs +137 -0
  7. package/build/eslint-plugin/rules/blank-line-after-open-brace.test.mjs +112 -0
  8. package/build/eslint-plugin/rules/comment-style.mjs +190 -0
  9. package/build/eslint-plugin/rules/comment-style.test.mjs +190 -0
  10. package/build/eslint-plugin/rules/enforce-node-protocol.mjs +114 -0
  11. package/build/eslint-plugin/rules/enforce-node-protocol.test.mjs +116 -0
  12. package/build/eslint-plugin/rules/paren-comparisons-in-logical.mjs +94 -0
  13. package/build/eslint-plugin/rules/paren-comparisons-in-logical.test.mjs +107 -0
  14. package/build/eslint-plugin/rules/split-type-imports.mjs +354 -0
  15. package/build/eslint-plugin/rules/split-type-imports.test.mjs +268 -0
  16. package/build/eslint-plugin/test-setup.mjs +21 -0
  17. package/build/fs-ops.mjs +184 -0
  18. package/build/tsconfig.json +18 -3
  19. package/dist/backpressure.d.ts +94 -45
  20. package/dist/backpressure.js +229 -93
  21. package/dist/backpressure.js.map +1 -1
  22. package/dist/cli/index.d.ts +166 -0
  23. package/dist/cli/index.js +551 -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/disposable-stack.d.ts +59 -0
  32. package/dist/disposable-stack.js +155 -0
  33. package/dist/disposable-stack.js.map +1 -0
  34. package/dist/docChrome.d.ts +260 -0
  35. package/dist/docChrome.js +361 -0
  36. package/dist/docChrome.js.map +1 -0
  37. package/dist/eslint-plugin/config.d.mts +193 -0
  38. package/dist/eslint-plugin/index.d.mts +2 -0
  39. package/dist/eslint-plugin/plugin.d.mts +87 -0
  40. package/dist/eslint-plugin/rules/blank-line-after-open-brace.d.mts +19 -0
  41. package/dist/eslint-plugin/rules/comment-style.d.mts +15 -0
  42. package/dist/eslint-plugin/rules/enforce-node-protocol.d.mts +18 -0
  43. package/dist/eslint-plugin/rules/paren-comparisons-in-logical.d.mts +15 -0
  44. package/dist/eslint-plugin/rules/split-type-imports.d.mts +16 -0
  45. package/dist/featureOptions-docs.d.ts +96 -0
  46. package/dist/featureOptions-docs.js +193 -0
  47. package/dist/featureOptions-docs.js.map +1 -0
  48. package/dist/featureOptions.d.ts +674 -0
  49. package/dist/featureOptions.js +870 -0
  50. package/dist/featureOptions.js.map +1 -0
  51. package/dist/ffmpeg/codecs.d.ts +256 -72
  52. package/dist/ffmpeg/codecs.js +477 -262
  53. package/dist/ffmpeg/codecs.js.map +1 -1
  54. package/dist/ffmpeg/dgram-util.d.ts +46 -0
  55. package/dist/ffmpeg/dgram-util.js +38 -0
  56. package/dist/ffmpeg/dgram-util.js.map +1 -0
  57. package/dist/ffmpeg/exec.d.ts +83 -64
  58. package/dist/ffmpeg/exec.js +77 -86
  59. package/dist/ffmpeg/exec.js.map +1 -1
  60. package/dist/ffmpeg/fmp4.d.ts +49 -2
  61. package/dist/ffmpeg/fmp4.js +47 -11
  62. package/dist/ffmpeg/fmp4.js.map +1 -1
  63. package/dist/ffmpeg/hap-enums.d.ts +214 -0
  64. package/dist/ffmpeg/hap-enums.js +92 -0
  65. package/dist/ffmpeg/hap-enums.js.map +1 -0
  66. package/dist/ffmpeg/index.d.ts +16 -9
  67. package/dist/ffmpeg/index.js +6 -0
  68. package/dist/ffmpeg/index.js.map +1 -1
  69. package/dist/ffmpeg/mp4-assembler.d.ts +161 -0
  70. package/dist/ffmpeg/mp4-assembler.js +424 -0
  71. package/dist/ffmpeg/mp4-assembler.js.map +1 -0
  72. package/dist/ffmpeg/mp4-parser.d.ts +94 -0
  73. package/dist/ffmpeg/mp4-parser.js +130 -0
  74. package/dist/ffmpeg/mp4-parser.js.map +1 -0
  75. package/dist/ffmpeg/options.d.ts +62 -149
  76. package/dist/ffmpeg/options.js +608 -499
  77. package/dist/ffmpeg/options.js.map +1 -1
  78. package/dist/ffmpeg/process.d.ts +142 -96
  79. package/dist/ffmpeg/process.js +406 -278
  80. package/dist/ffmpeg/process.js.map +1 -1
  81. package/dist/ffmpeg/record.d.ts +325 -186
  82. package/dist/ffmpeg/record.js +418 -565
  83. package/dist/ffmpeg/record.js.map +1 -1
  84. package/dist/ffmpeg/recording-process-double.d.ts +157 -0
  85. package/dist/ffmpeg/recording-process-double.js +190 -0
  86. package/dist/ffmpeg/recording-process-double.js.map +1 -0
  87. package/dist/ffmpeg/rtp-parser.d.ts +70 -0
  88. package/dist/ffmpeg/rtp-parser.js +77 -0
  89. package/dist/ffmpeg/rtp-parser.js.map +1 -0
  90. package/dist/ffmpeg/rtp.d.ts +198 -141
  91. package/dist/ffmpeg/rtp.js +474 -251
  92. package/dist/ffmpeg/rtp.js.map +1 -1
  93. package/dist/ffmpeg/settings.d.ts +5 -2
  94. package/dist/ffmpeg/settings.js +20 -5
  95. package/dist/ffmpeg/settings.js.map +1 -1
  96. package/dist/ffmpeg/stream.d.ts +57 -107
  97. package/dist/ffmpeg/stream.js +121 -150
  98. package/dist/ffmpeg/stream.js.map +1 -1
  99. package/dist/formatters.d.ts +106 -0
  100. package/dist/formatters.js +174 -0
  101. package/dist/formatters.js.map +1 -0
  102. package/dist/homebridge-enums.d.ts +30 -0
  103. package/dist/homebridge-enums.js +17 -0
  104. package/dist/homebridge-enums.js.map +1 -0
  105. package/dist/index.d.ts +13 -6
  106. package/dist/index.js +8 -2
  107. package/dist/index.js.map +1 -1
  108. package/dist/logclient/auth.d.ts +114 -0
  109. package/dist/logclient/auth.js +199 -0
  110. package/dist/logclient/auth.js.map +1 -0
  111. package/dist/logclient/cli-run.d.ts +76 -0
  112. package/dist/logclient/cli-run.js +639 -0
  113. package/dist/logclient/cli-run.js.map +1 -0
  114. package/dist/logclient/cli.d.ts +3 -0
  115. package/dist/logclient/cli.js +97 -0
  116. package/dist/logclient/cli.js.map +1 -0
  117. package/dist/logclient/client.d.ts +145 -0
  118. package/dist/logclient/client.js +600 -0
  119. package/dist/logclient/client.js.map +1 -0
  120. package/dist/logclient/config.d.ts +173 -0
  121. package/dist/logclient/config.js +199 -0
  122. package/dist/logclient/config.js.map +1 -0
  123. package/dist/logclient/endpoints.d.ts +54 -0
  124. package/dist/logclient/endpoints.js +73 -0
  125. package/dist/logclient/endpoints.js.map +1 -0
  126. package/dist/logclient/filter.d.ts +45 -0
  127. package/dist/logclient/filter.js +51 -0
  128. package/dist/logclient/filter.js.map +1 -0
  129. package/dist/logclient/frame.d.ts +93 -0
  130. package/dist/logclient/frame.js +203 -0
  131. package/dist/logclient/frame.js.map +1 -0
  132. package/dist/logclient/index.d.ts +31 -0
  133. package/dist/logclient/index.js +12 -0
  134. package/dist/logclient/index.js.map +1 -0
  135. package/dist/logclient/parser.d.ts +211 -0
  136. package/dist/logclient/parser.js +393 -0
  137. package/dist/logclient/parser.js.map +1 -0
  138. package/dist/logclient/rest.d.ts +41 -0
  139. package/dist/logclient/rest.js +111 -0
  140. package/dist/logclient/rest.js.map +1 -0
  141. package/dist/logclient/settings.d.ts +15 -0
  142. package/dist/logclient/settings.js +64 -0
  143. package/dist/logclient/settings.js.map +1 -0
  144. package/dist/logclient/socket-double.d.ts +201 -0
  145. package/dist/logclient/socket-double.js +384 -0
  146. package/dist/logclient/socket-double.js.map +1 -0
  147. package/dist/logclient/socket.d.ts +257 -0
  148. package/dist/logclient/socket.js +620 -0
  149. package/dist/logclient/socket.js.map +1 -0
  150. package/dist/logclient/stitch.d.ts +83 -0
  151. package/dist/logclient/stitch.js +146 -0
  152. package/dist/logclient/stitch.js.map +1 -0
  153. package/dist/logclient/time-expression.d.ts +42 -0
  154. package/dist/logclient/time-expression.js +181 -0
  155. package/dist/logclient/time-expression.js.map +1 -0
  156. package/dist/logclient/time-window.d.ts +38 -0
  157. package/dist/logclient/time-window.js +53 -0
  158. package/dist/logclient/time-window.js.map +1 -0
  159. package/dist/logclient/types.d.ts +107 -0
  160. package/dist/logclient/types.js +6 -0
  161. package/dist/logclient/types.js.map +1 -0
  162. package/dist/mqttClient.d.ts +287 -0
  163. package/dist/mqttClient.js +433 -0
  164. package/dist/mqttClient.js.map +1 -0
  165. package/dist/service.d.ts +64 -15
  166. package/dist/service.js +93 -66
  167. package/dist/service.js.map +1 -1
  168. package/dist/ui/featureOptions.js +870 -0
  169. package/dist/ui/featureOptions.js.map +1 -0
  170. package/dist/ui/formatters.js +174 -0
  171. package/dist/ui/formatters.js.map +1 -0
  172. package/dist/ui/pluginConfigSession.mjs +141 -0
  173. package/dist/ui/webUi-featureOptions/categoryState.mjs +135 -0
  174. package/dist/ui/webUi-featureOptions/effects/keyboard.mjs +61 -0
  175. package/dist/ui/webUi-featureOptions/effects/persist.mjs +226 -0
  176. package/dist/ui/webUi-featureOptions/effects/theme.mjs +398 -0
  177. package/dist/ui/webUi-featureOptions/effects/tokens.mjs +152 -0
  178. package/dist/ui/webUi-featureOptions/rendering.mjs +431 -0
  179. package/dist/ui/webUi-featureOptions/selectors.mjs +360 -0
  180. package/dist/ui/webUi-featureOptions/state.mjs +319 -0
  181. package/dist/ui/webUi-featureOptions/store.mjs +181 -0
  182. package/dist/ui/webUi-featureOptions/utils.mjs +213 -0
  183. package/dist/ui/webUi-featureOptions/views/connectionError.mjs +152 -0
  184. package/dist/ui/webUi-featureOptions/views/deviceInfo.mjs +80 -0
  185. package/dist/ui/webUi-featureOptions/views/header.mjs +77 -0
  186. package/dist/ui/webUi-featureOptions/views/nav.mjs +341 -0
  187. package/dist/ui/webUi-featureOptions/views/options.mjs +521 -0
  188. package/dist/ui/webUi-featureOptions/views/search.mjs +395 -0
  189. package/dist/ui/webUi-featureOptions.mjs +702 -0
  190. package/dist/ui/webUi.mjs +225 -88
  191. package/dist/util.d.ts +679 -45
  192. package/dist/util.js +830 -77
  193. package/dist/util.js.map +1 -1
  194. package/package.json +33 -15
  195. package/build/eslint-rules.mjs +0 -511
  196. package/dist/featureoptions.d.ts +0 -264
  197. package/dist/featureoptions.js +0 -480
  198. package/dist/featureoptions.js.map +0 -1
  199. package/dist/mqttclient.d.ts +0 -178
  200. package/dist/mqttclient.js +0 -310
  201. package/dist/mqttclient.js.map +0 -1
  202. package/dist/ui/featureoptions.js +0 -480
  203. package/dist/ui/featureoptions.js.map +0 -1
  204. package/dist/ui/webUi-featureoptions.mjs +0 -3765
@@ -1,35 +1,66 @@
1
1
  /* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
2
2
  *
3
- * ffmpeg/record.ts: Provide FFmpeg process control to support livestreaming and HomeKit Secure Video.
3
+ * ffmpeg/record.ts: FFmpeg process control for fMP4 recording and livestreaming, composing Mp4SegmentAssembler for the shared pipeline.
4
4
  */
5
+ /**
6
+ * fMP4 FFmpeg processes for HomeKit Secure Video (HKSV) events and livestreaming.
7
+ *
8
+ * This module defines an abstract base {@link FfmpegFMp4Process} and its concrete fMP4-mode specializations,
9
+ * {@link FfmpegRecordingProcess} for HKSV event recording (stdin pipe input, transcoded output) and
10
+ * {@link FfmpegLivestreamProcess} for fMP4 livestreaming (RTSP input, codec copy). The base exists **solely to centralize
11
+ * composition wiring** - it owns the internal {@link Mp4SegmentAssembler}, delegates `getInitSegment` / `segments` to it,
12
+ * and propagates assembler teardown reasons up to the process. It contains no pipeline logic and no command-line assembly;
13
+ * the byte-to-segment pipeline lives in the assembler so this base stays composition-only.
14
+ *
15
+ * Both concrete subclasses:
16
+ *
17
+ * - Spawn FFmpeg on construction and expose the inherited `signal`, `ready`, `exited`, `stdin`, `stderr`, `stderrLog`,
18
+ * `abort()`, and `[Symbol.asyncDispose]`.
19
+ * - Narrow the inherited public `stdout` to `never`, because the assembler owns the stream and a concurrent external reader
20
+ * would race.
21
+ * - Build their FFmpeg arg vector via the pure helper `buildFMp4CommandLine`, which takes fully-resolved options plus
22
+ * mode-specific hook values and returns the vector. Neither subclass calls `super()` until the arg vector is finalized,
23
+ * so the constructor-before-super contract is respected.
24
+ *
25
+ * The command-line hook values that differ per mode:
26
+ *
27
+ * | Hook | Recording | Livestream |
28
+ * |--------------------------|--------------------------------------------|-----------------------------------------|
29
+ * | `inputArgs` | `-i pipe:0` + probesize + `-ss` | `-i <url>` + `-rtsp_transport tcp` |
30
+ * | `separateAudioInputArgs` | `[]` | Separate audio URL when configured |
31
+ * | `audioInputIndex` | `0` | `0` or `1` (if separate audio) |
32
+ * | `audioTarget` | `recordingConfig.audioCodec` (transcoding) | `init.audio` when provided |
33
+ * | `videoEncoderArgs` | `options.recordEncoder(...)` | `-codec:v copy` |
34
+ * | `postFilterArgs` | `[]` | `-frag_duration <segmentLength * 1000>` |
35
+ * | `metadataLabel` | `"HKSV Event"` | `"Livestream Buffer"` |
36
+ *
37
+ * The shared pipeline primitive ({@link Mp4SegmentAssembler}) also means this module avoids template-method coupling between
38
+ * the base and the concrete subclasses: no abstract hook methods, no mode-specific state on the base. The base is pure lifecycle
39
+ * composition; the subclasses are pure args builders plus (for recording) a known-error message substitution.
40
+ *
41
+ * @module
42
+ */
43
+ import { AudioRecordingCodecType, AudioRecordingSamplerate } from "./hap-enums.js";
5
44
  import { HKSV_IDR_INTERVAL, HKSV_TIMEOUT } from "./settings.js";
6
- import { runWithTimeout } from "../util.js";
7
- import { BOX_HEADER_SIZE } from "./fmp4.js";
45
+ import { isHbpuAbortReason, onAbort } from "../util.js";
8
46
  import { FfmpegProcess } from "./process.js";
9
- import { once } from "node:events";
10
- // Utility to map HKSV audio recording codec types to their AAC Object Type identifiers. We also use satisfies here to ensure we account for any future changes that
11
- // would require updating this mapping.
47
+ import { Mp4SegmentAssembler } from "./mp4-assembler.js";
48
+ // Translation tables for the hap-nodejs enum values that shape the FFmpeg audio encoder args. `as const satisfies` pins the map shape to exhaustively cover every enum
49
+ // member at compile time - a new enum value added upstream would require updating the maps or the build breaks.
12
50
  const translateAudioRecordingCodecType = {
13
- [1 /* AudioRecordingCodecType.AAC_ELD */]: "38",
14
- [0 /* AudioRecordingCodecType.AAC_LC */]: "1"
51
+ [AudioRecordingCodecType.AAC_ELD]: "38",
52
+ [AudioRecordingCodecType.AAC_LC]: "1"
15
53
  };
16
- // Utility to map audio sample rates to strings. We also use satisfies here to ensure we account for any future changes that would require updating this mapping.
17
54
  const translateAudioSampleRate = {
18
- [0 /* AudioRecordingSamplerate.KHZ_8 */]: "8",
19
- [1 /* AudioRecordingSamplerate.KHZ_16 */]: "16",
20
- [2 /* AudioRecordingSamplerate.KHZ_24 */]: "24",
21
- [3 /* AudioRecordingSamplerate.KHZ_32 */]: "32",
22
- [4 /* AudioRecordingSamplerate.KHZ_44_1 */]: "44.1",
23
- [5 /* AudioRecordingSamplerate.KHZ_48 */]: "48"
55
+ [AudioRecordingSamplerate.KHZ_8]: "8",
56
+ [AudioRecordingSamplerate.KHZ_16]: "16",
57
+ [AudioRecordingSamplerate.KHZ_24]: "24",
58
+ [AudioRecordingSamplerate.KHZ_32]: "32",
59
+ [AudioRecordingSamplerate.KHZ_44_1]: "44.1",
60
+ [AudioRecordingSamplerate.KHZ_48]: "48"
24
61
  };
25
- // ISO BMFF box type constants encoded as 32-bit integers for comparison without string allocation in the box-parsing hot path.
26
- const BOX_TYPE_MDAT = 0x6D646174;
27
- const BOX_TYPE_MOOF = 0x6D6F6F66;
28
- const BOX_TYPE_MOOV = 0x6D6F6F76;
29
- // Reusable empty buffer sentinel for the box-parsing loop. Avoids repeated zero-byte allocations on every box reset.
30
- const EMPTY_BUFFER = Buffer.alloc(0);
31
- // Known HKSV-related errors due to occasional inconsistencies produced by the input stream and FFmpeg's own occasional quirkiness. Compiled once at module scope
32
- // rather than on every error event.
62
+ // Known HKSV-related errors due to occasional inconsistencies produced by the input stream and FFmpeg's own occasional quirkiness. Compiled once at module scope rather
63
+ // than on every teardown event.
33
64
  const FFMPEG_KNOWN_HKSV_ERROR = new RegExp([
34
65
  "(Cannot determine format of input stream 0:0 after EOF)",
35
66
  "(Could not write header \\(incorrect codec parameters \\?\\): Broken pipe)",
@@ -39,607 +70,429 @@ const FFMPEG_KNOWN_HKSV_ERROR = new RegExp([
39
70
  "(Invalid data found when processing input)",
40
71
  "(moov atom not found)"
41
72
  ].join("|"));
42
- /**
43
- * Abstract base class for fMP4 FFmpeg processes. Owns the shared command line skeleton (preamble, video mapping, movflags, audio encoding, output format) and the fMP4
44
- * box-parsing loop. Subclasses provide mode-specific pieces (input args, encoder selection, box handling) via protected hook methods, following the template method
45
- * pattern.
46
- *
47
- * @see FfmpegRecordingProcess
48
- * @see FfmpegLivestreamProcess
49
- * @see FfmpegProcess
50
- * @see {@link https://ffmpeg.org/ffmpeg.html | FFmpeg Documentation}
51
- */
52
- class FfmpegFMp4Process extends FfmpegProcess {
53
- isLoggingErrors;
54
- // The HomeKit recording configuration and resolved base options are stored as protected fields so subclass hook methods can reference them without needing their own
55
- // copies of the shared state.
56
- fMp4Options;
57
- recordingConfig;
58
- /**
59
- * Constructs a new fMP4 FFmpeg process. Stores shared state and applies defaults to the base options. The command line is not assembled here...subclasses call
60
- * `buildCommandLine()` after their own initialization to trigger the template method assembly.
61
- *
62
- * @param ffmpegOptions - FFmpeg configuration options.
63
- * @param recordingConfig - HomeKit recording configuration for the session.
64
- * @param fMp4Options - Partial base options with defaults applied for any unset fields.
65
- * @param isVerbose - If `true`, enables more verbose logging for debugging purposes. Defaults to `false`.
66
- */
67
- constructor(ffmpegOptions, recordingConfig, fMp4Options = {}, isVerbose = false) {
68
- // Initialize our parent.
69
- super(ffmpegOptions);
70
- // We want to log errors when they occur.
71
- this.isLoggingErrors = true;
72
- // Store the recording configuration for use by subclass hook methods.
73
- this.recordingConfig = recordingConfig;
74
- // Apply defaults to the base options and store them. Subclasses store their own mode-specific options separately.
75
- this.fMp4Options = {
76
- audioFilters: fMp4Options.audioFilters ?? [],
77
- audioStream: fMp4Options.audioStream ?? 0,
78
- codec: fMp4Options.codec ?? "h264",
79
- enableAudio: fMp4Options.enableAudio ?? true,
80
- hardwareDecoding: fMp4Options.hardwareDecoding ?? (this.options.codecSupport.ffmpegVersion.startsWith("8.") ? this.options.config.hardwareDecoding : false),
81
- hardwareTranscoding: fMp4Options.hardwareTranscoding ?? this.options.config.hardwareTranscoding,
82
- transcodeAudio: fMp4Options.transcodeAudio ?? true,
83
- videoFilters: fMp4Options.videoFilters ?? [],
84
- videoStream: fMp4Options.videoStream ?? 0
85
- };
86
- // Store the verbose flag for use during command line assembly. We don't build the command line here...subclasses call buildCommandLine() after initializing their
87
- // own state, which avoids the virtual-call-from-constructor problem.
88
- this._isVerbose = isVerbose;
73
+ // URL prefixes whose presence in a separate-audio-input URL means we should explicitly request TCP transport, matching the behavior of the primary video input for RTSP
74
+ // sources. Module-scope constant - shape never changes, allocated once.
75
+ const RTSP_TRANSPORT_PATTERNS = ["rtsp://", "rtsps://"];
76
+ // Apply defaults to a partial base options object. Hardware decoding defaults vary with FFmpeg version (only the 8.x line supports it stably); hardware transcoding
77
+ // follows the caller's resolved config.
78
+ function resolveBaseOptions(options, partial) {
79
+ return {
80
+ audioFilters: partial.audioFilters ?? [],
81
+ audioStream: partial.audioStream ?? 0,
82
+ codec: partial.codec ?? "h264",
83
+ enableAudio: partial.enableAudio ?? true,
84
+ hardwareDecoding: partial.hardwareDecoding ?? (options.config.codecSupport.ffmpegAtLeast(8) ? options.config.hardwareDecoding : false),
85
+ hardwareTranscoding: partial.hardwareTranscoding ?? options.config.hardwareTranscoding,
86
+ transcodeAudio: partial.transcodeAudio ?? true,
87
+ videoFilters: partial.videoFilters ?? [],
88
+ videoStream: partial.videoStream ?? 0
89
+ };
90
+ }
91
+ // Assemble the FFmpeg command line for any fMP4 subclass. The builder consumes pre-resolved hook values supplied by the caller; subclass constructors resolve their
92
+ // hooks inline against their own init and FfmpegOptions, then invoke this function once to produce the final arg vector.
93
+ function buildFMp4CommandLine(input) {
94
+ const { audioInputIndex, audioTarget, fMp4Options, inputArgs, metadataLabel, options, postFilterArgs, separateAudioInputArgs, verbose, videoEncoderArgs } = input;
95
+ // Configure our video parameters for our input:
96
+ //
97
+ // -hide_banner Suppress printing the startup banner in FFmpeg.
98
+ // -nostats Suppress printing progress reports while encoding in FFmpeg.
99
+ // -fflags flags Set the format flags to discard any corrupt packets rather than exit.
100
+ // -err_detect ignore_err Ignore decoding errors and continue rather than exit.
101
+ // -max_delay 500000 Set an upper limit on how much time FFmpeg can take in demuxing packets, in microseconds.
102
+ const args = [
103
+ "-hide_banner",
104
+ "-nostats",
105
+ "-fflags", "+discardcorrupt",
106
+ "-err_detect", "ignore_err",
107
+ ...options.videoDecoder(fMp4Options.codec),
108
+ "-max_delay", "500000",
109
+ // Mode-specific input arguments (RTSP input for livestream, stdin pipe for recording).
110
+ ...inputArgs,
111
+ // Mode-specific separate audio input arguments (livestream with a separate audio endpoint, empty for recording).
112
+ ...separateAudioInputArgs,
113
+ // Select the video track and apply mode-specific encoder arguments.
114
+ "-map", "0:v:" + fMp4Options.videoStream.toString(),
115
+ ...videoEncoderArgs
116
+ ];
117
+ // Configure our video filters, if we have them.
118
+ if (fMp4Options.videoFilters.length) {
119
+ args.push("-filter:v", fMp4Options.videoFilters.join(", "));
89
120
  }
90
- // Per-instance verbose flag, distinct from the inherited isVerbose which reflects the global codecSupport.verbose setting. We keep both so either a global debug
91
- // setting or a per-session opt-in can enable verbose FFmpeg logging...the check in buildCommandLine() ORs them together.
92
- _isVerbose;
93
- // Assembles the FFmpeg command line by calling hook methods in the standard order. The shared skeleton lives here; mode-specific pieces come from subclass overrides.
94
- // Subclasses call this as the last step of their constructor, after their own state is fully initialized.
95
- buildCommandLine() {
96
- // Configure our video parameters for our input:
97
- //
98
- // -hide_banner Suppress printing the startup banner in FFmpeg.
99
- // -nostats Suppress printing progress reports while encoding in FFmpeg.
100
- // -fflags flags Set the format flags to discard any corrupt packets rather than exit.
101
- // -err_detect ignore_err Ignore decoding errors and continue rather than exit.
102
- // -max_delay 500000 Set an upper limit on how much time FFmpeg can take in demuxing packets, in microseconds.
103
- this.commandLineArgs = [
104
- "-hide_banner",
105
- "-nostats",
106
- "-fflags", "+discardcorrupt",
107
- "-err_detect", "ignore_err",
108
- ...this.options.videoDecoder(this.fMp4Options.codec),
109
- "-max_delay", "500000",
110
- // Mode-specific input arguments (RTSP input for livestream, stdin pipe for recording).
111
- ...this.inputArgs(),
112
- // Mode-specific separate audio input arguments (livestream with a separate audio endpoint, empty for recording).
113
- ...this.separateAudioInputArgs()
114
- ];
115
- // Configure our recording options for the video stream:
121
+ // Mode-specific post-filter arguments (frag_duration for livestream, empty for recording).
122
+ args.push(...postFilterArgs);
123
+ // -movflags flags In the generated fMP4 stream: set the default-base-is-moof flag in the header, write an initial empty MOOV box, start a new fragment
124
+ // at each keyframe, skip creating a segment index (SIDX) box in fragments, and skip writing the final MOOV trailer since it is unneeded.
125
+ // -flush_packets 1 Ensure we flush our write buffer after each muxed packet.
126
+ // -reset_timestamps Reset timestamps at the beginning of each segment.
127
+ // -metadata Set the metadata to the name of the camera to distinguish between FFmpeg sessions.
128
+ args.push("-movflags", "default_base_moof+empty_moov+frag_keyframe+skip_sidx+skip_trailer", "-flush_packets", "1", "-reset_timestamps", "1", "-metadata", "comment=" + options.name() + " " + metadataLabel);
129
+ // Assemble the audio encoding block. This is shared between both modes - the only mode-specific piece is which FFmpeg input index carries the audio stream.
130
+ if (fMp4Options.enableAudio) {
131
+ // Configure the audio portion of the command line:
116
132
  //
117
- // -map 0:v:X Selects the video track from the input.
118
- this.commandLineArgs.push("-map", "0:v:" + this.fMp4Options.videoStream.toString(),
119
- // Mode-specific video encoder arguments (copy for livestream, recordEncoder for recording).
120
- ...this.videoEncoderArgs());
121
- // Configure our video filters, if we have them.
122
- if (this.fMp4Options.videoFilters.length) {
123
- this.commandLineArgs.push("-filter:v", this.fMp4Options.videoFilters.join(", "));
124
- }
125
- // Mode-specific post-filter arguments (frag_duration for livestream, empty for recording).
126
- this.commandLineArgs.push(...this.postFilterArgs());
127
- // -movflags flags In the generated fMP4 stream: set the default-base-is-moof flag in the header, write an initial empty MOOV box, start a new fragment
128
- // at each keyframe, skip creating a segment index (SIDX) box in fragments, and skip writing the final MOOV trailer since it's unneeded.
129
- // -flush_packets 1 Ensure we flush our write buffer after each muxed packet.
130
- // -reset_timestamps Reset timestamps at the beginning of each segment.
131
- // -metadata Set the metadata to the name of the camera to distinguish between FFmpeg sessions.
132
- this.commandLineArgs.push("-movflags", "default_base_moof+empty_moov+frag_keyframe+skip_sidx+skip_trailer", "-flush_packets", "1", "-reset_timestamps", "1", "-metadata", "comment=" + this.options.name() + " " + this.metadataLabel());
133
- // Assemble the audio encoding block. This is shared between both modes...the only mode-specific piece is which FFmpeg input index carries the audio stream.
134
- let transcodeAudio = this.fMp4Options.transcodeAudio;
135
- if (this.fMp4Options.enableAudio) {
136
- // Configure the audio portion of the command line. Options we use are:
137
- //
138
- // -map N:a:X? Selects the audio stream from input N, if it exists. The input index is 0 when audio and video share the same input, or 1 when a
139
- // separate audio input has been configured.
140
- this.commandLineArgs.push("-map", this.audioInputIndex().toString() + ":a:" + this.fMp4Options.audioStream.toString() + "?");
141
- // Configure our audio filters, if we have them.
142
- if (this.fMp4Options.audioFilters.length) {
143
- this.commandLineArgs.push("-filter:a", this.fMp4Options.audioFilters.join(", "));
144
- // Audio filters require transcoding. If the user has decided to filter, we enforce this requirement even if they wanted to copy the audio stream.
145
- transcodeAudio = true;
146
- }
147
- if (transcodeAudio) {
148
- // Configure the audio portion of the command line. Options we use are:
149
- //
150
- // -codec:a Encode using the codecs available to us on given platforms.
151
- // -profile:a Specify either low-complexity AAC or enhanced low-delay AAC for HKSV events.
152
- // -ar samplerate Sample rate to use for this audio. This is specified by HKSV.
153
- // -ac number Set the number of audio channels.
154
- this.commandLineArgs.push(...this.options.audioEncoder({ codec: this.recordingConfig.audioCodec.type }), "-profile:a", translateAudioRecordingCodecType[this.recordingConfig.audioCodec.type], "-ar", translateAudioSampleRate[this.recordingConfig.audioCodec.samplerate] + "k", "-ac", (this.recordingConfig.audioCodec.audioChannels ?? 1).toString());
155
- }
156
- else {
157
- // Configure the audio portion of the command line. Options we use are:
158
- //
159
- // -codec:a copy Copy the audio stream, since it's already in AAC.
160
- this.commandLineArgs.push("-codec:a", "copy");
133
+ // -map N:a:X? Selects the audio stream from input N, if it exists. The input index is 0 when audio and video share the same input, or 1 when a
134
+ // separate audio input has been configured.
135
+ args.push("-map", audioInputIndex.toString() + ":a:" + fMp4Options.audioStream.toString() + "?");
136
+ // A resolved audio target is the single signal to transcode: its presence means we encode to it, its absence means we copy the already-encoded audio through
137
+ // untouched. The target carries its own filters, so filtering-without-transcoding is unrepresentable here by construction rather than enforced by a runtime override.
138
+ if (audioTarget) {
139
+ // Audio filters ride inside the transcode request. When the target supplies them, we apply them ahead of the encoder.
140
+ if (audioTarget.filters?.length) {
141
+ args.push("-filter:a", audioTarget.filters.join(", "));
161
142
  }
143
+ // Configure the audio portion of the command line:
144
+ //
145
+ // -codec:a Encode using the codecs available to us on given platforms.
146
+ // -profile:a Specify either low-complexity AAC or enhanced low-delay AAC per the resolved audio target's codec.
147
+ // -ar samplerate Sample rate to use for this audio, per the resolved audio target.
148
+ // -ac number Set the number of audio channels.
149
+ args.push(...options.audioEncoder({ codec: audioTarget.codec }), "-profile:a", translateAudioRecordingCodecType[audioTarget.codec], "-ar", translateAudioSampleRate[audioTarget.samplerate] + "k", "-ac", (audioTarget.channels ?? 1).toString());
162
150
  }
163
- // Configure our video parameters for outputting our final stream:
164
- //
165
- // -f mp4 Tell ffmpeg that it should create an MP4-encoded output stream.
166
- // pipe:1 Output the stream to standard output.
167
- this.commandLineArgs.push("-f", "mp4", "pipe:1");
168
- // Additional logging, but only if we're debugging.
169
- if (this._isVerbose || this.isVerbose) {
170
- this.commandLineArgs.unshift("-loglevel", "level+verbose");
151
+ else {
152
+ // -codec:a copy Copy the audio stream through untouched when no transcode target is supplied.
153
+ args.push("-codec:a", "copy");
171
154
  }
172
155
  }
173
- /**
174
- * Prepares and configures the FFmpeg process for reading and parsing output fMP4 data. The box parsing loop is shared...each complete box is dispatched to the
175
- * subclass via handleParsedBox().
176
- */
177
- configureProcess() {
178
- let dataListener;
179
- // Call our parent to get started.
180
- super.configureProcess();
181
- // Initialize our variables that we need to process incoming FFmpeg packets.
182
- let header = EMPTY_BUFFER;
183
- let bufferRemaining = EMPTY_BUFFER;
184
- let dataLength = 0;
185
- let type = 0;
186
- // Process FFmpeg output and parse out the fMP4 stream it's generating. Here, we take on the task of parsing the fMP4 stream that's being generated and split it up
187
- // into the MP4 boxes that HAP-NodeJS is ultimately expecting.
188
- this.process?.stdout.on("data", dataListener = (buffer) => {
189
- // If we have anything left from the last buffer we processed, prepend it to this buffer.
190
- if (bufferRemaining.length > 0) {
191
- buffer = Buffer.concat([bufferRemaining, buffer]);
192
- bufferRemaining = EMPTY_BUFFER;
193
- }
194
- let offset = 0;
195
- // The MP4 container format is well-documented and designed around the concept of boxes. A box (or atom as they used to be called) is at the center of an MP4
196
- // container. It's composed of an 8-byte header, followed by the data payload it carries.
197
- for (;;) {
198
- let data;
199
- // No existing header, let's start a new box.
200
- if (!header.length) {
201
- // If there aren't enough bytes for a complete box header, save them for the next chunk.
202
- if (buffer.length < BOX_HEADER_SIZE) {
203
- bufferRemaining = buffer;
204
- break;
205
- }
206
- // Grab the header. The first four bytes represents the length of the entire box. Second four bytes represent the box type.
207
- header = buffer.subarray(0, BOX_HEADER_SIZE);
208
- // Now we retrieve the length of the box.
209
- dataLength = header.readUInt32BE(0);
210
- // Read the box type as a 32-bit integer to avoid per-box string allocation. Box types are 4-byte ASCII codes ("moof", "mdat", etc.) - a legacy of Apple's
211
- // original QuickTime "atoms" from 1991, carried forward when MPEG-4 Part 12 standardized the container as ISO BMFF and renamed atoms to "boxes."
212
- type = header.readUInt32BE(4);
213
- // Finally, we get the data portion of the box.
214
- data = buffer.subarray(BOX_HEADER_SIZE, dataLength);
215
- // Mark our data offset so we account for the length of the data header and subtract it from the overall length to capture just the data portion.
216
- dataLength -= offset = BOX_HEADER_SIZE;
217
- }
218
- else {
219
- // Grab the data from our buffer.
220
- data = buffer.subarray(0, dataLength);
221
- offset = 0;
222
- }
223
- // If we don't have enough data in this buffer, save what we have for the next buffer we see and append it there.
224
- if (data.length < dataLength) {
225
- bufferRemaining = data;
226
- break;
227
- }
228
- // Dispatch the complete box to the subclass for mode-specific handling.
229
- this.handleParsedBox(header, data, dataLength, type);
230
- // Prepare to start a new box for the next buffer that we will be processing.
231
- header = EMPTY_BUFFER;
232
- type = 0;
233
- // We've parsed an entire box, and there's no more data in this buffer to parse.
234
- if (buffer.length === (offset + dataLength)) {
235
- dataLength = 0;
236
- break;
237
- }
238
- // If there's anything left in the buffer, move us to the new box and let's keep iterating.
239
- buffer = buffer.subarray(offset + dataLength);
240
- dataLength = 0;
241
- }
242
- });
243
- // Make sure we cleanup our listeners when we're done.
244
- this.process?.once("exit", () => {
245
- this.process?.stdout.off("data", dataListener);
246
- });
156
+ // Configure our video parameters for outputting our final stream:
157
+ //
158
+ // -f mp4 Tell ffmpeg that it should create an MP4-encoded output stream.
159
+ // pipe:1 Output the stream to standard output.
160
+ args.push("-f", "mp4", "pipe:1");
161
+ // Additional logging, but only if we're debugging.
162
+ if (verbose || options.config.codecSupport.verbose) {
163
+ args.unshift("-loglevel", "level+verbose");
247
164
  }
248
- /**
249
- * Stops the FFmpeg process and performs cleanup. Subclasses override this to emit mode-specific events before calling super, which handles the shared teardown and
250
- * emits the "close" event.
251
- */
252
- stopProcess() {
253
- // Call our parent to get started.
254
- super.stopProcess();
255
- // Signal that the process has ended.
256
- this._isEnded = true;
257
- this.emit("close");
165
+ return args;
166
+ }
167
+ // Build the separate-audio-input arg segment for livestream sessions and report whether a separate input was actually configured. Returns both results together as a
168
+ // named pair because the input index in the subsequent audio mapping step depends on whether a separate input exists - the two results must stay consistent.
169
+ function buildLivestreamAudioInputArgs(livestream, enableAudio) {
170
+ if (!enableAudio || !livestream.audioInput) {
171
+ return { args: [], hasSeparateInput: false };
258
172
  }
259
- /**
260
- * Stops the FFmpeg process and logs errors if specified.
261
- *
262
- * @param logErrors - If `true`, logs FFmpeg errors. Defaults to the internal process logging state.
263
- *
264
- * @example
265
- *
266
- * ```ts
267
- * process.stop();
268
- * ```
269
- */
270
- stop(logErrors = this.isLoggingErrors) {
271
- const savedLogErrors = this.isLoggingErrors;
272
- // Flag whether we should log abnormal exits (e.g. being killed) or not.
273
- this.isLoggingErrors = logErrors;
274
- // Call our parent to finish the job.
275
- super.stop();
276
- // Restore our previous logging state.
277
- this.isLoggingErrors = savedLogErrors;
173
+ const args = [];
174
+ // Normalize the audio input configuration. A plain string is treated as a URL shorthand.
175
+ const audioInput = (typeof livestream.audioInput === "string") ? { url: livestream.audioInput } : livestream.audioInput;
176
+ // When a raw audio format is specified, we need to explicitly tell FFmpeg how to interpret the incoming stream since it cannot probe raw audio sources.
177
+ //
178
+ // -f format Specify the raw audio format (e.g., mulaw, alaw, s16le).
179
+ // -ar sampleRate Specify the audio sample rate in Hz.
180
+ // -ac channels Specify the number of audio channels.
181
+ if (audioInput.format) {
182
+ args.push("-f", audioInput.format, "-ar", (audioInput.sampleRate ?? 8000).toString(), "-ac", (audioInput.channels ?? 1).toString());
278
183
  }
279
- /**
280
- * Logs errors from FFmpeg process execution, handling known benign HKSV stream errors gracefully.
281
- *
282
- * @param exitCode - The exit code from the FFmpeg process.
283
- * @param signal - The signal (if any) used to terminate the process.
284
- */
285
- logFfmpegError(exitCode, signal) {
286
- // If we're ignoring errors, we're done.
287
- if (!this.isLoggingErrors) {
184
+ // For RTSP and RTSPS audio sources, we explicitly request TCP transport to match the behavior we use for the primary video input.
185
+ if (RTSP_TRANSPORT_PATTERNS.some((protocol) => audioInput.url.toLowerCase().startsWith(protocol))) {
186
+ args.push("-rtsp_transport", "tcp");
187
+ }
188
+ // -i url Audio input URL.
189
+ args.push("-i", audioInput.url);
190
+ return { args, hasSeparateInput: true };
191
+ }
192
+ // Compose the command line for a recording session. Extracted so the subclass constructor can compute its full arg vector ahead of the super() call.
193
+ function buildRecordingCommandLine(options, init) {
194
+ const recording = init.recording ?? {};
195
+ const fMp4Options = resolveBaseOptions(options, recording);
196
+ const { recordingConfig } = init;
197
+ const fps = recording.fps ?? 30;
198
+ // Recording transcodes audio to the HKSV-selected codec whenever transcoding is requested or an audio filter forces it; otherwise the already-encoded audio is copied
199
+ // through untouched. The filters ride inside the target, so the filters-require-transcoding rule is expressed by the target's presence rather than a runtime override.
200
+ const audioTarget = (fMp4Options.transcodeAudio || (fMp4Options.audioFilters.length > 0)) ? {
201
+ channels: recordingConfig.audioCodec.audioChannels,
202
+ codec: recordingConfig.audioCodec.type,
203
+ filters: fMp4Options.audioFilters,
204
+ // homebridge types `audioCodec.samplerate` more loosely than our local enum, but HKSV only ever supplies a valid `AudioRecordingSamplerate` member, so the `as`
205
+ // narrows a known-good value into the exhaustive translation table.
206
+ samplerate: recordingConfig.audioCodec.samplerate
207
+ } : undefined;
208
+ // The default mirrors FFmpeg's own default probesize and is sufficient to discover the fMP4 stream's parameters before decoding begins.
209
+ const probesize = recording.probesize ?? 5_000_000;
210
+ const timeshift = recording.timeshift ?? 0;
211
+ // Recording input: read fMP4 data from standard input with low-delay optimizations and an optional timeshift for HKSV event alignment.
212
+ //
213
+ // -flags low_delay Tell FFmpeg to optimize for low delay / realtime decoding.
214
+ // -probesize number How many bytes should be analyzed for stream information.
215
+ // -f mp4 Tell FFmpeg that it should expect an MP4-encoded input stream.
216
+ // -i pipe:0 Use standard input to get video data.
217
+ // -ss Fast-forward to where HKSV is expecting us to be for a recording event.
218
+ const inputArgs = [
219
+ "-flags", "low_delay",
220
+ "-probesize", probesize.toString(),
221
+ "-f", "mp4",
222
+ "-i", "pipe:0",
223
+ "-ss", timeshift.toString() + "ms"
224
+ ];
225
+ // Recordings transcode video using the platform-appropriate encoder for HKSV.
226
+ const videoEncoderArgs = options.recordEncoder({
227
+ bitrate: recordingConfig.videoCodec.parameters.bitRate,
228
+ fps: recordingConfig.videoCodec.resolution[2],
229
+ hardwareDecoding: fMp4Options.hardwareDecoding,
230
+ hardwareTranscoding: fMp4Options.hardwareTranscoding,
231
+ height: recordingConfig.videoCodec.resolution[1],
232
+ idrInterval: HKSV_IDR_INTERVAL,
233
+ inputFps: fps,
234
+ level: recordingConfig.videoCodec.parameters.level,
235
+ profile: recordingConfig.videoCodec.parameters.profile,
236
+ width: recordingConfig.videoCodec.resolution[0]
237
+ });
238
+ return buildFMp4CommandLine({
239
+ audioInputIndex: 0,
240
+ audioTarget,
241
+ fMp4Options,
242
+ inputArgs,
243
+ metadataLabel: "HKSV Event",
244
+ options,
245
+ postFilterArgs: [],
246
+ separateAudioInputArgs: [],
247
+ verbose: init.verbose ?? false,
248
+ videoEncoderArgs
249
+ });
250
+ }
251
+ // Compose the command line for a livestream session. Extracted so the subclass constructor can compute its full arg vector ahead of the super() call.
252
+ function buildLivestreamCommandLine(options, init) {
253
+ const fMp4Options = resolveBaseOptions(options, init.livestream);
254
+ const { audio, livestream } = init;
255
+ // Livestream input: connect to an RTSP source with direct I/O and TCP transport.
256
+ //
257
+ // -avioflags direct Tell FFmpeg to minimize buffering to reduce latency for more realtime processing.
258
+ // -rtsp_transport tcp Tell the RTSP stream handler that we're looking for a TCP connection.
259
+ // -i url RTSPS URL to get our input stream from.
260
+ const inputArgs = [
261
+ "-avioflags", "direct",
262
+ "-rtsp_transport", "tcp",
263
+ "-i", livestream.url
264
+ ];
265
+ // If a separate audio input has been configured, build the FFmpeg input arguments for it. The returned `hasSeparateInput` flag drives the subsequent audio-mapping
266
+ // step so the primary/secondary input index selection stays consistent with the configured inputs.
267
+ const separateAudio = buildLivestreamAudioInputArgs(livestream, fMp4Options.enableAudio);
268
+ // Livestreams remux the video stream directly without transcoding.
269
+ const videoEncoderArgs = ["-codec:v", "copy"];
270
+ // Livestreams emit fMP4 fragments at the configured cadence. `segmentLength` is expressed in milliseconds; FFmpeg wants microseconds. Default to one second when the
271
+ // caller does not specify an override, since one-second fragments match HomeKit's expected fMP4 fragment cadence for livestreaming consumers.
272
+ const segmentLengthMs = init.segmentLength ?? 1000;
273
+ const postFilterArgs = ["-frag_duration", (segmentLengthMs * 1000).toString()];
274
+ return buildFMp4CommandLine({
275
+ audioInputIndex: separateAudio.hasSeparateInput ? 1 : 0,
276
+ audioTarget: audio,
277
+ fMp4Options,
278
+ inputArgs,
279
+ metadataLabel: "Livestream Buffer",
280
+ options,
281
+ postFilterArgs,
282
+ separateAudioInputArgs: separateAudio.args,
283
+ verbose: init.verbose ?? false,
284
+ videoEncoderArgs
285
+ });
286
+ }
287
+ // Propagate assembler aborts up to the process when the assembler ends its life for a reason the process's own exit handler cannot recover. The assembler dispatches
288
+ // on a two-way branch: the named reason `"closed"` (source ended naturally) defers to the process's own exit path, while every other reason - `"timeout"`
289
+ // (inter-segment watchdog), `"failed"` (source stream error), or any reason supplied to an external `abort()` call - propagates so the FFmpeg child is actively torn
290
+ // down via the process's kill path. `"closed"` must NOT propagate because the child's own `"exit"` event follows shortly and the base class computes the correct
291
+ // reason from the actual exit code - propagating `"closed"` here would race with and pre-empt that, converting a nonzero-exit `"failed"` into a `"closed"` and losing
292
+ // the error signal. `onAbort` provides one-shot semantics AND covers the pre-aborted-signal edge case where a parent-aborted assembler would otherwise miss the bridge
293
+ // entirely.
294
+ function bridgeAssemblerToProcess(process, assembler) {
295
+ onAbort(assembler.signal, () => {
296
+ if (process.aborted) {
288
297
  return;
289
298
  }
290
- // See if we know about this error.
291
- if (this.stderrLog.some(x => FFMPEG_KNOWN_HKSV_ERROR.test(x))) {
292
- this.log.error("FFmpeg ended unexpectedly due to issues processing the media stream. This error can be safely ignored - it will occur occasionally.");
299
+ const reason = assembler.signal.reason;
300
+ // Natural source-end defers to the process's own exit path.
301
+ if (isHbpuAbortReason(reason, "closed")) {
293
302
  return;
294
303
  }
295
- // Otherwise, revert to our default logging in our parent.
296
- super.logFfmpegError(exitCode, signal);
297
- }
304
+ process.abort(reason);
305
+ });
298
306
  }
299
307
  /**
300
- * Manages a HomeKit Secure Video recording FFmpeg process.
308
+ * Abstract base for FFmpeg processes that produce fragmented MP4 segments on their stdout. Owns the composition wiring between the process's stdout and an internal
309
+ * {@link Mp4SegmentAssembler}, plus the bridge that propagates assembler teardown to the process when the assembler aborts for reasons the process's own exit handler
310
+ * cannot discover on its own (watchdog timeout, source stream error).
301
311
  *
302
- * @example
312
+ * This base deliberately contains **no pipeline logic** and **no command-line assembly**. The byte-to-segment pipeline lives in {@link Mp4SegmentAssembler}, and each
313
+ * concrete subclass builds its own FFmpeg arg vector. The base exists solely to consolidate the composition shape - internal assembler field, the delegating public
314
+ * methods, and the bridge registration - that would otherwise duplicate across every fMP4 subclass. The constructor takes only `segmentTimeout` as a mode-specific knob
315
+ * and holds no mode-specific state, so the base avoids template-method coupling with its subclasses.
303
316
  *
304
- * ```ts
305
- * const process = new FfmpegRecordingProcess(ffmpegOptions, recordingConfig, 30, true, 5000000, 0);
306
- * process.start();
307
- * ```
317
+ * Subclasses must call `super(options, init, segmentTimeout?)` from their constructor, having already folded their subclass-specific init into a base-compatible
318
+ * {@link FfmpegProcessInit} (typically by spreading their own init and setting `args` to their built command line).
308
319
  *
309
- * @see FfmpegFMp4Process
320
+ * @see Mp4SegmentAssembler
321
+ * @see FfmpegProcess
310
322
  *
311
323
  * @category FFmpeg
312
324
  */
313
- export class FfmpegRecordingProcess extends FfmpegFMp4Process {
314
- /**
315
- * Indicates whether the recording has timed out waiting for FFmpeg output.
316
- */
317
- isTimedOut;
318
- fps;
319
- probesize;
320
- recordingBuffer;
321
- timeshift;
325
+ export class FfmpegFMp4Process extends FfmpegProcess {
326
+ #assembler;
322
327
  /**
323
- * Constructs a new FFmpeg recording process for HKSV events.
328
+ * Construct and spawn a new fMP4 segment-producing process.
324
329
  *
325
- * @param options - FFmpeg configuration options.
326
- * @param recordingConfig - HomeKit recording configuration for the session.
327
- * @param fMp4Options - fMP4 recording options.
328
- * @param isVerbose - If `true`, enables more verbose logging for debugging purposes. Defaults to `false`.
330
+ * @param options - Shared {@link FfmpegOptions} configuration (codec support, logger, debug flag, name).
331
+ * @param init - Base-class init (FfmpegProcessInit) plus the finalized `args` built by the subclass.
332
+ * @param segmentTimeout - Optional inter-segment watchdog in milliseconds. When set, the assembler aborts with
333
+ * `HbpuAbortError("timeout")` if no media segment arrives within the window, and the bridge
334
+ * propagates the timeout up to the process. Omit for subclasses that tolerate quiet periods
335
+ * (e.g., livestreams).
329
336
  */
330
- constructor(options, recordingConfig, fMp4Options = {}, isVerbose = false) {
331
- super(options, recordingConfig, fMp4Options, isVerbose);
332
- // Store recording-specific options.
333
- this.fps = fMp4Options.fps ?? 30;
334
- this.isTimedOut = false;
335
- this.probesize = fMp4Options.probesize ?? 5000000;
336
- this.recordingBuffer = [];
337
- this.timeshift = fMp4Options.timeshift ?? 0;
338
- // Assemble the FFmpeg command line now that all state is initialized.
339
- this.buildCommandLine();
340
- }
341
- // Recording input: read fMP4 data from standard input with low-delay optimizations and an optional timeshift for HKSV event alignment.
342
- //
343
- // -flags low_delay Tell FFmpeg to optimize for low delay / realtime decoding.
344
- // -probesize number How many bytes should be analyzed for stream information.
345
- // -f mp4 Tell FFmpeg that it should expect an MP4-encoded input stream.
346
- // -i pipe:0 Use standard input to get video data.
347
- // -ss Fast forward to where HKSV is expecting us to be for a recording event.
348
- inputArgs() {
349
- return [
350
- "-flags", "low_delay",
351
- "-probesize", this.probesize.toString(),
352
- "-f", "mp4",
353
- "-i", "pipe:0",
354
- "-ss", this.timeshift.toString() + "ms"
355
- ];
356
- }
357
- // Recordings always read audio from the primary input...no separate audio source.
358
- separateAudioInputArgs() {
359
- return [];
360
- }
361
- // Audio is always on the primary input (index 0) for recordings.
362
- audioInputIndex() {
363
- return 0;
364
- }
365
- // Recordings transcode video using the platform-appropriate encoder for HKSV.
366
- videoEncoderArgs() {
367
- return this.options.recordEncoder({
368
- bitrate: this.recordingConfig.videoCodec.parameters.bitRate,
369
- fps: this.recordingConfig.videoCodec.resolution[2],
370
- hardwareDecoding: this.fMp4Options.hardwareDecoding,
371
- hardwareTranscoding: this.fMp4Options.hardwareTranscoding,
372
- height: this.recordingConfig.videoCodec.resolution[1],
373
- idrInterval: HKSV_IDR_INTERVAL,
374
- inputFps: this.fps,
375
- level: this.recordingConfig.videoCodec.parameters.level,
376
- profile: this.recordingConfig.videoCodec.parameters.profile,
377
- width: this.recordingConfig.videoCodec.resolution[0]
378
- });
379
- }
380
- // Recordings have no post-filter arguments.
381
- postFilterArgs() {
382
- return [];
383
- }
384
- // Metadata label identifying this as an HKSV event recording.
385
- metadataLabel() {
386
- return "HKSV Event";
387
- }
388
- // Each parsed box is queued in the recording buffer for consumption by segmentGenerator().
389
- handleParsedBox(header, data, dataLength, type) {
390
- this.recordingBuffer.push({ data, header, length: dataLength, type });
391
- this.emit("mp4box");
337
+ constructor(options, init, segmentTimeout) {
338
+ super(options, init);
339
+ this.#assembler = new Mp4SegmentAssembler(this._stdout, { segmentTimeout, signal: this.signal });
340
+ bridgeAssemblerToProcess(this, this.#assembler);
392
341
  }
393
342
  /**
394
- * Stops the FFmpeg process and performs cleanup, ensuring the segment generator can exit.
343
+ * Resolve with the fMP4 initialization segment (typically `ftyp` + `moov`) once it appears on stdout. Rejects with `this.signal.reason` if the process aborts before
344
+ * the initialization segment completes.
345
+ *
346
+ * @returns A promise resolving to the initialization segment bytes.
395
347
  */
396
- stopProcess() {
397
- // Emit mp4box to unblock segmentGenerator() if it's waiting, then let the base class handle the rest.
398
- this._isEnded = true;
399
- this.emit("mp4box");
400
- super.stopProcess();
348
+ async getInitSegment() {
349
+ return this.#assembler.initSegment;
401
350
  }
402
351
  /**
403
- * Asynchronously generates complete segments from FFmpeg output, formatted for HomeKit Secure Video.
352
+ * Async generator yielding each completed media segment (concatenated `moof` + `mdat` pair) as a single Buffer. Terminates cleanly when the process or the caller's
353
+ * signal aborts, or when the underlying stdout ends.
404
354
  *
405
- * This async generator yields fMP4 segments as Buffers, or ends on process termination or timeout.
355
+ * @param init - Optional init options. `signal` composes with the process's own signal; aborting it terminates only this generator call, not the process.
406
356
  *
407
- * @yields A Buffer containing a complete MP4 segment suitable for HomeKit.
357
+ * @returns An async generator yielding media segment buffers in stream order.
358
+ */
359
+ segments(init = {}) {
360
+ return this.#assembler.segments(init);
361
+ }
362
+ /**
363
+ * The number of assembled media segments buffered in the internal assembler but not yet pulled through {@link FfmpegFMp4Process.segments} - the consumer's catch-up
364
+ * reserve when the FFmpeg source stalls. Delegates to {@link Mp4SegmentAssembler.bufferedSegments}.
408
365
  *
409
- * @example
366
+ * @returns The buffered-segment depth.
367
+ */
368
+ get bufferedSegments() {
369
+ return this.#assembler.bufferedSegments;
370
+ }
371
+ /**
372
+ * Async generator yielding the whole segment stream as a kind-tagged sequence: one {@link Mp4Segment} of kind `"init"` carrying the initialization bytes, then one of
373
+ * kind `"media"` per completed media fragment. Delegates to {@link Mp4SegmentAssembler.stream}; it is a third view over the same pipeline as
374
+ * {@link FfmpegFMp4Process.getInitSegment} / {@link FfmpegFMp4Process.segments} and shares their single-consumer contract - use one view or the other, never both.
410
375
  *
411
- * ```ts
412
- * for await(const segment of process.segmentGenerator()) {
376
+ * @param init - Optional init options. `signal` composes with the process's own signal; aborting it terminates only this generator call, not the process.
413
377
  *
414
- * // Process each segment for HomeKit.
415
- * }
416
- * ```
378
+ * @returns An async generator yielding one `"init"` segment followed by `"media"` segments in stream order.
417
379
  */
418
- async *segmentGenerator() {
419
- let segment = [];
420
- // Loop forever, generating either FTYP/MOOV box pairs or MOOF/MDAT box pairs for HomeKit Secure Video.
421
- for (;;) {
422
- // FFmpeg has finished its output - we're done.
423
- if (this._isEnded) {
424
- return;
425
- }
426
- // If the buffer is empty, wait for our FFmpeg process to produce more boxes.
427
- if (!this.recordingBuffer.length) {
428
- // Segments are output by FFmpeg according to our specified IDR interval. If we don't see a segment within the timeframe we need for HKSV's timing requirements,
429
- // we flag it accordingly and return null back to the generator that's calling us.
430
- // eslint-disable-next-line no-await-in-loop
431
- await runWithTimeout(once(this, "mp4box"), HKSV_TIMEOUT);
432
- }
433
- // Grab the next fMP4 box from our buffer.
434
- const box = this.recordingBuffer.shift();
435
- // FFmpeg hasn't produced any output. Given the time-sensitive nature of HKSV that constrains us to no more than 5 seconds to provide the next segment, we're done.
436
- if (!box) {
437
- this.isTimedOut = true;
438
- return;
439
- }
440
- // Queue up this fMP4 box to send back to HomeKit.
441
- segment.push(box.header, box.data);
442
- // What we want to send are two types of complete segments, made up of multiple MP4 boxes:
443
- //
444
- // - a complete MOOV box, usually with an accompanying FTYP box, that's sent at the very beginning of any valid fMP4 stream. HomeKit Secure Video looks for this
445
- // before anything else.
446
- //
447
- // - a complete MOOF/MDAT pair. MOOF describes the sample locations and their sizes and MDAT contains the actual audio and video data related to that segment. Think
448
- // of MOOF as the audio/video data "header", and MDAT as the "payload".
449
- //
450
- // Once we see these, we combine all the segments in our queue to send back to HomeKit.
451
- if ((box.type === BOX_TYPE_MOOV) || (box.type === BOX_TYPE_MDAT)) {
452
- yield Buffer.concat(segment);
453
- segment = [];
454
- }
455
- }
380
+ stream(init = {}) {
381
+ return this.#assembler.stream(init);
456
382
  }
457
383
  }
458
384
  /**
459
- * Manages a HomeKit livestream FFmpeg process for generating fMP4 segments.
385
+ * FFmpeg process specialization for HomeKit Secure Video (HKSV) event recording. Builds its command line from the provided HKSV recording configuration and delegates
386
+ * segment production to {@link FfmpegFMp4Process}. Overrides `FfmpegProcess.logFailedTeardown` to substitute a friendly user-facing message when the stderr log
387
+ * matches one of the tolerated HKSV error patterns, suppressing the canonical ERROR dump for those known benign cases. Also overrides
388
+ * `FfmpegProcess.logTimeoutTeardown` to demote the benign inter-segment watchdog reap to debug - a recording ends exactly this way when its segment source quiets,
389
+ * so the base's WARN would be alarming.
460
390
  *
461
391
  * @example
462
392
  *
463
393
  * ```ts
464
- * const process = new FfmpegLivestreamProcess(ffmpegOptions, recordingConfig, url, 30, true);
465
- * process.start();
394
+ * await using proc = new FfmpegRecordingProcess(ffmpegOptions, {
395
+ *
396
+ * recording: { fps: 30, probesize: 5_000_000, timeshift: 0 },
397
+ * recordingConfig,
398
+ * signal: delegate.abortController.signal
399
+ * });
466
400
  *
467
- * const initSegment = await process.getInitSegment();
401
+ * const init = await proc.getInitSegment();
402
+ *
403
+ * for await (const segment of proc.segments()) {
404
+ *
405
+ * // Forward each media segment to HomeKit.
406
+ * }
468
407
  * ```
469
408
  *
470
409
  * @see FfmpegFMp4Process
410
+ * @see FfmpegProcess
471
411
  *
472
412
  * @category FFmpeg
473
413
  */
474
- export class FfmpegLivestreamProcess extends FfmpegFMp4Process {
475
- /**
476
- * Optional override for the fMP4 fragment duration, in milliseconds. When set, the `-frag_duration` argument is updated before starting the FFmpeg process.
477
- */
478
- segmentLength;
479
- // Set to true during separateAudioInputArgs() when a separate audio input is configured, so that audioInputIndex() returns the correct FFmpeg input index.
480
- _hasAudioInput;
481
- _initSegment;
482
- _initSegmentParts;
483
- hasInitSegment;
484
- livestreamOptions;
414
+ export class FfmpegRecordingProcess extends FfmpegFMp4Process {
485
415
  /**
486
- * Constructs a new FFmpeg livestream process.
416
+ * Construct and spawn a new HKSV recording process.
487
417
  *
488
- * @param options - FFmpeg configuration options.
489
- * @param recordingConfig - HomeKit recording configuration for the session.
490
- * @param livestreamOptions - livestream segmenting options.
491
- * @param isVerbose - If `true`, enables more verbose logging for debugging purposes. Defaults to `false`.
418
+ * @param options - Shared {@link FfmpegOptions} configuration (codec support, logger, debug flag, name).
419
+ * @param init - Init options. See {@link FfmpegRecordingInit}.
492
420
  */
493
- constructor(options, recordingConfig, livestreamOptions, isVerbose = false) {
494
- super(options, recordingConfig, livestreamOptions, isVerbose);
495
- // Store livestream-specific options.
496
- this._hasAudioInput = false;
497
- this._initSegment = Buffer.alloc(0);
498
- this._initSegmentParts = [];
499
- this.hasInitSegment = false;
500
- this.livestreamOptions = livestreamOptions;
501
- // Assemble the FFmpeg command line now that all state is initialized.
502
- this.buildCommandLine();
503
- }
504
- // Livestream input: connect to an RTSP source with direct I/O and TCP transport.
505
- //
506
- // -avioflags direct Tell FFmpeg to minimize buffering to reduce latency for more realtime processing.
507
- // -rtsp_transport tcp Tell the RTSP stream handler that we're looking for a TCP connection.
508
- // -i url RTSPS URL to get our input stream from.
509
- inputArgs() {
510
- return [
511
- "-avioflags", "direct",
512
- "-rtsp_transport", "tcp",
513
- "-i", this.livestreamOptions.url
514
- ];
515
- }
516
- // If a separate audio input has been configured, build the FFmpeg input arguments for it. This enables support for devices like DoorBird where video and audio are
517
- // served from different endpoints.
518
- separateAudioInputArgs() {
519
- if (!this.fMp4Options.enableAudio || !this.livestreamOptions.audioInput) {
520
- return [];
521
- }
522
- const args = [];
523
- // Normalize the audio input configuration. A plain string is treated as a URL shorthand.
524
- const audioInput = (typeof this.livestreamOptions.audioInput === "string") ?
525
- { url: this.livestreamOptions.audioInput } :
526
- this.livestreamOptions.audioInput;
527
- // When a raw audio format is specified, we need to explicitly tell FFmpeg how to interpret the incoming stream since it cannot probe raw audio sources.
528
- //
529
- // -f format Specify the raw audio format (e.g., mulaw, alaw, s16le).
530
- // -ar sampleRate Specify the audio sample rate in Hz.
531
- // -ac channels Specify the number of audio channels.
532
- if (audioInput.format) {
533
- args.push("-f", audioInput.format, "-ar", (audioInput.sampleRate ?? 8000).toString(), "-ac", (audioInput.channels ?? 1).toString());
534
- }
535
- // For RTSP and RTSPS audio sources, we explicitly request TCP transport to match the behavior we use for the primary video input.
536
- if (["rtsp://", "rtsps://"].some((protocol) => audioInput.url.toLowerCase().startsWith(protocol))) {
537
- args.push("-rtsp_transport", "tcp");
538
- }
539
- // -i url Audio input URL.
540
- args.push("-i", audioInput.url);
541
- // Track that we have a separate audio input so audioInputIndex() returns the correct value.
542
- this._hasAudioInput = true;
543
- return args;
544
- }
545
- // When a separate audio input is configured, audio is on the second FFmpeg input (index 1). Otherwise it shares the primary input (index 0).
546
- audioInputIndex() {
547
- return this._hasAudioInput ? 1 : 0;
548
- }
549
- // Livestreams remux the video stream directly without transcoding.
550
- videoEncoderArgs() {
551
- return ["-codec:v", "copy"];
552
- }
553
- // Livestreams emit fMP4 fragments at one-second intervals by default.
554
- //
555
- // -frag_duration number Length of each fMP4 fragment, in microseconds.
556
- postFilterArgs() {
557
- return ["-frag_duration", "1000000"];
558
- }
559
- // Metadata label identifying this as a livestream buffer.
560
- metadataLabel() {
561
- return "Livestream Buffer";
562
- }
563
- // Livestream box handling: accumulate the initialization segment (everything before the first moof box), then emit each subsequent box as a segment event.
564
- handleParsedBox(header, data, _dataLength, type) {
565
- // If this is part of the initialization segment, store it for future use.
566
- if (!this.hasInitSegment) {
567
- // The initialization segment is everything before the first moof box. Once we've seen a moof box, we know we've captured it in full. We collect the parts into an
568
- // array and concatenate once at the end to avoid creating intermediate buffers on every pre-moof box.
569
- if (type === BOX_TYPE_MOOF) {
570
- this._initSegment = Buffer.concat(this._initSegmentParts);
571
- this._initSegmentParts = [];
572
- this.hasInitSegment = true;
573
- this.emit("initsegment");
574
- }
575
- else {
576
- this._initSegmentParts.push(header, data);
577
- }
578
- }
579
- if (this.hasInitSegment) {
580
- // We only emit segments once we have the initialization segment.
581
- this.emit("segment", Buffer.concat([header, data]));
582
- }
421
+ constructor(options, init) {
422
+ super(options, { ...init, args: init.args ?? buildRecordingCommandLine(options, init) }, HKSV_TIMEOUT);
583
423
  }
584
- /**
585
- * Starts the FFmpeg process, adjusting the fragment duration if segmentLength has been set.
586
- *
587
- * @example
588
- *
589
- * ```ts
590
- * process.start();
591
- * ```
592
- */
593
- start() {
594
- if (this.segmentLength !== undefined) {
595
- const fragIndex = this.commandLineArgs.indexOf("-frag_duration");
596
- if (fragIndex !== -1) {
597
- this.commandLineArgs[fragIndex + 1] = (this.segmentLength * 1000).toString();
598
- }
424
+ // Known-HKSV-error friendly message. When the abort reason is `"failed"`, inspect the accumulated stderr for any of the tolerated HKSV error patterns and substitute
425
+ // a single user-facing log line for the canonical ERROR dump. Non-matching failures fall through to the base's canonical ERROR dump via `super.logFailedTeardown`.
426
+ // Policy is driven entirely by `signal.reason` and the observed stderr, with no class state of our own.
427
+ logFailedTeardown(reason) {
428
+ if (this.stderrLog.some((line) => FFMPEG_KNOWN_HKSV_ERROR.test(line))) {
429
+ this.log.error("%s: FFmpeg ended unexpectedly due to issues processing the media stream. This error can be safely ignored - it will occur occasionally.", this.options.name());
430
+ return;
599
431
  }
600
- // Start the FFmpeg session.
601
- super.start();
432
+ super.logFailedTeardown(reason);
602
433
  }
603
- /**
604
- * Gets the fMP4 initialization segment generated by FFmpeg for the livestream.
605
- *
606
- * @returns A promise resolving to the initialization segment as a Buffer.
607
- *
608
- * @example
609
- *
610
- * ```ts
611
- * const initSegment = await process.getInitSegment();
612
- * ```
613
- */
614
- async getInitSegment() {
615
- // If we have the initialization segment, return it.
616
- if (this.hasInitSegment) {
617
- return this._initSegment;
618
- }
619
- // Wait until the initialization segment is available.
620
- await once(this, "initsegment");
621
- return this._initSegment;
434
+ // FfmpegRecordingProcess is the only fMP4 process that arms an inter-segment watchdog - it alone passes HKSV_TIMEOUT to super, while the sibling
435
+ // FfmpegLivestreamProcess passes no segmentTimeout, so that watchdog never fires there. The watchdog firing is benign by default - it is simply how an HKSV recording
436
+ // ends when its segment source quiets. The watchdog is output-only (it re-arms on completed output segments and never sees stdin), so the library cannot tell a starved
437
+ // source (a benign wire stall) from a fed-but-stuck FFmpeg (a real local hang); it reports the reap quietly at debug and leaves the severity verdict to the consumer,
438
+ // which alone holds the input-feed and reachability context. The base class warns here because a stall on a general (streaming) process genuinely is a problem.
439
+ logTimeoutTeardown(_reason) {
440
+ this.log.debug("%s: the recording's inter-segment watchdog window elapsed with no new segment.", this.options.name());
622
441
  }
442
+ }
443
+ /**
444
+ * The production {@link RecordingProcessFactory}: builds the concrete FFmpeg-backed recording process. A consumer holds this typed as the abstraction; a test substitutes
445
+ * a fake factory. `create` is exactly the {@link FfmpegRecordingProcess} constructor call, so wiring construction through this seam is behavior-neutral.
446
+ *
447
+ * @see RecordingProcessFactory
448
+ *
449
+ * @category FFmpeg
450
+ */
451
+ export const recordingProcessFactory = {
452
+ create: (options, init) => new FfmpegRecordingProcess(options, init)
453
+ };
454
+ /**
455
+ * FFmpeg process specialization for fMP4 livestreaming from an RTSP source. Builds its command line from the provided livestream source and delegates segment production
456
+ * to {@link FfmpegFMp4Process}.
457
+ *
458
+ * Used by HBUP as an alternative HKSV segment source when pulling directly from an RTSP URL (bypasses the Protect livestream API for debug and diagnostic scenarios).
459
+ * Matches the polymorphic `{ getInitSegment(): Promise<Buffer>; segments(): AsyncGenerator<Buffer> }` interface HBUP uses across the native Protect livestream and this
460
+ * class. Unlike recording, livestream does not enforce an inter-segment watchdog timeout: a live camera feed legitimately quiets down during low-motion periods and does
461
+ * not carry HKSV's 5-second hard timing contract. Callers that need a liveness cap can compose their own timeout via the process's `signal`.
462
+ *
463
+ * @example
464
+ *
465
+ * ```ts
466
+ * await using proc = new FfmpegLivestreamProcess(ffmpegOptions, {
467
+ *
468
+ * audio: { codec: AudioRecordingCodecType.AAC_LC, samplerate: AudioRecordingSamplerate.KHZ_16 },
469
+ * livestream: { url: "rtsp://camera/stream" },
470
+ * segmentLength: 1000,
471
+ * signal: session.controller.signal
472
+ * });
473
+ *
474
+ * const init = await proc.getInitSegment();
475
+ *
476
+ * for await (const segment of proc.segments()) {
477
+ *
478
+ * // Forward each media segment to the downstream consumer.
479
+ * }
480
+ * ```
481
+ *
482
+ * @see FfmpegFMp4Process
483
+ * @see FfmpegProcess
484
+ *
485
+ * @category FFmpeg
486
+ */
487
+ export class FfmpegLivestreamProcess extends FfmpegFMp4Process {
623
488
  /**
624
- * Returns the initialization segment as a Buffer, or null if not yet available.
625
- *
626
- * @returns The initialization segment Buffer, or `null` if not yet generated.
627
- *
628
- * @example
489
+ * Construct and spawn a new fMP4 livestream process.
629
490
  *
630
- * ```ts
631
- * const init = process.initSegment;
632
- * if(init) {
633
- *
634
- * // Use the initialization segment.
635
- * }
636
- * ```
491
+ * @param options - Shared {@link FfmpegOptions} configuration (codec support, logger, debug flag, name).
492
+ * @param init - Init options. See {@link FfmpegLivestreamInit}.
637
493
  */
638
- get initSegment() {
639
- if (!this.hasInitSegment) {
640
- return null;
641
- }
642
- return this._initSegment;
494
+ constructor(options, init) {
495
+ super(options, { ...init, args: init.args ?? buildLivestreamCommandLine(options, init) });
643
496
  }
644
497
  }
645
498
  //# sourceMappingURL=record.js.map