blob: 1759a95f1fc7f06ef93c533dd80abd9e0e69bd6a [file] [log] [blame]
# 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)