EPG for OTT middleware¶
The server returns electronic programme guide (EPG) data for the selected day for a single channel in JSON format. The endpoint is intended for OTT middleware back-ends that aggregate the schedule from Perfect Streamer to build the programme guide for the end client.
URL and authentication¶
The endpoint is served by the built-in admin server. By default it is available on ports 43971 (HTTP) and 43981 (HTTPS); the ports are configured under Settings/Server Settings.
GET /data/epg/channel?src=<src_id>&ch=<channel_id>&lang=<lang>&t=<time>
Authentication is HTTP Digest, like for the rest of /data/*. For middleware a viewer (read-only) account is sufficient.
Note
Requests from the loopback address (127.0.0.1) bypass HTTP Digest verification — the server treats them as anonymous. This is convenient for local scripts and health checks of middleware deployed on the same machine as Perfect Streamer; for remote access credentials are mandatory.
Request parameters¶
Parameter |
Type |
Required |
Default |
Description |
|---|---|---|---|---|
|
unsigned integer |
no |
|
EPG source. |
|
string |
yes |
— |
Channel identifier from the EPG database: the value of the |
|
integer or ISO 639 |
no |
system default language |
|
|
unsigned integer (Unix epoch, sec) |
no |
current server time |
Any point inside the day of interest. The server returns events for the UTC day that contains |
Note
The day is taken in UTC, not in the local time zone. If the middleware builds the schedule by the local calendar day, the UTC-day boundary may not coincide with local midnight; in that case make two requests (for adjacent UTC days) and stitch the results by the start field.
Response format¶
Content-Type:application/json.HTTP headers forbid caching on the client side and on intermediate proxies:
Cache-Control: no-cache, no-store, must-revalidate Pragma: no-cache Expires: 0
Response body is a JSON with the list of events for the day:
{
"event": [
{"start": 1715000000, "end": 1715003400, "title": "Morning News", "desc": "Overview of the day's events"},
{"start": 1715003400, "end": 1715007000, "title": "Weather Forecast", "desc": ""}
]
}
Event fields:
start,end— programme start and end in Unix epoch (sec), UTC.title— programme title in the selected language. If a title in the requested language is missing for the event, the server falls back to the entry in the system default language, then to any other available one.desc— extended description. May be an empty string if the database had no separate description for the event.
Output specifics:
Events with an empty title and no description, as well as events with invalid timestamps, are excluded from the output.
Duplicates by
startare dropped: for the same start moment one record is returned with the best language priority (requested → system default → others).The order of events in the array is not guaranteed — if needed, the middleware sorts the list by the
startfield on its own.If the channel is not found, the source does not exist, or there are no events for the chosen day, the server returns
200 OKwith an empty array{"event":[]}or, in the rare case of a missing source, with an empty body. The middleware must handle both variants correctly.
Caching on the server¶
The ready JSON is cached by the server under the key (channel_id, UTC date, language), so repeated requests for the same day and channel are served without touching the database. The middleware does not need to manage the cache.
The cache is flushed automatically:
when new events arrive from MPEG-TS EIT of input streams (
src=0);on a successful refresh of an external XMLTV source (
src > 0— scheduled or forced viaPOST /data/epg/update?s=<src_id>);when a day falls out of the EPG retention window.
A separate endpoint for forced cache flushing is neither provided nor needed.
HTTP response codes¶
Code |
Condition |
|---|---|
|
Request processed. Body is a JSON with the list of events (possibly empty). |
|
The |
|
HTTP Digest authentication is missing or invalid (for requests not from a loopback address). |
Other situations (unknown src, missing channel, no events for the day) do not produce 4xx — the middleware gets 200 OK with the empty array {"event":[]}.
On error, the response body is a fixed-format JSON envelope:
{"status": 400, "message": "Bad Request"}
The status field duplicates the HTTP code; the message field carries a refined error reason in English (or the standard HTTP-status text if no additional information is available). The Content-Type of the error response is application/json.
Example¶
curl -u middleware:secret --digest \
'http://pss.example.com:43971/data/epg/channel?src=0&ch=12.0.1&lang=rus&t=1715000000'
Sample response:
{
"event": [
{"start": 1715000000, "end": 1715003400, "title": "Morning News", "desc": "Overview of the day's events"},
{"start": 1715003400, "end": 1715007000, "title": "Weather Forecast", "desc": ""}
]
}
Performance and scaling¶
Perfect Streamer server cache¶
Inside the PSS process there is an in-memory LRU response cache keyed by (channel_id, UTC date, language) with a hard cap of 1024 entries per EPG source. Under typical load (tens–hundreds of channels × 1–3 languages × keep-day days) all current entries fit into the cache; repeat requests are served without touching SQLite.
Order of magnitude (debug build, local loopback, no HTTPS):
Scenario |
Latency (single request) |
Throughput (P=8) |
|---|---|---|
Cache hit |
~0.3 ms |
~1100 req/s |
Cache miss (SQL + JSON) |
~1.0–1.5 ms |
~1000 req/s |
In a release build with debug logging off, the numbers are roughly 2–3× better. Bandwidth — about 14 KB per response for a typical channel-day.
When an external reverse-proxy (nginx) is needed¶
The server cache speeds up repeat requests for the same (channel, day, language), but every request still passes through the built-in PSS HTTP server and consumes a thread from its pool. With many clients it makes sense to move caching to the edge:
up to ~1,000 online clients — the internal cache is usually enough, a reverse-proxy is not required.
tens of thousands and more — a caching reverse-proxy (for example, nginx) is recommended. An edge cache handles 99 % of requests without PSS, smooths out peaks (middleware startup, mass player refresh) and allows SSL termination to be moved to a separate node.
geographically distributed delivery — an external CDN/proxy is needed even before counting clients.
PSS sends its own header Cache-Control: no-cache, no-store, must-revalidate so that end clients do not cache the EPG for long. The reverse-proxy may (and should) cache the response itself — below it is shown how to tell nginx explicitly to ignore the upstream Cache-Control and keep its own TTL.
Sample nginx configuration¶
A minimal config for an EPG edge cache aimed at tens of thousands of clients polling every 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;
}
}
Notes and recommendations¶
TTL ``proxy_cache_valid 200 60s`` — a compromise between EPG freshness and load on the upstream. The programme guide does not change in real time, so 30–300 seconds is reasonable. After importing new events PSS flushes its own cache instantly, and the edge cache catches up at the next TTL.
``proxy_cache_lock on`` is mandatory for many clients: on a cache miss it coalesces parallel requests for the same key into a single upstream request, shielding SQLite from peak
BUSYunder load.``keys_zone`` and ``max_size`` are sized by the count of
(channel × day × language): 32 MB ofkeys_zonecovers hundreds of thousands of keys; 2 GB ofmax_sizecovers a month of history for hundreds of channels with room to spare.gzip significantly cuts traffic: responses compress well (repeated JSON keys, Cyrillic in UTF-8).
``X-Cache-Status`` in the response lets the middleware see
HIT/MISS/EXPIREDand gauge the cache effectiveness.If nginx and PSS live on the same machine, the admin server does not require HTTP Digest for loopback, so the upstream block can be left without
proxy_set_header Authorization …. For a cross-network setup, create a dedicatedvieweraccount and add Digest authentication toproxy_pass.HTTPS is best terminated at nginx: PSS supports HTTPS directly, but an edge server is usually more efficient at handling TLS handshakes with thousands of concurrent clients.