mirror of
https://github.com/smittix/intercept.git
synced 2026-07-17 05:48:11 -07:00
Stream partial decoded images during SSTV decode progress
The decode canvas was always black because nothing drew on it. Now the backend encodes partial JPEG snapshots every 5% progress and the frontend uses an <img> tag with in-place DOM updates instead of recreating innerHTML on every SSE event. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -345,18 +345,33 @@ const SSTVGeneral = (function() {
|
|||||||
const liveContent = document.getElementById('sstvGeneralLiveContent');
|
const liveContent = document.getElementById('sstvGeneralLiveContent');
|
||||||
if (!liveContent) return;
|
if (!liveContent) return;
|
||||||
|
|
||||||
liveContent.innerHTML = `
|
let container = liveContent.querySelector('.sstv-general-decode-container');
|
||||||
<div class="sstv-general-canvas-container">
|
if (!container) {
|
||||||
<canvas id="sstvGeneralCanvas" width="320" height="256"></canvas>
|
liveContent.innerHTML = `
|
||||||
</div>
|
<div class="sstv-general-decode-container">
|
||||||
<div class="sstv-general-decode-info">
|
<div class="sstv-general-canvas-container">
|
||||||
<div class="sstv-general-mode-label">${data.mode || 'Detecting mode...'}</div>
|
<img id="sstvGeneralDecodeImg" width="320" height="256" alt="Decoding..." style="display:block;background:#000;">
|
||||||
<div class="sstv-general-progress-bar">
|
</div>
|
||||||
<div class="progress" style="width: ${data.progress || 0}%"></div>
|
<div class="sstv-general-decode-info">
|
||||||
|
<div class="sstv-general-mode-label"></div>
|
||||||
|
<div class="sstv-general-progress-bar">
|
||||||
|
<div class="progress" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
<div class="sstv-general-status-message"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sstv-general-status-message">${data.message || 'Decoding...'}</div>
|
`;
|
||||||
</div>
|
container = liveContent.querySelector('.sstv-general-decode-container');
|
||||||
`;
|
}
|
||||||
|
|
||||||
|
container.querySelector('.sstv-general-mode-label').textContent = data.mode || 'Detecting mode...';
|
||||||
|
container.querySelector('.progress').style.width = (data.progress || 0) + '%';
|
||||||
|
container.querySelector('.sstv-general-status-message').textContent = data.message || 'Decoding...';
|
||||||
|
|
||||||
|
if (data.partial_image) {
|
||||||
|
const img = container.querySelector('#sstvGeneralDecodeImg');
|
||||||
|
if (img) img.src = data.partial_image;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+26
-11
@@ -780,18 +780,33 @@ const SSTV = (function() {
|
|||||||
const liveContent = document.getElementById('sstvLiveContent');
|
const liveContent = document.getElementById('sstvLiveContent');
|
||||||
if (!liveContent) return;
|
if (!liveContent) return;
|
||||||
|
|
||||||
liveContent.innerHTML = `
|
let container = liveContent.querySelector('.sstv-decode-container');
|
||||||
<div class="sstv-canvas-container">
|
if (!container) {
|
||||||
<canvas id="sstvCanvas" width="320" height="256"></canvas>
|
liveContent.innerHTML = `
|
||||||
</div>
|
<div class="sstv-decode-container">
|
||||||
<div class="sstv-decode-info">
|
<div class="sstv-canvas-container">
|
||||||
<div class="sstv-mode-label">${data.mode || 'Detecting mode...'}</div>
|
<img id="sstvDecodeImg" width="320" height="256" alt="Decoding..." style="display:block;background:#000;">
|
||||||
<div class="sstv-progress-bar">
|
</div>
|
||||||
<div class="progress" style="width: ${data.progress || 0}%"></div>
|
<div class="sstv-decode-info">
|
||||||
|
<div class="sstv-mode-label"></div>
|
||||||
|
<div class="sstv-progress-bar">
|
||||||
|
<div class="progress" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
<div class="sstv-status-message"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sstv-status-message">${data.message || 'Decoding...'}</div>
|
`;
|
||||||
</div>
|
container = liveContent.querySelector('.sstv-decode-container');
|
||||||
`;
|
}
|
||||||
|
|
||||||
|
container.querySelector('.sstv-mode-label').textContent = data.mode || 'Detecting mode...';
|
||||||
|
container.querySelector('.progress').style.width = (data.progress || 0) + '%';
|
||||||
|
container.querySelector('.sstv-status-message').textContent = data.message || 'Decoding...';
|
||||||
|
|
||||||
|
if (data.partial_image) {
|
||||||
|
const img = container.querySelector('#sstvDecodeImg');
|
||||||
|
if (img) img.src = data.partial_image;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ original monolithic utils/sstv.py.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import io
|
||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
@@ -95,6 +97,7 @@ class DecodeProgress:
|
|||||||
signal_level: int | None = None # 0-100 RMS audio level, None = not measured
|
signal_level: int | None = None # 0-100 RMS audio level, None = not measured
|
||||||
sstv_tone: str | None = None # 'leader', 'sync', 'noise', None
|
sstv_tone: str | None = None # 'leader', 'sync', 'noise', None
|
||||||
vis_state: str | None = None # VIS detector state name
|
vis_state: str | None = None # VIS detector state name
|
||||||
|
partial_image: str | None = None # base64 data URL of partial decode
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
result: dict = {
|
result: dict = {
|
||||||
@@ -114,6 +117,8 @@ class DecodeProgress:
|
|||||||
result['sstv_tone'] = self.sstv_tone
|
result['sstv_tone'] = self.sstv_tone
|
||||||
if self.vis_state:
|
if self.vis_state:
|
||||||
result['vis_state'] = self.vis_state
|
result['vis_state'] = self.vis_state
|
||||||
|
if self.partial_image:
|
||||||
|
result['partial_image'] = self.partial_image
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@@ -380,6 +385,7 @@ class SSTVDecoder:
|
|||||||
image_decoder: SSTVImageDecoder | None = None
|
image_decoder: SSTVImageDecoder | None = None
|
||||||
current_mode_name: str | None = None
|
current_mode_name: str | None = None
|
||||||
chunk_counter = 0
|
chunk_counter = 0
|
||||||
|
last_partial_pct = -1
|
||||||
|
|
||||||
logger.info("Audio decode thread started")
|
logger.info("Audio decode thread started")
|
||||||
rtl_fm_error: str = ''
|
rtl_fm_error: str = ''
|
||||||
@@ -418,12 +424,28 @@ class SSTVDecoder:
|
|||||||
# Currently decoding an image
|
# Currently decoding an image
|
||||||
complete = image_decoder.feed(samples)
|
complete = image_decoder.feed(samples)
|
||||||
|
|
||||||
|
# Encode partial image every 5% progress
|
||||||
|
pct = image_decoder.progress_percent
|
||||||
|
partial_url = None
|
||||||
|
if pct >= last_partial_pct + 5 or complete:
|
||||||
|
last_partial_pct = pct
|
||||||
|
try:
|
||||||
|
img = image_decoder.get_image()
|
||||||
|
if img is not None:
|
||||||
|
buf = io.BytesIO()
|
||||||
|
img.save(buf, format='JPEG', quality=40)
|
||||||
|
b64 = base64.b64encode(buf.getvalue()).decode('ascii')
|
||||||
|
partial_url = f'data:image/jpeg;base64,{b64}'
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Emit progress
|
# Emit progress
|
||||||
self._emit_progress(DecodeProgress(
|
self._emit_progress(DecodeProgress(
|
||||||
status='decoding',
|
status='decoding',
|
||||||
mode=current_mode_name,
|
mode=current_mode_name,
|
||||||
progress_percent=image_decoder.progress_percent,
|
progress_percent=pct,
|
||||||
message=f'Decoding {current_mode_name}: {image_decoder.progress_percent}%'
|
message=f'Decoding {current_mode_name}: {pct}%',
|
||||||
|
partial_image=partial_url,
|
||||||
))
|
))
|
||||||
|
|
||||||
if complete:
|
if complete:
|
||||||
|
|||||||
Reference in New Issue
Block a user