@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.
Files changed (86) hide show
  1. package/CONTRIBUTING.md +9 -17
  2. package/config.schema.json +6 -0
  3. package/dist/account/ownership.js +16 -16
  4. package/dist/account/ownership.js.map +1 -1
  5. package/dist/account/persistence.js +24 -74
  6. package/dist/account/persistence.js.map +1 -1
  7. package/dist/account/temporary-authentication.js +2 -2
  8. package/dist/account/temporary-authentication.js.map +1 -1
  9. package/dist/configuration.js +18 -25
  10. package/dist/configuration.js.map +1 -1
  11. package/dist/device/member-evidence.js +1 -1
  12. package/dist/device/member-evidence.js.map +1 -1
  13. package/dist/device/member-trust.js +14 -32
  14. package/dist/device/member-trust.js.map +1 -1
  15. package/dist/device/snapshot.js +5 -17
  16. package/dist/device/snapshot.js.map +1 -1
  17. package/dist/diagnostics.js +246 -300
  18. package/dist/diagnostics.js.map +1 -1
  19. package/dist/homekit/adapter.js +4 -1
  20. package/dist/homekit/adapter.js.map +1 -1
  21. package/dist/homekit/adapters/battery.js +3 -3
  22. package/dist/homekit/adapters/battery.js.map +1 -1
  23. package/dist/homekit/adapters/camera-controls.js +15 -37
  24. package/dist/homekit/adapters/camera-controls.js.map +1 -1
  25. package/dist/homekit/adapters/camera-streaming.js +9 -19
  26. package/dist/homekit/adapters/camera-streaming.js.map +1 -1
  27. package/dist/homekit/adapters/contact.js +5 -5
  28. package/dist/homekit/adapters/contact.js.map +1 -1
  29. package/dist/homekit/adapters/doorbell.js +3 -15
  30. package/dist/homekit/adapters/doorbell.js.map +1 -1
  31. package/dist/homekit/adapters/lock.js +15 -214
  32. package/dist/homekit/adapters/lock.js.map +1 -1
  33. package/dist/homekit/adapters/motion.js +2 -2
  34. package/dist/homekit/adapters/motion.js.map +1 -1
  35. package/dist/homekit/adapters/package-sensor.js +2 -2
  36. package/dist/homekit/adapters/package-sensor.js.map +1 -1
  37. package/dist/homekit/adapters/security-system.js +11 -211
  38. package/dist/homekit/adapters/security-system.js.map +1 -1
  39. package/dist/homekit/adapters/siren.js +5 -33
  40. package/dist/homekit/adapters/siren.js.map +1 -1
  41. package/dist/homekit/adapters/smart-light.js +19 -217
  42. package/dist/homekit/adapters/smart-light.js.map +1 -1
  43. package/dist/homekit/coverage-matrix.js +8 -65
  44. package/dist/homekit/coverage-matrix.js.map +1 -1
  45. package/dist/homekit/device-control.js +249 -13
  46. package/dist/homekit/device-control.js.map +1 -1
  47. package/dist/homekit/reconciler.js +5 -5
  48. package/dist/homekit/reconciler.js.map +1 -1
  49. package/dist/media/last-successful-image.js +3 -20
  50. package/dist/media/last-successful-image.js.map +1 -1
  51. package/dist/media/live-preview.js +163 -42
  52. package/dist/media/live-preview.js.map +1 -1
  53. package/dist/media/live-stream.js +108 -67
  54. package/dist/media/live-stream.js.map +1 -1
  55. package/dist/media/recording.js +37 -120
  56. package/dist/media/recording.js.map +1 -1
  57. package/dist/media/snapshot.js +1 -9
  58. package/dist/media/snapshot.js.map +1 -1
  59. package/dist/media/station-live-sessions.js +0 -15
  60. package/dist/media/station-live-sessions.js.map +1 -1
  61. package/dist/platform.js +10 -10
  62. package/dist/platform.js.map +1 -1
  63. package/dist/runtime/channel.js +91 -69
  64. package/dist/runtime/channel.js.map +1 -1
  65. package/dist/runtime/owner.js +6 -6
  66. package/dist/runtime/owner.js.map +1 -1
  67. package/dist/runtime/sdk-client.js +3 -14
  68. package/dist/runtime/sdk-client.js.map +1 -1
  69. package/dist/runtime/tracker.js +8 -38
  70. package/dist/runtime/tracker.js.map +1 -1
  71. package/dist/storage.js +1 -1
  72. package/dist/storage.js.map +1 -1
  73. package/dist/ui/runtime-channel-client.js +145 -282
  74. package/dist/ui/runtime-channel-client.js.map +1 -1
  75. package/dist/ui/server.js +39 -53
  76. package/dist/ui/server.js.map +1 -1
  77. package/homebridge-ui/public/app.css +150 -78
  78. package/homebridge-ui/public/i18n/en.json +14 -2
  79. package/homebridge-ui/public/i18n/fr.json +14 -2
  80. package/homebridge-ui/public/index.html +59 -10
  81. package/homebridge-ui/public/js/app.js +206 -42
  82. package/homebridge-ui/public/js/dashboard.js +4 -19
  83. package/package.json +3 -7
  84. package/homebridge-ui/public/js/legacy-settings.js +0 -37
  85. package/homebridge-ui/public/js/profile-wizard.js +0 -31
  86. 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, PROCESS_STOP_GRACE_MS } from './live-stream.js';
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
- export const LIVE_PREVIEW_MAX_MS = 3 * 60_000;
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 FFmpeg input format each SDK codec is read in. AV1 has none, so such a camera has no preview. */
9
- const INPUT_FORMATS = { h264: 'h264', h265: 'hevc' };
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, for the settings page.
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
- * Starts at the first keyframe and has no audio. A write FFmpeg refuses holds the SDK source until it drains,
14
- * so the SDK's keyframe-aware queue is the only one. A codec change, a source or FFmpeg failure, the time limit
15
- * and the returned stop all end it, and `onEnd` is called exactly once whichever came first.
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 ffmpeg;
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 end = () => {
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 (ffmpeg) {
36
- const process = ffmpeg;
37
- process.stdin?.destroy();
38
- process.kill('SIGTERM');
39
- const kill = setTimeout(() => process.kill('SIGKILL'), PROCESS_STOP_GRACE_MS);
40
- kill.unref();
41
- process.once('exit', () => clearTimeout(kill));
42
- }
43
- onEnd();
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 adapt = (format) => {
48
- const process = spawn(executable, [...commonArguments(format), '-an', '-vf', 'fps=5,scale=640:-2', '-q:v', '8', '-f', 'image2pipe', '-c:v', 'mjpeg', 'pipe:1'], { stdio: ['pipe', 'pipe', 'ignore'] });
49
- process.once('error', end);
50
- process.once('exit', end);
51
- process.stdin.on('error', end);
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
- process.stdout.on('data', (chunk) => {
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
- onPicture(pending.subarray(0, at + JPEG_END.length));
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
- return process;
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 (!ffmpeg) {
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
- ffmpeg = adapt(format);
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
- const stdin = ffmpeg.stdin;
88
- if (!stdin.write(frame.data)) {
89
- consumer.pause();
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 end;
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,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAK3C,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAE1E,uGAAuG;AACvG,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,GAAG,MAAM,CAAC;AAE9C,kGAAkG;AAClG,MAAM,iBAAiB,GAAG,IAAI,GAAG,IAAI,CAAC;AAEtC,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;AAE3C,wGAAwG;AACxG,MAAM,aAAa,GAAqD,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAEvG;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAC9B,UAAkB,EAClB,MAAuB,EACvB,SAAiC,EACjC,KAAiB;IAEjB,MAAM,WAAW,GAAG,IAAI,eAAe,EAAE,CAAC;IAC1C,IAAI,IAAoC,CAAC;IACzC,IAAI,MAA4C,CAAC;IACjD,IAAI,KAA0C,CAAC;IAC/C,IAAI,KAAK,GAAG,KAAK,CAAC;IAClB,MAAM,GAAG,GAAG,GAAS,EAAE;QACrB,IAAI,KAAK;YAAE,OAAO;QAClB,KAAK,GAAG,IAAI,CAAC;QACb,YAAY,CAAC,QAAQ,CAAC,CAAC;QACvB,WAAW,CAAC,KAAK,EAAE,CAAC;QACpB,IAAI,CAAC;YACH,IAAI,EAAE,IAAI,EAAE,CAAC;QACf,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,GAAG,SAAS,CAAC;QACnB,CAAC;QACD,IAAI,MAAM,EAAE,CAAC;YACX,MAAM,OAAO,GAAG,MAAM,CAAC;YACvB,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC;YACzB,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YACxB,MAAM,IAAI,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,qBAAqB,CAAC,CAAC;YAC9E,IAAI,CAAC,KAAK,EAAE,CAAC;YACb,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;QACjD,CAAC;QACD,KAAK,EAAE,CAAC;IACV,CAAC,CAAC;IACF,MAAM,QAAQ,GAAG,UAAU,CAAC,GAAG,EAAE,mBAAmB,CAAC,CAAC;IACtD,QAAQ,CAAC,KAAK,EAAE,CAAC;IAEjB,MAAM,KAAK,GAAG,CAAC,MAAc,EAA4B,EAAE;QACzD,MAAM,OAAO,GAAG,KAAK,CACnB,UAAU,EACV,CAAC,GAAG,eAAe,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,oBAAoB,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAC,EAC5H,EAAE,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CACtC,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QAC3B,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAC1B,OAAO,CAAC,KAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QAChC,IAAI,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAC9B,OAAO,CAAC,MAAO,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;YAC3C,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;YAC1C,KAAK,IAAI,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,EAAE,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACnF,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,EAAE,EAAE,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;gBACrD,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,EAAE,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;YACnD,CAAC;YACD,IAAI,OAAO,CAAC,MAAM,GAAG,iBAAiB;gBAAE,GAAG,EAAE,CAAC;QAChD,CAAC,CAAC,CAAC;QACH,OAAO,OAAO,CAAC;IACjB,CAAC,CAAC;IAEF,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAC9C,CAAC,QAAQ,EAAE,EAAE;QACX,IAAI,KAAK,EAAE,CAAC;YACV,QAAQ,CAAC,IAAI,EAAE,CAAC;YAChB,OAAO;QACT,CAAC;QACD,IAAI,GAAG,QAAQ,CAAC;QAChB,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QAC1B,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QACzB,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;QACnD,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAqB,EAAE,EAAE;YAC7C,IAAI,KAAK;gBAAE,OAAO;YAClB,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,MAAM,GAAG,aAAa,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;gBAC1C,IAAI,CAAC,KAAK,CAAC,QAAQ;oBAAE,OAAO;gBAC5B,IAAI,CAAC,MAAM;oBAAE,OAAO,GAAG,EAAE,CAAC;gBAC1B,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;gBACpB,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;YACzB,CAAC;YACD,IAAI,KAAK,CAAC,KAAK,KAAK,KAAK;gBAAE,OAAO,GAAG,EAAE,CAAC;YACxC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAM,CAAC;YAC5B,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC7B,QAAQ,CAAC,KAAK,EAAE,CAAC;gBACjB,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;YAC/C,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC,EACD,GAAG,CACJ,CAAC;IACF,OAAO,GAAG,CAAC;AACb,CAAC"}
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
- export const PROCESS_STOP_GRACE_MS = 2_000;
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 sourceFailure(error) {
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
- if (error instanceof StationKeyUnavailableError) {
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) => new Promise((resolve) => {
168
- execFile(path, ['-hide_banner', '-version'], { timeout: FFMPEG_VERSION_PROBE_TIMEOUT_MS, maxBuffer: 64 * 1024, windowsHide: true }, (error, stdout) => {
169
- const banner = error ? undefined : stdout.split(/\r?\n/, 1)[0]?.trim().slice(0, FFMPEG_VERSION_BANNER_LENGTH);
170
- resolve(banner === undefined || banner === '' ? undefined : banner);
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
- return checks.find(([, passed]) => !passed)?.[0] ?? 'qualified';
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
- /** The first hardware encoder qualified for `selection`, or `libx264` where none is. */
483
- encoderFor(selection) {
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.maxVideoPacketSize;
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
- * Reports what one adaptation process did, whether or not the session it belonged to fails for it.
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.stdin.destroy();
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
- stopProcess(returnAudioProcess);
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
- stopProcess(videoProcess);
665
- stopProcess(audioProcess);
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
- stopProcess(videoProcess);
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 encoder = copyVideo && videoConfig.codec === 'h264'
788
- ? 'copy'
789
- : constantQuality
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
- stopProcess(child);
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
- stopProcess(child);
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
- stopProcess(audioProcess);
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
- ? 'no-controller-audio'
1065
- : 'adaptation-failed');
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' : sourceFailure(error), error);
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(sourceFailure(error), error));
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(input, selection, targetAddress, target, sequence, encoder) {
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(input.codec === 'h265' ? 'hevc' : input.codec), ...encodeArguments(encoder, selection), ...output];
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) {