diff --git a/.github/workflows/ubuntu-build.yml b/.github/workflows/ubuntu-build.yml
index 37a97780..a29323d2 100644
--- a/.github/workflows/ubuntu-build.yml
+++ b/.github/workflows/ubuntu-build.yml
@@ -34,7 +34,15 @@ jobs:
sudo wget https://packages.osrfoundation.org/gazebo.gpg -O /usr/share/keyrings/pkgs-osrf-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/pkgs-osrf-archive-keyring.gpg] http://packages.osrfoundation.org/gazebo/ubuntu-stable $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/gazebo-stable.list > /dev/null
sudo apt update && sudo apt install --no-install-recommends -y \
- rapidjson-dev gz-harmonic
+ rapidjson-dev \
+ libopencv-dev \
+ libunwind-dev \
+ libgstreamer1.0-dev \
+ libgstreamer-plugins-base1.0-dev \
+ gstreamer1.0-plugins-bad \
+ gstreamer1.0-libav \
+ gstreamer1.0-gl \
+ gz-harmonic
# Put ccache into github cache for faster build
- name: Prepare ccache timestamp
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0278b07c..3ec097df 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -52,8 +52,11 @@ else()
endif()
# --------------------------------------------------------------------------- #
-# Find RapidJSON.
find_package(RapidJSON REQUIRED)
+find_package(OpenCV REQUIRED)
+
+pkg_check_modules(GST REQUIRED gstreamer-1.0 gstreamer-app-1.0)
+
# --------------------------------------------------------------------------- #
# Build plugin.
@@ -95,6 +98,21 @@ target_link_libraries(CameraZoomPlugin PRIVATE
gz-sim${GZ_SIM_VER}::gz-sim${GZ_SIM_VER}
)
+add_library(GstCameraPlugin
+ SHARED
+ src/GstCameraPlugin.cc
+)
+target_include_directories(GstCameraPlugin PRIVATE
+ include
+ ${OpenCV_INCLUDE_DIRS}
+ ${GST_INCLUDE_DIRS}
+)
+target_link_libraries(GstCameraPlugin PRIVATE
+ gz-sim${GZ_SIM_VER}::gz-sim${GZ_SIM_VER}
+ ${OpenCV_LIBS}
+ ${GST_LINK_LIBRARIES}
+)
+
# --------------------------------------------------------------------------- #
# Install.
@@ -103,6 +121,7 @@ install(
ArduPilotPlugin
ParachutePlugin
CameraZoomPlugin
+ GstCameraPlugin
DESTINATION lib/${PROJECT_NAME}
)
diff --git a/README.md b/README.md
index db0649b1..cedda898 100644
--- a/README.md
+++ b/README.md
@@ -51,6 +51,7 @@ Manual - Gazebo Garden Dependencies:
```bash
sudo apt update
sudo apt install libgz-sim7-dev rapidjson-dev
+sudo apt install libopencv-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev gstreamer1.0-plugins-bad gstreamer1.0-libav gstreamer1.0-gl
```
#### Harmonic (apt)
@@ -60,6 +61,7 @@ Manual - Gazebo Harmonic Dependencies:
```bash
sudo apt update
sudo apt install libgz-sim8-dev rapidjson-dev
+sudo apt install libopencv-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev gstreamer1.0-plugins-bad gstreamer1.0-libav gstreamer1.0-gl
```
#### Rosdep
@@ -82,6 +84,7 @@ rosdep install --from-paths src --ignore-src -y
```bash
brew update
brew install rapidjson
+brew install opencv gstreamer
```
Ensure the `GZ_VERSION` environment variable is set to either
@@ -197,6 +200,38 @@ greater than one:
MANUAL> param set SIM_SPEEDUP 10
```
+### 3. Streaming camera video
+
+Images from camera sensors may be streamed with GStreamer using
+the `GstCameraPlugin` sensor plugin. The example gimbal models include the
+plugin element:
+
+```xml
+
+ 127.0.0.1
+ 5600
+ true
+ false
+
+```
+
+The `` and `` parameters are deduced from the
+topic name for the camera sensor, but may be overriden if required.
+
+The `gimbal.sdf` world includes a 3 degrees of freedom gimbal with a
+zoomable camera. To start streaming:
+
+```bash
+gz topic -t /world/gimbal/model/mount/model/gimbal/link/pitch_link/sensor/camera/image/enable_streaming -m gz.msgs.Boolean -p "data: 1"
+```
+
+Display the streamed video:
+
+```bash
+gst-launch-1.0 -v udpsrc port=5600 caps='application/x-rtp, media=(string)video, clock-rate=(int)90000, encoding-name=(string)H264' ! rtph264depay ! avdec_h264 ! videoconvert ! autovideosink sync=false
+```
+
## Models
In addition to the Iris and Zephyr models included here, a selection
diff --git a/include/GstCameraPlugin.hh b/include/GstCameraPlugin.hh
new file mode 100644
index 00000000..a9875a94
--- /dev/null
+++ b/include/GstCameraPlugin.hh
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2024 ArduPilot
+ */
+
+/*
+ * Copyright (C) 2012-2016 Open Source Robotics Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+#ifndef GSTCAMERAPLUGIN_HH_
+#define GSTCAMERAPLUGIN_HH_
+
+#include
+
+#include
+
+namespace gz {
+namespace sim {
+inline namespace GZ_SIM_VERSION_NAMESPACE {
+namespace systems {
+
+/// \brief Plugin to stream camera sensor data using GStreamer.
+/// \class GstCameraPlugin
+///
+/// A Gazebo plugin that can be attached to a camera and then streams the
+/// video data using gstreamer.
+///
+/// Parameters
+/// the UDP host IP, defaults to 127.0.0.1
+/// the UDP port, defaults to 5600
+/// the RTMP location
+/// set to true if not using
+/// set to true to use CUDA (if available)
+/// the camera image topic
+/// the topic to enable / disable video streaming
+///
+/// Start streaming
+/// assumes: /camera/enable_streaming
+///
+/// gz topic -t "/camera/enable_streaming" -m gz.msgs.Boolean -p "data: 1"
+///
+/// Connect to the stream via command line and open an OpenGL window:
+///
+/// gst-launch-1.0 -v udpsrc port=5600 caps='application/x-rtp,
+/// media=(string)video, clock-rate=(int)90000,
+/// encoding-name=(string)H264'
+/// ! rtph264depay ! avdec_h264 ! videoconvert
+/// ! autovideosink sync=false
+///
+class GstCameraPlugin :
+ public System,
+ public ISystemConfigure,
+ public ISystemPreUpdate
+{
+ /// \brief Destructor
+ public: virtual ~GstCameraPlugin();
+
+ /// \brief Constructor
+ public: GstCameraPlugin();
+
+ // Documentation inherited
+ public: void PreUpdate(const gz::sim::UpdateInfo &_info,
+ gz::sim::EntityComponentManager &_ecm) final;
+
+ // Documentation inherited
+ public: void Configure(const Entity &_entity,
+ const std::shared_ptr &_sdf,
+ EntityComponentManager &_ecm,
+ EventManager &) final;
+
+ /// \internal
+ /// \brief Private implementation
+ private: class Impl;
+ private: std::unique_ptr impl;
+};
+
+} // namespace systems
+}
+} // namespace sim
+} // namespace gz
+
+#endif // GSTCAMERAPLUGIN_HH_
diff --git a/models/gimbal_small_1d/model.sdf b/models/gimbal_small_1d/model.sdf
index adbe05b9..f449fae6 100644
--- a/models/gimbal_small_1d/model.sdf
+++ b/models/gimbal_small_1d/model.sdf
@@ -123,6 +123,15 @@
1
10
1
+
+
+ 127.0.0.1
+ 5600
+ true
+ false
+
+
diff --git a/models/gimbal_small_2d/model.sdf b/models/gimbal_small_2d/model.sdf
index 9a559b96..cddceb05 100644
--- a/models/gimbal_small_2d/model.sdf
+++ b/models/gimbal_small_2d/model.sdf
@@ -159,6 +159,15 @@
1
10
1
+
+
+ 127.0.0.1
+ 5600
+ true
+ false
+
+
diff --git a/models/gimbal_small_3d/model.sdf b/models/gimbal_small_3d/model.sdf
index 19b7805c..d265f7ea 100644
--- a/models/gimbal_small_3d/model.sdf
+++ b/models/gimbal_small_3d/model.sdf
@@ -210,6 +210,14 @@
125.0
0.42514285714
+
+
+ 127.0.0.1
+ 5600
+ true
+ false
+
diff --git a/src/GstCameraPlugin.cc b/src/GstCameraPlugin.cc
new file mode 100644
index 00000000..7c7f9842
--- /dev/null
+++ b/src/GstCameraPlugin.cc
@@ -0,0 +1,599 @@
+/*
+ * Copyright (C) 2012-2016 Open Source Robotics Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+#include "GstCameraPlugin.hh"
+
+#include
+#include
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+namespace gz {
+namespace sim {
+inline namespace GZ_SIM_VERSION_NAMESPACE {
+namespace systems {
+
+//////////////////////////////////////////////////
+
+class GstCameraPlugin::Impl {
+ public:
+ void InitializeCamera();
+ void StartStreaming();
+ static void *StartThread(void *);
+ void StartGstThread();
+
+ void OnImage(const msgs::Image &msg);
+ void OnVideoStreamEnable(const msgs::Boolean &_msg);
+ void OnRenderTeardown();
+
+ void StopStreaming();
+ void StopGstThread();
+
+ std::string udpHost{"127.0.0.1"};
+ int udpPort{5600};
+ bool useRtmpPipeline{false};
+ std::string rtmpLocation;
+ bool useBasicPipeline{false};
+ bool useCuda{false};
+ std::string imageTopic;
+ std::string enableTopic;
+
+ unsigned int width{0};
+ unsigned int height{0};
+
+ // Unused by actual pipeline since it's based on the gazebo topic rate?
+ unsigned int rate{5};
+
+ pthread_t threadId;
+ bool isGstMainLoopActive{false};
+ bool requestedStartStreaming{false};
+
+ GMainLoop *gst_loop{nullptr};
+ GstElement *source{nullptr};
+ void CreateMpeg2tsPipeline(GstElement *pipeline);
+ void CreateRtmpPipeline(GstElement *pipeline);
+ void CreateGenericPipeline(GstElement *pipeline);
+ GstElement *CreateEncoder();
+
+ bool is_initialised{false};
+ Sensor parentSensor;
+ rendering::ScenePtr scene;
+ rendering::CameraPtr camera;
+ std::string cameraName;
+ std::vector connections;
+ transport::Node node;
+};
+
+//////////////////////////////////////////////////
+GstCameraPlugin::GstCameraPlugin()
+ : impl(std::make_unique())
+{
+}
+
+GstCameraPlugin::~GstCameraPlugin()
+{
+ impl->OnRenderTeardown();
+}
+
+void GstCameraPlugin::Configure(
+ const Entity &_entity,
+ const std::shared_ptr &_sdf,
+ EntityComponentManager &_ecm,
+ EventManager &_eventMgr)
+{
+ impl->parentSensor = Sensor(_entity);
+
+ if (!impl->parentSensor.Valid(_ecm))
+ {
+ gzerr << "GstCameraPlugin: must be attached to a camera sensor. "
+ "Failed to initialize" << std::endl;
+ return;
+ }
+
+ if (auto maybeName = impl->parentSensor.Name(_ecm))
+ {
+ gzmsg << "GstCameraPlugin: attached to sensor ["
+ << maybeName.value() << "]" << std::endl;
+ }
+ else
+ {
+ gzerr << "GstCameraPlugin: camera sensor has invalid name. "
+ "Failed to initialize" << std::endl;
+ return;
+ }
+
+ if (_sdf->HasElement("udp_host"))
+ {
+ impl->udpHost = _sdf->Get("udp_host");
+ }
+
+ if (_sdf->HasElement("udp_port"))
+ {
+ impl->udpPort = _sdf->Get("udp_port");
+ }
+ gzmsg << "GstCameraPlugin: streaming video to "
+ << impl->udpHost << ":"
+ << impl->udpPort << std::endl;
+
+ // uses MPEG2TS pipeline by default. RTMP and Generic are
+ // mutually exclusive with priority to RTMP
+ if (_sdf->HasElement("rtmp_location"))
+ {
+ impl->rtmpLocation = _sdf->Get("rtmp_location");
+ impl->useRtmpPipeline = true;
+
+ }
+ else if (_sdf->HasElement("use_basic_pipeline"))
+ {
+ impl->useBasicPipeline = _sdf->Get("use_basic_pipeline");
+ }
+
+ // Use CUDA for video encoding
+ if (_sdf->HasElement("use_cuda"))
+ {
+ impl->useCuda = _sdf->Get("use_cuda");
+ }
+
+ if (_sdf->HasElement("image_topic"))
+ {
+ impl->imageTopic = _sdf->Get("image_topic");
+ }
+
+ if (_sdf->HasElement("enable_topic"))
+ {
+ impl->enableTopic = _sdf->Get("enable_topic");
+ }
+
+ //! @note subscriptions are deferred to Pre-Update as the enclosing
+ // sensor must be fully initialised before entity - component queries
+ // for topics names etc. to succeed.
+
+ // subscribe to events
+ impl->connections.push_back(
+ _eventMgr.Connect(
+ std::bind(&GstCameraPlugin::Impl::OnRenderTeardown, impl.get())));
+}
+
+void GstCameraPlugin::PreUpdate(const UpdateInfo &_info,
+ EntityComponentManager &_ecm)
+{
+ if (impl->cameraName.empty())
+ {
+ Entity cameraEntity = impl->parentSensor.Entity();
+ impl->cameraName = removeParentScope(
+ scopedName(cameraEntity, _ecm, "::", false), "::");
+ gzmsg << "GstCameraPlugin: camera name ["
+ << impl->cameraName << "]" << std::endl;
+ }
+
+ // complete initialisation deferred from Configure()
+ if (!impl->is_initialised)
+ {
+ if (impl->imageTopic.empty())
+ {
+ auto maybeTopic = impl->parentSensor.Topic(_ecm);
+ if (!maybeTopic.has_value())
+ {
+ return;
+ }
+ impl->imageTopic = maybeTopic.value();
+ }
+
+ if (impl->enableTopic.empty())
+ {
+ auto maybeTopic = impl->parentSensor.Topic(_ecm);
+ if (!maybeTopic.has_value())
+ {
+ return;
+ }
+ impl->enableTopic = maybeTopic.value() + "/enable_streaming";
+ }
+ gzmsg << "GstCameraPlugin: image topic ["
+ << impl->imageTopic << "]" << std::endl;
+ gzmsg << "GstCameraPlugin: enable topic ["
+ << impl->enableTopic << "]" << std::endl;
+
+ // subscribe to gazebo topics
+ impl->node.Subscribe(impl->imageTopic,
+ &GstCameraPlugin::Impl::OnImage, impl.get());
+ impl->node.Subscribe(impl->enableTopic,
+ &GstCameraPlugin::Impl::OnVideoStreamEnable, impl.get());
+
+ impl->is_initialised = true;
+ }
+
+ if (!impl->camera && !impl->cameraName.empty())
+ {
+ impl->InitializeCamera();
+ return;
+ }
+}
+
+void GstCameraPlugin::Impl::InitializeCamera()
+{
+ // Wait for render engine to be available.
+ if (rendering::loadedEngines().empty())
+ {
+ return;
+ }
+
+ // Get scene.
+ if (!scene)
+ {
+ scene = rendering::sceneFromFirstRenderEngine();
+ }
+
+ // Return if scene not ready or no sensors available.
+ if (scene == nullptr || !scene->IsInitialized()
+ || scene->SensorCount() == 0)
+ {
+ gzwarn << "GstCameraPlugin: no scene or camera sensors available"
+ << std::endl;
+ return;
+ }
+
+ // Get camera.
+ if (!camera)
+ {
+ auto sensor = scene->SensorByName(cameraName);
+ if (!sensor)
+ {
+ gzerr << "GstCameraPlugin: unable to find sensor ["
+ << cameraName << "]" << std::endl;
+ return;
+ }
+
+ camera = std::dynamic_pointer_cast(sensor);
+ if (!camera)
+ {
+ gzerr << "GstCameraPlugin: sensor ["
+ << cameraName << "] is not a camera" << std::endl;
+ return;
+ }
+ }
+}
+
+void GstCameraPlugin::Impl::StartStreaming()
+{
+ if (!isGstMainLoopActive)
+ {
+ pthread_create(&threadId, NULL, StartThread, this);
+ }
+}
+
+void *GstCameraPlugin::Impl::StartThread(void *param)
+{
+ GstCameraPlugin::Impl *impl = (GstCameraPlugin::Impl *)param;
+ impl->StartGstThread();
+ return nullptr;
+}
+
+void GstCameraPlugin::Impl::StartGstThread()
+{
+ gst_init(nullptr, nullptr);
+
+ gst_loop = g_main_loop_new(nullptr, FALSE);
+ if (!gst_loop)
+ {
+ gzerr << "GstCameraPlugin: failed to create GStreamer main loop"
+ << std::endl;
+ return;
+ }
+
+ GstElement *pipeline = gst_pipeline_new(nullptr);
+ if (!pipeline)
+ {
+ gzerr << "GstCameraPlugin: GStreamer pipeline failed" << std::endl;
+ return;
+ }
+
+ source = gst_element_factory_make("appsrc", nullptr);
+ if (useRtmpPipeline)
+ {
+ CreateRtmpPipeline(pipeline);
+ }
+ else if (useBasicPipeline)
+ {
+ CreateGenericPipeline(pipeline);
+ }
+ else
+ {
+ CreateMpeg2tsPipeline(pipeline);
+ }
+
+ // Configure source element
+ g_object_set(G_OBJECT(source), "caps",
+ gst_caps_new_simple("video/x-raw",
+ "format", G_TYPE_STRING, "I420",
+ "width", G_TYPE_INT, width,
+ "height", G_TYPE_INT, height,
+ "framerate", GST_TYPE_FRACTION,
+ this->rate, 1, nullptr),
+ "is-live", TRUE,
+ "do-timestamp", TRUE,
+ "stream-type", GST_APP_STREAM_TYPE_STREAM,
+ "format", GST_FORMAT_TIME, nullptr);
+
+ gst_object_ref(source);
+
+ // Start
+ auto ret = gst_element_set_state(pipeline, GST_STATE_PLAYING);
+ if (ret != GST_STATE_CHANGE_SUCCESS)
+ {
+ gzmsg << "GstCameraPlugin: GStreamer element set state returned: "
+ << ret << std::endl;
+ }
+
+ // this call blocks until the main_loop is killed
+ gzmsg << "GstCameraPlugin: starting GStreamer main loop" << std::endl;
+ isGstMainLoopActive = true;
+ g_main_loop_run(gst_loop);
+ isGstMainLoopActive = false;
+ gzmsg << "GstCameraPlugin: stopping GStreamer main loop" << std::endl;
+
+ // Clean up
+ gst_element_set_state(pipeline, GST_STATE_NULL);
+ gst_object_unref(GST_OBJECT(pipeline));
+ gst_object_unref(source);
+ g_main_loop_unref(gst_loop);
+ gst_loop = nullptr;
+ source = nullptr;
+}
+
+void GstCameraPlugin::Impl::CreateRtmpPipeline(GstElement *pipeline)
+{
+ gzdbg << "GstCameraPlugin: creating RTMP pipeline" << std::endl;
+ GstElement *queue = gst_element_factory_make("queue", nullptr);
+ GstElement *converter = gst_element_factory_make("videoconvert", nullptr);
+ GstElement *encoder = CreateEncoder();
+ GstElement *payloader = gst_element_factory_make("flvmux", nullptr);
+ GstElement *sink = gst_element_factory_make("rtmpsink", nullptr);
+
+ g_object_set(G_OBJECT(sink), "location", rtmpLocation.c_str(), nullptr);
+
+ if (!source || !queue || !converter || !encoder || !payloader || !sink)
+ {
+ gzerr << "GstCameraPlugin: failed to create GStreamer elements"
+ << std::endl;
+ return;
+ }
+
+ // Connect all elements to pipeline
+ gst_bin_add_many(GST_BIN(pipeline), source, queue, converter, encoder,
+ payloader, sink, nullptr);
+
+ // Link all elements
+ if (gst_element_link_many(source, queue, converter, encoder,
+ payloader, sink, nullptr) != TRUE)
+ {
+ gzerr << "GstCameraPlugin: failed to link GStreamer elements"
+ << std::endl;
+ return;
+ }
+}
+
+void GstCameraPlugin::Impl::CreateGenericPipeline(GstElement *pipeline)
+{
+ gzdbg << "GstCameraPlugin: creating generic pipeline" << std::endl;
+ GstElement *queue = gst_element_factory_make("queue", nullptr);
+ GstElement *converter = gst_element_factory_make("videoconvert", nullptr);
+ GstElement *encoder = CreateEncoder();
+ GstElement *payloader = gst_element_factory_make("rtph264pay", nullptr);
+ GstElement *sink = gst_element_factory_make("udpsink", nullptr);
+
+ g_object_set(G_OBJECT(sink), "host", udpHost.c_str(),
+ "port", udpPort, nullptr);
+
+ if (!source || !queue || !converter || !encoder || !payloader || !sink)
+ {
+ gzerr << "GstCameraPlugin: failed to create GStreamer elements"
+ << std::endl;
+ return;
+ }
+
+ // Connect all elements to pipeline
+ gst_bin_add_many(GST_BIN(pipeline), source, queue, converter, encoder,
+ payloader, sink, nullptr);
+
+ // Link all elements
+ if (gst_element_link_many(source, queue, converter, encoder,
+ payloader, sink, nullptr) != TRUE)
+ {
+ gzerr << "GstCameraPlugin: failed to link GStreamer elements"
+ << std::endl;
+ return;
+ }
+}
+
+void GstCameraPlugin::Impl::CreateMpeg2tsPipeline(GstElement *pipeline)
+{
+ gzdbg << "GstCameraPlugin: creating MPEG2TS pipeline" << std::endl;
+ GstElement *queue = gst_element_factory_make("queue", nullptr);
+ GstElement *converter = gst_element_factory_make("videoconvert", nullptr);
+ GstElement *encoder = CreateEncoder();
+ GstElement *h264_parser = gst_element_factory_make("h264parse", nullptr);
+ GstElement *payloader = gst_element_factory_make("mpegtsmux", nullptr);
+ GstElement *queue_mpeg = gst_element_factory_make("queue", nullptr);
+ GstElement *sink = gst_element_factory_make("udpsink", nullptr);
+
+ g_object_set(G_OBJECT(payloader), "alignment", 7, nullptr);
+ g_object_set(G_OBJECT(sink), "host", udpHost.c_str(), "port", udpPort,
+ "sync", false, nullptr);
+
+ if (!source || !queue || !converter || !encoder || !h264_parser
+ || !payloader || !queue_mpeg || !sink)
+ {
+ gzerr << "GstCameraPlugin: failed to create GStreamer elements"
+ << std::endl;
+ return;
+ }
+
+ gst_bin_add_many(GST_BIN(pipeline), source, queue, converter, encoder,
+ h264_parser, payloader, queue_mpeg, sink, nullptr);
+ if (gst_element_link_many(source, queue, converter, encoder,
+ h264_parser, payloader, queue_mpeg, sink, nullptr) != TRUE)
+ {
+ gzerr << "GstCameraPlugin: failed to link GStreamer elements"
+ << std::endl;
+ return;
+ }
+}
+
+GstElement* GstCameraPlugin::Impl::CreateEncoder()
+{
+ GstElement* encoder{nullptr};
+ if (useCuda)
+ {
+ gzdbg << "Using Cuda" << std::endl;
+ encoder = gst_element_factory_make("nvh264enc", nullptr);
+ g_object_set(G_OBJECT(encoder), "bitrate", 800, "preset", 1, nullptr);
+ }
+ else
+ {
+ encoder = gst_element_factory_make("x264enc", nullptr);
+ g_object_set(G_OBJECT(encoder), "bitrate", 800, "speed-preset", 6,
+ "tune", 4, "key-int-max", 10, nullptr);
+ }
+ return encoder;
+}
+
+void GstCameraPlugin::Impl::OnImage(const msgs::Image &msg)
+{
+ if (requestedStartStreaming)
+ {
+ width = msg.width();
+ height = msg.height();
+ StartStreaming();
+ requestedStartStreaming = false;
+ return;
+ }
+
+ if (!isGstMainLoopActive) return;
+
+ // Alloc buffer
+ const guint size = width * height * 1.5;
+ GstBuffer *buffer = gst_buffer_new_allocate(NULL, size, NULL);
+
+ if (!buffer)
+ {
+ gzerr << "GstCameraPlugin: gst_buffer_new_allocate failed"
+ << std::endl;
+ return;
+ }
+
+ GstMapInfo map;
+
+ if (!gst_buffer_map(buffer, &map, GST_MAP_WRITE))
+ {
+ gzerr << "GstCameraPlugin: gst_buffer_map failed" << std::endl;
+ return;
+ }
+
+ // Color Conversion from RGB to YUV
+ cv::Mat frame = cv::Mat(height, width, CV_8UC3);
+ cv::Mat frameYUV = cv::Mat(height, width, CV_8UC3);
+ frame.data = reinterpret_cast(
+ const_cast(msg.data().c_str()));
+
+ cvtColor(frame, frameYUV, cv::COLOR_RGB2YUV_I420);
+ memcpy(map.data, frameYUV.data, size);
+ gst_buffer_unmap(buffer, &map);
+
+ GstFlowReturn ret =
+ gst_app_src_push_buffer(GST_APP_SRC(this->source), buffer);
+ if (ret != GST_FLOW_OK)
+ {
+ // Something wrong, stop pushing
+ gzerr << "GstCameraPlugin: gst_app_src_push_buffer failed"
+ << std::endl;
+ g_main_loop_quit(gst_loop);
+ }
+}
+
+void GstCameraPlugin::Impl::OnVideoStreamEnable(const msgs::Boolean &msg)
+{
+ gzmsg << "GstCameraPlugin:: streaming: "
+ << (msg.data() ? "started" : "stopped") << std::endl;
+ if (msg.data())
+ {
+ requestedStartStreaming = true;
+ }
+ else
+ {
+ requestedStartStreaming = false;
+ StopStreaming();
+ }
+}
+
+void GstCameraPlugin::Impl::OnRenderTeardown()
+{
+ StopStreaming();
+ camera.reset();
+ scene.reset();
+}
+
+void GstCameraPlugin::Impl::StopStreaming()
+{
+ if (isGstMainLoopActive)
+ {
+ StopGstThread();
+
+ pthread_join(threadId, NULL);
+ isGstMainLoopActive = false;
+ }
+}
+
+void GstCameraPlugin::Impl::StopGstThread()
+{
+ if (gst_loop)
+ {
+ g_main_loop_quit(gst_loop);
+ }
+}
+
+//////////////////////////////////////////////////
+
+} // namespace systems
+} // namespace GZ_SIM_VERSION_NAMESPACE
+} // namespace sim
+} // namespace gz
+
+GZ_ADD_PLUGIN(
+ gz::sim::systems::GstCameraPlugin,
+ gz::sim::System,
+ gz::sim::systems::GstCameraPlugin::ISystemConfigure,
+ gz::sim::systems::GstCameraPlugin::ISystemPreUpdate)
+
+GZ_ADD_PLUGIN_ALIAS(
+ gz::sim::systems::GstCameraPlugin,
+ "GstCameraPlugin")