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 });