Fix video playback from R2 with proper range request handling

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 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2025-11-25 22:07:17 -08:00
parent 2d71a13621
commit fd5868f569
1 changed files with 26 additions and 9 deletions

View File

@ -139,7 +139,8 @@ export default {
const corsHeaders = { const corsHeaders = {
'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, HEAD, POST, DELETE, OPTIONS', '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') { if (request.method === 'OPTIONS') {
@ -450,15 +451,32 @@ async function handleClip(request, env, path, corsHeaders) {
async function serveVideo(request, bucket, filename, corsHeaders) { async function serveVideo(request, bucket, filename, corsHeaders) {
const range = request.headers.get('Range'); 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 object;
let start = 0;
let end = totalSize - 1;
if (range) { if (range) {
const rangeMatch = range.match(/bytes=(\d+)-(\d*)/); const rangeMatch = range.match(/bytes=(\d+)-(\d*)/);
if (rangeMatch) { if (rangeMatch) {
const start = parseInt(rangeMatch[1]); start = parseInt(rangeMatch[1]);
const end = rangeMatch[2] ? parseInt(rangeMatch[2]) : undefined; 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, { object = await bucket.get(filename, {
range: { offset: start, length: end ? end - start + 1 : undefined } range: { offset: start, length: end - start + 1 }
}); });
} else { } else {
object = await bucket.get(filename); 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 }); return new Response('Video not found', { status: 404, headers: corsHeaders });
} }
const contentType = getContentType(filename);
const headers = { const headers = {
'Content-Type': contentType, 'Content-Type': contentType,
'Cache-Control': 'public, max-age=31536000', 'Cache-Control': 'public, max-age=31536000',
@ -479,13 +496,13 @@ async function serveVideo(request, bucket, filename, corsHeaders) {
...corsHeaders, ...corsHeaders,
}; };
if (range && object.range) { if (range) {
headers['Content-Range'] = `bytes ${object.range.offset}-${object.range.offset + object.range.length - 1}/${object.size}`; headers['Content-Range'] = `bytes ${start}-${end}/${totalSize}`;
headers['Content-Length'] = object.range.length; headers['Content-Length'] = end - start + 1;
return new Response(object.body, { status: 206, headers }); return new Response(object.body, { status: 206, headers });
} else { } else {
headers['Content-Length'] = object.size; headers['Content-Length'] = totalSize;
return new Response(object.body, { status: 200, headers }); return new Response(object.body, { status: 200, headers });
} }
} }