140 lines
4.6 KiB
JavaScript
140 lines
4.6 KiB
JavaScript
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}`);
|
|
});
|