Skip to content

Commit

Permalink
Adds more examples and documentation (#83)
Browse files Browse the repository at this point in the history
* rename examples to _examples

will exclude it from appearing in a vendored module

* add shutdown example

* move test.sh into script dir

* generate a request mapping into the docs

* do some wacky markdown preprocessing lol

anything to avoid updating documentation!!

* replace the version in the readme too

* find the library version from the runtime deps of whatever tool uses goobs

one less thing to update on new releases!

* only print versions in basic example

* ensure obs is running for readme snippet update

* be explicit about latest in script/test.sh

* nit: test workflow step needed a name

* fix update-readme-snippets.sh

* basic example needs to use go modules to find correct library version

* try something else to find candidate library version

* internal: sample module to test version discovery from deps

* fix snippet when it's it's the last line

* todo write more usage docs

* godocs

* try to cleanly disconnect but if err force close

* add more detailed usage docs

* docs

---------

Co-authored-by: Andrey Kaipov <andreykaipov@users.noreply.github.com>
  • Loading branch information
andreykaipov and andreykaipov authored Dec 19, 2023
1 parent c17eaa7 commit b8f04e9
Show file tree
Hide file tree
Showing 25 changed files with 683 additions and 91 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ jobs:
- uses: magnetikonline/action-golang-cache@v4
with:
go-version-file: go.mod
- run: |
- name: make test
run: |
# some tests are flaky because it's running against a dockerized obs
# instance, so just retry a few times or whatever to be certain
for _ in $(seq 1 5); do
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/update-protocol-version.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ jobs:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
run: |
./script/bump-protocol-version.sh
version=$(grep obs_websocket_protocol_version version.go | tr -dc '0-9.')
./script/update-readme-snippets.sh
version=$(grep ProtocolVersion version.go | tr -dc '0-9.')
message="Bump OBS websocket protocol to $version"
echo "message=$message" >>$GITHUB_OUTPUT
Expand Down
7 changes: 3 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
goobs_version := $(shell grep obs_version version.go | cut -d= -f2 | tr -dc '[0-9.]')
obs_websocket_protocol_version := $(shell grep obs_websocket_protocol_version version.go | cut -d= -f2 | tr -dc '[0-9.]')
obs_websocket_protocol_version := $(shell grep ProtocolVersion version.go | tr -dc '[0-9.]')

export goobs_version
export obs_websocket_protocol_version

help:
Expand All @@ -12,8 +10,9 @@ help:
test: test.unit test.functional
test.unit:
cd internal; go test -v -count 1 ./...
cd internal/sample; go test -v -count 1 ./...
test.functional:
./test.sh
./script/test.sh

generate: generate.protocol generate.tests
$(MAKE) format
Expand Down
67 changes: 22 additions & 45 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,84 +14,61 @@
[goreport-img]: https://goreportcard.com/badge/github.com/andreykaipov/goobs?logo=go&logoColor=white&style=flat-square
[goreport-url]: https://goreportcard.com/report/github.com/andreykaipov/goobs

It's a Go client for
[obsproject/obs-websocket](https://github.com/obsproject/obs-websocket),
allowing us to interact with OBS Studio from Go!
Interact with OBS Studio from Go!

## installation

To add this library to your module, simply `go get` it like any other Go module
after you've initialized your own:
To use this library in your project, add it as a module after you've initialized your own:

```console
go mod init blah
go mod init github.com/beautifulperson/my-cool-obs-thing
go get github.com/andreykaipov/goobs
```

## usage

Here's a basic example, where we grab the version and print out the scenes.
Check out the [examples](./examples) for more.
The following example connects to the server and prints out some versions.

Check out the [docs](./docs/README.md) for more info, or just jump right into the [other examples](./_examples)!

[//]: # (snippet-1-begin)
```go
package main

import (
"fmt"
"log"

"github.com/andreykaipov/goobs"
)

func main() {
client, err := goobs.New("localhost:4455", goobs.WithPassword("goodpassword"))
if err != nil {
log.Fatal(err)
panic(err)
}
defer client.Disconnect()

version, err := client.General.GetVersion()
if err != nil {
log.Fatal(err)
panic(err)
}
fmt.Printf("OBS Studio version: %s\n", version.ObsVersion)
fmt.Printf("Websocket server version: %s\n", version.ObsWebSocketVersion)

resp, _ := client.Scenes.GetSceneList()
for _, v := range resp.Scenes {
fmt.Printf("%2d %s\n", v.SceneIndex, v.SceneName)
}
fmt.Printf("OBS Studio version: %s\n", version.ObsVersion)
fmt.Printf("Server protocol version: %s\n", version.ObsWebSocketVersion)
fmt.Printf("Client library version: %s\n", goobs.LibraryVersion)
fmt.Printf("Client protocol version: %s\n", goobs.ProtocolVersion)
}
```
[//]: # (snippet-1-end)

This outputs the following:
The corresponding output:

[//]: # (snippet-2-begin)
```console
go run examples/basic/main.go
OBS Studio version: 29.0.0
Websocket server version: 5.1.0
1 Just Chatting
2 Intermission
3 Be Right Back 2
4 Be Right Back
5 Stream Starting Soon
6 Background
7 Camera Secondary
8 Camera Primary
9 Main 2
10 Main
go run _examples/basic/main.go
OBS Studio version: 30.0.1
Server protocol version: 5.3.4
Client library version: 0.0.0-00010101000000-000000000000
Client protocol version: 5.1.0
```

## advanced configuration

- `GOOBS_LOG` can be set to `trace`, `debug`, `info`, or `error` to better understand what our client is doing under the hood.

- `GOOBS_PROFILE` can be set to enable profiling.
For example, the following will help us find unreleased memory:

```console
❯ GOOBS_PROFILE=memprofile=mem.out OBS_PORT=4455 go test -v -run=profile client_test.go
❯ go tool pprof -top -sample_index=inuse_space mem.out
```

Set `GOOBS_PROFILE=help` to see all the other available options.
[//]: # (snippet-2-end)
15 changes: 6 additions & 9 deletions examples/basic/main.go → _examples/basic/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,24 @@ package main

import (
"fmt"
"log"

"github.com/andreykaipov/goobs"
)

func main() {
client, err := goobs.New("localhost:4455", goobs.WithPassword("goodpassword"))
if err != nil {
log.Fatal(err)
panic(err)
}
defer client.Disconnect()

version, err := client.General.GetVersion()
if err != nil {
log.Fatal(err)
panic(err)
}
fmt.Printf("OBS Studio version: %s\n", version.ObsVersion)
fmt.Printf("Websocket server version: %s\n", version.ObsWebSocketVersion)

resp, _ := client.Scenes.GetSceneList()
for _, v := range resp.Scenes {
fmt.Printf("%2d %s\n", v.SceneIndex, v.SceneName)
}
fmt.Printf("OBS Studio version: %s\n", version.ObsVersion)
fmt.Printf("Server protocol version: %s\n", version.ObsWebSocketVersion)
fmt.Printf("Client protocol version: %s\n", goobs.ProtocolVersion)
fmt.Printf("Client library version: %s\n", goobs.LibraryVersion)
}
File renamed without changes.
File renamed without changes.
30 changes: 30 additions & 0 deletions _examples/shutdown/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package main

import (
"fmt"
"log"

"github.com/andreykaipov/goobs"
"github.com/andreykaipov/goobs/api/requests/general"
)

func main() {
client, err := goobs.New("localhost:4455", goobs.WithPassword("goodpassword"))
if err != nil {
log.Fatal(err)
}
defer client.Disconnect()

// this assumes you have the obs-shutdown-plugin installed
// see https://github.com/norihiro/obs-shutdown-plugin
params := general.NewCallVendorRequestParams().
WithVendorName("obs-shutdown-plugin").
WithRequestType("shutdown").
WithRequestData(map[string]interface{}{
"reason": "cleaning up",
"support_url": "https://github.com/norihiro/obs-shutdown-plugin/issues",
"force": true,
})
resp, err := client.General.CallVendorRequest(params)
fmt.Println(resp, err)
}
File renamed without changes.
19 changes: 8 additions & 11 deletions api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,15 @@ type Client struct {
// SendRequest abstracts the logic every subclient uses to send a request and
// receive the corresponding response.
//
// To get the response for a sent request, we can just read the next response
// from our channel. This works fine in a single-threaded context, and the
// message IDs of both the sent request and response should match. In
// a concurrent context, this isn't necessarily true, but since
// gorilla/websocket doesn't handle concurrency (it'll panic; see
// https://github.com/gorilla/websocket/issues/119), who cares?
// To get the response for a sent request, we simply read the next response
// off our incoming responses channel. This works fine in a single-threaded
// context, and the message IDs of both the sent request and response should
// match.
//
// Technically a request ID and response ID mismatch could happen if the server
// processes requests in a different order it received them (e.g. we should 1,
// then 2; but it processes 2, and then 1), then yeah... there'll be an error.
// We could add a mutex wrapping sending our request and reading from the
// channel, but I personally haven't experienced this yet, so
// A request ID and response ID mismatch could happen if the server processes
// requests in a different order it received them (e.g. we should 1, then 2; but
// it processes 2, and then 1). In this case there'll be an error, so note the
// mutex lock and deferred unlock to prevent this from happening.
//
// It should be noted multiple connections to the server are totally fine.
// Phrased differently, mesasge IDs are unique per client. Moreover, events will
Expand Down
41 changes: 29 additions & 12 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ func WithPassword(x string) Option {
return func(o *Client) { o.password = x }
}

// WithEventSubscriptions specifies the events we'd like to susbcribe to via
// `client.Listen()`. The value is a bitmask of any of the subscription values
// specified in api/events/subscriptions. By default, all event categories are
// WithEventSubscriptions specifies the events we'd like to subscribe to via
// [Client.Listen]. The value is a bitmask of any of the subscription values
// specified in [subscriptions]. By default, all event categories are
// subscribed, except for events marked as high volume. High volume events must
// be explicitly subscribed to.
func WithEventSubscriptions(x int) Option {
Expand All @@ -60,11 +60,9 @@ func WithLogger(x api.Logger) Option {
return func(o *Client) { o.Log = x }
}

// WithDialer sets the underlying Gorilla WebSocket Dialer (see
// https://pkg.go.dev/github.com/gorilla/websocket#Dialer), should one want to
// WithDialer sets the underlying [gorilla/websocket.Dialer] should one want to
// customize things like the handshake timeout or TLS configuration. If this is
// not set, it'll use the provided DefaultDialer (see
// https://pkg.go.dev/github.com/gorilla/websocket#pkg-variables).
// not set, it'll use the provided [gorilla/websocket.DefaultDialer].
func WithDialer(x *websocket.Dialer) Option {
return func(o *Client) { o.dialer = x }
}
Expand All @@ -85,8 +83,9 @@ func WithResponseTimeout(x time.Duration) Option {

/*
Disconnect sends a message to the OBS websocket server to close the client's
open connection. You don't really have to do this as any connections should
close when your program terminates or interrupts. But here's a function anyways.
open connection. You should defer a disconnection after creating your client to
ensure program doesn't leave any lingering connections open and potentially leak
memory.
*/
func (c *Client) Disconnect() error {
defer func() {
Expand All @@ -103,10 +102,14 @@ func (c *Client) Disconnect() error {

c.Log.Printf("[DEBUG] Sending disconnect message")
c.disconnected <- true
return c.conn.WriteMessage(
if err := c.conn.WriteMessage(
websocket.CloseMessage,
websocket.FormatCloseMessage(websocket.CloseNormalClosure, "Bye"),
)
); err != nil {
c.Log.Printf("[ERROR] Force closing connection", err)
return c.conn.Close()
}
return nil
}

/*
Expand All @@ -117,7 +120,7 @@ func New(host string, opts ...Option) (*Client, error) {
c := &Client{
host: host,
dialer: websocket.DefaultDialer,
requestHeader: http.Header{"User-Agent": []string{"goobs/" + goobs_version}},
requestHeader: http.Header{"User-Agent": []string{"goobs/" + LibraryVersion}},
eventSubscriptions: subscriptions.All,
errors: make(chan error),
disconnected: make(chan bool, 1),
Expand Down Expand Up @@ -413,6 +416,20 @@ func (c *Client) writeEvent(event any) {
}
}

// Listen hooks into the incoming events from the server. You'll have to make
// type assertions to ensure you're getting the right events, e.g.:
//
// client.Listen(func(event any) {
// switch val := event.(type) {
// case *events.InputVolumeChanged:
// // event i was looking for
// default:
// // other events
// }
// })
//
// If looking for high volume events, make sure you've initialized the client
// with the appropriate subscriptions with [WithEventSubscriptions].
func (c *Client) Listen(f func(any)) {
for event := range c.IncomingEvents {
f(event)
Expand Down
Loading

0 comments on commit b8f04e9

Please sign in to comment.