最近一段时间再看OBS的几个重要的线程,通过不断的学习和结合一些大佬的博客终于相对搞明白了这几个线程,分别是采集渲染线程和编码线程,分析如下:
struct obs_core_video几个关键变量的注释:
// 主画布 渲染当前场景下sources的画布
// 渲染主窗口的窗口时 直接把这个纹理贴到display中
gs_texture_t *render_texture;
// output画布 如果直播录像时的宽高(output宽高)和主画布宽高不一样,
// 需要在该画布上对render_texture做缩放
gs_texture_t *output_texture;
// 做GPU转换时 用来保存各个分量的画布 不可读
// 做转换的纹理 就是render_texture或output_texture
gs_texture_t *convert_textures[NUM_CHANNELS];
// 保存供output使用的的YUV分量数组 可读
// 这里的数据 经过map和read后 会保存在video_output::cache 等待video_thread读取
gs_stagesurf_t *copy_surfaces[NUM_TEXTURES][NUM_CHANNELS];
obs_graphics_thread中主要调三个函数:
while(1)
{
profile_start(tick_sources_name);
last_time = tick_sources(obs->video.video_time, last_time);
profile_end(tick_sources_name);
profile_start(output_frame_name);
output_frame(raw_active, gpu_active);
profile_end(output_frame_name);
profile_start(render_displays_name);
render_displays();
profile_end(render_displays_name);
Sleep(interval);
}
tick_sources()中遍历了所有source,对每个source(不止是当前场景的source)调用obs_source_video_tick:
pthread_mutex_lock(&data->sources_mutex);
source = data->first_source;
while (source)
{
struct obs_source *cur_source = obs_source_get_ref(source);
source = (struct obs_source *)source->context.next;
if (cur_source)
{
obs_source_video_tick(cur_source, seconds);
obs_source_release(cur_source);
}
}
pthread_mutex_unlock(&data->sources_mutex);
obs_source_video_tick中调用相关的XXX_tick函数,并且检查了UI存储的show_refs和activate_refs,如果存储的状态和source当前实际状态不一致,更新之(UI修改active状态后只更新了临时值,在videoRenderThread中才实际更新source):
scene_video_tick,obs_transition_tick,async_tick,source_info.video_tick
output_frame()中,做了如下事:
static inline void output_frame(bool raw_active, const bool gpu_active)
{
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); // 上次渲染的纹理索引
bool frame_ready = 0;
struct video_data frame;
memset(&frame, 0, sizeof(struct video_data));
profile_start(output_frame_gs_context_name);
gs_enter_context(video->graphics);
{
profile_start(output_frame_render_video_name);
GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_RENDER_VIDEO, output_frame_render_video_name);
render_video(video, raw_active, gpu_active, cur_texture); // 渲染所有source 保存画布数据 更新下次渲染纹理的索引
GS_DEBUG_MARKER_END();
profile_end(output_frame_render_video_name);
if (raw_active) // 当有推流或录像的时候 该值是true
{
profile_start(output_frame_download_frame_name);
// 填充frame::data, frame::linesize
frame_ready = download_frame(video, prev_texture, &frame);
profile_end(output_frame_download_frame_name);
}
profile_start(output_frame_gs_flush_name);
gs_flush(); // ID3D11DeviceContext::Flush()
profile_end(output_frame_gs_flush_name);
}
gs_leave_context();
profile_end(output_frame_gs_context_name);
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;
profile_start(output_frame_output_video_data_name);
// send video texture,触发信号量 通知video_thread线程可以取数据了
output_video_data(video, &frame, vframe_info.count);
profile_end(output_frame_output_video_data_name);
}
++video->cur_texture;
if (video->cur_texture == NUM_TEXTURES)
video->cur_texture = 0;
}
static inline void render_video(struct obs_core_video *video, bool raw_active,
const bool gpu_active, int cur_texture)
{
gs_begin_scene();
gs_enable_depth_test(false);
gs_set_cull_mode(GS_NEITHER);
render_main_texture(video); // 渲染所有source到画布上
if (raw_active || gpu_active) {
// 将画布上的数据拷贝到纹理中, 此处格式是GS_RGBA
// 画布对象是struct obs_core_video::render_texture
// 如果画布宽高和output宽高一致,则返回的是obs_core_video::render_texture,
// 否则在obs_core_video::output_texture对render_texture做缩放后 返回output_texture
gs_texture_t *texture = render_output_texture(video);
#ifdef _WIN32
if (gpu_active)
gs_flush();
#endif
if (video->gpu_conversion)// GPU格式转换
render_convert_texture(video, texture);
#ifdef _WIN32
if (gpu_active) {
gs_flush();
output_gpu_encoders(video, raw_active);
}
#endif
// 如果有录像或直播 将GPU转换后的纹理保存下来
// GPU转换后的纹理 存储在struct obs_core_video::convert_textures
// 数据保存在obs_core_video::copy_surfaces
if (raw_active)
stage_output_texture(video, cur_texture);
}
gs_set_render_target(NULL, NULL);
gs_enable_blending(true);
gs_end_scene();
}
其中render_main_texture会调用obs_view_render,渲染所有channel:
#define MAX_CHANNELS 64
void obs_view_render(obs_view_t *view)
{
if (!view)
return;
pthread_mutex_lock(&view->channels_mutex);
for (size_t i = 0; i < MAX_CHANNELS; i++)
{
struct obs_source *source = view->channels[i];
if (source)
{
if (source->removed)
{
obs_source_release(source);
view->channels[i] = NULL;
}
else
{
// 默认情况下 每次循环此处逻辑会进入三次
// 一次是当前应用的transition source
// 一次是OBS自动添加的was output audio (desktop audio)
// 一次是OBS自动添加的was input audio(麦克风)
// 但是后两个audio的channel 在调用obs_source_video_render时什么都不会做 直接return
// 而transition source则会触发渲染当前scene 或做两个scene的转场
obs_source_video_render(source);
}
}
}
pthread_mutex_unlock(&view->channels_mutex);
}
遍历和渲染scene的每个source的函数是scene_video_render,代码如下
item = scene->first_item;
while (item)
{
if (item->user_visible)
render_item(item); // 会调到video_render
item = item->next;
}
static inline void render_item(struct obs_scene_item *item) 中设置每个source对应的矩阵matrix:
gs_matrix_push(); // 保存之前的矩阵
gs_matrix_mul(&item->draw_transform); // 设置sourceitem的矩阵
if (item->item_render)
{
render_item_texture(item);
}
else
{
obs_source_video_render(item->source);
}
gs_matrix_pop(); // 恢复之前的矩阵
draw_transform是每个source的矩阵,每当其位置缩放旋转发生变化,都会调用函数更新矩阵:
static void update_item_transform(struct obs_scene_item *item, bool update_tex)
当UI上修改了缩放位置时 会直接调用这个函数
render_displays()中,遍历了所有obs_display,并对每个display调用其注册的所有回调函数
void render_display(struct obs_display *display),该函数中会调用display所有的draw_callbacks。
在draw_callbacks中会完成纹理的矩阵设置和渲染,典型的draw_callbacks如下:
OBSBasicProperties::DrawPreview (属性窗口)
OBSBasic::RenderProgram(studio mode下渲染右侧的已应用的scene)
OBSBasic::RenderMain (主窗口,如果是studio mode则是渲染左侧的编辑scene)关键函数如下:
// 函数中又调用了obs_render_main_texture_internal
// 之前已经把当前scene所有source画到obs_core_video::render_texture了
// 直接将render_texture画到main窗口即可
obs_render_main_texture_src_color_only();
===============================================
if (window->IsPreviewProgramMode()) // studio mode
{
window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height));
OBSScene scene = window->GetCurrentScene();
obs_source_t *source = obs_scene_get_source(scene);
if (source)
{
// 渲染编辑中的临时场景
// 主画布的数据是渲染的已应用的scene的画面
// 这个scene source是编辑中的临时场景 所以需要重新渲染其中的source
obs_source_video_render(source);
}
}
else
{
// 在主屏幕画当前scene的所有source : 直接将主画布的纹理贴到display即可
obs_render_main_texture_src_color_only();
}
// 画main中选中source的边框和鼠标悬停source的边框
window->ui->preview->DrawSceneEditing(); ```
UI调用obs_display_create创建了display后,会调用obs_display_add_draw_callback注册渲染回调函数,libobs的videoRenderThread调用函数指针,就会到UI的回调,并由UI完成窗口渲染
=====================video_thread====================
该线程即使不直播不录像也会创建 只是一直阻塞住 等待信号量 直到output_video_data触发事件
每次触发后,会循环读完所有数据(video_output_cur_frame) 然后阻塞等待下一个信号触发,每处理一个frame 会增加total_frames。此处拿到的frame 已经是output设置的宽高和format
video_output_cur_frame会通过回调把数据发出去:
```cpp
pthread_mutex_lock(&video->input_mutex);
for (size_t i = 0; i < video->inputs.num; i++)
{
struct video_input *input = video->inputs.array + i;
struct video_data frame = frame_info->frame;
if (scale_video_output(input, &frame))
input->callback(input->param, &frame);
}
pthread_mutex_unlock(&video->input_mutex);
录像和直播,回调函数都是:static void receive_video(void *param, struct video_data *frame)
该函数中 会调用do_encode对video编码(音频也是调用该函数进行编码)如果编码获取到packet 会发送到interleave_packets av交错排序保证时间戳单调性:
```cpp
bool do_encode(struct obs_encoder *encoder, struct encoder_frame *frame)
{
profile_start(do_encode_name);
if (!encoder->profile_encoder_encode_name)
encoder->profile_encoder_encode_name =
profile_store_name(obs_get_profiler_name_store(),
"encode(%s)", encoder->context.name);
struct encoder_packet pkt = {0};
bool received = false;
bool success;
pkt.timebase_num = encoder->timebase_num;
pkt.timebase_den = encoder->timebase_den;
pkt.encoder = encoder;
profile_start(encoder->profile_encoder_encode_name);
success = encoder->info.encode(encoder->context.data, frame, &pkt,&received);
profile_end(encoder->profile_encoder_encode_name);
send_off_encoder_packet(encoder, success, received, &pkt); // interleave_packets
profile_end(do_encode_name);
return success;
}