From fd5868f569d393b664197d766081e9ea85e7a989 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Tue, 25 Nov 2025 22:07:17 -0800 Subject: [PATCH] Fix video playback from R2 with proper range request handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical fixes for video streaming: - Fix range request handling by using HEAD request first - Get total file size before processing range requests - Properly calculate Content-Range header with accurate boundaries - Prevent issues where object.size wasn't available on range requests - Add proper CORS headers for video streaming - Expose Content-Length, Content-Range, Accept-Ranges headers - Allow Range header in requests - Enables video seeking and progressive loading in browsers - Improve range request logic - Ensure end byte doesn't exceed file size - Calculate correct Content-Length for partial responses - Always return accurate byte ranges in 206 responses These fixes resolve issues where videos wouldn't play or seek properly due to incorrect Content-Range headers and missing CORS headers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- worker/video-server-enhanced.js | 35 ++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/worker/video-server-enhanced.js b/worker/video-server-enhanced.js index 45ef644..cc8b9d2 100644 --- a/worker/video-server-enhanced.js +++ b/worker/video-server-enhanced.js @@ -139,7 +139,8 @@ export default { const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, HEAD, POST, DELETE, OPTIONS', - 'Access-Control-Allow-Headers': '*', + 'Access-Control-Allow-Headers': 'Range, Content-Type, Accept', + 'Access-Control-Expose-Headers': 'Content-Length, Content-Range, Accept-Ranges', }; if (request.method === 'OPTIONS') { @@ -450,15 +451,32 @@ async function handleClip(request, env, path, corsHeaders) { async function serveVideo(request, bucket, filename, corsHeaders) { const range = request.headers.get('Range'); + // First, get the file metadata to know the total size + const objectHead = await bucket.head(filename); + if (!objectHead) { + return new Response('Video not found', { status: 404, headers: corsHeaders }); + } + + const totalSize = objectHead.size; + const contentType = getContentType(filename); + let object; + let start = 0; + let end = totalSize - 1; + if (range) { const rangeMatch = range.match(/bytes=(\d+)-(\d*)/); if (rangeMatch) { - const start = parseInt(rangeMatch[1]); - const end = rangeMatch[2] ? parseInt(rangeMatch[2]) : undefined; + start = parseInt(rangeMatch[1]); + end = rangeMatch[2] ? parseInt(rangeMatch[2]) : totalSize - 1; + + // Ensure end doesn't exceed file size + if (end >= totalSize) { + end = totalSize - 1; + } object = await bucket.get(filename, { - range: { offset: start, length: end ? end - start + 1 : undefined } + range: { offset: start, length: end - start + 1 } }); } else { object = await bucket.get(filename); @@ -471,7 +489,6 @@ async function serveVideo(request, bucket, filename, corsHeaders) { 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', @@ -479,13 +496,13 @@ async function serveVideo(request, bucket, filename, corsHeaders) { ...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; + if (range) { + headers['Content-Range'] = `bytes ${start}-${end}/${totalSize}`; + headers['Content-Length'] = end - start + 1; return new Response(object.body, { status: 206, headers }); } else { - headers['Content-Length'] = object.size; + headers['Content-Length'] = totalSize; return new Response(object.body, { status: 200, headers }); } }