前言

前几个月一直在进行JT/T 1078协议(以下简称1078)相关项目的开发,其中涉及各种音视频协议和网络协议,还有音视频服务器和处理软件的知识。从一开始处于知识盲区的我,一路摸爬滚打,google和阅读书籍,挖坑填坑,总算是把项目需要的功能给研究出来了。当然,1078本身描述的功能很多,全部实现需要大量的人力和时间,一篇文章也不可能讲完。所以,打算用两篇文章主要记录一下实时视频和双向对讲的实现方法。这一篇为实时视频,双向对讲放在下一篇。功能皆使用 nodejsFFmpeg 实现,系统环境为 Linux .

准备

项目需求简单地说就是,有一台支持1078协议的设备,接收特定指令后,通过协议内容将音视频数据发送至服务器(以下简称推流),服务器再将音视频数据转发出去,使得客户端或者网页播放器能够收看到设备发送的数据(以下简称拉流),以达到实时视频的效果。后面功能的实现皆为网页端操作。

所以至少要准备:

  • 1078协议文档
  • 1078协议设备
  • 音视频服务器
  • 播放器

流程

实时视频流程大致如下:

1
2
3
4
5
6
7
participant 网页
participant 服务器
participant 设备

网页 ->> 设备: 下达推送视频指令
设备 ->> 服务器: 推送视频数据
服务器 ->> 网页: 转换为网页支持的格式

音视频服务器与解析

下达推送指令部分可按照协议要求的格式轻松搞定,主要问题在于如何将设备的音视频数据推流至服务器。服务器再转换为网页端能够拉流播放的格式。说到视频直播,推流拉流,首先想到的是RTMP。在搜索引擎中搜一搜,支持RTMP的音视频服务器,无论是开源还是闭源,都有很多。首先找到一个部署方便的,放在服务器上待命。
部署完成之后,可以使用FFmpeg推送本地文件至服务器测试好不好用。

FFmpeg命令如下:

1
ffmpeg -re -i localFile.mp4 -c copy -f flv rtmp://server:1935/live/streamName

RTMP链接后部分可能根据服务器有所不同。端口一般为1935.
播放RTMP链接,可以使用FFmpeg内置的ffplay,或者其他在线播放网站即可。

由1078文档内容可知,设备的音视频传输方式,定义在 5.5.3 部分。

实时音视频流数据的传输参考RTP协议,使用UDP或TCP承载。

可知,设备推流并不是RTMP协议,而是魔改的RTP协议,具体内容在表19中。这和服务器接收的数据协议不同,所以需要将设备推流转换为服务器能够接收的协议格式。
第一次搞这种东西,只看文档一头雾水,没别的办法,搭建一个TCP服务,让设备把数据发过来,验证数据格式是否与文档中描述的一致。

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
30
31
32
33
const net = require('net');
const HOST = 'xx', PORT = 'xx'; // 在下发指令中指定
const server = net.createServer(socket => {
console.log('TCP Server Created!');

socket.on('error', err => {
console.log('TCP Server Error ==', err);
});

socket.on('end', _ => {
console.log('TCP Server Closed.');
});

socket.setKeepAlive(true, 2000);
socket.setTimeout(10000, _ => {
socket.end('TCP Socket Broken.');
});
});
server.listen(PORT, HOST, _ => {
console.log(`Server Start Successfully, Listen on ${PORT}.`);
});
server.on('connection', socket => {
var { remoteAddress } = socket, { address, port } = socket.address();
console.log('Address is ', address, '. Port is', port, '. remoteAddress is ', remoteAddress);

socket.on('data', data => {
// console.log('TCP ON DATA');
});

socket.on('end', _ => {
console.log('Bye');
});
});

设备推流后会触发socket的on data 事件,由于data是Buffer,所以需要把data转换成 16进制 的数据才能对照文档分析。转换完成后,可以大致看到一些规律,与文档中描述的一致。一个data变量可能包含多个以 0x30 0x31 0x63 0x64 开头的数据,对应文档中的帧头标识。有没有可能在负载数据中也出现帧头呢,那就再往后取一个字节,降低概率。可以观察到第4至第5字节,都是 0x81 .具体含义可以看 RFC 3550.每个RTP包除了前30字节(也有可能少于30)的数据描述外,就是媒体数据payload了。
接下来要做的是,从TCP数据中分离出每个RTP包。TCP是基于流式传输的(我真的很讨厌“粘包”这个词),每个data不一定以帧头开始,或者结束,而且中间可能包含多个RTP包。但是已经知道了帧头一定是 0x30 0x31 0x63 0x64 0x81 , 所以可以用帧头来切分TCP数据。再将切分出来的RTP包传入到下一个函数中单独处理。这部分处理很简单,不再贴代码,需要注意的地方就是如何将两个TCP data中的RTP包数据组合再传入函数。
先假定现在是最理想的情况,即不需要考虑RTP包乱序,视频帧乱序的情况,以这种前提将payload存储下来看看能否正常播放。当然,上面的再取一个 0x81 也是在这种前提下考虑的。
接下来就是要分析RTP包的数据了。完全对照文档中的表19按字节读取即可。网络传输使用的是 大端模式 ,nodejs中使用的函数主要为以下几个:

1
2
3
4
buf.slice([start[, end]])
buf.readInt8(offset)
buf.readInt16BE(offset)
buf.readInt32BE(offset)

解析完成后,可以看到当前RTP包所承载的数据信息。如包序号,SIM卡号,通道号,数据类型,分包标记等。原子包不做处理,分包需要将后续的包组合成一个数据。从第5字节中的数据可知,当前包负载中的媒体类型,解析后对应表12查得可知,音频格式为 G.726,视频格式为 H.264.不同设备的格式可能有所不同。将音频包和视频包分别存储为音频和视频文件,再使用软件播放验证前面解析程序的正确性。全部正确的话,这些文件都是可以正常播放的。
接下来就需要想办法如何把这些数据推流到音视频服务器了。服务器接收的格式为RTMP流,需要把从RTP分离出的媒体数据转化为RTMP流推到服务器。由于本人还不能直接用nodejs撸出一个转码和推流服务的程序出来,接下来的工作便交给了 FFmpeg 处理。
先来看一下如何处理视频数据,用FFmpeg将本地文件推送至RTMP服务器,查官方文档可知,得到下面的命令:

1
2
3
4
5
6
7
8
9
10
ffmpeg -loglevel panic \
-probesize 32 \
-re \
-r 16 \
-i localfile.h264 \
-c:v libx264 \
-preset ultrafast \
-tune zerolatency \
-profile:v baseline \
-f flv "rtmp://xxx"

上面这条语句不止能推送文件,还是最终的优化版。
解释一下部分选项:

  • -loglevel panic 用于屏蔽ffmpeg的输出内容
  • -probesize 32 用于降低ffmpeg转换延迟
  • -r 16 设置帧率,需要与设备端一致,否则会出现播放速度太快的问题
  • -f flv “” 输出rtmp链接,这里需要使用SIM卡号和通道号组合成特定链接

有了这条命令,就该想办法如何把RTP中的payload输入到ffmpeg了。
ffmpeg支持从STDIN输入的数据,所以只需要将ffmpeg执行命令放入nodejs的child process子进程中,然后把payload数据写入该子进程的STDIN即可,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const { spawn } = require('child_process');
const FFMPEG = '/usr/bin/ffmpeg';

var cmds = [
'-loglevel', 'panic',
'-probesize', '32',
'-re',
'-r', '16',
'-i', '-',
'-c:v', 'libx264',
'-preset', 'ultrafast',
'-tune', 'zerolatency',
'-profile:v', 'baseline',
'-f', 'flv',
rtmpUrl
];
var child = spawn(FFMPEG, cmds);
child.stdin.write(data);

区别在于,将本地文件的名称换成了 - .ffmpeg会在子进程中将payload数据转化成rtmp推流到服务器。完成这一部分的代码之后,可以先尝试着将数据推流到音视频服务器,然后找一个能在线看rtmp的网站进行测试。程序无误的情况下,可以看到直播数据。在单台设备全通道的情况下,可以做到延迟 2-3s .说明上面的“大胆”假设是正确的,一下子感觉轻松了不少:)

再接下来需要进一步完善程序,如:

  • 设备收到停止视频推送指令后,需要关闭ffmpeg的子进程
  • 多设备,多通道情况下的ffmpeg子进程管理

这就与1078协议开发无关了,考验写程序功底的时候到了,本文不再赘述。

音频部分涉及到转码,留在与下一篇双向对讲功能一起说。

网页端

网页端无论是Flash还是H5的视频播放器有很多,翻翻文档API就可以查到如何播放指定链接的视频。如果音视频服务器不仅能提供rtmp流,还能提供flv流,网页播放的实现可以更加灵活。

后记

1078协议实时视频的开发,说难不难,说简单不简单。它难就难在网上的资料居然如此之少,完全需要靠自己摸索验证。而且在开发验证的过程中,很容易走弯路。开发完成后,回头再看,发现只是需要按文档读取数据而已,并没有涉及音视频领域更深一层的知识。
本文并没有完全使用nodejs进行转码和推送的实现,而是使用了ffmpeg作为子进程进行推流。恕本人学识有限,音视频知识的学习任重而道远,暂时只能以这样的“笨”方法实现功能需求。新的学习计划已提上日程,希望在以后的日子里能够进一步改进。