add more OBS nodes, allow specifying event subscription types in OBS connection dialog (#81)

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é <yagich@poto.cafe>
Co-committed-by: Lera Elvoé <yagich@poto.cafe>
This commit is contained in:
Lera Elvoé 2024-02-26 11:37:57 +00:00 committed by yagich
parent 2d5fcd25f6
commit 49b8a23281
12 changed files with 358 additions and 16 deletions

View file

@ -4,6 +4,8 @@ class_name NoOBSWS
const Authenticator := preload("res://addons/no-obs-ws/Authenticator.gd") const Authenticator := preload("res://addons/no-obs-ws/Authenticator.gd")
const Enums := preload("res://addons/no-obs-ws/Utility/Enums.gd") const Enums := preload("res://addons/no-obs-ws/Utility/Enums.gd")
@export var subscriptions: Enums.EventSubscription = Enums.EventSubscription.ALL
var _ws: WebSocketPeer var _ws: WebSocketPeer
# {request_id: RequestResponse} # {request_id: RequestResponse}
var _requests: Dictionary = {} var _requests: Dictionary = {}
@ -109,6 +111,7 @@ func _handle_message(message: Message) -> void:
else: else:
var m = Message.new() var m = Message.new()
m.op_code = Enums.WebSocketOpCode.IDENTIFY m.op_code = Enums.WebSocketOpCode.IDENTIFY
m._d["event_subscriptions"] = subscriptions
_send_message(m) _send_message(m)
Enums.WebSocketOpCode.IDENTIFIED: Enums.WebSocketOpCode.IDENTIFIED:
@ -165,6 +168,7 @@ func _authenticate(message: Message, password: String) -> void:
var m = Message.new() var m = Message.new()
m.op_code = Enums.WebSocketOpCode.IDENTIFY m.op_code = Enums.WebSocketOpCode.IDENTIFY
m._d["authentication"] = auth_string m._d["authentication"] = auth_string
m._d["event_subscriptions"] = subscriptions
#print("MY RESPONSE: ") #print("MY RESPONSE: ")
#print(m) #print(m)
_send_message(m) _send_message(m)

View file

@ -14,7 +14,11 @@ static func _twitch_eventsub_event_received(event_data : Dictionary):
static func _twitch_chat_received(msg_dict : Dictionary): static func _twitch_chat_received(msg_dict : Dictionary):
DeckHolder.send_event(&"twitch_chat", msg_dict) 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 ## Temporary Function for loading generic Connection credentials from user:// as a [CredSaver] Resource
static func load_credentials(service : String): static func load_credentials(service : String):

View file

@ -47,7 +47,10 @@ func parse(input: Dictionary) -> Variant:
func _value_request(_from_port : int) -> Variant: func _value_request(_from_port : int) -> Variant:
var input = await request_value_async(0) 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: func _receive(_on_input_port: int, data: Variant) -> void:

View file

@ -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

View file

@ -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)

View file

@ -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,
}
)

View file

@ -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()

View file

@ -347,9 +347,15 @@ func _on_obs_websocket_setup_dialog_connect_button_pressed(state: OBSWebsocketSe
match state: match state:
OBSWebsocketSetupDialog.ConnectionState.DISCONNECTED: OBSWebsocketSetupDialog.ConnectionState.DISCONNECTED:
obs_setup_dialog.set_button_state(OBSWebsocketSetupDialog.ConnectionState.CONNECTING) 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()) no_obsws.connect_to_obsws(obs_setup_dialog.get_port(), obs_setup_dialog.get_password())
await no_obsws.connection_ready await no_obsws.connection_ready
obs_setup_dialog.set_button_state(OBSWebsocketSetupDialog.ConnectionState.CONNECTED) 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: func _process(delta: float) -> void:

View file

@ -7,6 +7,7 @@ class_name OBSWebsocketSetupDialog
@onready var port_spin_box: SpinBox = %PortSpinBox @onready var port_spin_box: SpinBox = %PortSpinBox
@onready var password_line_edit: LineEdit = %PasswordLineEdit @onready var password_line_edit: LineEdit = %PasswordLineEdit
@onready var connect_button: Button = %ConnectButton @onready var connect_button: Button = %ConnectButton
@onready var obsws_subscriptions_container: PanelContainer = $VBoxContainer/OBSWSSubscriptionsContainer
signal connect_button_pressed(state: ConnectionState) signal connect_button_pressed(state: ConnectionState)
@ -20,7 +21,7 @@ var state: ConnectionState
@warning_ignore("narrowing_conversion") @warning_ignore("narrowing_conversion")
@onready var _old_port: int = port_spin_box.value @onready var _old_port: int = port_spin_box.value
@onready var _old_password: String = password_line_edit.text @onready var _old_password: String = password_line_edit.text
@onready var _old_subscriptions: int = obsws_subscriptions_container.get_subscriptions()
func get_port() -> int: func get_port() -> int:
@ -41,19 +42,24 @@ func set_button_state(p_state: ConnectionState) -> void:
ConnectionState.CONNECTING: ConnectionState.CONNECTING:
connect_button.text = "Connecting..." connect_button.text = "Connecting..."
ConnectionState.CONNECTED: 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" connect_button.text = "Disconnect"
func _ready() -> void: func _ready() -> void:
var loaded_creds = Connections.load_credentials("OBS") var loaded_creds = Connections.load_credentials("OBS")
if loaded_creds != null: if loaded_creds != null:
password_line_edit.text = loaded_creds.data.password password_line_edit.text = loaded_creds.data.password
port_spin_box.value = loaded_creds.data.port 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( connect_button.pressed.connect(
func(): func():
@ -74,3 +80,7 @@ func _ready() -> void:
_old_port = int(port_spin_box.value) _old_port = int(port_spin_box.value)
_old_password = password_line_edit.text _old_password = password_line_edit.text
) )
func get_subscriptions() -> int:
return obsws_subscriptions_container.get_subscriptions()

View file

@ -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="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"] [node name="OBSWebsocketSetupDialog" type="ConfirmationDialog"]
title = "OBS Websocket Setup" title = "OBS Websocket Setup"
@ -38,14 +39,21 @@ layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
secret = true secret = true
[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer"] [node name="ToggleSubscriptionsButton" type="Button" 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"]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 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" text = "Connect"
[connection signal="toggled" from="VBoxContainer/ToggleSubscriptionsButton" to="VBoxContainer/OBSWSSubscriptionsContainer" method="set_visible"]

View file

@ -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")

View file

@ -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