【obs-studio开源项目从入门到放弃】obs_graphics_thread 视频采集渲染线程理解


前言

libobs是整个项目的核心库,负责各种插件的加载、视频的渲染,图像的混合、视频的编码输出,音频的混音输出,其中一共创建了以下7个线程

  • 视频渲染线程 obs_graphics_thread
  • 视频编码输出线程 video_thread
  • 音频混音输出线程 audio_thread
  • 快捷键处理线程 obs_hotkey_thread
  • 推流掉线重连线程 reconnect_thread
  • 结束推流线程 end_data_capture_thread
  • GPU编码线程 gpu_encode_thread

弄清楚这些线程的创建时机,线程所负责的工作内容,以及线程之间的配合,非常有助于我们理解obs的内部是怎么工作的。下面贴一下在libobs项目搜索的所有创建线程的地方。
libobs.dll创建线程函数的调用关键代码

查找全部 "pthread_create", 查找结果 1, 当前项目: libobs\libobs.vcxproj, ""
  D:\dev\opensource\obs-studio\libobs\obs.c(431):	errorcode = pthread_create(&video->video_thread, NULL,
  D:\dev\opensource\obs-studio\libobs\obs.c(752):	if (pthread_create(&hotkeys->hotkey_thread, NULL, obs_hotkey_thread,
  D:\dev\opensource\obs-studio\libobs\obs-output.c(2229):	ret = pthread_create(&output->end_data_capture_thread, NULL,
  D:\dev\opensource\obs-studio\libobs\obs-output.c(2303):	ret = pthread_create(&output->reconnect_thread, NULL, &reconnect_thread,
  D:\dev\opensource\obs-studio\libobs\obs-video-gpu-encode.c(178):	if (pthread_create(&video->gpu_encode_thread, NULL, gpu_encode_thread,
  D:\dev\opensource\obs-studio\libobs\media-io\audio-io.c(429):	if (pthread_create(&out->thread, NULL, audio_thread, out) != 0)
  D:\dev\opensource\obs-studio\libobs\media-io\video-io.c(251):	if (pthread_create(&out->thread, NULL, video_thread, out) != 0)
 匹配行: 7  匹配文件数: 5  已搜索文件总数: 163

obs-studio调试前的准备工作

准备好一份可以断点调试的vs2019项目,具体编译步骤参考我的这篇文章windows10使用vs2019编译obs-studio

视频渲染线程的创建时机

通过vs2019强大的调试功能很容易获取到创建 obs_graphics_thread的调用堆栈。只需要在创建视频渲染线程的代码打上断点,F5启动,alt+7 打开调用堆栈窗口就可以看到如下的函数调用信息。

	// obs_init_video 创建了obs_graphics_thread线程
    errorcode = pthread_create(&video->video_thread, NULL, obs_graphics_thread, obs);

>	obs.dll!obs_init_video(obs_video_info * ovi)431	C
 	obs.dll!obs_reset_video(obs_video_info * ovi)1174	C
 	obs64.exe!AttemptToResetVideo(obs_video_info * ovi)4315	C++
 	obs64.exe!OBSBasic::ResetVideo()4429	C++
 	obs64.exe!OBSBasic::OBSInit()1775	C++
 	obs64.exe!OBSApp::OBSInit()1474	C++
 	obs64.exe!run_program(std::basic_fstream<char,std::char_traits<char>> & logFile, int argc, char * * argv)2138	C++
 	obs64.exe!main(int argc, char * * argv)2839	C++
 	obs64.exe!WinMain(HINSTANCE__ * __formal, HINSTANCE__ * __formal, char * __formal, int __formal)97	C++

通过对调用堆栈的分析,可以看到视频渲染线程的创建是在WinMain主线程中创建的。obs_reset_video 是libobs对外提供的视频初始化接口,该接口会重置obs对外视频输出的分辨率、帧率、视频格式。需要注意的一点是,当视频输出处于活动的状态时(此时正在录像或者推流)是不能重置这些视频参数的。具体细节可以阅读源码。

/**
 * Sets base video output base resolution/fps/format.
 *
 * @note This data cannot be changed if an output is currently active.
 * @note The graphics module cannot be changed without fully destroying the
 *       OBS context.
 *
 * @param   ovi  Pointer to an obs_video_info structure containing the
 *               specification of the graphics subsystem,
 * @return       OBS_VIDEO_SUCCESS if successful
 *               OBS_VIDEO_NOT_SUPPORTED if the adapter lacks capabilities
 *               OBS_VIDEO_INVALID_PARAM if a parameter is invalid
 *               OBS_VIDEO_CURRENTLY_ACTIVE if video is currently active
 *               OBS_VIDEO_MODULE_NOT_FOUND if the graphics module is not found
 *               OBS_VIDEO_FAIL for generic failure
 */
EXPORT int obs_reset_video(struct obs_video_info *ovi);

视频渲染线程的工作内容

主要有以下三点

  1. 根据设置的视频输出帧率,每间隔固定时间处理所有源的输入,并融合成一张图像缓存起来
  2. 如果开启推流和录像,则通过信号通知视频输出线程编码输出视频帧
  3. 渲染视频到UI窗口,使用户可以编辑推流画面

真正的工作函数 obs_graphics_thread_loop

接下来通过源码注释来详细说明,只保留关键代码说明,以下贴的源码删除了一些调试代码,否则看起来太罗嗦。windows平台真正的工作函数是obs_graphics_thread_loop,以前的版本没有单独封装出来这个函数,直接放在obs_graphics_thread里面做循环,不过这些都不重要。我们以最新的版本(27.1.3)源码为基准来做说明。

bool obs_graphics_thread_loop(struct obs_graphics_context *context)
{
	/* defer loop break to clean up sources */
	//检查推流是否停止,用来控制当前线程的退出
	const bool stop_requested = video_output_stopped(obs->video.video);

	// 记录处理当前帧的绝对时间,用来统计一帧图像耗时
	uint64_t frame_start = os_gettime_ns();
	uint64_t frame_time_ns;
	// raw_active表示是否开始视频输出,控制着视频渲染线程和视频输出线程之间的通信
	bool raw_active = obs->video.raw_active > 0;
#ifdef _WIN32
	const bool gpu_active = obs->video.gpu_encoder_active > 0;
	const bool active = raw_active || gpu_active;
#else
	const bool gpu_active = 0;
	const bool active = raw_active;
#endif
	// 清理统计信息的缓存
	if (!context->was_active && active)
		clear_base_frame_data();
	if (!context->raw_was_active && raw_active)
		clear_raw_frame_data();
#ifdef _WIN32
	if (!context->gpu_was_active && gpu_active)
		clear_gpu_frame_data();

	context->gpu_was_active = gpu_active;
#endif
	context->raw_was_active = raw_active;
	context->was_active = active;

	gs_enter_context(obs->video.graphics);
	gs_begin_frame();
	gs_leave_context();

	//调用所有source的tick函数,更新添加的所有视频源一帧图像
	//检查并处理视频的状态(show or hide)是否需要改变
	context->last_time = tick_sources(obs->video.video_time, context->last_time);

	//执行需要在 obs_graphics_thread线程中处理的任务,通过obs_queue_task注册任务
	//通过全局搜索 obs_queue_task注册任务接口,只有窗口采集,桌面采集两个源里面用到
	//将这两个源的销毁工作放到渲染线程中去做
	execute_graphics_tasks();
	
	//负责图像的合成并缓存到视频帧队列,此时保存的使原视视频格式
	//如果开启推流或者录像,会发送信号通知视频输出线程从视频帧队列取出一帧视频编码输出
	output_frame(raw_active, gpu_active);

	//渲染视频帧到UI窗口
	render_displays();

	//计算处理一帧视频的耗时
	frame_time_ns = os_gettime_ns() - frame_start;

	//休眠 等待下一个间隔的到来
	video_sleep(&obs->video, raw_active, gpu_active, &obs->video.video_time,
		    context->interval);

	context->frame_time_total_ns += frame_time_ns;
	context->fps_total_ns += (obs->video.video_time - context->last_time);
	context->fps_total_frames++;
	
	//每隔1s统计一下实际帧率,处理一帧的平均耗时
	if (context->fps_total_ns >= 1000000000ULL) {
		// 计算视频的实际帧率
		obs->video.video_fps =
			(double)context->fps_total_frames /
			((double)context->fps_total_ns / 1000000000.0);
		//计算处理一帧图像的平均耗时,可以理解为生成一帧图像的耗时
		//如果耗时大于设置fps的帧间隔,则视频处理能力不足,推流的实际帧率不满足设置的帧率
		//视频的处理存在性能瓶颈,需要做优化处理
		obs->video.video_avg_frame_time_ns =
			context->frame_time_total_ns /
			(uint64_t)context->fps_total_frames;

		context->frame_time_total_ns = 0;
		context->fps_total_ns = 0;
		context->fps_total_frames = 0;
	}
	
	return !stop_requested;
}

视频渲染线程与视频输出线程之间的配合

视频渲染线程负责生产视频帧,视频输出线程负责消耗视频帧,两个线程共同操作一个视频帧缓存队列,是一个标准的1对1生产者-消费者模型
两个线程的操作的视频帧缓存队列定义在 obs_core -> obs_core_video -> video_output -> cache
可以看到是一个数组实现的一个固定大小的队列

struct obs_core {
	...
	struct obs_core_video video;
}

typedef struct video_output video_t;
struct obs_core_video {
	...
	video_t *video;
}

struct video_output {
	...
	struct cached_frame_info cache[MAX_CACHE_SIZE];
}

视频渲染线程通知视频输出线程的入口在output_frame函数里面,接下来还是以源码注释的形式,详细分析视频帧是怎么通知到视频输出线程去做编码发送的。

static inline void output_frame(bool raw_active, const bool gpu_active)
{
	//获取全局对象obs中的obs_core_video 方便后续调用
	struct obs_core_video *video = &obs->video;
	//当前纹理坐标 前一个纹理坐标
	int cur_texture = video->cur_texture;
	int prev_texture = cur_texture == 0 ? NUM_TEXTURES - 1
					    : cur_texture - 1;
	//定义栈变量frame 用来存放从显存里面map出来的图像数据
	struct video_data frame;
	bool frame_ready = 0;

	memset(&frame, 0, sizeof(struct video_data));

	//进入obs图形子系统
	gs_enter_context(video->graphics);

	//渲染一帧视频纹理到output_texture
	render_video(video, raw_active, gpu_active, cur_texture);
	
	if (raw_active) {
		//通过调用obs的图形子系统api  gs_stagesurface_map 从surfaces获取到显存的图像数据指针
		//将图像数据指针存放在上面定义的栈变量frame中,
		//obs图形子系统对openGL D3D的图形api进行了封装,对外提供统一接口进行图像的渲染和存取
		//这也是obs-studio项目中牛逼的一个技术点
		frame_ready = download_frame(video, prev_texture, &frame);
	}
	gs_flush();
	//离开obs图形子系统
	gs_leave_context();

	//如果开启推流或者录制,并且 download_frame成功,则输出视频帧
	if (raw_active && frame_ready) {
		struct obs_vframe_info vframe_info;
		circlebuf_pop_front(&video->vframe_info_buffer, &vframe_info,
				    sizeof(vframe_info));
		//给视频帧打上时间戳
		frame.timestamp = vframe_info.timestamp;
		//保存视频帧到队列,并通知视频发送线程工作
		output_video_data(video, &frame, vframe_info.count);		
	}

	if (++video->cur_texture == NUM_TEXTURES)
		video->cur_texture = 0;
}

函数output_video_data保存视频帧到缓存队列,并通知视频发送线程工作。这个函数也比较重要,单独拿出做说明。

static inline void output_video_data(struct obs_core_video *video,
				     struct video_data *input_frame, int count)
{
	const struct video_output_info *info;
	//定义栈变量output_frame 其实待会是要将要缓存帧的首地址复制给他内部的data
	//这块儿要好好理解,对c语言指针使用熟练比较容易理解这块的代码
	struct video_frame output_frame;
	bool locked;
	
	//获取视频输出信息
	info = video_output_get_info(video->video);
	
	//如果有可以缓存的空间,就将缓存队列中可缓存空间的地址复制给output_frame
	//output_frame就代理缓存视频的地址
	//如果没有缓存空间返回false
	locked = video_output_lock_frame(video->video, &output_frame, count,
					 input_frame->timestamp);
	if (locked) {
		//gpu_conversion在 OBSBasic::ResetVideo() 设置为true
		if (video->gpu_conversion) {
			//将图像数据从显存拷贝到内存
			set_gpu_converted_data(video, &output_frame,
					       input_frame, info);
		} else {
			copy_rgbx_frame(&output_frame, input_frame, info);
		}
		//1.更新可缓存空间-1  2.发送信号量通知视频发送线程工作
		video_output_unlock_frame(video->video);
	}
}

总结

经过上面的源码分析,能够比较清楚的理解libobs中非常重要的视频渲染线程的创建时机、工作内容,以及怎么和视频输出线程搭配工作。具体的细节还是要多多阅读源码,理解作者的设计意图。

以上都是个人工作当中对obs-studio开源项目的理解,难免有错误的地方,如果有欢迎指出。

若有帮助幸甚。

如果可以帮我点个赞那更好了,让我有动力更快的更新完obs-studio这个系列的文章。

技术参考

  1. 视频技术参考: https://ke.qq.com/course/3202131?flowToken=1040950
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值