mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-12-20 23:36:38 +00:00
1645 lines
49 KiB
C++
1645 lines
49 KiB
C++
/* GStreamer
|
|
* Copyright (C) 2021 Seungha Yang <seungha@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.
|
|
*/
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "config.h"
|
|
#endif
|
|
|
|
#include "gstqsvencoder.h"
|
|
#include <mfxvideo++.h>
|
|
#include <string.h>
|
|
#include <string>
|
|
|
|
#ifdef G_OS_WIN32
|
|
#include "gstqsvallocator_d3d11.h"
|
|
|
|
#include <wrl.h>
|
|
|
|
/* *INDENT-OFF* */
|
|
using namespace Microsoft::WRL;
|
|
/* *INDENT-ON* */
|
|
#else
|
|
#include "gstqsvallocator_va.h"
|
|
#endif /* G_OS_WIN32 */
|
|
|
|
GST_DEBUG_CATEGORY_STATIC (gst_qsv_encoder_debug);
|
|
#define GST_CAT_DEFAULT gst_qsv_encoder_debug
|
|
|
|
/**
|
|
* GstQsvCodingOption:
|
|
*
|
|
* Since: 1.22
|
|
*/
|
|
GType
|
|
gst_qsv_coding_option_get_type (void)
|
|
{
|
|
static GType coding_opt_type = 0;
|
|
static const GEnumValue coding_opts[] = {
|
|
/**
|
|
* GstQsvCodingOption::unknown:
|
|
*
|
|
* Since: 1.22
|
|
*/
|
|
{MFX_CODINGOPTION_UNKNOWN, "Unknown", "unknown"},
|
|
|
|
/**
|
|
* GstQsvCodingOption::on:
|
|
*
|
|
* Since: 1.22
|
|
*/
|
|
{MFX_CODINGOPTION_ON, "On", "on"},
|
|
|
|
/**
|
|
* GstQsvCodingOption::off:
|
|
*
|
|
* Since: 1.22
|
|
*/
|
|
{MFX_CODINGOPTION_OFF, "Off", "off"},
|
|
{0, nullptr, nullptr}
|
|
};
|
|
|
|
if (g_once_init_enter (&coding_opt_type)) {
|
|
GType type = g_enum_register_static ("GstQsvCodingOption",
|
|
coding_opts);
|
|
g_once_init_leave (&coding_opt_type, type);
|
|
}
|
|
|
|
return coding_opt_type;
|
|
}
|
|
|
|
enum
|
|
{
|
|
PROP_0,
|
|
PROP_ADAPTER_LUID,
|
|
PROP_DEVICE_PATH,
|
|
PROP_TARGET_USAGE,
|
|
PROP_LOW_LATENCY,
|
|
};
|
|
|
|
#define DEFAULT_TARGET_USAGE MFX_TARGETUSAGE_BALANCED
|
|
#define DEFAULT_LOW_LATENCY FALSE
|
|
|
|
typedef struct _GstQsvEncoderSurface
|
|
{
|
|
mfxFrameSurface1 surface;
|
|
mfxEncodeCtrl encode_control;
|
|
|
|
/* array of mfxPayload (e.g., SEI data) associated with this surface */
|
|
GPtrArray *payload;
|
|
|
|
/* holds ownership */
|
|
GstQsvFrame *qsv_frame;
|
|
} GstQsvEncoderSurface;
|
|
|
|
typedef struct _GstQsvEncoderTask
|
|
{
|
|
mfxSyncPoint sync_point;
|
|
mfxBitstream bitstream;
|
|
} GstQsvEncoderTask;
|
|
|
|
struct _GstQsvEncoderPrivate
|
|
{
|
|
GstObject *device;
|
|
|
|
GstVideoCodecState *input_state;
|
|
GstQsvAllocator *allocator;
|
|
|
|
/* API specific alignment requirement (multiple of 16 or 32) */
|
|
GstVideoInfo aligned_info;
|
|
|
|
mfxSession session;
|
|
mfxVideoParam video_param;
|
|
|
|
/* List of mfxExtBuffer configured by subclass, subclass will hold
|
|
* allocated memory for each mfxExtBuffer */
|
|
GPtrArray *extra_params;
|
|
|
|
MFXVideoENCODE *encoder;
|
|
GstQsvMemoryType mem_type;
|
|
|
|
/* Internal buffer pool used to allocate fallback buffer when input buffer
|
|
* is not compatible with expected format/type/resolution etc */
|
|
GstBufferPool *internal_pool;
|
|
|
|
/* Array of GstQsvEncoderSurface, holding ownership */
|
|
GArray *surface_pool;
|
|
guint next_surface_index;
|
|
|
|
/* Array of GstQsvEncoderTask, holding ownership */
|
|
GArray *task_pool;
|
|
|
|
GQueue free_tasks;
|
|
GQueue pending_tasks;
|
|
|
|
/* Properties */
|
|
guint target_usage;
|
|
gboolean low_latency;
|
|
};
|
|
|
|
/**
|
|
* GstQsvEncoder:
|
|
*
|
|
* Base class for Intel Quick Sync video encoders
|
|
*
|
|
* Since: 1.22
|
|
*/
|
|
#define gst_qsv_encoder_parent_class parent_class
|
|
G_DEFINE_ABSTRACT_TYPE_WITH_CODE (GstQsvEncoder, gst_qsv_encoder,
|
|
GST_TYPE_VIDEO_ENCODER, G_ADD_PRIVATE (GstQsvEncoder);
|
|
GST_DEBUG_CATEGORY_INIT (gst_qsv_encoder_debug,
|
|
"qsvencoder", 0, "qsvencoder"));
|
|
|
|
static void gst_qsv_encoder_dispose (GObject * object);
|
|
static void gst_qsv_encoder_finalize (GObject * object);
|
|
static void gst_qsv_encoder_set_property (GObject * object, guint prop_id,
|
|
const GValue * value, GParamSpec * pspec);
|
|
static void gst_qsv_encoder_get_property (GObject * object, guint prop_id,
|
|
GValue * value, GParamSpec * pspec);
|
|
|
|
static void gst_qsv_encoder_set_context (GstElement * element,
|
|
GstContext * context);
|
|
|
|
static gboolean gst_qsv_encoder_open (GstVideoEncoder * encoder);
|
|
static gboolean gst_qsv_encoder_stop (GstVideoEncoder * encoder);
|
|
static gboolean gst_qsv_encoder_close (GstVideoEncoder * encoder);
|
|
static gboolean gst_qsv_encoder_set_format (GstVideoEncoder * encoder,
|
|
GstVideoCodecState * state);
|
|
static GstFlowReturn gst_qsv_encoder_handle_frame (GstVideoEncoder * encoder,
|
|
GstVideoCodecFrame * frame);
|
|
static GstFlowReturn gst_qsv_encoder_finish (GstVideoEncoder * encoder);
|
|
static gboolean gst_qsv_encoder_flush (GstVideoEncoder * encoder);
|
|
static gboolean gst_qsv_encoder_sink_query (GstVideoEncoder * encoder,
|
|
GstQuery * query);
|
|
static gboolean gst_qsv_encoder_src_query (GstVideoEncoder * encoder,
|
|
GstQuery * query);
|
|
static gboolean gst_qsv_encoder_propose_allocation (GstVideoEncoder * encoder,
|
|
GstQuery * query);
|
|
|
|
static void gst_qsv_encoder_surface_clear (GstQsvEncoderSurface * task);
|
|
static void gst_qsv_encoder_task_clear (GstQsvEncoderTask * task);
|
|
|
|
static void
|
|
gst_qsv_encoder_class_init (GstQsvEncoderClass * klass)
|
|
{
|
|
GObjectClass *object_class = G_OBJECT_CLASS (klass);
|
|
GstElementClass *element_class = GST_ELEMENT_CLASS (klass);
|
|
GstVideoEncoderClass *videoenc_class = GST_VIDEO_ENCODER_CLASS (klass);
|
|
GParamFlags param_flags = (GParamFlags) (GST_PARAM_DOC_SHOW_DEFAULT |
|
|
GST_PARAM_CONDITIONALLY_AVAILABLE | G_PARAM_READABLE |
|
|
G_PARAM_STATIC_STRINGS);
|
|
|
|
object_class->dispose = gst_qsv_encoder_dispose;
|
|
object_class->finalize = gst_qsv_encoder_finalize;
|
|
object_class->set_property = gst_qsv_encoder_set_property;
|
|
object_class->get_property = gst_qsv_encoder_get_property;
|
|
|
|
#ifdef G_OS_WIN32
|
|
g_object_class_install_property (object_class, PROP_ADAPTER_LUID,
|
|
g_param_spec_int64 ("adapter-luid", "Adapter LUID",
|
|
"DXGI Adapter LUID (Locally Unique Identifier) of created device",
|
|
G_MININT64, G_MAXINT64, 0, param_flags));
|
|
#else
|
|
g_object_class_install_property (object_class, PROP_DEVICE_PATH,
|
|
g_param_spec_string ("device-path", "Device Path",
|
|
"DRM device path", nullptr, param_flags));
|
|
#endif
|
|
|
|
g_object_class_install_property (object_class, PROP_TARGET_USAGE,
|
|
g_param_spec_uint ("target-usage", "Target Usage",
|
|
"1: Best quality, 4: Balanced, 7: Best speed",
|
|
1, 7, DEFAULT_TARGET_USAGE,
|
|
(GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
|
|
|
|
g_object_class_install_property (object_class, PROP_LOW_LATENCY,
|
|
g_param_spec_boolean ("low-latency", "Low Latency",
|
|
"Enables low-latency encoding", DEFAULT_LOW_LATENCY,
|
|
(GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
|
|
|
|
element_class->set_context = GST_DEBUG_FUNCPTR (gst_qsv_encoder_set_context);
|
|
|
|
videoenc_class->open = GST_DEBUG_FUNCPTR (gst_qsv_encoder_open);
|
|
videoenc_class->stop = GST_DEBUG_FUNCPTR (gst_qsv_encoder_stop);
|
|
videoenc_class->close = GST_DEBUG_FUNCPTR (gst_qsv_encoder_close);
|
|
videoenc_class->set_format = GST_DEBUG_FUNCPTR (gst_qsv_encoder_set_format);
|
|
videoenc_class->handle_frame =
|
|
GST_DEBUG_FUNCPTR (gst_qsv_encoder_handle_frame);
|
|
videoenc_class->finish = GST_DEBUG_FUNCPTR (gst_qsv_encoder_finish);
|
|
videoenc_class->flush = GST_DEBUG_FUNCPTR (gst_qsv_encoder_flush);
|
|
videoenc_class->sink_query = GST_DEBUG_FUNCPTR (gst_qsv_encoder_sink_query);
|
|
videoenc_class->src_query = GST_DEBUG_FUNCPTR (gst_qsv_encoder_src_query);
|
|
videoenc_class->propose_allocation =
|
|
GST_DEBUG_FUNCPTR (gst_qsv_encoder_propose_allocation);
|
|
|
|
gst_type_mark_as_plugin_api (GST_TYPE_QSV_ENCODER, (GstPluginAPIFlags) 0);
|
|
gst_type_mark_as_plugin_api (GST_TYPE_QSV_CODING_OPTION,
|
|
(GstPluginAPIFlags) 0);
|
|
}
|
|
|
|
static void
|
|
gst_qsv_encoder_init (GstQsvEncoder * self)
|
|
{
|
|
GstQsvEncoderPrivate *priv;
|
|
|
|
priv = self->priv =
|
|
(GstQsvEncoderPrivate *) gst_qsv_encoder_get_instance_private (self);
|
|
|
|
priv->extra_params = g_ptr_array_sized_new (8);
|
|
|
|
priv->surface_pool = g_array_new (FALSE, TRUE, sizeof (GstQsvEncoderSurface));
|
|
g_array_set_clear_func (priv->surface_pool,
|
|
(GDestroyNotify) gst_qsv_encoder_surface_clear);
|
|
|
|
priv->task_pool = g_array_new (FALSE, TRUE, sizeof (GstQsvEncoderTask));
|
|
g_array_set_clear_func (priv->task_pool,
|
|
(GDestroyNotify) gst_qsv_encoder_task_clear);
|
|
|
|
g_queue_init (&priv->free_tasks);
|
|
g_queue_init (&priv->pending_tasks);
|
|
|
|
priv->target_usage = DEFAULT_TARGET_USAGE;
|
|
priv->low_latency = DEFAULT_LOW_LATENCY;
|
|
}
|
|
|
|
static void
|
|
gst_qsv_encoder_dispose (GObject * object)
|
|
{
|
|
GstQsvEncoder *self = GST_QSV_ENCODER (object);
|
|
GstQsvEncoderPrivate *priv = self->priv;
|
|
|
|
gst_clear_object (&priv->device);
|
|
|
|
G_OBJECT_CLASS (parent_class)->dispose (object);
|
|
}
|
|
|
|
static void
|
|
gst_qsv_encoder_finalize (GObject * object)
|
|
{
|
|
GstQsvEncoder *self = GST_QSV_ENCODER (object);
|
|
GstQsvEncoderPrivate *priv = self->priv;
|
|
|
|
g_ptr_array_unref (priv->extra_params);
|
|
g_array_unref (priv->task_pool);
|
|
g_array_unref (priv->surface_pool);
|
|
|
|
G_OBJECT_CLASS (parent_class)->finalize (object);
|
|
}
|
|
|
|
static void
|
|
gst_qsv_encoder_set_property (GObject * object, guint prop_id,
|
|
const GValue * value, GParamSpec * pspec)
|
|
{
|
|
GstQsvEncoder *self = GST_QSV_ENCODER (object);
|
|
GstQsvEncoderPrivate *priv = self->priv;
|
|
|
|
switch (prop_id) {
|
|
case PROP_TARGET_USAGE:
|
|
priv->target_usage = g_value_get_uint (value);
|
|
break;
|
|
case PROP_LOW_LATENCY:
|
|
priv->low_latency = g_value_get_boolean (value);
|
|
break;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
gst_qsv_encoder_get_property (GObject * object, guint prop_id, GValue * value,
|
|
GParamSpec * pspec)
|
|
{
|
|
GstQsvEncoder *self = GST_QSV_ENCODER (object);
|
|
GstQsvEncoderPrivate *priv = self->priv;
|
|
GstQsvEncoderClass *klass = GST_QSV_ENCODER_GET_CLASS (self);
|
|
|
|
switch (prop_id) {
|
|
case PROP_ADAPTER_LUID:
|
|
g_value_set_int64 (value, klass->adapter_luid);
|
|
break;
|
|
case PROP_DEVICE_PATH:
|
|
g_value_set_string (value, klass->display_path);
|
|
break;
|
|
case PROP_TARGET_USAGE:
|
|
g_value_set_uint (value, priv->target_usage);
|
|
break;
|
|
case PROP_LOW_LATENCY:
|
|
g_value_set_boolean (value, priv->low_latency);
|
|
break;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
gst_qsv_encoder_set_context (GstElement * element, GstContext * context)
|
|
{
|
|
GstQsvEncoder *self = GST_QSV_ENCODER (element);
|
|
GstQsvEncoderClass *klass = GST_QSV_ENCODER_GET_CLASS (element);
|
|
GstQsvEncoderPrivate *priv = self->priv;
|
|
|
|
#ifdef G_OS_WIN32
|
|
gst_d3d11_handle_set_context_for_adapter_luid (element,
|
|
context, klass->adapter_luid, (GstD3D11Device **) & priv->device);
|
|
#else
|
|
gst_va_handle_set_context (element, context, klass->display_path,
|
|
(GstVaDisplay **) & priv->device);
|
|
#endif
|
|
|
|
GST_ELEMENT_CLASS (parent_class)->set_context (element, context);
|
|
}
|
|
|
|
#ifdef G_OS_WIN32
|
|
static gboolean
|
|
gst_qsv_encoder_open_platform_device (GstQsvEncoder * self)
|
|
{
|
|
GstQsvEncoderPrivate *priv = self->priv;
|
|
GstQsvEncoderClass *klass = GST_QSV_ENCODER_GET_CLASS (self);
|
|
ComPtr < ID3D10Multithread > multi_thread;
|
|
HRESULT hr;
|
|
ID3D11Device *device_handle;
|
|
mfxStatus status;
|
|
GstD3D11Device *device;
|
|
|
|
if (!gst_d3d11_ensure_element_data_for_adapter_luid (GST_ELEMENT (self),
|
|
klass->adapter_luid, (GstD3D11Device **) & priv->device)) {
|
|
GST_ERROR_OBJECT (self, "d3d11 device is unavailable");
|
|
return FALSE;
|
|
}
|
|
|
|
device = GST_D3D11_DEVICE_CAST (priv->device);
|
|
priv->allocator = gst_qsv_d3d11_allocator_new (device);
|
|
|
|
/* For D3D11 device handle to be used by QSV, multithread protection layer
|
|
* must be enabled before the MFXVideoCORE_SetHandle() call.
|
|
*
|
|
* TODO: Need to check performance impact by this mutithread protection layer,
|
|
* since it may have a negative impact on overall pipeline performance.
|
|
* If so, we should create encoding session dedicated d3d11 device and
|
|
* make use of shared resource */
|
|
device_handle = gst_d3d11_device_get_device_handle (device);
|
|
hr = device_handle->QueryInterface (IID_PPV_ARGS (&multi_thread));
|
|
if (!gst_d3d11_result (hr, device)) {
|
|
GST_ERROR_OBJECT (self, "ID3D10Multithread interface is unavailable");
|
|
return FALSE;
|
|
}
|
|
|
|
multi_thread->SetMultithreadProtected (TRUE);
|
|
status = MFXVideoCORE_SetHandle (priv->session, MFX_HANDLE_D3D11_DEVICE,
|
|
device_handle);
|
|
if (status != MFX_ERR_NONE) {
|
|
GST_ERROR_OBJECT (self, "Failed to set d3d11 device handle");
|
|
return FALSE;
|
|
}
|
|
|
|
/* NOTE: We never use this mfxFrameAllocator to allocate memory from our side,
|
|
* but required for QSV because:
|
|
* 1) QSV may request memory allocation for encoder's internal usage,
|
|
* MFX_FOURCC_P8 for example
|
|
* 2) Our mfxFrameAllocator provides bridge layer for
|
|
* gst_video_frame_{map,unmap} and mfxFrameAllocator::{Lock,Unlock},
|
|
* including mfxFrameAllocator::GetHDL.
|
|
* 3) GstQsvAllocator provides GstQsvFrame pool, and therefore allocated
|
|
* GstQsvFrame struct can be re-used without per-frame malloc/free
|
|
*/
|
|
status = MFXVideoCORE_SetFrameAllocator (priv->session,
|
|
gst_qsv_allocator_get_allocator_handle (priv->allocator));
|
|
if (status != MFX_ERR_NONE) {
|
|
GST_ERROR_OBJECT (self, "Failed to set frame allocator %d", status);
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
#else
|
|
static gboolean
|
|
gst_qsv_encoder_open_platform_device (GstQsvEncoder * self)
|
|
{
|
|
GstQsvEncoderPrivate *priv = self->priv;
|
|
GstQsvEncoderClass *klass = GST_QSV_ENCODER_GET_CLASS (self);
|
|
mfxStatus status;
|
|
GstVaDisplay *display;
|
|
|
|
if (!gst_va_ensure_element_data (GST_ELEMENT (self), klass->display_path,
|
|
(GstVaDisplay **) & priv->device)) {
|
|
GST_ERROR_OBJECT (self, "VA display is unavailable");
|
|
return FALSE;
|
|
}
|
|
|
|
display = GST_VA_DISPLAY (priv->device);
|
|
|
|
priv->allocator = gst_qsv_va_allocator_new (display);
|
|
|
|
status = MFXVideoCORE_SetHandle (priv->session, MFX_HANDLE_VA_DISPLAY,
|
|
gst_va_display_get_va_dpy (display));
|
|
if (status != MFX_ERR_NONE) {
|
|
GST_ERROR_OBJECT (self, "Failed to set VA display handle");
|
|
return FALSE;
|
|
}
|
|
|
|
status = MFXVideoCORE_SetFrameAllocator (priv->session,
|
|
gst_qsv_allocator_get_allocator_handle (priv->allocator));
|
|
if (status != MFX_ERR_NONE) {
|
|
GST_ERROR_OBJECT (self, "Failed to set frame allocator %d", status);
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
#endif
|
|
|
|
static gboolean
|
|
gst_qsv_encoder_open (GstVideoEncoder * encoder)
|
|
{
|
|
GstQsvEncoder *self = GST_QSV_ENCODER (encoder);
|
|
GstQsvEncoderPrivate *priv = self->priv;
|
|
GstQsvEncoderClass *klass = GST_QSV_ENCODER_GET_CLASS (self);
|
|
mfxStatus status;
|
|
|
|
status = MFXCreateSession (gst_qsv_get_loader (), klass->impl_index,
|
|
&priv->session);
|
|
if (status != MFX_ERR_NONE) {
|
|
GST_ERROR_OBJECT (self, "Failed to create session");
|
|
return FALSE;
|
|
}
|
|
|
|
if (!gst_qsv_encoder_open_platform_device (self)) {
|
|
g_clear_pointer (&priv->session, MFXClose);
|
|
gst_clear_object (&priv->allocator);
|
|
gst_clear_object (&priv->device);
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
gst_qsv_encoder_reset (GstQsvEncoder * self)
|
|
{
|
|
GstQsvEncoderPrivate *priv = self->priv;
|
|
|
|
if (priv->encoder) {
|
|
delete priv->encoder;
|
|
priv->encoder = nullptr;
|
|
}
|
|
|
|
if (priv->internal_pool) {
|
|
gst_buffer_pool_set_active (priv->internal_pool, FALSE);
|
|
gst_clear_object (&priv->internal_pool);
|
|
}
|
|
|
|
g_array_set_size (priv->surface_pool, 0);
|
|
g_array_set_size (priv->task_pool, 0);
|
|
g_queue_clear (&priv->free_tasks);
|
|
g_queue_clear (&priv->pending_tasks);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
gst_qsv_encoder_stop (GstVideoEncoder * encoder)
|
|
{
|
|
GstQsvEncoder *self = GST_QSV_ENCODER (encoder);
|
|
GstQsvEncoderPrivate *priv = self->priv;
|
|
|
|
gst_qsv_encoder_reset (self);
|
|
g_clear_pointer (&priv->input_state, gst_video_codec_state_unref);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
gst_qsv_encoder_close (GstVideoEncoder * encoder)
|
|
{
|
|
GstQsvEncoder *self = GST_QSV_ENCODER (encoder);
|
|
GstQsvEncoderPrivate *priv = self->priv;
|
|
|
|
g_clear_pointer (&priv->session, MFXClose);
|
|
gst_clear_object (&priv->allocator);
|
|
gst_clear_object (&priv->device);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
gst_qsv_encoder_payload_clear (mfxPayload * payload)
|
|
{
|
|
if (!payload)
|
|
return;
|
|
|
|
g_free (payload->Data);
|
|
g_free (payload);
|
|
}
|
|
|
|
static void
|
|
gst_qsv_encoder_surface_reset (GstQsvEncoderSurface * surface)
|
|
{
|
|
if (!surface)
|
|
return;
|
|
|
|
gst_clear_qsv_frame (&surface->qsv_frame);
|
|
g_ptr_array_set_size (surface->payload, 0);
|
|
memset (&surface->encode_control, 0, sizeof (mfxEncodeCtrl));
|
|
}
|
|
|
|
static void
|
|
gst_qsv_encoder_surface_clear (GstQsvEncoderSurface * surface)
|
|
{
|
|
if (!surface)
|
|
return;
|
|
|
|
gst_qsv_encoder_surface_reset (surface);
|
|
g_clear_pointer (&surface->payload, g_ptr_array_unref);
|
|
memset (&surface->surface, 0, sizeof (mfxFrameSurface1));
|
|
}
|
|
|
|
static void
|
|
gst_qsv_encoder_task_reset (GstQsvEncoder * self, GstQsvEncoderTask * task)
|
|
{
|
|
GstQsvEncoderPrivate *priv = self->priv;
|
|
|
|
if (!task)
|
|
return;
|
|
|
|
task->sync_point = nullptr;
|
|
task->bitstream.DataLength = 0;
|
|
g_queue_push_head (&priv->free_tasks, task);
|
|
}
|
|
|
|
static void
|
|
gst_qsv_encoder_task_clear (GstQsvEncoderTask * task)
|
|
{
|
|
if (!task)
|
|
return;
|
|
|
|
g_clear_pointer (&task->bitstream.Data, g_free);
|
|
memset (&task->bitstream, 0, sizeof (mfxBitstream));
|
|
}
|
|
|
|
static GstQsvEncoderSurface *
|
|
gst_qsv_encoder_get_next_surface (GstQsvEncoder * self)
|
|
{
|
|
GstQsvEncoderPrivate *priv = self->priv;
|
|
GstQsvEncoderSurface *surface = nullptr;
|
|
|
|
for (guint i = priv->next_surface_index; i < priv->surface_pool->len; i++) {
|
|
GstQsvEncoderSurface *iter =
|
|
&g_array_index (priv->surface_pool, GstQsvEncoderSurface, i);
|
|
|
|
/* This means surface is still being used by QSV */
|
|
if (iter->surface.Data.Locked > 0)
|
|
continue;
|
|
|
|
surface = iter;
|
|
priv->next_surface_index = i;
|
|
goto out;
|
|
}
|
|
|
|
for (guint i = 0; i < priv->next_surface_index; i++) {
|
|
GstQsvEncoderSurface *iter =
|
|
&g_array_index (priv->surface_pool, GstQsvEncoderSurface, i);
|
|
|
|
/* This means surface is still being used by QSV */
|
|
if (iter->surface.Data.Locked > 0)
|
|
continue;
|
|
|
|
surface = iter;
|
|
priv->next_surface_index = i;
|
|
goto out;
|
|
}
|
|
|
|
/* Magic number to avoid too large pool size */
|
|
if (priv->surface_pool->len > 64) {
|
|
GST_ERROR_OBJECT (self,
|
|
"No availble surface but pool size is too large already");
|
|
return nullptr;
|
|
}
|
|
|
|
/* Something went wrong, increase surface pool size */
|
|
GST_INFO_OBJECT (self, "No useable surfaces, increasing pool size to %d",
|
|
priv->surface_pool->len + 1);
|
|
|
|
g_array_set_size (priv->surface_pool, priv->surface_pool->len + 1);
|
|
surface = &g_array_index (priv->surface_pool, GstQsvEncoderSurface,
|
|
priv->surface_pool->len - 1);
|
|
|
|
memset (surface, 0, sizeof (GstQsvEncoderSurface));
|
|
surface->surface.Info =
|
|
g_array_index (priv->surface_pool, GstQsvEncoderSurface, 0).surface.Info;
|
|
surface->payload = g_ptr_array_new_with_free_func ((GDestroyNotify)
|
|
gst_qsv_encoder_payload_clear);
|
|
|
|
out:
|
|
priv->next_surface_index++;
|
|
priv->next_surface_index %= priv->surface_pool->len;
|
|
|
|
gst_qsv_encoder_surface_reset (surface);
|
|
return surface;
|
|
}
|
|
|
|
static mfxStatus
|
|
gst_qsv_encoder_encode_frame (GstQsvEncoder * self,
|
|
GstQsvEncoderSurface * surface, GstQsvEncoderTask * task, mfxU64 timestamp)
|
|
{
|
|
mfxFrameSurface1 *s;
|
|
GstQsvEncoderPrivate *priv = self->priv;
|
|
mfxStatus status;
|
|
guint retry_count = 0;
|
|
/* magic number */
|
|
const guint retry_threshold = 100;
|
|
mfxEncodeCtrl *encode_ctrl;
|
|
|
|
if (surface) {
|
|
s = &surface->surface;
|
|
s->Data.MemId = (mfxMemId) surface->qsv_frame;
|
|
s->Data.TimeStamp = timestamp;
|
|
encode_ctrl = &surface->encode_control;
|
|
} else {
|
|
/* draining */
|
|
s = nullptr;
|
|
encode_ctrl = nullptr;
|
|
}
|
|
|
|
do {
|
|
status = priv->encoder->EncodeFrameAsync (encode_ctrl,
|
|
s, &task->bitstream, &task->sync_point);
|
|
|
|
/* XXX: probably we should try to drain pending tasks if any in this case
|
|
* as documented? */
|
|
if (status == MFX_WRN_DEVICE_BUSY && retry_count < retry_threshold) {
|
|
GST_INFO_OBJECT (self, "GPU is busy, retry count (%d/%d)",
|
|
retry_count, retry_threshold);
|
|
retry_count++;
|
|
|
|
/* Magic number 10ms */
|
|
g_usleep (10000);
|
|
continue;
|
|
}
|
|
|
|
break;
|
|
} while (TRUE);
|
|
|
|
return status;
|
|
}
|
|
|
|
static GstVideoCodecFrame *
|
|
gst_qsv_encoder_find_output_frame (GstQsvEncoder * self, GstClockTime pts)
|
|
{
|
|
GList *frames, *iter;
|
|
GstVideoCodecFrame *ret = nullptr;
|
|
GstVideoCodecFrame *closest = nullptr;
|
|
guint64 min_pts_abs_diff = 0;
|
|
|
|
/* give up, just returns the oldest frame */
|
|
if (!GST_CLOCK_TIME_IS_VALID (pts))
|
|
return gst_video_encoder_get_oldest_frame (GST_VIDEO_ENCODER (self));
|
|
|
|
frames = gst_video_encoder_get_frames (GST_VIDEO_ENCODER (self));
|
|
|
|
for (iter = frames; iter; iter = g_list_next (iter)) {
|
|
GstVideoCodecFrame *frame = (GstVideoCodecFrame *) iter->data;
|
|
guint64 abs_diff;
|
|
|
|
if (!GST_CLOCK_TIME_IS_VALID (frame->pts))
|
|
continue;
|
|
|
|
if (pts == frame->pts) {
|
|
ret = frame;
|
|
break;
|
|
}
|
|
|
|
if (pts >= frame->pts)
|
|
abs_diff = pts - frame->pts;
|
|
else
|
|
abs_diff = frame->pts - pts;
|
|
|
|
if (!closest || abs_diff < min_pts_abs_diff) {
|
|
closest = frame;
|
|
min_pts_abs_diff = abs_diff;
|
|
}
|
|
}
|
|
|
|
if (!ret && closest)
|
|
ret = closest;
|
|
|
|
if (ret) {
|
|
gst_video_codec_frame_ref (ret);
|
|
} else {
|
|
ret = gst_video_encoder_get_oldest_frame (GST_VIDEO_ENCODER (self));
|
|
}
|
|
|
|
if (frames)
|
|
g_list_free_full (frames, (GDestroyNotify) gst_video_codec_frame_unref);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static GstFlowReturn
|
|
gst_qsv_encoder_finish_frame (GstQsvEncoder * self, GstQsvEncoderTask * task,
|
|
gboolean discard)
|
|
{
|
|
GstQsvEncoderPrivate *priv = self->priv;
|
|
GstQsvEncoderClass *klass = GST_QSV_ENCODER_GET_CLASS (self);
|
|
mfxStatus status;
|
|
mfxBitstream *bs;
|
|
GstVideoCodecFrame *frame;
|
|
GstClockTime qsv_pts = GST_CLOCK_TIME_NONE;
|
|
GstClockTime qsv_dts = GST_CLOCK_TIME_NONE;
|
|
GstBuffer *buffer;
|
|
gboolean keyframe = FALSE;
|
|
guint retry_count = 0;
|
|
/* magic number */
|
|
const guint retry_threshold = 100;
|
|
|
|
status = MFX_ERR_NONE;
|
|
do {
|
|
/* magic number 100 ms */
|
|
status = MFXVideoCORE_SyncOperation (priv->session, task->sync_point, 100);
|
|
|
|
/* Retry up to 10 sec (100 ms x 100 times), that should be enough time for
|
|
* encoding a frame using hardware */
|
|
if (status == MFX_WRN_IN_EXECUTION && retry_count < retry_threshold) {
|
|
GST_DEBUG_OBJECT (self,
|
|
"Operation is still in execution, retry count (%d/%d)",
|
|
retry_count, retry_threshold);
|
|
retry_count++;
|
|
continue;
|
|
}
|
|
|
|
break;
|
|
} while (TRUE);
|
|
|
|
if (discard) {
|
|
gst_qsv_encoder_task_reset (self, task);
|
|
return GST_FLOW_OK;
|
|
}
|
|
|
|
if (status != MFX_ERR_NONE && status != MFX_ERR_NONE_PARTIAL_OUTPUT) {
|
|
gst_qsv_encoder_task_reset (self, task);
|
|
|
|
if (status == MFX_ERR_ABORTED) {
|
|
GST_INFO_OBJECT (self, "Operation was aborted");
|
|
return GST_FLOW_FLUSHING;
|
|
}
|
|
|
|
GST_WARNING_OBJECT (self, "SyncOperation returned %d (%s)",
|
|
QSV_STATUS_ARGS (status));
|
|
|
|
return GST_FLOW_ERROR;
|
|
}
|
|
|
|
bs = &task->bitstream;
|
|
qsv_pts = gst_qsv_timestamp_to_gst (bs->TimeStamp);
|
|
|
|
/* SDK runtime seems to report zero DTS for all fraems in case of VP9.
|
|
* It sounds SDK bug, but we can workaround it safely because VP9 B-frame is
|
|
* not supported in this implementation.
|
|
*
|
|
* Also we perfer our nanoseconds timestamp instead of QSV's timescale.
|
|
* So let' ignore QSV's timescale for non-{h264,h265} cases.
|
|
*
|
|
* TODO: We may need to use DTS for MPEG2 (not implemented yet)
|
|
*/
|
|
if (klass->codec_id == MFX_CODEC_AVC || klass->codec_id == MFX_CODEC_HEVC)
|
|
qsv_dts = gst_qsv_timestamp_to_gst ((mfxU64) bs->DecodeTimeStamp);
|
|
|
|
if ((bs->FrameType & MFX_FRAMETYPE_IDR) != 0)
|
|
keyframe = TRUE;
|
|
|
|
if (klass->create_output_buffer) {
|
|
buffer = klass->create_output_buffer (self, bs);
|
|
} else {
|
|
buffer = gst_buffer_new_memdup (bs->Data + bs->DataOffset, bs->DataLength);
|
|
}
|
|
gst_qsv_encoder_task_reset (self, task);
|
|
|
|
if (!buffer) {
|
|
GST_ERROR_OBJECT (self, "No output buffer");
|
|
return GST_FLOW_ERROR;
|
|
}
|
|
|
|
frame = gst_qsv_encoder_find_output_frame (self, qsv_pts);
|
|
if (frame) {
|
|
if (GST_CLOCK_TIME_IS_VALID (qsv_dts)) {
|
|
frame->pts = qsv_pts;
|
|
frame->dts = qsv_dts;
|
|
} else {
|
|
frame->dts = frame->pts;
|
|
}
|
|
|
|
frame->output_buffer = buffer;
|
|
|
|
if (keyframe)
|
|
GST_VIDEO_CODEC_FRAME_SET_SYNC_POINT (frame);
|
|
|
|
return gst_video_encoder_finish_frame (GST_VIDEO_ENCODER (self), frame);
|
|
}
|
|
|
|
/* Empty available frame, something went wrong but we can just push this
|
|
* buffer */
|
|
GST_WARNING_OBJECT (self, "Failed to find corresponding frame");
|
|
GST_BUFFER_PTS (buffer) = qsv_pts;
|
|
if (GST_CLOCK_TIME_IS_VALID (qsv_dts))
|
|
GST_BUFFER_DTS (buffer) = qsv_dts;
|
|
else
|
|
GST_BUFFER_DTS (buffer) = qsv_pts;
|
|
|
|
if (!keyframe)
|
|
GST_BUFFER_FLAG_SET (buffer, GST_BUFFER_FLAG_DELTA_UNIT);
|
|
|
|
return gst_pad_push (GST_VIDEO_ENCODER_SRC_PAD (self), buffer);
|
|
}
|
|
|
|
static GstFlowReturn
|
|
gst_qsv_encoder_drain (GstQsvEncoder * self, gboolean discard)
|
|
{
|
|
GstQsvEncoderPrivate *priv = self->priv;
|
|
mfxStatus status = MFX_ERR_NONE;
|
|
GstFlowReturn ret = GST_FLOW_OK;
|
|
GstQsvEncoderTask *task;
|
|
|
|
if (!priv->session || !priv->encoder)
|
|
return GST_FLOW_OK;
|
|
|
|
GST_DEBUG_OBJECT (self, "Drain");
|
|
|
|
/* Drain pending tasks first if any */
|
|
while (g_queue_get_length (&priv->pending_tasks) > 0) {
|
|
task = (GstQsvEncoderTask *) g_queue_pop_tail (&priv->pending_tasks);
|
|
ret = gst_qsv_encoder_finish_frame (self, task, discard);
|
|
}
|
|
|
|
while (status == MFX_ERR_NONE) {
|
|
task = (GstQsvEncoderTask *) g_queue_pop_tail (&priv->free_tasks);
|
|
status = gst_qsv_encoder_encode_frame (self,
|
|
nullptr, task, MFX_TIMESTAMP_UNKNOWN);
|
|
|
|
/* once it's fully drained, then driver will return more data */
|
|
if (status == MFX_ERR_NONE && task->sync_point) {
|
|
ret = gst_qsv_encoder_finish_frame (self, task, discard);
|
|
continue;
|
|
}
|
|
|
|
if (status != MFX_ERR_MORE_DATA)
|
|
GST_WARNING_OBJECT (self, "Unexpected status return %d (%s)",
|
|
QSV_STATUS_ARGS (status));
|
|
|
|
g_queue_push_head (&priv->free_tasks, task);
|
|
}
|
|
|
|
/* Release GstQsvFrame objects */
|
|
for (guint i = 0; i < priv->surface_pool->len; i++) {
|
|
GstQsvEncoderSurface *iter =
|
|
&g_array_index (priv->surface_pool, GstQsvEncoderSurface, i);
|
|
|
|
if (iter->surface.Data.Locked > 0) {
|
|
GST_WARNING_OBJECT (self,
|
|
"Encoder was drained but QSV is holding surface %d", i);
|
|
continue;
|
|
}
|
|
|
|
gst_qsv_encoder_surface_reset (iter);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
#ifdef G_OS_WIN32
|
|
static gboolean
|
|
gst_qsv_encoder_prepare_d3d11_pool (GstQsvEncoder * self,
|
|
GstCaps * caps, GstVideoInfo * aligned_info)
|
|
{
|
|
GstQsvEncoderPrivate *priv = self->priv;
|
|
GstStructure *config;
|
|
GstD3D11AllocationParams *params;
|
|
GstD3D11Device *device = GST_D3D11_DEVICE_CAST (priv->device);
|
|
guint bind_flags = 0;
|
|
GstD3D11Format device_format;
|
|
|
|
gst_d3d11_device_get_format (device, GST_VIDEO_INFO_FORMAT (aligned_info),
|
|
&device_format);
|
|
if ((device_format.format_support[0] & D3D11_FORMAT_SUPPORT_RENDER_TARGET) ==
|
|
D3D11_FORMAT_SUPPORT_RENDER_TARGET) {
|
|
/* XXX: workaround for greenish artifacts
|
|
* https://gitlab.freedesktop.org/gstreamer/gstreamer/-/issues/1238
|
|
* bind to render target so that d3d11 memory allocator can clear texture
|
|
* with black color */
|
|
bind_flags = D3D11_BIND_RENDER_TARGET;
|
|
}
|
|
|
|
priv->internal_pool = gst_d3d11_buffer_pool_new (device);
|
|
config = gst_buffer_pool_get_config (priv->internal_pool);
|
|
params = gst_d3d11_allocation_params_new (device, aligned_info,
|
|
GST_D3D11_ALLOCATION_FLAG_DEFAULT, bind_flags,
|
|
D3D11_RESOURCE_MISC_SHARED);
|
|
|
|
gst_buffer_pool_config_set_d3d11_allocation_params (config, params);
|
|
gst_d3d11_allocation_params_free (params);
|
|
gst_buffer_pool_config_set_params (config, caps,
|
|
GST_VIDEO_INFO_SIZE (aligned_info), 0, 0);
|
|
gst_buffer_pool_set_config (priv->internal_pool, config);
|
|
gst_buffer_pool_set_active (priv->internal_pool, TRUE);
|
|
|
|
return TRUE;
|
|
}
|
|
#else
|
|
static gboolean
|
|
gst_qsv_encoder_prepare_va_pool (GstQsvEncoder * self,
|
|
GstCaps * caps, GstVideoInfo * aligned_info)
|
|
{
|
|
GstQsvEncoderPrivate *priv = self->priv;
|
|
GstAllocator *allocator;
|
|
GstStructure *config;
|
|
GArray *formats;
|
|
GstAllocationParams params;
|
|
GstVaDisplay *display = GST_VA_DISPLAY (priv->device);
|
|
|
|
formats = g_array_new (FALSE, FALSE, sizeof (GstVideoFormat));
|
|
g_array_append_val (formats, GST_VIDEO_INFO_FORMAT (aligned_info));
|
|
|
|
allocator = gst_va_allocator_new (display, formats);
|
|
if (!allocator) {
|
|
GST_ERROR_OBJECT (self, "Failed to create allocator");
|
|
return FALSE;
|
|
}
|
|
|
|
gst_allocation_params_init (¶ms);
|
|
|
|
priv->internal_pool = gst_va_pool_new_with_config (caps,
|
|
GST_VIDEO_INFO_SIZE (aligned_info), 0, 0,
|
|
VA_SURFACE_ATTRIB_USAGE_HINT_GENERIC, GST_VA_FEATURE_AUTO,
|
|
allocator, ¶ms);
|
|
gst_object_unref (allocator);
|
|
|
|
|
|
if (!priv->internal_pool) {
|
|
GST_ERROR_OBJECT (self, "Failed to create va pool");
|
|
return FALSE;
|
|
}
|
|
|
|
config = gst_buffer_pool_get_config (priv->internal_pool);
|
|
gst_buffer_pool_config_add_option (config, GST_BUFFER_POOL_OPTION_VIDEO_META);
|
|
gst_buffer_pool_config_set_params (config, caps,
|
|
GST_VIDEO_INFO_SIZE (aligned_info), 0, 0);
|
|
gst_buffer_pool_set_config (priv->internal_pool, config);
|
|
gst_buffer_pool_set_active (priv->internal_pool, TRUE);
|
|
|
|
return TRUE;
|
|
}
|
|
#endif
|
|
|
|
/* Prepare internal pool, which is used to allocate fallback buffer
|
|
* when upstream buffer is not directly accessible by QSV */
|
|
static gboolean
|
|
gst_qsv_encoder_prepare_pool (GstQsvEncoder * self, GstCaps * caps,
|
|
GstVideoInfo * aligned_info)
|
|
{
|
|
GstQsvEncoderPrivate *priv = self->priv;
|
|
gboolean ret = FALSE;
|
|
GstCaps *aligned_caps;
|
|
|
|
if (priv->internal_pool) {
|
|
gst_buffer_pool_set_active (priv->internal_pool, FALSE);
|
|
gst_clear_object (&priv->internal_pool);
|
|
}
|
|
|
|
aligned_caps = gst_video_info_to_caps (aligned_info);
|
|
|
|
#ifdef G_OS_WIN32
|
|
ret = gst_qsv_encoder_prepare_d3d11_pool (self, aligned_caps, aligned_info);
|
|
#else
|
|
ret = gst_qsv_encoder_prepare_va_pool (self, aligned_caps, aligned_info);
|
|
#endif
|
|
|
|
gst_caps_unref (aligned_caps);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static gboolean
|
|
gst_qsv_encoder_init_encode_session (GstQsvEncoder * self)
|
|
{
|
|
GstQsvEncoderPrivate *priv = self->priv;
|
|
GstQsvEncoderClass *klass = GST_QSV_ENCODER_GET_CLASS (self);
|
|
GstVideoInfo *info = &priv->input_state->info;
|
|
GstCaps *caps = priv->input_state->caps;
|
|
mfxVideoParam param;
|
|
mfxFrameInfo *frame_info;
|
|
mfxFrameAllocRequest alloc_request;
|
|
mfxStatus status;
|
|
MFXVideoENCODE *encoder_handle = nullptr;
|
|
guint bitstream_size;
|
|
gboolean ret;
|
|
guint64 min_delay_frames, max_delay_frames;
|
|
GstClockTime min_latency, max_latency;
|
|
|
|
gst_qsv_encoder_drain (self, FALSE);
|
|
gst_qsv_encoder_reset (self);
|
|
|
|
encoder_handle = new MFXVideoENCODE (priv->session);
|
|
|
|
memset (¶m, 0, sizeof (mfxVideoParam));
|
|
|
|
g_ptr_array_set_size (priv->extra_params, 0);
|
|
g_assert (klass->set_format);
|
|
if (!klass->set_format (self, priv->input_state, ¶m, priv->extra_params)) {
|
|
GST_ERROR_OBJECT (self, "Subclass failed to set format");
|
|
goto error;
|
|
}
|
|
|
|
/* LowPower mode supports smaller set of features, don't enable it for now */
|
|
param.mfx.LowPower = MFX_CODINGOPTION_OFF;
|
|
if (priv->low_latency)
|
|
param.AsyncDepth = 1;
|
|
else
|
|
param.AsyncDepth = 4;
|
|
|
|
param.mfx.TargetUsage = priv->target_usage;
|
|
|
|
frame_info = ¶m.mfx.FrameInfo;
|
|
|
|
gst_video_info_set_interlaced_format (&priv->aligned_info,
|
|
GST_VIDEO_INFO_FORMAT (info), GST_VIDEO_INFO_INTERLACE_MODE (info),
|
|
frame_info->Width, frame_info->Height);
|
|
|
|
/* Always video memory, even when upstream is non-hardware element */
|
|
priv->mem_type = GST_QSV_VIDEO_MEMORY | GST_QSV_ENCODER_IN_MEMORY;
|
|
param.IOPattern = MFX_IOPATTERN_IN_VIDEO_MEMORY;
|
|
if (!gst_qsv_encoder_prepare_pool (self, caps, &priv->aligned_info)) {
|
|
GST_ERROR_OBJECT (self, "Failed to prepare pool");
|
|
goto error;
|
|
}
|
|
|
|
status = encoder_handle->Query (¶m, ¶m);
|
|
/* If device is unhappy with LowPower = OFF, try again with unknown */
|
|
if (status < MFX_ERR_NONE) {
|
|
GST_INFO_OBJECT (self, "LowPower - OFF returned %d (%s)",
|
|
QSV_STATUS_ARGS (status));
|
|
param.mfx.LowPower = MFX_CODINGOPTION_UNKNOWN;
|
|
status = encoder_handle->Query (¶m, ¶m);
|
|
}
|
|
QSV_CHECK_STATUS (self, status, MFXVideoENCODE::Query);
|
|
|
|
status = encoder_handle->QueryIOSurf (¶m, &alloc_request);
|
|
QSV_CHECK_STATUS (self, status, MFXVideoENCODE::QueryIOSurf);
|
|
|
|
status = encoder_handle->Init (¶m);
|
|
QSV_CHECK_STATUS (self, status, MFXVideoENCODE::Init);
|
|
|
|
status = encoder_handle->GetVideoParam (¶m);
|
|
QSV_CHECK_STATUS (self, status, MFXVideoENCODE::GetVideoParam);
|
|
|
|
GST_DEBUG_OBJECT (self, "NumFrameSuggested: %d, AsyncDepth %d",
|
|
alloc_request.NumFrameSuggested, param.AsyncDepth);
|
|
|
|
g_assert (klass->set_output_state);
|
|
ret = klass->set_output_state (self, priv->input_state, priv->session);
|
|
if (!ret) {
|
|
GST_ERROR_OBJECT (self, "Subclass failed to set output state");
|
|
goto error;
|
|
}
|
|
|
|
/* Prepare surface pool with size NumFrameSuggested, then if it's not
|
|
* sufficient while encoding, we can increse the pool size dynamically
|
|
* if needed */
|
|
g_array_set_size (priv->surface_pool, alloc_request.NumFrameSuggested);
|
|
for (guint i = 0; i < priv->surface_pool->len; i++) {
|
|
GstQsvEncoderSurface *surface = &g_array_index (priv->surface_pool,
|
|
GstQsvEncoderSurface, i);
|
|
|
|
surface->surface.Info = param.mfx.FrameInfo;
|
|
surface->payload = g_ptr_array_new_with_free_func ((GDestroyNotify)
|
|
gst_qsv_encoder_payload_clear);
|
|
}
|
|
priv->next_surface_index = 0;
|
|
|
|
g_array_set_size (priv->task_pool, param.AsyncDepth);
|
|
if (klass->codec_id == MFX_CODEC_JPEG) {
|
|
gdouble factor = 4.0;
|
|
|
|
/* jpeg zero returns buffer size */
|
|
switch (GST_VIDEO_INFO_FORMAT (info)) {
|
|
case GST_VIDEO_FORMAT_NV12:
|
|
factor = 1.5;
|
|
break;
|
|
case GST_VIDEO_FORMAT_YUY2:
|
|
factor = 2.0;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
bitstream_size = (guint)
|
|
(factor * GST_VIDEO_INFO_WIDTH (info) * GST_VIDEO_INFO_HEIGHT (info));
|
|
} else {
|
|
bitstream_size =
|
|
(guint) param.mfx.BufferSizeInKB * param.mfx.BRCParamMultiplier * 1024;
|
|
}
|
|
|
|
for (guint i = 0; i < priv->task_pool->len; i++) {
|
|
GstQsvEncoderTask *task = &g_array_index (priv->task_pool,
|
|
GstQsvEncoderTask, i);
|
|
|
|
task->bitstream.Data = (mfxU8 *) g_malloc (bitstream_size);
|
|
task->bitstream.MaxLength = bitstream_size;
|
|
|
|
g_queue_push_head (&priv->free_tasks, task);
|
|
}
|
|
|
|
min_delay_frames = priv->task_pool->len;
|
|
/* takes the number of bframes into account */
|
|
if (param.mfx.GopRefDist > 1)
|
|
min_delay_frames += (param.mfx.GopRefDist - 1);
|
|
max_delay_frames = priv->surface_pool->len + priv->task_pool->len;
|
|
|
|
min_latency = gst_util_uint64_scale (min_delay_frames * GST_SECOND,
|
|
param.mfx.FrameInfo.FrameRateExtD, param.mfx.FrameInfo.FrameRateExtN);
|
|
max_latency = gst_util_uint64_scale (max_delay_frames * GST_SECOND,
|
|
param.mfx.FrameInfo.FrameRateExtD, param.mfx.FrameInfo.FrameRateExtN);
|
|
gst_video_encoder_set_latency (GST_VIDEO_ENCODER (self),
|
|
min_latency, max_latency);
|
|
|
|
priv->video_param = param;
|
|
priv->encoder = encoder_handle;
|
|
|
|
return TRUE;
|
|
|
|
error:
|
|
if (encoder_handle)
|
|
delete encoder_handle;
|
|
|
|
gst_qsv_encoder_reset (self);
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static gboolean
|
|
gst_qsv_encoder_reset_encode_session (GstQsvEncoder * self)
|
|
{
|
|
GstQsvEncoderPrivate *priv = self->priv;
|
|
GPtrArray *extra_params = priv->extra_params;
|
|
mfxStatus status;
|
|
mfxExtEncoderResetOption reset_opt;
|
|
|
|
if (!priv->encoder) {
|
|
GST_WARNING_OBJECT (self, "Encoder was not configured");
|
|
return gst_qsv_encoder_init_encode_session (self);
|
|
}
|
|
|
|
reset_opt.Header.BufferId = MFX_EXTBUFF_ENCODER_RESET_OPTION;
|
|
reset_opt.Header.BufferSz = sizeof (mfxExtEncoderResetOption);
|
|
reset_opt.StartNewSequence = MFX_CODINGOPTION_OFF;
|
|
|
|
gst_qsv_encoder_drain (self, FALSE);
|
|
|
|
g_ptr_array_add (extra_params, &reset_opt);
|
|
priv->video_param.ExtParam = (mfxExtBuffer **) extra_params->pdata;
|
|
priv->video_param.NumExtParam = extra_params->len;
|
|
|
|
status = priv->encoder->Reset (&priv->video_param);
|
|
g_ptr_array_remove_index (extra_params, extra_params->len - 1);
|
|
priv->video_param.NumExtParam = extra_params->len;
|
|
|
|
if (status != MFX_ERR_NONE) {
|
|
GST_WARNING_OBJECT (self, "MFXVideoENCODE_Reset returned %d (%s)",
|
|
QSV_STATUS_ARGS (status));
|
|
return gst_qsv_encoder_init_encode_session (self);
|
|
}
|
|
|
|
GST_DEBUG_OBJECT (self, "Encode session reset done");
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
gst_qsv_encoder_set_format (GstVideoEncoder * encoder,
|
|
GstVideoCodecState * state)
|
|
{
|
|
GstQsvEncoder *self = GST_QSV_ENCODER (encoder);
|
|
GstQsvEncoderPrivate *priv = self->priv;
|
|
|
|
g_clear_pointer (&priv->input_state, gst_video_codec_state_unref);
|
|
priv->input_state = gst_video_codec_state_ref (state);
|
|
|
|
return gst_qsv_encoder_init_encode_session (self);
|
|
}
|
|
|
|
static mfxU16
|
|
gst_qsv_encoder_get_pic_struct (GstQsvEncoder * self,
|
|
GstVideoCodecFrame * frame)
|
|
{
|
|
GstQsvEncoderClass *klass = GST_QSV_ENCODER_GET_CLASS (self);
|
|
GstQsvEncoderPrivate *priv = self->priv;
|
|
GstVideoInfo *info = &priv->input_state->info;
|
|
|
|
if (klass->codec_id != MFX_CODEC_AVC)
|
|
return MFX_PICSTRUCT_PROGRESSIVE;
|
|
|
|
if (!GST_VIDEO_INFO_IS_INTERLACED (info))
|
|
return MFX_PICSTRUCT_PROGRESSIVE;
|
|
|
|
if (GST_VIDEO_INFO_INTERLACE_MODE (info) == GST_VIDEO_INTERLACE_MODE_MIXED) {
|
|
if (!GST_BUFFER_FLAG_IS_SET (frame->input_buffer,
|
|
GST_VIDEO_BUFFER_FLAG_INTERLACED)) {
|
|
return MFX_PICSTRUCT_PROGRESSIVE;
|
|
}
|
|
|
|
if (GST_BUFFER_FLAG_IS_SET (frame->input_buffer, GST_VIDEO_BUFFER_FLAG_TFF))
|
|
return MFX_PICSTRUCT_FIELD_TFF;
|
|
|
|
return MFX_PICSTRUCT_FIELD_BFF;
|
|
}
|
|
|
|
switch (GST_VIDEO_INFO_FIELD_ORDER (info)) {
|
|
case GST_VIDEO_FIELD_ORDER_TOP_FIELD_FIRST:
|
|
return MFX_PICSTRUCT_FIELD_TFF;
|
|
break;
|
|
case GST_VIDEO_FIELD_ORDER_BOTTOM_FIELD_FIRST:
|
|
return MFX_PICSTRUCT_FIELD_BFF;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (GST_BUFFER_FLAG_IS_SET (frame->input_buffer, GST_VIDEO_BUFFER_FLAG_TFF))
|
|
return MFX_PICSTRUCT_FIELD_TFF;
|
|
|
|
return MFX_PICSTRUCT_FIELD_BFF;
|
|
}
|
|
|
|
static GstFlowReturn
|
|
gst_qsv_encoder_handle_frame (GstVideoEncoder * encoder,
|
|
GstVideoCodecFrame * frame)
|
|
{
|
|
GstQsvEncoder *self = GST_QSV_ENCODER (encoder);
|
|
GstQsvEncoderPrivate *priv = self->priv;
|
|
GstQsvEncoderClass *klass = GST_QSV_ENCODER_GET_CLASS (self);
|
|
GstFlowReturn ret = GST_FLOW_ERROR;
|
|
GstQsvEncoderSurface *surface;
|
|
GstQsvEncoderTask *task;
|
|
mfxU64 timestamp;
|
|
mfxStatus status;
|
|
|
|
if (klass->check_reconfigure && priv->encoder) {
|
|
GstQsvEncoderReconfigure reconfigure;
|
|
|
|
reconfigure = klass->check_reconfigure (self, priv->session,
|
|
&priv->video_param, priv->extra_params);
|
|
|
|
switch (reconfigure) {
|
|
case GST_QSV_ENCODER_RECONFIGURE_BITRATE:
|
|
if (!gst_qsv_encoder_reset_encode_session (self)) {
|
|
GST_ERROR_OBJECT (self, "Failed to reset session");
|
|
gst_video_encoder_finish_frame (encoder, frame);
|
|
|
|
return GST_FLOW_ERROR;
|
|
}
|
|
break;
|
|
case GST_QSV_ENCODER_RECONFIGURE_FULL:
|
|
if (!gst_qsv_encoder_init_encode_session (self)) {
|
|
GST_ERROR_OBJECT (self, "Failed to init session");
|
|
gst_video_encoder_finish_frame (encoder, frame);
|
|
|
|
return GST_FLOW_ERROR;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!priv->encoder) {
|
|
GST_ERROR_OBJECT (self, "Encoder object was not configured");
|
|
gst_video_encoder_finish_frame (encoder, frame);
|
|
|
|
return GST_FLOW_NOT_NEGOTIATED;
|
|
}
|
|
|
|
surface = gst_qsv_encoder_get_next_surface (self);
|
|
if (!surface) {
|
|
GST_ERROR_OBJECT (self, "No available surface");
|
|
goto out;
|
|
}
|
|
|
|
task = (GstQsvEncoderTask *) g_queue_pop_tail (&priv->free_tasks);
|
|
g_assert (task);
|
|
|
|
surface->qsv_frame =
|
|
gst_qsv_allocator_acquire_frame (priv->allocator, priv->mem_type,
|
|
&priv->input_state->info, gst_buffer_ref (frame->input_buffer),
|
|
priv->internal_pool);
|
|
|
|
if (!surface->qsv_frame) {
|
|
GST_ERROR_OBJECT (self, "Failed to wrap buffer with qsv frame");
|
|
gst_qsv_encoder_task_reset (self, task);
|
|
goto out;
|
|
}
|
|
|
|
surface->surface.Info.PicStruct =
|
|
gst_qsv_encoder_get_pic_struct (self, frame);
|
|
|
|
if (GST_VIDEO_CODEC_FRAME_IS_FORCE_KEYFRAME (frame)) {
|
|
surface->encode_control.FrameType =
|
|
MFX_FRAMETYPE_IDR | MFX_FRAMETYPE_I | MFX_FRAMETYPE_REF;
|
|
} else {
|
|
surface->encode_control.FrameType = MFX_FRAMETYPE_UNKNOWN;
|
|
}
|
|
|
|
if (klass->attach_payload) {
|
|
klass->attach_payload (self, frame, surface->payload);
|
|
if (surface->payload->len > 0) {
|
|
surface->encode_control.NumPayload = surface->payload->len;
|
|
surface->encode_control.Payload = (mfxPayload **) surface->payload->pdata;
|
|
}
|
|
}
|
|
|
|
timestamp = gst_qsv_timestamp_from_gst (frame->pts);
|
|
status = gst_qsv_encoder_encode_frame (self, surface, task, timestamp);
|
|
if (status != MFX_ERR_NONE && status != MFX_ERR_MORE_DATA) {
|
|
GST_ERROR_OBJECT (self, "Failed to encode frame, ret %d (%s)",
|
|
QSV_STATUS_ARGS (status));
|
|
gst_qsv_encoder_task_reset (self, task);
|
|
goto out;
|
|
}
|
|
|
|
if (status == MFX_ERR_NONE && task->sync_point) {
|
|
g_queue_push_head (&priv->pending_tasks, task);
|
|
} else {
|
|
gst_qsv_encoder_task_reset (self, task);
|
|
}
|
|
|
|
ret = GST_FLOW_OK;
|
|
/* Do not sync immediately, but record tasks which have output buffer here
|
|
* to improve throughput.
|
|
* In this way, hardware may be able to run encoding job from its background
|
|
* threads (if any). We will do sync only when there's no more free task item
|
|
*/
|
|
while (g_queue_get_length (&priv->pending_tasks) >= priv->task_pool->len) {
|
|
GstQsvEncoderTask *task =
|
|
(GstQsvEncoderTask *) g_queue_pop_tail (&priv->pending_tasks);
|
|
ret = gst_qsv_encoder_finish_frame (self, task, FALSE);
|
|
}
|
|
|
|
out:
|
|
gst_video_codec_frame_unref (frame);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static GstFlowReturn
|
|
gst_qsv_encoder_finish (GstVideoEncoder * encoder)
|
|
{
|
|
GstQsvEncoder *self = GST_QSV_ENCODER (encoder);
|
|
|
|
return gst_qsv_encoder_drain (self, FALSE);
|
|
}
|
|
|
|
static gboolean
|
|
gst_qsv_encoder_flush (GstVideoEncoder * encoder)
|
|
{
|
|
GstQsvEncoder *self = GST_QSV_ENCODER (encoder);
|
|
|
|
gst_qsv_encoder_drain (self, TRUE);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
gst_qsv_encoder_handle_context_query (GstQsvEncoder * self, GstQuery * query)
|
|
{
|
|
GstQsvEncoderPrivate *priv = self->priv;
|
|
|
|
#ifdef G_OS_WIN32
|
|
return gst_d3d11_handle_context_query (GST_ELEMENT (self), query,
|
|
(GstD3D11Device *) priv->device);
|
|
#else
|
|
return gst_va_handle_context_query (GST_ELEMENT (self), query,
|
|
(GstVaDisplay *) priv->device);
|
|
#endif
|
|
}
|
|
|
|
static gboolean
|
|
gst_qsv_encoder_sink_query (GstVideoEncoder * encoder, GstQuery * query)
|
|
{
|
|
GstQsvEncoder *self = GST_QSV_ENCODER (encoder);
|
|
|
|
switch (GST_QUERY_TYPE (query)) {
|
|
case GST_QUERY_CONTEXT:
|
|
if (gst_qsv_encoder_handle_context_query (self, query))
|
|
return TRUE;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return GST_VIDEO_ENCODER_CLASS (parent_class)->sink_query (encoder, query);
|
|
}
|
|
|
|
static gboolean
|
|
gst_qsv_encoder_src_query (GstVideoEncoder * encoder, GstQuery * query)
|
|
{
|
|
GstQsvEncoder *self = GST_QSV_ENCODER (encoder);
|
|
|
|
switch (GST_QUERY_TYPE (query)) {
|
|
case GST_QUERY_CONTEXT:
|
|
if (gst_qsv_encoder_handle_context_query (self, query))
|
|
return TRUE;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return GST_VIDEO_ENCODER_CLASS (parent_class)->src_query (encoder, query);
|
|
}
|
|
|
|
#ifdef G_OS_WIN32
|
|
static gboolean
|
|
gst_qsv_encoder_propose_allocation (GstVideoEncoder * encoder, GstQuery * query)
|
|
{
|
|
GstQsvEncoder *self = GST_QSV_ENCODER (encoder);
|
|
GstQsvEncoderPrivate *priv = self->priv;
|
|
GstD3D11Device *device = GST_D3D11_DEVICE (priv->device);
|
|
GstVideoInfo info;
|
|
GstBufferPool *pool;
|
|
GstCaps *caps;
|
|
guint size;
|
|
GstStructure *config;
|
|
GstCapsFeatures *features;
|
|
gboolean is_d3d11 = FALSE;
|
|
|
|
gst_query_parse_allocation (query, &caps, nullptr);
|
|
if (!caps) {
|
|
GST_WARNING_OBJECT (self, "null caps in query");
|
|
return FALSE;
|
|
}
|
|
|
|
if (!gst_video_info_from_caps (&info, caps)) {
|
|
GST_WARNING_OBJECT (self, "Failed to convert caps into info");
|
|
return FALSE;
|
|
}
|
|
|
|
features = gst_caps_get_features (caps, 0);
|
|
if (features && gst_caps_features_contains (features,
|
|
GST_CAPS_FEATURE_MEMORY_D3D11_MEMORY)) {
|
|
GST_DEBUG_OBJECT (self, "upstream support d3d11 memory");
|
|
pool = gst_d3d11_buffer_pool_new (device);
|
|
is_d3d11 = TRUE;
|
|
} else {
|
|
pool = gst_video_buffer_pool_new ();
|
|
}
|
|
|
|
config = gst_buffer_pool_get_config (pool);
|
|
gst_buffer_pool_config_add_option (config, GST_BUFFER_POOL_OPTION_VIDEO_META);
|
|
|
|
if (is_d3d11) {
|
|
GstD3D11AllocationParams *d3d11_params;
|
|
GstVideoAlignment align;
|
|
|
|
/* d3d11 buffer pool doesn't support generic video alignment
|
|
* because memory layout of CPU accessible staging texture is uncontrollable.
|
|
* Do D3D11 specific handling */
|
|
gst_video_alignment_reset (&align);
|
|
|
|
align.padding_right = GST_VIDEO_INFO_WIDTH (&priv->aligned_info) -
|
|
GST_VIDEO_INFO_WIDTH (&info);
|
|
align.padding_bottom = GST_VIDEO_INFO_HEIGHT (&priv->aligned_info) -
|
|
GST_VIDEO_INFO_HEIGHT (&info);
|
|
|
|
d3d11_params = gst_d3d11_allocation_params_new (device, &info,
|
|
GST_D3D11_ALLOCATION_FLAG_DEFAULT, 0, 0);
|
|
|
|
gst_d3d11_allocation_params_alignment (d3d11_params, &align);
|
|
gst_buffer_pool_config_set_d3d11_allocation_params (config, d3d11_params);
|
|
gst_d3d11_allocation_params_free (d3d11_params);
|
|
} else {
|
|
gst_buffer_pool_config_add_option (config,
|
|
GST_BUFFER_POOL_OPTION_VIDEO_ALIGNMENT);
|
|
}
|
|
|
|
size = GST_VIDEO_INFO_SIZE (&info);
|
|
gst_buffer_pool_config_set_params (config,
|
|
caps, size, priv->surface_pool->len, 0);
|
|
|
|
if (!gst_buffer_pool_set_config (pool, config)) {
|
|
GST_WARNING_OBJECT (self, "Failed to set pool config");
|
|
gst_object_unref (pool);
|
|
return FALSE;
|
|
}
|
|
|
|
/* d3d11 buffer pool will update actual CPU accessible buffer size based on
|
|
* allocated staging texture per gst_buffer_pool_set_config() call,
|
|
* need query again to get the size */
|
|
config = gst_buffer_pool_get_config (pool);
|
|
gst_buffer_pool_config_get_params (config, nullptr, &size, nullptr, nullptr);
|
|
gst_structure_free (config);
|
|
|
|
gst_query_add_allocation_pool (query, pool, size, priv->surface_pool->len, 0);
|
|
gst_query_add_allocation_meta (query, GST_VIDEO_META_API_TYPE, nullptr);
|
|
gst_object_unref (pool);
|
|
|
|
return TRUE;
|
|
}
|
|
#else
|
|
static gboolean
|
|
gst_qsv_encoder_propose_allocation (GstVideoEncoder * encoder, GstQuery * query)
|
|
{
|
|
GstQsvEncoder *self = GST_QSV_ENCODER (encoder);
|
|
GstQsvEncoderPrivate *priv = self->priv;
|
|
GstVideoInfo info;
|
|
GstAllocator *allocator = nullptr;
|
|
GstBufferPool *pool;
|
|
GstCaps *caps;
|
|
guint size;
|
|
GstStructure *config;
|
|
GstVideoAlignment align;
|
|
GstAllocationParams params;
|
|
GArray *formats;
|
|
|
|
gst_query_parse_allocation (query, &caps, nullptr);
|
|
if (!caps) {
|
|
GST_WARNING_OBJECT (self, "null caps in query");
|
|
return FALSE;
|
|
}
|
|
|
|
if (!gst_video_info_from_caps (&info, caps)) {
|
|
GST_WARNING_OBJECT (self, "Failed to convert caps into info");
|
|
return FALSE;
|
|
}
|
|
|
|
gst_allocation_params_init (¶ms);
|
|
|
|
formats = g_array_new (FALSE, FALSE, sizeof (GstVideoFormat));
|
|
g_array_append_val (formats, GST_VIDEO_INFO_FORMAT (&info));
|
|
|
|
allocator = gst_va_allocator_new (GST_VA_DISPLAY (priv->device), formats);
|
|
if (!allocator) {
|
|
GST_ERROR_OBJECT (self, "Failed to create allocator");
|
|
return FALSE;
|
|
}
|
|
|
|
pool = gst_va_pool_new_with_config (caps,
|
|
GST_VIDEO_INFO_SIZE (&info), priv->surface_pool->len, 0,
|
|
VA_SURFACE_ATTRIB_USAGE_HINT_GENERIC, GST_VA_FEATURE_AUTO,
|
|
allocator, ¶ms);
|
|
|
|
if (!pool) {
|
|
GST_ERROR_OBJECT (self, "Failed to create va pool");
|
|
gst_object_unref (allocator);
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
gst_video_alignment_reset (&align);
|
|
align.padding_right = GST_VIDEO_INFO_WIDTH (&priv->aligned_info) -
|
|
GST_VIDEO_INFO_WIDTH (&info);
|
|
align.padding_bottom = GST_VIDEO_INFO_HEIGHT (&priv->aligned_info) -
|
|
GST_VIDEO_INFO_HEIGHT (&info);
|
|
|
|
config = gst_buffer_pool_get_config (pool);
|
|
gst_buffer_pool_config_add_option (config, GST_BUFFER_POOL_OPTION_VIDEO_META);
|
|
gst_buffer_pool_config_add_option (config,
|
|
GST_BUFFER_POOL_OPTION_VIDEO_ALIGNMENT);
|
|
gst_buffer_pool_config_set_video_alignment (config, &align);
|
|
|
|
gst_buffer_pool_config_set_params (config,
|
|
caps, GST_VIDEO_INFO_SIZE (&info), priv->surface_pool->len, 0);
|
|
|
|
if (!gst_buffer_pool_set_config (pool, config)) {
|
|
GST_ERROR_OBJECT (self, "Failed to set pool config");
|
|
gst_clear_object (&allocator);
|
|
gst_object_unref (pool);
|
|
return FALSE;
|
|
}
|
|
|
|
if (allocator)
|
|
gst_query_add_allocation_param (query, allocator, ¶ms);
|
|
|
|
config = gst_buffer_pool_get_config (pool);
|
|
gst_buffer_pool_config_get_params (config, nullptr, &size, nullptr, nullptr);
|
|
gst_structure_free (config);
|
|
|
|
gst_query_add_allocation_pool (query, pool, size, priv->surface_pool->len, 0);
|
|
gst_query_add_allocation_meta (query, GST_VIDEO_META_API_TYPE, nullptr);
|
|
|
|
gst_clear_object (&allocator);
|
|
gst_object_unref (pool);
|
|
|
|
return TRUE;
|
|
}
|
|
#endif
|