From 093a874641fd5c73b19373cf7cd395e354360130 Mon Sep 17 00:00:00 2001 From: Andreas Kluge Svendsrud Date: Sat, 15 Feb 2025 21:16:20 +0100 Subject: [PATCH 01/10] feat(python_utils): Add H264Decoder class for handling H.264 video streams --- vortex_utils/python_utils.py | 106 +++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/vortex_utils/python_utils.py b/vortex_utils/python_utils.py index 7dbcbeb..2d709a9 100644 --- a/vortex_utils/python_utils.py +++ b/vortex_utils/python_utils.py @@ -1,8 +1,13 @@ from dataclasses import dataclass +import gi import numpy as np from scipy.spatial.transform import Rotation +gi.require_version('Gst', '1.0') +gi.require_version('GstApp', '1.0') +from gi.repository import GLib, Gst + def ssa(angle: float) -> float: return (angle + np.pi) % (2 * np.pi) - np.pi @@ -120,3 +125,104 @@ def __add__(self, other: "State") -> "State": def __sub__(self, other: "State") -> "State": return State(pose=self.pose - other.pose, twist=self.twist - other.twist) + + +class H264Decoder: + """Decodes H.264 streams using GStreamer.""" + + _gst_initialized = False + + def __init__(self): + """Initializes the H.264 decoder and sets up the GStreamer pipeline.""" + # Ensure GStreamer is initialized only once + if not H264Decoder._gst_initialized: + Gst.init(None) + H264Decoder._gst_initialized = True + + pipeline_desc = ( + "appsrc name=mysrc is-live=true ! " # Receive raw H.264 stream data + "h264parse ! " # Parse the H.264 stream + "avdec_h264 ! " # Decode H.264 frames + "videoconvert ! video/x-raw,format=BGR ! " # Convert frames to BGR format + "appsink name=appsink" # Sink for retrieving processed frames + ) + + self._pipeline = Gst.parse_launch(pipeline_desc) + self.appsrc = self._pipeline.get_by_name("mysrc") + self._appsink = self._pipeline.get_by_name("appsink") + + self._appsink.set_property("emit-signals", True) + self._appsink.set_property("sync", False) + self._appsink.connect("new-sample", self._on_new_sample) + + self._bus = self._pipeline.get_bus() + self._bus.add_signal_watch() + self._bus.connect("message", self._on_bus_message) + + self._main_loop = None + + self.decoded_frames = [] + + def start(self): + """Starts the GStreamer pipeline and runs the main event loop.""" + self._pipeline.set_state(Gst.State.PLAYING) + self._main_loop = GLib.MainLoop() + try: + self._main_loop.run() + except KeyboardInterrupt: + pass + finally: + self.stop() + + def stop(self): + """Stops the GStreamer pipeline and cleans up resources.""" + if self._pipeline: + self._pipeline.set_state(Gst.State.NULL) + if self._main_loop is not None: + self._main_loop.quit() + self._main_loop = None + + def push_data(self, data: bytes): + """Pushes H.264 encoded data into the pipeline for decoding.""" + if not self.appsrc: + raise RuntimeError( + "The pipeline's appsrc element was not found or not initialized." + ) + gst_buffer = Gst.Buffer.new_allocate(None, len(data), None) + gst_buffer.fill(0, data) + self.appsrc.emit("push-buffer", gst_buffer) + + def _on_bus_message(self, bus, message): + """Handles messages from the GStreamer bus.""" + msg_type = message.type + if msg_type == Gst.MessageType.ERROR: + err, debug = message.parse_error() + print(f"GStreamer ERROR: {err}, debug={debug}") + self.stop() + elif msg_type == Gst.MessageType.EOS: + print("End-Of-Stream reached.") + self.stop() + + def _on_new_sample(self, sink): + """Processes a new decoded video frame from the appsink.""" + sample = sink.emit("pull-sample") + if not sample: + return Gst.FlowReturn.ERROR + + buf = sample.get_buffer() + caps_format = sample.get_caps().get_structure(0) + width = caps_format.get_value("width") + height = caps_format.get_value("height") + + success, map_info = buf.map(Gst.MapFlags.READ) + if not success: + return Gst.FlowReturn.ERROR + + frame_data = np.frombuffer(map_info.data, dtype=np.uint8) + channels = len(frame_data) // (width * height) # typically 3 (BGR) or 4 (BGRA) + frame_data_reshaped = frame_data.reshape((height, width, channels)) + + self.decoded_frames.append(frame_data_reshaped.copy()) + + buf.unmap(map_info) + return Gst.FlowReturn.OK From 6678084513bf2c41d5afe0c5d5d9fe436c8abf1a Mon Sep 17 00:00:00 2001 From: Andreas Kluge Svendsrud Date: Sat, 15 Feb 2025 21:17:06 +0100 Subject: [PATCH 02/10] test(h264-decoder): add unit test for H264Decoder --- tests/resources/test_video.h264 | Bin 0 -> 32920 bytes tests/test_utils.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 tests/resources/test_video.h264 diff --git a/tests/resources/test_video.h264 b/tests/resources/test_video.h264 new file mode 100644 index 0000000000000000000000000000000000000000..a56ce6a11fb362d5ec60830b4db4d1dff200eff4 GIT binary patch literal 32920 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

hlOeH48=5c>P2izf_iML&X{ZpP}) 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

BWd#?9 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 RHEH*)y7?Gpn-4Y;{~thsX~X~k literal 0 HcmV?d00001 diff --git a/tests/test_utils.py b/tests/test_utils.py index a1fe409..2699e4d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,7 +1,10 @@ +import threading + import numpy as np import pytest from vortex_utils.python_utils import ( + H264Decoder, PoseData, State, TwistData, @@ -102,3 +105,32 @@ def test_state(): assert (state1 - state2).pose == PoseData(0.9, 1.8, 2.7, 0, 0, 0) assert (state1 + state2).twist == TwistData(1.1, 2.2, 3.3, 0.2, 0.4, 0.6) assert (state1 - state2).twist == TwistData(0.9, 1.8, 2.7, 0, 0, 0) + + +def test_h264_decoder(): + test_file = "tests/resources/test_video.h264" + + decoder = H264Decoder() + + decoding_thread = threading.Thread(target=decoder.start, daemon=True) + decoding_thread.start() + + with open(test_file, "rb") as f: + raw_data = f.read() + + chunk_size = 64 + for i in range(0, len(raw_data), chunk_size): + chunk = raw_data[i : i + chunk_size] + decoder.push_data(chunk) + + decoder.appsrc.emit("end-of-stream") + + decoding_thread.join(timeout=5.0) + + assert ( + len(decoder.decoded_frames) > 0 + ), "No frames were decoded from the H.264 stream." + + frame = decoder.decoded_frames[0] + assert isinstance(frame, np.ndarray), "Decoded frame is not a numpy array." + assert frame.ndim == 3, f"Expected 3D array (H, W, Channels), got {frame.shape}" From 61c1c0a62cb6afd22cdb1898485b8609200bd701 Mon Sep 17 00:00:00 2001 From: Andreas Date: Sun, 23 Feb 2025 20:08:17 +0100 Subject: [PATCH 03/10] docs: add prerequisites for H264Decoder --- vortex_utils/README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 vortex_utils/README.md diff --git a/vortex_utils/README.md b/vortex_utils/README.md new file mode 100644 index 0000000..5c1f904 --- /dev/null +++ b/vortex_utils/README.md @@ -0,0 +1,20 @@ +# H264Decoder +## Prerequisites +GStreamer +```bash +sudo apt install gstreamer1.0-tools gstreamer1.0-plugins-base \ +gstreamer1.0-plugins-good gstreamer1.0-plugins-bad \ +gstreamer1.0-plugins-ugly gstreamer1.0-libav python3-gi \ +python3-gst-1.0 +``` +If you experience display-related issues when launching the GUI (e.g., qt.qpa.wayland: eglSwapBuffers failed), you may need to force X11 instead of Wayland. +```bash +export QT_QPA_PLATFORM=xcb +``` +PyGObject +```bash +sudo apt update +sudo apt install -y libglib2.0-dev libcairo2-dev libgirepository1.0-dev \ +gir1.2-gtk-3.0 python3-dev +``` +- pygobject, can be installed with `pip install pygobject` \ No newline at end of file From 3e6a5631b27b604dc2976fce6c8476107eaf58ac Mon Sep 17 00:00:00 2001 From: Andreas Kluge Svendsrud <89779148+kluge7@users.noreply.github.com> Date: Sun, 23 Feb 2025 20:10:30 +0100 Subject: [PATCH 04/10] Update README.md --- vortex_utils/README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/vortex_utils/README.md b/vortex_utils/README.md index 5c1f904..a75b139 100644 --- a/vortex_utils/README.md +++ b/vortex_utils/README.md @@ -1,6 +1,6 @@ # H264Decoder ## Prerequisites -GStreamer +### GStreamer ```bash sudo apt install gstreamer1.0-tools gstreamer1.0-plugins-base \ gstreamer1.0-plugins-good gstreamer1.0-plugins-bad \ @@ -11,10 +11,14 @@ If you experience display-related issues when launching the GUI (e.g., qt.qpa.wa ```bash export QT_QPA_PLATFORM=xcb ``` -PyGObject +### PyGObject +Step 1: Install System Dependencies ```bash sudo apt update sudo apt install -y libglib2.0-dev libcairo2-dev libgirepository1.0-dev \ gir1.2-gtk-3.0 python3-dev ``` -- pygobject, can be installed with `pip install pygobject` \ No newline at end of file +Step 2: Install PyGObject +```bash +pip install pygobject +``` From c9ae5d2fd05bac699f546461484cd8f88adbc4a8 Mon Sep 17 00:00:00 2001 From: Andreas Kluge Svendsrud Date: Sun, 23 Feb 2025 20:55:33 +0100 Subject: [PATCH 05/10] fix(python_utils): H264Decoder now only stores last 3 frames in buffer --- vortex_utils/python_utils.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/vortex_utils/python_utils.py b/vortex_utils/python_utils.py index 2d709a9..53f61ff 100644 --- a/vortex_utils/python_utils.py +++ b/vortex_utils/python_utils.py @@ -140,11 +140,11 @@ def __init__(self): H264Decoder._gst_initialized = True pipeline_desc = ( - "appsrc name=mysrc is-live=true ! " # Receive raw H.264 stream data - "h264parse ! " # Parse the H.264 stream - "avdec_h264 ! " # Decode H.264 frames - "videoconvert ! video/x-raw,format=BGR ! " # Convert frames to BGR format - "appsink name=appsink" # Sink for retrieving processed frames + "appsrc name=mysrc is-live=true ! " + "h264parse ! " + "avdec_h264 ! " + "videoconvert ! video/x-raw,format=BGR ! " + "appsink name=appsink" ) self._pipeline = Gst.parse_launch(pipeline_desc) @@ -162,6 +162,7 @@ def __init__(self): self._main_loop = None self.decoded_frames = [] + self.max_frames = 3 # Keep only the last 3 frames here def start(self): """Starts the GStreamer pipeline and runs the main event loop.""" @@ -186,7 +187,7 @@ def push_data(self, data: bytes): """Pushes H.264 encoded data into the pipeline for decoding.""" if not self.appsrc: raise RuntimeError( - "The pipeline's appsrc element was not found or not initialized." + "The pipeline's appsrc element was not found or initialized." ) gst_buffer = Gst.Buffer.new_allocate(None, len(data), None) gst_buffer.fill(0, data) @@ -224,5 +225,8 @@ def _on_new_sample(self, sink): self.decoded_frames.append(frame_data_reshaped.copy()) + if len(self.decoded_frames) > self.max_frames: + self.decoded_frames.pop(0) + buf.unmap(map_info) return Gst.FlowReturn.OK From e7fd2a547fd483e2fd7713d886f3dcb3a56aa75b Mon Sep 17 00:00:00 2001 From: Andreas Kluge Svendsrud Date: Tue, 25 Feb 2025 17:14:27 +0100 Subject: [PATCH 06/10] ci(industrial-ci): add script for installing dependencies not supported by rosdep --- .github/workflows/industrial-ci.yml | 1 + scripts/ci_install_dependencies.sh | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 scripts/ci_install_dependencies.sh diff --git a/.github/workflows/industrial-ci.yml b/.github/workflows/industrial-ci.yml index 4b7d008..c9adce3 100644 --- a/.github/workflows/industrial-ci.yml +++ b/.github/workflows/industrial-ci.yml @@ -11,3 +11,4 @@ jobs: uses: vortexntnu/vortex-ci/.github/workflows/reusable-industrial-ci.yml@main with: ros_repo: '["main", "testing"]' + before_install_target_dependencies: 'scripts/ci_install_dependencies.sh' diff --git a/scripts/ci_install_dependencies.sh b/scripts/ci_install_dependencies.sh new file mode 100644 index 0000000..802a5a3 --- /dev/null +++ b/scripts/ci_install_dependencies.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# Script to install dependencies for H264Decoder +# This script installs GStreamer and PyGObject dependencies required for running the tests + +set -e # Exit on error + +### GStreamer Installation ### +echo "Installing GStreamer and related plugins..." +sudo apt update +sudo apt install -y gstreamer1.0-tools gstreamer1.0-plugins-base \ + gstreamer1.0-plugins-good gstreamer1.0-plugins-bad \ + gstreamer1.0-plugins-ugly gstreamer1.0-libav python3-gi \ + python3-gst-1.0 + +echo "GStreamer installation completed." + +echo "If you experience display-related issues with the GUI, try running:" +echo "export QT_QPA_PLATFORM=xcb" + +### PyGObject Installation ### +echo "Installing PyGObject dependencies..." +sudo apt install -y libglib2.0-dev libcairo2-dev libgirepository1.0-dev \ + gir1.2-gtk-3.0 python3-dev + +echo "Installing PyGObject via pip..." +pip install pygobject + +echo "Installation of all dependencies completed successfully." From 76ac83e9e7d811f5c62902eed0f21cd25920a3a5 Mon Sep 17 00:00:00 2001 From: Andreas Kluge Svendsrud Date: Tue, 25 Feb 2025 17:26:18 +0100 Subject: [PATCH 07/10] fix(scripts): ensure latest Meson version for PyCairo installation in ci_install_dependencies --- scripts/ci_install_dependencies.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) mode change 100644 => 100755 scripts/ci_install_dependencies.sh diff --git a/scripts/ci_install_dependencies.sh b/scripts/ci_install_dependencies.sh old mode 100644 new mode 100755 index 802a5a3..d18c96f --- a/scripts/ci_install_dependencies.sh +++ b/scripts/ci_install_dependencies.sh @@ -21,9 +21,13 @@ echo "export QT_QPA_PLATFORM=xcb" ### PyGObject Installation ### echo "Installing PyGObject dependencies..." sudo apt install -y libglib2.0-dev libcairo2-dev libgirepository1.0-dev \ - gir1.2-gtk-3.0 python3-dev + gir1.2-gtk-3.0 python3-dev ninja-build + +echo "Ensuring latest Meson version is installed..." +pip install --upgrade meson echo "Installing PyGObject via pip..." -pip install pygobject +pip install pycairo --no-cache-dir +pip install pygobject --no-cache-dir echo "Installation of all dependencies completed successfully." From 4e3a96c179ea6ed86378052a4e142405caf46f3a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2025 16:31:54 +0000 Subject: [PATCH 08/10] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 2699e4d..b6a7829 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -127,9 +127,9 @@ def test_h264_decoder(): decoding_thread.join(timeout=5.0) - assert ( - len(decoder.decoded_frames) > 0 - ), "No frames were decoded from the H.264 stream." + assert len(decoder.decoded_frames) > 0, ( + "No frames were decoded from the H.264 stream." + ) frame = decoder.decoded_frames[0] assert isinstance(frame, np.ndarray), "Decoded frame is not a numpy array." From b5c72e33cd95e3b2f597997911cd4067bd348dd9 Mon Sep 17 00:00:00 2001 From: Andreas Kluge Svendsrud Date: Tue, 25 Feb 2025 17:36:02 +0100 Subject: [PATCH 09/10] feat(code-coverage): add ci_install_dependencies script to this workflow --- .github/workflows/code-coverage.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 889e97c..f685149 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -10,5 +10,7 @@ on: jobs: call_reusable_workflow: uses: vortexntnu/vortex-ci/.github/workflows/reusable-code-coverage.yml@main + with: + before_install_target_dependencies: 'scripts/ci_install_dependencies.sh' secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} From 737b4b488a961839a73e0518fa28c5ba54ad6743 Mon Sep 17 00:00:00 2001 From: Andreas Kluge Svendsrud Date: Tue, 25 Feb 2025 18:06:29 +0100 Subject: [PATCH 10/10] docs(H264Decoder): update install guide --- vortex_utils/README.md | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/vortex_utils/README.md b/vortex_utils/README.md index a75b139..4b1cd50 100644 --- a/vortex_utils/README.md +++ b/vortex_utils/README.md @@ -1,24 +1,2 @@ # H264Decoder -## Prerequisites -### GStreamer -```bash -sudo apt install gstreamer1.0-tools gstreamer1.0-plugins-base \ -gstreamer1.0-plugins-good gstreamer1.0-plugins-bad \ -gstreamer1.0-plugins-ugly gstreamer1.0-libav python3-gi \ -python3-gst-1.0 -``` -If you experience display-related issues when launching the GUI (e.g., qt.qpa.wayland: eglSwapBuffers failed), you may need to force X11 instead of Wayland. -```bash -export QT_QPA_PLATFORM=xcb -``` -### PyGObject -Step 1: Install System Dependencies -```bash -sudo apt update -sudo apt install -y libglib2.0-dev libcairo2-dev libgirepository1.0-dev \ -gir1.2-gtk-3.0 python3-dev -``` -Step 2: Install PyGObject -```bash -pip install pygobject -``` +Install the dependencies by running the following script: [ci_install_dependencies.sh](/scripts/ci_install_dependencies.sh)