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 });
}