从摄像头到后端:前端视频帧采集、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"
}

⚠️ 注意两点:

  1. 不是所有浏览器都支持 codec 参数
    • Chrome 支持 vp9 / av1
    • 老 Android WebView 基本只有 h264 / vp8
  2. getUserMedia 只是“采集”,真正编码发生在 MediaRecorder 或 Canvas 导出

二、Blob 到底是什么?

MediaRecordercanvas.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()

十二、总结一句话

前端处理视频,不是“会播放”就行,而是:

  1. Camera → Canvas:拿像素
  2. RGBA → RGB:砍体积
  3. pako:再砍一截
  4. 分块 + binary WebSocket:不断连
  5. 多端用不同 API:H5 / App / 小程序各走各的
posted on 2026-08-13 15:58  羽丫头不乖  阅读(4)  评论(0)    收藏  举报