OBS源码解读2:核心概念

2,748 阅读8分钟

本系列假设读者已经掌握以下知识:

  • C 和 C++
  • 使用 Visual Studio 开发 C/C++ 项目
  • 使用 OBS

本系列使用的 OBS 版本和开发环境:

本文将介绍 OBS 的核心概念,这些概念将贯穿整个系列的每一篇文章。

英语能力比较好的读者可以先看看官方文档:

obsproject.com/docs/backen…

下面是本人对官方文档的解读,部分翻译自文档,大部分是个人的理解。

架构

OBS 的后端(即 libobs)使用 C 语言实现,提供了最核心的功能,包括:主流程、音视频子系统、通用的插件框架。

  • core/libobs/libobs 定义了最核心的数据类型和初始化方法
  • core/libobs/media-io 关于“音频编码和输出”、“视频编码和输出”最核心的流程都在这里
  • core/libobs/graphics 关于“视频渲染”最核心的方法都在这里

OBS 的前端(即 obs)基于 Qt/C++ 实现,实现了 UI 层的逻辑,可以调用 libobs 的方法与后端交互。

  • frontend/obs/osb-app.cpp 应用程序的入口
  • frontend/obs/window-basic-main.cpp 主窗口

4大对象

源是指任何能够被合成音频/视频的元素,这些元素的来源包括但不限于:显示器捕获、游戏画面和声音捕获、麦克风声音捕获、摄像头捕获、多媒体文件(图片、音频、视频等)、文字,等等。在 OBS 中,滤镜(也叫过滤器,后面统一叫滤镜)也是一种源。源是对音频或视频来源的一种抽象。

这些源在 OBS 中可以独立存在,也可以组合在一起形成分组、场景。分组、场景也可以作为源。

源的最终归宿是合成音频流和视频流。“从源到音频流”与“从源到视频流”的过程是不一样的,这由音频与视频天然的差异决定。下面会分别介绍“从源到音频流”(即音频管道)和“从源到视频流”(即视频管道)的过程。

源的类型

源(结构体)有4种类型,定义源的时候需要指定 type 字段表示其类型。不同类型的源的成员是不一样的。

  • OBS_SOURCE_TYPE_INPUT 输入源,例如显示器捕获
  • OBS_SOURCE_TYPE_FILTER 滤镜源,例如各种效果滤镜
  • OBS_SOURCE_TYPE_TRANSITION 转场源,每一种转场特效都被定义为一种源
  • OBS_SOURCE_TYPE_SCENE 场景源,包括“场景”和“分组”

输出

输出是指能够接收当前正在渲染的音视频数据并进行处理的对象。

典型的输出对象包括串流输出(streaming)和录像输出(recording)。

输出对象可以接收原始的音视频数据,也可以接收经过编码的音视频数据。如果一个输出对象需要接收原始的音视频数据,那么它应该在生成原始视频帧的时候获取数据并处理;如果一个输出对象需要接收编码后的数据,那么它应该关联一个编码器,并在编码器编码输出视频帧的时候获取。如果你想了解其中的过程,先了解一下“编码器”、“视频管道”和“音频管道”的内容。

编码器

编码器是 OBS 对音视频编码器的封装,OBS支持x264、NVENC、Quicksync等软/硬编码器。

编码器必须接收原始的音视频数据,然后按照某一种编码格式进行编码。

编码器不是输出对象,但是编码器可以把编码后的音视频数据传给输出对象处理。

服务

服务是 OBS 对串流服务的封装,支持自定义串流服务。服务必须结合串流输出一起使用。

3大线程

OBS 后端采用多线程架构,除了主线程以外,还有3个核心线程:

  • obs_graphics_thread 视频渲染线程,后面简称“graphics 线程”
  • video_thread 视频编码和输出线程,后面简称“video 线程”
  • audio_thread 音频编码和输出线程,后面简称“audio 线程”

关于3个核心线程的工作流程,请看“视频管道概述”和“音频管道概述”

通道

音视频的渲染从通道开始。

obs_set_output_source 这个方法把源对象设置到某个输出通道

源是有层级的

“输出通道”的概念不能和“输出对象”、“音频通道”等概念混淆。“输出通道”是用来设置需要输出的源对象,“输出对象”则负责推流、录像等功能。

特别注意:音频的“通道”,和这里的“通道”不是同一个概念。音频的“通道数”是指声音通道的数目,例如“单声道”、“立体声”等,在音视频领域里面是一个通用的概念。这里的“通道”是 OBS 自创的概念。

视频管道概述

2个线程:

  • obs_graphics_thread 负责视频渲染,也就是源合成音视频、生成原始视频帧、显示到窗口(预览)等功能
  • video_thread 负责视频编码和输出

视频管道流程:

  • 首先,源对象按照输出通道的顺序被绘制(只有在输出通道内的源对象才会被绘制),当所有源对象都绘制完成,生成最终纹理
  • 最终纹理需要转成能够被输出对象处理的格式(典型的格式是YUV)
  • 转完格式后,加上时间戳,形成“一帧”,交给当前的视频处理程序
  • 视频处理程序有帧缓存(队列),刚加进来的帧被放到缓存中,等待处理
  • 当信号量发出时(由渲染线程发出),video_thread 线程就会从帧缓存中取数据,尽可能地处理这些帧。如果帧缓存队列已满,就复制最后一帧,以降低编码的复杂度。(Why?)
  • video_thread 线程继续把这些帧发送给当前处于活动状态的output对象或者编码器(前面说了,output对象可以处理原始数据或者编码后的数据,这里的帧还是原始数据)
  • 如果是发送给编码器,那么编码器对原始数据进行编码,得到编码后的数据,再发送给一个output对象。编码器和输出对象需要预先连接好,一个编码器可以连接多个输出对象
  • 如果output对象需要接收编码后的音频和编码后的视频,那么需要一个队列对音频和视频的时间进行同步
  • 如果发送原始数据给output对象,那么output对象自己处理

音频管道概述

1个线程:

  • audio_thread

假设 AUDIO_OUTPUT_FRAMES 设置为1024,那么每达到 1024 个音频样本数就要调用一次音频处理程序。如果采样率为 48kHz,即每秒采样 48k,那么每约 21 毫秒就要调用一次,下面称为 tick(1000 / 48000 * 1024)

音频管道流程:

  • 首先,一个音频源可以通过 obs_source_output_audio 这个方法输出音频数据,音频数据将被放在一个环形输入缓冲区(关于环形缓冲区的设计,在《深入音频管道设计》里面会讲)
  • 如果采样率和通道数与设置的不匹配,音频将使用 swresample 进行重新混合/重新采样
  • 附加到音频源的过滤器在音频数据插入缓冲区之前对音频数据进行处理
  • 每一次 tick,audio 线程都会获取一次音频源树的快照。对每一个音频源(叶子节点),都会从输入缓冲区中获取与当前线程时间最近的一份数据,然后设置到输出缓冲区
  • 然后,从叶子节点开始,进行音频的混合、处理,逐级向上,直到根节点,这个过程产生的数据(混合后的音频)还是存储在输出缓冲区
  • 最终混合的音频数据将被发送给output对象或者编码器处理
  • 这里简略了输出通道,实际上应该是每一个输出通道生成一份 final mix
  • 如果是发送给编码器,那么编码器对原始数据进行编码,得到编码后的数据,再发送给一个output对象。编码器和输出对象需要预先连接好,一个编码器可以连接多个输出对象
  • 如果output对象需要接收编码后的音频和编码后的视频,那么需要一个队列对音频和视频的时间进行同步 如果发送原始数据给output对象,那么output对象自己处理

插件

插件就是模块,模块就是插件。 不要太纠结这两个概念,在 OBS 里面,它们大多数情况下就是同一个东西。

OBS 的大多数功能都是通过插件来实现。插件可以注册一个新的源、输出、编码器或服务给 OBS 的前端使用,而后端不需要关心具体使用哪种类型的源、输出、编码器或服务,只要从配置里面读就可以了。

插件最终被编译成 dll 文件使用,在 OBS 初始化的时候加载并初始化,详见本系列的《启动流程》和《插件加载流程》两篇文章。

数据流转

视频数据流转如图所示:

数据流转-简单版.png

参考资料