mirror of
https://github.com/xaionaro-go/streamctl.git
synced 2025-09-26 19:41:17 +08:00
Initial commit, pt. 99
This commit is contained in:
@@ -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 {
|
||||
|
@@ -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))
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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() {
|
||||
|
@@ -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
13
go.mod
@@ -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
26
go.sum
@@ -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
57
pkg/colorx/parse.go
Normal 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
19
pkg/colorx/parse_test.go
Normal 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)
|
||||
}
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
|
@@ -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,
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -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) {
|
||||
|
@@ -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
|
||||
}
|
||||
|
Reference in New Issue
Block a user