From a64ae11bce8ed9a7c8f1deba3338f77595dba903 Mon Sep 17 00:00:00 2001 From: Thomas Crider Date: Sun, 8 Apr 2018 15:05:22 -0400 Subject: [PATCH] obs-ffmpeg: Add linux VAAPI h.264 encoding support Closes obsproject/obs-studio#1482 Closes obsproject/obs-studio#1256 --- CI/install-dependencies-linux.sh | 17 +- plugins/obs-ffmpeg/CMakeLists.txt | 8 + plugins/obs-ffmpeg/obs-ffmpeg-vaapi.c | 536 ++++++++++++++++++++++++++ plugins/obs-ffmpeg/obs-ffmpeg.c | 23 ++ 4 files changed, 576 insertions(+), 8 deletions(-) create mode 100644 plugins/obs-ffmpeg/obs-ffmpeg-vaapi.c diff --git a/CI/install-dependencies-linux.sh b/CI/install-dependencies-linux.sh index 2cbd8de9b..21b68b688 100755 --- a/CI/install-dependencies-linux.sh +++ b/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 \ diff --git a/plugins/obs-ffmpeg/CMakeLists.txt b/plugins/obs-ffmpeg/CMakeLists.txt index 4c04ab1ed..1fe927e1e 100644 --- a/plugins/obs-ffmpeg/CMakeLists.txt +++ b/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}) diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-vaapi.c b/plugins/obs-ffmpeg/obs-ffmpeg-vaapi.c new file mode 100644 index 000000000..6f6e9b699 --- /dev/null +++ b/plugins/obs-ffmpeg/obs-ffmpeg-vaapi.c @@ -0,0 +1,536 @@ +/****************************************************************************** + Copyright (C) 2016 by Hugh Bailey + + 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 . +******************************************************************************/ + +#include + +#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(55, 27, 100) + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#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 diff --git a/plugins/obs-ffmpeg/obs-ffmpeg.c b/plugins/obs-ffmpeg/obs-ffmpeg.c index f60a8fb76..fb04c4cf0 100644 --- a/plugins/obs-ffmpeg/obs-ffmpeg.c +++ b/plugins/obs-ffmpeg/obs-ffmpeg.c @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -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; }