mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-09-30 20:02:06 +02:00
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.
464 lines
22 KiB
Markdown
464 lines
22 KiB
Markdown
# Подсистема стриминга
|
||
|
||
Подсистема стриминга обрабатывает доставку live, VOD и timeshift.
|
||
Это горячий путь (~10K-100K запросов/мин, <50 мс p99), и она использует отдельный лёгкий bootstrap, чтобы не загружать полный административный стек.
|
||
|
||
---
|
||
|
||
## Поток запроса
|
||
|
||
```text
|
||
запрос клиента
|
||
|
|
||
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`
|
||
|
||
Действия по порядку:
|
||
|
||
1. Загрузить коды ошибок, обработчик, пути, конфигурацию, бинарники.
|
||
2. Защита от флуда (только HTTP): проверить `FLOOD_TMP_PATH . 'block_' . $rIP`.
|
||
3. Загрузить настройки из файлового кэша (`CACHE_TMP_PATH . 'settings'`).
|
||
4. Верификация хоста (только HTTP): проверить по `allowed_domains`.
|
||
5. Инициализировать логгер.
|
||
6. Fail-closed гейт: вернуть 404, если настройки отсутствуют (кроме `/status`).
|
||
7. Вызвать `StreamingBootstrap::bootstrap()`.
|
||
|
||
### 2. StreamingBootstrap::bootstrap()
|
||
|
||
Файл: `src/Streaming/StreamingBootstrap.php`
|
||
|
||
```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`
|
||
|
||
```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` | Запрошенный видеокодек |
|
||
|
||
Валидация:
|
||
|
||
1. Расшифровать токен с помощью `live_streaming_pass`.
|
||
2. Проверить срок: `$rTokenData['expires'] < time() - $rServers[SERVER_ID]['time_offset']`.
|
||
3. Вернуть разобранные данные токена или сгенерировать ошибку.
|
||
|
||
Заголовки ответа устанавливаются через `StreamAuthMiddleware::sendStreamHeaders()`:
|
||
|
||
```text
|
||
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 строк):
|
||
|
||
1. Расшифровать токен через `StreamAuthMiddleware::decryptToken()`.
|
||
2. Определить сервер/прокси: `StreamAuth::checkAccess()` + `ProxySelector::availableProxy()`.
|
||
3. Применить ограничения подключений: `StreamAuth::validateConnections()`.
|
||
4. Создать запись о подключении: `ConnectionTracker::createConnection()`.
|
||
5. Доставить контент:
|
||
- **M3U8:** `HLSGenerator::generateHLS()` → клиент получает сегменты через `segment.php`.
|
||
- **TS:** Зацикленные сегменты с использованием `AsyncFileOperations::awaitFileExists()`.
|
||
6. Каждые 5 минут: обновить настройки, обновить `hls_last_read`, проверить жив ли процесс.
|
||
7. При выходе: `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`
|
||
|
||
Ключевые методы:
|
||
|
||
```php
|
||
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 вместо точного IP
|
||
- `restrict_same_ip` — возвращать ошибку при несовпадении IP вместо убийства
|
||
|
||
### ShutdownHandler
|
||
|
||
Файл: `src/Streaming/Lifecycle/ShutdownHandler.php`
|
||
|
||
Зарегистрирован через `register_shutdown_function()`. При завершении процесса PHP:
|
||
|
||
1. Закрыть запись подключения в `lines_live` или Redis.
|
||
2. Удалить tmp-файлы по пути `CONS_TMP_PATH . $uuid`.
|
||
3. Убрать on-demand поток из очереди, если применимо.
|
||
|
||
---
|
||
|
||
## Балансировка нагрузки
|
||
|
||
### Выбор сервера (StreamAuth::checkAccess)
|
||
|
||
Файл: `src/Streaming/Auth/StreamAuth.php`
|
||
|
||
```php
|
||
public static function checkAccess($rUserInfo, $rUserIP, $rCountryCode, $rUserISP = ''): int|false
|
||
```
|
||
|
||
Алгоритм:
|
||
|
||
1. Получить доступные серверы: `server_online == true`, `server_type == 0`, `online_clients < total_clients`.
|
||
2. Отсортировать по загрузке (по возрастанию) — наименее загруженные первыми.
|
||
3. Применить GeoIP-маршрутизацию (если `enable_geoip == 1`):
|
||
- Точное совпадение по стране → выбрать сразу.
|
||
- `geoip_type == 'strict'` → исключить несоответствующие.
|
||
- Иначе → присвоить весовой приоритет.
|
||
4. Применить ISP-маршрутизацию (если `enable_isp == 1`): та же логика, что и GeoIP.
|
||
5. Вернуть сервер с наименьшей загрузкой из группы с наивысшим приоритетом.
|
||
|
||
### Выбор прокси (ProxySelector::availableProxy)
|
||
|
||
Файл: `src/Streaming/Balancer/ProxySelector.php`
|
||
|
||
```php
|
||
public static function availableProxy($rProxies, $rCountryCode, $rUserISP = ''): int|null
|
||
```
|
||
|
||
Тот же алгоритм, что и `StreamAuth::checkAccess()`, применённый к списку прокси-серверов.
|
||
|
||
---
|
||
|
||
## Rate limiting и защита от флуда
|
||
|
||
Три уровня:
|
||
|
||
### 1. nginx (уровень подключения)
|
||
|
||
```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)
|
||
|
||
```php
|
||
if (file_exists(FLOOD_TMP_PATH . 'block_' . $rIP)) {
|
||
http_response_code(403);
|
||
exit();
|
||
}
|
||
```
|
||
|
||
Файловая блокировка IP. Блокировочные файлы создаёт вышестоящая логика детекции флуда.
|
||
|
||
### 3. ConnectionLimiter (на пользователя)
|
||
|
||
Применяется после валидации токена. Ограничивает одновременные потоки на пользователя на основе `max_connections`.
|
||
|
||
---
|
||
|
||
## Шифрование HLS
|
||
|
||
Файл: `src/Streaming/Delivery/HLSGenerator.php`
|
||
|
||
```php
|
||
public static function generateHLS($rSettings, $rM3U8, $rUsername, $rPassword,
|
||
$rStreamID, $rUUID, $rIP, ...): string|false
|
||
```
|
||
|
||
Когда `encrypt_hls == true`:
|
||
|
||
1. Сгенерировать токен AES-128 ключа из IP + StreamID + соль.
|
||
2. Заменить IV содержимым `STREAMS_PATH . $rStreamID . '_.iv'`.
|
||
3. Зашифровать ссылку на каждый сегмент: `IP/StreamID/Segment/UUID/SERVER_ID/VideoCodec/OnDemand`.
|
||
4. Заменить имена сегментов на `/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 с), чтобы подхватывать изменения конфигурации без перезапуска |
|
||
|
||
---
|
||
|
||
## Пути файловой системы
|
||
|
||
```text
|
||
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.
|
||
|
||
```bash
|
||
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 замирает (ребуферинг).
|
||
|
||
```text
|
||
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`.
|
||
|
||
```bash
|
||
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 дэшборд буфера |
|