2026-08-20 22:37:44 +03:00
# Подсистема потоковой передачи
2026-08-21 16:31:01 +03:00
Подсистема потоковой передачи обрабатывает доставку в реальном времени, VOD и timeshift.
2026-08-20 22:37:44 +03:00
Это быстрый путь (~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()
```
2026-08-27 18:07:43 +03:00
nginx переписывает все URL-адреса потоковой передачи на PHP точки входа в соответствии с `Public/stream/` :
2026-08-20 22:37:44 +03:00
|Шаблон URL-адреса|Точка входа|Цель|
| --- | --- | --- |
| `/auth/{token}` | `live.php` |Прямая трансляция|
| `/vauth/{token}` | `vod.php` |Доставка видео по запросу|
| `/tsauth/{token}` | `timeshift.php` |Архив/timeshift воспроизведение|
2026-08-21 16:31:01 +03:00
| `/hls/{token}` | `segment.php` |HLS сегментная доставка|
2026-08-20 22:37:44 +03:00
| `/key/{token}` | `key.php` |Ключ шифрования AES-128|
| `/subauth/{token}` | `subtitle.php` |Передача субтитров|
---
2026-09-16 22:04:48 +03:00
## Расположение каталогов
2026-08-20 22:37:44 +03:00
```
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
2026-08-27 18:07:43 +03:00
├── Fanout/
│ └── FanoutClient.php
2026-08-20 22:37:44 +03:00
├── Health/
│ └── ProcessChecker.php
├── Lifecycle/
│ └── ShutdownHandler.php
└── Protection/
└── ConnectionLimiter.php
2026-08-27 18:07:43 +03:00
src/Public/stream/
├── index.php # Entry router for the stream endpoints
2026-08-20 22:37:44 +03:00
├── 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
2026-08-27 18:07:43 +03:00
├── probe.php # Stream probe / off-air status
2026-08-20 22:37:44 +03:00
└── rtmp.php # RTMP publishing endpoint
```
---
## Конвейер начальной загрузки
### 1. StreamingRequestBootstrap::init()
Файл: `src/Infrastructure/Bootstrap/StreamingRequestBootstrap.php`
Действия в порядке:
1. Загружайте коды ошибок, обработчик, пути, конфигурацию, двоичные файлы.
2026-08-21 16:31:01 +03:00
2. Защита от наводнений (только HTTP): проверьте наличие `FLOOD_TMP_PATH . 'block_' . $rIP` .
2026-08-20 22:37:44 +03:00
3. Загрузите настройки из файлового кэша (`CACHE_TMP_PATH . 'settings'` ).
4. Проверка хоста (только HTTP): проверка на соответствие `allowed_domains` .
5. Инициализируйте регистратор.
6. Аварийно закрытый шлюз: возвращает 404, если настройки отсутствуют (кроме `/status` ).
2026-08-21 16:31:01 +03:00
7. Вызовите `StreamingBootstrap::bootstrap()` .
2026-08-20 22:37:44 +03:00
### 2. Потоковый загрузчик::bootstrap()
Файл: `src/Streaming/StreamingBootstrap.php`
```php
public static function bootstrap ( $rFilename , $rSettings )
```
Классифицирует конечную точку:
2026-08-27 18:07:43 +03:00
- **Конечные точки зондирования:** `probe` , `player_api` ( небольшая нагрузка)
2026-08-20 22:37:44 +03:00
- **Конечные точки по умолчанию:** `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']` .
2026-08-27 18:07:43 +03:00
> **Важный:** Путь к потоковой передаче считывается исключительно из файлового кэша. При обычной работе программа не запрашивает настройки в базе данных или запросы пользователей.
2026-08-20 22:37:44 +03:00
---
## Аутентификация по токену
Файл: `src/Streaming/Auth/StreamAuthMiddleware.php`
```php
StreamAuthMiddleware :: decryptToken ( $rToken , $rSettings , $rServers , $rIP ) : array
```
Содержимое токена:
|Поле|Описание|
| --- | --- |
| `username` |Имя пользователя строки|
2026-09-16 22:04:48 +03:00
| `password` |Строчный пароль|
2026-08-20 22:37:44 +03:00
| `stream_id` |Идентификатор целевого потока|
| `expires` |Временная метка истечения срока действия токена|
| `channel_info` |Потоковые метаданные (on_demand, прокси, pid)|
| `user_info` |Разрешения пользователя (max_connections, is_restreamer)|
| `country_code` |GeoIP код страны|
| `video_codec` |Запрашиваемый видеокодек|
Утверждение:
2026-09-16 22:04:48 +03:00
1. Считайте маркер с `Encryption::readToken()` в поле `live_streaming_pass` .
2026-08-21 16:31:01 +03:00
2. Проверьте истечение срока действия: `$rTokenData['expires'] < time() - $rServers[SERVER_ID]['time_offset']` .
2026-08-20 22:37:44 +03:00
3. Возвращает проанализированные данные токена или вызывает ошибку.
2026-09-16 22:04:48 +03:00
### Формат токена
Токены потоковой связи создаются с помощью `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` этот параметр отключается на панели, на которой есть другие серверы; включите его в **Настройки → Токены потока, защищенные от несанкционированного доступа** , как только каждый сервер запустит эту версию.
2026-08-20 22:37:44 +03:00
Заголовки ответов задаются через `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()` .
2026-08-21 16:31:01 +03:00
2. Разрешить использование сервера/прокси-сервера: `StreamAuth::checkAccess()` + `ProxySelector::availableProxy()` .
2026-08-20 22:37:44 +03:00
3. Установите ограничения на подключение: `StreamAuth::validateConnections()` .
4. Создайте запись о подключении: `ConnectionTracker::createConnection()` .
2026-08-21 16:31:01 +03:00
5. Hand delivery to the ** `xc_fanout` daemon** (see below): PHP emits an
2026-08-20 22:37:44 +03:00
`X-Accel-Redirect` и завершает байтовый путь — nginx передает байты в потоковом режиме.
2026-08-27 18:07:43 +03:00
- **тс:** `X-Accel-Redirect: /xc_fanout/<id>?c=<uuid>&prebuffer=N` (nginx
2026-08-20 22:37:44 +03:00
перезаписывается в файл демона `/live/<id>` ).
2026-08-27 18:07:43 +03:00
- **HLS:** список воспроизведения указывает на выделенные сегменты; `segment.php` показы в прямом эфире
2026-08-20 22:37:44 +03:00
сегментирует только через демон (`/xc_fanout_hls/<id>_<seq>` ), иначе `404` .
2026-08-21 16:31:01 +03:00
6. При выходе: `ShutdownHandler::handle()` → закрыть запись о подключении.
2026-08-20 22:37:44 +03:00
### VOD (vod.php)
2026-09-11 20:12:09 +03:00
Тот же процесс аутентификации, что и в live. Считывается из `VOD_PATH` вместо `STREAMS_PATH` . Диапазоны байтов (поиск) разрешаются с помощью `Streaming\Delivery\HttpRange` (одиночные диапазоны RFC 7233, включая диапазоны суффиксов). Видеофильм с прямым подключением передается с помощью cURL, запрашивая у источника точно запрошенный диапазон.
2026-08-20 22:37:44 +03:00
### Временной сдвиг (timeshift.php)
2026-09-11 20:12:09 +03:00
Обслуживает архивированные сегменты (timeshift / catch-up) из пути к архиву. Запрос TS передает в потоковом режиме несколько файлов подряд; им сопоставляется диапазон байт (поиск) — файлы перед началом пропускаются, первый вводится с правильным смещением, и доставка прекращается в конце диапазона.
2026-08-20 22:37:44 +03:00
2026-08-21 16:31:01 +03:00
### Доставка демона — `xc_fanout`
2026-08-20 22:37:44 +03:00
2026-08-21 16:31:01 +03:00
Live client delivery (TS **and** HLS) is **daemon-only** : PHP authorizes the
2026-09-16 22:04:48 +03:00
средство просмотра, а затем полностью покидает путь к байтам, так что средство просмотра больше не закрепляет
2026-08-21 16:31:01 +03:00
PHP-FPM работник, отвечающий за жизнедеятельность потока.
2026-08-20 22:37:44 +03:00
2026-08-27 18:07:43 +03:00
- **Расходимся веером.** `xc_fanout` (встроенный демон Go) извлекает каждый источник **однажды** и
2026-08-21 16:31:01 +03:00
предоставляет его каждому пользователю через сокет unix с помощью встроенного в оперативную память сегментатора HLS.
2026-09-11 20:12:09 +03:00
PHP не соответствует байтовому пути для каждого зрителя: рабочий процесс чтения для каждого зрителя
цикл обслуживания и путь к клиенту на диске `generateHLS()` исчезли.
`AsyncFileOperations::awaitFileExists()` по-прежнему используется для запуска потока
ожидания и путь в байтах VOD/timeshift (см. таблицу производительности).
- **Кто кормит демона.** Поскольку демон является единственным путем к клиенту, каждый живой
продюсер должен включить его, иначе канал нельзя будет смотреть:
поток ffmpeg переходит в свой принимающий сокет (`buildLive()` ; обратная петля
включая дочерних), создатели супервизора-демона делают то же самое, PHP
производители — сегментатор LLOD (`LlodCommand` ) и ретранслятор с обратной связью
(`LoopbackCommand` ) — протолкнуть через `Streaming\Fanout\IngestFeeder` , и
поток **отложенный** подается через `DelayCommand` , который перемещает каждый задержанный сегмент
по мере того, как он публикует его, в зависимости от продолжительности сегмента (его выходные данные с кодировщика являются
отложенный, поэтому тройник для него не используется). `IngestFeeder` буферизует то, что
не удалось отправить неблокирующую запись (короткая запись больше не разрывает пакеты),
выполняет повторную регистрацию и набор номера после перезапуска демона и содержит ключ HLS.
2026-08-27 18:07:43 +03:00
- **Две розетки.** Клиентский сокет (ориентированный на nginx) обслуживает `/live/<id>` и
2026-08-21 16:31:01 +03:00
`/hls/...` ; управляющий сокет, предназначенный только для PHP, регистрирует источники
2026-08-20 22:37:44 +03:00
(`PUT /streams/<id>` / `/ingest/<id>` ), отвечает на вопросы о статусе выхода в эфир
(`GET /streams/<id>` , `GET /probe/<id>` ) и предоставляет доступ к телеметрии.
2026-08-27 18:07:43 +03:00
- **Телеметрия / согласование данных.** `fanout_sync` опросы `GET /rates` (для каждого uuid
2026-09-11 20:12:09 +03:00
КБИТ/с → `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).
2026-08-27 18:07:43 +03:00
- **Вне эфира.** Если демон сообщает об отсутствии данных (`has_data=false` / устаревшие), PHP
2026-08-20 22:37:44 +03:00
показывает страницу "не в эфире" вместо того, чтобы позволить зрителю зависнуть.
- **Сохранено на диске HLS** только для timeshift / миниатюр / `.analyse` /
2026-09-11 20:12:09 +03:00
дочерние элементы 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 - это то, что
2026-09-16 22:04:48 +03:00
отображается список потоков, из которого адаптивный основной плейлист извлекает `BANDWIDTH` /`RESOLUTION` и
2026-09-11 20:12:09 +03:00
где `stream/auth.php` считывает видеокодек зрителя, и контролируемый поток никогда не запускается
ffprobe чтобы заполнить его.
- **Остановка** — `StreamProcess::stopStream()` освобождает первый (`DELETE /monitor/<id>` , который убивает
продюсер); сначала супервизор перезапускает процесс, убивая продюсера.
2026-09-16 22:04:48 +03:00
- **Возврат к PHP** — демон, который не работает или не принимает запросы, и поток указывает, что он не принимает
2026-09-11 20:12:09 +03:00
берем (задержка, созданные каналы, `yt-dlp` исходники платформы), запускаем `MonitorCommand` , как и раньше;
`MonitorCommand` отключается для потока, который контролирует демон.
- **"За этим следят?"** — для контролируемого потока `monitor_pid` - это pid демона, поэтому вызывающие абоненты используют
`StreamProcess::isWatched()` (PHP монитор активен или находится под наблюдением), а не
`ProcessManager::isMonitorAlive()` один.
Runbook на стороне демона - включение, проверка, откат кодов завершения работы ремультиплексора — это
`docs/en/09-encoder-supervision.md` в репозитории `XC_VM_Fanout` .
2026-08-20 22:37:44 +03:00
#### Наложение отправленного сообщения
2026-09-10 17:31:22 +03:00
Действие администратора "Отправить сообщение" приводит к появлению текстового баннера на видео, которое просматривает **один** зритель.
2026-08-20 22:37:44 +03:00
PHP отправляет его в сокет управления демоном
(`FanoutClient::sendSignal` → `POST /signal/<uuid>` ), и демон применяет
2026-09-16 22:04:48 +03:00
ffmpeg `drawtext` наложение на следующий HLS сегмент этого просмотра (или короткий фрагмент ~5 секунд).
2026-08-20 22:37:44 +03:00
окно), однократный запуск, максимальное усилие - сигнал никогда не прерывает воспроизведение. Демон должен
2026-08-21 16:31:01 +03:00
быть запущенным с помощью ffmpeg, который на самом деле имеет фильтр `drawtext` , так что
`service` программа запуска выбирает сборку с поддержкой drawtext.
2026-08-20 22:37:44 +03:00
---
## Управление подключениями
### Средство отслеживания подключений
Управляет текущим состоянием соединения. Серверная часть выбрана с помощью `$rSettings['redis_handler']` :
2026-08-27 18:07:43 +03:00
**Redis (preferred for scale):**
2026-08-20 22:37:44 +03:00
- Соединения, хранящиеся в отсортированных наборах:
- `LINE#{identity}` — подключения для пользователя
2026-08-21 16:31:01 +03:00
- `STREAM#{stream_id}` — соединения для потока
2026-08-20 22:37:44 +03:00
- `SERVER#{server_id}` — соединения на сервере
2026-08-27 18:07:43 +03:00
**MySQL (fallback):**
2026-08-20 22:37:44 +03:00
- Таблица: `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`
2026-08-21 16:31:01 +03:00
Зарегистрирован с помощью `register_shutdown_function()` . При завершении процесса PHP:
2026-08-20 22:37:44 +03:00
2026-08-21 16:31:01 +03:00
1. Закройте запись о соединении в `lines_live` или Redis.
2. Удалите tmp-файлы со значением `CONS_TMP_PATH . $uuid` .
2026-08-20 22:37:44 +03:00
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` ):
- Точное соответствие стране → выберите немедленно.
2026-08-21 16:31:01 +03:00
- `geoip_type == 'strict'` → исключить несоответствия.
2026-08-20 22:37:44 +03:00
- В противном случае → присвоить приоритетный вес.
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. Ограничитель подключений (для каждого пользователя)
2026-09-11 20:12:09 +03:00
Применяется после проверки токена. Ограничивает одновременные потоки для каждого пользователя на основе `max_connections` , закрывая сначала самые старые соединения (более старые соединения самого запрашивающего устройства перед другими). Средства просмотра, обслуживаемые демонами, отключаются с помощью демона — см. [Daemon delivery ](#daemon-delivery-xc_fanout ).
### 4. Серверы, работающие только через прокси
2026-09-16 22:04:48 +03:00
Сервер с параметром `enable_proxy` принимает только запросы, поступающие через один из его прокси-серверов. `auth.php` проверяет одноранговый узел TCP nginx saw — `XC_PEER_ADDR` , установленный на `$realip_remote_addr` в расположении потока `nginx.conf` , а не заголовок запроса, которым управляет клиент.
2026-08-20 22:37:44 +03:00
---
## HLS Шифрование
2026-08-27 18:07:43 +03:00
Клиент HLS обслуживается демоном `xc_fanout` (см. [Доставка демоном ](#daemon-delivery-xc_fanout )), поэтому происходит шифрование **сторона демона** :
2026-08-20 22:37:44 +03:00
2026-09-16 22:04:48 +03:00
1. `StreamProcess` записывает ключ AES-128 потока/IV в `content/streams/<id>_.key` / `_.iv` — перед тем, как он порождает производителя PHP, который регистрируется у демона через несколько мгновений после запуска.
2026-09-11 20:12:09 +03:00
2. При регистрации приема (`FanoutClient::registerIngest` ), когда включено `encrypt_hls` , ключ/IV передается демону, который шифрует HLS сегмента, которые он обслуживает. Каждый производитель передает их — ffmpeg потоков (включая дочерние циклы), контролируемые потоки и PHP производителей через `IngestFeeder::forStream()` — потому что плейлист всегда объявляет ключ: демон, запущенный без него, подавал простые сегменты, которые ни один игрок не смог бы расшифровать.
3. `HLSGenerator::tokenizeDaemonPlaylist()` переписывает URL-адреса сегментов плейлиста демона в ссылки с авторизацией для каждого сегмента `/hls/<token>` , которые `segment.php` проксируются от демона, и добавляет строку `#EXT-X-KEY` .
2026-08-27 18:07:43 +03:00
4. Ключ AES доставляется игрокам с помощью `key.php` (`src/Public/stream/key.php` ) с использованием того же механизма токенов.
2026-08-20 22:37:44 +03:00
2026-09-11 20:12:09 +03:00
Значение `#EXT-X-MEDIA-SEQUENCE` в плейлисте live повторно привязывается к значению `HlsSequence` , поэтому он никогда не возвращается назад при переходе из прямого эфира в прямой эфир без изменения нумерации воспроизводимого потока (его состояние сохраняется в виде `tmp/signals/hlsseq_<id>` , поэтому оно сохраняется после перезапуска потока).
2026-08-20 22:37:44 +03:00
---
## Представление
Ключевые проектные решения, касающиеся пропускной способности и задержки:
|Особенность|Механизм|
| --- | --- |
2026-08-27 18:07:43 +03:00
|Трансляция-онлайн-ожидание|`AsyncFileOperations::awaitFileExists()` ожидает `_.pid` /`_.monitor` /первого сегмента при появлении потока (и в пути длиной VOD/timeshift байт). Оперативная доставка клиента осуществляется демоном, а не считывается с помощью PHP.|
2026-08-20 22:37:44 +03:00
|Нулевой режим работы процессора|`time_nanosleep()` через `AsyncFileOperations::efficientSleep()` |
|nginx буферизация|128 буферов по 32 КБАЙТ на запрос|
|Объединение подключений в пул|Redis (предпочтительно) или постоянный MySQL|
|Чтение только из кэша|Настройки и пользовательские данные считываются из файлового кэша без запросов к базе данных|
2026-08-27 18:07:43 +03:00
|Досрочный выход (VOD/timeshift)|Эти байтовые циклы опрашивают `connection_status()` для остановки при отключении клиента. В Live нет байтового цикла для каждого пользователя PHP (обслуживается демоном).|
2026-09-16 22:04:48 +03:00
|Обновление настроек|Каждые 5 минут (300 секунд) для отслеживания изменений конфигурации без перезагрузки|
2026-08-20 22:37:44 +03:00
---
## Пути к файловой системе
```text
2026-08-27 18:07:43 +03:00
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/
2026-08-20 22:37:44 +03:00
CACHE_TMP_PATH = /home/xc_vm/tmp/cache/
FLOOD_TMP_PATH = /home/xc_vm/tmp/flood/
2026-08-27 18:07:43 +03:00
SIGNALS_TMP_PATH = /home/xc_vm/tmp/signals/
SIGNALS_PATH = /home/xc_vm/signals/
2026-08-20 22:37:44 +03:00
```
---
## Диагностика и оснастка
2026-09-10 17:31:22 +03:00
Автономный инструмент проверки целостности потока (`tools/stream-check/stream_check.py` ) теперь доступен на отдельной странице - см. [Диагностика и инструменты для потоковой передачи ](streaming-diagnostics.md ).
2026-08-20 22:37:44 +03:00
2026-08-27 18:07:43 +03:00
---
2026-08-20 22:37:44 +03:00
2026-08-27 18:07:43 +03:00
## Обоснование проекта (ADR)
2026-08-20 22:37:44 +03:00
2026-08-27 18:07:43 +03:00
Почему оперативная доставка переместилась с tmpfs на PHP байтовый путь — решения, стоящие за текущим
`xc_fanout` архитектура — записывается в отчетах об архитектурных решениях (repo-внутренние примечания,
не является частью опубликованного сайта):
2026-08-20 22:37:44 +03:00
2026-08-27 18:07:43 +03:00
- [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.
2026-09-16 22:04:48 +03:00
- [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.
2026-08-27 18:07:43 +03:00
- [ADR 0003 — Полное отключение демона ](https://github.com/Vateron-Media/XC_VM/blob/main/docs/adr/0003-full-daemon-cutover.md ) — отмена устаревшего байтового пути для live.
2026-08-20 22:37:44 +03:00
---
## Связанные файлы
|Файл|Цель|
| --- | --- |
| `src/Streaming/StreamingBootstrap.php` |основной загрузчик потоковой передачи|
| `src/Infrastructure/Bootstrap/StreamingRequestBootstrap.php` |Инициализация на уровне HTTP|
| `src/Streaming/Auth/StreamAuth.php` |выбор сервера и проверка подключения|
2026-09-16 22:04:48 +03:00
| `src/Streaming/Auth/StreamAuthMiddleware.php` |расшифровка токена и заголовки ответов|
2026-08-20 22:37:44 +03:00
| `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|
2026-09-11 20:12:09 +03:00
| `src/Domain/Stream/StreamProcess.php` |формирование команды (`buildLive` / `buildNativeLive` ), передача контроля и согласование|
| `src/Streaming/Fanout/FanoutClient.php` |API управления демонами (прием, контроль, принудительный источник)|
2026-08-20 22:37:44 +03:00
| `src/Core/Init/LegacyInitializer.php` |настройка глобальной переменной для потоковой передачи|
2026-09-10 17:31:22 +03:00
| `tools/stream-check/stream_check.py` |проверка целостности очереди + пакет плейлистов + панель мониторинга живого буфера + графический редактор SVG|