const VIDEO_MIME_TO_EXT: Record = { 'video/mp4': 'mp4', 'video/webm': 'webm', 'video/ogg': 'ogg', 'video/quicktime': 'mov', 'video/x-matroska': 'mkv', 'video/x-msvideo': 'avi', }; const EXT_TO_MIME: Record = { mp4: 'video/mp4', webm: 'video/webm', ogg: 'video/ogg', mov: 'video/quicktime', m4v: 'video/mp4', mkv: 'video/x-matroska', avi: 'video/x-msvideo', }; const ALLOWED_VIDEO_EXTENSIONS = new Set(Object.keys(EXT_TO_MIME)); export function normalizeVideoMime(mime: string | undefined): string | null { if (!mime) return null; const normalized = mime.split(';')[0]?.trim().toLowerCase() ?? ''; if (!normalized.startsWith('video/')) return null; return normalized; } export function getVideoExtensionFromMime(mime: string): string | null { return VIDEO_MIME_TO_EXT[mime] ?? null; } export function getVideoExtensionFromFileName(fileName: string): string | null { const ext = fileName.split('.').pop()?.toLowerCase(); if (!ext || !ALLOWED_VIDEO_EXTENSIONS.has(ext)) return null; return ext; } export function resolveVideoContentType(fileName: string, mime: string | undefined): string | null { const normalizedMime = normalizeVideoMime(mime); if (normalizedMime) { const extFromMime = getVideoExtensionFromMime(normalizedMime); const extFromName = getVideoExtensionFromFileName(fileName); if (extFromMime && extFromName && extFromMime !== extFromName) { return EXT_TO_MIME[extFromName] ?? normalizedMime; } return normalizedMime; } const ext = getVideoExtensionFromFileName(fileName); if (!ext) return null; return EXT_TO_MIME[ext] ?? null; } export function isAllowedVideoFile(fileName: string, mime: string | undefined): boolean { return resolveVideoContentType(fileName, mime) !== null; } export const VIDEO_OBJECT_KEY_PREFIX = 'videos/'; export const VIDEO_PROXY_PREFIX = '/api/upload/video/'; const SAFE_VIDEO_BASENAME = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.[a-z0-9]+$/i; export function buildVideoObjectKey(filename: string): string { return `${VIDEO_OBJECT_KEY_PREFIX}${filename}`; } export function videoProxyPathFromFilename(filename: string): string { return `${VIDEO_PROXY_PREFIX}${filename}`; } export function videoProxyPathToObjectKey(proxyPath: string): string | null { if (!proxyPath.startsWith(VIDEO_PROXY_PREFIX)) return null; const filename = proxyPath.slice(VIDEO_PROXY_PREFIX.length); if (!SAFE_VIDEO_BASENAME.test(filename)) return null; return buildVideoObjectKey(filename); } /** * Playback URL for a direct-upload (`r2`) version: media always streams through * the app's own upload route. Shared by the video page and the compare view so * the two cannot drift. */ export function resolveR2PlaybackUrl(version: { videoId: string; originalUrl: string }): string { if (version.originalUrl.startsWith(VIDEO_PROXY_PREFIX)) { return version.originalUrl; } if (version.originalUrl.startsWith(VIDEO_OBJECT_KEY_PREFIX)) { return videoProxyPathFromFilename(version.originalUrl.slice(VIDEO_OBJECT_KEY_PREFIX.length)); } if (version.videoId.startsWith(VIDEO_OBJECT_KEY_PREFIX)) { return videoProxyPathFromFilename(version.videoId.slice(VIDEO_OBJECT_KEY_PREFIX.length)); } return version.originalUrl; } /** * Guards what ends up in a `