diff --git a/backend/src/services/rustfs.ts b/backend/src/services/rustfs.ts index 3d5e9b8..1f54aec 100644 --- a/backend/src/services/rustfs.ts +++ b/backend/src/services/rustfs.ts @@ -7,7 +7,6 @@ import { CompleteMultipartUploadCommand, AbortMultipartUploadCommand, GetObjectCommand, - HeadObjectCommand, } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; @@ -169,61 +168,49 @@ export async function getVideoStream( ): Promise { const s3 = getRustFsClient(); - // Get object metadata first to know total size - const headResult = await s3.send( - new HeadObjectCommand({ - Bucket: RUSTFS_BUCKET, - Key: key, - }), - ); - const totalSize = headResult.ContentLength ?? 0; - const contentType = headResult.ContentType || "video/webm"; - - let range: { start: number; end: number } | undefined; - let command: GetObjectCommand; - - if (rangeHeader) { - // Parse Range header like "bytes=0-1023" or "bytes=1024-" - const match = rangeHeader.match(/bytes=(\d*)-(\d*)/); - if (match) { - const start = match[1] ? parseInt(match[1], 10) : 0; - const end = match[2] ? parseInt(match[2], 10) : totalSize - 1; - range = { start, end: Math.min(end, totalSize - 1) }; - - command = new GetObjectCommand({ - Bucket: RUSTFS_BUCKET, - Key: key, - Range: `bytes=${range.start}-${range.end}`, - }); - } else { - command = new GetObjectCommand({ - Bucket: RUSTFS_BUCKET, - Key: key, - }); - } - } else { - command = new GetObjectCommand({ - Bucket: RUSTFS_BUCKET, - Key: key, - }); - } + // Build the GetObject command, optionally with a Range + const command = new GetObjectCommand({ + Bucket: RUSTFS_BUCKET, + Key: key, + ...(rangeHeader ? { Range: rangeHeader } : {}), + }); const result = await s3.send(command); if (!result.Body) { throw new Error("Empty response body from RustFS"); } + const contentType = result.ContentType || "video/webm"; + // Convert SDK stream to web ReadableStream const sdkStream = result.Body as any; const webStream: ReadableStream = sdkStream.transformToWebStream ? sdkStream.transformToWebStream() : sdkStream; + // If we sent a range request, parse the Content-Range header from S3 + // Format: "bytes start-end/total" + if (rangeHeader && result.ContentRange) { + const match = result.ContentRange.match(/bytes (\d+)-(\d+)\/(\d+)/); + if (match) { + const start = parseInt(match[1], 10); + const end = parseInt(match[2], 10); + const totalSize = parseInt(match[3], 10); + return { + stream: webStream, + contentType, + contentLength: totalSize, + range: { start, end }, + }; + } + } + + // No range request (or no Content-Range in response) — full object return { stream: webStream, contentType, - contentLength: totalSize, - range, + contentLength: result.ContentLength ?? 0, + range: undefined, }; } diff --git a/frontend/src/components/VideoPlayer.tsx b/frontend/src/components/VideoPlayer.tsx index 7c72c21..9e36edb 100644 --- a/frontend/src/components/VideoPlayer.tsx +++ b/frontend/src/components/VideoPlayer.tsx @@ -1,79 +1,19 @@ -import { useState, useRef } from "react"; import type { VideoDetail } from "../types/video"; import { formatRunTime } from "../api/client"; export default function VideoPlayer({ video }: { video: VideoDetail }) { - const [loaded, setLoaded] = useState(false); - const [error, setError] = useState(false); - const videoRef = useRef(null); - return (
-
- {/* Loading state */} - {!loaded && !error && ( -
- {video.thumbnailUrl && ( - - )} -
-
- - Loading video… - -
-
- )} - - {/* Error state */} - {error && ( -
- {video.thumbnailUrl && ( - - )} -
- - - - - Failed to load video - -
-
- )} - - -
+