w-lab-app/src/components/domains/video-guides/VideoPlayer/VideoPlayer.tsx
2025-10-29 23:37:06 +01:00

254 lines
7.1 KiB
TypeScript

"use client";
import { useEffect, useRef, useState } from "react";
import styles from "./VideoPlayer.module.scss";
interface VideoPlayerProps {
mpd: string;
m3u8: string;
poster?: string;
autoPlay?: boolean;
}
export default function VideoPlayer({
mpd,
m3u8,
poster,
autoPlay = false,
}: VideoPlayerProps) {
const videoRef = useRef<HTMLVideoElement | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);
const [showPlayButton, setShowPlayButton] = useState(true);
useEffect(() => {
const video = videoRef.current;
if (!video) return;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let shakaPlayer: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let hls: any;
let cleanup = false;
const initPlayer = async () => {
try {
setIsLoading(true);
setHasError(false);
// eslint-disable-next-line no-console
console.log("[VideoPlayer] Initializing player...");
// eslint-disable-next-line no-console
console.log("[VideoPlayer] DASH URL:", mpd);
// eslint-disable-next-line no-console
console.log("[VideoPlayer] HLS URL:", m3u8);
// iOS/Safari - нативный HLS
if (video.canPlayType("application/vnd.apple.mpegurl")) {
// eslint-disable-next-line no-console
console.log("[VideoPlayer] Using native HLS support");
video.src = m3u8;
setIsLoading(false);
return;
}
// DASH через Shaka (предпочтительно для Android/Desktop)
try {
// eslint-disable-next-line no-console
console.log("[VideoPlayer] Trying Shaka Player for DASH...");
const shaka = await import("shaka-player/dist/shaka-player.compiled.js");
if (cleanup) return;
shakaPlayer = new shaka.default.Player(video);
shakaPlayer.configure({
streaming: {
bufferingGoal: 20,
rebufferingGoal: 2,
lowLatencyMode: false,
},
manifest: {
dash: {
ignoreMinBufferTime: true,
},
},
});
await shakaPlayer.load(mpd);
// eslint-disable-next-line no-console
console.log("[VideoPlayer] Shaka Player loaded successfully");
setIsLoading(false);
return;
} catch (e) {
// eslint-disable-next-line no-console
console.warn("[VideoPlayer] Shaka failed, fallback to HLS.js", e);
}
// Запасной вариант - HLS.js
try {
// eslint-disable-next-line no-console
console.log("[VideoPlayer] Trying HLS.js...");
const Hls = (await import("hls.js")).default;
if (cleanup) return;
if (Hls.isSupported()) {
hls = new Hls({
maxBufferLength: 30,
debug: false,
});
hls.loadSource(m3u8);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => {
// eslint-disable-next-line no-console
console.log("[VideoPlayer] HLS.js manifest parsed");
setIsLoading(false);
});
hls.on(
Hls.Events.ERROR,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(_event: any, data: any) => {
// eslint-disable-next-line no-console
console.error("[VideoPlayer] HLS.js error:", data);
if (data.fatal) {
setHasError(true);
setIsLoading(false);
}
},
);
return;
}
} catch (e) {
// eslint-disable-next-line no-console
console.warn("[VideoPlayer] HLS.js failed", e);
}
// Совсем запасной - прямой src
// eslint-disable-next-line no-console
console.log("[VideoPlayer] Using direct video src");
video.src = m3u8;
setIsLoading(false);
} catch (error) {
// eslint-disable-next-line no-console
console.error("[VideoPlayer] Fatal error:", error);
setHasError(true);
setIsLoading(false);
}
};
initPlayer();
return () => {
cleanup = true;
try {
shakaPlayer?.destroy();
} catch {
// Ignore cleanup errors
}
try {
hls?.destroy();
} catch {
// Ignore cleanup errors
}
};
}, [mpd, m3u8, autoPlay]);
const handlePlay = async () => {
const video = videoRef.current;
if (!video) return;
try {
// Убеждаемся что звук включен
video.muted = false;
await video.play();
setShowPlayButton(false);
// eslint-disable-next-line no-console
console.log("[VideoPlayer] Started playing with sound");
} catch (error) {
// eslint-disable-next-line no-console
console.error("[VideoPlayer] Play failed:", error);
}
};
const handleVideoPlay = () => {
setShowPlayButton(false);
};
const handleVideoPause = () => {
// Пауза через нативные контролы
};
return (
<div className={styles.playerWrapper}>
{isLoading && !hasError && (
<div className={styles.loading}>
<div className={styles.spinner} />
</div>
)}
{hasError && (
<div className={styles.error}>
<p>Unable to load video</p>
<p className={styles.errorSubtext}>
Please check your connection and try again
</p>
</div>
)}
{!isLoading && !hasError && showPlayButton && (
<button className={styles.playButton} onClick={handlePlay} type="button">
<svg
width="80"
height="80"
viewBox="0 0 80 80"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="40" cy="40" r="40" fill="rgba(255, 255, 255, 0.9)" />
<path
d="M32 24L56 40L32 56V24Z"
fill="#000"
/>
</svg>
</button>
)}
<video
ref={videoRef}
controls
playsInline
preload="metadata"
crossOrigin="anonymous"
poster={poster}
className={styles.video}
style={{
opacity: isLoading ? 0 : 1,
transition: "opacity 0.3s ease",
}}
onLoadedMetadata={() => {
// eslint-disable-next-line no-console
console.log("[VideoPlayer] Video metadata loaded");
}}
onCanPlay={() => {
// eslint-disable-next-line no-console
console.log("[VideoPlayer] Video can play");
setIsLoading(false);
}}
onPlay={handleVideoPlay}
onPause={handleVideoPause}
onError={(e) => {
// eslint-disable-next-line no-console
console.error("[VideoPlayer] Video element error:", e);
setHasError(true);
setIsLoading(false);
}}
/>
</div>
);
}