/*
 * Copyright (C) 2010, 2013 Ole André Vadla Ravnås <oleavr@soundrop.com>
 * Copyright (C) 2013 Intel Corporation
 *
 * 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.
 */

/**
 * SECTION:element-vtenc_h264
 * @title: vtenc_h264
 *
 * Apple VideoToolbox H264 encoder, which can either use HW or a SW
 * implementation depending on the device.
 *
 * ## Example pipeline
 * |[
 * gst-launch-1.0 -v videotestsrc ! vtenc_h264 ! qtmux ! filesink location=out.mov
 * ]| Encode a test video pattern and save it as an MOV file
 *
 */

/**
 * SECTION:element-vtenc_h264_hw
 * @title: vtenc_h264_hw
 *
 * Apple VideoToolbox H264 HW-only encoder (only available on macOS at
 * present).
 *
 * ## Example pipeline
 * |[
 * gst-launch-1.0 -v videotestsrc ! vtenc_h264_hw ! qtmux ! filesink location=out.mov
 * ]| Encode a test video pattern and save it as an MOV file
 *
 */

/**
 * SECTION:element-vtenc_prores
 * @title: vtenc_prores
 *
 * Apple VideoToolbox ProRes encoder
 *
 * ## Example pipeline
 * |[
 * gst-launch-1.0 -v videotestsrc ! vtenc_prores ! qtmux ! filesink location=out.mov
 * ]| Encode a test video pattern and save it as an MOV file
 *
 * Since: 1.20
 */

/**
 * SECTION:element-vtenc_h265
 * @title: vtenc_h265
 *
 * Apple VideoToolbox H265 encoder, which can either use HW or a SW
 * implementation depending on the device.
 *
 * ## Example pipeline
 * |[
 * gst-launch-1.0 -v videotestsrc ! vtenc_h265 ! qtmux ! filesink location=out.mov
 * ]| Encode a test video pattern and save it as an MOV file
 *
 * Since: 1.22
 */

/**
 * SECTION:element-vtenc_h265_hw
 * @title: vtenc_h265_hw
 *
 * Apple VideoToolbox H265 HW-only encoder (only available on macOS at
 * present).
 *
 * ## Example pipeline
 * |[
 * gst-launch-1.0 -v videotestsrc ! vtenc_h265_hw ! qtmux ! filesink location=out.mov
 * ]| Encode a test video pattern and save it as an MOV file
 *
 * Since: 1.22
 */

/**
 * SECTION:element-vtenc_h265a
 * @title: vtenc_h265a
 *
 * Apple VideoToolbox H265 encoder with alpha channel support.
 * This encoder can either use HW or a SW implementation depending on the device.
 *
 * ## Example pipeline (assuming a PNG with an alpha channel as input)
 * |[
 * gst-launch-1.0 -v filesrc location=test.png ! pngdec ! imagefreeze num-buffers=1000 ! videoconvert ! vtenc_h265a ! qtmux ! filesink location=out.mov
 * ]| Encode 1000 frames of a PNG image with an alpha channel and save it as an MOV file.
 *
 * Since: 1.26
 */

/**
 * SECTION:element-vtenc_h265a_hw
 * @title: vtenc_h265a_hw
 *
 * Apple VideoToolbox H265 HW-only encoder with alpha channel support.
 * Currently only available on macOS.
 *
 * ## Example pipeline (assuming a PNG with an alpha channel as input)
 * |[
 * gst-launch-1.0 -v filesrc location=test.png ! pngdec ! imagefreeze num-buffers=1000 ! videoconvert ! vtenc_h265a ! qtmux ! filesink location=out.mov
 * ]| Encode 1000 frames of a PNG image with an alpha channel and save it as an MOV file.
 *
 * Since: 1.26
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "vtenc.h"

#include "coremediabuffer.h"

#if !TARGET_OS_OSX
#include "corevideobuffer.h"
#endif

#include "vtutil.h"
#include "helpers.h"
#include <gst/pbutils/codec-utils.h>
#include <sys/sysctl.h>

#define VTENC_DEFAULT_BITRATE     0
#define VTENC_DEFAULT_FRAME_REORDERING TRUE
#define VTENC_DEFAULT_REALTIME FALSE
#define VTENC_DEFAULT_QUALITY 0.5
#define VTENC_DEFAULT_MAX_KEYFRAME_INTERVAL 0
#define VTENC_DEFAULT_MAX_KEYFRAME_INTERVAL_DURATION 0
#define VTENC_DEFAULT_PRESERVE_ALPHA TRUE
#define VTENC_OUTPUT_QUEUE_SIZE 3
#define VTENC_HEVCALPHA_INPUT_LIMIT 5

GST_DEBUG_CATEGORY (gst_vtenc_debug);
#define GST_CAT_DEFAULT (gst_vtenc_debug)

#define GST_VTENC_CODEC_DETAILS_QDATA \
    g_quark_from_static_string ("vtenc-codec-details")

#define CMTIME_TO_GST_CLOCK_TIME(time) time.value / (time.timescale / GST_SECOND)

/* define EnableHardwareAcceleratedVideoEncoder in < 10.9 */
#if defined(MAC_OS_X_VERSION_MAX_ALLOWED) && MAC_OS_X_VERSION_MAX_ALLOWED < 1090
const CFStringRef
    kVTVideoEncoderSpecification_EnableHardwareAcceleratedVideoEncoder =
CFSTR ("EnableHardwareAcceleratedVideoEncoder");
const CFStringRef
    kVTVideoEncoderSpecification_RequireHardwareAcceleratedVideoEncoder =
CFSTR ("RequireHardwareAcceleratedVideoEncoder");
const CFStringRef kVTCompressionPropertyKey_ProfileLevel =
CFSTR ("ProfileLevel");
const CFStringRef kVTProfileLevel_H264_Baseline_AutoLevel =
CFSTR ("H264_Baseline_AutoLevel");
#endif

#if defined(MAC_OS_X_VERSION_MAX_ALLOWED) && MAC_OS_X_VERSION_MAX_ALLOWED < 1080
const CFStringRef kVTCompressionPropertyKey_Quality = CFSTR ("Quality");
#endif

/* This property key is currently completely undocumented. The only way you can
 * know about its existence is if Apple tells you. It allows you to tell the
 * encoder to not preserve alpha even when outputting alpha formats. */
const CFStringRef gstVTCodecPropertyKey_PreserveAlphaChannel =
CFSTR ("kVTCodecPropertyKey_PreserveAlphaChannel");

enum
{
  PROP_0,
  PROP_USAGE,
  PROP_BITRATE,
  PROP_ALLOW_FRAME_REORDERING,
  PROP_REALTIME,
  PROP_QUALITY,
  PROP_MAX_KEYFRAME_INTERVAL,
  PROP_MAX_KEYFRAME_INTERVAL_DURATION,
  PROP_PRESERVE_ALPHA,
  PROP_RATE_CONTROL,
  PROP_DATA_RATE_LIMITS,
  PROP_MAX_FRAME_DELAY,
};

typedef struct _GstVTEncFrame GstVTEncFrame;

struct _GstVTEncFrame
{
  GstBuffer *buf;
  GstVideoFrame videoframe;
};

#define parent_class gst_vtenc_parent_class
G_DEFINE_TYPE (GstVTEnc, gst_vtenc, GST_TYPE_VIDEO_ENCODER);

static void gst_vtenc_get_property (GObject * obj, guint prop_id,
    GValue * value, GParamSpec * pspec);
static void gst_vtenc_set_property (GObject * obj, guint prop_id,
    const GValue * value, GParamSpec * pspec);
static void gst_vtenc_finalize (GObject * obj);

static gboolean gst_vtenc_start (GstVideoEncoder * enc);
static gboolean gst_vtenc_stop (GstVideoEncoder * enc);
static void gst_vtenc_output_loop (GstVTEnc * self);
static gboolean gst_vtenc_set_format (GstVideoEncoder * enc,
    GstVideoCodecState * input_state);
static GstFlowReturn gst_vtenc_handle_frame (GstVideoEncoder * enc,
    GstVideoCodecFrame * frame);
static GstStateChangeReturn gst_vtenc_change_state (GstElement * element,
    GstStateChange transition);
static GstFlowReturn gst_vtenc_finish (GstVideoEncoder * enc);
static gboolean gst_vtenc_flush (GstVideoEncoder * enc);
static gboolean gst_vtenc_sink_event (GstVideoEncoder * enc, GstEvent * event);
static VTCompressionSessionRef gst_vtenc_create_session (GstVTEnc * self);
static void gst_vtenc_destroy_session (GstVTEnc * self,
    VTCompressionSessionRef * session);
static void gst_vtenc_session_dump_properties (GstVTEnc * self,
    VTCompressionSessionRef session);
static void gst_vtenc_session_configure_expected_framerate (GstVTEnc * self,
    VTCompressionSessionRef session, gdouble framerate);
static void gst_vtenc_session_configure_max_keyframe_interval (GstVTEnc * self,
    VTCompressionSessionRef session, gint interval);
static void gst_vtenc_session_configure_max_keyframe_interval_duration
    (GstVTEnc * self, VTCompressionSessionRef session, gdouble duration);
static void gst_vtenc_session_configure_max_frame_delay (GstVTEnc * self,
    VTCompressionSessionRef session, int delay);
static void gst_vtenc_session_configure_bitrate (GstVTEnc * self,
    VTCompressionSessionRef session);
static OSStatus gst_vtenc_session_configure_property_int (GstVTEnc * self,
    VTCompressionSessionRef session, CFStringRef name, gint value);
static OSStatus gst_vtenc_session_configure_property_double (GstVTEnc * self,
    VTCompressionSessionRef session, CFStringRef name, gdouble value);
static void gst_vtenc_session_configure_allow_frame_reordering (GstVTEnc * self,
    VTCompressionSessionRef session, gboolean allow_frame_reordering);
static void gst_vtenc_session_configure_realtime (GstVTEnc * self,
    VTCompressionSessionRef session, gboolean realtime);

static GstFlowReturn gst_vtenc_encode_frame (GstVTEnc * self,
    GstVideoCodecFrame * frame);
static void gst_vtenc_session_output_callback (void *outputCallbackRefCon,
    void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags,
    CMSampleBufferRef sampleBuffer);
static gboolean gst_vtenc_buffer_is_keyframe (GstVTEnc * self,
    CMSampleBufferRef sbuf);


#if TARGET_OS_OSX
static GstVTEncFrame *gst_vtenc_frame_new (GstBuffer * buf,
    GstVideoInfo * videoinfo);
static void gst_vtenc_frame_free (GstVTEncFrame * frame);

static void gst_pixel_buffer_release_cb (void *releaseRefCon,
    const void *dataPtr, size_t dataSize, size_t numberOfPlanes,
    const void *planeAddresses[]);
#endif

#define GST_TYPE_VTENC_RATE_CONTROL (gst_vtenc_rate_control_get_type())
static GType
gst_vtenc_rate_control_get_type (void)
{
  static GType rc_type = 0;

  static const GEnumValue rc_types[] = {
    {GST_VTENC_RATE_CONTROL_ABR, "Average Bitrate", "abr"},
    {GST_VTENC_RATE_CONTROL_CBR, "Constant Bitrate", "cbr"},
    {0, NULL, NULL},
  };

  if (!rc_type) {
    rc_type = g_enum_register_static ("GstVtencRateControl", rc_types);
  }
  return rc_type;
}

static GstStaticCaps sink_caps =
GST_STATIC_CAPS (GST_VIDEO_CAPS_MAKE
    ("{ AYUV64, UYVY, NV12, I420, P010_10LE }"));

static void
gst_vtenc_base_init (GstVTEncClass * klass)
{
  const GstVTEncoderDetails *codec_details =
      GST_VTENC_CLASS_GET_CODEC_DETAILS (klass);
  GstElementClass *element_class = GST_ELEMENT_CLASS (klass);
  const int min_width = 1, max_width = G_MAXINT;
  const int min_height = 1, max_height = G_MAXINT;
  const int min_fps_n = 0, max_fps_n = G_MAXINT;
  const int min_fps_d = 1, max_fps_d = 1;
  GstCaps *src_caps;
  gchar *longname, *description;

  longname = g_strdup_printf ("%s encoder", codec_details->name);
  description = g_strdup_printf ("%s encoder", codec_details->name);

  gst_element_class_set_metadata (element_class, longname,
      "Codec/Encoder/Video/Hardware", description, codec_details->authors);

  g_free (longname);
  g_free (description);

  {
    GstCaps *caps = gst_static_caps_get (&sink_caps);
#if TARGET_OS_OSX
    gboolean enable_argb = TRUE;
    int retval;
    char cpu_name[30];
    size_t cpu_len = 30;

    if (__builtin_available (macOS 13.0, *)) {
      /* Can't negate a __builtin_available check */
    } else {
      /* Disable ARGB64/RGBA64 if we're on M1 Pro/Max and macOS < 13.0
       * due to a bug within VideoToolbox which causes encoding to fail. */
      retval = sysctlbyname ("machdep.cpu.brand_string", &cpu_name, &cpu_len,
          NULL, 0);

      if (retval == 0 &&
          (strstr (cpu_name, "M1 Pro") != NULL ||
              strstr (cpu_name, "M1 Max") != NULL)) {
        GST_WARNING
            ("Disabling ARGB64/RGBA64 caps due to a bug in VideoToolbox "
            "on M1 Pro/Max running macOS < 13.0.");
        enable_argb = FALSE;
      }
    }

    if (enable_argb) {
      caps = gst_vtutil_caps_append_video_format (caps, "ARGB64_BE");
      /* RGBA64_LE is kCVPixelFormatType_64RGBALE, only available on macOS 11.3+ */
      if (GST_APPLEMEDIA_HAVE_64RGBALE)
        caps = gst_vtutil_caps_append_video_format (caps, "RGBA64_LE");
    }
#endif
    gst_element_class_add_pad_template (element_class,
        gst_pad_template_new ("sink", GST_PAD_SINK, GST_PAD_ALWAYS, caps));
  }

  src_caps = gst_caps_new_simple (codec_details->mimetype,
      "width", GST_TYPE_INT_RANGE, min_width, max_width,
      "height", GST_TYPE_INT_RANGE, min_height, max_height,
      "framerate", GST_TYPE_FRACTION_RANGE,
      min_fps_n, min_fps_d, max_fps_n, max_fps_d, NULL);

  /* Signal our limited interlace support */
  {
    G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
    GValueArray *arr = g_value_array_new (2);
    GValue val = G_VALUE_INIT;

    g_value_init (&val, G_TYPE_STRING);

    g_value_set_string (&val, "progressive");
    arr = g_value_array_append (arr, &val);

    g_value_set_string (&val, "interleaved");
    arr = g_value_array_append (arr, &val);

    gst_structure_set_list (gst_caps_get_structure (src_caps, 0),
        "interlace-mode", arr);
    g_value_array_free (arr);
    G_GNUC_END_IGNORE_DEPRECATIONS;
  }

  switch (codec_details->format_id) {
    case kCMVideoCodecType_H264:
      gst_structure_set (gst_caps_get_structure (src_caps, 0),
          "stream-format", G_TYPE_STRING, "avc",
          "alignment", G_TYPE_STRING, "au", NULL);
      break;
    case kCMVideoCodecType_HEVC:
    case kCMVideoCodecType_HEVCWithAlpha:
      gst_structure_set (gst_caps_get_structure (src_caps, 0),
          "stream-format", G_TYPE_STRING, "hvc1",
          "alignment", G_TYPE_STRING, "au", NULL);
      break;
    case GST_kCMVideoCodecType_Some_AppleProRes:
      if (g_strcmp0 (codec_details->mimetype, "video/x-prores") == 0) {
        G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
        GValueArray *arr = g_value_array_new (6);
        GValue val = G_VALUE_INIT;

        g_value_init (&val, G_TYPE_STRING);
        g_value_set_string (&val, "standard");
        arr = g_value_array_append (arr, &val);
        g_value_set_string (&val, "4444xq");
        arr = g_value_array_append (arr, &val);
        g_value_set_string (&val, "4444");
        arr = g_value_array_append (arr, &val);
        g_value_set_string (&val, "hq");
        arr = g_value_array_append (arr, &val);
        g_value_set_string (&val, "lt");
        arr = g_value_array_append (arr, &val);
        g_value_set_string (&val, "proxy");
        arr = g_value_array_append (arr, &val);
        gst_structure_set_list (gst_caps_get_structure (src_caps, 0),
            "variant", arr);
        g_value_array_free (arr);
        g_value_unset (&val);
        G_GNUC_END_IGNORE_DEPRECATIONS;
        break;
      }
      /* fall through */
    default:
      g_assert_not_reached ();
  }

  gst_element_class_add_pad_template (element_class,
      gst_pad_template_new ("src", GST_PAD_SRC, GST_PAD_ALWAYS, src_caps));
  gst_caps_unref (src_caps);
}

static void
gst_vtenc_class_init (GstVTEncClass * klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
  GstElementClass *element_class = GST_ELEMENT_CLASS (klass);
  GstVideoEncoderClass *gstvideoencoder_class = GST_VIDEO_ENCODER_CLASS (klass);

  gobject_class->get_property = gst_vtenc_get_property;
  gobject_class->set_property = gst_vtenc_set_property;
  gobject_class->finalize = gst_vtenc_finalize;

  element_class->change_state = GST_DEBUG_FUNCPTR (gst_vtenc_change_state);

  gstvideoencoder_class->start = GST_DEBUG_FUNCPTR (gst_vtenc_start);
  gstvideoencoder_class->stop = GST_DEBUG_FUNCPTR (gst_vtenc_stop);
  gstvideoencoder_class->set_format = GST_DEBUG_FUNCPTR (gst_vtenc_set_format);
  gstvideoencoder_class->handle_frame =
      GST_DEBUG_FUNCPTR (gst_vtenc_handle_frame);
  gstvideoencoder_class->finish = GST_DEBUG_FUNCPTR (gst_vtenc_finish);
  gstvideoencoder_class->flush = GST_DEBUG_FUNCPTR (gst_vtenc_flush);
  gstvideoencoder_class->sink_event = GST_DEBUG_FUNCPTR (gst_vtenc_sink_event);

  g_object_class_install_property (gobject_class, PROP_BITRATE,
      g_param_spec_uint ("bitrate", "Bitrate",
          "Target video bitrate in kbps (0 = auto)",
          0, G_MAXUINT, VTENC_DEFAULT_BITRATE,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class, PROP_ALLOW_FRAME_REORDERING,
      g_param_spec_boolean ("allow-frame-reordering", "Allow frame reordering",
          "Whether to allow frame reordering or not",
          VTENC_DEFAULT_FRAME_REORDERING,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class, PROP_REALTIME,
      g_param_spec_boolean ("realtime", "Realtime",
          "Configure the encoder for realtime output",
          VTENC_DEFAULT_REALTIME,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class, PROP_QUALITY,
      g_param_spec_double ("quality", "Quality",
          "The desired compression quality",
          0.0, 1.0, VTENC_DEFAULT_QUALITY,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class, PROP_MAX_KEYFRAME_INTERVAL,
      g_param_spec_int ("max-keyframe-interval", "Max Keyframe Interval",
          "Maximum number of frames between keyframes (0 = auto)",
          0, G_MAXINT, VTENC_DEFAULT_MAX_KEYFRAME_INTERVAL,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class,
      PROP_MAX_KEYFRAME_INTERVAL_DURATION,
      g_param_spec_uint64 ("max-keyframe-interval-duration",
          "Max Keyframe Interval Duration",
          "Maximum number of nanoseconds between keyframes (0 = no limit)", 0,
          G_MAXUINT64, VTENC_DEFAULT_MAX_KEYFRAME_INTERVAL_DURATION,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));

  /**
   * vtenc_h264:rate-control
   *
   * Since: 1.26
   */
  /**
   * vtenc_h264_hw:rate-control
   *
   * Since: 1.26
   */
  /**
   * vtenc_h265:rate-control
   *
   * Since: 1.26
   */
  /**
   * vtenc_h265_hw:rate-control
   *
   * Since: 1.26
   */
  /**
   * vtenc_prores:rate-control
   *
   * Since: 1.26
   */
  g_object_class_install_property (gobject_class, PROP_RATE_CONTROL,
      g_param_spec_enum ("rate-control", "Rate Control",
          "Desired rate control for the encoder", GST_TYPE_VTENC_RATE_CONTROL,
          GST_VTENC_RATE_CONTROL_ABR,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));

  /**
   * vtenc_h264:data-rate-limits
   *
   * Since: 1.26
   */
  /**
   * vtenc_h264_hw:data-rate-limits
   *
   * Since: 1.26
   */
  /**
   * vtenc_h265:data-rate-limits
   *
   * Since: 1.26
   */
  /**
   * vtenc_h265_hw:data-rate-limits
   *
   * Since: 1.26
   */
  /**
   * vtenc_prores:data-rate-limits
   *
   * Since: 1.26
   */
  g_object_class_install_property (gobject_class, PROP_DATA_RATE_LIMITS,
      g_param_spec_string ("data-rate-limits", "Data Rate Limits",
          "Desired bitrate in kbps averaged over a duration in seconds: "
          "bitrate,duration (0,0 = disabled)", "0,0",
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));

  /*
   * H264 doesn't support alpha components, and H265 uses a separate element for encoding
   * with alpha, so only add the preserve-alpha property for ProRes.
   *
   * MaxFrameDelayCount seems to only be supported with ProRes
   */
  if (g_strcmp0 (G_OBJECT_CLASS_NAME (klass), "vtenc_prores") == 0) {
    /**
     * vtenc_prores:max-frame-delay
     *
     * Maximum frames allowed in the compression window (-1 = unlimited)
     *
     * Since: 1.26
     */
    g_object_class_install_property (gobject_class, PROP_MAX_FRAME_DELAY,
        g_param_spec_int ("max-frame-delay", "Maximum Frame Delay",
            "Maximum frames allowed in the compression window (-1 = unlimited)",
            -1, G_MAXINT, -1,
            G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));
    /**
     * vtenc_prores:preserve-alpha
     *
     * Preserve non-opaque video alpha values from the input video when
     * compressing, else treat all alpha component as opaque.
     *
     * Since: 1.20
     */
    g_object_class_install_property (gobject_class, PROP_PRESERVE_ALPHA,
        g_param_spec_boolean ("preserve-alpha", "Preserve Video Alpha Values",
            "Video alpha values (non opaque) need to be preserved",
            VTENC_DEFAULT_PRESERVE_ALPHA,
            G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));
  }

  gst_type_mark_as_plugin_api (GST_TYPE_VTENC, 0);
  gst_type_mark_as_plugin_api (GST_TYPE_VTENC_RATE_CONTROL, 0);
}

static void
gst_vtenc_init (GstVTEnc * self)
{
  GstVTEncClass *klass = (GstVTEncClass *) G_OBJECT_GET_CLASS (self);
  CFStringRef keyframe_props_keys[] = { kVTEncodeFrameOptionKey_ForceKeyFrame };
  CFBooleanRef keyframe_props_values[] = { kCFBooleanTrue };

  self->details = GST_VTENC_CLASS_GET_CODEC_DETAILS (klass);

  /* These could be controlled by properties later */
  self->dump_properties = FALSE;
  self->dump_attributes = FALSE;
  self->latency_frames = -1;
  self->session = NULL;
  self->profile_level = NULL;
  self->have_field_order = TRUE;

  self->keyframe_props =
      CFDictionaryCreate (NULL, (const void **) keyframe_props_keys,
      (const void **) keyframe_props_values, G_N_ELEMENTS (keyframe_props_keys),
      &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);

  g_mutex_init (&self->queue_mutex);
  g_cond_init (&self->queue_cond);

  g_mutex_init (&self->encoding_mutex);
  g_cond_init (&self->encoding_cond);
}

static void
gst_vtenc_finalize (GObject * obj)
{
  GstVTEnc *self = GST_VTENC_CAST (obj);

  CFRelease (self->keyframe_props);
  g_mutex_clear (&self->queue_mutex);
  g_cond_clear (&self->queue_cond);

  g_mutex_clear (&self->encoding_mutex);
  g_cond_clear (&self->encoding_cond);

  G_OBJECT_CLASS (parent_class)->finalize (obj);
}

static void
gst_vtenc_get_property (GObject * obj, guint prop_id, GValue * value,
    GParamSpec * pspec)
{
  GstVTEnc *self = GST_VTENC_CAST (obj);

  switch (prop_id) {
    case PROP_BITRATE:
      g_value_set_uint (value, self->bitrate / 1000);
      break;
    case PROP_ALLOW_FRAME_REORDERING:
      g_value_set_boolean (value, self->allow_frame_reordering);
      break;
    case PROP_REALTIME:
      g_value_set_boolean (value, self->realtime);
      break;
    case PROP_QUALITY:
      g_value_set_double (value, self->quality);
      break;
    case PROP_MAX_KEYFRAME_INTERVAL:
      g_value_set_int (value, self->max_keyframe_interval);
      break;
    case PROP_MAX_KEYFRAME_INTERVAL_DURATION:
      g_value_set_uint64 (value, self->max_keyframe_interval_duration);
      break;
    case PROP_RATE_CONTROL:
      g_value_set_enum (value, self->rate_control);
      break;
    case PROP_DATA_RATE_LIMITS:
      GST_OBJECT_LOCK (self);
      g_value_take_string (value, g_strdup_printf ("%u,%.5f",
              self->max_bitrate / 1000, self->bitrate_window));
      GST_OBJECT_UNLOCK (self);
      break;
    case PROP_MAX_FRAME_DELAY:
      g_value_set_int (value, self->max_frame_delay);
      break;
    case PROP_PRESERVE_ALPHA:
      g_value_set_boolean (value, self->preserve_alpha);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec);
      break;
  }
}

static void
gst_vtenc_flag_reconfigure (GstVTEnc * self)
{
  if (self->session != NULL)
    g_atomic_int_set (&self->require_reconfigure, TRUE);
}

static void
gst_vtenc_set_property (GObject * obj, guint prop_id, const GValue * value,
    GParamSpec * pspec)
{
  GstVTEnc *self = GST_VTENC_CAST (obj);

  GST_OBJECT_LOCK (self);

  switch (prop_id) {
    case PROP_BITRATE:
      self->bitrate = g_value_get_uint (value) * 1000;
      gst_vtenc_flag_reconfigure (self);
      break;
    case PROP_ALLOW_FRAME_REORDERING:
      self->allow_frame_reordering = g_value_get_boolean (value);
      gst_vtenc_flag_reconfigure (self);
      break;
    case PROP_REALTIME:
      self->realtime = g_value_get_boolean (value);
      gst_vtenc_flag_reconfigure (self);
      break;
    case PROP_QUALITY:
      self->quality = g_value_get_double (value);
      gst_vtenc_flag_reconfigure (self);
      break;
    case PROP_MAX_KEYFRAME_INTERVAL:
      self->max_keyframe_interval = g_value_get_int (value);
      gst_vtenc_flag_reconfigure (self);
      break;
    case PROP_MAX_KEYFRAME_INTERVAL_DURATION:
      self->max_keyframe_interval_duration = g_value_get_uint64 (value);
      gst_vtenc_flag_reconfigure (self);
      break;
    case PROP_RATE_CONTROL:
      self->rate_control = g_value_get_enum (value);
      gst_vtenc_flag_reconfigure (self);
      break;
    case PROP_DATA_RATE_LIMITS:
    {
      guint max_bitrate;
      float window;
      const char *s = g_value_get_string (value);
      if (s && sscanf (s, "%u,%f", &max_bitrate, &window) == 2) {
        self->max_bitrate = max_bitrate * 1000;
        self->bitrate_window = window;
        gst_vtenc_flag_reconfigure (self);
      } else {
        g_warning ("Failed to parse data rate limits: '%s'", s);
      }
    }
      break;
    case PROP_MAX_FRAME_DELAY:
      self->max_frame_delay = g_value_get_int (value);
      gst_vtenc_flag_reconfigure (self);
      break;
    case PROP_PRESERVE_ALPHA:
      self->preserve_alpha = g_value_get_boolean (value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec);
      break;
  }

  GST_OBJECT_UNLOCK (self);
}

static gboolean
gst_vtenc_ensure_output_loop (GstVTEnc * self)
{
  GstPad *pad = GST_VIDEO_ENCODER_SRC_PAD (self);
  GstTask *task = GST_PAD_TASK (pad);

  return gst_task_resume (task);
}

static void
gst_vtenc_pause_output_loop (GstVTEnc * self)
{
  g_mutex_lock (&self->queue_mutex);
  self->pause_task = TRUE;
  g_cond_signal (&self->queue_cond);
  g_mutex_unlock (&self->queue_mutex);

  gst_pad_pause_task (GST_VIDEO_ENCODER_SRC_PAD (self));
  GST_DEBUG_OBJECT (self, "paused output thread");

  g_mutex_lock (&self->queue_mutex);
  self->pause_task = FALSE;
  g_mutex_unlock (&self->queue_mutex);
}

static GstFlowReturn
gst_vtenc_drain_encoder (GstVTEnc * self, gboolean flush)
{
  GST_DEBUG_OBJECT (self, "drain_encoder, flushing: %d", flush);
  OSStatus vt_status;

  /* In case of EOS before the first buffer/caps */
  if (self->session == NULL)
    return GST_FLOW_OK;

  /* Only early-return here if we're draining (as that needs to output frames).
   * Flushing doesn't care about errors from downstream. */
  if (!flush && self->downstream_ret != GST_FLOW_OK) {
    /* Makes sure the output callback won't get stuck waiting for space in the queue */
    g_mutex_lock (&self->queue_mutex);
    self->is_flushing = TRUE;
    g_cond_signal (&self->queue_cond);
    g_mutex_unlock (&self->queue_mutex);

    GST_WARNING_OBJECT (self, "Output loop stopped with error (%s), leaving",
        gst_flow_get_name (self->downstream_ret));
    return self->downstream_ret;
  }

  g_mutex_lock (&self->queue_mutex);
  if (flush) {
    GST_DEBUG_OBJECT (self, "setting flushing flag");
    self->is_flushing = TRUE;
  } else {
    GST_DEBUG_OBJECT (self, "setting draining flag");
    self->is_draining = TRUE;
  }
  g_cond_signal (&self->queue_cond);
  g_mutex_unlock (&self->queue_mutex);

  if (!gst_vtenc_ensure_output_loop (self)) {
    GST_ERROR_OBJECT (self, "Output loop failed to resume");
    return GST_FLOW_ERROR;
  }

  /* We need to unlock the stream lock here because
   * it can wait for gst_vtenc_enqueue_buffer() to
   * handle a buffer... which will take the stream
   * lock from another thread and then deadlock */
  GST_VIDEO_ENCODER_STREAM_UNLOCK (self);
  GST_DEBUG_OBJECT (self, "starting VTCompressionSessionCompleteFrames");
  vt_status =
      VTCompressionSessionCompleteFrames (self->session,
      kCMTimePositiveInfinity);
  GST_DEBUG_OBJECT (self, "VTCompressionSessionCompleteFrames ended");
  if (vt_status != noErr) {
    GST_WARNING_OBJECT (self, "VTCompressionSessionCompleteFrames returned %d",
        (int) vt_status);
  }

  /* If self->first_frame is still true, that means we never got more than 1 frame
   * and that's all for now, so let's push it manually now */
  if (self->first_frame) {
    GST_DEBUG_OBJECT (self,
        "first frame never pushed downstream, sending to output loop");

    g_mutex_lock (&self->queue_mutex);
    gst_vec_deque_push_tail (self->output_queue, self->first_frame);
    self->first_frame = NULL;
    g_cond_signal (&self->queue_cond);
    g_mutex_unlock (&self->queue_mutex);
  }

  /* This will only pause after all frames are out because is_flushing/is_draining=TRUE */
  gst_vtenc_pause_output_loop (self);
  GST_VIDEO_ENCODER_STREAM_LOCK (self);

  if (flush) {
    GST_DEBUG_OBJECT (self, "clearing flushing flag");
    self->is_flushing = FALSE;
  } else {
    GST_DEBUG_OBJECT (self, "clearing draining flag");
    self->is_draining = FALSE;
  }

  if (self->downstream_ret == GST_FLOW_OK)
    GST_DEBUG_OBJECT (self, "buffer queue cleaned");
  else
    GST_DEBUG_OBJECT (self,
        "buffer queue not cleaned, output thread returned %s",
        gst_flow_get_name (self->downstream_ret));

  return self->downstream_ret;
}

static gboolean
gst_vtenc_start (GstVideoEncoder * enc)
{
  GstVTEnc *self = GST_VTENC_CAST (enc);

  /* DTS can be negative if b-frames are enabled */
  gst_video_encoder_set_min_pts (enc, GST_SECOND * 60 * 60 * 1000);
  self->dts_offset = GST_CLOCK_TIME_NONE;

  self->is_flushing = FALSE;
  self->is_draining = FALSE;
  self->downstream_ret = GST_FLOW_OK;
  g_atomic_int_set (&self->require_restart, FALSE);
  g_atomic_int_set (&self->require_reconfigure, FALSE);

  self->output_queue = gst_vec_deque_new (VTENC_OUTPUT_QUEUE_SIZE);
  /* Set clear_func to unref all remaining frames in gst_vec_deque_free() */
  gst_vec_deque_set_clear_func (self->output_queue,
      (GDestroyNotify) gst_video_codec_frame_unref);

  /* Create the output task, but pause it immediately */
  self->pause_task = TRUE;
  if (!gst_pad_start_task (GST_VIDEO_ENCODER_SRC_PAD (enc),
          (GstTaskFunction) gst_vtenc_output_loop, self, NULL)) {
    GST_ERROR_OBJECT (self, "failed to start output thread");
    return FALSE;
  }
  /* This blocks until the loop actually pauses */
  gst_pad_pause_task (GST_VIDEO_ENCODER_SRC_PAD (enc));
  self->pause_task = FALSE;

  return TRUE;
}

static gboolean
gst_vtenc_stop (GstVideoEncoder * enc)
{
  GstVTEnc *self = GST_VTENC_CAST (enc);

  GST_VIDEO_ENCODER_STREAM_LOCK (self);
  gst_vtenc_flush (enc);
  self->downstream_ret = GST_FLOW_FLUSHING;
  GST_VIDEO_ENCODER_STREAM_UNLOCK (self);

  gst_pad_stop_task (GST_VIDEO_ENCODER_SRC_PAD (enc));

  GST_OBJECT_LOCK (self);
  gst_vtenc_destroy_session (self, &self->session);
  GST_OBJECT_UNLOCK (self);

  self->negotiate_downstream = TRUE;
  self->is_flushing = TRUE;
  self->is_draining = FALSE;

  if (self->profile_level)
    CFRelease (self->profile_level);
  self->profile_level = NULL;

  if (self->input_state)
    gst_video_codec_state_unref (self->input_state);
  self->input_state = NULL;

  if (self->first_frame)
    gst_video_codec_frame_unref (self->first_frame);
  self->first_frame = NULL;

  self->video_info.width = self->video_info.height = 0;
  self->video_info.fps_n = self->video_info.fps_d = 0;

  gst_vec_deque_free (self->output_queue);
  self->output_queue = NULL;

  return TRUE;
}

static gboolean
gst_vtenc_h264_parse_profile_level_key (GstVTEnc * self, const gchar * profile,
    const gchar * level_arg)
{
  char level[64];
  gchar *key = NULL;

  if (profile == NULL)
    profile = "main";
  if (level_arg == NULL)
    level_arg = "AutoLevel";
  strlcpy (level, level_arg, sizeof (level));

  if (!strcmp (profile, "constrained-baseline") ||
      !strcmp (profile, "baseline")) {
    profile = "Baseline";
    self->h264_profile = GST_H264_PROFILE_BASELINE;
  } else if (g_str_has_prefix (profile, "high")) {
    profile = "High";
    self->h264_profile = GST_H264_PROFILE_HIGH;
  } else if (!strcmp (profile, "main")) {
    profile = "Main";
    self->h264_profile = GST_H264_PROFILE_MAIN;
  } else {
    GST_ERROR_OBJECT (self, "invalid profile: %s", profile);
    return FALSE;
  }

  if (strlen (level) == 1) {
    level[1] = '_';
    level[2] = '0';
    level[3] = 0;
  } else if (strlen (level) == 3) {
    level[1] = '_';
  }

  key = g_strdup_printf ("H264_%s_%s", profile, level);
  self->profile_level =
      CFStringCreateWithBytes (NULL, (const guint8 *) key, strlen (key),
      kCFStringEncodingASCII, 0);
  GST_INFO_OBJECT (self, "negotiated profile and level %s", key);

  g_free (key);

  return TRUE;
}

static gboolean
gst_vtenc_hevc_parse_profile_level_key (GstVTEnc * self, const gchar * profile,
    const gchar * level_arg)
{
  gchar *key = NULL;

  if (profile == NULL || !strcmp (profile, "main"))
    profile = "Main";
  else if (!strcmp (profile, "main-10"))
    profile = "Main10";
  else if (!strcmp (profile, "main-422-10"))
    /* TODO: this should probably be guarded with a version check (macOS 12.3+ / iOS 15.4+)
     * https://developer.apple.com/documentation/videotoolbox/kvtprofilelevel_hevc_main10_autolevel */
    profile = "Main42210";
  else {
    GST_ERROR_OBJECT (self, "invalid profile: %s", profile);
    return FALSE;
  }

  /* VT does not support specific levels for HEVC */
  key = g_strdup_printf ("HEVC_%s_AutoLevel", profile);
  self->profile_level =
      CFStringCreateWithBytes (NULL, (const guint8 *) key, strlen (key),
      kCFStringEncodingASCII, 0);
  GST_INFO_OBJECT (self, "negotiated profile and level %s", key);

  g_free (key);
  return TRUE;
}

static gboolean
gst_vtenc_negotiate_profile_and_level (GstVTEnc * self, GstStructure * s)
{
  const gchar *profile = gst_structure_get_string (s, "profile");
  const gchar *level = gst_structure_get_string (s, "level");

  if (self->profile_level)
    CFRelease (self->profile_level);

  if (self->specific_format_id == kCMVideoCodecType_HEVC ||
      self->specific_format_id == kCMVideoCodecType_HEVCWithAlpha) {
    return gst_vtenc_hevc_parse_profile_level_key (self, profile, level);
  } else {
    return gst_vtenc_h264_parse_profile_level_key (self, profile, level);
  }
}

static gboolean
gst_vtenc_negotiate_prores_variant (GstVTEnc * self, GstStructure * s)
{
  const char *variant = gst_structure_get_string (s, "variant");
  CMVideoCodecType codec_type =
      gst_vtutil_codec_type_from_prores_variant (variant);

  if (codec_type == GST_kCMVideoCodecType_Some_AppleProRes) {
    GST_ERROR_OBJECT (self, "unsupported prores variant: %s", variant);
    return FALSE;
  }

  self->specific_format_id = codec_type;
  return TRUE;
}

static gboolean
gst_vtenc_negotiate_specific_format_details (GstVideoEncoder * enc)
{
  GstVTEnc *self = GST_VTENC_CAST (enc);
  GstCaps *allowed_caps = NULL;
  gboolean ret = TRUE;

  allowed_caps = gst_pad_get_allowed_caps (GST_VIDEO_ENCODER_SRC_PAD (enc));
  if (allowed_caps) {
    GstStructure *s;

    if (gst_caps_is_empty (allowed_caps)) {
      GST_ERROR_OBJECT (self, "no allowed downstream caps");
      goto fail;
    }

    allowed_caps = gst_caps_make_writable (allowed_caps);
    allowed_caps = gst_caps_fixate (allowed_caps);
    s = gst_caps_get_structure (allowed_caps, 0);
    switch (self->details->format_id) {
      case kCMVideoCodecType_H264:
        self->specific_format_id = kCMVideoCodecType_H264;
        if (!gst_vtenc_negotiate_profile_and_level (self, s))
          goto fail;
        break;
      case kCMVideoCodecType_HEVC:
        self->specific_format_id = kCMVideoCodecType_HEVC;
        if (!gst_vtenc_negotiate_profile_and_level (self, s))
          goto fail;
        break;
      case kCMVideoCodecType_HEVCWithAlpha:
        self->specific_format_id = kCMVideoCodecType_HEVCWithAlpha;
        if (!gst_vtenc_negotiate_profile_and_level (self, s))
          goto fail;
        break;
      case GST_kCMVideoCodecType_Some_AppleProRes:
        if (g_strcmp0 (self->details->mimetype, "video/x-prores") != 0) {
          GST_ERROR_OBJECT (self, "format_id == %i mimetype must be Apple "
              "ProRes", GST_kCMVideoCodecType_Some_AppleProRes);
          goto fail;
        }
        if (!gst_vtenc_negotiate_prores_variant (self, s))
          goto fail;
        break;
      default:
        g_assert_not_reached ();
    }
  }

out:
  if (allowed_caps)
    gst_caps_unref (allowed_caps);

  return ret;

fail:
  ret = FALSE;
  goto out;
}

static gboolean
gst_vtenc_set_format (GstVideoEncoder * enc, GstVideoCodecState * state)
{
  GstVTEnc *self = GST_VTENC_CAST (enc);
  VTCompressionSessionRef session;

  if (self->input_state) {
    gst_vtenc_drain_encoder (self, FALSE);
    gst_video_codec_state_unref (self->input_state);
  }

  GST_OBJECT_LOCK (self);
  gst_vtenc_destroy_session (self, &self->session);
  GST_OBJECT_UNLOCK (self);

  self->input_state = gst_video_codec_state_ref (state);
  self->video_info = state->info;

  if (!gst_vtenc_negotiate_specific_format_details (enc))
    return FALSE;

  self->negotiate_downstream = TRUE;

  session = gst_vtenc_create_session (self);
  GST_OBJECT_LOCK (self);
  self->session = session;
  GST_OBJECT_UNLOCK (self);

  g_atomic_int_set (&self->require_restart, FALSE);
  g_atomic_int_set (&self->require_reconfigure, FALSE);

  return session != NULL;
}

static gboolean
gst_vtenc_is_negotiated (GstVTEnc * self)
{
  return self->session && self->video_info.width != 0;
}

/*
 * When the image is opaque but the output ProRes format has an alpha
 * component (4 component, 32 bits per pixel), Apple requires that we signal
 * that it should be ignored by setting the depth to 24 bits per pixel. Not
 * doing so causes the encoded files to fail validation.
 *
 * So we set that in the caps and qtmux sets the depth value in the container,
 * which will be read by demuxers so that decoders can skip those bytes
 * entirely. qtdemux does this, but vtdec does not use this information at
 * present.
 */
static gboolean
gst_vtenc_signal_ignored_alpha_component (GstVTEnc * self)
{
  if (self->preserve_alpha)
    return FALSE;
  if (self->specific_format_id == kCMVideoCodecType_AppleProRes4444XQ ||
      self->specific_format_id == kCMVideoCodecType_AppleProRes4444)
    return TRUE;
  return FALSE;
}

static gboolean
gst_vtenc_negotiate_downstream (GstVTEnc * self, CMSampleBufferRef sbuf)
{
  gboolean result;
  GstCaps *caps;
  GstStructure *s;
  GstVideoCodecState *state;

  caps = gst_pad_get_pad_template_caps (GST_VIDEO_ENCODER_SRC_PAD (self));
  caps = gst_caps_make_writable (caps);
  s = gst_caps_get_structure (caps, 0);
  gst_structure_set (s,
      "width", G_TYPE_INT, self->video_info.width,
      "height", G_TYPE_INT, self->video_info.height,
      "framerate", GST_TYPE_FRACTION,
      self->video_info.fps_n, self->video_info.fps_d, NULL);

  switch (self->details->format_id) {
    case kCMVideoCodecType_H264:
    case kCMVideoCodecType_HEVC:
    case kCMVideoCodecType_HEVCWithAlpha:
    {
      CMFormatDescriptionRef fmt;
      CFDictionaryRef atoms;
      CFStringRef boxKey;
      CFDataRef box;
      guint8 *codec_data;
      gsize codec_data_size;
      GstBuffer *codec_data_buf;

      fmt = CMSampleBufferGetFormatDescription (sbuf);
      atoms = CMFormatDescriptionGetExtension (fmt,
          kCMFormatDescriptionExtension_SampleDescriptionExtensionAtoms);

      if (self->details->format_id == kCMVideoCodecType_HEVC ||
          self->details->format_id == kCMVideoCodecType_HEVCWithAlpha)
        boxKey =
            CFStringCreateWithCString (NULL, "hvcC", kCFStringEncodingUTF8);
      else
        boxKey =
            CFStringCreateWithCString (NULL, "avcC", kCFStringEncodingUTF8);

      box = CFDictionaryGetValue (atoms, boxKey);
      CFRelease (boxKey);
      codec_data_size = CFDataGetLength (box);
      codec_data = g_malloc (codec_data_size);
      CFDataGetBytes (box, CFRangeMake (0, codec_data_size), codec_data);
      codec_data_buf = gst_buffer_new_wrapped (codec_data, codec_data_size);

      gst_structure_set (s, "codec_data", GST_TYPE_BUFFER, codec_data_buf,
          NULL);

      if (self->details->format_id == kCMVideoCodecType_HEVC ||
          self->details->format_id == kCMVideoCodecType_HEVCWithAlpha) {
        if (codec_data_size < 1 + 12) {
          GST_ERROR_OBJECT (self,
              "Codec data malformed, can't parse profile and level");
          gst_buffer_unref (codec_data_buf);
          gst_caps_unref (caps);
          return FALSE;
        }

        gst_codec_utils_h265_caps_set_level_tier_and_profile (caps,
            &codec_data[1], 12);
      } else {
        guint8 sps[3];

        if (codec_data_size < 1 + 3) {
          GST_ERROR_OBJECT (self,
              "Codec data malformed, can't parse profile and level");
          gst_buffer_unref (codec_data_buf);
          gst_caps_unref (caps);
          return FALSE;
        }

        sps[0] = codec_data[1];
        sps[1] = codec_data[2] & ~0xDF;
        sps[2] = codec_data[3];
        gst_codec_utils_h264_caps_set_level_and_profile (caps, sps, 3);
      }

      gst_buffer_unref (codec_data_buf);
    }
      break;
    case GST_kCMVideoCodecType_Some_AppleProRes:
      gst_structure_set (s, "variant", G_TYPE_STRING,
          gst_vtutil_codec_type_to_prores_variant (self->specific_format_id),
          NULL);
      if (gst_vtenc_signal_ignored_alpha_component (self))
        gst_structure_set (s, "depth", G_TYPE_INT, 24, NULL);
      break;
    default:
      g_assert_not_reached ();
  }

  state =
      gst_video_encoder_set_output_state (GST_VIDEO_ENCODER_CAST (self), caps,
      self->input_state);
  gst_video_codec_state_unref (state);
  result = gst_video_encoder_negotiate (GST_VIDEO_ENCODER_CAST (self));

  return result;
}

static GstFlowReturn
gst_vtenc_handle_frame (GstVideoEncoder * enc, GstVideoCodecFrame * frame)
{
  GstVTEnc *self = GST_VTENC_CAST (enc);

  if (!gst_vtenc_is_negotiated (self))
    goto not_negotiated;

  return gst_vtenc_encode_frame (self, frame);

not_negotiated:
  gst_video_encoder_release_frame (enc, frame);
  return GST_FLOW_NOT_NEGOTIATED;
}

static gboolean
gst_vtenc_sink_event (GstVideoEncoder * enc, GstEvent * event)
{
  GstVTEnc *self = GST_VTENC_CAST (enc);
  GstEventType type = GST_EVENT_TYPE (event);
  gboolean ret;

  switch (type) {
    case GST_EVENT_FLUSH_START:
      GST_DEBUG_OBJECT (self, "flush start received, setting flushing flag");

      g_mutex_lock (&self->queue_mutex);
      self->is_flushing = TRUE;
      g_cond_signal (&self->queue_cond);
      g_mutex_unlock (&self->queue_mutex);
      break;
    default:
      break;
  }

  ret = GST_VIDEO_ENCODER_CLASS (parent_class)->sink_event (enc, event);

  switch (type) {
    case GST_EVENT_FLUSH_STOP:
      /* The base class handles this event and calls _flush().
       * We can then safely reset the flushing flag. */
      GST_DEBUG_OBJECT (self, "flush stop received, removing flushing flag");

      g_mutex_lock (&self->queue_mutex);
      self->is_flushing = FALSE;
      g_mutex_unlock (&self->queue_mutex);
      break;
    default:
      break;
  }

  return ret;
}

static GstStateChangeReturn
gst_vtenc_change_state (GstElement * element, GstStateChange transition)
{
  GstVTEnc *self = GST_VTENC_CAST (element);

  if (transition == GST_STATE_CHANGE_PAUSED_TO_READY) {
    GST_DEBUG_OBJECT (self, "pausing output loop on PAUSED->READY");
    gst_vtenc_pause_output_loop (self);
  }

  return GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
}

static GstFlowReturn
gst_vtenc_finish (GstVideoEncoder * enc)
{
  GstVTEnc *self = GST_VTENC_CAST (enc);

  GST_DEBUG_OBJECT (self, "finish");
  return gst_vtenc_drain_encoder (self, FALSE);
}

static gboolean
gst_vtenc_flush (GstVideoEncoder * enc)
{
  GstVTEnc *self = GST_VTENC_CAST (enc);

  GST_DEBUG_OBJECT (self, "flush");
  gst_vtenc_drain_encoder (self, TRUE);

  self->downstream_ret = GST_FLOW_OK;

  return TRUE;
}

static void
gst_vtenc_set_colorimetry (GstVTEnc * self, VTCompressionSessionRef session)
{
  OSStatus status;
  CFStringRef primaries = NULL, transfer = NULL, matrix = NULL;
  GstVideoColorimetry cm = GST_VIDEO_INFO_COLORIMETRY (&self->video_info);

  /*
   * https://developer.apple.com/documentation/corevideo/cvimagebuffer/image_buffer_ycbcr_matrix_constants
   */
  switch (cm.matrix) {
    case GST_VIDEO_COLOR_MATRIX_BT709:
      matrix = kCVImageBufferYCbCrMatrix_ITU_R_709_2;
      break;
    case GST_VIDEO_COLOR_MATRIX_BT601:
      matrix = kCVImageBufferYCbCrMatrix_ITU_R_601_4;
      break;
    case GST_VIDEO_COLOR_MATRIX_SMPTE240M:
      matrix = kCVImageBufferYCbCrMatrix_SMPTE_240M_1995;
      break;
    case GST_VIDEO_COLOR_MATRIX_BT2020:
      matrix = kCVImageBufferYCbCrMatrix_ITU_R_2020;
      break;
    default:
      GST_WARNING_OBJECT (self, "Unsupported color matrix %u", cm.matrix);
  }

  /*
   * https://developer.apple.com/documentation/corevideo/cvimagebuffer/image_buffer_transfer_function_constants
   */
  switch (cm.transfer) {
    case GST_VIDEO_TRANSFER_BT709:
    case GST_VIDEO_TRANSFER_BT601:
    case GST_VIDEO_TRANSFER_UNKNOWN:
      transfer = kCVImageBufferTransferFunction_ITU_R_709_2;
      break;
    case GST_VIDEO_TRANSFER_SMPTE240M:
      transfer = kCVImageBufferTransferFunction_SMPTE_240M_1995;
      break;
    case GST_VIDEO_TRANSFER_BT2020_12:
      transfer = kCVImageBufferTransferFunction_ITU_R_2020;
      break;
    case GST_VIDEO_TRANSFER_SRGB:
      if (__builtin_available (macOS 10.13, *))
        transfer = kCVImageBufferTransferFunction_sRGB;
      else
        GST_WARNING_OBJECT (self, "macOS version is too old, the sRGB transfer "
            "function is not available");
      break;
    case GST_VIDEO_TRANSFER_SMPTE2084:
      if (__builtin_available (macOS 10.13, *))
        transfer = kCVImageBufferTransferFunction_SMPTE_ST_2084_PQ;
      else
        GST_WARNING_OBJECT (self, "macOS version is too old, the SMPTE2084 "
            "transfer function is not available");
      break;
    case GST_VIDEO_TRANSFER_ARIB_STD_B67:
      if (__builtin_available (macOS 10.13, *))
        transfer = kCVImageBufferTransferFunction_ITU_R_2100_HLG;
      else
        GST_WARNING_OBJECT (self,
            "macOS version is too old, the ITU-R BT.2100-1 "
            "transfer function is not available");
      break;
    default:
      GST_WARNING_OBJECT (self, "Unsupported color transfer %u", cm.transfer);
  }

  /*
   * https://developer.apple.com/documentation/corevideo/cvimagebuffer/image_buffer_color_primaries_constants
   */
  switch (cm.primaries) {
    case GST_VIDEO_COLOR_PRIMARIES_BT709:
      primaries = kCVImageBufferColorPrimaries_ITU_R_709_2;
      break;
    case GST_VIDEO_COLOR_PRIMARIES_SMPTE170M:
    case GST_VIDEO_COLOR_PRIMARIES_SMPTE240M:
      primaries = kCVImageBufferColorPrimaries_SMPTE_C;
      break;
    case GST_VIDEO_COLOR_PRIMARIES_BT2020:
      primaries = kCVImageBufferColorPrimaries_ITU_R_2020;
      break;
    case GST_VIDEO_COLOR_PRIMARIES_SMPTERP431:
      primaries = kCVImageBufferColorPrimaries_DCI_P3;
      break;
    case GST_VIDEO_COLOR_PRIMARIES_SMPTEEG432:
      primaries = kCVImageBufferColorPrimaries_P3_D65;
      break;
    case GST_VIDEO_COLOR_PRIMARIES_EBU3213:
      primaries = kCVImageBufferColorPrimaries_EBU_3213;
      break;
    default:
      GST_WARNING_OBJECT (self, "Unsupported color primaries %u", cm.primaries);
  }

  if (primaries) {
    status = VTSessionSetProperty (session,
        kVTCompressionPropertyKey_ColorPrimaries, primaries);
    GST_DEBUG_OBJECT (self, "kVTCompressionPropertyKey_ColorPrimaries =>"
        "%d", status);
  }

  if (transfer) {
    status = VTSessionSetProperty (session,
        kVTCompressionPropertyKey_TransferFunction, transfer);
    GST_DEBUG_OBJECT (self, "kVTCompressionPropertyKey_TransferFunction =>"
        "%d", status);
  }

  if (matrix) {
    status = VTSessionSetProperty (session,
        kVTCompressionPropertyKey_YCbCrMatrix, matrix);
    GST_DEBUG_OBJECT (self, "kVTCompressionPropertyKey_YCbCrMatrix => %d",
        status);
  }
}

static VTCompressionSessionRef
gst_vtenc_create_session (GstVTEnc * self)
{
  VTCompressionSessionRef session = NULL;
  CFMutableDictionaryRef encoder_spec = NULL, pb_attrs = NULL;
  OSStatus status;
  const GstVTEncoderDetails *codec_details =
      GST_VTENC_CLASS_GET_CODEC_DETAILS (G_OBJECT_GET_CLASS (self));

#if TARGET_OS_OSX
  /* Apple's M1 hardware encoding fails when provided with an interlaced ProRes source.
   * It's most likely a bug in VideoToolbox, as no such limitation has been officially mentioned anywhere.
   * For now let's disable HW encoding entirely when such case occurs. */
  gboolean enable_hw = !(GST_VIDEO_INFO_IS_INTERLACED (&self->video_info)
      && codec_details->format_id == GST_kCMVideoCodecType_Some_AppleProRes);

  if (!enable_hw)
    GST_WARNING_OBJECT (self,
        "Interlaced content detected, disabling HW-accelerated encoding due to https://gitlab.freedesktop.org/gstreamer/gstreamer/-/issues/1429");

  encoder_spec =
      CFDictionaryCreateMutable (NULL, 0, &kCFTypeDictionaryKeyCallBacks,
      &kCFTypeDictionaryValueCallBacks);
  gst_vtutil_dict_set_boolean (encoder_spec,
      kVTVideoEncoderSpecification_EnableHardwareAcceleratedVideoEncoder,
      enable_hw);
  if (codec_details->require_hardware)
    gst_vtutil_dict_set_boolean (encoder_spec,
        kVTVideoEncoderSpecification_RequireHardwareAcceleratedVideoEncoder,
        TRUE);
#endif

  /* This was set in gst_vtenc_negotiate_specific_format_details() */
  g_assert_cmpint (self->specific_format_id, !=, 0);

  if (self->profile_level) {
    /* If there's no B-frames, the DTS offset doesn't need to be calculated
     * (kVTCompressionPropertyKey_AllowFrameReordering enables B-Frames) */
    if (!self->allow_frame_reordering
        || CFStringHasPrefix (self->profile_level, CFSTR ("H264_Baseline"))) {
      self->dts_offset = 0;
    } else if (self->video_info.fps_n == 0) {
      GST_ERROR_OBJECT (self,
          "Variable framerate is not supported with B-Frames");
      goto beach;
    }

    pb_attrs = CFDictionaryCreateMutable (NULL, 0,
        &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    gst_vtutil_dict_set_i32 (pb_attrs, kCVPixelBufferWidthKey,
        self->video_info.width);
    gst_vtutil_dict_set_i32 (pb_attrs, kCVPixelBufferHeightKey,
        self->video_info.height);
  }

  status = VTCompressionSessionCreate (NULL,
      self->video_info.width, self->video_info.height,
      self->specific_format_id, encoder_spec, pb_attrs, NULL,
      gst_vtenc_session_output_callback, self, &session);
  GST_INFO_OBJECT (self, "VTCompressionSessionCreate for %d x %d => %d",
      self->video_info.width, self->video_info.height, (int) status);
  if (status != noErr) {
    GST_ERROR_OBJECT (self, "VTCompressionSessionCreate() returned: %d",
        (int) status);
    goto beach;
  }

  if (self->profile_level) {
    gst_vtenc_session_configure_expected_framerate (self, session,
        (gdouble) self->video_info.fps_n / (gdouble) self->video_info.fps_d);

    /*
     * https://developer.apple.com/documentation/videotoolbox/kvtcompressionpropertykey_profilelevel
     */
    status = VTSessionSetProperty (session,
        kVTCompressionPropertyKey_ProfileLevel, self->profile_level);
    GST_DEBUG_OBJECT (self, "kVTCompressionPropertyKey_ProfileLevel => %d",
        (int) status);

    status = VTSessionSetProperty (session,
        kVTCompressionPropertyKey_AllowTemporalCompression, kCFBooleanTrue);
    GST_DEBUG_OBJECT (self,
        "kVTCompressionPropertyKey_AllowTemporalCompression => %d",
        (int) status);

    gst_vtenc_session_configure_max_keyframe_interval (self, session,
        self->max_keyframe_interval);
    gst_vtenc_session_configure_max_keyframe_interval_duration (self, session,
        self->max_keyframe_interval_duration / ((gdouble) GST_SECOND));
    if (codec_details->format_id == GST_kCMVideoCodecType_Some_AppleProRes)
      gst_vtenc_session_configure_max_frame_delay (self, session,
          self->max_frame_delay);

    gst_vtenc_session_configure_bitrate (self, session);
  }

  /* Force encoder to not preserve alpha with 4444(XQ) ProRes formats if
   * requested */
  if (!self->preserve_alpha &&
      (self->specific_format_id == kCMVideoCodecType_AppleProRes4444XQ ||
          self->specific_format_id == kCMVideoCodecType_AppleProRes4444)) {
    status = VTSessionSetProperty (session,
        gstVTCodecPropertyKey_PreserveAlphaChannel, CFSTR ("NO"));
    GST_DEBUG_OBJECT (self, "kVTCodecPropertyKey_PreserveAlphaChannel => %d",
        (int) status);
  }

  gst_vtenc_set_colorimetry (self, session);

  /* Interlacing */
  switch (GST_VIDEO_INFO_INTERLACE_MODE (&self->video_info)) {
    case GST_VIDEO_INTERLACE_MODE_PROGRESSIVE:
      gst_vtenc_session_configure_property_int (self, session,
          kVTCompressionPropertyKey_FieldCount, 1);
      break;
    case GST_VIDEO_INTERLACE_MODE_INTERLEAVED:
      gst_vtenc_session_configure_property_int (self, session,
          kVTCompressionPropertyKey_FieldCount, 2);
      switch (GST_VIDEO_INFO_FIELD_ORDER (&self->video_info)) {
        case GST_VIDEO_FIELD_ORDER_TOP_FIELD_FIRST:
          status = VTSessionSetProperty (session,
              kVTCompressionPropertyKey_FieldDetail,
              kCMFormatDescriptionFieldDetail_TemporalTopFirst);
          GST_DEBUG_OBJECT (self, "kVTCompressionPropertyKey_FieldDetail "
              "TemporalTopFirst => %d", (int) status);
          break;
        case GST_VIDEO_FIELD_ORDER_BOTTOM_FIELD_FIRST:
          status = VTSessionSetProperty (session,
              kVTCompressionPropertyKey_FieldDetail,
              kCMFormatDescriptionFieldDetail_TemporalBottomFirst);
          GST_DEBUG_OBJECT (self, "kVTCompressionPropertyKey_FieldDetail "
              "TemporalBottomFirst => %d", (int) status);
          break;
        case GST_VIDEO_FIELD_ORDER_UNKNOWN:
          GST_INFO_OBJECT (self, "Unknown field order for interleaved content, "
              "will check first buffer");
          self->have_field_order = FALSE;
      }
      break;
    default:
      /* Caps negotiation should prevent this */
      g_assert_not_reached ();
  }

  gst_vtenc_session_configure_realtime (self, session, self->realtime);
  gst_vtenc_session_configure_allow_frame_reordering (self, session,
      self->allow_frame_reordering);
  if (codec_details->format_id != GST_kCMVideoCodecType_Some_AppleProRes)
    gst_vtenc_session_configure_property_double (self, session,
        kVTCompressionPropertyKey_Quality, self->quality);

  if (self->dump_properties) {
    gst_vtenc_session_dump_properties (self, session);
    self->dump_properties = FALSE;
  }
#if !TARGET_OS_WATCH
  if (__builtin_available (ios 8.0, macos 10.9, tvos 10.2, visionos 1.0, *)) {
    status = VTCompressionSessionPrepareToEncodeFrames (session);
    if (status != noErr) {
      GST_ERROR_OBJECT (self,
          "VTCompressionSessionPrepareToEncodeFrames() returned: %d",
          (int) status);
    }
  }
#endif

beach:
  if (encoder_spec)
    CFRelease (encoder_spec);
  if (pb_attrs)
    CFRelease (pb_attrs);

  return session;
}

static void
gst_vtenc_destroy_session (GstVTEnc * self, VTCompressionSessionRef * session)
{
  GST_DEBUG_OBJECT (self, "Destroying VT session");
  VTCompressionSessionInvalidate (*session);
  if (*session != NULL) {
    CFRelease (*session);
    *session = NULL;
  }
}

typedef struct
{
  GstVTEnc *self;
  VTCompressionSessionRef session;
} GstVTDumpPropCtx;

static void
gst_vtenc_session_dump_property (CFStringRef prop_name,
    CFDictionaryRef prop_attrs, GstVTDumpPropCtx * dpc)
{
  gchar *name_str;
  CFTypeRef prop_value;
  OSStatus status;

  name_str = gst_vtutil_string_to_utf8 (prop_name);
  if (dpc->self->dump_attributes) {
    gchar *attrs_str;

    attrs_str = gst_vtutil_object_to_string (prop_attrs);
    GST_DEBUG_OBJECT (dpc->self, "%s = %s", name_str, attrs_str);
    g_free (attrs_str);
  }

  status = VTSessionCopyProperty (dpc->session, prop_name, NULL, &prop_value);
  if (status == noErr) {
    gchar *value_str;

    value_str = gst_vtutil_object_to_string (prop_value);
    GST_DEBUG_OBJECT (dpc->self, "%s = %s", name_str, value_str);
    g_free (value_str);

    if (prop_value != NULL)
      CFRelease (prop_value);
  } else {
    GST_DEBUG_OBJECT (dpc->self, "%s = <failed to query: %d>",
        name_str, (int) status);
  }

  g_free (name_str);
}

static void
gst_vtenc_session_dump_properties (GstVTEnc * self,
    VTCompressionSessionRef session)
{
  GstVTDumpPropCtx dpc = { self, session };
  CFDictionaryRef dict;
  OSStatus status;

  status = VTSessionCopySupportedPropertyDictionary (session, &dict);
  if (status != noErr)
    goto error;
  CFDictionaryApplyFunction (dict,
      (CFDictionaryApplierFunction) gst_vtenc_session_dump_property, &dpc);
  CFRelease (dict);

  return;

error:
  GST_WARNING_OBJECT (self, "failed to dump properties");
}

static void
gst_vtenc_session_configure_expected_framerate (GstVTEnc * self,
    VTCompressionSessionRef session, gdouble framerate)
{
  gst_vtenc_session_configure_property_double (self, session,
      kVTCompressionPropertyKey_ExpectedFrameRate, framerate);
}

static void
gst_vtenc_session_configure_max_keyframe_interval (GstVTEnc * self,
    VTCompressionSessionRef session, gint interval)
{
  gst_vtenc_session_configure_property_int (self, session,
      kVTCompressionPropertyKey_MaxKeyFrameInterval, interval);
}

static void
gst_vtenc_session_configure_max_keyframe_interval_duration (GstVTEnc * self,
    VTCompressionSessionRef session, gdouble duration)
{
  gst_vtenc_session_configure_property_double (self, session,
      kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration, duration);
}

static void
gst_vtenc_session_configure_max_frame_delay (GstVTEnc * self,
    VTCompressionSessionRef session, int delay)
{
  if (delay < 0)
    delay = kVTUnlimitedFrameDelayCount;
  gst_vtenc_session_configure_property_int (self, session,
      kVTCompressionPropertyKey_MaxFrameDelayCount, delay);
}

static void
gst_vtenc_session_configure_bitrate (GstVTEnc * self,
    VTCompressionSessionRef session)
{
  gboolean emulate_cbr = FALSE;
  double bitrate_window = self->bitrate_window;
  guint max_bitrate = self->max_bitrate;
  guint bitrate = self->bitrate;
  CFStringRef key = kVTCompressionPropertyKey_AverageBitRate;

  if (self->rate_control == GST_VTENC_RATE_CONTROL_CBR) {
#ifdef __aarch64__
    /*
     * In addition to the OS requirements, CBR also requires Apple Silicon
     */
    if (__builtin_available (macOS 13.0, iOS 16.0, tvOS 16.0, visionOS 1.0, *)) {
      key = kVTCompressionPropertyKey_ConstantBitRate;
    } else
#endif
    {
      GST_INFO_OBJECT (self, "CBR is unsupported on your system, emulating "
          "with custom data rate limits");
      emulate_cbr = TRUE;
      max_bitrate = bitrate;
      gst_util_fraction_to_double (self->video_info.fps_d,
          self->video_info.fps_n, &bitrate_window);
    }
  }

  if (max_bitrate > 0 && bitrate_window > 0) {
    if (self->rate_control == GST_VTENC_RATE_CONTROL_CBR &&
        self->max_bitrate > 0 && self->bitrate_window > 0)
      GST_INFO_OBJECT (self, "Ignoring data-rate-limits property, CBR mode is "
          "enabled");

    if (key == kVTCompressionPropertyKey_AverageBitRate) {
      /* Convert to bytes */
      int size = (max_bitrate * bitrate_window) / 8;

      CFNumberRef cf_size = CFNumberCreate (NULL, kCFNumberIntType, &size);
      CFNumberRef cf_window = CFNumberCreate (NULL, kCFNumberFloatType,
          &bitrate_window);

      CFTypeRef values[2] = { cf_size, cf_window };
      CFArrayRef data = CFArrayCreate (NULL, values, 2, &kCFTypeArrayCallBacks);

      OSStatus code = VTSessionSetProperty (session,
          kVTCompressionPropertyKey_DataRateLimits, data);
      if (code != noErr)
        GST_WARNING_OBJECT (self,
            "FAILED: kVTCompressionPropertyKey_DataRateLimits %i, %f => %i",
            size, self->bitrate_window, code);
      else
        GST_DEBUG_OBJECT (self, "kVTCompressionPropertyKey_DataRateLimits "
            "%i, %f => %i", size, self->bitrate_window, code);

      CFRelease (cf_size);
      CFRelease (cf_window);
      CFRelease (data);
    }
  }

  if (!emulate_cbr)
    gst_vtenc_session_configure_property_int (self, session, key, bitrate);
}

static void
gst_vtenc_session_configure_allow_frame_reordering (GstVTEnc * self,
    VTCompressionSessionRef session, gboolean allow_frame_reordering)
{
  VTSessionSetProperty (session, kVTCompressionPropertyKey_AllowFrameReordering,
      allow_frame_reordering ? kCFBooleanTrue : kCFBooleanFalse);
}

static void
gst_vtenc_session_configure_realtime (GstVTEnc * self,
    VTCompressionSessionRef session, gboolean realtime)
{
  VTSessionSetProperty (session, kVTCompressionPropertyKey_RealTime,
      realtime ? kCFBooleanTrue : kCFBooleanFalse);
}

static OSStatus
gst_vtenc_session_configure_property_int (GstVTEnc * self,
    VTCompressionSessionRef session, CFStringRef name, gint value)
{
  CFNumberRef num;
  OSStatus status;
  gchar name_str[128];

  num = CFNumberCreate (NULL, kCFNumberIntType, &value);
  status = VTSessionSetProperty (session, name, num);
  CFRelease (num);

  CFStringGetCString (name, name_str, sizeof (name_str), kCFStringEncodingUTF8);
  if (status != noErr)
    GST_WARNING_OBJECT (self, "FAILED: %s(%d) => %d", name_str, value, status);
  else
    GST_DEBUG_OBJECT (self, "%s(%d) => %d", name_str, value, status);

  return status;
}

static OSStatus
gst_vtenc_session_configure_property_double (GstVTEnc * self,
    VTCompressionSessionRef session, CFStringRef name, gdouble value)
{
  CFNumberRef num;
  OSStatus status;
  gchar name_str[128];

  num = CFNumberCreate (NULL, kCFNumberDoubleType, &value);
  status = VTSessionSetProperty (session, name, num);
  CFRelease (num);

  CFStringGetCString (name, name_str, sizeof (name_str), kCFStringEncodingUTF8);
  if (status != noErr)
    GST_WARNING_OBJECT (self, "FAILED: %s(%f) => %d", name_str, value, status);
  else
    GST_DEBUG_OBJECT (self, "%s(%f) => %d", name_str, value, status);

  return status;
}

static void
gst_vtenc_update_latency (GstVTEnc * self)
{
  OSStatus status;
  CFNumberRef value;
  int frames = 0;
  GstClockTime frame_duration;
  GstClockTime latency;

  if (self->video_info.fps_d == 0) {
    GST_INFO_OBJECT (self, "framerate not known, can't set latency");
    return;
  }

  status = VTSessionCopyProperty (self->session,
      kVTCompressionPropertyKey_NumberOfPendingFrames, NULL, &value);
  if (status != noErr || !value) {
    GST_INFO_OBJECT (self, "failed to get NumberOfPendingFrames: %d", status);
    return;
  }

  CFNumberGetValue (value, kCFNumberSInt32Type, &frames);
  if (MAX (self->latency_frames, frames) != self->latency_frames) {
    self->latency_frames = frames;
    if (self->video_info.fps_d == 0 || self->video_info.fps_n == 0) {
      /* FIXME: Assume 25fps. This is better than reporting no latency at
       * all and then later failing in live pipelines
       */
      frame_duration = gst_util_uint64_scale (GST_SECOND, 1, 25);
    } else {
      frame_duration = gst_util_uint64_scale (GST_SECOND,
          self->video_info.fps_d, self->video_info.fps_n);
    }
    latency = frame_duration * frames;
    GST_INFO_OBJECT (self,
        "latency status %d frames %d fps %d/%d time %" GST_TIME_FORMAT, status,
        frames, self->video_info.fps_n, self->video_info.fps_d,
        GST_TIME_ARGS (latency));
    gst_video_encoder_set_latency (GST_VIDEO_ENCODER (self), latency, latency);
  }
  CFRelease (value);
}

static void
gst_vtenc_set_timestamps_from_sample_buf (GstVideoCodecFrame * frame,
    CMSampleBufferRef sample_buf)
{
  CMTime pts = CMSampleBufferGetOutputPresentationTimeStamp (sample_buf);
  frame->pts = CMTIME_TO_GST_CLOCK_TIME (pts);
  CMTime dts = CMSampleBufferGetOutputDecodeTimeStamp (sample_buf);
  if (CMTIME_IS_VALID (dts)) {
    frame->dts = CMTIME_TO_GST_CLOCK_TIME (dts);
  }
}

static Boolean
gst_vtenc_is_recoverable_error (OSStatus status)
{
  return status == kVTVideoEncoderMalfunctionErr
      || status == kVTVideoEncoderNotAvailableNowErr;
}

static gboolean
gst_vtenc_push_all_pending_frames (GstVTEnc * self)
{
  OSStatus status;

  GST_VIDEO_ENCODER_STREAM_UNLOCK (self);
  GST_DEBUG_OBJECT (self, "starting VTCompressionSessionCompleteFrames");
  status = VTCompressionSessionCompleteFrames (self->session,
      kCMTimePositiveInfinity);
  GST_DEBUG_OBJECT (self, "VTCompressionSessionCompleteFrames ended");
  GST_VIDEO_ENCODER_STREAM_LOCK (self);

  if (status != noErr) {
    GST_WARNING_OBJECT (self,
        "VTCompressionSessionCompleteFrames returned %d", (int) status);
  }

  return status == noErr;
}

static void
gst_vtenc_restart_session (GstVTEnc * self)
{
  VTCompressionSessionRef session;

  /* We need to push out all frames still inside the encoder,
   * otherwise destroy_session() will wait for all callbacks to fire
   * and very likely deadlock due to the object lock being taken */
  if (gst_vtenc_push_all_pending_frames (self)) {
    GST_DEBUG_OBJECT (self, "All frames out, restarting encoder session");
  } else {
    GST_DEBUG_OBJECT (self, "Failed to push all pending frames, restarting "
        "encoder session anyway");
  }

  GST_OBJECT_LOCK (self);
  gst_vtenc_destroy_session (self, &self->session);
  GST_OBJECT_UNLOCK (self);

  session = gst_vtenc_create_session (self);

  GST_OBJECT_LOCK (self);
  self->session = session;
  GST_OBJECT_UNLOCK (self);

  g_atomic_int_set (&self->require_reconfigure, FALSE);
  g_atomic_int_set (&self->require_restart, FALSE);
}

static void
gst_vtenc_reconfigure_session (GstVTEnc * self)
{
  if (!gst_vtenc_push_all_pending_frames (self)) {
    GST_DEBUG_OBJECT (self,
        "Will retry session reconfigure on next frame encode");
    return;
  }

  GST_DEBUG_OBJECT (self, "All frames out, reconfiguring encoder session");

  GST_OBJECT_LOCK (self);
  gst_vtenc_session_configure_bitrate (self, self->session);
  gst_vtenc_session_configure_allow_frame_reordering (self, self->session,
      self->allow_frame_reordering);
  gst_vtenc_session_configure_realtime (self, self->session, self->realtime);
  gst_vtenc_session_configure_property_double (self, self->session,
      kVTCompressionPropertyKey_Quality, self->quality);
  gst_vtenc_session_configure_max_keyframe_interval (self, self->session,
      self->max_keyframe_interval);
  gst_vtenc_session_configure_max_keyframe_interval_duration (self,
      self->session,
      self->max_keyframe_interval_duration / ((gdouble) GST_SECOND));
  gst_vtenc_session_configure_max_frame_delay (self, self->session,
      self->max_frame_delay);
  GST_OBJECT_UNLOCK (self);

  g_atomic_int_set (&self->require_reconfigure, FALSE);
}

static GstFlowReturn
gst_vtenc_encode_frame (GstVTEnc * self, GstVideoCodecFrame * frame)
{
  CMTime ts, duration;
  GstCoreMediaMeta *meta;
  CVPixelBufferRef pbuf = NULL;
  OSStatus vt_status;
  GstFlowReturn ret = GST_FLOW_OK;
  CFDictionaryRef frame_props = NULL;
  GstTaskState task_state;

  /* If this condition changes later while we're still in this function,
   * it'll just fail on next frame encode or in _finish() */
  task_state = gst_pad_get_task_state (GST_VIDEO_ENCODER_SRC_PAD (self));
  if (task_state == GST_TASK_STOPPED || task_state == GST_TASK_PAUSED) {
    /* Abort if our loop failed to push frames downstream... */
    if (self->downstream_ret != GST_FLOW_OK) {
      ret = self->downstream_ret;

      if (ret == GST_FLOW_FLUSHING) {
        GST_DEBUG_OBJECT (self,
            "Output loop stopped because of flushing, ignoring frame");
        goto release;
      } else {
        GST_WARNING_OBJECT (self,
            "Output loop stopped with error (%s), leaving",
            gst_flow_get_name (ret));
        goto drop;
      }
    }

    /* ...or if it stopped because of the flushing flag while the queue
     * was empty, in which case we didn't get GST_FLOW_FLUSHING... */
    g_mutex_lock (&self->queue_mutex);
    if (self->is_flushing) {
      g_mutex_unlock (&self->queue_mutex);
      GST_DEBUG_OBJECT (self, "Flushing flag set, ignoring frame");
      ret = GST_FLOW_FLUSHING;
      goto release;
    }
    g_mutex_unlock (&self->queue_mutex);

    /* .. or if it refuses to resume - e.g. it was stopped instead of paused */
    if (!gst_vtenc_ensure_output_loop (self)) {
      GST_ERROR_OBJECT (self, "Output loop failed to resume");
      ret = GST_FLOW_ERROR;
      goto drop;
    }
  }

  /* Flushes all remaining frames out of the encoder
   * and recreates the encoding session. */
  if (g_atomic_int_get (&self->require_restart))
    gst_vtenc_restart_session (self);
  else if (g_atomic_int_get (&self->require_reconfigure))
    gst_vtenc_reconfigure_session (self);

  if (GST_VIDEO_CODEC_FRAME_IS_FORCE_KEYFRAME (frame)) {
    GST_INFO_OBJECT (self, "received force-keyframe-event, will force intra");
    frame_props = self->keyframe_props;
  }

  ts = CMTimeMake (frame->pts, GST_SECOND);
  if (frame->duration != GST_CLOCK_TIME_NONE)
    duration = CMTimeMake (frame->duration, GST_SECOND);
  else
    duration = kCMTimeInvalid;

  /* If we don't have field order, we need to pick it up from the first buffer
   * that has that information. The encoder session also cannot be reconfigured
   * with a new field detail after it has been set, so we encode mixed streams
   * with whatever the first buffer's field order is. */
  if (!self->have_field_order) {
    CFStringRef field_detail = NULL;

    if (GST_VIDEO_BUFFER_IS_TOP_FIELD (frame->input_buffer))
      field_detail = kCMFormatDescriptionFieldDetail_TemporalTopFirst;
    else if (GST_VIDEO_BUFFER_IS_BOTTOM_FIELD (frame->input_buffer))
      field_detail = kCMFormatDescriptionFieldDetail_TemporalBottomFirst;

    if (field_detail) {
      vt_status = VTSessionSetProperty (self->session,
          kVTCompressionPropertyKey_FieldDetail, field_detail);
      GST_DEBUG_OBJECT (self, "kVTCompressionPropertyKey_FieldDetail => %d",
          (int) vt_status);
    } else {
      GST_WARNING_OBJECT (self, "have interlaced content, but don't know field "
          "order yet, skipping buffer");
      gst_video_encoder_release_frame (GST_VIDEO_ENCODER (self), frame);
      return GST_FLOW_OK;
    }

    self->have_field_order = TRUE;
  }

  meta = gst_buffer_get_core_media_meta (frame->input_buffer);
  if (meta != NULL) {
    pbuf = gst_core_media_buffer_get_pixel_buffer (frame->input_buffer);
  }
#if !TARGET_OS_OSX
  if (pbuf == NULL) {
    GstVideoFrame inframe, outframe;
    GstBuffer *outbuf;
    CVReturn cv_ret;
    OSType pixel_format_type =
        gst_video_format_to_cvpixelformat (GST_VIDEO_INFO_FORMAT
        (&self->video_info));

    /* FIXME: iOS has special stride requirements that we don't know yet.
     * Copy into a newly allocated pixelbuffer for now. Probably makes
     * sense to create a buffer pool around these at some point.
     */
    if (!gst_video_frame_map (&inframe, &self->video_info, frame->input_buffer,
            GST_MAP_READ)) {
      GST_ERROR_OBJECT (self, "failed to map input buffer");
      goto cv_error;
    }

    cv_ret =
        CVPixelBufferCreate (NULL, self->video_info.width,
        self->video_info.height, pixel_format_type, NULL, &pbuf);

    if (cv_ret != kCVReturnSuccess) {
      GST_ERROR_OBJECT (self, "CVPixelBufferCreate failed: %i", cv_ret);
      gst_video_frame_unmap (&inframe);
      goto cv_error;
    }

    outbuf =
        gst_core_video_buffer_new ((CVBufferRef) pbuf, &self->video_info, NULL);
    if (!gst_video_frame_map (&outframe, &self->video_info, outbuf,
            GST_MAP_WRITE)) {
      GST_ERROR_OBJECT (self, "Failed to map output buffer");
      gst_video_frame_unmap (&inframe);
      gst_buffer_unref (outbuf);
      CVPixelBufferRelease (pbuf);
      goto cv_error;
    }

    if (!gst_video_frame_copy (&outframe, &inframe)) {
      GST_ERROR_OBJECT (self, "Failed to copy output frame");
      gst_video_frame_unmap (&inframe);
      gst_buffer_unref (outbuf);
      CVPixelBufferRelease (pbuf);
      goto cv_error;
    }

    gst_buffer_unref (outbuf);
    gst_video_frame_unmap (&inframe);
    gst_video_frame_unmap (&outframe);
  }
#else
  if (pbuf == NULL) {
    GstVTEncFrame *vframe;
    CVReturn cv_ret;

    vframe = gst_vtenc_frame_new (frame->input_buffer, &self->video_info);
    if (!vframe) {
      GST_ERROR_OBJECT (self, "Failed to create a new input frame");
      goto cv_error;
    }

    {
      OSType pixel_format_type =
          gst_video_format_to_cvpixelformat (GST_VIDEO_INFO_FORMAT
          (&self->video_info));
      const size_t num_planes = GST_VIDEO_FRAME_N_PLANES (&vframe->videoframe);
      void *plane_base_addresses[GST_VIDEO_MAX_PLANES];
      size_t plane_widths[GST_VIDEO_MAX_PLANES];
      size_t plane_heights[GST_VIDEO_MAX_PLANES];
      size_t plane_bytes_per_row[GST_VIDEO_MAX_PLANES];
      size_t i;

      for (i = 0; i < num_planes; i++) {
        plane_base_addresses[i] =
            GST_VIDEO_FRAME_PLANE_DATA (&vframe->videoframe, i);
        plane_widths[i] = GST_VIDEO_FRAME_COMP_WIDTH (&vframe->videoframe, i);
        plane_heights[i] = GST_VIDEO_FRAME_COMP_HEIGHT (&vframe->videoframe, i);
        plane_bytes_per_row[i] =
            GST_VIDEO_FRAME_COMP_STRIDE (&vframe->videoframe, i);
        plane_bytes_per_row[i] =
            GST_VIDEO_FRAME_COMP_STRIDE (&vframe->videoframe, i);
      }

      cv_ret = CVPixelBufferCreateWithPlanarBytes (NULL,
          self->video_info.width, self->video_info.height,
          pixel_format_type,
          frame,
          GST_VIDEO_FRAME_SIZE (&vframe->videoframe),
          num_planes,
          plane_base_addresses,
          plane_widths,
          plane_heights,
          plane_bytes_per_row, gst_pixel_buffer_release_cb, vframe, NULL,
          &pbuf);
      if (cv_ret != kCVReturnSuccess) {
        GST_ERROR_OBJECT (self, "CVPixelBufferCreateWithPlanarBytes failed: %i",
            cv_ret);
        gst_vtenc_frame_free (vframe);
        goto cv_error;
      }
    }
  }
#endif

  /* We need to unlock the stream lock here because
   * the encode call can wait for gst_vtenc_enqueue_buffer()
   * to handle a buffer... which will take the stream lock
   * from another thread and then deadlock */
  GST_VIDEO_ENCODER_STREAM_UNLOCK (self);

  /* HEVCWithAlpha encoder has a bug where it does not throttle the amount
   * of input frames queued internally. Other encoders do not have this
   * problem and correctly block until the internal queue has space.
   * When paired with a fast enough source like videotestsrc, this can result in
   * a ton of memory being taken up by frames inside the encoder, eventually killing
   * the process because of OOM.
   *
   * The workaround here tries to block until the number of pending frames falls
   * below a certain threshold. Best we can do until Apple fixes this. */
  if (self->specific_format_id == kCMVideoCodecType_HEVCWithAlpha) {
    CFNumberRef pending_num;
    gint pending_frames;

    VTSessionCopyProperty (self->session,
        kVTCompressionPropertyKey_NumberOfPendingFrames, NULL, &pending_num);
    CFNumberGetValue (pending_num, kCFNumberSInt32Type, &pending_frames);
    CFRelease (pending_num);

    g_mutex_lock (&self->encoding_mutex);
    while (pending_frames > VTENC_HEVCALPHA_INPUT_LIMIT) {
      g_cond_wait (&self->encoding_cond, &self->encoding_mutex);
      VTSessionCopyProperty (self->session,
          kVTCompressionPropertyKey_NumberOfPendingFrames, NULL, &pending_num);
      CFNumberGetValue (pending_num, kCFNumberIntType, &pending_frames);
      CFRelease (pending_num);
    }
    g_mutex_unlock (&self->encoding_mutex);
  }

  vt_status = VTCompressionSessionEncodeFrame (self->session,
      pbuf, ts, duration, frame_props,
      GINT_TO_POINTER (frame->system_frame_number), NULL);
  GST_VIDEO_ENCODER_STREAM_LOCK (self);

  if (gst_vtenc_is_recoverable_error (vt_status)) {
    GST_ELEMENT_WARNING (self, LIBRARY, ENCODE, (NULL),
        ("Failed to encode frame %d: %d, restarting session on next frame encode",
            frame->system_frame_number, (int) vt_status));

    g_atomic_int_set (&self->require_restart, TRUE);
  } else if (vt_status != noErr) {
    GST_ELEMENT_ERROR (self, LIBRARY, ENCODE, (NULL),
        ("Failed to encode frame %d: %d", frame->system_frame_number,
            (int) vt_status));
  }

  gst_video_codec_frame_unref (frame);
  CVPixelBufferRelease (pbuf);

  return ret;

cv_error:
  ret = GST_FLOW_ERROR;

drop:
  gst_video_encoder_drop_frame (GST_VIDEO_ENCODER_CAST (self), frame);
  return ret;

release:
  gst_video_encoder_release_frame (GST_VIDEO_ENCODER_CAST (self), frame);
  return ret;
}

static void
gst_vtenc_session_output_callback (void *outputCallbackRefCon,
    void *sourceFrameRefCon,
    OSStatus status,
    VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer)
{
  GstVTEnc *self = outputCallbackRefCon;
  GstVideoCodecFrame *frame;
  gboolean push_anyway;

  frame =
      gst_video_encoder_get_frame (GST_VIDEO_ENCODER_CAST (self),
      GPOINTER_TO_INT (sourceFrameRefCon));

  if (g_atomic_int_get (&self->require_restart)) {
    GST_DEBUG_OBJECT (self, "Ignoring frame because of scheduled restart");
    goto drop;
  }

  if (status != noErr) {
    if (gst_vtenc_is_recoverable_error (status)) {
      GST_ELEMENT_WARNING (self, LIBRARY, ENCODE, (NULL),
          ("Failed to encode frame (%d), restarting session on next frame encode",
              (int) status));
      g_atomic_int_set (&self->require_restart, TRUE);
    } else if (frame) {
      GST_ELEMENT_ERROR (self, LIBRARY, ENCODE, (NULL),
          ("Failed to encode frame %d: %d", frame->system_frame_number,
              (int) status));
    } else {
      GST_ELEMENT_ERROR (self, LIBRARY, ENCODE, (NULL),
          ("Failed to encode (frame unknown): %d", (int) status));
    }

    goto drop;
  }

  if (self->specific_format_id == kCMVideoCodecType_HEVCWithAlpha) {
    /* See comment in gst_vtenc_encode_frame() above */
    g_mutex_lock (&self->encoding_mutex);
    g_cond_signal (&self->encoding_cond);
    g_mutex_unlock (&self->encoding_mutex);
  }

  if (!frame) {
    GST_WARNING_OBJECT (self, "No corresponding frame found!");
    return;
  }

  /* This may happen if we don't have enough bitrate */
  if (sampleBuffer == NULL)
    goto drop;

  if (gst_vtenc_buffer_is_keyframe (self, sampleBuffer))
    GST_VIDEO_CODEC_FRAME_SET_SYNC_POINT (frame);

  /* We are dealing with block buffers here, so we don't need
   * to enable the use of the video meta API on the core media buffer */
  frame->output_buffer = gst_core_media_buffer_new (sampleBuffer, FALSE, NULL);

  gst_vtenc_set_timestamps_from_sample_buf (frame, sampleBuffer);

  g_mutex_lock (&self->queue_mutex);

  /* VT can give us frames with DTS>PTS. We need to offset DTS by the max possible difference
   * between PTS and DTS, which can be calculated from the PTS diff between the 1st and 2nd frame */
  if (self->first_frame) {
    if (GST_CLOCK_TIME_IS_VALID (self->first_frame->pts)
        && GST_CLOCK_TIME_IS_VALID (frame->pts)) {
      self->dts_offset = frame->pts - self->first_frame->pts;
      GST_INFO_OBJECT (self, "DTS offset: %" GST_TIME_FORMAT,
          GST_TIME_ARGS (self->dts_offset));
    } else {
      GST_INFO_OBJECT (self,
          "DTS offset set to 0 because of invalid PTS: frame 1 PTS %"
          GST_TIME_FORMAT ", frame 2 PTS %" GST_TIME_FORMAT,
          GST_TIME_ARGS (self->first_frame->pts), GST_TIME_ARGS (frame->pts));
      self->dts_offset = 0;
    }

    if (GST_CLOCK_TIME_IS_VALID (self->first_frame->dts))
      self->first_frame->dts -= self->dts_offset;

    gst_vec_deque_push_tail (self->output_queue, self->first_frame);
    self->first_frame = NULL;
  } else if (!GST_CLOCK_TIME_IS_VALID (self->dts_offset)) {
    self->first_frame = frame;
    g_mutex_unlock (&self->queue_mutex);
    return;
  }

  if (GST_CLOCK_TIME_IS_VALID (self->dts_offset)
      && GST_CLOCK_TIME_IS_VALID (frame->dts)) {
    frame->dts -= self->dts_offset;
  }

  GST_TRACE_OBJECT (self,
      "encoded frame %d PTS %" GST_TIME_FORMAT " DTS %" GST_TIME_FORMAT,
      frame->system_frame_number, GST_TIME_ARGS (frame->pts),
      GST_TIME_ARGS (frame->dts));

  /* Limit the amount of frames in our output queue to avoid processing
   * too many frames ahead, and also make sure we don't block here 
   * if the output loop paused due to a downstream error. */
  push_anyway = self->is_flushing || self->is_draining;
  while (!push_anyway
      && gst_vec_deque_get_length (self->output_queue) >
      VTENC_OUTPUT_QUEUE_SIZE) {
    g_cond_wait (&self->queue_cond, &self->queue_mutex);
    push_anyway = self->is_flushing || self->is_draining;
  }

  gst_vec_deque_push_tail (self->output_queue, frame);
  g_cond_signal (&self->queue_cond);
  g_mutex_unlock (&self->queue_mutex);
  return;

drop:
  gst_video_encoder_drop_frame (GST_VIDEO_ENCODER_CAST (self), frame);
}

static void
gst_vtenc_output_loop (GstVTEnc * self)
{
  GstVideoCodecFrame *outframe;
  GstCoreMediaMeta *meta;
  GstFlowReturn ret = GST_FLOW_OK;

  g_mutex_lock (&self->queue_mutex);
  while (gst_vec_deque_is_empty (self->output_queue) && !self->pause_task
      && !self->is_flushing && !self->is_draining) {
    g_cond_wait (&self->queue_cond, &self->queue_mutex);
  }

  /* If we're currently draining/flushing, make sure to not pause before we
   * output all the frames */
  if (self->pause_task && ((!self->is_flushing && !self->is_draining)
          || gst_vec_deque_is_empty (self->output_queue))) {
    GST_DEBUG_OBJECT (self, "pausing output loop as requested");
    g_mutex_unlock (&self->queue_mutex);
    gst_pad_pause_task (GST_VIDEO_ENCODER_CAST (self)->srcpad);
    return;
  }

  while ((outframe = gst_vec_deque_pop_head (self->output_queue))) {
    g_cond_signal (&self->queue_cond);
    g_mutex_unlock (&self->queue_mutex);

    /* Keep the stream lock -> queue lock order */
    GST_VIDEO_ENCODER_STREAM_LOCK (self);

    g_mutex_lock (&self->queue_mutex);
    if (self->is_flushing) {
      GST_LOG_OBJECT (self, "flushing frame %d", outframe->system_frame_number);
      gst_video_encoder_release_frame (GST_VIDEO_ENCODER_CAST (self), outframe);
      GST_VIDEO_ENCODER_STREAM_UNLOCK (self);
      continue;
    }
    g_mutex_unlock (&self->queue_mutex);

    if (self->negotiate_downstream &&
        (meta = gst_buffer_get_core_media_meta (outframe->output_buffer))) {
      if (!gst_vtenc_negotiate_downstream (self, meta->sample_buf)) {
        ret = GST_FLOW_NOT_NEGOTIATED;
        gst_video_encoder_release_frame (GST_VIDEO_ENCODER_CAST (self),
            outframe);
        g_mutex_lock (&self->queue_mutex);
        /* the rest of the frames will be pop'd and unref'd later */
        break;
      }

      self->negotiate_downstream = FALSE;
    }

    gst_vtenc_update_latency (self);

    GST_LOG_OBJECT (self,
        "finishing frame %d dts %" GST_TIME_FORMAT " pts %" GST_TIME_FORMAT,
        outframe->system_frame_number,
        GST_TIME_ARGS (outframe->dts), GST_TIME_ARGS (outframe->pts));
    GST_VIDEO_ENCODER_STREAM_UNLOCK (self);
    /* releases frame, even if it has no output buffer (i.e. failed to encode) */
    ret =
        gst_video_encoder_finish_frame (GST_VIDEO_ENCODER_CAST (self),
        outframe);
    g_mutex_lock (&self->queue_mutex);

    if (ret != GST_FLOW_OK)
      break;
  }

  g_mutex_unlock (&self->queue_mutex);
  GST_VIDEO_ENCODER_STREAM_LOCK (self);

  /* We need to empty the queue immediately so that enqueue_buffer()
   * can push out the current buffer, otherwise it can block other
   * encoder callbacks completely */
  if (ret != GST_FLOW_OK) {
    g_mutex_lock (&self->queue_mutex);

    while ((outframe = gst_vec_deque_pop_head (self->output_queue))) {
      GST_LOG_OBJECT (self, "flushing frame %d", outframe->system_frame_number);
      gst_video_encoder_release_frame (GST_VIDEO_ENCODER_CAST (self), outframe);
    }

    /* Don't consider the FLUSHING ret an error if something flagged is_flushing in the meantime */
    if (self->is_flushing && ret == GST_FLOW_FLUSHING) {
      ret = GST_FLOW_OK;
    }
    g_cond_signal (&self->queue_cond);
    g_mutex_unlock (&self->queue_mutex);
  }

  self->downstream_ret = ret;
  GST_VIDEO_ENCODER_STREAM_UNLOCK (self);

  if (ret != GST_FLOW_OK) {
    GST_DEBUG_OBJECT (self, "pausing output task because of downstream: %s",
        gst_flow_get_name (ret));
    gst_pad_pause_task (GST_VIDEO_ENCODER_SRC_PAD (self));
  }
}

static gboolean
gst_vtenc_buffer_is_keyframe (GstVTEnc * self, CMSampleBufferRef sbuf)
{
  gboolean result = FALSE;
  CFArrayRef attachments_for_sample;

  attachments_for_sample = CMSampleBufferGetSampleAttachmentsArray (sbuf, 0);
  if (attachments_for_sample != NULL) {
    CFDictionaryRef attachments;
    CFBooleanRef depends_on_others;

    attachments = CFArrayGetValueAtIndex (attachments_for_sample, 0);
    depends_on_others = CFDictionaryGetValue (attachments,
        kCMSampleAttachmentKey_DependsOnOthers);
    result = (depends_on_others == kCFBooleanFalse);
  }

  return result;
}

#if TARGET_OS_OSX
static GstVTEncFrame *
gst_vtenc_frame_new (GstBuffer * buf, GstVideoInfo * video_info)
{
  GstVTEncFrame *frame;

  frame = g_slice_new (GstVTEncFrame);
  frame->buf = gst_buffer_ref (buf);
  if (!gst_video_frame_map (&frame->videoframe, video_info, buf, GST_MAP_READ)) {
    gst_buffer_unref (frame->buf);
    g_slice_free (GstVTEncFrame, frame);
    return NULL;
  }

  return frame;
}

static void
gst_vtenc_frame_free (GstVTEncFrame * frame)
{
  gst_video_frame_unmap (&frame->videoframe);
  gst_buffer_unref (frame->buf);
  g_slice_free (GstVTEncFrame, frame);
}

static void
gst_pixel_buffer_release_cb (void *releaseRefCon, const void *dataPtr,
    size_t dataSize, size_t numberOfPlanes, const void *planeAddresses[])
{
  GstVTEncFrame *frame = (GstVTEncFrame *) releaseRefCon;
  gst_vtenc_frame_free (frame);
}
#endif

static gboolean
gst_vtenc_register (GstPlugin * plugin,
    const GstVTEncoderDetails * codec_details)
{
  GTypeInfo type_info = {
    sizeof (GstVTEncClass),
    (GBaseInitFunc) gst_vtenc_base_init,
    NULL,
    (GClassInitFunc) gst_vtenc_class_init,
    NULL,
    NULL,
    sizeof (GstVTEnc),
    0,
    (GInstanceInitFunc) gst_vtenc_init,
  };
  char *type_name;
  GType type;
  gboolean result;

  type_name = g_strdup_printf ("vtenc_%s", codec_details->element_name);

  type = g_type_register_static (GST_TYPE_VTENC, type_name, &type_info, 0);

  g_type_set_qdata (type, GST_VTENC_CODEC_DETAILS_QDATA,
      (gpointer) codec_details);

  result = gst_element_register (plugin, type_name, GST_RANK_PRIMARY, type);
  if (!result) {
    GST_ERROR_OBJECT (plugin, "failed to register element %s", type_name);
  }

  g_free (type_name);

  return result;
}

static void
gst_vtenc_init_once (void)
{
  static gsize init_once = 0;

  if (g_once_init_enter (&init_once)) {
    gst_applemedia_init_once ();
    GST_DEBUG_CATEGORY_INIT (gst_vtenc_debug, "vtenc", 0,
        "Apple VideoToolbox Encoder Wrapper");
    g_once_init_leave (&init_once, 1);
  }
}

static gboolean
gst_vtenc_register_h264 (GstPlugin * plugin)
{
  gst_vtenc_init_once ();

  static const GstVTEncoderDetails codec = {
    "H.264", "h264", "video/x-h264",
    "Ole André Vadla Ravnås <oleavr@soundrop.com>, "
        "Dominik Röttsches <dominik.rottsches@intel.com>",
    kCMVideoCodecType_H264, FALSE
  };

  return gst_vtenc_register (plugin, &codec);
}

static gboolean
gst_vtenc_register_h265 (GstPlugin * plugin)
{
  gst_vtenc_init_once ();

  static const GstVTEncoderDetails codec = {
    "H.265/HEVC", "h265", "video/x-h265",
    "Piotr Brzeziński <piotr@centricular.com>",
    kCMVideoCodecType_HEVC, FALSE
  };

  return gst_vtenc_register (plugin, &codec);
}

static gboolean
gst_vtenc_register_h265a (GstPlugin * plugin)
{
  gst_vtenc_init_once ();

  static const GstVTEncoderDetails codec = {
    "H.265/HEVC with alpha", "h265a", "video/x-h265",
    "Piotr Brzeziński <piotr@centricular.com>",
    kCMVideoCodecType_HEVCWithAlpha, FALSE
  };

  return gst_vtenc_register (plugin, &codec);
}

#if TARGET_OS_OSX
static gboolean
gst_vtenc_register_h264_hw (GstPlugin * plugin)
{
  gst_vtenc_init_once ();

  static const GstVTEncoderDetails codec = {
    "H.264 (HW only)", "h264_hw", "video/x-h264",
    "Ole André Vadla Ravnås <oleavr@soundrop.com>, "
        "Dominik Röttsches <dominik.rottsches@intel.com>",
    kCMVideoCodecType_H264, TRUE
  };

  return gst_vtenc_register (plugin, &codec);
}

static gboolean
gst_vtenc_register_h265_hw (GstPlugin * plugin)
{
  gst_vtenc_init_once ();

  static const GstVTEncoderDetails codec = {
    "H.265/HEVC (HW only)", "h265_hw", "video/x-h265",
    "Piotr Brzeziński <piotr@centricular.com>",
    kCMVideoCodecType_HEVC, TRUE
  };

  return gst_vtenc_register (plugin, &codec);
}

static gboolean
gst_vtenc_register_h265a_hw (GstPlugin * plugin)
{
  gst_vtenc_init_once ();

  static const GstVTEncoderDetails codec = {
    "H.265/HEVC with alpha (HW only)", "h265a_hw", "video/x-h265",
    "Piotr Brzeziński <piotr@centricular.com>",
    kCMVideoCodecType_HEVCWithAlpha, TRUE
  };

  return gst_vtenc_register (plugin, &codec);
}
#endif

static gboolean
gst_vtenc_register_prores (GstPlugin * plugin)
{
  gst_vtenc_init_once ();

  static const GstVTEncoderDetails codec = {
    "Apple ProRes", "prores", "video/x-prores",
    "Nirbheek Chauhan <nirbheek@centricular.com>",
    GST_kCMVideoCodecType_Some_AppleProRes, FALSE
  };

  return gst_vtenc_register (plugin, &codec);
}

GST_ELEMENT_REGISTER_DEFINE_CUSTOM (vtenc_h264, gst_vtenc_register_h264);
GST_ELEMENT_REGISTER_DEFINE_CUSTOM (vtenc_h265, gst_vtenc_register_h265);
GST_ELEMENT_REGISTER_DEFINE_CUSTOM (vtenc_h265a, gst_vtenc_register_h265a);
#if TARGET_OS_OSX
GST_ELEMENT_REGISTER_DEFINE_CUSTOM (vtenc_h264_hw, gst_vtenc_register_h264_hw);
GST_ELEMENT_REGISTER_DEFINE_CUSTOM (vtenc_h265_hw, gst_vtenc_register_h265_hw);
GST_ELEMENT_REGISTER_DEFINE_CUSTOM (vtenc_h265a_hw,
    gst_vtenc_register_h265a_hw);
#endif
GST_ELEMENT_REGISTER_DEFINE_CUSTOM (vtenc_prores, gst_vtenc_register_prores);

gboolean
gst_vtenc_register_elements (GstPlugin * plugin)
{
  gboolean ret = FALSE;

  ret |= GST_ELEMENT_REGISTER (vtenc_h264, plugin);
  ret |= GST_ELEMENT_REGISTER (vtenc_h265, plugin);
  ret |= GST_ELEMENT_REGISTER (vtenc_h265a, plugin);
#if TARGET_OS_OSX
  ret |= GST_ELEMENT_REGISTER (vtenc_h264_hw, plugin);
  ret |= GST_ELEMENT_REGISTER (vtenc_h265_hw, plugin);
  ret |= GST_ELEMENT_REGISTER (vtenc_h265a_hw, plugin);
#endif
  ret |= GST_ELEMENT_REGISTER (vtenc_prores, plugin);

  return ret;
}
