mirror of
https://github.com/xaionaro-go/streamctl.git
synced 2025-10-11 10:20:09 +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)
|
assertNoError(ctx, err)
|
||||||
|
|
||||||
if isEnabled[youtube.ID] {
|
if isEnabled[youtube.ID] {
|
||||||
err := streamD.StartStream(ctx, youtube.ID, title, description, cfg.Backends[youtube.ID].StreamProfiles[profileName])
|
err := streamD.StartStream(
|
||||||
|
ctx,
|
||||||
|
youtube.ID,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
cfg.Backends[youtube.ID].StreamProfiles[profileName],
|
||||||
|
)
|
||||||
assertNoError(ctx, err)
|
assertNoError(ctx, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if isEnabled[twitch.ID] {
|
if isEnabled[twitch.ID] {
|
||||||
err := streamD.StartStream(ctx, twitch.ID, title, description, cfg.Backends[twitch.ID].StreamProfiles[profileName])
|
err := streamD.StartStream(
|
||||||
|
ctx,
|
||||||
|
twitch.ID,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
cfg.Backends[twitch.ID].StreamProfiles[profileName],
|
||||||
|
)
|
||||||
assertNoError(ctx, err)
|
assertNoError(ctx, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -97,7 +97,8 @@ func init() {
|
|||||||
StreamStart.PersistentFlags().String("title", "", "stream title")
|
StreamStart.PersistentFlags().String("title", "", "stream title")
|
||||||
StreamStart.PersistentFlags().String("description", "", "stream description")
|
StreamStart.PersistentFlags().String("description", "", "stream description")
|
||||||
StreamStart.PersistentFlags().String("profile", "", "profile")
|
StreamStart.PersistentFlags().String("profile", "", "profile")
|
||||||
StreamStart.PersistentFlags().StringArray("youtube-templates", nil, "the list of templates used to create streams; if nothing is provided, then a stream won't be created")
|
StreamStart.PersistentFlags().
|
||||||
|
StringArray("youtube-templates", nil, "the list of templates used to create streams; if nothing is provided, then a stream won't be created")
|
||||||
|
|
||||||
Root.AddCommand(GenerateConfig)
|
Root.AddCommand(GenerateConfig)
|
||||||
Root.AddCommand(SetTitle)
|
Root.AddCommand(SetTitle)
|
||||||
@@ -153,8 +154,12 @@ func generateConfig(cmd *cobra.Command, args []string) {
|
|||||||
logger.Panicf(cmd.Context(), "file '%s' already exists", cfgPath)
|
logger.Panicf(cmd.Context(), "file '%s' already exists", cfgPath)
|
||||||
}
|
}
|
||||||
cfg := newConfig()
|
cfg := newConfig()
|
||||||
cfg[idTwitch].StreamProfiles = map[streamcontrol.ProfileName]streamcontrol.AbstractStreamProfile{"some_profile": twitch.StreamProfile{}}
|
cfg[idTwitch].StreamProfiles = map[streamcontrol.ProfileName]streamcontrol.AbstractStreamProfile{
|
||||||
cfg[idYoutube].StreamProfiles = map[streamcontrol.ProfileName]streamcontrol.AbstractStreamProfile{"some_profile": youtube.StreamProfile{}}
|
"some_profile": twitch.StreamProfile{},
|
||||||
|
}
|
||||||
|
cfg[idYoutube].StreamProfiles = map[streamcontrol.ProfileName]streamcontrol.AbstractStreamProfile{
|
||||||
|
"some_profile": youtube.StreamProfile{},
|
||||||
|
}
|
||||||
err := writeConfigToPath(cmd.Context(), cfgPath, cfg)
|
err := writeConfigToPath(cmd.Context(), cfgPath, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Panic(cmd.Context(), err)
|
logger.Panic(cmd.Context(), err)
|
||||||
@@ -203,7 +208,10 @@ func readConfigFromPath(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (*cfg)[idTwitch] != nil {
|
if (*cfg)[idTwitch] != nil {
|
||||||
err = streamcontrol.ConvertStreamProfiles[twitch.StreamProfile](ctx, (*cfg)[idTwitch].StreamProfiles)
|
err = streamcontrol.ConvertStreamProfiles[twitch.StreamProfile](
|
||||||
|
ctx,
|
||||||
|
(*cfg)[idTwitch].StreamProfiles,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to convert stream profiles of twitch: %w: <%s>", err, b)
|
return fmt.Errorf("unable to convert stream profiles of twitch: %w: <%s>", err, b)
|
||||||
}
|
}
|
||||||
@@ -211,11 +219,18 @@ func readConfigFromPath(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (*cfg)[idYoutube] != nil {
|
if (*cfg)[idYoutube] != nil {
|
||||||
err = streamcontrol.ConvertStreamProfiles[youtube.StreamProfile](ctx, (*cfg)[idYoutube].StreamProfiles)
|
err = streamcontrol.ConvertStreamProfiles[youtube.StreamProfile](
|
||||||
|
ctx,
|
||||||
|
(*cfg)[idYoutube].StreamProfiles,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to convert stream profiles of twitch: %w: <%s>", err, b)
|
return fmt.Errorf("unable to convert stream profiles of twitch: %w: <%s>", err, b)
|
||||||
}
|
}
|
||||||
logger.Debugf(ctx, "final stream profiles of youtube: %#+v", (*cfg)[idYoutube].StreamProfiles)
|
logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"final stream profiles of youtube: %#+v",
|
||||||
|
(*cfg)[idYoutube].StreamProfiles,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -244,7 +259,11 @@ func getTwitchStreamController(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
cfg streamcontrol.Config,
|
cfg streamcontrol.Config,
|
||||||
) (*twitch.Twitch, error) {
|
) (*twitch.Twitch, error) {
|
||||||
platCfg := streamcontrol.GetPlatformConfig[twitch.PlatformSpecificConfig, twitch.StreamProfile](ctx, cfg, idTwitch)
|
platCfg := streamcontrol.GetPlatformConfig[twitch.PlatformSpecificConfig, twitch.StreamProfile](
|
||||||
|
ctx,
|
||||||
|
cfg,
|
||||||
|
idTwitch,
|
||||||
|
)
|
||||||
if platCfg == nil {
|
if platCfg == nil {
|
||||||
logger.Infof(ctx, "twitch config was not found")
|
logger.Infof(ctx, "twitch config was not found")
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -268,7 +287,11 @@ func getYouTubeStreamController(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
cfg streamcontrol.Config,
|
cfg streamcontrol.Config,
|
||||||
) (*youtube.YouTube, error) {
|
) (*youtube.YouTube, error) {
|
||||||
platCfg := streamcontrol.GetPlatformConfig[youtube.PlatformSpecificConfig, youtube.StreamProfile](ctx, cfg, idYoutube)
|
platCfg := streamcontrol.GetPlatformConfig[youtube.PlatformSpecificConfig, youtube.StreamProfile](
|
||||||
|
ctx,
|
||||||
|
cfg,
|
||||||
|
idYoutube,
|
||||||
|
)
|
||||||
if platCfg == nil {
|
if platCfg == nil {
|
||||||
logger.Infof(ctx, "youtube config was not found")
|
logger.Infof(ctx, "youtube config was not found")
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -288,7 +311,10 @@ func getYouTubeStreamController(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStreamControllers(ctx context.Context, cfg streamcontrol.Config) streamcontrol.StreamControllers {
|
func getStreamControllers(
|
||||||
|
ctx context.Context,
|
||||||
|
cfg streamcontrol.Config,
|
||||||
|
) streamcontrol.StreamControllers {
|
||||||
var result streamcontrol.StreamControllers
|
var result streamcontrol.StreamControllers
|
||||||
|
|
||||||
twitch, err := getTwitchStreamController(ctx, cfg)
|
twitch, err := getTwitchStreamController(ctx, cfg)
|
||||||
|
@@ -39,9 +39,21 @@ const forceNetPProfOnAndroid = true
|
|||||||
func main() {
|
func main() {
|
||||||
loggerLevel := logger.LevelWarning
|
loggerLevel := logger.LevelWarning
|
||||||
pflag.Var(&loggerLevel, "log-level", "Log level")
|
pflag.Var(&loggerLevel, "log-level", "Log level")
|
||||||
listenAddr := pflag.String("listen-addr", ":3594", "the address to listen for incoming connections to")
|
listenAddr := pflag.String(
|
||||||
configPath := pflag.String("config-path", "/etc/streamd/streamd.yaml", "the path to the config file")
|
"listen-addr",
|
||||||
netPprofAddr := pflag.String("go-net-pprof-addr", "", "address to listen to for net/pprof requests")
|
":3594",
|
||||||
|
"the address to listen for incoming connections to",
|
||||||
|
)
|
||||||
|
configPath := pflag.String(
|
||||||
|
"config-path",
|
||||||
|
"/etc/streamd/streamd.yaml",
|
||||||
|
"the path to the config file",
|
||||||
|
)
|
||||||
|
netPprofAddr := pflag.String(
|
||||||
|
"go-net-pprof-addr",
|
||||||
|
"",
|
||||||
|
"address to listen to for net/pprof requests",
|
||||||
|
)
|
||||||
cpuProfile := pflag.String("go-profile-cpu", "", "file to write cpu profile to")
|
cpuProfile := pflag.String("go-profile-cpu", "", "file to write cpu profile to")
|
||||||
heapProfile := pflag.String("go-profile-heap", "", "file to write memory profile to")
|
heapProfile := pflag.String("go-profile-heap", "", "file to write memory profile to")
|
||||||
sentryDSN := pflag.String("sentry-dsn", "", "DSN of a Sentry instance to send error reports")
|
sentryDSN := pflag.String("sentry-dsn", "", "DSN of a Sentry instance to send error reports")
|
||||||
@@ -210,7 +222,13 @@ func main() {
|
|||||||
},
|
},
|
||||||
func(listenPort uint16, platID streamcontrol.PlatformName, authURL string) bool {
|
func(listenPort uint16, platID streamcontrol.PlatformName, authURL string) bool {
|
||||||
logger.Debugf(ctx, "streamd.UI.OpenOAuthURL(%d, %s, '%s')", listenPort, platID, authURL)
|
logger.Debugf(ctx, "streamd.UI.OpenOAuthURL(%d, %s, '%s')", listenPort, platID, authURL)
|
||||||
defer logger.Debugf(ctx, "/streamd.UI.OpenOAuthURL(%d, %s, '%s')", listenPort, platID, authURL)
|
defer logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"/streamd.UI.OpenOAuthURL(%d, %s, '%s')",
|
||||||
|
listenPort,
|
||||||
|
platID,
|
||||||
|
authURL,
|
||||||
|
)
|
||||||
|
|
||||||
grpcLocker.Lock()
|
grpcLocker.Lock()
|
||||||
logger.Debugf(ctx, "streamdGRPCLocker.Lock()-ed")
|
logger.Debugf(ctx, "streamdGRPCLocker.Lock()-ed")
|
||||||
|
@@ -159,7 +159,13 @@ func (ui *UI) oauth2Handler(
|
|||||||
defer removeReceiver()
|
defer removeReceiver()
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Debugf(ctx, "asking to open the URL '%s' using listen port %d for platform '%s'", arg.AuthURL, arg.ListenPort, platID)
|
logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"asking to open the URL '%s' using listen port %d for platform '%s'",
|
||||||
|
arg.AuthURL,
|
||||||
|
arg.ListenPort,
|
||||||
|
platID,
|
||||||
|
)
|
||||||
ui.OAuthURLOpenFn(arg.ListenPort, platID, arg.AuthURL)
|
ui.OAuthURLOpenFn(arg.ListenPort, platID, arg.AuthURL)
|
||||||
|
|
||||||
t := time.NewTicker(time.Hour)
|
t := time.NewTicker(time.Hour)
|
||||||
|
@@ -63,23 +63,71 @@ func parseFlags() Flags {
|
|||||||
defaultLogFile = ""
|
defaultLogFile = ""
|
||||||
}
|
}
|
||||||
pflag.Var(&loggerLevelValue, "log-level", "Log level")
|
pflag.Var(&loggerLevelValue, "log-level", "Log level")
|
||||||
listenAddr := pflag.String("listen-addr", "", "the address to listen for incoming connections to")
|
listenAddr := pflag.String(
|
||||||
remoteAddr := pflag.String("remote-addr", "", "the address (for example 127.0.0.1:3594) of streamd to connect to, instead of running the stream controllers locally")
|
"listen-addr",
|
||||||
|
"",
|
||||||
|
"the address to listen for incoming connections to",
|
||||||
|
)
|
||||||
|
remoteAddr := pflag.String(
|
||||||
|
"remote-addr",
|
||||||
|
"",
|
||||||
|
"the address (for example 127.0.0.1:3594) of streamd to connect to, instead of running the stream controllers locally",
|
||||||
|
)
|
||||||
configPath := pflag.String("config-path", "~/.streampanel.yaml", "the path to the config file")
|
configPath := pflag.String("config-path", "~/.streampanel.yaml", "the path to the config file")
|
||||||
netPprofAddrMain := pflag.String("go-net-pprof-addr-main", "", "address to listen to for net/pprof requests by the main process")
|
netPprofAddrMain := pflag.String(
|
||||||
netPprofAddrUI := pflag.String("go-net-pprof-addr-ui", "", "address to listen to for net/pprof requests by the UI process")
|
"go-net-pprof-addr-main",
|
||||||
netPprofAddrStreamD := pflag.String("go-net-pprof-addr-streamd", "", "address to listen to for net/pprof requests by the streamd process")
|
"",
|
||||||
|
"address to listen to for net/pprof requests by the main process",
|
||||||
|
)
|
||||||
|
netPprofAddrUI := pflag.String(
|
||||||
|
"go-net-pprof-addr-ui",
|
||||||
|
"",
|
||||||
|
"address to listen to for net/pprof requests by the UI process",
|
||||||
|
)
|
||||||
|
netPprofAddrStreamD := pflag.String(
|
||||||
|
"go-net-pprof-addr-streamd",
|
||||||
|
"",
|
||||||
|
"address to listen to for net/pprof requests by the streamd process",
|
||||||
|
)
|
||||||
cpuProfile := pflag.String("go-profile-cpu", "", "file to write cpu profile to")
|
cpuProfile := pflag.String("go-profile-cpu", "", "file to write cpu profile to")
|
||||||
heapProfile := pflag.String("go-profile-heap", "", "file to write memory profile to")
|
heapProfile := pflag.String("go-profile-heap", "", "file to write memory profile to")
|
||||||
logstashAddr := pflag.String("logstash-addr", "", "the address of logstash to send logs to (for example: 'tcp://192.168.0.2:5044')")
|
logstashAddr := pflag.String(
|
||||||
|
"logstash-addr",
|
||||||
|
"",
|
||||||
|
"the address of logstash to send logs to (for example: 'tcp://192.168.0.2:5044')",
|
||||||
|
)
|
||||||
sentryDSN := pflag.String("sentry-dsn", "", "DSN of a Sentry instance to send error reports")
|
sentryDSN := pflag.String("sentry-dsn", "", "DSN of a Sentry instance to send error reports")
|
||||||
page := pflag.String("page", string(consts.PageControl), "DSN of a Sentry instance to send error reports")
|
page := pflag.String(
|
||||||
|
"page",
|
||||||
|
string(consts.PageControl),
|
||||||
|
"DSN of a Sentry instance to send error reports",
|
||||||
|
)
|
||||||
logFile := pflag.String("log-file", defaultLogFile, "log file to write logs into")
|
logFile := pflag.String("log-file", defaultLogFile, "log file to write logs into")
|
||||||
subprocess := pflag.String("subprocess", "", "[internal use flag] run a specific sub-process (format: processName:addressToConnect)")
|
subprocess := pflag.String(
|
||||||
splitProcess := pflag.Bool("split-process", !isMobile(), "split the process into multiple processes for better stability")
|
"subprocess",
|
||||||
lockTimeout := pflag.Duration("lock-timeout", 2*time.Minute, "[debug option] change the timeout for locking, before reporting it as a deadlock")
|
"",
|
||||||
oauthListenPortTwitch := pflag.Uint16("oauth-listen-port-twitch", 8091, "the port that is used for OAuth callbacks while authenticating in Twitch")
|
"[internal use flag] run a specific sub-process (format: processName:addressToConnect)",
|
||||||
oauthListenPortYouTube := pflag.Uint16("oauth-listen-port-youtube", 8092, "the port that is used for OAuth callbacks while authenticating in YouTube")
|
)
|
||||||
|
splitProcess := pflag.Bool(
|
||||||
|
"split-process",
|
||||||
|
!isMobile(),
|
||||||
|
"split the process into multiple processes for better stability",
|
||||||
|
)
|
||||||
|
lockTimeout := pflag.Duration(
|
||||||
|
"lock-timeout",
|
||||||
|
2*time.Minute,
|
||||||
|
"[debug option] change the timeout for locking, before reporting it as a deadlock",
|
||||||
|
)
|
||||||
|
oauthListenPortTwitch := pflag.Uint16(
|
||||||
|
"oauth-listen-port-twitch",
|
||||||
|
8091,
|
||||||
|
"the port that is used for OAuth callbacks while authenticating in Twitch",
|
||||||
|
)
|
||||||
|
oauthListenPortYouTube := pflag.Uint16(
|
||||||
|
"oauth-listen-port-youtube",
|
||||||
|
8092,
|
||||||
|
"the port that is used for OAuth callbacks while authenticating in YouTube",
|
||||||
|
)
|
||||||
|
|
||||||
pflag.Parse()
|
pflag.Parse()
|
||||||
|
|
||||||
@@ -130,7 +178,11 @@ func getFlags(
|
|||||||
func(ctx context.Context, source mainprocess.ProcessName, content any) error {
|
func(ctx context.Context, source mainprocess.ProcessName, content any) error {
|
||||||
result, ok := content.(GetFlagsResult)
|
result, ok := content.(GetFlagsResult)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("got unexpected type '%T' instead of %T", content, GetFlagsResult{})
|
return fmt.Errorf(
|
||||||
|
"got unexpected type '%T' instead of %T",
|
||||||
|
content,
|
||||||
|
GetFlagsResult{},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
flags = result.Flags
|
flags = result.Flags
|
||||||
return nil
|
return nil
|
||||||
|
@@ -103,5 +103,11 @@ func getFlagsAndroidFromFiles(flags *Flags) {
|
|||||||
logger.Errorf(ctx, "unable to unserialize '%s': %v", flagsSerialized, err)
|
logger.Errorf(ctx, "unable to unserialize '%s': %v", flagsSerialized, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Debugf(ctx, "successfully parsed file '%s' with content '%s'; now the flags == %#+v", flagsFilePath, flagsSerialized, *flags)
|
logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"successfully parsed file '%s' with content '%s'; now the flags == %#+v",
|
||||||
|
flagsFilePath,
|
||||||
|
flagsSerialized,
|
||||||
|
*flags,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@@ -63,7 +63,10 @@ func runSubprocess(
|
|||||||
) {
|
) {
|
||||||
parts := strings.SplitN(subprocessFlag, ":", 2)
|
parts := strings.SplitN(subprocessFlag, ":", 2)
|
||||||
if len(parts) != 2 {
|
if len(parts) != 2 {
|
||||||
logger.Panicf(belt.WithField(preCtx, "process", ""), "expected 2 parts in --subprocess: name and address, separated via a colon")
|
logger.Panicf(
|
||||||
|
belt.WithField(preCtx, "process", ""),
|
||||||
|
"expected 2 parts in --subprocess: name and address, separated via a colon",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
procName := ProcessName(parts[0])
|
procName := ProcessName(parts[0])
|
||||||
addr := parts[1]
|
addr := parts[1]
|
||||||
@@ -123,7 +126,12 @@ func runSplitProcesses(
|
|||||||
case ProcessNameStreamd:
|
case ProcessNameStreamd:
|
||||||
err := m.SendMessage(ctx, ProcessNameUI, StreamDDied{})
|
err := m.SendMessage(ctx, ProcessNameUI, StreamDDied{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf(ctx, "failed to send a StreamDDied message to '%s': %v", ProcessNameUI, err)
|
logger.Errorf(
|
||||||
|
ctx,
|
||||||
|
"failed to send a StreamDDied message to '%s': %v",
|
||||||
|
ProcessNameUI,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,11 +197,25 @@ func runFork(
|
|||||||
|
|
||||||
ctx, cancelFn := context.WithCancel(ctx)
|
ctx, cancelFn := context.WithCancel(ctx)
|
||||||
os.Setenv(EnvPassword, password)
|
os.Setenv(EnvPassword, password)
|
||||||
args := []string{execPath, "--sentry-dsn=" + flags.SentryDSN, "--log-level=" + logger.Level(flags.LoggerLevel).String(), "--subprocess=" + string(procName) + ":" + addr, "--logstash-addr=" + flags.LogstashAddr}
|
args := []string{
|
||||||
|
execPath,
|
||||||
|
"--sentry-dsn=" + flags.SentryDSN,
|
||||||
|
"--log-level=" + logger.Level(flags.LoggerLevel).String(),
|
||||||
|
"--subprocess=" + string(procName) + ":" + addr,
|
||||||
|
"--logstash-addr=" + flags.LogstashAddr,
|
||||||
|
}
|
||||||
logger.Infof(ctx, "running '%s %s'", args[0], strings.Join(args[1:], " "))
|
logger.Infof(ctx, "running '%s %s'", args[0], strings.Join(args[1:], " "))
|
||||||
cmd := exec.Command(args[0], args[1:]...)
|
cmd := exec.Command(args[0], args[1:]...)
|
||||||
cmd.Stderr = logwriter.NewLogWriter(ctx, logger.FromCtx(ctx).WithField("log_writer_target", "split").WithField("output_type", "stderr"))
|
cmd.Stderr = logwriter.NewLogWriter(
|
||||||
cmd.Stdout = logwriter.NewLogWriter(ctx, logger.FromCtx(ctx).WithField("log_writer_target", "split"))
|
ctx,
|
||||||
|
logger.FromCtx(ctx).
|
||||||
|
WithField("log_writer_target", "split").
|
||||||
|
WithField("output_type", "stderr"),
|
||||||
|
)
|
||||||
|
cmd.Stdout = logwriter.NewLogWriter(
|
||||||
|
ctx,
|
||||||
|
logger.FromCtx(ctx).WithField("log_writer_target", "split"),
|
||||||
|
)
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
err = child_process_manager.ConfigureCommand(cmd)
|
err = child_process_manager.ConfigureCommand(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -213,7 +235,13 @@ func runFork(
|
|||||||
err := cmd.Wait()
|
err := cmd.Wait()
|
||||||
cancelFn()
|
cancelFn()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf(ctx, "error running '%s %s': %v", args[0], strings.Join(args[1:], " "), err)
|
logger.Errorf(
|
||||||
|
ctx,
|
||||||
|
"error running '%s %s': %v",
|
||||||
|
args[0],
|
||||||
|
strings.Join(args[1:], " "),
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
@@ -251,7 +279,11 @@ func setReadyFor(
|
|||||||
func(ctx context.Context, source ProcessName, content any) error {
|
func(ctx context.Context, source ProcessName, content any) error {
|
||||||
_, ok := content.(mainprocess.MessageReadyConfirmed)
|
_, ok := content.(mainprocess.MessageReadyConfirmed)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("got unexpected type '%T' instead of %T", content, mainprocess.MessageReadyConfirmed{})
|
return fmt.Errorf(
|
||||||
|
"got unexpected type '%T' instead of %T",
|
||||||
|
content,
|
||||||
|
mainprocess.MessageReadyConfirmed{},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
@@ -104,7 +104,11 @@ func runPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert(ctx, panel.StreamD != nil)
|
assert(ctx, panel.StreamD != nil)
|
||||||
listener, grpcServer, streamdGRPC, _ := initGRPCServers(ctx, panel.StreamD, flags.ListenAddr)
|
listener, grpcServer, streamdGRPC, _ := initGRPCServers(
|
||||||
|
ctx,
|
||||||
|
panel.StreamD,
|
||||||
|
flags.ListenAddr,
|
||||||
|
)
|
||||||
|
|
||||||
// to erase an oauth request answered locally from "UnansweredOAuthRequests" in the GRPC server:
|
// to erase an oauth request answered locally from "UnansweredOAuthRequests" in the GRPC server:
|
||||||
panel.OnInternallySubmittedOAuthCode = func(
|
panel.OnInternallySubmittedOAuthCode = func(
|
||||||
|
@@ -86,7 +86,10 @@ func initRuntime(
|
|||||||
|
|
||||||
if netPprofAddr != "" {
|
if netPprofAddr != "" {
|
||||||
observability.Go(ctx, func() {
|
observability.Go(ctx, func() {
|
||||||
http.Handle("/metrics", promhttp.Handler()) // TODO: either split this from pprof argument, or rename the argument (and re-describe it)
|
http.Handle(
|
||||||
|
"/metrics",
|
||||||
|
promhttp.Handler(),
|
||||||
|
) // TODO: either split this from pprof argument, or rename the argument (and re-describe it)
|
||||||
|
|
||||||
l.Infof("starting to listen for net/pprof requests at '%s'", netPprofAddr)
|
l.Infof("starting to listen for net/pprof requests at '%s'", netPprofAddr)
|
||||||
l.Error(http.ListenAndServe(netPprofAddr, nil))
|
l.Error(http.ListenAndServe(netPprofAddr, nil))
|
||||||
|
@@ -31,7 +31,12 @@ func mainProcessSignalHandler(
|
|||||||
logger.Debugf(ctx, "interrupting '%s'", name)
|
logger.Debugf(ctx, "interrupting '%s'", name)
|
||||||
err := f.Process.Signal(os.Interrupt)
|
err := f.Process.Signal(os.Interrupt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf(ctx, "unable to send Interrupt to '%s': %v", name, err)
|
logger.Errorf(
|
||||||
|
ctx,
|
||||||
|
"unable to send Interrupt to '%s': %v",
|
||||||
|
name,
|
||||||
|
err,
|
||||||
|
)
|
||||||
logger.Debugf(ctx, "killing '%s'", name)
|
logger.Debugf(ctx, "killing '%s'", name)
|
||||||
f.Process.Kill()
|
f.Process.Kill()
|
||||||
return
|
return
|
||||||
|
@@ -115,7 +115,13 @@ func runStreamd(
|
|||||||
},
|
},
|
||||||
func(listenPort uint16, platID streamcontrol.PlatformName, authURL string) bool {
|
func(listenPort uint16, platID streamcontrol.PlatformName, authURL string) bool {
|
||||||
logger.Debugf(ctx, "streamd.UI.OpenOAuthURL(%d, %s, '%s')", listenPort, platID, authURL)
|
logger.Debugf(ctx, "streamd.UI.OpenOAuthURL(%d, %s, '%s')", listenPort, platID, authURL)
|
||||||
defer logger.Debugf(ctx, "/streamd.UI.OpenOAuthURL(%d, %s, '%s')", listenPort, platID, authURL)
|
defer logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"/streamd.UI.OpenOAuthURL(%d, %s, '%s')",
|
||||||
|
listenPort,
|
||||||
|
platID,
|
||||||
|
authURL,
|
||||||
|
)
|
||||||
|
|
||||||
streamdGRPCLocker.Lock()
|
streamdGRPCLocker.Lock()
|
||||||
logger.Debugf(ctx, "streamdGRPCLocker.Lock()-ed")
|
logger.Debugf(ctx, "streamdGRPCLocker.Lock()-ed")
|
||||||
|
@@ -210,7 +210,11 @@ func (m *Manager) handleConnection(
|
|||||||
m.unregisterConnection(ctx, sourceName)
|
m.unregisterConnection(ctx, sourceName)
|
||||||
}(regMessage.Source)
|
}(regMessage.Source)
|
||||||
if err := encoder.Encode(RegistrationResult{}); err != nil {
|
if err := encoder.Encode(RegistrationResult{}); err != nil {
|
||||||
err = fmt.Errorf("unable to encode&send the registration result to '%s': %w", regMessage.Source, err)
|
err = fmt.Errorf(
|
||||||
|
"unable to encode&send the registration result to '%s': %w",
|
||||||
|
regMessage.Source,
|
||||||
|
err,
|
||||||
|
)
|
||||||
logger.Error(ctx, err)
|
logger.Error(ctx, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -237,7 +241,13 @@ func (m *Manager) handleConnection(
|
|||||||
logger.Tracef(ctx, "waiting for a message from '%s'", regMessage.Source)
|
logger.Tracef(ctx, "waiting for a message from '%s'", regMessage.Source)
|
||||||
decoder := gob.NewDecoder(conn)
|
decoder := gob.NewDecoder(conn)
|
||||||
err := decoder.Decode(&message)
|
err := decoder.Decode(&message)
|
||||||
logger.Tracef(ctx, "getting a message from '%s': %#+v %#+v", regMessage.Source, message, err)
|
logger.Tracef(
|
||||||
|
ctx,
|
||||||
|
"getting a message from '%s': %#+v %#+v",
|
||||||
|
regMessage.Source,
|
||||||
|
message,
|
||||||
|
err,
|
||||||
|
)
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
logger.Tracef(ctx, "context was closed")
|
logger.Tracef(ctx, "context was closed")
|
||||||
@@ -305,7 +315,12 @@ func (m *Manager) processMessage(
|
|||||||
close(errCh)
|
close(errCh)
|
||||||
return err.ErrorOrNil()
|
return err.ErrorOrNil()
|
||||||
case ProcessNameMain:
|
case ProcessNameMain:
|
||||||
logger.Tracef(ctx, "got a message to the main process from '%s': %#+v", source, message.Content)
|
logger.Tracef(
|
||||||
|
ctx,
|
||||||
|
"got a message to the main process from '%s': %#+v",
|
||||||
|
source,
|
||||||
|
message.Content,
|
||||||
|
)
|
||||||
switch content := message.Content.(type) {
|
switch content := message.Content.(type) {
|
||||||
case MessageReady:
|
case MessageReady:
|
||||||
var result *multierror.Error
|
var result *multierror.Error
|
||||||
@@ -318,7 +333,13 @@ func (m *Manager) processMessage(
|
|||||||
return onReceivedMessage(ctx, source, message.Content)
|
return onReceivedMessage(ctx, source, message.Content)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
logger.Tracef(ctx, "got a message to '%s' from '%s': %#+v", message.Destination, source, message.Content)
|
logger.Tracef(
|
||||||
|
ctx,
|
||||||
|
"got a message to '%s' from '%s': %#+v",
|
||||||
|
message.Destination,
|
||||||
|
source,
|
||||||
|
message.Content,
|
||||||
|
)
|
||||||
return m.sendMessage(ctx, source, message.Destination, message.Content)
|
return m.sendMessage(ctx, source, message.Destination, message.Content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -349,7 +370,14 @@ func (m *Manager) sendMessage(
|
|||||||
) (_ret error) {
|
) (_ret error) {
|
||||||
logger.Tracef(ctx, "sending message %#+v from '%s' to '%s'", content, source, destination)
|
logger.Tracef(ctx, "sending message %#+v from '%s' to '%s'", content, source, destination)
|
||||||
defer func() {
|
defer func() {
|
||||||
logger.Tracef(ctx, "/sending message %#+v from '%s' to '%s': %v", content, source, destination, _ret)
|
logger.Tracef(
|
||||||
|
ctx,
|
||||||
|
"/sending message %#+v from '%s' to '%s': %v",
|
||||||
|
content,
|
||||||
|
source,
|
||||||
|
destination,
|
||||||
|
_ret,
|
||||||
|
)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if !m.isExpectedProcess(destination) {
|
if !m.isExpectedProcess(destination) {
|
||||||
@@ -359,7 +387,11 @@ func (m *Manager) sendMessage(
|
|||||||
observability.Go(ctx, func() {
|
observability.Go(ctx, func() {
|
||||||
conn, err := m.waitForReadyProcess(ctx, destination, reflect.TypeOf(content))
|
conn, err := m.waitForReadyProcess(ctx, destination, reflect.TypeOf(content))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf(ctx, "%v", fmt.Errorf("unable to wait for process '%s': %w", destination, err))
|
logger.Errorf(
|
||||||
|
ctx,
|
||||||
|
"%v",
|
||||||
|
fmt.Errorf("unable to wait for process '%s': %w", destination, err),
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,7 +404,9 @@ func (m *Manager) sendMessage(
|
|||||||
|
|
||||||
h := m.connLocker.Lock(context.Background(), destination)
|
h := m.connLocker.Lock(context.Background(), destination)
|
||||||
defer h.Unlock()
|
defer h.Unlock()
|
||||||
defer time.Sleep(100 * time.Millisecond) // TODO: Delete this horrible hack (that is introduced to avoid erasing messages in the buffer)
|
defer time.Sleep(
|
||||||
|
100 * time.Millisecond,
|
||||||
|
) // TODO: Delete this horrible hack (that is introduced to avoid erasing messages in the buffer)
|
||||||
err = gob.NewEncoder(conn).Encode(message)
|
err = gob.NewEncoder(conn).Encode(message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf(ctx, "%v", fmt.Errorf("unable to encode&send message: %w", err))
|
logger.Errorf(ctx, "%v", fmt.Errorf("unable to encode&send message: %w", err))
|
||||||
@@ -419,7 +453,10 @@ func (m *Manager) waitForReadyProcess(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
conn, ch, isReady := xsync.DoR3(ctx, &m.connsLocker, func() (net.Conn, chan struct{}, bool) {
|
conn, ch, isReady := xsync.DoR3(
|
||||||
|
ctx,
|
||||||
|
&m.connsLocker,
|
||||||
|
func() (net.Conn, chan struct{}, bool) {
|
||||||
readyMap := m.childReadyFor[name]
|
readyMap := m.childReadyFor[name]
|
||||||
isReady := false
|
isReady := false
|
||||||
if readyMap != nil {
|
if readyMap != nil {
|
||||||
@@ -428,14 +465,25 @@ func (m *Manager) waitForReadyProcess(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return m.conns[name], m.connsChanged, isReady
|
return m.conns[name], m.connsChanged, isReady
|
||||||
})
|
},
|
||||||
|
)
|
||||||
|
|
||||||
if conn != nil && isReady {
|
if conn != nil && isReady {
|
||||||
logger.Debugf(ctx, "waitForReadyProcess(ctx, '%s', '%s'): waiting is complete", name, msgType)
|
logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"waitForReadyProcess(ctx, '%s', '%s'): waiting is complete",
|
||||||
|
name,
|
||||||
|
msgType,
|
||||||
|
)
|
||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Debugf(ctx, "waitForReadyProcess(ctx, '%s', '%s'): waiting for a change in connections", name, msgType)
|
logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"waitForReadyProcess(ctx, '%s', '%s'): waiting for a change in connections",
|
||||||
|
name,
|
||||||
|
msgType,
|
||||||
|
)
|
||||||
<-ch
|
<-ch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -452,7 +500,12 @@ func (m *Manager) registerConnection(
|
|||||||
conn net.Conn,
|
conn net.Conn,
|
||||||
) error {
|
) error {
|
||||||
logger.Debugf(ctx, "registerConnection(ctx, '%s', %s)", sourceName, conn.RemoteAddr().String())
|
logger.Debugf(ctx, "registerConnection(ctx, '%s', %s)", sourceName, conn.RemoteAddr().String())
|
||||||
defer logger.Debugf(ctx, "/registerConnection(ctx, '%s', %s)", sourceName, conn.RemoteAddr().String())
|
defer logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"/registerConnection(ctx, '%s', %s)",
|
||||||
|
sourceName,
|
||||||
|
conn.RemoteAddr().String(),
|
||||||
|
)
|
||||||
if !m.isExpectedProcess(sourceName) {
|
if !m.isExpectedProcess(sourceName) {
|
||||||
return fmt.Errorf("process '%s' is not ever expected", sourceName)
|
return fmt.Errorf("process '%s' is not ever expected", sourceName)
|
||||||
}
|
}
|
||||||
@@ -550,7 +603,9 @@ func (m *Manager) SendMessagePreReady(
|
|||||||
}
|
}
|
||||||
h := m.connLocker.Lock(context.Background(), dst)
|
h := m.connLocker.Lock(context.Background(), dst)
|
||||||
defer h.Unlock()
|
defer h.Unlock()
|
||||||
defer time.Sleep(100 * time.Millisecond) // TODO: Delete this horrible hack (that is introduced to avoid erasing messages in the buffer)
|
defer time.Sleep(
|
||||||
|
100 * time.Millisecond,
|
||||||
|
) // TODO: Delete this horrible hack (that is introduced to avoid erasing messages in the buffer)
|
||||||
err = encoder.Encode(msg)
|
err = encoder.Encode(msg)
|
||||||
logger.Tracef(ctx, "sending message %#+v: %v", msg, err)
|
logger.Tracef(ctx, "sending message %#+v: %v", msg, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -66,19 +66,25 @@ func Test(t *testing.T) {
|
|||||||
c0, err := NewClient("child0", m.Addr().String(), m.Password())
|
c0, err := NewClient("child0", m.Addr().String(), m.Password())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer c0.Close()
|
defer c0.Close()
|
||||||
go c0.Serve(belt.WithField(ctx, "process", "child0"), func(ctx context.Context, source ProcessName, content any) error {
|
go c0.Serve(
|
||||||
|
belt.WithField(ctx, "process", "child0"),
|
||||||
|
func(ctx context.Context, source ProcessName, content any) error {
|
||||||
handleCall("child0", content)
|
handleCall("child0", content)
|
||||||
return nil
|
return nil
|
||||||
})
|
},
|
||||||
|
)
|
||||||
c0.SendMessage(ctx, "main", MessageReady{})
|
c0.SendMessage(ctx, "main", MessageReady{})
|
||||||
|
|
||||||
c1, err := NewClient("child1", m.Addr().String(), m.Password())
|
c1, err := NewClient("child1", m.Addr().String(), m.Password())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer c1.Close()
|
defer c1.Close()
|
||||||
go c1.Serve(belt.WithField(ctx, "process", "child1"), func(ctx context.Context, source ProcessName, content any) error {
|
go c1.Serve(
|
||||||
|
belt.WithField(ctx, "process", "child1"),
|
||||||
|
func(ctx context.Context, source ProcessName, content any) error {
|
||||||
handleCall("child1", content)
|
handleCall("child1", content)
|
||||||
return nil
|
return nil
|
||||||
})
|
},
|
||||||
|
)
|
||||||
c1.SendMessage(ctx, "main", MessageReady{})
|
c1.SendMessage(ctx, "main", MessageReady{})
|
||||||
|
|
||||||
_, err = NewClient("child2", m.Addr().String(), m.Password())
|
_, err = NewClient("child2", m.Addr().String(), m.Password())
|
||||||
|
@@ -49,7 +49,10 @@ func OAuth2HandlerViaBrowser(ctx context.Context, arg OAuthHandlerArgument) erro
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Your browser has been launched (URL: %s).\nPlease approve the permissions.\n", arg.AuthURL)
|
fmt.Printf(
|
||||||
|
"Your browser has been launched (URL: %s).\nPlease approve the permissions.\n",
|
||||||
|
arg.AuthURL,
|
||||||
|
)
|
||||||
|
|
||||||
// Wait for the web server to get the code.
|
// Wait for the web server to get the code.
|
||||||
code := <-codeCh
|
code := <-codeCh
|
||||||
@@ -75,7 +78,11 @@ func NewCodeReceiver(
|
|||||||
fmt.Fprintf(w, "No code received :(\r\n\r\nYou can close this browser window.")
|
fmt.Fprintf(w, "No code received :(\r\n\r\nYou can close this browser window.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Fprintf(w, "Received code: %v\r\n\r\nYou can now safely close this browser window.", code)
|
fmt.Fprintf(
|
||||||
|
w,
|
||||||
|
"Received code: %v\r\n\r\nYou can now safely close this browser window.",
|
||||||
|
code,
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -75,7 +75,11 @@ func NewErrorMonitorLoggerHook(
|
|||||||
|
|
||||||
var _ loggertypes.PreHook = (*ErrorMonitorLoggerHook)(nil)
|
var _ loggertypes.PreHook = (*ErrorMonitorLoggerHook)(nil)
|
||||||
|
|
||||||
func (h *ErrorMonitorLoggerHook) ProcessInput(traceIDs belt.TraceIDs, level loggertypes.Level, args ...any) loggertypes.PreHookResult {
|
func (h *ErrorMonitorLoggerHook) ProcessInput(
|
||||||
|
traceIDs belt.TraceIDs,
|
||||||
|
level loggertypes.Level,
|
||||||
|
args ...any,
|
||||||
|
) loggertypes.PreHookResult {
|
||||||
if level > loggertypes.LevelWarning {
|
if level > loggertypes.LevelWarning {
|
||||||
return loggertypes.PreHookResult{
|
return loggertypes.PreHookResult{
|
||||||
Skip: false,
|
Skip: false,
|
||||||
@@ -93,7 +97,12 @@ func (h *ErrorMonitorLoggerHook) ProcessInput(traceIDs belt.TraceIDs, level logg
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *ErrorMonitorLoggerHook) ProcessInputf(traceIDs belt.TraceIDs, level loggertypes.Level, format string, args ...any) loggertypes.PreHookResult {
|
func (h *ErrorMonitorLoggerHook) ProcessInputf(
|
||||||
|
traceIDs belt.TraceIDs,
|
||||||
|
level loggertypes.Level,
|
||||||
|
format string,
|
||||||
|
args ...any,
|
||||||
|
) loggertypes.PreHookResult {
|
||||||
if level > loggertypes.LevelWarning {
|
if level > loggertypes.LevelWarning {
|
||||||
return loggertypes.PreHookResult{
|
return loggertypes.PreHookResult{
|
||||||
Skip: false,
|
Skip: false,
|
||||||
@@ -110,7 +119,13 @@ func (h *ErrorMonitorLoggerHook) ProcessInputf(traceIDs belt.TraceIDs, level log
|
|||||||
Skip: false,
|
Skip: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func (h *ErrorMonitorLoggerHook) ProcessInputFields(traceIDs belt.TraceIDs, level loggertypes.Level, message string, fields field.AbstractFields) loggertypes.PreHookResult {
|
|
||||||
|
func (h *ErrorMonitorLoggerHook) ProcessInputFields(
|
||||||
|
traceIDs belt.TraceIDs,
|
||||||
|
level loggertypes.Level,
|
||||||
|
message string,
|
||||||
|
fields field.AbstractFields,
|
||||||
|
) loggertypes.PreHookResult {
|
||||||
if level > loggertypes.LevelWarning {
|
if level > loggertypes.LevelWarning {
|
||||||
return loggertypes.PreHookResult{
|
return loggertypes.PreHookResult{
|
||||||
Skip: false,
|
Skip: false,
|
||||||
|
@@ -41,7 +41,11 @@ func main() {
|
|||||||
loggerLevel := logger.LevelInfo
|
loggerLevel := logger.LevelInfo
|
||||||
pflag.Var(&loggerLevel, "log-level", "Log level")
|
pflag.Var(&loggerLevel, "log-level", "Log level")
|
||||||
mpvPath := pflag.String("mpv", "mpv", "path to mpv")
|
mpvPath := pflag.String("mpv", "mpv", "path to mpv")
|
||||||
backend := pflag.String("backend", backends[0], "player backend, supported values: "+strings.Join(backends, ", "))
|
backend := pflag.String(
|
||||||
|
"backend",
|
||||||
|
backends[0],
|
||||||
|
"player backend, supported values: "+strings.Join(backends, ", "),
|
||||||
|
)
|
||||||
pflag.Parse()
|
pflag.Parse()
|
||||||
|
|
||||||
l := logrus.Default().WithLevel(loggerLevel)
|
l := logrus.Default().WithLevel(loggerLevel)
|
||||||
|
@@ -119,7 +119,18 @@ func (p *MPV) execMPV(ctx context.Context) (_err error) {
|
|||||||
err := os.Remove(socketPath)
|
err := os.Remove(socketPath)
|
||||||
logger.Debugf(ctx, "socket deletion result: '%s': %v", socketPath, err)
|
logger.Debugf(ctx, "socket deletion result: '%s': %v", socketPath, err)
|
||||||
|
|
||||||
args := []string{p.PathToMPV, "--idle", "--keep-open=always", "--keep-open-pause=no", "--no-hidpi-window-scale", "--no-osc", "--no-osd-bar", "--window-scale=1", "--input-ipc-server=" + socketPath, fmt.Sprintf("--title=%s", p.Title)}
|
args := []string{
|
||||||
|
p.PathToMPV,
|
||||||
|
"--idle",
|
||||||
|
"--keep-open=always",
|
||||||
|
"--keep-open-pause=no",
|
||||||
|
"--no-hidpi-window-scale",
|
||||||
|
"--no-osc",
|
||||||
|
"--no-osd-bar",
|
||||||
|
"--window-scale=1",
|
||||||
|
"--input-ipc-server=" + socketPath,
|
||||||
|
fmt.Sprintf("--title=%s", p.Title),
|
||||||
|
}
|
||||||
switch observability.LogLevelFilter.GetLevel() {
|
switch observability.LogLevelFilter.GetLevel() {
|
||||||
case logger.LevelPanic, logger.LevelFatal:
|
case logger.LevelPanic, logger.LevelFatal:
|
||||||
args = append(args, "--msg-level=all=no")
|
args = append(args, "--msg-level=all=no")
|
||||||
@@ -135,8 +146,18 @@ func (p *MPV) execMPV(ctx context.Context) (_err error) {
|
|||||||
logger.Debugf(ctx, "running command '%s %s'", args[0], strings.Join(args[1:], " "))
|
logger.Debugf(ctx, "running command '%s %s'", args[0], strings.Join(args[1:], " "))
|
||||||
cmd := exec.Command(args[0], args[1:]...)
|
cmd := exec.Command(args[0], args[1:]...)
|
||||||
|
|
||||||
cmd.Stdout = logwriter.NewLogWriter(ctx, logger.FromCtx(ctx).WithField("log_writer_target", "mpv").WithField("output_type", "stdout"))
|
cmd.Stdout = logwriter.NewLogWriter(
|
||||||
cmd.Stderr = logwriter.NewLogWriter(ctx, logger.FromCtx(ctx).WithField("log_writer_target", "mpv").WithField("output_type", "stderr"))
|
ctx,
|
||||||
|
logger.FromCtx(ctx).
|
||||||
|
WithField("log_writer_target", "mpv").
|
||||||
|
WithField("output_type", "stdout"),
|
||||||
|
)
|
||||||
|
cmd.Stderr = logwriter.NewLogWriter(
|
||||||
|
ctx,
|
||||||
|
logger.FromCtx(ctx).
|
||||||
|
WithField("log_writer_target", "mpv").
|
||||||
|
WithField("output_type", "stderr"),
|
||||||
|
)
|
||||||
err = child_process_manager.ConfigureCommand(cmd)
|
err = child_process_manager.ConfigureCommand(cmd)
|
||||||
errmon.ObserveErrorCtx(ctx, err)
|
errmon.ObserveErrorCtx(ctx, err)
|
||||||
err = cmd.Start()
|
err = cmd.Start()
|
||||||
|
@@ -34,7 +34,12 @@ func (r *Recoder) StartRecoding(
|
|||||||
return xsync.DoR1(ctx, &r.Locker, func() error {
|
return xsync.DoR1(ctx, &r.Locker, func() error {
|
||||||
relay := rtmprelay.NewRtmpRelay(&input.URL, &output.URL)
|
relay := rtmprelay.NewRtmpRelay(&input.URL, &output.URL)
|
||||||
if err := relay.Start(); err != nil {
|
if err := relay.Start(); err != nil {
|
||||||
return fmt.Errorf("unable to start RTMP relay from '%s' to '%s': %w", input.URL, output.URL, err)
|
return fmt.Errorf(
|
||||||
|
"unable to start RTMP relay from '%s' to '%s': %w",
|
||||||
|
input.URL,
|
||||||
|
output.URL,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
r.Relay = relay
|
r.Relay = relay
|
||||||
|
@@ -55,7 +55,11 @@ func NewOutputFromURL(
|
|||||||
Closer: astikit.NewCloser(),
|
Closer: astikit.NewCloser(),
|
||||||
}
|
}
|
||||||
|
|
||||||
formatContext, err := astiav.AllocOutputFormatContext(nil, formatFromScheme(url.Scheme), url.String())
|
formatContext, err := astiav.AllocOutputFormatContext(
|
||||||
|
nil,
|
||||||
|
formatFromScheme(url.Scheme),
|
||||||
|
url.String(),
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("allocating output format context failed: %w", err)
|
return nil, fmt.Errorf("allocating output format context failed: %w", err)
|
||||||
}
|
}
|
||||||
@@ -69,7 +73,10 @@ func NewOutputFromURL(
|
|||||||
// if output is a file:
|
// if output is a file:
|
||||||
if !output.FormatContext.OutputFormat().Flags().Has(astiav.IOFormatFlagNofile) {
|
if !output.FormatContext.OutputFormat().Flags().Has(astiav.IOFormatFlagNofile) {
|
||||||
logger.Tracef(ctx, "destination '%s' is a file", url.String())
|
logger.Tracef(ctx, "destination '%s' is a file", url.String())
|
||||||
ioContext, err := astiav.OpenIOContext(url.String(), astiav.NewIOContextFlags(astiav.IOContextFlagWrite))
|
ioContext, err := astiav.OpenIOContext(
|
||||||
|
url.String(),
|
||||||
|
astiav.NewIOContextFlags(astiav.IOContextFlagWrite),
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(fmt.Errorf("main: opening io context failed: %w", err))
|
log.Fatal(fmt.Errorf("main: opening io context failed: %w", err))
|
||||||
}
|
}
|
||||||
|
@@ -35,7 +35,9 @@ func New(
|
|||||||
WaiterChan: make(chan struct{}),
|
WaiterChan: make(chan struct{}),
|
||||||
RecoderConfig: cfg,
|
RecoderConfig: cfg,
|
||||||
}
|
}
|
||||||
close(result.WaiterChan) // to prevent Wait() from blocking when the process is not started, yet.
|
close(
|
||||||
|
result.WaiterChan,
|
||||||
|
) // to prevent Wait() from blocking when the process is not started, yet.
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -109,7 +109,11 @@ func (srv *GRPCServer) newInputByURL(
|
|||||||
config := recoder.InputConfig{}
|
config := recoder.InputConfig{}
|
||||||
input, err := recoder.NewInputFromURL(ctx, path.Url.Url, path.Url.AuthKey, config)
|
input, err := recoder.NewInputFromURL(ctx, path.Url.Url, path.Url.AuthKey, config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to initialize an input using URL '%s' and config %#+v", path.Url, config)
|
return nil, fmt.Errorf(
|
||||||
|
"unable to initialize an input using URL '%s' and config %#+v",
|
||||||
|
path.Url,
|
||||||
|
config,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
inputID := xsync.DoR1(ctx, &srv.InputLocker, func() InputID {
|
inputID := xsync.DoR1(ctx, &srv.InputLocker, func() InputID {
|
||||||
@@ -163,7 +167,11 @@ func (srv *GRPCServer) newOutputByURL(
|
|||||||
config := recoder.OutputConfig{}
|
config := recoder.OutputConfig{}
|
||||||
output, err := recoder.NewOutputFromURL(ctx, path.Url.Url, path.Url.AuthKey, config)
|
output, err := recoder.NewOutputFromURL(ctx, path.Url.Url, path.Url.AuthKey, config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to initialize an output using URL '%s' and config %#+v", path.Url, config)
|
return nil, fmt.Errorf(
|
||||||
|
"unable to initialize an output using URL '%s' and config %#+v",
|
||||||
|
path.Url,
|
||||||
|
config,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
outputID := xsync.DoR1(ctx, &srv.OutputLocker, func() OutputID {
|
outputID := xsync.DoR1(ctx, &srv.OutputLocker, func() OutputID {
|
||||||
|
@@ -41,7 +41,11 @@ func (r *Recoder) NewInputFromPublisher(
|
|||||||
) (recoder.Input, error) {
|
) (recoder.Input, error) {
|
||||||
publisher, ok := publisherIface.(*xaionarogortmp.Pubsub)
|
publisher, ok := publisherIface.(*xaionarogortmp.Pubsub)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("expected a publisher or type %T, but received %T", publisherIface, publisher)
|
return nil, fmt.Errorf(
|
||||||
|
"expected a publisher or type %T, but received %T",
|
||||||
|
publisherIface,
|
||||||
|
publisher,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Input{
|
return &Input{
|
||||||
@@ -85,7 +89,11 @@ func (r *Recoder) NewInputFromURL(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("got an error on command 'Connect' to the input endpoint '%s': %w", urlString, err)
|
return nil, fmt.Errorf(
|
||||||
|
"got an error on command 'Connect' to the input endpoint '%s': %w",
|
||||||
|
urlString,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("not implemented, yet")
|
return nil, fmt.Errorf("not implemented, yet")
|
||||||
|
@@ -49,7 +49,12 @@ func newRTMPClient(
|
|||||||
dialFunc = rtmp.Dial
|
dialFunc = rtmp.Dial
|
||||||
case "rtmps":
|
case "rtmps":
|
||||||
dialFunc = func(protocol, addr string, config *rtmp.ConnConfig) (*rtmp.ClientConn, error) {
|
dialFunc = func(protocol, addr string, config *rtmp.ConnConfig) (*rtmp.ClientConn, error) {
|
||||||
return rtmp.TLSDial(protocol, addr, config, http.DefaultTransport.(*http.Transport).TLSClientConfig)
|
return rtmp.TLSDial(
|
||||||
|
protocol,
|
||||||
|
addr,
|
||||||
|
config,
|
||||||
|
http.DefaultTransport.(*http.Transport).TLSClientConfig,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unexpected scheme '%s' in URL '%s'", url.Scheme, url.String())
|
return nil, fmt.Errorf("unexpected scheme '%s' in URL '%s'", url.Scheme, url.String())
|
||||||
|
@@ -73,7 +73,11 @@ func (r *Recoder) StartRecoding(
|
|||||||
return fmt.Errorf("recoding is already running")
|
return fmt.Errorf("recoding is already running")
|
||||||
}
|
}
|
||||||
|
|
||||||
stream, err := output.Client.CreateStream(ctx, &rtmpmsg.NetConnectionCreateStream{}, chunkSize)
|
stream, err := output.Client.CreateStream(
|
||||||
|
ctx,
|
||||||
|
&rtmpmsg.NetConnectionCreateStream{},
|
||||||
|
chunkSize,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to create a stream on the remote side: %w", err)
|
return fmt.Errorf("unable to create a stream on the remote side: %w", err)
|
||||||
}
|
}
|
||||||
|
@@ -176,7 +176,8 @@ func (g *GIT) push(
|
|||||||
if err == git.NoErrAlreadyUpToDate {
|
if err == git.NoErrAlreadyUpToDate {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err != nil && strings.Contains(err.Error(), "is at") && strings.Contains(err.Error(), "but expected") {
|
if err != nil && strings.Contains(err.Error(), "is at") &&
|
||||||
|
strings.Contains(err.Error(), "but expected") {
|
||||||
return ErrNeedsRebase{Err: err}
|
return ErrNeedsRebase{Err: err}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -200,7 +201,11 @@ func (g *GIT) Read() ([]byte, error) {
|
|||||||
b, err := io.ReadAll(f)
|
b, err := io.ReadAll(f)
|
||||||
f.Close()
|
f.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to read the content of file '%s' from the virtual git repository: %w", g.FilePath, err)
|
return nil, fmt.Errorf(
|
||||||
|
"unable to read the content of file '%s' from the virtual git repository: %w",
|
||||||
|
g.FilePath,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return b, nil
|
return b, nil
|
||||||
@@ -271,11 +276,21 @@ func (g *GIT) Pull(
|
|||||||
logger.Debugf(ctx, "git is already in sync: %s == %s", lastKnownCommitHash, newCommitHash)
|
logger.Debugf(ctx, "git is already in sync: %s == %s", lastKnownCommitHash, newCommitHash)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
logger.Debugf(ctx, "got a different commit from git: %s != %s", lastKnownCommitHash, newCommitHash)
|
logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"got a different commit from git: %s != %s",
|
||||||
|
lastKnownCommitHash,
|
||||||
|
newCommitHash,
|
||||||
|
)
|
||||||
|
|
||||||
oldCommit, _ := g.Repo.CommitObject(newCommitHash)
|
oldCommit, _ := g.Repo.CommitObject(newCommitHash)
|
||||||
if oldCommit != nil {
|
if oldCommit != nil {
|
||||||
logger.Debugf(ctx, "we already have this commit in the history on our side, skipping it: %s: %#+v", newCommitHash, oldCommit)
|
logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"we already have this commit in the history on our side, skipping it: %s: %#+v",
|
||||||
|
newCommitHash,
|
||||||
|
oldCommit,
|
||||||
|
)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,7 +316,11 @@ func (g *GIT) CommitAndPush(
|
|||||||
|
|
||||||
_, err := worktree.Add(g.FilePath)
|
_, err := worktree.Add(g.FilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return plumbing.Hash{}, fmt.Errorf("unable to add file '%s' to the git's worktree: %w", g.FilePath, err)
|
return plumbing.Hash{}, fmt.Errorf(
|
||||||
|
"unable to add file '%s' to the git's worktree: %w",
|
||||||
|
g.FilePath,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
@@ -312,12 +331,15 @@ func (g *GIT) CommitAndPush(
|
|||||||
return plumbing.Hash{}, fmt.Errorf("unable to determine the host name: %w", err)
|
return plumbing.Hash{}, fmt.Errorf("unable to determine the host name: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
hash, err := worktree.Commit(fmt.Sprintf("Update from '%s' at %s", host, ts), &git.CommitOptions{
|
hash, err := worktree.Commit(
|
||||||
|
fmt.Sprintf("Update from '%s' at %s", host, ts),
|
||||||
|
&git.CommitOptions{
|
||||||
All: true,
|
All: true,
|
||||||
AllowEmptyCommits: false,
|
AllowEmptyCommits: false,
|
||||||
Author: signature,
|
Author: signature,
|
||||||
Committer: signature,
|
Committer: signature,
|
||||||
})
|
},
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return hash, fmt.Errorf("unable to commit the new config to the git repo: %w", err)
|
return hash, fmt.Errorf("unable to commit the new config to the git repo: %w", err)
|
||||||
}
|
}
|
||||||
@@ -387,7 +409,11 @@ func (g *GIT) Write(
|
|||||||
)
|
)
|
||||||
logger.Debugf(ctx, "file open result: %v", err)
|
logger.Debugf(ctx, "file open result: %v", err)
|
||||||
if err != nil && !os.IsNotExist(err) {
|
if err != nil && !os.IsNotExist(err) {
|
||||||
return plumbing.Hash{}, fmt.Errorf("unable to open file '%s' for reading: %w", g.FilePath, err)
|
return plumbing.Hash{}, fmt.Errorf(
|
||||||
|
"unable to open file '%s' for reading: %w",
|
||||||
|
g.FilePath,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var sha1SumBefore [sha1.Size]byte
|
var sha1SumBefore [sha1.Size]byte
|
||||||
@@ -420,12 +446,19 @@ func (g *GIT) Write(
|
|||||||
0644,
|
0644,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return plumbing.Hash{}, fmt.Errorf("unable to open file '%s' for writing: %w", g.FilePath, err)
|
return plumbing.Hash{}, fmt.Errorf(
|
||||||
|
"unable to open file '%s' for writing: %w",
|
||||||
|
g.FilePath,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
_, err = io.Copy(f, bytes.NewReader(b))
|
_, err = io.Copy(f, bytes.NewReader(b))
|
||||||
f.Close()
|
f.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return plumbing.Hash{}, fmt.Errorf("unable to write the config into virtual git repo: %w", err)
|
return plumbing.Hash{}, fmt.Errorf(
|
||||||
|
"unable to write the config into virtual git repo: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
hash, err := g.CommitAndPush(ctx, worktree, ref)
|
hash, err := g.CommitAndPush(ctx, worktree, ref)
|
||||||
|
@@ -10,7 +10,11 @@ import (
|
|||||||
func Screenshot(cfg Config) (*image.RGBA, error) {
|
func Screenshot(cfg Config) (*image.RGBA, error) {
|
||||||
activeDisplays := screenshot.NumActiveDisplays()
|
activeDisplays := screenshot.NumActiveDisplays()
|
||||||
if cfg.DisplayID >= uint(activeDisplays) {
|
if cfg.DisplayID >= uint(activeDisplays) {
|
||||||
return nil, fmt.Errorf("display ID %d is out of range (max: %d)", cfg.DisplayID, activeDisplays-1)
|
return nil, fmt.Errorf(
|
||||||
|
"display ID %d is out of range (max: %d)",
|
||||||
|
cfg.DisplayID,
|
||||||
|
activeDisplays-1,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
rgbaFull, err := screenshot.CaptureDisplay(int(cfg.DisplayID))
|
rgbaFull, err := screenshot.CaptureDisplay(int(cfg.DisplayID))
|
||||||
|
@@ -130,10 +130,14 @@ var _ yaml.BytesUnmarshaler = (*RawMessage)(nil)
|
|||||||
var _ yaml.BytesMarshaler = (*RawMessage)(nil)
|
var _ yaml.BytesMarshaler = (*RawMessage)(nil)
|
||||||
|
|
||||||
func (RawMessage) GetParent() (ProfileName, bool) {
|
func (RawMessage) GetParent() (ProfileName, bool) {
|
||||||
panic("the value is not parsed; don't use the platform config directly, and use function GetPlatformConfig instead")
|
panic(
|
||||||
|
"the value is not parsed; don't use the platform config directly, and use function GetPlatformConfig instead",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
func (RawMessage) GetOrder() int {
|
func (RawMessage) GetOrder() int {
|
||||||
panic("the value is not parsed; don't use the platform config directly, and use function GetPlatformConfig instead")
|
panic(
|
||||||
|
"the value is not parsed; don't use the platform config directly, and use function GetPlatformConfig instead",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *RawMessage) UnmarshalJSON(b []byte) error {
|
func (m *RawMessage) UnmarshalJSON(b []byte) error {
|
||||||
@@ -192,7 +196,11 @@ func (cfg *Config) UnmarshalYAML(b []byte) (_err error) {
|
|||||||
t := map[PlatformName]*unparsedPlatformConfig{}
|
t := map[PlatformName]*unparsedPlatformConfig{}
|
||||||
err := yaml.Unmarshal(b, &t)
|
err := yaml.Unmarshal(b, &t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to unmarshal YAML of the root of the config: %w; config: <%s>", err, b)
|
return fmt.Errorf(
|
||||||
|
"unable to unmarshal YAML of the root of the config: %w; config: <%s>",
|
||||||
|
err,
|
||||||
|
b,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if *cfg == nil {
|
if *cfg == nil {
|
||||||
@@ -316,7 +324,9 @@ func GetPlatformSpecificConfig[T any](
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetStreamProfiles[S StreamProfile](streamProfiles map[ProfileName]AbstractStreamProfile) StreamProfiles[S] {
|
func GetStreamProfiles[S StreamProfile](
|
||||||
|
streamProfiles map[ProfileName]AbstractStreamProfile,
|
||||||
|
) StreamProfiles[S] {
|
||||||
s := make(map[ProfileName]S, len(streamProfiles))
|
s := make(map[ProfileName]S, len(streamProfiles))
|
||||||
for k, p := range streamProfiles {
|
for k, p := range streamProfiles {
|
||||||
switch p := p.(type) {
|
switch p := p.(type) {
|
||||||
@@ -345,7 +355,9 @@ func InitConfig[T any, S StreamProfile](cfg Config, id PlatformName, platCfg Pla
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToAbstractStreamProfiles[S StreamProfile](in map[ProfileName]S) map[ProfileName]AbstractStreamProfile {
|
func ToAbstractStreamProfiles[S StreamProfile](
|
||||||
|
in map[ProfileName]S,
|
||||||
|
) map[ProfileName]AbstractStreamProfile {
|
||||||
m := make(map[ProfileName]AbstractStreamProfile, len(in))
|
m := make(map[ProfileName]AbstractStreamProfile, len(in))
|
||||||
for k, v := range in {
|
for k, v := range in {
|
||||||
m[k] = v
|
m[k] = v
|
||||||
|
@@ -10,7 +10,11 @@ type ErrInvalidStreamProfileType struct {
|
|||||||
var _ error = ErrInvalidStreamProfileType{}
|
var _ error = ErrInvalidStreamProfileType{}
|
||||||
|
|
||||||
func (e ErrInvalidStreamProfileType) Error() string {
|
func (e ErrInvalidStreamProfileType) Error() string {
|
||||||
return fmt.Sprintf("received an invalid stream profile type: expected:%T, received:%T", e.Expected, e.Received)
|
return fmt.Sprintf(
|
||||||
|
"received an invalid stream profile type: expected:%T, received:%T",
|
||||||
|
e.Expected,
|
||||||
|
e.Received,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ErrNoStreamControllerForProfile struct {
|
type ErrNoStreamControllerForProfile struct {
|
||||||
|
@@ -213,7 +213,9 @@ func (obs *OBS) GetStreamStatus(
|
|||||||
|
|
||||||
var startedAt *time.Time
|
var startedAt *time.Time
|
||||||
if streamStatus.OutputActive {
|
if streamStatus.OutputActive {
|
||||||
startedAt = ptr(time.Now().Add(-time.Duration(streamStatus.OutputDuration * float64(time.Millisecond))))
|
startedAt = ptr(
|
||||||
|
time.Now().Add(-time.Duration(streamStatus.OutputDuration * float64(time.Millisecond))),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &streamcontrol.StreamStatus{
|
return &streamcontrol.StreamStatus{
|
||||||
|
@@ -71,26 +71,44 @@ func (sr SceneRule) MarshalYAML() (b []byte, _err error) {
|
|||||||
|
|
||||||
triggerBytes, err := yaml.Marshal(sr.TriggerQuery)
|
triggerBytes, err := yaml.Marshal(sr.TriggerQuery)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to serialize the trigger %T:%#+v: %w", sr.TriggerQuery, sr.TriggerQuery, err)
|
return nil, fmt.Errorf(
|
||||||
|
"unable to serialize the trigger %T:%#+v: %w",
|
||||||
|
sr.TriggerQuery,
|
||||||
|
sr.TriggerQuery,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerMap := map[string]any{}
|
triggerMap := map[string]any{}
|
||||||
err = yaml.Unmarshal(triggerBytes, &triggerMap)
|
err = yaml.Unmarshal(triggerBytes, &triggerMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to unserialize the trigger '%s' into a map: %w", triggerBytes, err)
|
return nil, fmt.Errorf(
|
||||||
|
"unable to unserialize the trigger '%s' into a map: %w",
|
||||||
|
triggerBytes,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerMap["type"] = registry.ToTypeName(sr.TriggerQuery)
|
triggerMap["type"] = registry.ToTypeName(sr.TriggerQuery)
|
||||||
|
|
||||||
actionBytes, err := yaml.Marshal(sr.Action)
|
actionBytes, err := yaml.Marshal(sr.Action)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to serialize the action %T:%#+v: %w", sr.Action, sr.Action, err)
|
return nil, fmt.Errorf(
|
||||||
|
"unable to serialize the action %T:%#+v: %w",
|
||||||
|
sr.Action,
|
||||||
|
sr.Action,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
actionMap := map[string]any{}
|
actionMap := map[string]any{}
|
||||||
err = yaml.Unmarshal(actionBytes, &actionMap)
|
err = yaml.Unmarshal(actionBytes, &actionMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to unserialize the action '%s' into a map: %w", actionBytes, err)
|
return nil, fmt.Errorf(
|
||||||
|
"unable to unserialize the action '%s' into a map: %w",
|
||||||
|
actionBytes,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
actionMap["type"] = registry.ToTypeName(sr.Action)
|
actionMap["type"] = registry.ToTypeName(sr.Action)
|
||||||
|
@@ -120,7 +120,13 @@ type StreamController[ProfileType StreamProfile] interface {
|
|||||||
StreamControllerCommons
|
StreamControllerCommons
|
||||||
|
|
||||||
ApplyProfile(ctx context.Context, profile ProfileType, customArgs ...any) error
|
ApplyProfile(ctx context.Context, profile ProfileType, customArgs ...any) error
|
||||||
StartStream(ctx context.Context, title string, description string, profile ProfileType, customArgs ...any) error
|
StartStream(
|
||||||
|
ctx context.Context,
|
||||||
|
title string,
|
||||||
|
description string,
|
||||||
|
profile ProfileType,
|
||||||
|
customArgs ...any,
|
||||||
|
) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type AbstractStreamController interface {
|
type AbstractStreamController interface {
|
||||||
|
@@ -42,7 +42,9 @@ func New(
|
|||||||
return nil, fmt.Errorf("'channel' is not set")
|
return nil, fmt.Errorf("'channel' is not set")
|
||||||
}
|
}
|
||||||
if cfg.Config.ClientID == "" || cfg.Config.ClientSecret == "" {
|
if cfg.Config.ClientID == "" || cfg.Config.ClientSecret == "" {
|
||||||
return nil, fmt.Errorf("'clientid' or/and 'clientsecret' is/are not set; go to https://dev.twitch.tv/console/apps/create and create an app if it not created, yet")
|
return nil, fmt.Errorf(
|
||||||
|
"'clientid' or/and 'clientsecret' is/are not set; go to https://dev.twitch.tv/console/apps/create and create an app if it not created, yet",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
getPortsFn := cfg.Config.GetOAuthListenPorts
|
getPortsFn := cfg.Config.GetOAuthListenPorts
|
||||||
@@ -92,7 +94,10 @@ func New(
|
|||||||
errmon.ObserveErrorCtx(ctx, err)
|
errmon.ObserveErrorCtx(ctx, err)
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
if now.Sub(prevTokenUpdate) < time.Second*30 {
|
if now.Sub(prevTokenUpdate) < time.Second*30 {
|
||||||
logger.Errorf(ctx, "updating the token too often, most likely it won't help, so asking to re-authenticate")
|
logger.Errorf(
|
||||||
|
ctx,
|
||||||
|
"updating the token too often, most likely it won't help, so asking to re-authenticate",
|
||||||
|
)
|
||||||
t.prepareLocker.Do(ctx, func() {
|
t.prepareLocker.Do(ctx, func() {
|
||||||
t.client.SetAppAccessToken("")
|
t.client.SetAppAccessToken("")
|
||||||
t.client.SetUserAccessToken("")
|
t.client.SetUserAccessToken("")
|
||||||
@@ -121,7 +126,10 @@ func getUserID(
|
|||||||
return "", fmt.Errorf("unable to query user info: %w", err)
|
return "", fmt.Errorf("unable to query user info: %w", err)
|
||||||
}
|
}
|
||||||
if len(resp.Data.Users) != 1 {
|
if len(resp.Data.Users) != 1 {
|
||||||
return "", fmt.Errorf("expected 1 user with login, but received %d users", len(resp.Data.Users))
|
return "", fmt.Errorf(
|
||||||
|
"expected 1 user with login, but received %d users",
|
||||||
|
len(resp.Data.Users),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return resp.Data.Users[0].ID, nil
|
return resp.Data.Users[0].ID, nil
|
||||||
}
|
}
|
||||||
@@ -146,7 +154,12 @@ func (t *Twitch) prepareNoLock(ctx context.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logger.Debugf(ctx, "broadcaster_id: %s (login: %s)", t.broadcasterID, t.config.Config.Channel)
|
logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"broadcaster_id: %s (login: %s)",
|
||||||
|
t.broadcasterID,
|
||||||
|
t.config.Config.Channel,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -171,7 +184,13 @@ func (t *Twitch) editChannelInfo(
|
|||||||
return fmt.Errorf("unable to update the channel info (%#+v): %w", *params, err)
|
return fmt.Errorf("unable to update the channel info (%#+v): %w", *params, err)
|
||||||
}
|
}
|
||||||
if resp.ErrorStatus != 0 {
|
if resp.ErrorStatus != 0 {
|
||||||
return fmt.Errorf("unable to update the channel info (%#+v), the response reported an error: %d %v: %v", *params, resp.ErrorStatus, resp.Error, resp.ErrorMessage)
|
return fmt.Errorf(
|
||||||
|
"unable to update the channel info (%#+v), the response reported an error: %d %v: %v",
|
||||||
|
*params,
|
||||||
|
resp.ErrorStatus,
|
||||||
|
resp.Error,
|
||||||
|
resp.ErrorMessage,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
logger.Debugf(ctx, "success")
|
logger.Debugf(ctx, "success")
|
||||||
return nil
|
return nil
|
||||||
@@ -219,7 +238,10 @@ func (t *Twitch) ApplyProfile(
|
|||||||
|
|
||||||
if profile.CategoryName != nil {
|
if profile.CategoryName != nil {
|
||||||
if profile.CategoryID != nil {
|
if profile.CategoryID != nil {
|
||||||
logger.Warnf(ctx, "both category name and ID are set; these are contradicting stream profile settings; prioritizing the name")
|
logger.Warnf(
|
||||||
|
ctx,
|
||||||
|
"both category name and ID are set; these are contradicting stream profile settings; prioritizing the name",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
categoryID, err := t.getCategoryID(ctx, *profile.CategoryName)
|
categoryID, err := t.getCategoryID(ctx, *profile.CategoryName)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -237,7 +259,10 @@ func (t *Twitch) ApplyProfile(
|
|||||||
if tag == "" {
|
if tag == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
tag = truncateStringByByteLength(tag, 25) // see also: https://github.com/twitchdev/issues/issues/789
|
tag = truncateStringByByteLength(
|
||||||
|
tag,
|
||||||
|
25,
|
||||||
|
) // see also: https://github.com/twitchdev/issues/issues/789
|
||||||
tags = append(tags, tag)
|
tags = append(tags, tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,7 +272,10 @@ func (t *Twitch) ApplyProfile(
|
|||||||
if tags != nil {
|
if tags != nil {
|
||||||
logger.Debugf(ctx, "has tags")
|
logger.Debugf(ctx, "has tags")
|
||||||
if len(tags) == 0 {
|
if len(tags) == 0 {
|
||||||
logger.Warnf(ctx, "unfortunately, there is a bug in the helix lib, which does not allow to set zero tags, so adding tag 'stream' to the list of tags as a placeholder")
|
logger.Warnf(
|
||||||
|
ctx,
|
||||||
|
"unfortunately, there is a bug in the helix lib, which does not allow to set zero tags, so adding tag 'stream' to the list of tags as a placeholder",
|
||||||
|
)
|
||||||
params.Tags = []string{"English"}
|
params.Tags = []string{"English"}
|
||||||
} else {
|
} else {
|
||||||
params.Tags = tags
|
params.Tags = tags
|
||||||
@@ -294,7 +322,11 @@ func (t *Twitch) getCategoryID(
|
|||||||
Names: []string{categoryName},
|
Names: []string{categoryName},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("unable to query the category info (of name '%s'): %w", categoryName, err)
|
return "", fmt.Errorf(
|
||||||
|
"unable to query the category info (of name '%s'): %w",
|
||||||
|
categoryName,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(resp.Data.Games) != 1 {
|
if len(resp.Data.Games) != 1 {
|
||||||
@@ -360,7 +392,10 @@ func (t *Twitch) StartStream(
|
|||||||
result = multierror.Append(result, fmt.Errorf("unable to set description: %w", err))
|
result = multierror.Append(result, fmt.Errorf("unable to set description: %w", err))
|
||||||
}
|
}
|
||||||
if err := t.ApplyProfile(ctx, profile, customArgs...); err != nil {
|
if err := t.ApplyProfile(ctx, profile, customArgs...); err != nil {
|
||||||
result = multierror.Append(result, fmt.Errorf("unable to apply the stream-specific profile: %w", err))
|
result = multierror.Append(
|
||||||
|
result,
|
||||||
|
fmt.Errorf("unable to apply the stream-specific profile: %w", err),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return multierror.Append(result).ErrorOrNil()
|
return multierror.Append(result).ErrorOrNil()
|
||||||
}
|
}
|
||||||
@@ -614,7 +649,12 @@ func (t *Twitch) getNewTokenByUser(
|
|||||||
return fmt.Errorf("unable to get user access token: %w", err)
|
return fmt.Errorf("unable to get user access token: %w", err)
|
||||||
}
|
}
|
||||||
if resp.ErrorStatus != 0 {
|
if resp.ErrorStatus != 0 {
|
||||||
return fmt.Errorf("unable to query: %d %v: %v", resp.ErrorStatus, resp.Error, resp.ErrorMessage)
|
return fmt.Errorf(
|
||||||
|
"unable to query: %d %v: %v",
|
||||||
|
resp.ErrorStatus,
|
||||||
|
resp.Error,
|
||||||
|
resp.ErrorMessage,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
t.client.SetUserAccessToken(resp.Data.AccessToken)
|
t.client.SetUserAccessToken(resp.Data.AccessToken)
|
||||||
t.client.SetRefreshToken(resp.Data.RefreshToken)
|
t.client.SetRefreshToken(resp.Data.RefreshToken)
|
||||||
@@ -637,7 +677,12 @@ func (t *Twitch) getNewTokenByApp(
|
|||||||
return fmt.Errorf("unable to get app access token: %w", err)
|
return fmt.Errorf("unable to get app access token: %w", err)
|
||||||
}
|
}
|
||||||
if resp.ErrorStatus != 0 {
|
if resp.ErrorStatus != 0 {
|
||||||
return fmt.Errorf("unable to get app access token (the response contains an error): %d %v: %v", resp.ErrorStatus, resp.Error, resp.ErrorMessage)
|
return fmt.Errorf(
|
||||||
|
"unable to get app access token (the response contains an error): %d %v: %v",
|
||||||
|
resp.ErrorStatus,
|
||||||
|
resp.Error,
|
||||||
|
resp.ErrorMessage,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
logger.Debugf(ctx, "setting the app access token")
|
logger.Debugf(ctx, "setting the app access token")
|
||||||
t.client.SetAppAccessToken(resp.Data.AccessToken)
|
t.client.SetAppAccessToken(resp.Data.AccessToken)
|
||||||
@@ -735,7 +780,8 @@ func (t *Twitch) GetAllCategories(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pagination = &resp.Data.Pagination
|
pagination = &resp.Data.Pagination
|
||||||
logger.FromCtx(ctx).Tracef("I have %d categories now; new categories: %d", len(categoriesMap), newCategoriesCount)
|
logger.FromCtx(ctx).
|
||||||
|
Tracef("I have %d categories now; new categories: %d", len(categoriesMap), newCategoriesCount)
|
||||||
}
|
}
|
||||||
logger.FromCtx(ctx).Tracef("%d categories in total")
|
logger.FromCtx(ctx).Tracef("%d categories in total")
|
||||||
|
|
||||||
|
@@ -53,7 +53,9 @@ func New(
|
|||||||
saveCfgFn func(Config) error,
|
saveCfgFn func(Config) error,
|
||||||
) (*YouTube, error) {
|
) (*YouTube, error) {
|
||||||
if cfg.Config.ClientID == "" || cfg.Config.ClientSecret == "" {
|
if cfg.Config.ClientID == "" || cfg.Config.ClientSecret == "" {
|
||||||
return nil, fmt.Errorf("'clientid' or/and 'clientsecret' is/are not set; go to https://console.cloud.google.com/apis/credentials and create an app if it not created, yet")
|
return nil, fmt.Errorf(
|
||||||
|
"'clientid' or/and 'clientsecret' is/are not set; go to https://console.cloud.google.com/apis/credentials and create an app if it not created, yet",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancelFn := context.WithCancel(ctx)
|
ctx, cancelFn := context.WithCancel(ctx)
|
||||||
@@ -430,7 +432,10 @@ func (yt *YouTube) ApplyProfile(
|
|||||||
) error {
|
) error {
|
||||||
return yt.updateActiveBroadcasts(ctx, func(broadcast *youtube.LiveBroadcast) error {
|
return yt.updateActiveBroadcasts(ctx, func(broadcast *youtube.LiveBroadcast) error {
|
||||||
if broadcast.Snippet == nil {
|
if broadcast.Snippet == nil {
|
||||||
return fmt.Errorf("YouTube have not provided the current snippet of broadcast %v", broadcast.Id)
|
return fmt.Errorf(
|
||||||
|
"YouTube have not provided the current snippet of broadcast %v",
|
||||||
|
broadcast.Id,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
setProfile(broadcast, profile)
|
setProfile(broadcast, profile)
|
||||||
return nil
|
return nil
|
||||||
@@ -443,7 +448,10 @@ func (yt *YouTube) SetTitle(
|
|||||||
) error {
|
) error {
|
||||||
return yt.updateActiveBroadcasts(ctx, func(broadcast *youtube.LiveBroadcast) error {
|
return yt.updateActiveBroadcasts(ctx, func(broadcast *youtube.LiveBroadcast) error {
|
||||||
if broadcast.Snippet == nil {
|
if broadcast.Snippet == nil {
|
||||||
return fmt.Errorf("YouTube have not provided the current snippet of broadcast %v", broadcast.Id)
|
return fmt.Errorf(
|
||||||
|
"YouTube have not provided the current snippet of broadcast %v",
|
||||||
|
broadcast.Id,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
setTitle(broadcast, title)
|
setTitle(broadcast, title)
|
||||||
return nil
|
return nil
|
||||||
@@ -456,7 +464,10 @@ func (yt *YouTube) SetDescription(
|
|||||||
) error {
|
) error {
|
||||||
return yt.updateActiveBroadcasts(ctx, func(broadcast *youtube.LiveBroadcast) error {
|
return yt.updateActiveBroadcasts(ctx, func(broadcast *youtube.LiveBroadcast) error {
|
||||||
if broadcast.Snippet == nil {
|
if broadcast.Snippet == nil {
|
||||||
return fmt.Errorf("YouTube have not provided the current snippet of broadcast %v", broadcast.Id)
|
return fmt.Errorf(
|
||||||
|
"YouTube have not provided the current snippet of broadcast %v",
|
||||||
|
broadcast.Id,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
setDescription(broadcast, description)
|
setDescription(broadcast, description)
|
||||||
return nil
|
return nil
|
||||||
@@ -595,7 +606,12 @@ func (yt *YouTube) StartStream(
|
|||||||
logger.Debugf(ctx, "profile == %#+v", profile)
|
logger.Debugf(ctx, "profile == %#+v", profile)
|
||||||
|
|
||||||
templateBroadcastIDs = append(templateBroadcastIDs, profile.TemplateBroadcastIDs...)
|
templateBroadcastIDs = append(templateBroadcastIDs, profile.TemplateBroadcastIDs...)
|
||||||
logger.Debugf(ctx, "templateBroadcastIDs == %v; customArgs == %v", templateBroadcastIDs, customArgs)
|
logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"templateBroadcastIDs == %v; customArgs == %v",
|
||||||
|
templateBroadcastIDs,
|
||||||
|
customArgs,
|
||||||
|
)
|
||||||
|
|
||||||
templateBroadcastIDMap := map[string]struct{}{}
|
templateBroadcastIDMap := map[string]struct{}{}
|
||||||
for _, broadcastID := range templateBroadcastIDs {
|
for _, broadcastID := range templateBroadcastIDs {
|
||||||
@@ -629,7 +645,11 @@ func (yt *YouTube) StartStream(
|
|||||||
return fmt.Errorf("unable to get the list of active broadcasts: %w", err)
|
return fmt.Errorf("unable to get the list of active broadcasts: %w", err)
|
||||||
}
|
}
|
||||||
if len(response.Items) != len(templateBroadcastIDs) {
|
if len(response.Items) != len(templateBroadcastIDs) {
|
||||||
return fmt.Errorf("expected %d broadcasts, but found %d", len(templateBroadcastIDs), len(response.Items))
|
return fmt.Errorf(
|
||||||
|
"expected %d broadcasts, but found %d",
|
||||||
|
len(templateBroadcastIDs),
|
||||||
|
len(response.Items),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
broadcasts = append(broadcasts, response.Items...)
|
broadcasts = append(broadcasts, response.Items...)
|
||||||
}
|
}
|
||||||
@@ -637,18 +657,29 @@ func (yt *YouTube) StartStream(
|
|||||||
{
|
{
|
||||||
logger.Debugf(ctx, "getting video info of %v", templateBroadcastIDs)
|
logger.Debugf(ctx, "getting video info of %v", templateBroadcastIDs)
|
||||||
|
|
||||||
response, err := yt.YouTubeService.Videos.List(videoParts).Id(templateBroadcastIDs...).Context(ctx).Do()
|
response, err := yt.YouTubeService.Videos.List(videoParts).
|
||||||
|
Id(templateBroadcastIDs...).
|
||||||
|
Context(ctx).
|
||||||
|
Do()
|
||||||
logger.Debugf(ctx, "YouTube.Video result: %v", err)
|
logger.Debugf(ctx, "YouTube.Video result: %v", err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to get the list of active broadcasts: %w", err)
|
return fmt.Errorf("unable to get the list of active broadcasts: %w", err)
|
||||||
}
|
}
|
||||||
if len(response.Items) != len(templateBroadcastIDs) {
|
if len(response.Items) != len(templateBroadcastIDs) {
|
||||||
return fmt.Errorf("expected %d videos, but found %d", len(templateBroadcastIDs), len(response.Items))
|
return fmt.Errorf(
|
||||||
|
"expected %d videos, but found %d",
|
||||||
|
len(templateBroadcastIDs),
|
||||||
|
len(response.Items),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
videos = append(videos, response.Items...)
|
videos = append(videos, response.Items...)
|
||||||
}
|
}
|
||||||
|
|
||||||
playlistsResponse, err := yt.YouTubeService.Playlists.List(playlistParts).MaxResults(1000).Mine(true).Context(ctx).Do()
|
playlistsResponse, err := yt.YouTubeService.Playlists.List(playlistParts).
|
||||||
|
MaxResults(1000).
|
||||||
|
Mine(true).
|
||||||
|
Context(ctx).
|
||||||
|
Do()
|
||||||
logger.Debugf(ctx, "YouTube.Playlists result: %v", err)
|
logger.Debugf(ctx, "YouTube.Playlists result: %v", err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to get the list of playlists: %w", err)
|
return fmt.Errorf("unable to get the list of playlists: %w", err)
|
||||||
@@ -659,7 +690,12 @@ func (yt *YouTube) StartStream(
|
|||||||
logger.Debugf(ctx, "getting playlist items for %s", templateBroadcastID)
|
logger.Debugf(ctx, "getting playlist items for %s", templateBroadcastID)
|
||||||
|
|
||||||
for _, playlist := range playlistsResponse.Items {
|
for _, playlist := range playlistsResponse.Items {
|
||||||
playlistItemsResponse, err := yt.YouTubeService.PlaylistItems.List(playlistItemParts).MaxResults(1000).PlaylistId(playlist.Id).VideoId(templateBroadcastID).Context(ctx).Do()
|
playlistItemsResponse, err := yt.YouTubeService.PlaylistItems.List(playlistItemParts).
|
||||||
|
MaxResults(1000).
|
||||||
|
PlaylistId(playlist.Id).
|
||||||
|
VideoId(templateBroadcastID).
|
||||||
|
Context(ctx).
|
||||||
|
Do()
|
||||||
logger.Debugf(ctx, "YouTube.PlaylistItems result: %v", err)
|
logger.Debugf(ctx, "YouTube.PlaylistItems result: %v", err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to get the list of playlist items: %w", err)
|
return fmt.Errorf("unable to get the list of playlist items: %w", err)
|
||||||
@@ -676,15 +712,28 @@ func (yt *YouTube) StartStream(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Debugf(ctx, "found %d playlists for %s", len(playlistIDMap[templateBroadcastID]), templateBroadcastID)
|
logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"found %d playlists for %s",
|
||||||
|
len(playlistIDMap[templateBroadcastID]),
|
||||||
|
templateBroadcastID,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var highestStreamNum uint64
|
var highestStreamNum uint64
|
||||||
if profile.AutoNumerate {
|
if profile.AutoNumerate {
|
||||||
resp, err := yt.YouTubeService.LiveBroadcasts.List(liveBroadcastParts).Context(ctx).Mine(true).MaxResults(100).Fields().Do(googleapi.QueryParameter("order", "date"))
|
resp, err := yt.YouTubeService.LiveBroadcasts.List(liveBroadcastParts).
|
||||||
|
Context(ctx).
|
||||||
|
Mine(true).
|
||||||
|
MaxResults(100).
|
||||||
|
Fields().
|
||||||
|
Do(googleapi.QueryParameter("order", "date"))
|
||||||
logger.Debugf(ctx, "YouTube.LiveBroadcasts result: %v", err)
|
logger.Debugf(ctx, "YouTube.LiveBroadcasts result: %v", err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to request previous streams to figure out the next stream number for auto-numeration: %w", err)
|
return fmt.Errorf(
|
||||||
|
"unable to request previous streams to figure out the next stream number for auto-numeration: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, b := range resp.Items {
|
for _, b := range resp.Items {
|
||||||
@@ -709,7 +758,11 @@ func (yt *YouTube) StartStream(
|
|||||||
templateBroadcastID := broadcast.Id
|
templateBroadcastID := broadcast.Id
|
||||||
|
|
||||||
if video.Id != broadcast.Id {
|
if video.Id != broadcast.Id {
|
||||||
return fmt.Errorf("internal error: the orders of videos and broadcasts do not match: %s != %s", video.Id, broadcast.Id)
|
return fmt.Errorf(
|
||||||
|
"internal error: the orders of videos and broadcasts do not match: %s != %s",
|
||||||
|
video.Id,
|
||||||
|
broadcast.Id,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
broadcast.Id = ""
|
broadcast.Id = ""
|
||||||
@@ -720,7 +773,9 @@ func (yt *YouTube) StartStream(
|
|||||||
broadcast.ContentDetails.MonitorStream = nil
|
broadcast.ContentDetails.MonitorStream = nil
|
||||||
broadcast.ContentDetails.ForceSendFields = []string{"EnableAutoStop"}
|
broadcast.ContentDetails.ForceSendFields = []string{"EnableAutoStop"}
|
||||||
broadcast.Snippet.ScheduledStartTime = now.Format("2006-01-02T15:04:05") + ".00Z"
|
broadcast.Snippet.ScheduledStartTime = now.Format("2006-01-02T15:04:05") + ".00Z"
|
||||||
broadcast.Snippet.ScheduledEndTime = now.Add(time.Hour*12).Format("2006-01-02T15:04:05") + ".00Z"
|
broadcast.Snippet.ScheduledEndTime = now.Add(time.Hour*12).
|
||||||
|
Format("2006-01-02T15:04:05") +
|
||||||
|
".00Z"
|
||||||
broadcast.Snippet.LiveChatId = ""
|
broadcast.Snippet.LiveChatId = ""
|
||||||
broadcast.Status.SelfDeclaredMadeForKids = broadcast.Status.MadeForKids
|
broadcast.Status.SelfDeclaredMadeForKids = broadcast.Status.MadeForKids
|
||||||
broadcast.Status.ForceSendFields = []string{"SelfDeclaredMadeForKids"}
|
broadcast.Status.ForceSendFields = []string{"SelfDeclaredMadeForKids"}
|
||||||
@@ -747,7 +802,10 @@ func (yt *YouTube) StartStream(
|
|||||||
logger.Debugf(ctx, "YouTube.LiveBroadcasts result: %v", err)
|
logger.Debugf(ctx, "YouTube.LiveBroadcasts result: %v", err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "invalidScheduledStartTime") {
|
if strings.Contains(err.Error(), "invalidScheduledStartTime") {
|
||||||
logger.Debugf(ctx, "it seems the local system clock is off, trying to fix the schedule time")
|
logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"it seems the local system clock is off, trying to fix the schedule time",
|
||||||
|
)
|
||||||
|
|
||||||
now, err = timeapiio.Now()
|
now, err = timeapiio.Now()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -758,7 +816,9 @@ func (yt *YouTube) StartStream(
|
|||||||
now = time.Now().Add(time.Hour)
|
now = time.Now().Add(time.Hour)
|
||||||
}
|
}
|
||||||
broadcast.Snippet.ScheduledStartTime = now.Format("2006-01-02T15:04:05") + ".00Z"
|
broadcast.Snippet.ScheduledStartTime = now.Format("2006-01-02T15:04:05") + ".00Z"
|
||||||
broadcast.Snippet.ScheduledEndTime = now.Add(time.Hour*12).Format("2006-01-02T15:04:05") + ".00Z"
|
broadcast.Snippet.ScheduledEndTime = now.Add(time.Hour*12).
|
||||||
|
Format("2006-01-02T15:04:05") +
|
||||||
|
".00Z"
|
||||||
newBroadcast, err = yt.YouTubeService.LiveBroadcasts.Insert(
|
newBroadcast, err = yt.YouTubeService.LiveBroadcasts.Insert(
|
||||||
[]string{"snippet", "contentDetails", "monetizationDetails", "status"},
|
[]string{"snippet", "contentDetails", "monetizationDetails", "status"},
|
||||||
broadcast,
|
broadcast,
|
||||||
@@ -789,13 +849,22 @@ func (yt *YouTube) StartStream(
|
|||||||
video.Snippet.Tags = append(video.Snippet.Tags, profile.Tags...)
|
video.Snippet.Tags = append(video.Snippet.Tags, profile.Tags...)
|
||||||
video.Snippet.Tags = append(video.Snippet.Tags, templateTags...)
|
video.Snippet.Tags = append(video.Snippet.Tags, templateTags...)
|
||||||
default:
|
default:
|
||||||
logger.Errorf(ctx, "unexpected value of the 'TemplateTags' setting: '%v'", profile.TemplateTags)
|
logger.Errorf(
|
||||||
|
ctx,
|
||||||
|
"unexpected value of the 'TemplateTags' setting: '%v'",
|
||||||
|
profile.TemplateTags,
|
||||||
|
)
|
||||||
video.Snippet.Tags = profile.Tags
|
video.Snippet.Tags = profile.Tags
|
||||||
}
|
}
|
||||||
video.Snippet.Tags = deduplicate(video.Snippet.Tags)
|
video.Snippet.Tags = deduplicate(video.Snippet.Tags)
|
||||||
tagsTruncated := TruncateTags(video.Snippet.Tags)
|
tagsTruncated := TruncateTags(video.Snippet.Tags)
|
||||||
if len(tagsTruncated) != len(video.Snippet.Tags) {
|
if len(tagsTruncated) != len(video.Snippet.Tags) {
|
||||||
logger.Infof(ctx, "YouTube tags were truncated, the amount was reduced from %d to %d to satisfy the 500 characters limit", len(video.Snippet.Tags), len(tagsTruncated))
|
logger.Infof(
|
||||||
|
ctx,
|
||||||
|
"YouTube tags were truncated, the amount was reduced from %d to %d to satisfy the 500 characters limit",
|
||||||
|
len(video.Snippet.Tags),
|
||||||
|
len(tagsTruncated),
|
||||||
|
)
|
||||||
video.Snippet.Tags = tagsTruncated
|
video.Snippet.Tags = tagsTruncated
|
||||||
}
|
}
|
||||||
b, err = yaml.Marshal(video)
|
b, err = yaml.Marshal(video)
|
||||||
@@ -832,7 +901,9 @@ func (yt *YouTube) StartStream(
|
|||||||
logger.Debugf(ctx, "adding the video to playlist %#+v", newPlaylistItem)
|
logger.Debugf(ctx, "adding the video to playlist %#+v", newPlaylistItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = yt.YouTubeService.PlaylistItems.Insert(playlistItemParts, newPlaylistItem).Context(ctx).Do()
|
_, err = yt.YouTubeService.PlaylistItems.Insert(playlistItemParts, newPlaylistItem).
|
||||||
|
Context(ctx).
|
||||||
|
Do()
|
||||||
logger.Debugf(ctx, "YouTube.PlaylistItems result: %v", err)
|
logger.Debugf(ctx, "YouTube.PlaylistItems result: %v", err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to add video to playlist %#+v: %w", playlistID, err)
|
return fmt.Errorf("unable to add video to playlist %#+v: %w", playlistID, err)
|
||||||
@@ -843,16 +914,25 @@ func (yt *YouTube) StartStream(
|
|||||||
logger.Debugf(ctx, "downloading the thumbnail")
|
logger.Debugf(ctx, "downloading the thumbnail")
|
||||||
resp, err := http.Get(broadcast.Snippet.Thumbnails.Standard.Url)
|
resp, err := http.Get(broadcast.Snippet.Thumbnails.Standard.Url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to download the thumbnail from the template video: %w", err)
|
return fmt.Errorf(
|
||||||
|
"unable to download the thumbnail from the template video: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
logger.Debugf(ctx, "reading the thumbnail")
|
logger.Debugf(ctx, "reading the thumbnail")
|
||||||
thumbnail, err := io.ReadAll(resp.Body)
|
thumbnail, err := io.ReadAll(resp.Body)
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to read the thumbnail from the response from the template video: %w", err)
|
return fmt.Errorf(
|
||||||
|
"unable to read the thumbnail from the response from the template video: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
logger.Debugf(ctx, "setting the thumbnail")
|
logger.Debugf(ctx, "setting the thumbnail")
|
||||||
_, err = yt.YouTubeService.Thumbnails.Set(newBroadcast.Id).Media(bytes.NewReader(thumbnail)).Context(ctx).Do()
|
_, err = yt.YouTubeService.Thumbnails.Set(newBroadcast.Id).
|
||||||
|
Media(bytes.NewReader(thumbnail)).
|
||||||
|
Context(ctx).
|
||||||
|
Do()
|
||||||
logger.Debugf(ctx, "YouTube.Thumbnails result: %v", err)
|
logger.Debugf(ctx, "YouTube.Thumbnails result: %v", err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to set the thumbnail: %w", err)
|
return fmt.Errorf("unable to set the thumbnail: %w", err)
|
||||||
@@ -907,7 +987,13 @@ func (yt *YouTube) GetStreamStatus(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
_startedAt, err = time.Parse(timeLayoutFallback, ts)
|
_startedAt, err = time.Parse(timeLayoutFallback, ts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to parse '%s' with layouts '%s' and '%s': %w", ts, timeLayout, timeLayoutFallback, err)
|
return fmt.Errorf(
|
||||||
|
"unable to parse '%s' with layouts '%s' and '%s': %w",
|
||||||
|
ts,
|
||||||
|
timeLayout,
|
||||||
|
timeLayoutFallback,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
startedAt = &_startedAt
|
startedAt = &_startedAt
|
||||||
@@ -939,7 +1025,12 @@ func (yt *YouTube) GetStreamStatus(
|
|||||||
Streams: streams,
|
Streams: streams,
|
||||||
}
|
}
|
||||||
if observability.LogLevelFilter.GetLevel() >= logger.LevelTrace {
|
if observability.LogLevelFilter.GetLevel() >= logger.LevelTrace {
|
||||||
logger.Tracef(ctx, "len(customData.UpcomingBroadcasts) == %d; len(customData.Streams) == %d", len(customData.UpcomingBroadcasts), len(customData.Streams))
|
logger.Tracef(
|
||||||
|
ctx,
|
||||||
|
"len(customData.UpcomingBroadcasts) == %d; len(customData.Streams) == %d",
|
||||||
|
len(customData.UpcomingBroadcasts),
|
||||||
|
len(customData.Streams),
|
||||||
|
)
|
||||||
for idx, broadcast := range customData.UpcomingBroadcasts {
|
for idx, broadcast := range customData.UpcomingBroadcasts {
|
||||||
b, err := json.Marshal(broadcast)
|
b, err := json.Marshal(broadcast)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -948,7 +1039,11 @@ func (yt *YouTube) GetStreamStatus(
|
|||||||
logger.Tracef(ctx, "UpcomingBroadcasts[%3d] == %s", idx, b)
|
logger.Tracef(ctx, "UpcomingBroadcasts[%3d] == %s", idx, b)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.Tracef(ctx, "len(customData.ActiveBroadcasts) == %d", len(customData.ActiveBroadcasts))
|
logger.Tracef(
|
||||||
|
ctx,
|
||||||
|
"len(customData.ActiveBroadcasts) == %d",
|
||||||
|
len(customData.ActiveBroadcasts),
|
||||||
|
)
|
||||||
for idx, bc := range customData.ActiveBroadcasts {
|
for idx, bc := range customData.ActiveBroadcasts {
|
||||||
b, err := json.Marshal(bc)
|
b, err := json.Marshal(bc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -48,7 +48,12 @@ type StreamD interface {
|
|||||||
) error
|
) error
|
||||||
SetTitle(ctx context.Context, platID streamcontrol.PlatformName, title string) error
|
SetTitle(ctx context.Context, platID streamcontrol.PlatformName, title string) error
|
||||||
SetDescription(ctx context.Context, platID streamcontrol.PlatformName, description string) error
|
SetDescription(ctx context.Context, platID streamcontrol.PlatformName, description string) error
|
||||||
ApplyProfile(ctx context.Context, platID streamcontrol.PlatformName, profile streamcontrol.AbstractStreamProfile, customArgs ...any) error
|
ApplyProfile(
|
||||||
|
ctx context.Context,
|
||||||
|
platID streamcontrol.PlatformName,
|
||||||
|
profile streamcontrol.AbstractStreamProfile,
|
||||||
|
customArgs ...any,
|
||||||
|
) error
|
||||||
OBSOLETE_GitRelogin(ctx context.Context) error
|
OBSOLETE_GitRelogin(ctx context.Context) error
|
||||||
GetBackendData(ctx context.Context, platID streamcontrol.PlatformName) (any, error)
|
GetBackendData(ctx context.Context, platID streamcontrol.PlatformName) (any, error)
|
||||||
Restart(ctx context.Context) error
|
Restart(ctx context.Context) error
|
||||||
@@ -189,7 +194,12 @@ type StreamD interface {
|
|||||||
ListTimers(ctx context.Context) ([]Timer, error)
|
ListTimers(ctx context.Context) ([]Timer, error)
|
||||||
|
|
||||||
AddOBSSceneRule(ctx context.Context, sceneName SceneName, sceneRule SceneRule) error
|
AddOBSSceneRule(ctx context.Context, sceneName SceneName, sceneRule SceneRule) error
|
||||||
UpdateOBSSceneRule(ctx context.Context, sceneName SceneName, idx uint64, sceneRule SceneRule) error
|
UpdateOBSSceneRule(
|
||||||
|
ctx context.Context,
|
||||||
|
sceneName SceneName,
|
||||||
|
idx uint64,
|
||||||
|
sceneRule SceneRule,
|
||||||
|
) error
|
||||||
RemoveOBSSceneRule(ctx context.Context, sceneName SceneName, idx uint64) error
|
RemoveOBSSceneRule(ctx context.Context, sceneName SceneName, idx uint64) error
|
||||||
ListOBSSceneRules(ctx context.Context, sceneName SceneName) (SceneRules, error)
|
ListOBSSceneRules(ctx context.Context, sceneName SceneName) (SceneRules, error)
|
||||||
}
|
}
|
||||||
|
@@ -47,9 +47,15 @@ func NewConfig() Config {
|
|||||||
|
|
||||||
func NewSampleConfig() Config {
|
func NewSampleConfig() Config {
|
||||||
cfg := NewConfig()
|
cfg := NewConfig()
|
||||||
cfg.Backends[obs.ID].StreamProfiles = map[streamcontrol.ProfileName]streamcontrol.AbstractStreamProfile{"some_profile": obs.StreamProfile{}}
|
cfg.Backends[obs.ID].StreamProfiles = map[streamcontrol.ProfileName]streamcontrol.AbstractStreamProfile{
|
||||||
cfg.Backends[twitch.ID].StreamProfiles = map[streamcontrol.ProfileName]streamcontrol.AbstractStreamProfile{"some_profile": twitch.StreamProfile{}}
|
"some_profile": obs.StreamProfile{},
|
||||||
cfg.Backends[youtube.ID].StreamProfiles = map[streamcontrol.ProfileName]streamcontrol.AbstractStreamProfile{"some_profile": youtube.StreamProfile{}}
|
}
|
||||||
|
cfg.Backends[twitch.ID].StreamProfiles = map[streamcontrol.ProfileName]streamcontrol.AbstractStreamProfile{
|
||||||
|
"some_profile": twitch.StreamProfile{},
|
||||||
|
}
|
||||||
|
cfg.Backends[youtube.ID].StreamProfiles = map[streamcontrol.ProfileName]streamcontrol.AbstractStreamProfile{
|
||||||
|
"some_profile": youtube.StreamProfile{},
|
||||||
|
}
|
||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -166,16 +166,32 @@ func (s *MonitorSourceOBSVideo) GetImageBytes(
|
|||||||
}
|
}
|
||||||
resp, err := obsServer.GetSourceScreenshot(ctx, req)
|
resp, err := obsServer.GetSourceScreenshot(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", time.Now().Add(time.Second), fmt.Errorf("unable to get a screenshot of '%s': %w", s.Name, err)
|
return nil, "", time.Now().
|
||||||
|
Add(time.Second),
|
||||||
|
fmt.Errorf(
|
||||||
|
"unable to get a screenshot of '%s': %w",
|
||||||
|
s.Name,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
imgB64 := resp.GetImageData()
|
imgB64 := resp.GetImageData()
|
||||||
imgBytes, mimeType, err := imgb64.Decode(string(imgB64))
|
imgBytes, mimeType, err := imgb64.Decode(string(imgB64))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", time.Time{}, fmt.Errorf("unable to decode the screenshot of '%s': %w", s.Name, err)
|
return nil, "", time.Time{}, fmt.Errorf(
|
||||||
|
"unable to decode the screenshot of '%s': %w",
|
||||||
|
s.Name,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Tracef(ctx, "the decoded image is of format '%s' (expected format: '%s') and size %d", mimeType, s.ImageFormat, len(imgBytes))
|
logger.Tracef(
|
||||||
|
ctx,
|
||||||
|
"the decoded image is of format '%s' (expected format: '%s') and size %d",
|
||||||
|
mimeType,
|
||||||
|
s.ImageFormat,
|
||||||
|
len(imgBytes),
|
||||||
|
)
|
||||||
return imgBytes, mimeType, time.Now().Add(time.Duration(s.UpdateInterval)), nil
|
return imgBytes, mimeType, time.Now().Add(time.Duration(s.UpdateInterval)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,11 +240,19 @@ func (s *MonitorSourceOBSVolume) GetImage(
|
|||||||
|
|
||||||
colorActive, err := colorx.Parse(s.ColorActive)
|
colorActive, err := colorx.Parse(s.ColorActive)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, time.Time{}, fmt.Errorf("unable to parse the `color_active` value '%s': %w", s.ColorActive, err)
|
return nil, time.Time{}, fmt.Errorf(
|
||||||
|
"unable to parse the `color_active` value '%s': %w",
|
||||||
|
s.ColorActive,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
colorPassive, err := colorx.Parse(s.ColorPassive)
|
colorPassive, err := colorx.Parse(s.ColorPassive)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, time.Time{}, fmt.Errorf("unable to parse the `color_passive` value '%s': %w", s.ColorPassive, err)
|
return nil, time.Time{}, fmt.Errorf(
|
||||||
|
"unable to parse the `color_passive` value '%s': %w",
|
||||||
|
s.ColorPassive,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
size := img.Bounds().Size()
|
size := img.Bounds().Size()
|
||||||
|
@@ -66,21 +66,30 @@ func (cfg *Config) UnmarshalYAML(b []byte) (_err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Backends[obs.ID] != nil {
|
if cfg.Backends[obs.ID] != nil {
|
||||||
err = streamcontrol.ConvertStreamProfiles[obs.StreamProfile](context.Background(), cfg.Backends[obs.ID].StreamProfiles)
|
err = streamcontrol.ConvertStreamProfiles[obs.StreamProfile](
|
||||||
|
context.Background(),
|
||||||
|
cfg.Backends[obs.ID].StreamProfiles,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to convert stream profiles of OBS: %w", err)
|
return fmt.Errorf("unable to convert stream profiles of OBS: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Backends[twitch.ID] != nil {
|
if cfg.Backends[twitch.ID] != nil {
|
||||||
err = streamcontrol.ConvertStreamProfiles[twitch.StreamProfile](context.Background(), cfg.Backends[twitch.ID].StreamProfiles)
|
err = streamcontrol.ConvertStreamProfiles[twitch.StreamProfile](
|
||||||
|
context.Background(),
|
||||||
|
cfg.Backends[twitch.ID].StreamProfiles,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to convert stream profiles of twitch: %w", err)
|
return fmt.Errorf("unable to convert stream profiles of twitch: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Backends[youtube.ID] != nil {
|
if cfg.Backends[youtube.ID] != nil {
|
||||||
err = streamcontrol.ConvertStreamProfiles[youtube.StreamProfile](context.Background(), cfg.Backends[youtube.ID].StreamProfiles)
|
err = streamcontrol.ConvertStreamProfiles[youtube.StreamProfile](
|
||||||
|
context.Background(),
|
||||||
|
cfg.Backends[youtube.ID].StreamProfiles,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to convert stream profiles of youtube: %w", err)
|
return fmt.Errorf("unable to convert stream profiles of youtube: %w", err)
|
||||||
}
|
}
|
||||||
|
@@ -99,7 +99,10 @@ func (d *StreamD) onConfigUpdateViaGIT(ctx context.Context, cfg *config.Config)
|
|||||||
d.UI.DisplayError(fmt.Errorf("unable to save data: %w", err))
|
d.UI.DisplayError(fmt.Errorf("unable to save data: %w", err))
|
||||||
}
|
}
|
||||||
if d.GitInitialized {
|
if d.GitInitialized {
|
||||||
d.UI.Restart(ctx, "Received an updated config from another device, please restart the application")
|
d.UI.Restart(
|
||||||
|
ctx,
|
||||||
|
"Received an updated config from another device, please restart the application",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +206,9 @@ func (d *StreamD) startPeriodicGitSyncer(ctx context.Context) {
|
|||||||
observability.Go(ctx, func() {
|
observability.Go(ctx, func() {
|
||||||
err := d.sendConfigViaGIT(ctx)
|
err := d.sendConfigViaGIT(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.UI.DisplayError(fmt.Errorf("unable to send the config to the remote git repository: %w", err))
|
d.UI.DisplayError(
|
||||||
|
fmt.Errorf("unable to send the config to the remote git repository: %w", err),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ticker := time.NewTicker(time.Minute)
|
ticker := time.NewTicker(time.Minute)
|
||||||
|
@@ -88,7 +88,12 @@ func Memoize[REQ any, REPLY any, T func(context.Context, REQ) (REPLY, error)](
|
|||||||
logger.Tracef(ctx, "using the cached value")
|
logger.Tracef(ctx, "using the cached value")
|
||||||
return v.Reply, v.Error
|
return v.Reply, v.Error
|
||||||
}
|
}
|
||||||
logger.Tracef(ctx, "the cached value expired: %s < %s", v.SavedAt.Format(timeFormat), cutoffTS.Format(timeFormat))
|
logger.Tracef(
|
||||||
|
ctx,
|
||||||
|
"the cached value expired: %s < %s",
|
||||||
|
v.SavedAt.Format(timeFormat),
|
||||||
|
cutoffTS.Format(timeFormat),
|
||||||
|
)
|
||||||
delete(cache, key)
|
delete(cache, key)
|
||||||
} else {
|
} else {
|
||||||
logger.Errorf(ctx, "cache-failure: expected type %T, but got %T", (*cacheItem)(nil), cachedResult)
|
logger.Errorf(ctx, "cache-failure: expected type %T, but got %T", (*cacheItem)(nil), cachedResult)
|
||||||
|
@@ -243,7 +243,11 @@ func (grpc *GRPCServer) IsBackendEnabled(
|
|||||||
) (*streamd_grpc.IsBackendEnabledReply, error) {
|
) (*streamd_grpc.IsBackendEnabledReply, error) {
|
||||||
enabled, err := grpc.StreamD.IsBackendEnabled(ctx, streamcontrol.PlatformName(req.GetPlatID()))
|
enabled, err := grpc.StreamD.IsBackendEnabled(ctx, streamcontrol.PlatformName(req.GetPlatID()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to check if backend '%s' is enabled: %w", req.GetPlatID(), err)
|
return nil, fmt.Errorf(
|
||||||
|
"unable to check if backend '%s' is enabled: %w",
|
||||||
|
req.GetPlatID(),
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return &streamd_grpc.IsBackendEnabledReply{
|
return &streamd_grpc.IsBackendEnabledReply{
|
||||||
IsInitialized: enabled,
|
IsInitialized: enabled,
|
||||||
@@ -564,7 +568,10 @@ func (grpc *GRPCServer) openBrowser(
|
|||||||
count++
|
count++
|
||||||
err := handler.Sender.Send(&req)
|
err := handler.Sender.Send(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = multierror.Append(resultErr, fmt.Errorf("unable to send oauth request: %w", err))
|
err = multierror.Append(
|
||||||
|
resultErr,
|
||||||
|
fmt.Errorf("unable to send oauth request: %w", err),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -583,10 +590,25 @@ func (grpc *GRPCServer) OpenOAuthURL(
|
|||||||
) (_ret error) {
|
) (_ret error) {
|
||||||
logger.Debugf(ctx, "OpenOAuthURL(ctx, %d, '%s', '%s')", listenPort, platID, authURL)
|
logger.Debugf(ctx, "OpenOAuthURL(ctx, %d, '%s', '%s')", listenPort, platID, authURL)
|
||||||
defer func() {
|
defer func() {
|
||||||
logger.Debugf(ctx, "/OpenOAuthURL(ctx, %d, '%s', '%s'): %v", listenPort, platID, authURL, _ret)
|
logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"/OpenOAuthURL(ctx, %d, '%s', '%s'): %v",
|
||||||
|
listenPort,
|
||||||
|
platID,
|
||||||
|
authURL,
|
||||||
|
_ret,
|
||||||
|
)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return xsync.DoA4R1(ctx, &grpc.OAuthURLHandlerLocker, grpc.openOAuthURL, ctx, listenPort, platID, authURL)
|
return xsync.DoA4R1(
|
||||||
|
ctx,
|
||||||
|
&grpc.OAuthURLHandlerLocker,
|
||||||
|
grpc.openOAuthURL,
|
||||||
|
ctx,
|
||||||
|
listenPort,
|
||||||
|
platID,
|
||||||
|
authURL,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (grpc *GRPCServer) openOAuthURL(
|
func (grpc *GRPCServer) openOAuthURL(
|
||||||
@@ -703,7 +725,12 @@ func (grpc *GRPCServer) ListStreamServers(
|
|||||||
|
|
||||||
var result []*streamd_grpc.StreamServerWithStatistics
|
var result []*streamd_grpc.StreamServerWithStatistics
|
||||||
for _, srv := range servers {
|
for _, srv := range servers {
|
||||||
srvGRPC, err := goconv.StreamServerConfigGo2GRPC(ctx, srv.Type, srv.ListenAddr, srv.Options())
|
srvGRPC, err := goconv.StreamServerConfigGo2GRPC(
|
||||||
|
ctx,
|
||||||
|
srv.Type,
|
||||||
|
srv.ListenAddr,
|
||||||
|
srv.Options(),
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to convert the server server value: %w", err)
|
return nil, fmt.Errorf("unable to convert the server server value: %w", err)
|
||||||
}
|
}
|
||||||
@@ -727,7 +754,11 @@ func (grpc *GRPCServer) StartStreamServer(
|
|||||||
) (*streamd_grpc.StartStreamServerReply, error) {
|
) (*streamd_grpc.StartStreamServerReply, error) {
|
||||||
srvType, addr, opts, err := goconv.StreamServerConfigGRPC2Go(ctx, req.GetConfig())
|
srvType, addr, opts, err := goconv.StreamServerConfigGRPC2Go(ctx, req.GetConfig())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to convert the stream server config %#+v: %w", req.GetConfig(), err)
|
return nil, fmt.Errorf(
|
||||||
|
"unable to convert the stream server config %#+v: %w",
|
||||||
|
req.GetConfig(),
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = grpc.StreamD.StartStreamServer(
|
err = grpc.StreamD.StartStreamServer(
|
||||||
@@ -922,8 +953,12 @@ func (grpc *GRPCServer) AddStreamForward(
|
|||||||
api.StreamForwardingQuirks{
|
api.StreamForwardingQuirks{
|
||||||
RestartUntilYoutubeRecognizesStream: api.RestartUntilYoutubeRecognizesStream{
|
RestartUntilYoutubeRecognizesStream: api.RestartUntilYoutubeRecognizesStream{
|
||||||
Enabled: cfg.Quirks.RestartUntilYoutubeRecognizesStream.Enabled,
|
Enabled: cfg.Quirks.RestartUntilYoutubeRecognizesStream.Enabled,
|
||||||
StartTimeout: sec2dur(cfg.Quirks.RestartUntilYoutubeRecognizesStream.StartTimeout),
|
StartTimeout: sec2dur(
|
||||||
StopStartDelay: sec2dur(cfg.Quirks.RestartUntilYoutubeRecognizesStream.StopStartDelay),
|
cfg.Quirks.RestartUntilYoutubeRecognizesStream.StartTimeout,
|
||||||
|
),
|
||||||
|
StopStartDelay: sec2dur(
|
||||||
|
cfg.Quirks.RestartUntilYoutubeRecognizesStream.StopStartDelay,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
StartAfterYoutubeRecognizedStream: api.StartAfterYoutubeRecognizedStream{
|
StartAfterYoutubeRecognizedStream: api.StartAfterYoutubeRecognizedStream{
|
||||||
Enabled: cfg.Quirks.StartAfterYoutubeRecognizedStream.Enabled,
|
Enabled: cfg.Quirks.StartAfterYoutubeRecognizedStream.Enabled,
|
||||||
@@ -949,8 +984,12 @@ func (grpc *GRPCServer) UpdateStreamForward(
|
|||||||
api.StreamForwardingQuirks{
|
api.StreamForwardingQuirks{
|
||||||
RestartUntilYoutubeRecognizesStream: api.RestartUntilYoutubeRecognizesStream{
|
RestartUntilYoutubeRecognizesStream: api.RestartUntilYoutubeRecognizesStream{
|
||||||
Enabled: cfg.Quirks.RestartUntilYoutubeRecognizesStream.Enabled,
|
Enabled: cfg.Quirks.RestartUntilYoutubeRecognizesStream.Enabled,
|
||||||
StartTimeout: sec2dur(cfg.Quirks.RestartUntilYoutubeRecognizesStream.StartTimeout),
|
StartTimeout: sec2dur(
|
||||||
StopStartDelay: sec2dur(cfg.Quirks.RestartUntilYoutubeRecognizesStream.StopStartDelay),
|
cfg.Quirks.RestartUntilYoutubeRecognizesStream.StartTimeout,
|
||||||
|
),
|
||||||
|
StopStartDelay: sec2dur(
|
||||||
|
cfg.Quirks.RestartUntilYoutubeRecognizesStream.StopStartDelay,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
StartAfterYoutubeRecognizedStream: api.StartAfterYoutubeRecognizedStream{
|
StartAfterYoutubeRecognizedStream: api.StartAfterYoutubeRecognizedStream{
|
||||||
Enabled: cfg.Quirks.StartAfterYoutubeRecognizedStream.Enabled,
|
Enabled: cfg.Quirks.StartAfterYoutubeRecognizedStream.Enabled,
|
||||||
@@ -1113,7 +1152,11 @@ func (grpc *GRPCServer) StreamPlayerOpen(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
req *streamd_grpc.StreamPlayerOpenRequest,
|
req *streamd_grpc.StreamPlayerOpenRequest,
|
||||||
) (*streamd_grpc.StreamPlayerOpenReply, error) {
|
) (*streamd_grpc.StreamPlayerOpenReply, error) {
|
||||||
err := grpc.StreamD.StreamPlayerOpenURL(ctx, streamtypes.StreamID(req.GetStreamID()), req.GetRequest().GetLink())
|
err := grpc.StreamD.StreamPlayerOpenURL(
|
||||||
|
ctx,
|
||||||
|
streamtypes.StreamID(req.GetStreamID()),
|
||||||
|
req.GetRequest().GetLink(),
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1125,7 +1168,10 @@ func (grpc *GRPCServer) StreamPlayerProcessTitle(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
req *streamd_grpc.StreamPlayerProcessTitleRequest,
|
req *streamd_grpc.StreamPlayerProcessTitleRequest,
|
||||||
) (*streamd_grpc.StreamPlayerProcessTitleReply, error) {
|
) (*streamd_grpc.StreamPlayerProcessTitleReply, error) {
|
||||||
title, err := grpc.StreamD.StreamPlayerProcessTitle(ctx, streamtypes.StreamID(req.GetStreamID()))
|
title, err := grpc.StreamD.StreamPlayerProcessTitle(
|
||||||
|
ctx,
|
||||||
|
streamtypes.StreamID(req.GetStreamID()),
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1214,7 +1260,11 @@ func (grpc *GRPCServer) StreamPlayerSetSpeed(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
req *streamd_grpc.StreamPlayerSetSpeedRequest,
|
req *streamd_grpc.StreamPlayerSetSpeedRequest,
|
||||||
) (*streamd_grpc.StreamPlayerSetSpeedReply, error) {
|
) (*streamd_grpc.StreamPlayerSetSpeedReply, error) {
|
||||||
err := grpc.StreamD.StreamPlayerSetSpeed(ctx, streamtypes.StreamID(req.GetStreamID()), req.GetRequest().Speed)
|
err := grpc.StreamD.StreamPlayerSetSpeed(
|
||||||
|
ctx,
|
||||||
|
streamtypes.StreamID(req.GetStreamID()),
|
||||||
|
req.GetRequest().Speed,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1226,7 +1276,11 @@ func (grpc *GRPCServer) StreamPlayerSetPause(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
req *streamd_grpc.StreamPlayerSetPauseRequest,
|
req *streamd_grpc.StreamPlayerSetPauseRequest,
|
||||||
) (*streamd_grpc.StreamPlayerSetPauseReply, error) {
|
) (*streamd_grpc.StreamPlayerSetPauseReply, error) {
|
||||||
err := grpc.StreamD.StreamPlayerSetPause(ctx, streamtypes.StreamID(req.GetStreamID()), req.GetRequest().IsPaused)
|
err := grpc.StreamD.StreamPlayerSetPause(
|
||||||
|
ctx,
|
||||||
|
streamtypes.StreamID(req.GetStreamID()),
|
||||||
|
req.GetRequest().IsPaused,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@@ -45,7 +45,10 @@ func (d *StreamD) EXPERIMENTAL_ReinitStreamControllers(ctx context.Context) erro
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result = multierror.Append(result, fmt.Errorf("unable to initialize '%s': %w", platName, err))
|
result = multierror.Append(
|
||||||
|
result,
|
||||||
|
fmt.Errorf("unable to initialize '%s': %w", platName, err),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result.ErrorOrNil()
|
return result.ErrorOrNil()
|
||||||
@@ -128,7 +131,8 @@ func newTwitch(
|
|||||||
error,
|
error,
|
||||||
) {
|
) {
|
||||||
platCfg := streamcontrol.ConvertPlatformConfig[twitch.PlatformSpecificConfig, twitch.StreamProfile](
|
platCfg := streamcontrol.ConvertPlatformConfig[twitch.PlatformSpecificConfig, twitch.StreamProfile](
|
||||||
ctx, cfg,
|
ctx,
|
||||||
|
cfg,
|
||||||
)
|
)
|
||||||
if platCfg == nil {
|
if platCfg == nil {
|
||||||
return nil, fmt.Errorf("twitch config was not found")
|
return nil, fmt.Errorf("twitch config was not found")
|
||||||
@@ -139,7 +143,8 @@ func newTwitch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
hadSetNewUserData := false
|
hadSetNewUserData := false
|
||||||
if platCfg.Config.Channel == "" || platCfg.Config.ClientID == "" || platCfg.Config.ClientSecret == "" {
|
if platCfg.Config.Channel == "" || platCfg.Config.ClientID == "" ||
|
||||||
|
platCfg.Config.ClientSecret == "" {
|
||||||
ok, err := setUserData(ctx, platCfg)
|
ok, err := setUserData(ctx, platCfg)
|
||||||
if !ok {
|
if !ok {
|
||||||
err := saveCfgFunc(&streamcontrol.AbstractPlatformConfig{
|
err := saveCfgFunc(&streamcontrol.AbstractPlatformConfig{
|
||||||
@@ -197,7 +202,8 @@ func newYouTube(
|
|||||||
error,
|
error,
|
||||||
) {
|
) {
|
||||||
platCfg := streamcontrol.ConvertPlatformConfig[youtube.PlatformSpecificConfig, youtube.StreamProfile](
|
platCfg := streamcontrol.ConvertPlatformConfig[youtube.PlatformSpecificConfig, youtube.StreamProfile](
|
||||||
ctx, cfg,
|
ctx,
|
||||||
|
cfg,
|
||||||
)
|
)
|
||||||
if platCfg == nil {
|
if platCfg == nil {
|
||||||
return nil, fmt.Errorf("youtube config was not found")
|
return nil, fmt.Errorf("youtube config was not found")
|
||||||
@@ -295,7 +301,9 @@ func (d *StreamD) listenOBSEvents(
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := o.GetClient(obs.GetClientOption(goobs.WithEventSubscriptions(subscriptions.InputVolumeMeters)))
|
client, err := o.GetClient(
|
||||||
|
obs.GetClientOption(goobs.WithEventSubscriptions(subscriptions.InputVolumeMeters)),
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf(ctx, "unable to get an OBS client: %v", err)
|
logger.Errorf(ctx, "unable to get an OBS client: %v", err)
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
|
@@ -36,7 +36,10 @@ func (a *platformsControllerAdapter) CheckStreamStartedByURL(
|
|||||||
case strings.Contains(destination.Hostname(), "twitch"):
|
case strings.Contains(destination.Hostname(), "twitch"):
|
||||||
platID = twitch.ID
|
platID = twitch.ID
|
||||||
default:
|
default:
|
||||||
return false, fmt.Errorf("do not know how to check if the stream started for '%s'", destination.String())
|
return false, fmt.Errorf(
|
||||||
|
"do not know how to check if the stream started for '%s'",
|
||||||
|
destination.String(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return a.CheckStreamStartedByPlatformID(ctx, platID)
|
return a.CheckStreamStartedByPlatformID(ctx, platID)
|
||||||
}
|
}
|
||||||
|
@@ -183,7 +183,12 @@ func getOBSImageBytes(
|
|||||||
) ([]byte, time.Time, error) {
|
) ([]byte, time.Time, error) {
|
||||||
img, nextUpdateAt, err := el.Source.GetImage(ctx, obsServer, el, obsState)
|
img, nextUpdateAt, err := el.Source.GetImage(ctx, obsServer, el, obsState)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, time.Now().Add(time.Second), fmt.Errorf("unable to get the image from the source: %w", err)
|
return nil, time.Now().
|
||||||
|
Add(time.Second),
|
||||||
|
fmt.Errorf(
|
||||||
|
"unable to get the image from the source: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, filter := range el.Filters {
|
for _, filter := range el.Filters {
|
||||||
@@ -292,7 +297,10 @@ func (d *StreamD) initStreamServer(ctx context.Context) (_err error) {
|
|||||||
)
|
)
|
||||||
assert(d.StreamServer != nil)
|
assert(d.StreamServer != nil)
|
||||||
defer d.notifyAboutChange(ctx, events.StreamServersChange)
|
defer d.notifyAboutChange(ctx, events.StreamServersChange)
|
||||||
return d.StreamServer.Init(ctx, sstypes.InitOptionDefaultStreamPlayerOptions(d.streamPlayerOptions()))
|
return d.StreamServer.Init(
|
||||||
|
ctx,
|
||||||
|
sstypes.InitOptionDefaultStreamPlayerOptions(d.streamPlayerOptions()),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *StreamD) streamPlayerOptions() sptypes.Options {
|
func (d *StreamD) streamPlayerOptions() sptypes.Options {
|
||||||
@@ -526,7 +534,9 @@ func (d *StreamD) SaveConfig(ctx context.Context) error {
|
|||||||
if d.GitStorage != nil {
|
if d.GitStorage != nil {
|
||||||
err = d.sendConfigViaGIT(ctx)
|
err = d.sendConfigViaGIT(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.UI.DisplayError(fmt.Errorf("unable to send the config to the remote git repository: %w", err))
|
d.UI.DisplayError(
|
||||||
|
fmt.Errorf("unable to send the config to the remote git repository: %w", err),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -550,7 +560,10 @@ func (d *StreamD) SetConfig(ctx context.Context, cfg *config.Config) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *StreamD) IsBackendEnabled(ctx context.Context, id streamcontrol.PlatformName) (bool, error) {
|
func (d *StreamD) IsBackendEnabled(
|
||||||
|
ctx context.Context,
|
||||||
|
id streamcontrol.PlatformName,
|
||||||
|
) (bool, error) {
|
||||||
return xsync.RDoR2(ctx, &d.ControllersLocker, func() (bool, error) {
|
return xsync.RDoR2(ctx, &d.ControllersLocker, func() (bool, error) {
|
||||||
switch id {
|
switch id {
|
||||||
case obs.ID:
|
case obs.ID:
|
||||||
@@ -600,7 +613,12 @@ func (d *StreamD) StartStream(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to convert the profile into OBS profile: %w", err)
|
return fmt.Errorf("unable to convert the profile into OBS profile: %w", err)
|
||||||
}
|
}
|
||||||
err = d.StreamControllers.OBS.StartStream(d.ctxForController(ctx), title, description, *profile, customArgs...)
|
err = d.StreamControllers.OBS.StartStream(
|
||||||
|
d.ctxForController(ctx),
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
*profile,
|
||||||
|
customArgs...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to start the stream on OBS: %w", err)
|
return fmt.Errorf("unable to start the stream on OBS: %w", err)
|
||||||
}
|
}
|
||||||
@@ -610,7 +628,12 @@ func (d *StreamD) StartStream(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to convert the profile into Twitch profile: %w", err)
|
return fmt.Errorf("unable to convert the profile into Twitch profile: %w", err)
|
||||||
}
|
}
|
||||||
err = d.StreamControllers.Twitch.StartStream(d.ctxForController(ctx), title, description, *profile, customArgs...)
|
err = d.StreamControllers.Twitch.StartStream(
|
||||||
|
d.ctxForController(ctx),
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
*profile,
|
||||||
|
customArgs...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to start the stream on Twitch: %w", err)
|
return fmt.Errorf("unable to start the stream on Twitch: %w", err)
|
||||||
}
|
}
|
||||||
@@ -620,7 +643,12 @@ func (d *StreamD) StartStream(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to convert the profile into YouTube profile: %w", err)
|
return fmt.Errorf("unable to convert the profile into YouTube profile: %w", err)
|
||||||
}
|
}
|
||||||
err = d.StreamControllers.YouTube.StartStream(d.ctxForController(ctx), title, description, *profile, customArgs...)
|
err = d.StreamControllers.YouTube.StartStream(
|
||||||
|
d.ctxForController(ctx),
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
*profile,
|
||||||
|
customArgs...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to start the stream on YouTube: %w", err)
|
return fmt.Errorf("unable to start the stream on YouTube: %w", err)
|
||||||
}
|
}
|
||||||
@@ -1492,9 +1520,24 @@ func (d *StreamD) UpdateStreamPlayer(
|
|||||||
disabled bool,
|
disabled bool,
|
||||||
streamPlaybackConfig sptypes.Config,
|
streamPlaybackConfig sptypes.Config,
|
||||||
) (_err error) {
|
) (_err error) {
|
||||||
logger.Debugf(ctx, "UpdateStreamPlayer(ctx, '%s', '%s', %v, %#+v)", streamID, playerType, disabled, streamPlaybackConfig)
|
logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"UpdateStreamPlayer(ctx, '%s', '%s', %v, %#+v)",
|
||||||
|
streamID,
|
||||||
|
playerType,
|
||||||
|
disabled,
|
||||||
|
streamPlaybackConfig,
|
||||||
|
)
|
||||||
defer func() {
|
defer func() {
|
||||||
logger.Debugf(ctx, "/UpdateStreamPlayer(ctx, '%s', '%s', %v, %#+v): %v", streamID, playerType, disabled, streamPlaybackConfig, _err)
|
logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"/UpdateStreamPlayer(ctx, '%s', '%s', %v, %#+v): %v",
|
||||||
|
streamID,
|
||||||
|
playerType,
|
||||||
|
disabled,
|
||||||
|
streamPlaybackConfig,
|
||||||
|
_err,
|
||||||
|
)
|
||||||
}()
|
}()
|
||||||
defer d.notifyAboutChange(ctx, events.StreamPlayersChange)
|
defer d.notifyAboutChange(ctx, events.StreamPlayersChange)
|
||||||
var result *multierror.Error
|
var result *multierror.Error
|
||||||
|
@@ -74,7 +74,11 @@ func (p *Panel) ShowErrorReports() {
|
|||||||
|
|
||||||
content := container.NewVBox()
|
content := container.NewVBox()
|
||||||
if len(reports) == 0 {
|
if len(reports) == 0 {
|
||||||
content.Add(widget.NewRichTextWithText("No significant errors were reported since the application was started, yet..."))
|
content.Add(
|
||||||
|
widget.NewRichTextWithText(
|
||||||
|
"No significant errors were reported since the application was started, yet...",
|
||||||
|
),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
for _, report := range reports {
|
for _, report := range reports {
|
||||||
errLabel := report.Error.Error()
|
errLabel := report.Error.Error()
|
||||||
|
@@ -52,7 +52,9 @@ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
|||||||
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=
|
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=
|
||||||
-----END OPENSSH PRIVATE KEY-----`)
|
-----END OPENSSH PRIVATE KEY-----`)
|
||||||
|
|
||||||
gitInstruction := widget.NewRichTextFromMarkdown("We can sync the configuration among all of your devices via a git repository. To get a git repository you may, for example, use GitHub; but never use public repositories, always use private ones (because the repository will contain all the access credentials to YouTube/Twitch/whatever).")
|
gitInstruction := widget.NewRichTextFromMarkdown(
|
||||||
|
"We can sync the configuration among all of your devices via a git repository. To get a git repository you may, for example, use GitHub; but never use public repositories, always use private ones (because the repository will contain all the access credentials to YouTube/Twitch/whatever).",
|
||||||
|
)
|
||||||
gitInstruction.Wrapping = fyne.TextWrapWord
|
gitInstruction.Wrapping = fyne.TextWrapWord
|
||||||
|
|
||||||
w.SetContent(container.NewBorder(
|
w.SetContent(container.NewBorder(
|
||||||
|
@@ -149,7 +149,14 @@ func (p *Panel) updateMonitorPageImagesNoLock(
|
|||||||
if !changed && lastWinSize == winSize && lastOrientation == orientation {
|
if !changed && lastWinSize == winSize && lastOrientation == orientation {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logger.Tracef(ctx, "updating the image '%s': %v %#+v %#+v", el.ElementName, changed, lastWinSize, winSize)
|
logger.Tracef(
|
||||||
|
ctx,
|
||||||
|
"updating the image '%s': %v %#+v %#+v",
|
||||||
|
el.ElementName,
|
||||||
|
changed,
|
||||||
|
lastWinSize,
|
||||||
|
winSize,
|
||||||
|
)
|
||||||
imgSize := image.Point{
|
imgSize := image.Point{
|
||||||
X: int(winSize.Width * float32(el.Width) / 100),
|
X: int(winSize.Width * float32(el.Width) / 100),
|
||||||
Y: int(winSize.Height * float32(el.Height) / 100),
|
Y: int(winSize.Height * float32(el.Height) / 100),
|
||||||
@@ -192,7 +199,13 @@ func (p *Panel) updateMonitorPageImagesNoLock(
|
|||||||
if !changed && lastWinSize == winSize && lastOrientation == orientation {
|
if !changed && lastWinSize == winSize && lastOrientation == orientation {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logger.Tracef(ctx, "updating the screenshot image: %v %#+v %#+v", changed, lastWinSize, winSize)
|
logger.Tracef(
|
||||||
|
ctx,
|
||||||
|
"updating the screenshot image: %v %#+v %#+v",
|
||||||
|
changed,
|
||||||
|
lastWinSize,
|
||||||
|
winSize,
|
||||||
|
)
|
||||||
winSize := image.Point{X: int(winSize.Width), Y: int(winSize.Height)}
|
winSize := image.Point{X: int(winSize.Width), Y: int(winSize.Height)}
|
||||||
img = imgFillTo(
|
img = imgFillTo(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -366,7 +379,10 @@ func (p *Panel) newMonitorSettingsWindow(ctx context.Context) {
|
|||||||
deleteButton := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() {
|
deleteButton := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() {
|
||||||
w := dialog.NewConfirm(
|
w := dialog.NewConfirm(
|
||||||
fmt.Sprintf("Delete monitor element '%s'?", name),
|
fmt.Sprintf("Delete monitor element '%s'?", name),
|
||||||
fmt.Sprintf("Are you sure you want to delete the element '%s' from the Monitor page?", name),
|
fmt.Sprintf(
|
||||||
|
"Are you sure you want to delete the element '%s' from the Monitor page?",
|
||||||
|
name,
|
||||||
|
),
|
||||||
func(b bool) {
|
func(b bool) {
|
||||||
if !b {
|
if !b {
|
||||||
return
|
return
|
||||||
@@ -484,7 +500,9 @@ func (p *Panel) editMonitorElementWindow(
|
|||||||
SceneName: &sceneName,
|
SceneName: &sceneName,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.DisplayError(fmt.Errorf("unable to get the list of items of scene '%s': %w", sceneName, err))
|
p.DisplayError(
|
||||||
|
fmt.Errorf("unable to get the list of items of scene '%s': %w", sceneName, err),
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, item := range resp.SceneItems {
|
for _, item := range resp.SceneItems {
|
||||||
@@ -649,7 +667,9 @@ func (p *Panel) editMonitorElementWindow(
|
|||||||
brightness.SetText(fmt.Sprintf("%f", brightnessValue))
|
brightness.SetText(fmt.Sprintf("%f", brightnessValue))
|
||||||
|
|
||||||
obsVideoUpdateInterval := xfyne.NewNumericalEntry()
|
obsVideoUpdateInterval := xfyne.NewNumericalEntry()
|
||||||
obsVideoUpdateInterval.SetText(fmt.Sprintf("%v", time.Duration(obsVideoSource.UpdateInterval).Seconds()))
|
obsVideoUpdateInterval.SetText(
|
||||||
|
fmt.Sprintf("%v", time.Duration(obsVideoSource.UpdateInterval).Seconds()),
|
||||||
|
)
|
||||||
obsVideoUpdateInterval.OnChanged = func(s string) {
|
obsVideoUpdateInterval.OnChanged = func(s string) {
|
||||||
if s == "" || s == "-" {
|
if s == "" || s == "-" {
|
||||||
s = "0.2"
|
s = "0.2"
|
||||||
@@ -767,7 +787,15 @@ func (p *Panel) editMonitorElementWindow(
|
|||||||
widget.NewLabel("Source:"),
|
widget.NewLabel("Source:"),
|
||||||
sourceOBSVideoSelect,
|
sourceOBSVideoSelect,
|
||||||
widget.NewLabel("Source image size (use '0' for preserving the original size or ratio):"),
|
widget.NewLabel("Source image size (use '0' for preserving the original size or ratio):"),
|
||||||
container.NewHBox(widget.NewLabel("X:"), sourceWidth, widget.NewLabel(`px`), widget.NewSeparator(), widget.NewLabel("Y:"), sourceHeight, widget.NewLabel(`px`)),
|
container.NewHBox(
|
||||||
|
widget.NewLabel("X:"),
|
||||||
|
sourceWidth,
|
||||||
|
widget.NewLabel(`px`),
|
||||||
|
widget.NewSeparator(),
|
||||||
|
widget.NewLabel("Y:"),
|
||||||
|
sourceHeight,
|
||||||
|
widget.NewLabel(`px`),
|
||||||
|
),
|
||||||
widget.NewLabel("Format:"),
|
widget.NewLabel("Format:"),
|
||||||
imageFormatSelect,
|
imageFormatSelect,
|
||||||
widget.NewLabel("Update interval:"),
|
widget.NewLabel("Update interval:"),
|
||||||
@@ -779,7 +807,9 @@ func (p *Panel) editMonitorElementWindow(
|
|||||||
})
|
})
|
||||||
|
|
||||||
obsVolumeUpdateInterval := xfyne.NewNumericalEntry()
|
obsVolumeUpdateInterval := xfyne.NewNumericalEntry()
|
||||||
obsVolumeUpdateInterval.SetText(fmt.Sprintf("%v", time.Duration(obsVideoSource.UpdateInterval).Seconds()))
|
obsVolumeUpdateInterval.SetText(
|
||||||
|
fmt.Sprintf("%v", time.Duration(obsVideoSource.UpdateInterval).Seconds()),
|
||||||
|
)
|
||||||
obsVolumeUpdateInterval.OnChanged = func(s string) {
|
obsVolumeUpdateInterval.OnChanged = func(s string) {
|
||||||
if s == "" || s == "-" {
|
if s == "" || s == "-" {
|
||||||
s = "0.2"
|
s = "0.2"
|
||||||
@@ -796,7 +826,11 @@ func (p *Panel) editMonitorElementWindow(
|
|||||||
if volumeColorActiveParsed, err = colorx.Parse(obsVolumeSource.ColorActive); err != nil {
|
if volumeColorActiveParsed, err = colorx.Parse(obsVolumeSource.ColorActive); err != nil {
|
||||||
volumeColorActiveParsed = color.RGBA{R: 0, G: 255, B: 0, A: 255}
|
volumeColorActiveParsed = color.RGBA{R: 0, G: 255, B: 0, A: 255}
|
||||||
}
|
}
|
||||||
volumeColorActive := colorpicker.NewColorSelectModalRect(w, fyne.NewSize(30, 20), volumeColorActiveParsed)
|
volumeColorActive := colorpicker.NewColorSelectModalRect(
|
||||||
|
w,
|
||||||
|
fyne.NewSize(30, 20),
|
||||||
|
volumeColorActiveParsed,
|
||||||
|
)
|
||||||
volumeColorActive.SetOnChange(func(c color.Color) {
|
volumeColorActive.SetOnChange(func(c color.Color) {
|
||||||
r32, g32, b32, a32 := c.RGBA()
|
r32, g32, b32, a32 := c.RGBA()
|
||||||
r8, g8, b8, a8 := uint8(r32>>8), uint8(g32>>8), uint8(b32>>8), uint8(a32>>8)
|
r8, g8, b8, a8 := uint8(r32>>8), uint8(g32>>8), uint8(b32>>8), uint8(a32>>8)
|
||||||
@@ -807,7 +841,11 @@ func (p *Panel) editMonitorElementWindow(
|
|||||||
if volumeColorPassiveParsed, err = colorx.Parse(obsVolumeSource.ColorPassive); err != nil {
|
if volumeColorPassiveParsed, err = colorx.Parse(obsVolumeSource.ColorPassive); err != nil {
|
||||||
volumeColorPassiveParsed = color.RGBA{R: 0, G: 0, B: 0, A: 0}
|
volumeColorPassiveParsed = color.RGBA{R: 0, G: 0, B: 0, A: 0}
|
||||||
}
|
}
|
||||||
volumeColorPassive := colorpicker.NewColorSelectModalRect(w, fyne.NewSize(30, 20), volumeColorPassiveParsed)
|
volumeColorPassive := colorpicker.NewColorSelectModalRect(
|
||||||
|
w,
|
||||||
|
fyne.NewSize(30, 20),
|
||||||
|
volumeColorPassiveParsed,
|
||||||
|
)
|
||||||
volumeColorPassive.SetOnChange(func(c color.Color) {
|
volumeColorPassive.SetOnChange(func(c color.Color) {
|
||||||
r32, g32, b32, a32 := c.RGBA()
|
r32, g32, b32, a32 := c.RGBA()
|
||||||
r8, g8, b8, a8 := uint8(r32>>8), uint8(g32>>8), uint8(b32>>8), uint8(a32>>8)
|
r8, g8, b8, a8 := uint8(r32>>8), uint8(g32>>8), uint8(b32>>8), uint8(a32>>8)
|
||||||
@@ -884,11 +922,33 @@ func (p *Panel) editMonitorElementWindow(
|
|||||||
widget.NewLabel("Z-Index / layer:"),
|
widget.NewLabel("Z-Index / layer:"),
|
||||||
zIndex,
|
zIndex,
|
||||||
widget.NewLabel("Display size:"),
|
widget.NewLabel("Display size:"),
|
||||||
container.NewHBox(widget.NewLabel("X:"), displayWidth, widget.NewLabel(`%`), widget.NewSeparator(), widget.NewLabel("Y:"), displayHeight, widget.NewLabel(`%`)),
|
container.NewHBox(
|
||||||
|
widget.NewLabel("X:"),
|
||||||
|
displayWidth,
|
||||||
|
widget.NewLabel(`%`),
|
||||||
|
widget.NewSeparator(),
|
||||||
|
widget.NewLabel("Y:"),
|
||||||
|
displayHeight,
|
||||||
|
widget.NewLabel(`%`),
|
||||||
|
),
|
||||||
widget.NewLabel("Align:"),
|
widget.NewLabel("Align:"),
|
||||||
container.NewHBox(widget.NewLabel("X:"), alignX, widget.NewSeparator(), widget.NewLabel("Y:"), alignY),
|
container.NewHBox(
|
||||||
|
widget.NewLabel("X:"),
|
||||||
|
alignX,
|
||||||
|
widget.NewSeparator(),
|
||||||
|
widget.NewLabel("Y:"),
|
||||||
|
alignY,
|
||||||
|
),
|
||||||
widget.NewLabel("Offset:"),
|
widget.NewLabel("Offset:"),
|
||||||
container.NewHBox(widget.NewLabel("X:"), offsetX, widget.NewLabel(`%`), widget.NewSeparator(), widget.NewLabel("Y:"), offsetY, widget.NewLabel(`%`)),
|
container.NewHBox(
|
||||||
|
widget.NewLabel("X:"),
|
||||||
|
offsetX,
|
||||||
|
widget.NewLabel(`%`),
|
||||||
|
widget.NewSeparator(),
|
||||||
|
widget.NewLabel("Y:"),
|
||||||
|
offsetY,
|
||||||
|
widget.NewLabel(`%`),
|
||||||
|
),
|
||||||
widget.NewLabel("Quality:"),
|
widget.NewLabel("Quality:"),
|
||||||
isLossless,
|
isLossless,
|
||||||
imageQuality,
|
imageQuality,
|
||||||
|
@@ -248,7 +248,11 @@ func (p *Panel) LazyInitStreamD(ctx context.Context) (_err error) {
|
|||||||
|
|
||||||
if p.Config.RemoteStreamDAddr != "" {
|
if p.Config.RemoteStreamDAddr != "" {
|
||||||
if err := p.initRemoteStreamD(ctx); err != nil {
|
if err := p.initRemoteStreamD(ctx); err != nil {
|
||||||
return fmt.Errorf("unable to initialize the remote stream controller '%s': %w", p.Config.RemoteStreamDAddr, err)
|
return fmt.Errorf(
|
||||||
|
"unable to initialize the remote stream controller '%s': %w",
|
||||||
|
p.Config.RemoteStreamDAddr,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := p.initBuiltinStreamD(ctx); err != nil {
|
if err := p.initBuiltinStreamD(ctx); err != nil {
|
||||||
@@ -312,7 +316,12 @@ func (p *Panel) Loop(ctx context.Context, opts ...LoopOption) error {
|
|||||||
p.setStatusFunc("Connecting...")
|
p.setStatusFunc("Connecting...")
|
||||||
err := p.startOAuthListenerForRemoteStreamD(ctx, streamD)
|
err := p.startOAuthListenerForRemoteStreamD(ctx, streamD)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.setStatusFunc(fmt.Sprintf("Connection failed, please restart the application.\n\nError: %v", err))
|
p.setStatusFunc(
|
||||||
|
fmt.Sprintf(
|
||||||
|
"Connection failed, please restart the application.\n\nError: %v",
|
||||||
|
err,
|
||||||
|
),
|
||||||
|
)
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
}
|
}
|
||||||
closeLoadingWindow()
|
closeLoadingWindow()
|
||||||
@@ -341,7 +350,9 @@ func (p *Panel) Loop(ctx context.Context, opts ...LoopOption) error {
|
|||||||
|
|
||||||
p.initMainWindow(ctx, initCfg.StartingPage)
|
p.initMainWindow(ctx, initCfg.StartingPage)
|
||||||
if streamDRunErr != nil {
|
if streamDRunErr != nil {
|
||||||
p.DisplayError(fmt.Errorf("unable to initialize the streaming controllers: %w", streamDRunErr))
|
p.DisplayError(
|
||||||
|
fmt.Errorf("unable to initialize the streaming controllers: %w", streamDRunErr),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Tracef(ctx, "p.rearrangeProfiles")
|
logger.Tracef(ctx, "p.rearrangeProfiles")
|
||||||
@@ -367,7 +378,10 @@ func (p *Panel) startOAuthListenerForRemoteStreamD(
|
|||||||
streamD *client.Client,
|
streamD *client.Client,
|
||||||
) error {
|
) error {
|
||||||
ctx, cancelFn := context.WithCancel(ctx)
|
ctx, cancelFn := context.WithCancel(ctx)
|
||||||
receiver, listenPort, err := oauthhandler.NewCodeReceiver(ctx, p.Config.OAuth.ListenPorts.Twitch)
|
receiver, listenPort, err := oauthhandler.NewCodeReceiver(
|
||||||
|
ctx,
|
||||||
|
p.Config.OAuth.ListenPorts.Twitch,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cancelFn()
|
cancelFn()
|
||||||
return fmt.Errorf("unable to start listener for OAuth responses: %w", err)
|
return fmt.Errorf("unable to start listener for OAuth responses: %w", err)
|
||||||
@@ -401,7 +415,13 @@ func (p *Panel) startOAuthListenerForRemoteStreamD(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := p.openBrowser(ctx, req.GetAuthURL(), "It is required to confirm access in Twitch/YouTube using browser"); err != nil {
|
if err := p.openBrowser(ctx, req.GetAuthURL(), "It is required to confirm access in Twitch/YouTube using browser"); err != nil {
|
||||||
p.DisplayError(fmt.Errorf("unable to open browser with URL '%s': %w", req.GetAuthURL(), err))
|
p.DisplayError(
|
||||||
|
fmt.Errorf(
|
||||||
|
"unable to open browser with URL '%s': %w",
|
||||||
|
req.GetAuthURL(),
|
||||||
|
err,
|
||||||
|
),
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,7 +440,13 @@ func (p *Panel) startOAuthListenerForRemoteStreamD(
|
|||||||
Code: code,
|
Code: code,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.DisplayError(fmt.Errorf("unable to submit the oauth code of '%s': %w", req.GetPlatID(), err))
|
p.DisplayError(
|
||||||
|
fmt.Errorf(
|
||||||
|
"unable to submit the oauth code of '%s': %w",
|
||||||
|
req.GetPlatID(),
|
||||||
|
err,
|
||||||
|
),
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -626,13 +652,19 @@ func (p *Panel) OnSubmittedOAuthCode(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Panel) OAuthHandlerTwitch(ctx context.Context, arg oauthhandler.OAuthHandlerArgument) error {
|
func (p *Panel) OAuthHandlerTwitch(
|
||||||
|
ctx context.Context,
|
||||||
|
arg oauthhandler.OAuthHandlerArgument,
|
||||||
|
) error {
|
||||||
logger.Infof(ctx, "OAuthHandlerTwitch: %#+v", arg)
|
logger.Infof(ctx, "OAuthHandlerTwitch: %#+v", arg)
|
||||||
defer logger.Infof(ctx, "/OAuthHandlerTwitch")
|
defer logger.Infof(ctx, "/OAuthHandlerTwitch")
|
||||||
return p.oauthHandler(ctx, twitch.ID, arg)
|
return p.oauthHandler(ctx, twitch.ID, arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Panel) OAuthHandlerYouTube(ctx context.Context, arg oauthhandler.OAuthHandlerArgument) error {
|
func (p *Panel) OAuthHandlerYouTube(
|
||||||
|
ctx context.Context,
|
||||||
|
arg oauthhandler.OAuthHandlerArgument,
|
||||||
|
) error {
|
||||||
logger.Infof(ctx, "OAuthHandlerYouTube: %#+v", arg)
|
logger.Infof(ctx, "OAuthHandlerYouTube: %#+v", arg)
|
||||||
defer logger.Infof(ctx, "/OAuthHandlerYouTube")
|
defer logger.Infof(ctx, "/OAuthHandlerYouTube")
|
||||||
return p.oauthHandler(ctx, youtube.ID, arg)
|
return p.oauthHandler(ctx, youtube.ID, arg)
|
||||||
@@ -657,7 +689,11 @@ func (p *Panel) oauthHandler(
|
|||||||
return fmt.Errorf("unable to open browser with URL '%s': %w", arg.AuthURL, err)
|
return fmt.Errorf("unable to open browser with URL '%s': %w", arg.AuthURL, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Infof(ctx, "Your browser has been launched (URL: %s).\nPlease approve the permissions.\n", arg.AuthURL)
|
logger.Infof(
|
||||||
|
ctx,
|
||||||
|
"Your browser has been launched (URL: %s).\nPlease approve the permissions.\n",
|
||||||
|
arg.AuthURL,
|
||||||
|
)
|
||||||
|
|
||||||
// Wait for the web server to get the code.
|
// Wait for the web server to get the code.
|
||||||
code := <-codeCh
|
code := <-codeCh
|
||||||
@@ -685,7 +721,12 @@ func (p *Panel) openBrowser(
|
|||||||
|
|
||||||
if p.Config.Browser.Command != "" {
|
if p.Config.Browser.Command != "" {
|
||||||
args := []string{p.Config.Browser.Command, url}
|
args := []string{p.Config.Browser.Command, url}
|
||||||
logger.Debugf(ctx, "the browser command is configured to be '%s', so running '%s'", p.Config.Browser.Command, strings.Join(args, " "))
|
logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"the browser command is configured to be '%s', so running '%s'",
|
||||||
|
p.Config.Browser.Command,
|
||||||
|
strings.Join(args, " "),
|
||||||
|
)
|
||||||
return exec.Command(args[0], args[1:]...).Start()
|
return exec.Command(args[0], args[1:]...).Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -759,7 +800,9 @@ func (p *Panel) InputTwitchUserInfo(
|
|||||||
resizeWindow(w, fyne.NewSize(600, 200))
|
resizeWindow(w, fyne.NewSize(600, 200))
|
||||||
|
|
||||||
channelField := widget.NewEntry()
|
channelField := widget.NewEntry()
|
||||||
channelField.SetPlaceHolder("channel ID (copy&paste it from the browser: https://www.twitch.tv/<the channel ID is here>)")
|
channelField.SetPlaceHolder(
|
||||||
|
"channel ID (copy&paste it from the browser: https://www.twitch.tv/<the channel ID is here>)",
|
||||||
|
)
|
||||||
clientIDField := widget.NewEntry()
|
clientIDField := widget.NewEntry()
|
||||||
clientIDField.SetPlaceHolder("client ID")
|
clientIDField.SetPlaceHolder("client ID")
|
||||||
clientSecretField := widget.NewEntry()
|
clientSecretField := widget.NewEntry()
|
||||||
@@ -767,7 +810,10 @@ func (p *Panel) InputTwitchUserInfo(
|
|||||||
instructionText := widget.NewRichText(
|
instructionText := widget.NewRichText(
|
||||||
&widget.TextSegment{Text: "Go to\n", Style: widget.RichTextStyle{Inline: true}},
|
&widget.TextSegment{Text: "Go to\n", Style: widget.RichTextStyle{Inline: true}},
|
||||||
&widget.HyperlinkSegment{Text: twitchAppsCreateLink.String(), URL: twitchAppsCreateLink},
|
&widget.HyperlinkSegment{Text: twitchAppsCreateLink.String(), URL: twitchAppsCreateLink},
|
||||||
&widget.TextSegment{Text: `,` + "\n" + `create an application (enter "http://localhost:8091/" as the "OAuth Redirect URLs" value), then click "Manage" then "New Secret", and copy&paste client ID and client secret.`, Style: widget.RichTextStyle{Inline: true}},
|
&widget.TextSegment{
|
||||||
|
Text: `,` + "\n" + `create an application (enter "http://localhost:8091/" as the "OAuth Redirect URLs" value), then click "Manage" then "New Secret", and copy&paste client ID and client secret.`,
|
||||||
|
Style: widget.RichTextStyle{Inline: true},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
instructionText.Wrapping = fyne.TextWrapWord
|
instructionText.Wrapping = fyne.TextWrapWord
|
||||||
|
|
||||||
@@ -810,7 +856,9 @@ func (p *Panel) InputTwitchUserInfo(
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var youtubeCredentialsCreateLink, _ = url.Parse("https://console.cloud.google.com/apis/credentials/oauthclient")
|
var youtubeCredentialsCreateLink, _ = url.Parse(
|
||||||
|
"https://console.cloud.google.com/apis/credentials/oauthclient",
|
||||||
|
)
|
||||||
|
|
||||||
func (p *Panel) InputYouTubeUserInfo(
|
func (p *Panel) InputYouTubeUserInfo(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
@@ -825,10 +873,22 @@ func (p *Panel) InputYouTubeUserInfo(
|
|||||||
clientSecretField.SetPlaceHolder("client secret")
|
clientSecretField.SetPlaceHolder("client secret")
|
||||||
instructionText := widget.NewRichText(
|
instructionText := widget.NewRichText(
|
||||||
&widget.TextSegment{Text: "Go to\n", Style: widget.RichTextStyle{Inline: true}},
|
&widget.TextSegment{Text: "Go to\n", Style: widget.RichTextStyle{Inline: true}},
|
||||||
&widget.HyperlinkSegment{Text: youtubeCredentialsCreateLink.String(), URL: youtubeCredentialsCreateLink},
|
&widget.HyperlinkSegment{
|
||||||
&widget.TextSegment{Text: `,` + "\n" + `configure "consent screen" (note: you may add yourself into Test Users to avoid problems further on, and don't forget to add "YouTube Data API v3" scopes) and go back to` + "\n", Style: widget.RichTextStyle{Inline: true}},
|
Text: youtubeCredentialsCreateLink.String(),
|
||||||
&widget.HyperlinkSegment{Text: youtubeCredentialsCreateLink.String(), URL: youtubeCredentialsCreateLink},
|
URL: youtubeCredentialsCreateLink,
|
||||||
&widget.TextSegment{Text: `,` + "\n" + `choose "Desktop app", confirm and copy&paste client ID and client secret.`, Style: widget.RichTextStyle{Inline: true}},
|
},
|
||||||
|
&widget.TextSegment{
|
||||||
|
Text: `,` + "\n" + `configure "consent screen" (note: you may add yourself into Test Users to avoid problems further on, and don't forget to add "YouTube Data API v3" scopes) and go back to` + "\n",
|
||||||
|
Style: widget.RichTextStyle{Inline: true},
|
||||||
|
},
|
||||||
|
&widget.HyperlinkSegment{
|
||||||
|
Text: youtubeCredentialsCreateLink.String(),
|
||||||
|
URL: youtubeCredentialsCreateLink,
|
||||||
|
},
|
||||||
|
&widget.TextSegment{
|
||||||
|
Text: `,` + "\n" + `choose "Desktop app", confirm and copy&paste client ID and client secret.`,
|
||||||
|
Style: widget.RichTextStyle{Inline: true},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
instructionText.Wrapping = fyne.TextWrapWord
|
instructionText.Wrapping = fyne.TextWrapWord
|
||||||
|
|
||||||
@@ -879,11 +939,23 @@ func (p *Panel) profileCreateOrUpdate(ctx context.Context, profile Profile) erro
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
cfg.Backends[platformName].StreamProfiles[profile.Name] = platformProfile
|
cfg.Backends[platformName].StreamProfiles[profile.Name] = platformProfile
|
||||||
logger.Tracef(ctx, "profileCreateOrUpdate(%s): cfg.Backends[%s].StreamProfiles[%s] = %#+v", profile.Name, platformName, profile.Name, platformProfile)
|
logger.Tracef(
|
||||||
|
ctx,
|
||||||
|
"profileCreateOrUpdate(%s): cfg.Backends[%s].StreamProfiles[%s] = %#+v",
|
||||||
|
profile.Name,
|
||||||
|
platformName,
|
||||||
|
profile.Name,
|
||||||
|
platformProfile,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
cfg.ProfileMetadata[profile.Name] = profile.ProfileMetadata
|
cfg.ProfileMetadata[profile.Name] = profile.ProfileMetadata
|
||||||
|
|
||||||
logger.Tracef(ctx, "profileCreateOrUpdate(%s): cfg.Backends == %#+v", profile.Name, cfg.Backends)
|
logger.Tracef(
|
||||||
|
ctx,
|
||||||
|
"profileCreateOrUpdate(%s): cfg.Backends == %#+v",
|
||||||
|
profile.Name,
|
||||||
|
cfg.Backends,
|
||||||
|
)
|
||||||
|
|
||||||
err = p.StreamD.SetConfig(ctx, cfg)
|
err = p.StreamD.SetConfig(ctx, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1002,7 +1074,11 @@ func (p *Panel) refilterProfiles(ctx context.Context) {
|
|||||||
if p.filterValue == "" {
|
if p.filterValue == "" {
|
||||||
p.profilesOrderFiltered = p.profilesOrderFiltered[:len(p.profilesOrder)]
|
p.profilesOrderFiltered = p.profilesOrderFiltered[:len(p.profilesOrder)]
|
||||||
copy(p.profilesOrderFiltered, p.profilesOrder)
|
copy(p.profilesOrderFiltered, p.profilesOrder)
|
||||||
logger.Tracef(ctx, "refilterProfiles(): profilesOrderFiltered <- p.profilesOrder: %#+v", p.profilesOrder)
|
logger.Tracef(
|
||||||
|
ctx,
|
||||||
|
"refilterProfiles(): profilesOrderFiltered <- p.profilesOrder: %#+v",
|
||||||
|
p.profilesOrder,
|
||||||
|
)
|
||||||
logger.Tracef(ctx, "refilterProfiles(): p.profilesListWidget.Refresh()")
|
logger.Tracef(ctx, "refilterProfiles(): p.profilesListWidget.Refresh()")
|
||||||
p.profilesListWidget.Refresh()
|
p.profilesListWidget.Refresh()
|
||||||
return
|
return
|
||||||
@@ -1037,7 +1113,12 @@ func (p *Panel) refilterProfiles(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if titleMatch || subValueMatch {
|
if titleMatch || subValueMatch {
|
||||||
logger.Tracef(ctx, "refilterProfiles(): profilesOrderFiltered[%3d] = %s", len(p.profilesOrderFiltered), profileName)
|
logger.Tracef(
|
||||||
|
ctx,
|
||||||
|
"refilterProfiles(): profilesOrderFiltered[%3d] = %s",
|
||||||
|
len(p.profilesOrderFiltered),
|
||||||
|
profileName,
|
||||||
|
)
|
||||||
p.profilesOrderFiltered = append(p.profilesOrderFiltered, profileName)
|
p.profilesOrderFiltered = append(p.profilesOrderFiltered, profileName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1153,10 +1234,18 @@ func (p *Panel) openSettingsWindow(ctx context.Context) error {
|
|||||||
logger.Debugf(ctx, "current OBS config: %#+v", obsCfg)
|
logger.Debugf(ctx, "current OBS config: %#+v", obsCfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdBeforeStartStream, _ := cfg.Backends[obs.ID].GetCustomString(config.CustomConfigKeyBeforeStreamStart)
|
cmdBeforeStartStream, _ := cfg.Backends[obs.ID].GetCustomString(
|
||||||
cmdBeforeStopStream, _ := cfg.Backends[obs.ID].GetCustomString(config.CustomConfigKeyBeforeStreamStop)
|
config.CustomConfigKeyBeforeStreamStart,
|
||||||
cmdAfterStartStream, _ := cfg.Backends[obs.ID].GetCustomString(config.CustomConfigKeyAfterStreamStart)
|
)
|
||||||
cmdAfterStopStream, _ := cfg.Backends[obs.ID].GetCustomString(config.CustomConfigKeyAfterStreamStop)
|
cmdBeforeStopStream, _ := cfg.Backends[obs.ID].GetCustomString(
|
||||||
|
config.CustomConfigKeyBeforeStreamStop,
|
||||||
|
)
|
||||||
|
cmdAfterStartStream, _ := cfg.Backends[obs.ID].GetCustomString(
|
||||||
|
config.CustomConfigKeyAfterStreamStart,
|
||||||
|
)
|
||||||
|
cmdAfterStopStream, _ := cfg.Backends[obs.ID].GetCustomString(
|
||||||
|
config.CustomConfigKeyAfterStreamStop,
|
||||||
|
)
|
||||||
|
|
||||||
beforeStartStreamCommandEntry := widget.NewEntry()
|
beforeStartStreamCommandEntry := widget.NewEntry()
|
||||||
beforeStartStreamCommandEntry.SetText(cmdBeforeStartStream)
|
beforeStartStreamCommandEntry.SetText(cmdBeforeStartStream)
|
||||||
@@ -1210,7 +1299,9 @@ func (p *Panel) openSettingsWindow(ctx context.Context) error {
|
|||||||
w.Close()
|
w.Close()
|
||||||
})
|
})
|
||||||
|
|
||||||
templateInstruction := widget.NewRichTextFromMarkdown("Commands support [Go templates](https://pkg.go.dev/text/template) with two custom functions predefined:\n* `devnull` nullifies any inputs\n* `httpGET` makes an HTTP GET request and inserts the response body")
|
templateInstruction := widget.NewRichTextFromMarkdown(
|
||||||
|
"Commands support [Go templates](https://pkg.go.dev/text/template) with two custom functions predefined:\n* `devnull` nullifies any inputs\n* `httpGET` makes an HTTP GET request and inserts the response body",
|
||||||
|
)
|
||||||
templateInstruction.Wrapping = fyne.TextWrapWord
|
templateInstruction.Wrapping = fyne.TextWrapWord
|
||||||
|
|
||||||
obsAlreadyLoggedIn := widget.NewLabel("")
|
obsAlreadyLoggedIn := widget.NewLabel("")
|
||||||
@@ -1368,7 +1459,10 @@ func (p *Panel) openSettingsWindow(ctx context.Context) error {
|
|||||||
displayIDSelector,
|
displayIDSelector,
|
||||||
widget.NewLabel("Crop to:"),
|
widget.NewLabel("Crop to:"),
|
||||||
container.NewHBox(
|
container.NewHBox(
|
||||||
screenshotCropXEntry, screenshotCropYEntry, screenshotCropWEntry, screenshotCropHEntry,
|
screenshotCropXEntry,
|
||||||
|
screenshotCropYEntry,
|
||||||
|
screenshotCropWEntry,
|
||||||
|
screenshotCropHEntry,
|
||||||
),
|
),
|
||||||
widget.NewSeparator(),
|
widget.NewSeparator(),
|
||||||
widget.NewSeparator(),
|
widget.NewSeparator(),
|
||||||
@@ -1512,7 +1606,9 @@ func (p *Panel) getUpdatedStatus_backends_noLock(ctx context.Context) {
|
|||||||
} {
|
} {
|
||||||
isEnabled, err := p.StreamD.IsBackendEnabled(ctx, backendID)
|
isEnabled, err := p.StreamD.IsBackendEnabled(ctx, backendID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.ReportError(fmt.Errorf("unable to get info if backend '%s' is enabled: %w", backendID, err))
|
p.ReportError(
|
||||||
|
fmt.Errorf("unable to get info if backend '%s' is enabled: %w", backendID, err),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
backendEnabled[backendID] = isEnabled
|
backendEnabled[backendID] = isEnabled
|
||||||
}
|
}
|
||||||
@@ -1629,7 +1725,12 @@ func (p *Panel) getUpdatedStatus_startStopStreamButton_noLock(ctx context.Contex
|
|||||||
logger.Tracef(ctx, "ytStreamStatus == %#+v", ytStreamStatus)
|
logger.Tracef(ctx, "ytStreamStatus == %#+v", ytStreamStatus)
|
||||||
|
|
||||||
if d, ok := ytStreamStatus.CustomData.(youtube.StreamStatusCustomData); ok {
|
if d, ok := ytStreamStatus.CustomData.(youtube.StreamStatusCustomData); ok {
|
||||||
logger.Tracef(ctx, "len(d.UpcomingBroadcasts) == %d; len(d.Streams) == %d", len(d.UpcomingBroadcasts), len(d.Streams))
|
logger.Tracef(
|
||||||
|
ctx,
|
||||||
|
"len(d.UpcomingBroadcasts) == %d; len(d.Streams) == %d",
|
||||||
|
len(d.UpcomingBroadcasts),
|
||||||
|
len(d.Streams),
|
||||||
|
)
|
||||||
if len(d.UpcomingBroadcasts) != 0 {
|
if len(d.UpcomingBroadcasts) != 0 {
|
||||||
p.startStopButton.Enable()
|
p.startStopButton.Enable()
|
||||||
}
|
}
|
||||||
@@ -1694,18 +1795,30 @@ func (p *Panel) initMainWindow(
|
|||||||
profileControl.Add(button)
|
profileControl.Add(button)
|
||||||
}
|
}
|
||||||
|
|
||||||
p.setupStreamButton = widget.NewButtonWithIcon(setupStreamString(), theme.SettingsIcon(), func() {
|
p.setupStreamButton = widget.NewButtonWithIcon(
|
||||||
|
setupStreamString(),
|
||||||
|
theme.SettingsIcon(),
|
||||||
|
func() {
|
||||||
p.onSetupStreamButton(ctx)
|
p.onSetupStreamButton(ctx)
|
||||||
})
|
},
|
||||||
|
)
|
||||||
p.setupStreamButton.Disable()
|
p.setupStreamButton.Disable()
|
||||||
|
|
||||||
p.startStopButton = widget.NewButtonWithIcon(startStreamString(), theme.MediaRecordIcon(), func() {
|
p.startStopButton = widget.NewButtonWithIcon(
|
||||||
|
startStreamString(),
|
||||||
|
theme.MediaRecordIcon(),
|
||||||
|
func() {
|
||||||
p.onStartStopButton(ctx)
|
p.onStartStopButton(ctx)
|
||||||
})
|
},
|
||||||
|
)
|
||||||
p.startStopButton.Importance = widget.SuccessImportance
|
p.startStopButton.Importance = widget.SuccessImportance
|
||||||
p.startStopButton.Disable()
|
p.startStopButton.Disable()
|
||||||
|
|
||||||
profilesList := widget.NewList(p.profilesListLength, p.profilesListItemCreate, p.profilesListItemUpdate)
|
profilesList := widget.NewList(
|
||||||
|
p.profilesListLength,
|
||||||
|
p.profilesListItemCreate,
|
||||||
|
p.profilesListItemUpdate,
|
||||||
|
)
|
||||||
profilesList.OnSelected = func(id widget.ListItemID) {
|
profilesList.OnSelected = func(id widget.ListItemID) {
|
||||||
p.onProfilesListSelect(id)
|
p.onProfilesListSelect(id)
|
||||||
for _, button := range selectedProfileButtons {
|
for _, button := range selectedProfileButtons {
|
||||||
@@ -2116,7 +2229,9 @@ func (p *Panel) setupStreamNoLock(ctx context.Context) {
|
|||||||
} {
|
} {
|
||||||
isEnabled, err := p.StreamD.IsBackendEnabled(ctx, backendID)
|
isEnabled, err := p.StreamD.IsBackendEnabled(ctx, backendID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.DisplayError(fmt.Errorf("unable to get info if backend '%s' is enabled: %w", backendID, err))
|
p.DisplayError(
|
||||||
|
fmt.Errorf("unable to get info if backend '%s' is enabled: %w", backendID, err),
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
backendEnabled[backendID] = isEnabled
|
backendEnabled[backendID] = isEnabled
|
||||||
@@ -2268,7 +2383,9 @@ func (p *Panel) stopStreamNoLock(ctx context.Context) {
|
|||||||
} {
|
} {
|
||||||
isEnabled, err := p.StreamD.IsBackendEnabled(ctx, backendID)
|
isEnabled, err := p.StreamD.IsBackendEnabled(ctx, backendID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.DisplayError(fmt.Errorf("unable to get info if backend '%s' is enabled: %w", backendID, err))
|
p.DisplayError(
|
||||||
|
fmt.Errorf("unable to get info if backend '%s' is enabled: %w", backendID, err),
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
backendEnabled[backendID] = isEnabled
|
backendEnabled[backendID] = isEnabled
|
||||||
@@ -2627,9 +2744,13 @@ func newTagsEditor(
|
|||||||
tagsControlsContainer.Add(widget.NewSeparator())
|
tagsControlsContainer.Add(widget.NewSeparator())
|
||||||
tagsControlsContainer.Add(widget.NewSeparator())
|
tagsControlsContainer.Add(widget.NewSeparator())
|
||||||
for _, additionalButtonInfo := range additionalButtons {
|
for _, additionalButtonInfo := range additionalButtons {
|
||||||
button := widget.NewButtonWithIcon(additionalButtonInfo.Label, additionalButtonInfo.Icon, func() {
|
button := widget.NewButtonWithIcon(
|
||||||
|
additionalButtonInfo.Label,
|
||||||
|
additionalButtonInfo.Icon,
|
||||||
|
func() {
|
||||||
additionalButtonInfo.Callback(t, selectedTagsOrdered())
|
additionalButtonInfo.Callback(t, selectedTagsOrdered())
|
||||||
})
|
},
|
||||||
|
)
|
||||||
tagsControlsContainer.Add(button)
|
tagsControlsContainer.Add(button)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2655,7 +2776,11 @@ func newTagsEditor(
|
|||||||
tagLabel := tagName
|
tagLabel := tagName
|
||||||
overflown := false
|
overflown := false
|
||||||
for {
|
for {
|
||||||
size := fyne.MeasureText(tagLabel, fyne.CurrentApp().Settings().Theme().Size("text"), fyne.TextStyle{})
|
size := fyne.MeasureText(
|
||||||
|
tagLabel,
|
||||||
|
fyne.CurrentApp().Settings().Theme().Size("text"),
|
||||||
|
fyne.TextStyle{},
|
||||||
|
)
|
||||||
if size.Width < 100 {
|
if size.Width < 100 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -2732,7 +2857,9 @@ func (p *Panel) profileWindow(
|
|||||||
isEnabled, err := p.StreamD.IsBackendEnabled(ctx, backendID)
|
isEnabled, err := p.StreamD.IsBackendEnabled(ctx, backendID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.Close()
|
w.Close()
|
||||||
p.DisplayError(fmt.Errorf("unable to get info if backend '%s' is enabled: %w", backendID, err))
|
p.DisplayError(
|
||||||
|
fmt.Errorf("unable to get info if backend '%s' is enabled: %w", backendID, err),
|
||||||
|
)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
backendEnabled[backendID] = isEnabled
|
backendEnabled[backendID] = isEnabled
|
||||||
@@ -2756,7 +2883,9 @@ func (p *Panel) profileWindow(
|
|||||||
bottomContent = append(bottomContent, widget.NewRichTextFromMarkdown("# OBS:"))
|
bottomContent = append(bottomContent, widget.NewRichTextFromMarkdown("# OBS:"))
|
||||||
if backendEnabled[obs.ID] {
|
if backendEnabled[obs.ID] {
|
||||||
if platProfile := values.PerPlatform[obs.ID]; platProfile != nil {
|
if platProfile := values.PerPlatform[obs.ID]; platProfile != nil {
|
||||||
obsProfile = ptr(streamcontrol.GetPlatformSpecificConfig[obs.StreamProfile](ctx, platProfile))
|
obsProfile = ptr(
|
||||||
|
streamcontrol.GetPlatformSpecificConfig[obs.StreamProfile](ctx, platProfile),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
obsProfile = &obs.StreamProfile{}
|
obsProfile = &obs.StreamProfile{}
|
||||||
}
|
}
|
||||||
@@ -2778,7 +2907,9 @@ func (p *Panel) profileWindow(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if platProfile := values.PerPlatform[twitch.ID]; platProfile != nil {
|
if platProfile := values.PerPlatform[twitch.ID]; platProfile != nil {
|
||||||
twitchProfile = ptr(streamcontrol.GetPlatformSpecificConfig[twitch.StreamProfile](ctx, platProfile))
|
twitchProfile = ptr(
|
||||||
|
streamcontrol.GetPlatformSpecificConfig[twitch.StreamProfile](ctx, platProfile),
|
||||||
|
)
|
||||||
for _, tag := range twitchProfile.Tags {
|
for _, tag := range twitchProfile.Tags {
|
||||||
addTag(tag)
|
addTag(tag)
|
||||||
}
|
}
|
||||||
@@ -2802,9 +2933,13 @@ func (p *Panel) profileWindow(
|
|||||||
if strings.Contains(cleanTwitchCategoryName(cat.Name), text) {
|
if strings.Contains(cleanTwitchCategoryName(cat.Name), text) {
|
||||||
selectedTwitchCategoryContainer := container.NewHBox()
|
selectedTwitchCategoryContainer := container.NewHBox()
|
||||||
catName := cat.Name
|
catName := cat.Name
|
||||||
tagContainerRemoveButton := widget.NewButtonWithIcon(catName, theme.ContentAddIcon(), func() {
|
tagContainerRemoveButton := widget.NewButtonWithIcon(
|
||||||
|
catName,
|
||||||
|
theme.ContentAddIcon(),
|
||||||
|
func() {
|
||||||
twitchCategory.OnSubmitted(catName)
|
twitchCategory.OnSubmitted(catName)
|
||||||
})
|
},
|
||||||
|
)
|
||||||
selectedTwitchCategoryContainer.Add(tagContainerRemoveButton)
|
selectedTwitchCategoryContainer.Add(tagContainerRemoveButton)
|
||||||
selectTwitchCategoryBox.Add(selectedTwitchCategoryContainer)
|
selectTwitchCategoryBox.Add(selectedTwitchCategoryContainer)
|
||||||
count++
|
count++
|
||||||
@@ -2821,10 +2956,14 @@ func (p *Panel) profileWindow(
|
|||||||
setSelectedTwitchCategory := func(catName string) {
|
setSelectedTwitchCategory := func(catName string) {
|
||||||
selectedTwitchCategoryBox.RemoveAll()
|
selectedTwitchCategoryBox.RemoveAll()
|
||||||
selectedTwitchCategoryContainer := container.NewHBox()
|
selectedTwitchCategoryContainer := container.NewHBox()
|
||||||
tagContainerRemoveButton := widget.NewButtonWithIcon(catName, theme.ContentClearIcon(), func() {
|
tagContainerRemoveButton := widget.NewButtonWithIcon(
|
||||||
|
catName,
|
||||||
|
theme.ContentClearIcon(),
|
||||||
|
func() {
|
||||||
selectedTwitchCategoryBox.Remove(selectedTwitchCategoryContainer)
|
selectedTwitchCategoryBox.Remove(selectedTwitchCategoryContainer)
|
||||||
twitchProfile.CategoryName = nil
|
twitchProfile.CategoryName = nil
|
||||||
})
|
},
|
||||||
|
)
|
||||||
selectedTwitchCategoryContainer.Add(tagContainerRemoveButton)
|
selectedTwitchCategoryContainer.Add(tagContainerRemoveButton)
|
||||||
selectedTwitchCategoryBox.Add(selectedTwitchCategoryContainer)
|
selectedTwitchCategoryBox.Add(selectedTwitchCategoryContainer)
|
||||||
twitchProfile.CategoryName = &catName
|
twitchProfile.CategoryName = &catName
|
||||||
@@ -2878,7 +3017,9 @@ func (p *Panel) profileWindow(
|
|||||||
youtubeTags = append(youtubeTags, tagName)
|
youtubeTags = append(youtubeTags, tagName)
|
||||||
}
|
}
|
||||||
if platProfile := values.PerPlatform[youtube.ID]; platProfile != nil {
|
if platProfile := values.PerPlatform[youtube.ID]; platProfile != nil {
|
||||||
youtubeProfile = ptr(streamcontrol.GetPlatformSpecificConfig[youtube.StreamProfile](ctx, platProfile))
|
youtubeProfile = ptr(
|
||||||
|
streamcontrol.GetPlatformSpecificConfig[youtube.StreamProfile](ctx, platProfile),
|
||||||
|
)
|
||||||
for _, tag := range youtubeProfile.Tags {
|
for _, tag := range youtubeProfile.Tags {
|
||||||
addTag(tag)
|
addTag(tag)
|
||||||
}
|
}
|
||||||
@@ -2890,8 +3031,14 @@ func (p *Panel) profileWindow(
|
|||||||
youtubeProfile.AutoNumerate = b
|
youtubeProfile.AutoNumerate = b
|
||||||
})
|
})
|
||||||
autoNumerateCheck.SetChecked(youtubeProfile.AutoNumerate)
|
autoNumerateCheck.SetChecked(youtubeProfile.AutoNumerate)
|
||||||
autoNumerateHint := NewHintWidget(w, "When enabled, it adds the number of the stream to the stream's title.\n\nFor example 'Watching presidential debate' -> 'Watching presidential debate [#52]'.")
|
autoNumerateHint := NewHintWidget(
|
||||||
bottomContent = append(bottomContent, container.NewHBox(autoNumerateCheck, autoNumerateHint))
|
w,
|
||||||
|
"When enabled, it adds the number of the stream to the stream's title.\n\nFor example 'Watching presidential debate' -> 'Watching presidential debate [#52]'.",
|
||||||
|
)
|
||||||
|
bottomContent = append(
|
||||||
|
bottomContent,
|
||||||
|
container.NewHBox(autoNumerateCheck, autoNumerateHint),
|
||||||
|
)
|
||||||
|
|
||||||
youtubeTemplate := widget.NewEntry()
|
youtubeTemplate := widget.NewEntry()
|
||||||
youtubeTemplate.SetPlaceHolder("youtube live recording template")
|
youtubeTemplate.SetPlaceHolder("youtube live recording template")
|
||||||
@@ -2909,9 +3056,13 @@ func (p *Panel) profileWindow(
|
|||||||
if strings.Contains(cleanYoutubeRecordingName(bc.Snippet.Title), text) {
|
if strings.Contains(cleanYoutubeRecordingName(bc.Snippet.Title), text) {
|
||||||
selectedYoutubeRecordingsContainer := container.NewHBox()
|
selectedYoutubeRecordingsContainer := container.NewHBox()
|
||||||
recName := bc.Snippet.Title
|
recName := bc.Snippet.Title
|
||||||
tagContainerRemoveButton := widget.NewButtonWithIcon(recName, theme.ContentAddIcon(), func() {
|
tagContainerRemoveButton := widget.NewButtonWithIcon(
|
||||||
|
recName,
|
||||||
|
theme.ContentAddIcon(),
|
||||||
|
func() {
|
||||||
youtubeTemplate.OnSubmitted(recName)
|
youtubeTemplate.OnSubmitted(recName)
|
||||||
})
|
},
|
||||||
|
)
|
||||||
selectedYoutubeRecordingsContainer.Add(tagContainerRemoveButton)
|
selectedYoutubeRecordingsContainer.Add(tagContainerRemoveButton)
|
||||||
selectYoutubeTemplateBox.Add(selectedYoutubeRecordingsContainer)
|
selectYoutubeTemplateBox.Add(selectedYoutubeRecordingsContainer)
|
||||||
count++
|
count++
|
||||||
@@ -2929,10 +3080,14 @@ func (p *Panel) profileWindow(
|
|||||||
selectedYoutubeBroadcastBox.RemoveAll()
|
selectedYoutubeBroadcastBox.RemoveAll()
|
||||||
selectedYoutubeBroadcastContainer := container.NewHBox()
|
selectedYoutubeBroadcastContainer := container.NewHBox()
|
||||||
recName := bc.Snippet.Title
|
recName := bc.Snippet.Title
|
||||||
tagContainerRemoveButton := widget.NewButtonWithIcon(recName, theme.ContentClearIcon(), func() {
|
tagContainerRemoveButton := widget.NewButtonWithIcon(
|
||||||
|
recName,
|
||||||
|
theme.ContentClearIcon(),
|
||||||
|
func() {
|
||||||
selectedYoutubeBroadcastBox.Remove(selectedYoutubeBroadcastContainer)
|
selectedYoutubeBroadcastBox.Remove(selectedYoutubeBroadcastContainer)
|
||||||
youtubeProfile.TemplateBroadcastIDs = youtubeProfile.TemplateBroadcastIDs[:0]
|
youtubeProfile.TemplateBroadcastIDs = youtubeProfile.TemplateBroadcastIDs[:0]
|
||||||
})
|
},
|
||||||
|
)
|
||||||
selectedYoutubeBroadcastContainer.Add(tagContainerRemoveButton)
|
selectedYoutubeBroadcastContainer.Add(tagContainerRemoveButton)
|
||||||
selectedYoutubeBroadcastBox.Add(selectedYoutubeBroadcastContainer)
|
selectedYoutubeBroadcastBox.Add(selectedYoutubeBroadcastContainer)
|
||||||
youtubeProfile.TemplateBroadcastIDs = []string{bc.Id}
|
youtubeProfile.TemplateBroadcastIDs = []string{bc.Id}
|
||||||
@@ -2966,7 +3121,9 @@ func (p *Panel) profileWindow(
|
|||||||
bottomContent = append(bottomContent, youtubeTemplate)
|
bottomContent = append(bottomContent, youtubeTemplate)
|
||||||
|
|
||||||
templateTagsLabel := widget.NewLabel("Template tags:")
|
templateTagsLabel := widget.NewLabel("Template tags:")
|
||||||
templateTags := widget.NewSelect([]string{"ignore", "use as primary", "use as additional"}, func(s string) {
|
templateTags := widget.NewSelect(
|
||||||
|
[]string{"ignore", "use as primary", "use as additional"},
|
||||||
|
func(s string) {
|
||||||
switch s {
|
switch s {
|
||||||
case "ignore":
|
case "ignore":
|
||||||
youtubeProfile.TemplateTags = youtube.TemplateTagsIgnore
|
youtubeProfile.TemplateTags = youtube.TemplateTagsIgnore
|
||||||
@@ -2977,7 +3134,8 @@ func (p *Panel) profileWindow(
|
|||||||
default:
|
default:
|
||||||
p.DisplayError(fmt.Errorf("unexpected new value of 'template tags': '%s'", s))
|
p.DisplayError(fmt.Errorf("unexpected new value of 'template tags': '%s'", s))
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
)
|
||||||
switch youtubeProfile.TemplateTags {
|
switch youtubeProfile.TemplateTags {
|
||||||
case youtube.TemplateTagsUndefined, youtube.TemplateTagsIgnore:
|
case youtube.TemplateTagsUndefined, youtube.TemplateTagsIgnore:
|
||||||
templateTags.SetSelected("ignore")
|
templateTags.SetSelected("ignore")
|
||||||
@@ -2986,11 +3144,22 @@ func (p *Panel) profileWindow(
|
|||||||
case youtube.TemplateTagsUseAsAdditional:
|
case youtube.TemplateTagsUseAsAdditional:
|
||||||
templateTags.SetSelected("use as additional")
|
templateTags.SetSelected("use as additional")
|
||||||
default:
|
default:
|
||||||
p.DisplayError(fmt.Errorf("unexpected current value of 'template tags': '%s'", youtubeProfile.TemplateTags))
|
p.DisplayError(
|
||||||
|
fmt.Errorf(
|
||||||
|
"unexpected current value of 'template tags': '%s'",
|
||||||
|
youtubeProfile.TemplateTags,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
templateTags.SetSelected(youtubeProfile.TemplateTags.String())
|
templateTags.SetSelected(youtubeProfile.TemplateTags.String())
|
||||||
templateTagsHint := NewHintWidget(w, "'ignore' will ignore the tags set in the template; 'use as primary' will put the tags of the template first and then add the profile tags; 'use as additional' will put the tags of the profile first and then add the template tags")
|
templateTagsHint := NewHintWidget(
|
||||||
bottomContent = append(bottomContent, container.NewHBox(templateTagsLabel, templateTags, templateTagsHint))
|
w,
|
||||||
|
"'ignore' will ignore the tags set in the template; 'use as primary' will put the tags of the template first and then add the profile tags; 'use as additional' will put the tags of the profile first and then add the template tags",
|
||||||
|
)
|
||||||
|
bottomContent = append(
|
||||||
|
bottomContent,
|
||||||
|
container.NewHBox(templateTagsLabel, templateTags, templateTagsHint),
|
||||||
|
)
|
||||||
|
|
||||||
youtubeTagsEditor := newTagsEditor(youtubeTags, 0)
|
youtubeTagsEditor := newTagsEditor(youtubeTags, 0)
|
||||||
bottomContent = append(bottomContent, widget.NewLabel("Tags:"))
|
bottomContent = append(bottomContent, widget.NewLabel("Tags:"))
|
||||||
|
@@ -288,7 +288,14 @@ func (p *Panel) displayStreamServers(
|
|||||||
p.previousNumBytesLocker.Do(ctx, func() {
|
p.previousNumBytesLocker.Do(ctx, func() {
|
||||||
prevNumBytes := p.previousNumBytes[key]
|
prevNumBytes := p.previousNumBytes[key]
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
bwStr := bwString(srv.NumBytesProducerRead, prevNumBytes[0], srv.NumBytesConsumerWrote, prevNumBytes[1], now, p.previousNumBytesTS[key])
|
bwStr := bwString(
|
||||||
|
srv.NumBytesProducerRead,
|
||||||
|
prevNumBytes[0],
|
||||||
|
srv.NumBytesConsumerWrote,
|
||||||
|
prevNumBytes[1],
|
||||||
|
now,
|
||||||
|
p.previousNumBytesTS[key],
|
||||||
|
)
|
||||||
bwText := widget.NewRichTextWithText(bwStr)
|
bwText := widget.NewRichTextWithText(bwStr)
|
||||||
hasDynamicValue = hasDynamicValue || bwStr != ""
|
hasDynamicValue = hasDynamicValue || bwStr != ""
|
||||||
p.previousNumBytes[key] = [4]uint64{srv.NumBytesProducerRead, srv.NumBytesConsumerWrote}
|
p.previousNumBytes[key] = [4]uint64{srv.NumBytesProducerRead, srv.NumBytesConsumerWrote}
|
||||||
@@ -571,7 +578,14 @@ func (p *Panel) displayStreamDestinations(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Panel) openAddPlayerWindow(ctx context.Context) {
|
func (p *Panel) openAddPlayerWindow(ctx context.Context) {
|
||||||
p.openAddOrEditPlayerWindow(ctx, "Add player", false, sptypes.DefaultConfig(ctx), nil, p.StreamD.AddStreamPlayer)
|
p.openAddOrEditPlayerWindow(
|
||||||
|
ctx,
|
||||||
|
"Add player",
|
||||||
|
false,
|
||||||
|
sptypes.DefaultConfig(ctx),
|
||||||
|
nil,
|
||||||
|
p.StreamD.AddStreamPlayer,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Panel) openEditPlayerWindow(
|
func (p *Panel) openEditPlayerWindow(
|
||||||
@@ -593,7 +607,14 @@ func (p *Panel) openEditPlayerWindow(
|
|||||||
p.DisplayError(fmt.Errorf("unable to find a stream player for '%s'", streamID))
|
p.DisplayError(fmt.Errorf("unable to find a stream player for '%s'", streamID))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
p.openAddOrEditPlayerWindow(ctx, "Edit player", !playerCfg.Disabled, playerCfg.StreamPlayback, &streamID, p.StreamD.UpdateStreamPlayer)
|
p.openAddOrEditPlayerWindow(
|
||||||
|
ctx,
|
||||||
|
"Edit player",
|
||||||
|
!playerCfg.Disabled,
|
||||||
|
playerCfg.StreamPlayback,
|
||||||
|
&streamID,
|
||||||
|
p.StreamD.UpdateStreamPlayer,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Panel) openAddOrEditPlayerWindow(
|
func (p *Panel) openAddOrEditPlayerWindow(
|
||||||
@@ -779,14 +800,28 @@ func (p *Panel) displayStreamPlayers(
|
|||||||
c := container.NewHBox()
|
c := container.NewHBox()
|
||||||
deleteButton := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() {
|
deleteButton := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() {
|
||||||
w := dialog.NewConfirm(
|
w := dialog.NewConfirm(
|
||||||
fmt.Sprintf("Delete player for stream '%s' (%s) ?", player.StreamID, player.PlayerType),
|
fmt.Sprintf(
|
||||||
|
"Delete player for stream '%s' (%s) ?",
|
||||||
|
player.StreamID,
|
||||||
|
player.PlayerType,
|
||||||
|
),
|
||||||
"",
|
"",
|
||||||
func(b bool) {
|
func(b bool) {
|
||||||
if !b {
|
if !b {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logger.Debugf(ctx, "remove player '%s' (%s)", player.StreamID, player.PlayerType)
|
logger.Debugf(
|
||||||
defer logger.Debugf(ctx, "/remove player '%s' (%s)", player.StreamID, player.PlayerType)
|
ctx,
|
||||||
|
"remove player '%s' (%s)",
|
||||||
|
player.StreamID,
|
||||||
|
player.PlayerType,
|
||||||
|
)
|
||||||
|
defer logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"/remove player '%s' (%s)",
|
||||||
|
player.StreamID,
|
||||||
|
player.PlayerType,
|
||||||
|
)
|
||||||
err := p.StreamD.RemoveStreamPlayer(ctx, player.StreamID)
|
err := p.StreamD.RemoveStreamPlayer(ctx, player.StreamID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.DisplayError(err)
|
p.DisplayError(err)
|
||||||
@@ -816,8 +851,22 @@ func (p *Panel) displayStreamPlayers(
|
|||||||
if !b {
|
if !b {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logger.Debugf(ctx, "stop/start player %s on '%s': disabled:%v->%v", player.PlayerType, player.StreamID, player.Disabled, !player.Disabled)
|
logger.Debugf(
|
||||||
defer logger.Debugf(ctx, "/stop/start player %s on '%s': disabled:%v->%v", player.PlayerType, player.StreamID, player.Disabled, !player.Disabled)
|
ctx,
|
||||||
|
"stop/start player %s on '%s': disabled:%v->%v",
|
||||||
|
player.PlayerType,
|
||||||
|
player.StreamID,
|
||||||
|
player.Disabled,
|
||||||
|
!player.Disabled,
|
||||||
|
)
|
||||||
|
defer logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"/stop/start player %s on '%s': disabled:%v->%v",
|
||||||
|
player.PlayerType,
|
||||||
|
player.StreamID,
|
||||||
|
player.Disabled,
|
||||||
|
!player.Disabled,
|
||||||
|
)
|
||||||
err := p.StreamD.UpdateStreamPlayer(
|
err := p.StreamD.UpdateStreamPlayer(
|
||||||
xcontext.DetachDone(ctx),
|
xcontext.DetachDone(ctx),
|
||||||
player.StreamID,
|
player.StreamID,
|
||||||
@@ -843,7 +892,12 @@ func (p *Panel) displayStreamPlayers(
|
|||||||
if !player.Disabled {
|
if !player.Disabled {
|
||||||
pos, err := p.StreamD.StreamPlayerGetPosition(ctx, player.StreamID)
|
pos, err := p.StreamD.StreamPlayerGetPosition(ctx, player.StreamID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf(ctx, "unable to get the current position at player '%s': %v", player.StreamID, err)
|
logger.Errorf(
|
||||||
|
ctx,
|
||||||
|
"unable to get the current position at player '%s': %v",
|
||||||
|
player.StreamID,
|
||||||
|
err,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
c.Add(widget.NewSeparator())
|
c.Add(widget.NewSeparator())
|
||||||
posStr := pos.String()
|
posStr := pos.String()
|
||||||
@@ -932,8 +986,22 @@ func (p *Panel) openAddOrEditRestreamWindow(
|
|||||||
quirks sstypes.ForwardingQuirks,
|
quirks sstypes.ForwardingQuirks,
|
||||||
) error,
|
) error,
|
||||||
) {
|
) {
|
||||||
logger.Debugf(ctx, "openAddOrEditRestreamWindow(ctx, '%s', '%s', '%s', %#+v)", title, streamID, dstID, fwd)
|
logger.Debugf(
|
||||||
defer logger.Debugf(ctx, "/openAddOrEditRestreamWindow(ctx, '%s', '%s', '%s', %#+v)", title, streamID, dstID, fwd)
|
ctx,
|
||||||
|
"openAddOrEditRestreamWindow(ctx, '%s', '%s', '%s', %#+v)",
|
||||||
|
title,
|
||||||
|
streamID,
|
||||||
|
dstID,
|
||||||
|
fwd,
|
||||||
|
)
|
||||||
|
defer logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"/openAddOrEditRestreamWindow(ctx, '%s', '%s', '%s', %#+v)",
|
||||||
|
title,
|
||||||
|
streamID,
|
||||||
|
dstID,
|
||||||
|
fwd,
|
||||||
|
)
|
||||||
w := p.app.NewWindow(AppName + ": " + title)
|
w := p.app.NewWindow(AppName + ": " + title)
|
||||||
resizeWindow(w, fyne.NewSize(400, 300))
|
resizeWindow(w, fyne.NewSize(400, 300))
|
||||||
|
|
||||||
@@ -1146,7 +1214,11 @@ func (p *Panel) displayStreamForwards(
|
|||||||
c := container.NewHBox()
|
c := container.NewHBox()
|
||||||
deleteButton := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() {
|
deleteButton := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() {
|
||||||
w := dialog.NewConfirm(
|
w := dialog.NewConfirm(
|
||||||
fmt.Sprintf("Delete restreaming (stream forwarding) %s -> %s ?", fwd.StreamID, fwd.DestinationID),
|
fmt.Sprintf(
|
||||||
|
"Delete restreaming (stream forwarding) %s -> %s ?",
|
||||||
|
fwd.StreamID,
|
||||||
|
fwd.DestinationID,
|
||||||
|
),
|
||||||
"",
|
"",
|
||||||
func(b bool) {
|
func(b bool) {
|
||||||
if !b {
|
if !b {
|
||||||
@@ -1183,8 +1255,18 @@ func (p *Panel) displayStreamForwards(
|
|||||||
if !b {
|
if !b {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logger.Debugf(ctx, "pause/unpause restreaming (stream forwarding): enabled:%v->%v", fwd.Enabled, !fwd.Enabled)
|
logger.Debugf(
|
||||||
defer logger.Debugf(ctx, "/pause/unpause restreaming (stream forwarding): enabled:%v->%v", !fwd.Enabled, fwd.Enabled)
|
ctx,
|
||||||
|
"pause/unpause restreaming (stream forwarding): enabled:%v->%v",
|
||||||
|
fwd.Enabled,
|
||||||
|
!fwd.Enabled,
|
||||||
|
)
|
||||||
|
defer logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"/pause/unpause restreaming (stream forwarding): enabled:%v->%v",
|
||||||
|
!fwd.Enabled,
|
||||||
|
fwd.Enabled,
|
||||||
|
)
|
||||||
err := p.StreamD.UpdateStreamForward(
|
err := p.StreamD.UpdateStreamForward(
|
||||||
ctx,
|
ctx,
|
||||||
fwd.StreamID,
|
fwd.StreamID,
|
||||||
@@ -1229,7 +1311,14 @@ func (p *Panel) displayStreamForwards(
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
p.previousNumBytesLocker.Do(ctx, func() {
|
p.previousNumBytesLocker.Do(ctx, func() {
|
||||||
prevNumBytes := p.previousNumBytes[key]
|
prevNumBytes := p.previousNumBytes[key]
|
||||||
bwStr := bwString(fwd.NumBytesRead, prevNumBytes[0], fwd.NumBytesWrote, prevNumBytes[1], now, p.previousNumBytesTS[key])
|
bwStr := bwString(
|
||||||
|
fwd.NumBytesRead,
|
||||||
|
prevNumBytes[0],
|
||||||
|
fwd.NumBytesWrote,
|
||||||
|
prevNumBytes[1],
|
||||||
|
now,
|
||||||
|
p.previousNumBytesTS[key],
|
||||||
|
)
|
||||||
bwText := widget.NewRichTextWithText(bwStr)
|
bwText := widget.NewRichTextWithText(bwStr)
|
||||||
hasDynamicValue = hasDynamicValue || bwStr != ""
|
hasDynamicValue = hasDynamicValue || bwStr != ""
|
||||||
p.previousNumBytes[key] = [4]uint64{fwd.NumBytesRead, fwd.NumBytesWrote}
|
p.previousNumBytes[key] = [4]uint64{fwd.NumBytesRead, fwd.NumBytesWrote}
|
||||||
|
@@ -224,7 +224,13 @@ func (ui *timersUI) start(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
duration := time.Hour*time.Duration(hours) + time.Minute*time.Duration(mins) + time.Second*time.Duration(secs)
|
duration := time.Hour*time.Duration(
|
||||||
|
hours,
|
||||||
|
) + time.Minute*time.Duration(
|
||||||
|
mins,
|
||||||
|
) + time.Second*time.Duration(
|
||||||
|
secs,
|
||||||
|
)
|
||||||
|
|
||||||
if duration == 0 {
|
if duration == 0 {
|
||||||
ui.panel.DisplayError(fmt.Errorf("the time is not set for the timer"))
|
ui.panel.DisplayError(fmt.Errorf("the time is not set for the timer"))
|
||||||
|
@@ -15,7 +15,10 @@ type updateTimerHandler struct {
|
|||||||
startTS time.Time
|
startTS time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func newUpdateTimerHandler(startStopButton *widget.Button, startedAt time.Time) *updateTimerHandler {
|
func newUpdateTimerHandler(
|
||||||
|
startStopButton *widget.Button,
|
||||||
|
startedAt time.Time,
|
||||||
|
) *updateTimerHandler {
|
||||||
ctx, cancelFn := context.WithCancel(context.Background())
|
ctx, cancelFn := context.WithCancel(context.Background())
|
||||||
h := &updateTimerHandler{
|
h := &updateTimerHandler{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
|
@@ -121,7 +121,8 @@ func getMousePos(window fyne.Window) fyne.Position {
|
|||||||
func (w *HintWidget) isHovering(mousePos fyne.Position) bool {
|
func (w *HintWidget) isHovering(mousePos fyne.Position) bool {
|
||||||
pos0 := GetAbsolutePosition(w, w.Window.Canvas().Content())
|
pos0 := GetAbsolutePosition(w, w.Window.Canvas().Content())
|
||||||
pos1 := pos0.Add(w.Label.Size())
|
pos1 := pos0.Add(w.Label.Size())
|
||||||
if mousePos.X >= pos0.X && mousePos.Y >= pos0.Y && mousePos.X <= pos1.X && mousePos.Y <= pos1.Y {
|
if mousePos.X >= pos0.X && mousePos.Y >= pos0.Y && mousePos.X <= pos1.X &&
|
||||||
|
mousePos.Y <= pos1.Y {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -12,10 +12,16 @@ type dummyPlatformsController struct{}
|
|||||||
|
|
||||||
var _ streamservertypes.PlatformsController = (*dummyPlatformsController)(nil)
|
var _ streamservertypes.PlatformsController = (*dummyPlatformsController)(nil)
|
||||||
|
|
||||||
func (dummyPlatformsController) CheckStreamStartedByURL(ctx context.Context, destination *url.URL) (bool, error) {
|
func (dummyPlatformsController) CheckStreamStartedByURL(
|
||||||
|
ctx context.Context,
|
||||||
|
destination *url.URL,
|
||||||
|
) (bool, error) {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dummyPlatformsController) CheckStreamStartedByPlatformID(ctx context.Context, platID streamcontrol.PlatformName) (bool, error) {
|
func (dummyPlatformsController) CheckStreamStartedByPlatformID(
|
||||||
|
ctx context.Context,
|
||||||
|
platID streamcontrol.PlatformName,
|
||||||
|
) (bool, error) {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
@@ -37,8 +37,16 @@ func assertNoError(ctx context.Context, err error) {
|
|||||||
func main() {
|
func main() {
|
||||||
loggerLevel := logger.LevelWarning
|
loggerLevel := logger.LevelWarning
|
||||||
pflag.Var(&loggerLevel, "log-level", "Log level")
|
pflag.Var(&loggerLevel, "log-level", "Log level")
|
||||||
rtmpListenAddr := pflag.String("rtmp-listen-addr", "127.0.0.1:1935", "the TCP port to serve an RTMP server on")
|
rtmpListenAddr := pflag.String(
|
||||||
streamID := pflag.String("stream-id", "test/test", "the path of the stream in rtmp://address/path")
|
"rtmp-listen-addr",
|
||||||
|
"127.0.0.1:1935",
|
||||||
|
"the TCP port to serve an RTMP server on",
|
||||||
|
)
|
||||||
|
streamID := pflag.String(
|
||||||
|
"stream-id",
|
||||||
|
"test/test",
|
||||||
|
"the path of the stream in rtmp://address/path",
|
||||||
|
)
|
||||||
mpvPath := pflag.String("mpv", "mpv", "path to mpv")
|
mpvPath := pflag.String("mpv", "mpv", "path to mpv")
|
||||||
pflag.Parse()
|
pflag.Parse()
|
||||||
|
|
||||||
|
@@ -96,7 +96,14 @@ func (sp *StreamPlayers) Create(
|
|||||||
) (_ret *StreamPlayerHandler, _err error) {
|
) (_ret *StreamPlayerHandler, _err error) {
|
||||||
logger.Debugf(ctx, "StreamPlayers.Create(ctx, '%s', %#+v)", streamID, opts)
|
logger.Debugf(ctx, "StreamPlayers.Create(ctx, '%s', %#+v)", streamID, opts)
|
||||||
defer func() {
|
defer func() {
|
||||||
logger.Debugf(ctx, "/StreamPlayers.Create(ctx, '%s', %#+v): (%v, %v)", streamID, opts, _ret, _err)
|
logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"/StreamPlayers.Create(ctx, '%s', %#+v): (%v, %v)",
|
||||||
|
streamID,
|
||||||
|
opts,
|
||||||
|
_ret,
|
||||||
|
_err,
|
||||||
|
)
|
||||||
}()
|
}()
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
@@ -114,11 +121,18 @@ func (sp *StreamPlayers) Create(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if p.Config.CatchupMaxSpeedFactor <= 1 {
|
if p.Config.CatchupMaxSpeedFactor <= 1 {
|
||||||
return nil, fmt.Errorf("MaxCatchupSpeedFactor should be higher than 1, but it is %v", p.Config.CatchupMaxSpeedFactor)
|
return nil, fmt.Errorf(
|
||||||
|
"MaxCatchupSpeedFactor should be higher than 1, but it is %v",
|
||||||
|
p.Config.CatchupMaxSpeedFactor,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.Config.MaxCatchupAtLag <= p.Config.JitterBufDuration {
|
if p.Config.MaxCatchupAtLag <= p.Config.JitterBufDuration {
|
||||||
return nil, fmt.Errorf("MaxCatchupAtLag (%v) should be higher than JitterBufDuration (%v)", p.Config.MaxCatchupAtLag, p.Config.JitterBufDuration)
|
return nil, fmt.Errorf(
|
||||||
|
"MaxCatchupAtLag (%v) should be higher than JitterBufDuration (%v)",
|
||||||
|
p.Config.MaxCatchupAtLag,
|
||||||
|
p.Config.JitterBufDuration,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.startU(ctx); err != nil {
|
if err := p.startU(ctx); err != nil {
|
||||||
@@ -157,13 +171,17 @@ func (sp *StreamPlayers) Get(streamID streamtypes.StreamID) *StreamPlayerHandler
|
|||||||
|
|
||||||
func (sp *StreamPlayers) GetAll() map[streamtypes.StreamID]*StreamPlayerHandler {
|
func (sp *StreamPlayers) GetAll() map[streamtypes.StreamID]*StreamPlayerHandler {
|
||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
return xsync.DoR1(ctx, &sp.StreamPlayersLocker, func() map[streamtypes.StreamID]*StreamPlayerHandler {
|
return xsync.DoR1(
|
||||||
|
ctx,
|
||||||
|
&sp.StreamPlayersLocker,
|
||||||
|
func() map[streamtypes.StreamID]*StreamPlayerHandler {
|
||||||
r := map[streamtypes.StreamID]*StreamPlayerHandler{}
|
r := map[streamtypes.StreamID]*StreamPlayerHandler{}
|
||||||
for k, v := range sp.StreamPlayers {
|
for k, v := range sp.StreamPlayers {
|
||||||
r[k] = v
|
r[k] = v
|
||||||
}
|
}
|
||||||
return r
|
return r
|
||||||
})
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -570,7 +588,10 @@ func (p *StreamPlayerHandler) controllerLoop(
|
|||||||
errmon.ObserveErrorCtx(ctx, p.Close())
|
errmon.ObserveErrorCtx(ctx, p.Close())
|
||||||
return
|
return
|
||||||
case <-getRestartChan:
|
case <-getRestartChan:
|
||||||
logger.Debugf(ctx, "received a notification that the player should be restarted immediately")
|
logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"received a notification that the player should be restarted immediately",
|
||||||
|
)
|
||||||
restart()
|
restart()
|
||||||
return
|
return
|
||||||
case <-t.C:
|
case <-t.C:
|
||||||
@@ -580,9 +601,17 @@ func (p *StreamPlayerHandler) controllerLoop(
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
l, err := player.GetLength(ctx)
|
l, err := player.GetLength(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf(ctx, "StreamPlayer[%s].controllerLoop: unable to get the current length: %v", p.StreamID, err)
|
logger.Errorf(
|
||||||
|
ctx,
|
||||||
|
"StreamPlayer[%s].controllerLoop: unable to get the current length: %v",
|
||||||
|
p.StreamID,
|
||||||
|
err,
|
||||||
|
)
|
||||||
if prevLength != 0 {
|
if prevLength != 0 {
|
||||||
logger.Debugf(ctx, "previously GetLength worked, so it seems like the player died or something, restarting")
|
logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"previously GetLength worked, so it seems like the player died or something, restarting",
|
||||||
|
)
|
||||||
restart()
|
restart()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -593,11 +622,25 @@ func (p *StreamPlayerHandler) controllerLoop(
|
|||||||
|
|
||||||
pos, err := player.GetPosition(ctx)
|
pos, err := player.GetPosition(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf(ctx, "StreamPlayer[%s].controllerLoop: unable to get the current position: %v", p.StreamID, err)
|
logger.Errorf(
|
||||||
|
ctx,
|
||||||
|
"StreamPlayer[%s].controllerLoop: unable to get the current position: %v",
|
||||||
|
p.StreamID,
|
||||||
|
err,
|
||||||
|
)
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logger.Tracef(ctx, "StreamPlayer[%s].controllerLoop: now == %v, posUpdatedAt == %v, len == %v; pos == %v; readTimeout == %v", p.StreamID, now, posUpdatedAt, l, pos, p.Config.ReadTimeout)
|
logger.Tracef(
|
||||||
|
ctx,
|
||||||
|
"StreamPlayer[%s].controllerLoop: now == %v, posUpdatedAt == %v, len == %v; pos == %v; readTimeout == %v",
|
||||||
|
p.StreamID,
|
||||||
|
now,
|
||||||
|
posUpdatedAt,
|
||||||
|
l,
|
||||||
|
pos,
|
||||||
|
p.Config.ReadTimeout,
|
||||||
|
)
|
||||||
|
|
||||||
if pos < 0 {
|
if pos < 0 {
|
||||||
logger.Debugf(ctx, "negative position: %v", pos)
|
logger.Debugf(ctx, "negative position: %v", pos)
|
||||||
@@ -628,7 +671,11 @@ func (p *StreamPlayerHandler) controllerLoop(
|
|||||||
if curSpeed == 1 {
|
if curSpeed == 1 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logger.Debugf(ctx, "StreamPlayer[%s].controllerLoop: resetting the speed to 1", p.StreamID)
|
logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"StreamPlayer[%s].controllerLoop: resetting the speed to 1",
|
||||||
|
p.StreamID,
|
||||||
|
)
|
||||||
err := player.SetSpeed(ctx, 1)
|
err := player.SetSpeed(ctx, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf(ctx, "unable to reset the speed to 1: %v", err)
|
logger.Errorf(ctx, "unable to reset the speed to 1: %v", err)
|
||||||
@@ -643,23 +690,34 @@ func (p *StreamPlayerHandler) controllerLoop(
|
|||||||
(lag.Seconds()-p.Config.JitterBufDuration.Seconds())/
|
(lag.Seconds()-p.Config.JitterBufDuration.Seconds())/
|
||||||
(p.Config.MaxCatchupAtLag.Seconds()-p.Config.JitterBufDuration.Seconds())
|
(p.Config.MaxCatchupAtLag.Seconds()-p.Config.JitterBufDuration.Seconds())
|
||||||
|
|
||||||
speed = float64(uint(speed*10)) / 10 // to avoid flickering (for example between 1.0001 and 1.0)
|
speed = float64(
|
||||||
|
uint(speed*10),
|
||||||
|
) / 10 // to avoid flickering (for example between 1.0001 and 1.0)
|
||||||
|
|
||||||
if speed > p.Config.CatchupMaxSpeedFactor {
|
if speed > p.Config.CatchupMaxSpeedFactor {
|
||||||
logger.Warnf(
|
logger.Warnf(
|
||||||
ctx,
|
ctx,
|
||||||
"speed is calculated higher than the maximum: %v > %v: (%v-1)*(%v-%v)/(%v-%v); lag calculation: %v - %v",
|
"speed is calculated higher than the maximum: %v > %v: (%v-1)*(%v-%v)/(%v-%v); lag calculation: %v - %v",
|
||||||
speed, p.Config.CatchupMaxSpeedFactor,
|
speed,
|
||||||
p.Config.CatchupMaxSpeedFactor,
|
p.Config.CatchupMaxSpeedFactor,
|
||||||
lag.Seconds(), p.Config.JitterBufDuration.Seconds(),
|
p.Config.CatchupMaxSpeedFactor,
|
||||||
p.Config.MaxCatchupAtLag.Seconds(), p.Config.JitterBufDuration.Seconds(),
|
lag.Seconds(),
|
||||||
l, pos,
|
p.Config.JitterBufDuration.Seconds(),
|
||||||
|
p.Config.MaxCatchupAtLag.Seconds(),
|
||||||
|
p.Config.JitterBufDuration.Seconds(),
|
||||||
|
l,
|
||||||
|
pos,
|
||||||
)
|
)
|
||||||
speed = p.Config.CatchupMaxSpeedFactor
|
speed = p.Config.CatchupMaxSpeedFactor
|
||||||
}
|
}
|
||||||
|
|
||||||
if speed != curSpeed {
|
if speed != curSpeed {
|
||||||
logger.Debugf(ctx, "StreamPlayer[%s].controllerLoop: setting the speed to %v", p.StreamID, speed)
|
logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"StreamPlayer[%s].controllerLoop: setting the speed to %v",
|
||||||
|
p.StreamID,
|
||||||
|
speed,
|
||||||
|
)
|
||||||
err = player.SetSpeed(ctx, speed)
|
err = player.SetSpeed(ctx, speed)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf(ctx, "unable to set the speed to %v: %v", speed, err)
|
logger.Errorf(ctx, "unable to set the speed to %v: %v", speed, err)
|
||||||
|
@@ -184,7 +184,9 @@ func StreamsHandle(url string) (core.Producer, error) {
|
|||||||
return rtmp.DialPlay(url)
|
return rtmp.DialPlay(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
func StreamsConsumerHandle(url string) (core.Consumer, types.NumBytesReaderWroter, func(context.Context) error, error) {
|
func StreamsConsumerHandle(
|
||||||
|
url string,
|
||||||
|
) (core.Consumer, types.NumBytesReaderWroter, func(context.Context) error, error) {
|
||||||
cons := flv.NewConsumer()
|
cons := flv.NewConsumer()
|
||||||
trafficCounter := &types.TrafficCounter{}
|
trafficCounter := &types.TrafficCounter{}
|
||||||
run := func(ctx context.Context) error {
|
run := func(ctx context.Context) error {
|
||||||
|
@@ -74,7 +74,12 @@ func (s *StreamServer) initNoLock(ctx context.Context) error {
|
|||||||
for dstID, dstCfg := range cfg.Destinations {
|
for dstID, dstCfg := range cfg.Destinations {
|
||||||
err := s.addStreamDestination(ctx, dstID, dstCfg.URL)
|
err := s.addStreamDestination(ctx, dstID, dstCfg.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to initialize stream destination '%s' to %#+v: %w", dstID, dstCfg, err)
|
return fmt.Errorf(
|
||||||
|
"unable to initialize stream destination '%s' to %#+v: %w",
|
||||||
|
dstID,
|
||||||
|
dstCfg,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +93,12 @@ func (s *StreamServer) initNoLock(ctx context.Context) error {
|
|||||||
if !fwd.Disabled {
|
if !fwd.Disabled {
|
||||||
err := s.addStreamForward(ctx, streamID, dstID)
|
err := s.addStreamForward(ctx, streamID, dstID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to launch stream forward from '%s' to '%s': %w", streamID, dstID, err)
|
return fmt.Errorf(
|
||||||
|
"unable to launch stream forward from '%s' to '%s': %w",
|
||||||
|
streamID,
|
||||||
|
dstID,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -315,7 +325,11 @@ func (s *StreamServer) addStreamForward(
|
|||||||
) error {
|
) error {
|
||||||
streamSrc := s.StreamHandler.Get(string(streamID))
|
streamSrc := s.StreamHandler.Get(string(streamID))
|
||||||
if streamSrc == nil {
|
if streamSrc == nil {
|
||||||
return fmt.Errorf("unable to find stream ID '%s', available stream IDs: %s", streamID, strings.Join(s.StreamHandler.GetAll(), ", "))
|
return fmt.Errorf(
|
||||||
|
"unable to find stream ID '%s', available stream IDs: %s",
|
||||||
|
streamID,
|
||||||
|
strings.Join(s.StreamHandler.GetAll(), ", "),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
dst, err := s.findStreamDestinationByID(ctx, destinationID)
|
dst, err := s.findStreamDestinationByID(ctx, destinationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -418,7 +432,11 @@ func (s *StreamServer) listStreamForwards(
|
|||||||
streamIDSrc := types.StreamID(name)
|
streamIDSrc := types.StreamID(name)
|
||||||
streamDst, err := s.findStreamDestinationByURL(ctx, fwd.URL)
|
streamDst, err := s.findStreamDestinationByURL(ctx, fwd.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to convert URL '%s' to a stream ID: %w", fwd.URL, err)
|
return nil, fmt.Errorf(
|
||||||
|
"unable to convert URL '%s' to a stream ID: %w",
|
||||||
|
fwd.URL,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
result = append(result, StreamForward{
|
result = append(result, StreamForward{
|
||||||
StreamID: streamIDSrc,
|
StreamID: streamIDSrc,
|
||||||
@@ -467,7 +485,13 @@ func (s *StreamServer) removeStreamForward(
|
|||||||
|
|
||||||
err = fwd.Close()
|
err = fwd.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to close forwarding from %s to %s (%s): %w", streamID, dstID, fwd.URL, err)
|
return fmt.Errorf(
|
||||||
|
"unable to close forwarding from %s to %s (%s): %w",
|
||||||
|
streamID,
|
||||||
|
dstID,
|
||||||
|
fwd.URL,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
stream.Cleanup()
|
stream.Cleanup()
|
||||||
return nil
|
return nil
|
||||||
@@ -562,7 +586,10 @@ func (s *StreamServer) findStreamDestinationByURL(
|
|||||||
return dst, nil
|
return dst, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return types.StreamDestination{}, fmt.Errorf("unable to find a stream destination by URL '%s'", url)
|
return types.StreamDestination{}, fmt.Errorf(
|
||||||
|
"unable to find a stream destination by URL '%s'",
|
||||||
|
url,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StreamServer) findStreamDestinationByID(
|
func (s *StreamServer) findStreamDestinationByID(
|
||||||
@@ -574,5 +601,8 @@ func (s *StreamServer) findStreamDestinationByID(
|
|||||||
return dst, nil
|
return dst, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return types.StreamDestination{}, fmt.Errorf("unable to find a stream destination by StreamID '%s'", destinationID)
|
return types.StreamDestination{}, fmt.Errorf(
|
||||||
|
"unable to find a stream destination by StreamID '%s'",
|
||||||
|
destinationID,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@@ -112,7 +112,14 @@ type conn struct {
|
|||||||
func (c *conn) appendDOT(dot []byte, group string) []byte {
|
func (c *conn) appendDOT(dot []byte, group string) []byte {
|
||||||
host := c.host()
|
host := c.host()
|
||||||
dot = fmt.Appendf(dot, "%s [group=host];\n", host)
|
dot = fmt.Appendf(dot, "%s [group=host];\n", host)
|
||||||
dot = fmt.Appendf(dot, "%d [group=%s, label=%q, title=%q];\n", c.ID, group, c.FormatName, c.label())
|
dot = fmt.Appendf(
|
||||||
|
dot,
|
||||||
|
"%d [group=%s, label=%q, title=%q];\n",
|
||||||
|
c.ID,
|
||||||
|
group,
|
||||||
|
c.FormatName,
|
||||||
|
c.label(),
|
||||||
|
)
|
||||||
if group == "producer" {
|
if group == "producer" {
|
||||||
dot = fmt.Appendf(dot, "%s -> %d [label=%q];\n", host, c.ID, humanBytes(c.BytesRecv))
|
dot = fmt.Appendf(dot, "%s -> %d [label=%q];\n", host, c.ID, humanBytes(c.BytesRecv))
|
||||||
} else {
|
} else {
|
||||||
|
@@ -89,7 +89,9 @@ func (s *StreamHandler) HandleConsumerFunc(scheme string, handler ConsumerHandle
|
|||||||
s.consumerHandlers[scheme] = handler
|
s.consumerHandlers[scheme] = handler
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StreamHandler) GetConsumer(url string) (core.Consumer, types.NumBytesReaderWroter, func(context.Context) error, error) {
|
func (s *StreamHandler) GetConsumer(
|
||||||
|
url string,
|
||||||
|
) (core.Consumer, types.NumBytesReaderWroter, func(context.Context) error, error) {
|
||||||
if i := strings.IndexByte(url, ':'); i > 0 {
|
if i := strings.IndexByte(url, ':'); i > 0 {
|
||||||
scheme := url[:i]
|
scheme := url[:i]
|
||||||
|
|
||||||
|
@@ -65,7 +65,12 @@ func (s *StreamServer) init(ctx context.Context) error {
|
|||||||
for dstID, dstCfg := range cfg.Destinations {
|
for dstID, dstCfg := range cfg.Destinations {
|
||||||
err := s.addStreamDestination(ctx, dstID, dstCfg.URL)
|
err := s.addStreamDestination(ctx, dstID, dstCfg.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to initialize stream destination '%s' to %#+v: %w", dstID, dstCfg, err)
|
return fmt.Errorf(
|
||||||
|
"unable to initialize stream destination '%s' to %#+v: %w",
|
||||||
|
dstID,
|
||||||
|
dstCfg,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +84,12 @@ func (s *StreamServer) init(ctx context.Context) error {
|
|||||||
if !fwd.Disabled {
|
if !fwd.Disabled {
|
||||||
_, err := s.addStreamForward(ctx, streamID, dstID, fwd.Quirks)
|
_, err := s.addStreamForward(ctx, streamID, dstID, fwd.Quirks)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to launch stream forward from '%s' to '%s': %w", streamID, dstID, err)
|
return fmt.Errorf(
|
||||||
|
"unable to launch stream forward from '%s' to '%s': %w",
|
||||||
|
streamID,
|
||||||
|
dstID,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,7 +153,11 @@ func (s *StreamServer) startServer(
|
|||||||
observability.Go(ctx, func() {
|
observability.Go(ctx, func() {
|
||||||
err = portServer.Server.Serve(listener)
|
err = portServer.Server.Serve(listener)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("unable to start serving RTMP at '%s': %w", listener.Addr().String(), err)
|
err = fmt.Errorf(
|
||||||
|
"unable to start serving RTMP at '%s': %w",
|
||||||
|
listener.Addr().String(),
|
||||||
|
err,
|
||||||
|
)
|
||||||
logger.Error(ctx, err)
|
logger.Error(ctx, err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -371,7 +385,15 @@ func (s *StreamServer) addStreamForward(
|
|||||||
|
|
||||||
ctx = belt.WithField(ctx, "stream_forward", fmt.Sprintf("%s->%s", streamID, destinationID))
|
ctx = belt.WithField(ctx, "stream_forward", fmt.Sprintf("%s->%s", streamID, destinationID))
|
||||||
if actFwd, ok := s.ActiveStreamForwardings[destinationID]; ok {
|
if actFwd, ok := s.ActiveStreamForwardings[destinationID]; ok {
|
||||||
return buildStreamForward(streamID, destinationID, cfg, actFwd), fmt.Errorf("there is already an active stream forwarding to '%s'", destinationID)
|
return buildStreamForward(
|
||||||
|
streamID,
|
||||||
|
destinationID,
|
||||||
|
cfg,
|
||||||
|
actFwd,
|
||||||
|
), fmt.Errorf(
|
||||||
|
"there is already an active stream forwarding to '%s'",
|
||||||
|
destinationID,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
dst, err := s.findStreamDestinationByID(ctx, destinationID)
|
dst, err := s.findStreamDestinationByID(ctx, destinationID)
|
||||||
@@ -633,7 +655,10 @@ func (s *StreamServer) findStreamDestinationByID(
|
|||||||
return dst, nil
|
return dst, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return types.StreamDestination{}, fmt.Errorf("unable to find a stream destination by StreamID '%s'", destinationID)
|
return types.StreamDestination{}, fmt.Errorf(
|
||||||
|
"unable to find a stream destination by StreamID '%s'",
|
||||||
|
destinationID,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StreamServer) AddStreamPlayer(
|
func (s *StreamServer) AddStreamPlayer(
|
||||||
|
@@ -73,7 +73,11 @@ func (srv *RTMPServer) init(
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
if (cfg.ServerCert != nil) != (cfg.ServerKey != nil) {
|
if (cfg.ServerCert != nil) != (cfg.ServerKey != nil) {
|
||||||
return fmt.Errorf("fields 'ServerCert' and 'ServerKey' should be used together (cur values: ServerCert == %#+v; ServerKey == %#+v)", cfg.ServerCert, cfg.ServerKey)
|
return fmt.Errorf(
|
||||||
|
"fields 'ServerCert' and 'ServerKey' should be used together (cur values: ServerCert == %#+v; ServerKey == %#+v)",
|
||||||
|
cfg.ServerCert,
|
||||||
|
cfg.ServerKey,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.ServerCert != nil && cfg.ServerKey != nil {
|
if cfg.ServerCert != nil && cfg.ServerKey != nil {
|
||||||
@@ -84,7 +88,10 @@ func (srv *RTMPServer) init(
|
|||||||
}
|
}
|
||||||
if cfg.IsTLS && (srv.ServerCert == "" || srv.ServerKey == "") {
|
if cfg.IsTLS && (srv.ServerCert == "" || srv.ServerKey == "") {
|
||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
logger.Warnf(ctx, "TLS is enabled, but no certificate is supplied, generating an ephemeral self-signed certificate") // TODO: implement the support of providing the certificates in the UI
|
logger.Warnf(
|
||||||
|
ctx,
|
||||||
|
"TLS is enabled, but no certificate is supplied, generating an ephemeral self-signed certificate",
|
||||||
|
) // TODO: implement the support of providing the certificates in the UI
|
||||||
err := srv.generateServerCertificate()
|
err := srv.generateServerCertificate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to set the TLS certificate: %w", err)
|
return fmt.Errorf("unable to set the TLS certificate: %w", err)
|
||||||
@@ -173,7 +180,11 @@ func (srv *RTMPServer) setServerCertificate(
|
|||||||
Bytes: cert.Raw,
|
Bytes: cert.Raw,
|
||||||
}
|
}
|
||||||
if err := pem.Encode(certFile, certPEM); err != nil {
|
if err := pem.Encode(certFile, certPEM); err != nil {
|
||||||
return fmt.Errorf("unable to write the server certificate to file '%s' in PEM format: %w", certFile.Name(), err)
|
return fmt.Errorf(
|
||||||
|
"unable to write the server certificate to file '%s' in PEM format: %w",
|
||||||
|
certFile.Name(),
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
keyFile, err = os.CreateTemp("", "rtmps-server-certkey-*.pem")
|
keyFile, err = os.CreateTemp("", "rtmps-server-certkey-*.pem")
|
||||||
@@ -192,7 +203,11 @@ func (srv *RTMPServer) setServerCertificate(
|
|||||||
Bytes: keyBytes,
|
Bytes: keyBytes,
|
||||||
}
|
}
|
||||||
if err := pem.Encode(keyFile, privatePem); err != nil {
|
if err := pem.Encode(keyFile, privatePem); err != nil {
|
||||||
return fmt.Errorf("unable to write the server certificate to file '%s' in PEM format: %w", certFile.Name(), err)
|
return fmt.Errorf(
|
||||||
|
"unable to write the server certificate to file '%s' in PEM format: %w",
|
||||||
|
certFile.Name(),
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
srv.ServerCert = certFile.Name()
|
srv.ServerCert = certFile.Name()
|
||||||
|
@@ -117,7 +117,13 @@ func (s *StreamServer) init(
|
|||||||
s.mutex.Do(ctx, func() {
|
s.mutex.Do(ctx, func() {
|
||||||
_, err := s.startServer(ctx, srv.Type, srv.Listen, srv.Options()...)
|
_, err := s.startServer(ctx, srv.Type, srv.Listen, srv.Options()...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf(ctx, "unable to initialize %s server at %s: %w", srv.Type, srv.Listen, err)
|
logger.Errorf(
|
||||||
|
ctx,
|
||||||
|
"unable to initialize %s server at %s: %w",
|
||||||
|
srv.Type,
|
||||||
|
srv.Listen,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -145,7 +151,11 @@ func (s *StreamServer) PathReady(path defs.Path) {
|
|||||||
s.streamsStatusLocker.Do(context.Background(), func() {
|
s.streamsStatusLocker.Do(context.Background(), func() {
|
||||||
publisher := s.publishers[appKey]
|
publisher := s.publishers[appKey]
|
||||||
if publisher != nil {
|
if publisher != nil {
|
||||||
logger.Errorf(ctx, "double-registration of a publisher for '%s' (this is an internal error in the code): %w", appKey)
|
logger.Errorf(
|
||||||
|
ctx,
|
||||||
|
"double-registration of a publisher for '%s' (this is an internal error in the code): %w",
|
||||||
|
appKey,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.publishers[appKey] = newPublisherClosedNotifier()
|
s.publishers[appKey] = newPublisherClosedNotifier()
|
||||||
@@ -386,7 +396,13 @@ func (s *StreamServer) WaitPublisherChan(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.Debugf(ctx, "WaitPublisherChan('%s', %v): publisher==%#+v", appKey, waitForNext, publisher)
|
logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"WaitPublisherChan('%s', %v): publisher==%#+v",
|
||||||
|
appKey,
|
||||||
|
waitForNext,
|
||||||
|
publisher,
|
||||||
|
)
|
||||||
|
|
||||||
if publisher != nil && publisher != curPublisher {
|
if publisher != nil && publisher != curPublisher {
|
||||||
ch <- publisher
|
ch <- publisher
|
||||||
@@ -399,7 +415,12 @@ func (s *StreamServer) WaitPublisherChan(
|
|||||||
logger.Debugf(ctx, "WaitPublisherChan('%s', %v): cancelled", appKey, waitForNext)
|
logger.Debugf(ctx, "WaitPublisherChan('%s', %v): cancelled", appKey, waitForNext)
|
||||||
return
|
return
|
||||||
case <-waitCh:
|
case <-waitCh:
|
||||||
logger.Debugf(ctx, "WaitPublisherChan('%s', %v): an event happened, rechecking", appKey, waitForNext)
|
logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"WaitPublisherChan('%s', %v): an event happened, rechecking",
|
||||||
|
appKey,
|
||||||
|
waitForNext,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -433,13 +454,23 @@ func (s *StreamServer) startServer(
|
|||||||
|
|
||||||
for _, portSrv := range s.serverHandlers {
|
for _, portSrv := range s.serverHandlers {
|
||||||
if portSrv.ListenAddr() == listenAddr {
|
if portSrv.ListenAddr() == listenAddr {
|
||||||
return nil, fmt.Errorf("we already have an port server %#+v instance at '%s'", portSrv, listenAddr)
|
return nil, fmt.Errorf(
|
||||||
|
"we already have an port server %#+v instance at '%s'",
|
||||||
|
portSrv,
|
||||||
|
listenAddr,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
portSrv, err := s.newServer(ctx, serverType, listenAddr, opts...)
|
portSrv, err := s.newServer(ctx, serverType, listenAddr, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to initialize a new instance of a port server %s at %s with options %v: %w", serverType, listenAddr, opts, err)
|
return nil, fmt.Errorf(
|
||||||
|
"unable to initialize a new instance of a port server %s at %s with options %v: %w",
|
||||||
|
serverType,
|
||||||
|
listenAddr,
|
||||||
|
opts,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Tracef(ctx, "adding serverHandler %#+v %#+v", portSrv, portSrv.Config())
|
logger.Tracef(ctx, "adding serverHandler %#+v %#+v", portSrv, portSrv.Config())
|
||||||
|
@@ -66,7 +66,11 @@ func (h *Handler) OnCreateStream(timestamp uint32, cmd *rtmpmsg.NetConnectionCre
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) OnPublish(_ *rtmp.StreamContext, timestamp uint32, cmd *rtmpmsg.NetStreamPublish) error {
|
func (h *Handler) OnPublish(
|
||||||
|
_ *rtmp.StreamContext,
|
||||||
|
timestamp uint32,
|
||||||
|
cmd *rtmpmsg.NetStreamPublish,
|
||||||
|
) error {
|
||||||
log.Printf("OnPublish: %#v", cmd)
|
log.Printf("OnPublish: %#v", cmd)
|
||||||
|
|
||||||
if h.sub != nil {
|
if h.sub != nil {
|
||||||
@@ -94,7 +98,11 @@ func (h *Handler) OnPublish(_ *rtmp.StreamContext, timestamp uint32, cmd *rtmpms
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) OnPlay(rtmpctx *rtmp.StreamContext, timestamp uint32, cmd *rtmpmsg.NetStreamPlay) error {
|
func (h *Handler) OnPlay(
|
||||||
|
rtmpctx *rtmp.StreamContext,
|
||||||
|
timestamp uint32,
|
||||||
|
cmd *rtmpmsg.NetStreamPlay,
|
||||||
|
) error {
|
||||||
if h.sub != nil {
|
if h.sub != nil {
|
||||||
return errors.New("Cannot play on this stream")
|
return errors.New("Cannot play on this stream")
|
||||||
}
|
}
|
||||||
|
@@ -93,7 +93,10 @@ func (pb *Pubsub) ClosedChan() <-chan struct{} {
|
|||||||
|
|
||||||
const sendQueueLength = 2 * 60 * 10 // presumably it will give about 10 seconds of queue: 2 tracks * 60FPS * 30 seconds
|
const sendQueueLength = 2 * 60 * 10 // presumably it will give about 10 seconds of queue: 2 tracks * 60FPS * 30 seconds
|
||||||
|
|
||||||
func (pb *Pubsub) Sub(connCloser io.Closer, eventCallback func(context.Context, *flvtag.FlvTag) error) *Sub {
|
func (pb *Pubsub) Sub(
|
||||||
|
connCloser io.Closer,
|
||||||
|
eventCallback func(context.Context, *flvtag.FlvTag) error,
|
||||||
|
) *Sub {
|
||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
return xsync.DoR1(ctx, &pb.m, func() *Sub {
|
return xsync.DoR1(ctx, &pb.m, func() *Sub {
|
||||||
subID := pb.nextSubID
|
subID := pb.nextSubID
|
||||||
@@ -303,7 +306,11 @@ func (s *Sub) senderLoop(
|
|||||||
|
|
||||||
case tag, ok := <-s.sendQueue:
|
case tag, ok := <-s.sendQueue:
|
||||||
if !ok {
|
if !ok {
|
||||||
logger.Debugf(ctx, "Sub[%d].senderLoop: the queue is closed; closing the client", s.subID)
|
logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"Sub[%d].senderLoop: the queue is closed; closing the client",
|
||||||
|
s.subID,
|
||||||
|
)
|
||||||
s.Close()
|
s.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -313,7 +320,12 @@ func (s *Sub) senderLoop(
|
|||||||
err := s.onEvent(ctx, tag)
|
err := s.onEvent(ctx, tag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
metrics.FromCtx(ctx).Count("submit_process_error").Add(1)
|
metrics.FromCtx(ctx).Count("submit_process_error").Add(1)
|
||||||
logger.Errorf(ctx, "Sub[%d].senderLoop: unable to send an FLV tag: %v", s.subID, err)
|
logger.Errorf(
|
||||||
|
ctx,
|
||||||
|
"Sub[%d].senderLoop: unable to send an FLV tag: %v",
|
||||||
|
s.subID,
|
||||||
|
err,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
metrics.FromCtx(ctx).Count("submit_process_success").Add(1)
|
metrics.FromCtx(ctx).Count("submit_process_success").Add(1)
|
||||||
}
|
}
|
||||||
@@ -331,7 +343,11 @@ func (s *Sub) Submit(
|
|||||||
case s.sendQueue <- flv:
|
case s.sendQueue <- flv:
|
||||||
metrics.FromCtx(ctx).Count("submit_pushed").Add(1)
|
metrics.FromCtx(ctx).Count("submit_pushed").Add(1)
|
||||||
default:
|
default:
|
||||||
logger.Errorf(ctx, "subscriber #%d queue is full, cannot send a tag; closing the connection, because cannot restore from this", s.subID)
|
logger.Errorf(
|
||||||
|
ctx,
|
||||||
|
"subscriber #%d queue is full, cannot send a tag; closing the connection, because cannot restore from this",
|
||||||
|
s.subID,
|
||||||
|
)
|
||||||
observability.Go(ctx, func() {
|
observability.Go(ctx, func() {
|
||||||
s.CloseOrLog(ctx)
|
s.CloseOrLog(ctx)
|
||||||
})
|
})
|
||||||
|
@@ -12,5 +12,9 @@ func NewStreamForwards(
|
|||||||
s StreamServer,
|
s StreamServer,
|
||||||
platformsController types.PlatformsController,
|
platformsController types.PlatformsController,
|
||||||
) *StreamForwards {
|
) *StreamForwards {
|
||||||
return streamforward.NewStreamForwards(s, xaionarogortmp.NewRecoderFactory(), platformsController)
|
return streamforward.NewStreamForwards(
|
||||||
|
s,
|
||||||
|
xaionarogortmp.NewRecoderFactory(),
|
||||||
|
platformsController,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@@ -88,7 +88,13 @@ func (s *StreamServer) init(
|
|||||||
observability.Go(ctx, func() {
|
observability.Go(ctx, func() {
|
||||||
_, err := s.startServer(ctx, srv.Type, srv.Listen)
|
_, err := s.startServer(ctx, srv.Type, srv.Listen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf(ctx, "unable to initialize %s server at %s: %w", srv.Type, srv.Listen, err)
|
logger.Errorf(
|
||||||
|
ctx,
|
||||||
|
"unable to initialize %s server at %s: %w",
|
||||||
|
srv.Type,
|
||||||
|
srv.Listen,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -210,7 +216,11 @@ func (s *StreamServer) startServer(
|
|||||||
OnConnect: func(conn net.Conn) (io.ReadWriteCloser, *rtmp.ConnConfig) {
|
OnConnect: func(conn net.Conn) (io.ReadWriteCloser, *rtmp.ConnConfig) {
|
||||||
ctx := belt.WithField(ctx, "client", conn.RemoteAddr().String())
|
ctx := belt.WithField(ctx, "client", conn.RemoteAddr().String())
|
||||||
h := yutoppgortmp.NewHandler(s.RelayService)
|
h := yutoppgortmp.NewHandler(s.RelayService)
|
||||||
wrcc := types.NewReaderWriterCloseCounter(conn, &portSrv.ReadCount, &portSrv.WriteCount)
|
wrcc := types.NewReaderWriterCloseCounter(
|
||||||
|
conn,
|
||||||
|
&portSrv.ReadCount,
|
||||||
|
&portSrv.WriteCount,
|
||||||
|
)
|
||||||
return wrcc, &rtmp.ConnConfig{
|
return wrcc, &rtmp.ConnConfig{
|
||||||
Handler: h,
|
Handler: h,
|
||||||
ControlState: rtmp.StreamControlStateConfig{
|
ControlState: rtmp.StreamControlStateConfig{
|
||||||
@@ -223,7 +233,11 @@ func (s *StreamServer) startServer(
|
|||||||
observability.Go(ctx, func() {
|
observability.Go(ctx, func() {
|
||||||
err = portSrv.Serve(listener)
|
err = portSrv.Serve(listener)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("unable to start serving RTMP at '%s': %w", listener.Addr().String(), err)
|
err = fmt.Errorf(
|
||||||
|
"unable to start serving RTMP at '%s': %w",
|
||||||
|
listener.Addr().String(),
|
||||||
|
err,
|
||||||
|
)
|
||||||
logger.Error(ctx, err)
|
logger.Error(ctx, err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@@ -52,9 +52,21 @@ func (fwds *StreamForwards) NewActiveStreamForward(
|
|||||||
pauseFunc func(ctx context.Context, fwd *ActiveStreamForwarding),
|
pauseFunc func(ctx context.Context, fwd *ActiveStreamForwarding),
|
||||||
opts ...Option,
|
opts ...Option,
|
||||||
) (_ret *ActiveStreamForwarding, _err error) {
|
) (_ret *ActiveStreamForwarding, _err error) {
|
||||||
logger.Debugf(ctx, "NewActiveStreamForward(ctx, '%s', '%s', relayService, pauseFunc)", streamID, urlString)
|
logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"NewActiveStreamForward(ctx, '%s', '%s', relayService, pauseFunc)",
|
||||||
|
streamID,
|
||||||
|
urlString,
|
||||||
|
)
|
||||||
defer func() {
|
defer func() {
|
||||||
logger.Debugf(ctx, "/NewActiveStreamForward(ctx, '%s', '%s', relayService, pauseFunc): %#+v %v", streamID, urlString, _ret, _err)
|
logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"/NewActiveStreamForward(ctx, '%s', '%s', relayService, pauseFunc): %#+v %v",
|
||||||
|
streamID,
|
||||||
|
urlString,
|
||||||
|
_ret,
|
||||||
|
_err,
|
||||||
|
)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
urlParsed, err := url.Parse(urlString)
|
urlParsed, err := url.Parse(urlString)
|
||||||
@@ -176,7 +188,10 @@ func (fwd *ActiveStreamForwarding) waitForPublisherAndStart(
|
|||||||
})
|
})
|
||||||
|
|
||||||
logger.Debugf(ctx, "DestinationStreamingLocker.Lock(ctx, '%s')", fwd.DestinationURL)
|
logger.Debugf(ctx, "DestinationStreamingLocker.Lock(ctx, '%s')", fwd.DestinationURL)
|
||||||
destinationUnlocker := fwd.StreamForwards.DestinationStreamingLocker.Lock(ctx, fwd.DestinationURL)
|
destinationUnlocker := fwd.StreamForwards.DestinationStreamingLocker.Lock(
|
||||||
|
ctx,
|
||||||
|
fwd.DestinationURL,
|
||||||
|
)
|
||||||
defer func() {
|
defer func() {
|
||||||
if destinationUnlocker != nil { // if ctx was cancelled before we locked then the unlocker is nil
|
if destinationUnlocker != nil { // if ctx was cancelled before we locked then the unlocker is nil
|
||||||
destinationUnlocker.Unlock()
|
destinationUnlocker.Unlock()
|
||||||
@@ -337,7 +352,12 @@ func (fwd *ActiveStreamForwarding) openOutputFor(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
recoderInstance recoder.Recoder,
|
recoderInstance recoder.Recoder,
|
||||||
) (recoder.Output, error) {
|
) (recoder.Output, error) {
|
||||||
output, err := recoderInstance.NewOutputFromURL(ctx, fwd.DestinationURL.String(), fwd.DestinationStreamKey, recoder.OutputConfig{})
|
output, err := recoderInstance.NewOutputFromURL(
|
||||||
|
ctx,
|
||||||
|
fwd.DestinationURL.String(),
|
||||||
|
fwd.DestinationStreamKey,
|
||||||
|
recoder.OutputConfig{},
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to open '%s' as the output: %w", fwd.DestinationURL, err)
|
return nil, fmt.Errorf("unable to open '%s' as the output: %w", fwd.DestinationURL, err)
|
||||||
}
|
}
|
||||||
|
@@ -72,7 +72,12 @@ func (s *StreamForwards) init(
|
|||||||
for dstID, dstCfg := range cfg.Destinations {
|
for dstID, dstCfg := range cfg.Destinations {
|
||||||
err := s.addActiveStreamDestination(ctx, dstID, dstCfg.URL, dstCfg.StreamKey)
|
err := s.addActiveStreamDestination(ctx, dstID, dstCfg.URL, dstCfg.StreamKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ret = fmt.Errorf("unable to initialize stream destination '%s' to %#+v: %w", dstID, dstCfg, err)
|
_ret = fmt.Errorf(
|
||||||
|
"unable to initialize stream destination '%s' to %#+v: %w",
|
||||||
|
dstID,
|
||||||
|
dstCfg,
|
||||||
|
err,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,7 +89,12 @@ func (s *StreamForwards) init(
|
|||||||
}
|
}
|
||||||
_, err := s.newActiveStreamForward(ctx, streamID, dstID, fwd.Quirks)
|
_, err := s.newActiveStreamForward(ctx, streamID, dstID, fwd.Quirks)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ret = fmt.Errorf("unable to launch stream forward from '%s' to '%s': %w", streamID, dstID, err)
|
_ret = fmt.Errorf(
|
||||||
|
"unable to launch stream forward from '%s' to '%s': %w",
|
||||||
|
streamID,
|
||||||
|
dstID,
|
||||||
|
err,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,7 +197,10 @@ func (s *StreamForwards) newActiveStreamForward(
|
|||||||
DestinationID: destinationID,
|
DestinationID: destinationID,
|
||||||
}
|
}
|
||||||
if _, ok := s.ActiveStreamForwardings[key]; ok {
|
if _, ok := s.ActiveStreamForwardings[key]; ok {
|
||||||
return nil, fmt.Errorf("there is already an active stream forwarding to '%s'", destinationID)
|
return nil, fmt.Errorf(
|
||||||
|
"there is already an active stream forwarding to '%s'",
|
||||||
|
destinationID,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
dst, err := s.findStreamDestinationByID(ctx, destinationID)
|
dst, err := s.findStreamDestinationByID(ctx, destinationID)
|
||||||
@@ -227,7 +240,10 @@ func (s *StreamForwards) newActiveStreamForward(
|
|||||||
) {
|
) {
|
||||||
if quirks.StartAfterYoutubeRecognizedStream.Enabled {
|
if quirks.StartAfterYoutubeRecognizedStream.Enabled {
|
||||||
if quirks.RestartUntilYoutubeRecognizesStream.Enabled {
|
if quirks.RestartUntilYoutubeRecognizesStream.Enabled {
|
||||||
logger.Errorf(ctx, "StartAfterYoutubeRecognizedStream should not be used together with RestartUntilYoutubeRecognizesStream")
|
logger.Errorf(
|
||||||
|
ctx,
|
||||||
|
"StartAfterYoutubeRecognizedStream should not be used together with RestartUntilYoutubeRecognizesStream",
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
logger.Debugf(ctx, "fwd %s->%s is waiting for YouTube to recognize the stream", streamID, destinationID)
|
logger.Debugf(ctx, "fwd %s->%s is waiting for YouTube to recognize the stream", streamID, destinationID)
|
||||||
started, err := s.PlatformsController.CheckStreamStartedByPlatformID(
|
started, err := s.PlatformsController.CheckStreamStartedByPlatformID(
|
||||||
@@ -284,13 +300,21 @@ func (s *StreamForwards) restartUntilYoutubeRecognizesStream(
|
|||||||
cfg types.RestartUntilYoutubeRecognizesStream,
|
cfg types.RestartUntilYoutubeRecognizesStream,
|
||||||
) {
|
) {
|
||||||
ctx = belt.WithField(ctx, "module", "restartUntilYoutubeRecognizesStream")
|
ctx = belt.WithField(ctx, "module", "restartUntilYoutubeRecognizesStream")
|
||||||
ctx = belt.WithField(ctx, "stream_forward", fmt.Sprintf("%s->%s", fwd.StreamID, fwd.DestinationID))
|
ctx = belt.WithField(
|
||||||
|
ctx,
|
||||||
|
"stream_forward",
|
||||||
|
fmt.Sprintf("%s->%s", fwd.StreamID, fwd.DestinationID),
|
||||||
|
)
|
||||||
|
|
||||||
logger.Debugf(ctx, "restartUntilYoutubeRecognizesStream(ctx, %#+v, %#+v)", fwd, cfg)
|
logger.Debugf(ctx, "restartUntilYoutubeRecognizesStream(ctx, %#+v, %#+v)", fwd, cfg)
|
||||||
defer func() { logger.Debugf(ctx, "restartUntilYoutubeRecognizesStream(ctx, %#+v, %#+v)", fwd, cfg) }()
|
defer func() { logger.Debugf(ctx, "restartUntilYoutubeRecognizesStream(ctx, %#+v, %#+v)", fwd, cfg) }()
|
||||||
|
|
||||||
if !cfg.Enabled {
|
if !cfg.Enabled {
|
||||||
logger.Errorf(ctx, "an attempt to start restartUntilYoutubeRecognizesStream when the hack is disabled for this stream forwarder: %#+v", cfg)
|
logger.Errorf(
|
||||||
|
ctx,
|
||||||
|
"an attempt to start restartUntilYoutubeRecognizesStream when the hack is disabled for this stream forwarder: %#+v",
|
||||||
|
cfg,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,21 +340,39 @@ func (s *StreamForwards) restartUntilYoutubeRecognizesStream(
|
|||||||
return
|
return
|
||||||
case <-time.After(cfg.StartTimeout):
|
case <-time.After(cfg.StartTimeout):
|
||||||
}
|
}
|
||||||
logger.Debugf(ctx, "waited %v, checking if the remote platform accepted the stream", cfg.StartTimeout)
|
logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"waited %v, checking if the remote platform accepted the stream",
|
||||||
|
cfg.StartTimeout,
|
||||||
|
)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
streamOK, err := s.PlatformsController.CheckStreamStartedByPlatformID(
|
streamOK, err := s.PlatformsController.CheckStreamStartedByPlatformID(
|
||||||
memoize.SetNoCache(ctx, true),
|
memoize.SetNoCache(ctx, true),
|
||||||
youtube.ID,
|
youtube.ID,
|
||||||
)
|
)
|
||||||
logger.Debugf(ctx, "the result of checking the stream on the remote platform: %v %v", streamOK, err)
|
logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"the result of checking the stream on the remote platform: %v %v",
|
||||||
|
streamOK,
|
||||||
|
err,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf(ctx, "unable to check if the stream with URL '%s' is started: %v", fwd.ActiveForwarding.DestinationURL, err)
|
logger.Errorf(
|
||||||
|
ctx,
|
||||||
|
"unable to check if the stream with URL '%s' is started: %v",
|
||||||
|
fwd.ActiveForwarding.DestinationURL,
|
||||||
|
err,
|
||||||
|
)
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if streamOK {
|
if streamOK {
|
||||||
logger.Debugf(ctx, "waiting %v to recheck if the stream will be still OK", cfg.StopStartDelay)
|
logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"waiting %v to recheck if the stream will be still OK",
|
||||||
|
cfg.StopStartDelay,
|
||||||
|
)
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
@@ -340,9 +382,19 @@ func (s *StreamForwards) restartUntilYoutubeRecognizesStream(
|
|||||||
memoize.SetNoCache(ctx, true),
|
memoize.SetNoCache(ctx, true),
|
||||||
youtube.ID,
|
youtube.ID,
|
||||||
)
|
)
|
||||||
logger.Debugf(ctx, "the result of checking the stream on the remote platform: %v %v", streamOK, err)
|
logger.Debugf(
|
||||||
|
ctx,
|
||||||
|
"the result of checking the stream on the remote platform: %v %v",
|
||||||
|
streamOK,
|
||||||
|
err,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf(ctx, "unable to check if the stream with URL '%s' is started: %v", fwd.ActiveForwarding.DestinationURL, err)
|
logger.Errorf(
|
||||||
|
ctx,
|
||||||
|
"unable to check if the stream with URL '%s' is started: %v",
|
||||||
|
fwd.ActiveForwarding.DestinationURL,
|
||||||
|
err,
|
||||||
|
)
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -353,7 +405,10 @@ func (s *StreamForwards) restartUntilYoutubeRecognizesStream(
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Infof(ctx, "the remote platform still does not see the stream, restarting the stream forwarding: stopping...")
|
logger.Infof(
|
||||||
|
ctx,
|
||||||
|
"the remote platform still does not see the stream, restarting the stream forwarding: stopping...",
|
||||||
|
)
|
||||||
|
|
||||||
err := fwd.ActiveForwarding.Stop()
|
err := fwd.ActiveForwarding.Stop()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -366,7 +421,10 @@ func (s *StreamForwards) restartUntilYoutubeRecognizesStream(
|
|||||||
case <-time.After(cfg.StopStartDelay):
|
case <-time.After(cfg.StopStartDelay):
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Infof(ctx, "the remote platform still does not see the stream, restarting the stream forwarding: starting...")
|
logger.Infof(
|
||||||
|
ctx,
|
||||||
|
"the remote platform still does not see the stream, restarting the stream forwarding: starting...",
|
||||||
|
)
|
||||||
|
|
||||||
err = fwd.ActiveForwarding.Start(ctx)
|
err = fwd.ActiveForwarding.Start(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -447,9 +505,12 @@ func (s *StreamForwards) ListStreamForwards(
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
return xsync.DoR2(ctx, &s.Mutex, func() ([]StreamForward, error) {
|
return xsync.DoR2(ctx, &s.Mutex, func() ([]StreamForward, error) {
|
||||||
return s.getStreamForwards(ctx, func(si types.StreamID, di ordered.Optional[types.DestinationID]) bool {
|
return s.getStreamForwards(
|
||||||
|
ctx,
|
||||||
|
func(si types.StreamID, di ordered.Optional[types.DestinationID]) bool {
|
||||||
return true
|
return true
|
||||||
})
|
},
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,7 +547,12 @@ func (s *StreamForwards) getStreamForwards(
|
|||||||
if !filterFunc(streamID, ordered.Optional[types.DestinationID]{}) {
|
if !filterFunc(streamID, ordered.Optional[types.DestinationID]{}) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
logger.Tracef(ctx, "len(s.Config.Streams[%s].Forwardings) == %d", streamID, len(stream.Forwardings))
|
logger.Tracef(
|
||||||
|
ctx,
|
||||||
|
"len(s.Config.Streams[%s].Forwardings) == %d",
|
||||||
|
streamID,
|
||||||
|
len(stream.Forwardings),
|
||||||
|
)
|
||||||
for dstID, cfg := range stream.Forwardings {
|
for dstID, cfg := range stream.Forwardings {
|
||||||
if !filterFunc(streamID, ordered.Opt(dstID)) {
|
if !filterFunc(streamID, ordered.Opt(dstID)) {
|
||||||
continue
|
continue
|
||||||
@@ -589,9 +655,12 @@ func (s *StreamForwards) GetStreamForwardsByDestination(
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
return xsync.DoR2(ctx, &s.Mutex, func() ([]StreamForward, error) {
|
return xsync.DoR2(ctx, &s.Mutex, func() ([]StreamForward, error) {
|
||||||
return s.getStreamForwards(ctx, func(streamID types.StreamID, dstID ordered.Optional[types.DestinationID]) bool {
|
return s.getStreamForwards(
|
||||||
|
ctx,
|
||||||
|
func(streamID types.StreamID, dstID ordered.Optional[types.DestinationID]) bool {
|
||||||
return !dstID.IsSet() || dstID.Get() == destID
|
return !dstID.IsSet() || dstID.Get() == destID
|
||||||
})
|
},
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -647,7 +716,15 @@ func (s *StreamForwards) UpdateStreamDestination(
|
|||||||
streamKey string,
|
streamKey string,
|
||||||
) error {
|
) error {
|
||||||
ctx = belt.WithField(ctx, "module", "StreamServer")
|
ctx = belt.WithField(ctx, "module", "StreamServer")
|
||||||
return xsync.DoA4R1(ctx, &s.Mutex, s.updateStreamDestination, ctx, destinationID, url, streamKey)
|
return xsync.DoA4R1(
|
||||||
|
ctx,
|
||||||
|
&s.Mutex,
|
||||||
|
s.updateStreamDestination,
|
||||||
|
ctx,
|
||||||
|
destinationID,
|
||||||
|
url,
|
||||||
|
streamKey,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StreamForwards) updateStreamDestination(
|
func (s *StreamForwards) updateStreamDestination(
|
||||||
@@ -659,14 +736,20 @@ func (s *StreamForwards) updateStreamDestination(
|
|||||||
s.WithConfig(ctx, func(ctx context.Context, cfg *types.Config) {
|
s.WithConfig(ctx, func(ctx context.Context, cfg *types.Config) {
|
||||||
for key := range s.ActiveStreamForwardings {
|
for key := range s.ActiveStreamForwardings {
|
||||||
if key.DestinationID == destinationID {
|
if key.DestinationID == destinationID {
|
||||||
_ret = fmt.Errorf("there is already an active stream forwarding to '%s'", destinationID)
|
_ret = fmt.Errorf(
|
||||||
|
"there is already an active stream forwarding to '%s'",
|
||||||
|
destinationID,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err := s.removeActiveStreamDestination(ctx, destinationID)
|
err := s.removeActiveStreamDestination(ctx, destinationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ret = fmt.Errorf("unable to remove (to then re-add) the active stream destination: %w", err)
|
_ret = fmt.Errorf(
|
||||||
|
"unable to remove (to then re-add) the active stream destination: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -725,9 +808,12 @@ func (s *StreamForwards) removeActiveStreamDestination(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
destinationID types.DestinationID,
|
destinationID types.DestinationID,
|
||||||
) error {
|
) error {
|
||||||
streamForwards, err := s.getStreamForwards(ctx, func(si types.StreamID, di ordered.Optional[types.DestinationID]) bool {
|
streamForwards, err := s.getStreamForwards(
|
||||||
|
ctx,
|
||||||
|
func(si types.StreamID, di ordered.Optional[types.DestinationID]) bool {
|
||||||
return true
|
return true
|
||||||
})
|
},
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to list stream forwardings: %w", err)
|
return fmt.Errorf("unable to list stream forwardings: %w", err)
|
||||||
}
|
}
|
||||||
@@ -756,7 +842,10 @@ func (s *StreamForwards) findStreamDestinationByID(
|
|||||||
return dst, nil
|
return dst, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return types.StreamDestination{}, fmt.Errorf("unable to find a stream destination by StreamID '%s'", destinationID)
|
return types.StreamDestination{}, fmt.Errorf(
|
||||||
|
"unable to find a stream destination by StreamID '%s'",
|
||||||
|
destinationID,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StreamForwards) getLocalhostEndpoint(ctx context.Context) (*url.URL, error) {
|
func (s *StreamForwards) getLocalhostEndpoint(ctx context.Context) (*url.URL, error) {
|
||||||
|
@@ -362,7 +362,10 @@ func (s *StreamPlayers) setStreamPlayer(
|
|||||||
|
|
||||||
var opts SetupOptions
|
var opts SetupOptions
|
||||||
if playerCfg.DefaultStreamPlayerOptions != nil {
|
if playerCfg.DefaultStreamPlayerOptions != nil {
|
||||||
opts = append(opts, SetupOptionDefaultStreamPlayerOptions(playerCfg.DefaultStreamPlayerOptions))
|
opts = append(
|
||||||
|
opts,
|
||||||
|
SetupOptionDefaultStreamPlayerOptions(playerCfg.DefaultStreamPlayerOptions),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var streamCfg map[types.StreamID]*types.StreamConfig
|
var streamCfg map[types.StreamID]*types.StreamConfig
|
||||||
|
@@ -9,5 +9,8 @@ import (
|
|||||||
|
|
||||||
type PlatformsController interface {
|
type PlatformsController interface {
|
||||||
CheckStreamStartedByURL(ctx context.Context, destination *url.URL) (bool, error)
|
CheckStreamStartedByURL(ctx context.Context, destination *url.URL) (bool, error)
|
||||||
CheckStreamStartedByPlatformID(ctx context.Context, platID streamcontrol.PlatformName) (bool, error)
|
CheckStreamStartedByPlatformID(
|
||||||
|
ctx context.Context,
|
||||||
|
platID streamcontrol.PlatformName,
|
||||||
|
) (bool, error)
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user