使用VALKEY缓存登录装唐
This commit is contained in:
138
server.js
138
server.js
@@ -8,6 +8,8 @@ const http = require('http');
|
||||
const WebSocket = require('ws');
|
||||
const ffmpeg = require('fluent-ffmpeg');
|
||||
const { S3Client, ListBucketsCommand, ListObjectsV2Command, GetObjectCommand } = require('@aws-sdk/client-s3');
|
||||
const crypto = require('crypto');
|
||||
const Redis = require('ioredis');
|
||||
|
||||
dotenv.config();
|
||||
|
||||
@@ -29,6 +31,12 @@ if (typeof ffmpeg.setFfprobePath === 'function') {
|
||||
ffmpeg.setFfprobePath(JELLYFIN_FFPROBE_PATH);
|
||||
}
|
||||
|
||||
const redisUrl = process.env.VALKEY_URL || process.env.REDIS_URL || 'redis://localhost:6379';
|
||||
const redisDb = parseInt(process.env.VALKEY_DB || process.env.REDIS_DB || '0', 10);
|
||||
const redisClient = new Redis(redisUrl, {
|
||||
db: isNaN(redisDb) ? 0 : redisDb
|
||||
});
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(express.static('public'));
|
||||
@@ -264,8 +272,20 @@ const stopActiveTranscode = (progressKey) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
const extractS3Credentials = (req) => {
|
||||
const extractS3Credentials = async (req) => {
|
||||
const query = req.query || {};
|
||||
const sessionId = req.headers['x-session-id'] || req.body?.sessionId || query.sessionId || '';
|
||||
if (sessionId) {
|
||||
try {
|
||||
const cachedCreds = await redisClient.get(`session:${sessionId}`);
|
||||
if (cachedCreds) {
|
||||
const creds = JSON.parse(cachedCreds);
|
||||
return { username: creds.username, password: creds.password };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Session retrieval error:', e);
|
||||
}
|
||||
}
|
||||
const username = req.headers['x-s3-username'] || req.body?.username || query.username || query.accessKeyId || '';
|
||||
const password = req.headers['x-s3-password'] || req.body?.password || query.password || query.secretAccessKey || '';
|
||||
return {
|
||||
@@ -328,10 +348,9 @@ const clearDownloadCache = () => {
|
||||
};
|
||||
|
||||
|
||||
// Endpoint to list available buckets
|
||||
app.get('/api/buckets', async (req, res) => {
|
||||
try {
|
||||
const auth = extractS3Credentials(req);
|
||||
const auth = await extractS3Credentials(req);
|
||||
const s3Client = createS3Client(auth);
|
||||
const command = new ListBucketsCommand({});
|
||||
const response = await s3Client.send(command);
|
||||
@@ -351,7 +370,7 @@ app.get('/api/videos', async (req, res) => {
|
||||
return res.status(400).json({ error: 'Bucket name is required' });
|
||||
}
|
||||
const allObjects = [];
|
||||
const auth = extractS3Credentials(req);
|
||||
const auth = await extractS3Credentials(req);
|
||||
const s3Client = createS3Client(auth);
|
||||
let continuationToken;
|
||||
|
||||
@@ -387,6 +406,33 @@ app.get('/api/videos', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/login', async (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
if (!username || !password) return res.status(400).json({ error: 'Missing credentials' });
|
||||
try {
|
||||
const s3Client = createS3Client({ username, password });
|
||||
const command = new ListBucketsCommand({});
|
||||
const response = await s3Client.send(command);
|
||||
const buckets = response.Buckets || [];
|
||||
|
||||
const sessionId = crypto.randomBytes(32).toString('hex');
|
||||
await redisClient.set(`session:${sessionId}`, JSON.stringify({ username, password }), 'EX', 7 * 24 * 3600);
|
||||
|
||||
res.json({ success: true, sessionId, username, buckets });
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
res.status(401).json({ error: 'Login failed', detail: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/logout', async (req, res) => {
|
||||
const sessionId = req.headers['x-session-id'] || req.body?.sessionId;
|
||||
if (sessionId) {
|
||||
try { await redisClient.del(`session:${sessionId}`); } catch (e) { }
|
||||
}
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
app.get('/api/config', (req, res) => {
|
||||
const title = process.env.APP_TITLE || 'S3 Media Transcoder';
|
||||
res.json({
|
||||
@@ -441,7 +487,7 @@ const waitForSegment = async (hlsDir, segIndex, timeoutMs = 45000) => {
|
||||
const start = Date.now();
|
||||
const segPath = path.join(hlsDir, `segment_${segIndex}.ts`);
|
||||
const m3u8Path = path.join(hlsDir, `temp.m3u8`);
|
||||
|
||||
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
if (fs.existsSync(m3u8Path)) {
|
||||
const m3u8Content = fs.readFileSync(m3u8Path, 'utf8');
|
||||
@@ -450,7 +496,7 @@ const waitForSegment = async (hlsDir, segIndex, timeoutMs = 45000) => {
|
||||
}
|
||||
if (m3u8Content.includes(`#EXT-X-ENDLIST`)) {
|
||||
if (fs.existsSync(segPath)) return true;
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
@@ -462,18 +508,18 @@ app.get('/api/hls/playlist.m3u8', async (req, res) => {
|
||||
const bucket = req.query.bucket;
|
||||
const key = req.query.key;
|
||||
if (!bucket || !key) return res.status(400).send('Bad Request');
|
||||
|
||||
|
||||
const safeKeySegments = key.split('/').map(segment => segment.replace(/[^a-zA-Z0-9_\-]/g, '_'));
|
||||
const safeBucket = bucket.replace(/[^a-zA-Z0-9_\-]/g, '_');
|
||||
const tmpInputPath = path.join(CACHE_DIR, `s3-input-${safeBucket}-${safeKeySegments.join('-')}.tmp`);
|
||||
|
||||
const auth = extractS3Credentials(req);
|
||||
|
||||
const auth = await extractS3Credentials(req);
|
||||
const s3Client = createS3Client(auth);
|
||||
let totalBytes = 0;
|
||||
const progressKey = getProgressKey(key);
|
||||
const streamSessionId = createStreamSessionId();
|
||||
let downloadedBytes = 0;
|
||||
|
||||
|
||||
if (!fs.existsSync(tmpInputPath)) {
|
||||
try {
|
||||
const command = new GetObjectCommand({ Bucket: bucket, Key: key });
|
||||
@@ -494,11 +540,11 @@ app.get('/api/hls/playlist.m3u8', async (req, res) => {
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const writeStream = fs.createWriteStream(tmpInputPath);
|
||||
|
||||
|
||||
s3Stream.on('data', (chunk) => {
|
||||
downloadedBytes += chunk.length;
|
||||
const percent = totalBytes ? Math.min(100, Math.round((downloadedBytes / totalBytes) * 100)) : 0;
|
||||
|
||||
|
||||
const downloadState = {
|
||||
status: 'downloading',
|
||||
percent,
|
||||
@@ -529,7 +575,7 @@ app.get('/api/hls/playlist.m3u8', async (req, res) => {
|
||||
};
|
||||
broadcastWs(progressKey, { type: 'progress', key, progress: progressMap[progressKey] });
|
||||
|
||||
} catch(err) {
|
||||
} catch (err) {
|
||||
console.error('S3 Download Failed:', err);
|
||||
return res.status(500).send('S3 Download Failed');
|
||||
}
|
||||
@@ -548,27 +594,27 @@ app.get('/api/hls/playlist.m3u8', async (req, res) => {
|
||||
};
|
||||
broadcastWs(progressKey, { type: 'progress', key, progress: progressMap[progressKey] });
|
||||
}
|
||||
|
||||
|
||||
let duration = 0;
|
||||
try {
|
||||
const metadata = await probeFile(tmpInputPath);
|
||||
duration = parseFloat(metadata.format?.duration || 0);
|
||||
} catch(err) {}
|
||||
} catch (err) { }
|
||||
|
||||
if (duration <= 0) duration = 3600;
|
||||
|
||||
|
||||
const totalSegments = Math.ceil(duration / HLS_SEGMENT_TIME);
|
||||
|
||||
|
||||
let m3u8 = `#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:${HLS_SEGMENT_TIME}\n#EXT-X-MEDIA-SEQUENCE:0\n#EXT-X-PLAYLIST-TYPE:VOD\n`;
|
||||
for (let i = 0; i < totalSegments; i++) {
|
||||
let segDur = HLS_SEGMENT_TIME;
|
||||
if (i === totalSegments - 1 && duration % HLS_SEGMENT_TIME !== 0) {
|
||||
segDur = (duration % HLS_SEGMENT_TIME) || HLS_SEGMENT_TIME;
|
||||
}
|
||||
m3u8 += `#EXTINF:${segDur.toFixed(6)},\nsegment.ts?bucket=${encodeURIComponent(bucket)}&key=${encodeURIComponent(key)}&seg=${i}&encoder=${req.query.encoder||'h264_rkmpp'}&decoder=${req.query.decoder||'auto'}\n`;
|
||||
m3u8 += `#EXTINF:${segDur.toFixed(6)},\nsegment.ts?bucket=${encodeURIComponent(bucket)}&key=${encodeURIComponent(key)}&seg=${i}&encoder=${req.query.encoder || 'h264_rkmpp'}&decoder=${req.query.decoder || 'auto'}\n`;
|
||||
}
|
||||
m3u8 += `#EXT-X-ENDLIST\n`;
|
||||
|
||||
|
||||
res.setHeader('Content-Type', 'application/vnd.apple.mpegurl');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.send(m3u8);
|
||||
@@ -582,17 +628,17 @@ app.get('/api/hls/segment.ts', async (req, res) => {
|
||||
const seg = parseInt(req.query.seg || '0');
|
||||
const requestedEncoder = req.query.encoder || 'h264_rkmpp';
|
||||
const requestedDecoder = req.query.decoder || 'auto';
|
||||
|
||||
|
||||
if (!bucket || !key || isNaN(seg)) return res.status(400).send('Bad Request');
|
||||
|
||||
|
||||
const safeKeySegments = key.split('/').map(segment => segment.replace(/[^a-zA-Z0-9_\-]/g, '_'));
|
||||
const safeBucket = bucket.replace(/[^a-zA-Z0-9_\-]/g, '_');
|
||||
const tmpInputPath = path.join(CACHE_DIR, `s3-input-${safeBucket}-${safeKeySegments.join('-')}.tmp`);
|
||||
|
||||
|
||||
const progressKey = safeKeySegments.join('/');
|
||||
const hlsDir = path.join(CACHE_DIR, `hls-${safeBucket}-${progressKey}`);
|
||||
if (!fs.existsSync(hlsDir)) fs.mkdirSync(hlsDir, { recursive: true });
|
||||
|
||||
|
||||
const targetSegPath = path.join(hlsDir, `segment_${seg}.ts`);
|
||||
let currentProcess = hlsProcesses.get(progressKey);
|
||||
|
||||
@@ -616,36 +662,36 @@ app.get('/api/hls/segment.ts', async (req, res) => {
|
||||
|
||||
if (needsNewProcess) {
|
||||
if (currentProcess && currentProcess.command) {
|
||||
try { currentProcess.command.kill('SIGKILL'); } catch(e){}
|
||||
try { currentProcess.command.kill('SIGKILL'); } catch (e) { }
|
||||
}
|
||||
|
||||
|
||||
const startTime = Math.max(0, seg * HLS_SEGMENT_TIME);
|
||||
|
||||
|
||||
let sourceMetadata = null;
|
||||
try { sourceMetadata = await probeFile(tmpInputPath); } catch(e){}
|
||||
|
||||
try { sourceMetadata = await probeFile(tmpInputPath); } catch (e) { }
|
||||
|
||||
const encoderName = availableEncoderValues.has(requestedEncoder) ? requestedEncoder : 'h264_rkmpp';
|
||||
const decoderName = availableDecoderValues.has(requestedDecoder) ? requestedDecoder : 'auto';
|
||||
|
||||
|
||||
const m3u8Path = path.join(hlsDir, `temp.m3u8`);
|
||||
if (fs.existsSync(m3u8Path)) fs.unlinkSync(m3u8Path);
|
||||
|
||||
|
||||
const ffmpegCommand = ffmpeg().input(tmpInputPath);
|
||||
if (startTime > 0) ffmpegCommand.seekInput(startTime);
|
||||
|
||||
|
||||
ffmpegCommand.videoCodec(encoderName).audioCodec('aac');
|
||||
|
||||
|
||||
if (isVaapiCodec(encoderName)) {
|
||||
ffmpegCommand.inputOptions(['-vaapi_device', '/dev/dri/renderD128']).videoFilters('format=nv12,hwupload');
|
||||
}
|
||||
const resolvedDecoderName = decoderName === 'auto' && isRkmppCodec(encoderName) ? getRkmppDecoderName(sourceMetadata) : decoderName;
|
||||
if (resolvedDecoderName && resolvedDecoderName !== 'auto') ffmpegCommand.inputOptions(['-c:v', resolvedDecoderName]);
|
||||
|
||||
|
||||
const segmentFilename = path.join(hlsDir, `segment_%d.ts`);
|
||||
const hlsOptions = createFfmpegOptions(encoderName).concat([
|
||||
'-f', 'hls',
|
||||
'-hls_time', HLS_SEGMENT_TIME.toString(),
|
||||
'-hls_list_size', '0',
|
||||
'-hls_list_size', '0',
|
||||
'-hls_segment_filename', segmentFilename,
|
||||
'-start_number', seg.toString(),
|
||||
'-copyts',
|
||||
@@ -653,22 +699,22 @@ app.get('/api/hls/segment.ts', async (req, res) => {
|
||||
'-muxdelay', '0',
|
||||
'-muxpreload', '0'
|
||||
]);
|
||||
|
||||
|
||||
ffmpegCommand.outputOptions(hlsOptions).output(m3u8Path);
|
||||
ffmpegCommand.on('error', (err) => {
|
||||
console.error('HLS FFmpeg Error:', err.message);
|
||||
});
|
||||
|
||||
|
||||
ffmpegCommand.on('progress', (progress) => {
|
||||
const timemarkSeconds = parseTimemarkToSeconds(progress.timemark || '0');
|
||||
const absoluteSeconds = startTime + (isFinite(timemarkSeconds) ? timemarkSeconds : 0);
|
||||
const totalDuration = parseFloat(sourceMetadata?.format?.duration || 0);
|
||||
|
||||
|
||||
let percent = 0;
|
||||
if (totalDuration > 0) {
|
||||
percent = Math.min(Math.max(Math.round((absoluteSeconds / totalDuration) * 100), 0), 100);
|
||||
}
|
||||
|
||||
|
||||
const progressState = {
|
||||
status: 'transcoding',
|
||||
percent,
|
||||
@@ -684,26 +730,26 @@ app.get('/api/hls/segment.ts', async (req, res) => {
|
||||
};
|
||||
progressMap[progressKey] = progressState;
|
||||
broadcastWs(progressKey, { type: 'progress', key, progress: progressState });
|
||||
|
||||
|
||||
console.log(`[FFmpeg] ${progressKey} | ${progress.timemark} | ${progress.currentFps}fps | ${progress.currentKbps}kbps | ${percent}%`);
|
||||
});
|
||||
|
||||
|
||||
ffmpegCommand.on('end', () => {
|
||||
console.log(`[FFmpeg] ${progressKey} HLS transcode completed.`);
|
||||
});
|
||||
|
||||
|
||||
ffmpegCommand.run();
|
||||
currentProcess = { command: ffmpegCommand, currentSeg: seg };
|
||||
hlsProcesses.set(progressKey, currentProcess);
|
||||
}
|
||||
|
||||
|
||||
const ready = await waitForSegment(hlsDir, seg);
|
||||
if (!ready) {
|
||||
return res.status(500).send('Segment generation timeout');
|
||||
}
|
||||
|
||||
|
||||
if (currentProcess) currentProcess.currentSeg = Math.max(currentProcess.currentSeg, seg);
|
||||
|
||||
|
||||
res.setHeader('Content-Type', 'video/MP2T');
|
||||
res.sendFile(targetSegPath);
|
||||
});
|
||||
@@ -734,7 +780,7 @@ app.get('/api/stream', async (req, res) => {
|
||||
const tmpInputPath = path.join(CACHE_DIR, `s3-input-${safeBucket}-${safeKeySegments.join('-')}.tmp`);
|
||||
const cacheExists = fs.existsSync(tmpInputPath);
|
||||
|
||||
const auth = extractS3Credentials(req);
|
||||
const auth = await extractS3Credentials(req);
|
||||
const s3Client = createS3Client(auth);
|
||||
|
||||
try {
|
||||
@@ -956,7 +1002,7 @@ app.get('/api/stream', async (req, res) => {
|
||||
if (ffmpegCommand && typeof ffmpegCommand.kill === 'function') {
|
||||
try {
|
||||
ffmpegCommand.kill('SIGKILL');
|
||||
} catch (_) {}
|
||||
} catch (_) { }
|
||||
}
|
||||
if (transcodeProcesses.get(progressKey)?.streamSessionId === streamSessionId) {
|
||||
transcodeProcesses.delete(progressKey);
|
||||
|
||||
Reference in New Issue
Block a user