초기 커밋: DefVideo 소스 등록
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>
This commit is contained in:
78
server/src/services/streaming.ts
Normal file
78
server/src/services/streaming.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user