Stream video through backend with Range request support
Previously videos were served via direct RustFS URLs, which meant: - No HTTP Range support (browsers had to download the entire file) - Large videos couldn't play at all - Video player rendered broken Now videos stream through GET /api/videos/:id/stream which: - Proxies video data from RustFS to the browser - Supports Range requests (HTTP 206 Partial Content) for seeking - Sets proper headers (Accept-Ranges, Content-Range, Content-Type) - Caches for 24 hours Frontend changes: - VideoPlayer: added playsInline, preload=metadata, object-contain, error state - VideoDetail: removed duplicate inline video, now uses VideoPlayer component - index.css: style WebKit video controls (dark panel, no border-radius)
This commit is contained in:
@@ -4,20 +4,22 @@ 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<HTMLVideoElement>(null);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-white/5 overflow-hidden">
|
||||
<div className="aspect-video bg-black relative">
|
||||
{!loaded && (
|
||||
{}
|
||||
{!loaded && !error && (
|
||||
<div className="absolute inset-0 flex items-center justify-center z-10">
|
||||
{video.thumbnailUrl ? (
|
||||
{video.thumbnailUrl && (
|
||||
<img
|
||||
src={video.thumbnailUrl}
|
||||
alt=""
|
||||
className="absolute inset-0 w-full h-full object-cover blur-sm opacity-50"
|
||||
/>
|
||||
) : null}
|
||||
)}
|
||||
<div className="relative z-20 flex flex-col items-center gap-2">
|
||||
<div className="w-10 h-10 rounded-full border-2 border-white/20 border-t-white/80 animate-spin" />
|
||||
<span className="text-xs text-white/40">
|
||||
@@ -26,13 +28,48 @@ export default function VideoPlayer({ video }: { video: VideoDetail }) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{}
|
||||
{error && (
|
||||
<div className="absolute inset-0 flex items-center justify-center z-10">
|
||||
{video.thumbnailUrl && (
|
||||
<img
|
||||
src={video.thumbnailUrl}
|
||||
alt=""
|
||||
className="absolute inset-0 w-full h-full object-cover opacity-30"
|
||||
/>
|
||||
)}
|
||||
<div className="relative z-20 flex flex-col items-center gap-2">
|
||||
<svg
|
||||
className="w-12 h-12 text-red-400/60"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-xs text-white/40">
|
||||
Failed to load video
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={video.videoUrl}
|
||||
controls
|
||||
className="w-full h-full"
|
||||
playsInline
|
||||
preload="metadata"
|
||||
className="w-full h-full object-contain"
|
||||
poster={video.thumbnailUrl}
|
||||
onCanPlay={() => setLoaded(true)}
|
||||
onError={() => setError(true)}
|
||||
>
|
||||
Your browser does not support video playback.
|
||||
</video>
|
||||
|
||||
@@ -24,3 +24,15 @@ a {
|
||||
color: #1896d3;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
video::-webkit-media-controls {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
video::-webkit-media-controls-enclosure {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
video::-webkit-media-controls-panel {
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import VideoPlayer from "../components/VideoPlayer";
|
||||
import VideoSidebar from "../components/VideoSidebar";
|
||||
import { fetchVideo, formatRunTime } from "../api/client";
|
||||
import type { VideoDetail } from "../types/video";
|
||||
@@ -60,9 +61,7 @@ function PageSkeleton() {
|
||||
export default function VideoDetail() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [video, setVideo] = useState<VideoDetail | null>(null);
|
||||
const [videoReady, setVideoReady] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
@@ -79,22 +78,8 @@ export default function VideoDetail() {
|
||||
);
|
||||
}
|
||||
|
||||
if (!video || !videoReady) {
|
||||
return (
|
||||
<>
|
||||
<PageSkeleton />
|
||||
{video && (
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={video.videoUrl}
|
||||
preload="auto"
|
||||
onCanPlay={() => setVideoReady(true)}
|
||||
className="fixed opacity-0 pointer-events-none"
|
||||
aria-hidden
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
if (!video) {
|
||||
return <PageSkeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -114,41 +99,7 @@ export default function VideoDetail() {
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-4 lg:items-start">
|
||||
<div className="lg:w-1/2 min-w-0">
|
||||
<div className="rounded-lg border border-white/5 overflow-hidden">
|
||||
<div className="aspect-video bg-black">
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={video.videoUrl}
|
||||
controls
|
||||
className="w-full h-full"
|
||||
poster={video.thumbnailUrl}
|
||||
>
|
||||
Your browser does not support video playback.
|
||||
</video>
|
||||
</div>
|
||||
<div className="px-4 py-3 flex items-center justify-between border-t border-white/5 bg-white/3">
|
||||
<a
|
||||
href={video.mtvUrl}
|
||||
download
|
||||
className="inline-flex items-center gap-2 rounded-md bg-white/4 border border-white/7 px-3 py-1.5 text-xs font-medium text-white/50 hover:bg-white/8 hover:text-white/80 hover:border-white/15 transition-colors no-underline"
|
||||
>
|
||||
<svg
|
||||
className="w-3.5 h-3.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||
/>
|
||||
</svg>
|
||||
.mtv replay
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<VideoPlayer video={video} />
|
||||
</div>
|
||||
<div className="lg:w-1/2">
|
||||
<VideoSidebar video={video} />
|
||||
|
||||
Reference in New Issue
Block a user