第 11 章 视频怎么”存”:连续图片 + 声音

你点名想懂”音视频怎么保存”。声音那两章(9、10)讲完了。这一章收尾”视频”——你会发现,视频的本质,其实就是”一连串图片 + 一段声音”捆在一起。

先问你一个直觉题:

一部 2 小时的电影,每秒 24 帧,每帧假设是一张 1920×1080 的图。如果”视频=很多张 JPEG 拼起来”,那文件得多大?这么存现实吗?

你大概能猜:大得离谱。所以视频存储有它自己的一套聪明办法。咱们顺着”连续 → 离散 → 压缩”这条你已熟悉的主线走。

11.1 视频 = 很多帧图片(先把时间切碎)

和音频采样一个套路:声音是把”连续的时间”离散成采样点;视频是把”连续运动的画面”离散成一帧一帧的静态图。

  • 每秒多少帧,叫帧率(Frame Rate),单位 fps。电影 24fps,游戏常 30/60fps。
  • 每一帧,本质上就是你在第 3 章会学到的”一张像素阵列”(宽×高个像素点,每点一个颜色)。

所以”视频”最朴素的存法:把每一帧当成一张图,按顺序存,再配一段音频。这就是所谓的”很多张 JPEG 拼起来”。

问题正如你直觉所料:这么干,2 小时电影能堆到几个 TB,谁也存不起、传不动。

11.1.1 为什么不直接存”一堆 PNG”:连回第 3 章的像素账本

我带你用第 3 章的”像素账本”算一笔账,你就知道为什么”很多张 JPEG”都不行,更别说 PNG。

第 3 章讲过:一张 1920×1080 的图,有约 207 万(1920×1080)个像素。每个像素在 RGB 下要 3 字节(R/G/B 各 1 字节),所以一帧原始像素 = 1920×1080×3 ≈ 6.2 MB。如果存的是带透明通道的 RGBA,就是 8.3 MB。

电影按 24fps:每秒 = 6.2 MB × 24 ≈ 149 MB/秒。2 小时 = 7200 秒 → 约 1.07 TB。

即使是 PNG(无损、压缩后通常比原始小不少,但仍是逐帧独立压),也常要原始体积的 1/3 到 1/2,那还是几百 GB 起步。这还没算声音!这显然荒谬——你手机里一部 2 小时电影才 1~2 GB。差了将近 1000 倍。

这笔账同时告诉你两件事,串起前面几章:

  1. 体积爆炸的根源是”帧数 × 每帧像素数 × 每像素字节数”(第 3 章的像素数量 × 第 9 章”时间离散成帧”)。这就是”连续画面”直接数字化的代价。
  2. 要瘦身,必须在”帧内”和”帧间”两处下手(11.2、11.3)。PNG/JPEG 只做了帧内压缩,没碰帧间冗余——而视频里帧间冗余才是真正的大头。

所以”视频 = 一堆图片”在概念上没错,但在工程上必须加两层压缩才能落地。这也是为什么从来没有人把视频存成”几千个 PNG 文件”——那等于主动放弃了 90% 的体积节省。

11.2 帧内压缩:先给”单张图”瘦身

第一层优化,和图片 JPEG 同理:每一帧本身是张图,用 JPEG 那套”扔掉眼睛不敏感细节”的办法压一压。这叫帧内压缩(Intra-frame)。

压完,体积能降不少,但还不够——因为相邻的帧之间,画面几乎没变(你上一帧和这一帧,背景一样,只有人挪了一点点)。

关键观察:视频里绝大部分相邻帧,是”高度重复”的。 这正是视频压缩最大的金矿。

11.2.1 帧内压缩到底怎么压”单张图”:DCT 与宏块的思想

我在 11.2 说帧内压缩”和 JPEG 同理”,这里补一句它到底用了什么招,让你不只是”听说”。JPEG 类压缩的核心动作叫离散余弦变换(DCT, Discrete Cosine Transform),思想很妙:

一张图被切成 8×8 的小块(叫宏块, Macroblock),每个小块本来是 64 个像素的亮度值。DCT 把这 64 个数字”重新表达”成另一组 64 个数字——但新的一组里,前几个数代表”大块明暗趋势”(低频),后面的数代表”细碎纹理”(高频)。然后编码器对”高频那堆小数字”狠狠量化(第 9 章量化思想又出现了:把不重要的小数字直接归零),再用游程编码压掉一连串的 0。

为什么这招灵?因为人眼对”大块明暗”敏感、对”细碎高频纹理”迟钝(和第 10 章音频掩蔽、11.10 的 YUV 色度降采样是同一个”感知弱点”哲学)。扔掉高频细节,图看起来差不多,但数据量骤减。你作为程序员可以这样类比:DCT 像是把信号从”时域/空域”换到”频率域”,然后在频率域里做有损量化——和你做信号分析时把波形变到频域再处理,是同一种数学直觉。

11.3 帧间压缩:为什么视频不等于”很多张 JPEG”

第二层优化,是视频独有的杀手锏——帧间压缩(Inter-frame)。

思路:既然相邻帧大部分一样,那我只存”和上一帧不一样的地方” 不就行了?就像你改文档用”修订模式”,只记录”改了哪几个字”,而不是每次都把整篇重写。

于是视频里有了几种帧:

  • I 帧(Intra,关键帧):完整的一张图,自带全部信息(相当于 JPEG)。
  • P 帧(Predicted,前向预测帧):不存完整图,只存”相对前面某帧,哪儿动了、怎么动”。
  • B 帧(Bi-directional,双向预测帧):更狠,参考”前面和后面”两帧来推算自己。

一组 I 帧开头、后面跟一串 P/B 帧,叫一个 GOP(图像组)。播放时,解码器从 I 帧出发,按 P/B 的”差异指令”一路推算出每帧画面。

这就是为什么视频远小于”很多张 JPEG 之和”:你只付了少量 I 帧的”全价”,其余都是”差异补丁”。

11.3.1 预测思想:连回第 9 章”相邻重复可利用”的全书哲学

我点明一件你大概已经隐约感觉到了的事:视频的帧间压缩,和第 9 章声音的”采样只记离散点”、乃至全书的”现实连续 → 离散化 → 利用冗余压缩”,是同一个哲学的三个切面。

  • 声音里,你发现”相邻采样点之间变化平缓”,所以量化误差很小(9.3)。
  • 视频里,你发现”相邻帧之间画面几乎不变”,所以只需记差异(P/B 帧)。

它们的共同内核是:真实世界的信息在时间/空间上高度相关,前一个样本和下一个样本不是随机的,而是”差不多”的。既然差不多,我就只存”变化量”而不是”全部”,这就是所有多媒体压缩的命门——用”预测 + 残差”代替”完整重述”。P 帧存的其实就是”我用前一帧预测当前帧,预测值和真实值的差(残差)”。

你作为写过后端的人,可以把它类比成增量更新(delta):与其每次全量同步一个大对象,不如只传”相对上一次改了什么”。视频的 P/B 帧,就是画面层面的”增量同步”;GOP 就是”每隔一段发一次全量快照(I 帧),中间全发增量”。这套思想你在做数据同步、日志复制时一定用过——现在你看到它也被用来省视频体积。

副作用你要知道:因为 B/P 帧依赖别的帧,所以视频不能像图片那样随便从任意一帧开始播(得先找到上一个 I 帧)。你拖动进度条时偶尔”卡一下”,常是在等解码器定位最近的 I 帧。这是压缩带来的代价。

11.3.2 GOP 长度与关键帧间隔:压缩率和”可拖动性”的拉锯

GOP(图像组)不是随便切的,里面有个工程权衡你该知道:GOP 越长(两个 I 帧之间塞越多 P/B 帧),体积越小,但拖动越卡。

  • I 帧是”全价完整图”,最占空间;P/B 帧是”差异补丁”,很便宜(11.3)。
  • 如果你让 GOP 很长(比如 300 帧才一个 I 帧),大部分画面都靠”差异”表达,整体体积最小。但代价是:用户拖到 150 帧处,解码器必须从最近那个 I 帧(第 0 帧)一路推算 150 帧才能画出当前画面,等待明显,拖动发涩。
  • 反过来 GOP 很短(比如每 15 帧一个 I 帧),拖动秒开,但体积变大(I 帧多了)。

所以视频网站/游戏过场常把 GOP 设在”几秒一个 I 帧”的甜点区。你作为后端,这又是一个熟悉的取舍:用”随机访问成本”换”存储/传输成本”,和缓存粒度、分片大小的选择是同一类判断。编码参数里的 keyframe interval(关键帧间隔)就是拧这个旋钮的。

11.4 码率、编码、容器:三个词别再混

视频文件的描述,也是”编码 + 容器”的结构,外加一个码率(Bitrate):

  • 编码(Codec):怎么把帧压缩成数据的算法。常见的:
    • H.264 (AVC):最普及,兼容极好,多年老大。
    • H.265 (HEVC):比 H.264 省约一半体积,但专利复杂、老设备不支持。
    • VP9 / AV1:开源/新兴,Web、流媒体在推。
  • 码率(Bitrate):每秒数据量,单位 bps/kbps/Mbps。码率越高,画质越好、体积越大。这是你调视频”清晰度”最直接的手柄(和音频的 kbps 同理)。
  • 容器(Container):把视频流、音频流、字幕、元数据打包的文件格式。常见 .mp4(容器)、.mkv、.mov、.webm。

注意:mp4 是容器,里面视频流可能是 H.264 也可能是 H.265,音频流可能是 AAC 或 OGG。不要以为 “.mp4” 就等于某个编码——又是”箱子”和”叠法”两回事(第 10 章讲过)。

11.4.1 帧率与码率的关系:两个独立的手柄

这里有个新手常混的点:帧率(fps)和码率(bps)是两个独立的旋钮,但会互相影响体积。

  • 帧率决定”每秒有多少帧画面”——影响流畅度(动作片/游戏要高,24fps 看静态对话够)。
  • 码率决定”每秒总共分配多少数据”——影响每帧的清晰度。

它们之间有个朴素关系:在总码率固定时,帧率越高,平均每帧分到的数据越少,单帧越糊;反之帧率低,每帧能更清晰。这就解释了为什么有些”高帧率但低码率”的视频看起来”丝滑但满屏马赛克”——帧够多,但每帧都被压得太惨。

所以调视频清晰度的正确顺序是:先定帧率(由内容决定:游戏录屏 60fps、电影 24fps),再用码率去填”这个帧率下要够清晰”的预算。这和第 10 章”先定采样率位深、再定压缩码率”是同一个决策结构——你先在”时间离散密度”上做选择,再在”每单位的数据预算”上做选择。

11.5 游戏过场动画 vs 实时渲染:殊途同归

最后连回游戏。游戏里的”视频”有两种来源,值得你分清:

  1. 预渲染视频(CG 过场):开发阶段用工作站花几小时渲染好的精美片段,存成 mp4/ogv,游戏里直接当视频文件播。优点是画质炸裂(因为渲染时不赶 16 毫秒),缺点是占存储空间、且不能和玩家互动。
  2. 实时渲染过场:引擎临时”接管”摄像机,用游戏自己的实时渲染管线把画面算出来。优点是能互动、零额外存储(画面是算出来的),缺点是画质受实时算力限制。

你看,这正好是第 1 章”实时 vs 预计算”那条线的回响:

  • 预渲染视频 = “提前把菜做好冻起来”(占硬盘,省实时算力)。
  • 实时过场 = “当场现炒”(占算力,省存储,能互动)。

理解了”连续→离散→压缩”,你再看 B 站/抖音/Netflix 的清晰度选项、游戏里 CG 与实时过场的切换,全都通透了。

11.6 把”音视频”这一大块收个口

你最初的疑问——“音视频怎么保存到计算机、保存了哪些信息”——现在可以完整回答了:

  • 声音:连续声波 → 采样(采样率)+ 量化(位深)+ 声道 → PCM 数字 → 感知压缩(MP3/OGG,有损)→ 装进容器(WAV/MP3/OGG)→ 游戏里按长短决定载入/流式。
  • 视频:连续画面 → 按帧离散(帧率)→ 帧内压缩(JPEG 类)+ 帧间压缩(I/P/B 帧,只存差异)→ 编码(H.264 等)+ 码率控制 → 容器(MP4 等),并和一路音频流捆在一起。

两者底层是同一个哲学:现实是连续的,计算机要离散;离散后用”人类感知弱点”和”相邻重复”两座金矿来压缩;最后装进带说明书的容器。你以后看任何多媒体格式,都能用这套骨架去拆解。

11.7 你在 Unity 里放一段过场视频会踩的坑

理论落到 Unity 的 VideoPlayer:

  • 音频不同步:视频文件里音频流和视频流分开解码,靠时间戳对齐。低端机解码慢,会出现”嘴型对不上”。实战常把过场视频的音轨单独抽出来用 AudioSource 播,反而更稳。
  • 首帧卡顿:视频没预加载完就播,会黑屏一下。解决是先让 VideoPlayer 预热(Prepare())再 Play()。
  • 分辨率与体积:4K 过场在手机上解码不动,常降级到 1080p。这和第 3 章”像素密度 vs 性能”是一回事——屏幕就那么大,塞 4K 是浪费。

11.7.1 音视频怎么”对上拍”:PTS 与时间戳

你一定好奇:视频和那一路音频是分开解码的(11.6),播放时怎么保证”嘴型和声音同步”?答案是时间戳(PTS, Presentation Time Stamp)。视频流和音频流的每一帧/每一块都带着一个”我该在第几秒出现”的标签。播放器维护一个统一时钟,谁到点到谁上屏/出声。

B 帧(双向预测)还引出另一个概念 DTS(Decode Time Stamp,解码时间戳):因为 B 帧要参考”后面的帧”,所以它解码顺序和播放顺序不一致——得先解完后面的帧,才能解中间的 B 帧。于是解码按 DTS 走、播放按 PTS 走,两者错开。这解释了为什么 11.7 说”低端机解码慢会嘴型对不上”:解码跟不上统一时钟,PTS 对齐就崩了。你作为后端,可以类比成”消息的生产时间戳 vs 消费顺序”——乱序到达时靠时间戳重新排回正确顺序。

11.8 为什么 B 站 1080p 比 4K 流畅

回到码率(11.4):4K 帧像素是 1080p 的约 4 倍,同样码率下每个像素分到的数据只剩 1/4,反而更糊或有块效应;而 1080p 在码率充足时干净流畅。所以”分辨率高 ≠ 清晰”,码率才是清晰度的真实手柄。你调视频清晰度,先问码率,再问分辨率。

11.9 一个”10 秒视频有多少数据”的估算小推演

我把前面所有账本合起来,给你做一次完整估算,让你对”视频体积”有肌肉记忆。

假设一段游戏过场:分辨率 1280×720、30fps、H.264 编码、目标码率 3000 kbps(3 Mbps,常见 720p 网络视频档位)、配一路 128kbps 的 AAC 音轨。时长 10 秒。

  • 视频数据量:码率 3000 kbps = 3000 ÷ 8 = 375 KB/s。10 秒 = 3750 KB ≈ 3.66 MB。
  • 音频数据量:128 kbps = 16 KB/s。10 秒 = 160 KB ≈ 0.16 MB。
  • 合计 ≈ 3.8 MB。

再对照”不压缩会怎样”:一帧原始像素 = 1280×720×3 ≈ 2.76 MB;30fps → 每秒 82.8 MB;10 秒 ≈ 828 MB。压缩后 3.8 MB vs 不压缩 828 MB,省了 200 多倍。

你看,这 200 倍就是”帧内压缩 + 帧间压缩 + 感知压缩”三座金矿合力挖出来的。而那个”3 Mbps 码率”的旋钮(11.4),直接决定这 10 秒是 3.8 MB 还是 7.6 MB——你调清晰度,本质上就是调这个值。

顺带一题:如果这段视频是 60fps 而非 30fps,在同样 3 Mbps 总码率下,10 秒体积几乎不变(因为体积由总码率×时长决定),但每帧清晰度会减半——这正是 11.4.1 说的”帧率与码率互相挤占预算”。

11.10 YUV 色彩空间:为什么视频不直接存 RGB(比 RGB 省在哪)

我补一个能让你”看穿”视频压缩底层的关键点:视频几乎不直接存 RGB,而是存 YUV。

一张图的颜色,RGB 用”红绿蓝”三个分量描述。但人眼的生理特点很明确:对”亮度”极其敏感,对”颜色(色度)“相对迟钝。比如你闭眼想一片草地,你能立刻说出”亮不亮”,但要精确说出”是偏黄绿还是偏蓝绿”就难了。

YUV 把颜色拆成:

  • Y(Luma,亮度):画面的明暗信息——人眼最在乎的。
  • U、V(Chroma,色度):颜色偏差信息——人眼不在乎细节。

于是编码器可以对 Y 给满分辨率,对 U、V 降采样(比如只保留 1/4 的色度像素,即 “4:2:0” 格式)。因为人眼看不出色度被砍,但体积立刻省掉约一半(RGB 三个分量全分辨率 vs YUV 里两个色度分量被稀释)。

这和第 10 章音频的”掩蔽效应”是同一个省钱哲学的不同舞台:音频是”利用人耳听不见的扔掉”,视频 YUV 是”利用人眼对色度不敏感扔掉”。两者都建在”人类感知有弱点”这个事实上。你作为程序员,可以把它类比成”按需保留高精度字段”——对关键字段(亮度)用高精度,对非关键字段(色度)降精度,整体省一大笔。

更重要的是,YUV 还让”帧间预测(11.3)“更顺手:因为亮度承载了绝大部分结构信息,预测残差在 Y 上最重要,U/V 上的残差本就小,压缩效率更高。所以 YUV 不是”换个颜色表示法”那么简单,它是整个视频压缩能省那么多体积的底层支点之一。

11.11 Unity 的 VideoPlayer:落地时你要拧的几个旋钮

最后把理论落到 Unity 的 VideoPlayer 组件,你至少要知道这几个关键参数各管什么(原理你已全懂,这里只是”旋钮对照表”):

  • Source(来源):Video Clip(直接挂一个视频资源)还是 URL(从网络/本地路径流式拉)。对应 10.4 的”载入 vs 流式”——URL 模式就是视频版的 Streaming。
  • Render Mode(渲染到哪):Camera Far Plane(画在摄像机远裁剪面,像背景)、Render Texture(渲染到一张纹理,你再把它贴到任意物体/UI 上)、Material Override(覆盖某个物体的材质)。这是”视频画面最终出现在游戏世界的哪个位置”的开关。
  • Audio Track(音轨):视频里可能有多条音轨,这里选播哪条(比如国语/英语)。配合 11.7 说的”抽音轨用 AudioSource 播”来避免不同步。
  • Play On Awake / Loop / Speed:是否开场自动播、是否循环、播放速度(调 playbackSpeed 可做慢动作回放)。
  • Prepare() 与 isPrepared:如 11.7 所述,播放前先 Prepare() 预热解码,等 isPrepared 为 true 再 Play(),可消除首帧黑屏。

记住:这些旋钮没有一个是”装饰”。Source 决定走内存还是走 IO(10.4 的账本),Render Mode 决定画面去哪(和第 2 章坐标/第 7 章摄像机是同一空间逻辑),Audio Track 决定声音怎么和视频绑(11.6 说的”视频和一路音频捆在一起”)。你拧它们时,脑子里想的应该是前面十一节讲过的原理,而不是”随便试试哪个好看”。

【思考题】

  1. 为什么说”视频 = 很多张 JPEG 拼起来”既不假、又不现实?真正视频存储比这多了什么?
  2. I 帧、P 帧、B 帧各自存的是什么?为什么视频不能像图片那样从任意位置秒开?
  3. 码率(Bitrate)控制视频的什么?和音频的 kbps 是同一类概念吗?
  4. “.mp4” 是编码还是容器?里面一定装 H.264 吗?
  5. 游戏 CG 过场用”预渲染视频”,实时过场用”实时渲染”,各自拿什么换什么?这和第 1 章哪句话呼应?

【参考答案】

  1. 不假:视频确实由逐帧静态图组成。不现实:若每帧都存完整图(无帧间压缩),2 小时电影体积达 TB 级,存不下传不动。真正的视频存储多了帧间压缩——只存相邻帧的差异(P/B 帧),大幅削减冗余,所以远小于”很多张 JPEG 之和”。
  2. I 帧存完整图(自包含);P 帧存”相对前帧的差异/运动”;B 帧参考前后两帧推算。因 P/B 依赖他帧,播放须从最近的 I 帧起解,故不能任意位置秒开——拖进度时卡顿常在等定位 I 帧。
  3. 码率控制”每秒数据量”,直接决定清晰度与体积:码率高→清晰但大。和音频 kbps 是同一类概念(都是”每秒比特数”),只是对象一是视频流、一是音频流。
  4. .mp4 是容器,不是编码。里面视频流通常是 H.264,但也可能是 H.265/VP9 等;音频流可能是 AAC/OGG。所以不能说 mp4 一定装 H.264。“箱子”和”叠法”分开看。
  5. 预渲染视频:用硬盘存储换实时算力(画质高但占空间、不可互动);实时过场:用实时算力换存储与互动性(省空间、能互动但画质受限)。呼应第 1 章”把贵的提前做掉、运行时拼装”——CG 是把渲染”提前”做好冻起来,实时过场是”当场现炒”。