Add DASH MPD parsing and MP4 to FLAC conversion functionality

This commit is contained in:
Seth 2025-04-20 04:53:40 -04:00
parent 34e6265cae
commit bb570387fd
4 changed files with 64 additions and 9 deletions

View file

@ -2,6 +2,8 @@ import { FlacStreamTagger } from "flac-stream-tagger";
import Ffmpeg from "fluent-ffmpeg";
import ffmpegPath from "ffmpeg-static";
import { PassThrough } from "stream";
import { DashMPD } from '@liveinstantly/dash-mpd-parser';
Ffmpeg.setFfmpegPath(ffmpegPath as string);
@ -20,13 +22,33 @@ export default class {
await new Promise((resolve, reject) => {
Ffmpeg(inputStream)
.audioCodec("flac")
.format("flac")
.on("error", reject)
.on("end", resolve)
.pipe()
.on("data", chunk => outputChunks.push(chunk));
});
return Buffer.concat(outputChunks);;
}
async convertMp4ToFlac(buffer: Buffer) {
const inputStream = new PassThrough();
inputStream.end(buffer);
const outputChunks: Buffer[] = [];
await new Promise((resolve, reject) => {
Ffmpeg(inputStream)
.audioCodec("flac")
.format("flac")
.on("error", reject)
.on("end", () => resolve(Buffer.concat(outputChunks)))
.pipe()
.on("data", chunk => outputChunks.push(chunk));
});
return Buffer.concat(outputChunks);
}
async fetchTrack(id: number) {
@ -62,19 +84,30 @@ export default class {
async downloadFlac(manifestMimeType: string, manifest: string) {
const id = Bun.nanoseconds().toString(36);
if (manifestMimeType === "application/dash+xml") {
await Bun.write(`tmp/${id}.mpd`, Buffer.from(manifest, "base64").toString("utf-8"))
const mpd = new DashMPD();
mpd.parse(Buffer.from(manifest, "base64").toString("utf-8"))
await Bun.$`${ffmpegPath} -protocol_whitelist https,file,tls,tcp -i tmp/${id}.mpd -c:a copy tmp/${id}.flac`.quiet()
const Representation = mpd.getJSON().MPD.Period[0].AdaptationSet[0].Representation[0];
const segmentTemplate = Representation.SegmentTemplate;
const segmentCount = segmentTemplate.SegmentTimeline.S[0]["@r"];
const segmentBuffers = [];
await Bun.file(`tmp/${id}.mpd`).delete();
const initialization = await fetch(segmentTemplate["@initialization"])
const initializationBuffer = await initialization.arrayBuffer()
segmentBuffers.push(Buffer.from(initializationBuffer));
const flac = Bun.file(`tmp/${id}.flac`)
const segmentPromises = [];
for (let i = 0; i < 1 + segmentCount; i++) {
const segmentUrl = segmentTemplate["@media"].replace("$Number$", segmentTemplate["@startNumber"] + i);
segmentPromises.push(fetch(segmentUrl).then(segment => segment.arrayBuffer()).then(segmentBuffer => Buffer.from(segmentBuffer)));
}
const audioBuffer = await flac.arrayBuffer();
const segmentBuffersArray = await Promise.all(segmentPromises);
segmentBuffers.push(...segmentBuffersArray);
await flac.delete()
const flac = await this.convertMp4ToFlac(Buffer.concat(segmentBuffers))
return { buffer: audioBuffer, mimeType: "audio/flac" }
return { buffer: flac, mimeType: "audio/flac" }
} else if (manifestMimeType === "application/vnd.tidal.bts") {
const data = JSON.parse(Buffer.from(manifest, "base64").toString("utf-8")) as {
"mimeType": string,