Standalone Python (stdlib-only) tool that verifies a stream delivers
segments correctly and its delivery queue does not break:
- HLS: EXT-X-MEDIA-SEQUENCE contiguity, no dropped/rewound segments, no
EXT-X-DISCONTINUITY, every newly appearing segment downloadable.
- MPEG-TS: per-PID continuity_counter, sync-byte loss, TEI, delivery stalls,
with a --tolerance for rare source glitches relayed by -c copy.
- --live: colored TUI dashboard modelling a virtual player — received
timeline from PCR (TS) / EXTINF (HLS), playhead, and buffered cache
seconds graphed over time.
Documented in docs/{en,ru}/development/streaming-subsystem.md.
22 KiB
Подсистема стриминга
Подсистема стриминга обрабатывает доставку live, VOD и timeshift. Это горячий путь (~10K-100K запросов/мин, <50 мс p99), и она использует отдельный лёгкий bootstrap, чтобы не загружать полный административный стек.
Поток запроса
запрос клиента
|
nginx rewrite (/auth/{token} -> /stream/live.php?token={token})
|
StreamingRequestBootstrap::init()
|
StreamingBootstrap::bootstrap()
|
LegacyInitializer::initStreaming()
|
логика endpoint (live.php / vod.php / timeshift.php)
|
ShutdownHandler::handle()
nginx переписывает все стриминговые URL на PHP-точки входа в www/stream/:
| Шаблон URL | Точка входа | Назначение |
|---|---|---|
/auth/{token} |
live.php |
Доставка live-потока |
/vauth/{token} |
vod.php |
Доставка видео по запросу |
/tsauth/{token} |
timeshift.php |
Воспроизведение архива/timeshift |
/hls/{token} |
segment.php |
Доставка HLS-сегментов |
/key/{token} |
key.php |
Ключ шифрования AES-128 |
/subauth/{token} |
subtitle.php |
Доставка субтитров |
Структура каталогов
src/Streaming/
├── StreamingBootstrap.php
├── AsyncFileOperations.php
├── TimeshiftClient.php
├── Auth/
│ ├── StreamAuth.php
│ └── StreamAuthMiddleware.php
├── Balancer/
│ └── ProxySelector.php
├── Codec/
│ ├── FFmpegCommand.php
│ ├── FfmpegPaths.php
│ └── FFprobeRunner.php
├── Delivery/
│ ├── HLSGenerator.php
│ ├── OffAirHandler.php
│ ├── SegmentReader.php
│ ├── SignalSender.php
│ └── StreamRedirector.php
├── Health/
│ └── ProcessChecker.php
├── Lifecycle/
│ └── ShutdownHandler.php
└── Protection/
└── ConnectionLimiter.php
src/www/stream/
├── init.php # Прослойка legacy bootstrap (устарела)
├── auth.php # Шлюз валидации токена
├── live.php # Доставка live-стриминга
├── vod.php # Доставка VOD
├── timeshift.php # Воспроизведение архива/timeshift
├── segment.php # Доставка HLS-сегментов
├── key.php # Доставка ключа шифрования
├── subtitle.php # Доставка субтитров
├── thumb.php # Доставка превью
└── rtmp.php # Endpoint публикации RTMP
Bootstrap-конвейер
1. StreamingRequestBootstrap::init()
Файл: src/Infrastructure/Bootstrap/StreamingRequestBootstrap.php
Действия по порядку:
- Загрузить коды ошибок, обработчик, пути, конфигурацию, бинарники.
- Защита от флуда (только HTTP): проверить
FLOOD_TMP_PATH . 'block_' . $rIP. - Загрузить настройки из файлового кэша (
CACHE_TMP_PATH . 'settings'). - Верификация хоста (только HTTP): проверить по
allowed_domains. - Инициализировать логгер.
- Fail-closed гейт: вернуть 404, если настройки отсутствуют (кроме
/status). - Вызвать
StreamingBootstrap::bootstrap().
2. StreamingBootstrap::bootstrap()
Файл: src/Streaming/StreamingBootstrap.php
public static function bootstrap($rFilename, $rSettings)
Классифицирует endpoint:
- Probe endpoints:
probe,player_api(лёгкая нагрузка) - Default endpoints:
live,thumb,subtitle,timeshift,vod,status - Privileged endpoints:
rtmp,portal
Загружает AsyncFileOperations.php и DatabaseHandler.php, сохраняет настройки в $GLOBALS['rSettings'] и данные доступа в $GLOBALS['rAccess'], затем вызывает LegacyInitializer::initStreaming().
Возвращает экземпляр базы данных $db (используется legacy-точками входа).
3. LegacyInitializer::initStreaming()
Файл: src/Core/Init/LegacyInitializer.php
Заполняет глобальные переменные из кэша:
$GLOBALS['rSettings'],$GLOBALS['rServers'],$GLOBALS['rBouquets']$GLOBALS['rBlockedUA'],$GLOBALS['rBlockedISP'],$GLOBALS['rBlockedIPs']$GLOBALS['rAllowedIPs'],$GLOBALS['rProxies'],$GLOBALS['rSegmentSettings']$GLOBALS['rFFMPEG_CPU'],$GLOBALS['rFFMPEG_GPU'],$GLOBALS['rFFPROBE']
Подключается к базе данных/Redis в зависимости от $rSettings['redis_handler'].
Важно: Стриминговый путь читает исключительно из файлового кэша. Он не обращается к базе данных за настройками или поиском пользователей в нормальной работе.
Аутентификация по токену
Файл: src/Streaming/Auth/StreamAuthMiddleware.php
StreamAuthMiddleware::decryptToken($rToken, $rSettings, $rServers, $rIP): array
Содержимое токена:
| Поле | Описание |
|---|---|
username |
Имя пользователя линии |
password |
Пароль линии |
stream_id |
ID целевого потока |
expires |
Временная метка истечения токена |
channel_info |
Метаданные потока (on_demand, proxy, pid) |
user_info |
Права пользователя (max_connections, is_restreamer) |
country_code |
Код страны GeoIP |
video_codec |
Запрошенный видеокодек |
Валидация:
- Расшифровать токен с помощью
live_streaming_pass. - Проверить срок:
$rTokenData['expires'] < time() - $rServers[SERVER_ID]['time_offset']. - Вернуть разобранные данные токена или сгенерировать ошибку.
Заголовки ответа устанавливаются через StreamAuthMiddleware::sendStreamHeaders():
Access-Control-Allow-Origin: *
X-XSS-Protection: 0
X-Content-Type-Options: nosniff
Alt-Svc: h3-29, h3-T051, h3-Q050 (подсказки HTTP/3)
Доставка потока
Live (live.php)
Основная точка доставки (~650 строк):
- Расшифровать токен через
StreamAuthMiddleware::decryptToken(). - Определить сервер/прокси:
StreamAuth::checkAccess()+ProxySelector::availableProxy(). - Применить ограничения подключений:
StreamAuth::validateConnections(). - Создать запись о подключении:
ConnectionTracker::createConnection(). - Доставить контент:
- M3U8:
HLSGenerator::generateHLS()→ клиент получает сегменты черезsegment.php. - TS: Зацикленные сегменты с использованием
AsyncFileOperations::awaitFileExists().
- M3U8:
- Каждые 5 минут: обновить настройки, обновить
hls_last_read, проверить жив ли процесс. - При выходе:
ShutdownHandler::handle()→ закрыть запись подключения.
VOD (vod.php)
Та же логика аутентификации, что и у live. Читает из VOD_PATH вместо STREAMS_PATH.
Timeshift (timeshift.php)
Отдаёт архивные сегменты. Использует TimeshiftClient для определения архивного файла.
Управление подключениями
ConnectionTracker
Управляет состоянием активных подключений. Бэкенд выбирается через $rSettings['redis_handler']:
Redis (предпочтительно для масштаба):
- Подключения хранятся в sorted sets:
LINE#{identity}— подключения пользователяSTREAM#{stream_id}— подключения к потокуSERVER#{server_id}— подключения на сервере
MySQL (резервный вариант):
- Таблица:
lines_liveс полями:activity_id,user_id,stream_id,server_id,uuid,pid,hls_end
Ключевые методы:
ConnectionTracker::createConnection($data)
ConnectionTracker::updateConnection($connection, $changes, 'open'|'close')
ConnectionTracker::getConnection($uuid)
ConnectionTracker::getLineConnections($user_id)
ConnectionTracker::getCapacity()
ConnectionLimiter
Файл: src/Streaming/Protection/ConnectionLimiter.php
Применяет ограничения подключений на пользователя при превышении max_connections:
| Приоритет | Критерий | Действие |
|---|---|---|
| 2 | Тот же IP + тот же User-Agent | Убить первым |
| 1 | Тот же IP (любой UA) | Убить следующим |
| 0 | Любое подключение | Убить как fallback |
Настройки:
disallow_2nd_ip_con— требовать один IP на пользователяip_subnet_match— сопоставлять по подсети /24 вместо точного IPrestrict_same_ip— возвращать ошибку при несовпадении IP вместо убийства
ShutdownHandler
Файл: src/Streaming/Lifecycle/ShutdownHandler.php
Зарегистрирован через register_shutdown_function(). При завершении процесса PHP:
- Закрыть запись подключения в
lines_liveили Redis. - Удалить tmp-файлы по пути
CONS_TMP_PATH . $uuid. - Убрать on-demand поток из очереди, если применимо.
Балансировка нагрузки
Выбор сервера (StreamAuth::checkAccess)
Файл: src/Streaming/Auth/StreamAuth.php
public static function checkAccess($rUserInfo, $rUserIP, $rCountryCode, $rUserISP = ''): int|false
Алгоритм:
- Получить доступные серверы:
server_online == true,server_type == 0,online_clients < total_clients. - Отсортировать по загрузке (по возрастанию) — наименее загруженные первыми.
- Применить GeoIP-маршрутизацию (если
enable_geoip == 1):- Точное совпадение по стране → выбрать сразу.
geoip_type == 'strict'→ исключить несоответствующие.- Иначе → присвоить весовой приоритет.
- Применить ISP-маршрутизацию (если
enable_isp == 1): та же логика, что и GeoIP. - Вернуть сервер с наименьшей загрузкой из группы с наивысшим приоритетом.
Выбор прокси (ProxySelector::availableProxy)
Файл: src/Streaming/Balancer/ProxySelector.php
public static function availableProxy($rProxies, $rCountryCode, $rUserISP = ''): int|null
Тот же алгоритм, что и StreamAuth::checkAccess(), применённый к списку прокси-серверов.
Rate limiting и защита от флуда
Три уровня:
1. nginx (уровень подключения)
limit_req_zone $binary_remote_addr zone=one:30m rate=20r/s;
limit_req zone=one burst=8;
20 запросов/секунду на IP с burst-окном на 8 запросов. Скользящее окно 30 минут.
2. StreamingRequestBootstrap (блокировка IP)
if (file_exists(FLOOD_TMP_PATH . 'block_' . $rIP)) {
http_response_code(403);
exit();
}
Файловая блокировка IP. Блокировочные файлы создаёт вышестоящая логика детекции флуда.
3. ConnectionLimiter (на пользователя)
Применяется после валидации токена. Ограничивает одновременные потоки на пользователя на основе max_connections.
Шифрование HLS
Файл: src/Streaming/Delivery/HLSGenerator.php
public static function generateHLS($rSettings, $rM3U8, $rUsername, $rPassword,
$rStreamID, $rUUID, $rIP, ...): string|false
Когда encrypt_hls == true:
- Сгенерировать токен AES-128 ключа из IP + StreamID + соль.
- Заменить IV содержимым
STREAMS_PATH . $rStreamID . '_.iv'. - Зашифровать ссылку на каждый сегмент:
IP/StreamID/Segment/UUID/SERVER_ID/VideoCodec/OnDemand. - Заменить имена сегментов на
/hls/{encrypted_token}.
Доставка ключа происходит через key.php с использованием того же механизма токенов.
Производительность
Ключевые проектные решения для пропускной способности и задержки:
| Особенность | Механизм |
|---|---|
| Неблокирующее ожидание файла | AsyncFileOperations::awaitFileExists() использует inotify (Linux) или оптимизированный polling |
| Нулевая CPU-нагрузка при ожидании | time_nanosleep() через AsyncFileOperations::efficientSleep() |
| Буферизация nginx | 128 буферов по 32 КБ на запрос |
| Пул подключений | Redis (предпочтительно) или persistent MySQL |
| Чтения только из кэша | Настройки и данные пользователей читаются из файлового кэша, без запросов к БД |
| Ранний выход | Мониторинг connection_status() каждые 5 секунд для определения отключения клиента |
| Обновление настроек | Каждые 5 минут (300 с), чтобы подхватывать изменения конфигурации без перезапуска |
Пути файловой системы
STREAMS_PATH = /home/xc_vm/www/stream/
CONS_TMP_PATH = /home/xc_vm/tmp/
CACHE_TMP_PATH = /home/xc_vm/tmp/cache/
FLOOD_TMP_PATH = /home/xc_vm/tmp/flood/
SIGNALS_PATH = /home/xc_vm/tmp/signals/
VIDEO_PATH = /home/xc_vm/www/video/
ARCHIVE_PATH = /home/xc_vm/www/archive/
VOD_PATH = /home/xc_vm/www/vod/
Диагностика и инструменты
Два инструмента проверяют, что поток доставляется корректно — что сегменты приходят по порядку и очередь доставки не бьётся.
tools/stream_queue_check.py (Python, только stdlib)
Автономный монитор целостности очереди сегментов/пакетов с опциональным live дэшбордом буфера. Авто-детект HLS vs MPEG-TS.
python3 tools/stream_queue_check.py "<url>" --duration 30 # разовая проверка
python3 tools/stream_queue_check.py "<url>" --json # cron / мониторинг
python3 tools/stream_queue_check.py "<url>" --live --duration 0 # live дэшборд
Что значит «очередь цела» по типу потока:
| Поток | Проверка очереди |
|---|---|
HLS (.m3u8) |
EXT-X-MEDIA-SEQUENCE монотонна и без пропусков (сегменты не выпадают и не откатываются), нет EXT-X-DISCONTINUITY, каждый новый сегмент скачивается. Master-плейлисты резолвятся в первый вариант. |
MPEG-TS (.ts, /play/<token>/ts) |
per-PID continuity_counter (потеря / дубли / переупорядочивание пакетов = разрыв очереди), потеря sync-байта, transport-error indicator, стойла доставки. |
Основные опции:
| Флаг | Назначение |
|---|---|
--duration N |
сколько секунд наблюдать (0 = до Ctrl-C в --live) |
--tolerance N |
допустить N транзиентных разрывов до вердикта BROKEN (игнор редких глитчей источника при -c copy) |
--stall-timeout S |
пауза в доставке, считаемая стойлом; держи выше длительности сегмента (по умолч. 15) |
--live |
цветной TUI-дэшборд (ниже) |
--prebuffer S / --buffer-target S |
live: пребуфер виртуального плеера и шкала графика буфера |
--json / --no-color |
машинный вывод / без ANSI |
Код возврата: 0 здоров, 2 проблема очереди или стойло, 1 неверный вызов.
Live дэшборд (--live)
Моделирует виртуальный плеер: playhead идёт в реальном времени, пока контент
«получается». Для TS таймлайн получения берётся из PCR (часы потока), для
HLS — из длительностей EXTINF сегментов. Буфер («кеш») = получено − воспроизведено;
если он доходит до нуля, playhead замирает (ребуферинг).
STREAM QUEUE / BUFFER MONITOR TS up 00:22
cache buffer (s), last 60s:
▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▄▄▄▇▇▇▆▆▆▅▅▅▄▄▇▇▇▆▆▆▅ <- burst-then-drain = пила доставки
IN CACHE : [█████████████████░░░░░░░░░░░░░] 11.6s / 20s
PLAYING : PLAYING head 00:18 received 00:29
rate 1000 kbit/s received 4.1 MB last data 7.0s ago
QUEUE OK cc:0 sync:0 gaps:0 disc:0 rebuffers:0
График и gauge буфера цветные: зелёный (норма) / жёлтый (мало) / красный (голодание). Для HLS ряд блоков показывает сегменты, оставшиеся в кеше впереди playhead.
console.php stream:check (PHP, компаньон)
Пробует URL источника и с --decode скачивает/декодирует медиа, чтобы поймать
битые сегменты. HLS проверяется посегментно; одноразовый TS-эндпоинт захватывается
через cURL и декодируется офлайн (живой -i ffmpeg на нём зависает).
Файл: src/Cli/Commands/StreamCheckCommand.php.
console.php stream:check "<url>" # проба метаданных (тип, кодеки)
console.php stream:check "<url>" --decode=30 --json
Замечание — пейсинг доставки. Цикл отдачи live-TS в
live.phpсливает доступные данные без throttle и делает паузу только когда догнал голову записи ffmpeg. Прежняя версия спала одну секунду после каждого чтения, ограничивая отдачу доread_buffer_sizeв секунду и вызывая голодание клиентов;stream_queue_check.py --liveвизуализирует поведение буфера.
Связанные файлы
| Файл | Назначение |
|---|---|
src/Streaming/StreamingBootstrap.php |
основной bootstrap стриминга |
src/Infrastructure/Bootstrap/StreamingRequestBootstrap.php |
инициализация на HTTP-уровне |
src/Streaming/Auth/StreamAuth.php |
выбор сервера и валидация подключений |
src/Streaming/Auth/StreamAuthMiddleware.php |
расшифровка токена и заголовки ответа |
src/Streaming/Balancer/ProxySelector.php |
выбор прокси-сервера |
src/Streaming/Protection/ConnectionLimiter.php |
ограничения подключений на пользователя |
src/Streaming/Delivery/HLSGenerator.php |
генерация плейлиста M3U8 |
src/Streaming/Delivery/SegmentReader.php |
извлечение сегментов из плейлистов |
src/Streaming/Delivery/StreamRedirector.php |
доступность потока и маршрутизация серверов |
src/Streaming/AsyncFileOperations.php |
неблокирующие утилиты файловой системы |
src/Streaming/Lifecycle/ShutdownHandler.php |
очистка подключения при выходе |
src/Domain/Stream/ConnectionTracker.php |
состояние подключений в Redis/MySQL |
src/Core/Init/LegacyInitializer.php |
настройка глобальных переменных для стриминга |
src/Cli/Commands/StreamCheckCommand.php |
stream:check — проба/декод потока на битые сегменты |
tools/stream_queue_check.py |
монитор целостности очереди + live дэшборд буфера |