mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
feat(bunny-cdn): refactor CDN hostname resolution and update asset URLs for improved flexibility
This commit is contained in:
+9
-6
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
||||
import Hls from 'hls.js';
|
||||
import Link from 'next/link';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { resolvePublicBunnyCdnHostname } from '@/lib/bunny-cdn';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface Version {
|
||||
@@ -99,8 +100,6 @@ const isSafeUrl = (url: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const BUNNY_PULL_ZONE_HOSTNAME = 'vz-965f4f4a-fc1.b-cdn.net';
|
||||
|
||||
export default function CompareVersionsPageClient({ projectId, videoId }: { projectId: string; videoId: string }) {
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
@@ -867,6 +866,7 @@ function BunnyPanel({
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const hlsRef = useRef<Hls | null>(null);
|
||||
const bunnyCdnHostname = useMemo(() => resolvePublicBunnyCdnHostname(), []);
|
||||
const [portraitFrameWidth, setPortraitFrameWidth] = useState<number>(0);
|
||||
const [isPortraitSource, setIsPortraitSource] = useState(false);
|
||||
|
||||
@@ -980,8 +980,11 @@ function BunnyPanel({
|
||||
const onPlay = () => { isPlaying = true; };
|
||||
const onPause = () => { isPlaying = false; };
|
||||
const onEnded = () => { isPlaying = false; };
|
||||
const hlsUrl = `https://${BUNNY_PULL_ZONE_HOSTNAME}/${version.videoId}/playlist.m3u8`;
|
||||
const originalUrl = `https://${BUNNY_PULL_ZONE_HOSTNAME}/${version.videoId}/original`;
|
||||
if (!bunnyCdnHostname) {
|
||||
return;
|
||||
}
|
||||
const hlsUrl = `https://${bunnyCdnHostname}/${version.videoId}/playlist.m3u8`;
|
||||
const originalUrl = `https://${bunnyCdnHostname}/${version.videoId}/original`;
|
||||
const activateOriginalFallback = (): void => {
|
||||
sourceMode = 'original';
|
||||
clearRetryTimer();
|
||||
@@ -1088,7 +1091,7 @@ function BunnyPanel({
|
||||
onUnregister(version.id);
|
||||
adapter.destroy();
|
||||
};
|
||||
}, [version.id, version.videoId, onRegister, onUnregister]);
|
||||
}, [version.id, version.videoId, onRegister, onUnregister, bunnyCdnHostname]);
|
||||
|
||||
return (
|
||||
<div ref={panelRef} className="relative w-full h-full group flex items-center justify-center bg-black">
|
||||
|
||||
@@ -12,10 +12,12 @@ import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { parseVideoUrl, fetchVideoMetadata, getThumbnailUrl, type VideoSource } from '@/lib/video-providers';
|
||||
import { resolvePublicBunnyCdnHostname } from '@/lib/bunny-cdn';
|
||||
import * as tus from 'tus-js-client';
|
||||
|
||||
export default function NewVideoPageClient({ projectId }: { projectId: string }) {
|
||||
const router = useRouter();
|
||||
const bunnyCdnHostname = resolvePublicBunnyCdnHostname();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isFetchingMeta, setIsFetchingMeta] = useState(false);
|
||||
@@ -301,7 +303,9 @@ export default function NewVideoPageClient({ projectId }: { projectId: string })
|
||||
finalVideoId = bunnyData.videoId;
|
||||
// Bunny will generate thumbnails automatically after processing.
|
||||
// We'll just provide the standard CDN thumbnail URL format as fallback.
|
||||
finalThumbnailUrl = `https://vz-thumbnail.b-cdn.net/${bunnyData.videoId}/thumbnail.jpg`;
|
||||
finalThumbnailUrl = bunnyCdnHostname
|
||||
? `https://${bunnyCdnHostname}/${bunnyData.videoId}/thumbnail.jpg`
|
||||
: null;
|
||||
}
|
||||
|
||||
// Final POST to our database
|
||||
|
||||
@@ -4,6 +4,7 @@ import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response
|
||||
import { rateLimit } from '@/lib/rate-limit';
|
||||
import { validateShareLinkAccess } from '@/lib/share-links';
|
||||
import { getShareSessionFromRequest } from '@/lib/share-session';
|
||||
import { resolveServerBunnyCdnHostname } from '@/lib/bunny-cdn';
|
||||
import { NextRequest } from 'next/server';
|
||||
import { DownloadEgressSource } from '@prisma/client';
|
||||
|
||||
@@ -17,7 +18,6 @@ type BunnyDownloadSource = {
|
||||
|
||||
const BUNNY_DOWNLOAD_FALLBACK_HEIGHTS = [2160, 1440, 1080, 720, 480, 360, 240];
|
||||
const BUNNY_ALLOWED_QUALITIES = new Set(BUNNY_DOWNLOAD_FALLBACK_HEIGHTS);
|
||||
const DEFAULT_BUNNY_CDN_HOSTNAME = 'vz-965f4f4a-fc1.b-cdn.net';
|
||||
const BUNNY_MAX_PROBE_CANDIDATES = 4;
|
||||
const BUNNY_SOURCE_RESOLUTION_CACHE_TTL_MS = 60 * 1000;
|
||||
const BUNNY_REMOTE_FETCH_TIMEOUT_MS = 8 * 1000;
|
||||
@@ -65,20 +65,14 @@ function buildContentDisposition(fileNameWithExt: string): string {
|
||||
return `attachment; filename="${asciiFallback}"; filename*=UTF-8''${encoded}`;
|
||||
}
|
||||
|
||||
function resolveBunnyCdnHostname(): string {
|
||||
const raw = process.env.BUNNY_CDN_URL || process.env.NEXT_PUBLIC_BUNNY_CDN_URL;
|
||||
if (!raw) return DEFAULT_BUNNY_CDN_HOSTNAME;
|
||||
|
||||
try {
|
||||
const url = new URL(raw);
|
||||
return url.hostname || DEFAULT_BUNNY_CDN_HOSTNAME;
|
||||
} catch {
|
||||
return raw.replace(/^https?:\/\//, '').replace(/\/+$/, '') || DEFAULT_BUNNY_CDN_HOSTNAME;
|
||||
}
|
||||
function resolveBunnyCdnHostname(): string | null {
|
||||
return resolveServerBunnyCdnHostname();
|
||||
}
|
||||
|
||||
function buildBunnyOriginalUrl(videoId: string): string {
|
||||
return `https://${resolveBunnyCdnHostname()}/${videoId}/original`;
|
||||
const hostname = resolveBunnyCdnHostname();
|
||||
if (!hostname) return '';
|
||||
return `https://${hostname}/${videoId}/original`;
|
||||
}
|
||||
|
||||
function buildBunnySourceCacheKey(
|
||||
@@ -140,6 +134,7 @@ async function isRemoteFileAvailable(url: string): Promise<boolean> {
|
||||
|
||||
async function resolveHighestBunnyMp4Url(videoId: string): Promise<string> {
|
||||
const hostname = resolveBunnyCdnHostname();
|
||||
if (!hostname) return '';
|
||||
const playlistUrl = `https://${hostname}/${videoId}/playlist.m3u8`;
|
||||
|
||||
let playlistHeights: number[] = [];
|
||||
@@ -172,6 +167,7 @@ async function resolveHighestBunnyMp4Url(videoId: string): Promise<string> {
|
||||
|
||||
async function resolveBunnyOriginalSource(videoId: string): Promise<BunnyDownloadSource | null> {
|
||||
const originalUrl = buildBunnyOriginalUrl(videoId);
|
||||
if (!originalUrl) return null;
|
||||
if (await isRemoteFileAvailable(originalUrl)) {
|
||||
return {
|
||||
sourceType: 'original',
|
||||
@@ -184,8 +180,17 @@ async function resolveBunnyOriginalSource(videoId: string): Promise<BunnyDownloa
|
||||
}
|
||||
|
||||
async function resolveBunnyCompressedSource(videoId: string, requestedQuality: number | null): Promise<BunnyDownloadSource> {
|
||||
const hostname = resolveBunnyCdnHostname();
|
||||
if (!hostname) {
|
||||
return {
|
||||
sourceType: 'compressed',
|
||||
quality: null,
|
||||
url: '',
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof requestedQuality === 'number' && Number.isFinite(requestedQuality) && requestedQuality > 0) {
|
||||
const requestedUrl = `https://${resolveBunnyCdnHostname()}/${videoId}/play_${requestedQuality}p.mp4`;
|
||||
const requestedUrl = `https://${hostname}/${videoId}/play_${requestedQuality}p.mp4`;
|
||||
if (await isRemoteFileAvailable(requestedUrl)) {
|
||||
return {
|
||||
sourceType: 'compressed',
|
||||
@@ -196,6 +201,13 @@ async function resolveBunnyCompressedSource(videoId: string, requestedQuality: n
|
||||
}
|
||||
|
||||
const fallbackUrl = await resolveHighestBunnyMp4Url(videoId);
|
||||
if (!fallbackUrl) {
|
||||
return {
|
||||
sourceType: 'compressed',
|
||||
quality: null,
|
||||
url: '',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
sourceType: 'compressed',
|
||||
@@ -209,6 +221,8 @@ async function resolveBunnyDownloadSource(
|
||||
requestedQuality: number | null,
|
||||
sourcePreference: BunnyDownloadSourcePreference
|
||||
): Promise<BunnyDownloadSource | null> {
|
||||
if (!resolveBunnyCdnHostname()) return null;
|
||||
|
||||
const now = Date.now();
|
||||
const cacheKey = buildBunnySourceCacheKey(videoId, requestedQuality, sourcePreference);
|
||||
const cached = getCachedBunnyDownloadSource(cacheKey, now);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { deriveGuestUploadContext, verifyGuestUploadToken } from '@/lib/guest-up
|
||||
import { ensureGuestIdentityFromRequest, setGuestIdentityCookie } from '@/lib/guest-identity';
|
||||
import { getShareSessionFromRequest } from '@/lib/share-session';
|
||||
import { validateUrl, validateOptionalUrl } from '@/lib/validation';
|
||||
import { resolveServerBunnyCdnHostname } from '@/lib/bunny-cdn';
|
||||
import {
|
||||
SAFE_BUNNY_VIDEO_ID,
|
||||
SAFE_IMAGE_PROXY_PATH,
|
||||
@@ -26,11 +27,6 @@ type RouteParams = { params: Promise<{ videoId: string }> };
|
||||
const UNATTACHED_UPLOAD_TTL_MS = 15 * 60 * 1000;
|
||||
const ASSET_LIST_DEFAULT_LIMIT = 40;
|
||||
const ASSET_LIST_MAX_LIMIT = 100;
|
||||
const BUNNY_ALLOWED_THUMBNAIL_HOSTS = new Set([
|
||||
'iframe.mediadelivery.net',
|
||||
'video.bunnycdn.com',
|
||||
'vz-965f4f4a-fc1.b-cdn.net',
|
||||
]);
|
||||
const YOUTUBE_TITLE_CACHE_TTL_MS = 5 * 60 * 1000;
|
||||
|
||||
type AssetWithViewerFields = {
|
||||
@@ -62,10 +58,19 @@ type YouTubeTitleCacheRecord = {
|
||||
const youtubeTitleCache = new Map<string, YouTubeTitleCacheRecord>();
|
||||
|
||||
function isAllowedBunnyMediaUrl(url: string): boolean {
|
||||
const allowedHosts = new Set<string>([
|
||||
'iframe.mediadelivery.net',
|
||||
'video.bunnycdn.com',
|
||||
]);
|
||||
const bunnyCdnHostname = resolveServerBunnyCdnHostname();
|
||||
if (bunnyCdnHostname) {
|
||||
allowedHosts.add(bunnyCdnHostname);
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
if (parsed.protocol !== 'https:') return false;
|
||||
return BUNNY_ALLOWED_THUMBNAIL_HOSTS.has(parsed.hostname);
|
||||
return allowedHosts.has(parsed.hostname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
@@ -330,7 +335,10 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
|
||||
|
||||
displayName = sanitizeAssetDisplayName(requestedDisplayName, `Bunny ${providerVideoId}`);
|
||||
if (!thumbnailUrl) {
|
||||
thumbnailUrl = `https://vz-965f4f4a-fc1.b-cdn.net/${providerVideoId}/thumbnail.jpg`;
|
||||
const bunnyCdnHostname = resolveServerBunnyCdnHostname();
|
||||
if (bunnyCdnHostname) {
|
||||
thumbnailUrl = `https://${bunnyCdnHostname}/${providerVideoId}/thumbnail.jpg`;
|
||||
}
|
||||
}
|
||||
kind = 'VIDEO';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user