最近实验需要在树莓派上搭建一个简单的视频服务,而且,希望画质一定的情况下,消耗的带宽越少越好。关于带宽的问题,其实开始并没有考虑太多,但是在尝试用uv4l工具创建 mpeg 流的时候发现,尽管分辨率很低(720p)不到,需要的数据率却达到了大约 5MB/s。我们待测试的通信层不具备这样高的传输传输能力。因此需要想办法把数据率降下来。综上,我们需要产生一个编码后的视频流,如 H264。

幸运的是我发现了h264-live-player这个项目。这个项目是基于 Node.js 的工程,利用 Websocket 传输 H264 编码数据,在客户端用Broadway解码,而服务端的 H264 流通过raspivid产生。

在接下来的部分,我先简要介绍一下 Raspivid 的使用,然后介绍一下h264-live-player的情况。如果只是想上手使用,可以直接拉到最后。

1 Raspivid

raspivid是一个在树莓派上用于捕捉视频数据的命令行工具。在h264-live-player中,lib/raspivid.js文件调用了这个命令来产生 H264 的视频流。在这个文件中使用的命令是:

1
raspivid -t 0 -o - -w WIDTH -h HEIGHT -fps FPS

其中,-t 0表示捕捉的时间不限。-o -表示将 H264 流输出到stdout。后面的-w, -h, -fps则分别是制定画面的宽高还有帧率。在raspivid命令产生 H264 流后,h264-live-player会通过一系列的回调函数通过 Websocket 将 H264 数据发送给前端。

2 h264-live-player 关键代码解析

注意,原作者的工程里面存在一些问题,其中重点是客户端刷新后视频流解析会出现异常。我在我的 fork 中修复了这些问题,还做了一些其他的改进。因此这里的介绍都以我的 fork 中的代码为准。

2.1 后端

首先还是要看lib/raspivid.js这个文件。RpiServer这个类继承于ServerServer中预留了get_feed给子类实现,器作用是产生视频流。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
get_feed() {
if (this.streamer !== undefined) {
this.streamer.kill();
}
var msk = "raspivid -t 0 -o - -w %d -h %d -fps %d";
var cmd = util.format(msk, this.options.width, this.options.height, this.options.fps);
console.log(cmd);
var streamer = spawn('raspivid', ['-t', '0', '-o', '-', '-w', this.options.width, '-h', this.options.height, '-fps', this.options.fps, '-pf', 'baseline']);
streamer.on("exit", function(code){
if (code) {
console.log("Failure", code);
}
});
this.streamer = streamer;
return streamer.stdout;
}

这个函数返回的是raspivid子进程的stdout流,也即 H264 流。

然后我们来看lib/_server.js文件中_Server的定义。注意start_feed这个函数:

1
2
3
4
5
6
7
8
9
10
start_feed() {
if (this.readStream) {
this.readStream.end();
}
var readStream = this.get_feed();
this.readStream = readStream;

readStream = readStream.pipe(new Splitter(NALseparator));
readStream.on("data", this.broadcast);
}

这个函数在客户端发起播放流的请求后调用。这里Server调用子类实现的get_feed函数获取视频流,然后视频流上注册data事件的回调函数。

这里需要解释一下readStream = readStream.pipe(new Splitter(NALseparator));这行代码。这里我们为视频流增加了一个Splitter,生成Splitter的参数为一个Buffer

1
const NALseparator = new Buffer([0, 0, 0, 1]); //NAL break
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>
> 在H264规范中,帧中间的会插入`00 00 00 01`作为帧间隔标识。这里插入的`Splitter`的作用是,在每次遇到`NALseperator`形式的字符流时,将之前收到的数据作为一个`chunk`,调用`data`事件的回调函数。

再来看看`broadcast`函数。在视频流收到一定的函数时会调用这个函数:

```js
broadcast(data) {
this.wss.clients.forEach(function(socket) {
if (socket.readyState !== WebSocket.OPEN) {
return;
}
if(socket.buzy)
return;

socket.buzy = true;
socket.buzy = false;

socket.send(Buffer.concat([NALseparator, data]), { binary: true}, function ack(error) {
socket.buzy = false;
});
});
}

这里的代码非常简单,核心就是通过socket.send将数据发送给客户端。注意这里的数据的内容是Buffer.concat([NALseperator, data])。这是因为Splitter会截断分隔符。

2.2 前端

前端的代码集中在vendor/wsavc/index.js中。重点是下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var framesList = [];

this.ws.onmessage = (evt) => {
if (typeof evt.data == "string") return this.cmd(JSON.parse(evt.data));
this.pktnum++;
var frame = new Uint8Array(evt.data);
//log("[Pkt " + this.pktnum + " (" + evt.data.byteLength + " bytes)]");
//this.decode(frame);
framesList.push(frame);
};

var shiftFrame = function () {
if (!running) return;

if (framesList.length > 10) {
log("Dropping frames", framesList.length);
framesList = [];
}

var frame = framesList.shift();

if (frame) {
this.decode(frame);
}

requestAnimationFrame(shiftFrame);
}.bind(this);

shiftFrame();

在接收到服务器发送的数据时,数据会被转换成Uint8Array,然后压入到一个队列中。而在shiftFrame这个函数会周期性的调用,从队列中取出数据进行解码。解码后会触发Broadway解码器的onPictureDecoded回调,在这个回调中canvas中的图像会被更新。

3 h264-live-player 的部署和使用

3.1 安装 Node.js 到树莓派

SSH 登录到树莓派,然后运行

1
2
3
4
5
sudo apt-get update
sudo apt-get dist-upgrade

curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
sudo apt-get install -y nodejs

使用下面的命令来验证安装成功:

1
2
3
4
$ node -v
v8.14.1
$ npm -v # npm是Node.js的包管理器
6.4.1

3.2 安装 h264-live-player

1
2
3
4
5
6
# 下载仓库
git clone git@gitlab.vlionthu.com:tdma-uav/raspberry-pi-video-stream.git player

cd player
# 安装依赖
npm install

3.3 运行

1
2
cd player
node server_rpi.js

上面的运行方法会在 terminal 中启动服务脚本。如果要这个程序常驻后台,可以尝试使用pm2

1
2
3
4
5
6
7
8
9
10
sudo npm install -g pm2    # 安装pm2,这里的-g表示安装到全局环境下

cd player # cd to player folder

# 启动
pm2 start ./server-rpi.js \
-i 1 \
--name "video-stream" \
-o "/home/pi/player/stdout.log" \
-e "/home/pi/player/stderr.log"

3.4 在网页端访问摄像头

1
http://rasp_ip:8080

可以通过添加/?r的 query 参数来上下翻转画面。