2010-04-23 15:59:31 +00:00
|
|
|
/*
|
|
|
|
* gstvaapicontext.c - VA context abstraction
|
|
|
|
*
|
2012-01-16 09:41:10 +00:00
|
|
|
* Copyright (C) 2010-2011 Splitted-Desktop Systems
|
2013-01-29 13:14:45 +00:00
|
|
|
* Copyright (C) 2011-2013 Intel Corporation
|
2010-04-23 15:59:31 +00:00
|
|
|
*
|
2010-05-03 07:07:27 +00:00
|
|
|
* This library is free software; you can redistribute it and/or
|
|
|
|
* modify it under the terms of the GNU Lesser General Public License
|
|
|
|
* as published by the Free Software Foundation; either version 2.1
|
|
|
|
* of the License, or (at your option) any later version.
|
2010-04-23 15:59:31 +00:00
|
|
|
*
|
2010-05-03 07:07:27 +00:00
|
|
|
* This library is distributed in the hope that it will be useful,
|
2010-04-23 15:59:31 +00:00
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
2010-05-03 07:07:27 +00:00
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
|
|
* Lesser General Public License for more details.
|
2010-04-23 15:59:31 +00:00
|
|
|
*
|
2010-05-03 07:07:27 +00:00
|
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
|
|
* License along with this library; if not, write to the Free
|
|
|
|
* Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
|
|
* Boston, MA 02110-1301 USA
|
2010-04-23 15:59:31 +00:00
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* SECTION:gstvaapicontext
|
|
|
|
* @short_description: VA context abstraction
|
|
|
|
*/
|
|
|
|
|
2012-01-30 17:12:59 +00:00
|
|
|
#include "sysdeps.h"
|
2010-04-23 15:59:31 +00:00
|
|
|
#include <assert.h>
|
|
|
|
#include "gstvaapicompat.h"
|
|
|
|
#include "gstvaapicontext.h"
|
2011-12-13 15:53:15 +00:00
|
|
|
#include "gstvaapisurface.h"
|
|
|
|
#include "gstvaapisurface_priv.h"
|
2010-04-23 15:59:31 +00:00
|
|
|
#include "gstvaapisurfacepool.h"
|
2011-12-14 13:35:13 +00:00
|
|
|
#include "gstvaapiimage.h"
|
|
|
|
#include "gstvaapisubpicture.h"
|
2013-01-10 12:41:39 +00:00
|
|
|
#include "gstvaapiminiobject.h"
|
2010-04-23 15:59:31 +00:00
|
|
|
#include "gstvaapiutils.h"
|
|
|
|
#include "gstvaapi_priv.h"
|
|
|
|
|
|
|
|
#define DEBUG 1
|
|
|
|
#include "gstvaapidebug.h"
|
|
|
|
|
2012-09-07 14:14:11 +00:00
|
|
|
G_DEFINE_TYPE(GstVaapiContext, gst_vaapi_context, GST_VAAPI_TYPE_OBJECT)
|
2010-04-23 15:59:31 +00:00
|
|
|
|
|
|
|
#define GST_VAAPI_CONTEXT_GET_PRIVATE(obj) \
|
|
|
|
(G_TYPE_INSTANCE_GET_PRIVATE((obj), \
|
|
|
|
GST_VAAPI_TYPE_CONTEXT, \
|
|
|
|
GstVaapiContextPrivate))
|
|
|
|
|
2011-12-14 13:35:13 +00:00
|
|
|
typedef struct _GstVaapiOverlayRectangle GstVaapiOverlayRectangle;
|
|
|
|
struct _GstVaapiOverlayRectangle {
|
|
|
|
GstVaapiContext *context;
|
|
|
|
GstVaapiSubpicture *subpicture;
|
2013-01-10 12:41:39 +00:00
|
|
|
GstVaapiRectangle render_rect;
|
2011-12-14 13:35:13 +00:00
|
|
|
guint seq_num;
|
2013-01-11 14:19:45 +00:00
|
|
|
guint layer_id;
|
overlay: fix check for pixels buffer change.
A GstVideoOverlayRectangle is created whenever the underlying pixels data
change. However, when global-alpha is supported, it is possible to re-use
the same GstVideoOverlayRectangle but with a change to the global-alpha
value. This process causes a change of sequence number, so we can no longer
check for that.
Still, if sequence numbers did not change, then there was no change in
global-alpha either. So, we need a way to compare the underlying GstBuffer
pointers. There is no API to retrieve the original pixels buffer from
a GstVideoOverlayRectangle. So, we use the following heuristics:
1. Use gst_video_overlay_rectangle_get_pixels_unscaled_argb() with the same
format flags from which the GstVideoOverlayRectangle was created. This
will work if there was no prior consumer of the GstVideoOverlayRectangle
with alternate (non-"native") format flags.
2. In overlay_rectangle_has_changed_pixels(), we have to use the same
gst_video_overlay_rectangle_get_pixels_unscaled_argb() function but
with flags that match the subpicture. This is needed to cope with
platforms that don't support global-alpha in HW, so the gst-video
layer takes care of that and fixes this up with a possibly new
GstBuffer, and hence pixels data (or) in-place by caching the current
global-alpha value applied. So we have to determine the rectangle
was previously used, based on what previous flags were used to
retrieve the ARGB pixels buffer.
2013-01-11 10:12:26 +00:00
|
|
|
GstBuffer *rect_buffer;
|
2013-01-10 17:42:37 +00:00
|
|
|
GstVideoOverlayRectangle *rect;
|
2013-01-10 12:41:39 +00:00
|
|
|
guint is_associated : 1;
|
2011-12-14 13:35:13 +00:00
|
|
|
};
|
|
|
|
|
2010-04-23 15:59:31 +00:00
|
|
|
/* XXX: optimize for the effective number of reference frames */
|
|
|
|
struct _GstVaapiContextPrivate {
|
|
|
|
VAConfigID config_id;
|
|
|
|
GPtrArray *surfaces;
|
|
|
|
GstVaapiVideoPool *surfaces_pool;
|
2013-01-10 17:42:37 +00:00
|
|
|
GPtrArray *overlays[2];
|
|
|
|
guint overlay_id;
|
2010-04-23 15:59:31 +00:00
|
|
|
GstVaapiProfile profile;
|
|
|
|
GstVaapiEntrypoint entrypoint;
|
|
|
|
guint width;
|
|
|
|
guint height;
|
2012-09-10 16:15:02 +00:00
|
|
|
guint ref_frames;
|
2010-04-23 15:59:31 +00:00
|
|
|
guint is_constructed : 1;
|
|
|
|
};
|
|
|
|
|
|
|
|
enum {
|
|
|
|
PROP_0,
|
|
|
|
|
|
|
|
PROP_PROFILE,
|
|
|
|
PROP_ENTRYPOINT,
|
|
|
|
PROP_WIDTH,
|
2012-09-10 16:15:02 +00:00
|
|
|
PROP_HEIGHT,
|
|
|
|
PROP_REF_FRAMES
|
2010-04-23 15:59:31 +00:00
|
|
|
};
|
|
|
|
|
2012-09-10 16:15:02 +00:00
|
|
|
static guint
|
|
|
|
get_max_ref_frames(GstVaapiProfile profile)
|
|
|
|
{
|
|
|
|
guint ref_frames;
|
|
|
|
|
|
|
|
switch (gst_vaapi_profile_get_codec(profile)) {
|
|
|
|
case GST_VAAPI_CODEC_H264: ref_frames = 16; break;
|
2012-09-10 16:17:10 +00:00
|
|
|
case GST_VAAPI_CODEC_JPEG: ref_frames = 0; break;
|
2012-09-10 16:15:02 +00:00
|
|
|
default: ref_frames = 2; break;
|
|
|
|
}
|
|
|
|
return ref_frames;
|
|
|
|
}
|
|
|
|
|
2013-01-10 17:42:37 +00:00
|
|
|
static inline void
|
|
|
|
gst_video_overlay_rectangle_replace(GstVideoOverlayRectangle **old_rect_ptr,
|
|
|
|
GstVideoOverlayRectangle *new_rect)
|
|
|
|
{
|
|
|
|
gst_mini_object_replace((GstMiniObject **)old_rect_ptr,
|
|
|
|
GST_MINI_OBJECT_CAST(new_rect));
|
|
|
|
}
|
|
|
|
|
overlay: fix check for pixels buffer change.
A GstVideoOverlayRectangle is created whenever the underlying pixels data
change. However, when global-alpha is supported, it is possible to re-use
the same GstVideoOverlayRectangle but with a change to the global-alpha
value. This process causes a change of sequence number, so we can no longer
check for that.
Still, if sequence numbers did not change, then there was no change in
global-alpha either. So, we need a way to compare the underlying GstBuffer
pointers. There is no API to retrieve the original pixels buffer from
a GstVideoOverlayRectangle. So, we use the following heuristics:
1. Use gst_video_overlay_rectangle_get_pixels_unscaled_argb() with the same
format flags from which the GstVideoOverlayRectangle was created. This
will work if there was no prior consumer of the GstVideoOverlayRectangle
with alternate (non-"native") format flags.
2. In overlay_rectangle_has_changed_pixels(), we have to use the same
gst_video_overlay_rectangle_get_pixels_unscaled_argb() function but
with flags that match the subpicture. This is needed to cope with
platforms that don't support global-alpha in HW, so the gst-video
layer takes care of that and fixes this up with a possibly new
GstBuffer, and hence pixels data (or) in-place by caching the current
global-alpha value applied. So we have to determine the rectangle
was previously used, based on what previous flags were used to
retrieve the ARGB pixels buffer.
2013-01-11 10:12:26 +00:00
|
|
|
static inline GstBuffer *
|
|
|
|
gst_video_overlay_rectangle_get_pixels_raw(GstVideoOverlayRectangle *rect)
|
|
|
|
{
|
|
|
|
guint width, height, stride, flags;
|
|
|
|
|
|
|
|
flags = gst_video_overlay_rectangle_get_flags(rect);
|
|
|
|
|
|
|
|
/* Try to retrieve the original buffer that was passed to
|
|
|
|
gst_video_overlay_rectangle_new_argb(). This will only work if
|
|
|
|
there was no previous user that required pixels with non native
|
|
|
|
alpha type */
|
|
|
|
return gst_video_overlay_rectangle_get_pixels_unscaled_argb(rect,
|
|
|
|
&width, &height, &stride, flags);
|
|
|
|
}
|
|
|
|
|
2013-01-10 12:41:39 +00:00
|
|
|
#define overlay_rectangle_ref(overlay) \
|
|
|
|
gst_vaapi_mini_object_ref(GST_VAAPI_MINI_OBJECT(overlay))
|
|
|
|
|
|
|
|
#define overlay_rectangle_unref(overlay) \
|
|
|
|
gst_vaapi_mini_object_unref(GST_VAAPI_MINI_OBJECT(overlay))
|
|
|
|
|
|
|
|
#define overlay_rectangle_replace(old_overlay_ptr, new_overlay) \
|
|
|
|
gst_vaapi_mini_object_replace((GstVaapiMiniObject **)(old_overlay_ptr), \
|
|
|
|
(GstVaapiMiniObject *)(new_overlay))
|
|
|
|
|
|
|
|
static void
|
|
|
|
overlay_rectangle_finalize(GstVaapiOverlayRectangle *overlay);
|
|
|
|
|
|
|
|
static gboolean
|
|
|
|
overlay_rectangle_associate(GstVaapiOverlayRectangle *overlay);
|
|
|
|
|
|
|
|
static gboolean
|
|
|
|
overlay_rectangle_deassociate(GstVaapiOverlayRectangle *overlay);
|
|
|
|
|
|
|
|
static inline const GstVaapiMiniObjectClass *
|
|
|
|
overlay_rectangle_class(void)
|
|
|
|
{
|
|
|
|
static const GstVaapiMiniObjectClass GstVaapiOverlayRectangleClass = {
|
|
|
|
sizeof(GstVaapiOverlayRectangle),
|
|
|
|
(GDestroyNotify)overlay_rectangle_finalize
|
|
|
|
};
|
|
|
|
return &GstVaapiOverlayRectangleClass;
|
|
|
|
}
|
|
|
|
|
2011-12-14 13:35:13 +00:00
|
|
|
static GstVaapiOverlayRectangle *
|
2013-01-11 14:19:45 +00:00
|
|
|
overlay_rectangle_new(GstVideoOverlayRectangle *rect, GstVaapiContext *context,
|
|
|
|
guint layer_id)
|
2011-12-14 13:35:13 +00:00
|
|
|
{
|
|
|
|
GstVaapiOverlayRectangle *overlay;
|
2013-01-10 12:41:39 +00:00
|
|
|
GstVaapiRectangle *render_rect;
|
|
|
|
guint width, height;
|
|
|
|
gint x, y;
|
2011-12-14 13:35:13 +00:00
|
|
|
|
2013-01-10 12:41:39 +00:00
|
|
|
overlay = (GstVaapiOverlayRectangle *)
|
|
|
|
gst_vaapi_mini_object_new0(overlay_rectangle_class());
|
2011-12-14 13:35:13 +00:00
|
|
|
if (!overlay)
|
|
|
|
return NULL;
|
|
|
|
|
2013-01-10 12:41:39 +00:00
|
|
|
overlay->context = context;
|
|
|
|
overlay->seq_num = gst_video_overlay_rectangle_get_seqnum(rect);
|
2013-01-11 14:19:45 +00:00
|
|
|
overlay->layer_id = layer_id;
|
2013-01-10 17:42:37 +00:00
|
|
|
overlay->rect = gst_video_overlay_rectangle_ref(rect);
|
2013-01-10 12:41:39 +00:00
|
|
|
|
overlay: fix check for pixels buffer change.
A GstVideoOverlayRectangle is created whenever the underlying pixels data
change. However, when global-alpha is supported, it is possible to re-use
the same GstVideoOverlayRectangle but with a change to the global-alpha
value. This process causes a change of sequence number, so we can no longer
check for that.
Still, if sequence numbers did not change, then there was no change in
global-alpha either. So, we need a way to compare the underlying GstBuffer
pointers. There is no API to retrieve the original pixels buffer from
a GstVideoOverlayRectangle. So, we use the following heuristics:
1. Use gst_video_overlay_rectangle_get_pixels_unscaled_argb() with the same
format flags from which the GstVideoOverlayRectangle was created. This
will work if there was no prior consumer of the GstVideoOverlayRectangle
with alternate (non-"native") format flags.
2. In overlay_rectangle_has_changed_pixels(), we have to use the same
gst_video_overlay_rectangle_get_pixels_unscaled_argb() function but
with flags that match the subpicture. This is needed to cope with
platforms that don't support global-alpha in HW, so the gst-video
layer takes care of that and fixes this up with a possibly new
GstBuffer, and hence pixels data (or) in-place by caching the current
global-alpha value applied. So we have to determine the rectangle
was previously used, based on what previous flags were used to
retrieve the ARGB pixels buffer.
2013-01-11 10:12:26 +00:00
|
|
|
gst_buffer_replace(&overlay->rect_buffer,
|
|
|
|
gst_video_overlay_rectangle_get_pixels_raw(rect));
|
|
|
|
if (!overlay->rect_buffer)
|
|
|
|
goto error;
|
|
|
|
|
2013-01-10 12:41:39 +00:00
|
|
|
overlay->subpicture = gst_vaapi_subpicture_new_from_overlay_rectangle(
|
|
|
|
GST_VAAPI_OBJECT_DISPLAY(context), rect);
|
|
|
|
if (!overlay->subpicture)
|
|
|
|
goto error;
|
|
|
|
|
|
|
|
gst_video_overlay_rectangle_get_render_rectangle(rect,
|
|
|
|
&x, &y, &width, &height);
|
|
|
|
render_rect = &overlay->render_rect;
|
|
|
|
render_rect->x = x;
|
|
|
|
render_rect->y = y;
|
|
|
|
render_rect->width = width;
|
|
|
|
render_rect->height = height;
|
2011-12-14 13:35:13 +00:00
|
|
|
return overlay;
|
2013-01-10 12:41:39 +00:00
|
|
|
|
|
|
|
error:
|
|
|
|
overlay_rectangle_unref(overlay);
|
|
|
|
return NULL;
|
2011-12-14 13:35:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2013-01-10 12:41:39 +00:00
|
|
|
overlay_rectangle_finalize(GstVaapiOverlayRectangle *overlay)
|
2011-12-14 13:35:13 +00:00
|
|
|
{
|
overlay: fix check for pixels buffer change.
A GstVideoOverlayRectangle is created whenever the underlying pixels data
change. However, when global-alpha is supported, it is possible to re-use
the same GstVideoOverlayRectangle but with a change to the global-alpha
value. This process causes a change of sequence number, so we can no longer
check for that.
Still, if sequence numbers did not change, then there was no change in
global-alpha either. So, we need a way to compare the underlying GstBuffer
pointers. There is no API to retrieve the original pixels buffer from
a GstVideoOverlayRectangle. So, we use the following heuristics:
1. Use gst_video_overlay_rectangle_get_pixels_unscaled_argb() with the same
format flags from which the GstVideoOverlayRectangle was created. This
will work if there was no prior consumer of the GstVideoOverlayRectangle
with alternate (non-"native") format flags.
2. In overlay_rectangle_has_changed_pixels(), we have to use the same
gst_video_overlay_rectangle_get_pixels_unscaled_argb() function but
with flags that match the subpicture. This is needed to cope with
platforms that don't support global-alpha in HW, so the gst-video
layer takes care of that and fixes this up with a possibly new
GstBuffer, and hence pixels data (or) in-place by caching the current
global-alpha value applied. So we have to determine the rectangle
was previously used, based on what previous flags were used to
retrieve the ARGB pixels buffer.
2013-01-11 10:12:26 +00:00
|
|
|
gst_buffer_replace(&overlay->rect_buffer, NULL);
|
2013-01-10 17:42:37 +00:00
|
|
|
gst_video_overlay_rectangle_unref(overlay->rect);
|
|
|
|
|
2011-12-14 13:35:13 +00:00
|
|
|
if (overlay->subpicture) {
|
2013-01-10 12:41:39 +00:00
|
|
|
overlay_rectangle_deassociate(overlay);
|
2011-12-14 13:35:13 +00:00
|
|
|
g_object_unref(overlay->subpicture);
|
|
|
|
overlay->subpicture = NULL;
|
|
|
|
}
|
2013-01-10 12:41:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static gboolean
|
|
|
|
overlay_rectangle_associate(GstVaapiOverlayRectangle *overlay)
|
|
|
|
{
|
|
|
|
GstVaapiSubpicture * const subpicture = overlay->subpicture;
|
|
|
|
GPtrArray * const surfaces = overlay->context->priv->surfaces;
|
|
|
|
guint i, n_associated;
|
|
|
|
|
|
|
|
if (overlay->is_associated)
|
|
|
|
return TRUE;
|
|
|
|
|
|
|
|
n_associated = 0;
|
|
|
|
for (i = 0; i < surfaces->len; i++) {
|
|
|
|
GstVaapiSurface * const surface = g_ptr_array_index(surfaces, i);
|
|
|
|
if (gst_vaapi_surface_associate_subpicture(surface, subpicture,
|
|
|
|
NULL, &overlay->render_rect))
|
|
|
|
n_associated++;
|
|
|
|
}
|
|
|
|
|
|
|
|
overlay->is_associated = TRUE;
|
|
|
|
return n_associated == surfaces->len;
|
|
|
|
}
|
|
|
|
|
|
|
|
static gboolean
|
|
|
|
overlay_rectangle_deassociate(GstVaapiOverlayRectangle *overlay)
|
|
|
|
{
|
|
|
|
GstVaapiSubpicture * const subpicture = overlay->subpicture;
|
|
|
|
GPtrArray * const surfaces = overlay->context->priv->surfaces;
|
|
|
|
guint i, n_associated;
|
|
|
|
|
|
|
|
if (!overlay->is_associated)
|
|
|
|
return TRUE;
|
|
|
|
|
|
|
|
n_associated = surfaces->len;
|
|
|
|
for (i = 0; i < surfaces->len; i++) {
|
|
|
|
GstVaapiSurface * const surface = g_ptr_array_index(surfaces, i);
|
|
|
|
if (gst_vaapi_surface_deassociate_subpicture(surface, subpicture))
|
|
|
|
n_associated--;
|
|
|
|
}
|
|
|
|
|
|
|
|
overlay->is_associated = FALSE;
|
|
|
|
return n_associated == 0;
|
|
|
|
}
|
|
|
|
|
2013-01-10 17:42:37 +00:00
|
|
|
static gboolean
|
|
|
|
overlay_rectangle_changed_pixels(GstVaapiOverlayRectangle *overlay,
|
|
|
|
GstVideoOverlayRectangle *rect)
|
|
|
|
{
|
overlay: fix check for pixels buffer change.
A GstVideoOverlayRectangle is created whenever the underlying pixels data
change. However, when global-alpha is supported, it is possible to re-use
the same GstVideoOverlayRectangle but with a change to the global-alpha
value. This process causes a change of sequence number, so we can no longer
check for that.
Still, if sequence numbers did not change, then there was no change in
global-alpha either. So, we need a way to compare the underlying GstBuffer
pointers. There is no API to retrieve the original pixels buffer from
a GstVideoOverlayRectangle. So, we use the following heuristics:
1. Use gst_video_overlay_rectangle_get_pixels_unscaled_argb() with the same
format flags from which the GstVideoOverlayRectangle was created. This
will work if there was no prior consumer of the GstVideoOverlayRectangle
with alternate (non-"native") format flags.
2. In overlay_rectangle_has_changed_pixels(), we have to use the same
gst_video_overlay_rectangle_get_pixels_unscaled_argb() function but
with flags that match the subpicture. This is needed to cope with
platforms that don't support global-alpha in HW, so the gst-video
layer takes care of that and fixes this up with a possibly new
GstBuffer, and hence pixels data (or) in-place by caching the current
global-alpha value applied. So we have to determine the rectangle
was previously used, based on what previous flags were used to
retrieve the ARGB pixels buffer.
2013-01-11 10:12:26 +00:00
|
|
|
guint width, height, stride, flags;
|
|
|
|
GstBuffer *buffer;
|
|
|
|
|
2013-01-10 17:42:37 +00:00
|
|
|
if (overlay->seq_num == gst_video_overlay_rectangle_get_seqnum(rect))
|
|
|
|
return FALSE;
|
overlay: fix check for pixels buffer change.
A GstVideoOverlayRectangle is created whenever the underlying pixels data
change. However, when global-alpha is supported, it is possible to re-use
the same GstVideoOverlayRectangle but with a change to the global-alpha
value. This process causes a change of sequence number, so we can no longer
check for that.
Still, if sequence numbers did not change, then there was no change in
global-alpha either. So, we need a way to compare the underlying GstBuffer
pointers. There is no API to retrieve the original pixels buffer from
a GstVideoOverlayRectangle. So, we use the following heuristics:
1. Use gst_video_overlay_rectangle_get_pixels_unscaled_argb() with the same
format flags from which the GstVideoOverlayRectangle was created. This
will work if there was no prior consumer of the GstVideoOverlayRectangle
with alternate (non-"native") format flags.
2. In overlay_rectangle_has_changed_pixels(), we have to use the same
gst_video_overlay_rectangle_get_pixels_unscaled_argb() function but
with flags that match the subpicture. This is needed to cope with
platforms that don't support global-alpha in HW, so the gst-video
layer takes care of that and fixes this up with a possibly new
GstBuffer, and hence pixels data (or) in-place by caching the current
global-alpha value applied. So we have to determine the rectangle
was previously used, based on what previous flags were used to
retrieve the ARGB pixels buffer.
2013-01-11 10:12:26 +00:00
|
|
|
|
|
|
|
flags = to_GstVideoOverlayFormatFlags(
|
|
|
|
gst_vaapi_subpicture_get_flags(overlay->subpicture));
|
|
|
|
|
|
|
|
buffer = gst_video_overlay_rectangle_get_pixels_unscaled_argb(rect,
|
|
|
|
&width, &height, &stride, flags);
|
|
|
|
return GST_BUFFER_DATA(overlay->rect_buffer) != GST_BUFFER_DATA(buffer);
|
2013-01-10 17:42:37 +00:00
|
|
|
}
|
|
|
|
|
2013-01-11 10:53:05 +00:00
|
|
|
static gboolean
|
2013-01-11 14:19:45 +00:00
|
|
|
overlay_rectangle_changed_render_rect(GstVaapiOverlayRectangle *overlay,
|
2013-01-11 10:53:05 +00:00
|
|
|
GstVideoOverlayRectangle *rect)
|
|
|
|
{
|
|
|
|
GstVaapiRectangle * const render_rect = &overlay->render_rect;
|
|
|
|
guint width, height;
|
|
|
|
gint x, y;
|
|
|
|
|
|
|
|
gst_video_overlay_rectangle_get_render_rectangle(rect,
|
|
|
|
&x, &y, &width, &height);
|
|
|
|
|
|
|
|
if (x == render_rect->x &&
|
|
|
|
y == render_rect->y &&
|
|
|
|
width == render_rect->width &&
|
|
|
|
height == render_rect->height)
|
2013-01-11 14:19:45 +00:00
|
|
|
return FALSE;
|
2013-01-11 10:53:05 +00:00
|
|
|
|
|
|
|
render_rect->x = x;
|
|
|
|
render_rect->y = y;
|
|
|
|
render_rect->width = width;
|
|
|
|
render_rect->height = height;
|
2013-01-11 14:19:45 +00:00
|
|
|
return TRUE;
|
2013-01-11 10:53:05 +00:00
|
|
|
}
|
|
|
|
|
2012-05-15 08:24:08 +00:00
|
|
|
static inline gboolean
|
|
|
|
overlay_rectangle_update_global_alpha(GstVaapiOverlayRectangle *overlay,
|
|
|
|
GstVideoOverlayRectangle *rect)
|
|
|
|
{
|
2012-07-20 10:36:33 +00:00
|
|
|
#ifdef HAVE_GST_VIDEO_OVERLAY_HWCAPS
|
2012-05-15 08:24:08 +00:00
|
|
|
const guint flags = gst_video_overlay_rectangle_get_flags(rect);
|
|
|
|
if (!(flags & GST_VIDEO_OVERLAY_FORMAT_FLAG_GLOBAL_ALPHA))
|
|
|
|
return TRUE;
|
2012-07-20 10:36:33 +00:00
|
|
|
#endif
|
2012-05-15 08:24:08 +00:00
|
|
|
return gst_vaapi_subpicture_set_global_alpha(overlay->subpicture,
|
|
|
|
gst_video_overlay_rectangle_get_global_alpha(rect));
|
|
|
|
}
|
|
|
|
|
2013-01-10 17:42:37 +00:00
|
|
|
static gboolean
|
|
|
|
overlay_rectangle_update(GstVaapiOverlayRectangle *overlay,
|
2013-01-11 14:19:45 +00:00
|
|
|
GstVideoOverlayRectangle *rect, gboolean *reassociate_ptr)
|
2013-01-10 17:42:37 +00:00
|
|
|
{
|
|
|
|
if (overlay_rectangle_changed_pixels(overlay, rect))
|
|
|
|
return FALSE;
|
2013-01-11 14:19:45 +00:00
|
|
|
if (overlay_rectangle_changed_render_rect(overlay, rect))
|
|
|
|
*reassociate_ptr = TRUE;
|
2012-05-15 08:24:08 +00:00
|
|
|
if (!overlay_rectangle_update_global_alpha(overlay, rect))
|
|
|
|
return FALSE;
|
2013-01-10 17:42:37 +00:00
|
|
|
gst_video_overlay_rectangle_replace(&overlay->rect, rect);
|
|
|
|
return TRUE;
|
|
|
|
}
|
|
|
|
|
2013-01-10 12:41:39 +00:00
|
|
|
static inline GPtrArray *
|
|
|
|
overlay_new(void)
|
|
|
|
{
|
|
|
|
return g_ptr_array_new_with_free_func(
|
|
|
|
(GDestroyNotify)gst_vaapi_mini_object_unref);
|
2011-12-14 13:35:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2013-01-10 12:41:39 +00:00
|
|
|
overlay_destroy(GPtrArray **overlay_ptr)
|
2011-12-14 13:35:13 +00:00
|
|
|
{
|
2013-01-10 12:41:39 +00:00
|
|
|
GPtrArray * const overlay = *overlay_ptr;
|
2011-12-14 13:35:13 +00:00
|
|
|
|
2013-01-10 12:41:39 +00:00
|
|
|
if (!overlay)
|
|
|
|
return;
|
|
|
|
g_ptr_array_unref(overlay);
|
|
|
|
*overlay_ptr = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
overlay_clear(GPtrArray *overlay)
|
|
|
|
{
|
|
|
|
if (overlay && overlay->len > 0)
|
|
|
|
g_ptr_array_remove_range(overlay, 0, overlay->len);
|
2011-12-14 13:35:13 +00:00
|
|
|
}
|
|
|
|
|
2013-01-10 17:42:37 +00:00
|
|
|
static GstVaapiOverlayRectangle *
|
|
|
|
overlay_lookup(GPtrArray *overlays, GstVideoOverlayRectangle *rect)
|
|
|
|
{
|
|
|
|
guint i;
|
|
|
|
|
|
|
|
for (i = 0; i < overlays->len; i++) {
|
|
|
|
GstVaapiOverlayRectangle * const overlay =
|
|
|
|
g_ptr_array_index(overlays, i);
|
|
|
|
|
|
|
|
if (overlay->rect == rect)
|
|
|
|
return overlay;
|
|
|
|
}
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
2013-01-11 14:19:45 +00:00
|
|
|
static gboolean
|
|
|
|
overlay_reassociate(GPtrArray *overlays)
|
|
|
|
{
|
|
|
|
guint i;
|
|
|
|
|
|
|
|
for (i = 0; i < overlays->len; i++)
|
|
|
|
overlay_rectangle_deassociate(g_ptr_array_index(overlays, i));
|
|
|
|
|
|
|
|
for (i = 0; i < overlays->len; i++) {
|
|
|
|
if (!overlay_rectangle_associate(g_ptr_array_index(overlays, i)))
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
return TRUE;
|
|
|
|
}
|
|
|
|
|
2011-12-14 13:35:13 +00:00
|
|
|
static void
|
2013-01-10 17:42:37 +00:00
|
|
|
gst_vaapi_context_clear_overlay(GstVaapiContext *context)
|
2011-12-14 13:35:13 +00:00
|
|
|
{
|
|
|
|
GstVaapiContextPrivate * const priv = context->priv;
|
|
|
|
|
2013-01-10 17:42:37 +00:00
|
|
|
overlay_clear(priv->overlays[0]);
|
|
|
|
overlay_clear(priv->overlays[1]);
|
|
|
|
priv->overlay_id = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static inline void
|
|
|
|
gst_vaapi_context_destroy_overlay(GstVaapiContext *context)
|
|
|
|
{
|
|
|
|
gst_vaapi_context_clear_overlay(context);
|
2011-12-14 13:35:13 +00:00
|
|
|
}
|
|
|
|
|
2010-04-23 15:59:31 +00:00
|
|
|
static void
|
|
|
|
unref_surface_cb(gpointer data, gpointer user_data)
|
|
|
|
{
|
2011-12-13 15:53:15 +00:00
|
|
|
GstVaapiSurface * const surface = GST_VAAPI_SURFACE(data);
|
|
|
|
|
|
|
|
gst_vaapi_surface_set_parent_context(surface, NULL);
|
|
|
|
g_object_unref(surface);
|
2010-04-23 15:59:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
gst_vaapi_context_destroy_surfaces(GstVaapiContext *context)
|
|
|
|
{
|
|
|
|
GstVaapiContextPrivate * const priv = context->priv;
|
|
|
|
|
2011-12-14 13:35:13 +00:00
|
|
|
gst_vaapi_context_destroy_overlay(context);
|
|
|
|
|
2010-04-23 15:59:31 +00:00
|
|
|
if (priv->surfaces) {
|
|
|
|
g_ptr_array_foreach(priv->surfaces, unref_surface_cb, NULL);
|
|
|
|
g_ptr_array_free(priv->surfaces, TRUE);
|
|
|
|
priv->surfaces = NULL;
|
|
|
|
}
|
|
|
|
|
2012-06-30 20:34:15 +00:00
|
|
|
g_clear_object(&priv->surfaces_pool);
|
2010-04-23 15:59:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
gst_vaapi_context_destroy(GstVaapiContext *context)
|
|
|
|
{
|
|
|
|
GstVaapiDisplay * const display = GST_VAAPI_OBJECT_DISPLAY(context);
|
|
|
|
GstVaapiContextPrivate * const priv = context->priv;
|
|
|
|
VAContextID context_id;
|
|
|
|
VAStatus status;
|
|
|
|
|
|
|
|
context_id = GST_VAAPI_OBJECT_ID(context);
|
|
|
|
GST_DEBUG("context %" GST_VAAPI_ID_FORMAT, GST_VAAPI_ID_ARGS(context_id));
|
|
|
|
|
|
|
|
if (context_id != VA_INVALID_ID) {
|
|
|
|
GST_VAAPI_DISPLAY_LOCK(display);
|
|
|
|
status = vaDestroyContext(
|
|
|
|
GST_VAAPI_DISPLAY_VADISPLAY(display),
|
|
|
|
context_id
|
|
|
|
);
|
|
|
|
GST_VAAPI_DISPLAY_UNLOCK(display);
|
|
|
|
if (!vaapi_check_status(status, "vaDestroyContext()"))
|
|
|
|
g_warning("failed to destroy context %" GST_VAAPI_ID_FORMAT,
|
|
|
|
GST_VAAPI_ID_ARGS(context_id));
|
|
|
|
GST_VAAPI_OBJECT_ID(context) = VA_INVALID_ID;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (priv->config_id != VA_INVALID_ID) {
|
|
|
|
GST_VAAPI_DISPLAY_LOCK(display);
|
|
|
|
status = vaDestroyConfig(
|
|
|
|
GST_VAAPI_DISPLAY_VADISPLAY(display),
|
|
|
|
priv->config_id
|
|
|
|
);
|
|
|
|
GST_VAAPI_DISPLAY_UNLOCK(display);
|
|
|
|
if (!vaapi_check_status(status, "vaDestroyConfig()"))
|
|
|
|
g_warning("failed to destroy config %" GST_VAAPI_ID_FORMAT,
|
|
|
|
GST_VAAPI_ID_ARGS(priv->config_id));
|
|
|
|
priv->config_id = VA_INVALID_ID;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-12-14 13:35:13 +00:00
|
|
|
static gboolean
|
|
|
|
gst_vaapi_context_create_overlay(GstVaapiContext *context)
|
|
|
|
{
|
|
|
|
GstVaapiContextPrivate * const priv = context->priv;
|
|
|
|
|
2013-01-10 17:42:37 +00:00
|
|
|
if (!priv->overlays[0] || !priv->overlays[1])
|
2013-01-10 12:41:39 +00:00
|
|
|
return FALSE;
|
|
|
|
|
2013-01-10 17:42:37 +00:00
|
|
|
gst_vaapi_context_clear_overlay(context);
|
2011-12-14 13:35:13 +00:00
|
|
|
return TRUE;
|
|
|
|
}
|
|
|
|
|
2010-04-23 15:59:31 +00:00
|
|
|
static gboolean
|
|
|
|
gst_vaapi_context_create_surfaces(GstVaapiContext *context)
|
|
|
|
{
|
|
|
|
GstVaapiContextPrivate * const priv = context->priv;
|
|
|
|
GstCaps *caps;
|
|
|
|
GstVaapiSurface *surface;
|
2012-09-10 16:15:02 +00:00
|
|
|
guint i, num_surfaces;
|
2010-04-23 15:59:31 +00:00
|
|
|
|
|
|
|
/* Number of scratch surfaces beyond those used as reference */
|
|
|
|
const guint SCRATCH_SURFACES_COUNT = 4;
|
|
|
|
|
2011-12-14 13:35:13 +00:00
|
|
|
if (!gst_vaapi_context_create_overlay(context))
|
|
|
|
return FALSE;
|
|
|
|
|
2010-04-23 15:59:31 +00:00
|
|
|
if (!priv->surfaces) {
|
|
|
|
priv->surfaces = g_ptr_array_new();
|
|
|
|
if (!priv->surfaces)
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!priv->surfaces_pool) {
|
|
|
|
caps = gst_caps_new_simple(
|
2011-11-04 21:16:23 +00:00
|
|
|
GST_VAAPI_SURFACE_CAPS_NAME,
|
|
|
|
"type", G_TYPE_STRING, "vaapi",
|
2010-04-23 15:59:31 +00:00
|
|
|
"width", G_TYPE_INT, priv->width,
|
|
|
|
"height", G_TYPE_INT, priv->height,
|
|
|
|
NULL
|
|
|
|
);
|
|
|
|
if (!caps)
|
|
|
|
return FALSE;
|
|
|
|
priv->surfaces_pool = gst_vaapi_surface_pool_new(
|
|
|
|
GST_VAAPI_OBJECT_DISPLAY(context),
|
|
|
|
caps
|
|
|
|
);
|
|
|
|
gst_caps_unref(caps);
|
|
|
|
if (!priv->surfaces_pool)
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
|
2012-09-10 16:15:02 +00:00
|
|
|
num_surfaces = priv->ref_frames + SCRATCH_SURFACES_COUNT;
|
2010-04-23 15:59:31 +00:00
|
|
|
gst_vaapi_video_pool_set_capacity(priv->surfaces_pool, num_surfaces);
|
|
|
|
|
|
|
|
for (i = priv->surfaces->len; i < num_surfaces; i++) {
|
|
|
|
surface = gst_vaapi_surface_new(
|
|
|
|
GST_VAAPI_OBJECT_DISPLAY(context),
|
|
|
|
GST_VAAPI_CHROMA_TYPE_YUV420,
|
|
|
|
priv->width, priv->height
|
|
|
|
);
|
|
|
|
if (!surface)
|
|
|
|
return FALSE;
|
|
|
|
g_ptr_array_add(priv->surfaces, surface);
|
|
|
|
if (!gst_vaapi_video_pool_add_object(priv->surfaces_pool, surface))
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
return TRUE;
|
|
|
|
}
|
|
|
|
|
|
|
|
static gboolean
|
|
|
|
gst_vaapi_context_create(GstVaapiContext *context)
|
|
|
|
{
|
|
|
|
GstVaapiDisplay * const display = GST_VAAPI_OBJECT_DISPLAY(context);
|
|
|
|
GstVaapiContextPrivate * const priv = context->priv;
|
|
|
|
VAProfile va_profile;
|
|
|
|
VAEntrypoint va_entrypoint;
|
|
|
|
VAConfigAttrib attrib;
|
|
|
|
VAContextID context_id;
|
|
|
|
VASurfaceID surface_id;
|
|
|
|
VAStatus status;
|
|
|
|
GArray *surfaces = NULL;
|
|
|
|
gboolean success = FALSE;
|
|
|
|
guint i;
|
|
|
|
|
|
|
|
if (!priv->surfaces && !gst_vaapi_context_create_surfaces(context))
|
|
|
|
goto end;
|
|
|
|
|
|
|
|
surfaces = g_array_sized_new(
|
|
|
|
FALSE,
|
|
|
|
FALSE,
|
|
|
|
sizeof(VASurfaceID),
|
|
|
|
priv->surfaces->len
|
|
|
|
);
|
|
|
|
if (!surfaces)
|
|
|
|
goto end;
|
|
|
|
|
|
|
|
for (i = 0; i < priv->surfaces->len; i++) {
|
|
|
|
GstVaapiSurface * const surface = g_ptr_array_index(priv->surfaces, i);
|
|
|
|
if (!surface)
|
|
|
|
goto end;
|
|
|
|
surface_id = GST_VAAPI_OBJECT_ID(surface);
|
|
|
|
g_array_append_val(surfaces, surface_id);
|
|
|
|
}
|
|
|
|
assert(surfaces->len == priv->surfaces->len);
|
|
|
|
|
|
|
|
if (!priv->profile || !priv->entrypoint)
|
|
|
|
goto end;
|
|
|
|
va_profile = gst_vaapi_profile_get_va_profile(priv->profile);
|
|
|
|
va_entrypoint = gst_vaapi_entrypoint_get_va_entrypoint(priv->entrypoint);
|
|
|
|
|
|
|
|
GST_VAAPI_DISPLAY_LOCK(display);
|
|
|
|
attrib.type = VAConfigAttribRTFormat;
|
|
|
|
status = vaGetConfigAttributes(
|
|
|
|
GST_VAAPI_DISPLAY_VADISPLAY(display),
|
|
|
|
va_profile,
|
|
|
|
va_entrypoint,
|
|
|
|
&attrib, 1
|
|
|
|
);
|
|
|
|
GST_VAAPI_DISPLAY_UNLOCK(display);
|
|
|
|
if (!vaapi_check_status(status, "vaGetConfigAttributes()"))
|
|
|
|
goto end;
|
|
|
|
if (!(attrib.value & VA_RT_FORMAT_YUV420))
|
|
|
|
goto end;
|
|
|
|
|
|
|
|
GST_VAAPI_DISPLAY_LOCK(display);
|
|
|
|
status = vaCreateConfig(
|
|
|
|
GST_VAAPI_DISPLAY_VADISPLAY(display),
|
|
|
|
va_profile,
|
|
|
|
va_entrypoint,
|
|
|
|
&attrib, 1,
|
|
|
|
&priv->config_id
|
|
|
|
);
|
|
|
|
GST_VAAPI_DISPLAY_UNLOCK(display);
|
|
|
|
if (!vaapi_check_status(status, "vaCreateConfig()"))
|
|
|
|
goto end;
|
|
|
|
|
|
|
|
GST_VAAPI_DISPLAY_LOCK(display);
|
|
|
|
status = vaCreateContext(
|
|
|
|
GST_VAAPI_DISPLAY_VADISPLAY(display),
|
|
|
|
priv->config_id,
|
|
|
|
priv->width, priv->height,
|
|
|
|
VA_PROGRESSIVE,
|
|
|
|
(VASurfaceID *)surfaces->data, surfaces->len,
|
|
|
|
&context_id
|
|
|
|
);
|
|
|
|
GST_VAAPI_DISPLAY_UNLOCK(display);
|
|
|
|
if (!vaapi_check_status(status, "vaCreateContext()"))
|
|
|
|
goto end;
|
|
|
|
|
|
|
|
GST_DEBUG("context %" GST_VAAPI_ID_FORMAT, GST_VAAPI_ID_ARGS(context_id));
|
|
|
|
GST_VAAPI_OBJECT_ID(context) = context_id;
|
|
|
|
success = TRUE;
|
|
|
|
end:
|
|
|
|
if (surfaces)
|
|
|
|
g_array_free(surfaces, TRUE);
|
|
|
|
return success;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
gst_vaapi_context_finalize(GObject *object)
|
|
|
|
{
|
|
|
|
GstVaapiContext * const context = GST_VAAPI_CONTEXT(object);
|
2013-01-10 12:41:39 +00:00
|
|
|
GstVaapiContextPrivate * const priv = context->priv;
|
2010-04-23 15:59:31 +00:00
|
|
|
|
2013-01-10 17:42:37 +00:00
|
|
|
overlay_destroy(&priv->overlays[0]);
|
|
|
|
overlay_destroy(&priv->overlays[1]);
|
2010-04-23 15:59:31 +00:00
|
|
|
gst_vaapi_context_destroy(context);
|
|
|
|
gst_vaapi_context_destroy_surfaces(context);
|
|
|
|
|
|
|
|
G_OBJECT_CLASS(gst_vaapi_context_parent_class)->finalize(object);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
gst_vaapi_context_set_property(
|
|
|
|
GObject *object,
|
|
|
|
guint prop_id,
|
|
|
|
const GValue *value,
|
|
|
|
GParamSpec *pspec
|
|
|
|
)
|
|
|
|
{
|
|
|
|
GstVaapiContext * const context = GST_VAAPI_CONTEXT(object);
|
|
|
|
GstVaapiContextPrivate * const priv = context->priv;
|
|
|
|
|
|
|
|
switch (prop_id) {
|
|
|
|
case PROP_PROFILE:
|
|
|
|
gst_vaapi_context_set_profile(context, g_value_get_uint(value));
|
|
|
|
break;
|
|
|
|
case PROP_ENTRYPOINT:
|
|
|
|
priv->entrypoint = g_value_get_uint(value);
|
|
|
|
break;
|
|
|
|
case PROP_WIDTH:
|
|
|
|
priv->width = g_value_get_uint(value);
|
|
|
|
break;
|
|
|
|
case PROP_HEIGHT:
|
|
|
|
priv->height = g_value_get_uint(value);
|
|
|
|
break;
|
2012-09-10 16:15:02 +00:00
|
|
|
case PROP_REF_FRAMES:
|
|
|
|
priv->ref_frames = g_value_get_uint(value);
|
|
|
|
break;
|
2010-04-23 15:59:31 +00:00
|
|
|
default:
|
|
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
gst_vaapi_context_get_property(
|
|
|
|
GObject *object,
|
|
|
|
guint prop_id,
|
|
|
|
GValue *value,
|
|
|
|
GParamSpec *pspec
|
|
|
|
)
|
|
|
|
{
|
|
|
|
GstVaapiContext * const context = GST_VAAPI_CONTEXT(object);
|
|
|
|
GstVaapiContextPrivate * const priv = context->priv;
|
|
|
|
|
|
|
|
switch (prop_id) {
|
|
|
|
case PROP_PROFILE:
|
|
|
|
g_value_set_uint(value, gst_vaapi_context_get_profile(context));
|
|
|
|
break;
|
|
|
|
case PROP_ENTRYPOINT:
|
|
|
|
g_value_set_uint(value, gst_vaapi_context_get_entrypoint(context));
|
|
|
|
break;
|
|
|
|
case PROP_WIDTH:
|
|
|
|
g_value_set_uint(value, priv->width);
|
|
|
|
break;
|
|
|
|
case PROP_HEIGHT:
|
|
|
|
g_value_set_uint(value, priv->height);
|
|
|
|
break;
|
2012-09-10 16:15:02 +00:00
|
|
|
case PROP_REF_FRAMES:
|
|
|
|
g_value_set_uint(value, priv->ref_frames);
|
|
|
|
break;
|
2010-04-23 15:59:31 +00:00
|
|
|
default:
|
|
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
gst_vaapi_context_class_init(GstVaapiContextClass *klass)
|
|
|
|
{
|
|
|
|
GObjectClass * const object_class = G_OBJECT_CLASS(klass);
|
|
|
|
|
|
|
|
g_type_class_add_private(klass, sizeof(GstVaapiContextPrivate));
|
|
|
|
|
|
|
|
object_class->finalize = gst_vaapi_context_finalize;
|
|
|
|
object_class->set_property = gst_vaapi_context_set_property;
|
|
|
|
object_class->get_property = gst_vaapi_context_get_property;
|
|
|
|
|
|
|
|
g_object_class_install_property
|
|
|
|
(object_class,
|
|
|
|
PROP_PROFILE,
|
|
|
|
g_param_spec_uint("profile",
|
|
|
|
"Profile",
|
|
|
|
"The profile used for decoding",
|
|
|
|
0, G_MAXUINT32, 0,
|
|
|
|
G_PARAM_READWRITE));
|
|
|
|
|
|
|
|
g_object_class_install_property
|
|
|
|
(object_class,
|
|
|
|
PROP_ENTRYPOINT,
|
|
|
|
g_param_spec_uint("entrypoint",
|
|
|
|
"Entrypoint",
|
|
|
|
"The decoder entrypoint",
|
|
|
|
0, G_MAXUINT32, 0,
|
|
|
|
G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY));
|
|
|
|
|
|
|
|
g_object_class_install_property
|
|
|
|
(object_class,
|
|
|
|
PROP_WIDTH,
|
|
|
|
g_param_spec_uint("width",
|
|
|
|
"Width",
|
|
|
|
"The width of decoded surfaces",
|
|
|
|
0, G_MAXINT32, 0,
|
|
|
|
G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY));
|
|
|
|
|
|
|
|
g_object_class_install_property
|
|
|
|
(object_class,
|
|
|
|
PROP_HEIGHT,
|
|
|
|
g_param_spec_uint("height",
|
|
|
|
"Height",
|
|
|
|
"The height of the decoded surfaces",
|
|
|
|
0, G_MAXINT32, 0,
|
|
|
|
G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY));
|
2012-09-10 16:15:02 +00:00
|
|
|
|
|
|
|
g_object_class_install_property
|
|
|
|
(object_class,
|
|
|
|
PROP_REF_FRAMES,
|
|
|
|
g_param_spec_uint("ref-frames",
|
|
|
|
"Reference Frames",
|
|
|
|
"The number of reference frames",
|
|
|
|
0, G_MAXINT32, 0,
|
|
|
|
G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY));
|
2010-04-23 15:59:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
gst_vaapi_context_init(GstVaapiContext *context)
|
|
|
|
{
|
|
|
|
GstVaapiContextPrivate *priv = GST_VAAPI_CONTEXT_GET_PRIVATE(context);
|
|
|
|
|
|
|
|
context->priv = priv;
|
|
|
|
priv->config_id = VA_INVALID_ID;
|
|
|
|
priv->surfaces = NULL;
|
|
|
|
priv->surfaces_pool = NULL;
|
2013-01-10 17:42:37 +00:00
|
|
|
priv->overlays[0] = overlay_new();
|
|
|
|
priv->overlays[1] = overlay_new();
|
2010-04-23 15:59:31 +00:00
|
|
|
priv->profile = 0;
|
|
|
|
priv->entrypoint = 0;
|
|
|
|
priv->width = 0;
|
|
|
|
priv->height = 0;
|
2012-09-10 16:15:02 +00:00
|
|
|
priv->ref_frames = 0;
|
2010-04-23 15:59:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* gst_vaapi_context_new:
|
|
|
|
* @display: a #GstVaapiDisplay
|
|
|
|
* @profile: a #GstVaapiProfile
|
|
|
|
* @entrypoint: a #GstVaapiEntrypoint
|
|
|
|
* @width: coded width from the bitstream
|
|
|
|
* @height: coded height from the bitstream
|
|
|
|
*
|
|
|
|
* Creates a new #GstVaapiContext with the specified codec @profile
|
|
|
|
* and @entrypoint.
|
|
|
|
*
|
|
|
|
* Return value: the newly allocated #GstVaapiContext object
|
|
|
|
*/
|
|
|
|
GstVaapiContext *
|
|
|
|
gst_vaapi_context_new(
|
|
|
|
GstVaapiDisplay *display,
|
|
|
|
GstVaapiProfile profile,
|
|
|
|
GstVaapiEntrypoint entrypoint,
|
2012-09-10 16:15:02 +00:00
|
|
|
guint width,
|
|
|
|
guint height
|
2010-04-23 15:59:31 +00:00
|
|
|
)
|
2012-09-10 16:15:02 +00:00
|
|
|
{
|
|
|
|
GstVaapiContextInfo info;
|
|
|
|
|
|
|
|
info.profile = profile;
|
|
|
|
info.entrypoint = entrypoint;
|
|
|
|
info.width = width;
|
|
|
|
info.height = height;
|
|
|
|
info.ref_frames = get_max_ref_frames(profile);
|
|
|
|
return gst_vaapi_context_new_full(display, &info);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* gst_vaapi_context_new_full:
|
|
|
|
* @display: a #GstVaapiDisplay
|
|
|
|
* @cip: a pointer to the #GstVaapiContextInfo
|
|
|
|
*
|
|
|
|
* Creates a new #GstVaapiContext with the configuration specified by
|
|
|
|
* @cip, thus including profile, entry-point, encoded size and maximum
|
|
|
|
* number of reference frames reported by the bitstream.
|
|
|
|
*
|
|
|
|
* Return value: the newly allocated #GstVaapiContext object
|
|
|
|
*/
|
|
|
|
GstVaapiContext *
|
|
|
|
gst_vaapi_context_new_full(GstVaapiDisplay *display, GstVaapiContextInfo *cip)
|
2010-04-23 15:59:31 +00:00
|
|
|
{
|
|
|
|
GstVaapiContext *context;
|
|
|
|
|
|
|
|
g_return_val_if_fail(GST_VAAPI_IS_DISPLAY(display), NULL);
|
2012-09-10 16:15:02 +00:00
|
|
|
g_return_val_if_fail(cip->profile, NULL);
|
|
|
|
g_return_val_if_fail(cip->entrypoint, NULL);
|
|
|
|
g_return_val_if_fail(cip->width > 0, NULL);
|
|
|
|
g_return_val_if_fail(cip->height > 0, NULL);
|
2010-04-23 15:59:31 +00:00
|
|
|
|
|
|
|
context = g_object_new(
|
|
|
|
GST_VAAPI_TYPE_CONTEXT,
|
|
|
|
"display", display,
|
|
|
|
"id", GST_VAAPI_ID(VA_INVALID_ID),
|
2012-09-10 16:15:02 +00:00
|
|
|
"profile", cip->profile,
|
|
|
|
"entrypoint", cip->entrypoint,
|
|
|
|
"width", cip->width,
|
|
|
|
"height", cip->height,
|
|
|
|
"ref-frames", cip->ref_frames,
|
2010-04-23 15:59:31 +00:00
|
|
|
NULL
|
|
|
|
);
|
|
|
|
if (!context->priv->is_constructed) {
|
|
|
|
g_object_unref(context);
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
return context;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* gst_vaapi_context_reset:
|
|
|
|
* @context: a #GstVaapiContext
|
|
|
|
* @profile: a #GstVaapiProfile
|
|
|
|
* @entrypoint: a #GstVaapiEntrypoint
|
|
|
|
* @width: coded width from the bitstream
|
|
|
|
* @height: coded height from the bitstream
|
|
|
|
*
|
|
|
|
* Resets @context to the specified codec @profile and @entrypoint.
|
|
|
|
* The surfaces will be reallocated if the coded size changed.
|
|
|
|
*
|
|
|
|
* Return value: %TRUE on success
|
|
|
|
*/
|
|
|
|
gboolean
|
|
|
|
gst_vaapi_context_reset(
|
|
|
|
GstVaapiContext *context,
|
|
|
|
GstVaapiProfile profile,
|
|
|
|
GstVaapiEntrypoint entrypoint,
|
|
|
|
unsigned int width,
|
|
|
|
unsigned int height
|
|
|
|
)
|
2012-09-10 16:15:02 +00:00
|
|
|
{
|
|
|
|
GstVaapiContextPrivate * const priv = context->priv;
|
|
|
|
GstVaapiContextInfo info;
|
|
|
|
|
|
|
|
info.profile = profile;
|
|
|
|
info.entrypoint = entrypoint;
|
|
|
|
info.width = width;
|
|
|
|
info.height = height;
|
|
|
|
info.ref_frames = priv->ref_frames;
|
|
|
|
|
|
|
|
return gst_vaapi_context_reset_full(context, &info);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* gst_vaapi_context_reset_full:
|
|
|
|
* @context: a #GstVaapiContext
|
|
|
|
* @cip: a pointer to the new #GstVaapiContextInfo details
|
|
|
|
*
|
|
|
|
* Resets @context to the configuration specified by @cip, thus
|
|
|
|
* including profile, entry-point, encoded size and maximum number of
|
|
|
|
* reference frames reported by the bitstream.
|
|
|
|
*
|
|
|
|
* Return value: %TRUE on success
|
|
|
|
*/
|
|
|
|
gboolean
|
|
|
|
gst_vaapi_context_reset_full(GstVaapiContext *context, GstVaapiContextInfo *cip)
|
2010-04-23 15:59:31 +00:00
|
|
|
{
|
|
|
|
GstVaapiContextPrivate * const priv = context->priv;
|
|
|
|
gboolean size_changed, codec_changed;
|
|
|
|
|
2012-09-10 16:15:02 +00:00
|
|
|
size_changed = priv->width != cip->width || priv->height != cip->height;
|
2010-04-23 15:59:31 +00:00
|
|
|
if (size_changed) {
|
|
|
|
gst_vaapi_context_destroy_surfaces(context);
|
2012-09-10 16:15:02 +00:00
|
|
|
priv->width = cip->width;
|
|
|
|
priv->height = cip->height;
|
2010-04-23 15:59:31 +00:00
|
|
|
}
|
|
|
|
|
2012-09-10 16:15:02 +00:00
|
|
|
codec_changed = priv->profile != cip->profile || priv->entrypoint != cip->entrypoint;
|
2010-04-23 15:59:31 +00:00
|
|
|
if (codec_changed) {
|
|
|
|
gst_vaapi_context_destroy(context);
|
2012-09-10 16:15:02 +00:00
|
|
|
priv->profile = cip->profile;
|
|
|
|
priv->entrypoint = cip->entrypoint;
|
2010-04-23 15:59:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (size_changed && !gst_vaapi_context_create_surfaces(context))
|
|
|
|
return FALSE;
|
|
|
|
|
|
|
|
if (codec_changed && !gst_vaapi_context_create(context))
|
|
|
|
return FALSE;
|
|
|
|
|
|
|
|
priv->is_constructed = TRUE;
|
|
|
|
return TRUE;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* gst_vaapi_context_get_id:
|
|
|
|
* @context: a #GstVaapiContext
|
|
|
|
*
|
|
|
|
* Returns the underlying VAContextID of the @context.
|
|
|
|
*
|
|
|
|
* Return value: the underlying VA context id
|
|
|
|
*/
|
|
|
|
GstVaapiID
|
|
|
|
gst_vaapi_context_get_id(GstVaapiContext *context)
|
|
|
|
{
|
|
|
|
g_return_val_if_fail(GST_VAAPI_IS_CONTEXT(context), VA_INVALID_ID);
|
|
|
|
|
|
|
|
return GST_VAAPI_OBJECT_ID(context);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* gst_vaapi_context_get_profile:
|
|
|
|
* @context: a #GstVaapiContext
|
|
|
|
*
|
|
|
|
* Returns the VA profile used by the @context.
|
|
|
|
*
|
|
|
|
* Return value: the VA profile used by the @context
|
|
|
|
*/
|
|
|
|
GstVaapiProfile
|
|
|
|
gst_vaapi_context_get_profile(GstVaapiContext *context)
|
|
|
|
{
|
|
|
|
g_return_val_if_fail(GST_VAAPI_IS_CONTEXT(context), 0);
|
|
|
|
|
|
|
|
return context->priv->profile;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* gst_vaapi_context_set_profile:
|
|
|
|
* @context: a #GstVaapiContext
|
|
|
|
* @profile: the new #GstVaapiProfile to use
|
|
|
|
*
|
|
|
|
* Sets the new @profile to use with the @context. If @profile matches
|
|
|
|
* the previous profile, this call has no effect. Otherwise, the
|
|
|
|
* underlying VA context is recreated, while keeping the previously
|
|
|
|
* allocated surfaces.
|
|
|
|
*
|
|
|
|
* Return value: %TRUE on success
|
|
|
|
*/
|
|
|
|
gboolean
|
|
|
|
gst_vaapi_context_set_profile(GstVaapiContext *context, GstVaapiProfile profile)
|
|
|
|
{
|
|
|
|
g_return_val_if_fail(GST_VAAPI_IS_CONTEXT(context), FALSE);
|
|
|
|
g_return_val_if_fail(profile, FALSE);
|
|
|
|
|
|
|
|
return gst_vaapi_context_reset(context,
|
|
|
|
profile,
|
|
|
|
context->priv->entrypoint,
|
|
|
|
context->priv->width,
|
|
|
|
context->priv->height);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* gst_vaapi_context_get_entrypoint:
|
|
|
|
* @context: a #GstVaapiContext
|
|
|
|
*
|
|
|
|
* Returns the VA entrypoint used by the @context
|
|
|
|
*
|
|
|
|
* Return value: the VA entrypoint used by the @context
|
|
|
|
*/
|
|
|
|
GstVaapiEntrypoint
|
|
|
|
gst_vaapi_context_get_entrypoint(GstVaapiContext *context)
|
|
|
|
{
|
|
|
|
g_return_val_if_fail(GST_VAAPI_IS_CONTEXT(context), 0);
|
|
|
|
|
|
|
|
return context->priv->entrypoint;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* gst_vaapi_context_get_size:
|
|
|
|
* @context: a #GstVaapiContext
|
|
|
|
* @pwidth: return location for the width, or %NULL
|
|
|
|
* @pheight: return location for the height, or %NULL
|
|
|
|
*
|
|
|
|
* Retrieves the size of the surfaces attached to @context.
|
|
|
|
*/
|
|
|
|
void
|
|
|
|
gst_vaapi_context_get_size(
|
|
|
|
GstVaapiContext *context,
|
|
|
|
guint *pwidth,
|
|
|
|
guint *pheight
|
|
|
|
)
|
|
|
|
{
|
|
|
|
g_return_if_fail(GST_VAAPI_IS_CONTEXT(context));
|
|
|
|
|
|
|
|
if (pwidth)
|
|
|
|
*pwidth = context->priv->width;
|
|
|
|
|
|
|
|
if (pheight)
|
|
|
|
*pheight = context->priv->height;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* gst_vaapi_context_get_surface:
|
|
|
|
* @context: a #GstVaapiContext
|
|
|
|
*
|
|
|
|
* Acquires a free surface. The returned surface but be released with
|
|
|
|
* gst_vaapi_context_put_surface(). This function returns %NULL if
|
|
|
|
* there is no free surface available in the pool. The surfaces are
|
|
|
|
* pre-allocated during context creation though.
|
|
|
|
*
|
|
|
|
* Return value: a free surface, or %NULL if none is available
|
|
|
|
*/
|
|
|
|
GstVaapiSurface *
|
|
|
|
gst_vaapi_context_get_surface(GstVaapiContext *context)
|
|
|
|
{
|
2012-01-05 13:50:26 +00:00
|
|
|
GstVaapiSurface *surface;
|
|
|
|
|
2010-04-23 15:59:31 +00:00
|
|
|
g_return_val_if_fail(GST_VAAPI_IS_CONTEXT(context), NULL);
|
|
|
|
|
2012-01-05 13:50:26 +00:00
|
|
|
surface = gst_vaapi_video_pool_get_object(context->priv->surfaces_pool);
|
|
|
|
if (!surface)
|
|
|
|
return NULL;
|
|
|
|
|
|
|
|
gst_vaapi_surface_set_parent_context(surface, context);
|
|
|
|
return surface;
|
2010-04-23 15:59:31 +00:00
|
|
|
}
|
|
|
|
|
2010-05-15 05:36:15 +00:00
|
|
|
/**
|
|
|
|
* gst_vaapi_context_get_surface_count:
|
|
|
|
* @context: a #GstVaapiContext
|
|
|
|
*
|
|
|
|
* Retrieves the number of free surfaces left in the pool.
|
|
|
|
*
|
|
|
|
* Return value: the number of free surfaces available in the pool
|
|
|
|
*/
|
|
|
|
guint
|
|
|
|
gst_vaapi_context_get_surface_count(GstVaapiContext *context)
|
|
|
|
{
|
|
|
|
g_return_val_if_fail(GST_VAAPI_IS_CONTEXT(context), 0);
|
|
|
|
|
|
|
|
return gst_vaapi_video_pool_get_size(context->priv->surfaces_pool);
|
|
|
|
}
|
|
|
|
|
2010-04-23 15:59:31 +00:00
|
|
|
/**
|
|
|
|
* gst_vaapi_context_put_surface:
|
|
|
|
* @context: a #GstVaapiContext
|
|
|
|
* @surface: the #GstVaapiSurface to release
|
|
|
|
*
|
|
|
|
* Releases a surface acquired by gst_vaapi_context_get_surface().
|
|
|
|
*/
|
|
|
|
void
|
|
|
|
gst_vaapi_context_put_surface(GstVaapiContext *context, GstVaapiSurface *surface)
|
|
|
|
{
|
|
|
|
g_return_if_fail(GST_VAAPI_IS_CONTEXT(context));
|
|
|
|
g_return_if_fail(GST_VAAPI_IS_SURFACE(surface));
|
|
|
|
|
2012-01-05 13:50:26 +00:00
|
|
|
gst_vaapi_surface_set_parent_context(surface, NULL);
|
2010-04-23 15:59:31 +00:00
|
|
|
gst_vaapi_video_pool_put_object(context->priv->surfaces_pool, surface);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* gst_vaapi_context_find_surface_by_id:
|
|
|
|
* @context: a #GstVaapiContext
|
|
|
|
* @id: the VA surface id to find
|
|
|
|
*
|
|
|
|
* Finds VA surface by @id in the list of surfaces attached to the @context.
|
|
|
|
*
|
|
|
|
* Return value: the matching #GstVaapiSurface object, or %NULL if
|
|
|
|
* none was found
|
|
|
|
*/
|
|
|
|
GstVaapiSurface *
|
|
|
|
gst_vaapi_context_find_surface_by_id(GstVaapiContext *context, GstVaapiID id)
|
|
|
|
{
|
|
|
|
GstVaapiContextPrivate *priv;
|
|
|
|
GstVaapiSurface *surface;
|
|
|
|
guint i;
|
|
|
|
|
|
|
|
g_return_val_if_fail(GST_VAAPI_IS_CONTEXT(context), NULL);
|
|
|
|
|
|
|
|
priv = context->priv;
|
|
|
|
g_return_val_if_fail(priv->surfaces, NULL);
|
|
|
|
|
|
|
|
for (i = 0; i < priv->surfaces->len; i++) {
|
|
|
|
surface = g_ptr_array_index(priv->surfaces, i);
|
|
|
|
if (GST_VAAPI_OBJECT_ID(surface) == id)
|
|
|
|
return surface;
|
|
|
|
}
|
|
|
|
return NULL;
|
|
|
|
}
|
2011-12-14 13:35:13 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* gst_vaapi_context_apply_composition:
|
|
|
|
* @context: a #GstVaapiContext
|
|
|
|
* @composition: a #GstVideoOverlayComposition
|
|
|
|
*
|
|
|
|
* Applies video composition planes to all surfaces bound to @context.
|
|
|
|
* This helper function resets any additional subpictures the user may
|
|
|
|
* have associated himself. A %NULL @composition will also clear all
|
|
|
|
* the existing subpictures.
|
|
|
|
*
|
|
|
|
* Return value: %TRUE if all composition planes could be applied,
|
|
|
|
* %FALSE otherwise
|
|
|
|
*/
|
|
|
|
gboolean
|
|
|
|
gst_vaapi_context_apply_composition(
|
|
|
|
GstVaapiContext *context,
|
|
|
|
GstVideoOverlayComposition *composition
|
|
|
|
)
|
|
|
|
{
|
|
|
|
GstVaapiContextPrivate *priv;
|
2013-01-10 17:42:37 +00:00
|
|
|
GPtrArray *curr_overlay, *next_overlay;
|
2013-01-10 12:41:39 +00:00
|
|
|
guint i, n_rectangles;
|
2013-01-11 14:19:45 +00:00
|
|
|
gboolean reassociate = FALSE;
|
2011-12-14 13:35:13 +00:00
|
|
|
|
|
|
|
g_return_val_if_fail(GST_VAAPI_IS_CONTEXT(context), FALSE);
|
|
|
|
|
|
|
|
priv = context->priv;
|
|
|
|
|
2013-01-10 12:41:39 +00:00
|
|
|
if (!priv->surfaces)
|
2011-12-14 13:35:13 +00:00
|
|
|
return FALSE;
|
|
|
|
|
2013-01-10 12:41:39 +00:00
|
|
|
if (!composition) {
|
2013-01-10 17:42:37 +00:00
|
|
|
gst_vaapi_context_clear_overlay(context);
|
2011-12-14 13:35:13 +00:00
|
|
|
return TRUE;
|
2013-01-10 12:41:39 +00:00
|
|
|
}
|
|
|
|
|
2013-01-10 17:42:37 +00:00
|
|
|
curr_overlay = priv->overlays[priv->overlay_id];
|
|
|
|
next_overlay = priv->overlays[priv->overlay_id ^ 1];
|
|
|
|
overlay_clear(next_overlay);
|
2011-12-14 13:35:13 +00:00
|
|
|
|
|
|
|
n_rectangles = gst_video_overlay_composition_n_rectangles(composition);
|
|
|
|
for (i = 0; i < n_rectangles; i++) {
|
2013-01-10 12:41:39 +00:00
|
|
|
GstVideoOverlayRectangle * const rect =
|
|
|
|
gst_video_overlay_composition_get_rectangle(composition, i);
|
|
|
|
GstVaapiOverlayRectangle *overlay;
|
2011-12-14 13:35:13 +00:00
|
|
|
|
2013-01-10 17:42:37 +00:00
|
|
|
overlay = overlay_lookup(curr_overlay, rect);
|
2013-01-11 14:19:45 +00:00
|
|
|
if (overlay && overlay_rectangle_update(overlay, rect, &reassociate)) {
|
2013-01-10 17:42:37 +00:00
|
|
|
overlay_rectangle_ref(overlay);
|
2013-01-11 14:19:45 +00:00
|
|
|
if (overlay->layer_id != i)
|
|
|
|
reassociate = TRUE;
|
|
|
|
}
|
2013-01-10 17:42:37 +00:00
|
|
|
else {
|
2013-01-11 14:19:45 +00:00
|
|
|
overlay = overlay_rectangle_new(rect, context, i);
|
2013-01-10 17:42:37 +00:00
|
|
|
if (!overlay) {
|
|
|
|
GST_WARNING("could not create VA overlay rectangle");
|
|
|
|
goto error;
|
|
|
|
}
|
2013-01-11 14:19:45 +00:00
|
|
|
reassociate = TRUE;
|
2011-12-14 13:35:13 +00:00
|
|
|
}
|
2013-01-10 17:42:37 +00:00
|
|
|
g_ptr_array_add(next_overlay, overlay);
|
2011-12-14 13:35:13 +00:00
|
|
|
}
|
2013-01-10 17:42:37 +00:00
|
|
|
|
|
|
|
overlay_clear(curr_overlay);
|
|
|
|
priv->overlay_id ^= 1;
|
2013-01-11 14:19:45 +00:00
|
|
|
|
|
|
|
if (reassociate && !overlay_reassociate(next_overlay))
|
|
|
|
return FALSE;
|
2011-12-14 13:35:13 +00:00
|
|
|
return TRUE;
|
2013-01-10 12:41:39 +00:00
|
|
|
|
|
|
|
error:
|
2013-01-10 17:42:37 +00:00
|
|
|
gst_vaapi_context_clear_overlay(context);
|
2013-01-10 12:41:39 +00:00
|
|
|
return FALSE;
|
2011-12-14 13:35:13 +00:00
|
|
|
}
|