#!/usr/bin/env python3 """ Minimal ROS2 camera publisher using GStreamer v4l2src. Publishes sensor_msgs/Image to //image_raw. No OpenCV or cv_bridge required. """ import os import threading import gi gi.require_version('Gst', '1.0') from gi.repository import Gst, GLib import rclpy from rclpy.executors import SingleThreadedExecutor from rclpy.node import Node from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy from sensor_msgs.msg import Image DEVICE = os.environ.get('VIDEO_DEVICE', '/dev/video0') WIDTH = int(os.environ.get('WIDTH', '640')) HEIGHT = int(os.environ.get('HEIGHT', '480')) FPS = int(os.environ.get('FPS', '30')) FORMAT = os.environ.get('CAMERA_FORMAT', 'raw').lower() NAMESPACE = os.environ.get('CAMERA_NAMESPACE', '/camera') TOPIC = f'{NAMESPACE}/image_raw' class CameraPublisher(Node): def __init__(self): # DDS must initialise before GStreamer — same constraint as webrtc_streamer. super().__init__('camera_publisher') qos = QoSProfile( # reliability=ReliabilityPolicy.BEST_EFFORT, history=HistoryPolicy.KEEP_LAST, depth=1, ) self.pub = self.create_publisher(Image, TOPIC, qos) self.get_logger().info(f'Publishing to {TOPIC}') Gst.init(None) glib_loop = GLib.MainLoop() threading.Thread(target=glib_loop.run, daemon=True).start() if FORMAT == 'mjpeg': src = ( f'v4l2src device={DEVICE} ! ' f'image/jpeg,width={WIDTH},height={HEIGHT},framerate={FPS}/1 ! ' 'jpegdec ! ' ) else: src = ( f'v4l2src device={DEVICE} ! ' f'video/x-raw,width={WIDTH},height={HEIGHT},framerate={FPS}/1 ! ' ) desc = src + 'videoconvert ! video/x-raw,format=BGR ! appsink name=sink emit-signals=true sync=false max-buffers=1 drop=true' self.get_logger().info(f'Pipeline: {desc}') self._pipeline = Gst.parse_launch(desc) sink = self._pipeline.get_by_name('sink') sink.connect('new-sample', self._on_frame) self._bus = self._pipeline.get_bus() self._bus.add_signal_watch() self._bus.connect('message::error', self._on_error) self._bus.connect('message::warning', self._on_warning) self._pipeline.set_state(Gst.State.PLAYING) self._frame_count = 0 def _on_error(self, bus, msg): err, debug = msg.parse_error() self.get_logger().error(f'GStreamer error: {err.message}') if debug: self.get_logger().error(f'GStreamer debug: {debug}') def _on_warning(self, bus, msg): self.get_logger().warning(f'GStreamer warning: {msg.parse_warning()[0].message}') def _on_frame(self, sink): sample = sink.emit('pull-sample') buf = sample.get_buffer() caps = sample.get_caps().get_structure(0) w = caps.get_value('width') h = caps.get_value('height') msg = Image() msg.header.stamp = self.get_clock().now().to_msg() msg.header.frame_id = 'camera' msg.height = h msg.width = w msg.encoding = 'bgr8' msg.step = w * 3 msg.is_bigendian = False msg.data = buf.extract_dup(0, buf.get_size()) self.pub.publish(msg) self._frame_count += 1 if self._frame_count % 100 == 0: self.get_logger().info(f'Frames published: {self._frame_count}') return Gst.FlowReturn.OK def main(): rclpy.init() node = CameraPublisher() executor = SingleThreadedExecutor() executor.add_node(node) try: executor.spin() finally: node._pipeline.set_state(Gst.State.NULL) node.destroy_node() rclpy.shutdown() if __name__ == '__main__': main()