From db754b2baeb48db124d8125b675d7f4d976fda7c Mon Sep 17 00:00:00 2001 From: PageZ948 Date: Wed, 3 Jun 2026 08:38:56 -0400 Subject: [PATCH] Fix multi-sensor cameras collapsing into one tile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Channels were keyed solely on the metadata `Name` field. Multi-sensor cameras (e.g. Avigilon 32C-H5A) export one file per sensor that share an identical `Name` and `Serial`, so their sensors merged into a single tile and one sensor's clips were lost to start-time dedup — surfacing as "only 3 of 4 cameras loaded". Add cameraIdFromFile(), which derives a per-sensor id from the export filename prefix (stripping the Alta timestamp, e.g. "Backyard_4-...Z.mp4" -> "Backyard 4"), and key channels by it, falling back to the metadata Name for non-Alta filenames. Verified: all 4 sensors now load as distinct tiles (4 cameras | 13 clips). Co-Authored-By: Claude Opus 4.8 (1M context) --- templates/index.html | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/templates/index.html b/templates/index.html index 962d44c..9fb14e0 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1623,6 +1623,20 @@ return name.replace(/\.(mp4|mov|avi|mkv|txt)$/i, ''); } + // Derive a per-sensor camera id from an Alta export filename by stripping the + // trailing timestamp, e.g. "Backyard_4-2026-06-03T10-02-48.000Z.mp4" -> "Backyard 4". + // Multi-sensor cameras (e.g. Avigilon 32C-H5A) export one file per sensor that + // share the SAME metadata Name and Serial, so the filename prefix is the only + // thing distinguishing the sensors. Keying channels by it stops distinct sensors + // from collapsing into one tile. Returns '' when no Alta timestamp is present so + // callers can fall back to the metadata Name. + function cameraIdFromFile(fileName) { + const base = baseName(fileName); + const stripped = base.replace(/-\d{4}-\d{2}-\d{2}T[\d\-.]+Z?$/i, ''); + if (stripped === base) return ''; + return stripped.replace(/_/g, ' ').trim(); + } + // ─── Channel / Segment Management ─── function getOrCreateChannel(name) { @@ -1700,7 +1714,7 @@ for (let mi = pendingMetas.length - 1; mi >= 0; mi--) { const m = pendingMetas[mi]; if (vBase === baseName(m.fileName)) { - const cameraName = m.meta['Name'] || `Camera ${channels.size + 1}`; + const cameraName = cameraIdFromFile(v.fileName) || m.meta['Name'] || `Camera ${channels.size + 1}`; addSegment(cameraName, m.meta, v.blob); pendingVideos.splice(vi, 1); pendingMetas.splice(mi, 1); @@ -1717,7 +1731,7 @@ // Remaining videos without metadata for (const v of pendingVideos) { - const name = baseName(v.fileName).replace(/_/g, ' '); + const name = cameraIdFromFile(v.fileName) || baseName(v.fileName).replace(/_/g, ' '); const ch = getOrCreateChannel(name); const url = URL.createObjectURL(v.blob); ch.segments.push({ startTime: null, endTime: null, url, blob: v.blob, meta: {}, videoEl: null, loaded: false });