Adiciona suporte a encoders de hardware (NVENC, VAAPI, QSV) no ffmpeg

---

- Implementada função buildEncoderFlags() em supervisor.ts e reprovision.mjs que gera o bloco de flags do ffmpeg conforme a env var FFMPEG_HWACCEL (nvenc, vaapi, qsv ou vazio para libx264);
- Template stream.template.conf refatorado para usar {{ENCODER_FLAGS}} no lugar do bloco x264 fixo;
- NVENC configurado com perfil high, mapeamento de presets x264→p1-p7 e tune zerolatency→ll;
- docker-compose.yml atualizado com seções comentadas para gpus, devices, FFMPEG_HWACCEL e instrução de volume WSL2 para libnvidia-encode;
- Dockerfile adiciona mesa-va-drivers e intel-media-va-driver para suporte a VAAPI e remove declaração VOLUME redundante;
- fetchAllStatuses() corrigido: supervisorctl status || true evita exceção com exit code 3 quando há processos parados;
- reprovision.mjs atualizado para incluir AUTO_RELOAD e AUTO_RELOAD_INTERVAL no contexto de renderização do template;

---
This commit is contained in:
2026-04-27 23:44:02 -03:00
parent 14094cf5ed
commit 4918fa091e
7 changed files with 164 additions and 39 deletions
+3 -11
View File
@@ -1,13 +1,5 @@
# Changelog # Changelog
## Unreleased
### Added
- **Chromium auto-reload** — per-stream toggle to reload the browser page on a configurable interval via Chrome DevTools Protocol. Implemented as a dedicated Supervisord process (`autoreload-{id}`) using a raw WebSocket CDP connection (no external dependencies). Responds cleanly to `supervisorctl stop` via SIGTERM trap. Interval is set in minutes from the card's 3-dot menu and persisted server-side. Toggle applies via `POST /api/streams/{id}/autoreload` without restarting the stream.
---
## Decap Stream v1.0.0 ## Decap Stream v1.0.0
Turn any web page into an RTMP/HLS stream. Chromium renders the page in a virtual display, ffmpeg captures it, and MediaMTX publishes it, all managed through a web UI. Turn any web page into an RTMP/HLS stream. Chromium renders the page in a virtual display, ffmpeg captures it, and MediaMTX publishes it, all managed through a web UI.
@@ -19,9 +11,9 @@ Turn any web page into an RTMP/HLS stream. Chromium renders the page in a virtua
- **Scalable card sizes** — mini / sm / md / lg with proportional scaling across all elements - **Scalable card sizes** — mini / sm / md / lg with proportional scaling across all elements
- **Inline VNC** — inspect any stream's virtual display without leaving the UI - **Inline VNC** — inspect any stream's virtual display without leaving the UI
- **Autologin with CDP detection** — skips login if the session is still alive on container restart - **Autologin with CDP detection** — skips login if the session is still alive on container restart
- **Built-in HLS player** — with controls; static standalone page optimized for TV browsers (`/player.html?id=<id>`) - **Built-in HLS player** — with controls; static standalone page optimized for TV browsers (`/player/<id>.html`)
- **Per-card Pure mode** — open streams as a raw `.m3u8` link or a zero-dependency `.html` page, usable in VLC or any HLS-capable player - **Pure mode** — global toggle in Settings to open streams as a raw `.m3u8` link or a zero-dependency `.html` page, usable in VLC or any HLS-capable player
- **Per-card new-tab toggle** — open any action button in a new tab; settings are per-card and saved in the browser - **Open in new tab** — global toggle in Settings to open any action button in a new tab; saved in the browser
- **Optional UI authentication** — set `AUTH_USER` + `AUTH_PASS` to password-protect the entire UI - **Optional UI authentication** — set `AUTH_USER` + `AUTH_PASS` to password-protect the entire UI
- **Persistent desired state** — streams restore automatically on container restart - **Persistent desired state** — streams restore automatically on container restart
+17 -10
View File
@@ -39,9 +39,10 @@ All processes are managed by Supervisord. The web UI is a Next.js app that contr
- **Fully configurable encoding** — resolution, scale, FPS, bitrate, preset, tune, GOP, threads, all per stream - **Fully configurable encoding** — resolution, scale, FPS, bitrate, preset, tune, GOP, threads, all per stream
- **GPU acceleration** — optional per-stream Chromium GPU flag (disabled by default for container compatibility) - **GPU acceleration** — optional per-stream Chromium GPU flag (disabled by default for container compatibility)
- **Built-in HLS player** — watch any stream in the browser via a standalone HTML page optimized for TVs (Back + Mute buttons, reconnect on stall, direct MediaMTX connection when available) - **Built-in HLS player** — watch any stream in the browser via a standalone HTML page optimized for TVs (Back + Mute buttons, reconnect on stall, direct MediaMTX connection when available)
- **Per-card Pure mode** — toggle in the card menu to open Play Stream as a raw `.m3u8` link or Run HTML as a minimal `.html` page with no UI; works with native players and TV browsers - **Pure mode** — global toggle in Settings to open Play Stream as a raw `.m3u8` link or Run HTML as a minimal `.html` page with no UI; works with native players and TV browsers
- **Per-card new tab** — toggle to open any button in a new tab instead of navigating in place; both settings are per-card and saved in the browser - **Open in new tab** — global toggle in Settings to open any button in a new tab instead of navigating in place; saved in the browser
- **Chromium auto-reload** — per-card toggle to reload the browser page on a configurable interval; uses Chrome DevTools Protocol (no xdotool focus tricks); configured from the card menu and persisted on the server - **Chromium auto-reload** — per-stream toggle to reload the Chromium page on a configurable interval via Chrome DevTools Protocol; configured from the card menu and persisted on the server
- **Player client-side auto-reload** — global toggle in Settings to reload the HLS player itself on a configurable interval (in minutes)
## Platform Support ## Platform Support
@@ -61,20 +62,26 @@ services:
image: ghcr.io/riguettodev/decap-stream:latest image: ghcr.io/riguettodev/decap-stream:latest
container_name: decap-stream container_name: decap-stream
restart: unless-stopped restart: unless-stopped
shm_size: "2gb" shm_size: "1gb"
security_opt: security_opt:
- seccomp:unconfined # required for Chromium syscalls - seccomp:unconfined
# gpus: all # Uncomment for NVIDIA (nvenc) — requires nvidia-container-toolkit on host
# devices:
# - /dev/dri:/dev/dri # Uncomment for Intel/AMD (vaapi or qsv)
environment: environment:
# TZ: America/Sao_Paulo TZ: America/Sao_Paulo
# AUTH_USER: admin # optional: enables login if both are set # FFMPEG_HWACCEL: nvenc # GPU encoding: nvenc (NVIDIA), vaapi (Intel/AMD), qsv (Intel QSV) / Requires: nvenc → gpus: all | vaapi/qsv → devices: /dev/dri
# LD_LIBRARY_PATH: /usr/lib/wsl/lib # WSL2 + nvenc only: injects NVENC libs not auto-mounted by Docker
# AUTH_USER: admin # Se definido (junto com AUTH_PASS), habilita login
# AUTH_PASS: secure_password # AUTH_PASS: secure_password
ports: ports:
- "3000:3000" # Web UI — main entry point - "3000:3000" # Web UI — main entry point
- "127.0.0.1:6080:6080" # VNC — localhost only; remote access via tunnel/VPN - "127.0.0.1:6080:6080" # VNC — localhost only; remote access via tunnel/VPN
# - "1935:1935" # RTMP — expose only for external ingest (e.g. OBS) # - "1935:1935" # RTMP — internal only; expose only for external ingest (e.g. OBS)
# - "8888:8888" # HLS — internal only; proxied through the UI at /api/hls/ # - "8888:8888" # HLS — internal only; proxied through Next.js at /api/hls/
volumes: volumes:
- streams:/app/data/streams # Persistent: streams.json, chrome profiles, thumbs - streams:/app/data/streams # Persistent: streams.json, chrome profiles, thumbs
# - /usr/lib/wsl/lib:/usr/lib/wsl/lib:ro # WSL2 + nvenc: exposes libnvidia-encode.so.1
# - logs:/app/data/logs # Optional # - logs:/app/data/logs # Optional
volumes: volumes:
@@ -109,7 +116,7 @@ Each stream gets a slug ID you define (e.g. `grafana-prod`):
| RTMP ingest | `rtmp://<host>:1935/live/<id>` | | RTMP ingest | `rtmp://<host>:1935/live/<id>` |
| HLS manifest (proxied) | `http://<host>:3000/api/hls/live/<id>/index.m3u8` | | HLS manifest (proxied) | `http://<host>:3000/api/hls/live/<id>/index.m3u8` |
| HLS manifest (direct) | `http://<host>:8888/live/<id>/index.m3u8` — requires port 8888 exposed | | HLS manifest (direct) | `http://<host>:8888/live/<id>/index.m3u8` — requires port 8888 exposed |
| HTML player | `http://<host>:3000/player.html?id=<id>` — static minimal page, no UI chrome | | HTML player | `http://<host>:3000/player/<id>.html` — static minimal page, no UI chrome |
| VNC (inline) | `http://<host>:3000/vnc/<id>` | | VNC (inline) | `http://<host>:3000/vnc/<id>` |
> **Pure mode** (toggle per card): Play Stream opens the proxied HLS `.m3u8` directly; Run HTML opens the `.html` player. Both can be pasted into VLC or any HLS-capable player, or loaded natively on TV browsers that support HLS. > **Pure mode** (toggle per card): Play Stream opens the proxied HLS `.m3u8` directly; Run HTML opens the `.html` player. Both can be pasted into VLC or any HLS-capable player, or loaded natively on TV browsers that support HLS.
+1 -1
View File
@@ -27,6 +27,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
&& apt-get install -y --no-install-recommends \ && apt-get install -y --no-install-recommends \
xvfb x11vnc novnc websockify \ xvfb x11vnc novnc websockify \
ffmpeg supervisor xdotool tzdata \ ffmpeg supervisor xdotool tzdata \
mesa-va-drivers intel-media-va-driver \
chromium \ chromium \
curl gnupg \ curl gnupg \
&& ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime \ && ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime \
@@ -70,7 +71,6 @@ COPY docker/server.mjs /opt/server.mjs
COPY docker/entrypoint.sh /entrypoint.sh COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /opt/scripts/*.sh /entrypoint.sh RUN chmod +x /opt/scripts/*.sh /entrypoint.sh
VOLUME ["/app/data"]
EXPOSE 3000 1935 8888 6080 EXPOSE 3000 1935 8888 6080
CMD ["/entrypoint.sh"] CMD ["/entrypoint.sh"]
+6
View File
@@ -6,8 +6,13 @@ services:
shm_size: "1gb" shm_size: "1gb"
security_opt: security_opt:
- seccomp:unconfined - seccomp:unconfined
# gpus: all # Uncomment for NVIDIA (nvenc) — requires nvidia-container-toolkit on host
# devices:
# - /dev/dri:/dev/dri # Uncomment for Intel/AMD (vaapi or qsv)
environment: environment:
TZ: America/Sao_Paulo TZ: America/Sao_Paulo
# FFMPEG_HWACCEL: nvenc # GPU encoding: nvenc (NVIDIA), vaapi (Intel/AMD), qsv (Intel QSV) / Requires: nvenc → gpus: all | vaapi/qsv → devices: /dev/dri
# LD_LIBRARY_PATH: /usr/lib/wsl/lib # WSL2 + nvenc only: injects NVENC libs not auto-mounted by Docker
# AUTH_USER: admin # Se definido (junto com AUTH_PASS), habilita login # AUTH_USER: admin # Se definido (junto com AUTH_PASS), habilita login
# AUTH_PASS: secure_password # AUTH_PASS: secure_password
ports: ports:
@@ -17,6 +22,7 @@ services:
# - "8888:8888" # HLS — internal only; proxied through Next.js at /api/hls/ # - "8888:8888" # HLS — internal only; proxied through Next.js at /api/hls/
volumes: volumes:
- streams:/app/data/streams # Persistent: streams.json, chrome profiles, thumbs - streams:/app/data/streams # Persistent: streams.json, chrome profiles, thumbs
# - /usr/lib/wsl/lib:/usr/lib/wsl/lib:ro # WSL2 + nvenc: exposes libnvidia-encode.so.1
# - logs:/app/data/logs # Optional # - logs:/app/data/logs # Optional
volumes: volumes:
+65
View File
@@ -23,6 +23,70 @@ function render(tpl, vars) {
return tpl.replace(/\{\{(\w+)\}\}/g, (_, k) => String(vars[k] ?? '')) return tpl.replace(/\{\{(\w+)\}\}/g, (_, k) => String(vars[k] ?? ''))
} }
const NVENC_PRESET = {
ultrafast: 'p1', superfast: 'p1', veryfast: 'p2',
faster: 'p3', fast: 'p3', medium: 'p4',
slow: 'p5', slower: 'p6', veryslow: 'p7',
}
function buildEncoderFlags(stream) {
const { preset, tune, gop, bitrate, bufsize } = stream
const hwaccel = (process.env.FFMPEG_HWACCEL ?? '').toLowerCase().trim()
const lines = []
const ln = (s) => lines.push(` ${s} \\`)
if (hwaccel === 'nvenc') {
ln(`-c:v h264_nvenc`)
ln(`-preset ${NVENC_PRESET[preset] ?? 'p4'}`)
ln(`-tune ${tune === 'zerolatency' ? 'll' : 'hq'}`)
ln(`-profile:v high`)
ln(`-pix_fmt yuv420p`)
ln(`-rc cbr`)
ln(`-g ${gop}`)
ln(`-keyint_min ${gop}`)
ln(`-b:v ${bitrate}`)
ln(`-maxrate ${bitrate}`)
ln(`-bufsize ${bufsize}`)
} else if (hwaccel === 'vaapi') {
ln(`-vaapi_device /dev/dri/renderD128`)
ln(`-vf 'format=nv12,hwupload'`)
ln(`-c:v h264_vaapi`)
ln(`-profile:v baseline`)
ln(`-level 3.1`)
ln(`-g ${gop}`)
ln(`-keyint_min ${gop}`)
ln(`-b:v ${bitrate}`)
ln(`-maxrate ${bitrate}`)
ln(`-bufsize ${bufsize}`)
} else if (hwaccel === 'qsv') {
ln(`-c:v h264_qsv`)
ln(`-preset veryfast`)
ln(`-profile:v baseline`)
ln(`-level 3.1`)
ln(`-pix_fmt nv12`)
ln(`-g ${gop}`)
ln(`-keyint_min ${gop}`)
ln(`-b:v ${bitrate}`)
ln(`-maxrate ${bitrate}`)
ln(`-bufsize ${bufsize}`)
} else {
ln(`-c:v libx264`)
ln(`-preset ${preset}`)
ln(`-tune ${tune}`)
ln(`-profile:v baseline`)
ln(`-level 3.1`)
ln(`-pix_fmt yuv420p`)
ln(`-g ${gop}`)
ln(`-keyint_min ${gop}`)
ln(`-sc_threshold 0`)
ln(`-b:v ${bitrate}`)
ln(`-maxrate ${bitrate}`)
ln(`-bufsize ${bufsize}`)
}
return lines.join('\n')
}
for (const stream of streams) { for (const stream of streams) {
const dir = path.join(STREAMS_DIR, stream.id) const dir = path.join(STREAMS_DIR, stream.id)
fs.mkdirSync(path.join(dir, 'chrome-profile'), { recursive: true }) fs.mkdirSync(path.join(dir, 'chrome-profile'), { recursive: true })
@@ -49,6 +113,7 @@ for (const stream of streams) {
USER: stream.user ?? '', USER: stream.user ?? '',
PASS: stream.pass ?? '', PASS: stream.pass ?? '',
GPU_FLAGS: stream.gpu ? '' : ' --disable-gpu \\\n', GPU_FLAGS: stream.gpu ? '' : ' --disable-gpu \\\n',
ENCODER_FLAGS: buildEncoderFlags(stream),
AUTO_RELOAD: stream.autoReload ? 'true' : 'false', AUTO_RELOAD: stream.autoReload ? 'true' : 'false',
AUTO_RELOAD_INTERVAL: stream.autoReloadInterval ?? 3600, AUTO_RELOAD_INTERVAL: stream.autoReloadInterval ?? 3600,
} }
+1 -12
View File
@@ -73,18 +73,7 @@ command=bash -c "sleep {{STREAM_DELAY}} && ffmpeg \
-i {{DISPLAY}} \ -i {{DISPLAY}} \
-f lavfi -i anullsrc=channel_layout=stereo:sample_rate=44100 \ -f lavfi -i anullsrc=channel_layout=stereo:sample_rate=44100 \
-shortest \ -shortest \
-c:v libx264 \ {{ENCODER_FLAGS}}
-preset {{PRESET}} \
-tune {{TUNE}} \
-profile:v baseline \
-level 3.1 \
-pix_fmt yuv420p \
-g {{GOP}} \
-keyint_min {{GOP}} \
-sc_threshold 0 \
-b:v {{BITRATE}} \
-maxrate {{BITRATE}} \
-bufsize {{BUFSIZE}} \
-c:a aac \ -c:a aac \
-b:a 128k \ -b:a 128k \
-ar 44100 \ -ar 44100 \
+70 -4
View File
@@ -29,6 +29,70 @@ function supervisorctl(cmd: string) {
} }
} }
const NVENC_PRESET: Record<string, string> = {
ultrafast: "p1", superfast: "p1", veryfast: "p2",
faster: "p3", fast: "p3", medium: "p4",
slow: "p5", slower: "p6", veryslow: "p7",
}
function buildEncoderFlags(stream: Stream): string {
const { preset, tune, gop, bitrate, bufsize } = stream
const hwaccel = (process.env.FFMPEG_HWACCEL ?? "").toLowerCase().trim()
const lines: string[] = []
const ln = (s: string) => lines.push(` ${s} \\`)
if (hwaccel === "nvenc") {
ln(`-c:v h264_nvenc`)
ln(`-preset ${NVENC_PRESET[preset] ?? "p4"}`)
ln(`-tune ${tune === "zerolatency" ? "ll" : "hq"}`)
ln(`-profile:v high`)
ln(`-pix_fmt yuv420p`)
ln(`-rc cbr`)
ln(`-g ${gop}`)
ln(`-keyint_min ${gop}`)
ln(`-b:v ${bitrate}`)
ln(`-maxrate ${bitrate}`)
ln(`-bufsize ${bufsize}`)
} else if (hwaccel === "vaapi") {
ln(`-vaapi_device /dev/dri/renderD128`)
ln(`-vf 'format=nv12,hwupload'`)
ln(`-c:v h264_vaapi`)
ln(`-profile:v baseline`)
ln(`-level 3.1`)
ln(`-g ${gop}`)
ln(`-keyint_min ${gop}`)
ln(`-b:v ${bitrate}`)
ln(`-maxrate ${bitrate}`)
ln(`-bufsize ${bufsize}`)
} else if (hwaccel === "qsv") {
ln(`-c:v h264_qsv`)
ln(`-preset veryfast`)
ln(`-profile:v baseline`)
ln(`-level 3.1`)
ln(`-pix_fmt nv12`)
ln(`-g ${gop}`)
ln(`-keyint_min ${gop}`)
ln(`-b:v ${bitrate}`)
ln(`-maxrate ${bitrate}`)
ln(`-bufsize ${bufsize}`)
} else {
ln(`-c:v libx264`)
ln(`-preset ${preset}`)
ln(`-tune ${tune}`)
ln(`-profile:v baseline`)
ln(`-level 3.1`)
ln(`-pix_fmt yuv420p`)
ln(`-g ${gop}`)
ln(`-keyint_min ${gop}`)
ln(`-sc_threshold 0`)
ln(`-b:v ${bitrate}`)
ln(`-maxrate ${bitrate}`)
ln(`-bufsize ${bufsize}`)
}
return lines.join("\n")
}
// converts "1920x1080" → "1920,1080" for Chrome --window-size flag // converts "1920x1080" → "1920,1080" for Chrome --window-size flag
function resolutionToChrome(res: string): string { function resolutionToChrome(res: string): string {
return res.replace("x", ",") return res.replace("x", ",")
@@ -64,6 +128,7 @@ export function provisionStream(stream: Stream): void {
USER: stream.user ?? "", USER: stream.user ?? "",
PASS: stream.pass ?? "", PASS: stream.pass ?? "",
GPU_FLAGS: stream.gpu ? "" : " --disable-gpu \\\n", GPU_FLAGS: stream.gpu ? "" : " --disable-gpu \\\n",
ENCODER_FLAGS: buildEncoderFlags(stream),
AUTO_RELOAD: stream.autoReload ? "true" : "false", AUTO_RELOAD: stream.autoReload ? "true" : "false",
AUTO_RELOAD_INTERVAL: stream.autoReloadInterval ?? 3600, AUTO_RELOAD_INTERVAL: stream.autoReloadInterval ?? 3600,
} }
@@ -153,20 +218,21 @@ function fetchAllStatuses(): Record<string, Record<string, ProgramStatus>> {
const result: Record<string, Record<string, ProgramStatus>> = {} const result: Record<string, Record<string, ProgramStatus>> = {}
try { try {
// One call for all programs — avoid N×5 blocking execSync calls per poll cycle // One call for all programs — avoid N×5 blocking execSync calls per poll cycle
// supervisorctl exits 3 when any process is EXITED/STOPPED — || true keeps execSync from throwing
const out = execSync( const out = execSync(
`supervisorctl -c /etc/supervisor/supervisord.conf status`, `supervisorctl -c /etc/supervisor/supervisord.conf status || true`,
{ stdio: "pipe" } { stdio: "pipe", shell: "/bin/sh" }
).toString() ).toString()
for (const line of out.split("\n")) { for (const line of out.split("\n")) {
// e.g. "ffmpeg-abc123 RUNNING pid 42, uptime 0:01:00" // e.g. "ffmpeg-abc123 RUNNING pid 42, uptime 0:01:00"
const m = line.match(/^(\S+)-(\S+)\s+(RUNNING|STOPPED|FATAL|STARTING)/) const m = line.match(/^(xvfb|chromium|autologin|autoreload|x11vnc|ffmpeg)-(\S+)\s+(RUNNING|STOPPED|FATAL|STARTING)/)
if (!m) continue if (!m) continue
const [, program, id, status] = m const [, program, id, status] = m
if (!result[id]) result[id] = {} if (!result[id]) result[id] = {}
result[id][program] = status as ProgramStatus result[id][program] = status as ProgramStatus
} }
} catch { } catch {
// supervisorctl can exit non-zero; return whatever was parsed // fallback: supervisorctl completely unavailable
} }
_statusCache = result _statusCache = result