java rtsp websocket_基于websocket和goahead实现前端RTSP流视频控制

本文介绍了在嵌入式设备视频监控前端程序中,如何利用vue、webpack、C++、goahead和websocket实现RTSP流视频控制。详细讨论了goahead服务器的工作原理、视频编码的两种类型以及三种不同的视频流获取方案。重点讲解了通过后端将H264转成jpeg和amr码流,通过websocket推送给前端的实现过程,包括异常重连机制、图片传输码流格式和并发传输消息接收错乱问题的解决方案。
摘要由CSDN通过智能技术生成

最近在开发嵌入式设备视频监控的前端程序,前端使用vue框架实现基本逻辑,后端是C++,用webpack编译器编译成asp过程,服务器是goahead,用goahead解析asp过程,通过websocket调用Ajax通信,进而调用底层C++接口实现整体功能。

goahead是一种嵌入式服务器,经常用于搭载小型Linux的终端设备中,这种服务器小巧简洁,但功能单一,受限于硬件设备,作用是将C++获取到的底层数据转化成http数据报发给前端,且只支持asp过程。

视频编码主要分为存储型编码和压制型编码:

压制型编码是将画面中的音频,视频、字幕等多个轨道合并到一起,然后再进行传输的编码,这种编码的好处是可以有效解决传输过程中产生的音画不同步问题,典型的编码方案有mp4、mkv、rmvb等方式

存储型编码主要是为视频生成一系列关键帧,其他帧只保留与关键帧有差异的地方,没有差异的部分全部删掉以节省存储空间,在播放时通过实时计算恢复出整个视频的全貌,所以这种编码也被称作残片编码。主流残片存储编码方案:H265(较新,未来不可限量,但目前编码时间过长),VP8(成熟,但码率较高,对带宽有一定要求),H264(久经考验,成熟,画质高,码率低,当下最优解), 我们项目中使用的是H264编码存储方案

由于要在浏览器中获取视频流,而浏览器不支持H264解码,所以必须进行转码,我调研了目前主流的方案:

第一种是海康、大华以及众多二次集成商,他们使用的是基于微软ActiveX插件实现实时显示相机画面的方案,好处是人员需求少,方案成熟,只需要前端就可以搞定,但问题在于兼容性差,只支持IE浏览器,其他品牌浏览器均不支持ActiveX,且该插件具有系统级权限,有极高的安全风险,在安装时浏览器会弹出红色预警提示,大概率会使用户对产品安全性产生质疑。且目前主流浏览器的最新几个版本都不支持插件方案,在未来,插件方案极大概率会被浏览器厂商彻底抛弃。

第二种是建立一个中转服务器,就后台拉取的RTSP流转发给前端的websocket,Websocket在2011年成为国际标准,2011年之后的浏览器全部支持,能复用http协议,可在tcp上面快速建立长连接,性能高且能发送二进制数据,是解决视频直播、点播问题的有效方案,这种方案需要稳定的网络传输环境,并不适合嵌入式设备。所以我们应用在了云平台上,详见。

第三种就是后端将H264转成jpeg和amr码流推送给前端websocket,这可以有效解决嵌入式设备中网络不稳定的问题,下面讲一讲该方案在实现过程中我遇到的典型问题:

一、异常重连机制

首先各种浏览器内核对websocket的网页刷新处理是不一样的,比如在类Chrome浏览器,对websocket进程管理使用的是单例模式,每次刷新会使用一个新的进程代替原有的进程,原有的进程会被挂起。

cfe21d9abb98

谷歌websock.png

而在类IE浏览器中每次刷新会新建一个进程,一个websocket连接最多能并发连接五个进程,超过的话则失去响应。

cfe21d9abb98

IEwebsock.png

由于设备硬件资源有限,不稳定,偶尔会出现异常重启,所以我区分webkit内核(断线刷新)和非webkit内核(断线不刷新尝试重连)进行了重连设计,核心都是onMessage和onClose函数中设计一个状态变量wsIsOpened,当断线时,将该状态置为0,然后尝试重连

注意,在重连时,websocket首先进行TCP握手连接,成功之后此时websocket的open状态为true,但是由于websocket还要发送一次通信协议升级协议,因此此时send函数不能调用,强行调用会报错,应当与后端商定,添加一条升级成功的消息

我在这里设计的成功消息,自定义了一个2001状态码,同时在该消息中还发送了一个后端生成的用户消息签名,用于生成密钥,伪代码如下:

const onClose = function(evt) {

wsIsOpened = 0

/*

* TODO 重连代码

*/

}

const onMessage = function(evt) {

if (evt && evt.data) {

// 判断是否为秘钥协商命令

if (+myCmd === 2001) {

const auth_asw = {

'ContentType': 'json',

'MasterType': 1,

'Cmd': 2001,

'EncryptMode': 0,

'SYN':0

'ACK':1

}

}

const asw_json = JSON.stringify(auth_asw)

send(asw_json)

wsIsOpened = 1 // 秘钥协商响应包发送完毕,则认为链接已建立,此时可进行其它业务数据传输

}

}

二、图片传输码流格式

传输码流格式,websocket中常用的码流格式有两种,一种是二进制,数据头是:10001010(130); 还有一种是字符串数据头:10001001(129),图片,zip等大文件传输要使用二进制,二进制不需要编码,这样传输节约带宽。而一般的消息报文应当使用字符串模式,这样报文容易解析,代码量小

区分二进制和字符串接收,前端发送时可以这么写

if (isArraybuffer) {

socket.binaryType = 'arraybuffer'

socket.onsend(Data)

}else{

socket.binaryType = 'blob'

socket.onsend(Data)

}

前端接收时只需要在onmessage方法中做一下类型判断就可以了,比如接收图片流时,只需要定义一个图片缓存数组picBuffer,如果发现是二进制码流,直接将其转换成base64编码,push进数组队尾,当前端调用图片流时再shift出队头的图片即可,伪代码类似这样:

const picBuffer=[]

...

if (typeof evt.data !== 'string') {

//将接收到的二进制码转成base64编码

picBuffer.push(arrayBufferToBase64(evt.data))

return

}else{

//将接收到的码按字符串处理

}

...

const getPicStream = function() {

if (picBuffer.length > 0) {

return picBuffer.shift()

} else {

return null

}

}

const cleanPicStream = function() {

if (picBuffer.length > 0) {

picBuffer.length = 0

}

}

const arrayBufferToBase64 = function(buffer) {

let binary = ''

const bytes = new Uint8Array(buffer)

const len = bytes.byteLength

for (let i = 0; i < len; i++) {

binary += String.fromCharCode(bytes[i])

}

const pic = 'data:image/jpeg;base64,' + window.btoa(binary)

return pic

}

export {

cleanPicStream,

getPicStream

}

三、并发传输消息接收错乱问题

当一次性发送多条消息后,由于消息返回的先后不同,接收时会有消息错乱的情况,我的方法是新建一个map,当接收到消息时在onmessage函数中将消息和状态码全部缓存起来,当前端获取状态时,通过状态码返回消息,同时将该状态码删掉,这样就可以避免并发处理消息时产生的消息错乱问题

websocket 代码

const responseArray = new Map()

const haveResponse = function(Cmd) {

return responseArray.has(+Cmd)

}

const getResponse = function(Cmd) {

const strResponse = responseArray.get(+Cmd)

responseArray.delete(+Cmd)

return strResponse

}

const websocket = new Socket(IP)

websocket.onmessage = function(evt) {

responseArray.set(evt.Cmd, evt.resJson)

}

export {

websocket,

haveResponse,

getResponse,

}

调用代码

onsocketRes(CMDCode, cb){

if (haveResponse(CMDCode)) {

// 返回消息分发给前端框架

const msgStr = getResponse(CMDCode)

cb(msgStr)

}

}

注意,在并发发送消息后,由于返回的消息的时延不可控制,以及async、promise等方法的队列优先级高于setInterval,所以在检测网络超时的逻辑中不能使用,必须使用回调函数cb的方式。

websocket文件

const Socket = window.MozWebSocket || window.WebSocket

let websocket = null // 防止内存泄露

let connCb = null // 成功连接后回调

const responseArray = new Map()

const picBuffer = []

let wsIP = null

const concurrency = 5 // websocket的并发数

let wsIsOpened = 0 // websocket的链接状态,0,未链接,1,已链接

const onSend = function(msg) {

websocket.send(msg)

}

const onOpen = function(e, isNew) {

if (e && e.type === 'open') {

return new Promise((resovle, rej) => {

resovle(true)

})

}

}

const haveResponse = function(Cmd) {

return responseArray.has(+Cmd)

}

const getResponse = function(Cmd) {

const strResponse = responseArray.get(+Cmd)

responseArray.delete(+Cmd)

return strResponse

}

const getPicStream = function() {

if (picBuffer.length > 0) {

return picBuffer.shift()

} else {

return null

}

}

const cleanPicStream = function() {

if (picBuffer.length > 0) {

picBuffer.length = 0

}

}

const onClose = function(evt) {

websocket = null

wsIsOpened = 0

// 三秒后进行重新链接

setTimeout(() => {

websocket = new webSocket()

}, 3000)

}

const checkConnectStatus = function() {

// savedSocket = new webSocket();

// 三秒时间来检测秘钥协商是否已经完成,wsIsOpened是否为1

// 如果秘钥协商少于三秒完成,则立即返回

let reconTimer = null

let count = 0

return new Promise((resolve, reject) => {

reconTimer = setInterval(() => {

if (wsIsOpened) {

clearInterval(reconTimer)

reconTimer = null

resolve(true)

} else if (count > 30) {

clearInterval(reconTimer)

reconTimer = null

resolve(false)

}

count++

}, 100)

})

}

const arrayBufferToBase64 = function(buffer) {

let binary = ''

const bytes = new Uint8Array(buffer)

const len = bytes.byteLength

for (let i = 0; i < len; i++) {

binary += String.fromCharCode(bytes[i])

}

const pic = 'data:image/jpeg;base64,' + window.btoa(binary)

return pic

}

const onMessage = function(evt) {

if (typeof evt.data !== 'string') {

picBuffer.push(arrayBufferToBase64(evt.data))

return

}

if (evt && evt.data) {

// 判断是否为秘钥协商命令

const recvData = evt.data

if (+evt.Cmd === 2001) {

// 应答websocket升级协议,同时交换密钥

const asw_json = JSON.stringify(auth_asw)

send(asw_json)

wsIsOpened = 1 // 秘钥协商响应包发送完毕,则认为链接已建立,此时可进行其它业务数据传输

} else {

const resJson = evt.data

responseArray.set(+myCmd, resJson)

}

} else {

console.log('链接错误,数据包不能解析')

}

}

const onError = function(evt) {

console.log('Error occured: ', evt)

}

const webSocket = function() {

try {

if (!websocket) {

websocket = new Socket(settingWsIP)

}

} catch (evt) {

websocket = null

connCb('-1', 'connect error!')

}

websocket.onsend = onSend

websocket.onopen = onOpen

websocket.onclose = onClose

websocket.onmessage = onMessage

websocket.onerror = onError

webSocket.isOnBinaryStream = false

webSocket.binaryType = null

return websocket

}

export {

concurrency,

haveResponse,

getResponse,

getPicStream,

cleanPicStream,

checkConnectStatus,

webSocket,

wsIsOpened

}

调用方法

if (wsIsOpened && savedSocket) {

const socket = savedSocket

// 如果直播协议切换成功,切换成二进制传输协议

if (CMDKey === 'REQUEST_STREAM_START') {

socket.binaryType = 'arraybuffer'

socket.isOnBinaryStream = true

return request({

url: '/reqBinaryStream',

method: 'websocket',

socket

})

}

//非直播协议,按照是否加密区分发送的是消息还是大文件

if (EncryptMode === 0) {

// 是zip文件,直接按二进制码流推送, 数据头:10001010; ascll码数据头:10001001

socket.binaryType = 'arraybuffer'

socket.onsend(ZipFileData)

} else {

// 消息加密

socket.binaryType = 'blob'

const CMDJsonStr = JSON.stringify(CMDJson)

const AESCMDJson = AESEncrypt(SHA224ModeKey, CMDJsonStr)

socket.onsend(AESCMDJson)

}

let resultJson = null

let _WATCH_DOG = null

let count = 0

const timeOut = getTimeOut(CMDKey, myfaceCount)

// 启动看门狗,检测超时

_WATCH_DOG = setInterval(() => {

if (haveResponse(CMDCode)) {

// 返回消息分发给前端框架

const msgStr = getResponse(CMDCode)

resultJson = JSON.parse(msgStr)

clearInterval(_WATCH_DOG)

_WATCH_DOG = null

cb(resultJson)

} else if (count > timeOut) {

// 超时报错

clearInterval(_WATCH_DOG)

_WATCH_DOG = null

cb(new Error('recv response timeout'))

}

count++

}, 100)

} else {

// 按F5时刷新页面,重新建立websocket链接,并延时检测链接是否重新建立完成

savedSocket = new webSocket()

const reconFlag = await checkConnectStatus()

// 如果重连成功,则重新触发一次获取响应数据

if (reconFlag) {

// 恢复通信现场,重新发送消息

tanslateJson(CMDKey, listQuery, savedSocket, cb, Prams)

} else {

// 网络错误

cb(new Error('socket connect error'))

}

}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值