feat: implement initial configuration and gstreamer pipeline setup

This commit is contained in:
Garionion 2025-04-09 18:10:34 +02:00
parent d672f04be2
commit 4a6f36f1ef
Signed by: garionion
SSH key fingerprint: SHA256:6uQQGh4dHIdYnrR+qeLdgx5SDmbttGp2HusA73563QA
9 changed files with 566 additions and 50 deletions

View file

@ -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";
};
};
});

32
go.mod
View file

@ -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
)

58
go.sum
View file

@ -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=

32
internal/config/confgi.go Normal file
View file

@ -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
}

View file

@ -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)
//}
}

View file

@ -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
}

60
internal/ndi/discovery.go Normal file
View file

@ -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())
}
}

74
main.go
View file

@ -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()
}

View file

@ -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;
}