import fs from "node:fs" import path from "node:path" import { FFMPEGLib, Utils } from "../FFMPEGLib" export default class MultiqualityHLSJob extends FFMPEGLib { constructor(params = {}) { super() this.params = { outputMasterName: "master.m3u8", levels: [ { original: true, codec: "libx264", bitrate: "10M", preset: "ultrafast", }, ], ...params, } } buildArgs = () => { const cmdStr = [ `-v error -hide_banner -progress pipe:1`, `-i ${this.params.input}`, `-filter_complex`, ] // set split args let splitLevels = [`[0:v]split=${this.params.levels.length}`] this.params.levels.forEach((level, i) => { splitLevels[0] += `[v${i + 1}]` }) for (const [index, level] of this.params.levels.entries()) { 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) } 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`, ] 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}`) cmdStr.push(`-var_stream_map`) let streamMapVar = [] for (const [index, level] of this.params.levels.entries()) { streamMapVar.push(`v:${index}`) } cmdStr.push(`"${streamMapVar.join(" ")}"`) cmdStr.push(`"stream_%v/stream.m3u8"`) return cmdStr.join(" ") } run = async () => { const cmdStr = this.buildArgs() const outputPath = this.params.outputDir ?? path.join(path.dirname(this.params.input), "hls") const outputFile = path.join(outputPath, this.params.outputMasterName) this.emit("start", { input: this.params.input, output: outputPath, params: this.params, }) if (!fs.existsSync(outputPath)) { fs.mkdirSync(outputPath, { recursive: true }) } try { const inputProbe = await Utils.probe(this.params.input) const result = await this.ffmpeg({ args: cmdStr, cwd: outputPath, onProcess: (process) => { this.handleProgress( process.stdout, parseFloat(inputProbe.format.duration), (progress) => { this.emit("progress", progress) }, ) }, }) this.emit("end", { outputPath: outputPath, outputFile: outputFile, }) return result } catch (err) { return this.emit("error", err) } } }