Compare commits
2 Commits
ae1ab88d4a
...
5e85dda038
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e85dda038 | |||
|
|
6f2290bd6c |
@@ -47,6 +47,14 @@ func main() {
|
|||||||
log.Fatalf("setup: %v", err)
|
log.Fatalf("setup: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load .env file from the config directory so secrets (tokens, passwords)
|
||||||
|
// are available via {{env "VAR"}} in module templates without requiring
|
||||||
|
// shell exports or service-level environment configuration.
|
||||||
|
envPath := filepath.Join(filepath.Dir(*cfgPath), ".env")
|
||||||
|
if err := loadEnvFile(envPath); err != nil {
|
||||||
|
log.Printf("warn: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
cfg, err := config.Load(*cfgPath)
|
cfg, err := config.Load(*cfgPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("config: %v", err)
|
log.Fatalf("config: %v", err)
|
||||||
@@ -321,31 +329,97 @@ func pollKey(ctx context.Context, sd *device.StreamDeck, keyIdx int, keyCfg conf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
imgTrue, err := loadImage(filepath.Join(iconsDir, keyCfg.IconTrue))
|
// Check if icon_true is an animated GIF.
|
||||||
if err != nil {
|
trueIsGIF := strings.ToLower(filepath.Ext(keyCfg.IconTrue)) == ".gif"
|
||||||
log.Printf("key %d: load icon_true %q: %v", keyIdx, keyCfg.IconTrue, err)
|
falseIsGIF := strings.ToLower(filepath.Ext(keyCfg.IconFalse)) == ".gif"
|
||||||
return
|
|
||||||
|
// Pre-load GIF frames for whichever icons are GIFs.
|
||||||
|
var trueFrames [][]byte
|
||||||
|
var trueDelays []time.Duration
|
||||||
|
var falseFrames [][]byte
|
||||||
|
var falseDelays []time.Duration
|
||||||
|
var imgTrue, imgFalse image.Image
|
||||||
|
|
||||||
|
if trueIsGIF {
|
||||||
|
var err error
|
||||||
|
trueFrames, trueDelays, err = loadGIF(sd, filepath.Join(iconsDir, keyCfg.IconTrue))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("key %d: load icon_true gif %q: %v", keyIdx, keyCfg.IconTrue, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
imgTrue, err = loadImage(filepath.Join(iconsDir, keyCfg.IconTrue))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("key %d: load icon_true %q: %v", keyIdx, keyCfg.IconTrue, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
imgFalse, err := loadImage(filepath.Join(iconsDir, keyCfg.IconFalse))
|
|
||||||
if err != nil {
|
if falseIsGIF {
|
||||||
log.Printf("key %d: load icon_false %q: %v", keyIdx, keyCfg.IconFalse, err)
|
var err error
|
||||||
return
|
falseFrames, falseDelays, err = loadGIF(sd, filepath.Join(iconsDir, keyCfg.IconFalse))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("key %d: load icon_false gif %q: %v", keyIdx, keyCfg.IconFalse, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
imgFalse, err = loadImage(filepath.Join(iconsDir, keyCfg.IconFalse))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("key %d: load icon_false %q: %v", keyIdx, keyCfg.IconFalse, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lastState := -1 // unknown, forces initial icon set
|
lastState := -1 // unknown, forces initial icon set
|
||||||
|
var animCancel context.CancelFunc
|
||||||
|
var animWg sync.WaitGroup
|
||||||
|
|
||||||
|
stopAnim := func() {
|
||||||
|
if animCancel != nil {
|
||||||
|
animCancel()
|
||||||
|
animWg.Wait()
|
||||||
|
animCancel = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
applyState := func() {
|
applyState := func() {
|
||||||
state := queryPollState(keyCfg.Poll)
|
state := queryPollState(keyCfg.Poll)
|
||||||
if state == lastState {
|
if state == lastState {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
stopAnim()
|
||||||
lastState = state
|
lastState = state
|
||||||
img := imgFalse
|
|
||||||
if state == 1 {
|
if state == 1 {
|
||||||
img = imgTrue
|
if trueIsGIF {
|
||||||
}
|
animCtx, cancel := context.WithCancel(ctx)
|
||||||
if err := sd.SetKeyImage(keyIdx, img); err != nil {
|
animCancel = cancel
|
||||||
log.Printf("key %d: set poll icon: %v", keyIdx, err)
|
animWg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer animWg.Done()
|
||||||
|
animateKey(animCtx, sd, keyIdx, trueFrames, trueDelays)
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
if err := sd.SetKeyImage(keyIdx, imgTrue); err != nil {
|
||||||
|
log.Printf("key %d: set poll icon: %v", keyIdx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if falseIsGIF {
|
||||||
|
animCtx, cancel := context.WithCancel(ctx)
|
||||||
|
animCancel = cancel
|
||||||
|
animWg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer animWg.Done()
|
||||||
|
animateKey(animCtx, sd, keyIdx, falseFrames, falseDelays)
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
if err := sd.SetKeyImage(keyIdx, imgFalse); err != nil {
|
||||||
|
log.Printf("key %d: set poll icon: %v", keyIdx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,6 +431,7 @@ func pollKey(ctx context.Context, sd *device.StreamDeck, keyIdx int, keyCfg conf
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
stopAnim()
|
||||||
return
|
return
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
applyState()
|
applyState()
|
||||||
@@ -364,6 +439,7 @@ func pollKey(ctx context.Context, sd *device.StreamDeck, keyIdx int, keyCfg conf
|
|||||||
// Wait briefly for the toggle command to take effect, then re-poll.
|
// Wait briefly for the toggle command to take effect, then re-poll.
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
stopAnim()
|
||||||
return
|
return
|
||||||
case <-time.After(400 * time.Millisecond):
|
case <-time.After(400 * time.Millisecond):
|
||||||
}
|
}
|
||||||
@@ -377,7 +453,7 @@ func pollKey(ctx context.Context, sd *device.StreamDeck, keyIdx int, keyCfg conf
|
|||||||
// If Match is empty, uses exit code: 0 → on, non-zero → off.
|
// If Match is empty, uses exit code: 0 → on, non-zero → off.
|
||||||
func queryPollState(poll *config.PollConfig) int {
|
func queryPollState(poll *config.PollConfig) int {
|
||||||
cmd := exec.Command("sh", "-c", poll.Command)
|
cmd := exec.Command("sh", "-c", poll.Command)
|
||||||
output, err := cmd.Output()
|
output, err := cmd.CombinedOutput()
|
||||||
if poll.Match == "" {
|
if poll.Match == "" {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return 1
|
return 1
|
||||||
@@ -434,18 +510,10 @@ func animateKey(ctx context.Context, sd *device.StreamDeck, keyIdx int, frames [
|
|||||||
}
|
}
|
||||||
if err := sd.SetKeyFrame(keyIdx, frame); err != nil {
|
if err := sd.SetKeyFrame(keyIdx, frame); err != nil {
|
||||||
writeErrors++
|
writeErrors++
|
||||||
if writeErrors == 1 {
|
if writeErrors == 50 {
|
||||||
// Log the first error so the user knows something happened.
|
|
||||||
log.Printf("key %d: frame write error: %v", keyIdx, err)
|
|
||||||
} else if writeErrors == 50 {
|
|
||||||
// Persistent errors — likely a real device issue, not transient.
|
|
||||||
log.Printf("key %d: %d consecutive frame write errors — device may be unhealthy", keyIdx, writeErrors)
|
log.Printf("key %d: %d consecutive frame write errors — device may be unhealthy", keyIdx, writeErrors)
|
||||||
}
|
}
|
||||||
} else if writeErrors > 0 {
|
} else if writeErrors > 0 {
|
||||||
// Recovered — log a summary if we suppressed errors.
|
|
||||||
if writeErrors > 1 {
|
|
||||||
log.Printf("key %d: recovered after %d frame write errors", keyIdx, writeErrors)
|
|
||||||
}
|
|
||||||
writeErrors = 0
|
writeErrors = 0
|
||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
@@ -617,6 +685,39 @@ func loadPrivilegedCommands(path string) (map[string]string, error) {
|
|||||||
return wl.Commands, nil
|
return wl.Commands, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadEnvFile reads a .env file and sets each KEY=VALUE pair in the process
|
||||||
|
// environment. Blank lines and lines starting with # are ignored. Quoted values
|
||||||
|
// (single or double) are unquoted. A missing file is silently ignored.
|
||||||
|
func loadEnvFile(path string) error {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("load env %q: %w", path, err)
|
||||||
|
}
|
||||||
|
for _, line := range strings.Split(string(data), "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key, val, ok := strings.Cut(line, "=")
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key = strings.TrimSpace(key)
|
||||||
|
val = strings.TrimSpace(val)
|
||||||
|
// Strip matching quotes around value.
|
||||||
|
if len(val) >= 2 &&
|
||||||
|
((val[0] == '"' && val[len(val)-1] == '"') ||
|
||||||
|
(val[0] == '\'' && val[len(val)-1] == '\'')) {
|
||||||
|
val = val[1 : len(val)-1]
|
||||||
|
}
|
||||||
|
os.Setenv(key, val)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func defaultConfigPath() string {
|
func defaultConfigPath() string {
|
||||||
return config.DefaultConfigPath()
|
return config.DefaultConfigPath()
|
||||||
}
|
}
|
||||||
|
|||||||
47
install.sh
47
install.sh
@@ -219,6 +219,46 @@ else
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# obs-cmd (optional — required for OBS module)
|
||||||
|
if command -v obs-cmd &>/dev/null; then
|
||||||
|
ok "obs-cmd found"
|
||||||
|
else
|
||||||
|
info "obs-cmd not found (required for OBS Studio module)"
|
||||||
|
if prompt_yn "Install obs-cmd now?" "y"; then
|
||||||
|
OBS_CMD_VERSION=$(curl -sL "https://api.github.com/repos/grigio/obs-cmd/releases/latest" | grep '"tag_name"' | head -1 | sed 's/.*"v\(.*\)".*/\1/')
|
||||||
|
if [[ -z "$OBS_CMD_VERSION" ]]; then
|
||||||
|
warn "Could not fetch obs-cmd release — install manually: cargo install obs-cmd"
|
||||||
|
elif $IS_MAC; then
|
||||||
|
# Detect architecture — Apple Silicon vs Intel
|
||||||
|
ARCH="$(uname -m)"
|
||||||
|
if [[ "$ARCH" == "arm64" ]]; then
|
||||||
|
OBS_CMD_ASSET="obs-cmd-arm64-macos.tar.gz"
|
||||||
|
else
|
||||||
|
OBS_CMD_ASSET="obs-cmd-x64-macos.tar.gz"
|
||||||
|
fi
|
||||||
|
step "Downloading obs-cmd v${OBS_CMD_VERSION} (${ARCH})..."
|
||||||
|
curl -sL "https://github.com/grigio/obs-cmd/releases/download/v${OBS_CMD_VERSION}/${OBS_CMD_ASSET}" \
|
||||||
|
| tar xz -C /tmp
|
||||||
|
install -m 755 /tmp/obs-cmd /usr/local/bin/obs-cmd
|
||||||
|
rm -f /tmp/obs-cmd
|
||||||
|
else
|
||||||
|
# Linux — x86_64 musl static binary
|
||||||
|
step "Downloading obs-cmd v${OBS_CMD_VERSION}..."
|
||||||
|
curl -sL "https://github.com/grigio/obs-cmd/releases/download/v${OBS_CMD_VERSION}/obs-cmd-v${OBS_CMD_VERSION}-x86_64-unknown-linux-musl.tar.gz" \
|
||||||
|
| tar xz -C /tmp
|
||||||
|
sudo install -m 755 /tmp/obs-cmd /usr/local/bin/obs-cmd
|
||||||
|
rm -f /tmp/obs-cmd
|
||||||
|
fi
|
||||||
|
if command -v obs-cmd &>/dev/null; then
|
||||||
|
ok "obs-cmd installed"
|
||||||
|
else
|
||||||
|
warn "obs-cmd install may have failed — OBS module won't work until it's available"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
info "Skipped — OBS module will not work until obs-cmd is installed"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
nl
|
nl
|
||||||
|
|
||||||
# ── 2. Build ───────────────────────────────────────────────────────────────────
|
# ── 2. Build ───────────────────────────────────────────────────────────────────
|
||||||
@@ -328,6 +368,13 @@ else
|
|||||||
ok "config.yaml already exists — not overwritten"
|
ok "config.yaml already exists — not overwritten"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Always install/update modules.yaml — this is a registry of available module
|
||||||
|
# definitions, not user data. User customisations go in config.yaml (params
|
||||||
|
# overrides per key). Keeping modules.yaml current ensures new modules (OBS,
|
||||||
|
# Slack, etc.) are available immediately after upgrade.
|
||||||
|
install -m 644 modules.example.yaml "${CONFIG_DIR}/modules.yaml"
|
||||||
|
ok "modules.yaml installed (updated to latest)"
|
||||||
|
|
||||||
# Copy bundled icons (never overwrite existing ones the user may have customised).
|
# Copy bundled icons (never overwrite existing ones the user may have customised).
|
||||||
if [[ -d "icons" ]]; then
|
if [[ -d "icons" ]]; then
|
||||||
copied=0
|
copied=0
|
||||||
|
|||||||
@@ -56,3 +56,76 @@ modules:
|
|||||||
exec: |
|
exec: |
|
||||||
curl -s -X POST https://slack.com/api/dnd.endSnooze \
|
curl -s -X POST https://slack.com/api/dnd.endSnooze \
|
||||||
-H "Authorization: Bearer {{env "SLACK_TOKEN"}}"
|
-H "Authorization: Bearer {{env "SLACK_TOKEN"}}"
|
||||||
|
|
||||||
|
# OBS Studio — media player and streaming control via obs-cmd
|
||||||
|
#
|
||||||
|
# Requires: obs-cmd (https://github.com/grigio/obs-cmd)
|
||||||
|
# macOS: brew install grigio/obs-cmd/obs-cmd
|
||||||
|
# Linux: cargo install obs-cmd (or download binary from GitHub releases)
|
||||||
|
#
|
||||||
|
# OBS WebSocket must be enabled: Tools → WebSocket Server Settings (on by default in OBS 28+)
|
||||||
|
#
|
||||||
|
# Add to ~/.config/streamdeck-go/.env:
|
||||||
|
# OBS_WEBSOCKET_PASSWORD=your-password
|
||||||
|
# OBS_HOST=localhost (optional, default: localhost)
|
||||||
|
# OBS_PORT=4455 (optional, default: 4455)
|
||||||
|
obs:
|
||||||
|
play:
|
||||||
|
params:
|
||||||
|
source: "Media Source"
|
||||||
|
exec: |
|
||||||
|
/usr/local/bin/obs-cmd --websocket obsws://{{envDefault "OBS_HOST" "localhost"}}:{{envDefault "OBS_PORT" "4455"}}/{{env "OBS_WEBSOCKET_PASSWORD"}} media-input play "{{.source}}"
|
||||||
|
|
||||||
|
pause:
|
||||||
|
params:
|
||||||
|
source: "Media Source"
|
||||||
|
exec: |
|
||||||
|
/usr/local/bin/obs-cmd --websocket obsws://{{envDefault "OBS_HOST" "localhost"}}:{{envDefault "OBS_PORT" "4455"}}/{{env "OBS_WEBSOCKET_PASSWORD"}} media-input pause "{{.source}}"
|
||||||
|
|
||||||
|
stop:
|
||||||
|
params:
|
||||||
|
source: "Media Source"
|
||||||
|
exec: |
|
||||||
|
/usr/local/bin/obs-cmd --websocket obsws://{{envDefault "OBS_HOST" "localhost"}}:{{envDefault "OBS_PORT" "4455"}}/{{env "OBS_WEBSOCKET_PASSWORD"}} media-input stop "{{.source}}"
|
||||||
|
|
||||||
|
restart:
|
||||||
|
params:
|
||||||
|
source: "Media Source"
|
||||||
|
exec: |
|
||||||
|
/usr/local/bin/obs-cmd --websocket obsws://{{envDefault "OBS_HOST" "localhost"}}:{{envDefault "OBS_PORT" "4455"}}/{{env "OBS_WEBSOCKET_PASSWORD"}} media-input restart "{{.source}}"
|
||||||
|
|
||||||
|
toggle_record:
|
||||||
|
exec: |
|
||||||
|
/usr/local/bin/obs-cmd --websocket obsws://{{envDefault "OBS_HOST" "localhost"}}:{{envDefault "OBS_PORT" "4455"}}/{{env "OBS_WEBSOCKET_PASSWORD"}} recording toggle
|
||||||
|
|
||||||
|
toggle_record_pause:
|
||||||
|
exec: |
|
||||||
|
/usr/local/bin/obs-cmd --websocket obsws://{{envDefault "OBS_HOST" "localhost"}}:{{envDefault "OBS_PORT" "4455"}}/{{env "OBS_WEBSOCKET_PASSWORD"}} recording toggle-pause
|
||||||
|
|
||||||
|
is_recording_paused:
|
||||||
|
exec: |
|
||||||
|
/usr/local/bin/obs-cmd --websocket obsws://{{envDefault "OBS_HOST" "localhost"}}:{{envDefault "OBS_PORT" "4455"}}/{{env "OBS_WEBSOCKET_PASSWORD"}} recording status
|
||||||
|
|
||||||
|
toggle_stream:
|
||||||
|
exec: |
|
||||||
|
/usr/local/bin/obs-cmd --websocket obsws://{{envDefault "OBS_HOST" "localhost"}}:{{envDefault "OBS_PORT" "4455"}}/{{env "OBS_WEBSOCKET_PASSWORD"}} streaming toggle
|
||||||
|
|
||||||
|
scene_switch:
|
||||||
|
params:
|
||||||
|
scene: "Scene 1"
|
||||||
|
exec: |
|
||||||
|
/usr/local/bin/obs-cmd --websocket obsws://{{envDefault "OBS_HOST" "localhost"}}:{{envDefault "OBS_PORT" "4455"}}/{{env "OBS_WEBSOCKET_PASSWORD"}} scene switch "{{.scene}}"
|
||||||
|
|
||||||
|
toggle_mute:
|
||||||
|
params:
|
||||||
|
source: "Mic/Aux"
|
||||||
|
exec: |
|
||||||
|
/usr/local/bin/obs-cmd --websocket obsws://{{envDefault "OBS_HOST" "localhost"}}:{{envDefault "OBS_PORT" "4455"}}/{{env "OBS_WEBSOCKET_PASSWORD"}} input toggle-mute "{{.source}}"
|
||||||
|
|
||||||
|
is_recording:
|
||||||
|
exec: |
|
||||||
|
/usr/local/bin/obs-cmd --websocket obsws://{{envDefault "OBS_HOST" "localhost"}}:{{envDefault "OBS_PORT" "4455"}}/{{env "OBS_WEBSOCKET_PASSWORD"}} recording status
|
||||||
|
|
||||||
|
is_streaming:
|
||||||
|
exec: |
|
||||||
|
/usr/local/bin/obs-cmd --websocket obsws://{{envDefault "OBS_HOST" "localhost"}}:{{envDefault "OBS_PORT" "4455"}}/{{env "OBS_WEBSOCKET_PASSWORD"}} streaming status
|
||||||
|
|||||||
@@ -100,6 +100,15 @@ func funcMap() template.FuncMap {
|
|||||||
// Use this to keep secrets (tokens, passwords) out of modules.yaml.
|
// Use this to keep secrets (tokens, passwords) out of modules.yaml.
|
||||||
"env": os.Getenv,
|
"env": os.Getenv,
|
||||||
|
|
||||||
|
// envDefault returns the value of an environment variable, or
|
||||||
|
// fallback if the variable is empty/unset.
|
||||||
|
"envDefault": func(key, fallback string) string {
|
||||||
|
if v := os.Getenv(key); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
},
|
||||||
|
|
||||||
// expiry converts a duration string like "+1h" to a Unix epoch timestamp string.
|
// expiry converts a duration string like "+1h" to a Unix epoch timestamp string.
|
||||||
// Pass "" or "0" to get "0" (no expiry). Supports Go duration syntax (e.g. "30m", "2h").
|
// Pass "" or "0" to get "0" (no expiry). Supports Go duration syntax (e.g. "30m", "2h").
|
||||||
// Note: time.ParseDuration does not support days (d) or weeks (w).
|
// Note: time.ParseDuration does not support days (d) or weeks (w).
|
||||||
|
|||||||
@@ -56,3 +56,76 @@ modules:
|
|||||||
exec: |
|
exec: |
|
||||||
curl -s -X POST https://slack.com/api/dnd.endSnooze \
|
curl -s -X POST https://slack.com/api/dnd.endSnooze \
|
||||||
-H "Authorization: Bearer {{env "SLACK_TOKEN"}}"
|
-H "Authorization: Bearer {{env "SLACK_TOKEN"}}"
|
||||||
|
|
||||||
|
# OBS Studio — media player and streaming control via obs-cmd
|
||||||
|
#
|
||||||
|
# Requires: obs-cmd (https://github.com/grigio/obs-cmd)
|
||||||
|
# macOS: brew install grigio/obs-cmd/obs-cmd
|
||||||
|
# Linux: cargo install obs-cmd (or download binary from GitHub releases)
|
||||||
|
#
|
||||||
|
# OBS WebSocket must be enabled: Tools → WebSocket Server Settings (on by default in OBS 28+)
|
||||||
|
#
|
||||||
|
# Add to ~/.config/streamdeck-go/.env:
|
||||||
|
# OBS_WEBSOCKET_PASSWORD=your-password
|
||||||
|
# OBS_HOST=localhost (optional, default: localhost)
|
||||||
|
# OBS_PORT=4455 (optional, default: 4455)
|
||||||
|
obs:
|
||||||
|
play:
|
||||||
|
params:
|
||||||
|
source: "Media Source"
|
||||||
|
exec: |
|
||||||
|
/usr/local/bin/obs-cmd --websocket obsws://{{envDefault "OBS_HOST" "localhost"}}:{{envDefault "OBS_PORT" "4455"}}/{{env "OBS_WEBSOCKET_PASSWORD"}} media-input play "{{.source}}"
|
||||||
|
|
||||||
|
pause:
|
||||||
|
params:
|
||||||
|
source: "Media Source"
|
||||||
|
exec: |
|
||||||
|
/usr/local/bin/obs-cmd --websocket obsws://{{envDefault "OBS_HOST" "localhost"}}:{{envDefault "OBS_PORT" "4455"}}/{{env "OBS_WEBSOCKET_PASSWORD"}} media-input pause "{{.source}}"
|
||||||
|
|
||||||
|
stop:
|
||||||
|
params:
|
||||||
|
source: "Media Source"
|
||||||
|
exec: |
|
||||||
|
/usr/local/bin/obs-cmd --websocket obsws://{{envDefault "OBS_HOST" "localhost"}}:{{envDefault "OBS_PORT" "4455"}}/{{env "OBS_WEBSOCKET_PASSWORD"}} media-input stop "{{.source}}"
|
||||||
|
|
||||||
|
restart:
|
||||||
|
params:
|
||||||
|
source: "Media Source"
|
||||||
|
exec: |
|
||||||
|
/usr/local/bin/obs-cmd --websocket obsws://{{envDefault "OBS_HOST" "localhost"}}:{{envDefault "OBS_PORT" "4455"}}/{{env "OBS_WEBSOCKET_PASSWORD"}} media-input restart "{{.source}}"
|
||||||
|
|
||||||
|
toggle_record:
|
||||||
|
exec: |
|
||||||
|
/usr/local/bin/obs-cmd --websocket obsws://{{envDefault "OBS_HOST" "localhost"}}:{{envDefault "OBS_PORT" "4455"}}/{{env "OBS_WEBSOCKET_PASSWORD"}} recording toggle
|
||||||
|
|
||||||
|
toggle_record_pause:
|
||||||
|
exec: |
|
||||||
|
/usr/local/bin/obs-cmd --websocket obsws://{{envDefault "OBS_HOST" "localhost"}}:{{envDefault "OBS_PORT" "4455"}}/{{env "OBS_WEBSOCKET_PASSWORD"}} recording toggle-pause
|
||||||
|
|
||||||
|
is_recording_paused:
|
||||||
|
exec: |
|
||||||
|
/usr/local/bin/obs-cmd --websocket obsws://{{envDefault "OBS_HOST" "localhost"}}:{{envDefault "OBS_PORT" "4455"}}/{{env "OBS_WEBSOCKET_PASSWORD"}} recording status
|
||||||
|
|
||||||
|
toggle_stream:
|
||||||
|
exec: |
|
||||||
|
/usr/local/bin/obs-cmd --websocket obsws://{{envDefault "OBS_HOST" "localhost"}}:{{envDefault "OBS_PORT" "4455"}}/{{env "OBS_WEBSOCKET_PASSWORD"}} streaming toggle
|
||||||
|
|
||||||
|
scene_switch:
|
||||||
|
params:
|
||||||
|
scene: "Scene 1"
|
||||||
|
exec: |
|
||||||
|
/usr/local/bin/obs-cmd --websocket obsws://{{envDefault "OBS_HOST" "localhost"}}:{{envDefault "OBS_PORT" "4455"}}/{{env "OBS_WEBSOCKET_PASSWORD"}} scene switch "{{.scene}}"
|
||||||
|
|
||||||
|
toggle_mute:
|
||||||
|
params:
|
||||||
|
source: "Mic/Aux"
|
||||||
|
exec: |
|
||||||
|
/usr/local/bin/obs-cmd --websocket obsws://{{envDefault "OBS_HOST" "localhost"}}:{{envDefault "OBS_PORT" "4455"}}/{{env "OBS_WEBSOCKET_PASSWORD"}} input toggle-mute "{{.source}}"
|
||||||
|
|
||||||
|
is_recording:
|
||||||
|
exec: |
|
||||||
|
/usr/local/bin/obs-cmd --websocket obsws://{{envDefault "OBS_HOST" "localhost"}}:{{envDefault "OBS_PORT" "4455"}}/{{env "OBS_WEBSOCKET_PASSWORD"}} recording status
|
||||||
|
|
||||||
|
is_streaming:
|
||||||
|
exec: |
|
||||||
|
/usr/local/bin/obs-cmd --websocket obsws://{{envDefault "OBS_HOST" "localhost"}}:{{envDefault "OBS_PORT" "4455"}}/{{env "OBS_WEBSOCKET_PASSWORD"}} streaming status
|
||||||
|
|||||||
Reference in New Issue
Block a user