SrGooglo a478432d61 Implement music sync room and refine related features
- Add WebSocket-based sync room for real-time music playback sync.
- Expand music exploration search to include albums and artists.
- Adjust track and release data fetching and deletion on server.
- Enhance DASH segmentation job with codec overrides and MPD updates.
- Update music service configuration for websockets and middlewares.
- Make minor UI adjustments to the search component.
2025-05-21 19:04:59 +00:00

224 lines
5.1 KiB
JavaScript

import fs from "node:fs"
import path from "node:path"
import { FFMPEGLib, Utils } from "../FFMPEGLib"
const codecOverrides = {
wav: "flac",
}
export default class SegmentedAudioMPDJob extends FFMPEGLib {
constructor(params = {}) {
super()
this.params = {
outputMasterName: "master.mpd",
audioCodec: "libopus",
audioBitrate: "320k",
audioSampleRate: "48000",
segmentTime: 10,
minBufferTime: 5,
includeMetadata: true,
...params,
}
}
buildSegmentationArgs = () => {
const args = [
`-v error -hide_banner -progress pipe:1`,
`-i ${this.params.input}`,
`-c:a ${this.params.audioCodec}`,
`-map 0:a`,
`-f dash`,
`-segment_time ${this.params.segmentTime}`,
`-use_template 1`,
`-use_timeline 1`,
//`-dash_segment_type mp4`,
//`-init_seg_name "init.m4s"`,
]
if (this.params.includeMetadata === false) {
args.push(`-map_metadata -1`)
}
if (
typeof this.params.audioBitrate === "string" &&
this.params.audioBitrate !== "default"
) {
args.push(`-b:a ${this.params.audioBitrate}`)
}
if (
typeof this.params.audioSampleRate !== "undefined" &&
this.params.audioSampleRate !== "default"
) {
args.push(`-ar ${this.params.audioSampleRate}`)
}
args.push(this.params.outputMasterName)
return args
}
_updateMpdMinBufferTime = async (mpdPath, newMinBufferTimeSecs) => {
try {
const mpdTagRegex = /(<MPD[^>]*)/
let mpdContent = await fs.promises.readFile(mpdPath, "utf-8")
const minBufferTimeAttribute = `minBufferTime="PT${newMinBufferTimeSecs}.0S"`
const existingMinBufferTimeRegex =
/(<MPD[^>]*minBufferTime=")[^"]*(")/
if (existingMinBufferTimeRegex.test(mpdContent)) {
mpdContent = mpdContent.replace(
existingMinBufferTimeRegex,
`$1PT${newMinBufferTimeSecs}.0S$2`,
)
await fs.promises.writeFile(mpdPath, mpdContent, "utf-8")
} else {
if (mpdTagRegex.test(mpdContent)) {
mpdContent = mpdContent.replace(
mpdTagRegex,
`$1 ${minBufferTimeAttribute}`,
)
await fs.promises.writeFile(mpdPath, mpdContent, "utf-8")
}
}
} catch (error) {
console.error(
`[SegmentedAudioMPDJob] Error updating MPD minBufferTime for ${mpdPath}:`,
error,
)
}
}
_updateMpdBandwidthAndSamplingRate = async ({
mpdPath,
bandwidth,
samplingRate,
} = {}) => {
try {
let mpdContent = await fs.promises.readFile(mpdPath, "utf-8")
// Regex to find all <Representation ...> tags
const representationRegex = /(<Representation\b[^>]*)(>)/g
mpdContent = mpdContent.replace(
representationRegex,
(match, startTag, endTag) => {
// Remove existing bandwidth and audioSamplingRate attributes if present
let newTag = startTag
.replace(/\sbandwidth="[^"]*"/, "")
.replace(/\saudioSamplingRate="[^"]*"/, "")
// Add new attributes
newTag += ` bandwidth="${bandwidth}" audioSamplingRate="${samplingRate}"`
return newTag + endTag
},
)
await fs.promises.writeFile(mpdPath, mpdContent, "utf-8")
} catch (error) {
console.error(
`[SegmentedAudioMPDJob] Error updating MPD bandwidth/audioSamplingRate for ${mpdPath}:`,
error,
)
}
}
run = async () => {
const outputPath =
this.params.outputDir ?? `${path.dirname(this.params.input)}/dash`
const outputFile = path.join(outputPath, this.params.outputMasterName)
try {
this.emit("start", {
input: this.params.input,
output: outputPath,
params: this.params,
})
const inputProbe = await Utils.probe(this.params.input)
if (
this.params.audioCodec === "copy" &&
codecOverrides[inputProbe.format.format_name]
) {
this.params.audioCodec =
codecOverrides[inputProbe.format.format_name]
}
const segmentationCmd = this.buildSegmentationArgs()
if (!fs.existsSync(outputPath)) {
fs.mkdirSync(outputPath, { recursive: true })
}
const ffmpegResult = 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(outputFile)) {
await this._updateMpdMinBufferTime(
outputFile,
this.params.minBufferTime,
)
} else {
console.warn(
`[SegmentedAudioMPDJob] MPD file ${outputFile} not found after ffmpeg run. Skipping minBufferTime update.`,
)
}
let outputProbe = await Utils.probe(outputFile)
let bandwidth = null
let samplingRate = null
if (
outputProbe &&
outputProbe.streams &&
outputProbe.streams.length > 0
) {
bandwidth =
outputProbe.format.bit_rate ??
outputProbe.streams[0].bit_rate
samplingRate = outputProbe.streams[0].sample_rate
}
if (bandwidth && samplingRate) {
await this._updateMpdBandwidthAndSamplingRate({
mpdPath: outputFile,
bandwidth: bandwidth,
samplingRate: samplingRate,
})
}
this.emit("end", {
probe: {
input: inputProbe,
output: outputProbe,
},
outputPath: outputPath,
outputFile: outputFile,
})
return ffmpegResult
} catch (err) {
this.emit("error", err)
}
}
}