From 73b9d71b0d94129cd3d0421f17bcccbc04dee168 Mon Sep 17 00:00:00 2001 From: Abdellatif Anaflous <62770500+hktitof@users.noreply.github.com> Date: Thu, 10 Sep 2026 12:45:30 +0100 Subject: [PATCH] fix(capture): accept a bare pipeline string in video pipelines entries (#701) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(capture): accept a bare pipeline string in video pipelines entries NEKO_CAPTURE_VIDEO_PIPELINES expects every entry to be a full pipeline config object, but the singular capture.video.pipeline option trains people to write the whole pipeline as a plain string, so a dict like {"high": "ximagesrc ... ! appsink"} failed to unmarshal, the error got reduced to a warn and neko fell back to the default vp8 pipeline silently, also overriding a directly set video codec the json string hook decodes the env value into a generic map, so the shorthand is restored in the mapstructure chain: a string value for an entry now becomes VideoConfig{GstPipeline: ...}, the same shape the single pipeline option produces, object entries are untouched the new config test drives the production env flow (Init + Set) and fails on master with the same two warnings the reporter saw Fixes #689 * feat(capture): add shorthand support for pipeline entries as plain strings. --------- Co-authored-by: Miroslav Šedivý --- server/internal/config/capture.go | 18 +++++++- server/internal/config/capture_test.go | 58 ++++++++++++++++++++++++++ webpage/docs/configuration/capture.md | 12 ++++++ 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 server/internal/config/capture_test.go diff --git a/server/internal/config/capture.go b/server/internal/config/capture.go index ff7fa727..048c3f94 100644 --- a/server/internal/config/capture.go +++ b/server/internal/config/capture.go @@ -2,8 +2,10 @@ package config import ( "os" + "reflect" "strings" + "github.com/go-viper/mapstructure/v2" "github.com/pion/webrtc/v4" "github.com/rs/zerolog/log" "github.com/spf13/cobra" @@ -322,6 +324,17 @@ func (Capture) InitV2(cmd *cobra.Command) error { return nil } +// videoPipelineShorthandHook accepts a bare pipeline string in place of a full +// video pipeline config, mirroring the capture.video.pipeline shorthand. +func videoPipelineShorthandHook() mapstructure.DecodeHookFunc { + return func(from reflect.Type, to reflect.Type, data any) (any, error) { + if from.Kind() == reflect.String && to == reflect.TypeOf(types.VideoConfig{}) { + return types.VideoConfig{GstPipeline: data.(string)}, nil + } + return data, nil + } +} + func (s *Capture) Set() { var ok bool @@ -342,7 +355,10 @@ func (s *Capture) Set() { s.VideoIDs = viper.GetStringSlice("capture.video.ids") if err := viper.UnmarshalKey("capture.video.pipelines", &s.VideoPipelines, viper.DecodeHook( - utils.JsonStringAutoDecode(s.VideoPipelines), + mapstructure.ComposeDecodeHookFunc( + utils.JsonStringAutoDecode(s.VideoPipelines), + videoPipelineShorthandHook(), + ), )); err != nil { log.Warn().Err(err).Msgf("unable to parse video pipelines") } diff --git a/server/internal/config/capture_test.go b/server/internal/config/capture_test.go new file mode 100644 index 00000000..fda6fed1 --- /dev/null +++ b/server/internal/config/capture_test.go @@ -0,0 +1,58 @@ +package config + +import ( + "strings" + "testing" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// loadCaptureFromEnv mirrors the production config flow: env prefix setup, +// flag registration via Init, then value population via Set. +func loadCaptureFromEnv(t *testing.T, pipelines string) *Capture { + t.Helper() + + viper.Reset() + viper.SetEnvPrefix("NEKO") + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + viper.AutomaticEnv() + + t.Setenv("NEKO_CAPTURE_VIDEO_PIPELINES", pipelines) + + c := &Capture{} + cmd := &cobra.Command{} + if err := c.Init(cmd); err != nil { + t.Fatal(err) + } + c.Set() + return c +} + +func TestVideoPipelinesEnv(t *testing.T) { + const pipeline = "ximagesrc display-name={display} show-pointer=false ! appsink name=appsink" + + t.Run("object entries with gst_pipeline keep working", func(t *testing.T) { + c := loadCaptureFromEnv(t, `{"high":{"gst_pipeline":"`+pipeline+`"}}`) + + cfg, ok := c.VideoPipelines["high"] + if !ok { + t.Fatalf("pipeline entry missing, got %v", c.VideoPipelines) + } + if cfg.GstPipeline != pipeline { + t.Errorf("expected pipeline %q, got %q", pipeline, cfg.GstPipeline) + } + }) + + t.Run("bare pipeline string entries are accepted", func(t *testing.T) { + c := loadCaptureFromEnv(t, `{"high":"`+pipeline+`"}`) + + cfg, ok := c.VideoPipelines["high"] + if !ok { + t.Fatalf("bare pipeline string entry was dropped and neko fell back to the default vp8 pipeline, got %v", c.VideoPipelines) + } + if cfg.GstPipeline != pipeline { + t.Errorf("expected pipeline %q, got %q", pipeline, cfg.GstPipeline) + } + }) +} diff --git a/webpage/docs/configuration/capture.md b/webpage/docs/configuration/capture.md index 00bfd15d..64b8fdf5 100644 --- a/webpage/docs/configuration/capture.md +++ b/webpage/docs/configuration/capture.md @@ -165,6 +165,18 @@ capture: gst_pipeline: "" ``` +As a shorthand, a pipeline entry can also be a plain string instead of an object, which is equivalent to setting only : + +```yaml title="config.yaml" +capture: + video: + ... + pipelines: + : "" +``` + +This shorthand also works when defining via the `NEKO_CAPTURE_VIDEO_PIPELINES` environment variable as a JSON object, e.g. `NEKO_CAPTURE_VIDEO_PIPELINES='{"":""}'`. + Since now you have to define the whole pipeline, you need to specify the src element to get the video frames and the sink element to send the encoded video frames to neko. In your pipeline, you can use `{display}` as a placeholder for the display name that will be replaced by the actual display name at runtime. You need to set the `name` property of the sink element to `appsink` so that neko can capture the video frames. Your typical pipeline string would look like this: