This commit is contained in:
CN-JS-HuiBai
2026-04-02 22:17:24 +08:00
parent 504d02c0fa
commit dac755127a
2 changed files with 73 additions and 16 deletions

View File

@@ -63,6 +63,7 @@ document.addEventListener('DOMContentLoaded', () => {
let seekOffset = 0; let seekOffset = 0;
let isDraggingSeek = false; let isDraggingSeek = false;
let isStreamActive = false; let isStreamActive = false;
let pendingSeekTimeout = null;
const formatBytes = (bytes) => { const formatBytes = (bytes) => {
if (!bytes || bytes === 0) return '0 B'; if (!bytes || bytes === 0) return '0 B';
@@ -101,6 +102,8 @@ document.addEventListener('DOMContentLoaded', () => {
if (message.type === 'duration' && message.duration) { if (message.type === 'duration' && message.duration) {
videoDuration = message.duration; videoDuration = message.duration;
if (seekTotalTime) seekTotalTime.textContent = formatTime(videoDuration); if (seekTotalTime) seekTotalTime.textContent = formatTime(videoDuration);
showSeekBar();
updateSeekBarPosition(seekOffset + (videoPlayer.currentTime || 0));
} }
if (message.type === 'progress') { if (message.type === 'progress') {
handleProgress(message.progress); handleProgress(message.progress);
@@ -283,9 +286,15 @@ document.addEventListener('DOMContentLoaded', () => {
if (!selectedKey || !selectedBucket || videoDuration <= 0) return; if (!selectedKey || !selectedBucket || videoDuration <= 0) return;
targetSeconds = Math.max(0, Math.min(targetSeconds, videoDuration - 0.5)); targetSeconds = Math.max(0, Math.min(targetSeconds, videoDuration - 0.5));
seekOffset = targetSeconds; seekOffset = targetSeconds;
isStreamActive = false;
updateSeekBarPosition(targetSeconds);
// Show seeking indicator // Show seeking indicator
if (seekingOverlay) seekingOverlay.classList.remove('hidden'); if (seekingOverlay) seekingOverlay.classList.remove('hidden');
if (pendingSeekTimeout) {
clearTimeout(pendingSeekTimeout);
pendingSeekTimeout = null;
}
// Build new stream URL with ss parameter // Build new stream URL with ss parameter
const codec = codecSelect?.value || 'h264'; const codec = codecSelect?.value || 'h264';
@@ -304,11 +313,12 @@ document.addEventListener('DOMContentLoaded', () => {
videoPlayer.play().catch(() => {}); videoPlayer.play().catch(() => {});
videoPlayer.removeEventListener('canplay', onCanPlay); videoPlayer.removeEventListener('canplay', onCanPlay);
}; };
videoPlayer.addEventListener('canplay', onCanPlay); videoPlayer.addEventListener('canplay', onCanPlay, { once: true });
// Timeout fallback: hide seeking overlay after 8s even if canplay doesn't fire // Timeout fallback: hide seeking overlay after 8s even if canplay doesn't fire
setTimeout(() => { pendingSeekTimeout = setTimeout(() => {
if (seekingOverlay) seekingOverlay.classList.add('hidden'); if (seekingOverlay) seekingOverlay.classList.add('hidden');
pendingSeekTimeout = null;
}, 8000); }, 8000);
}; };
@@ -591,9 +601,13 @@ document.addEventListener('DOMContentLoaded', () => {
playerOverlay.classList.add('hidden'); playerOverlay.classList.add('hidden');
videoPlayer.classList.add('hidden'); videoPlayer.classList.add('hidden');
videoPlayer.pause(); videoPlayer.pause();
videoPlayer.removeAttribute('src');
videoPlayer.load();
isStreamActive = false; isStreamActive = false;
videoDuration = 0; videoDuration = 0;
seekOffset = 0; seekOffset = 0;
if (seekCurrentTime) seekCurrentTime.textContent = formatTime(0);
if (seekTotalTime) seekTotalTime.textContent = formatTime(0);
hideSeekBar(); hideSeekBar();
if (transcodeBtn) { if (transcodeBtn) {
@@ -652,6 +666,7 @@ document.addEventListener('DOMContentLoaded', () => {
videoPlayer.classList.remove('hidden'); videoPlayer.classList.remove('hidden');
isStreamActive = true; isStreamActive = true;
showSeekBar(); showSeekBar();
updateSeekBarPosition(seekOffset);
if (playBtn) { if (playBtn) {
playBtn.disabled = false; playBtn.disabled = false;
playBtn.textContent = 'Play'; playBtn.textContent = 'Play';
@@ -680,6 +695,9 @@ document.addEventListener('DOMContentLoaded', () => {
handleProgress({ status: 'cancelled', percent: 0, details: 'Transcode stopped' }); handleProgress({ status: 'cancelled', percent: 0, details: 'Transcode stopped' });
isStreamActive = false; isStreamActive = false;
videoPlayer.pause();
videoPlayer.removeAttribute('src');
videoPlayer.load();
hideSeekBar(); hideSeekBar();
if (transcodeBtn) { if (transcodeBtn) {
transcodeBtn.disabled = false; transcodeBtn.disabled = false;
@@ -719,6 +737,7 @@ document.addEventListener('DOMContentLoaded', () => {
isStreamActive = true; isStreamActive = true;
showSeekBar(); showSeekBar();
videoPlayer.addEventListener('loadedmetadata', () => { videoPlayer.addEventListener('loadedmetadata', () => {
updateSeekBarPosition(seekOffset);
if (playBtn) { if (playBtn) {
playBtn.disabled = false; playBtn.disabled = false;
playBtn.textContent = 'Play'; playBtn.textContent = 'Play';

View File

@@ -120,6 +120,29 @@ const probeFile = (filePath) => {
}); });
}; };
const stopActiveTranscode = (progressKey) => {
const activeCommand = transcodeProcesses.get(progressKey);
if (!activeCommand) {
return;
}
try {
if (typeof activeCommand.removeAllListeners === 'function') {
activeCommand.removeAllListeners('progress');
activeCommand.removeAllListeners('stderr');
activeCommand.removeAllListeners('end');
activeCommand.removeAllListeners('error');
}
if (typeof activeCommand.kill === 'function') {
activeCommand.kill('SIGKILL');
}
} catch (killError) {
console.warn(`Failed to kill transcode process for ${progressKey}:`, killError);
} finally {
transcodeProcesses.delete(progressKey);
}
};
const extractS3Credentials = (req) => { const extractS3Credentials = (req) => {
const query = req.query || {}; const query = req.query || {};
const username = req.headers['x-s3-username'] || req.body?.username || query.username || query.accessKeyId || ''; const username = req.headers['x-s3-username'] || req.body?.username || query.username || query.accessKeyId || '';
@@ -145,6 +168,9 @@ wss.on('connection', (ws) => {
const currentProgress = progressMap[progressKey]; const currentProgress = progressMap[progressKey];
if (currentProgress) { if (currentProgress) {
if (typeof currentProgress.duration === 'number' && currentProgress.duration > 0) {
ws.send(JSON.stringify({ type: 'duration', key: message.key, duration: currentProgress.duration }));
}
ws.send(JSON.stringify({ type: 'progress', key: message.key, progress: currentProgress })); ws.send(JSON.stringify({ type: 'progress', key: message.key, progress: currentProgress }));
if (currentProgress.status === 'finished' && currentProgress.mp4Url) { if (currentProgress.status === 'finished' && currentProgress.mp4Url) {
ws.send(JSON.stringify({ type: 'ready', key: message.key, mp4Url: currentProgress.mp4Url })); ws.send(JSON.stringify({ type: 'ready', key: message.key, mp4Url: currentProgress.mp4Url }));
@@ -260,20 +286,10 @@ app.post('/api/stop-transcode', (req, res) => {
} }
const progressKey = getProgressKey(key); const progressKey = getProgressKey(key);
const command = transcodeProcesses.get(progressKey); if (!transcodeProcesses.has(progressKey)) {
if (!command) {
return res.status(404).json({ error: 'No active transcode found for this key' }); return res.status(404).json({ error: 'No active transcode found for this key' });
} }
stopActiveTranscode(progressKey);
try {
if (typeof command.kill === 'function') {
command.kill('SIGKILL');
}
} catch (killError) {
console.warn('Failed to kill transcode process:', killError);
}
transcodeProcesses.delete(progressKey);
progressMap[progressKey] = { progressMap[progressKey] = {
status: 'cancelled', status: 'cancelled',
percent: 0, percent: 0,
@@ -323,6 +339,8 @@ app.get('/api/stream', async (req, res) => {
const s3Client = createS3Client(auth); const s3Client = createS3Client(auth);
try { try {
stopActiveTranscode(progressKey);
let totalBytes = 0; let totalBytes = 0;
let downloadedBytes = 0; let downloadedBytes = 0;
@@ -392,6 +410,10 @@ app.get('/api/stream', async (req, res) => {
try { try {
const metadata = await probeFile(tmpInputPath); const metadata = await probeFile(tmpInputPath);
const duration = metadata.format?.duration || 0; const duration = metadata.format?.duration || 0;
progressMap[progressKey] = {
...(progressMap[progressKey] || {}),
duration: parseFloat(duration) || 0
};
broadcastWs(progressKey, { type: 'duration', key, duration: parseFloat(duration) }); broadcastWs(progressKey, { type: 'duration', key, duration: parseFloat(duration) });
} catch (probeErr) { } catch (probeErr) {
console.error('Probe failed:', probeErr); console.error('Probe failed:', probeErr);
@@ -425,14 +447,23 @@ app.get('/api/stream', async (req, res) => {
ffmpegCommand ffmpegCommand
.on('progress', (progress) => { .on('progress', (progress) => {
const effectiveDuration = Math.max(0, (progressMap[progressKey]?.duration || 0) - startSeconds);
const timemarkSeconds = ffmpeg.timemarkToSeconds(progress.timemark || '0');
const absoluteSeconds = startSeconds + (isFinite(timemarkSeconds) ? timemarkSeconds : 0);
const percent = effectiveDuration > 0
? Math.min(Math.max(Math.round((absoluteSeconds / (progressMap[progressKey]?.duration || effectiveDuration)) * 100), 0), 100)
: Math.min(Math.max(Math.round(progress.percent || 0), 0), 100);
const progressState = { const progressState = {
status: 'transcoding', status: 'transcoding',
percent: Math.min(Math.max(Math.round(progress.percent || 0), 0), 100), percent,
frame: progress.frames || null, frame: progress.frames || null,
fps: progress.currentFps || null, fps: progress.currentFps || null,
bitrate: progress.currentKbps || null, bitrate: progress.currentKbps || null,
timemark: progress.timemark || null, timemark: progress.timemark || null,
details: `Streaming transcode ${Math.min(Math.max(Math.round(progress.percent || 0), 0), 100)}%`, absoluteSeconds,
duration: progressMap[progressKey]?.duration || null,
startSeconds,
details: `Streaming transcode ${percent}%`,
mp4Url: null mp4Url: null
}; };
progressMap[progressKey] = progressState; progressMap[progressKey] = progressState;
@@ -446,6 +477,8 @@ app.get('/api/stream', async (req, res) => {
progressMap[progressKey] = { progressMap[progressKey] = {
status: 'finished', status: 'finished',
percent: 100, percent: 100,
duration: progressMap[progressKey]?.duration || null,
startSeconds,
details: 'Streaming transcode complete', details: 'Streaming transcode complete',
mp4Url: null mp4Url: null
}; };
@@ -456,6 +489,8 @@ app.get('/api/stream', async (req, res) => {
const failedState = { const failedState = {
status: 'failed', status: 'failed',
percent: progressMap[progressKey]?.percent || 0, percent: progressMap[progressKey]?.percent || 0,
duration: progressMap[progressKey]?.duration || null,
startSeconds,
details: err.message || 'Streaming transcode failed', details: err.message || 'Streaming transcode failed',
mp4Url: null mp4Url: null
}; };
@@ -477,6 +512,9 @@ app.get('/api/stream', async (req, res) => {
ffmpegCommand.kill('SIGKILL'); ffmpegCommand.kill('SIGKILL');
} catch (_) {} } catch (_) {}
} }
if (transcodeProcesses.get(progressKey) === ffmpegCommand) {
transcodeProcesses.delete(progressKey);
}
}); });
startStream(videoCodec); startStream(videoCodec);