@homebridge-plugins/homebridge-eufy-security 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/FUNDING.yml +1 -0
  3. package/LICENSE +176 -0
  4. package/README.md +67 -0
  5. package/config.schema.json +6 -0
  6. package/dist/accessories/AutoSyncStationAccessory.js +156 -0
  7. package/dist/accessories/AutoSyncStationAccessory.js.map +1 -0
  8. package/dist/accessories/BaseAccessory.js +247 -0
  9. package/dist/accessories/BaseAccessory.js.map +1 -0
  10. package/dist/accessories/CameraAccessory.js +431 -0
  11. package/dist/accessories/CameraAccessory.js.map +1 -0
  12. package/dist/accessories/Device.js +67 -0
  13. package/dist/accessories/Device.js.map +1 -0
  14. package/dist/accessories/EntrySensorAccessory.js +48 -0
  15. package/dist/accessories/EntrySensorAccessory.js.map +1 -0
  16. package/dist/accessories/LockAccessory.js +142 -0
  17. package/dist/accessories/LockAccessory.js.map +1 -0
  18. package/dist/accessories/MotionSensorAccessory.js +48 -0
  19. package/dist/accessories/MotionSensorAccessory.js.map +1 -0
  20. package/dist/accessories/SmartDropAccessory.js +145 -0
  21. package/dist/accessories/SmartDropAccessory.js.map +1 -0
  22. package/dist/accessories/StationAccessory.js +371 -0
  23. package/dist/accessories/StationAccessory.js.map +1 -0
  24. package/dist/config.js +25 -0
  25. package/dist/config.js.map +1 -0
  26. package/dist/controller/LocalLivestreamManager.js +116 -0
  27. package/dist/controller/LocalLivestreamManager.js.map +1 -0
  28. package/dist/controller/recordingDelegate.js +208 -0
  29. package/dist/controller/recordingDelegate.js.map +1 -0
  30. package/dist/controller/snapshotDelegate.js +345 -0
  31. package/dist/controller/snapshotDelegate.js.map +1 -0
  32. package/dist/controller/streamingDelegate.js +345 -0
  33. package/dist/controller/streamingDelegate.js.map +1 -0
  34. package/dist/index.js +11 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/interfaces.js +2 -0
  37. package/dist/interfaces.js.map +1 -0
  38. package/dist/media/Snapshot-Unavailable.png +0 -0
  39. package/dist/media/Snapshot-Unavailable.xcf +0 -0
  40. package/dist/media/Snapshot-black.png +0 -0
  41. package/dist/media/camera-disabled.png +0 -0
  42. package/dist/media/camera-offline.png +0 -0
  43. package/dist/media/media/Snapshot-Unavailable.png +0 -0
  44. package/dist/media/media/Snapshot-Unavailable.xcf +0 -0
  45. package/dist/media/media/Snapshot-black.png +0 -0
  46. package/dist/media/media/camera-disabled.png +0 -0
  47. package/dist/media/media/camera-offline.png +0 -0
  48. package/dist/platform.js +716 -0
  49. package/dist/platform.js.map +1 -0
  50. package/dist/settings.js +38 -0
  51. package/dist/settings.js.map +1 -0
  52. package/dist/utils/Talkback.js +92 -0
  53. package/dist/utils/Talkback.js.map +1 -0
  54. package/dist/utils/accessoriesStore.js +206 -0
  55. package/dist/utils/accessoriesStore.js.map +1 -0
  56. package/dist/utils/configTypes.js +35 -0
  57. package/dist/utils/configTypes.js.map +1 -0
  58. package/dist/utils/ffmpeg.js +843 -0
  59. package/dist/utils/ffmpeg.js.map +1 -0
  60. package/dist/utils/interfaces.js +8 -0
  61. package/dist/utils/interfaces.js.map +1 -0
  62. package/dist/utils/utils.js +44 -0
  63. package/dist/utils/utils.js.map +1 -0
  64. package/dist/version.js +2 -0
  65. package/dist/version.js.map +1 -0
  66. package/eslint.config.mjs +18 -0
  67. package/homebridge-eufy-security.png +0 -0
  68. package/homebridge-ui/public/app.js +225 -0
  69. package/homebridge-ui/public/assets/devices/4g_lte_starlight_large.jpg +0 -0
  70. package/homebridge-ui/public/assets/devices/BATTERY_DOORBELL_C30.png +0 -0
  71. package/homebridge-ui/public/assets/devices/BATTERY_DOORBELL_C31.png +0 -0
  72. package/homebridge-ui/public/assets/devices/batterydoorbell1080p_large.jpg +0 -0
  73. package/homebridge-ui/public/assets/devices/batterydoorbell2kdual_large.jpg +0 -0
  74. package/homebridge-ui/public/assets/devices/batterydoorbell_e340_large.png +0 -0
  75. package/homebridge-ui/public/assets/devices/eufy-security-client.png +0 -0
  76. package/homebridge-ui/public/assets/devices/eufycam2_large.png +0 -0
  77. package/homebridge-ui/public/assets/devices/eufycam2c_large.jpg +0 -0
  78. package/homebridge-ui/public/assets/devices/eufycam2cpro_large.jpg +0 -0
  79. package/homebridge-ui/public/assets/devices/eufycam2pro_large.jpg +0 -0
  80. package/homebridge-ui/public/assets/devices/eufycam3_large.jpg +0 -0
  81. package/homebridge-ui/public/assets/devices/eufycam3c_large.jpg +0 -0
  82. package/homebridge-ui/public/assets/devices/eufycam3pro_large.png +0 -0
  83. package/homebridge-ui/public/assets/devices/eufycam_large.jpg +0 -0
  84. package/homebridge-ui/public/assets/devices/eufycame330_large.jpg +0 -0
  85. package/homebridge-ui/public/assets/devices/floodlight2_large.jpg +0 -0
  86. package/homebridge-ui/public/assets/devices/floodlight2pro_large.jpg +0 -0
  87. package/homebridge-ui/public/assets/devices/floodlight_large.jpg +0 -0
  88. package/homebridge-ui/public/assets/devices/floodlightcame340_large.jpg +0 -0
  89. package/homebridge-ui/public/assets/devices/garage_camera_t8452_large.jpg +0 -0
  90. package/homebridge-ui/public/assets/devices/homebase2_large.png +0 -0
  91. package/homebridge-ui/public/assets/devices/homebase3_large.png +0 -0
  92. package/homebridge-ui/public/assets/devices/homebase_large.jpg +0 -0
  93. package/homebridge-ui/public/assets/devices/homebasemini_large.jpg +0 -0
  94. package/homebridge-ui/public/assets/devices/indoorcamC210_large.png +0 -0
  95. package/homebridge-ui/public/assets/devices/indoorcamC220_large.png +0 -0
  96. package/homebridge-ui/public/assets/devices/indoorcamE30_large.png +0 -0
  97. package/homebridge-ui/public/assets/devices/indoorcamc120_large.png +0 -0
  98. package/homebridge-ui/public/assets/devices/indoorcammini_large.jpg +0 -0
  99. package/homebridge-ui/public/assets/devices/indoorcamp24_large.png +0 -0
  100. package/homebridge-ui/public/assets/devices/indoorcams350_large.jpg +0 -0
  101. package/homebridge-ui/public/assets/devices/keypad_large.png +0 -0
  102. package/homebridge-ui/public/assets/devices/minibase_chime_T8023_large.jpg +0 -0
  103. package/homebridge-ui/public/assets/devices/motionsensor_large.png +0 -0
  104. package/homebridge-ui/public/assets/devices/sensor_large.png +0 -0
  105. package/homebridge-ui/public/assets/devices/smartdrop_t8790_large.png +0 -0
  106. package/homebridge-ui/public/assets/devices/smartlock_t8500_large.png +0 -0
  107. package/homebridge-ui/public/assets/devices/smartlock_t8500_wifibridge_large.jpg +0 -0
  108. package/homebridge-ui/public/assets/devices/smartlock_t8503_large.png +0 -0
  109. package/homebridge-ui/public/assets/devices/smartlock_t8504_large.jpg +0 -0
  110. package/homebridge-ui/public/assets/devices/smartlock_t8510P_t8520P_large.png +0 -0
  111. package/homebridge-ui/public/assets/devices/smartlock_touch_and_wifi_t8502_large.png +0 -0
  112. package/homebridge-ui/public/assets/devices/smartlock_touch_and_wifi_t8506_large.png +0 -0
  113. package/homebridge-ui/public/assets/devices/smartlock_touch_and_wifi_t8520_large.png +0 -0
  114. package/homebridge-ui/public/assets/devices/smartlock_touch_t8510_large.png +0 -0
  115. package/homebridge-ui/public/assets/devices/smartlock_touch_t8510_wifibridge_large.jpg +0 -0
  116. package/homebridge-ui/public/assets/devices/smartlock_video_t8530_large.png +0 -0
  117. package/homebridge-ui/public/assets/devices/smartlockwifibridge_t8021_large.jpg +0 -0
  118. package/homebridge-ui/public/assets/devices/smartsafe_s10_t7400_large.png +0 -0
  119. package/homebridge-ui/public/assets/devices/smartsafe_s12_t7401_large.png +0 -0
  120. package/homebridge-ui/public/assets/devices/smarttrack_card_t87B2_large.png +0 -0
  121. package/homebridge-ui/public/assets/devices/smarttrack_link_t87B0_large.png +0 -0
  122. package/homebridge-ui/public/assets/devices/solocamc210_large.jpg +0 -0
  123. package/homebridge-ui/public/assets/devices/solocamc35_large.png +0 -0
  124. package/homebridge-ui/public/assets/devices/solocame20_large.jpg +0 -0
  125. package/homebridge-ui/public/assets/devices/solocame30_large.png +0 -0
  126. package/homebridge-ui/public/assets/devices/solocame40_large.jpg +0 -0
  127. package/homebridge-ui/public/assets/devices/solocaml20_large.jpg +0 -0
  128. package/homebridge-ui/public/assets/devices/solocams220_large.jpg +0 -0
  129. package/homebridge-ui/public/assets/devices/solocams340_large.png +0 -0
  130. package/homebridge-ui/public/assets/devices/solocams40_large.jpg +0 -0
  131. package/homebridge-ui/public/assets/devices/soloindoorcamc24_large.jpg +0 -0
  132. package/homebridge-ui/public/assets/devices/solooutdoorcamc22_large.png +0 -0
  133. package/homebridge-ui/public/assets/devices/solooutdoorcamc24_large.jpg +0 -0
  134. package/homebridge-ui/public/assets/devices/unknown.png +0 -0
  135. package/homebridge-ui/public/assets/devices/walllight_s100_large.jpg +0 -0
  136. package/homebridge-ui/public/assets/devices/walllight_s120_large.jpg +0 -0
  137. package/homebridge-ui/public/assets/devices/wireddoorbell1080p_large.jpg +0 -0
  138. package/homebridge-ui/public/assets/devices/wireddoorbell2k_large.png +0 -0
  139. package/homebridge-ui/public/assets/devices/wireddoorbelldual_large.jpg +0 -0
  140. package/homebridge-ui/public/assets/icons/attach.svg +1 -0
  141. package/homebridge-ui/public/assets/icons/battery_0.svg +1 -0
  142. package/homebridge-ui/public/assets/icons/battery_1.svg +1 -0
  143. package/homebridge-ui/public/assets/icons/battery_2.svg +1 -0
  144. package/homebridge-ui/public/assets/icons/battery_3.svg +1 -0
  145. package/homebridge-ui/public/assets/icons/battery_4.svg +1 -0
  146. package/homebridge-ui/public/assets/icons/battery_5.svg +1 -0
  147. package/homebridge-ui/public/assets/icons/battery_6.svg +1 -0
  148. package/homebridge-ui/public/assets/icons/bolt.svg +1 -0
  149. package/homebridge-ui/public/assets/icons/bug-report.svg +1 -0
  150. package/homebridge-ui/public/assets/icons/copy.svg +1 -0
  151. package/homebridge-ui/public/assets/icons/delete.svg +1 -0
  152. package/homebridge-ui/public/assets/icons/download.svg +1 -0
  153. package/homebridge-ui/public/assets/icons/info.svg +1 -0
  154. package/homebridge-ui/public/assets/icons/inventory.svg +1 -0
  155. package/homebridge-ui/public/assets/icons/refresh.svg +1 -0
  156. package/homebridge-ui/public/assets/icons/satellite_alt.svg +1 -0
  157. package/homebridge-ui/public/assets/icons/settings.svg +1 -0
  158. package/homebridge-ui/public/assets/icons/settings_backup_restore.svg +1 -0
  159. package/homebridge-ui/public/assets/icons/solar_power.svg +1 -0
  160. package/homebridge-ui/public/assets/icons/warning.svg +1 -0
  161. package/homebridge-ui/public/components/device-card.js +162 -0
  162. package/homebridge-ui/public/components/guard-modes.js +88 -0
  163. package/homebridge-ui/public/components/number-input.js +121 -0
  164. package/homebridge-ui/public/components/select.js +73 -0
  165. package/homebridge-ui/public/components/toggle.js +68 -0
  166. package/homebridge-ui/public/index.html +27 -0
  167. package/homebridge-ui/public/services/api.js +214 -0
  168. package/homebridge-ui/public/services/config.js +144 -0
  169. package/homebridge-ui/public/style.css +775 -0
  170. package/homebridge-ui/public/utils/countries.js +73 -0
  171. package/homebridge-ui/public/utils/device-images.js +89 -0
  172. package/homebridge-ui/public/utils/helpers.js +87 -0
  173. package/homebridge-ui/public/views/dashboard.js +226 -0
  174. package/homebridge-ui/public/views/device-detail.js +610 -0
  175. package/homebridge-ui/public/views/diagnostics.js +296 -0
  176. package/homebridge-ui/public/views/login.js +636 -0
  177. package/homebridge-ui/public/views/settings.js +192 -0
  178. package/homebridge-ui/public/views/unsupported-detail.js +296 -0
  179. package/homebridge-ui/server.js +1327 -0
  180. package/media/Snapshot-Unavailable.png +0 -0
  181. package/media/Snapshot-Unavailable.xcf +0 -0
  182. package/media/Snapshot-black.png +0 -0
  183. package/media/camera-disabled.png +0 -0
  184. package/media/camera-offline.png +0 -0
  185. package/package.json +64 -0
@@ -0,0 +1,843 @@
1
+ // Node built-ins
2
+ import { execFileSync, spawn } from 'child_process';
3
+ import EventEmitter from 'events';
4
+ import net from 'net';
5
+ import os from 'os';
6
+ // External packages
7
+ import ffmpegPath from 'ffmpeg-for-homebridge';
8
+ import { pickPort } from 'pick-port';
9
+ import { ffmpegLogger } from './utils.js';
10
+ /** Timeout for one-shot TCP servers waiting for a connection (ms) */
11
+ const TCP_SERVER_TIMEOUT_MS = 30_000;
12
+ /** Timeout for ffmpeg getResult() before force-killing the process (ms) */
13
+ const PROCESS_RESULT_TIMEOUT_MS = 15_000;
14
+ /** Grace period after SIGTERM before sending SIGKILL (ms) */
15
+ const KILL_GRACE_PERIOD_MS = 2_000;
16
+ /** Returns true when the value is a non-empty string (guards `undefined | ''`). */
17
+ function isNonEmpty(value) {
18
+ return !!value && value !== '';
19
+ }
20
+ /**
21
+ * Cached result of probing the ffmpeg binary for libfdk_aac support.
22
+ * `undefined` means we haven't checked yet.
23
+ */
24
+ let _hasFdkAac;
25
+ /**
26
+ * Returns true when the ffmpeg binary on this system supports the libfdk_aac
27
+ * encoder. The result is cached after the first call.
28
+ */
29
+ export function hasFdkAac() {
30
+ if (_hasFdkAac !== undefined) {
31
+ return _hasFdkAac;
32
+ }
33
+ const ffmpegExec = ffmpegPath || 'ffmpeg';
34
+ try {
35
+ const output = execFileSync(ffmpegExec, ['-encoders'], {
36
+ timeout: 5_000,
37
+ encoding: 'utf-8',
38
+ stdio: ['pipe', 'pipe', 'pipe'],
39
+ });
40
+ _hasFdkAac = output.includes('libfdk_aac');
41
+ }
42
+ catch {
43
+ _hasFdkAac = false;
44
+ }
45
+ if (_hasFdkAac) {
46
+ ffmpegLogger.info('libfdk_aac encoder available.');
47
+ }
48
+ else {
49
+ ffmpegLogger.warn('libfdk_aac encoder is NOT available — falling back to the native aac encoder (AAC-LC instead of AAC-ELD). ' +
50
+ 'This usually means the ffmpeg-for-homebridge binary could not be installed on your platform. ' +
51
+ 'You can install a compatible ffmpeg manually (built with --enable-libfdk-aac --enable-nonfree) ' +
52
+ 'and set the path in your camera\'s videoConfig.videoProcessor setting.');
53
+ }
54
+ return _hasFdkAac;
55
+ }
56
+ /**
57
+ * Returns the preferred AAC encoder and codec options for AAC-ELD output.
58
+ * Falls back to the built-in `aac` encoder when `libfdk_aac` is absent.
59
+ */
60
+ function getAacEldCodecAndOptions() {
61
+ if (hasFdkAac()) {
62
+ return { codec: 'libfdk_aac', codecOptions: '-profile:a aac_eld' };
63
+ }
64
+ // The native aac encoder doesn't support ELD, but LC works with HomeKit.
65
+ return { codec: 'aac', codecOptions: '-profile:a aac_low' };
66
+ }
67
+ /**
68
+ * Returns the preferred AAC encoder for a given recording codec type.
69
+ * Falls back to the built-in `aac` encoder when `libfdk_aac` is absent.
70
+ */
71
+ function getAacRecordingCodecAndOptions(codecType) {
72
+ const isLC = codecType === 0 /* AudioRecordingCodecType.AAC_LC */;
73
+ if (hasFdkAac()) {
74
+ return {
75
+ codec: 'libfdk_aac',
76
+ codecOptions: isLC ? '-profile:a aac_low' : '-profile:a aac_eld',
77
+ };
78
+ }
79
+ return {
80
+ codec: 'aac',
81
+ codecOptions: '-profile:a aac_low',
82
+ };
83
+ }
84
+ /** Map HomeKit audio samplerate enum values to kHz strings for ffmpeg `-ar`. */
85
+ const SAMPLERATE_MAP = new Map([
86
+ [0 /* AudioRecordingSamplerate.KHZ_8 */, '8'],
87
+ [1 /* AudioRecordingSamplerate.KHZ_16 */, '16'],
88
+ [2 /* AudioRecordingSamplerate.KHZ_24 */, '24'],
89
+ [3 /* AudioRecordingSamplerate.KHZ_32 */, '32'],
90
+ [4 /* AudioRecordingSamplerate.KHZ_44_1 */, '44.1'],
91
+ [5 /* AudioRecordingSamplerate.KHZ_48 */, '48'],
92
+ ]);
93
+ /**
94
+ * Creates a TCP server that accepts exactly one connection, then auto-closes.
95
+ * If no connection arrives within the timeout, the server closes anyway.
96
+ * @returns The port the server is listening on.
97
+ */
98
+ async function createOneShotTcpServer(onConnection, existingPort) {
99
+ const port = existingPort ?? await pickPort({ type: 'tcp' });
100
+ // eslint-disable-next-line prefer-const
101
+ let killTimeout;
102
+ const server = net.createServer((socket) => {
103
+ if (killTimeout) {
104
+ clearTimeout(killTimeout);
105
+ }
106
+ server.close();
107
+ socket.on('error', () => { }); // ignore — handled elsewhere
108
+ onConnection(socket);
109
+ });
110
+ server.on('error', () => { }); // ignore — handled elsewhere
111
+ killTimeout = setTimeout(() => {
112
+ server.close();
113
+ }, TCP_SERVER_TIMEOUT_MS);
114
+ server.listen(port);
115
+ return { port, server };
116
+ }
117
+ class FFmpegProgress extends EventEmitter {
118
+ started = false;
119
+ constructor() {
120
+ super();
121
+ }
122
+ static async create(port) {
123
+ const instance = new FFmpegProgress();
124
+ const { server } = await createOneShotTcpServer((socket) => {
125
+ socket.on('data', instance.analyzeProgress.bind(instance));
126
+ }, port);
127
+ server.on('close', () => instance.emit('progress stopped'));
128
+ return instance;
129
+ }
130
+ analyzeProgress(progressData) {
131
+ const progress = new Map();
132
+ progressData.toString().split(/\r?\n/).forEach((line) => {
133
+ const split = line.split('=', 2);
134
+ if (split.length !== 2) {
135
+ return;
136
+ }
137
+ progress.set(split[0], split[1]);
138
+ });
139
+ if (!this.started) {
140
+ if (progress.get('progress') !== undefined) {
141
+ this.started = true;
142
+ this.emit('progress started');
143
+ }
144
+ }
145
+ }
146
+ }
147
+ export class FFmpegParameters {
148
+ progressPort;
149
+ debug;
150
+ // default parameters
151
+ processor;
152
+ hideBanner = true;
153
+ useWallclockAsTimestamp = true;
154
+ inputSource = '-i pipe:';
155
+ protocolWhitelist;
156
+ inputCodec;
157
+ inputFormat;
158
+ output = 'pipe:1';
159
+ isVideo;
160
+ isAudio;
161
+ isSnapshot;
162
+ // generic options
163
+ analyzeDuration;
164
+ probeSize;
165
+ stimeout;
166
+ readrate;
167
+ codec = 'copy';
168
+ codecOptions;
169
+ bitrate;
170
+ // output options
171
+ payloadType;
172
+ ssrc;
173
+ srtpSuite;
174
+ srtpParams;
175
+ format;
176
+ // video options
177
+ fps;
178
+ pixFormat;
179
+ colorRange;
180
+ filters;
181
+ width;
182
+ height;
183
+ bufsize;
184
+ maxrate;
185
+ crop = false;
186
+ // audio options
187
+ sampleRate;
188
+ channels;
189
+ flagsGlobalHeader = false;
190
+ // snapshot options
191
+ numberFrames;
192
+ delaySnapshot = false;
193
+ // recording options / fragmented mp4
194
+ movflags;
195
+ maxMuxingQueueSize;
196
+ iFrameInterval;
197
+ processAudio = true;
198
+ constructor(port, isVideo, isAudio, isSnapshot, debug = false) {
199
+ this.progressPort = port;
200
+ this.isVideo = isVideo;
201
+ this.isAudio = isAudio;
202
+ this.isSnapshot = isSnapshot;
203
+ this.debug = debug;
204
+ }
205
+ /** Allocate a progress port and construct an instance. */
206
+ static async create(isVideo, isAudio, isSnapshot, debug) {
207
+ const port = await pickPort({ type: 'tcp' });
208
+ return new FFmpegParameters(port, isVideo, isAudio, isSnapshot, debug);
209
+ }
210
+ static async forAudio(debug = false) {
211
+ const ffmpeg = await FFmpegParameters.create(false, true, false, debug);
212
+ ffmpeg.useWallclockAsTimestamp = false;
213
+ ffmpeg.flagsGlobalHeader = true;
214
+ return ffmpeg;
215
+ }
216
+ static async forVideo(debug = false) {
217
+ return FFmpegParameters.create(true, false, false, debug);
218
+ }
219
+ static async forSnapshot(debug = false) {
220
+ const ffmpeg = await FFmpegParameters.create(false, false, true, debug);
221
+ ffmpeg.useWallclockAsTimestamp = false;
222
+ ffmpeg.numberFrames = 1;
223
+ ffmpeg.format = 'image2';
224
+ return ffmpeg;
225
+ }
226
+ static async forVideoRecording(debug = false) {
227
+ const ffmpeg = await FFmpegParameters.create(true, false, false, debug);
228
+ ffmpeg.useWallclockAsTimestamp = true;
229
+ return ffmpeg;
230
+ }
231
+ static async forAudioRecording(debug = false) {
232
+ return FFmpegParameters.create(false, true, false, debug);
233
+ }
234
+ setResolution(width, height) {
235
+ this.width = width;
236
+ this.height = height;
237
+ }
238
+ usesStdInAsInput() {
239
+ return this.inputSource === '-i pipe:';
240
+ }
241
+ setInputSource(value) {
242
+ this.inputSource = `-i ${value}`;
243
+ }
244
+ async setInputStream(input) {
245
+ const { port } = await createOneShotTcpServer((socket) => {
246
+ input.pipe(socket);
247
+ });
248
+ this.setInputSource(`tcp://127.0.0.1:${port}`);
249
+ }
250
+ setDelayedSnapshot() {
251
+ this.delaySnapshot = true;
252
+ }
253
+ setup(cameraConfig, request) {
254
+ const videoConfig = cameraConfig.videoConfig ??= {};
255
+ if (isNonEmpty(videoConfig.videoProcessor)) {
256
+ this.processor = videoConfig.videoProcessor;
257
+ }
258
+ if (videoConfig.readRate) {
259
+ this.readrate = videoConfig.readRate;
260
+ }
261
+ if (videoConfig.stimeout) {
262
+ this.stimeout = videoConfig.stimeout;
263
+ }
264
+ if (videoConfig.probeSize) {
265
+ this.probeSize = videoConfig.probeSize;
266
+ }
267
+ if (videoConfig.analyzeDuration) {
268
+ this.analyzeDuration = videoConfig.analyzeDuration;
269
+ }
270
+ if (this.isVideo) {
271
+ const req = request;
272
+ this.codec = isNonEmpty(videoConfig.vcodec) ? videoConfig.vcodec : 'libx264';
273
+ if (this.codec !== 'copy') {
274
+ this.fps = videoConfig.maxFPS ?? req.video.fps;
275
+ const bitrate = videoConfig.maxBitrate ?? req.video.max_bit_rate;
276
+ this.bitrate = bitrate;
277
+ this.bufsize = bitrate * 2;
278
+ this.maxrate = bitrate;
279
+ this.codecOptions = videoConfig.encoderOptions
280
+ ?? (this.codec === 'libx264' ? '-preset ultrafast -tune zerolatency' : '');
281
+ this.pixFormat = 'yuv420p';
282
+ this.colorRange = 'mpeg';
283
+ this.applyVisualConfig(req.video.width, req.video.height, videoConfig);
284
+ }
285
+ }
286
+ if (this.isAudio) {
287
+ const req = request;
288
+ let codec;
289
+ let codecOptions;
290
+ switch (req.audio.codec) {
291
+ case "OPUS" /* AudioStreamingCodecType.OPUS */:
292
+ codec = 'libopus';
293
+ codecOptions = '-application lowdelay';
294
+ break;
295
+ default: {
296
+ const aac = getAacEldCodecAndOptions();
297
+ codec = aac.codec;
298
+ codecOptions = aac.codecOptions;
299
+ break;
300
+ }
301
+ }
302
+ if (isNonEmpty(videoConfig.acodec)) {
303
+ codec = videoConfig.acodec;
304
+ codecOptions = '';
305
+ }
306
+ if (videoConfig.acodecOptions !== undefined) {
307
+ codecOptions = videoConfig.acodecOptions;
308
+ }
309
+ if (this.flagsGlobalHeader) {
310
+ if (codecOptions !== '') {
311
+ codecOptions += ' ';
312
+ }
313
+ codecOptions += '-flags +global_header';
314
+ }
315
+ this.codec = codec;
316
+ this.codecOptions = codecOptions;
317
+ if (this.codec !== 'copy') {
318
+ this.sampleRate = req.audio.sample_rate;
319
+ this.channels = req.audio.channel;
320
+ this.bitrate = videoConfig.audioBitrate ? videoConfig.audioBitrate : req.audio.max_bit_rate;
321
+ }
322
+ }
323
+ if (this.isSnapshot) {
324
+ const req = request;
325
+ this.applyVisualConfig(req.width, req.height, videoConfig);
326
+ }
327
+ }
328
+ setRTPTarget(sessionInfo, request) {
329
+ const isVideo = this.isVideo;
330
+ const mediaRequest = isVideo ? request.video : request.audio;
331
+ const port = isVideo ? sessionInfo.videoPort : sessionInfo.audioPort;
332
+ const pktSize = isVideo ? 1128 : 188;
333
+ this.payloadType = mediaRequest.pt;
334
+ this.ssrc = isVideo ? sessionInfo.videoSSRC : sessionInfo.audioSSRC;
335
+ this.srtpParams = (isVideo ? sessionInfo.videoSRTP : sessionInfo.audioSRTP).toString('base64');
336
+ this.srtpSuite = 'AES_CM_128_HMAC_SHA1_80';
337
+ this.format = 'rtp';
338
+ this.output = `srtp://${sessionInfo.address}:${port}?rtcpport=${port}&pkt_size=${pktSize}`;
339
+ }
340
+ setOutput(output) {
341
+ this.output = output;
342
+ }
343
+ setupForRecording(videoConfig, configuration) {
344
+ this.movflags = 'frag_keyframe+empty_moov+default_base_moof+omit_tfhd_offset';
345
+ this.maxMuxingQueueSize = 1024;
346
+ if (isNonEmpty(videoConfig.videoProcessor)) {
347
+ this.processor = videoConfig.videoProcessor;
348
+ }
349
+ if (this.isVideo) {
350
+ if (isNonEmpty(videoConfig.vcodec)) {
351
+ this.codec = videoConfig.vcodec;
352
+ }
353
+ else {
354
+ this.codec = 'libx264';
355
+ }
356
+ if (this.codec === 'libx264') {
357
+ this.pixFormat = 'yuv420p';
358
+ const profile = configuration.videoCodec.parameters.profile === 2 /* H264Profile.HIGH */
359
+ ? 'high'
360
+ : configuration.videoCodec.parameters.profile === 1 /* H264Profile.MAIN */
361
+ ? 'main'
362
+ : 'baseline';
363
+ const level = configuration.videoCodec.parameters.level === 2 /* H264Level.LEVEL4_0 */
364
+ ? '4.0'
365
+ : configuration.videoCodec.parameters.level === 1 /* H264Level.LEVEL3_2 */
366
+ ? '3.2'
367
+ : '3.1';
368
+ this.codecOptions = `-preset ultrafast -tune zerolatency -profile:v ${profile} -level:v ${level}`;
369
+ }
370
+ if (this.codec !== 'copy') {
371
+ this.bitrate = videoConfig.maxBitrate ?? configuration.videoCodec.parameters.bitRate;
372
+ this.width = configuration.videoCodec.resolution[0];
373
+ this.height = configuration.videoCodec.resolution[1];
374
+ this.fps = videoConfig.maxFPS ?? configuration.videoCodec.resolution[2];
375
+ this.crop = (videoConfig.crop !== false); // only false if 'crop: false' was specifically set
376
+ }
377
+ this.iFrameInterval = configuration.videoCodec.parameters.iFrameInterval;
378
+ }
379
+ if (this.isAudio) {
380
+ if (isNonEmpty(videoConfig.acodec)) {
381
+ this.codec = videoConfig.acodec;
382
+ }
383
+ else {
384
+ const aac = getAacRecordingCodecAndOptions(configuration.audioCodec.type);
385
+ this.codec = aac.codec;
386
+ this.codecOptions = aac.codecOptions + ' -flags +global_header';
387
+ }
388
+ if (isNonEmpty(videoConfig.acodec) && (this.codec === 'libfdk_aac' || this.codec === 'aac')) {
389
+ this.codecOptions = (configuration.audioCodec.type === 0 /* AudioRecordingCodecType.AAC_LC */)
390
+ ? '-profile:a aac_low'
391
+ : '-profile:a aac_eld';
392
+ this.codecOptions += ' -flags +global_header';
393
+ }
394
+ if (this.codec !== 'copy') {
395
+ const samplerate = SAMPLERATE_MAP.get(configuration.audioCodec.samplerate);
396
+ if (!samplerate) {
397
+ throw new Error(`Unsupported audio samplerate: ${configuration.audioCodec.samplerate}`);
398
+ }
399
+ this.sampleRate = samplerate;
400
+ this.bitrate = configuration.audioCodec.bitrate;
401
+ this.channels = configuration.audioCodec.audioChannels;
402
+ }
403
+ }
404
+ }
405
+ async setTalkbackInput(sessionInfo) {
406
+ this.useWallclockAsTimestamp = false;
407
+ this.protocolWhitelist = 'pipe,udp,rtp,file,crypto,tcp';
408
+ this.inputFormat = 'sdp';
409
+ const talkbackCodec = hasFdkAac() ? 'libfdk_aac' : 'aac';
410
+ this.inputCodec = talkbackCodec;
411
+ this.codec = talkbackCodec;
412
+ this.sampleRate = 16;
413
+ this.channels = 1;
414
+ this.bitrate = 20;
415
+ this.format = 'adts';
416
+ const ipVer = sessionInfo.ipv6 ? 'IP6' : 'IP4';
417
+ const sdpInput = 'v=0\r\n' +
418
+ 'o=- 0 0 IN ' + ipVer + ' ' + sessionInfo.address + '\r\n' +
419
+ 's=Talk\r\n' +
420
+ 'c=IN ' + ipVer + ' ' + sessionInfo.address + '\r\n' +
421
+ 't=0 0\r\n' +
422
+ 'm=audio ' + sessionInfo.audioReturnPort + ' RTP/AVP 110\r\n' +
423
+ 'b=AS:24\r\n' +
424
+ 'a=rtpmap:110 MPEG4-GENERIC/16000/1\r\n' +
425
+ 'a=rtcp-mux\r\n' + // FFmpeg ignores this, but might as well
426
+ 'a=fmtp:110 ' +
427
+ 'profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3; ' +
428
+ 'config=F8F0212C00BC00\r\n' +
429
+ 'a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:' + sessionInfo.audioSRTP.toString('base64') + '\r\n';
430
+ const { port } = await createOneShotTcpServer((socket) => {
431
+ socket.end(sdpInput);
432
+ });
433
+ this.setInputSource(`tcp://127.0.0.1:${port}`);
434
+ }
435
+ setTalkbackChannels(channels) {
436
+ this.channels = channels;
437
+ }
438
+ buildGenericParameters() {
439
+ const params = [];
440
+ if (this.hideBanner)
441
+ params.push('-hide_banner');
442
+ params.push('-loglevel level+verbose');
443
+ if (this.useWallclockAsTimestamp)
444
+ params.push('-use_wallclock_as_timestamps 1');
445
+ return params;
446
+ }
447
+ buildInputParameters() {
448
+ const params = [];
449
+ if (this.analyzeDuration)
450
+ params.push(`-analyzeduration ${this.analyzeDuration}`);
451
+ if (this.probeSize)
452
+ params.push(`-probesize ${this.probeSize}`);
453
+ if (this.stimeout)
454
+ params.push(`-stimeout ${this.stimeout * 10000000}`);
455
+ if (this.readrate)
456
+ params.push('-re');
457
+ if (this.protocolWhitelist)
458
+ params.push(`-protocol_whitelist ${this.protocolWhitelist}`);
459
+ if (this.inputFormat)
460
+ params.push(`-f ${this.inputFormat}`);
461
+ if (this.inputCodec)
462
+ params.push(`-c:a ${this.inputCodec}`);
463
+ params.push(this.inputSource);
464
+ if (this.isVideo)
465
+ params.push('-an -sn -dn');
466
+ if (this.isAudio)
467
+ params.push('-vn -sn -dn');
468
+ return params;
469
+ }
470
+ /** Clamp a requested dimension to an optional max from the video config. */
471
+ static clampDimension(requested, max) {
472
+ return (max && max < requested) ? max : requested;
473
+ }
474
+ /** Apply common visual settings (dimensions, filters, crop) from the video config. */
475
+ applyVisualConfig(width, height, videoConfig) {
476
+ this.width = FFmpegParameters.clampDimension(width, videoConfig.maxWidth);
477
+ this.height = FFmpegParameters.clampDimension(height, videoConfig.maxHeight);
478
+ if (isNonEmpty(videoConfig.videoFilter)) {
479
+ this.filters = videoConfig.videoFilter;
480
+ }
481
+ if (videoConfig.crop) {
482
+ this.crop = videoConfig.crop;
483
+ }
484
+ }
485
+ /**
486
+ * Builds scale/crop video filter arguments based on current width, height, crop settings,
487
+ * and any user-specified filters. Shared between video and snapshot encoding.
488
+ */
489
+ buildVideoFilterParams() {
490
+ const filters = this.filters ? this.filters.split(',') : [];
491
+ const noneFilter = filters.indexOf('none');
492
+ if (noneFilter >= 0) {
493
+ filters.splice(noneFilter, 1);
494
+ }
495
+ if (noneFilter < 0 && this.width && this.height) {
496
+ if (this.crop) {
497
+ filters.push(`scale=${this.width}:${this.height}:force_original_aspect_ratio=increase`);
498
+ filters.push(`crop=${this.width}:${this.height}`);
499
+ filters.push(`scale='trunc(${this.width}/2)*2:trunc(${this.height}/2)*2'`);
500
+ }
501
+ else {
502
+ filters.push(`scale='min(${this.width},iw)':'min(${this.height},ih)':force_original_aspect_ratio=decrease`);
503
+ filters.push('scale=\'trunc(iw/2)*2:trunc(ih/2)*2\'');
504
+ }
505
+ }
506
+ if (filters.length > 0) {
507
+ return ['-filter:v ' + filters.join(',')];
508
+ }
509
+ return [];
510
+ }
511
+ buildEncodingParameters() {
512
+ const params = [];
513
+ if (this.isVideo) {
514
+ if (this.fps)
515
+ params.push(`-r ${this.fps}`);
516
+ params.push(`-vcodec ${this.codec}`);
517
+ if (this.pixFormat)
518
+ params.push(`-pix_fmt ${this.pixFormat}`);
519
+ if (this.colorRange)
520
+ params.push(`-color_range ${this.colorRange}`);
521
+ if (this.codecOptions)
522
+ params.push(this.codecOptions);
523
+ params.push(...this.buildVideoFilterParams());
524
+ if (this.bitrate)
525
+ params.push(`-b:v ${this.bitrate}k`);
526
+ if (this.bufsize)
527
+ params.push(`-bufsize ${this.bufsize}k`);
528
+ if (this.maxrate)
529
+ params.push(`-maxrate ${this.maxrate}k`);
530
+ }
531
+ if (this.isAudio && this.processAudio) {
532
+ params.push(`-acodec ${this.codec}`);
533
+ if (this.codecOptions)
534
+ params.push(this.codecOptions);
535
+ if (this.bitrate)
536
+ params.push(`-b:a ${this.bitrate}k`);
537
+ if (this.sampleRate)
538
+ params.push(`-ar ${this.sampleRate}k`);
539
+ if (this.channels)
540
+ params.push(`-ac ${this.channels}`);
541
+ }
542
+ if (this.isSnapshot) {
543
+ if (this.numberFrames)
544
+ params.push(`-frames:v ${this.numberFrames}`);
545
+ if (this.delaySnapshot)
546
+ params.push('-ss 00:00:00.500');
547
+ params.push(...this.buildVideoFilterParams());
548
+ }
549
+ return params;
550
+ }
551
+ buildOutputParameters() {
552
+ const params = [];
553
+ if (this.payloadType)
554
+ params.push(`-payload_type ${this.payloadType}`);
555
+ if (this.ssrc)
556
+ params.push(`-ssrc ${this.ssrc}`);
557
+ if (this.format)
558
+ params.push(`-f ${this.format}`);
559
+ if (this.srtpSuite)
560
+ params.push(`-srtp_out_suite ${this.srtpSuite}`);
561
+ if (this.srtpParams)
562
+ params.push(`-srtp_out_params ${this.srtpParams}`);
563
+ params.push(this.output);
564
+ return params;
565
+ }
566
+ buildParameters() {
567
+ const params = [
568
+ ...this.buildGenericParameters(),
569
+ ...this.buildInputParameters(),
570
+ ...this.buildEncodingParameters(),
571
+ ...this.buildOutputParameters(),
572
+ `-progress tcp://127.0.0.1:${this.progressPort}`,
573
+ ];
574
+ return params;
575
+ }
576
+ static getRecordingArguments(parameters) {
577
+ if (parameters.length === 0) {
578
+ return [];
579
+ }
580
+ const params = [...parameters[0].buildGenericParameters()];
581
+ // input
582
+ params.push(parameters[0].inputSource);
583
+ if (parameters.length > 1 && parameters[0].inputSource !== parameters[1].inputSource) {
584
+ if (parameters[1].processAudio) {
585
+ params.push(parameters[1].inputSource);
586
+ }
587
+ else {
588
+ params.push('-f lavfi -i anullsrc -shortest');
589
+ }
590
+ }
591
+ if (parameters.length === 1) {
592
+ params.push('-an');
593
+ }
594
+ params.push('-sn -dn');
595
+ // video encoding
596
+ params.push(...parameters[0].buildEncodingParameters());
597
+ if (parameters[0].iFrameInterval) {
598
+ params.push(`-force_key_frames expr:gte(t,n_forced*${parameters[0].iFrameInterval / 1000})`);
599
+ }
600
+ // audio encoding
601
+ if (parameters.length > 1) {
602
+ if (parameters[1].processAudio) {
603
+ params.push('-bsf:a aac_adtstoasc');
604
+ }
605
+ params.push(...parameters[1].buildEncodingParameters());
606
+ }
607
+ // fragmented mp4 options
608
+ if (parameters[0].movflags)
609
+ params.push(`-movflags ${parameters[0].movflags}`);
610
+ if (parameters[0].maxMuxingQueueSize)
611
+ params.push(`-max_muxing_queue_size ${parameters[0].maxMuxingQueueSize}`);
612
+ // output
613
+ params.push('-f mp4');
614
+ params.push(parameters[0].output);
615
+ params.push(`-progress tcp://127.0.0.1:${parameters[0].progressPort}`);
616
+ return params;
617
+ }
618
+ static getCombinedArguments(parameters) {
619
+ if (parameters.length === 0) {
620
+ return [];
621
+ }
622
+ const params = [...parameters[0].buildGenericParameters()];
623
+ for (const p of parameters) {
624
+ params.push(...p.buildInputParameters());
625
+ params.push(...p.buildEncodingParameters());
626
+ params.push(...p.buildOutputParameters());
627
+ }
628
+ params.push(`-progress tcp://127.0.0.1:${parameters[0].progressPort}`);
629
+ return params;
630
+ }
631
+ getStreamStartText() {
632
+ if (this.isVideo) {
633
+ const detail = this.codec === 'copy' ? 'native' : `${this.width}x${this.height}, ${this.fps} fps, ${this.bitrate} kbps`;
634
+ return `Starting video stream: ${detail}`;
635
+ }
636
+ if (this.isAudio) {
637
+ const detail = this.codec === 'copy' ? 'native' : `${this.sampleRate} kHz, ${this.bitrate} kbps, codec: ${this.codec}`;
638
+ return `Starting audio stream: ${detail}`;
639
+ }
640
+ return 'Starting unknown stream';
641
+ }
642
+ }
643
+ export class FFmpeg extends EventEmitter {
644
+ process;
645
+ name;
646
+ progress;
647
+ parameters;
648
+ ffmpegExec = ffmpegPath || 'ffmpeg';
649
+ stdin;
650
+ stdout;
651
+ starttime;
652
+ killTimeout;
653
+ constructor(name, parameters) {
654
+ super();
655
+ this.name = name;
656
+ if (Array.isArray(parameters)) {
657
+ if (parameters.length === 0) {
658
+ throw new Error('No ffmpeg parameters found.');
659
+ }
660
+ this.parameters = parameters;
661
+ }
662
+ else {
663
+ this.parameters = [parameters];
664
+ }
665
+ if (this.parameters[0].processor) {
666
+ this.ffmpegExec = this.parameters[0].processor;
667
+ }
668
+ }
669
+ /**
670
+ * Shared initialisation: timestamps the start, creates the progress monitor,
671
+ * spawns the ffmpeg process, and wires up stderr logging.
672
+ */
673
+ async spawnProcess(processArgs, label, opts) {
674
+ this.starttime = Date.now();
675
+ this.progress = await FFmpegProgress.create(this.parameters[0].progressPort);
676
+ this.progress.on('progress started', this.onProgressStarted.bind(this));
677
+ ffmpegLogger.debug(this.name, `${label}: ${this.ffmpegExec} ${processArgs.join(' ')}`);
678
+ this.parameters.forEach((p) => ffmpegLogger.info(this.name, p.getStreamStartText()));
679
+ const child = spawn(this.ffmpegExec, processArgs.join(' ').split(/\s+/), { env: process.env });
680
+ child.stderr.on('data', this.handleStderrData.bind(this));
681
+ if (opts?.attachLifecycle) {
682
+ child.on('error', this.onProcessError.bind(this));
683
+ child.on('exit', this.onProcessExit.bind(this));
684
+ }
685
+ this.process = child;
686
+ this.stdin = child.stdin;
687
+ this.stdout = child.stdout;
688
+ return child;
689
+ }
690
+ async start() {
691
+ const processArgs = FFmpegParameters.getCombinedArguments(this.parameters);
692
+ await this.spawnProcess(processArgs, 'Stream command', { attachLifecycle: true });
693
+ }
694
+ async getResult(input) {
695
+ const processArgs = FFmpegParameters.getCombinedArguments(this.parameters);
696
+ const child = await this.spawnProcess(processArgs, 'Process command');
697
+ return new Promise((resolve, reject) => {
698
+ const killTimeout = setTimeout(() => {
699
+ this.stop();
700
+ reject('ffmpeg process timed out.');
701
+ }, PROCESS_RESULT_TIMEOUT_MS);
702
+ child.on('error', (error) => {
703
+ reject(error);
704
+ this.onProcessError(error);
705
+ });
706
+ let resultBuffer = Buffer.alloc(0);
707
+ child.stdout.on('data', (data) => {
708
+ resultBuffer = Buffer.concat([resultBuffer, data]);
709
+ });
710
+ child.on('exit', () => {
711
+ if (killTimeout) {
712
+ clearTimeout(killTimeout);
713
+ }
714
+ if (resultBuffer.length > 0) {
715
+ resolve(resultBuffer);
716
+ }
717
+ else {
718
+ reject('Failed to fetch data.');
719
+ }
720
+ });
721
+ if (input) {
722
+ child.stdin.end(input);
723
+ }
724
+ });
725
+ }
726
+ async startFragmentedMP4Session() {
727
+ const port = await pickPort({ type: 'tcp' });
728
+ return new Promise((resolve) => {
729
+ const server = net.createServer((socket) => {
730
+ server.close();
731
+ resolve({
732
+ socket: socket,
733
+ process: this.process,
734
+ generator: this.parseFragmentedMP4(socket),
735
+ });
736
+ });
737
+ server.listen(port, async () => {
738
+ this.parameters[0].setOutput(`tcp://127.0.0.1:${port}`);
739
+ const processArgs = FFmpegParameters.getRecordingArguments(this.parameters);
740
+ await this.spawnProcess(processArgs, 'Stream command', { attachLifecycle: true });
741
+ });
742
+ });
743
+ }
744
+ async *parseFragmentedMP4(socket) {
745
+ while (true) {
746
+ const header = await this.readLength(socket, 8);
747
+ const length = header.readInt32BE(0) - 8;
748
+ const type = header.slice(4).toString();
749
+ const data = await this.readLength(socket, length);
750
+ yield {
751
+ header,
752
+ length,
753
+ type,
754
+ data,
755
+ };
756
+ }
757
+ }
758
+ async readLength(socket, length) {
759
+ if (length <= 0) {
760
+ return Buffer.alloc(0);
761
+ }
762
+ const value = socket.read(length);
763
+ if (value) {
764
+ return value;
765
+ }
766
+ return new Promise((resolve, reject) => {
767
+ const readHandler = () => {
768
+ const value = socket.read(length);
769
+ if (value) {
770
+ cleanup();
771
+ resolve(value);
772
+ }
773
+ };
774
+ const endHandler = () => {
775
+ cleanup();
776
+ reject(new Error(`FFMPEG socket closed during read for ${length} bytes!`));
777
+ };
778
+ const cleanup = () => {
779
+ socket.removeListener('readable', readHandler);
780
+ socket.removeListener('close', endHandler);
781
+ };
782
+ if (!socket) {
783
+ throw new Error('FFMPEG socket is closed now!');
784
+ }
785
+ socket.on('readable', readHandler);
786
+ socket.on('close', endHandler);
787
+ });
788
+ }
789
+ stop() {
790
+ const usesStdIn = this.parameters.some(p => p.usesStdInAsInput());
791
+ if (usesStdIn) {
792
+ this.process?.stdin.destroy();
793
+ this.process?.kill('SIGTERM');
794
+ }
795
+ else {
796
+ this.process?.stdin.write('q' + os.EOL);
797
+ }
798
+ this.killTimeout = setTimeout(() => {
799
+ this.process?.kill('SIGKILL');
800
+ }, KILL_GRACE_PERIOD_MS);
801
+ }
802
+ onProgressStarted() {
803
+ this.emit('started');
804
+ const runtime = this.starttime ? (Date.now() - this.starttime) / 1000 : undefined;
805
+ ffmpegLogger.debug(this.name, `process started. Getting the first response took ${runtime} seconds.`);
806
+ }
807
+ handleStderrData(chunk) {
808
+ const output = chunk.toString();
809
+ const isError = output.includes('[panic]') || output.includes('[error]') || output.includes('[fatal]');
810
+ if (isError) {
811
+ ffmpegLogger.error(this.name, 'ffmpeg log message:\n' + output);
812
+ }
813
+ else if (this.parameters[0].debug) {
814
+ ffmpegLogger.debug(this.name, 'ffmpeg log message:\n' + output);
815
+ }
816
+ }
817
+ onProcessError(error) {
818
+ this.emit('error', error);
819
+ }
820
+ onProcessExit(code, signal) {
821
+ this.emit('exit');
822
+ if (this.killTimeout) {
823
+ clearTimeout(this.killTimeout);
824
+ }
825
+ const message = 'FFmpeg exited with code: ' + code + ' and signal: ' + signal;
826
+ if (this.killTimeout && code === 0) {
827
+ ffmpegLogger.info(this.name, message + ' (Expected)');
828
+ }
829
+ else if (code === null || code === 255) {
830
+ if (this.process?.killed) {
831
+ ffmpegLogger.info(this.name, message + ' (Forced)');
832
+ }
833
+ else {
834
+ ffmpegLogger.error(this.name, message + ' (Unexpected)');
835
+ }
836
+ }
837
+ else {
838
+ this.emit('error', message + ' (Error)');
839
+ // ffmpegLogger.error(this.name, message + ' (Error)');
840
+ }
841
+ }
842
+ }
843
+ //# sourceMappingURL=ffmpeg.js.map