const express = require('express'); const cors = require('cors'); const dotenv = require('dotenv'); const fs = require('fs'); const path = require('path'); const ffmpeg = require('fluent-ffmpeg'); const { S3Client, ListObjectsV2Command, GetObjectCommand } = require('@aws-sdk/client-s3'); dotenv.config(); const app = express(); const PORT = process.env.PORT || 3000; app.use(cors()); app.use(express.json()); app.use(express.static('public')); // Configure AWS S3 Client const s3Client = new S3Client({ region: process.env.AWS_REGION || 'us-east-1', endpoint: process.env.S3_ENDPOINT, forcePathStyle: process.env.S3_FORCE_PATH_STYLE === 'true', credentials: { accessKeyId: process.env.AWS_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY } }); const BUCKET_NAME = process.env.S3_BUCKET_NAME; // Endpoint to list videos in the bucket app.get('/api/videos', async (req, res) => { try { if (!BUCKET_NAME) { return res.status(500).json({ error: 'S3_BUCKET_NAME not configured' }); } const command = new ListObjectsV2Command({ Bucket: BUCKET_NAME, }); const response = await s3Client.send(command); // Filter for common video formats const videoExtensions = ['.mp4', '.avi', '.mov', '.mkv', '.webm', '.flv', '.wmv', '.m4v']; const videos = (response.Contents || []) .map(item => item.Key) .filter(key => { if (!key) return false; const lowerKey = key.toLowerCase(); return videoExtensions.some(ext => lowerKey.endsWith(ext)); }); res.json({ videos }); } catch (error) { console.error('Error fetching videos:', error); res.status(500).json({ error: 'Failed to fetch videos from S3', detail: error.message }); } }); // Endpoint to transcode S3 video streaming to HLS app.post('/api/transcode', async (req, res) => { const { key } = req.body; if (!key) { return res.status(400).json({ error: 'Video key is required' }); } try { const safeKeyName = key.replace(/[^a-zA-Z0-9_\-]/g, ''); const outputDir = path.join(__dirname, 'public', 'hls', safeKeyName); const m3u8Path = path.join(outputDir, 'index.m3u8'); const hlsUrl = `/hls/${safeKeyName}/index.m3u8`; // If it already exists, just return the URL if (fs.existsSync(m3u8Path)) { return res.json({ message: 'Already transcoded', hlsUrl }); } // Create output directory if it doesn't exist fs.mkdirSync(outputDir, { recursive: true }); // Get S3 stream const command = new GetObjectCommand({ Bucket: BUCKET_NAME, Key: key }); const response = await s3Client.send(command); const s3Stream = response.Body; // Triggers fluent-ffmpeg to transcode to HLS console.log(`Starting transcoding for ${key}`); ffmpeg(s3Stream) // Use hardware acceleration if available (optional for boilerplate) .outputOptions([ '-codec:v copy', // Use copy if it's already h264, else change to libx264 '-codec:a aac', '-hls_time 10', // 10 second segments '-hls_list_size 0', // keep all segments in playlist '-f hls' ]) .output(m3u8Path) .on('end', () => { console.log(`Finished transcoding ${key} to HLS`); }) .on('error', (err) => { console.error(`Error transcoding ${key}:`, err); }) .run(); // Return immediately so the client can start polling or waiting res.json({ message: 'Transcoding started', hlsUrl }); } catch (error) { console.error('Error in transcode:', error); res.status(500).json({ error: 'Failed to initiate transcoding', detail: error.message }); } }); // Status check for HLS playlist availability app.get('/api/status', (req, res) => { const { key } = req.query; if (!key) return res.status(400).json({ error: 'Key is required' }); const safeKeyName = key.replace(/[^a-zA-Z0-9_\-]/g, ''); const m3u8Path = path.join(__dirname, 'public', 'hls', safeKeyName, 'index.m3u8'); // Check if the playlist file exists if (fs.existsSync(m3u8Path)) { res.json({ ready: true, hlsUrl: `/hls/${safeKeyName}/index.m3u8` }); } else { res.json({ ready: false }); } }); app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); });