// -*-eval: (highlight-doxygen-mode 1);-*- /* Copyright (C) 2025 Johannes Randerath This file is part of FancyBirds. Fancybirds is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Fancybirds is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Foobar. If not, see . */ /** * Handling the webcam and stream from the source computer. * */ #include "gst/gstbin.h" #include "gst/gstbus.h" #include "gst/gstclock.h" #include "gst/gstelement.h" #include "gst/gstelementfactory.h" #include "gst/gstmessage.h" #include "gst/gstobject.h" #include "gst/gstpipeline.h" #include "gst/gstutils.h" #include #include #include #include #include #define CONF_KEY(sec, nam) !strcmp(section, sec) && !strcmp(name, nam) /** * Holds main data to be passed around. * Used by Gstreamer to communicate data between the different functions and in case of a callback. */ typedef struct _CustomData { GstElement *pipeline; ///< Gstreamer pipeline holding the stream elements. GstElement *source; ///< This is the video input from the physical webcam. } CustomData; /** * Holds data read from config ini file. * Configuration is to be written to a ini file, which is then read using inih and the results are saved in this struct and read from here whereever needed. */ typedef struct _ConfigData { GUri *url; ///< URL of the server to stream to. Must be a valid URI. GUri *device; ///< Device file. Must be an absolute path. GUri *ssh_key; ///< Path to SSH Key used to authenticate with the server. } ConfigData; /** * Inih callback to save config data. * When inih reads the config file, it will call this function once for each value. The function writes the values to the corresponding fields in a ConfigData struct. * @param user A pointer to a ConfigData struct for the values to be written to. * @param section The name of the ini files section. Used to identify the value. * @param name Name of the config value. Used to identify the value. * @param value User-defined setting of the configuration value identified by section and name. Set to the appropriate field in ConfigData. * @return 0 if successful -1 if not. */ static int config_callback(void *user, const char *section, const char *name, const char *value) { ConfigData *conf = (ConfigData *)user; if (CONF_KEY("v4l2", "device")) { char *extended_value = calloc(strlen(value) + strlen("file://") + 1, sizeof(char)); sprintf(extended_value, "file://%s", value); GUri *path = g_uri_parse(extended_value, G_URI_FLAGS_NONE, NULL); free(extended_value); if (path) { conf->device = path; return 0; } } if (CONF_KEY("remote", "url")) { GUri *uri = g_uri_parse(value, G_URI_FLAGS_NONE, NULL); if (uri) { conf->url = uri; return 0; } } if (CONF_KEY("remote", "ssh-key")) { char *extended_value = calloc(strlen(value) + strlen("file://") + 1, sizeof(char)); sprintf(extended_value, "file://%s", value); GUri *path = g_uri_parse(extended_value, G_URI_FLAGS_NONE, NULL); free(extended_value); if (path) { conf->ssh_key = path; return 0; } } return -1; } int main(int argc, char *argv[]) { GstBus *bus; GstMessage *msg; GstElement *video_queue; GstElement *video_convert; GstElement *video_sink; CustomData data; GstStateChangeReturn ret; gboolean terminate = FALSE; ConfigData conf; // Read configuration data from ini file and write to conf. config_callback is called for each value. ini_parse("../config.ini", config_callback, &conf); // Initialize Gstreamer and set up the pipeline along with its elements. gst_init(&argc, &argv); data.source = gst_element_factory_make("v4l2src", "source"); video_queue = gst_element_factory_make("queue", "video_queue"); video_convert = gst_element_factory_make("videoconvert", "video_convert"); video_sink = gst_element_factory_make("autovideosink", "video_sink"); data.pipeline = gst_pipeline_new("media-pipeline"); if (!data.source || !video_queue || !video_convert || !video_sink || !data.pipeline) { g_printerr("Couldn't create all elements.\n"); return -1; } // Use the device file specified by configuration as video input source. g_object_set(data.source, "device", g_uri_to_string(conf.device) + strlen("file://"), NULL); // Add all elements to the pipeline and link them together. gst_bin_add_many(GST_BIN(data.pipeline), data.source, video_queue, video_convert, video_sink, NULL); if (!gst_element_link_many(data.source, video_queue, video_convert, video_sink, NULL)) { g_printerr("Couldn't link stream elements.\n"); gst_object_unref(data.pipeline); return -1; } // Start streaming. ret = gst_element_set_state(data.pipeline, GST_STATE_PLAYING); if (ret == GST_STATE_CHANGE_FAILURE) { g_printerr("Failed to start playing.\n"); return -1; } // Listen to any events we might want to react to. bus = gst_element_get_bus(data.pipeline); do { msg = gst_bus_timed_pop_filtered(bus, GST_CLOCK_TIME_NONE, GST_MESSAGE_STATE_CHANGED | GST_MESSAGE_ERROR | GST_MESSAGE_EOS); switch (GST_MESSAGE_TYPE(msg)) { // If an error occured, print everything we know and quit. case GST_MESSAGE_ERROR: { GError *err; gchar *debug_info; gst_message_parse_error(msg, &err, &debug_info); g_printerr("Error received from element %s: %s\n", GST_ELEMENT_NAME(msg->src), err->message); g_printerr("Debug info: %s\n", debug_info ? debug_info : "none"); g_error_free(err); g_free(debug_info); terminate = TRUE; break; } // Stream is done. Quit. case GST_MESSAGE_EOS: g_print("End-of-stream reached.\n"); terminate = TRUE; break; // For every change in pipeline state, print what happened. case GST_MESSAGE_STATE_CHANGED: if (GST_MESSAGE_SRC(msg) == GST_OBJECT(data.pipeline)) { GstState old_state, new_state, pending_state; gst_message_parse_state_changed(msg, &old_state, &new_state, &pending_state); g_print("Changed from state %d to state %d\n", old_state, new_state); } break; // Unhandled event. Ignore. default: break; } }while (!terminate); g_free(conf.device); g_free(conf.ssh_key); g_free(conf.url); gst_object_unref(bus); gst_element_set_state(data.pipeline, GST_STATE_NULL); gst_object_unref(data.pipeline); return 0; }