feat(cloudflare): add linearvoid-media edge worker for R2 HLS delivery

Replace the ad-hoc `worker.js` with a self-contained `linearvoid-media`
worker under `infrastructure/cloudflare/` that owns `media.linearvoid.tv`.
It serves HLS objects from the R2 bucket, enforcing immutable Cache-Control
on segments, a short TTL on live manifests, plus HLS CORS, OPTIONS
preflight, Range support, and a `/health` probe. Includes wrapper config
with production domain, Vitest unit tests, Miniflare R2 integration tests,
automated deploy workflow, and docs updates.
This commit is contained in:
zwitschi committed 2026-09-03 08:52:31 +02:00
1 parent 5f1f796a20
commit 8c5e70959b
16 files changed
+3359 -42

No files matched your search

@@ -0,0 +1,77 @@
import { Miniflare } from "miniflare";
import { beforeAll, afterAll, describe, it, expect } from "vitest";
const IMMUTABLE = "public, max-age=31536000, immutable";
const MANIFEST = "public, max-age=1, s-maxage=2, must-revalidate";
// Real-workerd integration test: boots the worker with an in-memory R2 bucket and
// dispatches requests through the fetch handler.
let mf;
beforeAll(async () => {
mf = new Miniflare({
modules: true,
scriptPath: "src/worker.js",
r2Buckets: { MEDIA_BUCKET: "linearvoid-media" },
});
const bucket = await mf.getR2Bucket("MEDIA_BUCKET");
await bucket.put("hls/playlist.m3u8", "#EXTM3U\n#EXT-X-ENDLIST\n", {
httpMetadata: { contentType: "application/vnd.apple.mpegurl" },
});
await bucket.put("hls/seg-000001.m4s", new Uint8Array([0, 1, 2, 3, 4]));
await bucket.put("hls/init.mp4", new Uint8Array([9, 9, 9]));
});
afterAll(async () => {
await mf?.dispose();
});
const base = "http://media.linearvoid.tv";
describe("linearvoid-media worker (R2)", () => {
it("serves a media segment with immutable cache headers", async () => {
const res = await mf.dispatchFetch(`${base}/hls/seg-000001.m4s`);
expect(res.status).toBe(200);
expect(res.headers.get("cache-control")).toBe(IMMUTABLE);
expect(res.headers.get("content-type")).toBe("video/mp4");
expect(res.headers.get("access-control-allow-origin")).toBe("*");
expect(await res.arrayBuffer()).toEqual(new Uint8Array([0, 1, 2, 3, 4]).buffer);
});
it("serves a live manifest with a short TTL", async () => {
const res = await mf.dispatchFetch(`${base}/hls/playlist.m3u8`);
expect(res.status).toBe(200);
expect(res.headers.get("cache-control")).toBe(MANIFEST);
expect(res.headers.get("content-type")).toBe("application/vnd.apple.mpegurl");
});
it("honours Range requests with 206 + Content-Range", async () => {
const res = await mf.dispatchFetch(`${base}/hls/seg-000001.m4s`, {
headers: { Range: "bytes=0-2" },
});
expect(res.status).toBe(206);
expect(res.headers.get("content-range")).toBe("bytes 0-2/5");
const bytes = new Uint8Array(await res.arrayBuffer());
expect([...bytes]).toEqual([0, 1, 2]);
});
it("returns 404 for a missing object", async () => {
const res = await mf.dispatchFetch(`${base}/hls/does-not-exist.m4s`);
expect(res.status).toBe(404);
});
it("answers OPTIONS preflight with CORS headers", async () => {
const res = await mf.dispatchFetch(`${base}/hls/seg-000001.m4s`, {
method: "OPTIONS",
});
expect(res.status).toBe(204);
expect(res.headers.get("access-control-allow-origin")).toBe("*");
});
it("rejects non-GET/HEAD methods", async () => {
const res = await mf.dispatchFetch(`${base}/hls/seg-000001.m4s`, {
method: "POST",
});
expect(res.status).toBe(405);
});
});