Skip to content

feat: NDI support & input sources abstraction#439

Merged
gioelecerati merged 7 commits intomainfrom
gio/feat/ndi
Feb 12, 2026
Merged

feat: NDI support & input sources abstraction#439
gioelecerati merged 7 commits intomainfrom
gio/feat/ndi

Conversation

@gioelecerati
Copy link
Copy Markdown
Contributor

@gioelecerati gioelecerati commented Feb 11, 2026

Changes

  • Add NDI support with ctypes
    • NDI sdk detection for darwin, linux, win
    • Use native resolution
  • InputSource abstraction used by both the new NDI receiver and the existing spout receiver
  • OutputSink abstraction - spout sender moved into it
  • Frontend changes

How to test

  • Install the NDI sdk or NDI tools on your machine
  • Expose a NDI source (you can use for example OBS with the DIstroAV plugin enabling the "Preview" output, or install Resolume Arena and enable NDI output from the menu)
  • Start scope
  • Video input -> NDI -> Select the desired source
  • Play

In a follow-up PR

  • NDI sender for the OutputSink

Signed-off-by: gioelecerati <[email protected]>
Copy link
Copy Markdown
Collaborator

@leszko leszko left a comment

Choose a reason for hiding this comment

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

I skimmed through the backend part. Nice work @gioelecerati I think it's in the good direction, I'll do the complete review when you finish your PR.

Some early comments:

  1. I see that right now you copy a lot of logic from the Spout received logic, but do I understand correctly that you plan to replace the spout logic and now we'll only use the input source?
  2. Now you've implemented the input, but I guess you're planning to do the same for the output. Is that correct?
  3. As a side note: frame_processor.py is pretty overloaded. I see that you created a separate package core/inputs. That's great. Also when you work on _SpoutFrame class (or whatever you rename it to, consider moving it to a separate file. The same with some functions that are not strictly related to FrameProcessor. Thanks.

Comment on lines +631 to +634
# Handle generic input source settings
if "input_source" in parameters:
input_source_config = parameters.pop("input_source")
self._update_input_source(input_source_config)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do we plan to allow changing the input_source when the stream is ongoing?

These parameters here are like "runtime" parameters. There are also the "intial_parameters" used during the initialization.

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.

On the UI I am disallowing to switch from NDI to other inputs and viceversa, but it's just a patch because I am not sure how resolution changes are handled - for example, my NDI source was 1920x1080, switching to the 512x512 example video kept the output video as 1920x1080 but stretched, if you have any insight about this I will work to improve it

return sources


class _SpoutFrame:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If it's generic now, then I think we should rename it. Also, I think you can move it out of frame_processor.py

@gioelecerati
Copy link
Copy Markdown
Contributor Author

I see that right now you copy a lot of logic from the Spout received logic, but do I understand correctly that you plan to replace the spout logic and now we'll only use the input source?

Yes, the spout receiver should use the new input source abstraction in the end

Now you've implemented the input, but I guess you're planning to do the same for the output. Is that correct?

Yes! Would like to make a follow-up PR making a NDI sender. I need to hook it up with the NDI sdk detection, so we can display the option only if the runtime is running on the machine

@gioelecerati gioelecerati marked this pull request as ready for review February 12, 2026 06:38
@gioelecerati gioelecerati requested a review from leszko February 12, 2026 06:38
@gioelecerati
Copy link
Copy Markdown
Contributor Author

@leszko Marked this as ready for review after addressing your comments and tested it on windows
Some notes:

  • I created a output sink and moved the spout sender there. The syphon sender is not showing up for me on my mac, is that known issue? I don't see it on the main branch as well
  • Removed the unused classes and methods, frame processor is back at roughly the size it had before these changes - let me know if you would break it down even more

Copy link
Copy Markdown
Collaborator

@leszko leszko left a comment

Choose a reason for hiding this comment

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

Very nice PR @gioelecerati . Tested NDI and Spout in Windows and everything works smoothly! 🎉

Added some inline comments, but other than that the PR looks good.

Comment on lines +295 to +316
export const getInputSourceResolution = async (
sourceType: string,
identifier: string,
timeoutMs = 5000
): Promise<InputSourceResolution> => {
const response = await fetch(
`/api/v1/input-sources/${sourceType}/sources/${encodeURIComponent(identifier)}/resolution?timeout_ms=${timeoutMs}`,
{
method: "GET",
headers: { "Content-Type": "application/json" },
}
);

if (!response.ok) {
const errorText = await response.text();
throw new Error(
`Probe resolution failed: ${response.status} ${response.statusText}: ${errorText}`
);
}

return response.json();
};
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

How did we handle the resolution before? I mean the resolution that the Spout sent, didn't we need a similar logic?

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.

No Spout wasn't probing, it just had 512x512 hardcoded. Which I tried to do the same with NDI, but I was having the output trimmed or stretched depending on how I was applying it

This is also part of the reason why I went for not allowing source switch while you're on NDI, since the output will have the initial parameters resolution set as the NDI source resolution, ending up with a stretched output if you switch to camera or other source

Is there a way to change the resolution mid stream or are we planning to have that hot reloading parameters for w & h?

Either case, if it's possible to change res, I will address it in a follow up PR

"""Wraps a raw numpy array to match the VideoFrame interface.

Allows code that expects a VideoFrame (with .to_ndarray()) to work
with plain numpy arrays from input sources like Spout and NDI.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I wonder for Spout, do we need the conversion from Tensor to VideoFrame? Isn't Spout all about keeping everything in GPU?

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.

Ooof actually this rawframe is not used anymore, I will delete it

Not sure if I am knowledgeable enough about spout to answer your question tho, can you elaborate?

Signed-off-by: gioelecerati <[email protected]>
Signed-off-by: gioelecerati <[email protected]>
@gioelecerati gioelecerati merged commit 8ab4261 into main Feb 12, 2026
6 checks passed
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