28181協議全稱爲GB/T28181《安全防範視頻監控聯網系統信息傳輸、交換、控制技術要求》,是由公安部科技信息化局提出,由全國安全防範報警系統標準化技術委員會(SAC/TC100)歸口,公安部一所等多家單位共同起草的一部國家標準(如下簡稱28181)。html
28181協議在全國平安城市、交通、道路等監控中普遍採用,若想作統一的大監控平臺,則支持28181協議接入是必不可少的。現在不少客戶都是想在以前使用的28181平臺的基礎上進行拓展。redis
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接口;服務器
1 接入服務器發送Invite請求 接入服務器向流媒體服務器發送Invite請求,請求流媒體服務返回攜帶SDP 消息體,消息體中 描述了媒體服務器接收媒體流的IP、端口、媒體格式等內容; Invite請求代碼以下:session
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代碼以下:async
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推流方式在公網應用中效果比較差,須要進一步優化或者改進。tcp
3 接入服務器接收ACK應答並Invite請求設備開始推流 回調函數中ack應答處理js代碼以下:ide
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); });
至此,整個拉流過程已經完成。