添加停止转码按钮
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
51
server.js
51
server.js
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user