From a2a3adf08e54ff880746868815d941bd464cd42e Mon Sep 17 00:00:00 2001 From: Akash Shingha Date: Mon, 9 Mar 2026 21:16:42 +0200 Subject: [PATCH 1/7] Update parameters.yaml for topic and resolution; add run_image2rtsp.sh launcher script --- config/parameters.yaml | 8 ++++---- run_image2rtsp.sh | 5 +++++ 2 files changed, 9 insertions(+), 4 deletions(-) create mode 100755 run_image2rtsp.sh diff --git a/config/parameters.yaml b/config/parameters.yaml index 2ca4cb0..c68dc21 100644 --- a/config/parameters.yaml +++ b/config/parameters.yaml @@ -3,12 +3,12 @@ # If the source is a ros2 topic (default case) compressed: False - topic: "color/image_raw" + topic: "camera/image" default_pipeline: | ( appsrc name=imagesrc do-timestamp=true min-latency=0 max-latency=0 max-bytes=1000 is-live=true ! videoconvert ! videoscale ! - video/x-raw, framerate=30/1, width=640, height=480 ! + video/x-raw, framerate=30/1, width=1920, height=1080 ! x264enc tune=zerolatency bitrate=500 key-int-max=30 ! video/x-h264, profile=baseline ! rtph264pay name=pay0 pt=96 ) @@ -31,9 +31,9 @@ # Notice: Here the framerate might be set to the camera framerate, otherwise "503 Service Unavailable" error will appear. # RTSP setup - mountpoint: "/back" + mountpoint: "/live" port: "8554" - local_only: True # True = rtsp://127.0.0.1:portAndMountpoint (The stream is accessible only from the local machine) + local_only: False # True = rtsp://127.0.0.1:portAndMountpoint (The stream is accessible only from the local machine) # False = rtsp://0.0.0.0:portAndMountpoint (The stream is accessible from the outside) # For example, to access the stream running on the machine with IP = 192.168.20.20, # use rtsp://192.186.20.20:portAndMountpoint diff --git a/run_image2rtsp.sh b/run_image2rtsp.sh new file mode 100755 index 0000000..ec59528 --- /dev/null +++ b/run_image2rtsp.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -e + +# Minimal launcher: runs a package launch with optional args. +exec ros2 launch image2rtsp "${1:-image2rtsp.launch.py}" "${@:2}" From 09afdc5d3c60ded9ab9c540664ddddacfa4f6788 Mon Sep 17 00:00:00 2001 From: Akash Shingha Date: Tue, 10 Mar 2026 02:02:31 +0200 Subject: [PATCH 2/7] Add Docker support and enhance logging for image processing --- .dockerignore | 6 ++++++ .gitignore | 2 ++ Dockerfile | 41 +++++++++++++++++++++++++++++++++++++++++ apt-requirements.txt | 18 ++++++++++++++++++ apt-runtime.txt | 10 ++++++++++ docker-compose.yml | 31 +++++++++++++++++++++++++++++++ docker-entrypoint.sh | 15 +++++++++++++++ requirements.txt | 1 + src/video.cpp | 1 + 9 files changed, 125 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 apt-requirements.txt create mode 100644 apt-runtime.txt create mode 100644 docker-compose.yml create mode 100755 docker-entrypoint.sh create mode 100644 requirements.txt diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6036ce2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +build +install +log +.git +*.pyc +__pycache__ diff --git a/.gitignore b/.gitignore index 881e002..8433776 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ build/ # VSC stuff .vscode +# Local python virtualenv +.venv/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a1e98ee --- /dev/null +++ b/Dockerfile @@ -0,0 +1,41 @@ +FROM ros:humble-ros-base AS build +SHELL ["/bin/bash", "-lc"] +WORKDIR /ws + +# Install OS packages first (cached layer when requirements.txt unchanged) +COPY apt-requirements.txt /tmp/requirements.txt +RUN apt-get update \ + && xargs -a /tmp/requirements.txt apt-get install -y --no-install-recommends \ + && rm -rf /var/lib/apt/lists/* /tmp/requirements.txt + +# Copy package manifest(s) so rosdep can install system deps and this layer caches +COPY package.xml ./package.xml +RUN source /opt/ros/humble/setup.bash \ + && rosdep update || true \ + && rosdep install -i --from-paths . --rosdistro humble -y || true + +# Copy rest of the sources after deps to avoid busting the deps layer on code changes +COPY . . + +# Build with Ninja + ccache for faster incremental builds inside the container +RUN source /opt/ros/humble/setup.bash \ + && CCACHE_DIR=/ccache mkdir -p /ccache \ + && chmod 777 /ccache \ + && colcon build --parallel-workers $(nproc) \ + --cmake-args -G Ninja -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache + +FROM ros:humble-ros-base +WORKDIR /ws +## Install minimal runtime packages required by the built binaries +COPY apt-runtime.txt /tmp/apt-runtime.txt +RUN apt-get update \ + && xargs -a /tmp/apt-runtime.txt apt-get install -y --no-install-recommends \ + && rm -rf /var/lib/apt/lists/* /tmp/apt-runtime.txt + +COPY --from=build /ws/install /ws/install +COPY docker-entrypoint.sh /ros_entrypoint.sh +RUN chmod +x /ros_entrypoint.sh +ENV ROS_DISTRO=humble +ENTRYPOINT ["/ros_entrypoint.sh"] +CMD ["image2rtsp.launch.py"] diff --git a/apt-requirements.txt b/apt-requirements.txt new file mode 100644 index 0000000..2166a05 --- /dev/null +++ b/apt-requirements.txt @@ -0,0 +1,18 @@ +python3-colcon-common-extensions +python3-pip +git +build-essential +cmake +pkg-config +libopencv-dev +python3-opencv +curl +python3-rosdep +python3-rosdistro +libgstreamer1.0-dev +libgstreamer-plugins-base1.0-dev +libgstreamer-plugins-good1.0-dev +libgstreamer-plugins-bad1.0-dev +libgstrtspserver-1.0-dev +ccache +ninja-build diff --git a/apt-runtime.txt b/apt-runtime.txt new file mode 100644 index 0000000..b4780ea --- /dev/null +++ b/apt-runtime.txt @@ -0,0 +1,10 @@ +libgstreamer1.0-0 +gstreamer1.0-plugins-base +gstreamer1.0-plugins-good +gstreamer1.0-plugins-bad +gstreamer1.0-plugins-ugly +libgstrtspserver-1.0-0 +python3-opencv +libopencv-dev +ros-humble-ros2cli +ros-humble-rclpy diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..638db72 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,31 @@ +services: + image2rtsp: + build: . + image: image2rtsp:latest + # Use host networking so the container binds to the host interfaces directly + # (Linux only). Remove port mapping when using host networking. + network_mode: "host" + environment: + - ROS_DISTRO=humble + # Propagate host ROS environment so DDS/discovery match (set on host if needed) + - ROS_DOMAIN_ID=${ROS_DOMAIN_ID:-0} + - RMW_IMPLEMENTATION=${RMW_IMPLEMENTATION:-} + # Ensure the node binds to non-localhost interface (disable local_only) + # and subscribe to the actual image topic available on the host + command: ["image2rtsp.launch.py", "local_only:=false", "topic:=/camera/image"] + + # Optional: add a lightweight test publisher service if you want the container + # to publish dummy images for testing (uncomment and adjust image if required). + # test_publisher: + # image: image2rtsp:latest + # network_mode: "host" + # environment: + # - ROS_DISTRO=humble + # - ROS_DOMAIN_ID=${ROS_DOMAIN_ID:-0} + # - RMW_IMPLEMENTATION=${RMW_IMPLEMENTATION:-} + # command: ["/bin/bash", "-c", "python3 - <<'PY'\nimport time, rclpy\nfrom rclpy.node import Node\nfrom sensor_msgs.msg import Image\nrclpy.init()\nnode = Node('dummy_pub')\npub = node.create_publisher(Image, '/camera/image', 10)\nmsg = Image()\nmsg.width=640; msg.height=480; msg.encoding='rgb8'; msg.is_bigendian=0; msg.step=msg.width*3\nmsg.data = bytes([0])*(msg.step*msg.height)\ntry:\n while rclpy.ok():\n pub.publish(msg)\n rclpy.spin_once(node, timeout_sec=0.01)\n time.sleep(1/30)\nexcept KeyboardInterrupt:\n pass\nrclpy.shutdown()\nPY"] + # Do not mount the workspace by default — the image contains the built overlay. + # To use a host workspace for live development, uncomment and adjust the line below: + # volumes: + # - ./:/ws:rw + restart: unless-stopped diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..6c9d899 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -e + +# Source ROS and workspace overlays then exec ros2 launch with provided args +if [ -n "${ROS_DISTRO:-}" ]; then + source "/opt/ros/${ROS_DISTRO}/setup.bash" || true +else + source /opt/ros/humble/setup.bash || true +fi + +if [ -f /ws/install/setup.bash ]; then + source /ws/install/setup.bash +fi + +exec ros2 launch image2rtsp "$@" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0dd006b --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +opencv-python diff --git a/src/video.cpp b/src/video.cpp index bde96c5..4c74d8d 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -159,6 +159,7 @@ void Image2rtsp::topic_callback(const sensor_msgs::msg::Image::SharedPtr msg){ GstCaps *caps; // image properties. see return of Image2rtsp::gst_caps_new_from_image char *gst_type, *gst_format = (char *)""; if (appsrc != NULL){ + RCLCPP_INFO(this->get_logger(), "Received image %dx%d, encoding=%s", msg->width, msg->height, msg->encoding.c_str()); // Set caps from message caps = gst_caps_new_from_image(msg); gst_app_src_set_caps(appsrc, caps); From 4cc9d151edaf202ba9b7a61e53ce572b18c67f83 Mon Sep 17 00:00:00 2001 From: Akash Shingha Date: Wed, 11 Mar 2026 15:42:47 +0200 Subject: [PATCH 3/7] Enhance Dockerfile for interactive shell support and update logging levels; modify parameters and dependencies for improved functionality --- Dockerfile | 11 +++++++ apt-runtime.txt | 3 ++ config/parameters.yaml | 2 +- docker-compose.yml | 18 +++-------- include/image2rtsp.hpp | 8 ++--- launch/image2rtsp.launch.py | 4 ++- src/image2rtsp.cpp | 12 ++++---- src/video.cpp | 60 +++++++++++++++++++++++++++++-------- 8 files changed, 80 insertions(+), 38 deletions(-) diff --git a/Dockerfile b/Dockerfile index a1e98ee..bce32ce 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,5 +37,16 @@ COPY --from=build /ws/install /ws/install COPY docker-entrypoint.sh /ros_entrypoint.sh RUN chmod +x /ros_entrypoint.sh ENV ROS_DISTRO=humble + +## Make ROS and workspace overlays available for interactive shells +# This ensures `docker exec -it bash` has `ros2` on PATH +RUN echo "source /opt/ros/${ROS_DISTRO}/setup.bash || true" > /etc/profile.d/ros2.sh \ + && echo "[ -f /ws/install/setup.bash ] && source /ws/install/setup.bash" >> /etc/profile.d/ros2.sh \ + && chmod +x /etc/profile.d/ros2.sh + +# Also source in non-login interactive bash shells +RUN echo "source /opt/ros/${ROS_DISTRO}/setup.bash || true" >> /etc/bash.bashrc \ + && echo "[ -f /ws/install/setup.bash ] && source /ws/install/setup.bash" >> /etc/bash.bashrc + ENTRYPOINT ["/ros_entrypoint.sh"] CMD ["image2rtsp.launch.py"] diff --git a/apt-runtime.txt b/apt-runtime.txt index b4780ea..9fa736c 100644 --- a/apt-runtime.txt +++ b/apt-runtime.txt @@ -8,3 +8,6 @@ python3-opencv libopencv-dev ros-humble-ros2cli ros-humble-rclpy + +net-tools +iputils-ping diff --git a/config/parameters.yaml b/config/parameters.yaml index c68dc21..eb4b3ff 100644 --- a/config/parameters.yaml +++ b/config/parameters.yaml @@ -8,7 +8,7 @@ ( appsrc name=imagesrc do-timestamp=true min-latency=0 max-latency=0 max-bytes=1000 is-live=true ! videoconvert ! videoscale ! - video/x-raw, framerate=30/1, width=1920, height=1080 ! + video/x-raw, framerate=8/1 ! x264enc tune=zerolatency bitrate=500 key-int-max=30 ! video/x-h264, profile=baseline ! rtph264pay name=pay0 pt=96 ) diff --git a/docker-compose.yml b/docker-compose.yml index 638db72..8598f32 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,22 +10,12 @@ services: # Propagate host ROS environment so DDS/discovery match (set on host if needed) - ROS_DOMAIN_ID=${ROS_DOMAIN_ID:-0} - RMW_IMPLEMENTATION=${RMW_IMPLEMENTATION:-} + # GStreamer debug level (uncomment for verbose GStreamer logging) + # - GST_DEBUG=${GST_DEBUG:-} # Ensure the node binds to non-localhost interface (disable local_only) # and subscribe to the actual image topic available on the host + command: ["image2rtsp.launch.py", "local_only:=false", "topic:=/camera/image"] - # Optional: add a lightweight test publisher service if you want the container - # to publish dummy images for testing (uncomment and adjust image if required). - # test_publisher: - # image: image2rtsp:latest - # network_mode: "host" - # environment: - # - ROS_DISTRO=humble - # - ROS_DOMAIN_ID=${ROS_DOMAIN_ID:-0} - # - RMW_IMPLEMENTATION=${RMW_IMPLEMENTATION:-} - # command: ["/bin/bash", "-c", "python3 - <<'PY'\nimport time, rclpy\nfrom rclpy.node import Node\nfrom sensor_msgs.msg import Image\nrclpy.init()\nnode = Node('dummy_pub')\npub = node.create_publisher(Image, '/camera/image', 10)\nmsg = Image()\nmsg.width=640; msg.height=480; msg.encoding='rgb8'; msg.is_bigendian=0; msg.step=msg.width*3\nmsg.data = bytes([0])*(msg.step*msg.height)\ntry:\n while rclpy.ok():\n pub.publish(msg)\n rclpy.spin_once(node, timeout_sec=0.01)\n time.sleep(1/30)\nexcept KeyboardInterrupt:\n pass\nrclpy.shutdown()\nPY"] - # Do not mount the workspace by default — the image contains the built overlay. - # To use a host workspace for live development, uncomment and adjust the line below: - # volumes: - # - ./:/ws:rw + restart: unless-stopped diff --git a/include/image2rtsp.hpp b/include/image2rtsp.hpp index b5b7693..69b26ea 100644 --- a/include/image2rtsp.hpp +++ b/include/image2rtsp.hpp @@ -15,6 +15,8 @@ class Image2rtsp : public rclcpp::Node{ public: Image2rtsp(); GstRTSPServer *rtsp_server; + uint framerate; + GstAppSrc *appsrc; private: string topic; @@ -23,14 +25,12 @@ private: string pipeline; string default_pipeline; string camera_pipeline; - uint framerate; bool local_only; bool camera; bool compressed; - GstAppSrc *appsrc; void video_mainloop_start(); - void rtsp_server_add_url(const char *url, const char *sPipeline, GstElement **appsrc); + void rtsp_server_add_url(const char *url, const char *sPipeline); void topic_callback(const sensor_msgs::msg::Image::SharedPtr msg); void compressed_topic_callback(const sensor_msgs::msg::CompressedImage::SharedPtr msg); uint extract_framerate(const std::string& pipeline, uint default_framerate); @@ -40,7 +40,7 @@ private: rclcpp::Subscription::SharedPtr subscription_compressed_; }; -static void media_configure(GstRTSPMediaFactory *factory, GstRTSPMedia *media, GstElement **appsrc); +static void media_configure(GstRTSPMediaFactory *factory, GstRTSPMedia *media, gpointer user_data); static void *mainloop(void *arg); static gboolean session_cleanup(Image2rtsp *node, rclcpp::Logger logger, gboolean ignored); diff --git a/launch/image2rtsp.launch.py b/launch/image2rtsp.launch.py index ffa0a25..7397979 100644 --- a/launch/image2rtsp.launch.py +++ b/launch/image2rtsp.launch.py @@ -15,6 +15,8 @@ def generate_launch_description(): package='image2rtsp', executable='image2rtsp', name='image2rtsp', - parameters=[config] + parameters=[config], + # Reduce runtime logging verbosity to WARN to avoid info spam in container logs + arguments=['--ros-args', '--log-level', 'warn'] ) ]) \ No newline at end of file diff --git a/src/image2rtsp.cpp b/src/image2rtsp.cpp index 58d101f..21b297f 100644 --- a/src/image2rtsp.cpp +++ b/src/image2rtsp.cpp @@ -49,15 +49,15 @@ Image2rtsp::Image2rtsp() : Node("image2rtsp"){ if (camera == false){ if (compressed == false){ subscription_ = this->create_subscription(topic, 10, std::bind(&Image2rtsp::topic_callback, this, _1)); - RCLCPP_INFO(this->get_logger(), "Subscribing to sensor_msgs::msg::Image"); + RCLCPP_DEBUG(this->get_logger(), "Subscribing to sensor_msgs::msg::Image"); } else { subscription_compressed_ = this->create_subscription(topic, 10, std::bind(&Image2rtsp::compressed_topic_callback, this, _1)); - RCLCPP_INFO(this->get_logger(), "Subscribing to sensor_msgs::msg::CompressedImage"); + RCLCPP_DEBUG(this->get_logger(), "Subscribing to sensor_msgs::msg::CompressedImage"); } } else { - RCLCPP_INFO(this->get_logger(), "Trying to access camera device"); + RCLCPP_DEBUG(this->get_logger(), "Trying to access camera device"); } // Start the RTSP server @@ -67,9 +67,9 @@ Image2rtsp::Image2rtsp() : Node("image2rtsp"){ pipeline = camera ? camera_pipeline : default_pipeline; framerate = extract_framerate(pipeline, 30); - rtsp_server_add_url(mountpoint.c_str(), pipeline.c_str(), camera ? nullptr : (GstElement **)&appsrc); + rtsp_server_add_url(mountpoint.c_str(), pipeline.c_str()); - RCLCPP_INFO(this->get_logger(), "Stream available at rtsp://%s:%s%s", gst_rtsp_server_get_address(rtsp_server), port.c_str(), mountpoint.c_str()); + RCLCPP_DEBUG(this->get_logger(), "Stream available at rtsp://%s:%s%s", gst_rtsp_server_get_address(rtsp_server), port.c_str(), mountpoint.c_str()); } uint Image2rtsp::extract_framerate(const std::string& pipeline, uint default_framerate = 30) { @@ -99,7 +99,7 @@ uint Image2rtsp::extract_framerate(const std::string& pipeline, uint default_fra RCLCPP_WARN(this->get_logger(), "Invalid framerate value %d, using default: %d", framerate, default_framerate); return default_framerate; } - RCLCPP_INFO(this->get_logger(), "Using set framerate %d", framerate); + RCLCPP_DEBUG(this->get_logger(), "Using set framerate %d", framerate); return framerate; } catch (const std::exception& e) { RCLCPP_WARN(this->get_logger(), "Failed to parse framerate '%s', using default: %d", framerate_str.c_str(), default_framerate); diff --git a/src/video.cpp b/src/video.cpp index 4c74d8d..7a94285 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -38,7 +38,7 @@ GstRTSPServer *Image2rtsp::rtsp_server_create(const std::string &port, const boo return server; } -void Image2rtsp::rtsp_server_add_url(const char *url, const char *sPipeline, GstElement **appsrc){ +void Image2rtsp::rtsp_server_add_url(const char *url, const char *sPipeline){ GstRTSPMountPoints *mounts; GstRTSPMediaFactory *factory; @@ -55,9 +55,12 @@ void Image2rtsp::rtsp_server_add_url(const char *url, const char *sPipeline, Gst /* notify when our media is ready, This is called whenever someone asks for * the media and a new pipeline is created */ - g_signal_connect(factory, "media-configure", (GCallback)media_configure, appsrc); + // Pass `this` as user_data so media_configure can access node state and push a preroll frame + g_signal_connect(factory, "media-configure", (GCallback)media_configure, this); - gst_rtsp_media_factory_set_shared(factory, TRUE); + // Use non-shared media factory so each client gets its own pipeline + // This avoids prerolling a shared pipeline without available appsrc data + gst_rtsp_media_factory_set_shared(factory, FALSE); /* attach the factory to the url */ gst_rtsp_mount_points_add_factory(mounts, url, factory); @@ -66,17 +69,50 @@ void Image2rtsp::rtsp_server_add_url(const char *url, const char *sPipeline, Gst g_object_unref(mounts); } -static void media_configure(GstRTSPMediaFactory *factory, GstRTSPMedia *media, GstElement **appsrc){ - if(appsrc){ - GstElement *pipeline = gst_rtsp_media_get_element(media); +static void media_configure(GstRTSPMediaFactory *factory, GstRTSPMedia *media, gpointer user_data){ + Image2rtsp *node = static_cast(user_data); + GstElement *pipeline = gst_rtsp_media_get_element(media); + GstElement *imagesrc = gst_bin_get_by_name(GST_BIN(pipeline), "imagesrc"); - *appsrc = gst_bin_get_by_name(GST_BIN(pipeline), "imagesrc"); + if (imagesrc){ + /* store appsrc in node for later pushes */ + node->appsrc = GST_APP_SRC(imagesrc); - /* this instructs appsrc that we will be dealing with timed buffer */ - gst_util_set_object_arg(G_OBJECT(*appsrc), "format", "time"); + /* instruct appsrc that we will be dealing with timed buffers */ + gst_util_set_object_arg(G_OBJECT(node->appsrc), "format", "time"); + /* mark stream-type to not require preroll and reduce buffering */ + gst_app_src_set_stream_type(node->appsrc, GST_APP_STREAM_TYPE_STREAM); + gst_app_src_set_max_buffers(node->appsrc, 0); + gst_app_src_set_max_bytes(node->appsrc, 0); + gst_app_src_set_max_time(node->appsrc, 0); + + /* create a minimal dummy preroll frame to satisfy pipeline preroll */ + guint width = 2; + guint height = 2; + guint fr = node->framerate > 0 ? node->framerate : 30; + GstCaps *caps = gst_caps_new_simple("video/x-raw", + "format", G_TYPE_STRING, "RGB", + "width", G_TYPE_INT, width, + "height", G_TYPE_INT, height, + "framerate", GST_TYPE_FRACTION, fr, 1, + NULL); + gst_app_src_set_caps(node->appsrc, caps); + + gsize buf_size = width * height * 3; + GstBuffer *buf = gst_buffer_new_allocate(NULL, buf_size, NULL); + GstMapInfo map; + if (gst_buffer_map(buf, &map, GST_MAP_WRITE)){ + if (map.data) memset(map.data, 64, buf_size); + gst_buffer_unmap(buf, &map); + } + GST_BUFFER_FLAG_SET(buf, GST_BUFFER_FLAG_LIVE); + gst_app_src_push_buffer(node->appsrc, buf); + + gst_caps_unref(caps); gst_object_unref(pipeline); - }else{ + return; + } else { guint i, n_streams; n_streams = gst_rtsp_media_n_streams(media); @@ -149,7 +185,7 @@ static gboolean session_cleanup(Image2rtsp *node, rclcpp::Logger logger, gboolea { char s[32]; snprintf(s, 32, (char *)"Sessions cleaned: %d", num); - RCLCPP_INFO(node->get_logger(), s); + RCLCPP_DEBUG(node->get_logger(), s); } return TRUE; } @@ -159,7 +195,7 @@ void Image2rtsp::topic_callback(const sensor_msgs::msg::Image::SharedPtr msg){ GstCaps *caps; // image properties. see return of Image2rtsp::gst_caps_new_from_image char *gst_type, *gst_format = (char *)""; if (appsrc != NULL){ - RCLCPP_INFO(this->get_logger(), "Received image %dx%d, encoding=%s", msg->width, msg->height, msg->encoding.c_str()); + RCLCPP_DEBUG(this->get_logger(), "Received image %dx%d, encoding=%s", msg->width, msg->height, msg->encoding.c_str()); // Set caps from message caps = gst_caps_new_from_image(msg); gst_app_src_set_caps(appsrc, caps); From 8c37334b208d1144f27e04c9697088f4bff726c2 Mon Sep 17 00:00:00 2001 From: Akash Shingha Date: Wed, 11 Mar 2026 18:43:51 +0200 Subject: [PATCH 4/7] Refactor Dockerfile and update dependencies for improved build efficiency and runtime support --- Dockerfile | 10 ++++------ apt-requirements.txt | 7 ++----- apt-runtime.txt | 6 ------ 3 files changed, 6 insertions(+), 17 deletions(-) diff --git a/Dockerfile b/Dockerfile index bce32ce..16465e1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,22 +2,21 @@ FROM ros:humble-ros-base AS build SHELL ["/bin/bash", "-lc"] WORKDIR /ws -# Install OS packages first (cached layer when requirements.txt unchanged) +# Install build dependencies COPY apt-requirements.txt /tmp/requirements.txt RUN apt-get update \ && xargs -a /tmp/requirements.txt apt-get install -y --no-install-recommends \ && rm -rf /var/lib/apt/lists/* /tmp/requirements.txt -# Copy package manifest(s) so rosdep can install system deps and this layer caches +# Copy package manifest and install rosdeps COPY package.xml ./package.xml +RUN add-apt-repository multiverse || true RUN source /opt/ros/humble/setup.bash \ && rosdep update || true \ && rosdep install -i --from-paths . --rosdistro humble -y || true -# Copy rest of the sources after deps to avoid busting the deps layer on code changes +# Copy sources and build COPY . . - -# Build with Ninja + ccache for faster incremental builds inside the container RUN source /opt/ros/humble/setup.bash \ && CCACHE_DIR=/ccache mkdir -p /ccache \ && chmod 777 /ccache \ @@ -39,7 +38,6 @@ RUN chmod +x /ros_entrypoint.sh ENV ROS_DISTRO=humble ## Make ROS and workspace overlays available for interactive shells -# This ensures `docker exec -it bash` has `ros2` on PATH RUN echo "source /opt/ros/${ROS_DISTRO}/setup.bash || true" > /etc/profile.d/ros2.sh \ && echo "[ -f /ws/install/setup.bash ] && source /ws/install/setup.bash" >> /etc/profile.d/ros2.sh \ && chmod +x /etc/profile.d/ros2.sh diff --git a/apt-requirements.txt b/apt-requirements.txt index 2166a05..81cd4a6 100644 --- a/apt-requirements.txt +++ b/apt-requirements.txt @@ -1,18 +1,15 @@ python3-colcon-common-extensions -python3-pip -git build-essential cmake pkg-config libopencv-dev python3-opencv -curl python3-rosdep python3-rosdistro +software-properties-common libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-good1.0-dev -libgstreamer-plugins-bad1.0-dev libgstrtspserver-1.0-dev -ccache ninja-build +ccache diff --git a/apt-runtime.txt b/apt-runtime.txt index 9fa736c..c60c1d2 100644 --- a/apt-runtime.txt +++ b/apt-runtime.txt @@ -1,13 +1,7 @@ libgstreamer1.0-0 gstreamer1.0-plugins-base gstreamer1.0-plugins-good -gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly libgstrtspserver-1.0-0 python3-opencv -libopencv-dev ros-humble-ros2cli -ros-humble-rclpy - -net-tools -iputils-ping From 5fd57decc95a848b81f9cb879a98c8bf2c37b845 Mon Sep 17 00:00:00 2001 From: Akash Shingha Date: Wed, 11 Mar 2026 19:15:02 +0200 Subject: [PATCH 5/7] Remove python3-opencv and ros-humble-ros2cli from apt requirements for cleaner dependencies --- apt-requirements.txt | 2 +- apt-runtime.txt | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/apt-requirements.txt b/apt-requirements.txt index 81cd4a6..b74768a 100644 --- a/apt-requirements.txt +++ b/apt-requirements.txt @@ -3,7 +3,6 @@ build-essential cmake pkg-config libopencv-dev -python3-opencv python3-rosdep python3-rosdistro software-properties-common @@ -13,3 +12,4 @@ libgstreamer-plugins-good1.0-dev libgstrtspserver-1.0-dev ninja-build ccache + diff --git a/apt-runtime.txt b/apt-runtime.txt index c60c1d2..c9c63e7 100644 --- a/apt-runtime.txt +++ b/apt-runtime.txt @@ -4,4 +4,3 @@ gstreamer1.0-plugins-good gstreamer1.0-plugins-ugly libgstrtspserver-1.0-0 python3-opencv -ros-humble-ros2cli From f151a78ebde28c6bf301ff076bd2dc9f61fd2940 Mon Sep 17 00:00:00 2001 From: Akash Shingha Date: Thu, 12 Mar 2026 14:43:17 +0200 Subject: [PATCH 6/7] Enhance Dockerfile and launch configuration for improved logging and UDP transport; update runtime dependencies and remove unnecessary dummy frames in video processing --- Dockerfile | 4 ++++ apt-requirements.txt | 1 + apt-runtime.txt | 5 +++++ docker-compose.yml | 3 ++- fastdds_no_shm.xml | 21 +++++++++++++++++++++ launch/image2rtsp.launch.py | 12 ++++++++++-- src/image2rtsp.cpp | 8 +++++++- src/video.cpp | 27 ++++----------------------- 8 files changed, 54 insertions(+), 27 deletions(-) create mode 100644 fastdds_no_shm.xml diff --git a/Dockerfile b/Dockerfile index 16465e1..80d744f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,7 +35,11 @@ RUN apt-get update \ COPY --from=build /ws/install /ws/install COPY docker-entrypoint.sh /ros_entrypoint.sh RUN chmod +x /ros_entrypoint.sh +# Bake FastDDS UDP-only profile into the image so the shared-memory transport +# fix applies regardless of how the container is started (compose or docker run). +COPY fastdds_no_shm.xml /ws/fastdds_no_shm.xml ENV ROS_DISTRO=humble +ENV FASTRTPS_DEFAULT_PROFILES_FILE=/ws/fastdds_no_shm.xml ## Make ROS and workspace overlays available for interactive shells RUN echo "source /opt/ros/${ROS_DISTRO}/setup.bash || true" > /etc/profile.d/ros2.sh \ diff --git a/apt-requirements.txt b/apt-requirements.txt index b74768a..67fd704 100644 --- a/apt-requirements.txt +++ b/apt-requirements.txt @@ -3,6 +3,7 @@ build-essential cmake pkg-config libopencv-dev +python3-opencv python3-rosdep python3-rosdistro software-properties-common diff --git a/apt-runtime.txt b/apt-runtime.txt index c9c63e7..982f1f8 100644 --- a/apt-runtime.txt +++ b/apt-runtime.txt @@ -1,6 +1,11 @@ libgstreamer1.0-0 gstreamer1.0-plugins-base gstreamer1.0-plugins-good +gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly libgstrtspserver-1.0-0 python3-opencv +ros-humble-ros2cli +ros-humble-rclpy +net-tools +iputils-ping diff --git a/docker-compose.yml b/docker-compose.yml index 8598f32..149a92a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,10 +12,11 @@ services: - RMW_IMPLEMENTATION=${RMW_IMPLEMENTATION:-} # GStreamer debug level (uncomment for verbose GStreamer logging) # - GST_DEBUG=${GST_DEBUG:-} + # FASTRTPS_DEFAULT_PROFILES_FILE is set in the Dockerfile (baked into image) # Ensure the node binds to non-localhost interface (disable local_only) # and subscribe to the actual image topic available on the host - command: ["image2rtsp.launch.py", "local_only:=false", "topic:=/camera/image"] + command: ["image2rtsp.launch.py", "local_only:=false", "topic:=/camera/image", "log_level:=debug"] restart: unless-stopped diff --git a/fastdds_no_shm.xml b/fastdds_no_shm.xml new file mode 100644 index 0000000..a67988c --- /dev/null +++ b/fastdds_no_shm.xml @@ -0,0 +1,21 @@ + + + + + + UDPv4Transport + UDPv4 + + + + + + UDPv4Transport + + false + + + diff --git a/launch/image2rtsp.launch.py b/launch/image2rtsp.launch.py index 7397979..7e45101 100644 --- a/launch/image2rtsp.launch.py +++ b/launch/image2rtsp.launch.py @@ -1,6 +1,8 @@ import os from ament_index_python.packages import get_package_share_directory from launch import LaunchDescription +from launch.actions import DeclareLaunchArgument +from launch.substitutions import LaunchConfiguration from launch_ros.actions import Node def generate_launch_description(): @@ -10,13 +12,19 @@ def generate_launch_description(): 'parameters.yaml' ) + log_level = LaunchConfiguration('log_level') + return LaunchDescription([ + DeclareLaunchArgument( + 'log_level', + default_value='warn', + description='ROS logger level (debug, info, warn, error, fatal)' + ), Node( package='image2rtsp', executable='image2rtsp', name='image2rtsp', parameters=[config], - # Reduce runtime logging verbosity to WARN to avoid info spam in container logs - arguments=['--ros-args', '--log-level', 'warn'] + arguments=['--ros-args', '--log-level', log_level] ) ]) \ No newline at end of file diff --git a/src/image2rtsp.cpp b/src/image2rtsp.cpp index 21b297f..7d78eda 100644 --- a/src/image2rtsp.cpp +++ b/src/image2rtsp.cpp @@ -69,7 +69,13 @@ Image2rtsp::Image2rtsp() : Node("image2rtsp"){ framerate = extract_framerate(pipeline, 30); rtsp_server_add_url(mountpoint.c_str(), pipeline.c_str()); - RCLCPP_DEBUG(this->get_logger(), "Stream available at rtsp://%s:%s%s", gst_rtsp_server_get_address(rtsp_server), port.c_str(), mountpoint.c_str()); + const char *server_address = gst_rtsp_server_get_address(rtsp_server); + if (local_only) { + RCLCPP_DEBUG(this->get_logger(), "Stream available at rtsp://%s:%s%s", server_address, port.c_str(), mountpoint.c_str()); + } else { + RCLCPP_DEBUG(this->get_logger(), "RTSP server bound to %s:%s%s", server_address, port.c_str(), mountpoint.c_str()); + RCLCPP_DEBUG(this->get_logger(), "Connect clients using rtsp://:%s%s (0.0.0.0 is bind-only)", port.c_str(), mountpoint.c_str()); + } } uint Image2rtsp::extract_framerate(const std::string& pipeline, uint default_framerate = 30) { diff --git a/src/video.cpp b/src/video.cpp index 7a94285..cd37e2c 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -87,29 +87,9 @@ static void media_configure(GstRTSPMediaFactory *factory, GstRTSPMedia *media, g gst_app_src_set_max_bytes(node->appsrc, 0); gst_app_src_set_max_time(node->appsrc, 0); - /* create a minimal dummy preroll frame to satisfy pipeline preroll */ - guint width = 2; - guint height = 2; - guint fr = node->framerate > 0 ? node->framerate : 30; - GstCaps *caps = gst_caps_new_simple("video/x-raw", - "format", G_TYPE_STRING, "RGB", - "width", G_TYPE_INT, width, - "height", G_TYPE_INT, height, - "framerate", GST_TYPE_FRACTION, fr, 1, - NULL); - gst_app_src_set_caps(node->appsrc, caps); - - gsize buf_size = width * height * 3; - GstBuffer *buf = gst_buffer_new_allocate(NULL, buf_size, NULL); - GstMapInfo map; - if (gst_buffer_map(buf, &map, GST_MAP_WRITE)){ - if (map.data) memset(map.data, 64, buf_size); - gst_buffer_unmap(buf, &map); - } - GST_BUFFER_FLAG_SET(buf, GST_BUFFER_FLAG_LIVE); - gst_app_src_push_buffer(node->appsrc, buf); - - gst_caps_unref(caps); + /* caps and first buffer are set by topic_callback from the real image + * so that caps always match the actual camera resolution/format. + * Pushing a dummy preroll with wrong caps breaks x264enc mid-stream. */ gst_object_unref(pipeline); return; } else { @@ -199,6 +179,7 @@ void Image2rtsp::topic_callback(const sensor_msgs::msg::Image::SharedPtr msg){ // Set caps from message caps = gst_caps_new_from_image(msg); gst_app_src_set_caps(appsrc, caps); + gst_caps_unref(caps); buf = gst_buffer_new_allocate(nullptr, msg->data.size(), nullptr); gst_buffer_fill(buf, 0, msg->data.data(), msg->data.size()); GST_BUFFER_FLAG_SET(buf, GST_BUFFER_FLAG_LIVE); From 7d4eca3c4611625c1e7eb9891a4b7981c19b5516 Mon Sep 17 00:00:00 2001 From: Akash Shingha Date: Thu, 12 Mar 2026 17:11:03 +0200 Subject: [PATCH 7/7] Refactor Dockerfile and update dependency management for improved clarity; enhance README with dependency details and installation instructions --- Dockerfile | 8 ++++---- README.md | 24 ++++++++++++++++++++++++ apt-requirements.txt | 2 +- apt-runtime.txt | 9 ++++++--- docker-compose.yml | 3 ++- requirements.txt | 3 ++- 6 files changed, 39 insertions(+), 10 deletions(-) diff --git a/Dockerfile b/Dockerfile index 80d744f..26903d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,10 +3,10 @@ SHELL ["/bin/bash", "-lc"] WORKDIR /ws # Install build dependencies -COPY apt-requirements.txt /tmp/requirements.txt +COPY apt-requirements.txt /tmp/apt-build.txt RUN apt-get update \ - && xargs -a /tmp/requirements.txt apt-get install -y --no-install-recommends \ - && rm -rf /var/lib/apt/lists/* /tmp/requirements.txt + && grep -Ev '^[[:space:]]*($|#)' /tmp/apt-build.txt | xargs apt-get install -y --no-install-recommends \ + && rm -rf /var/lib/apt/lists/* /tmp/apt-build.txt # Copy package manifest and install rosdeps COPY package.xml ./package.xml @@ -29,7 +29,7 @@ WORKDIR /ws ## Install minimal runtime packages required by the built binaries COPY apt-runtime.txt /tmp/apt-runtime.txt RUN apt-get update \ - && xargs -a /tmp/apt-runtime.txt apt-get install -y --no-install-recommends \ + && grep -Ev '^[[:space:]]*($|#)' /tmp/apt-runtime.txt | xargs apt-get install -y --no-install-recommends \ && rm -rf /var/lib/apt/lists/* /tmp/apt-runtime.txt COPY --from=build /ws/install /ws/install diff --git a/README.md b/README.md index a921d63..23f0157 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,34 @@ You are reading now the README for a **default** ROS2 package. If you want to us ## Dependencies - ROS2 Humble +- Dependency files in this repository: + - `apt-requirements.txt`: build-only system dependencies. + - `apt-runtime.txt`: runtime system dependencies (minimal default set). + - `requirements.txt`: pip dependencies (currently none required). +- OpenCV policy: use APT package `python3-opencv` (do not install `opencv-python` via pip by default). + +- Optional debug-only packages: + - `ros-humble-ros2cli` + - `net-tools` + - `iputils-ping` + - These are listed as commented entries in `apt-runtime.txt` and are not installed by default. + - gstreamer libs: ```bash sudo apt-get install libgstreamer-plugins-base1.0-dev libgstreamer-plugins-good1.0-dev libgstreamer-plugins-bad1.0-dev libgstrtspserver-1.0-dev gstreamer1.0-plugins-ugly gstreamer1.0-plugins-bad ``` + +For a minimal host setup using repository-managed lists: +```bash +sudo apt-get update +grep -Ev '^[[:space:]]*($|#)' apt-requirements.txt | xargs sudo apt-get install -y --no-install-recommends +grep -Ev '^[[:space:]]*($|#)' apt-runtime.txt | xargs sudo apt-get install -y --no-install-recommends +``` + +Install debug tools only when needed: +```bash +sudo apt-get install -y --no-install-recommends ros-humble-ros2cli net-tools iputils-ping +``` ## Install - Navigate to the root directory, create a new directory named `ros2_ws/src`, and then change the current working directory to `ros2_ws/src`: ```bashrc diff --git a/apt-requirements.txt b/apt-requirements.txt index 67fd704..86eb89a 100644 --- a/apt-requirements.txt +++ b/apt-requirements.txt @@ -3,12 +3,12 @@ build-essential cmake pkg-config libopencv-dev -python3-opencv python3-rosdep python3-rosdistro software-properties-common libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev +libgstreamer-plugins-bad1.0-dev libgstreamer-plugins-good1.0-dev libgstrtspserver-1.0-dev ninja-build diff --git a/apt-runtime.txt b/apt-runtime.txt index 982f1f8..e6ff5be 100644 --- a/apt-runtime.txt +++ b/apt-runtime.txt @@ -5,7 +5,10 @@ gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly libgstrtspserver-1.0-0 python3-opencv -ros-humble-ros2cli ros-humble-rclpy -net-tools -iputils-ping + +# Optional debug-only tools (not required for normal runtime) +# Uncomment/install manually only when troubleshooting: +# ros-humble-ros2cli +# net-tools +# iputils-ping diff --git a/docker-compose.yml b/docker-compose.yml index 149a92a..f0d3c10 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,8 @@ services: # Ensure the node binds to non-localhost interface (disable local_only) # and subscribe to the actual image topic available on the host - command: ["image2rtsp.launch.py", "local_only:=false", "topic:=/camera/image", "log_level:=debug"] + #log_level:= info, debug, warning, error, fatal + command: ["image2rtsp.launch.py", "local_only:=false", "topic:=/camera/image", "log_level:=info"] restart: unless-stopped diff --git a/requirements.txt b/requirements.txt index 0dd006b..15662a4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -opencv-python +# No required pip dependencies. +# OpenCV is provided via apt package: python3-opencv.