feat: Implement HLS download and enhance copy link functionality to support original and proxied URLs.

This commit is contained in:
kuekhaoyang
2025-11-29 11:05:04 +08:00
parent 4e7024f2bf
commit 220d1b0faf
22 changed files with 401 additions and 226 deletions
+85
View File
@@ -0,0 +1,85 @@
import { Segment } from '@/lib/utils/hlsManifestParser';
import { downloadSegmentQueue } from '@/lib/utils/segmentDownloader';
interface PreloadParams {
currentTime: number;
segments: Segment[];
videoRef: React.RefObject<HTMLVideoElement | null>;
lastStartIndexRef: React.MutableRefObject<number>;
isInitializedRef: React.MutableRefObject<boolean>;
abortControllerRef: React.MutableRefObject<AbortController | null>;
videoUrl: string;
}
export function preloadSegments({
currentTime,
segments,
videoRef,
lastStartIndexRef,
isInitializedRef,
abortControllerRef,
videoUrl
}: PreloadParams) {
// Find segment index for currentTime
let startIndex = 0;
for (let i = 0; i < segments.length; i++) {
if (currentTime < segments[i].startTime + segments[i].duration) {
startIndex = i;
break;
}
}
// Check browser buffer health
if (videoRef.current) {
const buffered = videoRef.current.buffered;
let bufferEnd = 0;
for (let i = 0; i < buffered.length; i++) {
if (buffered.start(i) <= currentTime && buffered.end(i) >= currentTime) {
bufferEnd = buffered.end(i);
break;
}
}
// If browser buffer is less than 30s ahead, let browser handle it
if (bufferEnd - currentTime < 30) {
return;
}
}
// Offset start index by 3 segments to avoid competing with browser playback
startIndex = Math.min(startIndex + 3, segments.length - 1);
if (startIndex >= segments.length) return;
// Check if this is sequential playback or a seek
const diff = startIndex - lastStartIndexRef.current;
const isSequential = diff >= 0 && diff < 3;
// Skip if already downloading sequentially
if (isSequential && isInitializedRef.current && abortControllerRef.current) {
return;
}
// 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}.`);
}
lastStartIndexRef.current = startIndex;
// Abort previous queue and start new one
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
abortControllerRef.current = new AbortController();
downloadSegmentQueue({
segments: segments,
startIndex,
signal: abortControllerRef.current.signal,
videoUrl: videoUrl
});
}
+28
View File
@@ -0,0 +1,28 @@
import { NextRequest } from 'next/server';
export async function processM3u8Content(
content: string,
baseUrl: string,
origin: string
): Promise<string> {
const lines = content.split('\n');
const base = new URL(baseUrl);
const processedLines = lines.map(line => {
// Skip comments and empty lines
if (line.trim().startsWith('#') || !line.trim()) {
return line;
}
// Resolve relative URLs
try {
const absoluteUrl = new URL(line.trim(), base).toString();
// Wrap in proxy
return `${origin}/api/proxy?url=${encodeURIComponent(absoluteUrl)}`;
} catch (e) {
return line;
}
});
return processedLines.join('\n');
}