1. 项目缘起与整体设计思路
1.1 为什么要在ARM平台上折腾MJPEG-Streamer
先说说这个项目到底在干什么。MJPEG-Streamer是一个把摄像头采集到的MJPEG格式视频流,通过HTTP服务推送给浏览器或其他客户端的小工具。它的核心价值在于轻量、依赖少、延迟低,特别适合嵌入式设备做本地视频监控或者远程画面查看。我第一次接触它是在一个ARM开发板上做图像采集项目,板子性能有限,跑不动复杂的流媒体服务器,MJPEG-Streamer就成了最合适的选择。
但问题来了,官方源码默认是给x86桌面环境编译的,直接拿到ARM平台上编译,十有八九会报错。原因也不复杂:输入插件依赖V4L2(Video for Linux 2)接口,输出插件依赖JPEG编解码库,而ARM平台上的库版本、头文件路径、交叉编译工具链都和桌面环境不一样。所以这个移植笔记的核心目标,就是让MJPEG-Streamer在ARM板上稳定跑起来,能正常采集摄像头画面并通过网络推流。
适合谁看?如果你手上有ARM开发板、树莓派类的单板计算机,或者任何嵌入式Linux设备,想快速搭一个低成本的视频流服务,这篇内容应该能帮你省下不少查资料和试错的时间。不需要你精通内核驱动,但基本的Linux操作和交叉编译概念得有一点。
1.2 整体方案选型与架构拆解
MJPEG-Streamer的架构其实很清晰,分成三个部分:输入插件、核心调度、输出插件。输入插件负责从摄像头拿数据,核心部分负责把数据从输入搬到输出,输出插件负责把数据以HTTP流的形式发出去。在ARM移植场景下,我们主要关注的是输入插件和输出插件的依赖问题。
输入插件方面,最常用的是input_uvc.so,它通过V4L2接口和USB摄像头通信。这个插件依赖libjpeg库来做MJPEG帧的解析和必要的格式转换。输出插件方面,output_http.so是最常用的,它启动一个轻量HTTP服务器,把MJPEG流以multipart/x-mixed-replace的方式推给客户端。这个插件本身不依赖太多外部库,但需要确保网络相关的基础库在ARM平台上可用。
我选择的是交叉编译方案,而不是在ARM板上直接编译。原因有两个:一是ARM板的存储和内存有限,直接编译容易因为资源不足中断;二是交叉编译工具链在PC上跑得快,调试效率高。当然,如果你的ARM板性能足够,直接在板上编译也不是不行,后面我会把两种方式的差异和注意事项都讲清楚。
注意:交叉编译工具链的版本要和目标板上的系统库版本匹配,否则编译出来的二进制文件可能在板上跑不起来,报“GLIBC版本不兼容”之类的错误。这一点我在第一次移植时就踩过坑,后面会详细说。
2. 核心细节解析与实操要点
2.1 交叉编译工具链的选择与验证
交叉编译工具链是整个移植工作的基础。你需要根据ARM板的CPU架构和系统环境来选。常见的ARM架构有armv7l、aarch64(也就是arm64),前者是32位,后者是64位。你可以通过在ARM板上执行uname -m来确认。比如输出armv7l就说明是32位ARM,输出aarch64就是64位。
工具链的来源一般有三种:芯片厂商提供的官方工具链、Linaro等社区维护的通用工具链、以及发行版自带的交叉编译包。我个人的经验是,如果芯片厂商有提供,优先用厂商的,因为兼容性最有保障。如果没有,Linaro的通用工具链也能应付大多数场景。
选好工具链之后,第一件事是验证它能不能正常工作。写一个最简单的C程序,比如就打印一行字,然后用交叉编译器编译,把生成的二进制文件拷到ARM板上运行。这一步看起来简单,但能帮你排除掉很多低级问题,比如工具链路径没配好、动态链接器路径不对等等。
# 假设工具链前缀是 arm-linux-gnueabihf- arm-linux-gnueabihf-gcc -o hello hello.c file hello # 输出应该显示 ARM 架构file命令的输出很关键,它会告诉你这个二进制文件的目标架构和动态链接器路径。如果显示的是x86架构,说明你用的还是本机编译器,交叉编译前缀没生效。
2.2 依赖库的交叉编译顺序与要点
MJPEG-Streamer在ARM平台上需要两个核心依赖库:libjpeg和libv4l。libjpeg负责JPEG编解码,libv4l负责V4L2设备的封装访问。这两个库都需要先交叉编译好,再编译MJPEG-Streamer本身。
libjpeg的交叉编译相对简单,它的构建系统是经典的configure+make。关键是在configure阶段指定正确的host参数和安装路径。我一般会把所有交叉编译的库安装到一个独立的sysroot目录下,这样编译MJPEG-Streamer时只需要指定一个--sysroot参数就能找到所有依赖,管理起来清爽。
./configure --host=arm-linux-gnueabihf \ --prefix=/opt/arm-sysroot/usr \ --enable-shared \ --enable-static make -j4 make installlibv4l的编译稍微麻烦一点,它依赖内核头文件。你需要确保工具链的sysroot里有对应版本的Linux内核头文件。如果没有,可以从内核源码里把include/uapi目录下的头文件拷过去。libv4l的configure阶段需要指定--with-kernel-headers参数指向这些头文件的位置。
实操心得:libv4l在交叉编译时经常报找不到
linux/videodev2.h的错误。最直接的解决办法是从内核源码的include/uapi/linux/目录下把videodev2.h和相关头文件拷到工具链的sysroot里。别去改源码里的include路径,那样会引入更多问题。
2.3 MJPEG-Streamer源码的配置与编译
依赖库准备好之后,就可以编译MJPEG-Streamer本身了。它的构建系统也是configure+make,但需要额外指定一些参数来告诉它依赖库的位置。
./configure --host=arm-linux-gnueabihf \ --prefix=/opt/arm-sysroot/usr \ --disable-v4l2 \ --enable-v4l2 \ --disable-input-file \ --enable-input-uvc \ --disable-output-file \ --enable-output-http这里有几个关键点。--disable-v4l2和--enable-v4l2看起来矛盾,实际上第一个是禁用旧的V4L1接口,第二个是启用V4L2接口。--disable-input-file和--disable-output-file是为了减小最终二进制体积,如果你不需要从文件读输入或者输出到文件,就把它们关掉。--enable-input-uvc和--enable-output-http是我们真正需要的功能。
编译过程中最常见的错误是找不到jpeglib.h或者libjpeg.so。这时候需要检查CPPFLAGS和LDFLAGS环境变量是否指向了正确的sysroot路径。
export CPPFLAGS="-I/opt/arm-sysroot/usr/include" export LDFLAGS="-L/opt/arm-sysroot/usr/lib"编译完成后,用file命令确认生成的mjpg_streamer二进制文件是ARM架构,然后用arm-linux-gnueabihf-readelf -d查看它依赖的动态库列表,确保所有依赖都能在ARM板上找到。
3. 实操过程与核心环节实现
3.1 从零开始的完整编译流程
我把整个编译流程整理成了一套可以直接照着做的步骤。假设你已经有了交叉编译工具链,并且工具链的前缀是arm-linux-gnueabihf-,目标板的sysroot放在/opt/arm-sysroot。
第一步,准备目录结构。我会在/opt下建三个目录:arm-sysroot放目标板的库和头文件,arm-build放编译过程中的中间文件,arm-output放最终要拷到板子上的文件。
mkdir -p /opt/arm-sysroot/usr/{include,lib} mkdir -p /opt/arm-build mkdir -p /opt/arm-output第二步,交叉编译libjpeg。下载libjpeg的源码包,解压后进入目录。
cd /opt/arm-build tar xzf jpegsrc.v9d.tar.gz cd jpeg-9d ./configure --host=arm-linux-gnueabihf \ --prefix=/opt/arm-sysroot/usr \ --enable-shared \ --enable-static make -j$(nproc) make install编译完成后,检查/opt/arm-sysroot/usr/lib下是否有libjpeg.so和libjpeg.a,以及/opt/arm-sysroot/usr/include下是否有jpeglib.h。
第三步,交叉编译libv4l。这一步需要内核头文件,我一般从内核源码的include/uapi目录拷贝。
cd /opt/arm-build tar xjf v4l-utils-1.20.0.tar.bz2 cd v4l-utils-1.20.0 ./configure --host=arm-linux-gnueabihf \ --prefix=/opt/arm-sysroot/usr \ --with-kernel-headers=/opt/arm-sysroot/usr/include \ --disable-doxygen-doc \ --disable-libdvbv5 \ --disable-v4l-utils make -j$(nproc) make install这里--disable-v4l-utils是为了只编译libv4l库,不编译那些用不上的命令行工具,节省时间。
第四步,编译MJPEG-Streamer。
cd /opt/arm-build tar xzf mjpg-streamer.tar.gz cd mjpg-streamer/mjpg-streamer-experimental export CPPFLAGS="-I/opt/arm-sysroot/usr/include" export LDFLAGS="-L/opt/arm-sysroot/usr/lib" make -j$(nproc)注意,MJPEG-Streamer的experimental分支用的是Makefile而不是configure脚本,所以不需要执行configure。编译完成后,在当前目录下会生成mjpg_streamer可执行文件和plugins目录。
3.2 部署到ARM板上的文件组织
编译完成后,需要把以下文件拷到ARM板上:
mjpg_streamer可执行文件plugins/input_uvc.soplugins/output_http.solibjpeg.so.9(如果板子上没有的话)libv4l2.so和libv4lconvert.so(如果板子上没有的话)
我一般会在ARM板上建一个/opt/mjpg-streamer目录,把可执行文件和插件放进去,库文件放到/usr/lib或者通过LD_LIBRARY_PATH指定。
# 在ARM板上执行 mkdir -p /opt/mjpg-streamer/plugins # 把文件拷进去之后 export LD_LIBRARY_PATH=/opt/mjpg-streamer:$LD_LIBRARY_PATH3.3 启动参数配置与摄像头兼容性测试
MJPEG-Streamer的启动命令需要指定输入插件和输出插件的参数。一个典型的启动命令是这样的:
./mjpg_streamer -i "input_uvc.so -d /dev/video0 -r 640x480 -f 30" \ -o "output_http.so -p 8080 -w ./www"-d /dev/video0指定摄像头设备节点,-r 640x480指定分辨率,-f 30指定帧率。输出插件的-p 8080指定HTTP服务端口,-w ./www指定网页文件目录。
摄像头兼容性是移植过程中最头疼的问题。不是所有USB摄像头都能在ARM板上被V4L2正确识别。我遇到过几种典型情况:摄像头被识别了但不出图、出图但颜色不对、出图但帧率极低。排查方法是用v4l2-ctl工具查看摄像头支持的格式。
v4l2-ctl -d /dev/video0 --list-formats-ext如果输出里没有MJPG格式,说明这个摄像头不支持MJPEG输出,MJPEG-Streamer的input_uvc插件可能无法正常工作。这时候可以尝试用YUYV格式,但CPU占用会高很多,因为需要软件编码成JPEG。
避坑技巧:如果摄像头只支持YUYV格式,可以在input_uvc插件的参数里加
-y强制使用YUYV,然后让MJPEG-Streamer内部做JPEG编码。但ARM板的CPU性能有限,640x480@30fps的YUYV转JPEG可能会跑满一个核心。建议降到640x480@15fps或者320x240@30fps。
4. 常见问题与排查技巧实录
4.1 编译阶段的高频错误与解决
编译阶段最常见的问题我整理成了一个速查表,方便你对照排查。
| 错误现象 | 可能原因 | 解决方法 |
|---|---|---|
configure: error: C compiler cannot create executables | 工具链路径不对或权限不足 | 检查--host参数和工具链可执行权限 |
fatal error: jpeglib.h: No such file or directory | CPPFLAGS未指向sysroot | 导出CPPFLAGS=-I/opt/arm-sysroot/usr/include |
cannot find -ljpeg | LDFLAGS未指向sysroot | 导出LDFLAGS=-L/opt/arm-sysroot/usr/lib |
undefined reference to 'v4l2_open' | libv4l未正确链接 | 确认libv4l已安装到sysroot,且LDFLAGS包含其路径 |
error: 'VIDIOC_S_FMT' undeclared | 内核头文件版本不匹配 | 从内核源码拷贝最新的videodev2.h到sysroot |
还有一个比较隐蔽的问题:编译出来的二进制文件在ARM板上运行时提示No such file or directory,但文件明明存在。这通常是因为动态链接器的路径不对。用readelf -l查看二进制文件的interpreter路径,确认它指向的链接器在ARM板上存在。
arm-linux-gnueabihf-readelf -l mjpg_streamer | grep interpreter如果输出的路径是/lib/ld-linux-armhf.so.3,但ARM板上这个文件在/lib/arm-linux-gnueabihf/目录下,就需要在编译时通过-Wl,--dynamic-linker参数指定正确的路径,或者在ARM板上建一个软链接。
4.2 运行阶段的典型故障排查
运行阶段的问题往往更棘手,因为涉及到硬件和系统环境的交互。我挑几个最有代表性的场景来说。
场景一:启动后浏览器打不开画面。首先确认ARM板和客户端在同一网络下,然后用curl在ARM板上本地测试HTTP服务是否正常。
curl -I http://localhost:8080/?action=stream如果返回200 OK,说明服务本身没问题,问题出在网络配置或防火墙。如果返回Connection refused,说明output_http插件没启动成功,检查端口是否被占用。
场景二:画面卡顿或延迟高。这通常是带宽或CPU性能瓶颈。先用top看CPU占用,如果某个核心跑满,说明JPEG编码或网络发送是瓶颈。可以尝试降低分辨率或帧率,或者换用支持硬件JPEG编码的摄像头。
场景三:画面颜色异常,偏绿或偏紫。这是典型的像素格式不匹配问题。V4L2支持多种像素格式,如果摄像头输出的是YUYV但程序按RGB解析,颜色就会错乱。解决方法是在input_uvc插件参数里明确指定格式,或者用v4l2-ctl把摄像头格式设成程序期望的格式。
v4l2-ctl -d /dev/video0 --set-fmt-video=width=640,height=480,pixelformat=MJPG4.3 性能调优与长期运行稳定性
MJPEG-Streamer在ARM板上长期运行,有几个调优点值得注意。
第一,内存缓冲区的设置。input_uvc插件默认使用mmap方式采集,缓冲区数量可以通过-n参数调整。缓冲区太少容易丢帧,太多会占内存。我一般设成4,兼顾流畅度和内存占用。
第二,网络发送的优化。output_http插件默认是阻塞发送,如果客户端网络慢,会拖累整个采集流程。可以在启动参数里加-c开启客户端连接数限制,避免一个慢客户端影响其他客户端。
第三,看门狗机制。长期运行最怕程序卡死,我一般会写一个简单的shell脚本,每隔几分钟检查一次HTTP服务是否响应,不响应就重启进程。
#!/bin/bash while true; do if ! curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/?action=stream | grep -q 200; then killall mjpg_streamer sleep 1 /opt/mjpg-streamer/mjpg_streamer -i "input_uvc.so -d /dev/video0 -r 640x480 -f 15" -o "output_http.so -p 8080" & fi sleep 60 done这个脚本虽然简单,但在实际部署中非常管用。我有一块板子连续跑了三个月,中间因为USB摄像头偶发掉线重启过几次,但整体服务可用性很高。
最后分享一个小技巧:如果你的ARM板支持硬件JPEG编码(比如某些带有VPU的芯片),可以尝试用
input_uvc.so的-softfps参数配合硬件编码器,能大幅降低CPU占用。不过这个需要针对具体芯片做适配,通用性不强,这里就不展开说了。