| # 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 contextlib |
| import ctypes |
| import threading |
| |
| import gi |
| 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 |
| |
| 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 |
| |
| libgdk = ctypes.CDLL('libgdk-3.so.0') |
| libgdk.gdk_wayland_window_get_wl_surface.restype = ctypes.c_void_p |
| libgdk.gdk_wayland_window_get_wl_surface.argtypes = [ctypes.c_void_p] |
| 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) |
| |
| wl_display = libgdk.gdk_wayland_display_get_wl_display(hash(Gdk.Display.get_default())) |
| context = Gst.Context.new('GstWaylandDisplayHandleContextType', True) |
| structure = libgst.gst_context_writable_structure(hash(context)) |
| libgst.gst_structure_set(structure, ctypes.c_char_p('display'.encode()), |
| 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.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') |
| 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 |
| |
| # Note: Buffer IS writable (ref is 1 in native land). However gst-python |
| # took an additional ref so it's now 2 and gst_buffer_is_writable |
| # returns false. We can't modify the buffer without fiddling with refcount. |
| assert buf.mini_object.refcount == 2 |
| buf.mini_object.refcount = 1 |
| try: |
| self.render_svg(svg, buf) |
| buf.pts = pts |
| finally: |
| buf.mini_object.refcount = 2 |
| |
| with self.cond: |
| return self.get_flow_return_locked(Gst.FlowReturn.OK) |
| |
| def render_svg(self, svg, buf): |
| with _gst_buffer_map(buf, Gst.MapFlags.WRITE) as mapped: |
| stride = libcairo.cairo_format_stride_for_width( |
| int(cairo.FORMAT_ARGB32), self.width) |
| 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() |