关于实时视频传输,业界已经有很是多成熟方案,分别应用在不一样需求场景。本文介绍一种基于 HTTP ,很是简单、易理解的方案,实用性不强,但有助于理解 HTTP 协议。javascript
node 的硬件操做能力偏弱,运行时自己并无提供太多硬件接口,因此要调用硬件设备须要找到合适的库。html
有许多工具能够实现从摄像头读取视频流,简单起见,咱们选用了比较通用的框架: OpenCV,这是一个 c++ 写的计算机视觉处理工具,包含了各种图像、视频处理功能,对应的 node 版本:node-opencv,安装过程比较繁琐,在 windows 下容易出错,建议参考官网提供的教程(熟悉 docker 的同窗,可使用 node-opencv 镜像)。前端
视频是一个很是复杂的概念,简单起见,本例中仅经过取帧的方式实现,也就是间隔一段时间从摄像头读取当前图像,连续多张图就构成了一个视频。实现这一点,能够经过调用 OpenCV 的 VideoCapture
类获取帧,代码:java
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 接口读取当前帧。node
有了视频帧以后,接下来的问题就是如何传输到客户端,这里有不少成熟的传输技术,包括: HLS、RTSP、RTMP等。这些技术有必定的复杂性,各自有其适用场景,若是业务场景对实时性、性能没有过高要求,那显得有点牛刀杀鸡了。有一个更简单,对前端更友好的方案: http 的 multipart 类型。c++
multipart 经过 content-type 头定义。这里稍微解释一下,content-type 用于声明资源的媒体类型,浏览器会根据媒体类型的值作出不一样动做。好比,一般来讲,chrome 遇到application/zip
会下载资源;遇到application/pdf
会启动预览,正是经过判断这个头部作出的分支选择。git
而 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 响应流:github
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 文档,返回结构大体以下:chrome
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 环境下:docker
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 响应传输视频帧。胜在简单、直观,但存在许多问题:
multipart/x-mixed-replace
是单次 http 请求-响应模型,若是网络中断,会致使视频流异常终止,必须从新链接针对专业、高性能要求的场景,建议仍是使用专用协议,如 HLS、RTSP 等。