-
-
Notifications
You must be signed in to change notification settings - Fork 40
The GoXLR Utility API
The GoXLR Utility is an 'API Driven' application, meaning all configuration happens via an API. The goxlr-client
binary and Web UI are examples of API clients, and the message format is simple JSON.
There are currently three methods of communicating with the API:
- The Unix Socket
/tmp/goxlr.socket
, or on Windows the Named Pipe@goxlr.socket
which are always present - Simple HTTP Requests via
/api/command
on the embedded web server - Websocket communication via
/api/websocket
on the embedded web server.
Note, that all three interfaces have the same JSON message processing and responses attached to them (so any command will work on all three), however the websocket has an additional JSON Patch mechanism, for real-time updates.
We'll go through each of these, and how to communicate with them.
These should be your primary point of entry to the utility, even if you intend to use the HTTP channels for actual work. They will always be present, and active, so long as the GoXLR Utility is running. Users can turn off the web server component of the Daemon, or change the port, but this socket can be used to determine if that's the case.
The message send / receive format is relatively straight forward:
[Message Length as unsigned 32bit BigEndian Integer][JSON Message]
These sockets are simple send / receive, you send a request, you get a response in the same format. To parse out the JSON response, you would read the length, then use that to read the message.
If you're writing in rust, you can utilize the interprocess
crate, along with the utilities ipc
crate to automatically handle connections and marshaling data. Check out the goxlr-client
for an example on how to do this.
If you're a C# / Other Language user, there's a VERY basic example on handling this socket available as a GitHub Gist.
If your intent is to use the web based interfaces, you'd want to perform a GetStatus
request, which will return http_settings
as part of the response json structure detailing the location and state of the web interface.
Once you have the web server details (or assumed the defaults), you can use HTTP to Send commands to the daemon. These are incredibly simple, send a POST
request with Content-Type: application/json
to /api/command
, with the request in the body and you'll receive a JSON response. (more on messaging later).
So for example, to fetch the Daemon Status, you'd simply send a "GetStatus"
message to the endpoint, and will receive the "Status" {..}
response.
Websockets behave slightly differently than simple requests due to extra async behaviors to consider. The websocket supports asynchronous reuse, allowing you to send commands with potentially different execution lengths, and get the response as soon as it's available (Note that all requests will be processed in the order received). The main issue that this can cause is that the responses may not arrive in the order they are sent, so an ID is required to be attached to all requests, which will be returned with the response.
So an example GetStatus request will look something like this:
{
"id": 72, /* id is an unsigned 32bit integer */
"data": "GetStatus",
}
And responses will follow a similar format:
{
"id": 72, /* Matches the Request */
"data": { "Status" {..} }
}
How to match up requests and responses is an exercise for the integrator, I use simple javascript promises in the Web UI.
Whenever anything about the GoXLR changes (be it a physical interaction, or another app changing a setting), a JSON Patch message is emitted to any and all clients connected via websockets.
This patch is intended to be applied to a Status
object received from executing a GetStatus
message, although how this is achieved may be dependent on your platform or language, some options include:
- Directly mutating the Status structure (
fast-json-patch
for javascript, possibly Reflection in type-safe languages) - Storing the raw JSON response from
GetStatus
, patching that, and replacing any de-serialized objects - Serialize an object back to JSON, patch the JSON, then de-serialize replacing the original object
For a rust example on handling the websocket and patches, check out this project, it fetches the websocket address from the unix socket, grabs the Status
, patches whenever updates arrive, and updates an audio source in OBS whenever a specific channel volume changes (either via fader, or the UI), it could be the basis for a 'VOD Track' implementation.
When working in a UI, you'll need to be considerate of patch events for your own changes. When sending a change to the daemon, in addition to the general 'Ok' response, it will emit a 'Patch' event for that change, confirming it has been made. Depending on the type of interaction, and the speed of updates, the patches may be slightly behind what has been sent (an example is spamming a slider up and down really fast), and if you've mapped your component directly to the Status
value there might be some contention over the actual value.
In addition, if you're modifying the volume of a fader on the full sized GoXLR, the GoXLR itself will trigger internal events for the volume changes to the fader as the motorized sliders move, which the daemon will pick up and emit as a new patch (unfortunately, the GoXLR itself is authoritative as far as volumes are concerned, and being physical may overshoot / undershoot an intended target making it difficult to handle daemon-side), sending a command to the Daemon which sets the volume from 0 to 255 could result in many patch events as the fader moves.
In the WebUI, this is solved by temporarily ignoring patch events for sliders while the user is actively manipulating them. Once the user finishes, simply replace the value in the Status
object with the new value, which will ostensibly be the same value as the last change sent to the Daemon. While this might not perfectly catch overshoot / undershoot, and could leave the Status
object slightly desynced, it's generally close enough.
While not formally documented (yet!), determining the JSON messages to send across to the GoXLR is relatively straight forward. All of the commands and responses are defined in the ipc
crates lib.rs
file, DaemonRequest
are the requests, and DaemonResponse
are the possible responses.
All requests should be derived from the DaemonRequest
enum, if you send a request that's not listed there, or is of an incorrect format, an error will be returned. Serialization of the enums is straight forward, the constant is converted to a String, and the parameters are represented either by themselves, or as an Array if there's more than one. If there ARE parameters, the command needs to be wrapped in a JSON object {}
so the DaemonRequest::GetStatus
constant is represented in JSON as:
"GetStatus"
With this information, we can infer the behavior of the DaemonRequest::OpenPath
command, PathTypes
is an enum, and we know the constants are just represented as Strings in the JSON, and there's only one parameter, so an array isn't needed, but it needs to be wrapped in an object, so to open the profiles directory, the JSON would be:
{ "OpenPath": "Profiles" }
And DaemonRequest::Command
has two parameters, which need to be represented as an array, so we end up with:
{ "Command": [String, GoXLRCommand] }
The String
in this case is a device serial number (they're included in the GetStatus
response).. As for the GoXLRCommand
, this is another enum further down the same file, listing all of the device commands that can be sent. These commands all serialize out the same way as the top level elements. It should be noted, that the commands include a lot of types, these can be found in the types
crates lib.rs
file. They're all enums, so their values get sent as Strings.
So to set a Fader, we look to the GoXLRCommand::SetFader
constant, it requires a FaderName
and a ChannelName
(both defined in the types
crate), so we'll change Fader A
to the channel Mic
, remembering to start at the DaemonRequest
level, we end up with:
{ "Command": [Serial, { "SetFader": ["A", "Mic"] } ] }
Serialization of responses tends to behave in exactly the same way as serializing requests, except they're based on the DaemonResponse
enum. when executing a GoXLRCommand
, you'll get one of two responses, either Ok
as a string by itself, or an error:
{ "Error": "This is an Error!" }
Obviously, errors can be sent for any Request.
There are three other types of responses, HttpSettings
fetched by calling GetHttpState
, DaemonStatus
fetched by calling GetStatus
and Patch
, discussed earlier in the websocket section.
Serialisation of these structs is pretty straight forward, although they're defined in the ipc
crates device.rs
file.
In the case of a struct, the values inside it are represented as key: value
pairs, so for example a DaemonConfig
inside the DaemonStatus struct would be represented as:
/* "Status" is from DaemonResponse */
"Status": {
"config": {
"daemon_version": "0.9.0",
/* Rust boolean is mapped directly to javascript boolean */
"autostart_enabled": true,
"show_tray_icon": true
},
...
}
For anything that's an Option
, there will be either a value, or null
, this generally applies to feature that aren't present on a Mini.
For Maps (EnumMap
, HashMap
, BTreeMap
), the first type is the key, and the second the value, for example, the mixers
are defined in rust as:
pub mixers: HashMap<String, MixerStatus>,
And map to:
"mixers": {
"SERIAL_NUMBER": {
/* Into the MixerStatus object */
"hardware": {
...
}
},
"SERIAL_NUMBER_2": {
...
}
}
For the EnumMap<A, B>
type, all keys of type A are GUARANTEED to be present and set in the response, in HashMap
s and BTreeMap
s that guarantee is not met (note, in future releases, some HashMaps may be converted to EnumMaps).
And finally, for types defined as Vec
, they will be presented as a simple Javascript array.
Hopefully that should be enough to get you going with the API, hit me up on discord if you have any questions or need a hand. Often the best way to get a handle on the Requests and Responses would be to open a web browser's development console with the UI open, and watch the Web Socket there do its magic as you make changes.