Files
KiraTV/DOCS.md
T
joaquin a49f67bbff docs: complete technical documentation (architecture, DB schema, ops)
Full reference covering:
- Architecture and design decisions
- PostgreSQL schema (all tables)
- Stream engine internals (ProviderPool, BroadcastGroup, ffmpeg flags)
- Xtream Codes API endpoints
- Admin dashboard features and WebSocket protocol
- EPG and Jellyfin integration
- Nginx + systemd configs
- Environment variables and Python dependencies
- Step-by-step reinstall guide
- Common operations (update, logs, Redis/Postgres inspection)
- Lessons learned (ffmpeg flags to avoid, health thresholds, zapping logic)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 01:18:28 +02:00

32 KiB

KiraStream — Documentación Técnica Completa

Proxy IPTV multiusuario con fan-out de streams, API Xtream Codes, EPG, dashboard en tiempo real e integración Jellyfin.


Índice

  1. Arquitectura general
  2. Stack tecnológico
  3. Esquema de base de datos
  4. Motor de streams
  5. API Xtream Codes
  6. Panel de administración
  7. EPG
  8. Integración Jellyfin
  9. Infraestructura y despliegue
  10. Variables de entorno
  11. Reinstalación desde cero
  12. Operaciones habituales
  13. Decisiones de diseño y lecciones aprendidas

1. Arquitectura general

[Apps IPTV — TiviMate, Smarters, etc.]
            │  Xtream Codes API
            ▼
┌───────────────────────────────────────────┐
│           Nginx (puerto 80)               │
│   /panel/* → static build React          │
│   /*       → proxy FastAPI :8000          │
└───────────────────┬───────────────────────┘
                    │
┌───────────────────▼───────────────────────┐
│         KiraStream (FastAPI :8000)        │
│                                           │
│  ┌─────────────────┐  ┌────────────────┐  │
│  │   API Xtream    │  │   API Admin    │  │
│  │ /player_api.php │  │ /api/admin/*   │  │
│  │ /{u}/{p}/{id}   │  │ WebSocket /ws  │  │
│  └────────┬────────┘  └───────┬────────┘  │
│           │                   │            │
│  ┌────────▼───────────────────▼────────┐  │
│  │           Stream Engine             │  │
│  │  ProviderPool   BroadcastGroup      │  │
│  │  (slots Redis)  (fan-out asyncio)   │  │
│  │                 ffmpeg copy-mode    │  │
│  └─────────────────────────────────────┘  │
│                                           │
│  ┌──────────┐  ┌──────────────────────┐  │
│  │  Redis   │  │     PostgreSQL 17     │  │
│  │ (estado) │  │  (config + catálogos) │  │
│  └──────────┘  └──────────────────────┘  │
└───────────────────────────────────────────┘
            │  Xtream Codes (consume)
            ▼
[Proveedores IPTV — cuenta1 … cuentaN]

Principios clave

  • Fan-out: N usuarios viendo el mismo canal → 1 solo slot del proveedor. El motor descarga una vez y distribuye a todos los clientes.
  • Sin transcodificación: los streams se reenvían bit a bit (copia remux con ffmpeg -c copy).
  • Multi-cuenta: varias cuentas del mismo proveedor para multiplicar conexiones simultáneas.
  • Catálogos por usuario: cada usuario ve solo las categorías que tiene asignadas.
  • Estado efímero en Redis, configuración en PostgreSQL.

2. Stack tecnológico

Capa Tecnología Versión
Backend Python + FastAPI + uvicorn Python 3.13, FastAPI 0.115
ORM SQLAlchemy async + Alembic 2.0
Base de datos PostgreSQL 17
Cache / estado Redis 8
Proceso de stream ffmpeg sistema
EPG XMLTV fetch + merge por tvg-id —
Frontend React 18 + Vite + TailwindCSS React 18, Vite 6
Proxy inverso Nginx sistema
Servicio systemd —

3. Esquema de base de datos

Tablas principales

-- Cuentas del proveedor IPTV (una por "lista")
provider_accounts (
  id SERIAL PK,
  name TEXT,
  base_url TEXT,              -- p.ej. http://proveedor.com:8080
  username TEXT,
  password TEXT,
  max_connections INT DEFAULT 1,
  is_active BOOL DEFAULT true,
  last_sync_at TIMESTAMPTZ,
  auto_sync_hours INT DEFAULT 0,  -- 0 = desactivado
  last_sync_categories JSONB      -- {live:[1,2], movie:[3], series:[]}
)

-- URLs alternativas por cuenta (failover multi-dominio)
provider_urls (
  id SERIAL PK,
  provider_account_id INT FK → provider_accounts(id) CASCADE,
  url TEXT,                   -- URL base alternativa
  priority INT DEFAULT 0,     -- menor = mayor prioridad
  is_active BOOL DEFAULT true,
  status TEXT DEFAULT 'unknown',  -- ok / error / timeout / unknown
  response_ms INT,
  last_checked_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ
)

-- Categorías (importadas desde el proveedor)
categories (
  id SERIAL PK,
  name TEXT,
  type TEXT,                  -- live / movie / series
  provider_category_id TEXT,
  provider_account_id INT FK → provider_accounts(id)
)

-- Canales deduplicados
channels (
  id SERIAL PK,
  name TEXT,
  tvg_id TEXT,
  tvg_logo TEXT,
  stream_id_at_provider TEXT,
  category_id INT FK → categories(id),
  type TEXT,                  -- live / movie / series
  is_active BOOL DEFAULT true
)

-- Mapa canal ↔ cuenta del proveedor (un canal puede estar en varias cuentas)
channel_provider_map (
  id SERIAL PK,
  channel_id INT FK → channels(id),
  provider_account_id INT FK → provider_accounts(id),
  stream_url TEXT
)

-- Usuarios finales (Xtream Codes compatible)
users (
  id SERIAL PK,
  username TEXT UNIQUE,
  password_hash TEXT,
  max_connections INT DEFAULT 1,
  expiry_date TIMESTAMPTZ,
  is_active BOOL DEFAULT true,
  is_priority BOOL DEFAULT false,  -- puede desalojar streams no-priority
  preferred_provider_ids JSONB,    -- [1,3] — proveedores preferidos
  created_at TIMESTAMPTZ
)

-- Entradas de catálogo por usuario
user_catalog_entries (
  id SERIAL PK,
  user_id INT FK → users(id),
  category_id INT FK → categories(id),
  type TEXT
)

-- Categorías personalizadas (no vienen del proveedor)
custom_categories (
  id SERIAL PK,
  name TEXT,
  type TEXT,
  display_order INT DEFAULT 0
)

custom_category_channels (
  id SERIAL PK,
  custom_category_id INT FK,
  channel_id INT FK,
  display_order INT DEFAULT 0
)

-- Fuentes EPG
epg_sources (
  id SERIAL PK,
  name TEXT,
  url TEXT,
  last_fetched_at TIMESTAMPTZ,
  is_active BOOL DEFAULT true
)

-- Configuración global
app_settings (
  key TEXT PK,
  value TEXT
)

-- Configuración Jellyfin
jellyfin_configs (
  id SERIAL PK,
  name TEXT,
  base_url TEXT,
  api_key TEXT,
  category_name TEXT,
  provider_account_id INT FK,
  is_active BOOL DEFAULT true,
  last_sync_at TIMESTAMPTZ
)

-- Log de actividad
stream_logs (
  id SERIAL PK,
  user_id INT,
  username TEXT,
  channel_id INT,
  channel_name TEXT,
  provider_account_id INT,
  started_at TIMESTAMPTZ,
  ended_at TIMESTAMPTZ,
  bytes_transferred BIGINT
)

4. Motor de streams

El motor tiene dos capas: ProviderPool (gestión de slots) y BroadcastGroup (fan-out y proxy).

4.1 ProviderPool (backend/core/pool.py)

Singleton que gestiona todos los BroadcastGroup activos y la asignación de slots de proveedor.

Estado en Redis:

ks:stream:{channel_id}   → hash {provider_account_id, stream_url, clients, started_at}
ks:slot:{account_id}     → channel_id (clave = slot ocupado)
ks:user_conns:{user_id}  → set de client_ids activos
ks:monitoring            → pub/sub channel para WebSocket

Flujo acquire(channel_id, user_id, ...):

  1. Comprueba límite de conexiones del usuario (Redis + in-memory)
  2. Si el canal ya está activo → attach al BroadcastGroup existente
  3. Si no → busca slot libre entre las cuentas del proveedor (ordenadas por preferencia del usuario)
  4. Si no hay slot libre y el usuario es is_priority → evicta el stream con menos clientes sin usuarios priority
  5. Reserva slot en Redis, crea BroadcastGroup, arranca stream
  6. Devuelve ClientHandle con una asyncio.Queue personal del cliente

Flujo release(channel_id, client_id):

  1. Elimina el cliente del grupo
  2. Si quedan 0 clientes → para el grupo, libera slot Redis
  3. Si quedan clientes → actualiza contador

Zapping detection: si un usuario ya tiene una conexión de menos de 10s, la desaloja silenciosamente en lugar de rechazar la nueva. Esto permite cambiar de canal rápido desde una TV sin ver el "stream bloqueado".

Multi-dominio failover (_build_stream_urls):
Para cada slot asignado, consulta provider_urls ordenadas por prioridad y construye una lista de URLs alternativas sustituyendo el dominio base manteniendo el path del stream. El BroadcastGroup recibe todas y hace failover automático.

4.2 BroadcastGroup (backend/core/restream.py)

Descarga un stream del proveedor y lo distribuye a N clientes mediante colas asyncio.

Arquitectura:

ffmpeg (proceso externo)
  stdout → _pump_once → _dispatch → Queue[cliente1]
                                  → Queue[cliente2]
                                  → Queue[clienteN]
  stderr → _log_ffmpeg_stderr (logging + contador av_desync)

ClientHandle.read() → itera la Queue y yield chunks al StreamingResponse HTTP

Comando ffmpeg (copy-mode):

ffmpeg -hide_banner -loglevel warning \
  -reconnect 1 \
  -reconnect_streamed 1 \
  -reconnect_delay_max 2 \
  -timeout 10000000 \
  -i {stream_url} \
  -c copy \
  -f mpegts pipe:1
  • -reconnect_streamed 1: al expirar el token 302 del proveedor, ffmpeg reenvía el GET original y obtiene un token nuevo. No se desconectan los clientes.
  • -timeout 10000000: stall detection de 10 segundos a nivel socket.
  • -c copy: remux sin transcodificación. Calidad bit a bit idéntica al proveedor.

Por qué ffmpeg en lugar de aiohttp directo:
El proveedor usa redirecciones 302 con tokens de sesión de corta duración (típicamente 60-120s para HD, ~28s para algunos 4K). Con aiohttp puro, al expirar el token hay que reconectar a nivel Python, lo que producía desync A/V visible porque el nuevo stream reinicia sus PTS/DTS. ffmpeg con -reconnect_streamed renegocia el token internamente y normaliza los PTS/DTS en el remux, haciendo la transición imperceptible para el cliente.

Flags que NO usar:

  • -use_wallclock_as_timestamps 1: reemplaza cada PTS/DTS con el reloj del servidor. Genera una advertencia de stderr por cada paquete con jitter de red, resultando en cientos de warnings por minuto que atascan ffmpeg y producen freezes visibles. No usar.
  • -fflags +discardcorrupt: descarta paquetes corruptos en el punto de reconexión. El problema es que ese paquete puede ser el IDR keyframe que HEVC necesita para arrancar. Sin él, el decoder espera el siguiente IDR (hasta 2s) y el player salta al live edge → salto hacia atrás visible. No usar.
  • -avoid_negative_ts make_zero: útil para VoD pero en live con reconexiones produce acumulación de offsets incorrectos que desincroniza A/V. No usar para live.

Failover multi-URL:
_pump_with_retry itera las URLs del proveedor en orden. Si una falla, prueba la siguiente inmediatamente (sin espera). Solo hace backoff (máx 5s) cuando ha fallado el ciclo completo de todas las URLs.

Monitor de salud (_health_check_loop):
Tarea asyncio que corre cada 5s y comprueba la antigüedad del último dato recibido:

  • < 12s → av_health = "ok"
  • 12-25s → av_health = "warning"
  • > 25s → av_health = "error" + mata ffmpeg vía SIGTERM para forzar reconexión inmediata (cuenta como "corrección")

Buffer (_buffer_pct):
Porcentaje del queue del cliente más lento. 0% = cliente consume en tiempo real (ideal). 100% = cliente bloqueado o muy lento (riesgo de drops).

Eviction de clientes lentos:
Si un cliente lleva más de 30s con la queue llena (sin consumir), es desalojado automáticamente.

4.3 HealthChecker (backend/core/health_checker.py)

Tarea de fondo que cada 5 minutos comprueba todos los dominios activos de cada proveedor haciendo una petición a player_api.php?action=user_info. Actualiza status, response_ms y last_checked_at en la tabla provider_urls.

4.4 VodTracker (backend/core/vod_tracker.py)

Gestiona sesiones VOD/series (sin fan-out, cada cliente es independiente). Hace proxy directo del stream del proveedor sin pasar por ProviderPool, ya que VOD no tiene límite de slots compartidos.


5. API Xtream Codes

Ruta base: / — compatible con TiviMate, IPTV Smarters, IPTV Pro y cualquier app que soporte Xtream Codes.

Autenticación de usuario

Todos los endpoints de la API pública llevan ?username=X&password=Y o /{username}/{password}/.

Endpoints

Endpoint Función
GET /player_api.php?action=user_info Info del usuario + límites de conexión
GET /player_api.php?action=get_live_categories Categorías live del catálogo del usuario
GET /player_api.php?action=get_live_streams Canales live del usuario
GET /player_api.php?action=get_vod_categories Categorías VOD
GET /player_api.php?action=get_vod_streams Películas
GET /player_api.php?action=get_vod_info&vod_id=X Detalle película
GET /player_api.php?action=get_series_categories Categorías series
GET /player_api.php?action=get_series Series
GET /player_api.php?action=get_series_info&series_id=X Detalle serie + episodios
GET /get.php?type=m3u_plus Playlist M3U filtrada por catálogo del usuario
GET /xmltv.php EPG XMLTV filtrado por canales del usuario
GET /live/{u}/{p}/{stream_id} Stream live (proxy con fan-out)
GET /live/{u}/{p}/{stream_id}.ts Ídem con extensión
GET /movie/{u}/{p}/{stream_id}.{ext} Stream VOD
GET /series/{u}/{p}/{stream_id}.{ext} Episodio de serie

Lógica de stream live

GET /live/usuario/password/840238.ts
  → autenticar usuario
  → resolver canal (stream_id → channel_id)
  → pool.acquire(channel_id, user_id, ...)
  → StreamingResponse(handle.read())  ← HTTP chunked al cliente
  → [on disconnect] pool.release(channel_id, client_id, user_id)

6. Panel de administración

URL: http://{IP}/panel/
Credenciales por defecto: admin / definido en config.py

Páginas

Dashboard (/)

  • 6 StatCards: streams activos, usuarios conectados, slots ocupados, bajada total del proveedor, CPU total ffmpeg, salud A/V global
  • Tabla de streams: canal, proveedor, codec/resolución detectada, estado de conexión, salud A/V (ok/aviso/error), barra de buffer, usuarios con botón de corte, bitrate ↓/↑, tiempo activo, datos transferidos
  • Estado de slots: qué proveedor tiene qué canal
  • WebSocket: actualización en tiempo real (cada 2s + eventos Redis)

Proveedores (/providers)

  • CRUD de cuentas del proveedor (nombre, URL base, usuario, contraseña, max_connections)
  • Sync manual: importa categorías y canales desde la API del proveedor
  • Auto-sync configurable (cada N horas)
  • Multi-dominio: añadir URLs alternativas por proveedor con indicador de salud (ok/error/timeout/unknown + ms)
  • Botón "Verificar todos" para forzar health check inmediato

Usuarios (/users)

  • CRUD de usuarios Xtream Codes
  • Campos: usuario, contraseña, max_connections, expiry_date, is_priority, proveedores preferidos
  • Ver conexiones activas

Catálogos (/catalogs)

  • Asignar categorías del proveedor a cada usuario
  • Filtro por tipo (live/movie/series)

Categorías personalizadas (/custom-categories)

  • Crear categorías propias con canales seleccionados manualmente
  • Control de orden de visualización

EPG (/epg)

  • Añadir fuentes XMLTV (URL)
  • Estado de última importación

Jellyfin (/jellyfin)

  • Configurar instancias Jellyfin
  • Sync de biblioteca local como categorías KiraStream

Logs (/logs)

  • Historial de streams: usuario, canal, proveedor, duración, bytes

Configuración (/settings)

  • Variables de configuración globales editables desde el panel

WebSocket de monitoring

WS /api/admin/monitoring/ws
→ Server emite en dos situaciones:
  1. Cada 2s (broadcast general)
  2. En cada cambio de estado (vía Redis pub/sub)

Payload:
{
  "type": "state" | "update",
  "data": [
    {
      "stream_type": "live",
      "channel_id": 310412,
      "channel_name": "ES: LA 1 UHD",
      "provider_name": "Lista1",
      "client_count": 2,
      "clients": [{"client_id": "...", "username": "joaquin"}],
      "started_at": 1779000000.0,
      "bytes_pumped": 125000000,
      "bps_down": 3200000,
      "bps_up": 6400000,
      "status": "ok",
      "reconnect_count": 3,
      "last_data_ago": 0.3,
      "stream_info": {"video_codec": "hevc", "width": 3840, "height": 2160, ...},
      "ffmpeg_pid": 3251,
      "ffmpeg_cpu_pct": 0.38,
      "av_health": "ok",
      "av_desync_count": 12,
      "corrections": 0,
      "buffer_pct": 0.0,
      "active_url_domain": "proveedor.com",
      "url_count": 3
    }
  ]
}

7. EPG

  • Fuentes: URLs XMLTV configuradas en el panel
  • Merge: por tvg-id. Si varios canales tienen el mismo tvg-id, se unifica la EPG
  • Refresco: tarea de fondo cada N horas (configurable, por defecto 12h)
  • Endpoint cliente: GET /xmltv.php?username=X&password=Y — devuelve EPG filtrada por los canales del catálogo del usuario

8. Integración Jellyfin

  • Configura una o varias instancias Jellyfin con su URL base y API key
  • Al hacer sync, importa la biblioteca local de Jellyfin como una categoría adicional en KiraStream
  • Los streams de Jellyfin se sirven como VOD a través de KiraStream (proxy transparente)
  • Los usuarios ven el contenido de Jellyfin mezclado con el IPTV dentro de la misma app cliente

9. Infraestructura y despliegue

Entorno

  • Contenedor: Proxmox LXC, PCT 117, hostname kiraIPTV, IP 10.10.10.234
  • OS: Debian 13 Trixie
  • Recursos: 4 cores, 8 GB RAM, 50 GB ZFS local

Estructura en disco

/opt/kirastream/
├── backend/
│   ├── __init__.py
│   ├── main.py                  # Punto de entrada FastAPI
│   ├── config.py                # Settings (pydantic-settings)
│   ├── database.py              # Engine PostgreSQL + init_db()
│   ├── api/
│   │   ├── auth.py              # JWT admin
│   │   ├── xtream.py            # API Xtream Codes pública
│   │   └── admin/
│   │       ├── providers.py     # CRUD proveedores + sync + multi-URL
│   │       ├── users.py         # CRUD usuarios
│   │       ├── catalogs.py      # Catálogos por usuario
│   │       ├── custom_categories.py
│   │       ├── epg_admin.py     # Fuentes EPG
│   │       ├── monitoring.py    # REST + WebSocket monitoring
│   │       ├── logs.py          # Historial de streams
│   │       ├── settings.py      # Config global
│   │       └── jellyfin_admin.py
│   ├── core/
│   │   ├── pool.py              # ProviderPool (slots + Redis)
│   │   ├── restream.py          # BroadcastGroup (fan-out + ffmpeg)
│   │   ├── xtream_client.py     # Cliente HTTP para API del proveedor
│   │   ├── epg_manager.py       # Fetch + merge XMLTV
│   │   ├── catalog.py           # Resolver catálogo por usuario
│   │   ├── health_checker.py    # Health check de dominios del proveedor
│   │   ├── vod_tracker.py       # Sesiones VOD/series
│   │   ├── probe.py             # ffprobe para detectar codec/resolución
│   │   └── jellyfin_client.py
│   ├── models/
│   │   ├── provider.py          # ProviderAccount, ProviderUrl
│   │   ├── channel.py           # Channel, Category, ChannelProviderMap
│   │   ├── user.py              # User, UserCatalogEntry
│   │   ├── epg.py               # EpgSource
│   │   ├── custom_category.py
│   │   ├── log.py               # StreamLog
│   │   ├── notification.py
│   │   └── jellyfin.py
│   └── static/                  # Frontend compilado (generado por npm run build)
│       ├── index.html
│       └── assets/
│           ├── index-*.js
│           └── index-*.css
├── frontend/
│   ├── src/
│   │   ├── App.tsx
│   │   ├── main.tsx
│   │   ├── lib/api.ts           # Axios con base /api/admin
│   │   ├── components/
│   │   │   └── Layout.tsx
│   │   └── pages/
│   │       ├── Dashboard.tsx    # Monitoring tiempo real
│   │       ├── Providers.tsx    # Multi-URL, health badges
│   │       ├── Users.tsx
│   │       ├── Catalogs.tsx
│   │       ├── CustomCategories.tsx
│   │       ├── EPG.tsx
│   │       ├── Jellyfin.tsx
│   │       ├── Logs.tsx
│   │       ├── Settings.tsx
│   │       └── Login.tsx
│   ├── package.json
│   ├── vite.config.ts
│   └── tailwind.config.js
├── nginx/
│   └── kirastream.conf
├── logs/
│   └── backend.log
├── venv/                        # Virtualenv Python
└── README.md

Nginx (/etc/nginx/sites-enabled/kirastream)

upstream kirastream_backend {
    server 127.0.0.1:8000;
    keepalive 32;
}

server {
    listen 80 default_server;
    server_name _;

    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
    proxy_connect_timeout 10s;

    # Panel de administración
    location /panel {
        alias /opt/kirastream/backend/static;
        try_files $uri $uri/ /panel/index.html;
    }

    # API y Xtream Codes
    location / {
        proxy_pass http://kirastream_backend;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_buffering off;
        proxy_cache off;
        add_header X-Accel-Buffering no;
    }

    # WebSocket monitoring (timeout extra largo)
    location /api/admin/monitoring/ws {
        proxy_pass http://kirastream_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 86400s;
    }

    # Assets del frontend (cache inmutable)
    location /panel/assets/ {
        alias /opt/kirastream/backend/static/assets/;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    gzip on;
    gzip_types application/json application/javascript text/css text/plain;
    access_log /var/log/nginx/kirastream_access.log;
    error_log /var/log/nginx/kirastream_error.log;
}

Systemd (/etc/systemd/system/kirastream.service)

[Unit]
Description=KiraStream IPTV Server
After=network.target postgresql.service

[Service]
Type=simple
User=root
WorkingDirectory=/opt/kirastream

ExecStartPre=/bin/bash -c 'pg_ctlcluster 17 main start 2>/dev/null; true'
ExecStartPre=/bin/bash -c 'redis-cli ping 2>/dev/null || (redis-server --port 6379 --bind 127.0.0.1 --daemonize yes --dir /var/lib/redis/ && sleep 1); true'
ExecStartPre=/bin/bash -c 'pkill -9 uvicorn 2>/dev/null; sleep 1; true'

ExecStart=/opt/kirastream/venv/bin/uvicorn backend.main:app \
    --host 0.0.0.0 --port 8000 --workers 1 --log-level info

StandardOutput=append:/opt/kirastream/logs/backend.log
StandardError=append:/opt/kirastream/logs/backend.log

Restart=always
RestartSec=5
KillMode=control-group

[Install]
WantedBy=multi-user.target

10. Variables de entorno

Fichero: /opt/kirastream/.env (si no existe, se usan los defaults de config.py)

# Base de datos
DATABASE_URL=postgresql+asyncpg://kirastream:PASSWORD@localhost/kirastream

# Redis
REDIS_URL=redis://localhost:6379/0

# JWT — generar con: openssl rand -hex 32
SECRET_KEY=CAMBIAR_ESTO

# Panel admin
ADMIN_USERNAME=admin
ADMIN_PASSWORD=CAMBIAR_ESTO

# Stream (opcionales)
STREAM_CHUNK_SIZE=65536       # bytes por chunk (64 KB)
STREAM_QUEUE_MAXSIZE=512      # chunks en queue por cliente
STREAM_CONNECT_TIMEOUT=10     # segundos
STREAM_READ_TIMEOUT=30        # segundos

# EPG
EPG_REFRESH_HOURS=12

# Logging
LOG_LEVEL=INFO                # DEBUG / INFO / WARNING / ERROR

Dependencias Python (pip freeze actual)

aiofiles==24.1.0
aiohttp==3.12.4
alembic==1.16.1
asyncpg==0.30.0
bcrypt==4.3.0
fastapi==0.115.12
hiredis==3.3.1
httpx==0.28.1
lxml==5.4.0
passlib==1.7.4
pydantic==2.13.4
pydantic-settings==2.9.1
PyJWT==2.9.0
python-jose==3.3.0
python-multipart==0.0.20
redis==5.3.0
SQLAlchemy==2.0.41
structlog==25.3.0
tenacity==9.1.2
uvicorn==0.34.3
uvloop==0.22.1
websockets==15.0.1

11. Reinstalación desde cero

Paso 1 — Preparar el sistema

# En el host Proxmox, acceder al contenedor
pct exec 117 -- bash

# Instalar dependencias del sistema
apt update && apt install -y \
  postgresql-17 \
  redis-server \
  nginx \
  ffmpeg \
  python3.13 python3.13-venv \
  nodejs npm \
  git curl

# Verificar ffmpeg
ffmpeg -version | head -1

Paso 2 — Clonar repositorio

git clone https://git.kiracloud.es/joaquin/KiraTV.git /opt/kirastream
cd /opt/kirastream

Paso 3 — Base de datos PostgreSQL

# Crear usuario y base de datos
sudo -u postgres psql <<EOF
CREATE USER kirastream WITH PASSWORD 'ELEGIR_PASSWORD';
CREATE DATABASE kirastream OWNER kirastream;
GRANT ALL PRIVILEGES ON DATABASE kirastream TO kirastream;
EOF

Paso 4 — Entorno Python

cd /opt/kirastream
python3.13 -m venv venv
venv/bin/pip install --upgrade pip

# Instalar dependencias (usar el requirements.txt del repo o la lista del apartado 10)
venv/bin/pip install \
  fastapi uvicorn[standard] uvloop \
  sqlalchemy[asyncio] asyncpg alembic \
  pydantic-settings \
  redis hiredis \
  aiohttp aiofiles httpx \
  passlib[bcrypt] python-jose[cryptography] python-multipart PyJWT \
  lxml structlog tenacity

Paso 5 — Variables de entorno

cat > /opt/kirastream/.env <<EOF
DATABASE_URL=postgresql+asyncpg://kirastream:ELEGIR_PASSWORD@localhost/kirastream
REDIS_URL=redis://localhost:6379/0
SECRET_KEY=$(openssl rand -hex 32)
ADMIN_USERNAME=admin
ADMIN_PASSWORD=ELEGIR_PASSWORD
EOF

Paso 6 — Inicializar base de datos

cd /opt/kirastream
# La función init_db() en database.py crea todas las tablas automáticamente
# al primer arranque. También puede ejecutarse manualmente:
venv/bin/python -c "
import asyncio
from backend.database import init_db
asyncio.run(init_db())
"

Paso 7 — Compilar frontend

cd /opt/kirastream/frontend
npm install
npm run build
# Los archivos compilados van a /opt/kirastream/backend/static/

Paso 8 — Nginx

# Copiar configuración
cp /opt/kirastream/nginx/kirastream.conf /etc/nginx/sites-available/kirastream
ln -sf /etc/nginx/sites-available/kirastream /etc/nginx/sites-enabled/kirastream
rm -f /etc/nginx/sites-enabled/default

nginx -t && systemctl reload nginx

Paso 9 — Systemd

cp /opt/kirastream/kirastream.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable kirastream
systemctl start kirastream

# Verificar
systemctl status kirastream
tail -f /opt/kirastream/logs/backend.log

Paso 10 — Verificación

# API health
curl http://localhost:8000/health

# Panel admin
# Abrir en navegador: http://10.10.10.234/panel/

# Logs en vivo
tail -f /opt/kirastream/logs/backend.log

12. Operaciones habituales

Actualizar desde git

# En el host Proxmox
cd /tmp/kirastream
git pull origin main

# Push de archivos modificados al contenedor
pct push 117 backend/core/restream.py /opt/kirastream/backend/core/restream.py
pct push 117 backend/core/pool.py /opt/kirastream/backend/core/pool.py
# etc.

# Si hay cambios en el frontend
pct exec 117 -- bash -c "cd /opt/kirastream/frontend && npm run build"

# Reiniciar backend
pct exec 117 -- systemctl restart kirastream

Ver logs

# Logs en vivo
pct exec 117 -- tail -f /opt/kirastream/logs/backend.log

# Últimas 100 líneas
pct exec 117 -- tail -100 /opt/kirastream/logs/backend.log

# Solo errores
pct exec 117 -- grep ERROR /opt/kirastream/logs/backend.log | tail -20

# Logs de ffmpeg de un canal concreto
pct exec 117 -- grep "ch310412" /opt/kirastream/logs/backend.log | tail -30

Redis — inspección de estado

pct exec 117 -- redis-cli

# Streams activos
KEYS ks:stream:*
HGETALL ks:stream:310412

# Slots ocupados
KEYS ks:slot:*

# Conexiones de un usuario
SMEMBERS ks:user_conns:2

PostgreSQL — consultas útiles

pct exec 117 -- sudo -u postgres psql kirastream

-- Usuarios activos
SELECT username, max_connections, expiry_date FROM users WHERE is_active;

-- Canales por categoría
SELECT c.name, count(*) FROM channels ch
JOIN categories c ON ch.category_id = c.id GROUP BY c.name ORDER BY count DESC;

-- Salud de dominios
SELECT pa.name, pu.url, pu.status, pu.response_ms
FROM provider_urls pu JOIN provider_accounts pa ON pa.id = pu.provider_account_id
ORDER BY pa.name, pu.priority;

Reinicio limpio

# Para todos los streams activos y reinicia
pct exec 117 -- systemctl restart kirastream

# Si Redis tiene estado corrupto
pct exec 117 -- redis-cli FLUSHDB
pct exec 117 -- systemctl restart kirastream

13. Decisiones de diseño y lecciones aprendidas

Por qué una sola instancia uvicorn (workers=1)

El ProviderPool y los BroadcastGroup son objetos en memoria con asyncio.Lock. Si hubiera múltiples workers, cada proceso tendría su propio pool y se perdería la coordinación de fan-out (dos workers podrían abrir dos conexiones al proveedor para el mismo canal). El estado de coordinación está en Redis para soportar múltiples procesos en el futuro, pero la implementación actual usa un solo worker con asyncio concurrente, que es suficiente para cientos de streams simultáneos dado que la operación es mayoritariamente I/O-bound.

Por qué ffmpeg en lugar de aiohttp directo

Los proveedores Xtream usan redirecciones 302 con tokens de sesión de corta duración (60-120s para HD, ~28s para algunos streams 4K). Con aiohttp puro, al expirar el token hay que reconectar a nivel Python interrumpiendo el stream. ffmpeg con -reconnect_streamed 1 renegocia el token internamente sin interrumpir el pipe de stdout, haciendo la transición invisible para el cliente.

Tokens 302 y freezes en 4K

Algunos canales 4K tienen tokens que expiran cada ~28 segundos. ffmpeg los renegocia en <1 segundo típicamente. El player IPTV debe tener suficiente buffer para cubrir esa brecha (TiviMate por defecto tiene 3-8s). Si el freeze persiste, revisar la velocidad de red entre KiraStream y el proveedor.

Flags ffmpeg que NO funcionan para live IPTV

Flag Problema
-use_wallclock_as_timestamps 1 Genera una advertencia de stderr por cada paquete con jitter de red (cientos por minuto), atascando ffmpeg y produciendo freezes
-fflags +discardcorrupt Descarta el IDR keyframe del punto de reconexión; el decoder HEVC espera hasta el siguiente IDR (hasta 2s) → salto visible al live edge
-avoid_negative_ts make_zero Acumula offsets incorrectos en reconexiones con EAC3, dejando A/V desincronizado ~350ms

Umbrales del monitor de salud

Los proveedores renuevan tokens y la reconexión de ffmpeg tarda 1-4 segundos. Con umbrales demasiado bajos (warning >5s) el dashboard marca "aviso" en cada reconexión normal. Los umbrales correctos son:

  • warning: sin datos >12s
  • error + auto-corrección: sin datos >25s

Zapping vs multi-dispositivo

La lógica de _check_user_connections distingue entre cambiar de canal rápido ("zapping": conexión existente <10s de antigüedad → evict silencioso) y uso multi-dispositivo genuino (conexión >10s → rechaza con error para mostrar "stream bloqueado" en el nuevo dispositivo).

Fan-out y límites de memoria

Con STREAM_QUEUE_MAXSIZE=512 y STREAM_CHUNK_SIZE=65536 bytes, cada cliente tiene un buffer de servidor de hasta 32 MB. Para 25 Mbps (4K) eso es ~10 segundos de buffer máximo. En operación normal el buffer está casi vacío (el cliente consume al ritmo del proveedor). Si buffer_pct supera 70% de forma sostenida, el cliente tiene problemas de red o el dispositivo es lento.