From 4cc9d151edaf202ba9b7a61e53ce572b18c67f83 Mon Sep 17 00:00:00 2001 From: Akash Shingha Date: Wed, 11 Mar 2026 15:42:47 +0200 Subject: [PATCH] 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);