feat: Implement debounced HLS segment preloading, preserve cache across manifest fetches, and add initialization tracking.

This commit is contained in:
kuekhaoyang
2025-11-23 13:58:23 +08:00
parent 1e31a842e9
commit 9ee468b246
+51 -41
View File
@@ -12,29 +12,22 @@ export function useHLSPreloader({ src, currentTime }: UseHLSPreloaderProps) {
const segmentsRef = useRef<Segment[]>([]);
const [isManifestLoaded, setIsManifestLoaded] = useState(false);
const lastStartIndexRef = useRef<number>(-1);
const isInitializedRef = useRef(false);
const downloadTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// Fetch and parse manifest when src changes
useEffect(() => {
if (!src || !src.endsWith('.m3u8')) return;
// Reset initialization flag when src changes
isInitializedRef.current = false;
lastStartIndexRef.current = -1;
const fetchManifest = async () => {
try {
console.log('[Preloader] Fetching manifest:', src);
// Clear cache for this video on mount (fresh start each time)
if ('caches' in window) {
const cache = await caches.open('video-cache-v1');
const requests = await cache.keys();
const baseUrl = src.substring(0, src.lastIndexOf('/') + 1);
for (const request of requests) {
if (request.url.startsWith(baseUrl)) {
await cache.delete(request);
}
}
console.log('[Preloader] Cleared previous cache for this video');
}
// Parse manifest but keep existing cache (for faster playback on revisit)
const segments = await parseHLSManifest(src);
segmentsRef.current = segments;
setIsManifestLoaded(true);
@@ -48,43 +41,60 @@ export function useHLSPreloader({ src, currentTime }: UseHLSPreloaderProps) {
fetchManifest();
}, [src]);
// Manage downloads based on currentTime
// Manage downloads based on currentTime with debounce
useEffect(() => {
if (!isManifestLoaded || segmentsRef.current.length === 0) return;
// Find segment index for currentTime
let startIndex = 0;
for (let i = 0; i < segmentsRef.current.length; i++) {
if (currentTime < segmentsRef.current[i].startTime + segmentsRef.current[i].duration) {
startIndex = i;
break;
// Clear any pending download timeout
if (downloadTimeoutRef.current) {
clearTimeout(downloadTimeoutRef.current);
}
// Debounce to avoid rapid restarts during initialization
downloadTimeoutRef.current = setTimeout(() => {
// Find segment index for currentTime
let startIndex = 0;
for (let i = 0; i < segmentsRef.current.length; i++) {
if (currentTime < segmentsRef.current[i].startTime + segmentsRef.current[i].duration) {
startIndex = i;
break;
}
}
}
if (startIndex >= segmentsRef.current.length) return;
if (startIndex >= segmentsRef.current.length) return;
// Check if this is sequential playback or a seek
const diff = startIndex - lastStartIndexRef.current;
const isSequential = diff >= 0 && diff < 3;
// Check if this is sequential playback or a seek
const diff = startIndex - lastStartIndexRef.current;
const isSequential = diff >= 0 && diff < 3;
if (isSequential && abortControllerRef.current) {
return; // Continue current download
}
// Skip if already downloading sequentially
if (isSequential && isInitializedRef.current && abortControllerRef.current) {
return; // Continue current download
}
console.log(`[Preloader] Seek detected. Current Time: ${currentTime.toFixed(2)}s. Starting from segment ${startIndex}.`);
lastStartIndexRef.current = startIndex;
// Only log on significant seeks or initial start
if (!isInitializedRef.current) {
console.log(`[Preloader] Initial start at segment ${startIndex} (${currentTime.toFixed(2)}s)`);
isInitializedRef.current = true;
} else if (!isSequential) {
console.log(`[Preloader] Seek detected. Current Time: ${currentTime.toFixed(2)}s. Starting from segment ${startIndex}.`);
}
// Abort previous queue and start new one
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
abortControllerRef.current = new AbortController();
lastStartIndexRef.current = startIndex;
// Abort previous queue and start new one
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
abortControllerRef.current = new AbortController();
downloadSegmentQueue({
segments: segmentsRef.current,
startIndex,
signal: abortControllerRef.current.signal
});
}, isInitializedRef.current ? 0 : 100); // 100ms debounce on initial load only
downloadSegmentQueue({
segments: segmentsRef.current,
startIndex,
signal: abortControllerRef.current.signal
});
}, [isManifestLoaded, currentTime]);
// Cleanup on unmount