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分辨率下我设的是2Mbps1080P设的是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 titleK230 Web监控/title /head body video idplayer 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; codecsavc1.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 /htmlcodecs参数里的avc1.4D401F是H.264的编码配置4D表示Main Profile40表示Level 4.01F是具体的约束。这个参数必须和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可能扛不住。我的建议是如果要多路降低分辨率和帧率比如两路640x48015fps这样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度左右还在安全范围内。如果你打算长期运行建议加一个小散热片心里更踏实。