Refactor SegmentedAudioMPDJob to extend FFMPEGLib

This commit is contained in:
SrGooglo 2025-04-24 06:08:32 +00:00
parent f62e885c65
commit 80acb13912
2 changed files with 199 additions and 93 deletions

View File

@ -0,0 +1,110 @@
import { EventEmitter } from "node:events"
import child_process from "node:child_process"
function getBinaryPath(name) {
try {
return child_process
.execSync(`which ${name}`, { encoding: "utf8" })
.trim()
} catch (error) {
return null
}
}
export class FFMPEGLib extends EventEmitter {
constructor() {
super()
this.ffmpegBin = getBinaryPath("ffmpeg")
this.ffprobeBin = getBinaryPath("ffprobe")
}
handleProgress(stdout, endTime, onProgress = () => {}) {
let currentTime = 0
stdout.on("data", (data) => {
for (const line of data.toString().split("\n")) {
if (line.startsWith("out_time_ms=")) {
currentTime = parseInt(line.split("=")[1]) / 1000000
} else if (line.startsWith("progress=")) {
const status = line.split("=")[1]
if (status === "end") {
onProgress(100)
} else if (endTime > 0 && currentTime > 0) {
onProgress(
Math.min(
100,
Math.round((currentTime / endTime) * 100),
),
)
}
}
}
})
}
ffmpeg(payload) {
return this.exec(this.ffmpegBin, payload)
}
ffprobe(payload) {
return this.exec(this.ffprobeBin, payload)
}
exec(bin, { args, onProcess, cwd }) {
if (Array.isArray(args)) {
args = args.join(" ")
}
return new Promise((resolve, reject) => {
const process = child_process.exec(
`${bin} ${args}`,
{
cwd: cwd,
},
(error, stdout, stderr) => {
if (error) {
reject(stderr)
} else {
resolve(stdout.toString())
}
},
)
if (typeof onProcess === "function") {
onProcess(process)
}
})
}
}
export class Utils {
static async probe(input) {
const lib = new FFMPEGLib()
const result = await lib
.ffprobe({
args: [
"-v",
"error",
"-print_format",
"json",
"-show_format",
"-show_streams",
input,
],
})
.catch((err) => {
console.log(err)
return null
})
if (!result) {
return null
}
return JSON.parse(result)
}
}
export default FFMPEGLib

View File

@ -1,112 +1,108 @@
import fs from "node:fs"
import path from "node:path"
import { exec } from "node:child_process"
import { EventEmitter } from "node:events"
export default class SegmentedAudioMPDJob {
constructor({
input,
outputDir,
outputMasterName = "master.mpd",
import { FFMPEGLib, Utils } from "../FFMPEGLib"
audioCodec = "aac",
audioBitrate = undefined,
audioSampleRate = undefined,
segmentTime = 10,
}) {
this.input = input
this.outputDir = outputDir
this.outputMasterName = outputMasterName
export default class SegmentedAudioMPDJob extends FFMPEGLib {
constructor(params = {}) {
super()
this.audioCodec = audioCodec
this.audioBitrate = audioBitrate
this.segmentTime = segmentTime
this.audioSampleRate = audioSampleRate
this.bin = require("ffmpeg-static")
return this
this.params = {
outputMasterName: "master.mpd",
audioCodec: "libopus",
audioBitrate: "320k",
audioSampleRate: "48000",
segmentTime: 10,
includeMetadata: true,
...params,
}
}
events = new EventEmitter()
buildCommand = () => {
const cmdStr = [
this.bin,
`-v quiet -stats`,
`-i ${this.input}`,
`-c:a ${this.audioCodec}`,
buildSegmentationArgs = () => {
const args = [
//`-threads 1`, // limits to one thread
`-v error -hide_banner -progress pipe:1`,
`-i ${this.params.input}`,
`-c:a ${this.params.audioCodec}`,
`-map 0:a`,
`-map_metadata -1`,
`-f dash`,
`-dash_segment_type mp4`,
`-segment_time ${this.segmentTime}`,
`-segment_time ${this.params.segmentTime}`,
`-use_template 1`,
`-use_timeline 1`,
`-init_seg_name "init.m4s"`,
]
if (typeof this.audioBitrate !== "undefined") {
cmdStr.push(`-b:a ${this.audioBitrate}`)
if (this.params.includeMetadata === false) {
args.push(`-map_metadata -1`)
}
if (typeof this.audioSampleRate !== "undefined") {
cmdStr.push(`-ar ${this.audioSampleRate}`)
if (
typeof this.params.audioBitrate === "string" &&
this.params.audioBitrate !== "default"
) {
args.push(`-b:a ${this.params.audioBitrate}`)
}
cmdStr.push(this.outputMasterName)
return cmdStr.join(" ")
if (
typeof this.params.audioSampleRate !== "undefined" &&
this.params.audioSampleRate !== "default"
) {
args.push(`-ar ${this.params.audioSampleRate}`)
}
run = () => {
const cmdStr = this.buildCommand()
args.push(this.params.outputMasterName)
console.log(cmdStr)
const cwd = `${path.dirname(this.input)}/dash`
if (!fs.existsSync(cwd)) {
fs.mkdirSync(cwd, { recursive: true })
return args
}
console.log(`[DASH] Started audio segmentation`, {
input: this.input,
cwd: cwd,
run = async () => {
const segmentationCmd = this.buildSegmentationArgs()
const outputPath =
this.params.outputDir ?? `${path.dirname(this.params.input)}/dash`
const outputFile = path.join(outputPath, this.params.outputMasterName)
this.emit("start", {
input: this.params.input,
output: outputPath,
params: this.params,
})
const process = exec(
cmdStr,
{
cwd: cwd,
if (!fs.existsSync(outputPath)) {
fs.mkdirSync(outputPath, { recursive: true })
}
const inputProbe = await Utils.probe(this.params.input)
try {
const result = await this.ffmpeg({
args: segmentationCmd,
onProcess: (process) => {
this.handleProgress(
process.stdout,
parseFloat(inputProbe.format.duration),
(progress) => {
this.emit("progress", progress)
},
(error, stdout, stderr) => {
if (error) {
console.log(`[DASH] Failed to segment audio >`, error)
return this.events.emit("error", error)
}
if (stderr) {
//return this.events.emit("error", stderr)
}
console.log(`[DASH] Finished segmenting audio >`, cwd)
return this.events.emit("end", {
filepath: path.join(cwd, this.outputMasterName),
isDirectory: true,
})
}
)
process.stdout.on("data", (data) => {
console.log(data.toString())
},
cwd: outputPath,
})
}
on = (key, cb) => {
this.events.on(key, cb)
return this
let outputProbe = await Utils.probe(outputFile)
this.emit("end", {
probe: {
input: inputProbe,
output: outputProbe,
},
outputPath: outputPath,
outputFile: outputFile,
})
return result
} catch (err) {
return this.emit("error", err)
}
}
}