diff --git a/plugins/obs-ffmpeg/CMakeLists.txt b/plugins/obs-ffmpeg/CMakeLists.txt index 5f61c4918..90460880e 100644 --- a/plugins/obs-ffmpeg/CMakeLists.txt +++ b/plugins/obs-ffmpeg/CMakeLists.txt @@ -11,7 +11,8 @@ include_directories(${FFMPEG_INCLUDE_DIRS}) set(obs-ffmpeg_HEADERS obs-ffmpeg-formats.h - obs-ffmpeg-compat.h) + obs-ffmpeg-compat.h + closest-pixel-format.h) set(obs-ffmpeg_SOURCES obs-ffmpeg.c obs-ffmpeg-aac.c diff --git a/plugins/obs-ffmpeg/closest-pixel-format.h b/plugins/obs-ffmpeg/closest-pixel-format.h new file mode 100644 index 000000000..07e760ddb --- /dev/null +++ b/plugins/obs-ffmpeg/closest-pixel-format.h @@ -0,0 +1,115 @@ +#pragma once + +static const enum AVPixelFormat i420_formats[] = { + AV_PIX_FMT_YUV420P, + AV_PIX_FMT_NV12, + AV_PIX_FMT_NV21, + AV_PIX_FMT_YUYV422, + AV_PIX_FMT_UYVY422, + AV_PIX_FMT_YUV422P, + AV_PIX_FMT_NONE +}; + +static const enum AVPixelFormat nv12_formats[] = { + AV_PIX_FMT_NV12, + AV_PIX_FMT_NV21, + AV_PIX_FMT_YUV420P, + AV_PIX_FMT_YUYV422, + AV_PIX_FMT_UYVY422, + AV_PIX_FMT_NONE +}; + +static const enum AVPixelFormat yuy2_formats[] = { + AV_PIX_FMT_YUYV422, + AV_PIX_FMT_UYVY422, + AV_PIX_FMT_NV12, + AV_PIX_FMT_NV21, + AV_PIX_FMT_YUV420P, + AV_PIX_FMT_NONE +}; + +static const enum AVPixelFormat uyvy_formats[] = { + AV_PIX_FMT_UYVY422, + AV_PIX_FMT_YUYV422, + AV_PIX_FMT_NV12, + AV_PIX_FMT_NV21, + AV_PIX_FMT_YUV420P, + AV_PIX_FMT_NONE +}; + +static const enum AVPixelFormat rgba_formats[] = { + AV_PIX_FMT_RGBA, + AV_PIX_FMT_BGRA, + AV_PIX_FMT_YUYV422, + AV_PIX_FMT_UYVY422, + AV_PIX_FMT_NV12, + AV_PIX_FMT_NV21, + AV_PIX_FMT_NONE +}; + +static const enum AVPixelFormat bgra_formats[] = { + AV_PIX_FMT_BGRA, + AV_PIX_FMT_RGBA, + AV_PIX_FMT_YUYV422, + AV_PIX_FMT_UYVY422, + AV_PIX_FMT_NV12, + AV_PIX_FMT_NV21, + AV_PIX_FMT_NONE +}; + +static enum AVPixelFormat get_best_format( + const enum AVPixelFormat *best, + const enum AVPixelFormat *formats) +{ + while (*best != AV_PIX_FMT_NONE) { + enum AVPixelFormat best_format = *best; + const enum AVPixelFormat *cur_formats = formats; + + while (*cur_formats != AV_PIX_FMT_NONE) { + enum AVPixelFormat avail_format = *cur_formats; + + if (best_format == avail_format) + return best_format; + + cur_formats++; + } + + best++; + } + + return AV_PIX_FMT_NONE; +} + +static inline enum AVPixelFormat get_closest_format( + enum AVPixelFormat format, + const enum AVPixelFormat *formats) +{ + enum AVPixelFormat best_format = AV_PIX_FMT_NONE; + + if (!formats || formats[0] == AV_PIX_FMT_NONE) + return format; + + switch ((int)format) { + + case AV_PIX_FMT_YUV420P: + best_format = get_best_format(i420_formats, formats); + break; + case AV_PIX_FMT_NV12: + best_format = get_best_format(nv12_formats, formats); + break; + case AV_PIX_FMT_YUYV422: + best_format = get_best_format(yuy2_formats, formats); + break; + case AV_PIX_FMT_UYVY422: + best_format = get_best_format(uyvy_formats, formats); + break; + case AV_PIX_FMT_RGBA: + best_format = get_best_format(rgba_formats, formats); + break; + case AV_PIX_FMT_BGRA: + best_format = get_best_format(bgra_formats, formats); + break; + } + + return (best_format == AV_PIX_FMT_NONE) ? formats[0] : best_format; +} diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-output.c b/plugins/obs-ffmpeg/obs-ffmpeg-output.c index 2fc758ccf..bec95fb6c 100644 --- a/plugins/obs-ffmpeg/obs-ffmpeg-output.c +++ b/plugins/obs-ffmpeg/obs-ffmpeg-output.c @@ -27,13 +27,23 @@ #include #include "obs-ffmpeg-formats.h" +#include "closest-pixel-format.h" #include "obs-ffmpeg-compat.h" -//#define OBS_FFMPEG_VIDEO_FORMAT VIDEO_FORMAT_I420 -#define OBS_FFMPEG_VIDEO_FORMAT VIDEO_FORMAT_NV12 - -/* NOTE: much of this stuff is test stuff that was more or less copied from - * the muxing.c ffmpeg example */ +struct ffmpeg_cfg { + const char *url; + int video_bitrate; + int audio_bitrate; + const char *video_encoder; + const char *audio_encoder; + const char *video_settings; + const char *audio_settings; + enum AVPixelFormat format; + int scale_width; + int scale_height; + int width; + int height; +}; struct ffmpeg_data { AVStream *video; @@ -43,18 +53,13 @@ struct ffmpeg_data { AVFormatContext *output; struct SwsContext *swscale; - int video_bitrate; AVPicture dst_picture; AVFrame *vframe; int frame_size; int total_frames; - int width; - int height; - uint64_t start_timestamp; - int audio_bitrate; uint32_t audio_samplerate; enum audio_format audio_format; size_t audio_planes; @@ -64,7 +69,7 @@ struct ffmpeg_data { AVFrame *aframe; int total_samples; - const char *filename_test; + struct ffmpeg_cfg config; bool initialized; }; @@ -89,9 +94,12 @@ struct ffmpeg_output { /* ------------------------------------------------------------------------- */ static bool new_stream(struct ffmpeg_data *data, AVStream **stream, - AVCodec **codec, enum AVCodecID id) + AVCodec **codec, enum AVCodecID id, const char *name) { - *codec = avcodec_find_encoder(id); + *codec = (!!name && *name) ? + avcodec_find_encoder_by_name(name) : + avcodec_find_encoder(id); + if (!*codec) { blog(LOG_WARNING, "Couldn't find encoder '%s'", avcodec_get_name(id)); @@ -109,14 +117,43 @@ static bool new_stream(struct ffmpeg_data *data, AVStream **stream, return true; } +static void parse_params(AVCodecContext *context, char **opts) +{ + if (!context || !context->priv_data) + return; + + while (*opts) { + char *opt = *opts; + char *assign = strchr(opt, '='); + + if (assign) { + char *name = opt; + char *value; + + *assign = 0; + value = assign+1; + + av_opt_set(context->priv_data, name, value, 0); + } + + opts++; + } +} + static bool open_video_codec(struct ffmpeg_data *data) { AVCodecContext *context = data->video->codec; + char **opts = strlist_split(data->config.video_settings, ' ', false); int ret; if (data->vcodec->id == AV_CODEC_ID_H264) av_opt_set(context->priv_data, "preset", "veryfast", 0); + if (opts) { + parse_params(context, opts); + strlist_free(opts); + } + ret = avcodec_open2(context, data->vcodec, NULL); if (ret < 0) { blog(LOG_WARNING, "Failed to open video codec: %s", @@ -148,12 +185,11 @@ static bool open_video_codec(struct ffmpeg_data *data) static bool init_swscale(struct ffmpeg_data *data, AVCodecContext *context) { - enum AVPixelFormat format; - format = obs_to_ffmpeg_video_format(OBS_FFMPEG_VIDEO_FORMAT); - data->swscale = sws_getContext( - context->width, context->height, format, - context->width, context->height, context->pix_fmt, + data->config.width, data->config.height, + data->config.format, + data->config.scale_width, data->config.scale_height, + context->pix_fmt, SWS_BICUBIC, NULL, NULL, NULL); if (!data->swscale) { @@ -166,11 +202,9 @@ static bool init_swscale(struct ffmpeg_data *data, AVCodecContext *context) static bool create_video_stream(struct ffmpeg_data *data) { + enum AVPixelFormat closest_format; AVCodecContext *context; struct obs_video_info ovi; - enum AVPixelFormat vformat; - - vformat = obs_to_ffmpeg_video_format(OBS_FFMPEG_VIDEO_FORMAT); if (!obs_get_video_info(&ovi)) { blog(LOG_WARNING, "No active video"); @@ -178,20 +212,21 @@ static bool create_video_stream(struct ffmpeg_data *data) } if (!new_stream(data, &data->video, &data->vcodec, - data->output->oformat->video_codec)) + data->output->oformat->video_codec, + data->config.video_encoder)) return false; + closest_format = get_closest_format(data->config.format, + data->vcodec->pix_fmts); + context = data->video->codec; - context->codec_id = data->output->oformat->video_codec; - context->bit_rate = data->video_bitrate * 1000; - context->rc_buffer_size = data->video_bitrate * 1000; - context->rc_max_rate = data->video_bitrate * 1000; - context->width = data->width; - context->height = data->height; + context->bit_rate = data->config.video_bitrate * 1000; + context->width = data->config.scale_width; + context->height = data->config.scale_height; context->time_base.num = ovi.fps_den; context->time_base.den = ovi.fps_num; context->gop_size = 120; - context->pix_fmt = vformat; + context->pix_fmt = closest_format; if (data->output->oformat->flags & AVFMT_GLOBALHEADER) context->flags |= CODEC_FLAG_GLOBAL_HEADER; @@ -199,9 +234,13 @@ static bool create_video_stream(struct ffmpeg_data *data) if (!open_video_codec(data)) return false; - if (context->pix_fmt != vformat) + if (context->pix_fmt != data->config.format || + data->config.width != data->config.scale_width || + data->config.height != data->config.scale_height) { + if (!init_swscale(data, context)) return false; + } return true; } @@ -209,8 +248,14 @@ static bool create_video_stream(struct ffmpeg_data *data) static bool open_audio_codec(struct ffmpeg_data *data) { AVCodecContext *context = data->audio->codec; + char **opts = strlist_split(data->config.video_settings, ' ', false); int ret; + if (opts) { + parse_params(context, opts); + strlist_free(opts); + } + data->aframe = av_frame_alloc(); if (!data->aframe) { blog(LOG_WARNING, "Failed to allocate audio frame"); @@ -250,11 +295,12 @@ static bool create_audio_stream(struct ffmpeg_data *data) } if (!new_stream(data, &data->audio, &data->acodec, - data->output->oformat->audio_codec)) + data->output->oformat->audio_codec, + data->config.audio_encoder)) return false; context = data->audio->codec; - context->bit_rate = data->audio_bitrate * 1000; + context->bit_rate = data->config.audio_bitrate * 1000; context->channels = get_audio_channels(aoi.speakers); context->sample_rate = aoi.samples_per_sec; context->sample_fmt = data->acodec->sample_fmts ? @@ -292,19 +338,19 @@ static inline bool open_output_file(struct ffmpeg_data *data) int ret; if ((format->flags & AVFMT_NOFILE) == 0) { - ret = avio_open(&data->output->pb, data->filename_test, + ret = avio_open(&data->output->pb, data->config.url, AVIO_FLAG_WRITE); if (ret < 0) { - blog(LOG_WARNING, "Couldn't open file '%s', %s", - data->filename_test, av_err2str(ret)); + blog(LOG_WARNING, "Couldn't open '%s', %s", + data->config.url, av_err2str(ret)); return false; } } ret = avformat_write_header(data->output, NULL); if (ret < 0) { - blog(LOG_WARNING, "Error opening file '%s': %s", - data->filename_test, av_err2str(ret)); + blog(LOG_WARNING, "Error opening '%s': %s", + data->config.url, av_err2str(ret)); return false; } @@ -348,29 +394,24 @@ static void ffmpeg_data_free(struct ffmpeg_data *data) memset(data, 0, sizeof(struct ffmpeg_data)); } -static bool ffmpeg_data_init(struct ffmpeg_data *data, const char *filename, - int vbitrate, int abitrate, int width, int height) +static bool ffmpeg_data_init(struct ffmpeg_data *data, + struct ffmpeg_cfg *config) { bool is_rtmp = false; memset(data, 0, sizeof(struct ffmpeg_data)); - data->filename_test = filename; - data->video_bitrate = vbitrate; - data->audio_bitrate = abitrate; - data->width = width; - data->height = height; + data->config = *config; - if (!filename || !*filename) + if (!config->url || !*config->url) return false; av_register_all(); avformat_network_init(); - is_rtmp = (astrcmp_n(filename, "rtmp://", 7) == 0); + is_rtmp = (astrcmpi_n(config->url, "rtmp://", 7) == 0); - /* TODO: settings */ avformat_alloc_output_context2(&data->output, NULL, - is_rtmp ? "flv" : NULL, data->filename_test); + is_rtmp ? "flv" : NULL, data->config.url); if (is_rtmp) { data->output->oformat->video_codec = AV_CODEC_ID_H264; data->output->oformat->audio_codec = AV_CODEC_ID_AAC; @@ -488,18 +529,16 @@ static void receive_video(void *param, struct video_data *frame) AVCodecContext *context = data->video->codec; AVPacket packet = {0}; int ret = 0, got_packet; - enum AVPixelFormat format; av_init_packet(&packet); if (!data->start_timestamp) data->start_timestamp = frame->timestamp; - format = obs_to_ffmpeg_video_format(OBS_FFMPEG_VIDEO_FORMAT); - if (context->pix_fmt != format) + if (!!data->swscale) sws_scale(data->swscale, (const uint8_t *const *)frame->data, (const int*)frame->linesize, - 0, context->height, data->dst_picture.data, + 0, data->config.height, data->dst_picture.data, data->dst_picture.linesize); else copy_data(&data->dst_picture, frame, context->height); @@ -711,37 +750,47 @@ static void *write_thread(void *data) static bool try_connect(struct ffmpeg_output *output) { - const char *filename_test; + video_t *video = obs_output_video(output->output); + struct ffmpeg_cfg config; obs_data_t *settings; - int audio_bitrate, video_bitrate; - int width, height; + bool success; int ret; settings = obs_output_get_settings(output->output); - filename_test = obs_data_get_string(settings, "filename"); - video_bitrate = (int)obs_data_get_int(settings, "video_bitrate"); - audio_bitrate = (int)obs_data_get_int(settings, "audio_bitrate"); + config.url = obs_data_get_string(settings, "url"); + config.video_bitrate = (int)obs_data_get_int(settings, "video_bitrate"); + config.audio_bitrate = (int)obs_data_get_int(settings, "audio_bitrate"); + config.video_encoder = obs_data_get_string(settings, "video_encoder"); + config.audio_encoder = obs_data_get_string(settings, "audio_encoder"); + config.video_settings = obs_data_get_string(settings, "video_settings"); + config.audio_settings = obs_data_get_string(settings, "audio_settings"); + config.scale_width = (int)obs_data_get_int(settings, "scale_width"); + config.scale_height = (int)obs_data_get_int(settings, "scale_height"); + config.width = (int)obs_output_get_width(output->output); + config.height = (int)obs_output_get_height(output->output); + config.format = obs_to_ffmpeg_video_format( + video_output_get_format(video)); + + if (config.format == AV_PIX_FMT_NONE) { + blog(LOG_DEBUG, "invalid pixel format used for FFmpeg output"); + return false; + } + + if (!config.scale_width) + config.scale_width = config.width; + if (!config.scale_height) + config.scale_height = config.height; + + success = ffmpeg_data_init(&output->ff_data, &config); obs_data_release(settings); - if (!filename_test || !*filename_test) - return false; - - width = (int)obs_output_get_width(output->output); - height = (int)obs_output_get_height(output->output); - - if (!ffmpeg_data_init(&output->ff_data, filename_test, - video_bitrate, audio_bitrate, - width, height)) + if (!success) return false; struct audio_convert_info aci = { .format = output->ff_data.audio_format }; - struct video_scale_info vsi = { - .format = OBS_FFMPEG_VIDEO_FORMAT - }; - output->active = true; if (!obs_output_can_begin_data_capture(output->output, 0)) @@ -755,7 +804,7 @@ static bool try_connect(struct ffmpeg_output *output) return false; } - obs_output_set_video_conversion(output->output, &vsi); + obs_output_set_video_conversion(output->output, NULL); obs_output_set_audio_conversion(output->output, &aci); obs_output_begin_data_capture(output->output, 0); output->write_thread_active = true;