Skip to content

thinking-home/prism

Repository files navigation

Prism

Сетевой медиа-плеер из двух частей:

  • 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/ и удаляются при вытеснении сессии и при завершении приложения.

Нагрузка на CPU (пейсинг и уборка)

По умолчанию каждый процесс 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/ автоматически; в установку службы плагины кладёт инсталлятор.

Плагин Prism.Plugins.Library (метаданные)

Хранит метаданные библиотеки — источник истины на сервере. Минимальный набор: для фильма — название; для эпизода — сериал, номер сезона и эпизода. Отображаемое название подмешивается в /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 в комплекте инсталлятора.

Установка ffmpeg

Windows

Вариант через пакетный менеджер (рекомендуется):

# 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.

macOS

# Homebrew
brew install ffmpeg

# либо MacPorts
sudo port install ffmpeg

Linux

# 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.

Запуск

Нужно запустить оба приложения.

1. Сервер — Prism.Host.Console

# один раз собрать решение — заодно развернутся включённые плагины в 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/ffmpeg

2. Клиент — Prism.Client

cd 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 и инсталлятор

На 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 без расширений.

MQTT-брокер

Android-плеер (Prism.Player.Android) управляется и публикует свой статус по MQTT — нужен любой брокер в локальной сети, например Eclipse Mosquitto.

Минимальная настройка на Windows

  1. Установите mosquitto (инсталлятор с сайта регистрирует его службой Windows).

  2. По умолчанию mosquitto ≥ 2.0 слушает только localhost — внешние клиенты (плеер) подключиться не смогут. Добавьте в конец C:\Program Files\Mosquitto\mosquitto.conf:

    listener 1883 0.0.0.0
    allow_anonymous true
    

    allow_anonymous true обязателен: как только объявлен listener, анонимный доступ автоматически запрещается. Для домашней сети за роутером этого достаточно; если брокер доступен извне — вместо анонимного доступа заведите пользователя (mosquitto_passwd + password_file в конфиге).

  3. Перезапустите службу и откройте порт в брандмауэре (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, секция Player)

У каждого запускаемого проекта свой 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 для максимальной совместимости с браузерами.

About

No description, website, or topics provided.

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors