mirror of
https://codeberg.org/StreamGraph/StreamGraph.git
synced 2024-11-13 19:49:55 +01:00
add no-obs-ws addon
This commit is contained in:
parent
c4e35043df
commit
aa5eff2788
8 changed files with 6297 additions and 0 deletions
16
addons/no-obs-ws/Authenticator.gd
Normal file
16
addons/no-obs-ws/Authenticator.gd
Normal file
|
@ -0,0 +1,16 @@
|
|||
extends RefCounted
|
||||
|
||||
# The result of the authentication string creation process. Use getter for public access.
|
||||
var _auth_string: String:
|
||||
get = get_auth_string
|
||||
|
||||
|
||||
func _init(password: String, challenge: String, salt: String) -> void:
|
||||
var salted := password + salt
|
||||
var base64_secret := Marshalls.raw_to_base64(salted.sha256_buffer())
|
||||
var b64_secret_plus_challenge = base64_secret + challenge
|
||||
_auth_string = Marshalls.raw_to_base64(b64_secret_plus_challenge.sha256_buffer())
|
||||
|
||||
|
||||
func get_auth_string() -> String:
|
||||
return _auth_string
|
312
addons/no-obs-ws/NoOBSWS.gd
Normal file
312
addons/no-obs-ws/NoOBSWS.gd
Normal file
|
@ -0,0 +1,312 @@
|
|||
extends Node
|
||||
class_name NoOBSWS
|
||||
|
||||
const Authenticator := preload("res://addons/no-obs-ws/Authenticator.gd")
|
||||
const Enums := preload("res://addons/no-obs-ws/Utility/Enums.gd")
|
||||
|
||||
var _ws: WebSocketPeer
|
||||
# {request_id: RequestResponse}
|
||||
var _requests: Dictionary = {}
|
||||
var _batch_requests: Dictionary = {}
|
||||
|
||||
const WS_URL := "127.0.0.1:%s"
|
||||
|
||||
signal connection_ready()
|
||||
signal connection_failed()
|
||||
signal connection_closed_clean(code: int, reason: String)
|
||||
|
||||
signal error(message: String)
|
||||
|
||||
signal event_received(event: Message)
|
||||
|
||||
signal _auth_required()
|
||||
|
||||
|
||||
func connect_to_obsws(port: int, password: String = "") -> void:
|
||||
_ws = WebSocketPeer.new()
|
||||
_ws.connect_to_url(WS_URL % port)
|
||||
_auth_required.connect(_authenticate.bind(password))
|
||||
|
||||
|
||||
func make_generic_request(request_type: String, request_data: Dictionary = {}) -> RequestResponse:
|
||||
var response := RequestResponse.new()
|
||||
var message := Message.new()
|
||||
|
||||
var crypto := Crypto.new()
|
||||
var request_id := crypto.generate_random_bytes(16).hex_encode()
|
||||
|
||||
var data := {
|
||||
"request_type": request_type,
|
||||
"request_id": request_id,
|
||||
"request_data": request_data,
|
||||
}
|
||||
message._d.merge(data, true)
|
||||
|
||||
message.op_code = Enums.WebSocketOpCode.REQUEST
|
||||
|
||||
response.id = request_id
|
||||
response.type = request_type
|
||||
|
||||
_requests[request_id] = response
|
||||
|
||||
_send_message(message)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
func make_batch_request(halt_on_failure: bool = false, execution_type: Enums.RequestBatchExecutionType = Enums.RequestBatchExecutionType.SERIAL_REALTIME) -> BatchRequest:
|
||||
var batch_request := BatchRequest.new()
|
||||
|
||||
var crypto := Crypto.new()
|
||||
var request_id := crypto.generate_random_bytes(16).hex_encode()
|
||||
|
||||
batch_request._id = request_id
|
||||
batch_request._send_callback = _send_message
|
||||
|
||||
batch_request.halt_on_failure = halt_on_failure
|
||||
batch_request.execution_type = execution_type
|
||||
|
||||
_batch_requests[request_id] = batch_request
|
||||
|
||||
return batch_request
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if is_instance_valid(_ws):
|
||||
_poll_socket()
|
||||
|
||||
|
||||
func _poll_socket() -> void:
|
||||
_ws.poll()
|
||||
|
||||
var state = _ws.get_ready_state()
|
||||
match state:
|
||||
WebSocketPeer.STATE_OPEN:
|
||||
while _ws.get_available_packet_count():
|
||||
_handle_packet(_ws.get_packet())
|
||||
WebSocketPeer.STATE_CLOSING:
|
||||
pass
|
||||
WebSocketPeer.STATE_CLOSED:
|
||||
if _ws.get_close_code() == -1:
|
||||
connection_failed.emit()
|
||||
else:
|
||||
connection_closed_clean.emit(_ws.get_close_code(), _ws.get_close_reason())
|
||||
_ws = null
|
||||
|
||||
|
||||
func _handle_packet(packet: PackedByteArray) -> void:
|
||||
var message = Message.from_json(packet.get_string_from_utf8())
|
||||
print("got message with code ", message.op_code)
|
||||
_handle_message(message)
|
||||
|
||||
|
||||
func _handle_message(message: Message) -> void:
|
||||
# print(message)
|
||||
match message.op_code:
|
||||
Enums.WebSocketOpCode.HELLO:
|
||||
if message.get("authentication") != null:
|
||||
_auth_required.emit(message)
|
||||
else:
|
||||
var m = Message.new()
|
||||
m.op_code = Enums.WebSocketOpCode.IDENTIFY
|
||||
_send_message(m)
|
||||
|
||||
Enums.WebSocketOpCode.IDENTIFIED:
|
||||
connection_ready.emit()
|
||||
|
||||
Enums.WebSocketOpCode.EVENT:
|
||||
event_received.emit(message)
|
||||
|
||||
Enums.WebSocketOpCode.REQUEST_RESPONSE:
|
||||
print("Req Response")
|
||||
var id = message.get_data().get("request_id")
|
||||
if id == null:
|
||||
error.emit("Received request response, but there was no request id field.")
|
||||
return
|
||||
|
||||
var response = _requests.get(id) as RequestResponse
|
||||
if response == null:
|
||||
error.emit("Received request response, but there was no request made with that id.")
|
||||
return
|
||||
|
||||
response.message = message
|
||||
|
||||
response.response_received.emit()
|
||||
_requests.erase(id)
|
||||
|
||||
Enums.WebSocketOpCode.REQUEST_BATCH_RESPONSE:
|
||||
var id = message.get_data().get("request_id")
|
||||
if id == null:
|
||||
error.emit("Received batch request response, but there was no request id field.")
|
||||
return
|
||||
|
||||
var response = _batch_requests.get(id) as BatchRequest
|
||||
if response == null:
|
||||
error.emit("Received batch request response, but there was no request made with that id.")
|
||||
return
|
||||
|
||||
response.response = message
|
||||
|
||||
response.response_received.emit()
|
||||
_batch_requests.erase(id)
|
||||
|
||||
|
||||
func _send_message(message: Message) -> void:
|
||||
_ws.send_text(message.to_obsws_json())
|
||||
|
||||
|
||||
func _authenticate(message: Message, password: String) -> void:
|
||||
var authenticator = Authenticator.new(
|
||||
password,
|
||||
message.authentication.challenge,
|
||||
message.authentication.salt,
|
||||
)
|
||||
var auth_string = authenticator.get_auth_string()
|
||||
var m = Message.new()
|
||||
m.op_code = Enums.WebSocketOpCode.IDENTIFY
|
||||
m._d["authentication"] = auth_string
|
||||
print("MY RESPONSE: ")
|
||||
print(m)
|
||||
_send_message(m)
|
||||
|
||||
|
||||
class Message:
|
||||
var op_code: int
|
||||
var _d: Dictionary = {"rpc_version": 1}
|
||||
|
||||
func _get(property: StringName):
|
||||
if property in _d:
|
||||
return _d[property]
|
||||
else:
|
||||
return null
|
||||
|
||||
|
||||
func _get_property_list() -> Array:
|
||||
var prop_list = []
|
||||
_d.keys().map(
|
||||
func(x):
|
||||
var d = {
|
||||
"name": x,
|
||||
"type": typeof(_d[x])
|
||||
}
|
||||
prop_list.append(d)
|
||||
)
|
||||
return prop_list
|
||||
|
||||
|
||||
func to_obsws_json() -> String:
|
||||
var data = {
|
||||
"op": op_code,
|
||||
"d": {}
|
||||
}
|
||||
|
||||
data.d = snake_to_camel_recursive(_d)
|
||||
|
||||
return JSON.stringify(data)
|
||||
|
||||
|
||||
func get_data() -> Dictionary:
|
||||
return _d
|
||||
|
||||
|
||||
func _to_string() -> String:
|
||||
return var_to_str(_d)
|
||||
|
||||
|
||||
static func from_json(json: String) -> Message:
|
||||
var ev = Message.new()
|
||||
var dictified = JSON.parse_string(json)
|
||||
|
||||
if dictified == null:
|
||||
return null
|
||||
|
||||
dictified = dictified as Dictionary
|
||||
ev.op_code = dictified.get("op", -1)
|
||||
var data = dictified.get("d", null)
|
||||
if data == null:
|
||||
return null
|
||||
|
||||
data = data as Dictionary
|
||||
ev._d = camel_to_snake_recursive(data)
|
||||
|
||||
return ev
|
||||
|
||||
|
||||
static func camel_to_snake_recursive(d: Dictionary) -> Dictionary:
|
||||
var snaked = {}
|
||||
for prop in d:
|
||||
prop = prop as String
|
||||
if d[prop] is Dictionary:
|
||||
snaked[prop.to_snake_case()] = camel_to_snake_recursive(d[prop])
|
||||
else:
|
||||
snaked[prop.to_snake_case()] = d[prop]
|
||||
return snaked
|
||||
|
||||
|
||||
static func snake_to_camel_recursive(d: Dictionary) -> Dictionary:
|
||||
var cameled = {}
|
||||
for prop in d:
|
||||
prop = prop as String
|
||||
if d[prop] is Dictionary:
|
||||
cameled[prop.to_camel_case()] = snake_to_camel_recursive(d[prop])
|
||||
else:
|
||||
cameled[prop.to_camel_case()] = d[prop]
|
||||
return cameled
|
||||
|
||||
|
||||
class RequestResponse:
|
||||
signal response_received()
|
||||
|
||||
var id: String
|
||||
var type: String
|
||||
var message: Message
|
||||
|
||||
|
||||
class BatchRequest:
|
||||
signal response_received()
|
||||
|
||||
var _id: String
|
||||
var _send_callback: Callable
|
||||
|
||||
var halt_on_failure: bool = false
|
||||
var execution_type: Enums.RequestBatchExecutionType = Enums.RequestBatchExecutionType.SERIAL_REALTIME
|
||||
|
||||
var requests: Array[Message]
|
||||
# {String: int}
|
||||
var request_ids: Dictionary
|
||||
|
||||
var response: Message = null
|
||||
|
||||
func send() -> void:
|
||||
var message = Message.new()
|
||||
message.op_code = Enums.WebSocketOpCode.REQUEST_BATCH
|
||||
message._d["halt_on_failure"] = halt_on_failure
|
||||
message._d["execution_type"] = execution_type
|
||||
message._d["request_id"] = _id
|
||||
message._d["requests"] = []
|
||||
for r in requests:
|
||||
message._d.requests.append(Message.snake_to_camel_recursive(r.get_data()))
|
||||
|
||||
_send_callback.call(message)
|
||||
|
||||
|
||||
func add_request(request_type: String, request_id: String = "", request_data: Dictionary = {}) -> int:
|
||||
var message = Message.new()
|
||||
|
||||
if request_id == "":
|
||||
var crypto := Crypto.new()
|
||||
request_id = crypto.generate_random_bytes(16).hex_encode()
|
||||
|
||||
var data := {
|
||||
"request_type": request_type,
|
||||
"request_id": request_id,
|
||||
"request_data": request_data,
|
||||
}
|
||||
|
||||
message._d.merge(data, true)
|
||||
message.op_code = Enums.WebSocketOpCode.REQUEST
|
||||
|
||||
requests.append(message)
|
||||
request_ids[request_id] = requests.size() - 1
|
||||
|
||||
return request_ids[request_id]
|
64
addons/no-obs-ws/Utility/EnumGen.gd
Normal file
64
addons/no-obs-ws/Utility/EnumGen.gd
Normal file
|
@ -0,0 +1,64 @@
|
|||
static func generate_enums(protocol_json_path: String, output_to_path: String) -> void:
|
||||
var protocol := FileAccess.open(protocol_json_path, FileAccess.READ).get_as_text()
|
||||
var protocol_json: Dictionary = JSON.parse_string(protocol)
|
||||
var res := "# This file is automatically generated, please do not change it. If you wish to edit it, check /addons/deckobsws/Utility/EnumGen.gd\n\n"
|
||||
for e in protocol_json.enums:
|
||||
# if all are deprecated, don't make the enum
|
||||
var deprecated_count: int
|
||||
for enumlet in e.enumIdentifiers:
|
||||
if enumlet.deprecated:
|
||||
deprecated_count += 1
|
||||
|
||||
if deprecated_count == e.enumIdentifiers.size():
|
||||
continue
|
||||
|
||||
res += "enum %s {\n" % e.enumType
|
||||
|
||||
for enumlet in e.enumIdentifiers:
|
||||
var enumlet_value: int
|
||||
|
||||
match typeof(enumlet.enumValue):
|
||||
TYPE_FLOAT:
|
||||
enumlet_value = int(enumlet.enumValue)
|
||||
TYPE_STRING when "<<" in enumlet.enumValue:
|
||||
enumlet_value = tokenize_lbitshift(enumlet.enumValue)
|
||||
TYPE_STRING when "|" in enumlet.enumValue:
|
||||
var token: String = (enumlet.enumValue as String)\
|
||||
.trim_prefix("(")\
|
||||
.trim_suffix(")")
|
||||
var split := Array(token.split("|")).map(
|
||||
func(x: String):
|
||||
return x.strip_edges()
|
||||
)
|
||||
var calc: int
|
||||
for enum_partial in e.enumIdentifiers:
|
||||
if enum_partial.enumIdentifier not in split:
|
||||
continue
|
||||
|
||||
|
||||
calc |= tokenize_lbitshift(enum_partial.enumValue)
|
||||
enumlet_value = calc
|
||||
TYPE_STRING:
|
||||
enumlet_value = int(enumlet.enumValue)
|
||||
|
||||
res += "\t%s = %s,\n" % [
|
||||
(enumlet.enumIdentifier as String).to_snake_case().to_upper(),
|
||||
enumlet_value
|
||||
]
|
||||
|
||||
res += "}\n\n\n"
|
||||
|
||||
var result_file := FileAccess.open(output_to_path, FileAccess.WRITE)
|
||||
result_file.store_string(res.strip_edges() + "\n")
|
||||
|
||||
|
||||
static func tokenize_lbitshift(s: String) -> int:
|
||||
var tokens := Array(s\
|
||||
.trim_prefix("(")\
|
||||
.trim_suffix(")")\
|
||||
.split("<<")).map(
|
||||
func(x: String):
|
||||
return int(x)
|
||||
)
|
||||
|
||||
return tokens[0] << tokens [1]
|
96
addons/no-obs-ws/Utility/Enums.gd
Normal file
96
addons/no-obs-ws/Utility/Enums.gd
Normal file
|
@ -0,0 +1,96 @@
|
|||
# This file is automatically generated, please do not change it. If you wish to edit it, check /addons/deckobsws/Utility/EnumGen.gd
|
||||
|
||||
enum EventSubscription {
|
||||
NONE = 0,
|
||||
GENERAL = 1,
|
||||
CONFIG = 2,
|
||||
SCENES = 4,
|
||||
INPUTS = 8,
|
||||
TRANSITIONS = 16,
|
||||
FILTERS = 32,
|
||||
OUTPUTS = 64,
|
||||
SCENE_ITEMS = 128,
|
||||
MEDIA_INPUTS = 256,
|
||||
VENDORS = 512,
|
||||
UI = 1024,
|
||||
ALL = 2047,
|
||||
INPUT_VOLUME_METERS = 65536,
|
||||
INPUT_ACTIVE_STATE_CHANGED = 131072,
|
||||
INPUT_SHOW_STATE_CHANGED = 262144,
|
||||
SCENE_ITEM_TRANSFORM_CHANGED = 524288,
|
||||
}
|
||||
|
||||
|
||||
enum RequestBatchExecutionType {
|
||||
NONE = -1,
|
||||
SERIAL_REALTIME = 0,
|
||||
SERIAL_FRAME = 1,
|
||||
PARALLEL = 2,
|
||||
}
|
||||
|
||||
|
||||
enum RequestStatus {
|
||||
UNKNOWN = 0,
|
||||
NO_ERROR = 10,
|
||||
SUCCESS = 100,
|
||||
MISSING_REQUEST_TYPE = 203,
|
||||
UNKNOWN_REQUEST_TYPE = 204,
|
||||
GENERIC_ERROR = 205,
|
||||
UNSUPPORTED_REQUEST_BATCH_EXECUTION_TYPE = 206,
|
||||
MISSING_REQUEST_FIELD = 300,
|
||||
MISSING_REQUEST_DATA = 301,
|
||||
INVALID_REQUEST_FIELD = 400,
|
||||
INVALID_REQUEST_FIELD_TYPE = 401,
|
||||
REQUEST_FIELD_OUT_OF_RANGE = 402,
|
||||
REQUEST_FIELD_EMPTY = 403,
|
||||
TOO_MANY_REQUEST_FIELDS = 404,
|
||||
OUTPUT_RUNNING = 500,
|
||||
OUTPUT_NOT_RUNNING = 501,
|
||||
OUTPUT_PAUSED = 502,
|
||||
OUTPUT_NOT_PAUSED = 503,
|
||||
OUTPUT_DISABLED = 504,
|
||||
STUDIO_MODE_ACTIVE = 505,
|
||||
STUDIO_MODE_NOT_ACTIVE = 506,
|
||||
RESOURCE_NOT_FOUND = 600,
|
||||
RESOURCE_ALREADY_EXISTS = 601,
|
||||
INVALID_RESOURCE_TYPE = 602,
|
||||
NOT_ENOUGH_RESOURCES = 603,
|
||||
INVALID_RESOURCE_STATE = 604,
|
||||
INVALID_INPUT_KIND = 605,
|
||||
RESOURCE_NOT_CONFIGURABLE = 606,
|
||||
INVALID_FILTER_KIND = 607,
|
||||
RESOURCE_CREATION_FAILED = 700,
|
||||
RESOURCE_ACTION_FAILED = 701,
|
||||
REQUEST_PROCESSING_FAILED = 702,
|
||||
CANNOT_ACT = 703,
|
||||
}
|
||||
|
||||
|
||||
enum WebSocketCloseCode {
|
||||
DONT_CLOSE = 0,
|
||||
UNKNOWN_REASON = 4000,
|
||||
MESSAGE_DECODE_ERROR = 4002,
|
||||
MISSING_DATA_FIELD = 4003,
|
||||
INVALID_DATA_FIELD_TYPE = 4004,
|
||||
INVALID_DATA_FIELD_VALUE = 4005,
|
||||
UNKNOWN_OP_CODE = 4006,
|
||||
NOT_IDENTIFIED = 4007,
|
||||
ALREADY_IDENTIFIED = 4008,
|
||||
AUTHENTICATION_FAILED = 4009,
|
||||
UNSUPPORTED_RPC_VERSION = 4010,
|
||||
SESSION_INVALIDATED = 4011,
|
||||
UNSUPPORTED_FEATURE = 4012,
|
||||
}
|
||||
|
||||
|
||||
enum WebSocketOpCode {
|
||||
HELLO = 0,
|
||||
IDENTIFY = 1,
|
||||
IDENTIFIED = 2,
|
||||
REIDENTIFY = 3,
|
||||
EVENT = 5,
|
||||
REQUEST = 6,
|
||||
REQUEST_RESPONSE = 7,
|
||||
REQUEST_BATCH = 8,
|
||||
REQUEST_BATCH_RESPONSE = 9,
|
||||
}
|
5786
addons/no-obs-ws/Utility/protocol.json
Normal file
5786
addons/no-obs-ws/Utility/protocol.json
Normal file
File diff suppressed because it is too large
Load diff
7
addons/no-obs-ws/plugin.cfg
Normal file
7
addons/no-obs-ws/plugin.cfg
Normal file
|
@ -0,0 +1,7 @@
|
|||
[plugin]
|
||||
|
||||
name="no-obs-ws"
|
||||
description="An obs-websocket client and translation layer for GDScript"
|
||||
author="Yagich"
|
||||
version="0.1"
|
||||
script="plugin.gd"
|
12
addons/no-obs-ws/plugin.gd
Normal file
12
addons/no-obs-ws/plugin.gd
Normal file
|
@ -0,0 +1,12 @@
|
|||
@tool
|
||||
extends EditorPlugin
|
||||
|
||||
|
||||
func _enter_tree() -> void:
|
||||
# Initialization of the plugin goes here.
|
||||
pass
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
# Clean-up of the plugin goes here.
|
||||
pass
|
|
@ -27,6 +27,10 @@ NodeDB="*res://classes/deck/node_db.gd"
|
|||
|
||||
window/subwindows/embed_subwindows=false
|
||||
|
||||
[editor_plugins]
|
||||
|
||||
enabled=PackedStringArray("res://addons/no-obs-ws/plugin.cfg")
|
||||
|
||||
[input]
|
||||
|
||||
group_nodes={
|
||||
|
|
Loading…
Reference in a new issue