尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Colibri:本地优先低延迟局域网屏幕共享与WebSocket推流

发布时间:2026/9/18 4:20:35

资讯中心
01
ARTICLE

Colibri:本地优先低延迟局域网屏幕共享与WebSocket推流

Colibri:本地优先低延迟局域网屏幕共享与WebSocket推流
蜂鸟这个意象是我给这套工具起名 Colibri 的全部理由体积小、悬停准、翅膀振频高到肉眼几乎看不清。Colibri 就是一个本地优先的轻量屏幕共享工具跑在你自己的机器上把当前桌面以低延迟推给同一局域网里的另一台设备看。它解决的不是远程控制别人电脑这种重型需求而是我想把这块屏幕给旁边那台平板、那台备用机、那台接在投影上的机器看一眼这种每天都可能发生的小事。市面上远程桌面软件不少但它们要么太重要么在局域网里绕一大圈云中转延迟和隐私都让人不踏实。这套东西适合三类人手上有两台以上设备、经常需要在设备之间递一眼画面的人想学屏幕采集、帧编码、WebSocket 推流这条链路需要一个能跑起来的最小骨架的人以及不想为了看个画面就装几百兆客户端的人。下面我把从选型到跑通、从参数计算到踩坑排查的全过程摊开讲代码可以直接抄。1. Colibri 到底在解决什么问题1.1 从共享个屏幕要等三秒说起事情的开头很朴素。我桌面上有一台主力开发机一台放在旁边的旧笔记本用来查文档和看日志还有一块平板用来当第二块参考屏。日常最高频的动作不是远程操作而是把开发机上的某个窗口给旁边那台看一眼。传统做法有三个截图发过去、开远程桌面软件、或者干脆把窗口拖过去。截图的问题在于它是静态的日志在滚、进度条在动截一张图等于什么都没说远程桌面软件的问题在于它默认假设你要控制握手、鉴权、建立会话、协商编码整套流程走完三五秒过去了而且大部分商业方案会优先走公网中转局域网里反而绕远路。我要的东西很简单一条从 A 机器屏幕到 B 机器浏览器的单向通道尽量少的中间环节打开网页就能看到延迟低到能看清光标移动。这就是 Colibri 的产品边界。它只做单向画面推送不做键鼠回传不做文件传输不做多用户会话管理。砍掉这些之后整个系统可以压缩到几百行代码启动时间从秒级降到毫秒级而且没有任何需要注册账号的环节。我把这个取舍叫做只保留振翅的那部分——蜂鸟能悬停靠的不是把所有鸟的功能都堆上去而是把振翅这一件事做到极致。1.2 为什么名字叫 Colibri而不是叫 ScreenShare命名这件事看起来无关紧要其实它决定了你后面每一个技术决策的倾向。叫 ScreenShare你的潜意识会往通用、完整、可配置的方向走最后一定会加上分辨率下拉框、码率滑块、多显示器切换、加密开关然后变成一个半成品的小号远程桌面。叫 Colibri你脑子里锚定的是轻和快遇到要不要加这个功能的时候答案天然就是不加。这个心理锚点在实际开发里非常管用。我中途至少有三次想加功能一次是想加剪贴板同步一次是想加多观看端一次是想加录制。每次我都问自己一句蜂鸟会背着一台摄像机飞吗然后就砍掉了。最后留下的功能只有三个选定显示器、调节画质、开始/停止。这三件事覆盖了我 95% 的使用场景。顺带说一句Colibri 这个名字在开源圈里被好几个项目用过有浏览器、有建模工具、有音视频方案。你如果去搜会看到一堆不相干的结果。这不影响什么项目名只对自己的使用场景负责。1.3 明确不做什么比明确做什么更重要把不做什么写下来是这套工具能在一周内跑起来的关键。不做键鼠回传意味着不需要处理输入事件的序列化和时序对齐不做多观看端意味着服务端不需要维护订阅者列表和广播队列一个连接一个采集循环就够了不做公网访问意味着不需要考虑鉴权、TLS、穿透这些能把项目周期拉长三倍的东西。只在局域网里用这是 Colibri 的硬边界。有人会问那我在外面想用怎么办。我的回答是这种需求用一个更重的工具去解决别让 Colibri 变成一个什么都能干但什么都干不好的东西。工具的价值往往来自它的克制而不是它的功能表长度。2. 整体架构与技术选型每一层为什么这么拼2.1 三层结构采集层、传输层、渲染层Colibri 的骨架就三层中间用一条明确的数据流串起来采集层负责把屏幕像素抓下来传输层负责把像素搬到对面渲染层负责把像素画到浏览器上。三层之间只传递一种数据——编码后的帧字节。这个约束很重要它让每一层都可以独立替换。比如采集层从 X11 换成 DXGI传输层完全不用改传输层从 WebSocket 换成 WebRTC渲染层也完全不用改。我在实际开发中就换过两次第一次是把采集从 PIL 的 ImageGrab 换成 mss帧率直接从 8 提到 30第二次是把编码从纯 Python 的 JPEG 换成 numpy 预处理加 Pillow 编码单帧耗时降了大概 40%。如果当初把三层揉在一起写这两次替换都得重写整个项目。分层的另一个好处是排障。延迟高了你可以在每一层的边界打时间戳一眼就能看出时间花在哪。这个习惯我是从做后端服务的时候带过来的屏幕共享这种实时性敏感的东西没有分层计时基本等于盲调。2.2 采集方案横向对比别一上来就选最复杂的采集这块不同平台的路子差别很大。Linux 下最通用的是 X11 的抓屏接口mss 这个库底层走的就是 XGetImage/XShmGetImage 这条路简单、跨发行版、装个包就能用。Windows 下更现代的是 DXGI Desktop Duplication它直接拿到 GPU 合成后的帧效率高还能拿到脏矩形信息但只能在 Windows 8 以上用而且代码量比 mss 大一个数量级。macOS 上老接口是 CGDisplayStream新的是 ScreenCaptureKit后者性能好但要求较新的系统版本。平台方案上手难度帧率上限是否提供脏矩形Linuxmss / XShmGetImage低30-60否需要自己算Linux(Wayland)PipeWire portal高30-60是WindowsDXGI Desktop Duplication中60是macOSScreenCaptureKit中60是跨平台浏览器扩展 getDisplayMedia低30是我第一版选的是 mss理由是它跨平台、API 极简、出错信息清楚。选它的时候我很清楚它拿不到脏矩形、也拿不到硬件加速但第一版的目标是跑通不是跑快。这个顺序特别重要——我见过太多人卡在第一步因为一上来就想选那个理论上最优的方案结果光编译环境就折腾了三天。2.3 传输协议WebSocket 和 WebRTC 各自适合什么场景传输层的选择本质是在简单和低延迟之间做权衡。WebSocket 是 TCP 之上的好处是浏览器原生支持、服务端几十行就能写出来、穿透性好因为在同一个 HTTP 端口体系里局域网直接连就行。坏处也很明显TCP 的可靠性保证意味着丢包要重传队头阻塞会让延迟累积。WebRTC 走 UDP天生为实时媒体设计有拥塞控制、有丢包隐藏、有专门的数据通道延迟表现明显更好。代价是信令流程复杂、服务端要么用成熟的媒体服务器要么自己实现 ICE 那一套代码量翻好几倍。我的实际选择是局域网内、观看端在同一台交换机下、目标是 30fps 的桌面画面WebSocket 完全够用。因为局域网丢包率极低通常是 0TCP 重传几乎不会触发队头阻塞也就无从谈起。真正需要 WebRTC 的场景是链路质量不可控的时候比如跨城市、跨运营商。这个判断帮我省掉了至少两周的开发时间。等你确确实实遇到了 WebSocket 撑不住的场景再换也不迟因为三层结构摆在那儿传输层是最好替换的一层。2.4 编码策略为什么先用 MJPEG再考虑 H.264编码这块我走过一段弯路。一开始想直接上 H.264因为压缩率摆在那儿同画质下码率能低一个数量级。上手之后才发现问题H.264 有帧间依赖P 帧要参考前面的 I 帧一旦某一帧丢了或者到晚了后面一串帧全都画不对得等下一个 I 帧才能恢复。在 WebSocket 这种有序传输里还好但一旦队列开始堆积你会看到画面花屏然后突然清晰然后继续花屏体验比单纯的卡顿还糟。而且 H.264 的软件编码在 1080p 下对 CPU 的占用不低硬件编码又要处理不同平台的接口差异。MJPEG 就笨多了每一帧都是独立完整的 JPEG丢掉一帧下一帧照样能画延迟永远不会累积。压缩率是不如 H.264但桌面内容的特性帮了大忙——大片纯色背景、规则的文字和线条JPEG 在这类内容上的压缩效率相当可观。质量参数 70 的时候一张 1080p 的桌面截图大概 80 到 200KB取决于屏幕上有多少复杂内容。这个数字换算下来30fps 也就 2.4 到 6MB/s千兆局域网轻轻松松。所以 Colibri 的编码策略是先 MJPEG 跑通全部链路把延迟压到能接受的范围等到确实需要降码率或者提帧率的时候再去评估 H.264 硬件编码。这个顺序反过来的话你会在调试花屏问题的时候浪费掉大量时间而那本来是可以避免的。3. 核心模块实现细节与关键参数3.1 帧尺寸与数据量先算清楚再动手动手写采集之前先把数据量算一遍这个动作能帮你排除掉很多不切实际的方案。假设计算的是 1920x1080 的屏幕未压缩的像素格式是 BGRA每个像素 4 字节单帧原始大小1920 × 1080 × 4 8,294,400 字节约 8.29 MB30fps 每秒数据量8.29 × 30 248.8 MB/s换算成带宽约 1.99 Gbps60fps 每秒数据量约 3.98 Gbps这个数字意味着什么意味着哪怕在千兆局域网里未压缩传 60fps 也是不可能的30fps 也占满了整条链路还有余。所以编码不是优化项是必需项。再看编码之后按单帧 150KB 估算30fps 是 4.5MB/s也就是 36Mbps千兆网的利用率不到 4%。这就是为什么 MJPEG 这种压缩率不算高的方案在这个场景下完全够用——瓶颈根本不在带宽上而在 CPU 编码能力和延迟上。如果屏幕是 2560x1440单帧原始数据涨到 14.7MB30fps 是 442MB/s编码之后按 250KB/帧算30fps 是 7.5MB/s仍然在可接受范围。但如果上了 4K单帧原始数据就 33MB 了这时候 JPEG 软件编码会成为明显的瓶颈就必须考虑硬件编码或者降采样。3.2 帧率、画质和 CPU 占用三者的平衡点这三个参数是互相拉扯的。帧率上去了每帧留给编码的时间就少了而 JPEG 编码的耗时基本和像素量成正比跟帧率无关所以帧率越高 CPU 占用越高。画质参数越高压缩后的体积越大CPU 编码耗时也越长。想让三个都好看唯一的路是硬件编码或者降低分辨率。我在主力开发机一台四核八线程的老机器上实测的数据是这样的分辨率画质帧率单帧编码耗时单核 CPU 占用1920x10807030约 12ms约 40%1920x10807060约 12ms约 75%1920x10809030约 22ms约 65%1280x7207030约 6ms约 22%1280x7207060约 6ms约 40%看这张表能得出两个结论。第一1080p 下 60fps 是勉强的单帧 12ms 意味着光编码就要占掉 720ms 的每秒时间加上采集和传输CPU 基本上被吃满了而且留给系统调度其他进程的余量很小。第二降到 720p 之后余量非常充裕60fps 也只用了 40% 的单核。所以我的默认配置是 1080p / 画质 70 / 30fps需要更流畅的场景手动切到 720p / 60fps。注意这里的 CPU 占用是单核的如果编码放在独立线程里多核机器上对整体系统的影响会小很多。但 Python 的 GIL 会让多线程编码收效有限真要多核并行得用多进程。3.3 脏矩形把带宽省下来的第一手段桌面画面有个非常明显的特征大部分时间大部分区域是不动的。你在打字变化的可能只有光标附近那一小块你在看文档整屏可能十几秒都不变。如果老老实实每帧都传完整画面等于 99% 的带宽浪费在了没变的地方。脏矩形的思路很直接把屏幕切成固定大小的块每帧只传发生变化的块。块的粒度我选的是 64x64。1080p 下的块数量是 1920/64 30 列1080/64 ≈ 17 行一共 510 块。每帧对这 510 个块算一次哈希跟上一帧对比找出变化的块。510 次哈希听起来多实际上非常快。用 xxHash32 这类非加密哈希1080p 一帧的哈希计算大概 2 到 4ms比 JPEG 编码的 12ms 便宜多了。也可以用更粗暴的办法把每块像素做一次采样比如每 8 个像素取一个再做比较速度更快但可能漏掉细微变化。我选的是哈希因为它不会漏。实际效果很惊人。纯文字工作场景下变化块通常只有 5 到 20 个也就是原来数据的 1% 到 4%。带宽从 4.5MB/s 直接降到 200KB/s 以内。代价是前端要维护一张块画布收到块之后只更新对应区域而不是整屏重绘。这部分前端的复杂度上去了但换来的是带宽和 CPU 的双重下降非常划算。3.4 背压处理延迟不累积的核心机制这一节是我认为整个项目里最重要的一节。屏幕共享最糟糕的体验不是卡而是延迟越积越高——你看到画面里鼠标在动但那是三秒前的鼠标。出现这个现象的原因几乎总是同一个生产帧的速度超过了消费帧的速度队列没有上限帧在队列里排长队。假设服务端每秒采集 60 帧客户端每秒只能解码显示 30 帧多出来的 30 帧如果排队等候一秒之后队列就积了 30 帧两秒之后积了 60 帧。你看到的画面会永远落后两秒而且会持续恶化。解决方式有两种。第一种是丢帧队列长度超过阈值就丢掉。第二种更激进——只保留最新帧队列长度为 1。我选的是第二种因为屏幕共享的语义决定了旧帧没有任何价值。三秒前的画面哪怕是完整的也不如当前这一刻的最新画面有价值。具体实现上服务端维护一个长度为 1 的缓冲区新帧来了直接覆盖旧帧覆盖即丢弃。客户端也做同样的处理收到帧之后先放进 pending 变量如果正在绘制pending 被新帧覆盖绘制完成后再取 pending。这样无论中间哪个环节慢下来延迟都不会超过一帧的时间也就是最多 33ms。这个机制我称之为最新帧优先它让 Colibri 在网络抖动或者 CPU 抖动的时候依然能保持看起来是实时的代价只是丢掉了中间的若干帧而丢帧在视觉上表现为轻微的不连贯比延迟累积好接受得多。4. 从零跑通完整实操步骤4.1 环境准备与依赖安装先说环境。我用的是一台 Ubuntu 22.04 的开发机Python 3.10。需要装的包不多pip install mss pillow websockets numpy四个包的职责分别是mss 负责抓屏pillow 负责 JPEG 编码websockets 负责 WebSocket 服务端numpy 负责像素格式转换。如果是在 Windows 上跑mss 一样能用只是底层走的接口不同pillow 和 websockets 都是纯 Python 友好的装起来没有额外麻烦。关于 Python 版本我建议 3.9 以上。原因在于 websockets 库在 3.9 之后对异步接口做了一些调整老版本里serve和serve_forever的用法有区别跟着老教程走容易踩坑。另外一个建议是给这个项目单独建一个虚拟环境因为 mss 和 pillow 都是需要编译或者有平台特定的二进制包混在系统环境里升级的时候容易出问题。python3 -m venv venv source venv/bin/activate pip install -r requirements.txt4.2 服务端最小可运行版本第一版服务端我只写了四十行目标是能跑起来看到画面不管性能。import asyncio import io import time import mss from PIL import Image import websockets QUALITY 70 TARGET_FPS 30 async def push(ws): sct mss.mss() monitor sct.monitors[1] interval 1.0 / TARGET_FPS while True: start time.perf_counter() shot sct.grab(monitor) img Image.frombytes(RGB, shot.size, shot.rgb) buf io.BytesIO() img.save(buf, formatJPEG, qualityQUALITY, optimizeFalse) payload buf.getvalue() await ws.send(payload) elapsed time.perf_counter() - start await asyncio.sleep(max(0.0, interval - elapsed)) async def main(): async with websockets.serve(push, 0.0.0.0, 8765, max_sizeNone): await asyncio.Future() if __name__ __main__: asyncio.run(main())有几个细节值得单独说。sct.monitors[1]指的是主显示器monitors[0]是所有显示器拼起来的虚拟大屏用哪个取决于你的需求。shot.rgb这个属性会自动把 BGRA 转成 RGB方便但多了一次内存拷贝追求极致性能的话可以直接用shot.bgra加 numpy 切片省掉这次拷贝。optimizeFalse是刻意关掉的因为 Pillow 的 optimize 会尝试多种霍夫曼表来减小体积代价是编码耗时明显上升在实时场景下非常不划算。max_sizeNone是必须的否则 websockets 库会对单条消息大小做限制稍微复杂一点的画面就会直接断连。这套代码在我那台机器上跑 1080p 的实测是 25 到 30fps单帧端到端大约 45ms。4.3 前端接入与渲染时机控制前端第一版更短!DOCTYPE html html head meta charsetutf-8 titleColibri/title style html, body { margin: 0; background: #111; height: 100%; } canvas { display: block; width: 100%; height: auto; } /style /head body canvas idscreen/canvas script const canvas document.getElementById(screen); const ctx canvas.getContext(2d); const ws new WebSocket(ws://127.0.0.1:8765); ws.binaryType blob; let pending null; let drawing false; ws.onmessage (event) { pending event.data; if (drawing) return; drawing true; drain(); }; async function drain() { while (pending) { const blob pending; pending null; const bitmap await createImageBitmap(blob); if (canvas.width ! bitmap.width) { canvas.width bitmap.width; canvas.height bitmap.height; } ctx.drawImage(bitmap, 0, 0); bitmap.close(); } drawing false; } /script /body /htmlpending加drawing这个组合就是前面说的背压处理在前端的落地。消息来了先塞进 pending如果正在绘制就立刻返回pending 被后来的消息覆盖掉绘制循环每次只取当前的 pending取完就清空然后接着循环。这样无论消息来得多快同一时刻最多只有一帧在等待绘制。用createImageBitmap而不是直接给img.src赋值原因有两个。第一img.src URL.createObjectURL(blob)每帧都要创建并销毁一个对象 URL垃圾回收压力大跑个几分钟就能看到内存曲线锯齿状上蹿下跳。第二createImageBitmap返回的是一个可以精确控制生命周期的对象用完调close()显式释放内存占用平稳得多。这个改动看起来小实际对长时间运行的体验影响很大——改之前跑十分钟就开始掉帧改之后连续跑几个小时都很平稳。4.4 让服务端随系统启动Colibri 的使用场景决定了它应该是开机就在那儿打开网页就能用而不是每次手动去终端敲命令。Linux 下用 systemd 用户服务是最省事的[Unit] DescriptionColibri screen push service Aftergraphical-session.target [Service] Typesimple WorkingDirectory/home/you/colibri ExecStart/home/you/colibri/venv/bin/python server.py Restarton-failure RestartSec3 [Install] WantedBydefault.target放到~/.config/systemd/user/colibri.service然后systemctl --user enable --now colibri。这里有个坑如果服务在图形会话建立之前就启动抓屏会失败因为 X11 的显示变量还没设置好。Aftergraphical-session.target就是为了解决这个次序问题但如果你的桌面环境不提供这个 target可能得改成显式设置DISPLAY:0环境变量。我在自己的机器上两种情况都遇到过最后是加了一个启动时的重试循环抓屏失败就等两秒重试比死磕 systemd 的依赖关系省事得多。5. 常见问题排查实录5.1 画面延迟越积越高最后差了好几秒这是我遇到过的第一个、也是最典型的问题。症状是刚连上的时候画面很跟手跑了两三分钟之后延迟越来越大最后鼠标动一下要等三四秒才能看到反应。排查思路很固定在服务端的发送处和客户端的接收处各打一个时间戳比对两者的差值如果差值持续增长就是队列堆积。根因几乎必然是队列没有上限。我第一版用的是asyncio.Queue()默认是无限队列采集端使劲往里塞发送端慢慢往外取差值就这么越拉越大。改成asyncio.Queue(maxsize1)并且在put之前先尝试get_nowait清空问题立刻消失。客户端的pending机制是同一件事的另一半。这两个地方必须同时改只改一边的话延迟还是会在另一边堆积。提示判断延迟有没有累积有个不需要写代码的办法。在屏幕上放一个显示毫秒级时间的时钟观看端和源端各拍一张照片对比。差值稳定就是正常的差值越来越大就是队列积了。5.2 颜色错乱、画面偏色或者整体发白颜色问题的根源永远是通道顺序。屏幕采集拿到的原始数据通常是 BGRA 顺序蓝、绿、红、透明度而 JPEG 编码器期望的是 RGB红、绿、蓝。这两个顺序搞混红色和蓝色就会互换画面上人脸会变成蓝色天空会变成橙色非常明显。mss 的shot.rgb属性会自动做这个转换所以用它的路径一般不会出问题。但如果为了性能改用shot.bgra加 numpy 手动转换就很容易在切片索引上写错import numpy as np # BGRA - RGB去掉 alpha 通道并把 B 和 R 调换 arr np.frombuffer(shot.bgra, dtypenp.uint8).reshape(shot.height, shot.width, 4) rgb arr[:, :, [2, 1, 0]] img Image.fromarray(rgb, modeRGB)另一个容易忽略的点是Image.fromarray的返回对象可能是只读的后续如果想在上面做任何原地操作会直接报错。用rgb.copy()或者np.ascontiguousarray处理一下更保险。整体发白通常是 alpha 通道被当成颜色通道参与了编码检查一下 reshape 的维度是否包含第 4 通道。5.3 多显示器和高 DPI 缩放的坑多显示器第一坑mss.monitors返回的列表里索引 0 是所有显示器的合并区域从索引 1 开始才是各个独立的显示器。如果你以为索引 0 是主屏抓出来的会是一个横跨所有显示器的超宽画面而且坐标原点是所有显示器的左上角可能包含负数区域。这个设计反直觉但文档里有说明。高 DPI 缩放是第二个坑Windows 上尤其明显。系统缩放设成 125% 时抓屏拿到的是物理像素比如 2400x1350但浏览器里 CSS 像素是逻辑像素1920x1080。你把 2400 宽的 canvas 塞进一个 1920 宽的容器画面会被拉伸文字发虚。解决办法是在 canvas 上分别设置绘图缓冲尺寸和 CSS 显示尺寸canvas.width bitmap.width设的是缓冲尺寸canvas.style.width 100%设的是显示尺寸两个都设对浏览器就会自己处理好缩放。如果想让画面更锐利可以把容器的 CSS 尺寸设成物理像素的整数分之一避免非整数缩放带来的插值模糊。5.4 常见问题速查表现象最可能的原因快速验证方式处理办法延迟持续增长帧队列无上限打印队列长度队列长度设为 1覆盖旧帧画面全黑抓屏权限或显示变量单独跑抓屏脚本检查 DISPLAY 或换成门户接口红蓝互换像素通道顺序错误对比源端截图BGRA 转 RGB 时交换第 0 和第 2 通道帧率卡在个位数编码耗时超过帧间隔单帧计时降分辨率、降画质、关掉 optimize跑久了内存上涨对象 URL 未释放看浏览器内存曲线改用 createImageBitmap 并 close画面撕裂绘制与显示不同步快速拖动窗口观察用 requestAnimationFrame 对齐刷新连接立刻断开单条消息超限看服务端日志设置 max_sizeNone字迹模糊缓冲尺寸与显示尺寸不匹配看 canvas 属性同时设置 width 和 style.width这张表里的每一条都是我实际撞过的其中连接立刻断开那条卡了我最久因为报错信息很不直观看起来像是网络问题实际上是消息大小超限触发了库的保护机制。6. 性能调优和长时间运行的一些体会6.1 把延迟拆开看才知道该优化哪里在动手优化之前先在链路的每个边界打上时间戳。这套数据我从实测里拿到的是这样的采集一帧大约 5 到 12ms取决于屏幕内容复杂度和有没有用共享内存JPEG 编码 1080p 画质 70 大约 10 到 14msWebSocket 发送在局域网里 1 到 3ms浏览器收到之后的解码 3 到 8ms绘制加上浏览器的合成大约 5 到 16ms这里的大头是垂直同步等待。加起来的端到端延迟在 25 到 50ms 之间和我用毫秒时钟对比法实测的结果基本吻合。看这个拆解能发现最大的两块是编码和绘制各占三分之一左右。编码这块的优化路径很明确上多进程绕开 GIL、上硬件编码、或者降分辨率。绘制这块的优化空间在于别让浏览器等垂直同步。用requestAnimationFrame把绘制对齐到刷新周期能把抖动压下去但总延迟的下限还是由显示器刷新率决定60Hz 的屏幕理论下限就是 16.7ms。如果你对延迟极其敏感把显示器刷到 120Hz这块直接砍一半。采集这块也有一个容易忽略的优化点mss 在 Linux 下默认走 XGetImage每次抓屏都要把像素从服务端拷贝到客户端。如果 X 服务和程序在同一台机器上可以用共享内存XShm省掉这次拷贝mss 在某些版本里会自动检测但不确定的时候可以手动指定。这个改动在 1080p 下能省 3 到 5ms。6.2 几个投入产出比很高的优化第一个把 JPEG 的编码放在独立进程里。Python 的多线程受 GIL 限制编码这种纯 CPU 密集的操作放线程里几乎没收益放进程里才能真并行。用multiprocessing加一个进程池把采集到的原始像素丢进池子编码完再通过管道传回来整体帧率能提 30% 到 50%。代价是像素数据要跨进程传输1080p 一帧 8MB跨进程拷贝本身也要几毫秒所以这个方案更适合 4K 或者高帧率场景1080p 30fps 的收益不算明显。第二个根据内容自适应调整画质。屏幕内容差异很大看代码的时候是白底黑字压缩率天然很高看视频或者玩游戏的时候画面复杂同样的画质参数下体积会翻好几倍。可以简单统计一下上一帧的压缩后大小如果超过阈值就把这一帧的画质调低一档反之调高一档。这个自适应逻辑二十行代码就能写完效果比固定画质参数好不少尤其在帧率忽高忽低的场景下。第三个也是我最后悔没有早点做的给前端加一个暂停按钮。要求很低就是停止接收和绘制新帧。听起来鸡肋但实际用起来非常高频。我在写代码的时候经常需要盯着某个画面看很久这时候推送还在继续CPU 白白浪费风扇呼呼转。加了暂停之后服务端检测到客户端暂停就自动把帧率降到 1fps 的心跳CPU 占用几乎归零点继续之后一秒内恢复到满帧。这个功能的实现成本不到半小时但它带来的日常体验提升比前面所有优化加起来都大。最后分享一个我在调试时反复用到的技巧在画面上叠加一个帧序号和时间戳的水印直接画在服务端编码之前的图像上。这样任何时候截一张图你都能立刻知道它是第几帧、什么时候产生的延迟是多少有没有跳帧。这个信息在排查疑难问题的时候特别有用比翻日志快多了。我现在这个水印默认是关的但调试开关一直留着。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。