EPG pour middleware OTT

Le serveur fournit les données du guide des programmes (EPG) pour la journée sélectionnée pour une chaîne, au format JSON. L’endpoint est destiné aux back-ends de middleware OTT qui agrègent la grille depuis Perfect Streamer pour construire le programme côté client final.

URL et authentification

L’endpoint est servi par le serveur admin intégré. Par défaut, il est disponible sur les ports 43971 (HTTP) et 43981 (HTTPS) ; les ports se configurent dans Settings/Server Settings.

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

L’authentification est HTTP Digest, comme pour le reste de /data/*. Pour le middleware, un compte avec le rôle viewer (lecture seule) suffit.

Note

Les requêtes depuis l’adresse loopback (127.0.0.1) sont exécutées sans contrôle HTTP Digest — le serveur les considère comme anonymes. Pratique pour les scripts locaux et les health-checks d’un middleware déployé sur la même machine que Perfect Streamer ; pour les accès distants, des identifiants sont obligatoires.

Paramètres de la requête

Paramètre

Type

Obligatoire

Par défaut

Description

src

entier non signé

non

0

Source EPG. 0 — données importées depuis l’EIT MPEG-TS des flux d’entrée. 1, 2, … — identifiant d’entrée dans /config/epg/epg-source (source XMLTV externe).

ch

chaîne

oui

—

Identifiant de la chaîne dans la base EPG : valeur du champ channel_id de la table channel. La liste des identifiants disponibles peut être obtenue via une requête SQL par POST /data/epg/sql.

lang

entier ou ISO 639

non

langue système par défaut

0 — langue système par défaut ; un entier > 0 — identifiant interne de langue (voir GET /schema/lang) ; une chaîne — code ISO 639 à deux ou trois lettres, par exemple eng, rus, fra.

t

entier non signé (Unix epoch, s)

non

heure actuelle du serveur

Tout point à l’intérieur de la journée concernée. Le serveur renvoie les événements de la journée UTC à laquelle appartient t : intervalle [t / 86400 · 86400, (t / 86400 + 1) · 86400). Pour obtenir les données du « lendemain », il suffit d’ajouter 86400 à l’heure courante.

Note

La journée est prise en UTC, pas dans le fuseau local. Si le middleware construit la grille selon le jour calendaire local, la frontière de la journée UTC peut ne pas coïncider avec minuit local ; il faut alors lancer deux requêtes (pour deux jours UTC adjacents) et recoller les résultats par le champ start.

Format de la réponse

  • Content-Type: application/json.

  • Les en-têtes HTTP interdisent la mise en cache côté client et sur les proxys intermédiaires :

    Cache-Control: no-cache, no-store, must-revalidate
    Pragma: no-cache
    Expires: 0
    
  • Le corps de la réponse est un JSON avec la liste des événements du jour :

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

Champs de l’événement :

  • start, end — début et fin de l’émission en Unix epoch (s), UTC.

  • title — titre de l’émission dans la langue choisie. Si le titre n’existe pas dans la langue demandée pour l’événement, le serveur prend l’entrée dans la langue système par défaut, puis dans toute autre langue disponible.

  • desc — description étendue. Peut être une chaîne vide si la base ne contenait pas de description séparée pour l’événement.

Particularités de la sortie :

  • Les événements avec un titre vide et sans description, ainsi que ceux avec des horodatages incorrects, sont exclus de la sortie.

  • Les doublons par start sont éliminés : pour un même instant de début, une seule entrée est renvoyée avec la meilleure priorité de langue (demandée → défaut système → autres).

  • L’ordre des événements dans le tableau n’est pas garanti — si nécessaire, le middleware trie lui-même la liste par le champ start.

  • Si la chaîne est introuvable, si la source n’existe pas ou s’il n’y a aucun événement pour la journée choisie, le serveur renvoie 200 OK avec le tableau vide {"event":[]} ou, dans le rare cas d’une source absente, avec un corps vide. Le middleware doit gérer correctement les deux variantes.

Mise en cache sur le serveur

Le JSON prêt est mis en cache par le serveur avec la clé (channel_id, date UTC, langue) ; les requêtes répétées pour les mêmes jour et chaîne sont donc servies sans accéder à la base de données. Le middleware n’a rien à faire pour gérer le cache.

Le cache est purgé automatiquement :

  • à l’arrivée de nouveaux événements depuis l’EIT MPEG-TS des flux d’entrée (src=0) ;

  • lors d’une actualisation réussie d’une source XMLTV externe (src > 0 — planifiée ou forcée via POST /data/epg/update?s=<src_id>) ;

  • lorsqu’un jour sort de la fenêtre de conservation EPG.

Un endpoint dédié pour la purge forcée du cache n’est ni prévu ni nécessaire.

Codes de réponse HTTP

Code

Condition

200 OK

Requête traitée. Le corps est un JSON avec la liste des événements (éventuellement vide).

400 Bad Request

Le paramètre ch est absent ou vide ; ou src, t ne s’analysent pas comme entier non signé ; ou un lang numérique pointe sur un identifiant de langue inexistant.

401 Unauthorized

L’authentification HTTP Digest est absente ou invalide (pour les requêtes ne provenant pas d’une adresse loopback).

Les autres situations (src inconnu, chaîne absente, pas d’événements ce jour-là) ne produisent pas de 4xx — le middleware reçoit 200 OK avec le tableau vide {"event":[]}.

En cas d’erreur, le corps de la réponse est une enveloppe JSON de format fixe :

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

Le champ status reprend le code HTTP ; le champ message contient une cause d’erreur précisée en anglais (ou le texte standard du statut HTTP s’il n’y a pas d’information supplémentaire). Le Content-Type de la réponse d’erreur est application/json.

Exemple

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

Exemple de réponse :

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

Performance et mise à l’échelle

Cache serveur Perfect Streamer

Au sein du processus PSS, il existe un cache LRU en mémoire des réponses, indexé par (channel_id, date UTC, langue), avec un plafond strict de 1024 entrées par source EPG. Avec une charge typique (dizaines à centaines de chaînes × 1–3 langues × keep-day jours), toutes les entrées actuelles tiennent intégralement en cache ; les requêtes répétées sont servies sans accéder à SQLite.

Ordre de grandeur (build de débogage, loopback local, sans HTTPS) :

Scénario

Latence (requête unique)

Débit (P=8)

Cache hit

~0,3 ms

~1100 requêtes/s

Cache miss (SQL + JSON)

~1,0–1,5 ms

~1000 requêtes/s

En build release et sans journalisation de debug, les chiffres sont environ 2 à 3 fois meilleurs. Bande passante — environ 14 Ko par réponse pour une chaîne typique sur la journée.

Quand un reverse-proxy externe (nginx) est nécessaire

Le cache serveur accélère les requêtes répétées pour le même (chaîne, jour, langue), mais chaque requête passe tout de même par le serveur HTTP intégré de PSS et consomme un thread de son pool. Avec beaucoup de clients, il est judicieux de déplacer la mise en cache au edge :

  • jusqu’à ~1 000 clients en ligne — le cache interne suffit généralement, un reverse-proxy n’est pas obligatoire.

  • dizaines de milliers et plus — un reverse-proxy avec cache (par exemple nginx) est recommandé. Un cache edge traite 99 % des requêtes sans PSS, amortit les pics (démarrage de middleware, rafraîchissement massif des lecteurs) et permet de placer la terminaison SSL sur un nœud séparé.

  • distribution géographiquement répartie — un CDN/proxy externe est nécessaire avant même de compter les clients.

PSS envoie son propre en-tête Cache-Control: no-cache, no-store, must-revalidate pour que les clients finaux ne mettent pas l’EPG en cache durablement. Le reverse-proxy peut (et doit) mettre la réponse en cache lui-même — on montre plus bas comment indiquer explicitement à nginx d’ignorer le Cache-Control upstream et de tenir son propre TTL.

Exemple de configuration nginx

Une configuration minimale pour un cache edge EPG dimensionnée pour des dizaines de milliers de clients avec un intervalle d’interrogation de 1 à 5 minutes :

# /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;
    }
}

Explications et recommandations

  • TTL ``proxy_cache_valid 200 60s`` — compromis entre fraîcheur de l’EPG et charge sur l’upstream. La grille ne change pas en temps réel, 30 à 300 s sont donc raisonnables. Après l’import de nouveaux événements, PSS purge son propre cache instantanément, et le cache edge rattrape au prochain TTL.

  • ``proxy_cache_lock on`` est obligatoire pour un grand nombre de clients : lors d’un cache miss, il coalesce les requêtes parallèles sur la même clé en une seule requête upstream, protégeant SQLite des pics de BUSY sous charge.

  • ``keys_zone`` et ``max_size`` sont dimensionnés selon le nombre (chaîne × jour × langue) : 32 Mo de keys_zone couvrent des centaines de milliers de clés ; 2 Go de max_size couvrent un mois d’historique pour des centaines de chaînes, avec marge.

  • gzip réduit nettement le trafic : les réponses se compressent bien (clés JSON répétées, cyrillique en UTF-8).

  • ``X-Cache-Status`` dans la réponse permet au middleware de voir HIT/MISS/EXPIRED et d’évaluer l’efficacité du cache.

  • Si nginx et PSS résident sur la même machine, le serveur admin n’exige pas HTTP Digest pour le loopback ; le bloc upstream peut donc rester sans proxy_set_header Authorization …. Pour un déploiement à travers les réseaux, créez un compte viewer dédié et ajoutez l’authentification Digest dans proxy_pass.

  • HTTPS se termine de préférence sur nginx : PSS prend HTTPS directement en charge, mais un serveur edge est généralement plus efficace pour gérer les handshakes TLS avec des milliers de clients simultanés.