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:
+36
-15
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import {
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { parseVideoUrl, fetchVideoMetadata, getThumbnailUrl, type VideoSource } from '@/lib/video-providers';
|
||||
import { resolvePublicBunnyCdnHostname } from '@/lib/bunny-cdn';
|
||||
|
||||
interface VideoCardProps {
|
||||
video: {
|
||||
@@ -86,6 +87,20 @@ export function VideoCard({ video, projectId, canManage, onDeleted }: VideoCardP
|
||||
// Delete dialog
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const bunnyCdnHostname = useMemo(() => resolvePublicBunnyCdnHostname(), []);
|
||||
const resolvedThumbnailUrl = useMemo(() => {
|
||||
if (!video.thumbnailUrl) return '';
|
||||
try {
|
||||
const parsed = new URL(video.thumbnailUrl);
|
||||
if (parsed.hostname === 'vz-thumbnail.b-cdn.net' && bunnyCdnHostname) {
|
||||
parsed.hostname = bunnyCdnHostname;
|
||||
return parsed.toString();
|
||||
}
|
||||
return parsed.toString();
|
||||
} catch {
|
||||
return video.thumbnailUrl;
|
||||
}
|
||||
}, [video.thumbnailUrl, bunnyCdnHostname]);
|
||||
|
||||
const handleEdit = async () => {
|
||||
setIsSaving(true);
|
||||
@@ -199,20 +214,26 @@ export function VideoCard({ video, projectId, canManage, onDeleted }: VideoCardP
|
||||
<span className="text-[11px] text-muted-foreground/90">Video may already be playable</span>
|
||||
</div>
|
||||
) : (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={`${video.thumbnailUrl?.replace('vz-thumbnail.b-cdn.net', 'vz-965f4f4a-fc1.b-cdn.net')}${retryKey ? `?t=${retryKey}` : ''}`}
|
||||
alt={video.title}
|
||||
className="absolute inset-0 w-full h-full object-cover transition-transform group-hover:scale-105"
|
||||
onError={() => {
|
||||
setImgError(true);
|
||||
// Check again after 10 seconds in case Bunny is still processing
|
||||
setTimeout(() => {
|
||||
setRetryKey(Date.now());
|
||||
setImgError(false);
|
||||
}, 10000);
|
||||
}}
|
||||
/>
|
||||
resolvedThumbnailUrl ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={`${resolvedThumbnailUrl}${retryKey ? `?t=${retryKey}` : ''}`}
|
||||
alt={video.title}
|
||||
className="absolute inset-0 w-full h-full object-cover transition-transform group-hover:scale-105"
|
||||
onError={() => {
|
||||
setImgError(true);
|
||||
// Check again after 10 seconds in case Bunny is still processing
|
||||
setTimeout(() => {
|
||||
setRetryKey(Date.now());
|
||||
setImgError(false);
|
||||
}, 10000);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-muted/80 text-xs text-muted-foreground font-medium">
|
||||
Thumbnail unavailable
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
{!imgError && (
|
||||
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { resolvePublicBunnyCdnHostname } from '@/lib/bunny-cdn';
|
||||
|
||||
type ProjectOption = {
|
||||
id: string;
|
||||
@@ -89,6 +90,7 @@ export function VideoDragDropUploader({
|
||||
const hasLoadedProjectsRef = useRef(false);
|
||||
|
||||
const needsProjectSelection = !fixedProjectId;
|
||||
const bunnyCdnHostname = useMemo(() => resolvePublicBunnyCdnHostname(), []);
|
||||
|
||||
const projectsById = useMemo(() => {
|
||||
return new Map(projects.map((project) => [project.id, project.name]));
|
||||
@@ -296,7 +298,9 @@ export function VideoDragDropUploader({
|
||||
videoUrl: `https://iframe.mediadelivery.net/embed/${initPayload.data.libraryId}/${initPayload.data.videoId}`,
|
||||
providerId: 'bunny',
|
||||
videoId: initPayload.data.videoId,
|
||||
thumbnailUrl: `https://vz-thumbnail.b-cdn.net/${initPayload.data.videoId}/thumbnail.jpg`,
|
||||
thumbnailUrl: bunnyCdnHostname
|
||||
? `https://${bunnyCdnHostname}/${initPayload.data.videoId}/thumbnail.jpg`
|
||||
: null,
|
||||
duration: null,
|
||||
uploadToken,
|
||||
}),
|
||||
@@ -341,7 +345,7 @@ export function VideoDragDropUploader({
|
||||
setIsUploading(false);
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to upload video');
|
||||
}
|
||||
}, [cleanupUploadState, projectsById, router]);
|
||||
}, [bunnyCdnHostname, cleanupUploadState, projectsById, router]);
|
||||
|
||||
const handleDropFile = useCallback((file: File) => {
|
||||
if (!canUpload) {
|
||||
|
||||
@@ -36,6 +36,7 @@ import type {
|
||||
} from '@/components/video-page/types';
|
||||
import { useApprovals } from '@/components/video-page/hooks/use-approvals';
|
||||
import { useVideoAssets } from '@/components/video-page/hooks/use-video-assets';
|
||||
import { resolvePublicBunnyCdnHostname } from '@/lib/bunny-cdn';
|
||||
|
||||
function formatTime(seconds: number): string {
|
||||
const totalSeconds = Math.floor(seconds);
|
||||
@@ -59,7 +60,6 @@ function formatBunnyQualityLabel(level: { height?: number; bitrate?: number }, i
|
||||
}
|
||||
|
||||
const SPEED_OPTIONS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
|
||||
const BUNNY_PULL_ZONE_HOSTNAME = 'vz-965f4f4a-fc1.b-cdn.net';
|
||||
|
||||
export type VideoPageMode = 'dashboard' | 'watch';
|
||||
|
||||
@@ -255,6 +255,7 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
|
||||
}, [video?.versions, activeVersionId]);
|
||||
const activeProviderId = activeVersion?.providerId;
|
||||
const activeVersionDuration = activeVersion?.duration;
|
||||
const bunnyCdnHostname = useMemo(() => resolvePublicBunnyCdnHostname(), []);
|
||||
const embedUrl = useMemo(() => {
|
||||
if (!activeVersion) return '';
|
||||
if (activeVersion.providerId === 'youtube') {
|
||||
@@ -264,7 +265,8 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
|
||||
return `${base}&origin=${encodeURIComponent(origin)}`;
|
||||
}
|
||||
if (activeVersion.providerId === 'bunny') {
|
||||
return `https://${BUNNY_PULL_ZONE_HOSTNAME}/${activeVersion.videoId}/playlist.m3u8`;
|
||||
if (!bunnyCdnHostname) return '';
|
||||
return `https://${bunnyCdnHostname}/${activeVersion.videoId}/playlist.m3u8`;
|
||||
}
|
||||
try {
|
||||
const url = new URL(activeVersion.originalUrl);
|
||||
@@ -275,7 +277,7 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}, [activeVersion]);
|
||||
}, [activeVersion, bunnyCdnHostname]);
|
||||
|
||||
const {
|
||||
isReady,
|
||||
|
||||
@@ -20,6 +20,7 @@ import { BunnyPreviewPlayer, type BunnyPreviewPlayerHandle } from '@/components/
|
||||
import { AssetListSection } from '@/components/video-page/asset-list-section';
|
||||
import type { VideoAsset } from '@/components/video-page/types';
|
||||
import { extractPastedImageFile, validateImageFile } from '@/components/video-page/image-upload-utils';
|
||||
import { resolvePublicBunnyCdnHostname } from '@/lib/bunny-cdn';
|
||||
|
||||
interface AssetsPaneProps {
|
||||
videoId: string;
|
||||
@@ -82,6 +83,7 @@ export const AssetsPane = memo(function AssetsPane({
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||
const [previewImageTitle, setPreviewImageTitle] = useState<string | null>(null);
|
||||
const [selectedAsset, setSelectedAsset] = useState<VideoAsset | null>(null);
|
||||
const bunnyCdnHostname = useMemo(() => resolvePublicBunnyCdnHostname(), []);
|
||||
const [focusedAssetId, setFocusedAssetId] = useState<string | null>(null);
|
||||
const bunnyPreviewPlayerRef = useRef<BunnyPreviewPlayerHandle | null>(null);
|
||||
const youtubeIframeRef = useRef<HTMLIFrameElement | null>(null);
|
||||
@@ -369,7 +371,9 @@ export const AssetsPane = memo(function AssetsPane({
|
||||
});
|
||||
|
||||
const sourceUrl = `https://iframe.mediadelivery.net/embed/${initData.libraryId}/${initData.videoId}`;
|
||||
const thumbnailUrl = `https://vz-965f4f4a-fc1.b-cdn.net/${initData.videoId}/thumbnail.jpg`;
|
||||
const thumbnailUrl = bunnyCdnHostname
|
||||
? `https://${bunnyCdnHostname}/${initData.videoId}/thumbnail.jpg`
|
||||
: undefined;
|
||||
const createdAsset = await createAsset({
|
||||
provider: 'BUNNY',
|
||||
sourceUrl,
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { resolvePublicBunnyCdnHostname } from '@/lib/bunny-cdn';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { BunnyPlaybackState, BunnyQualityOption } from '@/components/video-page/types';
|
||||
|
||||
@@ -28,17 +29,6 @@ export interface BunnyPreviewPlayerHandle {
|
||||
|
||||
const SPEED_OPTIONS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
|
||||
|
||||
function resolveBunnyCdnHostname(): string | null {
|
||||
const configured = process.env.NEXT_PUBLIC_BUNNY_CDN_URL;
|
||||
if (!configured) return null;
|
||||
try {
|
||||
const parsed = new URL(configured);
|
||||
return parsed.hostname || null;
|
||||
} catch {
|
||||
return configured.replace(/^https?:\/\//, '').replace(/\/+$/, '') || null;
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(value: number): string {
|
||||
if (!Number.isFinite(value) || value < 0) return '0:00';
|
||||
const total = Math.floor(value);
|
||||
@@ -78,7 +68,7 @@ export const BunnyPreviewPlayer = forwardRef<BunnyPreviewPlayerHandle, BunnyPrev
|
||||
const [selectedQualityLevel, setSelectedQualityLevel] = useState<number>(-1);
|
||||
const [bunnySourcePreference, setBunnySourcePreference] = useState<'auto' | 'original'>('auto');
|
||||
const [bunnyPlaybackState, setBunnyPlaybackState] = useState<BunnyPlaybackState>('none');
|
||||
const bunnyCdnHostname = useMemo(() => resolveBunnyCdnHostname(), []);
|
||||
const bunnyCdnHostname = useMemo(() => resolvePublicBunnyCdnHostname(), []);
|
||||
|
||||
const playlistUrl = useMemo(() => {
|
||||
if (!providerVideoId || !bunnyCdnHostname) return null;
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import type { BunnyDownloadPreference, Comment, DownloadTarget, Version, VideoData } from '@/components/video-page/types';
|
||||
|
||||
const BUNNY_PULL_ZONE_HOSTNAME = 'vz-965f4f4a-fc1.b-cdn.net';
|
||||
import { resolvePublicBunnyCdnHostname } from '@/lib/bunny-cdn';
|
||||
|
||||
function sanitizeDownloadFileName(value: string): string {
|
||||
return value
|
||||
@@ -14,17 +13,9 @@ function sanitizeDownloadFileName(value: string): string {
|
||||
}
|
||||
|
||||
function getAllowedHosts() {
|
||||
const bunnyCdnHostname = resolvePublicBunnyCdnHostname();
|
||||
return [
|
||||
BUNNY_PULL_ZONE_HOSTNAME,
|
||||
...(process.env.NEXT_PUBLIC_BUNNY_CDN_URL
|
||||
? (() => {
|
||||
try {
|
||||
return [new URL(process.env.NEXT_PUBLIC_BUNNY_CDN_URL).hostname];
|
||||
} catch {
|
||||
return [process.env.NEXT_PUBLIC_BUNNY_CDN_URL.replace(/^https?:\/\//, '').replace(/\/+$/, '')];
|
||||
}
|
||||
})()
|
||||
: []),
|
||||
...(bunnyCdnHostname ? [bunnyCdnHostname] : []),
|
||||
...(process.env.NEXT_PUBLIC_DIRECT_DOWNLOAD_ALLOWED_HOSTS ?? '').split(','),
|
||||
]
|
||||
.map((host) => host.trim().toLowerCase())
|
||||
|
||||
@@ -5,6 +5,7 @@ import { toast } from 'sonner';
|
||||
import * as tus from 'tus-js-client';
|
||||
import { parseVideoUrl, getThumbnailUrl, fetchVideoMetadata, type VideoSource } from '@/lib/video-providers';
|
||||
import type { VersionActionsConfig, VideoData } from '@/components/video-page/types';
|
||||
import { resolvePublicBunnyCdnHostname } from '@/lib/bunny-cdn';
|
||||
|
||||
interface UseVersionActionsParams extends VersionActionsConfig {
|
||||
setVideo: Dispatch<SetStateAction<VideoData | null>>;
|
||||
@@ -33,6 +34,7 @@ export function useVersionActions({
|
||||
const [showDeleteVersionDialog, setShowDeleteVersionDialog] = useState(false);
|
||||
const [versionToDelete, setVersionToDelete] = useState<string | null>(null);
|
||||
const [isDeletingVersion, setIsDeletingVersion] = useState(false);
|
||||
const bunnyCdnHostname = resolvePublicBunnyCdnHostname();
|
||||
|
||||
const handleNewVersionUrlChange = (url: string) => {
|
||||
setNewVersionUrl(url);
|
||||
@@ -126,7 +128,9 @@ export function useVersionActions({
|
||||
finalVideoUrl = `https://iframe.mediadelivery.net/embed/${libraryId}/${bunnyVideoId}`;
|
||||
finalProviderId = 'bunny';
|
||||
finalProviderVideoId = bunnyVideoId;
|
||||
finalThumbnailUrl = `https://vz-965f4f4a-fc1.b-cdn.net/${bunnyVideoId}/thumbnail.jpg`;
|
||||
finalThumbnailUrl = bunnyCdnHostname
|
||||
? `https://${bunnyCdnHostname}/${bunnyVideoId}/thumbnail.jpg`
|
||||
: null;
|
||||
}
|
||||
|
||||
const res = await fetch(`/api/projects/${projectId}/videos/${videoId}/versions`, {
|
||||
|
||||
Reference in New Issue
Block a user