第 2 课:视频的本质就是连续的图片
# 第 2 课:视频的本质就是连续的图片
本课目标:把视频最底层的四个概念 —— 像素、分辨率、帧、帧率 —— 用大白话讲透, 再回到
libobs/media-io/和libobs/obs.h,看 OBS 是怎么用结构体把「一帧画面」精确描述出来的。 这是整条视频链路的第一块砖。
前两课我们学会了把 OBS 跑起来、在源码里找路。从这一课开始进入正题。先说一个听起来反直觉、却能解放思想的事实:
视频里其实没有「视频」这种东西,只有一堆图片。 所谓「视频」,不过是一叠静止图片,按时间顺序快速翻看而已。
顺着这句话,我们把它拆成四个概念。前三个回答「一张图片是什么」,最后一个回答「怎么让图片动起来」。
# 2.1 像素:图像的「原子」
📍我们在哪:流水线的第一站是「采集」,可要看懂采集出来的画面,得先认识它的最小零件 —— 像素。
拿出任意一张照片,在屏幕上不停放大、放大、再放大,最终你会看到画面变成一个个方形的小色块:
![]()
这些方块,就是像素(pixel,源自 picture element「图像元素」)。它是图像里最小的、不可再分的单位 —— 一个像素就是一个带着某种颜色的点。
把无数像素排成一张网格,就是一张图片。上图那个爱心,其实就是一张 11 × 10 的网格,总共 110 个像素,有的染成红色、有的留白。任何图片,本质都是这样一张「色点网格」。
至于「一个像素的颜色,在电脑里到底用几个字节、怎么存」—— 这是个大话题,我们专门留到第 3 课(RGB / YUV)。本课你只需记住:像素 = 一个带颜色的点,图像 = 像素网格。
# 2.2 分辨率:这张网格有多大
📍承上启下:像素是一个点,那把点排成网格、这张网格有多大,就是分辨率 —— 顺手也埋下「越清晰越占空间」的伏笔。
既然图片是像素网格,那「网格有多少行多少列」就决定了它的精细程度。这个「宽 × 高」,就是分辨率(resolution),单位是像素。
你一定见过这些数字:

| 俗称 | 分辨率(宽 × 高) | 像素总数 |
|---|---|---|
| 720p | 1280 × 720 | ≈ 92 万 |
| 1080p(全高清) | 1920 × 1080 | ≈ 207 万 |
| 4K(2160p) | 3840 × 2160 | ≈ 830 万 |
几个要点:
- 名字里的「p」是 progressive(逐行扫描)的意思,表示每一帧都是一整张完整画面。(与之相对的是「i」= interlaced 隔行扫描,把奇偶行拆开交替显示,是老电视时代的技术;OBS 里有一整套「去隔行 / deinterlace」处理就是为它准备的,我们后面会遇到。)
- 像素越多 → 画面越清晰,但数据量也越大。 注意上表:4K 的宽和高都是 1080p 的 2 倍,所以像素总数是它的 2 × 2 = 4 倍。分辨率每「升一级」,数据往往是成倍地涨。
这个「越清晰越大」的矛盾,先记在心里 —— 它是后面「为什么要编码」的一半理由。
# 2.3 帧与帧率:让图片动起来
📍承上启下:前面都在说「一张图片」;这一节让图片动起来 —— 一张叫帧,每秒放多少张叫帧率,视频就是这么来的。
到这里我们都还在说「一张图片」。视频怎么来的?把许多张图片按顺序快速播放。 其中每一张静止画面,就叫一帧(frame)。

就像小时候在书角画小人、快速翻页就「动」起来一样。你的眼睛有个特性叫视觉暂留(persistence of vision):一个画面消失后,视网膜还会短暂保留它的影像。于是当画面切换得足够快,大脑就把一连串静止图片脑补成了连续的运动。
那「足够快」是多快?这就引出帧率(frame rate),即每秒播放多少帧,单位是 fps(frames per second):

- 24 fps —— 电影的经典帧率,够骗过眼睛,还带点「电影感」;
- 30 fps —— 早年视频、直播的常见值;
- 60 fps —— 游戏直播的主流,运动画面明显更顺滑。
帧率和「每帧间隔」是倒数关系:60 fps 意味着每 1/60 秒(≈ 16.7 毫秒)就要换一帧。同样一秒钟,帧率越高,塞进去的帧越多,画面越丝滑 —— 但要处理和传输的数据也成倍增加。又一次,「更好」和「更大」绑在了一起。
🗒️ 术语速记:像素=画面最小的一个色点;分辨率=一帧有多少像素(如 1920×1080);帧=某一瞬间的一整幅画面;帧率(fps)=每秒多少帧。
# 2.4 一个小谜题:为什么会有 29.97 fps?
📍接上一节:刚说完帧率,这一节抓一个反常的帧率 29.97 fps 当谜题,顺带看 OBS 为什么把帧率存成分数而不是小数。
如果你翻过视频软件的设置,可能见过 29.97 或 59.94 这种别扭的帧率,而不是整齐的 30、60。这不是 bug,而是一段历史遗留:
当年美国的彩色电视(NTSC)要兼容已有的黑白电视信号,工程师不得不把帧率从 30 略微下调到 30 ÷ 1.001 ≈ 29.97,好给彩色信息腾出位置又不干扰伴音。这个 1.001 的尾巴就这么一直留到了今天。
问题来了:29.97 是个除不尽的小数(30000 ÷ 1001),用浮点数存会有精度误差,日积月累音画就会对不齐。OBS 怎么办?它干脆不存小数,而是把帧率存成一个分数 —— 分子和分母分开存。 看源码:
// libobs/media-io/frame-rate.h:7
struct media_frames_per_second {
uint32_t numerator; // 分子,如 30000
uint32_t denominator; // 分母,如 1001
};
// 真正的 fps 值 = 分子 ÷ 分母
static inline double media_frames_per_second_to_fps(struct media_frames_per_second fps)
{
return (double)fps.numerator / fps.denominator;
}
2
3
4
5
6
7
8
9
10
11
于是 29.97 在 OBS 里被精确地表示成 numerator = 30000, denominator = 1001,59.94 则是 60000 / 1001,毫无精度损失。一个小小的结构体,藏着一段电视史 —— 这正是读真实工程源码的乐趣。
# 2.5 源码印证:OBS 眼里的「一帧」和「一条视频流」
📍承上启下:概念讲够了,回源码兑现 —— 看 OBS 用哪两个结构体,把本课的像素、分辨率、帧率精确地装进代码。
现在把本课四个概念,和 OBS 的真实数据结构对上。
# 一条视频流的「说明书」:video_output_info
OBS 用 struct video_output_info 来描述「一条视频流长什么样」。注意看,本课的概念几乎全在里面:
// libobs/media-io/video-io.h:127
struct video_output_info {
const char *name;
enum video_format format; // 像素格式:颜色怎么存(第 3 课)
uint32_t fps_num; // 帧率的分子 ┐ 帧率
uint32_t fps_den; // 帧率的分母 ┘ (本课 2.3 / 2.4)
uint32_t width; // 宽 ┐ 分辨率
uint32_t height; // 高 ┘ (本课 2.2)
size_t cache_size;
...
};
2
3
4
5
6
7
8
9
10
11
12
分辨率(width/height)、帧率(fps_num/fps_den)、像素格式(format),三个概念在这一个结构体里齐活了。以后你在 OBS 设置里调的「输出分辨率」「FPS」,最终就是填进这几个字段。
# 具体某一帧:obs_source_frame
而当一个「源」(摄像头、屏幕…)真正产出某一张具体画面时,它交给 OBS 的是 struct obs_source_frame。本课的每个概念,都是它的一个字段:

// libobs/obs.h:283
struct obs_source_frame {
uint8_t *data[MAX_AV_PLANES]; // 像素数据本身(一大块字节)
uint32_t linesize[MAX_AV_PLANES]; // 每一行占多少字节
uint32_t width; // 宽 ┐ 分辨率
uint32_t height; // 高 ┘
uint64_t timestamp; // 时间戳:这帧该在何时显示
enum video_format format; // 像素格式(第 3 课)
...
};
2
3
4
5
6
7
8
9
10
data/linesize—— 像素数据本身:所有像素的颜色字节,一行一行排好;linesize记录「一行有多少字节」,方便逐行读取。width/height—— 分辨率,本课主角。timestamp—— 时间戳,这一帧应该在什么时刻显示。它和帧率直接相关,更是模块 4「音画同步」的关键 —— 声音帧和画面帧都靠各自的timestamp来对齐。format—— 像素格式,下一课的主题。
顺带看一眼「一帧到底占多少内存」是怎么算出来的。
libobs/media-io/video-frame.h:32有这么个函数:void video_frame_init(struct video_frame *frame, enum video_format format, uint32_t width, uint32_t height);1
2
3它按 格式 × 宽 × 高 算出需要多少字节,然后一次性分配好内存。换句话说:一帧的大小 = 分辨率 × 每像素字节数。 记住这个公式,下一节我们就用它。
# 2.6 把第 0 课的账,精确地再算一遍
📍收束:带着新学的概念,把第 0 课那笔「裸流有多大」的账再算一遍 —— 这回每个数字你都说得清来历,也为「编码」攒足了动机。
第 0 课我们粗略算过「1080p60 的裸流有多大」,当时很多数字是「先给你一个结果」。学完本课,你已经能说清每个数字的来历了:
1920 × 1080 ← 分辨率(2.2):一帧有这么多像素
× 3 字节 ← 每个像素约 3 字节(RGB,第 3 课细讲)
───────────────
≈ 6.2 MB ← 一帧的大小(正是 video_frame_init 要分配的内存)
× 60 ← 帧率(2.3):每秒 60 帧
───────────────
≈ 356 MB / 秒
2
3
4
5
6
7
每一个乘数,现在都对应本课一个明确的概念。而结论没变,而且更触目惊心了:一秒钟约 356 MB,一分钟就是 20 多 GB。 这样的裸数据,既存不下也传不出去。
所以视频链路走到「采集」之后,必须有一步把它压小 —— 那就是模块 5 的「编码」。本课为它备好了全部前置概念。
# 2.7 本课小结
- 视频 = 一叠按时间快速播放的静止图片。 拆成四个概念:
- 像素:图像最小单位,一个带颜色的点;图像 = 像素网格。
- 分辨率:宽 × 高(像素数),如 1080p = 1920 × 1080;像素越多越清晰、数据越大(4K 像素是 1080p 的 4 倍)。
- 帧:一张静止画面;靠视觉暂留,连续播放就成了运动。
- 帧率(fps):每秒帧数,24/30/60;越高越流畅、数据越大。
- 帧率在 OBS 里存成分数(
media_frames_per_second的numerator/denominator),从而精确表示 29.97 = 30000/1001 这类值。 - 这些概念在源码里都有落点:
video_output_info(一条流的分辨率/帧率/格式)、obs_source_frame(具体一帧的 width/height/data/timestamp/format)。 - 一帧大小 = 分辨率 × 每像素字节数;裸视频体积大到必须压缩 —— 为模块 5「编码」埋好了动机。
# 2.8 动手 / 观察(本课作业)
- 在 OBS 界面里找概念:打开 OBS → 设置 → 视频,看「基础(画布)分辨率」「输出(缩放)分辨率」和「常用 FPS 值」三个选项 —— 它们正是本课的分辨率和帧率,直接对应
video_output_info的字段。 - 亲手看见像素:用任意看图 / 画图软件打开一张图,放大到 800% 以上,直到你能看见一个个方形色块。
- 读两段源码:打开
libobs/media-io/video-io.h:127,数一数video_output_info有哪些字段、各对应本课哪个概念;再打开libobs/media-io/frame-rate.h,理解 fps 为什么要存成分数。
# 下一课预告
我们一直回避的那个问题 —— 「一个像素的颜色,到底怎么存进电脑?」 —— 下一课揭晓。
第 3 课:颜色是怎么被存进电脑的 —— 讲 RGB 与 YUV、亮度与色度,以及一个关键问题:为什么直播和视频几乎都用 YUV,而不是你更熟悉的 RGB? 答案又一次和「省数据」有关。我们会回到 libobs/media-io/video-io.h 里那一长串 VIDEO_FORMAT_*,看 OBS 到底支持多少种颜色存法。
📁 本课配图:
imgs/02-01~imgs/02-05📌 源码锚点:libobs/media-io/frame-rate.h:7、libobs/media-io/video-io.h:127、libobs/obs.h:283、libobs/media-io/video-frame.h:32
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言