You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Lets plugins register their own video input sources (e.g. YouTube, RTSP,
screen capture) by subclassing `InputSource` and exposing them via the
existing `register_nodes` hook. Replaces the hardcoded
`spout/ndi/syphon[/video_file]` tuples that gated input-source behavior
with a registry-driven path, so plugin sources flow through the same
code as built-ins.
Source-kind plugins (declared via `__scope_kind__ = "source"` on the
plugin's top-level package) are pinned to the local instance even in
cloud mode, since their frames originate locally.
## Backend — registry & dispatch
- `PluginManager.register_plugin_nodes` dispatches by class:
`InputSource` subclasses go into a new `_plugin_input_sources` dict
keyed by `source_id`; everything else still lands in `NodeRegistry`.
Exposed via `get_plugin_input_sources()` on both `PluginManager` and the
module.
- `get_input_source_classes()` merges the plugin dict into the built-in
map. Built-ins win on `source_id` collision, so a rogue plugin can't
hijack `spout`/`ndi`/`syphon`/`video_file`.
- `SourceManager.setup_multi_sources` drops the source-mode whitelist —
any node whose `source_mode` resolves in the registry spawns a receive
thread.
- `webrtc._parse_graph_node_ids` uses the registry instead of a
hardcoded tuple when splitting WebRTC-driven sources from server-side
ones. **Behavior change:** `video_file` is now classified as a
server-side source here (it already was in `SourceManager`), so graphs
with a `video_file` node no longer allocate a WebRTC track for it.
## Backend — `__scope_kind__` and cloud routing
- `probe_plugin_kind(package_spec)` detects source-kind plugins at
install time by AST-parsing the package's `__init__.py` for a top-level
`__scope_kind__ = "<literal>"`. Supports local paths (walk + AST-parse)
and `git+` URLs (shallow-clone to tempdir, then walk). PyPI specifiers
return `None` (no probe) — install via git URL or local path is the
workaround for source-kind plugins published to PyPI.
- `_read_scope_kind` reads the same attribute on already-installed
plugins via `importlib.import_module(top_level)`. Result is cached per
top-level module name to keep plugin-list refreshes cheap; cache is
cleared on install/uninstall/reload.
- `app.list_plugins`/`install_plugin`/`uninstall_plugin`/`reload_plugin`
route by kind:
- **list**: in cloud mode, merges local source-kind plugins (tagged
`origin=local`) with cloud plugins (tagged `origin=cloud`).
Local-listing failures degrade gracefully to cloud-only.
- **install**: probes `__scope_kind__`; source-kind plugins install
locally despite cloud mode.
- **uninstall/reload**: targets local if the plugin name is locally
installed; otherwise proxies to cloud.
## Backend — install/uninstall lifecycle
- `install_plugin_async` re-fires `register_plugin_nodes` after a
successful install so the new plugin's nodes/input sources show up
immediately — no server restart needed.
- `_uninstall_plugin_sync` walks the distribution's scope entry points
and unregisters them from pluggy by entry-point name (the plugin object
is often a class instance with no useful `__name__`). Then rebuilds
plugin-derived caches by re-firing the hooks. Without this, a subsequent
reload of any other plugin would resurrect the uninstalled plugin's
registrations.
- `_split_git_spec` handles `git+ssh://user@host/repo.git@branch`,
strips pip-style `#fragment`/`?query` (e.g. `#egg=…`) before splitting
on `@`.
## Frontend
- The Source node dropdown is built dynamically from
`/api/v1/input-sources`: browser-driven File/Camera are prepended, every
available backend source (plugin-registered included) is appended, and
`video_file` is hidden as a headless-only alias for File. The currently
selected mode stays visible as `"<name> (unavailable)"` if the host
doesn't support it, so cross-machine graphs don't silently lose UI.
- Unknown modes render a generic text input with the backend's
`source_description` as help text, so plugin sources get a reasonable UI
with no frontend changes.
- `sourceMode` is widened from a narrow union (`video | camera | spout |
ndi | syphon`) to `string`. The 8 ad-hoc `=== "video" || === "camera"`
checks across `StreamPage`, `useVideoSource`, and `SourceNode` are
consolidated behind `isBrowserSourceMode` in `graphUtils.ts`.
- `availableInputSources` is threaded through the graph-editor prop
chain (`StreamPage` → `GraphEditor` → `useGraphState` → `enrichNodes` →
`SourceNode`) and refetched whenever pipelines change, so
newly-installed plugin sources surface without a page reload.
- `PluginsTab` shows a `Local`/`Cloud` origin badge next to each plugin,
and `restartAndRefresh` makes the plugin/pipeline list refresh even if
the restart-wait times out (the install/uninstall/reload itself already
succeeded server-side).
## Companion plugin
Paired with an out-of-tree plugin at `~/scope-youtube` (`scope-youtube`)
that registers a `YouTubeInputSource` via `@hookimpl register_nodes` and
declares `__scope_kind__ = "source"` so it stays local in cloud mode.
---------
Signed-off-by: Rafal Leszko <rafal@livepeer.org>
Signed-off-by: Rafał Leszko <rafal@livepeer.org>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0 commit comments