1
0
Эх сурвалжийг харах

obs-ffmpeg: Add linux VAAPI h.264 encoding support

Closes obsproject/obs-studio#1482
Closes obsproject/obs-studio#1256
Thomas Crider 7 жил өмнө
parent
commit
a64ae11bce

+ 9 - 8
CI/install-dependencies-linux.sh

@@ -1,18 +1,18 @@
 #!/bin/sh
 set -ex
 
-sudo add-apt-repository ppa:kirillshkrogalev/ffmpeg-next -y
+sudo add-apt-repository ppa:jonathonf/ffmpeg-3 -y
 sudo apt-get -qq update
 sudo apt-get install -y \
         build-essential \
         checkinstall \
         cmake \
         libasound2-dev \
-        libavcodec-ffmpeg-dev \
-        libavdevice-ffmpeg-dev \
-        libavfilter-ffmpeg-dev \
-        libavformat-ffmpeg-dev \
-        libavutil-ffmpeg-dev \
+        libavcodec-dev \
+        libavdevice-dev \
+        libavfilter-dev \
+        libavformat-dev \
+        libavutil-dev \
         libcurl4-openssl-dev \
         libfdk-aac-dev \
         libfontconfig-dev \
@@ -24,10 +24,11 @@ sudo apt-get install -y \
         libpulse-dev \
         libqt5x11extras5-dev \
         libspeexdsp-dev \
-        libswresample-ffmpeg-dev \
-        libswscale-ffmpeg-dev \
+        libswresample-dev \
+        libswscale-dev \
         libudev-dev \
         libv4l-dev \
+        libva-dev \
         libvlc-dev \
         libx11-dev \
         libx264-dev \

+ 8 - 0
plugins/obs-ffmpeg/CMakeLists.txt

@@ -13,6 +13,7 @@ set(obs-ffmpeg_HEADERS
 	obs-ffmpeg-formats.h
 	obs-ffmpeg-compat.h
 	closest-pixel-format.h)
+
 set(obs-ffmpeg_SOURCES
 	obs-ffmpeg.c
 	obs-ffmpeg-audio-encoders.c
@@ -21,6 +22,13 @@ set(obs-ffmpeg_SOURCES
 	obs-ffmpeg-mux.c
 	obs-ffmpeg-source.c)
 
+if(UNIX AND NOT APPLE)
+	list(APPEND obs-ffmpeg_SOURCES
+		obs-ffmpeg-vaapi.c)
+	LIST(APPEND obs-ffmpeg_PLATFORM_DEPS
+		${LIBVA_LBRARIES})
+endif()
+
 add_library(obs-ffmpeg MODULE
 	${obs-ffmpeg_HEADERS}
 	${obs-ffmpeg_SOURCES})

+ 536 - 0
plugins/obs-ffmpeg/obs-ffmpeg-vaapi.c

@@ -0,0 +1,536 @@
+/******************************************************************************
+    Copyright (C) 2016 by Hugh Bailey <[email protected]>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 2 of the License, or
+    (at your option) any later version.
+
+    This program 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 General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+******************************************************************************/
+
+#include <libavutil/avutil.h>
+
+#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(55, 27, 100)
+
+#include <util/darray.h>
+#include <util/dstr.h>
+#include <util/base.h>
+#include <media-io/video-io.h>
+#include <obs-module.h>
+#include <obs-avc.h>
+
+#include <unistd.h>
+
+#include <libavutil/opt.h>
+#include <libavutil/pixdesc.h>
+#include <libavutil/hwcontext.h>
+#include <libavcodec/avcodec.h>
+#include <libavformat/avformat.h>
+#include <libavfilter/avfilter.h>
+
+#include "obs-ffmpeg-formats.h"
+
+#define do_log(level, format, ...) \
+	blog(level, "[FFMPEG VAAPI encoder: '%s'] " format, \
+			obs_encoder_get_name(enc->encoder), ##__VA_ARGS__)
+
+#define warn(format, ...) do_log(LOG_WARNING, format, ##__VA_ARGS__)
+#define info(format, ...) do_log(LOG_INFO, format, ##__VA_ARGS__)
+#define debug(format, ...) do_log(LOG_DEBUG, format, ##__VA_ARGS__)
+
+struct vaapi_encoder {
+	obs_encoder_t *encoder;
+
+	AVBufferRef *vadevice_ref;
+	AVBufferRef *vaframes_ref;
+
+	AVCodec *       vaapi;
+	AVCodecContext *context;
+
+	AVFrame *vframe;
+
+	DARRAY(uint8_t) buffer;
+
+	uint8_t *header;
+	size_t   header_size;
+
+	uint8_t *sei;
+	size_t   sei_size;
+
+	int  height;
+	bool first_packet;
+	bool initialized;
+};
+
+static const char *vaapi_getname(void *unused)
+{
+	UNUSED_PARAMETER(unused);
+	return "FFMPEG VAAPI";
+}
+
+static inline bool valid_format(enum video_format format)
+{
+	return format == VIDEO_FORMAT_NV12;
+}
+
+static void vaapi_video_info(void *data, struct video_scale_info *info)
+{
+	struct vaapi_encoder *enc = data;
+	enum video_format     pref_format;
+
+	pref_format = obs_encoder_get_preferred_video_format(enc->encoder);
+
+	if (!valid_format(pref_format)) {
+		pref_format = valid_format(info->format) ? info->format
+							 : VIDEO_FORMAT_NV12;
+	}
+
+	info->format = pref_format;
+}
+
+static bool vaapi_init_codec(struct vaapi_encoder *enc, const char *path)
+{
+	int ret;
+
+	ret = av_hwdevice_ctx_create(&enc->vadevice_ref, AV_HWDEVICE_TYPE_VAAPI,
+			path, NULL, 0);
+	if (ret < 0) {
+		warn("Failed to create VAAPI device context: %s",
+				av_err2str(ret));
+		return false;
+	}
+
+	enc->vaframes_ref = av_hwframe_ctx_alloc(enc->vadevice_ref);
+	if (!enc->vaframes_ref) {
+		warn("Failed to alloc HW frames context");
+		return false;
+	}
+
+	AVHWFramesContext *frames_ctx =
+			(AVHWFramesContext *)enc->vaframes_ref->data;
+	frames_ctx->format            = AV_PIX_FMT_VAAPI;
+	frames_ctx->sw_format         = AV_PIX_FMT_NV12;
+	frames_ctx->width             = enc->context->width;
+	frames_ctx->height            = enc->context->height;
+	frames_ctx->initial_pool_size = 20;
+
+	ret = av_hwframe_ctx_init(enc->vaframes_ref);
+	if (ret < 0) {
+		warn("Failed to init HW frames context: %s", av_err2str(ret));
+		return false;
+	}
+
+	/* 2. Create software frame and picture */
+	enc->vframe = av_frame_alloc();
+	if (!enc->vframe) {
+		warn("Failed to allocate video frame");
+		return false;
+	}
+
+	enc->vframe->format = enc->context->pix_fmt;
+	enc->vframe->width  = enc->context->width;
+	enc->vframe->height = enc->context->height;
+	enc->vframe->colorspace  = enc->context->colorspace;
+	enc->vframe->color_range = enc->context->color_range;
+
+	ret = av_frame_get_buffer(enc->vframe, base_get_alignment());
+	if (ret < 0) {
+		warn("Failed to allocate vframe: %s", av_err2str(ret));
+		return false;
+	}
+
+	/* 3. set up codec */
+	enc->context->pix_fmt       = AV_PIX_FMT_VAAPI;
+	enc->context->hw_frames_ctx = av_buffer_ref(enc->vaframes_ref);
+
+	ret = avcodec_open2(enc->context, enc->vaapi, NULL);
+	if (ret < 0) {
+		warn("Failed to open VAAPI codec: %s", av_err2str(ret));
+		return false;
+	}
+
+	enc->initialized = true;
+	return true;
+}
+
+static bool vaapi_update(void *data, obs_data_t *settings)
+{
+	struct vaapi_encoder *enc = data;
+
+	const char *device = obs_data_get_string(settings, "vaapi_device");
+
+	int profile = (int)obs_data_get_int(settings, "profile");
+	int bf      = (int)obs_data_get_int(settings, "bf");
+
+	int level      = (int)obs_data_get_int(settings, "level");
+	int bitrate    = (int)obs_data_get_int(settings, "bitrate");
+	int keyint_sec = (int)obs_data_get_int(settings, "keyint_sec");
+
+	int qp      = (int)obs_data_get_int(settings, "qp");
+	int quality = (int)obs_data_get_int(settings, "quality");
+
+	av_opt_set_int(enc->context->priv_data, "qp", qp, 0);
+	av_opt_set_int(enc->context->priv_data, "quality", quality, 0);
+
+	video_t *                       video = obs_encoder_video(enc->encoder);
+	const struct video_output_info *voi   = video_output_get_info(video);
+	struct video_scale_info         info;
+
+	info.format     = voi->format;
+	info.colorspace = voi->colorspace;
+	info.range      = voi->range;
+
+	vaapi_video_info(enc, &info);
+
+	enc->context->profile      = profile;
+	enc->context->max_b_frames = bf;
+	enc->context->level        = level;
+	enc->context->bit_rate     = bitrate * 1000;
+
+	enc->context->width  = obs_encoder_get_width(enc->encoder);
+	enc->context->height = obs_encoder_get_height(enc->encoder);
+
+	enc->context->time_base  = (AVRational){voi->fps_den, voi->fps_num};
+	enc->context->pix_fmt    = obs_to_ffmpeg_video_format(info.format);
+	enc->context->colorspace = info.colorspace == VIDEO_CS_709
+			? AVCOL_SPC_BT709
+			: AVCOL_SPC_BT470BG;
+	enc->context->color_range = info.range == VIDEO_RANGE_FULL
+			? AVCOL_RANGE_JPEG
+			: AVCOL_RANGE_MPEG;
+
+	if (keyint_sec > 0) {
+		enc->context->gop_size =
+				keyint_sec * voi->fps_num / voi->fps_den;
+	} else {
+		enc->context->gop_size = 120;
+	}
+
+	enc->height = enc->context->height;
+
+	info("settings:\n"
+	     "\tdevice:       %s\n"
+	     "\tqp:           %d\n"
+	     "\tquality:      %d\n"
+	     "\tprofile:      %d\n"
+	     "\tlevel:        %d\n"
+	     "\tbitrate:      %d\n"
+	     "\tkeyint:       %d\n"
+	     "\twidth:        %d\n"
+	     "\theight:       %d\n"
+	     "\tb-frames:     %d\n",
+			device, qp, quality, profile, level, bitrate,
+			enc->context->gop_size, enc->context->width,
+			enc->context->height, enc->context->max_b_frames);
+
+	return vaapi_init_codec(enc, device);
+}
+
+static void vaapi_destroy(void *data)
+{
+	struct vaapi_encoder *enc = data;
+
+	if (enc->initialized) {
+		AVPacket pkt   = {0};
+		int      r_pkt = 1;
+
+		while (r_pkt) {
+#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 40, 101)
+			if (avcodec_receive_packet(enc->context, &pkt) < 0)
+				break;
+#else
+			if (avcodec_encode_video2(enc->context, &pkt, NULL,
+					    &r_pkt) < 0)
+				break;
+#endif
+
+			if (r_pkt)
+				av_packet_unref(&pkt);
+		}
+	}
+
+	avcodec_close(enc->context);
+	av_frame_unref(enc->vframe);
+	av_frame_free(&enc->vframe);
+	av_buffer_unref(&enc->vaframes_ref);
+	av_buffer_unref(&enc->vadevice_ref);
+	da_free(enc->buffer);
+	bfree(enc->header);
+	bfree(enc->sei);
+
+	bfree(enc);
+}
+
+static void *vaapi_create(obs_data_t *settings, obs_encoder_t *encoder)
+{
+	struct vaapi_encoder *enc;
+	avcodec_register_all();
+
+	enc          = bzalloc(sizeof(*enc));
+	enc->encoder = encoder;
+
+	int vaapi_codec = (int)obs_data_get_int(settings, "vaapi_codec");
+
+	if (vaapi_codec == AV_CODEC_ID_H264) {
+		enc->vaapi = avcodec_find_encoder_by_name("h264_vaapi");
+	}
+
+	enc->first_packet = true;
+
+	blog(LOG_INFO, "---------------------------------");
+
+	if (!enc->vaapi) {
+		warn("Couldn't find encoder");
+		goto fail;
+	}
+
+	enc->context = avcodec_alloc_context3(enc->vaapi);
+	if (!enc->context) {
+		warn("Failed to create codec context");
+		goto fail;
+	}
+
+	if (!vaapi_update(enc, settings))
+		goto fail;
+
+	return enc;
+
+fail:
+	vaapi_destroy(enc);
+	return NULL;
+}
+
+static inline void copy_data(AVFrame *pic, const struct encoder_frame *frame,
+		int height, enum AVPixelFormat format)
+{
+	int h_chroma_shift, v_chroma_shift;
+	av_pix_fmt_get_chroma_sub_sample(
+			format, &h_chroma_shift, &v_chroma_shift);
+	for (int plane = 0; plane < MAX_AV_PLANES; plane++) {
+		if (!frame->data[plane])
+			continue;
+
+		int frame_rowsize = (int)frame->linesize[plane];
+		int pic_rowsize   = pic->linesize[plane];
+		int bytes         = frame_rowsize < pic_rowsize ? frame_rowsize
+							: pic_rowsize;
+		int plane_height = height >> (plane ? v_chroma_shift : 0);
+
+		for (int y = 0; y < plane_height; y++) {
+			int pos_frame = y * frame_rowsize;
+			int pos_pic   = y * pic_rowsize;
+
+			memcpy(pic->data[plane] + pos_pic,
+					frame->data[plane] + pos_frame, bytes);
+		}
+	}
+}
+
+static bool vaapi_encode(void *data, struct encoder_frame *frame,
+		struct encoder_packet *packet, bool *received_packet)
+{
+	struct vaapi_encoder *enc     = data;
+	AVFrame *             hwframe = NULL;
+	AVPacket              av_pkt;
+	int                   got_packet;
+	int                   ret;
+
+	hwframe = av_frame_alloc();
+	if (!hwframe) {
+		warn("vaapi_encode: failed to allocate hw frame");
+		return false;
+	}
+
+	ret = av_hwframe_get_buffer(enc->vaframes_ref, hwframe, 0);
+	if (ret < 0) {
+		warn("vaapi_encode: failed to get buffer for hw frame: %s",
+				av_err2str(ret));
+		goto fail;
+	}
+
+	copy_data(enc->vframe, frame, enc->height, enc->context->pix_fmt);
+
+	enc->vframe->pts = frame->pts;
+	hwframe->pts     = frame->pts;
+	hwframe->width   = enc->vframe->width;
+	hwframe->height  = enc->vframe->height;
+
+	ret = av_hwframe_transfer_data(hwframe, enc->vframe, 0);
+	if (ret < 0) {
+		warn("vaapi_encode: failed to upload hw frame: %s",
+				av_err2str(ret));
+		goto fail;
+	}
+
+	ret = av_frame_copy_props(hwframe, enc->vframe);
+	if (ret < 0) {
+		warn("vaapi_encode: failed to copy props to hw frame: %s",
+				av_err2str(ret));
+		goto fail;
+	}
+
+	av_init_packet(&av_pkt);
+
+#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 40, 101)
+	ret = avcodec_send_frame(enc->context, hwframe);
+	if (ret == 0)
+		ret = avcodec_receive_packet(enc->context, &av_pkt);
+
+	got_packet = (ret == 0);
+
+	if (ret == AVERROR_EOF || ret == AVERROR(EAGAIN))
+		ret = 0;
+#else
+	ret = avcodec_encode_video2(
+			enc->context, &av_pkt, hwframe, &got_packet);
+#endif
+	if (ret < 0) {
+		warn("vaapi_encode: Error encoding: %s", av_err2str(ret));
+		goto fail;
+	}
+
+	if (got_packet && av_pkt.size) {
+		if (enc->first_packet) {
+			uint8_t *new_packet;
+			size_t   size;
+
+			enc->first_packet = false;
+			obs_extract_avc_headers(av_pkt.data, av_pkt.size,
+					&new_packet, &size, &enc->header,
+					&enc->header_size, &enc->sei,
+					&enc->sei_size);
+
+			da_copy_array(enc->buffer, new_packet, size);
+			bfree(new_packet);
+		} else {
+			da_copy_array(enc->buffer, av_pkt.data, av_pkt.size);
+		}
+
+		packet->pts      = av_pkt.pts;
+		packet->dts      = av_pkt.dts;
+		packet->data     = enc->buffer.array;
+		packet->size     = enc->buffer.num;
+		packet->type     = OBS_ENCODER_VIDEO;
+		packet->keyframe = obs_avc_keyframe(packet->data, packet->size);
+		*received_packet = true;
+	} else {
+		*received_packet = false;
+	}
+
+	av_packet_unref(&av_pkt);
+	av_frame_free(&hwframe);
+	return true;
+
+fail:
+	av_frame_free(&hwframe);
+	return false;
+}
+
+static void set_visible(obs_properties_t *ppts, const char *name, bool visible)
+{
+	obs_property_t *p = obs_properties_get(ppts, name);
+	obs_property_set_visible(p, visible);
+}
+
+static void vaapi_defaults(obs_data_t *settings)
+{
+	obs_data_set_default_string(
+			settings, "vaapi_device", "/dev/dri/renderD128");
+	obs_data_set_default_int(settings, "vaapi_codec", AV_CODEC_ID_H264);
+	obs_data_set_default_int(settings, "profile",
+			FF_PROFILE_H264_CONSTRAINED_BASELINE);
+	obs_data_set_default_int(settings, "level", 40);
+	obs_data_set_default_int(settings, "bitrate", 2500);
+	obs_data_set_default_int(settings, "keyint_sec", 0);
+	obs_data_set_default_int(settings, "bf", 0);
+	obs_data_set_default_int(settings, "qp", 20);
+	obs_data_set_default_int(settings, "quality", 0);
+	obs_data_set_default_int(settings, "rendermode", 0);
+}
+
+static obs_properties_t *vaapi_properties(void *unused)
+{
+	UNUSED_PARAMETER(unused);
+
+	obs_properties_t *props = obs_properties_create();
+	obs_property_t *  list;
+
+	list = obs_properties_add_list(props, "vaapi_device", "VAAPI Device",
+			OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING);
+	char path[128] = "/dev/dri/renderD1";
+	for (int i = 28;; i++) {
+		sprintf(path, "/dev/dri/renderD1%d", i);
+		if (access(path, F_OK) == 0) {
+			char card[128] = "Card: ";
+			sprintf(card, "Card%d: %s", i - 28, path);
+			obs_property_list_add_string(list, card, path);
+		} else {
+			break;
+		}
+	}
+
+	list = obs_properties_add_list(props, "vaapi_codec", "VAAPI Codec",
+			OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
+
+	obs_property_list_add_int(list, "H.264 (default)", AV_CODEC_ID_H264);
+
+	list = obs_properties_add_list(props, "level", "Level",
+			OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
+	obs_property_list_add_int(list, "480p30 (3.0)", 30);
+	obs_property_list_add_int(list, "720p30/480p60  (3.1)", 31);
+	obs_property_list_add_int(
+			list, "Compatibility mode  (4.0 default)", 40);
+	obs_property_list_add_int(list, "720p60/1080p30 (4.1)", 41);
+	obs_property_list_add_int(list, "1080p60 (4.2)", 42);
+
+	obs_properties_add_int(props, "bitrate", obs_module_text("Bitrate"), 0,
+			300000, 50);
+
+	obs_properties_add_int(props, "keyint_sec",
+			obs_module_text("Keyframe Interval (seconds)"), 0, 20,
+			1);
+
+	return props;
+}
+
+static bool vaapi_extra_data(void *data, uint8_t **extra_data, size_t *size)
+{
+	struct vaapi_encoder *enc = data;
+
+	*extra_data = enc->header;
+	*size       = enc->header_size;
+	return true;
+}
+
+static bool vaapi_sei_data(void *data, uint8_t **extra_data, size_t *size)
+{
+	struct vaapi_encoder *enc = data;
+
+	*extra_data = enc->sei;
+	*size       = enc->sei_size;
+	return true;
+}
+
+struct obs_encoder_info vaapi_encoder_info = {
+	.id             = "ffmpeg_vaapi",
+	.type           = OBS_ENCODER_VIDEO,
+	.codec          = "h264",
+	.get_name       = vaapi_getname,
+	.create         = vaapi_create,
+	.destroy        = vaapi_destroy,
+	.encode         = vaapi_encode,
+	.get_defaults   = vaapi_defaults,
+	.get_properties = vaapi_properties,
+	.get_extra_data = vaapi_extra_data,
+	.get_sei_data   = vaapi_sei_data,
+	.get_video_info = vaapi_video_info
+};
+
+#endif

+ 23 - 0
plugins/obs-ffmpeg/obs-ffmpeg.c

@@ -2,6 +2,7 @@
 #include <util/darray.h>
 #include <util/platform.h>
 #include <libavutil/log.h>
+#include <libavutil/avutil.h>
 #include <libavcodec/avcodec.h>
 #include <libavformat/avformat.h>
 #include <pthread.h>
@@ -21,6 +22,14 @@ extern struct obs_encoder_info aac_encoder_info;
 extern struct obs_encoder_info opus_encoder_info;
 extern struct obs_encoder_info nvenc_encoder_info;
 
+#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(55, 27, 100)
+#define LIBAVUTIL_VAAPI_AVAILABLE
+#endif
+
+#ifdef LIBAVUTIL_VAAPI_AVAILABLE
+extern struct obs_encoder_info vaapi_encoder_info;
+#endif
+
 static DARRAY(struct log_context {
 	void *context;
 	char str[4096];
@@ -162,6 +171,14 @@ cleanup:
 
 #endif
 
+#ifdef LIBAVUTIL_VAAPI_AVAILABLE
+static bool vaapi_supported(void)
+{
+	AVCodec *vaenc = avcodec_find_encoder_by_name("h264_vaapi");
+	return !!vaenc;
+}
+#endif
+
 bool obs_module_load(void)
 {
 	da_init(active_log_contexts);
@@ -180,6 +197,12 @@ bool obs_module_load(void)
 		blog(LOG_INFO, "NVENC supported");
 		obs_register_encoder(&nvenc_encoder_info);
 	}
+#if !defined(_WIN32) && defined(LIBAVUTIL_VAAPI_AVAILABLE)
+	if (vaapi_supported()) {
+		blog(LOG_INFO, "FFMPEG VAAPI supported");
+		obs_register_encoder(&vaapi_encoder_info);
+	}
+#endif
 #endif
 	return true;
 }