mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-14 00:03:42 +08:00
189 lines
8.8 KiB
TypeScript
189 lines
8.8 KiB
TypeScript
import { useEffect, useRef } from 'react';
|
|
import Hls from 'hls.js';
|
|
|
|
interface UseHlsPlayerProps {
|
|
videoRef: React.RefObject<HTMLVideoElement | null>;
|
|
src: string;
|
|
autoPlay?: boolean;
|
|
onAutoPlayPrevented?: (error: Error) => void;
|
|
onError?: (message: string) => void;
|
|
}
|
|
|
|
export function useHlsPlayer({
|
|
videoRef,
|
|
src,
|
|
autoPlay = false,
|
|
onAutoPlayPrevented,
|
|
onError
|
|
}: UseHlsPlayerProps) {
|
|
const hlsRef = useRef<Hls | null>(null);
|
|
|
|
useEffect(() => {
|
|
const video = videoRef.current;
|
|
if (!video || !src) return;
|
|
|
|
// Cleanup previous HLS instance
|
|
if (hlsRef.current) {
|
|
hlsRef.current.destroy();
|
|
hlsRef.current = null;
|
|
}
|
|
|
|
let hls: Hls | null = null;
|
|
|
|
// Check if HLS is supported natively (Safari, Mobile Chrome)
|
|
// We prefer native playback if available as it's usually more battery efficient
|
|
const isNativeHlsSupported = video.canPlayType('application/vnd.apple.mpegurl');
|
|
|
|
if (Hls.isSupported()) {
|
|
// Use hls.js for browsers without native support (Desktop Chrome, Firefox, Edge)
|
|
// OR if we want to force hls.js for better control (optional, but sticking to native first is safer)
|
|
|
|
// Note: Some desktop browsers (like Safari) support native HLS.
|
|
// We usually prefer native, BUT sometimes native implementation is buggy or lacks features.
|
|
// For now, we follow the standard pattern: Native first, then HLS.js.
|
|
// EXCEPT for Chrome on Desktop which reports canPlayType as '' (false).
|
|
|
|
if (!isNativeHlsSupported) {
|
|
hls = new Hls({
|
|
// Worker & Performance
|
|
enableWorker: true,
|
|
lowLatencyMode: false, // Disable low latency for more stable playback
|
|
|
|
// Buffer Settings - More aggressive buffering for smoother playback
|
|
maxBufferLength: 60, // Buffer up to 60 seconds ahead
|
|
maxMaxBufferLength: 120, // Allow up to 2 minutes of buffer
|
|
maxBufferSize: 60 * 1000 * 1000, // 60MB buffer size
|
|
maxBufferHole: 0.5, // Allow small gaps in buffer
|
|
|
|
// Start with more buffer before playing
|
|
startFragPrefetch: true, // Enable prefetching next fragment
|
|
|
|
// ABR (Adaptive Bitrate) Settings - Be more conservative
|
|
abrEwmaDefaultEstimate: 500000, // Start with conservative bandwidth estimate (500kbps)
|
|
abrEwmaFastLive: 3, // Fast adaptation for live
|
|
abrEwmaSlowLive: 9, // Slow adaptation for live
|
|
abrEwmaFastVoD: 3, // Fast adaptation for VoD
|
|
abrEwmaSlowVoD: 9, // Slow adaptation for VoD
|
|
abrBandWidthFactor: 0.8, // Use 80% of estimated bandwidth (conservative)
|
|
abrBandWidthUpFactor: 0.7, // Even more conservative when switching up
|
|
|
|
// Loading Settings - More retries and longer timeouts
|
|
fragLoadingMaxRetry: 6,
|
|
fragLoadingRetryDelay: 1000,
|
|
fragLoadingMaxRetryTimeout: 64000,
|
|
manifestLoadingMaxRetry: 4,
|
|
manifestLoadingRetryDelay: 1000,
|
|
manifestLoadingMaxRetryTimeout: 64000,
|
|
levelLoadingMaxRetry: 4,
|
|
levelLoadingRetryDelay: 1000,
|
|
levelLoadingMaxRetryTimeout: 64000,
|
|
|
|
// Timeouts
|
|
fragLoadingTimeOut: 20000, // 20 seconds for fragment loading
|
|
manifestLoadingTimeOut: 10000, // 10 seconds for manifest
|
|
levelLoadingTimeOut: 10000, // 10 seconds for level
|
|
|
|
// Backbuffer - Keep some played content for seeking back
|
|
backBufferLength: 30, // Keep 30 seconds of played content
|
|
});
|
|
hlsRef.current = hls;
|
|
|
|
hls.loadSource(src);
|
|
hls.attachMedia(video);
|
|
|
|
hls.on(Hls.Events.FRAG_LOADED, (event, data) => {
|
|
// Force play if we have the first segment and it's not playing yet
|
|
// detailed: data.frag.sn is the sequence number
|
|
if (autoPlay && video.paused && data.frag.start === 0) {
|
|
video.play().catch(console.warn);
|
|
}
|
|
});
|
|
|
|
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
|
|
|
// Check for HEVC/H.265 codec (limited browser support)
|
|
if (hls) {
|
|
const levels = hls.levels;
|
|
if (levels && levels.length > 0) {
|
|
const hasHEVC = levels.some(level =>
|
|
level.videoCodec?.toLowerCase().includes('hev') ||
|
|
level.videoCodec?.toLowerCase().includes('h265')
|
|
);
|
|
if (hasHEVC) {
|
|
console.warn('[HLS] ⚠️ HEVC/H.265 codec detected - may not play in all browsers');
|
|
console.warn('[HLS] Supported: Safari with hardware acceleration, some Edge versions');
|
|
console.warn('[HLS] Not supported: Most Chrome/Firefox versions');
|
|
// Notify parent about potential codec issues
|
|
onError?.('检测到 HEVC/H.265 编码,当前浏览器可能不支持');
|
|
}
|
|
}
|
|
}
|
|
|
|
if (autoPlay) {
|
|
video.play().catch((err) => {
|
|
console.warn('[HLS] Autoplay prevented:', err);
|
|
onAutoPlayPrevented?.(err);
|
|
});
|
|
}
|
|
});
|
|
|
|
let networkErrorRetries = 0;
|
|
let mediaErrorRetries = 0;
|
|
const MAX_RETRIES = 3;
|
|
|
|
hls.on(Hls.Events.ERROR, (event, data) => {
|
|
if (data.fatal) {
|
|
switch (data.type) {
|
|
case Hls.ErrorTypes.NETWORK_ERROR:
|
|
networkErrorRetries++;
|
|
console.error(`[HLS] Network error (${networkErrorRetries}/${MAX_RETRIES}), trying to recover...`, data);
|
|
if (networkErrorRetries <= MAX_RETRIES) {
|
|
hls?.startLoad();
|
|
} else {
|
|
console.error('[HLS] Too many network errors, giving up');
|
|
onError?.('网络错误:无法加载视频流');
|
|
hls?.destroy();
|
|
}
|
|
break;
|
|
case Hls.ErrorTypes.MEDIA_ERROR:
|
|
mediaErrorRetries++;
|
|
console.error(`[HLS] Media error (${mediaErrorRetries}/${MAX_RETRIES}), trying to recover...`, data);
|
|
if (mediaErrorRetries <= MAX_RETRIES) {
|
|
hls?.recoverMediaError();
|
|
} else {
|
|
console.error('[HLS] Too many media errors, giving up');
|
|
onError?.('媒体错误:视频格式不支持或已损坏');
|
|
hls?.destroy();
|
|
}
|
|
break;
|
|
default:
|
|
console.error('[HLS] Fatal error, cannot recover:', data);
|
|
onError?.(`致命错误:${data.details || '未知错误'}`);
|
|
hls?.destroy();
|
|
break;
|
|
}
|
|
} else {
|
|
// Non-fatal errors
|
|
console.warn('[HLS] Non-fatal error:', data.type, data.details);
|
|
}
|
|
});
|
|
} else {
|
|
// Native HLS support
|
|
video.src = src;
|
|
}
|
|
} else if (isNativeHlsSupported) {
|
|
// Fallback for environments where Hls.js is not supported but native is (e.g. iOS without MSE?)
|
|
video.src = src;
|
|
} else {
|
|
console.error('[HLS] HLS not supported in this browser');
|
|
onError?.('当前浏览器不支持 HLS 视频播放');
|
|
}
|
|
|
|
return () => {
|
|
if (hls) {
|
|
hls.destroy();
|
|
}
|
|
};
|
|
}, [src, videoRef, autoPlay, onAutoPlayPrevented, onError]);
|
|
}
|