diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/obswebsocket.cr | 923 |
1 files changed, 923 insertions, 0 deletions
diff --git a/src/obswebsocket.cr b/src/obswebsocket.cr new file mode 100644 index 0000000..3978b47 --- /dev/null +++ b/src/obswebsocket.cr @@ -0,0 +1,923 @@ +require "json" +require "pretty_print" +require "http" +require "uuid" + +struct Nil + def as_s? + self + end +end + +module OBSWebSocket + extend self + def req( type : String, id : String, data : ( String | Nil | JSON::Any ) = nil ) + request = JSON.build do |json| + json.object do + json.field "op", 6 + json.field "d" do + json.object do + json.field "requestType", type + json.field "requestId", id + if data + json.field "requestData", data + end + end + end + end + end + return( request.to_s ) + end + + class SourceMissingException < Exception + end + + class ResponseDataMissingException < Exception + end + + class Server + @@reqchan = Channel( Tuple( ( Channel( JSON::Any ) | Nil ), String ) ).new + @scenecollection = OBSWebSocket::SceneCollection.new( @@reqchan ) + @@inputs = OBSWebSocket::Inputs.new( @@reqchan ) + @@scenes = OBSWebSocket::SceneList.new( @@reqchan, @@inputs ) + @sources = OBSWebSocket::Sources.new( @@inputs, @@scenes ) + @outputs = OBSWebSocket::Outputs.new( @@reqchan ) + @video = OBSWebSocket::VideoSettings.new( @@reqchan ) + @stats = OBSWebSocket::Stats.new( @@reqchan ) + @negotiated = false + def reqchan + return @@reqchan + end + def scenecollection + return @scenecollection + end + def scenes + return @@scenes + end + def inputs + return @@inputs + end + def outputs + return @outputs + end + def sources + return @sources + end + def video + return @video + end + def stats + return @stats + end + def transition! + reschan = Channel( JSON::Any ).new + @@reqchan.send( { reschan, OBSWebSocket.req( "TriggerStudioModeTransition", UUID.random.to_s ) } ) + return reschan.receive + end + # request channel, response channel + def send( json : String ) + self.reqchan.send( { nil, json } ) + end + def send( reschan : ( Channel( JSON::Any ) | Nil ), json : String ) + self.reqchan.send( { reschan, json } ) + end + def request( type : String, id : String, data : ( String | Nil | JSON::Any ) = nil ) + request = JSON.build do |json| + json.object do + json.field "op", 6 + json.field "d" do + json.object do + json.field "requestType", type + json.field "requestId", id + if data + json.field "requestData", data + end + end + end + end + end + self.reqchan.send( { nil, request.to_s } ) + end + def request( reschan : ( Channel( JSON::Any ) | Nil ), type : String, id : String, data : ( String | Nil | JSON::Any ) = nil ) + request = JSON.build do |json| + json.object do + json.field "op", 6 + json.field "d" do + json.object do + json.field "requestType", type + json.field "requestId", id + if data + json.field "requestData", data + end + end + end + end + end + self.reqchan.send( { reschan, request.to_s } ) + end + def initialize( uri : String ) + spawn do + loop do + obs5_pubsub = HTTP::WebSocket.new( URI.parse( uri ), HTTP::Headers{"Cookie" => "SESSIONID=1235", "Sec-WebSocket-Protocol" => "obswebsocket.json"} ) + openrequests = Hash( String, Channel( JSON::Any ) ).new + eventsubs = Array( Channel(JSON::Any) ).new + #metachan = Channel( Tuple( String, Channel( JSON::Any ) ) ).new + spawn do + queue = Array( Tuple( Channel( JSON::Any ) | Nil, String ) ).new + while msgtuple = @@reqchan.receive + if msgtuple[1] == "clear queue" + queue.each do | msg | + reschan = msg[0] + print( "SENT: " ) + json = JSON.parse(msg[1]) + pp json + if reschan + openrequests[ json["d"]["requestId"].as_s ] = reschan + end + obs5_pubsub.send( msg[1] ) + end + elsif msgtuple[1] == "subscribe events" + reschan = msgtuple[0] + if reschan + eventsubs.push(reschan) + end + elsif @negotiated + reschan = msgtuple[0] + print "SENT: " + json = JSON.parse( msgtuple[1] ) + pp json + if reschan + openrequests[ json["d"]["requestId"].as_s ] = reschan + end + obs5_pubsub.send( msgtuple[1] ) + elsif JSON.parse( msgtuple[1] )["op"].as_i64 == 1 + print "SENT: " + json = JSON.parse( msgtuple[1] ) + pp json + obs5_pubsub.send( msgtuple[1] ) + else + print "QUEUED: " + pp msgtuple[1] + queue.push( { msgtuple[0], msgtuple[1] } ) + end + end + end + obs5_pubsub.on_message do | message | + json = JSON.parse( message ) + print( "RECEIVED: ") + print( json.to_pretty_json ) + print( "\n") + if json["error"]? + puts json["error"] + #ircipc.send( { "#" + settings["reqchan"], "| obs5: #{json["error"]}" } ) + next + end + case json["op"].as_i64 + when 0 # hello + json_text = %({"op":1,"d":{"rpcVersion":1,"eventSubscriptions":526335}}) + self.send( json_text ) + when 2 # identified + @negotiated = true + self.reqchan.send( { nil, "clear queue" } ) + when 5 # event + eventsubs.each do | eventchan | + eventchan.send( json["d"] ) + end + case json["d"]["eventType"].as_s + # Note: "spawn do" when more request callouts might happen + when "CurrentPreviewSceneChanged" + edata = json["d"]["eventData"] + @@scenes.previewcache( edata["sceneName"].as_s ) + when "CurrentProgramSceneChanged" + edata = json["d"]["eventData"] + @@scenes.programcache( edata["sceneName"].as_s ) + when "InputCreated" + edata = json["d"]["eventData"] + @@inputs[edata["inputName"].to_s] = OBSWebSocket::Input.new( @@reqchan, edata ) + when "InputNameChanged" + edata = json["d"]["eventData"] + @@inputs.renamecache( edata["oldInputName"].as_s, edata["inputName"].as_s ) + when "InputRemoved" + edata = json["d"]["eventData"] + @@inputs.deletecache( edata["inputName"].as_s ) + when "SceneItemRemoved" + spawn do + edata = json["d"]["eventData"] + @@scenes[edata["sceneName"].to_s].deletecache( edata["sceneItemId"].as_i64, edata["sourceName"].as_s ) + end + when "SceneItemEnableStateChanged" + spawn do + edata = json["d"]["eventData"] + @@scenes[edata["sceneName"].as_s][edata["sceneItemId"].as_i64].enabledcache( edata["sceneItemEnabled"].as_bool ) + end + when "SceneItemTransformChanged" + spawn do + edata = json["d"]["eventData"] + scenes[edata["sceneName"].as_s][edata["sceneItemId"].as_i64].transform.cache( edata["sceneItemTransform"] ) + end + when "SourceDestroyed" + # We always dynamically assemble sources from inputs and scenes, so this event is ignored. + when "SourceFilterEnableStateChanged" + edata = json["d"]["eventData"] + spawn do + if ( source = @sources[edata["sourceName"].as_s]? ) + puts "SourceFilterEnableStateChanged: #{source.class}" + source.filters[edata["filterName"].as_s].enabledcache( edata["filterEnabled"].as_bool ) + else + raise SourceMissingException.new( "SourceFilterEnableStateChanged: missing source #{ edata["sourceName"].as_s}" ) + end + end + end + when 7 # response + if json["d"]["requestStatus"]["result"].as_bool == false + puts "ERROR: #{json["d"]["requestType"].as_s} #{json["d"]["requestStatus"]["code"].as_i64}: #{json["d"]["requestStatus"]["comment"].as_s}" + end + if openrequests[ json["d"]["requestId"].as_s ]? + channel = openrequests[ json["d"]["requestId"].as_s ] + channel.send( json["d"] ) + channel.close + openrequests.delete( json["d"]["requestId"].as_s ) + else + puts "PUBSUBREAD THREAD: no response channel known for requestId: #{ json["d"]["requestId"].as_s }" + end + end + end + obs5_pubsub.run + sleep 10 + next + end + end + end + end + + class Outputs + @outputs = Hash( String, OBSWebSocket::Output ).new + def initialize( @reqchan : Channel( Tuple( Channel( JSON::Any ) | Nil, String ) ) ) + end + def populate + reschan = Channel( JSON::Any ).new + @reqchan.send( { reschan, OBSWebSocket.req( "GetOutputList", UUID.random.to_s ) } ) + d = reschan.receive + if ( rdata = d["responseData"]? ) + print "OUTPUTS THREAD: " + pp rdata + rdata["outputs"].as_a.each do | output | + @outputs[output["outputName"].as_s] = OBSWebSocket::Output.new( @reqchan, output ) + end + else + raise ResponseDataMissingException.new( "#{d["requestType"].as_s} #{d["requestStatus"]["code"].as_i64}: #{d["requestStatus"]["comment"].as_s}" ) + end + end + def []( key : String ) + @outputs[key]? || populate + @outputs[key] + end + def to_h + @outputs.empty? && populate + @outputs + end + end + + class Output + getter status : OBSWebSocket::OutputStatus + getter flags : Hash( String, Bool ) + getter state : Hash( String, String | Bool | Int64 | Float64 | Hash( String, Bool ) ) + def initialize( @reqchan : Channel( Tuple( Channel( JSON::Any ) | Nil, String ) ), json : JSON::Any ) + @flags = Hash( String, Bool ).from_json(json["outputFlags"].to_json) + @status = OBSWebSocket::OutputStatus.new( @reqchan, json["outputName"].as_s ) + @state = Hash(String, String | Bool | Int64 | Float64 | Hash( String, Bool ) ).from_json(json.to_json) + end + end + + class OutputStatus + @age = Int64.new( 0 ) + @status = Hash(String, String | Bool | Int64 | Float64 ).new + getter name : String + def initialize( @reqchan : Channel( Tuple( Channel( JSON::Any ) | Nil, String ) ), @name : String ) + end + def populate + reschan = Channel( JSON::Any ).new + @reqchan.send( { reschan, OBSWebSocket.req( "GetOutputStatus", UUID.random.to_s, JSON.parse( { "outputName" => @name }.to_json ) ) } ) + d = reschan.receive + if ( rdata = d["responseData"]? ) + @age = Time.utc.to_unix + @status = Hash(String, String | Bool | Int64 | Float64 ).from_json(rdata.to_json) + else + raise ResponseDataMissingException.new( "#{d["requestType"].as_s} #{d["requestStatus"]["code"].as_i64}: #{d["requestStatus"]["comment"].as_s}" ) + end + end + def []( key : String ) + ( @age < ( Time.utc.to_unix - 10 ) ) && populate + @status[key]? || populate + @status[key] + end + def to_h : Hash(String, String | Bool | Int64 | Float64 ) + ( @age < ( Time.utc.to_unix - 10 ) ) && populate + @status.empty? && populate + @status + end + end + + class VideoSettings + @settings = Hash(String, String | Bool | Int64 | Float64 ).new + def initialize( @reqchan : Channel( Tuple( Channel( JSON::Any ) | Nil, String ) ) ) + end + def populate + reschan = Channel( JSON::Any ).new + @reqchan.send( { reschan, OBSWebSocket.req( "GetVideoSettings", UUID.random.to_s ) } ) + d = reschan.receive + if ( rdata = d["responseData"]? ) + @settings = Hash(String, String | Bool | Int64 | Float64 ).from_json(rdata.to_json) + else + raise ResponseDataMissingException.new( "#{d["requestType"].as_s} #{d["requestStatus"]["code"].as_i64}: #{d["requestStatus"]["comment"].as_s}" ) + end + end + def settings + @settings.empty? && populate + @settings + end + def []( key : String ) + @settings[key]? || populate + @settings[key] + end + def to_h + @settings.empty? && populate + @settings + end + end + + class Stats + @stats = Hash(String, String | Bool | Int64 | Float64 ).new + @age = Int64.new( 0 ) + def initialize( @reqchan : Channel( Tuple( Channel( JSON::Any ) | Nil, String ) ) ) + end + def populate + reschan = Channel( JSON::Any ).new + @reqchan.send( { reschan, OBSWebSocket.req( "GetStats", UUID.random.to_s ) } ) + d = reschan.receive + if ( rdata = d["responseData"]? ) + @stats = Hash(String, String | Bool | Int64 | Float64 ).from_json(rdata.to_json) + @age = Time.utc.to_unix + else + raise ResponseDataMissingException.new( "#{d["requestType"].as_s} #{d["requestStatus"]["code"].as_i64}: #{d["requestStatus"]["comment"].as_s}" ) + end + end + def []( key : String ) + ( @age < ( Time.utc.to_unix - 10 ) ) && populate + @stats[key]? || populate + @stats[key] + end + def to_h : Hash(String, String | Bool | Int64 | Float64 ) + @stats.empty? && populate + @stats + end + end + + class SceneCollection # Note: distinct from SceneList + @current = String.new + @list = Hash( String, OBSWebSocket::SceneList ).new + @reqchan = Channel( Tuple( Channel( JSON::Any ) | Nil, String ) ).new + def initialize( @reqchan : Channel( Tuple( Channel( JSON::Any ) | Nil, String ) ) ) + end + def populate + reschan = Channel( JSON::Any ).new + @reqchan.send( { reschan, OBSWebSocket.req( "GetSceneCollectionList", UUID.random.to_s ) } ) + d = reschan.receive + if ( rdata = d["responseData"]? ) + @current = rdata["currentSceneCollectionName"].as_s + rdata["sceneCollections"].as_a.each{ | scenecollection | + @list[scenecollection.as_s] = OBSWebSocket::SceneList.new( @reqchan ) + } + else + raise ResponseDataMissingException.new( "#{d["requestType"].as_s} #{d["requestStatus"]["code"].as_i64}: #{d["requestStatus"]["comment"].as_s}" ) + end + return true + end + def current() + @current.empty? && populate() + return @current + end + def []( scenelist : String ) + if list[scenelist]? + return list[scenelist] + else + self.populate + end + end + end + + class SceneList # Note: distinct from SceneCollection + @scenes_by_index = Array(String).new + @scenes = Hash(String, OBSWebSocket::Scene).new + @program = String.new + @preview = String.new + def initialize( @reqchan : Channel( Tuple( Channel( JSON::Any ) | Nil, String ) ), @inputs : OBSWebSocket::Inputs ) + end + def populate + reschan = Channel( JSON::Any ).new + @reqchan.send( { reschan, OBSWebSocket.req( "GetSceneList", UUID.random.to_s ) } ) + d = reschan.receive + if ( rdata = d["responseData"]? ) + @program = rdata["currentProgramSceneName"].as_s + @preview = rdata["currentPreviewSceneName"].as_s + rdata["scenes"].as_a.each_with_index{ | scene, i | + name = scene["sceneName"].as_s + @scenes_by_index.push(name) + @scenes[name] = OBSWebSocket::Scene.new( @reqchan, name, self ) + } + else + raise ResponseDataMissingException.new( "#{d["requestType"].as_s} #{d["requestStatus"]["code"].as_i64}: #{d["requestStatus"]["comment"].as_s}" ) + end + return true + end + def invalidate + puts "SCENELIST THREAD: scenes invalidated" + @scenes_by_index = Array(String).new + @scenes = Hash(String, OBSWebSocket::Scene).new + @program = String.new + @preview = String.new + end + def programcache( v : String ) + @program = v + end + def program + @program.empty? && populate() + @scenes[@program] + end + def previewcache( v : String ) + @preview = v + end + def preview + @preview.empty? && populate() + @scenes[@preview] + end + def current + @program.empty? && populate + @scenes[@program] + end + def []( index : Int ) : OBSWebSocket::Scene + @scenes_by_index[index]? || populate + @scenes[@scenes_by_index[index]]? || populate + @scenes[@scenes_by_index[index]] + end + def []( index : String ) : OBSWebSocket::Scene + @scenes[index]? || populate + @scenes[index] + end + def []?( index : String ) : OBSWebSocket::Scene | Nil + @scenes[index]? || populate + @scenes[index]? + end + def to_h : Hash( String, OBSWebSocket::Scene ) + @scenes.empty? && populate + return @scenes + end + end + + class Scene + @items_by_id = Hash( Int64, OBSWebSocket::SceneItem ).new + @items_by_name = Hash( String, Int64 ).new + @items_by_index = Array( Int64 ).new + @subscenes = Array( OBSWebSocket::Scene ).new + getter filters : OBSWebSocket::SourceFilters + getter name : String + getter parent : OBSWebSocket::SceneList # needed for reverse metasceneitem lookup + @age = Int64.new( 0 ) + def initialize( @reqchan : Channel( Tuple( Channel( JSON::Any ) | Nil, String ) ), @name : String, @parent : OBSWebSocket::SceneList ) + @filters = OBSWebSocket::SourceFilters.new( @reqchan, @name ) + end + def populate + reschan = Channel( JSON::Any ).new + @reqchan.send( { reschan, OBSWebSocket.req( "GetSceneItemList", UUID.random.to_s, JSON.parse( { "sceneName" => @name }.to_json ) ) } ) + d = reschan.receive + if ( rdata = d["responseData"]? ) + # handle "sourceType": "OBS_SOURCE_TYPE_SCENE" + rdata["sceneItems"].as_a.each_with_index{ | item, i | + sname = item["sourceName"].as_s + siid = item["sceneItemId"].as_i64 + @items_by_id[siid] = OBSWebSocket::SceneItem.new( @reqchan, @name, item ) + @items_by_index.push( siid ) + @items_by_name[sname] = siid + if item["sourceType"].as_s == "OBS_SOURCE_TYPE_SCENE" + @subscenes.push( @parent[sname] ) + end + } + @age = Time.utc.to_unix + else + raise ResponseDataMissingException.new( "#{d["requestType"].as_s} #{d["requestStatus"]["code"].as_i64}: #{d["requestStatus"]["comment"].as_s}" ) + end + return true + end + # Sources may be present a scene via other scenes. + # We can still look these up by source name. + # FIXME: does not recurse + # Note: OBS refuses to create a referential loop + # FIXME: only returns one SceneItem + def metascene : Hash( String, OBSWebSocket::SceneItem ) + @subscenes.empty? && populate + metascene = Hash( String, OBSWebSocket::SceneItem ).new + @subscenes.each do | subscene | + metascene.merge!(subscene.to_h) + end + @items_by_id.values.each do | sitem | + metascene[ sitem.name ] = sitem + end + return metascene + end + # less requests generated with this method + # but Nil union is more annoying + # FIXME: See metascene() + def metasceneitem?( name : String ) : OBSWebSocket::SceneItem | Nil + result = nil + if self[name]? + result = self[name] + else + @subscenes.each do | scene | + if scene[name]? + result = scene[name] + break + end + end + end + return result + end + def []?( name : String ) : OBSWebSocket::SceneItem | Nil + ( @items_by_name[name]? || ( @age > 0 ) ) || populate + if @items_by_name[name]? + ( @items_by_id[ @items_by_name[name] ]? || ( @age > 0 ) ) || populate + return @items_by_id[ @items_by_name[name] ] + else + return nil + end + end + def []( name : String ) : OBSWebSocket::SceneItem + @items_by_name[name]? || populate + @items_by_id[ @items_by_name[name] ]? || populate + return @items_by_id[ @items_by_name[name] ] + end + def []( id : Int64 ) : OBSWebSocket::SceneItem + @items_by_id[id]? || populate + return @items_by_id[id] + end + def by_index( index : Int64 ) + @items_by_index[index]? || populate + @items_by_id[ @items_by_index[index] ]? || populate + return @items_by_id[ @items_by_index[index] ] + end + def to_h : Hash( String, OBSWebSocket::SceneItem ) + @items_by_id.empty? && populate + hash = Hash( String, OBSWebSocket::SceneItem ).new + @items_by_name.each do | key, value | + hash[key] = @items_by_id[value] + end + return hash + end + def current! + program! + end + def program! + @reqchan.send( { nil, OBSWebSocket.req( "SetCurrentProgramScene", UUID.random.to_s, JSON.parse( { "sceneName" => @name }.to_json ) ) } ) + end + def preview! + @reqchan.send( { nil, OBSWebSocket.req( "SetCurrentPreviewScene", UUID.random.to_s, JSON.parse( { "sceneName" => @name }.to_json ) ) } ) + end + def createinput( iname : String, kind : String, settings : OBSWebSocket::InputSettings | Hash( String, String | Bool | Int64 | Float64 ) | Nil = nil, enabled : Bool = true ) + reqdata = ( Hash( String, String | Bool | Int64 | Float64 | Hash( String, String | Bool | Int64 | Float64 ) ) ).new + reqdata["sceneName"] = @name + reqdata["inputName"] = iname + reqdata["inputKind"] = kind + if settings + reqdata["inputSettings"] = settings.to_h + end + unless enabled + reqdata["sceneItemEnabled"] = false + end + + reschan = Channel( JSON::Any ).new + @reqchan.send( { reschan, OBSWebSocket.req( "CreateInput", UUID.random.to_s, JSON.parse(reqdata.to_json) ) } ) + d = reschan.receive + # maybe block until the event comes in? + # will have to depend on the EventSubscription state + if d["requestStatus"]["result"].as_bool # success? + # "responseData": { "sceneItemId": 219 } + # Is this ever useful? It's a lot more requests than to just redo the entire GetSceneItemList + #siid = d["responseData"]["sceneItemId"].as_i64 + parent.@inputs.deletecache + self.deletecache + end + + end + def deletecache( ) + + @items_by_id = Hash( Int64, OBSWebSocket::SceneItem ).new + @items_by_name = Hash( String, Int64 ).new + @items_by_index = Array( Int64 ).new + @subscenes = Array( OBSWebSocket::Scene ).new + end + def deletecache( siid : Int64, siname : String ) + @items_by_id.delete( siid ) + @items_by_name.delete( siname ) + @items_by_index.delete( siid ) + end + end + + class SceneItem + getter id : Int64 + getter name : String + getter kind : ( String | Nil ) + getter type : String + getter scenename : String + getter enabled : Bool + getter blendmode : ( String | Nil ) + getter locked : Bool + getter transform : OBSWebSocket::SceneItemTransform + def initialize( @reqchan : Channel( Tuple( Channel( JSON::Any ) | Nil, String ) ), @scenename : String, json : JSON::Any ) + @id = json["sceneItemId"].as_i64 + @name = json["sourceName"].as_s + @kind = json["inputKind"]?.as_s? + @type = json["sourceType"].as_s + @locked = json["sceneItemLocked"].as_bool + @enabled = json["sceneItemEnabled"].as_bool + @blendmode = json["sceneItemBlendmode"]?.as_s? + @transform = OBSWebSocket::SceneItemTransform.new( @reqchan, json["sceneItemTransform"], @name, @id ) + end + def enabledcache( v : Bool ) + @enabled = v + end + def enabled + @enabled + end + def toggle! + if @enabled + disable! + else + enable! + end + end + def enable! + @reqchan.send( { nil, OBSWebSocket.req( "SetSceneItemEnabled", UUID.random.to_s, JSON.parse( { "sceneName" => @scenename, "sceneItemId" => @id, "sceneItemEnabled" => true }.to_json ) ) } ) + end + def disable! + @reqchan.send( { nil, OBSWebSocket.req( "SetSceneItemEnabled", UUID.random.to_s, JSON.parse( { "sceneName" => @scenename, "sceneItemId" => @id, "sceneItemEnabled" => false }.to_json ) ) } ) + end + def transform( newtransform : OBSWebSocket::SceneItemTransform | Hash( String, String | Bool | Int64 | Float64 ) ) + reqdata = ( Hash( String, String | Bool | Int64 | Float64 | Hash( String, String | Bool | Int64 | Float64 ) ) ).new + reqdata["sceneItemId"] = @id.to_i64 + reqdata["sceneName"] = @scenename + reqdata["sceneItemTransform"] = Hash( String, String | Bool | Int64 | Float64).new.merge( newtransform.to_h ) + reschan = Channel( JSON::Any ).new + @reqchan.send( { reschan, OBSWebSocket.req( "SetSceneItemTransform", UUID.random.to_s, JSON.parse(reqdata.to_json) ) } ) + d = reschan.receive + end + end + + class SceneItemTransform + @sceneitemtransform = Hash(String, String | Bool | Int64 | Float64 ).new + def initialize( @reqchan : Channel( Tuple( Channel( JSON::Any ) | Nil, String ) ), json : JSON::Any, @sceneitemname : String, @sceneitemid : Int64 ) + @sceneitemtransform = Hash(String, String | Bool | Int64 | Float64 ).from_json(json.to_json) + end + def populate + reschan = Channel( JSON::Any ).new + @reqchan.send( { reschan, OBSWebSocket.req( "GetSceneItemTransform", UUID.random.to_s, JSON.parse( { "sceneName" => @sceneitemname, "sceneItemId" => @sceneitemid }.to_json ) ) } ) + d = reschan.receive + if ( rdata = d["responseData"]? ) + @sceneitemtransform = Hash(String, String | Bool | Int64 | Float64 ).from_json(rdata["sceneItemTransform"].to_json) + else + raise ResponseDataMissingException.new( "#{d["requestType"].as_s} #{d["requestStatus"]["code"].as_i64}: #{d["requestStatus"]["comment"].as_s}" ) + end + end + def []( key : String ) + @sceneitemtransform[key]? || populate + @sceneitemtransform[key] + end + def to_h + @sceneitemtransform.empty? && populate + @sceneitemtransform + end + def cache( newtransform = JSON::Any | Hash(String, String | Bool | Int64 | Float64 ) ) + @sceneitemtransform = Hash(String, String | Bool | Int64 | Float64 ).from_json(newtransform.to_json) + end + end + + class Sources + getter scenes : OBSWebSocket::SceneList + getter inputs : OBSWebSocket::Inputs + def initialize( @inputs : OBSWebSocket::Inputs, @scenes : OBSWebSocket::SceneList ) + end + def []?( index : String ) : OBSWebSocket::Scene | OBSWebSocket::Input | Nil + if @inputs[index]? + return @inputs[index] + elsif @scenes[index]? + return @scenes[index] + else + return nil + end + end + def to_h() : Hash( String, OBSWebSocket::Scene | OBSWebSocket::Input ) + return @inputs.to_h.merge( @scenes.to_h ) + end + end + + class Inputs + @inputs = Hash( String, OBSWebSocket::Input ).new + def initialize( @reqchan : Channel( Tuple( Channel( JSON::Any ) | Nil, String ) ) ) + end + def populate + reschan = Channel( JSON::Any ).new + @reqchan.send( { reschan, OBSWebSocket.req( "GetInputList", UUID.random.to_s) } ) + d = reschan.receive + if ( rdata = d["responseData"]? ) + rdata["inputs"].as_a.each{ | input | + @inputs[input["inputName"].as_s] = OBSWebSocket::Input.new( @reqchan, input ) + } + else + raise ResponseDataMissingException.new( "#{d["requestType"].as_s} #{d["requestStatus"]["code"].as_i64}: #{d["requestStatus"]["comment"].as_s}" ) + end + end + def []?( index : String ) : OBSWebSocket::Input | Nil + @inputs[index]? || populate + return @inputs[index]? + end + def []( index : String ) + @inputs[index]? || populate + return @inputs[index] + end + def []=( index : String, input = OBSWebSocket::Input ) + @inputs[index] = input + end + def to_h : Hash( String, OBSWebSocket::Input ) + @inputs.empty? && populate + return @inputs + end + def renamecache( old : String, new : String ) + @inputs[new] = @inputs[old] + @inputs.delete( old ) + @inputs[new].namecache( new ) + end + def deletecache() + @inputs = Hash( String, OBSWebSocket::Input ).new + end + def deletecache( input : String ) + @inputs.delete( input ) + end + end + + class Input + getter kind : String + getter name : String + getter settings : OBSWebSocket::InputSettings + getter filters : OBSWebSocket::SourceFilters + def initialize( @reqchan : Channel( Tuple( Channel( JSON::Any ) | Nil, String ) ), json : JSON::Any ) + @kind = json["inputKind"].as_s + @name = json["inputName"].as_s + @settings = OBSWebSocket::InputSettings.new( @reqchan, @name ) + @filters = OBSWebSocket::SourceFilters.new( @reqchan, @name ) + end + def delete!( ) + reschan = Channel( JSON::Any ).new + @reqchan.send( { reschan, OBSWebSocket.req( "RemoveInput", UUID.random.to_s, JSON.parse({ "inputName" => @name }.to_json) ) } ) + d = reschan.receive + end + def namecache( name : String ) + @name = name + @settings.namecache( name ) + + @filters.to_h.values.each do | value | + value.sourcenamecache( name ) + end + end + end + + class InputSettings + @inputsettings = Hash(String, String | Bool | Int64 | Float64 ).new + @inputkind = String.new + getter name : String + def initialize( @reqchan : Channel( Tuple( Channel( JSON::Any ) | Nil, String ) ), @name : String ) + end + def initialize( @reqchan : Channel( Tuple( Channel( JSON::Any ) | Nil, String ) ), @name : String, @inputkind : String, @inputsettings : Hash( String, String | Bool | Int64 | Float64 ) ) + end + def populate + reschan = Channel( JSON::Any ).new + @reqchan.send( { reschan, OBSWebSocket.req( "GetInputSettings", UUID.random.to_s, JSON.parse( { "inputName" => @name }.to_json ) ) } ) + d = reschan.receive + if ( rdata = d["responseData"]? ) + @inputsettings = Hash(String, String | Bool | Int64 | Float64 ).from_json(rdata["inputSettings"].to_json) + @inputkind = rdata["inputKind"].as_s + else + raise ResponseDataMissingException.new( "#{d["requestType"].as_s} #{d["requestStatus"]["code"].as_i64}: #{d["requestStatus"]["comment"].as_s}" ) + end + end + def kind + @inputkind.empty? && populate + @inputkind + end + def []( key : String ) + @inputsettings[key]? || populate + @inputsettings[key] + end + def to_h + @inputsettings.empty? && populate + @inputsettings + end + def namecache( name : String) + @name = name + end + end + + class SourceFilters # Note: a property of both OBSWebSocket::Input and OBSWebSocket::Scene + @filters_by_name = Hash( String, OBSWebSocket::SourceFilter ).new + @filters_by_index = Array( String ).new + getter sourcename : String + def initialize( @reqchan : Channel( Tuple( Channel( JSON::Any ) | Nil, String ) ), @sourcename : String ) + end + def populate + reschan = Channel( JSON::Any ).new + @reqchan.send( { reschan, OBSWebSocket.req( "GetSourceFilterList", UUID.random.to_s, JSON.parse( { "sourceName" => @sourcename }.to_json ) ) } ) + d = reschan.receive + if ( rdata = d["responseData"]? ) + rdata["filters"].as_a.each do | filter | + name = filter["filterName"].as_s + @filters_by_index.push( name ) + @filters_by_name[name] = OBSWebSocket::SourceFilter.new( @reqchan, @sourcename, filter ) + end + else + raise ResponseDataMissingException.new( "#{d["requestType"].as_s} #{d["requestStatus"]["code"].as_i64}: #{d["requestStatus"]["comment"].as_s}" ) + end + end + def []( index : Int64 ) + @filters_by_index[index]? || populate + @filters_by_name[@filters_by_index[index]]? || populate + @filters_by_name[@filters_by_index[index]] + end + def []?( key : String ) : OBSWebSocket::SourceFilter | Nil + @filters_by_name[key]? || populate + @filters_by_name[key]? + end + def []( key : String ) + @filters_by_name[key]? || populate + @filters_by_name[key] + end + def to_h + @filters_by_name.empty? && populate + @filters_by_name + end + end + + class SourceFilter + getter name : String + getter kind : String + getter enabled : Bool + getter settings : OBSWebSocket::SourceFilterSettings + getter sourcename : String + def initialize( @reqchan : Channel( Tuple( Channel( JSON::Any ) | Nil, String ) ), @sourcename, json : JSON::Any ) + @kind = json["filterKind"].as_s + @name = json["filterName"].as_s + @enabled = json["filterEnabled"].as_bool + @settings = OBSWebSocket::SourceFilterSettings.new( @reqchan, @name, json["filterSettings"] ) + end + def toggle! + if @enabled + disable! + else + enable! + end + end + def enable! + reschan = Channel( JSON::Any ).new + @reqchan.send( { reschan, OBSWebSocket.req( "SetSourceFilterEnabled", UUID.random.to_s, JSON.parse( { "sourceName" => @sourcename, "filterName" => @name, "filterEnabled" => true }.to_json ) ) } ) + d = reschan.receive + return d["requestStatus"]["result"].as_bool + end + def disable! + reschan = Channel( JSON::Any ).new + @reqchan.send( { reschan, OBSWebSocket.req( "SetSourceFilterEnabled", UUID.random.to_s, JSON.parse( { "sourceName" => @sourcename, "filterName" => @name, "filterEnabled" => false }.to_json ) ) } ) + d = reschan.receive + return d["requestStatus"]["result"].as_bool + end + def enabledcache( bool : Bool ) + @enabled = bool + end + def sourcenamecache( name : String ) + @sourcename = name + end + end + + class SourceFilterSettings + getter settings : Hash(String, String | Bool | Int64 | Float64 ) + getter sourcename : String + def initialize( @reqchan : Channel( Tuple( Channel( JSON::Any ) | Nil, String ) ), @sourcename, json : JSON::Any ) + @settings = Hash(String, String | Bool | Int64 | Float64 ).from_json(json.to_json) + end + # do we need to populate() this somehow? + # SetSourceFilterSettings is available, but + # GetSourceFilterSettings is not. Must use + # GetSourceFilters instead. + def []( key : String ) + @settings[key] + end + def to_h + @settings + end + + end +end |