@homebridge-plugins/homebridge-eufy-security 5.0.0-beta.35 → 5.0.0-beta.36
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/CONTRIBUTING.md +9 -17
- package/config.schema.json +6 -0
- package/dist/account/ownership.js +16 -16
- package/dist/account/ownership.js.map +1 -1
- package/dist/account/persistence.js +24 -74
- package/dist/account/persistence.js.map +1 -1
- package/dist/account/temporary-authentication.js +2 -2
- package/dist/account/temporary-authentication.js.map +1 -1
- package/dist/configuration.js +18 -25
- package/dist/configuration.js.map +1 -1
- package/dist/device/member-evidence.js +1 -1
- package/dist/device/member-evidence.js.map +1 -1
- package/dist/device/member-trust.js +14 -32
- package/dist/device/member-trust.js.map +1 -1
- package/dist/device/snapshot.js +5 -17
- package/dist/device/snapshot.js.map +1 -1
- package/dist/diagnostics.js +246 -300
- package/dist/diagnostics.js.map +1 -1
- package/dist/homekit/adapter.js +4 -1
- package/dist/homekit/adapter.js.map +1 -1
- package/dist/homekit/adapters/battery.js +3 -3
- package/dist/homekit/adapters/battery.js.map +1 -1
- package/dist/homekit/adapters/camera-controls.js +15 -37
- package/dist/homekit/adapters/camera-controls.js.map +1 -1
- package/dist/homekit/adapters/camera-streaming.js +9 -19
- package/dist/homekit/adapters/camera-streaming.js.map +1 -1
- package/dist/homekit/adapters/contact.js +5 -5
- package/dist/homekit/adapters/contact.js.map +1 -1
- package/dist/homekit/adapters/doorbell.js +3 -15
- package/dist/homekit/adapters/doorbell.js.map +1 -1
- package/dist/homekit/adapters/lock.js +15 -214
- package/dist/homekit/adapters/lock.js.map +1 -1
- package/dist/homekit/adapters/motion.js +2 -2
- package/dist/homekit/adapters/motion.js.map +1 -1
- package/dist/homekit/adapters/package-sensor.js +2 -2
- package/dist/homekit/adapters/package-sensor.js.map +1 -1
- package/dist/homekit/adapters/security-system.js +11 -211
- package/dist/homekit/adapters/security-system.js.map +1 -1
- package/dist/homekit/adapters/siren.js +5 -33
- package/dist/homekit/adapters/siren.js.map +1 -1
- package/dist/homekit/adapters/smart-light.js +19 -217
- package/dist/homekit/adapters/smart-light.js.map +1 -1
- package/dist/homekit/coverage-matrix.js +8 -65
- package/dist/homekit/coverage-matrix.js.map +1 -1
- package/dist/homekit/device-control.js +249 -13
- package/dist/homekit/device-control.js.map +1 -1
- package/dist/homekit/reconciler.js +5 -5
- package/dist/homekit/reconciler.js.map +1 -1
- package/dist/media/last-successful-image.js +3 -20
- package/dist/media/last-successful-image.js.map +1 -1
- package/dist/media/live-preview.js +163 -42
- package/dist/media/live-preview.js.map +1 -1
- package/dist/media/live-stream.js +108 -67
- package/dist/media/live-stream.js.map +1 -1
- package/dist/media/recording.js +37 -120
- package/dist/media/recording.js.map +1 -1
- package/dist/media/snapshot.js +1 -9
- package/dist/media/snapshot.js.map +1 -1
- package/dist/media/station-live-sessions.js +0 -15
- package/dist/media/station-live-sessions.js.map +1 -1
- package/dist/platform.js +10 -10
- package/dist/platform.js.map +1 -1
- package/dist/runtime/channel.js +91 -69
- package/dist/runtime/channel.js.map +1 -1
- package/dist/runtime/owner.js +6 -6
- package/dist/runtime/owner.js.map +1 -1
- package/dist/runtime/sdk-client.js +3 -14
- package/dist/runtime/sdk-client.js.map +1 -1
- package/dist/runtime/tracker.js +8 -38
- package/dist/runtime/tracker.js.map +1 -1
- package/dist/storage.js +1 -1
- package/dist/storage.js.map +1 -1
- package/dist/ui/runtime-channel-client.js +145 -282
- package/dist/ui/runtime-channel-client.js.map +1 -1
- package/dist/ui/server.js +39 -53
- package/dist/ui/server.js.map +1 -1
- package/homebridge-ui/public/app.css +150 -78
- package/homebridge-ui/public/i18n/en.json +14 -2
- package/homebridge-ui/public/i18n/fr.json +14 -2
- package/homebridge-ui/public/index.html +59 -10
- package/homebridge-ui/public/js/app.js +206 -42
- package/homebridge-ui/public/js/dashboard.js +4 -19
- package/package.json +3 -7
- package/homebridge-ui/public/js/legacy-settings.js +0 -37
- package/homebridge-ui/public/js/profile-wizard.js +0 -31
- package/homebridge-ui/public/js/setup-wizard.js +0 -52
|
@@ -1,26 +1,72 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { commonArguments,
|
|
2
|
+
import { adaptationReporter, AdaptationStderr, commonArguments, stopProcess, } from './live-stream.js';
|
|
3
3
|
/** How long one preview runs before it ends on its own, so a page left open does not hold a camera. */
|
|
4
|
-
|
|
4
|
+
const LIVE_PREVIEW_MAX_MS = 3 * 60_000;
|
|
5
|
+
/** How long coded video may flow without decoding to a picture before the HomeKit side ends as unplayable. */
|
|
6
|
+
export const LIVE_PREVIEW_PICTURE_MS = 10_000;
|
|
5
7
|
/** The most bytes one picture may reach before FFmpeg's output is taken as no longer pictures. */
|
|
6
8
|
const MAX_PICTURE_BYTES = 1024 * 1024;
|
|
7
9
|
const JPEG_END = Buffer.from([0xff, 0xd9]);
|
|
8
|
-
/** The
|
|
9
|
-
const
|
|
10
|
+
/** The JPEG output both preview sides are decoded into: five pictures a second, 640 pixels wide, on stdout. */
|
|
11
|
+
const STILLS = ['-an', '-vf', 'fps=5,scale=640:-2', '-q:v', '8', '-f', 'image2pipe', '-c:v', 'mjpeg', 'pipe:1'];
|
|
12
|
+
/** What an iPhone asks a camera for, on Wi-Fi and on cellular: High 4.0 at 30 fps over a 1378-byte MTU. */
|
|
13
|
+
const IPHONE = { fps: 30, profile: 'high', level: '4.0', payloadType: 99, ssrc: 1, mtu: 1378, rtcpInterval: 0.5 };
|
|
14
|
+
const IPHONE_ASKS = {
|
|
15
|
+
wifi: { ...IPHONE, width: 1280, height: 720, maxBitRate: 299 },
|
|
16
|
+
cellular: { ...IPHONE, width: 640, height: 360, maxBitRate: 132 },
|
|
17
|
+
};
|
|
10
18
|
/**
|
|
11
|
-
* A camera's live video as JPEG pictures, five a second and 640 pixels wide,
|
|
19
|
+
* A camera's live video as JPEG pictures for the settings page, five a second and 640 pixels wide, on two sides:
|
|
20
|
+
* `raw` decoded straight from the camera, and `homekit` coded as a live session configured with `adaptation`
|
|
21
|
+
* codes it for that iPhone ask, and decoded again.
|
|
12
22
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
23
|
+
* Each picture carries its side, the encoder of the HomeKit side and, once that has written for a second, the
|
|
24
|
+
* kilobits a second it wrote.
|
|
25
|
+
*
|
|
26
|
+
* The SDK source is fanned out to two FFmpegs: one decodes it into the `raw` pictures, one runs the session's own
|
|
27
|
+
* encoder and argument list and writes the coded H.264 elementary stream, which a third decodes into the
|
|
28
|
+
* `homekit` pictures. The camera side starts at the first keyframe, and each HomeKit run at a keyframe too. No
|
|
29
|
+
* audio. A write either refuses holds the SDK source until it drains, so the SDK's keyframe-aware queue is the
|
|
30
|
+
* only one, and the HomeKit decoder holds the encoder the same way.
|
|
31
|
+
*
|
|
32
|
+
* `adapt` ends the HomeKit run and starts the next with other settings at the next keyframe, leaving the source
|
|
33
|
+
* and the camera side running. A HomeKit run that fails, or yields no picture within
|
|
34
|
+
* {@link LIVE_PREVIEW_PICTURE_MS}, ends alone with an empty `homekit` picture while the camera side showed, and
|
|
35
|
+
* no other starts until `adapt`. `onRun` is called once with each run's result, and with one result where none
|
|
36
|
+
* started. A codec change, a source or camera side failure, the time limit and `stop` end the whole preview, and
|
|
37
|
+
* `onEnd` is called exactly once, saying whether the last run was unplayable. `diagnostics` receives each
|
|
38
|
+
* process's `started` and `output` notices as a live session's do, under the `preview-raw`, `preview-encode` and
|
|
39
|
+
* `preview-decode` roles.
|
|
16
40
|
*/
|
|
17
|
-
export function startLivePreview(executable, source, onPicture, onEnd) {
|
|
41
|
+
export function startLivePreview(executable, media, source, adaptation, onPicture, onRun, onEnd, diagnostics) {
|
|
42
|
+
const report = adaptationReporter(diagnostics);
|
|
18
43
|
const abandonment = new AbortController();
|
|
19
44
|
let live;
|
|
20
|
-
let
|
|
45
|
+
let raw;
|
|
46
|
+
let rawPictures = 0;
|
|
21
47
|
let codec;
|
|
48
|
+
let homekit;
|
|
49
|
+
let latest;
|
|
50
|
+
let wanted = true;
|
|
51
|
+
let last;
|
|
22
52
|
let ended = false;
|
|
23
|
-
const
|
|
53
|
+
const finish = (stopped) => {
|
|
54
|
+
const run = homekit;
|
|
55
|
+
homekit = undefined;
|
|
56
|
+
clearTimeout(run?.pictureless);
|
|
57
|
+
run?.processes.forEach(stopProcess);
|
|
58
|
+
const { profile, level, width, height, fps, maxBitRate } = IPHONE_ASKS[adaptation.ask];
|
|
59
|
+
last = {
|
|
60
|
+
outcome: run?.pictures ? 'picture' : stopped ? 'stopped' : rawPictures ? 'unplayable' : 'no-video',
|
|
61
|
+
selection: { profile, level, width, height, fps, maxBitRate },
|
|
62
|
+
...adaptation,
|
|
63
|
+
...(run ? { encoder: run.coding.encoder } : {}),
|
|
64
|
+
...(codec ? { source: codec } : {}),
|
|
65
|
+
...(run?.coding.kbps === undefined ? {} : { kbps: run.coding.kbps }),
|
|
66
|
+
};
|
|
67
|
+
onRun(last);
|
|
68
|
+
};
|
|
69
|
+
const end = (stopped = false) => {
|
|
24
70
|
if (ended)
|
|
25
71
|
return;
|
|
26
72
|
ended = true;
|
|
@@ -32,34 +78,85 @@ export function startLivePreview(executable, source, onPicture, onEnd) {
|
|
|
32
78
|
catch {
|
|
33
79
|
live = undefined;
|
|
34
80
|
}
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
81
|
+
if (homekit || !last)
|
|
82
|
+
finish(stopped);
|
|
83
|
+
if (raw)
|
|
84
|
+
stopProcess(raw);
|
|
85
|
+
onEnd(last?.outcome === 'unplayable');
|
|
86
|
+
};
|
|
87
|
+
const fail = () => {
|
|
88
|
+
if (!rawPictures)
|
|
89
|
+
return end();
|
|
90
|
+
const encoder = homekit.coding.encoder;
|
|
91
|
+
finish(false);
|
|
92
|
+
onPicture(Buffer.alloc(0), { side: 'homekit', encoder });
|
|
44
93
|
};
|
|
45
|
-
const deadline = setTimeout(end, LIVE_PREVIEW_MAX_MS);
|
|
94
|
+
const deadline = setTimeout(() => end(), LIVE_PREVIEW_MAX_MS);
|
|
46
95
|
deadline.unref();
|
|
47
|
-
const
|
|
48
|
-
const process = spawn(executable,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
96
|
+
const spawnRole = (role, args) => {
|
|
97
|
+
const process = spawn(executable, args, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
98
|
+
const stderr = new AdaptationStderr();
|
|
99
|
+
const lost = () => {
|
|
100
|
+
if (process === raw)
|
|
101
|
+
end();
|
|
102
|
+
else if (homekit?.processes.includes(process))
|
|
103
|
+
fail();
|
|
104
|
+
};
|
|
105
|
+
process.stderr.on('data', (chunk) => stderr.observe(chunk));
|
|
106
|
+
process.once('error', lost);
|
|
107
|
+
process.once('exit', (code, signal) => {
|
|
108
|
+
report(role, 'output', stderr, code, signal);
|
|
109
|
+
lost();
|
|
110
|
+
});
|
|
111
|
+
process.stdin.on('error', lost);
|
|
112
|
+
return process;
|
|
113
|
+
};
|
|
114
|
+
const stills = (output, side, run) => {
|
|
52
115
|
let pending = Buffer.alloc(0);
|
|
53
|
-
|
|
116
|
+
output.on('data', (chunk) => {
|
|
54
117
|
pending = Buffer.concat([pending, chunk]);
|
|
55
118
|
for (let at = pending.indexOf(JPEG_END); at !== -1; at = pending.indexOf(JPEG_END)) {
|
|
56
|
-
|
|
119
|
+
if (run && run !== homekit)
|
|
120
|
+
return;
|
|
121
|
+
if (run) {
|
|
122
|
+
clearTimeout(run.pictureless);
|
|
123
|
+
run.pictures += 1;
|
|
124
|
+
}
|
|
125
|
+
else
|
|
126
|
+
rawPictures += 1;
|
|
127
|
+
onPicture(pending.subarray(0, at + JPEG_END.length), { side, ...(run ?? latest).coding });
|
|
57
128
|
pending = pending.subarray(at + JPEG_END.length);
|
|
58
129
|
}
|
|
59
130
|
if (pending.length > MAX_PICTURE_BYTES)
|
|
60
131
|
end();
|
|
61
132
|
});
|
|
62
|
-
|
|
133
|
+
};
|
|
134
|
+
const startHomeKit = (input) => {
|
|
135
|
+
const ask = IPHONE_ASKS[adaptation.ask];
|
|
136
|
+
const { encoder, args } = media.videoAdaptation(input, ask, ['-f', 'h264', 'pipe:1'], adaptation);
|
|
137
|
+
const cause = last ? 'controller-selection' : 'first';
|
|
138
|
+
diagnostics?.report({ role: 'preview-encode', event: 'started', cause, encoder, source: input });
|
|
139
|
+
const encoding = spawnRole('preview-encode', args);
|
|
140
|
+
diagnostics?.report({ role: 'preview-decode', event: 'started', cause, source: 'h264' });
|
|
141
|
+
const decoding = spawnRole('preview-decode', [...commonArguments('h264'), ...STILLS]);
|
|
142
|
+
const run = { processes: [encoding, decoding], coding: { encoder }, pictures: 0 };
|
|
143
|
+
let firstCodedAt;
|
|
144
|
+
let countedBytes = 0;
|
|
145
|
+
encoding.stdout.on('data', (chunk) => {
|
|
146
|
+
const now = Date.now();
|
|
147
|
+
if (firstCodedAt === undefined) {
|
|
148
|
+
firstCodedAt = now;
|
|
149
|
+
run.pictureless = setTimeout(() => run === homekit && fail(), LIVE_PREVIEW_PICTURE_MS);
|
|
150
|
+
run.pictureless.unref();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
countedBytes += chunk.length;
|
|
154
|
+
if (now - firstCodedAt >= 1_000)
|
|
155
|
+
run.coding.kbps = Math.round((countedBytes * 8) / (now - firstCodedAt));
|
|
156
|
+
});
|
|
157
|
+
encoding.stdout.pipe(decoding.stdin);
|
|
158
|
+
stills(decoding.stdout, 'homekit', run);
|
|
159
|
+
return (latest = run);
|
|
63
160
|
};
|
|
64
161
|
source.live({ signal: abandonment.signal }).then((consumer) => {
|
|
65
162
|
if (ended) {
|
|
@@ -67,30 +164,54 @@ export function startLivePreview(executable, source, onPicture, onEnd) {
|
|
|
67
164
|
return;
|
|
68
165
|
}
|
|
69
166
|
live = consumer;
|
|
70
|
-
consumer.on('error', end);
|
|
71
|
-
consumer.on('stop', end);
|
|
167
|
+
consumer.on('error', () => end());
|
|
168
|
+
consumer.on('stop', () => end());
|
|
72
169
|
consumer.on('budget', (notice) => notice.extend());
|
|
170
|
+
let held = 0;
|
|
171
|
+
const feed = (stdin, data) => {
|
|
172
|
+
if (stdin.write(data))
|
|
173
|
+
return;
|
|
174
|
+
held += 1;
|
|
175
|
+
consumer.pause();
|
|
176
|
+
const release = () => {
|
|
177
|
+
stdin.off('drain', release).off('close', release);
|
|
178
|
+
if (--held === 0)
|
|
179
|
+
consumer.resume();
|
|
180
|
+
};
|
|
181
|
+
stdin.on('drain', release).on('close', release);
|
|
182
|
+
};
|
|
73
183
|
consumer.on('video', (frame) => {
|
|
74
184
|
if (ended)
|
|
75
185
|
return;
|
|
76
|
-
if (!
|
|
77
|
-
const format = INPUT_FORMATS[frame.codec];
|
|
186
|
+
if (!raw) {
|
|
78
187
|
if (!frame.keyframe)
|
|
79
188
|
return;
|
|
80
|
-
if (!format)
|
|
81
|
-
return end();
|
|
82
189
|
codec = frame.codec;
|
|
83
|
-
|
|
190
|
+
diagnostics?.report({ role: 'preview-raw', event: 'started', cause: 'first', source: codec });
|
|
191
|
+
raw = spawnRole('preview-raw', [...commonArguments(codec === 'h265' ? 'hevc' : codec), ...STILLS]);
|
|
192
|
+
stills(raw.stdout, 'raw');
|
|
84
193
|
}
|
|
85
194
|
if (frame.codec !== codec)
|
|
86
195
|
return end();
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
stdin.once('drain', () => consumer.resume());
|
|
196
|
+
if (wanted && frame.keyframe) {
|
|
197
|
+
wanted = false;
|
|
198
|
+
homekit = startHomeKit(codec);
|
|
91
199
|
}
|
|
200
|
+
feed(raw.stdin, frame.data);
|
|
201
|
+
if (homekit)
|
|
202
|
+
feed(homekit.processes[0].stdin, frame.data);
|
|
92
203
|
});
|
|
93
|
-
}, end);
|
|
94
|
-
return
|
|
204
|
+
}, () => end());
|
|
205
|
+
return {
|
|
206
|
+
stop: () => end(true),
|
|
207
|
+
adapt: (next) => {
|
|
208
|
+
if (ended)
|
|
209
|
+
return;
|
|
210
|
+
if (homekit)
|
|
211
|
+
finish(true);
|
|
212
|
+
adaptation = next;
|
|
213
|
+
wanted = true;
|
|
214
|
+
},
|
|
215
|
+
};
|
|
95
216
|
}
|
|
96
217
|
//# sourceMappingURL=live-preview.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"live-preview.js","sourceRoot":"","sources":["../../src/media/live-preview.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"live-preview.js","sourceRoot":"","sources":["../../src/media/live-preview.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { LiveStreamStartError, StationKeyUnavailableError, StationUnreachableError } from '@mega-yfue/eufy-sdk';
|
|
2
2
|
import { createSocket } from 'node:dgram';
|
|
3
3
|
import { execFile, spawn } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
4
5
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
6
|
/** How long a deliberately stopped adaptation process is given to exit before it is killed. */
|
|
6
|
-
|
|
7
|
+
const PROCESS_STOP_GRACE_MS = 2_000;
|
|
8
|
+
/** Closes an adaptation process's input and terminates it, killing it if it has not exited within the grace. */
|
|
9
|
+
export function stopProcess(process) {
|
|
10
|
+
process.stdin?.destroy();
|
|
11
|
+
process.kill('SIGTERM');
|
|
12
|
+
const killDeadline = setTimeout(() => process.kill('SIGKILL'), PROCESS_STOP_GRACE_MS);
|
|
13
|
+
killDeadline.unref?.();
|
|
14
|
+
process.on('exit', () => clearTimeout(killDeadline));
|
|
15
|
+
}
|
|
7
16
|
/**
|
|
8
17
|
* Whether an adaptation opened for `running` can code what the source announced as `announced`.
|
|
9
18
|
*
|
|
@@ -79,14 +88,20 @@ const SOURCE_ACQUISITION_TIMEOUT = Symbol('source-acquisition-timeout');
|
|
|
79
88
|
* and reported `audio-only`. Every other stage is a source error, because nothing else distinguishes it
|
|
80
89
|
* from a transport that failed.
|
|
81
90
|
*/
|
|
82
|
-
function
|
|
91
|
+
function liveSourceFailure(error) {
|
|
92
|
+
return error instanceof LiveStreamStartError && error.stage === 'audio-only' ? 'source-audio-only' : sourceFailure(error);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Why a source stopped producing, as this domain's bounded vocabulary.
|
|
96
|
+
*
|
|
97
|
+
* A station names its own failures apart from the camera's: every camera behind a base that did not connect
|
|
98
|
+
* fails together, and a sealed start the station gave no key for does not resolve by retrying.
|
|
99
|
+
*/
|
|
100
|
+
export function sourceFailure(error) {
|
|
83
101
|
if (error instanceof StationUnreachableError) {
|
|
84
102
|
return 'station-unreachable';
|
|
85
103
|
}
|
|
86
|
-
|
|
87
|
-
return 'station-key-unavailable';
|
|
88
|
-
}
|
|
89
|
-
return error instanceof LiveStreamStartError && error.stage === 'audio-only' ? 'source-audio-only' : 'source-error';
|
|
104
|
+
return error instanceof StationKeyUnavailableError ? 'station-key-unavailable' : 'source-error';
|
|
90
105
|
}
|
|
91
106
|
/** How many diagnostic lines one adaptation retains, which is what a failure is attributed from. */
|
|
92
107
|
const ADAPTATION_STDERR_TAIL_LINES = 8;
|
|
@@ -164,12 +179,14 @@ const FFMPEG_VERSION_BANNER_LENGTH = 120;
|
|
|
164
179
|
* to know whether this binary can be asked to encode: a missing file, a file that is not executable, one
|
|
165
180
|
* that is not FFmpeg, and one that hangs all mean the same thing to the media that would have used it.
|
|
166
181
|
*/
|
|
167
|
-
const probeFfmpegVersion = (path) =>
|
|
168
|
-
execFile(path, ['-hide_banner', '-version'], {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
});
|
|
182
|
+
const probeFfmpegVersion = async (path) => {
|
|
183
|
+
const { stdout } = await promisify(execFile)(path, ['-hide_banner', '-version'], {
|
|
184
|
+
timeout: FFMPEG_VERSION_PROBE_TIMEOUT_MS,
|
|
185
|
+
maxBuffer: 64 * 1024,
|
|
186
|
+
windowsHide: true,
|
|
187
|
+
}).catch(() => ({ stdout: '' }));
|
|
188
|
+
return stdout.split(/\r?\n/, 1)[0]?.trim().slice(0, FFMPEG_VERSION_BANNER_LENGTH) || undefined;
|
|
189
|
+
};
|
|
173
190
|
/**
|
|
174
191
|
* Resolves the complete identity of one adaptation binary, without opening any media.
|
|
175
192
|
*
|
|
@@ -180,6 +197,30 @@ export async function resolveFfmpegIdentity(path, source, probe = probeFfmpegVer
|
|
|
180
197
|
const version = await probe(path).catch(() => undefined);
|
|
181
198
|
return { path, source, ...(version === undefined ? {} : { version }) };
|
|
182
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Reports what one adaptation process did to `diagnostics`, whether or not the session it belonged to fails for it.
|
|
202
|
+
*
|
|
203
|
+
* An audio adaptation is restarted rather than failed and a return-audio one fails only talkback, so without a
|
|
204
|
+
* report of their own those two exit with no account anywhere of why they stopped. An `output` report carries
|
|
205
|
+
* nothing but the tail, so it is made only when the process actually wrote something: a silent process that
|
|
206
|
+
* ended as intended has nothing to attribute.
|
|
207
|
+
*/
|
|
208
|
+
export function adaptationReporter(diagnostics) {
|
|
209
|
+
return (role, event, stderr, code, signal, extras) => {
|
|
210
|
+
const tail = stderr.tail();
|
|
211
|
+
if (event === 'output' && tail.length === 0) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
diagnostics?.report({
|
|
215
|
+
role,
|
|
216
|
+
event,
|
|
217
|
+
...(typeof code === 'number' ? { code } : {}),
|
|
218
|
+
...(typeof signal === 'string' ? { signal } : {}),
|
|
219
|
+
...extras,
|
|
220
|
+
...(tail.length ? { stderr: tail } : {}),
|
|
221
|
+
});
|
|
222
|
+
};
|
|
223
|
+
}
|
|
183
224
|
/** The `libx264` constant rate factor a live adaptation codes at when the negotiated bit rate is ignored. */
|
|
184
225
|
const CONSTANT_QUALITY_CRF = 23;
|
|
185
226
|
/** `libx264`'s profile, level, speed and pixel format, shared by its rate-capped and constant-quality forms. */
|
|
@@ -424,9 +465,10 @@ async function qualification(executable, encoder, selection, run, deadline) {
|
|
|
424
465
|
field('pic_height_in_map_units_minus1') + 1 === Math.ceil(selection.height / 16),
|
|
425
466
|
],
|
|
426
467
|
['frames', (traced.stderr.match(/\bPacket: /g)?.length ?? 0) === selection.fps * QUALIFICATION_SECONDS],
|
|
427
|
-
['bytes', coded.stdout.length * 8 <= videoBudgetInsideCeiling(selection) * 1_000 * (QUALIFICATION_SECONDS + 1)],
|
|
428
468
|
];
|
|
429
|
-
|
|
469
|
+
const bound = videoBudgetInsideCeiling(selection) * 125 * (QUALIFICATION_SECONDS + 1);
|
|
470
|
+
return (checks.find(([, passed]) => !passed)?.[0] ??
|
|
471
|
+
(coded.stdout.length <= bound ? 'qualified' : ['bytes', coded.stdout.length, bound]));
|
|
430
472
|
}
|
|
431
473
|
/** The live video packet size an administrator can choose for networks that drop the negotiated one. */
|
|
432
474
|
export const SMALL_VIDEO_PACKET_SIZE = 1128;
|
|
@@ -479,11 +521,38 @@ export class FfmpegLiveMedia {
|
|
|
479
521
|
}
|
|
480
522
|
return outcomes;
|
|
481
523
|
}
|
|
482
|
-
/**
|
|
483
|
-
|
|
524
|
+
/**
|
|
525
|
+
* What a session codes `selection` from a `codec` source with: `copy` for a passed-through H.264 source,
|
|
526
|
+
* {@link CONSTANT_QUALITY} where the negotiated bit rate is ignored, else the first hardware encoder qualified
|
|
527
|
+
* for `selection`, or `libx264` where none is or `settings` turn hardware off. Without `settings`, this
|
|
528
|
+
* adapter's own.
|
|
529
|
+
*/
|
|
530
|
+
encoderFor(codec, selection, settings) {
|
|
531
|
+
if ((settings?.liveVideoPassthrough ?? this.copyVideo) && codec === 'h264')
|
|
532
|
+
return 'copy';
|
|
533
|
+
if (settings?.ignoreHomeBitRate ?? this.constantQuality)
|
|
534
|
+
return CONSTANT_QUALITY;
|
|
535
|
+
if (settings?.hardwareVideoEncoder === false)
|
|
536
|
+
return LIBX264;
|
|
484
537
|
const key = `${selection.profile}@${selection.level}`;
|
|
485
538
|
return HARDWARE_ENCODERS.find((encoder) => this.qualified.get(encoder.name)?.has(key)) ?? LIBX264;
|
|
486
539
|
}
|
|
540
|
+
/** The largest video packet a session sends under `settings`, or under this adapter's own without them. */
|
|
541
|
+
packetSize(settings) {
|
|
542
|
+
if (!settings)
|
|
543
|
+
return this.maxVideoPacketSize;
|
|
544
|
+
return settings.smallVideoPackets ? SMALL_VIDEO_PACKET_SIZE : Number.POSITIVE_INFINITY;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* The encoder and the argument list a session would code `selection` from a `codec` source with, its video
|
|
548
|
+
* written to `output` instead of RTP: every argument before `output` is the session's own, or that of a session
|
|
549
|
+
* of an adapter configured with `settings`.
|
|
550
|
+
*/
|
|
551
|
+
videoAdaptation(codec, selection, output, settings) {
|
|
552
|
+
const video = { ...selection, mtu: Math.min(selection.mtu, this.packetSize(settings)) };
|
|
553
|
+
const encoder = this.encoderFor(codec, video, settings);
|
|
554
|
+
return { encoder: encoder === 'copy' ? encoder : encoder.name, args: videoArguments(codec, video, encoder, output) };
|
|
555
|
+
}
|
|
487
556
|
/**
|
|
488
557
|
* Reserves the negotiated output ports so `SetupEndpoints` can be answered with them, and returns a
|
|
489
558
|
* session that holds nothing else: no SDK handle, adaptation process, or device session exists until
|
|
@@ -493,9 +562,7 @@ export class FfmpegLiveMedia {
|
|
|
493
562
|
async prepare(transport) {
|
|
494
563
|
const videoPort = await this.reservePort(transport.addressVersion);
|
|
495
564
|
const targetAddress = transport.addressVersion === 'ipv6' ? `[${transport.targetAddress}]` : transport.targetAddress;
|
|
496
|
-
const maxVideoPacketSize = this.
|
|
497
|
-
const copyVideo = this.copyVideo;
|
|
498
|
-
const constantQuality = this.constantQuality;
|
|
565
|
+
const maxVideoPacketSize = this.packetSize(transport.liveVideoSettings);
|
|
499
566
|
let audioPort;
|
|
500
567
|
try {
|
|
501
568
|
audioPort = transport.audio ? await this.reservePort(transport.addressVersion) : undefined;
|
|
@@ -587,38 +654,14 @@ export class FfmpegLiveMedia {
|
|
|
587
654
|
}
|
|
588
655
|
sink.once('drain', () => releaseAdaptation(sink));
|
|
589
656
|
};
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
*
|
|
593
|
-
* An audio adaptation is restarted rather than failed and a return-audio one fails only talkback, so
|
|
594
|
-
* without a report of their own those two exit with no account anywhere of why they stopped. An `output`
|
|
595
|
-
* report carries nothing but the tail, so it is made only when the process actually wrote something:
|
|
596
|
-
* a silent process that ended as intended has nothing to attribute.
|
|
597
|
-
*/
|
|
598
|
-
const reportAdaptation = (role, event, stderr, code, signal) => {
|
|
599
|
-
const tail = stderr.tail();
|
|
600
|
-
if (event === 'output' && tail.length === 0) {
|
|
601
|
-
return;
|
|
602
|
-
}
|
|
603
|
-
adaptationDiagnostics?.report({
|
|
604
|
-
role,
|
|
605
|
-
event,
|
|
606
|
-
...(typeof code === 'number' ? { code } : {}),
|
|
607
|
-
...(typeof signal === 'string' ? { signal } : {}),
|
|
608
|
-
...(tail.length ? { stderr: tail } : {}),
|
|
609
|
-
});
|
|
610
|
-
};
|
|
611
|
-
const stopProcess = (process) => {
|
|
657
|
+
const reportAdaptation = adaptationReporter(adaptationDiagnostics);
|
|
658
|
+
const stopAdaptation = (process) => {
|
|
612
659
|
if (!process) {
|
|
613
660
|
return;
|
|
614
661
|
}
|
|
615
662
|
stoppingProcesses.add(process);
|
|
616
|
-
process
|
|
663
|
+
stopProcess(process);
|
|
617
664
|
releaseAdaptation(process.stdin);
|
|
618
|
-
process.kill('SIGTERM');
|
|
619
|
-
const killDeadline = setTimeout(() => process.kill('SIGKILL'), PROCESS_STOP_GRACE_MS);
|
|
620
|
-
killDeadline.unref?.();
|
|
621
|
-
process.on('exit', () => clearTimeout(killDeadline));
|
|
622
665
|
};
|
|
623
666
|
/** Stop a caller-owned SDK handle without allowing synchronous or asynchronous faults to abort cleanup. */
|
|
624
667
|
const stopTalkbackHandle = (handle) => {
|
|
@@ -632,7 +675,7 @@ export class FfmpegLiveMedia {
|
|
|
632
675
|
return;
|
|
633
676
|
}
|
|
634
677
|
talkbackEnded = true;
|
|
635
|
-
|
|
678
|
+
stopAdaptation(returnAudioProcess);
|
|
636
679
|
returnAudioProcess?.stdout.destroy();
|
|
637
680
|
returnAudioProcess?.eld.destroy();
|
|
638
681
|
returnAudioProcess = undefined;
|
|
@@ -661,8 +704,8 @@ export class FfmpegLiveMedia {
|
|
|
661
704
|
clearTimeout(initialRtcpGrace);
|
|
662
705
|
clearTimeout(videoStartBackstop);
|
|
663
706
|
stopTalkback();
|
|
664
|
-
|
|
665
|
-
|
|
707
|
+
stopAdaptation(videoProcess);
|
|
708
|
+
stopAdaptation(audioProcess);
|
|
666
709
|
source?.stop();
|
|
667
710
|
if (negotiated) {
|
|
668
711
|
transport.onSessionReleased?.(videoFailed ? 'failed' : 'requested', {
|
|
@@ -778,18 +821,15 @@ export class FfmpegLiveMedia {
|
|
|
778
821
|
return;
|
|
779
822
|
}
|
|
780
823
|
videoCause = 'source-configuration';
|
|
781
|
-
|
|
824
|
+
stopAdaptation(videoProcess);
|
|
782
825
|
videoProcess = undefined;
|
|
783
826
|
}
|
|
784
827
|
if (!videoProcess) {
|
|
785
828
|
adaptationConfig = videoConfig;
|
|
786
829
|
videoSequence = (videoSequence + OUTPUT_SEQUENCE_STEP) & 0xffff;
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
? CONSTANT_QUALITY
|
|
791
|
-
: this.encoderFor(negotiated.video);
|
|
792
|
-
const child = this.createProcess(this.executable, videoArguments(videoConfig, negotiated.video, targetAddress, transport.video, videoSequence, encoder));
|
|
830
|
+
const { video } = negotiated;
|
|
831
|
+
const encoder = this.encoderFor(videoConfig.codec, video, transport.liveVideoSettings);
|
|
832
|
+
const child = this.createProcess(this.executable, videoArguments(videoConfig.codec, video, encoder, outputArguments(targetAddress, transport.video, video.payloadType, video.ssrc, video.mtu, videoSequence)));
|
|
793
833
|
videoProcess = child;
|
|
794
834
|
const encoderName = encoder === 'copy' ? encoder : encoder.name;
|
|
795
835
|
adaptationDiagnostics?.report({
|
|
@@ -869,14 +909,14 @@ export class FfmpegLiveMedia {
|
|
|
869
909
|
if (!stoppingProcesses.has(child) && audioProcess === child) {
|
|
870
910
|
reportAdaptation('live-audio', 'input-failed', stderr);
|
|
871
911
|
}
|
|
872
|
-
|
|
912
|
+
stopAdaptation(child);
|
|
873
913
|
clearChild();
|
|
874
914
|
});
|
|
875
915
|
child.on('error', () => {
|
|
876
916
|
if (!stoppingProcesses.has(child)) {
|
|
877
917
|
reportAdaptation('live-audio', producedOutput ? 'exited-while-streaming' : 'spawn-failed', stderr);
|
|
878
918
|
}
|
|
879
|
-
|
|
919
|
+
stopAdaptation(child);
|
|
880
920
|
clearChild();
|
|
881
921
|
});
|
|
882
922
|
child.on('exit', (code, signal) => {
|
|
@@ -889,7 +929,7 @@ export class FfmpegLiveMedia {
|
|
|
889
929
|
});
|
|
890
930
|
}
|
|
891
931
|
if (audioInputCodec !== frame.codec) {
|
|
892
|
-
|
|
932
|
+
stopAdaptation(audioProcess);
|
|
893
933
|
audioProcess = undefined;
|
|
894
934
|
audioInputCodec = undefined;
|
|
895
935
|
writeAudio(frame);
|
|
@@ -1060,9 +1100,11 @@ export class FfmpegLiveMedia {
|
|
|
1060
1100
|
return;
|
|
1061
1101
|
}
|
|
1062
1102
|
reportAdaptation('return-audio', producedOutput ? 'exited-while-streaming' : 'exited-before-output', stderr, code, signal);
|
|
1063
|
-
failTalkback(stderr.tail().some((line) => line.includes('Error during demuxing: Operation timed out'))
|
|
1064
|
-
? '
|
|
1065
|
-
:
|
|
1103
|
+
failTalkback(!stderr.tail().some((line) => line.includes('Error during demuxing: Operation timed out'))
|
|
1104
|
+
? 'adaptation-failed'
|
|
1105
|
+
: receivedVideoKeyframe
|
|
1106
|
+
? 'no-controller-audio'
|
|
1107
|
+
: 'source-unavailable');
|
|
1066
1108
|
});
|
|
1067
1109
|
child.stdin.end(returnAudioSdp(audioPort.port, selection, transport.audio, transport.addressVersion));
|
|
1068
1110
|
await delay(RETURN_AUDIO_BIND_GRACE_MS);
|
|
@@ -1101,7 +1143,7 @@ export class FfmpegLiveMedia {
|
|
|
1101
1143
|
}
|
|
1102
1144
|
catch (error) {
|
|
1103
1145
|
acquisition.abort(new Error('live media source acquisition was cancelled'));
|
|
1104
|
-
failVideo(error === SOURCE_ACQUISITION_TIMEOUT ? 'source-acquisition-timeout' :
|
|
1146
|
+
failVideo(error === SOURCE_ACQUISITION_TIMEOUT ? 'source-acquisition-timeout' : liveSourceFailure(error), error);
|
|
1105
1147
|
throw error === SOURCE_ACQUISITION_TIMEOUT ? new Error('live media source acquisition timed out') : error;
|
|
1106
1148
|
}
|
|
1107
1149
|
finally {
|
|
@@ -1122,7 +1164,7 @@ export class FfmpegLiveMedia {
|
|
|
1122
1164
|
notice.extend();
|
|
1123
1165
|
}
|
|
1124
1166
|
});
|
|
1125
|
-
source.on('error', (error) => failVideo(
|
|
1167
|
+
source.on('error', (error) => failVideo(liveSourceFailure(error), error));
|
|
1126
1168
|
source.on('stop', () => failVideo('source-stopped'));
|
|
1127
1169
|
await returnAudioReady;
|
|
1128
1170
|
},
|
|
@@ -1240,12 +1282,11 @@ function outputArguments(targetAddress, target, payloadType, ssrc, packetSize, s
|
|
|
1240
1282
|
* honoured: the output spends what the picture needs. A controller asks 299 kbps for 720p and does not raise it
|
|
1241
1283
|
* during a session, which the ceiling above codes as a blurred, then blocky, picture.
|
|
1242
1284
|
*/
|
|
1243
|
-
function videoArguments(
|
|
1244
|
-
const output = outputArguments(targetAddress, target, selection.payloadType, selection.ssrc, selection.mtu, sequence);
|
|
1285
|
+
function videoArguments(codec, selection, encoder, output) {
|
|
1245
1286
|
if (encoder === 'copy') {
|
|
1246
1287
|
return [...commonArguments('h264'), '-an', '-c:v', 'copy', ...output];
|
|
1247
1288
|
}
|
|
1248
|
-
return [...commonArguments(
|
|
1289
|
+
return [...commonArguments(codec === 'h265' ? 'hevc' : codec), ...encodeArguments(encoder, selection), ...output];
|
|
1249
1290
|
}
|
|
1250
1291
|
/** What one encoder is given to code `selection`, between its input and its output, in a session and in qualification alike. */
|
|
1251
1292
|
function encodeArguments(encoder, selection) {
|