/** * Enhanced Cloudflare Worker for serving videos from R2 storage * Features: * - Admin panel with authentication * - Video visibility controls (private, shareable, clip_shareable) * - Clip generation and serving * - Permission-based access control */ // Admin login HTML const LOGIN_HTML = ` Admin Login

🎥 Admin Login

`; export default { async fetch(request, env) { const url = new URL(request.url); const path = url.pathname; // CORS headers const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, HEAD, POST, DELETE, OPTIONS', 'Access-Control-Allow-Headers': '*', }; if (request.method === 'OPTIONS') { return new Response(null, { headers: corsHeaders }); } try { // Admin routes if (path.startsWith('/admin')) { return await handleAdminRoutes(request, env, path, corsHeaders); } // Clip serving if (path.startsWith('/clip/')) { return await handleClip(request, env, path, corsHeaders); } // Public gallery (only shows shareable videos) if (path === '/gallery') { return await handlePublicGallery(env.R2_BUCKET, env.VIDEO_METADATA, corsHeaders); } // Public API (only lists shareable videos) if (path === '/' || path === '/api/list') { return await handlePublicList(env.R2_BUCKET, env.VIDEO_METADATA, corsHeaders); } // Handle HLS live streams (/live/stream-name/file.m3u8 or file.ts) if (path.startsWith('/live/')) { return await handleHLSStream(request, env, path, corsHeaders); } // Serve video file (with permission check) const filename = path.substring(1); if (filename) { return await handleVideoFile(request, env, filename, corsHeaders); } return new Response('Not found', { status: 404, headers: corsHeaders }); } catch (error) { console.error('Error:', error); return new Response(`Error: ${error.message}`, { status: 500, headers: corsHeaders }); } }, }; /** * Handle admin routes */ async function handleAdminRoutes(request, env, path, corsHeaders) { // Login page if (path === '/admin/login' && request.method === 'GET') { return new Response(LOGIN_HTML, { headers: { 'Content-Type': 'text/html; charset=utf-8' } }); } // Login POST if (path === '/admin/login' && request.method === 'POST') { const { password } = await request.json(); const correctPassword = env.ADMIN_PASSWORD || 'changeme'; if (password === correctPassword) { // Create session token const token = await generateToken(password); return new Response(JSON.stringify({ success: true }), { headers: { 'Content-Type': 'application/json', 'Set-Cookie': `admin_auth=${token}; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=86400` } }); } return new Response(JSON.stringify({ error: 'Invalid password' }), { status: 401, headers: { 'Content-Type': 'application/json' } }); } // Check authentication for other admin routes const isAuthenticated = await verifyAuth(request, env); if (!isAuthenticated) { return new Response('Unauthorized', { status: 401, headers: { 'Location': '/admin/login' } }); } // Admin panel if (path === '/admin' || path === '/admin/') { const adminHTML = await getAdminHTML(); return new Response(adminHTML, { headers: { 'Content-Type': 'text/html; charset=utf-8' } }); } // API: List all videos with metadata if (path === '/admin/api/videos') { return await handleAdminListVideos(env.R2_BUCKET, env.VIDEO_METADATA, corsHeaders); } // API: Update video visibility if (path === '/admin/api/videos/visibility' && request.method === 'POST') { const { filename, visibility } = await request.json(); await env.VIDEO_METADATA.put(filename, JSON.stringify({ visibility })); return new Response(JSON.stringify({ success: true }), { headers: { 'Content-Type': 'application/json', ...corsHeaders } }); } // API: Delete video if (path.startsWith('/admin/api/videos/') && request.method === 'DELETE') { const filename = decodeURIComponent(path.replace('/admin/api/videos/', '')); await env.R2_BUCKET.delete(filename); await env.VIDEO_METADATA.delete(filename); return new Response(JSON.stringify({ success: true }), { headers: { 'Content-Type': 'application/json', ...corsHeaders } }); } // API: Upload video if (path === '/admin/api/upload' && request.method === 'POST') { return await handleVideoUpload(request, env, corsHeaders); } return new Response('Not found', { status: 404 }); } /** * Get admin HTML (reads from the admin.html file embedded as string or fetched) */ async function getAdminHTML() { return ` Video Admin Panel

🎥 Video Admin Panel

Manage video visibility and sharing

0
Total Videos
0
Private
0
Shareable
0
Clip Shareable

Loading videos...

Video
`; } /** * List all videos for admin (includes metadata) */ async function handleAdminListVideos(bucket, kv, corsHeaders) { const objects = await bucket.list(); const videos = await Promise.all( objects.objects.map(async (obj) => { const metadataStr = await kv.get(obj.key); const metadata = metadataStr ? JSON.parse(metadataStr) : {}; return { name: obj.key, size: obj.size, uploaded: obj.uploaded, visibility: metadata.visibility || 'shareable', url: `/${obj.key}`, }; }) ); return new Response(JSON.stringify({ count: videos.length, videos }), { headers: { 'Content-Type': 'application/json', ...corsHeaders } }); } /** * List public videos only (shareable ones) */ async function handlePublicList(bucket, kv, corsHeaders) { const objects = await bucket.list(); const videos = await Promise.all( objects.objects.map(async (obj) => { const metadataStr = await kv.get(obj.key); const metadata = metadataStr ? JSON.parse(metadataStr) : {}; return { name: obj.key, size: obj.size, uploaded: obj.uploaded, visibility: metadata.visibility || 'shareable', url: `/${obj.key}`, }; }) ); // Filter to only shareable videos const shareableVideos = videos.filter(v => v.visibility === 'shareable'); return new Response(JSON.stringify({ count: shareableVideos.length, videos: shareableVideos }), { headers: { 'Content-Type': 'application/json', ...corsHeaders } }); } /** * Serve video file with permission check */ async function handleVideoFile(request, env, filename, corsHeaders) { // Check permissions const metadataStr = await env.VIDEO_METADATA.get(filename); const metadata = metadataStr ? JSON.parse(metadataStr) : {}; const visibility = metadata.visibility || 'shareable'; // If private, require authentication if (visibility === 'private') { const isAuth = await verifyAuth(request, env); if (!isAuth) { return new Response('This video is private', { status: 403, headers: corsHeaders }); } } // If clip_shareable, only allow clips if (visibility === 'clip_shareable') { const isAuth = await verifyAuth(request, env); if (!isAuth) { return new Response('Full video not available. Only clips can be shared.', { status: 403, headers: corsHeaders }); } } // Serve the video return await serveVideo(request, env.R2_BUCKET, filename, corsHeaders); } /** * Handle clip requests */ async function handleClip(request, env, path, corsHeaders) { const filename = path.replace('/clip/', '').split('?')[0]; const url = new URL(request.url); const start = url.searchParams.get('start') || '0'; const end = url.searchParams.get('end'); // Check permissions const metadataStr = await env.VIDEO_METADATA.get(filename); const metadata = metadataStr ? JSON.parse(metadataStr) : {}; const visibility = metadata.visibility || 'shareable'; // Clips are allowed for clip_shareable and shareable videos if (visibility === 'private') { const isAuth = await verifyAuth(request, env); if (!isAuth) { return new Response('This video is private', { status: 403, headers: corsHeaders }); } } // For actual clip generation, you'd need to: // 1. Use ffmpeg in a separate service, or // 2. Use byte-range requests to approximate clips, or // 3. Pre-generate clips on upload // For now, we'll serve with Content-Range headers as an approximation // This won't give exact frame-accurate clips but will seek to the right position return new Response(` Video Clip `, { headers: { 'Content-Type': 'text/html; charset=utf-8' } }); } /** * Serve video with range support */ async function serveVideo(request, bucket, filename, corsHeaders) { const range = request.headers.get('Range'); let object; if (range) { const rangeMatch = range.match(/bytes=(\d+)-(\d*)/); if (rangeMatch) { const start = parseInt(rangeMatch[1]); const end = rangeMatch[2] ? parseInt(rangeMatch[2]) : undefined; object = await bucket.get(filename, { range: { offset: start, length: end ? end - start + 1 : undefined } }); } else { object = await bucket.get(filename); } } else { object = await bucket.get(filename); } if (!object) { return new Response('Video not found', { status: 404, headers: corsHeaders }); } const contentType = getContentType(filename); const headers = { 'Content-Type': contentType, 'Cache-Control': 'public, max-age=31536000', 'Accept-Ranges': 'bytes', ...corsHeaders, }; if (range && object.range) { headers['Content-Range'] = `bytes ${object.range.offset}-${object.range.offset + object.range.length - 1}/${object.size}`; headers['Content-Length'] = object.range.length; return new Response(object.body, { status: 206, headers }); } else { headers['Content-Length'] = object.size; return new Response(object.body, { status: 200, headers }); } } /** * Public gallery (only shareable videos) */ async function handlePublicGallery(bucket, kv, corsHeaders) { const objects = await bucket.list(); const videos = await Promise.all( objects.objects.map(async (obj) => { const metadataStr = await kv.get(obj.key); const metadata = metadataStr ? JSON.parse(metadataStr) : {}; return { ...obj, visibility: metadata.visibility || 'shareable' }; }) ); const shareableVideos = videos.filter(v => v.visibility === 'shareable'); const videoItems = shareableVideos .map(obj => { const sizeInMB = (obj.size / (1024 * 1024)).toFixed(2); const uploadDate = new Date(obj.uploaded).toLocaleDateString(); return `

${obj.key}

Size: ${sizeInMB} MB | Uploaded: ${uploadDate}

`; }) .join('\n'); const emptyState = shareableVideos.length === 0 ? `
🎬

No Videos Available Yet

This gallery is currently empty. Check back soon for new content!

📢

For Content Creators

Upload videos through the admin panel to make them available here.

` : videoItems; const html = ` Video Gallery

🎥 Video Gallery

${shareableVideos.length} video${shareableVideos.length === 1 ? '' : 's'} available

${emptyState}
`; return new Response(html, { headers: { 'Content-Type': 'text/html; charset=utf-8', ...corsHeaders } }); } /** * Handle video upload from admin panel */ async function handleVideoUpload(request, env, corsHeaders) { try { // Parse multipart form data const formData = await request.formData(); const videoFile = formData.get('video'); if (!videoFile) { return new Response(JSON.stringify({ error: 'No video file provided' }), { status: 400, headers: { 'Content-Type': 'application/json', ...corsHeaders } }); } // Get filename const filename = videoFile.name; // Validate file type const validExtensions = ['.mp4', '.mkv', '.mov', '.avi', '.webm', '.flv', '.wmv']; const ext = filename.substring(filename.lastIndexOf('.')).toLowerCase(); if (!validExtensions.includes(ext)) { return new Response(JSON.stringify({ error: 'Invalid file type' }), { status: 400, headers: { 'Content-Type': 'application/json', ...corsHeaders } }); } // Upload to R2 await env.R2_BUCKET.put(filename, videoFile.stream(), { httpMetadata: { contentType: getContentType(filename) } }); // Set default visibility to shareable await env.VIDEO_METADATA.put(filename, JSON.stringify({ visibility: 'shareable', uploadedAt: new Date().toISOString(), uploadMethod: 'admin_panel' })); return new Response(JSON.stringify({ success: true, filename: filename, url: `/${filename}` }), { status: 200, headers: { 'Content-Type': 'application/json', ...corsHeaders } }); } catch (error) { console.error('Upload error:', error); return new Response(JSON.stringify({ error: 'Upload failed', details: error.message }), { status: 500, headers: { 'Content-Type': 'application/json', ...corsHeaders } }); } } /** * Verify admin authentication */ async function verifyAuth(request, env) { const cookie = request.headers.get('Cookie') || ''; const match = cookie.match(/admin_auth=([^;]+)/); if (!match) return false; const token = match[1]; const correctPassword = env.ADMIN_PASSWORD || 'changeme'; const expectedToken = await generateToken(correctPassword); return token === expectedToken; } /** * Generate auth token */ async function generateToken(password) { const encoder = new TextEncoder(); const data = encoder.encode(password + 'salt123'); const hashBuffer = await crypto.subtle.digest('SHA-256', data); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); } /** * Handle HLS live stream requests */ async function handleHLSStream(request, env, path, corsHeaders) { // Path format: /live/stream-name/file.m3u8 or /live/stream-name/file.ts const filename = path.substring(1); // Remove leading slash try { const object = await env.R2_BUCKET.get(filename); if (!object) { return new Response('Stream not found', { status: 404, headers: corsHeaders }); } // Determine content type based on extension const contentType = filename.endsWith('.m3u8') ? 'application/vnd.apple.mpegurl' : filename.endsWith('.ts') ? 'video/MP2T' : 'application/octet-stream'; // Set appropriate caching headers const cacheControl = filename.endsWith('.m3u8') ? 'no-cache, no-store, must-revalidate' // Playlist changes frequently : 'public, max-age=31536000'; // Chunks are immutable const headers = { 'Content-Type': contentType, 'Cache-Control': cacheControl, ...corsHeaders, }; return new Response(object.body, { headers }); } catch (error) { console.error('Error serving HLS stream:', error); return new Response('Error serving stream', { status: 500, headers: corsHeaders }); } } /** * Get content type */ function getContentType(filename) { const ext = filename.split('.').pop().toLowerCase(); const types = { 'mp4': 'video/mp4', 'mkv': 'video/x-matroska', 'mov': 'video/quicktime', 'avi': 'video/x-msvideo', 'webm': 'video/webm', 'flv': 'video/x-flv', 'wmv': 'video/x-ms-wmv', 'm3u8': 'application/vnd.apple.mpegurl', 'ts': 'video/MP2T', }; return types[ext] || 'application/octet-stream'; }