Skip to content

feat(nodes): extract Slint overlay into standalone video::slint node#237

Closed
staging-devin-ai-integration[bot] wants to merge 13 commits intomainfrom
devin/1775137558-slint-standalone-node
Closed

feat(nodes): extract Slint overlay into standalone video::slint node#237
staging-devin-ai-integration[bot] wants to merge 13 commits intomainfrom
devin/1775137558-slint-standalone-node

Conversation

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

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

Summary

Extracts the Slint UI overlay PoC from the compositor into a standalone video::slint source node. The node compiles a .slint file at init, renders RGBA8 frames at a configurable resolution/fps, and outputs them like any other video source (e.g. video::colorbars). Its output can be wired into the compositor as a regular layer.

Key design decisions

  • Shared Slint thread: All SlintNode instances share a single dedicated std::thread (lazily spawned via OnceLock). slint::platform::set_platform is process-global; Slint types are !Send (Rc-based). Each node communicates via tagged work items and per-node result channels.
  • Thread-local window adapter: CURRENT_WINDOW thread-local is swapped before each definition.create() / component.show() so each instance gets its own MinimalSoftwareWindow. Cleared after show() to prevent stale references.
  • static_ui config flag: When true, frames are cached and reused until properties change (via UpdateParams or keyframe cycling). When false (default), every frame is re-rendered so Slint timers and animations advance correctly.
  • Non-blocking result channel: Frame results use try_send instead of blocking_send to avoid head-of-line blocking on the shared thread. If a node's consumer is slow, frames are dropped rather than stalling all other nodes.
  • pollster::block_on replaces spin_on for .slint compilation — yields to the OS instead of busy-waiting.
  • set_platform error propagated instead of silently discarded.
  • Feature independence: slint feature does not depend on compositor. The compositor compiles cleanly without any Slint code.

Changes

  • New crates/nodes/src/video/slint.rs — standalone node with SlintConfig, shared thread, rendering internals
  • New feature slint in crates/nodes/Cargo.toml and apps/skit/Cargo.toml
  • Removed all slint_overlay code from compositor (mod.rs, config.rs, kernel.rs, slint_overlay.rs)
  • Updated sample pipelines to use standalone video::slint nodes wired as compositor inputs
  • New samples/slint/watermark.slint + samples/pipelines/oneshot/video_slint_watermark.yml (static overlay demo)

Review & Testing Checklist for Human

  • Run the watermark oneshot pipeline: just skit -- pipeline run samples/pipelines/oneshot/video_slint_watermark.yml — verify the watermark badge renders correctly on the colorbars output
  • Run the dynamic scoreboard pipeline with MoQ and verify runtime UpdateParams updates properties as expected
  • Verify static_ui: true caches frames correctly (watermark) and static_ui: false re-renders every frame (scoreboard)
  • Confirm cargo build --features compositor compiles without Slint deps
  • Review the try_send frame-drop behavior under load — the channel capacity is 2; in normal operation it should never be full

Notes

  • Devin Review flagged a potential node state leak on task cancellation (if tokio drops the future mid-await, Unregister isn't sent). The TrySendError::Closed path provides partial cleanup when a subsequent Render arrives, but a Drop guard would be more robust. Left as a future improvement.
  • update_timers_and_animations() is process-global and clock-based — safe for multi-node usage on the shared thread.
  • Only properties are mergeable at runtime via UpdateParams; init-time fields are documented as non-updatable in the YAML samples.

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


Staging: Open in Devin

@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

staging-devin-ai-integration[bot]

This comment was marked as resolved.

staging-devin-ai-integration[bot]

This comment was marked as resolved.

Add a new video source node (video::slint) that renders .slint UI
components to RGBA8 frames via the software renderer.  The node has no
inputs and a single broadcast output, following the same lifecycle as
colorbars (Ready → Start → Running).

Key design decisions:

- All Slint operations run on a single shared std::thread (lazily
  slint::platform::set_platform() is process-global.
- Each node gets a unique NodeId (UUID) and communicates with the shared
  thread through tagged work items and per-node result channels.
- A thread-local RefCell<Option<Rc<dyn WindowAdapter>>> is swapped
  before each definition.create() / component.show() call so every
  ComponentInstance is associated with its own MinimalSoftwareWindow.
- The new 'slint' feature does NOT depend on 'compositor', keeping the
  two fully decoupled.
- Properties can be set at init and updated at runtime via UpdateParams.
- Property keyframes cycle at a configurable interval.
- frame_count == 0 gives infinite real-time output; frame_count > 0
  produces exactly N frames then stops.

Sample pipelines updated to use standalone slint nodes wired as
compositor inputs instead of embedding slint_overlays in compositor
config.

Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
@staging-devin-ai-integration staging-devin-ai-integration bot force-pushed the devin/1775137558-slint-standalone-node branch from 9769088 to b079b13 Compare April 2, 2026 14:17
@staging-devin-ai-integration staging-devin-ai-integration bot changed the base branch from devin/1775123159-slint-compositor-overlay to main April 2, 2026 14:17
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 2 new potential issues.

View 9 additional findings in Devin Review.

Staging: Open in Devin
Debug

Playground

Comment thread crates/nodes/src/video/slint.rs
Comment on lines +151 to +158
fn validate_slint_asset_path(path: &str) -> Result<(), String> {
if path.contains("..") || !path.starts_with("samples/slint/") {
return Err(format!(
"Invalid slint_file: must start with 'samples/slint/' and not contain '..': {path}"
));
}
Ok(())
}
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.

📝 Info: Path validation does not constrain Slint compiler import resolution

validate_slint_asset_path at line 178 ensures the initial slint_file path starts with samples/slint/ and contains no ... However, the Slint compiler's build_from_path resolves import directives within the .slint file, which could reference files outside the sandbox via absolute paths or relative paths that resolve beyond samples/slint/. This follows the same limitation as compositor/overlay.rs:48 (validate_asset_path) which validates the initial path but cannot control downstream file I/O. For the current use case (server-controlled .slint files in the repo), this is acceptable, but it's worth noting if user-supplied .slint files are ever supported.

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.

Acknowledged. The trust boundary here is filesystem access — .slint files must be placed under samples/slint/ by someone with write access to the working directory. Slint's build_from_path does follow import directives, so a malicious .slint file could reference paths outside the directory. This is acceptable for the current use case (server-side rendering of trusted assets) but worth documenting if the feature ever expands to user-uploaded content. I'll add a brief comment to validate_slint_asset_path noting this limitation.

streamkit-devin and others added 2 commits April 2, 2026 14:43
…rde default to slint_file

- Switch result channel from std::sync::mpsc to tokio::sync::mpsc so the
  async run() method uses .recv().await instead of blocking the tokio
  worker thread.  The Slint thread side uses blocking_send().
- Add #[serde(default)] to SlintConfig.slint_file so partial UpdateParams
  JSON (e.g. only properties) deserializes correctly instead of silently
  failing.

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: Claudio Costa <cstcld91@gmail.com>
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 1 new potential issue.

View 11 additional findings in Devin Review.

Staging: Open in Devin
Debug

Playground

Comment thread crates/nodes/src/video/slint.rs Outdated

// Pump Slint's internal animation timers so time-based animations
// (e.g. slide-in transitions) advance on each tick.
slint::platform::update_timers_and_animations();
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.

📝 Info: update_timers_and_animations is process-global but clock-based, safe for multi-node

The slint::platform::update_timers_and_animations() call at line 316 is invoked for EVERY Render work item from ANY node. Since it's process-global, calling it for node A also advances timers for node B's component. However, this function is clock-based (uses wall-clock elapsed time), not call-count-based. Calling it multiple times in quick succession simply updates to the current time — the second call sees near-zero elapsed time. So multiple nodes rendering on the shared thread won't cause timers to run faster than real time.

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.

Acknowledged. update_timers_and_animations() is indeed process-global, so it runs N times per frame cycle when N nodes are active. Since Slint timers are wall-clock-based, the extra calls should be benign — the timer callback fires based on elapsed real time, not call count. The scoreboard.slint Timer increments elapsed per tick, but the tick interval is set to 1s, so additional calls to update_timers_and_animations within the same second won't cause extra timer firings. Worth verifying with real multi-node testing, but not a correctness issue in practice.

…f replacing whole config

Partial UpdateParams JSON (e.g. {"properties": {"score": 5}}) would
silently fail because the deserialized SlintConfig had an empty slint_file
which failed validate().  Now UpdateParams merges only the runtime-changeable
fields (properties, property_keyframes, keyframe_interval) into the existing
config, preserving immutable init-time fields.

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.

streamkit-devin and others added 2 commits April 2, 2026 14:58
… UpdateParams

Partial UpdateParams JSON (e.g. {"properties": {"home_score": 4}})
was replacing the entire properties map, dropping unmentioned keys.
Now uses extend() to merge only the provided keys.

property_keyframes and keyframe_interval are treated as init-time config
and left unchanged by merge_update, since serde defaults make it
impossible to distinguish absent fields from user-provided empty values.

Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
The scoreboard's Slint Timer is wall-clock-based and doesn't advance
meaningfully in batch mode (frame_count > 0) where frames render faster
than real-time.  Replace the oneshot demo with a static watermark badge
that works correctly in both batch and real-time modes.

The scoreboard remains available in the dynamic pipeline
(video_moq_slint_scoreboard.yml) where frame_count: 0 ensures real-time
pacing and timers work as expected.

Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
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 3 new potential issues.

View 14 additional findings in Devin Review.

Staging: Open in Devin
Debug

Playground

Comment thread crates/nodes/src/video/slint.rs Outdated
Comment thread crates/nodes/src/video/slint.rs
Comment on lines +165 to +172
fn validate_slint_asset_path(path: &str) -> Result<(), String> {
if path.contains("..") || !path.starts_with("samples/slint/") {
return Err(format!(
"Invalid slint_file: must start with 'samples/slint/' and not contain '..': {path}"
));
}
Ok(())
}
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.

📝 Info: Path validation does not restrict Slint import statements

validate_slint_asset_path at crates/nodes/src/video/slint.rs:165-172 validates the top-level .slint file path (must start with samples/slint/, no ..). However, the Slint compiler's build_from_path (line 394) resolves import statements within the .slint file, which could reference paths outside the sandbox (e.g., import { Foo } from "/etc/some_file"). This is a defense-in-depth concern rather than a practical vulnerability because: (1) an attacker would need write access to place a crafted .slint file inside samples/slint/; (2) the Slint compiler only parses .slint syntax, it won't expose arbitrary file contents; (3) the worst case is a compilation error from invalid imports. Still, if the threat model evolves, consider using Compiler::set_include_paths to restrict import resolution.

Staging: Open in Devin

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

Debug

Playground

streamkit-devin and others added 4 commits April 2, 2026 15:31
Without an explicit request_redraw(), MinimalSoftwareWindow's
draw_if_needed() only invokes the render closure when the window is
marked dirty.  For static UIs (no property changes, no active timers)
the window stops being dirty after the initial show(), causing
draw_if_needed() to skip rendering and leave the pixel buffer zeroed
(fully transparent).  This made overlays like the watermark invisible.

Calling request_redraw() before every draw_if_needed() guarantees a
full repaint each frame.  With RepaintBufferType::NewBuffer there is
no wasted incremental-diff cost — the renderer always paints the
entire scene anyway.

Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
Static UIs and frames between keyframe boundaries now reuse the
previously rendered RGBA8 buffer instead of re-rendering through the
Slint software renderer and premultiplied-to-straight conversion on
every frame.

Re-rendering is triggered only when:
- The node is first registered (dirty = true)
- An UpdateConfig work item arrives (runtime property change)
- The keyframe index crosses a boundary

The frame counter still advances on cached frames so keyframe
boundaries are detected at the correct time.

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

- Remove invalid 'vertical-alignment' property from Rectangle in
  watermark.slint — this caused Slint compilation to fail silently,
  producing no frames at all (the actual root cause of the invisible
  watermark).

- Cache rendered RGBA8 buffer on the shared Slint thread.  Static UIs
  and frames between keyframe boundaries now reuse the previously
  rendered buffer instead of re-rendering on every frame.  Re-rendering
  triggers only on: first frame, UpdateConfig, or keyframe boundary.
  The frame counter still advances on cached frames so keyframe
  boundaries are detected at the correct time.

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: Claudio Costa <cstcld91@gmail.com>
staging-devin-ai-integration[bot]

This comment was marked as resolved.

streamkit-devin and others added 3 commits April 2, 2026 15:43
- Wrap accent dot in a VerticalLayout with alignment:center so it is
  vertically centred within the badge row (previously top-aligned).
- Shrink badge to 180×44 px for a tighter, cleaner look.
- Slightly increase border-radius and padding for better proportions.
- Use semi-bold LIVE tagline with higher-contrast white-70% colour.
- Update pipeline YAML to match new badge dimensions (x=1080).

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

Critical fixes:
- Always call update_timers_and_animations() unconditionally in the
  Render handler so Slint Timer/animation state advances even when
  frames are served from cache.
- Add static_ui config flag (default false) to opt into frame caching
  for truly static UIs; dynamic UIs always re-render every frame.
- Replace blocking_send with try_send on the result channel to avoid
  head-of-line blocking on the shared Slint thread when one node's
  consumer is slow.

Suggestions:
- Propagate set_platform() error instead of silently discarding it.
- Clear CURRENT_WINDOW thread-local after component.show() to prevent
  stale Rc references.
- Replace spin_on (busy-wait) with pollster::block_on for .slint
  compilation; yields to the OS instead of burning CPU.
- Document merge_update limitation in dynamic pipeline YAML comments.

Nits:
- Add #[serde(default)] at struct level on SlintConfig (matches
  ColorBarsConfig pattern).
- Simplify registration to use SlintConfig::default().
- Change SAFETY comment to Invariant: (SAFETY is for unsafe blocks).

Watermark pipeline updated with static_ui: true.

Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
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 2 new potential issues.

View 16 additional findings in Devin Review.

Staging: Open in Devin
Debug

Playground

}]
}

#[allow(clippy::too_many_lines)]
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.

🟡 Lint suppression #[allow(clippy::too_many_lines)] lacks required rationale comment (AGENTS.md violation)

AGENTS.md mandates: "Do not blindly suppress lint warnings or errors with ignore/exception rules. Instead, consider refactoring or improving the code to address the underlying issue. If an exception is truly necessary, it must include a comment explaining the rationale." The #[allow(clippy::too_many_lines)] at line 649 has no accompanying comment. Some other lint suppressions in this file do include comments (e.g., crates/nodes/src/video/slint.rs:263 has // Receiver must be owned by this thread), but this one does not.

Suggested change
#[allow(clippy::too_many_lines)]
#[allow(clippy::too_many_lines)] // Node run loop follows Ready→Start→RenderLoop→Cleanup lifecycle; splitting would fragment the linear control flow without reducing complexity
Staging: Open in Devin

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

Debug

Playground

Comment on lines +854 to +855
// Unregister from the shared thread so it can drop our instance.
let _ = thread_handle.work_tx.send(SlintWorkItem::Unregister { node_id });
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.

🚩 Shared Slint thread: node state leak on task cancellation

If a node's tokio task is cancelled (dropped mid-await) rather than exiting cleanly, the cleanup code at crates/nodes/src/video/slint.rs:855 that sends SlintWorkItem::Unregister never executes. The NodeState (including the SlintInstance with its Rc-based window and component) would remain in the shared thread's nodes HashMap indefinitely. The shared thread does have a partial cleanup path via TrySendError::Closed at line 365-367 — if a subsequent Render work item arrives after the receiver is dropped, the node is removed. However, if no Render is pending when the task is cancelled, the state leaks until process exit. A Drop guard or scope-exit pattern could ensure Unregister is always sent.

Staging: Open in Devin

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

Debug

Playground

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 2, 2026
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>
@streamer45
Copy link
Copy Markdown
Owner

Closing in favor of plugin

@streamer45 streamer45 closed this Apr 3, 2026
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.

2 participants