Plan de implementación — Optrax V-Station
Documento técnico de implementación
Módulos de visualización en vivo, grabación continua 24/7, retención y evidencia legal
| Campo | Valor |
|---|---|
| Versión | 2.0 |
| Fecha | Septiembre 2026 |
| Producto | vstation (Optrax V-Station) |
| Referencia anterior | Plan Miraflores v1.0 (Jun 2026) |
| Estado arquitectura | Producción validada en vstation.optrax.io |
Índice
- Propósito y alcance
- Arquitectura general (v2)
- Estándares transversales
- Módulo A — Visualización en vivo
- Módulo B — Grabación continua
- Retención, ILM y evidencia legal
- Playback y operación en UI
- Integración Optrax / Core
- Despliegue e infraestructura
- Plan de implementación por fases
- Pruebas de aceptación
- Operación, monitoreo y mantenimiento
- Cambios respecto al plan v1.0
- Riesgos y mitigaciones
- Requerimientos de hardware y dimensionamiento
- Anexos
1. Propósito y alcance
Este documento define la implementación técnica de vstation como plataforma de videovigilancia Optrax, actualizada a la arquitectura v2 desplegada en producción.
Alcance funcional
| Módulo | Descripción |
|---|---|
| A — Visualización en vivo | Operadores consultan cámaras RTSP en navegador vía WebRTC (LiveKit). Ingesta on-demand (solo cámaras visibles). |
| B — Grabación continua | MediaMTX hace pull RTSP directo desde cámaras/NVR, segmenta y sube a MinIO. vstation orquesta paths, retención e ILM. |
| Retención y evidencia | Política por grupo (retention_days), expiración ILM en MinIO, retención legal por objeto (Object Lock). |
| Playback | Revisión de segmentos desde MinIO en la UI (timeline, menú de fragmentos, filtro por fecha). |
| Transversal | Auth JWT, roles, salud de cámaras (gst-discoverer), integración Optrax Core, mapas, eventos temporales. |
Fuera de alcance
- Planos eléctricos, rack físico y direccionamiento IP definitivo del cliente.
- Sustitución del clúster MinIO distribuido de Miraflores (este documento describe la lógica de software; el dimensionamiento físico sigue en
docs/capacity-recommendations.md).
2. Arquitectura general (v2)
Principio rector
Dos pipelines independientes — vivo ≠ grabación — sin duplicar tráfico ni procesos ffmpeg innecesarios en vstation.
┌──────────────────────────────────────────────────────────────────────────────┐
│ MÓDULO A — VIVO (on-demand) │
│ │
│ Cámara RTSP ──► GStreamer WHIP (vstation) ──► LiveKit SFU ──► Navegador │
│ solo cuando alguien abre la cámara │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ MÓDULO B — GRABACIÓN (24/7, modo pull) │
│ │
│ Cámara RTSP ──► MediaMTX (pull path ingest/camera-{id}) │
│ │ record + hook minio.sh │
│ ▼ │
│ MinIO bucket vstation-lock (Object Lock) │
│ ▲ │
│ vstation ── API MediaMTX :9997 (alta/baja paths) │
│ ── ILM lifecycle rules por camera_groups.retention_days │
│ ── playback proxy / legal hold / purge workers │
└──────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ PLANO DE CONTROL — vstation-app │
│ React + Express + PostgreSQL + Redis │ JWT │ workers de grabación/retención │
└──────────────────────────────────────────────────────────────────────────────┘
Componentes y roles
| Componente | Rol | Ubicación típica |
|---|---|---|
| vstation-app | UI, API, ingesta WHIP, orquestación grabación, playback, ILM | Droplet app (Docker) |
| PostgreSQL | Inventario cámaras, grupos, usuarios, permisos, metadata | Mismo droplet / HA |
| Redis | Eventos temporales de cámaras (1 h retención) | Mismo droplet |
| LiveKit | SFU WebRTC + WHIP Ingress | Servidor dedicado o cloud |
| MediaMTX | Pull RTSP + grabación por segmentos | Servidor dedicado (ej. :8554 RTSP, :9997 API) |
| MinIO | Object storage vstation-lock con Object Lock | Servidor dedicado (:9000) |
| Martin | Mapas vectoriales self-hosted (Perú .mbtiles) | Contenedor en droplet app |
| Nginx + Certbot | TLS, reverse proxy, /api, WebSocket LiveKit | Droplet app |
Lo que ya NO forma parte de la arquitectura v2
| Eliminado / obsoleto | Sustituto |
|---|---|
| 4 servidores "Optrax Recorder" con ffmpeg por 200 cámaras | MediaMTX pull directo (0 ffmpeg en vstation) |
Relay ffmpeg vstation → MediaMTX (VSTATION_RECORDING_MODE=relay) | Modo pull (legacy solo si se exige) |
Snapshots JPEG por cámara (GET /api/cameras/:id/snapshot) | Eliminado |
| ffprobe como detector principal de codec/salud | gst-discoverer-1.0 (+ fallback ffprobe) |
| Grabación vía egress LiveKit a MinIO | Pull MediaMTX → MinIO |
Edición manual de mediamtx.yml por cada cámara | API MediaMTX sincronizada desde vstation |
3. Estándares transversales
Red y seguridad
vstation no configura VLANs. La segmentación es infraestructura (switch/firewall del cliente o cloud). El software solo necesita conectividad IP + puertos abiertos según §3.1 y §15.0.2.
- Firewall (obligatorio en todos los despliegues): API MediaMTX
:9997solo desde vstation; MinIO:9000solo desde MediaMTX + vstation; PostgreSQL/Redis no expuestos a internet. - TLS: Certificados Let's Encrypt vía Certbot en Nginx (
vstation.optrax.io). - Auth: JWT local + Auth Gateway / Keycloak (
AUTH_GATEWAY_URL,auth.optrax.io); sesiones Express legacy; permisos granulares (view_camera,edit_camera, etc.). Al cambiar rol de usuario se resetean permisos al set del rol (resetUserPermissionsToRole). - Embed / integraciones: rutas
/embed/(iframe Patrol-Master, CSP en Nginx); API internaINTERNAL_SERVICE_KEY+/api/internal/cameras. - Telemetría: OpenTelemetry opcional (
OTEL_*endocker-compose.yml).
3.1 Segmentación de red — qué ya está y qué falta
| Modelo | Estado | Cómo se segmenta | Acción en vstation |
|---|---|---|---|
| Optrax cloud (prod actual) | Implementado | Droplets separados (app, LiveKit, MediaMTX, MinIO) + Cloud Firewall DO; red Docker interna vstation-net (app ↔ PG ↔ Redis ↔ Martin en el mismo host) | Ninguna. Solo mantener reglas firewall y URLs en .env / nginx/conf.d/app.conf |
| On-prem / Miraflores (800+ cámaras) | Depende del cliente | VLANs L2 en core switch municipal (si ya existen del plan v1.0, reutilizar) | Ninguna en software. Validar rutas RTSP y firewall entre VLANs |
Producción Optrax actual (referencia — ya operativa):
| Servicio | Host | Segmentación |
|---|---|---|
| vstation + Nginx | 64.23.135.236 | Público HTTPS :443; proxy a LiveKit y MinIO |
| LiveKit SFU | 146.190.159.52 | WebRTC vía /livekit-ws/ en Nginx |
| MediaMTX | 161.35.100.138 | API :9997; pull RTSP desde cámaras |
| MinIO | 146.190.47.200 | S3 :9000; acceso restringido por firewall |
No hay VLAN L2 entre estos nodos: la separación es por servidor + reglas de firewall. Eso cumple el objetivo de aislar grabación, vivo y almacenamiento sin configurar VLANs en la app.
3.2 Implementación recomendada de VLANs (on-prem / Miraflores)
Diseño óptimo para vstation v2: 5 VLANs + firewall L3 central. Aísla cámaras, grabación 24/7, vivo WebRTC, almacenamiento y operadores. Alineado con los dos pipelines independientes (§2) y con el dimensionamiento §15.
Si Miraflores ya tiene VLAN de videovigilancia (plan v1.0): conservar IDs/subredes existentes y mapear los roles de la tabla siguiente; no rediseñar L2. Solo añadir ACLs de §3.2.4.
Optrax cloud: no usar VLANs L2; el equivalente es §3.2.6 (VPC + Cloud Firewall).
3.2.1 Topología lógica
┌─────────────────────────────────────────┐
VLAN 50 Operadores │ PCs videowall / estaciones de mando │
(Corp) │ Solo HTTPS :443 → vstation │
└──────────────────┬──────────────────────┘
│
┌──────────────────▼──────────────────────┐
│ Firewall L3 (pfSense / FortiGate) │
│ Único punto de routing inter-VLAN │
└──┬─────────┬─────────┬─────────┬────────┘
│ │ │ │
VLAN 21 App │ VLAN 22│ VLAN 20 │ VLAN 30 │
vstation+Nginx │ LiveKit │ MediaMTX│ MinIO │
WHIP + API │ SFU │ pull │ S3 │
│ │ │ │
└────┬────┴────┬────┘ │
│ │ │
│ RTSP │ S3 :9000 │
│ :554 │ │
┌───────▼─────────▼──────────────▼───────┐
VLAN 10 Cámaras/NVR │ 800 cámaras / NVR / switches PoE │
│ Sin internet · Sin acceso operadores │
└───────────────────────────────────────┘
VLAN 40 Gestión SSH, SNMP, cadvisor — solo desde jump host / VPN admin
Principio: ningún operador habla RTSP con cámaras. Todo pasa por vstation/LiveKit (vivo) o MediaMTX (grabación).
3.2.2 Tabla de VLANs (referencia Miraflores 800 cámaras)
| VLAN | Nombre | ID | CIDR ejemplo | Gateway | Servicios / hosts |
|---|---|---|---|---|---|
| 10 | VID-CAM | 10 | 10.50.10.0/23 | .1 | Cámaras IP, NVR, switches PoE acceso. Estática/DHCP reservado por MAC |
| 20 | VID-REC | 20 | 10.50.20.0/24 | .1 | MediaMTX (pull 24/7). 1–2 servidores según §15.9 |
| 21 | VID-APP | 21 | 10.50.21.0/24 | .1 | vstation (app, PG, Redis, Nginx). WHIP + orquestación MTX/MinIO |
| 22 | VID-LIVE | 22 | 10.50.22.0/24 | .1 | LiveKit SFU (WebRTC). Separado para escalar vivo sin tocar grabación |
| 30 | VID-STO | 30 | 10.50.30.0/24 | .1 | MinIO + backup. Sin ruta default a internet |
| 40 | VID-MGMT | 40 | 10.50.40.0/24 | .1 | Jump host, monitoreo, logs. Acceso admin vía VPN |
| 50 | CORP-OPS | (existente) | Red municipal | — | Operadores. Solo :443 → VIP vstation en VLAN 21 |
Por qué 5 VLANs de vídeo y no 3: separar MediaMTX (20) de LiveKit (22) evita que picos de WebRTC (20 operadores × 10 tiles, §15.4) compitan con 800 Mbps sostenidos de grabación. MinIO (30) aislado impide exfiltración directa de evidencia.
3.2.3 Colocación de servidores y direccionamiento
| Servidor | VLAN | IP ejemplo | Rol |
|---|---|---|---|
| MediaMTX-01 | 20 | 10.50.20.10 | Pull RTSP desde VLAN 10; upload segmentos → MinIO |
| vstation-01 | 21 | 10.50.21.10 | UI, API, GStreamer WHIP, workers ILM |
| LiveKit-01 | 22 | 10.50.22.10 | SFU; WHIP ingress desde vstation |
| MinIO-01 | 30 | 10.50.30.10 | Bucket vstation-lock |
| Nginx VIP / DNS | 21 | 10.50.21.20 o FQDN interno | TLS terminado; proxy /livekit-ws/, /minio-proxy/ |
.env on-prem (IPs privadas):
MEDIAMTX_API_URL=http://10.50.20.10:9997
MEDIAMTX_MINIO_ENDPOINT=http://10.50.30.10:9000
LIVEKIT_URL=wss://vstation.municipio.local/livekit-ws/
LIVEKIT_API_URL=https://10.50.22.10
VSTATION_STUN_SERVER= # vacío — LiveKit en misma red privada
3.2.4 Matriz ACL (firewall inter-VLAN) — mínimo necesario
Denegar todo por defecto; permitir solo lo siguiente:
| Origen | Destino | Puerto(s) | Protocolo | Motivo |
|---|---|---|---|---|
| VLAN 10 (cámaras) | VLAN 20 (MTX) | 554 | TCP | Grabación pull MediaMTX |
| VLAN 10 (cámaras) | VLAN 21 (vstation) | 554 | TCP | WHIP on-demand + gst-discoverer health |
| VLAN 21 (vstation) | VLAN 20 (MTX) | 9997 | TCP | API alta/baja paths grabación |
| VLAN 20 (MTX) | VLAN 30 (MinIO) | 9000 | TCP | Upload segmentos minio.sh |
| VLAN 21 (vstation) | VLAN 30 (MinIO) | 9000 | TCP | ILM, legal hold, playback, purge |
| VLAN 21 (vstation) | VLAN 22 (LiveKit) | 7880, 7881, UDP 50000-60000 | TCP/UDP | WHIP + API LiveKit |
| VLAN 50 (operadores) | VLAN 21 (vstation) | 443 | TCP | UI HTTPS |
| VLAN 50 (operadores) | VLAN 22 (LiveKit) | UDP 50000-60000, TCP 443 | UDP/TCP | WebRTC media (si no se tunela todo por Nginx) |
| VLAN 40 (mgmt) | VLAN 20,21,22,30 | 22 | TCP | SSH admin |
| Denegar | VLAN 10 ↔ VLAN 50 | * | * | Cámaras ↔ operadores |
| Denegar | VLAN 10 → VLAN 30 | * | * | Cámaras ↔ MinIO |
| Denegar | VLAN 50 → VLAN 30 | * | * | Operadores ↔ almacenamiento directo |
| Denegar | Internet → VLAN 10,20,30 | * | * | Superficie de ataque |
Puertos validados por tests unitarios (streamConnectivity.test.ts): RTSP 554, API MTX 9997, MinIO 9000, HTTPS 443 (§15.0.2).
3.2.5 QoS y capacidad de enlace
Para 800 cámaras @ 1000 kbps (§15.9):
| Enlace | Ancho mínimo | QoS recomendado |
|---|---|---|
| VLAN 10 → VLAN 20 (grabación) | ≥ 2 Gbps agregado | Cola strict priority RTSP; DSCP AF41 en switches capa 3 |
| VLAN 21 ↔ VLAN 22 (vivo pico) | 1 Gbps | Best-effort o AF31; vivo es on-demand |
| VLAN 20/21 → VLAN 30 (MinIO) | ≥ 2 Gbps | Prioridad escritura; NVMe en MinIO |
| VLAN 50 → VLAN 21 (operadores) | 100 Mbps agregado | Suficiente para 20×10 tiles WebRTC (~300 Mbps pico, §15.4) |
Doble pull: cámaras en vivo + grabación reciben 2 sesiones RTSP desde VLAN 21 y 20. Dimensionar VLAN 10 con ~20 % headroom sobre bitrate nominal.
3.2.6 Equivalente cloud (Optrax — ya implementado)
En DigitalOcean no hay VLAN L2; la misma lógica se logra así:
| Rol VLAN on-prem | Equivalente Optrax prod |
|---|---|
| VID-APP (21) | Droplet 64.23.135.236 + vstation-net Docker |
| VID-LIVE (22) | Droplet LiveKit 146.190.159.52 |
| VID-REC (20) | Droplet MediaMTX 161.35.100.138 |
| VID-STO (30) | Droplet MinIO 146.190.47.200 |
| Firewall L3 | Cloud Firewall por droplet (reglas §3.2.4) |
| VPC privado (opcional mejora) | Mover tráfico MTX↔MinIO↔vstation a VPC private IP y cerrar :9000/:9997 en IP pública |
3.2.7 Plan de implementación por fases (red)
| Fase | Tarea | Validación |
|---|---|---|
| R0 | Crear VLANs 10,20,21,22,30,40 en core switch; routing solo en firewall | ping inter-VLAN bloqueado excepto reglas ACL |
| R1 | Desplegar MinIO (30) + MediaMTX (20); abrir 10→20:554, 20→30:9000 | npm run recording:smoke desde jump host en VLAN 21 |
| R2 | Desplegar vstation (21) + LiveKit (22); abrir ACLs §3.2.4 | npm test + vivo WHIP 5 cámaras piloto |
| R3 | Conectar VLAN 10 (cámaras); import Optrax 800 cámaras | Paths MTX activos = cámaras con recordingEnabled |
| R4 | Abrir VLAN 50 → 21:443 para operadores | Pruebas aceptación §11 (A1–B5) |
| R5 | QoS en enlaces troncales hacia NVR; monitoreo cadvisor | Sin pérdida segmentos 24 h bajo carga |
3.2.8 Checklist rápido
- Cámaras sin gateway a internet (VLAN 10)
- MinIO sin IP pública; Object Lock activo
- MediaMTX API
:9997no expuesta a operadores ni cámaras -
VSTATION_STUN_SERVERvacío si LiveKit en VLAN 22 alcanzable desde vstation - NTP (chrony) sincronizado en VLAN 20,21,22,30
- Documentar mapa IP → servicio para operaciones
3.3 Red Docker local (nodo app — ya implementado)
En docker-compose.yml, los contenedores del mismo droplet comparten vstation-net. No sustituye VLANs entre servidores físicos; solo aísla PG/Redis del host.
Tiempo y nomenclatura
- NTP/chrony en todos los nodos (critical para correlación eventos ↔ segmentos).
- Prefijo MinIO:
recordings/camera-{id}/{yyyy}/{mm}/{dd}/segmento.mp4 - Path MediaMTX grabación:
ingest/camera-{id}
Contenedores Docker (producción)
Servicios en docker-compose.yml:
| Servicio | Imagen / build | Puerto |
|---|---|---|
vstation-app | Build local (Dockerfile multi-stage + GStreamer WHIP) | 5001 |
vstation-db | postgres:16-alpine | 5432 |
vstation-redis | redis:7-alpine | 6379 |
vstation-martin | ghcr.io/maplibre/martin | 3000 |
optrax_nginx | reverse proxy | 443 |
| Observabilidad | cadvisor, node-exporter, promtail, postgres-exporter | varios |
4. Módulo A — Visualización en vivo
Flujo técnico
Operador abre cámara en UI
→ vstation solicita token LiveKit
→ startRtspIngestForCamera (si no hay ingesta activa)
→ GStreamer: rtspsrc → depay → parse → pay → whipsink
→ LiveKit WHIP Ingress → room camera-{id}
→ Cliente WebRTC (livekit-client) reproduce en <video>
Operador cierra / desmonta tile
→ stopRtspIngestForCamera + desconexión sala
Configuración recomendada
VSTATION_INGEST_MODE=whip
VSTATION_H265_PASSTHROUGH=false # true solo si todos los clientes soportan HEVC
VSTATION_STUN_SERVER=stun://stun.l.google.com:19302 # vacío si LiveKit en misma red
LIVEKIT_URL=wss://livekit.ejemplo.com
LIVEKIT_API_URL=https://livekit.ejemplo.com
LIVEKIT_API_KEY=...
LIVEKIT_API_SECRET=...
| Variable | Valor | Motivo |
|---|---|---|
VSTATION_INGEST_MODE | whip | Menor latencia; sin puerto RTMP intermedio |
| Perfil cámara | H.264 nativo | Evita transcode (~45% CPU/cámara con H.265) |
| Ingesta | On-demand | Solo cámaras abiertas consumen CPU/red |
Salud de cámaras (online/offline)
- Primario:
gst-discoverer-1.0sobre URL RTSP (codec, presencia de vídeo). - Fallback: ffprobe si discoverer no está disponible.
- Último recurso: probe TCP al puerto RTSP (menos fiable).
- Paquete Docker requerido:
gstreamer1.0-plugins-base-apps(incluyegst-discoverer-1.0).
Worker periódico en server/utils/statusChecker.ts actualiza cameras.status e historial.
Dimensionamiento orientativo
Ver docs/capacity-recommendations.md — escenario 20 operadores × 10 streams distintos = 200 ingestas WHIP ≈ 27 GB RAM / 21 cores en servidor de ingesta+SFU.
5. Módulo B — Grabación continua
Modo recomendado: pull
vstation no lanza ffmpeg por cámara. Sincroniza paths en MediaMTX vía API cada 30 s (reconcileRecordingPolicies):
recordingEnabled=true en grupo + storage MinIO configurado
→ PATCH ingest/camera-{id} { source: rtsp://..., record: true }
recordingEnabled=false o cámara disabled
→ DELETE ingest/camera-{id}
Configuración vstation
VSTATION_RECORDING_MODE=pull
MEDIAMTX_RTSP_URL= # vacío en modo pull
MEDIAMTX_API_URL=http://mediamtx:9997
MEDIAMTX_API_USER=admin
MEDIAMTX_API_PASSWORD=...
MEDIAMTX_MINIO_ENDPOINT=http://minio:9000
MEDIAMTX_MINIO_ACCESS_KEY=...
MEDIAMTX_MINIO_SECRET_KEY=...
MEDIAMTX_MINIO_REGION=us-east-1
VSTATION_FASTSTART_WORKER=false # desactivar a escala (>500 cámaras)
Configuración MediaMTX (servidor de grabación)
Elementos clave en mediamtx.yml:
api: yes
apiAddress: :9997
paths:
ingest/camera-~^([0-9]+)$:
record: yes
recordPath: /recordings/camera-%path/ %Y/%m/%d/%H-%M-%S
recordFormat: fmp4
runOnRecordSegmentComplete: /usr/local/bin/minio.sh $MTX_SEGMENT_PATH
El script minio.sh sube cada segmento al bucket vstation-lock con prefijo recordings/camera-{id}/....
Modo legacy relay (no recomendado a escala)
VSTATION_RECORDING_MODE=relay
MEDIAMTX_RTSP_URL=rtsp://mediamtx:8554
Un proceso ffmpeg -c copy por cámara en vstation publica a {MEDIAMTX_RTSP_URL}/ingest/camera-{id}. Válido para decenas de cámaras; no para 800–1500.
Comparativa pull vs relay vs plan v1.0
| Aspecto | Plan v1.0 (4 recorders) | Relay vstation | Pull v2 (actual) |
|---|---|---|---|
| Procesos grabación en vstation | 0 (recorders externos) | 1 × cámara | 0 |
| Saltos de red | Cámara → recorder → MinIO | Cámara → vstation → MTX → MinIO | Cámara → MTX → MinIO |
| Control on/off desde UI | Manual por recorder | Automático | Automático vía API MTX |
| Escalabilidad 800+ cams | Diseñado para ello | No | Sí (MTX dedicado) |
6. Retención, ILM y evidencia legal
Política de retención por grupo
- Campo BD:
camera_groups.retention_days(1–365). - vstation sincroniza reglas ILM en MinIO por prefijo
recordings/camera-{id}/. - Importante (fix v2): ILM se mantiene aunque
recordingEnabled=falseen el grupo — los segmentos ya subidos siguen expirando según política.
Capas de expiración
| Mecanismo | Alcance | Mínimo |
|---|---|---|
| ILM MinIO | Todos los objetos del prefijo sin legal hold | 1 día |
| Purge worker vstation | Filas en tabla recordings (modo egress legacy) | Según retention_days |
Test purge (VSTATION_RETENTION_TEST_MINUTES=1) | Solo dev/staging | 1 minuto |
recordDeleteAfter en MediaMTX solo limpia disco local del nodo MTX; no es la retención de negocio.
Retención legal (evidencia)
| Aspecto | Comportamiento |
|---|---|
| Default | Sin legal hold (legal_hold=false) |
| Activación UI | Menú fragmentos 🎬 → icono candado 🔒/🔓 (permiso edit_camera) |
| Segmentos pull (MinIO) | PATCH /api/cameras/:id/recordings/object-legal-hold |
| Grabaciones BD (egress) | PATCH /api/recordings/:id/legal-hold |
| Efecto | Object Lock ON en MinIO → ILM no borra ese objeto |
| Requisito bucket | Object Lock habilitado en vstation-lock |
7. Playback y operación en UI
Fuentes de fragmentos
| Fuente | Origen | Uso |
|---|---|---|
source: minio | Listado directo S3 (modo pull) | Producción actual |
source: db | Tabla recordings + validación MinIO | Egress LiveKit legacy |
Endpoint: GET /api/cameras/:id/recordings/window?start=&end=
| Modo | Ventana máxima efectiva | Implementación |
|---|---|---|
| Listado MinIO (pull, prod) | 73 h (menú UI 72 h) | RECORDING_WINDOW_MAX_MS_MINIO en server/utils/recordingWindow.ts |
| Validación BD (egress legacy) | 6 h | RECORDING_WINDOW_MAX_MS_DB — evita timeouts por HeadObject masivo |
Respuesta incluye requestedStart, start (posible recorte), truncated, source: minio|db.
Reproductor en tile de cámara
- Timeline visible: ventana 60 min (eventos + posición playback).
- Menú fragmentos: ventana de consulta 72 h.
- Calendario en menú: filtra fragmentos por día (días con grabaciones en blanco, sin grabaciones en gris).
- Candado por fragmento: retención legal sin salir del menú.
- Click en fragmento: carga directa del segmento (fix ratio 72h vs 60min).
URLs de playback
VSTATION_PLAYBACK_SIGNED_URLS=true
VSTATION_PLAYBACK_SIGNED_URL_TTL_SECONDS=900
Proxy: GET /api/cameras/:id/recordings/object-playback?key=...
8. Integración Optrax / Core
Sincronización con core.optrax.io (Face-Dashboard):
| Endpoint | Función |
|---|---|
GET /api/optrax/status | Estado de configuración |
GET /api/optrax/preview | Preview de cámaras sin importar |
POST /api/optrax/sync | Importar/actualizar cámaras |
POST /api/optrax/sync-groups | Sincronizar grupos desde Core |
GET /api/optrax/playback/redirect | Deep-link playback por external_id |
GET /api/optrax/playback/resolve | Resolver external_id → cameraId (JSON) |
Variables:
OPTRAX_API_URL=https://core.optrax.io
OPTRAX_API_TOKEN=...
OPTRAX_CAMERAS_PATH=/api/device-cameras
Al sincronizar, vstation respeta grupos, coordenadas y dispara reconcileRecordingPolicies para paths MTX.
9. Despliegue e infraestructura
Referencia producción actual (Optrax)
| Recurso | Host | Notas |
|---|---|---|
| vstation | 64.23.135.236 / vstation.optrax.io | Docker Compose, rama main |
| MediaMTX | 161.35.100.138:9997 | API + grabación |
| MinIO | 146.190.47.200:9000 | Bucket vstation-lock |
| LiveKit | 146.190.159.52 | SFU; expuesto vía Nginx /livekit-ws/ |
Red: segmentación por droplet + firewall (§3.1). VLANs no aplican en cloud; diseño on-prem en §3.2.
Procedimiento de deploy
# En servidor app
cd /root/vstation
git pull origin main
docker compose up -d --build
# Verificación
curl -s https://vstation.optrax.io/api/health
docker logs vstation-app --tail 50 | grep recording
Artefactos de infra en repositorio
| Archivo | Uso |
|---|---|
infra/mediamtx.example.yml | Plantilla MediaMTX grabación |
infra/minio-upload.example.sh | Hook upload segmentos |
promtail-config.example.yml | Copiar a promtail-config.yml (gitignored) |
docs/runbook-incidentes.md | Runbook operativo §12 / Fase 4 |
.github/workflows/test.yml | CI: npm test + npm run check |
Checklist .env producción — grabación pull (Optrax)
-
VSTATION_RECORDING_MODE=pull(prod Optrax) -
MEDIAMTX_RTSP_URLvacío -
MEDIAMTX_API_URL+ credenciales API -
MEDIAMTX_MINIO_*apuntando al MinIO de grabación - Grupos con
recordingEnabled=trueyretention_daysdefinido - Bucket
vstation-lockcon Object Lock - Hook
minio.shactivo en MediaMTX (servidor MTX; verinfra/)
Checklist .env producción — vivo (Optrax)
-
VSTATION_INGEST_MODE=whip - LiveKit URL/API key/secret
- Cámaras H.264 donde sea posible (continuo)
-
gst-discoverer-1.0presente en imagen Docker (verificado en build)
10. Plan de implementación por fases
Fase 0 — Infraestructura base (1–2 semanas)
- Droplet/servidor app: Docker, PostgreSQL, Redis, Nginx, TLS (Optrax)
- Servidor LiveKit (SFU + WHIP ingress) (Optrax)
- Servidor MediaMTX + script MinIO (Optrax; plantillas en
infra/) - MinIO
vstation-lockcon Object Lock + ILM habilitado (Optrax) - Optrax cloud: segmentación por droplets + Cloud Firewall (§3.1 — ya implementado)
- On-prem: implementar VLANs §3.2 (5 VLANs + ACL §3.2.4) o reutilizar las existentes del plan v1.0
- Fases R0–R5 de red completadas (§3.2.7)
- Reglas firewall puertos §3.2.4 / §15.0.2 verificadas en cada nodo
Fase 1 — vstation core (1 semana)
- Deploy vstation-app desde GitHub
main(Optrax) - Setup inicial + usuario admin
- Importar cámaras piloto (manual o Optrax sync)
- Validar vivo WHIP (5–10 cámaras)
- Validar salud gst-discoverer
Fase 2 — Grabación pull (1–2 semanas)
- Configurar
VSTATION_RECORDING_MODE=pull - Activar
recordingEnableden grupo piloto - Verificar paths
ingest/camera-*en MediaMTX API - Verificar objetos en MinIO bajo
recordings/camera-{id}/ - Validar playback en UI + timeline
Fase 3 — Retención y evidencia (1 semana)
- Definir
retention_dayspor grupo - Confirmar reglas ILM en MinIO (
mc ilm rule list) - Probar retención legal desde UI (candado)
- Confirmar que ILM no borra objetos con legal hold ON
Fase 4 — Escala y operación (continuo)
- Import masivo Optrax (800+ cámaras) (Miraflores / cliente)
- Ajustar
recordSegmentDurationa 60s víaPATCH /api/mediamtx/config - Monitoreo cadvisor (compose); [ ] alertas Prometheus/reglas
- Simulaciones:
npm run stream:sim,npm run recording:sim - Runbook de incidentes →
docs/runbook-incidentes.md
11. Pruebas de aceptación
Automatizadas (CI local)
npm test
Suite actual (CI: .github/workflows/test.yml — npm test + npm run check):
| Área | Archivo | Implicación HW (resumen) |
|---|---|---|
| gst-discoverer / codec | server/utils/gstDiscoverer.test.ts | GStreamer + gst-discoverer en Docker; H.264 passthrough vs H.265 transcode/GPU |
| Conectividad RTSP | server/utils/streamConnectivity.test.ts | Firewall 554/tcp; RTSP accesible desde vstation y MediaMTX |
| Ventana playback MinIO/BD | server/utils/recordingWindow.test.ts | Coherencia menú 72 h (MinIO) vs validación BD 6 h |
| Segmentos MinIO / prefijos día | server/utils/recordingSegments.test.ts | Keys .fmp4, parseo timestamp MTX, prefijos listado 72 h |
| Modo grabación pull/relay | server/services/recordingMode.test.ts | resolveRecordingMode según .env |
| Retención / ILM / purge | server/services/recordingRetention.test.ts | MinIO Object Lock; TB = f(cámaras, bitrate, días); legal hold = storage extra |
| Timeline / fragmentos UI | client/src/lib/timelineFragments.test.ts | Sin impacto servidor; carga en PC operador (decode vídeo) |
Detalle completo de requerimientos derivados: §15.0.
Manuales — Módulo A (vivo)
| # | Prueba | Criterio |
|---|---|---|
| A1 | Abrir cámara RTSP H.264 | Video en menos de 5 s, badge Live |
| A2 | Cerrar tile / cambiar página | Ingesta se detiene (sin procesos huérfanos) |
| A3 | 10 cámaras simultáneas | CPU/RAM dentro de plan |
| A4 | Cámara offline | Estado "Fuera de línea" en ≤2 ciclos de health check |
| A5 | Cámara H.265 | Transcode o passthrough según config; Firefox documentado |
Manuales — Módulo B (grabación)
| # | Prueba | Criterio |
|---|---|---|
| B1 | Activar grabación en grupo | Path MTX creado con record: true |
| B2 | Esperar 2–3 segmentos | Objetos visibles en MinIO y menú fragmentos |
| B3 | Desactivar grabación | Path MTX eliminado; segmentos previos permanecen |
| B4 | Playback click fragmento | Entra en modo PLAYBACK del segmento correcto |
| B5 | Calendario | Días con grabaciones en blanco; filtro por fecha funciona |
Manuales — Retención / legal hold
| # | Prueba | Criterio |
|---|---|---|
| R1 | Cambiar retention_days | ILM actualizado en MinIO |
| R2 | Activar candado en fragmento | Legal hold ON en MinIO; objeto no expira |
| R3 | Quitar candado | ILM puede expirar según edad |
Smoke script grabación pull
npm run recording:smoke
12. Operación, monitoreo y mantenimiento
Logs clave
docker logs vstation-app 2>&1 | grep -E '\[recording\]|\[camera\]|\[minio'
Mensajes esperados al arrancar:
[recording] Recording workers started (mode=pull + MinIO ILM)
Métricas recomendadas
| Métrica | Fuente | Alerta |
|---|---|---|
| CPU/RAM app | cadvisor | >80% sostenido |
| Paths MTX activos | GET /v3/paths/list | < cámaras con recording enabled |
| Espacio MinIO | MinIO console / API | >85% |
| ILM pending | MinIO | objetos sin expirar anómalos |
| LiveKit rooms | LiveKit API | rooms huérfanas |
Tareas periódicas
| Frecuencia | Tarea |
|---|---|
| Diaria | Revisar cámaras offline persistentes |
| Semanal | Verificar espacio MinIO y healing |
| Mensual | Revisar reglas ILM vs grupos en BD |
| Por release | git pull + docker compose up -d --build + health check |
Ajuste segmentos MediaMTX (sin redeploy)
PATCH /api/mediamtx/config
Authorization: Bearer <token>
Content-Type: application/json
{ "recordSegmentDuration": "60s" }
13. Cambios respecto al plan v1.0
| Tema | Plan v1.0 (Miraflores) | Arquitectura v2 (actual) |
|---|---|---|
| Grabación | 4 servidores recorder dedicados | MediaMTX pull centralizado |
| Vivo | MediaMTX → RTMP → LiveKit Ingress | GStreamer WHIP directo → LiveKit |
| Control de grabación | Por servidor recorder | vstation ↔ API MediaMTX |
| Orquestación UI | V-Studio separado | vstation unificado (React) |
| Salud cámaras | No especificado | gst-discoverer + fallback |
| Snapshots | Implícito en NVR | Eliminado en vstation |
| Retención legal | No detallado | Object Lock + UI candado |
| Playback | Metadata DB por segmento | Listado MinIO + proxy |
| Tests | Manuales en plan | 39 unit tests automatizados |
14. Riesgos y mitigaciones
| Riesgo | Impacto | Mitigación |
|---|---|---|
| MediaMTX API inaccesible | No alta/baja paths grabación | Monitoreo + credenciales API; retry en worker |
| MinIO saturado (IOPS) | Pérdida segmentos | NVMe, segmentos 60s, EC en clúster |
| 200+ ingestas WHIP | CPU/RAM app | Servidor ingesta dedicado; H.264 nativo |
| H.265 en Firefox | Pantalla negra | Transcode o documentar browser soportado |
| Legal hold masivo | Storage sin límite | Proceso operativo; solo evidencia real |
Ventana API en /recordings/window | BD limitada 6 h; MinIO hasta 73 h | Modo pull usa listado MinIO alineado al menú 72 h; calendario por día |
15. Requerimientos de hardware y dimensionamiento
Esta sección consolida tres fuentes de evidencia del repositorio:
| Fuente | Comando / artefacto | Qué dimensiona |
|---|---|---|
| Tests unitarios | npm test (39/39 pass) | Paquetes Docker, MinIO Object Lock, ILM, red RTSP, perfil codec |
| Simulación grabación | npm run recording:sim | RAM/CPU/red/disco MediaMTX + MinIO por N cámaras 24/7 |
| Simulación vivo | npm run stream:sim | RAM/CPU/red LiveKit + WHIP por operadores × tiles |
Perfil de referencia: 500 cámaras grabando 24/7 + operadores viendo cámaras distintas (--layout varied). Escenarios 800 (Miraflores) y 1300 incluidos en matrices finales.
15.0 Requerimientos derivados de tests unitarios
Suite CI (npm test, septiembre 2026): 39 tests, 19 suites, 0 fallos.
15.0.1 gstDiscoverer.test.ts — salud de cámara y perfil codec
| Test / comportamiento validado | Requerimiento de hardware / infra |
|---|---|
Detección H.264 (normalizeVideoCodec, parseDiscovererOutput) | Cámaras H.264 nativas → ingesta WHIP passthrough, 0 % GPU servidor, ~88 MB RAM/cámara activa |
| Detección H.265 / HEVC | Si VSTATION_H265_PASSTHROUGH=true: decode solo Chrome/Edge (no Firefox). Si false: transcode GStreamer → ~45 % CPU/cámara; con 100 cámaras ≈ 45 cores → GPU NVENC/VAAPI obligatoria |
| VP8 y otros codecs detectados | No soportados en pipeline CCTV actual; reconfigurar cámara a H.264 |
runGstDiscoverer retorna null si binario ausente | Imagen Docker debe incluir gst-discoverer-1.0 (gstreamer1.0-plugins-base-apps). Sin él: fallback ffprobe (menor precisión) |
Probe periódico por cámara (worker statusChecker) | Conectividad RTSP saliente desde vstation-app hacia cada cámara/NVR; ancho de banda despreciable (~KB/s) pero 554/tcp debe estar abierto |
Paquetes mínimos en contenedor vstation-app:
gstreamer1.0-tools
gstreamer1.0-plugins-base
gstreamer1.0-plugins-good
gstreamer1.0-plugins-bad # whipsink
gstreamer1.0-plugins-base-apps # gst-discoverer-1.0
15.0.2 streamConnectivity.test.ts — conectividad RTSP
| Test validado | Requerimiento de red / firewall |
|---|---|
parseHostPort RTSP puerto 554 por defecto | Regla firewall: 554/tcp cámara/NVR ↔ vstation (salud + vivo) y ↔ MediaMTX (grabación) |
RTSP URL inválida → null | Sin impacto HW; documentar formato rtsp://user:pass@host:554/path |
defaultPortForProtocol RTMP 1935 | Solo si VSTATION_INGEST_MODE=rtmp (legacy): 1935/tcp cámara → vstation |
| Probe TCP fallback | Puerto destino accesible; no sustituye RTSP real en producción |
VLAN vs firewall: estos tests validan conectividad IP/puerto, no VLAN IDs. En Optrax cloud la segmentación ya está resuelta por droplets (§3.1). VLANs L2 solo aplican en on-prem (§3.2) y las configura el switch del cliente, no vstation.
Matriz de puertos (firewall — independiente del modelo VLAN):
| Origen → Destino | Puerto | Protocolo | Uso |
|---|---|---|---|
| Cámara/NVR → MediaMTX | 554 | RTSP/TCP | Grabación pull 24/7 |
| Cámara/NVR → vstation | 554 | RTSP/TCP | WHIP on-demand + health check |
| vstation → MediaMTX | 9997 | HTTP | API alta/baja paths |
| MediaMTX → MinIO | 9000 | S3/HTTP | Upload segmentos |
| vstation → MinIO | 9000 | S3/HTTP | ILM, legal hold, playback |
| Operador → LiveKit | 443 | WSS/WebRTC | Visualización vivo |
| Operador → vstation | 443 | HTTPS | UI + playback proxy |
15.0.3 recordingRetention.test.ts — almacenamiento, ILM y legal hold
| Test / regla validada | Requerimiento de hardware / capacidad |
|---|---|
clampRetentionDays 1–365 días | Disco MinIO = cámaras × bitrate × retentionDays (ver §15.3 y §15.10) |
buildVstationIlmRules — una regla ILM por cámara | MinIO soporta miles de reglas; a 800+ cámaras monitorizar latencia de PutBucketLifecycleConfiguration |
collectCameraRetentionRules — ILM independiente de recordingEnabled | Reglas activas aunque grabación pausada; no reduce almacenamiento hasta purge manual |
partitionLifecycleRules — preserva reglas ajenas | Bucket dedicado vstation-lock; no compartir con otros workloads ILM |
purgeExpiredRecordings — borra objeto + marca BD | Worker vstation: CPU/RAM mínimos; IOPS MinIO escala con segmentos/hora → preferir segmentos 60 s |
Legal hold ON → no purge (no elimina grabaciones con legal hold) | MinIO Object Lock habilitado en bucket (Compliance/Governance según política). Objetos retenidos no cuentan en ILM → planificar overhead de storage para evidencia |
parseTestRetentionMinutesFromEnv — modo minutos | Solo dev/staging (VSTATION_RETENTION_TEST_MINUTES); sin impacto en dimensionamiento producción |
Fallo borrado MinIO → no marca deleted | Red estable vstation ↔ MinIO; reintentos no deben saturar API S3 |
Fórmula almacenamiento grabación (H.264 @ bitrate B kbps, R días, N cámaras):
TB_total ≈ N × (B / 8) × 86_400 × R / 1e12
Ejemplo validado por simulación: 800 cams × 1000 kbps × 45 d → ~427 TB (con margen operativo).
15.0.4 timelineFragments.test.ts — playback UI (cliente)
| Test validado | Impacto hardware |
|---|---|
| Ratio ventana 60 min vs menú 72 h | Ninguno en servidor — lógica cliente |
| Calendario / filtro por día local | Ninguno en servidor |
findFragmentCoveringTime / seek en fragmento | Playback vía proxy vstation → MinIO: egress MinIO proporcional a operadores revisando grabaciones (típico ≪ vivo) |
PC operador (playback + vivo): ver §15.6. Los tests confirman que la carga de timeline es despreciable frente al decode de vídeo.
15.0.5 Checklist hardware mínimo por subsistema (derivado de tests + sims)
| Subsistema | Componente | Mínimo (tests) | Recomendado (500 cams + 10 ops) |
|---|---|---|---|
| vstation-app | CPU | 2 cores | 4 cores (sin ffmpeg grabación) |
| RAM | 2 GB | 4 GB (+ PG/Redis en mismo nodo: +2 GB) | |
| Disco | 20 GB | 50 GB SSD | |
| Paquetes | gst-discoverer + GStreamer WHIP | Igual + ffprobe fallback | |
| MediaMTX grabación | CPU | 1.5 cores / 200 cams | 5 cores / 500 cams |
| RAM | 5 GB / 200 cams | 12 GB / 500 cams | |
| Red | 200 Mbps / 200 cams | ≥ 1 Gbps / 500 cams | |
| MinIO | Disco | NVMe/SSD | RAID/NVMe ≥ 750 MB/s escritura sostenida |
| Feature | Object Lock ON | ILM + versioning | |
| Capacidad | f(retention) | 178 TB @ 500×30d×1Mbps | |
| LiveKit + WHIP | CPU | 6 cores / 50 ingestas | 11 cores / 100 ingestas |
| RAM | 8 GB / 50 ingestas | 14 GB / 100 ingestas | |
| GPU | 0 % (H.264 passthrough) | 0 %; NVENC si H.265 transcode | |
| PC operador | RAM sistema | 8 GB | 16 GB |
| GPU | Decode H.264 HW | Dedicada o iGPU reciente | |
| Red | 20 Mbps | 50 Mbps |
15.1 Premisas del escenario
| Parámetro | Valor de referencia |
|---|---|
| Cámaras grabando | 500 simultáneas |
| Modo grabación | pull (MediaMTX directo; 0 ffmpeg en vstation) |
| Perfil vídeo grabación | H.264, 1000–1500 kbps, 720p–1080p |
| Segmentos | 60 s (óptimo) o 30 s |
| Retención | 30 días (ajustar según retention_days del grupo) |
| Vivo | WHIP H.264 passthrough (VSTATION_INGEST_MODE=whip) |
| Layout operadores | Varied — cada operador ve cámaras diferentes |
| Ingesta vivo | On-demand — solo cámaras abiertas en pantalla |
Nota de doble pull: una cámara que se graba y además se ve en vivo recibe dos consumidores RTSP (MediaMTX + GStreamer WHIP). Dimensionar la red de cámaras/NVR con ese solapamiento.
15.2 Topología de servidores recomendada (500 + vivo)
Separar grabación y vivo en nodos distintos. vstation-app puede convivir con LiveKit en el nodo de operación.
┌──────────────── NODO GRABACIÓN ─────────────────┐
│ MediaMTX (500 paths) + MinIO vstation-lock │
│ 500–788 Mbps escritura sostenida │
└──────────────────────────────────────────────────┘
┌──────────────── NODO VIVO / APP ─────────────────┐
│ vstation-app + PostgreSQL + Redis + Nginx │
│ LiveKit SFU + WHIP ingress │
│ 50–200 ingestas WHIP según operadores activos │
└──────────────────────────────────────────────────┘
┌──────────────── PCs OPERADOR ────────────────────┐
│ Chrome/Edge, decode H.264 HW, 8–16 GB RAM │
└──────────────────────────────────────────────────┘
15.3 Grabación — 500 cámaras (MediaMTX pull directo)
Comando de simulación:
npx tsx scripts/mediamtx-recording-sim.ts --cameras 500 --topology direct \
--bitrate-kbps 1000 --segment-sec 60s --retention-days 30
| Métrica | Necesario (mínimo) | Óptimo (recomendado) |
|---|---|---|
| Servidor grabación RAM | 9 GB | 12 GB (+25 % headroom) |
| Servidor grabación CPU | 3.4 cores | 5 cores |
| GPU grabación | 0 % | 0 % |
| Red cámaras → MediaMTX | 500 Mbps | ≥ 1 Gbps dedicado |
| Escritura sostenida MinIO | 500 Mbps (~63 MB/s) | NVMe/RAID ≥ 750 MB/s |
| Segmentos/hora (60 s) | 30 000 | 30 000 |
| Almacenamiento/día @ 1000 kbps | 5.1 TB | 5.1 TB |
| Almacenamiento 30 días | 154 TB | 178 TB (+margen) |
| vstation-app (orquestación) | 0.3 GB RAM, 0.1 core | Mismo nodo app (sin carga ffmpeg) |
Variante bitrate alto (1500 kbps, segmentos 30 s):
| Métrica | Necesario | Óptimo |
|---|---|---|
| Red ingress | 750 Mbps | ≥ 2 Gbps |
| Escritura MinIO | 788 Mbps (~94 MB/s) | NVMe obligatorio |
| TB / 30 días | 232 TB | 267 TB |
| Segmentos/hora | 60 000 | Preferir 60 s → 30 000/h |
15.4 Visualización simultánea — cámaras distintas por operador
Comando (sin relay grabación — grabación va por nodo MTX aparte):
npx tsx scripts/stream-load-sim.ts --users 10 --streams 10 --layout varied \
--profile h264_passthrough --no-recording
| Escenario operador | Cámaras únicas vivo | RAM servidor (necesario) | CPU servidor (necesario) | RAM óptima (+25 %) | CPU óptima |
|---|---|---|---|---|---|
| 5 × 10 tiles | 50 | 6 GB | 4.3 cores | 8 GB | 6 cores |
| 10 × 10 tiles (ref.) | 100 | 10.6 GB | 8.4 cores | 14 GB | 11 cores |
| 20 × 10 tiles (pico) | 200 | 26.6 GB | 20.7 cores | 34 GB | 27 cores |
Desglose capas — escenario referencia 10 operadores × 10 cámaras (100 ingestas):
| Capa | RAM | CPU | GPU |
|---|---|---|---|
| Infra vstation (app + PG + Redis + Nginx) | 0.8 GB | 0.1 core | 0 % |
| Ingesta WHIP (100 cámaras) | 8.8 GB | 8.0 cores | 0 % |
| LiveKit SFU (100 suscripciones) | 1.0 GB | 0.3 core | 0 % |
| Total servidor vivo | ~10.6 GB | ~8.4 cores | 0 % |
Ancho de banda vivo (100 cámaras @ 1500 kbps):
| Dirección | Mbps |
|---|---|
| Ingress cámaras → vstation/LiveKit | ~150 |
| Egress SFU → operadores | ~150 |
Comparativa mosaico compartido (10 operadores, mismas 16 cámaras):
npx tsx scripts/stream-load-sim.ts --users 10 --streams 10 --layout shared \
--unique-cameras 16 --profile h264_passthrough --no-recording
| Métrica | Valor |
|---|---|
| Ingestas WHIP | 16 (no 100) |
| RAM servidor óptima | 4 GB |
| CPU servidor óptima | 3 cores |
→ Si todos ven el mismo mosaico, el dimensionamiento de vivo baja ~7× frente a varied.
15.5 Escenario combinado — 500 grabando + 10 operadores varied
Configuración objetivo para despliegue tipo Miraflores reducido / piloto regional:
| Nodo | Función | RAM necesaria | RAM óptima | CPU necesaria | CPU óptima | Red |
|---|---|---|---|---|---|---|
| Grabación | MTX + MinIO | 9 GB | 12 GB | 3.4 cores | 5 cores | 1 Gbps+ |
| Vivo + App | vstation + LiveKit | 11 GB | 14 GB | 8.4 cores | 11 cores | 1 Gbps |
| Total infra | — | 20 GB | 26 GB | 12 cores | 16 cores | 2 Gbps agregado |
| Almacenamiento | MinIO 30 d | 154 TB | 178 TB | — | — | — |
Red agregada estimada (con solapamiento ~100 cámaras vivo+grabación):
| Tramo | Mbps estimados |
|---|---|
| Cámaras → MediaMTX (500 grabando) | ~500 |
| Cámaras → vstation WHIP (100 vivo extra) | ~150 |
| SFU → operadores | ~150 |
| Total pico aproximado | ~800 Mbps |
15.6 Requerimientos por PC operador
| Recurso | Necesario | Óptimo |
|---|---|---|
| RAM navegador | 5 GB (10 tiles) | 8 GB sistema |
| GPU | Decode H.264 HW | Dedicada o iGPU reciente |
| CPU cliente | ~5 % (10 tiles) | 4+ cores |
| Navegador | Chrome / Edge | Última versión estable |
| Red estación | 20 Mbps | 50 Mbps |
Evitar Firefox si hay cámaras H.265 passthrough.
15.7 Perfiles a evitar a esta escala
| Anti-patrón | Impacto en 500 cams |
|---|---|
VSTATION_RECORDING_MODE=relay | ~17 GB RAM solo ffmpeg + 25+ cores en vstation |
| H.265 transcode en vivo | ~45 % CPU/cámara → 100 cams ≈ 45 cores solo ingesta |
| Segmentos 30 s @ 1500 kbps | 60 000 archivos/h → IOPS MinIO alto |
VSTATION_FASTSTART_WORKER=true | Picos CPU por re-mux MP4 |
| LiveKit + MTX + MinIO en 1 solo servidor | Contención RAM/red/CPU |
15.8 Configuración .env objetivo (500 + pull)
# Grabación
VSTATION_RECORDING_MODE=pull
MEDIAMTX_RTSP_URL=
MEDIAMTX_API_URL=http://<mtx-grabacion>:9997
MEDIAMTX_MINIO_ENDPOINT=http://<minio>:9000
# Vivo
VSTATION_INGEST_MODE=whip
VSTATION_H265_PASSTHROUGH=false
LIVEKIT_URL=wss://...
LIVEKIT_API_URL=https://...
# Escala
VSTATION_FASTSTART_WORKER=false
MediaMTX — segmento recomendado:
PATCH /api/mediamtx/config
{ "recordSegmentDuration": "60s" }
15.9 Escenario Miraflores — 800 cámaras grabando 45 días
Comando de simulación (perfil acordado Miraflores: H.264 @ 1000 kbps, segmentos 60 s):
npx tsx scripts/mediamtx-recording-sim.ts --cameras 800 --topology direct \
--bitrate-kbps 1000 --segment-sec 60s --retention-days 45
| Métrica | Necesario (mínimo) | Óptimo (recomendado) |
|---|---|---|
| Servidor grabación RAM | 12.6 GB | 16 GB |
| Servidor grabación CPU | 5.4 cores | 7 cores |
| GPU grabación | 0 % | 0 % |
| Red cámaras → MediaMTX | 800 Mbps | ≥ 2 Gbps dedicado |
| Escritura sostenida MinIO | 800 Mbps (~100 MB/s) | NVMe ≥ 1 GB/s |
| Segmentos/hora (60 s) | 48 000 | 48 000 |
| Almacenamiento/día | 8.2 TB | 8.2 TB |
| Almacenamiento 45 días | 371 TB | 427 TB (+margen) |
Combinado con 10 operadores × 10 cámaras distintas (100 ingestas WHIP):
| Nodo | RAM óptima | CPU óptima | Red |
|---|---|---|---|
| Grabación (MTX + MinIO) | 16 GB | 7 cores | 2 Gbps |
| Vivo + App | 14 GB | 11 cores | 1 Gbps |
| Total infra | 30 GB | 18 cores | ~2.5 Gbps agregado pico |
| Almacenamiento | 427 TB | — | — |
Doble pull: si las 100 cámaras en vivo también se graban, no suman paths extra en MTX (misma cámara), pero la red NVR recibe 2 consumidores RTSP por cámara solapada.
15.10 Matriz consolidada por escala (simulaciones sept 2026)
Grabación continua — MediaMTX pull directo, H.264 @ 1000 kbps, segmentos 60 s:
| Cámaras | Retención | RAM srv | CPU srv | Red ingress | TB almacenamiento | Segmentos/h |
|---|---|---|---|---|---|---|
| 200 | 30 d | 7 GB | 2 cores | 0.5 Gbps | 72 TB | 12 000 |
| 500 | 30 d | 12 GB | 5 cores | 1.3 Gbps | 178 TB | 30 000 |
| 800 | 30 d | 16 GB | 7 cores | 2 Gbps | 285 TB | 48 000 |
| 800 | 45 d | 16 GB | 7 cores | 2 Gbps | 427 TB | 48 000 |
| 1300 | 30 d | 24 GB | 12 cores | 3.2 Gbps | 462 TB | 78 000 |
Vivo on-demand — H.264 passthrough, layout varied (--no-recording):
| Operadores × tiles | Ingestas únicas | RAM óptima | CPU óptima | Ingress+egress |
|---|---|---|---|---|
| 5 × 10 | 50 | 8 GB | 6 cores | ~75 Mbps |
| 10 × 10 | 100 | 14 GB | 11 cores | ~150 Mbps |
| 20 × 10 | 200 | 25 GB | 22 cores | ~300 Mbps |
Vivo H.265 transcode (anti-patrón a escala):
| Operadores × tiles | RAM óptima | CPU óptima | GPU |
|---|---|---|---|
| 10 × 10 (100 cams) | 30 GB | 60 cores | NVENC recomendada |
15.11 Validación en staging
# Tests unitarios (39) — validar requisitos software antes de hardware
npm test
# Grabación por escala
npm run recording:sim
npx tsx scripts/mediamtx-recording-sim.ts --cameras 500 --topology direct --bitrate-kbps 1000 --segment-sec 60s
npx tsx scripts/mediamtx-recording-sim.ts --cameras 800 --topology direct --bitrate-kbps 1000 --segment-sec 60s --retention-days 45
# Vivo 10 operadores × 10 cámaras distintas
npm run stream:sim
npx tsx scripts/stream-load-sim.ts --users 10 --streams 10 --layout varied --no-recording
# Smoke grabación pull real
npm run recording:smoke
Medir en producción: cadvisor :8080, MediaMTX /v3/paths/list, iostat en nodos MinIO.
16. Anexos
A. Variables de entorno completas
Ver .env.example en el repositorio. Resumen por subsistema:
| Grupo | Variables principales |
|---|---|
| App/DB | DATABASE_URL, SESSION_SECRET, JWT_SECRET, REDIS_URL |
| Auth Gateway | AUTH_GATEWAY_URL, AUTH_GATEWAY_CLIENT_ID, KEYCLOAK_*, APP_ACCESS_ROLE |
| Integración | INTERNAL_SERVICE_KEY, VSTATION_PUBLIC_URL |
| OTEL | OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT |
| LiveKit | LIVEKIT_URL, LIVEKIT_API_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET |
| Ingesta | VSTATION_INGEST_MODE, VSTATION_H265_PASSTHROUGH, VSTATION_STUN_SERVER |
| Grabación | VSTATION_RECORDING_MODE, MEDIAMTX_API_URL, MEDIAMTX_*, MEDIAMTX_MINIO_* |
| Playback | VSTATION_PLAYBACK_SIGNED_URLS, VSTATION_PLAYBACK_SIGNED_URL_TTL_SECONDS |
| Retención test | VSTATION_RETENTION_TEST_MINUTES (solo dev) |
| Optrax | OPTRAX_API_URL, OPTRAX_API_TOKEN |
| Mapas | VITE_MAP_PROVIDER, VITE_MARTIN_URL, VITE_MARTIN_SOURCE |
B. API endpoints principales
| Método | Ruta | Función |
|---|---|---|
| GET | /api/health | Health check |
| GET | /api/cameras | Inventario |
| GET | /api/cameras/:id/recordings/window | Fragmentos playback |
| GET | /api/cameras/:id/recordings/object-playback | Stream segmento MinIO |
| PATCH | /api/cameras/:id/recordings/object-legal-hold | Legal hold segmento pull |
| PATCH | /api/recordings/:id/legal-hold | Legal hold fila BD |
| GET | /api/mediamtx/config | Config global MTX |
| PATCH | /api/mediamtx/config | Ajuste segment duration |
| POST | /api/optrax/sync | Sync cámaras Core |
C. Servicios backend (grabación)
| Archivo | Responsabilidad |
|---|---|
recordingMode.ts | Detecta pull/relay/off |
mediamtxPullRecording.ts | Alta/baja paths MTX |
mediamtxProxy.ts | Cliente API MediaMTX |
cameraRecording.ts | Fachada start/stop grabación |
livekitRecording.ts | Workers política + retención BD |
minioRetentionLifecycle.ts | Sync ILM MinIO |
recordingRetention.ts | Lógica pura retención/purge |
minioTestRetentionPurge.ts | Purge test (dev) |
D. Documentos relacionados
| Documento | Contenido |
|---|---|
docs/capacity-recommendations.md | Dimensionamiento extendido (200–1500 cámaras) |
docs/plan-implementacion-vstation-v2.md | Este documento |
docs/runbook-incidentes.md | Runbook MTX / MinIO / LiveKit |
README.md | Setup desarrollo |
.env.example | Referencia completa de configuración |
E. Scripts de simulación y tests
| Script / comando | Escenario | Salida HW |
|---|---|---|
npm test | 39 unit tests | §15.0 — paquetes, red, MinIO, codec |
npm run recording:sim | 500 cams pull | §15.3, §15.10 |
npm run stream:sim | 10×10 varied | §15.4, §15.10 |
npm run recording:smoke | E2E pull real | Validación entorno |
mediamtx-recording-sim.ts --cameras 800 --retention-days 45 | Miraflores | §15.9 |
Documento generado a partir del estado del repositorio vstation (rama main, septiembre 2026). Validar siempre en staging antes de despliegues masivos.