添加停止转码按钮

This commit is contained in:
CN-JS-HuiBai
2026-04-02 20:55:22 +08:00
parent c8151a7779
commit 4e4db42934
4 changed files with 114 additions and 1 deletions

View File

@@ -653,6 +653,14 @@ header p {
transform: translateY(-1px); transform: translateY(-1px);
} }
.stop-btn {
background: #dc2626;
}
.stop-btn:hover {
background: #b91c1c;
}
.play-btn:disabled { .play-btn:disabled {
opacity: 0.65; opacity: 0.65;
background: rgba(37, 99, 235, 0.5); background: rgba(37, 99, 235, 0.5);

View File

@@ -102,6 +102,7 @@
<h3>Now Playing</h3> <h3>Now Playing</h3>
<p id="current-video-title">video.mp4</p> <p id="current-video-title">video.mp4</p>
<button id="transcode-btn" class="play-btn hidden">Start Transcode</button> <button id="transcode-btn" class="play-btn hidden">Start Transcode</button>
<button id="stop-transcode-btn" class="play-btn stop-btn hidden">Stop Transcode</button>
<button id="play-btn" class="play-btn hidden">Play</button> <button id="play-btn" class="play-btn hidden">Play</button>
</div> </div>
</section> </section>

View File

@@ -19,7 +19,7 @@ document.addEventListener('DOMContentLoaded', () => {
const nowPlaying = document.getElementById('now-playing'); const nowPlaying = document.getElementById('now-playing');
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 playBtn = document.getElementById('play-btn'); const stopTranscodeBtn = document.getElementById('stop-transcode-btn'); const stopTranscodeBtn = document.getElementById('stop-transcode-btn'); const playBtn = document.getElementById('play-btn');
const progressInfo = document.getElementById('progress-info'); const progressInfo = document.getElementById('progress-info');
const progressText = document.getElementById('progress-text'); const progressText = document.getElementById('progress-text');
const progressFill = document.getElementById('progress-fill'); const progressFill = document.getElementById('progress-fill');
@@ -114,6 +114,11 @@ document.addEventListener('DOMContentLoaded', () => {
progressInfo.classList.add('hidden'); progressInfo.classList.add('hidden');
progressText.textContent = ''; progressText.textContent = '';
progressFill.style.width = '0%'; progressFill.style.width = '0%';
if (stopTranscodeBtn) {
stopTranscodeBtn.classList.add('hidden');
stopTranscodeBtn.disabled = false;
stopTranscodeBtn.textContent = 'Stop Transcode';
}
}; };
const setAuthHeaders = (username, password) => { const setAuthHeaders = (username, password) => {
@@ -403,6 +408,11 @@ document.addEventListener('DOMContentLoaded', () => {
transcodeBtn.textContent = 'Start Transcode'; transcodeBtn.textContent = 'Start Transcode';
transcodeBtn.classList.remove('hidden'); transcodeBtn.classList.remove('hidden');
} }
if (stopTranscodeBtn) {
stopTranscodeBtn.classList.add('hidden');
stopTranscodeBtn.disabled = false;
stopTranscodeBtn.textContent = 'Stop Transcode';
}
if (playBtn) { if (playBtn) {
playBtn.classList.add('hidden'); playBtn.classList.add('hidden');
} }
@@ -422,6 +432,11 @@ document.addEventListener('DOMContentLoaded', () => {
transcodeBtn.disabled = true; transcodeBtn.disabled = true;
transcodeBtn.textContent = 'Starting...'; transcodeBtn.textContent = 'Starting...';
} }
if (stopTranscodeBtn) {
stopTranscodeBtn.classList.remove('hidden');
stopTranscodeBtn.disabled = false;
stopTranscodeBtn.textContent = 'Stop Transcode';
}
stopPolling(); stopPolling();
transcodingOverlay.classList.remove('hidden'); transcodingOverlay.classList.remove('hidden');
setProgress({ status: 'Starting download...', percent: 0, details: 'Starting download...' }); setProgress({ status: 'Starting download...', percent: 0, details: 'Starting download...' });
@@ -448,6 +463,38 @@ document.addEventListener('DOMContentLoaded', () => {
} }
}; };
const stopTranscode = async () => {
if (!currentVideoKey || !stopTranscodeBtn) return;
stopTranscodeBtn.disabled = true;
stopTranscodeBtn.textContent = 'Stopping...';
try {
const res = await fetch('/api/stop-transcode', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: currentVideoKey })
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Failed to stop transcode');
setProgress({ status: 'cancelled', percent: 0, details: 'Transcode stopped' });
if (transcodeBtn) {
transcodeBtn.disabled = false;
transcodeBtn.textContent = 'Start Transcode';
transcodeBtn.classList.remove('hidden');
}
stopTranscodeBtn.classList.add('hidden');
} catch (err) {
console.error('Stop transcode failed:', err);
alert(`Stop transcode failed: ${err.message}`);
} finally {
if (stopTranscodeBtn) {
stopTranscodeBtn.disabled = false;
stopTranscodeBtn.textContent = 'Stop Transcode';
}
}
};
// Poll the backend to check if the generated MP4 file is accessible // Poll the backend to check if the generated MP4 file is accessible
const pollForMp4Ready = (key, mp4Url) => { const pollForMp4Ready = (key, mp4Url) => {
let attempts = 0; let attempts = 0;
@@ -490,6 +537,9 @@ document.addEventListener('DOMContentLoaded', () => {
transcodingOverlay.classList.add('hidden'); transcodingOverlay.classList.add('hidden');
videoPlayer.classList.remove('hidden'); videoPlayer.classList.remove('hidden');
playBtn.classList.add('hidden'); playBtn.classList.add('hidden');
if (stopTranscodeBtn) {
stopTranscodeBtn.classList.add('hidden');
}
resetProgress(); resetProgress();
videoPlayer.src = url; videoPlayer.src = url;
@@ -511,6 +561,9 @@ document.addEventListener('DOMContentLoaded', () => {
if (clearTranscodeCacheBtn) { if (clearTranscodeCacheBtn) {
clearTranscodeCacheBtn.addEventListener('click', clearTranscodeCache); clearTranscodeCacheBtn.addEventListener('click', clearTranscodeCache);
} }
if (stopTranscodeBtn) {
stopTranscodeBtn.addEventListener('click', stopTranscode);
}
if (loginBtn) { if (loginBtn) {
loginBtn.addEventListener('click', login); loginBtn.addEventListener('click', login);
} }

View File

@@ -59,6 +59,7 @@ const createS3Client = (credentials) => {
}; };
const progressMap = {}; const progressMap = {};
const transcodeProcesses = new Map();
const wsSubscriptions = new Map(); const wsSubscriptions = new Map();
const getProgressKey = (key) => key.split('/').map(segment => segment.replace(/[^a-zA-Z0-9_\-]/g, '_')).join('/'); const getProgressKey = (key) => key.split('/').map(segment => segment.replace(/[^a-zA-Z0-9_\-]/g, '_')).join('/');
@@ -187,6 +188,14 @@ const clearMp4Cache = () => {
}; };
const clearTranscodeCache = () => { const clearTranscodeCache = () => {
for (const command of transcodeProcesses.values()) {
try {
if (typeof command.kill === 'function') {
command.kill('SIGKILL');
}
} catch (_) {}
}
transcodeProcesses.clear();
clearMp4Cache(); clearMp4Cache();
Object.keys(progressMap).forEach((key) => delete progressMap[key]); Object.keys(progressMap).forEach((key) => delete progressMap[key]);
}; };
@@ -287,6 +296,42 @@ app.post('/api/clear-transcode-cache', (req, res) => {
} }
}); });
app.post('/api/stop-transcode', (req, res) => {
try {
const { key } = req.body;
if (!key) {
return res.status(400).json({ error: 'Video key is required' });
}
const progressKey = getProgressKey(key);
const command = transcodeProcesses.get(progressKey);
if (!command) {
return res.status(404).json({ error: 'No active transcode found for this key' });
}
try {
if (typeof command.kill === 'function') {
command.kill('SIGKILL');
}
} catch (killError) {
console.warn('Failed to kill transcode process:', killError);
}
transcodeProcesses.delete(progressKey);
progressMap[progressKey] = {
status: 'cancelled',
percent: 0,
details: 'Transcode stopped by user',
mp4Url: `/mp4/${progressKey}/video.mp4`
};
broadcastWs(progressKey, { type: 'progress', key, progress: progressMap[progressKey] });
res.json({ message: 'Transcode stopped' });
} catch (error) {
console.error('Error stopping transcode:', error);
res.status(500).json({ error: 'Failed to stop transcode', detail: error.message });
}
});
// Endpoint to transcode S3 video streaming to MP4 // Endpoint to transcode S3 video streaming to MP4
app.post('/api/transcode', async (req, res) => { app.post('/api/transcode', async (req, res) => {
const { bucket, key, codec, encoder } = req.body; const { bucket, key, codec, encoder } = req.body;
@@ -320,6 +365,7 @@ app.post('/api/transcode', async (req, res) => {
const mp4Url = `/mp4/${progressKey}/video.mp4`; const mp4Url = `/mp4/${progressKey}/video.mp4`;
progressMap[progressKey] = { status: 'pending', percent: 0, details: 'Waiting for ffmpeg to start', mp4Url }; progressMap[progressKey] = { status: 'pending', percent: 0, details: 'Waiting for ffmpeg to start', mp4Url };
let currentFfmpegCommand = null;
// If it already exists, just return the URL // If it already exists, just return the URL
if (fs.existsSync(mp4Path)) { if (fs.existsSync(mp4Path)) {
@@ -396,6 +442,8 @@ app.post('/api/transcode', async (req, res) => {
.videoCodec(encoderName) .videoCodec(encoderName)
.audioCodec('aac') .audioCodec('aac')
.outputOptions(createFfmpegOptions(encoderName)); .outputOptions(createFfmpegOptions(encoderName));
transcodeProcesses.set(progressKey, command);
currentFfmpegCommand = command;
if (/_vaapi$/.test(encoderName)) { if (/_vaapi$/.test(encoderName)) {
command command
.inputOptions(['-vaapi_device', '/dev/dri/renderD128']) .inputOptions(['-vaapi_device', '/dev/dri/renderD128'])
@@ -435,6 +483,7 @@ app.post('/api/transcode', async (req, res) => {
console.error(`Output verification failed for ${mp4Path}:`, verifyError); console.error(`Output verification failed for ${mp4Path}:`, verifyError);
progressState = { status: 'failed', percent: progressMap[progressKey]?.percent || 0, details: `Output verification failed: ${verifyError.message}`, mp4Url }; progressState = { status: 'failed', percent: progressMap[progressKey]?.percent || 0, details: `Output verification failed: ${verifyError.message}`, mp4Url };
} }
transcodeProcesses.delete(progressKey);
progressMap[progressKey] = progressState; progressMap[progressKey] = progressState;
broadcastWs(progressKey, { type: 'progress', key, progress: progressState }); broadcastWs(progressKey, { type: 'progress', key, progress: progressState });
if (progressState.status === 'finished') { if (progressState.status === 'finished') {
@@ -445,6 +494,7 @@ app.post('/api/transcode', async (req, res) => {
const errMessage = err?.message || ''; const errMessage = err?.message || '';
const isHardwareFailure = !attemptedSoftwareFallback && encoderName !== codecMap.software[safeCodec] && shouldRetryWithSoftware(errMessage); const isHardwareFailure = !attemptedSoftwareFallback && encoderName !== codecMap.software[safeCodec] && shouldRetryWithSoftware(errMessage);
if (isHardwareFailure) { if (isHardwareFailure) {
transcodeProcesses.delete(progressKey);
attemptedSoftwareFallback = true; attemptedSoftwareFallback = true;
console.warn(`Hardware encoder failed for ${key}; retrying with software encoder`, errMessage); console.warn(`Hardware encoder failed for ${key}; retrying with software encoder`, errMessage);
try { try {
@@ -465,6 +515,7 @@ app.post('/api/transcode', async (req, res) => {
} }
cleanupTmpInput(); cleanupTmpInput();
transcodeProcesses.delete(progressKey);
console.error(`Error transcoding ${key}:`, err); console.error(`Error transcoding ${key}:`, err);
const failedState = { status: 'failed', percent: progressMap[progressKey]?.percent || 0, details: err.message || 'Transcoding failed', mp4Url }; const failedState = { status: 'failed', percent: progressMap[progressKey]?.percent || 0, details: err.message || 'Transcoding failed', mp4Url };
progressMap[progressKey] = failedState; progressMap[progressKey] = failedState;