/* * Copyright (C) 2009 Sebastian Dröge * * 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., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. */ /** * SECTION:element-subtitleoverlay * * #GstBin that auto-magically overlays a video stream with subtitles by * autoplugging the required elements. * * It supports raw, timestamped text, different textual subtitle formats and * DVD subpicture subtitles. * * * Examples * |[ * gst-launch -v filesrc location=test.mkv ! matroskademux name=demux ! "video/x-h264" ! queue2 ! decodebin ! subtitleoverlay name=overlay ! videoconvert ! autovideosink demux. ! "video/x-dvd-subpicture" ! queue2 ! overlay. * ]| This will play back the given Matroska file with h264 video and subpicture subtitles. * */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include "gstsubtitleoverlay.h" #include #include #include #include "gst/glib-compat-private.h" GST_DEBUG_CATEGORY_STATIC (subtitle_overlay_debug); #define GST_CAT_DEFAULT subtitle_overlay_debug #define IS_SUBTITLE_CHAIN_IGNORE_ERROR(flow) \ G_UNLIKELY (flow == GST_FLOW_ERROR || flow == GST_FLOW_NOT_NEGOTIATED) #define IS_VIDEO_CHAIN_IGNORE_ERROR(flow) \ G_UNLIKELY (flow == GST_FLOW_ERROR) static GstStaticPadTemplate srctemplate = GST_STATIC_PAD_TEMPLATE ("src", GST_PAD_SRC, GST_PAD_ALWAYS, GST_STATIC_CAPS_ANY); static GstStaticPadTemplate video_sinktemplate = GST_STATIC_PAD_TEMPLATE ("video_sink", GST_PAD_SINK, GST_PAD_ALWAYS, GST_STATIC_CAPS_ANY); static GstStaticPadTemplate subtitle_sinktemplate = GST_STATIC_PAD_TEMPLATE ("subtitle_sink", GST_PAD_SINK, GST_PAD_ALWAYS, GST_STATIC_CAPS_ANY); enum { PROP_0, PROP_SILENT, PROP_FONT_DESC, PROP_SUBTITLE_ENCODING }; #define gst_subtitle_overlay_parent_class parent_class G_DEFINE_TYPE (GstSubtitleOverlay, gst_subtitle_overlay, GST_TYPE_BIN); static GQuark _subtitle_overlay_event_marker_id = 0; static void do_async_start (GstSubtitleOverlay * self) { if (!self->do_async) { GstMessage *msg = gst_message_new_async_start (GST_OBJECT_CAST (self)); GST_DEBUG_OBJECT (self, "Posting async-start"); GST_BIN_CLASS (parent_class)->handle_message (GST_BIN_CAST (self), msg); self->do_async = TRUE; } } static void do_async_done (GstSubtitleOverlay * self) { if (self->do_async) { GstMessage *msg = gst_message_new_async_done (GST_OBJECT_CAST (self), GST_CLOCK_TIME_NONE); GST_DEBUG_OBJECT (self, "Posting async-done"); GST_BIN_CLASS (parent_class)->handle_message (GST_BIN_CAST (self), msg); self->do_async = FALSE; } } static GstPadProbeReturn _pad_blocked_cb (GstPad * pad, GstPadProbeInfo * info, gpointer user_data); static void block_video (GstSubtitleOverlay * self) { if (self->video_block_id != 0) return; if (self->video_block_pad) { self->video_block_id = gst_pad_add_probe (self->video_block_pad, GST_PAD_PROBE_TYPE_BLOCK_DOWNSTREAM, _pad_blocked_cb, self, NULL); } } static void unblock_video (GstSubtitleOverlay * self) { if (self->video_block_id) { gst_pad_remove_probe (self->video_block_pad, self->video_block_id); self->video_sink_blocked = FALSE; self->video_block_id = 0; } } static void block_subtitle (GstSubtitleOverlay * self) { if (self->subtitle_block_id != 0) return; if (self->subtitle_block_pad) { self->subtitle_block_id = gst_pad_add_probe (self->subtitle_block_pad, GST_PAD_PROBE_TYPE_BLOCK_DOWNSTREAM, _pad_blocked_cb, self, NULL); } } static void unblock_subtitle (GstSubtitleOverlay * self) { if (self->subtitle_block_id) { gst_pad_remove_probe (self->subtitle_block_pad, self->subtitle_block_id); self->subtitle_sink_blocked = FALSE; self->subtitle_block_id = 0; } } static void gst_subtitle_overlay_finalize (GObject * object) { GstSubtitleOverlay *self = GST_SUBTITLE_OVERLAY (object); if (self->lock) { g_mutex_free (self->lock); self->lock = NULL; } if (self->factories_lock) { g_mutex_free (self->factories_lock); self->factories_lock = NULL; } if (self->factories) gst_plugin_feature_list_free (self->factories); self->factories = NULL; gst_caps_replace (&self->factory_caps, NULL); if (self->font_desc) { g_free (self->font_desc); self->font_desc = NULL; } if (self->encoding) { g_free (self->encoding); self->encoding = NULL; } G_OBJECT_CLASS (parent_class)->finalize (object); } static gboolean _is_renderer (GstElementFactory * factory) { const gchar *klass, *name; klass = gst_element_factory_get_klass (factory); name = gst_plugin_feature_get_name (GST_PLUGIN_FEATURE_CAST (factory)); if (strstr (klass, "Overlay/Subtitle") != NULL || strstr (klass, "Overlay/SubPicture") != NULL) return TRUE; if (strcmp (name, "textoverlay") == 0) return TRUE; return FALSE; } static gboolean _is_parser (GstElementFactory * factory) { const gchar *klass; klass = gst_element_factory_get_klass (factory); if (strstr (klass, "Parser/Subtitle") != NULL) return TRUE; return FALSE; } static const gchar *const _sub_pad_names[] = { "subpicture", "subpicture_sink", "text", "text_sink", "subtitle_sink", "subtitle" }; static inline gboolean _is_raw_video (GstStructure * s) { const gchar *name; name = gst_structure_get_name (s); if (g_str_equal (name, "video/x-raw")) return TRUE; return FALSE; } static gboolean _is_video_pad (GstPad * pad, gboolean * hw_accelerated) { GstPad *peer = gst_pad_get_peer (pad); GstCaps *caps; gboolean ret; const gchar *name; if (peer) { caps = gst_pad_get_current_caps (peer); if (!caps) { caps = gst_pad_query_caps (peer, NULL); } gst_object_unref (peer); } else { caps = gst_pad_query_caps (pad, NULL); } name = gst_structure_get_name (gst_caps_get_structure (caps, 0)); if (g_str_equal (name, "video/x-raw")) { ret = TRUE; if (hw_accelerated) *hw_accelerated = FALSE; } else if (g_str_has_prefix (name, "video/x-surface")) { ret = TRUE; if (hw_accelerated) *hw_accelerated = TRUE; } else { ret = FALSE; if (hw_accelerated) *hw_accelerated = FALSE; } gst_caps_unref (caps); return ret; } static GstCaps * _get_sub_caps (GstElementFactory * factory) { const GList *templates; GList *walk; gboolean is_parser = _is_parser (factory); templates = gst_element_factory_get_static_pad_templates (factory); for (walk = (GList *) templates; walk; walk = g_list_next (walk)) { GstStaticPadTemplate *templ = walk->data; if (templ->direction == GST_PAD_SINK && templ->presence == GST_PAD_ALWAYS) { gboolean found = FALSE; if (is_parser) { found = TRUE; } else { guint i; for (i = 0; i < G_N_ELEMENTS (_sub_pad_names); i++) { if (strcmp (templ->name_template, _sub_pad_names[i]) == 0) { found = TRUE; break; } } } if (found) return gst_static_caps_get (&templ->static_caps); } } return NULL; } static gboolean _factory_filter (GstPluginFeature * feature, GstCaps ** subcaps) { GstElementFactory *factory; guint rank; const gchar *name; const GList *templates; GList *walk; gboolean is_renderer; GstCaps *templ_caps = NULL; gboolean have_video_sink = FALSE; /* we only care about element factories */ if (!GST_IS_ELEMENT_FACTORY (feature)) return FALSE; factory = GST_ELEMENT_FACTORY_CAST (feature); /* only select elements with autoplugging rank or textoverlay */ name = gst_plugin_feature_get_name (feature); rank = gst_plugin_feature_get_rank (feature); if (strcmp ("textoverlay", name) != 0 && rank < GST_RANK_MARGINAL) return FALSE; /* Check if it's a renderer or a parser */ if (_is_renderer (factory)) { is_renderer = TRUE; } else if (_is_parser (factory)) { is_renderer = FALSE; } else { return FALSE; } /* Check if there's a video sink in case of a renderer */ if (is_renderer) { templates = gst_element_factory_get_static_pad_templates (factory); for (walk = (GList *) templates; walk; walk = g_list_next (walk)) { GstStaticPadTemplate *templ = walk->data; /* we only care about the always-sink templates */ if (templ->direction == GST_PAD_SINK && templ->presence == GST_PAD_ALWAYS) { if (strcmp (templ->name_template, "video") == 0 || strcmp (templ->name_template, "video_sink") == 0) { have_video_sink = TRUE; } } } } templ_caps = _get_sub_caps (factory); if (is_renderer && have_video_sink && templ_caps) { GST_DEBUG ("Found renderer element %s (%s) with caps %" GST_PTR_FORMAT, gst_element_factory_get_longname (factory), gst_plugin_feature_get_name (feature), templ_caps); *subcaps = gst_caps_merge (*subcaps, templ_caps); return TRUE; } else if (!is_renderer && !have_video_sink && templ_caps) { GST_DEBUG ("Found parser element %s (%s) with caps %" GST_PTR_FORMAT, gst_element_factory_get_longname (factory), gst_plugin_feature_get_name (feature), templ_caps); *subcaps = gst_caps_merge (*subcaps, templ_caps); return TRUE; } else { if (templ_caps) gst_caps_unref (templ_caps); return FALSE; } } /* Call with factories_lock! */ static gboolean gst_subtitle_overlay_update_factory_list (GstSubtitleOverlay * self) { GstRegistry *registry; guint cookie; registry = gst_registry_get (); cookie = gst_registry_get_feature_list_cookie (registry); if (!self->factories || self->factories_cookie != cookie) { GstCaps *subcaps; GList *factories; subcaps = gst_caps_new_empty (); factories = gst_registry_feature_filter (registry, (GstPluginFeatureFilter) _factory_filter, FALSE, &subcaps); GST_DEBUG_OBJECT (self, "Created factory caps: %" GST_PTR_FORMAT, subcaps); gst_caps_replace (&self->factory_caps, subcaps); gst_caps_unref (subcaps); if (self->factories) gst_plugin_feature_list_free (self->factories); self->factories = factories; self->factories_cookie = cookie; } return (self->factories != NULL); } G_LOCK_DEFINE_STATIC (_factory_caps); static GstCaps *_factory_caps = NULL; static guint32 _factory_caps_cookie = 0; GstCaps * gst_subtitle_overlay_create_factory_caps (void) { GstRegistry *registry; GList *factories; GstCaps *subcaps = NULL; guint cookie; registry = gst_registry_get (); cookie = gst_registry_get_feature_list_cookie (registry); G_LOCK (_factory_caps); if (!_factory_caps || _factory_caps_cookie != cookie) { if (_factory_caps) gst_caps_unref (_factory_caps); _factory_caps = gst_caps_new_empty (); factories = gst_registry_feature_filter (registry, (GstPluginFeatureFilter) _factory_filter, FALSE, &_factory_caps); GST_DEBUG ("Created factory caps: %" GST_PTR_FORMAT, _factory_caps); gst_plugin_feature_list_free (factories); _factory_caps_cookie = cookie; } subcaps = gst_caps_ref (_factory_caps); G_UNLOCK (_factory_caps); return subcaps; } static gboolean check_factory_for_caps (GstElementFactory * factory, const GstCaps * caps) { GstCaps *fcaps = _get_sub_caps (factory); gboolean ret = (fcaps) ? gst_caps_can_intersect (fcaps, caps) : FALSE; if (fcaps) gst_caps_unref (fcaps); if (ret) gst_object_ref (factory); return ret; } static GList * gst_subtitle_overlay_get_factories_for_caps (const GList * list, const GstCaps * caps) { const GList *walk = list; GList *result = NULL; while (walk) { GstElementFactory *factory = walk->data; walk = g_list_next (walk); if (check_factory_for_caps (factory, caps)) { result = g_list_prepend (result, factory); } } return result; } static gint _sort_by_ranks (GstPluginFeature * f1, GstPluginFeature * f2) { gint diff; const gchar *rname1, *rname2; diff = gst_plugin_feature_get_rank (f2) - gst_plugin_feature_get_rank (f1); if (diff != 0) return diff; /* If the ranks are the same sort by name to get deterministic results */ rname1 = gst_plugin_feature_get_name (f1); rname2 = gst_plugin_feature_get_name (f2); diff = strcmp (rname1, rname2); return diff; } static GstPad * _get_sub_pad (GstElement * element) { GstPad *pad; guint i; for (i = 0; i < G_N_ELEMENTS (_sub_pad_names); i++) { pad = gst_element_get_static_pad (element, _sub_pad_names[i]); if (pad) return pad; } return NULL; } static GstPad * _get_video_pad (GstElement * element) { static const gchar *const pad_names[] = { "video", "video_sink" }; GstPad *pad; guint i; for (i = 0; i < G_N_ELEMENTS (pad_names); i++) { pad = gst_element_get_static_pad (element, pad_names[i]); if (pad) return pad; } return NULL; } static gboolean _create_element (GstSubtitleOverlay * self, GstElement ** element, const gchar * factory_name, GstElementFactory * factory, const gchar * element_name, gboolean mandatory) { GstElement *elt; g_assert (!factory || !factory_name); if (factory_name) { elt = gst_element_factory_make (factory_name, element_name); } else { factory_name = gst_plugin_feature_get_name (GST_PLUGIN_FEATURE_CAST (factory)); elt = gst_element_factory_create (factory, element_name); } if (G_UNLIKELY (!elt)) { if (!factory) { GstMessage *msg; msg = gst_missing_element_message_new (GST_ELEMENT_CAST (self), factory_name); gst_element_post_message (GST_ELEMENT_CAST (self), msg); if (mandatory) GST_ELEMENT_ERROR (self, CORE, MISSING_PLUGIN, (NULL), ("no '%s' plugin found", factory_name)); else GST_ELEMENT_WARNING (self, CORE, MISSING_PLUGIN, (NULL), ("no '%s' plugin found", factory_name)); } else { if (mandatory) { GST_ELEMENT_ERROR (self, CORE, FAILED, (NULL), ("can't instantiate '%s'", factory_name)); } else { GST_ELEMENT_WARNING (self, CORE, FAILED, (NULL), ("can't instantiate '%s'", factory_name)); } } return FALSE; } if (G_UNLIKELY (gst_element_set_state (elt, GST_STATE_READY) != GST_STATE_CHANGE_SUCCESS)) { gst_object_unref (elt); if (mandatory) { GST_ELEMENT_ERROR (self, CORE, STATE_CHANGE, (NULL), ("failed to set '%s' to READY", factory_name)); } else { GST_WARNING_OBJECT (self, "Failed to set '%s' to READY", factory_name); } return FALSE; } if (G_UNLIKELY (!gst_bin_add (GST_BIN_CAST (self), gst_object_ref (elt)))) { gst_element_set_state (elt, GST_STATE_NULL); gst_object_unref (elt); if (mandatory) { GST_ELEMENT_ERROR (self, CORE, FAILED, (NULL), ("failed to add '%s' to subtitleoverlay", factory_name)); } else { GST_WARNING_OBJECT (self, "Failed to add '%s' to subtitleoverlay", factory_name); } return FALSE; } gst_element_sync_state_with_parent (elt); *element = elt; return TRUE; } static void _remove_element (GstSubtitleOverlay * self, GstElement ** element) { if (*element) { gst_bin_remove (GST_BIN_CAST (self), *element); gst_element_set_state (*element, GST_STATE_NULL); gst_object_unref (*element); *element = NULL; } } static gboolean _pad_query_convert_to_time (GstPad * pad, GstFormat src_format, gint64 src_val, gint64 * dest_val) { GstFormat dest_format; if (src_val == GST_CLOCK_TIME_NONE) { *dest_val = GST_CLOCK_TIME_NONE; return TRUE; } dest_format = GST_FORMAT_TIME; return gst_pad_query_convert (pad, src_format, src_val, dest_format, dest_val); } static void _generate_update_segment_event (GstPad * pad, GstSegment * segment, GstEvent ** event1) { GstEvent *event; GstStructure *structure; GstSegment newsegment; gboolean use_newseg = FALSE; gint64 start, stop, base, time, position, duration; /* always push newsegment with format TIME */ if (segment->format != GST_FORMAT_TIME) { gboolean res; GstPad *peer; peer = gst_pad_get_peer (pad); if (peer) { res = _pad_query_convert_to_time (peer, segment->format, segment->start, &start); res = res && _pad_query_convert_to_time (peer, segment->format, segment->stop, &stop); res = res && _pad_query_convert_to_time (peer, segment->format, segment->time, &time); res = res && _pad_query_convert_to_time (peer, segment->format, segment->base, &base); res = res && _pad_query_convert_to_time (peer, segment->format, segment->position, &position); res = res && _pad_query_convert_to_time (peer, segment->format, segment->duration, &duration); if (!res) { start = 0; stop = GST_CLOCK_TIME_NONE; time = 0; position = 0; duration = GST_CLOCK_TIME_NONE; base = 0; } else { use_newseg = TRUE; gst_segment_init (&newsegment, GST_FORMAT_TIME); newsegment.rate = segment->rate; newsegment.applied_rate = segment->applied_rate; newsegment.start = start; newsegment.stop = stop; newsegment.time = time; newsegment.base = base; newsegment.position = position; newsegment.duration = duration; } gst_object_unref (peer); } } if (use_newseg) { event = gst_event_new_segment (&newsegment); } else { event = gst_event_new_segment (segment); } structure = gst_event_writable_structure (event); gst_structure_id_set (structure, _subtitle_overlay_event_marker_id, G_TYPE_BOOLEAN, TRUE, NULL); *event1 = event; } static gboolean _setup_passthrough (GstSubtitleOverlay * self) { GstPad *src, *sink; GstElement *identity; GST_DEBUG_OBJECT (self, "Doing video passthrough"); if (self->passthrough_identity) { GST_DEBUG_OBJECT (self, "Already in passthrough mode"); goto out; } /* Unlink & destroy everything */ gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->srcpad), NULL); gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->video_sinkpad), NULL); gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->subtitle_sinkpad), NULL); self->silent_property = NULL; _remove_element (self, &self->post_colorspace); _remove_element (self, &self->overlay); _remove_element (self, &self->parser); _remove_element (self, &self->renderer); _remove_element (self, &self->pre_colorspace); _remove_element (self, &self->passthrough_identity); if (G_UNLIKELY (!_create_element (self, &self->passthrough_identity, "identity", NULL, "passthrough-identity", TRUE))) { return FALSE; } identity = self->passthrough_identity; g_object_set (G_OBJECT (identity), "silent", TRUE, "signal-handoffs", FALSE, NULL); /* Set src ghostpad target */ src = gst_element_get_static_pad (self->passthrough_identity, "src"); if (G_UNLIKELY (!src)) { GST_ELEMENT_ERROR (self, CORE, PAD, (NULL), ("Failed to get srcpad from identity")); return FALSE; } if (G_UNLIKELY (!gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->srcpad), src))) { GST_ELEMENT_ERROR (self, CORE, PAD, (NULL), ("Failed to set srcpad target")); gst_object_unref (src); return FALSE; } gst_object_unref (src); sink = gst_element_get_static_pad (self->passthrough_identity, "sink"); if (G_UNLIKELY (!sink)) { GST_ELEMENT_ERROR (self, CORE, PAD, (NULL), ("Failed to get sinkpad from identity")); return FALSE; } /* Send segment to the identity. This is dropped because identity * is not linked downstream yet */ if (self->video_segment.format != GST_FORMAT_UNDEFINED) { GstEvent *event1; _generate_update_segment_event (sink, &self->video_segment, &event1); GST_DEBUG_OBJECT (self, "Pushing video segment event: %" GST_PTR_FORMAT, event1); gst_pad_send_event (sink, event1); } /* Link sink ghostpads to identity */ if (G_UNLIKELY (!gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->video_sinkpad), sink))) { GST_ELEMENT_ERROR (self, CORE, PAD, (NULL), ("Failed to set video sinkpad target")); gst_object_unref (sink); return FALSE; } gst_object_unref (sink); GST_DEBUG_OBJECT (self, "Video passthrough setup successfully"); out: /* Unblock pads */ unblock_video (self); unblock_subtitle (self); return TRUE; } /* Must be called with subtitleoverlay lock! */ static gboolean _has_property_with_type (GObject * object, const gchar * property, GType type) { GObjectClass *gobject_class; GParamSpec *pspec; gobject_class = G_OBJECT_GET_CLASS (object); pspec = g_object_class_find_property (gobject_class, property); return (pspec && pspec->value_type == type); } static void gst_subtitle_overlay_set_fps (GstSubtitleOverlay * self) { if (!self->parser || self->fps_d == 0) return; if (!_has_property_with_type (G_OBJECT (self->parser), "video-fps", GST_TYPE_FRACTION)) return; GST_DEBUG_OBJECT (self, "Updating video-fps property in parser"); g_object_set (self->parser, "video-fps", self->fps_n, self->fps_d, NULL); } static const gchar * _get_silent_property (GstElement * element, gboolean * invert) { static const struct { const gchar *name; gboolean invert; } properties[] = { { "silent", FALSE}, { "enable", TRUE}}; guint i; for (i = 0; i < G_N_ELEMENTS (properties); i++) { if (_has_property_with_type (G_OBJECT (element), properties[i].name, G_TYPE_BOOLEAN)) { *invert = properties[i].invert; return properties[i].name; } } return NULL; } static gboolean _setup_parser (GstSubtitleOverlay * self) { GstPad *video_peer; /* Try to get the latest video framerate */ video_peer = gst_pad_get_peer (self->video_sinkpad); if (video_peer) { GstCaps *video_caps; gint fps_n, fps_d; video_caps = gst_pad_get_current_caps (video_peer); if (!video_caps) { video_caps = gst_pad_query_caps (video_peer, NULL); if (!gst_caps_is_fixed (video_caps)) { gst_caps_unref (video_caps); video_caps = NULL; } } if (video_caps) { GstStructure *st = gst_caps_get_structure (video_caps, 0); if (gst_structure_get_fraction (st, "framerate", &fps_n, &fps_d)) { GST_DEBUG_OBJECT (self, "New video fps: %d/%d", fps_n, fps_d); self->fps_n = fps_n; self->fps_d = fps_d; } } if (video_caps) gst_caps_unref (video_caps); gst_object_unref (video_peer); } if (_has_property_with_type (G_OBJECT (self->parser), "subtitle-encoding", G_TYPE_STRING)) g_object_set (self->parser, "subtitle-encoding", self->encoding, NULL); /* Try to set video fps on the parser */ gst_subtitle_overlay_set_fps (self); return TRUE; } static gboolean _setup_renderer (GstSubtitleOverlay * self, GstElement * renderer) { GstElementFactory *factory = gst_element_get_factory (renderer); const gchar *name = gst_plugin_feature_get_name (GST_PLUGIN_FEATURE_CAST (factory)); if (strcmp (name, "textoverlay") == 0) { /* Set some textoverlay specific properties */ g_object_set (G_OBJECT (renderer), "halign", "center", "valign", "bottom", "wait-text", FALSE, NULL); if (self->font_desc) g_object_set (G_OBJECT (renderer), "font-desc", self->font_desc, NULL); self->silent_property = "silent"; self->silent_property_invert = FALSE; } else { self->silent_property = _get_silent_property (renderer, &self->silent_property_invert); if (_has_property_with_type (G_OBJECT (renderer), "subtitle-encoding", G_TYPE_STRING)) g_object_set (renderer, "subtitle-encoding", self->encoding, NULL); if (_has_property_with_type (G_OBJECT (renderer), "font-desc", G_TYPE_STRING)) g_object_set (renderer, "font-desc", self->font_desc, NULL); } return TRUE; } /* subtitle_src==NULL means: use subtitle_sink ghostpad */ static gboolean _link_renderer (GstSubtitleOverlay * self, GstElement * renderer, GstPad * subtitle_src) { GstPad *sink, *src; gboolean is_video, is_hw; is_video = _is_video_pad (self->video_sinkpad, &is_hw); if (is_video) { gboolean render_is_hw; /* First check that renderer also supports the video format */ sink = _get_video_pad (renderer); if (G_UNLIKELY (!sink)) { GST_WARNING_OBJECT (self, "Can't get video sink from renderer"); return FALSE; } if (is_video != _is_video_pad (sink, &render_is_hw) || is_hw != render_is_hw) { GST_DEBUG_OBJECT (self, "Renderer doesn't support %s video", is_hw ? "surface" : "raw"); gst_object_unref (sink); return FALSE; } gst_object_unref (sink); if (!is_hw) { /* First link everything internally */ if (G_UNLIKELY (!_create_element (self, &self->post_colorspace, COLORSPACE, NULL, "post-colorspace", FALSE))) { return FALSE; } src = gst_element_get_static_pad (renderer, "src"); if (G_UNLIKELY (!src)) { GST_WARNING_OBJECT (self, "Can't get src pad from renderer"); return FALSE; } sink = gst_element_get_static_pad (self->post_colorspace, "sink"); if (G_UNLIKELY (!sink)) { GST_WARNING_OBJECT (self, "Can't get sink pad from " COLORSPACE); gst_object_unref (src); return FALSE; } if (G_UNLIKELY (gst_pad_link (src, sink) != GST_PAD_LINK_OK)) { GST_WARNING_OBJECT (self, "Can't link renderer with " COLORSPACE); gst_object_unref (src); gst_object_unref (sink); return FALSE; } gst_object_unref (src); gst_object_unref (sink); if (G_UNLIKELY (!_create_element (self, &self->pre_colorspace, COLORSPACE, NULL, "pre-colorspace", FALSE))) { return FALSE; } sink = _get_video_pad (renderer); if (G_UNLIKELY (!sink)) { GST_WARNING_OBJECT (self, "Can't get video sink from renderer"); return FALSE; } src = gst_element_get_static_pad (self->pre_colorspace, "src"); if (G_UNLIKELY (!src)) { GST_WARNING_OBJECT (self, "Can't get srcpad from " COLORSPACE); gst_object_unref (sink); return FALSE; } if (G_UNLIKELY (gst_pad_link (src, sink) != GST_PAD_LINK_OK)) { GST_WARNING_OBJECT (self, "Can't link " COLORSPACE " to renderer"); gst_object_unref (src); gst_object_unref (sink); return FALSE; } gst_object_unref (src); gst_object_unref (sink); /* Set src ghostpad target */ src = gst_element_get_static_pad (self->post_colorspace, "src"); if (G_UNLIKELY (!src)) { GST_WARNING_OBJECT (self, "Can't get src pad from " COLORSPACE); return FALSE; } } else { /* Set src ghostpad target in the harware accelerated case */ src = gst_element_get_static_pad (renderer, "src"); if (G_UNLIKELY (!src)) { GST_WARNING_OBJECT (self, "Can't get src pad from renderer"); return FALSE; } } } else { /* No video pad */ GstCaps *allowed_caps, *video_caps = NULL; GstPad *video_peer; gboolean can_intersect = FALSE; video_peer = gst_pad_get_peer (self->video_sinkpad); if (video_peer) { video_caps = gst_pad_get_current_caps (video_peer); if (!video_caps) { video_caps = gst_pad_query_caps (video_peer, NULL); } gst_object_unref (video_peer); } sink = _get_video_pad (renderer); if (G_UNLIKELY (!sink)) { GST_WARNING_OBJECT (self, "Can't get video sink from renderer"); return FALSE; } allowed_caps = gst_pad_query_caps (sink, NULL); gst_object_unref (sink); if (allowed_caps && video_caps) can_intersect = gst_caps_can_intersect (allowed_caps, video_caps); if (allowed_caps) gst_caps_unref (allowed_caps); if (video_caps) gst_caps_unref (video_caps); if (G_UNLIKELY (!can_intersect)) { GST_WARNING_OBJECT (self, "Renderer with custom caps is not " "compatible with video stream"); return FALSE; } src = gst_element_get_static_pad (renderer, "src"); if (G_UNLIKELY (!src)) { GST_WARNING_OBJECT (self, "Can't get src pad from renderer"); return FALSE; } } if (G_UNLIKELY (!gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->srcpad), src))) { GST_WARNING_OBJECT (self, "Can't set srcpad target"); gst_object_unref (src); return FALSE; } gst_object_unref (src); /* Set the sink ghostpad targets */ if (self->pre_colorspace) { sink = gst_element_get_static_pad (self->pre_colorspace, "sink"); if (G_UNLIKELY (!sink)) { GST_WARNING_OBJECT (self, "Can't get sink pad from " COLORSPACE); return FALSE; } } else { sink = _get_video_pad (renderer); if (G_UNLIKELY (!sink)) { GST_WARNING_OBJECT (self, "Can't get sink pad from %" GST_PTR_FORMAT, renderer); return FALSE; } } if (G_UNLIKELY (!gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->video_sinkpad), sink))) { GST_WARNING_OBJECT (self, "Can't set video sinkpad target"); gst_object_unref (sink); return FALSE; } gst_object_unref (sink); sink = _get_sub_pad (renderer); if (G_UNLIKELY (!sink)) { GST_WARNING_OBJECT (self, "Failed to get subpad"); return FALSE; } if (subtitle_src) { if (G_UNLIKELY (gst_pad_link (subtitle_src, sink) != GST_PAD_LINK_OK)) { GST_WARNING_OBJECT (self, "Failed to link subtitle srcpad with renderer"); gst_object_unref (sink); return FALSE; } } else { if (G_UNLIKELY (!gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->subtitle_sinkpad), sink))) { GST_WARNING_OBJECT (self, "Failed to set subtitle sink target"); gst_object_unref (sink); return FALSE; } } gst_object_unref (sink); return TRUE; } static GstPadProbeReturn _pad_blocked_cb (GstPad * pad, GstPadProbeInfo * info, gpointer user_data) { GstSubtitleOverlay *self = GST_SUBTITLE_OVERLAY_CAST (user_data); GstCaps *subcaps; GList *l, *factories = NULL; GST_DEBUG_OBJECT (pad, "Pad blocked"); GST_SUBTITLE_OVERLAY_LOCK (self); if (pad == self->video_block_pad) self->video_sink_blocked = TRUE; else if (pad == self->subtitle_block_pad) self->subtitle_sink_blocked = TRUE; /* Now either both or the video sink are blocked */ /* Get current subtitle caps */ subcaps = self->subcaps; if (!subcaps) { GstPad *peer; peer = gst_pad_get_peer (self->subtitle_sinkpad); if (peer) { subcaps = gst_pad_get_current_caps (peer); if (!subcaps) { subcaps = gst_pad_query_caps (peer, NULL); if (!gst_caps_is_fixed (subcaps)) { gst_caps_unref (subcaps); subcaps = NULL; } } gst_object_unref (peer); } gst_caps_replace (&self->subcaps, subcaps); if (subcaps) gst_caps_unref (subcaps); } GST_DEBUG_OBJECT (self, "Current subtitle caps: %" GST_PTR_FORMAT, subcaps); /* If there are no subcaps but the subtitle sink is blocked upstream * must behave wrong as there are no fixed caps set for the first * buffer or in-order event */ if (G_UNLIKELY (!subcaps && self->subtitle_sink_blocked)) { GST_ELEMENT_WARNING (self, CORE, NEGOTIATION, (NULL), ("Subtitle sink is blocked but we have no subtitle caps")); subcaps = NULL; } if (self->subtitle_error || (self->silent && !self->silent_property)) { _setup_passthrough (self); do_async_done (self); goto out; } /* Now do something with the caps */ if (subcaps && !self->subtitle_flush) { GstPad *target = gst_ghost_pad_get_target (GST_GHOST_PAD_CAST (self->subtitle_sinkpad)); if (target && gst_pad_query_accept_caps (target, subcaps)) { GST_DEBUG_OBJECT (pad, "Target accepts caps"); gst_object_unref (target); /* Unblock pads */ unblock_video (self); unblock_subtitle (self); goto out; } else if (target) { gst_object_unref (target); } } if (self->subtitle_sink_blocked && !self->video_sink_blocked) { GST_DEBUG_OBJECT (self, "Subtitle sink blocked but video not blocked"); block_video (self); goto out; } self->subtitle_flush = FALSE; /* Find our factories */ g_mutex_lock (self->factories_lock); gst_subtitle_overlay_update_factory_list (self); if (subcaps) { factories = gst_subtitle_overlay_get_factories_for_caps (self->factories, subcaps); if (!factories) { GstMessage *msg; msg = gst_missing_decoder_message_new (GST_ELEMENT_CAST (self), subcaps); gst_element_post_message (GST_ELEMENT_CAST (self), msg); GST_ELEMENT_WARNING (self, CORE, MISSING_PLUGIN, (NULL), ("no suitable subtitle plugin found")); subcaps = NULL; self->subtitle_error = TRUE; } } g_mutex_unlock (self->factories_lock); if (!subcaps) { _setup_passthrough (self); do_async_done (self); goto out; } /* Now the interesting parts are done: subtitle overlaying! */ /* Sort the factories by rank */ factories = g_list_sort (factories, (GCompareFunc) _sort_by_ranks); for (l = factories; l; l = l->next) { GstElementFactory *factory = l->data; gboolean is_renderer = _is_renderer (factory); GstPad *sink, *src; /* Unlink & destroy everything */ gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->srcpad), NULL); gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->video_sinkpad), NULL); gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->subtitle_sinkpad), NULL); self->silent_property = NULL; _remove_element (self, &self->post_colorspace); _remove_element (self, &self->overlay); _remove_element (self, &self->parser); _remove_element (self, &self->renderer); _remove_element (self, &self->pre_colorspace); _remove_element (self, &self->passthrough_identity); GST_DEBUG_OBJECT (self, "Trying factory '%s'", GST_STR_NULL (gst_plugin_feature_get_name (GST_PLUGIN_FEATURE_CAST (factory)))); if (G_UNLIKELY ((is_renderer && !_create_element (self, &self->renderer, NULL, factory, "renderer", FALSE)) || (!is_renderer && !_create_element (self, &self->parser, NULL, factory, "parser", FALSE)))) continue; if (!is_renderer) { GstCaps *parser_caps; GList *overlay_factories, *k; if (!_setup_parser (self)) continue; /* Find our factories */ src = gst_element_get_static_pad (self->parser, "src"); parser_caps = gst_pad_query_caps (src, NULL); gst_object_unref (src); g_assert (parser_caps != NULL); g_mutex_lock (self->factories_lock); gst_subtitle_overlay_update_factory_list (self); GST_DEBUG_OBJECT (self, "Searching overlay factories for caps %" GST_PTR_FORMAT, parser_caps); overlay_factories = gst_subtitle_overlay_get_factories_for_caps (self->factories, parser_caps); g_mutex_unlock (self->factories_lock); if (!overlay_factories) { GST_WARNING_OBJECT (self, "Found no suitable overlay factories for caps %" GST_PTR_FORMAT, parser_caps); gst_caps_unref (parser_caps); continue; } gst_caps_unref (parser_caps); /* Sort the factories by rank */ overlay_factories = g_list_sort (overlay_factories, (GCompareFunc) _sort_by_ranks); for (k = overlay_factories; k; k = k->next) { GstElementFactory *overlay_factory = k->data; GST_DEBUG_OBJECT (self, "Trying overlay factory '%s'", GST_STR_NULL (gst_plugin_feature_get_name (GST_PLUGIN_FEATURE_CAST (overlay_factory)))); /* Try this factory and link it, otherwise unlink everything * again and remove the overlay. Up to this point only the * parser was instantiated and setup, nothing was linked */ gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->srcpad), NULL); gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->video_sinkpad), NULL); gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->subtitle_sinkpad), NULL); self->silent_property = NULL; _remove_element (self, &self->post_colorspace); _remove_element (self, &self->overlay); _remove_element (self, &self->pre_colorspace); if (!_create_element (self, &self->overlay, NULL, overlay_factory, "overlay", FALSE)) { GST_DEBUG_OBJECT (self, "Could not create element"); continue; } if (!_setup_renderer (self, self->overlay)) { GST_DEBUG_OBJECT (self, "Could not setup element"); continue; } src = gst_element_get_static_pad (self->parser, "src"); if (!_link_renderer (self, self->overlay, src)) { GST_DEBUG_OBJECT (self, "Could not link element"); gst_object_unref (src); continue; } gst_object_unref (src); /* Everything done here, go out of loop */ GST_DEBUG_OBJECT (self, "%s is a suitable element", GST_STR_NULL (gst_plugin_feature_get_name (GST_PLUGIN_FEATURE_CAST (overlay_factory)))); break; } if (overlay_factories) gst_plugin_feature_list_free (overlay_factories); if (G_UNLIKELY (k == NULL)) { GST_WARNING_OBJECT (self, "Failed to find usable overlay factory"); continue; } /* Now link subtitle sinkpad of the bin and the parser */ sink = gst_element_get_static_pad (self->parser, "sink"); if (!gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->subtitle_sinkpad), sink)) { gst_object_unref (sink); continue; } gst_object_unref (sink); /* Everything done here, go out of loop */ break; } else { /* Is renderer factory */ if (!_setup_renderer (self, self->renderer)) continue; /* subtitle_src==NULL means: use subtitle_sink ghostpad */ if (!_link_renderer (self, self->renderer, NULL)) continue; /* Everything done here, go out of loop */ break; } } if (G_UNLIKELY (l == NULL)) { GST_ELEMENT_WARNING (self, CORE, FAILED, (NULL), ("Failed to find any usable factories")); self->subtitle_error = TRUE; _setup_passthrough (self); do_async_done (self); goto out; } /* Send segments to the renderer if necessary. These are not sent * outside this element because of the proxy pad event handler */ if (self->video_segment.format != GST_FORMAT_UNDEFINED) { GstEvent *event1; GstPad *sink; if (self->pre_colorspace) { sink = gst_element_get_static_pad (self->pre_colorspace, "sink"); } else { sink = _get_video_pad ((self->renderer) ? self->renderer : self->overlay); } _generate_update_segment_event (sink, &self->video_segment, &event1); GST_DEBUG_OBJECT (self, "Pushing video update segment event: %" GST_PTR_FORMAT, gst_event_get_structure (event1)); gst_pad_send_event (sink, event1); gst_object_unref (sink); } if (self->subtitle_segment.format != GST_FORMAT_UNDEFINED) { GstEvent *event1; GstPad *sink; if (self->renderer) sink = _get_sub_pad (self->renderer); else sink = gst_element_get_static_pad (self->parser, "sink"); _generate_update_segment_event (sink, &self->subtitle_segment, &event1); GST_DEBUG_OBJECT (self, "Pushing subtitle update segment event: %" GST_PTR_FORMAT, gst_event_get_structure (event1)); gst_pad_send_event (sink, event1); gst_object_unref (sink); } GST_DEBUG_OBJECT (self, "Everything worked, unblocking pads"); unblock_video (self); unblock_subtitle (self); do_async_done (self); out: if (factories) gst_plugin_feature_list_free (factories); GST_SUBTITLE_OVERLAY_UNLOCK (self); return GST_PAD_PROBE_OK; } static GstStateChangeReturn gst_subtitle_overlay_change_state (GstElement * element, GstStateChange transition) { GstSubtitleOverlay *self = GST_SUBTITLE_OVERLAY (element); GstStateChangeReturn ret = GST_STATE_CHANGE_SUCCESS; switch (transition) { case GST_STATE_CHANGE_NULL_TO_READY: GST_DEBUG_OBJECT (self, "State change NULL->READY"); g_mutex_lock (self->factories_lock); if (G_UNLIKELY (!gst_subtitle_overlay_update_factory_list (self))) { g_mutex_unlock (self->factories_lock); return GST_STATE_CHANGE_FAILURE; } g_mutex_unlock (self->factories_lock); GST_SUBTITLE_OVERLAY_LOCK (self); /* Set the internal pads to blocking */ block_video (self); block_subtitle (self); GST_SUBTITLE_OVERLAY_UNLOCK (self); break; case GST_STATE_CHANGE_READY_TO_PAUSED: GST_DEBUG_OBJECT (self, "State change READY->PAUSED"); gst_segment_init (&self->video_segment, GST_FORMAT_UNDEFINED); gst_segment_init (&self->subtitle_segment, GST_FORMAT_UNDEFINED); self->fps_n = self->fps_d = 0; self->subtitle_flush = FALSE; self->subtitle_error = FALSE; self->downstream_chain_error = FALSE; do_async_start (self); ret = GST_STATE_CHANGE_ASYNC; break; case GST_STATE_CHANGE_PAUSED_TO_PLAYING: GST_DEBUG_OBJECT (self, "State change PAUSED->PLAYING"); default: break; } { GstStateChangeReturn bret; bret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition); GST_DEBUG_OBJECT (self, "Base class state changed returned: %d", bret); if (G_UNLIKELY (bret == GST_STATE_CHANGE_FAILURE)) return ret; else if (bret == GST_STATE_CHANGE_ASYNC) ret = bret; else if (G_UNLIKELY (bret == GST_STATE_CHANGE_NO_PREROLL)) { do_async_done (self); ret = bret; } } switch (transition) { case GST_STATE_CHANGE_PLAYING_TO_PAUSED: GST_DEBUG_OBJECT (self, "State change PLAYING->PAUSED"); break; case GST_STATE_CHANGE_PAUSED_TO_READY: GST_DEBUG_OBJECT (self, "State change PAUSED->READY"); /* Set the pads back to blocking state */ GST_SUBTITLE_OVERLAY_LOCK (self); block_video (self); block_subtitle (self); GST_SUBTITLE_OVERLAY_UNLOCK (self); do_async_done (self); break; case GST_STATE_CHANGE_READY_TO_NULL: GST_DEBUG_OBJECT (self, "State change READY->NULL"); GST_SUBTITLE_OVERLAY_LOCK (self); gst_caps_replace (&self->subcaps, NULL); /* Unlink ghost pads */ gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->srcpad), NULL); gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->video_sinkpad), NULL); gst_ghost_pad_set_target (GST_GHOST_PAD_CAST (self->subtitle_sinkpad), NULL); /* Unblock pads */ unblock_video (self); unblock_subtitle (self); /* Remove elements */ self->silent_property = NULL; _remove_element (self, &self->post_colorspace); _remove_element (self, &self->overlay); _remove_element (self, &self->parser); _remove_element (self, &self->renderer); _remove_element (self, &self->pre_colorspace); _remove_element (self, &self->passthrough_identity); GST_SUBTITLE_OVERLAY_UNLOCK (self); break; default: break; } return ret; } static void gst_subtitle_overlay_handle_message (GstBin * bin, GstMessage * message) { GstSubtitleOverlay *self = GST_SUBTITLE_OVERLAY_CAST (bin); if (GST_MESSAGE_TYPE (message) == GST_MESSAGE_ERROR) { GstObject *src = GST_MESSAGE_SRC (message); /* Convert error messages from the subtitle pipeline to * warnings and switch to passthrough mode */ if (src && ( (self->overlay && gst_object_has_ancestor (src, GST_OBJECT_CAST (self->overlay))) || (self->parser && gst_object_has_ancestor (src, GST_OBJECT_CAST (self->parser))) || (self->renderer && gst_object_has_ancestor (src, GST_OBJECT_CAST (self->renderer))))) { GError *err = NULL; gchar *debug = NULL; GstMessage *wmsg; gst_message_parse_error (message, &err, &debug); GST_DEBUG_OBJECT (self, "Got error message from subtitle element %s: %s (%s)", GST_MESSAGE_SRC_NAME (message), GST_STR_NULL (err->message), GST_STR_NULL (debug)); wmsg = gst_message_new_warning (src, err, debug); gst_message_unref (message); g_error_free (err); g_free (debug); message = wmsg; GST_SUBTITLE_OVERLAY_LOCK (self); self->subtitle_error = TRUE; block_subtitle (self); block_video (self); GST_SUBTITLE_OVERLAY_UNLOCK (self); } } GST_BIN_CLASS (parent_class)->handle_message (bin, message); } static void gst_subtitle_overlay_get_property (GObject * object, guint prop_id, GValue * value, GParamSpec * pspec) { GstSubtitleOverlay *self = GST_SUBTITLE_OVERLAY_CAST (object); switch (prop_id) { case PROP_SILENT: g_value_set_boolean (value, self->silent); break; case PROP_FONT_DESC: GST_SUBTITLE_OVERLAY_LOCK (self); g_value_set_string (value, self->font_desc); GST_SUBTITLE_OVERLAY_UNLOCK (self); break; case PROP_SUBTITLE_ENCODING: GST_SUBTITLE_OVERLAY_LOCK (self); g_value_set_string (value, self->encoding); GST_SUBTITLE_OVERLAY_UNLOCK (self); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void gst_subtitle_overlay_set_property (GObject * object, guint prop_id, const GValue * value, GParamSpec * pspec) { GstSubtitleOverlay *self = GST_SUBTITLE_OVERLAY_CAST (object); switch (prop_id) { case PROP_SILENT: GST_SUBTITLE_OVERLAY_LOCK (self); self->silent = g_value_get_boolean (value); if (self->silent_property) { gboolean silent = self->silent; if (self->silent_property_invert) silent = !silent; if (self->overlay) g_object_set (self->overlay, self->silent_property, silent, NULL); else if (self->renderer) g_object_set (self->renderer, self->silent_property, silent, NULL); } else { block_subtitle (self); block_video (self); } GST_SUBTITLE_OVERLAY_UNLOCK (self); break; case PROP_FONT_DESC: GST_SUBTITLE_OVERLAY_LOCK (self); g_free (self->font_desc); self->font_desc = g_value_dup_string (value); if (self->overlay && _has_property_with_type (G_OBJECT (self->overlay), "font-desc", G_TYPE_STRING)) g_object_set (self->overlay, "font-desc", self->font_desc, NULL); else if (self->renderer && _has_property_with_type (G_OBJECT (self->renderer), "font-desc", G_TYPE_STRING)) g_object_set (self->renderer, "font-desc", self->font_desc, NULL); GST_SUBTITLE_OVERLAY_UNLOCK (self); break; case PROP_SUBTITLE_ENCODING: GST_SUBTITLE_OVERLAY_LOCK (self); g_free (self->encoding); self->encoding = g_value_dup_string (value); if (self->renderer && _has_property_with_type (G_OBJECT (self->renderer), "subtitle-encoding", G_TYPE_STRING)) g_object_set (self->renderer, "subtitle-encoding", self->encoding, NULL); if (self->parser && _has_property_with_type (G_OBJECT (self->parser), "subtitle-encoding", G_TYPE_STRING)) g_object_set (self->parser, "subtitle-encoding", self->encoding, NULL); GST_SUBTITLE_OVERLAY_UNLOCK (self); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void gst_subtitle_overlay_class_init (GstSubtitleOverlayClass * klass) { GObjectClass *gobject_class = (GObjectClass *) klass; GstElementClass *element_class = (GstElementClass *) klass; GstBinClass *bin_class = (GstBinClass *) klass; gobject_class->finalize = gst_subtitle_overlay_finalize; gobject_class->set_property = gst_subtitle_overlay_set_property; gobject_class->get_property = gst_subtitle_overlay_get_property; g_object_class_install_property (gobject_class, PROP_SILENT, g_param_spec_boolean ("silent", "Silent", "Whether to show subtitles", FALSE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (gobject_class, PROP_FONT_DESC, g_param_spec_string ("font-desc", "Subtitle font description", "Pango font description of font " "to be used for subtitle rendering", NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (gobject_class, PROP_SUBTITLE_ENCODING, g_param_spec_string ("subtitle-encoding", "subtitle encoding", "Encoding to assume if input subtitles are not in UTF-8 encoding. " "If not set, the GST_SUBTITLE_ENCODING environment variable will " "be checked for an encoding to use. If that is not set either, " "ISO-8859-15 will be assumed.", NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); gst_element_class_add_pad_template (element_class, gst_static_pad_template_get (&srctemplate)); gst_element_class_add_pad_template (element_class, gst_static_pad_template_get (&video_sinktemplate)); gst_element_class_add_pad_template (element_class, gst_static_pad_template_get (&subtitle_sinktemplate)); gst_element_class_set_static_metadata (element_class, "Subtitle Overlay", "Video/Overlay/Subtitle", "Overlays a video stream with subtitles", "Sebastian Dröge "); element_class->change_state = GST_DEBUG_FUNCPTR (gst_subtitle_overlay_change_state); bin_class->handle_message = GST_DEBUG_FUNCPTR (gst_subtitle_overlay_handle_message); } static GstFlowReturn gst_subtitle_overlay_src_proxy_chain (GstPad * proxypad, GstObject * parent, GstBuffer * buffer) { GstPad *ghostpad; GstSubtitleOverlay *self; GstFlowReturn ret; ghostpad = GST_PAD_CAST (parent); if (G_UNLIKELY (!ghostpad)) { gst_buffer_unref (buffer); return GST_FLOW_ERROR; } self = GST_SUBTITLE_OVERLAY_CAST (gst_pad_get_parent (ghostpad)); if (G_UNLIKELY (!self || self->srcpad != ghostpad)) { gst_buffer_unref (buffer); gst_object_unref (ghostpad); return GST_FLOW_ERROR; } ret = gst_proxy_pad_chain_default (proxypad, parent, buffer); if (IS_VIDEO_CHAIN_IGNORE_ERROR (ret)) { GST_ERROR_OBJECT (self, "Downstream chain error: %s", gst_flow_get_name (ret)); self->downstream_chain_error = TRUE; } gst_object_unref (self); return ret; } static gboolean gst_subtitle_overlay_src_proxy_event (GstPad * proxypad, GstObject * parent, GstEvent * event) { GstPad *ghostpad = NULL; GstSubtitleOverlay *self = NULL; gboolean ret = FALSE; const GstStructure *s; ghostpad = GST_PAD_CAST (parent); if (G_UNLIKELY (!ghostpad)) goto out; self = GST_SUBTITLE_OVERLAY_CAST (gst_pad_get_parent (ghostpad)); if (G_UNLIKELY (!self || self->srcpad != ghostpad)) goto out; s = gst_event_get_structure (event); if (s && gst_structure_id_has_field (s, _subtitle_overlay_event_marker_id)) { GST_DEBUG_OBJECT (ghostpad, "Dropping event with marker: %" GST_PTR_FORMAT, gst_event_get_structure (event)); gst_event_unref (event); event = NULL; ret = TRUE; } else { ret = gst_proxy_pad_event_default (proxypad, parent, event); event = NULL; } out: if (event) gst_event_unref (event); if (self) gst_object_unref (self); return ret; } static gboolean gst_subtitle_overlay_video_sink_setcaps (GstSubtitleOverlay * self, GstCaps * caps) { GstPad *target; gboolean ret = TRUE; GstVideoInfo info; GST_DEBUG_OBJECT (self, "Setting caps: %" GST_PTR_FORMAT, caps); if (!gst_video_info_from_caps (&info, caps)) { GST_ERROR_OBJECT (self, "Failed to parse caps"); ret = FALSE; GST_SUBTITLE_OVERLAY_UNLOCK (self); goto out; } target = gst_ghost_pad_get_target (GST_GHOST_PAD_CAST (self->video_sinkpad)); GST_SUBTITLE_OVERLAY_LOCK (self); if (!target || !gst_pad_query_accept_caps (target, caps)) { GST_DEBUG_OBJECT (target, "Target did not accept caps -- reconfiguring"); block_subtitle (self); block_video (self); } if (self->fps_n != info.fps_n || self->fps_d != info.fps_d) { GST_DEBUG_OBJECT (self, "New video fps: %d/%d", info.fps_n, info.fps_d); self->fps_n = info.fps_n; self->fps_d = info.fps_d; gst_subtitle_overlay_set_fps (self); } GST_SUBTITLE_OVERLAY_UNLOCK (self); if (target) gst_object_unref (target); out: return ret; } static gboolean gst_subtitle_overlay_video_sink_event (GstPad * pad, GstObject * parent, GstEvent * event) { GstSubtitleOverlay *self = GST_SUBTITLE_OVERLAY (parent); gboolean ret; switch (GST_EVENT_TYPE (event)) { case GST_EVENT_FLUSH_STOP: { GST_DEBUG_OBJECT (pad, "Resetting video segment because of flush-stop event"); gst_segment_init (&self->video_segment, GST_FORMAT_UNDEFINED); self->fps_n = self->fps_d = 0; break; } case GST_EVENT_CAPS: { GstCaps *caps; gst_event_parse_caps (event, &caps); ret = gst_subtitle_overlay_video_sink_setcaps (self, caps); if (!ret) goto done; break; } default: break; } ret = gst_proxy_pad_event_default (pad, parent, gst_event_ref (event)); if (GST_EVENT_TYPE (event) == GST_EVENT_SEGMENT) { GST_DEBUG_OBJECT (pad, "segment event: %" GST_PTR_FORMAT, event); gst_event_copy_segment (event, &self->video_segment); if (self->video_segment.format != GST_FORMAT_TIME) goto invalid_format; } done: gst_event_unref (event); return ret; /* ERRORS */ invalid_format: { GST_ERROR_OBJECT (pad, "Segment event in non-time format: %s", gst_format_get_name (self->video_segment.format)); ret = FALSE; goto done; } } static GstFlowReturn gst_subtitle_overlay_video_sink_chain (GstPad * pad, GstObject * parent, GstBuffer * buffer) { GstSubtitleOverlay *self = GST_SUBTITLE_OVERLAY (parent); GstFlowReturn ret = gst_proxy_pad_chain_default (pad, parent, buffer); if (G_UNLIKELY (self->downstream_chain_error) || self->passthrough_identity) { return ret; } else if (IS_VIDEO_CHAIN_IGNORE_ERROR (ret)) { GST_DEBUG_OBJECT (self, "Subtitle renderer produced chain error: %s", gst_flow_get_name (ret)); GST_SUBTITLE_OVERLAY_LOCK (self); self->subtitle_error = TRUE; block_subtitle (self); block_video (self); GST_SUBTITLE_OVERLAY_UNLOCK (self); return GST_FLOW_OK; } return ret; } static GstFlowReturn gst_subtitle_overlay_subtitle_sink_chain (GstPad * pad, GstObject * parent, GstBuffer * buffer) { GstSubtitleOverlay *self = GST_SUBTITLE_OVERLAY (parent); if (self->subtitle_error) { gst_buffer_unref (buffer); return GST_FLOW_OK; } else { GstFlowReturn ret = gst_proxy_pad_chain_default (pad, parent, buffer); if (IS_SUBTITLE_CHAIN_IGNORE_ERROR (ret)) { GST_DEBUG_OBJECT (self, "Subtitle chain error: %s", gst_flow_get_name (ret)); GST_SUBTITLE_OVERLAY_LOCK (self); self->subtitle_error = TRUE; block_subtitle (self); block_video (self); GST_SUBTITLE_OVERLAY_UNLOCK (self); return GST_FLOW_OK; } return ret; } } static GstCaps * gst_subtitle_overlay_subtitle_sink_getcaps (GstPad * pad, GstCaps * filter) { GstSubtitleOverlay *self = GST_SUBTITLE_OVERLAY (gst_pad_get_parent (pad)); GstCaps *ret; g_mutex_lock (self->factories_lock); if (G_UNLIKELY (!gst_subtitle_overlay_update_factory_list (self))) ret = gst_caps_new_empty (); else if (filter) ret = gst_caps_intersect_full (filter, self->factory_caps, GST_CAPS_INTERSECT_FIRST); else ret = gst_caps_ref (self->factory_caps); g_mutex_unlock (self->factories_lock); GST_DEBUG_OBJECT (pad, "Returning subtitle caps %" GST_PTR_FORMAT, ret); gst_object_unref (self); return ret; } static gboolean gst_subtitle_overlay_subtitle_sink_setcaps (GstSubtitleOverlay * self, GstCaps * caps) { gboolean ret = TRUE; GstPad *target = NULL;; GST_DEBUG_OBJECT (self, "Setting caps: %" GST_PTR_FORMAT, caps); target = gst_ghost_pad_get_target (GST_GHOST_PAD_CAST (self->subtitle_sinkpad)); GST_SUBTITLE_OVERLAY_LOCK (self); gst_caps_replace (&self->subcaps, caps); if (target && gst_pad_query_accept_caps (target, caps)) { GST_DEBUG_OBJECT (self, "Target accepts caps"); GST_SUBTITLE_OVERLAY_UNLOCK (self); goto out; } GST_DEBUG_OBJECT (self, "Target did not accept caps"); self->subtitle_error = FALSE; block_subtitle (self); block_video (self); GST_SUBTITLE_OVERLAY_UNLOCK (self); out: if (target) gst_object_unref (target); return ret; } static GstPadLinkReturn gst_subtitle_overlay_subtitle_sink_link (GstPad * pad, GstPad * peer) { GstSubtitleOverlay *self = GST_SUBTITLE_OVERLAY (gst_pad_get_parent (pad)); GstPadLinkReturn ret; GstCaps *caps; GST_DEBUG_OBJECT (pad, "Linking pad to peer %" GST_PTR_FORMAT, peer); caps = gst_pad_get_current_caps (peer); if (!caps) { caps = gst_pad_query_caps (peer, NULL); if (!gst_caps_is_fixed (caps)) { gst_caps_unref (caps); caps = NULL; } } if (caps) { GST_SUBTITLE_OVERLAY_LOCK (self); GST_DEBUG_OBJECT (pad, "Have fixed peer caps: %" GST_PTR_FORMAT, caps); gst_caps_replace (&self->subcaps, caps); self->subtitle_error = FALSE; block_subtitle (self); block_video (self); GST_SUBTITLE_OVERLAY_UNLOCK (self); gst_caps_unref (caps); } ret = gst_ghost_pad_link_default (pad, peer); gst_object_unref (self); return ret; } static void gst_subtitle_overlay_subtitle_sink_unlink (GstPad * pad) { GstSubtitleOverlay *self = GST_SUBTITLE_OVERLAY (gst_object_ref (GST_PAD_PARENT (pad))); /* FIXME: Can't use gst_pad_get_parent() here because this is called with * the object lock from state changes */ GST_DEBUG_OBJECT (pad, "Pad unlinking"); gst_caps_replace (&self->subcaps, NULL); gst_ghost_pad_unlink_default (pad); GST_SUBTITLE_OVERLAY_LOCK (self); self->subtitle_error = FALSE; block_subtitle (self); block_video (self); GST_SUBTITLE_OVERLAY_UNLOCK (self); gst_object_unref (self); } static gboolean gst_subtitle_overlay_subtitle_sink_event (GstPad * pad, GstObject * parent, GstEvent * event) { GstSubtitleOverlay *self = GST_SUBTITLE_OVERLAY (parent); gboolean ret; GST_DEBUG_OBJECT (pad, "Got event %" GST_PTR_FORMAT, event); if (GST_EVENT_TYPE (event) == GST_EVENT_CUSTOM_DOWNSTREAM_OOB && gst_event_has_name (event, "playsink-custom-subtitle-flush")) { GST_DEBUG_OBJECT (pad, "Custom subtitle flush event"); GST_SUBTITLE_OVERLAY_LOCK (self); self->subtitle_flush = TRUE; self->subtitle_error = FALSE; block_subtitle (self); block_video (self); GST_SUBTITLE_OVERLAY_UNLOCK (self); gst_event_unref (event); event = NULL; ret = TRUE; goto out; } switch (GST_EVENT_TYPE (event)) { case GST_EVENT_CAPS: { GstCaps *caps; gst_event_parse_caps (event, &caps); ret = gst_subtitle_overlay_subtitle_sink_setcaps (self, caps); if (!ret) goto out; break; } case GST_EVENT_FLUSH_STOP: GST_DEBUG_OBJECT (pad, "Resetting subtitle segment because of flush-stop"); gst_segment_init (&self->subtitle_segment, GST_FORMAT_UNDEFINED); /* fall through */ case GST_EVENT_FLUSH_START: case GST_EVENT_SEGMENT: case GST_EVENT_EOS: { GstStructure *structure; /* Add our event marker to make sure no events from here go ever outside * the element, they're only interesting for our internal elements */ event = GST_EVENT_CAST (gst_event_make_writable (event)); structure = gst_event_writable_structure (event); gst_structure_id_set (structure, _subtitle_overlay_event_marker_id, G_TYPE_BOOLEAN, TRUE, NULL); break; } default: break; } ret = gst_proxy_pad_event_default (pad, parent, gst_event_ref (event)); if (GST_EVENT_TYPE (event) == GST_EVENT_SEGMENT) { const GstSegment *eventsegment; GST_DEBUG_OBJECT (pad, "segment event: %" GST_PTR_FORMAT, event); gst_event_parse_segment (event, &eventsegment); if (eventsegment->format == GST_FORMAT_TIME) { gst_event_copy_segment (event, &self->subtitle_segment); } else { GstPad *peer; gboolean res = FALSE; gint64 tstart = 0; gint64 tstop = GST_CLOCK_TIME_NONE; gint64 tposition = 0; gint64 tduration = 0; gint64 ttime = 0; gint64 tbase = 0; GstFormat format = eventsegment->format; GST_DEBUG_OBJECT (pad, "Subtitle newsegment event (%s) not in TIME " "format, converting", gst_format_get_name (format)); peer = gst_pad_get_peer (pad); if (peer) { res = _pad_query_convert_to_time (peer, format, eventsegment->start, &tstart); res = res && _pad_query_convert_to_time (peer, format, eventsegment->stop, &tstop); res = res && _pad_query_convert_to_time (peer, format, eventsegment->position, &tposition); res = res && _pad_query_convert_to_time (peer, format, eventsegment->base, &tbase); res = res && _pad_query_convert_to_time (peer, format, eventsegment->time, &ttime); res = res && _pad_query_convert_to_time (peer, format, eventsegment->duration, &tduration); gst_object_unref (peer); } if (!res) { tstart = 0; tstop = GST_CLOCK_TIME_NONE; tposition = 0; tduration = GST_CLOCK_TIME_NONE; ttime = 0; tbase = 0; } gst_segment_init (&self->subtitle_segment, GST_FORMAT_TIME); self->subtitle_segment.start = tstart; self->subtitle_segment.stop = tstop; self->subtitle_segment.base = tbase; self->subtitle_segment.time = ttime; self->subtitle_segment.position = tposition; self->subtitle_segment.duration = tduration; } GST_DEBUG_OBJECT (pad, "New subtitle segment: %" GST_SEGMENT_FORMAT, &self->subtitle_segment); } gst_event_unref (event); out: return ret; } static gboolean gst_subtitle_overlay_subtitle_sink_query (GstPad * pad, GstObject * parent, GstQuery * query) { gboolean ret; switch (GST_QUERY_TYPE (query)) { case GST_QUERY_ACCEPT_CAPS: { GstCaps *caps, *othercaps; gst_query_parse_accept_caps (query, &caps); othercaps = gst_subtitle_overlay_subtitle_sink_getcaps (pad, NULL); ret = gst_caps_is_subset (caps, othercaps); gst_caps_unref (othercaps); gst_query_set_accept_caps_result (query, ret); ret = TRUE; break; } case GST_QUERY_CAPS: { GstCaps *filter, *caps; gst_query_parse_caps (query, &filter); caps = gst_subtitle_overlay_subtitle_sink_getcaps (pad, filter); gst_query_set_caps_result (query, caps); gst_caps_unref (caps); ret = TRUE; break; } default: ret = gst_pad_query_default (pad, parent, query); break; } return ret; } static void gst_subtitle_overlay_init (GstSubtitleOverlay * self) { GstPadTemplate *templ; GstPad *proxypad = NULL; self->lock = g_mutex_new (); self->factories_lock = g_mutex_new (); templ = gst_static_pad_template_get (&srctemplate); self->srcpad = gst_ghost_pad_new_no_target_from_template ("src", templ); gst_object_unref (templ); proxypad = GST_PAD_CAST (gst_proxy_pad_get_internal (GST_PROXY_PAD (self->srcpad))); gst_pad_set_event_function (proxypad, GST_DEBUG_FUNCPTR (gst_subtitle_overlay_src_proxy_event)); gst_pad_set_chain_function (proxypad, GST_DEBUG_FUNCPTR (gst_subtitle_overlay_src_proxy_chain)); gst_object_unref (proxypad); gst_element_add_pad (GST_ELEMENT_CAST (self), self->srcpad); templ = gst_static_pad_template_get (&video_sinktemplate); self->video_sinkpad = gst_ghost_pad_new_no_target_from_template ("video_sink", templ); gst_object_unref (templ); gst_pad_set_event_function (self->video_sinkpad, GST_DEBUG_FUNCPTR (gst_subtitle_overlay_video_sink_event)); gst_pad_set_chain_function (self->video_sinkpad, GST_DEBUG_FUNCPTR (gst_subtitle_overlay_video_sink_chain)); proxypad = GST_PAD_CAST (gst_proxy_pad_get_internal (GST_PROXY_PAD (self->video_sinkpad))); self->video_block_pad = proxypad; gst_object_unref (proxypad); gst_element_add_pad (GST_ELEMENT_CAST (self), self->video_sinkpad); templ = gst_static_pad_template_get (&subtitle_sinktemplate); self->subtitle_sinkpad = gst_ghost_pad_new_no_target_from_template ("subtitle_sink", templ); gst_object_unref (templ); gst_pad_set_link_function (self->subtitle_sinkpad, GST_DEBUG_FUNCPTR (gst_subtitle_overlay_subtitle_sink_link)); gst_pad_set_unlink_function (self->subtitle_sinkpad, GST_DEBUG_FUNCPTR (gst_subtitle_overlay_subtitle_sink_unlink)); gst_pad_set_event_function (self->subtitle_sinkpad, GST_DEBUG_FUNCPTR (gst_subtitle_overlay_subtitle_sink_event)); gst_pad_set_query_function (self->subtitle_sinkpad, GST_DEBUG_FUNCPTR (gst_subtitle_overlay_subtitle_sink_query)); gst_pad_set_chain_function (self->subtitle_sinkpad, GST_DEBUG_FUNCPTR (gst_subtitle_overlay_subtitle_sink_chain)); proxypad = GST_PAD_CAST (gst_proxy_pad_get_internal (GST_PROXY_PAD (self->subtitle_sinkpad))); self->subtitle_block_pad = proxypad; gst_object_unref (proxypad); gst_element_add_pad (GST_ELEMENT_CAST (self), self->subtitle_sinkpad); self->fps_n = 0; self->fps_d = 0; } gboolean gst_subtitle_overlay_plugin_init (GstPlugin * plugin) { GST_DEBUG_CATEGORY_INIT (subtitle_overlay_debug, "subtitleoverlay", 0, "Subtitle Overlay"); _subtitle_overlay_event_marker_id = g_quark_from_static_string ("gst-subtitle-overlay-event-marker"); return gst_element_register (plugin, "subtitleoverlay", GST_RANK_NONE, GST_TYPE_SUBTITLE_OVERLAY); }