mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-11-25 19:21:06 +00:00
9a232f7983
Slipped through with earlier changes to use drop/release_frame() explicitly. We should only drop when something goes wrong in the encoder, and just release otherwise. Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/7475>
2543 lines
82 KiB
C
2543 lines
82 KiB
C
/*
|
|
* 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"
|
|
#include "corevideobuffer.h"
|
|
#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
|
|
|
|
#ifdef HAVE_VIDEOTOOLBOX_10_9_6
|
|
extern OSStatus
|
|
VTCompressionSessionPrepareToEncodeFrames (VTCompressionSessionRef session)
|
|
__attribute__((weak_import));
|
|
#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;
|
|
};
|
|
|
|
static GstElementClass *parent_class = NULL;
|
|
|
|
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, guint bitrate);
|
|
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_enqueue_buffer (void *outputCallbackRefCon,
|
|
void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags,
|
|
CMSampleBufferRef sampleBuffer);
|
|
static gboolean gst_vtenc_buffer_is_keyframe (GstVTEnc * self,
|
|
CMSampleBufferRef sbuf);
|
|
|
|
|
|
#ifndef HAVE_IOS
|
|
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 }"));
|
|
|
|
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,
|
|
"Ole André Vadla Ravnås <oleavr@soundrop.com>, Dominik Röttsches <dominik.rottsches@intel.com>");
|
|
|
|
g_free (longname);
|
|
g_free (description);
|
|
|
|
{
|
|
GstCaps *caps = gst_static_caps_get (&sink_caps);
|
|
#ifndef HAVE_IOS
|
|
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);
|
|
G_GNUC_END_IGNORE_DEPRECATIONS;
|
|
gst_structure_set_list (gst_caps_get_structure (src_caps, 0),
|
|
"interlace-mode", arr);
|
|
}
|
|
|
|
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;
|
|
GstElementClass *element_class;
|
|
GstVideoEncoderClass *gstvideoencoder_class;
|
|
|
|
gobject_class = (GObjectClass *) klass;
|
|
element_class = (GstElementClass *) klass;
|
|
gstvideoencoder_class = (GstVideoEncoderClass *) klass;
|
|
|
|
parent_class = g_type_class_peek_parent (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_vtenc_start;
|
|
gstvideoencoder_class->stop = gst_vtenc_stop;
|
|
gstvideoencoder_class->set_format = gst_vtenc_set_format;
|
|
gstvideoencoder_class->handle_frame = gst_vtenc_handle_frame;
|
|
gstvideoencoder_class->finish = gst_vtenc_finish;
|
|
gstvideoencoder_class->flush = gst_vtenc_flush;
|
|
gstvideoencoder_class->sink_event = 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));
|
|
|
|
/**
|
|
* 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));
|
|
|
|
/**
|
|
* 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) {
|
|
/**
|
|
* 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));
|
|
}
|
|
}
|
|
|
|
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_set_bitrate (GstVTEnc * self, guint bitrate)
|
|
{
|
|
GST_OBJECT_LOCK (self);
|
|
|
|
self->bitrate = bitrate;
|
|
|
|
if (self->session != NULL)
|
|
gst_vtenc_session_configure_bitrate (self, self->session, bitrate);
|
|
|
|
GST_OBJECT_UNLOCK (self);
|
|
}
|
|
|
|
static void
|
|
gst_vtenc_set_allow_frame_reordering (GstVTEnc * self,
|
|
gboolean allow_frame_reordering)
|
|
{
|
|
GST_OBJECT_LOCK (self);
|
|
self->allow_frame_reordering = allow_frame_reordering;
|
|
if (self->session != NULL) {
|
|
gst_vtenc_session_configure_allow_frame_reordering (self,
|
|
self->session, allow_frame_reordering);
|
|
}
|
|
GST_OBJECT_UNLOCK (self);
|
|
}
|
|
|
|
static void
|
|
gst_vtenc_set_realtime (GstVTEnc * self, gboolean realtime)
|
|
{
|
|
GST_OBJECT_LOCK (self);
|
|
self->realtime = realtime;
|
|
if (self->session != NULL)
|
|
gst_vtenc_session_configure_realtime (self, self->session, realtime);
|
|
GST_OBJECT_UNLOCK (self);
|
|
}
|
|
|
|
static void
|
|
gst_vtenc_set_quality (GstVTEnc * self, gdouble quality)
|
|
{
|
|
GST_OBJECT_LOCK (self);
|
|
self->quality = quality;
|
|
if (self->session != NULL) {
|
|
GST_INFO_OBJECT (self, "setting quality %f", quality);
|
|
gst_vtenc_session_configure_property_double (self, self->session,
|
|
kVTCompressionPropertyKey_Quality, quality);
|
|
}
|
|
GST_OBJECT_UNLOCK (self);
|
|
}
|
|
|
|
static void
|
|
gst_vtenc_set_max_keyframe_interval (GstVTEnc * self, gint interval)
|
|
{
|
|
GST_OBJECT_LOCK (self);
|
|
self->max_keyframe_interval = interval;
|
|
if (self->session != NULL) {
|
|
gst_vtenc_session_configure_max_keyframe_interval (self, self->session,
|
|
interval);
|
|
}
|
|
GST_OBJECT_UNLOCK (self);
|
|
}
|
|
|
|
static void
|
|
gst_vtenc_set_max_keyframe_interval_duration (GstVTEnc * self,
|
|
GstClockTime interval)
|
|
{
|
|
GST_OBJECT_LOCK (self);
|
|
self->max_keyframe_interval_duration = interval;
|
|
if (self->session != NULL) {
|
|
gst_vtenc_session_configure_max_keyframe_interval_duration (self,
|
|
self->session, interval / ((gdouble) GST_SECOND));
|
|
}
|
|
GST_OBJECT_UNLOCK (self);
|
|
}
|
|
|
|
static void
|
|
gst_vtenc_set_max_frame_delay (GstVTEnc * self, int delay)
|
|
{
|
|
GST_OBJECT_LOCK (self);
|
|
self->max_frame_delay = delay;
|
|
if (self->session != NULL)
|
|
gst_vtenc_session_configure_max_frame_delay (self, self->session, delay);
|
|
GST_OBJECT_UNLOCK (self);
|
|
}
|
|
|
|
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_set_property (GObject * obj, guint prop_id, const GValue * value,
|
|
GParamSpec * pspec)
|
|
{
|
|
GstVTEnc *self = GST_VTENC_CAST (obj);
|
|
|
|
switch (prop_id) {
|
|
case PROP_BITRATE:
|
|
gst_vtenc_set_bitrate (self, g_value_get_uint (value) * 1000);
|
|
break;
|
|
case PROP_ALLOW_FRAME_REORDERING:
|
|
gst_vtenc_set_allow_frame_reordering (self, g_value_get_boolean (value));
|
|
break;
|
|
case PROP_REALTIME:
|
|
gst_vtenc_set_realtime (self, g_value_get_boolean (value));
|
|
break;
|
|
case PROP_QUALITY:
|
|
gst_vtenc_set_quality (self, g_value_get_double (value));
|
|
break;
|
|
case PROP_MAX_KEYFRAME_INTERVAL:
|
|
gst_vtenc_set_max_keyframe_interval (self, g_value_get_int (value));
|
|
break;
|
|
case PROP_MAX_KEYFRAME_INTERVAL_DURATION:
|
|
gst_vtenc_set_max_keyframe_interval_duration (self,
|
|
g_value_get_uint64 (value));
|
|
break;
|
|
case PROP_RATE_CONTROL:
|
|
self->rate_control = g_value_get_enum (value);
|
|
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) {
|
|
GST_OBJECT_LOCK (self);
|
|
self->max_bitrate = max_bitrate * 1000;
|
|
self->bitrate_window = window;
|
|
GST_OBJECT_UNLOCK (self);
|
|
} else {
|
|
g_warning ("Failed to parse data rate limits: '%s'", s);
|
|
}
|
|
}
|
|
break;
|
|
case PROP_MAX_FRAME_DELAY:
|
|
gst_vtenc_set_max_frame_delay (self, g_value_get_int (value));
|
|
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;
|
|
}
|
|
}
|
|
|
|
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 void
|
|
gst_vtenc_set_flushing_flag (GstVTEnc * self)
|
|
{
|
|
g_mutex_lock (&self->queue_mutex);
|
|
self->is_flushing = TRUE;
|
|
g_cond_signal (&self->queue_cond);
|
|
g_mutex_unlock (&self->queue_mutex);
|
|
}
|
|
|
|
static GstFlowReturn
|
|
gst_vtenc_finish_encoding (GstVTEnc * self, gboolean is_flushing)
|
|
{
|
|
GST_DEBUG_OBJECT (self,
|
|
"complete encoding and clean buffer queue, is flushing %d", is_flushing);
|
|
OSStatus vt_status;
|
|
|
|
/* In case of EOS before the first buffer/caps */
|
|
if (self->session == NULL)
|
|
return GST_FLOW_OK;
|
|
|
|
/* If output loop failed to push things downstream */
|
|
if (self->downstream_ret != GST_FLOW_OK
|
|
&& self->downstream_ret != GST_FLOW_FLUSHING) {
|
|
/* Tells enqueue_buffer() to instantly discard any new encoded frames */
|
|
gst_vtenc_set_flushing_flag (self);
|
|
GST_WARNING_OBJECT (self, "Output loop stopped with error (%s), leaving",
|
|
gst_flow_get_name (self->downstream_ret));
|
|
return self->downstream_ret;
|
|
}
|
|
|
|
if (is_flushing)
|
|
gst_vtenc_set_flushing_flag (self);
|
|
|
|
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);
|
|
}
|
|
|
|
gst_vtenc_pause_output_loop (self);
|
|
GST_VIDEO_ENCODER_STREAM_LOCK (self);
|
|
|
|
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->is_flushing = FALSE;
|
|
self->downstream_ret = GST_FLOW_OK;
|
|
g_atomic_int_set (&self->require_restart, 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;
|
|
|
|
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;
|
|
|
|
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";
|
|
strncpy (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';
|
|
} 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_finish_encoding (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);
|
|
|
|
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;
|
|
guint8 sps[12];
|
|
|
|
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) {
|
|
sps[0] = codec_data[1];
|
|
sps[11] = codec_data[12];
|
|
gst_codec_utils_h265_caps_set_level_tier_and_profile (caps, sps, 12);
|
|
} else {
|
|
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);
|
|
return gst_vtenc_finish_encoding (self, FALSE);
|
|
}
|
|
|
|
static gboolean
|
|
gst_vtenc_flush (GstVideoEncoder * enc)
|
|
{
|
|
GstVTEnc *self = GST_VTENC_CAST (enc);
|
|
GstFlowReturn ret;
|
|
|
|
ret = gst_vtenc_finish_encoding (self, TRUE);
|
|
|
|
return (ret == GST_FLOW_OK);
|
|
}
|
|
|
|
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;
|
|
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 gboolean
|
|
gst_vtenc_compute_dts_offset (GstVTEnc * self, gint fps_n, gint fps_d)
|
|
{
|
|
gint num_offset_frames;
|
|
|
|
// kVTCompressionPropertyKey_AllowFrameReordering enables B-Frames
|
|
if (!self->allow_frame_reordering ||
|
|
(self->specific_format_id == kCMVideoCodecType_H264
|
|
&& self->h264_profile == GST_H264_PROFILE_BASELINE)) {
|
|
num_offset_frames = 0;
|
|
} else {
|
|
if (self->specific_format_id == kCMVideoCodecType_H264) {
|
|
// H264 encoder always sets 2 max_num_ref_frames
|
|
num_offset_frames = 1;
|
|
} else {
|
|
// HEVC encoder uses B-pyramid
|
|
num_offset_frames = 2;
|
|
}
|
|
}
|
|
|
|
if (fps_d == 0 && num_offset_frames != 0) {
|
|
GST_ERROR_OBJECT (self,
|
|
"Variable framerate is not supported with B-Frames");
|
|
return FALSE;
|
|
}
|
|
|
|
self->dts_offset =
|
|
gst_util_uint64_scale (num_offset_frames * GST_SECOND,
|
|
self->video_info.fps_d, self->video_info.fps_n);
|
|
|
|
GST_DEBUG_OBJECT (self, "DTS Offset:%" GST_TIME_FORMAT,
|
|
GST_TIME_ARGS (self->dts_offset));
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
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 !HAVE_IOS
|
|
/* 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
|
|
|
|
if (self->profile_level) {
|
|
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);
|
|
}
|
|
|
|
/* This was set in gst_vtenc_negotiate_specific_format_details() */
|
|
g_assert_cmpint (self->specific_format_id, !=, 0);
|
|
|
|
if (self->profile_level) {
|
|
if (!gst_vtenc_compute_dts_offset (self, self->video_info.fps_d,
|
|
self->video_info.fps_n)) {
|
|
goto beach;
|
|
}
|
|
}
|
|
|
|
status = VTCompressionSessionCreate (NULL,
|
|
self->video_info.width, self->video_info.height,
|
|
self->specific_format_id, encoder_spec, pb_attrs, NULL,
|
|
gst_vtenc_enqueue_buffer, 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, self->bitrate);
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
#ifdef HAVE_VIDEOTOOLBOX_10_9_6
|
|
if (VTCompressionSessionPrepareToEncodeFrames) {
|
|
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, guint bitrate)
|
|
{
|
|
gboolean emulate_cbr = FALSE;
|
|
double bitrate_window = self->bitrate_window;
|
|
guint max_bitrate = self->max_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, *)) {
|
|
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_update_timestamps (GstVTEnc * self, 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) - self->dts_offset;
|
|
}
|
|
}
|
|
|
|
static Boolean
|
|
gst_vtenc_is_recoverable_error (OSStatus status)
|
|
{
|
|
return status == kVTVideoEncoderMalfunctionErr
|
|
|| status == kVTVideoEncoderNotAvailableNowErr;
|
|
}
|
|
|
|
static void
|
|
gst_vtenc_restart_session (GstVTEnc * self)
|
|
{
|
|
OSStatus status;
|
|
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 */
|
|
GST_VIDEO_ENCODER_STREAM_UNLOCK (self);
|
|
status = VTCompressionSessionCompleteFrames (self->session,
|
|
kCMTimePositiveInfinity);
|
|
if (status != noErr) {
|
|
GST_WARNING_OBJECT (self,
|
|
"Error when emptying encoder before restart: %d, will retry on next frame encode",
|
|
(int) status);
|
|
GST_VIDEO_ENCODER_STREAM_LOCK (self);
|
|
return;
|
|
} else {
|
|
GST_DEBUG_OBJECT (self, "All frames out, restarting encoder session");
|
|
}
|
|
GST_VIDEO_ENCODER_STREAM_LOCK (self);
|
|
|
|
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_restart, 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);
|
|
|
|
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);
|
|
}
|
|
#ifdef HAVE_IOS
|
|
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_enqueue_buffer (void *outputCallbackRefCon,
|
|
void *sourceFrameRefCon,
|
|
OSStatus status,
|
|
VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer)
|
|
{
|
|
GstVTEnc *self = outputCallbackRefCon;
|
|
GstVideoCodecFrame *frame;
|
|
|
|
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;
|
|
}
|
|
|
|
g_mutex_lock (&self->queue_mutex);
|
|
if (self->is_flushing) {
|
|
GST_DEBUG_OBJECT (self, "Ignoring frame %d because we're flushing",
|
|
frame->system_frame_number);
|
|
|
|
gst_video_encoder_release_frame (GST_VIDEO_ENCODER_CAST (self), frame);
|
|
g_mutex_unlock (&self->queue_mutex);
|
|
return;
|
|
}
|
|
g_mutex_unlock (&self->queue_mutex);
|
|
|
|
/* 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_update_timestamps (self, frame, sampleBuffer);
|
|
|
|
/* Limit the amount of frames in our output queue
|
|
* to avoid processing too many frames ahead */
|
|
g_mutex_lock (&self->queue_mutex);
|
|
while (gst_vec_deque_get_length (self->output_queue) >
|
|
VTENC_OUTPUT_QUEUE_SIZE) {
|
|
g_cond_wait (&self->queue_cond, &self->queue_mutex);
|
|
}
|
|
|
|
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;
|
|
gboolean should_pause;
|
|
|
|
g_mutex_lock (&self->queue_mutex);
|
|
while (gst_vec_deque_is_empty (self->output_queue) && !self->pause_task
|
|
&& !self->is_flushing) {
|
|
g_cond_wait (&self->queue_cond, &self->queue_mutex);
|
|
}
|
|
|
|
if (self->pause_task) {
|
|
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", outframe->system_frame_number);
|
|
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);
|
|
self->downstream_ret = ret;
|
|
|
|
/* 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);
|
|
}
|
|
|
|
g_cond_signal (&self->queue_cond);
|
|
g_mutex_unlock (&self->queue_mutex);
|
|
}
|
|
|
|
GST_VIDEO_ENCODER_STREAM_UNLOCK (self);
|
|
|
|
/* Check is_flushing here in case we had an empty queue.
|
|
* In that scenario we also want to pause, as the encoder callback
|
|
* will discard any frames that are output while flushing */
|
|
g_mutex_lock (&self->queue_mutex);
|
|
should_pause = ret != GST_FLOW_OK || self->is_flushing;
|
|
g_mutex_unlock (&self->queue_mutex);
|
|
if (should_pause) {
|
|
GST_DEBUG_OBJECT (self, "pausing output task: %s",
|
|
ret != GST_FLOW_OK ? gst_flow_get_name (ret) : "flushing");
|
|
gst_pad_pause_task (GST_VIDEO_ENCODER_CAST (self)->srcpad);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
#ifndef HAVE_IOS
|
|
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 void
|
|
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,
|
|
};
|
|
gchar *type_name;
|
|
GType type;
|
|
gboolean result;
|
|
|
|
type_name = g_strdup_printf ("vtenc_%s", codec_details->element_name);
|
|
|
|
type =
|
|
g_type_register_static (GST_TYPE_VIDEO_ENCODER, 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);
|
|
}
|
|
|
|
static const GstVTEncoderDetails gst_vtenc_codecs[] = {
|
|
{"H.264", "h264", "video/x-h264", kCMVideoCodecType_H264, FALSE},
|
|
{"H.265/HEVC", "h265", "video/x-h265", kCMVideoCodecType_HEVC, FALSE},
|
|
{"H.265/HEVC with alpha", "h265a", "video/x-h265",
|
|
kCMVideoCodecType_HEVCWithAlpha, FALSE},
|
|
#ifndef HAVE_IOS
|
|
{"H.264 (HW only)", "h264_hw", "video/x-h264", kCMVideoCodecType_H264, TRUE},
|
|
{"H.265/HEVC (HW only)", "h265_hw", "video/x-h265", kCMVideoCodecType_HEVC,
|
|
TRUE},
|
|
{"H.265/HEVC with alpha (HW only)", "h265a_hw", "video/x-h265",
|
|
kCMVideoCodecType_HEVCWithAlpha, TRUE},
|
|
#endif
|
|
{"Apple ProRes", "prores", "video/x-prores",
|
|
GST_kCMVideoCodecType_Some_AppleProRes, FALSE},
|
|
};
|
|
|
|
void
|
|
gst_vtenc_register_elements (GstPlugin * plugin)
|
|
{
|
|
guint i;
|
|
|
|
GST_DEBUG_CATEGORY_INIT (gst_vtenc_debug, "vtenc",
|
|
0, "Apple VideoToolbox Encoder Wrapper");
|
|
|
|
for (i = 0; i != G_N_ELEMENTS (gst_vtenc_codecs); i++)
|
|
gst_vtenc_register (plugin, &gst_vtenc_codecs[i]);
|
|
}
|