Files
SolTube/server/core/lib/live-videos/preprocessed-video.ts
ShreejitPanchal 6601501eff Init commit
2026-05-19 19:56:02 +08:00

111 lines
3.1 KiB
TypeScript

import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
import { tryAtomicMove } from '@server/helpers/fs.js'
import { CONFIG } from '@server/initializers/config.js'
import { remove } from 'fs-extra/esm'
import { existsSync } from 'node:fs'
import { dirname } from 'node:path'
import { spawn } from 'node:child_process'
const lTags = loggerTagsFactory('live-videos', 'preprocessed-video')
export function getLiveVideoBurnedPath (videoPath: string) {
return `${videoPath}.burned.mp4`
}
export function getLiveVideoSubtitlePath (videoPath: string) {
if (existsSync(videoPath + '.vtt')) return videoPath + '.vtt'
if (existsSync(videoPath + '.srt')) return videoPath + '.srt'
return null
}
export function getLiveVideoPlaybackInputPath (videoPath: string) {
const burnedPath = getLiveVideoBurnedPath(videoPath)
return existsSync(burnedPath) ? burnedPath : videoPath
}
export async function syncPreprocessedLiveVideo (videoPath: string) {
const subtitlePath = getLiveVideoSubtitlePath(videoPath)
const burnedPath = getLiveVideoBurnedPath(videoPath)
if (!subtitlePath) {
await remove(burnedPath).catch(() => undefined)
return null
}
const tmpOutputPath = `${burnedPath}.tmp.mp4`
await remove(tmpOutputPath).catch(() => undefined)
const ffmpegPath = CONFIG.LIVE.FFMPEG.PATH?.trim() || 'ffmpeg'
const subtitleFilterPath = escapeSubtitleFilterPath(subtitlePath)
const ffmpegArgs = [
'-y',
'-hide_banner',
'-loglevel', 'warning',
'-i', videoPath,
'-map', '0:v:0',
'-map', '0:a?',
'-vf', `subtitles=${subtitleFilterPath}`,
'-c:v', 'libx264',
'-preset', 'veryfast',
'-c:a', 'aac',
'-movflags', '+faststart',
tmpOutputPath
]
await runFFmpeg(ffmpegPath, ffmpegArgs, { videoPath, subtitlePath })
await remove(burnedPath).catch(() => undefined)
await tryAtomicMove(tmpOutputPath, burnedPath)
return burnedPath
}
function escapeSubtitleFilterPath (subtitlePath: string) {
return subtitlePath
.replace(/\\/g, '\\\\')
.replace(/:/g, '\\:')
.replace(/ /g, '\\ ')
.replace(/\[/g, '\\[')
.replace(/\]/g, '\\]')
.replace(/,/g, '\\,')
.replace(/'/g, "\\'")
}
function runFFmpeg (ffmpegPath: string, ffmpegArgs: string[], meta: { videoPath: string, subtitlePath: string }) {
return new Promise<void>((resolve, reject) => {
const ffmpegProcess = spawn(ffmpegPath, ffmpegArgs, {
cwd: dirname(meta.videoPath),
stdio: [ 'ignore', 'ignore', 'pipe' ]
})
let stderrBuffer = ''
ffmpegProcess.stderr.on('data', chunk => {
const line = chunk?.toString?.() || ''
stderrBuffer = (stderrBuffer + line).slice(-4_000)
})
ffmpegProcess.once('error', err => {
reject(err)
})
ffmpegProcess.once('exit', code => {
if (code === 0) {
resolve()
return
}
const stderr = stderrBuffer.trim()
logger.error('Cannot preprocess live video with burned subtitles.', {
code,
stderr,
...meta,
...lTags()
})
reject(new Error(stderr || `ffmpeg exited with code ${code}`))
})
})
}