Porting CLI to Go

I’m away for the next 10 days, so can’t try anything. Did you try running the updated TtyRTMP ?

Hi @RSWilli Using your launch example with command line from rtmpsink (but with a working location url)

./launch videotestsrc ! ffenc_flv ! flvmux ! rtmpsink location='rtmp://localhost/path/to/stream live=1'

output:

0:00:00.016689152 13351     0x3d628480 ERROR           GST_PIPELINE gst/parse/grammar.y:665:gst_parse_element_make: no element "ffenc_flv"
0:00:00.016716845 13351     0x3d628480 ERROR           GST_PIPELINE gst/parse/grammar.y:1385:priv_gst_parse_yyparse: link has no sink [source=@0x3d628010]
0:00:00.017397698 13351     0x3d628480 ERROR           GST_PIPELINE gst/parse/grammar.y:1385:priv_gst_parse_yyparse: link has no source [sink=@0x3d453790]
0:00:00.019120195 13351     0x3d628480 ERROR           GST_PIPELINE gst/parse/grammar.y:718:gst_parse_element_make: no property "live" in element "rtmpsink"
0:00:00.019128607 13351     0x3d628480 ERROR           GST_PIPELINE gst/parse/grammar.y:1385:priv_gst_parse_yyparse: link has no sink [source=@0x3d453790]

Clearly, there is something wrong. At the stage of development you are at, should this be working?

I think the launch example is currently wrong (aka working only for simple cases) because it doesn’t handle whitespaces in string properties correctly (simply joining os args with whitespace does change the semantics).

Also this tells you that the element you need is not installed. A similar sounding element is part of gst-plugins-libav and called avenc_flv

0:00:00.016689152 13351     0x3d628480 ERROR           GST_PIPELINE gst/parse/grammar.y:665:gst_parse_element_make: no element "ffenc_flv"

no element “avcenc_flv”

0:00:00.014413276 14536      0x82f1480 ERROR           GST_PIPELINE gst/parse/grammar.y:665:gst_parse_element_make: no element "avcenc_flv"
0:00:00.014442912 14536      0x82f1480 ERROR           GST_PIPELINE gst/parse/grammar.y:1385:priv_gst_parse_yyparse: link has no sink [source=@0x82f1010]
0:00:00.015164299 14536      0x82f1480 ERROR           GST_PIPELINE gst/parse/grammar.y:1385:priv_gst_parse_yyparse: link has no source [sink=@0x811c790]
0:00:00.016998710 14536      0x82f1480 ERROR           GST_PIPELINE gst/parse/grammar.y:718:gst_parse_element_make: no property "live" in element "rtmpsink"
0:00:00.017008132 14536      0x82f1480 ERROR           GST_PIPELINE gst/parse/grammar.y:1385:priv_gst_parse_yyparse: link has no sink [source=@0x811c790]

Can you estimate when this new version of go_gst will be released?

I’m on it currently, but I’ll have to do more testing and also try to get the upstream gotk4 guys to merge my changes. Or we’ll have to maintain our own stuff.

Either way your problems are not caused by faulty bindings. gst-plugins-libav is a separate package you’ll need to install.

@RSWilli - gst.LinkMany only accepts elements, so how to add an element by name? This is necessary to direct the flow back to an earlier element. For examaple:

flvmux := gst.ElementFactoryMake("flvmux", "mux")
...
pipeline(..., flvmux, ...)
...
gst.LinkMany(..., flvmux, ..., mux) // error

I don’t see a way to make use of named elements.

@ea7kir : The variable is your name. You only need to add the element once, and link it multiple times.

gst.Init()
pipeline := gst.NewPipeline("").(gst.Pipeline)
videotestsrc := gst.ElementFactoryMake("videotestsrc", "")
videoconvert := gst.ElementFactoryMake("videoconvert", "")
video_x_raw := gst.ElementFactoryMake("video/x-raw", "")
queue := gst.ElementFactoryMake("queue", "")
x264enc := gst.ElementFactoryMake("x264enc", "")
video_x_h264 := gst.ElementFactoryMake("video/x-h264", "")
flvmux := gst.ElementFactoryMake("flvmux", "mux") // optional name "mux"
rtmpsink := gst.ElementFactoryMake("rtmpsink", "")
audiotestsrc := gst.ElementFactoryMake("audiotestsrc", "")
voaacenc := gst.ElementFactoryMake("voaacenc", "")

pipeline.AddMany(videotestsrc, videoconvert, video_x_raw, queue, x264enc,
	video_x_h264, flvmux, rtmpsink, audiotestsrc, voaacenc)

// link the first chain of elements
gst.LinkMany(videotestsrc, videoconvert, video_x_raw, queue, x264enc,
		video_x_h264, flvmux, rtmpsink)

// link the second chain of elements and link to the same flvmux
gst.LinkMany(audiotestsrc, voaacenc, flvmux)

There is barely any magic here. The variable contains the element and you can reference it like with any other object/data in any other programming language. The gst-launch-1.0 command needs to add some more syntax for this, but in an application this is just basic variable passing.

Naming the elements is not necessary, but sometimes useful to either retrieve the element by name or to better distinguish it in the pipeline graph.

This is contrary to your previous advice.

0:00:00.010120109 217389      0xb1cf070 WARN     GST_ELEMENT_FACTORY gstelementfactory.c:712:gst_element_factory_make_with_properties: no such element factory "video/x-raw"!
0:00:00.010869668 217389      0xb1cf070 WARN     GST_ELEMENT_FACTORY gstelementfactory.c:712:gst_element_factory_make_with_properties: no such element factory "video/x-h264"!

Of course, it would help if GStreamer had better documentation.

Repo here GitHub - ea7kir/TryRTMP-v2

I have another Issue with go.mod files in 2 projects…
Project 1:

replace github.com/diamondburned/gotk4 => github.com/rswilli/gotk4 v0.0.0-20250623083639-9b99a5788c39

require github.com/go-gst/go-gst v1.4.1-0.20250623085139-c062eb82a7e0

require (
	github.com/diamondburned/gotk4 v0.3.1 // indirect
	golang.org/x/sync v0.8.0 // indirect
)

Project 2:

replace github.com/diamondburned/gotk4 => github.com/rswilli/gotk4 v0.0.0-20250623083639-9b99a5788c39

require (
	github.com/go-gst/go-gst v1.4.1-0.20250623085139-c062eb82a7e0
	github.com/pelletier/go-toml/v2 v2.2.4
)

require (
	github.com/go-gst/go-glib v1.4.0 // indirect
	github.com/go-gst/go-pointer v0.0.0-20241127163939-ba766f075b4c // indirect
	github.com/mattn/go-pointer v0.0.1 // indirect
	golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
)

Both very basic, except one includes yaml. What is going on here?

This is contrary to your previous advice.

I’m sorry I copied your code. This is wrong of course the LinkMany calls should give you a hint.

I think you copied it from an old version, not the latest v2 repro which is your code. So your answer is unclear. The link was include in my post yesterday.

I’ve been trying to convert this basic command line to go-gst for over 25 days without any success. I begin to wonder if it’s even possible.

@RSWilli I’ve implemented everything you’ve helped me with. It compiles and runs without error, but it doesn’t work. However, it does print a warning message, which means nothing to me.

0:00:00.018874325 13071 0x55aa992e9170 WARN               vadisplay gstvadisplay_drm.c:143:gst_va_display_drm_create_va_display:<vadisplaydrm0> Failed to open /dev/dri/renderD128: Permission denied

I’ve uploaded the complete code to GitHub HERE and ask if you will run it for yourself. I can’t share the destination URL here, but I can PM it to you, along with the where it can be viewed, so please let me know if you are willing.

Good job! I can’t help you with plugin specific problems though, since I don’t use RTMP with gstreamer.

The warning you are getting could point to a driver issue in your system though. It could also mean a permission problem (aka a missing group of a user). Does the same log appear when you run the gst-launch-1.0 command on the same machine?

#!/bin/bash
RTMP_DEST=rtmp://url.available.by.pm
gst-launch-1.0 \
    videotestsrc is-live=1 \
    ! videoconvert \
    ! "video/x-raw, width=1920, height=1080, framerate=25/1" \
    ! queue \
    ! x264enc cabac=1 bframes=2 ref=1 \
    ! "video/x-h264,profile=main" \
    ! flvmux streamable=true name=mux \
    ! rtmpsink location="${RTMP_DEST} live=1" \
    audiotestsrc is-live=1 wave=ticks \
    ! voaacenc bitrate=128000 \
    ! mux.

On the same machine, it connects and displays perfectly on the destination, but with the following warnings. It does NOT print Failed to open /dev/dri/renderD128: Permission denied as with go-gst.

Setting pipeline to PAUSED ...
Pipeline is live and does not need PREROLL ...
0:00:00.017988851 31686 0x7f36e4000b70 WARN              aggregator gstaggregator.c:2312:gst_aggregator_query_latency_unlocked:<mux> Latency query failed
0:00:00.018027774 31686 0x7f36e4000b70 WARN              aggregator gstaggregator.c:2312:gst_aggregator_query_latency_unlocked:<mux> Latency query failed
Pipeline is PREROLLED ...
Setting pipeline to PLAYING ...
New clock: GstSystemClock
0:00:00.018175338 31686 0x7f36e4000b70 WARN              aggregator gstaggregator.c:2312:gst_aggregator_query_latency_unlocked:<mux> Latency query failed
0:00:00.018338743 31686 0x7f36e4000b70 WARN              aggregator gstaggregator.c:2312:gst_aggregator_query_latency_unlocked:<mux> Latency query failed
0:00:00.018487949 31686 0x7f36e4000b70 WARN              aggregator gstaggregator.c:2312:gst_aggregator_query_latency_unlocked:<mux> Latency query failed
Redistribute latency...
Redistribute latency...
0:00:03.139943405 31686 0x7f36e4000b70 WARN                  flvmux gstflvmux.c:1294:gst_flv_mux_buffer_to_tag_internal:<mux:audio> Got backwards dts! (0:00:00.023000000 < 0:00:00.101000000)
0:00:03.140023808 31686 0x7f36e4000b70 WARN                  flvmux gstflvmux.c:1294:gst_flv_mux_buffer_to_tag_internal:<mux:audio> Got backwards dts! (0:00:00.046000000 < 0:00:00.141000000)
... repeating until I terminate the script.

As far as I can remember, the last time I ran this script was with an earlier version of Gstreamer and there were no warnings. I’m nor sure about that, but now I’m on version 1.26.1.

  1. Are there any clues here?
  2. How can I get you to clone the repro and run it?

I’m happy to share the send & receive URLs with you.

@RSWilli - I’m trying again to get this working. Now on Debian 13 RC2 and using latest generated bindings. CLI version now runs without warnings:

Setting pipeline to PAUSED ...
Pipeline is live and does not need PREROLL ...
Pipeline is PREROLLED ...
Setting pipeline to PLAYING ...
New clock: GstSystemClock
Redistribute latency...
Redistribute latency...
0:00:37.7 / 99:99:99.

The Go version also runs without error, but doesn’t produce any output. here’s the code. This is really holding me back, so please can you take a look.

func createPipeline() (gst.Pipeline, error) {
	fmt.Println("creating pipeline")

	gst.Init()
	pipeline := gst.NewPipeline("").(gst.Pipeline)

	videotestsrc := gst.ElementFactoryMake("videotestsrc", "")
	videotestsrc.SetObjectProperty("is-live", true)

	videoconvert := gst.ElementFactoryMake("videoconvert", "")

	capsfilter1 := gst.ElementFactoryMake("capsfilter", "")
	capsfilter1.SetObjectProperty("caps", gst.CapsFromString("video/x-raw, width=1920, height=1080, framerate=25/1"))

	queue := gst.ElementFactoryMake("queue", "")

	x264enc := gst.ElementFactoryMake("x264enc", "")
	x264enc.SetObjectProperty("cabac", true)
	x264enc.SetObjectProperty("bframes", 2)
	x264enc.SetObjectProperty("ref", 1)

	capsfilter2 := gst.ElementFactoryMake("capsfilter", "")
	capsfilter2.SetObjectProperty("caps", gst.CapsFromString("video/x-h264, profile=main"))

	flvmux := gst.ElementFactoryMake("flvmux", "")
	flvmux.SetObjectProperty("streamable", true)

	rtmpsink := gst.ElementFactoryMake("rtmpsink", "")
	rtmpsink.SetObjectProperty("location", RTMP_DEST+" live=1")

	audiotestsrc := gst.ElementFactoryMake("audiotestsrc", "")
	audiotestsrc.SetObjectProperty("is-live", true)
	gst.UtilSetObjectArg(audiotestsrc, "wave", "tick") // alternative method

	voaacenc := gst.ElementFactoryMake("voaacenc", "")
	voaacenc.SetObjectProperty("bitrate", 128000)

	fmt.Println("link the first chain of elements")
	gst.LinkMany(videotestsrc, videoconvert, capsfilter1, queue, x264enc,
		capsfilter2, flvmux, rtmpsink)

	fmt.Println("link the second chain of elements and link to the same flvmux")
	gst.LinkMany(audiotestsrc, voaacenc, flvmux)

	return pipeline, nil
}

func mainLoop(pipeline gst.Pipeline) error {
	pipeline.SetState(gst.StatePlaying)
	fmt.Println("pipeline should now be playing")
	for msg := range pipeline.GetBus().Messages(context.Background()) {
		switch msg.Type() {
		case gst.MessageEOS:
			return nil
		case gst.MessageError:
			debug, gerr := msg.ParseError()
			if debug != "" {
				fmt.Println(gerr.Error(), debug)
			}
			return gerr
		default:
			fmt.Println(msg)
		}
		// pipeline.DebugBinToDotFileWithTs(gst.DebugGraphShowVerbose, "pipeline")
	}
	return fmt.Errorf("unexpected end of messages without EOS")
}

func StartGtreamer() error {
	pipeline, err := createPipeline()
	if err != nil {
		return fmt.Errorf("error creating pipeline:%s", err)
	}
	fmt.Println("mainLoop starting")
	err = mainLoop(pipeline)
	if err != nil {
		return fmt.Errorf("error running pipeline:%s", err)
	}
	fmt.Println("ending ok")
	return nil
}

func main() {
	log.SetFlags(log.LstdFlags | log.Lshortfile)
	fmt.Println("connecting to", RTMP_DEST)
	if err := StartGtreamer(); err != nil {
		log.Fatalln(err)
	}
}

Sorry I don’t have time to look at your repo. Can you post more logs here please? The messages posted on the bus and the pipeline graph would help a lot. (you can censor sensible urls)

GO version - I’ve only once seen this WARN message
No output ever appears after the the last log message

2025/07/30 14:23:43 trythis.go:43: creating pipeline
0:00:00.024349686 18345 0x563fad48f5d0 WARN               vadisplay gstvadisplay_drm.c:143:gst_va_display_drm_create_va_display:<vadisplaydrm0> Failed to open /dev/dri/renderD128: Permission denied
2025/07/30 14:23:43 trythis.go:84: link the first chain of elements
2025/07/30 14:23:43 trythis.go:90: link the second chain of elements and link to the same flvmux
2025/07/30 14:23:43 trythis.go:124: mainLoop starting
2025/07/30 14:23:43 trythis.go:100: pipeline should now be playing
^Csignal: interrupt

CLI version:

Setting pipeline to PAUSED ...
0:00:00.019446093 28050 0x7f4428000b70 WARN              aggregator gstaggregator.c:2334:gst_aggregator_query_latency_unlocked:<mux> Latency query failed
0:00:00.019956578 28050 0x7f4428000b70 WARN              aggregator gstaggregator.c:2334:gst_aggregator_query_latency_unlocked:<mux> Latency query failed
Pipeline is live and does not need PREROLL ...
Pipeline is PREROLLED ...
Setting pipeline to PLAYING ...
New clock: GstSystemClock
0:00:00.020474599 28050 0x7f4428000b70 WARN              aggregator gstaggregator.c:2334:gst_aggregator_query_latency_unlocked:<mux> Latency query failed
Redistribute latency...
Redistribute latency...
0:00:03.324501541 28050 0x7f4428000b70 WARN                  flvmux gstflvmux.c:1294:gst_flv_mux_buffer_to_tag_internal:<mux:video> Got backwards dts! (0:00:00.578000000 < 0:00:00.603000000)
0:00:03.370895196 28050 0x7f4428000b70 WARN                  flvmux gstflvmux.c:1294:gst_flv_mux_buffer_to_tag_internal:<mux:video> Got backwards dts! (0:00:00.618000000 < 0:00:00.650000000)
0:00:03.417409542 28050 0x7f4428000b70 WARN                  flvmux gstflvmux.c:1294:gst_flv_mux_buffer_to_tag_internal:<mux:video> Got backwards dts! (0:00:00.658000000 < 0:00:00.696000000)
0:00:03.441082891 28050 0x7f4428000b70 WARN                  flvmux gstflvmux.c:1294:gst_flv_mux_buffer_to_tag_internal:<mux:video> Got backwards dts! (0:00:00.698000000 < 0:00:00.719000000)
0:00:03.486982549 28050 0x7f4428000b70 WARN                  flvmux gstflvmux.c:1294:gst_flv_mux_buffer_to_tag_internal:<mux:video> Got backwards dts! (0:00:00.738000000 < 0:00:00.766000000)
0:00:03.533370867 28050 0x7f4428000b70 WARN                  flvmux gstflvmux.c:1294:gst_flv_mux_buffer_to_tag_internal:<mux:video> Got backwards dts! (0:00:00.778000000 < 0:00:00.812000000)
0:00:03.579890816 28050 0x7f4428000b70 WARN                  flvmux gstflvmux.c:1294:gst_flv_mux_buffer_to_tag_internal:<mux:video> Got backwards dts! (0:00:00.818000000 < 0:00:00.859000000)
0:00:03.603125469 28050 0x7f4428000b70 WARN                  flvmux gstflvmux.c:1294:gst_flv_mux_buffer_to_tag_internal:<mux:video> Got backwards dts! (0:00:00.858000000 < 0:00:00.882000000)

As for .dot files. Go version doesn’t produce any. CLI does, but I can’t display them. Debugging for me has never gone beyond log statements.

@RSWilli - I’m thinking the pipeline isn’t running, or is empty. How does gst.LinkMany() know about the pipeline? After all, there could be more than one.

log.Printf("%v", pipeline)
&{[] {[] {[] {[] {[] {0xc000112068}}}} {[] {0xc000112068}}}}

Looks empty to me. There’s a pipeline.Link(), amongst others, so is it something like this that’s missing?

Looks empty to me

Yeah the gst.Pipeline is a struct containing only a single pointer. This pointer though points into C memory that has all the references needed.

How does gst.LinkMany() know about the pipeline? After all, there could be more than one.

Yes, but that does not mean that the function call needs this directly. gst.Element.Link also does not need the pipeline, because the element already has a reference to the pipeline

No output ever appears after the the last log message

That’s odd I’d at least expected it to output some messages in the for loop. Can you please share the result of the following call

	pipeline.SetState(gst.StatePlaying)
	fmt.Println("pipeline should now be playing")
	pipeline.DebugBinToDotFileWithTs(gst.DebugGraphShowVerbose, "pipeline")

BEFORE you block in the loop. Make sure to censor all the things you need. Please don’t share the dot, but a rendered image instead.