vigilar/vigilar/web/templates/recordings.html
Aaron D. Lee 4274d1373f Add pet labeling UI overlay to recording playback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 13:30:52 -04:00

191 lines
7.3 KiB
HTML

{% extends "base.html" %}
{% block title %}Vigilar — Recordings{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/pet-labeling.css') }}">
{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0"><i class="bi bi-camera-video me-2"></i>Recordings</h5>
<div class="d-flex gap-2 align-items-center">
<input type="date" class="form-control form-control-sm bg-dark text-light border-secondary"
id="timeline-date" style="width: auto;">
<select class="form-select form-select-sm bg-dark text-light border-secondary"
id="filter-camera" style="width: auto;">
<option value="">All Cameras</option>
</select>
<div class="btn-group btn-group-sm" id="filter-type">
<button class="btn btn-outline-light active" data-type="all">All</button>
<button class="btn btn-outline-danger" data-type="person">Person</button>
<button class="btn btn-outline-primary" data-type="vehicle">Vehicle</button>
<button class="btn btn-outline-secondary" data-type="motion">Motion</button>
</div>
</div>
</div>
<!-- Timeline canvases (one per camera) -->
<div id="timeline-container">
<div class="card bg-dark border-secondary mb-3" id="timeline-placeholder">
<div class="card-body text-center text-muted py-3">
Select a camera to view the recording timeline
</div>
</div>
</div>
<!-- Video player area -->
<div class="card bg-dark border-secondary mb-3 d-none" id="player-card">
<div class="card-header border-secondary d-flex justify-content-between align-items-center">
<span id="player-title">Recording</span>
<button class="btn btn-sm btn-outline-light" id="player-close">
<i class="bi bi-x-lg"></i>
</button>
</div>
<div class="card-body p-0">
<!-- data-pet-labeling and data-detections are updated by JS when a recording is loaded -->
<div class="position-relative" id="player-video-wrap" data-pet-labeling="true" data-detections="[]">
<video id="player-video" class="w-100" controls></video>
</div>
</div>
</div>
<!-- Legend -->
<div class="d-flex gap-3 mb-3 small text-muted">
<span><span class="badge" style="background:#dc3545">&nbsp;</span> Person</span>
<span><span class="badge" style="background:#0d6efd">&nbsp;</span> Vehicle</span>
<span><span class="badge" style="background:#6c757d">&nbsp;</span> Motion</span>
</div>
<!-- Fallback table for recordings list -->
<div class="card bg-dark border-secondary">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-dark table-hover mb-0">
<thead>
<tr>
<th>Camera</th>
<th>Started</th>
<th>Duration</th>
<th>Trigger</th>
<th>Size</th>
<th></th>
</tr>
</thead>
<tbody id="recordings-list">
{% for rec in recordings %}
<tr>
<td>{{ rec.camera_id }}</td>
<td>{{ rec.started_at }}</td>
<td>{{ rec.duration_s }}s</td>
<td><span class="badge bg-secondary">{{ rec.trigger }}</span></td>
<td>{{ rec.file_size }}</td>
<td>
<a href="/recordings/{{ rec.id }}/download" class="btn btn-sm btn-outline-light">
<i class="bi bi-download"></i>
</a>
</td>
</tr>
{% else %}
<tr>
<td colspan="6" class="text-center text-muted py-3">No recordings found</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='js/timeline.js') }}"></script>
<script src="{{ url_for('static', filename='js/pet-labeling.js') }}"></script>
<script>
(function() {
'use strict';
const dateInput = document.getElementById('timeline-date');
const cameraSelect = document.getElementById('filter-camera');
const container = document.getElementById('timeline-container');
const placeholder = document.getElementById('timeline-placeholder');
const playerCard = document.getElementById('player-card');
const playerVideo = document.getElementById('player-video');
const playerTitle = document.getElementById('player-title');
const playerClose = document.getElementById('player-close');
// Default to today
dateInput.value = new Date().toISOString().split('T')[0];
playerClose.addEventListener('click', () => {
playerCard.classList.add('d-none');
playerVideo.pause();
playerVideo.src = '';
});
const playerVideoWrap = document.getElementById('player-video-wrap');
async function loadDetections(recordingId) {
try {
const resp = await fetch(`/recordings/${recordingId}/detections`);
if (!resp.ok) return [];
return await resp.json();
} catch (e) {
return [];
}
}
function handleSegmentClick(seg) {
playerCard.classList.remove('d-none');
playerTitle.textContent = `Recording #${seg.id} (${seg.type || 'motion'})`;
playerVideo.src = `/recordings/${seg.id}/download`;
playerVideo.play().catch(() => {});
// Reset overlay while detections load
if (window.PetLabeling) {
PetLabeling.updateDetections(playerVideoWrap, []);
}
// Fetch detections for this recording and render overlay
loadDetections(seg.id).then(detections => {
if (window.PetLabeling) {
PetLabeling.updateDetections(playerVideoWrap, detections);
}
});
}
function loadTimeline() {
const cameraId = cameraSelect.value;
const date = dateInput.value;
if (!cameraId) {
placeholder.classList.remove('d-none');
// Remove any existing timeline canvases
container.querySelectorAll('.timeline-row').forEach(el => el.remove());
return;
}
placeholder.classList.add('d-none');
container.querySelectorAll('.timeline-row').forEach(el => el.remove());
const row = document.createElement('div');
row.className = 'card bg-dark border-secondary mb-2 timeline-row';
row.innerHTML = `
<div class="card-header border-secondary py-1 small">${cameraId}</div>
<div class="card-body p-2">
<canvas data-height="48" style="width:100%;cursor:pointer"></canvas>
</div>`;
container.appendChild(row);
const canvas = row.querySelector('canvas');
const tl = new VigilarTimeline(canvas, {
cameraId: cameraId,
date: date,
onClick: handleSegmentClick,
});
tl.load();
}
cameraSelect.addEventListener('change', loadTimeline);
dateInput.addEventListener('change', loadTimeline);
})();
</script>
{% endblock %}