From 9c622e0ff40bb898c095494027808b1d2e2cdb65 Mon Sep 17 00:00:00 2001 From: Stefan Hamminga Date: Mon, 24 Jun 2024 11:55:56 +0300 Subject: [PATCH] Initial commit --- .gitignore | 3 + .gitmodules | 3 + README.md | 81 ++++++++++++ gst_root | 1 + include/gstmsoverlay.h | 88 +++++++++++++ meson.build | 70 +++++++++++ meson_options.txt | 13 ++ src/gstmsoverlay.c | 276 +++++++++++++++++++++++++++++++++++++++++ 8 files changed, 535 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 README.md create mode 160000 gst_root create mode 100644 include/gstmsoverlay.h create mode 100644 meson.build create mode 100644 meson_options.txt create mode 100644 src/gstmsoverlay.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a72d603 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +builddir +build +.vscode diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..49017a3 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "gst_root"] + path = gst_root + url = https://gitlab.freedesktop.org/gstreamer/gstreamer.git diff --git a/README.md b/README.md new file mode 100644 index 0000000..c85ee53 --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +# GStreamer Microsecond Timestamp Overlay + +This is an adaptation of the `clockoverlay` and `timeoverlay` plugins from gstreamer-base. Stripped bare and just overlaying the Linux epoch time, with a microsecond resolution. + +## Use-case Examples + +A single stream solution can be tested like this: + +```bash +gst-launch-1.0 \ + videotestsrc is-live=true do-timestamp=true \ + ! msoverlay halignment=left valignment=top \ + ! video/x-raw,format=RGB,framerate=30/1,width=640,height=480 \ + ! videoconvert \ + ! video/x-raw,format=NV12 \ + ! nvautogpuh264enc tune=3 \ + ! rtph264pay config-interval=0 pt=96 \ + ! rtph264depay \ + ! h264parse \ + ! nvh264dec \ + ! msoverlay halignment=left valignment=bottom \ + ! autovideosink +``` + +Take a screenshot of the output above and subtract the top-left number from the bottom-left to see the total pipeline latency. + +For a more comprehensive solution we can also make the stream traverse the network stack: + +Server: + +```bash +gst-launch-1.0 videotestsrc is-live=true do-timestamp=true ! msoverlay halignment=left valignment=top ! video/x-raw,format=RGB,framerate=60/1,width=1920,height=1080 ! videoconvert ! video/x-raw,format=NV12 ! nvautogpuh264enc tune=3 ! rtph264pay config-interval=1 pt=96 ! rtpsink uri=rtp://127.0.0.1:5000 +``` + +Live client: + +```bash +gst-launch-1.0 rtpsrc uri=rtp://127.0.0.1:5000?encoding-name=H264 ! rtph264depay ! h264parse ! nvh264dec ! msoverlay halignment=left valignment=bottom ! autovideosink +``` + +PNG sampling client outputting 5 frames, one per second: + +```bash +gst-launch-1.0 rtpsrc uri=rtp://127.0.0.1:5000?encoding-name=H264 ! rtph264depay ! h264parse ! nvh264dec ! msoverlay halignment=left valignment=bottom ! videorate drop-only=true ! video/x-raw,framerate=1/1 ! pngenc ! multifilesink num-buffers=10 location="latency_%02d.png" +``` + +It is essential both the client and server run on the same machine, unless you _really_ trust your system clock accuracy. + +## Dependencies + +TODO. Basically the same dependencies as for building the gstreamer-plugins-base packages. + +## Building + +```bash +# Assuming you're in the project root + +# Option A is to use the submodule to fetch the GStreamer sources: +git submodule update --init --recursive + +# Older versions require deleting the builddir and omitting the '--reconfigure' option +meson setup --reconfigure builddir +# Option B is to use an existing GStreamer source by specifying the 'gst_root' (the GStreamer mono-repo root) +# meson setup --reconfigure builddir -Dgst_root=${WHERE_I_CLONED_GSTREAMER}/gstreamer + +meson compile -C builddir + +# This will install the built plugin file +meson install -C builddir + +# Confirm installation +gst-inspect-1.0 | grep msoverlay +``` + +## Verification + +This should show you a fairly extensive list of options and details: + +```bash +gst-inspect-1.0 msoverlay +``` diff --git a/gst_root b/gst_root new file mode 160000 index 0000000..9e1abc0 --- /dev/null +++ b/gst_root @@ -0,0 +1 @@ +Subproject commit 9e1abc0797575afec4194ce4a9154b25010e5104 diff --git a/include/gstmsoverlay.h b/include/gstmsoverlay.h new file mode 100644 index 0000000..de12a87 --- /dev/null +++ b/include/gstmsoverlay.h @@ -0,0 +1,88 @@ +/* GStreamer + * Copyright (C) <1999> Erik Walthinsen + * Copyright (C) <2005> Tim-Philipp Müller + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + + +#ifndef __GST_MS_OVERLAY_H__ +#define __GST_MS_OVERLAY_H__ + +#include "gstbasetextoverlay.h" + +G_BEGIN_DECLS + +GST_ELEMENT_REGISTER_DECLARE (msoverlay); + + +#define GST_TYPE_MS_OVERLAY \ + (gst_ms_overlay_get_type()) +#define GST_MS_OVERLAY(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_MS_OVERLAY,GstMsOverlay)) +#define GST_MS_OVERLAY_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_MS_OVERLAY,GstMsOverlayClass)) +#define GST_IS_MS_OVERLAY(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_MS_OVERLAY)) +#define GST_IS_MS_OVERLAY_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_MS_OVERLAY)) + +typedef struct _GstMsOverlay GstMsOverlay; +typedef struct _GstMsOverlayClass GstMsOverlayClass; + +/** + * GstMsOverlay: + * + * Opaque msoverlay data structure. + */ +struct _GstMsOverlay { + GstBaseTextOverlay textoverlay; + gchar *format; /* as in strftime () */ + + /* for wcsftime */ + gunichar2 *wformat; + + gchar *text; +}; + +struct _GstMsOverlayClass { + GstBaseTextOverlayClass parent_class; +}; + +GType gst_ms_overlay_get_type (void); + + +// /* This is a hack hat allows us to use nonliterals for strftime without +// * triggering a warning from -Wformat-nonliteral. We need to allow this +// * because we export the format string as a property of the element. +// * For the inspiration of this and a discussion of why this is necessary, +// * see http://gcc.gnu.org/bugzilla/show_bug.cgi?id=39438 +// */ +// #ifdef __GNUC__ +// #pragma GCC system_header +// static size_t my_strftime(char *s, size_t max, const char *format, +// const struct tm *tm) +// { +// return strftime (s, max, format, tm); +// } +// #define strftime my_strftime +// #endif + + +G_END_DECLS + +#endif /* __GST_MS_OVERLAY_H__ */ + diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..e6761e2 --- /dev/null +++ b/meson.build @@ -0,0 +1,70 @@ +project('msoverlay', 'c', + version : '1.24.3', + license : 'LGPL', + default_options: [ + 'prefix=/usr', + 'warning_level=2', + 'buildtype=debugoptimized' + ]) + +gst_dep = dependency('gstreamer-1.0') +gstbase_dep = dependency('gstreamer-base-1.0') +gstvideo_dep = dependency('gstreamer-video-1.0') +pango_dep = dependency('pangocairo', version : '>=1.22.0', required : true, + fallback: ['pango', 'libpangocairo_dep'], + default_options: ['cairo=enabled']) +cc = meson.get_compiler('c') +m_dep = cc.find_library('m') + +gst_version = gst_dep.version() +version_arr = gst_version.split('.') +gst_version_major = version_arr[0].to_int() +gst_version_minor = version_arr[1].to_int() +gst_version_micro = version_arr[2].to_int() + +api_version = '1.0' +soversion = 0 +libversion = '@0@.@1@.0'.format(soversion, gst_version_minor * 100 + gst_version_micro) + +plugins_install_dir = join_paths(get_option('libdir'), 'gstreamer-1.0') + +plugin_c_args = ['-DHAVE_CONFIG_H'] + +cdata = configuration_data() + +cdata.set_quoted('VERSION', gst_version) +cdata.set_quoted('PACKAGE', 'msoverlay') +cdata.set_quoted('PACKAGE_VERSION', gst_version) +cdata.set_quoted('PACKAGE_NAME', 'GStreamer ms overlay') +cdata.set_quoted('GST_PACKAGE_NAME', 'GStreamer ms overlay') +cdata.set_quoted('GST_PACKAGE_ORIGIN', 'https://gstreamer.freedesktop.org') +cdata.set_quoted('GST_API_VERSION', api_version) +cdata.set_quoted('GST_LICENSE', 'LGPL') +cdata.set_quoted('LIBDIR', join_paths(get_option('prefix'), get_option('libdir'))) + +gst_root = get_option('gst_root') + +plugin_sources = files( + 'src/gstmsoverlay.c', + 'include/gstmsoverlay.h', + join_paths(gst_root, 'subprojects/gst-plugins-base/ext/pango/gstbasetextoverlay.c'), + join_paths(gst_root, 'subprojects/gst-plugins-base/ext/pango/gstpangoelement.c') +) + +install_dir = join_paths(get_option('libdir'), 'gstreamer-1.0') + +include_dirs = include_directories(join_paths(gst_root, 'subprojects/gst-plugins-base/ext/pango/'), 'include') + +msoverlay = library( + 'msoverlay', + plugin_sources, + c_args: plugin_c_args, + include_directories: include_dirs, + dependencies: [gst_dep, gstbase_dep, gstvideo_dep, pango_dep, m_dep], + install: true, + install_dir: plugins_install_dir +) + +configure_file(output : 'config.h', configuration : cdata) + + diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..054ba3b --- /dev/null +++ b/meson_options.txt @@ -0,0 +1,13 @@ + +# Common feature options +option('gobject-cast-checks', type : 'feature', value : 'auto', yield : true, + description: 'Enable run-time GObject cast checks (auto = enabled for development, disabled for stable releases)') +option('glib-asserts', type : 'feature', value : 'enabled', yield : true, + description: 'Enable GLib assertion (auto = enabled for development, disabled for stable releases)') + +option('package-name', type : 'string', yield : true, + description : 'package name to use in plugins') +option('package-origin', type : 'string', value : 'Unknown package origin', yield : true, + description : 'package origin URL to use in plugins') + +option('gst_root', type: 'string', value: './gst_root', description: 'Root path for GStreamer build') diff --git a/src/gstmsoverlay.c b/src/gstmsoverlay.c new file mode 100644 index 0000000..93259bc --- /dev/null +++ b/src/gstmsoverlay.c @@ -0,0 +1,276 @@ +/* GStreamer + * Copyright (C) <1999> Erik Walthinsen + * Copyright (C) <2005> Tim-Philipp Müller + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +/** + * SECTION:element-msoverlay + * @title: msoverlay + * @see_also: #GstBaseTextOverlay, #GstTimeOverlay + * + * This element overlays the Linux epoch time on top of a video + * stream. You can position the text and configure the font details + * using its properties. + * + * By default, the time is displayed in the top left corner of the picture, with some + * padding to the left and to the top. + * + * ## Example launch lines + * |[ + * gst-launch-1.0 -v videotestsrc ! msoverlay ! autovideosink + * ]| + * Display the current wall clock time in the top left corner of the video picture + * |[ + * gst-launch-1.0 -v videotestsrc ! msoverlay halignment=right valignment=bottom font-desc="Sans, 36" ! videoconvert ! autovideosink + * ]| + * Another pipeline that displays the epoch time with some leading + * text in the bottom right corner of the video picture, with the background + * of the text being shaded in order to make it more legible on top of a + * bright video background. + * + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +// #include +#include + +#include "gstmsoverlay.h" +#include "gstpangoelements.h" + + + +#define DEFAULT_PROP_TIMEFORMAT "%S" + + +enum +{ + PROP_0, + // PROP_TIMEFORMAT, + PROP_LAST +}; + +#define gst_ms_overlay_parent_class parent_class +G_DEFINE_TYPE (GstMsOverlay, gst_ms_overlay, GST_TYPE_BASE_TEXT_OVERLAY); +GST_ELEMENT_REGISTER_DEFINE_WITH_CODE (msoverlay, "msoverlay", + GST_RANK_NONE, GST_TYPE_MS_OVERLAY, pango_element_init (plugin)); + +static void gst_ms_overlay_finalize (GObject * object); +static void gst_ms_overlay_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * pspec); +static void gst_ms_overlay_get_property (GObject * object, guint prop_id, + GValue * value, GParamSpec * pspec); + +static gchar * +gst_ms_overlay_render_time (GstMsOverlay * overlay) +{ + struct timeval tv; + if (gettimeofday(&tv, NULL) != 0) { + return g_strdup("XXX.XXXXXX"); + } + + gchar buf[256]; + int written = g_snprintf(buf, sizeof(buf), "%ld.%06ld", tv.tv_sec, tv.tv_usec); + + if (written <= 0 || written >= sizeof(buf)) { + return g_strdup("YYY.YYYYYY"); + } + + return g_strdup (buf); +} + +/* Called with lock held */ +static gchar * +gst_ms_overlay_get_text (GstBaseTextOverlay * overlay, + GstBuffer * video_frame) +{ + gchar *time_str, *txt, *ret; + GstMsOverlay *ms_overlay = GST_MS_OVERLAY (overlay); + + txt = g_strdup (overlay->default_text); + + GST_OBJECT_LOCK (overlay); + time_str = gst_ms_overlay_render_time (ms_overlay); + GST_OBJECT_UNLOCK (overlay); + + if (txt != NULL && *txt != '\0') { + ret = g_strdup_printf ("%s %s", txt, time_str); + } else { + ret = time_str; + time_str = NULL; + } + + if (g_strcmp0 (ret, ms_overlay->text)) { + overlay->need_render = TRUE; + g_free (ms_overlay->text); + ms_overlay->text = g_strdup (ret); + } + + g_free (txt); + g_free (time_str); + + return ret; +} + +static void +gst_ms_overlay_class_init (GstMsOverlayClass * klass) +{ + GObjectClass *gobject_class; + GstElementClass *gstelement_class; + GstBaseTextOverlayClass *gsttextoverlay_class; + + gobject_class = (GObjectClass *) klass; + gstelement_class = (GstElementClass *) klass; + gsttextoverlay_class = (GstBaseTextOverlayClass *) klass; + + gobject_class->finalize = gst_ms_overlay_finalize; + gobject_class->set_property = gst_ms_overlay_set_property; + gobject_class->get_property = gst_ms_overlay_get_property; + + gst_element_class_set_static_metadata (gstelement_class, "Millisecond overlay", + "Filter/Editor/Video", + "Overlays the current timestamp in ms on a video stream", + "Stefan Hamminga "); + + gsttextoverlay_class->get_text = gst_ms_overlay_get_text; + + // g_object_class_install_property (gobject_class, PROP_TIMEFORMAT, + // g_param_spec_string ("time-format", "Date/Time Format", + // "Format to use for time and date value, as in strftime.", + // DEFAULT_PROP_TIMEFORMAT, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); +} + + +static void +gst_ms_overlay_finalize (GObject * object) +{ + GstMsOverlay *overlay = GST_MS_OVERLAY (object); + + g_free (overlay->format); + g_free (overlay->text); + overlay->format = NULL; + + g_free (overlay->wformat); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + + +static void +gst_ms_overlay_init (GstMsOverlay * overlay) +{ + GstBaseTextOverlay *textoverlay; + PangoContext *context; + PangoFontDescription *font_description; + + textoverlay = GST_BASE_TEXT_OVERLAY (overlay); + + textoverlay->valign = GST_BASE_TEXT_OVERLAY_VALIGN_TOP; + textoverlay->halign = GST_BASE_TEXT_OVERLAY_HALIGN_LEFT; + + overlay->format = g_strdup (DEFAULT_PROP_TIMEFORMAT); + +#ifdef G_OS_WIN32 + overlay->wformat = + g_utf8_to_utf16 (DEFAULT_PROP_TIMEFORMAT, -1, NULL, NULL, NULL); +#endif + + context = textoverlay->pango_context; + + pango_context_set_language (context, pango_language_from_string ("en_US")); + pango_context_set_base_dir (context, PANGO_DIRECTION_LTR); + + font_description = pango_font_description_new (); + pango_font_description_set_family_static (font_description, "Monospace"); + pango_font_description_set_style (font_description, PANGO_STYLE_NORMAL); + pango_font_description_set_variant (font_description, PANGO_VARIANT_NORMAL); + pango_font_description_set_weight (font_description, PANGO_WEIGHT_NORMAL); + pango_font_description_set_stretch (font_description, PANGO_STRETCH_NORMAL); + pango_font_description_set_size (font_description, 18 * PANGO_SCALE); + pango_context_set_font_description (context, font_description); + pango_font_description_free (font_description); +} + +static void +gst_ms_overlay_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * pspec) +{ + GstMsOverlay *overlay = GST_MS_OVERLAY (object); + + GST_OBJECT_LOCK (overlay); + switch (prop_id) { +// case PROP_TIMEFORMAT: +// g_free (overlay->format); +// overlay->format = g_value_dup_string (value); +// if (!overlay->format) +// overlay->format = g_strdup (DEFAULT_PROP_TIMEFORMAT); +// #ifdef G_OS_WIN32 +// g_free (overlay->wformat); +// overlay->wformat = +// g_utf8_to_utf16 (overlay->format, -1, NULL, NULL, NULL); +// #endif +// break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } + GST_OBJECT_UNLOCK (overlay); +} + + +static void +gst_ms_overlay_get_property (GObject * object, guint prop_id, + GValue * value, GParamSpec * pspec) +{ + GstMsOverlay *overlay = GST_MS_OVERLAY (object); + + GST_OBJECT_LOCK (overlay); + switch (prop_id) { + // case PROP_TIMEFORMAT: + // g_value_set_string (value, overlay->format); + // break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } + GST_OBJECT_UNLOCK (overlay); +} + +static gboolean +plugin_init (GstPlugin * plugin) +{ + return GST_ELEMENT_REGISTER (msoverlay, plugin); +} + + + +GST_PLUGIN_DEFINE ( + GST_VERSION_MAJOR, + GST_VERSION_MINOR, + msoverlay, + PACKAGE_NAME, + plugin_init, + VERSION, + GST_LICENSE, + PACKAGE, + GST_PACKAGE_ORIGIN +)