Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document Method for outputting entire Editor Window #14

Open
Zehir opened this issue Mar 5, 2025 · 15 comments
Open

Document Method for outputting entire Editor Window #14

Zehir opened this issue Mar 5, 2025 · 15 comments
Labels
documentation Improvements or additions to documentation

Comments

@Zehir
Copy link

Zehir commented Mar 5, 2025

Hello,

I was wondering if this add-on could be used to stream the Godot Editor over NDI?
I’m already using NDI with other software in OBS, and since the Godot Editor itself is made with Godot, I assume this should be possible.

@unvermuthet
Copy link
Owner

Yes. Just check Enable Editor Output on the NDIOutput Node. Let me know if that works for you!

@unvermuthet unvermuthet added the question Further information is requested label Mar 6, 2025
@unvermuthet
Copy link
Owner

unvermuthet commented Mar 6, 2025

Make sure to enable Output Continuously in the Editor Settings. I've heard it stutters pretty badly otherwise.

@Zehir
Copy link
Author

Zehir commented Mar 6, 2025

For some reason the Editor display only a gray box but the game is displaying fine.

Any idea ?

Image

@Zehir
Copy link
Author

Zehir commented Mar 6, 2025

Again, goblins in the computer.. it's finally working!
But it's not exactly what I was hoping for. I was trying to get the popup and context menu to be displayed, but they are not. I suppose it's a Godot thing.
But ty for your help

@unvermuthet
Copy link
Owner

unvermuthet commented Mar 6, 2025

Yes, only the Editor Viewport showing the Game is transmitted. Not the Editor Viewport showing the Editor. Perhaps a misleading name.

@Zehir
Copy link
Author

Zehir commented Mar 6, 2025

You can transmit the Editor directly by injecting the NDI node in Godot to transmit the Editor himself. But it's the same as capturing the windows, you don't get the tooltips, context menu or pop-up

@unvermuthet
Copy link
Owner

unvermuthet commented Mar 6, 2025

Damn O: That's so cool! How did you do it?

@Zehir
Copy link
Author

Zehir commented Mar 6, 2025

Damn O: That's so cool! How did you do it?

With this little plugin I made to test it;
addons/godot-ndi-editor/godot-ndi-editor.gd

@tool
extends EditorPlugin

var ndi: NDIOutput

func _enter_tree() -> void:
	ndi = NDIOutput.new()
	ndi.name = "Godot Editor"
	ndi.enable_editor_output = true
	add_child(ndi)


func _exit_tree() -> void:
	if is_instance_valid(ndi):
		ndi.queue_free()

addons/godot-ndi-editor/plugin.cfg

[plugin]

name="godot-ndi-editor"
description="Inject NDI Output node to Godot Editor"
author="Zehir"
version="1.0.0"
script="godot-ndi-editor.gd"

Image

Feel free to add this example somewhere more visible than this issue.

The toolbox, popup, and other things are displayed in new windows. Maybe there is a way to inject the NDI node into them too, or have a viewport node that captures the content of another node in a transparent window to capture that and push it to an NDI Output 'popup' node. With the correct tooltip position. Most of the time the popup content is inside the area of the main windows but sometime it's overflowing. But for streaming it's still better than not displaying it at all.

@unvermuthet
Copy link
Owner

I love seeing this work because not once did it cross my mind and it just works anyway. It's like a emergent feature.

Thanks for sharing in such detail. I only have the class docs right now and I'm not sure if I can add a freeform section to those. It will definitly be included once I get a docs page up and running.

@unvermuthet unvermuthet added documentation Improvements or additions to documentation and removed question Further information is requested labels Mar 6, 2025
@unvermuthet unvermuthet changed the title Stream the Godot Editor Outputting the entire Editor Window Mar 6, 2025
@unvermuthet unvermuthet changed the title Outputting the entire Editor Window Document Method for outputting entire Editor Window Mar 6, 2025
@unvermuthet unvermuthet reopened this Mar 6, 2025
@Zehir
Copy link
Author

Zehir commented Mar 6, 2025

About the tooltip thingy problem the easy way to work with it it's to enable the Single Window Mode;

But sadly its not compatible with the new Embed game editor.

Maybe there is a way to look for all windows and create a virtual desktop of all viewports and add them on top of each other and output that to NDI. But performance may be not great

@unvermuthet
Copy link
Owner

Maybe I'm missing something but what would be the advantage over doing a Window Capture in OBS?

@Zehir
Copy link
Author

Zehir commented Mar 6, 2025

I am already using NDI to get the transparency of one window;

Image

And I was hoping that the tooltip would show. I just discovered the Single Window Mode 10 minutes ago.

But yes, in my case, with just the main window, there are no advantages.

But you could also inject the output node into the 2D viewport to always display it, even if you opened another tab. I suppose it's working.

@Zehir
Copy link
Author

Zehir commented Mar 6, 2025

Did some more testing. There are 1906 window nodes that could be shown. You could listen for the about_to_popup signal, then get the rendered texture of the Viewport, and use that texture to display it in a big viewport. Add the NDI node to that viewport.

You also need to listen for added nodes because some of them, like tooltips, are instantiated only when shown.

@Zehir
Copy link
Author

Zehir commented Mar 7, 2025

Did some more tests, but I think we could integrate this into your addon.

So this code does these things:

  • Adds an NDIOutput node in the main editor view.
  • Creates another window ("capture window") with all other views that are displayed on top of the window in a separate window and adds an NDIOutput node too.
  • Displays the menu and elements where they are in the script, except for windows that have borders like the plugin page. These are centered on the screen if you have only one bordered window.
  • That window is not transparent, but when capturing it with NDI, it appears transparent.

There are still things that need to be improved:

  • Sprite2D pooling: for now, there is a static list of 10, but I think it's large enough.
  • The editor view could be included in the separate window and have only one NDIOutput node.
  • Add settings.
  • Handle the running game.
  • The embedded game window.
  • Resize the capture windows when the editor is resized

About the embedded window: it consists of two windows. One of them has the buttons and is in the tree of the Godot editor, while the second one is the game window, which has no border and moves when you move the main window. So, I suppose there is a reference to that window somewhere in the tree—we may be able to access it and capture the video.

Regarding performance, it's working quite well.

Another possible idea is to just use the "capture window" with the main editor as the background and not use NDI at all.

@tool
extends EditorPlugin

var ndi: NDIOutput
var test_button: Button

var main_windows: Window
var poped_windows: Array[Window] = []
var popup_stuff: Window
var sprites_pool: Array[Sprite2D] = []


func _enter_tree() -> void:
	main_windows = get_tree().root
	ndi = NDIOutput.new()
	ndi.name = "Godot Editor"
	ndi.enable_editor_output = true
	add_child(ndi)
	
	test_button = Button.new()
	test_button.text = "Click me"
	test_button.pressed.connect(test)
	add_control_to_container(EditorPlugin.CONTAINER_TOOLBAR, test_button)
	
	
	
	for child in get_tree().root.find_children("*", "Window", true, false):
		connect_signals(child)

	popup_stuff = Window.new()
	popup_stuff.initial_position = Window.WINDOW_INITIAL_POSITION_CENTER_OTHER_SCREEN
	popup_stuff.size = Vector2(1920, 1080)
	popup_stuff.title = "Popup Stuff"
	popup_stuff.name = "Popup Stuff"
	popup_stuff.transparent = true
	popup_stuff.transparent_bg = true
	var ndi = NDIOutput.new()
	ndi.name = "Godot Editor Overlay"
	ndi.enable_editor_output = true
	popup_stuff.add_child(ndi)
	
	for i in 10:
		var sprite_2d = Sprite2D.new()
		sprite_2d.centered = false
		sprite_2d.visible = false
		sprite_2d.name = "popup_stuff_%d" %i
		sprites_pool.append(sprite_2d)
		popup_stuff.add_child(sprite_2d)
		
	add_child(popup_stuff)

	get_tree().node_added.connect(connect_signals)

func _exit_tree() -> void:
	if is_instance_valid(ndi):
		ndi.queue_free()
	if is_instance_valid(test_button):
		test_button.queue_free()

func test() -> void:
	print("Hello world")
	
	
	#await get_tree().create_timer(2.0).timeout


func connect_signals(node: Node):
	if node is Window:
		if node.is_embedded():
			return
		var callback:= on_visibility_changed.bind(node)
		if not node.visibility_changed.is_connected(callback):
			node.visibility_changed.connect(callback)

func on_visibility_changed(window: Window):
	if window.visible:
		add_poped_windows.call(window)
	else:
		remove_poped_windows(window)

func add_poped_windows(window: Window):
	#print("add_poped_windows", window.name, window.borderless)
	poped_windows.append(window)
	on_focus_entered(window)
	var callback:= remove_poped_windows.bind(window)
	if not window.tree_exiting.is_connected(callback):
		window.tree_exiting.connect(callback, CONNECT_ONE_SHOT)
		
	#if not window.focus_entered.is_connected(on_focus_entered.bind(window)):
	#	window.focus_entered.connect(on_focus_entered.bind(window))

	#if not window.focus_exited.is_connected(on_focus_exited.bind(window)):
	#	window.focus_exited.connect(on_focus_exited.bind(window))


func remove_poped_windows(window: Window):
	#print("remove_poped_windows", window.name)
	on_focus_exited(window)
	var index = poped_windows.find(window)
	if index > -1:
		sprites_pool[index].texture = null
		sprites_pool[index].visible = false
		poped_windows.remove_at(index)

func _process(delta: float) -> void:
	var poped_count = poped_windows.size()
	var borderless_count: int = 0
	var latest_borderless_index: int = -1
	for i in range(0, poped_count):
		var window: Window = poped_windows[i]
		if not window.borderless:
			borderless_count += 1
			latest_borderless_index = i
		var sprite: Sprite2D = sprites_pool[i]
		#prints(window.name, window.get_visible_rect())
		sprite.texture = window.get_texture()
		sprite.position = window.position - main_windows.position
		sprite.centered = false
		
	if borderless_count == 1:
		var window: Window = poped_windows[latest_borderless_index]
		var sprite: Sprite2D = sprites_pool[latest_borderless_index]
		sprite.centered = true
		sprite.position = popup_stuff.get_visible_rect().size / 2
	
func on_focus_entered(window: Window):
	var index = poped_windows.find(window)
	sprites_pool[index].visible = true

func on_focus_exited(window: Window):
	var index = poped_windows.find(window)
	sprites_pool[index].visible = false

@unvermuthet
Copy link
Owner

unvermuthet commented Mar 7, 2025

Porting this to C++ and maintaining it's functionality in the future feels like too much work, for what can mostly be recreated with a OBS Window Capture. I'd prefer documenting a small guide on how to do it for people who need it but I reccon most expect the Output Editor feature to work as it does.

Edit: Guess it doesn't work with the Embedded Game Window.

@unvermuthet unvermuthet moved this to Todo in godot-ndi Mar 7, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
Status: Todo
Development

No branches or pull requests

2 participants