Initial commit — Alta Video Player (WebAVP)

Web-based surveillance video player for Alta/Ava Security camera exports
with multi-camera sync, timeline, digital zoom, motion analytics, and
cryptographic integrity verification.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-04 12:50:24 +00:00
commit 691f643edc
6 changed files with 5568 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
**Alta Video Player (WebAVP)** — a web-based surveillance video player for Alta/Ava Security camera exports. Users drag-drop video files or ZIP archives (including AES-256 encrypted ones) and get a multi-camera synchronized playback experience with timeline, digital zoom, cryptographic integrity verification, and automatic motion analytics.
## Running the App
```bash
python3 app.py
# Serves on http://0.0.0.0:5152
```
No build step. No dependencies needed at runtime — `app.py` uses only Python stdlib (`http.server`). The `requirements.txt` (flask, requests, gunicorn) is vestigial; the server was rewritten to pure stdlib.
## Architecture
### Backend (`app.py`)
Minimal HTTP server with three routes:
- `GET /` — serves `templates/index.html`
- `GET /static/*` — serves static files with path traversal protection
- `GET /api/verify-cert?serial=...&certificateHash=...` — proxies certificate verification to `https://aware.avasecurity.com/api/v1/public/verifyServerCertificate`
### Frontend (`templates/index.html`)
Single self-contained HTML file (~5000 lines) with inline CSS and JavaScript in an IIFE. This is the entire application — there is no framework, no build system, no separate JS modules.
**State model:** Global `channels` Map keyed by channel name. Each channel holds segments (video blobs + time ranges), metadata, DOM references, color, and zoom state. Global timeline state (`globalStart`, `globalEnd`, `currentTime`) synchronizes all cameras. Motion analytics state lives in the `motionState` object.
**Key subsystems:**
- **File ingestion** — drag-drop files/folders, ZIP extraction (JSZip for plain, custom AES-256-CTR for encrypted), metadata pairing by base filename, concurrent import guard (`isImporting` flag)
- **Multi-camera grid** — responsive CSS grid (19 cameras), drag-to-reorder, click-to-expand
- **Playback engine** — `requestAnimationFrame` tick loop, per-channel segment visibility management, variable speed (0.25x8x), frame stepping
- **Timeline** — interactive scrub bar with zoom (mouse wheel), minimap, per-channel segment indicators with color-coded dots, motion heatmap row
- **Digital zoom** — per-camera scroll-to-zoom (up to 10x) with click-drag panning
- **Magnifier tool** — draw rectangle to zoom into region
- **Slideshow mode** — animated grid showing only currently active feeds with transitions
- **Motion analytics** — automatic background motion detection on load (see below)
- **Integrity verification** — offline X.509 certificate parsing, RSASSA-PKCS1-v1_5 and ECDSA signature verification via Web Crypto API, optional cloud verification through `/api/verify-cert`
- **Session persistence** — IndexedDB caching of video blobs and metadata for page refresh survival
**External dependency:** `/static/jszip.min.js` (vendored, for unencrypted ZIP parsing).
## Motion Analytics Subsystem
Zero-dependency canvas-based motion detection that runs automatically when videos are loaded.
### How it works
1. **Auto-scan on load**`scheduleAutoScan()` is called from `flushPending()` and `loadSessionData()` after videos finish loading. Debounced 800ms to batch all segments.
2. **Dedicated scan videos** — scan creates temporary offscreen `<video>` elements per segment (with `preload='auto'` for fast seeking) that don't interfere with the playback engine. Each is destroyed after its segment is processed.
3. **Pixel-diff at 160x120** — frames are drawn to a small offscreen canvas. Each pixel's RGB delta is compared against a threshold. Changed pixels are counted and mapped to a 10x8 hotspot grid.
4. **Sensitivity slider (1100)** — maps to two internal thresholds via `sensitivityToThresholds()`: `pixelThreshold` (how different a pixel must be) and `changeThreshold` (what % of pixels must change). Presets: Indoor (30), Default (40), Parking (45), Outdoor (55).
5. **Motion clusters** — consecutive motion detections within `clusterGap` (5s) are merged into clusters with start/end times, peak change %, and hotspot data.
6. **Results visualization** — motion heatmap row on timeline (purple/red intensity), clickable cluster markers, scrollable event cards in the analytics panel with hotspot mini-grids.
### Key state: `motionState`
- `sensitivity` (1100), `scanInterval` (2s default), `clusterGap` (5s)
- `detector.canvas` / `detector.ctx` — 160x120 offscreen canvas for pixel comparison
- `detector.prevFrames` — Map of channelName → previous ImageData
- `motionProfiles` — Map of channelName → array of `{ time, changePercent, hotspots, hasMotion }`
- `motionClusters` — sorted array of `{ startTime, endTime, channelName, peakChange, ... }`
- `isScanning` / `scanAbort` — scan lifecycle
- `isMonitoring` — live monitor mode flag
### Key functions
- `detectMotion(videoEl, channelName)` — core pixel-diff, returns `{ hasMotion, changePercent, hotspots }`
- `scanTimeline({ auto })` — full scan orchestrator, creates offscreen videos, yields to UI every 12 frames
- `scheduleAutoScan()` — debounced auto-trigger after video load
- `setScanUIState('scanning'|'idle')` — manages rail button grey-out and progress UI
- `seekToMotion(direction)` — skip to next/previous motion cluster
- `monitorTick(absTime)` — called from `tick()` during playback for live detection
- `buildMotionClusters()` — post-scan cluster identification
- `renderMotionResults()` — populates panel: sparkline, summary, event cards
### UI elements
- **Analytics panel** — right slide-out (400px), class `.analytics-panel`, toggled via `railAnalyticsBtn`
- **Rail button** — greyed out with pulsing purple border (`.scanning` class) during auto-scan
- **Skip-to-motion buttons** — `btnPrevMotion` / `btnNextMotion` in transport controls
- **Keyboard shortcuts** — Shift+N (next motion), Shift+P (previous motion)
### Two-tier distribution strategy
The browser version provides motion detection only (zero dependencies, fully offline). A future downloadable local app will add:
- Object classification via bundled ONNX/YOLO model (person/vehicle/animal detection)
- BYOA (Bring Your Own Agent) cloud AI integration for scene understanding, license plates, cross-camera tracking
- Optional local LLM support (Ollama) for fully offline AI reasoning
## Memory Management
Video elements must be properly destroyed to avoid browser memory exhaustion:
- **`destroyVideoEl(videoEl)`** — pauses video, removes `src`, calls `.load()` to force browser to release buffered data. Must be called before removing video elements from DOM.
- **`video.preload = 'metadata'`** — all playback videos use metadata-only preloading to avoid buffering entire files into RAM. Scan videos use `'auto'` temporarily and are destroyed after use.
- **`newSession()`** — comprehensive teardown: stops scan/monitor, destroys all video elements, revokes all blob URLs, nulls blob references, releases WebGL contexts, clears all state.
- **`isImporting` guard** — prevents concurrent file imports which could cause race conditions and duplicate segments.
- Slideshow video elements are destroyed on pane transitions and when slideshow is toggled off.
## Key Conventions
- All frontend code lives in `templates/index.html` — CSS at top, then the IIFE script block
- Video elements are created on-demand and hidden (not removed) for performance
- Segment visibility is recalculated every animation frame during playback
- The `batchingSegments` flag defers rendering during bulk file imports
- Keyboard shortcuts are defined inline (Space=play/pause, arrows=seek, S=slideshow, M=magnifier, F=fullscreen, [/]=speed, 0=reset zoom, Shift+N/P=skip motion)
- Motion scan runs automatically on load — the analytics rail button is disabled until scan completes
- Right-side panels (log, analytics) auto-close when the other opens
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""Alta Video Player — lightweight HTTPS server using only Python stdlib."""
import json
import mimetypes
import os
from http.server import HTTPServer, SimpleHTTPRequestHandler
from urllib.parse import parse_qs, urlencode, urlparse
from urllib.request import urlopen
from urllib.error import URLError
PORT = 5152
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
STATIC_DIR = os.path.join(BASE_DIR, "static")
TEMPLATES_DIR = os.path.join(BASE_DIR, "templates")
class Handler(SimpleHTTPRequestHandler):
def do_GET(self):
parsed = urlparse(self.path)
path = parsed.path
if path == "/":
self._serve_file(os.path.join(TEMPLATES_DIR, "index.html"), "text/html")
elif path.startswith("/static/"):
rel = path[len("/static/"):]
file_path = os.path.join(STATIC_DIR, rel)
if not os.path.realpath(file_path).startswith(os.path.realpath(STATIC_DIR)):
self.send_error(403)
return
mime = mimetypes.guess_type(file_path)[0] or "application/octet-stream"
self._serve_file(file_path, mime)
elif path == "/api/verify-cert":
self._proxy_verify(parsed.query)
else:
self.send_error(404)
def _serve_file(self, file_path, content_type):
try:
with open(file_path, "rb") as f:
data = f.read()
self.send_response(200)
self.send_header("Content-Type", content_type)
self.send_header("Content-Length", len(data))
self.end_headers()
self.wfile.write(data)
except FileNotFoundError:
self.send_error(404)
def _proxy_verify(self, query_string):
params = parse_qs(query_string)
serial = params.get("serial", [""])[0]
cert_hash = params.get("certificateHash", [""])[0]
if not serial or not cert_hash:
self._json_response(400, {"verified": False, "error": "Missing parameters"})
return
try:
qs = urlencode({"serial": serial, "certificateHash": cert_hash})
url = f"https://aware.avasecurity.com/api/v1/public/verifyServerCertificate?{qs}"
resp = urlopen(url, timeout=10)
if 200 <= resp.status < 300:
self._json_response(200, {"verified": True})
else:
self._json_response(200, {"verified": False, "error": f"HTTP {resp.status}"})
except URLError as e:
self._json_response(200, {"verified": False, "error": str(e)})
def _json_response(self, status, data):
body = json.dumps(data).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", len(body))
self.end_headers()
self.wfile.write(body)
def log_message(self, fmt, *args):
print(f"[AVP] {args[0]}")
if __name__ == "__main__":
server = HTTPServer(("0.0.0.0", PORT), Handler)
print(f"Alta Video Player running on http://0.0.0.0:{PORT}")
server.serve_forever()
+322
View File
@@ -0,0 +1,322 @@
# Design System — Alta Style
> This document defines the visual identity for all tools and interfaces built under
> the Alta brand. It covers colors, typography, component styling, and interaction
> patterns. It does NOT prescribe layouts, page structures, or application architecture —
> use whatever structure fits your application.
>
> Apply this to web apps, Electron apps, dashboards, internal tools, Chrome extensions,
> or any UI that should feel like part of the Alta product family.
>
> Colors are sourced from the Alta Video platform's global CSS variables
> (`--colorPrimary`, `--colorLink`, etc.) and computed element styles.
---
## 1. Color Palette
### 1.1 Backgrounds
| Token | Value | When to use |
|----------------------|------------------------------|------------------------------------------------|
| `--bg-app` | `#121826` | Page/body background — deep navy |
| `--bg-panel` | `#121826` | Headers, nav bars, top-level panels |
| `--bg-surface` | `#181F32` | Cards, containers, secondary sections |
| `--bg-surface-hover` | `rgba(0, 110, 215, 0.12)` | Hovered rows, list items, interactive elements |
| `--bg-surface-active`| `rgba(0, 110, 215, 0.20)` | Selected/pressed/active states |
| `--bg-input` | `rgba(0, 110, 215, 0.10)` | Text inputs, search bars, selects, dropdowns |
| `--bg-button` | `rgba(0, 110, 215, 0.20)` | Default button background |
| `--bg-button-hover` | `rgba(0, 110, 215, 0.32)` | Hovered button background |
> **Key principle:** Alta layers semi-transparent tints of the brand blue
> (`rgba(0, 110, 215, *)`) on the navy background. The secondary surface color
> `#181F32` provides a subtle lift for cards and sections without transparency.
### 1.2 Borders
| Token | Value | When to use |
|--------------------|-------------|----------------------------------------------------|
| `--border-default` | `#C2C7CC` | Standard borders on components in dark context |
| `--border-light` | `#EBEEF0` | Lighter borders, dividers within cards/panels |
| `--border-subtle` | `rgba(244, 244, 246, 0.12)` | Very subtle separators on dark backgrounds |
> Alta uses light-colored borders (`#C2C7CC`, `#EBEEF0`) even on the dark theme.
> These are typically used at low opacity or on specific components. For most
> dark-on-dark dividers, use `--border-subtle`.
### 1.3 Text
| Token | Value | When to use |
|--------------------|-------------|------------------------------------------|
| `--text-primary` | `#F4F4F6` | Headings, labels, body content |
| `--text-secondary` | `#656972` | Descriptions, metadata, timestamps |
| `--text-muted` | `#8D9399` | Placeholders, disabled text, hints |
| `--text-on-accent` | `#FFFFFF` | Text on accent or status backgrounds |
> Primary text is off-white (`#F4F4F6`), not pure white. Reserve `#FFFFFF` for
> text on accent-colored or status-colored backgrounds.
### 1.4 Accent & Brand
| Token | Value | When to use |
|--------------------------|------------------------------|---------------------------------------|
| `--accent-primary` | `#006ED7` | Brand blue — links, primary actions, active states (Alta `--colorPrimary` / `--colorLink`) |
| `--accent-primary-hover` | `#0080F0` | Hovered links and primary elements |
| `--accent-primary-muted` | `rgba(0, 110, 215, 0.20)` | Tinted backgrounds using brand blue |
### 1.5 Status
| Token | Value | When to use |
|--------------------|-----------|---------------------------------------------------|
| `--status-success` | `#20C62F` | Success, connected, healthy, complete |
| `--status-error` | `#DE1111` | Error, failed, disconnected, destructive actions |
| `--status-warning` | `#EAA301` | Warning, caution, pending, needs attention |
| `--status-info` | `#8D9399` | Informational, neutral, secondary indicators |
| `--status-purple` | `#8957E5` | Special category badges, analytics |
| `--status-motion` | `#A855F7` | Activity, progress, event indicators |
### 1.6 Overlays
| Token | Value | When to use |
|-------------------|----------------------------|--------------------------------------|
| `--overlay-dark` | `rgba(18, 24, 38, 0.60)` | Dimming layer behind modals/dialogs |
| `--overlay-panel` | `rgba(18, 24, 38, 0.85)` | Floating panels, dropdown backdrops |
---
## 2. Typography
**Font stack:** `'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif`
| Role | Size | Weight | Notes |
|--------------------|-------|--------|----------------------------------|
| Page / section title | 16px | 600 | |
| Body / labels | 14px | 400 | |
| Emphasized labels | 14px | 600 | Selected items, active states |
| Compact labels | 13px | 500 | Dense UI, toolbar text |
| Captions | 12px | 400 | Metadata, timestamps, subtitles |
| Badges | 10px | 600 | Inline status/category badges |
| Micro | 11px | 600 | Tiny indicators, status labels |
**Spacing:** letter-spacing `-0.01em` on titles, `0` on body.
**Line-height:** `1.3` titles, `1.4` body.
---
## 3. Component Styles
### 3.1 Buttons
| Variant | Background | Text | Hover |
|------------|---------------------------|---------------------|--------------------------------|
| Primary | `--accent-primary` | `#FFFFFF` | `--accent-primary-hover` |
| Secondary | `--bg-button` | `--text-primary` | `--bg-button-hover` |
| Ghost/Icon | `transparent` | `--text-secondary` | `--bg-button`, text brightens |
| Danger | `--status-error` | `#FFFFFF` | lighten ~10% |
- No visible borders on buttons — Alta buttons are borderless
- Border-radius: `6px`
- Padding: `8px 16px` (text) / `8px` (icon-only)
- Heights: `32px` small, `36px` default, `40px` large
- Font: 13px, weight 500
### 3.2 Inputs
- Height: `36px`
- Background: `--bg-input`
- Border: `1px solid var(--border-subtle)`, radius `6px`
- Placeholder: `--text-muted`
- Focus: border changes to `--accent-primary`, add `var(--shadow-focus)` ring
### 3.3 Badges
- Padding: `2px 6px`, radius `4px`
- Font: 10px, weight 600, `#FFFFFF` text
- Use `--status-purple` for special/category badges
- Use `--accent-primary` for general tags
### 3.4 Status Indicators
- Dot size: `8px` circle
- Colors: `--status-success`, `--status-error`, `--status-warning`
- Always pair with a text label or icon — never color alone
- Pulsing dots: `animation: pulse 1.5s ease-in-out infinite` (opacity 1 → 0.35)
### 3.5 Cards & Containers
- Background: `--bg-surface` (`#181F32`)
- Border: `1px solid var(--border-subtle)`
- Border-radius: `8px`
- Elevated variant adds: `var(--shadow-elevated)`
### 3.6 Tables & Lists
- Header row: `--text-muted`, 11px, weight 600, uppercase, `letter-spacing: 0.05em`
- Body rows: `--text-primary`, 1314px
- Row hover: `--bg-surface-hover`
- Selected row: `--bg-surface-active`
- Row dividers: `rgba(244, 244, 246, 0.08)`
### 3.7 Tooltips & Popovers
- Background: `--bg-surface`
- Border: `1px solid var(--border-subtle)`
- Border-radius: `8px`, padding `8px 12px`
- Shadow: `var(--shadow-elevated)`
- Text: `--text-primary`, 13px
### 3.8 Navigation Items
- Default: icon/text in `--text-secondary`
- Hover: bg `--bg-surface-hover`, text brightens to `--text-primary`
- Active: bg `--accent-primary`, text/icon `#FFFFFF`, radius `8px`
### 3.9 Modals & Dialogs
- Backdrop: `--overlay-dark`
- Panel: `--bg-surface` background, `1px solid var(--border-subtle)`, radius `12px`
- Shadow: `var(--shadow-elevated)`
- Title: 16px weight 600, body: 14px weight 400
### 3.10 Tabs
- Inactive: `--text-secondary`, transparent background
- Active: `--text-primary`, bottom border `2px solid var(--accent-primary)`
- Hover: text brightens toward `--text-primary`
### 3.11 Progress & Bar Charts
- Track: `--bg-input`
- Fill: use status colors based on meaning (green good, yellow caution, red bad)
- Height: `8px`, radius `4px`
### 3.12 Toast / Notification
- Background: `--bg-surface`
- Border-left: `3px solid` using appropriate status color
- Border-radius: `8px`
- Shadow: `var(--shadow-elevated)`
- Auto-dismiss with fade-out
---
## 4. Iconography
- **Style:** Outline, 1.5px stroke weight
- **Library:** Lucide Icons (preferred) or Heroicons Outline
- **Sizes:** `20px` navigation, `16px` inline, `14px` compact/dense UI
- **Colors:** `--text-secondary` default → `--text-primary` on hover → `#FFFFFF` on accent backgrounds
- Don't mix filled and outline styles in the same interface
---
## 5. Animation & Transitions
| What | Duration | Easing |
|---------------------|----------|----------------------------|
| Hover states | 150ms | `ease-out` |
| Panel open/close | 250ms | `cubic-bezier(.4,0,.2,1)` |
| Fade in/out | 150ms | `ease` |
| Pulse indicators | 1500ms | `ease-in-out` (infinite) |
| Content transitions | 200ms | `ease-out` |
All motion is functional. No decorative animation.
---
## 6. Theme Rules
1. **Dark-only.** No light mode.
2. **Navy, not black.** The base is `#121826`. Never use `#000000` for backgrounds.
3. **Two surface levels.** `#121826` for the page, `#181F32` for elevated cards/sections.
4. **Vivid brand blue.** The accent is `#006ED7` — a strong, saturated blue. Not muted, not pastel.
5. **Off-white text.** Use `#F4F4F6`, not `#FFFFFF`. Pure white is only for accent backgrounds.
6. **Real status colors.** Green `#20C62F`, red `#DE1111`, orange `#EAA301` — these are vivid and intentional.
7. **Pair color with meaning.** Never use color alone for status — always include a label or icon.
8. **WCAG AA contrast.** All text must meet ≥ 4.5:1 ratio against its background.
---
## 7. CSS Variables
```css
:root {
/* Backgrounds */
--bg-app: #121826;
--bg-panel: #121826;
--bg-surface: #181F32;
--bg-surface-hover: rgba(0, 110, 215, 0.12);
--bg-surface-active: rgba(0, 110, 215, 0.20);
--bg-input: rgba(0, 110, 215, 0.10);
--bg-button: rgba(0, 110, 215, 0.20);
--bg-button-hover: rgba(0, 110, 215, 0.32);
/* Borders */
--border-default: #C2C7CC;
--border-light: #EBEEF0;
--border-subtle: rgba(244, 244, 246, 0.12);
/* Text */
--text-primary: #F4F4F6;
--text-secondary: #656972;
--text-muted: #8D9399;
--text-on-accent: #FFFFFF;
/* Accent */
--accent-primary: #006ED7;
--accent-primary-hover: #0080F0;
--accent-primary-muted: rgba(0, 110, 215, 0.20);
/* Status */
--status-success: #20C62F;
--status-error: #DE1111;
--status-warning: #EAA301;
--status-info: #8D9399;
--status-purple: #8957E5;
--status-motion: #A855F7;
/* Overlays */
--overlay-dark: rgba(18, 24, 38, 0.60);
--overlay-panel: rgba(18, 24, 38, 0.85);
/* Spacing */
--space-xs: 4px;
--space-sm: 8px;
--space-md: 12px;
--space-lg: 16px;
--space-xl: 24px;
--space-2xl: 32px;
/* Radii */
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--radius-xl: 12px;
/* Shadows */
--shadow-elevated: 0 8px 24px rgba(0, 0, 0, 0.4);
--shadow-focus: 0 0 0 3px rgba(0, 110, 215, 0.25);
/* Typography */
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
```
---
## 8. Applying This System
When starting any new UI:
1. Set `body { background: var(--bg-app); color: var(--text-primary); font-family: var(--font-family); }`
2. Use `--bg-surface` (`#181F32`) for any card or container — not a custom gray
3. Use `--accent-primary` (`#006ED7`) for links, active states, and primary actions
4. Use `--bg-button` / `--bg-button-hover` for secondary buttons
5. Apply hover/active states using the brand blue opacity scale
6. Pick status colors from the status tokens — `#20C62F` green, `#DE1111` red, `#EAA301` orange
7. Keep transitions ≤ 250ms and functional
8. Use Lucide outline icons at 1.5px stroke weight
This system works for dashboards, forms, data tables, settings pages, tools,
admin panels, extensions, or any other interface. The layout is yours —
the colors, type, and component feel are Alta.
+3
View File
@@ -0,0 +1,3 @@
flask
requests
gunicorn
+13
View File
File diff suppressed because one or more lines are too long
+5038
View File
File diff suppressed because it is too large Load Diff