blob: 6f800b6d3ddcb5f3db70ae1614e128a873333f8c [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 numpy
import os
import threading
import time
import gi
gi.require_version('Gdk', '3.0')
gi.require_version('GObject', '2.0')
gi.require_version('GLib', '2.0')
gi.require_version('Gst', '1.0')
gi.require_version('GstAllocators', '1.0')
gi.require_version('GstBase', '1.0')
gi.require_version('GstGL', '1.0')
gi.require_version('GstVideo', '1.0')
from gi.repository import Gdk, GObject, GLib, Gst, GstAllocators, GstBase, GstGL, GstVideo
from OpenGL.arrays.arraydatatype import ArrayDatatype
from OpenGL.GLES3 import (
glActiveTexture, glBindBuffer, glBindTexture, glBindVertexArray, glBlendEquation, glBlendFunc,
glBufferData, glDeleteBuffers, glDeleteVertexArrays, glDisable, glDrawElements, glEnable,
glEnableVertexAttribArray, glGenBuffers, glGenVertexArrays, glVertexAttribPointer,glViewport)
from OpenGL.GLES3 import (
GL_ARRAY_BUFFER, GL_BLEND, GL_ELEMENT_ARRAY_BUFFER, GL_FALSE, GL_FLOAT, GL_FUNC_ADD,
GL_ONE_MINUS_SRC_ALPHA, GL_SRC_ALPHA, GL_STATIC_DRAW, GL_TEXTURE0, GL_TEXTURE_2D,
GL_TRIANGLES, GL_UNSIGNED_SHORT)
# 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
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_void_p, ctypes.c_void_p, ctypes.c_void_p]
libgstgl = ctypes.CDLL('libgstgl-1.0.so.0')
libgstgl.gst_gl_memory_get_texture_id.argtypes = [ctypes.c_void_p]
libgstgl.gst_gl_memory_get_texture_id.restype = ctypes.c_uint
libgstgl.gst_gl_sync_meta_wait_cpu.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
libgstgl.gst_gl_sync_meta_wait_cpu.restype = None
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]
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]
@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)
def _get_gl_texture_id(buf):
if not buf:
return 0
memory = buf.peek_memory(0)
assert GstGL.is_gl_memory(memory)
return libgstgl.gst_gl_memory_get_texture_id(hash(memory))
POSITIONS = numpy.array([
1.0, 1.0,
-1.0, 1.0,
-1.0, -1.0,
1.0, -1.0,
], dtype=numpy.float32)
TEXCOORDS = numpy.array([
1.0, 0.0,
0.0, 0.0,
0.0, 1.0,
1.0, 1.0,
], dtype=numpy.float32)
INDICES = numpy.array([
0, 1, 2, 0, 2, 3
], dtype=numpy.uint16)
NUM_BUFFERS = 2
class DmaOverlayBuffer():
def __init__(self, pool, glupload):
self.glcontext = glupload.context
res, self.dmabuf = pool.acquire_buffer()
assert res == Gst.FlowReturn.OK
assert GstAllocators.is_dmabuf_memory(self.dmabuf.peek_memory(0))
with _gst_buffer_map(self.dmabuf, Gst.MapFlags.WRITE) as mapped:
self.ptr = ctypes.addressof(mapped)
self.len = ctypes.sizeof(mapped)
self.clear()
meta = GstVideo.buffer_get_video_meta(self.dmabuf)
assert meta
self.surface = libcairo.cairo_image_surface_create_for_data(
self.ptr,
int(cairo.FORMAT_ARGB32),
meta.width,
meta.height,
meta.stride[0])
self.cairo = libcairo.cairo_create(self.surface)
res, self.gl_buffer = glupload.perform_with_buffer(self.dmabuf)
assert res == GstGL.GLUploadReturn.DONE
memory = self.gl_buffer.peek_memory(0)
assert GstGL.is_gl_memory(memory)
self.texture_id = libgstgl.gst_gl_memory_get_texture_id(hash(memory))
self.sync = GstGL.buffer_add_gl_sync_meta(self.glcontext, self.gl_buffer)
def __del__(self):
if self.surface:
libcairo.cairo_surface_destroy(self.surface)
if self.cairo:
libcairo.cairo_destroy(self.cairo)
def render_svg(self, svg):
self.wait_sync()
self.clear()
if svg:
data = svg.encode()
handle = librsvg.rsvg_handle_new_from_data(data, len(data), 0)
if handle:
librsvg.rsvg_handle_render_cairo(handle, self.cairo)
librsvg.rsvg_handle_close(handle, 0)
libgobject.g_object_unref(handle)
libcairo.cairo_surface_flush(self.surface)
def clear(self):
ctypes.memset(self.ptr, 0, self.len)
def set_sync_point(self):
self.sync.set_sync_point(self.glcontext)
def wait_sync(self):
libgstgl.gst_gl_sync_meta_wait_cpu(hash(self.sync), hash(self.glcontext))
class GlSvgOverlaySink(Gst.Bin, GstVideo.VideoOverlay):
__gstmetadata__ = ('GlSvgOverlaySink',
'Sink/Video',
'glimagesink with SVG overlays',
'Coral <coral-support@google.com>')
__gsttemplates__ = (Gst.PadTemplate.new('sink',
Gst.PadDirection.SINK,
Gst.PadPresence.ALWAYS,
GstGL.GLUpload.get_input_template_caps()
))
__gproperties__ = {
'svg': (str,
'SVG data',
'SVG overlay data',
'',
GObject.ParamFlags.WRITABLE
),
}
__gsignals__ = {
'drawn': (GObject.SignalFlags.RUN_LAST, None, ())
}
def __init__(self):
Gst.Bin.__init__(self)
self.shader = None
self.vao = 0
self.positions_buffer = 0
self.texcoords_buffer = 0
self.vbo_indices = 0
self.glcontext = None
self.glimagesink = Gst.ElementFactory.make('glimagesink')
self.add(self.glimagesink)
self.add_pad(Gst.GhostPad('sink', self.glimagesink.get_static_pad('sink')))
self.glimagesink.connect('client-draw', self.on_draw)
self.glimagesink.connect('client-reshape', self.on_reshape)
self.glimagesink.get_static_pad('sink').add_probe(
Gst.PadProbeType.EVENT_UPSTREAM, self.on_glimagesink_event)
self.render_thread = None
self.cond = threading.Condition()
self.rendering = False
self.svg = None
self.buffers = [None] * NUM_BUFFERS
self.index = 0
self.print_fps = int(os.environ.get('PRINT_FPS', '0'))
self.incoming_frames = 0
self.incoming_overlays = 0
self.rendered_overlays = 0
self.draws = 0
self.fps_start = 0
if self.print_fps:
self.glimagesink.get_static_pad('sink').add_probe(
Gst.PadProbeType.BUFFER, self.on_incoming_frame)
def do_expose(self):
self.glimagesink.expose()
def do_handle_events(self, handle_events):
self.glimagesink.handle_events(handle_events)
def do_set_render_rectangle(self, x, y, width, height):
return self.glimagesink.set_render_rectangle(x, y, width, height)
def do_set_window_handle(self, handle):
self.glimagesink.set_window_handle(handle)
def on_glimagesink_event(self, pad, info):
event = info.get_event()
if event.type == Gst.EventType.RECONFIGURE:
return Gst.PadProbeReturn.DROP
return Gst.PadProbeReturn.OK
def on_glimagesink_event(self, pad, info):
event = info.get_event()
if event.type == Gst.EventType.RECONFIGURE:
return Gst.PadProbeReturn.DROP
return Gst.PadProbeReturn.OK
def on_incoming_frame(self, pad, info):
self.incoming_frames += 1
return Gst.PadProbeReturn.OK
def post_error(self, string, debug=''):
Gst.error(string)
gerror = GLib.Error.new_literal(Gst.ResourceError.quark(), string, Gst.CoreError.FAILED)
message = Gst.Message.new_error(self, gerror, debug)
return self.post_message(message)
def do_change_state(self, transition):
if transition == Gst.StateChange.READY_TO_NULL:
self.glcontext.thread_add(self.deinit_gl)
self.glcontext = None
elif transition == Gst.StateChange.PAUSED_TO_READY:
with self.cond:
self.rendering = False
self.cond.notify_all()
self.render_thread.join()
self.glcontext.thread_add(self.free_buffers)
result = Gst.Bin.do_change_state(self, transition)
if transition == Gst.StateChange.NULL_TO_READY:
self.glcontext = self.glimagesink.get_property('context')
if self.glcontext:
self.glcontext.thread_add(self.init_gl)
else:
self.post_error('failed to get gl context')
result = Gst.StateChangeReturn.FAILURE
elif transition == Gst.StateChange.READY_TO_PAUSED:
self.try_create_buffers()
self.rendering = True
self.render_thread = threading.Thread(target=self.render_loop)
self.render_thread.start()
elif transition == Gst.StateChange.PAUSED_TO_PLAYING:
self.try_create_buffers()
return result
def do_set_property(self, prop, value):
if prop.name == 'svg':
if not self.get_back_buffer():
Gst.warning('Not ready to draw overlays, dropping data')
return
with self.cond:
self.incoming_overlays += 1
self.svg = value or ''
self.cond.notify_all()
else:
self.glimagesink.set_property(prop, value)
def do_get_property(self, prop):
return self.glimagesink.get_property(prop)
def init_gl(self, glcontext):
assert not self.shader
assert glcontext == self.glcontext
self.shader = GstGL.GLShader.new_default(self.glcontext)
a_position = self.shader.get_attribute_location('a_position')
a_texcoord = self.shader.get_attribute_location('a_texcoord')
self.vao = glGenVertexArrays(1)
glBindVertexArray(self.vao)
self.positions_buffer = glGenBuffers(1)
glBindBuffer(GL_ARRAY_BUFFER, self.positions_buffer)
glBufferData(GL_ARRAY_BUFFER, ArrayDatatype.arrayByteCount(POSITIONS), POSITIONS, GL_STATIC_DRAW)
self.texcoords_buffer = glGenBuffers(1)
glBindBuffer(GL_ARRAY_BUFFER, self.texcoords_buffer)
glBufferData(GL_ARRAY_BUFFER, ArrayDatatype.arrayByteCount(TEXCOORDS), TEXCOORDS, GL_STATIC_DRAW)
self.vbo_indices = glGenBuffers(1)
glBindBuffer(GL_ARRAY_BUFFER, self.vbo_indices)
glBufferData(GL_ARRAY_BUFFER, ArrayDatatype.arrayByteCount(INDICES), INDICES, GL_STATIC_DRAW)
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, self.vbo_indices);
glBindBuffer(GL_ARRAY_BUFFER, self.positions_buffer);
glVertexAttribPointer.baseFunction(a_position, 2, GL_FLOAT, GL_FALSE, 0, None)
glBindBuffer(GL_ARRAY_BUFFER, self.texcoords_buffer);
glVertexAttribPointer.baseFunction(a_texcoord, 2, GL_FLOAT, GL_FALSE, 0, None)
glEnableVertexAttribArray(a_position)
glEnableVertexAttribArray(a_texcoord)
glBindVertexArray(0)
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)
glBindBuffer(GL_ARRAY_BUFFER, 0)
def deinit_gl(self, glcontext):
assert glcontext == self.glcontext
self.overlay_shader = None
self.shader = None
glDeleteVertexArrays(1, [self.vao])
self.vao = None
glDeleteBuffers(1, [self.positions_buffer])
self.positions_buffer = None
glDeleteBuffers(1, [self.texcoords_buffer])
self.texcoords_buffer = None
glDeleteBuffers(1, [self.vbo_indices])
self.vbo_indices = None
def on_reshape(self, sink, context, width, height):
sinkelement = self.glimagesink.get_by_interface(GstVideo.VideoOverlay)
if sinkelement.width and sinkelement.height:
src_ratio = sinkelement.width / sinkelement.height
dst_ratio = width / height
if src_ratio > dst_ratio:
w = width
h = width / src_ratio
x = 0
y = (height - h) / 2
elif src_ratio < dst_ratio:
w = height * src_ratio
h = height
x = (width - w) / 2
y = 0
else:
w = width
h = height;
x = 0
y = 0
else:
w = width
h = height
x = 0
y = 0
glViewport(int(x), int(y), int(w), int(h))
return True
# TODO: affine gltransformation support
def on_draw(self, sink, context, sample):
assert context == self.glcontext
self.draws += 1
assert context == self.glcontext
frame_texture = _get_gl_texture_id(sample.get_buffer())
overlay_buffer = self.get_front_buffer()
overlay_texture = overlay_buffer.texture_id if overlay_buffer else 0
glDisable(GL_BLEND)
glBindVertexArray(self.vao)
glActiveTexture(GL_TEXTURE0)
glBindTexture(GL_TEXTURE_2D, frame_texture)
self.shader.use()
self.shader.set_uniform_1i('frame', 0)
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, None)
if overlay_texture:
glBindTexture(GL_TEXTURE_2D, overlay_texture)
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glBlendEquation(GL_FUNC_ADD)
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, None)
glActiveTexture(GL_TEXTURE0)
glBindTexture(GL_TEXTURE_2D, 0)
glDisable(GL_BLEND)
context.clear_shader()
glBindVertexArray(0)
if overlay_buffer:
overlay_buffer.set_sync_point()
self.emit('drawn')
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:
incoming_fps = self.incoming_frames / elapsed
draw_fps = self.draws / elapsed
incoming_overlay_fps = self.incoming_overlays / elapsed
render_fps = self.rendered_overlays / elapsed
print('glsvgoverlaysink: in frames {} ({:.2f} fps) svg {} ({:.2f} fps), rendered {} ({:.2f} fps), draw {} ({:.2f} fps)'.format(
self.incoming_frames, self.incoming_frames / elapsed,
self.incoming_overlays, self.incoming_overlays / elapsed,
self.rendered_overlays, self.rendered_overlays / elapsed,
self.draws, self.draws / elapsed))
self.incoming_frames = 0
self.incoming_overlays = 0
self.rendered_overlays = 0
self.draws = 0
self.fps_start = time.monotonic()
return True
def render_loop(self):
while True:
with self.cond:
rendering = self.rendering
svg = self.svg
self.svg = None
if not rendering:
break
if svg is None:
with self.cond:
self.cond.wait()
continue
buf = self.get_back_buffer()
buf.render_svg(svg)
self.rendered_overlays += 1
self.swap_buffers()
def try_create_buffers(self):
if self.buffers[0]:
return
sink = self.glimagesink.get_by_interface(GstVideo.VideoOverlay)
width = sink.width
height = sink.height
if not width or not height:
return
render_caps = 'video/x-raw, width={}, height={}, format=BGRA'
gl_caps = 'video/x-raw(memory:GLMemory), width={}, height={}, format=RGBA, texture-target=2D'
min_stride = libcairo.cairo_format_stride_for_width(int(cairo.FORMAT_ARGB32), width)
render_caps = Gst.Caps.from_string(render_caps.format(width, height))
gl_caps = Gst.Caps.from_string(gl_caps.format(width, height))
glupload = GstGL.GLUpload.new(self.glcontext)
glupload.set_caps(render_caps, gl_caps)
query = Gst.Query.new_allocation(render_caps, True)
glupload.propose_allocation(None, query)
assert query.get_n_allocation_pools()
pool, size, min_bufs, max_bufs = query.parse_nth_allocation_pool(0)
assert pool.set_active(True)
for i in range(0, NUM_BUFFERS):
self.buffers[i] = DmaOverlayBuffer(pool, glupload)
assert pool.set_active(False)
def free_buffers(self, glcontext):
self.buffers = [None] * NUM_BUFFERS
def get_front_buffer(self):
return self.buffers[(self.index - 1) % len(self.buffers)]
def get_back_buffer(self):
return self.buffers[self.index]
def swap_buffers(self):
self.index = (self.index + 1) % len(self.buffers)
def get_default_wayland_display_context(self):
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)
return context
def get_gl_display_context(self):
if not self.glcontext:
return None
context = Gst.Context.new(GstGL.GL_DISPLAY_CONTEXT_TYPE, True)
GstGL.context_set_gl_display(context, self.glcontext.get_display())
return context
def get_wayland_window_handle(self, widget):
return libgdk.gdk_wayland_window_get_wl_surface(hash(widget.get_window()))
def get_sharable_local_context(self):
if not self.glcontext:
return None
_, new_glcontext = self.glcontext.get_display().create_context(self.glcontext)
gst_context = Gst.Context.new('gst.gl.local_context', True)
structure = libgst.gst_context_writable_structure(hash(gst_context))
libgst.gst_structure_set(structure, ctypes.c_char_p('context'.encode()),
hash(GObject.GType.from_name('GstGLContext')), hash(new_glcontext), 0)
return gst_context
__gstelementfactory__ = ("glsvgoverlaysink", Gst.Rank.NONE, GlSvgOverlaySink)