mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 09:36:08 +00:00
refactor(video): share R2 playback URL resolution and guard drift resync
- move resolveR2PlaybackUrl into lib/video-upload-validation.ts so the compare view and the main video page cannot drift apart - validate the resolved URL with isPlayableVideoUrl before it reaches <video src> - add a per-player cooldown so a follower that cannot keep up is not seeked every second, which would stutter rather than correct
This commit is contained in:
@@ -57,6 +57,7 @@ export function isAllowedVideoFile(fileName: string, mime: string | undefined):
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -66,17 +67,50 @@ export function buildVideoObjectKey(filename: string): string {
|
||||
}
|
||||
|
||||
export function videoProxyPathFromFilename(filename: string): string {
|
||||
return `/api/upload/video/${filename}`;
|
||||
return `${VIDEO_PROXY_PREFIX}${filename}`;
|
||||
}
|
||||
|
||||
export function videoProxyPathToObjectKey(proxyPath: string): string | null {
|
||||
const prefix = '/api/upload/video/';
|
||||
if (!proxyPath.startsWith(prefix)) return null;
|
||||
const filename = proxyPath.slice(prefix.length);
|
||||
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 `<video src>`: proxy paths must be a well-formed
|
||||
* upload route, anything else must be plain http(s).
|
||||
*/
|
||||
export function isPlayableVideoUrl(url: string): boolean {
|
||||
if (url.startsWith(VIDEO_PROXY_PREFIX)) {
|
||||
return videoProxyPathToObjectKey(url) !== null;
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function objectKeyToVideoProxyPath(objectKey: string): string | null {
|
||||
if (!objectKey.startsWith(VIDEO_OBJECT_KEY_PREFIX)) return null;
|
||||
const filename = objectKey.slice(VIDEO_OBJECT_KEY_PREFIX.length);
|
||||
|
||||
Reference in New Issue
Block a user