我不只是一个网络主播,我是一个VTuber 。我每隔一个周末就会在 Twitch上进行流媒体直播,在那里我从事各种工作,并尝试在做这些事情时解释我的过程。我开始这样做是为了帮助更适应公开演讲,这绝对是克服恐惧的催化剂。
这个网站旨在成为我的专业作品集,我还没有真正的好方法来编码我参加过的活动或我做过的直播。随着我越来越深入地了解开发者关系的技巧,这些事情变得越来越明显,我需要这样的东西。
然而,只制作带有 YouTube 嵌入的部分就可以了,但我认为这不符合 Xe Iaso 品牌的真正精神。如果我的网站上有一个流媒体部分,我希望人们能够在我的网站上观看这些流媒体。我不想受制于 YouTube 或 Twitch 如何压缩视频或限制画中画的使用。我想自行托管视频。
不幸的是,这有一些问题:
- 我的流源文件是千兆字节,每小时素材大约有 2 GB 的视频。根据一些粗略的粗略估计,即使我的嵌入式视频文件的点击率非常低(我对 Prometheus 的估计约为 40%),但提供未经优化的视频将是非常不经济的。
- 我搞砸了XeDN的基本架构,它缓存文件的部分方式涉及将这些文件读入 ram。 fly.io 上的 XeDN 节点每个只有 256MB 的内存。我可以改变它的工作方式以使其更容易,但这将是我真的不想考虑的大量架构和工程。 XeDN 是一个非常简单的东西,我很容易记住它的所有限制和优点。我不想失去那个。
- XeDN 目前由 5 GB 的数据量提供支持,这个数字是任意选择的,这样我一两年内都不必考虑这个问题。我可以增加卷的大小,但我还不想考虑这个。
- 我使用HLS分发视频的方式已经与XeDN的核心架构的工作方式相得益彰。我想让它变得更好,而不是必须大量重新发明所有活动部件的基本架构。
所以,我需要更好地压缩视频。在我开始这段旅程之前,我尽了最大的努力让我的视频每小时大约 1 GB。在做了更多的餐巾纸背面带宽和存储数学之后,我对此表示满意。我将不得不比我希望的更早地扩大规模,但这是让事情变得更复杂的自然结果。感谢朋友的帮助,我想出了如何压缩我的视频很多。一个 3.5 小时的视频全部压缩到 330 MB。事实上,我每两周进行大约 2-4 小时的流媒体播放,这意味着我需要考虑大约 6 月份 XeDN 的存储需求。这对我来说是可以接受的。我一直在关注 XeDN 卷的磁盘使用情况,并设置了一个警报,以防磁盘使用量以某种方式出现巨大的峰值,但在那之前应该没问题。
由 Pastel-mix 生成的图像 — 风景、春天、温泉、高度详细、色彩缤纷、樱花、水彩、无人
视频压缩
如果您从未深入过这个兔子洞,让我们花点时间介绍一下视频压缩解决的基本问题。在高层次上,视频是一堆图片,播放速度足够快,可以让您的大脑认为有运动。
处理这个问题的一种天真的方法是只获取每一帧视频并将其存储在磁盘上并进行一点压缩。这就是许多专业格式(如 RED raw 和 Apple 的 ProRes 视频)的视频存储方式,但无与伦比的质量水平伴随着无与伦比的磁盘空间使用水平。一分钟的 4K ProRes 视频可以轻松占用 6 GB 的空间。这种质量级别对于专业视频编辑来说是绝对必需的,因为它为您提供了灵活性,但对于大多数其他用途(例如制作我非常不擅长使用 CSS 的流媒体录制),它就太过分了。
代代相传之前 | 代代相传后 |
---|---|
不过,有一种简单的方法可以压缩所有这些,为此我们将不得不看看动画,就像迪斯尼在他们仍然手工制作所有动画时所做的那样。
当迪士尼电影是手工绘制时,他们有两层艺术家:关键帧艺术家和中间帧艺术家。关键帧艺术家为单个场景的关键部分绘制整个基础图像(或者更现实地说,是在拍摄时手动合成的那些东西的各个图层),然后其余工作由中间帧完成艺术家。人们再次在术语上打磨,完全合成的图像被称为关键帧,其余的被称为补间帧。
借助现代视频压缩技术,我们实际上可以利用视频文件中的每一帧与其之前的帧几乎相同的事实。因此,我们不能存储整个视频帧,而只能记录该帧与前一帧之间的差异。这是导致在线视频流在高速互联网部署到大众并被大众使用之前可行的关键发现。这就是我们拥有 Netflix 的原因。
VTubing 和压缩
了解了视频压缩的工作原理(tl;dr:大多数帧都存储为前一帧之间的差异,保存与它们相关联的整个图像的帧),让我们来看看我的流是如何工作的。这是我最近的一个流中的一个随机静止帧:
核心元素非常简单,其中很多都不会改变:
- 我的 VTuber 头像有一个“facecam”窗口,背景每分钟变化一次
- 框架左侧有我的印记,在我的摄像头窗口下方
- 有一个终端窗口适合的切口(精心定位,以便切口隐藏 Windows 终端选项卡栏)
更新最频繁的两件事是终端窗口和面部摄像头。在写这篇文章的时候,叠加层是静态的(但是我一直在考虑用 Godot 做一些动态的东西,这样我就可以嵌入 Twitch 聊天,但是对隐私和用户生成的内容的担忧让我仍然考虑执行此操作的最佳方法)。这意味着大约 5-10% 的视口不会在每一帧都发生变化,这意味着可以为发生变化的事物节省每秒带宽。
我的 VTuber 软件 VSeeFace 功能有点不完整,这意味着每帧没有太多变化。这意味着面部摄像头也没有太大变化(它主要在关键帧之间切换,以模拟常见的语音声音),这进一步节省了比特率。
考虑来自我的一个流的这两个随机顺序帧:
第 1 帧 | 第 2 帧 |
---|---|
你注意到区别了吗?这是我的 VTuber 头像在为 /s/ 声音做动画。这意味着大约 0.05% 的视口实际发生了变化。这让我可以在不损失质量的情况下降低比特率,就像在 Beat Saber 中那样。考虑我玩Synth Riders的这两个随机连续帧:
第 1 帧 | 第 2 帧 |
---|---|
这两个帧之间有更多明显的差异,因此需要更高的比特率才能以一种观看起来不痛的方式对该视频进行编码。
编码-ening
例如,让我们看一下我最近的流,其中我主要修改了基于 Nix 的 Emacs 配置,以便我可以使用它而不是我的 Spacemacs 配置。在我的视频文件夹中,1 小时 54 分钟的视频文件占用了大约 4.82 GB 的空间。当我将它传递给 ffprobe(告诉您媒体文件信息的 ffmpeg 工具)时,我得到了这个元数据:
Input #0, matroska,webm, from 'emacs-hacking.mkv': Metadata: ENCODER : Lavf58.76.100 Duration: 01:53:47.14, start: 0.000000, bitrate: 6066 kb/s Stream #0:0: Video: h264 (High), yuv420p(tv, bt709/reserved/reserved, progressive), 1280x720 [SAR 1:1 DAR 16:9], 30 fps, 30 tbr, 1k tbn (default) Metadata: DURATION : 01:53:47.133000000 Stream #0:1: Audio: aac (LC), 48000 Hz, stereo, fltp (default) Metadata: title : simple_aac DURATION : 01:53:47.136000000
以下是我们可以从中得到的关键信息:
- 这是 6066 kb/秒的 h.264 视频
- 这是每秒 30 帧的 720p 视频
- 音轨为AAC ,这是 h.264 视频文件最常用的音频编解码器
- 来自 HLS 公告帖子的首次尝试
- ffmpeg命令
- 输出尺寸
- 看得见的品质
所以,让我们开始吧!在我最初的 HLS 帖子中,我使用了这个命令:
ffmpeg \ -i ./source.mp4 \ -level 3.0 \ -start_number 0 \ -hls_time 10 \ -hls_list_size 0 \ -f hls \ index.m3u8
如果我让它运行,我会得到一个 470MB 的文件夹,其中包含 HLS 块。每个 HLS 块大约是 10 秒的视频,平均每个块大约是 1.45MB。这使我们每小时的总成本为 235 MB。这比您从相当简单的设置中期望的要好得多,这就是我在我的博客上使用的设置。它不是最好的,也不是最差的,但它可以完成工作并且对于大多数用途来说已经足够好了。
通过 ffprobe 运行一个随机块给我这个元数据:
Input #0, mpegts, from 'baseline/index5.ts': Duration: 00:00:16.67, start: 51.421333, bitrate: 520 kb/s Program 1 Metadata: service_name : Service01 service_provider: FFmpeg Stream #0:0[0x100]: Video: h264 (Constrained Baseline) ([27][0][0][0] / 0x001B), yuv420p(tv, bt709/reserved/reserved, progressive), 1280x720 [SAR 1:1 DAR 16:9], 30 fps, 30 tbr, 90k tbn Stream #0:1[0x101]: Audio: aac (LC) ([15][0][0][0] / 0x000F), 48000 Hz, stereo, fltp, 144 kb/s
它看起来很棒:
第 1 帧 | 第 2 帧 |
---|---|
然而,我们可以更深入。源视频约为 6000 kb/秒,而 ffmpeg 已将其压缩至 520 kb/秒。但是,我们甚至没有了解 ffmpeg 真正有趣的功能:多通道编码。
多次通过漂流编码
我刚刚进行的编码被称为单通道编码。 ffmpeg 从视频文件的开头开始,并对所有内容进行编码。对于正在发生的事情并没有真正的预知,它是视奏视频并在飞行中进行。多通道编码让 ffmpeg 扫描整个视频一次,然后使用该信息更优化地编码内容。为了用多通道编码对此进行编码,我们需要有两个 ffmpeg 命令:
ffmpeg \ -i ./emacs-hacking.mkv \ -b:v 550k \ -level 3.0 \ -f mp4 \ -pass 1 \ -y \ /dev/null
ffmpeg \ -i ./emacs-hacking.mkv \ -b:v 550k \ -level 3.0 \ -start_number 0 \ -hls_time 10 \ -hls_list_size 0 \ -f hls \ -pass 2 \ ./two-pass/index.m3u8
这让我们得到了一个稍大的结果大小(主要是因为任意选择的比特率),但它为每小时 300 MB 的视频提供了 600 MB 的输出。这对于我想要的东西来说有点太大了,但它是更深入的一个很好的起点。
ffprobe 报告有关随机 HLS 块的信息:
Input #0, mpegts, from './two-pass/index277.ts': Duration: 00:00:08.33, start: 2776.421333, bitrate: 561 kb/s Program 1 Metadata: service_name : Service01 service_provider: FFmpeg Stream #0:0[0x100]: Video: h264 (Constrained Baseline) ([27][0][0][0] / 0x001B), yuv420p(tv, bt709/reserved/reserved, progressive), 1280x720 [SAR 1:1 DAR 16:9], 30 fps, 30 tbr, 90k tbn Stream #0:1[0x101]: Audio: aac (LC) ([15][0][0][0] / 0x000F), 48000 Hz, stereo, fltp, 131 kb/s
这是两个连续的帧:
第 1 帧 | 第 2 帧 |
---|---|
这是可以接受的,我不会因为在我的主要基于文本的流中看到它而感到羞耻。
我们不需要讨厌的比特率!
在视频压缩中,比特率是视频每秒的数据位数。这包括关键帧。该过程的这一部分是您开始使用比特率数字并进一步压缩的地方。在我的测试中,我发现 150kb/sec 是文本可读性和文件大小之间的最佳平衡。这意味着您可以使用如下所示的 ffmpeg 命令:
ffmpeg \ -i ./emacs-hacking.mkv \ -b:v 150k \ -profile:v high \ -preset slow \ -tune animation \ -pix_fmt yuv420p \ -f mp4 \ -pass 1 \ -y \ /dev/null
ffmpeg \ -i ./emacs-hacking.mkv \ -b:v 150k \ -profile:v high \ -preset slow \ -tune animation \ -pix_fmt yuv420p \ -start_number 0 \ -hls_playlist_type vod \ -hls_allow_cache 1 \ -hls_time 0 \ -hls_list_size 0 \ -f hls \ -pass 2 \ ./two-pass-150/index.m3u8
这个文件的编码方式还有一个主要区别。我将hls_time
从 10 秒更改为 0 秒。 HLS 建立在将视频分块成十亿个小块的基础上, -hls_time
标志告诉 ffmpeg 在大约那么多秒后在下一个关键帧分块视频。这意味着我们之前制作的每个 HLS 块都有大约 10 秒加上到视频的下一个关键帧的间隙,所以平均大约 10-15 秒。当我们将比特率降低到如此低的水平时,我们还需要更积极地将每个关键帧移动到它自己的 HLS 块中,否则看起来会有点不正常,以至于人们无法真正放置手指在。
-tune animation
告诉 ffmpeg 将更多去块数据放入生成的文件中,这样您就不太容易注意到压缩伪影。原生地,每组像素都被压缩为一堆像素的超级块,大多数时候您看不到像素组之间的差异。您可以通过计算机 UI 的动画和视频录制很容易地看到这一点,尤其是当视频变化非常快时。如果您曾在 YouTube 视频中看到闪闪发光的东西变得块状,这就是原因。其他编码设置是我从动漫盗版论坛上得到的,该论坛提供了有关如何使动漫在较小的文件大小下看起来不错的建议。我想如果你足够眯眼的话,你可以认为我的 vtubing 东西是动漫,所以让我们把它扔进锅里看看有什么棒。
执行此命令后,我得到的总文件大小为 267 兆字节。这更接近我想要的!
这是随机块上的 ffprobe 输出:
Input #0, mpegts, from './two-pass-150/index341.ts': Duration: 00:00:08.39, start: 2768.080167, bitrate: 258 kb/s Program 1 Metadata: service_name : Service01 service_provider: FFmpeg Stream #0:0[0x100]: Video: h264 (High) ([27][0][0][0] / 0x001B), yuv420p(tv, bt709/reserved/reserved, progressive), 1280x720 [SAR 1:1 DAR 16:9], 30 fps, 30 tbr, 90k tbn Stream #0:1[0x101]: Audio: aac (LC) ([15][0][0][0] / 0x000F), 48000 Hz, stereo, fltp, 138 kb/s
是的!视频流现在经过优化,音频成为更大的关注点。这是另外两个连续帧:
第 1 帧 | 第 2 帧 |
---|---|
现在这就是您如何知道您在比特率和视觉质量之间找到了良好的平衡。我复制的那个随机 HLS 块是 265 KB,大约是 8 秒的视频。这与 ffmpeg 给我们的比特率估计值 258 kb/sec 一致。这也足够小,对带宽和 XeDN 存储架构的工作方式非常有效。这大约是这篇文章封面图片的 jpg 形式的两倍。
如果您尝试使用较低的比特率,您会开始在文本字符周围发现光晕,并且当您的 comcast 连接突然出现 pingspike 时,总体上看起来就像 YouTube 下降到 240p,然后看起来就像您有 1 美元一样对于图像中的每个像素,您将获得一美元。
更深
在这一点上,我们已经遇到了视频编码的实际问题,但我们在音频部门仍有很大的空间。到目前为止,视频每秒占用大约 150 kb 的数据,但音频每秒占用 128 kb。如果我可以使用类似opus的音频和av1的视频,那就太好了,但我还不能使用它们。我可能可以通过使用一种是 av1 编码而另一种是 mp4 编码来进行多种编码,但这对我的需要来说有点太复杂了。更不用说这对节省空间问题没有帮助。
所以,让我们压缩音频。在朋友的建议下,我尝试使用libfdk_aac
编解码器将音频降低到每秒 32 kb,但这样听起来不像是试图通过电话线播放音乐会交响乐。我查看了有关如何使用libfdk_aac
的文档并遇到了一个小问题:
libfdk_aac 的许可与 GPL 不兼容,因此当 GPL 许可代码也包含在内时,GPL 不允许分发包含不兼容代码的二进制文件。因此,此编码器已被指定为“非免费”,您无法下载支持它的预构建 ffmpeg。这可以通过自己编译 ffmpeg 来解决。
我检查了一下,我的包管理器中的 ffmpeg 版本不是用libfdk_aac
的。
Nix 的基本概念之一是包构建是接受包含构建指令的属性集的函数,然后将所有这些传递给构建器函数,该函数将所有元数据编译成在沙箱中运行的 bash 脚本。 Nix 将处理抓取任何依赖项并将它们放入构建环境的$PATH
中。这一切都 Just Works™️。
有两种方法可以覆盖包构建的工作方式:
- 更改传递给包函数的参数
- 更改传递给构建器函数的属性
在这种情况下,我们需要更改 ffmpeg 的配置标志和构建依赖项,因此我们将选择使用每个包都可用的.overrideAttrs
方法覆盖构建的属性。这是我们需要的覆盖:
pkgs.ffmpeg_5-full.overrideAttrs (old: rec { configureFlags = old.configureFlags ++ [ "--enable-libfdk_aac" "--enable-nonfree" ]; buildInputs = old.buildInputs ++ [ pkgs.fdk_aac ]; })
这会覆盖buildInputs
(本机依赖项,例如 openssl 和命令行工具)和configureFlags
(传递给./configure
的参数,大多数 C/C++ 构建系统的入口点)以添加对libfdk_aac
的支持。然后我们可以将其放入flake.nix
文件中,并在环境中预填充该文件来制作devShell
:
{ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; utils.url = "github:numtide/flake-utils"; };
outputs = { self, nixpkgs, utils }: utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; overlays = [ ]; };
ffmpeg = pkgs.ffmpeg_5-full.overrideAttrs (old: rec { configureFlags = old.configureFlags ++ [ "--enable-libfdk_aac" "--enable-nonfree" ]; buildInputs = old.buildInputs ++ [ pkgs.fdk_aac ]; }); in rec { devShell = with pkgs; mkShell { buildInputs = [ ffmpeg ]; }; }); }
flake.nix
文件,以便它将自定义 ffmpeg 构建公开为一个包。现在 ffmpeg 命令看起来像这样:
ffmpeg \ -i ./emacs-hacking.mkv \ -b:v 150k \ -profile:v high \ -preset slow \ -tune animation \ -pix_fmt yuv420p \ -f mp4 \ -pass 1 \ -an \ -y \ /dev/null
ffmpeg \ -i ./emacs-hacking.mkv \ -b:v 150k \ -profile:v high \ -preset slow \ -tune animation \ -pix_fmt yuv420p \ -start_number 0 \ -hls_playlist_type vod \ -hls_allow_cache 1 \ -hls_time 0 \ -hls_list_size 0 \ -f hls \ -c:a libfdk_aac \ -profile:a aac_he_v2 \ -b:a 32k \ -pass 2 \ ./two-pass-fdk/index.m3u8
这将使我们最终得到一个 178 兆字节的文件夹。 ffprobe 会让我们确认我们已经赢了:
Input #0, mpegts, from './two-pass-fdk/index341.ts': Duration: 00:00:08.39, start: 2768.163000, bitrate: 146 kb/s Program 1 Metadata: service_name : Service01 service_provider: FFmpeg Stream #0:0[0x100]: Video: h264 (High) ([27][0][0][0] / 0x001B), yuv420p(tv, bt709/reserved/reserved, progressive), 1280x720 [SAR 1:1 DAR 16:9], 30 fps, 30 tbr, 90k tbn Stream #0:1[0x101]: Audio: aac (HE-AACv2) ([15][0][0][0] / 0x000F), 48000 Hz, stereo, fltp, 33 kb/s
这可能是获得合理声音的基础。最终结果是每小时流式镜头 89 兆字节的数据。它还应该最大限度地与设备兼容,并让我节省带宽和存储费用。这些块的大小也约为 1-1.5 张 JPEG 图像,这意味着它们对于 XeDN 的存储/检索架构非常有效。
注意事项
唯一的问题是它特别针对我的流 VOD 进行了非常优化,如果我想对 Synth Riders 游戏进行编码,我需要大幅提高视频和音频质量,否则看起来会很糟糕。例如,这是我为较早的帧比较记录的 Synth Riders 游戏玩法:
这使我能够将 248 MB 的文件压缩到 68 MB。我不得不在音频质量上做出妥协,因为我使用的 AAC 编解码器配置文件针对人类语音进行了调整,这对于我的主要是人类语音的流来说是可以理解的。特别是对于那个视频,我使用了这些标志:
-c:a libfdk_aac -b:a 64k
这告诉 ffmpeg 使用libfdk_aac
作为音频编解码器,并设置每秒 64 kb 的比特率。我在这里没有指定编解码器配置文件,因为我想获得默认的低效率编解码器。这堆编解码器为您提供与 Spotify 或 Apple Music 大致相同的音频体验(除非您启用无损或杜比全景声音频),因此它应该足够好。
如果我不去优化压缩,我最终会得到一个 9 MB 的视频,看起来和听起来像这样:
这有点有趣,因为在我进行压缩之前,我不认为音频会像现在这样好。你真的可以听到这首歌的高频完全被破坏了(我用于 VTubing 视频的编解码器配置文件针对人类语音进行了优化,而人类语音没有很多高频声音),尤其是与之前的录音相比。
总的来说,我对这个结果很满意。我有一个来自 YouTube 的越野车,我可以在我自己的基础设施上托管所有视频。对于大多数用户而言,此解决方案与 YouTube 上托管的内容没有区别(除了缺少观看次数、评论和其他反功能,如喜欢/不喜欢按钮)。我很确定这将满足我的需求并允许我自行托管我创建的内容。