Files
hms-mediaengine/docs/plugin-sdk/example-walkthrough.md
T
HMS MediaEngine Agent 985f156525 Phase 3: Plugin-SDK-Dokumentation (docs/plugin-sdk/, 7 Dateien)
- README: Pluginarten, Verzeichnisstruktur, Quick Start (§14.1/§14.2)
- manifest-reference: komplette plugin.json-Feldreferenz mit
  Validator-Regeln und ZIP-Sicherheit (§27.2)
- shader-contract: Standard-Uniformsatz je Backend, cbuffer-Layout,
  16-Byte-Alignment, Multipass, AQ-Bindung, GLES-Einschraenkungen (§14.4)
- parameters-and-dmx: dmx_slots max 8 (§14.7), Kurven, 16-Bit-Werte,
  gemeinsamer Effektvertrag mix/blend/quality (§14.8)
- lifecycle: Zustandsmaschine, Quarantaene, Show-Lock (§14.5/§26.3)
- adaptive-quality: Varianten, Semantik-Erhalt, Kompilierung vor
  Aktivierung, Framegrenzen-Wechsel (§5.2)
- example-walkthrough: komplettes Filter-Plugin anhand vignette
- 1199 Zeilen, Deutsch, alle Beispiele aus echten Repo-Dateien
2026-09-11 01:57:30 +02:00

11 KiB
Raw Blame History

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:

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).

{
  "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:

// 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:

#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):

#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).

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).

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).