←

Snapshots JPEG via streaming channel en Hikvision

Contexto

Necesitaba capturar imagenes JPEG de camaras conectadas a un NVR Hikvision bajo demanda (por ejemplo, adjuntar un snapshot a una alerta de movimiento via Telegram). El NVR expone esto via ISAPI pero la respuesta es binaria, no XML.

Lo que aprendi

El endpoint /ISAPI/Streaming/channels/{channel_id}/picture retorna JPEG crudo:

async def capture(self, channel_id: int) -> bytes:
    """Captura un snapshot de una camara. Retorna JPEG bytes."""
    path = f"/ISAPI/Streaming/channels/{channel_id}/picture"
    data = await self.client.get_raw(path)
    return data

Los channel IDs siguen un patron: X01 es el stream principal de la camara X, X02 es el sub-stream. Para snapshot, siempre usar el main stream (101, 201, 301...).

Para guardar snapshots organizados por fecha con cleanup automatico:

from pathlib import Path
from datetime import datetime, timezone

async def capture_and_save(self, channel_id: int) -> str:
    """Captura y guarda snapshot. Retorna path del archivo."""
    data = await self.capture(channel_id)
    now = datetime.now(timezone.utc)

    # Organizar por fecha: media/snapshots/2026-04-03/101_143000.jpg
    date_dir = self.base_path / now.strftime("%Y-%m-%d")
    date_dir.mkdir(parents=True, exist_ok=True)

    filename = f"{channel_id}_{now.strftime('%H%M%S')}.jpg"
    file_path = date_dir / filename
    file_path.write_bytes(data)
    return str(file_path)

def cleanup_old(self, days: int = 30):
    """Elimina directorios de snapshots mas viejos que N dias."""
    cutoff = datetime.now(timezone.utc) - timedelta(days=days)
    for date_dir in self.base_path.iterdir():
        if date_dir.is_dir():
            dir_date = datetime.strptime(date_dir.name, "%Y-%m-%d")
            if dir_date < cutoff:
                shutil.rmtree(date_dir)

Por que importa

Un snapshot bajo demanda es la base para alertas inteligentes: deteccion de movimiento + snapshot + envio a Telegram/WhatsApp. Sin esto, solo recibes "movimiento detectado" sin saber que lo causo.