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