@trillboards/edge-sdk 0.2.3 → 0.2.6
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/README.md +4 -3
- package/deploy/docker/Dockerfile.cloud-sense +117 -0
- package/deploy/docker/Dockerfile.cpu +30 -21
- package/deploy/docker/Dockerfile.cuda +30 -21
- package/deploy/docker/Dockerfile.openvino +30 -20
- package/deploy/docker/README.md +9 -4
- package/deploy/docker/connector-entrypoint.sh +122 -0
- package/deploy/helm/README.md +1 -1
- package/deploy/k8s/daemonset.yaml +1 -1
- package/deploy/macos/README.md +161 -0
- package/deploy/macos/TrillboardsSetupApp.m +603 -0
- package/deploy/macos/build-macos-pkg.sh +588 -0
- package/deploy/macos/com.trillboards.edge.plist +62 -0
- package/deploy/macos/com.trillboards.litertlm.plist +45 -0
- package/deploy/macos/pair.command +519 -0
- package/deploy/macos/scripts/ensure-litertlm.sh +62 -0
- package/deploy/macos/scripts/postinstall +153 -0
- package/deploy/macos/scripts/setup-litertlm.sh +272 -0
- package/deploy/macos/test-install-pair.sh +383 -0
- package/deploy/macos/test-setup-litertlm-reinstall.sh +109 -0
- package/deploy/systemd/install.sh +25 -15
- package/deploy/systemd/trillboards-edge.service +4 -1
- package/deploy/windows/README.md +70 -0
- package/deploy/windows/build-windows.ps1 +225 -0
- package/deploy/windows/bundle-windows-installer.ps1 +153 -0
- package/deploy/windows/check-feeds.js +26 -0
- package/deploy/windows/e2e/fixtures/person.jpg +0 -0
- package/deploy/windows/e2e/test-local-sense.ps1 +185 -0
- package/deploy/windows/install-update.ps1 +101 -0
- package/deploy/windows/installer.nsi +138 -0
- package/deploy/windows/register-litertlm-task.ps1 +42 -0
- package/deploy/windows/register-task.ps1 +44 -0
- package/deploy/windows/run-connector.ps1 +69 -0
- package/deploy/windows/setup-litertlm.ps1 +361 -0
- package/deploy/windows/stop-existing.ps1 +72 -0
- package/deploy/windows/trillboards-edge-task.vbs +25 -0
- package/deploy/windows/trillboards-edge.cmd +33 -0
- package/deploy/windows/trillboards-pair.cmd +276 -0
- package/deploy/windows/trillboards-setup.ps1 +456 -0
- package/deploy/windows/trillboards-setup.vbs +13 -0
- package/dist/AudienceAnalyzeClient.d.ts +136 -0
- package/dist/AudienceAnalyzeClient.d.ts.map +1 -0
- package/dist/AudienceAnalyzeClient.js +246 -0
- package/dist/AudienceAnalyzeClient.js.map +1 -0
- package/dist/ChannelProber.d.ts +150 -0
- package/dist/ChannelProber.d.ts.map +1 -0
- package/dist/ChannelProber.js +887 -0
- package/dist/ChannelProber.js.map +1 -0
- package/dist/CloudEscalationPolicy.d.ts +143 -0
- package/dist/CloudEscalationPolicy.d.ts.map +1 -0
- package/dist/CloudEscalationPolicy.js +137 -0
- package/dist/CloudEscalationPolicy.js.map +1 -0
- package/dist/CloudSensingManager.d.ts +533 -0
- package/dist/CloudSensingManager.d.ts.map +1 -0
- package/dist/CloudSensingManager.js +1254 -0
- package/dist/CloudSensingManager.js.map +1 -0
- package/dist/DiscoveryReportClient.d.ts +66 -0
- package/dist/DiscoveryReportClient.d.ts.map +1 -0
- package/dist/DiscoveryReportClient.js +85 -0
- package/dist/DiscoveryReportClient.js.map +1 -0
- package/dist/EdgeAgent.d.ts +12 -2
- package/dist/EdgeAgent.d.ts.map +1 -1
- package/dist/EdgeAgent.js +122 -65
- package/dist/EdgeAgent.js.map +1 -1
- package/dist/LiteRtVlmProvider.d.ts +193 -0
- package/dist/LiteRtVlmProvider.d.ts.map +1 -0
- package/dist/LiteRtVlmProvider.js +436 -0
- package/dist/LiteRtVlmProvider.js.map +1 -0
- package/dist/MosaicSource.d.ts +108 -0
- package/dist/MosaicSource.d.ts.map +1 -0
- package/dist/MosaicSource.js +274 -0
- package/dist/MosaicSource.js.map +1 -0
- package/dist/OnvifClient.d.ts +83 -0
- package/dist/OnvifClient.d.ts.map +1 -0
- package/dist/OnvifClient.js +209 -0
- package/dist/OnvifClient.js.map +1 -0
- package/dist/PairClient.d.ts +106 -0
- package/dist/PairClient.d.ts.map +1 -0
- package/dist/PairClient.js +186 -0
- package/dist/PairClient.js.map +1 -0
- package/dist/RecorderAddressRecovery.d.ts +53 -0
- package/dist/RecorderAddressRecovery.d.ts.map +1 -0
- package/dist/RecorderAddressRecovery.js +359 -0
- package/dist/RecorderAddressRecovery.js.map +1 -0
- package/dist/SceneChangeDetector.d.ts +291 -0
- package/dist/SceneChangeDetector.d.ts.map +1 -0
- package/dist/SceneChangeDetector.js +682 -0
- package/dist/SceneChangeDetector.js.map +1 -0
- package/dist/SensingProfileClient.d.ts +59 -0
- package/dist/SensingProfileClient.d.ts.map +1 -0
- package/dist/SensingProfileClient.js +78 -0
- package/dist/SensingProfileClient.js.map +1 -0
- package/dist/SetupSessionClient.d.ts +114 -0
- package/dist/SetupSessionClient.d.ts.map +1 -0
- package/dist/SetupSessionClient.js +95 -0
- package/dist/SetupSessionClient.js.map +1 -0
- package/dist/VlmAdmissionGate.d.ts +126 -0
- package/dist/VlmAdmissionGate.d.ts.map +1 -0
- package/dist/VlmAdmissionGate.js +166 -0
- package/dist/VlmAdmissionGate.js.map +1 -0
- package/dist/autoSense.d.ts +216 -0
- package/dist/autoSense.d.ts.map +1 -0
- package/dist/autoSense.js +521 -0
- package/dist/autoSense.js.map +1 -0
- package/dist/cli.d.ts +27 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +511 -21
- package/dist/cli.js.map +1 -1
- package/dist/combinedSense.d.ts +128 -0
- package/dist/combinedSense.d.ts.map +1 -0
- package/dist/combinedSense.js +261 -0
- package/dist/combinedSense.js.map +1 -0
- package/dist/config.d.ts +351 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +99 -2
- package/dist/config.js.map +1 -1
- package/dist/connectorCadence.d.ts +135 -0
- package/dist/connectorCadence.d.ts.map +1 -0
- package/dist/connectorCadence.js +151 -0
- package/dist/connectorCadence.js.map +1 -0
- package/dist/dataDir.d.ts +33 -0
- package/dist/dataDir.d.ts.map +1 -0
- package/dist/dataDir.js +76 -0
- package/dist/dataDir.js.map +1 -0
- package/dist/deployedCadence.d.ts +167 -0
- package/dist/deployedCadence.d.ts.map +1 -0
- package/dist/deployedCadence.js +209 -0
- package/dist/deployedCadence.js.map +1 -0
- package/dist/deployedCadenceSource.d.ts +48 -0
- package/dist/deployedCadenceSource.d.ts.map +1 -0
- package/dist/deployedCadenceSource.js +74 -0
- package/dist/deployedCadenceSource.js.map +1 -0
- package/dist/diagnose.d.ts +1 -1
- package/dist/diagnose.d.ts.map +1 -1
- package/dist/diagnose.js +13 -12
- package/dist/diagnose.js.map +1 -1
- package/dist/discovery.d.ts +131 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +499 -0
- package/dist/discovery.js.map +1 -0
- package/dist/gemmaModelOta.d.ts +118 -0
- package/dist/gemmaModelOta.d.ts.map +1 -0
- package/dist/gemmaModelOta.js +291 -0
- package/dist/gemmaModelOta.js.map +1 -0
- package/dist/index.d.ts +16 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +40 -1
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +41 -18
- package/dist/init.js.map +1 -1
- package/dist/keepAwake.d.ts +33 -0
- package/dist/keepAwake.d.ts.map +1 -0
- package/dist/keepAwake.js +54 -0
- package/dist/keepAwake.js.map +1 -0
- package/dist/legacyRecorderProfile.d.ts +20 -0
- package/dist/legacyRecorderProfile.d.ts.map +1 -0
- package/dist/legacyRecorderProfile.js +27 -0
- package/dist/legacyRecorderProfile.js.map +1 -0
- package/dist/localInferenceSchema.d.ts +132 -0
- package/dist/localInferenceSchema.d.ts.map +1 -0
- package/dist/localInferenceSchema.js +274 -0
- package/dist/localInferenceSchema.js.map +1 -0
- package/dist/localSenseE2eHarness.d.ts +101 -0
- package/dist/localSenseE2eHarness.d.ts.map +1 -0
- package/dist/localSenseE2eHarness.js +279 -0
- package/dist/localSenseE2eHarness.js.map +1 -0
- package/dist/localSensing.d.ts +268 -0
- package/dist/localSensing.d.ts.map +1 -0
- package/dist/localSensing.js +492 -0
- package/dist/localSensing.js.map +1 -0
- package/dist/modelIntegrity.d.ts +59 -0
- package/dist/modelIntegrity.d.ts.map +1 -0
- package/dist/modelIntegrity.js +165 -0
- package/dist/modelIntegrity.js.map +1 -0
- package/dist/pairVerify.d.ts +100 -0
- package/dist/pairVerify.d.ts.map +1 -0
- package/dist/pairVerify.js +296 -0
- package/dist/pairVerify.js.map +1 -0
- package/dist/qrScanner.d.ts +33 -0
- package/dist/qrScanner.d.ts.map +1 -0
- package/dist/qrScanner.js +103 -0
- package/dist/qrScanner.js.map +1 -0
- package/dist/runtimePaths.d.ts +10 -0
- package/dist/runtimePaths.d.ts.map +1 -0
- package/dist/runtimePaths.js +79 -0
- package/dist/runtimePaths.js.map +1 -0
- package/dist/sensingRunMode.d.ts +68 -0
- package/dist/sensingRunMode.d.ts.map +1 -0
- package/dist/sensingRunMode.js +346 -0
- package/dist/sensingRunMode.js.map +1 -0
- package/dist/setupMode.d.ts +98 -0
- package/dist/setupMode.d.ts.map +1 -0
- package/dist/setupMode.js +828 -0
- package/dist/setupMode.js.map +1 -0
- package/dist/setupProgress.d.ts +31 -0
- package/dist/setupProgress.d.ts.map +1 -0
- package/dist/setupProgress.js +173 -0
- package/dist/setupProgress.js.map +1 -0
- package/package.json +13 -4
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.RecorderAddressRecovery = void 0;
|
|
37
|
+
exports.normalizeMacAddress = normalizeMacAddress;
|
|
38
|
+
exports.lookupMacAddress = lookupMacAddress;
|
|
39
|
+
exports.rewriteStreamHost = rewriteStreamHost;
|
|
40
|
+
exports.shouldAttemptRecorderAddressRecovery = shouldAttemptRecorderAddressRecovery;
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const child_process_1 = require("child_process");
|
|
44
|
+
const ChannelProber_1 = require("./ChannelProber");
|
|
45
|
+
const discovery_1 = require("./discovery");
|
|
46
|
+
const OnvifClient_1 = require("./OnvifClient");
|
|
47
|
+
const DEFAULT_RECOVERY_COOLDOWN_MS = 60000;
|
|
48
|
+
const DEFAULT_VALIDATION_TIMEOUT_MS = 8000;
|
|
49
|
+
const ADDRESS_FAILURE_RE = /(timed? out|timeout|econnrefused|connection refused|enetunreach|ehostunreach|network is unreachable|host is unreachable|no route to host|could not resolve|getaddrinfo|not found|\b404\b)/i;
|
|
50
|
+
function normalizeMacAddress(value) {
|
|
51
|
+
const match = String(value || '').match(/(?:^|[^0-9a-f])([0-9a-f]{1,2}(?:[:-][0-9a-f]{1,2}){5})(?![0-9a-f])/i);
|
|
52
|
+
if (!match)
|
|
53
|
+
return null;
|
|
54
|
+
const normalized = match[1]
|
|
55
|
+
.split(/[:-]/)
|
|
56
|
+
.map((octet) => octet.padStart(2, '0'))
|
|
57
|
+
.join(':')
|
|
58
|
+
.toLowerCase();
|
|
59
|
+
return normalized === '00:00:00:00:00:00' ? null : normalized;
|
|
60
|
+
}
|
|
61
|
+
function runAddressCommand(executable, args) {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
(0, child_process_1.execFile)(executable, args, { timeout: 2000, maxBuffer: 64 * 1024, windowsHide: true }, (error, stdout) => error ? reject(error) : resolve(String(stdout || '')));
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/** Read one already-contacted LAN host from the operating system's neighbor table. */
|
|
67
|
+
async function lookupMacAddress(host) {
|
|
68
|
+
const commands = process.platform === 'win32'
|
|
69
|
+
? [['arp.exe', ['-a', host]]]
|
|
70
|
+
: process.platform === 'darwin'
|
|
71
|
+
? [['/usr/sbin/arp', ['-n', host]]]
|
|
72
|
+
: [
|
|
73
|
+
['/usr/sbin/ip', ['neigh', 'show', host]],
|
|
74
|
+
['ip', ['neigh', 'show', host]],
|
|
75
|
+
['/usr/sbin/arp', ['-n', host]],
|
|
76
|
+
['arp', ['-n', host]],
|
|
77
|
+
];
|
|
78
|
+
for (const [executable, args] of commands) {
|
|
79
|
+
try {
|
|
80
|
+
const mac = normalizeMacAddress(await runAddressCommand(executable, [...args]));
|
|
81
|
+
if (mac)
|
|
82
|
+
return mac;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Try the next platform spelling.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
function decoded(value) {
|
|
91
|
+
try {
|
|
92
|
+
return decodeURIComponent(value);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function credentialsFromUrl(url) {
|
|
99
|
+
try {
|
|
100
|
+
const parsed = new URL(url);
|
|
101
|
+
if (!parsed.username)
|
|
102
|
+
return null;
|
|
103
|
+
return { username: decoded(parsed.username), password: decoded(parsed.password) };
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function hostFromUrl(url) {
|
|
110
|
+
try {
|
|
111
|
+
return new URL(url).hostname || null;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** Replace only the host, preserving credentials, port, path, query, and RTSPS. */
|
|
118
|
+
function rewriteStreamHost(streamUrl, host) {
|
|
119
|
+
const parsed = new URL(streamUrl);
|
|
120
|
+
parsed.hostname = host;
|
|
121
|
+
return parsed.toString();
|
|
122
|
+
}
|
|
123
|
+
function shouldAttemptRecorderAddressRecovery(error) {
|
|
124
|
+
const message = error instanceof Error ? error.message : String(error ?? '');
|
|
125
|
+
return ADDRESS_FAILURE_RE.test(message);
|
|
126
|
+
}
|
|
127
|
+
function onvifPorts(candidate) {
|
|
128
|
+
const ports = new Set();
|
|
129
|
+
for (const xaddr of candidate.xaddrs || []) {
|
|
130
|
+
try {
|
|
131
|
+
const parsed = new URL(xaddr);
|
|
132
|
+
ports.add(Number(parsed.port) || (parsed.protocol === 'https:' ? 443 : 80));
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Keep the conventional fallbacks below.
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
ports.add(80);
|
|
139
|
+
ports.add(8899);
|
|
140
|
+
return [...ports];
|
|
141
|
+
}
|
|
142
|
+
async function defaultSerialMatch(candidate, expectedSerial, credentials) {
|
|
143
|
+
for (const port of onvifPorts(candidate)) {
|
|
144
|
+
try {
|
|
145
|
+
const info = await new OnvifClient_1.OnvifClient({
|
|
146
|
+
host: candidate.host,
|
|
147
|
+
port,
|
|
148
|
+
username: credentials.username,
|
|
149
|
+
password: credentials.password,
|
|
150
|
+
timeoutMs: 5000,
|
|
151
|
+
}).getDeviceInformation();
|
|
152
|
+
if (String(info.serialNumber || '').trim() === expectedSerial)
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// A candidate that is not this recorder is normal during LAN discovery.
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Finds a recorder again after DHCP changes its address.
|
|
163
|
+
*
|
|
164
|
+
* Recovery is deliberately conservative: the MAC address must match before
|
|
165
|
+
* credentials are used. An ONVIF serial is then verified when setup captured
|
|
166
|
+
* one; otherwise an existing channel recipe validates the matched recorder.
|
|
167
|
+
* Credentials are never tried against an unmatched LAN device.
|
|
168
|
+
*/
|
|
169
|
+
class RecorderAddressRecovery {
|
|
170
|
+
constructor(config, options = {}) {
|
|
171
|
+
this.groups = new Map();
|
|
172
|
+
this.currentHost = new Map();
|
|
173
|
+
this.inFlight = new Map();
|
|
174
|
+
this.lastFailedAt = new Map();
|
|
175
|
+
this.configPath = options.configPath;
|
|
176
|
+
this.log = options.log ?? (() => undefined);
|
|
177
|
+
this.cooldownMs = Math.max(1000, options.cooldownMs ?? DEFAULT_RECOVERY_COOLDOWN_MS);
|
|
178
|
+
this.now = options.now ?? Date.now;
|
|
179
|
+
this.discover = options.discover ?? discovery_1.discoverOnvifHosts;
|
|
180
|
+
this.sweep = options.sweep ?? discovery_1.sweepLanForRecorders;
|
|
181
|
+
this.matchSerial = options.matchSerial ?? defaultSerialMatch;
|
|
182
|
+
this.lookupMac = options.lookupMac ?? lookupMacAddress;
|
|
183
|
+
this.validateStream = options.validateStream ?? (async (url, feed) => {
|
|
184
|
+
const result = await (0, ChannelProber_1.grabOneFrameAnyTransport)(url, {
|
|
185
|
+
ffmpegPath: feed.ffmpegPath,
|
|
186
|
+
timeoutMs: DEFAULT_VALIDATION_TIMEOUT_MS,
|
|
187
|
+
});
|
|
188
|
+
return result.ok;
|
|
189
|
+
});
|
|
190
|
+
for (const camera of config.cameras || []) {
|
|
191
|
+
if (camera.sourceType !== 'recorder' || !camera.recorder?.id || !camera.rtspUrl)
|
|
192
|
+
continue;
|
|
193
|
+
const id = camera.recorder.id;
|
|
194
|
+
const group = this.groups.get(id) || { identity: camera.recorder, cameras: [] };
|
|
195
|
+
group.cameras.push(camera);
|
|
196
|
+
this.groups.set(id, group);
|
|
197
|
+
const host = hostFromUrl(camera.rtspUrl);
|
|
198
|
+
if (host && !this.currentHost.has(id))
|
|
199
|
+
this.currentHost.set(id, host);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
canRecover(feed) {
|
|
203
|
+
if (!feed.recorder?.id)
|
|
204
|
+
return false;
|
|
205
|
+
const identity = this.groups.get(feed.recorder.id)?.identity;
|
|
206
|
+
return !!identity && !!normalizeMacAddress(identity.mac || '');
|
|
207
|
+
}
|
|
208
|
+
async recover(feed, currentUrl, error) {
|
|
209
|
+
const recorderId = feed.recorder?.id;
|
|
210
|
+
if (!recorderId || !this.groups.has(recorderId))
|
|
211
|
+
return null;
|
|
212
|
+
if (!shouldAttemptRecorderAddressRecovery(error))
|
|
213
|
+
return null;
|
|
214
|
+
if (!this.canRecover(feed))
|
|
215
|
+
return null;
|
|
216
|
+
const knownHost = this.currentHost.get(recorderId);
|
|
217
|
+
const failedHost = hostFromUrl(currentUrl);
|
|
218
|
+
if (knownHost && failedHost && knownHost !== failedHost) {
|
|
219
|
+
return rewriteStreamHost(currentUrl, knownHost);
|
|
220
|
+
}
|
|
221
|
+
const active = this.inFlight.get(recorderId);
|
|
222
|
+
if (active) {
|
|
223
|
+
const host = await active;
|
|
224
|
+
return host ? rewriteStreamHost(currentUrl, host) : null;
|
|
225
|
+
}
|
|
226
|
+
const lastFailure = this.lastFailedAt.get(recorderId) || 0;
|
|
227
|
+
if (this.now() - lastFailure < this.cooldownMs)
|
|
228
|
+
return null;
|
|
229
|
+
const attempt = this.findCurrentHost(this.groups.get(recorderId), failedHost)
|
|
230
|
+
.then((host) => {
|
|
231
|
+
if (!host) {
|
|
232
|
+
this.lastFailedAt.set(recorderId, this.now());
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
this.currentHost.set(recorderId, host);
|
|
236
|
+
this.updateGroupUrls(recorderId, host);
|
|
237
|
+
this.persistGroupUrls(recorderId, host);
|
|
238
|
+
this.log(`[recorder] DHCP address changed to ${host}; updated every channel.`);
|
|
239
|
+
return host;
|
|
240
|
+
})
|
|
241
|
+
.finally(() => this.inFlight.delete(recorderId));
|
|
242
|
+
this.inFlight.set(recorderId, attempt);
|
|
243
|
+
const host = await attempt;
|
|
244
|
+
return host ? rewriteStreamHost(currentUrl, host) : null;
|
|
245
|
+
}
|
|
246
|
+
async findCurrentHost(group, failedHost) {
|
|
247
|
+
const [onvifResult, sweepResult] = await Promise.allSettled([
|
|
248
|
+
this.discover({ timeoutMs: 4000 }),
|
|
249
|
+
this.sweep({}),
|
|
250
|
+
]);
|
|
251
|
+
const onvif = onvifResult.status === 'fulfilled' ? onvifResult.value : [];
|
|
252
|
+
const swept = sweepResult.status === 'fulfilled' ? sweepResult.value : [];
|
|
253
|
+
const candidates = new Map();
|
|
254
|
+
for (const candidate of onvif)
|
|
255
|
+
candidates.set(candidate.host, candidate);
|
|
256
|
+
for (const candidate of swept) {
|
|
257
|
+
if (!candidates.has(candidate.host)) {
|
|
258
|
+
candidates.set(candidate.host, {
|
|
259
|
+
host: candidate.host,
|
|
260
|
+
xaddrs: candidate.xaddrs || [],
|
|
261
|
+
scopes: [],
|
|
262
|
+
name: candidate.name,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (failedHost)
|
|
267
|
+
candidates.delete(failedHost);
|
|
268
|
+
const expectedMac = normalizeMacAddress(group.identity.mac || '');
|
|
269
|
+
if (!expectedMac)
|
|
270
|
+
return null;
|
|
271
|
+
const firstUrl = group.cameras.find((camera) => !!camera.rtspUrl)?.rtspUrl || '';
|
|
272
|
+
const credentials = credentialsFromUrl(firstUrl);
|
|
273
|
+
for (const candidate of candidates.values()) {
|
|
274
|
+
const candidateMac = normalizeMacAddress((await this.lookupMac(candidate.host)) || '');
|
|
275
|
+
if (candidateMac !== expectedMac)
|
|
276
|
+
continue;
|
|
277
|
+
if (group.identity.serial) {
|
|
278
|
+
if (!credentials)
|
|
279
|
+
return null;
|
|
280
|
+
return await this.matchSerial(candidate, group.identity.serial, credentials)
|
|
281
|
+
? candidate.host
|
|
282
|
+
: null;
|
|
283
|
+
}
|
|
284
|
+
// Hardware identity matched. Prove at least one existing channel before
|
|
285
|
+
// changing all live URLs; credentials are sent only to this matched host.
|
|
286
|
+
for (const camera of group.cameras) {
|
|
287
|
+
if (!camera.rtspUrl)
|
|
288
|
+
continue;
|
|
289
|
+
try {
|
|
290
|
+
if (await this.validateStream(rewriteStreamHost(camera.rtspUrl, candidate.host), camera)) {
|
|
291
|
+
return candidate.host;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
// Try another known-good channel on the same recorder.
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
updateGroupUrls(recorderId, host) {
|
|
303
|
+
const group = this.groups.get(recorderId);
|
|
304
|
+
if (!group)
|
|
305
|
+
return;
|
|
306
|
+
for (const camera of group.cameras) {
|
|
307
|
+
if (camera.rtspUrl)
|
|
308
|
+
camera.rtspUrl = rewriteStreamHost(camera.rtspUrl, host);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
persistGroupUrls(recorderId, host) {
|
|
312
|
+
if (!this.configPath)
|
|
313
|
+
return;
|
|
314
|
+
const resolved = path.resolve(this.configPath);
|
|
315
|
+
let parsed;
|
|
316
|
+
try {
|
|
317
|
+
parsed = JSON.parse(fs.readFileSync(resolved, 'utf8'));
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
this.log(`[recorder] Reconnected at ${host}, but could not read the local config: ${error.message}`);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (!Array.isArray(parsed.cameras))
|
|
324
|
+
return;
|
|
325
|
+
parsed.cameras = parsed.cameras.map((camera) => {
|
|
326
|
+
if (camera.recorder?.id !== recorderId || !camera.rtspUrl)
|
|
327
|
+
return camera;
|
|
328
|
+
return { ...camera, rtspUrl: rewriteStreamHost(camera.rtspUrl, host) };
|
|
329
|
+
});
|
|
330
|
+
const tempPath = `${resolved}.tmp-${process.pid}-${this.now()}`;
|
|
331
|
+
try {
|
|
332
|
+
fs.writeFileSync(tempPath, JSON.stringify(parsed, null, 2) + '\n', { mode: 0o600 });
|
|
333
|
+
try {
|
|
334
|
+
fs.chmodSync(tempPath, 0o600);
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
// Windows ACLs do not use POSIX modes.
|
|
338
|
+
}
|
|
339
|
+
fs.renameSync(tempPath, resolved);
|
|
340
|
+
try {
|
|
341
|
+
fs.chmodSync(resolved, 0o600);
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
// Best effort on Windows.
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
try {
|
|
349
|
+
fs.unlinkSync(tempPath);
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
// Already moved or never created.
|
|
353
|
+
}
|
|
354
|
+
this.log(`[recorder] Reconnected at ${host}, but could not save the new address: ${error.message}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
exports.RecorderAddressRecovery = RecorderAddressRecovery;
|
|
359
|
+
//# sourceMappingURL=RecorderAddressRecovery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RecorderAddressRecovery.js","sourceRoot":"","sources":["../src/RecorderAddressRecovery.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,kDASC;AAcD,4CAoBC;AA6BD,8CAIC;AAED,oFAGC;AApHD,uCAAyB;AACzB,2CAA6B;AAC7B,iDAAyC;AAGzC,mDAA2D;AAC3D,2CAAuF;AACvF,+CAA4C;AAE5C,MAAM,4BAA4B,GAAG,KAAM,CAAC;AAC5C,MAAM,6BAA6B,GAAG,IAAK,CAAC;AAC5C,MAAM,kBAAkB,GACtB,4LAA4L,CAAC;AAuB/L,SAAgB,mBAAmB,CAAC,KAAa;IAC/C,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,qEAAqE,CAAC,CAAC;IAC/G,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC;SACxB,KAAK,CAAC,MAAM,CAAC;SACb,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;SACtC,IAAI,CAAC,GAAG,CAAC;SACT,WAAW,EAAE,CAAC;IACjB,OAAO,UAAU,KAAK,mBAAmB,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC;AAChE,CAAC;AAED,SAAS,iBAAiB,CAAC,UAAkB,EAAE,IAAc;IAC3D,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,IAAA,wBAAQ,EACN,UAAU,EACV,IAAI,EACJ,EAAE,OAAO,EAAE,IAAK,EAAE,SAAS,EAAE,EAAE,GAAG,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,EAC3D,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,CACzE,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED,sFAAsF;AAC/E,KAAK,UAAU,gBAAgB,CAAC,IAAY;IACjD,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO;QAC3C,CAAC,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,CAAU,CAAC;QACtC,CAAC,CAAC,OAAO,CAAC,QAAQ,KAAK,QAAQ;YAC7B,CAAC,CAAC,CAAC,CAAC,eAAe,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,CAAU,CAAC;YAC5C,CAAC,CAAC;gBACE,CAAC,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,CAAU;gBAClD,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,CAAU;gBACxC,CAAC,eAAe,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,CAAU;gBACxC,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,CAAU;aAC/B,CAAC;IACR,KAAK,MAAM,CAAC,UAAU,EAAE,IAAI,CAAC,IAAI,QAAQ,EAAE,CAAC;QAC1C,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,mBAAmB,CAAC,MAAM,iBAAiB,CAAC,UAAU,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;YAChF,IAAI,GAAG;gBAAE,OAAO,GAAG,CAAC;QACtB,CAAC;QAAC,MAAM,CAAC;YACP,kCAAkC;QACpC,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,OAAO,CAAC,KAAa;IAC5B,IAAI,CAAC;QACH,OAAO,kBAAkB,CAAC,KAAK,CAAC,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,SAAS,kBAAkB,CAAC,GAAW;IACrC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QAC5B,IAAI,CAAC,MAAM,CAAC,QAAQ;YAAE,OAAO,IAAI,CAAC;QAClC,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;IACpF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAS,WAAW,CAAC,GAAW;IAC9B,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,IAAI,IAAI,CAAC;IACvC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,mFAAmF;AACnF,SAAgB,iBAAiB,CAAC,SAAiB,EAAE,IAAY;IAC/D,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC;IAClC,MAAM,CAAC,QAAQ,GAAG,IAAI,CAAC;IACvB,OAAO,MAAM,CAAC,QAAQ,EAAE,CAAC;AAC3B,CAAC;AAED,SAAgB,oCAAoC,CAAC,KAAc;IACjE,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;IAC7E,OAAO,kBAAkB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AAC1C,CAAC;AAED,SAAS,UAAU,CAAC,SAAoB;IACtC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,KAAK,MAAM,KAAK,IAAI,SAAS,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC;QAC3C,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;YAC9B,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAC9E,CAAC;QAAC,MAAM,CAAC;YACP,yCAAyC;QAC3C,CAAC;IACH,CAAC;IACD,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACd,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChB,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC;AACpB,CAAC;AAED,KAAK,UAAU,kBAAkB,CAC/B,SAAoB,EACpB,cAAsB,EACtB,WAAmD;IAEnD,KAAK,MAAM,IAAI,IAAI,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACzC,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,IAAI,yBAAW,CAAC;gBACjC,IAAI,EAAE,SAAS,CAAC,IAAI;gBACpB,IAAI;gBACJ,QAAQ,EAAE,WAAW,CAAC,QAAQ;gBAC9B,QAAQ,EAAE,WAAW,CAAC,QAAQ;gBAC9B,SAAS,EAAE,IAAK;aACjB,CAAC,CAAC,oBAAoB,EAAE,CAAC;YAC1B,IAAI,MAAM,CAAC,IAAI,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,cAAc;gBAAE,OAAO,IAAI,CAAC;QAC7E,CAAC;QAAC,MAAM,CAAC;YACP,wEAAwE;QAC1E,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;GAOG;AACH,MAAa,uBAAuB;IAelC,YAAY,MAAuB,EAAE,UAA0C,EAAE;QAdhE,WAAM,GAAG,IAAI,GAAG,EAAyB,CAAC;QAC1C,gBAAW,GAAG,IAAI,GAAG,EAAkB,CAAC;QACxC,aAAQ,GAAG,IAAI,GAAG,EAAkC,CAAC;QACrD,iBAAY,GAAG,IAAI,GAAG,EAAkB,CAAC;QAYxD,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;QACrC,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAC5C,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,IAAK,EAAE,OAAO,CAAC,UAAU,IAAI,4BAA4B,CAAC,CAAC;QACtF,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;QACnC,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,8BAAkB,CAAC;QACvD,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,gCAAoB,CAAC;QACnD,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,kBAAkB,CAAC;QAC7D,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,gBAAgB,CAAC;QACvD,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC,cAAc,IAAI,CAAC,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;YACnE,MAAM,MAAM,GAAG,MAAM,IAAA,wCAAwB,EAAC,GAAG,EAAE;gBACjD,UAAU,EAAE,IAAI,CAAC,UAAU;gBAC3B,SAAS,EAAE,6BAA6B;aACzC,CAAC,CAAC;YACH,OAAO,MAAM,CAAC,EAAE,CAAC;QACnB,CAAC,CAAC,CAAC;QAEH,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC;YAC1C,IAAI,MAAM,CAAC,UAAU,KAAK,UAAU,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO;gBAAE,SAAS;YAC1F,MAAM,EAAE,GAAG,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC9B,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;YAChF,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YAC3B,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;YAC3B,MAAM,IAAI,GAAG,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YACzC,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC;gBAAE,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;QACxE,CAAC;IACH,CAAC;IAED,UAAU,CAAC,IAAkB;QAC3B,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE;YAAE,OAAO,KAAK,CAAC;QACrC,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,QAAQ,CAAC;QAC7D,OAAO,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,mBAAmB,CAAC,QAAQ,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC;IACjE,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,IAAkB,EAAE,UAAkB,EAAE,KAAc;QAClE,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,EAAE,EAAE,CAAC;QACrC,IAAI,CAAC,UAAU,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC;YAAE,OAAO,IAAI,CAAC;QAC7D,IAAI,CAAC,oCAAoC,CAAC,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAC9D,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;QAExC,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACnD,MAAM,UAAU,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC;QAC3C,IAAI,SAAS,IAAI,UAAU,IAAI,SAAS,KAAK,UAAU,EAAE,CAAC;YACxD,OAAO,iBAAiB,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;QAClD,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAC7C,IAAI,MAAM,EAAE,CAAC;YACX,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC;YAC1B,OAAO,IAAI,CAAC,CAAC,CAAC,iBAAiB,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAC3D,CAAC;QAED,MAAM,WAAW,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QAC3D,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,WAAW,GAAG,IAAI,CAAC,UAAU;YAAE,OAAO,IAAI,CAAC;QAE5D,MAAM,OAAO,GAAG,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAE,EAAE,UAAU,CAAC;aAC3E,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;YACb,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;gBAC9C,OAAO,IAAI,CAAC;YACd,CAAC;YACD,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;YACvC,IAAI,CAAC,eAAe,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;YACvC,IAAI,CAAC,gBAAgB,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;YACxC,IAAI,CAAC,GAAG,CAAC,sCAAsC,IAAI,0BAA0B,CAAC,CAAC;YAC/E,OAAO,IAAI,CAAC;QACd,CAAC,CAAC;aACD,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC;QACnD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QAEvC,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC;QAC3B,OAAO,IAAI,CAAC,CAAC,CAAC,iBAAiB,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC3D,CAAC;IAEO,KAAK,CAAC,eAAe,CAAC,KAAoB,EAAE,UAAyB;QAC3E,MAAM,CAAC,WAAW,EAAE,WAAW,CAAC,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC;YAC1D,IAAI,CAAC,QAAQ,CAAC,EAAE,SAAS,EAAE,IAAK,EAAE,CAAC;YACnC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;SACf,CAAC,CAAC;QACH,MAAM,KAAK,GAAG,WAAW,CAAC,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1E,MAAM,KAAK,GAAG,WAAW,CAAC,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1E,MAAM,UAAU,GAAG,IAAI,GAAG,EAAqB,CAAC;QAChD,KAAK,MAAM,SAAS,IAAI,KAAK;YAAE,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;QACzE,KAAK,MAAM,SAAS,IAAI,KAAK,EAAE,CAAC;YAC9B,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;gBACpC,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE;oBAC7B,IAAI,EAAE,SAAS,CAAC,IAAI;oBACpB,MAAM,EAAE,SAAS,CAAC,MAAM,IAAI,EAAE;oBAC9B,MAAM,EAAE,EAAE;oBACV,IAAI,EAAE,SAAS,CAAC,IAAI;iBACrB,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QACD,IAAI,UAAU;YAAE,UAAU,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QAE9C,MAAM,WAAW,GAAG,mBAAmB,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC;QAClE,IAAI,CAAC,WAAW;YAAE,OAAO,IAAI,CAAC;QAC9B,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,OAAO,IAAI,EAAE,CAAC;QACjF,MAAM,WAAW,GAAG,kBAAkB,CAAC,QAAQ,CAAC,CAAC;QACjD,KAAK,MAAM,SAAS,IAAI,UAAU,CAAC,MAAM,EAAE,EAAE,CAAC;YAC5C,MAAM,YAAY,GAAG,mBAAmB,CAAC,CAAC,MAAM,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;YACvF,IAAI,YAAY,KAAK,WAAW;gBAAE,SAAS;YAC3C,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;gBAC1B,IAAI,CAAC,WAAW;oBAAE,OAAO,IAAI,CAAC;gBAC9B,OAAO,MAAM,IAAI,CAAC,WAAW,CAAC,SAAS,EAAE,KAAK,CAAC,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;oBAC1E,CAAC,CAAC,SAAS,CAAC,IAAI;oBAChB,CAAC,CAAC,IAAI,CAAC;YACX,CAAC;YACD,wEAAwE;YACxE,0EAA0E;YAC1E,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;gBACnC,IAAI,CAAC,MAAM,CAAC,OAAO;oBAAE,SAAS;gBAC9B,IAAI,CAAC;oBACH,IAAI,MAAM,IAAI,CAAC,cAAc,CAAC,iBAAiB,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,EAAE,CAAC;wBACzF,OAAO,SAAS,CAAC,IAAI,CAAC;oBACxB,CAAC;gBACH,CAAC;gBAAC,MAAM,CAAC;oBACP,uDAAuD;gBACzD,CAAC;YACH,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAEO,eAAe,CAAC,UAAkB,EAAE,IAAY;QACtD,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAC1C,IAAI,CAAC,KAAK;YAAE,OAAO;QACnB,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;YACnC,IAAI,MAAM,CAAC,OAAO;gBAAE,MAAM,CAAC,OAAO,GAAG,iBAAiB,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QAC/E,CAAC;IACH,CAAC;IAEO,gBAAgB,CAAC,UAAkB,EAAE,IAAY;QACvD,IAAI,CAAC,IAAI,CAAC,UAAU;YAAE,OAAO;QAC7B,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC/C,IAAI,MAA+B,CAAC;QACpC,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAA4B,CAAC;QACpF,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,GAAG,CAAC,6BAA6B,IAAI,0CAA2C,KAAe,CAAC,OAAO,EAAE,CAAC,CAAC;YAChH,OAAO;QACT,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC;YAAE,OAAO;QAC3C,MAAM,CAAC,OAAO,GAAI,MAAM,CAAC,OAA8B,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE;YACrE,IAAI,MAAM,CAAC,QAAQ,EAAE,EAAE,KAAK,UAAU,IAAI,CAAC,MAAM,CAAC,OAAO;gBAAE,OAAO,MAAM,CAAC;YACzE,OAAO,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,iBAAiB,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC;QACzE,CAAC,CAAC,CAAC;QAEH,MAAM,QAAQ,GAAG,GAAG,QAAQ,QAAQ,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;QAChE,IAAI,CAAC;YACH,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YACpF,IAAI,CAAC;gBACH,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;YAChC,CAAC;YAAC,MAAM,CAAC;gBACP,uCAAuC;YACzC,CAAC;YACD,EAAE,CAAC,UAAU,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;YAClC,IAAI,CAAC;gBACH,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;YAChC,CAAC;YAAC,MAAM,CAAC;gBACP,0BAA0B;YAC5B,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC;gBACH,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;YAC1B,CAAC;YAAC,MAAM,CAAC;gBACP,kCAAkC;YACpC,CAAC;YACD,IAAI,CAAC,GAAG,CAAC,6BAA6B,IAAI,yCAA0C,KAAe,CAAC,OAAO,EAAE,CAAC,CAAC;QACjH,CAAC;IACH,CAAC;CACF;AA3LD,0DA2LC"}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SceneChangeDetector + MotionGate — the connector's ONE motion implementation.
|
|
3
|
+
*
|
|
4
|
+
* This is the cheap stage of the cascade that stands in front of the expensive
|
|
5
|
+
* one: in a real store most frames are an empty aisle, and today every one of
|
|
6
|
+
* them is decoded, prompted and generated over by Gemma at 6–32 s a frame. The
|
|
7
|
+
* cheap stage answers "did anything change?" in single-digit milliseconds on a
|
|
8
|
+
* ~178x100 greyscale frame — about 116x fewer pixels than the 640x480 JPEG the
|
|
9
|
+
* VLM would otherwise chew through.
|
|
10
|
+
*
|
|
11
|
+
* PORTED FROM FRIGATE (MIT), `frigate/motion/improved_motion.py` +
|
|
12
|
+
* `frigate/config/camera/motion.py`, keeping its defaults and — more
|
|
13
|
+
* importantly — its two non-obvious behaviors:
|
|
14
|
+
*
|
|
15
|
+
* 1. THE BACKGROUND ONLY MOVES AFTER MOTION PERSISTS. `accumulateWeighted`
|
|
16
|
+
* runs every frame while the scene is still, but while motion is present
|
|
17
|
+
* it runs only once the motion has lasted `BACKGROUND_UPDATE_AFTER_FRAMES`
|
|
18
|
+
* (10) frames. Without that rule a slowly-moving object is absorbed into
|
|
19
|
+
* the background and becomes invisible mid-walk.
|
|
20
|
+
* 2. A CALIBRATION STATE MACHINE. A camera flipping its IR-cut filter at dusk
|
|
21
|
+
* changes every pixel at once; so does a cloud crossing the sun. Frigate
|
|
22
|
+
* treats "more than `lightningThreshold` of the frame moved" as a signal
|
|
23
|
+
* to re-enter calibration rather than as motion, and stays calibrating
|
|
24
|
+
* until the scene settles (<5% moved AND <=4 regions). We suppress
|
|
25
|
+
* VLM-triggering while calibrating for exactly that reason — otherwise
|
|
26
|
+
* dusk costs a 16-camera store one full inference storm per day.
|
|
27
|
+
*
|
|
28
|
+
* RELATIONSHIP TO THE ANDROID `SceneChangeDetector`. agent-core carries a class
|
|
29
|
+
* of the same name (`ctv/core/audience/SceneChangeDetector.kt`) built on
|
|
30
|
+
* luminance-HISTOGRAM chi-squared distance over Android `Bitmap`s. That is the
|
|
31
|
+
* right shape for a tablet whose camera frames a single face at arm's length,
|
|
32
|
+
* and the wrong shape here: a person crossing a wide CCTV frame moves a few
|
|
33
|
+
* percent of the pixels and barely perturbs a global histogram, which is why
|
|
34
|
+
* this port is region-based (threshold → dilate → connected components) instead.
|
|
35
|
+
* Two runtimes (Kotlin/Bitmap vs Node/Buffer) with two different jobs; there is
|
|
36
|
+
* exactly ONE motion implementation on each side, and this is the connector's.
|
|
37
|
+
*/
|
|
38
|
+
/** A single-channel 8-bit frame — what motion actually runs on. */
|
|
39
|
+
export interface GreyFrame {
|
|
40
|
+
width: number;
|
|
41
|
+
height: number;
|
|
42
|
+
/** Row-major, one byte per pixel. */
|
|
43
|
+
data: Uint8Array;
|
|
44
|
+
}
|
|
45
|
+
/** A motion region, in MOTION-FRAME pixels (not source pixels). */
|
|
46
|
+
export interface MotionBox {
|
|
47
|
+
x: number;
|
|
48
|
+
y: number;
|
|
49
|
+
w: number;
|
|
50
|
+
h: number;
|
|
51
|
+
/** Foreground pixel count in this region. */
|
|
52
|
+
area: number;
|
|
53
|
+
}
|
|
54
|
+
export interface MotionDetectResult {
|
|
55
|
+
/** At least one region survived the contour-area floor. */
|
|
56
|
+
motion: boolean;
|
|
57
|
+
boxes: MotionBox[];
|
|
58
|
+
/** Moving fraction of the frame, 0..1. */
|
|
59
|
+
pctMotion: number;
|
|
60
|
+
/** The detector does not yet trust its background model. */
|
|
61
|
+
calibrating: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Coarse fingerprint of WHERE the motion is. Two frames whose motion sits in
|
|
64
|
+
* the same cells share a signature — that is what makes a parked car or a
|
|
65
|
+
* swinging sign cheap (see MotionGate's stationary suppression).
|
|
66
|
+
*/
|
|
67
|
+
signature: string;
|
|
68
|
+
}
|
|
69
|
+
export interface SceneChangeDetectorOptions {
|
|
70
|
+
/** Height (px) motion runs at. Frigate default 100. */
|
|
71
|
+
frameHeight?: number;
|
|
72
|
+
/** Per-pixel delta that counts as changed. Frigate default 30. */
|
|
73
|
+
threshold?: number;
|
|
74
|
+
/** Minimum region size (motion-frame px) that counts. Frigate default 10. */
|
|
75
|
+
contourArea?: number;
|
|
76
|
+
/** Background blend rate once settled. Frigate default 0.01. */
|
|
77
|
+
frameAlpha?: number;
|
|
78
|
+
/** Background blend rate while calibrating. Frigate default 0.2. */
|
|
79
|
+
calibratingAlpha?: number;
|
|
80
|
+
/** Moving fraction above which the frame is a lighting event. Default 0.8. */
|
|
81
|
+
lightningThreshold?: number;
|
|
82
|
+
/** Percentile contrast stretch before differencing. Frigate default true. */
|
|
83
|
+
improveContrast?: boolean;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The Frigate motion core, one instance per camera feed. Stateful: it owns the
|
|
87
|
+
* running background model, the contrast history and the calibration state, so
|
|
88
|
+
* frames must be fed to the SAME instance in capture order.
|
|
89
|
+
*/
|
|
90
|
+
export declare class SceneChangeDetector {
|
|
91
|
+
private readonly frameHeight;
|
|
92
|
+
private readonly threshold;
|
|
93
|
+
private readonly contourArea;
|
|
94
|
+
private readonly frameAlpha;
|
|
95
|
+
private readonly calibratingAlpha;
|
|
96
|
+
private readonly lightningThreshold;
|
|
97
|
+
private readonly improveContrast;
|
|
98
|
+
private avgFrame;
|
|
99
|
+
private avgWidth;
|
|
100
|
+
private avgHeight;
|
|
101
|
+
private motionFrameCount;
|
|
102
|
+
private calibrating;
|
|
103
|
+
private readonly contrastMin;
|
|
104
|
+
private readonly contrastMax;
|
|
105
|
+
private contrastIndex;
|
|
106
|
+
/** False until the ring has been seeded from a real frame (see CONTRAST_HISTORY). */
|
|
107
|
+
private contrastSeeded;
|
|
108
|
+
private framesSeen;
|
|
109
|
+
constructor(options?: SceneChangeDetectorOptions);
|
|
110
|
+
/** Height (px) callers must downscale to before calling `detect`. */
|
|
111
|
+
getFrameHeight(): number;
|
|
112
|
+
isCalibrating(): boolean;
|
|
113
|
+
/** Frames fed so far (a feed with 0 has never produced a motion verdict). */
|
|
114
|
+
getFramesSeen(): number;
|
|
115
|
+
/**
|
|
116
|
+
* Forget the background model. Call when the view itself changed (camera
|
|
117
|
+
* re-opened on a different channel, connector resumed after a long stop) so a
|
|
118
|
+
* stale baseline cannot report a whole-frame "change" that never happened.
|
|
119
|
+
*/
|
|
120
|
+
reset(): void;
|
|
121
|
+
/**
|
|
122
|
+
* One motion verdict. `frame` must already be greyscale at `frameHeight`
|
|
123
|
+
* (see `decodeGreyFrame`), which is what keeps this stage ~116x cheaper than
|
|
124
|
+
* handing the full JPEG to the model.
|
|
125
|
+
*/
|
|
126
|
+
detect(frame: GreyFrame): MotionDetectResult;
|
|
127
|
+
/**
|
|
128
|
+
* Frigate's `improve_contrast`: stretch the 4th–96th percentile band to
|
|
129
|
+
* 0..255 using the MEAN of the last 50 readings, so one odd frame cannot
|
|
130
|
+
* yank the mapping and manufacture motion everywhere.
|
|
131
|
+
*/
|
|
132
|
+
private stretchContrast;
|
|
133
|
+
}
|
|
134
|
+
/** Why a frame was (or was not) sent to the model. */
|
|
135
|
+
export type MotionGateReason =
|
|
136
|
+
/** Periodic proof-of-life: a genuinely static scene still reports. */
|
|
137
|
+
'heartbeat'
|
|
138
|
+
/** The scene changed. */
|
|
139
|
+
| 'motion'
|
|
140
|
+
/** Nothing moved. */
|
|
141
|
+
| 'no_motion'
|
|
142
|
+
/** Background model not trusted yet (startup, or a lighting event). */
|
|
143
|
+
| 'calibrating'
|
|
144
|
+
/** Motion in the same places as the last analyzed frame (parked car, sign). */
|
|
145
|
+
| 'stationary'
|
|
146
|
+
/** Motion, but the last analysis is too recent. */
|
|
147
|
+
| 'rate_limited'
|
|
148
|
+
/** Frame could not be decoded — the gate fails OPEN and analyzes it. */
|
|
149
|
+
| 'undecodable';
|
|
150
|
+
export interface MotionGateResult {
|
|
151
|
+
analyze: boolean;
|
|
152
|
+
reason: MotionGateReason;
|
|
153
|
+
/** Scene is live — drives the periodic/face-triggered cadence switch. */
|
|
154
|
+
motion: boolean;
|
|
155
|
+
pctMotion: number;
|
|
156
|
+
boxes: MotionBox[];
|
|
157
|
+
}
|
|
158
|
+
export interface MotionGateStats {
|
|
159
|
+
evaluated: number;
|
|
160
|
+
analyzed: number;
|
|
161
|
+
skipped: number;
|
|
162
|
+
byReason: Record<MotionGateReason, number>;
|
|
163
|
+
calibrating: boolean;
|
|
164
|
+
framesSeen: number;
|
|
165
|
+
}
|
|
166
|
+
export interface MotionGateOptions extends SceneChangeDetectorOptions {
|
|
167
|
+
/**
|
|
168
|
+
* Analyze at least this often per feed no matter how still the scene is
|
|
169
|
+
* (ms). This is what keeps "nothing is happening" distinguishable from "the
|
|
170
|
+
* connector is dead", so it is never optional. Wired to the existing
|
|
171
|
+
* `periodicIntervalMs` cadence knob.
|
|
172
|
+
*/
|
|
173
|
+
heartbeatIntervalMs: number;
|
|
174
|
+
/**
|
|
175
|
+
* Floor between two motion-triggered analyses of one feed (ms). Wired to the
|
|
176
|
+
* existing `faceTriggeredIntervalMs` knob, so a busy feed can never analyze
|
|
177
|
+
* faster than the cadence the config already declares.
|
|
178
|
+
*/
|
|
179
|
+
minAnalyzeIntervalMs?: number;
|
|
180
|
+
/**
|
|
181
|
+
* How many consecutive same-place motion verdicts to suppress before letting
|
|
182
|
+
* one through. Frigate re-checks a stationary object every `fps * 10` frames
|
|
183
|
+
* instead of every frame; 10 is that factor. 0 disables suppression.
|
|
184
|
+
*/
|
|
185
|
+
stationaryReanalyzeEvery?: number;
|
|
186
|
+
/** Decoder override (tests / hosts without an image library). */
|
|
187
|
+
decode?: (jpeg: Buffer, targetHeight: number) => Promise<GreyFrame | null>;
|
|
188
|
+
/** Clock injection for tests. */
|
|
189
|
+
now?: () => number;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Per-feed cascade decision: decode cheap → motion → should this frame cost a
|
|
193
|
+
* VLM call? Owns the feed's `SceneChangeDetector`, its heartbeat clock and its
|
|
194
|
+
* stationary-suppression counter.
|
|
195
|
+
*
|
|
196
|
+
* FAILS OPEN. Any decode failure analyzes the frame. A motion gate that cannot
|
|
197
|
+
* see must not be allowed to silence a camera — the whole product is the alert
|
|
198
|
+
* that fires.
|
|
199
|
+
*/
|
|
200
|
+
export declare class MotionGate {
|
|
201
|
+
private readonly detector;
|
|
202
|
+
/**
|
|
203
|
+
* NOT readonly: the still-scene reporting floor tracks the cadence an operator
|
|
204
|
+
* deploys, and that can now change on a RUNNING connector. It is retuned in
|
|
205
|
+
* place rather than by rebuilding the gate — see `setHeartbeatIntervalMs`.
|
|
206
|
+
*/
|
|
207
|
+
private heartbeatIntervalMs;
|
|
208
|
+
private readonly minAnalyzeIntervalMs;
|
|
209
|
+
private readonly stationaryReanalyzeEvery;
|
|
210
|
+
private readonly decodeFrame;
|
|
211
|
+
private readonly now;
|
|
212
|
+
private lastAnalyzedAtMs;
|
|
213
|
+
private lastAnalyzedSignature;
|
|
214
|
+
private stationarySuppressions;
|
|
215
|
+
private evaluated;
|
|
216
|
+
private analyzed;
|
|
217
|
+
private readonly byReason;
|
|
218
|
+
constructor(options: MotionGateOptions);
|
|
219
|
+
/**
|
|
220
|
+
* Decide whether this JPEG earns an inference. Decision order matters:
|
|
221
|
+
* heartbeat first (proof-of-life outranks everything), then the motion
|
|
222
|
+
* verdict, then the suppressions that make a busy-but-boring scene cheap.
|
|
223
|
+
*/
|
|
224
|
+
evaluate(jpeg: Buffer, nowMs?: number): Promise<MotionGateResult>;
|
|
225
|
+
/**
|
|
226
|
+
* Confirm the frame really reached the model. Called by the manager only
|
|
227
|
+
* after `analyze()` resolves, so a frame the admission gate or the network
|
|
228
|
+
* dropped does not reset the heartbeat and silence the feed.
|
|
229
|
+
*/
|
|
230
|
+
markAnalyzed(nowMs?: number): void;
|
|
231
|
+
/** The still-scene reporting floor currently in force (ms). */
|
|
232
|
+
getHeartbeatIntervalMs(): number;
|
|
233
|
+
/**
|
|
234
|
+
* Retune the still-scene reporting floor WITHOUT forgetting anything.
|
|
235
|
+
*
|
|
236
|
+
* Changing a cadence is a statement about how often a still scene must report.
|
|
237
|
+
* It is NOT a statement that the scene changed, that the camera moved, or that
|
|
238
|
+
* the last analysis never happened — but rebuilding the gate says all three:
|
|
239
|
+
* a new `SceneChangeDetector` has no background model, is back in
|
|
240
|
+
* `calibrating`, has an un-seeded contrast ring (the exact state the
|
|
241
|
+
* CONTRAST_HISTORY seeding fix exists to avoid), and a null
|
|
242
|
+
* `lastAnalyzedAtMs`, which makes the very next look heartbeat-DUE and buys a
|
|
243
|
+
* forced inference per feed for a scene the gate had already proven still.
|
|
244
|
+
*
|
|
245
|
+
* The heartbeat is a scalar the next `evaluate()` reads. Moving it is the
|
|
246
|
+
* whole change.
|
|
247
|
+
*
|
|
248
|
+
* @returns true when the interval actually changed.
|
|
249
|
+
*/
|
|
250
|
+
setHeartbeatIntervalMs(intervalMs: number): boolean;
|
|
251
|
+
/** Forget the background model (camera re-opened on a different view). */
|
|
252
|
+
reset(): void;
|
|
253
|
+
getStats(): MotionGateStats;
|
|
254
|
+
private decide;
|
|
255
|
+
}
|
|
256
|
+
/** Test seam: forget which decoder was resolved. */
|
|
257
|
+
export declare function resetGreyDecoderCache(): void;
|
|
258
|
+
/**
|
|
259
|
+
* JPEG → greyscale at `targetHeight`, the ~116x pixel reduction the whole
|
|
260
|
+
* cascade rests on. `sharp` (a hard dependency of @trillboards/edge-sensing, so
|
|
261
|
+
* present wherever sensing runs) does it natively; `jpeg-js` is the pure-JS
|
|
262
|
+
* fallback. Returns null when neither is available, which makes the gate fail
|
|
263
|
+
* open rather than blind a camera.
|
|
264
|
+
*
|
|
265
|
+
* WHY NOT FRIGATE'S LITERAL `INTER_NEAREST`. Frigate picks nearest because
|
|
266
|
+
* `cv2.resize` nearest is the cheapest thing that exists and it blurs the
|
|
267
|
+
* result immediately afterwards anyway. sharp's "nearest" is not a true
|
|
268
|
+
* point-sample — it pre-shrinks on load — so asking for it buys none of
|
|
269
|
+
* Frigate's determinism and costs the two decoders their agreement (measured
|
|
270
|
+
* MAD 21.8 grey levels against a real point-sample, versus 4.2 for cubic
|
|
271
|
+
* against the fallback's box average). Both decoders low-pass before
|
|
272
|
+
* decimating, which is what the subsequent absdiff+threshold wants, and they
|
|
273
|
+
* land within ~4 grey levels of each other — far inside the threshold of 30, so
|
|
274
|
+
* a host with sharp and a host without cannot disagree about whether a scene
|
|
275
|
+
* moved.
|
|
276
|
+
*/
|
|
277
|
+
export declare function decodeGreyFrame(jpeg: Buffer, targetHeight: number): Promise<GreyFrame | null>;
|
|
278
|
+
/**
|
|
279
|
+
* RGBA → greyscale + box-average downscale: each output pixel is the mean of
|
|
280
|
+
* the source rectangle it covers. Averaging (rather than point-sampling) is
|
|
281
|
+
* what keeps this in step with the sharp path and what stops a single noisy
|
|
282
|
+
* pixel from surviving a 4.8x decimation into the difference image.
|
|
283
|
+
*/
|
|
284
|
+
export declare function downscaleRgbaToGrey(rgba: Uint8Array, width: number, height: number, targetHeight: number): GreyFrame;
|
|
285
|
+
/**
|
|
286
|
+
* Quantize motion regions onto a coarse grid so "the same thing is still there"
|
|
287
|
+
* produces a stable string. Deliberately lossy: a person walking one cell over
|
|
288
|
+
* changes it, a swinging sign does not.
|
|
289
|
+
*/
|
|
290
|
+
export declare function motionSignature(boxes: MotionBox[], width: number, height: number): string;
|
|
291
|
+
//# sourceMappingURL=SceneChangeDetector.d.ts.map
|