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);