Skip to content

feat: add a GStreamer runner to launch pipelines - #323

Open
ylatuya wants to merge 6 commits into
masterfrom
gst-runner
Open

ylatuya wants to merge 6 commits into
masterfrom
gst-runner

Conversation

@ylatuya

@ylatuya ylatuya commented Dec 10, 2025

Copy link
Copy Markdown
Contributor

Add a new GStreamer runner to launch pipelines that gives use more control in errors and allow us to differentiate for example between a pipelines miss-configuration or a format that's not supported.

This a first iteration in order to support #319

@dabrain34

dabrain34 commented Dec 10, 2025 •

Copy link
Copy Markdown
Contributor

I would put the gstreamer cffi in decoders as it should be not located in fluster base. IMHO fluster is GStreamer agnostic.

@ylatuya

ylatuya commented Dec 10, 2025

Copy link
Copy Markdown
Contributor Author

I would put the gstreamer cffi in decoders as it should not be located in the fluster base. IMHO, fluster is GStreamer agnostic.

The fluster.gstreamer namespace contains generic helpers around GStreamer that are not specific to decoders, that's why I decided to leave them under fluster.gstreamer instead of fluster.decoders.gstreamer. They can be used by a GStreamer decoder or a future GStreamer encoder.

Comment thread fluster/gstreamer/gst_cffi.py Outdated
@rsanchez87

rsanchez87 commented Mar 31, 2026 •

Copy link
Copy Markdown
Contributor

An important thing I'm seeing is that in l51 of pyproject.toml, it would be necessary to add fluster.gstreamer to the package list:

packages = ["fluster", "fluster.decoders", "fluster.gstreamer"]

cc: @ylatuya

Comment thread fluster/decoders/gstreamer.py Outdated
Comment thread fluster/decoders/gstreamer.py Outdated
@rsanchez87

rsanchez87 commented Mar 31, 2026 •

Copy link
Copy Markdown
Contributor

Something to keep in mind... now that #319 uses exit code 69 for "not supported" (matching the gst-launch upstream change), the runner here should return 69 also instead of INIT_ERROR = 2 for unsupported formats.

Otherwise the NotSupportedError path in utils.py will not be activated, since it only checks 69

@ylatuya
ylatuya force-pushed the gst-runner branch 3 times, most recently from ae60864 to afab38d Compare April 29, 2026 09:27
@ylatuya

ylatuya commented Apr 29, 2026

Copy link
Copy Markdown
Contributor Author

@dabrain34 I have rebased this MR on top of your work.

The runner is now returning a new NOT_SUPPORTED exit code when a missing-plugin or missing-codec error is emitted in the bus. The decoder implementation will raise a NotSupported exception in that scenario, allowing for test results to be reported correctly.

@mdimopoulos
mdimopoulos force-pushed the gst-runner branch 2 times, most recently from 3acb295 to 28e5e05 Compare May 14, 2026 08:39
Comment thread fluster/gstreamer/__init__.py
Comment thread fluster/gstreamer/runner.py
@ylatuya
ylatuya force-pushed the gst-runner branch 2 times, most recently from 664f1bb to 7ace11d Compare September 22, 2026 20:22
Comment thread fluster/gstreamer/runner.py
@rsanchez87
rsanchez87 self-requested a review September 23, 2026 06:24
@dabrain34

Copy link
Copy Markdown
Contributor

can you rebase the branch please ?

@dabrain34 dabrain34 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be nice to have a small unit test for the player as it brings potential new issues

Comment thread fluster/decoders/gstreamer.py Outdated
Comment thread fluster/decoders/gstreamer.py Outdated

pipeline = self.gen_pipeline(input_filepath, output_filepath, output_format, optional_params)
run_command(shlex.split(pipeline), timeout=timeout, verbose=verbose, env=env)
result = run_pipeline(pipeline, timeout=timeout, verbose=verbose, env=env)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

result is never used, you can remove it

Comment thread fluster/gstreamer/runner.py Outdated
return result
else:
# No message received, check for timeout
if timeout_ns != GST_CLOCK_TIME_NONE:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code should be run before polling the message as otherwise you'll never end if you still receive message after the timeout.

Comment thread fluster/gstreamer/__init__.py Outdated
cmd.append(pipeline)
process_env = os.environ.copy() if env is None else env.copy()
process_env.update(GStreamerInstallation().get_environment())
result = subprocess.run(cmd, env=process_env, capture_output=True, text=True, check=False)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For more security in terms of potential deadlocks during the player teardown/cleanup, its highly recommandable to use the timeout here as well

Comment thread fluster/gstreamer/__init__.py Outdated
cmd.extend(["--timeout", str(timeout)])
cmd.append(pipeline)
process_env = os.environ.copy() if env is None else env.copy()
process_env.update(GStreamerInstallation().get_environment())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you need to tell where fluster.gstreamer.runner as it might fail to run it from outside of the fluster main folder.

laptop:~ $ path_to_fluster/fluster.py run -s -d GStreamer-H.264-Libav -ts JVT-FR-EXT

will fail silently

Suggested change
process_env.update(GStreamerInstallation().get_environment())
process_env.update(GStreamerInstallation().get_environment())
pkg_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
process_env["PYTHONPATH"]= os.pathsep.join(filter(None, [pkg_root, process_env.get("PYTHONPATH")]))

Comment thread fluster/gstreamer/gst_ctypes.py Outdated
return self._plugin_path

@property
def bin_path(self) -> Optional[str]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useless

Comment thread fluster/gstreamer/gst_ctypes.py Outdated
self._gst.gst_bus_timed_pop_filtered.restype = ctypes.c_void_p

# gst_bus_poll
self._gst.gst_bus_poll.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_uint64]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont think this is necessary, please clean up

Comment thread fluster/gstreamer/gst_ctypes.py Outdated
msg = self._gst.gst_bus_timed_pop_filtered(bus, timeout, message_types)
return msg if msg else None

def bus_poll(self, bus: ctypes.c_void_p, message_types: int, timeout: int) -> Optional[ctypes.c_void_p]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this method is unnecessary

Comment thread fluster/gstreamer/gst_ctypes.py Outdated
raise GStreamerError("GStreamer not initialized")
return int(self._gst.gst_element_set_state(element, state))

def element_get_state(self, element: ctypes.c_void_p, timeout: int = GST_CLOCK_TIME_NONE) -> Tuple[int, int, int]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is useless, please remove

raise GStreamerError("GStreamer initialization failed")
self._initialized = True

def deinit(self) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should it be called of the process ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: gst_deinit() now runs at exit — runner.py:408 → runner.py:151 → gst_ctypes.py:491.

@dabrain34

Copy link
Copy Markdown
Contributor

I would not merge as the main feature is not working to track if the media is not supported by GStreamer. Investigating

Add a new GStreamer runner to launch pipelines that gives use more
control in errors and allow us to differentiate for example between a
pipelines miss-configuration or a format that's not supported.
@rsanchez87
rsanchez87 requested review from rsanchez87 and removed request for rsanchez87 September 30, 2026 14:09

@rsanchez87 rsanchez87 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review last changes, checking functionality

@rsanchez87
rsanchez87 self-requested a review September 30, 2026 14:11
@rsanchez87

Copy link
Copy Markdown
Contributor

I would not merge as the main feature is not working to track if the media is not supported by GStreamer. Investigating

Hi @dabrain34, I think all the comments have been resolved at this point, and it's ready for review. Once you approve it, I'll clean up the commits and squash them properly for the Git history, thanks

@dabrain34

dabrain34 commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

unfortunately I still experience the failure instead of not supported. The reason is that this is not negociated. you can give a try to:

./fluster.py run -s -d GStreamer-VP9-Vulkan -tv vp90-2-02-size-08x10.webm --verbose

I got this error with a nvidia hardware which should support VP9 but not this resolution, so the element should return an error (I dont recall from the top of my head) and the player should return a not supported .

you can check what I did in https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/10088/diffs?file_path=subprojects%2Fgstreamer%2Ftools%2Fgst-launch.c#line_2370bc12d_A408 I think that was the one reported by the pipeline we should track

by the way on a non-supporting vulkan setup, I get the following error when running this line:

sink=@(nil)]
0:00:00.013592064    1508253    1508253 ERROR           GST_PIPELINE subprojects/gstreamer/gst/parse/grammar.y:1385:priv_gst_parse_yyparse: link has no source [sink=@0x3fa31c80]

(python3:1508253): GStreamer-WARNING **: 17:42:10.382: No error messages for domain gst_parse_error
ERROR: Failed to parse pipeline: appsrc num-buffers=0 !  vulkanvp9dec ! vulkandownload  ! fakesink: no element "vulkanvp9dec" [No error message for domain gst_parse_error.] (domain=2147, code=1)

** (python3:1508253): WARNING **: 17:42:10.383: Leaks detected and logged under GST_DEBUG=GST_TRACER:7
Skipping decoder GStreamer-VP9-Vulkan because it cannot be run
Skipping decoder GStreamer-VP9-Vulkan because it cannot be run

you can enable the leak tracer with GST_TRACERS=leaks

Read the bus message type as unsigned and compare it exactly so extended message types are not mistaken for EOS/ERROR/WARNING, and report media GStreamer cannot handle as not supported (EX_UNAVAILABLE, 69), including the missing-plugin element message.

Also set PYTHONPATH so the runner can be imported from any directory, add a subprocess timeout and verbose output, decode GError messages, let get_environment() take a base environment, and remove the unused bindings, gst-launch --no-fault leftovers and dead code.
Cover bus message classification, unsupported media detection, the message loop timeout and the exit code mapping of run_pipeline without requiring GStreamer to be installed.
Reading GstMessage.type with a hardcoded offset was wrong on 32-bit systems: GstMiniObject is nine 4-byte fields with no padding there, so it is 36 bytes and the type field lives at offset 36, not 32.

Describe the GstMiniObject leading fields and the GstMessage head as ctypes Structures so ctypes computes the offset and padding for the current architecture, and keep reading the type as unsigned so extended message types are not sign-extended.
Assert the GstMiniObject size (64 bytes on 64-bit, 36 on 32-bit) and that the message type offset matches, and exercise the Windows library lookup with a faked platform so both the MSVC (glib-2.0-0.dll) and MinGW (libglib-2.0-0.dll) spellings are checked.
gst_parse_launch() can return a partially built pipeline together with
an error, e.g. when an element does not exist. parse_launch() raised
without unreffing it, so it leaked. Unref it before raising, and add
tests for the error, error-without-pipeline and success cases.
@rsanchez87

rsanchez87 commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

unfortunately I still experience the failure instead of not supported. The reason is that this is not negociated. you can give a try to:

./fluster.py run -s -d GStreamer-VP9-Vulkan -tv vp90-2-02-size-08x10.webm --verbose

I got this error with a nvidia hardware which should support VP9 but not this resolution, so the element should return an error (I dont recall from the top of my head) and the player should return a not supported .

you can check what I did in https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/10088/diffs?file_path=subprojects%2Fgstreamer%2Ftools%2Fgst-launch.c#line_2370bc12d_A408 I think that was the one reported by the pipeline we should track

by the way on a non-supporting vulkan setup, I get the following error when running this line:

sink=@(nil)]
0:00:00.013592064    1508253    1508253 ERROR           GST_PIPELINE subprojects/gstreamer/gst/parse/grammar.y:1385:priv_gst_parse_yyparse: link has no source [sink=@0x3fa31c80]

(python3:1508253): GStreamer-WARNING **: 17:42:10.382: No error messages for domain gst_parse_error
ERROR: Failed to parse pipeline: appsrc num-buffers=0 !  vulkanvp9dec ! vulkandownload  ! fakesink: no element "vulkanvp9dec" [No error message for domain gst_parse_error.] (domain=2147, code=1)

** (python3:1508253): WARNING **: 17:42:10.383: Leaks detected and logged under GST_DEBUG=GST_TRACER:7
Skipping decoder GStreamer-VP9-Vulkan because it cannot be run
Skipping decoder GStreamer-VP9-Vulkan because it cannot be run

you can enable the leak tracer with GST_TRACERS=leaks

Thanks for the detailed repro steps @dabrain34 ,

I tried it on a machine with GStreamer 1.24.2, where the Vulkan plugin only ships vulkanh264dec and vulkanh265dec. There is no vulkanvp9dec, so the run ends at the "no element" parse failure and the decoder is skipped. That means I could not reproduce the vp90-2-02-size-08x10.webm case you describe.

While doing this I did find and fix a leak on that error path, using GST_TRACERS=leaks. gst_parse_launch() can return a partially built pipeline together with the error, and GstCtypes.parse_launch() raised without unreffing it. With the fix, no leaks are reported on that run. The fix is in ccd89bb, with unit tests.

For the not negotiated case: right now the runner only maps GST_STREAM_ERROR_CODEC_NOT_FOUND, GST_STREAM_ERROR_NOT_IMPLEMENTED and GST_CORE_ERROR_MISSING_PLUGIN to the not supported exit code, so a negotiation failure ends up as a plain error. To fix it properly I need to know which error the Vulkan decoder actually posts for that resolution. As far as I can tell, the current gst-launch.c on main does not map errors to specific exit codes, so I assume the logic you mean is in the MR you linked. Could you tell me which error domain and code (or message) you tracked there?

If it helps, I can also print the error domain and code in the runner's --verbose output, so the 08x10 case can be classified from your NVIDIA setup and we can add exactly that error to the mapping, with a test

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants