使用 multipart/x-mixed-replace 实现 http 实时视频流

更新日期: 2019-03-18阅读: 3272标签: 视频

关于实时视频传输,业界已经有非常多成熟方案,分别应用在不同需求场景。本文介绍一种基于 HTTP ,非常简单、易理解的方案,实用性不强,但有助于理解 HTTP 协议。

从摄像头读取视频帧

node 的硬件操作能力偏弱,运行时本身并没有提供太多硬件接口,所以要调用硬件设备需要找到合适的库。

有许多工具可以实现从摄像头读取视频流,简单起见,我们选用了比较通用的框架: OpenCV,这是一个 c++ 写的计算机视觉处理工具,包含了各类图像、视频处理功能,对应的 node 版本:node-opencv,安装过程比较繁琐,在 windows 下容易出错,建议参考官网提供的教程(熟悉 docker 的同学,可以使用 node-opencv 镜像)。

视频是一个非常复杂的概念,简单起见,本例中仅通过取帧的方式实现,也就是间隔一段时间从摄像头读取当前图像,连续多张图就构成了一个视频。实现这一点,可以通过调用 OpenCV 的 VideoCapture 类获取帧,代码

import { promisfy } from "promisfy";
import cv from "opencv";

const video = new cv.VideoCapture(0);
const read = promisfy(video.read, video);
setInterval(() => {
  const frame = await read();
  console.log(frame.length);
}, 100);

代码很简单,新建 VideoCapture 实例后,调用 read 接口读取当前帧。


使用 multipart 实现响应流

有了视频帧之后,接下来的问题就是如何传输到客户端,这里有很多成熟的传输技术,包括: HLSRTSPRTMP等。这些技术有一定的复杂性,各自有其适用场景,如果业务场景对实时性、性能没有太高要求,那显得有点牛刀杀鸡了。有一个更简单,对前端更友好的方案: http 的 multipart 类型。

multipart 通过 content-type 头定义。这里稍微解释一下,content-type 用于声明资源的媒体类型浏览器会根据媒体类型的值做出不同动作。比如,通常来说,chrome 遇到application/zip会下载资源;遇到application/pdf会启动预览,正是通过判断这个头部做出的分支选择。

而 multipart 类型值声明服务器会将 多份数据 合并成当个请求。比较常见的例子是 form 表单提交,浏览器默认的 form 表单提交行为就是通过指定 content-type: multipart/form-data; boundary=xxx 头,服务器接收到后会根据 boundary 分割内容,提取多个字段。规范文档 rfc1341 指定了四种子类型:multipart/mixed、multipart/alternative、multipart/digest、multipart/parallel,主流浏览器则扩展了一种新的类型: multipart/x-mixed-replace(不过由于很少用到这个特性,而且实现上容易出安全问题,MDN 已经标志为过期特性),该类型声明 body 内容由多份 XML 文档按序组合组合而成,每次到来的文档都会被用于创建新的 dom 文档对象,并触发文档对象 onload 事件。下面,我们尝试构建一个最简单的 multipart 响应流:

import { Readable } from "stream";
import http from "http";

const boundary = "gc0p4Jq0M2Yt08jU534c0p";

// 必须使用流的方式实现
// 否则res会提前关闭
// 可读流的实现,请参考:https://nodejs.org/api/stream.html#stream_implementing_a_readable_stream
class MockStream extends Readable {
  constructor(...arg) {
    super(...arg);
    this.count = 0;
  }
  async _read() {
    const buffer = Buffer.concat([
      new Buffer(`--${boundary}\r\n`),
      new Buffer("Content-Type: text/html\r\n\r\n"),
      new Buffer(`<html><body>${this.count}</body></html>\r\n\r\n`)
    ]);
    this.count++;
    setTimeout(() => {
      this.push(buffer);
    }, 1000);
  }
}

http
  .createServer((req, res) => {
    // 首先输出响应头
    res.writeHead(200, {
      "Content-Type": `multipart/x-mixed-replace; boundary="${boundary}"`
    });
    const stream = new MockStream();
    stream.pipe(res);
  })
  .listen(3000);

上例实现了在一次 response 中返回多份 html 文档,返回结构大致如下:

HTTP/1.0 200 OK
Content-Type: multipart/x-mixed-replace; boundary=gc0p4Jq0M2Yt08jU534c0p
X-Request-ID: bcd9f083-af7a-4419-94bd-0e47851a542d
Date: Tue, 12 Mar 2019 05:04:39 GMT

--gc0p4Jq0M2Yt08jU534c0p
Content-Type: text/html

<html><body>0</body></html>

--gc0p4Jq0M2Yt08jU534c0p
Content-Type: text/html

<html><body>1</body></html>

...

与常见的 http 响应相比,上例有两个特点。第一,在 header 中并没有指明 content-length 头,客户端无法预知资源大小,按规范,在这条 TCP 连接中断之前所传输过来的数据都是本次响应的内容,这个特性可以用于构建一个持久、可扩展的响应流,非常契合实时视频传输场景。第二点是,response 的 body 部分由多份资源按序排列而成,并使用 boundary 字符串标志资源的分割点,客户端可以使用 boundary 字符串抽取、解析出每一份资源的内容。比如在 nodejs 环境下:

import http from "http";
import fs from "fs";

const req = http.request("http://localhost:3000/api/video", res => {
  let cache = new Buffer(0);
  res.on("data", part => {
    cache = Buffer.concat([cache, part]);
    cache = retriveFrame(cache);
  });
});

req.end();

function retriveFrame(buff) {
  const boundary = new Buffer("gc0p4Jq0M2Yt08jU534c0p");
  const index = buff.indexOf(boundary);

  // 当前读取到的buff还未到达分割点
  if (index === -1) {
    return buff;
  }

  // 以boundary为界,分割出帧
  const frame = buff.slice(0, index);
  console.log(frame);

  // 保留解析出帧后,剩余的部分
  // 供下次解析
  return buff.slice(index + boundary.length);
}

传输视频流

有了视频帧,有了传输手段,接下来我们看看如何传输视频流。首先,我们需要构造服务端:

import cv from "opencv";
import { promisfy } from "promisfy";
import { Readable } from "stream";
import fs from "fs";
import http from "http";

const boundary = "gc0p4Jq0M2Yt08jU534c0p";

class VideoStream extends Readable {
  constructor(opt) {
    super(opt);
    this._vid = opt.vid;
  }
  async _read() {
    const vid = this._vid;
    const read = promisfy(vid.read, vid);
    const frame = await read();
    const buffer = Buffer.concat([
      new Buffer(`--${boundary}\r\n`),
      new Buffer("Content-Type: image/jpeg\r\n\r\n")
    ]);
    const result = Buffer.concat([buffer, frame.toBuffer()]);
    this.push(result);
  }
}

http
  .createServer((req, res) => {
    res.writeHead(200, {
      "Content-Type": `multipart/x-mixed-replace; boundary="${boundary}"`
    });
    const vid = new cv.VideoCapture(0);
    const stream = new VideoStream({ vid });
    stream.pipe(res);
  })
  .listen(4000);

代码逻辑分两部分,一是持续调用 OpenCV 接口,读取摄像头帧;二是通过 stream 形式,将图片通过 http 协议输出到客户端。只要客户端支持 multipart/x-mixed-replace 头,就可以从响应中读取视频帧,chrome、Firefox 在这一点上有比较好的支持,只要使用 <img /> 标签就可以实现视频流效果:

<img src="http://localhost:4000/" />

上例代码已经放在仓库 node-case 上,欢迎取阅。


总结

本文提供了一个简单的视频直播方案,有两个重点,一是在 node 环境下如何获取摄像头帧;二是如果通过一个简单的 HTTP 响应传输视频帧。胜在简单、直观,但存在许多问题:

  1. OpenCV 编解码效率并不高,替代方案是 FFMPEG,本文未涉及
  2. multipart/x-mixed-replace 是单次 http 请求-响应模型,如果网络中断,会导致视频流异常终止,必须重新连接
  3. 无法同时输出音频

针对专业、高性能要求的场景,建议还是使用专用协议,如 HLS、RTSP 等。

来自:https://segmentfault.com/a/1190000018563132


链接: https://www.fly63.com/article/detial/2427

h5页面自动播放视频、音频_关于媒体文件自动全屏播放的实现方式

在移动端(ios和android)播放视频的时候,我们即使定义了autoplay属性,仍然不能自动播放。这是由于手机浏览器为了防止浪费用户的网络流量,在默认情况下是不允许媒体文件自动播放的,除非用户自己对浏览器进行设置才能支持autoplay。

如何将视频设置为网页背景【转】

有时候为一个网页添加一个动画效果的背景,会让网页增加一定的韵味,让网页看起来与众不同。需要用到了video/标签,然后在source里面写视频的路径,autoplay用来使其自动播放,muted用来使其静音,loop为循环播放

js实现截取视频帧图片作为封面预览图

前端需要把视频文件的第一帧图像截取出来,并做为缩略图显示在页面上,这里需要利用HTML5中强大的画布canvas来实现该功能

优化 MP4 视频以便更快的网络串流

随着 Flash 的落寞 以及 移动设备的爆发性增长 ,越来越多的内容以 HTML5 视频的方式传递。在上一篇文章中你甚至能看到 使用 HTML5 视频替换 GIF 动图来优化网站访问速度 这样的技巧

js获取上传音视频的时长

获取上传视频路径,将该路径放入video标签,获取视频时长。方式一:隐藏一个音频标签,播放获取。方式二;通过new Audio的方式获取。上传之前限制一下视频的时长

移动端视频h5表现问题汇总

同屏播放视频、移动端视频预加载:由于移动端不能预加载视频,所以hack一种方案:监听WXJSBridge WeixinJSBridgeReady、微信安卓环境下需要在touchmove事件中阻止掉默认事件,否则不能触发视频播放 、 由于微信安卓版本基于x5内核,视频会出现全屏按钮,而且去不掉,会误导用户点击

Web端直接播放 .ts 视频

最近项目中需要前端播放 .ts 格式视频,捣鼓了几天学习到很多知识,也发掘了一种优秀的解决方案,项目中已存储的 .ts 切片数量众多,已经占用了NAS服务器绝大部分的资源,生成的 .m3u8 索引虽然非常小

h5视频播放踩坑记录

随着抖音、快手这类的视频类app的火爆,移动端h5视频类应用也随之兴起,使用video播放的场景也越来越多,本篇文章主要例举了移动端视频播放的一些场景和个人在开发过程中遇到的一些问题

如何实现沉浸式视频体验?

沉浸式视频体验,大致内容是一个页面里有几十个视频,用户点击其中一个视频时,该视频自动滑动到屏幕可视区域的顶部开始播放,并暂停其他视频,该视频滑出屏幕可视区域之后要自动暂停。

视频全屏弹幕实现

这个问题如果思路错了就是个死胡同。所以我在找问题的时候趟了很多坑。思路错在我不应该使用视频的全屏,敲黑板!划重点啦!不要使用视频的全屏!(解决了问题的时候才幡然醒悟)

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!