golang實現國標GB28181流媒體點播預覽服務方案的框架流程

背景

28181協議全稱爲GB/T28181《安全防範視頻監控聯網系統信息傳輸、交換、控制技術要求》,是由公安部科技信息化局提出,由全國安全防範報警系統標準化技術委員會(SAC/TC100)歸口,公安部一所等多家單位共同起草的一部國家標準(以下簡稱28181)。

28181協議在全國平安城市、交通、道路等監控中廣泛採用,若想做統一的大監控平臺,則支持28181協議接入是必不可少的。如今很多客戶都是想在之前使用的28181平臺的基礎上進行拓展。

EasyDSS雲平臺目標支持市面上絕大多數監控設備接入,所以支持28181協議也是必然的要求;結合EasyDSS雲平臺的標準設備接入能解決市面上絕大多數設備以及平臺的接入。

說明

EasyGBS流媒體服務器是提供流轉發的視頻服務器,負責將GB28181設備/平臺推送的PS流轉成ES流,然後推送給EasyDSS流媒體服務器進行分發。
同時,GB28181流媒體服務器對外提供HTTP API接口,通過接口可以獲知流媒體轉發服務的運行狀態信息,轉發會話信息,服務器配置和版本信息等;

EasyGBS流媒體服務器提供以下功能:
1. 接受和處理GB28181接入服務器的推流請求(如有推流權限驗證則調用驗證服務器接口);
2. 接受和處理GB28181設備的推流;
3. 實時流媒體處理,PS(TS)轉ES;
4. 推送ES流到EasyDSS流媒體服務器;
5. 接受和處理GB28181接入服務器的斷開推流請求;
6. 對外提供服務器獲取狀態、信息,控制等http API接口;

GB28181流媒體服務器對接EasyDSS雲平臺整體框架

框架

流媒體點播詳細流程

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

至此,整個拉流過程已經完成,這時,不出意外的話設備端將會通過我們指定的傳輸方式將流推送到我們指定的UDP/TCP服務器上,並轉發到我們指定的EasyGBS流媒體服務器。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章