Created
April 22, 2026 06:38
-
-
Save pojntfx/82935bbe4321682f5e43fcc619539b47 to your computer and use it in GitHub Desktop.
puregotk-based GStreamer example with unreleased fixes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | |
| ) | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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