
这篇聊聊MJPG-streamer。如果你在嵌入式Linux板子上做过USB摄像头采集或者碰过物联网类的视频监控小项目大概率听过这个名字。韦东山老师的视频里专门有一节课讲这个方案的实现和原理我看完之后最大的感受是它不像FFmpeg那样庞大复杂也不像GStreamer那样概念满天飞MJPG-streamer就是那种“小而美、拿来就能用”的典型代表特别适合资源受限的物联网设备。这篇总结我把它掰开揉碎讲清楚MJPG-streamer到底怎么工作、代码是怎么组织的、在开发板上怎么跑起来以及我在实际操作中踩过的那些坑。1. 整体设计思路为什么物联网视频监控偏偏选它1.1 物联网视频监控的场景约束先说说物联网视频监控和普通网络摄像头的区别。普通的PC摄像头方案你可以直接上FFmpeg做推流或者用OpenCV处理画面随便折腾因为PC性能足够强。但换到嵌入式板子上情况完全不同处理器主频可能只有几百兆赫兹内存可能只有128MB甚至更少Flash存储也捉襟见肘。物联网视频监控的核心场景是一个低成本的摄像头节点把采集到的画面通过网络传输到远端浏览器或客户端查看。这个场景要求三件事一是采集端要足够轻量不能在编码上消耗太多CPU二是网络传输要尽量用HTTP这种通用协议方便各种终端直接访问三是代码要简洁可控方便针对具体硬件裁剪。MJPG-streamer恰好就是围绕这三个需求设计的。1.2 选型对比为什么不是FFmpeg或GStreamer我经常被问到既然FFmpeg功能那么强大为什么还要用MJPG-streamer这个问题得从实际资源消耗说起。FFmpeg是一个完整的音视频处理框架它要做的是“转码”“封装”“滤波”“推流”这些全能型工作你把它裁剪到嵌入式平台上交叉编译本身就是一场折磨而且运行时内存占用和CPU负载都不低。GStreamer则是一套插件化多媒体框架概念非常灵活但也正因为太灵活调试管线反而成了麻烦事。MJPG-streamer的思路完全不同。它不转码、不封装、不搞复杂的滤镜管线它只做一件事从摄像头拿到JPEG图像数据然后通过HTTP协议把它们一张一张地推给客户端。每一个JPEG帧本来就是独立的图片浏览器拿到后不断刷新显示就形成了连续的视频效果。这个方案的巧妙之处在于它把“视频”这个复杂问题降维成了“连续传输图片”这个简单问题。摄像头支持MJPEG格式时硬件直接输出压缩好的JPEG帧CPU几乎零负担即使摄像头只支持YUYV原始格式用libjpeg做软编码也远比H.264编码要省资源得多。1.3 MJPG-streamer的架构优势MJPG-streamer采用了插件架构整个程序由一个主程序和若干个动态加载的插件组成。输入插件负责从摄像头或文件获取图像数据输出插件负责把数据分发到网络或保存到本地。这种设计让它的扩展性非常好想增加一个输入源写一个input插件就行想增加一种输出方式写一个output插件就行。而且自带插件已经覆盖了最常见的需求input_uvc对应USB摄像头V4L2接口input_file对应静态图片或文件回放output_http提供HTTP网页访问output_file可以把帧图片直接落盘。这意味着你不需要改一行代码就能把USB摄像头变成一台可以通过浏览器直接访问的微型网络摄像机。对于物联网视频监控原型验证甚至小规模产品部署这个组合拳完全够用。另外如果你拿它和RTSP方案对比HTTP方案还有一个隐藏优势不需要特殊的播放器。手机浏览器、PC浏览器直接输入IP地址加端口就能看跨平台兼容性极好。这对物联网项目来说非常关键因为你不可能要求用户都安装VLC或者专门做一个客户端。2. 核心原理拆解从摄像头到浏览器的完整链路2.1 V4L2采集流程MJPG-streamer的input_uvc插件底层走的是Linux内核的V4L2Video for Linux 2框架。V4L2是Linux下视频设备的标准接口摄像头驱动会注册为类似/dev/video0的设备节点应用层通过一系列ioctl调用来控制采集。典型的采集流程是打开设备节点用VIDIOC_QUERYCAP查询设备能力确认它确实是视频采集设备。用VIDIOC_S_FMT设置采集格式。这里要注意如果你想让硬件直接输出MJPEG帧需要把像素格式设置为V4L2_PIX_FMT_MJPEG同时设置分辨率和帧率。用VIDIOC_REQBUFS申请帧缓冲区。通常申请4个缓冲区就够了这些缓冲区由驱动分配应用通过mmap映射到用户空间。把所有缓冲区都放入驱动的采集队列VIDIOC_QBUF然后调用VIDIOC_STREAMON开始采集。驱动采集到一帧数据后会把它放入一个缓冲区应用用VIDIOC_DQBUF取出这个缓冲区处理完数据后再用VIDIOC_QBUF还回去形成循环。这个过程在代码里非常清晰主循环就是DQBUF、处理、QBUF三个动作反复执行。很多初学者容易犯的错是在DQBUF之后忘记重新QBUF导致缓冲区越用越少最后采集直接卡死。MJPG-streamer的代码结构很适合当作V4L2编程的入门范本逻辑比那些动辄几百行的框架代码干净得多。2.2 JPEG编码的两种模式这里要澄清一个概念MJPEG并不是一种编码协议它本质上是“Motion JPEG”也就是一系列JPEG图片的连续播放。在摄像头层面支持MJPEG输出的摄像头内部其实有硬件JPEG编码器能直接输出压缩好的JPEG帧。这种情况下主控芯片只需要搬运数据CPU占用率可以忽略不计。如果摄像头不支持MJPEG只输出YUYV这种原始格式那么MJPG-streamer就需要借助libjpeg库把YUYV数据转换成JPEG。这个转换是纯软件计算会消耗一定的CPU。以640x480分辨率为例一帧YUYV原始数据大约614KB软件压缩后可能只有30KB到80KB压缩率非常可观但CPU负担会明显上升。韦东山老师在视频里特别强调了硬件MJPEG模式的重要性。我当时实验用的是中星微ZC0301芯片的USB摄像头它支持硬件MJPEG输出实测640x48030帧的采集完全无压力。而如果换成只支持YUYV的摄像头同样分辨率能跑到15帧就已经不错了。所以在选型上我强烈建议优先选择支持MJPEG硬件输出的摄像头这能直接决定方案的流畅度。2.3 HTTP流媒体协议的秘密MJPG-streamer的http输出插件核心是一个轻量级HTTP服务器它监听一个TCP端口默认8080然后根据客户端请求的URL路径分发不同的处理逻辑。最常用的是两个URL/?actionsnapshot获取单张JPEG静态图片。/?actionstream获取连续的MJPEG视频流。视频流背后的HTTP响应头非常关键它用的是一种叫multipart/x-mixed-replace的MIME类型。这个类型的意思是HTTP响应体里会包含多个部分part每个部分都是独立的JPEG图片它们依次被发送到客户端。浏览器支持这种类型所以收到第一帧后不会断开连接而是一帧一帧地刷新显示。响应头大致长这样HTTP/1.1 200 OK Content-Type: multipart/x-mixed-replace; boundaryframe --frame Content-Type: image/jpeg Content-Length: 45231 [JPEG二进制数据] --frame Content-Type: image/jpeg Content-Length: 45887 [JPEG二进制数据]这个机制用生活化的比喻来说就像是快递员不停给你送独立的照片你每收到一张就把它贴到墙上贴得够快看起来就是动态画面。2.4 缓冲区的共享与同步MJPG-streamer里有一个全局的帧缓冲区数组输入插件负责往里面填充数据输出插件负责从里面读取数据。多个输出插件可以同时工作比如一个输出HTTP流另一个输出文件。这里有一个多线程竞争问题输入线程正在写缓冲区的时候输出线程不能同时去读否则会读到半帧数据导致画面撕裂。MJPG-streamer的解决办法是每个缓冲区配一个pthread互斥锁和一个条件变量。输入线程写完一帧后会更新帧序号并广播条件变量通知所有输出线程。输出线程拿到锁后检查帧序号是否变化如果变化就读取最新数据否则等待。这套同步机制直接决定了多路输出的稳定性。我测试过同时开HTTP视频流和文件输出两个功能只要摄像头采集帧率不低于10帧两个输出端都能正常工作。3. 实战过程在开发板上从零跑通MJPG-streamer3.1 环境准备与交叉编译我实验用的平台是韦东山老师的imx6ull开发板运行Linux系统USB接口接了一个支持MJPEG输出的摄像头。在PC上的Ubuntu交叉编译环境里我需要准备好arm版本的交叉编译器、libjpeg库和MJPG-streamer源码。首先解决libjpeg的交叉编译问题。MJPG-streamer依赖libjpeg做JPEG解码和编码所以必须先把libjpeg编成arm版本wget http://www.ijg.org/files/jpegsrc.v9e.tar.gz tar xzf jpegsrc.v9e.tar.gz cd jpeg-9e ./configure --prefix/opt/arm-libjpeg --hostarm-linux-gnueabihf make make install--host参数指定目标平台是arm-linux-gnueabihf--prefix指定安装路径编译出来的头文件和静态库会放到/opt/arm-libjpeg目录下。然后是MJPG-streamer的编译。它的Makefile不是标准的configure生成型而是需要手动修改变量。核心要改的就是Makefile里的CC、CFLAGS、LDFLAGS等参数。我当时直接用命令行参数覆盖的方式cd mjpg-streamer/mjpg-streamer-experimental make CCarm-linux-gnueabihf-gcc \ CFLAGS-I/opt/arm-libjpeg/include \ LDFLAGS-L/opt/arm-libjpeg/lib -ljpeg这一行命令会编译出mjpg_streamer主程序以及input_uvc.so和output_http.so等插件文件。如果你遇到链接报错找不到libjpeg多半是-L路径没有指对用find /opt/arm-libjpeg -name libjpeg*确认一下库文件位置。3.2 开发板上的部署与运行编译好的文件拷贝到开发板后我习惯把它们统一放到/usr/bin和/usr/lib目录这样省去设置环境变量的麻烦。也可以直接放一个单独的目录比如/root/mjpg然后用完整路径运行。运行命令的基本格式是export LD_LIBRARY_PATH/opt/arm-libjpeg/lib:$LD_LIBRARY_PATH ./mjpg_streamer -i ./input_uvc.so -d /dev/video0 -r 640x480 -f 30 -o ./output_http.so -w ./www -p 8080-i指定输入插件-d是摄像头设备节点-r是分辨率-f是帧率-o指定输出插件-w是网页文件目录-p是监听端口。-w目录下放着HTML页面和JavaScript脚本MJPG-streamer自带的www目录里有一个简易的网页播放器直接浏览器访问板子IP加端口就能看到画面。实测下来这个过程耗时极短从启动到浏览器看到画面大约只需两三秒。板子CPU占用率在10%以下内存占用也只有几MB对于一个完整的视频监控节点来说这个资源消耗非常友好。3.3 验证硬编码MJPEG模式是否生效这里有一个排查重点如何确认摄像头真的工作在硬件MJPEG模式而不是软件编码模式方法很简单看启动日志。./mjpg_streamer -i ./input_uvc.so -d /dev/video0 -r 640x480 -f 30 -o ./output_http.so -w ./www -p 8080 MJPG-streamer: starting application MJPG-streamer: input plugin: input_uvc.so input_uvc: using V4L2 device: /dev/video0 input_uvc: current format: 640x480 input_uvc: found format: MJPG关键是这一行found format: MJPG。这说明V4L2协商格式时驱动接受了MJPEG格式摄像头会直接输出JPEG帧。如果这行显示的是found format: YUYV说明摄像头不支持硬件MJPEG程序会走libjpeg软编码路径。我用一个只支持YUYV的摄像头测试同样的分辨率和帧率CPU占用率直接飙升到40%以上。这个对比非常直观也印证了韦东山老师在视频里强调的观点硬MJPEG是流畅性的关键。3.4 参数选择的经验值分辨率、帧率、JPEG质量这几个参数之间是相互制约的。我整理了实际测试的一组数据都是MJPEG硬编码模式分辨率帧率JPEG帧平均大小实测带宽占用CPU占用率320x2401518KB约2.2Mbps约5%640x4801545KB约5.4Mbps约8%640x4803045KB约10.8Mbps约12%1280x72015120KB约14.4Mbps约25%从这个表格可以得出几个结论分辨率和帧率越高带宽占用越大同一分辨率下帧率翻倍带宽基本翻倍JPEG帧大小受画面复杂度影响很大画面里有剧烈运动或者高清纹理时帧大小会明显变大。物联网场景下我建议优先选择640x48015帧或320x24015帧这两个档位在带宽和画质之间平衡得比较好。如果你走Wi-Fi传输注意摄像头帧率不要超过网络吞吐能力太多否则路由器拥堵会出现严重延迟。3.5 多路输出的并发测试MJPG-streamer的一个典型玩法是同时开启多个输出插件。比如我想一边在浏览器看实时画面一边把关键帧落到本地存储方便事后分析。这时命令可以写成./mjpg_streamer -i ./input_uvc.so -d /dev/video0 -r 640x480 -f 15 \ -o ./output_http.so -w ./www -p 8080 \ -o ./output_file.so -f /tmp/frames -d 1000output_file.so的-d参数是保存帧的时间间隔单位毫秒-d 1000表示每秒存一张图。两个输出插件共用同一份输入数据靠之前讲到的锁机制保证同步。实测中我发现只要输入帧率不低于10帧两条输出链路都能保持稳定。但如果帧率降得太低比如只有5帧HTTP流会出现明显的卡顿感文件输出倒是无所谓因为反正每秒钟只需取一帧。4. 性能瓶颈分析与优化技巧4.1 局域网传输的带宽瓶颈用手机浏览器和PC浏览器同时访问同一个MJPG-streamer节点它们各自收到一份完整的视频流。也就是说客户端数量乘以单路码率就是总出口带宽。以640x48030帧为例单路约10Mbps两个客户端就需要20Mbps这对百兆有线网络来说还好但如果节点接的是Wi-Fi很容易把无线带宽吃满。优化思路有两个方向一是降低视频参数对源端做限制二是在传输层做改造比如加一层简单的带宽限制或缓存策略。不过MJPG-streamer本身没有做客户端数量的限制生产环境中一般建议在前面加反向代理做访问控制否则任何知道IP的人都能直接看到你的画面。4.2 延迟构成分析视频监控的延迟来自采集、编码、传输、解码、显示五个环节。MJPG-streamer方案里硬件MJPEG模式把采集和编码合二为一延迟极低。HTTP协议本身的传输延迟也基本可以忽略。真正影响体验的延迟主要来自浏览器端的播放策略和网络缓冲。实测从摄像头捕捉到动作到浏览器屏幕上出现对应画面整体延迟在100到300毫秒之间。对于监控场景这个数值完全够用。如果你追求更低延迟可以在摄像头输出参数里把帧率调高并确保同一局域网内没有大流量下载任务干扰。4.3 压缩质量与CPU的取舍如果你只能用YUYV软编码模式libjpeg的质量参数就非常关键。MJPG-streamer的input_uvc插件里有一个-q参数取值范围50到100默认80。数值越高图片越清晰但压缩率越低CPU负担越大。我的建议是监控画面通常不需要100%质量-q 80到-q 85是一个甜点区肉眼几乎看不出画质损失但帧大小能控制在合理范围。不要把质量调到90以上除非你每帧是一张要保存的高清证据图否则纯粹是浪费带宽和CPU。4.4 开机自启与看门狗物联网设备往往无人值守需要保证视频监控进程断了能自动拉起来。我当时在开发板上写了一个简单的守护shell脚本#!/bin/sh while true; do if ! pgrep -f mjpg_streamer; then /root/mjpg/mjpg_streamer \ -i /usr/lib/input_uvc.so -d /dev/video0 -r 640x480 -f 15 \ -o /usr/lib/output_http.so -w /root/mjpg/www -p 8080 /var/log/mjpg.log 21 fi sleep 10 done把脚本放到init.d或者systemd里设置开机启动。每10秒检查一次进程是否存在如果异常退出就自动拉起。这个简单粗暴的方案在生产环境里跑了很久都很稳定。5. 常见的坑与排查实录5.1 打开摄像头失败No such file or directory这个报错十有八九不是文件真的不存在而是没有权限。开发板上的/dev/video0设备往往属于root或video组。排查步骤先ls -l /dev/video0确认设备节点存在。再看用户是否在video组里可以用id命令查一下。如果不想换用户可以直接用chmod 666 /dev/video0临时解决然后重启mjpg_streamer。要注意系统重启后权限会重新分配所以最好还是把用户加入video组或者写udev规则。5.2 画面花屏或偏绿花屏的原因通常是像素格式不匹配。比如软件包默认协商成YUYV但摄像头实际输出的格式顺序不同或者分辨率设置超出了摄像头的实际支持范围。排查顺序用v4l2-ctl --list-formats-ext -d /dev/video0查看摄像头支持的所有格式。确认你的-r分辨率在摄像头支持列表里。确认启动日志里current format显示的确实是MJPG或YUYV而不是奇怪的格式。如果摄像头只支持某些固定分辨率而你设置了一个它不支持的尺寸V4L2驱动往往不会直接报错而是默默输出一个不正确的画面。这就是“能跑但画面异常”的根本原因。5.3 HTTP访问返回404-w参数指定了网页根目录如果目录不存在或者里面没有index.html访问根路径就会返回404。排查方法是先确认-w目录下文件确实存在然后在开发板上用curl http://127.0.0.1:8080/?actionsnapshot测试直接看命令返回的HTTP状态码。如果snapshot没问题但stream打不开多半是防火墙或路由器封了8080端口。开发板上先关掉不必要的防火墙规则或者把端口换成8081试试。5.4 画面断断续续帧率上不去这个问题要从两头排查一是摄像头端的输出帧率二是网络端的带宽。最简单有效的定位方法是先在开发板本地测试不经过HTTP直接让output_file插件往磁盘写帧看写帧速度是否达到预期。如果本地写帧速度正常那问题就在网络上如果本地速度也很慢那就要看看摄像头信号线、USB带宽或者供电是否足够。USB摄像头供电不足是经常被忽略的元凶。某些摄像头在USB 2.0口上如果主板供电能力弱会出现帧率抖动甚至设备反复离线。这时候换一个带独立供电的USB HUB问题往往迎刃而解。5.5 内存耗尽与缓冲区不足MJPG-streamer的--buffer参数控制线程间共享缓冲区的数量默认是10帧。如果客户端连接数量多或者读取速度慢缓冲区会被占满新的帧没有位置存放输入线程会被阻塞。表现就是摄像头采集卡顿或HTTP流延迟越来越大。这种情况可以适当调高缓冲区数量比如./mjpg_streamer --buffer 20 -i ./input_uvc.so -d /dev/video0 -o ./output_http.so -p 8080但也要注意缓冲区越大内存占用越高。一帧640x480的JPEG大约45KB20帧也就不到1MB对现代设备完全不是问题。真正容易爆内存的是有些板卡默认的/dev/shm分区太小如果输出插件把帧临时存到共享内存里就可能写满。遇到这个情况检查一下df -h /dev/shm空间不足就重新挂载大一点。6. 扩展方向从MJPG-streamer出发还能怎么玩6.1 视频流加密与鉴权MJPG-streamer原生没有用户名密码验证直接暴露在公网等于裸奔。你要部署到公网环境一定要在前面加一层保护。最省事的方案是用Nginx做反向代理把8080端口隐藏起来通过HTTPS和Basic Auth访问。Nginx的auth_basic指令五分钟就能配置好效果立竿见影。6.2 对接云平台物联网场景下视频监控通常需要接入云平台。你可以写一个简单的上报任务周期性通过/?actionsnapshot抓取某一帧然后用MQTT协议把它发布到云端的主题里。云平台收到图片后可以做AI分析或者归档存储。这样既保留了MJPG-streamer实时预览的能力又补上了云端接入能力。6.3 录像回放搭配output_file插件定时保存帧图片然后在服务器上把这些JPEG序列编码成MP4。这个方案不需要修改MJPG-streamer的代码只需要写一个调度脚本。缺点是文件是离散的图片整理起来相对繁琐。更高级的做法是改造output插件直接封装成AVI或在MJPG流的基础上做H.264转码但这个工作量会大不少。6.4 替代与延伸方案思考如果你对延迟和编码效率有更高的要求建议在MJPG-streamer的基础上接入RTSP或WebRTC链路。不过也要注意这些方案复杂度会直线上升需要的CPU和内存也更多。我的原则很清楚场景简单能用MJPG-streamer就直接用场景复杂先算清楚成本再考虑替换路线不要一上来就上重框架。我个人在实际项目里一直把MJPG-streamer当作物联网视频节点的“标配底座”它能让你在十分钟内看到画面给后续开发和调试建立充足的信心。作为入门嵌入式视频开发的第一课它的代码量适中原理清晰踩完坑之后再去看更复杂的GStreamer或者FFmpeg你会觉得那些框架里很多概念都能在MJPG-streamer里找到对应理解成本会低得多。