golines --max-len=60

This commit is contained in:
Dmitrii Okunev
2024-10-16 22:58:55 +01:00
parent df42a4da66
commit fb33738f1c
73 changed files with 1812 additions and 433 deletions

View File

@@ -147,12 +147,24 @@ func streamSetup(cmd *cobra.Command, args []string) {
assertNoError(ctx, err) assertNoError(ctx, err)
if isEnabled[youtube.ID] { if isEnabled[youtube.ID] {
err := streamD.StartStream(ctx, youtube.ID, title, description, cfg.Backends[youtube.ID].StreamProfiles[profileName]) err := streamD.StartStream(
ctx,
youtube.ID,
title,
description,
cfg.Backends[youtube.ID].StreamProfiles[profileName],
)
assertNoError(ctx, err) assertNoError(ctx, err)
} }
if isEnabled[twitch.ID] { if isEnabled[twitch.ID] {
err := streamD.StartStream(ctx, twitch.ID, title, description, cfg.Backends[twitch.ID].StreamProfiles[profileName]) err := streamD.StartStream(
ctx,
twitch.ID,
title,
description,
cfg.Backends[twitch.ID].StreamProfiles[profileName],
)
assertNoError(ctx, err) assertNoError(ctx, err)
} }
} }

View File

@@ -97,7 +97,8 @@ func init() {
StreamStart.PersistentFlags().String("title", "", "stream title") StreamStart.PersistentFlags().String("title", "", "stream title")
StreamStart.PersistentFlags().String("description", "", "stream description") StreamStart.PersistentFlags().String("description", "", "stream description")
StreamStart.PersistentFlags().String("profile", "", "profile") StreamStart.PersistentFlags().String("profile", "", "profile")
StreamStart.PersistentFlags().StringArray("youtube-templates", nil, "the list of templates used to create streams; if nothing is provided, then a stream won't be created") StreamStart.PersistentFlags().
StringArray("youtube-templates", nil, "the list of templates used to create streams; if nothing is provided, then a stream won't be created")
Root.AddCommand(GenerateConfig) Root.AddCommand(GenerateConfig)
Root.AddCommand(SetTitle) Root.AddCommand(SetTitle)
@@ -153,8 +154,12 @@ func generateConfig(cmd *cobra.Command, args []string) {
logger.Panicf(cmd.Context(), "file '%s' already exists", cfgPath) logger.Panicf(cmd.Context(), "file '%s' already exists", cfgPath)
} }
cfg := newConfig() cfg := newConfig()
cfg[idTwitch].StreamProfiles = map[streamcontrol.ProfileName]streamcontrol.AbstractStreamProfile{"some_profile": twitch.StreamProfile{}} cfg[idTwitch].StreamProfiles = map[streamcontrol.ProfileName]streamcontrol.AbstractStreamProfile{
cfg[idYoutube].StreamProfiles = map[streamcontrol.ProfileName]streamcontrol.AbstractStreamProfile{"some_profile": youtube.StreamProfile{}} "some_profile": twitch.StreamProfile{},
}
cfg[idYoutube].StreamProfiles = map[streamcontrol.ProfileName]streamcontrol.AbstractStreamProfile{
"some_profile": youtube.StreamProfile{},
}
err := writeConfigToPath(cmd.Context(), cfgPath, cfg) err := writeConfigToPath(cmd.Context(), cfgPath, cfg)
if err != nil { if err != nil {
logger.Panic(cmd.Context(), err) logger.Panic(cmd.Context(), err)
@@ -203,7 +208,10 @@ func readConfigFromPath(
} }
if (*cfg)[idTwitch] != nil { if (*cfg)[idTwitch] != nil {
err = streamcontrol.ConvertStreamProfiles[twitch.StreamProfile](ctx, (*cfg)[idTwitch].StreamProfiles) err = streamcontrol.ConvertStreamProfiles[twitch.StreamProfile](
ctx,
(*cfg)[idTwitch].StreamProfiles,
)
if err != nil { if err != nil {
return fmt.Errorf("unable to convert stream profiles of twitch: %w: <%s>", err, b) return fmt.Errorf("unable to convert stream profiles of twitch: %w: <%s>", err, b)
} }
@@ -211,11 +219,18 @@ func readConfigFromPath(
} }
if (*cfg)[idYoutube] != nil { if (*cfg)[idYoutube] != nil {
err = streamcontrol.ConvertStreamProfiles[youtube.StreamProfile](ctx, (*cfg)[idYoutube].StreamProfiles) err = streamcontrol.ConvertStreamProfiles[youtube.StreamProfile](
ctx,
(*cfg)[idYoutube].StreamProfiles,
)
if err != nil { if err != nil {
return fmt.Errorf("unable to convert stream profiles of twitch: %w: <%s>", err, b) return fmt.Errorf("unable to convert stream profiles of twitch: %w: <%s>", err, b)
} }
logger.Debugf(ctx, "final stream profiles of youtube: %#+v", (*cfg)[idYoutube].StreamProfiles) logger.Debugf(
ctx,
"final stream profiles of youtube: %#+v",
(*cfg)[idYoutube].StreamProfiles,
)
} }
return nil return nil
@@ -244,7 +259,11 @@ func getTwitchStreamController(
ctx context.Context, ctx context.Context,
cfg streamcontrol.Config, cfg streamcontrol.Config,
) (*twitch.Twitch, error) { ) (*twitch.Twitch, error) {
platCfg := streamcontrol.GetPlatformConfig[twitch.PlatformSpecificConfig, twitch.StreamProfile](ctx, cfg, idTwitch) platCfg := streamcontrol.GetPlatformConfig[twitch.PlatformSpecificConfig, twitch.StreamProfile](
ctx,
cfg,
idTwitch,
)
if platCfg == nil { if platCfg == nil {
logger.Infof(ctx, "twitch config was not found") logger.Infof(ctx, "twitch config was not found")
return nil, nil return nil, nil
@@ -268,7 +287,11 @@ func getYouTubeStreamController(
ctx context.Context, ctx context.Context,
cfg streamcontrol.Config, cfg streamcontrol.Config,
) (*youtube.YouTube, error) { ) (*youtube.YouTube, error) {
platCfg := streamcontrol.GetPlatformConfig[youtube.PlatformSpecificConfig, youtube.StreamProfile](ctx, cfg, idYoutube) platCfg := streamcontrol.GetPlatformConfig[youtube.PlatformSpecificConfig, youtube.StreamProfile](
ctx,
cfg,
idYoutube,
)
if platCfg == nil { if platCfg == nil {
logger.Infof(ctx, "youtube config was not found") logger.Infof(ctx, "youtube config was not found")
return nil, nil return nil, nil
@@ -288,7 +311,10 @@ func getYouTubeStreamController(
) )
} }
func getStreamControllers(ctx context.Context, cfg streamcontrol.Config) streamcontrol.StreamControllers { func getStreamControllers(
ctx context.Context,
cfg streamcontrol.Config,
) streamcontrol.StreamControllers {
var result streamcontrol.StreamControllers var result streamcontrol.StreamControllers
twitch, err := getTwitchStreamController(ctx, cfg) twitch, err := getTwitchStreamController(ctx, cfg)

View File

@@ -39,9 +39,21 @@ const forceNetPProfOnAndroid = true
func main() { func main() {
loggerLevel := logger.LevelWarning loggerLevel := logger.LevelWarning
pflag.Var(&loggerLevel, "log-level", "Log level") pflag.Var(&loggerLevel, "log-level", "Log level")
listenAddr := pflag.String("listen-addr", ":3594", "the address to listen for incoming connections to") listenAddr := pflag.String(
configPath := pflag.String("config-path", "/etc/streamd/streamd.yaml", "the path to the config file") "listen-addr",
netPprofAddr := pflag.String("go-net-pprof-addr", "", "address to listen to for net/pprof requests") ":3594",
"the address to listen for incoming connections to",
)
configPath := pflag.String(
"config-path",
"/etc/streamd/streamd.yaml",
"the path to the config file",
)
netPprofAddr := pflag.String(
"go-net-pprof-addr",
"",
"address to listen to for net/pprof requests",
)
cpuProfile := pflag.String("go-profile-cpu", "", "file to write cpu profile to") cpuProfile := pflag.String("go-profile-cpu", "", "file to write cpu profile to")
heapProfile := pflag.String("go-profile-heap", "", "file to write memory profile to") heapProfile := pflag.String("go-profile-heap", "", "file to write memory profile to")
sentryDSN := pflag.String("sentry-dsn", "", "DSN of a Sentry instance to send error reports") sentryDSN := pflag.String("sentry-dsn", "", "DSN of a Sentry instance to send error reports")
@@ -210,7 +222,13 @@ func main() {
}, },
func(listenPort uint16, platID streamcontrol.PlatformName, authURL string) bool { func(listenPort uint16, platID streamcontrol.PlatformName, authURL string) bool {
logger.Debugf(ctx, "streamd.UI.OpenOAuthURL(%d, %s, '%s')", listenPort, platID, authURL) logger.Debugf(ctx, "streamd.UI.OpenOAuthURL(%d, %s, '%s')", listenPort, platID, authURL)
defer logger.Debugf(ctx, "/streamd.UI.OpenOAuthURL(%d, %s, '%s')", listenPort, platID, authURL) defer logger.Debugf(
ctx,
"/streamd.UI.OpenOAuthURL(%d, %s, '%s')",
listenPort,
platID,
authURL,
)
grpcLocker.Lock() grpcLocker.Lock()
logger.Debugf(ctx, "streamdGRPCLocker.Lock()-ed") logger.Debugf(ctx, "streamdGRPCLocker.Lock()-ed")

View File

@@ -159,7 +159,13 @@ func (ui *UI) oauth2Handler(
defer removeReceiver() defer removeReceiver()
} }
logger.Debugf(ctx, "asking to open the URL '%s' using listen port %d for platform '%s'", arg.AuthURL, arg.ListenPort, platID) logger.Debugf(
ctx,
"asking to open the URL '%s' using listen port %d for platform '%s'",
arg.AuthURL,
arg.ListenPort,
platID,
)
ui.OAuthURLOpenFn(arg.ListenPort, platID, arg.AuthURL) ui.OAuthURLOpenFn(arg.ListenPort, platID, arg.AuthURL)
t := time.NewTicker(time.Hour) t := time.NewTicker(time.Hour)

View File

@@ -63,23 +63,71 @@ func parseFlags() Flags {
defaultLogFile = "" defaultLogFile = ""
} }
pflag.Var(&loggerLevelValue, "log-level", "Log level") pflag.Var(&loggerLevelValue, "log-level", "Log level")
listenAddr := pflag.String("listen-addr", "", "the address to listen for incoming connections to") listenAddr := pflag.String(
remoteAddr := pflag.String("remote-addr", "", "the address (for example 127.0.0.1:3594) of streamd to connect to, instead of running the stream controllers locally") "listen-addr",
"",
"the address to listen for incoming connections to",
)
remoteAddr := pflag.String(
"remote-addr",
"",
"the address (for example 127.0.0.1:3594) of streamd to connect to, instead of running the stream controllers locally",
)
configPath := pflag.String("config-path", "~/.streampanel.yaml", "the path to the config file") configPath := pflag.String("config-path", "~/.streampanel.yaml", "the path to the config file")
netPprofAddrMain := pflag.String("go-net-pprof-addr-main", "", "address to listen to for net/pprof requests by the main process") netPprofAddrMain := pflag.String(
netPprofAddrUI := pflag.String("go-net-pprof-addr-ui", "", "address to listen to for net/pprof requests by the UI process") "go-net-pprof-addr-main",
netPprofAddrStreamD := pflag.String("go-net-pprof-addr-streamd", "", "address to listen to for net/pprof requests by the streamd process") "",
"address to listen to for net/pprof requests by the main process",
)
netPprofAddrUI := pflag.String(
"go-net-pprof-addr-ui",
"",
"address to listen to for net/pprof requests by the UI process",
)
netPprofAddrStreamD := pflag.String(
"go-net-pprof-addr-streamd",
"",
"address to listen to for net/pprof requests by the streamd process",
)
cpuProfile := pflag.String("go-profile-cpu", "", "file to write cpu profile to") cpuProfile := pflag.String("go-profile-cpu", "", "file to write cpu profile to")
heapProfile := pflag.String("go-profile-heap", "", "file to write memory profile to") heapProfile := pflag.String("go-profile-heap", "", "file to write memory profile to")
logstashAddr := pflag.String("logstash-addr", "", "the address of logstash to send logs to (for example: 'tcp://192.168.0.2:5044')") logstashAddr := pflag.String(
"logstash-addr",
"",
"the address of logstash to send logs to (for example: 'tcp://192.168.0.2:5044')",
)
sentryDSN := pflag.String("sentry-dsn", "", "DSN of a Sentry instance to send error reports") sentryDSN := pflag.String("sentry-dsn", "", "DSN of a Sentry instance to send error reports")
page := pflag.String("page", string(consts.PageControl), "DSN of a Sentry instance to send error reports") page := pflag.String(
"page",
string(consts.PageControl),
"DSN of a Sentry instance to send error reports",
)
logFile := pflag.String("log-file", defaultLogFile, "log file to write logs into") 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)") subprocess := pflag.String(
splitProcess := pflag.Bool("split-process", !isMobile(), "split the process into multiple processes for better stability") "subprocess",
lockTimeout := pflag.Duration("lock-timeout", 2*time.Minute, "[debug option] change the timeout for locking, before reporting it as a deadlock") "",
oauthListenPortTwitch := pflag.Uint16("oauth-listen-port-twitch", 8091, "the port that is used for OAuth callbacks while authenticating in Twitch") "[internal use flag] run a specific sub-process (format: processName:addressToConnect)",
oauthListenPortYouTube := pflag.Uint16("oauth-listen-port-youtube", 8092, "the port that is used for OAuth callbacks while authenticating in YouTube") )
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",
)
oauthListenPortTwitch := pflag.Uint16(
"oauth-listen-port-twitch",
8091,
"the port that is used for OAuth callbacks while authenticating in Twitch",
)
oauthListenPortYouTube := pflag.Uint16(
"oauth-listen-port-youtube",
8092,
"the port that is used for OAuth callbacks while authenticating in YouTube",
)
pflag.Parse() pflag.Parse()
@@ -130,7 +178,11 @@ func getFlags(
func(ctx context.Context, source mainprocess.ProcessName, content any) error { func(ctx context.Context, source mainprocess.ProcessName, content any) error {
result, ok := content.(GetFlagsResult) result, ok := content.(GetFlagsResult)
if !ok { if !ok {
return fmt.Errorf("got unexpected type '%T' instead of %T", content, GetFlagsResult{}) return fmt.Errorf(
"got unexpected type '%T' instead of %T",
content,
GetFlagsResult{},
)
} }
flags = result.Flags flags = result.Flags
return nil return nil

View File

@@ -103,5 +103,11 @@ func getFlagsAndroidFromFiles(flags *Flags) {
logger.Errorf(ctx, "unable to unserialize '%s': %v", flagsSerialized, err) logger.Errorf(ctx, "unable to unserialize '%s': %v", flagsSerialized, err)
} }
logger.Debugf(ctx, "successfully parsed file '%s' with content '%s'; now the flags == %#+v", flagsFilePath, flagsSerialized, *flags) logger.Debugf(
ctx,
"successfully parsed file '%s' with content '%s'; now the flags == %#+v",
flagsFilePath,
flagsSerialized,
*flags,
)
} }

View File

@@ -63,7 +63,10 @@ func runSubprocess(
) { ) {
parts := strings.SplitN(subprocessFlag, ":", 2) parts := strings.SplitN(subprocessFlag, ":", 2)
if len(parts) != 2 { if len(parts) != 2 {
logger.Panicf(belt.WithField(preCtx, "process", ""), "expected 2 parts in --subprocess: name and address, separated via a colon") logger.Panicf(
belt.WithField(preCtx, "process", ""),
"expected 2 parts in --subprocess: name and address, separated via a colon",
)
} }
procName := ProcessName(parts[0]) procName := ProcessName(parts[0])
addr := parts[1] addr := parts[1]
@@ -123,7 +126,12 @@ func runSplitProcesses(
case ProcessNameStreamd: case ProcessNameStreamd:
err := m.SendMessage(ctx, ProcessNameUI, StreamDDied{}) err := m.SendMessage(ctx, ProcessNameUI, StreamDDied{})
if err != nil { if err != nil {
logger.Errorf(ctx, "failed to send a StreamDDied message to '%s': %v", ProcessNameUI, err) logger.Errorf(
ctx,
"failed to send a StreamDDied message to '%s': %v",
ProcessNameUI,
err,
)
} }
} }
} }
@@ -189,11 +197,25 @@ func runFork(
ctx, cancelFn := context.WithCancel(ctx) ctx, cancelFn := context.WithCancel(ctx)
os.Setenv(EnvPassword, password) os.Setenv(EnvPassword, password)
args := []string{execPath, "--sentry-dsn=" + flags.SentryDSN, "--log-level=" + logger.Level(flags.LoggerLevel).String(), "--subprocess=" + string(procName) + ":" + addr, "--logstash-addr=" + flags.LogstashAddr} 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:], " ")) logger.Infof(ctx, "running '%s %s'", args[0], strings.Join(args[1:], " "))
cmd := exec.Command(args[0], args[1:]...) cmd := exec.Command(args[0], args[1:]...)
cmd.Stderr = logwriter.NewLogWriter(ctx, logger.FromCtx(ctx).WithField("log_writer_target", "split").WithField("output_type", "stderr")) cmd.Stderr = logwriter.NewLogWriter(
cmd.Stdout = logwriter.NewLogWriter(ctx, logger.FromCtx(ctx).WithField("log_writer_target", "split")) ctx,
logger.FromCtx(ctx).
WithField("log_writer_target", "split").
WithField("output_type", "stderr"),
)
cmd.Stdout = logwriter.NewLogWriter(
ctx,
logger.FromCtx(ctx).WithField("log_writer_target", "split"),
)
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
err = child_process_manager.ConfigureCommand(cmd) err = child_process_manager.ConfigureCommand(cmd)
if err != nil { if err != nil {
@@ -213,7 +235,13 @@ func runFork(
err := cmd.Wait() err := cmd.Wait()
cancelFn() cancelFn()
if err != nil { if err != nil {
logger.Errorf(ctx, "error running '%s %s': %v", args[0], strings.Join(args[1:], " "), err) logger.Errorf(
ctx,
"error running '%s %s': %v",
args[0],
strings.Join(args[1:], " "),
err,
)
} }
}) })
return nil return nil
@@ -251,7 +279,11 @@ func setReadyFor(
func(ctx context.Context, source ProcessName, content any) error { func(ctx context.Context, source ProcessName, content any) error {
_, ok := content.(mainprocess.MessageReadyConfirmed) _, ok := content.(mainprocess.MessageReadyConfirmed)
if !ok { if !ok {
return fmt.Errorf("got unexpected type '%T' instead of %T", content, mainprocess.MessageReadyConfirmed{}) return fmt.Errorf(
"got unexpected type '%T' instead of %T",
content,
mainprocess.MessageReadyConfirmed{},
)
} }
return nil return nil
}, },

View File

@@ -104,7 +104,11 @@ func runPanel(
} }
assert(ctx, panel.StreamD != nil) assert(ctx, panel.StreamD != nil)
listener, grpcServer, streamdGRPC, _ := initGRPCServers(ctx, panel.StreamD, flags.ListenAddr) listener, grpcServer, streamdGRPC, _ := initGRPCServers(
ctx,
panel.StreamD,
flags.ListenAddr,
)
// to erase an oauth request answered locally from "UnansweredOAuthRequests" in the GRPC server: // to erase an oauth request answered locally from "UnansweredOAuthRequests" in the GRPC server:
panel.OnInternallySubmittedOAuthCode = func( panel.OnInternallySubmittedOAuthCode = func(

View File

@@ -86,7 +86,10 @@ func initRuntime(
if netPprofAddr != "" { if netPprofAddr != "" {
observability.Go(ctx, func() { observability.Go(ctx, func() {
http.Handle("/metrics", promhttp.Handler()) // TODO: either split this from pprof argument, or rename the argument (and re-describe it) http.Handle(
"/metrics",
promhttp.Handler(),
) // TODO: either split this from pprof argument, or rename the argument (and re-describe it)
l.Infof("starting to listen for net/pprof requests at '%s'", netPprofAddr) l.Infof("starting to listen for net/pprof requests at '%s'", netPprofAddr)
l.Error(http.ListenAndServe(netPprofAddr, nil)) l.Error(http.ListenAndServe(netPprofAddr, nil))

View File

@@ -31,7 +31,12 @@ func mainProcessSignalHandler(
logger.Debugf(ctx, "interrupting '%s'", name) logger.Debugf(ctx, "interrupting '%s'", name)
err := f.Process.Signal(os.Interrupt) err := f.Process.Signal(os.Interrupt)
if err != nil { if err != nil {
logger.Errorf(ctx, "unable to send Interrupt to '%s': %v", name, err) logger.Errorf(
ctx,
"unable to send Interrupt to '%s': %v",
name,
err,
)
logger.Debugf(ctx, "killing '%s'", name) logger.Debugf(ctx, "killing '%s'", name)
f.Process.Kill() f.Process.Kill()
return return

View File

@@ -115,7 +115,13 @@ func runStreamd(
}, },
func(listenPort uint16, platID streamcontrol.PlatformName, authURL string) bool { func(listenPort uint16, platID streamcontrol.PlatformName, authURL string) bool {
logger.Debugf(ctx, "streamd.UI.OpenOAuthURL(%d, %s, '%s')", listenPort, platID, authURL) logger.Debugf(ctx, "streamd.UI.OpenOAuthURL(%d, %s, '%s')", listenPort, platID, authURL)
defer logger.Debugf(ctx, "/streamd.UI.OpenOAuthURL(%d, %s, '%s')", listenPort, platID, authURL) defer logger.Debugf(
ctx,
"/streamd.UI.OpenOAuthURL(%d, %s, '%s')",
listenPort,
platID,
authURL,
)
streamdGRPCLocker.Lock() streamdGRPCLocker.Lock()
logger.Debugf(ctx, "streamdGRPCLocker.Lock()-ed") logger.Debugf(ctx, "streamdGRPCLocker.Lock()-ed")

View File

@@ -210,7 +210,11 @@ func (m *Manager) handleConnection(
m.unregisterConnection(ctx, sourceName) m.unregisterConnection(ctx, sourceName)
}(regMessage.Source) }(regMessage.Source)
if err := encoder.Encode(RegistrationResult{}); err != nil { if err := encoder.Encode(RegistrationResult{}); err != nil {
err = fmt.Errorf("unable to encode&send the registration result to '%s': %w", regMessage.Source, err) err = fmt.Errorf(
"unable to encode&send the registration result to '%s': %w",
regMessage.Source,
err,
)
logger.Error(ctx, err) logger.Error(ctx, err)
return return
} }
@@ -237,7 +241,13 @@ func (m *Manager) handleConnection(
logger.Tracef(ctx, "waiting for a message from '%s'", regMessage.Source) logger.Tracef(ctx, "waiting for a message from '%s'", regMessage.Source)
decoder := gob.NewDecoder(conn) decoder := gob.NewDecoder(conn)
err := decoder.Decode(&message) err := decoder.Decode(&message)
logger.Tracef(ctx, "getting a message from '%s': %#+v %#+v", regMessage.Source, message, err) logger.Tracef(
ctx,
"getting a message from '%s': %#+v %#+v",
regMessage.Source,
message,
err,
)
select { select {
case <-ctx.Done(): case <-ctx.Done():
logger.Tracef(ctx, "context was closed") logger.Tracef(ctx, "context was closed")
@@ -305,7 +315,12 @@ func (m *Manager) processMessage(
close(errCh) close(errCh)
return err.ErrorOrNil() return err.ErrorOrNil()
case ProcessNameMain: case ProcessNameMain:
logger.Tracef(ctx, "got a message to the main process from '%s': %#+v", source, message.Content) logger.Tracef(
ctx,
"got a message to the main process from '%s': %#+v",
source,
message.Content,
)
switch content := message.Content.(type) { switch content := message.Content.(type) {
case MessageReady: case MessageReady:
var result *multierror.Error var result *multierror.Error
@@ -318,7 +333,13 @@ func (m *Manager) processMessage(
return onReceivedMessage(ctx, source, message.Content) return onReceivedMessage(ctx, source, message.Content)
} }
default: default:
logger.Tracef(ctx, "got a message to '%s' from '%s': %#+v", message.Destination, source, message.Content) logger.Tracef(
ctx,
"got a message to '%s' from '%s': %#+v",
message.Destination,
source,
message.Content,
)
return m.sendMessage(ctx, source, message.Destination, message.Content) return m.sendMessage(ctx, source, message.Destination, message.Content)
} }
} }
@@ -349,7 +370,14 @@ func (m *Manager) sendMessage(
) (_ret error) { ) (_ret error) {
logger.Tracef(ctx, "sending message %#+v from '%s' to '%s'", content, source, destination) logger.Tracef(ctx, "sending message %#+v from '%s' to '%s'", content, source, destination)
defer func() { defer func() {
logger.Tracef(ctx, "/sending message %#+v from '%s' to '%s': %v", content, source, destination, _ret) logger.Tracef(
ctx,
"/sending message %#+v from '%s' to '%s': %v",
content,
source,
destination,
_ret,
)
}() }()
if !m.isExpectedProcess(destination) { if !m.isExpectedProcess(destination) {
@@ -359,7 +387,11 @@ func (m *Manager) sendMessage(
observability.Go(ctx, func() { observability.Go(ctx, func() {
conn, err := m.waitForReadyProcess(ctx, destination, reflect.TypeOf(content)) conn, err := m.waitForReadyProcess(ctx, destination, reflect.TypeOf(content))
if err != nil { if err != nil {
logger.Errorf(ctx, "%v", fmt.Errorf("unable to wait for process '%s': %w", destination, err)) logger.Errorf(
ctx,
"%v",
fmt.Errorf("unable to wait for process '%s': %w", destination, err),
)
return return
} }
@@ -372,7 +404,9 @@ func (m *Manager) sendMessage(
h := m.connLocker.Lock(context.Background(), destination) h := m.connLocker.Lock(context.Background(), destination)
defer h.Unlock() defer h.Unlock()
defer time.Sleep(100 * time.Millisecond) // TODO: Delete this horrible hack (that is introduced to avoid erasing messages in the buffer) defer time.Sleep(
100 * time.Millisecond,
) // TODO: Delete this horrible hack (that is introduced to avoid erasing messages in the buffer)
err = gob.NewEncoder(conn).Encode(message) err = gob.NewEncoder(conn).Encode(message)
if err != nil { if err != nil {
logger.Errorf(ctx, "%v", fmt.Errorf("unable to encode&send message: %w", err)) logger.Errorf(ctx, "%v", fmt.Errorf("unable to encode&send message: %w", err))
@@ -419,7 +453,10 @@ func (m *Manager) waitForReadyProcess(
} }
for { for {
conn, ch, isReady := xsync.DoR3(ctx, &m.connsLocker, func() (net.Conn, chan struct{}, bool) { conn, ch, isReady := xsync.DoR3(
ctx,
&m.connsLocker,
func() (net.Conn, chan struct{}, bool) {
readyMap := m.childReadyFor[name] readyMap := m.childReadyFor[name]
isReady := false isReady := false
if readyMap != nil { if readyMap != nil {
@@ -428,14 +465,25 @@ func (m *Manager) waitForReadyProcess(
} }
} }
return m.conns[name], m.connsChanged, isReady return m.conns[name], m.connsChanged, isReady
}) },
)
if conn != nil && isReady { if conn != nil && isReady {
logger.Debugf(ctx, "waitForReadyProcess(ctx, '%s', '%s'): waiting is complete", name, msgType) logger.Debugf(
ctx,
"waitForReadyProcess(ctx, '%s', '%s'): waiting is complete",
name,
msgType,
)
return conn, nil return conn, nil
} }
logger.Debugf(ctx, "waitForReadyProcess(ctx, '%s', '%s'): waiting for a change in connections", name, msgType) logger.Debugf(
ctx,
"waitForReadyProcess(ctx, '%s', '%s'): waiting for a change in connections",
name,
msgType,
)
<-ch <-ch
} }
} }
@@ -452,7 +500,12 @@ func (m *Manager) registerConnection(
conn net.Conn, conn net.Conn,
) error { ) error {
logger.Debugf(ctx, "registerConnection(ctx, '%s', %s)", sourceName, conn.RemoteAddr().String()) logger.Debugf(ctx, "registerConnection(ctx, '%s', %s)", sourceName, conn.RemoteAddr().String())
defer logger.Debugf(ctx, "/registerConnection(ctx, '%s', %s)", sourceName, conn.RemoteAddr().String()) defer logger.Debugf(
ctx,
"/registerConnection(ctx, '%s', %s)",
sourceName,
conn.RemoteAddr().String(),
)
if !m.isExpectedProcess(sourceName) { if !m.isExpectedProcess(sourceName) {
return fmt.Errorf("process '%s' is not ever expected", sourceName) return fmt.Errorf("process '%s' is not ever expected", sourceName)
} }
@@ -550,7 +603,9 @@ func (m *Manager) SendMessagePreReady(
} }
h := m.connLocker.Lock(context.Background(), dst) h := m.connLocker.Lock(context.Background(), dst)
defer h.Unlock() defer h.Unlock()
defer time.Sleep(100 * time.Millisecond) // TODO: Delete this horrible hack (that is introduced to avoid erasing messages in the buffer) defer time.Sleep(
100 * time.Millisecond,
) // TODO: Delete this horrible hack (that is introduced to avoid erasing messages in the buffer)
err = encoder.Encode(msg) err = encoder.Encode(msg)
logger.Tracef(ctx, "sending message %#+v: %v", msg, err) logger.Tracef(ctx, "sending message %#+v: %v", msg, err)
if err != nil { if err != nil {

View File

@@ -66,19 +66,25 @@ func Test(t *testing.T) {
c0, err := NewClient("child0", m.Addr().String(), m.Password()) c0, err := NewClient("child0", m.Addr().String(), m.Password())
require.NoError(t, err) require.NoError(t, err)
defer c0.Close() defer c0.Close()
go c0.Serve(belt.WithField(ctx, "process", "child0"), func(ctx context.Context, source ProcessName, content any) error { go c0.Serve(
belt.WithField(ctx, "process", "child0"),
func(ctx context.Context, source ProcessName, content any) error {
handleCall("child0", content) handleCall("child0", content)
return nil return nil
}) },
)
c0.SendMessage(ctx, "main", MessageReady{}) c0.SendMessage(ctx, "main", MessageReady{})
c1, err := NewClient("child1", m.Addr().String(), m.Password()) c1, err := NewClient("child1", m.Addr().String(), m.Password())
require.NoError(t, err) require.NoError(t, err)
defer c1.Close() defer c1.Close()
go c1.Serve(belt.WithField(ctx, "process", "child1"), func(ctx context.Context, source ProcessName, content any) error { go c1.Serve(
belt.WithField(ctx, "process", "child1"),
func(ctx context.Context, source ProcessName, content any) error {
handleCall("child1", content) handleCall("child1", content)
return nil return nil
}) },
)
c1.SendMessage(ctx, "main", MessageReady{}) c1.SendMessage(ctx, "main", MessageReady{})
_, err = NewClient("child2", m.Addr().String(), m.Password()) _, err = NewClient("child2", m.Addr().String(), m.Password())

View File

@@ -49,7 +49,10 @@ func OAuth2HandlerViaBrowser(ctx context.Context, arg OAuthHandlerArgument) erro
return err return err
} }
fmt.Printf("Your browser has been launched (URL: %s).\nPlease approve the permissions.\n", arg.AuthURL) fmt.Printf(
"Your browser has been launched (URL: %s).\nPlease approve the permissions.\n",
arg.AuthURL,
)
// Wait for the web server to get the code. // Wait for the web server to get the code.
code := <-codeCh code := <-codeCh
@@ -75,7 +78,11 @@ func NewCodeReceiver(
fmt.Fprintf(w, "No code received :(\r\n\r\nYou can close this browser window.") fmt.Fprintf(w, "No code received :(\r\n\r\nYou can close this browser window.")
return return
} }
fmt.Fprintf(w, "Received code: %v\r\n\r\nYou can now safely close this browser window.", code) fmt.Fprintf(
w,
"Received code: %v\r\n\r\nYou can now safely close this browser window.",
code,
)
}), }),
} }

View File

@@ -75,7 +75,11 @@ func NewErrorMonitorLoggerHook(
var _ loggertypes.PreHook = (*ErrorMonitorLoggerHook)(nil) var _ loggertypes.PreHook = (*ErrorMonitorLoggerHook)(nil)
func (h *ErrorMonitorLoggerHook) ProcessInput(traceIDs belt.TraceIDs, level loggertypes.Level, args ...any) loggertypes.PreHookResult { func (h *ErrorMonitorLoggerHook) ProcessInput(
traceIDs belt.TraceIDs,
level loggertypes.Level,
args ...any,
) loggertypes.PreHookResult {
if level > loggertypes.LevelWarning { if level > loggertypes.LevelWarning {
return loggertypes.PreHookResult{ return loggertypes.PreHookResult{
Skip: false, Skip: false,
@@ -93,7 +97,12 @@ func (h *ErrorMonitorLoggerHook) ProcessInput(traceIDs belt.TraceIDs, level logg
} }
} }
func (h *ErrorMonitorLoggerHook) ProcessInputf(traceIDs belt.TraceIDs, level loggertypes.Level, format string, args ...any) loggertypes.PreHookResult { func (h *ErrorMonitorLoggerHook) ProcessInputf(
traceIDs belt.TraceIDs,
level loggertypes.Level,
format string,
args ...any,
) loggertypes.PreHookResult {
if level > loggertypes.LevelWarning { if level > loggertypes.LevelWarning {
return loggertypes.PreHookResult{ return loggertypes.PreHookResult{
Skip: false, Skip: false,
@@ -110,7 +119,13 @@ func (h *ErrorMonitorLoggerHook) ProcessInputf(traceIDs belt.TraceIDs, level log
Skip: false, Skip: false,
} }
} }
func (h *ErrorMonitorLoggerHook) ProcessInputFields(traceIDs belt.TraceIDs, level loggertypes.Level, message string, fields field.AbstractFields) loggertypes.PreHookResult {
func (h *ErrorMonitorLoggerHook) ProcessInputFields(
traceIDs belt.TraceIDs,
level loggertypes.Level,
message string,
fields field.AbstractFields,
) loggertypes.PreHookResult {
if level > loggertypes.LevelWarning { if level > loggertypes.LevelWarning {
return loggertypes.PreHookResult{ return loggertypes.PreHookResult{
Skip: false, Skip: false,

View File

@@ -41,7 +41,11 @@ func main() {
loggerLevel := logger.LevelInfo loggerLevel := logger.LevelInfo
pflag.Var(&loggerLevel, "log-level", "Log level") pflag.Var(&loggerLevel, "log-level", "Log level")
mpvPath := pflag.String("mpv", "mpv", "path to mpv") mpvPath := pflag.String("mpv", "mpv", "path to mpv")
backend := pflag.String("backend", backends[0], "player backend, supported values: "+strings.Join(backends, ", ")) backend := pflag.String(
"backend",
backends[0],
"player backend, supported values: "+strings.Join(backends, ", "),
)
pflag.Parse() pflag.Parse()
l := logrus.Default().WithLevel(loggerLevel) l := logrus.Default().WithLevel(loggerLevel)

View File

@@ -119,7 +119,18 @@ func (p *MPV) execMPV(ctx context.Context) (_err error) {
err := os.Remove(socketPath) err := os.Remove(socketPath)
logger.Debugf(ctx, "socket deletion result: '%s': %v", socketPath, err) logger.Debugf(ctx, "socket deletion result: '%s': %v", socketPath, err)
args := []string{p.PathToMPV, "--idle", "--keep-open=always", "--keep-open-pause=no", "--no-hidpi-window-scale", "--no-osc", "--no-osd-bar", "--window-scale=1", "--input-ipc-server=" + socketPath, fmt.Sprintf("--title=%s", p.Title)} args := []string{
p.PathToMPV,
"--idle",
"--keep-open=always",
"--keep-open-pause=no",
"--no-hidpi-window-scale",
"--no-osc",
"--no-osd-bar",
"--window-scale=1",
"--input-ipc-server=" + socketPath,
fmt.Sprintf("--title=%s", p.Title),
}
switch observability.LogLevelFilter.GetLevel() { switch observability.LogLevelFilter.GetLevel() {
case logger.LevelPanic, logger.LevelFatal: case logger.LevelPanic, logger.LevelFatal:
args = append(args, "--msg-level=all=no") args = append(args, "--msg-level=all=no")
@@ -135,8 +146,18 @@ func (p *MPV) execMPV(ctx context.Context) (_err error) {
logger.Debugf(ctx, "running command '%s %s'", args[0], strings.Join(args[1:], " ")) logger.Debugf(ctx, "running command '%s %s'", args[0], strings.Join(args[1:], " "))
cmd := exec.Command(args[0], args[1:]...) cmd := exec.Command(args[0], args[1:]...)
cmd.Stdout = logwriter.NewLogWriter(ctx, logger.FromCtx(ctx).WithField("log_writer_target", "mpv").WithField("output_type", "stdout")) cmd.Stdout = logwriter.NewLogWriter(
cmd.Stderr = logwriter.NewLogWriter(ctx, logger.FromCtx(ctx).WithField("log_writer_target", "mpv").WithField("output_type", "stderr")) ctx,
logger.FromCtx(ctx).
WithField("log_writer_target", "mpv").
WithField("output_type", "stdout"),
)
cmd.Stderr = logwriter.NewLogWriter(
ctx,
logger.FromCtx(ctx).
WithField("log_writer_target", "mpv").
WithField("output_type", "stderr"),
)
err = child_process_manager.ConfigureCommand(cmd) err = child_process_manager.ConfigureCommand(cmd)
errmon.ObserveErrorCtx(ctx, err) errmon.ObserveErrorCtx(ctx, err)
err = cmd.Start() err = cmd.Start()

View File

@@ -34,7 +34,12 @@ func (r *Recoder) StartRecoding(
return xsync.DoR1(ctx, &r.Locker, func() error { return xsync.DoR1(ctx, &r.Locker, func() error {
relay := rtmprelay.NewRtmpRelay(&input.URL, &output.URL) relay := rtmprelay.NewRtmpRelay(&input.URL, &output.URL)
if err := relay.Start(); err != nil { if err := relay.Start(); err != nil {
return fmt.Errorf("unable to start RTMP relay from '%s' to '%s': %w", input.URL, output.URL, err) return fmt.Errorf(
"unable to start RTMP relay from '%s' to '%s': %w",
input.URL,
output.URL,
err,
)
} }
r.Relay = relay r.Relay = relay

View File

@@ -55,7 +55,11 @@ func NewOutputFromURL(
Closer: astikit.NewCloser(), Closer: astikit.NewCloser(),
} }
formatContext, err := astiav.AllocOutputFormatContext(nil, formatFromScheme(url.Scheme), url.String()) formatContext, err := astiav.AllocOutputFormatContext(
nil,
formatFromScheme(url.Scheme),
url.String(),
)
if err != nil { if err != nil {
return nil, fmt.Errorf("allocating output format context failed: %w", err) return nil, fmt.Errorf("allocating output format context failed: %w", err)
} }
@@ -69,7 +73,10 @@ func NewOutputFromURL(
// if output is a file: // if output is a file:
if !output.FormatContext.OutputFormat().Flags().Has(astiav.IOFormatFlagNofile) { if !output.FormatContext.OutputFormat().Flags().Has(astiav.IOFormatFlagNofile) {
logger.Tracef(ctx, "destination '%s' is a file", url.String()) logger.Tracef(ctx, "destination '%s' is a file", url.String())
ioContext, err := astiav.OpenIOContext(url.String(), astiav.NewIOContextFlags(astiav.IOContextFlagWrite)) ioContext, err := astiav.OpenIOContext(
url.String(),
astiav.NewIOContextFlags(astiav.IOContextFlagWrite),
)
if err != nil { if err != nil {
log.Fatal(fmt.Errorf("main: opening io context failed: %w", err)) log.Fatal(fmt.Errorf("main: opening io context failed: %w", err))
} }

View File

@@ -35,7 +35,9 @@ func New(
WaiterChan: make(chan struct{}), WaiterChan: make(chan struct{}),
RecoderConfig: cfg, RecoderConfig: cfg,
} }
close(result.WaiterChan) // to prevent Wait() from blocking when the process is not started, yet. close(
result.WaiterChan,
) // to prevent Wait() from blocking when the process is not started, yet.
return result return result
} }

View File

@@ -109,7 +109,11 @@ func (srv *GRPCServer) newInputByURL(
config := recoder.InputConfig{} config := recoder.InputConfig{}
input, err := recoder.NewInputFromURL(ctx, path.Url.Url, path.Url.AuthKey, config) input, err := recoder.NewInputFromURL(ctx, path.Url.Url, path.Url.AuthKey, config)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to initialize an input using URL '%s' and config %#+v", path.Url, config) return nil, fmt.Errorf(
"unable to initialize an input using URL '%s' and config %#+v",
path.Url,
config,
)
} }
inputID := xsync.DoR1(ctx, &srv.InputLocker, func() InputID { inputID := xsync.DoR1(ctx, &srv.InputLocker, func() InputID {
@@ -163,7 +167,11 @@ func (srv *GRPCServer) newOutputByURL(
config := recoder.OutputConfig{} config := recoder.OutputConfig{}
output, err := recoder.NewOutputFromURL(ctx, path.Url.Url, path.Url.AuthKey, config) output, err := recoder.NewOutputFromURL(ctx, path.Url.Url, path.Url.AuthKey, config)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to initialize an output using URL '%s' and config %#+v", path.Url, config) return nil, fmt.Errorf(
"unable to initialize an output using URL '%s' and config %#+v",
path.Url,
config,
)
} }
outputID := xsync.DoR1(ctx, &srv.OutputLocker, func() OutputID { outputID := xsync.DoR1(ctx, &srv.OutputLocker, func() OutputID {

View File

@@ -41,7 +41,11 @@ func (r *Recoder) NewInputFromPublisher(
) (recoder.Input, error) { ) (recoder.Input, error) {
publisher, ok := publisherIface.(*xaionarogortmp.Pubsub) publisher, ok := publisherIface.(*xaionarogortmp.Pubsub)
if !ok { if !ok {
return nil, fmt.Errorf("expected a publisher or type %T, but received %T", publisherIface, publisher) return nil, fmt.Errorf(
"expected a publisher or type %T, but received %T",
publisherIface,
publisher,
)
} }
return &Input{ return &Input{
@@ -85,7 +89,11 @@ func (r *Recoder) NewInputFromURL(
}, },
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("got an error on command 'Connect' to the input endpoint '%s': %w", urlString, err) return nil, fmt.Errorf(
"got an error on command 'Connect' to the input endpoint '%s': %w",
urlString,
err,
)
} }
return nil, fmt.Errorf("not implemented, yet") return nil, fmt.Errorf("not implemented, yet")

View File

@@ -49,7 +49,12 @@ func newRTMPClient(
dialFunc = rtmp.Dial dialFunc = rtmp.Dial
case "rtmps": case "rtmps":
dialFunc = func(protocol, addr string, config *rtmp.ConnConfig) (*rtmp.ClientConn, error) { dialFunc = func(protocol, addr string, config *rtmp.ConnConfig) (*rtmp.ClientConn, error) {
return rtmp.TLSDial(protocol, addr, config, http.DefaultTransport.(*http.Transport).TLSClientConfig) return rtmp.TLSDial(
protocol,
addr,
config,
http.DefaultTransport.(*http.Transport).TLSClientConfig,
)
} }
default: default:
return nil, fmt.Errorf("unexpected scheme '%s' in URL '%s'", url.Scheme, url.String()) return nil, fmt.Errorf("unexpected scheme '%s' in URL '%s'", url.Scheme, url.String())

View File

@@ -73,7 +73,11 @@ func (r *Recoder) StartRecoding(
return fmt.Errorf("recoding is already running") return fmt.Errorf("recoding is already running")
} }
stream, err := output.Client.CreateStream(ctx, &rtmpmsg.NetConnectionCreateStream{}, chunkSize) stream, err := output.Client.CreateStream(
ctx,
&rtmpmsg.NetConnectionCreateStream{},
chunkSize,
)
if err != nil { if err != nil {
return fmt.Errorf("unable to create a stream on the remote side: %w", err) return fmt.Errorf("unable to create a stream on the remote side: %w", err)
} }

View File

@@ -176,7 +176,8 @@ func (g *GIT) push(
if err == git.NoErrAlreadyUpToDate { if err == git.NoErrAlreadyUpToDate {
return nil return nil
} }
if err != nil && strings.Contains(err.Error(), "is at") && strings.Contains(err.Error(), "but expected") { if err != nil && strings.Contains(err.Error(), "is at") &&
strings.Contains(err.Error(), "but expected") {
return ErrNeedsRebase{Err: err} return ErrNeedsRebase{Err: err}
} }
if err != nil { if err != nil {
@@ -200,7 +201,11 @@ func (g *GIT) Read() ([]byte, error) {
b, err := io.ReadAll(f) b, err := io.ReadAll(f)
f.Close() f.Close()
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to read the content of file '%s' from the virtual git repository: %w", g.FilePath, err) return nil, fmt.Errorf(
"unable to read the content of file '%s' from the virtual git repository: %w",
g.FilePath,
err,
)
} }
return b, nil return b, nil
@@ -271,11 +276,21 @@ func (g *GIT) Pull(
logger.Debugf(ctx, "git is already in sync: %s == %s", lastKnownCommitHash, newCommitHash) logger.Debugf(ctx, "git is already in sync: %s == %s", lastKnownCommitHash, newCommitHash)
return nil return nil
} }
logger.Debugf(ctx, "got a different commit from git: %s != %s", lastKnownCommitHash, newCommitHash) logger.Debugf(
ctx,
"got a different commit from git: %s != %s",
lastKnownCommitHash,
newCommitHash,
)
oldCommit, _ := g.Repo.CommitObject(newCommitHash) oldCommit, _ := g.Repo.CommitObject(newCommitHash)
if oldCommit != nil { if oldCommit != nil {
logger.Debugf(ctx, "we already have this commit in the history on our side, skipping it: %s: %#+v", newCommitHash, oldCommit) logger.Debugf(
ctx,
"we already have this commit in the history on our side, skipping it: %s: %#+v",
newCommitHash,
oldCommit,
)
return nil return nil
} }
@@ -301,7 +316,11 @@ func (g *GIT) CommitAndPush(
_, err := worktree.Add(g.FilePath) _, err := worktree.Add(g.FilePath)
if err != nil { if err != nil {
return plumbing.Hash{}, fmt.Errorf("unable to add file '%s' to the git's worktree: %w", g.FilePath, err) return plumbing.Hash{}, fmt.Errorf(
"unable to add file '%s' to the git's worktree: %w",
g.FilePath,
err,
)
} }
now := time.Now() now := time.Now()
@@ -312,12 +331,15 @@ func (g *GIT) CommitAndPush(
return plumbing.Hash{}, fmt.Errorf("unable to determine the host name: %w", err) return plumbing.Hash{}, fmt.Errorf("unable to determine the host name: %w", err)
} }
hash, err := worktree.Commit(fmt.Sprintf("Update from '%s' at %s", host, ts), &git.CommitOptions{ hash, err := worktree.Commit(
fmt.Sprintf("Update from '%s' at %s", host, ts),
&git.CommitOptions{
All: true, All: true,
AllowEmptyCommits: false, AllowEmptyCommits: false,
Author: signature, Author: signature,
Committer: signature, Committer: signature,
}) },
)
if err != nil { if err != nil {
return hash, fmt.Errorf("unable to commit the new config to the git repo: %w", err) return hash, fmt.Errorf("unable to commit the new config to the git repo: %w", err)
} }
@@ -387,7 +409,11 @@ func (g *GIT) Write(
) )
logger.Debugf(ctx, "file open result: %v", err) logger.Debugf(ctx, "file open result: %v", err)
if err != nil && !os.IsNotExist(err) { if err != nil && !os.IsNotExist(err) {
return plumbing.Hash{}, fmt.Errorf("unable to open file '%s' for reading: %w", g.FilePath, err) return plumbing.Hash{}, fmt.Errorf(
"unable to open file '%s' for reading: %w",
g.FilePath,
err,
)
} }
var sha1SumBefore [sha1.Size]byte var sha1SumBefore [sha1.Size]byte
@@ -420,12 +446,19 @@ func (g *GIT) Write(
0644, 0644,
) )
if err != nil { if err != nil {
return plumbing.Hash{}, fmt.Errorf("unable to open file '%s' for writing: %w", g.FilePath, err) return plumbing.Hash{}, fmt.Errorf(
"unable to open file '%s' for writing: %w",
g.FilePath,
err,
)
} }
_, err = io.Copy(f, bytes.NewReader(b)) _, err = io.Copy(f, bytes.NewReader(b))
f.Close() f.Close()
if err != nil { if err != nil {
return plumbing.Hash{}, fmt.Errorf("unable to write the config into virtual git repo: %w", err) return plumbing.Hash{}, fmt.Errorf(
"unable to write the config into virtual git repo: %w",
err,
)
} }
hash, err := g.CommitAndPush(ctx, worktree, ref) hash, err := g.CommitAndPush(ctx, worktree, ref)

View File

@@ -10,7 +10,11 @@ import (
func Screenshot(cfg Config) (*image.RGBA, error) { func Screenshot(cfg Config) (*image.RGBA, error) {
activeDisplays := screenshot.NumActiveDisplays() activeDisplays := screenshot.NumActiveDisplays()
if cfg.DisplayID >= uint(activeDisplays) { if cfg.DisplayID >= uint(activeDisplays) {
return nil, fmt.Errorf("display ID %d is out of range (max: %d)", cfg.DisplayID, activeDisplays-1) return nil, fmt.Errorf(
"display ID %d is out of range (max: %d)",
cfg.DisplayID,
activeDisplays-1,
)
} }
rgbaFull, err := screenshot.CaptureDisplay(int(cfg.DisplayID)) rgbaFull, err := screenshot.CaptureDisplay(int(cfg.DisplayID))

View File

@@ -130,10 +130,14 @@ var _ yaml.BytesUnmarshaler = (*RawMessage)(nil)
var _ yaml.BytesMarshaler = (*RawMessage)(nil) var _ yaml.BytesMarshaler = (*RawMessage)(nil)
func (RawMessage) GetParent() (ProfileName, bool) { func (RawMessage) GetParent() (ProfileName, bool) {
panic("the value is not parsed; don't use the platform config directly, and use function GetPlatformConfig instead") panic(
"the value is not parsed; don't use the platform config directly, and use function GetPlatformConfig instead",
)
} }
func (RawMessage) GetOrder() int { func (RawMessage) GetOrder() int {
panic("the value is not parsed; don't use the platform config directly, and use function GetPlatformConfig instead") panic(
"the value is not parsed; don't use the platform config directly, and use function GetPlatformConfig instead",
)
} }
func (m *RawMessage) UnmarshalJSON(b []byte) error { func (m *RawMessage) UnmarshalJSON(b []byte) error {
@@ -192,7 +196,11 @@ func (cfg *Config) UnmarshalYAML(b []byte) (_err error) {
t := map[PlatformName]*unparsedPlatformConfig{} t := map[PlatformName]*unparsedPlatformConfig{}
err := yaml.Unmarshal(b, &t) err := yaml.Unmarshal(b, &t)
if err != nil { if err != nil {
return fmt.Errorf("unable to unmarshal YAML of the root of the config: %w; config: <%s>", err, b) return fmt.Errorf(
"unable to unmarshal YAML of the root of the config: %w; config: <%s>",
err,
b,
)
} }
if *cfg == nil { if *cfg == nil {
@@ -316,7 +324,9 @@ func GetPlatformSpecificConfig[T any](
} }
} }
func GetStreamProfiles[S StreamProfile](streamProfiles map[ProfileName]AbstractStreamProfile) StreamProfiles[S] { func GetStreamProfiles[S StreamProfile](
streamProfiles map[ProfileName]AbstractStreamProfile,
) StreamProfiles[S] {
s := make(map[ProfileName]S, len(streamProfiles)) s := make(map[ProfileName]S, len(streamProfiles))
for k, p := range streamProfiles { for k, p := range streamProfiles {
switch p := p.(type) { switch p := p.(type) {
@@ -345,7 +355,9 @@ func InitConfig[T any, S StreamProfile](cfg Config, id PlatformName, platCfg Pla
} }
} }
func ToAbstractStreamProfiles[S StreamProfile](in map[ProfileName]S) map[ProfileName]AbstractStreamProfile { func ToAbstractStreamProfiles[S StreamProfile](
in map[ProfileName]S,
) map[ProfileName]AbstractStreamProfile {
m := make(map[ProfileName]AbstractStreamProfile, len(in)) m := make(map[ProfileName]AbstractStreamProfile, len(in))
for k, v := range in { for k, v := range in {
m[k] = v m[k] = v

View File

@@ -10,7 +10,11 @@ type ErrInvalidStreamProfileType struct {
var _ error = ErrInvalidStreamProfileType{} var _ error = ErrInvalidStreamProfileType{}
func (e ErrInvalidStreamProfileType) Error() string { func (e ErrInvalidStreamProfileType) Error() string {
return fmt.Sprintf("received an invalid stream profile type: expected:%T, received:%T", e.Expected, e.Received) return fmt.Sprintf(
"received an invalid stream profile type: expected:%T, received:%T",
e.Expected,
e.Received,
)
} }
type ErrNoStreamControllerForProfile struct { type ErrNoStreamControllerForProfile struct {

View File

@@ -213,7 +213,9 @@ func (obs *OBS) GetStreamStatus(
var startedAt *time.Time var startedAt *time.Time
if streamStatus.OutputActive { if streamStatus.OutputActive {
startedAt = ptr(time.Now().Add(-time.Duration(streamStatus.OutputDuration * float64(time.Millisecond)))) startedAt = ptr(
time.Now().Add(-time.Duration(streamStatus.OutputDuration * float64(time.Millisecond))),
)
} }
return &streamcontrol.StreamStatus{ return &streamcontrol.StreamStatus{

View File

@@ -71,26 +71,44 @@ func (sr SceneRule) MarshalYAML() (b []byte, _err error) {
triggerBytes, err := yaml.Marshal(sr.TriggerQuery) triggerBytes, err := yaml.Marshal(sr.TriggerQuery)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to serialize the trigger %T:%#+v: %w", sr.TriggerQuery, sr.TriggerQuery, err) return nil, fmt.Errorf(
"unable to serialize the trigger %T:%#+v: %w",
sr.TriggerQuery,
sr.TriggerQuery,
err,
)
} }
triggerMap := map[string]any{} triggerMap := map[string]any{}
err = yaml.Unmarshal(triggerBytes, &triggerMap) err = yaml.Unmarshal(triggerBytes, &triggerMap)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to unserialize the trigger '%s' into a map: %w", triggerBytes, err) return nil, fmt.Errorf(
"unable to unserialize the trigger '%s' into a map: %w",
triggerBytes,
err,
)
} }
triggerMap["type"] = registry.ToTypeName(sr.TriggerQuery) triggerMap["type"] = registry.ToTypeName(sr.TriggerQuery)
actionBytes, err := yaml.Marshal(sr.Action) actionBytes, err := yaml.Marshal(sr.Action)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to serialize the action %T:%#+v: %w", sr.Action, sr.Action, err) return nil, fmt.Errorf(
"unable to serialize the action %T:%#+v: %w",
sr.Action,
sr.Action,
err,
)
} }
actionMap := map[string]any{} actionMap := map[string]any{}
err = yaml.Unmarshal(actionBytes, &actionMap) err = yaml.Unmarshal(actionBytes, &actionMap)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to unserialize the action '%s' into a map: %w", actionBytes, err) return nil, fmt.Errorf(
"unable to unserialize the action '%s' into a map: %w",
actionBytes,
err,
)
} }
actionMap["type"] = registry.ToTypeName(sr.Action) actionMap["type"] = registry.ToTypeName(sr.Action)

View File

@@ -120,7 +120,13 @@ type StreamController[ProfileType StreamProfile] interface {
StreamControllerCommons StreamControllerCommons
ApplyProfile(ctx context.Context, profile ProfileType, customArgs ...any) error ApplyProfile(ctx context.Context, profile ProfileType, customArgs ...any) error
StartStream(ctx context.Context, title string, description string, profile ProfileType, customArgs ...any) error StartStream(
ctx context.Context,
title string,
description string,
profile ProfileType,
customArgs ...any,
) error
} }
type AbstractStreamController interface { type AbstractStreamController interface {

View File

@@ -42,7 +42,9 @@ func New(
return nil, fmt.Errorf("'channel' is not set") return nil, fmt.Errorf("'channel' is not set")
} }
if cfg.Config.ClientID == "" || cfg.Config.ClientSecret == "" { if cfg.Config.ClientID == "" || cfg.Config.ClientSecret == "" {
return nil, fmt.Errorf("'clientid' or/and 'clientsecret' is/are not set; go to https://dev.twitch.tv/console/apps/create and create an app if it not created, yet") return nil, fmt.Errorf(
"'clientid' or/and 'clientsecret' is/are not set; go to https://dev.twitch.tv/console/apps/create and create an app if it not created, yet",
)
} }
getPortsFn := cfg.Config.GetOAuthListenPorts getPortsFn := cfg.Config.GetOAuthListenPorts
@@ -92,7 +94,10 @@ func New(
errmon.ObserveErrorCtx(ctx, err) errmon.ObserveErrorCtx(ctx, err)
now := time.Now() now := time.Now()
if now.Sub(prevTokenUpdate) < time.Second*30 { if now.Sub(prevTokenUpdate) < time.Second*30 {
logger.Errorf(ctx, "updating the token too often, most likely it won't help, so asking to re-authenticate") logger.Errorf(
ctx,
"updating the token too often, most likely it won't help, so asking to re-authenticate",
)
t.prepareLocker.Do(ctx, func() { t.prepareLocker.Do(ctx, func() {
t.client.SetAppAccessToken("") t.client.SetAppAccessToken("")
t.client.SetUserAccessToken("") t.client.SetUserAccessToken("")
@@ -121,7 +126,10 @@ func getUserID(
return "", fmt.Errorf("unable to query user info: %w", err) return "", fmt.Errorf("unable to query user info: %w", err)
} }
if len(resp.Data.Users) != 1 { if len(resp.Data.Users) != 1 {
return "", fmt.Errorf("expected 1 user with login, but received %d users", len(resp.Data.Users)) return "", fmt.Errorf(
"expected 1 user with login, but received %d users",
len(resp.Data.Users),
)
} }
return resp.Data.Users[0].ID, nil return resp.Data.Users[0].ID, nil
} }
@@ -146,7 +154,12 @@ func (t *Twitch) prepareNoLock(ctx context.Context) error {
if err != nil { if err != nil {
return return
} }
logger.Debugf(ctx, "broadcaster_id: %s (login: %s)", t.broadcasterID, t.config.Config.Channel) logger.Debugf(
ctx,
"broadcaster_id: %s (login: %s)",
t.broadcasterID,
t.config.Config.Channel,
)
}) })
return err return err
} }
@@ -171,7 +184,13 @@ func (t *Twitch) editChannelInfo(
return fmt.Errorf("unable to update the channel info (%#+v): %w", *params, err) return fmt.Errorf("unable to update the channel info (%#+v): %w", *params, err)
} }
if resp.ErrorStatus != 0 { if resp.ErrorStatus != 0 {
return fmt.Errorf("unable to update the channel info (%#+v), the response reported an error: %d %v: %v", *params, resp.ErrorStatus, resp.Error, resp.ErrorMessage) return fmt.Errorf(
"unable to update the channel info (%#+v), the response reported an error: %d %v: %v",
*params,
resp.ErrorStatus,
resp.Error,
resp.ErrorMessage,
)
} }
logger.Debugf(ctx, "success") logger.Debugf(ctx, "success")
return nil return nil
@@ -219,7 +238,10 @@ func (t *Twitch) ApplyProfile(
if profile.CategoryName != nil { if profile.CategoryName != nil {
if profile.CategoryID != nil { if profile.CategoryID != nil {
logger.Warnf(ctx, "both category name and ID are set; these are contradicting stream profile settings; prioritizing the name") logger.Warnf(
ctx,
"both category name and ID are set; these are contradicting stream profile settings; prioritizing the name",
)
} }
categoryID, err := t.getCategoryID(ctx, *profile.CategoryName) categoryID, err := t.getCategoryID(ctx, *profile.CategoryName)
if err == nil { if err == nil {
@@ -237,7 +259,10 @@ func (t *Twitch) ApplyProfile(
if tag == "" { if tag == "" {
continue continue
} }
tag = truncateStringByByteLength(tag, 25) // see also: https://github.com/twitchdev/issues/issues/789 tag = truncateStringByByteLength(
tag,
25,
) // see also: https://github.com/twitchdev/issues/issues/789
tags = append(tags, tag) tags = append(tags, tag)
} }
@@ -247,7 +272,10 @@ func (t *Twitch) ApplyProfile(
if tags != nil { if tags != nil {
logger.Debugf(ctx, "has tags") logger.Debugf(ctx, "has tags")
if len(tags) == 0 { if len(tags) == 0 {
logger.Warnf(ctx, "unfortunately, there is a bug in the helix lib, which does not allow to set zero tags, so adding tag 'stream' to the list of tags as a placeholder") logger.Warnf(
ctx,
"unfortunately, there is a bug in the helix lib, which does not allow to set zero tags, so adding tag 'stream' to the list of tags as a placeholder",
)
params.Tags = []string{"English"} params.Tags = []string{"English"}
} else { } else {
params.Tags = tags params.Tags = tags
@@ -294,7 +322,11 @@ func (t *Twitch) getCategoryID(
Names: []string{categoryName}, Names: []string{categoryName},
}) })
if err != nil { if err != nil {
return "", fmt.Errorf("unable to query the category info (of name '%s'): %w", categoryName, err) return "", fmt.Errorf(
"unable to query the category info (of name '%s'): %w",
categoryName,
err,
)
} }
if len(resp.Data.Games) != 1 { if len(resp.Data.Games) != 1 {
@@ -360,7 +392,10 @@ func (t *Twitch) StartStream(
result = multierror.Append(result, fmt.Errorf("unable to set description: %w", err)) result = multierror.Append(result, fmt.Errorf("unable to set description: %w", err))
} }
if err := t.ApplyProfile(ctx, profile, customArgs...); err != nil { if err := t.ApplyProfile(ctx, profile, customArgs...); err != nil {
result = multierror.Append(result, fmt.Errorf("unable to apply the stream-specific profile: %w", err)) result = multierror.Append(
result,
fmt.Errorf("unable to apply the stream-specific profile: %w", err),
)
} }
return multierror.Append(result).ErrorOrNil() return multierror.Append(result).ErrorOrNil()
} }
@@ -614,7 +649,12 @@ func (t *Twitch) getNewTokenByUser(
return fmt.Errorf("unable to get user access token: %w", err) return fmt.Errorf("unable to get user access token: %w", err)
} }
if resp.ErrorStatus != 0 { if resp.ErrorStatus != 0 {
return fmt.Errorf("unable to query: %d %v: %v", resp.ErrorStatus, resp.Error, resp.ErrorMessage) return fmt.Errorf(
"unable to query: %d %v: %v",
resp.ErrorStatus,
resp.Error,
resp.ErrorMessage,
)
} }
t.client.SetUserAccessToken(resp.Data.AccessToken) t.client.SetUserAccessToken(resp.Data.AccessToken)
t.client.SetRefreshToken(resp.Data.RefreshToken) t.client.SetRefreshToken(resp.Data.RefreshToken)
@@ -637,7 +677,12 @@ func (t *Twitch) getNewTokenByApp(
return fmt.Errorf("unable to get app access token: %w", err) return fmt.Errorf("unable to get app access token: %w", err)
} }
if resp.ErrorStatus != 0 { if resp.ErrorStatus != 0 {
return fmt.Errorf("unable to get app access token (the response contains an error): %d %v: %v", resp.ErrorStatus, resp.Error, resp.ErrorMessage) return fmt.Errorf(
"unable to get app access token (the response contains an error): %d %v: %v",
resp.ErrorStatus,
resp.Error,
resp.ErrorMessage,
)
} }
logger.Debugf(ctx, "setting the app access token") logger.Debugf(ctx, "setting the app access token")
t.client.SetAppAccessToken(resp.Data.AccessToken) t.client.SetAppAccessToken(resp.Data.AccessToken)
@@ -735,7 +780,8 @@ func (t *Twitch) GetAllCategories(
} }
pagination = &resp.Data.Pagination pagination = &resp.Data.Pagination
logger.FromCtx(ctx).Tracef("I have %d categories now; new categories: %d", len(categoriesMap), newCategoriesCount) logger.FromCtx(ctx).
Tracef("I have %d categories now; new categories: %d", len(categoriesMap), newCategoriesCount)
} }
logger.FromCtx(ctx).Tracef("%d categories in total") logger.FromCtx(ctx).Tracef("%d categories in total")

View File

@@ -53,7 +53,9 @@ func New(
saveCfgFn func(Config) error, saveCfgFn func(Config) error,
) (*YouTube, error) { ) (*YouTube, error) {
if cfg.Config.ClientID == "" || cfg.Config.ClientSecret == "" { if cfg.Config.ClientID == "" || cfg.Config.ClientSecret == "" {
return nil, fmt.Errorf("'clientid' or/and 'clientsecret' is/are not set; go to https://console.cloud.google.com/apis/credentials and create an app if it not created, yet") return nil, fmt.Errorf(
"'clientid' or/and 'clientsecret' is/are not set; go to https://console.cloud.google.com/apis/credentials and create an app if it not created, yet",
)
} }
ctx, cancelFn := context.WithCancel(ctx) ctx, cancelFn := context.WithCancel(ctx)
@@ -430,7 +432,10 @@ func (yt *YouTube) ApplyProfile(
) error { ) error {
return yt.updateActiveBroadcasts(ctx, func(broadcast *youtube.LiveBroadcast) error { return yt.updateActiveBroadcasts(ctx, func(broadcast *youtube.LiveBroadcast) error {
if broadcast.Snippet == nil { if broadcast.Snippet == nil {
return fmt.Errorf("YouTube have not provided the current snippet of broadcast %v", broadcast.Id) return fmt.Errorf(
"YouTube have not provided the current snippet of broadcast %v",
broadcast.Id,
)
} }
setProfile(broadcast, profile) setProfile(broadcast, profile)
return nil return nil
@@ -443,7 +448,10 @@ func (yt *YouTube) SetTitle(
) error { ) error {
return yt.updateActiveBroadcasts(ctx, func(broadcast *youtube.LiveBroadcast) error { return yt.updateActiveBroadcasts(ctx, func(broadcast *youtube.LiveBroadcast) error {
if broadcast.Snippet == nil { if broadcast.Snippet == nil {
return fmt.Errorf("YouTube have not provided the current snippet of broadcast %v", broadcast.Id) return fmt.Errorf(
"YouTube have not provided the current snippet of broadcast %v",
broadcast.Id,
)
} }
setTitle(broadcast, title) setTitle(broadcast, title)
return nil return nil
@@ -456,7 +464,10 @@ func (yt *YouTube) SetDescription(
) error { ) error {
return yt.updateActiveBroadcasts(ctx, func(broadcast *youtube.LiveBroadcast) error { return yt.updateActiveBroadcasts(ctx, func(broadcast *youtube.LiveBroadcast) error {
if broadcast.Snippet == nil { if broadcast.Snippet == nil {
return fmt.Errorf("YouTube have not provided the current snippet of broadcast %v", broadcast.Id) return fmt.Errorf(
"YouTube have not provided the current snippet of broadcast %v",
broadcast.Id,
)
} }
setDescription(broadcast, description) setDescription(broadcast, description)
return nil return nil
@@ -595,7 +606,12 @@ func (yt *YouTube) StartStream(
logger.Debugf(ctx, "profile == %#+v", profile) logger.Debugf(ctx, "profile == %#+v", profile)
templateBroadcastIDs = append(templateBroadcastIDs, profile.TemplateBroadcastIDs...) templateBroadcastIDs = append(templateBroadcastIDs, profile.TemplateBroadcastIDs...)
logger.Debugf(ctx, "templateBroadcastIDs == %v; customArgs == %v", templateBroadcastIDs, customArgs) logger.Debugf(
ctx,
"templateBroadcastIDs == %v; customArgs == %v",
templateBroadcastIDs,
customArgs,
)
templateBroadcastIDMap := map[string]struct{}{} templateBroadcastIDMap := map[string]struct{}{}
for _, broadcastID := range templateBroadcastIDs { for _, broadcastID := range templateBroadcastIDs {
@@ -629,7 +645,11 @@ func (yt *YouTube) StartStream(
return fmt.Errorf("unable to get the list of active broadcasts: %w", err) return fmt.Errorf("unable to get the list of active broadcasts: %w", err)
} }
if len(response.Items) != len(templateBroadcastIDs) { if len(response.Items) != len(templateBroadcastIDs) {
return fmt.Errorf("expected %d broadcasts, but found %d", len(templateBroadcastIDs), len(response.Items)) return fmt.Errorf(
"expected %d broadcasts, but found %d",
len(templateBroadcastIDs),
len(response.Items),
)
} }
broadcasts = append(broadcasts, response.Items...) broadcasts = append(broadcasts, response.Items...)
} }
@@ -637,18 +657,29 @@ func (yt *YouTube) StartStream(
{ {
logger.Debugf(ctx, "getting video info of %v", templateBroadcastIDs) logger.Debugf(ctx, "getting video info of %v", templateBroadcastIDs)
response, err := yt.YouTubeService.Videos.List(videoParts).Id(templateBroadcastIDs...).Context(ctx).Do() response, err := yt.YouTubeService.Videos.List(videoParts).
Id(templateBroadcastIDs...).
Context(ctx).
Do()
logger.Debugf(ctx, "YouTube.Video result: %v", err) logger.Debugf(ctx, "YouTube.Video result: %v", err)
if err != nil { if err != nil {
return fmt.Errorf("unable to get the list of active broadcasts: %w", err) return fmt.Errorf("unable to get the list of active broadcasts: %w", err)
} }
if len(response.Items) != len(templateBroadcastIDs) { if len(response.Items) != len(templateBroadcastIDs) {
return fmt.Errorf("expected %d videos, but found %d", len(templateBroadcastIDs), len(response.Items)) return fmt.Errorf(
"expected %d videos, but found %d",
len(templateBroadcastIDs),
len(response.Items),
)
} }
videos = append(videos, response.Items...) videos = append(videos, response.Items...)
} }
playlistsResponse, err := yt.YouTubeService.Playlists.List(playlistParts).MaxResults(1000).Mine(true).Context(ctx).Do() playlistsResponse, err := yt.YouTubeService.Playlists.List(playlistParts).
MaxResults(1000).
Mine(true).
Context(ctx).
Do()
logger.Debugf(ctx, "YouTube.Playlists result: %v", err) logger.Debugf(ctx, "YouTube.Playlists result: %v", err)
if err != nil { if err != nil {
return fmt.Errorf("unable to get the list of playlists: %w", err) return fmt.Errorf("unable to get the list of playlists: %w", err)
@@ -659,7 +690,12 @@ func (yt *YouTube) StartStream(
logger.Debugf(ctx, "getting playlist items for %s", templateBroadcastID) logger.Debugf(ctx, "getting playlist items for %s", templateBroadcastID)
for _, playlist := range playlistsResponse.Items { for _, playlist := range playlistsResponse.Items {
playlistItemsResponse, err := yt.YouTubeService.PlaylistItems.List(playlistItemParts).MaxResults(1000).PlaylistId(playlist.Id).VideoId(templateBroadcastID).Context(ctx).Do() playlistItemsResponse, err := yt.YouTubeService.PlaylistItems.List(playlistItemParts).
MaxResults(1000).
PlaylistId(playlist.Id).
VideoId(templateBroadcastID).
Context(ctx).
Do()
logger.Debugf(ctx, "YouTube.PlaylistItems result: %v", err) logger.Debugf(ctx, "YouTube.PlaylistItems result: %v", err)
if err != nil { if err != nil {
return fmt.Errorf("unable to get the list of playlist items: %w", err) return fmt.Errorf("unable to get the list of playlist items: %w", err)
@@ -676,15 +712,28 @@ func (yt *YouTube) StartStream(
} }
} }
logger.Debugf(ctx, "found %d playlists for %s", len(playlistIDMap[templateBroadcastID]), templateBroadcastID) logger.Debugf(
ctx,
"found %d playlists for %s",
len(playlistIDMap[templateBroadcastID]),
templateBroadcastID,
)
} }
var highestStreamNum uint64 var highestStreamNum uint64
if profile.AutoNumerate { if profile.AutoNumerate {
resp, err := yt.YouTubeService.LiveBroadcasts.List(liveBroadcastParts).Context(ctx).Mine(true).MaxResults(100).Fields().Do(googleapi.QueryParameter("order", "date")) resp, err := yt.YouTubeService.LiveBroadcasts.List(liveBroadcastParts).
Context(ctx).
Mine(true).
MaxResults(100).
Fields().
Do(googleapi.QueryParameter("order", "date"))
logger.Debugf(ctx, "YouTube.LiveBroadcasts result: %v", err) logger.Debugf(ctx, "YouTube.LiveBroadcasts result: %v", err)
if err != nil { if err != nil {
return fmt.Errorf("unable to request previous streams to figure out the next stream number for auto-numeration: %w", err) return fmt.Errorf(
"unable to request previous streams to figure out the next stream number for auto-numeration: %w",
err,
)
} }
for _, b := range resp.Items { for _, b := range resp.Items {
@@ -709,7 +758,11 @@ func (yt *YouTube) StartStream(
templateBroadcastID := broadcast.Id templateBroadcastID := broadcast.Id
if video.Id != broadcast.Id { if video.Id != broadcast.Id {
return fmt.Errorf("internal error: the orders of videos and broadcasts do not match: %s != %s", video.Id, broadcast.Id) return fmt.Errorf(
"internal error: the orders of videos and broadcasts do not match: %s != %s",
video.Id,
broadcast.Id,
)
} }
now := time.Now().UTC() now := time.Now().UTC()
broadcast.Id = "" broadcast.Id = ""
@@ -720,7 +773,9 @@ func (yt *YouTube) StartStream(
broadcast.ContentDetails.MonitorStream = nil broadcast.ContentDetails.MonitorStream = nil
broadcast.ContentDetails.ForceSendFields = []string{"EnableAutoStop"} broadcast.ContentDetails.ForceSendFields = []string{"EnableAutoStop"}
broadcast.Snippet.ScheduledStartTime = now.Format("2006-01-02T15:04:05") + ".00Z" broadcast.Snippet.ScheduledStartTime = now.Format("2006-01-02T15:04:05") + ".00Z"
broadcast.Snippet.ScheduledEndTime = now.Add(time.Hour*12).Format("2006-01-02T15:04:05") + ".00Z" broadcast.Snippet.ScheduledEndTime = now.Add(time.Hour*12).
Format("2006-01-02T15:04:05") +
".00Z"
broadcast.Snippet.LiveChatId = "" broadcast.Snippet.LiveChatId = ""
broadcast.Status.SelfDeclaredMadeForKids = broadcast.Status.MadeForKids broadcast.Status.SelfDeclaredMadeForKids = broadcast.Status.MadeForKids
broadcast.Status.ForceSendFields = []string{"SelfDeclaredMadeForKids"} broadcast.Status.ForceSendFields = []string{"SelfDeclaredMadeForKids"}
@@ -747,7 +802,10 @@ func (yt *YouTube) StartStream(
logger.Debugf(ctx, "YouTube.LiveBroadcasts result: %v", err) logger.Debugf(ctx, "YouTube.LiveBroadcasts result: %v", err)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "invalidScheduledStartTime") { if strings.Contains(err.Error(), "invalidScheduledStartTime") {
logger.Debugf(ctx, "it seems the local system clock is off, trying to fix the schedule time") logger.Debugf(
ctx,
"it seems the local system clock is off, trying to fix the schedule time",
)
now, err = timeapiio.Now() now, err = timeapiio.Now()
if err != nil { if err != nil {
@@ -758,7 +816,9 @@ func (yt *YouTube) StartStream(
now = time.Now().Add(time.Hour) now = time.Now().Add(time.Hour)
} }
broadcast.Snippet.ScheduledStartTime = now.Format("2006-01-02T15:04:05") + ".00Z" broadcast.Snippet.ScheduledStartTime = now.Format("2006-01-02T15:04:05") + ".00Z"
broadcast.Snippet.ScheduledEndTime = now.Add(time.Hour*12).Format("2006-01-02T15:04:05") + ".00Z" broadcast.Snippet.ScheduledEndTime = now.Add(time.Hour*12).
Format("2006-01-02T15:04:05") +
".00Z"
newBroadcast, err = yt.YouTubeService.LiveBroadcasts.Insert( newBroadcast, err = yt.YouTubeService.LiveBroadcasts.Insert(
[]string{"snippet", "contentDetails", "monetizationDetails", "status"}, []string{"snippet", "contentDetails", "monetizationDetails", "status"},
broadcast, broadcast,
@@ -789,13 +849,22 @@ func (yt *YouTube) StartStream(
video.Snippet.Tags = append(video.Snippet.Tags, profile.Tags...) video.Snippet.Tags = append(video.Snippet.Tags, profile.Tags...)
video.Snippet.Tags = append(video.Snippet.Tags, templateTags...) video.Snippet.Tags = append(video.Snippet.Tags, templateTags...)
default: default:
logger.Errorf(ctx, "unexpected value of the 'TemplateTags' setting: '%v'", profile.TemplateTags) logger.Errorf(
ctx,
"unexpected value of the 'TemplateTags' setting: '%v'",
profile.TemplateTags,
)
video.Snippet.Tags = profile.Tags video.Snippet.Tags = profile.Tags
} }
video.Snippet.Tags = deduplicate(video.Snippet.Tags) video.Snippet.Tags = deduplicate(video.Snippet.Tags)
tagsTruncated := TruncateTags(video.Snippet.Tags) tagsTruncated := TruncateTags(video.Snippet.Tags)
if len(tagsTruncated) != len(video.Snippet.Tags) { if len(tagsTruncated) != len(video.Snippet.Tags) {
logger.Infof(ctx, "YouTube tags were truncated, the amount was reduced from %d to %d to satisfy the 500 characters limit", len(video.Snippet.Tags), len(tagsTruncated)) logger.Infof(
ctx,
"YouTube tags were truncated, the amount was reduced from %d to %d to satisfy the 500 characters limit",
len(video.Snippet.Tags),
len(tagsTruncated),
)
video.Snippet.Tags = tagsTruncated video.Snippet.Tags = tagsTruncated
} }
b, err = yaml.Marshal(video) b, err = yaml.Marshal(video)
@@ -832,7 +901,9 @@ func (yt *YouTube) StartStream(
logger.Debugf(ctx, "adding the video to playlist %#+v", newPlaylistItem) logger.Debugf(ctx, "adding the video to playlist %#+v", newPlaylistItem)
} }
_, err = yt.YouTubeService.PlaylistItems.Insert(playlistItemParts, newPlaylistItem).Context(ctx).Do() _, err = yt.YouTubeService.PlaylistItems.Insert(playlistItemParts, newPlaylistItem).
Context(ctx).
Do()
logger.Debugf(ctx, "YouTube.PlaylistItems result: %v", err) logger.Debugf(ctx, "YouTube.PlaylistItems result: %v", err)
if err != nil { if err != nil {
return fmt.Errorf("unable to add video to playlist %#+v: %w", playlistID, err) return fmt.Errorf("unable to add video to playlist %#+v: %w", playlistID, err)
@@ -843,16 +914,25 @@ func (yt *YouTube) StartStream(
logger.Debugf(ctx, "downloading the thumbnail") logger.Debugf(ctx, "downloading the thumbnail")
resp, err := http.Get(broadcast.Snippet.Thumbnails.Standard.Url) resp, err := http.Get(broadcast.Snippet.Thumbnails.Standard.Url)
if err != nil { if err != nil {
return fmt.Errorf("unable to download the thumbnail from the template video: %w", err) return fmt.Errorf(
"unable to download the thumbnail from the template video: %w",
err,
)
} }
logger.Debugf(ctx, "reading the thumbnail") logger.Debugf(ctx, "reading the thumbnail")
thumbnail, err := io.ReadAll(resp.Body) thumbnail, err := io.ReadAll(resp.Body)
resp.Body.Close() resp.Body.Close()
if err != nil { if err != nil {
return fmt.Errorf("unable to read the thumbnail from the response from the template video: %w", err) return fmt.Errorf(
"unable to read the thumbnail from the response from the template video: %w",
err,
)
} }
logger.Debugf(ctx, "setting the thumbnail") logger.Debugf(ctx, "setting the thumbnail")
_, err = yt.YouTubeService.Thumbnails.Set(newBroadcast.Id).Media(bytes.NewReader(thumbnail)).Context(ctx).Do() _, err = yt.YouTubeService.Thumbnails.Set(newBroadcast.Id).
Media(bytes.NewReader(thumbnail)).
Context(ctx).
Do()
logger.Debugf(ctx, "YouTube.Thumbnails result: %v", err) logger.Debugf(ctx, "YouTube.Thumbnails result: %v", err)
if err != nil { if err != nil {
return fmt.Errorf("unable to set the thumbnail: %w", err) return fmt.Errorf("unable to set the thumbnail: %w", err)
@@ -907,7 +987,13 @@ func (yt *YouTube) GetStreamStatus(
if err != nil { if err != nil {
_startedAt, err = time.Parse(timeLayoutFallback, ts) _startedAt, err = time.Parse(timeLayoutFallback, ts)
if err != nil { if err != nil {
return fmt.Errorf("unable to parse '%s' with layouts '%s' and '%s': %w", ts, timeLayout, timeLayoutFallback, err) return fmt.Errorf(
"unable to parse '%s' with layouts '%s' and '%s': %w",
ts,
timeLayout,
timeLayoutFallback,
err,
)
} }
} }
startedAt = &_startedAt startedAt = &_startedAt
@@ -939,7 +1025,12 @@ func (yt *YouTube) GetStreamStatus(
Streams: streams, Streams: streams,
} }
if observability.LogLevelFilter.GetLevel() >= logger.LevelTrace { if observability.LogLevelFilter.GetLevel() >= logger.LevelTrace {
logger.Tracef(ctx, "len(customData.UpcomingBroadcasts) == %d; len(customData.Streams) == %d", len(customData.UpcomingBroadcasts), len(customData.Streams)) logger.Tracef(
ctx,
"len(customData.UpcomingBroadcasts) == %d; len(customData.Streams) == %d",
len(customData.UpcomingBroadcasts),
len(customData.Streams),
)
for idx, broadcast := range customData.UpcomingBroadcasts { for idx, broadcast := range customData.UpcomingBroadcasts {
b, err := json.Marshal(broadcast) b, err := json.Marshal(broadcast)
if err != nil { if err != nil {
@@ -948,7 +1039,11 @@ func (yt *YouTube) GetStreamStatus(
logger.Tracef(ctx, "UpcomingBroadcasts[%3d] == %s", idx, b) logger.Tracef(ctx, "UpcomingBroadcasts[%3d] == %s", idx, b)
} }
} }
logger.Tracef(ctx, "len(customData.ActiveBroadcasts) == %d", len(customData.ActiveBroadcasts)) logger.Tracef(
ctx,
"len(customData.ActiveBroadcasts) == %d",
len(customData.ActiveBroadcasts),
)
for idx, bc := range customData.ActiveBroadcasts { for idx, bc := range customData.ActiveBroadcasts {
b, err := json.Marshal(bc) b, err := json.Marshal(bc)
if err != nil { if err != nil {

View File

@@ -48,7 +48,12 @@ type StreamD interface {
) error ) error
SetTitle(ctx context.Context, platID streamcontrol.PlatformName, title string) error SetTitle(ctx context.Context, platID streamcontrol.PlatformName, title string) error
SetDescription(ctx context.Context, platID streamcontrol.PlatformName, description string) error SetDescription(ctx context.Context, platID streamcontrol.PlatformName, description string) error
ApplyProfile(ctx context.Context, platID streamcontrol.PlatformName, profile streamcontrol.AbstractStreamProfile, customArgs ...any) error ApplyProfile(
ctx context.Context,
platID streamcontrol.PlatformName,
profile streamcontrol.AbstractStreamProfile,
customArgs ...any,
) error
OBSOLETE_GitRelogin(ctx context.Context) error OBSOLETE_GitRelogin(ctx context.Context) error
GetBackendData(ctx context.Context, platID streamcontrol.PlatformName) (any, error) GetBackendData(ctx context.Context, platID streamcontrol.PlatformName) (any, error)
Restart(ctx context.Context) error Restart(ctx context.Context) error
@@ -189,7 +194,12 @@ type StreamD interface {
ListTimers(ctx context.Context) ([]Timer, error) ListTimers(ctx context.Context) ([]Timer, error)
AddOBSSceneRule(ctx context.Context, sceneName SceneName, sceneRule SceneRule) error AddOBSSceneRule(ctx context.Context, sceneName SceneName, sceneRule SceneRule) error
UpdateOBSSceneRule(ctx context.Context, sceneName SceneName, idx uint64, sceneRule SceneRule) error UpdateOBSSceneRule(
ctx context.Context,
sceneName SceneName,
idx uint64,
sceneRule SceneRule,
) error
RemoveOBSSceneRule(ctx context.Context, sceneName SceneName, idx uint64) error RemoveOBSSceneRule(ctx context.Context, sceneName SceneName, idx uint64) error
ListOBSSceneRules(ctx context.Context, sceneName SceneName) (SceneRules, error) ListOBSSceneRules(ctx context.Context, sceneName SceneName) (SceneRules, error)
} }

View File

@@ -47,9 +47,15 @@ func NewConfig() Config {
func NewSampleConfig() Config { func NewSampleConfig() Config {
cfg := NewConfig() cfg := NewConfig()
cfg.Backends[obs.ID].StreamProfiles = map[streamcontrol.ProfileName]streamcontrol.AbstractStreamProfile{"some_profile": obs.StreamProfile{}} cfg.Backends[obs.ID].StreamProfiles = map[streamcontrol.ProfileName]streamcontrol.AbstractStreamProfile{
cfg.Backends[twitch.ID].StreamProfiles = map[streamcontrol.ProfileName]streamcontrol.AbstractStreamProfile{"some_profile": twitch.StreamProfile{}} "some_profile": obs.StreamProfile{},
cfg.Backends[youtube.ID].StreamProfiles = map[streamcontrol.ProfileName]streamcontrol.AbstractStreamProfile{"some_profile": youtube.StreamProfile{}} }
cfg.Backends[twitch.ID].StreamProfiles = map[streamcontrol.ProfileName]streamcontrol.AbstractStreamProfile{
"some_profile": twitch.StreamProfile{},
}
cfg.Backends[youtube.ID].StreamProfiles = map[streamcontrol.ProfileName]streamcontrol.AbstractStreamProfile{
"some_profile": youtube.StreamProfile{},
}
return cfg return cfg
} }

View File

@@ -166,16 +166,32 @@ func (s *MonitorSourceOBSVideo) GetImageBytes(
} }
resp, err := obsServer.GetSourceScreenshot(ctx, req) resp, err := obsServer.GetSourceScreenshot(ctx, req)
if err != nil { if err != nil {
return nil, "", time.Now().Add(time.Second), 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() imgB64 := resp.GetImageData()
imgBytes, mimeType, err := imgb64.Decode(string(imgB64)) imgBytes, mimeType, err := imgb64.Decode(string(imgB64))
if err != nil { if err != nil {
return nil, "", time.Time{}, fmt.Errorf("unable to decode the screenshot of '%s': %w", s.Name, err) return nil, "", time.Time{}, fmt.Errorf(
"unable to decode the screenshot of '%s': %w",
s.Name,
err,
)
} }
logger.Tracef(ctx, "the decoded image is of format '%s' (expected format: '%s') and size %d", mimeType, s.ImageFormat, len(imgBytes)) logger.Tracef(
ctx,
"the decoded image is of format '%s' (expected format: '%s') and size %d",
mimeType,
s.ImageFormat,
len(imgBytes),
)
return imgBytes, mimeType, time.Now().Add(time.Duration(s.UpdateInterval)), nil return imgBytes, mimeType, time.Now().Add(time.Duration(s.UpdateInterval)), nil
} }
@@ -224,11 +240,19 @@ func (s *MonitorSourceOBSVolume) GetImage(
colorActive, err := colorx.Parse(s.ColorActive) colorActive, err := colorx.Parse(s.ColorActive)
if err != nil { if err != nil {
return nil, time.Time{}, fmt.Errorf("unable to parse the `color_active` value '%s': %w", s.ColorActive, err) return nil, time.Time{}, fmt.Errorf(
"unable to parse the `color_active` value '%s': %w",
s.ColorActive,
err,
)
} }
colorPassive, err := colorx.Parse(s.ColorPassive) colorPassive, err := colorx.Parse(s.ColorPassive)
if err != nil { if err != nil {
return nil, time.Time{}, fmt.Errorf("unable to parse the `color_passive` value '%s': %w", s.ColorPassive, err) return nil, time.Time{}, fmt.Errorf(
"unable to parse the `color_passive` value '%s': %w",
s.ColorPassive,
err,
)
} }
size := img.Bounds().Size() size := img.Bounds().Size()

View File

@@ -66,21 +66,30 @@ func (cfg *Config) UnmarshalYAML(b []byte) (_err error) {
} }
if cfg.Backends[obs.ID] != nil { if cfg.Backends[obs.ID] != nil {
err = streamcontrol.ConvertStreamProfiles[obs.StreamProfile](context.Background(), cfg.Backends[obs.ID].StreamProfiles) err = streamcontrol.ConvertStreamProfiles[obs.StreamProfile](
context.Background(),
cfg.Backends[obs.ID].StreamProfiles,
)
if err != nil { if err != nil {
return fmt.Errorf("unable to convert stream profiles of OBS: %w", err) return fmt.Errorf("unable to convert stream profiles of OBS: %w", err)
} }
} }
if cfg.Backends[twitch.ID] != nil { if cfg.Backends[twitch.ID] != nil {
err = streamcontrol.ConvertStreamProfiles[twitch.StreamProfile](context.Background(), cfg.Backends[twitch.ID].StreamProfiles) err = streamcontrol.ConvertStreamProfiles[twitch.StreamProfile](
context.Background(),
cfg.Backends[twitch.ID].StreamProfiles,
)
if err != nil { if err != nil {
return fmt.Errorf("unable to convert stream profiles of twitch: %w", err) return fmt.Errorf("unable to convert stream profiles of twitch: %w", err)
} }
} }
if cfg.Backends[youtube.ID] != nil { if cfg.Backends[youtube.ID] != nil {
err = streamcontrol.ConvertStreamProfiles[youtube.StreamProfile](context.Background(), cfg.Backends[youtube.ID].StreamProfiles) err = streamcontrol.ConvertStreamProfiles[youtube.StreamProfile](
context.Background(),
cfg.Backends[youtube.ID].StreamProfiles,
)
if err != nil { if err != nil {
return fmt.Errorf("unable to convert stream profiles of youtube: %w", err) return fmt.Errorf("unable to convert stream profiles of youtube: %w", err)
} }

View File

@@ -99,7 +99,10 @@ func (d *StreamD) onConfigUpdateViaGIT(ctx context.Context, cfg *config.Config)
d.UI.DisplayError(fmt.Errorf("unable to save data: %w", err)) d.UI.DisplayError(fmt.Errorf("unable to save data: %w", err))
} }
if d.GitInitialized { if d.GitInitialized {
d.UI.Restart(ctx, "Received an updated config from another device, please restart the application") d.UI.Restart(
ctx,
"Received an updated config from another device, please restart the application",
)
} }
} }
@@ -203,7 +206,9 @@ func (d *StreamD) startPeriodicGitSyncer(ctx context.Context) {
observability.Go(ctx, func() { observability.Go(ctx, func() {
err := d.sendConfigViaGIT(ctx) err := d.sendConfigViaGIT(ctx)
if err != nil { if err != nil {
d.UI.DisplayError(fmt.Errorf("unable to send the config to the remote git repository: %w", err)) d.UI.DisplayError(
fmt.Errorf("unable to send the config to the remote git repository: %w", err),
)
} }
ticker := time.NewTicker(time.Minute) ticker := time.NewTicker(time.Minute)

View File

@@ -88,7 +88,12 @@ func Memoize[REQ any, REPLY any, T func(context.Context, REQ) (REPLY, error)](
logger.Tracef(ctx, "using the cached value") logger.Tracef(ctx, "using the cached value")
return v.Reply, v.Error return v.Reply, v.Error
} }
logger.Tracef(ctx, "the cached value expired: %s < %s", v.SavedAt.Format(timeFormat), cutoffTS.Format(timeFormat)) logger.Tracef(
ctx,
"the cached value expired: %s < %s",
v.SavedAt.Format(timeFormat),
cutoffTS.Format(timeFormat),
)
delete(cache, key) delete(cache, key)
} else { } else {
logger.Errorf(ctx, "cache-failure: expected type %T, but got %T", (*cacheItem)(nil), cachedResult) logger.Errorf(ctx, "cache-failure: expected type %T, but got %T", (*cacheItem)(nil), cachedResult)

View File

@@ -243,7 +243,11 @@ func (grpc *GRPCServer) IsBackendEnabled(
) (*streamd_grpc.IsBackendEnabledReply, error) { ) (*streamd_grpc.IsBackendEnabledReply, error) {
enabled, err := grpc.StreamD.IsBackendEnabled(ctx, streamcontrol.PlatformName(req.GetPlatID())) enabled, err := grpc.StreamD.IsBackendEnabled(ctx, streamcontrol.PlatformName(req.GetPlatID()))
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to check if backend '%s' is enabled: %w", req.GetPlatID(), err) return nil, fmt.Errorf(
"unable to check if backend '%s' is enabled: %w",
req.GetPlatID(),
err,
)
} }
return &streamd_grpc.IsBackendEnabledReply{ return &streamd_grpc.IsBackendEnabledReply{
IsInitialized: enabled, IsInitialized: enabled,
@@ -564,7 +568,10 @@ func (grpc *GRPCServer) openBrowser(
count++ count++
err := handler.Sender.Send(&req) err := handler.Sender.Send(&req)
if err != nil { if err != nil {
err = multierror.Append(resultErr, fmt.Errorf("unable to send oauth request: %w", err)) err = multierror.Append(
resultErr,
fmt.Errorf("unable to send oauth request: %w", err),
)
} }
} }
} }
@@ -583,10 +590,25 @@ func (grpc *GRPCServer) OpenOAuthURL(
) (_ret error) { ) (_ret error) {
logger.Debugf(ctx, "OpenOAuthURL(ctx, %d, '%s', '%s')", listenPort, platID, authURL) logger.Debugf(ctx, "OpenOAuthURL(ctx, %d, '%s', '%s')", listenPort, platID, authURL)
defer func() { defer func() {
logger.Debugf(ctx, "/OpenOAuthURL(ctx, %d, '%s', '%s'): %v", listenPort, platID, authURL, _ret) logger.Debugf(
ctx,
"/OpenOAuthURL(ctx, %d, '%s', '%s'): %v",
listenPort,
platID,
authURL,
_ret,
)
}() }()
return xsync.DoA4R1(ctx, &grpc.OAuthURLHandlerLocker, grpc.openOAuthURL, ctx, listenPort, platID, authURL) return xsync.DoA4R1(
ctx,
&grpc.OAuthURLHandlerLocker,
grpc.openOAuthURL,
ctx,
listenPort,
platID,
authURL,
)
} }
func (grpc *GRPCServer) openOAuthURL( func (grpc *GRPCServer) openOAuthURL(
@@ -703,7 +725,12 @@ func (grpc *GRPCServer) ListStreamServers(
var result []*streamd_grpc.StreamServerWithStatistics var result []*streamd_grpc.StreamServerWithStatistics
for _, srv := range servers { for _, srv := range servers {
srvGRPC, err := goconv.StreamServerConfigGo2GRPC(ctx, srv.Type, srv.ListenAddr, srv.Options()) srvGRPC, err := goconv.StreamServerConfigGo2GRPC(
ctx,
srv.Type,
srv.ListenAddr,
srv.Options(),
)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to convert the server server value: %w", err) return nil, fmt.Errorf("unable to convert the server server value: %w", err)
} }
@@ -727,7 +754,11 @@ func (grpc *GRPCServer) StartStreamServer(
) (*streamd_grpc.StartStreamServerReply, error) { ) (*streamd_grpc.StartStreamServerReply, error) {
srvType, addr, opts, err := goconv.StreamServerConfigGRPC2Go(ctx, req.GetConfig()) srvType, addr, opts, err := goconv.StreamServerConfigGRPC2Go(ctx, req.GetConfig())
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to convert the stream server config %#+v: %w", req.GetConfig(), err) return nil, fmt.Errorf(
"unable to convert the stream server config %#+v: %w",
req.GetConfig(),
err,
)
} }
err = grpc.StreamD.StartStreamServer( err = grpc.StreamD.StartStreamServer(
@@ -922,8 +953,12 @@ func (grpc *GRPCServer) AddStreamForward(
api.StreamForwardingQuirks{ api.StreamForwardingQuirks{
RestartUntilYoutubeRecognizesStream: api.RestartUntilYoutubeRecognizesStream{ RestartUntilYoutubeRecognizesStream: api.RestartUntilYoutubeRecognizesStream{
Enabled: cfg.Quirks.RestartUntilYoutubeRecognizesStream.Enabled, Enabled: cfg.Quirks.RestartUntilYoutubeRecognizesStream.Enabled,
StartTimeout: sec2dur(cfg.Quirks.RestartUntilYoutubeRecognizesStream.StartTimeout), StartTimeout: sec2dur(
StopStartDelay: sec2dur(cfg.Quirks.RestartUntilYoutubeRecognizesStream.StopStartDelay), cfg.Quirks.RestartUntilYoutubeRecognizesStream.StartTimeout,
),
StopStartDelay: sec2dur(
cfg.Quirks.RestartUntilYoutubeRecognizesStream.StopStartDelay,
),
}, },
StartAfterYoutubeRecognizedStream: api.StartAfterYoutubeRecognizedStream{ StartAfterYoutubeRecognizedStream: api.StartAfterYoutubeRecognizedStream{
Enabled: cfg.Quirks.StartAfterYoutubeRecognizedStream.Enabled, Enabled: cfg.Quirks.StartAfterYoutubeRecognizedStream.Enabled,
@@ -949,8 +984,12 @@ func (grpc *GRPCServer) UpdateStreamForward(
api.StreamForwardingQuirks{ api.StreamForwardingQuirks{
RestartUntilYoutubeRecognizesStream: api.RestartUntilYoutubeRecognizesStream{ RestartUntilYoutubeRecognizesStream: api.RestartUntilYoutubeRecognizesStream{
Enabled: cfg.Quirks.RestartUntilYoutubeRecognizesStream.Enabled, Enabled: cfg.Quirks.RestartUntilYoutubeRecognizesStream.Enabled,
StartTimeout: sec2dur(cfg.Quirks.RestartUntilYoutubeRecognizesStream.StartTimeout), StartTimeout: sec2dur(
StopStartDelay: sec2dur(cfg.Quirks.RestartUntilYoutubeRecognizesStream.StopStartDelay), cfg.Quirks.RestartUntilYoutubeRecognizesStream.StartTimeout,
),
StopStartDelay: sec2dur(
cfg.Quirks.RestartUntilYoutubeRecognizesStream.StopStartDelay,
),
}, },
StartAfterYoutubeRecognizedStream: api.StartAfterYoutubeRecognizedStream{ StartAfterYoutubeRecognizedStream: api.StartAfterYoutubeRecognizedStream{
Enabled: cfg.Quirks.StartAfterYoutubeRecognizedStream.Enabled, Enabled: cfg.Quirks.StartAfterYoutubeRecognizedStream.Enabled,
@@ -1113,7 +1152,11 @@ func (grpc *GRPCServer) StreamPlayerOpen(
ctx context.Context, ctx context.Context,
req *streamd_grpc.StreamPlayerOpenRequest, req *streamd_grpc.StreamPlayerOpenRequest,
) (*streamd_grpc.StreamPlayerOpenReply, error) { ) (*streamd_grpc.StreamPlayerOpenReply, error) {
err := grpc.StreamD.StreamPlayerOpenURL(ctx, streamtypes.StreamID(req.GetStreamID()), req.GetRequest().GetLink()) err := grpc.StreamD.StreamPlayerOpenURL(
ctx,
streamtypes.StreamID(req.GetStreamID()),
req.GetRequest().GetLink(),
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1125,7 +1168,10 @@ func (grpc *GRPCServer) StreamPlayerProcessTitle(
ctx context.Context, ctx context.Context,
req *streamd_grpc.StreamPlayerProcessTitleRequest, req *streamd_grpc.StreamPlayerProcessTitleRequest,
) (*streamd_grpc.StreamPlayerProcessTitleReply, error) { ) (*streamd_grpc.StreamPlayerProcessTitleReply, error) {
title, err := grpc.StreamD.StreamPlayerProcessTitle(ctx, streamtypes.StreamID(req.GetStreamID())) title, err := grpc.StreamD.StreamPlayerProcessTitle(
ctx,
streamtypes.StreamID(req.GetStreamID()),
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1214,7 +1260,11 @@ func (grpc *GRPCServer) StreamPlayerSetSpeed(
ctx context.Context, ctx context.Context,
req *streamd_grpc.StreamPlayerSetSpeedRequest, req *streamd_grpc.StreamPlayerSetSpeedRequest,
) (*streamd_grpc.StreamPlayerSetSpeedReply, error) { ) (*streamd_grpc.StreamPlayerSetSpeedReply, error) {
err := grpc.StreamD.StreamPlayerSetSpeed(ctx, streamtypes.StreamID(req.GetStreamID()), req.GetRequest().Speed) err := grpc.StreamD.StreamPlayerSetSpeed(
ctx,
streamtypes.StreamID(req.GetStreamID()),
req.GetRequest().Speed,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1226,7 +1276,11 @@ func (grpc *GRPCServer) StreamPlayerSetPause(
ctx context.Context, ctx context.Context,
req *streamd_grpc.StreamPlayerSetPauseRequest, req *streamd_grpc.StreamPlayerSetPauseRequest,
) (*streamd_grpc.StreamPlayerSetPauseReply, error) { ) (*streamd_grpc.StreamPlayerSetPauseReply, error) {
err := grpc.StreamD.StreamPlayerSetPause(ctx, streamtypes.StreamID(req.GetStreamID()), req.GetRequest().IsPaused) err := grpc.StreamD.StreamPlayerSetPause(
ctx,
streamtypes.StreamID(req.GetStreamID()),
req.GetRequest().IsPaused,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -45,7 +45,10 @@ func (d *StreamD) EXPERIMENTAL_ReinitStreamControllers(ctx context.Context) erro
continue continue
} }
if err != nil { if err != nil {
result = multierror.Append(result, fmt.Errorf("unable to initialize '%s': %w", platName, err)) result = multierror.Append(
result,
fmt.Errorf("unable to initialize '%s': %w", platName, err),
)
} }
} }
return result.ErrorOrNil() return result.ErrorOrNil()
@@ -128,7 +131,8 @@ func newTwitch(
error, error,
) { ) {
platCfg := streamcontrol.ConvertPlatformConfig[twitch.PlatformSpecificConfig, twitch.StreamProfile]( platCfg := streamcontrol.ConvertPlatformConfig[twitch.PlatformSpecificConfig, twitch.StreamProfile](
ctx, cfg, ctx,
cfg,
) )
if platCfg == nil { if platCfg == nil {
return nil, fmt.Errorf("twitch config was not found") return nil, fmt.Errorf("twitch config was not found")
@@ -139,7 +143,8 @@ func newTwitch(
} }
hadSetNewUserData := false hadSetNewUserData := false
if platCfg.Config.Channel == "" || platCfg.Config.ClientID == "" || platCfg.Config.ClientSecret == "" { if platCfg.Config.Channel == "" || platCfg.Config.ClientID == "" ||
platCfg.Config.ClientSecret == "" {
ok, err := setUserData(ctx, platCfg) ok, err := setUserData(ctx, platCfg)
if !ok { if !ok {
err := saveCfgFunc(&streamcontrol.AbstractPlatformConfig{ err := saveCfgFunc(&streamcontrol.AbstractPlatformConfig{
@@ -197,7 +202,8 @@ func newYouTube(
error, error,
) { ) {
platCfg := streamcontrol.ConvertPlatformConfig[youtube.PlatformSpecificConfig, youtube.StreamProfile]( platCfg := streamcontrol.ConvertPlatformConfig[youtube.PlatformSpecificConfig, youtube.StreamProfile](
ctx, cfg, ctx,
cfg,
) )
if platCfg == nil { if platCfg == nil {
return nil, fmt.Errorf("youtube config was not found") return nil, fmt.Errorf("youtube config was not found")
@@ -295,7 +301,9 @@ func (d *StreamD) listenOBSEvents(
default: default:
} }
client, err := o.GetClient(obs.GetClientOption(goobs.WithEventSubscriptions(subscriptions.InputVolumeMeters))) client, err := o.GetClient(
obs.GetClientOption(goobs.WithEventSubscriptions(subscriptions.InputVolumeMeters)),
)
if err != nil { if err != nil {
logger.Errorf(ctx, "unable to get an OBS client: %v", err) logger.Errorf(ctx, "unable to get an OBS client: %v", err)
time.Sleep(time.Second) time.Sleep(time.Second)

View File

@@ -36,7 +36,10 @@ func (a *platformsControllerAdapter) CheckStreamStartedByURL(
case strings.Contains(destination.Hostname(), "twitch"): case strings.Contains(destination.Hostname(), "twitch"):
platID = twitch.ID platID = twitch.ID
default: default:
return false, fmt.Errorf("do not know how to check if the stream started for '%s'", destination.String()) return false, fmt.Errorf(
"do not know how to check if the stream started for '%s'",
destination.String(),
)
} }
return a.CheckStreamStartedByPlatformID(ctx, platID) return a.CheckStreamStartedByPlatformID(ctx, platID)
} }

View File

@@ -183,7 +183,12 @@ func getOBSImageBytes(
) ([]byte, time.Time, error) { ) ([]byte, time.Time, error) {
img, nextUpdateAt, err := el.Source.GetImage(ctx, obsServer, el, obsState) img, nextUpdateAt, err := el.Source.GetImage(ctx, obsServer, el, obsState)
if err != nil { if err != nil {
return nil, time.Now().Add(time.Second), fmt.Errorf("unable to get the image from the source: %w", err) return nil, time.Now().
Add(time.Second),
fmt.Errorf(
"unable to get the image from the source: %w",
err,
)
} }
for _, filter := range el.Filters { for _, filter := range el.Filters {
@@ -292,7 +297,10 @@ func (d *StreamD) initStreamServer(ctx context.Context) (_err error) {
) )
assert(d.StreamServer != nil) assert(d.StreamServer != nil)
defer d.notifyAboutChange(ctx, events.StreamServersChange) defer d.notifyAboutChange(ctx, events.StreamServersChange)
return d.StreamServer.Init(ctx, sstypes.InitOptionDefaultStreamPlayerOptions(d.streamPlayerOptions())) return d.StreamServer.Init(
ctx,
sstypes.InitOptionDefaultStreamPlayerOptions(d.streamPlayerOptions()),
)
} }
func (d *StreamD) streamPlayerOptions() sptypes.Options { func (d *StreamD) streamPlayerOptions() sptypes.Options {
@@ -526,7 +534,9 @@ func (d *StreamD) SaveConfig(ctx context.Context) error {
if d.GitStorage != nil { if d.GitStorage != nil {
err = d.sendConfigViaGIT(ctx) err = d.sendConfigViaGIT(ctx)
if err != nil { if err != nil {
d.UI.DisplayError(fmt.Errorf("unable to send the config to the remote git repository: %w", err)) d.UI.DisplayError(
fmt.Errorf("unable to send the config to the remote git repository: %w", err),
)
} }
} }
}) })
@@ -550,7 +560,10 @@ func (d *StreamD) SetConfig(ctx context.Context, cfg *config.Config) error {
return nil return nil
} }
func (d *StreamD) IsBackendEnabled(ctx context.Context, id streamcontrol.PlatformName) (bool, error) { func (d *StreamD) IsBackendEnabled(
ctx context.Context,
id streamcontrol.PlatformName,
) (bool, error) {
return xsync.RDoR2(ctx, &d.ControllersLocker, func() (bool, error) { return xsync.RDoR2(ctx, &d.ControllersLocker, func() (bool, error) {
switch id { switch id {
case obs.ID: case obs.ID:
@@ -600,7 +613,12 @@ func (d *StreamD) StartStream(
if err != nil { if err != nil {
return fmt.Errorf("unable to convert the profile into OBS profile: %w", err) return fmt.Errorf("unable to convert the profile into OBS profile: %w", err)
} }
err = d.StreamControllers.OBS.StartStream(d.ctxForController(ctx), title, description, *profile, customArgs...) err = d.StreamControllers.OBS.StartStream(
d.ctxForController(ctx),
title,
description,
*profile,
customArgs...)
if err != nil { if err != nil {
return fmt.Errorf("unable to start the stream on OBS: %w", err) return fmt.Errorf("unable to start the stream on OBS: %w", err)
} }
@@ -610,7 +628,12 @@ func (d *StreamD) StartStream(
if err != nil { if err != nil {
return fmt.Errorf("unable to convert the profile into Twitch profile: %w", err) return fmt.Errorf("unable to convert the profile into Twitch profile: %w", err)
} }
err = d.StreamControllers.Twitch.StartStream(d.ctxForController(ctx), title, description, *profile, customArgs...) err = d.StreamControllers.Twitch.StartStream(
d.ctxForController(ctx),
title,
description,
*profile,
customArgs...)
if err != nil { if err != nil {
return fmt.Errorf("unable to start the stream on Twitch: %w", err) return fmt.Errorf("unable to start the stream on Twitch: %w", err)
} }
@@ -620,7 +643,12 @@ func (d *StreamD) StartStream(
if err != nil { if err != nil {
return fmt.Errorf("unable to convert the profile into YouTube profile: %w", err) return fmt.Errorf("unable to convert the profile into YouTube profile: %w", err)
} }
err = d.StreamControllers.YouTube.StartStream(d.ctxForController(ctx), title, description, *profile, customArgs...) err = d.StreamControllers.YouTube.StartStream(
d.ctxForController(ctx),
title,
description,
*profile,
customArgs...)
if err != nil { if err != nil {
return fmt.Errorf("unable to start the stream on YouTube: %w", err) return fmt.Errorf("unable to start the stream on YouTube: %w", err)
} }
@@ -1492,9 +1520,24 @@ func (d *StreamD) UpdateStreamPlayer(
disabled bool, disabled bool,
streamPlaybackConfig sptypes.Config, streamPlaybackConfig sptypes.Config,
) (_err error) { ) (_err error) {
logger.Debugf(ctx, "UpdateStreamPlayer(ctx, '%s', '%s', %v, %#+v)", streamID, playerType, disabled, streamPlaybackConfig) logger.Debugf(
ctx,
"UpdateStreamPlayer(ctx, '%s', '%s', %v, %#+v)",
streamID,
playerType,
disabled,
streamPlaybackConfig,
)
defer func() { defer func() {
logger.Debugf(ctx, "/UpdateStreamPlayer(ctx, '%s', '%s', %v, %#+v): %v", streamID, playerType, disabled, streamPlaybackConfig, _err) logger.Debugf(
ctx,
"/UpdateStreamPlayer(ctx, '%s', '%s', %v, %#+v): %v",
streamID,
playerType,
disabled,
streamPlaybackConfig,
_err,
)
}() }()
defer d.notifyAboutChange(ctx, events.StreamPlayersChange) defer d.notifyAboutChange(ctx, events.StreamPlayersChange)
var result *multierror.Error var result *multierror.Error

View File

@@ -74,7 +74,11 @@ func (p *Panel) ShowErrorReports() {
content := container.NewVBox() content := container.NewVBox()
if len(reports) == 0 { if len(reports) == 0 {
content.Add(widget.NewRichTextWithText("No significant errors were reported since the application was started, yet...")) content.Add(
widget.NewRichTextWithText(
"No significant errors were reported since the application was started, yet...",
),
)
} else { } else {
for _, report := range reports { for _, report := range reports {
errLabel := report.Error.Error() errLabel := report.Error.Error()

View File

@@ -52,7 +52,9 @@ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX= XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=
-----END OPENSSH PRIVATE KEY-----`) -----END OPENSSH PRIVATE KEY-----`)
gitInstruction := widget.NewRichTextFromMarkdown("We can sync the configuration among all of your devices via a git repository. To get a git repository you may, for example, use GitHub; but never use public repositories, always use private ones (because the repository will contain all the access credentials to YouTube/Twitch/whatever).") gitInstruction := widget.NewRichTextFromMarkdown(
"We can sync the configuration among all of your devices via a git repository. To get a git repository you may, for example, use GitHub; but never use public repositories, always use private ones (because the repository will contain all the access credentials to YouTube/Twitch/whatever).",
)
gitInstruction.Wrapping = fyne.TextWrapWord gitInstruction.Wrapping = fyne.TextWrapWord
w.SetContent(container.NewBorder( w.SetContent(container.NewBorder(

View File

@@ -149,7 +149,14 @@ func (p *Panel) updateMonitorPageImagesNoLock(
if !changed && lastWinSize == winSize && lastOrientation == orientation { if !changed && lastWinSize == winSize && lastOrientation == orientation {
return return
} }
logger.Tracef(ctx, "updating the image '%s': %v %#+v %#+v", el.ElementName, changed, lastWinSize, winSize) logger.Tracef(
ctx,
"updating the image '%s': %v %#+v %#+v",
el.ElementName,
changed,
lastWinSize,
winSize,
)
imgSize := image.Point{ imgSize := image.Point{
X: int(winSize.Width * float32(el.Width) / 100), X: int(winSize.Width * float32(el.Width) / 100),
Y: int(winSize.Height * float32(el.Height) / 100), Y: int(winSize.Height * float32(el.Height) / 100),
@@ -192,7 +199,13 @@ func (p *Panel) updateMonitorPageImagesNoLock(
if !changed && lastWinSize == winSize && lastOrientation == orientation { if !changed && lastWinSize == winSize && lastOrientation == orientation {
return return
} }
logger.Tracef(ctx, "updating the screenshot image: %v %#+v %#+v", changed, lastWinSize, winSize) logger.Tracef(
ctx,
"updating the screenshot image: %v %#+v %#+v",
changed,
lastWinSize,
winSize,
)
winSize := image.Point{X: int(winSize.Width), Y: int(winSize.Height)} winSize := image.Point{X: int(winSize.Width), Y: int(winSize.Height)}
img = imgFillTo( img = imgFillTo(
ctx, ctx,
@@ -366,7 +379,10 @@ func (p *Panel) newMonitorSettingsWindow(ctx context.Context) {
deleteButton := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() { deleteButton := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() {
w := dialog.NewConfirm( w := dialog.NewConfirm(
fmt.Sprintf("Delete monitor element '%s'?", name), fmt.Sprintf("Delete monitor element '%s'?", name),
fmt.Sprintf("Are you sure you want to delete the element '%s' from the Monitor page?", name), fmt.Sprintf(
"Are you sure you want to delete the element '%s' from the Monitor page?",
name,
),
func(b bool) { func(b bool) {
if !b { if !b {
return return
@@ -484,7 +500,9 @@ func (p *Panel) editMonitorElementWindow(
SceneName: &sceneName, SceneName: &sceneName,
}) })
if err != nil { if err != nil {
p.DisplayError(fmt.Errorf("unable to get the list of items of scene '%s': %w", sceneName, err)) p.DisplayError(
fmt.Errorf("unable to get the list of items of scene '%s': %w", sceneName, err),
)
return return
} }
for _, item := range resp.SceneItems { for _, item := range resp.SceneItems {
@@ -649,7 +667,9 @@ func (p *Panel) editMonitorElementWindow(
brightness.SetText(fmt.Sprintf("%f", brightnessValue)) brightness.SetText(fmt.Sprintf("%f", brightnessValue))
obsVideoUpdateInterval := xfyne.NewNumericalEntry() obsVideoUpdateInterval := xfyne.NewNumericalEntry()
obsVideoUpdateInterval.SetText(fmt.Sprintf("%v", time.Duration(obsVideoSource.UpdateInterval).Seconds())) obsVideoUpdateInterval.SetText(
fmt.Sprintf("%v", time.Duration(obsVideoSource.UpdateInterval).Seconds()),
)
obsVideoUpdateInterval.OnChanged = func(s string) { obsVideoUpdateInterval.OnChanged = func(s string) {
if s == "" || s == "-" { if s == "" || s == "-" {
s = "0.2" s = "0.2"
@@ -767,7 +787,15 @@ func (p *Panel) editMonitorElementWindow(
widget.NewLabel("Source:"), widget.NewLabel("Source:"),
sourceOBSVideoSelect, sourceOBSVideoSelect,
widget.NewLabel("Source image size (use '0' for preserving the original size or ratio):"), 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`)), container.NewHBox(
widget.NewLabel("X:"),
sourceWidth,
widget.NewLabel(`px`),
widget.NewSeparator(),
widget.NewLabel("Y:"),
sourceHeight,
widget.NewLabel(`px`),
),
widget.NewLabel("Format:"), widget.NewLabel("Format:"),
imageFormatSelect, imageFormatSelect,
widget.NewLabel("Update interval:"), widget.NewLabel("Update interval:"),
@@ -779,7 +807,9 @@ func (p *Panel) editMonitorElementWindow(
}) })
obsVolumeUpdateInterval := xfyne.NewNumericalEntry() obsVolumeUpdateInterval := xfyne.NewNumericalEntry()
obsVolumeUpdateInterval.SetText(fmt.Sprintf("%v", time.Duration(obsVideoSource.UpdateInterval).Seconds())) obsVolumeUpdateInterval.SetText(
fmt.Sprintf("%v", time.Duration(obsVideoSource.UpdateInterval).Seconds()),
)
obsVolumeUpdateInterval.OnChanged = func(s string) { obsVolumeUpdateInterval.OnChanged = func(s string) {
if s == "" || s == "-" { if s == "" || s == "-" {
s = "0.2" s = "0.2"
@@ -796,7 +826,11 @@ func (p *Panel) editMonitorElementWindow(
if volumeColorActiveParsed, err = colorx.Parse(obsVolumeSource.ColorActive); err != nil { if volumeColorActiveParsed, err = colorx.Parse(obsVolumeSource.ColorActive); err != nil {
volumeColorActiveParsed = color.RGBA{R: 0, G: 255, B: 0, A: 255} volumeColorActiveParsed = color.RGBA{R: 0, G: 255, B: 0, A: 255}
} }
volumeColorActive := colorpicker.NewColorSelectModalRect(w, fyne.NewSize(30, 20), volumeColorActiveParsed) volumeColorActive := colorpicker.NewColorSelectModalRect(
w,
fyne.NewSize(30, 20),
volumeColorActiveParsed,
)
volumeColorActive.SetOnChange(func(c color.Color) { volumeColorActive.SetOnChange(func(c color.Color) {
r32, g32, b32, a32 := c.RGBA() r32, g32, b32, a32 := c.RGBA()
r8, g8, b8, a8 := uint8(r32>>8), uint8(g32>>8), uint8(b32>>8), uint8(a32>>8) r8, g8, b8, a8 := uint8(r32>>8), uint8(g32>>8), uint8(b32>>8), uint8(a32>>8)
@@ -807,7 +841,11 @@ func (p *Panel) editMonitorElementWindow(
if volumeColorPassiveParsed, err = colorx.Parse(obsVolumeSource.ColorPassive); err != nil { if volumeColorPassiveParsed, err = colorx.Parse(obsVolumeSource.ColorPassive); err != nil {
volumeColorPassiveParsed = color.RGBA{R: 0, G: 0, B: 0, A: 0} volumeColorPassiveParsed = color.RGBA{R: 0, G: 0, B: 0, A: 0}
} }
volumeColorPassive := colorpicker.NewColorSelectModalRect(w, fyne.NewSize(30, 20), volumeColorPassiveParsed) volumeColorPassive := colorpicker.NewColorSelectModalRect(
w,
fyne.NewSize(30, 20),
volumeColorPassiveParsed,
)
volumeColorPassive.SetOnChange(func(c color.Color) { volumeColorPassive.SetOnChange(func(c color.Color) {
r32, g32, b32, a32 := c.RGBA() r32, g32, b32, a32 := c.RGBA()
r8, g8, b8, a8 := uint8(r32>>8), uint8(g32>>8), uint8(b32>>8), uint8(a32>>8) r8, g8, b8, a8 := uint8(r32>>8), uint8(g32>>8), uint8(b32>>8), uint8(a32>>8)
@@ -884,11 +922,33 @@ func (p *Panel) editMonitorElementWindow(
widget.NewLabel("Z-Index / layer:"), widget.NewLabel("Z-Index / layer:"),
zIndex, zIndex,
widget.NewLabel("Display size:"), widget.NewLabel("Display size:"),
container.NewHBox(widget.NewLabel("X:"), displayWidth, widget.NewLabel(`%`), widget.NewSeparator(), widget.NewLabel("Y:"), displayHeight, widget.NewLabel(`%`)), container.NewHBox(
widget.NewLabel("X:"),
displayWidth,
widget.NewLabel(`%`),
widget.NewSeparator(),
widget.NewLabel("Y:"),
displayHeight,
widget.NewLabel(`%`),
),
widget.NewLabel("Align:"), widget.NewLabel("Align:"),
container.NewHBox(widget.NewLabel("X:"), alignX, widget.NewSeparator(), widget.NewLabel("Y:"), alignY), container.NewHBox(
widget.NewLabel("X:"),
alignX,
widget.NewSeparator(),
widget.NewLabel("Y:"),
alignY,
),
widget.NewLabel("Offset:"), widget.NewLabel("Offset:"),
container.NewHBox(widget.NewLabel("X:"), offsetX, widget.NewLabel(`%`), widget.NewSeparator(), widget.NewLabel("Y:"), offsetY, widget.NewLabel(`%`)), container.NewHBox(
widget.NewLabel("X:"),
offsetX,
widget.NewLabel(`%`),
widget.NewSeparator(),
widget.NewLabel("Y:"),
offsetY,
widget.NewLabel(`%`),
),
widget.NewLabel("Quality:"), widget.NewLabel("Quality:"),
isLossless, isLossless,
imageQuality, imageQuality,

View File

@@ -248,7 +248,11 @@ func (p *Panel) LazyInitStreamD(ctx context.Context) (_err error) {
if p.Config.RemoteStreamDAddr != "" { if p.Config.RemoteStreamDAddr != "" {
if err := p.initRemoteStreamD(ctx); err != nil { if err := p.initRemoteStreamD(ctx); err != nil {
return fmt.Errorf("unable to initialize the remote stream controller '%s': %w", p.Config.RemoteStreamDAddr, err) return fmt.Errorf(
"unable to initialize the remote stream controller '%s': %w",
p.Config.RemoteStreamDAddr,
err,
)
} }
} else { } else {
if err := p.initBuiltinStreamD(ctx); err != nil { if err := p.initBuiltinStreamD(ctx); err != nil {
@@ -312,7 +316,12 @@ func (p *Panel) Loop(ctx context.Context, opts ...LoopOption) error {
p.setStatusFunc("Connecting...") p.setStatusFunc("Connecting...")
err := p.startOAuthListenerForRemoteStreamD(ctx, streamD) err := p.startOAuthListenerForRemoteStreamD(ctx, streamD)
if err != nil { if err != nil {
p.setStatusFunc(fmt.Sprintf("Connection failed, please restart the application.\n\nError: %v", err)) p.setStatusFunc(
fmt.Sprintf(
"Connection failed, please restart the application.\n\nError: %v",
err,
),
)
<-ctx.Done() <-ctx.Done()
} }
closeLoadingWindow() closeLoadingWindow()
@@ -341,7 +350,9 @@ func (p *Panel) Loop(ctx context.Context, opts ...LoopOption) error {
p.initMainWindow(ctx, initCfg.StartingPage) p.initMainWindow(ctx, initCfg.StartingPage)
if streamDRunErr != nil { if streamDRunErr != nil {
p.DisplayError(fmt.Errorf("unable to initialize the streaming controllers: %w", streamDRunErr)) p.DisplayError(
fmt.Errorf("unable to initialize the streaming controllers: %w", streamDRunErr),
)
} }
logger.Tracef(ctx, "p.rearrangeProfiles") logger.Tracef(ctx, "p.rearrangeProfiles")
@@ -367,7 +378,10 @@ func (p *Panel) startOAuthListenerForRemoteStreamD(
streamD *client.Client, streamD *client.Client,
) error { ) error {
ctx, cancelFn := context.WithCancel(ctx) ctx, cancelFn := context.WithCancel(ctx)
receiver, listenPort, err := oauthhandler.NewCodeReceiver(ctx, p.Config.OAuth.ListenPorts.Twitch) receiver, listenPort, err := oauthhandler.NewCodeReceiver(
ctx,
p.Config.OAuth.ListenPorts.Twitch,
)
if err != nil { if err != nil {
cancelFn() cancelFn()
return fmt.Errorf("unable to start listener for OAuth responses: %w", err) return fmt.Errorf("unable to start listener for OAuth responses: %w", err)
@@ -401,7 +415,13 @@ func (p *Panel) startOAuthListenerForRemoteStreamD(
} }
if err := p.openBrowser(ctx, req.GetAuthURL(), "It is required to confirm access in Twitch/YouTube using browser"); err != nil { if err := p.openBrowser(ctx, req.GetAuthURL(), "It is required to confirm access in Twitch/YouTube using browser"); err != nil {
p.DisplayError(fmt.Errorf("unable to open browser with URL '%s': %w", req.GetAuthURL(), err)) p.DisplayError(
fmt.Errorf(
"unable to open browser with URL '%s': %w",
req.GetAuthURL(),
err,
),
)
continue continue
} }
@@ -420,7 +440,13 @@ func (p *Panel) startOAuthListenerForRemoteStreamD(
Code: code, Code: code,
}) })
if err != nil { if err != nil {
p.DisplayError(fmt.Errorf("unable to submit the oauth code of '%s': %w", req.GetPlatID(), err)) p.DisplayError(
fmt.Errorf(
"unable to submit the oauth code of '%s': %w",
req.GetPlatID(),
err,
),
)
continue continue
} }
} }
@@ -626,13 +652,19 @@ func (p *Panel) OnSubmittedOAuthCode(
return nil return nil
} }
func (p *Panel) OAuthHandlerTwitch(ctx context.Context, arg oauthhandler.OAuthHandlerArgument) error { func (p *Panel) OAuthHandlerTwitch(
ctx context.Context,
arg oauthhandler.OAuthHandlerArgument,
) error {
logger.Infof(ctx, "OAuthHandlerTwitch: %#+v", arg) logger.Infof(ctx, "OAuthHandlerTwitch: %#+v", arg)
defer logger.Infof(ctx, "/OAuthHandlerTwitch") defer logger.Infof(ctx, "/OAuthHandlerTwitch")
return p.oauthHandler(ctx, twitch.ID, arg) return p.oauthHandler(ctx, twitch.ID, arg)
} }
func (p *Panel) OAuthHandlerYouTube(ctx context.Context, arg oauthhandler.OAuthHandlerArgument) error { func (p *Panel) OAuthHandlerYouTube(
ctx context.Context,
arg oauthhandler.OAuthHandlerArgument,
) error {
logger.Infof(ctx, "OAuthHandlerYouTube: %#+v", arg) logger.Infof(ctx, "OAuthHandlerYouTube: %#+v", arg)
defer logger.Infof(ctx, "/OAuthHandlerYouTube") defer logger.Infof(ctx, "/OAuthHandlerYouTube")
return p.oauthHandler(ctx, youtube.ID, arg) return p.oauthHandler(ctx, youtube.ID, arg)
@@ -657,7 +689,11 @@ func (p *Panel) oauthHandler(
return fmt.Errorf("unable to open browser with URL '%s': %w", arg.AuthURL, err) return fmt.Errorf("unable to open browser with URL '%s': %w", arg.AuthURL, err)
} }
logger.Infof(ctx, "Your browser has been launched (URL: %s).\nPlease approve the permissions.\n", arg.AuthURL) logger.Infof(
ctx,
"Your browser has been launched (URL: %s).\nPlease approve the permissions.\n",
arg.AuthURL,
)
// Wait for the web server to get the code. // Wait for the web server to get the code.
code := <-codeCh code := <-codeCh
@@ -685,7 +721,12 @@ func (p *Panel) openBrowser(
if p.Config.Browser.Command != "" { if p.Config.Browser.Command != "" {
args := []string{p.Config.Browser.Command, url} args := []string{p.Config.Browser.Command, url}
logger.Debugf(ctx, "the browser command is configured to be '%s', so running '%s'", p.Config.Browser.Command, strings.Join(args, " ")) logger.Debugf(
ctx,
"the browser command is configured to be '%s', so running '%s'",
p.Config.Browser.Command,
strings.Join(args, " "),
)
return exec.Command(args[0], args[1:]...).Start() return exec.Command(args[0], args[1:]...).Start()
} }
@@ -759,7 +800,9 @@ func (p *Panel) InputTwitchUserInfo(
resizeWindow(w, fyne.NewSize(600, 200)) resizeWindow(w, fyne.NewSize(600, 200))
channelField := widget.NewEntry() channelField := widget.NewEntry()
channelField.SetPlaceHolder("channel ID (copy&paste it from the browser: https://www.twitch.tv/<the channel ID is here>)") channelField.SetPlaceHolder(
"channel ID (copy&paste it from the browser: https://www.twitch.tv/<the channel ID is here>)",
)
clientIDField := widget.NewEntry() clientIDField := widget.NewEntry()
clientIDField.SetPlaceHolder("client ID") clientIDField.SetPlaceHolder("client ID")
clientSecretField := widget.NewEntry() clientSecretField := widget.NewEntry()
@@ -767,7 +810,10 @@ func (p *Panel) InputTwitchUserInfo(
instructionText := widget.NewRichText( instructionText := widget.NewRichText(
&widget.TextSegment{Text: "Go to\n", Style: widget.RichTextStyle{Inline: true}}, &widget.TextSegment{Text: "Go to\n", Style: widget.RichTextStyle{Inline: true}},
&widget.HyperlinkSegment{Text: twitchAppsCreateLink.String(), URL: twitchAppsCreateLink}, &widget.HyperlinkSegment{Text: twitchAppsCreateLink.String(), URL: twitchAppsCreateLink},
&widget.TextSegment{Text: `,` + "\n" + `create an application (enter "http://localhost:8091/" as the "OAuth Redirect URLs" value), then click "Manage" then "New Secret", and copy&paste client ID and client secret.`, Style: widget.RichTextStyle{Inline: true}}, &widget.TextSegment{
Text: `,` + "\n" + `create an application (enter "http://localhost:8091/" as the "OAuth Redirect URLs" value), then click "Manage" then "New Secret", and copy&paste client ID and client secret.`,
Style: widget.RichTextStyle{Inline: true},
},
) )
instructionText.Wrapping = fyne.TextWrapWord instructionText.Wrapping = fyne.TextWrapWord
@@ -810,7 +856,9 @@ func (p *Panel) InputTwitchUserInfo(
return true, nil return true, nil
} }
var youtubeCredentialsCreateLink, _ = url.Parse("https://console.cloud.google.com/apis/credentials/oauthclient") var youtubeCredentialsCreateLink, _ = url.Parse(
"https://console.cloud.google.com/apis/credentials/oauthclient",
)
func (p *Panel) InputYouTubeUserInfo( func (p *Panel) InputYouTubeUserInfo(
ctx context.Context, ctx context.Context,
@@ -825,10 +873,22 @@ func (p *Panel) InputYouTubeUserInfo(
clientSecretField.SetPlaceHolder("client secret") clientSecretField.SetPlaceHolder("client secret")
instructionText := widget.NewRichText( instructionText := widget.NewRichText(
&widget.TextSegment{Text: "Go to\n", Style: widget.RichTextStyle{Inline: true}}, &widget.TextSegment{Text: "Go to\n", Style: widget.RichTextStyle{Inline: true}},
&widget.HyperlinkSegment{Text: youtubeCredentialsCreateLink.String(), URL: youtubeCredentialsCreateLink}, &widget.HyperlinkSegment{
&widget.TextSegment{Text: `,` + "\n" + `configure "consent screen" (note: you may add yourself into Test Users to avoid problems further on, and don't forget to add "YouTube Data API v3" scopes) and go back to` + "\n", Style: widget.RichTextStyle{Inline: true}}, Text: youtubeCredentialsCreateLink.String(),
&widget.HyperlinkSegment{Text: youtubeCredentialsCreateLink.String(), URL: youtubeCredentialsCreateLink}, URL: youtubeCredentialsCreateLink,
&widget.TextSegment{Text: `,` + "\n" + `choose "Desktop app", confirm and copy&paste client ID and client secret.`, Style: widget.RichTextStyle{Inline: true}}, },
&widget.TextSegment{
Text: `,` + "\n" + `configure "consent screen" (note: you may add yourself into Test Users to avoid problems further on, and don't forget to add "YouTube Data API v3" scopes) and go back to` + "\n",
Style: widget.RichTextStyle{Inline: true},
},
&widget.HyperlinkSegment{
Text: youtubeCredentialsCreateLink.String(),
URL: youtubeCredentialsCreateLink,
},
&widget.TextSegment{
Text: `,` + "\n" + `choose "Desktop app", confirm and copy&paste client ID and client secret.`,
Style: widget.RichTextStyle{Inline: true},
},
) )
instructionText.Wrapping = fyne.TextWrapWord instructionText.Wrapping = fyne.TextWrapWord
@@ -879,11 +939,23 @@ func (p *Panel) profileCreateOrUpdate(ctx context.Context, profile Profile) erro
continue continue
} }
cfg.Backends[platformName].StreamProfiles[profile.Name] = platformProfile cfg.Backends[platformName].StreamProfiles[profile.Name] = platformProfile
logger.Tracef(ctx, "profileCreateOrUpdate(%s): cfg.Backends[%s].StreamProfiles[%s] = %#+v", profile.Name, platformName, profile.Name, platformProfile) logger.Tracef(
ctx,
"profileCreateOrUpdate(%s): cfg.Backends[%s].StreamProfiles[%s] = %#+v",
profile.Name,
platformName,
profile.Name,
platformProfile,
)
} }
cfg.ProfileMetadata[profile.Name] = profile.ProfileMetadata cfg.ProfileMetadata[profile.Name] = profile.ProfileMetadata
logger.Tracef(ctx, "profileCreateOrUpdate(%s): cfg.Backends == %#+v", profile.Name, cfg.Backends) logger.Tracef(
ctx,
"profileCreateOrUpdate(%s): cfg.Backends == %#+v",
profile.Name,
cfg.Backends,
)
err = p.StreamD.SetConfig(ctx, cfg) err = p.StreamD.SetConfig(ctx, cfg)
if err != nil { if err != nil {
@@ -1002,7 +1074,11 @@ func (p *Panel) refilterProfiles(ctx context.Context) {
if p.filterValue == "" { if p.filterValue == "" {
p.profilesOrderFiltered = p.profilesOrderFiltered[:len(p.profilesOrder)] p.profilesOrderFiltered = p.profilesOrderFiltered[:len(p.profilesOrder)]
copy(p.profilesOrderFiltered, p.profilesOrder) copy(p.profilesOrderFiltered, p.profilesOrder)
logger.Tracef(ctx, "refilterProfiles(): profilesOrderFiltered <- p.profilesOrder: %#+v", p.profilesOrder) logger.Tracef(
ctx,
"refilterProfiles(): profilesOrderFiltered <- p.profilesOrder: %#+v",
p.profilesOrder,
)
logger.Tracef(ctx, "refilterProfiles(): p.profilesListWidget.Refresh()") logger.Tracef(ctx, "refilterProfiles(): p.profilesListWidget.Refresh()")
p.profilesListWidget.Refresh() p.profilesListWidget.Refresh()
return return
@@ -1037,7 +1113,12 @@ func (p *Panel) refilterProfiles(ctx context.Context) {
} }
if titleMatch || subValueMatch { if titleMatch || subValueMatch {
logger.Tracef(ctx, "refilterProfiles(): profilesOrderFiltered[%3d] = %s", len(p.profilesOrderFiltered), profileName) logger.Tracef(
ctx,
"refilterProfiles(): profilesOrderFiltered[%3d] = %s",
len(p.profilesOrderFiltered),
profileName,
)
p.profilesOrderFiltered = append(p.profilesOrderFiltered, profileName) p.profilesOrderFiltered = append(p.profilesOrderFiltered, profileName)
} }
} }
@@ -1153,10 +1234,18 @@ func (p *Panel) openSettingsWindow(ctx context.Context) error {
logger.Debugf(ctx, "current OBS config: %#+v", obsCfg) logger.Debugf(ctx, "current OBS config: %#+v", obsCfg)
} }
cmdBeforeStartStream, _ := cfg.Backends[obs.ID].GetCustomString(config.CustomConfigKeyBeforeStreamStart) cmdBeforeStartStream, _ := cfg.Backends[obs.ID].GetCustomString(
cmdBeforeStopStream, _ := cfg.Backends[obs.ID].GetCustomString(config.CustomConfigKeyBeforeStreamStop) config.CustomConfigKeyBeforeStreamStart,
cmdAfterStartStream, _ := cfg.Backends[obs.ID].GetCustomString(config.CustomConfigKeyAfterStreamStart) )
cmdAfterStopStream, _ := cfg.Backends[obs.ID].GetCustomString(config.CustomConfigKeyAfterStreamStop) cmdBeforeStopStream, _ := cfg.Backends[obs.ID].GetCustomString(
config.CustomConfigKeyBeforeStreamStop,
)
cmdAfterStartStream, _ := cfg.Backends[obs.ID].GetCustomString(
config.CustomConfigKeyAfterStreamStart,
)
cmdAfterStopStream, _ := cfg.Backends[obs.ID].GetCustomString(
config.CustomConfigKeyAfterStreamStop,
)
beforeStartStreamCommandEntry := widget.NewEntry() beforeStartStreamCommandEntry := widget.NewEntry()
beforeStartStreamCommandEntry.SetText(cmdBeforeStartStream) beforeStartStreamCommandEntry.SetText(cmdBeforeStartStream)
@@ -1210,7 +1299,9 @@ func (p *Panel) openSettingsWindow(ctx context.Context) error {
w.Close() w.Close()
}) })
templateInstruction := widget.NewRichTextFromMarkdown("Commands support [Go templates](https://pkg.go.dev/text/template) with two custom functions predefined:\n* `devnull` nullifies any inputs\n* `httpGET` makes an HTTP GET request and inserts the response body") templateInstruction := widget.NewRichTextFromMarkdown(
"Commands support [Go templates](https://pkg.go.dev/text/template) with two custom functions predefined:\n* `devnull` nullifies any inputs\n* `httpGET` makes an HTTP GET request and inserts the response body",
)
templateInstruction.Wrapping = fyne.TextWrapWord templateInstruction.Wrapping = fyne.TextWrapWord
obsAlreadyLoggedIn := widget.NewLabel("") obsAlreadyLoggedIn := widget.NewLabel("")
@@ -1368,7 +1459,10 @@ func (p *Panel) openSettingsWindow(ctx context.Context) error {
displayIDSelector, displayIDSelector,
widget.NewLabel("Crop to:"), widget.NewLabel("Crop to:"),
container.NewHBox( container.NewHBox(
screenshotCropXEntry, screenshotCropYEntry, screenshotCropWEntry, screenshotCropHEntry, screenshotCropXEntry,
screenshotCropYEntry,
screenshotCropWEntry,
screenshotCropHEntry,
), ),
widget.NewSeparator(), widget.NewSeparator(),
widget.NewSeparator(), widget.NewSeparator(),
@@ -1512,7 +1606,9 @@ func (p *Panel) getUpdatedStatus_backends_noLock(ctx context.Context) {
} { } {
isEnabled, err := p.StreamD.IsBackendEnabled(ctx, backendID) isEnabled, err := p.StreamD.IsBackendEnabled(ctx, backendID)
if err != nil { if err != nil {
p.ReportError(fmt.Errorf("unable to get info if backend '%s' is enabled: %w", backendID, err)) p.ReportError(
fmt.Errorf("unable to get info if backend '%s' is enabled: %w", backendID, err),
)
} }
backendEnabled[backendID] = isEnabled backendEnabled[backendID] = isEnabled
} }
@@ -1629,7 +1725,12 @@ func (p *Panel) getUpdatedStatus_startStopStreamButton_noLock(ctx context.Contex
logger.Tracef(ctx, "ytStreamStatus == %#+v", ytStreamStatus) logger.Tracef(ctx, "ytStreamStatus == %#+v", ytStreamStatus)
if d, ok := ytStreamStatus.CustomData.(youtube.StreamStatusCustomData); ok { if d, ok := ytStreamStatus.CustomData.(youtube.StreamStatusCustomData); ok {
logger.Tracef(ctx, "len(d.UpcomingBroadcasts) == %d; len(d.Streams) == %d", len(d.UpcomingBroadcasts), len(d.Streams)) logger.Tracef(
ctx,
"len(d.UpcomingBroadcasts) == %d; len(d.Streams) == %d",
len(d.UpcomingBroadcasts),
len(d.Streams),
)
if len(d.UpcomingBroadcasts) != 0 { if len(d.UpcomingBroadcasts) != 0 {
p.startStopButton.Enable() p.startStopButton.Enable()
} }
@@ -1694,18 +1795,30 @@ func (p *Panel) initMainWindow(
profileControl.Add(button) profileControl.Add(button)
} }
p.setupStreamButton = widget.NewButtonWithIcon(setupStreamString(), theme.SettingsIcon(), func() { p.setupStreamButton = widget.NewButtonWithIcon(
setupStreamString(),
theme.SettingsIcon(),
func() {
p.onSetupStreamButton(ctx) p.onSetupStreamButton(ctx)
}) },
)
p.setupStreamButton.Disable() p.setupStreamButton.Disable()
p.startStopButton = widget.NewButtonWithIcon(startStreamString(), theme.MediaRecordIcon(), func() { p.startStopButton = widget.NewButtonWithIcon(
startStreamString(),
theme.MediaRecordIcon(),
func() {
p.onStartStopButton(ctx) p.onStartStopButton(ctx)
}) },
)
p.startStopButton.Importance = widget.SuccessImportance p.startStopButton.Importance = widget.SuccessImportance
p.startStopButton.Disable() p.startStopButton.Disable()
profilesList := widget.NewList(p.profilesListLength, p.profilesListItemCreate, p.profilesListItemUpdate) profilesList := widget.NewList(
p.profilesListLength,
p.profilesListItemCreate,
p.profilesListItemUpdate,
)
profilesList.OnSelected = func(id widget.ListItemID) { profilesList.OnSelected = func(id widget.ListItemID) {
p.onProfilesListSelect(id) p.onProfilesListSelect(id)
for _, button := range selectedProfileButtons { for _, button := range selectedProfileButtons {
@@ -2116,7 +2229,9 @@ func (p *Panel) setupStreamNoLock(ctx context.Context) {
} { } {
isEnabled, err := p.StreamD.IsBackendEnabled(ctx, backendID) isEnabled, err := p.StreamD.IsBackendEnabled(ctx, backendID)
if err != nil { if err != nil {
p.DisplayError(fmt.Errorf("unable to get info if backend '%s' is enabled: %w", backendID, err)) p.DisplayError(
fmt.Errorf("unable to get info if backend '%s' is enabled: %w", backendID, err),
)
return return
} }
backendEnabled[backendID] = isEnabled backendEnabled[backendID] = isEnabled
@@ -2268,7 +2383,9 @@ func (p *Panel) stopStreamNoLock(ctx context.Context) {
} { } {
isEnabled, err := p.StreamD.IsBackendEnabled(ctx, backendID) isEnabled, err := p.StreamD.IsBackendEnabled(ctx, backendID)
if err != nil { if err != nil {
p.DisplayError(fmt.Errorf("unable to get info if backend '%s' is enabled: %w", backendID, err)) p.DisplayError(
fmt.Errorf("unable to get info if backend '%s' is enabled: %w", backendID, err),
)
return return
} }
backendEnabled[backendID] = isEnabled backendEnabled[backendID] = isEnabled
@@ -2627,9 +2744,13 @@ func newTagsEditor(
tagsControlsContainer.Add(widget.NewSeparator()) tagsControlsContainer.Add(widget.NewSeparator())
tagsControlsContainer.Add(widget.NewSeparator()) tagsControlsContainer.Add(widget.NewSeparator())
for _, additionalButtonInfo := range additionalButtons { for _, additionalButtonInfo := range additionalButtons {
button := widget.NewButtonWithIcon(additionalButtonInfo.Label, additionalButtonInfo.Icon, func() { button := widget.NewButtonWithIcon(
additionalButtonInfo.Label,
additionalButtonInfo.Icon,
func() {
additionalButtonInfo.Callback(t, selectedTagsOrdered()) additionalButtonInfo.Callback(t, selectedTagsOrdered())
}) },
)
tagsControlsContainer.Add(button) tagsControlsContainer.Add(button)
} }
@@ -2655,7 +2776,11 @@ func newTagsEditor(
tagLabel := tagName tagLabel := tagName
overflown := false overflown := false
for { for {
size := fyne.MeasureText(tagLabel, fyne.CurrentApp().Settings().Theme().Size("text"), fyne.TextStyle{}) size := fyne.MeasureText(
tagLabel,
fyne.CurrentApp().Settings().Theme().Size("text"),
fyne.TextStyle{},
)
if size.Width < 100 { if size.Width < 100 {
break break
} }
@@ -2732,7 +2857,9 @@ func (p *Panel) profileWindow(
isEnabled, err := p.StreamD.IsBackendEnabled(ctx, backendID) isEnabled, err := p.StreamD.IsBackendEnabled(ctx, backendID)
if err != nil { if err != nil {
w.Close() w.Close()
p.DisplayError(fmt.Errorf("unable to get info if backend '%s' is enabled: %w", backendID, err)) p.DisplayError(
fmt.Errorf("unable to get info if backend '%s' is enabled: %w", backendID, err),
)
return nil return nil
} }
backendEnabled[backendID] = isEnabled backendEnabled[backendID] = isEnabled
@@ -2756,7 +2883,9 @@ func (p *Panel) profileWindow(
bottomContent = append(bottomContent, widget.NewRichTextFromMarkdown("# OBS:")) bottomContent = append(bottomContent, widget.NewRichTextFromMarkdown("# OBS:"))
if backendEnabled[obs.ID] { if backendEnabled[obs.ID] {
if platProfile := values.PerPlatform[obs.ID]; platProfile != nil { if platProfile := values.PerPlatform[obs.ID]; platProfile != nil {
obsProfile = ptr(streamcontrol.GetPlatformSpecificConfig[obs.StreamProfile](ctx, platProfile)) obsProfile = ptr(
streamcontrol.GetPlatformSpecificConfig[obs.StreamProfile](ctx, platProfile),
)
} else { } else {
obsProfile = &obs.StreamProfile{} obsProfile = &obs.StreamProfile{}
} }
@@ -2778,7 +2907,9 @@ func (p *Panel) profileWindow(
} }
if platProfile := values.PerPlatform[twitch.ID]; platProfile != nil { if platProfile := values.PerPlatform[twitch.ID]; platProfile != nil {
twitchProfile = ptr(streamcontrol.GetPlatformSpecificConfig[twitch.StreamProfile](ctx, platProfile)) twitchProfile = ptr(
streamcontrol.GetPlatformSpecificConfig[twitch.StreamProfile](ctx, platProfile),
)
for _, tag := range twitchProfile.Tags { for _, tag := range twitchProfile.Tags {
addTag(tag) addTag(tag)
} }
@@ -2802,9 +2933,13 @@ func (p *Panel) profileWindow(
if strings.Contains(cleanTwitchCategoryName(cat.Name), text) { if strings.Contains(cleanTwitchCategoryName(cat.Name), text) {
selectedTwitchCategoryContainer := container.NewHBox() selectedTwitchCategoryContainer := container.NewHBox()
catName := cat.Name catName := cat.Name
tagContainerRemoveButton := widget.NewButtonWithIcon(catName, theme.ContentAddIcon(), func() { tagContainerRemoveButton := widget.NewButtonWithIcon(
catName,
theme.ContentAddIcon(),
func() {
twitchCategory.OnSubmitted(catName) twitchCategory.OnSubmitted(catName)
}) },
)
selectedTwitchCategoryContainer.Add(tagContainerRemoveButton) selectedTwitchCategoryContainer.Add(tagContainerRemoveButton)
selectTwitchCategoryBox.Add(selectedTwitchCategoryContainer) selectTwitchCategoryBox.Add(selectedTwitchCategoryContainer)
count++ count++
@@ -2821,10 +2956,14 @@ func (p *Panel) profileWindow(
setSelectedTwitchCategory := func(catName string) { setSelectedTwitchCategory := func(catName string) {
selectedTwitchCategoryBox.RemoveAll() selectedTwitchCategoryBox.RemoveAll()
selectedTwitchCategoryContainer := container.NewHBox() selectedTwitchCategoryContainer := container.NewHBox()
tagContainerRemoveButton := widget.NewButtonWithIcon(catName, theme.ContentClearIcon(), func() { tagContainerRemoveButton := widget.NewButtonWithIcon(
catName,
theme.ContentClearIcon(),
func() {
selectedTwitchCategoryBox.Remove(selectedTwitchCategoryContainer) selectedTwitchCategoryBox.Remove(selectedTwitchCategoryContainer)
twitchProfile.CategoryName = nil twitchProfile.CategoryName = nil
}) },
)
selectedTwitchCategoryContainer.Add(tagContainerRemoveButton) selectedTwitchCategoryContainer.Add(tagContainerRemoveButton)
selectedTwitchCategoryBox.Add(selectedTwitchCategoryContainer) selectedTwitchCategoryBox.Add(selectedTwitchCategoryContainer)
twitchProfile.CategoryName = &catName twitchProfile.CategoryName = &catName
@@ -2878,7 +3017,9 @@ func (p *Panel) profileWindow(
youtubeTags = append(youtubeTags, tagName) youtubeTags = append(youtubeTags, tagName)
} }
if platProfile := values.PerPlatform[youtube.ID]; platProfile != nil { if platProfile := values.PerPlatform[youtube.ID]; platProfile != nil {
youtubeProfile = ptr(streamcontrol.GetPlatformSpecificConfig[youtube.StreamProfile](ctx, platProfile)) youtubeProfile = ptr(
streamcontrol.GetPlatformSpecificConfig[youtube.StreamProfile](ctx, platProfile),
)
for _, tag := range youtubeProfile.Tags { for _, tag := range youtubeProfile.Tags {
addTag(tag) addTag(tag)
} }
@@ -2890,8 +3031,14 @@ func (p *Panel) profileWindow(
youtubeProfile.AutoNumerate = b youtubeProfile.AutoNumerate = b
}) })
autoNumerateCheck.SetChecked(youtubeProfile.AutoNumerate) autoNumerateCheck.SetChecked(youtubeProfile.AutoNumerate)
autoNumerateHint := NewHintWidget(w, "When enabled, it adds the number of the stream to the stream's title.\n\nFor example 'Watching presidential debate' -> 'Watching presidential debate [#52]'.") autoNumerateHint := NewHintWidget(
bottomContent = append(bottomContent, container.NewHBox(autoNumerateCheck, autoNumerateHint)) w,
"When enabled, it adds the number of the stream to the stream's title.\n\nFor example 'Watching presidential debate' -> 'Watching presidential debate [#52]'.",
)
bottomContent = append(
bottomContent,
container.NewHBox(autoNumerateCheck, autoNumerateHint),
)
youtubeTemplate := widget.NewEntry() youtubeTemplate := widget.NewEntry()
youtubeTemplate.SetPlaceHolder("youtube live recording template") youtubeTemplate.SetPlaceHolder("youtube live recording template")
@@ -2909,9 +3056,13 @@ func (p *Panel) profileWindow(
if strings.Contains(cleanYoutubeRecordingName(bc.Snippet.Title), text) { if strings.Contains(cleanYoutubeRecordingName(bc.Snippet.Title), text) {
selectedYoutubeRecordingsContainer := container.NewHBox() selectedYoutubeRecordingsContainer := container.NewHBox()
recName := bc.Snippet.Title recName := bc.Snippet.Title
tagContainerRemoveButton := widget.NewButtonWithIcon(recName, theme.ContentAddIcon(), func() { tagContainerRemoveButton := widget.NewButtonWithIcon(
recName,
theme.ContentAddIcon(),
func() {
youtubeTemplate.OnSubmitted(recName) youtubeTemplate.OnSubmitted(recName)
}) },
)
selectedYoutubeRecordingsContainer.Add(tagContainerRemoveButton) selectedYoutubeRecordingsContainer.Add(tagContainerRemoveButton)
selectYoutubeTemplateBox.Add(selectedYoutubeRecordingsContainer) selectYoutubeTemplateBox.Add(selectedYoutubeRecordingsContainer)
count++ count++
@@ -2929,10 +3080,14 @@ func (p *Panel) profileWindow(
selectedYoutubeBroadcastBox.RemoveAll() selectedYoutubeBroadcastBox.RemoveAll()
selectedYoutubeBroadcastContainer := container.NewHBox() selectedYoutubeBroadcastContainer := container.NewHBox()
recName := bc.Snippet.Title recName := bc.Snippet.Title
tagContainerRemoveButton := widget.NewButtonWithIcon(recName, theme.ContentClearIcon(), func() { tagContainerRemoveButton := widget.NewButtonWithIcon(
recName,
theme.ContentClearIcon(),
func() {
selectedYoutubeBroadcastBox.Remove(selectedYoutubeBroadcastContainer) selectedYoutubeBroadcastBox.Remove(selectedYoutubeBroadcastContainer)
youtubeProfile.TemplateBroadcastIDs = youtubeProfile.TemplateBroadcastIDs[:0] youtubeProfile.TemplateBroadcastIDs = youtubeProfile.TemplateBroadcastIDs[:0]
}) },
)
selectedYoutubeBroadcastContainer.Add(tagContainerRemoveButton) selectedYoutubeBroadcastContainer.Add(tagContainerRemoveButton)
selectedYoutubeBroadcastBox.Add(selectedYoutubeBroadcastContainer) selectedYoutubeBroadcastBox.Add(selectedYoutubeBroadcastContainer)
youtubeProfile.TemplateBroadcastIDs = []string{bc.Id} youtubeProfile.TemplateBroadcastIDs = []string{bc.Id}
@@ -2966,7 +3121,9 @@ func (p *Panel) profileWindow(
bottomContent = append(bottomContent, youtubeTemplate) bottomContent = append(bottomContent, youtubeTemplate)
templateTagsLabel := widget.NewLabel("Template tags:") templateTagsLabel := widget.NewLabel("Template tags:")
templateTags := widget.NewSelect([]string{"ignore", "use as primary", "use as additional"}, func(s string) { templateTags := widget.NewSelect(
[]string{"ignore", "use as primary", "use as additional"},
func(s string) {
switch s { switch s {
case "ignore": case "ignore":
youtubeProfile.TemplateTags = youtube.TemplateTagsIgnore youtubeProfile.TemplateTags = youtube.TemplateTagsIgnore
@@ -2977,7 +3134,8 @@ func (p *Panel) profileWindow(
default: default:
p.DisplayError(fmt.Errorf("unexpected new value of 'template tags': '%s'", s)) p.DisplayError(fmt.Errorf("unexpected new value of 'template tags': '%s'", s))
} }
}) },
)
switch youtubeProfile.TemplateTags { switch youtubeProfile.TemplateTags {
case youtube.TemplateTagsUndefined, youtube.TemplateTagsIgnore: case youtube.TemplateTagsUndefined, youtube.TemplateTagsIgnore:
templateTags.SetSelected("ignore") templateTags.SetSelected("ignore")
@@ -2986,11 +3144,22 @@ func (p *Panel) profileWindow(
case youtube.TemplateTagsUseAsAdditional: case youtube.TemplateTagsUseAsAdditional:
templateTags.SetSelected("use as additional") templateTags.SetSelected("use as additional")
default: default:
p.DisplayError(fmt.Errorf("unexpected current value of 'template tags': '%s'", youtubeProfile.TemplateTags)) p.DisplayError(
fmt.Errorf(
"unexpected current value of 'template tags': '%s'",
youtubeProfile.TemplateTags,
),
)
} }
templateTags.SetSelected(youtubeProfile.TemplateTags.String()) templateTags.SetSelected(youtubeProfile.TemplateTags.String())
templateTagsHint := NewHintWidget(w, "'ignore' will ignore the tags set in the template; 'use as primary' will put the tags of the template first and then add the profile tags; 'use as additional' will put the tags of the profile first and then add the template tags") templateTagsHint := NewHintWidget(
bottomContent = append(bottomContent, container.NewHBox(templateTagsLabel, templateTags, templateTagsHint)) w,
"'ignore' will ignore the tags set in the template; 'use as primary' will put the tags of the template first and then add the profile tags; 'use as additional' will put the tags of the profile first and then add the template tags",
)
bottomContent = append(
bottomContent,
container.NewHBox(templateTagsLabel, templateTags, templateTagsHint),
)
youtubeTagsEditor := newTagsEditor(youtubeTags, 0) youtubeTagsEditor := newTagsEditor(youtubeTags, 0)
bottomContent = append(bottomContent, widget.NewLabel("Tags:")) bottomContent = append(bottomContent, widget.NewLabel("Tags:"))

View File

@@ -288,7 +288,14 @@ func (p *Panel) displayStreamServers(
p.previousNumBytesLocker.Do(ctx, func() { p.previousNumBytesLocker.Do(ctx, func() {
prevNumBytes := p.previousNumBytes[key] prevNumBytes := p.previousNumBytes[key]
now := time.Now() now := time.Now()
bwStr := bwString(srv.NumBytesProducerRead, prevNumBytes[0], srv.NumBytesConsumerWrote, prevNumBytes[1], now, p.previousNumBytesTS[key]) bwStr := bwString(
srv.NumBytesProducerRead,
prevNumBytes[0],
srv.NumBytesConsumerWrote,
prevNumBytes[1],
now,
p.previousNumBytesTS[key],
)
bwText := widget.NewRichTextWithText(bwStr) bwText := widget.NewRichTextWithText(bwStr)
hasDynamicValue = hasDynamicValue || bwStr != "" hasDynamicValue = hasDynamicValue || bwStr != ""
p.previousNumBytes[key] = [4]uint64{srv.NumBytesProducerRead, srv.NumBytesConsumerWrote} p.previousNumBytes[key] = [4]uint64{srv.NumBytesProducerRead, srv.NumBytesConsumerWrote}
@@ -571,7 +578,14 @@ func (p *Panel) displayStreamDestinations(
} }
func (p *Panel) openAddPlayerWindow(ctx context.Context) { func (p *Panel) openAddPlayerWindow(ctx context.Context) {
p.openAddOrEditPlayerWindow(ctx, "Add player", false, sptypes.DefaultConfig(ctx), nil, p.StreamD.AddStreamPlayer) p.openAddOrEditPlayerWindow(
ctx,
"Add player",
false,
sptypes.DefaultConfig(ctx),
nil,
p.StreamD.AddStreamPlayer,
)
} }
func (p *Panel) openEditPlayerWindow( func (p *Panel) openEditPlayerWindow(
@@ -593,7 +607,14 @@ func (p *Panel) openEditPlayerWindow(
p.DisplayError(fmt.Errorf("unable to find a stream player for '%s'", streamID)) p.DisplayError(fmt.Errorf("unable to find a stream player for '%s'", streamID))
return return
} }
p.openAddOrEditPlayerWindow(ctx, "Edit player", !playerCfg.Disabled, playerCfg.StreamPlayback, &streamID, p.StreamD.UpdateStreamPlayer) p.openAddOrEditPlayerWindow(
ctx,
"Edit player",
!playerCfg.Disabled,
playerCfg.StreamPlayback,
&streamID,
p.StreamD.UpdateStreamPlayer,
)
} }
func (p *Panel) openAddOrEditPlayerWindow( func (p *Panel) openAddOrEditPlayerWindow(
@@ -779,14 +800,28 @@ func (p *Panel) displayStreamPlayers(
c := container.NewHBox() c := container.NewHBox()
deleteButton := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() { deleteButton := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() {
w := dialog.NewConfirm( w := dialog.NewConfirm(
fmt.Sprintf("Delete player for stream '%s' (%s) ?", player.StreamID, player.PlayerType), fmt.Sprintf(
"Delete player for stream '%s' (%s) ?",
player.StreamID,
player.PlayerType,
),
"", "",
func(b bool) { func(b bool) {
if !b { if !b {
return return
} }
logger.Debugf(ctx, "remove player '%s' (%s)", player.StreamID, player.PlayerType) logger.Debugf(
defer logger.Debugf(ctx, "/remove player '%s' (%s)", player.StreamID, player.PlayerType) ctx,
"remove player '%s' (%s)",
player.StreamID,
player.PlayerType,
)
defer logger.Debugf(
ctx,
"/remove player '%s' (%s)",
player.StreamID,
player.PlayerType,
)
err := p.StreamD.RemoveStreamPlayer(ctx, player.StreamID) err := p.StreamD.RemoveStreamPlayer(ctx, player.StreamID)
if err != nil { if err != nil {
p.DisplayError(err) p.DisplayError(err)
@@ -816,8 +851,22 @@ func (p *Panel) displayStreamPlayers(
if !b { if !b {
return return
} }
logger.Debugf(ctx, "stop/start player %s on '%s': disabled:%v->%v", player.PlayerType, player.StreamID, player.Disabled, !player.Disabled) logger.Debugf(
defer logger.Debugf(ctx, "/stop/start player %s on '%s': disabled:%v->%v", player.PlayerType, player.StreamID, player.Disabled, !player.Disabled) ctx,
"stop/start player %s on '%s': disabled:%v->%v",
player.PlayerType,
player.StreamID,
player.Disabled,
!player.Disabled,
)
defer logger.Debugf(
ctx,
"/stop/start player %s on '%s': disabled:%v->%v",
player.PlayerType,
player.StreamID,
player.Disabled,
!player.Disabled,
)
err := p.StreamD.UpdateStreamPlayer( err := p.StreamD.UpdateStreamPlayer(
xcontext.DetachDone(ctx), xcontext.DetachDone(ctx),
player.StreamID, player.StreamID,
@@ -843,7 +892,12 @@ func (p *Panel) displayStreamPlayers(
if !player.Disabled { if !player.Disabled {
pos, err := p.StreamD.StreamPlayerGetPosition(ctx, player.StreamID) pos, err := p.StreamD.StreamPlayerGetPosition(ctx, player.StreamID)
if err != nil { if err != nil {
logger.Errorf(ctx, "unable to get the current position at player '%s': %v", player.StreamID, err) logger.Errorf(
ctx,
"unable to get the current position at player '%s': %v",
player.StreamID,
err,
)
} else { } else {
c.Add(widget.NewSeparator()) c.Add(widget.NewSeparator())
posStr := pos.String() posStr := pos.String()
@@ -932,8 +986,22 @@ func (p *Panel) openAddOrEditRestreamWindow(
quirks sstypes.ForwardingQuirks, quirks sstypes.ForwardingQuirks,
) error, ) error,
) { ) {
logger.Debugf(ctx, "openAddOrEditRestreamWindow(ctx, '%s', '%s', '%s', %#+v)", title, streamID, dstID, fwd) logger.Debugf(
defer logger.Debugf(ctx, "/openAddOrEditRestreamWindow(ctx, '%s', '%s', '%s', %#+v)", title, streamID, dstID, fwd) ctx,
"openAddOrEditRestreamWindow(ctx, '%s', '%s', '%s', %#+v)",
title,
streamID,
dstID,
fwd,
)
defer logger.Debugf(
ctx,
"/openAddOrEditRestreamWindow(ctx, '%s', '%s', '%s', %#+v)",
title,
streamID,
dstID,
fwd,
)
w := p.app.NewWindow(AppName + ": " + title) w := p.app.NewWindow(AppName + ": " + title)
resizeWindow(w, fyne.NewSize(400, 300)) resizeWindow(w, fyne.NewSize(400, 300))
@@ -1146,7 +1214,11 @@ func (p *Panel) displayStreamForwards(
c := container.NewHBox() c := container.NewHBox()
deleteButton := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() { deleteButton := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() {
w := dialog.NewConfirm( w := dialog.NewConfirm(
fmt.Sprintf("Delete restreaming (stream forwarding) %s -> %s ?", fwd.StreamID, fwd.DestinationID), fmt.Sprintf(
"Delete restreaming (stream forwarding) %s -> %s ?",
fwd.StreamID,
fwd.DestinationID,
),
"", "",
func(b bool) { func(b bool) {
if !b { if !b {
@@ -1183,8 +1255,18 @@ func (p *Panel) displayStreamForwards(
if !b { if !b {
return return
} }
logger.Debugf(ctx, "pause/unpause restreaming (stream forwarding): enabled:%v->%v", fwd.Enabled, !fwd.Enabled) logger.Debugf(
defer logger.Debugf(ctx, "/pause/unpause restreaming (stream forwarding): enabled:%v->%v", !fwd.Enabled, fwd.Enabled) ctx,
"pause/unpause restreaming (stream forwarding): enabled:%v->%v",
fwd.Enabled,
!fwd.Enabled,
)
defer logger.Debugf(
ctx,
"/pause/unpause restreaming (stream forwarding): enabled:%v->%v",
!fwd.Enabled,
fwd.Enabled,
)
err := p.StreamD.UpdateStreamForward( err := p.StreamD.UpdateStreamForward(
ctx, ctx,
fwd.StreamID, fwd.StreamID,
@@ -1229,7 +1311,14 @@ func (p *Panel) displayStreamForwards(
now := time.Now() now := time.Now()
p.previousNumBytesLocker.Do(ctx, func() { p.previousNumBytesLocker.Do(ctx, func() {
prevNumBytes := p.previousNumBytes[key] prevNumBytes := p.previousNumBytes[key]
bwStr := bwString(fwd.NumBytesRead, prevNumBytes[0], fwd.NumBytesWrote, prevNumBytes[1], now, p.previousNumBytesTS[key]) bwStr := bwString(
fwd.NumBytesRead,
prevNumBytes[0],
fwd.NumBytesWrote,
prevNumBytes[1],
now,
p.previousNumBytesTS[key],
)
bwText := widget.NewRichTextWithText(bwStr) bwText := widget.NewRichTextWithText(bwStr)
hasDynamicValue = hasDynamicValue || bwStr != "" hasDynamicValue = hasDynamicValue || bwStr != ""
p.previousNumBytes[key] = [4]uint64{fwd.NumBytesRead, fwd.NumBytesWrote} p.previousNumBytes[key] = [4]uint64{fwd.NumBytesRead, fwd.NumBytesWrote}

View File

@@ -224,7 +224,13 @@ func (ui *timersUI) start(
} }
} }
duration := time.Hour*time.Duration(hours) + time.Minute*time.Duration(mins) + time.Second*time.Duration(secs) duration := time.Hour*time.Duration(
hours,
) + time.Minute*time.Duration(
mins,
) + time.Second*time.Duration(
secs,
)
if duration == 0 { if duration == 0 {
ui.panel.DisplayError(fmt.Errorf("the time is not set for the timer")) ui.panel.DisplayError(fmt.Errorf("the time is not set for the timer"))

View File

@@ -15,7 +15,10 @@ type updateTimerHandler struct {
startTS time.Time startTS time.Time
} }
func newUpdateTimerHandler(startStopButton *widget.Button, startedAt time.Time) *updateTimerHandler { func newUpdateTimerHandler(
startStopButton *widget.Button,
startedAt time.Time,
) *updateTimerHandler {
ctx, cancelFn := context.WithCancel(context.Background()) ctx, cancelFn := context.WithCancel(context.Background())
h := &updateTimerHandler{ h := &updateTimerHandler{
ctx: ctx, ctx: ctx,

View File

@@ -121,7 +121,8 @@ func getMousePos(window fyne.Window) fyne.Position {
func (w *HintWidget) isHovering(mousePos fyne.Position) bool { func (w *HintWidget) isHovering(mousePos fyne.Position) bool {
pos0 := GetAbsolutePosition(w, w.Window.Canvas().Content()) pos0 := GetAbsolutePosition(w, w.Window.Canvas().Content())
pos1 := pos0.Add(w.Label.Size()) pos1 := pos0.Add(w.Label.Size())
if mousePos.X >= pos0.X && mousePos.Y >= pos0.Y && mousePos.X <= pos1.X && mousePos.Y <= pos1.Y { if mousePos.X >= pos0.X && mousePos.Y >= pos0.Y && mousePos.X <= pos1.X &&
mousePos.Y <= pos1.Y {
return true return true
} }

View File

@@ -12,10 +12,16 @@ type dummyPlatformsController struct{}
var _ streamservertypes.PlatformsController = (*dummyPlatformsController)(nil) var _ streamservertypes.PlatformsController = (*dummyPlatformsController)(nil)
func (dummyPlatformsController) CheckStreamStartedByURL(ctx context.Context, destination *url.URL) (bool, error) { func (dummyPlatformsController) CheckStreamStartedByURL(
ctx context.Context,
destination *url.URL,
) (bool, error) {
return true, nil return true, nil
} }
func (dummyPlatformsController) CheckStreamStartedByPlatformID(ctx context.Context, platID streamcontrol.PlatformName) (bool, error) { func (dummyPlatformsController) CheckStreamStartedByPlatformID(
ctx context.Context,
platID streamcontrol.PlatformName,
) (bool, error) {
return true, nil return true, nil
} }

View File

@@ -37,8 +37,16 @@ func assertNoError(ctx context.Context, err error) {
func main() { func main() {
loggerLevel := logger.LevelWarning loggerLevel := logger.LevelWarning
pflag.Var(&loggerLevel, "log-level", "Log level") pflag.Var(&loggerLevel, "log-level", "Log level")
rtmpListenAddr := pflag.String("rtmp-listen-addr", "127.0.0.1:1935", "the TCP port to serve an RTMP server on") rtmpListenAddr := pflag.String(
streamID := pflag.String("stream-id", "test/test", "the path of the stream in rtmp://address/path") "rtmp-listen-addr",
"127.0.0.1:1935",
"the TCP port to serve an RTMP server on",
)
streamID := pflag.String(
"stream-id",
"test/test",
"the path of the stream in rtmp://address/path",
)
mpvPath := pflag.String("mpv", "mpv", "path to mpv") mpvPath := pflag.String("mpv", "mpv", "path to mpv")
pflag.Parse() pflag.Parse()

View File

@@ -96,7 +96,14 @@ func (sp *StreamPlayers) Create(
) (_ret *StreamPlayerHandler, _err error) { ) (_ret *StreamPlayerHandler, _err error) {
logger.Debugf(ctx, "StreamPlayers.Create(ctx, '%s', %#+v)", streamID, opts) logger.Debugf(ctx, "StreamPlayers.Create(ctx, '%s', %#+v)", streamID, opts)
defer func() { defer func() {
logger.Debugf(ctx, "/StreamPlayers.Create(ctx, '%s', %#+v): (%v, %v)", streamID, opts, _ret, _err) logger.Debugf(
ctx,
"/StreamPlayers.Create(ctx, '%s', %#+v): (%v, %v)",
streamID,
opts,
_ret,
_err,
)
}() }()
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
@@ -114,11 +121,18 @@ func (sp *StreamPlayers) Create(
} }
if p.Config.CatchupMaxSpeedFactor <= 1 { if p.Config.CatchupMaxSpeedFactor <= 1 {
return nil, fmt.Errorf("MaxCatchupSpeedFactor should be higher than 1, but it is %v", p.Config.CatchupMaxSpeedFactor) return nil, fmt.Errorf(
"MaxCatchupSpeedFactor should be higher than 1, but it is %v",
p.Config.CatchupMaxSpeedFactor,
)
} }
if p.Config.MaxCatchupAtLag <= p.Config.JitterBufDuration { if p.Config.MaxCatchupAtLag <= p.Config.JitterBufDuration {
return nil, fmt.Errorf("MaxCatchupAtLag (%v) should be higher than JitterBufDuration (%v)", p.Config.MaxCatchupAtLag, p.Config.JitterBufDuration) return nil, fmt.Errorf(
"MaxCatchupAtLag (%v) should be higher than JitterBufDuration (%v)",
p.Config.MaxCatchupAtLag,
p.Config.JitterBufDuration,
)
} }
if err := p.startU(ctx); err != nil { if err := p.startU(ctx); err != nil {
@@ -157,13 +171,17 @@ func (sp *StreamPlayers) Get(streamID streamtypes.StreamID) *StreamPlayerHandler
func (sp *StreamPlayers) GetAll() map[streamtypes.StreamID]*StreamPlayerHandler { func (sp *StreamPlayers) GetAll() map[streamtypes.StreamID]*StreamPlayerHandler {
ctx := context.TODO() ctx := context.TODO()
return xsync.DoR1(ctx, &sp.StreamPlayersLocker, func() map[streamtypes.StreamID]*StreamPlayerHandler { return xsync.DoR1(
ctx,
&sp.StreamPlayersLocker,
func() map[streamtypes.StreamID]*StreamPlayerHandler {
r := map[streamtypes.StreamID]*StreamPlayerHandler{} r := map[streamtypes.StreamID]*StreamPlayerHandler{}
for k, v := range sp.StreamPlayers { for k, v := range sp.StreamPlayers {
r[k] = v r[k] = v
} }
return r return r
}) },
)
} }
const ( const (
@@ -570,7 +588,10 @@ func (p *StreamPlayerHandler) controllerLoop(
errmon.ObserveErrorCtx(ctx, p.Close()) errmon.ObserveErrorCtx(ctx, p.Close())
return return
case <-getRestartChan: case <-getRestartChan:
logger.Debugf(ctx, "received a notification that the player should be restarted immediately") logger.Debugf(
ctx,
"received a notification that the player should be restarted immediately",
)
restart() restart()
return return
case <-t.C: case <-t.C:
@@ -580,9 +601,17 @@ func (p *StreamPlayerHandler) controllerLoop(
now := time.Now() now := time.Now()
l, err := player.GetLength(ctx) l, err := player.GetLength(ctx)
if err != nil { if err != nil {
logger.Errorf(ctx, "StreamPlayer[%s].controllerLoop: unable to get the current length: %v", p.StreamID, err) logger.Errorf(
ctx,
"StreamPlayer[%s].controllerLoop: unable to get the current length: %v",
p.StreamID,
err,
)
if prevLength != 0 { if prevLength != 0 {
logger.Debugf(ctx, "previously GetLength worked, so it seems like the player died or something, restarting") logger.Debugf(
ctx,
"previously GetLength worked, so it seems like the player died or something, restarting",
)
restart() restart()
return return
} }
@@ -593,11 +622,25 @@ func (p *StreamPlayerHandler) controllerLoop(
pos, err := player.GetPosition(ctx) pos, err := player.GetPosition(ctx)
if err != nil { if err != nil {
logger.Errorf(ctx, "StreamPlayer[%s].controllerLoop: unable to get the current position: %v", p.StreamID, err) logger.Errorf(
ctx,
"StreamPlayer[%s].controllerLoop: unable to get the current position: %v",
p.StreamID,
err,
)
time.Sleep(time.Second) time.Sleep(time.Second)
return return
} }
logger.Tracef(ctx, "StreamPlayer[%s].controllerLoop: now == %v, posUpdatedAt == %v, len == %v; pos == %v; readTimeout == %v", p.StreamID, now, posUpdatedAt, l, pos, p.Config.ReadTimeout) logger.Tracef(
ctx,
"StreamPlayer[%s].controllerLoop: now == %v, posUpdatedAt == %v, len == %v; pos == %v; readTimeout == %v",
p.StreamID,
now,
posUpdatedAt,
l,
pos,
p.Config.ReadTimeout,
)
if pos < 0 { if pos < 0 {
logger.Debugf(ctx, "negative position: %v", pos) logger.Debugf(ctx, "negative position: %v", pos)
@@ -628,7 +671,11 @@ func (p *StreamPlayerHandler) controllerLoop(
if curSpeed == 1 { if curSpeed == 1 {
return return
} }
logger.Debugf(ctx, "StreamPlayer[%s].controllerLoop: resetting the speed to 1", p.StreamID) logger.Debugf(
ctx,
"StreamPlayer[%s].controllerLoop: resetting the speed to 1",
p.StreamID,
)
err := player.SetSpeed(ctx, 1) err := player.SetSpeed(ctx, 1)
if err != nil { if err != nil {
logger.Errorf(ctx, "unable to reset the speed to 1: %v", err) logger.Errorf(ctx, "unable to reset the speed to 1: %v", err)
@@ -643,23 +690,34 @@ func (p *StreamPlayerHandler) controllerLoop(
(lag.Seconds()-p.Config.JitterBufDuration.Seconds())/ (lag.Seconds()-p.Config.JitterBufDuration.Seconds())/
(p.Config.MaxCatchupAtLag.Seconds()-p.Config.JitterBufDuration.Seconds()) (p.Config.MaxCatchupAtLag.Seconds()-p.Config.JitterBufDuration.Seconds())
speed = float64(uint(speed*10)) / 10 // to avoid flickering (for example between 1.0001 and 1.0) speed = float64(
uint(speed*10),
) / 10 // to avoid flickering (for example between 1.0001 and 1.0)
if speed > p.Config.CatchupMaxSpeedFactor { if speed > p.Config.CatchupMaxSpeedFactor {
logger.Warnf( logger.Warnf(
ctx, ctx,
"speed is calculated higher than the maximum: %v > %v: (%v-1)*(%v-%v)/(%v-%v); lag calculation: %v - %v", "speed is calculated higher than the maximum: %v > %v: (%v-1)*(%v-%v)/(%v-%v); lag calculation: %v - %v",
speed, p.Config.CatchupMaxSpeedFactor, speed,
p.Config.CatchupMaxSpeedFactor, p.Config.CatchupMaxSpeedFactor,
lag.Seconds(), p.Config.JitterBufDuration.Seconds(), p.Config.CatchupMaxSpeedFactor,
p.Config.MaxCatchupAtLag.Seconds(), p.Config.JitterBufDuration.Seconds(), lag.Seconds(),
l, pos, p.Config.JitterBufDuration.Seconds(),
p.Config.MaxCatchupAtLag.Seconds(),
p.Config.JitterBufDuration.Seconds(),
l,
pos,
) )
speed = p.Config.CatchupMaxSpeedFactor speed = p.Config.CatchupMaxSpeedFactor
} }
if speed != curSpeed { if speed != curSpeed {
logger.Debugf(ctx, "StreamPlayer[%s].controllerLoop: setting the speed to %v", p.StreamID, speed) logger.Debugf(
ctx,
"StreamPlayer[%s].controllerLoop: setting the speed to %v",
p.StreamID,
speed,
)
err = player.SetSpeed(ctx, speed) err = player.SetSpeed(ctx, speed)
if err != nil { if err != nil {
logger.Errorf(ctx, "unable to set the speed to %v: %v", speed, err) logger.Errorf(ctx, "unable to set the speed to %v: %v", speed, err)

View File

@@ -184,7 +184,9 @@ func StreamsHandle(url string) (core.Producer, error) {
return rtmp.DialPlay(url) return rtmp.DialPlay(url)
} }
func StreamsConsumerHandle(url string) (core.Consumer, types.NumBytesReaderWroter, func(context.Context) error, error) { func StreamsConsumerHandle(
url string,
) (core.Consumer, types.NumBytesReaderWroter, func(context.Context) error, error) {
cons := flv.NewConsumer() cons := flv.NewConsumer()
trafficCounter := &types.TrafficCounter{} trafficCounter := &types.TrafficCounter{}
run := func(ctx context.Context) error { run := func(ctx context.Context) error {

View File

@@ -74,7 +74,12 @@ func (s *StreamServer) initNoLock(ctx context.Context) error {
for dstID, dstCfg := range cfg.Destinations { for dstID, dstCfg := range cfg.Destinations {
err := s.addStreamDestination(ctx, dstID, dstCfg.URL) err := s.addStreamDestination(ctx, dstID, dstCfg.URL)
if err != nil { if err != nil {
return fmt.Errorf("unable to initialize stream destination '%s' to %#+v: %w", dstID, dstCfg, err) return fmt.Errorf(
"unable to initialize stream destination '%s' to %#+v: %w",
dstID,
dstCfg,
err,
)
} }
} }
@@ -88,7 +93,12 @@ func (s *StreamServer) initNoLock(ctx context.Context) error {
if !fwd.Disabled { if !fwd.Disabled {
err := s.addStreamForward(ctx, streamID, dstID) err := s.addStreamForward(ctx, streamID, dstID)
if err != nil { if err != nil {
return fmt.Errorf("unable to launch stream forward from '%s' to '%s': %w", streamID, dstID, err) return fmt.Errorf(
"unable to launch stream forward from '%s' to '%s': %w",
streamID,
dstID,
err,
)
} }
} }
} }
@@ -315,7 +325,11 @@ func (s *StreamServer) addStreamForward(
) error { ) error {
streamSrc := s.StreamHandler.Get(string(streamID)) streamSrc := s.StreamHandler.Get(string(streamID))
if streamSrc == nil { if streamSrc == nil {
return fmt.Errorf("unable to find stream ID '%s', available stream IDs: %s", streamID, strings.Join(s.StreamHandler.GetAll(), ", ")) return fmt.Errorf(
"unable to find stream ID '%s', available stream IDs: %s",
streamID,
strings.Join(s.StreamHandler.GetAll(), ", "),
)
} }
dst, err := s.findStreamDestinationByID(ctx, destinationID) dst, err := s.findStreamDestinationByID(ctx, destinationID)
if err != nil { if err != nil {
@@ -418,7 +432,11 @@ func (s *StreamServer) listStreamForwards(
streamIDSrc := types.StreamID(name) streamIDSrc := types.StreamID(name)
streamDst, err := s.findStreamDestinationByURL(ctx, fwd.URL) streamDst, err := s.findStreamDestinationByURL(ctx, fwd.URL)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to convert URL '%s' to a stream ID: %w", fwd.URL, err) return nil, fmt.Errorf(
"unable to convert URL '%s' to a stream ID: %w",
fwd.URL,
err,
)
} }
result = append(result, StreamForward{ result = append(result, StreamForward{
StreamID: streamIDSrc, StreamID: streamIDSrc,
@@ -467,7 +485,13 @@ func (s *StreamServer) removeStreamForward(
err = fwd.Close() err = fwd.Close()
if err != nil { if err != nil {
return fmt.Errorf("unable to close forwarding from %s to %s (%s): %w", streamID, dstID, fwd.URL, err) return fmt.Errorf(
"unable to close forwarding from %s to %s (%s): %w",
streamID,
dstID,
fwd.URL,
err,
)
} }
stream.Cleanup() stream.Cleanup()
return nil return nil
@@ -562,7 +586,10 @@ func (s *StreamServer) findStreamDestinationByURL(
return dst, nil return dst, nil
} }
} }
return types.StreamDestination{}, fmt.Errorf("unable to find a stream destination by URL '%s'", url) return types.StreamDestination{}, fmt.Errorf(
"unable to find a stream destination by URL '%s'",
url,
)
} }
func (s *StreamServer) findStreamDestinationByID( func (s *StreamServer) findStreamDestinationByID(
@@ -574,5 +601,8 @@ func (s *StreamServer) findStreamDestinationByID(
return dst, nil return dst, nil
} }
} }
return types.StreamDestination{}, fmt.Errorf("unable to find a stream destination by StreamID '%s'", destinationID) return types.StreamDestination{}, fmt.Errorf(
"unable to find a stream destination by StreamID '%s'",
destinationID,
)
} }

View File

@@ -112,7 +112,14 @@ type conn struct {
func (c *conn) appendDOT(dot []byte, group string) []byte { func (c *conn) appendDOT(dot []byte, group string) []byte {
host := c.host() host := c.host()
dot = fmt.Appendf(dot, "%s [group=host];\n", host) dot = fmt.Appendf(dot, "%s [group=host];\n", host)
dot = fmt.Appendf(dot, "%d [group=%s, label=%q, title=%q];\n", c.ID, group, c.FormatName, c.label()) dot = fmt.Appendf(
dot,
"%d [group=%s, label=%q, title=%q];\n",
c.ID,
group,
c.FormatName,
c.label(),
)
if group == "producer" { if group == "producer" {
dot = fmt.Appendf(dot, "%s -> %d [label=%q];\n", host, c.ID, humanBytes(c.BytesRecv)) dot = fmt.Appendf(dot, "%s -> %d [label=%q];\n", host, c.ID, humanBytes(c.BytesRecv))
} else { } else {

View File

@@ -89,7 +89,9 @@ func (s *StreamHandler) HandleConsumerFunc(scheme string, handler ConsumerHandle
s.consumerHandlers[scheme] = handler s.consumerHandlers[scheme] = handler
} }
func (s *StreamHandler) GetConsumer(url string) (core.Consumer, types.NumBytesReaderWroter, func(context.Context) error, error) { func (s *StreamHandler) GetConsumer(
url string,
) (core.Consumer, types.NumBytesReaderWroter, func(context.Context) error, error) {
if i := strings.IndexByte(url, ':'); i > 0 { if i := strings.IndexByte(url, ':'); i > 0 {
scheme := url[:i] scheme := url[:i]

View File

@@ -65,7 +65,12 @@ func (s *StreamServer) init(ctx context.Context) error {
for dstID, dstCfg := range cfg.Destinations { for dstID, dstCfg := range cfg.Destinations {
err := s.addStreamDestination(ctx, dstID, dstCfg.URL) err := s.addStreamDestination(ctx, dstID, dstCfg.URL)
if err != nil { if err != nil {
return fmt.Errorf("unable to initialize stream destination '%s' to %#+v: %w", dstID, dstCfg, err) return fmt.Errorf(
"unable to initialize stream destination '%s' to %#+v: %w",
dstID,
dstCfg,
err,
)
} }
} }
@@ -79,7 +84,12 @@ func (s *StreamServer) init(ctx context.Context) error {
if !fwd.Disabled { if !fwd.Disabled {
_, err := s.addStreamForward(ctx, streamID, dstID, fwd.Quirks) _, err := s.addStreamForward(ctx, streamID, dstID, fwd.Quirks)
if err != nil { if err != nil {
return fmt.Errorf("unable to launch stream forward from '%s' to '%s': %w", streamID, dstID, err) return fmt.Errorf(
"unable to launch stream forward from '%s' to '%s': %w",
streamID,
dstID,
err,
)
} }
} }
} }
@@ -143,7 +153,11 @@ func (s *StreamServer) startServer(
observability.Go(ctx, func() { observability.Go(ctx, func() {
err = portServer.Server.Serve(listener) err = portServer.Server.Serve(listener)
if err != nil { if err != nil {
err = fmt.Errorf("unable to start serving RTMP at '%s': %w", listener.Addr().String(), err) err = fmt.Errorf(
"unable to start serving RTMP at '%s': %w",
listener.Addr().String(),
err,
)
logger.Error(ctx, err) logger.Error(ctx, err)
} }
}) })
@@ -371,7 +385,15 @@ func (s *StreamServer) addStreamForward(
ctx = belt.WithField(ctx, "stream_forward", fmt.Sprintf("%s->%s", streamID, destinationID)) ctx = belt.WithField(ctx, "stream_forward", fmt.Sprintf("%s->%s", streamID, destinationID))
if actFwd, ok := s.ActiveStreamForwardings[destinationID]; ok { if actFwd, ok := s.ActiveStreamForwardings[destinationID]; ok {
return buildStreamForward(streamID, destinationID, cfg, actFwd), fmt.Errorf("there is already an active stream forwarding to '%s'", destinationID) return buildStreamForward(
streamID,
destinationID,
cfg,
actFwd,
), fmt.Errorf(
"there is already an active stream forwarding to '%s'",
destinationID,
)
} }
dst, err := s.findStreamDestinationByID(ctx, destinationID) dst, err := s.findStreamDestinationByID(ctx, destinationID)
@@ -633,7 +655,10 @@ func (s *StreamServer) findStreamDestinationByID(
return dst, nil return dst, nil
} }
} }
return types.StreamDestination{}, fmt.Errorf("unable to find a stream destination by StreamID '%s'", destinationID) return types.StreamDestination{}, fmt.Errorf(
"unable to find a stream destination by StreamID '%s'",
destinationID,
)
} }
func (s *StreamServer) AddStreamPlayer( func (s *StreamServer) AddStreamPlayer(

View File

@@ -73,7 +73,11 @@ func (srv *RTMPServer) init(
} }
}() }()
if (cfg.ServerCert != nil) != (cfg.ServerKey != nil) { if (cfg.ServerCert != nil) != (cfg.ServerKey != nil) {
return fmt.Errorf("fields 'ServerCert' and 'ServerKey' should be used together (cur values: ServerCert == %#+v; ServerKey == %#+v)", cfg.ServerCert, cfg.ServerKey) return fmt.Errorf(
"fields 'ServerCert' and 'ServerKey' should be used together (cur values: ServerCert == %#+v; ServerKey == %#+v)",
cfg.ServerCert,
cfg.ServerKey,
)
} }
if cfg.ServerCert != nil && cfg.ServerKey != nil { if cfg.ServerCert != nil && cfg.ServerKey != nil {
@@ -84,7 +88,10 @@ func (srv *RTMPServer) init(
} }
if cfg.IsTLS && (srv.ServerCert == "" || srv.ServerKey == "") { if cfg.IsTLS && (srv.ServerCert == "" || srv.ServerKey == "") {
ctx := context.TODO() ctx := context.TODO()
logger.Warnf(ctx, "TLS is enabled, but no certificate is supplied, generating an ephemeral self-signed certificate") // TODO: implement the support of providing the certificates in the UI logger.Warnf(
ctx,
"TLS is enabled, but no certificate is supplied, generating an ephemeral self-signed certificate",
) // TODO: implement the support of providing the certificates in the UI
err := srv.generateServerCertificate() err := srv.generateServerCertificate()
if err != nil { if err != nil {
return fmt.Errorf("unable to set the TLS certificate: %w", err) return fmt.Errorf("unable to set the TLS certificate: %w", err)
@@ -173,7 +180,11 @@ func (srv *RTMPServer) setServerCertificate(
Bytes: cert.Raw, Bytes: cert.Raw,
} }
if err := pem.Encode(certFile, certPEM); err != nil { if err := pem.Encode(certFile, certPEM); err != nil {
return fmt.Errorf("unable to write the server certificate to file '%s' in PEM format: %w", certFile.Name(), err) return fmt.Errorf(
"unable to write the server certificate to file '%s' in PEM format: %w",
certFile.Name(),
err,
)
} }
keyFile, err = os.CreateTemp("", "rtmps-server-certkey-*.pem") keyFile, err = os.CreateTemp("", "rtmps-server-certkey-*.pem")
@@ -192,7 +203,11 @@ func (srv *RTMPServer) setServerCertificate(
Bytes: keyBytes, Bytes: keyBytes,
} }
if err := pem.Encode(keyFile, privatePem); err != nil { if err := pem.Encode(keyFile, privatePem); err != nil {
return fmt.Errorf("unable to write the server certificate to file '%s' in PEM format: %w", certFile.Name(), err) return fmt.Errorf(
"unable to write the server certificate to file '%s' in PEM format: %w",
certFile.Name(),
err,
)
} }
srv.ServerCert = certFile.Name() srv.ServerCert = certFile.Name()

View File

@@ -117,7 +117,13 @@ func (s *StreamServer) init(
s.mutex.Do(ctx, func() { s.mutex.Do(ctx, func() {
_, err := s.startServer(ctx, srv.Type, srv.Listen, srv.Options()...) _, err := s.startServer(ctx, srv.Type, srv.Listen, srv.Options()...)
if err != nil { if err != nil {
logger.Errorf(ctx, "unable to initialize %s server at %s: %w", srv.Type, srv.Listen, err) logger.Errorf(
ctx,
"unable to initialize %s server at %s: %w",
srv.Type,
srv.Listen,
err,
)
} }
}) })
}) })
@@ -145,7 +151,11 @@ func (s *StreamServer) PathReady(path defs.Path) {
s.streamsStatusLocker.Do(context.Background(), func() { s.streamsStatusLocker.Do(context.Background(), func() {
publisher := s.publishers[appKey] publisher := s.publishers[appKey]
if publisher != nil { if publisher != nil {
logger.Errorf(ctx, "double-registration of a publisher for '%s' (this is an internal error in the code): %w", appKey) logger.Errorf(
ctx,
"double-registration of a publisher for '%s' (this is an internal error in the code): %w",
appKey,
)
return return
} }
s.publishers[appKey] = newPublisherClosedNotifier() s.publishers[appKey] = newPublisherClosedNotifier()
@@ -386,7 +396,13 @@ func (s *StreamServer) WaitPublisherChan(
}, },
) )
logger.Debugf(ctx, "WaitPublisherChan('%s', %v): publisher==%#+v", appKey, waitForNext, publisher) logger.Debugf(
ctx,
"WaitPublisherChan('%s', %v): publisher==%#+v",
appKey,
waitForNext,
publisher,
)
if publisher != nil && publisher != curPublisher { if publisher != nil && publisher != curPublisher {
ch <- publisher ch <- publisher
@@ -399,7 +415,12 @@ func (s *StreamServer) WaitPublisherChan(
logger.Debugf(ctx, "WaitPublisherChan('%s', %v): cancelled", appKey, waitForNext) logger.Debugf(ctx, "WaitPublisherChan('%s', %v): cancelled", appKey, waitForNext)
return return
case <-waitCh: case <-waitCh:
logger.Debugf(ctx, "WaitPublisherChan('%s', %v): an event happened, rechecking", appKey, waitForNext) logger.Debugf(
ctx,
"WaitPublisherChan('%s', %v): an event happened, rechecking",
appKey,
waitForNext,
)
} }
} }
}) })
@@ -433,13 +454,23 @@ func (s *StreamServer) startServer(
for _, portSrv := range s.serverHandlers { for _, portSrv := range s.serverHandlers {
if portSrv.ListenAddr() == listenAddr { if portSrv.ListenAddr() == listenAddr {
return nil, fmt.Errorf("we already have an port server %#+v instance at '%s'", portSrv, listenAddr) return nil, fmt.Errorf(
"we already have an port server %#+v instance at '%s'",
portSrv,
listenAddr,
)
} }
} }
portSrv, err := s.newServer(ctx, serverType, listenAddr, opts...) portSrv, err := s.newServer(ctx, serverType, listenAddr, opts...)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to initialize a new instance of a port server %s at %s with options %v: %w", serverType, listenAddr, opts, err) return nil, fmt.Errorf(
"unable to initialize a new instance of a port server %s at %s with options %v: %w",
serverType,
listenAddr,
opts,
err,
)
} }
logger.Tracef(ctx, "adding serverHandler %#+v %#+v", portSrv, portSrv.Config()) logger.Tracef(ctx, "adding serverHandler %#+v %#+v", portSrv, portSrv.Config())

View File

@@ -66,7 +66,11 @@ func (h *Handler) OnCreateStream(timestamp uint32, cmd *rtmpmsg.NetConnectionCre
return nil return nil
} }
func (h *Handler) OnPublish(_ *rtmp.StreamContext, timestamp uint32, cmd *rtmpmsg.NetStreamPublish) error { func (h *Handler) OnPublish(
_ *rtmp.StreamContext,
timestamp uint32,
cmd *rtmpmsg.NetStreamPublish,
) error {
log.Printf("OnPublish: %#v", cmd) log.Printf("OnPublish: %#v", cmd)
if h.sub != nil { if h.sub != nil {
@@ -94,7 +98,11 @@ func (h *Handler) OnPublish(_ *rtmp.StreamContext, timestamp uint32, cmd *rtmpms
}) })
} }
func (h *Handler) OnPlay(rtmpctx *rtmp.StreamContext, timestamp uint32, cmd *rtmpmsg.NetStreamPlay) error { func (h *Handler) OnPlay(
rtmpctx *rtmp.StreamContext,
timestamp uint32,
cmd *rtmpmsg.NetStreamPlay,
) error {
if h.sub != nil { if h.sub != nil {
return errors.New("Cannot play on this stream") return errors.New("Cannot play on this stream")
} }

View File

@@ -93,7 +93,10 @@ func (pb *Pubsub) ClosedChan() <-chan struct{} {
const sendQueueLength = 2 * 60 * 10 // presumably it will give about 10 seconds of queue: 2 tracks * 60FPS * 30 seconds const sendQueueLength = 2 * 60 * 10 // presumably it will give about 10 seconds of queue: 2 tracks * 60FPS * 30 seconds
func (pb *Pubsub) Sub(connCloser io.Closer, eventCallback func(context.Context, *flvtag.FlvTag) error) *Sub { func (pb *Pubsub) Sub(
connCloser io.Closer,
eventCallback func(context.Context, *flvtag.FlvTag) error,
) *Sub {
ctx := context.TODO() ctx := context.TODO()
return xsync.DoR1(ctx, &pb.m, func() *Sub { return xsync.DoR1(ctx, &pb.m, func() *Sub {
subID := pb.nextSubID subID := pb.nextSubID
@@ -303,7 +306,11 @@ func (s *Sub) senderLoop(
case tag, ok := <-s.sendQueue: case tag, ok := <-s.sendQueue:
if !ok { if !ok {
logger.Debugf(ctx, "Sub[%d].senderLoop: the queue is closed; closing the client", s.subID) logger.Debugf(
ctx,
"Sub[%d].senderLoop: the queue is closed; closing the client",
s.subID,
)
s.Close() s.Close()
return return
} }
@@ -313,7 +320,12 @@ func (s *Sub) senderLoop(
err := s.onEvent(ctx, tag) err := s.onEvent(ctx, tag)
if err != nil { if err != nil {
metrics.FromCtx(ctx).Count("submit_process_error").Add(1) metrics.FromCtx(ctx).Count("submit_process_error").Add(1)
logger.Errorf(ctx, "Sub[%d].senderLoop: unable to send an FLV tag: %v", s.subID, err) logger.Errorf(
ctx,
"Sub[%d].senderLoop: unable to send an FLV tag: %v",
s.subID,
err,
)
} else { } else {
metrics.FromCtx(ctx).Count("submit_process_success").Add(1) metrics.FromCtx(ctx).Count("submit_process_success").Add(1)
} }
@@ -331,7 +343,11 @@ func (s *Sub) Submit(
case s.sendQueue <- flv: case s.sendQueue <- flv:
metrics.FromCtx(ctx).Count("submit_pushed").Add(1) metrics.FromCtx(ctx).Count("submit_pushed").Add(1)
default: default:
logger.Errorf(ctx, "subscriber #%d queue is full, cannot send a tag; closing the connection, because cannot restore from this", s.subID) logger.Errorf(
ctx,
"subscriber #%d queue is full, cannot send a tag; closing the connection, because cannot restore from this",
s.subID,
)
observability.Go(ctx, func() { observability.Go(ctx, func() {
s.CloseOrLog(ctx) s.CloseOrLog(ctx)
}) })

View File

@@ -12,5 +12,9 @@ func NewStreamForwards(
s StreamServer, s StreamServer,
platformsController types.PlatformsController, platformsController types.PlatformsController,
) *StreamForwards { ) *StreamForwards {
return streamforward.NewStreamForwards(s, xaionarogortmp.NewRecoderFactory(), platformsController) return streamforward.NewStreamForwards(
s,
xaionarogortmp.NewRecoderFactory(),
platformsController,
)
} }

View File

@@ -88,7 +88,13 @@ func (s *StreamServer) init(
observability.Go(ctx, func() { observability.Go(ctx, func() {
_, err := s.startServer(ctx, srv.Type, srv.Listen) _, err := s.startServer(ctx, srv.Type, srv.Listen)
if err != nil { if err != nil {
logger.Errorf(ctx, "unable to initialize %s server at %s: %w", srv.Type, srv.Listen, err) logger.Errorf(
ctx,
"unable to initialize %s server at %s: %w",
srv.Type,
srv.Listen,
err,
)
} }
}) })
} }
@@ -210,7 +216,11 @@ func (s *StreamServer) startServer(
OnConnect: func(conn net.Conn) (io.ReadWriteCloser, *rtmp.ConnConfig) { OnConnect: func(conn net.Conn) (io.ReadWriteCloser, *rtmp.ConnConfig) {
ctx := belt.WithField(ctx, "client", conn.RemoteAddr().String()) ctx := belt.WithField(ctx, "client", conn.RemoteAddr().String())
h := yutoppgortmp.NewHandler(s.RelayService) h := yutoppgortmp.NewHandler(s.RelayService)
wrcc := types.NewReaderWriterCloseCounter(conn, &portSrv.ReadCount, &portSrv.WriteCount) wrcc := types.NewReaderWriterCloseCounter(
conn,
&portSrv.ReadCount,
&portSrv.WriteCount,
)
return wrcc, &rtmp.ConnConfig{ return wrcc, &rtmp.ConnConfig{
Handler: h, Handler: h,
ControlState: rtmp.StreamControlStateConfig{ ControlState: rtmp.StreamControlStateConfig{
@@ -223,7 +233,11 @@ func (s *StreamServer) startServer(
observability.Go(ctx, func() { observability.Go(ctx, func() {
err = portSrv.Serve(listener) err = portSrv.Serve(listener)
if err != nil { if err != nil {
err = fmt.Errorf("unable to start serving RTMP at '%s': %w", listener.Addr().String(), err) err = fmt.Errorf(
"unable to start serving RTMP at '%s': %w",
listener.Addr().String(),
err,
)
logger.Error(ctx, err) logger.Error(ctx, err)
} }
}) })

View File

@@ -52,9 +52,21 @@ func (fwds *StreamForwards) NewActiveStreamForward(
pauseFunc func(ctx context.Context, fwd *ActiveStreamForwarding), pauseFunc func(ctx context.Context, fwd *ActiveStreamForwarding),
opts ...Option, opts ...Option,
) (_ret *ActiveStreamForwarding, _err error) { ) (_ret *ActiveStreamForwarding, _err error) {
logger.Debugf(ctx, "NewActiveStreamForward(ctx, '%s', '%s', relayService, pauseFunc)", streamID, urlString) logger.Debugf(
ctx,
"NewActiveStreamForward(ctx, '%s', '%s', relayService, pauseFunc)",
streamID,
urlString,
)
defer func() { defer func() {
logger.Debugf(ctx, "/NewActiveStreamForward(ctx, '%s', '%s', relayService, pauseFunc): %#+v %v", streamID, urlString, _ret, _err) logger.Debugf(
ctx,
"/NewActiveStreamForward(ctx, '%s', '%s', relayService, pauseFunc): %#+v %v",
streamID,
urlString,
_ret,
_err,
)
}() }()
urlParsed, err := url.Parse(urlString) urlParsed, err := url.Parse(urlString)
@@ -176,7 +188,10 @@ func (fwd *ActiveStreamForwarding) waitForPublisherAndStart(
}) })
logger.Debugf(ctx, "DestinationStreamingLocker.Lock(ctx, '%s')", fwd.DestinationURL) logger.Debugf(ctx, "DestinationStreamingLocker.Lock(ctx, '%s')", fwd.DestinationURL)
destinationUnlocker := fwd.StreamForwards.DestinationStreamingLocker.Lock(ctx, fwd.DestinationURL) destinationUnlocker := fwd.StreamForwards.DestinationStreamingLocker.Lock(
ctx,
fwd.DestinationURL,
)
defer func() { defer func() {
if destinationUnlocker != nil { // if ctx was cancelled before we locked then the unlocker is nil if destinationUnlocker != nil { // if ctx was cancelled before we locked then the unlocker is nil
destinationUnlocker.Unlock() destinationUnlocker.Unlock()
@@ -337,7 +352,12 @@ func (fwd *ActiveStreamForwarding) openOutputFor(
ctx context.Context, ctx context.Context,
recoderInstance recoder.Recoder, recoderInstance recoder.Recoder,
) (recoder.Output, error) { ) (recoder.Output, error) {
output, err := recoderInstance.NewOutputFromURL(ctx, fwd.DestinationURL.String(), fwd.DestinationStreamKey, recoder.OutputConfig{}) output, err := recoderInstance.NewOutputFromURL(
ctx,
fwd.DestinationURL.String(),
fwd.DestinationStreamKey,
recoder.OutputConfig{},
)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to open '%s' as the output: %w", fwd.DestinationURL, err) return nil, fmt.Errorf("unable to open '%s' as the output: %w", fwd.DestinationURL, err)
} }

View File

@@ -72,7 +72,12 @@ func (s *StreamForwards) init(
for dstID, dstCfg := range cfg.Destinations { for dstID, dstCfg := range cfg.Destinations {
err := s.addActiveStreamDestination(ctx, dstID, dstCfg.URL, dstCfg.StreamKey) err := s.addActiveStreamDestination(ctx, dstID, dstCfg.URL, dstCfg.StreamKey)
if err != nil { if err != nil {
_ret = fmt.Errorf("unable to initialize stream destination '%s' to %#+v: %w", dstID, dstCfg, err) _ret = fmt.Errorf(
"unable to initialize stream destination '%s' to %#+v: %w",
dstID,
dstCfg,
err,
)
return return
} }
} }
@@ -84,7 +89,12 @@ func (s *StreamForwards) init(
} }
_, err := s.newActiveStreamForward(ctx, streamID, dstID, fwd.Quirks) _, err := s.newActiveStreamForward(ctx, streamID, dstID, fwd.Quirks)
if err != nil { if err != nil {
_ret = fmt.Errorf("unable to launch stream forward from '%s' to '%s': %w", streamID, dstID, err) _ret = fmt.Errorf(
"unable to launch stream forward from '%s' to '%s': %w",
streamID,
dstID,
err,
)
return return
} }
} }
@@ -187,7 +197,10 @@ func (s *StreamForwards) newActiveStreamForward(
DestinationID: destinationID, DestinationID: destinationID,
} }
if _, ok := s.ActiveStreamForwardings[key]; ok { if _, ok := s.ActiveStreamForwardings[key]; ok {
return nil, fmt.Errorf("there is already an active stream forwarding to '%s'", destinationID) return nil, fmt.Errorf(
"there is already an active stream forwarding to '%s'",
destinationID,
)
} }
dst, err := s.findStreamDestinationByID(ctx, destinationID) dst, err := s.findStreamDestinationByID(ctx, destinationID)
@@ -227,7 +240,10 @@ func (s *StreamForwards) newActiveStreamForward(
) { ) {
if quirks.StartAfterYoutubeRecognizedStream.Enabled { if quirks.StartAfterYoutubeRecognizedStream.Enabled {
if quirks.RestartUntilYoutubeRecognizesStream.Enabled { if quirks.RestartUntilYoutubeRecognizesStream.Enabled {
logger.Errorf(ctx, "StartAfterYoutubeRecognizedStream should not be used together with RestartUntilYoutubeRecognizesStream") logger.Errorf(
ctx,
"StartAfterYoutubeRecognizedStream should not be used together with RestartUntilYoutubeRecognizesStream",
)
} else { } else {
logger.Debugf(ctx, "fwd %s->%s is waiting for YouTube to recognize the stream", streamID, destinationID) logger.Debugf(ctx, "fwd %s->%s is waiting for YouTube to recognize the stream", streamID, destinationID)
started, err := s.PlatformsController.CheckStreamStartedByPlatformID( started, err := s.PlatformsController.CheckStreamStartedByPlatformID(
@@ -284,13 +300,21 @@ func (s *StreamForwards) restartUntilYoutubeRecognizesStream(
cfg types.RestartUntilYoutubeRecognizesStream, cfg types.RestartUntilYoutubeRecognizesStream,
) { ) {
ctx = belt.WithField(ctx, "module", "restartUntilYoutubeRecognizesStream") ctx = belt.WithField(ctx, "module", "restartUntilYoutubeRecognizesStream")
ctx = belt.WithField(ctx, "stream_forward", fmt.Sprintf("%s->%s", fwd.StreamID, fwd.DestinationID)) ctx = belt.WithField(
ctx,
"stream_forward",
fmt.Sprintf("%s->%s", fwd.StreamID, fwd.DestinationID),
)
logger.Debugf(ctx, "restartUntilYoutubeRecognizesStream(ctx, %#+v, %#+v)", fwd, cfg) logger.Debugf(ctx, "restartUntilYoutubeRecognizesStream(ctx, %#+v, %#+v)", fwd, cfg)
defer func() { logger.Debugf(ctx, "restartUntilYoutubeRecognizesStream(ctx, %#+v, %#+v)", fwd, cfg) }() defer func() { logger.Debugf(ctx, "restartUntilYoutubeRecognizesStream(ctx, %#+v, %#+v)", fwd, cfg) }()
if !cfg.Enabled { if !cfg.Enabled {
logger.Errorf(ctx, "an attempt to start restartUntilYoutubeRecognizesStream when the hack is disabled for this stream forwarder: %#+v", cfg) logger.Errorf(
ctx,
"an attempt to start restartUntilYoutubeRecognizesStream when the hack is disabled for this stream forwarder: %#+v",
cfg,
)
return return
} }
@@ -316,21 +340,39 @@ func (s *StreamForwards) restartUntilYoutubeRecognizesStream(
return return
case <-time.After(cfg.StartTimeout): case <-time.After(cfg.StartTimeout):
} }
logger.Debugf(ctx, "waited %v, checking if the remote platform accepted the stream", cfg.StartTimeout) logger.Debugf(
ctx,
"waited %v, checking if the remote platform accepted the stream",
cfg.StartTimeout,
)
for { for {
streamOK, err := s.PlatformsController.CheckStreamStartedByPlatformID( streamOK, err := s.PlatformsController.CheckStreamStartedByPlatformID(
memoize.SetNoCache(ctx, true), memoize.SetNoCache(ctx, true),
youtube.ID, youtube.ID,
) )
logger.Debugf(ctx, "the result of checking the stream on the remote platform: %v %v", streamOK, err) logger.Debugf(
ctx,
"the result of checking the stream on the remote platform: %v %v",
streamOK,
err,
)
if err != nil { if err != nil {
logger.Errorf(ctx, "unable to check if the stream with URL '%s' is started: %v", fwd.ActiveForwarding.DestinationURL, err) logger.Errorf(
ctx,
"unable to check if the stream with URL '%s' is started: %v",
fwd.ActiveForwarding.DestinationURL,
err,
)
time.Sleep(time.Second) time.Sleep(time.Second)
continue continue
} }
if streamOK { if streamOK {
logger.Debugf(ctx, "waiting %v to recheck if the stream will be still OK", cfg.StopStartDelay) logger.Debugf(
ctx,
"waiting %v to recheck if the stream will be still OK",
cfg.StopStartDelay,
)
select { select {
case <-ctx.Done(): case <-ctx.Done():
return return
@@ -340,9 +382,19 @@ func (s *StreamForwards) restartUntilYoutubeRecognizesStream(
memoize.SetNoCache(ctx, true), memoize.SetNoCache(ctx, true),
youtube.ID, youtube.ID,
) )
logger.Debugf(ctx, "the result of checking the stream on the remote platform: %v %v", streamOK, err) logger.Debugf(
ctx,
"the result of checking the stream on the remote platform: %v %v",
streamOK,
err,
)
if err != nil { if err != nil {
logger.Errorf(ctx, "unable to check if the stream with URL '%s' is started: %v", fwd.ActiveForwarding.DestinationURL, err) logger.Errorf(
ctx,
"unable to check if the stream with URL '%s' is started: %v",
fwd.ActiveForwarding.DestinationURL,
err,
)
time.Sleep(time.Second) time.Sleep(time.Second)
continue continue
} }
@@ -353,7 +405,10 @@ func (s *StreamForwards) restartUntilYoutubeRecognizesStream(
break break
} }
logger.Infof(ctx, "the remote platform still does not see the stream, restarting the stream forwarding: stopping...") logger.Infof(
ctx,
"the remote platform still does not see the stream, restarting the stream forwarding: stopping...",
)
err := fwd.ActiveForwarding.Stop() err := fwd.ActiveForwarding.Stop()
if err != nil { if err != nil {
@@ -366,7 +421,10 @@ func (s *StreamForwards) restartUntilYoutubeRecognizesStream(
case <-time.After(cfg.StopStartDelay): case <-time.After(cfg.StopStartDelay):
} }
logger.Infof(ctx, "the remote platform still does not see the stream, restarting the stream forwarding: starting...") logger.Infof(
ctx,
"the remote platform still does not see the stream, restarting the stream forwarding: starting...",
)
err = fwd.ActiveForwarding.Start(ctx) err = fwd.ActiveForwarding.Start(ctx)
if err != nil { if err != nil {
@@ -447,9 +505,12 @@ func (s *StreamForwards) ListStreamForwards(
}() }()
return xsync.DoR2(ctx, &s.Mutex, func() ([]StreamForward, error) { return xsync.DoR2(ctx, &s.Mutex, func() ([]StreamForward, error) {
return s.getStreamForwards(ctx, func(si types.StreamID, di ordered.Optional[types.DestinationID]) bool { return s.getStreamForwards(
ctx,
func(si types.StreamID, di ordered.Optional[types.DestinationID]) bool {
return true return true
}) },
)
}) })
} }
@@ -486,7 +547,12 @@ func (s *StreamForwards) getStreamForwards(
if !filterFunc(streamID, ordered.Optional[types.DestinationID]{}) { if !filterFunc(streamID, ordered.Optional[types.DestinationID]{}) {
continue continue
} }
logger.Tracef(ctx, "len(s.Config.Streams[%s].Forwardings) == %d", streamID, len(stream.Forwardings)) logger.Tracef(
ctx,
"len(s.Config.Streams[%s].Forwardings) == %d",
streamID,
len(stream.Forwardings),
)
for dstID, cfg := range stream.Forwardings { for dstID, cfg := range stream.Forwardings {
if !filterFunc(streamID, ordered.Opt(dstID)) { if !filterFunc(streamID, ordered.Opt(dstID)) {
continue continue
@@ -589,9 +655,12 @@ func (s *StreamForwards) GetStreamForwardsByDestination(
}() }()
return xsync.DoR2(ctx, &s.Mutex, func() ([]StreamForward, error) { return xsync.DoR2(ctx, &s.Mutex, func() ([]StreamForward, error) {
return s.getStreamForwards(ctx, func(streamID types.StreamID, dstID ordered.Optional[types.DestinationID]) bool { return s.getStreamForwards(
ctx,
func(streamID types.StreamID, dstID ordered.Optional[types.DestinationID]) bool {
return !dstID.IsSet() || dstID.Get() == destID return !dstID.IsSet() || dstID.Get() == destID
}) },
)
}) })
} }
@@ -647,7 +716,15 @@ func (s *StreamForwards) UpdateStreamDestination(
streamKey string, streamKey string,
) error { ) error {
ctx = belt.WithField(ctx, "module", "StreamServer") ctx = belt.WithField(ctx, "module", "StreamServer")
return xsync.DoA4R1(ctx, &s.Mutex, s.updateStreamDestination, ctx, destinationID, url, streamKey) return xsync.DoA4R1(
ctx,
&s.Mutex,
s.updateStreamDestination,
ctx,
destinationID,
url,
streamKey,
)
} }
func (s *StreamForwards) updateStreamDestination( func (s *StreamForwards) updateStreamDestination(
@@ -659,14 +736,20 @@ func (s *StreamForwards) updateStreamDestination(
s.WithConfig(ctx, func(ctx context.Context, cfg *types.Config) { s.WithConfig(ctx, func(ctx context.Context, cfg *types.Config) {
for key := range s.ActiveStreamForwardings { for key := range s.ActiveStreamForwardings {
if key.DestinationID == destinationID { if key.DestinationID == destinationID {
_ret = fmt.Errorf("there is already an active stream forwarding to '%s'", destinationID) _ret = fmt.Errorf(
"there is already an active stream forwarding to '%s'",
destinationID,
)
return return
} }
} }
err := s.removeActiveStreamDestination(ctx, destinationID) err := s.removeActiveStreamDestination(ctx, destinationID)
if err != nil { if err != nil {
_ret = fmt.Errorf("unable to remove (to then re-add) the active stream destination: %w", err) _ret = fmt.Errorf(
"unable to remove (to then re-add) the active stream destination: %w",
err,
)
return return
} }
@@ -725,9 +808,12 @@ func (s *StreamForwards) removeActiveStreamDestination(
ctx context.Context, ctx context.Context,
destinationID types.DestinationID, destinationID types.DestinationID,
) error { ) error {
streamForwards, err := s.getStreamForwards(ctx, func(si types.StreamID, di ordered.Optional[types.DestinationID]) bool { streamForwards, err := s.getStreamForwards(
ctx,
func(si types.StreamID, di ordered.Optional[types.DestinationID]) bool {
return true return true
}) },
)
if err != nil { if err != nil {
return fmt.Errorf("unable to list stream forwardings: %w", err) return fmt.Errorf("unable to list stream forwardings: %w", err)
} }
@@ -756,7 +842,10 @@ func (s *StreamForwards) findStreamDestinationByID(
return dst, nil return dst, nil
} }
} }
return types.StreamDestination{}, fmt.Errorf("unable to find a stream destination by StreamID '%s'", destinationID) return types.StreamDestination{}, fmt.Errorf(
"unable to find a stream destination by StreamID '%s'",
destinationID,
)
} }
func (s *StreamForwards) getLocalhostEndpoint(ctx context.Context) (*url.URL, error) { func (s *StreamForwards) getLocalhostEndpoint(ctx context.Context) (*url.URL, error) {

View File

@@ -362,7 +362,10 @@ func (s *StreamPlayers) setStreamPlayer(
var opts SetupOptions var opts SetupOptions
if playerCfg.DefaultStreamPlayerOptions != nil { if playerCfg.DefaultStreamPlayerOptions != nil {
opts = append(opts, SetupOptionDefaultStreamPlayerOptions(playerCfg.DefaultStreamPlayerOptions)) opts = append(
opts,
SetupOptionDefaultStreamPlayerOptions(playerCfg.DefaultStreamPlayerOptions),
)
} }
var streamCfg map[types.StreamID]*types.StreamConfig var streamCfg map[types.StreamID]*types.StreamConfig

View File

@@ -9,5 +9,8 @@ import (
type PlatformsController interface { type PlatformsController interface {
CheckStreamStartedByURL(ctx context.Context, destination *url.URL) (bool, error) CheckStreamStartedByURL(ctx context.Context, destination *url.URL) (bool, error)
CheckStreamStartedByPlatformID(ctx context.Context, platID streamcontrol.PlatformName) (bool, error) CheckStreamStartedByPlatformID(
ctx context.Context,
platID streamcontrol.PlatformName,
) (bool, error)
} }