diff --git a/examples/plugins/registered_elements/.gitignore b/examples/plugins/registered_elements/.gitignore new file mode 100644 index 0000000..707ee20 --- /dev/null +++ b/examples/plugins/registered_elements/.gitignore @@ -0,0 +1,2 @@ +*.dot +__debug* \ No newline at end of file diff --git a/examples/plugins/registered_elements/README.md b/examples/plugins/registered_elements/README.md new file mode 100644 index 0000000..6ad3273 --- /dev/null +++ b/examples/plugins/registered_elements/README.md @@ -0,0 +1,8 @@ +# Registered Elements example + +This example shows how you can define custom gstreamer elements in go, register them in the element factory and use them in the same application in a pipeline. + +We define two elements: + +* `gocustombin` a custom GstBin that uses an audiomixer to aggregate the input of two `gocustomsrc` +* `gocustomsrc` a custom GstBin that uses an audiotestsrc and a volume element. \ No newline at end of file diff --git a/examples/plugins/registered_elements/internal/common/assert.go b/examples/plugins/registered_elements/internal/common/assert.go new file mode 100644 index 0000000..1cd3d33 --- /dev/null +++ b/examples/plugins/registered_elements/internal/common/assert.go @@ -0,0 +1,11 @@ +package common + +import "fmt" + +var FinalizersCalled int = 0 + +func AssertFinalizersCalled(x int) { + if FinalizersCalled != x { + panic(fmt.Sprintf("finalizers did not run correctly, memory leak, wanted: %d, got: %d", x, FinalizersCalled)) + } +} diff --git a/examples/plugins/registered_elements/internal/common/util.go b/examples/plugins/registered_elements/internal/common/util.go new file mode 100644 index 0000000..f3ecacb --- /dev/null +++ b/examples/plugins/registered_elements/internal/common/util.go @@ -0,0 +1,9 @@ +package common + +func Must[T any](v T, err error) T { + if err != nil { + panic("got error:" + err.Error()) + } + + return v +} diff --git a/examples/plugins/registered_elements/internal/custombin/element.go b/examples/plugins/registered_elements/internal/custombin/element.go new file mode 100644 index 0000000..31db7f0 --- /dev/null +++ b/examples/plugins/registered_elements/internal/custombin/element.go @@ -0,0 +1,87 @@ +package custombin + +import ( + "time" + + "github.com/go-gst/go-glib/glib" + "github.com/go-gst/go-gst/examples/plugins/registered_elements/internal/common" + "github.com/go-gst/go-gst/gst" +) + +type customBin struct { + // self *gst.Bin + source1 *gst.Element + source2 *gst.Element + mixer *gst.Element +} + +// ClassInit is the place where you define pads and properties +func (*customBin) ClassInit(klass *glib.ObjectClass) { + class := gst.ToElementClass(klass) + class.SetMetadata( + "custom test source", + "Src/Test", + "Demo source bin with volume", + "Wilhelm Bartel ", + ) + class.AddPadTemplate(gst.NewPadTemplate( + "src", + gst.PadDirectionSource, + gst.PadPresenceAlways, + gst.NewCapsFromString("audio/x-raw,channels=2,rate=48000"), + )) +} + +// SetProperty gets called for every property. The id is the index in the slice defined above. +func (s *customBin) SetProperty(self *glib.Object, id uint, value *glib.Value) {} + +// GetProperty is called to retrieve the value of the property at index `id` in the properties +// slice provided at ClassInit. +func (o *customBin) GetProperty(self *glib.Object, id uint) *glib.Value { + return nil +} + +// New is called by the bindings to create a new instance of your go element. Use this to initialize channels, maps, etc. +// +// Think of New like the constructor of your struct +func (*customBin) New() glib.GoObjectSubclass { + return &customBin{} +} + +// InstanceInit should initialize the element. Keep in mind that the properties are not yet present. When this is called. +func (s *customBin) InstanceInit(instance *glib.Object) { + self := gst.ToGstBin(instance) + + s.source1 = common.Must(gst.NewElementWithProperties("gocustomsrc", map[string]interface{}{ + "duration": int64(5 * time.Second), + })) + s.source2 = common.Must(gst.NewElementWithProperties("gocustomsrc", map[string]interface{}{ + "duration": int64(10 * time.Second), + })) + + s.mixer = common.Must(gst.NewElement("audiomixer")) + + klass := instance.Class() + class := gst.ToElementClass(klass) + + self.AddMany( + s.source1, + s.source2, + s.mixer, + ) + + srcpad := s.mixer.GetStaticPad("src") + + ghostpad := gst.NewGhostPadFromTemplate("src", srcpad, class.GetPadTemplate("src")) + + s.source1.Link(s.mixer) + s.source2.Link(s.mixer) + + self.AddPad(ghostpad.Pad) +} + +func (s *customBin) Constructed(o *glib.Object) {} + +func (s *customBin) Finalize(o *glib.Object) { + common.FinalizersCalled++ +} diff --git a/examples/plugins/registered_elements/internal/custombin/register.go b/examples/plugins/registered_elements/internal/custombin/register.go new file mode 100644 index 0000000..e181f75 --- /dev/null +++ b/examples/plugins/registered_elements/internal/custombin/register.go @@ -0,0 +1,22 @@ +package custombin + +import ( + "github.com/go-gst/go-gst/gst" +) + +// Register needs to be called after gst.Init() to make the gocustombin available in the standard +// gst element registry. After this call the element can be used like any other gstreamer element +func Register() bool { + return gst.RegisterElement( + // no plugin: + nil, + // The name of the element + "gocustombin", + // The rank of the element + gst.RankNone, + // The GoElement implementation for the element + &customBin{}, + // The base subclass this element extends + gst.ExtendsBin, + ) +} diff --git a/examples/plugins/registered_elements/internal/customsrc/element.go b/examples/plugins/registered_elements/internal/customsrc/element.go new file mode 100644 index 0000000..d7de038 --- /dev/null +++ b/examples/plugins/registered_elements/internal/customsrc/element.go @@ -0,0 +1,139 @@ +package customsrc + +import ( + "fmt" + "math" + "time" + + "github.com/go-gst/go-glib/glib" + "github.com/go-gst/go-gst/examples/plugins/registered_elements/internal/common" + "github.com/go-gst/go-gst/gst" +) + +// default: 1024, this value makes it easier to calculate num buffers with the sample rate +const samplesperbuffer = 4800 + +const samplerate = 48000 + +var properties = []*glib.ParamSpec{ + glib.NewInt64Param( + "duration", + "duration", + "duration the source", + 0, + math.MaxInt64, + 0, + glib.ParameterReadWrite, + ), +} + +type customSrc struct { + // self *gst.Bin + source *gst.Element + volume *gst.Element + + duration time.Duration +} + +// ClassInit is the place where you define pads and properties +func (*customSrc) ClassInit(klass *glib.ObjectClass) { + class := gst.ToElementClass(klass) + class.SetMetadata( + "custom test source", + "Src/Test", + "Demo source bin with volume", + "Wilhelm Bartel ", + ) + class.AddPadTemplate(gst.NewPadTemplate( + "src", + gst.PadDirectionSource, + gst.PadPresenceAlways, + gst.NewCapsFromString(fmt.Sprintf("audio/x-raw,channels=2,rate=%d", samplerate)), + )) + class.InstallProperties(properties) +} + +// SetProperty gets called for every property. The id is the index in the slice defined above. +func (s *customSrc) SetProperty(self *glib.Object, id uint, value *glib.Value) { + param := properties[id] + + bin := gst.ToGstBin(self) + + switch param.Name() { + case "duration": + state := bin.GetCurrentState() + if !(state == gst.StateNull || state != gst.StateReady) { + return + } + + gv, _ := value.GoValue() + + durI, _ := gv.(int64) + + s.duration = time.Duration(durI) + + s.updateSource() + } +} + +// GetProperty is called to retrieve the value of the property at index `id` in the properties +// slice provided at ClassInit. +func (o *customSrc) GetProperty(self *glib.Object, id uint) *glib.Value { + param := properties[id] + + switch param.Name() { + case "duration": + v, _ := glib.GValue(int64(o.duration)) + return v + } + + return nil +} + +func (*customSrc) New() glib.GoObjectSubclass { + return &customSrc{} +} + +// InstanceInit should initialize the element. Keep in mind that the properties are not yet present. When this is called. +func (s *customSrc) InstanceInit(instance *glib.Object) { + self := gst.ToGstBin(instance) + + s.source = common.Must(gst.NewElement("audiotestsrc")) + s.volume = common.Must(gst.NewElement("volume")) + + klass := instance.Class() + class := gst.ToElementClass(klass) + + self.AddMany( + s.source, + s.volume, + ) + + srcpad := s.volume.GetStaticPad("src") + + ghostpad := gst.NewGhostPadFromTemplate("src", srcpad, class.GetPadTemplate("src")) + + gst.ElementLinkMany( + s.source, + s.volume, + ) + + self.AddPad(ghostpad.Pad) + + s.updateSource() +} + +func (s *customSrc) Constructed(o *glib.Object) {} + +func (s *customSrc) Finalize(o *glib.Object) { + common.FinalizersCalled++ +} + +// updateSource will get called to update the audiotestsrc when a property changes +func (s *customSrc) updateSource() { + if s.source != nil { + numBuffers := (float64(s.duration / time.Second)) / (float64(samplesperbuffer) / float64(samplerate)) + + s.source.SetProperty("num-buffers", int(math.Ceil(numBuffers))) + } +} diff --git a/examples/plugins/registered_elements/internal/customsrc/register.go b/examples/plugins/registered_elements/internal/customsrc/register.go new file mode 100644 index 0000000..2a85ea2 --- /dev/null +++ b/examples/plugins/registered_elements/internal/customsrc/register.go @@ -0,0 +1,22 @@ +package customsrc + +import ( + "github.com/go-gst/go-gst/gst" +) + +// Register needs to be called after gst.Init() to make the gocustomsrc available in the standard +// gst element registry. After this call the element can be used like any other gstreamer element +func Register() bool { + return gst.RegisterElement( + // no plugin: + nil, + // The name of the element + "gocustomsrc", + // The rank of the element + gst.RankNone, + // The GoElement implementation for the element + &customSrc{}, + // The base subclass this element extends + gst.ExtendsBin, + ) +} diff --git a/examples/plugins/registered_elements/main.go b/examples/plugins/registered_elements/main.go new file mode 100644 index 0000000..13105a4 --- /dev/null +++ b/examples/plugins/registered_elements/main.go @@ -0,0 +1,120 @@ +package main + +import ( + "context" + "fmt" + "os" + "os/signal" + "path/filepath" + "runtime" + "runtime/pprof" + + "github.com/go-gst/go-glib/glib" + "github.com/go-gst/go-gst/examples/plugins/registered_elements/internal/common" + "github.com/go-gst/go-gst/examples/plugins/registered_elements/internal/custombin" + "github.com/go-gst/go-gst/examples/plugins/registered_elements/internal/customsrc" + "github.com/go-gst/go-gst/gst" +) + +func run(ctx context.Context) error { + ctx, cancel := signal.NotifyContext(ctx, os.Interrupt) + defer cancel() + + wd, err := os.Getwd() + + if err != nil { + return err + } + + gst.Init(nil) + + customsrc.Register() + custombin.Register() + + systemclock := gst.ObtainSystemClock() + + pipeline, err := gst.NewPipelineFromString("gocustombin ! fakesink sync=true") + + if err != nil { + return err + } + + pipeline.ForceClock(systemclock.Clock) + + bus := pipeline.GetBus() + + mainloop := glib.NewMainLoop(glib.MainContextDefault(), false) + + pipeline.SetState(gst.StatePlaying) + + bus.AddWatch(func(msg *gst.Message) bool { + switch msg.Type() { + case gst.MessageStateChanged: + old, new := msg.ParseStateChanged() + dot := pipeline.DebugBinToDotData(gst.DebugGraphShowVerbose) + + f, err := os.OpenFile(filepath.Join(wd, fmt.Sprintf("pipeline-%s-to-%s.dot", old, new)), os.O_TRUNC|os.O_CREATE|os.O_RDWR, 0600) + + if err != nil { + cancel() + return false + } + + defer f.Close() + + _, err = f.Write([]byte(dot)) + + if err != nil { + fmt.Println(err) + cancel() + return false + } + + case gst.MessageEOS: + fmt.Println(msg.String()) + cancel() + return false + } + + // the String method is expensive and should not be used in prodution: + fmt.Println(msg.String()) + return true + }) + + go mainloop.Run() + + go func() { + <-ctx.Done() + + mainloop.Quit() + }() + + <-ctx.Done() + + pipeline.BlockSetState(gst.StateNull) + + gst.Deinit() + + return ctx.Err() +} + +func main() { + ctx := context.Background() + + err := run(ctx) + + if err != nil { + fmt.Fprintln(os.Stderr, err) + } + + runtime.GC() + runtime.GC() + runtime.GC() + + prof := pprof.Lookup("go-glib-reffed-objects") + + prof.WriteTo(os.Stdout, 1) + + // we are creating 3 custom elements in total. If this panics, then the go struct will memory leak + common.AssertFinalizersCalled(3) +} diff --git a/go.mod b/go.mod index d35604c..778c332 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,11 @@ module github.com/go-gst/go-gst -go 1.23 +go 1.23.1 + +toolchain go1.23.2 require github.com/mattn/go-pointer v0.0.1 -require github.com/go-gst/go-glib v1.4.0 +require github.com/go-gst/go-glib v1.4.1-0.20241115142200-3da60b6536bd require golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect diff --git a/go.sum b/go.sum index 820e598..558e5d9 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ 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-glib v1.4.1-0.20241115142200-3da60b6536bd h1:9iZxYxazkdrKSGmKpiV+eEoaFeNXLGW3PPHcDfHp1n8= +github.com/go-gst/go-glib v1.4.1-0.20241115142200-3da60b6536bd/go.mod h1:GUIpWmkxQ1/eL+FYSjKpLDyTZx6Vgd9nNXt8dA31d5M= 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= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=