
折腾直播这两年最让我抓狂的事不是网络卡顿也不是内容翻车而是每次开播前要在 OBS、音频控制台、多功能采集卡工具、监控面板之间来回切换四个窗口叠在一起鼠标找半天。尤其用多机位做访谈类节目时切完画面要立刻调整麦克风音量还要盯一眼 RTMP 推流状态手忙脚乱是常态。后来我花了两周时间基于 OBS 的 WebSocket 协议做了一套本地 Web 控制面板把它命名为openrig现在所有的开播、切景、音量微调、画面预览都集中在一个触屏页面上实测用了几个月稳定性和效率都明显提升。这篇博文会把 openrig 的定位、搭建过程、核心代码、踩坑记录完整分享出来适合那些正在做直播间、录播间或推流项目并且想用一套自定义控制台来替代鼠标多点操作的朋友。1. openrig 到底是什么一个「直播驾驶舱」的定位拆解很多人听到「控制台」第一反应是监控大屏或者那种几百个按钮的播控台。但 openrig 的思路完全不一样它更像是飞机驾驶舱把最需要高频操作的那几个按钮和仪表放在触手可及的地方而不是把所有信息都堆在一个页面上。1.1 直播控制链路里的「高频操作」和「低频操作」在做 openrig 之前我先把一场标准直播的所有操作列了一个清单然后按使用频率和误操作成本分类高频操作切换画面节目标题、嘉宾机位、主持人特写、插播视频、麦克风静音/音量微调、开始推流/停止推流、延迟 30 秒的开关状态确认低频但关键操作设置推流分辨率、切换直播服务器、重置网络统计、采集卡信号重新探测日常维护操作升级 OBS、调整滤镜参数、修改音频混合器路由openrig 只负责第一类高频操作第二类放在二级菜单里第三类一律不碰。这个边界非常重要因为控制面板一旦开始承担「所有功能」就会变得跟 OBS 本身一样复杂失去了自制控制台的意义。我的原则是只用 openrig 做那些开播后你还要用手去点的操作。1.2 三个设计原则本地优先、Web 操控、模块化确定边界之后openrig 的设计围绕三个原则展开本地优先整个服务跑在直播间内的同一台主机或局域网服务器上不依赖公网所有控制指令走局域网延迟基本可以忽略。即使路由器断外网面板本地照样能切景只是推流会报错。Web 操控前端做成标准 Web 页面任何能开浏览器、能连 Wi-Fi 的设备都能当控制台。我用过带触屏的平板、老安卓手机甚至一台十几寸的触摸显示器不需要装任何客户端。模块化后端把各个控制能力拆成独立模块比如 OBS 控制模块、音频仪表模块、推流状态模块、前置检查模块每个模块可以单独启停。这样如果某天我不想用音频仪表了直接不加载那个插件就行不会影响到其他功能。1.3 技术选型思路为什么是 OBS 而不是 FFmpeg 直接推流很多做自动化推流的朋友会直接写 FFmpeg 命令把摄像头和音频合成推流。这确实是一种更底层的方案但有一个致命问题FFmpeg 切场景非常麻烦要维护复杂的 filter 图临时插入一个视频片段或者切到另一路摄像头脚本会很别扭。而OBS Studio 本身已经解决了大部分直播合成需求包括场景布局、滤镜、音频混音、转场。它暴露出来的 WebSocket 接口就像汽车的方向盘和油门我们只需要做一套自定义仪表盘而不是重新造发动机。所以我选择用 OBS 作为渲染引擎用 obs-websocket 作为控制通道openrig 只是遥控器。这个定位让我的开发量减少了一大半也保证所有视频处理仍然由 OBS 这个成熟工具完成。2. 从零搭建 openrig硬件准备与最小环境如果你也想搭一套 openrig不需要买特别贵的东西。我用的是一台淘汰下来的 Mini PC系统装了 Ubuntu Server跑 Docker平时只开 OBS 和 openrig 服务。直播主机资源占用维持在 30% 左右非常稳。2.1 硬件清单和连接逻辑这里给出我实测过的硬件配置你可以按实际预算调整硬件数量说明Mini PC / 主流台式机1建议 16GB 内存OBS 在渲染 1080p 多场景时内存占用 1.5-2GB带 HDMI 输入的采集卡1-4用于接入摄像机信号推荐免驱 UVC 方案的采集卡容易被 OBS 识别麦克风/音频接口1如果不依赖内置声卡推荐外置 USB 音频接口回采和控制更干净触控显示器或平板1平板通过浏览器访问 openrig 页面建议 10 寸以上按钮大概 60-80px 才不容易误触千兆交换机/路由器1保证局域网内低延迟5G Wi-Fi 勉强够用但千兆有线更稳连接逻辑不复杂摄像头 HDMI 线进采集卡采集卡插 USB麦克风进声卡。OBS 负责把采集画面和音频混合成场景。openrig 服务跑在同一台或另一台机器上通过局域网访问它提供的页面。2.2 软件版本清单我踩过版本坑所以直接把测试通过的组合写在这里OBS Studio 28.0 以上内置 obs-websocket 5.x不再需要单独安装插件Python 3.10 以上后端 API 服务Node.js 16前端构建工具链如果你只想要静态页面也可以不装obs-websocket-py 1.4.0 以上Python 的 WebSocket 客户端库注意OBS 28 之前使用的是 obs-websocket 4.x协议和 5.x 不兼容。如果你是从老版本升级上来的务必检查 OBS 设置里的 WebSocket 服务器版本。2.3 初始化 OBS 端的 WebSocket 服务打开 OBS进入「工具」-「WebSocket 服务器设置」勾选「启用 WebSocket 服务器」端口保持默认 4455设置一个强密码密码会用在后续 Python 和前端连接然后为了安全建议将服务器绑定地址改为127.0.0.1也就是只允许本机连接。但 openrig 后端服务如果跑在另一台机器上则需要改成0.0.0.0并在防火墙里只放行局域网段。我自己的部署是 OBS 和 openrig 后端装在同一台机器上所以绑定 127.0.0.1 就够了。3. 把 OBS 变成「可编程的推流机」核心 API 与场景切换实现obs-websocket 提供了一整套协议覆盖场景、采集源、输出、转场、音频、媒体播放等操作。openrig 里最常用的就是场景切换、推流状态获取、音频音量设置、截图预览这四个类别。3.1 为什么不用按键精灵模拟鼠标点击早期我确实想过用按键精灵或 AutoHotkey 模拟点击 OBS 界面后来放弃了。原因有两个模拟点击的坐标会随窗口位置和分辨率变化一旦 OBS 窗口被移动脚本就失效非常脆弱。模拟点击无法实时知道 OBS 内部状态。比如当前是否正在推流、当前场景是哪个这些信息只能从 UI 上去「看颜色」程序里无法稳定判断。而 obs-websocket 是明文协议本质上是结构化请求/响应加上状态事件推送完全是编程友好的。我们用 Python 调用SetCurrentProgramScene来回的场景切换响应时间稳定在几十毫秒内而且是 OBS 原生支持的不干扰窗口布局。3.2 用 Python 实现第一批控制命令首先安装客户端库pip install obs-websocket-py然后写一个最基础的控制脚本先验证连接from obswebsocket import obsws, requests host 127.0.0.1 port 4455 password your_strong_password ws obsws(host, port, password) ws.connect() scenes ws.call(requests.GetSceneList()).getScenes() print(当前场景列表) for s in scenes: print( -, s[sceneName]) ws.disconnect()跑通之后场景切换就一行ws.call(requests.SetCurrentProgramScene(sceneName主持人特写))开始推流和停止推流# 开始推流 ws.call(requests.StartStream()) # 停止推流 ws.call(requests.StopStream()) # 获取当前推流状态 output_active ws.call(requests.GetStreamStatus()).getActive() print(是否正在推流:, output_active)音频音量调节也是高频操作比如把「嘉宾麦克风」推到 80% 音量ws.call(requests.SetInputVolume(inputName嘉宾麦克风, inputVolumeMul0.8))3.3 openrig 后端如何组织这些命令我在 openrig 里没有直接在前端调用这些脚本而是加了一层 FastAPI 中间服务。前端按钮点击后请求 HTTP 接口后端再转成 obs-websocket 请求。这样做的好处是前端浏览器没法保持 WebSocket 长连接而且如果 OBS 偶尔断连后端可以自动重连前端状态展示不会立刻懵掉。后端核心逻辑类似这样from fastapi import FastAPI from pydantic import BaseModel from obswebsocket import obsws, requests app FastAPI() ws None def connect_obs(): global ws ws obsws(127.0.0.1, 4455, your_strong_password) ws.connect() app.on_event(startup) def startup(): connect_obs() class SceneRequest(BaseModel): scene_name: str app.post(/api/scene/switch) def switch_scene(req: SceneRequest): try: ws.call(requests.SetCurrentProgramScene(sceneNamereq.scene_name)) return {ok: True} except Exception as e: return {ok: False, error: str(e)} app.get(/api/stream/status) def stream_status(): res ws.call(requests.GetStreamStatus()) return {active: res.getActive(), delay: res.getDelaySeconds()} app.post(/api/input/volume) def set_volume(input_name: str, volume: float): ws.call(requests.SetInputVolume(inputNameinput_name, inputVolumeMulvolume)) return {ok: True}代码非常薄但足够用。如果需要更完整的事件推送比如 OBS 切景后同步刷新面板按钮状态可以再开一个 WebSocket 路由订阅CurrentProgramSceneChanged事件然后把事件转给前端。务必注意不要把 WebSocket 密码直接写在前端代码里。所有敏感操作都放在后端前端只调用后端接口。即使局域网相对安全这种习惯也不能丢。4. 让操作有反馈音频电平监控与画面预览直播控制台如果只是按钮点击后、「过几分钟看看推流是否正常」那就没有意义了。我需要的是点击切景按钮后眼睛能看到画面是否变化耳朵能通过电平表判断音量是否正常。4.1 实时音频电平从 OBS 订阅到前端展示obs-websocket 5.x 支持订阅事件其中InputVolumeMeters会周期性返回每个输入的音量电平数据。最新协议里这个事件返回的是inputLevelsMul数组每个元素包含inputName和多个通道的电平值。后端订阅示例import asyncio from obswebsocket import obsws, events def on_input_volume_meters(data): inputs data.getInputs() for item in inputs: name item[inputName] levels item[inputLevelsMul][0] # 取第一个通道 print(name, levels) ws.register(events.InputVolumeMeters, on_input_volume_meters)实际部署时我把电平数据经过格式化后通过 WebSocket 推给前端。前端在 Canvas 上画进度条每秒刷新 10 次。这样做的好处是不需要在前端调用任何录音相关 API直接看 OBS 内部混音结果。4.2 画面预览方案对比截图 vs 本地 WebRTC一开始我尝试用 OBS 的虚拟摄像头把视频流转成 WebRTC 再拉到浏览器里优点是流畅、延迟低缺点是需要在系统层面装 v4l2loopbackLinux或虚拟摄像头驱动Windows而且有时会和采集卡驱动冲突。后来我发现 obs-websocket 里有个GetSourceScreenshot方法可以直接把某个来源或当前预览画面截图返回 base64 编码。openrig 的做法是每 1 秒调用一次这个方法把图片显示在面板右侧。截图尺寸我限制为 640 宽压缩成 JPEG在局域网里传输延迟不到 200ms足够判断画面是否卡在某个固定画面。核心代码def get_preview_base64(): res ws.call(requests.GetSourceScreenshot( sourceName预览画面, imageFormatjpeg, width640, height360, )) return res.getImageData() # 形如 data:image/jpeg;base64,...前端拿到这个字符串直接放进img标签即可。如果你有更专业的监控需求再考虑 NDI 或 WebRTC但 openrig 最小可用版本真的不需要那么复杂。4.3 触控屏适配大按钮、防误触和按钮状态反馈用平板访问 openrig 页面时按钮尺寸和反馈是决定性体验。我踩过很多次误触的坑最终总结了几条经验主要按钮尺寸至少width: 90px; height: 56px「开始直播」这种关键按钮单独放大到 120px 以上。每个按钮要有明显的 active 态。比如点击后立即变灰色并显示 loading接口返回失败时再变成红色并弹提示而不能只是「点了没反应」。长按操作需要加防重复触发。我会在按钮点击后增加一个 800ms 的 disable 窗口避免手抖连续切景。无线触控屏有时会飘需要前端做触摸事件的touch-action: manipulation设置同时按钮之间留足够间距。5. 一键开播从硬件通电到推流出去的完整工作流「一键开播」听上去很高端其实就是把一系列动作串联起来。我实现的目标是按下开播按钮后openrig 依次自动完成检测、准备、推流三步每一步都有状态提示。5.1 开播前检查清单自动探测是否存在「隐形故障」直播前最常见的坑是采集卡没识别到信号、麦克风音量静音、网络上传带宽不够。这些指标在 OBS 界面里一眼能看出来但是脚本感知不直观。所以 openrig 在开播前会执行一系列探测调用GetInputList检查所有输入源是否存在特别确认采集卡对应的设备名没有变。调用GetMute检查关键麦克风是否被静音如果静音则自动解除。调用GetVideoSettings获取当前输出分辨率并和预设的期望分辨率对比。用系统命令做一次简单的网络带宽测试比如 iPerf 或 speedtest-cli如果不想依赖外部服务就检查 RTMP 地址所在端口是否可达。所有检查结果会汇总成一个状态列表绿色代表正常红色代表有问题。如果某个红项被标记为「致命错误」开播流程会终止避免推流上去全是黑屏。5.2 串联执行流程切场景、设延迟、开推流检查通过后开播流程按顺序执行把当前场景切到「开场标题」画面。将 OBS 的流延迟设置为 30 秒防止直播中出现不可控画面如果做带货或观众互动不需要延迟可以设为 0。再次确认音频推流电平正常然后调用StartStream。启动一个后台心跳监控每 5 秒检查一次推流状态如果输出断流超过 10 秒则自动切换到大屏故障提示画面并通知主持人口播补救。代码里就是多个ws.call的串联中间加入检查断言。关键代码def startup_stream(): check_preconditions() ws.call(requests.SetCurrentProgramScene(sceneName开场标题)) ws.call(requests.SetStreamSettings(streamSettings{delay: 30})) ws.call(requests.StartStream()) monitor_stream_health()5.3 异常处理推流断了不要慌要自动告诉你推流过程中obs-websocket 会推送StreamStateChanged事件。openrig 后端监听到状态变为Stopped且不是手动停止时会在面板顶部弹出红色横幅同时声音提示。如果主机连接了喇叭可以播放一段提示音如果没有喇叭就用触控屏闪烁提醒。处理逻辑我写得很保守自动重连只尝试两次两次之间间隔 3 秒。如果超过两次仍然失败就停止自动重连等待人工干预。因为很多断流原因是局域网内 OBS 与网络设备之间的隐性冲突盲目的高频重连反而会把情况搞得更糟。6. 实测几个月的坑与解决思路给同路人排雷openrig 版本迭代到现在我整理了三个最典型的坑每个都是血泪教训。6.1 升级 OBS 后 WebSocket 全断版本不兼容的惨案有一次我手贱把 OBS 从 27 升到 28结果 openrig 后端突然全部报错查了半天发现是 obs-websocket 从 4.x 变成了内置的 5.x事件和请求的名字都变了。比如GetSceneList虽然还在但SetCurrentScene改成了SetCurrentProgramScene事件类SwitchScenes改成了CurrentProgramSceneChanged。解决思路很简单全部接口切换到 5.x 命名同时把连接库升级到obs-websocket-py1.4.0。如果你也在自己写控制台升级前一定先读官方协议文档的「Breaking Changes」。6.2 触控屏按钮「飘」无线平板的延迟与抖动我最初用的是普通 Wi-Fi 平板访问面板按按钮经常出现点击位置偏移。排查后发现问题不在网络而在浏览器默认的 300ms 点击延迟和双指缩放手势。解决方法是给按钮加touch-action: none并且启用FastClick类库消除延迟。另外如果用的是 Windows 触屏一体机记得关闭「长按右键」功能不然手按在按钮上稍久一点就弹右键菜单。6.3 采集卡信号时有时无设备枚举顺序不稳定直播机器的 USB 设备枚举顺序偶尔会变导致 OBS 里采集卡源显示「无信号」。openrig 的启动检查里我用了一种简单粗暴但有效的方式在 OBS 场景里预设两个同样的采集源分别指向不同 USB 端口如果一个源没信号开播检查会自动切到另一个源并且给出提示。这虽然算不上优雅但在现场直播时真的能救命。另一个小技巧是USB 采集卡不要插在 USB 3.0 延长线或集线器上尽量直插主板接口。很多奇怪的无信号都是供电不足引起的。6.4 音量条跳动与真实听感不一致用 API 数值而不是主观感受最后提醒一个容易忽视的细节obs-websocket 返回的电平值是数字范围不代表人耳感知的响度。真正的响度需要经过 RMS 或 LUFS 转换。我在 openrig 电平条上做了一组简单映射-60dB到-3dB之间线性映射到 0-100%低于-60dB直接显示 0。这样比直接拿原始浮点值显示要直观不少。如果你的直播内容包含语音聊天建议把语音通道在 OBS 里单独走一条总线openrig 上再单独画一条「语音响度」监控避免和背景音乐混在一起看不清楚。openrig 从最初的「一键切景」走到现在的「直播驾驶舱」本质上是我对直播工作流的重新思考。它没有用任何黑科技只是把 OBS 已经提供的接口用正确的方式暴露给了一个更顺手的界面。我个人在使用了几个月后最大的感受是直播前的紧张感被大幅消解了——因为知道所有关键操作都会按顺序执行所有异常都会被提前探测。如果你也在管理直播间不妨从最基础的单场景切控制开始试着用 WebSocket 把你的直播流程串起来你会发现原来手忙脚乱的日子真的可以结束。最后一个小技巧面板上的每一个按钮都值得用一个真实的按键音来确认那种「嗒」的一下会让你的操作信心提升不少。