Add adaptive FPS and honest device reachability during streaming
DDP uses fire-and-forget UDP, so when a WiFi device becomes overwhelmed by sustained traffic, sends appear successful while the device is actually unreachable. This adds: - HTTP liveness probe (GET /json/info, 2s timeout) every 10s during streaming, exposed as device_streaming_reachable in target state - Adaptive FPS (opt-in): exponential backoff when device is unreachable, gradual recovery when it stabilizes — finds sustainable send rate - Honest health checks: removed the lie that forced device_online=true during streaming; now runs actual health checks regardless - Target editor toggle, FPS display shows effective rate when throttled, health dot reflects streaming reachability, red highlight when unreachable - Auto-backup scheduling support in settings modal Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -143,6 +143,7 @@ class TargetEditorModal extends Modal {
|
||||
brightness_threshold: document.getElementById('target-editor-brightness-threshold').value,
|
||||
fps: document.getElementById('target-editor-fps').value,
|
||||
keepalive_interval: document.getElementById('target-editor-keepalive-interval').value,
|
||||
adaptive_fps: document.getElementById('target-editor-adaptive-fps').checked,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -272,6 +273,8 @@ export async function showTargetEditor(targetId = null, cloneData = null) {
|
||||
document.getElementById('target-editor-brightness-threshold').value = thresh;
|
||||
document.getElementById('target-editor-brightness-threshold-value').textContent = thresh;
|
||||
|
||||
document.getElementById('target-editor-adaptive-fps').checked = target.adaptive_fps ?? false;
|
||||
|
||||
_populateCssDropdown(target.color_strip_source_id || '');
|
||||
_populateBrightnessVsDropdown(target.brightness_value_source_id || '');
|
||||
} else if (cloneData) {
|
||||
@@ -290,6 +293,8 @@ export async function showTargetEditor(targetId = null, cloneData = null) {
|
||||
document.getElementById('target-editor-brightness-threshold').value = cloneThresh;
|
||||
document.getElementById('target-editor-brightness-threshold-value').textContent = cloneThresh;
|
||||
|
||||
document.getElementById('target-editor-adaptive-fps').checked = cloneData.adaptive_fps ?? false;
|
||||
|
||||
_populateCssDropdown(cloneData.color_strip_source_id || '');
|
||||
_populateBrightnessVsDropdown(cloneData.brightness_value_source_id || '');
|
||||
} else {
|
||||
@@ -305,6 +310,8 @@ export async function showTargetEditor(targetId = null, cloneData = null) {
|
||||
document.getElementById('target-editor-brightness-threshold').value = 0;
|
||||
document.getElementById('target-editor-brightness-threshold-value').textContent = '0';
|
||||
|
||||
document.getElementById('target-editor-adaptive-fps').checked = false;
|
||||
|
||||
_populateCssDropdown('');
|
||||
_populateBrightnessVsDropdown('');
|
||||
}
|
||||
@@ -364,6 +371,8 @@ export async function saveTargetEditor() {
|
||||
const brightnessVsId = document.getElementById('target-editor-brightness-vs').value;
|
||||
const minBrightnessThreshold = parseInt(document.getElementById('target-editor-brightness-threshold').value) || 0;
|
||||
|
||||
const adaptiveFps = document.getElementById('target-editor-adaptive-fps').checked;
|
||||
|
||||
const payload = {
|
||||
name,
|
||||
device_id: deviceId,
|
||||
@@ -372,6 +381,7 @@ export async function saveTargetEditor() {
|
||||
min_brightness_threshold: minBrightnessThreshold,
|
||||
fps,
|
||||
keepalive_interval: standbyInterval,
|
||||
adaptive_fps: adaptiveFps,
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -765,8 +775,29 @@ function _patchTargetMetrics(target) {
|
||||
const metrics = target.metrics || {};
|
||||
|
||||
const fps = card.querySelector('[data-tm="fps"]');
|
||||
if (fps) fps.innerHTML = `${state.fps_current ?? 0}<span class="target-fps-target">/${state.fps_target || 0}</span>`
|
||||
+ `<span class="target-fps-avg">avg ${state.fps_actual?.toFixed(1) || '0.0'}</span>`;
|
||||
if (fps) {
|
||||
const effFps = state.fps_effective;
|
||||
const tgtFps = state.fps_target || 0;
|
||||
const fpsLabel = (effFps != null && effFps < tgtFps)
|
||||
? `${state.fps_current ?? 0}<span class="target-fps-target">/${effFps}↓${tgtFps}</span>`
|
||||
: `${state.fps_current ?? 0}<span class="target-fps-target">/${tgtFps}</span>`;
|
||||
const unreachableClass = state.device_streaming_reachable === false ? ' fps-unreachable' : '';
|
||||
fps.innerHTML = `<span class="${unreachableClass}">${fpsLabel}</span>`
|
||||
+ `<span class="target-fps-avg">avg ${state.fps_actual?.toFixed(1) || '0.0'}</span>`;
|
||||
}
|
||||
|
||||
// Update health dot to reflect streaming reachability when processing
|
||||
const healthDot = card.querySelector('.health-dot');
|
||||
if (healthDot && state.processing) {
|
||||
const reachable = state.device_streaming_reachable;
|
||||
if (reachable === false) {
|
||||
healthDot.className = 'health-dot health-offline';
|
||||
healthDot.title = t('device.health.streaming_unreachable') || 'Unreachable during streaming';
|
||||
} else if (reachable === true) {
|
||||
healthDot.className = 'health-dot health-online';
|
||||
healthDot.title = t('device.health.online');
|
||||
}
|
||||
}
|
||||
|
||||
const timing = card.querySelector('[data-tm="timing"]');
|
||||
if (timing && state.timing_total_ms != null) timing.innerHTML = _buildLedTimingHTML(state);
|
||||
|
||||
Reference in New Issue
Block a user