Skip to content

feat(canvas): waveflow:canvas/v1 world for per-track Canvas plugins (#473) - #478

Merged
InstaZDLL merged 2 commits into
mainfrom
feat/473-canvas-world
Aug 2, 2026
Merged

feat(canvas): waveflow:canvas/v1 world for per-track Canvas plugins (#473)#478
InstaZDLL merged 2 commits into
mainfrom
feat/473-canvas-world

Conversation

@InstaZDLL

@InstaZDLL InstaZDLL commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Phase A of #473 (Spotify Canvas) — the generic core foundation. Adds a new versioned plugin world, waveflow:canvas/v1, that lets a plugin resolve a per-track Canvas (a short looping video the host renders behind the now-playing view). The grey-area Spotify sourcing is NOT here — it will live in a separate, unsigned plugin repo (Phase B). This PR only lands the reusable, non-grey world + wiring, isolated exactly like the ui world was in #443.

The world

provider.track-canvas(artist, title, album?, duration-ms?) -> result<option<canvas>, string>, canvas = { url, entity-id? }. Host imports are the standard four (http/log/storage/config) — no library read. url must be a directly-playable mp4 (the webview has no HLS.js).

Distinct from a metadata plugin's per-album motion-cover-url: Canvas is per-track and sits above motion artwork in the backdrop precedence — manual Canvas > plugin Canvas > motion > slideshow > static cover.

Backend

  • plugin-sdk: worlds::CANVAS_V1 + WIT (wit/canvas/{plugin,deps/host/host}.wit). manifest.rs already validates via worlds::is_known, so it accepts the label automatically.
  • core: bindings::canvas bindgen (reuses source's host types via with:, no new host import), runtime::canvas_track_canvas + ProviderCanvas DTO.
  • app: commands::canvas::fetch_track_canvas fans out to enabled canvas plugins (per-plugin lock + blocking task + 20 s timeout), returns the first hit that passes the shared is_safe_motion_url SSRF guard, and is fail-soft — a plugin error, panic, or timeout is logged + skipped, never surfaced, so a misbehaving provider can't break playback. Registered in lib.rs.
  • fixture + test: a test-only, never-bundled plugins/canvas-fixture/ + committed wasm, exercised by tests/plugin_canvas.rs (hit / miss / provider-error — 3 tests green).

Frontend

  • fetchTrackCanvas wrapper + PluginCanvas type.
  • useTrackCanvas now takes the track (was trackId) and resolves manual local Canvas first, then the plugin fallback; its careful per-id caching / generation / profile-switch guards are unchanged.
  • CanvasStage tells a local path from a remote URL by the http(s) prefix (same split as MotionCoverOverlay).
  • The immersive picker's "remove" affordance keys on the manual clip only (a plugin URL isn't "a Canvas to remove").
  • The 3 consumers (ImmersiveNowPlaying / ImmersiveView / NowPlayingPanel) pass currentTrack.

Nothing changes at runtime until a canvas-world plugin is installed (none ships yet), so this is inert for current users.

Docs

CLAUDE.md (canvas-world clause + updated Track Canvas catalogue line), docs/features/plugins.md (new "The Canvas world" section), docs/features/ui.md (Track Canvas plugin-sourcing paragraph).

Deferred (Phase B/C)

The Spotify Canvas plugin itself (separate unsigned repo — TOTP + protobuf canvaz, sp_dc cookie once + ban warning, fail-soft) and the registry world enum gaining waveflow:canvas/v1 (needed only to list the plugin).

Validation

  • cargo clippy -p waveflow-core -p waveflow --features waveflow-core/plugins --all-targets — clean
  • cargo test -p waveflow-core --features plugins (plugin_canvas + plugin_ui + plugin_web_radio) — all green, no regressions
  • bun run typecheck / bun run lint — clean

Summary by CodeRabbit

  • Nouvelles fonctionnalités

    • Les Canvas vidéo peuvent désormais être fournis par des plugins pour les morceaux sans Canvas manuel.
    • Les vidéos distantes sont lues directement dans le lecteur.
    • La priorité d’affichage est désormais : Canvas manuel, plugin, motion cover, diaporama, puis pochette statique.
    • En cas d’erreur, de délai dépassé ou d’URL non sûre, le lecteur utilise automatiquement la source suivante.
  • Documentation

    • Ajout de la documentation sur l’utilisation, les permissions, les limites et l’intégration des plugins Canvas.

@InstaZDLL InstaZDLL added scope: frontend React/Vite frontend (src/) scope: backend Rust/Tauri backend (src-tauri/) scope: docs Docs, README, assets scope: plugins Plugin runtime, SDK, store, and bundled plugins type: feat New feature size: xl > 500 lines labels Aug 2, 2026
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: adbf78ca-e4dc-4eeb-a17a-2404b8a5758f

📥 Commits

Reviewing files that changed from the base of the PR and between fcdc1e5 and 0a17f91.

📒 Files selected for processing (4)
  • src-tauri/crates/core/src/plugin/runtime.rs
  • src/components/player/CanvasStage.tsx
  • src/components/player/ImmersiveView.tsx
  • src/lib/tauri/canvas.ts

📝 Walkthrough

Walkthrough

Cette PR ajoute le monde waveflow:canvas/v1, son exécution côté hôte et son intégration frontend. Le Canvas manuel reste prioritaire. Les plugins fournissent un repli distant avec validation SSRF, délai maximal et gestion tolérante des erreurs.

Changes

Intégration Canvas par plugin

Layer / File(s) Summary
Contrat Canvas et fixture WASM
src-tauri/crates/plugin-sdk/wit/canvas/..., src-tauri/plugins/canvas-fixture/..., src-tauri/crates/core/src/plugin/bindings.rs, src-tauri/crates/plugin-sdk/src/lib.rs
Le SDK ajoute le monde Canvas, les interfaces hôte et la fonction track-canvas. La fixture implémente les résultats succès, absence et erreur.
Exécution runtime et recherche backend
src-tauri/crates/core/src/plugin/runtime.rs, src-tauri/crates/app/src/commands/canvas.rs, src-tauri/crates/app/src/lib.rs, src-tauri/crates/core/tests/plugin_canvas.rs
Le runtime invoque les fournisseurs Canvas. La commande Tauri les exécute en parallèle avec verrou, délai de 20 secondes, validation d’URL et repli None. Les tests couvrent les résultats principaux.
Résolution et affichage frontend
src/hooks/useTrackCanvas.ts, src/lib/tauri/canvas.ts, src/components/player/..., src/components/layout/NowPlayingPanel.tsx, docs/features/..., CLAUDE.md
Le frontend transmet les métadonnées du morceau, essaie les plugins après le Canvas manuel et met en cache la source obtenue. CanvasStage accepte les URL distantes. La documentation décrit le contrat et la priorité d’affichage.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

  • InstaZDLL/WaveFlow#467 — Cette PR étend le flux Canvas manuel avec des fournisseurs plugin.
  • InstaZDLL/WaveFlow#352 — Elle reprend le modèle d’exécution parallèle, de délai et de gestion d’erreur des fournisseurs plugin.
  • InstaZDLL/WaveFlow#220 — Elle s’appuie sur les fondations du SDK plugin et ajoute le monde Canvas.

Sequence Diagram(s)

sequenceDiagram
  participant Interface
  participant useTrackCanvas
  participant Tauri
  participant CanvasRuntime
  participant CanvasPlugin
  participant Video
  Interface->>useTrackCanvas: transmet currentTrack
  useTrackCanvas->>Tauri: demande un Canvas plugin si aucun Canvas manuel
  Tauri->>CanvasRuntime: transmet les métadonnées du morceau
  CanvasRuntime->>CanvasPlugin: appelle track-canvas
  CanvasPlugin-->>CanvasRuntime: retourne canvas ou erreur
  CanvasRuntime-->>Tauri: retourne le premier résultat valide
  Tauri-->>useTrackCanvas: retourne PluginCanvas ou null
  useTrackCanvas-->>Video: fournit une source locale ou distante
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Le titre est concis, conforme à Conventional Commits et décrit clairement l’ajout du monde de plugins Canvas par piste.
Description check ✅ Passed La description couvre l’objectif, l’architecture, les tests, la documentation et les éléments différés de manière détaillée.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/473-canvas-world

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src-tauri/crates/app/src/commands/canvas.rs`:
- Around line 161-193: Inspect the waveflow:host/http.send implementation in
host_impl.rs and add an independent request timeout if none exists, using the
existing HTTP client/request path and timeout conventions. Ensure a stalled host
HTTP call returns within the configured limit without relying on the outer
CANVAS_PLUGIN_TIMEOUT or Wasmtime interruption, while preserving normal
successful responses and error propagation.

In `@src-tauri/crates/core/src/plugin/runtime.rs`:
- Around line 733-748: Factor the duplicated plugin-instantiation logic shared
by instantiate_source and instantiate_canvas into a macro_rules! helper that
accepts the function name and generated Plugin type. Use the macro to generate
both functions while preserving plugin loading, linker/store creation,
instantiation, and SourceError conversion behavior.

In `@src-tauri/plugins/canvas-fixture/Cargo.toml`:
- Around line 26-27: Alignez la dépendance wit-bindgen-rt de la fixture
canvas-fixture sur la version 0.41 utilisée par les bindings générés dans
bindings.rs, en conservant la fonctionnalité bitflags. Vérifiez les autres
fixtures et plugins pour appliquer leur convention d’appariement, sans régénérer
les bindings sauf si le projet standardise plutôt la version 0.44.

In `@src-tauri/plugins/canvas-fixture/src/lib.rs`:
- Around line 21-38: Étendez l’implémentation de Fixture::track_canvas avec des
branches déterministes basées sur title pour retourner des URL rejetables,
notamment file:///etc/passwd pour « scheme », une adresse IP privée pour «
private » et une URL .m3u8 pour « hls ». Conservez les comportements existants
pour les titres vides, « boom » et les autres titres afin que le test
d’intégration puisse vérifier que l’hôte refuse ces valeurs et retourne None.

In `@src-tauri/plugins/canvas-fixture/wit/world.wit`:
- Around line 16-46: Les définitions WIT du SDK et de la fixture doivent être
liées et vérifiées pour éviter toute divergence. Dans
src-tauri/plugins/canvas-fixture/wit/world.wit lignes 16-46, remplacez la copie
locale de package waveflow:canvas@1.0.0 en configurant
[package.metadata.component.target] dans
src-tauri/plugins/canvas-fixture/Cargo.toml pour pointer vers le WIT Canvas du
SDK. Dans src-tauri/crates/plugin-sdk/wit/canvas/deps/host/host.wit lignes 1-49,
ajoutez une assertion CI comparant les empreintes de toutes les copies de
package waveflow:host@1.0.0.

In `@src/components/player/CanvasStage.tsx`:
- Around line 68-71: Centralize remote Canvas URL detection in a shared
isRemoteCanvasUrl(path: string) helper and replace the duplicated regex in
src/components/player/CanvasStage.tsx lines 68-71, using it to choose between
the raw URL and convertFileSrc(path). Update
src/components/player/ImmersiveView.tsx lines 99-102 to use the same helper when
computing hasManualCanvas.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8ced7173-0686-4b68-9518-cebb9ad370da

📥 Commits

Reviewing files that changed from the base of the PR and between 9658d72 and fcdc1e5.

⛔ Files ignored due to path filters (2)
  • src-tauri/crates/core/tests/fixtures/canvas-fixture/plugin.wasm is excluded by !**/*.wasm
  • src-tauri/plugins/canvas-fixture/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (24)
  • CLAUDE.md
  • docs/features/plugins.md
  • docs/features/ui.md
  • src-tauri/crates/app/src/commands/canvas.rs
  • src-tauri/crates/app/src/lib.rs
  • src-tauri/crates/core/src/plugin/bindings.rs
  • src-tauri/crates/core/src/plugin/runtime.rs
  • src-tauri/crates/core/tests/fixtures/canvas-fixture/manifest.toml
  • src-tauri/crates/core/tests/plugin_canvas.rs
  • src-tauri/crates/plugin-sdk/src/lib.rs
  • src-tauri/crates/plugin-sdk/wit/canvas/deps/host/host.wit
  • src-tauri/crates/plugin-sdk/wit/canvas/plugin.wit
  • src-tauri/plugins/canvas-fixture/.gitignore
  • src-tauri/plugins/canvas-fixture/Cargo.toml
  • src-tauri/plugins/canvas-fixture/src/bindings.rs
  • src-tauri/plugins/canvas-fixture/src/lib.rs
  • src-tauri/plugins/canvas-fixture/wit/deps/waveflow-host/host.wit
  • src-tauri/plugins/canvas-fixture/wit/world.wit
  • src/components/layout/NowPlayingPanel.tsx
  • src/components/player/CanvasStage.tsx
  • src/components/player/ImmersiveNowPlaying.tsx
  • src/components/player/ImmersiveView.tsx
  • src/hooks/useTrackCanvas.ts
  • src/lib/tauri/canvas.ts

Comment thread src-tauri/crates/app/src/commands/canvas.rs
Comment thread src-tauri/crates/core/src/plugin/runtime.rs Outdated
Comment thread src-tauri/plugins/canvas-fixture/Cargo.toml
Comment thread src-tauri/plugins/canvas-fixture/src/lib.rs
Comment thread src-tauri/plugins/canvas-fixture/wit/world.wit
Comment thread src/components/player/CanvasStage.tsx Outdated
@InstaZDLL InstaZDLL self-assigned this Aug 2, 2026
@InstaZDLL

Copy link
Copy Markdown
Owner Author

@coderabbitai verified each finding against current code. Applied 2, skipped 4 with reasons — pushed in 0a17f91.

Applied

  • runtime.rs — instantiate dedup: added a define_instantiate! macro over the three identical world-instantiation fns (source / metadata / canvas); ui stays separate (it injects a library snapshot). All plugin tests still green.
  • CanvasStage.tsx / ImmersiveView.tsx — shared URL helper: extracted isRemoteCanvasUrl(source) into lib/tauri/canvas.ts; both the renderer and the manual-only picker check now use it (no more duplicated regex).

Skipped (still-valid check)

  • canvas.rs — host http timeout: already satisfied. The shared plugin HTTP client is built with .timeout(15s).connect_timeout(5s) (runtime.rs:207-208), so a stalled host/http.send returns within 15s independently of the 20s fanout timeout and the 30s wasmtime epoch. No change needed.
  • canvas-fixture wit-bindgen-rt 0.44: this IS the project standard — ui-fixture and web-radio both pin 0.44. The 0.41 in the generated bindings.rs is cargo-component 0.21.1's embedded generator version (identical across every fixture/plugin), and the RT crate is backward-compatible; the fixture builds + the integration test passes. Downgrading to 0.41 would diverge from the sibling convention.
  • canvas-fixture/src/lib.rs — rejectable-URL branches: the SSRF guard (is_safe_motion_url) runs in the app-side fetch_track_canvas fanout, not in the core canvas_track_canvas the fixture test exercises (that helper returns the URL unfiltered by design). The guard is already exhaustively unit-tested in motion_cache against exactly these cases — file:///etc/passwd, localhost, loopback, private ranges, 169.254.169.254, IPv4-mapped IPv6 (motion_cache.rs:279-305). Also, .m3u8 is a playback concern (webview has no HLS.js → handled by CanvasStage's onError fallback), not an SSRF concern, so it wouldn't be a guard rejection. Fixture branches would be dead code no runnable test reaches.
  • world.wit — reference SDK WIT + CI fingerprint: the local WIT copy is the deliberate, documented convention for every plugin (ui-fixture, web-radio, release-radar) — a real external plugin can't reach into the host repo, so it carries its own copy; the Cargo.toml comment spells this out. Fixture drift is already caught by plugin_canvas.rs, which instantiates the fixture against the host's own bindgen (a divergent world type would fail to instantiate). A cross-fixture CI fingerprint check is a broader, all-fixtures infra change, out of scope for this canvas PR.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Current commit and changed files =='
git rev-parse --short HEAD
git show --stat --oneline 0a17f91d -- 2>/dev/null || true

echo '== Runtime symbols and timeout configuration =='
ast-grep outline src-tauri/crates/core/src/plugin/runtime.rs --items all --view expanded | sed -n '1,240p'
rg -n -C 4 'define_instantiate|instantiate_(source|metadata|canvas|ui)|connect_timeout|\.timeout\(' \
  src-tauri/crates/core/src/plugin/runtime.rs

echo '== Canvas URL helper and call sites =='
ast-grep outline src/lib/tauri/canvas.ts --items all --view expanded
rg -n -C 4 'isRemoteCanvasUrl|fetchTrackCanvas' \
  src/lib/tauri/canvas.ts \
  src/components/player/CanvasStage.tsx \
  src/components/player/ImmersiveView.tsx

echo '== App-side Canvas SSRF validation and core helper =='
ast-grep outline src-tauri/crates/app/src/commands/canvas.rs --items all --view expanded
rg -n -C 5 'fetch_track_canvas|canvas_track_canvas|is_safe_motion_url|timeout' \
  src-tauri/crates/app/src/commands/canvas.rs \
  src-tauri/crates/core/src/plugin/runtime.rs \
  src-tauri/crates/core/src/artwork/motion_cache.rs

echo '== Fixture dependency and local WIT conventions =='
fd -a -t f 'Cargo.toml|world.wit' src-tauri/plugins | sort | while read -r f; do
  printf '\n--- %s ---\n' "$f"
  rg -n -C 2 'wit-bindgen-rt|Local copy|local copy|WIT|world' "$f" || true
done

echo '== Canvas integration test =='
ast-grep outline src-tauri/crates/core/tests/plugin_canvas.rs --items all --view expanded
sed -n '1,220p' src-tauri/crates/core/tests/plugin_canvas.rs

Length of output: 45973


@InstaZDLL Merci. J’ai vérifié le commit 0a17f91d.

Les deux corrections sont présentes :

  • define_instantiate! couvre les mondes source, metadata et canvas. Le monde ui conserve son chemin distinct avec le snapshot de bibliothèque.
  • CanvasStage.tsx et ImmersiveView.tsx utilisent isRemoteCanvasUrl depuis src/lib/tauri/canvas.ts.

Les quatre éléments non appliqués ne nécessitent pas de modification :

  • Le client HTTP des plugins applique bien un délai global de 15 secondes et un délai de connexion de 5 secondes.
  • wit-bindgen-rt version 0.44 correspond aux fixtures canvas-fixture et ui-fixture.
  • fetch_track_canvas applique is_safe_motion_url avant de renvoyer l’URL au frontend. Les cas SSRF indiqués sont déjà testés dans motion_cache.rs.
  • Le test d’intégration plugin_canvas.rs charge le composant fixture avec les bindings hôte et vérifie les résultats hit, miss et erreur fournisseur. La copie WIT locale est aussi conforme à la convention observée pour les fixtures et plugins.

Je retire donc les quatre constats non applicables. Je n’ai pas exécuté les tests dans cette vérification.


✏️ Learnings added
Learnt from: InstaZDLL
URL: https://github.com/InstaZDLL/WaveFlow/pull/478

Timestamp: 2026-08-02T09:49:48.950Z
Learning: Dans `src-tauri/crates/core/src/plugin/runtime.rs`, le client HTTP utilisé par l’hôte de plugin applique `reqwest::blocking::Client::timeout(Duration::from_secs(15))`, `connect_timeout(Duration::from_secs(5))` et désactive les redirections. Les mondes de plugin qui utilisent `host/http.send` héritent de cette limite.

Learnt from: InstaZDLL
URL: https://github.com/InstaZDLL/WaveFlow/pull/478

Timestamp: 2026-08-02T09:49:48.950Z
Learning: Dans le flux Canvas, `src-tauri/crates/core/src/plugin/runtime.rs::canvas_track_canvas` retourne intentionnellement l’URL non filtrée du fournisseur. `src-tauri/crates/app/src/commands/canvas.rs::fetch_track_canvas` est la limite de sécurité qui appelle `motion_cache::is_safe_motion_url` avant de transmettre l’URL au frontend. Les tests de `src-tauri/crates/core/src/artwork/motion_cache.rs` couvrent les schémas non HTTPS et les cibles localhost, loopback, privées, link-local et IPv4-mapped IPv6.

Learnt from: InstaZDLL
URL: https://github.com/InstaZDLL/WaveFlow/pull/478

Timestamp: 2026-08-02T09:49:48.950Z
Learning: Les crates de plugins et fixtures Rust sous `src-tauri/plugins/` utilisent des copies WIT locales afin que `cargo component` puisse résoudre le monde sans dépendre de l’arborescence hôte. Les fixtures `canvas-fixture` et `ui-fixture` utilisent `wit-bindgen-rt` version `0.44`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@InstaZDLL InstaZDLL added this to the v1.8.0 milestone Aug 2, 2026
@InstaZDLL
InstaZDLL merged commit de01b35 into main Aug 2, 2026
14 checks passed
@InstaZDLL
InstaZDLL deleted the feat/473-canvas-world branch August 2, 2026 10:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: backend Rust/Tauri backend (src-tauri/) scope: docs Docs, README, assets scope: frontend React/Vite frontend (src/) scope: plugins Plugin runtime, SDK, store, and bundled plugins size: xl > 500 lines type: feat New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant