88 lines · 3.7 KB
import { env, waitUntil } from "cloudflare:workers";
import { contentTypeForAsset, parseVideoPath, videoR2Key } from "~/lib/video";

const IMMUTABLE = "public, max-age=31536000, immutable";
const RANGE = /^bytes=(\d*)-(\d*)$/;

function text(request: Request, status: number, message: string, extra: Record<string, string> = {}): Response {
  return new Response(request.method === "HEAD" ? null : message, {
    status,
    headers: { "content-type": "text/plain", "cache-control": "no-store", ...extra },
  });
}

function parseRange(header: string | null): R2Range | undefined | null {
  if (!header) return undefined;
  const match = RANGE.exec(header.trim());
  if (!match || (!match[1] && !match[2])) return null;
  if (!match[1]) return { suffix: Number(match[2]) };
  const offset = Number(match[1]);
  if (!match[2]) return { offset };
  const end = Number(match[2]);
  return end < offset ? null : { offset, length: end - offset + 1 };
}

function assetHeaders(asset: string, object: R2Object): Headers {
  return new Headers({
    "content-type": contentTypeForAsset(asset),
    "cache-control": IMMUTABLE,
    etag: object.httpEtag,
    "accept-ranges": "bytes",
    "x-content-type-options": "nosniff",
  });
}

/**
 * Prepared HLS assets live in R2 under `videos/<id>/<version>/`. Each version is immutable, so the colo cache
 * holds full objects forever; Range and If-None-Match are answered by the cache on a hit. A ranged miss reads
 * just that range from R2 and is not cached, the next full fetch fills the cache.
 */
export async function handleVideoRoutes(request: Request, url: URL): Promise<Response | null> {