/** * Serves an audio file dump's waveform, a sub-resource of its dump, for the * player to seek on. The frontend builds the URL with `dumpWaveformUrl`. * lib/ffmpeg.ts makes it from the upload on first request and keeps it, so a * listener downloads a few hundred bytes of peaks rather than the file a * second time to decode it. * * - `GET /api/dumps/:dumpId/waveform?v=&token=` — public; a private dump's * waveform needs its poster's media `token`. Answers a `DumpWaveform`; 404 * when the dump isn't an audio file or its audio can't be read. * * @module */ import { Router } from "@oak/oak"; import { APIException } from "../lib/api-exception.ts"; import { DUMPS_DIR } from "../config.ts"; import { resolveMediaDump } from "../lib/dump-media.ts"; import { audioWaveform, ensureGenerated, generatedPaths, } from "../lib/ffmpeg.ts"; import { serveFile } from "../lib/serve-file.ts"; /** * Mounted by `api/main.ts` behind the error, Open Graph and CORS middleware. * An `APIException` thrown by a handler becomes the JSON error response there. */ const router: Router = new Router({ prefix: "/api/dumps" }); router.get("/:dumpId/waveform", async (ctx) => { const { dump, cacheControl } = await resolveMediaDump(ctx, ctx.params.dumpId); if (dump.kind !== "file" || !dump.fileMime?.startsWith("audio/")) { throw new APIException(404, "No waveform for this dump"); } const outPath = generatedPaths(dump.id).waveform; const srcPath = `${DUMPS_DIR}/${dump.id}`; if ( !await ensureGenerated(outPath, () => audioWaveform(srcPath, outPath)) || !await serveFile(ctx, outPath, { mime: "application/json", cacheControl }) ) { throw new APIException(404, "Could not generate waveform"); } }); export default router;