| /* GStreamer | |
| * Copyright (C) 2020 Mathieu Duponchelle <mathieu@centricular.com> | |
| * 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 "gstdwritetextoverlay.h" | |
| #include <gst/base/base.h> | |
| #include <caption.h> | |
| #include <mutex> | |
| GST_DEBUG_CATEGORY_STATIC (dwrite_text_overlay_debug); | |
| #define GST_CAT_DEFAULT dwrite_text_overlay_debug | |
| enum | |
| { | |
| PROP_0, | |
| PROP_ENABLE_CC, | |
| PROP_CC_FIELD, | |
| PROP_CC_TIMEOUT, | |
| PROP_REMOVE_CC_META, | |
| }; | |
| /* *INDENT-OFF* */ | |
| static std::vector<GParamSpec *> _pspec; | |
| /* *INDENT-ON* */ | |
| #define DEFAULT_ENABLE_CC TRUE | |
| #define DEFAULT_CC_FIELD -1 | |
| #define DEFAULT_CC_TIMEOUT GST_CLOCK_TIME_NONE | |
| #define DEFAULT_REMOVE_CC_META FALSE | |
| /* *INDENT-OFF* */ | |
| struct GstDWriteTextOverlayPrivate | |
| { | |
| std::mutex lock; | |
| caption_frame_t frame; | |
| GstClockTime caption_running_time; | |
| GstClockTime running_time; | |
| guint8 selected_field; | |
| std::string closed_caption; | |
| std::string text; | |
| /* properties */ | |
| gboolean enable_cc = DEFAULT_ENABLE_CC; | |
| gint field = DEFAULT_CC_FIELD; | |
| GstClockTime timeout = DEFAULT_CC_TIMEOUT; | |
| gboolean remove_caption_meta = DEFAULT_REMOVE_CC_META; | |
| }; | |
| /* *INDENT-ON* */ | |
| struct _GstDWriteTextOverlay | |
| { | |
| GstDWriteBaseOverlay parent; | |
| GstDWriteTextOverlayPrivate *priv; | |
| }; | |
| static void gst_dwrite_text_overlay_finalize (GObject * object); | |
| static void gst_dwrite_text_overlay_set_property (GObject * object, | |
| guint prop_id, const GValue * value, GParamSpec * pspec); | |
| static void gst_dwrite_text_overlay_get_property (GObject * object, | |
| guint prop_id, GValue * value, GParamSpec * pspec); | |
| static gboolean gst_dwrite_text_overlay_start (GstBaseTransform * trans); | |
| static gboolean gst_dwrite_text_overlay_sink_event (GstBaseTransform * trans, | |
| GstEvent * event); | |
| static WString gst_dwrite_text_overlay_get_text (GstDWriteBaseOverlay * overlay, | |
| const WString & default_text, GstBuffer * buffer); | |
| static void | |
| gst_dwrite_text_overlay_after_transform (GstDWriteBaseOverlay * overlay, | |
| GstBuffer * buffer); | |
| #define gst_dwrite_text_overlay_parent_class parent_class | |
| G_DEFINE_TYPE (GstDWriteTextOverlay, gst_dwrite_text_overlay, | |
| GST_TYPE_DWRITE_BASE_OVERLAY); | |
| static void | |
| gst_dwrite_text_overlay_class_init (GstDWriteTextOverlayClass * 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_text_overlay_finalize; | |
| object_class->set_property = gst_dwrite_text_overlay_set_property; | |
| object_class->get_property = gst_dwrite_text_overlay_get_property; | |
| gst_dwrite_text_overlay_build_param_specs (_pspec); | |
| for (guint i = 0; i < _pspec.size (); i++) | |
| g_object_class_install_property (object_class, i + 1, _pspec[i]); | |
| gst_element_class_set_static_metadata (element_class, | |
| "DirectWrite Text Overlay", "Filter/Editor/Video", | |
| "Adds text strings on top of a video buffer", | |
| "Seungha Yang <seungha@centricular.com>"); | |
| trans_class->start = GST_DEBUG_FUNCPTR (gst_dwrite_text_overlay_start); | |
| trans_class->sink_event = | |
| GST_DEBUG_FUNCPTR (gst_dwrite_text_overlay_sink_event); | |
| overlay_class->get_text = | |
| GST_DEBUG_FUNCPTR (gst_dwrite_text_overlay_get_text); | |
| overlay_class->after_transform = | |
| GST_DEBUG_FUNCPTR (gst_dwrite_text_overlay_after_transform); | |
| GST_DEBUG_CATEGORY_INIT (dwrite_text_overlay_debug, | |
| "dwritetextoverlay", 0, "dwritetextoverlay"); | |
| } | |
| static void | |
| gst_dwrite_text_overlay_init (GstDWriteTextOverlay * self) | |
| { | |
| self->priv = new GstDWriteTextOverlayPrivate (); | |
| self->priv->closed_caption.reserve (CAPTION_FRAME_TEXT_BYTES); | |
| g_object_set (self, "text-alignment", DWRITE_TEXT_ALIGNMENT_CENTER, | |
| "paragraph-alignment", DWRITE_PARAGRAPH_ALIGNMENT_FAR, | |
| "font-size", 20.0, nullptr); | |
| } | |
| static void | |
| gst_dwrite_text_overlay_finalize (GObject * object) | |
| { | |
| GstDWriteTextOverlay *self = GST_DWRITE_TEXT_OVERLAY (object); | |
| delete self->priv; | |
| G_OBJECT_CLASS (parent_class)->finalize (object); | |
| } | |
| static void | |
| gst_dwrite_text_overlay_set_property (GObject * object, guint prop_id, | |
| const GValue * value, GParamSpec * pspec) | |
| { | |
| GstDWriteTextOverlay *self = GST_DWRITE_TEXT_OVERLAY (object); | |
| GstDWriteTextOverlayPrivate *priv = self->priv; | |
| std::lock_guard < std::mutex > lk (priv->lock); | |
| switch (prop_id) { | |
| case PROP_ENABLE_CC: | |
| priv->enable_cc = g_value_get_boolean (value); | |
| break; | |
| case PROP_CC_FIELD: | |
| priv->field = g_value_get_int (value); | |
| if (priv->field == -1) { | |
| priv->selected_field = 0xff; | |
| } else { | |
| priv->selected_field = (guint) priv->field; | |
| } | |
| break; | |
| case PROP_CC_TIMEOUT: | |
| priv->timeout = g_value_get_uint64 (value); | |
| break; | |
| case PROP_REMOVE_CC_META: | |
| priv->remove_caption_meta = g_value_get_boolean (value); | |
| break; | |
| default: | |
| G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); | |
| break; | |
| } | |
| } | |
| static void | |
| gst_dwrite_text_overlay_get_property (GObject * object, guint prop_id, | |
| GValue * value, GParamSpec * pspec) | |
| { | |
| GstDWriteTextOverlay *self = GST_DWRITE_TEXT_OVERLAY (object); | |
| GstDWriteTextOverlayPrivate *priv = self->priv; | |
| std::lock_guard < std::mutex > lk (priv->lock); | |
| switch (prop_id) { | |
| case PROP_ENABLE_CC: | |
| g_value_set_boolean (value, priv->enable_cc); | |
| break; | |
| case PROP_CC_FIELD: | |
| g_value_set_int (value, priv->field); | |
| break; | |
| case PROP_CC_TIMEOUT: | |
| g_value_set_uint64 (value, priv->timeout); | |
| break; | |
| case PROP_REMOVE_CC_META: | |
| g_value_set_boolean (value, priv->remove_caption_meta); | |
| break; | |
| default: | |
| G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); | |
| break; | |
| } | |
| } | |
| static gboolean | |
| gst_dwrite_text_overlay_start (GstBaseTransform * trans) | |
| { | |
| GstDWriteTextOverlay *self = GST_DWRITE_TEXT_OVERLAY (trans); | |
| GstDWriteTextOverlayPrivate *priv = self->priv; | |
| caption_frame_init (&priv->frame); | |
| priv->running_time = GST_CLOCK_TIME_NONE; | |
| priv->caption_running_time = GST_CLOCK_TIME_NONE; | |
| priv->selected_field = 0xff; | |
| priv->closed_caption.clear (); | |
| return GST_BASE_TRANSFORM_CLASS (parent_class)->start (trans); | |
| } | |
| static gboolean | |
| gst_dwrite_text_overlay_sink_event (GstBaseTransform * trans, GstEvent * event) | |
| { | |
| GstDWriteTextOverlay *self = GST_DWRITE_TEXT_OVERLAY (trans); | |
| GstDWriteTextOverlayPrivate *priv = self->priv; | |
| switch (GST_EVENT_TYPE (event)) { | |
| case GST_EVENT_SEGMENT: | |
| priv->caption_running_time = GST_CLOCK_TIME_NONE; | |
| priv->running_time = GST_CLOCK_TIME_NONE; | |
| break; | |
| default: | |
| break; | |
| } | |
| return GST_BASE_TRANSFORM_CLASS (parent_class)->sink_event (trans, event); | |
| } | |
| static guint | |
| gst_dwrite_text_overlay_extract_cdp (GstDWriteTextOverlay * self, | |
| const guint8 * cdp, guint cdp_len, guint * pos) | |
| { | |
| GstByteReader br; | |
| guint16 u16; | |
| guint8 u8; | |
| guint8 flags; | |
| guint len = 0; | |
| GST_TRACE_OBJECT (self, "Extracting CDP"); | |
| /* Header + footer length */ | |
| if (cdp_len < 11) { | |
| GST_WARNING_OBJECT (self, "cdp packet too short (%u). expected at " | |
| "least %u", cdp_len, 11); | |
| return 0; | |
| } | |
| gst_byte_reader_init (&br, cdp, cdp_len); | |
| u16 = gst_byte_reader_get_uint16_be_unchecked (&br); | |
| if (u16 != 0x9669) { | |
| GST_WARNING_OBJECT (self, "cdp packet does not have initial magic bytes " | |
| "of 0x9669"); | |
| return 0; | |
| } | |
| u8 = gst_byte_reader_get_uint8_unchecked (&br); | |
| if (u8 != cdp_len) { | |
| GST_WARNING_OBJECT (self, "cdp packet length (%u) does not match passed " | |
| "in value (%u)", u8, cdp_len); | |
| return 0; | |
| } | |
| /* skip framerate value */ | |
| gst_byte_reader_skip_unchecked (&br, 1); | |
| flags = gst_byte_reader_get_uint8_unchecked (&br); | |
| /* No cc_data? */ | |
| if ((flags & 0x40) == 0) { | |
| GST_DEBUG_OBJECT (self, "cdp packet does have any cc_data"); | |
| return 0; | |
| } | |
| /* cdp_hdr_sequence_cntr */ | |
| gst_byte_reader_skip_unchecked (&br, 2); | |
| /* skip timecode */ | |
| if (flags & 0x80) { | |
| if (gst_byte_reader_get_remaining (&br) < 5) { | |
| GST_WARNING_OBJECT (self, "cdp packet does not have enough data to " | |
| "contain a timecode (%u). Need at least 5 bytes", | |
| gst_byte_reader_get_remaining (&br)); | |
| return 0; | |
| } | |
| gst_byte_reader_skip_unchecked (&br, 5); | |
| } | |
| /* ccdata_present */ | |
| if (flags & 0x40) { | |
| guint8 cc_count; | |
| if (gst_byte_reader_get_remaining (&br) < 2) { | |
| GST_WARNING_OBJECT (self, "not enough data to contain valid cc_data"); | |
| return 0; | |
| } | |
| u8 = gst_byte_reader_get_uint8_unchecked (&br); | |
| if (u8 != 0x72) { | |
| GST_WARNING_OBJECT (self, "missing cc_data start code of 0x72, " | |
| "found 0x%02x", u8); | |
| return 0; | |
| } | |
| cc_count = gst_byte_reader_get_uint8_unchecked (&br); | |
| if ((cc_count & 0xe0) != 0xe0) { | |
| GST_WARNING_OBJECT (self, "reserved bits are not 0xe0, found 0x%02x", u8); | |
| return 0; | |
| } | |
| cc_count &= 0x1f; | |
| len = 3 * cc_count; | |
| if (gst_byte_reader_get_remaining (&br) < len) { | |
| GST_WARNING_OBJECT (self, "not enough bytes (%u) left for the " | |
| "number of byte triples (%u)", gst_byte_reader_get_remaining (&br), | |
| cc_count); | |
| return 0; | |
| } | |
| *pos = gst_byte_reader_get_pos (&br); | |
| } | |
| /* skip everything else we don't care about */ | |
| return len; | |
| } | |
| static void | |
| gst_dwrite_text_overlay_decode_cc_data (GstDWriteTextOverlay * self, | |
| const guint8 * data, guint len, GstClockTime running_time) | |
| { | |
| GstDWriteTextOverlayPrivate *priv = self->priv; | |
| GstByteReader br; | |
| GST_TRACE_OBJECT (self, "Decoding CC data"); | |
| gst_byte_reader_init (&br, data, len); | |
| while (gst_byte_reader_get_remaining (&br) >= 3) { | |
| guint8 cc_type; | |
| guint16 cc_data; | |
| cc_type = gst_byte_reader_get_uint8_unchecked (&br); | |
| cc_data = gst_byte_reader_get_uint16_be_unchecked (&br); | |
| if ((cc_type & 0x04) != 0x04) | |
| continue; | |
| cc_type = cc_type & 0x03; | |
| if (cc_type != 0x00 && cc_type != 0x01) | |
| continue; | |
| if (priv->selected_field == 0xff) { | |
| GST_INFO_OBJECT (self, "Selected field %d", cc_type); | |
| priv->selected_field = cc_type; | |
| } | |
| if (cc_type != priv->selected_field) | |
| continue; | |
| auto status = caption_frame_decode (&priv->frame, cc_data, 0.0); | |
| switch (status) { | |
| case LIBCAPTION_READY: | |
| { | |
| auto len = caption_frame_to_text (&priv->frame, | |
| &priv->closed_caption[0], FALSE); | |
| priv->closed_caption.resize (len); | |
| break; | |
| } | |
| case LIBCAPTION_CLEAR: | |
| priv->closed_caption.clear (); | |
| break; | |
| default: | |
| break; | |
| } | |
| priv->caption_running_time = running_time; | |
| } | |
| } | |
| static void | |
| gst_dwrite_text_overlay_decode_s334_1a (GstDWriteTextOverlay * self, | |
| const guint8 * data, guint len, GstClockTime running_time) | |
| { | |
| GstDWriteTextOverlayPrivate *priv = self->priv; | |
| GstByteReader br; | |
| GST_TRACE_OBJECT (self, "Decoding S334-1A"); | |
| gst_byte_reader_init (&br, data, len); | |
| while (gst_byte_reader_get_remaining (&br) >= 3) { | |
| guint8 cc_type; | |
| guint16 cc_data; | |
| cc_type = gst_byte_reader_get_uint8_unchecked (&br); | |
| cc_data = gst_byte_reader_get_uint16_be_unchecked (&br); | |
| cc_type = cc_type & 0x01; | |
| if (priv->selected_field == 0xff) { | |
| GST_INFO_OBJECT (self, "Selected field %d", cc_type); | |
| priv->selected_field = cc_type; | |
| } | |
| if (cc_type != priv->selected_field) | |
| continue; | |
| auto status = caption_frame_decode (&priv->frame, cc_data, 0.0); | |
| switch (status) { | |
| case LIBCAPTION_READY: | |
| { | |
| auto len = caption_frame_to_text (&priv->frame, | |
| &priv->closed_caption[0], FALSE); | |
| priv->closed_caption.resize (len); | |
| break; | |
| } | |
| case LIBCAPTION_CLEAR: | |
| priv->closed_caption.clear (); | |
| break; | |
| default: | |
| break; | |
| } | |
| priv->caption_running_time = running_time; | |
| } | |
| } | |
| static void | |
| gst_dwrite_text_overlay_decode_raw (GstDWriteTextOverlay * self, | |
| const guint8 * data, guint len, GstClockTime running_time) | |
| { | |
| GstDWriteTextOverlayPrivate *priv = self->priv; | |
| GstByteReader br; | |
| GST_TRACE_OBJECT (self, "Decoding CEA608 RAW"); | |
| gst_byte_reader_init (&br, data, len); | |
| while (gst_byte_reader_get_remaining (&br) >= 2) { | |
| guint16 cc_data; | |
| cc_data = gst_byte_reader_get_uint16_be_unchecked (&br); | |
| auto status = caption_frame_decode (&priv->frame, cc_data, 0.0); | |
| switch (status) { | |
| case LIBCAPTION_READY: | |
| { | |
| auto len = caption_frame_to_text (&priv->frame, | |
| &priv->closed_caption[0], FALSE); | |
| priv->closed_caption.resize (len); | |
| break; | |
| } | |
| case LIBCAPTION_CLEAR: | |
| priv->closed_caption.clear (); | |
| break; | |
| default: | |
| break; | |
| } | |
| priv->caption_running_time = running_time; | |
| } | |
| } | |
| static void | |
| xml_text (GMarkupParseContext * context, const gchar * text, gsize text_len, | |
| gpointer user_data, GError ** error) | |
| { | |
| gchar **accum = (gchar **) user_data; | |
| gchar *concat; | |
| if (*accum) { | |
| concat = g_strconcat (*accum, text, NULL); | |
| g_free (*accum); | |
| *accum = concat; | |
| } else { | |
| *accum = g_strdup (text); | |
| } | |
| } | |
| static gchar * | |
| gst_dwrite_text_overlay_strip_markup (GstDWriteTextOverlay * self, | |
| const gchar * markup) | |
| { | |
| GMarkupParser parser = { 0, }; | |
| GMarkupParseContext *context; | |
| gchar *accum = nullptr; | |
| parser.text = xml_text; | |
| context = g_markup_parse_context_new (&parser, | |
| (GMarkupParseFlags) 0, &accum, nullptr); | |
| if (!g_markup_parse_context_parse (context, "<root>", 6, nullptr)) | |
| goto error; | |
| if (!g_markup_parse_context_parse (context, markup, strlen (markup), nullptr)) | |
| goto error; | |
| if (!g_markup_parse_context_parse (context, "</root>", 7, nullptr)) | |
| goto error; | |
| if (!g_markup_parse_context_end_parse (context, nullptr)) | |
| goto error; | |
| done: | |
| g_markup_parse_context_free (context); | |
| return accum; | |
| error: | |
| g_free (accum); | |
| accum = nullptr; | |
| goto done; | |
| } | |
| static void | |
| gst_dwrite_text_overlay_extract_meta (GstDWriteTextOverlay * self, | |
| GstDWriteSubtitleMeta * meta) | |
| { | |
| GstDWriteTextOverlayPrivate *priv = self->priv; | |
| GstCaps *caps = nullptr; | |
| GstStructure *s; | |
| const gchar *format; | |
| std::string str; | |
| GstMapInfo info; | |
| if (!meta || !meta->subtitle || !meta->stream) | |
| return; | |
| caps = gst_stream_get_caps (meta->stream); | |
| if (!caps) | |
| return; | |
| if (gst_buffer_get_size (meta->subtitle) == 0) | |
| goto out; | |
| if (!gst_buffer_map (meta->subtitle, &info, GST_MAP_READ)) | |
| goto out; | |
| s = gst_caps_get_structure (caps, 0); | |
| format = gst_structure_get_string (s, "format"); | |
| /* TODO: parse pango attributs and make layout based on that */ | |
| if (g_strcmp0 (format, "pango-markup") == 0) { | |
| gchar *stripped = gst_dwrite_text_overlay_strip_markup (self, | |
| (gchar *) info.data); | |
| gst_buffer_unmap (meta->subtitle, &info); | |
| if (!stripped) | |
| goto out; | |
| if (priv->text.empty ()) { | |
| priv->text = stripped; | |
| } else { | |
| priv->text += "\n"; | |
| priv->text += stripped; | |
| } | |
| } else { | |
| std::string ret; | |
| ret.resize (info.size); | |
| memcpy (&ret[0], info.data, info.size); | |
| gst_buffer_unmap (meta->subtitle, &info); | |
| auto len = strlen (ret.c_str ()); | |
| ret.resize (len); | |
| if (priv->text.empty ()) | |
| priv->text = ret; | |
| else | |
| priv->text += " " + ret; | |
| } | |
| out: | |
| gst_clear_caps (&caps); | |
| } | |
| static gboolean | |
| gst_dwrite_text_overlay_foreach_meta (GstBuffer * buffer, GstMeta ** meta, | |
| GstDWriteTextOverlay * self) | |
| { | |
| GstDWriteTextOverlayPrivate *priv = self->priv; | |
| GstVideoCaptionMeta *cc_meta; | |
| if (priv->enable_cc && (*meta)->info->api == GST_VIDEO_CAPTION_META_API_TYPE) { | |
| cc_meta = (GstVideoCaptionMeta *) (*meta); | |
| switch (cc_meta->caption_type) { | |
| case GST_VIDEO_CAPTION_TYPE_CEA608_RAW: | |
| gst_dwrite_text_overlay_decode_raw (self, cc_meta->data, cc_meta->size, | |
| priv->running_time); | |
| break; | |
| case GST_VIDEO_CAPTION_TYPE_CEA608_S334_1A: | |
| gst_dwrite_text_overlay_decode_s334_1a (self, cc_meta->data, | |
| cc_meta->size, priv->running_time); | |
| break; | |
| case GST_VIDEO_CAPTION_TYPE_CEA708_RAW: | |
| gst_dwrite_text_overlay_decode_cc_data (self, cc_meta->data, | |
| cc_meta->size, priv->running_time); | |
| break; | |
| case GST_VIDEO_CAPTION_TYPE_CEA708_CDP: | |
| { | |
| guint len, pos = 0; | |
| len = gst_dwrite_text_overlay_extract_cdp (self, cc_meta->data, | |
| cc_meta->size, &pos); | |
| if (len > 0) { | |
| gst_dwrite_text_overlay_decode_cc_data (self, cc_meta->data + pos, | |
| len, priv->running_time); | |
| } | |
| break; | |
| } | |
| default: | |
| break; | |
| } | |
| } else if ((*meta)->info->api == GST_DWRITE_SUBTITLE_META_API_TYPE) { | |
| GstDWriteSubtitleMeta *smeta = (GstDWriteSubtitleMeta *) (*meta); | |
| gst_dwrite_text_overlay_extract_meta (self, smeta); | |
| } | |
| return TRUE; | |
| } | |
| static WString | |
| gst_dwrite_text_overlay_get_text (GstDWriteBaseOverlay * overlay, | |
| const WString & default_text, GstBuffer * buffer) | |
| { | |
| GstBaseTransform *trans = GST_BASE_TRANSFORM (overlay); | |
| GstDWriteTextOverlay *self = GST_DWRITE_TEXT_OVERLAY (overlay); | |
| GstDWriteTextOverlayPrivate *priv = self->priv; | |
| std::lock_guard < std::mutex > lk (priv->lock); | |
| WString text_wide; | |
| priv->text.clear (); | |
| priv->running_time = gst_segment_to_running_time (&trans->segment, | |
| GST_FORMAT_TIME, GST_BUFFER_PTS (buffer)); | |
| gst_buffer_foreach_meta (buffer, | |
| (GstBufferForeachMetaFunc) gst_dwrite_text_overlay_foreach_meta, self); | |
| if (priv->enable_cc) { | |
| if (GST_CLOCK_TIME_IS_VALID (priv->timeout) && | |
| GST_CLOCK_TIME_IS_VALID (priv->running_time) && | |
| GST_CLOCK_TIME_IS_VALID (priv->caption_running_time) && | |
| priv->running_time >= priv->caption_running_time) { | |
| GstClockTime diff = priv->running_time - priv->caption_running_time; | |
| if (diff > priv->timeout) { | |
| GST_INFO_OBJECT (self, "Reached timeout, clearing text"); | |
| priv->closed_caption.clear (); | |
| } | |
| } | |
| } else { | |
| priv->closed_caption.clear (); | |
| } | |
| if (priv->closed_caption.empty () && priv->text.empty ()) | |
| return default_text; | |
| if (!priv->text.empty ()) | |
| text_wide = gst_dwrite_string_to_wstring (priv->text); | |
| if (!priv->closed_caption.empty ()) { | |
| if (!text_wide.empty ()) | |
| text_wide += L"\n"; | |
| text_wide += gst_dwrite_string_to_wstring (priv->closed_caption); | |
| } | |
| if (default_text.empty ()) | |
| return text_wide; | |
| return default_text + WString (L" ") + text_wide; | |
| } | |
| static gboolean | |
| gst_dwrite_text_overlay_remove_meta (GstBuffer * buffer, GstMeta ** meta, | |
| GstDWriteTextOverlay * self) | |
| { | |
| GstDWriteTextOverlayPrivate *priv = self->priv; | |
| if ((*meta)->info->api == GST_VIDEO_CAPTION_META_API_TYPE && | |
| priv->enable_cc && priv->remove_caption_meta) { | |
| GST_TRACE_OBJECT (self, "Removing caption meta"); | |
| *meta = nullptr; | |
| } else if ((*meta)->info->api == GST_DWRITE_SUBTITLE_META_API_TYPE) { | |
| *meta = nullptr; | |
| } | |
| return TRUE; | |
| } | |
| static void | |
| gst_dwrite_text_overlay_after_transform (GstDWriteBaseOverlay * overlay, | |
| GstBuffer * buffer) | |
| { | |
| GstDWriteTextOverlay *self = GST_DWRITE_TEXT_OVERLAY (overlay); | |
| GstDWriteTextOverlayPrivate *priv = self->priv; | |
| std::lock_guard < std::mutex > lk (priv->lock); | |
| gst_buffer_foreach_meta (buffer, | |
| (GstBufferForeachMetaFunc) gst_dwrite_text_overlay_remove_meta, self); | |
| } | |
| void | |
| gst_dwrite_text_overlay_build_param_specs (std::vector < GParamSpec * >&pspec) | |
| { | |
| pspec.push_back (g_param_spec_boolean ("enable-cc", "Enable CC", | |
| "Enable closed caption rendering", | |
| DEFAULT_ENABLE_CC, | |
| (GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS))); | |
| pspec.push_back (g_param_spec_int ("cc-field", "CC Field", | |
| "The closed caption field to render when available, (-1 = automatic)", | |
| -1, 1, DEFAULT_CC_FIELD, | |
| (GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS))); | |
| pspec.push_back (g_param_spec_uint64 ("cc-timeout", "CC Timeout", | |
| "Duration after which to erase overlay when no cc data has arrived " | |
| "for the selected field, in nanoseconds unit", 16 * GST_SECOND, | |
| GST_CLOCK_TIME_NONE, DEFAULT_CC_TIMEOUT, | |
| (GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS))); | |
| pspec.push_back (g_param_spec_boolean ("remove-cc-meta", "Remove CC Meta", | |
| "Remove caption meta from output buffers " | |
| "when closed caption rendering is enabled", | |
| DEFAULT_REMOVE_CC_META, | |
| (GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS))); | |
| } |