Files
XC_VM/docs/ru/development/streaming-subsystem.md
T
2026-09-16 22:04:48 +03:00

530 lines
41 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Подсистема потоковой передачи
Подсистема потоковой передачи обрабатывает доставку в реальном времени, VOD и timeshift.
Это быстрый путь (~10-100 тыс. запросов в минуту, <50 мс p99), и он использует отдельный облегченный bootstrap, чтобы избежать загрузки всего стека администратора.
---
## Поток запросов
```text
client request
|
nginx rewrite (/auth/{token} -> /stream/live.php?token={token})
|
StreamingRequestBootstrap::init()
|
StreamingBootstrap::bootstrap()
|
LegacyInitializer::initStreaming()
|
endpoint logic (live.php / vod.php / timeshift.php)
|
ShutdownHandler::handle()
```
nginx переписывает все URL-адреса потоковой передачи на PHP точки входа в соответствии с `Public/stream/`:
|Шаблон URL-адреса|Точка входа|Цель|
| --- | --- | --- |
| `/auth/{token}` | `live.php` |Прямая трансляция|
| `/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
├── Auth/
│ ├── StreamAuth.php
│ └── StreamAuthMiddleware.php
├── Balancer/
│ └── ProxySelector.php
├── Codec/
│ ├── FFmpegCommand.php
│ ├── FfmpegPaths.php
│ └── FFprobeRunner.php
├── Delivery/
│ ├── HLSGenerator.php
│ ├── OffAirHandler.php
│ └── StreamRedirector.php
├── Fanout/
│ └── FanoutClient.php
├── Health/
│ └── ProcessChecker.php
├── Lifecycle/
│ └── ShutdownHandler.php
└── Protection/
└── ConnectionLimiter.php
src/Public/stream/
├── index.php # Entry router for the stream endpoints
├── auth.php # Token validation gateway
├── live.php # Live streaming delivery
├── vod.php # VOD delivery
├── timeshift.php # Archive/timeshift playback
├── segment.php # HLS segment delivery
├── key.php # Encryption key delivery
├── subtitle.php # Subtitle delivery
├── thumb.php # Thumbnail delivery
├── probe.php # Stream probe / off-air status
└── rtmp.php # RTMP publishing endpoint
```
---
## Конвейер начальной загрузки
### 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. Аварийно закрытый шлюз: возвращает 404, если настройки отсутствуют (кроме `/status`).
7. Вызовите `StreamingBootstrap::bootstrap()`.
### 2. Потоковый загрузчик::bootstrap()
Файл: `src/Streaming/StreamingBootstrap.php`
```php
public static function bootstrap($rFilename, $rSettings)
```
Классифицирует конечную точку:
- **Конечные точки зондирования:** `probe`, `player_api` ( небольшая нагрузка)
- **Конечные точки по умолчанию:** `live`, `thumb`, `subtitle`, `timeshift`, `vod`, `status`
- **Привилегированные конечные точки:** `rtmp`, `portal`
Загружает `AsyncFileOperations.php` и `DatabaseHandler.php`, сохраняет настройки в `$GLOBALS['rSettings']` и получает доступ к данным в `$GLOBALS['rAccess']`, затем вызывает `LegacyInitializer::initStreaming()`.
Возвращает экземпляр базы данных `$db` (используемый устаревшими точками входа).
### 3. LegacyInitializer::Инициализация потока()
Файл: `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` |Идентификатор целевого потока|
| `expires` |Временная метка истечения срока действия токена|
| `channel_info` |Потоковые метаданные (on_demand, прокси, pid)|
| `user_info` |Разрешения пользователя (max_connections, is_restreamer)|
| `country_code` |GeoIP код страны|
| `video_codec` |Запрашиваемый видеокодек|
Утверждение:
1. Считайте маркер с `Encryption::readToken()` в поле `live_streaming_pass`.
2. Проверьте истечение срока действия: `$rTokenData['expires'] < time() - $rServers[SERVER_ID]['time_offset']`.
3. Возвращает проанализированные данные токена или вызывает ошибку.
### Формат токена
Токены потоковой связи создаются с помощью `Encryption::mintToken()` и считываются с помощью `Encryption::readToken()`; ничто другое не вызывает устаревший `encrypt()`/`decrypt()` для токена (`StreamTokenCallSitesTest` применяет его принудительно).
| `secure_stream_tokens` |Отчеканенные токены|Устаревшие токены читаются следующим образом|
| --- | --- | --- |
|`1` (по умолчанию при новой установке)|Запечатано: AES-256-GCM, случайный номер, `base64url(nonce ‖ ciphertext ‖ tag)`|Только в том случае, если токен содержит учетные данные, которые снова проверяются по базе данных|
| `0` |Устаревший AES-CBC|Везде|
Устаревшим форматом является AES-CBC с фиксированным значением IV и без MAC: измененный токен расшифровывается в измененные байты, а ошибка заполнения отличается от неверных учетных данных, которых достаточно для считывания или подделки токена. Запечатанные токены не могут быть прочитаны или изменены без ключа, и они сохраняют тот же алфавит, что и URL-адреса, поэтому маршрут или схема не меняются.
Где устаревшие токены все еще считываются при включенной настройке и почему:
- `auth.php` `/play/` ссылки, `rtmp.php` токены и `probe.php` `/play/` ссылки содержат имя пользователя и пароль, которые при повторном просмотре будут найдены, поэтому подделка ничего не даст. Сохраненные плейлисты и ссылки на портал имеют старый формат. Каждый токен, который не удается прочитать `auth.php`, засчитывается по адресу, указанному в `BruteforceGuard`, что останавливает чтение старого токена из-за сообщений об ошибках.
- Все, чьему содержимому доверяют в том виде, в каком оно есть, — JSON-файлы live/vod/timeshift (`user_info`, `channel_info`), HLS сегменты и ключевые токены, токены миниатюр и субтитров, токены администратора плеера `uitoken`, URL-адрес прокси-сервера веб-плеера и токен подтверждения портала MAG — отказывается от устаревшего формата.
Серверы более старой версии не могут считывать запечатанные токены. Поэтому при переносе `021_add_secure_stream_tokens.sql` этот параметр отключается на панели, на которой есть другие серверы; включите его в **Настройки → Токены потока, защищенные от несанкционированного доступа**, как только каждый сервер запустит эту версию.
Заголовки ответов задаются через `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 hints)
```
---
## Потоковая доставка
### Жить (live.php)
Основная конечная точка доставки (~650 строк):
1. Расшифруйте токен с помощью `StreamAuthMiddleware::decryptToken()`.
2. Разрешить использование сервера/прокси-сервера: `StreamAuth::checkAccess()` + `ProxySelector::availableProxy()`.
3. Установите ограничения на подключение: `StreamAuth::validateConnections()`.
4. Создайте запись о подключении: `ConnectionTracker::createConnection()`.
5. Hand delivery to the **`xc_fanout` daemon** (see below): PHP emits an
`X-Accel-Redirect` и завершает байтовый путь — nginx передает байты в потоковом режиме.
- **тс:** `X-Accel-Redirect: /xc_fanout/<id>?c=<uuid>&prebuffer=N` (nginx
перезаписывается в файл демона `/live/<id>`).
- **HLS:** список воспроизведения указывает на выделенные сегменты; `segment.php` показы в прямом эфире
сегментирует только через демон (`/xc_fanout_hls/<id>_<seq>`), иначе `404`.
6. При выходе: `ShutdownHandler::handle()` → закрыть запись о подключении.
### VOD (vod.php)
Тот же процесс аутентификации, что и в live. Считывается из `VOD_PATH` вместо `STREAMS_PATH`. Диапазоны байтов (поиск) разрешаются с помощью `Streaming\Delivery\HttpRange` (одиночные диапазоны RFC 7233, включая диапазоны суффиксов). Видеофильм с прямым подключением передается с помощью cURL, запрашивая у источника точно запрошенный диапазон.
### Временной сдвиг (timeshift.php)
Обслуживает архивированные сегменты (timeshift / catch-up) из пути к архиву. Запрос TS передает в потоковом режиме несколько файлов подряд; им сопоставляется диапазон байт (поиск) — файлы перед началом пропускаются, первый вводится с правильным смещением, и доставка прекращается в конце диапазона.
### Доставка демона — `xc_fanout`
Live client delivery (TS **and** HLS) is **daemon-only**: PHP authorizes the
средство просмотра, а затем полностью покидает путь к байтам, так что средство просмотра больше не закрепляет
PHP-FPM работник, отвечающий за жизнедеятельность потока.
- **Расходимся веером.** `xc_fanout` (встроенный демон Go) извлекает каждый источник **однажды** и
предоставляет его каждому пользователю через сокет unix с помощью встроенного в оперативную память сегментатора HLS.
PHP не соответствует байтовому пути для каждого зрителя: рабочий процесс чтения для каждого зрителя
цикл обслуживания и путь к клиенту на диске `generateHLS()` исчезли.
`AsyncFileOperations::awaitFileExists()` по-прежнему используется для запуска потока
ожидания и путь в байтах VOD/timeshift (см. таблицу производительности).
- **Кто кормит демона.** Поскольку демон является единственным путем к клиенту, каждый живой
продюсер должен включить его, иначе канал нельзя будет смотреть:
поток ffmpeg переходит в свой принимающий сокет (`buildLive()`; обратная петля
включая дочерних), создатели супервизора-демона делают то же самое, PHP
производители — сегментатор LLOD (`LlodCommand`) и ретранслятор с обратной связью
(`LoopbackCommand`) — протолкнуть через `Streaming\Fanout\IngestFeeder`, и
поток **отложенный** подается через `DelayCommand`, который перемещает каждый задержанный сегмент
по мере того, как он публикует его, в зависимости от продолжительности сегмента (его выходные данные с кодировщика являются
отложенный, поэтому тройник для него не используется). `IngestFeeder` буферизует то, что
не удалось отправить неблокирующую запись (короткая запись больше не разрывает пакеты),
выполняет повторную регистрацию и набор номера после перезапуска демона и содержит ключ HLS.
- **Две розетки.** Клиентский сокет (ориентированный на nginx) обслуживает `/live/<id>` и
`/hls/...`; управляющий сокет, предназначенный только для PHP, регистрирует источники
(`PUT /streams/<id>` / `/ingest/<id>`), отвечает на вопросы о статусе выхода в эфир
(`GET /streams/<id>`, `GET /probe/<id>`) и предоставляет доступ к телеметрии.
- **Телеметрия / согласование данных.** `fanout_sync` опросы `GET /rates` (для каждого uuid
КБИТ/с → `lines_divergence`) и сверяет `GET /connections` с данными
`lines_live` строк в обоих направлениях: строка, просмотрщик которой покинул демон, называется
закрыто (PHP не удается увидеть отключение при `X-Accel`), и демон-просмотрщик, чей
пропущенная строка — собранная, с истекшим сроком действия или запрещенная строка — удаляется по истечении 20 секунд
(`DELETE /connections/<uuid>`).
- **Удары и ограничения по подключению.** Строка TS viewer, обслуживаемая демоном, содержит `pid = 0`:
здесь нет рабочего, которого можно было бы убить. `ConnectionLimiter` / `ConnectionTracker::closeConnection()`
завершите его с помощью `ConnectionTracker::dropDaemonViewer()` — `FanoutClient::dropConnection()`
на этом узле, или сигнал `drop_con` о том, что узел зрителя превращается в
тот же вызов. Ограничитель никогда не отключает запрашивающее соединение сам по себе (это
идентифицируется по uuid, поскольку каждая строка демона имеет общий pid 0).
- **Вне эфира.** Если демон сообщает об отсутствии данных (`has_data=false` / устаревшие), PHP
показывает страницу "не в эфире" вместо того, чтобы позволить зрителю зависнуть.
- **Сохранено на диске HLS** только для timeshift / миниатюр / `.analyse` /
дочерние элементы loopback / проверки запуска по требованию — не для доставки клиенту.
#### Управление потоковой передачей и встроенный ремультиплексор
При включенном **Контроль разветвленного энкодера** (`fanout_supervise`, миграция 018, по умолчанию включено),
прямая трансляция не получает PHP watchdog. `StreamProcess::startMonitor()` строит свои команды и раздает
их супервизору демона (`FanoutClient::supervise` → `PUT /monitor/<id>`), который запускает,
отслеживает и перезапускает их — отработка отказа, приоритетное резервное копирование, принудительный источник, сбой вывода, потеря звука,
включая снижение частоты кадров и перезапуск по расписанию. PHP продолжает создавать каждую команду и выполнять все
запись в базу данных; демон выполняет то, что ему передают.
- **Передача** — `StreamProcess::superviseStream()` сначала запрашивает у демона
(`GET /monitors/state`: доступно, `accepting`), создает спецификацию
(`StreamProcess::buildSupervisorSpec()`: одна команда для каждого источника, политики и работоспособности, сопоставленных с
соблюдены настройки `MonitorCommand`), записывает pid демона как `monitor_pid`, затем передает его
over. Without a restart a running encoder is **adopted**, not replaced; `cron:streams` moves
PHP-отслеживает потоки таким образом при следующем проходе.
- **Команды** — прямая трансляция, доступная только для копирования, запускает собственный ремуксор демона, `xc_fanout remux`,
построенный с помощью `StreamProcess::buildNativeLive()` рядом с `buildLive()`: он считывает исходный код изначально
(MPEG-TS по протоколу http(s), HLS с сегментами TS, udp/rtp) и записывает то же самое на диск HLS и демон
подача в виде строки ffmpeg `-f tee` без ffmpeg. Какие потоки соответствуют требованиям, это
`StreamProcess::nativeRefusal()` / `isNativeSource()`; `fanout_source_backend` решает:
`auto` = повторный мультиплексор с командой ffmpeg в виде `fallback_cmd` (используется при выходе из режима 3,
"не удается обслуживать этот источник"), `native` = только для ремультиплексора, `ffmpeg` = только для ffmpeg. Только панель
записывает команду ремультиплексора, когда демон узла объявляет об этом (`features` в
`GET /monitors/state`, `FanoutClient::supportsRemux()`) — более старый двоичный файл неправильно разобрал бы его.
- **Какой продюсер баллотировался и почему** — переданная команда записывается рядом с записью потока.
файлы, подобные пути к самопроизвольному запуску `<id>_.ffmpeg`: `<id>_.fanout` для ремультиплексора,
`<id>_.ffmpeg` для ffmpeg (в `auto` - оба). Когда включен собственный сервер и выполняется поток
ffmpeg в любом случае, причина `StreamProcess::nativeRefusal()` добавляется к `<id>.errors`
(`[panel] ffmpeg runs this stream: transcoding is enabled`), тот же файл, что и у производителя.
stderr переходит в. Соответствующий тип `streams_types.type_key` = `live`; `gen_timestamps` и
`read_native` намеренно не являются отказами (оба значения по умолчанию равны 1, поэтому они ничего не говорят о
канал — смотрите программу запуска демона).
- **Reconcile** — the daemon cannot write the database, so `StreamProcess::reconcileSupervised()`
копирует его состояние в `streams_servers` (статус, pid, текущий источник, кодеки, разрешение,
измеренный битрейт): каждый проход `cron:streams` и каждые 5 секунд от демона `signals`. A
контролируемый поток, строка которого пропущена или помечена как остановленная, освобождается. Кодеки и размер изображения
записываются в `stream_info` JSON, а также в плоские столбцы — этот JSON - это то, что
отображается список потоков, из которого адаптивный основной плейлист извлекает `BANDWIDTH`/`RESOLUTION` и
где `stream/auth.php` считывает видеокодек зрителя, и контролируемый поток никогда не запускается
ffprobe чтобы заполнить его.
- **Остановка** — `StreamProcess::stopStream()` освобождает первый (`DELETE /monitor/<id>`, который убивает
продюсер); сначала супервизор перезапускает процесс, убивая продюсера.
- **Возврат к PHP** — демон, который не работает или не принимает запросы, и поток указывает, что он не принимает
берем (задержка, созданные каналы, `yt-dlp` исходники платформы), запускаем `MonitorCommand`, как и раньше;
`MonitorCommand` отключается для потока, который контролирует демон.
- **"За этим следят?"** — для контролируемого потока `monitor_pid` - это pid демона, поэтому вызывающие абоненты используют
`StreamProcess::isWatched()` (PHP монитор активен или находится под наблюдением), а не
`ProcessManager::isMonitorAlive()` один.
Runbook на стороне демона - включение, проверка, откат кодов завершения работы ремультиплексора — это
`docs/en/09-encoder-supervision.md` в репозитории `XC_VM_Fanout`.
#### Наложение отправленного сообщения
Действие администратора "Отправить сообщение" приводит к появлению текстового баннера на видео, которое просматривает **один** зритель.
PHP отправляет его в сокет управления демоном
(`FanoutClient::sendSignal` → `POST /signal/<uuid>`), и демон применяет
ffmpeg `drawtext` наложение на следующий HLS сегмент этого просмотра (или короткий фрагмент ~5 секунд).
окно), однократный запуск, максимальное усилие - сигнал никогда не прерывает воспроизведение. Демон должен
быть запущенным с помощью ffmpeg, который на самом деле имеет фильтр `drawtext`, так что
`service` программа запуска выбирает сборку с поддержкой drawtext.
---
## Управление подключениями
### Средство отслеживания подключений
Управляет текущим состоянием соединения. Серверная часть выбрана с помощью `$rSettings['redis_handler']`:
**Redis (preferred for scale):**
- Соединения, хранящиеся в отсортированных наборах:
- `LINE#{identity}` — подключения для пользователя
- `STREAM#{stream_id}` — соединения для потока
- `SERVER#{server_id}` — соединения на сервере
**MySQL (fallback):**
- Таблица: `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()
```
### Ограничитель подключения
Файл: `src/Streaming/Protection/ConnectionLimiter.php`
Устанавливает ограничения на подключение для каждого пользователя при превышении значения `max_connections`:
|Приоритет|Критерий|Действие|
| --- | --- | --- |
|2|Тот же IP + тот же пользовательский агент|Убей первым|
|1|Тот же IP-адрес (любой UA)|Убей следующего|
|0|Какая-либо связь|Убить в качестве запасного варианта|
Настройки:
- `disallow_2nd_ip_con` — принудительно использовать один IP-адрес для каждого пользователя
- `ip_subnet_match` — соответствует подсети /24 вместо точного IP-адреса
- `restrict_same_ip` — возвращает ошибку при несоответствии IP-адресов вместо уничтожения
### Устройство для выключения
Файл: `src/Streaming/Lifecycle/ShutdownHandler.php`
Зарегистрирован с помощью `register_shutdown_function()`. При завершении процесса PHP:
1. Закройте запись о соединении в `lines_live` или Redis.
2. Удалите tmp-файлы со значением `CONS_TMP_PATH . $uuid`.
3. Удалите поток по требованию из очереди, если это применимо.
---
## балансировка нагрузки
### Выбор сервера (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. Примените маршрутизацию через интернет-провайдера (если `enable_isp == 1`): та же логика, что и GeoIP.
5. Верните сервер с наименьшей пропускной способностью из группы с наивысшим приоритетом.
### Выбор прокси-сервера (ProxySelector::Доступный прокси)
Файл: `src/Streaming/Balancer/ProxySelector.php`
```php
public static function availableProxy($rProxies, $rCountryCode, $rUserISP = ''): int|null
```
Тот же алгоритм, что и `StreamAuth::checkAccess()`, но примененный к списку прокси-серверов.
---
## Ограничение скорости и защита от наводнений
Три слоя:
### 1. nginx (уровень подключения)
```nginx
limit_req_zone $binary_remote_addr zone=one:30m rate=20r/s;
limit_req zone=one burst=8;
```
20 запросов в секунду на IP-адрес с пакетом из 8 запросов. 30-минутное скользящее окно.
### 2. StreamingRequestBootstrap (IP-блокировка)
```php
if (file_exists(FLOOD_TMP_PATH . 'block_' . $rIP)) {
http_response_code(403);
exit();
}
```
IP-блокировка на основе файлов. Файлы блоков создаются с помощью вышестоящей логики обнаружения наводнений.
### 3. Ограничитель подключений (для каждого пользователя)
Применяется после проверки токена. Ограничивает одновременные потоки для каждого пользователя на основе `max_connections`, закрывая сначала самые старые соединения (более старые соединения самого запрашивающего устройства перед другими). Средства просмотра, обслуживаемые демонами, отключаются с помощью демона — см. [Daemon delivery](#daemon-delivery-xc_fanout).
### 4. Серверы, работающие только через прокси
Сервер с параметром `enable_proxy` принимает только запросы, поступающие через один из его прокси-серверов. `auth.php` проверяет одноранговый узел TCP nginx saw — `XC_PEER_ADDR`, установленный на `$realip_remote_addr` в расположении потока `nginx.conf`, а не заголовок запроса, которым управляет клиент.
---
## HLS Шифрование
Клиент HLS обслуживается демоном `xc_fanout` (см. [Доставка демоном](#daemon-delivery-xc_fanout)), поэтому происходит шифрование **сторона демона**:
1. `StreamProcess` записывает ключ AES-128 потока/IV в `content/streams/<id>_.key` / `_.iv` — перед тем, как он порождает производителя PHP, который регистрируется у демона через несколько мгновений после запуска.
2. При регистрации приема (`FanoutClient::registerIngest`), когда включено `encrypt_hls`, ключ/IV передается демону, который шифрует HLS сегмента, которые он обслуживает. Каждый производитель передает их — ffmpeg потоков (включая дочерние циклы), контролируемые потоки и PHP производителей через `IngestFeeder::forStream()` — потому что плейлист всегда объявляет ключ: демон, запущенный без него, подавал простые сегменты, которые ни один игрок не смог бы расшифровать.
3. `HLSGenerator::tokenizeDaemonPlaylist()` переписывает URL-адреса сегментов плейлиста демона в ссылки с авторизацией для каждого сегмента `/hls/<token>`, которые `segment.php` проксируются от демона, и добавляет строку `#EXT-X-KEY`.
4. Ключ AES доставляется игрокам с помощью `key.php` (`src/Public/stream/key.php`) с использованием того же механизма токенов.
Значение `#EXT-X-MEDIA-SEQUENCE` в плейлисте live повторно привязывается к значению `HlsSequence`, поэтому он никогда не возвращается назад при переходе из прямого эфира в прямой эфир без изменения нумерации воспроизводимого потока (его состояние сохраняется в виде `tmp/signals/hlsseq_<id>`, поэтому оно сохраняется после перезапуска потока).
---
## Представление
Ключевые проектные решения, касающиеся пропускной способности и задержки:
|Особенность|Механизм|
| --- | --- |
|Трансляция-онлайн-ожидание|`AsyncFileOperations::awaitFileExists()` ожидает `_.pid`/`_.monitor`/первого сегмента при появлении потока (и в пути длиной VOD/timeshift байт). Оперативная доставка клиента осуществляется демоном, а не считывается с помощью PHP.|
|Нулевой режим работы процессора|`time_nanosleep()` через `AsyncFileOperations::efficientSleep()`|
|nginx буферизация|128 буферов по 32 КБАЙТ на запрос|
|Объединение подключений в пул|Redis (предпочтительно) или постоянный MySQL|
|Чтение только из кэша|Настройки и пользовательские данные считываются из файлового кэша без запросов к базе данных|
|Досрочный выход (VOD/timeshift)|Эти байтовые циклы опрашивают `connection_status()` для остановки при отключении клиента. В Live нет байтового цикла для каждого пользователя PHP (обслуживается демоном).|
|Обновление настроек|Каждые 5 минут (300 секунд) для отслеживания изменений конфигурации без перезагрузки|
---
## Пути к файловой системе
```text
STREAMS_PATH = /home/xc_vm/content/streams/
VOD_PATH = /home/xc_vm/content/vod/
ARCHIVE_PATH = /home/xc_vm/content/archive/
VIDEO_PATH = /home/xc_vm/content/video/
CONS_TMP_PATH = /home/xc_vm/tmp/opened_cons/
CACHE_TMP_PATH = /home/xc_vm/tmp/cache/
FLOOD_TMP_PATH = /home/xc_vm/tmp/flood/
SIGNALS_TMP_PATH = /home/xc_vm/tmp/signals/
SIGNALS_PATH = /home/xc_vm/signals/
```
---
## Диагностика и оснастка
Автономный инструмент проверки целостности потока (`tools/stream-check/stream_check.py`) теперь доступен на отдельной странице - см. [Диагностика и инструменты для потоковой передачи](streaming-diagnostics.md).
---
## Обоснование проекта (ADR)
Почему оперативная доставка переместилась с tmpfs на PHP байтовый путь — решения, стоящие за текущим
`xc_fanout` архитектура — записывается в отчетах об архитектурных решениях (repo-внутренние примечания,
не является частью опубликованного сайта):
- [ADR 0001 — Tmpfs-free streaming](https://github.com/Vateron-Media/XC_VM/blob/main/docs/adr/0001-tmpfs-free-streaming.md) — PHP out of the byte path, native fan-out, in-RAM HLS.
- [ADR 0002 — `xc_fanout` daemon](https://github.com/Vateron-Media/XC_VM/blob/main/docs/adr/0002-xc-fanout-daemon.md) — the native live fan-out daemon.
- [ADR 0003 — Полное отключение демона](https://github.com/Vateron-Media/XC_VM/blob/main/docs/adr/0003-full-daemon-cutover.md) — отмена устаревшего байтового пути для live.
---
## Связанные файлы
|Файл|Цель|
| --- | --- |
| `src/Streaming/StreamingBootstrap.php` |основной загрузчик потоковой передачи|
| `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/StreamRedirector.php` |доступность потока и маршрутизация сервера|
| `src/Streaming/AsyncFileOperations.php` |неблокирующие утилиты для файловой системы|
| `src/Streaming/Lifecycle/ShutdownHandler.php` |очистка соединения при выходе|
| `src/Domain/Stream/ConnectionTracker.php` |состояние соединения в Redis/MySQL|
| `src/Domain/Stream/StreamProcess.php` |формирование команды (`buildLive` / `buildNativeLive`), передача контроля и согласование|
| `src/Streaming/Fanout/FanoutClient.php` |API управления демонами (прием, контроль, принудительный источник)|
| `src/Core/Init/LegacyInitializer.php` |настройка глобальной переменной для потоковой передачи|
| `tools/stream-check/stream_check.py` |проверка целостности очереди + пакет плейлистов + панель мониторинга живого буфера + графический редактор SVG|