71 lines · 2.7 KB
import * as z from "zod";

export const VIDEO_PREFIX = "/videos/";
export const VIDEO_R2_PREFIX = "videos/";
export const SCRUB_MANIFEST = "scrub/manifest.json";

const idPattern = /^[a-z0-9][a-z0-9-]{0,63}$/;
const assetPattern =
  /^(?:master\.m3u8|poster\.jpg|captions\/[a-z]{2}\.vtt|scrub\/(?:manifest\.json|sheet-[0-9]{3}\.webp)|[1-9][0-9]{0,3}\/(?:index\.m3u8|init\.mp4|[0-9]{5}\.m4s))$/;

export interface VideoPath {
  id: string;
  version: string;
  asset: string;
}

/** `/videos/<id>/<version>/<asset>`; anything outside the prepared layout is refused before touching R2. */
export function parseVideoPath(pathname: string): VideoPath | null {
  if (!pathname.startsWith(VIDEO_PREFIX)) return null;
  const [id, version, ...rest] = pathname.slice(VIDEO_PREFIX.length).split("/");
  const asset = rest.join("/");
  if (!id || !version || !idPattern.test(id) || !idPattern.test(version) || !assetPattern.test(asset)) return null;
  return { id, version, asset };
}

export function contentTypeForAsset(asset: string): string {
  if (asset.endsWith(".m3u8")) return "application/vnd.apple.mpegurl";
  if (asset.endsWith(".jpg")) return "image/jpeg";
  if (asset.endsWith(".webp")) return "image/webp";
  if (asset.endsWith(".vtt")) return "text/vtt; charset=utf-8";
  if (asset.endsWith(".json")) return "application/json";
  return "video/mp4";
}

export function videoR2Key({ id, version, asset }: VideoPath): string {
  return `${VIDEO_R2_PREFIX}${id}/${version}/${asset}`;
}

export function videoUrl(id: string, version: string, asset: string): string {
  return `${VIDEO_PREFIX}${id}/${version}/${asset}`;