import fs from 'fs'; import { promises as fsp } from 'fs'; import path from 'path'; import { pipeline } from 'stream/promises'; import { Request, Response } from 'express'; import { config } from '../config'; const CHUNK_SIZE = 10 * 1024 * 1024; // 10MB export function resolveVideoPath(videoId: string): string { const filePath = path.resolve(config.videosDir, videoId); // Path traversal guard if (!filePath.startsWith(path.resolve(config.videosDir))) { throw new Error('Invalid video path'); } return filePath; } export async function streamVideo(req: Request, res: Response): Promise { const { videoId } = req.params; let filePath: string; try { filePath = resolveVideoPath(videoId); } catch { res.status(400).json({ error: 'Invalid video ID' }); return; } let stat: fs.Stats; try { stat = await fsp.stat(filePath); } catch { res.status(404).json({ error: 'Video not found' }); return; } const fileSize = stat.size; const rangeHeader = req.headers.range; if (!rangeHeader) { res.writeHead(200, { 'Content-Type': 'video/mp4', 'Content-Length': fileSize, 'Accept-Ranges': 'bytes', }); const stream = fs.createReadStream(filePath, { highWaterMark: 1024 * 1024 }); await pipeline(stream, res); return; } const [startStr, endStr] = rangeHeader.replace(/bytes=/, '').split('-'); const start = parseInt(startStr, 10); const end = Math.min( endStr ? parseInt(endStr, 10) : start + CHUNK_SIZE - 1, fileSize - 1 ); if (start > end || start >= fileSize) { res.writeHead(416, { 'Content-Range': `bytes */${fileSize}` }); res.end(); return; } res.writeHead(206, { 'Content-Range': `bytes ${start}-${end}/${fileSize}`, 'Accept-Ranges': 'bytes', 'Content-Length': end - start + 1, 'Content-Type': 'video/mp4', }); const stream = fs.createReadStream(filePath, { start, end, highWaterMark: 1024 * 1024, }); await pipeline(stream, res); }