obs-ffmpeg: Make FFmpeg a usable output

This makes FFmpeg usable as an output, and removes or changes most of
the code that was originally intended for testing purposes.

Changes the settings for the FFmpeg output to the following:
* url:             Sets the output URL or file path
* video_bitrate:   Sets the video bitrate
* audio_bitrate:   Sets the audio bitrate
* video_encoder:   Sets the video encoder (by name, blank for default)
* audio_encoder:   Sets the audio encoder (by name, blank for default)
* video_settings:  Sets custom video encoder FFmpeg settings
* audio_settings:  Sets custom audio encoder FFmpeg settings
* scale_width:     Image scale width (0 if none)
* scale_height:    Image scale height (0 if none)

The reason why scale_width and scale_height are provided is because it
may internally convert formats, and it may be a bit more optimal to use
that scaler instead of the pre-output scaler just in case it already has
to convert formats internally anyway (though you can do it either way
you wish).

Video format handling has also changed; it will now attempt to use the
closest format to the current format if available for a given video
codec.
This commit is contained in:
jp9000 2015-01-25 10:34:58 -08:00
parent fcb8561d6c
commit d3f92ca5d6
3 changed files with 238 additions and 73 deletions

View File

@ -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

View File

@ -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;
}

View File

@ -27,13 +27,23 @@
#include <libswscale/swscale.h>
#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;