diff --git a/public/css/style.css b/public/css/style.css index 50311aa..2f23745 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -401,6 +401,27 @@ header p { font-weight: 600; } +.play-btn { + margin-top: 1rem; + padding: 0.85rem 1.25rem; + border: none; + border-radius: 999px; + background: var(--accent); + color: #fff; + font-weight: 700; + cursor: pointer; + transition: transform 0.2s ease, background 0.2s ease; +} + +.play-btn:hover { + background: var(--accent-hover); + transform: translateY(-1px); +} + +.play-btn.hidden { + display: none !important; +} + .hidden { display: none !important; } diff --git a/public/index.html b/public/index.html index 0273a9f..4a59ffe 100644 --- a/public/index.html +++ b/public/index.html @@ -39,6 +39,14 @@ +
+ + +

Fetching S3 Objects...

@@ -63,6 +71,7 @@ diff --git a/public/js/main.js b/public/js/main.js index df7836f..be17cff 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -3,14 +3,23 @@ document.addEventListener('DOMContentLoaded', () => { const loadingSpinner = document.getElementById('loading-spinner'); const refreshBtn = document.getElementById('refresh-btn'); const codecSelect = document.getElementById('codec-select'); + const encoderSelect = document.getElementById('encoder-select'); const playerOverlay = document.getElementById('player-overlay'); const transcodingOverlay = document.getElementById('transcoding-overlay'); const videoPlayer = document.getElementById('video-player'); const nowPlaying = document.getElementById('now-playing'); const currentVideoTitle = document.getElementById('current-video-title'); + const playBtn = document.getElementById('play-btn'); let currentPollInterval = null; + if (playBtn) { + playBtn.addEventListener('click', () => { + videoPlayer.play().catch(e => console.log('Play blocked:', e)); + playBtn.classList.add('hidden'); + }); + } + // Fetch list of videos from the backend const fetchVideos = async () => { videoListEl.classList.add('hidden'); @@ -134,6 +143,7 @@ document.addEventListener('DOMContentLoaded', () => { playerOverlay.classList.add('hidden'); videoPlayer.classList.add('hidden'); videoPlayer.pause(); + if (playBtn) playBtn.classList.add('hidden'); nowPlaying.classList.remove('hidden'); currentVideoTitle.textContent = key.split('/').pop(); @@ -142,10 +152,11 @@ document.addEventListener('DOMContentLoaded', () => { try { const codec = codecSelect?.value || 'h264'; + const encoder = encoderSelect?.value || 'software'; const res = await fetch('/api/transcode', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ key, codec }) + body: JSON.stringify({ key, codec, encoder }) }); const data = await res.json(); @@ -170,7 +181,7 @@ document.addEventListener('DOMContentLoaded', () => { const res = await fetch(`/api/status?key=${encodeURIComponent(key)}`); const data = await res.json(); - if (data.ready) { + if (data.ready) { stopPolling(); playHlsStream(data.hlsUrl); } else if (attempts >= maxAttempts) { @@ -194,20 +205,21 @@ document.addEventListener('DOMContentLoaded', () => { const playHlsStream = (url) => { transcodingOverlay.classList.add('hidden'); videoPlayer.classList.remove('hidden'); + playBtn.classList.add('hidden'); if (Hls.isSupported()) { const hls = new Hls(); hls.loadSource(url); hls.attachMedia(videoPlayer); hls.on(Hls.Events.MANIFEST_PARSED, () => { - videoPlayer.play().catch(e => console.log('Auto-play blocked')); + playBtn.classList.remove('hidden'); }); } else if (videoPlayer.canPlayType('application/vnd.apple.mpegurl')) { // Safari uses native HLS videoPlayer.src = url; videoPlayer.addEventListener('loadedmetadata', () => { - videoPlayer.play().catch(e => console.log('Auto-play blocked')); - }); + playBtn.classList.remove('hidden'); + }, { once: true }); } }; diff --git a/server.js b/server.js index d242a66..f34f019 100644 --- a/server.js +++ b/server.js @@ -65,14 +65,20 @@ app.get('/api/videos', async (req, res) => { // Endpoint to transcode S3 video streaming to HLS app.post('/api/transcode', async (req, res) => { - const { key, codec } = req.body; + const { key, codec, encoder } = req.body; if (!key) { return res.status(400).json({ error: 'Video key is required' }); } const safeCodec = codec === 'h265' ? 'h265' : 'h264'; - const videoCodec = safeCodec === 'h265' ? 'libx265' : 'libx264'; + const safeEncoder = ['nvidia', 'intel'].includes(encoder) ? encoder : 'software'; + const codecMap = { + software: { h264: 'libx264', h265: 'libx265' }, + nvidia: { h264: 'h264_nvenc', h265: 'hevc_nvenc' }, + intel: { h264: 'h264_qsv', h265: 'hevc_qsv' } + }; + const videoCodec = codecMap[safeEncoder][safeCodec]; try { const safeKeySegments = key.split('/').map(segment => segment.replace(/[^a-zA-Z0-9_\-]/g, '_'));