@homebridge-plugins/homebridge-eufy-security 5.0.0-beta.35 → 5.0.0-beta.37
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 +268 -308
- 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 +126 -72
- 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 +15 -2
- package/homebridge-ui/public/i18n/fr.json +15 -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/i18n/runtime/en.json +1 -0
- 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
package/dist/diagnostics.js
CHANGED
|
@@ -229,6 +229,7 @@ const SDK_EVENT_KEYS = new Set([
|
|
|
229
229
|
'protocol-command',
|
|
230
230
|
'protocol-unhandled',
|
|
231
231
|
'realtime-readiness',
|
|
232
|
+
'rtc-sign-refused',
|
|
232
233
|
'session-connecting',
|
|
233
234
|
'session-idle',
|
|
234
235
|
'session-resumed',
|
|
@@ -417,6 +418,10 @@ function boundedText(value, maximumLength) {
|
|
|
417
418
|
.trim();
|
|
418
419
|
return printable === '' ? undefined : printable;
|
|
419
420
|
}
|
|
421
|
+
/** Whether a value is a plain JSON object, which is the only shape a record crossing a process boundary has. */
|
|
422
|
+
function isRecord(value) {
|
|
423
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
424
|
+
}
|
|
420
425
|
function fleetPath(storageRoot) {
|
|
421
426
|
return join(storageRoot, DIAGNOSTICS_DIRECTORY, FLEET_FILE);
|
|
422
427
|
}
|
|
@@ -443,10 +448,9 @@ function readFleet(storageRoot) {
|
|
|
443
448
|
if (candidate.version !== 1 || !Array.isArray(candidate.devices)) {
|
|
444
449
|
return undefined;
|
|
445
450
|
}
|
|
446
|
-
const devices = candidate.devices.flatMap((
|
|
447
|
-
if (!
|
|
451
|
+
const devices = candidate.devices.flatMap((row) => {
|
|
452
|
+
if (!isRecord(row))
|
|
448
453
|
return [];
|
|
449
|
-
const row = entry;
|
|
450
454
|
const accessory = opaqueAccessoryAlias(row.accessory);
|
|
451
455
|
if (accessory === undefined || typeof row.represented !== 'boolean' || typeof row.attached !== 'boolean') {
|
|
452
456
|
return [];
|
|
@@ -532,19 +536,16 @@ function readHostEnvironment(storageRoot) {
|
|
|
532
536
|
* entire purpose of the record and a redacted one answers nothing. Neither is device or account material: the
|
|
533
537
|
* path is this plugin's own setting or the binary it ships, and the banner is that build's public identity.
|
|
534
538
|
*/
|
|
535
|
-
function readFfmpegIdentity(
|
|
536
|
-
if (!
|
|
539
|
+
function readFfmpegIdentity(recorded) {
|
|
540
|
+
if (!isRecord(recorded)) {
|
|
537
541
|
return undefined;
|
|
538
542
|
}
|
|
539
|
-
const recorded = value;
|
|
540
543
|
if (recorded.source !== 'bundled' && recorded.source !== 'configured') {
|
|
541
544
|
return undefined;
|
|
542
545
|
}
|
|
543
546
|
const path = typeof recorded.path === 'string' ? boundedText(recorded.path, MAX_FFMPEG_IDENTITY_LENGTH) : undefined;
|
|
544
547
|
const version = typeof recorded.version === 'string' ? boundedText(recorded.version, MAX_FFMPEG_IDENTITY_LENGTH) : undefined;
|
|
545
|
-
const encoders = recorded.encoders
|
|
546
|
-
? readEncoderQualification(recorded.encoders)
|
|
547
|
-
: undefined;
|
|
548
|
+
const encoders = isRecord(recorded.encoders) ? readEncoderQualification(recorded.encoders) : undefined;
|
|
548
549
|
return path === undefined
|
|
549
550
|
? undefined
|
|
550
551
|
: {
|
|
@@ -556,18 +557,23 @@ function readFfmpegIdentity(value) {
|
|
|
556
557
|
}
|
|
557
558
|
/**
|
|
558
559
|
* The recorded encoder qualification, keeping only encoder names, `profile@level` selections and outcomes this
|
|
559
|
-
* build names, so the record is bounded by those three closed sets.
|
|
560
|
+
* build names, so the record is bounded by those three closed sets, and a `bytes` outcome's two integers.
|
|
560
561
|
*/
|
|
561
562
|
function readEncoderQualification(recorded) {
|
|
562
563
|
return Object.fromEntries(LIVE_VIDEO_ENCODERS.filter((name) => Object.hasOwn(recorded, name)).map((name) => {
|
|
563
|
-
const outcomes = recorded[name]
|
|
564
|
+
const outcomes = isRecord(recorded[name]) ? Object.entries(recorded[name]) : [];
|
|
564
565
|
return [
|
|
565
566
|
name,
|
|
566
567
|
Object.fromEntries(outcomes.flatMap(([selection, value]) => {
|
|
567
568
|
const [profile, level] = selection.split('@');
|
|
568
|
-
const outcome =
|
|
569
|
-
|
|
570
|
-
|
|
569
|
+
const outcome = Array.isArray(value) &&
|
|
570
|
+
value.length === 3 &&
|
|
571
|
+
value[0] === 'bytes' &&
|
|
572
|
+
value.slice(1).every(Number.isSafeInteger)
|
|
573
|
+
? value
|
|
574
|
+
: allowlistedLabel(value, QUALIFICATION_OUTCOMES);
|
|
575
|
+
return allowlistedLabel(profile, HOMEKIT_LIVE_VIDEO_PROFILES) &&
|
|
576
|
+
allowlistedLabel(level, HOMEKIT_LIVE_VIDEO_LEVELS) &&
|
|
571
577
|
selection === `${profile}@${level}` &&
|
|
572
578
|
outcome
|
|
573
579
|
? [[selection, outcome]]
|
|
@@ -1231,8 +1237,8 @@ export class GuidedDiagnostics {
|
|
|
1231
1237
|
await this.replaceOwnedFile(join(directory, REPRODUCTION_MARKERS_FILE), `${retained}${marker}`);
|
|
1232
1238
|
}
|
|
1233
1239
|
}
|
|
1234
|
-
function formatMessage(
|
|
1235
|
-
const template =
|
|
1240
|
+
function formatMessage(key, parameters = {}) {
|
|
1241
|
+
const template = EN_MESSAGES[key] ?? key;
|
|
1236
1242
|
return template
|
|
1237
1243
|
.replace(/\{([a-zA-Z0-9]+)\}/g, (placeholder, name) => parameters[name] === undefined ? placeholder : String(parameters[name]))
|
|
1238
1244
|
.replace(/\s+/g, ' ')
|
|
@@ -1240,7 +1246,7 @@ function formatMessage(catalog, key, parameters = {}) {
|
|
|
1240
1246
|
}
|
|
1241
1247
|
function localize(target, key, parameters = {}) {
|
|
1242
1248
|
const translator = target.localize;
|
|
1243
|
-
return translator?.(key, parameters) ?? formatMessage(
|
|
1249
|
+
return translator?.(key, parameters) ?? formatMessage(key, parameters);
|
|
1244
1250
|
}
|
|
1245
1251
|
class JsonLineLog {
|
|
1246
1252
|
onError;
|
|
@@ -1257,11 +1263,8 @@ class JsonLineLog {
|
|
|
1257
1263
|
this.directory = join(storageRoot, LOG_DIRECTORY);
|
|
1258
1264
|
this.path = join(this.directory, LOG_FILE);
|
|
1259
1265
|
}
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
if (!payload) {
|
|
1263
|
-
return;
|
|
1264
|
-
}
|
|
1266
|
+
/** Queues one record the sanitizer already rebuilt. */
|
|
1267
|
+
write(payload) {
|
|
1265
1268
|
const record = this.serialize(payload);
|
|
1266
1269
|
const bytes = Buffer.byteLength(record);
|
|
1267
1270
|
if (bytes > MAX_LOG_RECORD_BYTES || this.queuedBytes + bytes > MAX_QUEUED_LOG_BYTES) {
|
|
@@ -1269,21 +1272,12 @@ class JsonLineLog {
|
|
|
1269
1272
|
this.droppedRecords += 1;
|
|
1270
1273
|
return;
|
|
1271
1274
|
}
|
|
1272
|
-
const
|
|
1273
|
-
const droppedAt = this.droppedAt;
|
|
1274
|
-
this.droppedRecords = 0;
|
|
1275
|
-
this.droppedAt = undefined;
|
|
1275
|
+
const dropped = this.drainDropped();
|
|
1276
1276
|
this.queuedBytes += bytes;
|
|
1277
1277
|
this.pending = this.pending
|
|
1278
1278
|
.then(async () => {
|
|
1279
|
-
if (
|
|
1280
|
-
await this.append(
|
|
1281
|
-
scope: 'diagnostics',
|
|
1282
|
-
level: 'warn',
|
|
1283
|
-
event: 'records-dropped',
|
|
1284
|
-
droppedRecords,
|
|
1285
|
-
}, droppedAt));
|
|
1286
|
-
}
|
|
1279
|
+
if (dropped)
|
|
1280
|
+
await this.append(dropped);
|
|
1287
1281
|
await this.append(record);
|
|
1288
1282
|
})
|
|
1289
1283
|
.catch(() => this.onError())
|
|
@@ -1292,22 +1286,21 @@ class JsonLineLog {
|
|
|
1292
1286
|
});
|
|
1293
1287
|
}
|
|
1294
1288
|
flush() {
|
|
1295
|
-
const
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
this.droppedAt = undefined;
|
|
1299
|
-
if (droppedRecords > 0) {
|
|
1300
|
-
this.pending = this.pending
|
|
1301
|
-
.then(() => this.append(this.serialize({
|
|
1302
|
-
scope: 'diagnostics',
|
|
1303
|
-
level: 'warn',
|
|
1304
|
-
event: 'records-dropped',
|
|
1305
|
-
droppedRecords,
|
|
1306
|
-
}, droppedAt)))
|
|
1307
|
-
.catch(() => this.onError());
|
|
1289
|
+
const dropped = this.drainDropped();
|
|
1290
|
+
if (dropped) {
|
|
1291
|
+
this.pending = this.pending.then(() => this.append(dropped)).catch(() => this.onError());
|
|
1308
1292
|
}
|
|
1309
1293
|
return this.pending;
|
|
1310
1294
|
}
|
|
1295
|
+
/** The `records-dropped` record for what was refused since the last one, resetting the tally, or nothing. */
|
|
1296
|
+
drainDropped() {
|
|
1297
|
+
const { droppedRecords, droppedAt } = this;
|
|
1298
|
+
this.droppedRecords = 0;
|
|
1299
|
+
this.droppedAt = undefined;
|
|
1300
|
+
return droppedRecords === 0
|
|
1301
|
+
? undefined
|
|
1302
|
+
: this.serialize({ scope: 'diagnostics', level: 'warn', event: 'records-dropped', droppedRecords }, droppedAt);
|
|
1303
|
+
}
|
|
1311
1304
|
serialize(payload, timestamp = new Date(this.now()).toISOString()) {
|
|
1312
1305
|
return `${JSON.stringify({ ...payload, timestamp })}\n`;
|
|
1313
1306
|
}
|
|
@@ -1366,12 +1359,12 @@ class JsonLineLog {
|
|
|
1366
1359
|
* Routes human messages to Homebridge and structured events to the plugin JSONL log.
|
|
1367
1360
|
* File writes preserve accepted order; records beyond the bounded pending queue are counted and reported.
|
|
1368
1361
|
*/
|
|
1369
|
-
export function createDiagnosticLogger(target, storageRoot,
|
|
1362
|
+
export function createDiagnosticLogger(target, storageRoot, now = Date.now) {
|
|
1370
1363
|
let fileFailureReported = false;
|
|
1371
1364
|
const reportFileFailure = () => {
|
|
1372
1365
|
if (!fileFailureReported) {
|
|
1373
1366
|
fileFailureReported = true;
|
|
1374
|
-
target.warn(`[diagnostic-log-write-failed] ${formatMessage(
|
|
1367
|
+
target.warn(`[diagnostic-log-write-failed] ${formatMessage('log.diagnosticFileFailed')}`);
|
|
1375
1368
|
}
|
|
1376
1369
|
};
|
|
1377
1370
|
const file = storageRoot ? new JsonLineLog(storageRoot, reportFileFailure, now) : undefined;
|
|
@@ -1385,7 +1378,7 @@ export function createDiagnosticLogger(target, storageRoot, catalog = EN_MESSAGE
|
|
|
1385
1378
|
if (verbose && (!session || !DIAGNOSTICS_PROFILES[session.profile].includes(evidenceClassOf(event)))) {
|
|
1386
1379
|
return;
|
|
1387
1380
|
}
|
|
1388
|
-
file.write(
|
|
1381
|
+
file.write(event);
|
|
1389
1382
|
}
|
|
1390
1383
|
: undefined;
|
|
1391
1384
|
return {
|
|
@@ -1393,13 +1386,15 @@ export function createDiagnosticLogger(target, storageRoot, catalog = EN_MESSAGE
|
|
|
1393
1386
|
error: (message) => target.error(message),
|
|
1394
1387
|
flush: () => file?.flush() ?? Promise.resolve(),
|
|
1395
1388
|
info: (message) => target.info(message),
|
|
1396
|
-
localize: (key, parameters) => formatMessage(
|
|
1389
|
+
localize: (key, parameters) => formatMessage(key, parameters),
|
|
1397
1390
|
warn: (message) => target.warn(message),
|
|
1398
1391
|
};
|
|
1399
1392
|
}
|
|
1400
1393
|
/** The class an SDK message belongs to, or nothing when its text matches none of them. */
|
|
1401
1394
|
function classifySdkEvent(message) {
|
|
1402
1395
|
const normalized = message.toLowerCase();
|
|
1396
|
+
if (normalized.includes('rtc sign for') && normalized.includes('refused'))
|
|
1397
|
+
return 'rtc-sign-refused';
|
|
1403
1398
|
if (normalized.includes('restored persisted session'))
|
|
1404
1399
|
return 'session-restored';
|
|
1405
1400
|
if (normalized.includes('retrying') || normalized.includes('reconnecting'))
|
|
@@ -1531,6 +1526,7 @@ const LIVE_TRACE_PHASES = {
|
|
|
1531
1526
|
'sequence-restart': retainedInteger('dataType', 3),
|
|
1532
1527
|
'level2-wait': retainedInteger('waitMs', MAX_STARTUP_WINDOW_MS),
|
|
1533
1528
|
'level2-ready': retainedInteger('cipherId', 65535),
|
|
1529
|
+
'level2-set-payload-only': retainedInteger('cipherId', 65535),
|
|
1534
1530
|
'session-connect-wait': retainedInteger('waitMs', MAX_STARTUP_WINDOW_MS),
|
|
1535
1531
|
'session-connected': retainedInteger('waitedMs', MAX_STARTUP_WINDOW_MS),
|
|
1536
1532
|
'session-unreachable': retainedInteger('waitedMs', MAX_STARTUP_WINDOW_MS),
|
|
@@ -1622,16 +1618,22 @@ const LIVE_TRACE_PHASES = {
|
|
|
1622
1618
|
const reason = allowlistedLabel(c.reason, ['level2-key', 'address']);
|
|
1623
1619
|
return reason ? { reason } : undefined;
|
|
1624
1620
|
},
|
|
1621
|
+
/** `errorName` is an upstream class name, so it stays out of the archive; reason and stage say enough. */
|
|
1622
|
+
'start-failed': (c) => {
|
|
1623
|
+
const reason = allowlistedLabel(c.reason, ['warm-timeout', 'source-error', 'source-ended']);
|
|
1624
|
+
const stage = allowlistedLabel(c.stage, ['awaiting-first-frame', 'audio-only', 'awaiting-keyframe']);
|
|
1625
|
+
const attempts = boundedInteger(c.attempts, 1000);
|
|
1626
|
+
return reason && stage && attempts !== undefined ? { reason, stage, attempts } : undefined;
|
|
1627
|
+
},
|
|
1625
1628
|
warming: (c) => {
|
|
1626
1629
|
const retryMs = boundedInteger(c.retryMs, MAX_STARTUP_WINDOW_MS);
|
|
1627
1630
|
const deadlineMs = boundedInteger(c.deadlineMs, MAX_STARTUP_WINDOW_MS);
|
|
1628
1631
|
return retryMs === undefined || deadlineMs === undefined ? undefined : { retryMs, deadlineMs };
|
|
1629
1632
|
},
|
|
1630
1633
|
};
|
|
1631
|
-
function sanitizeSdkLiveStartTrace(
|
|
1632
|
-
if (!
|
|
1634
|
+
function sanitizeSdkLiveStartTrace(candidate) {
|
|
1635
|
+
if (!isRecord(candidate))
|
|
1633
1636
|
return undefined;
|
|
1634
|
-
const candidate = value;
|
|
1635
1637
|
const phase = String(candidate.phase);
|
|
1636
1638
|
if (!Object.hasOwn(LIVE_TRACE_PHASES, phase))
|
|
1637
1639
|
return undefined;
|
|
@@ -1664,7 +1666,8 @@ function opaqueAccessoryAlias(value) {
|
|
|
1664
1666
|
: undefined;
|
|
1665
1667
|
}
|
|
1666
1668
|
/**
|
|
1667
|
-
* Narrows one realtime startup outcome to its state
|
|
1669
|
+
* Narrows one realtime startup outcome to its state, the push plane's start counts, and the mqtt and wired P2P
|
|
1670
|
+
* planes' counts nested under their names, or nothing where the state is unknown.
|
|
1668
1671
|
*
|
|
1669
1672
|
* Read by both halves of the path — {@link reportRealtimeReadiness} builds the record and the file sink rebuilds it.
|
|
1670
1673
|
*/
|
|
@@ -1672,20 +1675,25 @@ function sanitizeRealtimeReadiness(value) {
|
|
|
1672
1675
|
const state = allowlistedLabel(value.state, ['ready', 'partial', 'disabled', 'superseded', 'timed-out']);
|
|
1673
1676
|
if (!state)
|
|
1674
1677
|
return undefined;
|
|
1675
|
-
const counts = Object.fromEntries(['required', 'ready', 'failed', 'pending'].flatMap((key) => {
|
|
1676
|
-
const count = nonNegativeInteger(
|
|
1678
|
+
const counts = (plane) => Object.fromEntries(['required', 'ready', 'failed', 'pending'].flatMap((key) => {
|
|
1679
|
+
const count = isRecord(plane) ? nonNegativeInteger(plane[key]) : undefined;
|
|
1677
1680
|
return count === undefined ? [] : [[key, count]];
|
|
1678
1681
|
}));
|
|
1679
|
-
|
|
1682
|
+
const planes = Object.fromEntries(['mqtt', 'wiredP2p'].flatMap((plane) => {
|
|
1683
|
+
const planeCounts = counts(value[plane]);
|
|
1684
|
+
return Object.keys(planeCounts).length ? [[plane, planeCounts]] : [];
|
|
1685
|
+
}));
|
|
1686
|
+
return { event: 'realtime-readiness', state, ...counts(value), ...planes };
|
|
1680
1687
|
}
|
|
1681
1688
|
/**
|
|
1682
|
-
* Records how the SDK's realtime startup settled, with
|
|
1689
|
+
* Records how the SDK's realtime startup settled, with each plane's start counts, as `sdk-log` evidence.
|
|
1683
1690
|
*
|
|
1684
|
-
* Carries the state and the counts alone, which
|
|
1685
|
-
*
|
|
1691
|
+
* Carries the state and the counts alone, which name the plane — push, mqtt or wired P2P — that failed or never
|
|
1692
|
+
* settled behind a `partial` start.
|
|
1686
1693
|
*/
|
|
1687
1694
|
export function reportRealtimeReadiness(target, readiness) {
|
|
1688
|
-
const
|
|
1695
|
+
const { state, push, mqtt, wiredP2p } = readiness;
|
|
1696
|
+
const record = sanitizeRealtimeReadiness({ ...push, state, mqtt, wiredP2p });
|
|
1689
1697
|
if (target?.debug && record) {
|
|
1690
1698
|
target.debug(JSON.stringify({ scope: 'sdk', level: 'debug', subsystem: 'push', ...record }));
|
|
1691
1699
|
}
|
|
@@ -1856,6 +1864,7 @@ const REASONS = new Set([
|
|
|
1856
1864
|
'source-input-unstable',
|
|
1857
1865
|
'source-stopped',
|
|
1858
1866
|
'source-unavailable',
|
|
1867
|
+
'source-warm-timeout',
|
|
1859
1868
|
'station-key-unavailable',
|
|
1860
1869
|
'station-unreachable',
|
|
1861
1870
|
'stored-download-failed',
|
|
@@ -1885,7 +1894,7 @@ const HOMEKIT_EVENT_ROUTES = {
|
|
|
1885
1894
|
'camera.streaming': new Set(['camera-enabled-changed']),
|
|
1886
1895
|
'camera.controls': new Set(['camera-enabled-changed']),
|
|
1887
1896
|
};
|
|
1888
|
-
const HOMEKIT_OBSERVATIONS = new Set(['malformed', 'missing', 'valid']);
|
|
1897
|
+
const HOMEKIT_OBSERVATIONS = new Set(['malformed', 'missing', 'valid', 'ignored']);
|
|
1889
1898
|
/**
|
|
1890
1899
|
* What announced a change, for an adapter that follows more than one announcement for the same state.
|
|
1891
1900
|
*
|
|
@@ -1893,9 +1902,9 @@ const HOMEKIT_OBSERVATIONS = new Set(['malformed', 'missing', 'valid']);
|
|
|
1893
1902
|
* field is omitted where an adapter states no announcement and refused where it states one absent from here.
|
|
1894
1903
|
*/
|
|
1895
1904
|
const HOMEKIT_ANNOUNCEMENTS = new Set(['write', 'poll']);
|
|
1896
|
-
const HOMEKIT_LIVE_VIDEO_OPERATIONS =
|
|
1897
|
-
const HOMEKIT_LIVE_VIDEO_PROFILES =
|
|
1898
|
-
const HOMEKIT_LIVE_VIDEO_LEVELS =
|
|
1905
|
+
const HOMEKIT_LIVE_VIDEO_OPERATIONS = ['start', 'reconfigure'];
|
|
1906
|
+
const HOMEKIT_LIVE_VIDEO_PROFILES = ['baseline', 'main', 'high'];
|
|
1907
|
+
const HOMEKIT_LIVE_VIDEO_LEVELS = ['3.1', '3.2', '4.0'];
|
|
1899
1908
|
/**
|
|
1900
1909
|
* The inclusive bounds a controller-selected live geometry and frame rate are retained within.
|
|
1901
1910
|
*
|
|
@@ -1919,13 +1928,13 @@ const MAX_LIVE_VIDEO_MTU = 65_535;
|
|
|
1919
1928
|
/** The bit rate ceiling a controller negotiated, in kbps, bounded to the unsigned 16-bit field that carries it. */
|
|
1920
1929
|
const MAX_LIVE_VIDEO_BIT_RATE = 65_535;
|
|
1921
1930
|
/** The two address families HomeKit negotiates a destination in. */
|
|
1922
|
-
const HOMEKIT_LIVE_ADDRESS_VERSIONS =
|
|
1923
|
-
const HOMEKIT_LIVE_SESSION_STAGES =
|
|
1931
|
+
const HOMEKIT_LIVE_ADDRESS_VERSIONS = ['ipv4', 'ipv6'];
|
|
1932
|
+
const HOMEKIT_LIVE_SESSION_STAGES = [
|
|
1924
1933
|
'sdk-source-acquisition',
|
|
1925
1934
|
'first-source-keyframe',
|
|
1926
1935
|
'first-adapted-output',
|
|
1927
1936
|
'controller-rtcp',
|
|
1928
|
-
]
|
|
1937
|
+
];
|
|
1929
1938
|
/** The SDK's own stage and reason for a live warm-up that ended without a keyframe. */
|
|
1930
1939
|
const SDK_LIVE_START_STAGES = ['awaiting-first-frame', 'audio-only', 'awaiting-keyframe'];
|
|
1931
1940
|
const SDK_LIVE_START_REASONS = ['warm-timeout', 'source-error', 'source-ended'];
|
|
@@ -1935,7 +1944,16 @@ const SDK_LIVE_START_REASONS = ['warm-timeout', 'source-error', 'source-ended'];
|
|
|
1935
1944
|
* A superset of what the media domain can report, because the SDK runs FFmpeg of its own for snapshot
|
|
1936
1945
|
* decoding and WebRTC containers and forwards its output here under the same evidence class.
|
|
1937
1946
|
*/
|
|
1938
|
-
const ADAPTATION_ROLES = new Set([
|
|
1947
|
+
const ADAPTATION_ROLES = new Set([
|
|
1948
|
+
'live-video',
|
|
1949
|
+
'live-audio',
|
|
1950
|
+
'return-audio',
|
|
1951
|
+
'recording',
|
|
1952
|
+
'preview-encode',
|
|
1953
|
+
'preview-decode',
|
|
1954
|
+
'preview-raw',
|
|
1955
|
+
'sdk',
|
|
1956
|
+
]);
|
|
1939
1957
|
/** What that process did. `output` is a process reported for what it wrote rather than for how it ended. */
|
|
1940
1958
|
const ADAPTATION_EVENTS = new Set([
|
|
1941
1959
|
'started',
|
|
@@ -1946,6 +1964,16 @@ const ADAPTATION_EVENTS = new Set([
|
|
|
1946
1964
|
'exited-while-streaming',
|
|
1947
1965
|
'output',
|
|
1948
1966
|
]);
|
|
1967
|
+
/** How a settings-page live preview ended. */
|
|
1968
|
+
const PREVIEW_OUTCOMES = ['picture', 'unplayable', 'no-video', 'stopped'];
|
|
1969
|
+
/** The iPhone asks a live preview codes for, and the live video settings it may try. */
|
|
1970
|
+
const PREVIEW_ASKS = ['wifi', 'cellular'];
|
|
1971
|
+
const PREVIEW_SETTINGS = [
|
|
1972
|
+
'smallVideoPackets',
|
|
1973
|
+
'liveVideoPassthrough',
|
|
1974
|
+
'hardwareVideoEncoder',
|
|
1975
|
+
'ignoreHomeBitRate',
|
|
1976
|
+
];
|
|
1949
1977
|
/**
|
|
1950
1978
|
* Why a process was started, on the record that reports it starting.
|
|
1951
1979
|
*
|
|
@@ -1964,7 +1992,7 @@ const LIVE_VIDEO_ENCODERS = [
|
|
|
1964
1992
|
'h264_vaapi',
|
|
1965
1993
|
'h264_v4l2m2m',
|
|
1966
1994
|
];
|
|
1967
|
-
/** What qualifying a hardware encoder for one selection found
|
|
1995
|
+
/** What qualifying a hardware encoder for one selection found, but `bytes`, which is read with its two integers. */
|
|
1968
1996
|
const QUALIFICATION_OUTCOMES = [
|
|
1969
1997
|
'qualified',
|
|
1970
1998
|
'budget',
|
|
@@ -1974,7 +2002,6 @@ const QUALIFICATION_OUTCOMES = [
|
|
|
1974
2002
|
'level',
|
|
1975
2003
|
'geometry',
|
|
1976
2004
|
'frames',
|
|
1977
|
-
'bytes',
|
|
1978
2005
|
];
|
|
1979
2006
|
/** The codec a live video source delivers. */
|
|
1980
2007
|
const LIVE_VIDEO_SOURCES = ['h264', 'h265', 'av1'];
|
|
@@ -2019,74 +2046,32 @@ const ADAPTATION_SIGNALS = new Set([
|
|
|
2019
2046
|
const MAX_ADAPTATION_STDERR_LINES = 8;
|
|
2020
2047
|
const MAX_ADAPTATION_STDERR_LINE_LENGTH = 200;
|
|
2021
2048
|
/**
|
|
2022
|
-
* The
|
|
2049
|
+
* The fixed-shape operational notices, each `[level, messageKey, durationMessageKey?]`.
|
|
2023
2050
|
*
|
|
2024
2051
|
* Operational rather than verbose, so each is `plugin-log` evidence that every profile declares.
|
|
2025
2052
|
*/
|
|
2026
|
-
const MEDIA_NOTICES = {
|
|
2027
|
-
'camera-snapshot-cache-invalid': {
|
|
2028
|
-
level: 'warn',
|
|
2029
|
-
messageKey: 'log.snapshotCacheInvalid',
|
|
2030
|
-
},
|
|
2031
|
-
};
|
|
2032
2053
|
const RUNTIME_NOTICES = {
|
|
2033
|
-
'status-publication-failed':
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
'
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
'
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
'
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
'ownership-acquisition-failed': {
|
|
2055
|
-
level: 'error',
|
|
2056
|
-
messageKey: 'log.notice.ownershipAcquisitionFailed',
|
|
2057
|
-
},
|
|
2058
|
-
'ownership-release-incomplete': {
|
|
2059
|
-
level: 'warn',
|
|
2060
|
-
messageKey: 'log.notice.ownershipReleaseIncomplete',
|
|
2061
|
-
},
|
|
2062
|
-
'registry-subscriber-failed': {
|
|
2063
|
-
level: 'warn',
|
|
2064
|
-
messageKey: 'log.notice.registrySubscriberFailed',
|
|
2065
|
-
},
|
|
2066
|
-
'event-subscriber-failed': {
|
|
2067
|
-
level: 'warn',
|
|
2068
|
-
messageKey: 'log.notice.eventSubscriberFailed',
|
|
2069
|
-
},
|
|
2070
|
-
'state-subscriber-failed': {
|
|
2071
|
-
level: 'warn',
|
|
2072
|
-
messageKey: 'log.notice.stateSubscriberFailed',
|
|
2073
|
-
},
|
|
2074
|
-
'channel-serve-failed': {
|
|
2075
|
-
level: 'warn',
|
|
2076
|
-
messageKey: 'log.notice.channelServeFailed',
|
|
2077
|
-
},
|
|
2078
|
-
'diagnostics-authorization-armed': {
|
|
2079
|
-
level: 'info',
|
|
2080
|
-
messageKey: 'log.notice.diagnosticsAuthorizationArmed',
|
|
2081
|
-
},
|
|
2082
|
-
'stand-down-not-rearmed': {
|
|
2083
|
-
level: 'warn',
|
|
2084
|
-
messageKey: 'log.notice.standDownNotRearmed',
|
|
2085
|
-
},
|
|
2086
|
-
'stand-down-account-replaced': {
|
|
2087
|
-
level: 'warn',
|
|
2088
|
-
messageKey: 'log.notice.standDownAccountReplaced',
|
|
2089
|
-
},
|
|
2054
|
+
'status-publication-failed': ['warn', 'log.notice.statusPublicationFailed'],
|
|
2055
|
+
'ownership-release-not-finalized': ['error', 'log.notice.ownershipReleaseNotFinalized'],
|
|
2056
|
+
'ownership-release-failed': ['error', 'log.notice.ownershipReleaseFailed'],
|
|
2057
|
+
'shutdown-failed': ['error', 'log.notice.shutdownFailed'],
|
|
2058
|
+
'shutdown-timeout': ['warn', 'log.notice.shutdownTimeout', 'log.notice.shutdownTimeoutWithDuration'],
|
|
2059
|
+
'ownership-acquisition-failed': ['error', 'log.notice.ownershipAcquisitionFailed'],
|
|
2060
|
+
'ownership-release-incomplete': ['warn', 'log.notice.ownershipReleaseIncomplete'],
|
|
2061
|
+
'registry-subscriber-failed': ['warn', 'log.notice.registrySubscriberFailed'],
|
|
2062
|
+
'event-subscriber-failed': ['warn', 'log.notice.eventSubscriberFailed'],
|
|
2063
|
+
'state-subscriber-failed': ['warn', 'log.notice.stateSubscriberFailed'],
|
|
2064
|
+
'channel-serve-failed': ['warn', 'log.notice.channelServeFailed'],
|
|
2065
|
+
'diagnostics-authorization-armed': ['info', 'log.notice.diagnosticsAuthorizationArmed'],
|
|
2066
|
+
'stand-down-not-rearmed': ['warn', 'log.notice.standDownNotRearmed'],
|
|
2067
|
+
'stand-down-account-replaced': ['warn', 'log.notice.standDownAccountReplaced'],
|
|
2068
|
+
'camera-snapshot-cache-invalid': ['warn', 'log.snapshotCacheInvalid'],
|
|
2069
|
+
'discarded-v4-settings-unacknowledged': ['warn', 'log.discardedSettings'],
|
|
2070
|
+
};
|
|
2071
|
+
/** The scope a notice is recorded under where it is not `runtime-notice`. */
|
|
2072
|
+
const NOTICE_SCOPES = {
|
|
2073
|
+
'camera-snapshot-cache-invalid': 'media-notice',
|
|
2074
|
+
'discarded-v4-settings-unacknowledged': 'configuration-notice',
|
|
2090
2075
|
};
|
|
2091
2076
|
const RUNTIME_CONDITIONS = {
|
|
2092
2077
|
degraded: {
|
|
@@ -2181,12 +2166,13 @@ const HOMEKIT_REASON_ACTIONS = {
|
|
|
2181
2166
|
'smart-light-operation-failed:timeout': 'log.action.controlTimedOut',
|
|
2182
2167
|
'camera-control-operation-failed:timeout': 'log.action.controlTimedOut',
|
|
2183
2168
|
'camera-live-session-failed:station-unreachable': 'log.action.checkStation',
|
|
2169
|
+
'camera-live-session-failed:source-warm-timeout': 'log.action.retryLiveViewNoPicture',
|
|
2184
2170
|
'camera-snapshot-unavailable:live-station-unreachable': 'log.action.checkStation',
|
|
2185
2171
|
'camera-talkback-failed:no-controller-audio': 'log.action.trySmallVideoPackets',
|
|
2186
2172
|
};
|
|
2187
2173
|
/** Projects a value onto an active condition, or nothing where the catalog does not write that code with those keys. */
|
|
2188
2174
|
export function knownCondition(value) {
|
|
2189
|
-
if (
|
|
2175
|
+
if (!isRecord(value))
|
|
2190
2176
|
return undefined;
|
|
2191
2177
|
const { code, summaryKey, actionKey, serials } = value;
|
|
2192
2178
|
if (typeof code !== 'string')
|
|
@@ -2223,14 +2209,13 @@ function homeKitConditionPresentation(code, active, reason) {
|
|
|
2223
2209
|
function sanitizeStructuredEvent(message) {
|
|
2224
2210
|
let value;
|
|
2225
2211
|
try {
|
|
2226
|
-
|
|
2227
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
2228
|
-
return undefined;
|
|
2229
|
-
value = parsed;
|
|
2212
|
+
value = JSON.parse(message);
|
|
2230
2213
|
}
|
|
2231
2214
|
catch {
|
|
2232
2215
|
return undefined;
|
|
2233
2216
|
}
|
|
2217
|
+
if (!isRecord(value))
|
|
2218
|
+
return undefined;
|
|
2234
2219
|
const level = allowlistedLabel(value.level, ['debug', 'info', 'warn', 'error']);
|
|
2235
2220
|
if (!level || typeof value.scope !== 'string')
|
|
2236
2221
|
return undefined;
|
|
@@ -2253,18 +2238,17 @@ function sanitizeStructuredEvent(message) {
|
|
|
2253
2238
|
}
|
|
2254
2239
|
const details = [];
|
|
2255
2240
|
for (const detail of Array.isArray(value.details) ? value.details.slice(0, MAX_SDK_DETAILS) : []) {
|
|
2256
|
-
if (!
|
|
2241
|
+
if (!isRecord(detail))
|
|
2257
2242
|
continue;
|
|
2258
|
-
const
|
|
2259
|
-
const errorType = allowlistedLabel(candidate.errorType, SDK_ERROR_TYPES);
|
|
2243
|
+
const errorType = allowlistedLabel(detail.errorType, SDK_ERROR_TYPES);
|
|
2260
2244
|
if (errorType) {
|
|
2261
2245
|
details.push({ errorType });
|
|
2262
2246
|
continue;
|
|
2263
2247
|
}
|
|
2264
|
-
const type = allowlistedLabel(
|
|
2248
|
+
const type = allowlistedLabel(detail.type, ['boolean', 'number', 'object', 'string', 'undefined']);
|
|
2265
2249
|
if (!type)
|
|
2266
2250
|
continue;
|
|
2267
|
-
const length = nonNegativeInteger(
|
|
2251
|
+
const length = nonNegativeInteger(detail.length);
|
|
2268
2252
|
details.push({ type, ...(length === undefined ? {} : { length }) });
|
|
2269
2253
|
}
|
|
2270
2254
|
return {
|
|
@@ -2276,72 +2260,14 @@ function sanitizeStructuredEvent(message) {
|
|
|
2276
2260
|
...(value.detailsTruncated === true ? { detailsTruncated: true } : {}),
|
|
2277
2261
|
};
|
|
2278
2262
|
}
|
|
2279
|
-
if (value.scope === 'runtime-notice') {
|
|
2263
|
+
if (value.scope === 'runtime-notice' || value.scope === 'media-notice' || value.scope === 'configuration-notice') {
|
|
2280
2264
|
if (typeof value.code !== 'string' || !Object.hasOwn(RUNTIME_NOTICES, value.code))
|
|
2281
2265
|
return undefined;
|
|
2282
|
-
const notice =
|
|
2283
|
-
|
|
2284
|
-
const messageKey = durationMs !== undefined && 'durationMessageKey' in notice ? notice.durationMessageKey : notice.messageKey;
|
|
2285
|
-
return {
|
|
2286
|
-
scope: 'runtime-notice',
|
|
2287
|
-
level: notice.level,
|
|
2288
|
-
code: value.code,
|
|
2289
|
-
messageKey,
|
|
2290
|
-
...(durationMs === undefined ? {} : { durationMs }),
|
|
2291
|
-
};
|
|
2292
|
-
}
|
|
2293
|
-
if (value.scope === 'configuration-notice') {
|
|
2294
|
-
if (value.code !== 'discarded-v4-settings-unacknowledged')
|
|
2295
|
-
return undefined;
|
|
2296
|
-
return {
|
|
2297
|
-
scope: 'configuration-notice',
|
|
2298
|
-
level: 'warn',
|
|
2299
|
-
code: value.code,
|
|
2300
|
-
messageKey: 'log.discardedSettings',
|
|
2301
|
-
};
|
|
2302
|
-
}
|
|
2303
|
-
if (value.scope === 'media-notice') {
|
|
2304
|
-
if (typeof value.code !== 'string' || !Object.hasOwn(MEDIA_NOTICES, value.code))
|
|
2305
|
-
return undefined;
|
|
2306
|
-
const notice = MEDIA_NOTICES[value.code];
|
|
2307
|
-
return { scope: 'media-notice', level: notice.level, code: value.code, messageKey: notice.messageKey };
|
|
2266
|
+
const notice = runtimeNoticeRecord(value.code, nonNegativeInteger(value.durationMs));
|
|
2267
|
+
return notice.scope === value.scope ? notice : undefined;
|
|
2308
2268
|
}
|
|
2309
2269
|
if (value.scope === 'homekit') {
|
|
2310
|
-
|
|
2311
|
-
const unrouted = sanitizeUnroutedEvent(value);
|
|
2312
|
-
return unrouted ? { scope: 'homekit', level: 'debug', ...unrouted } : undefined;
|
|
2313
|
-
}
|
|
2314
|
-
if (value.event === 'station-claim') {
|
|
2315
|
-
const claim = sanitizeStationClaim(value);
|
|
2316
|
-
return claim ? { scope: 'homekit', level: 'debug', ...claim } : undefined;
|
|
2317
|
-
}
|
|
2318
|
-
if (value.adapter === 'camera.streaming' && value.event === 'live-video-selected') {
|
|
2319
|
-
const selection = sanitizeLiveVideoSelection(value);
|
|
2320
|
-
return selection ? { scope: 'homekit', level: 'debug', ...selection } : undefined;
|
|
2321
|
-
}
|
|
2322
|
-
if (value.adapter === 'camera.streaming' && HOMEKIT_LIVE_REQUEST_EVENTS.has(String(value.event))) {
|
|
2323
|
-
const lifecycle = sanitizeLiveSessionTrace(value);
|
|
2324
|
-
return lifecycle ? { scope: 'homekit', level: 'debug', ...lifecycle } : undefined;
|
|
2325
|
-
}
|
|
2326
|
-
if (typeof value.adapter !== 'string' ||
|
|
2327
|
-
typeof value.event !== 'string' ||
|
|
2328
|
-
!HOMEKIT_EVENT_ROUTES[value.adapter]?.has(value.event) ||
|
|
2329
|
-
typeof value.observation !== 'string' ||
|
|
2330
|
-
!HOMEKIT_OBSERVATIONS.has(value.observation)) {
|
|
2331
|
-
return undefined;
|
|
2332
|
-
}
|
|
2333
|
-
const alias = opaqueAccessoryAlias(value.accessory);
|
|
2334
|
-
return {
|
|
2335
|
-
scope: 'homekit',
|
|
2336
|
-
level: 'debug',
|
|
2337
|
-
adapter: value.adapter,
|
|
2338
|
-
event: value.event,
|
|
2339
|
-
observation: value.observation,
|
|
2340
|
-
...(typeof value.announcedBy === 'string' && HOMEKIT_ANNOUNCEMENTS.has(value.announcedBy)
|
|
2341
|
-
? { announcedBy: value.announcedBy }
|
|
2342
|
-
: {}),
|
|
2343
|
-
...(alias === undefined ? {} : { accessory: alias }),
|
|
2344
|
-
};
|
|
2270
|
+
return sanitizeHomeKitEvent(value);
|
|
2345
2271
|
}
|
|
2346
2272
|
if (value.scope === 'runtime') {
|
|
2347
2273
|
const event = allowlistedLabel(value.event, ['ready', 'stopped']);
|
|
@@ -2352,6 +2278,10 @@ function sanitizeStructuredEvent(message) {
|
|
|
2352
2278
|
if (value.scope === 'ffmpeg') {
|
|
2353
2279
|
return sanitizeAdaptationNotice(value, level);
|
|
2354
2280
|
}
|
|
2281
|
+
if (value.scope === 'live-preview') {
|
|
2282
|
+
const preview = sanitizeLivePreview(value);
|
|
2283
|
+
return preview ? { scope: 'live-preview', level: 'info', ...preview } : undefined;
|
|
2284
|
+
}
|
|
2355
2285
|
if (value.scope === 'diagnostic-condition') {
|
|
2356
2286
|
const homeKitDefinition = typeof value.code === 'string' && Object.hasOwn(HOMEKIT_CONDITIONS, value.code)
|
|
2357
2287
|
? HOMEKIT_CONDITIONS[value.code]
|
|
@@ -2386,7 +2316,7 @@ function sanitizeStructuredEvent(message) {
|
|
|
2386
2316
|
const affectedAccessoryCount = nonNegativeInteger(value.affectedAccessoryCount);
|
|
2387
2317
|
const accessoryAliases = Array.isArray(value.accessoryAliases)
|
|
2388
2318
|
? value.accessoryAliases
|
|
2389
|
-
.filter((alias) =>
|
|
2319
|
+
.filter((alias) => opaqueAccessoryAlias(alias) !== undefined)
|
|
2390
2320
|
.slice(0, MAX_ACCESSORY_ALIASES)
|
|
2391
2321
|
: [];
|
|
2392
2322
|
const { level: conditionLevel, actionKey } = homeKitConditionPresentation(value.code, value.active, value.reason);
|
|
@@ -2409,19 +2339,22 @@ function sanitizeStructuredEvent(message) {
|
|
|
2409
2339
|
}
|
|
2410
2340
|
return undefined;
|
|
2411
2341
|
}
|
|
2412
|
-
/**
|
|
2413
|
-
|
|
2414
|
-
const
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
target.debug?.(JSON.stringify({
|
|
2419
|
-
scope: 'runtime-notice',
|
|
2420
|
-
level: notice.level,
|
|
2342
|
+
/** The record one notice is written as, read by both halves of the path. */
|
|
2343
|
+
function runtimeNoticeRecord(code, durationMs) {
|
|
2344
|
+
const [level, messageKey, durationMessageKey] = RUNTIME_NOTICES[code];
|
|
2345
|
+
return {
|
|
2346
|
+
scope: NOTICE_SCOPES[code] ?? 'runtime-notice',
|
|
2347
|
+
level,
|
|
2421
2348
|
code,
|
|
2422
|
-
messageKey,
|
|
2349
|
+
messageKey: durationMs !== undefined && durationMessageKey ? durationMessageKey : messageKey,
|
|
2423
2350
|
...(durationMs === undefined ? {} : { durationMs }),
|
|
2424
|
-
}
|
|
2351
|
+
};
|
|
2352
|
+
}
|
|
2353
|
+
/** Emits one fixed-shape operational notice selected by its allowlisted code. */
|
|
2354
|
+
export function reportRuntimeNotice(target, code, fields = {}) {
|
|
2355
|
+
const record = runtimeNoticeRecord(code, fields.durationMs === undefined ? undefined : Math.max(0, Math.trunc(fields.durationMs)));
|
|
2356
|
+
target[record.level]?.(`[${code}] ${localize(target, record.messageKey, { durationMs: record.durationMs ?? 0, finishBy: fields.finishBy ?? '' })}`);
|
|
2357
|
+
target.debug?.(JSON.stringify(record));
|
|
2425
2358
|
}
|
|
2426
2359
|
/**
|
|
2427
2360
|
* Picks up an authorization the persisted session file confirms, reporting whether it did.
|
|
@@ -2442,18 +2375,6 @@ export function armDiagnosticsAuthorization(target, storageRoot, supportCaseId,
|
|
|
2442
2375
|
});
|
|
2443
2376
|
return true;
|
|
2444
2377
|
}
|
|
2445
|
-
/** Emits the startup notice for discarded settings awaiting acknowledgement. */
|
|
2446
|
-
export function reportDiscardedV4Settings(target) {
|
|
2447
|
-
const code = 'discarded-v4-settings-unacknowledged';
|
|
2448
|
-
const messageKey = 'log.discardedSettings';
|
|
2449
|
-
target.warn(`[${code}] ${localize(target, messageKey)}`);
|
|
2450
|
-
target.debug?.(JSON.stringify({
|
|
2451
|
-
scope: 'configuration-notice',
|
|
2452
|
-
level: 'warn',
|
|
2453
|
-
code,
|
|
2454
|
-
messageKey,
|
|
2455
|
-
}));
|
|
2456
|
-
}
|
|
2457
2378
|
/** States once which encoders code live video on this host: every qualified hardware encoder, then `libx264`. */
|
|
2458
2379
|
export function reportLiveVideoEncoders(target, encoders) {
|
|
2459
2380
|
const names = [
|
|
@@ -2462,19 +2383,6 @@ export function reportLiveVideoEncoders(target, encoders) {
|
|
|
2462
2383
|
];
|
|
2463
2384
|
target.info(`[live-video-encoders] ${localize(target, 'log.liveVideoEncoders', { encoders: names.join(', ') })}`);
|
|
2464
2385
|
}
|
|
2465
|
-
/** Emits one identity-free warning after an invalid retained camera image is discarded. */
|
|
2466
|
-
export function reportInvalidSnapshotCache(target) {
|
|
2467
|
-
const code = 'camera-snapshot-cache-invalid';
|
|
2468
|
-
const { level, messageKey } = MEDIA_NOTICES[code];
|
|
2469
|
-
target[level](`[${code}] ${localize(target, messageKey)}`);
|
|
2470
|
-
target.debug?.(JSON.stringify({ scope: 'media-notice', level, code, messageKey }));
|
|
2471
|
-
}
|
|
2472
|
-
/**
|
|
2473
|
-
* Emits one allowlisted HomeKit event trace only when host debug output is available.
|
|
2474
|
-
*
|
|
2475
|
-
* `aliasFor` resolves the trace's device identity to the support-case alias the conditions about that accessory
|
|
2476
|
-
* already carry. The identity itself never reaches the record: an unresolved one is recorded as no accessory.
|
|
2477
|
-
*/
|
|
2478
2386
|
/**
|
|
2479
2387
|
* Narrows one offered station arbitration decision, or nothing where its own labels do not narrow.
|
|
2480
2388
|
*
|
|
@@ -2515,49 +2423,63 @@ export function reportStationClaim(target, decision) {
|
|
|
2515
2423
|
target.debug(JSON.stringify({ scope: 'homekit', level: 'debug', ...claim }));
|
|
2516
2424
|
}
|
|
2517
2425
|
}
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
return;
|
|
2535
|
-
}
|
|
2536
|
-
if (HOMEKIT_LIVE_REQUEST_EVENTS.has(
|
|
2537
|
-
const lifecycle = sanitizeLiveSessionTrace(
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
!(
|
|
2545
|
-
|
|
2546
|
-
typeof trace.observation !== 'string' ||
|
|
2547
|
-
!HOMEKIT_OBSERVATIONS.has(trace.observation)) {
|
|
2548
|
-
return;
|
|
2426
|
+
/**
|
|
2427
|
+
* Narrows one offered HomeKit adapter fact to its `homekit` record, or nothing where its route does not narrow.
|
|
2428
|
+
*
|
|
2429
|
+
* Read by both halves of the path: {@link reportHomeKitEvent} builds the record and the file sink rebuilds it.
|
|
2430
|
+
*/
|
|
2431
|
+
function sanitizeHomeKitEvent(value) {
|
|
2432
|
+
if (value.event === 'event-unrouted') {
|
|
2433
|
+
const unrouted = sanitizeUnroutedEvent(value);
|
|
2434
|
+
return unrouted ? { scope: 'homekit', level: 'debug', ...unrouted } : undefined;
|
|
2435
|
+
}
|
|
2436
|
+
if (value.event === 'station-claim') {
|
|
2437
|
+
const claim = sanitizeStationClaim(value);
|
|
2438
|
+
return claim ? { scope: 'homekit', level: 'debug', ...claim } : undefined;
|
|
2439
|
+
}
|
|
2440
|
+
if (value.adapter === 'camera.streaming' && value.event === 'live-video-selected') {
|
|
2441
|
+
const selection = sanitizeLiveVideoSelection(value);
|
|
2442
|
+
return selection ? { scope: 'homekit', level: 'debug', ...selection } : undefined;
|
|
2443
|
+
}
|
|
2444
|
+
if (value.adapter === 'camera.streaming' && HOMEKIT_LIVE_REQUEST_EVENTS.has(String(value.event))) {
|
|
2445
|
+
const lifecycle = sanitizeLiveSessionTrace(value);
|
|
2446
|
+
return lifecycle ? { scope: 'homekit', level: 'debug', ...lifecycle } : undefined;
|
|
2447
|
+
}
|
|
2448
|
+
if (typeof value.adapter !== 'string' ||
|
|
2449
|
+
typeof value.event !== 'string' ||
|
|
2450
|
+
!HOMEKIT_EVENT_ROUTES[value.adapter]?.has(value.event) ||
|
|
2451
|
+
typeof value.observation !== 'string' ||
|
|
2452
|
+
!HOMEKIT_OBSERVATIONS.has(value.observation)) {
|
|
2453
|
+
return undefined;
|
|
2549
2454
|
}
|
|
2550
|
-
|
|
2455
|
+
const alias = opaqueAccessoryAlias(value.accessory);
|
|
2456
|
+
return {
|
|
2551
2457
|
scope: 'homekit',
|
|
2552
2458
|
level: 'debug',
|
|
2553
|
-
adapter:
|
|
2554
|
-
event:
|
|
2555
|
-
observation:
|
|
2556
|
-
...(typeof
|
|
2557
|
-
? { announcedBy:
|
|
2459
|
+
adapter: value.adapter,
|
|
2460
|
+
event: value.event,
|
|
2461
|
+
observation: value.observation,
|
|
2462
|
+
...(typeof value.announcedBy === 'string' && HOMEKIT_ANNOUNCEMENTS.has(value.announcedBy)
|
|
2463
|
+
? { announcedBy: value.announcedBy }
|
|
2558
2464
|
: {}),
|
|
2559
|
-
...
|
|
2560
|
-
|
|
2465
|
+
...(typeof value.code === 'number' && Number.isInteger(value.code) && value.code >= 0 && value.code <= 0xffff
|
|
2466
|
+
? { code: value.code }
|
|
2467
|
+
: {}),
|
|
2468
|
+
...(alias === undefined ? {} : { accessory: alias }),
|
|
2469
|
+
};
|
|
2470
|
+
}
|
|
2471
|
+
/**
|
|
2472
|
+
* Emits one allowlisted HomeKit event trace only when host debug output is available.
|
|
2473
|
+
*
|
|
2474
|
+
* `aliasFor` resolves the trace's device identity to the support-case alias the conditions about that accessory
|
|
2475
|
+
* already carry. The identity itself never reaches the record: an unresolved one is recorded as no accessory.
|
|
2476
|
+
*/
|
|
2477
|
+
export function reportHomeKitEvent(target, trace, aliasFor) {
|
|
2478
|
+
const alias = 'serial' in trace && typeof trace.serial === 'string' ? aliasFor?.(trace.serial) : undefined;
|
|
2479
|
+
const record = sanitizeHomeKitEvent({ ...trace, accessory: alias });
|
|
2480
|
+
if (target.debug && record) {
|
|
2481
|
+
target.debug(JSON.stringify(record));
|
|
2482
|
+
}
|
|
2561
2483
|
}
|
|
2562
2484
|
/**
|
|
2563
2485
|
* Records one FFmpeg adaptation fact as the `ffmpeg-log` evidence the media support profiles declare.
|
|
@@ -2577,19 +2499,8 @@ export function reportAdaptationNotice(target, trace) {
|
|
|
2577
2499
|
}
|
|
2578
2500
|
}
|
|
2579
2501
|
/**
|
|
2580
|
-
* Reduces one FFmpeg stderr line to what a support case may keep, or nothing where nothing is left of it
|
|
2581
|
-
*
|
|
2582
|
-
* The line is the only place an encoder-level cause is stated outright, and it is also the only place this
|
|
2583
|
-
* plugin's own argument list can be echoed back: the output URL carries base64 SRTP key material, the
|
|
2584
|
-
* controller address sits beside it, and an SDK snapshot filename carries a device serial. Each of those is
|
|
2585
|
-
* replaced by what it is rather than searched for afterwards, because a redaction that depends on
|
|
2586
|
-
* recognising a secret fails on the first message shape nobody predicted.
|
|
2587
|
-
*
|
|
2588
|
-
* The key material is replaced before the path rules run, and not after. Base64 includes `/`, so a path rule
|
|
2589
|
-
* applied first splits a key into sub-runs too short for any length threshold to catch — measured on random
|
|
2590
|
-
* 30-byte keys, roughly one line in twenty then kept an eight-character fragment verbatim. The cost of this
|
|
2591
|
-
* order is that a long path made only of letters and separators is labelled as redacted rather than as a
|
|
2592
|
-
* path, which loses a label and never a secret.
|
|
2502
|
+
* Reduces one FFmpeg stderr line to what a support case may keep, or nothing where nothing is left of it: a
|
|
2503
|
+
* bounded printable line, never a `progress=` one, through {@link redactSensitiveText}.
|
|
2593
2504
|
*/
|
|
2594
2505
|
function redactAdaptationStderr(line) {
|
|
2595
2506
|
const printable = boundedText(line, MAX_ADAPTATION_STDERR_LINE_LENGTH);
|
|
@@ -2663,6 +2574,8 @@ function sanitizeAdaptationNotice(value, level) {
|
|
|
2663
2574
|
.filter((line) => line !== undefined)
|
|
2664
2575
|
.slice(-MAX_ADAPTATION_STDERR_LINES);
|
|
2665
2576
|
const sourceFragments = nonNegativeInteger(value.sourceFragments);
|
|
2577
|
+
const sourceWidth = nonNegativeInteger(value.sourceWidth);
|
|
2578
|
+
const sourceHeight = nonNegativeInteger(value.sourceHeight);
|
|
2666
2579
|
const cause = allowlistedLabel(value.cause, ADAPTATION_CAUSES);
|
|
2667
2580
|
const encoder = allowlistedLabel(value.encoder, LIVE_VIDEO_ENCODERS);
|
|
2668
2581
|
const source = allowlistedLabel(value.source, LIVE_VIDEO_SOURCES);
|
|
@@ -2674,12 +2587,64 @@ function sanitizeAdaptationNotice(value, level) {
|
|
|
2674
2587
|
...(cause === undefined ? {} : { cause }),
|
|
2675
2588
|
...(encoder === undefined ? {} : { encoder }),
|
|
2676
2589
|
...(source === undefined ? {} : { source }),
|
|
2590
|
+
...(sourceWidth === undefined || sourceHeight === undefined ? {} : { sourceWidth, sourceHeight }),
|
|
2677
2591
|
...(code === undefined ? {} : { code }),
|
|
2678
2592
|
...(signal === undefined ? {} : { signal }),
|
|
2679
2593
|
...(sourceFragments === undefined ? {} : { sourceFragments }),
|
|
2680
2594
|
...(stderr.length ? { stderr } : {}),
|
|
2681
2595
|
};
|
|
2682
2596
|
}
|
|
2597
|
+
/**
|
|
2598
|
+
* Records how one settings-page live preview ended, as a `live-preview` record kept whether or not a support
|
|
2599
|
+
* window is open: it is not one of the {@link VERBOSE_SCOPES}, because it holds only allowlisted labels and
|
|
2600
|
+
* bounded numbers, one per run a user started.
|
|
2601
|
+
*/
|
|
2602
|
+
export function reportLivePreview(target, trace) {
|
|
2603
|
+
const { level, ...selection } = trace.selection;
|
|
2604
|
+
const preview = sanitizeLivePreview({ ...trace, ...selection, levelName: level });
|
|
2605
|
+
if (preview)
|
|
2606
|
+
target.debug?.(JSON.stringify({ scope: 'live-preview', level: 'info', ...preview }));
|
|
2607
|
+
}
|
|
2608
|
+
/**
|
|
2609
|
+
* Narrows one offered live preview run, or nothing where its selection or outcome does not narrow.
|
|
2610
|
+
*
|
|
2611
|
+
* Read by both halves of the path, {@link reportLivePreview} and the file sink, so a record only one of them
|
|
2612
|
+
* recognised could not reach a support archive.
|
|
2613
|
+
*/
|
|
2614
|
+
function sanitizeLivePreview(value) {
|
|
2615
|
+
const selection = sanitizePreviewSelection(value);
|
|
2616
|
+
const outcome = allowlistedLabel(value.outcome, PREVIEW_OUTCOMES);
|
|
2617
|
+
if (!selection || !outcome)
|
|
2618
|
+
return undefined;
|
|
2619
|
+
const encoder = allowlistedLabel(value.encoder, LIVE_VIDEO_ENCODERS);
|
|
2620
|
+
const source = allowlistedLabel(value.source, LIVE_VIDEO_SOURCES);
|
|
2621
|
+
const kbps = boundedInteger(value.kbps, MAX_LIVE_VIDEO_BIT_RATE);
|
|
2622
|
+
const accessory = opaqueAccessoryAlias(value.accessory);
|
|
2623
|
+
const ask = allowlistedLabel(value.ask, PREVIEW_ASKS);
|
|
2624
|
+
const settings = PREVIEW_SETTINGS.filter((setting) => typeof value[setting] === 'boolean');
|
|
2625
|
+
return {
|
|
2626
|
+
...selection,
|
|
2627
|
+
...(encoder === undefined ? {} : { encoder }),
|
|
2628
|
+
...(source === undefined ? {} : { source }),
|
|
2629
|
+
...(kbps === undefined ? {} : { kbps }),
|
|
2630
|
+
...(ask === undefined ? {} : { ask }),
|
|
2631
|
+
...Object.fromEntries(settings.map((setting) => [setting, value[setting]])),
|
|
2632
|
+
outcome,
|
|
2633
|
+
...(accessory === undefined ? {} : { accessory }),
|
|
2634
|
+
};
|
|
2635
|
+
}
|
|
2636
|
+
/** A live preview's selection as flat bounded fields, or nothing where any of them falls outside its allowlist. */
|
|
2637
|
+
function sanitizePreviewSelection(candidate) {
|
|
2638
|
+
const fields = {
|
|
2639
|
+
profile: allowlistedLabel(candidate.profile, HOMEKIT_LIVE_VIDEO_PROFILES),
|
|
2640
|
+
levelName: allowlistedLabel(candidate.levelName, HOMEKIT_LIVE_VIDEO_LEVELS),
|
|
2641
|
+
width: boundedInteger(candidate.width, MAX_LIVE_VIDEO_DIMENSION, MIN_LIVE_VIDEO_DIMENSION),
|
|
2642
|
+
height: boundedInteger(candidate.height, MAX_LIVE_VIDEO_DIMENSION, MIN_LIVE_VIDEO_DIMENSION),
|
|
2643
|
+
fps: boundedInteger(candidate.fps, MAX_LIVE_VIDEO_FRAME_RATE, MIN_LIVE_VIDEO_FRAME_RATE),
|
|
2644
|
+
maxBitRate: boundedInteger(candidate.maxBitRate, MAX_LIVE_VIDEO_BIT_RATE),
|
|
2645
|
+
};
|
|
2646
|
+
return Object.values(fields).includes(undefined) ? undefined : fields;
|
|
2647
|
+
}
|
|
2683
2648
|
function sanitizeLiveSessionTrace(value) {
|
|
2684
2649
|
if (value.adapter !== 'camera.streaming') {
|
|
2685
2650
|
return undefined;
|
|
@@ -2720,8 +2685,7 @@ function sanitizeLiveSessionTrace(value) {
|
|
|
2720
2685
|
value.outcome !== 'failed' ||
|
|
2721
2686
|
typeof value.reason !== 'string' ||
|
|
2722
2687
|
!REASONS.has(value.reason) ||
|
|
2723
|
-
|
|
2724
|
-
!HOMEKIT_LIVE_SESSION_STAGES.has(value.stage)) {
|
|
2688
|
+
!allowlistedLabel(value.stage, HOMEKIT_LIVE_SESSION_STAGES)) {
|
|
2725
2689
|
return undefined;
|
|
2726
2690
|
}
|
|
2727
2691
|
const sourceStage = allowlistedLabel(value.sourceStage, SDK_LIVE_START_STAGES);
|
|
@@ -2738,27 +2702,23 @@ function sanitizeLiveSessionTrace(value) {
|
|
|
2738
2702
|
};
|
|
2739
2703
|
}
|
|
2740
2704
|
function sanitizeLiveVideoSelection(value) {
|
|
2741
|
-
const operation =
|
|
2742
|
-
const profile =
|
|
2743
|
-
const codecLevel = typeof value.levelName === 'string' ? value.levelName :
|
|
2705
|
+
const operation = allowlistedLabel(value.operation, HOMEKIT_LIVE_VIDEO_OPERATIONS);
|
|
2706
|
+
const profile = allowlistedLabel(value.profile, HOMEKIT_LIVE_VIDEO_PROFILES);
|
|
2707
|
+
const codecLevel = allowlistedLabel(typeof value.levelName === 'string' ? value.levelName : value.level, HOMEKIT_LIVE_VIDEO_LEVELS);
|
|
2744
2708
|
const width = boundedInteger(value.width, MAX_LIVE_VIDEO_DIMENSION, MIN_LIVE_VIDEO_DIMENSION);
|
|
2745
2709
|
const height = boundedInteger(value.height, MAX_LIVE_VIDEO_DIMENSION, MIN_LIVE_VIDEO_DIMENSION);
|
|
2746
2710
|
const fps = boundedInteger(value.fps, MAX_LIVE_VIDEO_FRAME_RATE, MIN_LIVE_VIDEO_FRAME_RATE);
|
|
2747
2711
|
const mtu = boundedInteger(value.mtu, MAX_LIVE_VIDEO_MTU, MIN_LIVE_VIDEO_MTU);
|
|
2748
2712
|
const maxBitRate = boundedInteger(value.maxBitRate, MAX_LIVE_VIDEO_BIT_RATE, 0);
|
|
2749
|
-
const addressVersion =
|
|
2713
|
+
const addressVersion = allowlistedLabel(value.addressVersion, HOMEKIT_LIVE_ADDRESS_VERSIONS);
|
|
2750
2714
|
if (!operation ||
|
|
2751
|
-
!HOMEKIT_LIVE_VIDEO_OPERATIONS.has(operation) ||
|
|
2752
2715
|
!profile ||
|
|
2753
|
-
!HOMEKIT_LIVE_VIDEO_PROFILES.has(profile) ||
|
|
2754
2716
|
!codecLevel ||
|
|
2755
|
-
!HOMEKIT_LIVE_VIDEO_LEVELS.has(codecLevel) ||
|
|
2756
2717
|
width === undefined ||
|
|
2757
2718
|
height === undefined ||
|
|
2758
2719
|
fps === undefined ||
|
|
2759
2720
|
mtu === undefined ||
|
|
2760
|
-
!addressVersion
|
|
2761
|
-
!HOMEKIT_LIVE_ADDRESS_VERSIONS.has(addressVersion)) {
|
|
2721
|
+
!addressVersion) {
|
|
2762
2722
|
return undefined;
|
|
2763
2723
|
}
|
|
2764
2724
|
const alias = opaqueAccessoryAlias(value.accessory);
|