通过仔细学习《GBT 28181-2016 公共安全视频监控联网系统信息传输、交换、控制技术要求》技术规范文档。整篇主要是基于SIP协议对视频监控系统互联中涉及的消息内容格式进行了规范,由于GB28181是适用于各个领域各种情况下的视频监控系统信令交互,涉及的信令太多,在实际应用中往往只使用其中一部份信令,今天我主要讲几个最常用的信令应用。

设备注册

当有新的监控设备要接入视频监控平台时,首先要在服务端进行注册操作,其中涉及用户注册时身份认证是本信令的难点。

几款支持国标GB28181平台的视频监控设备接入方案 安防监控国标_信令

  1. SIP代理终端(视频监控设备)向 SIP服务器发送 Register请求并携带设备信息,表明有新设备要接入监控平台。
  2. SIP服务器向SIP代理终端回401,增加响应消息头域 WWW-Authenticate信息,要求终端进行身份验证。
  3. SIP代理终端收到401响应后,取出nonce字段,根据服务端身份认证要求对设备信息进行加密处理(一般是MD5),携带认证信息重新发起Register请求。
  4. SIP服务器对请求进行验证,如果检查新注册终端身份合法,向终端发送200成功响应。
/**
*@SIP信令基础类
**/
public class BaseCmd implements SipCmd {
    protected SipProxy proxy;
    protected SipFactory sipFactory;
    protected AddressFactory addressFactory;
    protected HeaderFactory headerFactory;
    protected MessageFactory messageFactory;
    protected SipProvider sipProvider;

    protected void asyProxy() {
        if (proxy == null) {
            proxy = FactoryBeanObjects.getBean(SipProxy.class);
            sipFactory = proxy.getSipFactory();
            sipProvider = proxy.getSipProvider();
            try {
                addressFactory = sipFactory.createAddressFactory();
                headerFactory = sipFactory.createHeaderFactory();
                messageFactory = sipFactory.createMessageFactory();
            } catch (PeerUnavailableException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void execute(Object[] args) throws Exception {
        throw new NotImplementedException();
    }

    protected void sendRequest(SipDevicer from, SipDevicer to, String reqName, String contentSubType, String content) throws Exception {
        Request request = getRequest(from, to, reqName, "SDP", content);
        sipProvider.sendRequest(request);
    }

    protected Request getRequest(SipDevicer from, SipDevicer to, String reqName, String contentSubType, String content) throws Exception {
        asyProxy();
        ///创建会话编号Call-ID
        CallIdHeader callIdHeader = sipProvider.getNewCallId();
        ///创建会话状态和名称 CSeq: <n> <RequestName>
        CSeqHeader cSeqHeader = headerFactory.createCSeqHeader(1, reqName);

        ///创建消息来源 From
        SipURI fromSipUri = addressFactory.createSipURI(from.getId(), from.getHost());
        Address fromAddress = addressFactory.createAddress(fromSipUri);
        fromAddress.setDisplayName(from.getId());

        ///创建消息来源程序名称 tag
        FromHeader fromHeader = headerFactory.createFromHeader(fromAddress, null);
        if (!StringUtil.isEmpty(from.getTag())) {
            fromHeader.setTag(from.getTag());
        }

        ///创建消息来源 To:xxx <sip:xxx@ip>
        SipURI toAddress = addressFactory.createSipURI(to.getId(), to.getHost());
        Address toNameAddress = addressFactory.createAddress(toAddress);
        toNameAddress.setDisplayName(to.getId());
        ToHeader toHeader = headerFactory.createToHeader(toNameAddress, null);
        if (!StringUtil.isEmpty(to.getTag())) {
            toHeader.setTag(to.getTag());
        }

        ///最大转发数量限制了通讯中转发的数量。它是由一个整数组成,每转发一次,整数减一。
        MaxForwardsHeader maxForwards = headerFactory.createMaxForwardsHeader(70);

        ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
        ///VIA域告诉大家本请求发送到哪里并且应答到哪里
        ViaHeader viaHeader = headerFactory.createViaHeader(from.getIp(), from.getPort(), "udp", "branchlive");
        viaHeaders.add(viaHeader);
        ///创建 Contact
        SipURI requestURI = addressFactory.createSipURI(to.getId(), to.getHost());
        requestURI.setTransportParam("udp");

        Request request = messageFactory.createRequest(requestURI, reqName, callIdHeader, cSeqHeader,
                fromHeader, toHeader, viaHeaders, maxForwards);
        if (content != null && !StringUtil.isEmpty(contentSubType)) {
            ///消息正文的描述
            ContentTypeHeader contentTypeHeader = headerFactory.createContentTypeHeader("text", contentSubType);
            request.setContent(content, contentTypeHeader);
        }
        Address contactAddress = addressFactory.createAddress(requestURI);
        ContactHeader contact = headerFactory.createContactHeader(contactAddress);
        request.setHeader(contact);

        return request;
    }
}
/**
 * 设备注册
 */
public class RegisterCmd  extends BaseCmd {
    @Override
    public void execute(Object[] args) throws Exception {
        SipDevicer from = (SipDevicer)args[0];
        SipDevicer to = (SipDevicer)args[1];
        to.setTag("acktagclient");

        if(args[2]==null) {
            ///首次注册 UAC->UAS
            sendRequest(from, to, Request.REGISTER, "SDP","");
        }else{
            ///认证注册 UAC->UAS
            AuthorizationHeader author=(AuthorizationHeader)args[2];
            Request request = getRequest(from, to, Request.REGISTER, "SDP", "");
            request.setHeader(author);
            sipProvider.sendRequest(request);
        }
    }
}
/**
*@测试程序启动类
**/
public class RootWindow {
    public static void main(String[] args) throws Exception {
        
        try {
            System.out.println("Sip 客户端运行成功!");
            AppConfig cfg= FactoryBeanObjects.regedit(new AppConfig());
            FactoryBeanObjects.regedit(new SipProxy());
            FactoryBeanObjects.regedit(new XSipServerImpl(), IXSipServer.class);
            FactoryBeanObjects.regedit(new MsgV1ProcessorImpl(), IMsgProcessor.class);
            FactoryBeanObjects.regedit(new ResponseHandllerFactory());
            SipFrame tc = FactoryBeanObjects.regedit(new SipFrame());
            tc.show();
        } catch (Throwable e) {
            System.out.println("Problem initializing the SIP stack.");
            e.printStackTrace();
            System.exit(-1);
        }
    }
}

测试工具程序运行结果如下图

几款支持国标GB28181平台的视频监控设备接入方案 安防监控国标_ide_02


通过点击【注册】按钮,测试程序向服务端发送Register请求,但服务端一般都会回复401响应,要求客户端身份认证。

几款支持国标GB28181平台的视频监控设备接入方案 安防监控国标_ide_03


客户端收到401后,根据服务端要求的身份认证方式对当前设备信息进行认证操作,常用的是MD5方式加密设备编号和其它信息,如下代码可以实现设备身份认证。再次点击认证后,客户端程序将携带认证信息发起第二次注册请求。

/**
     * 根据服务端回复的认证方式对当前设备进行身份认证
     * @param服务端回复
     */
    private void unauthorized(Response r) throws  Exception{

        getSipFrame().changeRegiseditBtnTile("认证");
        WWWAuthenticateHeader wwwHeader = (WWWAuthenticateHeader) r.getHeader(WWWAuthenticateHeader.NAME);
        if (wwwHeader != null) {
            SipProxy sipProxy = FactoryBeanObjects.getBean(SipProxy.class);
            SipDevicer from = getSipFrame().getFrom();
            SipURI fromSipURI = sipProxy.getAddressFactory().createSipURI(from.getId(), from.getHost());
            String realm = wwwHeader.getRealm();
            String nonce = wwwHeader.getNonce();
            String a1Str = String.format("%s:%s:12345678", from.getId(), realm);
            String a2Str = String.format("REGISTER:sip:%s@%s", from.getId(), from.getHost());
            System.out.println("a1Str=" + a1Str);
            System.out.println("a2Str=" + a2Str);
            String A1 = MD5Util.MD5(a1Str);
            String A2 = MD5Util.MD5(a2Str);
            String resStr = MD5Util.MD5(A1 + ":" + nonce + ":" + A2);
            HeaderFactory headerFactory = FactoryBeanObjects.getBean(SipProxy.class).getHeaderFactory();
            AuthorizationHeader aHeader = headerFactory.createAuthorizationHeader("Digest");
            aHeader.setUsername(getSipFrame().getFrom().getId());
            aHeader.setRealm(realm);
            aHeader.setNonce(nonce);
            aHeader.setURI(fromSipURI);
            aHeader.setResponse(resStr);
            aHeader.setAlgorithm("MD5");
            getSipFrame().setAuthorization(aHeader);
        }
    }

几款支持国标GB28181平台的视频监控设备接入方案 安防监控国标_服务器_04


到这里可以看到,服务器返回“设备ID 注册成功”,表明新客户端注册完成。

媒体点播

在GB28182规范所有应用中,要说最常用的信令,必然是媒体流点播,这个信令如果从规范里看是有些复杂,它规定了媒体流在网络中传输涉及整个生命周期的所有细节。所以这个信令从结构上看就显的繁琐,但必须承认从理想方面设计每个细节的规定都是合理的,可惜现实与理想总是有些差距。在实际的项目应用中都会对此规范进行不同程度的简化。

Invite标准版

几款支持国标GB28181平台的视频监控设备接入方案 安防监控国标_java_05

  1. 媒体流接收者向SIP服务器发送携带SDP消息体的Invite消息。
  2. SIP服务器收到Invite请求后,向媒体服务器转发送此Invite消息,此消息不携带SDP消息体。
  3. 媒体服务器收到SIP服务器的Invite请求后,回复200OK 响应,携带SDP消息体。
  4. SIP服务器收到媒体服务器返回的200OK 响应后,向媒体流发送者发送携带SDP消息的Invite请求。
  5. 媒体流发送者收到SIP服务器的Invite请求后,回复200OK 响应,携带SDP消息体。
  6. SIP服务器收到媒体流发送者返回的200OK 响应后,向媒体服务器发送 ACK 请求。
  7. SIP服务器收到媒体流发送者返回的200OK 响应后,向媒体流发送者发送 ACK 请求,表明媒体流发送者的Invite会话建立过程,媒体流发送者收到ACK请求后开始推流。
  8. SIP服务器向媒体服务器发送Invite,表明Invite请求已建立。
  9. 媒体服务器回复200,表明已开始接收数据流信息,返回携带SDP的信息。
  10. SIP服务器将消息9转发给媒体流接收者。
  11. 媒体流接收者收到200OK 响应后,回复 ACK 消息,完成与SIP服务器的Invite会话建立
    过程。
  12. SIP服务器将消息11转发给媒体服务器,完成与媒体服务器的Invite会话建立过程。
  13. 媒体流接收者向SIP服务器发送 BYE消息,断开消息1、10、11建立的同媒体流接收者的
    Invite会话。
  14. SIP服务器收到 BYE消息后回复200OK 响应,会话断开。
  15. SIP服务器收到 BYE消息后向媒体服务器发送 BYE消息,断开消息8、9、12建立的同媒体
    服务器的Invite会话。
  16. 媒体服务器收到 BYE消息后回复200OK 响应,会话断开。
  17. SIP服务器向媒体服务器发送 BYE 消息,断开消息2、3、6建立的同媒体服务器的Invite
    会话。
  18. 媒体服务器收到 BYE消息后回复200OK 响应,会话断开。
  19. SIP 服务器向媒体流发送者发送 BYE 消息,断开消息4、5、7建立的同媒体流发送者的
    Invite会话。
  20. 媒体流发送者收到 BYE消息后回复200OK 响应,会话断开
Invite简化版

几款支持国标GB28181平台的视频监控设备接入方案 安防监控国标_服务器_06

  1. 媒体流接收者向SIP服务器发送携带SDP消息体的Invite消息。
  2. SIP服务器收到Invite消息后更新媒体服务器的信息转发给媒体发送者。
  3. 媒体发送者收到Invite后,回复携带SDP信息的ok响应。
  4. SIP服务器收到ok后根据媒体服务器的信息回复媒体流接收者ok消息。
  5. 媒体发送者开始向媒体服务器尝试推流。
  6. 媒体流接收者根据回复消息从媒体服务器拉流播放。
  7. 媒体流接收者在挂断的时候向SIP服务器发送Bye消息。
  8. SIP服务器收到Bye后,立刻向媒体发送者转发此挂断请求。
  9. 媒体发送者收到Bye后,立刻停止推流,并回复Ok消息。
  10. SIP服务器收到ok后,向媒体流接收者转发此消息,表明媒体流已中断。
/***
 * 视频点播信令
 */
public class InviteCmd extends BaseCmd {
    /**
     * @Version 协议版本
     */
    private String v="0";
    /**
     * @所有者/创建者和会话标识符
     */
    private String o;
    /**
     * @会话名称(Subject )//Play标识为点播请求   Playback标识回播请求
     */
    private String s;
    /**
     * @Connection Data 连接信息
     */
    private String c;
    /**
     * @会话生命周期
     */
    private String t;
    /**
     * @Media(Type、Port、RTP/AVP Profile)
     */
    private String m;
    /**
     * @扩展属性定义
     */
    private String a1="recvonly";
    /**
     * @扩展属性定义
     */
    private String a2="rtpmap:96 PS/90000";
    /**
     * @扩展属性定义
     */
    private String a3="rtpmap:98 H264/90000";
    /**
     * @扩展属性定义
     */
    private String a4="rtpmap:97 MPEG4/90000";
    /**
     * @SSRC值
     */
    private String y="0100000001";
    /**
     * @媒体参数
     */
    private String f="";
    public InviteCmd(){

    }
    public InviteCmd(String content) throws IllegalAccessException {
        if (!StringUtil.isEmpty(content)) {
            String[] strAttrs = content.split("\r\n");
            Field[] fields = this.getClass().getDeclaredFields();
            for (int i = 0; i < fields.length; i++) {
                fields[i].setAccessible(true);
                for (String f : strAttrs) {
                    String[] ar = f.split("=");
                    Object v = null;
                    if (ar.length > 1) {
                        v = ar[1];
                    }
                    if (ar[0].equals(fields[i].getName())) {
                        fields[i].set(this, v);
                    }
                }
            }
        }
    }
    @Override
    public void execute(Object[] args) throws Exception {
        SipDevicer from = (SipDevicer)args[0];
        SipDevicer to = (SipDevicer)args[1];
        to.setTag("tlive");
        setV("0");
        //音视频流目的地址
        setO(String.format("%s 0 0 IN IP4 %s",to.getId(),to.getIp()));
        //Play标识为点播请求   Playback标识回播请求
        setS("Play");
        //音视频流目的地址
        setC(String.format("IN IP4 %s",to.getIp()));
        //t行第一参数为视频开始时间  第二个参数为结束时间    t = 0 0表示实时视音频点播
        setT("0 0");
        //video:表示请求音视频流  audio:表示请求音频流  5522:音视频流目的端口  RTP/AVP:视频流使用协议 96 97 98:客户端支持码流格式
        setM(String.format("video %s RTP/AVP 96 98 97",to.getPort()));
        sendRequest(from, to, Request.INVITE, "SDP",toString());
    }

    @Override
    public String toString() {
        StringBuilder builder=new StringBuilder();
        builder.append(String.format("v=%s\r\n",this.v));
        builder.append(String.format("o=%s\r\n",this.o));
        builder.append(String.format("s=%s\r\n",this.s));
        builder.append(String.format("c=%s\r\n",this.c));
        builder.append(String.format("t=%s\r\n",this.t));
        builder.append(String.format("m=%s\r\n",this.m));
        builder.append(String.format("a=%s\r\n",this.a1));
        builder.append(String.format("a=%s\r\n",this.a2));
        builder.append(String.format("a=%s\r\n",this.a3));
        builder.append(String.format("y=%s\r\n",this.y));
        builder.append(String.format("f=%s\r\n",this.f));
     /*   String content = "v=0\r\n" +
                "o=34020000001320000001 0 0 IN IP4 192.168.8.9\r\n" +
                "s=Play\r\n" +
                "c=IN IP4 192.168.8.150\r\n" +
                "t=0 0\r\n" +
                "m=video 6000 RTP/AVP 96 98 97\r\n" +
                "a=recvonly\r\n" +
                "a=rtpmap:96 PS/90000\r\n" +
                "a=rtpmap:98 H264/90000\r\n" +
                "a=rtpmap:97 MPEG4/90000\r\n" +
                "y=0100000001\r\n" +
                "f=";*/
        return builder.toString();
    }
}

运行SIP测试程序点击【点播】按钮,向SIP服务器发送Invite请求,收到服务器回复后通过点击【拉流】完成媒体流的点播全过程。

几款支持国标GB28181平台的视频监控设备接入方案 安防监控国标_服务器_07


至此GB2828规范中的新设备注册和视频流点播信令测试操作就完成,在和社会主流视频监控厂家(如:海康、大华)对接的时候,还要根据实际服务回复的信息做些参数调整,总之万变不其宗。