Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/code-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ on:
jobs:
code-coverage:
uses: vortexntnu/vortex-ci/.github/workflows/reusable-code-coverage.yml@main
continue-on-error: true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💀

secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} # Set in the repository secrets
14 changes: 3 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
Repository to move image to gstreamer
```

# Setup
# Dependencies
```
sudo apt install libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
```

## launch variables
Expand All @@ -14,8 +15,7 @@ ros2 launch image_to_gstreamer image_to_gstreamer.launch.py host:=127.0.0.1 port
```
## terminal launch for gstreamer
```
gst-launch-1.0 udpsrc port=5001 caps="application/x-rtp,media=video,encoding-name=H265,payload=96"
! rtph265depay ! avdec_h265 ! videoconvert ! fpsdisplaysink
gst-launch-1.0 udpsrc port=5001 caps="application/x-rtp,media=video,encoding-name=H265,payload=96" ! rtph265depay ! avdec_h265 ! videoconvert ! fpsdisplaysink
```

## Using pre-commit
Expand Down Expand Up @@ -47,11 +47,3 @@ pre-commit autoupdate
- This project uses GitHub Actions for CI.
- Workflows are located in [`.github/workflows`](.github/workflows/).
- For more information, see [vortex-ci](https://github.com/vortexntnu/vortex-ci).








53 changes: 53 additions & 0 deletions gstreamer_from_ros/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
cmake_minimum_required(VERSION 3.8)
project(gstreamer_from_ros)

find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(rclcpp_components REQUIRED)
find_package(sensor_msgs REQUIRED)

find_package(PkgConfig REQUIRED)
pkg_check_modules(GST REQUIRED
gstreamer-1.0
gstreamer-app-1.0
)

add_library(gstreamer_from_ros_component SHARED
src/gstreamer_from_ros_node.cpp
)

target_include_directories(gstreamer_from_ros_component PUBLIC
include
${GST_INCLUDE_DIRS}
)

target_link_libraries(gstreamer_from_ros_component
${GST_LIBRARIES}
)

ament_target_dependencies(gstreamer_from_ros_component
rclcpp
rclcpp_components
sensor_msgs
)

rclcpp_components_register_node(gstreamer_from_ros_component
PLUGIN "gstreamer_from_ros::GStreamerFromRos"
EXECUTABLE gstreamer_from_ros_node
)

install(TARGETS gstreamer_from_ros_component
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION lib/${PROJECT_NAME}
)

install(DIRECTORY include/
DESTINATION include/
)

install(DIRECTORY config launch
DESTINATION share/${PROJECT_NAME}
)

ament_package()
14 changes: 14 additions & 0 deletions gstreamer_from_ros/config/gstreamer_from_ros.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**:
ros__parameters:
input_topic: "/blackfly_s/image_raw"
destination_ip: "127.0.0.1"
destination_port: 5001

expected_input_fps: 15
bitrate: 500000
preset_level: 1
iframe_interval: 15
control_rate: 1
pt: 96
config_interval: 1
input_format: "RGB"
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#ifndef GSTREAMER_FROM_ROS__GSTREAMER_FROM_ROS_HPP_
#define GSTREAMER_FROM_ROS__GSTREAMER_FROM_ROS_HPP_

#include <string>

#include <rclcpp/rclcpp.hpp>
#include <sensor_msgs/msg/image.hpp>

#include <gst/app/gstappsrc.h>
#include <gst/gst.h>

namespace gstreamer_from_ros {

class GStreamerFromRos : public rclcpp::Node {
public:
explicit GStreamerFromRos(const rclcpp::NodeOptions& options);
~GStreamerFromRos();

private:
void create_pipeline();
void imageCb(const sensor_msgs::msg::Image::SharedPtr msg);

rclcpp::Subscription<sensor_msgs::msg::Image>::SharedPtr sub_;
rclcpp::TimerBase::SharedPtr timer_;
int bitrate_;
int preset_level_;
int iframe_interval_;
int control_rate_;
int pt_;
int config_interval_;
int expected_input_fps_;
std::string input_format_;
bool hw_encoder_;

GstElement* pipeline_;
GstElement* appsrc_;

bool pipeline_started_;

std::string input_topic_;
std::string destination_ip_;
int destination_port_;
};

} // namespace gstreamer_from_ros

#endif // GSTREAMER_FROM_ROS__GSTREAMER_FROM_ROS_HPP_
52 changes: 52 additions & 0 deletions gstreamer_from_ros/launch/gstreamer_from_ros.launch.py

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should also be able to configure the input topic from here on runtime (we want to launch multiple instances of this with different input topic)

Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, OpaqueFunction
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import ComposableNodeContainer
from launch_ros.descriptions import ComposableNode


def _launch_setup(context, *args, **kwargs):
use_nvidia = LaunchConfiguration('gst_nvidia_encoder').perform(context).lower() == 'true'

container = ComposableNodeContainer(
name='gstreamer_from_ros_container',
namespace='',
package='rclcpp_components',
executable='component_container_mt',
composable_node_descriptions=[
ComposableNode(
package='gstreamer_from_ros',
plugin='gstreamer_from_ros::GStreamerFromRos',
name='gstreamer_from_ros_node',
parameters=[{
'input_topic': '/zed_node/left/image_rect_color',
'destination_ip': '10.0.0.154',
'destination_port': 5001,
'expected_input_fps': 15,
'bitrate': 500000,
'preset_level': 1,
'iframe_interval': 15,
'control_rate': 1,
'pt': 96,
'config_interval': 1,
'input_format': 'RGB',
'hw_encoder': use_nvidia,
}],
),
],
output='screen',
)

return [container]


def generate_launch_description():
return LaunchDescription([
DeclareLaunchArgument(
'gst_nvidia_encoder',
default_value='true',
description='Use NVIDIA hardware H.265 encoder (nvv4l2h265enc). '
'Set false to use software x265enc.',
),
OpaqueFunction(function=_launch_setup),
])
28 changes: 28 additions & 0 deletions gstreamer_from_ros/package.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0"?>
<package format="3">
<name>gstreamer_from_ros</name>
<version>0.1.0</version>
<description>takes image form ROS node and sends it via gstreamer</description>

<maintainer email="gardeg@github.com">Gard</maintainer>
<license>MIT</license>

<buildtool_depend>ament_cmake</buildtool_depend>

<depend>rclcpp</depend>
<depend>rclcpp_components</depend>
<depend>sensor_msgs</depend>


<build_depend>pkg-config</build_depend>
<exec_depend>pkg-config</exec_depend>

<build_depend>libgstreamer1.0-dev</build_depend>
<build_depend>libgstreamer-plugins-base1.0-dev</build_depend>
<exec_depend>libgstreamer1.0-dev</exec_depend>
<exec_depend>libgstreamer-plugins-base1.0-dev</exec_depend>

<export>
<build_type>ament_cmake</build_type>
</export>
</package>
145 changes: 145 additions & 0 deletions gstreamer_from_ros/src/gstreamer_from_ros_node.cpp

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a fan of having default value for IP, Port and input topic here, can lead to unexpeceted behavior

Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
#include "gstreamer_from_ros/gstreamer_from_ros.hpp"

#include <rclcpp_components/register_node_macro.hpp>

namespace gstreamer_from_ros {

GStreamerFromRos::GStreamerFromRos(const rclcpp::NodeOptions& options)
: Node("gstreamer_from_ros_node", options),
pipeline_(nullptr),
appsrc_(nullptr),
pipeline_started_(false) {
gst_init(nullptr, nullptr);

input_topic_ = declare_parameter<std::string>("input_topic", "");
destination_ip_ = declare_parameter<std::string>("destination_ip", "");
destination_port_ = declare_parameter<int>("destination_port", 5000);
bitrate_ = declare_parameter<int>("bitrate", 500000);
preset_level_ = declare_parameter<int>("preset_level", 1);
iframe_interval_ = declare_parameter<int>("iframe_interval", 15);
control_rate_ = declare_parameter<int>("control_rate", 1);
pt_ = declare_parameter<int>("pt", 96);
config_interval_ = declare_parameter<int>("config_interval", 1);
expected_input_fps_ = declare_parameter<int>("expected_input_fps", 15);
input_format_ = declare_parameter<std::string>("input_format", "RGB");
hw_encoder_ = declare_parameter<bool>("hw_encoder", true);

auto qos = rclcpp::QoS(rclcpp::KeepLast(3)).best_effort().durability_volatile();

sub_ = create_subscription<sensor_msgs::msg::Image>(
input_topic_, qos,
std::bind(&GStreamerFromRos::imageCb, this, std::placeholders::_1));

timer_ = create_wall_timer(std::chrono::seconds(5), [this]() {
RCLCPP_INFO(get_logger(), "Waiting for images on topic: '%s'",
input_topic_.c_str());
});

create_pipeline();
}

GStreamerFromRos::~GStreamerFromRos() {
if (pipeline_) {
gst_element_set_state(pipeline_, GST_STATE_NULL);
gst_object_unref(pipeline_);
}
}

void GStreamerFromRos::create_pipeline() {
pipeline_ = gst_pipeline_new("ros2-h265-pipeline");
appsrc_ = gst_element_factory_make("appsrc", "source");
GstElement* convert = gst_element_factory_make("videoconvert", "convert");
GstElement* parser = gst_element_factory_make("h265parse", "parser");
GstElement* pay = gst_element_factory_make("rtph265pay", "pay");
GstElement* sink = gst_element_factory_make("udpsink", "sink");

GstElement* encoder = nullptr;
GstElement* nvconv = nullptr;

if (hw_encoder_) {
nvconv = gst_element_factory_make("nvvidconv", "nvconv");
encoder = gst_element_factory_make("nvv4l2h265enc", "encoder");
} else {
encoder = gst_element_factory_make("x265enc", "encoder");
}

bool elements_ok = appsrc_ && convert && encoder && parser && pay && sink && pipeline_;
if (hw_encoder_) elements_ok = elements_ok && nvconv;

if (!elements_ok) {
RCLCPP_FATAL(get_logger(), "Failed to create GStreamer elements");
return;
}

if (hw_encoder_) {
g_object_set(encoder, "bitrate", bitrate_, "preset-level", preset_level_,
"iframeinterval", iframe_interval_, "control-rate", control_rate_, NULL);
} else {
// x265enc bitrate is in kbits/sec; key-int-max is the I-frame interval
g_object_set(encoder, "bitrate", bitrate_ / 1000,
"key-int-max", iframe_interval_,
"speed-preset", 0, // ultrafast — minimise latency
NULL);
}

g_object_set(pay, "config-interval", config_interval_, "pt", pt_, NULL);
g_object_set(sink, "host", destination_ip_.c_str(), "port", destination_port_, "sync", FALSE, NULL);

if (hw_encoder_) {
gst_bin_add_many(GST_BIN(pipeline_), appsrc_, convert, nvconv, encoder,
parser, pay, sink, NULL);
if (!gst_element_link_many(appsrc_, convert, nvconv, encoder, parser, pay, sink, NULL)) {
RCLCPP_FATAL(get_logger(), "Pipeline linking failed");
return;
}
} else {
gst_bin_add_many(GST_BIN(pipeline_), appsrc_, convert, encoder, parser, pay, sink, NULL);
if (!gst_element_link_many(appsrc_, convert, encoder, parser, pay, sink, NULL)) {
RCLCPP_FATAL(get_logger(), "Pipeline linking failed");
return;
}
}

RCLCPP_INFO(get_logger(), "GStreamer H.265 pipeline created (%s encoder)",
hw_encoder_ ? "NVIDIA hw" : "x265 sw");
}

void GStreamerFromRos::imageCb(const sensor_msgs::msg::Image::SharedPtr msg) {
static size_t frame_count = 0;
frame_count++;

if (!pipeline_started_) {
GstCaps* caps = gst_caps_new_simple(
"video/x-raw", "format", G_TYPE_STRING, input_format_.c_str(),
"width", G_TYPE_INT, msg->width,
"height", G_TYPE_INT, msg->height,
"framerate", GST_TYPE_FRACTION, expected_input_fps_, 1, NULL);

g_object_set(appsrc_, "caps", caps, "format", GST_FORMAT_TIME,
"is-live", TRUE, "do-timestamp", TRUE, NULL);
gst_caps_unref(caps);

gst_element_set_state(pipeline_, GST_STATE_PLAYING);
pipeline_started_ = true;
timer_->cancel();

RCLCPP_INFO(get_logger(), "H.265 pipeline started (%s)",
hw_encoder_ ? "NVIDIA hw encoder" : "x265 sw encoder");
}

GstBuffer* buffer = gst_buffer_new_allocate(nullptr, msg->data.size(), nullptr);
gst_buffer_fill(buffer, 0, msg->data.data(), msg->data.size());

GstFlowReturn ret;
g_signal_emit_by_name(appsrc_, "push-buffer", buffer, &ret);
gst_buffer_unref(buffer);

if (ret != GST_FLOW_OK)
RCLCPP_WARN(get_logger(), "Failed to push buffer");
else
RCLCPP_DEBUG(get_logger(), "Pushed frame #%zu into GStreamer", frame_count);
}

} // namespace gstreamer_from_ros

RCLCPP_COMPONENTS_REGISTER_NODE(gstreamer_from_ros::GStreamerFromRos)
Loading
Loading