diff --git a/public/js/main.js b/public/js/main.js index 1766b46..27532b8 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -63,6 +63,7 @@ document.addEventListener('DOMContentLoaded', () => { let seekOffset = 0; let isDraggingSeek = false; let isStreamActive = false; + let pendingSeekTimeout = null; const formatBytes = (bytes) => { if (!bytes || bytes === 0) return '0 B'; @@ -101,6 +102,8 @@ document.addEventListener('DOMContentLoaded', () => { if (message.type === 'duration' && message.duration) { videoDuration = message.duration; if (seekTotalTime) seekTotalTime.textContent = formatTime(videoDuration); + showSeekBar(); + updateSeekBarPosition(seekOffset + (videoPlayer.currentTime || 0)); } if (message.type === 'progress') { handleProgress(message.progress); @@ -283,9 +286,15 @@ document.addEventListener('DOMContentLoaded', () => { if (!selectedKey || !selectedBucket || videoDuration <= 0) return; targetSeconds = Math.max(0, Math.min(targetSeconds, videoDuration - 0.5)); seekOffset = targetSeconds; + isStreamActive = false; + updateSeekBarPosition(targetSeconds); // Show seeking indicator if (seekingOverlay) seekingOverlay.classList.remove('hidden'); + if (pendingSeekTimeout) { + clearTimeout(pendingSeekTimeout); + pendingSeekTimeout = null; + } // Build new stream URL with ss parameter const codec = codecSelect?.value || 'h264'; @@ -304,11 +313,12 @@ document.addEventListener('DOMContentLoaded', () => { videoPlayer.play().catch(() => {}); videoPlayer.removeEventListener('canplay', onCanPlay); }; - videoPlayer.addEventListener('canplay', onCanPlay); + videoPlayer.addEventListener('canplay', onCanPlay, { once: true }); // Timeout fallback: hide seeking overlay after 8s even if canplay doesn't fire - setTimeout(() => { + pendingSeekTimeout = setTimeout(() => { if (seekingOverlay) seekingOverlay.classList.add('hidden'); + pendingSeekTimeout = null; }, 8000); }; @@ -591,9 +601,13 @@ document.addEventListener('DOMContentLoaded', () => { playerOverlay.classList.add('hidden'); videoPlayer.classList.add('hidden'); videoPlayer.pause(); + videoPlayer.removeAttribute('src'); + videoPlayer.load(); isStreamActive = false; videoDuration = 0; seekOffset = 0; + if (seekCurrentTime) seekCurrentTime.textContent = formatTime(0); + if (seekTotalTime) seekTotalTime.textContent = formatTime(0); hideSeekBar(); if (transcodeBtn) { @@ -652,6 +666,7 @@ document.addEventListener('DOMContentLoaded', () => { videoPlayer.classList.remove('hidden'); isStreamActive = true; showSeekBar(); + updateSeekBarPosition(seekOffset); if (playBtn) { playBtn.disabled = false; playBtn.textContent = 'Play'; @@ -680,6 +695,9 @@ document.addEventListener('DOMContentLoaded', () => { handleProgress({ status: 'cancelled', percent: 0, details: 'Transcode stopped' }); isStreamActive = false; + videoPlayer.pause(); + videoPlayer.removeAttribute('src'); + videoPlayer.load(); hideSeekBar(); if (transcodeBtn) { transcodeBtn.disabled = false; @@ -719,6 +737,7 @@ document.addEventListener('DOMContentLoaded', () => { isStreamActive = true; showSeekBar(); videoPlayer.addEventListener('loadedmetadata', () => { + updateSeekBarPosition(seekOffset); if (playBtn) { playBtn.disabled = false; playBtn.textContent = 'Play'; diff --git a/server.js b/server.js index d5bfba1..d1a11f7 100644 --- a/server.js +++ b/server.js @@ -120,6 +120,29 @@ const probeFile = (filePath) => { }); }; +const stopActiveTranscode = (progressKey) => { + const activeCommand = transcodeProcesses.get(progressKey); + if (!activeCommand) { + return; + } + + try { + if (typeof activeCommand.removeAllListeners === 'function') { + activeCommand.removeAllListeners('progress'); + activeCommand.removeAllListeners('stderr'); + activeCommand.removeAllListeners('end'); + activeCommand.removeAllListeners('error'); + } + if (typeof activeCommand.kill === 'function') { + activeCommand.kill('SIGKILL'); + } + } catch (killError) { + console.warn(`Failed to kill transcode process for ${progressKey}:`, killError); + } finally { + transcodeProcesses.delete(progressKey); + } +}; + const extractS3Credentials = (req) => { const query = req.query || {}; const username = req.headers['x-s3-username'] || req.body?.username || query.username || query.accessKeyId || ''; @@ -145,6 +168,9 @@ wss.on('connection', (ws) => { const currentProgress = progressMap[progressKey]; if (currentProgress) { + if (typeof currentProgress.duration === 'number' && currentProgress.duration > 0) { + ws.send(JSON.stringify({ type: 'duration', key: message.key, duration: currentProgress.duration })); + } ws.send(JSON.stringify({ type: 'progress', key: message.key, progress: currentProgress })); if (currentProgress.status === 'finished' && currentProgress.mp4Url) { ws.send(JSON.stringify({ type: 'ready', key: message.key, mp4Url: currentProgress.mp4Url })); @@ -260,20 +286,10 @@ app.post('/api/stop-transcode', (req, res) => { } const progressKey = getProgressKey(key); - const command = transcodeProcesses.get(progressKey); - if (!command) { + if (!transcodeProcesses.has(progressKey)) { return res.status(404).json({ error: 'No active transcode found for this key' }); } - - try { - if (typeof command.kill === 'function') { - command.kill('SIGKILL'); - } - } catch (killError) { - console.warn('Failed to kill transcode process:', killError); - } - - transcodeProcesses.delete(progressKey); + stopActiveTranscode(progressKey); progressMap[progressKey] = { status: 'cancelled', percent: 0, @@ -323,6 +339,8 @@ app.get('/api/stream', async (req, res) => { const s3Client = createS3Client(auth); try { + stopActiveTranscode(progressKey); + let totalBytes = 0; let downloadedBytes = 0; @@ -392,6 +410,10 @@ app.get('/api/stream', async (req, res) => { try { const metadata = await probeFile(tmpInputPath); const duration = metadata.format?.duration || 0; + progressMap[progressKey] = { + ...(progressMap[progressKey] || {}), + duration: parseFloat(duration) || 0 + }; broadcastWs(progressKey, { type: 'duration', key, duration: parseFloat(duration) }); } catch (probeErr) { console.error('Probe failed:', probeErr); @@ -425,14 +447,23 @@ app.get('/api/stream', async (req, res) => { ffmpegCommand .on('progress', (progress) => { + const effectiveDuration = Math.max(0, (progressMap[progressKey]?.duration || 0) - startSeconds); + const timemarkSeconds = ffmpeg.timemarkToSeconds(progress.timemark || '0'); + const absoluteSeconds = startSeconds + (isFinite(timemarkSeconds) ? timemarkSeconds : 0); + const percent = effectiveDuration > 0 + ? Math.min(Math.max(Math.round((absoluteSeconds / (progressMap[progressKey]?.duration || effectiveDuration)) * 100), 0), 100) + : Math.min(Math.max(Math.round(progress.percent || 0), 0), 100); const progressState = { status: 'transcoding', - percent: Math.min(Math.max(Math.round(progress.percent || 0), 0), 100), + percent, frame: progress.frames || null, fps: progress.currentFps || null, bitrate: progress.currentKbps || null, timemark: progress.timemark || null, - details: `Streaming transcode ${Math.min(Math.max(Math.round(progress.percent || 0), 0), 100)}%`, + absoluteSeconds, + duration: progressMap[progressKey]?.duration || null, + startSeconds, + details: `Streaming transcode ${percent}%`, mp4Url: null }; progressMap[progressKey] = progressState; @@ -446,6 +477,8 @@ app.get('/api/stream', async (req, res) => { progressMap[progressKey] = { status: 'finished', percent: 100, + duration: progressMap[progressKey]?.duration || null, + startSeconds, details: 'Streaming transcode complete', mp4Url: null }; @@ -456,6 +489,8 @@ app.get('/api/stream', async (req, res) => { const failedState = { status: 'failed', percent: progressMap[progressKey]?.percent || 0, + duration: progressMap[progressKey]?.duration || null, + startSeconds, details: err.message || 'Streaming transcode failed', mp4Url: null }; @@ -477,6 +512,9 @@ app.get('/api/stream', async (req, res) => { ffmpegCommand.kill('SIGKILL'); } catch (_) {} } + if (transcodeProcesses.get(progressKey) === ffmpegCommand) { + transcodeProcesses.delete(progressKey); + } }); startStream(videoCodec);