Init commit
This commit is contained in:
110
server/core/lib/live-videos/preprocessed-video.ts
Normal file
110
server/core/lib/live-videos/preprocessed-video.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
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}`))
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user