You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过纯JavaScript接收multipart/x-mixed-replace响应实现视频流渲染

核心前提说明
  • 原生EventSource(即SSE客户端API)仅支持解析text/event-stream MIME类型的响应,和multipart/x-mixed-replace是HTTP长连接下两种完全独立的协议格式,无法直接用SSE API接收MJPEG视频流。
  • 浏览器原生的<img>、<object>标签内置了multipart/x-mixed-replace解析逻辑,不需要写任何JavaScript即可直接播放MJPEG流,这是性能最好、实现成本最低的方案,示例代码:
<img src="http://localhost:8080/camera" width="300" height="200" alt="摄像头实时流">

方案1:原生JS手动解析multipart/x-mixed-replace流

如果需要通过JavaScript控制流的加载、帧处理逻辑,可以用fetch + ReadableStream API手动逐块读取响应,按照multipart格式切分每一帧图像,渲染到img元素中,不需要修改服务端逻辑。

页面元素准备

<div>
  实时摄像头流:<img id="camera-stream" width="300" height="200" alt="摄像头流">
</div>

JavaScript实现代码

const STREAM_URL = "http://localhost:8080/camera";
const streamImg = document.getElementById("camera-stream");

// 工具函数:在二进制数组中查找子序列位置
function findSubArray(buffer, sub) {
  const subLen = sub.length;
  for (let i = 0; i <= buffer.length - subLen; i++) {
    let isMatch = true;
    for (let j = 0; j < subLen; j++) {
      if (buffer[i + j] !== sub[j]) {
        isMatch = false;
        break;
      }
    }
    if (isMatch) return i;
  }
  return -1;
}

async function startMjpegStream() {
  const resp = await fetch(STREAM_URL);
  if (!resp.ok) throw new Error(`流请求失败,状态码:${resp.status}`);

  // 从响应头提取multipart边界标识
  const contentType = resp.headers.get("Content-Type");
  const boundaryReg = /boundary=(?:"([^"]+)"|([^;]+))/i;
  const boundaryMatch = contentType.match(boundaryReg);
  if (!boundaryMatch) throw new Error("响应头无合法multipart边界,非MJPEG流");
  const boundary = new TextEncoder().encode(`--${boundaryMatch[1] || boundaryMatch[2]}`);
  const blockEnd = new TextEncoder().encode("\r\n\r\n");

  const reader = resp.body.getReader();
  let buffer = new Uint8Array(0);

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    // 拼接新读取的二进制块到缓冲区
    const mergedBuffer = new Uint8Array(buffer.length + value.length);
    mergedBuffer.set(buffer);
    mergedBuffer.set(value, buffer.length);
    buffer = mergedBuffer;

    // 按边界切分独立帧
    let boundaryPos;
    while ((boundaryPos = findSubArray(buffer, boundary)) !== -1) {
      const frameBlock = buffer.slice(0, boundaryPos);
      buffer = buffer.slice(boundaryPos + boundary.length);

      // 跳过空块
      if (frameBlock.length < blockEnd.length) continue;

      // 定位帧头结束位置,提取JPEG二进制数据
      const headerEndPos = findSubArray(frameBlock, blockEnd);
      if (headerEndPos === -1) continue;
      // 去掉帧末尾的\r\n分隔符
      const jpegBinary = frameBlock.slice(headerEndPos + blockEnd.length, frameBlock.length - 2);

      // 生成BlobURL赋值给img元素,释放上一帧URL避免内存泄漏
      const frameBlob = new Blob([jpegBinary], { type: "image/jpeg" });
      const frameUrl = URL.createObjectURL(frameBlob);
      if (streamImg.src.startsWith("blob:")) URL.revokeObjectURL(streamImg.src);
      streamImg.src = frameUrl;
    }
  }
}

startMjpegStream().catch(err => console.error("流加载异常:", err));

方案2:基于SSE实现(需服务端配合改造)

如果一定要使用SSE技术栈,必须修改服务端的响应逻辑,因为SSE仅支持文本格式传输,无法直接传递二进制图像:

  1. 服务端将采集到的每一帧JPEG图像做Base64编码
  2. 响应头设置为Content-Type: text/event-stream
  3. 按照SSE规范返回消息,格式为data: [Base64编码后的帧字符串]\n\n

前端实现代码

const streamImg = document.getElementById("camera-stream");
const sseClient = new EventSource("http://localhost:8080/camera-sse");

sseClient.onmessage = (event) => {
  // event.data为服务端返回的Base64帧数据
  streamImg.src = `data:image/jpeg;base64,${event.data}`;
};

sseClient.onerror = (err) => {
  console.error("SSE流异常:", err);
};

注意:该方案Base64编码会带来33%左右的额外带宽开销,延迟也高于原生MJPEG流,非特殊场景不推荐使用。


常见注意事项

  • 若前端页面和流服务不同源,需要服务端配置正确的CORS响应头,否则浏览器会拦截跨域请求
  • 手动解析流时必须及时回收上一帧的Blob URL,否则长时间运行会造成内存泄漏
  • 上述原生JS方案支持所有现代浏览器,不兼容IE

内容的提问来源于stack exchange,提问作者Hasan A Yousef

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 17:51:23