Package overlaysrc as a standalone plugin

This packages overlaysrc as a standard reusable plugin that any user
application can build upon.

Change-Id: I8146dee55c733b9e4cfc9cafd768cff170214bf9
diff --git a/debian/control b/debian/control
index 4bae460..14b3038 100644
--- a/debian/control
+++ b/debian/control
@@ -2,12 +2,12 @@
 Maintainer: Coral <coral-support@google.com>
 Section: python
 Priority: optional
-Build-Depends: dh-python, python3-setuptools, python3-all, debhelper (>= 9)
+Build-Depends: dh-python, python3-setuptools, python3-all, debhelper (>= 9), libgstreamer1.0-dev, pkg-config
 Standards-Version: 3.9.8
 Homepage: https://coral.withgoogle.com/
 
 Package: python3-edgetpuvision
-Architecture: all
+Architecture: any
 Depends: ${misc:Depends},
          ${python3:Depends},
          gir1.2-glib-2.0,
@@ -18,6 +18,7 @@
          gstreamer1.0-plugins-bad,
          gstreamer1.0-plugins-good,
          gstreamer1.0-plugins-ugly,
+         gstreamer1.0-python3-plugin-loader,
          python3-cairo,
          python3-edgetpu,
          python3-gi,
diff --git a/debian/rules b/debian/rules
index 66fdb8d..f30fb7e 100755
--- a/debian/rules
+++ b/debian/rules
@@ -1,8 +1,13 @@
 #!/usr/bin/make -f
 
-# This file was automatically generated by stdeb 0.8.5 at
-# Wed, 16 Jan 2019 21:25:55 +0000
+PLUGINSDIR := $(shell pkg-config --variable=pluginsdir gstreamer-1.0)
+DESTDIR := debian/python3-edgetpuvision/$(PLUGINSDIR)/python
+
 export PYBUILD_NAME=edgetpuvision
 %:
 	dh $@ --with python3 --buildsystem=pybuild
 
+override_dh_install:
+	dh_install
+	install -d $(DESTDIR)
+	install -g 0 -o 0 plugins/*.py $(DESTDIR)
diff --git a/edgetpuvision/gst_native.py b/edgetpuvision/gst_native.py
index e358d77..71584dc 100644
--- a/edgetpuvision/gst_native.py
+++ b/edgetpuvision/gst_native.py
@@ -21,38 +21,17 @@
 gi.require_version('Gdk', '3.0')
 gi.require_version('GObject', '2.0')
 gi.require_version('Gst', '1.0')
-gi.require_version('GstBase', '1.0')
-gi.require_version('GstVideo', '1.0')
-from gi.repository import Gdk, GObject, Gst, GstBase, GstVideo
+from gi.repository import Gdk, GObject, Gst
 
 Gdk.init([])
 
-# Gst.Buffer.map(Gst.MapFlags.WRITE) is broken, this is a workaround. See
-# http://lifestyletransfer.com/how-to-make-gstreamer-buffer-writable-in-python/
-# https://gitlab.gnome.org/GNOME/gobject-introspection/issues/69
-class GstMapInfo(ctypes.Structure):
-    _fields_ = [('memory', ctypes.c_void_p),                # GstMemory *memory
-                ('flags', ctypes.c_int),                    # GstMapFlags flags
-                ('data', ctypes.POINTER(ctypes.c_byte)),    # guint8 *data
-                ('size', ctypes.c_size_t),                  # gsize size
-                ('maxsize', ctypes.c_size_t),               # gsize maxsize
-                ('user_data', ctypes.c_void_p * 4),         # gpointer user_data[4]
-                ('_gst_reserved', ctypes.c_void_p * 4)]     # GST_PADDING
-
 # ctypes imports for missing or broken introspection APIs.
 libgst = ctypes.CDLL('libgstreamer-1.0.so.0')
 libgst.gst_context_writable_structure.restype = ctypes.c_void_p
 libgst.gst_context_writable_structure.argtypes = [ctypes.c_void_p]
 libgst.gst_structure_set.restype = ctypes.c_void_p
 libgst.gst_structure_set.argtypes = [ctypes.c_void_p, ctypes.c_char_p,
-        ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p]
-GST_MAP_INFO_POINTER = ctypes.POINTER(GstMapInfo)
-libgst.gst_buffer_map.argtypes = [ctypes.c_void_p, GST_MAP_INFO_POINTER, ctypes.c_int]
-libgst.gst_buffer_map.restype = ctypes.c_int
-libgst.gst_buffer_unmap.argtypes = [ctypes.c_void_p, GST_MAP_INFO_POINTER]
-libgst.gst_buffer_unmap.restype = None
-libgst.gst_mini_object_is_writable.argtypes = [ctypes.c_void_p]
-libgst.gst_mini_object_is_writable.restype = ctypes.c_int
+        ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
 
 libgdk = ctypes.CDLL('libgdk-3.so.0')
 libgdk.gdk_wayland_window_get_wl_surface.restype = ctypes.c_void_p
@@ -60,33 +39,6 @@
 libgdk.gdk_wayland_display_get_wl_display.restype = ctypes.c_void_p
 libgdk.gdk_wayland_display_get_wl_display.argtypes = [ctypes.c_void_p]
 
-libcairo = ctypes.CDLL('libcairo.so.2')
-libcairo.cairo_image_surface_create_for_data.restype = ctypes.c_void_p
-libcairo.cairo_image_surface_create_for_data.argtypes = [ctypes.c_void_p,
-        ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int]
-libcairo.cairo_surface_flush.restype = None
-libcairo.cairo_surface_flush.argtypes = [ctypes.c_void_p]
-libcairo.cairo_surface_destroy.restype = None
-libcairo.cairo_surface_destroy.argtypes = [ctypes.c_void_p]
-libcairo.cairo_format_stride_for_width.restype = ctypes.c_int
-libcairo.cairo_format_stride_for_width.argtypes = [ctypes.c_int, ctypes.c_int]
-libcairo.cairo_create.restype = ctypes.c_void_p
-libcairo.cairo_create.argtypes = [ctypes.c_void_p]
-libcairo.cairo_destroy.restype = None
-libcairo.cairo_destroy.argtypes = [ctypes.c_void_p]
-
-librsvg = ctypes.CDLL('librsvg-2.so.2')
-librsvg.rsvg_handle_new_from_data.restype = ctypes.c_void_p
-librsvg.rsvg_handle_new_from_data.argtypes = [ctypes.c_char_p, ctypes.c_size_t, ctypes.c_void_p]
-librsvg.rsvg_handle_render_cairo.restype = ctypes.c_bool
-librsvg.rsvg_handle_render_cairo.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
-librsvg.rsvg_handle_close.restype = ctypes.c_bool
-librsvg.rsvg_handle_close.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
-
-libgobject = ctypes.CDLL('libgobject-2.0.so.0')
-libgobject.g_object_unref.restype = None
-libgobject.g_object_unref.argtypes = [ctypes.c_void_p]
-
 def set_display_contexts(sink, widget):
     handle = libgdk.gdk_wayland_window_get_wl_surface(hash(widget.get_window()))
     sink.set_window_handle(handle)
@@ -98,190 +50,3 @@
             hash(GObject.TYPE_POINTER), wl_display, 0)
     sink.set_context(context)
 
-@contextlib.contextmanager
-def _gst_buffer_map(buffer, flags):
-    ptr = hash(buffer)
-    if flags & Gst.MapFlags.WRITE and libgst.gst_mini_object_is_writable(ptr) == 0:
-        raise ValueError('Buffer not writable')
-
-    mapping = GstMapInfo()
-    success = libgst.gst_buffer_map(ptr, mapping, flags)
-    if not success:
-        raise RuntimeError('gst_buffer_map failed')
-    try:
-        yield ctypes.cast(mapping.data, ctypes.POINTER(ctypes.c_byte * mapping.size)).contents
-    finally:
-        libgst.gst_buffer_unmap(ptr, mapping)
-
-class OverlaySource(GstBase.BaseSrc):
-    __gstmetadata__ = ('<longname>', '<class>', '<description>', '<author>')
-    __gsttemplates__ = (Gst.PadTemplate.new('src',
-                                               Gst.PadDirection.SRC,
-                                               Gst.PadPresence.ALWAYS,
-                                               Gst.Caps.from_string(
-                                                'video/x-raw,format=BGRA,framerate=0/1'
-                                                )))
-
-    @staticmethod
-    def _plugin_init(plugin):
-        gtype = GObject.type_register(OverlaySource)
-        Gst.Element.register(plugin, 'overlaysrc', 0, gtype)
-        return True
-
-    @staticmethod
-    def plugin_register():
-        version = Gst.version()
-        Gst.Plugin.register_static(
-            version[0], version[1],         # GStreamer version
-            '',                             # name
-            '',                             # description
-            OverlaySource._plugin_init,     # init_func
-            '',                             # version
-            'unknown',                      # license
-            '',                             # source
-            '',                             # package
-            ''                              # origin
-        )
-
-    def __init__(self):
-        GstBase.BaseSrc.__init__(self)
-        self.set_format(Gst.Format.TIME)
-        self.set_do_timestamp(False)
-        self.set_live(True)
-        self.cond = threading.Condition()
-        self.width = 0
-        self.height = 0
-        self.min_stride = 0
-        self.flushing = False
-        self.eos = False
-        self.svg = None
-        self.pts = 0
-
-
-    def do_decide_allocation(self, query):
-        if query.get_n_allocation_pools() > 0:
-            pool, size, min_buffers, max_buffers = query.parse_nth_allocation_pool(0)
-            query.set_nth_allocation_pool(0, pool, size, min_buffers, min(max_buffers, 3))
-        return GstBase.BaseSrc.do_decide_allocation(self, query)
-
-    def do_event(self, event):
-        if event.type == Gst.EventType.SEEK:
-            _, _, flags, _, _, _, _ = event.parse_seek()
-            if flags | Gst.SeekFlags.FLUSH:
-                self.send_event(Gst.Event.new_flush_start())
-                self.send_event(Gst.Event.new_flush_stop(True))
-            return True
-        return GstBase.BaseSrc.do_event(self, event)
-
-    def set_eos(self):
-        with self.cond:
-            self.eos = True
-
-    def do_start (self):
-        self.set_svg(None, 0)
-        return True
-
-    def do_stop (self):
-        self.set_svg(None, 0)
-        return True
-
-    def set_svg(self, svg, pts):
-        with self.cond:
-            self.svg = svg
-            self.pts = pts
-            self.eos = False
-            self.cond.notify_all()
-
-    def set_flushing(self, flushing):
-        with self.cond:
-            self.flushing = flushing
-            self.cond.notify_all()
-
-    def do_set_caps(self, caps):
-        structure = caps.get_structure(0)
-        self.width = structure.get_value('width')
-        self.height = structure.get_value('height')
-        self.min_stride = libcairo.cairo_format_stride_for_width(
-                int(cairo.FORMAT_ARGB32), self.width)
-        return True
-
-    def do_unlock(self):
-        self.set_flushing(True)
-        return True
-
-    def do_unlock_stop(self):
-        self.set_flushing(False)
-        return True
-
-    def get_flow_return_locked(self, default=None):
-        if self.eos:
-            self.eos = False
-            self.svg = None
-            return Gst.FlowReturn.EOS
-        if self.flushing:
-            return Gst.FlowReturn.FLUSHING
-        return default
-
-    def do_fill(self, offset, size, buf):
-        with self.cond:
-            result = self.get_flow_return_locked()
-            if result:
-                return result
-
-            while self.svg is None:
-                self.cond.wait()
-                result = self.get_flow_return_locked()
-                if result:
-                    return result
-
-            assert self.svg is not None
-            svg = self.svg
-            pts = self.pts
-            self.svg = None
-
-        self.render_svg(svg, buf)
-        buf.pts = pts
-
-        with self.cond:
-            return self.get_flow_return_locked(Gst.FlowReturn.OK)
-
-    def render_svg(self, svg, buf):
-        meta = GstVideo.buffer_get_video_meta(buf)
-        if meta:
-            assert meta.n_planes == 1
-            assert meta.width == self.width
-            assert meta.height == self.height
-            assert meta.stride[0] >= self.min_stride
-            stride = meta.stride[0]
-        else:
-            stride = self.min_stride
-
-        with _gst_buffer_map(buf, Gst.MapFlags.WRITE) as mapped:
-            assert len(mapped) >= stride * self.height
-
-            # Fill with transparency.
-            ctypes.memset(ctypes.addressof(mapped), 0, ctypes.sizeof(mapped))
-
-            # If svg is '' (can't be None here) we return 100% transparency.
-            if not svg:
-                return
-
-            surface = libcairo.cairo_image_surface_create_for_data(
-                    ctypes.addressof(mapped),
-                    int(cairo.FORMAT_ARGB32),
-                    self.width,
-                    self.height,
-                    stride)
-
-            # Render the SVG overlay.
-            data = svg.encode('utf-8')
-            context = libcairo.cairo_create(surface)
-            handle = librsvg.rsvg_handle_new_from_data(data, len(data), 0)
-            librsvg.rsvg_handle_render_cairo(handle, context)
-            librsvg.rsvg_handle_close(handle, 0)
-            libgobject.g_object_unref(handle)
-            libcairo.cairo_surface_flush(surface)
-            libcairo.cairo_surface_destroy(surface)
-            libcairo.cairo_destroy(context)
-
-OverlaySource.plugin_register()
diff --git a/edgetpuvision/gstreamer.py b/edgetpuvision/gstreamer.py
index 6e78f22..8e0e95e 100644
--- a/edgetpuvision/gstreamer.py
+++ b/edgetpuvision/gstreamer.py
@@ -231,7 +231,7 @@
                              command=custom_command)
         overlay = pipeline.get_by_name('overlay')
         if overlay:
-            overlay.set_svg(svg, pts)
+            overlay.emit('queue-svg', svg, pts)
 
         if save_frame:
             images.put((data, layout.inference_size, svg))
diff --git a/edgetpuvision/pipelines.py b/edgetpuvision/pipelines.py
index 3a59d93..8d3f795 100644
--- a/edgetpuvision/pipelines.py
+++ b/edgetpuvision/pipelines.py
@@ -71,10 +71,7 @@
          Filter('imagefreeze'),
          Filter('glupload'),
          Pad('mixer')],
-        [Source('overlay', name='overlay'),
-         Caps('video/x-raw', format='BGRA', width=layout.render_size.width, height=layout.render_size.height),
-         Filter('glupload'),
-         Queue(max_size_buffers=1),
+        [Source('glsvgoverlay', name='overlay', width=layout.render_size.width, height=layout.render_size.height),
          Pad('mixer')],
         [Pad('t'),
          Queue(),
@@ -90,13 +87,9 @@
          Filter('glupload'),
          Tee(name='t')],
         [Pad('t'),
-         Filter('glupload'),
          Queue(),
          Pad('mixer')],
-        [Source('overlay', name='overlay'),
-         Caps('video/x-raw', format='BGRA', width=layout.render_size.width, height=layout.render_size.height),
-         Filter('glupload'),
-         Queue(max_size_buffers=1),
+        [Source('glsvgoverlay', name='overlay', width=layout.render_size.width, height=layout.render_size.height),
          Pad('mixer')],
         [Pad('t'),
          Queue(max_size_buffers=1, leaky='downstream'),
@@ -111,13 +104,9 @@
          Filter('glupload'),
          Tee(name='t')],
         [Pad('t'),
-         Filter('glupload'),
          Queue(),
          Pad('mixer')],
-        [Source('overlay', name='overlay'),
-         Caps('video/x-raw', format='BGRA', width=layout.render_size.width, height=layout.render_size.height),
-         Filter('glupload'),
-         Queue(max_size_buffers=1),
+        [Source('glsvgoverlay', name='overlay', width=layout.render_size.width, height=layout.render_size.height),
          Pad('mixer')],
         [Pad(name='t'),
          Queue(max_size_buffers=1, leaky='downstream'),
diff --git a/plugins/glsvgoverlaysrc.py b/plugins/glsvgoverlaysrc.py
new file mode 100644
index 0000000..1759a95
--- /dev/null
+++ b/plugins/glsvgoverlaysrc.py
@@ -0,0 +1,369 @@
+# Copyright 2019 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import cairo
+import collections
+import contextlib
+import ctypes
+import os
+import threading
+import time
+
+import gi
+gi.require_version('GObject', '2.0')
+gi.require_version('GLib', '2.0')
+gi.require_version('Gst', '1.0')
+gi.require_version('GstBase', '1.0')
+gi.require_version('GstVideo', '1.0')
+from gi.repository import GObject, GLib, Gst, GstBase, GstVideo
+
+# Gst.Buffer.map(Gst.MapFlags.WRITE) is broken, this is a workaround. See
+# http://lifestyletransfer.com/how-to-make-gstreamer-buffer-writable-in-python/
+# https://gitlab.gnome.org/GNOME/gobject-introspection/issues/69
+class GstMapInfo(ctypes.Structure):
+    _fields_ = [('memory', ctypes.c_void_p),                # GstMemory *memory
+                ('flags', ctypes.c_int),                    # GstMapFlags flags
+                ('data', ctypes.POINTER(ctypes.c_byte)),    # guint8 *data
+                ('size', ctypes.c_size_t),                  # gsize size
+                ('maxsize', ctypes.c_size_t),               # gsize maxsize
+                ('user_data', ctypes.c_void_p * 4),         # gpointer user_data[4]
+                ('_gst_reserved', ctypes.c_void_p * 4)]     # GST_PADDING
+
+# ctypes imports for missing or broken introspection APIs.
+libgst = ctypes.CDLL('libgstreamer-1.0.so.0')
+GST_MAP_INFO_POINTER = ctypes.POINTER(GstMapInfo)
+libgst.gst_buffer_map.argtypes = [ctypes.c_void_p, GST_MAP_INFO_POINTER, ctypes.c_int]
+libgst.gst_buffer_map.restype = ctypes.c_int
+libgst.gst_buffer_unmap.argtypes = [ctypes.c_void_p, GST_MAP_INFO_POINTER]
+libgst.gst_buffer_unmap.restype = None
+libgst.gst_mini_object_is_writable.argtypes = [ctypes.c_void_p]
+libgst.gst_mini_object_is_writable.restype = ctypes.c_int
+
+libcairo = ctypes.CDLL('libcairo.so.2')
+libcairo.cairo_image_surface_create_for_data.restype = ctypes.c_void_p
+libcairo.cairo_image_surface_create_for_data.argtypes = [ctypes.c_void_p,
+        ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int]
+libcairo.cairo_surface_flush.restype = None
+libcairo.cairo_surface_flush.argtypes = [ctypes.c_void_p]
+libcairo.cairo_surface_destroy.restype = None
+libcairo.cairo_surface_destroy.argtypes = [ctypes.c_void_p]
+libcairo.cairo_format_stride_for_width.restype = ctypes.c_int
+libcairo.cairo_format_stride_for_width.argtypes = [ctypes.c_int, ctypes.c_int]
+libcairo.cairo_create.restype = ctypes.c_void_p
+libcairo.cairo_create.argtypes = [ctypes.c_void_p]
+libcairo.cairo_destroy.restype = None
+libcairo.cairo_destroy.argtypes = [ctypes.c_void_p]
+
+librsvg = ctypes.CDLL('librsvg-2.so.2')
+librsvg.rsvg_handle_new_from_data.restype = ctypes.c_void_p
+librsvg.rsvg_handle_new_from_data.argtypes = [ctypes.c_char_p, ctypes.c_size_t, ctypes.c_void_p]
+librsvg.rsvg_handle_render_cairo.restype = ctypes.c_bool
+librsvg.rsvg_handle_render_cairo.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
+librsvg.rsvg_handle_close.restype = ctypes.c_bool
+librsvg.rsvg_handle_close.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
+
+libgobject = ctypes.CDLL('libgobject-2.0.so.0')
+libgobject.g_object_unref.restype = None
+libgobject.g_object_unref.argtypes = [ctypes.c_void_p]
+
+@contextlib.contextmanager
+def _gst_buffer_map(buffer, flags):
+    ptr = hash(buffer)
+    if flags & Gst.MapFlags.WRITE and libgst.gst_mini_object_is_writable(ptr) == 0:
+        raise ValueError('Buffer not writable')
+
+    mapping = GstMapInfo()
+    success = libgst.gst_buffer_map(ptr, mapping, flags)
+    if not success:
+        raise RuntimeError('gst_buffer_map failed')
+    try:
+        yield ctypes.cast(mapping.data, ctypes.POINTER(ctypes.c_byte * mapping.size)).contents
+    finally:
+        libgst.gst_buffer_unmap(ptr, mapping)
+
+DEFAULT_IS_LIVE = True
+DEFAULT_HEIGHT = 640
+DEFAULT_WIDTH = 480
+
+class SvgOverlaySource(GstBase.PushSrc):
+    __gstmetadata__ = ('SVG overlay source',
+                       'Source',
+                       'Renders SVG using rsvg & cairo',
+                       'Coral <coral-support@google.com>')
+    __gsttemplates__ = (Gst.PadTemplate.new('src',
+                        Gst.PadDirection.SRC,
+                        Gst.PadPresence.ALWAYS,
+                        Gst.Caps.from_string(
+                        'video/x-raw,format=BGRA,framerate=0/1'
+                        )))
+
+    def __init__(self):
+        GstBase.PushSrc.__init__(self)
+        self.set_format(Gst.Format.TIME)
+        self.set_do_timestamp(False)
+        self.set_live(DEFAULT_IS_LIVE)
+        self.cond = threading.Condition()
+        self.width = DEFAULT_WIDTH
+        self.height = DEFAULT_HEIGHT
+        self.min_stride = 0
+        self.flushing = False
+        self.eos = False
+        self.queue = collections.deque()
+        self.print_fps = int(os.environ.get('PRINT_FPS', '0'))
+        self.fps_start = 0
+        self.input_frames = 0
+        self.output_frames = 0
+
+    def reset(self):
+        with self.cond:
+            self.eos = False
+            self.queue.clear()
+
+    def do_decide_allocation(self, query):
+        if query.get_n_allocation_pools() > 0:
+            pool, size, min_buffers, max_buffers = query.parse_nth_allocation_pool(0)
+            query.set_nth_allocation_pool(0, pool, size, min_buffers, min(max_buffers, 3))
+        return GstBase.BaseSrc.do_decide_allocation(self, query)
+
+    def do_event(self, event):
+        if event.type == Gst.EventType.SEEK:
+            _, _, flags, _, _, _, _ = event.parse_seek()
+            if flags | Gst.SeekFlags.FLUSH:
+                self.send_event(Gst.Event.new_flush_start())
+                self.send_event(Gst.Event.new_flush_stop(True))
+            self.reset()
+            return True
+        return GstBase.BaseSrc.do_event(self, event)
+
+    def do_gst_base_src_query(self, query):
+        if query.type == Gst.QueryType.LATENCY:
+            query.set_latency(self.is_live, 0, Gst.CLOCK_TIME_NONE)
+            return True
+        else:
+            return GstBase.BaseSrc.do_query(self, query)
+
+    def do_start (self):
+        self.reset()
+        return True
+
+    def do_stop (self):
+        self.reset()
+        return True
+
+    def set_eos(self):
+        with self.cond:
+            self.eos = True
+
+    def queue_svg(self, svg:str, pts:GObject.TYPE_UINT64):
+        with self.cond:
+            self.input_frames += 1
+            if self.is_live:
+                self.queue.clear()
+            self.queue.append((svg, pts))
+            self.cond.notify_all()
+
+    def set_flushing(self, flushing):
+        with self.cond:
+            self.flushing = flushing
+            self.cond.notify_all()
+
+    def do_fixate(self, caps):
+        s = caps.get_structure(0).copy()
+        s.fixate_field_nearest_int("width", self.width)
+        s.fixate_field_nearest_int("height", self.height)
+        result = Gst.Caps.new_empty()
+        result.append_structure(s)
+        result = result.fixate()
+        return result
+
+    def do_set_caps(self, caps):
+        structure = caps.get_structure(0)
+        self.width = structure.get_value('width')
+        self.height = structure.get_value('height')
+        self.min_stride = libcairo.cairo_format_stride_for_width(
+                int(cairo.FORMAT_ARGB32), self.width)
+        return True
+
+    def do_unlock(self):
+        self.set_flushing(True)
+        return True
+
+    def do_unlock_stop(self):
+        self.set_flushing(False)
+        return True
+
+    def get_flow_return_locked(self, default=None):
+        if not self.queue and self.eos:
+            self.eos = False
+            return Gst.FlowReturn.EOS
+        if self.flushing:
+            return Gst.FlowReturn.FLUSHING
+        return default
+
+    def do_gst_push_src_fill(self, buf):
+        with self.cond:
+            result = self.get_flow_return_locked()
+            if result:
+                return result
+
+            while not self.queue:
+                self.cond.wait()
+                result = self.get_flow_return_locked()
+                if result:
+                    return result
+
+            assert self.queue
+            svg, pts = self.queue.popleft()
+
+        self.render_svg(svg, buf)
+        buf.pts = pts
+
+        with self.cond:
+            self.output_frames += 1
+            if not self.fps_start:
+                self.fps_start = time.monotonic()
+
+            elapsed = time.monotonic() - self.fps_start
+            if self.print_fps and elapsed > self.print_fps:
+                print('gloverlaysrc: in {} ({:.2f} fps), out {} ({:.2f} fps)'.format(
+                    self.input_frames, self.input_frames / elapsed,
+                    self.output_frames, self.output_frames / elapsed))
+                self.fps_start = time.monotonic()
+                self.input_frames = 0
+                self.output_frames = 0
+            return self.get_flow_return_locked(Gst.FlowReturn.OK)
+
+    def render_svg(self, svg, buf):
+        meta = GstVideo.buffer_get_video_meta(buf)
+        if meta:
+            assert meta.n_planes == 1
+            assert meta.width == self.width
+            assert meta.height == self.height
+            assert meta.stride[0] >= self.min_stride
+            stride = meta.stride[0]
+        else:
+            stride = self.min_stride
+
+        with _gst_buffer_map(buf, Gst.MapFlags.WRITE) as mapped:
+            assert len(mapped) >= stride * self.height
+
+            # Fill with transparency.
+            ctypes.memset(ctypes.addressof(mapped), 0, ctypes.sizeof(mapped))
+
+            surface = libcairo.cairo_image_surface_create_for_data(
+                    ctypes.addressof(mapped),
+                    int(cairo.FORMAT_ARGB32),
+                    self.width,
+                    self.height,
+                    stride)
+
+            # Render the SVG overlay.
+            data = svg.encode('utf-8')
+            context = libcairo.cairo_create(surface)
+            handle = librsvg.rsvg_handle_new_from_data(data, len(data), 0)
+            librsvg.rsvg_handle_render_cairo(handle, context)
+            librsvg.rsvg_handle_close(handle, 0)
+            libgobject.g_object_unref(handle)
+            libcairo.cairo_surface_flush(surface)
+            libcairo.cairo_surface_destroy(surface)
+            libcairo.cairo_destroy(context)
+
+class GlSvgOverlaySource(Gst.Bin):
+    __gstmetadata__ = ('GL SVG overlay source',
+                       'Source',
+                       'Renders SVG to OpenGL textures',
+                       'Coral <coral-support@google.com>')
+    __gsttemplates__ = (Gst.PadTemplate.new('src',
+                        Gst.PadDirection.SRC,
+                        Gst.PadPresence.ALWAYS,
+                        Gst.Caps.from_string(
+                        'video/x-raw(memory:GLMemory),format=RGBA,framerate=0/1'
+                        )))
+    __gproperties__ = {
+        "is-live": (bool,
+            "Is live",
+            "Whether to act as a live source",
+            DEFAULT_IS_LIVE,
+            GObject.ParamFlags.READWRITE
+            ),
+        "width": (int,
+            "Frame width",
+            "Frame width, also settable via caps negotiation",
+            1,
+            GLib.MAXINT,
+            DEFAULT_WIDTH,
+            GObject.ParamFlags.READWRITE
+            ),
+        "height": (int,
+            "Frame height",
+            "Frame height, also settable via caps negotiation",
+            1,
+            GLib.MAXINT,
+            DEFAULT_WIDTH,
+            GObject.ParamFlags.READWRITE
+            ),
+        }
+
+
+    def __init__(self):
+        GstBase.PushSrc.__init__(self)
+        self.src = SvgOverlaySource()
+        self.queue = Gst.ElementFactory.make('queue')
+        self.upload = Gst.ElementFactory.make('glupload')
+        self.convert = Gst.ElementFactory.make('glcolorconvert')
+        self.filter = Gst.ElementFactory.make('capsfilter')
+
+        self.add(self.src)
+        self.add(self.queue)
+        self.add(self.upload)
+        self.add(self.convert)
+        self.add(self.filter)
+
+        self.src.link_pads('src', self.queue, 'sink')
+        self.queue.link_pads('src', self.upload, 'sink')
+        self.upload.link_pads('src', self.convert, 'sink')
+        self.convert.link_pads('src', self.filter, 'sink')
+        pad = Gst.GhostPad('src', self.filter.get_static_pad('src'))
+        self.add_pad(pad)
+        self.filter.set_property('caps', Gst.Caps.from_string('video/x-raw(memory:GLMemory),format=RGBA'))
+
+    def do_get_property(self, prop):
+        if prop.name == 'is-live':
+            return self.src.is_live
+        elif prop.name == 'width':
+            return self.src.width
+        elif prop.name == 'height':
+            return self.src.height
+        else:
+            raise AttributeError('Unknown property %s' % prop.name)
+
+    def do_set_property(self, prop, value):
+        if prop.name == 'is-live':
+            self.src.set_live(value)
+        elif prop.name == 'width':
+            self.src.width = value
+        elif prop.name == 'height':
+            self.src.height = value
+        else:
+            raise AttributeError('Unknown property %s' % prop.name)
+
+    @GObject.Signal(flags=GObject.SignalFlags.ACTION | GObject.SignalFlags.RUN_LAST)
+    def set_eos(self):
+        self.src.set_eos()
+
+    @GObject.Signal(flags=GObject.SignalFlags.ACTION | GObject.SignalFlags.RUN_LAST)
+    def queue_svg(self, svg:str, pts:GObject.TYPE_UINT64):
+        self.src.queue_svg(svg, pts)
+
+__gstelementfactory__ = ("glsvgoverlaysrc", Gst.Rank.NONE, GlSvgOverlaySource)