1. 庐山派K230做Web监控,我为什么选这条路
庐山派K230这颗板子最近在创客圈里热度不低,6TOPS的NPU算力、双核RISC-V加一颗专用AI核、自带MIPI CSI接口和千兆网口,价格还压在两百块以内。很多人拿到手第一反应是跑个YOLO做目标检测,或者折腾激光打蚊子那种趣味项目。但我拿到板子之后,最先想解决的是一个更朴素的需求:把摄像头画面低延迟地推到浏览器里,随时随地打开网页就能看。
这个需求听起来简单,实际上手你会发现坑不少。市面上成品网络摄像头一大把,海康、大华、萤石、小米,各有各的生态,但如果你想自己控制整条链路——从图像采集、编码、传输到前端渲染——成品摄像头基本不给你这个机会。而庐山派K230恰好处于一个甜点位置:它有足够强的ISP和编码能力,有完整的Linux SDK,还有MIPI CSI接口可以直接接常见的摄像头模组,比如OV5647这类树莓派生态里烂大街的便宜货。
我最终搭出来的方案,端到端延迟稳定在120到180毫秒之间,局域网内用手机浏览器打开就能看,不需要装任何插件,也不依赖任何第三方云服务。整套东西跑在K230上,CPU占用率不到40%,还有余力同时跑一个轻量级的目标检测模型。这篇文章就把我从零搭建这套系统的完整过程拆开讲,包括方案选型时踩过的坑、编码参数怎么调、Web端怎么做到低延迟播放,以及那些文档里不会写的实操细节。
适合谁来参考?如果你手上有庐山派K230或者类似的RISC-V AI开发板,想做一个自己完全掌控的Web端监控系统,或者你想把摄像头的视频流嵌入到自己的Web应用里,这篇内容应该能帮你省下不少试错时间。即使你用的是树莓派或者其他Linux开发板,里面的编码和传输思路也是通用的。
2. 整体方案设计与技术选型拆解
2.1 为什么不用RTSP加转码那一套
一提到网络摄像头,很多人第一反应是RTSP推流,然后前端用flv.js或者hls.js去拉。这套方案在传统安防领域确实成熟,海康、大华的摄像头默认就吐RTSP流。但放到K230这种嵌入式板子上,问题就来了。
RTSP本身只是一个控制协议,真正的视频数据走的是RTP。你要在浏览器里播放,中间必须有一个转码或者转封装的服务。如果摄像头输出的是H.264,你可以用RTSP转WebRTC或者转fMP4,但这一步在K230上跑起来很吃力。我实测过在K230上跑一个RTSP服务器加转封装进程,CPU直接飙到70%以上,而且延迟很难压到200毫秒以内,因为RTSP的缓冲机制天然就会引入几百毫秒的延迟。
另一个思路是用Mjpeg。Mjpeg的好处是浏览器原生支持,一个<img>标签就能显示,实现极其简单。但代价是带宽爆炸。720P分辨率下,Mjpeg的码率轻松跑到20Mbps以上,而且每一帧都是独立JPEG,压缩效率远不如H.264。局域网里玩玩还行,稍微远一点或者多路同时看就扛不住了。
我最终选的是H.264编码加WebSocket传输加前端MSE播放这条路线。具体来说:K230的VPU硬件编码器把摄像头采集的NV12数据编码成H.264裸流,通过WebSocket推送到浏览器,前端用Media Source Extensions把H.264数据喂给<video>标签。这条链路的好处是延迟极低,因为WebSocket是全双工的,数据一到就能推,不需要等一个完整的GOP。而且H.264的压缩效率足够高,720P下2Mbps就能有不错的画质。
2.2 硬件选型:摄像头模组怎么挑
庐山派K230板载了一个MIPI CSI接口,官方配套的摄像头模组是OV5647。这颗传感器在树莓派社区里非常常见,500万像素,最高支持1080P30,价格便宜,驱动也成熟。我一开始用的就是它,但后来发现一个问题:OV5647的默认驱动在K230上输出的帧率不太稳定,而且低光照下的表现一般。
如果你对手动对焦或者低光性能有要求,可以考虑IMX219或者IMX477。IMX219是树莓派Camera V2用的传感器,800万像素,驱动在K230的SDK里也有支持。IMX477更贵一些,但低光表现明显更好。不过要注意,换传感器意味着你要重新编译内核驱动,K230的SDK里虽然带了这些驱动,但默认的设备树配置可能不包含,需要自己改。
我最后用的是OV5647,原因很简单:便宜、够用、驱动最成熟。对于监控场景来说,500万像素绰绰有余,1080P30的规格也完全满足实时预览的需求。如果你要做AI分析,OV5647的输出直接喂给NPU也够用。
注意:K230的MIPI CSI接口对排线比较敏感,插拔的时候一定要断电操作,而且排线的金手指方向不能搞反。我见过有人带电插拔直接把CSI控制器烧了的,修起来很麻烦。
2.3 软件栈:从V4L2到WebSocket的完整链路
K230的SDK基于Linux,摄像头采集走的是标准的V4L2框架。整个软件栈我分成了四个层次:
第一层是采集层,用V4L2从/dev/video0读取NV12格式的帧数据。K230的ISP支持多种输出格式,我选NV12是因为VPU编码器对NV12的支持最好,不需要额外的格式转换。
第二层是编码层,调用K230的VPU硬件编码器,把NV12帧编码成H.264。K230的SDK里提供了k230_vpu相关的API,也可以走标准的V4L2 M2M接口。我用的是SDK提供的封装库,因为直接操作VPU寄存器太底层了,没必要。
第三层是传输层,用一个轻量级的WebSocket服务器把H.264裸流推给浏览器。这里我没有用现成的WebSocket库,而是自己写了一个基于epoll的简单服务器,因为K230的资源有限,现成的库往往带了很多用不上的功能,编译出来体积大、内存占用高。
第四层是播放层,浏览器端用JavaScript接收WebSocket数据,通过MSE API喂给<video>标签。这里的关键是要处理好H.264的NALU边界,因为MSE需要的是完整的帧数据,而不是随便切的字节流。
3. 核心细节解析与实操要点
3.1 V4L2采集:参数配置与缓冲区管理
V4L2的采集流程说起来不复杂:打开设备、设置格式、申请缓冲区、入队、开始流、出队取帧、处理完再入队。但实际写代码的时候,有几个参数必须仔细调。
首先是像素格式。K230的ISP支持NV12、NV16、YUYV等多种格式。我选NV12,因为VPU编码器对NV12的支持最直接,不需要额外的色彩空间转换。如果你选YUYV,编码前还得转一次,白白浪费CPU。
其次是缓冲区数量。V4L2的缓冲区数量直接影响延迟和丢帧率。缓冲区太少,采集和编码之间容易打架;缓冲区太多,延迟会累积。我实测下来,4个缓冲区是一个比较平衡的值。少于3个容易丢帧,多于6个延迟会明显增加。
struct v4l2_requestbuffers req = {0}; req.count = 4; req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory = V4L2_MEMORY_MMAP; ioctl(fd, VIDIOC_REQBUFS, &req);然后是帧率设置。V4L2的帧率通过VIDIOC_S_PARM设置,但要注意K230的ISP实际输出帧率可能和设定值有偏差。我设定的是30fps,实际测下来在28到30之间波动,这个偏差在可接受范围内。
实操心得:V4L2的
VIDIOC_DQBUF默认是阻塞模式,如果你在单线程里做采集和编码,阻塞模式没问题。但如果你想用多线程,记得把fd设成非阻塞,然后用poll或者select来等帧。我一开始用阻塞模式加多线程,结果采集线程经常卡住,后来改成非阻塞加poll才稳定下来。
3.2 H.264编码参数:码率、GOP和延迟的三角关系
VPU编码器的参数调优是整套系统里最影响体验的部分。K230的VPU支持H.264和H.265,我选H.264是因为浏览器的MSE对H.264的支持最广泛,H.265虽然压缩效率更高,但很多浏览器还不支持。
码率方面,720P分辨率下我设的是2Mbps,1080P设的是4Mbps。这个码率在局域网里完全够用,画质也说得过去。如果你要在公网上传,可以适当降低,但低于1Mbps之后画质下降会很明显。
GOP长度是影响延迟的关键参数。GOP越长,I帧越少,压缩效率越高,但一旦丢包,恢复时间也越长。对于实时监控场景,我建议把GOP设在15到30帧之间。我设的是15,也就是每半秒一个I帧。这样即使丢了一个I帧,最多半秒就能恢复。
编码模式方面,VPU支持CBR和VBR。CBR是恒定码率,VBR是可变码率。监控场景我建议用CBR,因为码率稳定,网络传输更好控制。VBR在画面剧烈变化的时候码率会飙升,容易把网络打满。
// 编码参数配置示例 vpu_enc_param_t param; param.width = 1280; param.height = 720; param.bitrate = 2048; // 2Mbps param.gop = 15; param.rc_mode = VPU_RC_CBR; param.profile = VPU_H264_PROFILE_MAIN;还有一个容易被忽略的参数是B帧。B帧能提高压缩效率,但会引入额外的编码延迟,因为编码器需要等后面的帧才能编码当前帧。实时监控场景我建议关闭B帧,只用I帧和P帧。这样编码延迟最低,解码端也不需要额外的缓冲。
3.3 WebSocket传输:分片、粘包与背压处理
WebSocket传输H.264裸流,最大的坑是粘包和分片。TCP是字节流协议,WebSocket虽然基于消息,但如果你发送的消息太大,底层还是会分片。浏览器收到的可能是一个不完整的NALU,或者多个NALU粘在一起。
我的处理方式是:每个WebSocket消息只发一个完整的NALU。K230的VPU编码器输出的每一帧数据,我会先解析出NALU边界,然后逐个发送。NALU的分隔符是0x00000001或者0x000001,解析的时候要注意这两种情况。
// 简单的NALU分割逻辑 uint8_t *start = frame_data; uint8_t *end = frame_data + frame_size; while (start < end) { // 查找起始码 uint8_t *nalu_start = find_start_code(start, end); if (!nalu_start) break; uint8_t *nalu_end = find_start_code(nalu_start + 4, end); if (!nalu_end) nalu_end = end; // 发送这个NALU ws_send(nalu_start, nalu_end - nalu_start); start = nalu_end; }背压处理是另一个关键点。如果浏览器端的消费速度跟不上K230的发送速度,WebSocket的发送缓冲区会堆积,最终导致内存暴涨或者连接断开。我的做法是:在应用层维护一个发送队列,如果队列长度超过阈值(比如10帧),就主动丢弃最旧的P帧,只保留最新的I帧。这样虽然会丢一些帧,但能保证连接不断,而且画面能快速恢复。
注意:丢弃P帧的时候一定要小心,不能随便丢。如果丢了一个P帧,后面的P帧解码会出错,直到下一个I帧才能恢复。所以要么不丢,要么丢到下一个I帧之前的所有P帧。我一般是检测到队列积压时,直接清空队列,然后等下一个I帧重新开始。
3.4 前端MSE播放:从WebSocket到video标签
浏览器端的逻辑比后端简单,但也有一些细节要注意。MSE的SourceBuffer需要的是fMP4格式的数据,而不是裸的H.264流。所以K230发送的H.264数据,前端需要先封装成fMP4才能喂给SourceBuffer。
我用的方案是在前端用JavaScript做fMP4封装。具体来说,就是构造一个简单的MP4容器,把H.264的SPS、PPS和每一帧数据按照MP4的格式打包。这部分代码稍微有点长,但逻辑不复杂,核心就是构造moov、moof和mdat这几个box。
// 简化的fMP4封装逻辑 function createInitSegment(sps, pps) { // 构造ftyp box // 构造moov box,包含avcC配置 // 返回初始化片段 } function createMediaSegment(nalus, timestamp) { // 构造moof box // 构造mdat box,包含NALU数据 // 返回媒体片段 }延迟优化方面,MSE的SourceBuffer有一个mode属性,可以设成segments或者sequence。segments模式是默认的,适合点播场景;sequence模式适合直播场景,延迟更低。我设的是sequence模式。
另外,<video>标签的latencyHint属性也可以设成interactive,告诉浏览器这是一个交互式场景,浏览器会尽量减少缓冲。不过这个属性目前支持度还不太高,主要靠SourceBuffer的模式来控制。
4. 完整实操流程与核心环节实现
4.1 环境搭建:SDK编译与依赖安装
庐山派K230的SDK是基于Buildroot的,官方提供了完整的编译工具链。我拿到板子之后,第一步是编译SDK,生成内核和根文件系统。
# 下载SDK git clone https://github.com/kendryte/k230_sdk.git cd k230_sdk # 配置编译选项 make menuconfig # 编译 make -j$(nproc)编译过程大概需要20到30分钟,取决于你的电脑性能。编译完成后,会在output目录下生成images文件夹,里面包含sysimage-sdcard.img,直接烧录到SD卡就能启动。
实操心得:K230的SDK编译对内存要求比较高,建议至少16GB内存。我一开始在8GB的虚拟机上编译,经常在链接阶段被OOM Killer杀掉。后来加到16GB才顺利编译完成。
启动之后,你需要确认几件事:摄像头设备节点是否存在(/dev/video0)、VPU设备是否正常(/dev/vpu)、网络是否连通。如果摄像头设备不存在,可能是设备树没有配置对,需要检查k230_canmv.dts里的CSI节点。
4.2 采集与编码程序的编写
采集和编码我写在一个C程序里,主循环大概是这样的:
// 初始化V4L2 int v4l2_fd = v4l2_init("/dev/video0", 1280, 720, V4L2_PIX_FMT_NV12); // 初始化VPU编码器 vpu_enc_handle_t enc = vpu_enc_init(1280, 720, 2048, 15); // 初始化WebSocket服务器 ws_server_t *server = ws_server_init(8080); while (running) { // 从V4L2取一帧 struct buffer *buf = v4l2_dequeue(v4l2_fd); // 送给VPU编码 vpu_enc_encode(enc, buf->data, buf->size); // 取编码后的H.264数据 uint8_t *h264_data; size_t h264_size; while (vpu_enc_get_frame(enc, &h264_data, &h264_size)) { // 分割NALU并发送 send_nalus(server, h264_data, h264_size); } // 归还缓冲区 v4l2_queue(v4l2_fd, buf); }这个主循环是单线程的,采集、编码、发送都在一个线程里。这样做的好处是逻辑简单,不需要考虑线程间的同步问题。坏处是如果WebSocket发送阻塞了,采集也会跟着卡住。所以我前面提到的背压处理就很重要,发送队列满了就丢帧,不能让发送阻塞主循环。
如果你想让采集和发送解耦,可以把WebSocket发送放到单独的线程里,主循环只负责采集和编码,编码后的数据扔到一个环形缓冲区里,发送线程从环形缓冲区里取数据发送。这样即使网络卡顿,采集也不会受影响。
4.3 WebSocket服务器的实现细节
WebSocket服务器的实现我参考了RFC 6455的规范,核心是握手和帧解析两部分。
握手阶段,浏览器会发一个HTTP Upgrade请求,服务器需要计算Sec-WebSocket-Accept并返回。计算方法是:把客户端发来的Sec-WebSocket-Key加上固定的GUID字符串,做SHA1哈希,然后Base64编码。
// 计算Sec-WebSocket-Accept char *key = get_header(request, "Sec-WebSocket-Key"); char *combined = concat(key, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); unsigned char hash[20]; SHA1(combined, strlen(combined), hash); char *accept = base64_encode(hash, 20);帧解析阶段,WebSocket的帧格式是:第一个字节包含FIN位和opcode,第二个字节包含MASK位和payload长度,后面可能还有扩展长度字段和掩码键。服务器发送给浏览器的帧不需要掩码,所以发送逻辑比较简单,只需要构造帧头加上数据就行。
// 发送一个WebSocket二进制帧 void ws_send_binary(int fd, uint8_t *data, size_t len) { uint8_t header[10]; header[0] = 0x82; // FIN + binary opcode if (len < 126) { header[1] = len; write(fd, header, 2); } else if (len < 65536) { header[1] = 126; header[2] = (len >> 8) & 0xFF; header[3] = len & 0xFF; write(fd, header, 4); } else { header[1] = 127; // 8字节长度,大端序 for (int i = 0; i < 8; i++) { header[2 + i] = (len >> (56 - i * 8)) & 0xFF; } write(fd, header, 10); } write(fd, data, len); }注意:WebSocket的帧长度字段是大端序,写的时候别搞反了。我一开始用小端序写,浏览器一直报协议错误,查了半天才发现是字节序的问题。
4.4 前端页面的完整实现
前端页面我写得很简单,一个<video>标签加一段JavaScript。核心逻辑是:建立WebSocket连接,收到数据后判断是初始化片段还是媒体片段,然后喂给SourceBuffer。
<!DOCTYPE html> <html> <head> <title>K230 Web监控</title> </head> <body> <video id="player" autoplay muted playsinline></video> <script> const video = document.getElementById('player'); const mediaSource = new MediaSource(); video.src = URL.createObjectURL(mediaSource); let sourceBuffer; mediaSource.addEventListener('sourceopen', () => { sourceBuffer = mediaSource.addSourceBuffer('video/mp4; codecs="avc1.4D401F"'); sourceBuffer.mode = 'sequence'; const ws = new WebSocket('ws://' + location.host + '/stream'); ws.binaryType = 'arraybuffer'; ws.onmessage = (event) => { const data = new Uint8Array(event.data); if (sourceBuffer && !sourceBuffer.updating) { sourceBuffer.appendBuffer(data); } }; }); </script> </body> </html>codecs参数里的avc1.4D401F是H.264的编码配置,4D表示Main Profile,40表示Level 4.0,1F是具体的约束。这个参数必须和K230编码器输出的SPS、PPS匹配,否则浏览器会拒绝播放。如果你不确定,可以从SPS里解析出来,或者直接用avc1.4D401F这个通用值试试。
自动播放方面,浏览器对自动播放有严格限制,必须有muted属性才能自动播放。我加了muted和playsinline,前者是为了绕过自动播放限制,后者是为了在iOS上全屏播放。
5. 常见问题与排查技巧实录
5.1 画面卡顿、延迟越来越大的排查思路
这是最常见的问题,表现是刚开始画面流畅,跑几分钟之后延迟越来越大,最后卡住不动。根本原因通常是消费速度跟不上生产速度,数据在某个环节堆积了。
排查的时候按链路逐段检查:
| 排查环节 | 检查方法 | 常见原因 |
|---|---|---|
| V4L2采集 | 看/proc/v4l2或者自己打日志统计帧率 | 帧率设置过高,ISP输出跟不上 |
| VPU编码 | 统计编码前后帧数是否一致 | 编码器参数太激进,编码耗时过长 |
| WebSocket发送 | 统计发送队列长度 | 网络带宽不足,或者浏览器消费慢 |
| 前端播放 | 看SourceBuffer.buffered的范围 | 缓冲区堆积,没有及时清理 |
我的经验是,90%的延迟问题出在WebSocket发送环节。K230的CPU性能有限,如果WebSocket发送用了阻塞IO,一旦网络稍有波动,发送就会阻塞,进而拖慢整个主循环。改成非阻塞IO加发送队列之后,这个问题基本就解决了。
另外,前端的SourceBuffer也要注意清理。如果buffered的范围越来越大,说明播放速度跟不上追加速度,需要主动调用remove()把旧的缓冲删掉。
// 定期清理旧缓冲 setInterval(() => { if (sourceBuffer.buffered.length > 0) { const start = sourceBuffer.buffered.start(0); const end = sourceBuffer.buffered.end(0); if (end - start > 5) { // 缓冲超过5秒就清理 sourceBuffer.remove(start, end - 2); } } }, 1000);5.2 浏览器报错"无法访问摄像头"或"非安全上下文"
这个问题和K230本身没关系,是浏览器的安全策略导致的。Chrome和Safari都要求getUserMedia必须在HTTPS或者localhost下才能调用。如果你只是用WebSocket传视频,不调用getUserMedia,那HTTP也能用。但如果你在前端还想调用本地摄像头做对比或者切换,就必须上HTTPS。
解决方案有两个:一是给K230的Web服务器配一个自签名证书,用HTTPS访问;二是用localhost访问,但这样只能在本机测试,手机上看不了。我建议配自签名证书,虽然浏览器会报证书警告,但点继续访问之后功能都正常。
# 生成自签名证书 openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes然后在WebSocket服务器里加载证书,把ws://改成wss://。注意WebSocket的加密和HTTP的加密是分开的,如果你用wss://,服务器端也要用TLS。
5.3 画面花屏、绿屏或者只有一半
花屏问题通常和NALU分割有关。如果NALU分割不正确,浏览器拿到的H.264数据不完整,解码就会出错。常见的表现是画面下半部分绿屏,或者画面撕裂。
排查方法:把K230发送的H.264数据保存成文件,用ffplay播放看看是否正常。如果ffplay播放正常,说明编码没问题,问题出在传输或者前端封装;如果ffplay也花屏,说明编码参数有问题。
# 保存H.264裸流 ./capture > test.h264 # 用ffplay播放 ffplay test.h264如果ffplay播放正常但浏览器花屏,大概率是fMP4封装的问题。检查avcCbox里的SPS和PPS是否正确,以及每个mdat里的NALU是否完整。我遇到过一种情况是SPS和PPS没有在初始化片段里正确设置,导致浏览器解码器初始化失败,画面一直是绿的。
5.4 网络断开后无法自动重连
WebSocket断开后,前端需要自动重连,否则用户得手动刷新页面。重连逻辑很简单,在onclose事件里设置一个定时器,隔几秒重新连接。
let reconnectTimer; function connect() { const ws = new WebSocket('ws://' + location.host + '/stream'); ws.binaryType = 'arraybuffer'; ws.onclose = () => { reconnectTimer = setTimeout(connect, 2000); }; ws.onmessage = (event) => { // 处理数据 }; }但要注意,重连之后SourceBuffer可能需要重新初始化,因为新的连接会重新发送SPS和PPS。我的做法是每次重连都重新创建MediaSource和SourceBuffer,虽然会闪一下,但能保证解码器状态正确。
实操心得:K230作为服务器,如果客户端异常断开(比如手机锁屏),服务器端的socket可能不会立即收到FIN包,导致连接泄漏。我建议在服务器端加一个心跳机制,每隔30秒发一个ping帧,如果连续两次没有收到pong,就主动关闭连接。这样能及时释放资源,避免连接数堆积。
6. 性能优化与进阶玩法
6.1 把延迟从180毫秒压到120毫秒
我最初的版本端到端延迟在180毫秒左右,后来做了几项优化,压到了120毫秒。优化的思路是减少每一环的缓冲。
第一,V4L2的缓冲区从4个减到3个。少一个缓冲区,延迟减少大约一帧的时间,也就是33毫秒。但不能再少了,再少容易丢帧。
第二,VPU编码器关闭B帧,并且把编码器的输入缓冲设成1。这样编码器一收到帧就立即编码,不会等后面的帧。
第三,WebSocket发送改成非阻塞,并且设置TCP_NODELAY选项。TCP_NODELAY会禁用Nagle算法,小包立即发送,不会攒着等大包。这个选项对延迟的影响很大,能减少几十毫秒。
int flag = 1; setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));第四,前端SourceBuffer的mode设成sequence,并且把<video>的playbackRate设成1.0,不要用默认的缓冲策略。
这几项加起来,延迟从180毫秒降到了120毫秒左右。再往下压就比较困难了,因为H.264编码本身就有几帧的延迟,这是物理限制。
6.2 同时跑AI检测:NPU和VPU怎么分工
K230的NPU有6TOPS算力,跑一个YOLOv5s或者YOLOv8n完全没问题。如果你想在监控的同时做目标检测,可以把NPU和VPU并行使用。
具体做法是:V4L2采集到的NV12帧,一路送给VPU编码,另一路送给NPU做推理。NPU推理的结果(比如检测框)可以通过WebSocket一起发给前端,前端在<video>上面叠加一个<canvas>来画框。
// 采集到的帧同时送给VPU和NPU vpu_enc_encode(enc, buf->data, buf->size); npu_infer(npu, buf->data, buf->size, &result); // 把检测结果和视频帧一起发送 send_detection_result(server, &result);要注意的是,NPU推理会占用CPU和内存带宽,可能会影响VPU编码的性能。我实测下来,跑YOLOv8n的时候,VPU编码的帧率从30fps降到了25fps左右,但延迟没有明显增加。如果你对帧率要求高,可以降低NPU的推理频率,比如每两帧推理一次。
6.3 多路摄像头的扩展思路
K230只有一个MIPI CSI接口,原生不支持多路摄像头。但你可以通过USB扩展,K230有一个USB 2.0 Host接口,可以接USB摄像头。USB摄像头的采集也是走V4L2,设备节点是/dev/video1或者更高。
多路摄像头的挑战在于带宽和CPU。两路720P30的H.264编码,VPU可能扛不住。我的建议是:如果要多路,降低分辨率和帧率,比如两路640x480@15fps,这样VPU还能应付。或者一路用MIPI摄像头做高清采集,另一路用USB摄像头做低清采集,分工使用。
前端方面,可以用多个<video>标签分别播放不同的流,或者用一个<canvas>把多路画面拼接起来。拼接的好处是只需要一个<video>标签,但需要在前端做图像合成,对浏览器性能有一定要求。
6.4 录像与回放:把H.264流存成MP4
监控系统通常还需要录像功能。K230上可以直接把H.264裸流存成文件,但裸流文件不方便播放,最好封装成MP4。封装MP4可以在K230上做,也可以在前端做。
在K230上封装MP4,我推荐用ffmpeg的命令行工具,但K230的存储空间有限,长时间录像需要外接存储或者定期清理。另一种做法是只存H.264裸流,回放的时候用前端封装成fMP4播放,这样K230端的逻辑最简单。
# 用ffmpeg把H.264裸流封装成MP4 ffmpeg -framerate 30 -i test.h264 -c copy output.mp4如果要在K230上实时封装,可以用libavformat库,但编译出来的二进制体积会比较大。我个人的做法是:K230只负责采集和编码,录像数据通过WebSocket发给一个后端服务,由后端服务负责存储和封装。这样K230的负担最轻,扩展性也最好。
7. 我踩过的那些坑和最后的小技巧
整套系统搭下来,前前后后花了大概两周时间,大部分时间不是在写代码,而是在排查各种奇怪的问题。有几个坑我印象特别深,这里分享一下,希望能帮你少走弯路。
第一个坑是V4L2的格式协商。K230的ISP支持多种格式,但并不是所有格式都能被VPU编码器接受。我一开始设的是YUYV,结果VPU编码器报错,说不支持这个格式。后来改成NV12才正常。所以选格式的时候,一定要先确认VPU支持哪些输入格式,不要想当然。
第二个坑是WebSocket的掩码。客户端发给服务器的帧必须带掩码,服务器发给客户端的帧不能带掩码。我一开始在服务器端也加了掩码,结果浏览器直接断开连接,报协议错误。查了RFC才知道,服务器到客户端的帧是禁止掩码的。
第三个坑是MSE的codecs参数。这个参数必须和实际的H.264流匹配,否则浏览器会拒绝播放。我一开始随便填了一个avc1.42E01E,结果浏览器报MEDIA_ERR_SRC_NOT_SUPPORTED。后来从SPS里解析出实际的profile和level,填了avc1.4D401F才正常。
最后分享一个小技巧:如果你觉得前端封装fMP4太麻烦,可以用WebRTC代替WebSocket加MSE。WebRTC原生支持H.264,延迟更低,而且不需要手动封装fMP4。但WebRTC的信令比较复杂,需要额外的信令服务器,而且K230上跑WebRTC的库比较重。如果你追求极致延迟,可以试试WebRTC;如果追求简单可靠,WebSocket加MSE这套方案已经足够好了。
这套系统我目前跑了大概一个月,稳定性还不错,每天24小时运行,偶尔会有一次WebSocket断开,但前端会自动重连,基本不需要人工干预。K230的发热量不大,不加散热片也能稳定运行,夏天室温30度的时候,芯片温度在60度左右,还在安全范围内。如果你打算长期运行,建议加一个小散热片,心里更踏实。