| /* GStreamer | |
| * Copyright (C) 2023 Seungha Yang <seungha@centricular.com> | |
| * | |
| * 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. | |
| */ | |
| #ifdef HAVE_CONFIG_H | |
| #include "config.h" | |
| #endif | |
| #include "gstdwritetimeoverlay.h" | |
| #include <mutex> | |
| #include <strsafe.h> | |
| GST_DEBUG_CATEGORY_STATIC (dwrite_time_overlay_debug); | |
| #define GST_CAT_DEFAULT dwrite_time_overlay_debug | |
| typedef enum | |
| { | |
| GST_DWRITE_TIME_OVERLAY_TIME_LINE_BUFFER_TIME, | |
| GST_DWRITE_TIME_OVERLAY_TIME_LINE_STREAM_TIME, | |
| GST_DWRITE_TIME_OVERLAY_TIME_LINE_RUNNING_TIME, | |
| GST_DWRITE_TIME_OVERLAY_TIME_LINE_TIME_CODE, | |
| GST_DWRITE_TIME_OVERLAY_TIME_LINE_ELAPSED_RUNNING_TIME, | |
| GST_DWRITE_TIME_OVERLAY_TIME_LINE_REFERENCE_TIMESTAMP, | |
| GST_DWRITE_TIME_OVERLAY_TIME_LINE_BUFFER_COUNT, | |
| GST_DWRITE_TIME_OVERLAY_TIME_LINE_BUFFER_OFFSET, | |
| } GstDWriteTimeOverlayTimeLine; | |
| #define GST_TYPE_DWrite_TIME_OVERLAY_TIME_LINE (gst_dwrite_time_overlay_time_line_type ()) | |
| static GType | |
| gst_dwrite_time_overlay_time_line_type (void) | |
| { | |
| static GType type; | |
| static const GEnumValue modes[] = { | |
| {GST_DWRITE_TIME_OVERLAY_TIME_LINE_BUFFER_TIME, | |
| "buffer-time", "buffer-time"}, | |
| {GST_DWRITE_TIME_OVERLAY_TIME_LINE_STREAM_TIME, | |
| "stream-time", "stream-time"}, | |
| {GST_DWRITE_TIME_OVERLAY_TIME_LINE_RUNNING_TIME, | |
| "running-time", "running-time"}, | |
| {GST_DWRITE_TIME_OVERLAY_TIME_LINE_TIME_CODE, "time-code", "time-code"}, | |
| {GST_DWRITE_TIME_OVERLAY_TIME_LINE_ELAPSED_RUNNING_TIME, | |
| "elapsed-running-time", "elapsed-running-time"}, | |
| {GST_DWRITE_TIME_OVERLAY_TIME_LINE_REFERENCE_TIMESTAMP, | |
| "reference-timestamp", "reference-timestamp"}, | |
| {GST_DWRITE_TIME_OVERLAY_TIME_LINE_BUFFER_COUNT, | |
| "buffer-count", "buffer-count"}, | |
| {GST_DWRITE_TIME_OVERLAY_TIME_LINE_BUFFER_OFFSET, | |
| "buffer-offset", "buffer-offset"}, | |
| {0, nullptr, nullptr}, | |
| }; | |
| GST_DWRITE_CALL_ONCE_BEGIN { | |
| type = g_enum_register_static ("GstDWriteTimeOverlayTimeLine", modes); | |
| } GST_DWRITE_CALL_ONCE_END; | |
| return type; | |
| } | |
| enum | |
| { | |
| PROP_0, | |
| PROP_TIME_LINE, | |
| PROP_SHOW_TIMES_AS_DATES, | |
| PROP_DATETIME_EPOCH, | |
| PROP_DATETIME_FORMAT, | |
| PROP_REFERENCE_TIMESTAMP_CAPS, | |
| }; | |
| #define DEFAULT_TIME_LINE GST_DWRITE_TIME_OVERLAY_TIME_LINE_BUFFER_TIME | |
| #define DEFAULT_SHOW_TIMES_AS_DATES FALSE | |
| #define DEFAULT_DATETIME_FORMAT "%F %T" /* YYYY-MM-DD hh:mm:ss */ | |
| static GstStaticCaps ntp_reference_timestamp_caps = | |
| GST_STATIC_CAPS ("timestamp/x-ntp"); | |
| struct GstDWriteTimeOverlayPrivate | |
| { | |
| GstDWriteTimeOverlayPrivate () | |
| { | |
| datetime_epoch = g_date_time_new_utc (1900, 1, 1, 0, 0, 0); | |
| reference_timestamp_caps = | |
| gst_static_caps_get (&ntp_reference_timestamp_caps); | |
| } | |
| ~GstDWriteTimeOverlayPrivate () | |
| { | |
| if (datetime_epoch) | |
| g_date_time_unref (datetime_epoch); | |
| gst_clear_caps (&reference_timestamp_caps); | |
| } | |
| std::mutex lock; | |
| GstDWriteTimeOverlayTimeLine time_line = DEFAULT_TIME_LINE; | |
| gboolean show_times_as_dates = DEFAULT_SHOW_TIMES_AS_DATES; | |
| guint64 buffer_count = 0; | |
| std::string datetime_format = DEFAULT_DATETIME_FORMAT; | |
| GDateTime *datetime_epoch; | |
| GstCaps *reference_timestamp_caps; | |
| GstClockTime first_running_time = GST_CLOCK_TIME_NONE; | |
| }; | |
| struct _GstDWriteTimeOverlay | |
| { | |
| GstDWriteBaseOverlay parent; | |
| GstDWriteTimeOverlayPrivate *priv; | |
| }; | |
| static void gst_dwrite_time_overlay_finalize (GObject * object); | |
| static void gst_dwrite_time_overlay_set_property (GObject * object, | |
| guint prop_id, const GValue * value, GParamSpec * pspec); | |
| static void gst_dwrite_time_overlay_get_property (GObject * object, | |
| guint prop_id, GValue * value, GParamSpec * pspec); | |
| static gboolean gst_dwrite_time_overlay_sink_event (GstBaseTransform * trans, | |
| GstEvent * event); | |
| static gboolean gst_dwrite_time_overlay_start (GstBaseTransform * overlay); | |
| static WString gst_dwrite_time_overlay_get_text (GstDWriteBaseOverlay * overlay, | |
| const WString & default_text, GstBuffer * buffer); | |
| #define gst_dwrite_time_overlay_parent_class parent_class | |
| G_DEFINE_TYPE (GstDWriteTimeOverlay, gst_dwrite_time_overlay, | |
| GST_TYPE_DWRITE_BASE_OVERLAY); | |
| static void | |
| gst_dwrite_time_overlay_class_init (GstDWriteTimeOverlayClass * klass) | |
| { | |
| GObjectClass *object_class = G_OBJECT_CLASS (klass); | |
| GstElementClass *element_class = GST_ELEMENT_CLASS (klass); | |
| GstBaseTransformClass *trans_class = GST_BASE_TRANSFORM_CLASS (klass); | |
| GstDWriteBaseOverlayClass *overlay_class = | |
| GST_DWRITE_BASE_OVERLAY_CLASS (klass); | |
| object_class->finalize = gst_dwrite_time_overlay_finalize; | |
| object_class->set_property = gst_dwrite_time_overlay_set_property; | |
| object_class->get_property = gst_dwrite_time_overlay_get_property; | |
| g_object_class_install_property (object_class, PROP_TIME_LINE, | |
| g_param_spec_enum ("time-mode", "Time Mode", "What time to show", | |
| GST_TYPE_DWrite_TIME_OVERLAY_TIME_LINE, DEFAULT_TIME_LINE, | |
| (GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS))); | |
| g_object_class_install_property (object_class, PROP_DATETIME_EPOCH, | |
| g_param_spec_boxed ("datetime-epoch", "Datetime Epoch", | |
| "When showing times as dates, the initial date from which time " | |
| "is counted, if not specified prime epoch is used (1900-01-01)", | |
| G_TYPE_DATE_TIME, | |
| (GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS))); | |
| g_object_class_install_property (object_class, PROP_DATETIME_FORMAT, | |
| g_param_spec_string ("datetime-format", "Datetime Format", | |
| "When showing times as dates, the format to render date and time in", | |
| DEFAULT_DATETIME_FORMAT, | |
| (GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS))); | |
| g_object_class_install_property (object_class, PROP_SHOW_TIMES_AS_DATES, | |
| g_param_spec_boolean ("show-times-as-dates", "Show times as dates", | |
| "Whether to display times, counted from datetime-epoch, as dates", | |
| DEFAULT_SHOW_TIMES_AS_DATES, | |
| (GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS))); | |
| g_object_class_install_property (object_class, PROP_REFERENCE_TIMESTAMP_CAPS, | |
| g_param_spec_boxed ("reference-timestamp-caps", | |
| "Reference Timestamp Caps", | |
| "Caps to use for the reference timestamp time mode", | |
| GST_TYPE_CAPS, | |
| (GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS))); | |
| gst_element_class_set_static_metadata (element_class, | |
| "DirectWrite Time Overlay", "Filter/Editor/Video", | |
| "Overlays buffer time stamps on a video stream", | |
| "Seungha Yang <seungha@centricular.com>"); | |
| trans_class->sink_event = | |
| GST_DEBUG_FUNCPTR (gst_dwrite_time_overlay_sink_event); | |
| trans_class->start = GST_DEBUG_FUNCPTR (gst_dwrite_time_overlay_start); | |
| overlay_class->get_text = | |
| GST_DEBUG_FUNCPTR (gst_dwrite_time_overlay_get_text); | |
| GST_DEBUG_CATEGORY_INIT (dwrite_time_overlay_debug, | |
| "dwritetimeoverlay", 0, "dwritetimeoverlay"); | |
| gst_type_mark_as_plugin_api (GST_TYPE_DWrite_TIME_OVERLAY_TIME_LINE, | |
| (GstPluginAPIFlags) 0); | |
| } | |
| static void | |
| gst_dwrite_time_overlay_init (GstDWriteTimeOverlay * self) | |
| { | |
| g_object_set (self, "text-alignment", DWRITE_TEXT_ALIGNMENT_LEADING, | |
| "paragraph-alignment", DWRITE_PARAGRAPH_ALIGNMENT_NEAR, nullptr); | |
| self->priv = new GstDWriteTimeOverlayPrivate (); | |
| } | |
| static void | |
| gst_dwrite_time_overlay_finalize (GObject * object) | |
| { | |
| GstDWriteTimeOverlay *self = GST_DWRITE_TIME_OVERLAY (object); | |
| delete self->priv; | |
| G_OBJECT_CLASS (parent_class)->finalize (object); | |
| } | |
| static void | |
| gst_dwrite_time_overlay_set_property (GObject * object, guint prop_id, | |
| const GValue * value, GParamSpec * pspec) | |
| { | |
| GstDWriteTimeOverlay *self = GST_DWRITE_TIME_OVERLAY (object); | |
| GstDWriteTimeOverlayPrivate *priv = self->priv; | |
| std::lock_guard < std::mutex > lk (priv->lock); | |
| switch (prop_id) { | |
| case PROP_TIME_LINE: | |
| priv->time_line = (GstDWriteTimeOverlayTimeLine) g_value_get_enum (value); | |
| break; | |
| case PROP_SHOW_TIMES_AS_DATES: | |
| priv->show_times_as_dates = g_value_get_boolean (value); | |
| break; | |
| case PROP_DATETIME_EPOCH: | |
| g_date_time_unref (priv->datetime_epoch); | |
| priv->datetime_epoch = (GDateTime *) g_value_dup_boxed (value); | |
| break; | |
| case PROP_DATETIME_FORMAT: | |
| { | |
| const gchar *format = g_value_get_string (value); | |
| if (format) | |
| priv->datetime_format = format; | |
| else | |
| priv->datetime_format = DEFAULT_DATETIME_FORMAT; | |
| break; | |
| } | |
| case PROP_REFERENCE_TIMESTAMP_CAPS: | |
| gst_clear_caps (&priv->reference_timestamp_caps); | |
| priv->reference_timestamp_caps = (GstCaps *) g_value_dup_boxed (value); | |
| break; | |
| default: | |
| G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); | |
| break; | |
| } | |
| } | |
| static void | |
| gst_dwrite_time_overlay_get_property (GObject * object, guint prop_id, | |
| GValue * value, GParamSpec * pspec) | |
| { | |
| GstDWriteTimeOverlay *self = GST_DWRITE_TIME_OVERLAY (object); | |
| GstDWriteTimeOverlayPrivate *priv = self->priv; | |
| std::lock_guard < std::mutex > lk (priv->lock); | |
| switch (prop_id) { | |
| case PROP_TIME_LINE: | |
| g_value_set_enum (value, priv->time_line); | |
| break; | |
| case PROP_SHOW_TIMES_AS_DATES: | |
| g_value_set_boolean (value, priv->show_times_as_dates); | |
| break; | |
| case PROP_DATETIME_EPOCH: | |
| g_value_set_boxed (value, priv->datetime_epoch); | |
| break; | |
| case PROP_DATETIME_FORMAT: | |
| g_value_set_string (value, priv->datetime_format.c_str ()); | |
| break; | |
| case PROP_REFERENCE_TIMESTAMP_CAPS: | |
| g_value_set_boxed (value, priv->reference_timestamp_caps); | |
| break; | |
| default: | |
| G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); | |
| break; | |
| } | |
| } | |
| static gboolean | |
| gst_dwrite_time_overlay_start (GstBaseTransform * trans) | |
| { | |
| GstDWriteTimeOverlay *self = GST_DWRITE_TIME_OVERLAY (trans); | |
| GstDWriteTimeOverlayPrivate *priv = self->priv; | |
| priv->first_running_time = GST_CLOCK_TIME_NONE; | |
| priv->buffer_count = 0; | |
| return GST_BASE_TRANSFORM_CLASS (parent_class)->start (trans); | |
| } | |
| static gboolean | |
| gst_dwrite_time_overlay_sink_event (GstBaseTransform * trans, GstEvent * event) | |
| { | |
| GstDWriteTimeOverlay *self = GST_DWRITE_TIME_OVERLAY (trans); | |
| GstDWriteTimeOverlayPrivate *priv = self->priv; | |
| switch (GST_EVENT_TYPE (event)) { | |
| case GST_EVENT_FLUSH_STOP: | |
| priv->first_running_time = GST_CLOCK_TIME_NONE; | |
| break; | |
| default: | |
| break; | |
| } | |
| return GST_BASE_TRANSFORM_CLASS (parent_class)->sink_event (trans, event); | |
| } | |
| static WString | |
| gst_dwrite_time_overlay_render_time (GstDWriteTimeOverlay * self, | |
| GstClockTime time) | |
| { | |
| wchar_t text[256]; | |
| HRESULT hr; | |
| guint h, m, s, ms; | |
| if (!GST_CLOCK_TIME_IS_VALID (time)) | |
| return WString (); | |
| h = (guint) (time / (GST_SECOND * 60 * 60)); | |
| m = (guint) ((time / (GST_SECOND * 60)) % 60); | |
| s = (guint) ((time / GST_SECOND) % 60); | |
| ms = (guint) ((time % GST_SECOND) / (1000 * 1000)); | |
| hr = StringCbPrintfW (text, sizeof (text), L"%u:%02u:%02u.%03u", h, m, s, ms); | |
| if (FAILED (hr)) | |
| return WString (); | |
| return WString (text); | |
| } | |
| static WString | |
| gst_dwrite_time_overlay_get_text (GstDWriteBaseOverlay * overlay, | |
| const WString & default_text, GstBuffer * buffer) | |
| { | |
| GstDWriteTimeOverlay *self = GST_DWRITE_TIME_OVERLAY (overlay); | |
| GstDWriteTimeOverlayPrivate *priv = self->priv; | |
| WString time_str; | |
| WString ret; | |
| std::lock_guard < std::mutex > lk (priv->lock); | |
| gboolean show_buffer_count = FALSE; | |
| if (priv->time_line == GST_DWRITE_TIME_OVERLAY_TIME_LINE_TIME_CODE) { | |
| GstVideoTimeCodeMeta *tc_meta = | |
| gst_buffer_get_video_time_code_meta (buffer); | |
| if (!tc_meta) { | |
| GST_DEBUG_OBJECT (self, "buffer without valid timecode"); | |
| time_str = L"00:00:00:00"; | |
| } else { | |
| gchar *str = gst_video_time_code_to_string (&tc_meta->tc); | |
| GST_DEBUG_OBJECT (self, "buffer with timecode %s", str); | |
| time_str = gst_dwrite_string_to_wstring (str); | |
| g_free (str); | |
| } | |
| } else { | |
| GstBaseTransform *trans = GST_BASE_TRANSFORM (overlay); | |
| GstClockTime ts, ts_buffer; | |
| GstSegment *seg = &trans->segment; | |
| ts = ts_buffer = GST_BUFFER_TIMESTAMP (buffer); | |
| if (GST_CLOCK_TIME_IS_VALID (ts)) { | |
| switch (priv->time_line) { | |
| case GST_DWRITE_TIME_OVERLAY_TIME_LINE_STREAM_TIME: | |
| ts = gst_segment_to_stream_time (seg, GST_FORMAT_TIME, ts_buffer); | |
| break; | |
| case GST_DWRITE_TIME_OVERLAY_TIME_LINE_RUNNING_TIME: | |
| ts = gst_segment_to_running_time (seg, GST_FORMAT_TIME, ts_buffer); | |
| break; | |
| case GST_DWRITE_TIME_OVERLAY_TIME_LINE_ELAPSED_RUNNING_TIME: | |
| ts = gst_segment_to_running_time (seg, GST_FORMAT_TIME, ts_buffer); | |
| if (!GST_CLOCK_TIME_IS_VALID (priv->first_running_time)) | |
| priv->first_running_time = ts; | |
| ts -= priv->first_running_time; | |
| break; | |
| case GST_DWRITE_TIME_OVERLAY_TIME_LINE_REFERENCE_TIMESTAMP: | |
| { | |
| GstReferenceTimestampMeta *meta; | |
| if (priv->reference_timestamp_caps) { | |
| meta = gst_buffer_get_reference_timestamp_meta (buffer, | |
| priv->reference_timestamp_caps); | |
| if (meta) | |
| ts = meta->timestamp; | |
| else | |
| ts = 0; | |
| } else { | |
| ts = 0; | |
| } | |
| break; | |
| } | |
| case GST_DWRITE_TIME_OVERLAY_TIME_LINE_BUFFER_COUNT: | |
| show_buffer_count = TRUE; | |
| priv->buffer_count++; | |
| break; | |
| case GST_DWRITE_TIME_OVERLAY_TIME_LINE_BUFFER_OFFSET: | |
| show_buffer_count = TRUE; | |
| ts = gst_segment_to_running_time (seg, GST_FORMAT_TIME, ts_buffer); | |
| priv->buffer_count = gst_util_uint64_scale (ts, overlay->info.fps_n, | |
| overlay->info.fps_d * GST_SECOND); | |
| break; | |
| case GST_DWRITE_TIME_OVERLAY_TIME_LINE_BUFFER_TIME: | |
| default: | |
| ts = ts_buffer; | |
| break; | |
| } | |
| if (show_buffer_count) { | |
| time_str = std::to_wstring (priv->buffer_count); | |
| } else if (priv->show_times_as_dates) { | |
| GDateTime *datetime; | |
| gchar *str; | |
| datetime = | |
| g_date_time_add_seconds (priv->datetime_epoch, | |
| ((gdouble) ts) / GST_SECOND); | |
| str = g_date_time_format (datetime, priv->datetime_format.c_str ()); | |
| time_str = gst_dwrite_string_to_wstring (str); | |
| g_free (str); | |
| g_date_time_unref (datetime); | |
| } else { | |
| time_str = gst_dwrite_time_overlay_render_time (self, ts); | |
| } | |
| } | |
| } | |
| if (default_text.empty ()) | |
| return time_str; | |
| return default_text + WString (L" ") + time_str; | |
| } |