在谈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混音,编码,音视频同步等等。一篇实在无法讲清楚。