Complete implementation of auto-show/hide an item in OBS on mouse-focus in XServer

This commit is contained in:
Dmitrii Okunev
2024-10-18 17:28:35 +01:00
parent 38bba4bbd1
commit f390abc6e2
25 changed files with 944 additions and 750 deletions

View File

@@ -60,16 +60,20 @@ $(GOPATH)/bin/pkg-config-wrapper:
sh -c 'cd 3rdparty/amd64/windows && wget https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2024-09-29-12-53/ffmpeg-n7.0.2-19-g45ecf80f0e-win64-gpl-shared-7.0.zip && unzip ffmpeg-n7.0.2-19-g45ecf80f0e-win64-gpl-shared-7.0.zip && rm -f ffmpeg-n7.0.2-19-g45ecf80f0e-win64-gpl-shared-7.0.zip'
streampanel-linux-amd64: builddir
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build $(GOBUILD_FLAGS) -o build/streampanel-linux-amd64 ./cmd/streampanel
$(eval INSTALL_DEST?=build/streampanel-linux-amd64)
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build $(GOBUILD_FLAGS) -o "$(INSTALL_DEST)" ./cmd/streampanel
streampanel-linux-arm64: builddir
CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build $(GOBUILD_FLAGS) -o build/streampanel-linux-arm64 ./cmd/streampanel
$(eval INSTALL_DEST?=build/streampanel-linux-arm64)
CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build $(GOBUILD_FLAGS) -o "$(INSTALL_DEST)" ./cmd/streampanel
streampanel-macos-amd64: builddir
CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build $(GOBUILD_FLAGS) -o build/streampanel-macos-amd64 ./cmd/streampanel
$(eval INSTALL_DEST?=build/streampanel-macos-amd64)
CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build $(GOBUILD_FLAGS) -o "$(INSTALL_DEST)" ./cmd/streampanel
streampanel-macos-arm64: builddir
CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build $(GOBUILD_FLAGS) -o build/streampanel-macos-arm64 ./cmd/streampanel
$(eval INSTALL_DEST?=build/streampanel-macos-arm64)
CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build $(GOBUILD_FLAGS) -o "$(INSTALL_DEST)" ./cmd/streampanel
3rdparty/arm64/termux-packages:
mkdir -p 3rdparty/arm64/

View File

@@ -196,6 +196,9 @@ func main() {
if obsGRPCClose != nil {
defer obsGRPCClose()
}
if err != nil {
log.Fatal(err)
}
obs_grpc.RegisterOBSServer(grpcServer, obsGRPC)
streamd_grpc.RegisterStreamDServer(grpcServer, streamdGRPC)
l.Infof("started server at %s", *listenAddr)

11
go.mod
View File

@@ -39,7 +39,6 @@ require (
dario.cat/mergo v1.0.0 // indirect
fyne.io/systray v1.11.0 // indirect
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 // indirect
github.com/MicahParks/jwkset v0.5.20 // indirect
github.com/MicahParks/keyfunc/v3 v3.3.5 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
@@ -59,7 +58,6 @@ require (
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
github.com/datarhei/gosrt v0.7.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/fatih/color v1.10.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
@@ -81,6 +79,7 @@ require (
github.com/go-ng/sort v0.0.0-20220617173827-2cc7cd04f7c7 // indirect
github.com/go-ng/xatomic v0.0.0-20230519181013-85c0ec87e55f // indirect
github.com/go-ng/xsort v0.0.0-20220617174223-1d146907bccc // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
@@ -138,12 +137,15 @@ require (
github.com/skeema/knownhosts v1.2.2 // indirect
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
github.com/tklauser/go-sysconf v0.3.14 // indirect
github.com/tklauser/numcpus v0.8.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/xaionaro-go/spinlock v0.0.0-20200518175509-30e6d1ce68a1 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
github.com/yuin/goldmark v1.7.1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/yutopp/go-amf0 v0.1.0 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
@@ -173,6 +175,7 @@ require (
require (
fyne.io/fyne/v2 v2.5.0
github.com/AgustinSRG/go-child-process-manager v1.0.1
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802
github.com/DataDog/gostackparse v0.6.0
github.com/DexterLB/mpvipc v0.0.0-20230829142118-145d6eabdc37
github.com/adrg/libvlc-go/v3 v3.1.5
@@ -185,6 +188,7 @@ require (
github.com/blang/mpv v0.0.0-20160810175505-d56d7352e068
github.com/bluenviron/gortsplib/v4 v4.11.0
github.com/chai2010/webp v1.1.1
github.com/davecgh/go-spew v1.1.1
github.com/dustin/go-humanize v1.0.1
github.com/getsentry/sentry-go v0.28.1
github.com/go-git/go-git/v5 v5.12.0
@@ -198,6 +202,7 @@ require (
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.18.0
github.com/sethvargo/go-password v0.3.1
github.com/shirou/gopsutil v3.21.11+incompatible
github.com/sirupsen/logrus v1.9.3
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.9.0
@@ -206,7 +211,7 @@ require (
github.com/xaionaro-go/gorex v0.0.0-20241010205749-bcd59d639c4d
github.com/xaionaro-go/lockmap v0.0.0-20240901172806-e17aea364748
github.com/xaionaro-go/mediamtx v0.0.0-20241009124606-94c22c603970
github.com/xaionaro-go/obs-grpc-proxy v0.0.0-20241009130412-03d201da4f74
github.com/xaionaro-go/obs-grpc-proxy v0.0.0-20241018162120-5faf4e7a684a
github.com/xaionaro-go/timeapiio v0.0.0-20240915203246-b907cf699af3
github.com/xaionaro-go/typing v0.0.0-20221123235249-2229101d38ba
github.com/xaionaro-go/unsafetools v0.0.0-20210722164218-75ba48cf7b3c

15
go.sum
View File

@@ -232,6 +232,8 @@ github.com/go-ng/xmath v0.0.0-20230704233441-028f5ea62335 h1:N17hl+3/Zqxg3SM+33Q
github.com/go-ng/xmath v0.0.0-20230704233441-028f5ea62335/go.mod h1:rmcKNA11zmis1auYtl0UY64vE/4+QKeS07w/htllpSE=
github.com/go-ng/xsort v0.0.0-20220617174223-1d146907bccc h1:VNz633GRJx2/hL0SpBNoNlLid4xtyi7LSJP1kHpD2Fo=
github.com/go-ng/xsort v0.0.0-20220617174223-1d146907bccc/go.mod h1:Pz/V4pxeXP0hjBlXIrm2ehR0GJ0l4Bon3fsOl6TmoJs=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -565,6 +567,8 @@ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sethvargo/go-password v0.3.1 h1:WqrLTjo7X6AcVYfC6R7GtSyuUQR9hGyAj/f1PYQZCJU=
github.com/sethvargo/go-password v0.3.1/go.mod h1:rXofC1zT54N7R8K/h1WDUdkf9BOx5OptoxrMBcrXzvs=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
@@ -608,6 +612,10 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/sunfish-shogi/bufseekio v0.0.0-20210207115823-a4185644b365/go.mod h1:dEzdXgvImkQ3WLI+0KQpmEx8T/C/ma9KeS3AfmU899I=
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
@@ -630,8 +638,8 @@ github.com/xaionaro-go/logrustash v0.0.0-20240804141650-d48034780a5f h1:mMrVrYtH
github.com/xaionaro-go/logrustash v0.0.0-20240804141650-d48034780a5f/go.mod h1:aszOZHoPPSgKwdbJUgonps3MSODqctkNhwQDDwlw0Eg=
github.com/xaionaro-go/mediamtx v0.0.0-20241009124606-94c22c603970 h1:QmbvVR2Jt+I2TTeGef79xhfmlnvvXl+FYEHoYpe7mUY=
github.com/xaionaro-go/mediamtx v0.0.0-20241009124606-94c22c603970/go.mod h1:3J9s+wGt6CV4MDnoXApKEdY3kdc5sd6AYEndLJSAIYI=
github.com/xaionaro-go/obs-grpc-proxy v0.0.0-20241009130412-03d201da4f74 h1:W3nQdfHickUpLouh1Gz/0cE9H6y1pW+vX79pnhB3G4w=
github.com/xaionaro-go/obs-grpc-proxy v0.0.0-20241009130412-03d201da4f74/go.mod h1:exSKIlCibB0ww+ABDwH+YG/iNdqVfdzXBBg5LYxkxGw=
github.com/xaionaro-go/obs-grpc-proxy v0.0.0-20241018162120-5faf4e7a684a h1:PyX7XpLkj+eAwrPMFMGpvZIG4zBfzAfwNhwTtbORqN0=
github.com/xaionaro-go/obs-grpc-proxy v0.0.0-20241018162120-5faf4e7a684a/go.mod h1:exSKIlCibB0ww+ABDwH+YG/iNdqVfdzXBBg5LYxkxGw=
github.com/xaionaro-go/spinlock v0.0.0-20190309154744-55278e21e817/go.mod h1:Nb/15eS0BMty6TMuWgRQM8WCDIUlyPZagcpchHT6c9Y=
github.com/xaionaro-go/spinlock v0.0.0-20200518175509-30e6d1ce68a1 h1:1Kqw9dv2LnznIhJoMt3dNzc/ctSj6VHjyGh4YZHjpE4=
github.com/xaionaro-go/spinlock v0.0.0-20200518175509-30e6d1ce68a1/go.mod h1:UwmTXX+EpoEYHuy0rSys1Rp5PW+eVTgZSjgMVLJENKg=
@@ -656,6 +664,8 @@ github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/yutopp/go-amf0 v0.1.0 h1:a3UeBZG7nRF0zfvmPn2iAfNo1RGzUpHz1VyJD2oGrik=
github.com/yutopp/go-amf0 v0.1.0/go.mod h1:QzDOBr9RV6sQh6E5GFEJROZbU0iQKijORBmprkb3FIk=
github.com/yutopp/go-flv v0.3.1 h1:4ILK6OgCJgUNm2WOjaucWM5lUHE0+sLNPdjq3L0Xtjk=
@@ -855,6 +865,7 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@@ -11,7 +11,7 @@ import (
func init() {
serializable.RegisterType[Noop]()
serializable.RegisterType[OBSElementShowHide]()
serializable.RegisterType[OBSItemShowHide]()
serializable.RegisterType[OBSWindowCaptureSetSource]()
serializable.RegisterType[StartStream]()
serializable.RegisterType[EndStream]()
@@ -24,23 +24,23 @@ type Action interface {
type ValueExpression = expression.Expression
type OBSElementShowHide struct {
ElementName *string `yaml:"element_name,omitempty" json:"element_name,omitempty"`
ElementUUID *string `yaml:"element_uuid,omitempty" json:"element_uuid,omitempty"`
type OBSItemShowHide struct {
ItemName *string `yaml:"item_name,omitempty" json:"item_name,omitempty"`
ItemUUID *string `yaml:"item_uuid,omitempty" json:"item_uuid,omitempty"`
ValueExpression ValueExpression `yaml:"value_expression,omitempty" json:"value_expression,omitempty"`
}
var _ Action = (*OBSElementShowHide)(nil)
var _ Action = (*OBSItemShowHide)(nil)
func (OBSElementShowHide) isAction() {}
func (OBSItemShowHide) isAction() {}
func (a OBSElementShowHide) String() string {
func (a OBSItemShowHide) String() string {
return string(tryJSON(a))
}
type OBSWindowCaptureSetSource struct {
ElementName *string `yaml:"element_name,omitempty" json:"element_name,omitempty"`
ElementUUID *string `yaml:"element_uuid,omitempty" json:"element_uuid,omitempty"`
ItemName *string `yaml:"item_name,omitempty" json:"item_name,omitempty"`
ItemUUID *string `yaml:"item_uuid,omitempty" json:"item_uuid,omitempty"`
ValueExpression ValueExpression `yaml:"value_expression,omitempty" json:"value_expression,omitempty"`
}

View File

@@ -18,13 +18,13 @@ type Event interface {
}
type WindowFocusChange struct {
Host *string `yaml:"host,omitempty" json:"host,omitempty"`
WindowID *uint64 `yaml:"window_id,omitempty" json:"window_id,omitempty"`
WindowTitle *string `yaml:"window_title,omitempty" json:"window_title,omitempty"`
WindowTitlePartial *string `yaml:"window_title_partial,omitempty" json:"window_title_partial,omitempty"`
UserID *uint64 `yaml:"user_id,omitempty" json:"user_id,omitempty"`
//lint:ignore U1000 this field is used by reflection
uiComment struct{} `uicomment:"This action will also add field .IsFocused to the event."`
ProcessID *uint64 `yaml:"process_id,omitempty" json:"process_id,omitempty"`
ProcessName *string `yaml:"process_name,omitempty" json:"process_name,omitempty"`
IsFocused *bool `yaml:"is_focused,omitempty" json:"is_focused,omitempty"`
}
func (ev *WindowFocusChange) Get() Event { return ev }
@@ -35,17 +35,25 @@ func (ev *WindowFocusChange) Match(cmpIface Event) bool {
return false
}
if !fieldMatch(ev.Host, cmp.Host) {
return false
}
if !fieldMatch(ev.WindowID, cmp.WindowID) {
return false
}
if !fieldMatch(ev.WindowTitle, cmp.WindowTitle) {
return false
}
if !partialFieldMatch(ev.WindowTitle, cmp.WindowTitlePartial) &&
!partialFieldMatch(cmp.WindowTitle, ev.WindowTitlePartial) {
if !fieldMatch(ev.UserID, cmp.UserID) {
return false
}
if !fieldMatch(ev.WindowID, cmp.WindowID) {
if !fieldMatch(ev.ProcessID, cmp.ProcessID) {
return false
}
if !fieldMatch(ev.ProcessName, cmp.ProcessName) {
return false
}
if !fieldMatch(ev.IsFocused, cmp.IsFocused) {
return false
}

View File

@@ -1,7 +1,5 @@
package event
import "strings"
func fieldMatch[T comparable](f1 *T, f2 *T) bool {
if f1 == nil || f2 == nil {
return true
@@ -9,11 +7,3 @@ func fieldMatch[T comparable](f1 *T, f2 *T) bool {
return *f1 == *f2
}
func partialFieldMatch(full *string, partial *string) bool {
if full == nil || partial == nil {
return true
}
return strings.Contains(*full, *partial)
}

View File

@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"github.com/davecgh/go-spew/spew"
"github.com/facebookincubator/go-belt/tool/logger"
"github.com/xaionaro-go/streamctl/pkg/expression"
"github.com/xaionaro-go/streamctl/pkg/observability"
@@ -37,13 +38,15 @@ func (d *StreamD) submitEvent(
ctx context.Context,
ev event.Event,
) error {
logger.Debugf(ctx, "submitEvent(ctx, %s)", spew.Sdump(ev))
defer logger.Debugf(ctx, "/submitEvent(ctx, %#v)", spew.Sdump(ev))
exprCtx := objToMap(ev)
for _, rule := range d.Config.TriggerRules {
if rule.EventQuery.Match(ev) {
observability.Go(ctx, func() {
err := d.doAction(ctx, rule.Action, exprCtx)
if err != nil {
logger.Errorf(ctx, "unable to perform action %#+v: %w", rule.Action, err)
logger.Errorf(ctx, "unable to perform action %s: %v", rule.Action, err)
}
})
}
@@ -63,7 +66,7 @@ func (d *StreamD) doAction(
return d.StartStream(ctx, a.PlatID, a.Title, a.Description, a.Profile, a.CustomArgs...)
case *action.EndStream:
return d.EndStream(ctx, a.PlatID)
case *action.OBSElementShowHide:
case *action.OBSItemShowHide:
value, err := expression.Eval[bool](a.ValueExpression, exprCtx)
if err != nil {
return fmt.Errorf("unable to Eval() the expression '%s': %w", a.ValueExpression, err)
@@ -71,8 +74,8 @@ func (d *StreamD) doAction(
return d.OBSElementSetShow(
ctx,
SceneElementIdentifier{
Name: a.ElementName,
UUID: a.ElementUUID,
Name: a.ItemName,
UUID: a.ItemUUID,
},
value,
)

File diff suppressed because it is too large Load Diff

View File

@@ -35,16 +35,16 @@ func ActionGRPC2Go(
}
case *streamd_grpc.Action_ObsAction:
switch o := a.ObsAction.OBSActionOneOf.(type) {
case *streamd_grpc.OBSAction_ElementShowHide:
result = &action.OBSElementShowHide{
ElementName: o.ElementShowHide.ElementName,
ElementUUID: o.ElementShowHide.ElementUUID,
ValueExpression: expression.Expression(o.ElementShowHide.ValueExpression),
case *streamd_grpc.OBSAction_ItemShowHide:
result = &action.OBSItemShowHide{
ItemName: o.ItemShowHide.ItemName,
ItemUUID: o.ItemShowHide.ItemUUID,
ValueExpression: expression.Expression(o.ItemShowHide.ValueExpression),
}
case *streamd_grpc.OBSAction_WindowCaptureSetSource:
result = &action.OBSWindowCaptureSetSource{
ElementName: o.WindowCaptureSetSource.ElementName,
ElementUUID: o.WindowCaptureSetSource.ElementUUID,
ItemName: o.WindowCaptureSetSource.ItemName,
ItemUUID: o.WindowCaptureSetSource.ItemUUID,
ValueExpression: expression.Expression(o.WindowCaptureSetSource.ValueExpression),
}
default:
@@ -82,13 +82,13 @@ func ActionGo2GRPC(
PlatID: string(a.PlatID),
},
}
case *action.OBSElementShowHide:
case *action.OBSItemShowHide:
result.ActionOneof = &streamd_grpc.Action_ObsAction{
ObsAction: &streamd_grpc.OBSAction{
OBSActionOneOf: &streamd_grpc.OBSAction_ElementShowHide{
ElementShowHide: &streamd_grpc.OBSActionElementShowHide{
ElementName: a.ElementName,
ElementUUID: a.ElementUUID,
OBSActionOneOf: &streamd_grpc.OBSAction_ItemShowHide{
ItemShowHide: &streamd_grpc.OBSActionItemShowHide{
ItemName: a.ItemName,
ItemUUID: a.ItemUUID,
ValueExpression: string(a.ValueExpression),
},
},
@@ -99,8 +99,8 @@ func ActionGo2GRPC(
ObsAction: &streamd_grpc.OBSAction{
OBSActionOneOf: &streamd_grpc.OBSAction_WindowCaptureSetSource{
WindowCaptureSetSource: &streamd_grpc.OBSActionWindowCaptureSetSource{
ElementName: a.ElementName,
ElementUUID: a.ElementUUID,
ItemName: a.ItemName,
ItemUUID: a.ItemUUID,
ValueExpression: string(a.ValueExpression),
},
},

View File

@@ -23,10 +23,13 @@ func EventGo2GRPC(in event.Event) (*streamd_grpc.Event, error) {
func triggerGo2GRPCWindowFocusChange(q *event.WindowFocusChange) *streamd_grpc.EventWindowFocusChange {
return &streamd_grpc.EventWindowFocusChange{
Host: q.Host,
WindowID: q.WindowID,
WindowTitle: q.WindowTitle,
WindowTitlePartial: q.WindowTitlePartial,
ProcessID: q.ProcessID,
ProcessName: q.ProcessName,
UserID: q.UserID,
IsFocused: q.IsFocused,
}
}
@@ -43,9 +46,12 @@ func triggerGRPC2GoWindowFocusChange(
q *streamd_grpc.EventWindowFocusChange,
) config.Event {
return &event.WindowFocusChange{
Host: q.Host,
WindowID: q.WindowID,
WindowTitle: q.WindowTitle,
WindowTitlePartial: q.WindowTitlePartial,
UserID: q.UserID,
ProcessID: q.ProcessID,
ProcessName: q.ProcessName,
IsFocused: q.IsFocused,
}
}

View File

@@ -541,21 +541,21 @@ message StreamPlayersChange {}
message NoopRequest {}
message OBSActionElementShowHide {
optional string elementName = 1;
optional string elementUUID = 2;
message OBSActionItemShowHide {
optional string itemName = 1;
optional string itemUUID = 2;
string valueExpression = 3;
}
message OBSActionWindowCaptureSetSource {
optional string elementName = 1;
optional string elementUUID = 2;
optional string itemName = 1;
optional string itemUUID = 2;
string valueExpression = 3;
}
message OBSAction {
oneof OBSActionOneOf {
OBSActionElementShowHide elementShowHide = 1;
OBSActionItemShowHide itemShowHide = 1;
OBSActionWindowCaptureSetSource windowCaptureSetSource = 2;
}
}
@@ -610,10 +610,13 @@ message EventOBSSceneChange {
}
message EventWindowFocusChange {
optional uint64 windowID = 1;
optional string windowTitle = 2;
optional string windowTitlePartial = 3;
optional uint64 userID = 4;
optional string host = 1;
optional uint64 windowID = 2;
optional string windowTitle = 3;
optional uint64 processID = 4;
optional string processName = 5;
optional uint64 userID = 6;
optional bool isFocused = 7;
}
message EventQuery {

View File

@@ -170,5 +170,51 @@ func (d *StreamD) OBSElementSetShow(
elID SceneElementIdentifier,
shouldShow bool,
) error {
return fmt.Errorf("not implemented, yet")
if elID.Name == nil && elID.UUID == nil {
return fmt.Errorf("elID.Name == nil && elID.UUID == nil (which is legit, but unexpected, so we fail just in case)")
}
obsServer, obsServerClose, err := d.OBS(ctx)
if obsServerClose != nil {
defer obsServerClose()
}
if err != nil {
return fmt.Errorf("unable to get a client to OBS: %w", err)
}
sceneListResp, err := obsServer.GetSceneList(ctx, &obs_grpc.GetSceneListRequest{})
if err != nil {
return fmt.Errorf("unable to get the scenes list: %w", err)
}
for _, scene := range sceneListResp.Scenes {
itemList, err := obsServer.GetSceneItemList(ctx, &obs_grpc.GetSceneItemListRequest{
SceneUUID: scene.SceneUUID,
})
if err != nil {
return fmt.Errorf("unable to get the list of items of scene %#+v: %w", scene, err)
}
for _, item := range itemList.GetSceneItems() {
if elID.Name != nil && *elID.Name != item.GetSourceName() {
continue
}
if elID.UUID != nil && *elID.UUID != item.GetSourceUUID() {
continue
}
req := &obs_grpc.SetSceneItemEnabledRequest{
SceneName: scene.SceneName,
SceneUUID: scene.SceneUUID,
SceneItemID: item.SceneItemID,
SceneItemEnabled: shouldShow,
}
_, err := obsServer.SetSceneItemEnabled(ctx, req)
if err != nil {
return fmt.Errorf("unable to submit %#+v: %w", shouldShow, item, err)
}
}
}
return nil
}

115
pkg/streampanel/events.go Normal file
View File

@@ -0,0 +1,115 @@
package streampanel
import (
"context"
"fmt"
"os"
"github.com/davecgh/go-spew/spew"
"github.com/facebookincubator/go-belt/tool/logger"
"github.com/hashicorp/go-multierror"
"github.com/xaionaro-go/streamctl/pkg/observability"
"github.com/xaionaro-go/streamctl/pkg/streamd/config/event"
"github.com/xaionaro-go/streamctl/pkg/windowmanagerhandler"
)
var hostname *string
func init() {
if _hostname, err := os.Hostname(); err == nil {
hostname = &_hostname
}
}
func (p *Panel) initEventSensor(ctx context.Context) {
es, err := newEventSensor()
if err != nil {
p.DisplayError(err)
return
}
observability.Go(ctx, func() {
logger.Debugf(ctx, "eventSensor")
defer logger.Debugf(ctx, "/eventSensor")
es.Loop(ctx, p.StreamD)
})
}
type eventSensor struct {
WMH *windowmanagerhandler.WindowManagerHandler
PreviouslyFocusedWindow *windowmanagerhandler.WindowFocusChange
}
func newEventSensor() (*eventSensor, error) {
wmh, err := windowmanagerhandler.New()
if err != nil {
return nil, fmt.Errorf("unable to init a window manager handler: %w", err)
}
return &eventSensor{
WMH: wmh,
}, nil
}
type submitEventer interface {
SubmitEvent(
ctx context.Context,
event event.Event,
) error
}
func (es *eventSensor) Loop(
ctx context.Context,
eventSubmitter submitEventer,
) {
windowFocusChangeChan := es.WMH.WindowFocusChangeChan(ctx)
for {
select {
case <-ctx.Done():
return
case ev := <-windowFocusChangeChan:
if err := es.submitEventWindowFocusChange(ctx, ev, eventSubmitter); err != nil {
logger.Errorf(ctx, "unable to submit the WindowFocusChange event %#+v: %w", ev, err)
}
}
}
}
func (es *eventSensor) submitEventWindowFocusChange(
ctx context.Context,
ev windowmanagerhandler.WindowFocusChange,
submitEventer submitEventer,
) error {
logger.Debugf(ctx, "submitEventWindowFocusChange(ctx, %s)", spew.Sdump(ev))
defer logger.Debugf(ctx, "/submitEventWindowFocusChange(ctx, %#v)", spew.Sdump(ev))
var err *multierror.Error
if es.PreviouslyFocusedWindow != nil {
ev := es.PreviouslyFocusedWindow
err = multierror.Append(err, submitEventer.SubmitEvent(ctx, &event.WindowFocusChange{
Host: hostname,
WindowID: (*uint64)(ev.WindowID),
WindowTitle: ev.WindowTitle,
UserID: ptr(uint64(*ev.UserID)),
ProcessID: ptr(uint64(*ev.ProcessID)),
ProcessName: ev.ProcessName,
IsFocused: ptr(false),
}))
}
es.PreviouslyFocusedWindow = &ev
err = multierror.Append(err, submitEventer.SubmitEvent(ctx, &event.WindowFocusChange{
Host: hostname,
WindowID: (*uint64)(ev.WindowID),
WindowTitle: ev.WindowTitle,
UserID: ptr(uint64(*ev.UserID)),
ProcessID: ptr(uint64(*ev.ProcessID)),
ProcessName: ev.ProcessName,
IsFocused: ptr(true),
}))
return err.ErrorOrNil()
}

View File

@@ -1,14 +0,0 @@
package streampanel
import (
"time"
"fyne.io/fyne/v2"
)
func hideWindow(w fyne.Window) {
for i := 0; i < 10; i++ {
time.Sleep(time.Millisecond)
w.Hide()
}
}

View File

@@ -231,23 +231,6 @@ func imgFillTo(
return img
}
func imgRotateFillTo(
ctx context.Context,
src image.Image,
size image.Point,
alignX streamdconsts.AlignX,
alignY streamdconsts.AlignY,
) image.Image {
return imgFillTo(
ctx,
src,
size,
size,
image.Point{},
alignX, alignY,
)
}
const (
ScreenshotMaxWidth = 384
ScreenshotMaxHeight = 216

View File

@@ -22,7 +22,6 @@ import (
"github.com/anthonynsimon/bild/adjust"
"github.com/facebookincubator/go-belt/tool/logger"
"github.com/lusingander/colorpicker"
"github.com/xaionaro-go/obs-grpc-proxy/pkg/obsgrpcproxy"
"github.com/xaionaro-go/obs-grpc-proxy/protobuf/go/obs_grpc"
"github.com/xaionaro-go/streamctl/pkg/colorx"
"github.com/xaionaro-go/streamctl/pkg/observability"
@@ -488,50 +487,37 @@ func (p *Panel) editMonitorElementWindow(
var audioSourceNames []string
videoSourceNameIsSet := map[string]struct{}{}
audioSourceNameIsSet := map[string]struct{}{}
for _, _scene := range resp.Scenes {
scene, err := obsgrpcproxy.FromAbstractObject[map[string]any](_scene)
if err != nil {
p.DisplayError(fmt.Errorf("unable to convert scene info: %w", err))
return
}
logger.Debugf(ctx, "scene info: %#+v", scene)
sceneName, _ := scene["sceneName"].(string)
for _, scene := range resp.Scenes {
resp, err := obsServer.GetSceneItemList(ctx, &obs_grpc.GetSceneItemListRequest{
SceneName: &sceneName,
SceneName: scene.SceneName,
})
if err != nil {
p.DisplayError(
fmt.Errorf("unable to get the list of items of scene '%s': %w", sceneName, err),
fmt.Errorf("unable to get the list of items of scene '%s': %w", scene.SceneName, err),
)
return
}
for _, item := range resp.SceneItems {
source, err := obsgrpcproxy.FromAbstractObject[map[string]any](item)
if err != nil {
p.DisplayError(fmt.Errorf("unable to convert source info: %w", err))
return
}
logger.Debugf(ctx, "source info: %#+v", source)
sourceName, _ := source["sourceName"].(string)
logger.Debugf(ctx, "source info: %#+v", item)
func() {
if _, ok := videoSourceNameIsSet[sourceName]; ok {
if _, ok := videoSourceNameIsSet[item.SourceName]; ok {
return
}
sceneItemTransform := source["sceneItemTransform"].(map[string]any)
sourceWidth, _ := sceneItemTransform["sourceWidth"].(float64)
sceneItemTransform := item.SceneItemTransform
sourceWidth := sceneItemTransform.SourceWidth
if sourceWidth == 0 {
return
}
videoSourceNameIsSet[sourceName] = struct{}{}
videoSourceNames = append(videoSourceNames, sourceName)
videoSourceNameIsSet[item.SourceName] = struct{}{}
videoSourceNames = append(videoSourceNames, item.SourceName)
}()
func() {
if _, ok := audioSourceNameIsSet[sourceName]; ok {
if _, ok := audioSourceNameIsSet[item.SourceName]; ok {
return
}
// TODO: filter only audio sources
audioSourceNameIsSet[sourceName] = struct{}{}
audioSourceNames = append(audioSourceNames, sourceName)
audioSourceNameIsSet[item.SourceName] = struct{}{}
audioSourceNames = append(audioSourceNames, item.SourceName)
}()
}
}

View File

@@ -1,21 +0,0 @@
package streampanel
import (
"context"
"fmt"
"github.com/xaionaro-go/streamctl/pkg/streamd/config"
)
func (p *Panel) setStreamDConfig(
ctx context.Context,
cfg *config.Config,
) error {
if err := p.StreamD.SetConfig(ctx, cfg); err != nil {
return fmt.Errorf("unable to set the config: %w", err)
}
if err := p.StreamD.SaveConfig(ctx); err != nil {
return fmt.Errorf("unable to save the config: %w", err)
}
return nil
}

View File

@@ -347,6 +347,7 @@ func (p *Panel) Loop(ctx context.Context, opts ...LoopOption) error {
}
p.reinitScreenshoter(ctx)
p.initEventSensor(ctx)
p.initMainWindow(ctx, initCfg.StartingPage)
if streamDRunErr != nil {
@@ -1630,25 +1631,18 @@ func (p *Panel) getUpdatedStatus_backends_noLock(ctx context.Context) {
return
}
sceneList, err := obsServer.GetSceneList(ctx, &obs_grpc.GetSceneListRequest{})
sceneListResp, err := obsServer.GetSceneList(ctx, &obs_grpc.GetSceneListRequest{})
if err != nil {
p.ReportError(fmt.Errorf("unable to get the list of scene from OBS: %w", err))
p.ReportError(err)
return
}
logger.Tracef(ctx, "OBS SceneList response: %#+v", sceneList)
p.obsSelectScene.Options = p.obsSelectScene.Options[:0]
for _, scene := range sceneList.Scenes {
sceneNameAny := scene.GetFields()["sceneName"]
sceneName := string(sceneNameAny.GetString_())
if sceneName == "" {
p.ReportError(fmt.Errorf("unable to parse the scene name from %#+v", scene))
return
for _, scene := range sceneListResp.Scenes {
p.obsSelectScene.Options = append(p.obsSelectScene.Options, *scene.SceneName)
}
p.obsSelectScene.Options = append(p.obsSelectScene.Options, sceneName)
}
if sceneList.CurrentProgramSceneName != p.obsSelectScene.Selected {
p.obsSelectScene.SetSelected(sceneList.CurrentProgramSceneName)
if sceneListResp.CurrentProgramSceneName != p.obsSelectScene.Selected {
p.obsSelectScene.SetSelected(sceneListResp.CurrentProgramSceneName)
}
})
} else {

View File

@@ -57,6 +57,10 @@ func reflectMakeFieldsFor(
return []fyne.CanvasObject{newReflectField(v, func(i uint64) {
setter(reflect.ValueOf(i).Convert(t))
}, namePrefix)}
case reflect.Bool:
return []fyne.CanvasObject{newReflectField(v, func(i bool) {
setter(reflect.ValueOf(i).Convert(t))
}, namePrefix)}
case reflect.Ptr:
return reflectMakeFieldsFor(v.Elem(), t.Elem(), func(newValue reflect.Value) {
if newValue.IsZero() && v.CanAddr() {

View File

@@ -0,0 +1,5 @@
package windowmanagerhandler
func ptr[T any](in T) *T {
return &in
}

View File

@@ -6,7 +6,7 @@ import (
)
type WindowManagerHandler struct {
*PlatformSpecificWindowManagerHandler
PlatformSpecificWindowManagerHandler
}
func New() (*WindowManagerHandler, error) {
@@ -22,6 +22,9 @@ func (wmh *WindowManagerHandler) WindowFocusChangeChan(ctx context.Context) <-ch
}
type WindowFocusChange struct {
WindowID WindowID
WindowTitle string
WindowID *WindowID
WindowTitle *string
ProcessID *PID
UserID *UID
ProcessName *string
}

View File

@@ -9,6 +9,8 @@ import (
)
type WindowID uint64
type PID int // using the same underlying type as `os` does
type UID int // using the same underlying type as `os` does
type XWMOrWaylandWM interface {
WindowFocusChangeChan(ctx context.Context) <-chan WindowFocusChange

View File

@@ -13,6 +13,7 @@ import (
"github.com/BurntSushi/xgbutil"
"github.com/BurntSushi/xgbutil/ewmh"
"github.com/facebookincubator/go-belt/tool/logger"
"github.com/shirou/gopsutil/process"
"github.com/xaionaro-go/streamctl/pkg/observability"
)
@@ -69,9 +70,36 @@ func (wmh *XWindowManagerHandler) WindowFocusChangeChan(ctx context.Context) <-c
continue
}
pid, err := ewmh.WmPidGet(wmh.XUtil, clientID)
if err != nil {
logger.Errorf(ctx, "unable to get the PID of the active window (%d): %w", clientID, err)
continue
}
proc, err := process.NewProcess(int32(pid))
if err != nil {
logger.Errorf(ctx, "unable to get process info of the active window (%d) using PID %d: %w", clientID, pid, err)
continue
}
uids, err := proc.Uids()
if err != nil {
logger.Errorf(ctx, "unable to get the UIDs of the active window (%d) using PID %d", clientID, pid, err)
continue
}
procName, err := proc.Name()
if err != nil {
logger.Errorf(ctx, "unable to get the process name of the active window (%d) using PID %d", clientID, pid, err)
continue
}
ch <- WindowFocusChange{
WindowID: WindowID(clientID),
WindowTitle: name,
WindowID: ptr(WindowID(clientID)),
WindowTitle: ptr(name),
UserID: ptr(UID(uids[0])),
ProcessID: ptr(PID(pid)),
ProcessName: ptr(procName),
}
}
})

View File

@@ -10,6 +10,8 @@ import (
type PlatformSpecificWindowManagerHandler struct{}
type WindowID struct{}
type PID struct{}
type UID struct{}
func (wmh *WindowManagerHandler) init(context.Context) error {
return fmt.Errorf("the support of window manager handler for this platform is not implemented, yet")