FancyBirds/stream_source.c

245 lines
9.0 KiB
C

// -*-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 <https://www.gnu.org/licenses/>.
*/
/**
* Handling the webcam and stream from the source computer.
*
*/
#include "gst/gstbin.h"
#include "gst/gstbus.h"
#include "gst/gstcaps.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 "gst/gstvalue.h"
#include <gst/gst.h>
#include <ini.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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.
GString *passphrase; ///< Passphrase used to encrypt srt traffic with
} 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")) {
char *extended_value = calloc(strlen(value) + strlen("https://") + 1, sizeof(char));
sprintf(extended_value, "https://%s", value);
GUri *url = g_uri_parse(extended_value, G_URI_FLAGS_NONE, NULL);
free(extended_value);
if (url) {
conf->url = url;
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;
}
}
if (CONF_KEY("remote", "passphrase")) {
conf->passphrase = g_string_new(value);
}
return -1;
}
int main(int argc, char *argv[]) {
GstBus *bus;
GstMessage *msg;
GstElement *video_capsfilter;
GstElement *video_convert;
GstElement *video_encode;
GstElement *video_parse;
GstElement *mux;
GstElement *srt_sink;
CustomData data;
GstStateChangeReturn ret;
gboolean terminate = FALSE;
ConfigData conf;
GString *srt_uri;
// 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_capsfilter = gst_element_factory_make("capsfilter", "video_capsfilter");
video_convert = gst_element_factory_make("videoconvert", "video_convert");
video_encode = gst_element_factory_make("x264enc", "video_encode");
video_parse = gst_element_factory_make("h264parse", "video_parse");
mux = gst_element_factory_make("matroskamux", "mux");
srt_sink = gst_element_factory_make("srtsink", "srt_sink");
data.pipeline = gst_pipeline_new("media-pipeline");
if (!data.source || !video_capsfilter || !video_convert || !video_encode || !video_parse || !mux || !srt_sink || !data.pipeline) {
g_printerr("Couldn't create all elements.\n");
if (!data.source) g_printerr("Couldn't create source.\n");
if (!video_capsfilter) g_printerr("Couldn't create capsfilter.\n");
if (!video_convert) g_printerr("Couldn't create convert.\n");
if (!video_encode) g_printerr("Couldn't create encode.\n");
if (!video_parse) g_printerr("Couldn't create parse.\n");
if (!mux) g_printerr("Couldn't create mux.\n");
if (!srt_sink) g_printerr("Couldn't create sink.\n");
if (!data.pipeline) g_printerr("Couldn't create pipeline.\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);
// Set video framerate to 30/1 on video/x-raw
GstCaps *video_caps = gst_caps_new_simple("video/x-raw", "framerate", GST_TYPE_FRACTION, 30, 1, NULL);
g_object_set(video_capsfilter, "caps", video_caps, NULL);
gst_caps_unref(video_caps);
g_object_set(video_encode, "tune", "zerolatency", NULL);
g_object_set(video_encode, "speed-preset", "veryfast", NULL);
g_object_set(video_encode, "bitrate", 2500, NULL);
g_object_set(video_encode, "key-int-max", 60, NULL);
g_object_set(video_encode, "aud", TRUE, NULL);
g_object_set(video_parse, "config-interval", -1, NULL);
g_object_set(mux, "streamable", TRUE, NULL);
srt_uri = g_string_new("");
g_string_printf(srt_uri, "srt://%s?mode=caller&pbkeylen=32&passphrase=%s&latency=150", g_uri_to_string(conf.url) + strlen("https://"), conf.passphrase->str);
g_print("%s\n", srt_uri->str);
g_object_set(srt_sink, "uri", srt_uri->str, NULL);
g_object_set(mux, "streamable", TRUE, NULL);
// Add all elements to the pipeline and link them together.
gst_bin_add_many(GST_BIN(data.pipeline), data.source, video_capsfilter, video_convert, video_encode, video_parse, mux, srt_sink, NULL);
if (!gst_element_link_many(data.source, video_capsfilter, video_convert, video_encode, video_parse, mux, srt_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;
}