确定框架
在选用推拉流框架的时候,有了解过nginx+rtmp/rtsp,Janus,以及其他开源的推拉流框架,要么是延迟严重(延迟一分多钟),要么配置复杂,而且这些框架对于只是转发远程画面这个简单需求来说,过于庞大了。机缘巧合之下,我了解到了一个简单易用的框架,就是ZeroMQ的Java版本–JeroMQ。
然后就是具体实现了,前提是我们有一个属于自己的公网服务器,否则是不可能实现跨局域网推拉流的,整体流程是:
受控端推流 --> 服务器收流 --> 主控端拉流
推流实现
这里用到JeroMQ的“订阅-发布”模式,公网服务器订阅,受控端发布,就实现了推流。
①服务器端订阅
public void startReceiveService() {
// 收流服务器
ZContext receiveContext = new ZContext();
ZMQ.Socket receiveSocket = receiveContext.createSocket(SocketType.SUB);
receiveSocket.bind("tcp://" + Constants.SERVER_IP + ":" + Constants.SERVER_PORT_RECEIVE);
receiveSocket.subscribe("".getBytes(ZMQ.CHARSET));
while (true) {
// 在这里阻塞,直到有数据发过来
byte[] reply = receiveSocket.recv(0);
if (reply != null) {
// 收流服务器收到消息后,用推流服务器推送收到的数据给其他客户端
try {
publishSocket.send(reply);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
②受控端推流
/**
* 推流连接服务器
*/
fun startPublishStreamService() {
publishStreamUrl = "tcp://${FollowManager.currentFollowBean.p}:${KeyValue.publishPortStream}"
publishStreamExecutorService.submit(Runnable {
if (publishStreamSocket != null) {
return@Runnable
}
try {
publishStreamContext = ZContext()
publishStreamSocket = publishStreamContext?.createSocket(SocketType.PUB)
publishStreamSocket?.connect(publishStreamUrl)
} catch (e: Exception) {
L.e(e.message)
AppUtils.relaunchAppOnChildThread("推流服务启动失败")
}
})
}
受控端在获取到画面数据后,直接调用publishStreamSocket?.send(data)
即可。
拉流实现
拉流过程是类似的,我们这里也是使用了JeroMQ的“订阅-发布”模式。
①主控端订阅
private fun startPreviewVideo() {
mZContext = ZContext()
mSocket = mZContext!!.createSocket(SocketType.SUB)
mSocket.connect(serverUrl)
mSocket.subscribe("".toByteArray())
val paint = Paint()
var widthScale = -1.0f
var heightScale = -1.0f
while (mSurfaceHolder != null && mZContext != null && !mZContext!!.isClosed && !Thread.currentThread().isInterrupted) {
try {
val receivedData = mSocket.recv()
获取到数据后渲染...
} catch (e:Exception) {
e.printStackTrace()
}
}
}
②服务器端推流
private ZMQ.Socket publishSocket;
public void startPublishService() {
// 推流服务器
ZContext publishContext = new ZContext();
publishSocket = publishContext.createSocket(SocketType.PUB);
Application.enableStreamSetting(publishSocket);
EncryptUtils.enableAuth(publishSocket);
publishSocket.bind("tcp://" + Constants.SERVER_IP + ":" + Constants.SERVER_PORT_PUBLISH);
}
以上简单几行代码,就实现了最核心的推拉流难题。
身份认证
推拉流是通了,但这样还不够,因为我们的服务器是所有人都能访问的,如果服务器IP和以上2个端口都被泄露了的话,岂不是人人都可以窥探到我们设备的画面?于是需要新增认证设置。
①服务器端
public static final ZCert serverCert = new ZCert();
public static void enableAuth(ZMQ.Socket socket) {
socket.setZapDomain("global".getBytes());
socket.setCurveServer(true);
socket.setCurvePublicKey(serverCert.getPublicKey());
socket.setCurveSecretKey(serverCert.getSecretKey());
}
②客户端
private val clientCert = ZCert().apply {
setMeta("name", "Client test certificate")
setMeta("meta1/meta2/meta3", "Third level of meta")
}
fun ZMQ.Socket.enableCommonAuth() {
curvePublicKey = clientCert.publicKey
curveSecretKey = clientCert.secretKey
curveServerKey = serverPublicKey
}
这里唯一要注意的点是,我们需要把服务器的serverPublicKey传给客户端,客户端在启动的时候需要用到这个serverPublicKey。怎么实现呢?首先在服务器启动的时候,把生成的serverPublicKey(byte数组)转成字符串发送给自己的邮箱,当然你也可以输出到本地文件,复制生成的字符串到客户端进行解码就可以了。
网络延迟
以上几乎已经是服务器端所有功能了,实际跑起来的时候,发现在网络不好的情况下,延迟还是比较大的,这是因为默认JeroMQ默认会缓存很长时间的画面,我们需要简单设置一下,尽量减少缓存,但太少又不行,会出现数据不推送,或者延迟没改善的问题,经过多次测试,以下设置可以把延迟控制在5秒内。
socket.setReceiveBufferSize(100*1024);
socket.setHWM(2);
在服务器和客户端都做同样的设置即可。