2026-04-25 15:08:25 -03:00
|
|
|
#!/usr/bin/env node
|
|
|
|
|
// Regenerates stream.conf and VNC token files from current image templates.
|
|
|
|
|
// Runs at container startup (before supervisord) so configs always match the image.
|
|
|
|
|
|
|
|
|
|
import fs from 'fs'
|
|
|
|
|
import path from 'path'
|
|
|
|
|
|
|
|
|
|
const DATA_DIR = process.env.DATA_DIR ?? '/app/data'
|
|
|
|
|
const STREAMS_FILE = path.join(DATA_DIR, 'streams', 'streams.json')
|
|
|
|
|
const STREAMS_DIR = path.join(DATA_DIR, 'streams')
|
|
|
|
|
const VNC_TOKENS_DIR = path.join(DATA_DIR, 'vnc-tokens')
|
|
|
|
|
const LOGS_DIR = path.join(DATA_DIR, 'logs')
|
|
|
|
|
const CONF_TPL = '/opt/scripts/stream.template.conf'
|
|
|
|
|
|
|
|
|
|
if (!fs.existsSync(STREAMS_FILE) || !fs.existsSync(CONF_TPL)) process.exit(0)
|
|
|
|
|
|
|
|
|
|
const streams = JSON.parse(fs.readFileSync(STREAMS_FILE, 'utf-8'))
|
|
|
|
|
if (streams.length === 0) process.exit(0)
|
|
|
|
|
|
|
|
|
|
const confTpl = fs.readFileSync(CONF_TPL, 'utf-8')
|
|
|
|
|
|
|
|
|
|
function render(tpl, vars) {
|
|
|
|
|
return tpl.replace(/\{\{(\w+)\}\}/g, (_, k) => String(vars[k] ?? ''))
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 23:44:02 -03:00
|
|
|
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')
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 15:08:25 -03:00
|
|
|
for (const stream of streams) {
|
|
|
|
|
const dir = path.join(STREAMS_DIR, stream.id)
|
|
|
|
|
fs.mkdirSync(path.join(dir, 'chrome-profile'), { recursive: true })
|
|
|
|
|
fs.mkdirSync(path.join(LOGS_DIR, stream.id), { recursive: true })
|
|
|
|
|
fs.mkdirSync(VNC_TOKENS_DIR, { recursive: true })
|
|
|
|
|
|
|
|
|
|
const vars = {
|
|
|
|
|
STREAM_ID: stream.id,
|
|
|
|
|
DISPLAY: stream.display,
|
|
|
|
|
RESOLUTION: stream.resolution,
|
|
|
|
|
CHROME_SIZE: stream.resolution.replace('x', ','),
|
|
|
|
|
STREAM_URL: stream.url,
|
|
|
|
|
DEBUG_PORT: stream.debugPort,
|
|
|
|
|
VNC_PORT: stream.vncPort,
|
|
|
|
|
STREAM_DELAY: stream.delay,
|
|
|
|
|
FPS: stream.fps,
|
|
|
|
|
PRESET: stream.preset,
|
|
|
|
|
TUNE: stream.tune,
|
|
|
|
|
GOP: stream.gop,
|
|
|
|
|
BITRATE: stream.bitrate,
|
|
|
|
|
BUFSIZE: stream.bufsize,
|
|
|
|
|
SCALE: String(stream.scale).replace('x', ':'),
|
|
|
|
|
THREADS: stream.threads ?? 0,
|
|
|
|
|
USER: stream.user ?? '',
|
|
|
|
|
PASS: stream.pass ?? '',
|
2026-04-27 22:05:41 -03:00
|
|
|
GPU_FLAGS: stream.gpu ? '' : ' --disable-gpu \\\n',
|
2026-04-27 23:44:02 -03:00
|
|
|
ENCODER_FLAGS: buildEncoderFlags(stream),
|
2026-04-27 22:05:41 -03:00
|
|
|
AUTO_RELOAD: stream.autoReload ? 'true' : 'false',
|
|
|
|
|
AUTO_RELOAD_INTERVAL: stream.autoReloadInterval ?? 3600,
|
2026-04-25 15:08:25 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fs.writeFileSync(path.join(dir, 'stream.conf'), render(confTpl, vars), 'utf-8')
|
|
|
|
|
fs.writeFileSync(
|
|
|
|
|
path.join(VNC_TOKENS_DIR, `${stream.id}.cfg`),
|
|
|
|
|
`${stream.id}: localhost:${stream.vncPort}\n`,
|
|
|
|
|
'utf-8'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
console.log(`[reprovision] ${stream.id}`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(`[reprovision] done (${streams.length} stream(s))`)
|