diff --git a/public/css/style.css b/public/css/style.css index 2f23745..8b18821 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -401,6 +401,35 @@ header p { font-weight: 600; } +.progress-info { + width: 100%; + margin-top: 1rem; + display: flex; + flex-direction: column; + gap: 0.75rem; + align-items: stretch; +} + +.progress-text { + color: var(--text-secondary); + font-size: 0.95rem; +} + +.progress-bar { + width: 100%; + height: 10px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.12); + overflow: hidden; +} + +.progress-fill { + width: 0%; + height: 100%; + background: linear-gradient(90deg, var(--accent), var(--accent-hover)); + transition: width 0.25s ease; +} + .play-btn { margin-top: 1rem; padding: 0.85rem 1.25rem; diff --git a/public/index.html b/public/index.html index 4a59ffe..c5bfffc 100644 --- a/public/index.html +++ b/public/index.html @@ -65,6 +65,12 @@

Transcoding via FFmpeg...

Generating HLS segments, please wait.

+ diff --git a/public/js/main.js b/public/js/main.js index be17cff..1ff3ea1 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -10,9 +10,27 @@ document.addEventListener('DOMContentLoaded', () => { const nowPlaying = document.getElementById('now-playing'); const currentVideoTitle = document.getElementById('current-video-title'); const playBtn = document.getElementById('play-btn'); + const progressInfo = document.getElementById('progress-info'); + const progressText = document.getElementById('progress-text'); + const progressFill = document.getElementById('progress-fill'); let currentPollInterval = null; + const setProgress = (progress) => { + if (!progressInfo || !progressText || !progressFill) return; + const percent = Math.min(Math.max(Math.round(progress?.percent || 0), 0), 100); + progressInfo.classList.remove('hidden'); + progressText.textContent = `${progress?.details || progress?.status || 'Transcoding...'} ${percent ? `${percent}%` : ''}`.trim(); + progressFill.style.width = `${percent}%`; + }; + + const resetProgress = () => { + if (!progressInfo || !progressText || !progressFill) return; + progressInfo.classList.add('hidden'); + progressText.textContent = ''; + progressFill.style.width = '0%'; + }; + if (playBtn) { playBtn.addEventListener('click', () => { videoPlayer.play().catch(e => console.log('Play blocked:', e)); @@ -144,11 +162,13 @@ document.addEventListener('DOMContentLoaded', () => { videoPlayer.classList.add('hidden'); videoPlayer.pause(); if (playBtn) playBtn.classList.add('hidden'); + resetProgress(); nowPlaying.classList.remove('hidden'); currentVideoTitle.textContent = key.split('/').pop(); transcodingOverlay.classList.remove('hidden'); + setProgress({ status: 'Starting transcoding...', percent: 0, details: 'Starting transcoding...' }); try { const codec = codecSelect?.value || 'h264'; @@ -181,8 +201,13 @@ document.addEventListener('DOMContentLoaded', () => { const res = await fetch(`/api/status?key=${encodeURIComponent(key)}`); const data = await res.json(); - if (data.ready) { + if (data.progress) { + setProgress(data.progress); + } + + if (data.ready) { stopPolling(); + setProgress({ status: 'Ready to play', percent: 100, details: 'Ready to play' }); playHlsStream(data.hlsUrl); } else if (attempts >= maxAttempts) { stopPolling(); @@ -206,6 +231,7 @@ document.addEventListener('DOMContentLoaded', () => { transcodingOverlay.classList.add('hidden'); videoPlayer.classList.remove('hidden'); playBtn.classList.add('hidden'); + resetProgress(); if (Hls.isSupported()) { const hls = new Hls(); diff --git a/server.js b/server.js index f34f019..6bef4cb 100644 --- a/server.js +++ b/server.js @@ -28,6 +28,7 @@ const s3Client = new S3Client({ }); const BUCKET_NAME = process.env.S3_BUCKET_NAME; +const progressMap = {}; // Endpoint to list videos in the bucket app.get('/api/videos', async (req, res) => { @@ -82,9 +83,12 @@ app.post('/api/transcode', async (req, res) => { try { const safeKeySegments = key.split('/').map(segment => segment.replace(/[^a-zA-Z0-9_\-]/g, '_')); + const progressKey = safeKeySegments.join('/'); const outputDir = path.join(__dirname, 'public', 'hls', ...safeKeySegments); const m3u8Path = path.join(outputDir, 'index.m3u8'); - const hlsUrl = `/hls/${safeKeySegments.join('/')}/index.m3u8`; + const hlsUrl = `/hls/${progressKey}/index.m3u8`; + + progressMap[progressKey] = { status: 'pending', percent: 0, details: 'Waiting for ffmpeg to start' }; // If it already exists, just return the URL if (fs.existsSync(m3u8Path)) { @@ -119,11 +123,24 @@ app.post('/api/transcode', async (req, res) => { '-f hls' ]) .output(m3u8Path) + .on('progress', (progress) => { + progressMap[progressKey] = { + status: 'transcoding', + percent: Math.min(Math.max(Math.round(progress.percent || 0), 0), 100), + frame: progress.frames || null, + fps: progress.currentFps || null, + bitrate: progress.currentKbps || null, + timemark: progress.timemark || null, + details: `Transcoding... ${Math.min(Math.max(Math.round(progress.percent || 0), 0), 100)}%` + }; + }) .on('end', () => { console.log(`Finished transcoding ${key} to HLS`); + progressMap[progressKey] = { status: 'finished', percent: 100, details: 'Transcoding complete' }; }) .on('error', (err) => { console.error(`Error transcoding ${key}:`, err); + progressMap[progressKey] = { status: 'failed', percent: progressMap[progressKey]?.percent || 0, details: err.message || 'Transcoding failed' }; }) .run(); @@ -142,13 +159,15 @@ app.get('/api/status', (req, res) => { if (!key) return res.status(400).json({ error: 'Key is required' }); const safeKeySegments = key.split('/').map(segment => segment.replace(/[^a-zA-Z0-9_\-]/g, '_')); + const progressKey = safeKeySegments.join('/'); const m3u8Path = path.join(__dirname, 'public', 'hls', ...safeKeySegments, 'index.m3u8'); + const progress = progressMap[progressKey] || null; // Check if the playlist file exists if (fs.existsSync(m3u8Path)) { - res.json({ ready: true, hlsUrl: `/hls/${safeKeySegments.join('/')}/index.m3u8` }); + res.json({ ready: true, hlsUrl: `/hls/${safeKeySegments.join('/')}/index.m3u8`, progress }); } else { - res.json({ ready: false }); + res.json({ ready: false, progress }); } });