191 lines
7.3 KiB
HTML
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"> </span> Person</span>
|
|
<span><span class="badge" style="background:#0d6efd"> </span> Vehicle</span>
|
|
<span><span class="badge" style="background:#6c757d"> </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 %}
|