如何通过纯JavaScript接收multipart/x-mixed-replace响应实现视频流渲染
核心前提说明
- 原生
EventSource(即SSE客户端API)仅支持解析text/event-streamMIME类型的响应,和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仅支持文本格式传输,无法直接传递二进制图像:
- 服务端将采集到的每一帧JPEG图像做Base64编码
- 响应头设置为
Content-Type: text/event-stream - 按照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
相关产品推荐
相关产品推荐

