Enhance Dockerfile for interactive shell support and update logging levels; modify parameters and dependencies for improved functionality

This commit is contained in:
Akash Shingha 2026-03-11 15:42:47 +02:00
parent 09afdc5d3c
commit 4cc9d151ed
8 changed files with 80 additions and 38 deletions

View File

@ -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 <container> 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"]

View File

@ -8,3 +8,6 @@ python3-opencv
libopencv-dev
ros-humble-ros2cli
ros-humble-rclpy
net-tools
iputils-ping

View File

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

View File

@ -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

View File

@ -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<sensor_msgs::msg::CompressedImage>::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);

View File

@ -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']
)
])

View File

@ -49,15 +49,15 @@ Image2rtsp::Image2rtsp() : Node("image2rtsp"){
if (camera == false){
if (compressed == false){
subscription_ = this->create_subscription<sensor_msgs::msg::Image>(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<sensor_msgs::msg::CompressedImage>(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);

View File

@ -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){
static void media_configure(GstRTSPMediaFactory *factory, GstRTSPMedia *media, gpointer user_data){
Image2rtsp *node = static_cast<Image2rtsp*>(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);