From 4b0b5a75bc8f05e151a9f7f8dfed4624341377cf Mon Sep 17 00:00:00 2001 From: CN-JS-HuiBai Date: Thu, 2 Apr 2026 17:57:53 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E7=95=8C=E9=9D=A2=EF=BC=8C=E5=A2=9E=E5=8A=A0=E4=BA=86=E7=BC=96?= =?UTF-8?q?=E7=A0=81=E6=96=B9=E5=BC=8F=E5=92=8C=E7=A1=AC=E4=BB=B6=E7=BC=96?= =?UTF-8?q?=E7=A0=81=E5=99=A8=E7=9A=84=E9=80=89=E6=8B=A9=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86=E6=92=AD=E6=94=BE=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E5=92=8C=E8=BD=AC=E7=A0=81=E8=BF=9B=E5=BA=A6=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E3=80=82=E5=90=8C=E6=97=B6=E5=9C=A8=E5=90=8E=E7=AB=AF=E5=BC=95?= =?UTF-8?q?=E5=85=A5=E4=BA=86fluent-ffmpeg=E5=BA=93=E6=9D=A5=E5=A4=84?= =?UTF-8?q?=E7=90=86=E8=A7=86=E9=A2=91=E8=BD=AC=E7=A0=81=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E4=BD=BF=E7=94=A8WebSocket=E5=AE=9E=E6=97=B6=E4=BC=A0=E8=BE=93?= =?UTF-8?q?=E8=BD=AC=E7=A0=81=E8=BF=9B=E5=BA=A6=E4=BF=A1=E6=81=AF=E5=88=B0?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/index.html | 4 +-- public/js/main.js | 37 +++++++++----------------- server.js | 67 +++++++++++++++++++++++++---------------------- 3 files changed, 48 insertions(+), 60 deletions(-) diff --git a/public/index.html b/public/index.html index c5bfffc..db4ec5b 100644 --- a/public/index.html +++ b/public/index.html @@ -9,8 +9,6 @@ - -
@@ -21,7 +19,7 @@

S3 Media Transcoder

-

Seamlessly stream videos from AWS S3 dynamically via FFmpeg HLS Transcoding

+

Seamlessly transcode videos from AWS S3 to MP4 for browser playback

diff --git a/public/js/main.js b/public/js/main.js index 12d3377..fcff542 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -246,7 +246,7 @@ document.addEventListener('DOMContentLoaded', () => { if (data.error) throw new Error(data.error); if (!wsConnected) { - pollForHlsReady(key, data.hlsUrl); + pollForHlsReady(key, data.mp4Url); } } catch (err) { console.error(err); @@ -254,7 +254,7 @@ document.addEventListener('DOMContentLoaded', () => { } }; - // Poll the backend to check if the generated m3u8 file is accessible + // Poll the backend to check if the generated MP4 file is accessible const pollForHlsReady = (key, hlsUrl) => { let attempts = 0; const maxAttempts = 120; // 60 seconds max wait for first segment @@ -291,35 +291,22 @@ document.addEventListener('DOMContentLoaded', () => { } }; - // Initialize HLS Player + // Initialize MP4 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 }); - } + videoPlayer.src = url; + videoPlayer.load(); + videoPlayer.addEventListener('loadedmetadata', () => { + if (playBtn) { + playBtn.disabled = false; + playBtn.textContent = 'Play'; + playBtn.classList.remove('hidden'); + } + }, { once: true }); }; // Bind events diff --git a/server.js b/server.js index d3a7795..d43c407 100644 --- a/server.js +++ b/server.js @@ -77,8 +77,8 @@ wss.on('connection', (ws) => { const currentProgress = progressMap[message.key]; if (currentProgress) { ws.send(JSON.stringify({ type: 'progress', key: message.key, progress: currentProgress })); - if (currentProgress.status === 'finished' && currentProgress.hlsUrl) { - ws.send(JSON.stringify({ type: 'ready', key: message.key, hlsUrl: currentProgress.hlsUrl })); + if (currentProgress.status === 'finished' && currentProgress.mp4Url) { + ws.send(JSON.stringify({ type: 'ready', key: message.key, mp4Url: currentProgress.mp4Url })); } } } @@ -96,11 +96,18 @@ app.get('/api/videos', async (req, res) => { if (!BUCKET_NAME) { return res.status(500).json({ error: 'S3_BUCKET_NAME not configured' }); } - const command = new ListObjectsV2Command({ - Bucket: BUCKET_NAME, - }); + const allObjects = []; + let continuationToken; - const response = await s3Client.send(command); + do { + const command = new ListObjectsV2Command({ + Bucket: BUCKET_NAME, + ContinuationToken: continuationToken, + }); + const response = await s3Client.send(command); + allObjects.push(...(response.Contents || [])); + continuationToken = response.IsTruncated ? response.NextContinuationToken : undefined; + } while (continuationToken); // Filter for a broader set of common video formats const videoExtensions = [ @@ -124,7 +131,7 @@ app.get('/api/videos', async (req, res) => { } }); -// Endpoint to transcode S3 video streaming to HLS +// Endpoint to transcode S3 video streaming to MP4 app.post('/api/transcode', async (req, res) => { const { key, codec, encoder } = req.body; @@ -144,15 +151,15 @@ 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/${progressKey}/index.m3u8`; + const outputDir = path.join(__dirname, 'public', 'mp4', ...safeKeySegments); + const mp4Path = path.join(outputDir, 'video.mp4'); + const mp4Url = `/mp4/${progressKey}/video.mp4`; - progressMap[progressKey] = { status: 'pending', percent: 0, details: 'Waiting for ffmpeg to start', hlsUrl }; + progressMap[progressKey] = { status: 'pending', percent: 0, details: 'Waiting for ffmpeg to start', mp4Url }; // If it already exists, just return the URL - if (fs.existsSync(m3u8Path)) { - return res.json({ message: 'Already transcoded', hlsUrl }); + if (fs.existsSync(mp4Path)) { + return res.json({ message: 'Already transcoded', mp4Url }); } // Create output directory if it doesn't exist @@ -167,7 +174,7 @@ app.post('/api/transcode', async (req, res) => { const response = await s3Client.send(command); const s3Stream = response.Body; - // Triggers fluent-ffmpeg to transcode to HLS + // Triggers fluent-ffmpeg to transcode to MP4 console.log(`Starting transcoding for ${key} with codec ${videoCodec}`); ffmpeg(s3Stream) @@ -175,14 +182,10 @@ app.post('/api/transcode', async (req, res) => { .audioCodec('aac') .outputOptions([ '-preset fast', - '-crf 23', - '-hls_time 6', - '-hls_list_size 0', - '-hls_allow_cache 1', - '-hls_flags independent_segments', - '-f hls' + '-crf 23' ]) - .output(m3u8Path) + .format('mp4') + .output(mp4Path) .on('progress', (progress) => { const progressState = { status: 'transcoding', @@ -192,28 +195,28 @@ app.post('/api/transcode', async (req, res) => { bitrate: progress.currentKbps || null, timemark: progress.timemark || null, details: `Transcoding... ${Math.min(Math.max(Math.round(progress.percent || 0), 0), 100)}%`, - hlsUrl + mp4Url }; progressMap[progressKey] = progressState; broadcastWs(progressKey, { type: 'progress', key: progressKey, progress: progressState }); }) .on('end', () => { - console.log(`Finished transcoding ${key} to HLS`); - const progressState = { status: 'finished', percent: 100, details: 'Transcoding complete', hlsUrl }; + console.log(`Finished transcoding ${key} to MP4`); + const progressState = { status: 'finished', percent: 100, details: 'Transcoding complete', mp4Url }; progressMap[progressKey] = progressState; broadcastWs(progressKey, { type: 'progress', key: progressKey, progress: progressState }); - broadcastWs(progressKey, { type: 'ready', key: progressKey, hlsUrl }); + broadcastWs(progressKey, { type: 'ready', key: progressKey, mp4Url }); }) .on('error', (err) => { console.error(`Error transcoding ${key}:`, err); - const failedState = { status: 'failed', percent: progressMap[progressKey]?.percent || 0, details: err.message || 'Transcoding failed', hlsUrl }; + const failedState = { status: 'failed', percent: progressMap[progressKey]?.percent || 0, details: err.message || 'Transcoding failed', mp4Url }; progressMap[progressKey] = failedState; broadcastWs(progressKey, { type: 'progress', key: progressKey, progress: failedState }); }) .run(); // Return immediately so the client can start polling or waiting - res.json({ message: 'Transcoding started', hlsUrl }); + res.json({ message: 'Transcoding started', mp4Url }); } catch (error) { console.error('Error in transcode:', error); @@ -221,19 +224,19 @@ app.post('/api/transcode', async (req, res) => { } }); -// Status check for HLS playlist availability +// Status check for MP4 availability app.get('/api/status', (req, res) => { const { key } = req.query; 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 mp4Path = path.join(__dirname, 'public', 'mp4', ...safeKeySegments, 'video.mp4'); 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`, progress }); + // Check if the MP4 file exists + if (fs.existsSync(mp4Path)) { + res.json({ ready: true, mp4Url: `/mp4/${safeKeySegments.join('/')}/video.mp4`, progress }); } else { res.json({ ready: false, progress }); }