Program Listing for File gstreamer_pipeline.cpp

Program Listing for File gstreamer_pipeline.cpp#

↰ Return to documentation for file (Src/gstreamer_pipeline.cpp)

module;

#include "kataglyphis_export.h"
#include <atomic>
#include <cstring>
#include <expected>
#include <gst/analytics/analytics.h>
#include <gst/analytics/gsttensor.h>
#include <gst/analytics/gsttensormeta.h>
#include <gst/app/gstappsink.h>
#include <gst/app/gstappsrc.h>
#include <gst/gst.h>
#include <gst/video/video.h>
#include <mutex>
#include <string>
#include <thread>
#include <vector>

module kataglyphis.gstreamer_pipeline;

import kataglyphis.project_config;

namespace kataglyphis::gstreamer {

namespace {
    std::atomic<bool> g_gstreamer_initialized{ false };
    std::mutex g_gstreamer_mutex;

    auto parse_pipeline_description(const std::string &pipeline_description, const char *failure_context)
      -> std::expected<GstElement *, GStreamerError>
    {
        GError *error = nullptr;
        GstElement *pipeline = gst_parse_launch(pipeline_description.c_str(), &error);
        // gst_parse_launch hands out a floating reference; sinking it makes the later unref the matching release.
        if (pipeline != nullptr) { gst_object_ref_sink(pipeline); }

        if (error != nullptr) {
            g_printerr("%s: %s\n", failure_context, error->message);
            g_error_free(error);
            if (pipeline != nullptr) { gst_object_unref(pipeline); }
            return std::unexpected(GStreamerError::PipelineCreationFailed);
        }

        if (pipeline == nullptr) {
            g_printerr("%s returned null\n", failure_context);
            return std::unexpected(GStreamerError::PipelineCreationFailed);
        }

        return pipeline;
    }

    // The first element of the given type in the pipeline (or the pipeline itself), with a reference the caller owns.
    auto find_element_of_type(GstElement *root, GType type) -> GstElement *
    {
        if (G_TYPE_CHECK_INSTANCE_TYPE(root, type)) { return GST_ELEMENT(gst_object_ref(root)); }
        if (!GST_IS_BIN(root)) { return nullptr; }

        GstElement *found = nullptr;
        GstIterator *iterator = gst_bin_iterate_recurse(GST_BIN(root));
        GValue item = G_VALUE_INIT;
        bool done = false;
        while (!done) {
            switch (gst_iterator_next(iterator, &item)) {
            case GST_ITERATOR_OK: {
                auto *element = GST_ELEMENT(g_value_get_object(&item));
                if (found == nullptr && G_TYPE_CHECK_INSTANCE_TYPE(element, type)) {
                    found = GST_ELEMENT(gst_object_ref(element));
                }
                g_value_reset(&item);
                break;
            }
            case GST_ITERATOR_RESYNC:
                if (found != nullptr) {
                    gst_object_unref(found);
                    found = nullptr;
                }
                gst_iterator_resync(iterator);
                break;
            default:
                done = true;
                break;
            }
        }
        g_value_unset(&item);
        gst_iterator_free(iterator);
        return found;
    }

    void append_tensor_metadata(GstBuffer *buffer, BufferInfo &buffer_info)
    {
        if (auto *meta = gst_buffer_get_tensor_meta(buffer)) {
            buffer_info.tensors.reserve(meta->num_tensors);
            for (gsize i = 0; i < meta->num_tensors; ++i) {
                const GstTensor *tensor = gst_tensor_meta_get(meta, i);
                TensorMeta tensor_meta;
                tensor_meta.tensor_index = i;
                tensor_meta.num_tensors = meta->num_tensors;
                tensor_meta.data_type = static_cast<int>(tensor->data_type);

                gsize num_dims = 0;
                auto *dims = gst_tensor_get_dims(const_cast<GstTensor *>(tensor), &num_dims);
                tensor_meta.dimensions.reserve(num_dims);
                for (gsize j = 0; j < num_dims; ++j) { tensor_meta.dimensions.push_back(dims[j]); }
                buffer_info.tensors.push_back(std::move(tensor_meta));
            }
        }
    }

    void append_frame_metadata(GstCaps *caps, GstBuffer *buffer, BufferInfo &buffer_info)
    {
        if (caps != nullptr) {
            GstStructure *structure = gst_caps_get_structure(caps, 0);
            if (structure != nullptr) {
                // Raw video caps carry width and height as G_TYPE_INT, which gst_structure_get_uint never matches.
                gint width = 0;
                gint height = 0;
                if (gst_structure_get_int(structure, "width", &width) != 0 && width > 0) {
                    buffer_info.metadata.width = static_cast<std::uint32_t>(width);
                }
                if (gst_structure_get_int(structure, "height", &height) != 0 && height > 0) {
                    buffer_info.metadata.height = static_cast<std::uint32_t>(height);
                }

                gint fps_n = 0;
                gint fps_d = 0;
                if (gst_structure_get_fraction(structure, "framerate", &fps_n, &fps_d) != 0 && fps_n >= 0
                    && fps_d > 0) {
                    buffer_info.metadata.fps_n = static_cast<std::uint32_t>(fps_n);
                    buffer_info.metadata.fps_d = static_cast<std::uint32_t>(fps_d);
                }

                const gchar *format = gst_structure_get_string(structure, "format");
                if (format != nullptr) { buffer_info.metadata.format = format; }
            }
        }

        buffer_info.metadata.timestamp_ns = GST_BUFFER_PTS(buffer);
        buffer_info.metadata.duration_ns = GST_BUFFER_DURATION(buffer);
    }

    auto extract_buffer_info(GstSample *sample) -> std::expected<BufferInfo, GStreamerError>
    {
        GstBuffer *buffer = gst_sample_get_buffer(sample);
        if (buffer == nullptr) { return std::unexpected(GStreamerError::BufferAllocationFailed); }

        GstMapInfo map_info;
        if (gst_buffer_map(buffer, &map_info, GST_MAP_READ) == 0) {
            return std::unexpected(GStreamerError::BufferAllocationFailed);
        }

        BufferInfo buffer_info;
        buffer_info.size = map_info.size;
        buffer_info.data.resize(map_info.size);
        std::memcpy(buffer_info.data.data(), map_info.data, map_info.size);

        append_tensor_metadata(buffer, buffer_info);
        append_frame_metadata(gst_sample_get_caps(sample), buffer, buffer_info);

        gst_buffer_unmap(buffer, &map_info);
        return buffer_info;
    }
}// namespace

struct GStreamerPipeline::Impl
{
    GstElement *pipeline{ nullptr };
    GstElement *appsink{ nullptr };
    GstElement *appsrc{ nullptr };

    BufferCallback buffer_callback;
    std::mutex callback_mutex;
    std::atomic<bool> is_playing{ false };
    std::atomic<bool> is_paused{ false };
    std::string caps_string;

    GMainLoop *main_loop{ nullptr };
    std::thread main_loop_thread;
    bool callbacks_installed{ false };

    ~Impl() { cleanup(); }

    void cleanup()
    {
        if (main_loop != nullptr) {
            g_main_loop_quit(main_loop);
            if (main_loop_thread.joinable()) { main_loop_thread.join(); }
            g_main_loop_unref(main_loop);
            main_loop = nullptr;
        }
        if (pipeline != nullptr) {
            gst_element_set_state(pipeline, GST_STATE_NULL);
            wait_for_callbacks();
        }
        // Both hold their own reference, so the pipeline below is only freed once they let go.
        if (appsink != nullptr) {
            if (callbacks_installed) {
                GstAppSinkCallbacks none{};
                gst_app_sink_set_callbacks(GST_APP_SINK(appsink), &none, nullptr, nullptr);
            }
            gst_object_unref(appsink);
            appsink = nullptr;
        }
        callbacks_installed = false;
        if (appsrc != nullptr) {
            gst_object_unref(appsrc);
            appsrc = nullptr;
        }
        if (pipeline != nullptr) {
            gst_object_unref(pipeline);
            pipeline = nullptr;
        }
        is_playing.store(false);
        is_paused.store(false);
    }

    // No callback runs after the NULL state change; the lock makes that ordering visible to TSan as well.
    void wait_for_callbacks() { std::scoped_lock lock(callback_mutex); }

    // The C API, not the appsink signals, which delivered nothing on Windows (run 36902874261).
    void connect_buffer_callback()
    {
        if (appsink == nullptr || callbacks_installed) { return; }
        {
            std::scoped_lock lock(callback_mutex);
            if (!buffer_callback) { return; }
        }
        GstAppSinkCallbacks callbacks{};
        callbacks.new_sample = &Impl::on_new_sample;
        gst_app_sink_set_callbacks(GST_APP_SINK(appsink), &callbacks, this, nullptr);
        callbacks_installed = true;
    }

    // appsink stops the stream on anything but GST_FLOW_OK.
    static auto on_new_sample(GstAppSink *sink, gpointer user_data) -> GstFlowReturn
    {
        auto *self = static_cast<Impl *>(user_data);
        GstSample *sample = gst_app_sink_pull_sample(sink);

        if (sample != nullptr) {
            auto buffer_info = extract_buffer_info(sample);
            if (buffer_info) {
                std::scoped_lock lock(self->callback_mutex);
                if (self->buffer_callback) { self->buffer_callback(*buffer_info); }
            }
            gst_sample_unref(sample);
        }
        return GST_FLOW_OK;
    }

    static auto on_pad_probe([[maybe_unused]] GstPad *pad,
      [[maybe_unused]] GstPadProbeInfo *info,
      [[maybe_unused]] gpointer user_data) -> GstPadProbeReturn
    { return GST_PAD_PROBE_OK; }
};

GStreamerPipeline::GStreamerPipeline() : impl_(std::make_unique<Impl>()) {}

GStreamerPipeline::~GStreamerPipeline() = default;

GStreamerPipeline::GStreamerPipeline(GStreamerPipeline &&) noexcept = default;

auto GStreamerPipeline::operator=(GStreamerPipeline &&) noexcept -> GStreamerPipeline & = default;

auto GStreamerPipeline::initialize_gstreamer(int *argc, char ***argv) -> std::expected<void, GStreamerError>
{

    std::scoped_lock lock(g_gstreamer_mutex);

    if (g_gstreamer_initialized.load()) { return {}; }

    GError *error = nullptr;
    if (gst_init_check(argc, argv, &error) == 0) {
        if (error != nullptr) { g_error_free(error); }
        return std::unexpected(GStreamerError::InitializationFailed);
    }

    g_gstreamer_initialized.store(true);
    return {};
}

auto GStreamerPipeline::deinitialize_gstreamer() -> void
{
    std::scoped_lock lock(g_gstreamer_mutex);
    if (g_gstreamer_initialized.load()) {
        gst_deinit();
        g_gstreamer_initialized.store(false);
    }
}

auto GStreamerPipeline::create_pipeline(const PipelineConfig &config) -> std::expected<void, GStreamerError>
{

    if (!g_gstreamer_initialized.load()) { return std::unexpected(GStreamerError::InitializationFailed); }

    impl_->cleanup();

    auto pipeline = parse_pipeline_description(config.pipeline_description, "Pipeline creation failed");
    if (!pipeline) { return std::unexpected(pipeline.error()); }
    impl_->pipeline = *pipeline;

    // Without these pull_sample, push_buffer and the buffer callback have nothing to talk to.
    impl_->appsink = find_element_of_type(impl_->pipeline, GST_TYPE_APP_SINK);
    impl_->appsrc = find_element_of_type(impl_->pipeline, GST_TYPE_APP_SRC);
    impl_->connect_buffer_callback();

    return {};
}

auto GStreamerPipeline::create_pipeline_from_string(const std::string &description)
  -> std::expected<void, GStreamerError>
{
    PipelineConfig config;
    config.pipeline_description = description;
    return create_pipeline(config);
}

auto GStreamerPipeline::create_inference_pipeline(const std::string &input_source,
  const std::string &model_path,
  const std::vector<std::size_t> & /*input_shape*/,
  const std::string &output_format) -> std::expected<void, GStreamerError>
{

    if (!g_gstreamer_initialized.load()) { return std::unexpected(GStreamerError::InitializationFailed); }

    impl_->cleanup();

    std::string pipeline_desc;

    if (input_source.contains("://")) {
        pipeline_desc = "uridecodebin uri=" + input_source + " ! ";
    } else if (input_source.starts_with("/dev/video")) {
        pipeline_desc = "v4l2src device=" + input_source + " ! ";
    } else {
        pipeline_desc = "filesrc location=" + input_source + " ! decodebin ! ";
    }

    pipeline_desc +=
      "videoconvert ! videoscale ! "
      "video/x-raw,format=RGB,width=640,height=480 ! "
      "tensor_transform mode=transpose option=1:2:0:3 ! "
      "tensor_transform mode=typecast option=float32 ! "
      "onnxruntime model-path="
      + model_path;

    if (output_format == "TENSOR") { pipeline_desc += " ! tensor_decoder mode=direct"; }

    pipeline_desc += " ! appsink name=sink emit-signals=true";

    auto pipeline = parse_pipeline_description(pipeline_desc, "Inference pipeline creation failed");
    if (!pipeline) { return std::unexpected(pipeline.error()); }
    impl_->pipeline = *pipeline;

    impl_->appsink = gst_bin_get_by_name(GST_BIN(impl_->pipeline), "sink");
    if (impl_->appsink == nullptr) { return std::unexpected(GStreamerError::ElementCreationFailed); }

    g_object_set(impl_->appsink, "emit-signals", TRUE, nullptr);
    impl_->connect_buffer_callback();

    return {};
}

auto GStreamerPipeline::start() -> std::expected<void, GStreamerError>
{
    if (impl_->pipeline == nullptr) { return std::unexpected(GStreamerError::PipelineCreationFailed); }

    GstStateChangeReturn ret = gst_element_set_state(impl_->pipeline, GST_STATE_PLAYING);

    if (ret == GST_STATE_CHANGE_FAILURE) { return std::unexpected(GStreamerError::StateChangeFailed); }

    impl_->is_playing.store(true);
    impl_->is_paused.store(false);
    return {};
}

auto GStreamerPipeline::stop() -> std::expected<void, GStreamerError>
{
    if (impl_->pipeline == nullptr) { return {}; }

    GstStateChangeReturn ret = gst_element_set_state(impl_->pipeline, GST_STATE_NULL);
    impl_->wait_for_callbacks();

    if (ret == GST_STATE_CHANGE_FAILURE) { return std::unexpected(GStreamerError::StateChangeFailed); }

    impl_->is_playing.store(false);
    impl_->is_paused.store(false);
    return {};
}

auto GStreamerPipeline::pause() -> std::expected<void, GStreamerError>
{
    if (impl_->pipeline == nullptr) { return std::unexpected(GStreamerError::PipelineCreationFailed); }

    GstStateChangeReturn ret = gst_element_set_state(impl_->pipeline, GST_STATE_PAUSED);

    if (ret == GST_STATE_CHANGE_FAILURE) { return std::unexpected(GStreamerError::StateChangeFailed); }

    impl_->is_paused.store(true);
    return {};
}

auto GStreamerPipeline::resume() -> std::expected<void, GStreamerError> { return start(); }

auto GStreamerPipeline::is_playing() const -> bool { return impl_->is_playing.load() && !impl_->is_paused.load(); }

auto GStreamerPipeline::is_paused() const -> bool { return impl_->is_paused.load(); }

auto GStreamerPipeline::set_buffer_callback(BufferCallback callback) -> void
{
    {
        std::scoped_lock lock(impl_->callback_mutex);
        impl_->buffer_callback = std::move(callback);
    }

    impl_->connect_buffer_callback();
}

auto GStreamerPipeline::pull_sample(std::uint32_t timeout_ms) -> std::expected<BufferInfo, GStreamerError>
{

    if (impl_->appsink == nullptr) { return std::unexpected(GStreamerError::ElementCreationFailed); }

    GstSample *sample = nullptr;

    if (timeout_ms == 0) {
        sample = gst_app_sink_pull_sample(GST_APP_SINK(impl_->appsink));
    } else {
        GstClockTime timeout = static_cast<GstClockTime>(timeout_ms) * GST_MSECOND;
        sample = gst_app_sink_try_pull_sample(GST_APP_SINK(impl_->appsink), timeout);
    }

    if (sample == nullptr) { return std::unexpected(GStreamerError::BufferAllocationFailed); }

    auto buffer_info = extract_buffer_info(sample);
    gst_sample_unref(sample);
    return buffer_info;
}

auto GStreamerPipeline::push_buffer(void *data, std::size_t size, const FrameMetadata &metadata)
  -> std::expected<void, GStreamerError>
{

    if (impl_->appsrc == nullptr) { return std::unexpected(GStreamerError::ElementCreationFailed); }

    GstBuffer *buffer = gst_buffer_new_allocate(nullptr, size, nullptr);
    if (buffer == nullptr) { return std::unexpected(GStreamerError::BufferAllocationFailed); }

    GstMapInfo map_info;
    if (gst_buffer_map(buffer, &map_info, GST_MAP_WRITE) == 0) {
        gst_buffer_unref(buffer);
        return std::unexpected(GStreamerError::BufferAllocationFailed);
    }

    std::memcpy(map_info.data, data, size);
    gst_buffer_unmap(buffer, &map_info);

    GST_BUFFER_PTS(buffer) = metadata.timestamp_ns;
    GST_BUFFER_DURATION(buffer) = metadata.duration_ns;

    // Takes the buffer's reference, unlike the push-buffer signal.
    const GstFlowReturn ret = gst_app_src_push_buffer(GST_APP_SRC(impl_->appsrc), buffer);

    if (ret != GST_FLOW_OK) { return std::unexpected(GStreamerError::StreamError); }

    return {};
}

auto GStreamerPipeline::get_position_ns() const -> std::expected<std::uint64_t, GStreamerError>
{
    if (impl_->pipeline == nullptr) { return std::unexpected(GStreamerError::PipelineCreationFailed); }

    gint64 position = 0;
    if (gst_element_query_position(impl_->pipeline, GST_FORMAT_TIME, &position) == 0) {
        return std::unexpected(GStreamerError::ResourceNotFound);
    }

    return static_cast<std::uint64_t>(position);
}

auto GStreamerPipeline::get_duration_ns() const -> std::expected<std::uint64_t, GStreamerError>
{
    if (impl_->pipeline == nullptr) { return std::unexpected(GStreamerError::PipelineCreationFailed); }

    gint64 duration = 0;
    if (gst_element_query_duration(impl_->pipeline, GST_FORMAT_TIME, &duration) == 0) {
        return std::unexpected(GStreamerError::ResourceNotFound);
    }

    return static_cast<std::uint64_t>(duration);
}

auto GStreamerPipeline::seek(std::uint64_t timestamp_ns) -> std::expected<void, GStreamerError>
{
    if (impl_->pipeline == nullptr) { return std::unexpected(GStreamerError::PipelineCreationFailed); }

    if (gst_element_seek_simple(impl_->pipeline,
          GST_FORMAT_TIME,
          static_cast<GstSeekFlags>(GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT),
          static_cast<gint64>(timestamp_ns))
        == 0) {
        return std::unexpected(GStreamerError::ResourceNotFound);
    }

    return {};
}

auto GStreamerPipeline::get_caps_string() const -> std::string { return impl_->caps_string; }

auto GStreamerPipeline::get_current_state() const -> int
{
    if (impl_->pipeline == nullptr) { return GST_STATE_NULL; }

    GstState state;
    gst_element_get_state(impl_->pipeline, &state, nullptr, 0);
    return static_cast<int>(state);
}

auto create_video_inference_pipeline(const std::string &video_source,
  const std::string &model_path,
  std::uint32_t width,
  std::uint32_t height,
  const std::string &output_sink) -> std::expected<GStreamerPipeline, GStreamerError>
{

    auto init_result = GStreamerPipeline::initialize_gstreamer();
    if (!init_result) { return std::unexpected(init_result.error()); }

    GStreamerPipeline pipeline;

    std::string pipeline_desc;

    if (video_source.contains("://")) {
        pipeline_desc = "uridecodebin uri=" + video_source + " ! ";
    } else {
        pipeline_desc = "filesrc location=" + video_source + " ! decodebin ! ";
    }

    pipeline_desc += "videoconvert ! videoscale ! "
        "video/x-raw,format=RGB,width=" + std::to_string(width) +
        ",height=" + std::to_string(height) + " ! "
        "tensor_transform mode=transpose option=1:2:0:3 ! "
        "tensor_transform mode=typecast option=float32 ! "
        "onnxruntime model-path=" + model_path;

    if (output_sink == "appsink") {
        pipeline_desc += " ! appsink name=sink emit-signals=true sync=false";
    } else if (output_sink == "fakesink") {
        pipeline_desc += " ! fakesink sync=false";
    }

    auto result = pipeline.create_pipeline_from_string(pipeline_desc);
    if (!result) { return std::unexpected(result.error()); }

    return pipeline;
}

auto create_camera_inference_pipeline(const std::string &device,
  const std::string &model_path,
  std::uint32_t width,
  std::uint32_t height,
  std::uint32_t fps) -> std::expected<GStreamerPipeline, GStreamerError>
{

    auto init_result = GStreamerPipeline::initialize_gstreamer();
    if (!init_result) { return std::unexpected(init_result.error()); }

    GStreamerPipeline pipeline;

    std::string pipeline_desc =
        "v4l2src device=" + device + " ! "
        "video/x-raw,width=" + std::to_string(width) +
        ",height=" + std::to_string(height) +
        ",framerate=" + std::to_string(fps) + "/1 ! "
        "videoconvert ! "
        "tensor_transform mode=transpose option=1:2:0:3 ! "
        "tensor_transform mode=typecast option=float32 ! "
        "onnxruntime model-path=" + model_path + " ! "
        "appsink name=sink emit-signals=true sync=false";

    auto result = pipeline.create_pipeline_from_string(pipeline_desc);
    if (!result) { return std::unexpected(result.error()); }

    return pipeline;
}

}// namespace kataglyphis::gstreamer