基于CircuitPython与Fruit Jam打造低成本实时直播图文叠加系统
1. 项目概述:打造你的专属直播图文叠加器
如果你做过直播,尤其是电商带货或者知识分享,肯定遇到过这样的场景:需要实时在屏幕上展示商品价格、库存,或者演讲者的姓名、头衔。这种出现在屏幕下方三分之一区域、实时更新的图文信息,在行业里有个专业术语叫“Chyron”或“Lower Third”(下三分之一)。传统上,这需要昂贵的专业广播设备。但现在,借助像Adafruit Fruit Jam这样的微控制器和CircuitPython,我们完全可以用极低的成本,自己动手搭建一个功能强大、高度可定制的实时图文叠加系统。
这个项目的核心思路非常清晰:让一个硬件小设备独立工作,从指定的网络API(比如你的电商后台、天气接口、股票行情)抓取实时数据,经过处理和美化后,通过HDMI接口输出纯净的图文画面。然后,利用OBS这类直播软件的“色键”功能,将这个图文画面里的纯色背景(比如蓝色)抠掉,只留下文字,再叠加到你的摄像头画面上。这样一来,你的直播画面就拥有了专业级的动态信息展示能力。
我之所以对这个方案情有独钟,是因为它将复杂的功能模块化、简单化了。数据处理和图文渲染由Fruit Jam硬件独立完成,稳定可靠;视频合成则由成熟的桌面软件OBS处理,效果专业。两者通过一根HDMI线连接,职责清晰,互不干扰。无论是个人主播还是小型工作室,都能快速部署,并且可以根据自己的数据源和显示样式进行深度定制,灵活性远超许多现成的软件方案。
2. 核心硬件与工具选型解析
工欲善其事,必先利其器。要复现这个项目,你需要准备几样核心硬件和软件。别担心,清单不复杂,大部分都是即插即用的标准件。
2.1 核心控制器:Adafruit Fruit Jam
项目的“大脑”是Adafruit Fruit Jam。我选择它,而不是其他常见的微控制器(如ESP32或树莓派Pico),主要基于几个关键考量:
- 原生DVI/HDMI视频输出:Fruit Jam的核心芯片RP2350内置了HSTX(高速发送器)外设,可以直接驱动DVI/HDMI显示器。这意味着你不需要额外的视频转换板或复杂的接线,一根HDMI线就能输出图像,极大地简化了硬件连接和软件驱动层面的工作。
- 强大的网络能力:板载ESP32-C6协处理器专门负责Wi-Fi连接。这种双核架构(RP2350处理图形和逻辑,ESP32-C6处理网络)让系统响应更流畅,网络请求不会阻塞图形渲染,保证了图文更新的实时性。
- CircuitPython原生支持:Adafruit对CircuitPython的生态支持非常完善。Fruit Jam拥有专属的
adafruit_fruitjam库,将网络请求、JSON解析、图形渲染等复杂操作封装成简单的API,让我们可以像搭积木一样快速构建应用,而无需从零开始编写底层驱动。
注意:购买时请确认你拿到的是“Fruit Jam - Mini RP2350”版本。Adafruit产品线中可能有其他名称相似的板子,务必核对产品编号(如#6200)。用错板子会导致代码无法运行。
2.2 视频采集与连接线缆
硬件部分另外两个关键是视频采集卡和线缆。
- HDMI转USB视频采集卡:这是连接硬件和电脑的桥梁。Fruit Jam输出的是HDMI信号,而你的电脑(运行OBS)需要将其识别为一个视频输入设备。我推荐使用Adafruit的#4669这类即插即用的采集卡。它的优点是免驱动(在主流操作系统上通常能被识别为UVC设备)、延迟低,并且画质足以清晰呈现文字图形。
- HDMI线缆与USB数据线:一根短而优质的HDMI线(如1.5英尺的#2420)非常必要,可以避免桌面线材杂乱。同时,你需要一根USB-C数据线(如#6324)为Fruit Jam供电并传输程序。务必使用支持数据传输的USB线,很多手机充电线只能供电,无法识别设备,会让你在调试时一头雾水。
2.3 软件环境准备
软件方面需要准备两样东西:
- CircuitPython固件:这是运行在Fruit Jam上的操作系统。你需要去CircuitPython官网,根据Fruit Jam的具体型号下载最新的
.uf2固件文件。我强烈建议使用10.x或更高版本,以确保对最新库和功能的兼容性。 - 代码编辑器:任何能编辑文本文件的工具都可以,比如VS Code、Thonny,甚至系统自带的记事本。因为CircuitPython的开发模式就是直接编辑存储设备(CIRCUITPY盘符)上的Python文件,非常直观。
3. 系统搭建与CircuitPython部署
拿到硬件后,第一步是让Fruit Jam“活”起来,运行我们自己的代码。
3.1 刷入CircuitPython固件
这个过程被称为“进入引导加载程序模式”(Bootloader Mode)。
- 连接设备:用USB数据线将Fruit Jam连接到电脑。此时它可能处于出厂状态,不会显示为磁盘。
- 进入Bootloader模式:
- 找到板子上的BOOT/BOOTSEL按钮(通常标有“BOOT”)和RESET按钮。
- 操作A(推荐):先按住BOOT按钮不松手,然后快速点按一下RESET按钮,之后继续按住BOOT按钮约1-2秒再松开。
- 操作B:也可以先不插USB线,按住BOOT按钮不放,然后将USB线插入电脑,等待几秒后再松开BOOT按钮。
- 复制固件:如果操作成功,电脑上会出现一个名为
RP2350的可移动磁盘。将之前下载的adafruit-circuitpython-adafruit_fruit_jam_mini_rp2350-xx.x.x.uf2文件直接拖入这个磁盘。 - 完成启动:
RP2350磁盘会消失,稍等片刻,会出现一个新的名为CIRCUITPY的磁盘。这说明CircuitPython系统已经成功刷入并启动了。
3.2 理解CIRCUITPY驱动与安全模式
成功刷入后,CIRCUITPY盘就是你与Fruit Jam交互的主要窗口。你可以像操作U盘一样,在这里创建、编辑Python文件。主程序文件名为code.py,设备上电后会自动运行这个文件。
有时你可能会遇到CIRCUITPY盘变成只读甚至不显示的情况,这通常是因为代码出错导致系统崩溃。这时就需要用到安全模式(Safe Mode)。
- 进入方法:在Fruit Jam启动过程中(上电或按RESET后),系统会有约1秒的等待期。此时板载LED可能会闪烁黄灯。在这1秒内,再次快速按下RESET按钮,即可进入安全模式。你可以把它理解为“在系统加载用户代码前,快速双击RESET”。
- 安全模式的作用:在此模式下,系统不会自动运行
code.py和boot.py,并禁用自动重载。这允许你连接电脑,安全地删除或修改导致问题的代码文件。退出安全模式只需再次按RESET或重新插拔USB。
如果连安全模式都无法进入,或者CIRCUITPY盘彻底无法出现,那就需要动用“终极武器”——Flash重置UF2文件(常被称为“nuke”文件)。将其拖入RP2350盘会彻底清空Flash,让板子恢复至可刷固件的纯净状态,当然,所有文件也会丢失。
4. 项目代码深度剖析与定制
项目的核心逻辑都写在code.py里。我们不仅要会“复制粘贴”,更要理解每一行代码背后的意图,这样才能根据自己的需求进行定制。
4.1 工程包结构与初始化
首先,你需要下载完整的项目工程包(Project Bundle)。这个压缩包里包含了code.py和所有必需的CircuitPython库文件。解压后,将全部内容复制到CIRCUITPY盘的根目录,覆盖原有文件。
让我们从代码开头看起:
import time import board import supervisor import terminalio from adafruit_fruitjam import FruitJam from adafruit_fruitjam.peripherals import request_display_config这里导入了核心模块。adafruit_fruitjam是主角,它封装了所有复杂功能。request_display_config用于初始化视频输出。
BG_COLOR = 0x0000FF # 蓝色背景,用于后续色键抠图 OVERLAY_FONT = terminalio.FONT # 使用系统内置字体 # OVERLAY_FONT = "Free_Mono_10.pcf" # 如需自定义字体,取消注释并指定字体文件路径BG_COLOR设置为蓝色(RGB: 0x0000FF),这是为了在OBS中使用“蓝屏抠像”(Color Key)。选择蓝色是因为它与人肤色和常见场景反差大,抠像效果干净。你也可以换成绿色(0x00FF00),但需要同步修改OBS中的Key Color。
OVERLAY_FONT默认使用CircuitPython内置的等宽字体,清晰度足够。如果你需要更艺术化的字体,可以将.pcf格式的字体文件放在CIRCUITPY盘,并修改这行代码指向你的字体文件。
4.2 显示配置与数据源定义
request_display_config(320, 240) display = supervisor.runtime.displayrequest_display_config(320, 240)是魔法开始的地方。它告诉RP2350的底层驱动,将HSTX外设配置为输出320x240分辨率的DVI信号。这个分辨率对于图文叠加来说足够清晰且性能开销小。获取到的display对象是后续所有图形操作的基础。
DATA_SOURCE = "https://www.adafruit.com/api/products/6358" TITLE_LOCATION = ["product_name"] SALE_PRICE_LOCATION = ["product_sale_price"] REG_PRICE_LOCATION = ["product_price"] STOCK_LOCATION = ["product_stock"]这是项目的数据驱动核心。DATA_SOURCE指向一个返回JSON数据的API地址。示例中使用的是Adafruit的产品API。你需要将其替换成你自己的数据源,比如:
- 一个简单的服务器端脚本,返回
{"product_name":"我的商品", "price":99, "stock":42} - 一个公开的天气API:
https://api.weather.com/... - 一个股票行情接口。
TITLE_LOCATION等变量是JSON路径,告诉库从返回的JSON对象中哪个字段提取数据。示例中是直接的一级字段名。如果你的API返回嵌套数据,例如{"data": {"item": {"name": "xxx"}}},则可以写成["data", "item", "name"]。
4.3 视觉布局预设与动态切换
TEXT_POSITIONS这个列表定义了五种不同的图文布局预设,这是实现“一键换肤”的关键。
TEXT_POSITIONS = [ { "title_anchor_point": (0, 0), "title_anchored_position": (4, 205 + 4), "stock_anchor_point": (1.0, 1.0), "stock_anchored_position": (display.width - 4, display.height - 4), ... "outline_size": 1, "outline_color": 0x000000, }, # ... 其他四种预设 ]理解两个关键概念:
anchor_point(锚点):定义了文本的对齐基准点。(0, 0)代表文本的左上角,(1.0, 1.0)代表右下角,(0.5, 0.5)代表中心。它决定了文本的“哪一点”去对齐坐标位置。anchored_position(锚定位置):定义了文本在屏幕上的坐标位置(以像素为单位)。
例如,第一个预设中,标题的anchor_point是(0,0)(左上角对齐),anchored_position是(4, 209)(距离左边缘4像素,距离上边缘209像素)。由于屏幕高240像素,这实际上把标题放在了靠近底部的位置,构成了经典的下三分之一布局。而库存信息的锚点是右下角(1.0, 1.0),位置在(屏幕宽-4, 屏幕高-4),即紧贴屏幕右下角。
outline_size和outline_color为文字添加描边。在复杂背景上,白色文字加黑色描边能极大提升可读性。你可以根据背景色调整描边颜色。
apply_hotkey_visuals(index)函数负责将选中的预设配置应用到当前的文本标签对象上。当按下按钮切换布局时,就是调用这个函数来更新所有文本的位置和样式。
4.4 数据格式化与FruitJam库初始化
从API获取的原始数据(如价格19.95)通常需要美化后才能显示。format_data函数就是干这个的:
def format_data(json_data): if "product_sale_price" in json_data: json_data["product_sale_price"] = f'${json_data["product_sale_price"]}' else: json_data["product_sale_price"] = f'${json_data["product_price"]}' json_data["product_stock"] = f'Stock: {json_data["product_stock"]}' return json_data它检查是否有促销价,有则用促销价,没有则用原价,并加上美元符号。库存数据前加上“Stock: ”前缀。你可以在这里发挥创意,添加单位、格式化日期、将数字转为中文大写等。
接下来是初始化FruitJam库,这是连接所有功能的枢纽:
fruitjam = FruitJam( url=DATA_SOURCE, json_path=(TITLE_LOCATION, STOCK_LOCATION, SALE_PRICE_LOCATION), status_neopixel=board.NEOPIXEL, default_bg=BG_COLOR, json_transform=[format_data], debug=True, )url和json_path:指定了数据从哪里来以及如何提取。status_neopixel:指定板载NeoPixel LED用于显示网络状态(如连接中、获取数据中、错误等),非常直观。default_bg:设置整个图层的背景色,即我们之前定义的蓝色。json_transform:传入我们自定义的format_data函数,在数据显示前进行美化。debug=True:开启调试模式,会在串口控制台打印更多信息,便于排查问题。
4.5 文本字段配置与主循环逻辑
库初始化后,需要创建具体的文本显示区域:
fruitjam.remove_all_text() fruitjam.add_text(text_wrap=35, text_maxlen=180, text_color=0xFFFFFF, outline_size=1) # 标题 fruitjam.add_text(text_wrap=0, text_maxlen=30, text_color=0xFFFFFF, outline_size=1) # 库存 fruitjam.add_text(text_wrap=0, text_maxlen=30, text_color=0xFFFFFF, outline_size=1) # 价格这里按顺序添加了三个文本字段,分别对应标题、库存和价格。
text_wrap=35:标题字段启用自动换行,每行最多35个字符,适合较长的商品名。text_maxlen:限制字段最大字符数,防止超长文本破坏布局。- 后两个字段
text_wrap=0表示不换行。
代码的主循环做了两件重要的事:
- 按钮检测与布局切换:不断检测Fruit Jam上的按钮1(
fruitjam.button1)是否被按下。如果检测到按下动作(通过对比当前状态和之前状态实现“边沿检测”,防抖),就切换config_index,并调用apply_hotkey_visuals更新布局。 - 定时数据刷新:通过
time.monotonic()记录上次获取数据的时间,每隔FETCH_DELAY(12秒)就调用fruitjam.fetch()从网络获取最新数据并更新显示。网络请求被try...except块包裹,确保了即使网络暂时中断,程序也不会崩溃,而是等待后重试。
5. 网络配置与OBS合成实战
硬件和代码就绪后,最后一步是让整个系统跑起来,并集成到直播流中。
5.1 配置Wi-Fi连接
Fruit Jam需要通过Wi-Fi访问互联网获取数据。配置方式非常“CircuitPython”:在CIRCUITPY盘的根目录下,创建一个名为settings.toml的文本文件。这是CircuitPython标准的配置文件格式。
文件内容如下:
CIRCUITPY_WIFI_SSID = "你的Wi-Fi名称" CIRCUITPY_WIFI_PASSWORD = "你的Wi-Fi密码"保存文件后,重启Fruit Jam(按RESET键)。板载的ESP32-C6协处理器会自动读取这个文件并连接网络。连接成功后,NeoPixel LED会呈现稳定的颜色(非闪烁状态),表明网络已就绪。
实操心得:
settings.toml文件中的密码是明文存储的。如果担心安全问题,可以在首次配置后删除该文件。Wi-Fi凭证会保存在ESP32-C6的非易失存储器中,即使断电也不会丢失,除非你刻意清除它。
5.2 硬件连接与信号流
按照以下顺序连接硬件:
- 使用USB-C线将Fruit Jam连接到电脑或充电器供电。
- 用短HDMI线连接Fruit Jam的DVI-D输出口和视频采集卡的HDMI输入口。
- 将视频采集卡的USB接口插入电脑。
此时,Fruit Jam应该开始启动,连接Wi-Fi,获取数据,并在其HDMI输出上显示蓝底白字的图文信息。你的电脑会将采集卡识别为一个新的摄像头设备,名为“USB Video”或类似。
5.3 OBS设置:色键合成详解
这是实现“叠加”效果的魔法步骤。
添加视频源:
- 在OBS中新建或打开一个场景。
- 首先添加你的主摄像头作为基础画面。
- 点击“来源”面板的“+”号,选择“视频采集设备”。
- 创建一个新源,命名为“FruitJam Chyron”。
- 在“设备”下拉菜单中,选择你的HDMI采集卡。你应该能看到Fruit Jam输出的蓝色背景文字画面。
应用色键滤镜:
- 在“来源”面板中,选中刚刚添加的“FruitJam Chyron”源。
- 右键点击它,选择“滤镜”。
- 在滤镜窗口的“效果滤镜”区域,点击“+”号,添加“色度键”或“色彩键”滤镜(不同OBS版本翻译略有不同)。
- 关键参数设置:
- 关键颜色类型:选择“自定义颜色”。然后点击旁边的色块,用取色器选取Fruit Jam画面中的蓝色背景。或者,因为我们的背景是纯蓝
#0000FF,可以直接将类型设为“蓝色”。 - 相似度:这是最重要的参数。慢慢向右拖动滑块,直到蓝色背景完全消失,只留下清晰的白色文字。如果拉得太多,可能会把文字边缘也抠掉;拉得太少,蓝色背景会有残留。我们的文字边缘是清晰的像素,所以比较容易处理。
- 平滑度:可以稍微增加一点(如5-10%),让抠像边缘更柔和,避免出现锯齿。
- 关键颜色溢出减少:这个参数可以帮助消除文字边缘可能残留的蓝色光晕,可以适当调整。
- 关键颜色类型:选择“自定义颜色”。然后点击旁边的色块,用取色器选取Fruit Jam画面中的蓝色背景。或者,因为我们的背景是纯蓝
调整位置与大小:
- 关闭滤镜窗口,回到主界面。
- 在预览画面上,你可以直接拖动“FruitJam Chyron”源来调整图文信息在屏幕上的位置,通常放在底部(Lower Third区域)。
- 如果需要缩放,可以按住源边缘的拖拽点进行调整,或者右键选择“变换”进行更精确的控制。
现在,你的直播画面就完美地合成了实时更新的图文信息。Fruit Jam会每隔12秒自动刷新数据,你也可以随时按下其上的按钮来切换五种不同的布局样式。
6. 进阶定制与疑难排错指南
掌握了基础搭建后,你可以尝试更多个性化定制,并了解如何解决可能遇到的问题。
6.1 自定义你的数据源与显示内容
项目的强大之处在于其可定制性。以下是一些扩展思路:
更换API:修改
DATA_SOURCE为任何返回JSON的公开或私有API。例如,显示天气:DATA_SOURCE = "https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41¤t_weather=true" # 假设返回 {"current_weather": {"temperature": 15.3, "windspeed": 10.2}} TEMP_LOCATION = ["current_weather", "temperature"] WIND_LOCATION = ["current_weather", "windspeed"]然后在
format_data函数中,将温度值格式化为f'{temp}°C'。增加或减少文本字段:
fruitjam.add_text()调用几次,就创建几个字段。你需要确保json_path参数中的路径元组数量与文本字段数量匹配,并且TEXT_POSITIONS预设字典里为每个字段都配置了对应的锚点和位置。修改刷新频率:调整
FETCH_DELAY变量的值(单位:秒)。对于股票行情可能需要更频繁(如5秒),对于新闻标题则可以更久(如30秒)。注意过于频繁的请求可能被API服务器限制。设计更多布局:在
TEXT_POSITIONS列表里添加新的字典配置。你可以设计将信息放在屏幕顶部、角落,或者采用垂直排列等。
6.2 常见问题与解决方案
在实际操作中,你可能会遇到以下问题:
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| CIRCUITPY盘不出现 | 1. USB线仅供电不支持数据。 2. 板子处于异常状态。 | 1.更换一条确认可传数据的USB线。 2. 尝试进入安全模式(启动时双击RESET)。 3. 仍无效则使用Flash重置UF2文件进行深度清理后重刷固件。 |
| OBS中看不到采集卡画面 | 1. 采集卡驱动问题。 2. 采集卡被其他软件占用。 | 1. 检查系统“设备管理器”中采集卡是否被识别且无感叹号。 2. 关闭可能占用摄像头的其他软件(如Zoom、微信、其他直播软件)。 3. 在OBS中尝试不同的“视频格式”或“分辨率/帧率”选项。 |
| 文字有蓝色残影或背景抠不干净 | 1. OBS色键参数设置不当。 2. Fruit Jam输出背景色不纯。 | 1.精细调整“相似度”滑块,这是最关键的一步。 2. 尝试微调“关键颜色溢出减少”。 3. 检查 BG_COLOR是否为纯蓝0x0000FF,确保代码无误。 |
| 无法连接到Wi-Fi | 1.settings.toml配置错误。2. Wi-Fi信号问题。 3. 板载ESP32-C6故障。 | 1. 检查settings.toml文件名、格式(TOML)及SSID/密码是否正确。2. 将Fruit Jam靠近路由器测试。 3. 观察板载NeoPixel LED状态:快速闪烁表示正在连接,常亮表示成功,特定颜色闪烁可能表示错误(需查库文档)。 |
| 数据不更新或更新错误 | 1. 网络连接不稳定。 2. API地址或JSON路径错误。 3. API返回数据格式变化。 | 1. 将Fruit Jam通过USB连接到电脑,用串口终端工具(如Thonny、PuTTY)查看debug=True输出的错误信息。2. 用浏览器直接访问 DATA_SOURCE,确认API能返回预期JSON,并核对json_path指向的字段名。 |
| 按下按钮无反应 | 1. 按钮消抖逻辑问题。 2. 主循环被阻塞。 | 1. 代码中的按钮检测逻辑是“边沿检测”,确保快速点按。长按可能不触发。 2. 检查网络请求 fruitjam.fetch()是否因超时而长时间阻塞主循环。可以尝试增加异常处理的超时时间,或减少FETCH_DELAY。 |
6.3 性能优化与稳定性建议
- 字体文件大小:如果使用自定义
.pcf字体,尽量选择字符集小的字体文件,以节省内存。 - 网络请求优化:如果API响应慢,可以考虑在
fruitjam.fetch()外围增加更具体的超时异常捕获(如socket.timeout),并设置更长的重试间隔,避免频繁失败重试导致系统卡顿。 - 电源供应:确保使用可靠的5V/2A以上的USB电源适配器为Fruit Jam供电。视频输出功耗较高,供电不足可能导致设备重启或显示异常。
- 散热考虑:长时间运行时,RP2350芯片会有一定发热。确保设备放置在通风良好的地方,避免高温环境。
这个基于CircuitPython和Fruit Jam的实时视频图文叠加系统,将一个看似专业的广播技术,拆解成了硬件连接、代码编写和软件设置三个清晰的步骤。它不仅仅是一个教学项目,更是一个强大的原型工具。你可以用它来为你的智能家居控制台添加状态显示,为小型展览制作动态信息牌,或者为任何需要将实时数据可视化并叠加到视频流中的场景提供解决方案。其核心价值在于“分离”与“聚合”的思想——让嵌入式设备做它擅长的实时数据采集与渲染,让专业的桌面软件做它擅长的视频合成与推流,两者通过标准接口(HDMI)协作,最终实现了一加一大于二的效果。动手试试吧,当你第一次在直播画面中看到自己硬件实时推送的信息时,那种成就感会告诉你,硬件创客的乐趣就在于此。
