Skip to content

Instantly share code, notes, and snippets.

@pojntfx
Created April 22, 2026 06:38
Show Gist options
  • Select an option

  • Save pojntfx/82935bbe4321682f5e43fcc619539b47 to your computer and use it in GitHub Desktop.

Select an option

Save pojntfx/82935bbe4321682f5e43fcc619539b47 to your computer and use it in GitHub Desktop.
puregotk-based GStreamer example with unreleased fixes
package main
import (
"log/slog"
"runtime"
"unsafe"
"codeberg.org/puregotk/puregotk/examples/gstreamer-go/gst"
"codeberg.org/puregotk/puregotk/v4/adw"
"codeberg.org/puregotk/puregotk/v4/gdk"
"codeberg.org/puregotk/puregotk/v4/glib"
"codeberg.org/puregotk/puregotk/v4/gobject"
"codeberg.org/puregotk/puregotk/v4/gtk"
. "github.com/pojntfx/go-gettext/pkg/i18n"
)
var gTypeWindow gobject.Type
type Window struct {
adw.ApplicationWindow
pictureVideo *gtk.Picture
entryURI *gtk.Entry
labelState *gtk.Label
buttonPlay *gtk.Button
buttonPause *gtk.Button
buttonStop *gtk.Button
pipeline *gst.Element
}
func NewWindow(FirstPropertyNameVar string, varArgs ...interface{}) Window {
obj := gobject.NewObject(gTypeWindow, FirstPropertyNameVar, varArgs...)
var v Window
obj.Cast(&v)
return v
}
func (w *Window) play(uri string) {
slog.Info("play called", "uri", uri)
w.stop()
// Create the GTK4 paintable sink for video
videoSink := gst.ElementFactoryMake("gtk4paintablesink", "videosink")
if videoSink == nil {
slog.Error("failed to create gtk4paintablesink")
w.labelState.SetText(L("Error: gtk4paintablesink not available"))
return
}
var sinkObj gobject.Object
sinkObj.Ptr = videoSink.GoPointer()
// Get the paintable and set it on the Picture widget
var paintableVal gobject.Value
paintableVal.Init(gobject.ObjectGLibType())
sinkObj.GetProperty("paintable", &paintableVal)
paintableObj := paintableVal.GetObject()
slog.Info("paintable", "ptr", paintableObj.GoPointer())
var paintable gdk.PaintableBase
paintable.Ptr = paintableObj.GoPointer()
w.pictureVideo.SetPaintable(&paintable)
// Create playbin
pipeline := gst.ElementFactoryMake("playbin", "player")
if pipeline == nil {
slog.Error("failed to create playbin element")
w.labelState.SetText(L("Error: failed to create playbin"))
return
}
var pipelineObj gobject.Object
pipelineObj.Ptr = pipeline.GoPointer()
// Set the URI
var uriVal gobject.Value
uriVal.Init(64) // G_TYPE_STRING
uriVal.SetString(uri)
pipelineObj.SetProperty("uri", &uriVal)
// Set video-sink directly (no glsinkbin wrapper)
var videoSinkVal gobject.Value
videoSinkVal.Init(gobject.ObjectGLibType())
videoSinkVal.SetObject(&sinkObj)
pipelineObj.SetProperty("video-sink", &videoSinkVal)
w.pipeline = pipeline
bus := pipeline.GetBus()
if bus != nil {
var busWatch gst.BusFunc = func(_ uintptr, msg *gst.Message, _ uintptr) bool {
switch msg.Type {
case gst.MessageEosValue:
slog.Info("end of stream")
w.labelState.SetText(L("Finished"))
w.stop()
case gst.MessageErrorValue:
var gerr *glib.Error
var debug string
msg.ParseError(&gerr, &debug)
errMsg := "unknown error"
if gerr != nil {
errMsg = gerr.Error()
}
slog.Error("pipeline error", "err", errMsg, "debug", debug)
w.labelState.SetText(L("Error: ") + errMsg)
w.stop()
case gst.MessageWarningValue:
var gerr *glib.Error
var debug string
msg.ParseWarning(&gerr, &debug)
warnMsg := "unknown warning"
if gerr != nil {
warnMsg = gerr.Error()
}
slog.Warn("pipeline warning", "warn", warnMsg, "debug", debug)
case gst.MessageBufferingValue:
w.labelState.SetText(L("Buffering..."))
case gst.MessageStateChangedValue:
if w.pipeline != nil {
var oldState, newState gst.State
w.pipeline.GetState(&newState, &oldState, 0)
switch newState {
case gst.StatePlayingValue:
w.labelState.SetText(L("Playing"))
case gst.StatePausedValue:
w.labelState.SetText(L("Paused"))
}
}
}
return true
}
bus.AddWatch(&busWatch, 0)
}
ret := pipeline.SetState(gst.StatePlayingValue)
slog.Info("SetState result", "ret", ret)
w.buttonPlay.SetSensitive(false)
w.buttonPause.SetSensitive(true)
w.buttonStop.SetSensitive(true)
}
func (w *Window) pause() {
if w.pipeline == nil {
return
}
var state gst.State
w.pipeline.GetState(&state, nil, 0)
if state == gst.StatePlayingValue {
w.pipeline.SetState(gst.StatePausedValue)
w.buttonPlay.SetSensitive(true)
w.buttonPause.SetSensitive(false)
} else if state == gst.StatePausedValue {
w.pipeline.SetState(gst.StatePlayingValue)
w.buttonPlay.SetSensitive(false)
w.buttonPause.SetSensitive(true)
}
}
func (w *Window) stop() {
if w.pipeline != nil {
w.pipeline.SetState(gst.StateNullValue)
w.pipeline = nil
}
w.buttonPlay.SetSensitive(true)
w.buttonPause.SetSensitive(false)
w.buttonStop.SetSensitive(false)
w.labelState.SetText(L("Stopped"))
}
func init() {
var windowClassInit gobject.ClassInitFunc = func(tc *gobject.TypeClass, u uintptr) {
typeClass := (*gtk.WidgetClass)(unsafe.Pointer(tc))
typeClass.SetTemplateFromResource(ResourceWindowUIPath)
typeClass.BindTemplateChildFull("picture_video", false, 0)
typeClass.BindTemplateChildFull("entry_uri", false, 0)
typeClass.BindTemplateChildFull("label_state", false, 0)
typeClass.BindTemplateChildFull("button_play", false, 0)
typeClass.BindTemplateChildFull("button_pause", false, 0)
typeClass.BindTemplateChildFull("button_stop", false, 0)
objClass := (*gobject.ObjectClass)(unsafe.Pointer(tc))
objClass.OverrideConstructed(func(o *gobject.Object) {
parentObjClass := (*gobject.ObjectClass)(unsafe.Pointer(tc.PeekParent()))
parentObjClass.GetConstructed()(o)
var parent adw.ApplicationWindow
o.Cast(&parent)
parent.InitTemplate()
var pictureVideo gtk.Picture
parent.Widget.GetTemplateChild(gTypeWindow, "picture_video").Cast(&pictureVideo)
var entryURI gtk.Entry
parent.Widget.GetTemplateChild(gTypeWindow, "entry_uri").Cast(&entryURI)
var labelState gtk.Label
parent.Widget.GetTemplateChild(gTypeWindow, "label_state").Cast(&labelState)
var buttonPlay gtk.Button
parent.Widget.GetTemplateChild(gTypeWindow, "button_play").Cast(&buttonPlay)
var buttonPause gtk.Button
parent.Widget.GetTemplateChild(gTypeWindow, "button_pause").Cast(&buttonPause)
var buttonStop gtk.Button
parent.Widget.GetTemplateChild(gTypeWindow, "button_stop").Cast(&buttonStop)
w := &Window{
ApplicationWindow: parent,
pictureVideo: &pictureVideo,
entryURI: &entryURI,
labelState: &labelState,
buttonPlay: &buttonPlay,
buttonPause: &buttonPause,
buttonStop: &buttonStop,
}
var pinner runtime.Pinner
pinner.Pin(w)
var cleanupCallback glib.DestroyNotify = func(data uintptr) {
w.stop()
pinner.Unpin()
}
o.SetDataFull(dataKeyGoInstance, uintptr(unsafe.Pointer(w)), &cleanupCallback)
onPlayClicked := func(gtk.Button) {
uri := entryURI.GetText()
if uri != "" {
w.play(uri)
}
}
buttonPlay.ConnectClicked(&onPlayClicked)
onPauseClicked := func(gtk.Button) {
w.pause()
}
buttonPause.ConnectClicked(&onPauseClicked)
onStopClicked := func(gtk.Button) {
w.stop()
}
buttonStop.ConnectClicked(&onStopClicked)
onEntryActivate := func(gtk.Entry) {
uri := entryURI.GetText()
if uri != "" {
w.play(uri)
}
}
entryURI.ConnectActivate(&onEntryActivate)
})
}
var windowInstanceInit gobject.InstanceInitFunc = func(ti *gobject.TypeInstance, tc *gobject.TypeClass) {}
var windowParentQuery gobject.TypeQuery
gobject.NewTypeQuery(adw.ApplicationWindowGLibType(), &windowParentQuery)
gTypeWindow = gobject.TypeRegisterStaticSimple(
windowParentQuery.Type,
"MyAppGstreamerGomodWindow",
windowParentQuery.ClassSize,
&windowClassInit,
windowParentQuery.InstanceSize,
&windowInstanceInit,
0,
)
}
using Gtk 4.0;
using Adw 1;
template $MyAppGstreamerGomodWindow: Adw.ApplicationWindow {
title: _("GStreamer Example");
default-width: 640;
default-height: 520;
content: Adw.ToolbarView {
[top]
Adw.HeaderBar {
title-widget: Adw.WindowTitle {
title: _("GStreamer Example");
};
}
content: Box {
orientation: vertical;
spacing: 12;
margin-top: 12;
margin-bottom: 12;
margin-start: 12;
margin-end: 12;
Picture picture_video {
vexpand: true;
content-fit: contain;
}
Entry entry_uri {
placeholder-text: _("Enter a URI to play (e.g. https://...)");
}
Box {
spacing: 12;
halign: center;
Button button_play {
label: _("Play");
styles ["suggested-action"]
}
Button button_pause {
label: _("Pause");
sensitive: false;
}
Button button_stop {
label: _("Stop");
sensitive: false;
styles ["destructive-action"]
}
Label label_state {
label: _("Stopped");
styles ["dim-label"]
}
}
};
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment