summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJoe Rayhawk <jrayhawk@fairlystable.org>2022-10-28 11:05:31 -0700
committerJoe Rayhawk <jrayhawk@fairlystable.org>2022-10-28 11:05:31 -0700
commit0a35d33f974f0d1f81e6d4cc2d718a5ef81237b0 (patch)
treed6f0c89e310bc327dd84687913845e872bc0fd43 /src
downloadcrystal-obs-websocket-5.0.1.20201028.alpha1.tar.gz
crystal-obs-websocket-5.0.1.20201028.alpha1.zip
Crystal OBS-WebSocket object relational mapping and control interface initial commit.v5.0.1.20201028.alpha1
Diffstat (limited to 'src')
-rw-r--r--src/obswebsocket.cr923
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