修复不显示清空缓存按钮的BUG

This commit is contained in:
CN-JS-HuiBai
2026-04-04 15:34:24 +08:00
parent df5999c338
commit 888ca621e4
2 changed files with 104 additions and 33 deletions

100
server.js
View File

@@ -157,6 +157,20 @@ const AVAILABLE_VIDEO_DECODERS = [
const getProgressKey = (key) => key.split('/').map(segment => segment.replace(/[^a-zA-Z0-9_\-]/g, '_')).join('/');
const makeSafeName = (name) => name.replace(/[^a-zA-Z0-9_\-]/g, '_');
const getHlsCacheDir = (bucket, key) => {
const safeBucket = makeSafeName(bucket);
const safeKey = key.split('/').map(makeSafeName).join('-');
return path.join(CACHE_DIR, `hls-${safeBucket}-${safeKey}`);
};
const getInputCachePath = (bucket, key) => {
const safeBucket = makeSafeName(bucket);
const safeKey = key.split('/').map(makeSafeName).join('-');
return path.join(CACHE_DIR, `s3-input-${safeBucket}-${safeKey}.tmp`);
};
const createStreamSessionId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
const addWsClient = (progressKey, ws) => {
@@ -276,9 +290,13 @@ const shouldRetryWithSoftware = (message) => {
return /Cannot load libcuda\.so\.1|Could not open encoder before EOF|Error while opening encoder|Operation not permitted|Invalid argument|mpp_create|rkmpp/i.test(message);
};
const probeFile = (filePath) => {
const probeFile = (filePath, timeoutMs = 15000) => {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`ffprobe timed out after ${timeoutMs}ms for ${filePath}`));
}, timeoutMs);
ffmpeg.ffprobe(filePath, (err, metadata) => {
clearTimeout(timer);
if (err) reject(err);
else resolve(metadata);
});
@@ -308,6 +326,12 @@ const parseTimemarkToSeconds = (timemark) => {
return (hours * 3600) + (minutes * 60) + seconds;
};
const sanitizeNumber = (value) => {
if (value === null || value === undefined) return null;
const num = Number(value);
return Number.isFinite(num) ? num : null;
};
const stopActiveTranscode = (progressKey) => {
const activeProcess = transcodeProcesses.get(progressKey);
if (!activeProcess?.command) {
@@ -563,10 +587,8 @@ app.get('/api/videos', async (req, res) => {
return videoExtensions.some(ext => lowerKey.endsWith(ext));
})
.map(key => {
const safeBucket = bucket.replace(/[^a-z0-9]/gi, '_');
const safeKeySegments = key.split('/').map(segment => segment.replace(/[^a-z0-9]/gi, '_'));
const hlsDir = path.join(CACHE_DIR, `hls-${safeBucket}-${safeKeySegments.join('-')}`);
const tmpInputPath = path.join(CACHE_DIR, `s3-input-${safeBucket}-${safeKeySegments.join('-')}.tmp`);
const hlsDir = getHlsCacheDir(bucket, key);
const tmpInputPath = getInputCachePath(bucket, key);
return {
key: key,
hasTranscodeCache: fs.existsSync(hlsDir),
@@ -627,9 +649,7 @@ app.post('/api/clear-video-transcode-cache', async (req, res) => {
if (!bucket || !key) {
return res.status(400).json({ error: 'Bucket and key are required' });
}
const safeBucket = bucket.replace(/[^a-z0-9]/gi, '_');
const safeKeySegments = key.split('/').map(segment => segment.replace(/[^a-z0-9]/gi, '_'));
const hlsDir = path.join(CACHE_DIR, `hls-${safeBucket}-${safeKeySegments.join('-')}`);
const hlsDir = getHlsCacheDir(bucket, key);
if (fs.existsSync(hlsDir)) {
fs.rmSync(hlsDir, { recursive: true, force: true });
@@ -647,9 +667,7 @@ app.post('/api/clear-video-download-cache', async (req, res) => {
if (!bucket || !key) {
return res.status(400).json({ error: 'Bucket and key are required' });
}
const safeBucket = bucket.replace(/[^a-z0-9]/gi, '_');
const safeKeySegments = key.split('/').map(segment => segment.replace(/[^a-z0-9]/gi, '_'));
const tmpInputPath = path.join(CACHE_DIR, `s3-input-${safeBucket}-${safeKeySegments.join('-')}.tmp`);
const tmpInputPath = getInputCachePath(bucket, key);
if (fs.existsSync(tmpInputPath)) {
fs.rmSync(tmpInputPath, { force: true });
@@ -714,9 +732,7 @@ app.get('/api/hls/playlist.m3u8', async (req, res) => {
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 tmpInputPath = getInputCachePath(bucket, key);
const auth = await extractS3Credentials(req);
const s3Client = createS3Client(auth);
@@ -732,13 +748,21 @@ app.get('/api/hls/playlist.m3u8', async (req, res) => {
let duration = 0;
try {
console.log(`[HLS] Probing file: ${key} (${tmpInputPath})`);
const metadata = await probeFile(tmpInputPath);
duration = parseFloat(metadata.format?.duration || 0);
} catch (err) { }
console.log(`[HLS] Probe complete: duration=${duration}s`);
} catch (err) {
console.error(`[HLS] Probe failed for ${key}:`, err.message);
}
if (duration <= 0) duration = 3600;
if (duration <= 0) {
duration = 3600;
console.warn(`[HLS] Duration invalid, using fallback: ${duration}s`);
}
const totalSegments = Math.ceil(duration / HLS_SEGMENT_TIME);
console.log(`[HLS] Generating m3u8: ${totalSegments} segments, duration=${duration}s, key=${key}`);
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++) {
@@ -750,6 +774,7 @@ app.get('/api/hls/playlist.m3u8', async (req, res) => {
}
m3u8 += `#EXT-X-ENDLIST\n`;
console.log(`[HLS] Sending m3u8 playlist to client (${m3u8.length} bytes)`);
res.setHeader('Content-Type', 'application/vnd.apple.mpegurl');
res.setHeader('Cache-Control', 'no-cache');
res.send(m3u8);
@@ -783,12 +808,12 @@ app.get('/api/hls/segment.ts', async (req, res) => {
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`);
console.log(`[HLS] Segment request: seg=${seg}, key=${key}, encoder=${requestedEncoder}`);
const progressKey = safeKeySegments.join('/');
const hlsDir = path.join(CACHE_DIR, `hls-${safeBucket}-${progressKey}`);
const tmpInputPath = getInputCachePath(bucket, key);
const progressKey = getProgressKey(key);
const hlsDir = getHlsCacheDir(bucket, key);
if (!fs.existsSync(hlsDir)) fs.mkdirSync(hlsDir, { recursive: true });
const targetSegPath = path.join(hlsDir, `segment_${seg}.ts`);
@@ -817,17 +842,24 @@ app.get('/api/hls/segment.ts', async (req, res) => {
const needsNewProcess = !currentProcess || (!fs.existsSync(targetSegPath) && (seg < (currentProcess.currentSeg || 0) || seg > (currentProcess.currentSeg || 0) + 4));
if (needsNewProcess) {
console.log(`[HLS] Starting new FFmpeg process for seg=${seg}, key=${key}`);
if (currentProcess && currentProcess.command) {
console.log(`[HLS] Killing previous FFmpeg process for ${progressKey}`);
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) {
console.error(`[HLS] Probe failed for segment transcode: ${e.message}`);
}
const encoderName = availableEncoderValues.has(requestedEncoder) ? requestedEncoder : 'h264_rkmpp';
const decoderName = availableDecoderValues.has(requestedDecoder) ? requestedDecoder : 'auto';
console.log(`[HLS] FFmpeg config: encoder=${encoderName}, decoder=${decoderName}, startTime=${startTime}s`);
const m3u8Path = path.join(hlsDir, `temp.m3u8`);
if (fs.existsSync(m3u8Path)) fs.unlinkSync(m3u8Path);
@@ -858,7 +890,7 @@ app.get('/api/hls/segment.ts', async (req, res) => {
ffmpegCommand.outputOptions(hlsOptions).output(m3u8Path);
ffmpegCommand.on('error', (err) => {
console.error('HLS FFmpeg Error:', err.message);
console.error(`[HLS] FFmpeg Error for ${progressKey}:`, err.message);
});
ffmpegCommand.on('progress', (progress) => {
@@ -874,9 +906,9 @@ app.get('/api/hls/segment.ts', async (req, res) => {
const progressState = {
status: 'transcoding',
percent,
frame: progress.frames || null,
fps: progress.currentFps || null,
bitrate: progress.currentKbps || null,
frame: sanitizeNumber(progress.frames),
fps: sanitizeNumber(progress.currentFps),
bitrate: sanitizeNumber(progress.currentKbps),
timemark: progress.timemark || null,
absoluteSeconds,
duration: totalDuration || null,
@@ -887,7 +919,7 @@ 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}%`);
console.log(`[FFmpeg] ${progressKey} | ${progress.timemark} | ${sanitizeNumber(progress.currentFps) ?? '-'}fps | ${sanitizeNumber(progress.currentKbps) ?? '-'}kbps | ${percent}%`);
});
ffmpegCommand.on('end', () => {
@@ -895,12 +927,16 @@ app.get('/api/hls/segment.ts', async (req, res) => {
});
ffmpegCommand.run();
console.log(`[HLS] FFmpeg process started for ${progressKey}`);
currentProcess = { command: ffmpegCommand, currentSeg: seg, lastActive: Date.now() };
hlsProcesses.set(progressKey, currentProcess);
} else {
console.log(`[HLS] Reusing existing FFmpeg process for seg=${seg}, currentSeg=${currentProcess?.currentSeg}`);
}
const ready = await waitForSegment(hlsDir, seg);
if (!ready) {
console.error(`[HLS] Segment generation timeout: seg=${seg}, key=${key}`);
return res.status(500).send('Segment generation timeout');
}
if (currentProcess) {
@@ -908,6 +944,7 @@ app.get('/api/hls/segment.ts', async (req, res) => {
currentProcess.lastActive = Date.now();
}
console.log(`[HLS] Serving segment: seg=${seg}`);
res.setHeader('Content-Type', 'video/MP2T');
res.sendFile(targetSegPath);
});
@@ -934,8 +971,7 @@ app.get('/api/stream', async (req, res) => {
const safeKeySegments = key.split('/').map(segment => segment.replace(/[^a-zA-Z0-9_\-]/g, '_'));
const progressKey = safeKeySegments.join('/');
const safeBucket = bucket.replace(/[^a-zA-Z0-9_\-]/g, '_');
const tmpInputPath = path.join(CACHE_DIR, `s3-input-${safeBucket}-${safeKeySegments.join('-')}.tmp`);
const tmpInputPath = getInputCachePath(bucket, key);
const cacheExists = fs.existsSync(tmpInputPath);
const auth = await extractS3Credentials(req);
@@ -1027,9 +1063,9 @@ app.get('/api/stream', async (req, res) => {
const progressState = {
status: 'transcoding',
percent,
frame: progress.frames || null,
fps: progress.currentFps || null,
bitrate: progress.currentKbps || null,
frame: sanitizeNumber(progress.frames),
fps: sanitizeNumber(progress.currentFps),
bitrate: sanitizeNumber(progress.currentKbps),
timemark: progress.timemark || null,
absoluteSeconds,
duration: progressMap[progressKey]?.duration || null,