旧笔记本变身家庭摄像头:在飞牛 fnOS 上用 go2rtc 驱动内置摄像头
本文记录一次真实的折腾过程:一台不再合盖的旧笔记本已经安装了飞牛 fnOS,我们继续利用它的内置摄像头,通过 Docker 和 go2rtc 搭建一个可在浏览器中查看的家庭视频流。
一、目标与最终效果
这次实现的目标很明确:
- 让 fnOS 识别并读取笔记本内置摄像头;
- 不在 fnOS 宿主系统中堆积不必要的软件包;
- 使用 Docker 运行轻量级视频服务;
- 把摄像头的原始画面转成浏览器可播放的视频流;
- 给 Web 管理界面加上认证,不使用无密码服务;
- 让 fnOS 识别容器的 Web 端口,通过 FN Connect 从外网访问。
完成后的处理链路如下:
联想笔记本内置摄像头
│
▼
Linux V4L2(/dev/video0)
│ YUYV 原始帧
▼
go2rtc 容器内的 FFmpeg
│ 转码为 H.264
▼
go2rtc WebRTC / MSE / Web 播放界面
│
├── 局域网 IP:1984
│
└── FN Connect HTTPS 代理入口
本文已完成局域网视频查看,并在电脑浏览器中验证了 FN Connect 外网访问。FN Connect 提供的是经 fnOS 授权的 HTTPS 代理入口,不是把 go2rtc 端口直接暴露到公网。
二、测试环境
本次使用的环境如下:
| NAS 系统 | 飞牛 fnOS,底层为 Debian 12 Bookworm |
| Linux 内核 | 6.18.18 x86_64 |
| Docker | 28.5.2 |
| Docker Compose | v5.0.2 |
| 摄像头 | Syntek Lenovo EasyCamera,USB ID 174f:1158 |
| 视频设备 | /dev/video0 和 /dev/video1 |
| 视频服务 | alexxit/go2rtc:latest |
三、先判断 Linux 是否识别摄像头
开始部署之前,第一件事不是安装软件,而是确认硬件链路是否成立。
通过 fnOS 的 SSH 进入系统后,执行:
uname -a
cat /etc/os-release
lsusb
ls -l /dev/video*
关键输出为:
Bus 001 Device 002: ID 174f:1158 Syntek Lenovo EasyCamera
crw-rw—- 1 root video … /dev/video0
crw-rw—- 1 root video … /dev/video1
这说明三件事:
V4L2 是什么
V4L2 的全称是 Video4Linux2,是 Linux 下摄像头、采集卡等视频设备的标准接口。应用程序不需要直接理解 USB 摄像头协议,只需要打开 /dev/video0,并通过 V4L2 设置分辨率、帧率和像素格式。
四、为什么同一个摄像头会有 video0 和 video1
一个 USB 摄像头出现多个 /dev/videoX 节点并不罕见。它们可能分别代表:
- 可读取画面的视频采集节点;
- 摄像头元数据或辅助功能节点;
- 不同像素格式或不同功能的独立接口。
本次对两个节点分别执行 FFmpeg 探测:
docker run –rm \\
–device /dev/video0:/dev/video0 \\
–entrypoint ffmpeg \\
alexxit/go2rtc:latest \\
-hide_banner -f v4l2 -list_formats all -i /dev/video0
/dev/video0 能够列出画面格式:
Raw : yuyv422 : YUYV 4:2:2 : 640×480 640×360 320×240 160×120
而探测 /dev/video1 时返回:
ioctl(VIDIOC_G_INPUT): Not a tty
因此可以确定:
- /dev/video0 是实际的画面采集节点;
- /dev/video1 不支持普通视频输入所需的 VIDIOC_G_INPUT 操作,不应映射给视频服务。
/dev/video0 探测命令末尾也出现了 Error opening input file。这条命令的目的只是让 FFmpeg 枚举格式,并没有配置实际输出;既然前面已经成功列出 YUYV 和分辨率,就不能根据末尾的错误认定设备不存在。后续的实际播放也证明了 /dev/video0 选择正确。
为什么用临时 Docker 容器探测
fnOS 宿主上没有安装 v4l2-ctl。安装 v4l-utils 当然也可以,但这次直接复用 go2rtc 镜像内自带的 FFmpeg,并使用 –rm 创建一次性容器。命令结束后容器自动删除,宿主系统不需要新增软件包。
五、为什么要转码为 H.264
这颗 EasyCamera 枚举出来的是 yuyv422,也就是未压缩的 YUYV 4:2:2 原始帧,而不是摄像头内部已经压缩好的 H.264。
640×480、30 fps、16 bit/像素的理论原始数据量约为:
640 × 480 × 30 × 16 ≈ 147,456,000 bit/s
也就是约 147 Mbit/s,还未计算协议开销。如果直接在网络上传输原始帧,既浪费带宽,浏览器也不一定能直接播放。
因此本方案让 go2rtc 调用 FFmpeg,把 YUYV 转码为 H.264:
ffmpeg:device?video=/dev/video0
&input_format=yuyv422
&video_size=640×480
&framerate=15
#video=h264
初始帧率选择 15 fps,是为了降低旧笔记本的 CPU 压力。对「偶尔看看家里情况」这种场景,15 fps 通常已经足够。
六、为什么选择 go2rtc
go2rtc 是一个面向摄像头和家庭自动化场景的视频协议转换工具,支持 RTSP、WebRTC、HLS、MP4 等多种输入输出方式。官方 Docker 镜像还内置了 FFmpeg,因此非常适合当前的单摄像头、低复杂度场景。
相比完整的 NVR 系统,这里暂时不需要持续录像、AI 识别或多路摄像头管理,因此不必一开始就部署 Frigate 等更重的方案。
go2rtc 的官方文档:
- go2rtc 项目主页
- Docker 部署说明
- FFmpeg 摄像头输入说明
七、第一次启动为什么不断重启
第一次启动时,我们尝试在 docker run 命令的镜像名后直接追加多个 -c 配置参数。容器启动后,fnOS 的 Docker 界面显示它一直重启。
通过以下命令停止重启并保留现场:
docker update –restart=no go2rtc-camera
docker stop go2rtc-camera
docker logs –tail 100 go2rtc-camera
docker inspect go2rtc-camera \\
–format '退出码={{.State.ExitCode}} 错误={{.State.Error}} 启动参数={{json .Config.Cmd}}'
日志非常明确:
[FATAL tini (7)] exec -c failed: No such file or directory
容器退出码为 127,通常表示要执行的命令不存在。
根本原因:Docker 镜像的 ENTRYPOINT 与 CMD
Docker 镜像可以同时定义:
- ENTRYPOINT:容器启动时固定执行的入口;
- CMD:传给入口的默认命令或参数。
go2rtc 镜像使用 tini 作为容器的一号进程,默认 CMD 中会包含真正的 go2rtc 启动命令。
但是,docker run IMAGE … 中写在镜像名后面的内容会替换镜像原有的 CMD。当时的命令使 CMD 变成了:
["-c", "streams.home_camera=…", "-c", "api.username=…", "…"]
于是 tini 尝试把 -c 当成可执行程序,系统当然找不到名为 -c 的程序,因此立即以 127 退出。
容器还配置了:
restart: unless–stopped
所以整个循环就变成了:
tini 启动 → 尝试执行 -c → 退出码 127
↑ │
└───── Docker 自动重启 ────────┘
这也是为什么在 Docker 界面上只能看到「不断重启」,但摄像头指示灯始终不亮:程序还没有运行到打开摄像头的阶段。
正确处理方式
修复时没有继续堆叠命令行参数,而是回到官方更推荐的方式:
八、最终配置
目录结构如下:
/root/go2rtc-camera/
├── compose.yaml
├── login.txt
└── config/
└── go2rtc.yaml
go2rtc.yaml
api:
listen: ":1984"
username: "your_camera_user"
password: "replace_with_a_strong_password"
local_auth: true
rtsp:
listen: "127.0.0.1:8554"
webrtc:
listen: ":8555"
streams:
home_camera: "ffmpeg:device?video=/dev/video0&input_format=yuyv422&video_size=640×480&framerate=15#video=h264"
log:
level: info
这里有几个重要细节:
- local_auth: true:即使请求被视为本地请求,也要求认证;
- RTSP 只监听 127.0.0.1:8554,不向局域网暴露;
- WebRTC 使用 8555 端口;
- 摄像头选择经过探测的 /dev/video0;
- 输入格式、分辨率和帧率都是根据实际设备能力设置的;
- 用户名和密码必须换成自己的值,不应把真实密码发布到博客或 Git 仓库。
compose.yaml
services:
go2rtc-camera:
image: alexxit/go2rtc:latest
container_name: go2rtc–camera
restart: unless–stopped
ports:
– "1984:1984/tcp"
– "8555:8555/tcp"
– "8555:8555/udp"
devices:
– /dev/video0:/dev/video0
environment:
TZ: Asia/Shanghai
volumes:
– ./config:/config:ro
Compose 配置的原理
devices 只向容器映射 /dev/video0,比直接赋予容器 privileged 权限更小。本方案使用 CPU 转码,因此不需要为了显卡加速开启特权模式。
初版配置使用过 network_mode: host。这种模式下容器直接共享宿主网络栈,局域网播放很方便,但 Docker 不会真正建立 ports 发布规则。即使在配置中填入端口,docker port go2rtc-camera 仍然没有输出,因为 Docker 官方明确规定 host 网络下端口发布不生效。
fnOS 需要根据 Docker 的有效端口映射生成容器访问入口,因此最终改用默认 bridge 网络,并显式发布 1984 和 8555。1984 是 go2rtc Web/API 端口;8555 是 WebRTC 的 TCP/UDP 媒体端口,它本来就不是可以用浏览器直接打开的网页。参考 Docker Host 网络说明。
./config:/config:ro 将配置目录以只读方式挂载。这意味着要修改配置时,应在 fnOS 宿主上编辑 YAML 并重启容器,而不是从 Web 页面直接覆盖文件。
九、启动和验证
在配置目录中启动:
cd /root/go2rtc-camera
docker compose up -d
检查状态和日志:
docker compose ps
docker compose logs –tail 50
docker port go2rtc-camera
切换到 bridge 并重建容器后,docker port 应出现真正的映射:
1984/tcp -> 0.0.0.0:1984
8555/tcp -> 0.0.0.0:8555
8555/udp -> 0.0.0.0:8555
然后在同一局域网的浏览器中打开:
http://<fnOS 局域网 IP>:1984
输入 go2rtc.yaml 中配置的用户名和密码,再选择 home_camera 即可播放。
通过 FN Connect 外网访问
开启 FN Connect 并登录 fnOS 后,从 Docker 界面找到 go2rtc-camera,再点击 1984 对应的访问入口。fnOS 会生成受当前登录会话保护的 HTTPS 代理地址。这个地址不等同于在 FN ID 后手工追加 :1984,应当从 fnOS 界面进入。
外网播放可以显式使用 MSE 模式:
/stream.html?src=home_camera&mode=mse
WebRTC 会另外尝试使用 8555 TCP/UDP 传输媒体,而 MSE 通过 1984 上的同源 WebSocket 传输 H.264,更适合 HTTPS 代理链路。实际验证中,电脑浏览器可以经 FN Connect 打开页面并观看视频。关于 FN Connect 的直连、P2P 和中继机制,可参考 飞牛官方远程访问说明。
运行时可以查看容器资源消耗:
docker stats –no-stream go2rtc-camera
如果后续将帧率调高到 30 fps,应重新观察 CPU 占用、温度和播放稳定性,不要只根据「能打开画面」判定长期运行没有问题。
十、修改登录用户名和密码
直接编辑宿主上的配置:
nano /root/go2rtc-camera/config/go2rtc.yaml
修改:
username: "your_camera_user"
password: "your_new_strong_password"
保存后重启:
cd /root/go2rtc-camera
docker compose restart
浏览器可能会缓存 HTTP Basic Auth 凭据。如果修改后没有重新弹出登录框,可以关闭相关页面或使用无痕窗口重新打开。
十一、安全注意事项
go2rtc 的 Web API 不只是一个静态播放页面,还包含配置和流管理能力。因此,「能看画面」不等于「可以无保护暴露到互联网」。
建议至少遵守以下原则:
go2rtc 官方也明确提醒,未正确保护的 API 可能让攻击者访问摄像头甚至服务器。可参考 go2rtc HTTP API 安全说明。
十二、这次实践的几个经验
1. 先验证硬件链路,再安装服务
如果 /dev/video0 根本不存在,部署再多容器也无法解决问题。从 USB 识别、设备节点到支持格式逐层确认,可以快速缩小故障范围。
2. 设备节点存在,不等于它一定能输出画面
/dev/video1 就是很好的例子。多设备节点必须通过 V4L2 能力探测区分,不应简单地把所有 /dev/video* 全部映射给容器。
3. 摄像头输出格式决定系统负载
如果摄像头本身能输出 H.264,go2rtc 可能只需要转封装,CPU 开销很小。当摄像头只能输出 YUYV 时,宿主必须承担实时编码成本。因此分辨率和帧率不能凭空填写,应根据设备能力和机器负载确定。
4. Docker 重启循环首先看退出码和真实启动参数
容器「一直重启」只是外在现象。本次通过 docker logs 看到 exec -c failed,再通过 docker inspect 看到被覆盖后的 CMD,才确定问题和摄像头、端口或密码都无关。
5. 简单场景不必一开始就上完整 NVR
本次需求主要是实时看画面,go2rtc 已经能够完成采集、转码和浏览器播放。如果未来需要移动侦测、录像存储、时间线回放或 AI 识别,再引入 motionEye、Frigate 或其他 NVR 系统更合理。
十三、后续可以继续做什么
在当前局域网播放和电脑端 FN Connect 外网访问稳定之后,可以按需增加:
- 进一步验证不同浏览器和客户端下的播放兼容性;
- 如果需要固定访问地址或更通用的客户端支持,再配置 VPN 或虚拟组网;
- 测试并调整 15/20/30 fps,在流畅度与 CPU 负载之间取舍;
- 为笔记本设置来电自启和异常断电恢复;
- 测试长时间运行时的 CPU 温度和风扇噪声;
- 增加定时截图、移动侦测或录像功能;
- 如果 CPU 占用成为瓶颈,再评估 Intel VAAPI 硬件编码。
结语
这次改造没有添加新硬件,只是把旧笔记本上原本闲置的摄像头重新利用了起来。整个过程的核心不是复制一份 Docker Compose,而是从硬件识别、V4L2 设备节点、像素格式、实时转码一直追踪到 Docker 的进程启动语义。
也正是这个过程,让一个「容器一直重启」的模糊现象,最终变成了一个可以被日志、退出码和启动参数完整解释的具体问题。
