@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
package/dist/diagnostics.js
CHANGED
|
@@ -417,6 +417,10 @@ function boundedText(value, maximumLength) {
|
|
|
417
417
|
.trim();
|
|
418
418
|
return printable === '' ? undefined : printable;
|
|
419
419
|
}
|
|
420
|
+
/** Whether a value is a plain JSON object, which is the only shape a record crossing a process boundary has. */
|
|
421
|
+
function isRecord(value) {
|
|
422
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
423
|
+
}
|
|
420
424
|
function fleetPath(storageRoot) {
|
|
421
425
|
return join(storageRoot, DIAGNOSTICS_DIRECTORY, FLEET_FILE);
|
|
422
426
|
}
|
|
@@ -443,10 +447,9 @@ function readFleet(storageRoot) {
|
|
|
443
447
|
if (candidate.version !== 1 || !Array.isArray(candidate.devices)) {
|
|
444
448
|
return undefined;
|
|
445
449
|
}
|
|
446
|
-
const devices = candidate.devices.flatMap((
|
|
447
|
-
if (!
|
|
450
|
+
const devices = candidate.devices.flatMap((row) => {
|
|
451
|
+
if (!isRecord(row))
|
|
448
452
|
return [];
|
|
449
|
-
const row = entry;
|
|
450
453
|
const accessory = opaqueAccessoryAlias(row.accessory);
|
|
451
454
|
if (accessory === undefined || typeof row.represented !== 'boolean' || typeof row.attached !== 'boolean') {
|
|
452
455
|
return [];
|
|
@@ -532,19 +535,16 @@ function readHostEnvironment(storageRoot) {
|
|
|
532
535
|
* entire purpose of the record and a redacted one answers nothing. Neither is device or account material: the
|
|
533
536
|
* path is this plugin's own setting or the binary it ships, and the banner is that build's public identity.
|
|
534
537
|
*/
|
|
535
|
-
function readFfmpegIdentity(
|
|
536
|
-
if (!
|
|
538
|
+
function readFfmpegIdentity(recorded) {
|
|
539
|
+
if (!isRecord(recorded)) {
|
|
537
540
|
return undefined;
|
|
538
541
|
}
|
|
539
|
-
const recorded = value;
|
|
540
542
|
if (recorded.source !== 'bundled' && recorded.source !== 'configured') {
|
|
541
543
|
return undefined;
|
|
542
544
|
}
|
|
543
545
|
const path = typeof recorded.path === 'string' ? boundedText(recorded.path, MAX_FFMPEG_IDENTITY_LENGTH) : undefined;
|
|
544
546
|
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;
|
|
547
|
+
const encoders = isRecord(recorded.encoders) ? readEncoderQualification(recorded.encoders) : undefined;
|
|
548
548
|
return path === undefined
|
|
549
549
|
? undefined
|
|
550
550
|
: {
|
|
@@ -556,18 +556,23 @@ function readFfmpegIdentity(value) {
|
|
|
556
556
|
}
|
|
557
557
|
/**
|
|
558
558
|
* 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.
|
|
559
|
+
* build names, so the record is bounded by those three closed sets, and a `bytes` outcome's two integers.
|
|
560
560
|
*/
|
|
561
561
|
function readEncoderQualification(recorded) {
|
|
562
562
|
return Object.fromEntries(LIVE_VIDEO_ENCODERS.filter((name) => Object.hasOwn(recorded, name)).map((name) => {
|
|
563
|
-
const outcomes = recorded[name]
|
|
563
|
+
const outcomes = isRecord(recorded[name]) ? Object.entries(recorded[name]) : [];
|
|
564
564
|
return [
|
|
565
565
|
name,
|
|
566
566
|
Object.fromEntries(outcomes.flatMap(([selection, value]) => {
|
|
567
567
|
const [profile, level] = selection.split('@');
|
|
568
|
-
const outcome =
|
|
569
|
-
|
|
570
|
-
|
|
568
|
+
const outcome = Array.isArray(value) &&
|
|
569
|
+
value.length === 3 &&
|
|
570
|
+
value[0] === 'bytes' &&
|
|
571
|
+
value.slice(1).every(Number.isSafeInteger)
|
|
572
|
+
? value
|
|
573
|
+
: allowlistedLabel(value, QUALIFICATION_OUTCOMES);
|
|
574
|
+
return allowlistedLabel(profile, HOMEKIT_LIVE_VIDEO_PROFILES) &&
|
|
575
|
+
allowlistedLabel(level, HOMEKIT_LIVE_VIDEO_LEVELS) &&
|
|
571
576
|
selection === `${profile}@${level}` &&
|
|
572
577
|
outcome
|
|
573
578
|
? [[selection, outcome]]
|
|
@@ -1231,8 +1236,8 @@ export class GuidedDiagnostics {
|
|
|
1231
1236
|
await this.replaceOwnedFile(join(directory, REPRODUCTION_MARKERS_FILE), `${retained}${marker}`);
|
|
1232
1237
|
}
|
|
1233
1238
|
}
|
|
1234
|
-
function formatMessage(
|
|
1235
|
-
const template =
|
|
1239
|
+
function formatMessage(key, parameters = {}) {
|
|
1240
|
+
const template = EN_MESSAGES[key] ?? key;
|
|
1236
1241
|
return template
|
|
1237
1242
|
.replace(/\{([a-zA-Z0-9]+)\}/g, (placeholder, name) => parameters[name] === undefined ? placeholder : String(parameters[name]))
|
|
1238
1243
|
.replace(/\s+/g, ' ')
|
|
@@ -1240,7 +1245,7 @@ function formatMessage(catalog, key, parameters = {}) {
|
|
|
1240
1245
|
}
|
|
1241
1246
|
function localize(target, key, parameters = {}) {
|
|
1242
1247
|
const translator = target.localize;
|
|
1243
|
-
return translator?.(key, parameters) ?? formatMessage(
|
|
1248
|
+
return translator?.(key, parameters) ?? formatMessage(key, parameters);
|
|
1244
1249
|
}
|
|
1245
1250
|
class JsonLineLog {
|
|
1246
1251
|
onError;
|
|
@@ -1257,11 +1262,8 @@ class JsonLineLog {
|
|
|
1257
1262
|
this.directory = join(storageRoot, LOG_DIRECTORY);
|
|
1258
1263
|
this.path = join(this.directory, LOG_FILE);
|
|
1259
1264
|
}
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
if (!payload) {
|
|
1263
|
-
return;
|
|
1264
|
-
}
|
|
1265
|
+
/** Queues one record the sanitizer already rebuilt. */
|
|
1266
|
+
write(payload) {
|
|
1265
1267
|
const record = this.serialize(payload);
|
|
1266
1268
|
const bytes = Buffer.byteLength(record);
|
|
1267
1269
|
if (bytes > MAX_LOG_RECORD_BYTES || this.queuedBytes + bytes > MAX_QUEUED_LOG_BYTES) {
|
|
@@ -1269,21 +1271,12 @@ class JsonLineLog {
|
|
|
1269
1271
|
this.droppedRecords += 1;
|
|
1270
1272
|
return;
|
|
1271
1273
|
}
|
|
1272
|
-
const
|
|
1273
|
-
const droppedAt = this.droppedAt;
|
|
1274
|
-
this.droppedRecords = 0;
|
|
1275
|
-
this.droppedAt = undefined;
|
|
1274
|
+
const dropped = this.drainDropped();
|
|
1276
1275
|
this.queuedBytes += bytes;
|
|
1277
1276
|
this.pending = this.pending
|
|
1278
1277
|
.then(async () => {
|
|
1279
|
-
if (
|
|
1280
|
-
await this.append(
|
|
1281
|
-
scope: 'diagnostics',
|
|
1282
|
-
level: 'warn',
|
|
1283
|
-
event: 'records-dropped',
|
|
1284
|
-
droppedRecords,
|
|
1285
|
-
}, droppedAt));
|
|
1286
|
-
}
|
|
1278
|
+
if (dropped)
|
|
1279
|
+
await this.append(dropped);
|
|
1287
1280
|
await this.append(record);
|
|
1288
1281
|
})
|
|
1289
1282
|
.catch(() => this.onError())
|
|
@@ -1292,22 +1285,21 @@ class JsonLineLog {
|
|
|
1292
1285
|
});
|
|
1293
1286
|
}
|
|
1294
1287
|
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());
|
|
1288
|
+
const dropped = this.drainDropped();
|
|
1289
|
+
if (dropped) {
|
|
1290
|
+
this.pending = this.pending.then(() => this.append(dropped)).catch(() => this.onError());
|
|
1308
1291
|
}
|
|
1309
1292
|
return this.pending;
|
|
1310
1293
|
}
|
|
1294
|
+
/** The `records-dropped` record for what was refused since the last one, resetting the tally, or nothing. */
|
|
1295
|
+
drainDropped() {
|
|
1296
|
+
const { droppedRecords, droppedAt } = this;
|
|
1297
|
+
this.droppedRecords = 0;
|
|
1298
|
+
this.droppedAt = undefined;
|
|
1299
|
+
return droppedRecords === 0
|
|
1300
|
+
? undefined
|
|
1301
|
+
: this.serialize({ scope: 'diagnostics', level: 'warn', event: 'records-dropped', droppedRecords }, droppedAt);
|
|
1302
|
+
}
|
|
1311
1303
|
serialize(payload, timestamp = new Date(this.now()).toISOString()) {
|
|
1312
1304
|
return `${JSON.stringify({ ...payload, timestamp })}\n`;
|
|
1313
1305
|
}
|
|
@@ -1366,12 +1358,12 @@ class JsonLineLog {
|
|
|
1366
1358
|
* Routes human messages to Homebridge and structured events to the plugin JSONL log.
|
|
1367
1359
|
* File writes preserve accepted order; records beyond the bounded pending queue are counted and reported.
|
|
1368
1360
|
*/
|
|
1369
|
-
export function createDiagnosticLogger(target, storageRoot,
|
|
1361
|
+
export function createDiagnosticLogger(target, storageRoot, now = Date.now) {
|
|
1370
1362
|
let fileFailureReported = false;
|
|
1371
1363
|
const reportFileFailure = () => {
|
|
1372
1364
|
if (!fileFailureReported) {
|
|
1373
1365
|
fileFailureReported = true;
|
|
1374
|
-
target.warn(`[diagnostic-log-write-failed] ${formatMessage(
|
|
1366
|
+
target.warn(`[diagnostic-log-write-failed] ${formatMessage('log.diagnosticFileFailed')}`);
|
|
1375
1367
|
}
|
|
1376
1368
|
};
|
|
1377
1369
|
const file = storageRoot ? new JsonLineLog(storageRoot, reportFileFailure, now) : undefined;
|
|
@@ -1385,7 +1377,7 @@ export function createDiagnosticLogger(target, storageRoot, catalog = EN_MESSAGE
|
|
|
1385
1377
|
if (verbose && (!session || !DIAGNOSTICS_PROFILES[session.profile].includes(evidenceClassOf(event)))) {
|
|
1386
1378
|
return;
|
|
1387
1379
|
}
|
|
1388
|
-
file.write(
|
|
1380
|
+
file.write(event);
|
|
1389
1381
|
}
|
|
1390
1382
|
: undefined;
|
|
1391
1383
|
return {
|
|
@@ -1393,7 +1385,7 @@ export function createDiagnosticLogger(target, storageRoot, catalog = EN_MESSAGE
|
|
|
1393
1385
|
error: (message) => target.error(message),
|
|
1394
1386
|
flush: () => file?.flush() ?? Promise.resolve(),
|
|
1395
1387
|
info: (message) => target.info(message),
|
|
1396
|
-
localize: (key, parameters) => formatMessage(
|
|
1388
|
+
localize: (key, parameters) => formatMessage(key, parameters),
|
|
1397
1389
|
warn: (message) => target.warn(message),
|
|
1398
1390
|
};
|
|
1399
1391
|
}
|
|
@@ -1531,6 +1523,7 @@ const LIVE_TRACE_PHASES = {
|
|
|
1531
1523
|
'sequence-restart': retainedInteger('dataType', 3),
|
|
1532
1524
|
'level2-wait': retainedInteger('waitMs', MAX_STARTUP_WINDOW_MS),
|
|
1533
1525
|
'level2-ready': retainedInteger('cipherId', 65535),
|
|
1526
|
+
'level2-set-payload-only': retainedInteger('cipherId', 65535),
|
|
1534
1527
|
'session-connect-wait': retainedInteger('waitMs', MAX_STARTUP_WINDOW_MS),
|
|
1535
1528
|
'session-connected': retainedInteger('waitedMs', MAX_STARTUP_WINDOW_MS),
|
|
1536
1529
|
'session-unreachable': retainedInteger('waitedMs', MAX_STARTUP_WINDOW_MS),
|
|
@@ -1622,16 +1615,22 @@ const LIVE_TRACE_PHASES = {
|
|
|
1622
1615
|
const reason = allowlistedLabel(c.reason, ['level2-key', 'address']);
|
|
1623
1616
|
return reason ? { reason } : undefined;
|
|
1624
1617
|
},
|
|
1618
|
+
/** `errorName` is an upstream class name, so it stays out of the archive; reason and stage say enough. */
|
|
1619
|
+
'start-failed': (c) => {
|
|
1620
|
+
const reason = allowlistedLabel(c.reason, ['warm-timeout', 'source-error', 'source-ended']);
|
|
1621
|
+
const stage = allowlistedLabel(c.stage, ['awaiting-first-frame', 'audio-only', 'awaiting-keyframe']);
|
|
1622
|
+
const attempts = boundedInteger(c.attempts, 1000);
|
|
1623
|
+
return reason && stage && attempts !== undefined ? { reason, stage, attempts } : undefined;
|
|
1624
|
+
},
|
|
1625
1625
|
warming: (c) => {
|
|
1626
1626
|
const retryMs = boundedInteger(c.retryMs, MAX_STARTUP_WINDOW_MS);
|
|
1627
1627
|
const deadlineMs = boundedInteger(c.deadlineMs, MAX_STARTUP_WINDOW_MS);
|
|
1628
1628
|
return retryMs === undefined || deadlineMs === undefined ? undefined : { retryMs, deadlineMs };
|
|
1629
1629
|
},
|
|
1630
1630
|
};
|
|
1631
|
-
function sanitizeSdkLiveStartTrace(
|
|
1632
|
-
if (!
|
|
1631
|
+
function sanitizeSdkLiveStartTrace(candidate) {
|
|
1632
|
+
if (!isRecord(candidate))
|
|
1633
1633
|
return undefined;
|
|
1634
|
-
const candidate = value;
|
|
1635
1634
|
const phase = String(candidate.phase);
|
|
1636
1635
|
if (!Object.hasOwn(LIVE_TRACE_PHASES, phase))
|
|
1637
1636
|
return undefined;
|
|
@@ -1885,7 +1884,7 @@ const HOMEKIT_EVENT_ROUTES = {
|
|
|
1885
1884
|
'camera.streaming': new Set(['camera-enabled-changed']),
|
|
1886
1885
|
'camera.controls': new Set(['camera-enabled-changed']),
|
|
1887
1886
|
};
|
|
1888
|
-
const HOMEKIT_OBSERVATIONS = new Set(['malformed', 'missing', 'valid']);
|
|
1887
|
+
const HOMEKIT_OBSERVATIONS = new Set(['malformed', 'missing', 'valid', 'ignored']);
|
|
1889
1888
|
/**
|
|
1890
1889
|
* What announced a change, for an adapter that follows more than one announcement for the same state.
|
|
1891
1890
|
*
|
|
@@ -1893,9 +1892,9 @@ const HOMEKIT_OBSERVATIONS = new Set(['malformed', 'missing', 'valid']);
|
|
|
1893
1892
|
* field is omitted where an adapter states no announcement and refused where it states one absent from here.
|
|
1894
1893
|
*/
|
|
1895
1894
|
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 =
|
|
1895
|
+
const HOMEKIT_LIVE_VIDEO_OPERATIONS = ['start', 'reconfigure'];
|
|
1896
|
+
const HOMEKIT_LIVE_VIDEO_PROFILES = ['baseline', 'main', 'high'];
|
|
1897
|
+
const HOMEKIT_LIVE_VIDEO_LEVELS = ['3.1', '3.2', '4.0'];
|
|
1899
1898
|
/**
|
|
1900
1899
|
* The inclusive bounds a controller-selected live geometry and frame rate are retained within.
|
|
1901
1900
|
*
|
|
@@ -1919,13 +1918,13 @@ const MAX_LIVE_VIDEO_MTU = 65_535;
|
|
|
1919
1918
|
/** The bit rate ceiling a controller negotiated, in kbps, bounded to the unsigned 16-bit field that carries it. */
|
|
1920
1919
|
const MAX_LIVE_VIDEO_BIT_RATE = 65_535;
|
|
1921
1920
|
/** The two address families HomeKit negotiates a destination in. */
|
|
1922
|
-
const HOMEKIT_LIVE_ADDRESS_VERSIONS =
|
|
1923
|
-
const HOMEKIT_LIVE_SESSION_STAGES =
|
|
1921
|
+
const HOMEKIT_LIVE_ADDRESS_VERSIONS = ['ipv4', 'ipv6'];
|
|
1922
|
+
const HOMEKIT_LIVE_SESSION_STAGES = [
|
|
1924
1923
|
'sdk-source-acquisition',
|
|
1925
1924
|
'first-source-keyframe',
|
|
1926
1925
|
'first-adapted-output',
|
|
1927
1926
|
'controller-rtcp',
|
|
1928
|
-
]
|
|
1927
|
+
];
|
|
1929
1928
|
/** The SDK's own stage and reason for a live warm-up that ended without a keyframe. */
|
|
1930
1929
|
const SDK_LIVE_START_STAGES = ['awaiting-first-frame', 'audio-only', 'awaiting-keyframe'];
|
|
1931
1930
|
const SDK_LIVE_START_REASONS = ['warm-timeout', 'source-error', 'source-ended'];
|
|
@@ -1935,7 +1934,16 @@ const SDK_LIVE_START_REASONS = ['warm-timeout', 'source-error', 'source-ended'];
|
|
|
1935
1934
|
* A superset of what the media domain can report, because the SDK runs FFmpeg of its own for snapshot
|
|
1936
1935
|
* decoding and WebRTC containers and forwards its output here under the same evidence class.
|
|
1937
1936
|
*/
|
|
1938
|
-
const ADAPTATION_ROLES = new Set([
|
|
1937
|
+
const ADAPTATION_ROLES = new Set([
|
|
1938
|
+
'live-video',
|
|
1939
|
+
'live-audio',
|
|
1940
|
+
'return-audio',
|
|
1941
|
+
'recording',
|
|
1942
|
+
'preview-encode',
|
|
1943
|
+
'preview-decode',
|
|
1944
|
+
'preview-raw',
|
|
1945
|
+
'sdk',
|
|
1946
|
+
]);
|
|
1939
1947
|
/** What that process did. `output` is a process reported for what it wrote rather than for how it ended. */
|
|
1940
1948
|
const ADAPTATION_EVENTS = new Set([
|
|
1941
1949
|
'started',
|
|
@@ -1946,6 +1954,16 @@ const ADAPTATION_EVENTS = new Set([
|
|
|
1946
1954
|
'exited-while-streaming',
|
|
1947
1955
|
'output',
|
|
1948
1956
|
]);
|
|
1957
|
+
/** How a settings-page live preview ended. */
|
|
1958
|
+
const PREVIEW_OUTCOMES = ['picture', 'unplayable', 'no-video', 'stopped'];
|
|
1959
|
+
/** The iPhone asks a live preview codes for, and the live video settings it may try. */
|
|
1960
|
+
const PREVIEW_ASKS = ['wifi', 'cellular'];
|
|
1961
|
+
const PREVIEW_SETTINGS = [
|
|
1962
|
+
'smallVideoPackets',
|
|
1963
|
+
'liveVideoPassthrough',
|
|
1964
|
+
'hardwareVideoEncoder',
|
|
1965
|
+
'ignoreHomeBitRate',
|
|
1966
|
+
];
|
|
1949
1967
|
/**
|
|
1950
1968
|
* Why a process was started, on the record that reports it starting.
|
|
1951
1969
|
*
|
|
@@ -1964,7 +1982,7 @@ const LIVE_VIDEO_ENCODERS = [
|
|
|
1964
1982
|
'h264_vaapi',
|
|
1965
1983
|
'h264_v4l2m2m',
|
|
1966
1984
|
];
|
|
1967
|
-
/** What qualifying a hardware encoder for one selection found
|
|
1985
|
+
/** What qualifying a hardware encoder for one selection found, but `bytes`, which is read with its two integers. */
|
|
1968
1986
|
const QUALIFICATION_OUTCOMES = [
|
|
1969
1987
|
'qualified',
|
|
1970
1988
|
'budget',
|
|
@@ -1974,7 +1992,6 @@ const QUALIFICATION_OUTCOMES = [
|
|
|
1974
1992
|
'level',
|
|
1975
1993
|
'geometry',
|
|
1976
1994
|
'frames',
|
|
1977
|
-
'bytes',
|
|
1978
1995
|
];
|
|
1979
1996
|
/** The codec a live video source delivers. */
|
|
1980
1997
|
const LIVE_VIDEO_SOURCES = ['h264', 'h265', 'av1'];
|
|
@@ -2019,74 +2036,32 @@ const ADAPTATION_SIGNALS = new Set([
|
|
|
2019
2036
|
const MAX_ADAPTATION_STDERR_LINES = 8;
|
|
2020
2037
|
const MAX_ADAPTATION_STDERR_LINE_LENGTH = 200;
|
|
2021
2038
|
/**
|
|
2022
|
-
* The
|
|
2039
|
+
* The fixed-shape operational notices, each `[level, messageKey, durationMessageKey?]`.
|
|
2023
2040
|
*
|
|
2024
2041
|
* Operational rather than verbose, so each is `plugin-log` evidence that every profile declares.
|
|
2025
2042
|
*/
|
|
2026
|
-
const MEDIA_NOTICES = {
|
|
2027
|
-
'camera-snapshot-cache-invalid': {
|
|
2028
|
-
level: 'warn',
|
|
2029
|
-
messageKey: 'log.snapshotCacheInvalid',
|
|
2030
|
-
},
|
|
2031
|
-
};
|
|
2032
2043
|
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
|
-
},
|
|
2044
|
+
'status-publication-failed': ['warn', 'log.notice.statusPublicationFailed'],
|
|
2045
|
+
'ownership-release-not-finalized': ['error', 'log.notice.ownershipReleaseNotFinalized'],
|
|
2046
|
+
'ownership-release-failed': ['error', 'log.notice.ownershipReleaseFailed'],
|
|
2047
|
+
'shutdown-failed': ['error', 'log.notice.shutdownFailed'],
|
|
2048
|
+
'shutdown-timeout': ['warn', 'log.notice.shutdownTimeout', 'log.notice.shutdownTimeoutWithDuration'],
|
|
2049
|
+
'ownership-acquisition-failed': ['error', 'log.notice.ownershipAcquisitionFailed'],
|
|
2050
|
+
'ownership-release-incomplete': ['warn', 'log.notice.ownershipReleaseIncomplete'],
|
|
2051
|
+
'registry-subscriber-failed': ['warn', 'log.notice.registrySubscriberFailed'],
|
|
2052
|
+
'event-subscriber-failed': ['warn', 'log.notice.eventSubscriberFailed'],
|
|
2053
|
+
'state-subscriber-failed': ['warn', 'log.notice.stateSubscriberFailed'],
|
|
2054
|
+
'channel-serve-failed': ['warn', 'log.notice.channelServeFailed'],
|
|
2055
|
+
'diagnostics-authorization-armed': ['info', 'log.notice.diagnosticsAuthorizationArmed'],
|
|
2056
|
+
'stand-down-not-rearmed': ['warn', 'log.notice.standDownNotRearmed'],
|
|
2057
|
+
'stand-down-account-replaced': ['warn', 'log.notice.standDownAccountReplaced'],
|
|
2058
|
+
'camera-snapshot-cache-invalid': ['warn', 'log.snapshotCacheInvalid'],
|
|
2059
|
+
'discarded-v4-settings-unacknowledged': ['warn', 'log.discardedSettings'],
|
|
2060
|
+
};
|
|
2061
|
+
/** The scope a notice is recorded under where it is not `runtime-notice`. */
|
|
2062
|
+
const NOTICE_SCOPES = {
|
|
2063
|
+
'camera-snapshot-cache-invalid': 'media-notice',
|
|
2064
|
+
'discarded-v4-settings-unacknowledged': 'configuration-notice',
|
|
2090
2065
|
};
|
|
2091
2066
|
const RUNTIME_CONDITIONS = {
|
|
2092
2067
|
degraded: {
|
|
@@ -2186,7 +2161,7 @@ const HOMEKIT_REASON_ACTIONS = {
|
|
|
2186
2161
|
};
|
|
2187
2162
|
/** Projects a value onto an active condition, or nothing where the catalog does not write that code with those keys. */
|
|
2188
2163
|
export function knownCondition(value) {
|
|
2189
|
-
if (
|
|
2164
|
+
if (!isRecord(value))
|
|
2190
2165
|
return undefined;
|
|
2191
2166
|
const { code, summaryKey, actionKey, serials } = value;
|
|
2192
2167
|
if (typeof code !== 'string')
|
|
@@ -2223,14 +2198,13 @@ function homeKitConditionPresentation(code, active, reason) {
|
|
|
2223
2198
|
function sanitizeStructuredEvent(message) {
|
|
2224
2199
|
let value;
|
|
2225
2200
|
try {
|
|
2226
|
-
|
|
2227
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
2228
|
-
return undefined;
|
|
2229
|
-
value = parsed;
|
|
2201
|
+
value = JSON.parse(message);
|
|
2230
2202
|
}
|
|
2231
2203
|
catch {
|
|
2232
2204
|
return undefined;
|
|
2233
2205
|
}
|
|
2206
|
+
if (!isRecord(value))
|
|
2207
|
+
return undefined;
|
|
2234
2208
|
const level = allowlistedLabel(value.level, ['debug', 'info', 'warn', 'error']);
|
|
2235
2209
|
if (!level || typeof value.scope !== 'string')
|
|
2236
2210
|
return undefined;
|
|
@@ -2253,18 +2227,17 @@ function sanitizeStructuredEvent(message) {
|
|
|
2253
2227
|
}
|
|
2254
2228
|
const details = [];
|
|
2255
2229
|
for (const detail of Array.isArray(value.details) ? value.details.slice(0, MAX_SDK_DETAILS) : []) {
|
|
2256
|
-
if (!
|
|
2230
|
+
if (!isRecord(detail))
|
|
2257
2231
|
continue;
|
|
2258
|
-
const
|
|
2259
|
-
const errorType = allowlistedLabel(candidate.errorType, SDK_ERROR_TYPES);
|
|
2232
|
+
const errorType = allowlistedLabel(detail.errorType, SDK_ERROR_TYPES);
|
|
2260
2233
|
if (errorType) {
|
|
2261
2234
|
details.push({ errorType });
|
|
2262
2235
|
continue;
|
|
2263
2236
|
}
|
|
2264
|
-
const type = allowlistedLabel(
|
|
2237
|
+
const type = allowlistedLabel(detail.type, ['boolean', 'number', 'object', 'string', 'undefined']);
|
|
2265
2238
|
if (!type)
|
|
2266
2239
|
continue;
|
|
2267
|
-
const length = nonNegativeInteger(
|
|
2240
|
+
const length = nonNegativeInteger(detail.length);
|
|
2268
2241
|
details.push({ type, ...(length === undefined ? {} : { length }) });
|
|
2269
2242
|
}
|
|
2270
2243
|
return {
|
|
@@ -2276,72 +2249,14 @@ function sanitizeStructuredEvent(message) {
|
|
|
2276
2249
|
...(value.detailsTruncated === true ? { detailsTruncated: true } : {}),
|
|
2277
2250
|
};
|
|
2278
2251
|
}
|
|
2279
|
-
if (value.scope === 'runtime-notice') {
|
|
2252
|
+
if (value.scope === 'runtime-notice' || value.scope === 'media-notice' || value.scope === 'configuration-notice') {
|
|
2280
2253
|
if (typeof value.code !== 'string' || !Object.hasOwn(RUNTIME_NOTICES, value.code))
|
|
2281
2254
|
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 };
|
|
2255
|
+
const notice = runtimeNoticeRecord(value.code, nonNegativeInteger(value.durationMs));
|
|
2256
|
+
return notice.scope === value.scope ? notice : undefined;
|
|
2308
2257
|
}
|
|
2309
2258
|
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
|
-
};
|
|
2259
|
+
return sanitizeHomeKitEvent(value);
|
|
2345
2260
|
}
|
|
2346
2261
|
if (value.scope === 'runtime') {
|
|
2347
2262
|
const event = allowlistedLabel(value.event, ['ready', 'stopped']);
|
|
@@ -2352,6 +2267,10 @@ function sanitizeStructuredEvent(message) {
|
|
|
2352
2267
|
if (value.scope === 'ffmpeg') {
|
|
2353
2268
|
return sanitizeAdaptationNotice(value, level);
|
|
2354
2269
|
}
|
|
2270
|
+
if (value.scope === 'live-preview') {
|
|
2271
|
+
const preview = sanitizeLivePreview(value);
|
|
2272
|
+
return preview ? { scope: 'live-preview', level: 'info', ...preview } : undefined;
|
|
2273
|
+
}
|
|
2355
2274
|
if (value.scope === 'diagnostic-condition') {
|
|
2356
2275
|
const homeKitDefinition = typeof value.code === 'string' && Object.hasOwn(HOMEKIT_CONDITIONS, value.code)
|
|
2357
2276
|
? HOMEKIT_CONDITIONS[value.code]
|
|
@@ -2386,7 +2305,7 @@ function sanitizeStructuredEvent(message) {
|
|
|
2386
2305
|
const affectedAccessoryCount = nonNegativeInteger(value.affectedAccessoryCount);
|
|
2387
2306
|
const accessoryAliases = Array.isArray(value.accessoryAliases)
|
|
2388
2307
|
? value.accessoryAliases
|
|
2389
|
-
.filter((alias) =>
|
|
2308
|
+
.filter((alias) => opaqueAccessoryAlias(alias) !== undefined)
|
|
2390
2309
|
.slice(0, MAX_ACCESSORY_ALIASES)
|
|
2391
2310
|
: [];
|
|
2392
2311
|
const { level: conditionLevel, actionKey } = homeKitConditionPresentation(value.code, value.active, value.reason);
|
|
@@ -2409,19 +2328,22 @@ function sanitizeStructuredEvent(message) {
|
|
|
2409
2328
|
}
|
|
2410
2329
|
return undefined;
|
|
2411
2330
|
}
|
|
2412
|
-
/**
|
|
2413
|
-
|
|
2414
|
-
const
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
target.debug?.(JSON.stringify({
|
|
2419
|
-
scope: 'runtime-notice',
|
|
2420
|
-
level: notice.level,
|
|
2331
|
+
/** The record one notice is written as, read by both halves of the path. */
|
|
2332
|
+
function runtimeNoticeRecord(code, durationMs) {
|
|
2333
|
+
const [level, messageKey, durationMessageKey] = RUNTIME_NOTICES[code];
|
|
2334
|
+
return {
|
|
2335
|
+
scope: NOTICE_SCOPES[code] ?? 'runtime-notice',
|
|
2336
|
+
level,
|
|
2421
2337
|
code,
|
|
2422
|
-
messageKey,
|
|
2338
|
+
messageKey: durationMs !== undefined && durationMessageKey ? durationMessageKey : messageKey,
|
|
2423
2339
|
...(durationMs === undefined ? {} : { durationMs }),
|
|
2424
|
-
}
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2342
|
+
/** Emits one fixed-shape operational notice selected by its allowlisted code. */
|
|
2343
|
+
export function reportRuntimeNotice(target, code, fields = {}) {
|
|
2344
|
+
const record = runtimeNoticeRecord(code, fields.durationMs === undefined ? undefined : Math.max(0, Math.trunc(fields.durationMs)));
|
|
2345
|
+
target[record.level]?.(`[${code}] ${localize(target, record.messageKey, { durationMs: record.durationMs ?? 0, finishBy: fields.finishBy ?? '' })}`);
|
|
2346
|
+
target.debug?.(JSON.stringify(record));
|
|
2425
2347
|
}
|
|
2426
2348
|
/**
|
|
2427
2349
|
* Picks up an authorization the persisted session file confirms, reporting whether it did.
|
|
@@ -2442,18 +2364,6 @@ export function armDiagnosticsAuthorization(target, storageRoot, supportCaseId,
|
|
|
2442
2364
|
});
|
|
2443
2365
|
return true;
|
|
2444
2366
|
}
|
|
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
2367
|
/** States once which encoders code live video on this host: every qualified hardware encoder, then `libx264`. */
|
|
2458
2368
|
export function reportLiveVideoEncoders(target, encoders) {
|
|
2459
2369
|
const names = [
|
|
@@ -2462,19 +2372,6 @@ export function reportLiveVideoEncoders(target, encoders) {
|
|
|
2462
2372
|
];
|
|
2463
2373
|
target.info(`[live-video-encoders] ${localize(target, 'log.liveVideoEncoders', { encoders: names.join(', ') })}`);
|
|
2464
2374
|
}
|
|
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
2375
|
/**
|
|
2479
2376
|
* Narrows one offered station arbitration decision, or nothing where its own labels do not narrow.
|
|
2480
2377
|
*
|
|
@@ -2515,49 +2412,63 @@ export function reportStationClaim(target, decision) {
|
|
|
2515
2412
|
target.debug(JSON.stringify({ scope: 'homekit', level: 'debug', ...claim }));
|
|
2516
2413
|
}
|
|
2517
2414
|
}
|
|
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;
|
|
2415
|
+
/**
|
|
2416
|
+
* Narrows one offered HomeKit adapter fact to its `homekit` record, or nothing where its route does not narrow.
|
|
2417
|
+
*
|
|
2418
|
+
* Read by both halves of the path: {@link reportHomeKitEvent} builds the record and the file sink rebuilds it.
|
|
2419
|
+
*/
|
|
2420
|
+
function sanitizeHomeKitEvent(value) {
|
|
2421
|
+
if (value.event === 'event-unrouted') {
|
|
2422
|
+
const unrouted = sanitizeUnroutedEvent(value);
|
|
2423
|
+
return unrouted ? { scope: 'homekit', level: 'debug', ...unrouted } : undefined;
|
|
2424
|
+
}
|
|
2425
|
+
if (value.event === 'station-claim') {
|
|
2426
|
+
const claim = sanitizeStationClaim(value);
|
|
2427
|
+
return claim ? { scope: 'homekit', level: 'debug', ...claim } : undefined;
|
|
2428
|
+
}
|
|
2429
|
+
if (value.adapter === 'camera.streaming' && value.event === 'live-video-selected') {
|
|
2430
|
+
const selection = sanitizeLiveVideoSelection(value);
|
|
2431
|
+
return selection ? { scope: 'homekit', level: 'debug', ...selection } : undefined;
|
|
2432
|
+
}
|
|
2433
|
+
if (value.adapter === 'camera.streaming' && HOMEKIT_LIVE_REQUEST_EVENTS.has(String(value.event))) {
|
|
2434
|
+
const lifecycle = sanitizeLiveSessionTrace(value);
|
|
2435
|
+
return lifecycle ? { scope: 'homekit', level: 'debug', ...lifecycle } : undefined;
|
|
2436
|
+
}
|
|
2437
|
+
if (typeof value.adapter !== 'string' ||
|
|
2438
|
+
typeof value.event !== 'string' ||
|
|
2439
|
+
!HOMEKIT_EVENT_ROUTES[value.adapter]?.has(value.event) ||
|
|
2440
|
+
typeof value.observation !== 'string' ||
|
|
2441
|
+
!HOMEKIT_OBSERVATIONS.has(value.observation)) {
|
|
2442
|
+
return undefined;
|
|
2549
2443
|
}
|
|
2550
|
-
|
|
2444
|
+
const alias = opaqueAccessoryAlias(value.accessory);
|
|
2445
|
+
return {
|
|
2551
2446
|
scope: 'homekit',
|
|
2552
2447
|
level: 'debug',
|
|
2553
|
-
adapter:
|
|
2554
|
-
event:
|
|
2555
|
-
observation:
|
|
2556
|
-
...(typeof
|
|
2557
|
-
? { announcedBy:
|
|
2448
|
+
adapter: value.adapter,
|
|
2449
|
+
event: value.event,
|
|
2450
|
+
observation: value.observation,
|
|
2451
|
+
...(typeof value.announcedBy === 'string' && HOMEKIT_ANNOUNCEMENTS.has(value.announcedBy)
|
|
2452
|
+
? { announcedBy: value.announcedBy }
|
|
2558
2453
|
: {}),
|
|
2559
|
-
...
|
|
2560
|
-
|
|
2454
|
+
...(typeof value.code === 'number' && Number.isInteger(value.code) && value.code >= 0 && value.code <= 0xffff
|
|
2455
|
+
? { code: value.code }
|
|
2456
|
+
: {}),
|
|
2457
|
+
...(alias === undefined ? {} : { accessory: alias }),
|
|
2458
|
+
};
|
|
2459
|
+
}
|
|
2460
|
+
/**
|
|
2461
|
+
* Emits one allowlisted HomeKit event trace only when host debug output is available.
|
|
2462
|
+
*
|
|
2463
|
+
* `aliasFor` resolves the trace's device identity to the support-case alias the conditions about that accessory
|
|
2464
|
+
* already carry. The identity itself never reaches the record: an unresolved one is recorded as no accessory.
|
|
2465
|
+
*/
|
|
2466
|
+
export function reportHomeKitEvent(target, trace, aliasFor) {
|
|
2467
|
+
const alias = 'serial' in trace && typeof trace.serial === 'string' ? aliasFor?.(trace.serial) : undefined;
|
|
2468
|
+
const record = sanitizeHomeKitEvent({ ...trace, accessory: alias });
|
|
2469
|
+
if (target.debug && record) {
|
|
2470
|
+
target.debug(JSON.stringify(record));
|
|
2471
|
+
}
|
|
2561
2472
|
}
|
|
2562
2473
|
/**
|
|
2563
2474
|
* Records one FFmpeg adaptation fact as the `ffmpeg-log` evidence the media support profiles declare.
|
|
@@ -2577,19 +2488,8 @@ export function reportAdaptationNotice(target, trace) {
|
|
|
2577
2488
|
}
|
|
2578
2489
|
}
|
|
2579
2490
|
/**
|
|
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.
|
|
2491
|
+
* Reduces one FFmpeg stderr line to what a support case may keep, or nothing where nothing is left of it: a
|
|
2492
|
+
* bounded printable line, never a `progress=` one, through {@link redactSensitiveText}.
|
|
2593
2493
|
*/
|
|
2594
2494
|
function redactAdaptationStderr(line) {
|
|
2595
2495
|
const printable = boundedText(line, MAX_ADAPTATION_STDERR_LINE_LENGTH);
|
|
@@ -2680,6 +2580,57 @@ function sanitizeAdaptationNotice(value, level) {
|
|
|
2680
2580
|
...(stderr.length ? { stderr } : {}),
|
|
2681
2581
|
};
|
|
2682
2582
|
}
|
|
2583
|
+
/**
|
|
2584
|
+
* Records how one settings-page live preview ended, as a `live-preview` record kept whether or not a support
|
|
2585
|
+
* window is open: it is not one of the {@link VERBOSE_SCOPES}, because it holds only allowlisted labels and
|
|
2586
|
+
* bounded numbers, one per run a user started.
|
|
2587
|
+
*/
|
|
2588
|
+
export function reportLivePreview(target, trace) {
|
|
2589
|
+
const { level, ...selection } = trace.selection;
|
|
2590
|
+
const preview = sanitizeLivePreview({ ...trace, ...selection, levelName: level });
|
|
2591
|
+
if (preview)
|
|
2592
|
+
target.debug?.(JSON.stringify({ scope: 'live-preview', level: 'info', ...preview }));
|
|
2593
|
+
}
|
|
2594
|
+
/**
|
|
2595
|
+
* Narrows one offered live preview run, or nothing where its selection or outcome does not narrow.
|
|
2596
|
+
*
|
|
2597
|
+
* Read by both halves of the path, {@link reportLivePreview} and the file sink, so a record only one of them
|
|
2598
|
+
* recognised could not reach a support archive.
|
|
2599
|
+
*/
|
|
2600
|
+
function sanitizeLivePreview(value) {
|
|
2601
|
+
const selection = sanitizePreviewSelection(value);
|
|
2602
|
+
const outcome = allowlistedLabel(value.outcome, PREVIEW_OUTCOMES);
|
|
2603
|
+
if (!selection || !outcome)
|
|
2604
|
+
return undefined;
|
|
2605
|
+
const encoder = allowlistedLabel(value.encoder, LIVE_VIDEO_ENCODERS);
|
|
2606
|
+
const source = allowlistedLabel(value.source, LIVE_VIDEO_SOURCES);
|
|
2607
|
+
const kbps = boundedInteger(value.kbps, MAX_LIVE_VIDEO_BIT_RATE);
|
|
2608
|
+
const accessory = opaqueAccessoryAlias(value.accessory);
|
|
2609
|
+
const ask = allowlistedLabel(value.ask, PREVIEW_ASKS);
|
|
2610
|
+
const settings = PREVIEW_SETTINGS.filter((setting) => typeof value[setting] === 'boolean');
|
|
2611
|
+
return {
|
|
2612
|
+
...selection,
|
|
2613
|
+
...(encoder === undefined ? {} : { encoder }),
|
|
2614
|
+
...(source === undefined ? {} : { source }),
|
|
2615
|
+
...(kbps === undefined ? {} : { kbps }),
|
|
2616
|
+
...(ask === undefined ? {} : { ask }),
|
|
2617
|
+
...Object.fromEntries(settings.map((setting) => [setting, value[setting]])),
|
|
2618
|
+
outcome,
|
|
2619
|
+
...(accessory === undefined ? {} : { accessory }),
|
|
2620
|
+
};
|
|
2621
|
+
}
|
|
2622
|
+
/** A live preview's selection as flat bounded fields, or nothing where any of them falls outside its allowlist. */
|
|
2623
|
+
function sanitizePreviewSelection(candidate) {
|
|
2624
|
+
const fields = {
|
|
2625
|
+
profile: allowlistedLabel(candidate.profile, HOMEKIT_LIVE_VIDEO_PROFILES),
|
|
2626
|
+
levelName: allowlistedLabel(candidate.levelName, HOMEKIT_LIVE_VIDEO_LEVELS),
|
|
2627
|
+
width: boundedInteger(candidate.width, MAX_LIVE_VIDEO_DIMENSION, MIN_LIVE_VIDEO_DIMENSION),
|
|
2628
|
+
height: boundedInteger(candidate.height, MAX_LIVE_VIDEO_DIMENSION, MIN_LIVE_VIDEO_DIMENSION),
|
|
2629
|
+
fps: boundedInteger(candidate.fps, MAX_LIVE_VIDEO_FRAME_RATE, MIN_LIVE_VIDEO_FRAME_RATE),
|
|
2630
|
+
maxBitRate: boundedInteger(candidate.maxBitRate, MAX_LIVE_VIDEO_BIT_RATE),
|
|
2631
|
+
};
|
|
2632
|
+
return Object.values(fields).includes(undefined) ? undefined : fields;
|
|
2633
|
+
}
|
|
2683
2634
|
function sanitizeLiveSessionTrace(value) {
|
|
2684
2635
|
if (value.adapter !== 'camera.streaming') {
|
|
2685
2636
|
return undefined;
|
|
@@ -2720,8 +2671,7 @@ function sanitizeLiveSessionTrace(value) {
|
|
|
2720
2671
|
value.outcome !== 'failed' ||
|
|
2721
2672
|
typeof value.reason !== 'string' ||
|
|
2722
2673
|
!REASONS.has(value.reason) ||
|
|
2723
|
-
|
|
2724
|
-
!HOMEKIT_LIVE_SESSION_STAGES.has(value.stage)) {
|
|
2674
|
+
!allowlistedLabel(value.stage, HOMEKIT_LIVE_SESSION_STAGES)) {
|
|
2725
2675
|
return undefined;
|
|
2726
2676
|
}
|
|
2727
2677
|
const sourceStage = allowlistedLabel(value.sourceStage, SDK_LIVE_START_STAGES);
|
|
@@ -2738,27 +2688,23 @@ function sanitizeLiveSessionTrace(value) {
|
|
|
2738
2688
|
};
|
|
2739
2689
|
}
|
|
2740
2690
|
function sanitizeLiveVideoSelection(value) {
|
|
2741
|
-
const operation =
|
|
2742
|
-
const profile =
|
|
2743
|
-
const codecLevel = typeof value.levelName === 'string' ? value.levelName :
|
|
2691
|
+
const operation = allowlistedLabel(value.operation, HOMEKIT_LIVE_VIDEO_OPERATIONS);
|
|
2692
|
+
const profile = allowlistedLabel(value.profile, HOMEKIT_LIVE_VIDEO_PROFILES);
|
|
2693
|
+
const codecLevel = allowlistedLabel(typeof value.levelName === 'string' ? value.levelName : value.level, HOMEKIT_LIVE_VIDEO_LEVELS);
|
|
2744
2694
|
const width = boundedInteger(value.width, MAX_LIVE_VIDEO_DIMENSION, MIN_LIVE_VIDEO_DIMENSION);
|
|
2745
2695
|
const height = boundedInteger(value.height, MAX_LIVE_VIDEO_DIMENSION, MIN_LIVE_VIDEO_DIMENSION);
|
|
2746
2696
|
const fps = boundedInteger(value.fps, MAX_LIVE_VIDEO_FRAME_RATE, MIN_LIVE_VIDEO_FRAME_RATE);
|
|
2747
2697
|
const mtu = boundedInteger(value.mtu, MAX_LIVE_VIDEO_MTU, MIN_LIVE_VIDEO_MTU);
|
|
2748
2698
|
const maxBitRate = boundedInteger(value.maxBitRate, MAX_LIVE_VIDEO_BIT_RATE, 0);
|
|
2749
|
-
const addressVersion =
|
|
2699
|
+
const addressVersion = allowlistedLabel(value.addressVersion, HOMEKIT_LIVE_ADDRESS_VERSIONS);
|
|
2750
2700
|
if (!operation ||
|
|
2751
|
-
!HOMEKIT_LIVE_VIDEO_OPERATIONS.has(operation) ||
|
|
2752
2701
|
!profile ||
|
|
2753
|
-
!HOMEKIT_LIVE_VIDEO_PROFILES.has(profile) ||
|
|
2754
2702
|
!codecLevel ||
|
|
2755
|
-
!HOMEKIT_LIVE_VIDEO_LEVELS.has(codecLevel) ||
|
|
2756
2703
|
width === undefined ||
|
|
2757
2704
|
height === undefined ||
|
|
2758
2705
|
fps === undefined ||
|
|
2759
2706
|
mtu === undefined ||
|
|
2760
|
-
!addressVersion
|
|
2761
|
-
!HOMEKIT_LIVE_ADDRESS_VERSIONS.has(addressVersion)) {
|
|
2707
|
+
!addressVersion) {
|
|
2762
2708
|
return undefined;
|
|
2763
2709
|
}
|
|
2764
2710
|
const alias = opaqueAccessoryAlias(value.accessory);
|