feat: implement initial configuration and gstreamer pipeline setup
This commit is contained in:
parent
d672f04be2
commit
4a6f36f1ef
9 changed files with 566 additions and 50 deletions
27
flake.nix
27
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";
|
||||
};
|
||||
};
|
||||
});
|
||||
|
|
32
go.mod
32
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
|
||||
)
|
||||
|
|
58
go.sum
58
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=
|
||||
|
|
32
internal/config/confgi.go
Normal file
32
internal/config/confgi.go
Normal 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
|
||||
}
|
46
internal/gstreamer/debug.go
Normal file
46
internal/gstreamer/debug.go
Normal 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)
|
||||
//}
|
||||
}
|
249
internal/gstreamer/gstreamer.go
Normal file
249
internal/gstreamer/gstreamer.go
Normal 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
60
internal/ndi/discovery.go
Normal 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
74
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()
|
||||
}
|
||||
|
|
38
package.nix
38
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;
|
||||
}
|
Loading…
Add table
Reference in a new issue