添加预制切片
This commit is contained in:
@@ -148,6 +148,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<button id="transcode-btn" class="play-btn hidden">开始播放</button>
|
<button id="transcode-btn" class="play-btn hidden">开始播放</button>
|
||||||
<button id="stop-transcode-btn" class="play-btn stop-btn hidden">停止播放</button>
|
<button id="stop-transcode-btn" class="play-btn stop-btn hidden">停止播放</button>
|
||||||
|
<button id="pre-slice-btn" class="play-btn stop-btn hidden">预切片 (HLS)</button>
|
||||||
<button id="clear-playing-download-cache-btn" class="play-btn stop-btn hidden">清空下载缓存</button>
|
<button id="clear-playing-download-cache-btn" class="play-btn stop-btn hidden">清空下载缓存</button>
|
||||||
<button id="clear-playing-transcode-cache-btn" class="play-btn stop-btn hidden">清空转码缓存</button>
|
<button id="clear-playing-transcode-cache-btn" class="play-btn stop-btn hidden">清空转码缓存</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const currentVideoTitle = document.getElementById('current-video-title');
|
const currentVideoTitle = document.getElementById('current-video-title');
|
||||||
const transcodeBtn = document.getElementById('transcode-btn');
|
const transcodeBtn = document.getElementById('transcode-btn');
|
||||||
const stopTranscodeBtn = document.getElementById('stop-transcode-btn');
|
const stopTranscodeBtn = document.getElementById('stop-transcode-btn');
|
||||||
|
const preSliceBtn = document.getElementById('pre-slice-btn');
|
||||||
const clearPlayingDownloadBtn = document.getElementById('clear-playing-download-cache-btn');
|
const clearPlayingDownloadBtn = document.getElementById('clear-playing-download-cache-btn');
|
||||||
const clearPlayingTranscodeBtn = document.getElementById('clear-playing-transcode-cache-btn');
|
const clearPlayingTranscodeBtn = document.getElementById('clear-playing-transcode-cache-btn');
|
||||||
const themeSelector = document.getElementById('theme-selector');
|
const themeSelector = document.getElementById('theme-selector');
|
||||||
@@ -885,6 +886,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
stopTranscodeBtn.textContent = '停止播放';
|
stopTranscodeBtn.textContent = '停止播放';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (preSliceBtn) {
|
||||||
|
preSliceBtn.classList.remove('hidden');
|
||||||
|
preSliceBtn.disabled = false;
|
||||||
|
}
|
||||||
if (clearPlayingDownloadBtn) {
|
if (clearPlayingDownloadBtn) {
|
||||||
if (hasDownloadCache) clearPlayingDownloadBtn.classList.remove('hidden');
|
if (hasDownloadCache) clearPlayingDownloadBtn.classList.remove('hidden');
|
||||||
else clearPlayingDownloadBtn.classList.add('hidden');
|
else clearPlayingDownloadBtn.classList.add('hidden');
|
||||||
@@ -1098,6 +1103,44 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const preSliceVideo = async () => {
|
||||||
|
if (!selectedKey) return;
|
||||||
|
preSliceBtn.disabled = true;
|
||||||
|
preSliceBtn.textContent = '预切片中...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sessionId = localStorage.getItem('sessionId');
|
||||||
|
const body = {
|
||||||
|
bucket: selectedBucket,
|
||||||
|
key: selectedKey,
|
||||||
|
encoder: encoderSelect?.value || 'h264_rkmpp',
|
||||||
|
decoder: 'auto',
|
||||||
|
subtitleIndex: subtitleSelector?.value || '-1',
|
||||||
|
sessionId
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await fetch('/api/pre-slice', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) throw new Error(data.error || 'Pre-slice failed');
|
||||||
|
|
||||||
|
alert('已在后台开始预切片,您可以在进度条中查看进度。');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Pre-slice failed:', err);
|
||||||
|
alert(`预切片失败: ${err.message}`);
|
||||||
|
preSliceBtn.disabled = false;
|
||||||
|
preSliceBtn.textContent = '预切片 (HLS)';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (preSliceBtn) {
|
||||||
|
preSliceBtn.addEventListener('click', preSliceVideo);
|
||||||
|
}
|
||||||
|
|
||||||
const stopPolling = () => {
|
const stopPolling = () => {
|
||||||
if (currentPollInterval) {
|
if (currentPollInterval) {
|
||||||
clearInterval(currentPollInterval);
|
clearInterval(currentPollInterval);
|
||||||
|
|||||||
305
server.js
305
server.js
@@ -864,7 +864,8 @@ const hlsProcesses = new Map();
|
|||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
for (const [key, processInfo] of hlsProcesses.entries()) {
|
for (const [key, processInfo] of hlsProcesses.entries()) {
|
||||||
if (processInfo.lastActive && now - processInfo.lastActive > 30000) {
|
// Only kill if NOT persistent and inactive for more than 30s
|
||||||
|
if (!processInfo.persistent && processInfo.lastActive && now - processInfo.lastActive > 30000) {
|
||||||
try {
|
try {
|
||||||
if (processInfo.command) {
|
if (processInfo.command) {
|
||||||
processInfo.command.kill('SIGKILL');
|
processInfo.command.kill('SIGKILL');
|
||||||
@@ -878,6 +879,178 @@ setInterval(() => {
|
|||||||
}
|
}
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
|
const startHlsTranscode = async (bucket, key, seg, requestedEncoder, requestedDecoder, subtitleIndex, isPersistent = false) => {
|
||||||
|
const baseProgressKey = getProgressKey(key);
|
||||||
|
const subtitleSuffix = (subtitleIndex !== null && subtitleIndex !== undefined && subtitleIndex !== '-1') ? `-sub${subtitleIndex}` : '';
|
||||||
|
const progressKey = `${baseProgressKey}${subtitleSuffix}`;
|
||||||
|
const hlsDir = getHlsCacheDir(bucket, key, subtitleIndex);
|
||||||
|
const tmpInputPath = getInputCachePath(bucket, key);
|
||||||
|
|
||||||
|
if (!fs.existsSync(hlsDir)) fs.mkdirSync(hlsDir, { recursive: true });
|
||||||
|
|
||||||
|
let currentProcess = hlsProcesses.get(progressKey);
|
||||||
|
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) {
|
||||||
|
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, subtitleIndex=${subtitleIndex}, persistent=${isPersistent}`);
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
const videoFilters = [];
|
||||||
|
if (isVaapiCodec(encoderName)) {
|
||||||
|
ffmpegCommand.inputOptions(['-vaapi_device', '/dev/dri/renderD128']);
|
||||||
|
videoFilters.push('format=nv12', 'hwupload');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add subtitle filter if requested
|
||||||
|
if (subtitleIndex !== undefined && subtitleIndex !== null && subtitleIndex !== '-1') {
|
||||||
|
const subIdx = parseInt(subtitleIndex);
|
||||||
|
const subtitleStream = (sourceMetadata?.streams || [])
|
||||||
|
.filter(s => s.codec_type === 'subtitle')[subIdx];
|
||||||
|
|
||||||
|
if (subtitleStream) {
|
||||||
|
console.log(`[HLS] Applying subtitle filter for stream ${subtitleStream.index} (codec: ${subtitleStream.codec_name})`);
|
||||||
|
const escapedPath = tmpInputPath.replace(/\\/g, '/').replace(/:/g, '\\:');
|
||||||
|
const isImageSub = ['pgs', 'dvdsub', 'hdmv_pgs_subtitle', 'dvd_subtitle'].includes(subtitleStream.codec_name);
|
||||||
|
|
||||||
|
if (isImageSub) {
|
||||||
|
ffmpegCommand.complexFilter([
|
||||||
|
{
|
||||||
|
filter: 'overlay',
|
||||||
|
options: { x: 0, y: 0 },
|
||||||
|
inputs: ['0:v', `0:s:${subIdx}`],
|
||||||
|
outputs: 'outv'
|
||||||
|
}
|
||||||
|
], 'outv');
|
||||||
|
} else {
|
||||||
|
videoFilters.push(`subtitles='${escapedPath}':si=${subIdx}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoFilters.length > 0) {
|
||||||
|
ffmpegCommand.videoFilters(videoFilters);
|
||||||
|
}
|
||||||
|
|
||||||
|
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_segment_filename', segmentFilename,
|
||||||
|
'-start_number', seg.toString(),
|
||||||
|
'-copyts',
|
||||||
|
'-avoid_negative_ts', 'disabled',
|
||||||
|
'-muxdelay', '0',
|
||||||
|
'-muxpreload', '0'
|
||||||
|
]);
|
||||||
|
|
||||||
|
ffmpegCommand.outputOptions(hlsOptions).output(m3u8Path);
|
||||||
|
ffmpegCommand.on('error', (err) => {
|
||||||
|
console.error(`[HLS] FFmpeg Error for ${progressKey}:`, 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,
|
||||||
|
frame: sanitizeNumber(progress.frames),
|
||||||
|
fps: sanitizeNumber(progress.currentFps),
|
||||||
|
bitrate: sanitizeNumber(progress.currentKbps),
|
||||||
|
timemark: progress.timemark || null,
|
||||||
|
absoluteSeconds,
|
||||||
|
duration: totalDuration || null,
|
||||||
|
startSeconds: startTime,
|
||||||
|
details: isPersistent ? `后台预切片中 ${percent}%` : `处理进度 ${percent}%`,
|
||||||
|
mp4Url: null
|
||||||
|
};
|
||||||
|
const broadcastKey = subtitleIndex && subtitleIndex !== '-1' ? `${key}-sub${subtitleIndex}` : key;
|
||||||
|
broadcastWs(progressKey, { type: 'progress', key: broadcastKey, progress: progressState });
|
||||||
|
|
||||||
|
if (!isPersistent) {
|
||||||
|
console.log(`[FFmpeg] ${progressKey} | ${progress.timemark} | ${sanitizeNumber(progress.currentFps) ?? '-'}fps | ${sanitizeNumber(progress.currentKbps) ?? '-'}kbps | ${percent}%`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ffmpegCommand.on('end', () => {
|
||||||
|
console.log(`[FFmpeg] ${progressKey} HLS transcode completed.`);
|
||||||
|
const broadcastKey = subtitleIndex && subtitleIndex !== '-1' ? `${key}-sub${subtitleIndex}` : key;
|
||||||
|
broadcastWs(progressKey, {
|
||||||
|
type: 'progress',
|
||||||
|
key: broadcastKey,
|
||||||
|
progress: {
|
||||||
|
status: 'finished',
|
||||||
|
percent: 100,
|
||||||
|
details: isPersistent ? '预切片完成' : '处理完成'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
hlsProcesses.delete(progressKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
ffmpegCommand.run();
|
||||||
|
console.log(`[HLS] FFmpeg process started for ${progressKey} (persistent=${isPersistent})`);
|
||||||
|
const newProcessInfo = { command: ffmpegCommand, currentSeg: seg, lastActive: Date.now(), persistent: isPersistent };
|
||||||
|
hlsProcesses.set(progressKey, newProcessInfo);
|
||||||
|
return newProcessInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
app.post('/api/pre-slice', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { bucket, key, encoder, decoder, subtitleIndex } = req.body;
|
||||||
|
if (!bucket || !key) return res.status(400).json({ error: 'Bucket and key are required' });
|
||||||
|
|
||||||
|
const tmpInputPath = getInputCachePath(bucket, key);
|
||||||
|
const auth = await extractS3Credentials(req);
|
||||||
|
const s3Client = createS3Client(auth);
|
||||||
|
const progressKey = getProgressKey(key);
|
||||||
|
|
||||||
|
console.log(`[Pre-slice] Starting pre-slice for ${key}`);
|
||||||
|
|
||||||
|
// Ensure downloaded
|
||||||
|
await ensureS3Downloaded(s3Client, bucket, key, tmpInputPath, progressKey, createStreamSessionId());
|
||||||
|
|
||||||
|
// Trigger HLS transcode as persistent (won't be killed by watchdog)
|
||||||
|
await startHlsTranscode(bucket, key, 0, encoder || 'h264_rkmpp', decoder || 'auto', subtitleIndex || '-1', true);
|
||||||
|
|
||||||
|
res.json({ message: 'Pre-slicing started in background' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error starting pre-slice:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to start pre-slice', detail: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.get('/api/hls/segment.ts', async (req, res) => {
|
app.get('/api/hls/segment.ts', async (req, res) => {
|
||||||
const bucket = req.query.bucket;
|
const bucket = req.query.bucket;
|
||||||
const key = req.query.key;
|
const key = req.query.key;
|
||||||
@@ -937,138 +1110,12 @@ app.get('/api/hls/segment.ts', async (req, res) => {
|
|||||||
|
|
||||||
if (needsNewProcess) {
|
if (needsNewProcess) {
|
||||||
console.log(`[HLS] Starting new FFmpeg process for seg=${seg}, key=${key}`);
|
console.log(`[HLS] Starting new FFmpeg process for seg=${seg}, key=${key}`);
|
||||||
if (currentProcess && currentProcess.command) {
|
currentProcess = await startHlsTranscode(bucket, key, seg, requestedEncoder, requestedDecoder, subtitleIndex);
|
||||||
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) {
|
|
||||||
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';
|
|
||||||
const subtitleIndex = req.query.subtitleIndex; // The subIndex (index among subtitle streams)
|
|
||||||
console.log(`[HLS] FFmpeg config: encoder=${encoderName}, decoder=${decoderName}, startTime=${startTime}s, subtitleIndex=${subtitleIndex}`);
|
|
||||||
|
|
||||||
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');
|
|
||||||
|
|
||||||
const videoFilters = [];
|
|
||||||
if (isVaapiCodec(encoderName)) {
|
|
||||||
ffmpegCommand.inputOptions(['-vaapi_device', '/dev/dri/renderD128']);
|
|
||||||
videoFilters.push('format=nv12', 'hwupload');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add subtitle filter if requested
|
|
||||||
if (subtitleIndex !== undefined && subtitleIndex !== null && subtitleIndex !== '-1') {
|
|
||||||
const subIdx = parseInt(subtitleIndex);
|
|
||||||
const subtitleStream = (sourceMetadata?.streams || [])
|
|
||||||
.filter(s => s.codec_type === 'subtitle')[subIdx];
|
|
||||||
|
|
||||||
if (subtitleStream) {
|
|
||||||
console.log(`[HLS] Applying subtitle filter for stream ${subtitleStream.index} (codec: ${subtitleStream.codec_name})`);
|
|
||||||
// For embedded subtitles, use the subIndex (index among subtitle streams)
|
|
||||||
// Escape path for Windows
|
|
||||||
const escapedPath = tmpInputPath.replace(/\\/g, '/').replace(/:/g, '\\:');
|
|
||||||
// Use 'ffmpeg' subtitles filter for text-based, or overlay for image-based
|
|
||||||
const isImageSub = ['pgs', 'dvdsub', 'hdmv_pgs_subtitle', 'dvd_subtitle'].includes(subtitleStream.codec_name);
|
|
||||||
|
|
||||||
if (isImageSub) {
|
|
||||||
// Image-based subs need complex filter for overlay
|
|
||||||
// This is more complex with fluent-ffmpeg's high-level API
|
|
||||||
// We might need to use complexFilter
|
|
||||||
ffmpegCommand.complexFilter([
|
|
||||||
{
|
|
||||||
filter: 'overlay',
|
|
||||||
options: { x: 0, y: 0 },
|
|
||||||
inputs: ['0:v', `0:s:${subIdx}`],
|
|
||||||
outputs: 'outv'
|
|
||||||
}
|
|
||||||
], 'outv');
|
|
||||||
} else {
|
|
||||||
// Text-based subs
|
|
||||||
videoFilters.push(`subtitles='${escapedPath}':si=${subIdx}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (videoFilters.length > 0) {
|
|
||||||
ffmpegCommand.videoFilters(videoFilters);
|
|
||||||
}
|
|
||||||
|
|
||||||
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_segment_filename', segmentFilename,
|
|
||||||
'-start_number', seg.toString(),
|
|
||||||
'-copyts',
|
|
||||||
'-avoid_negative_ts', 'disabled',
|
|
||||||
'-muxdelay', '0',
|
|
||||||
'-muxpreload', '0'
|
|
||||||
]);
|
|
||||||
|
|
||||||
ffmpegCommand.outputOptions(hlsOptions).output(m3u8Path);
|
|
||||||
ffmpegCommand.on('error', (err) => {
|
|
||||||
console.error(`[HLS] FFmpeg Error for ${progressKey}:`, 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,
|
|
||||||
frame: sanitizeNumber(progress.frames),
|
|
||||||
fps: sanitizeNumber(progress.currentFps),
|
|
||||||
bitrate: sanitizeNumber(progress.currentKbps),
|
|
||||||
timemark: progress.timemark || null,
|
|
||||||
absoluteSeconds,
|
|
||||||
duration: totalDuration || null,
|
|
||||||
startSeconds: startTime,
|
|
||||||
details: `处理进度 ${percent}%`,
|
|
||||||
mp4Url: null
|
|
||||||
};
|
|
||||||
const broadcastKey = subtitleIndex && subtitleIndex !== '-1' ? `${key}-sub${subtitleIndex}` : key;
|
|
||||||
broadcastWs(progressKey, { type: 'progress', key: broadcastKey, progress: progressState });
|
|
||||||
|
|
||||||
console.log(`[FFmpeg] ${progressKey} | ${progress.timemark} | ${sanitizeNumber(progress.currentFps) ?? '-'}fps | ${sanitizeNumber(progress.currentKbps) ?? '-'}kbps | ${percent}%`);
|
|
||||||
});
|
|
||||||
|
|
||||||
ffmpegCommand.on('end', () => {
|
|
||||||
console.log(`[FFmpeg] ${progressKey} HLS transcode completed.`);
|
|
||||||
});
|
|
||||||
|
|
||||||
ffmpegCommand.run();
|
|
||||||
console.log(`[HLS] FFmpeg process started for ${progressKey}`);
|
|
||||||
currentProcess = { command: ffmpegCommand, currentSeg: seg, lastActive: Date.now() };
|
|
||||||
hlsProcesses.set(progressKey, currentProcess);
|
|
||||||
} else {
|
} else {
|
||||||
console.log(`[HLS] Reusing existing FFmpeg process for seg=${seg}, currentSeg=${currentProcess?.currentSeg}`);
|
console.log(`[HLS] Reusing existing FFmpeg process for seg=${seg}, currentSeg=${currentProcess?.currentSeg}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const ready = await waitForSegment(hlsDir, seg);
|
const ready = await waitForSegment(hlsDir, seg);
|
||||||
if (!ready) {
|
if (!ready) {
|
||||||
console.error(`[HLS] Segment generation timeout: seg=${seg}, key=${key}`);
|
console.error(`[HLS] Segment generation timeout: seg=${seg}, key=${key}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user