local base_url = 'http://localhost:8899' -- Maximum amount of time an entire request may take local request_timeout = 3 -- Maximum time a single socket operation may block local socket_timeout = 0.01 -- How long to give MA3 back control between operations local yield_time = 0.01 local socket = require("socket") local json = require("json") local columns = {} ---------------------------------------------------------------- -- UI helpers ---------------------------------------------------------------- local function show_message(title, message) MessageBox({ title = title, message = message, commands = { { value = 1, name = "Ok" }, }, }) end local function show_error(message) ErrEcho("Resolume: " .. tostring(message)) show_message("Resolume Error", tostring(message)) end ---------------------------------------------------------------- -- Utility ---------------------------------------------------------------- local function yield() coroutine.yield(yield_time) end local function parse_url(url) local host, port, path host, port, path = url:match( "^http://([^:/]+):(%d+)(/.*)$" ) if not host then host, path = url:match( "^http://([^/]+)(/.*)$" ) if host then port = 80 end end if not host then host = url:match("^http://([^/]+)$") if host then port = 80 path = "/" end end if not host then return nil end return host, tonumber(port), path end ---------------------------------------------------------------- -- HTTP ---------------------------------------------------------------- local function http_request(url, method, body) local host, port, path = parse_url(url) if not host then show_error("Invalid URL: " .. tostring(url)) return nil end local start_time = os.clock() ------------------------------------------------------------ -- Create socket ------------------------------------------------------------ local tcp, err = socket.tcp() if not tcp then show_error("Could not create socket: " .. tostring(err)) return nil end -- Each individual socket operation can block for at most -- socket_timeout seconds. tcp:settimeout(socket_timeout) ------------------------------------------------------------ -- Connect ------------------------------------------------------------ local connected = false while not connected do local ok, connect_err = tcp:connect(host, port) if ok then connected = true elseif connect_err == "timeout" then -- Connection is still in progress. -- Give MA3 some time before trying again. yield() else tcp:close() show_error( "Connection failed: " .. tostring(connect_err) ) return nil end if os.clock() - start_time >= request_timeout then tcp:close() show_error("Connection timeout") return nil end end ------------------------------------------------------------ -- Construct HTTP request ------------------------------------------------------------ local request = method .. " " .. path .. " HTTP/1.1\r\n" .. "Host: " .. host .. ":" .. tostring(port) .. "\r\n" .. "Connection: close\r\n" if body then request = request .. "Content-Type: application/json\r\n" .. "Content-Length: " .. tostring(#body) .. "\r\n" end request = request .. "\r\n" if body then request = request .. body end ------------------------------------------------------------ -- Send ------------------------------------------------------------ local send_pos = 1 while send_pos <= #request do local sent, send_err, last_pos = tcp:send(request, send_pos) if sent then send_pos = sent + 1 elseif send_err == "timeout" then if last_pos then send_pos = last_pos + 1 end yield() else tcp:close() show_error( "Send failed: " .. tostring(send_err) ) return nil end if os.clock() - start_time >= request_timeout then tcp:close() show_error("Send timeout") return nil end end ------------------------------------------------------------ -- Receive ------------------------------------------------------------ local response = {} while true do local data, recv_err, partial = tcp:receive(4096) if data then response[#response + 1] = data end if partial then response[#response + 1] = partial end if recv_err == "closed" then break elseif recv_err == "timeout" then -- Nothing available yet. yield() elseif recv_err then tcp:close() show_error( "Receive failed: " .. tostring(recv_err) ) return nil end if os.clock() - start_time >= request_timeout then tcp:close() show_error("HTTP request timeout") return nil end end tcp:close() ------------------------------------------------------------ -- Parse HTTP response ------------------------------------------------------------ local raw_response = table.concat(response) local header_end = raw_response:find("\r\n\r\n", 1, true) if not header_end then show_error("Invalid HTTP response") return nil end local headers = raw_response:sub(1, header_end - 1) local response_body = raw_response:sub(header_end + 4) local status_code = tonumber( headers:match( "^HTTP/%d%.%d%s+(%d+)" ) ) if not status_code then show_error("Could not determine HTTP status") return nil end if status_code < 200 or status_code >= 300 then show_error( "HTTP error " .. tostring(status_code) ) return nil end return response_body end ---------------------------------------------------------------- -- JSON HTTP helpers ---------------------------------------------------------------- local function http_request_json(url, method, body) local response_body = http_request(url, method, body) if not response_body then return nil end local ok, response = pcall( json.decode, response_body ) if not ok or type(response) ~= "table" then show_error( "JSON error decoding: " .. tostring(response_body) ) return nil end return response end local function http_request_void(url, method) return http_request(url, method) end ---------------------------------------------------------------- -- Resolume ---------------------------------------------------------------- local function update_columns() -- Echo("Resolume: fetching composition...") local comp = http_request_json( base_url .. "/api/v1/composition", "GET" ) if not comp then return end if not comp.columns then show_error( "Resolume response contains no columns" ) return end columns = {} for _, column in ipairs(comp.columns) do if column.name and column.name.value then columns[column.name.value] = column.id end end -- Echo( -- "Resolume columns: " .. -- json.encode(columns) -- ) end local function connect_column(name) local column_id = columns[name] if not column_id then show_error( "Unknown column: " .. tostring(name) ) return end http_request_void( base_url .. "/api/v1/composition/columns/by-id/" .. tostring(column_id) .. "/connect", "POST" ) end ---------------------------------------------------------------- -- Plugin entry point ---------------------------------------------------------------- local function main(_display, rawArgs) if rawArgs == nil or rawArgs == "" then while true do update_columns() coroutine.yield(5) end return end local columnName = rawArgs if not columnName then show_error("No column specified") return end connect_column(columnName) end update_columns() return main