document.addEventListener('DOMContentLoaded', () => { const videoListEl = document.getElementById('video-list'); 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'); const progressInfo = document.getElementById('progress-info'); const progressText = document.getElementById('progress-text'); const progressFill = document.getElementById('progress-fill'); let currentPollInterval = null; let ws = null; let wsConnected = false; let subscribedKey = null; let currentVideoKey = null; const sendWsMessage = (message) => { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify(message)); } }; const subscribeToKey = (key) => { subscribedKey = key; if (wsConnected) { sendWsMessage({ type: 'subscribe', key }); } }; const handleWsMessage = (event) => { try { const message = JSON.parse(event.data); if (message.type === 'progress' && message.key === currentVideoKey) { setProgress(message.progress); } if (message.type === 'ready' && message.key === currentVideoKey) { setProgress({ status: 'Ready to play', percent: 100, details: 'Ready to play' }); playHlsStream(message.hlsUrl); } } catch (error) { console.error('WebSocket message parse error:', error); } }; const connectWebSocket = () => { const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'; ws = new WebSocket(`${protocol}://${window.location.host}`); ws.addEventListener('open', () => { wsConnected = true; if (subscribedKey) { sendWsMessage({ type: 'subscribe', key: subscribedKey }); } }); ws.addEventListener('message', handleWsMessage); ws.addEventListener('close', () => { wsConnected = false; setTimeout(connectWebSocket, 2000); }); ws.addEventListener('error', (error) => { console.error('WebSocket error:', error); wsConnected = false; }); }; 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)); playBtn.classList.add('hidden'); }); } // Fetch list of videos from the backend const fetchVideos = async () => { videoListEl.classList.add('hidden'); loadingSpinner.classList.remove('hidden'); videoListEl.innerHTML = ''; try { const res = await fetch('/api/videos'); if (!res.ok) throw new Error('Failed to fetch videos. Check S3 Config.'); const data = await res.json(); loadingSpinner.classList.add('hidden'); videoListEl.classList.remove('hidden'); if (data.videos.length === 0) { videoListEl.innerHTML = '
No videos found.
'; return; } // Build a tree structure from S3 keys, preserving original object storage directories const tree = {}; data.videos.forEach(key => { const parts = key.split('/'); let current = tree; parts.forEach((part, index) => { if (!current[part]) { current[part] = {}; } if (index === parts.length - 1) { current[part].__file = key; } current = current[part]; }); }); const createFileItem = (name, key) => { const li = document.createElement('li'); li.className = 'video-item file-item'; const ext = name.split('.').pop().toUpperCase(); li.innerHTML = `Error: ${err.message}
`; } }; // Handle video selection and trigger transcode const selectVideo = async (key, listItemNode) => { // Update UI document.querySelectorAll('.video-item').forEach(n => n.classList.remove('active')); listItemNode.classList.add('active'); // Reset player UI stopPolling(); playerOverlay.classList.add('hidden'); videoPlayer.classList.add('hidden'); videoPlayer.pause(); if (playBtn) { playBtn.disabled = true; playBtn.textContent = '等待转码完成'; playBtn.classList.remove('hidden'); } resetProgress(); currentVideoKey = key; subscribeToKey(key); 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'; const encoder = encoderSelect?.value || 'software'; const res = await fetch('/api/transcode', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key, codec, encoder }) }); const data = await res.json(); if (data.error) throw new Error(data.error); if (!wsConnected) { pollForHlsReady(key, data.hlsUrl); } } catch (err) { console.error(err); transcodingOverlay.innerHTML = `Transcode Failed: ${err.message}
`; } }; // Poll the backend to check if the generated m3u8 file is accessible const pollForHlsReady = (key, hlsUrl) => { let attempts = 0; const maxAttempts = 120; // 60 seconds max wait for first segment const pollIntervalMs = 500; currentPollInterval = setInterval(async () => { attempts++; try { const res = await fetch(`/api/status?key=${encodeURIComponent(key)}`); const data = await res.json(); 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(); transcodingOverlay.innerHTML = `Timeout waiting for HLS segments.
`; } } catch (err) { console.error("Poll Error:", err); } }, pollIntervalMs); }; const stopPolling = () => { if (currentPollInterval) { clearInterval(currentPollInterval); currentPollInterval = null; } }; // Initialize HLS Player const playHlsStream = (url) => { transcodingOverlay.classList.add('hidden'); videoPlayer.classList.remove('hidden'); playBtn.classList.add('hidden'); resetProgress(); if (Hls.isSupported()) { const hls = new Hls(); hls.loadSource(url); hls.attachMedia(videoPlayer); hls.on(Hls.Events.MANIFEST_PARSED, () => { if (playBtn) { playBtn.disabled = false; playBtn.textContent = 'Play'; playBtn.classList.remove('hidden'); } }); } else if (videoPlayer.canPlayType('application/vnd.apple.mpegurl')) { // Safari uses native HLS videoPlayer.src = url; videoPlayer.addEventListener('loadedmetadata', () => { if (playBtn) { playBtn.disabled = false; playBtn.textContent = 'Play'; playBtn.classList.remove('hidden'); } }, { once: true }); } }; // Bind events refreshBtn.addEventListener('click', fetchVideos); // Connect WebSocket and initial load connectWebSocket(); fetchVideos(); });