# Подсистема потоковой передачи Подсистема потоковой передачи обрабатывает доставку в реальном времени, 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/?c=&prebuffer=N` (nginx перезаписывается в файл демона `/live/`). - **HLS:** список воспроизведения указывает на выделенные сегменты; `segment.php` показы в прямом эфире сегментирует только через демон (`/xc_fanout_hls/_`), иначе `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/` и `/hls/...`; управляющий сокет, предназначенный только для PHP, регистрирует источники (`PUT /streams/` / `/ingest/`), отвечает на вопросы о статусе выхода в эфир (`GET /streams/`, `GET /probe/`) и предоставляет доступ к телеметрии. - **Телеметрия / согласование данных.** `fanout_sync` опросы `GET /rates` (для каждого uuid КБИТ/с → `lines_divergence`) и сверяет `GET /connections` с данными `lines_live` строк в обоих направлениях: строка, просмотрщик которой покинул демон, называется закрыто (PHP не удается увидеть отключение при `X-Accel`), и демон-просмотрщик, чей пропущенная строка — собранная, с истекшим сроком действия или запрещенная строка — удаляется по истечении 20 секунд (`DELETE /connections/`). - **Удары и ограничения по подключению.** Строка 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/`), который запускает, отслеживает и перезапускает их — отработка отказа, приоритетное резервное копирование, принудительный источник, сбой вывода, потеря звука, включая снижение частоты кадров и перезапуск по расписанию. 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()`) — более старый двоичный файл неправильно разобрал бы его. - **Какой продюсер баллотировался и почему** — переданная команда записывается рядом с записью потока. файлы, подобные пути к самопроизвольному запуску `_.ffmpeg`: `_.fanout` для ремультиплексора, `_.ffmpeg` для ffmpeg (в `auto` - оба). Когда включен собственный сервер и выполняется поток ffmpeg в любом случае, причина `StreamProcess::nativeRefusal()` добавляется к `.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/`, который убивает продюсер); сначала супервизор перезапускает процесс, убивая продюсера. - **Возврат к 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/`), и демон применяет 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/_.key` / `_.iv` — перед тем, как он порождает производителя PHP, который регистрируется у демона через несколько мгновений после запуска. 2. При регистрации приема (`FanoutClient::registerIngest`), когда включено `encrypt_hls`, ключ/IV передается демону, который шифрует HLS сегмента, которые он обслуживает. Каждый производитель передает их — ffmpeg потоков (включая дочерние циклы), контролируемые потоки и PHP производителей через `IngestFeeder::forStream()` — потому что плейлист всегда объявляет ключ: демон, запущенный без него, подавал простые сегменты, которые ни один игрок не смог бы расшифровать. 3. `HLSGenerator::tokenizeDaemonPlaylist()` переписывает URL-адреса сегментов плейлиста демона в ссылки с авторизацией для каждого сегмента `/hls/`, которые `segment.php` проксируются от демона, и добавляет строку `#EXT-X-KEY`. 4. Ключ AES доставляется игрокам с помощью `key.php` (`src/Public/stream/key.php`) с использованием того же механизма токенов. Значение `#EXT-X-MEDIA-SEQUENCE` в плейлисте live повторно привязывается к значению `HlsSequence`, поэтому он никогда не возвращается назад при переходе из прямого эфира в прямой эфир без изменения нумерации воспроизводимого потока (его состояние сохраняется в виде `tmp/signals/hlsseq_`, поэтому оно сохраняется после перезапуска потока). --- ## Представление Ключевые проектные решения, касающиеся пропускной способности и задержки: |Особенность|Механизм| | --- | --- | |Трансляция-онлайн-ожидание|`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|