Сетевой медиа-плеер из двух частей:
- Prism.Host — сервер (ASP.NET Core). Сканирует папку с видеофайлами (например,
двухчасовой
.mkv) и отдаёт только HTTP API: данные библиотеки и потоковое видео. Если файл уже в браузерном формате — стримит напрямую; иначе, при наличии нужного кодека в системе, обрабатывает через ffmpeg и выдаёт H264 или H265 по HLS. СамPrism.Host— библиотека; запускаемые обёртки над ней: Prism.Host.Console (обычный запуск, разработка) и Prism.Host.Service (служба Windows, ставится инсталляторомPrism.Host.Setup). - Prism.Client — клиент (Node.js + TypeScript + React + Vite). Минималистичный
веб-интерфейс: библиотека и плеер. У клиента одна-единственная настройка — URL
сервера; всё остальное он получает от сервера запросом (
/api/info). - Плагины (
Prism.Abstractions+Prism.Plugins.*) — опциональные модули, подключаемые списком вappsettings. Ядро корректно работает и без них. Метаданные библиотеки (названия фильмов, привязка эпизодов к сериалу) реализованы плагиномPrism.Plugins.Library— см. раздел про плагины.
Prism.Client (Vite/React) ──HTTP/CORS──> Prism.Host (ASP.NET Core / Kestrel)
├── GET /api/info данные о сервере (кодек, ffmpeg, …)
├── GET /api/media список библиотеки
├── GET /api/media/{id} одна запись (+ дорожки аудио/субтитров)
├── PUT/DELETE /api/media/{id}/metadata метаданные (плагин Prism.Plugins.Library)
├── GET /api/media/{id}/subtitle/N.vtt субтитры в WebVTT (текстовые дорожки)
├── GET /raw/{id} прямой стрим с Range (браузерные файлы)
├── GET /hls/{id}/playlist.m3u8?audio=N HLS-плейлист (VOD) для аудиодорожки N
└── GET /hls/{id}/segment/N.ts?audio=M сегмент H264/H265 на лету
Сервер не содержит UI — весь интерфейс в клиенте.
Для каждого файла сервер выбирает режим воспроизведения (Media/MediaLibrary.cs),
а клиент получает его в поле streamType:
Режим (streamType) |
Когда | Доставка |
|---|---|---|
direct |
mp4/webm с браузерным кодеком (h264/vp9/av1 + aac/opus…) | /raw с HTTP Range/перемоткой |
hls |
любой другой контейнер/кодек и у ffmpeg есть декодер | HLS, перекодирование в H264/H265 + AAC |
unsupported |
нужна перекодировка, но ffmpeg или исходный кодек недоступны | клиент показывает пояснение |
HLS-плейлист — это VOD-плейлист, рассчитанный заранее по длительности файла
(ffprobe), поэтому браузер знает всю шкалу времени и может перематывать в любую
точку.
Сегменты производят сессии (Media/HlsTranscoder.cs + Media/TranscodeSession.cs).
Сессия — это один процесс ffmpeg, который своим HLS-муксером (-f hls) нарезает поток
на сегменты по кадровым границам и пишет их во временную папку, ограничиваясь по
длительности (-t, по умолчанию 15 минут). Аудио внутри сессии кодируется единым
потоком, поэтому на стыках сегментов нет priming-зазоров и перекрытий (которые при
схеме «отдельный ffmpeg на каждый сегмент» дают регулярные щелчки). Таймстемпы
смещаются на глобальную позицию (-output_ts_offset), так что сегменты всех сессий
ложатся на единую шкалу времени.
Управление сессиями простое и без платформенных трюков:
- Границы. Внутри сессии звук бесшовный. Возможный микро-разрыв звука бывает
только на границе между сессиями — то есть примерно раз в
SessionMinutesминут при непрерывном просмотре. Чем длиннее сессия, тем реже разрывы (но больше расход CPU/диска на сессию). - Перемотка. Если для запрошенного сегмента нет подходящей сессии, запускается новая прямо с этой позиции (ключевые кадры форсируются ровно на сетке сегментов, поэтому новая сессия выравнивается по глобальной шкале). Разрыв возникает только там, куда пользователь и так перемотал.
- Без пауз и сигналов. Сессия ограничена по времени через
-t, поэтому ffmpeg не транскодирует весь фильм вперёд — он выдаёт свой диапазон и завершается. Старые сессии вытесняются по LRU (держим до 3 на файл). Это работает одинаково на macOS/Linux/Windows. - Временные сегменты лежат в
‹temp›/prism-hls/и удаляются при вытеснении сессии и при завершении приложения.
По умолчанию каждый процесс ffmpeg — один экземпляр на сессию. Чтобы он не
транскодировал весь 15-минутный диапазон на полной скорости (это грузит CPU и греет
ноутбук), включён пейсинг чтения: ffmpeg быстро выдаёт первые BufferBurstSeconds
секунд (быстрый старт/перемотка), а дальше читает вход со скоростью
MaxPlaybackRate×реального времени (-readrate + -readrate_initial_burst). Множитель
берётся с запасом (по умолчанию 2.0), чтобы воспроизведение на 2x не буксовало; общий
объём работы кодирования от этого не растёт. Плюс фоновый уборщик убивает сессии,
к которым SessionIdleSeconds секунд никто не обращался. На практике это снижает пик
CPU при перемотках в разы и опускает нагрузку до нуля, когда никто не смотрит.
Живая дебаг-панель — на клиенте по ссылке debug в шапке (или #/debug): число
сессий, суммарный %CPU и память ffmpeg, прогресс и метрики каждой сессии. Данные —
из GET /api/debug/sessions.
/api/media/{id} отдаёт списки audioTracks и subtitleTracks (язык, название,
кодек, каналы), клиент показывает селекторы в плеере.
- Аудио. Выбранная дорожка вшивается в HLS-поток (
?audio=N→ ffmpeg-map 0:a:N), поэтому это отдельная транскод-сессия. Переключение перезагружает поток, сохраняя позицию воспроизведения. - Субтитры. Извлекаются отдельно в WebVTT (
/api/media/{id}/subtitle/N.vtt) и подключаются как<track>— включаются/выключаются мгновенно, без пере-транскода и независимо от аудио. Поддерживаются текстовые субтитры (SRT/ASS/…); графические (PGS/VOBSUB/DVB) помеченыtextBased:falseи в списке недоступны.
Сервер расширяется плагинами — опциональными модулями. Ядро (стриминг, скан библиотеки) работает и без единого плагина.
- Контракт —
Prism.Abstractions:IPrismModule(регистрирует сервисы и эндпоинты) иIMediaMetaSource(подмешивает произвольные пары ключ-значение к записям/api/media). - Список активных плагинов — в
appsettings("Plugins": ["Prism.Plugins.Library"]). Каждый грузится изplugins/<имя>/<имя>.dll(папка рядом с приложением) в собственныйAssemblyLoadContext(зависимости плагина резолвятся из его папки; контракт — из хоста). Build плагина копирует его вPrism.Host.Console/plugins/автоматически; в установку службы плагины кладёт инсталлятор.
Хранит метаданные библиотеки — источник истины на сервере. Минимальный набор:
для фильма — название; для эпизода — сериал, номер сезона и эпизода. Отображаемое
название подмешивается в /api/media (поле title), без плагина там остаётся имя
файла.
-
СУБД задаётся конфигом (секция
Database), код про конкретную СУБД не знает:"Database": { "Provider": "sqlite", // "sqlite" | "postgres" "ConnectionString": "Data Source=data/prism.db" }
Для Postgres — своя строка подключения (можно ту же БД, что и у умного дома).
-
Схему ведёт мигратор ThinkingHome.Migrator (ключ версионирования
prism.library— своя история миграций в общей БД). Таблица плоскаяPrism_MediaMetadata(префиксPrism_, без схемы) — одинаково на SQLite и Postgres. -
Заполнение метаданных — вне ответственности сервера: он лишь отдаёт/принимает их через API. Пример:
# фильм curl -X PUT http://localhost:8080/api/media/<id>/metadata \ -H 'Content-Type: application/json' -d '{"kind":"movie","title":"Патриот"}' # эпизод → title в /api/media станет «Патриот · S01E02» curl -X PUT http://localhost:8080/api/media/<id>/metadata \ -H 'Content-Type: application/json' \ -d '{"kind":"episode","seriesTitle":"Патриот","season":1,"episode":2}' # снять метаданные (вернётся имя файла) curl -X DELETE http://localhost:8080/api/media/<id>/metadata
Собирается всё решением:
dotnet build Prism.sln(build плагина сам разворачивает его вplugins/). Данные и развёрнутые плагины (data/,plugins/) — в.gitignore.
- .NET 10 SDK (сервер)
- Node.js 18+ и npm (клиент)
- ffmpeg + ffprobe в
PATH(или укажитеPlayer:FfmpegPath/Player:FfprobePath, или положите в подпапкуffmpeg/рядом с приложением). Без них напрямую отдаются только уже браузерные файлы. Нужно для запуска из исходников — служба Windows получает ffmpeg в комплекте инсталлятора.
Вариант через пакетный менеджер (рекомендуется):
# winget (Windows 10/11)
winget install --id Gyan.FFmpeg -e
# либо Chocolatey
choco install ffmpeg
# либо Scoop
scoop install ffmpegВручную: скачайте сборку с https://www.gyan.dev/ffmpeg/builds/ (архив
ffmpeg-release-full), распакуйте, например, в C:\ffmpeg, и добавьте
C:\ffmpeg\bin в переменную среды PATH (Параметры → Система → О системе →
Дополнительные параметры системы → Переменные среды). Проверка: ffmpeg -version.
# Homebrew
brew install ffmpeg
# либо MacPorts
sudo port install ffmpeg# Debian / Ubuntu
sudo apt update && sudo apt install -y ffmpeg
# Fedora
sudo dnf install -y ffmpeg # потребуется репозиторий RPM Fusion
# Arch / Manjaro
sudo pacman -S ffmpeg
# openSUSE
sudo zypper install ffmpeg
# универсально (Snap)
sudo snap install ffmpegПроверить установку на любой ОС: ffmpeg -version и ffprobe -version.
Нужно запустить оба приложения.
# один раз собрать решение — заодно развернутся включённые плагины в plugins/
dotnet build Prism.sln
# положите видео в Prism.Host.Console/videos/ (или укажите папку через --media)
dotnet run --project Prism.Host.Console
# API слушает http://localhost:8080Параметры командной строки:
dotnet run --project Prism.Host.Console -- --file "/путь/к/movie.mkv" # отдать папку с этим файлом
dotnet run --project Prism.Host.Console -- --media "/путь/к/библиотеке" # отдать папку
dotnet run --project Prism.Host.Console -- --codec h265 # выдавать HEVC вместо H264
dotnet run --project Prism.Host.Console -- --ffmpeg /opt/homebrew/bin/ffmpegcd Prism.Client
npm install # один раз
npm run dev # дев-сервер на http://localhost:5173
# для продакшена: npm run build → статика в Prism.Client/distОткройте http://localhost:5173. Единственная настройка клиента — URL сервера
(шестерёнка в шапке, по умолчанию http://<хост>:8080, сохраняется в localStorage).
Остальные параметры клиент берёт с сервера через /api/info.
На Windows хост ставится как служба (Prism.Host.Service) — MSI собирает
проект Prism.Host.Setup (WiX 6 приезжает из NuGet при сборке, отдельно
ставить ничего не нужно). Проект входит в решение, но собирается только в
конфигурации Release:
dotnet build Prism.sln -c Release
# → Prism.Host.Setup/bin/Release/en-US/PrismHostSetup.msiСборка MSI сама публикует службу одним exe (self-contained — .NET на целевой
машине не нужен) и плагин (ProjectReference Publish="true" в .wixproj —
MSI пакует publish-выход, поэтому публикация и есть часть его сборки).
ffmpeg едет в комплекте: при сборке MSI скачивается пинованная
essentials-сборка с gyan.dev (кэшируется в obj/, версия задаётся в
.wixproj) и ставится в подпапку ffmpeg/ рядом со службой — хост ищет её
там в первую очередь, на целевой машине ffmpeg отдельно не нужен. Из-за этого
MSI весит ~115 МБ. Сборка ffmpeg — GPL; проект — MIT: текст и ссылки на
FFmpeg — в LICENSE и на шаге лицензии в установщике.
Инсталлятор ставит всё в
C:\Program Files\Prism Host, регистрирует службу Prism Host (автозапуск,
LocalSystem, стартует сразу после установки) и открывает входящие подключения
в брандмауэре для локальной сети. Деинсталляция останавливает и удаляет службу.
Корневая папка с видео выбирается в диалоге инсталлятора (по умолчанию
C:\Videos). Путь передаётся службе аргументом --media и запоминается в
реестре (HKLM\SOFTWARE\Prism\Host\MediaFolder) — обновление подхватит прошлый
выбор.
Бесшумная установка (без диалогов; из консоли администратора) — тем же способом можно сменить папку с видео без переустановки вручную:
msiexec /i PrismHostSetup.msi /qn MEDIAFOLDER=D:\Video
:: с журналом установки, если что-то пошло не так
msiexec /i PrismHostSetup.msi /qn MEDIAFOLDER=D:\Video /l*v install.logОстальная конфигурация установленного экземпляра — C:\Program Files\Prism Host\appsettings.json
(после правки перезапустить службу: sc stop PrismHost && sc start PrismHost;
папку с видео там менять бесполезно — аргумент --media из установки имеет
приоритет).
Для сборки MSI ничего ставить не нужно (
dotnet buildсам тянет WiX из NuGet), но чтобы.wixprojоткрывался в Visual Studio, нужно бесплатное расширение HeatWave for VS от FireGiant. Без него VS просто покажет проект как невыгруженный — сборке из CLI это не мешает. JetBrains Rider открывает.wixprojбез расширений.
Android-плеер (Prism.Player.Android) управляется и публикует свой статус по MQTT — нужен любой брокер в локальной сети, например Eclipse Mosquitto.
-
Установите mosquitto (инсталлятор с сайта регистрирует его службой Windows).
-
По умолчанию mosquitto ≥ 2.0 слушает только localhost — внешние клиенты (плеер) подключиться не смогут. Добавьте в конец
C:\Program Files\Mosquitto\mosquitto.conf:listener 1883 0.0.0.0 allow_anonymous trueallow_anonymous trueобязателен: как только объявленlistener, анонимный доступ автоматически запрещается. Для домашней сети за роутером этого достаточно; если брокер доступен извне — вместо анонимного доступа заведите пользователя (mosquitto_passwd+password_fileв конфиге). -
Перезапустите службу и откройте порт в брандмауэре (PowerShell от администратора):
Restart-Service mosquitto New-NetFirewallRule -DisplayName "Mosquitto MQTT" -Direction Inbound -Protocol TCP -LocalPort 1883 -Action Allow
Проверка: Get-NetTCPConnection -State Listen -LocalPort 1883 должен показать
адрес 0.0.0.0, а не 127.0.0.1.
У каждого запускаемого проекта свой appsettings.json: у Prism.Host.Console —
конфигурация разработки, у Prism.Host.Service — то, что уезжает в установку.
| Ключ | По умолчанию | Назначение |
|---|---|---|
MediaDirectory |
videos |
папка, сканируемая на медиа (относительно приложения) |
FfmpegPath |
(авто) | явный путь к бинарю ffmpeg |
FfprobePath |
(авто) | явный путь к бинарю ffprobe |
OutputCodec |
h264 |
h264 (libx264) или h265 (libx265/HEVC) |
SegmentSeconds |
6 |
длина HLS-сегмента |
SessionMinutes |
15 |
длина одной сессии транскодирования (реже разрывы ↔ больше диска) |
BufferBurstSeconds |
30 |
начальный «бёрст» перед пейсингом (быстрый старт ↔ нагрузка CPU); 0 — без пейсинга |
MaxPlaybackRate |
2.0 |
до какой скорости воспроизведения транскод «догоняет» (пейсинг = ×реального времени) |
SessionIdleSeconds |
25 |
через сколько секунд простоя убить сессию (освобождает CPU) |
EncoderPreset |
veryfast |
пресет скорости x264/x265 |
Crf |
23 |
качество видео (меньше = лучше/больше) |
AudioBitrateKbps |
256 |
битрейт аудио AAC, кбит/с |
AudioSampleRate |
48000 |
частота дискретизации аудио, Гц |
Аудио всегда перекодируется в AAC (libfdk_aac, если он есть в сборке ffmpeg,
иначе встроенный aac), 48 кГц, по умолчанию 256 кбит/с. Многоканальный звук
(5.1/7.1) сводится в стерео качественным даунмиксом (pan-фильтр), где центр/диалоги
идут на полном уровне, — это устраняет «тихий и глухой» звук обычного -ac 2.
HTTP-эндпоинт задаётся в секции Kestrel (по умолчанию http://0.0.0.0:8080).
Про H265: HEVC по HLS воспроизводится нативно в Safari, но не поддерживается hls.js в Chrome/Firefox, поэтому по умолчанию выбран
h264для максимальной совместимости с браузерами.