mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2025-04-26 06:54:49 +00:00
videotexturecache: implement GL specifics as a subclass
This commit is contained in:
parent
ff5ba2f126
commit
0560946c82
8 changed files with 374 additions and 234 deletions
|
@ -32,7 +32,7 @@
|
||||||
#include <gst/video/video.h>
|
#include <gst/video/video.h>
|
||||||
#include <gst/gl/gstglcontext.h>
|
#include <gst/gl/gstglcontext.h>
|
||||||
#include "coremediabuffer.h"
|
#include "coremediabuffer.h"
|
||||||
#include "videotexturecache.h"
|
#include "videotexturecache-gl.h"
|
||||||
|
|
||||||
#define DEFAULT_DEVICE_INDEX -1
|
#define DEFAULT_DEVICE_INDEX -1
|
||||||
#define DEFAULT_POSITION GST_AVF_VIDEO_SOURCE_POSITION_DEFAULT
|
#define DEFAULT_POSITION GST_AVF_VIDEO_SOURCE_POSITION_DEFAULT
|
||||||
|
@ -1088,15 +1088,19 @@ didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
|
||||||
gst_query_parse_allocation (query, &alloc_caps, NULL);
|
gst_query_parse_allocation (query, &alloc_caps, NULL);
|
||||||
features = gst_caps_get_features (alloc_caps, 0);
|
features = gst_caps_get_features (alloc_caps, 0);
|
||||||
if (gst_caps_features_contains (features, GST_CAPS_FEATURE_MEMORY_GL_MEMORY)) {
|
if (gst_caps_features_contains (features, GST_CAPS_FEATURE_MEMORY_GL_MEMORY)) {
|
||||||
|
GstVideoTextureCacheGL *cache_gl;
|
||||||
|
|
||||||
|
cache_gl = textureCache ? GST_VIDEO_TEXTURE_CACHE_GL (textureCache) : NULL;
|
||||||
|
|
||||||
gst_gl_context_helper_ensure_context (ctxh);
|
gst_gl_context_helper_ensure_context (ctxh);
|
||||||
GST_INFO_OBJECT (element, "pushing textures, context %p old context %p",
|
GST_INFO_OBJECT (element, "pushing textures, context %p old context %p",
|
||||||
ctxh->context, textureCache ? textureCache->ctx : NULL);
|
ctxh->context, cache_gl ? cache_gl->ctx : NULL);
|
||||||
if (textureCache && textureCache->ctx != ctxh->context) {
|
if (cache_gl && cache_gl->ctx != ctxh->context) {
|
||||||
g_object_unref (textureCache);
|
g_object_unref (textureCache);
|
||||||
textureCache = NULL;
|
textureCache = NULL;
|
||||||
}
|
}
|
||||||
if (!textureCache)
|
if (!textureCache)
|
||||||
textureCache = gst_video_texture_cache_new (ctxh->context);
|
textureCache = gst_video_texture_cache_gl_new (ctxh->context);
|
||||||
gst_video_texture_cache_set_format (textureCache, format, alloc_caps);
|
gst_video_texture_cache_set_format (textureCache, format, alloc_caps);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
#if !HAVE_IOS
|
#if !HAVE_IOS
|
||||||
#include "iosurfacememory.h"
|
#include "iosurfacememory.h"
|
||||||
#endif
|
#endif
|
||||||
|
#include "videotexturecache-gl.h"
|
||||||
|
|
||||||
static const GstMetaInfo *gst_core_video_meta_get_info (void);
|
static const GstMetaInfo *gst_core_video_meta_get_info (void);
|
||||||
|
|
||||||
|
@ -107,13 +108,15 @@ _create_glmem (GstAppleCoreVideoPixelBuffer * gpixbuf,
|
||||||
return gst_video_texture_cache_create_memory (cache, gpixbuf, plane, size);
|
return gst_video_texture_cache_create_memory (cache, gpixbuf, plane, size);
|
||||||
#else
|
#else
|
||||||
GstIOSurfaceMemory *mem;
|
GstIOSurfaceMemory *mem;
|
||||||
GstGLFormat tex_format =
|
|
||||||
gst_gl_format_from_video_info (cache->ctx, info, plane);
|
|
||||||
CVPixelBufferRef pixel_buf = gpixbuf->buf;
|
CVPixelBufferRef pixel_buf = gpixbuf->buf;
|
||||||
IOSurfaceRef surface = CVPixelBufferGetIOSurface (pixel_buf);
|
IOSurfaceRef surface = CVPixelBufferGetIOSurface (pixel_buf);
|
||||||
|
GstGLFormat tex_format;
|
||||||
|
GstVideoTextureCacheGL *cache_gl = GST_VIDEO_TEXTURE_CACHE_GL (cache);
|
||||||
|
|
||||||
|
tex_format = gst_gl_format_from_video_info (cache_gl->ctx, info, plane);
|
||||||
|
|
||||||
CFRetain (pixel_buf);
|
CFRetain (pixel_buf);
|
||||||
mem = gst_io_surface_memory_wrapped (cache->ctx,
|
mem = gst_io_surface_memory_wrapped (cache_gl->ctx,
|
||||||
surface, GST_GL_TEXTURE_TARGET_RECTANGLE, tex_format,
|
surface, GST_GL_TEXTURE_TARGET_RECTANGLE, tex_format,
|
||||||
info, plane, NULL, pixel_buf, (GDestroyNotify) CFRelease);
|
info, plane, NULL, pixel_buf, (GDestroyNotify) CFRelease);
|
||||||
return GST_MEMORY_CAST (mem);
|
return GST_MEMORY_CAST (mem);
|
||||||
|
@ -132,7 +135,7 @@ gst_core_video_wrap_pixel_buffer (GstBuffer * buf,
|
||||||
UInt32 size;
|
UInt32 size;
|
||||||
GstAppleCoreVideoPixelBuffer *gpixbuf;
|
GstAppleCoreVideoPixelBuffer *gpixbuf;
|
||||||
GstMemory *mem = NULL;
|
GstMemory *mem = NULL;
|
||||||
gboolean do_gl = cache != NULL;
|
gboolean do_gl = GST_IS_VIDEO_TEXTURE_CACHE_GL (cache);
|
||||||
|
|
||||||
gpixbuf = gst_apple_core_video_pixel_buffer_new (pixel_buf);
|
gpixbuf = gst_apple_core_video_pixel_buffer_new (pixel_buf);
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ applemedia_sources = [
|
||||||
'corevideobuffer.c',
|
'corevideobuffer.c',
|
||||||
'coremediabuffer.c',
|
'coremediabuffer.c',
|
||||||
'videotexturecache.m',
|
'videotexturecache.m',
|
||||||
|
'videotexturecache-gl.m',
|
||||||
'atdec.c',
|
'atdec.c',
|
||||||
'glcontexthelper.c'
|
'glcontexthelper.c'
|
||||||
]
|
]
|
||||||
|
|
59
sys/applemedia/videotexturecache-gl.h
Normal file
59
sys/applemedia/videotexturecache-gl.h
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2015 Alessandro Decina <twi@centricular.com>
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Library General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* Library General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Library General Public
|
||||||
|
* License along with this library; if not, write to the
|
||||||
|
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||||
|
* Boston, MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef __GST_CORE_VIDEO_TEXTURE_CACHE_GL_H__
|
||||||
|
#define __GST_CORE_VIDEO_TEXTURE_CACHE_GL_H__
|
||||||
|
|
||||||
|
#include <gst/video/gstvideometa.h>
|
||||||
|
#include <gst/gl/gl.h>
|
||||||
|
#include "corevideomemory.h"
|
||||||
|
#include "videotexturecache.h"
|
||||||
|
|
||||||
|
G_BEGIN_DECLS
|
||||||
|
|
||||||
|
#define GST_TYPE_VIDEO_TEXTURE_CACHE_GL (gst_video_texture_cache_gl_get_type())
|
||||||
|
#define GST_VIDEO_TEXTURE_CACHE_GL(o) (G_TYPE_CHECK_INSTANCE_CAST((o), GST_TYPE_VIDEO_TEXTURE_CACHE_GL, GstVideoTextureCacheGL))
|
||||||
|
#define GST_VIDEO_TEXTURE_CACHE_GL_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GST_TYPE_VIDEO_TEXTURE_CACHE_GL, GstVideoTextureCacheGLClass))
|
||||||
|
#define GST_IS_VIDEO_TEXTURE_CACHE_GL(o) (G_TYPE_CHECK_INSTANCE_TYPE((o), GST_TYPE_VIDEO_TEXTURE_CACHE_GL))
|
||||||
|
#define GST_IS_VIDEO_TEXTURE_CACHE_GL_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE((k), GST_TYPE_VIDEO_TEXTURE_CACHE_GL))
|
||||||
|
#define GST_VIDEO_TEXTURE_CACHE_GL_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS((o), GST_TYPE_VIDEO_TEXTURE_CACHE_GL, GstVideoTextureCacheGLClass))
|
||||||
|
GType gst_video_texture_cache_gl_get_type (void);
|
||||||
|
|
||||||
|
typedef struct _GstVideoTextureCacheGL
|
||||||
|
{
|
||||||
|
GstVideoTextureCache parent;
|
||||||
|
|
||||||
|
GstGLContext *ctx;
|
||||||
|
#if HAVE_IOS
|
||||||
|
CVOpenGLESTextureCacheRef cache;
|
||||||
|
#else
|
||||||
|
GstBufferPool *pool;
|
||||||
|
#endif
|
||||||
|
} GstVideoTextureCacheGL;
|
||||||
|
|
||||||
|
typedef struct _GstVideoTextureCacheGLClass
|
||||||
|
{
|
||||||
|
GstVideoTextureCacheClass parent_class;
|
||||||
|
} GstVideoTextureCacheGLClass;
|
||||||
|
|
||||||
|
GstVideoTextureCache * gst_video_texture_cache_gl_new (GstGLContext * ctx);
|
||||||
|
|
||||||
|
G_END_DECLS
|
||||||
|
|
||||||
|
#endif /* __GST_CORE_VIDEO_TEXTURE_CACHE_H__ */
|
262
sys/applemedia/videotexturecache-gl.m
Normal file
262
sys/applemedia/videotexturecache-gl.m
Normal file
|
@ -0,0 +1,262 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2010 Ole André Vadla Ravnås <oleavr@soundrop.com>
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Library General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* Library General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Library General Public
|
||||||
|
* License along with this library; if not, write to the
|
||||||
|
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||||
|
* Boston, MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifdef HAVE_CONFIG_H
|
||||||
|
# include "config.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if !HAVE_IOS
|
||||||
|
#import <AppKit/AppKit.h>
|
||||||
|
#include "iosurfacememory.h"
|
||||||
|
#endif
|
||||||
|
#include "iosglmemory.h"
|
||||||
|
#include "videotexturecache-gl.h"
|
||||||
|
#include "coremediabuffer.h"
|
||||||
|
#include "corevideobuffer.h"
|
||||||
|
#include "vtutil.h"
|
||||||
|
|
||||||
|
G_DEFINE_TYPE (GstVideoTextureCacheGL, gst_video_texture_cache_gl, GST_TYPE_VIDEO_TEXTURE_CACHE);
|
||||||
|
|
||||||
|
typedef struct _ContextThreadData
|
||||||
|
{
|
||||||
|
GstVideoTextureCacheGL *cache;
|
||||||
|
GstAppleCoreVideoPixelBuffer *gpixbuf;
|
||||||
|
guint plane;
|
||||||
|
gsize size;
|
||||||
|
GstMemory *memory;
|
||||||
|
} ContextThreadData;
|
||||||
|
|
||||||
|
typedef struct _TextureWrapper
|
||||||
|
{
|
||||||
|
#if HAVE_IOS
|
||||||
|
CVOpenGLESTextureCacheRef cache;
|
||||||
|
CVOpenGLESTextureRef texture;
|
||||||
|
#else
|
||||||
|
CVOpenGLTextureCacheRef cache;
|
||||||
|
CVOpenGLTextureRef texture;
|
||||||
|
#endif
|
||||||
|
} TextureWrapper;
|
||||||
|
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
PROP_0,
|
||||||
|
PROP_CONTEXT,
|
||||||
|
};
|
||||||
|
|
||||||
|
static GstMemory * gst_video_texture_cache_gl_create_memory (GstVideoTextureCache * cache,
|
||||||
|
GstAppleCoreVideoPixelBuffer *gpixbuf, guint plane, gsize size);
|
||||||
|
|
||||||
|
GstVideoTextureCache *
|
||||||
|
gst_video_texture_cache_gl_new (GstGLContext * ctx)
|
||||||
|
{
|
||||||
|
g_return_val_if_fail (GST_IS_GL_CONTEXT (ctx), NULL);
|
||||||
|
|
||||||
|
return g_object_new (GST_TYPE_VIDEO_TEXTURE_CACHE_GL,
|
||||||
|
"context", ctx, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
gst_video_texture_cache_gl_finalize (GObject * object)
|
||||||
|
{
|
||||||
|
GstVideoTextureCacheGL *cache_gl = GST_VIDEO_TEXTURE_CACHE_GL (object);
|
||||||
|
|
||||||
|
#if HAVE_IOS
|
||||||
|
CFRelease (cache_gl->cache); /* iOS has no "CVOpenGLESTextureCacheRelease" */
|
||||||
|
#else
|
||||||
|
#if 0
|
||||||
|
gst_buffer_pool_set_active (cache->pool, FALSE);
|
||||||
|
gst_object_unref (cache->pool);
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
gst_object_unref (cache_gl->ctx);
|
||||||
|
|
||||||
|
G_OBJECT_CLASS (gst_video_texture_cache_gl_parent_class)->finalize (object);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
gst_video_texture_cache_gl_set_property (GObject * object, guint prop_id,
|
||||||
|
const GValue * value, GParamSpec * pspec)
|
||||||
|
{
|
||||||
|
GstVideoTextureCacheGL *cache_gl = GST_VIDEO_TEXTURE_CACHE_GL (object);
|
||||||
|
|
||||||
|
switch (prop_id) {
|
||||||
|
case PROP_CONTEXT:
|
||||||
|
cache_gl->ctx = g_value_dup_object (value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
gst_video_texture_cache_gl_get_property (GObject * object, guint prop_id,
|
||||||
|
GValue * value, GParamSpec * pspec)
|
||||||
|
{
|
||||||
|
GstVideoTextureCacheGL *cache_gl = GST_VIDEO_TEXTURE_CACHE_GL (object);
|
||||||
|
|
||||||
|
switch (prop_id) {
|
||||||
|
case PROP_CONTEXT:
|
||||||
|
g_value_set_object (value, cache_gl->ctx);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
gst_video_texture_cache_gl_constructed (GObject * object)
|
||||||
|
{
|
||||||
|
GstVideoTextureCacheGL *cache_gl = GST_VIDEO_TEXTURE_CACHE_GL (object);
|
||||||
|
|
||||||
|
g_return_if_fail (GST_IS_GL_CONTEXT (cache_gl->ctx));
|
||||||
|
|
||||||
|
#if HAVE_IOS
|
||||||
|
CFMutableDictionaryRef cache_attrs =
|
||||||
|
CFDictionaryCreateMutable (NULL, 0, &kCFTypeDictionaryKeyCallBacks,
|
||||||
|
&kCFTypeDictionaryValueCallBacks);
|
||||||
|
CVOpenGLESTextureCacheCreate (kCFAllocatorDefault, (CFDictionaryRef) cache_attrs,
|
||||||
|
(__bridge CVEAGLContext) (gpointer) gst_gl_context_get_gl_context (cache_gl->ctx), NULL, &cache_gl->cache);
|
||||||
|
#else
|
||||||
|
gst_ios_surface_memory_init ();
|
||||||
|
#if 0
|
||||||
|
cache->pool = GST_BUFFER_POOL (gst_gl_buffer_pool_new (ctx));
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
gst_video_texture_cache_gl_init (GstVideoTextureCacheGL * cache_gl)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
gst_video_texture_cache_gl_class_init (GstVideoTextureCacheGLClass *klass)
|
||||||
|
{
|
||||||
|
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
|
||||||
|
GstVideoTextureCacheClass *cache_class = (GstVideoTextureCacheClass *) klass;
|
||||||
|
|
||||||
|
gobject_class->set_property = gst_video_texture_cache_gl_set_property;
|
||||||
|
gobject_class->get_property = gst_video_texture_cache_gl_get_property;
|
||||||
|
gobject_class->constructed = gst_video_texture_cache_gl_constructed;
|
||||||
|
gobject_class->finalize = gst_video_texture_cache_gl_finalize;
|
||||||
|
|
||||||
|
g_object_class_install_property (gobject_class, PROP_CONTEXT,
|
||||||
|
g_param_spec_object ("context", "Context",
|
||||||
|
"Associated OpenGL context", GST_TYPE_GL_CONTEXT,
|
||||||
|
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
|
||||||
|
|
||||||
|
cache_class->create_memory = gst_video_texture_cache_gl_create_memory;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if HAVE_IOS
|
||||||
|
static void
|
||||||
|
gst_video_texture_cache_gl_release_texture (TextureWrapper *data)
|
||||||
|
{
|
||||||
|
CFRelease(data->texture);
|
||||||
|
CFRelease(data->cache);
|
||||||
|
g_free(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
_do_create_memory (GstGLContext * context, ContextThreadData * data)
|
||||||
|
{
|
||||||
|
CVOpenGLESTextureRef texture = NULL;
|
||||||
|
GstVideoTextureCache *cache = GST_VIDEO_TEXTURE_CACHE (data->cache);
|
||||||
|
GstVideoTextureCacheGL *cache_gl = data->cache;
|
||||||
|
GstAppleCoreVideoPixelBuffer *gpixbuf = data->gpixbuf;
|
||||||
|
CVPixelBufferRef pixel_buf = gpixbuf->buf;
|
||||||
|
guint plane = data->plane;
|
||||||
|
gssize size = data->size;
|
||||||
|
GstGLTextureTarget gl_target;
|
||||||
|
GstAppleCoreVideoMemory *memory;
|
||||||
|
GstIOSGLMemory *gl_memory;
|
||||||
|
GstGLFormat texformat;
|
||||||
|
|
||||||
|
switch (GST_VIDEO_INFO_FORMAT (&cache->input_info)) {
|
||||||
|
case GST_VIDEO_FORMAT_BGRA:
|
||||||
|
if (CVOpenGLESTextureCacheCreateTextureFromImage (kCFAllocatorDefault,
|
||||||
|
cache_gl->cache, pixel_buf, NULL, GL_TEXTURE_2D, GL_RGBA,
|
||||||
|
GST_VIDEO_INFO_WIDTH (&cache->input_info),
|
||||||
|
GST_VIDEO_INFO_HEIGHT (&cache->input_info),
|
||||||
|
GL_RGBA, GL_UNSIGNED_BYTE, 0, &texture) != kCVReturnSuccess)
|
||||||
|
goto error;
|
||||||
|
|
||||||
|
texformat = GST_GL_RGBA;
|
||||||
|
plane = 0;
|
||||||
|
goto success;
|
||||||
|
case GST_VIDEO_FORMAT_NV12: {
|
||||||
|
GstGLFormat texifmt, texfmt;
|
||||||
|
|
||||||
|
if (plane == 0)
|
||||||
|
texformat = GST_GL_LUMINANCE;
|
||||||
|
else
|
||||||
|
texformat = GST_GL_LUMINANCE_ALPHA;
|
||||||
|
texfmt = gst_gl_sized_gl_format_from_gl_format_type (cache_gl->ctx, texformat, GL_UNSIGNED_BYTE);
|
||||||
|
|
||||||
|
if (CVOpenGLESTextureCacheCreateTextureFromImage (kCFAllocatorDefault,
|
||||||
|
cache_gl->cache, pixel_buf, NULL, GL_TEXTURE_2D, texformat,
|
||||||
|
GST_VIDEO_INFO_COMP_WIDTH (&cache->input_info, plane),
|
||||||
|
GST_VIDEO_INFO_COMP_HEIGHT (&cache->input_info, plane),
|
||||||
|
texfmt, GL_UNSIGNED_BYTE, plane, &texture) != kCVReturnSuccess)
|
||||||
|
goto error;
|
||||||
|
|
||||||
|
goto success;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
g_warn_if_reached ();
|
||||||
|
goto error;
|
||||||
|
}
|
||||||
|
|
||||||
|
success: {
|
||||||
|
TextureWrapper *texture_data = g_new0 (TextureWrapper, 1);
|
||||||
|
CFRetain(cache_gl->cache);
|
||||||
|
texture_data->cache = cache_gl->cache;
|
||||||
|
texture_data->texture = texture;
|
||||||
|
gl_target = gst_gl_texture_target_from_gl (CVOpenGLESTextureGetTarget (texture));
|
||||||
|
memory = gst_apple_core_video_memory_new_wrapped (gpixbuf, plane, size);
|
||||||
|
gl_memory = gst_ios_gl_memory_new_wrapped (context, memory,
|
||||||
|
gl_target, texformat, CVOpenGLESTextureGetName (texture), &cache->input_info,
|
||||||
|
plane, NULL, texture_data, (GDestroyNotify) gst_video_texture_cache_gl_release_texture);
|
||||||
|
|
||||||
|
data->memory = GST_MEMORY_CAST (gl_memory);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
error:
|
||||||
|
data->memory = NULL;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static GstMemory *
|
||||||
|
gst_video_texture_cache_gl_create_memory (GstVideoTextureCache * cache,
|
||||||
|
GstAppleCoreVideoPixelBuffer *gpixbuf, guint plane, gsize size)
|
||||||
|
{
|
||||||
|
GstVideoTextureCacheGL *cache_gl = GST_VIDEO_TEXTURE_CACHE_GL (cache);
|
||||||
|
ContextThreadData data = {cache_gl, gpixbuf, plane, size, NULL};
|
||||||
|
|
||||||
|
#if HAVE_IOS
|
||||||
|
gst_gl_context_thread_add (cache_gl->ctx,
|
||||||
|
(GstGLContextThreadFunc) _do_create_memory, &data);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return data.memory;
|
||||||
|
}
|
|
@ -21,7 +21,6 @@
|
||||||
#define __GST_CORE_VIDEO_TEXTURE_CACHE_H__
|
#define __GST_CORE_VIDEO_TEXTURE_CACHE_H__
|
||||||
|
|
||||||
#include <gst/video/gstvideometa.h>
|
#include <gst/video/gstvideometa.h>
|
||||||
#include <gst/gl/gl.h>
|
|
||||||
#include "corevideomemory.h"
|
#include "corevideomemory.h"
|
||||||
|
|
||||||
G_BEGIN_DECLS
|
G_BEGIN_DECLS
|
||||||
|
@ -38,12 +37,6 @@ typedef struct _GstVideoTextureCache
|
||||||
{
|
{
|
||||||
GObject parent;
|
GObject parent;
|
||||||
|
|
||||||
GstGLContext *ctx;
|
|
||||||
#if HAVE_IOS
|
|
||||||
CVOpenGLESTextureCacheRef cache;
|
|
||||||
#else
|
|
||||||
GstBufferPool *pool;
|
|
||||||
#endif
|
|
||||||
GstVideoInfo input_info;
|
GstVideoInfo input_info;
|
||||||
GstVideoInfo output_info;
|
GstVideoInfo output_info;
|
||||||
|
|
||||||
|
@ -55,9 +48,17 @@ typedef struct _GstVideoTextureCache
|
||||||
typedef struct _GstVideoTextureCacheClass
|
typedef struct _GstVideoTextureCacheClass
|
||||||
{
|
{
|
||||||
GObjectClass parent_class;
|
GObjectClass parent_class;
|
||||||
|
|
||||||
|
void (*set_format) (GstVideoTextureCache * cache,
|
||||||
|
GstVideoFormat in_format,
|
||||||
|
GstCaps * out_caps);
|
||||||
|
|
||||||
|
GstMemory * (*create_memory) (GstVideoTextureCache * cache,
|
||||||
|
GstAppleCoreVideoPixelBuffer *gpixbuf,
|
||||||
|
guint plane,
|
||||||
|
gsize size);
|
||||||
} GstVideoTextureCacheClass;
|
} GstVideoTextureCacheClass;
|
||||||
|
|
||||||
GstVideoTextureCache * gst_video_texture_cache_new (GstGLContext * ctx);
|
|
||||||
void gst_video_texture_cache_set_format (GstVideoTextureCache * cache,
|
void gst_video_texture_cache_set_format (GstVideoTextureCache * cache,
|
||||||
GstVideoFormat in_format,
|
GstVideoFormat in_format,
|
||||||
GstCaps * out_caps);
|
GstCaps * out_caps);
|
||||||
|
|
|
@ -23,127 +23,30 @@
|
||||||
|
|
||||||
#if !HAVE_IOS
|
#if !HAVE_IOS
|
||||||
#import <AppKit/AppKit.h>
|
#import <AppKit/AppKit.h>
|
||||||
#include "iosurfacememory.h"
|
|
||||||
#endif
|
#endif
|
||||||
#include "iosglmemory.h"
|
|
||||||
#include "videotexturecache.h"
|
#include "videotexturecache.h"
|
||||||
#include "coremediabuffer.h"
|
#include "coremediabuffer.h"
|
||||||
#include "corevideobuffer.h"
|
#include "corevideobuffer.h"
|
||||||
#include "vtutil.h"
|
#include "vtutil.h"
|
||||||
|
|
||||||
G_DEFINE_TYPE (GstVideoTextureCache, gst_video_texture_cache, G_TYPE_OBJECT);
|
G_DEFINE_ABSTRACT_TYPE (GstVideoTextureCache, gst_video_texture_cache, G_TYPE_OBJECT);
|
||||||
|
|
||||||
typedef struct _ContextThreadData
|
static void gst_video_texture_cache_default_set_format (GstVideoTextureCache * cache,
|
||||||
{
|
GstVideoFormat in_format, GstCaps * out_caps);
|
||||||
GstVideoTextureCache *cache;
|
|
||||||
GstAppleCoreVideoPixelBuffer *gpixbuf;
|
|
||||||
guint plane;
|
|
||||||
gsize size;
|
|
||||||
GstMemory *memory;
|
|
||||||
} ContextThreadData;
|
|
||||||
|
|
||||||
typedef struct _TextureWrapper
|
|
||||||
{
|
|
||||||
#if HAVE_IOS
|
|
||||||
CVOpenGLESTextureCacheRef cache;
|
|
||||||
CVOpenGLESTextureRef texture;
|
|
||||||
#else
|
|
||||||
CVOpenGLTextureCacheRef cache;
|
|
||||||
CVOpenGLTextureRef texture;
|
|
||||||
#endif
|
|
||||||
} TextureWrapper;
|
|
||||||
|
|
||||||
enum
|
|
||||||
{
|
|
||||||
PROP_0,
|
|
||||||
PROP_CONTEXT,
|
|
||||||
};
|
|
||||||
|
|
||||||
GstVideoTextureCache *
|
|
||||||
gst_video_texture_cache_new (GstGLContext * ctx)
|
|
||||||
{
|
|
||||||
g_return_val_if_fail (GST_IS_GL_CONTEXT (ctx), NULL);
|
|
||||||
|
|
||||||
return g_object_new (GST_TYPE_VIDEO_TEXTURE_CACHE,
|
|
||||||
"context", ctx, NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
static void
|
||||||
gst_video_texture_cache_finalize (GObject * object)
|
gst_video_texture_cache_finalize (GObject * object)
|
||||||
{
|
{
|
||||||
GstVideoTextureCache *cache = GST_VIDEO_TEXTURE_CACHE (object);
|
GstVideoTextureCache *cache = GST_VIDEO_TEXTURE_CACHE (object);
|
||||||
|
|
||||||
#if HAVE_IOS
|
|
||||||
CFRelease (cache->cache); /* iOS has no "CVOpenGLESTextureCacheRelease" */
|
|
||||||
#else
|
|
||||||
#if 0
|
|
||||||
gst_buffer_pool_set_active (cache->pool, FALSE);
|
|
||||||
gst_object_unref (cache->pool);
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
gst_object_unref (cache->ctx);
|
|
||||||
if (cache->in_caps)
|
if (cache->in_caps)
|
||||||
gst_caps_unref (cache->in_caps);
|
gst_caps_unref (cache->in_caps);
|
||||||
if (cache->out_caps)
|
if (cache->out_caps)
|
||||||
gst_caps_unref (cache->out_caps);
|
gst_caps_unref (cache->out_caps);
|
||||||
g_free (cache);
|
|
||||||
|
|
||||||
G_OBJECT_CLASS (gst_video_texture_cache_parent_class)->finalize (object);
|
G_OBJECT_CLASS (gst_video_texture_cache_parent_class)->finalize (object);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
|
||||||
gst_video_texture_cache_set_property (GObject * object, guint prop_id,
|
|
||||||
const GValue * value, GParamSpec * pspec)
|
|
||||||
{
|
|
||||||
GstVideoTextureCache *cache = GST_VIDEO_TEXTURE_CACHE (object);
|
|
||||||
|
|
||||||
switch (prop_id) {
|
|
||||||
case PROP_CONTEXT:
|
|
||||||
cache->ctx = g_value_dup_object (value);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
gst_video_texture_cache_get_property (GObject * object, guint prop_id,
|
|
||||||
GValue * value, GParamSpec * pspec)
|
|
||||||
{
|
|
||||||
GstVideoTextureCache *cache = GST_VIDEO_TEXTURE_CACHE (object);
|
|
||||||
|
|
||||||
switch (prop_id) {
|
|
||||||
case PROP_CONTEXT:
|
|
||||||
g_value_set_object (value, cache->ctx);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
gst_video_texture_cache_constructed (GObject * object)
|
|
||||||
{
|
|
||||||
GstVideoTextureCache * cache = GST_VIDEO_TEXTURE_CACHE (object);
|
|
||||||
|
|
||||||
g_return_if_fail (GST_IS_GL_CONTEXT (cache->ctx));
|
|
||||||
|
|
||||||
#if HAVE_IOS
|
|
||||||
CFMutableDictionaryRef cache_attrs =
|
|
||||||
CFDictionaryCreateMutable (NULL, 0, &kCFTypeDictionaryKeyCallBacks,
|
|
||||||
&kCFTypeDictionaryValueCallBacks);
|
|
||||||
CVOpenGLESTextureCacheCreate (kCFAllocatorDefault, (CFDictionaryRef) cache_attrs,
|
|
||||||
(__bridge CVEAGLContext) (gpointer)gst_gl_context_get_gl_context (cache->ctx), NULL, &cache->cache);
|
|
||||||
#else
|
|
||||||
gst_ios_surface_memory_init ();
|
|
||||||
#if 0
|
|
||||||
cache->pool = GST_BUFFER_POOL (gst_gl_buffer_pool_new (ctx));
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
static void
|
||||||
gst_video_texture_cache_init (GstVideoTextureCache * cache)
|
gst_video_texture_cache_init (GstVideoTextureCache * cache)
|
||||||
{
|
{
|
||||||
|
@ -155,19 +58,13 @@ gst_video_texture_cache_class_init (GstVideoTextureCacheClass *klass)
|
||||||
{
|
{
|
||||||
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
|
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
|
||||||
|
|
||||||
gobject_class->set_property = gst_video_texture_cache_set_property;
|
|
||||||
gobject_class->get_property = gst_video_texture_cache_get_property;
|
|
||||||
gobject_class->constructed = gst_video_texture_cache_constructed;
|
|
||||||
gobject_class->finalize = gst_video_texture_cache_finalize;
|
gobject_class->finalize = gst_video_texture_cache_finalize;
|
||||||
|
|
||||||
g_object_class_install_property (gobject_class, PROP_CONTEXT,
|
klass->set_format = gst_video_texture_cache_default_set_format;
|
||||||
g_param_spec_object ("context", "Context",
|
|
||||||
"Associated OpenGL context", GST_TYPE_GL_CONTEXT,
|
|
||||||
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
static void
|
||||||
gst_video_texture_cache_set_format (GstVideoTextureCache * cache,
|
gst_video_texture_cache_default_set_format (GstVideoTextureCache * cache,
|
||||||
GstVideoFormat in_format, GstCaps * out_caps)
|
GstVideoFormat in_format, GstCaps * out_caps)
|
||||||
{
|
{
|
||||||
GstCaps *in_caps;
|
GstCaps *in_caps;
|
||||||
|
@ -185,117 +82,26 @@ gst_video_texture_cache_set_format (GstVideoTextureCache * cache,
|
||||||
features = gst_caps_get_features (in_caps, 0);
|
features = gst_caps_get_features (in_caps, 0);
|
||||||
gst_video_info_from_caps (&cache->input_info, in_caps);
|
gst_video_info_from_caps (&cache->input_info, in_caps);
|
||||||
|
|
||||||
if (cache->in_caps)
|
gst_caps_take (&cache->in_caps, in_caps);
|
||||||
gst_caps_unref (cache->in_caps);
|
gst_caps_take (&cache->out_caps, out_caps);
|
||||||
if (cache->out_caps)
|
|
||||||
gst_caps_unref (cache->out_caps);
|
|
||||||
cache->in_caps = in_caps;
|
|
||||||
cache->out_caps = out_caps;
|
|
||||||
|
|
||||||
#if 0
|
|
||||||
GstStructure *config = gst_buffer_pool_get_config (cache->pool);
|
|
||||||
gst_buffer_pool_config_set_params (config, cache->in_caps,
|
|
||||||
GST_VIDEO_INFO_SIZE (&cache->input_info), 0, 0);
|
|
||||||
gst_buffer_pool_config_set_allocator (config,
|
|
||||||
gst_allocator_find (GST_IO_SURFACE_MEMORY_ALLOCATOR_NAME), NULL);
|
|
||||||
gst_buffer_pool_config_add_option (config,
|
|
||||||
GST_BUFFER_POOL_OPTION_GL_TEXTURE_TARGET_RECTANGLE);
|
|
||||||
gst_buffer_pool_set_config (cache->pool, config);
|
|
||||||
gst_buffer_pool_set_active (cache->pool, TRUE);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#if HAVE_IOS
|
void
|
||||||
static void
|
gst_video_texture_cache_set_format (GstVideoTextureCache * cache,
|
||||||
gst_video_texture_cache_release_texture (TextureWrapper *data)
|
GstVideoFormat in_format, GstCaps * out_caps)
|
||||||
{
|
{
|
||||||
CFRelease(data->texture);
|
GstVideoTextureCacheClass *cache_class = GST_VIDEO_TEXTURE_CACHE_GET_CLASS (cache);
|
||||||
CFRelease(data->cache);
|
|
||||||
g_free(data);
|
g_return_if_fail (cache_class->set_format);
|
||||||
|
cache_class->set_format (cache, in_format, out_caps);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
|
||||||
_do_create_memory (GstGLContext * context, ContextThreadData * data)
|
|
||||||
{
|
|
||||||
CVOpenGLESTextureRef texture = NULL;
|
|
||||||
GstVideoTextureCache *cache = data->cache;
|
|
||||||
GstAppleCoreVideoPixelBuffer *gpixbuf = data->gpixbuf;
|
|
||||||
CVPixelBufferRef pixel_buf = gpixbuf->buf;
|
|
||||||
guint plane = data->plane;
|
|
||||||
gssize size = data->size;
|
|
||||||
GstGLTextureTarget gl_target;
|
|
||||||
GstAppleCoreVideoMemory *memory;
|
|
||||||
GstIOSGLMemory *gl_memory;
|
|
||||||
GstGLFormat texformat;
|
|
||||||
|
|
||||||
switch (GST_VIDEO_INFO_FORMAT (&cache->input_info)) {
|
|
||||||
case GST_VIDEO_FORMAT_BGRA:
|
|
||||||
if (CVOpenGLESTextureCacheCreateTextureFromImage (kCFAllocatorDefault,
|
|
||||||
cache->cache, pixel_buf, NULL, GL_TEXTURE_2D, GL_RGBA,
|
|
||||||
GST_VIDEO_INFO_WIDTH (&cache->input_info),
|
|
||||||
GST_VIDEO_INFO_HEIGHT (&cache->input_info),
|
|
||||||
GL_RGBA, GL_UNSIGNED_BYTE, 0, &texture) != kCVReturnSuccess)
|
|
||||||
goto error;
|
|
||||||
|
|
||||||
texformat = GST_GL_RGBA;
|
|
||||||
plane = 0;
|
|
||||||
goto success;
|
|
||||||
case GST_VIDEO_FORMAT_NV12: {
|
|
||||||
GstGLFormat texifmt, texfmt;
|
|
||||||
|
|
||||||
if (plane == 0)
|
|
||||||
texformat = GST_GL_LUMINANCE;
|
|
||||||
else
|
|
||||||
texformat = GST_GL_LUMINANCE_ALPHA;
|
|
||||||
texfmt = gst_gl_sized_gl_format_from_gl_format_type (cache->ctx, texformat, GL_UNSIGNED_BYTE);
|
|
||||||
|
|
||||||
if (CVOpenGLESTextureCacheCreateTextureFromImage (kCFAllocatorDefault,
|
|
||||||
cache->cache, pixel_buf, NULL, GL_TEXTURE_2D, texformat,
|
|
||||||
GST_VIDEO_INFO_COMP_WIDTH (&cache->input_info, plane),
|
|
||||||
GST_VIDEO_INFO_COMP_HEIGHT (&cache->input_info, plane),
|
|
||||||
texfmt, GL_UNSIGNED_BYTE, plane, &texture) != kCVReturnSuccess)
|
|
||||||
goto error;
|
|
||||||
|
|
||||||
goto success;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
g_warn_if_reached ();
|
|
||||||
goto error;
|
|
||||||
}
|
|
||||||
|
|
||||||
success: {
|
|
||||||
TextureWrapper *texture_data = g_new(TextureWrapper, 1);
|
|
||||||
CFRetain(cache->cache);
|
|
||||||
texture_data->cache = cache->cache;
|
|
||||||
texture_data->texture = texture;
|
|
||||||
gl_target = gst_gl_texture_target_from_gl (CVOpenGLESTextureGetTarget (texture));
|
|
||||||
memory = gst_apple_core_video_memory_new_wrapped (gpixbuf, plane, size);
|
|
||||||
gl_memory = gst_ios_gl_memory_new_wrapped (context, memory,
|
|
||||||
gl_target, texformat, CVOpenGLESTextureGetName (texture), &cache->input_info,
|
|
||||||
plane, NULL, texture_data, (GDestroyNotify) gst_video_texture_cache_release_texture);
|
|
||||||
|
|
||||||
data->memory = GST_MEMORY_CAST (gl_memory);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
error:
|
|
||||||
data->memory = NULL;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
GstMemory *
|
GstMemory *
|
||||||
gst_video_texture_cache_create_memory (GstVideoTextureCache * cache,
|
gst_video_texture_cache_create_memory (GstVideoTextureCache * cache,
|
||||||
GstAppleCoreVideoPixelBuffer *gpixbuf,
|
GstAppleCoreVideoPixelBuffer *gpixbuf, guint plane, gsize size)
|
||||||
guint plane,
|
|
||||||
gsize size)
|
|
||||||
{
|
{
|
||||||
ContextThreadData data = {cache, gpixbuf, plane, size, NULL};
|
GstVideoTextureCacheClass *cache_class = GST_VIDEO_TEXTURE_CACHE_GET_CLASS (cache);
|
||||||
|
|
||||||
#if HAVE_IOS
|
g_return_val_if_fail (cache_class->create_memory, NULL);
|
||||||
gst_gl_context_thread_add (cache->ctx,
|
return cache_class->create_memory (cache, gpixbuf, plane, size);
|
||||||
(GstGLContextThreadFunc) _do_create_memory, &data);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return data.memory;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,7 @@
|
||||||
#include "vtutil.h"
|
#include "vtutil.h"
|
||||||
#include "corevideobuffer.h"
|
#include "corevideobuffer.h"
|
||||||
#include "coremediabuffer.h"
|
#include "coremediabuffer.h"
|
||||||
|
#include "videotexturecache-gl.h"
|
||||||
|
|
||||||
GST_DEBUG_CATEGORY_STATIC (gst_vtdec_debug_category);
|
GST_DEBUG_CATEGORY_STATIC (gst_vtdec_debug_category);
|
||||||
#define GST_CAT_DEFAULT gst_vtdec_debug_category
|
#define GST_CAT_DEFAULT gst_vtdec_debug_category
|
||||||
|
@ -222,7 +223,7 @@ setup_texture_cache (GstVtdec * vtdec, GstGLContext * context)
|
||||||
g_return_if_fail (vtdec->texture_cache == NULL);
|
g_return_if_fail (vtdec->texture_cache == NULL);
|
||||||
|
|
||||||
output_state = gst_video_decoder_get_output_state (GST_VIDEO_DECODER (vtdec));
|
output_state = gst_video_decoder_get_output_state (GST_VIDEO_DECODER (vtdec));
|
||||||
vtdec->texture_cache = gst_video_texture_cache_new (context);
|
vtdec->texture_cache = gst_video_texture_cache_gl_new (context);
|
||||||
gst_video_texture_cache_set_format (vtdec->texture_cache,
|
gst_video_texture_cache_set_format (vtdec->texture_cache,
|
||||||
GST_VIDEO_FORMAT_NV12, output_state->caps);
|
GST_VIDEO_FORMAT_NV12, output_state->caps);
|
||||||
gst_video_codec_state_unref (output_state);
|
gst_video_codec_state_unref (output_state);
|
||||||
|
@ -328,6 +329,11 @@ gst_vtdec_negotiate (GstVideoDecoder * decoder)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err == noErr && output_textures) {
|
if (err == noErr && output_textures) {
|
||||||
|
GstVideoTextureCacheGL *cache_gl = NULL;
|
||||||
|
|
||||||
|
if (vtdec->texture_cache)
|
||||||
|
cache_gl = GST_VIDEO_TEXTURE_CACHE_GL (vtdec->texture_cache);
|
||||||
|
|
||||||
/* call this regardless of whether caps have changed or not since a new
|
/* call this regardless of whether caps have changed or not since a new
|
||||||
* local context could have become available
|
* local context could have become available
|
||||||
*/
|
*/
|
||||||
|
@ -336,11 +342,9 @@ gst_vtdec_negotiate (GstVideoDecoder * decoder)
|
||||||
gst_gl_context_helper_ensure_context (vtdec->ctxh);
|
gst_gl_context_helper_ensure_context (vtdec->ctxh);
|
||||||
|
|
||||||
GST_INFO_OBJECT (vtdec, "pushing textures, context %p old context %p",
|
GST_INFO_OBJECT (vtdec, "pushing textures, context %p old context %p",
|
||||||
vtdec->ctxh->context,
|
vtdec->ctxh->context, cache_gl ? cache_gl->ctx : NULL);
|
||||||
vtdec->texture_cache ? vtdec->texture_cache->ctx : NULL);
|
|
||||||
|
|
||||||
if (vtdec->texture_cache
|
if (cache_gl && cache_gl->ctx != vtdec->ctxh->context) {
|
||||||
&& vtdec->texture_cache->ctx != vtdec->ctxh->context) {
|
|
||||||
g_object_unref (vtdec->texture_cache);
|
g_object_unref (vtdec->texture_cache);
|
||||||
vtdec->texture_cache = NULL;
|
vtdec->texture_cache = NULL;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue