在obs中,录制时,提供了两种种窗口截屏方法,
enum window_capture_method {
METHOD_AUTO,
METHOD_BITBLT,
METHOD_WGC,
};
但是在窗口像素抓取时默认用的BitBlt,下面的代码选择抓屏的方法:
static enum window_capture_method
choose_method(enum window_capture_method method, bool wgc_supported,
const char *current_class)
{
if (!wgc_supported)
return METHOD_BITBLT;
if (method != METHOD_AUTO)
return method;
if (!current_class)
return METHOD_BITBLT;
const char **class = wgc_partial_match_classes;
while (*class) {
if (astrstri(current_class, *class) != NULL) {
return METHOD_WGC;
}
class ++;
}
class = wgc_whole_match_classes;
while (*class) {
if (astrcmpi(current_class, *class) == 0) {
return METHOD_WGC;
}
class ++;
}
return METHOD_BITBLT;
}
真正调用BitBlt方法是在wc_tick函数中,
static void wc_tick(void *data, float seconds)
{
....
dc_capture_capture(&wc->capture, wc->window);
....
}
dc_capture_capture根据窗口句柄wc->window调用BitBit进行抓屏,这个函数在初始化时会赋值给结构体obs_source_info,如下:
struct obs_source_info window_capture_info = {
.id = "window_capture",
.type = OBS_SOURCE_TYPE_INPUT,
.output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_CUSTOM_DRAW |
OBS_SOURCE_SRGB,
.get_name = wc_getname,
.create = wc_create,
.destroy = wc_destroy,
.update = wc_update,
.video_render = wc_render,
.hide = wc_hide,
//抓屏方法赋值
.video_tick = wc_tick,
.get_width = wc_width,
.get_height = wc_height,
.get_defaults = wc_defaults,
.get_properties = wc_properties,
.icon_type = OBS_ICON_TYPE_WINDOW_CAPTURE,
};
该结构体在obs初始化时加载
bool obs_module_load(void)
{
。。。
//注册窗口采集的信息
obs_register_source(&window_capture_info);
。。。
return true;
}
window_capture_info不是在当前文件定义的,所以用extern声明
当前的obs_module_load是在插件dll中,需要在libobs中导入,方法如下:
static int load_module_exports(struct obs_module *mod, const char *path)
{
mod->load = os_dlsym(mod->module, "obs_module_load");
if (!mod->load)
return req_func_not_found("obs_module_load", path);
。。。
。。。
}
os_dlsym其实就是调用win32 API GetProcAddress根据dll 方法名获取函数地址,做windows C++开发的应该很熟悉GetProcAddress,os_dlsym代码如下:
void *os_dlsym(void *module, const char *func)
{
void *handle;
handle = (void *)GetProcAddress(module, func);
return handle;
}
有了GetProcAddress必然得有LoadLibraryW,在os_dlsym的上方其实os_dlopen就是根据路径加载dll. 就是逐一加载下面这个路径的dll.
经过跟踪代码,可以发现是在Qt界面初始化时加载所有的dll, 先是OBSBasic类的OBSInit方法,代码如下:
void OBSBasic::OBSInit()
{
ProfileScope("OBSBasic::OBSInit");
AddExtraModulePaths();
blog(LOG_INFO, "---------------------------------");
obs_load_all_modules(); //加载所有dll
blog(LOG_INFO, "---------------------------------");
obs_log_loaded_modules();
blog(LOG_INFO, "---------------------------------");
obs_post_load_modules();
}
然后是obs_load_all_modules()调用load_all_callback , 代码如下:
void obs_load_all_modules(void)
{
profile_start(obs_load_all_modules_name);
//传入load_all_callback,加载dll
obs_find_modules(load_all_callback, NULL);
#ifdef _WIN32
profile_start(reset_win32_symbol_paths_name);
reset_win32_symbol_paths();
profile_end(reset_win32_symbol_paths_name);
#endif
profile_end(obs_load_all_modules_name);
}
再就是obs_open_module,调用os_dlopen
int obs_open_module(obs_module_t **module, const char *path,
const char *data_path)
{
。。。
mod.module = os_dlopen(path);
。。。
}
至此dll的加载流程就清晰了,但是上图中其实有很多dll, 那得调用多次LoadLibrary,从一个dll的加载,然后alt 7查看堆栈,可以知道是在哪里调用的多次:
obs_find_moudules里面执行for循环调用find_modules_in_path
void obs_find_modules(obs_find_module_callback_t callback, void *param)
{
if (!obs)
return;
for (size_t i = 0; i < obs->module_paths.num; i++) {
struct obs_module_path *omp = obs->module_paths.array + i;
find_modules_in_path(omp, callback, param);
}
}
find_modules_in_path的代码
static void find_modules_in_path(struct obs_module_path *omp,
obs_find_module_callback_t callback,
void *param)
{
。。。
if (os_glob(search_path.array, 0, &gi) == 0) {
for (size_t i = 0; i < gi->gl_pathc; i++) {
if (search_directories == gi->gl_pathv[i].directory)
process_found_module(omp, gi->gl_pathv[i].path,
search_directories,
callback, param);
}
os_globfree(gi);
}
。。。
}
一共是21个dll.
process_found_module根据dll的名字,调用callback load dll.至此所有的dll加载就弄清楚了。窗口录制win-capture.dll以及其它dll就是在obs界面初始化全部调用进来的,在dll中load dll, dll有点多,挺绕的。
上面这么多题外话,搞清楚了窗口采集dll的加载过程,下面来说说窗口截图后是如何编码的?
要解决这个问题,必须从问题的源头来解决,obs采用的x264编码,那么就得从x264开始,包括ffmpeg也是集成了x264, 如果不用h264,那是不可能的,你自己写个编码标准,世界音视频协会不认,其他播放器打不开,不管怎样,最终还是要转为x264, 265等标准,视频帧的编码最终调用的是x264_encoder_encode,也就是在github上需要下载的那个依赖。
代码如下:
/* x264_encoder_encode:
* encode one picture.
* *pi_nal is the number of NAL units outputted in pp_nal.
* returns the number of bytes in the returned NALs.
* returns negative on error and zero if no NAL units returned.
* the payloads of all output NALs are guaranteed to be sequential in memory. */
X264_API int x264_encoder_encode( x264_t *, x264_nal_t **pp_nal, int *pi_nal, x264_picture_t *pic_in, x264_picture_t *pic_out );
关于x264的源码解析,雷神已经讲的很清楚了,可以去认真看看,底层开发真的是太无聊了,不像前端,后端,直接调接口,就可以很快做出各种应用,但是底层开发不行,你得了解各种深层次的原理机制,才能做出一点点事情,不过现在音视频也被很多大厂承包了,比如声网Agora, 靠提供音视频服务,弯道超车,已经上市了;还有其它厂,比如腾讯,阿里,网易也有自己的音视频服务,小公司要做音视频,不是不可以,得提供足够的资源,不然是不可能的。
经过调试,其实也可以调试出来,当你在界面是点击【开始录制】按钮后就会触发线程相关的代码,比较多,我暂时没有仔细看。然后线程就会启动
status = sp->exitStatus = (*start) (arg);
然后通过 video_thread调用video_output_cur_frame
static void *video_thread(void *param)
{
。。。
while (!video->stop && !video_output_cur_frame(video)) {
os_atomic_inc_long(&video->total_frames);
}
。。。
}
video_output_cur_framed调用 receive_video
static inline bool video_output_cur_frame(struct video_output *video)
{
。。。
input->callback(input->param, &frame);
。。。
触发callback,实际是调用receive_video,receive_video调用do_encode
static void receive_video(void *param, struct video_data *frame)
{
。。。
if (do_encode(encoder, &enc_frame))
encoder->cur_pts += encoder->timebase_num;
。。。
do_encode再进行回调的调用
bool do_encode(struct obs_encoder *encoder, struct encoder_frame *frame)
{
。。。
success = encoder->info.encode(encoder->context.data, frame, &pkt,
&received);
。。。
}
info的encode就是调用x264的x264_encoder_encode.
编码的实现大概流程就是这样,其中涉及到线程操作的部分没有仔细看,代码太多了,obs也迭代了十多年,搞清楚整个流程得花不少时间。