mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 10:34:17 +00:00
Refactor SegmentedAudioMPDJob to extend FFMPEGLib
This commit is contained in:
parent
f62e885c65
commit
80acb13912
110
packages/server/classes/FFMPEGLib/index.js
Normal file
110
packages/server/classes/FFMPEGLib/index.js
Normal 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
|
@ -1,112 +1,108 @@
|
|||||||
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 SegmentedAudioMPDJob {
|
import { FFMPEGLib, Utils } from "../FFMPEGLib"
|
||||||
constructor({
|
|
||||||
input,
|
|
||||||
outputDir,
|
|
||||||
outputMasterName = "master.mpd",
|
|
||||||
|
|
||||||
audioCodec = "aac",
|
export default class SegmentedAudioMPDJob extends FFMPEGLib {
|
||||||
audioBitrate = undefined,
|
constructor(params = {}) {
|
||||||
audioSampleRate = undefined,
|
super()
|
||||||
segmentTime = 10,
|
|
||||||
}) {
|
|
||||||
this.input = input
|
|
||||||
this.outputDir = outputDir
|
|
||||||
this.outputMasterName = outputMasterName
|
|
||||||
|
|
||||||
this.audioCodec = audioCodec
|
this.params = {
|
||||||
this.audioBitrate = audioBitrate
|
outputMasterName: "master.mpd",
|
||||||
this.segmentTime = segmentTime
|
audioCodec: "libopus",
|
||||||
this.audioSampleRate = audioSampleRate
|
audioBitrate: "320k",
|
||||||
|
audioSampleRate: "48000",
|
||||||
|
segmentTime: 10,
|
||||||
|
includeMetadata: true,
|
||||||
|
...params,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.bin = require("ffmpeg-static")
|
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`,
|
||||||
|
`-f dash`,
|
||||||
|
`-dash_segment_type mp4`,
|
||||||
|
`-segment_time ${this.params.segmentTime}`,
|
||||||
|
`-use_template 1`,
|
||||||
|
`-use_timeline 1`,
|
||||||
|
`-init_seg_name "init.m4s"`,
|
||||||
|
]
|
||||||
|
|
||||||
return this
|
if (this.params.includeMetadata === false) {
|
||||||
}
|
args.push(`-map_metadata -1`)
|
||||||
|
}
|
||||||
|
|
||||||
events = new EventEmitter()
|
if (
|
||||||
|
typeof this.params.audioBitrate === "string" &&
|
||||||
|
this.params.audioBitrate !== "default"
|
||||||
|
) {
|
||||||
|
args.push(`-b:a ${this.params.audioBitrate}`)
|
||||||
|
}
|
||||||
|
|
||||||
buildCommand = () => {
|
if (
|
||||||
const cmdStr = [
|
typeof this.params.audioSampleRate !== "undefined" &&
|
||||||
this.bin,
|
this.params.audioSampleRate !== "default"
|
||||||
`-v quiet -stats`,
|
) {
|
||||||
`-i ${this.input}`,
|
args.push(`-ar ${this.params.audioSampleRate}`)
|
||||||
`-c:a ${this.audioCodec}`,
|
}
|
||||||
`-map 0:a`,
|
|
||||||
`-map_metadata -1`,
|
|
||||||
`-f dash`,
|
|
||||||
`-dash_segment_type mp4`,
|
|
||||||
`-segment_time ${this.segmentTime}`,
|
|
||||||
`-use_template 1`,
|
|
||||||
`-use_timeline 1`,
|
|
||||||
`-init_seg_name "init.m4s"`,
|
|
||||||
]
|
|
||||||
|
|
||||||
if (typeof this.audioBitrate !== "undefined") {
|
args.push(this.params.outputMasterName)
|
||||||
cmdStr.push(`-b:a ${this.audioBitrate}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof this.audioSampleRate !== "undefined") {
|
return args
|
||||||
cmdStr.push(`-ar ${this.audioSampleRate}`)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
cmdStr.push(this.outputMasterName)
|
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)
|
||||||
|
|
||||||
return cmdStr.join(" ")
|
this.emit("start", {
|
||||||
}
|
input: this.params.input,
|
||||||
|
output: outputPath,
|
||||||
|
params: this.params,
|
||||||
|
})
|
||||||
|
|
||||||
run = () => {
|
if (!fs.existsSync(outputPath)) {
|
||||||
const cmdStr = this.buildCommand()
|
fs.mkdirSync(outputPath, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
console.log(cmdStr)
|
const inputProbe = await Utils.probe(this.params.input)
|
||||||
|
|
||||||
const cwd = `${path.dirname(this.input)}/dash`
|
try {
|
||||||
|
const result = await this.ffmpeg({
|
||||||
|
args: segmentationCmd,
|
||||||
|
onProcess: (process) => {
|
||||||
|
this.handleProgress(
|
||||||
|
process.stdout,
|
||||||
|
parseFloat(inputProbe.format.duration),
|
||||||
|
(progress) => {
|
||||||
|
this.emit("progress", progress)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cwd: outputPath,
|
||||||
|
})
|
||||||
|
|
||||||
if (!fs.existsSync(cwd)) {
|
let outputProbe = await Utils.probe(outputFile)
|
||||||
fs.mkdirSync(cwd, { recursive: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[DASH] Started audio segmentation`, {
|
this.emit("end", {
|
||||||
input: this.input,
|
probe: {
|
||||||
cwd: cwd,
|
input: inputProbe,
|
||||||
})
|
output: outputProbe,
|
||||||
|
},
|
||||||
|
outputPath: outputPath,
|
||||||
|
outputFile: outputFile,
|
||||||
|
})
|
||||||
|
|
||||||
const process = exec(
|
return result
|
||||||
cmdStr,
|
} catch (err) {
|
||||||
{
|
return this.emit("error", err)
|
||||||
cwd: cwd,
|
}
|
||||||
},
|
}
|
||||||
(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())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
on = (key, cb) => {
|
|
||||||
this.events.on(key, cb)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user