homebridge-plugin-utils 1.32.0 → 1.34.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/featureoptions.d.ts +3 -3
- package/dist/featureoptions.js +122 -97
- package/dist/featureoptions.js.map +1 -1
- package/dist/ffmpeg/codecs.js +20 -19
- 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 +71 -0
- package/dist/ffmpeg/fmp4.js +144 -0
- package/dist/ffmpeg/fmp4.js.map +1 -0
- package/dist/ffmpeg/index.d.ts +1 -0
- package/dist/ffmpeg/index.js +1 -0
- package/dist/ffmpeg/index.js.map +1 -1
- package/dist/ffmpeg/options.d.ts +13 -0
- package/dist/ffmpeg/options.js +22 -13
- package/dist/ffmpeg/options.js.map +1 -1
- package/dist/ffmpeg/process.d.ts +8 -2
- package/dist/ffmpeg/process.js +20 -9
- package/dist/ffmpeg/process.js.map +1 -1
- package/dist/ffmpeg/record.d.ts +121 -122
- package/dist/ffmpeg/record.js +346 -275
- package/dist/ffmpeg/record.js.map +1 -1
- package/dist/ffmpeg/rtp.js +1 -1
- package/dist/ffmpeg/rtp.js.map +1 -1
- package/dist/ffmpeg/stream.d.ts +1 -1
- package/dist/ffmpeg/stream.js +1 -1
- package/dist/ffmpeg/stream.js.map +1 -1
- package/dist/mqttclient.js +5 -4
- package/dist/mqttclient.js.map +1 -1
- package/dist/service.js +9 -3
- 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 -14
- package/dist/util.js +19 -11
- package/dist/util.js.map +1 -1
- package/package.json +9 -10
package/dist/ffmpeg/record.js
CHANGED
|
@@ -5,10 +5,9 @@
|
|
|
5
5
|
import { HKSV_IDR_INTERVAL, HKSV_TIMEOUT } from "./settings.js";
|
|
6
6
|
import { runWithTimeout } from "../util.js";
|
|
7
7
|
import { FfmpegProcess } from "./process.js";
|
|
8
|
-
import events from "node:events";
|
|
9
8
|
import { once } from "node:events";
|
|
10
|
-
// Utility to map HKSV audio recording
|
|
11
|
-
// updating this mapping.
|
|
9
|
+
// 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
|
|
10
|
+
// would require updating this mapping.
|
|
12
11
|
const translateAudioRecordingCodecType = {
|
|
13
12
|
[1 /* AudioRecordingCodecType.AAC_ELD */]: "38",
|
|
14
13
|
[0 /* AudioRecordingCodecType.AAC_LC */]: "1"
|
|
@@ -22,80 +21,72 @@ const translateAudioSampleRate = {
|
|
|
22
21
|
[4 /* AudioRecordingSamplerate.KHZ_44_1 */]: "44.1",
|
|
23
22
|
[5 /* AudioRecordingSamplerate.KHZ_48 */]: "48"
|
|
24
23
|
};
|
|
24
|
+
// Reusable empty buffer sentinel for the box-parsing loop. Avoids repeated zero-byte allocations on every box reset.
|
|
25
|
+
const EMPTY_BUFFER = Buffer.alloc(0);
|
|
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.
|
|
28
|
+
const FFMPEG_KNOWN_HKSV_ERROR = new RegExp([
|
|
29
|
+
"(Cannot determine format of input stream 0:0 after EOF)",
|
|
30
|
+
"(Could not write header \\(incorrect codec parameters \\?\\): Broken pipe)",
|
|
31
|
+
"(Could not write header for output file #0)",
|
|
32
|
+
"(Error closing file: Broken pipe)",
|
|
33
|
+
"(Error splitting the input into NAL units\\.)",
|
|
34
|
+
"(Invalid data found when processing input)",
|
|
35
|
+
"(moov atom not found)"
|
|
36
|
+
].join("|"));
|
|
25
37
|
/**
|
|
26
|
-
*
|
|
38
|
+
* Abstract base class for fMP4 FFmpeg processes. Owns the shared command line skeleton (preamble, video mapping, movflags, audio encoding, output format) and the fMP4
|
|
39
|
+
* box-parsing loop. Subclasses provide mode-specific pieces (input args, encoder selection, box handling) via protected hook methods, following the template method
|
|
40
|
+
* pattern.
|
|
27
41
|
*
|
|
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
|
|
42
|
+
* @see FfmpegRecordingProcess
|
|
43
|
+
* @see FfmpegLivestreamProcess
|
|
51
44
|
* @see FfmpegProcess
|
|
52
45
|
* @see {@link https://ffmpeg.org/ffmpeg.html | FFmpeg Documentation}
|
|
53
46
|
*/
|
|
54
47
|
class FfmpegFMp4Process extends FfmpegProcess {
|
|
55
|
-
hasInitSegment;
|
|
56
|
-
_initSegment;
|
|
57
|
-
isLivestream;
|
|
58
48
|
isLoggingErrors;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
49
|
+
// The HomeKit recording configuration and resolved base options are stored as protected fields so subclass hook methods can reference them without needing their own
|
|
50
|
+
// copies of the shared state.
|
|
51
|
+
fMp4Options;
|
|
52
|
+
recordingConfig;
|
|
62
53
|
/**
|
|
63
|
-
* Constructs a new fMP4 FFmpeg process
|
|
54
|
+
* 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
|
|
55
|
+
* `buildCommandLine()` after their own initialization to trigger the template method assembly.
|
|
64
56
|
*
|
|
65
57
|
* @param ffmpegOptions - FFmpeg configuration options.
|
|
66
58
|
* @param recordingConfig - HomeKit recording configuration for the session.
|
|
67
|
-
* @param
|
|
68
|
-
* @param fMp4Options - Configuration for the fMP4 session (fps, type, url, audio input, etc.).
|
|
59
|
+
* @param fMp4Options - Partial base options with defaults applied for any unset fields.
|
|
69
60
|
* @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
61
|
*/
|
|
77
62
|
constructor(ffmpegOptions, recordingConfig, fMp4Options = {}, isVerbose = false) {
|
|
78
63
|
// Initialize our parent.
|
|
79
64
|
super(ffmpegOptions);
|
|
80
65
|
// We want to log errors when they occur.
|
|
81
66
|
this.isLoggingErrors = true;
|
|
82
|
-
//
|
|
83
|
-
this.
|
|
84
|
-
|
|
85
|
-
this.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
67
|
+
// Store the recording configuration for use by subclass hook methods.
|
|
68
|
+
this.recordingConfig = recordingConfig;
|
|
69
|
+
// Apply defaults to the base options and store them. Subclasses store their own mode-specific options separately.
|
|
70
|
+
this.fMp4Options = {
|
|
71
|
+
audioFilters: fMp4Options.audioFilters ?? [],
|
|
72
|
+
audioStream: fMp4Options.audioStream ?? 0,
|
|
73
|
+
codec: fMp4Options.codec ?? "h264",
|
|
74
|
+
enableAudio: fMp4Options.enableAudio ?? true,
|
|
75
|
+
hardwareDecoding: fMp4Options.hardwareDecoding ?? (this.options.codecSupport.ffmpegVersion.startsWith("8.") ? this.options.config.hardwareDecoding : false),
|
|
76
|
+
hardwareTranscoding: fMp4Options.hardwareTranscoding ?? this.options.config.hardwareTranscoding,
|
|
77
|
+
transcodeAudio: fMp4Options.transcodeAudio ?? true,
|
|
78
|
+
videoFilters: fMp4Options.videoFilters ?? [],
|
|
79
|
+
videoStream: fMp4Options.videoStream ?? 0
|
|
80
|
+
};
|
|
81
|
+
// Store the verbose flag for use during command line assembly. We don't build the command line here...subclasses call buildCommandLine() after initializing their
|
|
82
|
+
// own state, which avoids the virtual-call-from-constructor problem.
|
|
83
|
+
this._isVerbose = isVerbose;
|
|
84
|
+
}
|
|
85
|
+
// Verbose flag stored during construction and consumed by buildCommandLine(). This avoids passing isVerbose through the hook method chain.
|
|
86
|
+
_isVerbose;
|
|
87
|
+
// 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.
|
|
88
|
+
// Subclasses call this as the last step of their constructor, after their own state is fully initialized.
|
|
89
|
+
buildCommandLine() {
|
|
99
90
|
// Configure our video parameters for our input:
|
|
100
91
|
//
|
|
101
92
|
// -hide_banner Suppress printing the startup banner in FFmpeg.
|
|
@@ -108,100 +99,53 @@ class FfmpegFMp4Process extends FfmpegProcess {
|
|
|
108
99
|
"-nostats",
|
|
109
100
|
"-fflags", "+discardcorrupt",
|
|
110
101
|
"-err_detect", "ignore_err",
|
|
111
|
-
...this.options.videoDecoder(fMp4Options.codec),
|
|
112
|
-
"-max_delay", "500000"
|
|
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()
|
|
113
108
|
];
|
|
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
109
|
// Configure our recording options for the video stream:
|
|
156
110
|
//
|
|
157
111
|
// -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
|
-
})));
|
|
112
|
+
this.commandLineArgs.push("-map", "0:v:" + this.fMp4Options.videoStream.toString(),
|
|
113
|
+
// Mode-specific video encoder arguments (copy for livestream, recordEncoder for recording).
|
|
114
|
+
...this.videoEncoderArgs());
|
|
170
115
|
// 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");
|
|
116
|
+
if (this.fMp4Options.videoFilters.length) {
|
|
117
|
+
this.commandLineArgs.push("-filter:v", this.fMp4Options.videoFilters.join(", "));
|
|
178
118
|
}
|
|
119
|
+
// Mode-specific post-filter arguments (frag_duration for livestream, empty for recording).
|
|
120
|
+
this.commandLineArgs.push(...this.postFilterArgs());
|
|
179
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
|
|
180
122
|
// at each keyframe, skip creating a segment index (SIDX) box in fragments, and skip writing the final MOOV trailer since it's unneeded.
|
|
181
123
|
// -flush_packets 1 Ensure we flush our write buffer after each muxed packet.
|
|
182
124
|
// -reset_timestamps Reset timestamps at the beginning of each segment.
|
|
183
125
|
// -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
|
-
|
|
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) {
|
|
186
130
|
// Configure the audio portion of the command line. Options we use are:
|
|
187
131
|
//
|
|
188
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
|
|
189
133
|
// separate audio input has been configured.
|
|
190
|
-
this.commandLineArgs.push("-map", audioInputIndex.toString() + ":a:" + fMp4Options.audioStream.toString() + "?");
|
|
134
|
+
this.commandLineArgs.push("-map", this.audioInputIndex().toString() + ":a:" + this.fMp4Options.audioStream.toString() + "?");
|
|
191
135
|
// 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
|
-
|
|
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;
|
|
196
140
|
}
|
|
197
|
-
if (
|
|
141
|
+
if (transcodeAudio) {
|
|
198
142
|
// Configure the audio portion of the command line. Options we use are:
|
|
199
143
|
//
|
|
200
144
|
// -codec:a Encode using the codecs available to us on given platforms.
|
|
201
145
|
// -profile:a Specify either low-complexity AAC or enhanced low-delay AAC for HKSV events.
|
|
202
146
|
// -ar samplerate Sample rate to use for this audio. This is specified by HKSV.
|
|
203
147
|
// -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());
|
|
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());
|
|
205
149
|
}
|
|
206
150
|
else {
|
|
207
151
|
// Configure the audio portion of the command line. Options we use are:
|
|
@@ -216,41 +160,43 @@ class FfmpegFMp4Process extends FfmpegProcess {
|
|
|
216
160
|
// pipe:1 Output the stream to standard output.
|
|
217
161
|
this.commandLineArgs.push("-f", "mp4", "pipe:1");
|
|
218
162
|
// Additional logging, but only if we're debugging.
|
|
219
|
-
if (
|
|
163
|
+
if (this._isVerbose || this.isVerbose) {
|
|
220
164
|
this.commandLineArgs.unshift("-loglevel", "level+verbose");
|
|
221
165
|
}
|
|
222
166
|
}
|
|
223
167
|
/**
|
|
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.
|
|
168
|
+
* 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
|
|
169
|
+
* subclass via handleParsedBox().
|
|
227
170
|
*/
|
|
228
171
|
configureProcess() {
|
|
229
172
|
let dataListener;
|
|
230
173
|
// Call our parent to get started.
|
|
231
174
|
super.configureProcess();
|
|
232
175
|
// Initialize our variables that we need to process incoming FFmpeg packets.
|
|
233
|
-
let header =
|
|
234
|
-
let bufferRemaining =
|
|
176
|
+
let header = EMPTY_BUFFER;
|
|
177
|
+
let bufferRemaining = EMPTY_BUFFER;
|
|
235
178
|
let dataLength = 0;
|
|
236
179
|
let type = "";
|
|
237
|
-
// Process FFmpeg output and parse out the fMP4 stream it's generating
|
|
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.
|
|
238
182
|
this.process?.stdout.on("data", dataListener = (buffer) => {
|
|
239
183
|
// If we have anything left from the last buffer we processed, prepend it to this buffer.
|
|
240
184
|
if (bufferRemaining.length > 0) {
|
|
241
185
|
buffer = Buffer.concat([bufferRemaining, buffer]);
|
|
242
|
-
bufferRemaining =
|
|
186
|
+
bufferRemaining = EMPTY_BUFFER;
|
|
243
187
|
}
|
|
244
188
|
let offset = 0;
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
// boxes that HAP-NodeJS is ultimately expecting.
|
|
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.
|
|
248
191
|
for (;;) {
|
|
249
192
|
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
193
|
// No existing header, let's start a new box.
|
|
253
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
|
+
}
|
|
254
200
|
// Grab the header. The first four bytes represents the length of the entire box. Second four bytes represent the box type.
|
|
255
201
|
header = buffer.subarray(0, 8);
|
|
256
202
|
// Now we retrieve the length of the box.
|
|
@@ -272,32 +218,10 @@ class FfmpegFMp4Process extends FfmpegProcess {
|
|
|
272
218
|
bufferRemaining = data;
|
|
273
219
|
break;
|
|
274
220
|
}
|
|
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
|
-
}
|
|
221
|
+
// Dispatch the complete box to the subclass for mode-specific handling.
|
|
222
|
+
this.handleParsedBox(header, data, dataLength, type);
|
|
298
223
|
// Prepare to start a new box for the next buffer that we will be processing.
|
|
299
|
-
|
|
300
|
-
header = Buffer.alloc(0);
|
|
224
|
+
header = EMPTY_BUFFER;
|
|
301
225
|
type = "";
|
|
302
226
|
// We've parsed an entire box, and there's no more data in this buffer to parse.
|
|
303
227
|
if (buffer.length === (offset + dataLength)) {
|
|
@@ -315,59 +239,16 @@ class FfmpegFMp4Process extends FfmpegProcess {
|
|
|
315
239
|
});
|
|
316
240
|
}
|
|
317
241
|
/**
|
|
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.
|
|
242
|
+
* Stops the FFmpeg process and performs cleanup. Subclasses override this to emit mode-specific events before calling super, which handles the shared teardown and
|
|
243
|
+
* emits the "close" event.
|
|
343
244
|
*/
|
|
344
245
|
stopProcess() {
|
|
345
246
|
// Call our parent to get started.
|
|
346
247
|
super.stopProcess();
|
|
347
|
-
//
|
|
248
|
+
// Signal that the process has ended.
|
|
348
249
|
this.isEnded = true;
|
|
349
|
-
this.emit("mp4box");
|
|
350
250
|
this.emit("close");
|
|
351
251
|
}
|
|
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
252
|
/**
|
|
372
253
|
* Stops the FFmpeg process and logs errors if specified.
|
|
373
254
|
*
|
|
@@ -399,24 +280,118 @@ class FfmpegFMp4Process extends FfmpegProcess {
|
|
|
399
280
|
if (!this.isLoggingErrors) {
|
|
400
281
|
return;
|
|
401
282
|
}
|
|
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
283
|
// See if we know about this error.
|
|
413
|
-
if (this.stderrLog.some(x =>
|
|
284
|
+
if (this.stderrLog.some(x => FFMPEG_KNOWN_HKSV_ERROR.test(x))) {
|
|
414
285
|
this.log.error("FFmpeg ended unexpectedly due to issues processing the media stream. This error can be safely ignored - it will occur occasionally.");
|
|
415
286
|
return;
|
|
416
287
|
}
|
|
417
288
|
// Otherwise, revert to our default logging in our parent.
|
|
418
289
|
super.logFfmpegError(exitCode, signal);
|
|
419
290
|
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Manages a HomeKit Secure Video recording FFmpeg process.
|
|
294
|
+
*
|
|
295
|
+
* @example
|
|
296
|
+
*
|
|
297
|
+
* ```ts
|
|
298
|
+
* const process = new FfmpegRecordingProcess(ffmpegOptions, recordingConfig, 30, true, 5000000, 0);
|
|
299
|
+
* process.start();
|
|
300
|
+
* ```
|
|
301
|
+
*
|
|
302
|
+
* @see FfmpegFMp4Process
|
|
303
|
+
*
|
|
304
|
+
* @category FFmpeg
|
|
305
|
+
*/
|
|
306
|
+
export class FfmpegRecordingProcess extends FfmpegFMp4Process {
|
|
307
|
+
/**
|
|
308
|
+
* Indicates whether the recording has timed out waiting for FFmpeg output.
|
|
309
|
+
*/
|
|
310
|
+
isTimedOut;
|
|
311
|
+
fps;
|
|
312
|
+
probesize;
|
|
313
|
+
recordingBuffer;
|
|
314
|
+
timeshift;
|
|
315
|
+
/**
|
|
316
|
+
* Constructs a new FFmpeg recording process for HKSV events.
|
|
317
|
+
*
|
|
318
|
+
* @param options - FFmpeg configuration options.
|
|
319
|
+
* @param recordingConfig - HomeKit recording configuration for the session.
|
|
320
|
+
* @param fMp4Options - fMP4 recording options.
|
|
321
|
+
* @param isVerbose - If `true`, enables more verbose logging for debugging purposes. Defaults to `false`.
|
|
322
|
+
*/
|
|
323
|
+
constructor(options, recordingConfig, fMp4Options = {}, isVerbose = false) {
|
|
324
|
+
super(options, recordingConfig, fMp4Options, isVerbose);
|
|
325
|
+
// Store recording-specific options.
|
|
326
|
+
this.fps = fMp4Options.fps ?? 30;
|
|
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");
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Stops the FFmpeg process and performs cleanup, ensuring the segment generator can exit.
|
|
388
|
+
*/
|
|
389
|
+
stopProcess() {
|
|
390
|
+
// Emit mp4box to unblock segmentGenerator() if it's waiting, then let the base class handle the rest.
|
|
391
|
+
this.isEnded = true;
|
|
392
|
+
this.emit("mp4box");
|
|
393
|
+
super.stopProcess();
|
|
394
|
+
}
|
|
420
395
|
/**
|
|
421
396
|
* Asynchronously generates complete segments from FFmpeg output, formatted for HomeKit Secure Video.
|
|
422
397
|
*
|
|
@@ -437,7 +412,7 @@ class FfmpegFMp4Process extends FfmpegProcess {
|
|
|
437
412
|
let segment = [];
|
|
438
413
|
// Loop forever, generating either FTYP/MOOV box pairs or MOOF/MDAT box pairs for HomeKit Secure Video.
|
|
439
414
|
for (;;) {
|
|
440
|
-
// FFmpeg has finished
|
|
415
|
+
// FFmpeg has finished its output - we're done.
|
|
441
416
|
if (this.isEnded) {
|
|
442
417
|
return;
|
|
443
418
|
}
|
|
@@ -472,54 +447,6 @@ class FfmpegFMp4Process extends FfmpegProcess {
|
|
|
472
447
|
}
|
|
473
448
|
}
|
|
474
449
|
}
|
|
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
450
|
}
|
|
524
451
|
/**
|
|
525
452
|
* Manages a HomeKit livestream FFmpeg process for generating fMP4 segments.
|
|
@@ -538,6 +465,16 @@ export class FfmpegRecordingProcess extends FfmpegFMp4Process {
|
|
|
538
465
|
* @category FFmpeg
|
|
539
466
|
*/
|
|
540
467
|
export class FfmpegLivestreamProcess extends FfmpegFMp4Process {
|
|
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;
|
|
541
478
|
/**
|
|
542
479
|
* Constructs a new FFmpeg livestream process.
|
|
543
480
|
*
|
|
@@ -548,6 +485,113 @@ export class FfmpegLivestreamProcess extends FfmpegFMp4Process {
|
|
|
548
485
|
*/
|
|
549
486
|
constructor(options, recordingConfig, livestreamOptions, isVerbose = false) {
|
|
550
487
|
super(options, recordingConfig, livestreamOptions, isVerbose);
|
|
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
|
+
}
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Starts the FFmpeg process, adjusting the fragment duration if segmentLength has been set.
|
|
579
|
+
*
|
|
580
|
+
* @example
|
|
581
|
+
*
|
|
582
|
+
* ```ts
|
|
583
|
+
* process.start();
|
|
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
|
+
}
|
|
592
|
+
}
|
|
593
|
+
// Start the FFmpeg session.
|
|
594
|
+
super.start();
|
|
551
595
|
}
|
|
552
596
|
/**
|
|
553
597
|
* Gets the fMP4 initialization segment generated by FFmpeg for the livestream.
|
|
@@ -561,7 +605,34 @@ export class FfmpegLivestreamProcess extends FfmpegFMp4Process {
|
|
|
561
605
|
* ```
|
|
562
606
|
*/
|
|
563
607
|
async getInitSegment() {
|
|
564
|
-
return
|
|
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();
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Returns the initialization segment as a Buffer, or null if not yet available.
|
|
618
|
+
*
|
|
619
|
+
* @returns The initialization segment Buffer, or `null` if not yet generated.
|
|
620
|
+
*
|
|
621
|
+
* @example
|
|
622
|
+
*
|
|
623
|
+
* ```ts
|
|
624
|
+
* const init = process.initSegment;
|
|
625
|
+
* if(init) {
|
|
626
|
+
*
|
|
627
|
+
* // Use the initialization segment.
|
|
628
|
+
* }
|
|
629
|
+
* ```
|
|
630
|
+
*/
|
|
631
|
+
get initSegment() {
|
|
632
|
+
if (!this.hasInitSegment) {
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
return this._initSegment;
|
|
565
636
|
}
|
|
566
637
|
}
|
|
567
638
|
//# sourceMappingURL=record.js.map
|