EPG para middleware OTT

O servidor fornece os dados do guia de programação (EPG) para o dia selecionado e para um único canal no formato JSON. O endpoint é destinado aos back-ends de middleware OTT que agregam a programação do Perfect Streamer para montar o guia no cliente final.

URL e autenticação

O endpoint é servido pelo servidor admin integrado. Por padrão está disponível nas portas 43971 (HTTP) e 43981 (HTTPS); as portas são configuradas em Settings/Server Settings.

GET /data/epg/channel?src=<src_id>&ch=<channel_id>&lang=<lang>&t=<time>

A autenticação é HTTP Digest, como para o restante de /data/*. Para o middleware, basta uma conta com o papel viewer (somente leitura).

Nota

Solicitações do endereço loopback (127.0.0.1) são executadas sem verificação HTTP Digest — o servidor as trata como anônimas. É conveniente para scripts locais e health-checks de middleware implantado na mesma máquina que o Perfect Streamer; para acessos remotos as credenciais são obrigatórias.

Parâmetros da solicitação

Parâmetro

Tipo

Obrigatório

Padrão

Descrição

src

inteiro sem sinal

não

0

Fonte EPG. 0 — dados importados do EIT MPEG-TS dos fluxos de entrada. 1, 2, … — identificador de entrada em /config/epg/epg-source (fonte XMLTV externa).

ch

cadeia

sim

—

Identificador do canal na base EPG: valor do campo channel_id da tabela channel. A lista de identificadores disponíveis pode ser obtida por uma consulta SQL via POST /data/epg/sql.

lang

inteiro ou ISO 639

não

idioma padrão do sistema

0 — idioma padrão do sistema; um inteiro > 0 — identificador interno de idioma (veja GET /schema/lang); uma cadeia — código ISO 639 de duas ou três letras, p. ex. eng, rus, fra.

t

inteiro sem sinal (Unix epoch, s)

não

hora atual do servidor

Qualquer ponto dentro do dia de interesse. O servidor retorna eventos do dia UTC ao qual t pertence: intervalo [t / 86400 · 86400, (t / 86400 + 1) · 86400). Para obter dados do «dia seguinte», basta somar 86400 ao horário atual.

Nota

O dia é tomado em UTC, não no fuso local. Se o middleware monta a programação pelo dia calendário local, o limite do dia UTC pode não coincidir com a meia-noite local; nesse caso é preciso fazer duas solicitações (para dias UTC adjacentes) e juntar os resultados pelo campo start.

Formato da resposta

  • Content-Type: application/json.

  • Os cabeçalhos HTTP proíbem o cache no lado do cliente e em proxies intermediários:

    Cache-Control: no-cache, no-store, must-revalidate
    Pragma: no-cache
    Expires: 0
    
  • O corpo da resposta é um JSON com a lista de eventos do dia:

{
  "event": [
    {"start": 1715000000, "end": 1715003400, "title": "Morning News", "desc": "Overview of the day's events"},
    {"start": 1715003400, "end": 1715007000, "title": "Weather Forecast", "desc": ""}
  ]
}

Campos do evento:

  • start, end — início e fim do programa em Unix epoch (s), UTC.

  • title — título do programa no idioma escolhido. Se o título no idioma solicitado não existir para o evento, o servidor recorre à entrada no idioma padrão do sistema e, depois, em qualquer outro disponível.

  • desc — descrição estendida. Pode ser uma cadeia vazia se a base não tiver descrição separada para o evento.

Particularidades da saída:

  • Eventos com título vazio e sem descrição, assim como aqueles com timestamps inválidos, são excluídos da saída.

  • Os duplicados por start são descartados: para o mesmo momento de início é retornada uma única entrada com a melhor prioridade de idioma (solicitado → padrão do sistema → demais).

  • A ordem dos eventos no array não é garantida — se necessário, o middleware ordena a lista pelo campo start por conta própria.

  • Se o canal não for encontrado, se a fonte não existir ou se não houver eventos para o dia escolhido, o servidor retorna 200 OK com o array vazio {"event":[]} ou, no caso raro de fonte ausente, com corpo vazio. O middleware deve tratar corretamente as duas variantes.

Cache no servidor

O JSON pronto é armazenado em cache pelo servidor sob a chave (channel_id, data UTC, idioma), de modo que solicitações repetidas para o mesmo dia e canal são atendidas sem acessar o banco de dados. O middleware não precisa gerenciar o cache.

O cache é limpo automaticamente:

  • quando chegam novos eventos do EIT MPEG-TS dos fluxos de entrada (src=0);

  • ao atualizar com sucesso uma fonte XMLTV externa (src > 0 — agendada ou forçada via POST /data/epg/update?s=<src_id>);

  • quando um dia sai da janela de retenção do EPG.

Um endpoint dedicado para a limpeza forçada do cache não está previsto nem é necessário.

Códigos de resposta HTTP

Código

Condição

200 OK

Solicitação processada. O corpo é um JSON com a lista de eventos (possivelmente vazia).

400 Bad Request

O parâmetro ch está ausente ou vazio; ou src, t não são analisáveis como inteiro sem sinal; ou um lang numérico aponta para um identificador de idioma inexistente.

401 Unauthorized

A autenticação HTTP Digest está ausente ou incorreta (para solicitações que não vêm de um endereço loopback).

Outras situações (src desconhecido, canal ausente, sem eventos no dia) não geram 4xx — o middleware recebe 200 OK com o array vazio {"event":[]}.

Em caso de erro, o corpo da resposta é um envelope JSON de formato fixo:

{"status": 400, "message": "Bad Request"}

O campo status duplica o código HTTP; o campo message traz a causa refinada do erro em inglês (ou o texto padrão do status HTTP, se não houver informação adicional). O Content-Type da resposta de erro é application/json.

Exemplo

curl -u middleware:secret --digest \
  'http://pss.example.com:43971/data/epg/channel?src=0&ch=12.0.1&lang=rus&t=1715000000'

Exemplo de resposta:

{
  "event": [
    {"start": 1715000000, "end": 1715003400, "title": "Morning News", "desc": "Overview of the day's events"},
    {"start": 1715003400, "end": 1715007000, "title": "Weather Forecast", "desc": ""}
  ]
}

Desempenho e escalabilidade

Cache do servidor Perfect Streamer

Dentro do processo PSS há um cache LRU de respostas em memória com chave (channel_id, data UTC, idioma) e teto rígido de 1024 entradas por fonte EPG. Sob carga típica (dezenas a centenas de canais × 1–3 idiomas × keep-day dias) todas as entradas atuais cabem inteiras no cache; solicitações repetidas são atendidas sem acessar o SQLite.

Ordem de grandeza (build de depuração, loopback local, sem HTTPS):

Cenário

Latência (solicitação única)

Vazão (P=8)

Cache hit

~0,3 ms

~1100 solicitações/s

Cache miss (SQL + JSON)

~1,0–1,5 ms

~1000 solicitações/s

Em um build de release e sem logging de depuração, os números ficam cerca de 2–3× melhores. Bandwidth — cerca de 14 KB por resposta para um canal-dia típico.

Quando é preciso um reverse-proxy externo (nginx)

O cache do servidor acelera solicitações repetidas para o mesmo (canal, dia, idioma), mas cada solicitação ainda passa pelo servidor HTTP integrado do PSS e consome um thread do seu pool. Com muitos clientes faz sentido mover o cache para o edge:

  • até ~1.000 clientes online — o cache interno costuma ser suficiente, reverse-proxy não é obrigatório.

  • dezenas de milhares ou mais — recomenda-se um reverse-proxy com cache (por exemplo, nginx). Um cache edge atende 99 % das solicitações sem o PSS, amortiza picos (start do middleware, refresh em massa do player) e permite mover a terminação SSL para um nó separado.

  • entrega geograficamente distribuída — um CDN/proxy externo é necessário antes mesmo de contar os clientes.

O PSS envia o próprio cabeçalho Cache-Control: no-cache, no-store, must-revalidate para que os clientes finais não armazenem o EPG por muito tempo. Já o reverse-proxy pode (e deve) cachear a resposta sozinho — abaixo mostra-se como informar explicitamente ao nginx que ignore o Cache-Control do upstream e mantenha o próprio TTL.

Exemplo de configuração nginx

Configuração mínima para um cache edge de EPG dimensionada para dezenas de milhares de clientes com intervalo de polling de 1–5 minutos:

# /etc/nginx/conf.d/pss-epg.conf

proxy_cache_path /var/cache/nginx/pss-epg
                 levels=1:2
                 keys_zone=pss_epg:32m
                 max_size=2g
                 inactive=30m
                 use_temp_path=off;

upstream pss_admin {
    server 127.0.0.1:43971;
    keepalive 64;
}

server {
    listen 80;
    # listen 443 ssl http2;  # recommended: SSL termination here
    server_name epg.example.com;

    # gzip helps: typical EPG JSON compresses ~5–8x.
    gzip on;
    gzip_types application/json;
    gzip_min_length 512;
    gzip_proxied any;

    location = /data/epg/channel {
        proxy_pass http://pss_admin;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # PSS returns no-cache; force-cache it at the edge.
        proxy_ignore_headers Cache-Control Expires Set-Cookie;
        proxy_hide_header   Cache-Control;
        proxy_hide_header   Pragma;
        proxy_hide_header   Expires;

        # Cache key = full URL with query string. The src/ch/lang/t
        # parameters already determine response uniqueness.
        proxy_cache_key     "$scheme$host$request_uri";

        proxy_cache         pss_epg;
        proxy_cache_valid   200 60s;          # staleness TTL
        proxy_cache_valid   400 404 10s;
        proxy_cache_lock           on;        # coalesce cache misses
        proxy_cache_lock_timeout   5s;
        proxy_cache_use_stale      error timeout updating
                                   http_500 http_502 http_503;

        # Serve the JSON to the client with its own TTL
        # (the player won't recheck EPG before that).
        add_header Cache-Control "public, max-age=60";
        add_header X-Cache-Status $upstream_cache_status always;
    }
}

Explicações e recomendações

  • TTL ``proxy_cache_valid 200 60s`` — compromisso entre frescura do EPG e carga no upstream. A programação não muda em tempo real, então 30–300 segundos são razoáveis. Após importar novos eventos, o PSS limpa o próprio cache imediatamente, e o cache edge alcança no próximo TTL.

  • ``proxy_cache_lock on`` é obrigatório com muitos clientes: em um cache miss, ele coalesce solicitações paralelas para a mesma chave em uma única solicitação upstream, protegendo o SQLite de picos de BUSY sob carga.

  • ``keys_zone`` e ``max_size`` são dimensionados pelo número de (canal × dia × idioma): 32 MB de keys_zone cobrem centenas de milhares de chaves; 2 GB de max_size cobrem um mês de histórico para centenas de canais com folga.

  • gzip reduz significativamente o tráfego: as respostas comprimem bem (chaves JSON repetidas, cirílico em UTF-8).

  • ``X-Cache-Status`` na resposta permite que o middleware veja HIT/MISS/EXPIRED e avalie a eficácia do cache.

  • Se nginx e PSS coabitam na mesma máquina, o servidor admin não exige HTTP Digest para loopback, portanto o bloco upstream pode ficar sem proxy_set_header Authorization …. Para distribuir por redes diferentes, crie uma conta viewer dedicada e adicione autenticação Digest em proxy_pass.

  • HTTPS é melhor terminado no nginx: o PSS suporta HTTPS diretamente, mas um servidor edge costuma ser mais eficiente para lidar com handshakes TLS de milhares de clientes simultâneos.