天天看點

實作國标GB28181流媒體直播服務解決方案

背景

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);     
        });      

繼續閱讀