Add Electron desktop shell and clean project docs
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
release/
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
@@ -4,16 +4,32 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## Project Overview
|
## 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.
|
**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, and cryptographic integrity verification.
|
||||||
|
|
||||||
## Running the App
|
## Running the App
|
||||||
|
|
||||||
|
Desktop mode:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
Web/Python mode:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 app.py
|
python3 app.py
|
||||||
# Serves on http://0.0.0.0:5152
|
# 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.
|
The Python server uses only stdlib modules. Electron dependencies are managed through `package.json` and `package-lock.json`.
|
||||||
|
|
||||||
|
Checks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run check
|
||||||
|
xvfb-run -a npm run smoke
|
||||||
|
```
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -26,73 +42,31 @@ Minimal HTTP server with three routes:
|
|||||||
|
|
||||||
### Frontend (`templates/index.html`)
|
### 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.
|
Single self-contained HTML file (~4000 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.
|
**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.
|
||||||
|
|
||||||
**Key subsystems:**
|
**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)
|
- **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 (1–9 cameras), drag-to-reorder, click-to-expand
|
- **Multi-camera grid** — responsive CSS grid (1–9 cameras), drag-to-reorder, click-to-expand
|
||||||
- **Playback engine** — `requestAnimationFrame` tick loop, per-channel segment visibility management, variable speed (0.25x–8x), frame stepping
|
- **Playback engine** — `requestAnimationFrame` tick loop, per-channel segment visibility management, variable speed (0.25x–8x), frame stepping
|
||||||
- **Timeline** — interactive scrub bar with zoom (mouse wheel), minimap, per-channel segment indicators with color-coded dots, motion heatmap row
|
- **Timeline** — interactive scrub bar with zoom (mouse wheel), minimap, per-channel segment indicators with color-coded dots
|
||||||
- **Digital zoom** — per-camera scroll-to-zoom (up to 10x) with click-drag panning
|
- **Digital zoom** — per-camera scroll-to-zoom (up to 10x) with click-drag panning
|
||||||
- **Magnifier tool** — draw rectangle to zoom into region
|
- **Magnifier tool** — draw rectangle to zoom into region
|
||||||
- **Slideshow mode** — animated grid showing only currently active feeds with transitions
|
- **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`
|
- **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
|
- **Session persistence** — IndexedDB caching of video blobs and metadata for page refresh survival
|
||||||
|
|
||||||
**External dependency:** `/static/jszip.min.js` (vendored, for unencrypted ZIP parsing).
|
**External dependency:** `/static/jszip.min.js` (vendored, for unencrypted ZIP parsing).
|
||||||
|
|
||||||
## Motion Analytics Subsystem
|
Motion analytics were intentionally removed. Keep future work focused on core video player behavior unless the product direction changes.
|
||||||
|
|
||||||
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 (1–100)** — 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` (1–100), `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
|
## Memory Management
|
||||||
|
|
||||||
Video elements must be properly destroyed to avoid browser memory exhaustion:
|
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.
|
- **`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.
|
- **`video.preload = 'metadata'`** — all playback videos use metadata-only preloading to avoid buffering entire files into RAM.
|
||||||
- **`newSession()`** — comprehensive teardown: stops scan/monitor, destroys all video elements, revokes all blob URLs, nulls blob references, releases WebGL contexts, clears all state.
|
- **`newSession()`** — comprehensive teardown: 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.
|
- **`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.
|
- Slideshow video elements are destroyed on pane transitions and when slideshow is toggled off.
|
||||||
|
|
||||||
@@ -102,6 +76,5 @@ Video elements must be properly destroyed to avoid browser memory exhaustion:
|
|||||||
- Video elements are created on-demand and hidden (not removed) for performance
|
- Video elements are created on-demand and hidden (not removed) for performance
|
||||||
- Segment visibility is recalculated every animation frame during playback
|
- Segment visibility is recalculated every animation frame during playback
|
||||||
- The `batchingSegments` flag defers rendering during bulk file imports
|
- 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)
|
- Keyboard shortcuts are defined inline (Space=play/pause, arrows=seek, S=slideshow, M=magnifier, F=fullscreen, [/]=speed, 0=reset zoom)
|
||||||
- Motion scan runs automatically on load — the analytics rail button is disabled until scan completes
|
- Right-side panel (log) is the only slide-out; opening it does not affect other panels
|
||||||
- Right-side panels (log, analytics) auto-close when the other opens
|
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# Alta Video Player (WebAVP)
|
||||||
|
|
||||||
|
WebAVP is a local player for Alta/Ava Security camera exports. It focuses on core review workflows: importing video exports, synchronizing multiple cameras on one timeline, zooming into footage, and validating export integrity.
|
||||||
|
|
||||||
|
The app can run in two modes:
|
||||||
|
|
||||||
|
## Core Features
|
||||||
|
|
||||||
|
- Drag/drop individual video and metadata files, folders, or ZIP archives
|
||||||
|
- Import unencrypted ZIPs through vendored JSZip
|
||||||
|
- Import AES-encrypted ZIP exports with an in-app password prompt
|
||||||
|
- Synchronize multiple camera segments on a shared timeline
|
||||||
|
- Scrub, zoom, pan, change playback speed, and frame-step footage
|
||||||
|
- Reorder, hide/show, expand, and manually lay out camera tiles
|
||||||
|
- Use region zoom, scroll zoom, and fisheye dewarp tools
|
||||||
|
- Verify signed exports offline and optionally confirm certificates with Alta's cloud verification endpoint
|
||||||
|
- Preserve sessions across refreshes with IndexedDB
|
||||||
|
|
||||||
|
## Standalone desktop app (Electron)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
The Electron shell loads the existing UI through the custom `webavp://app/` protocol, so local assets are served from the packaged app instead of brittle `file://` paths. Certificate verification is bridged through Electron IPC (`window.webavpNative.verifyCertificateOnline`) and performed in the main process.
|
||||||
|
|
||||||
|
Packaging scripts are included:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dist
|
||||||
|
```
|
||||||
|
|
||||||
|
`electron-builder` is configured for Linux AppImage/deb, macOS dmg, and Windows nsis. Cross-platform packaging still needs to be run on the target OS/build host.
|
||||||
|
|
||||||
|
## Web/Python mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 app.py
|
||||||
|
# http://0.0.0.0:5152
|
||||||
|
```
|
||||||
|
|
||||||
|
The Python stdlib server serves `/`, `/static/*`, and `/api/verify-cert`. There are no Python package dependencies.
|
||||||
|
|
||||||
|
## Checks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run check
|
||||||
|
xvfb-run -a npm run smoke
|
||||||
|
```
|
||||||
|
|
||||||
|
`check` runs Electron main/preload syntax checks and `py_compile` for the Python server. `smoke` starts Electron, waits for `webavp://app/index.html` to finish loading, then exits automatically. On a desktop session, `npm run smoke` is enough; on headless Linux, use `xvfb-run -a npm run smoke`.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Alta Video Player — lightweight HTTPS server using only Python stdlib."""
|
"""Alta Video Player — lightweight HTTP server using only Python stdlib."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import mimetypes
|
import mimetypes
|
||||||
|
|||||||
@@ -72,8 +72,7 @@
|
|||||||
| `--status-error` | `#DE1111` | Error, failed, disconnected, destructive actions |
|
| `--status-error` | `#DE1111` | Error, failed, disconnected, destructive actions |
|
||||||
| `--status-warning` | `#EAA301` | Warning, caution, pending, needs attention |
|
| `--status-warning` | `#EAA301` | Warning, caution, pending, needs attention |
|
||||||
| `--status-info` | `#8D9399` | Informational, neutral, secondary indicators |
|
| `--status-info` | `#8D9399` | Informational, neutral, secondary indicators |
|
||||||
| `--status-purple` | `#8957E5` | Special category badges, analytics |
|
| `--status-purple` | `#8957E5` | Special category badges |
|
||||||
| `--status-motion` | `#A855F7` | Activity, progress, event indicators |
|
|
||||||
|
|
||||||
### 1.6 Overlays
|
### 1.6 Overlays
|
||||||
|
|
||||||
@@ -273,7 +272,6 @@ All motion is functional. No decorative animation.
|
|||||||
--status-warning: #EAA301;
|
--status-warning: #EAA301;
|
||||||
--status-info: #8D9399;
|
--status-info: #8D9399;
|
||||||
--status-purple: #8957E5;
|
--status-purple: #8957E5;
|
||||||
--status-motion: #A855F7;
|
|
||||||
|
|
||||||
/* Overlays */
|
/* Overlays */
|
||||||
--overlay-dark: rgba(18, 24, 38, 0.60);
|
--overlay-dark: rgba(18, 24, 38, 0.60);
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
const { app, BrowserWindow, ipcMain, net, protocol, shell } = require('electron');
|
||||||
|
const fs = require('node:fs/promises');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const APP_SCHEME = 'webavp';
|
||||||
|
const ROOT_DIR = path.resolve(__dirname, '..');
|
||||||
|
const TEMPLATE_DIR = path.join(ROOT_DIR, 'templates');
|
||||||
|
const STATIC_DIR = path.join(ROOT_DIR, 'static');
|
||||||
|
|
||||||
|
if (process.env.WEBAVP_DISABLE_GPU === '1') {
|
||||||
|
app.disableHardwareAcceleration();
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol.registerSchemesAsPrivileged([
|
||||||
|
{
|
||||||
|
scheme: APP_SCHEME,
|
||||||
|
privileges: {
|
||||||
|
standard: true,
|
||||||
|
secure: true,
|
||||||
|
supportFetchAPI: true,
|
||||||
|
corsEnabled: true,
|
||||||
|
stream: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
function contentTypeFor(filePath) {
|
||||||
|
const ext = path.extname(filePath).toLowerCase();
|
||||||
|
switch (ext) {
|
||||||
|
case '.html': return 'text/html; charset=utf-8';
|
||||||
|
case '.js': return 'text/javascript; charset=utf-8';
|
||||||
|
case '.css': return 'text/css; charset=utf-8';
|
||||||
|
case '.json': return 'application/json; charset=utf-8';
|
||||||
|
case '.svg': return 'image/svg+xml';
|
||||||
|
case '.png': return 'image/png';
|
||||||
|
case '.jpg':
|
||||||
|
case '.jpeg': return 'image/jpeg';
|
||||||
|
case '.webp': return 'image/webp';
|
||||||
|
case '.mp4': return 'video/mp4';
|
||||||
|
case '.webm': return 'video/webm';
|
||||||
|
default: return 'application/octet-stream';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeJoin(baseDir, requestPath) {
|
||||||
|
const decoded = decodeURIComponent(requestPath);
|
||||||
|
const normalized = path.normalize(decoded).replace(/^([/\\])+/, '');
|
||||||
|
const resolved = path.resolve(baseDir, normalized);
|
||||||
|
const base = path.resolve(baseDir);
|
||||||
|
if (resolved !== base && !resolved.startsWith(base + path.sep)) {
|
||||||
|
throw new Error('Path traversal blocked');
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function serveFile(filePath) {
|
||||||
|
try {
|
||||||
|
const data = await fs.readFile(filePath);
|
||||||
|
return new Response(data, {
|
||||||
|
headers: { 'content-type': contentTypeFor(filePath) },
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (err && err.code === 'ENOENT') {
|
||||||
|
return new Response('Not found', { status: 404 });
|
||||||
|
}
|
||||||
|
console.error('[WebAVP] app protocol error:', err);
|
||||||
|
return new Response('Internal server error', { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerAppProtocol() {
|
||||||
|
protocol.handle(APP_SCHEME, async (request) => {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
if (url.hostname !== 'app') {
|
||||||
|
return new Response('Unknown host', { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.pathname === '/' || url.pathname === '/index.html') {
|
||||||
|
return serveFile(path.join(TEMPLATE_DIR, 'index.html'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.pathname.startsWith('/static/')) {
|
||||||
|
try {
|
||||||
|
const relativeStaticPath = url.pathname.slice('/static/'.length);
|
||||||
|
return serveFile(safeJoin(STATIC_DIR, relativeStaticPath));
|
||||||
|
} catch (err) {
|
||||||
|
return new Response('Forbidden', { status: 403 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response('Not found', { status: 404 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyCertificateOnline(_event, { serial, certificateHash } = {}) {
|
||||||
|
if (!serial || !certificateHash) {
|
||||||
|
return { verified: false, error: 'Missing parameters' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
serial: String(serial).toLowerCase(),
|
||||||
|
certificateHash: String(certificateHash),
|
||||||
|
});
|
||||||
|
const url = `https://aware.avasecurity.com/api/v1/public/verifyServerCertificate?${params.toString()}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await net.fetch(url, { method: 'GET' });
|
||||||
|
if (response.ok) {
|
||||||
|
return { verified: true };
|
||||||
|
}
|
||||||
|
return { verified: false, error: `HTTP ${response.status}` };
|
||||||
|
} catch (err) {
|
||||||
|
return { verified: false, error: err.message || 'Certificate verification request failed' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createWindow() {
|
||||||
|
const win = new BrowserWindow({
|
||||||
|
width: 1440,
|
||||||
|
height: 960,
|
||||||
|
minWidth: 1024,
|
||||||
|
minHeight: 720,
|
||||||
|
backgroundColor: '#121826',
|
||||||
|
title: 'Alta Video Player',
|
||||||
|
webPreferences: {
|
||||||
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
|
contextIsolation: true,
|
||||||
|
nodeIntegration: false,
|
||||||
|
sandbox: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
win.loadURL(`${APP_SCHEME}://app/index.html`);
|
||||||
|
|
||||||
|
if (process.env.WEBAVP_SMOKE_QUIT_AFTER_MS) {
|
||||||
|
const quitAfterMs = Number(process.env.WEBAVP_SMOKE_QUIT_AFTER_MS);
|
||||||
|
win.webContents.once('did-finish-load', async () => {
|
||||||
|
try {
|
||||||
|
const result = await win.webContents.executeJavaScript(`({
|
||||||
|
title: document.title,
|
||||||
|
hasJsZip: typeof window.JSZip === 'function',
|
||||||
|
hasNativeBridge: !!window.webavpNative?.verifyCertificateOnline
|
||||||
|
})`);
|
||||||
|
if (result.title !== 'Alta Video Player' || !result.hasJsZip || !result.hasNativeBridge) {
|
||||||
|
console.error('[WebAVP] Electron smoke checks failed:', JSON.stringify(result));
|
||||||
|
app.exit(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('[WebAVP] Electron smoke loaded webavp://app/index.html with JSZip and native bridge');
|
||||||
|
setTimeout(() => app.quit(), Number.isFinite(quitAfterMs) ? quitAfterMs : 250);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[WebAVP] Electron smoke failed:', err);
|
||||||
|
app.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
win.webContents.once('did-fail-load', (_event, errorCode, errorDescription, validatedURL) => {
|
||||||
|
console.error(`[WebAVP] Electron smoke failed to load ${validatedURL}: ${errorCode} ${errorDescription}`);
|
||||||
|
app.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
win.webContents.setWindowOpenHandler(({ url }) => {
|
||||||
|
shell.openExternal(url);
|
||||||
|
return { action: 'deny' };
|
||||||
|
});
|
||||||
|
|
||||||
|
return win;
|
||||||
|
}
|
||||||
|
|
||||||
|
app.whenReady().then(() => {
|
||||||
|
registerAppProtocol();
|
||||||
|
ipcMain.handle('certificate:verify-online', verifyCertificateOnline);
|
||||||
|
createWindow();
|
||||||
|
|
||||||
|
app.on('activate', () => {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (process.platform !== 'darwin') app.quit();
|
||||||
|
});
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
const { contextBridge, ipcRenderer } = require('electron');
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('webavpNative', {
|
||||||
|
isElectron: true,
|
||||||
|
verifyCertificateOnline({ serial, certificateHash }) {
|
||||||
|
return ipcRenderer.invoke('certificate:verify-online', { serial, certificateHash });
|
||||||
|
},
|
||||||
|
});
|
||||||
Generated
+4102
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"name": "webavp-desktop",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Standalone desktop shell for Alta Video Player.",
|
||||||
|
"main": "electron/main.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "electron .",
|
||||||
|
"check": "node --check electron/main.js && node --check electron/preload.js && python3 -m py_compile app.py",
|
||||||
|
"smoke": "WEBAVP_DISABLE_GPU=1 WEBAVP_SMOKE_QUIT_AFTER_MS=250 electron .",
|
||||||
|
"dist": "electron-builder"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"electron": "^31.7.7",
|
||||||
|
"electron-builder": "^24.13.3"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"appId": "com.alta.webavp",
|
||||||
|
"productName": "Alta Video Player",
|
||||||
|
"files": [
|
||||||
|
"electron/**",
|
||||||
|
"static/**",
|
||||||
|
"templates/**",
|
||||||
|
"package.json"
|
||||||
|
],
|
||||||
|
"linux": {
|
||||||
|
"target": [
|
||||||
|
"AppImage",
|
||||||
|
"deb"
|
||||||
|
],
|
||||||
|
"category": "AudioVideo"
|
||||||
|
},
|
||||||
|
"mac": {
|
||||||
|
"target": "dmg",
|
||||||
|
"category": "public.app-category.video"
|
||||||
|
},
|
||||||
|
"win": {
|
||||||
|
"target": "nsis"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
flask
|
|
||||||
requests
|
|
||||||
gunicorn
|
|
||||||
+10
-963
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user