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:
jetson committed 2026-09-21 11:07:42 +07:00
1 parent 42c80fd0fc
commit f9e411f407
7 files changed
+369 -37

No files matched your search

+18 -29
View File
@@ -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);
}
}