EPG für OTT-Middleware

Der Server liefert Programmdaten (EPG) für den gewählten Tag und einen einzigen Kanal im JSON-Format. Der Endpunkt ist für OTT-Middleware-Backends gedacht, die den Programmplan aus Perfect Streamer aggregieren, um das Programm beim Endkunden aufzubauen.

URL und Authentifizierung

Der Endpunkt wird vom integrierten Admin-Server bereitgestellt. Standardmäßig ist er unter den Ports 43971 (HTTP) und 43981 (HTTPS) erreichbar; die Ports werden unter Settings/Server Settings konfiguriert.

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

Die Authentifizierung erfolgt per HTTP Digest, wie bei den übrigen /data/*. Für Middleware reicht ein Konto mit der Rolle viewer (nur lesen).

Bemerkung

Anfragen von der Loopback-Adresse (127.0.0.1) umgehen die HTTP-Digest-Prüfung — der Server behandelt sie als anonym. Das ist praktisch für lokale Skripte und Health-Checks von Middleware, die auf derselben Maschine wie Perfect Streamer läuft; für entfernte Zugriffe sind Zugangsdaten zwingend.

Anfrageparameter

Parameter

Typ

Pflicht

Standard

Beschreibung

src

vorzeichenlose Ganzzahl

nein

0

EPG-Quelle. 0 — aus MPEG-TS-EIT der Eingangsströme importierte Daten. 1, 2, … — Eintrags-ID aus /config/epg/epg-source (externe XMLTV-Quelle).

ch

Zeichenkette

ja

—

Kanal-Kennung aus der EPG-Datenbank: Wert des Feldes channel_id in der Tabelle channel. Die Liste der verfügbaren Kennungen lässt sich per SQL-Abfrage über POST /data/epg/sql ermitteln.

lang

Ganzzahl oder ISO 639

nein

Systemstandardsprache

0 — Systemstandardsprache; eine Ganzzahl > 0 — interne Sprach-ID (siehe GET /schema/lang); eine Zeichenkette — zwei- oder dreibuchstabiger ISO-639-Code, z. B. eng, rus, fra.

t

vorzeichenlose Ganzzahl (Unix-Epoche, Sek.)

nein

aktuelle Serverzeit

Beliebiger Zeitpunkt innerhalb des interessierenden Tages. Der Server liefert Ereignisse für den UTC-Tag, zu dem t gehört: Intervall [t / 86400 · 86400, (t / 86400 + 1) · 86400). Für Daten des «nächsten Tages» reicht es, 86400 zur aktuellen Zeit zu addieren.

Bemerkung

Der Tag wird in UTC gerechnet, nicht in der lokalen Zeitzone. Baut die Middleware den Zeitplan nach dem lokalen Kalendertag auf, fällt die UTC-Tagesgrenze nicht zwingend auf die lokale Mitternacht; in diesem Fall sind zwei Anfragen (für zwei benachbarte UTC-Tage) nötig, deren Ergebnisse über das Feld start zusammengeführt werden.

Antwortformat

  • Content-Type: application/json.

  • Die HTTP-Header verbieten das Caching auf der Client-Seite und auf zwischengeschalteten Proxys:

    Cache-Control: no-cache, no-store, must-revalidate
    Pragma: no-cache
    Expires: 0
    
  • Der Antwortkörper ist ein JSON mit der Ereignisliste für den Tag:

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

Ereignisfelder:

  • start, end — Anfang und Ende der Sendung in Unix-Epoche (Sek.), UTC.

  • title — Sendungstitel in der gewählten Sprache. Fehlt ein Titel in der angeforderten Sprache, greift der Server auf den Eintrag in der Systemstandardsprache zurück und danach auf jede andere verfügbare Sprache.

  • desc — erweiterte Beschreibung. Kann ein leerer String sein, wenn die Datenbank keine separate Beschreibung für das Ereignis enthielt.

Besonderheiten der Ausgabe:

  • Ereignisse mit leerem Titel und ohne Beschreibung sowie Ereignisse mit ungültigen Zeitstempeln werden aus der Ausgabe ausgeschlossen.

  • Duplikate nach start werden ausgesondert: Für denselben Startzeitpunkt wird ein Datensatz mit der besten Sprachpriorität zurückgegeben (angefordert → System-Default → andere).

  • Die Reihenfolge der Ereignisse im Array ist nicht garantiert — bei Bedarf sortiert die Middleware die Liste selbst nach dem Feld start.

  • Wenn der Kanal nicht gefunden wird, die Quelle nicht existiert oder am gewählten Tag keine Ereignisse vorliegen, gibt der Server 200 OK mit dem leeren Array {"event":[]} zurück — oder, im seltenen Fall einer fehlenden Quelle, mit leerem Körper. Die Middleware muss beide Varianten korrekt behandeln.

Caching auf dem Server

Das fertige JSON wird vom Server unter dem Schlüssel (channel_id, UTC-Datum, Sprache) gecacht; wiederholte Anfragen für denselben Tag und Kanal werden daher ohne Datenbankzugriff bedient. Die Middleware muss am Cache nichts steuern.

Der Cache wird automatisch zurückgesetzt:

  • beim Eintreffen neuer Ereignisse aus MPEG-TS-EIT der Eingangsströme (src=0);

  • bei einer erfolgreichen Aktualisierung einer externen XMLTV-Quelle (src > 0 — geplant oder erzwungen über POST /data/epg/update?s=<src_id>);

  • wenn ein Tag aus dem EPG-Aufbewahrungsfenster fällt.

Ein separater Endpunkt zum erzwungenen Leeren des Caches ist weder vorgesehen noch erforderlich.

HTTP-Antwortcodes

Code

Bedingung

200 OK

Anfrage verarbeitet. Der Körper ist ein JSON mit der Ereignisliste (eventuell leer).

400 Bad Request

Der Parameter ch fehlt oder ist leer; oder src, t lassen sich nicht als vorzeichenlose Ganzzahl parsen; oder ein numerischer lang verweist auf eine nicht existierende Sprach-ID.

401 Unauthorized

Die HTTP-Digest-Authentifizierung fehlt oder ist ungültig (für Anfragen nicht von einer Loopback-Adresse).

Sonstige Situationen (unbekanntes src, fehlender Kanal, keine Ereignisse am Tag) führen nicht zu 4xx — die Middleware erhält 200 OK mit dem leeren Array {"event":[]}.

Bei einem Fehler ist der Antwortkörper ein JSON-Umschlag mit festem Format:

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

Das Feld status dupliziert den HTTP-Code; das Feld message enthält eine konkretere Fehlerursache auf Englisch (oder den Standard-HTTP-Statustext, falls keine weiteren Informationen vorliegen). Der Content-Type der Fehlerantwort ist application/json.

Beispiel

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

Beispielantwort:

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

Leistung und Skalierung

Perfect-Streamer-Server-Cache

Im PSS-Prozess gibt es einen In-Memory-LRU-Cache der Antworten mit dem Schlüssel (channel_id, UTC-Datum, Sprache) und einem Hard-Cap von 1024 Einträgen je EPG-Quelle. Bei typischer Last (zig bis hunderte Kanäle × 1–3 Sprachen × keep-day Tage) passen alle aktuellen Einträge vollständig in den Cache; wiederholte Anfragen werden ohne SQLite-Zugriff bedient.

Größenordnung (Debug-Build, lokales Loopback, ohne HTTPS):

Szenario

Latenz (Einzelanfrage)

Durchsatz (P=8)

Cache hit

~0,3 ms

~1100 Anfragen/s

Cache miss (SQL + JSON)

~1,0–1,5 ms

~1000 Anfragen/s

In einem Release-Build und ohne Debug-Logging sind die Werte etwa 2- bis 3-mal besser. Bandbreite — etwa 14 KB pro Antwort für einen typischen Kanal pro Tag.

Wann ein externer Reverse-Proxy (nginx) nötig ist

Der Server-Cache beschleunigt wiederholte Anfragen für dasselbe (channel, Tag, Sprache), aber jede Anfrage geht weiterhin durch den eingebauten PSS-HTTP-Server und belegt einen Thread aus dessen Pool. Bei vielen Clients ist es sinnvoll, das Caching an den Rand zu verlagern:

  • bis ~1.000 Online-Clients — der interne Cache reicht meist aus, ein Reverse-Proxy ist nicht erforderlich.

  • Zehntausende und mehr — ein cachender Reverse-Proxy (z. B. nginx) wird empfohlen. Ein Edge-Cache wickelt 99 % der Anfragen ohne PSS ab, dämpft Spitzen (Middleware-Start, massenhafte Player-Aktualisierung) und erlaubt es, die SSL-Terminierung auf einen separaten Knoten zu verlagern.

  • geografisch verteilte Auslieferung — ein externes CDN/Proxy ist nötig, noch bevor die Clientzahl gezählt wird.

PSS sendet einen eigenen Header Cache-Control: no-cache, no-store, must-revalidate, damit Endclients das EPG nicht lange zwischenspeichern. Der Reverse-Proxy darf (und sollte) die Antwort selbst cachen — unten wird gezeigt, wie man nginx explizit anweist, den Upstream-Cache-Control zu ignorieren und einen eigenen TTL zu führen.

Beispielkonfiguration für nginx

Eine minimale Konfiguration für einen EPG-Edge-Cache, ausgelegt für Zehntausende Clients mit Polling-Intervall 1–5 Minuten:

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

Erläuterungen und Empfehlungen

  • TTL ``proxy_cache_valid 200 60s`` — Kompromiss zwischen EPG-Frische und Last auf dem Upstream. Das Programm ändert sich nicht in Echtzeit, daher sind 30–300 Sekunden angemessen. Nach dem Import neuer Ereignisse leert PSS seinen Cache sofort, der Edge-Cache zieht beim nächsten TTL nach.

  • ``proxy_cache_lock on`` ist bei vielen Clients zwingend: Bei einem Cache-Miss bündelt es parallele Anfragen auf denselben Schlüssel zu einer einzigen Upstream-Anfrage und schützt SQLite vor Spitzen-BUSY unter Last.

  • ``keys_zone`` und ``max_size`` werden anhand der Anzahl (Kanal × Tag × Sprache) dimensioniert: 32 MB keys_zone reichen für Hunderttausende Schlüssel; 2 GB max_size decken einen Monat Verlauf für Hunderte Kanäle mit Reserve ab.

  • gzip reduziert den Verkehr deutlich: Antworten lassen sich gut komprimieren (sich wiederholende JSON-Schlüssel, Kyrillisch in UTF-8).

  • ``X-Cache-Status`` in der Antwort erlaubt der Middleware, HIT/MISS/EXPIRED zu sehen und die Effektivität des Caches zu beurteilen.

  • Liegen nginx und PSS auf derselben Maschine, verlangt der Admin-Server für Loopback keine HTTP-Digest-Authentifizierung, daher kann der Upstream-Block ohne proxy_set_header Authorization … bleiben. Bei einer Trennung über Netze legen Sie ein dediziertes viewer-Konto an und ergänzen Sie die Digest-Authentifizierung in proxy_pass.

  • HTTPS wird sinnvollerweise an nginx terminiert: PSS unterstützt HTTPS direkt, aber ein Edge-Server ist meist effizienter bei der Abwicklung von TLS-Handshakes für Tausende gleichzeitige Clients.