homebridge-plugin-utils 1.33.0 → 1.35.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/build/tsconfig.json +6 -4
- package/dist/backpressure.d.ts +79 -0
- package/dist/backpressure.js +137 -0
- package/dist/backpressure.js.map +1 -0
- package/dist/featureoptions.d.ts +3 -3
- package/dist/featureoptions.js +122 -97
- package/dist/featureoptions.js.map +1 -1
- package/dist/ffmpeg/codecs.js +26 -20
- package/dist/ffmpeg/codecs.js.map +1 -1
- package/dist/ffmpeg/exec.d.ts +1 -1
- package/dist/ffmpeg/exec.js.map +1 -1
- package/dist/ffmpeg/fmp4.d.ts +21 -2
- package/dist/ffmpeg/fmp4.js +55 -2
- package/dist/ffmpeg/fmp4.js.map +1 -1
- package/dist/ffmpeg/options.d.ts +6 -1
- package/dist/ffmpeg/options.js +13 -14
- package/dist/ffmpeg/options.js.map +1 -1
- package/dist/ffmpeg/process.d.ts +26 -14
- package/dist/ffmpeg/process.js +60 -36
- package/dist/ffmpeg/process.js.map +1 -1
- package/dist/ffmpeg/record.d.ts +121 -122
- package/dist/ffmpeg/record.js +363 -285
- package/dist/ffmpeg/record.js.map +1 -1
- package/dist/ffmpeg/rtp.d.ts +2 -2
- package/dist/ffmpeg/rtp.js +9 -2
- package/dist/ffmpeg/rtp.js.map +1 -1
- package/dist/ffmpeg/settings.d.ts +1 -0
- package/dist/ffmpeg/settings.js +3 -0
- package/dist/ffmpeg/settings.js.map +1 -1
- package/dist/ffmpeg/stream.d.ts +1 -1
- package/dist/ffmpeg/stream.js +6 -4
- package/dist/ffmpeg/stream.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mqttclient.js +6 -5
- package/dist/mqttclient.js.map +1 -1
- package/dist/service.d.ts +1 -1
- package/dist/service.js +10 -4
- package/dist/service.js.map +1 -1
- package/dist/ui/featureoptions.js +122 -97
- package/dist/ui/featureoptions.js.map +1 -1
- package/dist/util.d.ts +18 -19
- package/dist/util.js +43 -21
- package/dist/util.js.map +1 -1
- package/package.json +7 -8
package/dist/ffmpeg/record.js
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { HKSV_IDR_INTERVAL, HKSV_TIMEOUT } from "./settings.js";
|
|
6
6
|
import { runWithTimeout } from "../util.js";
|
|
7
|
+
import { BOX_HEADER_SIZE } from "./fmp4.js";
|
|
7
8
|
import { FfmpegProcess } from "./process.js";
|
|
8
|
-
import events from "node:events";
|
|
9
9
|
import { once } from "node:events";
|
|
10
|
-
// Utility to map HKSV audio recording
|
|
11
|
-
// updating this mapping.
|
|
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.
|
|
12
12
|
const translateAudioRecordingCodecType = {
|
|
13
13
|
[1 /* AudioRecordingCodecType.AAC_ELD */]: "38",
|
|
14
14
|
[0 /* AudioRecordingCodecType.AAC_LC */]: "1"
|
|
@@ -22,80 +22,77 @@ const translateAudioSampleRate = {
|
|
|
22
22
|
[4 /* AudioRecordingSamplerate.KHZ_44_1 */]: "44.1",
|
|
23
23
|
[5 /* AudioRecordingSamplerate.KHZ_48 */]: "48"
|
|
24
24
|
};
|
|
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.
|
|
33
|
+
const FFMPEG_KNOWN_HKSV_ERROR = new RegExp([
|
|
34
|
+
"(Cannot determine format of input stream 0:0 after EOF)",
|
|
35
|
+
"(Could not write header \\(incorrect codec parameters \\?\\): Broken pipe)",
|
|
36
|
+
"(Could not write header for output file #0)",
|
|
37
|
+
"(Error closing file: Broken pipe)",
|
|
38
|
+
"(Error splitting the input into NAL units\\.)",
|
|
39
|
+
"(Invalid data found when processing input)",
|
|
40
|
+
"(moov atom not found)"
|
|
41
|
+
].join("|"));
|
|
25
42
|
/**
|
|
26
|
-
*
|
|
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.
|
|
27
46
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* @example
|
|
32
|
-
*
|
|
33
|
-
* ```ts
|
|
34
|
-
* // Create a new recording process for an HKSV event.
|
|
35
|
-
* const process = new FfmpegRecordingProcess(ffmpegOptions, recordingConfig, 30, true, 5000000, 0);
|
|
36
|
-
*
|
|
37
|
-
* // Start the process.
|
|
38
|
-
* process.start();
|
|
39
|
-
*
|
|
40
|
-
* // Iterate over generated segments.
|
|
41
|
-
* for await(const segment of process.segmentGenerator()) {
|
|
42
|
-
*
|
|
43
|
-
* // Send segment to HomeKit, etc.
|
|
44
|
-
* }
|
|
45
|
-
*
|
|
46
|
-
* // Stop when finished.
|
|
47
|
-
* process.stop();
|
|
48
|
-
* ```
|
|
49
|
-
*
|
|
50
|
-
* @see FfmpegOptions
|
|
47
|
+
* @see FfmpegRecordingProcess
|
|
48
|
+
* @see FfmpegLivestreamProcess
|
|
51
49
|
* @see FfmpegProcess
|
|
52
50
|
* @see {@link https://ffmpeg.org/ffmpeg.html | FFmpeg Documentation}
|
|
53
51
|
*/
|
|
54
52
|
class FfmpegFMp4Process extends FfmpegProcess {
|
|
55
|
-
hasInitSegment;
|
|
56
|
-
_initSegment;
|
|
57
|
-
isLivestream;
|
|
58
53
|
isLoggingErrors;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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;
|
|
62
58
|
/**
|
|
63
|
-
* Constructs a new fMP4 FFmpeg process
|
|
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.
|
|
64
61
|
*
|
|
65
62
|
* @param ffmpegOptions - FFmpeg configuration options.
|
|
66
63
|
* @param recordingConfig - HomeKit recording configuration for the session.
|
|
67
|
-
* @param
|
|
68
|
-
* @param fMp4Options - Configuration for the fMP4 session (fps, type, url, audio input, etc.).
|
|
64
|
+
* @param fMp4Options - Partial base options with defaults applied for any unset fields.
|
|
69
65
|
* @param isVerbose - If `true`, enables more verbose logging for debugging purposes. Defaults to `false`.
|
|
70
|
-
*
|
|
71
|
-
* @example
|
|
72
|
-
*
|
|
73
|
-
* ```ts
|
|
74
|
-
* const process = new FfmpegFMp4Process(ffmpegOptions, recordingConfig, true, { fps: 30 });
|
|
75
|
-
* ```
|
|
76
66
|
*/
|
|
77
67
|
constructor(ffmpegOptions, recordingConfig, fMp4Options = {}, isVerbose = false) {
|
|
78
68
|
// Initialize our parent.
|
|
79
69
|
super(ffmpegOptions);
|
|
80
70
|
// We want to log errors when they occur.
|
|
81
71
|
this.isLoggingErrors = true;
|
|
82
|
-
//
|
|
83
|
-
this.
|
|
84
|
-
|
|
85
|
-
this.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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;
|
|
89
|
+
}
|
|
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() {
|
|
99
96
|
// Configure our video parameters for our input:
|
|
100
97
|
//
|
|
101
98
|
// -hide_banner Suppress printing the startup banner in FFmpeg.
|
|
@@ -108,100 +105,53 @@ class FfmpegFMp4Process extends FfmpegProcess {
|
|
|
108
105
|
"-nostats",
|
|
109
106
|
"-fflags", "+discardcorrupt",
|
|
110
107
|
"-err_detect", "ignore_err",
|
|
111
|
-
...this.options.videoDecoder(fMp4Options.codec),
|
|
112
|
-
"-max_delay", "500000"
|
|
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()
|
|
113
114
|
];
|
|
114
|
-
// Track which FFmpeg input index contains the audio stream. By default, audio and video share the same input (index 0). When a separate audio input is provided
|
|
115
|
-
// for livestreaming, the audio input becomes index 1.
|
|
116
|
-
let audioInputIndex = 0;
|
|
117
|
-
if (this.isLivestream && fMp4Options.url) {
|
|
118
|
-
// -avioflags direct Tell FFmpeg to minimize buffering to reduce latency for more realtime processing.
|
|
119
|
-
// -rtsp_transport tcp Tell the RTSP stream handler that we're looking for a TCP connection.
|
|
120
|
-
// -i url RTSPS URL to get our input stream from.
|
|
121
|
-
this.commandLineArgs.push("-avioflags", "direct", "-rtsp_transport", "tcp", "-i", fMp4Options.url);
|
|
122
|
-
}
|
|
123
|
-
else {
|
|
124
|
-
// -flags low_delay Tell FFmpeg to optimize for low delay / realtime decoding.
|
|
125
|
-
// -probesize number How many bytes should be analyzed for stream information.
|
|
126
|
-
// -f mp4 Tell FFmpeg that it should expect an MP4-encoded input stream.
|
|
127
|
-
// -i pipe:0 Use standard input to get video data.
|
|
128
|
-
// -ss Fast forward to where HKSV is expecting us to be for a recording event.
|
|
129
|
-
this.commandLineArgs.push("-flags", "low_delay", "-probesize", (fMp4Options.probesize ?? 5000000).toString(), "-f", "mp4", "-i", "pipe:0", "-ss", (fMp4Options.timeshift ?? 0).toString() + "ms");
|
|
130
|
-
}
|
|
131
|
-
// If a separate audio input has been configured for livestreaming, add it as a second FFmpeg input. This enables support for devices like DoorBird where video and
|
|
132
|
-
// audio are served from different endpoints.
|
|
133
|
-
if (this.isLivestream && fMp4Options.enableAudio && fMp4Options.audioInput) {
|
|
134
|
-
// Normalize the audio input configuration. A plain string is treated as a URL shorthand.
|
|
135
|
-
const audioInput = (typeof fMp4Options.audioInput === "string") ?
|
|
136
|
-
{ url: fMp4Options.audioInput } :
|
|
137
|
-
fMp4Options.audioInput;
|
|
138
|
-
// 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.
|
|
139
|
-
//
|
|
140
|
-
// -f format Specify the raw audio format (e.g., mulaw, alaw, s16le).
|
|
141
|
-
// -ar sampleRate Specify the audio sample rate in Hz.
|
|
142
|
-
// -ac channels Specify the number of audio channels.
|
|
143
|
-
if (audioInput.format) {
|
|
144
|
-
this.commandLineArgs.push("-f", audioInput.format, "-ar", (audioInput.sampleRate ?? 8000).toString(), "-ac", (audioInput.channels ?? 1).toString());
|
|
145
|
-
}
|
|
146
|
-
// For RTSP and RTSPS audio sources, we explicitly request TCP transport to match the behavior we use for the primary video input.
|
|
147
|
-
if (["rtsp://", "rtsps://"].some((protocol) => audioInput.url.toLowerCase().startsWith(protocol))) {
|
|
148
|
-
this.commandLineArgs.push("-rtsp_transport", "tcp");
|
|
149
|
-
}
|
|
150
|
-
// -i url Audio input URL.
|
|
151
|
-
this.commandLineArgs.push("-i", audioInput.url);
|
|
152
|
-
// Audio is now on the second input (index 1) instead of the primary input (index 0).
|
|
153
|
-
audioInputIndex = 1;
|
|
154
|
-
}
|
|
155
115
|
// Configure our recording options for the video stream:
|
|
156
116
|
//
|
|
157
117
|
// -map 0:v:X Selects the video track from the input.
|
|
158
|
-
this.commandLineArgs.push("-map", "0:v:" + fMp4Options.videoStream.toString(),
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
hardwareDecoding: fMp4Options.hardwareDecoding,
|
|
162
|
-
hardwareTranscoding: fMp4Options.hardwareTranscoding,
|
|
163
|
-
height: recordingConfig.videoCodec.resolution[1],
|
|
164
|
-
idrInterval: HKSV_IDR_INTERVAL,
|
|
165
|
-
inputFps: fMp4Options.fps,
|
|
166
|
-
level: recordingConfig.videoCodec.parameters.level,
|
|
167
|
-
profile: recordingConfig.videoCodec.parameters.profile,
|
|
168
|
-
width: recordingConfig.videoCodec.resolution[0]
|
|
169
|
-
})));
|
|
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());
|
|
170
121
|
// Configure our video filters, if we have them.
|
|
171
|
-
if (fMp4Options.videoFilters.length) {
|
|
172
|
-
this.commandLineArgs.push("-filter:v", fMp4Options.videoFilters.join(", "));
|
|
173
|
-
}
|
|
174
|
-
// If we're livestreaming, emit fragments at one-second intervals.
|
|
175
|
-
if (this.isLivestream) {
|
|
176
|
-
// -frag_duration number Length of each fMP4 fragment, in microseconds.
|
|
177
|
-
this.commandLineArgs.push("-frag_duration", "1000000");
|
|
122
|
+
if (this.fMp4Options.videoFilters.length) {
|
|
123
|
+
this.commandLineArgs.push("-filter:v", this.fMp4Options.videoFilters.join(", "));
|
|
178
124
|
}
|
|
125
|
+
// Mode-specific post-filter arguments (frag_duration for livestream, empty for recording).
|
|
126
|
+
this.commandLineArgs.push(...this.postFilterArgs());
|
|
179
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
|
|
180
128
|
// at each keyframe, skip creating a segment index (SIDX) box in fragments, and skip writing the final MOOV trailer since it's unneeded.
|
|
181
129
|
// -flush_packets 1 Ensure we flush our write buffer after each muxed packet.
|
|
182
130
|
// -reset_timestamps Reset timestamps at the beginning of each segment.
|
|
183
131
|
// -metadata Set the metadata to the name of the camera to distinguish between FFmpeg sessions.
|
|
184
|
-
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() + " " +
|
|
185
|
-
|
|
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) {
|
|
186
136
|
// Configure the audio portion of the command line. Options we use are:
|
|
187
137
|
//
|
|
188
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
|
|
189
139
|
// separate audio input has been configured.
|
|
190
|
-
this.commandLineArgs.push("-map", audioInputIndex.toString() + ":a:" + fMp4Options.audioStream.toString() + "?");
|
|
140
|
+
this.commandLineArgs.push("-map", this.audioInputIndex().toString() + ":a:" + this.fMp4Options.audioStream.toString() + "?");
|
|
191
141
|
// Configure our audio filters, if we have them.
|
|
192
|
-
if (fMp4Options.audioFilters.length) {
|
|
193
|
-
this.commandLineArgs.push("-filter:a", fMp4Options.audioFilters.join(", "));
|
|
194
|
-
// Audio filters require transcoding. If the user
|
|
195
|
-
|
|
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;
|
|
196
146
|
}
|
|
197
|
-
if (
|
|
147
|
+
if (transcodeAudio) {
|
|
198
148
|
// Configure the audio portion of the command line. Options we use are:
|
|
199
149
|
//
|
|
200
150
|
// -codec:a Encode using the codecs available to us on given platforms.
|
|
201
151
|
// -profile:a Specify either low-complexity AAC or enhanced low-delay AAC for HKSV events.
|
|
202
152
|
// -ar samplerate Sample rate to use for this audio. This is specified by HKSV.
|
|
203
153
|
// -ac number Set the number of audio channels.
|
|
204
|
-
this.commandLineArgs.push(...this.options.audioEncoder({ codec: recordingConfig.audioCodec.type }), "-profile:a", translateAudioRecordingCodecType[recordingConfig.audioCodec.type], "-ar", translateAudioSampleRate[recordingConfig.audioCodec.samplerate] + "k", "-ac", (recordingConfig.audioCodec.audioChannels ?? 1).toString());
|
|
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());
|
|
205
155
|
}
|
|
206
156
|
else {
|
|
207
157
|
// Configure the audio portion of the command line. Options we use are:
|
|
@@ -216,51 +166,54 @@ class FfmpegFMp4Process extends FfmpegProcess {
|
|
|
216
166
|
// pipe:1 Output the stream to standard output.
|
|
217
167
|
this.commandLineArgs.push("-f", "mp4", "pipe:1");
|
|
218
168
|
// Additional logging, but only if we're debugging.
|
|
219
|
-
if (
|
|
169
|
+
if (this._isVerbose || this.isVerbose) {
|
|
220
170
|
this.commandLineArgs.unshift("-loglevel", "level+verbose");
|
|
221
171
|
}
|
|
222
172
|
}
|
|
223
173
|
/**
|
|
224
|
-
* Prepares and configures the FFmpeg process for reading and parsing output fMP4 data.
|
|
225
|
-
*
|
|
226
|
-
* This method is called internally by the process lifecycle and is not typically invoked directly by consumers.
|
|
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().
|
|
227
176
|
*/
|
|
228
177
|
configureProcess() {
|
|
229
178
|
let dataListener;
|
|
230
179
|
// Call our parent to get started.
|
|
231
180
|
super.configureProcess();
|
|
232
181
|
// Initialize our variables that we need to process incoming FFmpeg packets.
|
|
233
|
-
let header =
|
|
234
|
-
let bufferRemaining =
|
|
182
|
+
let header = EMPTY_BUFFER;
|
|
183
|
+
let bufferRemaining = EMPTY_BUFFER;
|
|
235
184
|
let dataLength = 0;
|
|
236
|
-
let type =
|
|
237
|
-
// Process FFmpeg output and parse out the fMP4 stream it's generating
|
|
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.
|
|
238
188
|
this.process?.stdout.on("data", dataListener = (buffer) => {
|
|
239
189
|
// If we have anything left from the last buffer we processed, prepend it to this buffer.
|
|
240
190
|
if (bufferRemaining.length > 0) {
|
|
241
191
|
buffer = Buffer.concat([bufferRemaining, buffer]);
|
|
242
|
-
bufferRemaining =
|
|
192
|
+
bufferRemaining = EMPTY_BUFFER;
|
|
243
193
|
}
|
|
244
194
|
let offset = 0;
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
// boxes that HAP-NodeJS is ultimately expecting.
|
|
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.
|
|
248
197
|
for (;;) {
|
|
249
198
|
let data;
|
|
250
|
-
// 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
|
|
251
|
-
// container. It's composed of an 8-byte header, followed by the data payload it carries.
|
|
252
199
|
// No existing header, let's start a new box.
|
|
253
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
|
+
}
|
|
254
206
|
// Grab the header. The first four bytes represents the length of the entire box. Second four bytes represent the box type.
|
|
255
|
-
header = buffer.subarray(0,
|
|
207
|
+
header = buffer.subarray(0, BOX_HEADER_SIZE);
|
|
256
208
|
// Now we retrieve the length of the box.
|
|
257
209
|
dataLength = header.readUInt32BE(0);
|
|
258
|
-
//
|
|
259
|
-
|
|
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);
|
|
260
213
|
// Finally, we get the data portion of the box.
|
|
261
|
-
data = buffer.subarray(
|
|
214
|
+
data = buffer.subarray(BOX_HEADER_SIZE, dataLength);
|
|
262
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.
|
|
263
|
-
dataLength -= offset =
|
|
216
|
+
dataLength -= offset = BOX_HEADER_SIZE;
|
|
264
217
|
}
|
|
265
218
|
else {
|
|
266
219
|
// Grab the data from our buffer.
|
|
@@ -272,33 +225,11 @@ class FfmpegFMp4Process extends FfmpegProcess {
|
|
|
272
225
|
bufferRemaining = data;
|
|
273
226
|
break;
|
|
274
227
|
}
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
// If this is part of the initialization segment, store it for future use.
|
|
278
|
-
if (!this.hasInitSegment) {
|
|
279
|
-
// 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.
|
|
280
|
-
if (type === "moof") {
|
|
281
|
-
this.hasInitSegment = true;
|
|
282
|
-
this.emit("initsegment");
|
|
283
|
-
}
|
|
284
|
-
else {
|
|
285
|
-
this._initSegment = Buffer.concat([this._initSegment, header, data]);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
if (this.hasInitSegment) {
|
|
289
|
-
// We only emit segments once we have the initialization segment.
|
|
290
|
-
this.emit("segment", Buffer.concat([header, data]));
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
else {
|
|
294
|
-
// Add it to our queue to be eventually pushed out through our generator function.
|
|
295
|
-
this.recordingBuffer.push({ data: data, header: header, length: dataLength, type: type });
|
|
296
|
-
this.emit("mp4box");
|
|
297
|
-
}
|
|
228
|
+
// Dispatch the complete box to the subclass for mode-specific handling.
|
|
229
|
+
this.handleParsedBox(header, data, dataLength, type);
|
|
298
230
|
// Prepare to start a new box for the next buffer that we will be processing.
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
type = "";
|
|
231
|
+
header = EMPTY_BUFFER;
|
|
232
|
+
type = 0;
|
|
302
233
|
// We've parsed an entire box, and there's no more data in this buffer to parse.
|
|
303
234
|
if (buffer.length === (offset + dataLength)) {
|
|
304
235
|
dataLength = 0;
|
|
@@ -315,59 +246,16 @@ class FfmpegFMp4Process extends FfmpegProcess {
|
|
|
315
246
|
});
|
|
316
247
|
}
|
|
317
248
|
/**
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
* Waits until the initialization segment is available, then returns it.
|
|
321
|
-
*
|
|
322
|
-
* @returns A promise resolving to the initialization segment as a Buffer.
|
|
323
|
-
*
|
|
324
|
-
* @example
|
|
325
|
-
*
|
|
326
|
-
* ```ts
|
|
327
|
-
* const initSegment = await process.getInitSegment();
|
|
328
|
-
* ```
|
|
329
|
-
*/
|
|
330
|
-
async getInitSegment() {
|
|
331
|
-
// If we have the initialization segment, return it.
|
|
332
|
-
if (this.hasInitSegment) {
|
|
333
|
-
return this._initSegment;
|
|
334
|
-
}
|
|
335
|
-
// Wait until the initialization segment is seen and then try again.
|
|
336
|
-
await events.once(this, "initsegment");
|
|
337
|
-
return this.getInitSegment();
|
|
338
|
-
}
|
|
339
|
-
/**
|
|
340
|
-
* Stops the FFmpeg process and performs cleanup, including emitting termination events for segment generators.
|
|
341
|
-
*
|
|
342
|
-
* This is called as part of the process shutdown sequence.
|
|
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.
|
|
343
251
|
*/
|
|
344
252
|
stopProcess() {
|
|
345
253
|
// Call our parent to get started.
|
|
346
254
|
super.stopProcess();
|
|
347
|
-
//
|
|
348
|
-
this.
|
|
349
|
-
this.emit("mp4box");
|
|
255
|
+
// Signal that the process has ended.
|
|
256
|
+
this._isEnded = true;
|
|
350
257
|
this.emit("close");
|
|
351
258
|
}
|
|
352
|
-
/**
|
|
353
|
-
* Starts the FFmpeg process, adjusting segment length for livestreams if set.
|
|
354
|
-
*
|
|
355
|
-
* @example
|
|
356
|
-
*
|
|
357
|
-
* ```ts
|
|
358
|
-
* process.start();
|
|
359
|
-
* ```
|
|
360
|
-
*/
|
|
361
|
-
start() {
|
|
362
|
-
if (this.isLivestream && (this.segmentLength !== undefined)) {
|
|
363
|
-
const fragIndex = this.commandLineArgs.indexOf("-frag_duration");
|
|
364
|
-
if (fragIndex !== -1) {
|
|
365
|
-
this.commandLineArgs[fragIndex + 1] = (this.segmentLength * 1000).toString();
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
// Start the FFmpeg session.
|
|
369
|
-
super.start();
|
|
370
|
-
}
|
|
371
259
|
/**
|
|
372
260
|
* Stops the FFmpeg process and logs errors if specified.
|
|
373
261
|
*
|
|
@@ -399,24 +287,118 @@ class FfmpegFMp4Process extends FfmpegProcess {
|
|
|
399
287
|
if (!this.isLoggingErrors) {
|
|
400
288
|
return;
|
|
401
289
|
}
|
|
402
|
-
// Known HKSV-related errors due to occasional inconsistencies that are occasionally produced by the input stream and FFmpeg's own occasional quirkiness.
|
|
403
|
-
const ffmpegKnownHksvError = new RegExp([
|
|
404
|
-
"(Cannot determine format of input stream 0:0 after EOF)",
|
|
405
|
-
"(Could not write header \\(incorrect codec parameters \\?\\): Broken pipe)",
|
|
406
|
-
"(Could not write header for output file #0)",
|
|
407
|
-
"(Error closing file: Broken pipe)",
|
|
408
|
-
"(Error splitting the input into NAL units\\.)",
|
|
409
|
-
"(Invalid data found when processing input)",
|
|
410
|
-
"(moov atom not found)"
|
|
411
|
-
].join("|"));
|
|
412
290
|
// See if we know about this error.
|
|
413
|
-
if (this.stderrLog.some(x =>
|
|
291
|
+
if (this.stderrLog.some(x => FFMPEG_KNOWN_HKSV_ERROR.test(x))) {
|
|
414
292
|
this.log.error("FFmpeg ended unexpectedly due to issues processing the media stream. This error can be safely ignored - it will occur occasionally.");
|
|
415
293
|
return;
|
|
416
294
|
}
|
|
417
295
|
// Otherwise, revert to our default logging in our parent.
|
|
418
296
|
super.logFfmpegError(exitCode, signal);
|
|
419
297
|
}
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Manages a HomeKit Secure Video recording FFmpeg process.
|
|
301
|
+
*
|
|
302
|
+
* @example
|
|
303
|
+
*
|
|
304
|
+
* ```ts
|
|
305
|
+
* const process = new FfmpegRecordingProcess(ffmpegOptions, recordingConfig, 30, true, 5000000, 0);
|
|
306
|
+
* process.start();
|
|
307
|
+
* ```
|
|
308
|
+
*
|
|
309
|
+
* @see FfmpegFMp4Process
|
|
310
|
+
*
|
|
311
|
+
* @category FFmpeg
|
|
312
|
+
*/
|
|
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;
|
|
322
|
+
/**
|
|
323
|
+
* Constructs a new FFmpeg recording process for HKSV events.
|
|
324
|
+
*
|
|
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`.
|
|
329
|
+
*/
|
|
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");
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Stops the FFmpeg process and performs cleanup, ensuring the segment generator can exit.
|
|
395
|
+
*/
|
|
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();
|
|
401
|
+
}
|
|
420
402
|
/**
|
|
421
403
|
* Asynchronously generates complete segments from FFmpeg output, formatted for HomeKit Secure Video.
|
|
422
404
|
*
|
|
@@ -437,8 +419,8 @@ class FfmpegFMp4Process extends FfmpegProcess {
|
|
|
437
419
|
let segment = [];
|
|
438
420
|
// Loop forever, generating either FTYP/MOOV box pairs or MOOF/MDAT box pairs for HomeKit Secure Video.
|
|
439
421
|
for (;;) {
|
|
440
|
-
// FFmpeg has finished
|
|
441
|
-
if (this.
|
|
422
|
+
// FFmpeg has finished its output - we're done.
|
|
423
|
+
if (this._isEnded) {
|
|
442
424
|
return;
|
|
443
425
|
}
|
|
444
426
|
// If the buffer is empty, wait for our FFmpeg process to produce more boxes.
|
|
@@ -466,60 +448,12 @@ class FfmpegFMp4Process extends FfmpegProcess {
|
|
|
466
448
|
// of MOOF as the audio/video data "header", and MDAT as the "payload".
|
|
467
449
|
//
|
|
468
450
|
// Once we see these, we combine all the segments in our queue to send back to HomeKit.
|
|
469
|
-
if ((box.type ===
|
|
451
|
+
if ((box.type === BOX_TYPE_MOOV) || (box.type === BOX_TYPE_MDAT)) {
|
|
470
452
|
yield Buffer.concat(segment);
|
|
471
453
|
segment = [];
|
|
472
454
|
}
|
|
473
455
|
}
|
|
474
456
|
}
|
|
475
|
-
/**
|
|
476
|
-
* Returns the initialization segment as a Buffer, or null if not yet available.
|
|
477
|
-
*
|
|
478
|
-
* @returns The initialization segment Buffer, or `null` if not yet generated.
|
|
479
|
-
*
|
|
480
|
-
* @example
|
|
481
|
-
*
|
|
482
|
-
* ```ts
|
|
483
|
-
* const init = process.initSegment;
|
|
484
|
-
* if(init) {
|
|
485
|
-
*
|
|
486
|
-
* // Use the initialization segment.
|
|
487
|
-
* }
|
|
488
|
-
* ```
|
|
489
|
-
*/
|
|
490
|
-
get initSegment() {
|
|
491
|
-
if (!this.hasInitSegment) {
|
|
492
|
-
return null;
|
|
493
|
-
}
|
|
494
|
-
return this._initSegment;
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
/**
|
|
498
|
-
* Manages a HomeKit Secure Video recording FFmpeg process.
|
|
499
|
-
*
|
|
500
|
-
* @example
|
|
501
|
-
*
|
|
502
|
-
* ```ts
|
|
503
|
-
* const process = new FfmpegRecordingProcess(ffmpegOptions, recordingConfig, 30, true, 5000000, 0);
|
|
504
|
-
* process.start();
|
|
505
|
-
* ```
|
|
506
|
-
*
|
|
507
|
-
* @see FfmpegFMp4Process
|
|
508
|
-
*
|
|
509
|
-
* @category FFmpeg
|
|
510
|
-
*/
|
|
511
|
-
export class FfmpegRecordingProcess extends FfmpegFMp4Process {
|
|
512
|
-
/**
|
|
513
|
-
* Constructs a new FFmpeg recording process for HKSV events.
|
|
514
|
-
*
|
|
515
|
-
* @param options - FFmpeg configuration options.
|
|
516
|
-
* @param recordingConfig - HomeKit recording configuration for the session.
|
|
517
|
-
* @param fMp4Options - fMP4 recording options.
|
|
518
|
-
* @param isVerbose - If `true`, enables more verbose logging for debugging purposes. Defaults to `false`.
|
|
519
|
-
*/
|
|
520
|
-
constructor(options, recordingConfig, fMp4Options = {}, isVerbose = false) {
|
|
521
|
-
super(options, recordingConfig, fMp4Options, isVerbose);
|
|
522
|
-
}
|
|
523
457
|
}
|
|
524
458
|
/**
|
|
525
459
|
* Manages a HomeKit livestream FFmpeg process for generating fMP4 segments.
|
|
@@ -538,6 +472,16 @@ export class FfmpegRecordingProcess extends FfmpegFMp4Process {
|
|
|
538
472
|
* @category FFmpeg
|
|
539
473
|
*/
|
|
540
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;
|
|
541
485
|
/**
|
|
542
486
|
* Constructs a new FFmpeg livestream process.
|
|
543
487
|
*
|
|
@@ -548,6 +492,113 @@ export class FfmpegLivestreamProcess extends FfmpegFMp4Process {
|
|
|
548
492
|
*/
|
|
549
493
|
constructor(options, recordingConfig, livestreamOptions, isVerbose = false) {
|
|
550
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
|
+
}
|
|
583
|
+
}
|
|
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
|
+
}
|
|
599
|
+
}
|
|
600
|
+
// Start the FFmpeg session.
|
|
601
|
+
super.start();
|
|
551
602
|
}
|
|
552
603
|
/**
|
|
553
604
|
* Gets the fMP4 initialization segment generated by FFmpeg for the livestream.
|
|
@@ -561,7 +612,34 @@ export class FfmpegLivestreamProcess extends FfmpegFMp4Process {
|
|
|
561
612
|
* ```
|
|
562
613
|
*/
|
|
563
614
|
async getInitSegment() {
|
|
564
|
-
return
|
|
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;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
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
|
|
629
|
+
*
|
|
630
|
+
* ```ts
|
|
631
|
+
* const init = process.initSegment;
|
|
632
|
+
* if(init) {
|
|
633
|
+
*
|
|
634
|
+
* // Use the initialization segment.
|
|
635
|
+
* }
|
|
636
|
+
* ```
|
|
637
|
+
*/
|
|
638
|
+
get initSegment() {
|
|
639
|
+
if (!this.hasInitSegment) {
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
return this._initSegment;
|
|
565
643
|
}
|
|
566
644
|
}
|
|
567
645
|
//# sourceMappingURL=record.js.map
|