赞
踩
libobs是整个项目的核心库,负责各种插件的加载、视频的渲染,图像的混合、视频的编码输出,音频的混音输出,其中一共创建了以下7个线程
弄清楚这些线程的创建时机,线程所负责的工作内容,以及线程之间的配合,非常有助于我们理解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
准备好一份可以断点调试的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);
接下来通过源码注释来详细说明,只保留关键代码说明,以下贴的源码删除了一些调试代码,否则看起来太罗嗦。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这个系列的文章。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。