homebridge-plugin-utils 1.34.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -2
- package/build/eslint-plugin/README.md +164 -0
- package/build/eslint-plugin/config.mjs +308 -0
- package/build/eslint-plugin/index.mjs +7 -0
- package/build/eslint-plugin/plugin.mjs +46 -0
- package/build/eslint-plugin/rules/blank-line-after-open-brace.mjs +137 -0
- package/build/eslint-plugin/rules/blank-line-after-open-brace.test.mjs +112 -0
- package/build/eslint-plugin/rules/comment-style.mjs +190 -0
- package/build/eslint-plugin/rules/comment-style.test.mjs +190 -0
- package/build/eslint-plugin/rules/enforce-node-protocol.mjs +114 -0
- package/build/eslint-plugin/rules/enforce-node-protocol.test.mjs +116 -0
- package/build/eslint-plugin/rules/paren-comparisons-in-logical.mjs +94 -0
- package/build/eslint-plugin/rules/paren-comparisons-in-logical.test.mjs +107 -0
- package/build/eslint-plugin/rules/split-type-imports.mjs +354 -0
- package/build/eslint-plugin/rules/split-type-imports.test.mjs +268 -0
- package/build/eslint-plugin/test-setup.mjs +21 -0
- package/build/fs-ops.mjs +184 -0
- package/build/tsconfig.json +18 -3
- package/dist/backpressure.d.ts +128 -0
- package/dist/backpressure.js +273 -0
- package/dist/backpressure.js.map +1 -0
- package/dist/cli/index.d.ts +95 -0
- package/dist/cli/index.js +355 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/clock-double.d.ts +93 -0
- package/dist/clock-double.js +141 -0
- package/dist/clock-double.js.map +1 -0
- package/dist/clock.d.ts +39 -0
- package/dist/clock.js +34 -0
- package/dist/clock.js.map +1 -0
- package/dist/eslint-plugin/config.d.mts +193 -0
- package/dist/eslint-plugin/index.d.mts +2 -0
- package/dist/eslint-plugin/plugin.d.mts +87 -0
- package/dist/eslint-plugin/rules/blank-line-after-open-brace.d.mts +19 -0
- package/dist/eslint-plugin/rules/comment-style.d.mts +15 -0
- package/dist/eslint-plugin/rules/enforce-node-protocol.d.mts +18 -0
- package/dist/eslint-plugin/rules/paren-comparisons-in-logical.d.mts +15 -0
- package/dist/eslint-plugin/rules/split-type-imports.d.mts +16 -0
- package/dist/featureOptions-docs.d.ts +96 -0
- package/dist/featureOptions-docs.js +193 -0
- package/dist/featureOptions-docs.js.map +1 -0
- package/dist/featureOptions.d.ts +674 -0
- package/dist/featureOptions.js +870 -0
- package/dist/featureOptions.js.map +1 -0
- package/dist/ffmpeg/codecs.d.ts +256 -72
- package/dist/ffmpeg/codecs.js +477 -257
- package/dist/ffmpeg/codecs.js.map +1 -1
- package/dist/ffmpeg/dgram-util.d.ts +46 -0
- package/dist/ffmpeg/dgram-util.js +38 -0
- package/dist/ffmpeg/dgram-util.js.map +1 -0
- package/dist/ffmpeg/exec.d.ts +83 -64
- package/dist/ffmpeg/exec.js +77 -86
- package/dist/ffmpeg/exec.js.map +1 -1
- package/dist/ffmpeg/fmp4.d.ts +70 -4
- package/dist/ffmpeg/fmp4.js +100 -11
- package/dist/ffmpeg/fmp4.js.map +1 -1
- package/dist/ffmpeg/hap-enums.d.ts +214 -0
- package/dist/ffmpeg/hap-enums.js +92 -0
- package/dist/ffmpeg/hap-enums.js.map +1 -0
- package/dist/ffmpeg/index.d.ts +16 -9
- package/dist/ffmpeg/index.js +6 -0
- package/dist/ffmpeg/index.js.map +1 -1
- package/dist/ffmpeg/mp4-assembler.d.ts +120 -0
- package/dist/ffmpeg/mp4-assembler.js +387 -0
- package/dist/ffmpeg/mp4-assembler.js.map +1 -0
- package/dist/ffmpeg/mp4-parser.d.ts +94 -0
- package/dist/ffmpeg/mp4-parser.js +130 -0
- package/dist/ffmpeg/mp4-parser.js.map +1 -0
- package/dist/ffmpeg/options.d.ts +62 -149
- package/dist/ffmpeg/options.js +608 -499
- package/dist/ffmpeg/options.js.map +1 -1
- package/dist/ffmpeg/process.d.ts +143 -91
- package/dist/ffmpeg/process.js +406 -265
- package/dist/ffmpeg/process.js.map +1 -1
- package/dist/ffmpeg/record.d.ts +302 -188
- package/dist/ffmpeg/record.js +408 -560
- package/dist/ffmpeg/record.js.map +1 -1
- package/dist/ffmpeg/recording-process-double.d.ts +144 -0
- package/dist/ffmpeg/recording-process-double.js +168 -0
- package/dist/ffmpeg/recording-process-double.js.map +1 -0
- package/dist/ffmpeg/rtp-parser.d.ts +70 -0
- package/dist/ffmpeg/rtp-parser.js +77 -0
- package/dist/ffmpeg/rtp-parser.js.map +1 -0
- package/dist/ffmpeg/rtp.d.ts +198 -141
- package/dist/ffmpeg/rtp.js +473 -244
- package/dist/ffmpeg/rtp.js.map +1 -1
- package/dist/ffmpeg/settings.d.ts +5 -1
- package/dist/ffmpeg/settings.js +20 -2
- package/dist/ffmpeg/settings.js.map +1 -1
- package/dist/ffmpeg/stream.d.ts +57 -107
- package/dist/ffmpeg/stream.js +121 -148
- package/dist/ffmpeg/stream.js.map +1 -1
- package/dist/formatters.d.ts +106 -0
- package/dist/formatters.js +174 -0
- package/dist/formatters.js.map +1 -0
- package/dist/homebridge-enums.d.ts +30 -0
- package/dist/homebridge-enums.js +17 -0
- package/dist/homebridge-enums.js.map +1 -0
- package/dist/index.d.ts +12 -5
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/logclient/auth.d.ts +114 -0
- package/dist/logclient/auth.js +199 -0
- package/dist/logclient/auth.js.map +1 -0
- package/dist/logclient/cli-run.d.ts +76 -0
- package/dist/logclient/cli-run.js +639 -0
- package/dist/logclient/cli-run.js.map +1 -0
- package/dist/logclient/cli.d.ts +3 -0
- package/dist/logclient/cli.js +97 -0
- package/dist/logclient/cli.js.map +1 -0
- package/dist/logclient/client.d.ts +145 -0
- package/dist/logclient/client.js +600 -0
- package/dist/logclient/client.js.map +1 -0
- package/dist/logclient/config.d.ts +173 -0
- package/dist/logclient/config.js +199 -0
- package/dist/logclient/config.js.map +1 -0
- package/dist/logclient/endpoints.d.ts +54 -0
- package/dist/logclient/endpoints.js +73 -0
- package/dist/logclient/endpoints.js.map +1 -0
- package/dist/logclient/filter.d.ts +45 -0
- package/dist/logclient/filter.js +51 -0
- package/dist/logclient/filter.js.map +1 -0
- package/dist/logclient/frame.d.ts +93 -0
- package/dist/logclient/frame.js +203 -0
- package/dist/logclient/frame.js.map +1 -0
- package/dist/logclient/index.d.ts +31 -0
- package/dist/logclient/index.js +12 -0
- package/dist/logclient/index.js.map +1 -0
- package/dist/logclient/parser.d.ts +211 -0
- package/dist/logclient/parser.js +393 -0
- package/dist/logclient/parser.js.map +1 -0
- package/dist/logclient/rest.d.ts +41 -0
- package/dist/logclient/rest.js +111 -0
- package/dist/logclient/rest.js.map +1 -0
- package/dist/logclient/settings.d.ts +15 -0
- package/dist/logclient/settings.js +64 -0
- package/dist/logclient/settings.js.map +1 -0
- package/dist/logclient/socket-double.d.ts +201 -0
- package/dist/logclient/socket-double.js +384 -0
- package/dist/logclient/socket-double.js.map +1 -0
- package/dist/logclient/socket.d.ts +257 -0
- package/dist/logclient/socket.js +620 -0
- package/dist/logclient/socket.js.map +1 -0
- package/dist/logclient/stitch.d.ts +83 -0
- package/dist/logclient/stitch.js +146 -0
- package/dist/logclient/stitch.js.map +1 -0
- package/dist/logclient/time-expression.d.ts +42 -0
- package/dist/logclient/time-expression.js +181 -0
- package/dist/logclient/time-expression.js.map +1 -0
- package/dist/logclient/time-window.d.ts +38 -0
- package/dist/logclient/time-window.js +53 -0
- package/dist/logclient/time-window.js.map +1 -0
- package/dist/logclient/types.d.ts +107 -0
- package/dist/logclient/types.js +6 -0
- package/dist/logclient/types.js.map +1 -0
- package/dist/mqttClient.d.ts +287 -0
- package/dist/mqttClient.js +433 -0
- package/dist/mqttClient.js.map +1 -0
- package/dist/service.d.ts +64 -15
- package/dist/service.js +93 -66
- package/dist/service.js.map +1 -1
- package/dist/ui/featureOptions.js +870 -0
- package/dist/ui/featureOptions.js.map +1 -0
- package/dist/ui/formatters.js +174 -0
- package/dist/ui/formatters.js.map +1 -0
- package/dist/ui/pluginConfigSession.mjs +141 -0
- package/dist/ui/webUi-featureOptions/categoryState.mjs +135 -0
- package/dist/ui/webUi-featureOptions/effects/keyboard.mjs +61 -0
- package/dist/ui/webUi-featureOptions/effects/persist.mjs +226 -0
- package/dist/ui/webUi-featureOptions/effects/theme.mjs +398 -0
- package/dist/ui/webUi-featureOptions/effects/tokens.mjs +152 -0
- package/dist/ui/webUi-featureOptions/rendering.mjs +431 -0
- package/dist/ui/webUi-featureOptions/selectors.mjs +360 -0
- package/dist/ui/webUi-featureOptions/state.mjs +319 -0
- package/dist/ui/webUi-featureOptions/store.mjs +181 -0
- package/dist/ui/webUi-featureOptions/utils.mjs +200 -0
- package/dist/ui/webUi-featureOptions/views/connectionError.mjs +152 -0
- package/dist/ui/webUi-featureOptions/views/deviceInfo.mjs +80 -0
- package/dist/ui/webUi-featureOptions/views/header.mjs +77 -0
- package/dist/ui/webUi-featureOptions/views/nav.mjs +327 -0
- package/dist/ui/webUi-featureOptions/views/options.mjs +521 -0
- package/dist/ui/webUi-featureOptions/views/search.mjs +395 -0
- package/dist/ui/webUi-featureOptions.mjs +677 -0
- package/dist/ui/webUi.mjs +192 -87
- package/dist/util.d.ts +597 -45
- package/dist/util.js +787 -68
- package/dist/util.js.map +1 -1
- package/package.json +33 -15
- package/build/eslint-rules.mjs +0 -511
- package/dist/featureoptions.d.ts +0 -264
- package/dist/featureoptions.js +0 -480
- package/dist/featureoptions.js.map +0 -1
- package/dist/mqttclient.d.ts +0 -178
- package/dist/mqttclient.js +0 -310
- package/dist/mqttclient.js.map +0 -1
- package/dist/ui/featureoptions.js +0 -480
- package/dist/ui/featureoptions.js.map +0 -1
- package/dist/ui/webUi-featureoptions.mjs +0 -3765
package/dist/ffmpeg/record.js
CHANGED
|
@@ -1,30 +1,66 @@
|
|
|
1
1
|
/* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
2
|
*
|
|
3
|
-
* ffmpeg/record.ts:
|
|
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 {
|
|
45
|
+
import { isHbpuAbortReason, onAbort } from "../util.js";
|
|
7
46
|
import { FfmpegProcess } from "./process.js";
|
|
8
|
-
import {
|
|
9
|
-
//
|
|
10
|
-
// would require updating
|
|
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.
|
|
11
50
|
const translateAudioRecordingCodecType = {
|
|
12
|
-
[
|
|
13
|
-
[
|
|
51
|
+
[AudioRecordingCodecType.AAC_ELD]: "38",
|
|
52
|
+
[AudioRecordingCodecType.AAC_LC]: "1"
|
|
14
53
|
};
|
|
15
|
-
// 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.
|
|
16
54
|
const translateAudioSampleRate = {
|
|
17
|
-
[
|
|
18
|
-
[
|
|
19
|
-
[
|
|
20
|
-
[
|
|
21
|
-
[
|
|
22
|
-
[
|
|
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"
|
|
23
61
|
};
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
// Known HKSV-related errors due to occasional inconsistencies produced by the input stream and FFmpeg's own occasional quirkiness. Compiled once at module scope
|
|
27
|
-
// 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.
|
|
28
64
|
const FFMPEG_KNOWN_HKSV_ERROR = new RegExp([
|
|
29
65
|
"(Cannot determine format of input stream 0:0 after EOF)",
|
|
30
66
|
"(Could not write header \\(incorrect codec parameters \\?\\): Broken pipe)",
|
|
@@ -34,605 +70,417 @@ const FFMPEG_KNOWN_HKSV_ERROR = new RegExp([
|
|
|
34
70
|
"(Invalid data found when processing input)",
|
|
35
71
|
"(moov atom not found)"
|
|
36
72
|
].join("|"));
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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(", "));
|
|
84
120
|
}
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
//
|
|
96
|
-
// -max_delay 500000 Set an upper limit on how much time FFmpeg can take in demuxing packets, in microseconds.
|
|
97
|
-
this.commandLineArgs = [
|
|
98
|
-
"-hide_banner",
|
|
99
|
-
"-nostats",
|
|
100
|
-
"-fflags", "+discardcorrupt",
|
|
101
|
-
"-err_detect", "ignore_err",
|
|
102
|
-
...this.options.videoDecoder(this.fMp4Options.codec),
|
|
103
|
-
"-max_delay", "500000",
|
|
104
|
-
// Mode-specific input arguments (RTSP input for livestream, stdin pipe for recording).
|
|
105
|
-
...this.inputArgs(),
|
|
106
|
-
// Mode-specific separate audio input arguments (livestream with a separate audio endpoint, empty for recording).
|
|
107
|
-
...this.separateAudioInputArgs()
|
|
108
|
-
];
|
|
109
|
-
// 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:
|
|
110
132
|
//
|
|
111
|
-
// -map
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
//
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
this.commandLineArgs.push(...this.postFilterArgs());
|
|
121
|
-
// -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
|
|
122
|
-
// at each keyframe, skip creating a segment index (SIDX) box in fragments, and skip writing the final MOOV trailer since it's unneeded.
|
|
123
|
-
// -flush_packets 1 Ensure we flush our write buffer after each muxed packet.
|
|
124
|
-
// -reset_timestamps Reset timestamps at the beginning of each segment.
|
|
125
|
-
// -metadata Set the metadata to the name of the camera to distinguish between FFmpeg sessions.
|
|
126
|
-
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());
|
|
127
|
-
// 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.
|
|
128
|
-
let transcodeAudio = this.fMp4Options.transcodeAudio;
|
|
129
|
-
if (this.fMp4Options.enableAudio) {
|
|
130
|
-
// Configure the audio portion of the command line. Options we use are:
|
|
131
|
-
//
|
|
132
|
-
// -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
|
|
133
|
-
// separate audio input has been configured.
|
|
134
|
-
this.commandLineArgs.push("-map", this.audioInputIndex().toString() + ":a:" + this.fMp4Options.audioStream.toString() + "?");
|
|
135
|
-
// Configure our audio filters, if we have them.
|
|
136
|
-
if (this.fMp4Options.audioFilters.length) {
|
|
137
|
-
this.commandLineArgs.push("-filter:a", this.fMp4Options.audioFilters.join(", "));
|
|
138
|
-
// Audio filters require transcoding. If the user has decided to filter, we enforce this requirement even if they wanted to copy the audio stream.
|
|
139
|
-
transcodeAudio = true;
|
|
140
|
-
}
|
|
141
|
-
if (transcodeAudio) {
|
|
142
|
-
// Configure the audio portion of the command line. Options we use are:
|
|
143
|
-
//
|
|
144
|
-
// -codec:a Encode using the codecs available to us on given platforms.
|
|
145
|
-
// -profile:a Specify either low-complexity AAC or enhanced low-delay AAC for HKSV events.
|
|
146
|
-
// -ar samplerate Sample rate to use for this audio. This is specified by HKSV.
|
|
147
|
-
// -ac number Set the number of audio channels.
|
|
148
|
-
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());
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
151
|
-
// Configure the audio portion of the command line. Options we use are:
|
|
152
|
-
//
|
|
153
|
-
// -codec:a copy Copy the audio stream, since it's already in AAC.
|
|
154
|
-
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(", "));
|
|
155
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());
|
|
156
150
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
// pipe:1 Output the stream to standard output.
|
|
161
|
-
this.commandLineArgs.push("-f", "mp4", "pipe:1");
|
|
162
|
-
// Additional logging, but only if we're debugging.
|
|
163
|
-
if (this._isVerbose || this.isVerbose) {
|
|
164
|
-
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");
|
|
165
154
|
}
|
|
166
155
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
// Initialize our variables that we need to process incoming FFmpeg packets.
|
|
176
|
-
let header = EMPTY_BUFFER;
|
|
177
|
-
let bufferRemaining = EMPTY_BUFFER;
|
|
178
|
-
let dataLength = 0;
|
|
179
|
-
let type = "";
|
|
180
|
-
// 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
|
|
181
|
-
// into the MP4 boxes that HAP-NodeJS is ultimately expecting.
|
|
182
|
-
this.process?.stdout.on("data", dataListener = (buffer) => {
|
|
183
|
-
// If we have anything left from the last buffer we processed, prepend it to this buffer.
|
|
184
|
-
if (bufferRemaining.length > 0) {
|
|
185
|
-
buffer = Buffer.concat([bufferRemaining, buffer]);
|
|
186
|
-
bufferRemaining = EMPTY_BUFFER;
|
|
187
|
-
}
|
|
188
|
-
let offset = 0;
|
|
189
|
-
// 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
|
|
190
|
-
// container. It's composed of an 8-byte header, followed by the data payload it carries.
|
|
191
|
-
for (;;) {
|
|
192
|
-
let data;
|
|
193
|
-
// No existing header, let's start a new box.
|
|
194
|
-
if (!header.length) {
|
|
195
|
-
// If there aren't enough bytes for a complete box header, save them for the next chunk.
|
|
196
|
-
if (buffer.length < 8) {
|
|
197
|
-
bufferRemaining = buffer;
|
|
198
|
-
break;
|
|
199
|
-
}
|
|
200
|
-
// Grab the header. The first four bytes represents the length of the entire box. Second four bytes represent the box type.
|
|
201
|
-
header = buffer.subarray(0, 8);
|
|
202
|
-
// Now we retrieve the length of the box.
|
|
203
|
-
dataLength = header.readUInt32BE(0);
|
|
204
|
-
// Get the type of the box. This is always a string and has a funky history to it that makes for an interesting read!
|
|
205
|
-
type = header.subarray(4).toString();
|
|
206
|
-
// Finally, we get the data portion of the box.
|
|
207
|
-
data = buffer.subarray(8, dataLength);
|
|
208
|
-
// 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.
|
|
209
|
-
dataLength -= offset = 8;
|
|
210
|
-
}
|
|
211
|
-
else {
|
|
212
|
-
// Grab the data from our buffer.
|
|
213
|
-
data = buffer.subarray(0, dataLength);
|
|
214
|
-
offset = 0;
|
|
215
|
-
}
|
|
216
|
-
// If we don't have enough data in this buffer, save what we have for the next buffer we see and append it there.
|
|
217
|
-
if (data.length < dataLength) {
|
|
218
|
-
bufferRemaining = data;
|
|
219
|
-
break;
|
|
220
|
-
}
|
|
221
|
-
// Dispatch the complete box to the subclass for mode-specific handling.
|
|
222
|
-
this.handleParsedBox(header, data, dataLength, type);
|
|
223
|
-
// Prepare to start a new box for the next buffer that we will be processing.
|
|
224
|
-
header = EMPTY_BUFFER;
|
|
225
|
-
type = "";
|
|
226
|
-
// We've parsed an entire box, and there's no more data in this buffer to parse.
|
|
227
|
-
if (buffer.length === (offset + dataLength)) {
|
|
228
|
-
dataLength = 0;
|
|
229
|
-
break;
|
|
230
|
-
}
|
|
231
|
-
// If there's anything left in the buffer, move us to the new box and let's keep iterating.
|
|
232
|
-
buffer = buffer.subarray(offset + dataLength);
|
|
233
|
-
dataLength = 0;
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
|
-
// Make sure we cleanup our listeners when we're done.
|
|
237
|
-
this.process?.once("exit", () => {
|
|
238
|
-
this.process?.stdout.off("data", dataListener);
|
|
239
|
-
});
|
|
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");
|
|
240
164
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
// Signal that the process has ended.
|
|
249
|
-
this.isEnded = true;
|
|
250
|
-
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 };
|
|
251
172
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
*/
|
|
263
|
-
stop(logErrors = this.isLoggingErrors) {
|
|
264
|
-
const savedLogErrors = this.isLoggingErrors;
|
|
265
|
-
// Flag whether we should log abnormal exits (e.g. being killed) or not.
|
|
266
|
-
this.isLoggingErrors = logErrors;
|
|
267
|
-
// Call our parent to finish the job.
|
|
268
|
-
super.stop();
|
|
269
|
-
// Restore our previous logging state.
|
|
270
|
-
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());
|
|
271
183
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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) {
|
|
281
297
|
return;
|
|
282
298
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
299
|
+
const reason = assembler.signal.reason;
|
|
300
|
+
// Natural source-end defers to the process's own exit path.
|
|
301
|
+
if (isHbpuAbortReason(reason, "closed")) {
|
|
286
302
|
return;
|
|
287
303
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
304
|
+
process.abort(reason);
|
|
305
|
+
});
|
|
291
306
|
}
|
|
292
307
|
/**
|
|
293
|
-
*
|
|
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).
|
|
294
311
|
*
|
|
295
|
-
* @
|
|
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.
|
|
296
316
|
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
* process.start();
|
|
300
|
-
* ```
|
|
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).
|
|
301
319
|
*
|
|
302
|
-
* @see
|
|
320
|
+
* @see Mp4SegmentAssembler
|
|
321
|
+
* @see FfmpegProcess
|
|
303
322
|
*
|
|
304
323
|
* @category FFmpeg
|
|
305
324
|
*/
|
|
306
|
-
export class
|
|
307
|
-
|
|
308
|
-
* Indicates whether the recording has timed out waiting for FFmpeg output.
|
|
309
|
-
*/
|
|
310
|
-
isTimedOut;
|
|
311
|
-
fps;
|
|
312
|
-
probesize;
|
|
313
|
-
recordingBuffer;
|
|
314
|
-
timeshift;
|
|
325
|
+
export class FfmpegFMp4Process extends FfmpegProcess {
|
|
326
|
+
#assembler;
|
|
315
327
|
/**
|
|
316
|
-
*
|
|
328
|
+
* Construct and spawn a new fMP4 segment-producing process.
|
|
317
329
|
*
|
|
318
|
-
* @param options
|
|
319
|
-
* @param
|
|
320
|
-
* @param
|
|
321
|
-
*
|
|
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).
|
|
322
336
|
*/
|
|
323
|
-
constructor(options,
|
|
324
|
-
super(options,
|
|
325
|
-
|
|
326
|
-
this
|
|
327
|
-
this.isTimedOut = false;
|
|
328
|
-
this.probesize = fMp4Options.probesize ?? 5000000;
|
|
329
|
-
this.recordingBuffer = [];
|
|
330
|
-
this.timeshift = fMp4Options.timeshift ?? 0;
|
|
331
|
-
// Assemble the FFmpeg command line now that all state is initialized.
|
|
332
|
-
this.buildCommandLine();
|
|
333
|
-
}
|
|
334
|
-
// Recording input: read fMP4 data from standard input with low-delay optimizations and an optional timeshift for HKSV event alignment.
|
|
335
|
-
//
|
|
336
|
-
// -flags low_delay Tell FFmpeg to optimize for low delay / realtime decoding.
|
|
337
|
-
// -probesize number How many bytes should be analyzed for stream information.
|
|
338
|
-
// -f mp4 Tell FFmpeg that it should expect an MP4-encoded input stream.
|
|
339
|
-
// -i pipe:0 Use standard input to get video data.
|
|
340
|
-
// -ss Fast forward to where HKSV is expecting us to be for a recording event.
|
|
341
|
-
inputArgs() {
|
|
342
|
-
return [
|
|
343
|
-
"-flags", "low_delay",
|
|
344
|
-
"-probesize", this.probesize.toString(),
|
|
345
|
-
"-f", "mp4",
|
|
346
|
-
"-i", "pipe:0",
|
|
347
|
-
"-ss", this.timeshift.toString() + "ms"
|
|
348
|
-
];
|
|
349
|
-
}
|
|
350
|
-
// Recordings always read audio from the primary input...no separate audio source.
|
|
351
|
-
separateAudioInputArgs() {
|
|
352
|
-
return [];
|
|
353
|
-
}
|
|
354
|
-
// Audio is always on the primary input (index 0) for recordings.
|
|
355
|
-
audioInputIndex() {
|
|
356
|
-
return 0;
|
|
357
|
-
}
|
|
358
|
-
// Recordings transcode video using the platform-appropriate encoder for HKSV.
|
|
359
|
-
videoEncoderArgs() {
|
|
360
|
-
return this.options.recordEncoder({
|
|
361
|
-
bitrate: this.recordingConfig.videoCodec.parameters.bitRate,
|
|
362
|
-
fps: this.recordingConfig.videoCodec.resolution[2],
|
|
363
|
-
hardwareDecoding: this.fMp4Options.hardwareDecoding,
|
|
364
|
-
hardwareTranscoding: this.fMp4Options.hardwareTranscoding,
|
|
365
|
-
height: this.recordingConfig.videoCodec.resolution[1],
|
|
366
|
-
idrInterval: HKSV_IDR_INTERVAL,
|
|
367
|
-
inputFps: this.fps,
|
|
368
|
-
level: this.recordingConfig.videoCodec.parameters.level,
|
|
369
|
-
profile: this.recordingConfig.videoCodec.parameters.profile,
|
|
370
|
-
width: this.recordingConfig.videoCodec.resolution[0]
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
// Recordings have no post-filter arguments.
|
|
374
|
-
postFilterArgs() {
|
|
375
|
-
return [];
|
|
376
|
-
}
|
|
377
|
-
// Metadata label identifying this as an HKSV event recording.
|
|
378
|
-
metadataLabel() {
|
|
379
|
-
return "HKSV Event";
|
|
380
|
-
}
|
|
381
|
-
// Each parsed box is queued in the recording buffer for consumption by segmentGenerator().
|
|
382
|
-
handleParsedBox(header, data, dataLength, type) {
|
|
383
|
-
this.recordingBuffer.push({ data, header, length: dataLength, type });
|
|
384
|
-
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);
|
|
385
341
|
}
|
|
386
342
|
/**
|
|
387
|
-
*
|
|
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.
|
|
388
347
|
*/
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
this.isEnded = true;
|
|
392
|
-
this.emit("mp4box");
|
|
393
|
-
super.stopProcess();
|
|
348
|
+
async getInitSegment() {
|
|
349
|
+
return this.#assembler.initSegment;
|
|
394
350
|
}
|
|
395
351
|
/**
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
* This async generator yields fMP4 segments as Buffers, or ends on process termination or timeout.
|
|
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.
|
|
399
354
|
*
|
|
400
|
-
* @
|
|
355
|
+
* @param init - Optional init options. `signal` composes with the process's own signal; aborting it terminates only this generator call, not the process.
|
|
401
356
|
*
|
|
402
|
-
* @
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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}.
|
|
406
365
|
*
|
|
407
|
-
*
|
|
408
|
-
* }
|
|
409
|
-
* ```
|
|
366
|
+
* @returns The buffered-segment depth.
|
|
410
367
|
*/
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
// Loop forever, generating either FTYP/MOOV box pairs or MOOF/MDAT box pairs for HomeKit Secure Video.
|
|
414
|
-
for (;;) {
|
|
415
|
-
// FFmpeg has finished its output - we're done.
|
|
416
|
-
if (this.isEnded) {
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
// If the buffer is empty, wait for our FFmpeg process to produce more boxes.
|
|
420
|
-
if (!this.recordingBuffer.length) {
|
|
421
|
-
// 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,
|
|
422
|
-
// we flag it accordingly and return null back to the generator that's calling us.
|
|
423
|
-
// eslint-disable-next-line no-await-in-loop
|
|
424
|
-
await runWithTimeout(once(this, "mp4box"), HKSV_TIMEOUT);
|
|
425
|
-
}
|
|
426
|
-
// Grab the next fMP4 box from our buffer.
|
|
427
|
-
const box = this.recordingBuffer.shift();
|
|
428
|
-
// 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.
|
|
429
|
-
if (!box) {
|
|
430
|
-
this.isTimedOut = true;
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
|
-
// Queue up this fMP4 box to send back to HomeKit.
|
|
434
|
-
segment.push(box.header, box.data);
|
|
435
|
-
// What we want to send are two types of complete segments, made up of multiple MP4 boxes:
|
|
436
|
-
//
|
|
437
|
-
// - 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
|
|
438
|
-
// before anything else.
|
|
439
|
-
//
|
|
440
|
-
// - 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
|
|
441
|
-
// of MOOF as the audio/video data "header", and MDAT as the "payload".
|
|
442
|
-
//
|
|
443
|
-
// Once we see these, we combine all the segments in our queue to send back to HomeKit.
|
|
444
|
-
if ((box.type === "moov") || (box.type === "mdat")) {
|
|
445
|
-
yield Buffer.concat(segment);
|
|
446
|
-
segment = [];
|
|
447
|
-
}
|
|
448
|
-
}
|
|
368
|
+
get bufferedSegments() {
|
|
369
|
+
return this.#assembler.bufferedSegments;
|
|
449
370
|
}
|
|
450
371
|
}
|
|
451
372
|
/**
|
|
452
|
-
*
|
|
373
|
+
* FFmpeg process specialization for HomeKit Secure Video (HKSV) event recording. Builds its command line from the provided HKSV recording configuration and delegates
|
|
374
|
+
* segment production to {@link FfmpegFMp4Process}. Overrides `FfmpegProcess.logFailedTeardown` to substitute a friendly user-facing message when the stderr log
|
|
375
|
+
* matches one of the tolerated HKSV error patterns, suppressing the canonical ERROR dump for those known benign cases. Also overrides
|
|
376
|
+
* `FfmpegProcess.logTimeoutTeardown` to demote the benign inter-segment watchdog reap to debug - a recording ends exactly this way when its segment source quiets,
|
|
377
|
+
* so the base's WARN would be alarming.
|
|
453
378
|
*
|
|
454
379
|
* @example
|
|
455
380
|
*
|
|
456
381
|
* ```ts
|
|
457
|
-
*
|
|
458
|
-
*
|
|
382
|
+
* await using proc = new FfmpegRecordingProcess(ffmpegOptions, {
|
|
383
|
+
*
|
|
384
|
+
* recording: { fps: 30, probesize: 5_000_000, timeshift: 0 },
|
|
385
|
+
* recordingConfig,
|
|
386
|
+
* signal: delegate.abortController.signal
|
|
387
|
+
* });
|
|
388
|
+
*
|
|
389
|
+
* const init = await proc.getInitSegment();
|
|
459
390
|
*
|
|
460
|
-
* const
|
|
391
|
+
* for await (const segment of proc.segments()) {
|
|
392
|
+
*
|
|
393
|
+
* // Forward each media segment to HomeKit.
|
|
394
|
+
* }
|
|
461
395
|
* ```
|
|
462
396
|
*
|
|
463
397
|
* @see FfmpegFMp4Process
|
|
398
|
+
* @see FfmpegProcess
|
|
464
399
|
*
|
|
465
400
|
* @category FFmpeg
|
|
466
401
|
*/
|
|
467
|
-
export class
|
|
468
|
-
/**
|
|
469
|
-
* Optional override for the fMP4 fragment duration, in milliseconds. When set, the `-frag_duration` argument is updated before starting the FFmpeg process.
|
|
470
|
-
*/
|
|
471
|
-
segmentLength;
|
|
472
|
-
// Set to true during separateAudioInputArgs() when a separate audio input is configured, so that audioInputIndex() returns the correct FFmpeg input index.
|
|
473
|
-
_hasAudioInput;
|
|
474
|
-
_initSegment;
|
|
475
|
-
_initSegmentParts;
|
|
476
|
-
hasInitSegment;
|
|
477
|
-
livestreamOptions;
|
|
402
|
+
export class FfmpegRecordingProcess extends FfmpegFMp4Process {
|
|
478
403
|
/**
|
|
479
|
-
*
|
|
404
|
+
* Construct and spawn a new HKSV recording process.
|
|
480
405
|
*
|
|
481
|
-
* @param options
|
|
482
|
-
* @param
|
|
483
|
-
* @param livestreamOptions - livestream segmenting options.
|
|
484
|
-
* @param isVerbose - If `true`, enables more verbose logging for debugging purposes. Defaults to `false`.
|
|
406
|
+
* @param options - Shared {@link FfmpegOptions} configuration (codec support, logger, debug flag, name).
|
|
407
|
+
* @param init - Init options. See {@link FfmpegRecordingInit}.
|
|
485
408
|
*/
|
|
486
|
-
constructor(options,
|
|
487
|
-
super(options,
|
|
488
|
-
// Store livestream-specific options.
|
|
489
|
-
this._hasAudioInput = false;
|
|
490
|
-
this._initSegment = Buffer.alloc(0);
|
|
491
|
-
this._initSegmentParts = [];
|
|
492
|
-
this.hasInitSegment = false;
|
|
493
|
-
this.livestreamOptions = livestreamOptions;
|
|
494
|
-
// Assemble the FFmpeg command line now that all state is initialized.
|
|
495
|
-
this.buildCommandLine();
|
|
496
|
-
}
|
|
497
|
-
// Livestream input: connect to an RTSP source with direct I/O and TCP transport.
|
|
498
|
-
//
|
|
499
|
-
// -avioflags direct Tell FFmpeg to minimize buffering to reduce latency for more realtime processing.
|
|
500
|
-
// -rtsp_transport tcp Tell the RTSP stream handler that we're looking for a TCP connection.
|
|
501
|
-
// -i url RTSPS URL to get our input stream from.
|
|
502
|
-
inputArgs() {
|
|
503
|
-
return [
|
|
504
|
-
"-avioflags", "direct",
|
|
505
|
-
"-rtsp_transport", "tcp",
|
|
506
|
-
"-i", this.livestreamOptions.url
|
|
507
|
-
];
|
|
508
|
-
}
|
|
509
|
-
// 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
|
|
510
|
-
// served from different endpoints.
|
|
511
|
-
separateAudioInputArgs() {
|
|
512
|
-
if (!this.fMp4Options.enableAudio || !this.livestreamOptions.audioInput) {
|
|
513
|
-
return [];
|
|
514
|
-
}
|
|
515
|
-
const args = [];
|
|
516
|
-
// Normalize the audio input configuration. A plain string is treated as a URL shorthand.
|
|
517
|
-
const audioInput = (typeof this.livestreamOptions.audioInput === "string") ?
|
|
518
|
-
{ url: this.livestreamOptions.audioInput } :
|
|
519
|
-
this.livestreamOptions.audioInput;
|
|
520
|
-
// 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.
|
|
521
|
-
//
|
|
522
|
-
// -f format Specify the raw audio format (e.g., mulaw, alaw, s16le).
|
|
523
|
-
// -ar sampleRate Specify the audio sample rate in Hz.
|
|
524
|
-
// -ac channels Specify the number of audio channels.
|
|
525
|
-
if (audioInput.format) {
|
|
526
|
-
args.push("-f", audioInput.format, "-ar", (audioInput.sampleRate ?? 8000).toString(), "-ac", (audioInput.channels ?? 1).toString());
|
|
527
|
-
}
|
|
528
|
-
// For RTSP and RTSPS audio sources, we explicitly request TCP transport to match the behavior we use for the primary video input.
|
|
529
|
-
if (["rtsp://", "rtsps://"].some((protocol) => audioInput.url.toLowerCase().startsWith(protocol))) {
|
|
530
|
-
args.push("-rtsp_transport", "tcp");
|
|
531
|
-
}
|
|
532
|
-
// -i url Audio input URL.
|
|
533
|
-
args.push("-i", audioInput.url);
|
|
534
|
-
// Track that we have a separate audio input so audioInputIndex() returns the correct value.
|
|
535
|
-
this._hasAudioInput = true;
|
|
536
|
-
return args;
|
|
537
|
-
}
|
|
538
|
-
// When a separate audio input is configured, audio is on the second FFmpeg input (index 1). Otherwise it shares the primary input (index 0).
|
|
539
|
-
audioInputIndex() {
|
|
540
|
-
return this._hasAudioInput ? 1 : 0;
|
|
541
|
-
}
|
|
542
|
-
// Livestreams remux the video stream directly without transcoding.
|
|
543
|
-
videoEncoderArgs() {
|
|
544
|
-
return ["-codec:v", "copy"];
|
|
545
|
-
}
|
|
546
|
-
// Livestreams emit fMP4 fragments at one-second intervals by default.
|
|
547
|
-
//
|
|
548
|
-
// -frag_duration number Length of each fMP4 fragment, in microseconds.
|
|
549
|
-
postFilterArgs() {
|
|
550
|
-
return ["-frag_duration", "1000000"];
|
|
551
|
-
}
|
|
552
|
-
// Metadata label identifying this as a livestream buffer.
|
|
553
|
-
metadataLabel() {
|
|
554
|
-
return "Livestream Buffer";
|
|
555
|
-
}
|
|
556
|
-
// Livestream box handling: accumulate the initialization segment (everything before the first moof box), then emit each subsequent box as a segment event.
|
|
557
|
-
handleParsedBox(header, data, _dataLength, type) {
|
|
558
|
-
// If this is part of the initialization segment, store it for future use.
|
|
559
|
-
if (!this.hasInitSegment) {
|
|
560
|
-
// 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
|
|
561
|
-
// array and concatenate once at the end to avoid creating intermediate buffers on every pre-moof box.
|
|
562
|
-
if (type === "moof") {
|
|
563
|
-
this._initSegment = Buffer.concat(this._initSegmentParts);
|
|
564
|
-
this._initSegmentParts = [];
|
|
565
|
-
this.hasInitSegment = true;
|
|
566
|
-
this.emit("initsegment");
|
|
567
|
-
}
|
|
568
|
-
else {
|
|
569
|
-
this._initSegmentParts.push(header, data);
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
if (this.hasInitSegment) {
|
|
573
|
-
// We only emit segments once we have the initialization segment.
|
|
574
|
-
this.emit("segment", Buffer.concat([header, data]));
|
|
575
|
-
}
|
|
409
|
+
constructor(options, init) {
|
|
410
|
+
super(options, { ...init, args: init.args ?? buildRecordingCommandLine(options, init) }, HKSV_TIMEOUT);
|
|
576
411
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
* ```
|
|
585
|
-
*/
|
|
586
|
-
start() {
|
|
587
|
-
if (this.segmentLength !== undefined) {
|
|
588
|
-
const fragIndex = this.commandLineArgs.indexOf("-frag_duration");
|
|
589
|
-
if (fragIndex !== -1) {
|
|
590
|
-
this.commandLineArgs[fragIndex + 1] = (this.segmentLength * 1000).toString();
|
|
591
|
-
}
|
|
412
|
+
// 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
|
|
413
|
+
// 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`.
|
|
414
|
+
// Policy is driven entirely by `signal.reason` and the observed stderr, with no class state of our own.
|
|
415
|
+
logFailedTeardown(reason) {
|
|
416
|
+
if (this.stderrLog.some((line) => FFMPEG_KNOWN_HKSV_ERROR.test(line))) {
|
|
417
|
+
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());
|
|
418
|
+
return;
|
|
592
419
|
}
|
|
593
|
-
|
|
594
|
-
super.start();
|
|
420
|
+
super.logFailedTeardown(reason);
|
|
595
421
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
* ```ts
|
|
604
|
-
* const initSegment = await process.getInitSegment();
|
|
605
|
-
* ```
|
|
606
|
-
*/
|
|
607
|
-
async getInitSegment() {
|
|
608
|
-
// If we have the initialization segment, return it.
|
|
609
|
-
if (this.hasInitSegment) {
|
|
610
|
-
return this._initSegment;
|
|
611
|
-
}
|
|
612
|
-
// Wait until the initialization segment is seen and then try again.
|
|
613
|
-
await once(this, "initsegment");
|
|
614
|
-
return this.getInitSegment();
|
|
422
|
+
// FfmpegRecordingProcess is the only fMP4 process that arms an inter-segment watchdog - it alone passes HKSV_TIMEOUT to super, while the sibling
|
|
423
|
+
// FfmpegLivestreamProcess passes no segmentTimeout, so that watchdog never fires there. The watchdog firing is benign by default - it is simply how an HKSV recording
|
|
424
|
+
// 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
|
|
425
|
+
// 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,
|
|
426
|
+
// 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.
|
|
427
|
+
logTimeoutTeardown(_reason) {
|
|
428
|
+
this.log.debug("%s: the recording's inter-segment watchdog window elapsed with no new segment.", this.options.name());
|
|
615
429
|
}
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* The production {@link RecordingProcessFactory}: builds the concrete FFmpeg-backed recording process. A consumer holds this typed as the abstraction; a test substitutes
|
|
433
|
+
* a fake factory. `create` is exactly the {@link FfmpegRecordingProcess} constructor call, so wiring construction through this seam is behavior-neutral.
|
|
434
|
+
*
|
|
435
|
+
* @see RecordingProcessFactory
|
|
436
|
+
*
|
|
437
|
+
* @category FFmpeg
|
|
438
|
+
*/
|
|
439
|
+
export const recordingProcessFactory = {
|
|
440
|
+
create: (options, init) => new FfmpegRecordingProcess(options, init)
|
|
441
|
+
};
|
|
442
|
+
/**
|
|
443
|
+
* FFmpeg process specialization for fMP4 livestreaming from an RTSP source. Builds its command line from the provided livestream source and delegates segment production
|
|
444
|
+
* to {@link FfmpegFMp4Process}.
|
|
445
|
+
*
|
|
446
|
+
* 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).
|
|
447
|
+
* Matches the polymorphic `{ getInitSegment(): Promise<Buffer>; segments(): AsyncGenerator<Buffer> }` interface HBUP uses across the native Protect livestream and this
|
|
448
|
+
* 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
|
|
449
|
+
* 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`.
|
|
450
|
+
*
|
|
451
|
+
* @example
|
|
452
|
+
*
|
|
453
|
+
* ```ts
|
|
454
|
+
* await using proc = new FfmpegLivestreamProcess(ffmpegOptions, {
|
|
455
|
+
*
|
|
456
|
+
* audio: { codec: AudioRecordingCodecType.AAC_LC, samplerate: AudioRecordingSamplerate.KHZ_16 },
|
|
457
|
+
* livestream: { url: "rtsp://camera/stream" },
|
|
458
|
+
* segmentLength: 1000,
|
|
459
|
+
* signal: session.controller.signal
|
|
460
|
+
* });
|
|
461
|
+
*
|
|
462
|
+
* const init = await proc.getInitSegment();
|
|
463
|
+
*
|
|
464
|
+
* for await (const segment of proc.segments()) {
|
|
465
|
+
*
|
|
466
|
+
* // Forward each media segment to the downstream consumer.
|
|
467
|
+
* }
|
|
468
|
+
* ```
|
|
469
|
+
*
|
|
470
|
+
* @see FfmpegFMp4Process
|
|
471
|
+
* @see FfmpegProcess
|
|
472
|
+
*
|
|
473
|
+
* @category FFmpeg
|
|
474
|
+
*/
|
|
475
|
+
export class FfmpegLivestreamProcess extends FfmpegFMp4Process {
|
|
616
476
|
/**
|
|
617
|
-
*
|
|
618
|
-
*
|
|
619
|
-
* @returns The initialization segment Buffer, or `null` if not yet generated.
|
|
620
|
-
*
|
|
621
|
-
* @example
|
|
477
|
+
* Construct and spawn a new fMP4 livestream process.
|
|
622
478
|
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
625
|
-
* if(init) {
|
|
626
|
-
*
|
|
627
|
-
* // Use the initialization segment.
|
|
628
|
-
* }
|
|
629
|
-
* ```
|
|
479
|
+
* @param options - Shared {@link FfmpegOptions} configuration (codec support, logger, debug flag, name).
|
|
480
|
+
* @param init - Init options. See {@link FfmpegLivestreamInit}.
|
|
630
481
|
*/
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
return null;
|
|
634
|
-
}
|
|
635
|
-
return this._initSegment;
|
|
482
|
+
constructor(options, init) {
|
|
483
|
+
super(options, { ...init, args: init.args ?? buildLivestreamCommandLine(options, init) });
|
|
636
484
|
}
|
|
637
485
|
}
|
|
638
486
|
//# sourceMappingURL=record.js.map
|