obs 源码解析笔记

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

发布了46 篇原创文章 · 获赞 6 · 访问量 9404

猜你喜欢

转载自blog.csdn.net/weixin_44259356/article/details/102582493
obs