回退
This commit is contained in:
@@ -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';
|
||||||
|
|||||||
66
server.js
66
server.js
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user