当前位置 博文首页 > 藏经阁:obs源码分析【二】:录制功能剖析

    藏经阁:obs源码分析【二】:录制功能剖析

    作者:[db:作者] 时间:2021-08-05 18:54

    ??在谈obs录制之前,我们先来谈谈江湖上盛名已久的桌面录制绝学“ffmpeg命令行录制大法”,一行命令就能完成众多收费的录制软件才能完成的功能,既然一行代码就能搞定的功能,obs为什么要花几万行代码来完成呢?很不巧,ffmpeg命令行录制大法是有缺陷的,就像乾坤大挪移第七层是黑心老人自己幻想的境界,张无忌自然是无法练成的。ffmpeg是在linux/unix下开发的,windows系统的某些特性可能兼容不到,用ffmpeg录屏时“鼠标会闪烁”,这个bug目测没有什么解决办法,我找遍全网,都没找到解决办法,那么其它收费的录屏软件是如何解决的呢,八九不离十,绝对参考了obs的代码,这种涉及到操作系统的问题,没有参考代码或者文档,靠空想是不可能解决。
    ??在上面的前提下我们来看看obs是如何录屏,如何解决鼠标闪烁的问题。
    ??学obs, 一是看它的代码结构,二是把obs的代码抠出来运用到项目中,来看看obs的录制功能是如何实现的,先在Qt设计器找到录制的按钮。
    在这里插入图片描述
    ??查找recordButton对应的槽函数,Qt客户端项目嘛,简单,槽函数无非就那几种写法,直接来on_recordButton_clicked(), 这种是最简单的,刚好obs就是这种写法,而且主界面上其它的控件槽函数也在当前文件中。
    在这里插入图片描述
    ??那么去它的cpp源码看看,

    void OBSBasic::on_recordButton_clicked()
    {
    	if (outputHandler->RecordingActive()) {
    		bool confirm = config_get_bool(GetGlobalConfig(), "BasicWindow",
    					       "WarnBeforeStoppingRecord");
    
    		if (confirm && isVisible()) {
    			QMessageBox::StandardButton button =
    				OBSMessageBox::question(
    					this, QTStr("ConfirmStopRecord.Title"),
    					QTStr("ConfirmStopRecord.Text"),
    					QMessageBox::Yes | QMessageBox::No,
    					QMessageBox::No);
    
    			if (button == QMessageBox::No) {
    				ui->recordButton->setChecked(true);
    				return;
    			}
    		}
    		StopRecording();
    	} else {
    		if (!UIValidation::NoSourcesConfirmation(this)) {
    			ui->recordButton->setChecked(false);
    			return;
    		}
    
    		StartRecording();
    	}
    }
    

    ??在调试之前,请先在obs上添加录制窗口,不然无法录制。
    ??因为这个按钮是三态,有开启录制,暂停录制,停止录制的功能,我们最开始录制时,那么必然是走的下面这个分支StartRecording()。在看代码之前,我们先得明白桌面录制的业务需求。
    ??录制,分为画面和声音。对于画面,如果是整个桌面录制,那么就截取整个窗口的画面,如果是只录制某个窗口,那么当你在该窗口上录制叠加其它窗口时,上层窗口是不能录制的,这就涉及到了窗口截图了,就像钉钉会议的窗口分享,只分享该窗口的功能,把窗口的画面和声音进行编码,然后推流,对方就能看到了;obs的窗口截图采用的是Windows API Bitblt进行截图。声音的录制就比较复杂了,一个是电脑本身的扬声器的声音,比如你放视频,音乐,这些声音是系统扬声器发出的,那么当然也有麦克风的输入声音了,也就是说对声音采集,可能会有多路音频,那么就涉及到混音了,混音可以用ffmpeg的filter来做,当然obs也是用ffmpeg的filter来做的。谈了音视频的采集,下面来看看obs的录制代码,我们从StartRecording()开始。
    ??跟踪代码如下,录制前会进行目录路径检查,磁盘空间检查,这部分代码如果有需要,可以copy到你的项目中。
    在这里插入图片描述
    ??我们需要关注的是红线部分的代码,F12进去发现有两部分,我们在这两个可能进入的地方都打断点,
    在这里插入图片描述
    ??可以发现最终进入的是SimpleOutput::StartRecording()
    在这里插入图片描述
    ??进一步调试,发现会走到obs_output_actual_start,然后就到了录制的最后一部分代码了
    在这里插入图片描述
    ??很不幸,obs用了大量的函数指针,如果你不去把整个代码看一遍,根本不知道这里调用的是哪个函数,有了vs就很方便了,直接F11进入看看就ok
    在这里插入图片描述
    ??根据vs的提示,可以知道,我们到了插件的obs-ffmpeg模块,该函数指针其实是ffmpeg_mux_start, 截取两个比较关键的部分,采集与编码

    static bool ffmpeg_mux_start(void *data)
    {
    	struct ffmpeg_muxer *stream = data;
    	obs_data_t *settings;
    	const char *path;
    
    	//采集走起
    	if (!obs_output_can_begin_data_capture(stream->output, 0))
    		return false;
    	
    	//编码走起
    	if (!obs_output_initialize_encoders(stream->output, 0))
    		return false;
    

    ??再分析视频采集与编码之前,先得了解一下ffmpeg对音视频的封装了,因为我们是采集,是要把图片和声音数据合成视频文件mp4或者flv, 这个操作叫mux, 如果是做播放器开发,那么就是相反的操作,需要对mp4文件解封装,也叫demux, 如果在代码中看到很多mux与demux一脸懵逼,相信我这里的解释应该让你很清楚了,在写代码时,变量,函数的命名尽量让人见文知义。下面来分析一下录制的逻辑。
    ??录制是怎么做呢,视频是一张张的图片进行拼接成快速的动画,这些图片我们先要拿到,调用windows api可以拿到,当然,图片数据是rgba数据,四通道,这个很重要,经常在代码中看到32这个数字,啥意思呢,4*8知道吧,不用我多说,我们拿到rgba数据,需要转成yuv, 再用yuv编码成h264,然后封装为mp4或者flv。声音是怎么搞呢,从系统拿到声音的数据转成pcm, 再把pcm转成aac, 为什么要进行这些操作呢。音视频是有统一的标准的,国际上都是这么定义,开发者就按照这个标准来写代码,当然,ffmpeg把这些工作都做好了,程序员直接调用就好了。
    ??有了视频封装的基础,rgba–>yuv–>h264, buffer—>pcm–>acc, 最后封装为mp4,我们再来看看视频采集的代码

    obs_output_begin_data_capture(stream->output, 0);
    

    ??由于obs多线程执行,到这里,其实仍然无法找到视频采集,音频录制的入口,到底是为什么呢,那当然是多线程啦,由于我之前调试过代码,知道录制功能的大概位置,先给出obs的窗口录制的代码位置,obs录制时的窗口截图主要有3种方法,当我们进行窗口录制时,会调用windows api BitBlt进行截图,该方式属于gdi模式,代码如下:

    void dc_capture_capture(struct dc_capture *capture, HWND window)
    {
    	HDC hdc_target;
    	HDC hdc;
    
    	if (capture->capture_cursor) {
    		memset(&capture->ci, 0, sizeof(CURSORINFO));
    		capture->ci.cbSize = sizeof(CURSORINFO);
    		capture->cursor_captured = GetCursorInfo(&capture->ci);
    	}
    
    	hdc = dc_capture_get_dc(capture);
    	if (!hdc) {
    		blog(LOG_WARNING, "[capture_screen] Failed to get "
    				  "texture DC");
    		return;
    	}
    
    	hdc_target = GetDC(window);
    
    	//进行窗口截屏
    	BitBlt(hdc, 0, 0, capture->width, capture->height, hdc_target,
    	       capture->x, capture->y, SRCCOPY);
    
    	ReleaseDC(NULL, hdc_target);
    
    	//重绘鼠标
    	if (capture->cursor_captured && !capture->cursor_hidden)
    		draw_cursor(capture, hdc, window);
    
    	dc_capture_release_dc(capture);
    
    	capture->texture_written = true;
    }
    

    ??BitBlt拿到图片后,需要把图拷贝到ffmpeg的编码模块进行h264编码。
    ??以上代码属于windows-capture模块,obs采用的是微内核-插件模式,很多功能都用dll实现,主程序进行load.
    ??这部分代码里面有个非常关键的点 “鼠标重绘”,为什么要做这个操作呢,如果你用过ffmpeg录屏你就会发现,录制时鼠标一只在闪,这是为什么呢,具体得去看windows的文档了,obs把鼠标重绘了,解决了鼠标闪烁的bug。凡是用gdi截图录制的都会有鼠标闪烁的bug, 用dx的话就没有,但是dx不能截取窗口进行录制,相对于dx,BitBlt的效果是不怎么好的,但是BitBlt可以根据窗口句柄截图录制,这一点很方便。
    ??dc_capture_capture这部分代码是怎么执行的呢,它是在video线程里面跑起来的,那么就得需要找找线程是在哪里创建的了,一个庞大的项目,经过了十几年的迭代,想搞清楚可不是那么容易的,下一篇继续揭秘。
    ??本篇就先说这么多,后续博客继续更新。
    ??因为录制涉及到的东西比较多,视频采集编码、音频采集编码、系统设备获取,窗口画面截图等等,ffmpeg混音,编码,音视频同步等等。一篇实在无法讲清楚。

    ??阅读obs源码分析全部文章,请点击【obs源码分析专栏】

    cs