From 49b8a23281a3d26b9e710b8277e1fd2d4ca19a9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lera=20Elvo=C3=A9?= Date: Mon, 26 Feb 2024 11:37:57 +0000 Subject: [PATCH] add more OBS nodes, allow specifying event subscription types in OBS connection dialog (#81) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit adds: - OBS Event Received - OBS Set Source Visible - OBS Is Source Visible? - OBS Set Input Settings Reviewed-on: https://codeberg.org/StreamGraph/StreamGraph/pulls/81 Co-authored-by: Lera ElvoƩ Co-committed-by: Lera ElvoƩ --- addons/no-obs-ws/NoOBSWS.gd | 4 + classes/connections/connections.gd | 6 +- classes/deck/nodes/general/expression_node.gd | 5 +- classes/deck/nodes/obs/obs_event_received.gd | 48 +++++++++++ .../deck/nodes/obs/obs_is_source_visible.gd | 66 ++++++++++++++ .../deck/nodes/obs/obs_set_input_settings.gd | 59 +++++++++++++ .../deck/nodes/obs/obs_set_source_visible.gd | 86 +++++++++++++++++++ graph_node_renderer/deck_holder_renderer.gd | 6 ++ .../obs_websocket_setup_dialog.gd | 20 +++-- .../obs_websocket_setup_dialog.tscn | 26 ++++-- .../obs_ws_subscriptions_container.gd | 33 +++++++ .../obs_ws_subscriptions_container.tscn | 15 ++++ 12 files changed, 358 insertions(+), 16 deletions(-) create mode 100644 classes/deck/nodes/obs/obs_event_received.gd create mode 100644 classes/deck/nodes/obs/obs_is_source_visible.gd create mode 100644 classes/deck/nodes/obs/obs_set_input_settings.gd create mode 100644 classes/deck/nodes/obs/obs_set_source_visible.gd create mode 100644 graph_node_renderer/obs_ws_subscriptions_container.gd create mode 100644 graph_node_renderer/obs_ws_subscriptions_container.tscn diff --git a/addons/no-obs-ws/NoOBSWS.gd b/addons/no-obs-ws/NoOBSWS.gd index ea83718..200f3ee 100644 --- a/addons/no-obs-ws/NoOBSWS.gd +++ b/addons/no-obs-ws/NoOBSWS.gd @@ -4,6 +4,8 @@ class_name NoOBSWS const Authenticator := preload("res://addons/no-obs-ws/Authenticator.gd") const Enums := preload("res://addons/no-obs-ws/Utility/Enums.gd") +@export var subscriptions: Enums.EventSubscription = Enums.EventSubscription.ALL + var _ws: WebSocketPeer # {request_id: RequestResponse} var _requests: Dictionary = {} @@ -109,6 +111,7 @@ func _handle_message(message: Message) -> void: else: var m = Message.new() m.op_code = Enums.WebSocketOpCode.IDENTIFY + m._d["event_subscriptions"] = subscriptions _send_message(m) Enums.WebSocketOpCode.IDENTIFIED: @@ -165,6 +168,7 @@ func _authenticate(message: Message, password: String) -> void: var m = Message.new() m.op_code = Enums.WebSocketOpCode.IDENTIFY m._d["authentication"] = auth_string + m._d["event_subscriptions"] = subscriptions #print("MY RESPONSE: ") #print(m) _send_message(m) diff --git a/classes/connections/connections.gd b/classes/connections/connections.gd index 9de3b9c..f10399b 100644 --- a/classes/connections/connections.gd +++ b/classes/connections/connections.gd @@ -14,7 +14,11 @@ static func _twitch_eventsub_event_received(event_data : Dictionary): static func _twitch_chat_received(msg_dict : Dictionary): DeckHolder.send_event(&"twitch_chat", msg_dict) - + + +static func _obs_event_received(event_data: Dictionary): + DeckHolder.send_event(&"obs_event", event_data) + ## Temporary Function for loading generic Connection credentials from user:// as a [CredSaver] Resource static func load_credentials(service : String): diff --git a/classes/deck/nodes/general/expression_node.gd b/classes/deck/nodes/general/expression_node.gd index 0ce78c8..523fd35 100644 --- a/classes/deck/nodes/general/expression_node.gd +++ b/classes/deck/nodes/general/expression_node.gd @@ -47,7 +47,10 @@ func parse(input: Dictionary) -> Variant: func _value_request(_from_port : int) -> Variant: var input = await request_value_async(0) - return parse(input) + if input != null: + return parse(input) + else: + return parse({}) func _receive(_on_input_port: int, data: Variant) -> void: diff --git a/classes/deck/nodes/obs/obs_event_received.gd b/classes/deck/nodes/obs/obs_event_received.gd new file mode 100644 index 0000000..dbf8aea --- /dev/null +++ b/classes/deck/nodes/obs/obs_event_received.gd @@ -0,0 +1,48 @@ +# (c) 2023-present Eroax +# (c) 2023-present Yagich +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +extends DeckNode + +var cached_data: Dictionary + + +func _init() -> void: + name = "OBS Event Received" + node_type = name.to_snake_case() + description = "" + + add_input_port( + DeckType.Types.STRING, + "Event Name", + "field", + Port.UsageType.VALUE_REQUEST, + ) + + add_output_port( + DeckType.Types.ANY, + "Event Received", + "", + Port.UsageType.TRIGGER, + ) + + add_output_port( + DeckType.Types.DICTIONARY, + "Event Data", + ) + + +func _event_received(event_name: StringName, event_data: Dictionary = {}) -> void: + if event_name != &"obs_event": + return + + if event_data.get("event_type", "") != await resolve_input_port_value_async(0): + return + + cached_data = event_data.event_data + var id := UUID.v4() + send(0, event_data.event_data, id) + send(1, event_data.event_data, id) + + +func _value_request(__on_output_port: int) -> Variant: + return cached_data diff --git a/classes/deck/nodes/obs/obs_is_source_visible.gd b/classes/deck/nodes/obs/obs_is_source_visible.gd new file mode 100644 index 0000000..7780d44 --- /dev/null +++ b/classes/deck/nodes/obs/obs_is_source_visible.gd @@ -0,0 +1,66 @@ +# (c) 2023-present Eroax +# (c) 2023-present Yagich +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +extends DeckNode + +var noobs: NoOBSWS + + +func _init() -> void: + name = "OBS Is Source Visible?" + node_type = name.to_snake_case() + description = "Returns true if a source is visible in OBS." + + add_input_port( + DeckType.Types.STRING, + "Scene Name", + "field", + ) + + add_input_port( + DeckType.Types.NUMERIC, + "Source ID", + "spinbox:unbounded", + ) + + add_output_port( + DeckType.Types.BOOL, + "Visible", + ) + + +func _is_source_visible(scene_name: String, source_id: float) -> Variant: + if noobs == null: + noobs = Connections.obs_websocket + + var req := noobs.make_generic_request( + "GetSceneItemEnabled", + { + "scene_name": scene_name, + "scene_item_id": source_id, + } + ) + await req.response_received + var data := req.message + + return data.response_data.scene_item_enabled + + +func _receive(to_input_port: int, data: Variant) -> void: + if noobs == null: + noobs = Connections.obs_websocket + + if to_input_port == 0: + var source_id = await resolve_input_port_value_async(1) + var scene_name = str(data) + send(0, await _is_source_visible(scene_name, source_id)) + else: + var scene_name = await resolve_input_port_value_async(0) + var source_id = data + send(0, await _is_source_visible(scene_name, source_id)) + + +func _value_request(on_output_port: int) -> Variant: + var scene_name = await resolve_input_port_value_async(0) + var source_id = await resolve_input_port_value_async(1) + return await _is_source_visible(scene_name, source_id) diff --git a/classes/deck/nodes/obs/obs_set_input_settings.gd b/classes/deck/nodes/obs/obs_set_input_settings.gd new file mode 100644 index 0000000..3e5097b --- /dev/null +++ b/classes/deck/nodes/obs/obs_set_input_settings.gd @@ -0,0 +1,59 @@ +# (c) 2023-present Eroax +# (c) 2023-present Yagich +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +extends DeckNode + +var noobs: NoOBSWS + + +func _init() -> void: + name = "OBS Set Input Settings" + node_type = name.to_snake_case() + description = "Sets settings on a source in OBS." + aliases = ["source"] + + add_input_port( + DeckType.Types.STRING, + "Input/Source name", + "field", + ) + + add_input_port( + DeckType.Types.DICTIONARY, + "Settings", + ) + + add_input_port( + DeckType.Types.ANY, + "Set", + "button", + Port.UsageType.TRIGGER, + ) + + + +func _receive(to_input_port: int, data: Variant) -> void: + if noobs == null: + noobs = Connections.obs_websocket + + var input_name + var settings + + match to_input_port: + 0: + input_name = str(data) + settings = await resolve_input_port_value_async(1) + 1: + settings = data + input_name = await resolve_input_port_value_async(0) + 2: + input_name = await resolve_input_port_value_async(0) + settings = await resolve_input_port_value_async(1) + + var r := noobs.make_generic_request( + "SetInputSettings", + { + "input_name": input_name, + "input_settings": settings, + } + ) diff --git a/classes/deck/nodes/obs/obs_set_source_visible.gd b/classes/deck/nodes/obs/obs_set_source_visible.gd new file mode 100644 index 0000000..139ba17 --- /dev/null +++ b/classes/deck/nodes/obs/obs_set_source_visible.gd @@ -0,0 +1,86 @@ +# (c) 2023-present Eroax +# (c) 2023-present Yagich +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +extends DeckNode + +var noobs: NoOBSWS + +enum InputPorts{ + SCENE_NAME, + SOURCE_ID, + VISIBLE, + SET, +} + + +func _init() -> void: + name = "OBS Set Source Visible" + node_type = name.to_snake_case() + description = "Set a source's visibility." + + add_input_port( + DeckType.Types.STRING, + "Scene Name", + "field", + ) + + add_input_port( + DeckType.Types.NUMERIC, + "Source ID", + "spinbox:unbounded", + ) + + add_input_port( + DeckType.Types.BOOL, + "Visible", + "checkbox", + ) + + add_input_port( + DeckType.Types.ANY, + "Set", + "button", + Port.UsageType.TRIGGER, + ) + + get_input_ports()[2].set_value(true) + + +func _receive(to_input_port: int, data: Variant) -> void: + if noobs == null: + noobs = Connections.obs_websocket + + var scene_name + var source_id + var visible + + match to_input_port: + InputPorts.SCENE_NAME: + scene_name = str(data) + source_id = await resolve_input_port_value_async(InputPorts.SOURCE_ID) + visible = await resolve_input_port_value_async(InputPorts.VISIBLE) + InputPorts.SOURCE_ID: + source_id = data + scene_name = await resolve_input_port_value_async(InputPorts.SCENE_NAME) + visible = await resolve_input_port_value_async(InputPorts.VISIBLE) + InputPorts.VISIBLE: + scene_name = await resolve_input_port_value_async(InputPorts.SCENE_NAME) + source_id = await resolve_input_port_value_async(InputPorts.SOURCE_ID) + visible = bool(data) + InputPorts.SET: + scene_name = await resolve_input_port_value_async(InputPorts.SCENE_NAME) + source_id = await resolve_input_port_value_async(InputPorts.SOURCE_ID) + visible = await resolve_input_port_value_async(InputPorts.VISIBLE) + + var b := noobs.make_batch_request(false, NoOBSWS.Enums.RequestBatchExecutionType.SERIAL_FRAME) + b.add_request( + "SetSceneItemEnabled", + "", + { + "scene_name": scene_name, + "scene_item_id": source_id, + "scene_item_enabled": visible, + } + ) + b.add_request("Sleep", "", {"sleep_frames": 1}) + b.send() diff --git a/graph_node_renderer/deck_holder_renderer.gd b/graph_node_renderer/deck_holder_renderer.gd index 5e2aeb6..5b8532c 100644 --- a/graph_node_renderer/deck_holder_renderer.gd +++ b/graph_node_renderer/deck_holder_renderer.gd @@ -347,9 +347,15 @@ func _on_obs_websocket_setup_dialog_connect_button_pressed(state: OBSWebsocketSe match state: OBSWebsocketSetupDialog.ConnectionState.DISCONNECTED: obs_setup_dialog.set_button_state(OBSWebsocketSetupDialog.ConnectionState.CONNECTING) + no_obsws.subscriptions = obs_setup_dialog.get_subscriptions() + print(no_obsws.subscriptions) no_obsws.connect_to_obsws(obs_setup_dialog.get_port(), obs_setup_dialog.get_password()) await no_obsws.connection_ready obs_setup_dialog.set_button_state(OBSWebsocketSetupDialog.ConnectionState.CONNECTED) + no_obsws.event_received.connect( + func(m: NoOBSWS.Message): + Connections._obs_event_received(m.get_data()) + ) func _process(delta: float) -> void: diff --git a/graph_node_renderer/obs_websocket_setup_dialog.gd b/graph_node_renderer/obs_websocket_setup_dialog.gd index 6eb2fdb..7b64f4c 100644 --- a/graph_node_renderer/obs_websocket_setup_dialog.gd +++ b/graph_node_renderer/obs_websocket_setup_dialog.gd @@ -7,6 +7,7 @@ class_name OBSWebsocketSetupDialog @onready var port_spin_box: SpinBox = %PortSpinBox @onready var password_line_edit: LineEdit = %PasswordLineEdit @onready var connect_button: Button = %ConnectButton +@onready var obsws_subscriptions_container: PanelContainer = $VBoxContainer/OBSWSSubscriptionsContainer signal connect_button_pressed(state: ConnectionState) @@ -20,7 +21,7 @@ var state: ConnectionState @warning_ignore("narrowing_conversion") @onready var _old_port: int = port_spin_box.value @onready var _old_password: String = password_line_edit.text - +@onready var _old_subscriptions: int = obsws_subscriptions_container.get_subscriptions() func get_port() -> int: @@ -41,19 +42,24 @@ func set_button_state(p_state: ConnectionState) -> void: ConnectionState.CONNECTING: connect_button.text = "Connecting..." ConnectionState.CONNECTED: - Connections.save_credentials({"port" : get_port(), "password" : get_password()}, "OBS") + Connections.save_credentials({ + "port" : get_port(), + "password" : get_password(), + "subscriptions": obsws_subscriptions_container.get_subscriptions(), + }, "OBS") connect_button.text = "Disconnect" func _ready() -> void: - var loaded_creds = Connections.load_credentials("OBS") if loaded_creds != null: - password_line_edit.text = loaded_creds.data.password port_spin_box.value = loaded_creds.data.port - + obsws_subscriptions_container.set_subscriptions(loaded_creds.data.subscriptions) + else: + obsws_subscriptions_container.set_subscriptions(NoOBSWS.Enums.EventSubscription.ALL) + connect_button.pressed.connect( func(): @@ -74,3 +80,7 @@ func _ready() -> void: _old_port = int(port_spin_box.value) _old_password = password_line_edit.text ) + + +func get_subscriptions() -> int: + return obsws_subscriptions_container.get_subscriptions() diff --git a/graph_node_renderer/obs_websocket_setup_dialog.tscn b/graph_node_renderer/obs_websocket_setup_dialog.tscn index 3cfd516..4b13fe5 100644 --- a/graph_node_renderer/obs_websocket_setup_dialog.tscn +++ b/graph_node_renderer/obs_websocket_setup_dialog.tscn @@ -1,6 +1,7 @@ -[gd_scene load_steps=2 format=3 uid="uid://eioso6jb42jy"] +[gd_scene load_steps=3 format=3 uid="uid://eioso6jb42jy"] [ext_resource type="Script" path="res://graph_node_renderer/obs_websocket_setup_dialog.gd" id="1_6ggla"] +[ext_resource type="PackedScene" uid="uid://c1y0cg4arcbhw" path="res://graph_node_renderer/obs_ws_subscriptions_container.tscn" id="2_pab7h"] [node name="OBSWebsocketSetupDialog" type="ConfirmationDialog"] title = "OBS Websocket Setup" @@ -38,14 +39,21 @@ layout_mode = 2 size_flags_horizontal = 3 secret = true -[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer"] -layout_mode = 2 -theme_override_constants/margin_bottom = 20 - -[node name="CenterContainer" type="CenterContainer" parent="VBoxContainer"] -layout_mode = 2 - -[node name="ConnectButton" type="Button" parent="VBoxContainer/CenterContainer"] +[node name="ToggleSubscriptionsButton" type="Button" parent="VBoxContainer"] unique_name_in_owner = true layout_mode = 2 +size_flags_horizontal = 4 +toggle_mode = true +text = "Subscriptions" + +[node name="OBSWSSubscriptionsContainer" parent="VBoxContainer" instance=ExtResource("2_pab7h")] +visible = false +layout_mode = 2 + +[node name="ConnectButton" type="Button" parent="VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 4 text = "Connect" + +[connection signal="toggled" from="VBoxContainer/ToggleSubscriptionsButton" to="VBoxContainer/OBSWSSubscriptionsContainer" method="set_visible"] diff --git a/graph_node_renderer/obs_ws_subscriptions_container.gd b/graph_node_renderer/obs_ws_subscriptions_container.gd new file mode 100644 index 0000000..23cb4ec --- /dev/null +++ b/graph_node_renderer/obs_ws_subscriptions_container.gd @@ -0,0 +1,33 @@ +# (c) 2023-present Eroax +# (c) 2023-present Yagich +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +extends PanelContainer +@onready var h_flow_container: HFlowContainer = %HFlowContainer + + +func _ready() -> void: + for i in NoOBSWS.Enums.EventSubscription.keys(): + if NoOBSWS.Enums.EventSubscription[i] == NoOBSWS.Enums.EventSubscription.ALL: + continue + + if NoOBSWS.Enums.EventSubscription[i] == NoOBSWS.Enums.EventSubscription.NONE: + continue + + var cb := CheckBox.new() + cb.text = i.capitalize() + cb.set_meta(&"bit", NoOBSWS.Enums.EventSubscription[i]) + #cb.toggled.connect(check_toggled.bind(NoOBSWS.Enums.EventSubscription[i])) + h_flow_container.add_child(cb) + + +func get_subscriptions() -> int: + var res := 0 + for i: CheckBox in h_flow_container.get_children(): + if i.button_pressed: + res |= i.get_meta(&"bit") + return res + + +func set_subscriptions(subscriptions: int) -> void: + for i: CheckBox in h_flow_container.get_children(): + i.button_pressed = subscriptions & i.get_meta(&"bit") == i.get_meta(&"bit") diff --git a/graph_node_renderer/obs_ws_subscriptions_container.tscn b/graph_node_renderer/obs_ws_subscriptions_container.tscn new file mode 100644 index 0000000..21d812d --- /dev/null +++ b/graph_node_renderer/obs_ws_subscriptions_container.tscn @@ -0,0 +1,15 @@ +[gd_scene load_steps=2 format=3 uid="uid://c1y0cg4arcbhw"] + +[ext_resource type="Script" path="res://graph_node_renderer/obs_ws_subscriptions_container.gd" id="1_uvb8c"] + +[node name="OBSWSSubscriptionsContainer" type="PanelContainer"] +offset_right = 454.0 +offset_bottom = 194.0 +size_flags_vertical = 3 +script = ExtResource("1_uvb8c") + +[node name="HFlowContainer" type="HFlowContainer" parent="."] +unique_name_in_owner = true +layout_mode = 2 +size_flags_vertical = 3 +alignment = 1