Skip to content

feat(plugin-sdk): add video packet types, source node support, and host-side tick loop#238

Merged
streamer45 merged 4 commits intomainfrom
devin/1775152135-native-plugin-source-video-abi
Apr 2, 2026
Merged

feat(plugin-sdk): add video packet types, source node support, and host-side tick loop#238
streamer45 merged 4 commits intomainfrom
devin/1775152135-native-plugin-source-video-abi

Conversation

@staging-devin-ai-integration
Copy link
Copy Markdown
Contributor

@staging-devin-ai-integration staging-devin-ai-integration bot commented Apr 2, 2026

Summary

Implements Phases 1–3 of the native plugin SDK infrastructure to support video output pins with proper pixel format metadata and source nodes (zero inputs, tick-driven output). This is groundwork for turning nodes like the Slint renderer into standalone plugins.

Phase 1 — Video packet types in C ABI (addresses #169):

  • CPixelFormat enum (Rgba8/I420/Nv12) and CRawVideoFormat struct
  • CPacketType::RawVideo (8) and CPacketType::EncodedVideo (9) discriminants
  • raw_video_format field on CPacketTypeInfo
  • Bidirectional conversions (pixel_format_to_c/from_c, raw_video_format_to_c/from_c, video frame ↔ CPacket)
  • native_plugin_entry! macro updated to emit proper video pin metadata instead of mapping to Binary
  • NATIVE_PLUGIN_API_VERSION bumped from 2 → 3
  • Note: CPacketType::EncodedVideo discriminant exists for forward-compat, but inbound conversion maps to Binary until a CVideoCodec enum is added (TODO in code)

Phase 2 — Source node trait and macro in SDK:

  • SourceConfig struct with from_fps() / from_interval_us() helpers
  • NativeSourceNode trait (metadata, source_config, new, tick, update_params, cleanup)
  • native_source_plugin_entry! macro generating tick/get_source_config FFI exports and a stub process_packet
  • CSourceConfig and CTickResult C ABI types with convenience constructors
  • CNativePluginAPI extended with Option<fn> fields for get_source_config and tick (backward-compatible v3)

Phase 3 — Host-side wrapper for source plugins:

  • PluginMetadata gains is_source, tick_interval_us, max_ticks
  • Source capability detected during load() via temporary instance probe with get_source_config (warns if probe fails)
  • NativeNodeWrapper::run() split into run_processor() (existing input-driven loop) and run_source() (new tick-driven loop)
  • run_source() implements Initializing → Ready → Start → Running → tick → Stopped lifecycle
  • Tick timing uses tokio::time::interval with MissedTickBehavior::Skip (matching existing video nodes)
  • Zero tick_interval_us clamped to 1µs to prevent tokio::time::interval panic
  • Cancellation-aware inter-tick wait via tokio::select!
  • All exit paths (including pre-start) emit Stopped state for consistent lifecycle
  • Tick loop breaks on output channel close (source nodes lack the input-close backstop)
  • register_plugins() handles empty inputs for source plugins

Compositor fix — frame-aligned sync in oneshot mode:

  • In oneshot mode with multiple sources producing at different rates (e.g. colorbars vs slint), the compositor consumed frames from the faster source ahead of the slower one, causing asymmetric slot removal and producing frames with missing layers (black background + overlay)
  • Fix: check readiness of all active (non-closed) slots via is_empty() before dequeuing, ensuring frame-aligned consumption regardless of production rate differences

Closes #169

Review & Testing Checklist for Human

  • Compositor frame-aligned sync: Verify the oneshot frame-sync change doesn't break existing single-source oneshot pipelines (e.g. video_colorbars.yml). The change only affects multi-source oneshot compositing — single-source should behave identically since all slots trivially have frames.
  • C ABI correctness: Verify CPacketTypeInfo layout with raw_video_format field — ensure no padding/alignment issues across compilers. Check that the v3 Option fields in CNativePluginAPI are ABI-safe (nullable function pointers).
  • Source node lifecycle: Verify the Ready→Start handshake integrates correctly with the pipeline coordinator. The run_source() implementation assumes NodeControlMessage::Start is sent after all downstream nodes are ready.
  • Video frame round-trip: Test that a plugin declaring PacketType::RawVideo(RawVideoFormat { width: Some(1920), height: Some(1080), pixel_format: Rgba8 }) correctly preserves metadata through the C ABI boundary.
  • Backward compatibility: Load an existing v2 plugin (without get_source_config/tick) against the v3 host and verify it still works as a processor.

Suggested test plan:

  1. Run video_colorbars.yml oneshot pipeline — should produce exactly 300 frames as before.
  2. Run video_slint_watermark.yml (from PR feat(nodes): extract Slint overlay into standalone video::slint node #237) — should produce exactly 300 composited frames and terminate cleanly, with no extra black+overlay frames.
  3. Write a minimal source plugin using native_source_plugin_entry!, build, load, and verify tick loop runs correctly.

Notes

  • just lint and just test both pass clean (all 68 compositor tests pass).
  • Macro duplication between native_plugin_entry! and native_source_plugin_entry! (~530 lines shared metadata logic) is a known fast-follow to extract into a helper macro.
  • Phase 4 (porting the Slint node to a plugin) is deferred to a follow-up PR.

Link to Devin session: https://staging.itsdev.in/sessions/41c6ad828d354ab8a388cfe3cbe6475f
Requested by: @streamer45


Staging: Open in Devin

…st-side tick loop

Phase 1 — Video packet types in C ABI:
- Add CPixelFormat enum (Rgba8/I420/Nv12) and CRawVideoFormat struct
- Extend CPacketType with RawVideo (8) and EncodedVideo (9) discriminants
- Add raw_video_format field to CPacketTypeInfo
- Implement bidirectional conversions for video types in conversions.rs
- Update native_plugin_entry! macro to emit proper video pin metadata
- Bump NATIVE_PLUGIN_API_VERSION from 2 to 3

Phase 2 — Source node trait and macro in SDK:
- Add SourceConfig struct with from_fps() and from_interval_us() helpers
- Add NativeSourceNode trait (metadata, source_config, new, tick,
  update_params, cleanup)
- Add native_source_plugin_entry! macro generating tick/get_source_config
  FFI exports and stub process_packet
- Add CSourceConfig and CTickResult types with convenience constructors
- Extend CNativePluginAPI with Option<fn> fields for get_source_config
  and tick (backward-compatible v3 additions)

Phase 3 — Host-side wrapper for source plugins:
- Add is_source, tick_interval_us, max_ticks to PluginMetadata
- Detect source capability during plugin load via temporary instance probe
- Split NativeNodeWrapper::run() into run_processor() (existing) and
  run_source() (new tick-driven loop)
- Implement Ready → Start → Running → tick → Stopped lifecycle
- Add apply_params_update() helper for FFI parameter serialization
- Handle empty inputs for source plugins in register_plugins()

Closes #169

Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
@staging-devin-ai-integration
Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

Copy link
Copy Markdown
Contributor Author

@staging-devin-ai-integration staging-devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 4 potential issues.

View 3 additional findings in Devin Review.

Staging: Open in Devin
Debug

Playground

Comment thread crates/plugin-native/src/wrapper.rs Outdated
Comment thread sdks/plugin-sdk/native/src/conversions.rs
Comment on lines +122 to +143
if let Some(get_source_config) = api.get_source_config {
// Create a temporary instance with no params to query source config
let temp_handle = (api.create_instance)(
std::ptr::null(),
plugin_log_callback_noop,
std::ptr::null_mut(),
);
if !temp_handle.is_null() {
let cfg = get_source_config(temp_handle);
if cfg.is_source {
metadata.is_source = true;
metadata.tick_interval_us = cfg.tick_interval_us;
metadata.max_ticks = cfg.max_ticks;
info!(
kind = %metadata.kind,
tick_interval_us = cfg.tick_interval_us,
max_ticks = cfg.max_ticks,
"Detected source plugin"
);
}
(api.destroy_instance)(temp_handle);
}
Copy link
Copy Markdown
Contributor Author

@staging-devin-ai-integration staging-devin-ai-integration bot Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Source config probed with no-params instance may diverge from actual runtime config

During plugin loading at crates/plugin-native/src/lib.rs:122-144, a temporary instance is created with null params to probe get_source_config. The tick_interval_us and max_ticks values are cached in PluginMetadata and used later by run_source at crates/plugin-native/src/wrapper.rs:648-649. If a source plugin's tick interval depends on constructor parameters (e.g., a configurable frame rate), the probed values will be stale because the real instance is created later with actual params via create_node. The NativeSourceNode::source_config(&self) method signature allows per-instance config, but the host only reads it once at load time from the no-params probe. This is a documented design choice ("If instance creation fails we fall back to treating it as a processor plugin") but could surprise plugin authors whose tick rate depends on params.

Staging: Open in Devin

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid point. This is a deliberate design trade-off documented in the code — source plugins must support parameterless construction for the probe to work. The alternative (a static API query without instance creation) would require adding another function pointer to CNativePluginAPI and changing the macro contract. For now, the fallback behavior (treating the plugin as a processor) is safe, and the NativeSourceNode trait's new() signature already takes Option<serde_json::Value>, so well-behaved source plugins will handle None params gracefully.

If this becomes a friction point for plugin authors, we can add a static get_source_config variant (no instance required) in a future API revision.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good refinement of the earlier point. You're right that the probed tick_interval_us/max_ticks could diverge from the real instance's config if the source plugin adjusts tick rate based on params.

In practice this is acceptable for now — the probe establishes that the plugin is a source and provides reasonable defaults. If we later need per-instance tick config, the fix is straightforward: query get_source_config again on the real instance inside run_source() after create_instance is called with actual params, and use those values instead of the metadata cache. That would be a small additive change in wrapper.rs.

Comment thread crates/plugin-native/src/wrapper.rs Outdated
…ointer

- Replace inner tokio::select! loop with try_recv() for non-blocking
  control message drain (fixes unreachable else branch that would hang
  source plugins indefinitely)
- Move tick sleep from spawn_blocking to tokio::time::sleep with
  cancellation-aware select! (avoids blocking thread pool, enables
  responsive shutdown)
- Add tokio 'time' feature to plugin-native Cargo.toml
- Fix dangling pointer in packet_type_to_c: return null pointers in
  CPacketTypeInfo, document caller must patch after storing owned value
- Remove unused mut on control_channel_open in Ready→Start loop

Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
staging-devin-ai-integration[bot]

This comment was marked as resolved.

…cycle

- Replace tokio::time::sleep with tokio::time::interval +
  MissedTickBehavior::Skip to prevent accumulated drift in tick loop
- Remove hardcoded Vp9 from EncodedVideo inbound conversion; map to
  Binary until CVideoCodec enum is added (TODO left in code)
- Move tick_count increment before error/done checks so it always
  reflects actual ticks executed
- Emit Stopped state on all pre-start exit paths (cancellation,
  shutdown, channel close) for consistent lifecycle reporting
- Remove unnecessary control_channel_open variable from Ready→Start loop
- Add warning log when source config probe fails (null from
  create_instance with no params)
- Add #[must_use] on CPacketTypeOwned to prevent silent drops
- Remove unused _context param from apply_params_update
- Remove unused EncodedVideoFormat import

Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
staging-devin-ai-integration[bot]

This comment was marked as resolved.

…t close

- Clamp tick_interval_us to at least 1µs to prevent
  tokio::time::interval panic when a plugin returns 0.
- Break the outer tick loop when output channel closes, since source
  nodes lack the input-close backstop that processor nodes have.

Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
@streamer45 streamer45 merged commit 32dec1f into main Apr 2, 2026
17 checks passed
@streamer45 streamer45 deleted the devin/1775152135-native-plugin-source-video-abi branch April 2, 2026 20:29
staging-devin-ai-integration bot pushed a commit that referenced this pull request Apr 2, 2026
Port the Slint renderer from PR #237 into a standalone native plugin
using the NativeSourceNode trait from PR #238. This keeps slint and
slint-interpreter as plugin-only dependencies, avoiding committing
them to the core workspace.

The plugin renders .slint files to RGBA8 video frames at configurable
resolution and frame rate. All Slint operations run on a shared
dedicated thread with channel-based message passing to handle the
non-Send constraint of Slint types.

Includes:
- Plugin crate with config, shared thread, and NativeSourceNode impl
- justfile build/lint/fix/copy targets
- CI format check and clippy in lint-simple job
- Marketplace metadata (plugin.yml) and regenerated official-plugins.json
- Docs reference page and updated plugin index (9 -> 10)
- Sample .slint files (watermark, scoreboard, lower_third)
- Sample oneshot pipeline using plugin::native::slint

Signed-off-by: Devin AI <devin@cognition.ai>
Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
staging-devin-ai-integration bot pushed a commit that referenced this pull request Apr 3, 2026
…ic drain

Cherry-picked from PR #238 branch (84364c6). In oneshot mode, verify
all active slots have pending frames before dequeuing any, so fast
sources aren't consumed ahead of slower ones.

Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
streamer45 added a commit that referenced this pull request Apr 3, 2026
* feat: add native Slint UI plugin

Port the Slint renderer from PR #237 into a standalone native plugin
using the NativeSourceNode trait from PR #238. This keeps slint and
slint-interpreter as plugin-only dependencies, avoiding committing
them to the core workspace.

The plugin renders .slint files to RGBA8 video frames at configurable
resolution and frame rate. All Slint operations run on a shared
dedicated thread with channel-based message passing to handle the
non-Send constraint of Slint types.

Includes:
- Plugin crate with config, shared thread, and NativeSourceNode impl
- justfile build/lint/fix/copy targets
- CI format check and clippy in lint-simple job
- Marketplace metadata (plugin.yml) and regenerated official-plugins.json
- Docs reference page and updated plugin index (9 -> 10)
- Sample .slint files (watermark, scoreboard, lower_third)
- Sample oneshot pipeline using plugin::native::slint

Signed-off-by: Devin AI <devin@cognition.ai>
Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>

* fix: port sample pipelines from PR #237 with plugin node kind

Replace simplified steps-based oneshot pipeline with the full
compositor pipeline from PR #237 (colorbars + watermark overlay →
compositor → VP9 → WebM → http_output).

Add the missing dynamic pipeline (scoreboard + lower-third overlays
composited onto colorbars, streamed via MoQ).

Update docs example to match the correct node-based format.

All pipelines use plugin::native::slint instead of video::slint.

Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>

* fix: rename output pin to 'out' and add fontconfig dep to marketplace CI

- Rename plugin output pin from 'video' to 'out' to match the
  convention used by all built-in nodes. The pipeline compiler's
  'needs' syntax defaults from_pin to 'out', so using a different
  name broke graph wiring.

- Add libfontconfig1-dev to marketplace-build.yml system deps so
  the Slint plugin can be compiled by build_official_plugins.sh.

- Update docs to reflect the pin name change.

Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>

* fix: support parameterless construction for source config probe

The host probes source plugins by calling create_instance with no
params to read source_config().  The Slint plugin requires slint_file
which fails validation with default config.  Return a lightweight
probe-only instance when params is None so the host correctly
detects is_source=true and uses run_source() instead of
run_processor().

Without this fix the watermark node was treated as a processor,
crashed immediately (no input pins), and the compositor produced
output with only colorbars (no overlay).

Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>

* fix(plugin/slint): address review findings — scope guard, clone, naming, validation

- Add ClearWindow scope guard so CURRENT_WINDOW thread-local is cleared
  even if definition.create() or component.show() fails via early return
- Remove unnecessary data.clone() in static-UI render path — store first,
  then clone from cache (saves ~3.7 MB allocation per cache-miss render)
- Rename lib from slint_plugin to slint, matching the naming convention
  of all other native plugins (whisper, kokoro, vad, etc.)
- Add MAX_DIMENSION (7680 / 8K) upper bound on width/height to guard
  against config typos that would attempt multi-GB buffer allocations
- Reject absolute paths in validate_slint_asset_path() as defense-in-depth
- Document update_timers_and_animations() idempotency for multi-instance
  usage on the shared Slint thread

Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>

* fix(compositor): frame-aligned sync in oneshot mode prevents asymmetric drain

Cherry-picked from PR #238 branch (84364c6). In oneshot mode, verify
all active slots have pending frames before dequeuing any, so fast
sources aren't consumed ahead of slower ones.

Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>

* fix(plugin-native): re-query source config from live instance for per-instance max_ticks

The load-time probe creates a default instance (null params) to detect
source plugins, so max_ticks is always 0 (infinite). Per-instance params
like frame_count: 300 were never applied to the tick loop limit.

Now run_source() re-queries get_source_config() on the live instance
after it's created with actual params, so the tick loop respects the
configured frame_count.

Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>

* style: format wrapper.rs

Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>

* fix(plugin-native): use if-let instead of match for clippy single_match_else

Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>

* fix(plugin-native): avoid begin_call() leak when get_source_config is None

Split the tuple pattern into chained and_then/map so begin_call() is
only invoked when get_source_config is Some, preventing an in_flight_calls
counter leak in the else branch.

Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>

* improve graphics

* feat(client): add declarative overlay controls in client section (#240)

* feat(client): add declarative overlay controls in client section

Add a `controls` array to `ClientSection` so pipeline authors can
declare interactive widgets (toggle, text, number, button) targeting
specific node properties.  The StreamView renders these controls
automatically when a session is active, sending `TuneNodeAsync` /
`UpdateParams` messages on interaction.

Rust changes:
- New `ControlType` enum and `ControlConfig` struct in `yaml.rs`
- Extend `ClientSection` with `controls: Option<Vec<ControlConfig>>`
- Add `name` field to `NodeInfo` for node-name validation
- Lint rules: `control-unknown-node`, `control-number-no-bounds`
- Register new types for TypeScript generation

UI changes:
- New `OverlayControls` component with toggle/text/number/button widgets
- New `buildParamUpdate()` utility for dot-notation → nested JSON
- Integrate `OverlayControls` into `StreamView`

Sample:
- Add controls to `video_moq_slint_scoreboard.yml` for scoreboard
  and lower-third properties

Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>

* fix(controls): stabilize debounce/throttle with ref pattern, add useTuneNode hook

Address Devin Review feedback:

1. TextControl: store onSend in a ref so the debounce closure is stable
   across re-renders. Pending timers now always call the latest callback
   instead of leaking stale intermediate values.

2. NumberControl: apply the same ref pattern for onSend in the throttle
   closure for consistency, preventing similar issues.

3. Extract useTuneNode hook from useSession — a lightweight hook that
   only provides tuneNodeConfig without subscribing to pipeline or
   connection state, avoiding unnecessary re-renders in OverlayControls.

Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>

* fix(controls): address review feedback — dead code, guards, stale state, docs

1. Remove dead isDraggingRef in NumberControl (written but never read).

2. Guard buildParamUpdate against empty/malformed paths — filter empty
   segments, throw on zero valid segments. Add unit tests covering
   single/multi-segment, empty, dot-only, double-dot, and leading/
   trailing dot paths.

3. Fix ToggleControl stale checked on rapid double-click — use
   functional updater (setChecked(prev => ...)) with onSendRef
   pattern matching TextControl/NumberControl.

4. Update misleading 'stable' comment on makeSend to accurately
   describe that a new closure is created per render but child
   controls absorb this via onSendRef.

5. Move getWebSocketService() to module scope in useTuneNode since
   it returns a singleton — avoids a new reference on every render
   and keeps tuneNodeConfig deps minimal (only sessionId).

6. Document that ControlConfig.default is a UI-only hint — it seeds
   local widget state but is not sent to the server on mount.

Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>

* fix(controls): deep-merge partial nested updates in useTuneNode

useTuneNode now reads the current nodeParamsAtom state and deep-merges
the partial config before writing, so sibling nested properties are
preserved.  Previously, two controls targeting the same node but
different nested paths (e.g. properties.home_score and
properties.away_score) would clobber each other because writeNodeParams
does a shallow top-level merge.

Add deepMerge utility to controlProps.ts — recursively merges plain
objects, replaces arrays and primitives wholesale.  Includes 8 unit
tests covering nested merge, array replacement, type transitions,
successive merges, and immutability.

Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>

* test(e2e): add Playwright test for overlay controls

Add overlay-controls.spec.ts exercising all four control types (toggle,
text, number, button).  The test:

1. Selects the new 'Test: Overlay Controls' sample pipeline (colorbars →
   sink, no plugins/MoQ required).
2. Creates a session and verifies the Pipeline Controls section renders
   with correct labels and group headings.
3. Exercises each control type and asserts the correct TuneNodeAsync /
   UpdateParams WebSocket payload is sent:
   - Toggle: sends { draw_time: false }
   - Text:   sends { label: "World" } after 300ms debounce
   - Slider: sends { properties: { width: 800 } } (dot-notation path)
   - Button: sends { reset: true }
4. Asserts no unexpected console errors.
5. Destroys the session and cleans up.

Also adds data-testid='overlay-controls' to the OverlayControls
component for scoped locators, and fixes a TS6 useRef() arity error.

Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>

---------

Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-authored-by: StreamKit Devin <devin@streamkit.dev>
Co-authored-by: Claudio Costa <cstcld91@gmail.com>

* fix(lint): include controls in mode-mismatch-oneshot check

The `controls` field is dynamic-only but was not included in the
`has_dynamic_fields` check in `lint_client_section`, so a oneshot
pipeline with a `controls` section would not trigger the
`mode-mismatch-oneshot` warning.  Add it to the check and update the
warning message and doc comment accordingly.

Adds a unit test verifying the warning fires for controls-only oneshot
pipelines.

Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>

---------

Signed-off-by: Devin AI <devin@cognition.ai>
Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-authored-by: StreamKit Devin <devin@streamkit.dev>
Co-authored-by: Claudio Costa <cstcld91@gmail.com>
Co-authored-by: staging-devin-ai-integration[bot] <166158716+staging-devin-ai-integration[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proper video support for plugins, with example plugin

2 participants