mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 10:34:17 +00:00
- 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.
224 lines
5.1 KiB
JavaScript
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)
|
|
}
|
|
}
|
|
}
|