EFFECTS LIVE: 21 Effekte in der Pipeline (FX1/FX2 je Layer, 16/16 E2E)

Echte GPU-nahe GStreamer-Effekte, zur Laufzeit pro Layer schaltbar:
- FX_CATALOG: 21 verfuegbare Effekte (gst-inspect verifiziert):
  blur, sharpen, color, contrast, brightness, gamma, pixelate,
  solarize, vertigo, edge, ripple, radioactv, revtv, quark, op,
  burn, dilate, warp, streak, aging, exclusion
- FX1 + FX2 pro Layer (Kette: videoconvert ! FX1 ! videoconvert !
  FX2 ! videoconvert - Muster gegen gst-launch verifiziert)
- Intensitaet je FX live regelbar OHNE Rebuild
  (Property-Update direkt am Element, Frames laufen weiter 118->120)
- Strukturwechsel (FX waehlen/entfernen) -> atomarer Rebuild mit
  Rollback; Intensitaeten werden auf neue Pipeline uebertragen
- DMX: fx1_channel/fx2_channel optional konfigurierbar -> steuert
  FX-Intensitaet live (E2E: Kanal 20 = 255 -> Intensitaet 1.0)
- UI: Layer-Editor mit FX1/FX2-Dropdown (Katalog via /api/fx)
  + Intensitaets-Slider je FX
- /api/fx: Effekt-Katalog mit Label/Bereich fuer die UI
- config.py: fx1_channel/fx2_channel in Validierung (optional, 1-512)
- Ungueltige FX-IDs werden abgewiesen (fallback None)

E2E-BEWEIS (16/16 PASS, im Container):
1 Health · 2 Katalog 21 Effekte · 3-8 alle Kern-FX enthalten ·
9 Layer · 10 FX1 blur -> Rebuild laeuft (118 Frames) ·
11 Intensitaet live ohne Ruckler · 12 FX2 color dazu (161 Frames) ·
13 FX entfernen ok (180 Frames) · 14 fx1_channel konfiguriert ·
15 DMX steuert FX-Intensitaet auf 1.0 · 16 ungueltiger FX abgewiesen

Regression: kompletter App-E2E weiterhin 14/14 PASS (app=0 fx=0)
This commit is contained in:
HMS MediaEngine Agent
2026-09-11 12:01:22 +02:00
parent 68ce87e5ac
commit 10b988c33a
5 changed files with 351 additions and 3 deletions
+15
View File
@@ -192,6 +192,10 @@ function renderLayerEdit(ls){const e=$('ledit');
<div class="frow"><label>Position X</label><input type="number" id="lx${L.id}" value="${L.x}" onchange="api('layers/update',{id:${L.id},x:this.value})"> Y <input type="number" id="ly${L.id}" value="${L.y}" onchange="api('layers/update',{id:${L.id},y:this.value})"></div>
<div class="frow"><label>Größe</label><input type="number" id="lw${L.id}" value="${L.width}" onchange="api('layers/update',{id:${L.id},width:this.value})"> × <input type="number" id="lh${L.id}" value="${L.height}" onchange="api('layers/update',{id:${L.id},height:this.value})"> px</div>
<div class="frow"><label>Z-Order</label><input type="number" id="lz${L.id}" min="0" max="15" value="${L.z}" onchange="api('layers/update',{id:${L.id},z:this.value})"></div>
<div class="frow"><label>Effekt 1</label><select id="fx1-${L.id}" onchange="api('layers/update',{id:${L.id},fx1:this.value||null})"></select></div>
<div class="frow"><label>FX1 Intensität</label><input type="range" id="fx1i-${L.id}" min="0" max="100" value="${Math.round((L.fx1_intensity??0.5)*100)}" oninput="api('layers/update',{id:${L.id},fx1_intensity:this.value/100})"></div>
<div class="frow"><label>Effekt 2</label><select id="fx2-${L.id}" onchange="api('layers/update',{id:${L.id},fx2:this.value||null})"></select></div>
<div class="frow"><label>FX2 Intensität</label><input type="range" id="fx2i-${L.id}" min="0" max="100" value="${Math.round((L.fx2_intensity??0.5)*100)}" oninput="api('layers/update',{id:${L.id},fx2_intensity:this.value/100})"></div>
</div>`).join('')||'<p style="color:var(--dim)">Keine aktiven Layer. Medien-Tab öffnen und Datei als Layer hinzufügen.</p>'}}
// Medien
@@ -253,6 +257,17 @@ async function saveSettings(){const body={
if(d.saved){$('setmsg').textContent='Gespeichert ✓';setTimeout(()=>$('setmsg').textContent='',2500);
if(d.hint)toast(d.hint)}}
let FX=[];
async function loadFx(){try{FX=await(await fetch('/api/fx')).json();fillFxSelects()}catch(e){}}
function fillFxSelects(){document.querySelectorAll('select[id^="fx1-"],select[id^="fx2-"]').forEach(sel=>{
const cur=sel.dataset.cur||'';const isFx1=sel.id.startsWith('fx1-');
const key=isFx1?'fx1':'fx2';const lay=sel.dataset.lay;
const val=(lastStatus?.layers||[]).find(l=>String(l.id)===lay)?.[key]||'';
sel.innerHTML='<option value=""> keiner </option>'+FX.map(f=>`<option value="${f.id}"${f.id===val?' selected':''}>${f.label}</option>`).join('');});}
// Selects bei jedem Layer-Render nachfuellen + dataset setzen
const _origRender=renderLayerEdit;
renderLayerEdit=function(ls){_origRender(ls);document.querySelectorAll('select[id^="fx1-"]').forEach(s=>s.dataset.lay=s.id.slice(4));document.querySelectorAll('select[id^="fx2-"]').forEach(s=>s.dataset.lay=s.id.slice(4));fillFxSelects()};
loadFx();
poll();
</script>
</body>