abcVideo 플레이어 소스 (client / server / shared / pythonsource / docs / .claude). .gitignore 적용으로 node_modules·storage·samplevideo·미디어 등 대용량 일괄 제외. 103 files, ~964K. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
79 lines
2.0 KiB
TypeScript
79 lines
2.0 KiB
TypeScript
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<void> {
|
|
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);
|
|
}
|