修复不显示清空缓存按钮的BUG

This commit is contained in:
CN-JS-HuiBai
2026-04-04 15:34:24 +08:00
parent df5999c338
commit 888ca621e4
2 changed files with 104 additions and 33 deletions

View File

@@ -463,6 +463,34 @@ document.addEventListener('DOMContentLoaded', () => {
seekToTime(targetTime); seekToTime(targetTime);
}; };
const seekToTime = (targetTime) => {
if (!isStreamActive || videoDuration <= 0) return;
const clampedTime = Math.max(0, Math.min(targetTime, videoDuration));
console.log(`[Seek] Seeking to ${clampedTime.toFixed(2)}s / ${videoDuration.toFixed(2)}s`);
if (hlsInstance) {
// For HLS: seek within the current buffer if possible
const relativeTime = clampedTime - seekOffset;
if (relativeTime >= 0 && relativeTime <= (videoPlayer.duration || 0)) {
videoPlayer.currentTime = relativeTime;
} else {
// Need server-side restart from new position
seekOffset = clampedTime;
const streamUrl = buildHlsPlaylistUrl();
hlsInstance.destroy();
hlsInstance = new Hls({ maxBufferLength: 30, maxMaxBufferLength: 60 });
hlsInstance.loadSource(streamUrl);
hlsInstance.attachMedia(videoPlayer);
hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => {
videoPlayer.play().catch(() => {});
});
}
} else {
videoPlayer.currentTime = clampedTime;
}
updateSeekBarPosition(clampedTime);
};
if (seekBar) { if (seekBar) {
seekBar.addEventListener('mousedown', handleSeekStart); seekBar.addEventListener('mousedown', handleSeekStart);
document.addEventListener('mousemove', handleSeekMove); document.addEventListener('mousemove', handleSeekMove);
@@ -898,10 +926,13 @@ document.addEventListener('DOMContentLoaded', () => {
hlsInstance.loadSource(streamUrl); hlsInstance.loadSource(streamUrl);
hlsInstance.attachMedia(videoPlayer); hlsInstance.attachMedia(videoPlayer);
hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => { hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => {
console.log('[HLS] MANIFEST_PARSED - starting playback');
transcodingOverlay.classList.add('hidden'); transcodingOverlay.classList.add('hidden');
videoPlayer.classList.remove('hidden'); videoPlayer.classList.remove('hidden');
isStreamActive = true; isStreamActive = true;
videoPlayer.play().catch(() => { }); videoPlayer.play().catch((e) => {
console.warn('[HLS] Autoplay blocked:', e.message);
});
showSeekBar(); showSeekBar();
showCustomControls(); showCustomControls();
updatePlayControls(); updatePlayControls();
@@ -910,15 +941,19 @@ document.addEventListener('DOMContentLoaded', () => {
schedulePlaybackChromeHide(); schedulePlaybackChromeHide();
}); });
hlsInstance.on(Hls.Events.ERROR, function (event, data) { hlsInstance.on(Hls.Events.ERROR, function (event, data) {
console.error('[HLS] Error:', data.type, data.details, data.fatal ? '(FATAL)' : '', data);
if (data.fatal) { if (data.fatal) {
switch (data.type) { switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR: case Hls.ErrorTypes.NETWORK_ERROR:
console.warn('[HLS] Fatal network error, attempting recovery...');
hlsInstance.startLoad(); hlsInstance.startLoad();
break; break;
case Hls.ErrorTypes.MEDIA_ERROR: case Hls.ErrorTypes.MEDIA_ERROR:
console.warn('[HLS] Fatal media error, attempting recovery...');
hlsInstance.recoverMediaError(); hlsInstance.recoverMediaError();
break; break;
default: default:
console.error('[HLS] Fatal error, destroying instance');
hlsInstance.destroy(); hlsInstance.destroy();
break; break;
} }

100
server.js
View File

@@ -157,6 +157,20 @@ const AVAILABLE_VIDEO_DECODERS = [
const getProgressKey = (key) => key.split('/').map(segment => segment.replace(/[^a-zA-Z0-9_\-]/g, '_')).join('/'); const getProgressKey = (key) => key.split('/').map(segment => segment.replace(/[^a-zA-Z0-9_\-]/g, '_')).join('/');
const makeSafeName = (name) => name.replace(/[^a-zA-Z0-9_\-]/g, '_');
const getHlsCacheDir = (bucket, key) => {
const safeBucket = makeSafeName(bucket);
const safeKey = key.split('/').map(makeSafeName).join('-');
return path.join(CACHE_DIR, `hls-${safeBucket}-${safeKey}`);
};
const getInputCachePath = (bucket, key) => {
const safeBucket = makeSafeName(bucket);
const safeKey = key.split('/').map(makeSafeName).join('-');
return path.join(CACHE_DIR, `s3-input-${safeBucket}-${safeKey}.tmp`);
};
const createStreamSessionId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; const createStreamSessionId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
const addWsClient = (progressKey, ws) => { const addWsClient = (progressKey, ws) => {
@@ -276,9 +290,13 @@ const shouldRetryWithSoftware = (message) => {
return /Cannot load libcuda\.so\.1|Could not open encoder before EOF|Error while opening encoder|Operation not permitted|Invalid argument|mpp_create|rkmpp/i.test(message); return /Cannot load libcuda\.so\.1|Could not open encoder before EOF|Error while opening encoder|Operation not permitted|Invalid argument|mpp_create|rkmpp/i.test(message);
}; };
const probeFile = (filePath) => { const probeFile = (filePath, timeoutMs = 15000) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`ffprobe timed out after ${timeoutMs}ms for ${filePath}`));
}, timeoutMs);
ffmpeg.ffprobe(filePath, (err, metadata) => { ffmpeg.ffprobe(filePath, (err, metadata) => {
clearTimeout(timer);
if (err) reject(err); if (err) reject(err);
else resolve(metadata); else resolve(metadata);
}); });
@@ -308,6 +326,12 @@ const parseTimemarkToSeconds = (timemark) => {
return (hours * 3600) + (minutes * 60) + seconds; return (hours * 3600) + (minutes * 60) + seconds;
}; };
const sanitizeNumber = (value) => {
if (value === null || value === undefined) return null;
const num = Number(value);
return Number.isFinite(num) ? num : null;
};
const stopActiveTranscode = (progressKey) => { const stopActiveTranscode = (progressKey) => {
const activeProcess = transcodeProcesses.get(progressKey); const activeProcess = transcodeProcesses.get(progressKey);
if (!activeProcess?.command) { if (!activeProcess?.command) {
@@ -563,10 +587,8 @@ app.get('/api/videos', async (req, res) => {
return videoExtensions.some(ext => lowerKey.endsWith(ext)); return videoExtensions.some(ext => lowerKey.endsWith(ext));
}) })
.map(key => { .map(key => {
const safeBucket = bucket.replace(/[^a-z0-9]/gi, '_'); const hlsDir = getHlsCacheDir(bucket, key);
const safeKeySegments = key.split('/').map(segment => segment.replace(/[^a-z0-9]/gi, '_')); const tmpInputPath = getInputCachePath(bucket, key);
const hlsDir = path.join(CACHE_DIR, `hls-${safeBucket}-${safeKeySegments.join('-')}`);
const tmpInputPath = path.join(CACHE_DIR, `s3-input-${safeBucket}-${safeKeySegments.join('-')}.tmp`);
return { return {
key: key, key: key,
hasTranscodeCache: fs.existsSync(hlsDir), hasTranscodeCache: fs.existsSync(hlsDir),
@@ -627,9 +649,7 @@ app.post('/api/clear-video-transcode-cache', async (req, res) => {
if (!bucket || !key) { if (!bucket || !key) {
return res.status(400).json({ error: 'Bucket and key are required' }); return res.status(400).json({ error: 'Bucket and key are required' });
} }
const safeBucket = bucket.replace(/[^a-z0-9]/gi, '_'); const hlsDir = getHlsCacheDir(bucket, key);
const safeKeySegments = key.split('/').map(segment => segment.replace(/[^a-z0-9]/gi, '_'));
const hlsDir = path.join(CACHE_DIR, `hls-${safeBucket}-${safeKeySegments.join('-')}`);
if (fs.existsSync(hlsDir)) { if (fs.existsSync(hlsDir)) {
fs.rmSync(hlsDir, { recursive: true, force: true }); fs.rmSync(hlsDir, { recursive: true, force: true });
@@ -647,9 +667,7 @@ app.post('/api/clear-video-download-cache', async (req, res) => {
if (!bucket || !key) { if (!bucket || !key) {
return res.status(400).json({ error: 'Bucket and key are required' }); return res.status(400).json({ error: 'Bucket and key are required' });
} }
const safeBucket = bucket.replace(/[^a-z0-9]/gi, '_'); const tmpInputPath = getInputCachePath(bucket, key);
const safeKeySegments = key.split('/').map(segment => segment.replace(/[^a-z0-9]/gi, '_'));
const tmpInputPath = path.join(CACHE_DIR, `s3-input-${safeBucket}-${safeKeySegments.join('-')}.tmp`);
if (fs.existsSync(tmpInputPath)) { if (fs.existsSync(tmpInputPath)) {
fs.rmSync(tmpInputPath, { force: true }); fs.rmSync(tmpInputPath, { force: true });
@@ -714,9 +732,7 @@ app.get('/api/hls/playlist.m3u8', async (req, res) => {
const key = req.query.key; const key = req.query.key;
if (!bucket || !key) return res.status(400).send('Bad Request'); if (!bucket || !key) return res.status(400).send('Bad Request');
const safeKeySegments = key.split('/').map(segment => segment.replace(/[^a-zA-Z0-9_\-]/g, '_')); const tmpInputPath = getInputCachePath(bucket, key);
const safeBucket = bucket.replace(/[^a-zA-Z0-9_\-]/g, '_');
const tmpInputPath = path.join(CACHE_DIR, `s3-input-${safeBucket}-${safeKeySegments.join('-')}.tmp`);
const auth = await extractS3Credentials(req); const auth = await extractS3Credentials(req);
const s3Client = createS3Client(auth); const s3Client = createS3Client(auth);
@@ -732,13 +748,21 @@ app.get('/api/hls/playlist.m3u8', async (req, res) => {
let duration = 0; let duration = 0;
try { try {
console.log(`[HLS] Probing file: ${key} (${tmpInputPath})`);
const metadata = await probeFile(tmpInputPath); const metadata = await probeFile(tmpInputPath);
duration = parseFloat(metadata.format?.duration || 0); duration = parseFloat(metadata.format?.duration || 0);
} catch (err) { } console.log(`[HLS] Probe complete: duration=${duration}s`);
} catch (err) {
console.error(`[HLS] Probe failed for ${key}:`, err.message);
}
if (duration <= 0) duration = 3600; if (duration <= 0) {
duration = 3600;
console.warn(`[HLS] Duration invalid, using fallback: ${duration}s`);
}
const totalSegments = Math.ceil(duration / HLS_SEGMENT_TIME); const totalSegments = Math.ceil(duration / HLS_SEGMENT_TIME);
console.log(`[HLS] Generating m3u8: ${totalSegments} segments, duration=${duration}s, key=${key}`);
let m3u8 = `#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:${HLS_SEGMENT_TIME}\n#EXT-X-MEDIA-SEQUENCE:0\n#EXT-X-PLAYLIST-TYPE:VOD\n`; let m3u8 = `#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:${HLS_SEGMENT_TIME}\n#EXT-X-MEDIA-SEQUENCE:0\n#EXT-X-PLAYLIST-TYPE:VOD\n`;
for (let i = 0; i < totalSegments; i++) { for (let i = 0; i < totalSegments; i++) {
@@ -750,6 +774,7 @@ app.get('/api/hls/playlist.m3u8', async (req, res) => {
} }
m3u8 += `#EXT-X-ENDLIST\n`; m3u8 += `#EXT-X-ENDLIST\n`;
console.log(`[HLS] Sending m3u8 playlist to client (${m3u8.length} bytes)`);
res.setHeader('Content-Type', 'application/vnd.apple.mpegurl'); res.setHeader('Content-Type', 'application/vnd.apple.mpegurl');
res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Cache-Control', 'no-cache');
res.send(m3u8); res.send(m3u8);
@@ -783,12 +808,12 @@ app.get('/api/hls/segment.ts', async (req, res) => {
if (!bucket || !key || isNaN(seg)) return res.status(400).send('Bad Request'); if (!bucket || !key || isNaN(seg)) return res.status(400).send('Bad Request');
const safeKeySegments = key.split('/').map(segment => segment.replace(/[^a-zA-Z0-9_\-]/g, '_')); console.log(`[HLS] Segment request: seg=${seg}, key=${key}, encoder=${requestedEncoder}`);
const safeBucket = bucket.replace(/[^a-zA-Z0-9_\-]/g, '_');
const tmpInputPath = path.join(CACHE_DIR, `s3-input-${safeBucket}-${safeKeySegments.join('-')}.tmp`);
const progressKey = safeKeySegments.join('/'); const tmpInputPath = getInputCachePath(bucket, key);
const hlsDir = path.join(CACHE_DIR, `hls-${safeBucket}-${progressKey}`);
const progressKey = getProgressKey(key);
const hlsDir = getHlsCacheDir(bucket, key);
if (!fs.existsSync(hlsDir)) fs.mkdirSync(hlsDir, { recursive: true }); if (!fs.existsSync(hlsDir)) fs.mkdirSync(hlsDir, { recursive: true });
const targetSegPath = path.join(hlsDir, `segment_${seg}.ts`); const targetSegPath = path.join(hlsDir, `segment_${seg}.ts`);
@@ -817,17 +842,24 @@ app.get('/api/hls/segment.ts', async (req, res) => {
const needsNewProcess = !currentProcess || (!fs.existsSync(targetSegPath) && (seg < (currentProcess.currentSeg || 0) || seg > (currentProcess.currentSeg || 0) + 4)); const needsNewProcess = !currentProcess || (!fs.existsSync(targetSegPath) && (seg < (currentProcess.currentSeg || 0) || seg > (currentProcess.currentSeg || 0) + 4));
if (needsNewProcess) { if (needsNewProcess) {
console.log(`[HLS] Starting new FFmpeg process for seg=${seg}, key=${key}`);
if (currentProcess && currentProcess.command) { if (currentProcess && currentProcess.command) {
console.log(`[HLS] Killing previous FFmpeg process for ${progressKey}`);
try { currentProcess.command.kill('SIGKILL'); } catch (e) { } try { currentProcess.command.kill('SIGKILL'); } catch (e) { }
} }
const startTime = Math.max(0, seg * HLS_SEGMENT_TIME); const startTime = Math.max(0, seg * HLS_SEGMENT_TIME);
let sourceMetadata = null; let sourceMetadata = null;
try { sourceMetadata = await probeFile(tmpInputPath); } catch (e) { } try {
sourceMetadata = await probeFile(tmpInputPath);
} catch (e) {
console.error(`[HLS] Probe failed for segment transcode: ${e.message}`);
}
const encoderName = availableEncoderValues.has(requestedEncoder) ? requestedEncoder : 'h264_rkmpp'; const encoderName = availableEncoderValues.has(requestedEncoder) ? requestedEncoder : 'h264_rkmpp';
const decoderName = availableDecoderValues.has(requestedDecoder) ? requestedDecoder : 'auto'; const decoderName = availableDecoderValues.has(requestedDecoder) ? requestedDecoder : 'auto';
console.log(`[HLS] FFmpeg config: encoder=${encoderName}, decoder=${decoderName}, startTime=${startTime}s`);
const m3u8Path = path.join(hlsDir, `temp.m3u8`); const m3u8Path = path.join(hlsDir, `temp.m3u8`);
if (fs.existsSync(m3u8Path)) fs.unlinkSync(m3u8Path); if (fs.existsSync(m3u8Path)) fs.unlinkSync(m3u8Path);
@@ -858,7 +890,7 @@ app.get('/api/hls/segment.ts', async (req, res) => {
ffmpegCommand.outputOptions(hlsOptions).output(m3u8Path); ffmpegCommand.outputOptions(hlsOptions).output(m3u8Path);
ffmpegCommand.on('error', (err) => { ffmpegCommand.on('error', (err) => {
console.error('HLS FFmpeg Error:', err.message); console.error(`[HLS] FFmpeg Error for ${progressKey}:`, err.message);
}); });
ffmpegCommand.on('progress', (progress) => { ffmpegCommand.on('progress', (progress) => {
@@ -874,9 +906,9 @@ app.get('/api/hls/segment.ts', async (req, res) => {
const progressState = { const progressState = {
status: 'transcoding', status: 'transcoding',
percent, percent,
frame: progress.frames || null, frame: sanitizeNumber(progress.frames),
fps: progress.currentFps || null, fps: sanitizeNumber(progress.currentFps),
bitrate: progress.currentKbps || null, bitrate: sanitizeNumber(progress.currentKbps),
timemark: progress.timemark || null, timemark: progress.timemark || null,
absoluteSeconds, absoluteSeconds,
duration: totalDuration || null, duration: totalDuration || null,
@@ -887,7 +919,7 @@ app.get('/api/hls/segment.ts', async (req, res) => {
progressMap[progressKey] = progressState; progressMap[progressKey] = progressState;
broadcastWs(progressKey, { type: 'progress', key, progress: progressState }); broadcastWs(progressKey, { type: 'progress', key, progress: progressState });
console.log(`[FFmpeg] ${progressKey} | ${progress.timemark} | ${progress.currentFps}fps | ${progress.currentKbps}kbps | ${percent}%`); console.log(`[FFmpeg] ${progressKey} | ${progress.timemark} | ${sanitizeNumber(progress.currentFps) ?? '-'}fps | ${sanitizeNumber(progress.currentKbps) ?? '-'}kbps | ${percent}%`);
}); });
ffmpegCommand.on('end', () => { ffmpegCommand.on('end', () => {
@@ -895,12 +927,16 @@ app.get('/api/hls/segment.ts', async (req, res) => {
}); });
ffmpegCommand.run(); ffmpegCommand.run();
console.log(`[HLS] FFmpeg process started for ${progressKey}`);
currentProcess = { command: ffmpegCommand, currentSeg: seg, lastActive: Date.now() }; currentProcess = { command: ffmpegCommand, currentSeg: seg, lastActive: Date.now() };
hlsProcesses.set(progressKey, currentProcess); hlsProcesses.set(progressKey, currentProcess);
} else {
console.log(`[HLS] Reusing existing FFmpeg process for seg=${seg}, currentSeg=${currentProcess?.currentSeg}`);
} }
const ready = await waitForSegment(hlsDir, seg); const ready = await waitForSegment(hlsDir, seg);
if (!ready) { if (!ready) {
console.error(`[HLS] Segment generation timeout: seg=${seg}, key=${key}`);
return res.status(500).send('Segment generation timeout'); return res.status(500).send('Segment generation timeout');
} }
if (currentProcess) { if (currentProcess) {
@@ -908,6 +944,7 @@ app.get('/api/hls/segment.ts', async (req, res) => {
currentProcess.lastActive = Date.now(); currentProcess.lastActive = Date.now();
} }
console.log(`[HLS] Serving segment: seg=${seg}`);
res.setHeader('Content-Type', 'video/MP2T'); res.setHeader('Content-Type', 'video/MP2T');
res.sendFile(targetSegPath); res.sendFile(targetSegPath);
}); });
@@ -934,8 +971,7 @@ app.get('/api/stream', async (req, res) => {
const safeKeySegments = key.split('/').map(segment => segment.replace(/[^a-zA-Z0-9_\-]/g, '_')); const safeKeySegments = key.split('/').map(segment => segment.replace(/[^a-zA-Z0-9_\-]/g, '_'));
const progressKey = safeKeySegments.join('/'); const progressKey = safeKeySegments.join('/');
const safeBucket = bucket.replace(/[^a-zA-Z0-9_\-]/g, '_'); const tmpInputPath = getInputCachePath(bucket, key);
const tmpInputPath = path.join(CACHE_DIR, `s3-input-${safeBucket}-${safeKeySegments.join('-')}.tmp`);
const cacheExists = fs.existsSync(tmpInputPath); const cacheExists = fs.existsSync(tmpInputPath);
const auth = await extractS3Credentials(req); const auth = await extractS3Credentials(req);
@@ -1027,9 +1063,9 @@ app.get('/api/stream', async (req, res) => {
const progressState = { const progressState = {
status: 'transcoding', status: 'transcoding',
percent, percent,
frame: progress.frames || null, frame: sanitizeNumber(progress.frames),
fps: progress.currentFps || null, fps: sanitizeNumber(progress.currentFps),
bitrate: progress.currentKbps || null, bitrate: sanitizeNumber(progress.currentKbps),
timemark: progress.timemark || null, timemark: progress.timemark || null,
absoluteSeconds, absoluteSeconds,
duration: progressMap[progressKey]?.duration || null, duration: progressMap[progressKey]?.duration || null,