perf(frontend): drop react-player — YouTube iframe natif, HLS.js lazy seulement si .m3u8
This commit is contained in:
85
frontend/src/components/VideoPlayer.tsx
Normal file
85
frontend/src/components/VideoPlayer.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* VideoPlayer — zéro dépendance runtime
|
||||
*
|
||||
* youtube → <iframe> embed natif (0KB ajouté au bundle)
|
||||
* s3/local → <video> natif + HLS.js chargé lazily si .m3u8
|
||||
* external → <video> natif
|
||||
*/
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
type StorageType = 'youtube' | 's3' | 'local' | 'external';
|
||||
|
||||
interface VideoPlayerProps {
|
||||
storageType: StorageType;
|
||||
storageKey: string;
|
||||
}
|
||||
|
||||
export default function VideoPlayer({ storageType, storageKey }: VideoPlayerProps) {
|
||||
if (storageType === 'youtube') {
|
||||
return <YouTubePlayer videoId={storageKey} />;
|
||||
}
|
||||
|
||||
const url =
|
||||
storageType === 'external'
|
||||
? storageKey
|
||||
: `${import.meta.env.VITE_API_URL}/stream/${storageKey}`;
|
||||
|
||||
return <NativePlayer url={url} />;
|
||||
}
|
||||
|
||||
// ── YouTube ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function YouTubePlayer({ videoId }: { videoId: string }) {
|
||||
return (
|
||||
<iframe
|
||||
src={`https://www.youtube.com/embed/${videoId}?rel=0&color=white`}
|
||||
className="h-full w-full"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
title="Lecteur vidéo"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Native + HLS.js lazy ──────────────────────────────────────────────────────
|
||||
|
||||
function NativePlayer({ url }: { url: string }) {
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
|
||||
if (!url.includes('.m3u8')) {
|
||||
video.src = url;
|
||||
return;
|
||||
}
|
||||
|
||||
// HLS — import dynamique, ne charge que si nécessaire
|
||||
let hls: import('hls.js').default | null = null;
|
||||
|
||||
if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
// Safari supporte HLS nativement
|
||||
video.src = url;
|
||||
} else {
|
||||
import('hls.js').then(({ default: Hls }) => {
|
||||
if (!Hls.isSupported() || !video) return;
|
||||
hls = new Hls();
|
||||
hls.loadSource(url);
|
||||
hls.attachMedia(video);
|
||||
});
|
||||
}
|
||||
|
||||
return () => { hls?.destroy(); };
|
||||
}, [url]);
|
||||
|
||||
return (
|
||||
<video
|
||||
ref={videoRef}
|
||||
className="h-full w-full"
|
||||
controls
|
||||
playsInline
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,14 @@
|
||||
import { useState, useEffect, lazy, Suspense } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
import { apiFetch } from '../lib/api';
|
||||
|
||||
// Lazy — HLS/DASH chunks ne chargent que sur /video/:id, pas sur la homepage
|
||||
const ReactPlayer = lazy(() => import('react-player'));
|
||||
import VideoPlayer from '../components/VideoPlayer';
|
||||
|
||||
interface Video {
|
||||
id: number;
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
duration?: number;
|
||||
storageType: 'youtube' | 's3' | 'local';
|
||||
storageType: 'youtube' | 's3' | 'local' | 'external';
|
||||
storageKey: string;
|
||||
thumbnailUrl?: string;
|
||||
requiredLevel: number;
|
||||
@@ -22,23 +20,14 @@ interface VideoResponse {
|
||||
data: { video: Video };
|
||||
}
|
||||
|
||||
function buildPlayerUrl(storageType: Video['storageType'], storageKey: string): string {
|
||||
switch (storageType) {
|
||||
case 'youtube': return `https://www.youtube.com/watch?v=${storageKey}`;
|
||||
case 's3': return storageKey; // URL S3 complète attendue dans storageKey
|
||||
case 'local': return `${import.meta.env.VITE_API_URL}/stream/${storageKey}`;
|
||||
}
|
||||
}
|
||||
|
||||
export default function VideoPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [video, setVideo] = useState<Video | null>(null);
|
||||
const [error, setError] = useState<'not_found' | 'forbidden' | 'unknown' | null>(null);
|
||||
const [video, setVideo] = useState<Video | null>(null);
|
||||
const [error, setError] = useState<'forbidden' | 'not_found' | 'unknown' | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
|
||||
apiFetch<VideoResponse>(`/videos/${id}`)
|
||||
.then((res) => setVideo(res.data.video))
|
||||
.catch((err: Error) => {
|
||||
@@ -63,7 +52,7 @@ export default function VideoPage() {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-4 pt-16 text-center">
|
||||
<span className="font-mono text-3xl text-od-accent">⊘</span>
|
||||
<p className="text-od-text font-medium">Contenu premium</p>
|
||||
<p className="font-medium text-od-text">Contenu premium</p>
|
||||
<p className="text-sm text-od-muted">Cette vidéo nécessite un abonnement supérieur.</p>
|
||||
<Link to="/" className="font-mono text-xs text-od-muted hover:text-od-text transition-colors">
|
||||
← Retour
|
||||
@@ -83,22 +72,13 @@ export default function VideoPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const playerUrl = buildPlayerUrl(video.storageType, video.storageKey);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
|
||||
{/* Player */}
|
||||
<div className="overflow-hidden rounded border border-od-border bg-od-surface">
|
||||
<div className="aspect-video w-full">
|
||||
<Suspense fallback={<div className="h-full w-full bg-od-surface-hi animate-pulse" />}>
|
||||
<ReactPlayer
|
||||
src={playerUrl}
|
||||
width="100%"
|
||||
height="100%"
|
||||
controls
|
||||
/>
|
||||
</Suspense>
|
||||
<VideoPlayer storageType={video.storageType} storageKey={video.storageKey} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user