mirror of
https://github.com/xaionaro-go/streamctl.git
synced 2025-10-10 09:50:15 +08:00
golines --max-len=60
This commit is contained in:
@@ -147,12 +147,24 @@ func streamSetup(cmd *cobra.Command, args []string) {
|
||||
assertNoError(ctx, err)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@@ -97,7 +97,8 @@ func init() {
|
||||
StreamStart.PersistentFlags().String("title", "", "stream title")
|
||||
StreamStart.PersistentFlags().String("description", "", "stream description")
|
||||
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(SetTitle)
|
||||
@@ -153,8 +154,12 @@ func generateConfig(cmd *cobra.Command, args []string) {
|
||||
logger.Panicf(cmd.Context(), "file '%s' already exists", cfgPath)
|
||||
}
|
||||
cfg := newConfig()
|
||||
cfg[idTwitch].StreamProfiles = map[streamcontrol.ProfileName]streamcontrol.AbstractStreamProfile{"some_profile": twitch.StreamProfile{}}
|
||||
cfg[idYoutube].StreamProfiles = map[streamcontrol.ProfileName]streamcontrol.AbstractStreamProfile{"some_profile": youtube.StreamProfile{}}
|
||||
cfg[idTwitch].StreamProfiles = map[streamcontrol.ProfileName]streamcontrol.AbstractStreamProfile{
|
||||
"some_profile": twitch.StreamProfile{},
|
||||
}
|
||||
cfg[idYoutube].StreamProfiles = map[streamcontrol.ProfileName]streamcontrol.AbstractStreamProfile{
|
||||
"some_profile": youtube.StreamProfile{},
|
||||
}
|
||||
err := writeConfigToPath(cmd.Context(), cfgPath, cfg)
|
||||
if err != nil {
|
||||
logger.Panic(cmd.Context(), err)
|
||||
@@ -203,7 +208,10 @@ func readConfigFromPath(
|
||||
}
|
||||
|
||||
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 {
|
||||
return fmt.Errorf("unable to convert stream profiles of twitch: %w: <%s>", err, b)
|
||||
}
|
||||
@@ -211,11 +219,18 @@ func readConfigFromPath(
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
@@ -244,7 +259,11 @@ func getTwitchStreamController(
|
||||
ctx context.Context,
|
||||
cfg streamcontrol.Config,
|
||||
) (*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 {
|
||||
logger.Infof(ctx, "twitch config was not found")
|
||||
return nil, nil
|
||||
@@ -268,7 +287,11 @@ func getYouTubeStreamController(
|
||||
ctx context.Context,
|
||||
cfg streamcontrol.Config,
|
||||
) (*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 {
|
||||
logger.Infof(ctx, "youtube config was not found")
|
||||
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
|
||||
|
||||
twitch, err := getTwitchStreamController(ctx, cfg)
|
||||
|
@@ -39,9 +39,21 @@ const forceNetPProfOnAndroid = true
|
||||
func main() {
|
||||
loggerLevel := logger.LevelWarning
|
||||
pflag.Var(&loggerLevel, "log-level", "Log level")
|
||||
listenAddr := pflag.String("listen-addr", ":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")
|
||||
listenAddr := pflag.String(
|
||||
"listen-addr",
|
||||
":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")
|
||||
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")
|
||||
@@ -210,7 +222,13 @@ func main() {
|
||||
},
|
||||
func(listenPort uint16, platID streamcontrol.PlatformName, authURL string) bool {
|
||||
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()
|
||||
logger.Debugf(ctx, "streamdGRPCLocker.Lock()-ed")
|
||||
|
@@ -159,7 +159,13 @@ func (ui *UI) oauth2Handler(
|
||||
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)
|
||||
|
||||
t := time.NewTicker(time.Hour)
|
||||
|
@@ -63,23 +63,71 @@ func parseFlags() Flags {
|
||||
defaultLogFile = ""
|
||||
}
|
||||
pflag.Var(&loggerLevelValue, "log-level", "Log level")
|
||||
listenAddr := pflag.String("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")
|
||||
listenAddr := pflag.String(
|
||||
"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")
|
||||
netPprofAddrMain := pflag.String("go-net-pprof-addr-main", "", "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")
|
||||
netPprofAddrMain := pflag.String(
|
||||
"go-net-pprof-addr-main",
|
||||
"",
|
||||
"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")
|
||||
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")
|
||||
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")
|
||||
subprocess := pflag.String("subprocess", "", "[internal use flag] run a specific sub-process (format: processName:addressToConnect)")
|
||||
splitProcess := pflag.Bool("split-process", !isMobile(), "split the process into multiple processes for better stability")
|
||||
lockTimeout := pflag.Duration("lock-timeout", 2*time.Minute, "[debug option] change the timeout for locking, before reporting it as a deadlock")
|
||||
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")
|
||||
subprocess := pflag.String(
|
||||
"subprocess",
|
||||
"",
|
||||
"[internal use flag] run a specific sub-process (format: processName:addressToConnect)",
|
||||
)
|
||||
splitProcess := pflag.Bool(
|
||||
"split-process",
|
||||
!isMobile(),
|
||||
"split the process into multiple processes for better stability",
|
||||
)
|
||||
lockTimeout := pflag.Duration(
|
||||
"lock-timeout",
|
||||
2*time.Minute,
|
||||
"[debug option] change the timeout for locking, before reporting it as a deadlock",
|
||||
)
|
||||
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()
|
||||
|
||||
@@ -130,7 +178,11 @@ func getFlags(
|
||||
func(ctx context.Context, source mainprocess.ProcessName, content any) error {
|
||||
result, ok := content.(GetFlagsResult)
|
||||
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
|
||||
return nil
|
||||
|
@@ -103,5 +103,11 @@ func getFlagsAndroidFromFiles(flags *Flags) {
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
@@ -63,7 +63,10 @@ func runSubprocess(
|
||||
) {
|
||||
parts := strings.SplitN(subprocessFlag, ":", 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])
|
||||
addr := parts[1]
|
||||
@@ -123,7 +126,12 @@ func runSplitProcesses(
|
||||
case ProcessNameStreamd:
|
||||
err := m.SendMessage(ctx, ProcessNameUI, StreamDDied{})
|
||||
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)
|
||||
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:], " "))
|
||||
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.Stdout = logwriter.NewLogWriter(ctx, logger.FromCtx(ctx).WithField("log_writer_target", "split"))
|
||||
cmd.Stderr = logwriter.NewLogWriter(
|
||||
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
|
||||
err = child_process_manager.ConfigureCommand(cmd)
|
||||
if err != nil {
|
||||
@@ -213,7 +235,13 @@ func runFork(
|
||||
err := cmd.Wait()
|
||||
cancelFn()
|
||||
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
|
||||
@@ -251,7 +279,11 @@ func setReadyFor(
|
||||
func(ctx context.Context, source ProcessName, content any) error {
|
||||
_, ok := content.(mainprocess.MessageReadyConfirmed)
|
||||
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
|
||||
},
|
||||
|
@@ -104,7 +104,11 @@ func runPanel(
|
||||
}
|
||||
|
||||
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:
|
||||
panel.OnInternallySubmittedOAuthCode = func(
|
||||
|
@@ -86,7 +86,10 @@ func initRuntime(
|
||||
|
||||
if netPprofAddr != "" {
|
||||
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.Error(http.ListenAndServe(netPprofAddr, nil))
|
||||
|
@@ -31,7 +31,12 @@ func mainProcessSignalHandler(
|
||||
logger.Debugf(ctx, "interrupting '%s'", name)
|
||||
err := f.Process.Signal(os.Interrupt)
|
||||
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)
|
||||
f.Process.Kill()
|
||||
return
|
||||
|
@@ -115,7 +115,13 @@ func runStreamd(
|
||||
},
|
||||
func(listenPort uint16, platID streamcontrol.PlatformName, authURL string) bool {
|
||||
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()
|
||||
logger.Debugf(ctx, "streamdGRPCLocker.Lock()-ed")
|
||||
|
@@ -210,7 +210,11 @@ func (m *Manager) handleConnection(
|
||||
m.unregisterConnection(ctx, sourceName)
|
||||
}(regMessage.Source)
|
||||
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)
|
||||
return
|
||||
}
|
||||
@@ -237,7 +241,13 @@ func (m *Manager) handleConnection(
|
||||
logger.Tracef(ctx, "waiting for a message from '%s'", regMessage.Source)
|
||||
decoder := gob.NewDecoder(conn)
|
||||
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 {
|
||||
case <-ctx.Done():
|
||||
logger.Tracef(ctx, "context was closed")
|
||||
@@ -305,7 +315,12 @@ func (m *Manager) processMessage(
|
||||
close(errCh)
|
||||
return err.ErrorOrNil()
|
||||
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) {
|
||||
case MessageReady:
|
||||
var result *multierror.Error
|
||||
@@ -318,7 +333,13 @@ func (m *Manager) processMessage(
|
||||
return onReceivedMessage(ctx, source, message.Content)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -349,7 +370,14 @@ func (m *Manager) sendMessage(
|
||||
) (_ret error) {
|
||||
logger.Tracef(ctx, "sending message %#+v from '%s' to '%s'", content, source, destination)
|
||||
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) {
|
||||
@@ -359,7 +387,11 @@ func (m *Manager) sendMessage(
|
||||
observability.Go(ctx, func() {
|
||||
conn, err := m.waitForReadyProcess(ctx, destination, reflect.TypeOf(content))
|
||||
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
|
||||
}
|
||||
|
||||
@@ -372,7 +404,9 @@ func (m *Manager) sendMessage(
|
||||
|
||||
h := m.connLocker.Lock(context.Background(), destination)
|
||||
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)
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "%v", fmt.Errorf("unable to encode&send message: %w", err))
|
||||
@@ -419,7 +453,10 @@ func (m *Manager) waitForReadyProcess(
|
||||
}
|
||||
|
||||
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]
|
||||
isReady := false
|
||||
if readyMap != nil {
|
||||
@@ -428,14 +465,25 @@ func (m *Manager) waitForReadyProcess(
|
||||
}
|
||||
}
|
||||
return m.conns[name], m.connsChanged, 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -452,7 +500,12 @@ func (m *Manager) registerConnection(
|
||||
conn net.Conn,
|
||||
) error {
|
||||
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) {
|
||||
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)
|
||||
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)
|
||||
logger.Tracef(ctx, "sending message %#+v: %v", msg, err)
|
||||
if err != nil {
|
||||
|
@@ -66,19 +66,25 @@ func Test(t *testing.T) {
|
||||
c0, err := NewClient("child0", m.Addr().String(), m.Password())
|
||||
require.NoError(t, err)
|
||||
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)
|
||||
return nil
|
||||
})
|
||||
},
|
||||
)
|
||||
c0.SendMessage(ctx, "main", MessageReady{})
|
||||
|
||||
c1, err := NewClient("child1", m.Addr().String(), m.Password())
|
||||
require.NoError(t, err)
|
||||
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)
|
||||
return nil
|
||||
})
|
||||
},
|
||||
)
|
||||
c1.SendMessage(ctx, "main", MessageReady{})
|
||||
|
||||
_, err = NewClient("child2", m.Addr().String(), m.Password())
|
||||
|
@@ -49,7 +49,10 @@ func OAuth2HandlerViaBrowser(ctx context.Context, arg OAuthHandlerArgument) erro
|
||||
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.
|
||||
code := <-codeCh
|
||||
@@ -75,7 +78,11 @@ func NewCodeReceiver(
|
||||
fmt.Fprintf(w, "No code received :(\r\n\r\nYou can close this browser window.")
|
||||
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,
|
||||
)
|
||||
}),
|
||||
}
|
||||
|
||||
|
@@ -75,7 +75,11 @@ func NewErrorMonitorLoggerHook(
|
||||
|
||||
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 {
|
||||
return loggertypes.PreHookResult{
|
||||
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 {
|
||||
return loggertypes.PreHookResult{
|
||||
Skip: false,
|
||||
@@ -110,7 +119,13 @@ func (h *ErrorMonitorLoggerHook) ProcessInputf(traceIDs belt.TraceIDs, level log
|
||||
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 {
|
||||
return loggertypes.PreHookResult{
|
||||
Skip: false,
|
||||
|
@@ -41,7 +41,11 @@ func main() {
|
||||
loggerLevel := logger.LevelInfo
|
||||
pflag.Var(&loggerLevel, "log-level", "Log level")
|
||||
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()
|
||||
|
||||
l := logrus.Default().WithLevel(loggerLevel)
|
||||
|
@@ -119,7 +119,18 @@ func (p *MPV) execMPV(ctx context.Context) (_err error) {
|
||||
err := os.Remove(socketPath)
|
||||
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() {
|
||||
case logger.LevelPanic, logger.LevelFatal:
|
||||
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:], " "))
|
||||
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.Stderr = logwriter.NewLogWriter(ctx, logger.FromCtx(ctx).WithField("log_writer_target", "mpv").WithField("output_type", "stderr"))
|
||||
cmd.Stdout = logwriter.NewLogWriter(
|
||||
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)
|
||||
errmon.ObserveErrorCtx(ctx, err)
|
||||
err = cmd.Start()
|
||||
|
@@ -34,7 +34,12 @@ func (r *Recoder) StartRecoding(
|
||||
return xsync.DoR1(ctx, &r.Locker, func() error {
|
||||
relay := rtmprelay.NewRtmpRelay(&input.URL, &output.URL)
|
||||
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
|
||||
|
@@ -55,7 +55,11 @@ func NewOutputFromURL(
|
||||
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 {
|
||||
return nil, fmt.Errorf("allocating output format context failed: %w", err)
|
||||
}
|
||||
@@ -69,7 +73,10 @@ func NewOutputFromURL(
|
||||
// if output is a file:
|
||||
if !output.FormatContext.OutputFormat().Flags().Has(astiav.IOFormatFlagNofile) {
|
||||
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 {
|
||||
log.Fatal(fmt.Errorf("main: opening io context failed: %w", err))
|
||||
}
|
||||
|
@@ -35,7 +35,9 @@ func New(
|
||||
WaiterChan: make(chan struct{}),
|
||||
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
|
||||
}
|
||||
|
||||
|
@@ -109,7 +109,11 @@ func (srv *GRPCServer) newInputByURL(
|
||||
config := recoder.InputConfig{}
|
||||
input, err := recoder.NewInputFromURL(ctx, path.Url.Url, path.Url.AuthKey, config)
|
||||
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 {
|
||||
@@ -163,7 +167,11 @@ func (srv *GRPCServer) newOutputByURL(
|
||||
config := recoder.OutputConfig{}
|
||||
output, err := recoder.NewOutputFromURL(ctx, path.Url.Url, path.Url.AuthKey, config)
|
||||
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 {
|
||||
|
@@ -41,7 +41,11 @@ func (r *Recoder) NewInputFromPublisher(
|
||||
) (recoder.Input, error) {
|
||||
publisher, ok := publisherIface.(*xaionarogortmp.Pubsub)
|
||||
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{
|
||||
@@ -85,7 +89,11 @@ func (r *Recoder) NewInputFromURL(
|
||||
},
|
||||
})
|
||||
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")
|
||||
|
@@ -49,7 +49,12 @@ func newRTMPClient(
|
||||
dialFunc = rtmp.Dial
|
||||
case "rtmps":
|
||||
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:
|
||||
return nil, fmt.Errorf("unexpected scheme '%s' in URL '%s'", url.Scheme, url.String())
|
||||
|
@@ -73,7 +73,11 @@ func (r *Recoder) StartRecoding(
|
||||
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 {
|
||||
return fmt.Errorf("unable to create a stream on the remote side: %w", err)
|
||||
}
|
||||
|
@@ -176,7 +176,8 @@ func (g *GIT) push(
|
||||
if err == git.NoErrAlreadyUpToDate {
|
||||
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}
|
||||
}
|
||||
if err != nil {
|
||||
@@ -200,7 +201,11 @@ func (g *GIT) Read() ([]byte, error) {
|
||||
b, err := io.ReadAll(f)
|
||||
f.Close()
|
||||
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
|
||||
@@ -271,11 +276,21 @@ func (g *GIT) Pull(
|
||||
logger.Debugf(ctx, "git is already in sync: %s == %s", lastKnownCommitHash, newCommitHash)
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -301,7 +316,11 @@ func (g *GIT) CommitAndPush(
|
||||
|
||||
_, err := worktree.Add(g.FilePath)
|
||||
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()
|
||||
@@ -312,12 +331,15 @@ func (g *GIT) CommitAndPush(
|
||||
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,
|
||||
AllowEmptyCommits: false,
|
||||
Author: signature,
|
||||
Committer: signature,
|
||||
})
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
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)
|
||||
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
|
||||
@@ -420,12 +446,19 @@ func (g *GIT) Write(
|
||||
0644,
|
||||
)
|
||||
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))
|
||||
f.Close()
|
||||
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)
|
||||
|
@@ -10,7 +10,11 @@ import (
|
||||
func Screenshot(cfg Config) (*image.RGBA, error) {
|
||||
activeDisplays := screenshot.NumActiveDisplays()
|
||||
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))
|
||||
|
@@ -130,10 +130,14 @@ var _ yaml.BytesUnmarshaler = (*RawMessage)(nil)
|
||||
var _ yaml.BytesMarshaler = (*RawMessage)(nil)
|
||||
|
||||
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 {
|
||||
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 {
|
||||
@@ -192,7 +196,11 @@ func (cfg *Config) UnmarshalYAML(b []byte) (_err error) {
|
||||
t := map[PlatformName]*unparsedPlatformConfig{}
|
||||
err := yaml.Unmarshal(b, &t)
|
||||
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 {
|
||||
@@ -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))
|
||||
for k, p := range streamProfiles {
|
||||
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))
|
||||
for k, v := range in {
|
||||
m[k] = v
|
||||
|
@@ -10,7 +10,11 @@ type ErrInvalidStreamProfileType struct {
|
||||
var _ error = ErrInvalidStreamProfileType{}
|
||||
|
||||
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 {
|
||||
|
@@ -213,7 +213,9 @@ func (obs *OBS) GetStreamStatus(
|
||||
|
||||
var startedAt *time.Time
|
||||
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{
|
||||
|
@@ -71,26 +71,44 @@ func (sr SceneRule) MarshalYAML() (b []byte, _err error) {
|
||||
|
||||
triggerBytes, err := yaml.Marshal(sr.TriggerQuery)
|
||||
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{}
|
||||
err = yaml.Unmarshal(triggerBytes, &triggerMap)
|
||||
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)
|
||||
|
||||
actionBytes, err := yaml.Marshal(sr.Action)
|
||||
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{}
|
||||
err = yaml.Unmarshal(actionBytes, &actionMap)
|
||||
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)
|
||||
|
@@ -120,7 +120,13 @@ type StreamController[ProfileType StreamProfile] interface {
|
||||
StreamControllerCommons
|
||||
|
||||
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 {
|
||||
|
@@ -42,7 +42,9 @@ func New(
|
||||
return nil, fmt.Errorf("'channel' is not set")
|
||||
}
|
||||
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
|
||||
@@ -92,7 +94,10 @@ func New(
|
||||
errmon.ObserveErrorCtx(ctx, err)
|
||||
now := time.Now()
|
||||
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.client.SetAppAccessToken("")
|
||||
t.client.SetUserAccessToken("")
|
||||
@@ -121,7 +126,10 @@ func getUserID(
|
||||
return "", fmt.Errorf("unable to query user info: %w", err)
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -146,7 +154,12 @@ func (t *Twitch) prepareNoLock(ctx context.Context) error {
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
@@ -171,7 +184,13 @@ func (t *Twitch) editChannelInfo(
|
||||
return fmt.Errorf("unable to update the channel info (%#+v): %w", *params, err)
|
||||
}
|
||||
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")
|
||||
return nil
|
||||
@@ -219,7 +238,10 @@ func (t *Twitch) ApplyProfile(
|
||||
|
||||
if profile.CategoryName != 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)
|
||||
if err == nil {
|
||||
@@ -237,7 +259,10 @@ func (t *Twitch) ApplyProfile(
|
||||
if tag == "" {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -247,7 +272,10 @@ func (t *Twitch) ApplyProfile(
|
||||
if tags != nil {
|
||||
logger.Debugf(ctx, "has tags")
|
||||
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"}
|
||||
} else {
|
||||
params.Tags = tags
|
||||
@@ -294,7 +322,11 @@ func (t *Twitch) getCategoryID(
|
||||
Names: []string{categoryName},
|
||||
})
|
||||
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 {
|
||||
@@ -360,7 +392,10 @@ func (t *Twitch) StartStream(
|
||||
result = multierror.Append(result, fmt.Errorf("unable to set description: %w", err))
|
||||
}
|
||||
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()
|
||||
}
|
||||
@@ -614,7 +649,12 @@ func (t *Twitch) getNewTokenByUser(
|
||||
return fmt.Errorf("unable to get user access token: %w", err)
|
||||
}
|
||||
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.SetRefreshToken(resp.Data.RefreshToken)
|
||||
@@ -637,7 +677,12 @@ func (t *Twitch) getNewTokenByApp(
|
||||
return fmt.Errorf("unable to get app access token: %w", err)
|
||||
}
|
||||
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")
|
||||
t.client.SetAppAccessToken(resp.Data.AccessToken)
|
||||
@@ -735,7 +780,8 @@ func (t *Twitch) GetAllCategories(
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
|
@@ -53,7 +53,9 @@ func New(
|
||||
saveCfgFn func(Config) error,
|
||||
) (*YouTube, error) {
|
||||
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)
|
||||
@@ -430,7 +432,10 @@ func (yt *YouTube) ApplyProfile(
|
||||
) error {
|
||||
return yt.updateActiveBroadcasts(ctx, func(broadcast *youtube.LiveBroadcast) error {
|
||||
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)
|
||||
return nil
|
||||
@@ -443,7 +448,10 @@ func (yt *YouTube) SetTitle(
|
||||
) error {
|
||||
return yt.updateActiveBroadcasts(ctx, func(broadcast *youtube.LiveBroadcast) error {
|
||||
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)
|
||||
return nil
|
||||
@@ -456,7 +464,10 @@ func (yt *YouTube) SetDescription(
|
||||
) error {
|
||||
return yt.updateActiveBroadcasts(ctx, func(broadcast *youtube.LiveBroadcast) error {
|
||||
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)
|
||||
return nil
|
||||
@@ -595,7 +606,12 @@ func (yt *YouTube) StartStream(
|
||||
logger.Debugf(ctx, "profile == %#+v", profile)
|
||||
|
||||
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{}{}
|
||||
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)
|
||||
}
|
||||
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...)
|
||||
}
|
||||
@@ -637,18 +657,29 @@ func (yt *YouTube) StartStream(
|
||||
{
|
||||
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)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get the list of active broadcasts: %w", err)
|
||||
}
|
||||
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...)
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
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)
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
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
|
||||
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)
|
||||
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 {
|
||||
@@ -709,7 +758,11 @@ func (yt *YouTube) StartStream(
|
||||
templateBroadcastID := 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()
|
||||
broadcast.Id = ""
|
||||
@@ -720,7 +773,9 @@ func (yt *YouTube) StartStream(
|
||||
broadcast.ContentDetails.MonitorStream = nil
|
||||
broadcast.ContentDetails.ForceSendFields = []string{"EnableAutoStop"}
|
||||
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.Status.SelfDeclaredMadeForKids = broadcast.Status.MadeForKids
|
||||
broadcast.Status.ForceSendFields = []string{"SelfDeclaredMadeForKids"}
|
||||
@@ -747,7 +802,10 @@ func (yt *YouTube) StartStream(
|
||||
logger.Debugf(ctx, "YouTube.LiveBroadcasts result: %v", err)
|
||||
if err != nil {
|
||||
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()
|
||||
if err != nil {
|
||||
@@ -758,7 +816,9 @@ func (yt *YouTube) StartStream(
|
||||
now = time.Now().Add(time.Hour)
|
||||
}
|
||||
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(
|
||||
[]string{"snippet", "contentDetails", "monetizationDetails", "status"},
|
||||
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, templateTags...)
|
||||
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 = deduplicate(video.Snippet.Tags)
|
||||
tagsTruncated := TruncateTags(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
|
||||
}
|
||||
b, err = yaml.Marshal(video)
|
||||
@@ -832,7 +901,9 @@ func (yt *YouTube) StartStream(
|
||||
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)
|
||||
if err != nil {
|
||||
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")
|
||||
resp, err := http.Get(broadcast.Snippet.Thumbnails.Standard.Url)
|
||||
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")
|
||||
thumbnail, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
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")
|
||||
_, 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)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to set the thumbnail: %w", err)
|
||||
@@ -907,7 +987,13 @@ func (yt *YouTube) GetStreamStatus(
|
||||
if err != nil {
|
||||
_startedAt, err = time.Parse(timeLayoutFallback, ts)
|
||||
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
|
||||
@@ -939,7 +1025,12 @@ func (yt *YouTube) GetStreamStatus(
|
||||
Streams: streams,
|
||||
}
|
||||
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 {
|
||||
b, err := json.Marshal(broadcast)
|
||||
if err != nil {
|
||||
@@ -948,7 +1039,11 @@ func (yt *YouTube) GetStreamStatus(
|
||||
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 {
|
||||
b, err := json.Marshal(bc)
|
||||
if err != nil {
|
||||
|
@@ -48,7 +48,12 @@ type StreamD interface {
|
||||
) error
|
||||
SetTitle(ctx context.Context, platID streamcontrol.PlatformName, title 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
|
||||
GetBackendData(ctx context.Context, platID streamcontrol.PlatformName) (any, error)
|
||||
Restart(ctx context.Context) error
|
||||
@@ -189,7 +194,12 @@ type StreamD interface {
|
||||
ListTimers(ctx context.Context) ([]Timer, 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
|
||||
ListOBSSceneRules(ctx context.Context, sceneName SceneName) (SceneRules, error)
|
||||
}
|
||||
|
@@ -47,9 +47,15 @@ func NewConfig() Config {
|
||||
|
||||
func NewSampleConfig() Config {
|
||||
cfg := NewConfig()
|
||||
cfg.Backends[obs.ID].StreamProfiles = map[streamcontrol.ProfileName]streamcontrol.AbstractStreamProfile{"some_profile": obs.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{}}
|
||||
cfg.Backends[obs.ID].StreamProfiles = map[streamcontrol.ProfileName]streamcontrol.AbstractStreamProfile{
|
||||
"some_profile": obs.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
|
||||
}
|
||||
|
||||
|
@@ -166,16 +166,32 @@ func (s *MonitorSourceOBSVideo) GetImageBytes(
|
||||
}
|
||||
resp, err := obsServer.GetSourceScreenshot(ctx, req)
|
||||
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()
|
||||
imgBytes, mimeType, err := imgb64.Decode(string(imgB64))
|
||||
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
|
||||
}
|
||||
|
||||
@@ -224,11 +240,19 @@ func (s *MonitorSourceOBSVolume) GetImage(
|
||||
|
||||
colorActive, err := colorx.Parse(s.ColorActive)
|
||||
if err != nil {
|
||||
return nil, time.Time{}, fmt.Errorf("unable to parse the `color_active` value '%s': %w", s.ColorActive, err)
|
||||
return nil, time.Time{}, fmt.Errorf(
|
||||
"unable to parse the `color_active` value '%s': %w",
|
||||
s.ColorActive,
|
||||
err,
|
||||
)
|
||||
}
|
||||
colorPassive, err := colorx.Parse(s.ColorPassive)
|
||||
if err != nil {
|
||||
return nil, time.Time{}, fmt.Errorf("unable to parse the `color_passive` value '%s': %w", s.ColorPassive, err)
|
||||
return nil, time.Time{}, fmt.Errorf(
|
||||
"unable to parse the `color_passive` value '%s': %w",
|
||||
s.ColorPassive,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
size := img.Bounds().Size()
|
||||
|
@@ -66,21 +66,30 @@ func (cfg *Config) UnmarshalYAML(b []byte) (_err error) {
|
||||
}
|
||||
|
||||
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 {
|
||||
return fmt.Errorf("unable to convert stream profiles of OBS: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
return fmt.Errorf("unable to convert stream profiles of twitch: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
return fmt.Errorf("unable to convert stream profiles of youtube: %w", err)
|
||||
}
|
||||
|
@@ -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))
|
||||
}
|
||||
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() {
|
||||
err := d.sendConfigViaGIT(ctx)
|
||||
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)
|
||||
|
@@ -88,7 +88,12 @@ func Memoize[REQ any, REPLY any, T func(context.Context, REQ) (REPLY, error)](
|
||||
logger.Tracef(ctx, "using the cached value")
|
||||
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)
|
||||
} else {
|
||||
logger.Errorf(ctx, "cache-failure: expected type %T, but got %T", (*cacheItem)(nil), cachedResult)
|
||||
|
@@ -243,7 +243,11 @@ func (grpc *GRPCServer) IsBackendEnabled(
|
||||
) (*streamd_grpc.IsBackendEnabledReply, error) {
|
||||
enabled, err := grpc.StreamD.IsBackendEnabled(ctx, streamcontrol.PlatformName(req.GetPlatID()))
|
||||
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{
|
||||
IsInitialized: enabled,
|
||||
@@ -564,7 +568,10 @@ func (grpc *GRPCServer) openBrowser(
|
||||
count++
|
||||
err := handler.Sender.Send(&req)
|
||||
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) {
|
||||
logger.Debugf(ctx, "OpenOAuthURL(ctx, %d, '%s', '%s')", listenPort, platID, authURL)
|
||||
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(
|
||||
@@ -703,7 +725,12 @@ func (grpc *GRPCServer) ListStreamServers(
|
||||
|
||||
var result []*streamd_grpc.StreamServerWithStatistics
|
||||
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 {
|
||||
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) {
|
||||
srvType, addr, opts, err := goconv.StreamServerConfigGRPC2Go(ctx, req.GetConfig())
|
||||
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(
|
||||
@@ -922,8 +953,12 @@ func (grpc *GRPCServer) AddStreamForward(
|
||||
api.StreamForwardingQuirks{
|
||||
RestartUntilYoutubeRecognizesStream: api.RestartUntilYoutubeRecognizesStream{
|
||||
Enabled: cfg.Quirks.RestartUntilYoutubeRecognizesStream.Enabled,
|
||||
StartTimeout: sec2dur(cfg.Quirks.RestartUntilYoutubeRecognizesStream.StartTimeout),
|
||||
StopStartDelay: sec2dur(cfg.Quirks.RestartUntilYoutubeRecognizesStream.StopStartDelay),
|
||||
StartTimeout: sec2dur(
|
||||
cfg.Quirks.RestartUntilYoutubeRecognizesStream.StartTimeout,
|
||||
),
|
||||
StopStartDelay: sec2dur(
|
||||
cfg.Quirks.RestartUntilYoutubeRecognizesStream.StopStartDelay,
|
||||
),
|
||||
},
|
||||
StartAfterYoutubeRecognizedStream: api.StartAfterYoutubeRecognizedStream{
|
||||
Enabled: cfg.Quirks.StartAfterYoutubeRecognizedStream.Enabled,
|
||||
@@ -949,8 +984,12 @@ func (grpc *GRPCServer) UpdateStreamForward(
|
||||
api.StreamForwardingQuirks{
|
||||
RestartUntilYoutubeRecognizesStream: api.RestartUntilYoutubeRecognizesStream{
|
||||
Enabled: cfg.Quirks.RestartUntilYoutubeRecognizesStream.Enabled,
|
||||
StartTimeout: sec2dur(cfg.Quirks.RestartUntilYoutubeRecognizesStream.StartTimeout),
|
||||
StopStartDelay: sec2dur(cfg.Quirks.RestartUntilYoutubeRecognizesStream.StopStartDelay),
|
||||
StartTimeout: sec2dur(
|
||||
cfg.Quirks.RestartUntilYoutubeRecognizesStream.StartTimeout,
|
||||
),
|
||||
StopStartDelay: sec2dur(
|
||||
cfg.Quirks.RestartUntilYoutubeRecognizesStream.StopStartDelay,
|
||||
),
|
||||
},
|
||||
StartAfterYoutubeRecognizedStream: api.StartAfterYoutubeRecognizedStream{
|
||||
Enabled: cfg.Quirks.StartAfterYoutubeRecognizedStream.Enabled,
|
||||
@@ -1113,7 +1152,11 @@ func (grpc *GRPCServer) StreamPlayerOpen(
|
||||
ctx context.Context,
|
||||
req *streamd_grpc.StreamPlayerOpenRequest,
|
||||
) (*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 {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1125,7 +1168,10 @@ func (grpc *GRPCServer) StreamPlayerProcessTitle(
|
||||
ctx context.Context,
|
||||
req *streamd_grpc.StreamPlayerProcessTitleRequest,
|
||||
) (*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 {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1214,7 +1260,11 @@ func (grpc *GRPCServer) StreamPlayerSetSpeed(
|
||||
ctx context.Context,
|
||||
req *streamd_grpc.StreamPlayerSetSpeedRequest,
|
||||
) (*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 {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1226,7 +1276,11 @@ func (grpc *GRPCServer) StreamPlayerSetPause(
|
||||
ctx context.Context,
|
||||
req *streamd_grpc.StreamPlayerSetPauseRequest,
|
||||
) (*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 {
|
||||
return nil, err
|
||||
}
|
||||
|
@@ -45,7 +45,10 @@ func (d *StreamD) EXPERIMENTAL_ReinitStreamControllers(ctx context.Context) erro
|
||||
continue
|
||||
}
|
||||
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()
|
||||
@@ -128,7 +131,8 @@ func newTwitch(
|
||||
error,
|
||||
) {
|
||||
platCfg := streamcontrol.ConvertPlatformConfig[twitch.PlatformSpecificConfig, twitch.StreamProfile](
|
||||
ctx, cfg,
|
||||
ctx,
|
||||
cfg,
|
||||
)
|
||||
if platCfg == nil {
|
||||
return nil, fmt.Errorf("twitch config was not found")
|
||||
@@ -139,7 +143,8 @@ func newTwitch(
|
||||
}
|
||||
|
||||
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)
|
||||
if !ok {
|
||||
err := saveCfgFunc(&streamcontrol.AbstractPlatformConfig{
|
||||
@@ -197,7 +202,8 @@ func newYouTube(
|
||||
error,
|
||||
) {
|
||||
platCfg := streamcontrol.ConvertPlatformConfig[youtube.PlatformSpecificConfig, youtube.StreamProfile](
|
||||
ctx, cfg,
|
||||
ctx,
|
||||
cfg,
|
||||
)
|
||||
if platCfg == nil {
|
||||
return nil, fmt.Errorf("youtube config was not found")
|
||||
@@ -295,7 +301,9 @@ func (d *StreamD) listenOBSEvents(
|
||||
default:
|
||||
}
|
||||
|
||||
client, err := o.GetClient(obs.GetClientOption(goobs.WithEventSubscriptions(subscriptions.InputVolumeMeters)))
|
||||
client, err := o.GetClient(
|
||||
obs.GetClientOption(goobs.WithEventSubscriptions(subscriptions.InputVolumeMeters)),
|
||||
)
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "unable to get an OBS client: %v", err)
|
||||
time.Sleep(time.Second)
|
||||
|
@@ -36,7 +36,10 @@ func (a *platformsControllerAdapter) CheckStreamStartedByURL(
|
||||
case strings.Contains(destination.Hostname(), "twitch"):
|
||||
platID = twitch.ID
|
||||
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)
|
||||
}
|
||||
|
@@ -183,7 +183,12 @@ func getOBSImageBytes(
|
||||
) ([]byte, time.Time, error) {
|
||||
img, nextUpdateAt, err := el.Source.GetImage(ctx, obsServer, el, obsState)
|
||||
if err != nil {
|
||||
return nil, time.Now().Add(time.Second), fmt.Errorf("unable to get the image from the source: %w", err)
|
||||
return nil, time.Now().
|
||||
Add(time.Second),
|
||||
fmt.Errorf(
|
||||
"unable to get the image from the source: %w",
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
for _, filter := range el.Filters {
|
||||
@@ -292,7 +297,10 @@ func (d *StreamD) initStreamServer(ctx context.Context) (_err error) {
|
||||
)
|
||||
assert(d.StreamServer != nil)
|
||||
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 {
|
||||
@@ -526,7 +534,9 @@ func (d *StreamD) SaveConfig(ctx context.Context) error {
|
||||
if d.GitStorage != nil {
|
||||
err = d.sendConfigViaGIT(ctx)
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
switch id {
|
||||
case obs.ID:
|
||||
@@ -600,7 +613,12 @@ func (d *StreamD) StartStream(
|
||||
if err != nil {
|
||||
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 {
|
||||
return fmt.Errorf("unable to start the stream on OBS: %w", err)
|
||||
}
|
||||
@@ -610,7 +628,12 @@ func (d *StreamD) StartStream(
|
||||
if err != nil {
|
||||
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 {
|
||||
return fmt.Errorf("unable to start the stream on Twitch: %w", err)
|
||||
}
|
||||
@@ -620,7 +643,12 @@ func (d *StreamD) StartStream(
|
||||
if err != nil {
|
||||
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 {
|
||||
return fmt.Errorf("unable to start the stream on YouTube: %w", err)
|
||||
}
|
||||
@@ -1492,9 +1520,24 @@ func (d *StreamD) UpdateStreamPlayer(
|
||||
disabled bool,
|
||||
streamPlaybackConfig sptypes.Config,
|
||||
) (_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() {
|
||||
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)
|
||||
var result *multierror.Error
|
||||
|
@@ -74,7 +74,11 @@ func (p *Panel) ShowErrorReports() {
|
||||
|
||||
content := container.NewVBox()
|
||||
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 {
|
||||
for _, report := range reports {
|
||||
errLabel := report.Error.Error()
|
||||
|
@@ -52,7 +52,9 @@ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=
|
||||
-----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
|
||||
|
||||
w.SetContent(container.NewBorder(
|
||||
|
@@ -149,7 +149,14 @@ func (p *Panel) updateMonitorPageImagesNoLock(
|
||||
if !changed && lastWinSize == winSize && lastOrientation == orientation {
|
||||
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{
|
||||
X: int(winSize.Width * float32(el.Width) / 100),
|
||||
Y: int(winSize.Height * float32(el.Height) / 100),
|
||||
@@ -192,7 +199,13 @@ func (p *Panel) updateMonitorPageImagesNoLock(
|
||||
if !changed && lastWinSize == winSize && lastOrientation == orientation {
|
||||
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)}
|
||||
img = imgFillTo(
|
||||
ctx,
|
||||
@@ -366,7 +379,10 @@ func (p *Panel) newMonitorSettingsWindow(ctx context.Context) {
|
||||
deleteButton := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() {
|
||||
w := dialog.NewConfirm(
|
||||
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) {
|
||||
if !b {
|
||||
return
|
||||
@@ -484,7 +500,9 @@ func (p *Panel) editMonitorElementWindow(
|
||||
SceneName: &sceneName,
|
||||
})
|
||||
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
|
||||
}
|
||||
for _, item := range resp.SceneItems {
|
||||
@@ -649,7 +667,9 @@ func (p *Panel) editMonitorElementWindow(
|
||||
brightness.SetText(fmt.Sprintf("%f", brightnessValue))
|
||||
|
||||
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) {
|
||||
if s == "" || s == "-" {
|
||||
s = "0.2"
|
||||
@@ -767,7 +787,15 @@ func (p *Panel) editMonitorElementWindow(
|
||||
widget.NewLabel("Source:"),
|
||||
sourceOBSVideoSelect,
|
||||
widget.NewLabel("Source image size (use '0' for preserving the original size or ratio):"),
|
||||
container.NewHBox(widget.NewLabel("X:"), sourceWidth, widget.NewLabel(`px`), widget.NewSeparator(), widget.NewLabel("Y:"), sourceHeight, widget.NewLabel(`px`)),
|
||||
container.NewHBox(
|
||||
widget.NewLabel("X:"),
|
||||
sourceWidth,
|
||||
widget.NewLabel(`px`),
|
||||
widget.NewSeparator(),
|
||||
widget.NewLabel("Y:"),
|
||||
sourceHeight,
|
||||
widget.NewLabel(`px`),
|
||||
),
|
||||
widget.NewLabel("Format:"),
|
||||
imageFormatSelect,
|
||||
widget.NewLabel("Update interval:"),
|
||||
@@ -779,7 +807,9 @@ func (p *Panel) editMonitorElementWindow(
|
||||
})
|
||||
|
||||
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) {
|
||||
if s == "" || s == "-" {
|
||||
s = "0.2"
|
||||
@@ -796,7 +826,11 @@ func (p *Panel) editMonitorElementWindow(
|
||||
if volumeColorActiveParsed, err = colorx.Parse(obsVolumeSource.ColorActive); err != nil {
|
||||
volumeColorActiveParsed = color.RGBA{R: 0, G: 255, B: 0, A: 255}
|
||||
}
|
||||
volumeColorActive := colorpicker.NewColorSelectModalRect(w, fyne.NewSize(30, 20), volumeColorActiveParsed)
|
||||
volumeColorActive := colorpicker.NewColorSelectModalRect(
|
||||
w,
|
||||
fyne.NewSize(30, 20),
|
||||
volumeColorActiveParsed,
|
||||
)
|
||||
volumeColorActive.SetOnChange(func(c color.Color) {
|
||||
r32, g32, b32, a32 := c.RGBA()
|
||||
r8, g8, b8, a8 := uint8(r32>>8), uint8(g32>>8), uint8(b32>>8), uint8(a32>>8)
|
||||
@@ -807,7 +841,11 @@ func (p *Panel) editMonitorElementWindow(
|
||||
if volumeColorPassiveParsed, err = colorx.Parse(obsVolumeSource.ColorPassive); err != nil {
|
||||
volumeColorPassiveParsed = color.RGBA{R: 0, G: 0, B: 0, A: 0}
|
||||
}
|
||||
volumeColorPassive := colorpicker.NewColorSelectModalRect(w, fyne.NewSize(30, 20), volumeColorPassiveParsed)
|
||||
volumeColorPassive := colorpicker.NewColorSelectModalRect(
|
||||
w,
|
||||
fyne.NewSize(30, 20),
|
||||
volumeColorPassiveParsed,
|
||||
)
|
||||
volumeColorPassive.SetOnChange(func(c color.Color) {
|
||||
r32, g32, b32, a32 := c.RGBA()
|
||||
r8, g8, b8, a8 := uint8(r32>>8), uint8(g32>>8), uint8(b32>>8), uint8(a32>>8)
|
||||
@@ -884,11 +922,33 @@ func (p *Panel) editMonitorElementWindow(
|
||||
widget.NewLabel("Z-Index / layer:"),
|
||||
zIndex,
|
||||
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:"),
|
||||
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:"),
|
||||
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:"),
|
||||
isLossless,
|
||||
imageQuality,
|
||||
|
@@ -248,7 +248,11 @@ func (p *Panel) LazyInitStreamD(ctx context.Context) (_err error) {
|
||||
|
||||
if p.Config.RemoteStreamDAddr != "" {
|
||||
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 {
|
||||
if err := p.initBuiltinStreamD(ctx); err != nil {
|
||||
@@ -312,7 +316,12 @@ func (p *Panel) Loop(ctx context.Context, opts ...LoopOption) error {
|
||||
p.setStatusFunc("Connecting...")
|
||||
err := p.startOAuthListenerForRemoteStreamD(ctx, streamD)
|
||||
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()
|
||||
}
|
||||
closeLoadingWindow()
|
||||
@@ -341,7 +350,9 @@ func (p *Panel) Loop(ctx context.Context, opts ...LoopOption) error {
|
||||
|
||||
p.initMainWindow(ctx, initCfg.StartingPage)
|
||||
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")
|
||||
@@ -367,7 +378,10 @@ func (p *Panel) startOAuthListenerForRemoteStreamD(
|
||||
streamD *client.Client,
|
||||
) error {
|
||||
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 {
|
||||
cancelFn()
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -420,7 +440,13 @@ func (p *Panel) startOAuthListenerForRemoteStreamD(
|
||||
Code: code,
|
||||
})
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -626,13 +652,19 @@ func (p *Panel) OnSubmittedOAuthCode(
|
||||
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)
|
||||
defer logger.Infof(ctx, "/OAuthHandlerTwitch")
|
||||
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)
|
||||
defer logger.Infof(ctx, "/OAuthHandlerYouTube")
|
||||
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)
|
||||
}
|
||||
|
||||
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.
|
||||
code := <-codeCh
|
||||
@@ -685,7 +721,12 @@ func (p *Panel) openBrowser(
|
||||
|
||||
if p.Config.Browser.Command != "" {
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -759,7 +800,9 @@ func (p *Panel) InputTwitchUserInfo(
|
||||
resizeWindow(w, fyne.NewSize(600, 200))
|
||||
|
||||
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.SetPlaceHolder("client ID")
|
||||
clientSecretField := widget.NewEntry()
|
||||
@@ -767,7 +810,10 @@ func (p *Panel) InputTwitchUserInfo(
|
||||
instructionText := widget.NewRichText(
|
||||
&widget.TextSegment{Text: "Go to\n", Style: widget.RichTextStyle{Inline: true}},
|
||||
&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
|
||||
|
||||
@@ -810,7 +856,9 @@ func (p *Panel) InputTwitchUserInfo(
|
||||
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(
|
||||
ctx context.Context,
|
||||
@@ -825,10 +873,22 @@ func (p *Panel) InputYouTubeUserInfo(
|
||||
clientSecretField.SetPlaceHolder("client secret")
|
||||
instructionText := widget.NewRichText(
|
||||
&widget.TextSegment{Text: "Go to\n", Style: widget.RichTextStyle{Inline: true}},
|
||||
&widget.HyperlinkSegment{Text: youtubeCredentialsCreateLink.String(), URL: youtubeCredentialsCreateLink},
|
||||
&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}},
|
||||
&widget.HyperlinkSegment{
|
||||
Text: youtubeCredentialsCreateLink.String(),
|
||||
URL: youtubeCredentialsCreateLink,
|
||||
},
|
||||
&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
|
||||
|
||||
@@ -879,11 +939,23 @@ func (p *Panel) profileCreateOrUpdate(ctx context.Context, profile Profile) erro
|
||||
continue
|
||||
}
|
||||
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
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
@@ -1002,7 +1074,11 @@ func (p *Panel) refilterProfiles(ctx context.Context) {
|
||||
if p.filterValue == "" {
|
||||
p.profilesOrderFiltered = p.profilesOrderFiltered[:len(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()")
|
||||
p.profilesListWidget.Refresh()
|
||||
return
|
||||
@@ -1037,7 +1113,12 @@ func (p *Panel) refilterProfiles(ctx context.Context) {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1153,10 +1234,18 @@ func (p *Panel) openSettingsWindow(ctx context.Context) error {
|
||||
logger.Debugf(ctx, "current OBS config: %#+v", obsCfg)
|
||||
}
|
||||
|
||||
cmdBeforeStartStream, _ := cfg.Backends[obs.ID].GetCustomString(config.CustomConfigKeyBeforeStreamStart)
|
||||
cmdBeforeStopStream, _ := cfg.Backends[obs.ID].GetCustomString(config.CustomConfigKeyBeforeStreamStop)
|
||||
cmdAfterStartStream, _ := cfg.Backends[obs.ID].GetCustomString(config.CustomConfigKeyAfterStreamStart)
|
||||
cmdAfterStopStream, _ := cfg.Backends[obs.ID].GetCustomString(config.CustomConfigKeyAfterStreamStop)
|
||||
cmdBeforeStartStream, _ := cfg.Backends[obs.ID].GetCustomString(
|
||||
config.CustomConfigKeyBeforeStreamStart,
|
||||
)
|
||||
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.SetText(cmdBeforeStartStream)
|
||||
@@ -1210,7 +1299,9 @@ func (p *Panel) openSettingsWindow(ctx context.Context) error {
|
||||
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
|
||||
|
||||
obsAlreadyLoggedIn := widget.NewLabel("")
|
||||
@@ -1368,7 +1459,10 @@ func (p *Panel) openSettingsWindow(ctx context.Context) error {
|
||||
displayIDSelector,
|
||||
widget.NewLabel("Crop to:"),
|
||||
container.NewHBox(
|
||||
screenshotCropXEntry, screenshotCropYEntry, screenshotCropWEntry, screenshotCropHEntry,
|
||||
screenshotCropXEntry,
|
||||
screenshotCropYEntry,
|
||||
screenshotCropWEntry,
|
||||
screenshotCropHEntry,
|
||||
),
|
||||
widget.NewSeparator(),
|
||||
widget.NewSeparator(),
|
||||
@@ -1512,7 +1606,9 @@ func (p *Panel) getUpdatedStatus_backends_noLock(ctx context.Context) {
|
||||
} {
|
||||
isEnabled, err := p.StreamD.IsBackendEnabled(ctx, backendID)
|
||||
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
|
||||
}
|
||||
@@ -1629,7 +1725,12 @@ func (p *Panel) getUpdatedStatus_startStopStreamButton_noLock(ctx context.Contex
|
||||
logger.Tracef(ctx, "ytStreamStatus == %#+v", ytStreamStatus)
|
||||
|
||||
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 {
|
||||
p.startStopButton.Enable()
|
||||
}
|
||||
@@ -1694,18 +1795,30 @@ func (p *Panel) initMainWindow(
|
||||
profileControl.Add(button)
|
||||
}
|
||||
|
||||
p.setupStreamButton = widget.NewButtonWithIcon(setupStreamString(), theme.SettingsIcon(), func() {
|
||||
p.setupStreamButton = widget.NewButtonWithIcon(
|
||||
setupStreamString(),
|
||||
theme.SettingsIcon(),
|
||||
func() {
|
||||
p.onSetupStreamButton(ctx)
|
||||
})
|
||||
},
|
||||
)
|
||||
p.setupStreamButton.Disable()
|
||||
|
||||
p.startStopButton = widget.NewButtonWithIcon(startStreamString(), theme.MediaRecordIcon(), func() {
|
||||
p.startStopButton = widget.NewButtonWithIcon(
|
||||
startStreamString(),
|
||||
theme.MediaRecordIcon(),
|
||||
func() {
|
||||
p.onStartStopButton(ctx)
|
||||
})
|
||||
},
|
||||
)
|
||||
p.startStopButton.Importance = widget.SuccessImportance
|
||||
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) {
|
||||
p.onProfilesListSelect(id)
|
||||
for _, button := range selectedProfileButtons {
|
||||
@@ -2116,7 +2229,9 @@ func (p *Panel) setupStreamNoLock(ctx context.Context) {
|
||||
} {
|
||||
isEnabled, err := p.StreamD.IsBackendEnabled(ctx, backendID)
|
||||
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
|
||||
}
|
||||
backendEnabled[backendID] = isEnabled
|
||||
@@ -2268,7 +2383,9 @@ func (p *Panel) stopStreamNoLock(ctx context.Context) {
|
||||
} {
|
||||
isEnabled, err := p.StreamD.IsBackendEnabled(ctx, backendID)
|
||||
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
|
||||
}
|
||||
backendEnabled[backendID] = isEnabled
|
||||
@@ -2627,9 +2744,13 @@ func newTagsEditor(
|
||||
tagsControlsContainer.Add(widget.NewSeparator())
|
||||
tagsControlsContainer.Add(widget.NewSeparator())
|
||||
for _, additionalButtonInfo := range additionalButtons {
|
||||
button := widget.NewButtonWithIcon(additionalButtonInfo.Label, additionalButtonInfo.Icon, func() {
|
||||
button := widget.NewButtonWithIcon(
|
||||
additionalButtonInfo.Label,
|
||||
additionalButtonInfo.Icon,
|
||||
func() {
|
||||
additionalButtonInfo.Callback(t, selectedTagsOrdered())
|
||||
})
|
||||
},
|
||||
)
|
||||
tagsControlsContainer.Add(button)
|
||||
}
|
||||
|
||||
@@ -2655,7 +2776,11 @@ func newTagsEditor(
|
||||
tagLabel := tagName
|
||||
overflown := false
|
||||
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 {
|
||||
break
|
||||
}
|
||||
@@ -2732,7 +2857,9 @@ func (p *Panel) profileWindow(
|
||||
isEnabled, err := p.StreamD.IsBackendEnabled(ctx, backendID)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
backendEnabled[backendID] = isEnabled
|
||||
@@ -2756,7 +2883,9 @@ func (p *Panel) profileWindow(
|
||||
bottomContent = append(bottomContent, widget.NewRichTextFromMarkdown("# OBS:"))
|
||||
if backendEnabled[obs.ID] {
|
||||
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 {
|
||||
obsProfile = &obs.StreamProfile{}
|
||||
}
|
||||
@@ -2778,7 +2907,9 @@ func (p *Panel) profileWindow(
|
||||
}
|
||||
|
||||
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 {
|
||||
addTag(tag)
|
||||
}
|
||||
@@ -2802,9 +2933,13 @@ func (p *Panel) profileWindow(
|
||||
if strings.Contains(cleanTwitchCategoryName(cat.Name), text) {
|
||||
selectedTwitchCategoryContainer := container.NewHBox()
|
||||
catName := cat.Name
|
||||
tagContainerRemoveButton := widget.NewButtonWithIcon(catName, theme.ContentAddIcon(), func() {
|
||||
tagContainerRemoveButton := widget.NewButtonWithIcon(
|
||||
catName,
|
||||
theme.ContentAddIcon(),
|
||||
func() {
|
||||
twitchCategory.OnSubmitted(catName)
|
||||
})
|
||||
},
|
||||
)
|
||||
selectedTwitchCategoryContainer.Add(tagContainerRemoveButton)
|
||||
selectTwitchCategoryBox.Add(selectedTwitchCategoryContainer)
|
||||
count++
|
||||
@@ -2821,10 +2956,14 @@ func (p *Panel) profileWindow(
|
||||
setSelectedTwitchCategory := func(catName string) {
|
||||
selectedTwitchCategoryBox.RemoveAll()
|
||||
selectedTwitchCategoryContainer := container.NewHBox()
|
||||
tagContainerRemoveButton := widget.NewButtonWithIcon(catName, theme.ContentClearIcon(), func() {
|
||||
tagContainerRemoveButton := widget.NewButtonWithIcon(
|
||||
catName,
|
||||
theme.ContentClearIcon(),
|
||||
func() {
|
||||
selectedTwitchCategoryBox.Remove(selectedTwitchCategoryContainer)
|
||||
twitchProfile.CategoryName = nil
|
||||
})
|
||||
},
|
||||
)
|
||||
selectedTwitchCategoryContainer.Add(tagContainerRemoveButton)
|
||||
selectedTwitchCategoryBox.Add(selectedTwitchCategoryContainer)
|
||||
twitchProfile.CategoryName = &catName
|
||||
@@ -2878,7 +3017,9 @@ func (p *Panel) profileWindow(
|
||||
youtubeTags = append(youtubeTags, tagName)
|
||||
}
|
||||
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 {
|
||||
addTag(tag)
|
||||
}
|
||||
@@ -2890,8 +3031,14 @@ func (p *Panel) profileWindow(
|
||||
youtubeProfile.AutoNumerate = b
|
||||
})
|
||||
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]'.")
|
||||
bottomContent = append(bottomContent, container.NewHBox(autoNumerateCheck, autoNumerateHint))
|
||||
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]'.",
|
||||
)
|
||||
bottomContent = append(
|
||||
bottomContent,
|
||||
container.NewHBox(autoNumerateCheck, autoNumerateHint),
|
||||
)
|
||||
|
||||
youtubeTemplate := widget.NewEntry()
|
||||
youtubeTemplate.SetPlaceHolder("youtube live recording template")
|
||||
@@ -2909,9 +3056,13 @@ func (p *Panel) profileWindow(
|
||||
if strings.Contains(cleanYoutubeRecordingName(bc.Snippet.Title), text) {
|
||||
selectedYoutubeRecordingsContainer := container.NewHBox()
|
||||
recName := bc.Snippet.Title
|
||||
tagContainerRemoveButton := widget.NewButtonWithIcon(recName, theme.ContentAddIcon(), func() {
|
||||
tagContainerRemoveButton := widget.NewButtonWithIcon(
|
||||
recName,
|
||||
theme.ContentAddIcon(),
|
||||
func() {
|
||||
youtubeTemplate.OnSubmitted(recName)
|
||||
})
|
||||
},
|
||||
)
|
||||
selectedYoutubeRecordingsContainer.Add(tagContainerRemoveButton)
|
||||
selectYoutubeTemplateBox.Add(selectedYoutubeRecordingsContainer)
|
||||
count++
|
||||
@@ -2929,10 +3080,14 @@ func (p *Panel) profileWindow(
|
||||
selectedYoutubeBroadcastBox.RemoveAll()
|
||||
selectedYoutubeBroadcastContainer := container.NewHBox()
|
||||
recName := bc.Snippet.Title
|
||||
tagContainerRemoveButton := widget.NewButtonWithIcon(recName, theme.ContentClearIcon(), func() {
|
||||
tagContainerRemoveButton := widget.NewButtonWithIcon(
|
||||
recName,
|
||||
theme.ContentClearIcon(),
|
||||
func() {
|
||||
selectedYoutubeBroadcastBox.Remove(selectedYoutubeBroadcastContainer)
|
||||
youtubeProfile.TemplateBroadcastIDs = youtubeProfile.TemplateBroadcastIDs[:0]
|
||||
})
|
||||
},
|
||||
)
|
||||
selectedYoutubeBroadcastContainer.Add(tagContainerRemoveButton)
|
||||
selectedYoutubeBroadcastBox.Add(selectedYoutubeBroadcastContainer)
|
||||
youtubeProfile.TemplateBroadcastIDs = []string{bc.Id}
|
||||
@@ -2966,7 +3121,9 @@ func (p *Panel) profileWindow(
|
||||
bottomContent = append(bottomContent, youtubeTemplate)
|
||||
|
||||
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 {
|
||||
case "ignore":
|
||||
youtubeProfile.TemplateTags = youtube.TemplateTagsIgnore
|
||||
@@ -2977,7 +3134,8 @@ func (p *Panel) profileWindow(
|
||||
default:
|
||||
p.DisplayError(fmt.Errorf("unexpected new value of 'template tags': '%s'", s))
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
switch youtubeProfile.TemplateTags {
|
||||
case youtube.TemplateTagsUndefined, youtube.TemplateTagsIgnore:
|
||||
templateTags.SetSelected("ignore")
|
||||
@@ -2986,11 +3144,22 @@ func (p *Panel) profileWindow(
|
||||
case youtube.TemplateTagsUseAsAdditional:
|
||||
templateTags.SetSelected("use as additional")
|
||||
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())
|
||||
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")
|
||||
bottomContent = append(bottomContent, container.NewHBox(templateTagsLabel, templateTags, templateTagsHint))
|
||||
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",
|
||||
)
|
||||
bottomContent = append(
|
||||
bottomContent,
|
||||
container.NewHBox(templateTagsLabel, templateTags, templateTagsHint),
|
||||
)
|
||||
|
||||
youtubeTagsEditor := newTagsEditor(youtubeTags, 0)
|
||||
bottomContent = append(bottomContent, widget.NewLabel("Tags:"))
|
||||
|
@@ -288,7 +288,14 @@ func (p *Panel) displayStreamServers(
|
||||
p.previousNumBytesLocker.Do(ctx, func() {
|
||||
prevNumBytes := p.previousNumBytes[key]
|
||||
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)
|
||||
hasDynamicValue = hasDynamicValue || bwStr != ""
|
||||
p.previousNumBytes[key] = [4]uint64{srv.NumBytesProducerRead, srv.NumBytesConsumerWrote}
|
||||
@@ -571,7 +578,14 @@ func (p *Panel) displayStreamDestinations(
|
||||
}
|
||||
|
||||
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(
|
||||
@@ -593,7 +607,14 @@ func (p *Panel) openEditPlayerWindow(
|
||||
p.DisplayError(fmt.Errorf("unable to find a stream player for '%s'", streamID))
|
||||
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(
|
||||
@@ -779,14 +800,28 @@ func (p *Panel) displayStreamPlayers(
|
||||
c := container.NewHBox()
|
||||
deleteButton := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() {
|
||||
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) {
|
||||
if !b {
|
||||
return
|
||||
}
|
||||
logger.Debugf(ctx, "remove player '%s' (%s)", player.StreamID, player.PlayerType)
|
||||
defer logger.Debugf(ctx, "/remove player '%s' (%s)", player.StreamID, player.PlayerType)
|
||||
logger.Debugf(
|
||||
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)
|
||||
if err != nil {
|
||||
p.DisplayError(err)
|
||||
@@ -816,8 +851,22 @@ func (p *Panel) displayStreamPlayers(
|
||||
if !b {
|
||||
return
|
||||
}
|
||||
logger.Debugf(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)
|
||||
logger.Debugf(
|
||||
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(
|
||||
xcontext.DetachDone(ctx),
|
||||
player.StreamID,
|
||||
@@ -843,7 +892,12 @@ func (p *Panel) displayStreamPlayers(
|
||||
if !player.Disabled {
|
||||
pos, err := p.StreamD.StreamPlayerGetPosition(ctx, player.StreamID)
|
||||
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 {
|
||||
c.Add(widget.NewSeparator())
|
||||
posStr := pos.String()
|
||||
@@ -932,8 +986,22 @@ func (p *Panel) openAddOrEditRestreamWindow(
|
||||
quirks sstypes.ForwardingQuirks,
|
||||
) error,
|
||||
) {
|
||||
logger.Debugf(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)
|
||||
logger.Debugf(
|
||||
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)
|
||||
resizeWindow(w, fyne.NewSize(400, 300))
|
||||
|
||||
@@ -1146,7 +1214,11 @@ func (p *Panel) displayStreamForwards(
|
||||
c := container.NewHBox()
|
||||
deleteButton := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() {
|
||||
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) {
|
||||
if !b {
|
||||
@@ -1183,8 +1255,18 @@ func (p *Panel) displayStreamForwards(
|
||||
if !b {
|
||||
return
|
||||
}
|
||||
logger.Debugf(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)
|
||||
logger.Debugf(
|
||||
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(
|
||||
ctx,
|
||||
fwd.StreamID,
|
||||
@@ -1229,7 +1311,14 @@ func (p *Panel) displayStreamForwards(
|
||||
now := time.Now()
|
||||
p.previousNumBytesLocker.Do(ctx, func() {
|
||||
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)
|
||||
hasDynamicValue = hasDynamicValue || bwStr != ""
|
||||
p.previousNumBytes[key] = [4]uint64{fwd.NumBytesRead, fwd.NumBytesWrote}
|
||||
|
@@ -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 {
|
||||
ui.panel.DisplayError(fmt.Errorf("the time is not set for the timer"))
|
||||
|
@@ -15,7 +15,10 @@ type updateTimerHandler struct {
|
||||
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())
|
||||
h := &updateTimerHandler{
|
||||
ctx: ctx,
|
||||
|
@@ -121,7 +121,8 @@ func getMousePos(window fyne.Window) fyne.Position {
|
||||
func (w *HintWidget) isHovering(mousePos fyne.Position) bool {
|
||||
pos0 := GetAbsolutePosition(w, w.Window.Canvas().Content())
|
||||
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
|
||||
}
|
||||
|
||||
|
@@ -12,10 +12,16 @@ type dummyPlatformsController struct{}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
@@ -37,8 +37,16 @@ func assertNoError(ctx context.Context, err error) {
|
||||
func main() {
|
||||
loggerLevel := logger.LevelWarning
|
||||
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")
|
||||
streamID := pflag.String("stream-id", "test/test", "the path of the stream in rtmp://address/path")
|
||||
rtmpListenAddr := pflag.String(
|
||||
"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")
|
||||
pflag.Parse()
|
||||
|
||||
|
@@ -96,7 +96,14 @@ func (sp *StreamPlayers) Create(
|
||||
) (_ret *StreamPlayerHandler, _err error) {
|
||||
logger.Debugf(ctx, "StreamPlayers.Create(ctx, '%s', %#+v)", streamID, opts)
|
||||
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)
|
||||
|
||||
@@ -114,11 +121,18 @@ func (sp *StreamPlayers) Create(
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
@@ -157,13 +171,17 @@ func (sp *StreamPlayers) Get(streamID streamtypes.StreamID) *StreamPlayerHandler
|
||||
|
||||
func (sp *StreamPlayers) GetAll() map[streamtypes.StreamID]*StreamPlayerHandler {
|
||||
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{}
|
||||
for k, v := range sp.StreamPlayers {
|
||||
r[k] = v
|
||||
}
|
||||
return r
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -570,7 +588,10 @@ func (p *StreamPlayerHandler) controllerLoop(
|
||||
errmon.ObserveErrorCtx(ctx, p.Close())
|
||||
return
|
||||
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()
|
||||
return
|
||||
case <-t.C:
|
||||
@@ -580,9 +601,17 @@ func (p *StreamPlayerHandler) controllerLoop(
|
||||
now := time.Now()
|
||||
l, err := player.GetLength(ctx)
|
||||
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 {
|
||||
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()
|
||||
return
|
||||
}
|
||||
@@ -593,11 +622,25 @@ func (p *StreamPlayerHandler) controllerLoop(
|
||||
|
||||
pos, err := player.GetPosition(ctx)
|
||||
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)
|
||||
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 {
|
||||
logger.Debugf(ctx, "negative position: %v", pos)
|
||||
@@ -628,7 +671,11 @@ func (p *StreamPlayerHandler) controllerLoop(
|
||||
if curSpeed == 1 {
|
||||
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)
|
||||
if err != nil {
|
||||
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())/
|
||||
(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 {
|
||||
logger.Warnf(
|
||||
ctx,
|
||||
"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,
|
||||
lag.Seconds(), p.Config.JitterBufDuration.Seconds(),
|
||||
p.Config.MaxCatchupAtLag.Seconds(), p.Config.JitterBufDuration.Seconds(),
|
||||
l, pos,
|
||||
p.Config.CatchupMaxSpeedFactor,
|
||||
lag.Seconds(),
|
||||
p.Config.JitterBufDuration.Seconds(),
|
||||
p.Config.MaxCatchupAtLag.Seconds(),
|
||||
p.Config.JitterBufDuration.Seconds(),
|
||||
l,
|
||||
pos,
|
||||
)
|
||||
speed = p.Config.CatchupMaxSpeedFactor
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "unable to set the speed to %v: %v", speed, err)
|
||||
|
@@ -184,7 +184,9 @@ func StreamsHandle(url string) (core.Producer, error) {
|
||||
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()
|
||||
trafficCounter := &types.TrafficCounter{}
|
||||
run := func(ctx context.Context) error {
|
||||
|
@@ -74,7 +74,12 @@ func (s *StreamServer) initNoLock(ctx context.Context) error {
|
||||
for dstID, dstCfg := range cfg.Destinations {
|
||||
err := s.addStreamDestination(ctx, dstID, dstCfg.URL)
|
||||
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 {
|
||||
err := s.addStreamForward(ctx, streamID, dstID)
|
||||
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 {
|
||||
streamSrc := s.StreamHandler.Get(string(streamID))
|
||||
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)
|
||||
if err != nil {
|
||||
@@ -418,7 +432,11 @@ func (s *StreamServer) listStreamForwards(
|
||||
streamIDSrc := types.StreamID(name)
|
||||
streamDst, err := s.findStreamDestinationByURL(ctx, fwd.URL)
|
||||
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{
|
||||
StreamID: streamIDSrc,
|
||||
@@ -467,7 +485,13 @@ func (s *StreamServer) removeStreamForward(
|
||||
|
||||
err = fwd.Close()
|
||||
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()
|
||||
return nil
|
||||
@@ -562,7 +586,10 @@ func (s *StreamServer) findStreamDestinationByURL(
|
||||
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(
|
||||
@@ -574,5 +601,8 @@ func (s *StreamServer) findStreamDestinationByID(
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
@@ -112,7 +112,14 @@ type conn struct {
|
||||
func (c *conn) appendDOT(dot []byte, group string) []byte {
|
||||
host := c.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" {
|
||||
dot = fmt.Appendf(dot, "%s -> %d [label=%q];\n", host, c.ID, humanBytes(c.BytesRecv))
|
||||
} else {
|
||||
|
@@ -89,7 +89,9 @@ func (s *StreamHandler) HandleConsumerFunc(scheme string, handler ConsumerHandle
|
||||
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 {
|
||||
scheme := url[:i]
|
||||
|
||||
|
@@ -65,7 +65,12 @@ func (s *StreamServer) init(ctx context.Context) error {
|
||||
for dstID, dstCfg := range cfg.Destinations {
|
||||
err := s.addStreamDestination(ctx, dstID, dstCfg.URL)
|
||||
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 {
|
||||
_, err := s.addStreamForward(ctx, streamID, dstID, fwd.Quirks)
|
||||
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() {
|
||||
err = portServer.Server.Serve(listener)
|
||||
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)
|
||||
}
|
||||
})
|
||||
@@ -371,7 +385,15 @@ func (s *StreamServer) addStreamForward(
|
||||
|
||||
ctx = belt.WithField(ctx, "stream_forward", fmt.Sprintf("%s->%s", streamID, destinationID))
|
||||
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)
|
||||
@@ -633,7 +655,10 @@ func (s *StreamServer) findStreamDestinationByID(
|
||||
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(
|
||||
|
@@ -73,7 +73,11 @@ func (srv *RTMPServer) init(
|
||||
}
|
||||
}()
|
||||
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 {
|
||||
@@ -84,7 +88,10 @@ func (srv *RTMPServer) init(
|
||||
}
|
||||
if cfg.IsTLS && (srv.ServerCert == "" || srv.ServerKey == "") {
|
||||
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()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to set the TLS certificate: %w", err)
|
||||
@@ -173,7 +180,11 @@ func (srv *RTMPServer) setServerCertificate(
|
||||
Bytes: cert.Raw,
|
||||
}
|
||||
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")
|
||||
@@ -192,7 +203,11 @@ func (srv *RTMPServer) setServerCertificate(
|
||||
Bytes: keyBytes,
|
||||
}
|
||||
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()
|
||||
|
@@ -117,7 +117,13 @@ func (s *StreamServer) init(
|
||||
s.mutex.Do(ctx, func() {
|
||||
_, err := s.startServer(ctx, srv.Type, srv.Listen, srv.Options()...)
|
||||
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() {
|
||||
publisher := s.publishers[appKey]
|
||||
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
|
||||
}
|
||||
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 {
|
||||
ch <- publisher
|
||||
@@ -399,7 +415,12 @@ func (s *StreamServer) WaitPublisherChan(
|
||||
logger.Debugf(ctx, "WaitPublisherChan('%s', %v): cancelled", appKey, waitForNext)
|
||||
return
|
||||
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 {
|
||||
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...)
|
||||
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())
|
||||
|
@@ -66,7 +66,11 @@ func (h *Handler) OnCreateStream(timestamp uint32, cmd *rtmpmsg.NetConnectionCre
|
||||
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)
|
||||
|
||||
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 {
|
||||
return errors.New("Cannot play on this stream")
|
||||
}
|
||||
|
@@ -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
|
||||
|
||||
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()
|
||||
return xsync.DoR1(ctx, &pb.m, func() *Sub {
|
||||
subID := pb.nextSubID
|
||||
@@ -303,7 +306,11 @@ func (s *Sub) senderLoop(
|
||||
|
||||
case tag, ok := <-s.sendQueue:
|
||||
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()
|
||||
return
|
||||
}
|
||||
@@ -313,7 +320,12 @@ func (s *Sub) senderLoop(
|
||||
err := s.onEvent(ctx, tag)
|
||||
if err != nil {
|
||||
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 {
|
||||
metrics.FromCtx(ctx).Count("submit_process_success").Add(1)
|
||||
}
|
||||
@@ -331,7 +343,11 @@ func (s *Sub) Submit(
|
||||
case s.sendQueue <- flv:
|
||||
metrics.FromCtx(ctx).Count("submit_pushed").Add(1)
|
||||
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() {
|
||||
s.CloseOrLog(ctx)
|
||||
})
|
||||
|
@@ -12,5 +12,9 @@ func NewStreamForwards(
|
||||
s StreamServer,
|
||||
platformsController types.PlatformsController,
|
||||
) *StreamForwards {
|
||||
return streamforward.NewStreamForwards(s, xaionarogortmp.NewRecoderFactory(), platformsController)
|
||||
return streamforward.NewStreamForwards(
|
||||
s,
|
||||
xaionarogortmp.NewRecoderFactory(),
|
||||
platformsController,
|
||||
)
|
||||
}
|
||||
|
@@ -88,7 +88,13 @@ func (s *StreamServer) init(
|
||||
observability.Go(ctx, func() {
|
||||
_, err := s.startServer(ctx, srv.Type, srv.Listen)
|
||||
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) {
|
||||
ctx := belt.WithField(ctx, "client", conn.RemoteAddr().String())
|
||||
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{
|
||||
Handler: h,
|
||||
ControlState: rtmp.StreamControlStateConfig{
|
||||
@@ -223,7 +233,11 @@ func (s *StreamServer) startServer(
|
||||
observability.Go(ctx, func() {
|
||||
err = portSrv.Serve(listener)
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
@@ -52,9 +52,21 @@ func (fwds *StreamForwards) NewActiveStreamForward(
|
||||
pauseFunc func(ctx context.Context, fwd *ActiveStreamForwarding),
|
||||
opts ...Option,
|
||||
) (_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() {
|
||||
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)
|
||||
@@ -176,7 +188,10 @@ func (fwd *ActiveStreamForwarding) waitForPublisherAndStart(
|
||||
})
|
||||
|
||||
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() {
|
||||
if destinationUnlocker != nil { // if ctx was cancelled before we locked then the unlocker is nil
|
||||
destinationUnlocker.Unlock()
|
||||
@@ -337,7 +352,12 @@ func (fwd *ActiveStreamForwarding) openOutputFor(
|
||||
ctx context.Context,
|
||||
recoderInstance recoder.Recoder,
|
||||
) (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 {
|
||||
return nil, fmt.Errorf("unable to open '%s' as the output: %w", fwd.DestinationURL, err)
|
||||
}
|
||||
|
@@ -72,7 +72,12 @@ func (s *StreamForwards) init(
|
||||
for dstID, dstCfg := range cfg.Destinations {
|
||||
err := s.addActiveStreamDestination(ctx, dstID, dstCfg.URL, dstCfg.StreamKey)
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -84,7 +89,12 @@ func (s *StreamForwards) init(
|
||||
}
|
||||
_, err := s.newActiveStreamForward(ctx, streamID, dstID, fwd.Quirks)
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -187,7 +197,10 @@ func (s *StreamForwards) newActiveStreamForward(
|
||||
DestinationID: destinationID,
|
||||
}
|
||||
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)
|
||||
@@ -227,7 +240,10 @@ func (s *StreamForwards) newActiveStreamForward(
|
||||
) {
|
||||
if quirks.StartAfterYoutubeRecognizedStream.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 {
|
||||
logger.Debugf(ctx, "fwd %s->%s is waiting for YouTube to recognize the stream", streamID, destinationID)
|
||||
started, err := s.PlatformsController.CheckStreamStartedByPlatformID(
|
||||
@@ -284,13 +300,21 @@ func (s *StreamForwards) restartUntilYoutubeRecognizesStream(
|
||||
cfg types.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)
|
||||
defer func() { logger.Debugf(ctx, "restartUntilYoutubeRecognizesStream(ctx, %#+v, %#+v)", fwd, cfg) }()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -316,21 +340,39 @@ func (s *StreamForwards) restartUntilYoutubeRecognizesStream(
|
||||
return
|
||||
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 {
|
||||
streamOK, err := s.PlatformsController.CheckStreamStartedByPlatformID(
|
||||
memoize.SetNoCache(ctx, true),
|
||||
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 {
|
||||
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)
|
||||
continue
|
||||
}
|
||||
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 {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
@@ -340,9 +382,19 @@ func (s *StreamForwards) restartUntilYoutubeRecognizesStream(
|
||||
memoize.SetNoCache(ctx, true),
|
||||
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 {
|
||||
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)
|
||||
continue
|
||||
}
|
||||
@@ -353,7 +405,10 @@ func (s *StreamForwards) restartUntilYoutubeRecognizesStream(
|
||||
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()
|
||||
if err != nil {
|
||||
@@ -366,7 +421,10 @@ func (s *StreamForwards) restartUntilYoutubeRecognizesStream(
|
||||
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)
|
||||
if err != nil {
|
||||
@@ -447,9 +505,12 @@ func (s *StreamForwards) ListStreamForwards(
|
||||
}()
|
||||
|
||||
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
|
||||
})
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -486,7 +547,12 @@ func (s *StreamForwards) getStreamForwards(
|
||||
if !filterFunc(streamID, ordered.Optional[types.DestinationID]{}) {
|
||||
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 {
|
||||
if !filterFunc(streamID, ordered.Opt(dstID)) {
|
||||
continue
|
||||
@@ -589,9 +655,12 @@ func (s *StreamForwards) GetStreamForwardsByDestination(
|
||||
}()
|
||||
|
||||
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
|
||||
})
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -647,7 +716,15 @@ func (s *StreamForwards) UpdateStreamDestination(
|
||||
streamKey string,
|
||||
) error {
|
||||
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(
|
||||
@@ -659,14 +736,20 @@ func (s *StreamForwards) updateStreamDestination(
|
||||
s.WithConfig(ctx, func(ctx context.Context, cfg *types.Config) {
|
||||
for key := range s.ActiveStreamForwardings {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
err := s.removeActiveStreamDestination(ctx, destinationID)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -725,9 +808,12 @@ func (s *StreamForwards) removeActiveStreamDestination(
|
||||
ctx context.Context,
|
||||
destinationID types.DestinationID,
|
||||
) 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
|
||||
})
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to list stream forwardings: %w", err)
|
||||
}
|
||||
@@ -756,7 +842,10 @@ func (s *StreamForwards) findStreamDestinationByID(
|
||||
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) {
|
||||
|
@@ -362,7 +362,10 @@ func (s *StreamPlayers) setStreamPlayer(
|
||||
|
||||
var opts SetupOptions
|
||||
if playerCfg.DefaultStreamPlayerOptions != nil {
|
||||
opts = append(opts, SetupOptionDefaultStreamPlayerOptions(playerCfg.DefaultStreamPlayerOptions))
|
||||
opts = append(
|
||||
opts,
|
||||
SetupOptionDefaultStreamPlayerOptions(playerCfg.DefaultStreamPlayerOptions),
|
||||
)
|
||||
}
|
||||
|
||||
var streamCfg map[types.StreamID]*types.StreamConfig
|
||||
|
@@ -9,5 +9,8 @@ import (
|
||||
|
||||
type PlatformsController interface {
|
||||
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)
|
||||
}
|
||||
|
Reference in New Issue
Block a user