From 888ca621e4395782f3a9a30577645b8251789630 Mon Sep 17 00:00:00 2001 From: CN-JS-HuiBai Date: Sat, 4 Apr 2026 15:34:24 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=8D=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E6=B8=85=E7=A9=BA=E7=BC=93=E5=AD=98=E6=8C=89=E9=92=AE=E7=9A=84?= =?UTF-8?q?BUG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/js/main.js | 37 ++++++++++++++++- server.js | 100 +++++++++++++++++++++++++++++++--------------- 2 files changed, 104 insertions(+), 33 deletions(-) diff --git a/public/js/main.js b/public/js/main.js index ae81452..5668a1f 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -463,6 +463,34 @@ document.addEventListener('DOMContentLoaded', () => { 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) { seekBar.addEventListener('mousedown', handleSeekStart); document.addEventListener('mousemove', handleSeekMove); @@ -898,10 +926,13 @@ document.addEventListener('DOMContentLoaded', () => { hlsInstance.loadSource(streamUrl); hlsInstance.attachMedia(videoPlayer); hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => { + console.log('[HLS] MANIFEST_PARSED - starting playback'); transcodingOverlay.classList.add('hidden'); videoPlayer.classList.remove('hidden'); isStreamActive = true; - videoPlayer.play().catch(() => { }); + videoPlayer.play().catch((e) => { + console.warn('[HLS] Autoplay blocked:', e.message); + }); showSeekBar(); showCustomControls(); updatePlayControls(); @@ -910,15 +941,19 @@ document.addEventListener('DOMContentLoaded', () => { schedulePlaybackChromeHide(); }); hlsInstance.on(Hls.Events.ERROR, function (event, data) { + console.error('[HLS] Error:', data.type, data.details, data.fatal ? '(FATAL)' : '', data); if (data.fatal) { switch (data.type) { case Hls.ErrorTypes.NETWORK_ERROR: + console.warn('[HLS] Fatal network error, attempting recovery...'); hlsInstance.startLoad(); break; case Hls.ErrorTypes.MEDIA_ERROR: + console.warn('[HLS] Fatal media error, attempting recovery...'); hlsInstance.recoverMediaError(); break; default: + console.error('[HLS] Fatal error, destroying instance'); hlsInstance.destroy(); break; } diff --git a/server.js b/server.js index d362693..5d3c6a7 100644 --- a/server.js +++ b/server.js @@ -157,6 +157,20 @@ const AVAILABLE_VIDEO_DECODERS = [ 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 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); }; -const probeFile = (filePath) => { +const probeFile = (filePath, timeoutMs = 15000) => { 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) => { + clearTimeout(timer); if (err) reject(err); else resolve(metadata); }); @@ -308,6 +326,12 @@ const parseTimemarkToSeconds = (timemark) => { 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 activeProcess = transcodeProcesses.get(progressKey); if (!activeProcess?.command) { @@ -563,10 +587,8 @@ app.get('/api/videos', async (req, res) => { return videoExtensions.some(ext => lowerKey.endsWith(ext)); }) .map(key => { - const safeBucket = bucket.replace(/[^a-z0-9]/gi, '_'); - const safeKeySegments = key.split('/').map(segment => segment.replace(/[^a-z0-9]/gi, '_')); - const hlsDir = path.join(CACHE_DIR, `hls-${safeBucket}-${safeKeySegments.join('-')}`); - const tmpInputPath = path.join(CACHE_DIR, `s3-input-${safeBucket}-${safeKeySegments.join('-')}.tmp`); + const hlsDir = getHlsCacheDir(bucket, key); + const tmpInputPath = getInputCachePath(bucket, key); return { key: key, hasTranscodeCache: fs.existsSync(hlsDir), @@ -627,9 +649,7 @@ app.post('/api/clear-video-transcode-cache', async (req, res) => { if (!bucket || !key) { return res.status(400).json({ error: 'Bucket and key are required' }); } - const safeBucket = bucket.replace(/[^a-z0-9]/gi, '_'); - const safeKeySegments = key.split('/').map(segment => segment.replace(/[^a-z0-9]/gi, '_')); - const hlsDir = path.join(CACHE_DIR, `hls-${safeBucket}-${safeKeySegments.join('-')}`); + const hlsDir = getHlsCacheDir(bucket, key); if (fs.existsSync(hlsDir)) { fs.rmSync(hlsDir, { recursive: true, force: true }); @@ -647,9 +667,7 @@ app.post('/api/clear-video-download-cache', async (req, res) => { if (!bucket || !key) { return res.status(400).json({ error: 'Bucket and key are required' }); } - const safeBucket = bucket.replace(/[^a-z0-9]/gi, '_'); - const safeKeySegments = key.split('/').map(segment => segment.replace(/[^a-z0-9]/gi, '_')); - const tmpInputPath = path.join(CACHE_DIR, `s3-input-${safeBucket}-${safeKeySegments.join('-')}.tmp`); + const tmpInputPath = getInputCachePath(bucket, key); if (fs.existsSync(tmpInputPath)) { fs.rmSync(tmpInputPath, { force: true }); @@ -714,9 +732,7 @@ app.get('/api/hls/playlist.m3u8', async (req, res) => { const key = req.query.key; if (!bucket || !key) return res.status(400).send('Bad Request'); - const safeKeySegments = key.split('/').map(segment => segment.replace(/[^a-zA-Z0-9_\-]/g, '_')); - const safeBucket = bucket.replace(/[^a-zA-Z0-9_\-]/g, '_'); - const tmpInputPath = path.join(CACHE_DIR, `s3-input-${safeBucket}-${safeKeySegments.join('-')}.tmp`); + const tmpInputPath = getInputCachePath(bucket, key); const auth = await extractS3Credentials(req); const s3Client = createS3Client(auth); @@ -732,13 +748,21 @@ app.get('/api/hls/playlist.m3u8', async (req, res) => { let duration = 0; try { + console.log(`[HLS] Probing file: ${key} (${tmpInputPath})`); const metadata = await probeFile(tmpInputPath); 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); + 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`; 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`; + console.log(`[HLS] Sending m3u8 playlist to client (${m3u8.length} bytes)`); res.setHeader('Content-Type', 'application/vnd.apple.mpegurl'); res.setHeader('Cache-Control', 'no-cache'); 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'); - const safeKeySegments = key.split('/').map(segment => segment.replace(/[^a-zA-Z0-9_\-]/g, '_')); - const safeBucket = bucket.replace(/[^a-zA-Z0-9_\-]/g, '_'); - const tmpInputPath = path.join(CACHE_DIR, `s3-input-${safeBucket}-${safeKeySegments.join('-')}.tmp`); + console.log(`[HLS] Segment request: seg=${seg}, key=${key}, encoder=${requestedEncoder}`); - const progressKey = safeKeySegments.join('/'); - const hlsDir = path.join(CACHE_DIR, `hls-${safeBucket}-${progressKey}`); + const tmpInputPath = getInputCachePath(bucket, key); + + const progressKey = getProgressKey(key); + const hlsDir = getHlsCacheDir(bucket, key); if (!fs.existsSync(hlsDir)) fs.mkdirSync(hlsDir, { recursive: true }); 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)); if (needsNewProcess) { + console.log(`[HLS] Starting new FFmpeg process for seg=${seg}, key=${key}`); if (currentProcess && currentProcess.command) { + console.log(`[HLS] Killing previous FFmpeg process for ${progressKey}`); try { currentProcess.command.kill('SIGKILL'); } catch (e) { } } const startTime = Math.max(0, seg * HLS_SEGMENT_TIME); 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 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`); 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.on('error', (err) => { - console.error('HLS FFmpeg Error:', err.message); + console.error(`[HLS] FFmpeg Error for ${progressKey}:`, err.message); }); ffmpegCommand.on('progress', (progress) => { @@ -874,9 +906,9 @@ app.get('/api/hls/segment.ts', async (req, res) => { const progressState = { status: 'transcoding', percent, - frame: progress.frames || null, - fps: progress.currentFps || null, - bitrate: progress.currentKbps || null, + frame: sanitizeNumber(progress.frames), + fps: sanitizeNumber(progress.currentFps), + bitrate: sanitizeNumber(progress.currentKbps), timemark: progress.timemark || null, absoluteSeconds, duration: totalDuration || null, @@ -887,7 +919,7 @@ app.get('/api/hls/segment.ts', async (req, res) => { progressMap[progressKey] = 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', () => { @@ -895,12 +927,16 @@ app.get('/api/hls/segment.ts', async (req, res) => { }); ffmpegCommand.run(); + console.log(`[HLS] FFmpeg process started for ${progressKey}`); currentProcess = { command: ffmpegCommand, currentSeg: seg, lastActive: Date.now() }; hlsProcesses.set(progressKey, currentProcess); + } else { + console.log(`[HLS] Reusing existing FFmpeg process for seg=${seg}, currentSeg=${currentProcess?.currentSeg}`); } const ready = await waitForSegment(hlsDir, seg); if (!ready) { + console.error(`[HLS] Segment generation timeout: seg=${seg}, key=${key}`); return res.status(500).send('Segment generation timeout'); } if (currentProcess) { @@ -908,6 +944,7 @@ app.get('/api/hls/segment.ts', async (req, res) => { currentProcess.lastActive = Date.now(); } + console.log(`[HLS] Serving segment: seg=${seg}`); res.setHeader('Content-Type', 'video/MP2T'); 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 progressKey = safeKeySegments.join('/'); - const safeBucket = bucket.replace(/[^a-zA-Z0-9_\-]/g, '_'); - const tmpInputPath = path.join(CACHE_DIR, `s3-input-${safeBucket}-${safeKeySegments.join('-')}.tmp`); + const tmpInputPath = getInputCachePath(bucket, key); const cacheExists = fs.existsSync(tmpInputPath); const auth = await extractS3Credentials(req); @@ -1027,9 +1063,9 @@ app.get('/api/stream', async (req, res) => { const progressState = { status: 'transcoding', percent, - frame: progress.frames || null, - fps: progress.currentFps || null, - bitrate: progress.currentKbps || null, + frame: sanitizeNumber(progress.frames), + fps: sanitizeNumber(progress.currentFps), + bitrate: sanitizeNumber(progress.currentKbps), timemark: progress.timemark || null, absoluteSeconds, duration: progressMap[progressKey]?.duration || null,