blob: 5a0c5626c0902078caa88b962a39ac975b2a842c [file] [log] [blame]
import cairo
import contextlib
import ctypes
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_surface_status.restype = ctypes.c_int
libcairo.cairo_surface_status.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]
libcairo.cairo_scale.restype = None
libcairo.cairo_scale.argtypes = [ctypes.c_void_p, ctypes.c_double, ctypes.c_double]
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)
# GStreamer Element that attaches VideoOverlayComposition to buffers passing by.
class OverlayInjector(GstBase.BaseTransform):
__gstmetadata__ = ('<longname>', '<class>', '<description>', '<author>')
__gsttemplates__ = (Gst.PadTemplate.new('src',
Gst.PadDirection.SRC,
Gst.PadPresence.ALWAYS,
Gst.Caps.new_any()),
Gst.PadTemplate.new('sink',
Gst.PadDirection.SINK,
Gst.PadPresence.ALWAYS,
Gst.Caps.new_any()))
@staticmethod
def _plugin_init(plugin):
gtype = GObject.type_register(OverlayInjector)
Gst.Element.register(plugin, 'overlayinjector', 0, gtype)
return True
@staticmethod
def plugin_register():
version = Gst.version()
Gst.Plugin.register_static(
version[0], version[1], # GStreamer version
'', # name
'', # description
OverlayInjector._plugin_init, # init_func
'', # version
'unknown', # license
'', # source
'', # package
'' # origin
)
def __init__(self):
GstBase.BaseTransform.__init__(self)
GstBase.BaseTransform.set_in_place(self, True)
self.render_size = None
self.svg = None
self.rendered_svg = None
self.composition = None
self.scale_factor = 0.75
def set_svg(self, svg, render_size):
self.svg = svg
self.render_size = render_size
def do_transform_ip(self, frame_buf):
self.render()
if self.composition:
# 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.
if frame_buf.mini_object.refcount != 2:
return Gst.FlowReturn.ERROR
frame_buf.mini_object.refcount -= 1
GstVideo.buffer_add_video_overlay_composition_meta(frame_buf, self.composition)
frame_buf.mini_object.refcount += 1
return Gst.FlowReturn.OK
def render(self):
if not self.svg:
self.composition = None
self.rendered_svg = None
return
if self.svg == self.rendered_svg:
return
overlay_size = self.render_size * self.scale_factor
stride = libcairo.cairo_format_stride_for_width(
int(cairo.FORMAT_ARGB32), overlay_size.width)
overlay_buffer = Gst.Buffer.new_allocate(None,
stride * overlay_size.height)
with _gst_buffer_map(overlay_buffer, Gst.MapFlags.WRITE) as mapped:
# Fill with transparency and create surface from buffer.
ctypes.memset(ctypes.addressof(mapped), 0, ctypes.sizeof(mapped))
surface = libcairo.cairo_image_surface_create_for_data(
ctypes.addressof(mapped),
int(cairo.FORMAT_ARGB32),
overlay_size.width,
overlay_size.height,
stride)
# Render the SVG overlay.
data = self.svg.encode('utf-8')
context = libcairo.cairo_create(surface)
libcairo.cairo_scale(context, self.scale_factor, self.scale_factor)
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)
# Attach overlay to VideoOverlayComposition.
GstVideo.buffer_add_video_meta(overlay_buffer,
GstVideo.VideoFrameFlags.NONE, GstVideo.VideoFormat.BGRA,
overlay_size.width, overlay_size.height)
rect = GstVideo.VideoOverlayRectangle.new_raw(overlay_buffer,
0, 0, self.render_size.width, self.render_size.height,
GstVideo.VideoOverlayFormatFlags.PREMULTIPLIED_ALPHA)
self.composition = GstVideo.VideoOverlayComposition.new(rect)
self.rendered_svg = self.svg
OverlayInjector.plugin_register()