mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
feat: Implement direct video file uploads via Bunny.net and TUS protocol, adding a new API route and UI for file selection.
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import type { VideoProvider, VideoMetadata, EmbedOptions, ThumbnailSize } from './types';
|
||||
import { getCachedMetadata, setCachedMetadata } from './metadata-cache';
|
||||
|
||||
// Bunny Stream URL patterns
|
||||
// e.g. https://iframe.mediadelivery.net/play/libraryId/videoId
|
||||
// e.g. https://video.bunnycdn.com/play/libraryId/videoId
|
||||
const BUNNY_PATTERNS = [
|
||||
/(?:iframe\.mediadelivery\.net|video\.bunnycdn\.com)\/(?:play|embed)\/[0-9]+\/([a-zA-Z0-9_-]+)/,
|
||||
];
|
||||
|
||||
export const bunnyProvider: VideoProvider = {
|
||||
id: 'bunny',
|
||||
name: 'Bunny Stream',
|
||||
icon: 'Video',
|
||||
|
||||
canHandle(url: string): boolean {
|
||||
return BUNNY_PATTERNS.some(pattern => pattern.test(url));
|
||||
},
|
||||
|
||||
extractVideoId(url: string): string | null {
|
||||
for (const pattern of BUNNY_PATTERNS) {
|
||||
const match = url.match(pattern);
|
||||
if (match && match[1]) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
getEmbedUrl(videoId: string, options: EmbedOptions = {}): string {
|
||||
// Requires library ID, but our current DB only stores `videoId` for standard providers
|
||||
// For Bunny, we typically store the full embed URL as `originalUrl`
|
||||
// So if this function is called, we try to extract it from the environment or default
|
||||
const libraryId = process.env.NEXT_PUBLIC_BUNNY_STREAM_LIBRARY_ID || process.env.BUNNY_STREAM_LIBRARY_ID || '0';
|
||||
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (options.autoplay) params.set('autoplay', 'true');
|
||||
if (options.loop) params.set('loop', 'true');
|
||||
if (options.muted) params.set('muted', 'true');
|
||||
|
||||
// We can use video.bunnycdn.com or iframe.mediadelivery.net
|
||||
return `https://iframe.mediadelivery.net/embed/${libraryId}/${videoId}?${params.toString()}`;
|
||||
},
|
||||
|
||||
getThumbnailUrl(videoId: string, size: ThumbnailSize = 'medium'): string {
|
||||
const libraryId = process.env.NEXT_PUBLIC_BUNNY_STREAM_LIBRARY_ID || process.env.BUNNY_STREAM_LIBRARY_ID || '0';
|
||||
// Bunny stream thumbnails: https://vz-uuid.b-cdn.net/{videoId}/thumbnail.jpg
|
||||
// Since we don't have the b-cdn pull zone readily available in pure abstract,
|
||||
// we should rely on fetching metadata for actual thumbnails, OR construct via API
|
||||
// Actually, Bunny's public thumbnail format is:
|
||||
return `https://vz-965f4f4a-fc1.b-cdn.net/${videoId}/thumbnail.jpg`; // Fallback approximate
|
||||
},
|
||||
|
||||
async getMetadata(videoId: string): Promise<VideoMetadata> {
|
||||
const cacheKey = `bunny:${videoId}`;
|
||||
const cached = getCachedMetadata(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
// We can't fetch title/duration via public API without an API key,
|
||||
// so we return basic metadata. When videos are uploaded via our server,
|
||||
// the title will be passed during creation.
|
||||
const fallback: VideoMetadata = {
|
||||
title: 'Bunny Video',
|
||||
thumbnailUrl: this.getThumbnailUrl(videoId, 'large'),
|
||||
};
|
||||
|
||||
setCachedMetadata(cacheKey, fallback);
|
||||
return fallback;
|
||||
},
|
||||
};
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { youtubeProvider } from './youtube';
|
||||
import { directProvider } from './direct';
|
||||
import { bunnyProvider } from './bunny';
|
||||
import type { VideoProvider, VideoSource, VideoMetadata, VideoProviderType } from './types';
|
||||
|
||||
// Export types
|
||||
@@ -11,6 +12,7 @@ export * from './types';
|
||||
const providers: VideoProvider[] = [
|
||||
youtubeProvider,
|
||||
directProvider,
|
||||
bunnyProvider,
|
||||
];
|
||||
|
||||
// Provider lookup map for quick access
|
||||
@@ -49,17 +51,17 @@ export function getAllProviders(): VideoProvider[] {
|
||||
*/
|
||||
export function parseVideoUrl(url: string): VideoSource | null {
|
||||
const provider = detectProvider(url);
|
||||
|
||||
|
||||
if (!provider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
const videoId = provider.extractVideoId(url);
|
||||
|
||||
|
||||
if (!videoId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
providerId: provider.id as VideoProviderType,
|
||||
videoId,
|
||||
@@ -72,11 +74,11 @@ export function parseVideoUrl(url: string): VideoSource | null {
|
||||
*/
|
||||
export async function fetchVideoMetadata(source: VideoSource): Promise<VideoMetadata | null> {
|
||||
const provider = getProvider(source.providerId);
|
||||
|
||||
|
||||
if (!provider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
return await provider.getMetadata(source.videoId);
|
||||
} catch (error) {
|
||||
@@ -90,11 +92,11 @@ export async function fetchVideoMetadata(source: VideoSource): Promise<VideoMeta
|
||||
*/
|
||||
export function getEmbedUrl(source: VideoSource, options?: Parameters<VideoProvider['getEmbedUrl']>[1]): string | null {
|
||||
const provider = getProvider(source.providerId);
|
||||
|
||||
|
||||
if (!provider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
return provider.getEmbedUrl(source.videoId, options);
|
||||
}
|
||||
|
||||
@@ -103,11 +105,11 @@ export function getEmbedUrl(source: VideoSource, options?: Parameters<VideoProvi
|
||||
*/
|
||||
export function getThumbnailUrl(source: VideoSource, size?: Parameters<VideoProvider['getThumbnailUrl']>[1]): string | null {
|
||||
const provider = getProvider(source.providerId);
|
||||
|
||||
|
||||
if (!provider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
return provider.getThumbnailUrl(source.videoId, size);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,15 +14,15 @@ export interface VideoProvider {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string; // Lucide icon name
|
||||
|
||||
|
||||
// URL handling
|
||||
canHandle(url: string): boolean;
|
||||
extractVideoId(url: string): string | null;
|
||||
|
||||
|
||||
// Embed and display
|
||||
getEmbedUrl(videoId: string, options?: EmbedOptions): string;
|
||||
getThumbnailUrl(videoId: string, size?: ThumbnailSize): string;
|
||||
|
||||
|
||||
// Metadata fetching
|
||||
getMetadata(videoId: string): Promise<VideoMetadata>;
|
||||
}
|
||||
@@ -38,7 +38,7 @@ export interface EmbedOptions {
|
||||
export type ThumbnailSize = 'small' | 'medium' | 'large' | 'maxres';
|
||||
|
||||
// Supported provider types - extend as we add more
|
||||
export type VideoProviderType = 'youtube' | 'direct';
|
||||
export type VideoProviderType = 'youtube' | 'direct' | 'bunny';
|
||||
|
||||
// Video source stored in database
|
||||
export interface VideoSource {
|
||||
|
||||
Reference in New Issue
Block a user