/*
* Copyright(c) 2019 Intel Corporation
*     Authors: Jun Tian <jun.tian@intel.com> Xavier Hallade <xavier.hallade@intel.com>
* SPDX - License - Identifier: LGPL-2.1-or-later
*/

/**
 * SECTION:element-gstsvtav1enc
 *
 * The svtav1enc element does AV1 encoding using Scalable
 * Video Technology for AV1 Encoder (SVT-AV1 Encoder).
 *
 * <refsect2>
 * <title>Example launch line</title>
 * |[
 * gst-launch-1.0 -e videotestsrc ! video/x-raw ! svtav1enc ! matroskamux ! filesink location=out.mkv
 * ]|
 * Encodes test input into AV1 compressed data which is then packaged in out.mkv
 * </refsect2>
 */

#include <gst/gst.h>
#include <gst/video/video.h>
#include <gst/video/gstvideoencoder.h>
#include "gstsvtav1enc.h"

GST_DEBUG_CATEGORY_STATIC(gst_svtav1enc_debug_category);
#define GST_CAT_DEFAULT gst_svtav1enc_debug_category

#define GST_SVTAV1ENC_TYPE_INTRA_REFRESH_TYPE (gst_svtav1enc_intra_refresh_type_get_type())
static GType gst_svtav1enc_intra_refresh_type_get_type(void) {
    static GType            intra_refresh_type = 0;
    static const GEnumValue intra_refresh[]    = {
        {SVT_AV1_FWDKF_REFRESH, "Open GOP", "CRA"},
        {SVT_AV1_KF_REFRESH, "Closed GOP", "IDR"},
        {0, NULL, NULL},
    };

    if (!intra_refresh_type) {
        intra_refresh_type = g_enum_register_static("GstSvtAv1EncIntraRefreshType", intra_refresh);
    }
    return intra_refresh_type;
}

/* prototypes */
static void gst_svtav1enc_set_property(GObject *object, guint property_id, const GValue *value, GParamSpec *pspec);
static void gst_svtav1enc_get_property(GObject *object, guint property_id, GValue *value, GParamSpec *pspec);
static void gst_svtav1enc_finalize(GObject *object);

static void          gst_svtav1enc_allocate_svt_buffers(GstSvtAv1Enc *svtav1enc);
static void          gst_svtav1enc_deallocate_svt_buffers(GstSvtAv1Enc *svtav1enc);
static gboolean      gst_svtav1enc_configure_svt(GstSvtAv1Enc *svtav1enc);
static GstFlowReturn gst_svtav1enc_encode(GstSvtAv1Enc *svtav1enc, GstVideoCodecFrame *frame);
static gboolean      gst_svtav1enc_send_eos(GstSvtAv1Enc *svtav1enc);
static GstFlowReturn gst_svtav1enc_dequeue_encoded_frames(GstSvtAv1Enc *svtav1enc, gboolean closing_encoder,
                                                          gboolean output_frames);

static gboolean      gst_svtav1enc_open(GstVideoEncoder *encoder);
static gboolean      gst_svtav1enc_close(GstVideoEncoder *encoder);
static gboolean      gst_svtav1enc_start(GstVideoEncoder *encoder);
static gboolean      gst_svtav1enc_stop(GstVideoEncoder *encoder);
static gboolean      gst_svtav1enc_set_format(GstVideoEncoder *encoder, GstVideoCodecState *state);
static GstFlowReturn gst_svtav1enc_handle_frame(GstVideoEncoder *encoder, GstVideoCodecFrame *frame);
static GstFlowReturn gst_svtav1enc_finish(GstVideoEncoder *encoder);
static gboolean      gst_svtav1enc_propose_allocation(GstVideoEncoder *encoder, GstQuery *query);
static gboolean      gst_svtav1enc_flush(GstVideoEncoder *encoder);

static void gst_svtav1enc_parse_parameters_string(GstSvtAv1Enc *svtav1enc);

enum {
    PROP_0,
    PROP_PRESET,
    PROP_TARGET_BITRATE,
    PROP_MAX_BITRATE,
    PROP_MAX_QP_ALLOWED,
    PROP_MIN_QP_ALLOWED,
    PROP_CQP,
    PROP_CRF,
    PROP_MAXIMUM_BUFFER_SIZE,
    PROP_INTRA_PERIOD_LENGTH,
    PROP_INTRA_REFRESH_TYPE,
    PROP_LEVEL_OF_PARALLELISM,
    PROP_TARGET_SOCKET,
    PROP_PARAMETERS_STRING,
};

#define PROP_PRESET_DEFAULT 10
#define PROP_TARGET_BITRATE_DEFAULT 0
#define PROP_MAX_BITRATE_DEFAULT 0
#define PROP_QP_MAX_QP_ALLOWED_DEFAULT 63
#define PROP_QP_MIN_QP_ALLOWED_DEFAULT 1
#define PROP_CQP_DEFAULT -1
#define PROP_CRF_DEFAULT 35
#define PROP_MAXIMUM_BUFFER_SIZE_DEFAULT 1000
#define PROP_INTRA_PERIOD_LENGTH_DEFAULT -2
#define PROP_INTRA_REFRESH_TYPE_DEFAULT SVT_AV1_KF_REFRESH
#define PROP_LEVEL_OF_PARALLELISM_DEFAULT 0
#define PROP_TARGET_SOCKET_DEFAULT -1
#define PROP_PARAMETERS_STRING_DEFAULT NULL

#if G_BYTE_ORDER == G_LITTLE_ENDIAN
#define FORMAT_I420_10 "I420_10LE"
#else
#define FORMAT_I420_10 "I420_10BE"
#endif

/* pad templates */
static GstStaticPadTemplate gst_svtav1enc_sink_pad_template = GST_STATIC_PAD_TEMPLATE(
    "sink", GST_PAD_SINK, GST_PAD_ALWAYS,
    GST_STATIC_CAPS("video/x-raw, "
                    "format = (string) {I420, " FORMAT_I420_10 "}, "
                    "width = (int) [64, 16384], "
                    "height = (int) [64, 8704], "
                    "framerate = (fraction) [0, MAX]"));

static GstStaticPadTemplate gst_svtav1enc_src_pad_template = GST_STATIC_PAD_TEMPLATE(
    "src", GST_PAD_SRC, GST_PAD_ALWAYS,
    GST_STATIC_CAPS("video/x-av1, "
                    "stream-format = (string) obu-stream, "
                    "alignment = (string) tu, "
                    "width = (int) [64, 16384], "
                    "height = (int) [64, 8704], "
                    "framerate = (fraction) [0, MAX]"));

/* class initialization */
G_DEFINE_TYPE_WITH_CODE(GstSvtAv1Enc, gst_svtav1enc, GST_TYPE_VIDEO_ENCODER,
                        GST_DEBUG_CATEGORY_INIT(gst_svtav1enc_debug_category, "svtav1enc", 0,
                                                "SVT-AV1 encoder element"));

/* this mutex is required to avoid race conditions in SVT-AV1 memory allocations, which aren't thread-safe */
G_LOCK_DEFINE_STATIC(init_mutex);

static void gst_svtav1enc_class_init(GstSvtAv1EncClass *klass) {
    GObjectClass         *gobject_class       = G_OBJECT_CLASS(klass);
    GstVideoEncoderClass *video_encoder_class = GST_VIDEO_ENCODER_CLASS(klass);

    gst_element_class_add_static_pad_template(GST_ELEMENT_CLASS(klass), &gst_svtav1enc_src_pad_template);

    gst_element_class_add_static_pad_template(GST_ELEMENT_CLASS(klass), &gst_svtav1enc_sink_pad_template);

    gst_element_class_set_static_metadata(GST_ELEMENT_CLASS(klass),
                                          "SvtAv1Enc",
                                          "Codec/Encoder/Video",
                                          "Scalable Video Technology for AV1 Encoder (SVT-AV1 Encoder)",
                                          "Jun Tian <jun.tian@intel.com> Xavier Hallade <xavier.hallade@intel.com>");

    gobject_class->set_property             = gst_svtav1enc_set_property;
    gobject_class->get_property             = gst_svtav1enc_get_property;
    gobject_class->finalize                 = gst_svtav1enc_finalize;
    video_encoder_class->open               = GST_DEBUG_FUNCPTR(gst_svtav1enc_open);
    video_encoder_class->close              = GST_DEBUG_FUNCPTR(gst_svtav1enc_close);
    video_encoder_class->start              = GST_DEBUG_FUNCPTR(gst_svtav1enc_start);
    video_encoder_class->stop               = GST_DEBUG_FUNCPTR(gst_svtav1enc_stop);
    video_encoder_class->set_format         = GST_DEBUG_FUNCPTR(gst_svtav1enc_set_format);
    video_encoder_class->handle_frame       = GST_DEBUG_FUNCPTR(gst_svtav1enc_handle_frame);
    video_encoder_class->finish             = GST_DEBUG_FUNCPTR(gst_svtav1enc_finish);
    video_encoder_class->propose_allocation = GST_DEBUG_FUNCPTR(gst_svtav1enc_propose_allocation);
    video_encoder_class->flush              = GST_DEBUG_FUNCPTR(gst_svtav1enc_flush);

    g_object_class_install_property(gobject_class,
                                    PROP_PRESET,
                                    g_param_spec_uint("preset",
                                                      "Preset",
                                                      "Quality vs density tradeoff point"
                                                      " that the encoding is to be performed at"
                                                      " (0 is the highest quality, 13 is the highest speed) ",
                                                      0,
                                                      13,
                                                      PROP_PRESET_DEFAULT,
                                                      G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

    g_object_class_install_property(gobject_class,
                                    PROP_TARGET_BITRATE,
                                    g_param_spec_uint("target-bitrate",
                                                      "Target bitrate",
                                                      "Target bitrate in kbits/sec. Enables CBR or VBR mode",
                                                      0,
                                                      100000,
                                                      PROP_TARGET_BITRATE_DEFAULT,
                                                      G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

    g_object_class_install_property(
        gobject_class,
        PROP_MAX_BITRATE,
        g_param_spec_uint("max-bitrate",
                          "Maximum bitrate",
                          "Maximum bitrate in kbits/sec. Enables VBR mode if a different "
                          "target-bitrate is provided",
                          0,
                          100000,
                          PROP_MAX_BITRATE_DEFAULT,
                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | GST_PARAM_MUTABLE_PLAYING));

    g_object_class_install_property(gobject_class,
                                    PROP_MAX_QP_ALLOWED,
                                    g_param_spec_uint("max-qp-allowed",
                                                      "Max Quantization parameter",
                                                      "Maximum QP value allowed for rate control use"
                                                      " Only used in CBR and VBR mode.",
                                                      0,
                                                      63,
                                                      PROP_MAX_QP_ALLOWED,
                                                      G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

    g_object_class_install_property(gobject_class,
                                    PROP_MIN_QP_ALLOWED,
                                    g_param_spec_uint("min-qp-allowed",
                                                      "Min Quantization parameter",
                                                      "Minimum QP value allowed for rate control use"
                                                      " Only used in CBR and VBR mode.",
                                                      0,
                                                      63,
                                                      PROP_MIN_QP_ALLOWED,
                                                      G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

    g_object_class_install_property(gobject_class,
                                    PROP_CQP,
                                    g_param_spec_int("cqp",
                                                     "Quantization parameter",
                                                     "Quantization parameter used in CQP mode (-1 is disabled)",
                                                     -1,
                                                     63,
                                                     PROP_CQP_DEFAULT,
                                                     G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

    g_object_class_install_property(gobject_class,
                                    PROP_CRF,
                                    g_param_spec_int("crf",
                                                     "Constant Rate Factor",
                                                     "Quantization parameter used in CRF mode (-1 is disabled)",
                                                     -1,
                                                     63,
                                                     PROP_CRF_DEFAULT,
                                                     G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

    g_object_class_install_property(gobject_class,
                                    PROP_MAXIMUM_BUFFER_SIZE,
                                    g_param_spec_uint("maximum-buffer-size",
                                                      "Maximum Buffer Size",
                                                      "Maximum buffer size in milliseconds."
                                                      " Only used in CBR mode.",
                                                      20,
                                                      10000,
                                                      PROP_MAXIMUM_BUFFER_SIZE_DEFAULT,
                                                      G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

    g_object_class_install_property(gobject_class,
                                    PROP_INTRA_PERIOD_LENGTH,
                                    g_param_spec_int("intra-period-length",
                                                     "Intra Period Length",
                                                     "Period of Intra Frames insertion (-2 is auto, -1 no updates)",
                                                     -2,
                                                     G_MAXINT,
                                                     PROP_INTRA_PERIOD_LENGTH_DEFAULT,
                                                     G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

    g_object_class_install_property(gobject_class,
                                    PROP_INTRA_REFRESH_TYPE,
                                    g_param_spec_enum("intra-refresh-type",
                                                      "Intra refresh type",
                                                      "CRA (open GOP)"
                                                      "or IDR frames (closed GOP)",
                                                      GST_SVTAV1ENC_TYPE_INTRA_REFRESH_TYPE,
                                                      PROP_INTRA_REFRESH_TYPE_DEFAULT,
                                                      G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
    g_object_class_install_property(gobject_class,
                                    PROP_LEVEL_OF_PARALLELISM,
                                    g_param_spec_uint("logical-processors",
                                                      "Logical Processors",
                                                      "Number of logical CPU cores to be used. 0: auto",
                                                      0,
                                                      G_MAXUINT,
                                                      PROP_LEVEL_OF_PARALLELISM_DEFAULT,
                                                      G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

    g_object_class_install_property(gobject_class,
                                    PROP_TARGET_SOCKET,
                                    g_param_spec_int("target-socket",
                                                     "Target socket",
                                                     "Target CPU socket to run on. -1: all available",
                                                     -1,
                                                     15,
                                                     PROP_TARGET_SOCKET_DEFAULT,
                                                     G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

    g_object_class_install_property(
        gobject_class,
        PROP_PARAMETERS_STRING,
        g_param_spec_string("parameters-string",
                            "Parameters String",
                            "Colon-delimited list of key=value pairs of additional parameters to set",
                            PROP_PARAMETERS_STRING_DEFAULT,
                            G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
}

static void gst_svtav1enc_init(GstSvtAv1Enc *svtav1enc) {
    svtav1enc->svt_config           = g_new0(EbSvtAv1EncConfiguration, 1);
    svtav1enc->preset               = PROP_PRESET_DEFAULT;
    svtav1enc->target_bitrate       = PROP_TARGET_BITRATE_DEFAULT;
    svtav1enc->max_bitrate          = PROP_MAX_BITRATE_DEFAULT;
    svtav1enc->max_qp_allowed       = PROP_QP_MAX_QP_ALLOWED_DEFAULT;
    svtav1enc->min_qp_allowed       = PROP_QP_MIN_QP_ALLOWED_DEFAULT;
    svtav1enc->cqp                  = PROP_CQP_DEFAULT;
    svtav1enc->crf                  = PROP_CRF_DEFAULT;
    svtav1enc->maximum_buffer_size  = PROP_MAXIMUM_BUFFER_SIZE_DEFAULT;
    svtav1enc->intra_period_length  = PROP_INTRA_PERIOD_LENGTH_DEFAULT;
    svtav1enc->intra_refresh_type   = PROP_INTRA_REFRESH_TYPE_DEFAULT;
    svtav1enc->level_of_parallelism = PROP_LEVEL_OF_PARALLELISM_DEFAULT;
    svtav1enc->target_socket        = PROP_TARGET_SOCKET_DEFAULT;
    svtav1enc->parameters_string    = PROP_PARAMETERS_STRING_DEFAULT;
}

static void gst_svtav1enc_set_property(GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) {
    GstSvtAv1Enc *svtav1enc = GST_SVTAV1ENC(object);

    /* TODO: support reconfiguring on the fly when possible */
    if (svtav1enc->state) {
        GST_ERROR_OBJECT(svtav1enc, "encoder state has been set before properties, this isn't supported yet.");
        return;
    }

    GST_LOG_OBJECT(svtav1enc, "setting property %u", property_id);

    switch (property_id) {
    case PROP_PRESET: svtav1enc->preset = g_value_get_uint(value); break;
    case PROP_TARGET_BITRATE: svtav1enc->target_bitrate = g_value_get_uint(value) * 1000; break;
    case PROP_MAX_BITRATE: svtav1enc->max_bitrate = g_value_get_uint(value) * 1000; break;
    case PROP_MAX_QP_ALLOWED: svtav1enc->max_qp_allowed = g_value_get_uint(value); break;
    case PROP_MIN_QP_ALLOWED: svtav1enc->min_qp_allowed = g_value_get_uint(value); break;
    case PROP_CQP: svtav1enc->cqp = g_value_get_int(value); break;
    case PROP_CRF: svtav1enc->crf = g_value_get_int(value); break;
    case PROP_MAXIMUM_BUFFER_SIZE: svtav1enc->maximum_buffer_size = g_value_get_uint(value); break;
    case PROP_INTRA_PERIOD_LENGTH: svtav1enc->intra_period_length = g_value_get_int(value); break;
    case PROP_INTRA_REFRESH_TYPE: svtav1enc->intra_refresh_type = g_value_get_enum(value); break;
    case PROP_LEVEL_OF_PARALLELISM: svtav1enc->level_of_parallelism = g_value_get_uint(value); break;
    case PROP_TARGET_SOCKET: svtav1enc->target_socket = g_value_get_int(value); break;
    case PROP_PARAMETERS_STRING: {
        g_free(svtav1enc->parameters_string);
        svtav1enc->parameters_string = g_value_dup_string(value);
        break;
    }
    default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break;
    }
}

static void gst_svtav1enc_get_property(GObject *object, guint property_id, GValue *value, GParamSpec *pspec) {
    GstSvtAv1Enc *svtav1enc = GST_SVTAV1ENC(object);

    GST_LOG_OBJECT(svtav1enc, "getting property %u", property_id);

    switch (property_id) {
    case PROP_PRESET: g_value_set_uint(value, svtav1enc->preset); break;
    case PROP_TARGET_BITRATE: g_value_set_uint(value, svtav1enc->target_bitrate / 1000); break;
    case PROP_MAX_BITRATE: g_value_set_uint(value, svtav1enc->max_bitrate / 1000); break;
    case PROP_MAX_QP_ALLOWED: g_value_set_uint(value, svtav1enc->max_qp_allowed); break;
    case PROP_MIN_QP_ALLOWED: g_value_set_uint(value, svtav1enc->min_qp_allowed); break;
    case PROP_CQP: g_value_set_int(value, svtav1enc->cqp); break;
    case PROP_CRF: g_value_set_int(value, svtav1enc->crf); break;
    case PROP_MAXIMUM_BUFFER_SIZE: g_value_set_uint(value, svtav1enc->maximum_buffer_size); break;
    case PROP_INTRA_PERIOD_LENGTH: g_value_set_int(value, svtav1enc->intra_period_length); break;
    case PROP_INTRA_REFRESH_TYPE: g_value_set_enum(value, svtav1enc->intra_refresh_type); break;
    case PROP_LEVEL_OF_PARALLELISM: g_value_set_uint(value, svtav1enc->level_of_parallelism); break;
    case PROP_TARGET_SOCKET: g_value_set_int(value, svtav1enc->target_socket); break;
    case PROP_PARAMETERS_STRING: g_value_set_string(value, svtav1enc->parameters_string); break;
    default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break;
    }
}

static void gst_svtav1enc_finalize(GObject *object) {
    GstSvtAv1Enc *svtav1enc = GST_SVTAV1ENC(object);

    GST_DEBUG_OBJECT(svtav1enc, "finalizing svtav1enc");

    g_free(svtav1enc->svt_config);
    g_free(svtav1enc->parameters_string);

    G_OBJECT_CLASS(gst_svtav1enc_parent_class)->finalize(object);
}

static void gst_svtav1enc_allocate_svt_buffers(GstSvtAv1Enc *svtav1enc) {
    svtav1enc->input_buf                = g_new0(EbBufferHeaderType, 1);
    svtav1enc->input_buf->p_buffer      = (guint8 *)g_new0(EbSvtIOFormat, 1);
    svtav1enc->input_buf->size          = sizeof(EbBufferHeaderType);
    svtav1enc->input_buf->p_app_private = NULL;
    svtav1enc->input_buf->pic_type      = EB_AV1_INVALID_PICTURE;
    svtav1enc->input_buf->metadata      = NULL;
}

static void gst_svtav1enc_deallocate_svt_buffers(GstSvtAv1Enc *svtav1enc) {
    if (svtav1enc->input_buf) {
        g_free(svtav1enc->input_buf->p_buffer);
        svtav1enc->input_buf->p_buffer = NULL;
        g_free(svtav1enc->input_buf);
        svtav1enc->input_buf = NULL;
    }
}

static gboolean gst_svtav1enc_configure_svt(GstSvtAv1Enc *svtav1enc) {
    if (!svtav1enc->state) {
        GST_WARNING_OBJECT(svtav1enc, "no state, can't configure encoder yet");
        return false;
    }

    /* set object properties */
    svtav1enc->svt_config->enc_mode = svtav1enc->preset;
    if (svtav1enc->target_bitrate != 0) {
        svtav1enc->svt_config->target_bit_rate = svtav1enc->target_bitrate;
        if (svtav1enc->target_bitrate != svtav1enc->max_bitrate) {
            GST_DEBUG_OBJECT(svtav1enc,
                             "Enabling VBR mode (br %u max-br %u max-qp %u min-qp %u)",
                             svtav1enc->target_bitrate,
                             svtav1enc->max_bitrate,
                             svtav1enc->max_qp_allowed,
                             svtav1enc->min_qp_allowed);
            svtav1enc->svt_config->max_bit_rate      = svtav1enc->max_bitrate;
            svtav1enc->svt_config->rate_control_mode = SVT_AV1_RC_MODE_VBR;
        } else {
            GST_DEBUG_OBJECT(svtav1enc,
                             "Enabling CBR mode (br %u max-bs %u)",
                             svtav1enc->target_bitrate,
                             svtav1enc->maximum_buffer_size);
            svtav1enc->svt_config->rate_control_mode      = SVT_AV1_RC_MODE_CBR;
            svtav1enc->svt_config->maximum_buffer_size_ms = svtav1enc->maximum_buffer_size;
        }
        svtav1enc->svt_config->max_qp_allowed   = svtav1enc->max_qp_allowed;
        svtav1enc->svt_config->min_qp_allowed   = svtav1enc->min_qp_allowed;
        svtav1enc->svt_config->force_key_frames = false;
    } else if (svtav1enc->crf > 0) {
        GST_DEBUG_OBJECT(svtav1enc, "Enabling CRF mode (qp %u)", svtav1enc->crf);
        svtav1enc->svt_config->qp                = svtav1enc->crf;
        svtav1enc->svt_config->rate_control_mode = SVT_AV1_RC_MODE_CQP_OR_CRF;
        svtav1enc->svt_config->force_key_frames  = true;
    } else if (svtav1enc->cqp > 0) {
        GST_DEBUG_OBJECT(svtav1enc, "Enabling CQP mode (qp %u)", svtav1enc->cqp);
        svtav1enc->svt_config->qp                           = svtav1enc->cqp;
        svtav1enc->svt_config->rate_control_mode            = SVT_AV1_RC_MODE_CQP_OR_CRF;
        svtav1enc->svt_config->enable_adaptive_quantization = false;
        svtav1enc->svt_config->force_key_frames             = true;
    } else {
        GST_DEBUG_OBJECT(svtav1enc, "Using default rate control settings");
    }
    svtav1enc->svt_config->intra_period_length  = svtav1enc->intra_period_length;
    svtav1enc->svt_config->intra_refresh_type   = svtav1enc->intra_refresh_type;
    svtav1enc->svt_config->level_of_parallelism = svtav1enc->level_of_parallelism;
    svtav1enc->svt_config->target_socket        = svtav1enc->target_socket;
    gst_svtav1enc_parse_parameters_string(svtav1enc);

    /* set properties out of GstVideoInfo */
    const GstVideoInfo *info                      = &svtav1enc->state->info;
    svtav1enc->svt_config->encoder_bit_depth      = GST_VIDEO_INFO_COMP_DEPTH(info, 0);
    svtav1enc->svt_config->source_width           = GST_VIDEO_INFO_WIDTH(info);
    svtav1enc->svt_config->source_height          = GST_VIDEO_INFO_HEIGHT(info);
    svtav1enc->svt_config->frame_rate_numerator   = GST_VIDEO_INFO_FPS_N(info) > 0 ? GST_VIDEO_INFO_FPS_N(info) : 1;
    svtav1enc->svt_config->frame_rate_denominator = GST_VIDEO_INFO_FPS_D(info) > 0 ? GST_VIDEO_INFO_FPS_D(info) : 1;
    GST_LOG_OBJECT(svtav1enc,
                   "width %d, height %d, framerate %d/%d",
                   svtav1enc->svt_config->source_width,
                   svtav1enc->svt_config->source_height,
                   svtav1enc->svt_config->frame_rate_numerator,
                   svtav1enc->svt_config->frame_rate_denominator);

    switch (GST_VIDEO_INFO_COLORIMETRY(info).primaries) {
    case GST_VIDEO_COLOR_PRIMARIES_BT709: svtav1enc->svt_config->color_primaries = EB_CICP_CP_BT_709; break;
    case GST_VIDEO_COLOR_PRIMARIES_BT470M: svtav1enc->svt_config->color_primaries = EB_CICP_CP_BT_470_M; break;
    case GST_VIDEO_COLOR_PRIMARIES_BT470BG: svtav1enc->svt_config->color_primaries = EB_CICP_CP_BT_470_B_G; break;
    case GST_VIDEO_COLOR_PRIMARIES_SMPTE170M: svtav1enc->svt_config->color_primaries = EB_CICP_CP_BT_601; break;
    case GST_VIDEO_COLOR_PRIMARIES_SMPTE240M: svtav1enc->svt_config->color_primaries = EB_CICP_CP_SMPTE_240; break;
    case GST_VIDEO_COLOR_PRIMARIES_FILM: svtav1enc->svt_config->color_primaries = EB_CICP_CP_GENERIC_FILM; break;
    case GST_VIDEO_COLOR_PRIMARIES_BT2020: svtav1enc->svt_config->color_primaries = EB_CICP_CP_BT_2020; break;
    case GST_VIDEO_COLOR_PRIMARIES_SMPTERP431: svtav1enc->svt_config->color_primaries = EB_CICP_CP_SMPTE_431; break;
    case GST_VIDEO_COLOR_PRIMARIES_SMPTEEG432: svtav1enc->svt_config->color_primaries = EB_CICP_CP_SMPTE_432; break;
    case GST_VIDEO_COLOR_PRIMARIES_EBU3213: svtav1enc->svt_config->color_primaries = EB_CICP_CP_EBU_3213; break;
    default: svtav1enc->svt_config->color_primaries = EB_CICP_CP_UNSPECIFIED; break;
    }

    switch (GST_VIDEO_INFO_COLORIMETRY(info).transfer) {
    case GST_VIDEO_TRANSFER_BT709: svtav1enc->svt_config->transfer_characteristics = EB_CICP_TC_BT_709; break;
    case GST_VIDEO_TRANSFER_GAMMA28: svtav1enc->svt_config->transfer_characteristics = EB_CICP_TC_BT_470_B_G; break;
#if GST_CHECK_VERSION(1, 18, 0)
    case GST_VIDEO_TRANSFER_BT601: svtav1enc->svt_config->transfer_characteristics = EB_CICP_TC_BT_601; break;
#endif
    case GST_VIDEO_TRANSFER_SMPTE240M: svtav1enc->svt_config->transfer_characteristics = EB_CICP_TC_SMPTE_240; break;
    case GST_VIDEO_TRANSFER_GAMMA10: svtav1enc->svt_config->transfer_characteristics = EB_CICP_TC_LINEAR; break;
    case GST_VIDEO_TRANSFER_LOG100: svtav1enc->svt_config->transfer_characteristics = EB_CICP_TC_LOG_100; break;
    case GST_VIDEO_TRANSFER_LOG316: svtav1enc->svt_config->transfer_characteristics = EB_CICP_TC_LOG_100_SQRT10; break;
    case GST_VIDEO_TRANSFER_SRGB: svtav1enc->svt_config->transfer_characteristics = EB_CICP_TC_SRGB; break;
#if GST_CHECK_VERSION(1, 18, 0)
    case GST_VIDEO_TRANSFER_BT2020_10:
        svtav1enc->svt_config->transfer_characteristics = EB_CICP_TC_BT_2020_10_BIT;
        break;
#endif
    case GST_VIDEO_TRANSFER_BT2020_12:
        svtav1enc->svt_config->transfer_characteristics = EB_CICP_TC_BT_2020_12_BIT;
        break;
#if GST_CHECK_VERSION(1, 18, 0)
    case GST_VIDEO_TRANSFER_SMPTE2084: svtav1enc->svt_config->transfer_characteristics = EB_CICP_TC_SMPTE_2084; break;
    case GST_VIDEO_TRANSFER_ARIB_STD_B67: svtav1enc->svt_config->transfer_characteristics = EB_CICP_TC_HLG; break;
#endif
    default: svtav1enc->svt_config->transfer_characteristics = EB_CICP_TC_UNSPECIFIED; break;
    }

    switch (GST_VIDEO_INFO_COLORIMETRY(info).matrix) {
    case GST_VIDEO_COLOR_MATRIX_RGB: svtav1enc->svt_config->matrix_coefficients = EB_CICP_MC_IDENTITY; break;
    case GST_VIDEO_COLOR_MATRIX_BT709: svtav1enc->svt_config->matrix_coefficients = EB_CICP_MC_BT_709; break;
    case GST_VIDEO_COLOR_MATRIX_FCC: svtav1enc->svt_config->matrix_coefficients = EB_CICP_MC_FCC; break;
    case GST_VIDEO_COLOR_MATRIX_BT601: svtav1enc->svt_config->matrix_coefficients = EB_CICP_MC_BT_601; break;
    case GST_VIDEO_COLOR_MATRIX_SMPTE240M: svtav1enc->svt_config->matrix_coefficients = EB_CICP_MC_SMPTE_240; break;
    case GST_VIDEO_COLOR_MATRIX_BT2020: svtav1enc->svt_config->matrix_coefficients = EB_CICP_MC_BT_2020_NCL; break;

    default: svtav1enc->svt_config->matrix_coefficients = EB_CICP_MC_UNSPECIFIED; break;
    }

    if (GST_VIDEO_INFO_COLORIMETRY(info).range == GST_VIDEO_COLOR_RANGE_0_255) {
        svtav1enc->svt_config->color_range = EB_CR_FULL_RANGE;
    } else {
        svtav1enc->svt_config->color_range = EB_CR_STUDIO_RANGE;
    }

    switch (GST_VIDEO_INFO_CHROMA_SITE(info)) {
    case GST_VIDEO_CHROMA_SITE_V_COSITED: svtav1enc->svt_config->chroma_sample_position = EB_CSP_VERTICAL; break;
    case GST_VIDEO_CHROMA_SITE_COSITED: svtav1enc->svt_config->chroma_sample_position = EB_CSP_COLOCATED; break;
    default: svtav1enc->svt_config->chroma_sample_position = EB_CSP_UNKNOWN;
    }

#if GST_CHECK_VERSION(1, 18, 0)
    GstVideoMasteringDisplayInfo master_display_info;
    if (gst_video_mastering_display_info_from_caps(&master_display_info, svtav1enc->state->caps)) {
        svtav1enc->svt_config->mastering_display.r.x           = master_display_info.display_primaries[0].x;
        svtav1enc->svt_config->mastering_display.r.y           = master_display_info.display_primaries[0].y;
        svtav1enc->svt_config->mastering_display.g.x           = master_display_info.display_primaries[1].x;
        svtav1enc->svt_config->mastering_display.g.y           = master_display_info.display_primaries[1].y;
        svtav1enc->svt_config->mastering_display.b.x           = master_display_info.display_primaries[2].x;
        svtav1enc->svt_config->mastering_display.b.y           = master_display_info.display_primaries[2].y;
        svtav1enc->svt_config->mastering_display.white_point.x = master_display_info.white_point.x;
        svtav1enc->svt_config->mastering_display.white_point.y = master_display_info.white_point.y;
        svtav1enc->svt_config->mastering_display.max_luma      = master_display_info.max_display_mastering_luminance;
        svtav1enc->svt_config->mastering_display.min_luma      = master_display_info.min_display_mastering_luminance;
        svtav1enc->svt_config->high_dynamic_range_input        = true;
    } else {
        memset(&svtav1enc->svt_config->mastering_display, 0, sizeof(svtav1enc->svt_config->mastering_display));
        svtav1enc->svt_config->high_dynamic_range_input = false;
    }

    GstVideoContentLightLevel content_light_level;
    if (gst_video_content_light_level_from_caps(&content_light_level, svtav1enc->state->caps)) {
        svtav1enc->svt_config->content_light_level.max_cll  = content_light_level.max_content_light_level;
        svtav1enc->svt_config->content_light_level.max_fall = content_light_level.max_frame_average_light_level;
    } else {
        memset(&svtav1enc->svt_config->content_light_level, 0, sizeof(svtav1enc->svt_config->content_light_level));
    }
#endif

    EbErrorType res = svt_av1_enc_set_parameter(svtav1enc->svt_encoder, svtav1enc->svt_config);
    if (res != EB_ErrorNone) {
        GST_ELEMENT_ERROR(svtav1enc, LIBRARY, INIT, (NULL), ("svt_av1_enc_set_parameter failed with error %d", res));
        return false;
    }
    return true;
}

static gboolean gst_svtav1enc_start_svt(GstSvtAv1Enc *svtav1enc) {
    G_LOCK(init_mutex);
    EbErrorType res = svt_av1_enc_init(svtav1enc->svt_encoder);
    G_UNLOCK(init_mutex);

    if (res != EB_ErrorNone) {
        GST_ELEMENT_ERROR(svtav1enc, LIBRARY, INIT, (NULL), ("svt_av1_enc_init failed with error %d", res));
        return false;
    }
    return true;
}

static GstFlowReturn gst_svtav1enc_encode(GstSvtAv1Enc *svtav1enc, GstVideoCodecFrame *frame) {
    GstFlowReturn       ret                  = GST_FLOW_OK;
    EbErrorType         res                  = EB_ErrorNone;
    EbBufferHeaderType *input_buffer         = svtav1enc->input_buf;
    EbSvtIOFormat      *input_picture_buffer = (EbSvtIOFormat *)svtav1enc->input_buf->p_buffer;
    GstVideoFrame       video_frame;

    if (!gst_video_frame_map(&video_frame, &svtav1enc->state->info, frame->input_buffer, GST_MAP_READ)) {
        GST_ELEMENT_ERROR(svtav1enc, LIBRARY, ENCODE, (NULL), ("couldn't map input frame"));
        return GST_FLOW_ERROR;
    }

    input_picture_buffer->y_stride = GST_VIDEO_FRAME_COMP_STRIDE(&video_frame, 0) /
        GST_VIDEO_FRAME_COMP_PSTRIDE(&video_frame, 0);
    input_picture_buffer->cb_stride = GST_VIDEO_FRAME_COMP_STRIDE(&video_frame, 1) /
        GST_VIDEO_FRAME_COMP_PSTRIDE(&video_frame, 1);
    input_picture_buffer->cr_stride = GST_VIDEO_FRAME_COMP_STRIDE(&video_frame, 2) /
        GST_VIDEO_FRAME_COMP_PSTRIDE(&video_frame, 2);

    input_picture_buffer->luma = GST_VIDEO_FRAME_PLANE_DATA(&video_frame, 0);
    input_picture_buffer->cb   = GST_VIDEO_FRAME_PLANE_DATA(&video_frame, 1);
    input_picture_buffer->cr   = GST_VIDEO_FRAME_PLANE_DATA(&video_frame, 2);

    input_buffer->n_filled_len = GST_VIDEO_FRAME_SIZE(&video_frame);

    /* Fill in Buffers Header control data */
    input_buffer->flags         = 0;
    input_buffer->p_app_private = NULL;
    input_buffer->pts           = frame->pts;
    input_buffer->pic_type      = EB_AV1_INVALID_PICTURE;

    if (GST_VIDEO_CODEC_FRAME_IS_FORCE_KEYFRAME(frame)) {
        input_buffer->pic_type = EB_AV1_KEY_PICTURE;
    }

    input_buffer->metadata = NULL;

    res = svt_av1_enc_send_picture(svtav1enc->svt_encoder, input_buffer);
    if (res != EB_ErrorNone) {
        GST_ELEMENT_ERROR(svtav1enc, LIBRARY, ENCODE, (NULL), ("error in sending picture to encoder"));
        ret = GST_FLOW_ERROR;
    }
    gst_video_frame_unmap(&video_frame);

    return ret;
}

static gboolean gst_svtav1enc_send_eos(GstSvtAv1Enc *svtav1enc) {
    EbErrorType ret = EB_ErrorNone;

    EbBufferHeaderType input_buffer;
    input_buffer.n_alloc_len   = 0;
    input_buffer.n_filled_len  = 0;
    input_buffer.n_tick_count  = 0;
    input_buffer.p_app_private = NULL;
    input_buffer.flags         = EB_BUFFERFLAG_EOS;
    input_buffer.p_buffer      = NULL;
    input_buffer.metadata      = NULL;

    ret = svt_av1_enc_send_picture(svtav1enc->svt_encoder, &input_buffer);

    if (ret != EB_ErrorNone) {
        GST_ELEMENT_ERROR(svtav1enc, LIBRARY, ENCODE, (NULL), ("couldn't send EOS frame."));
        return false;
    }

    return (ret == EB_ErrorNone);
}

static gboolean gst_svtav1enc_flush(GstVideoEncoder *encoder) {
    GstFlowReturn ret = gst_svtav1enc_dequeue_encoded_frames(GST_SVTAV1ENC(encoder), true, false);

    return (ret != GST_FLOW_ERROR);
}

static GstFlowReturn gst_svtav1enc_dequeue_encoded_frames(GstSvtAv1Enc *svtav1enc, gboolean done_sending_pics,
                                                          gboolean output_frames) {
    GstFlowReturn ret           = GST_FLOW_OK;
    EbErrorType   res           = EB_ErrorNone;
    gboolean      encode_at_eos = false;

    do {
        GstVideoCodecFrame *frame      = NULL;
        EbBufferHeaderType *output_buf = NULL;

        res = svt_av1_enc_get_packet(svtav1enc->svt_encoder, &output_buf, done_sending_pics);

        if (output_buf != NULL)
            encode_at_eos = ((output_buf->flags & EB_BUFFERFLAG_EOS) == EB_BUFFERFLAG_EOS);

        if (encode_at_eos) {
            return GST_FLOW_EOS;
        } else if (res == EB_ErrorMax) {
            GST_ELEMENT_ERROR(svtav1enc, LIBRARY, ENCODE, (NULL), ("encode failed"));
            return GST_FLOW_ERROR;
        } else if (res != EB_NoErrorEmptyQueue && output_frames && output_buf) {
            // AV1 has no frame re-ordering so always get the oldest frame
            frame = gst_video_encoder_get_oldest_frame(GST_VIDEO_ENCODER(svtav1enc));
            if (output_buf->pic_type == EB_AV1_KEY_PICTURE || output_buf->pic_type == EB_AV1_INTRA_ONLY_PICTURE) {
                GST_VIDEO_CODEC_FRAME_SET_SYNC_POINT(frame);
            }

            if ((ret = gst_video_encoder_allocate_output_frame(
                     GST_VIDEO_ENCODER(svtav1enc), frame, output_buf->n_filled_len)) != GST_FLOW_OK) {
                svt_av1_enc_release_out_buffer(&output_buf);
                gst_video_codec_frame_unref(frame);
                return ret;
            }
            gst_buffer_fill(frame->output_buffer, 0, output_buf->p_buffer, output_buf->n_filled_len);

            frame->pts = frame->output_buffer->pts = output_buf->pts;

            GST_LOG_OBJECT(svtav1enc,
                           "#frame:%u pts:%" G_GINT64_FORMAT " SliceType:%d\n",
                           frame->system_frame_number,
                           (frame->pts),
                           output_buf->pic_type);

            svt_av1_enc_release_out_buffer(&output_buf);
            output_buf = NULL;

            ret = gst_video_encoder_finish_frame(GST_VIDEO_ENCODER(svtav1enc), frame);
        }

    } while (res == EB_ErrorNone && ret == GST_FLOW_OK);

    return ret;
}

static gboolean gst_svtav1enc_open(GstVideoEncoder *encoder) {
    GstSvtAv1Enc *svtav1enc = GST_SVTAV1ENC(encoder);

    GST_DEBUG_OBJECT(svtav1enc, "open");
    EbErrorType res = svt_av1_enc_init_handle(&svtav1enc->svt_encoder, svtav1enc->svt_config);
    if (res != EB_ErrorNone) {
        GST_ELEMENT_ERROR(svtav1enc, LIBRARY, INIT, (NULL), ("svt_av1_enc_init_handle failed with error %d", res));
        return false;
    }

    return true;
}
static gboolean gst_svtav1enc_close(GstVideoEncoder *encoder) {
    GstSvtAv1Enc *svtav1enc = GST_SVTAV1ENC(encoder);

    GST_DEBUG_OBJECT(svtav1enc, "close");

    svt_av1_enc_deinit_handle(svtav1enc->svt_encoder);
    svtav1enc->svt_encoder = NULL;
    return true;
}

static gboolean gst_svtav1enc_start(GstVideoEncoder *encoder) {
    GstSvtAv1Enc *svtav1enc = GST_SVTAV1ENC(encoder);

    GST_DEBUG_OBJECT(svtav1enc, "start");

    gst_svtav1enc_allocate_svt_buffers(svtav1enc);
    return true;
}

static gboolean gst_svtav1enc_stop(GstVideoEncoder *encoder) {
    GstSvtAv1Enc *svtav1enc = GST_SVTAV1ENC(encoder);

    GST_DEBUG_OBJECT(svtav1enc, "stop");

    if (svtav1enc->state)
        gst_video_codec_state_unref(svtav1enc->state);
    svtav1enc->state = NULL;

    svt_av1_enc_deinit(svtav1enc->svt_encoder);
    gst_svtav1enc_deallocate_svt_buffers(svtav1enc);

    return true;
}

static gboolean gst_svtav1enc_set_format(GstVideoEncoder *encoder, GstVideoCodecState *state) {
    GstSvtAv1Enc       *svtav1enc          = GST_SVTAV1ENC(encoder);
    GstClockTime        min_latency_frames = 0;
    GstCaps            *src_caps           = NULL;
    GstVideoCodecState *output_state;
    GST_DEBUG_OBJECT(svtav1enc, "set_format");

    if (svtav1enc->state && !gst_video_info_is_equal(&svtav1enc->state->info, &state->info)) {
        gst_svtav1enc_finish(encoder);
        gst_svtav1enc_stop(encoder);
        gst_svtav1enc_close(encoder);
        gst_svtav1enc_open(encoder);
        gst_svtav1enc_start(encoder);
    }
    svtav1enc->state = gst_video_codec_state_ref(state);

    if (!gst_svtav1enc_configure_svt(svtav1enc))
        return false;
    if (!gst_svtav1enc_start_svt(svtav1enc))
        return false;

    guint32 fps = svtav1enc->svt_config->frame_rate_numerator / svtav1enc->svt_config->frame_rate_denominator;
    fps         = fps > 120 ? 120 : fps;
    fps         = fps < 24 ? 24 : fps;

    min_latency_frames = ((fps * 5) >> 2);

    gst_video_encoder_set_latency(
        encoder,
        min_latency_frames * GST_SECOND /
            (svtav1enc->svt_config->frame_rate_numerator / svtav1enc->svt_config->frame_rate_denominator),
        -1);

    src_caps     = gst_static_pad_template_get_caps(&gst_svtav1enc_src_pad_template);
    output_state = gst_video_encoder_set_output_state(GST_VIDEO_ENCODER(encoder), src_caps, svtav1enc->state);
    gst_video_codec_state_unref(output_state);

    GST_DEBUG_OBJECT(svtav1enc, "output caps: %" GST_PTR_FORMAT, svtav1enc->state->caps);

    return gst_video_encoder_negotiate(encoder);
}

static GstFlowReturn gst_svtav1enc_handle_frame(GstVideoEncoder *encoder, GstVideoCodecFrame *frame) {
    GstSvtAv1Enc *svtav1enc = GST_SVTAV1ENC(encoder);
    GstFlowReturn ret       = GST_FLOW_OK;

    GST_DEBUG_OBJECT(svtav1enc, "handle_frame");

    ret = gst_svtav1enc_encode(svtav1enc, frame);
    gst_video_codec_frame_unref(frame);
    if (ret != GST_FLOW_OK) {
        GST_DEBUG_OBJECT(svtav1enc, "gst_svtav1enc_encode returned %d", ret);
        return ret;
    }

    return gst_svtav1enc_dequeue_encoded_frames(svtav1enc, false, true);
}

static GstFlowReturn gst_svtav1enc_finish(GstVideoEncoder *encoder) {
    GstSvtAv1Enc *svtav1enc = GST_SVTAV1ENC(encoder);

    GST_DEBUG_OBJECT(svtav1enc, "finish");

    gst_svtav1enc_send_eos(svtav1enc);

    return gst_svtav1enc_dequeue_encoded_frames(svtav1enc, true, true);
}

static gboolean gst_svtav1enc_propose_allocation(GstVideoEncoder *encoder, GstQuery *query) {
    GstSvtAv1Enc *svtav1enc = GST_SVTAV1ENC(encoder);

    GST_DEBUG_OBJECT(svtav1enc, "propose_allocation");

    gst_query_add_allocation_meta(query, GST_VIDEO_META_API_TYPE, NULL);

    return GST_VIDEO_ENCODER_CLASS(gst_svtav1enc_parent_class)->propose_allocation(encoder, query);
}

static void gst_svtav1enc_parse_parameters_string(GstSvtAv1Enc *svtav1enc) {
    gchar **key_values, **p;

    if (!svtav1enc->parameters_string)
        return;

    p = key_values = g_strsplit(svtav1enc->parameters_string, ":", -1);
    while (p && *p) {
        gchar      *equals;
        EbErrorType res;

        equals = strchr(*p, '=');
        if (!equals) {
            p++;
            continue;
        }

        *equals = '\0';
        equals++;

        GST_DEBUG_OBJECT(svtav1enc, "Setting parameter %s=%s", *p, equals);

        res = svt_av1_enc_parse_parameter(svtav1enc->svt_config, *p, equals);
        if (res != EB_ErrorNone) {
            GST_WARNING_OBJECT(svtav1enc, "Failed to set parameter %s=%s: %d", *p, equals, res);
        }

        p++;
    }

    g_strfreev(key_values);
}

static gboolean plugin_init(GstPlugin *plugin) {
    return gst_element_register(plugin, "svtav1enc", GST_RANK_SECONDARY, GST_TYPE_SVTAV1ENC);
}

#ifndef VERSION
#define VERSION "1.0"
#endif
#ifndef PACKAGE
#define PACKAGE "gstreamer-svt-av1"
#endif
#ifndef PACKAGE_NAME
#define PACKAGE_NAME "SVT-AV1 Encoder plugin for GStreamer"
#endif
#ifndef GST_PACKAGE_ORIGIN
#define GST_PACKAGE_ORIGIN "https://gitlab.com/AOMediaCodec"
#endif

GST_PLUGIN_DEFINE(GST_VERSION_MAJOR, GST_VERSION_MINOR, svtav1enc,
                  "Scalable Video Technology for AV1 Encoder (SVT-AV1 Encoder)", plugin_init, VERSION, "LGPL",
                  PACKAGE_NAME, GST_PACKAGE_ORIGIN)
