文章

探索 OBS 开发(1):libobs 框架

系统介绍 OBS 开发相关的基础技术。

探索 OBS 开发(1):libobs 框架

想要学习和提升音视频技术的朋友,快来加入我们的【音视频技术社群】,加入后你就能:

  • 1)下载 30+ 个开箱即用的「音视频及渲染 Demo 源代码」
  • 2)下载包含 500+ 知识条目的完整版「音视频知识图谱」
  • 3)下载包含 200+ 题目的完整版「音视频面试题集锦」
  • 4)技术和职业发展咨询 100% 得到回答
  • 5)获得简历优化建议和大厂内推

现在加入,送你一张 20 元优惠券:点击领取优惠券

知识星球新人优惠券 微信扫码也可领取优惠券


OBS Studio 的后端由 libobs 库提供支持。libobs 提供了主处理流程、音视频子系统以及所有插件的通用框架。

Libobs 插件对象

Libobs 采用了模块化设计,添加模块可以增加自定义功能。以下是四种可以制作插件的 libobs 对象:

  • 源(Sources):用于在流媒体中渲染视频和/或音频。例如捕获屏幕/游戏/音频、播放视频、显示图像或播放音频等。源还可以用于实现音频和视频滤镜。

  • 输出(Outputs):允许输出当前渲染的音视频。流媒体和录制是常见的输出类型,但并非唯一的输出类型。输出可以接收原始数据或编码后的数据。

  • 编码器(Encoders):是 OBS 特定的音视频编码器实现,用于使用编码器的输出。x264、NVENC、Quicksync 是编码器实现的示例。

  • 服务(Services):是自定义的流媒体服务实现,用于与流媒体服务相关的输出。例如,可以为 Twitch 和 YouTube 提供自定义实现,以便登录并使用它们的 API 进行诸如获取 RTMP 服务器或控制频道等操作。

(作者注:截至本文撰写时,服务 API 仍不完整)

Libobs 线程

Libobs 初始化时会启动三个主要线程:

  • obs_graphics_thread 函数,专门用于 libobs/obs-video.c 中的渲染。

  • video_thread 函数,专门用于 libobs/media-io/video-io.c 中的视频编码/输出。

  • audio_thread 函数,用于所有音频处理/编码/输出,位于 libobs/media-io/audio-io.c 中。

(作者注:obs_graphics_thread 最初名为 obs_video_thread;截至本文撰写时已更名,以避免与 video_thread 混淆)

输出通道

渲染视频或音频从输出通道开始。你可以通过 obs_set_output_source() 函数将源分配给输出通道。参数 _channel 可以是 0..(MAX_CHANNELS-1) 之间的任意数字。你可能最初会认为这是如何同时显示多个源的方法;然而,源是具有层次结构的。像场景或转场这样的源可以有多个子源,而这些子源又可以有子源,依此类推(有关详细信息,请参阅显示源)。通常,你会使用场景以组的形式绘制多个源,并为每个源设置特定的变换,因为场景只是另一种类型的源。“通道”设计允许高度复杂的视频展示设置。OBS Studio 前端尚未充分利用此后端设计进行渲染,目前仅使用一个输出通道一次渲染一个场景。然而,它确实会使用额外的通道来处理诸如在音频设置中设置的全局音频源等事务。

(作者注:“输出通道” 不要与输出对象或音频通道混淆。输出通道用于设置你想要输出的源,而输出对象用于实际的流媒体/录制等)

视频处理流程概述

视频图形处理流程由两个线程运行:一个专门用于渲染预览显示以及最终混合的图形线程(libobs/obs-video.c 中的 obs_graphics_thread 函数),以及一个专门用于视频编码/输出的线程(libobs/media-io/video-io.c 中的 video_thread 函数)。

分配给输出通道的源将从通道 0..(MAX_CHANNELS-1) 绘制。它们被绘制到最终纹理上,该纹理将用于输出 [1]。一旦所有源都被绘制完成,最终纹理将被转换为 libobs 设置的格式(通常是 YUV 格式)。在转换为后端视频格式后,它将与时间戳一起发送到当前视频处理器 obs_core_video::video

然后,它将该原始帧放入视频输出处理器中的一个队列,队列大小为 MAX_CACHE_SIZE。发布一个信号量后,视频-io 线程会在其能力范围内处理帧。如果视频帧队列已满,它将复制队列中的最后一帧,以尝试降低视频编码的复杂性(从而降低 CPU 使用率)[2]。这就是为什么当编码器无法跟上时你可能会看到帧跳过的原因。帧被发送到任何当前活动的原始输出或视频编码器 [3]。

如果它被发送到视频编码器对象(libobs/obs-encoder.c),它将对帧进行编码,并将编码后的数据包发送到该编码器连接的输出(可以是多个)。如果输出同时接受编码后的视频和音频,它会将数据包放入交错队列中,以确保按照单调时间戳顺序发送编码后的数据包 [4]。

编码后的数据包或原始帧随后被发送到输出。

音频处理流程概述

音频处理流程由音频处理器中的专用音频线程运行(libobs/media-io/audio-io.c 中的 audio_thread 函数);假设 AUDIO_OUTPUT_FRAMES 设置为 1024,则音频线程每 1024 个音频采样(在 48kHz 下约为 21 毫秒间隔)“滴答”一次,并调用 libobs/obs-audio.c 中的 audio_callback 函数,大部分音频处理在此完成。

带有音频的源将通过 obs_source_output_audio 函数输出其音频,这些音频数据将被追加或插入到循环缓冲区 obs_source::audio_input_buf 中。如果采样率或通道数与后端设置不匹配,音频将通过 swresample 自动进行混音/重采样 [5]。在插入之前,音频数据还会经过附加到源的任何音频滤镜的处理 [6]。

每个音频滴答周期,音频线程会拍摄音频源树的参考快照(存储所有输出/处理音频的源的引用)[7]。在每个音频叶节点(音频源)上,它会获取存储在循环缓冲区 obs_source::audio_input_buf 中的与当前音频线程时间戳最接近的音频,并将其放入 obs_source::audio_output_buf 中。

然后,存储在叶子节点 obs_source::audio_output_buf 中的音频样本会被发送到源树快照中的父节点进行混音或处理 [8]。像场景或转场这样具有多个子节点的源将通过 obs_source_info::audio_render 回调自行混音/处理其子节点的音频。这允许例如在两个源之间转换时,转场可以淡入一个源的音频并淡入新源的音频。然后,混音或处理后的音频数据同样被存储在该节点的 obs_source::audio_output_buf 中,并重复此过程,直到音频到达树的根节点。

最后,当音频到达快照树的底部时,每个输出通道中所有源的音频将被混合在一起,形成最终混合音频 [9]。该最终混合音频随后被发送到任何当前活动的原始输出或音频编码器 [10]。

如果它被发送到音频编码器对象(libobs/obs-encoder.c),它将对音频数据进行编码,并将编码后的数据包发送到该编码器连接的输出(可以是多个)。如果输出同时接受编码后的视频和音频,它会将数据包放入交错队列中,以确保按照单调时间戳顺序发送编码后的数据包 [4]。

编码后的数据包或原始音频数据随后被发送到输出。


本文转自微信公众号 关键帧Keyframe,推荐您关注来获取音视频、AI 领域的最新技术和产品信息

微信公众号 微信扫码关注我们

本文由作者按照 CC BY-NC-ND 4.0 进行授权