254 lines
7.1 KiB
TypeScript
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>
|
|
);
|
|
}
|