Add DASH MPD parsing and MP4 to FLAC conversion functionality
This commit is contained in:
parent
34e6265cae
commit
bb570387fd
4 changed files with 64 additions and 9 deletions
8
bun.lock
8
bun.lock
|
@ -4,11 +4,13 @@
|
|||
"": {
|
||||
"name": "bun-react-template",
|
||||
"dependencies": {
|
||||
"@liveinstantly/dash-mpd-parser": "^0.5.0",
|
||||
"@tidal-music/auth": "^1.3.4",
|
||||
"@types/fluent-ffmpeg": "^2.1.27",
|
||||
"ffmpeg-static": "^5.2.0",
|
||||
"flac-stream-tagger": "^1.0.10",
|
||||
"fluent-ffmpeg": "^2.1.3",
|
||||
"mp4box": "^0.5.4",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
|
@ -18,6 +20,8 @@
|
|||
"packages": {
|
||||
"@derhuerst/http-basic": ["@derhuerst/http-basic@8.2.4", "", { "dependencies": { "caseless": "^0.12.0", "concat-stream": "^2.0.0", "http-response-object": "^3.0.1", "parse-cache-control": "^1.0.1" } }, "sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw=="],
|
||||
|
||||
"@liveinstantly/dash-mpd-parser": ["@liveinstantly/dash-mpd-parser@0.5.0", "", { "dependencies": { "xmldom": "^0.6.0" } }, "sha512-ritwVw23l0lh4jeXLOtRitusbbAfg99Ztdz4rBg0HyGTZKwYC46FO8zotQFnZd9Vd+k8u9s90kwhf6PK91msKw=="],
|
||||
|
||||
"@tidal-music/auth": ["@tidal-music/auth@1.3.4", "", { "dependencies": { "@tidal-music/auth": "1.3.4", "@tidal-music/common": "^0.1.5", "@tidal-music/true-time": "^0.3.0" } }, "sha512-eN2tBqsj9L20RpNpxWSwfArIAk2rHK6LH2wtQK4OIN2u4pDJU1WbFdcK0owj3eoEzSgudYORErjPedbvsSrOgA=="],
|
||||
|
||||
"@tidal-music/common": ["@tidal-music/common@0.1.5", "", { "dependencies": { "@tidal-music/common": "0.1.5" } }, "sha512-C71JfsJ5P1CwObGTOlRH9vDjo4mruV5WeE6C0JmR8eVoivIaf1Ou9QCI7pDa7aWuCUjOOm3Dd5p5MkJ3aCDXHA=="],
|
||||
|
@ -62,6 +66,8 @@
|
|||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"mp4box": ["mp4box@0.5.4", "", {}, "sha512-GcCH0fySxBurJtvr0dfhz0IxHZjc1RP+F+I8xw+LIwkU1a+7HJx8NCDiww1I5u4Hz6g4eR1JlGADEGJ9r4lSfA=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"parse-cache-control": ["parse-cache-control@1.0.1", "", {}, "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg=="],
|
||||
|
@ -82,6 +88,8 @@
|
|||
|
||||
"which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="],
|
||||
|
||||
"xmldom": ["xmldom@0.6.0", "", {}, "sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg=="],
|
||||
|
||||
"http-response-object/@types/node": ["@types/node@10.17.60", "", {}, "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="],
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,11 +8,13 @@
|
|||
"start": "bun index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@liveinstantly/dash-mpd-parser": "^0.5.0",
|
||||
"@tidal-music/auth": "^1.3.4",
|
||||
"@types/fluent-ffmpeg": "^2.1.27",
|
||||
"ffmpeg-static": "^5.2.0",
|
||||
"flac-stream-tagger": "^1.0.10",
|
||||
"fluent-ffmpeg": "^2.1.3"
|
||||
"fluent-ffmpeg": "^2.1.3",
|
||||
"mp4box": "^0.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
|
|
|
@ -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,
|
||||
|
|
12
tmp.ts
Normal file
12
tmp.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
Bun.serve({
|
||||
routes: {
|
||||
"/test.mpd": async () => {
|
||||
return new Response(Bun.file("tmp/fpwp4o.mpd"), {
|
||||
headers: {
|
||||
// cors allow all
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
})
|
Loading…
Add table
Reference in a new issue