文章目录
前言
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);
视频渲染线程的工作内容
主要有以下三点
- 根据设置的视频输出帧率,每间隔固定时间处理所有源的输入,并融合成一张图像缓存起来
- 如果开启推流和录像,则通过信号通知视频输出线程编码输出视频帧
- 渲染视频到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这个系列的文章。