---
title: EPG für OTT-Middleware
url: https://doc.pstreamer.tv/de/manual/web_server_epg.html
lang: de
product: Perfect Streamer
version: 1.13.3.445
---

# 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:

```json
{
  "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:

```json
{"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

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

Beispielantwort:

```json
{
  "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:

```nginx
# /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.

## Verwandte Endpunkte

- `POST /data/epg/sql?s=<src_id>` — beliebige SQL-Abfrage an die EPG-Datenbank (insbesondere, um eine Liste der `channel_id` zu erhalten).
- `POST /data/epg/update?s=<src_id>` — erzwungene Aktualisierung einer externen XMLTV-Quelle.

Die vollständige Liste und ausführliche Beschreibung der HTTP-API finden Sie in `manual/http_data_api.txt`.
