Files
hms-mediaengine/_entwicklung/docs/plugin-sdk/example-walkthrough.md
T
HMS MediaEngine Agent 362e089be0 AUFGERAUMT: Root auf 10 sichtbare Elemente reduziert
Der Nutzer hat recht: Der Ordner war voller Entwicklungs-Muell.
Jetzt ist sauber getrennt:

ROOT (was der Nutzer sieht und braucht):
- run.py                     = das Programm
- hms_app/                   = der Anwendungscode
- HMS MediaEngine.app        = macOS Doppelklick-Starter
- HMS-Start.vbs              = Windows Doppelklick-Starter
- HMS-Install.vbs             = Windows Erst-Installation
- HMS-Mac-Install.command     = macOS Homebrew-Installation
- HMS-Portable-Install.command = macOS Portable-Installation (16GB-Fix)
- installer_gui.py           = grafischer Installer
- launcher.pyw + launcher_core.py = interne Start-Logik
- LIESMICH.txt               = 10-Zeilen-Kurzanleitung
- .gitignore

_entwicklung/ (alles andere, NICHT benoetigt):
- packages/ apps/ native/ plugins/ tools/ schemas/ tests/ docs/
  build/ fixture_profiles/
- PLAN.md STATUS.md ERRORS.md TEST_REPORT.md CHANGELOG.md README.md
- pyproject.toml uv.lock setup_*.sh/ps1 make_mac_app.py

Diese Trennung gilt ab sofort fuer alle Commits. Der Nutzer kann
_entwicklung/ loeschen wenn er Platz braucht - die App laeuft ohne.

Verifiziert: App startet nach Aufraeumen unveraendert (Health 200).
2026-09-11 23:44:06 +02:00

329 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Beispiel: Ein Filter-Plugin von null schreiben
Dieses Dokument führt Schritt für Schritt durch die Erstellung eines neuen Filter-Plugins. Als Vorlage dient das eingebaute Vignette-Plugin (`plugins/builtin/filters/com.hms.fx.vignette/`), das ein GPU-Pass-Filter ist (PLAN.md §15.2).
## Ziel
Wir erstellen ein Plugin `com.hms.fx.vignette` einen Filter, der die Ränder eines Bildes abdunkelt oder einfärbt. Das Plugin unterstützt drei Backends: D3D11 (HLSL), OpenGL (GLSL) und OpenGL ES (GLES).
## Schritt 1: Verzeichnisstruktur anlegen
Ein Plugin ist ein Verzeichnis mit `plugin.json` und Shader-Unterordnern je Backend:
```text
com.hms.fx.vignette/
├─ plugin.json
└─ shaders/
├─ d3d11/
│ └─ main.hlsl
├─ gl/
│ └─ main.frag
└─ gles/
└─ main.frag
```
## Schritt 2: Manifest schreiben
Lege `plugin.json` an. Die Pflichtfelder sind `schema_version`, `id`, `name`, `version`, `api_version`, `kind`, `vendor`, `entrypoints`, `capabilities`, `failure_mode` (siehe [manifest-reference.md](manifest-reference.md)).
```json
{
"schema_version": 1,
"id": "com.hms.fx.vignette",
"name": "Vignette",
"version": "1.0.0",
"api_version": 1,
"kind": "filter",
"vendor": "HMS",
"entrypoints": {
"d3d11": {
"type": "hlsl",
"passes": [
{"pixel_shader": "shaders/d3d11/main.hlsl"}
]
},
"gl": {
"type": "glsl",
"passes": [
{"fragment": "shaders/gl/main.frag"}
]
},
"gles": {
"type": "glsl_es",
"passes": [
{"fragment": "shaders/gles/main.frag"}
]
}
},
"capabilities": {
"minimum_tier": "PI_LITE",
"requires_input_texture": true,
"supported_backends": ["d3d11", "gl", "gles"]
},
"parameters": [
{"id": "amount", "label": "Amount", "type": "float", "minimum": 0, "maximum": 1, "default": 0.5, "dmx_slots": [1]},
{"id": "radius", "label": "Radius", "type": "float", "minimum": 0, "maximum": 2, "default": 0.5, "dmx_slots": [2]},
{"id": "softness", "label": "Softness", "type": "float", "minimum": 0, "maximum": 1, "default": 0.4, "dmx_slots": [3]},
{"id": "roundness", "label": "Roundness", "type": "float", "minimum": 0, "maximum": 1, "default": 0.5, "dmx_slots": [4]},
{"id": "center_x", "label": "Center X", "type": "float", "minimum": 0, "maximum": 1, "default": 0.5, "dmx_slots": [5]},
{"id": "center_y", "label": "Center Y", "type": "float", "minimum": 0, "maximum": 1, "default": 0.5, "dmx_slots": [6]},
{"id": "color", "label": "Color", "type": "color", "default": [0, 0, 0], "dmx_slots": [7]},
{"id": "invert", "label": "Invert", "type": "bool", "default": 0, "dmx_slots": [8]},
{"id": "mix", "label": "Mix", "type": "float", "minimum": 0, "maximum": 1, "default": 1, "dmx_slots": []}
],
"failure_mode": "bypass",
"adaptive_quality": {
"default": "auto",
"variants": [
{"id": "low", "internal_scale": 1.0, "samples": 1},
{"id": "medium", "internal_scale": 1.0, "samples": 1},
{"id": "high", "internal_scale": 1.0, "samples": 1}
],
"transition_ms": 180,
"semantic_parameters_unchanged": ["mix"]
}
}
```
Wichtige Punkte:
- `kind: "filter"` verarbeitet eine Eingangs-Textur.
- `requires_input_texture: true` der Filter braucht eine Eingangs-Textur.
- `dmx_slots` belegen die Slots 18; `mix` belegt keinen Slot (gemeinsamer Effektvertrag, §14.8).
- `failure_mode: "bypass"` bei Fehler wird der Effekt überbrückt (§3.5).
- `adaptive_quality` deklariert drei Varianten; `semantic_parameters_unchanged: ["mix"]`.
## Schritt 3: HLSL-Shader schreiben (D3D11)
Lege `shaders/d3d11/main.hlsl` an. Der Shader deklariert die Textur, den Sampler und den Konstantenpuffer `hms_params` mit dem Standard-Uniform-Satz (§14.4) und den Pluginparametern:
```hlsl
// HMS MediaEngine - Vignette (PLAN.md §15.2)
// Randabdunklung/-färbung; ein GPU-Pass.
Texture2D u_input_texture : register(t0);
SamplerState u_sampler : register(s0);
cbuffer hms_params : register(b0)
{
float4 u_resolution; // xy = Auflösung in Pixeln
float u_time_seconds;
float u_delta_seconds;
float u_frame_index;
float u_layer_opacity;
float u_audio_rms;
float u_audio_peak;
float u_audio_bass;
float u_audio_mid;
float u_audio_treble;
float u_audio_beat;
float param_amount;
float param_radius;
float param_softness;
float param_roundness;
float param_center_x;
float param_center_y;
float param_color_r;
float param_color_g;
float param_color_b;
float param_invert; // 0 = abdunkeln, 1 = aufhellen
float param_mix;
float _pad0;
float _pad1;
float _pad2;
float _pad3;
};
float4 mainPS(float4 pos : SV_POSITION, float2 uv : TEXCOORD0) : SV_Target
{
float4 src = u_input_texture.Sample(u_sampler, uv);
float m = saturate(param_mix);
if (m < 0.01)
{
return src; // Mix 0 = kostenloser Bypass (§15.3)
}
float2 c = float2(param_center_x, param_center_y);
float2 d = uv - c;
d.x *= lerp(1.0, u_resolution.x / max(u_resolution.y, 1.0), param_roundness);
float dist = length(d);
float radius = max(param_radius, 0.0001);
float soft = max(param_softness, 0.0001);
float v = smoothstep(radius, radius + soft, dist);
v = saturate(v * param_amount);
if (param_invert > 0.5)
v = 1.0 - v;
float3 vignette = lerp(src.rgb, float3(param_color_r, param_color_g, param_color_b), v);
float3 rgb = lerp(src.rgb, vignette, m);
float4 out_c = float4(rgb, src.a);
out_c.rgb *= u_layer_opacity;
out_c.a *= u_layer_opacity;
return out_c;
}
```
## Schritt 4: GLSL-Shader schreiben (OpenGL)
Lege `shaders/gl/main.frag` an. Die Semantik ist identisch zur HLSL-Variante (§12.6, §15.4), nur die Syntax unterscheidet sich:
```glsl
#version 330 core
// HMS MediaEngine - Vignette (GLSL, Linux x64)
// Semantik identisch zur HLSL-Variante (§12.6, §15.4).
uniform sampler2D u_input_texture;
uniform vec2 u_resolution;
uniform float u_time_seconds;
uniform float u_delta_seconds;
uniform float u_frame_index;
uniform float u_layer_opacity;
uniform float u_audio_rms;
uniform float u_audio_peak;
uniform float u_audio_bass;
uniform float u_audio_mid;
uniform float u_audio_treble;
uniform float u_audio_beat;
uniform float param_amount;
uniform float param_radius;
uniform float param_softness;
uniform float param_roundness;
uniform float param_center_x;
uniform float param_center_y;
uniform float param_color_r;
uniform float param_color_g;
uniform float param_color_b;
uniform float param_invert;
uniform float param_mix;
in vec2 v_uv;
out vec4 fragColor;
void main()
{
vec4 src = texture(u_input_texture, v_uv);
float m = clamp(param_mix, 0.0, 1.0);
if (m < 0.01)
{
fragColor = src;
return;
}
vec2 c = vec2(param_center_x, param_center_y);
vec2 d = v_uv - c;
d.x *= mix(1.0, u_resolution.x / max(u_resolution.y, 1.0), param_roundness);
float dist = length(d);
float radius = max(param_radius, 0.0001);
float soft = max(param_softness, 0.0001);
float v = smoothstep(radius, radius + soft, dist);
v = clamp(v * param_amount, 0.0, 1.0);
if (param_invert > 0.5)
v = 1.0 - v;
vec3 vignette = mix(src.rgb, vec3(param_color_r, param_color_g, param_color_b), v);
vec3 rgb = mix(src.rgb, vignette, m);
vec4 out_c = vec4(rgb, src.a);
out_c.rgb *= u_layer_opacity;
out_c.a *= u_layer_opacity;
fragColor = out_c;
}
```
## Schritt 5: GLES-Shader schreiben (OpenGL ES 2.0)
Lege `shaders/gles/main.frag` an. GLES 2.0 hat eigene Regeln: `#version 100`, `precision mediump float;`, `texture2D` statt `texture`, `varying` statt `in`/`out`, `gl_FragColor` statt `fragColor` (siehe [shader-contract.md](shader-contract.md)):
```glsl
#version 100
// HMS MediaEngine - Vignette (GLES, Raspberry Pi)
// ES 2.0; ein GPU-Pass.
precision mediump float;
uniform sampler2D u_input_texture;
uniform vec2 u_resolution;
uniform float u_time_seconds;
uniform float u_delta_seconds;
uniform float u_frame_index;
uniform float u_layer_opacity;
uniform float u_audio_rms;
uniform float u_audio_peak;
uniform float u_audio_bass;
uniform float u_audio_mid;
uniform float u_audio_treble;
uniform float u_audio_beat;
uniform float param_amount;
uniform float param_radius;
uniform float param_softness;
uniform float param_roundness;
uniform float param_center_x;
uniform float param_center_y;
uniform float param_color_r;
uniform float param_color_g;
uniform float param_color_b;
uniform float param_invert;
uniform float param_mix;
varying vec2 v_uv;
void main()
{
vec4 src = texture2D(u_input_texture, v_uv);
float m = clamp(param_mix, 0.0, 1.0);
if (m < 0.01)
{
gl_FragColor = src;
return;
}
vec2 c = vec2(param_center_x, param_center_y);
vec2 d = v_uv - c;
d.x *= mix(1.0, u_resolution.x / max(u_resolution.y, 1.0), param_roundness);
float dist = length(d);
float radius = max(param_radius, 0.0001);
float soft = max(param_softness, 0.0001);
float v = smoothstep(radius, radius + soft, dist);
v = clamp(v * param_amount, 0.0, 1.0);
if (param_invert > 0.5)
v = 1.0 - v;
vec3 vignette = mix(src.rgb, vec3(param_color_r, param_color_g, param_color_b), v);
vec3 rgb = mix(src.rgb, vignette, m);
vec4 out_c = vec4(rgb, src.a);
out_c.rgb *= u_layer_opacity;
out_c.a *= u_layer_opacity;
gl_FragColor = out_c;
}
```
## Schritt 6: Validieren
Das Plugin wird über den Lifecycle-Manager validiert (`packages/plugin_sdk/hms_plugin_sdk/lifecycle.py`). `discover()` lädt `plugin.json`, validiert es (inkl. Shader-Existenz) und überführt das Plugin in `validated` oder `quarantined`/`incompatible` (siehe [lifecycle.md](lifecycle.md)).
Der Validator prüft unter anderem:
- Manifest-Schema;
- eindeutige Plugin-ID und semantische Version;
- API-Kompatibilität;
- Pfad- und ZIP-Sicherheit;
- erlaubte Dateitypen und Größenlimits;
- Shader-Kompilierung;
- Capability-Prüfung;
- vollständige, vorab kompilierbare Adaptive-Quality-Varianten und unveränderte Parametersemantik;
- Lizenzmetadaten;
- Hash des Pakets.
## Schritt 7: Aktivieren
Nach erfolgreicher Validierung durchläuft das Plugin den Lebenszyklus `validated → installed → enabled → compiled → active`. Der Wechsel der Qualitätsstufe erfolgt atomar an einer Framegrenze (siehe [adaptive-quality.md](adaptive-quality.md)).
## Zusammenfassung
Ein Filter-Plugin besteht aus:
1. `plugin.json` mit Pflichtfeldern, Parametern und Adaptive Quality;
2. je deklariertem Backend eine Shader-Datei mit dem Standard-Uniform-Satz (§14.4);
3. semantisch identischen Implementierungen über alle Backends (§12.6, §15.4).