obs 源码解析笔记
由于obs rtp音频传输有问题,所以可能需要修改obs源码,学习了两天,发现官方文档有些混乱,国内有关说明又少,特此记录,也方便以后自己查阅。这里主要涉及工作有关源码其他基本略过,除非重要。
原文链接:https://blog.csdn.net/weixin_44259356/article/details/102582493
obs源码编译
见我的另一篇文章:VS2017-OBS24.03源码编译
https://blog.csdn.net/weixin_44259356/article/details/102548121
obs源码简介
obs有两套源码,新版源码采用微内核思想,核心功能很少,大部分功能通过插件的方式实现,方便后续维护开发,这里我用的是最新版源码24.03。
插件模块头介绍
libobs / obs-module.h –用于创建插件模块的主要标头。该文件自动包括以下文件:
libobs / obs.h –主libobs标头。该文件自动包括以下文件:
libobs / obs-source.h –用于在插件模块中实现源代码
libobs / obs-output.h –用于在插件模块中实现输出
libobs / obs-encoder.h –用于在插件模块中实现编码器
libobs / obs-service.h –用于在插件模块中实现服务
libobs / obs-data.h –用于管理libobs对象的设置
libobs / obs-properties.h –用于为libobs对象生成属性
libobs / graphics / graphics.h –用于图形渲染
目录结构
结构如下:
其中比较重要的是:
ALL_BUILD
整个项目通过它来启动,直接启动obs会有部分文件找不到。
obs
主程序
obs-ffmpeg
obs通过调用ffmpeg api来实现推流录像等功能,以插件的方式实现
obs-x264
也是插件方式实现x264编码,这里后续如果要为obs增加编码方式比如h265等可以参考此插件源码
w32-pthreads
obs发送数据流线程,用的是另外一个开源项目,功能比较强大,详细信息可以打开源码注释查看,这里特别注意线程里的互斥锁,我在调试中很多obs的问题都是使用没有及时释放的资源引起的。
libobs
obs视频采集线程
推流部分关键函数源码简介
点击开始录制,obs将会调用ui部分代码读取录像设置参数:
位于\UI\window-basic-main-outputs.cpp
bool AdvancedOutput::StartRecording()
{
const char *path;
const char *recFormat;
const char *filenameFormat;
bool noSpace = false;
bool overwriteIfExists = false;
if (!useStreamEncoder) {
if (!ffmpegOutput) {
UpdateRecordingSettings();
}
} else if (!obs_output_active(streamOutput)) {
UpdateStreamSettings();
}
UpdateAudioSettings();
if (!Active())
SetupOutputs();
if (!ffmpegOutput || ffmpegRecording) {
path = config_get_string(main->Config(), "AdvOut",
ffmpegRecording ? "FFFilePath"
: "RecFilePath");
recFormat = config_get_string(main->Config(), "AdvOut",
ffmpegRecording ? "FFExtension"
: "RecFormat");
filenameFormat = config_get_string(main->Config(), "Output",
"FilenameFormatting");
overwriteIfExists = config_get_bool(main->Config(), "Output",
"OverwriteIfExists");
noSpace = config_get_bool(main->Config(), "AdvOut",
ffmpegRecording
? "FFFileNameWithoutSpace"
: "RecFileNameWithoutSpace");
os_dir_t *dir = path && path[0] ? os_opendir(path) : nullptr;
if (!dir) {
if (main->isVisible())
OBSMessageBox::warning(
main, QTStr("Output.BadPath.Title"),
QTStr("Output.BadPath.Text"));
else
main->SysTrayNotify(
QTStr("Output.BadPath.Text"),
QSystemTrayIcon::Warning);
return false;
}
os_closedir(dir);
string strPath;
strPath += path;
char lastChar = strPath.back();
if (lastChar != '/' && lastChar != '\\')
strPath += "/";
strPath += GenerateSpecifiedFilename(recFormat, noSpace,
filenameFormat);
ensure_directory_exists(strPath);
if (!overwriteIfExists)
FindBestFilename(strPath, noSpace);
obs_data_t *settings = obs_data_create();
obs_data_set_string(settings, ffmpegRecording ? "url" : "path",
strPath.c_str());
obs_output_update(fileOutput, settings);
obs_data_release(settings);
}
if (!obs_output_start(fileOutput)) {
QString error_reason;
const char *error = obs_output_get_last_error(fileOutput);
if (error)
error_reason = QT_UTF8(error);
else
error_reason = QTStr("Output.StartFailedGeneric");
QMessageBox::critical(main,
QTStr("Output.StartRecordingFailed"),
error_reason);
return false;
}
return true;
}
读取完毕后,将参数应用于obs同时修改视频编码和音频编码设置,同时更新obs程序运行参数
inline void AdvancedOutput::SetupFFmpeg()
{
const char *url = config_get_string(main->Config(), "AdvOut", "FFURL");
int vBitrate = config_get_int(main->Config(), "AdvOut", "FFVBitrate");
int gopSize = config_get_int(main->Config(), "AdvOut", "FFVGOPSize");
bool rescale = config_get_bool(main->Config(), "AdvOut", "FFRescale");
const char *rescaleRes =
config_get_string(main->Config(), "AdvOut", "FFRescaleRes");
const char *formatName =
config_get_string(main->Config(), "AdvOut", "FFFormat");
const char *mimeType =
config_get_string(main->Config(), "AdvOut", "FFFormatMimeType");
const char *muxCustom =
config_get_string(main->Config(), "AdvOut", "FFMCustom");
const char *vEncoder =
config_get_string(main->Config(), "AdvOut", "FFVEncoder");
int vEncoderId =
config_get_int(main->Config(), "AdvOut", "FFVEncoderId");
const char *vEncCustom =
config_get_string(main->Config(), "AdvOut", "FFVCustom");
int aBitrate = config_get_int(main->Config(), "AdvOut", "FFABitrate");
int aMixes = config_get_int(main->Config(), "AdvOut", "FFAudioMixes");
const char *aEncoder =
config_get_string(main->Config(), "AdvOut", "FFAEncoder");
int aEncoderId =
config_get_int(main->Config(), "AdvOut", "FFAEncoderId");
const char *aEncCustom =
config_get_string(main->Config(), "AdvOut", "FFACustom");
obs_data_t *settings = obs_data_create();
obs_data_set_string(settings, "url", url);
obs_data_set_string(settings, "format_name", formatName);
obs_data_set_string(settings, "format_mime_type", mimeType);
obs_data_set_string(settings, "muxer_settings", muxCustom);
obs_data_set_int(settings, "gop_size", gopSize);
obs_data_set_int(settings, "video_bitrate", vBitrate);
obs_data_set_string(settings, "video_encoder", vEncoder);
obs_data_set_int(settings, "video_encoder_id", vEncoderId);
obs_data_set_string(settings, "video_settings", vEncCustom);
obs_data_set_int(settings, "audio_bitrate", aBitrate);
obs_data_set_string(settings, "audio_encoder", aEncoder);
obs_data_set_int(settings, "audio_encoder_id", aEncoderId);
obs_data_set_string(settings, "audio_settings", aEncCustom);
if (rescale && rescaleRes && *rescaleRes) {
int width;
int height;
int val = sscanf(rescaleRes, "%dx%d", &width, &height);
if (val == 2 && width && height) {
obs_data_set_int(settings, "scale_width", width);
obs_data_set_int(settings, "scale_height", height);
}
}
obs_output_set_mixers(fileOutput, aMixes);
obs_output_set_media(fileOutput, obs_get_video(), obs_get_audio());
obs_output_update(fileOutput, settings);
obs_data_release(settings);
}
参数设置成功后开始准备输出,位于:\libobs\obs-output.c
bool obs_output_start(obs_output_t *output)
{
bool encoded;
bool has_service;
if (!obs_output_valid(output, "obs_output_start"))
return false;
if (!output->context.data)
return false;
has_service = (output->info.flags & OBS_OUTPUT_SERVICE) != 0;
if (has_service && !obs_service_initialize(output->service, output))
return false;
encoded = (output->info.flags & OBS_OUTPUT_ENCODED) != 0;
if (encoded && output->delay_sec) {
return obs_output_delay_start(output);
} else {
if (obs_output_actual_start(output)) {
do_output_signal(output, "starting");
return true;
}
return false;
}
}
输出流准备好了之后,创建输出函数线程,开始调用ffmpeg有关函数准备输出。位于\obs-ffmpeg\obs-ffmpeg-output.c
static bool ffmpeg_output_start(void *data)
{
struct ffmpeg_output *output = data;
int ret;
if (output->connecting)
return false;
os_atomic_set_bool(&output->stopping, false);
output->audio_start_ts = 0;
output->video_start_ts = 0;
output->total_bytes = 0;
ret = pthread_create(&output->start_thread, NULL, start_thread, output);
return (output->connecting = (ret == 0));
}
然后创建ffmpeg输出,设置输出参数,
**avformat_alloc_output_context2(&data->output, output_format, NULL,
NULL);**
上面是调用ffmpeg关键代码设置输出方式,如rtp,rtmp等。下面是完整函数。
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->config = *config;
data->num_audio_streams = config->audio_mix_count;
data->audio_tracks = config->audio_tracks;
if (!config->url || !*config->url)
return false;
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58, 9, 100)
av_register_all();
#endif
avformat_network_init();
is_rtmp = (astrcmpi_n(config->url, "rtmp://", 7) == 0);
AVOutputFormat *output_format = av_guess_format(
is_rtmp ? "flv" : data->config.format_name, data->config.url,
is_rtmp ? NULL : data->config.format_mime_type);
if (output_format == NULL) {
ffmpeg_log_error(
LOG_WARNING, data,
"Couldn't find matching output format with "
"parameters: name=%s, url=%s, mime=%s",
safe_str(is_rtmp ? "flv" : data->config.format_name),
safe_str(data->config.url),
safe_str(is_rtmp ? NULL
: data->config.format_mime_type));
goto fail;
}
avformat_alloc_output_context2(&data->output, output_format, NULL,
NULL);
if (!data->output) {
ffmpeg_log_error(LOG_WARNING, data,
"Couldn't create avformat context");
goto fail;
}
if (is_rtmp) {
data->output->oformat->video_codec = AV_CODEC_ID_H264;
data->output->oformat->audio_codec = AV_CODEC_ID_AAC;
} else {
if (data->config.format_name)
set_encoder_ids(data);
}
if (!init_streams(data))
goto fail;
if (!open_output_file(data))
goto fail;
av_dump_format(data->output, 0, NULL, 1);
data->initialized = true;
return true;
fail:
blog(LOG_WARNING, "ffmpeg_data_init failed");
return false;
}
写入文件头,调用ffmpeg函数代码为:
ret = avformat_write_header(data->output, &dict);
以下为完整函数代码:
static inline bool open_output_file(struct ffmpeg_data *data)
{
AVOutputFormat *format = data->output->oformat;
int ret;
AVDictionary *dict = NULL;
if ((ret = av_dict_parse_string(&dict, data->config.muxer_settings, "=",
" ", 0))) {
ffmpeg_log_error(LOG_WARNING, data,
"Failed to parse muxer settings: %s\n%s",
av_err2str(ret), data->config.muxer_settings);
av_dict_free(&dict);
return false;
}
if (av_dict_count(dict) > 0) {
struct dstr str = {0};
AVDictionaryEntry *entry = NULL;
while ((entry = av_dict_get(dict, "", entry,
AV_DICT_IGNORE_SUFFIX)))
dstr_catf(&str, "\n\t%s=%s", entry->key, entry->value);
blog(LOG_INFO, "Using muxer settings: %s", str.array);
dstr_free(&str);
}
if ((format->flags & AVFMT_NOFILE) == 0) {
ret = avio_open2(&data->output->pb, data->config.url,
AVIO_FLAG_WRITE, NULL, &dict);
if (ret < 0) {
ffmpeg_log_error(LOG_WARNING, data,
"Couldn't open '%s', %s",
data->config.url, av_err2str(ret));
av_dict_free(&dict);
return false;
}
}
strncpy(data->output->filename, data->config.url,
sizeof(data->output->filename));
data->output->filename[sizeof(data->output->filename) - 1] = 0;
ret = avformat_write_header(data->output, &dict);
if (ret < 0) {
ffmpeg_log_error(LOG_WARNING, data, "Error opening '%s': %s",
data->config.url, av_err2str(ret));
return false;
}
if (av_dict_count(dict) > 0) {
struct dstr str = {0};
AVDictionaryEntry *entry = NULL;
while ((entry = av_dict_get(dict, "", entry,
AV_DICT_IGNORE_SUFFIX)))
dstr_catf(&str, "\n\t%s=%s", entry->key, entry->value);
blog(LOG_INFO, "Invalid muxer settings: %s", str.array);
dstr_free(&str);
}
av_dict_free(&dict);
return true;
}
创建数据线程,设置视频流,音频流编码,然后捕获数据流开始输出
static bool try_connect(struct ffmpeg_output *output)
{
video_t *video = obs_output_video(output->output);
const struct video_output_info *voi = video_output_get_info(video);
struct ffmpeg_cfg config;
obs_data_t *settings;
bool success;
int ret;
settings = obs_output_get_settings(output->output);
obs_data_set_default_int(settings, "gop_size", 120);
config.url = obs_data_get_string(settings, "url");
config.format_name = get_string_or_null(settings, "format_name");
config.format_mime_type =
get_string_or_null(settings, "format_mime_type");
config.muxer_settings = obs_data_get_string(settings, "muxer_settings");
config.video_bitrate = (int)obs_data_get_int(settings, "video_bitrate");
config.audio_bitrate = (int)obs_data_get_int(settings, "audio_bitrate");
config.gop_size = (int)obs_data_get_int(settings, "gop_size");
config.video_encoder = get_string_or_null(settings, "video_encoder");
config.video_encoder_id =
(int)obs_data_get_int(settings, "video_encoder_id");
config.audio_encoder = get_string_or_null(settings, "audio_encoder");
config.audio_encoder_id =
(int)obs_data_get_int(settings, "audio_encoder_id");
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));
config.audio_tracks = (int)obs_output_get_mixers(output->output);
config.audio_mix_count = get_audio_mix_count(config.audio_tracks);
if (format_is_yuv(voi->format)) {
config.color_range = voi->range == VIDEO_RANGE_FULL
? AVCOL_RANGE_JPEG
: AVCOL_RANGE_MPEG;
config.color_space = voi->colorspace == VIDEO_CS_709
? AVCOL_SPC_BT709
: AVCOL_SPC_BT470BG;
} else {
config.color_range = AVCOL_RANGE_UNSPECIFIED;
config.color_space = AVCOL_SPC_RGB;
}
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 (!success) {
if (output->ff_data.last_error) {
obs_output_set_last_error(output->output,
output->ff_data.last_error);
}
ffmpeg_data_free(&output->ff_data);
return false;
}
struct audio_convert_info aci = {.format =
output->ff_data.audio_format};
output->active = true;
if (!obs_output_can_begin_data_capture(output->output, 0))
return false;
ret = pthread_create(&output->write_thread, NULL, write_thread, output);
if (ret != 0) {
ffmpeg_log_error(LOG_WARNING, &output->ff_data,
"ffmpeg_output_start: failed to create write "
"thread.");
ffmpeg_output_full_stop(output);
return false;
}
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;
return true;
}
线程设置完毕后,开始通过线程写入数据,输出
static void *write_thread(void *data)
{
struct ffmpeg_output *output = data;
while (os_sem_wait(output->write_sem) == 0) {
/* check to see if shutting down */
if (os_event_try(output->stop_event) == 0)
break;
int ret = process_packet(output);
if (ret != 0) {
int code = OBS_OUTPUT_ERROR;
pthread_detach(output->write_thread);
output->write_thread_active = false;
if (ret == -ENOSPC)
code = OBS_OUTPUT_NO_SPACE;
obs_output_signal_stop(output->output, code);
ffmpeg_deactivate(output);
break;
}
}
output->active = false;
return NULL;
}
线程将反复调用下面函数处理数据包,注意这里线程有互斥锁判断,修改不当容易造成程序崩溃。
static int process_packet(struct ffmpeg_output *output)
{
AVPacket packet;
bool new_packet = false;
int ret;
pthread_mutex_lock(&output->write_mutex);
if (output->packets.num) {
packet = output->packets.array[0];
da_erase(output->packets, 0);
new_packet = true;
}
pthread_mutex_unlock(&output->write_mutex);
if (!new_packet)
return 0;
/*blog(LOG_DEBUG, "size = %d, flags = %lX, stream = %d, "
"packets queued: %lu",
packet.size, packet.flags,
packet.stream_index, output->packets.num);*/
if (stopping(output)) {
uint64_t sys_ts = get_packet_sys_dts(output, &packet);
if (sys_ts >= output->stop_ts) {
ffmpeg_output_full_stop(output);
return 0;
}
}
output->total_bytes += packet.size;
ret = av_interleaved_write_frame(output->ff_data.output, &packet);
if (ret < 0) {
av_free_packet(&packet);
ffmpeg_log_error(LOG_WARNING, &output->ff_data,
"receive_audio: Error writing packet: %s",
av_err2str(ret));
return ret;
}
return 0;
}
点击停止录制后,线程将会一步步释放资源。工作涉及整个流程大体如上,后续可能会补充。
附录国人写的一个ffmpeg官方函数调用实现rtmp或者rtp数据传输开源项目:
https://github.com/leixiaohua1020/simplest_ffmpeg_streamer/blob/master/simplest_ffmpeg_streamer/simplest_ffmpeg_streamer.cpp