|
|
@@ -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");
|
|
|
- obs_data_release(settings);
|
|
|
-
|
|
|
- if (!filename_test || !*filename_test)
|
|
|
+ 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;
|
|
|
|
|
|
- width = (int)obs_output_get_width(output->output);
|
|
|
- height = (int)obs_output_get_height(output->output);
|
|
|
+ success = ffmpeg_data_init(&output->ff_data, &config);
|
|
|
+ obs_data_release(settings);
|
|
|
|
|
|
- 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;
|