Refactor MultiqualityHLSJob to inherit from FFMPEGLib

This commit is contained in:
SrGooglo 2025-04-24 09:38:43 +00:00
parent 5842911fd1
commit 8c6ffadcc8
2 changed files with 110 additions and 124 deletions

View File

@ -1,147 +1,138 @@
import fs from "node:fs" import fs from "node:fs"
import path from "node:path" import path from "node:path"
import { exec } from "node:child_process"
import { EventEmitter } from "node:events"
export default class MultiqualityHLSJob { import { FFMPEGLib, Utils } from "../FFMPEGLib"
constructor({
input,
outputDir,
outputMasterName = "master.m3u8",
levels,
}) {
this.input = input
this.outputDir = outputDir
this.levels = levels
this.outputMasterName = outputMasterName
this.bin = require("ffmpeg-static") export default class MultiqualityHLSJob extends FFMPEGLib {
constructor(params = {}) {
super()
return this this.params = {
} outputMasterName: "master.m3u8",
levels: [
{
original: true,
codec: "libx264",
bitrate: "10M",
preset: "ultrafast",
},
],
...params,
}
}
events = new EventEmitter() buildArgs = () => {
const cmdStr = [
`-v error -hide_banner -progress pipe:1`,
`-i ${this.params.input}`,
`-filter_complex`,
]
buildCommand = () => { // set split args
const cmdStr = [ let splitLevels = [`[0:v]split=${this.params.levels.length}`]
this.bin,
`-v quiet -stats`,
`-i ${this.input}`,
`-filter_complex`,
]
// set split args this.params.levels.forEach((level, i) => {
let splitLevels = [ splitLevels[0] += `[v${i + 1}]`
`[0:v]split=${this.levels.length}` })
]
this.levels.forEach((level, i) => { for (const [index, level] of this.params.levels.entries()) {
splitLevels[0] += (`[v${i + 1}]`) if (level.original) {
}) splitLevels.push(`[v1]copy[v1out]`)
continue
}
for (const [index, level] of this.levels.entries()) { let scaleFilter = `[v${index + 1}]scale=w=${level.width}:h=trunc(ow/a/2)*2[v${index + 1}out]`
if (level.original) {
splitLevels.push(`[v1]copy[v1out]`)
continue
}
let scaleFilter = `[v${index + 1}]scale=w=${level.width}:h=trunc(ow/a/2)*2[v${index + 1}out]` splitLevels.push(scaleFilter)
}
splitLevels.push(scaleFilter) cmdStr.push(`"${splitLevels.join(";")}"`)
}
cmdStr.push(`"${splitLevels.join(";")}"`) // set levels map
for (const [index, level] of this.params.levels.entries()) {
let mapArgs = [
`-map "[v${index + 1}out]"`,
`-x264-params "nal-hrd=cbr:force-cfr=1"`,
`-c:v:${index} ${level.codec}`,
`-b:v:${index} ${level.bitrate}`,
`-maxrate:v:${index} ${level.bitrate}`,
`-minrate:v:${index} ${level.bitrate}`,
`-bufsize:v:${index} ${level.bitrate}`,
`-preset ${level.preset}`,
`-g 48`,
`-sc_threshold 0`,
`-keyint_min 48`,
]
// set levels map cmdStr.push(...mapArgs)
for (const [index, level] of this.levels.entries()) { }
let mapArgs = [
`-map "[v${index + 1}out]"`,
`-x264-params "nal-hrd=cbr:force-cfr=1"`,
`-c:v:${index} ${level.codec}`,
`-b:v:${index} ${level.bitrate}`,
`-maxrate:v:${index} ${level.bitrate}`,
`-minrate:v:${index} ${level.bitrate}`,
`-bufsize:v:${index} ${level.bitrate}`,
`-preset ${level.preset}`,
`-g 48`,
`-sc_threshold 0`,
`-keyint_min 48`,
]
cmdStr.push(...mapArgs) // set output
} cmdStr.push(`-f hls`)
cmdStr.push(`-hls_time 2`)
cmdStr.push(`-hls_playlist_type vod`)
cmdStr.push(`-hls_flags independent_segments`)
cmdStr.push(`-hls_segment_type mpegts`)
cmdStr.push(`-hls_segment_filename stream_%v/data%02d.ts`)
cmdStr.push(`-master_pl_name ${this.params.outputMasterName}`)
// set output cmdStr.push(`-var_stream_map`)
cmdStr.push(`-f hls`)
cmdStr.push(`-hls_time 2`)
cmdStr.push(`-hls_playlist_type vod`)
cmdStr.push(`-hls_flags independent_segments`)
cmdStr.push(`-hls_segment_type mpegts`)
cmdStr.push(`-hls_segment_filename stream_%v/data%02d.ts`)
cmdStr.push(`-master_pl_name ${this.outputMasterName}`)
cmdStr.push(`-var_stream_map`) let streamMapVar = []
let streamMapVar = [] for (const [index, level] of this.params.levels.entries()) {
streamMapVar.push(`v:${index}`)
}
for (const [index, level] of this.levels.entries()) { cmdStr.push(`"${streamMapVar.join(" ")}"`)
streamMapVar.push(`v:${index}`) cmdStr.push(`"stream_%v/stream.m3u8"`)
}
cmdStr.push(`"${streamMapVar.join(" ")}"`) return cmdStr.join(" ")
cmdStr.push(`"stream_%v/stream.m3u8"`) }
return cmdStr.join(" ") run = async () => {
} const cmdStr = this.buildArgs()
run = () => { const outputPath =
const cmdStr = this.buildCommand() this.params.outputDir ??
path.join(path.dirname(this.params.input), "hls")
const outputFile = path.join(outputPath, this.params.outputMasterName)
console.log(cmdStr) this.emit("start", {
input: this.params.input,
output: outputPath,
params: this.params,
})
const cwd = `${path.dirname(this.input)}/hls` if (!fs.existsSync(outputPath)) {
fs.mkdirSync(outputPath, { recursive: true })
}
if (!fs.existsSync(cwd)) { const inputProbe = await Utils.probe(this.params.input)
fs.mkdirSync(cwd, { recursive: true })
}
console.log(`[HLS] Started multiquality transcode`, { try {
input: this.input, const result = await this.ffmpeg({
cwd: cwd, args: cmdStr,
}) cwd: outputPath,
onProcess: (process) => {
this.handleProgress(
process.stdout,
parseFloat(inputProbe.format.duration),
(progress) => {
this.emit("progress", progress)
},
)
},
})
const process = exec( this.emit("end", {
cmdStr, outputPath: outputPath,
{ outputFile: outputFile,
cwd: cwd, })
},
(error, stdout, stderr) => {
if (error) {
console.log(`[HLS] Failed to transcode >`, error)
return this.events.emit("error", error) return result
} } catch (err) {
return this.emit("error", err)
if (stderr) { }
//return this.events.emit("error", stderr) }
} }
console.log(`[HLS] Finished transcode >`, cwd)
return this.events.emit("end", {
filepath: path.join(cwd, this.outputMasterName),
isDirectory: true,
})
}
)
process.stdout.on("data", (data) => {
console.log(data.toString())
})
}
on = (key, cb) => {
this.events.on(key, cb)
return this
}
}

View File

@ -27,12 +27,7 @@ export default async ({ filePath, workPath, onProgress }) => {
], ],
}) })
job.on("start", () => {
console.log("A-DASH started")
})
job.on("end", (data) => { job.on("end", (data) => {
console.log("A-DASH completed", data)
resolve(data) resolve(data)
}) })