Compare commits
No commits in common. "master" and "v1.0.0" have entirely different histories.
31
README.md
31
README.md
|
@ -1,31 +0,0 @@
|
||||||
StreamDeck OBS Replay
|
|
||||||
=====================
|
|
||||||
|
|
||||||
Enables toggling replay recording and saving replays from Stream Deck with feedback shown on the icons.
|
|
||||||
|
|
||||||
Installation - OBS Studio
|
|
||||||
------------
|
|
||||||
|
|
||||||
Install [obs-websocket](https://github.com/Palakis/obs-websocket) and configure
|
|
||||||
|
|
||||||
Add plugin to Stream Deck (double click) and configure your icons with the host, port (default: 4444) and password (if configured).
|
|
||||||
|
|
||||||
Installation - Streamlabs OBS
|
|
||||||
-----------------------------
|
|
||||||
|
|
||||||
Set port to `59650` and the plugin will auto detect.
|
|
||||||
|
|
||||||
Credits
|
|
||||||
-------
|
|
||||||
|
|
||||||
StreamLabs OBS (Base software)
|
|
||||||
|
|
||||||
OBS Studio (Base software)
|
|
||||||
|
|
||||||
Stéphane Lepin ([obs-websocket](https://github.com/Palakis/obs-websocket))
|
|
||||||
|
|
||||||
Chris de Graaf ([go-obs-websocket](https://github.com/christopher-dG/go-obs-websocket))
|
|
||||||
|
|
||||||
Elgato/Corsair for Stream Deck + SDK/Documentation
|
|
||||||
|
|
||||||
Bumbler (https://bumbler.tv) for icons/plugin icon
|
|
35
client.go
35
client.go
|
@ -1,35 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Client interface {
|
|
||||||
Connect() error
|
|
||||||
Disconnect() error
|
|
||||||
Connected() bool
|
|
||||||
|
|
||||||
ToggleReplay() error
|
|
||||||
SaveReplay() error
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewClient(key, host string, port int, password string) Client {
|
|
||||||
res, err := http.Get("http://" + host + ":" + strconv.Itoa(port) + "/api/info")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
io.Copy(ioutil.Discard, res.Body)
|
|
||||||
|
|
||||||
if res.StatusCode == 200 {
|
|
||||||
return NewSlobsClient(key, host, port, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewOBSWSClient(key, host, port, password)
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"meow.tf/streamdeck/obs-replay/obsws"
|
|
||||||
)
|
|
||||||
|
|
||||||
type OBSWSClient struct {
|
|
||||||
client *obsws.Client
|
|
||||||
key string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewOBSWSClient(key, host string, port int, password string) *OBSWSClient {
|
|
||||||
obsc := &obsws.Client{Host: host, Port: port, Password: password}
|
|
||||||
|
|
||||||
return &OBSWSClient{client: obsc, key: key}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *OBSWSClient) Connect() error {
|
|
||||||
err := c.client.Connect()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.client.AddEventHandler("StreamStatus", streamStatusUpdate(c.key))
|
|
||||||
c.client.AddEventHandler("ReplayStarted", func(obsws.Event) { loopContextState(c.key, 1) })
|
|
||||||
c.client.AddEventHandler("ReplayStopped", func(obsws.Event) { loopContextState(c.key, 0) })
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *OBSWSClient) Disconnect() error {
|
|
||||||
return c.client.Disconnect()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *OBSWSClient) Connected() bool {
|
|
||||||
return c.client.Connected()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *OBSWSClient) ToggleReplay() error {
|
|
||||||
req := obsws.NewStartStopReplayBufferRequest()
|
|
||||||
|
|
||||||
return req.Send(c.client)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *OBSWSClient) SaveReplay() error {
|
|
||||||
req := obsws.NewSaveReplayBufferRequest()
|
|
||||||
|
|
||||||
return req.Send(c.client)
|
|
||||||
}
|
|
||||||
|
|
||||||
func streamStatusUpdate(key string) func(obsws.Event) {
|
|
||||||
return func(e obsws.Event) {
|
|
||||||
evt := e.(obsws.StreamStatusEvent)
|
|
||||||
|
|
||||||
state := 0
|
|
||||||
|
|
||||||
if evt.Replay {
|
|
||||||
state = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
loopContextState(key, state)
|
|
||||||
}
|
|
||||||
}
|
|
116
client_slobs.go
116
client_slobs.go
|
@ -1,116 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"meow.tf/streamdeck/obs-replay/slobs"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SlobsClient struct {
|
|
||||||
client *slobs.Client
|
|
||||||
key string
|
|
||||||
password string
|
|
||||||
recording bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSlobsClient(key, host string, port int, password string) *SlobsClient {
|
|
||||||
slobsc := slobs.NewClient(host + ":" + strconv.Itoa(port))
|
|
||||||
|
|
||||||
return &SlobsClient{client: slobsc, key: key, password: password}
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
Running = "running"
|
|
||||||
Saving = "saving"
|
|
||||||
Stopping = "stopping"
|
|
||||||
Offline = "offline"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *SlobsClient) Connect() error {
|
|
||||||
err := c.client.Connect()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.client.Auth(c.password, func(err error) {
|
|
||||||
if err != nil {
|
|
||||||
// TODO alert that it failed to startup?
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.requestInitialData()
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *SlobsClient) requestInitialData() {
|
|
||||||
c.client.SendRPC("StreamingService", "getModel", func(e *slobs.RPCResponse) {
|
|
||||||
state := &slobs.IStreamingState{}
|
|
||||||
|
|
||||||
e.DecodeTo(&state)
|
|
||||||
|
|
||||||
switch state.ReplayBufferStatus {
|
|
||||||
case Saving:
|
|
||||||
fallthrough
|
|
||||||
case Running:
|
|
||||||
loopContextState(c.key, 1)
|
|
||||||
c.recording = true
|
|
||||||
case Offline:
|
|
||||||
loopContextState(c.key, 0)
|
|
||||||
c.recording = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
c.client.Subscribe("StreamingService", "replayBufferStatusChange", func(e *slobs.ResourceEvent) {
|
|
||||||
var status string
|
|
||||||
e.DecodeTo(&status)
|
|
||||||
|
|
||||||
switch status {
|
|
||||||
case Saving:
|
|
||||||
return
|
|
||||||
case Running:
|
|
||||||
loopContextState(c.key, 1)
|
|
||||||
c.recording = true
|
|
||||||
case Offline:
|
|
||||||
loopContextState(c.key, 0)
|
|
||||||
c.recording = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *SlobsClient) Disconnect() error {
|
|
||||||
return c.client.Disconnect()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *SlobsClient) Connected() bool {
|
|
||||||
return c.client.Connected()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *SlobsClient) ToggleReplay() error {
|
|
||||||
ret := make(chan struct{}, 1)
|
|
||||||
|
|
||||||
handler := func(res *slobs.RPCResponse) {
|
|
||||||
close(ret)
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.recording {
|
|
||||||
c.client.SendRPC("StreamingService", "stopReplayBuffer", handler)
|
|
||||||
} else {
|
|
||||||
c.client.SendRPC("StreamingService", "startReplayBuffer", handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
<-ret
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *SlobsClient) SaveReplay() error {
|
|
||||||
ret := make(chan struct{}, 1)
|
|
||||||
|
|
||||||
c.client.SendRPC("StreamingService", "saveReplay", func(res *slobs.RPCResponse) {
|
|
||||||
close(ret)
|
|
||||||
})
|
|
||||||
|
|
||||||
<-ret
|
|
||||||
return nil
|
|
||||||
}
|
|
2
go.mod
2
go.mod
|
@ -3,7 +3,7 @@ module meow.tf/streamdeck/obs-replay
|
||||||
go 1.13
|
go 1.13
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9
|
github.com/christopher-dG/go-obs-websocket v0.0.0-20181224025342-2efc3605bff5
|
||||||
github.com/gorilla/websocket v1.4.1
|
github.com/gorilla/websocket v1.4.1
|
||||||
github.com/mitchellh/mapstructure v1.1.2
|
github.com/mitchellh/mapstructure v1.1.2
|
||||||
github.com/valyala/fastjson v1.4.1
|
github.com/valyala/fastjson v1.4.1
|
||||||
|
|
5
go.sum
5
go.sum
|
@ -1,5 +1,6 @@
|
||||||
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9 h1:74lLNRzvsdIlkTgfDSMuaPjBr4cf6k7pwQQANm/yLKU=
|
github.com/christopher-dG/go-obs-websocket v0.0.0-20181224025342-2efc3605bff5 h1:VtKPsvxzKt/+EnkhcPp0Xg7MDjt/a+CNRSj5phITbjo=
|
||||||
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
|
github.com/christopher-dG/go-obs-websocket v0.0.0-20181224025342-2efc3605bff5/go.mod h1:hFg9UFHefvNCvpWpYtOaP/VT2HyokIJsmV1AUBjpTeQ=
|
||||||
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
||||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||||
|
|
|
@ -37,12 +37,12 @@
|
||||||
"Author": "Meow.tf",
|
"Author": "Meow.tf",
|
||||||
"Category": "OBS Replay",
|
"Category": "OBS Replay",
|
||||||
"CodePathWin": "replay.exe",
|
"CodePathWin": "replay.exe",
|
||||||
"Description": "Control OBS' Replays using StreamDeck and Streamlabs OBS/OBS Studio + obs-websocket",
|
"Description": "Control OBS' Replays using StreamDeck.",
|
||||||
"Name": "OBS Replay",
|
"Name": "OBS Replay",
|
||||||
"Icon": "images/pluginIcon",
|
"Icon": "images/pluginIcon",
|
||||||
"CategoryIcon": "images/pluginIcon",
|
"CategoryIcon": "images/pluginIcon",
|
||||||
"URL": "https://streamdeck.meow.tf/obsreplay",
|
"URL": "https://streamdeck.meow.tf/obsreplay",
|
||||||
"Version": "1.1.2",
|
"Version": "1.0.0",
|
||||||
"SDKVersion": 2,
|
"SDKVersion": 2,
|
||||||
"OS": [
|
"OS": [
|
||||||
{
|
{
|
||||||
|
|
183
replay.go
183
replay.go
|
@ -3,26 +3,25 @@ package main
|
||||||
import (
|
import (
|
||||||
"github.com/valyala/fastjson"
|
"github.com/valyala/fastjson"
|
||||||
"log"
|
"log"
|
||||||
|
"meow.tf/streamdeck/obs-replay/obsws"
|
||||||
"meow.tf/streamdeck/sdk"
|
"meow.tf/streamdeck/sdk"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
actionReplayToggle = "tf.meow.obsreplay.replay_toggle"
|
actionReplayToggle = "tf.meow.obsreplay.replay_toggle"
|
||||||
actionReplaySave = "tf.meow.obsreplay.replay_save"
|
actionReplaySave = "tf.meow.obsreplay.replay_save"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
contextMutex sync.RWMutex
|
contextMutex sync.RWMutex
|
||||||
clientMutex sync.RWMutex
|
clientMutex sync.RWMutex
|
||||||
stateMutex sync.RWMutex
|
stateMutex sync.RWMutex
|
||||||
|
|
||||||
clients = make(map[string]Client)
|
clients = make(map[string]*obsws.Client)
|
||||||
contexts = make(map[string]string)
|
contexts = make(map[string]string)
|
||||||
contextActions = make(map[string]string)
|
cachedStates = make(map[string]int)
|
||||||
cachedStates = make(map[string]int)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func replayToggle(action, context string, payload *fastjson.Value, deviceId string) {
|
func replayToggle(action, context string, payload *fastjson.Value, deviceId string) {
|
||||||
|
@ -33,30 +32,16 @@ func replayToggle(action, context string, payload *fastjson.Value, deviceId stri
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.ToggleReplay(); err != nil {
|
req := obsws.NewStartStopReplayBufferRequest()
|
||||||
|
|
||||||
|
err := req.Send(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
sdk.ShowAlert(context)
|
sdk.ShowAlert(context)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
time.AfterFunc(1*time.Second, func() {
|
sdk.ShowOk(context)
|
||||||
contextMutex.RLock()
|
|
||||||
key, exists := contexts[context]
|
|
||||||
contextMutex.RUnlock()
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
stateMutex.RLock()
|
|
||||||
state, exists := cachedStates[key]
|
|
||||||
stateMutex.RUnlock()
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sdk.SetState(context, state)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func replaySave(action, context string, payload *fastjson.Value, deviceId string) {
|
func replaySave(action, context string, payload *fastjson.Value, deviceId string) {
|
||||||
|
@ -67,22 +52,9 @@ func replaySave(action, context string, payload *fastjson.Value, deviceId string
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
contextMutex.RLock()
|
req := obsws.NewSaveReplayBufferRequest()
|
||||||
key, exists := contexts[context]
|
|
||||||
contextMutex.RUnlock()
|
|
||||||
|
|
||||||
if exists {
|
if err := req.Send(c); err != nil {
|
||||||
stateMutex.RLock()
|
|
||||||
state, exists := cachedStates[key]
|
|
||||||
stateMutex.RUnlock()
|
|
||||||
|
|
||||||
if exists && state == 0 {
|
|
||||||
sdk.ShowAlert(context)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.SaveReplay(); err != nil {
|
|
||||||
sdk.ShowAlert(context)
|
sdk.ShowAlert(context)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -90,7 +62,7 @@ func replaySave(action, context string, payload *fastjson.Value, deviceId string
|
||||||
sdk.ShowOk(context)
|
sdk.ShowOk(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
func clientForContext(context string) Client {
|
func clientForContext(context string) *obsws.Client {
|
||||||
contextMutex.RLock()
|
contextMutex.RLock()
|
||||||
key, exists := contexts[context]
|
key, exists := contexts[context]
|
||||||
contextMutex.RUnlock()
|
contextMutex.RUnlock()
|
||||||
|
@ -107,14 +79,6 @@ func clientForContext(context string) Client {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !c.Connected() {
|
|
||||||
err := c.Connect()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,15 +87,9 @@ func onWillAppear(e *sdk.WillAppearEvent) {
|
||||||
|
|
||||||
if settings != nil {
|
if settings != nil {
|
||||||
host := sdk.JsonStringValue(settings, "host")
|
host := sdk.JsonStringValue(settings, "host")
|
||||||
portStr := sdk.JsonStringValue(settings, "port")
|
port := settings.GetInt("port")
|
||||||
password := sdk.JsonStringValue(settings, "password")
|
password := sdk.JsonStringValue(settings, "password")
|
||||||
|
|
||||||
port, err := strconv.Atoi(portStr)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
port = 4444
|
|
||||||
}
|
|
||||||
|
|
||||||
key := checkClient(host, port, password)
|
key := checkClient(host, port, password)
|
||||||
|
|
||||||
if key == "" {
|
if key == "" {
|
||||||
|
@ -140,24 +98,17 @@ func onWillAppear(e *sdk.WillAppearEvent) {
|
||||||
|
|
||||||
contextMutex.Lock()
|
contextMutex.Lock()
|
||||||
contexts[e.Context] = key
|
contexts[e.Context] = key
|
||||||
contextActions[e.Context] = e.Action
|
|
||||||
contextMutex.Unlock()
|
contextMutex.Unlock()
|
||||||
|
|
||||||
if e.Action == actionReplayToggle {
|
stateMutex.RLock()
|
||||||
stateMutex.RLock()
|
if state, ok := cachedStates[key]; ok {
|
||||||
if state, ok := cachedStates[key]; ok {
|
sdk.SetState(e.Context, state)
|
||||||
sdk.SetState(e.Context, state)
|
|
||||||
}
|
|
||||||
stateMutex.RUnlock()
|
|
||||||
}
|
}
|
||||||
|
stateMutex.RUnlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkClient(host string, port int, password string) string {
|
func checkClient(host string, port int, password string) string {
|
||||||
if host == "" {
|
|
||||||
host = "127.0.0.1"
|
|
||||||
}
|
|
||||||
|
|
||||||
if port == 0 {
|
if port == 0 {
|
||||||
port = 4444
|
port = 4444
|
||||||
}
|
}
|
||||||
|
@ -169,9 +120,17 @@ func checkClient(host string, port int, password string) string {
|
||||||
clientMutex.RUnlock()
|
clientMutex.RUnlock()
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
client = NewClient(key, host, port, password)
|
client = &obsws.Client{Host: host, Port: port, Password: password}
|
||||||
|
|
||||||
defer client.Connect()
|
err := client.Connect()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
client.AddEventHandler("StreamStatus", streamStatusUpdate(key))
|
||||||
|
client.AddEventHandler("ReplayStarted", loopContextState(key, 1))
|
||||||
|
client.AddEventHandler("ReplayStopped", loopContextState(key, 0))
|
||||||
|
|
||||||
clientMutex.Lock()
|
clientMutex.Lock()
|
||||||
clients[key] = client
|
clients[key] = client
|
||||||
|
@ -181,19 +140,45 @@ func checkClient(host string, port int, password string) string {
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
func onWillDisappear(e *sdk.WillDisappearEvent) {
|
func streamStatusUpdate(key string) func(obsws.Event) {
|
||||||
contextDiscounnected(e.Context)
|
return func(e obsws.Event) {
|
||||||
|
evt := e.(obsws.StreamStatusEvent)
|
||||||
|
|
||||||
|
state := 0
|
||||||
|
|
||||||
|
if evt.Replay {
|
||||||
|
state = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
loopContextState(key, state)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func contextDiscounnected(context string) {
|
func loopContextState(key string, state int) func(obsws.Event) {
|
||||||
|
stateMutex.Lock()
|
||||||
|
cachedStates[key] = state
|
||||||
|
stateMutex.Unlock()
|
||||||
|
|
||||||
|
return func(event obsws.Event) {
|
||||||
|
contextMutex.RLock()
|
||||||
|
defer contextMutex.RUnlock()
|
||||||
|
|
||||||
|
for ctx, ctxKey := range contexts {
|
||||||
|
if ctxKey == key {
|
||||||
|
sdk.SetState(ctx, state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func onWillDisappear(e *sdk.WillDisappearEvent) {
|
||||||
contextMutex.Lock()
|
contextMutex.Lock()
|
||||||
defer contextMutex.Unlock()
|
defer contextMutex.Unlock()
|
||||||
|
|
||||||
// replayToggleContexts
|
// replayToggleContexts
|
||||||
key, ok := contexts[context]
|
key, ok := contexts[e.Context]
|
||||||
|
|
||||||
delete(contexts, context)
|
delete(contexts, e.Context)
|
||||||
delete(contextActions, context)
|
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
|
@ -213,30 +198,18 @@ func contextDiscounnected(context string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func onSettingsReceived(e *sdk.ReceiveSettingsEvent) {
|
func onSettingsReceived(e *sdk.ReceiveSettingsEvent) {
|
||||||
host := sdk.JsonStringValue(e.Settings, "host")
|
var host, password string
|
||||||
portStr := sdk.JsonStringValue(e.Settings, "port")
|
|
||||||
password := sdk.JsonStringValue(e.Settings, "password")
|
|
||||||
|
|
||||||
if host == "" {
|
host = sdk.JsonStringValue(e.Settings, "host")
|
||||||
host = "127.0.0.1"
|
port := e.Settings.GetInt("port")
|
||||||
}
|
password = sdk.JsonStringValue(e.Settings, "password")
|
||||||
|
|
||||||
port, err := strconv.Atoi(portStr)
|
if port == 0 {
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
port = 4444
|
port = 4444
|
||||||
}
|
}
|
||||||
|
|
||||||
key := checkClient(host, port, password)
|
key := checkClient(host, port, password)
|
||||||
|
|
||||||
contextMutex.RLock()
|
|
||||||
previousKey, existing := contexts[e.Context]
|
|
||||||
contextMutex.RUnlock()
|
|
||||||
|
|
||||||
if existing && previousKey != key {
|
|
||||||
contextDiscounnected(e.Context)
|
|
||||||
}
|
|
||||||
|
|
||||||
if key == "" {
|
if key == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -281,23 +254,3 @@ func cleanupSockets() {
|
||||||
client.Disconnect()
|
client.Disconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loopContextState(key string, state int) {
|
|
||||||
|
|
||||||
stateMutex.Lock()
|
|
||||||
cachedStates[key] = state
|
|
||||||
stateMutex.Unlock()
|
|
||||||
|
|
||||||
contextMutex.RLock()
|
|
||||||
defer contextMutex.RUnlock()
|
|
||||||
|
|
||||||
for ctx, ctxKey := range contexts {
|
|
||||||
if ctxKey == key {
|
|
||||||
action := contextActions[ctx]
|
|
||||||
|
|
||||||
if action == actionReplayToggle {
|
|
||||||
sdk.SetState(ctx, state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
275
slobs/client.go
275
slobs/client.go
|
@ -1,275 +0,0 @@
|
||||||
package slobs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"github.com/dchest/uniuri"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"log"
|
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SubscriptionHandler func(*ResourceEvent)
|
|
||||||
type ResponseHandler func(*RPCResponse)
|
|
||||||
|
|
||||||
const (
|
|
||||||
Event = "EVENT"
|
|
||||||
Subscription = "SUBSCRIPTION"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrNoRequest = errors.New("request not found")
|
|
||||||
ErrNoHandler = errors.New("handler not found")
|
|
||||||
)
|
|
||||||
|
|
||||||
type Client struct {
|
|
||||||
conn *websocket.Conn
|
|
||||||
connected bool
|
|
||||||
|
|
||||||
Address string
|
|
||||||
|
|
||||||
requestId int32
|
|
||||||
requests map[int]ResponseHandler
|
|
||||||
requestLock sync.RWMutex
|
|
||||||
|
|
||||||
subscriptions map[string]SubscriptionHandler
|
|
||||||
subscriptionLock sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewClient(address string) *Client {
|
|
||||||
return &Client{
|
|
||||||
Address: address,
|
|
||||||
requests: make(map[int]ResponseHandler),
|
|
||||||
subscriptions: make(map[string]SubscriptionHandler),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Connected() bool {
|
|
||||||
return c.connected
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Connect() error {
|
|
||||||
if c.connected {
|
|
||||||
return errors.New("already connected")
|
|
||||||
}
|
|
||||||
|
|
||||||
endpoint := "ws://" + c.Address + "/api/" + paddedRandomIntn(999) + "/" + uniuri.New() + "/websocket"
|
|
||||||
|
|
||||||
var err error
|
|
||||||
|
|
||||||
c.conn, _, err = websocket.DefaultDialer.Dial(endpoint, http.Header{})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, data, err := c.conn.ReadMessage()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if data[0] != 'o' {
|
|
||||||
return errors.New("invalid initial message")
|
|
||||||
}
|
|
||||||
|
|
||||||
c.connected = true
|
|
||||||
|
|
||||||
go c.loop()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Auth(key string, callback func(error)) {
|
|
||||||
c.SendRPC("TcpServerService", "auth", func(response *RPCResponse) {
|
|
||||||
if response.Error != nil {
|
|
||||||
callback(errors.New(response.Error.Message))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var result bool
|
|
||||||
|
|
||||||
json.Unmarshal(*response.Result, &result)
|
|
||||||
|
|
||||||
if !result {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(nil)
|
|
||||||
}, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Disconnect() error {
|
|
||||||
return c.conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Subscribe(resource, method string, handler SubscriptionHandler) error {
|
|
||||||
responseCh := make(chan error, 1)
|
|
||||||
|
|
||||||
c.SendRPC(resource, method, func(response *RPCResponse) {
|
|
||||||
if response.Error != nil {
|
|
||||||
responseCh <- errors.New(response.Error.Message)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
res := &ResourceEvent{}
|
|
||||||
|
|
||||||
json.Unmarshal(*response.Result, &res)
|
|
||||||
|
|
||||||
c.subscriptionLock.Lock()
|
|
||||||
c.subscriptions[res.ResourceId] = handler
|
|
||||||
c.subscriptionLock.Unlock()
|
|
||||||
|
|
||||||
close(responseCh)
|
|
||||||
})
|
|
||||||
|
|
||||||
return <-responseCh
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) SendRPC(resource, method string, handler ResponseHandler, args ...string) error {
|
|
||||||
m := make(map[string]interface{})
|
|
||||||
|
|
||||||
m["resource"] = resource
|
|
||||||
m["args"] = args
|
|
||||||
|
|
||||||
atomic.AddInt32(&c.requestId, 1)
|
|
||||||
|
|
||||||
newRequestId := int(atomic.LoadInt32(&c.requestId))
|
|
||||||
|
|
||||||
request := &RPCRequest{
|
|
||||||
ID: newRequestId,
|
|
||||||
Method: method,
|
|
||||||
Params: m,
|
|
||||||
JSONRPC: "2.0",
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := json.Marshal(request)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if handler != nil {
|
|
||||||
c.requestLock.Lock()
|
|
||||||
c.requests[newRequestId] = handler
|
|
||||||
c.requestLock.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.conn.WriteJSON([]string{string(b) + "\n"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) loop() error {
|
|
||||||
for {
|
|
||||||
_, data, err := c.conn.ReadMessage()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
c.connected = false
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(data) < 1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
switch data[0] {
|
|
||||||
case 'h':
|
|
||||||
// Heartbeat
|
|
||||||
continue
|
|
||||||
case 'a':
|
|
||||||
// Normal message
|
|
||||||
arr := make([]string, 0)
|
|
||||||
err := json.Unmarshal(data[1:], &arr)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, message := range arr {
|
|
||||||
resp := &RPCResponse{}
|
|
||||||
|
|
||||||
message = strings.TrimSpace(message)
|
|
||||||
|
|
||||||
log.Println("Handling", message)
|
|
||||||
|
|
||||||
err = json.Unmarshal([]byte(message), &resp)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
go c.handle(resp)
|
|
||||||
}
|
|
||||||
case 'c':
|
|
||||||
// Session closed
|
|
||||||
var v []interface{}
|
|
||||||
if err := json.Unmarshal(data[1:], &v); err != nil {
|
|
||||||
log.Printf("Closing session: %s", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
log.Println("Unknown:", data[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) handle(resp *RPCResponse) error {
|
|
||||||
if resp.ID != nil {
|
|
||||||
c.requestLock.RLock()
|
|
||||||
h, ok := c.requests[*resp.ID]
|
|
||||||
c.requestLock.RUnlock()
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
return ErrNoRequest
|
|
||||||
}
|
|
||||||
|
|
||||||
h(resp)
|
|
||||||
|
|
||||||
c.requestLock.Lock()
|
|
||||||
delete(c.requests, *resp.ID)
|
|
||||||
c.requestLock.Unlock()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
res := &ResourceEvent{}
|
|
||||||
|
|
||||||
err := json.Unmarshal(*resp.Result, &res)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch res.Type {
|
|
||||||
case Event:
|
|
||||||
c.subscriptionLock.RLock()
|
|
||||||
h, exists := c.subscriptions[res.ResourceId]
|
|
||||||
c.subscriptionLock.RUnlock()
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
return ErrNoHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
h(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func paddedRandomIntn(max int) string {
|
|
||||||
var (
|
|
||||||
ml = len(strconv.Itoa(max))
|
|
||||||
ri = rand.Intn(max)
|
|
||||||
is = strconv.Itoa(ri)
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(is) < ml {
|
|
||||||
is = strings.Repeat("0", ml-len(is)) + is
|
|
||||||
}
|
|
||||||
|
|
||||||
return is
|
|
||||||
}
|
|
47
slobs/rpc.go
47
slobs/rpc.go
|
@ -1,47 +0,0 @@
|
||||||
package slobs
|
|
||||||
|
|
||||||
import "encoding/json"
|
|
||||||
|
|
||||||
type RPCRequest struct {
|
|
||||||
Method string `json:"method"`
|
|
||||||
Params interface{} `json:"params,omitempty"`
|
|
||||||
ID int `json:"id"`
|
|
||||||
JSONRPC string `json:"jsonrpc"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RPCResponse represents a JSON-RPC response object.
|
|
||||||
//
|
|
||||||
// Result: holds the result of the rpc call if no error occurred, nil otherwise. can be nil even on success.
|
|
||||||
//
|
|
||||||
// Error: holds an RPCError object if an error occurred. must be nil on success.
|
|
||||||
//
|
|
||||||
// ID: may always be 0 for single requests. is unique for each request in a batch call (see CallBatch())
|
|
||||||
//
|
|
||||||
// JSONRPC: must always be set to "2.0" for JSON-RPC version 2.0
|
|
||||||
//
|
|
||||||
// See: http://www.jsonrpc.org/specification#response_object
|
|
||||||
type RPCResponse struct {
|
|
||||||
JSONRPC string `json:"jsonrpc"`
|
|
||||||
Result *json.RawMessage `json:"result,omitempty"`
|
|
||||||
Error *RPCError `json:"error,omitempty"`
|
|
||||||
ID *int `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RPCResponse) DecodeTo(v interface{}) error {
|
|
||||||
return json.Unmarshal(*r.Result, &v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RPCError represents a JSON-RPC error object if an RPC error occurred.
|
|
||||||
//
|
|
||||||
// Code: holds the error code
|
|
||||||
//
|
|
||||||
// Message: holds a short error message
|
|
||||||
//
|
|
||||||
// Data: holds additional error data, may be nil
|
|
||||||
//
|
|
||||||
// See: http://www.jsonrpc.org/specification#error_object
|
|
||||||
type RPCError struct {
|
|
||||||
Code int `json:"code"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Data interface{} `json:"data,omitempty"`
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
package slobs
|
|
||||||
|
|
||||||
import "encoding/json"
|
|
||||||
|
|
||||||
type ResourceEvent struct {
|
|
||||||
Type string `json:"_type"`
|
|
||||||
ResourceId string `json:"resourceId"`
|
|
||||||
Emitter string `json:"emitter"`
|
|
||||||
Data *json.RawMessage `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ResourceEvent) DecodeTo(v interface{}) error {
|
|
||||||
return json.Unmarshal(*e.Data, &v)
|
|
||||||
}
|
|
||||||
|
|
||||||
type IStreamingState struct {
|
|
||||||
StreamingStatus string `json:"streamingStatus"`
|
|
||||||
RecordingStatus string `json:"recordingStatus"`
|
|
||||||
ReplayBufferStatus string `json:"replayBufferStatus"`
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
package slobs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_StreamlabsOBS(t *testing.T) {
|
|
||||||
c := NewClient("127.0.0.1:59650")
|
|
||||||
|
|
||||||
err := c.Connect()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
closeCh := make(chan struct{}, 1)
|
|
||||||
|
|
||||||
c.Auth("a", func(err error) {
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
closeCh <- struct{}{}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Subscribe("StreamingService", "replayBufferStatusChange", func(event *ResourceEvent) {
|
|
||||||
var status string
|
|
||||||
event.DecodeTo(&status)
|
|
||||||
log.Println("Event received:", status)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
<-closeCh
|
|
||||||
}
|
|
Loading…
Reference in New Issue