diff --git a/server/bunny.ts b/server/bunny.ts index 3e25a26..3877b46 100644 --- a/server/bunny.ts +++ b/server/bunny.ts @@ -1,4 +1,5 @@ import { type Video, type InsertVideo } from "@shared/schema"; +import crypto from 'crypto'; interface BunnyVideo { guid: string; @@ -22,11 +23,13 @@ export class BunnyService { private apiKey: string; private libraryId: string; private hostname: string; + private securityKey: string; constructor() { this.apiKey = process.env.BUNNY_API_KEY!; this.libraryId = process.env.BUNNY_LIBRARY_ID!; this.hostname = process.env.BUNNY_HOSTNAME!; + this.securityKey = process.env.BUNNY_SECURITY_KEY || ''; // CDN security key for signing URLs if (!this.apiKey || !this.libraryId || !this.hostname) { throw new Error("Missing Bunny.net configuration"); @@ -51,18 +54,32 @@ export class BunnyService { return response.json(); } - private bunnyVideoToVideo(bunnyVideo: BunnyVideo): Video { - // Use proxy endpoint for thumbnails to handle private access - const thumbnailUrl = `/thumbnail/${bunnyVideo.guid}`; + private generateSignedUrl(path: string, expiryHours: number = 1): string { + if (!this.securityKey) { + // If no security key, return iframe embed as fallback + const videoId = path.split('/')[1]; + return `https://iframe.mediadelivery.net/embed/${this.libraryId}/${videoId}?controls=true&autoplay=false`; + } - // Try direct CDN URL first - some videos might be accessible - const directUrl = `https://${this.hostname}/${bunnyVideo.guid}/playlist.m3u8`; + const expireTimestamp = Math.floor(Date.now() / 1000) + (expiryHours * 3600); + const tokenContent = `${this.securityKey}${path}${expireTimestamp}`; - // Fallback iframe embed for private videos - const iframeUrl = `https://iframe.mediadelivery.net/embed/${this.libraryId}/${bunnyVideo.guid}?controls=true&autoplay=false`; + const hash = crypto.createHash('md5').update(tokenContent).digest(); + const token = Buffer.from(hash).toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); + + return `https://${this.hostname}${path}?token=${token}&expires=${expireTimestamp}`; + } + + private bunnyVideoToVideo(bunnyVideo: BunnyVideo): Video { + // Generate signed URLs for private video access + const videoPath = `/${bunnyVideo.guid}/playlist.m3u8`; + const thumbnailPath = `/${bunnyVideo.guid}/${bunnyVideo.thumbnailFileName || 'thumbnail.jpg'}`; - // Use direct URL for HLS streaming - const videoUrl = directUrl; + const videoUrl = this.generateSignedUrl(videoPath); + const thumbnailUrl = this.generateSignedUrl(thumbnailPath); return { id: bunnyVideo.guid,