背景
28181協定全稱為GB/T28181《安全防範視訊監控聯網系統資訊傳輸、交換、控制技術要求》,是由公安部科技資訊化局提出,由全國安全防範報警系統标準化技術委員會(SAC/TC100)歸口,公安部一所等多家機關共同起草的一部國家标準(以下簡稱28181)。
28181協定在全國平安城市、交通、道路等監控中廣泛采用,若想做統一的大監控平台,則支援28181協定接入是必不可少的。如今很多客戶都是想在之前使用的28181平台的基礎上進行拓展。
說明
LiveGBS GB28181流媒體伺服器負責将GB28181裝置/平台推送的PS流轉成ES流,然後進行分發。
同時,LiveGBS 對外提供HTTP API接口,通過接口可以獲知流媒體轉發服務的運作狀态資訊,轉發會話資訊,伺服器配置和版本資訊等;
LiveGBS GB28181流媒體伺服器提供以下功能:
1. 接受和處理GB28181接入伺服器的推流請求(如有推流權限驗證則調用驗證伺服器接口);
2. 接受和處理GB28181裝置的推流;
3. 實時流媒體處理,PS(TS)轉ES;
4. 推送ES流到EasyDSS流媒體伺服器;
5. 接受和處理GB28181接入伺服器的斷開推流請求;
6. 對外提供伺服器擷取狀态、資訊,控制等http API接口;
下載下傳:https://www.liveqing.com/docs/download/LiveGBS.html
LiveGBS流媒體直播詳細流程
1 接入伺服器發送Invite請求
接入伺服器向流媒體伺服器發送Invite請求,請求流媒體服務傳回攜帶SDP 消息體,消息體中
描述了媒體伺服器接收媒體流的IP、端口、媒體格式等内容;
Invite請求代碼如下:
const options = {
serialServer: serialServer,
serialDevice: code,
method: common.SIP_INVITE,
contentType: common.CONTENT_NONE,
content: sdp,
host: hostip,
port: hostport,
rtpovertcp: (parseInt(rtpovertcp)===0?'UDP':'TCP')
};
console.log('inviteMediaServer......sendRequest' + JSON.stringify(options));
uas.sendRequest(options);
2 流媒體服務接受Invite請求處理并ACK應答
流媒體服務接受Invite請求,并在回調函數中處理請求,js代碼如下:
uas.on('invite', async ctx => {
const request = ctx.request;
const content = JSON.parse(request.content);
const status = 200;
const serial = sip.parseUri(request.uri).user;
const host = config.server.serverHost;
let ssid = serial.substring(16,20);// PrefixInteger(sessionid,4);
let sirialid = serial.substring(3,8);
const ssrc = "0"+sirialid+ssid;
console.log("ssrc = "+ssrc);
let sdp = '';
//如果已存在
let bHas = this.session_.has(serial);
console.log(bHas);
if (bHas) {
console.log('this.session_ has exist serial: '+serial);
sdp = '';
}
else{
let port = config.server.udpPort;//流媒體接收TCP端口
let transport = 'RTP/AVP';
let a = "a=recvonly\r\n";
if(content.rtpovertcp === 'TCP' )
{
port = config.server.tcpPort;//流媒體接收TCP端口
transport = 'TCP/RTP/AVP';
a = "a=recvonly\r\na=setup:passive\r\n";
}
sdp = "v=0\r\n" +
`o=${serial} 0 0 IN IP4 ${host}\r\n` +
"s=Play\r\n" +
`c=IN IP4 ${host}\r\n` +
"t=0 0\r\n" +
`m=video ${port} ${transport} 96 98 97\r\n` +
"a=rtpmap:96 PS/90000\r\n" +
"a=rtpmap:98 H264/90000\r\n" +
"a=rtpmap:97 MPEG4/90000\r\n" +
`${a}`+
//`a=connection:new\r\n` +
`y=${ssrc}\r\n`;
// A new channel is coming, delete the old
rtpserver.deleteChannels(parseInt(ssrc));
// Create a new stram,and add to redis
this.registerStream(parseInt(ssrc),uuidv4(),true);
}
let response = sip.makeResponse(request, status, common.messages[status]);
uas.sendAckEx(response, sdp);
});
如上代碼所示,我們在SDP消息體中提供了兩種流傳輸方式,分别是TCP和UDP,通過Invite請求所帶的 “rtpovertcp ”參數來控制,TCP方式因為其不丢包的傳輸方式在GB28181裝置推流到公網伺服器的方案中得以廣泛應用,然而,目前市面上的多數支援國标的裝置都不支援tcp模式推流,udp仍然是主流的推流方式,不過,經測試udp推流方式在公網應用中效果比較差,需要進一步優化或者改進。
3 接入伺服器接收ACK應答并Invite請求裝置開始推流
回調函數中ack應答處理js代碼如下:
uas.once('ack', async ctx => {
const request = ctx.request;
const callId = request.headers['call-id'];
if (request.content.length > 0 )
{
const serial = serialDevice;//sip.parseUri(request.headers.from.uri).user;
let response ;
if(!this.session_.has(callId))
{
response = await this.inviteDevice(serial, code, callId, request.content);
//Invite Device is complete
if(response != undefined)
{
if(response.content)
{
const transform = require('sdp');
const res = transform.parse(response.content);
console.log(res.media[0].protocol);
if((res.media[0].protocol === 'RTP/AVP'&&parseInt(rtpovertcp)===0) ||
(res.media[0].protocol === 'TCP/RTP/AVP'&&parseInt(rtpovertcp)===1) ){
if (response.status === 200 )
{
//send ack to stream server
this.ackMediaServer(response.status,request,request.content);
this.session_.set(callId, response);
}
}
else{
response.status = 700;
}
}
console.log('inviteMediaServer ack is coming.......response='+JSON.stringify(response));
}
resolve(response);
}
else{
console.log('inviteMediaServer this.session_.has: '+callId);
}
}
});
如上代碼所示,在InviteDevice請求完成後,我們在傳回Response處理過程中做過一次特殊處理,即:如果TCP拉流時發現裝置拉流應答中傳回其推流模式依然是’RTP/AVP’的UDP模式,我們認為其裝置不支援TCP模式,進而向上層傳回700,不支援的流媒體傳輸方式。
4 Invite裝置正常傳回200應答并傳遞給流媒體伺服器
代碼在第3點中有所展現。
5 流媒體服務接受拉流請求成功應答
uas.on('ack', async ctx => {
const request = ctx.request;
if (request.content.length === 0) {
return;
}
const serial = sip.parseUri(request.headers.from.uri).user;
this.session_.set(serial, request);
const ssrc = serialTossrc(serial);
// resole a new stram,and refresh to redis
const info = JSON.parse(await redis.get(`stream:${parseInt(ssrc)}`));
this.registerStream(parseInt(ssrc),info.uuId,false);
});