feat: 15+ FPS live preview via WebSocket push
- flask-socketio + threading mode (no eventlet monkey-patching) - preview_queue in pipeline: downscale to 640p, JPEG encode, bounded queue - encoder thread per job reads queue → SocketIO broadcast to room - Frontend: SocketIO client replaces HTTP polling, handles preview_frame event - Configurable: preview_every_n, preview_max_dim, preview_jpeg_quality - 62/62 tests pass
This commit is contained in:
1 parent
42c80fd0fc
commit
f9e411f407
7 files changed
+369
-37
No files matched your search
+18
-29
@@ -352,13 +352,30 @@ function initStatusPage(jobId, initialStatus) {
|
||||
var cancelBtn = document.getElementById('cancel-btn');
|
||||
var terminalStates = ['COMPLETED', 'FAILED', 'CANCELLED'];
|
||||
var pollInterval = null;
|
||||
var previewInterval = null;
|
||||
var consecutiveFailures = 0;
|
||||
var connectionLostIndicator = null;
|
||||
var lastStatus = initialStatus;
|
||||
|
||||
if (!progressFill) return;
|
||||
|
||||
/* --- WebSocket live preview --- */
|
||||
var socket = io();
|
||||
socket.on('connect', function() {
|
||||
socket.emit('join_job', {job_id: jobId});
|
||||
});
|
||||
|
||||
socket.on('preview_frame', function(data) {
|
||||
if (livePreviewImg && data.frame) {
|
||||
livePreviewImg.src = 'data:image/jpeg;base64,' + data.frame;
|
||||
if (previewPlaceholder) previewPlaceholder.style.display = 'none';
|
||||
livePreviewImg.style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', function() {
|
||||
socket.emit('leave_job', {job_id: jobId});
|
||||
});
|
||||
|
||||
/* --- Cancel button --- */
|
||||
if (cancelBtn) {
|
||||
cancelBtn.addEventListener('click', function() {
|
||||
@@ -402,15 +419,8 @@ function initStatusPage(jobId, initialStatus) {
|
||||
progressRegion.setAttribute('aria-busy', data.status === 'RUNNING' || data.status === 'PENDING' ? 'true' : 'false');
|
||||
}
|
||||
|
||||
if (data.status === 'RUNNING' || data.status === 'PENDING') {
|
||||
updateLivePreview(jobId);
|
||||
}
|
||||
|
||||
if (terminalStates.indexOf(data.status) !== -1) {
|
||||
stopPolling();
|
||||
if (data.status !== 'COMPLETED') {
|
||||
updateLivePreview(jobId);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
@@ -460,20 +470,6 @@ function initStatusPage(jobId, initialStatus) {
|
||||
modelName.textContent = name || '—';
|
||||
}
|
||||
|
||||
/* --- Fetch and display latest annotated frame --- */
|
||||
function updateLivePreview(jid) {
|
||||
if (!livePreviewImg) return;
|
||||
livePreviewImg.src = '/api/jobs/' + jid + '/frame?t=' + Date.now();
|
||||
livePreviewImg.onload = function () {
|
||||
if (previewPlaceholder) previewPlaceholder.style.display = 'none';
|
||||
livePreviewImg.style.display = 'block';
|
||||
};
|
||||
livePreviewImg.onerror = function () {
|
||||
livePreviewImg.style.display = 'none';
|
||||
if (previewPlaceholder) previewPlaceholder.style.display = '';
|
||||
};
|
||||
}
|
||||
|
||||
/* --- Render result cards --- */
|
||||
function updateResults(results) {
|
||||
if (!resultsGrid) return;
|
||||
@@ -533,14 +529,10 @@ function initStatusPage(jobId, initialStatus) {
|
||||
/* --- Start / stop polling --- */
|
||||
function startPolling() {
|
||||
pollInterval = setInterval(pollJob, 2000);
|
||||
previewInterval = setInterval(function () {
|
||||
updateLivePreview(jobId);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
|
||||
if (previewInterval) { clearInterval(previewInterval); previewInterval = null; }
|
||||
}
|
||||
|
||||
/* --- Boot --- */
|
||||
@@ -558,8 +550,5 @@ function initStatusPage(jobId, initialStatus) {
|
||||
}
|
||||
if (terminalStates.indexOf(initialStatus) === -1) {
|
||||
startPolling();
|
||||
updateLivePreview(jobId);
|
||||
} else {
|
||||
updateLivePreview(jobId);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user