diff --git a/flake.nix b/flake.nix index 317ca78..e3bab37 100644 --- a/flake.nix +++ b/flake.nix @@ -6,29 +6,42 @@ flake-utils.url = "github:numtide/flake-utils"; }; - outputs = { self, nixpkgs, flake-utils, ... }: + outputs = {self, nixpkgs, flake-utils, ... }: flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { system = system; config.allowUnfree = true; }; in { devShell = pkgs.mkShell { - buildInputs = [ - pkgs.go - pkgs.ndi + buildInputs = with pkgs; [ + go_1_23 + + gst_all_1.gstreamer + gst_all_1.gstreamer.dev + gst_all_1.gst-plugins-base + gst_all_1.gst-plugins-good + gst_all_1.gst-plugins-bad + gst_all_1.gst-plugins-ugly + + gcc glib pkg-config + ndi ]; LIBNDI="${pkgs.ndi}/lib/libndi.so"; CFLAGS="-I${pkgs.ndi}/include"; LDFLAGS="-L${pkgs.ndi}/lib -lndi"; + + shellHook = '' + zsh + ''; }; packages = { - ndi_discover = import ./package.nix { inherit pkgs; inherit self; }; + catie = import ./package.nix { inherit pkgs; inherit self; }; }; defaultPackage = import ./package.nix { inherit pkgs; inherit self; }; apps = { - ndi_discover = { + catie = { type = "app"; - program = "${self.packages.${system}.ndi_discover}/bin/ndi_discover"; + program = "${self.packages.${system}.ndi_discover}/bin/catie"; }; }; }); diff --git a/go.mod b/go.mod index cbbcdc5..5b7ec0d 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,33 @@ module git.entr0py.de/garionion/catie -go 1.24 +go 1.23.1 -require github.com/bitfocus/gondi v0.0.2 +require ( + github.com/bitfocus/gondi v0.0.2 + github.com/go-gst/go-glib v1.4.0 + github.com/go-gst/go-gst v1.4.0 + github.com/goccy/go-graphviz v0.2.9 + github.com/ilyakaznacheev/cleanenv v1.5.0 + github.com/rs/zerolog v1.34.0 +) -require github.com/ebitengine/purego v0.3.2 // indirect +require ( + github.com/BurntSushi/toml v1.2.1 // indirect + github.com/disintegration/imaging v1.6.2 // indirect + github.com/ebitengine/purego v0.3.2 // indirect + github.com/flopp/go-findfont v0.1.0 // indirect + github.com/fogleman/gg v1.3.0 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-pointer v0.0.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/tetratelabs/wazero v1.8.1 // indirect + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect + golang.org/x/image v0.21.0 // indirect + golang.org/x/sys v0.12.0 // indirect + golang.org/x/text v0.19.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect +) diff --git a/go.sum b/go.sum index ee47d87..1efa8b0 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,62 @@ +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/bitfocus/gondi v0.0.2 h1:Q/kscMhnp0iX+Vkz274vG1loBw2lKyq5yq4b1XjllTE= github.com/bitfocus/gondi v0.0.2/go.mod h1:g/pdkw//j2qJD+l9Yv8xf3RtjnrwBrZQBmlYIJ8GXQs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/corona10/goimagehash v1.1.0 h1:teNMX/1e+Wn/AYSbLHX8mj+mF9r60R1kBeqE9MkoYwI= +github.com/corona10/goimagehash v1.1.0/go.mod h1:VkvE0mLn84L4aF8vCb6mafVajEb6QYMHl2ZJLn0mOGI= +github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/ebitengine/purego v0.3.2 h1:+pV+tskAkn/bxEcUzGtDfw2VAe3bRQ26kdzFjPPrCww= github.com/ebitengine/purego v0.3.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/flopp/go-findfont v0.1.0 h1:lPn0BymDUtJo+ZkV01VS3661HL6F4qFlkhcJN55u6mU= +github.com/flopp/go-findfont v0.1.0/go.mod h1:wKKxRDjD024Rh7VMwoU90i6ikQRCr+JTHB5n4Ejkqvw= +github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= +github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/go-gst/go-glib v1.4.0 h1:FB2uVfB0uqz7/M6EaDdWWlBZRQpvFAbWfL7drdw8lAE= +github.com/go-gst/go-glib v1.4.0/go.mod h1:GUIpWmkxQ1/eL+FYSjKpLDyTZx6Vgd9nNXt8dA31d5M= +github.com/go-gst/go-gst v1.4.0 h1:EikB43u4c3wc8d2RzlFRSfIGIXYzDy6Zls2vJqrG2BU= +github.com/go-gst/go-gst v1.4.0/go.mod h1:p8TLGtOxJLcrp6PCkTPdnanwWBxPZvYiHDbuSuwgO3c= +github.com/goccy/go-graphviz v0.2.9 h1:4yD2MIMpxNt+sOEARDh5jTE2S/jeAKi92w72B83mWGg= +github.com/goccy/go-graphviz v0.2.9/go.mod h1:hssjl/qbvUXGmloY81BwXt2nqoApKo7DFgDj5dLJGb8= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4= +github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0= +github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/tetratelabs/wazero v1.8.1 h1:NrcgVbWfkWvVc4UtT4LRLDf91PsOzDzefMdwhLfA550= +github.com/tetratelabs/wazero v1.8.1/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s= +golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ= +olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw= diff --git a/internal/config/confgi.go b/internal/config/confgi.go new file mode 100644 index 0000000..08f4696 --- /dev/null +++ b/internal/config/confgi.go @@ -0,0 +1,32 @@ +package config + +import "github.com/ilyakaznacheev/cleanenv" + +type Config struct { + Debug bool `toml:"debug" env:"DEBUG" env-default:"false"` + Pipeline Pipeline `toml:"pipeline"` + Outputs []Output `toml:"outputs"` +} + +type Output struct { + Type string `toml:"type" env:"OUTPUT_TYPE"` + Target string `toml:"target" env:"OUTPUT_TARGET"` +} + +type Pipeline struct { + Width int `toml:"width" env:"PIPELINE_WIDTH" env-default:"50"` + Height int `toml:"height" env:"PIPELINE_HEIGHT" env-default:"50"` +} + +func LoadConfig(path string) (*Config, error) { + var cfg Config + err := cleanenv.ReadConfig(path, &cfg) + if err != nil { + // It's often useful to ignore "file not found" errors if you want + // to allow configuration purely via environment variables. + // However, the specific behavior depends on the application's needs. + // For now, we'll return the error. + return nil, err + } + return &cfg, nil +} diff --git a/internal/gstreamer/debug.go b/internal/gstreamer/debug.go new file mode 100644 index 0000000..910c50b --- /dev/null +++ b/internal/gstreamer/debug.go @@ -0,0 +1,46 @@ +package gstreamer + +import ( + "context" + "fmt" + "github.com/go-gst/go-gst/gst" + "github.com/goccy/go-graphviz" + "github.com/goccy/go-graphviz/cgraph" +) + +func gstreamerBinToPNG(bin *gst.Bin, filename string) error { + ctx := context.Background() + g, err := graphviz.New(ctx) + if err != nil { + return fmt.Errorf("error creating graphviz context: %w", err) + } + + dotString := bin.DebugBinToDotData(gst.DebugGraphShowAll) + graph, err := cgraph.ParseBytes([]byte(dotString)) + if err != nil { + return err + } + + //graph.SetDPI(150) + //setFontSize(graph, 20) + + if err := g.RenderFilename(ctx, graph, graphviz.PNG, filename); err != nil { + return fmt.Errorf("error rendering graph: %w", err) + } + + return nil +} + +func setFontSize(graph *cgraph.Graph, fontSize float64) { + graph.SetFontSize(20) + graph.Set("arrowSize", "3") + for node, err := graph.FirstNode(); err == nil && node != nil; node, err = graph.NextNode(node) { + node.SetFontSize(fontSize) + for edge, err := graph.FirstEdge(node); err == nil && edge != nil; edge, err = graph.NextEdge(edge, node) { + edge.SetFontSize(fontSize) + } + } + //for subGraph, err := graph.FirstSubGraph(); err == nil && subGraph != nil; subGraph, err = graph.NextSubGraph() { + // setFontSize(subGraph, fontSize) + //} +} diff --git a/internal/gstreamer/gstreamer.go b/internal/gstreamer/gstreamer.go new file mode 100644 index 0000000..186b288 --- /dev/null +++ b/internal/gstreamer/gstreamer.go @@ -0,0 +1,249 @@ +package gstreamer + +import ( + "fmt" + "git.entr0py.de/garionion/catie/internal/config" + "github.com/go-gst/go-glib/glib" + "github.com/go-gst/go-gst/gst" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +type OutputType string + +const ( + OutputTypeNdi OutputType = "ndi" + OutputTypeWayland OutputType = "waylandsink" +) + +type Gstreamer struct { + logger zerolog.Logger + debug bool + + pipelineCfg config.Pipeline + outputCfg []config.Output + + pipeline *gst.Pipeline + mainLoop *glib.MainLoop +} + +func New(pipelineCfg config.Pipeline, outputCfg []config.Output, debug bool) (*Gstreamer, error) { + logger := log.Logger.With().Str("module", "gstreamer").Logger() + + gst.Init(nil) + + mainLoop := glib.NewMainLoop(glib.MainContextDefault(), false) + go mainLoop.Run() + + pipeline, err := gst.NewPipeline("graphix") + if err != nil { + return nil, fmt.Errorf("creating pipeline: %w", err) + } + + g := &Gstreamer{ + logger: logger, + debug: debug, + + pipelineCfg: pipelineCfg, + outputCfg: outputCfg, + + pipeline: pipeline, + mainLoop: mainLoop, + } + + go g.pipelineWatcher() + + logger.Debug().Msg("create initial video source") + plainVideoSrc, err := gst.NewElementWithProperties("videotestsrc", map[string]interface{}{ + "name": "initSrc", + "pattern": 2, // black + }) + if err != nil { + return nil, fmt.Errorf("creating initial videotestsrc: %w", err) + } + + logger.Debug().Msg("adding initial video source to pipeline") + if err := pipeline.Add(plainVideoSrc); err != nil { + return nil, fmt.Errorf("adding initial video source to pipeline: %w", err) + } + + logger.Debug().Msg("create compositor") + compositor, err := gst.NewElementWithProperties("compositor", map[string]interface{}{ + "name": "compositor", + "ignore-inactive-pads": true, + "background": 3, // transparent + }) + if err != nil { + return nil, fmt.Errorf("creating compositor: %w", err) + } + + logger.Debug().Msg("adding compositor to pipeline") + if err := pipeline.Add(compositor); err != nil { + return nil, fmt.Errorf("adding compositor to pipeline: %w", err) + } + + logger.Debug().Msg("linking initial video source to compositor") + caps := gst.NewCapsFromString(fmt.Sprintf("video/x-raw,format=BGRA,width=%d,height=%d", pipelineCfg.Width, pipelineCfg.Height)) + if err := plainVideoSrc.LinkFiltered(compositor, caps); err != nil { + return nil, fmt.Errorf("linking initial video source to compositor: %w", err) + } + + logger.Debug().Msg("setting initial video source to transparent") + pads, err := compositor.GetSinkPads() + if err != nil { + return nil, fmt.Errorf("getting sink pads from compositor: %w", err) + } + if len(pads) == 0 { + return nil, fmt.Errorf("no compositor sink pads found") + } + if err := pads[0].SetProperty("alpha", 0.0); err != nil { + return nil, fmt.Errorf("setting compositor sink pad alpha: %w", err) + } + + logger.Debug().Msg("creating tee") + tee, err := gst.NewElement("tee") + if err != nil { + return nil, fmt.Errorf("creating tee element: %w", err) + } + + logger.Debug().Msg("adding tee to pipeline") + if err := pipeline.Add(tee); err != nil { + return nil, fmt.Errorf("adding tee to pipeline: %w", err) + } + + if debug { + logger.Debug().Msg("create timeoverlay") + timeoverlay, err := gst.NewElement("timeoverlay") + if err != nil { + return nil, fmt.Errorf("creating timeoverlay element: %w", err) + } + + logger.Debug().Msg("add timeoverlay to pipeline") + if err := pipeline.Add(timeoverlay); err != nil { + return nil, fmt.Errorf("adding timeoverlay to pipeline: %w", err) + } + + logger.Debug().Msg("link timeoverlay to compositor") + if err := compositor.Link(timeoverlay); err != nil { + return nil, fmt.Errorf("linking compositor to timeoverlay: %w", err) + } + + logger.Debug().Msg("link tee to timeoverlay") + if err := timeoverlay.Link(tee); err != nil { + return nil, fmt.Errorf("linking tee to timeoverlay: %w", err) + } + } else { + logger.Debug().Msg("link tee to compositor") + if err := compositor.Link(tee); err != nil { + return nil, fmt.Errorf("linking compositor to tee: %w", err) + } + } + + logger.Debug().Msg("creating outputs") + for _, output := range outputCfg { + logger.Debug().Str("type", output.Type).Str("target", output.Target).Msg("creating output") + switch output.Type { + case string(OutputTypeNdi): + logger.Debug().Msg("create ndisink") + ndiSink, err := gst.NewElementWithProperties("ndisink", map[string]interface{}{ + "ndi-name": output.Target, + }) + if err != nil { + return nil, fmt.Errorf("creating ndisink %q element: %w", output.Target, err) + } + + logger.Debug().Msg("adding ndisink to pipeline") + if err := pipeline.Add(ndiSink); err != nil { + return nil, fmt.Errorf("adding ndisink %q to pipeline: %w", output.Target, err) + } + + logger.Debug().Msg("linking ndisink to tee") + teePad := tee.GetRequestPad("src_%u") + ndiPad := ndiSink.GetStaticPad("sink") + if plr := teePad.Link(ndiPad); plr != gst.PadLinkOK { + return nil, fmt.Errorf("linking ndisink %q to tee: PadLinkReturn: %v", output.Target, plr) + } + case string(OutputTypeWayland): + logger.Debug().Msg("create waylandsink") + waylandSink, err := gst.NewElement("waylandsink") + if err != nil { + return nil, fmt.Errorf("creating waylandsink %q element: %w", output.Target, err) + } + + logger.Debug().Msg("adding waylandsink to pipeline") + if err := pipeline.Add(waylandSink); err != nil { + return nil, fmt.Errorf("adding waylandsink %q to pipeline: %w", output.Target, err) + } + + logger.Debug().Msg("linking waylandsink to tee") + teePad := tee.GetRequestPad("src_%u") + waylandPad := waylandSink.GetStaticPad("sink") + if plr := teePad.Link(waylandPad); plr != gst.PadLinkOK { + return nil, fmt.Errorf("linking waylandsink to tee: PadLinkReturn: %v", plr) + } + } + } + + if debug { + logger.Debug().Msg("create png of pipeline") + if err := gstreamerBinToPNG(pipeline.Bin, "pipeline.png"); err != nil { + logger.Error().Err(err).Msg("creating png of pipeline") + } + } + + return g, nil +} + +func (g *Gstreamer) Run() error { + g.logger.Info().Msg("starting gstreamer") + if err := g.pipeline.SetState(gst.StatePlaying); err != nil { + return fmt.Errorf("error setting pipeline to playing: %w", err) + } + + return nil +} + +func (g *Gstreamer) pipelineWatcher() { + g.pipeline.GetPipelineBus().AddWatch(func(msg *gst.Message) bool { + switch msg.Type() { + case gst.MessageEOS: // When end-of-stream is received flush the pipeling and stop the main loop + err := g.pipeline.BlockSetState(gst.StateNull) + if err != nil { + return false + } + g.mainLoop.Quit() + case gst.MessageError: // Error messages are always fatal + err := msg.ParseError() + g.logger.Error().Err(err) + if debug := err.DebugString(); debug != "" { + g.logger.Debug().Msg(debug) + } + g.mainLoop.Quit() + default: + if g.debug { + g.logger.Debug().Msg(msg.String()) + } + + } + return true + }) + +} + +func (g *Gstreamer) setElementProperties(element *gst.Element, properties map[string]interface{}) error { + for key, value := range properties { + if err := g.setElementProperty(element, key, value); err != nil { + return err + } + } + + return nil +} + +func (g *Gstreamer) setElementProperty(element *gst.Element, key string, value interface{}) error { + if err := element.SetProperty(key, value); err != nil { + return fmt.Errorf("error setting property %q: %w", key, err) + } + + return nil +} diff --git a/internal/ndi/discovery.go b/internal/ndi/discovery.go new file mode 100644 index 0000000..0c8d805 --- /dev/null +++ b/internal/ndi/discovery.go @@ -0,0 +1,60 @@ +package ndi + +import ( + "fmt" + "github.com/bitfocus/gondi" + "os" +) + +var NDI_LIB_PATH = "" + +func discovery() { + var ( + libndi = "" + libndiEnv = os.Getenv("LIBNDI") + ) + if libndiEnv != "" { + libndi = libndiEnv + } else if NDI_LIB_PATH != "" { + libndi = fmt.Sprintf("%s/lib/libndi.so", NDI_LIB_PATH) + } else { + fmt.Println("LIBNDI environment variable is not set") + return + } + + fmt.Println("Initializing NDI") + if err := gondi.InitLibrary(libndi); err != nil { + fmt.Println("Failed to initialize NDI library:", err) + return + } + + version := gondi.GetVersion() + fmt.Printf("NDI version: %s\n", version) + + findInstance, err := gondi.NewFindInstance(true, "", "") + if err != nil { + panic(err) + } + defer findInstance.Destroy() + + // Wait for sources to appear + fmt.Println("Looking for sources...") + for { + more := findInstance.WaitForSources(5000) + if !more { + break + } + } + + // Fetch the sources + sources := findInstance.GetCurrentSources() + + if len(sources) == 0 { + fmt.Println("No sources found") + return + } + + for _, source := range sources { + fmt.Printf("Found source: %q: %q\n", source.Name(), source.Address()) + } +} diff --git a/main.go b/main.go index bdbc169..c159611 100644 --- a/main.go +++ b/main.go @@ -1,52 +1,54 @@ package main import ( - "fmt" + "flag" + "git.entr0py.de/garionion/catie/internal/config" + "git.entr0py.de/garionion/catie/internal/gstreamer" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/rs/zerolog/pkgerrors" "os" - - "github.com/bitfocus/gondi" + "sync" + "time" ) func main() { - libndi := os.Getenv("LIBNDI") - if libndi == "" { - fmt.Println("LIBNDI environment variable is not set") - return - } + zerolog.TimeFieldFormat = time.RFC1123Z + consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC1123Z} + log.Logger = log.Output(consoleWriter).With().Timestamp().Caller().Logger() - fmt.Println("Initializing NDI") - if err := gondi.InitLibrary(libndi); err != nil { - fmt.Println("Failed to initialize NDI library:", err) - return - } + // --- Configuration Loading --- + configPath := flag.String("config", "config.toml", "Path to the configuration file") + flag.Parse() - version := gondi.GetVersion() - fmt.Printf("NDI version: %s\n", version) - - findInstance, err := gondi.NewFindInstance(true, "", "") + cfg, err := config.LoadConfig(*configPath) if err != nil { - panic(err) - } - defer findInstance.Destroy() - - // Wait for sources to appear - fmt.Println("Looking for sources...") - for { - more := findInstance.WaitForSources(5000) - if !more { - break - } + log.Fatal().Err(err).Msgf("Failed to load configuration from %s", *configPath) } - // Fetch the sources - sources := findInstance.GetCurrentSources() - - if len(sources) == 0 { - fmt.Println("No sources found") - return + // --- Logging Setup --- + if cfg.Debug { + zerolog.SetGlobalLevel(zerolog.DebugLevel) } - for _, source := range sources { - fmt.Printf("Found source: %q: %q\n", source.Name(), source.Address()) + if log.Logger.GetLevel() == zerolog.DebugLevel { + zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack } + + log.Debug().Interface("config", cfg).Msg("starting application") + + gst, err := gstreamer.New(cfg.Pipeline, cfg.Outputs, cfg.Debug) + if err != nil { + log.Fatal().Err(err).Msg("Failed to create gstreamer") + } + + var wg sync.WaitGroup + wg.Add(1) + if err := gst.Run(); err != nil { + log.Fatal().Err(err).Msg("Failed to run gstreamer") + wg.Done() + } + + log.Info().Msg("Gstreamer is running") + wg.Wait() } diff --git a/package.nix b/package.nix index 0727f36..abe6178 100644 --- a/package.nix +++ b/package.nix @@ -2,25 +2,55 @@ with pkgs; let version = "0.0.1"; + gstPluginPath = lib.makeSearchPath "lib/gstreamer-1.0" [ + gst_all_1.gstreamer + gst_all_1.gst-plugins-base + gst_all_1.gst-plugins-good + gst_all_1.gst-plugins-bad + gst_all_1.gst-plugins-ugly + ]; in -pkgs.buildGo124Module { +pkgs.buildGo123Module { pname = "catie"; inherit version; - src = self; + src = lib.cleanSource self; buildInputs = [ + gst_all_1.gstreamer + gst_all_1.gstreamer.dev + gst_all_1.gst-plugins-base + gst_all_1.gst-plugins-good + gst_all_1.gst-plugins-bad + gst_all_1.gst-plugins-ugly + glib ndi ]; + nativeBuildInputs = [ + gcc glib + pkg-config + makeWrapper + ]; + + ldflags = [ + "-X git.entr0py.de/garionion/catie/internal/ndi.NDI_LIB_PATH=${pkgs.ndi}" + ]; + buildFlags = [ "CGO_CFLAGS=-I${pkgs.ndi}/include" "CGO_LDFLAGS=-L${pkgs.ndi}/lib -lndi" ]; + postInstall = '' + wrapProgram $out/bin/catie \ + --set GST_PLUGIN_SYSTEM_PATH ${gstPluginPath} \ + --set GST_PLUGIN_PATH ${gstPluginPath} + ''; + tags = [ ]; #vendorHash = lib.fakeHash; - vendorHash = "sha256-d0dcW2uV+a2GLBcY3FgNXNeajiJjFLEyCgqwZsEpW60="; - proxyVendor = true; + vendorHash = "sha256-/D1ZF4ordHROjrDQxrR/lNvsRFW9u4mDGWZE+M/zO/U="; + #proxyVendor = true; } \ No newline at end of file