EPG для OTT middleware

Сервер выдаёт данные программы передач (EPG) за выбранные сутки по одному каналу в формате JSON. Эндпоинт предназначен для бэкенда OTT middleware, который агрегирует расписание из Perfect Streamer для построения программы передач у конечного клиента.

URL и аутентификация

Эндпоинт обслуживается встроенным admin-сервером. По умолчанию он доступен по портам 43971 (HTTP) и 43981 (HTTPS); порты настраиваются в разделе Settings/Server Settings.

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

Аутентификация — HTTP Digest, как у остальных /data/*. Для middleware достаточно учётной записи с ролью viewer (только чтение).

Примечание

Запросы с loopback-адреса (127.0.0.1) выполняются без проверки HTTP Digest — сервер считает их анонимными. Это удобно для локальных скриптов и health-check’ов middleware, развёрнутого на той же машине, что и Perfect Streamer; для удалённых обращений учётные данные обязательны.

Параметры запроса

Параметр

Тип

Обязат.

По умолчанию

Описание

src

целое без знака

нет

0

Источник EPG. 0 — данные, импортированные из MPEG-TS EIT входных потоков. 1, 2, … — идентификатор записи из /config/epg/epg-source (внешний XMLTV-источник).

ch

строка

да

—

Идентификатор канала из базы EPG: значение поля channel_id таблицы channel. Список доступных идентификаторов можно получить SQL-запросом через POST /data/epg/sql.

lang

целое или ISO 639

нет

системный язык по умолчанию

0 — системный язык по умолчанию; целое > 0 — внутренний идентификатор языка (см. GET /schema/lang); строка — двух- или трёхбуквенный код ISO 639, например eng, rus, fra.

t

целое без знака (Unix epoch, сек)

нет

текущее время сервера

Любая точка внутри интересующих суток. Сервер выдаёт события за UTC-сутки, которым принадлежит t: интервал [t / 86400 · 86400, (t / 86400 + 1) · 86400). Для получения данных за «следующие сутки» достаточно прибавить 86400 к текущему времени.

Примечание

Сутки берутся в UTC, а не в локальном часовом поясе. Если middleware формирует расписание по локальному календарному дню, граница UTC-суток может не совпадать с локальной полночью; в таком случае нужно сделать два запроса (за смежные UTC-сутки) и склеить результаты по полю start.

Формат ответа

  • Content-Type: application/json.

  • HTTP-заголовки запрещают кеширование на стороне клиента и промежуточных прокси:

    Cache-Control: no-cache, no-store, must-revalidate
    Pragma: no-cache
    Expires: 0
    
  • Тело ответа — JSON со списком событий за сутки:

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

Поля события:

  • start, end — моменты начала и окончания передачи в Unix epoch (сек), UTC.

  • title — заголовок передачи на выбранном языке. Если для события на запрошенном языке заголовка нет, сервер берёт запись на системном языке по умолчанию, затем — на любом другом доступном.

  • desc — расширенное описание. Может быть пустой строкой, если в базе для события не было отдельного описания.

Особенности выдачи:

  • В выдачу не попадают события с пустым заголовком и без описания, а также события с некорректными метками времени.

  • Дубликаты по start отсекаются: для одного и того же момента начала возвращается одна запись с лучшим приоритетом языка (запрошенный → системный default → остальные).

  • Порядок событий в массиве не гарантируется — при необходимости middleware сортирует список по полю start самостоятельно.

  • Если канал не найден, источник не существует или за выбранные сутки нет событий, сервер возвращает 200 OK с пустым массивом {"event":[]} либо с пустым телом (в редком случае отсутствия источника). Middleware должен корректно обрабатывать оба варианта.

Кеширование на сервере

Готовый JSON кешируется сервером по ключу (channel_id, дата UTC, язык), поэтому повторные запросы за те же сутки одного канала обрабатываются без обращения к базе данных. От middleware никаких действий по управлению кешем не требуется.

Сброс кеша выполняется автоматически:

  • при поступлении новых событий из MPEG-TS EIT входных потоков (src=0);

  • при успешном обновлении внешнего XMLTV-источника (src > 0 — плановое обновление либо принудительное через POST /data/epg/update?s=<src_id>);

  • при выходе суток за окно хранения EPG.

Принудительный сброс кеша отдельным эндпоинтом не предусмотрен и не требуется.

HTTP-коды ответа

Код

Условие

200 OK

Запрос обработан. Тело — JSON со списком событий (возможно, пустым).

400 Bad Request

Параметр ch не указан или пуст; либо src, t не разбираются как беззнаковое целое; либо числовой lang указывает на несуществующий идентификатор языка.

401 Unauthorized

Отсутствует либо некорректна HTTP Digest аутентификация (для запросов не с loopback-адреса).

Прочие ситуации (неизвестный src, отсутствие канала, отсутствие событий за сутки) не приводят к 4xx — middleware получает 200 OK с пустым массивом {"event":[]}.

Тело ответа при ошибке — JSON-конверт фиксированного формата:

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

Поле status дублирует HTTP-код, поле message содержит уточнённую причину ошибки на английском (либо стандартный текст HTTP-статуса, если дополнительной информации нет). Content-Type ответа об ошибке — application/json.

Пример

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

Пример ответа:

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

Производительность и масштабирование

Серверный кеш Perfect Streamer

Внутри процесса PSS реализован in-memory LRU-кеш ответов с ключом (channel_id, дата UTC, язык) и хард-капом 1024 записи на источник EPG. При типовой нагрузке (десятки–сотни каналов × 1–3 языка × keep-day дней) все актуальные записи помещаются в кеш целиком; ответы на повторные запросы обрабатываются без обращения к SQLite.

Порядок величин (debug-сборка, локальный loopback, без HTTPS):

Сценарий

Задержка (одиночный запрос)

Пропускная способность (P=8)

Cache hit

~0.3 мс

~1100 запросов/с

Cache miss (SQL + JSON)

~1.0–1.5 мс

~1000 запросов/с

В release-сборке и без отладочного логирования цифры примерно в 2–3 раза лучше. Пропускная способность — около 14 КБ на ответ для типового канала за сутки.

Когда нужен внешний reverse-proxy (nginx)

Серверный кеш ускоряет повторные обращения к одному и тому же (channel, день, язык), но каждый запрос всё равно проходит через встроенный HTTP-сервер PSS и тратит поток из его пула. На большом числе клиентов имеет смысл вынести кеширование на edge:

  • до ~1 000 онлайн-клиентов — внутреннего кеша обычно достаточно, reverse-proxy не обязателен.

  • десятки тысяч и больше — рекомендуется reverse-proxy с кешем (например, nginx). Edge-кеш отрабатывает 99 % запросов без участия PSS, амортизирует пики (старт middleware, массовое обновление плеера) и даёт возможность поставить SSL-терминацию на отдельный узел.

  • географически распределённая раздача — внешний CDN/прокси нужен ещё до подсчёта числа клиентов.

PSS отдаёт собственный заголовок Cache-Control: no-cache, no-store, must-revalidate, чтобы конечные клиенты не закешировали EPG надолго. Reverse-proxy при этом может (и должен) кешировать ответ самостоятельно — ниже показано, как явно сказать nginx игнорировать Cache-Control upstream-а и держать собственный TTL.

Пример конфигурации nginx

Минимальный конфиг для edge-кеша EPG, рассчитанный на десятки тысяч клиентов с интервалом опроса 1–5 минут:

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

Пояснения и рекомендации

  • TTL ``proxy_cache_valid 200 60s`` — компромисс между свежестью EPG и нагрузкой на upstream. Программа передач не меняется в реальном времени, поэтому 30–300 секунд выглядят разумно. После импорта новых событий PSS сбрасывает свой кеш мгновенно, а edge-кеш догонит за следующий TTL.

  • ``proxy_cache_lock on`` обязательно для большого числа клиентов: при cache miss он коалесцирует параллельные запросы к одному ключу в один upstream-запрос, защищая SQLite от пиковых BUSY под нагрузкой.

  • ``keys_zone`` и ``max_size`` подбираются по числу (канал × сутки × язык): 32 МБ keys_zone хватит на сотни тысяч ключей; 2 ГБ max_size — на месяц истории по сотням каналов с запасом.

  • gzip существенно режет трафик: ответы хорошо сжимаются (повторяющиеся ключи JSON, кириллица в UTF-8).

  • ``X-Cache-Status`` в ответе позволяет на стороне middleware видеть HIT/MISS/EXPIRED и оценить эффективность кеша.

  • Если nginx и PSS живут на одной машине — admin-сервер не требует HTTP Digest для loopback, поэтому upstream-блок можно оставить без proxy_set_header Authorization …. Для разнесения по сетям заведите отдельную учётку viewer и добавьте Digest-аутентификацию в proxy_pass.

  • HTTPS разумно терминировать на nginx: PSS поддерживает HTTPS напрямую, но edge-сервер обычно эффективнее в обработке TLS-handshakes при тысячах одновременных клиентов.