Initial commit, pt. 99

This commit is contained in:
Dmitrii Okunev
2024-08-26 01:52:15 +01:00
parent cfb5cdf261
commit e2c0ae6cf6
17 changed files with 543 additions and 140 deletions

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"runtime"
"strings"
"time"
"github.com/facebookincubator/go-belt/tool/logger"
"github.com/spf13/pflag"
@@ -25,21 +26,22 @@ func (l *loggerLevel) UnmarshalYAML(b []byte) error {
}
type Flags struct {
LoggerLevel loggerLevel `yaml:"LoggerLevel,omitempty"`
ListenAddr string `yaml:"ListenAddr,omitempty"`
RemoteAddr string `yaml:"RemoteAddr,omitempty"`
ConfigPath string `yaml:"ConfigPath,omitempty"`
NetPprofAddrMain string `yaml:"NetPprofAddrMain,omitempty"`
NetPprofAddrUI string `yaml:"NetPprofAddrUI,omitempty"`
NetPprofAddrStreamD string `yaml:"NetPprofAddrStreamD,omitempty"`
CPUProfile string `yaml:"CPUProfile,omitempty"`
HeapProfile string `yaml:"HeapProfile,omitempty"`
LogstashAddr string `yaml:"LogstashAddr,omitempty"`
SentryDSN string `yaml:"SentryDSN,omitempty"`
Page string `yaml:"Page,omitempty"`
LogFile string `yaml:"LogFile,omitempty"`
Subprocess string `yaml:"Subprocess,omitempty"`
SplitProcess bool `yaml:"SplitProcess,omitempty"`
LoggerLevel loggerLevel `yaml:"LoggerLevel,omitempty"`
ListenAddr string `yaml:"ListenAddr,omitempty"`
RemoteAddr string `yaml:"RemoteAddr,omitempty"`
ConfigPath string `yaml:"ConfigPath,omitempty"`
NetPprofAddrMain string `yaml:"NetPprofAddrMain,omitempty"`
NetPprofAddrUI string `yaml:"NetPprofAddrUI,omitempty"`
NetPprofAddrStreamD string `yaml:"NetPprofAddrStreamD,omitempty"`
CPUProfile string `yaml:"CPUProfile,omitempty"`
HeapProfile string `yaml:"HeapProfile,omitempty"`
LogstashAddr string `yaml:"LogstashAddr,omitempty"`
SentryDSN string `yaml:"SentryDSN,omitempty"`
Page string `yaml:"Page,omitempty"`
LogFile string `yaml:"LogFile,omitempty"`
Subprocess string `yaml:"Subprocess,omitempty"`
SplitProcess bool `yaml:"SplitProcess,omitempty"`
LockTimeout time.Duration `yaml:"LockTimeout,omitempty"`
}
var platformGetFlagsFuncs []func(*Flags)
@@ -72,6 +74,7 @@ func parseFlags() Flags {
logFile := pflag.String("log-file", defaultLogFile, "log file to write logs into")
subprocess := pflag.String("subprocess", "", "[internal use flag] run a specific sub-process (format: processName:addressToConnect)")
splitProcess := pflag.Bool("split-process", !isMobile(), "split the process into multiple processes for better stability")
lockTimeout := pflag.Duration("lock-timeout", 2*time.Minute, "[debug option] change the timeout for locking, before reporting it as a deadlock")
pflag.Parse()
flags := Flags{
@@ -90,6 +93,7 @@ func parseFlags() Flags {
LogFile: *logFile,
Subprocess: *subprocess,
SplitProcess: *splitProcess,
LockTimeout: *lockTimeout,
}
for _, platformGetFlagsFunc := range platformGetFlagsFuncs {

View File

@@ -185,7 +185,7 @@ func runFork(
}
os.Setenv(EnvPassword, password)
args := []string{execPath, "--sentry-dsn=" + flags.SentryDSN, "--log-level=" + logger.Level(flags.LoggerLevel).String(), "--subprocess=" + string(procName) + ":" + addr}
args := []string{execPath, "--sentry-dsn=" + flags.SentryDSN, "--log-level=" + logger.Level(flags.LoggerLevel).String(), "--subprocess=" + string(procName) + ":" + addr, "--logstash-addr=" + flags.LogstashAddr}
logger.Infof(ctx, "running '%s %s'", args[0], strings.Join(args[1:], " "))
cmd := exec.Command(args[0], args[1:]...)
cmd.Stderr = NewLogWriter(ctx, logger.FromCtx(ctx))

View File

@@ -6,7 +6,6 @@ import (
"encoding/hex"
"io"
"os"
"strings"
"time"
"github.com/facebookincubator/go-belt/tool/logger"
@@ -63,25 +62,8 @@ func (l *logWriter) Flush() {
l.Logger.Logf(l.Logger.Level(), "%s", s)
}
func (l *logWriter) write(b []byte) (int, error) {
func (l *logWriter) Write(b []byte) (int, error) {
l.BufferLocker.Lock()
defer l.BufferLocker.Unlock()
return l.Buffer.Write(b)
}
func (l *logWriter) Write(b []byte) (int, error) {
isALogRusLine := false
s := string(b)
if len(s) > 14 {
switch {
case strings.HasPrefix(s, string(hexMustDecode("1b5b33"))):
isALogRusLine = true
case strings.HasPrefix(s, `time="`):
isALogRusLine = true
}
}
if isALogRusLine {
return os.Stderr.Write(b)
}
return l.write(b)
return io.MultiWriter(&l.Buffer, os.Stderr).Write(b)
}

View File

@@ -105,7 +105,7 @@ func initRuntime(
})
deadlock.Opts.LogBuf = NewLogWriter(ctx, l)
deadlock.Opts.DeadlockTimeout = 2 * time.Minute
deadlock.Opts.DeadlockTimeout = flags.LockTimeout
ctx, cancelFn := context.WithCancel(ctx)
return ctx, func() {

View File

@@ -231,6 +231,10 @@ func initGRPCServers(
logger.Panicf(ctx, "failed to listen: %v", err)
}
if streamD == nil {
logger.Panicf(ctx, "streamD is nil")
}
obsGRPC, obsGRPCClose, err := streamD.OBS(ctx)
observability.Go(ctx, func() {
<-ctx.Done()

13
go.mod
View File

@@ -51,9 +51,9 @@ require (
github.com/francoispqt/gojay v1.2.13 // indirect
github.com/fredbi/uri v1.1.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe // indirect
github.com/fyne-io/gl-js v0.0.0-20230506162202-1fdaa286a934 // indirect
github.com/fyne-io/glfw-js v0.0.0-20240101223322-6e1efdc71b7a // indirect
github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 // indirect
github.com/fyne-io/image v0.0.0-20240417123036-dc0ee9e7c964 // indirect
github.com/gen2brain/shm v0.0.0-20230802011745-f2460f5984f7 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
@@ -67,7 +67,7 @@ require (
github.com/go-redis/redis/v7 v7.2.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/go-text/render v0.1.0 // indirect
github.com/go-text/typesetting v0.1.0 // indirect
github.com/go-text/typesetting v0.1.1 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.0.0 // indirect
@@ -100,7 +100,7 @@ require (
github.com/jbenet/goprocess v0.1.4 // indirect
github.com/jeandeaual/go-locale v0.0.0-20240223122105-ce5225dcaa49 // indirect
github.com/jezek/xgb v1.1.0 // indirect
github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.17.7 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
@@ -197,7 +197,7 @@ require (
go.uber.org/zap v1.27.0 // indirect
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a // indirect
golang.org/x/image v0.18.0 // indirect
golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect
golang.org/x/mobile v0.0.0-20240404231514-09dbf07665ed // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.22.0 // indirect
@@ -241,6 +241,7 @@ require (
github.com/kbinani/screenshot v0.0.0-20230812210009-b87d31814237
github.com/libp2p/go-libp2p v0.33.2
github.com/libp2p/go-libp2p-kad-dht v0.25.2
github.com/lusingander/colorpicker v0.7.3
github.com/multiformats/go-multiaddr v0.12.3
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.18.0
@@ -253,7 +254,7 @@ require (
github.com/stretchr/testify v1.9.0
github.com/xaionaro-go/datacounter v1.0.4
github.com/xaionaro-go/lockmap v0.0.0-20240813004618-7ab689a46dec
github.com/xaionaro-go/obs-grpc-proxy v0.0.0-20240818223724-74c08692314f
github.com/xaionaro-go/obs-grpc-proxy v0.0.0-20240826003505-317c16ed8b69
github.com/xaionaro-go/typing v0.0.0-20221123235249-2229101d38ba
github.com/xaionaro-go/unsafetools v0.0.0-20210722164218-75ba48cf7b3c
github.com/yl2chen/cidranger v1.0.2

26
go.sum
View File

@@ -208,12 +208,12 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe h1:A/wiwvQ0CAjPkuJytaD+SsXkPU0asQ+guQEIg1BJGX4=
github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe/go.mod h1:d4clgH0/GrRwWjRzJJQXxT/h1TyuNSfF/X64zb/3Ggg=
github.com/fyne-io/gl-js v0.0.0-20230506162202-1fdaa286a934 h1:dZC5aKobSN07hf71oMivxUmAofFja5GrfPK2rBlttX4=
github.com/fyne-io/gl-js v0.0.0-20230506162202-1fdaa286a934/go.mod h1:d4clgH0/GrRwWjRzJJQXxT/h1TyuNSfF/X64zb/3Ggg=
github.com/fyne-io/glfw-js v0.0.0-20240101223322-6e1efdc71b7a h1:ybgRdYvAHTn93HW79bLiBiJwVL4jVeyGQRZMgImoeWs=
github.com/fyne-io/glfw-js v0.0.0-20240101223322-6e1efdc71b7a/go.mod h1:gsGA2dotD4v0SR6PmPCYvS9JuOeMwAtmfvDE7mbYXMY=
github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 h1:hnLq+55b7Zh7/2IRzWCpiTcAvjv/P8ERF+N7+xXbZhk=
github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2/go.mod h1:eO7W361vmlPOrykIg+Rsh1SZ3tQBaOsfzZhsIOb/Lm0=
github.com/fyne-io/image v0.0.0-20240417123036-dc0ee9e7c964 h1:0pTELtjlVAVGSazfwRNcqTVzqmkWb1GsNozCmmZfdZA=
github.com/fyne-io/image v0.0.0-20240417123036-dc0ee9e7c964/go.mod h1:J9Uunu842kOcTjzQj4Eq8XIDmF55szvT1PTS1cUb1UE=
github.com/gen2brain/shm v0.0.0-20230802011745-f2460f5984f7 h1:VLEKvjGJYAMCXw0/32r9io61tEXnMWDRxMk+peyRVFc=
github.com/gen2brain/shm v0.0.0-20230802011745-f2460f5984f7/go.mod h1:uF6rMu/1nvu+5DpiRLwusA6xB8zlkNoGzKn8lmYONUo=
github.com/getsentry/sentry-go v0.28.1 h1:zzaSm/vHmGllRM6Tpx1492r0YDzauArdBfkJRtY6P5k=
@@ -272,8 +272,8 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEe
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/go-text/render v0.1.0 h1:osrmVDZNHuP1RSu3pNG7Z77Sd2xSbcb/xWytAj9kyVs=
github.com/go-text/render v0.1.0/go.mod h1:jqEuNMenrmj6QRnkdpeaP0oKGFLDNhDkVKwGjsWWYU4=
github.com/go-text/typesetting v0.1.0 h1:vioSaLPYcHwPEPLT7gsjCGDCoYSbljxoHJzMnKwVvHw=
github.com/go-text/typesetting v0.1.0/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI=
github.com/go-text/typesetting v0.1.1 h1:bGAesCuo85nXnEN5LmFMVGAGpGkCPtHrZLi//qD7EJo=
github.com/go-text/typesetting v0.1.1/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI=
github.com/go-text/typesetting-utils v0.0.0-20240329101916-eee87fb235a3 h1:levTnuLLUmpavLGbJYLJA7fQnKeS7P1eCdAlM+vReXk=
github.com/go-text/typesetting-utils v0.0.0-20240329101916-eee87fb235a3/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o=
@@ -479,8 +479,8 @@ github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e h1:LvL4XsI70QxOGHed6yhQtAU34Kx3Qq2wwBzGFKY8zKk=
github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
@@ -543,6 +543,8 @@ github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8S
github.com/libp2p/go-yamux/v4 v4.0.1 h1:FfDR4S1wj6Bw2Pqbc8Uz7pCxeRBPbwsBbEdfwiCypkQ=
github.com/libp2p/go-yamux/v4 v4.0.1/go.mod h1:NWjl8ZTLOGlozrXSOZ/HlfG++39iKNnM5wwmtQP1YB4=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/lusingander/colorpicker v0.7.3 h1:vGiSFQp71JCAWZxkhvnZir6WUVlUOz9mRsOth0JdxXM=
github.com/lusingander/colorpicker v0.7.3/go.mod h1:M9hdkXOQJsB1zvmMg/Qv35G1QU3v3hNEHsjpSMXQZg8=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
@@ -856,8 +858,8 @@ github.com/xaionaro-go/lockmap v0.0.0-20240813004618-7ab689a46dec h1:whndJMZR1SZ
github.com/xaionaro-go/lockmap v0.0.0-20240813004618-7ab689a46dec/go.mod h1:UO+SYZ5JAJGOnNkDycFrFwkaaPeSqAEQUM0TUp9Vb24=
github.com/xaionaro-go/logrustash v0.0.0-20240804141650-d48034780a5f h1:mMrVrYtH9MyCUzBwPvuEntvqdCJ0zifCfqV6bHU6z1M=
github.com/xaionaro-go/logrustash v0.0.0-20240804141650-d48034780a5f/go.mod h1:aszOZHoPPSgKwdbJUgonps3MSODqctkNhwQDDwlw0Eg=
github.com/xaionaro-go/obs-grpc-proxy v0.0.0-20240818223724-74c08692314f h1:fUzyvKYpn8UqPAXuKH22Om1r6x0vJYw5zVbIozheJhM=
github.com/xaionaro-go/obs-grpc-proxy v0.0.0-20240818223724-74c08692314f/go.mod h1:exSKIlCibB0ww+ABDwH+YG/iNdqVfdzXBBg5LYxkxGw=
github.com/xaionaro-go/obs-grpc-proxy v0.0.0-20240826003505-317c16ed8b69 h1:8GDB88wkdpkJWkXgN2D/CKC6OixXfl52WDDLrV/+J0I=
github.com/xaionaro-go/obs-grpc-proxy v0.0.0-20240826003505-317c16ed8b69/go.mod h1:exSKIlCibB0ww+ABDwH+YG/iNdqVfdzXBBg5LYxkxGw=
github.com/xaionaro-go/typing v0.0.0-20221123235249-2229101d38ba h1:wuSIPuGbTeFkv/KSllbIZ511LFam95Y2xpKOq2As6oU=
github.com/xaionaro-go/typing v0.0.0-20221123235249-2229101d38ba/go.mod h1:6wJwFezlmN6Lk38K3eNzGUZ4MmHHzgTAIepvldyiprY=
github.com/xaionaro-go/unsafetools v0.0.0-20210722164218-75ba48cf7b3c h1:WeiZrQbFImtlpYCHdxWpjm033Zm0n1ihx3bD/9b6rYI=
@@ -986,8 +988,8 @@ golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPI
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mobile v0.0.0-20211207041440-4e6c2922fdee/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ=
golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a h1:sYbmY3FwUWCBTodZL1S3JUuOvaW6kM2o+clDzzDNBWg=
golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a/go.mod h1:Ede7gF0KGoHlj822RtphAHK1jLdrcuRBZg0sF1Q+SPc=
golang.org/x/mobile v0.0.0-20240404231514-09dbf07665ed h1:vZhAhVr5zF1IJaVKTawyTq78WSspLnK53iuMJ1fJgLc=
golang.org/x/mobile v0.0.0-20240404231514-09dbf07665ed/go.mod h1:z041I2NhLjANgIfD0XbB2AmUZ8sLUcSgyLaSNGEP50M=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=

57
pkg/colorx/parse.go Normal file
View File

@@ -0,0 +1,57 @@
package colorx
import (
"fmt"
"image/color"
"strings"
)
func Parse(s string) (color.Color, error) {
if len(s) == 0 {
return nil, fmt.Errorf("empty string")
}
if strings.HasPrefix(s, "#") {
return ParseHex(s[1:])
}
// TODO: add support of other formats
return ParseHex(s)
}
func hexToByte(in byte) uint8 {
switch {
case '0' <= in && in <= '9':
return in - '0'
case 'A' <= in && in <= 'F':
return 10 + (in - 'A')
case 'a' <= in && in <= 'f':
return 10 + (in - 'a')
}
panic(fmt.Errorf("unexpected character '%c'", in))
}
func ParseHex(s string) (_ret color.RGBA, _err error) {
defer func() {
if r := recover(); r != nil {
_err = fmt.Errorf("%v", r)
}
}()
switch len(s) {
case 6:
return color.RGBA{
R: hexToByte(s[0])<<4 | hexToByte(s[1]),
G: hexToByte(s[2])<<4 | hexToByte(s[3]),
B: hexToByte(s[4])<<4 | hexToByte(s[5]),
A: 1,
}, nil
case 8:
return color.RGBA{
R: hexToByte(s[0])<<4 | hexToByte(s[1]),
G: hexToByte(s[2])<<4 | hexToByte(s[3]),
B: hexToByte(s[4])<<4 | hexToByte(s[5]),
A: hexToByte(s[6])<<4 | hexToByte(s[7]),
}, nil
}
return color.RGBA{}, fmt.Errorf("unexpected length: %d", len(s))
}

19
pkg/colorx/parse_test.go Normal file
View File

@@ -0,0 +1,19 @@
package colorx
import (
"image/color"
"testing"
"github.com/stretchr/testify/require"
)
func TestParseHex(t *testing.T) {
c, err := ParseHex("010a030F")
require.NoError(t, err)
require.Equal(t, color.RGBA{
R: 1,
G: 0xA,
B: 3,
A: 0xF,
}, c)
}

View File

@@ -17,6 +17,7 @@ type OBS struct {
CurrentStream struct {
EnableRecording bool
}
IsClosed bool
}
var _ streamcontrol.StreamController[StreamProfile] = (*OBS)(nil)
@@ -37,8 +38,16 @@ func New(
}, nil
}
func (obs *OBS) GetClient() (*goobs.Client, error) {
type GetClientOption goobs.Option
func (obs *OBS) GetClient(clientOpts ...GetClientOption) (*goobs.Client, error) {
if obs.IsClosed {
return nil, fmt.Errorf("closed")
}
var opts []goobs.Option
for _, opt := range clientOpts {
opts = append(opts, goobs.Option(opt))
}
if obs.Config.Config.Password != "" {
opts = append(opts, goobs.WithPassword(obs.Config.Config.Password))
}
@@ -49,6 +58,7 @@ func (obs *OBS) GetClient() (*goobs.Client, error) {
}
func (obs *OBS) Close() error {
obs.IsClosed = true
return nil
}

View File

@@ -3,6 +3,7 @@ package config
import (
"context"
"image"
"image/color"
"github.com/anthonynsimon/bild/adjust"
)
@@ -29,7 +30,8 @@ type Filter interface {
}
type FilterColor struct {
Brightness float64
Brightness float64 `yaml:"brightness" json:"brightness"`
Opacity float64 `yaml:"opacity" json:"opacity"`
}
func (f *FilterColor) Filter(
@@ -39,16 +41,55 @@ func (f *FilterColor) Filter(
if f.Brightness != 0 {
img = adjust.Brightness(img, f.Brightness)
}
if f.Opacity != 0 {
img = processImage(img, func(pixel color.Color) color.Color {
switch pixel := pixel.(type) {
case color.RGBA:
pixel.A = uint8(float64(pixel.A) * f.Opacity)
return pixel
default:
r, g, b, a := pixel.RGBA()
a = uint32(float64(a) * f.Opacity)
return color.RGBA{
R: uint8(r),
G: uint8(g),
B: uint8(b),
A: uint8(a),
}
}
})
}
return img
}
func processImage(
img image.Image,
pixelCallback func(pixel color.Color) color.Color,
) image.Image {
size := img.Bounds().Size()
result := image.NewRGBA(image.Rectangle{
Min: image.Point{
X: 0,
Y: 0,
},
Max: size,
})
for x := 0; x < size.X; x++ {
for y := 0; y < size.Y; y++ {
pixel := img.At(x, y)
result.Set(x, y, pixelCallback(pixel))
}
}
return result
}
func (f *FilterColor) MonitorFilterType() MonitorFilterType {
return MonitorFilterTypeColor
}
type serializableFilter struct {
Type MonitorFilterType `yaml:"type"`
Config map[string]any `yaml:"config,omitempty"`
Type MonitorFilterType `yaml:"type" json:"type"`
Config map[string]any `yaml:"config,omitempty" json:"config"`
}
func (s serializableFilter) Unwrap() Filter {

View File

@@ -6,30 +6,34 @@ import (
"encoding/json"
"fmt"
"image"
"image/color"
"image/jpeg"
"image/png"
"math"
"time"
"github.com/chai2010/webp"
"github.com/facebookincubator/go-belt/tool/logger"
"github.com/xaionaro-go/obs-grpc-proxy/protobuf/go/obs_grpc"
"github.com/xaionaro-go/streamctl/pkg/colorx"
"github.com/xaionaro-go/streamctl/pkg/imgb64"
"github.com/xaionaro-go/streamctl/pkg/streamtypes"
)
type MonitorSourceType string
const (
MonitorSourceTypeUndefined = MonitorSourceType("")
MonitorSourceTypeDummy = MonitorSourceType("dummy")
MonitorSourceTypeOBSVideoSource = MonitorSourceType("obs_video")
MonitorSourceTypeOBSVolume = MonitorSourceType("obs_volume")
MonitorSourceTypeUndefined = MonitorSourceType("")
MonitorSourceTypeDummy = MonitorSourceType("dummy")
MonitorSourceTypeOBSVideo = MonitorSourceType("obs_video")
MonitorSourceTypeOBSVolume = MonitorSourceType("obs_volume")
)
func (mst MonitorSourceType) New() Source {
switch mst {
case MonitorSourceTypeDummy:
return &MonitorSourceDummy{}
case MonitorSourceTypeOBSVideoSource:
case MonitorSourceTypeOBSVideo:
return &MonitorSourceOBSVideo{}
case MonitorSourceTypeOBSVolume:
return &MonitorSourceOBSVolume{}
@@ -84,24 +88,25 @@ func (d *Duration) UnmarshalJSON(b []byte) error {
}
type MonitorSourceOBSVideo struct {
Name string `yaml:"name"`
Width float64 `yaml:"width"`
Height float64 `yaml:"height"`
ImageFormat ImageFormat `yaml:"image_format"`
UpdateInterval Duration `yaml:"update_interval"`
Name string `yaml:"name" json:"name"`
Width float64 `yaml:"width" json:"width"`
Height float64 `yaml:"height" json:"height"`
ImageFormat ImageFormat `yaml:"image_format" json:"image_format"`
UpdateInterval Duration `yaml:"update_interval" json:"update_interval"`
}
var _ Source = (*MonitorSourceOBSVideo)(nil)
var _ GetImageByteser = (*MonitorSourceOBSVideo)(nil)
func (*MonitorSourceOBSVideo) SourceType() MonitorSourceType {
return MonitorSourceTypeOBSVideoSource
return MonitorSourceTypeOBSVideo
}
func (s *MonitorSourceOBSVideo) GetImage(
ctx context.Context,
obsServer obs_grpc.OBSServer,
el MonitorElementConfig,
obsState *streamtypes.OBSState,
) (image.Image, time.Time, error) {
b, mimeType, nextUpdateTS, err := s.GetImageBytes(ctx, obsServer, el)
if err != nil {
@@ -156,7 +161,7 @@ func (s *MonitorSourceOBSVideo) GetImageBytes(
}
resp, err := obsServer.GetSourceScreenshot(ctx, req)
if err != nil {
return nil, "", time.Time{}, fmt.Errorf("unable to get a screenshot of '%s': %w", s.Name, err)
return nil, "", time.Now().Add(time.Second), fmt.Errorf("unable to get a screenshot of '%s': %w", s.Name, err)
}
imgB64 := resp.GetImageData()
@@ -170,19 +175,72 @@ func (s *MonitorSourceOBSVideo) GetImageBytes(
}
type MonitorSourceOBSVolume struct {
Name string `yaml:"name"`
Width float64 `yaml:"width"`
Height float64 `yaml:"height"`
Name string `yaml:"name" json:"name"`
UpdateInterval Duration `yaml:"update_interval" json:"update_interval"`
ColorActive string `yaml:"color_active" json:"color_active"`
ColorPassive string `yaml:"color_passive" json:"color_passive"`
}
var _ Source = (*MonitorSourceOBSVolume)(nil)
func (*MonitorSourceOBSVolume) GetImage(
func (s *MonitorSourceOBSVolume) GetImage(
ctx context.Context,
obsServer obs_grpc.OBSServer,
el MonitorElementConfig,
obsState *streamtypes.OBSState,
) (image.Image, time.Time, error) {
return (&MonitorSourceDummy{}).GetImage(ctx, obsServer, el)
if obsState == nil {
return nil, time.Time{}, fmt.Errorf("obsState == nil")
}
obsState.Lock()
volumeMeters := obsState.VolumeMeters[s.Name]
obsState.Unlock()
if len(volumeMeters) == 0 {
return nil, time.Now().Add(time.Second), fmt.Errorf("no data for volume of '%s'", s.Name)
}
var volume float64
for _, s := range volumeMeters {
for _, cmp := range s {
volume = math.Max(volume, cmp)
}
}
img := image.NewRGBA(image.Rectangle{
Min: image.Point{
X: 0,
Y: 0,
},
Max: image.Point{
X: int(el.Width),
Y: int(el.Height),
},
})
colorActive, err := colorx.Parse(s.ColorActive)
if err != nil {
return nil, time.Time{}, fmt.Errorf("unable to parse the `color_active` value '%s': %w", s.ColorActive, err)
}
colorPassive, err := colorx.Parse(s.ColorPassive)
if err != nil {
return nil, time.Time{}, fmt.Errorf("unable to parse the `color_passive` value '%s': %w", s.ColorPassive, err)
}
size := img.Bounds().Size()
for x := 0; x < size.X; x++ {
volumeExpected := float64(x+1) / float64(size.X)
var c color.Color
if volumeExpected <= volume {
c = colorActive
} else {
c = colorPassive
}
for y := 0; y < size.Y; y++ {
img.Set(x, y, c)
}
}
return img, time.Now().Add(time.Duration(s.UpdateInterval)), nil
}
func (*MonitorSourceOBSVolume) SourceType() MonitorSourceType {
@@ -195,6 +253,7 @@ func (*MonitorSourceDummy) GetImage(
ctx context.Context,
obsServer obs_grpc.OBSServer,
el MonitorElementConfig,
obsState *streamtypes.OBSState,
) (image.Image, time.Time, error) {
img := image.NewRGBA(image.Rectangle{
Min: image.Point{
@@ -220,6 +279,7 @@ type Source interface {
ctx context.Context,
obsServer obs_grpc.OBSServer,
el MonitorElementConfig,
obsState *streamtypes.OBSState,
) (image.Image, time.Time, error)
SourceType() MonitorSourceType

View File

@@ -6,7 +6,11 @@ import (
"fmt"
"sort"
"strings"
"time"
"github.com/andreykaipov/goobs"
"github.com/andreykaipov/goobs/api/events"
"github.com/andreykaipov/goobs/api/events/subscriptions"
"github.com/facebookincubator/go-belt/tool/logger"
"github.com/hashicorp/go-multierror"
"github.com/xaionaro-go/streamctl/pkg/streamcontrol"
@@ -253,10 +257,69 @@ func (d *StreamD) initOBSBackend(ctx context.Context) error {
if err != nil {
return err
}
if d.StreamControllers.OBS != nil {
err := d.StreamControllers.OBS.Close()
if err != nil {
logger.Warnf(ctx, "unable to close OBS: %v", err)
}
}
d.StreamControllers.OBS = obs
go d.listenOBSEvents(ctx, obs)
return nil
}
func (d *StreamD) listenOBSEvents(
ctx context.Context,
o *obs.OBS,
) {
for {
if o.IsClosed {
return
}
select {
case <-ctx.Done():
return
default:
}
client, err := o.GetClient(obs.GetClientOption(goobs.WithEventSubscriptions(subscriptions.InputVolumeMeters)))
if err != nil {
logger.Error(ctx, "unable to get an OBS client: %v", err)
time.Sleep(time.Second)
continue
}
func() {
for {
select {
case <-ctx.Done():
return
case ev, ok := <-client.IncomingEvents:
if !ok {
return
}
d.processOBSEvent(ctx, ev)
}
}
}()
}
}
func (d *StreamD) processOBSEvent(
ctx context.Context,
ev any,
) {
logger.Tracef(ctx, "got an OBS event: %T", ev)
switch ev := ev.(type) {
case *events.InputVolumeMeters:
for _, v := range ev.Inputs {
d.OBSState.Lock()
d.OBSState.VolumeMeters[v.Name] = v.Levels
d.OBSState.Unlock()
}
}
}
func (d *StreamD) initTwitchBackend(ctx context.Context) error {
twitch, err := newTwitch(
ctx,

View File

@@ -54,6 +54,8 @@ type SaveConfigFunc func(context.Context, config.Config) error
type OBSInstanceID = streamtypes.OBSInstanceID
type OBSState = streamtypes.OBSState
type StreamD struct {
UI ui.UI
@@ -83,6 +85,7 @@ type StreamD struct {
StreamServer *streamserver.StreamServer
StreamStatusCache *memoize.MemoizeData
OBSState OBSState
EventBus eventbus.Bus
}
@@ -105,6 +108,9 @@ func New(
OAuthListenPorts: map[uint16]struct{}{},
StreamStatusCache: memoize.NewMemoizeData(),
EventBus: eventbus.New(),
OBSState: OBSState{
VolumeMeters: map[string][][3]float64{},
},
}
err := d.readCache(ctx)
@@ -160,16 +166,21 @@ func (d *StreamD) Run(ctx context.Context) (_ret error) { // TODO: delete the fe
return nil
}
func getImageBytes(
func getOBSImageBytes(
ctx context.Context,
obsServer obs_grpc.OBSServer,
el config.MonitorElementConfig,
obsState *streamtypes.OBSState,
) ([]byte, time.Time, error) {
img, nextUpdateAt, err := el.Source.GetImage(ctx, obsServer, el)
img, nextUpdateAt, err := el.Source.GetImage(ctx, obsServer, el, obsState)
if err != nil {
return nil, time.Now().Add(time.Second), fmt.Errorf("unable to get the image from the source: %w", err)
}
for _, filter := range el.Filters {
img = filter.Filter(ctx, img)
}
var out bytes.Buffer
err = webp.Encode(&out, img, &webp.Options{
Lossless: el.ImageLossless,
@@ -226,7 +237,7 @@ func (d *StreamD) initImageTaker(ctx context.Context) error {
}
}
imgBytes, nextUpdateAt, err = getImageBytes(ctx, obsServer, el)
imgBytes, nextUpdateAt, err = getOBSImageBytes(ctx, obsServer, el, &d.OBSState)
if err != nil {
logger.Errorf(ctx, "unable to get the image of '%s': %w", elName, err)
if !waitUntilNextIteration() {
@@ -904,29 +915,34 @@ func (d *StreamD) OBS(
logger.Tracef(ctx, "OBS()")
defer logger.Tracef(ctx, "/OBS()")
proxy := obsgrpcproxy.New(func() (*goobs.Client, context.CancelFunc, error) {
d.ControllersLocker.RLock()
obs := d.StreamControllers.OBS
d.ControllersLocker.RUnlock()
if obs == nil {
return nil, nil, fmt.Errorf("connection to OBS is not initialized")
}
client, err := obs.GetClient()
logger.Tracef(ctx, "getting OBS client result: %v %v", client, err)
if err != nil {
return nil, nil, err
}
return client, func() {
err := client.Disconnect()
if err != nil {
logger.Errorf(ctx, "unable to disconnect from OBS: %w", err)
} else {
logger.Tracef(ctx, "disconnected from OBS")
proxy := obsgrpcproxy.New(
ctx,
func(ctx context.Context) (*goobs.Client, context.CancelFunc, error) {
logger.Tracef(ctx, "OBS proxy getting client")
defer logger.Tracef(ctx, "/OBS proxy getting client")
d.ControllersLocker.RLock()
obs := d.StreamControllers.OBS
d.ControllersLocker.RUnlock()
if obs == nil {
return nil, nil, fmt.Errorf("connection to OBS is not initialized")
}
}, nil
})
client, err := obs.GetClient()
logger.Tracef(ctx, "getting OBS client result: %v %v", client, err)
if err != nil {
return nil, nil, err
}
return client, func() {
err := client.Disconnect()
if err != nil {
logger.Errorf(ctx, "unable to disconnect from OBS: %w", err)
} else {
logger.Tracef(ctx, "disconnected from OBS")
}
}, nil
},
)
return proxy, func() {}, nil
}

View File

@@ -3,6 +3,7 @@ package streampanel
import (
"context"
"fmt"
"image/color"
"runtime"
"sort"
"strconv"
@@ -20,15 +21,16 @@ import (
"fyne.io/fyne/v2/widget"
"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"
"github.com/xaionaro-go/streamctl/pkg/streamcontrol"
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/obs"
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/twitch"
"github.com/xaionaro-go/streamctl/pkg/streamcontrol/youtube"
"github.com/xaionaro-go/streamctl/pkg/streamd/client"
"github.com/xaionaro-go/streamctl/pkg/streamd/config"
streamdconfig "github.com/xaionaro-go/streamctl/pkg/streamd/config"
streamdconsts "github.com/xaionaro-go/streamctl/pkg/streamd/consts"
"github.com/xaionaro-go/streamctl/pkg/streampanel/consts"
@@ -119,7 +121,10 @@ func (p *Panel) updateMonitorPageImages(
})
}
sort.Slice(elements, func(i, j int) bool {
return elements[i].ZIndex < elements[j].ZIndex
if elements[i].ZIndex != elements[j].ZIndex {
return elements[i].ZIndex < elements[j].ZIndex
}
return elements[i].ElementName < elements[j].ElementName
})
var wg sync.WaitGroup
@@ -418,11 +423,20 @@ func (p *Panel) editMonitorElementWindow(
w := p.app.NewWindow("Monitor element settings")
resizeWindow(w, fyne.NewSize(1500, 1000))
obsVideoSource := &streamdconfig.MonitorSourceOBSVideo{}
obsVideoSource := &streamdconfig.MonitorSourceOBSVideo{
UpdateInterval: streamdconfig.Duration(200 * time.Millisecond),
}
obsVolumeSource := &streamdconfig.MonitorSourceOBSVolume{
UpdateInterval: streamdconfig.Duration(200 * time.Millisecond),
ColorActive: "00FF00FF",
ColorPassive: "00000000",
}
dummy := &streamdconfig.MonitorSourceDummy{}
switch source := cfg.Source.(type) {
case *streamdconfig.MonitorSourceOBSVideo:
obsVideoSource = source
case *streamdconfig.MonitorSourceOBSVolume:
obsVolumeSource = source
case *streamdconfig.MonitorSourceDummy:
dummy = source
}
@@ -448,8 +462,10 @@ func (p *Panel) editMonitorElementWindow(
return
}
var sourceNames []string
sourceNameIsSet := map[string]struct{}{}
var videoSourceNames []string
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 {
@@ -473,24 +489,34 @@ func (p *Panel) editMonitorElementWindow(
}
logger.Debugf(ctx, "source info: %#+v", source)
sourceName, _ := source["sourceName"].(string)
if _, ok := sourceNameIsSet[sourceName]; ok {
continue
}
sceneItemTransform := source["sceneItemTransform"].(map[string]any)
sourceWidth, _ := sceneItemTransform["sourceWidth"].(float64)
if sourceWidth == 0 {
continue
}
sourceNameIsSet[sourceName] = struct{}{}
sourceNames = append(sourceNames, sourceName)
func() {
if _, ok := videoSourceNameIsSet[sourceName]; ok {
return
}
sceneItemTransform := source["sceneItemTransform"].(map[string]any)
sourceWidth, _ := sceneItemTransform["sourceWidth"].(float64)
if sourceWidth == 0 {
return
}
videoSourceNameIsSet[sourceName] = struct{}{}
videoSourceNames = append(videoSourceNames, sourceName)
}()
func() {
if _, ok := audioSourceNameIsSet[sourceName]; ok {
return
}
// TODO: filter only audio sources
audioSourceNameIsSet[sourceName] = struct{}{}
audioSourceNames = append(audioSourceNames, sourceName)
}()
}
}
sort.Strings(sourceNames)
sort.Strings(videoSourceNames)
obsSourceSelect := widget.NewSelect(sourceNames, func(s string) {
sourceOBSVideoSelect := widget.NewSelect(videoSourceNames, func(s string) {
obsVideoSource.Name = s
})
obsSourceSelect.SetSelected(obsVideoSource.Name)
sourceOBSVideoSelect.SetSelected(obsVideoSource.Name)
sourceWidth := xfyne.NewNumericalEntry()
sourceWidth.SetText(fmt.Sprintf("%v", obsVideoSource.Width))
@@ -579,13 +605,46 @@ func (p *Panel) editMonitorElementWindow(
isLossless.SetChecked(cfg.ImageLossless)
isLossless.OnChanged(cfg.ImageLossless)
if obsVideoSource.UpdateInterval == 0 {
obsVideoSource.UpdateInterval = config.Duration(200 * time.Millisecond)
brightnessValue := float64(0)
brightness := xfyne.NewNumericalEntry()
brightness.OnChanged = func(s string) {
if s == "" || s == "-" {
s = "0"
}
v, err := strconv.ParseFloat(s, 64)
if err != nil {
p.DisplayError(fmt.Errorf("unable to parse '%s' as a float: %w", s, err))
return
}
brightnessValue = v
}
updateInterval := xfyne.NewNumericalEntry()
updateInterval.SetText(fmt.Sprintf("%v", time.Duration(obsVideoSource.UpdateInterval).Seconds()))
updateInterval.OnChanged = func(s string) {
opacityValue := float64(0)
opacity := xfyne.NewNumericalEntry()
opacity.OnChanged = func(s string) {
if s == "" || s == "-" {
s = "0"
}
v, err := strconv.ParseFloat(s, 64)
if err != nil {
p.DisplayError(fmt.Errorf("unable to parse '%s' as a float: %w", s, err))
return
}
opacityValue = v
}
for _, filter := range cfg.Filters {
switch filter := filter.(type) {
case *streamdconfig.FilterColor:
brightnessValue += filter.Brightness
opacityValue += filter.Opacity
}
}
brightness.SetText(fmt.Sprintf("%f", brightnessValue))
obsVideoUpdateInterval := xfyne.NewNumericalEntry()
obsVideoUpdateInterval.SetText(fmt.Sprintf("%v", time.Duration(obsVideoSource.UpdateInterval).Seconds()))
obsVideoUpdateInterval.OnChanged = func(s string) {
if s == "" || s == "-" {
s = "0.2"
}
@@ -594,7 +653,7 @@ func (p *Panel) editMonitorElementWindow(
p.DisplayError(fmt.Errorf("unable to parse '%s' as a float: %w", s, err))
return
}
obsVideoSource.UpdateInterval = config.Duration(float64(time.Second) * v)
obsVideoSource.UpdateInterval = streamdconfig.Duration(float64(time.Second) * v)
}
zIndex := xfyne.NewNumericalEntry()
@@ -698,34 +757,108 @@ func (p *Panel) editMonitorElementWindow(
cfg.OffsetY = v
}
obsSourceConfig := container.NewVBox(
sourceOBSVideoConfig := container.NewVBox(
widget.NewLabel("Source:"),
obsSourceSelect,
sourceOBSVideoSelect,
widget.NewLabel("Source image size (use '0' for preserving the original size or ratio):"),
container.NewHBox(widget.NewLabel("X:"), sourceWidth, widget.NewLabel(`px`), widget.NewSeparator(), widget.NewLabel("Y:"), sourceHeight, widget.NewLabel(`px`)),
widget.NewLabel("Format:"),
imageFormatSelect,
widget.NewLabel("Update interval:"),
container.NewHBox(updateInterval, widget.NewLabel("seconds")),
container.NewHBox(obsVideoUpdateInterval, widget.NewLabel("seconds")),
)
sourceTypeSelect := widget.NewSelect([]string{"OBS", "dummy"}, func(s string) {
switch s {
case "OBS":
obsSourceConfig.Show()
case "dummy":
obsSourceConfig.Hide()
sourceOBSVolumeSelect := widget.NewSelect(audioSourceNames, func(s string) {
obsVolumeSource.Name = s
})
obsVolumeUpdateInterval := xfyne.NewNumericalEntry()
obsVolumeUpdateInterval.SetText(fmt.Sprintf("%v", time.Duration(obsVideoSource.UpdateInterval).Seconds()))
obsVolumeUpdateInterval.OnChanged = func(s string) {
if s == "" || s == "-" {
s = "0.2"
}
v, err := strconv.ParseFloat(s, 64)
if err != nil {
p.DisplayError(fmt.Errorf("unable to parse '%s' as a float: %w", s, err))
return
}
obsVolumeSource.UpdateInterval = streamdconfig.Duration(float64(time.Second) * v)
}
var volumeColorActiveParsed color.Color
if volumeColorActiveParsed, err = colorx.Parse(obsVolumeSource.ColorActive); err != nil {
volumeColorActiveParsed = color.RGBA{R: 0, G: 255, B: 0, A: 255}
}
volumeColorActive := colorpicker.NewColorSelectModalRect(w, fyne.NewSize(30, 20), volumeColorActiveParsed)
volumeColorActive.SetOnChange(func(c color.Color) {
r32, g32, b32, a32 := c.RGBA()
r8, g8, b8, a8 := uint8(r32>>8), uint8(g32>>8), uint8(b32>>8), uint8(a32>>8)
obsVolumeSource.ColorActive = fmt.Sprintf("%.2X%.2X%.2X%.2X", r8, g8, b8, a8)
})
var volumeColorPassiveParsed color.Color
if volumeColorPassiveParsed, err = colorx.Parse(obsVolumeSource.ColorPassive); err != nil {
volumeColorPassiveParsed = color.RGBA{R: 0, G: 0, B: 0, A: 0}
}
volumeColorPassive := colorpicker.NewColorSelectModalRect(w, fyne.NewSize(30, 20), volumeColorPassiveParsed)
volumeColorPassive.SetOnChange(func(c color.Color) {
r32, g32, b32, a32 := c.RGBA()
r8, g8, b8, a8 := uint8(r32>>8), uint8(g32>>8), uint8(b32>>8), uint8(a32>>8)
obsVolumeSource.ColorPassive = fmt.Sprintf("%.2X%.2X%.2X%.2X", r8, g8, b8, a8)
})
sourceOBSVideoSelect.SetSelected(obsVideoSource.Name)
sourceOBSVolumeConfig := container.NewVBox(
widget.NewLabel("Source:"),
sourceOBSVolumeSelect,
widget.NewLabel("Color active:"),
volumeColorActive,
widget.NewLabel("Color passive:"),
volumeColorPassive,
widget.NewLabel("Update interval:"),
container.NewHBox(obsVolumeUpdateInterval, widget.NewLabel("seconds")),
)
sourceTypeSelect := widget.NewSelect([]string{
string(streamdconfig.MonitorSourceTypeOBSVideo),
string(streamdconfig.MonitorSourceTypeOBSVolume),
string(streamdconfig.MonitorSourceTypeDummy),
}, func(s string) {
switch streamdconfig.MonitorSourceType(s) {
case streamdconfig.MonitorSourceTypeOBSVideo:
sourceOBSVolumeConfig.Hide()
sourceOBSVideoConfig.Show()
case streamdconfig.MonitorSourceTypeOBSVolume:
sourceOBSVideoConfig.Hide()
sourceOBSVolumeConfig.Show()
case streamdconfig.MonitorSourceTypeDummy:
sourceOBSVideoConfig.Hide()
sourceOBSVolumeConfig.Hide()
}
})
sourceTypeSelect.SetSelected("OBS")
sourceTypeSelect.SetSelected(string(streamdconfig.MonitorSourceTypeOBSVideo))
saveButton := widget.NewButtonWithIcon("Save", theme.DocumentSaveIcon(), func() {
switch sourceTypeSelect.Selected {
case "OBS":
if elementName.Text == "" {
p.DisplayError(fmt.Errorf("element name is not set"))
return
}
switch streamdconfig.MonitorSourceType(sourceTypeSelect.Selected) {
case streamdconfig.MonitorSourceTypeOBSVideo:
cfg.Source = obsVideoSource
case "dummy":
case streamdconfig.MonitorSourceTypeOBSVolume:
cfg.Source = obsVolumeSource
case streamdconfig.MonitorSourceTypeDummy:
cfg.Source = dummy
}
cfg.Filters = cfg.Filters[:0]
if brightnessValue != 0 || opacityValue != 0 {
cfg.Filters = append(cfg.Filters, &streamdconfig.FilterColor{
Brightness: brightnessValue,
Opacity: opacityValue,
})
}
err := saveFunc(ctx, elementName.Text, cfg)
if err != nil {
p.DisplayError(fmt.Errorf("unable to save the monitor element: %w", err))
@@ -754,9 +887,14 @@ func (p *Panel) editMonitorElementWindow(
isLossless,
imageQuality,
imageCompression,
widget.NewLabel("Brightness adjustment (-1.0 .. 1.0):"),
brightness,
widget.NewLabel("Opacity multiplier:"),
opacity,
widget.NewLabel("Source type:"),
sourceTypeSelect,
obsSourceConfig,
sourceOBSVideoConfig,
sourceOBSVolumeConfig,
),
)
w.SetContent(content)

View File

@@ -1774,8 +1774,8 @@ func (p *Panel) initMainWindow(
p.monitorPage = container.NewStack(
monitorBackgroundFyne,
p.screenshotContainer,
streamInfoContainer,
p.monitorLayersContainer,
streamInfoContainer,
)
p.obsSelectScene = widget.NewSelect(nil, func(s string) {

View File

@@ -3,6 +3,7 @@ package streamtypes
import (
"encoding/json"
"fmt"
"sync"
"gopkg.in/yaml.v2"
)
@@ -72,3 +73,8 @@ func (t *ServerType) UnmarshalYAML(b []byte) error {
*t = ParseServerType(s)
return nil
}
type OBSState struct {
sync.Mutex
VolumeMeters map[string][][3]float64
}