从摄像头到后端:前端视频帧采集、RGB 提取与压缩传输全链路
做设备端、工控屏、查验类项目时,最常见的需求不是“播放视频”,而是:
把摄像头里的画面,变成后端能算的数据
比如:
- 人脸比对
- OCR 识别
- OpenCV 处理
- 后端 AI 推理
前端要解决的问题其实是这一整条链路:
摄像头 → 视频帧 → RGB → 压缩 → 传输 → 后端
这篇文章把每个环节的关键坑点讲清楚。
一、第一步:拿到视频流,而不是“视频文件”
获取摄像头
最基础但最常用:
const stream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: 'user', // 前置
width: { ideal: 1280 },
height: { ideal: 720 },
frameRate: 25
}
});
你能控制什么编码参数?
{
codec: "vp8" | "vp9" | "h264" | "av1",
width: 1920,
height: 1080,
framerate: 30,
bitrate: 2500000,
colorSpace: "bt709"
}
⚠️ 注意两点:
- 不是所有浏览器都支持 codec 参数
- Chrome 支持
vp9/av1 - 老 Android WebView 基本只有
h264/vp8
- Chrome 支持
getUserMedia只是“采集”,真正编码发生在MediaRecorder或 Canvas 导出
二、Blob 到底是什么?
MediaRecorder 或 canvas.toDataURL() 最终给你的,通常是一个 Blob:
{
size: 1024000,
type: "video/webm",
[Symbol(type)]: "Blob"
}
Blob 里面有什么?
- 容器格式(WebM / MP4 / OGG)
- 编码后的帧(I 帧 / P 帧)
- 时间戳
- 分辨率、码率等元数据
比如 WebM:
EBML Header → Segment → Cluster → 视频帧
你拿不到“像素”,除非把它画到 Canvas 上。
三、从视频里“偷”一帧:Canvas 是桥梁
把 video 画到 canvas
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
此时你拿到了:
- 一张“静态图”
- 格式是 RGBA(每个像素 4 个字节)
四、RGBA → RGB:为什么后端总说“数据太大”?
getImageData 给你的是:
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// imageData.data 是 Uint8ClampedArray
// [R, G, B, A, R, G, B, A, ...]
问题 1:RGBA 体积太大
一张 1280×720 的图:
1280 × 720 × 4 = 3,686,400 字节 ≈ 3.5 MB
问题 2:JSON 不认识 Uint8Array
文档里踩坑总结得很清楚:
| 操作 | 后果 |
|---|---|
| 直接塞进 JSON | 浏览器把 Uint8Array 转成 object,体积暴涨 |
Array.from() |
1 个字节 → 1 个 number(8 字节),体积 ×8 |
| Base64 裸传 | 体积再膨胀 33% |
五、正确姿势:RGB + 压缩
1️⃣ RGBA → RGB(丢掉 Alpha)
const pixels = imageData.data;
const rgbData = new Uint8ClampedArray(canvas.width * canvas.height * 3);
let idx = 0;
for (let i = 0; i < pixels.length; i += 4) {
rgbData[idx++] = pixels[i]; // R
rgbData[idx++] = pixels[i + 1]; // G
rgbData[idx++] = pixels[i + 2]; // B
}
如果图片有透明背景,可以预乘白色:
const bg = [255, 255, 255];
for (let i = 0; i < pixels.length; i += 4) {
const a = pixels[i + 3] / 255;
rgbData[idx++] = pixels[i] * a + bg[0] * (1 - a);
rgbData[idx++] = pixels[i + 1] * a + bg[1] * (1 - a);
rgbData[idx++] = pixels[i + 2] * a + bg[2] * (1 - a);
}
2️⃣ 转成 Uint8Array(零拷贝)
const uint8 = new Uint8Array(rgbData.buffer);
✅ 这一步体积直接砍到原来的 75%
六、压缩:pako 是工控设备的好朋友
import pako from 'pako';
const compressed = pako.deflate(uint8, { level: 6 });
压缩后:
- 纯图像数据:压缩率 40–60%
- 如果画面简单(文档、证件):压缩率更高
后端解压
const restored = pako.inflate(compressed);
七、怎么发给后端?三种方案对比
❌ 方案 1:直接 WebSocket 发 RGB 数组
ws.send(uint8);
问题:
- 超过 1MB 浏览器直接断
- 老 WebView 不支持
⚠️ 方案 2:Base64 一次发完
ws.send(btoa(String.fromCharCode(...compressed)));
问题:
- 体积大
- 主线程卡顿
✅ 方案 3:分块 + 二进制帧(推荐)
function chunkData(data, size = 50 * 1024) {
const chunks = [];
for (let i = 0; i < data.length; i += size) {
chunks.push(data.slice(i, i + size));
}
return chunks;
}
ws.binaryType = 'arraybuffer';
chunkData(compressed).forEach(chunk => {
ws.send(chunk);
});
后端按 index / total 拼接即可。
八、Base64 的正确使用场景
Base64 不是不能用来传图,而是要看场景。
✅ 适合
- 单次上传(拍照)
- 接口调试
- 小程序 / uni-app 本地文件
// uni-app Android
plus.io.resolveLocalFileSystemURL(filePath, entry => {
entry.file(file => {
const reader = new plus.io.FileReader();
reader.readAsDataURL(file);
});
});
❌ 不适合
- 实时视频帧
- 连续推理
- 大分辨率
九、uni-app / 多端兼容的坑
文档里这段特别实用:
// H5
canvas.toDataURL()
// 微信小程序
wx.getFileSystemManager().readFile({ encoding: 'base64' })
// App
plus.io.FileReader
核心原则:
不要在 App 里指望 H5 的 Canvas API 性能
实时场景用原生插件,非实时用 Canvas 足够
十、一个完整的“采集 → 推理”数据结构
这是实战里最终用的格式:
{
width: 1280,
height: 720,
channel: 3,
data: Array.from(compressed)
}
后端(Python / OpenCV)可以直接:
import cv2
import numpy as np
import zlib
arr = np.frombuffer(zlib.decompress(compressed), dtype=np.uint8)
img = arr.reshape(height, width, 3)
cv2.imwrite('frame.jpg', img)
十一、WebSocket 连接状态别忘了
// 0 CONNECTING
// 1 OPEN
// 2 CLOSING
// 3 CLOSED
多端限制:
| 平台 | 限制 |
|---|---|
| H5 | 浏览器 TCP 连接数 |
| 小程序 | 单域名 5 个 |
| App | 无硬限,但吃内存 |
✅ 实时帧传输时:
- 一帧传完再发下一帧
- 断连自动重连
- 页面切后台主动
close()
十二、总结一句话
前端处理视频,不是“会播放”就行,而是:
- Camera → Canvas:拿像素
- RGBA → RGB:砍体积
- pako:再砍一截
- 分块 + binary WebSocket:不断连
- 多端用不同 API:H5 / App / 小程序各走各的
浙公网安备 33010602011771号