wvp-GB28181-pro 源码分析-查询设备信息和通道流程(二)

文章目录

  • 一、SIP通信方法介绍
    • 1.1 核心方法(Core Methods)
    • 1.2 扩展方法(Extension Methods)
  • 二、源码分析
    • 2.1 SIP协议处理过程
    • 2.2 查询设备信息的sip过程(CmdType=DeviceInfo)
      • 2.2.1 摄像机注册成功后,wvp会发命令查询设备信息
      • 2.2.2 查询信令发出
      • 2.2.3 处理设备查询返回的XML
    • 2.3 查询设备通道(目录)的sip过程(CmdType=Catalog)


2024年6月20日下载的wvp-GB28181-pro,版本号为2.7.2,使用ZLMediakit主干版本。

本节阐述sip通信步骤对应到wvp-gb28181-Pro中的代码。

一、SIP通信方法介绍

sip通信主要有6个核心方法和8个扩展方法。

1.1 核心方法(Core Methods)

SIP请求消息(方法Method)SIP操作
INVITE会话邀请
ACK确认会话邀请
CANCEL取消会话邀请
BYE结束会话
REGISTER注册
OPTIONS查询服务器能力

1.2 扩展方法(Extension Methods)

SIP请求消息(方法Method)SIP操作
Subscribe与所使用的用户代理商建立订阅,获取有关特定事件的通知
NOTIFYNOTIFY是用来由用户代理传达特定事件的发生
PUBLISHPUBLISH用于由用户代理发送的事件的状态信息
REFERREFER用于由一个用户代理来指另一个用户代理访问URI的对话框
INFOINFO所使用的用户代理发送呼叫信令信息,与它建立了一个媒体会话其他用户代理
UPDATEUPDATE用于修改会话的状态不改变对话的状态
PRACKPRACK用于确认收到临时响应(1XX)可靠传输
MESSAGE用来发送即时消息

二、源码分析

2.1 SIP协议处理过程

wvp针对这14个方法(注意wvp并没有全部实现14个方法),相应写了处理对象,如下图
在这里插入图片描述
将这些处理器RegisterRequestProcessor、MessageRequestProcessor等等Processor加入到SIPProcessorObserver观察者的容器中。
每一个processor中都有这个方法,用来添加消息处理的订阅。processor方法实现了InitializingBean类(不懂的可以去查),通过afterPropertiesSet()方法完成了消息的订阅。

@Override
public void afterPropertiesSet() throws Exception {
    // 添加消息处理的订阅
    sipProcessorObserver.addRequestProcessor(method, this);
}

当有相应方法的sip消息到来时,根据是Request还是Response,观察者将调用相应的Processor进行处理。
观察者SIPProcessorObserver类,在如下的包路径下

com.genersoft.iot.vmp.gb28181.transmit

观察者中的处理Request源码如下

    public void processRequest(RequestEvent requestEvent) {
        String method = requestEvent.getRequest().getMethod();
        ISIPRequestProcessor sipRequestProcessor = requestProcessorMap.get(method);
        if (sipRequestProcessor == null) {
            logger.warn("不支持方法{}的request", method);
            // TODO 回复错误玛
            return;
        }
        requestProcessorMap.get(method).process(requestEvent);

    }

处理Response的源码如下

    @Override
    @Async("taskExecutor")
    public void processResponse(ResponseEvent responseEvent) {
        Response response = responseEvent.getResponse();
        int status = response.getStatusCode();

        // Success
        if (((status >= Response.OK) && (status < Response.MULTIPLE_CHOICES)) || status == Response.UNAUTHORIZED) {
            CSeqHeader cseqHeader = (CSeqHeader) responseEvent.getResponse().getHeader(CSeqHeader.NAME);
            String method = cseqHeader.getMethod();
            ISIPResponseProcessor sipRequestProcessor = responseProcessorMap.get(method);
            if (sipRequestProcessor != null) {
                sipRequestProcessor.process(responseEvent);
            }
            if (status != Response.UNAUTHORIZED && responseEvent.getResponse() != null && sipSubscribe.getOkSubscribesSize() > 0 ) {
                CallIdHeader callIdHeader = (CallIdHeader)responseEvent.getResponse().getHeader(CallIdHeader.NAME);
                if (callIdHeader != null) {
                    SipSubscribe.Event subscribe = sipSubscribe.getOkSubscribe(callIdHeader.getCallId());
                    if (subscribe != null) {
                        SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult(responseEvent);
                        sipSubscribe.removeOkSubscribe(callIdHeader.getCallId());
                        subscribe.response(eventResult);
                    }
                }
            }
        } else if ((status >= Response.TRYING) && (status < Response.OK)) {
            // 增加其它无需回复的响应,如101、180等
        } else {
            logger.warn("接收到失败的response响应!status:" + status + ",message:" + response.getReasonPhrase());
            if (responseEvent.getResponse() != null && sipSubscribe.getErrorSubscribesSize() > 0 ) {
                CallIdHeader callIdHeader = (CallIdHeader)responseEvent.getResponse().getHeader(CallIdHeader.NAME);
                if (callIdHeader != null) {
                    SipSubscribe.Event subscribe = sipSubscribe.getErrorSubscribe(callIdHeader.getCallId());
                    if (subscribe != null) {
                        SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult(responseEvent);
                        subscribe.response(eventResult);
                        sipSubscribe.removeErrorSubscribe(callIdHeader.getCallId());
                    }
                }
            }
            if (responseEvent.getDialog() != null) {
                responseEvent.getDialog().delete();
            }
        }


    }

由于MessageRequestProcessor类型的消息较多,wvp将Message再分为多个handler,分别在对应的包下面,然后根据cmdType调用对应的handler。如cmdType = “Catalog”、cmdType = “DeviceInfo”。

在这里插入图片描述

2.2 查询设备信息的sip过程(CmdType=DeviceInfo)

设备信息查询流程示意图如下:
在这里插入图片描述

对应《GBT 28181-2016 公共安全视频监控联网系统信息传输、交换、控制技术要求》文件中的J.10.1到J.10.8节。如果需要GB文件,可以评论区留下邮箱,我看到就会发送(私信不发)
在这里插入图片描述

2.2.1 摄像机注册成功后,wvp会发命令查询设备信息

在RegisterRequestProcessor中会调用online方法,这里看不明白的话,可以去看 wvp-GB28181-pro 源码分析-服务启动流程及IPC注册(一)这篇文章。
在这里插入图片描述
online方法将device信息写到redis和mysql
在这里插入图片描述

2.2.2 查询信令发出

在这里插入图片描述
deviceInfoQuery方法会发出一个message请求,代码如下:

    /**
     * 查询设备信息
     *
     * @param device 视频设备
     */
    @Override
    public void deviceInfoQuery(Device device) throws InvalidArgumentException, SipException, ParseException {

        StringBuffer catalogXml = new StringBuffer(200);
        String charset = device.getCharset();
        catalogXml.append("<?xml version=\"1.0\" encoding=\"" + charset + "\"?>\r\n");
        catalogXml.append("<Query>\r\n");
        catalogXml.append("<CmdType>DeviceInfo</CmdType>\r\n");
        catalogXml.append("<SN>" + (int) ((Math.random() * 9 + 1) * 100000) + "</SN>\r\n");
        catalogXml.append("<DeviceID>" + device.getDeviceId() + "</DeviceID>\r\n");
        catalogXml.append("</Query>\r\n");
        Request request = headerProvider.createMessageRequest(device, catalogXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport()));
        sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request);

    }

2.2.3 处理设备查询返回的XML

DeviceInfoResponseMessageHandler处理类在如下包路径下

com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.cmd

代码如下:

    @Override
    public void handForDevice(RequestEvent evt, Device device, Element rootElement) {
        logger.debug("接收到DeviceInfo应答消息");
        // 检查设备是否存在, 不存在则不回复
        if (device == null || !device.isOnLine()) {
            logger.warn("[接收到DeviceInfo应答消息,但是设备已经离线]:" + (device != null ? device.getDeviceId():"" ));
            return;
        }
        SIPRequest request = (SIPRequest) evt.getRequest();
        try {
            rootElement = getRootElement(evt, device.getCharset());

            if (rootElement == null) {
                logger.warn("[ 接收到DeviceInfo应答消息 ] content cannot be null, {}", evt.getRequest());
                try {
                    responseAck((SIPRequest) evt.getRequest(), Response.BAD_REQUEST);
                } catch (SipException | InvalidArgumentException | ParseException e) {
                    logger.error("[命令发送失败] DeviceInfo应答消息 BAD_REQUEST: {}", e.getMessage());
                }
                return;
            }
            Element deviceIdElement = rootElement.element("DeviceID");
            String channelId = deviceIdElement.getTextTrim();
            String key = DeferredResultHolder.CALLBACK_CMD_DEVICEINFO + device.getDeviceId() + channelId;
            device.setName(getText(rootElement, "DeviceName"));

            device.setManufacturer(getText(rootElement, "Manufacturer"));
            device.setModel(getText(rootElement, "Model"));
            device.setFirmware(getText(rootElement, "Firmware"));
            if (ObjectUtils.isEmpty(device.getStreamMode())) {
                device.setStreamMode("UDP");
            }
            deviceService.updateDevice(device);

            RequestMessage msg = new RequestMessage();
            msg.setKey(key);
            msg.setData(device);
            deferredResultHolder.invokeAllResult(msg);
        } catch (DocumentException e) {
            throw new RuntimeException(e);
        }
        try {
            // 回复200 OK
            responseAck(request, Response.OK);
        } catch (SipException | InvalidArgumentException | ParseException e) {
            logger.error("[命令发送失败] DeviceInfo应答消息 200: {}", e.getMessage());
        }

    }

设备信息 通过 deviceService.updateDevice(device),更新到redis和mysql,具体可以自己跟一下。

2.3 查询设备通道(目录)的sip过程(CmdType=Catalog)

流程示意图如下:
在这里插入图片描述

同样,在摄像机注册成功后,wvp发起查询设备的目录信息请求,同样在DeviceServiceImpl类中的online方法体中发起。
在这里插入图片描述

    @Override
    public void sync(Device device) {
        if (catalogResponseMessageHandler.isSyncRunning(device.getDeviceId())) {
            logger.info("开启同步时发现同步已经存在");
            return;
        }
        int sn = (int)((Math.random()*9+1)*100000);
        catalogResponseMessageHandler.setChannelSyncReady(device, sn);
        try {
            sipCommander.catalogQuery(device, sn, event -> {
                String errorMsg = String.format("同步通道失败,错误码: %s, %s", event.statusCode, event.msg);
                catalogResponseMessageHandler.setChannelSyncEnd(device.getDeviceId(), errorMsg);
            });
        } catch (SipException | InvalidArgumentException | ParseException e) {
            logger.error("[同步通道], 信令发送失败:{}", e.getMessage() );
            String errorMsg = String.format("同步通道失败,信令发送失败: %s", e.getMessage());
            catalogResponseMessageHandler.setChannelSyncEnd(device.getDeviceId(), errorMsg);
        }
    }

在CatalogResponseMessageHandler类中处理返回的XML 信令

@Override
    public void handForDevice(RequestEvent evt, Device device, Element element) {
        taskQueue.offer(new HandlerCatchData(evt, device, element));
        // 回复200 OK
        try {
            responseAck((SIPRequest) evt.getRequest(), Response.OK);
        } catch (SipException | InvalidArgumentException | ParseException e) {
            logger.error("[命令发送失败] 目录查询回复: {}", e.getMessage());
        }
        // 已经开启消息处理则跳过
        if (processing.compareAndSet(false, true)) {
            taskExecutor.execute(() -> {
                while (!taskQueue.isEmpty()) {
                    // 全局异常捕获,保证下一条可以得到处理
                    try {
                        HandlerCatchData take = taskQueue.poll();
                        Element rootElement = null;
                        try {
                            rootElement = getRootElement(take.getEvt(), take.getDevice().getCharset());
                        } catch (DocumentException e) {
                            logger.error("[xml解析] 失败: ", e);
                            continue;
                        }
                        if (rootElement == null) {
                            logger.warn("[ 收到通道 ] content cannot be null, {}", evt.getRequest());
                            continue;
                        }
                        Element deviceListElement = rootElement.element("DeviceList");
                        Element sumNumElement = rootElement.element("SumNum");
                        Element snElement = rootElement.element("SN");
                        int sumNum = Integer.parseInt(sumNumElement.getText());

                        if (sumNum == 0) {
                            logger.info("[收到通道]设备:{}的: 0个", take.getDevice().getDeviceId());
                            // 数据已经完整接收
                            storager.cleanChannelsForDevice(take.getDevice().getDeviceId());
                            catalogDataCatch.setChannelSyncEnd(take.getDevice().getDeviceId(), null);
                        } else {
                            Iterator<Element> deviceListIterator = deviceListElement.elementIterator();
                            if (deviceListIterator != null) {
                                List<DeviceChannel> channelList = new ArrayList<>();
                                List<String> parentChannelIds = new ArrayList<>();
                                // 遍历DeviceList
                                while (deviceListIterator.hasNext()) {
                                    Element itemDevice = deviceListIterator.next();
                                    Element channelDeviceElement = itemDevice.element("DeviceID");
                                    if (channelDeviceElement == null) {
                                        continue;
                                    }
                                    DeviceChannel channel = XmlUtil.channelContentHandler(itemDevice, device, null);
                                    if (channel == null) {
                                        logger.info("[收到目录订阅]:但是解析失败 {}", new String(evt.getRequest().getRawContent()));
                                        continue;
                                    }
                                    if (channel.getParentId() != null && channel.getParentId().equals(sipConfig.getId())) {
                                        channel.setParentId(null);
                                    }
                                    SipUtils.updateGps(channel, device.getGeoCoordSys());
                                    channel.setDeviceId(take.getDevice().getDeviceId());

                                    channelList.add(channel);
                                }
                                int sn = Integer.parseInt(snElement.getText());
                                catalogDataCatch.put(take.getDevice().getDeviceId(), sn, sumNum, take.getDevice(), channelList);
                                logger.info("[收到通道]设备: {} -> {}个,{}/{}", take.getDevice().getDeviceId(), channelList.size(), catalogDataCatch.get(take.getDevice().getDeviceId()) == null ? 0 : catalogDataCatch.get(take.getDevice().getDeviceId()).size(), sumNum);
                                if (catalogDataCatch.get(take.getDevice().getDeviceId()).size() == sumNum) {
                                    // 数据已经完整接收, 此时可能存在某个设备离线变上线的情况,但是考虑到性能,此处不做处理,
                                    // 目前支持设备通道上线通知时和设备上线时向上级通知
                                    boolean resetChannelsResult = storager.resetChannels(take.getDevice().getDeviceId(), catalogDataCatch.get(take.getDevice().getDeviceId()));
                                    if (!resetChannelsResult) {
                                        String errorMsg = "接收成功,写入失败,共" + sumNum + "条,已接收" + catalogDataCatch.get(take.getDevice().getDeviceId()).size() + "条";
                                        catalogDataCatch.setChannelSyncEnd(take.getDevice().getDeviceId(), errorMsg);
                                    } else {
                                        catalogDataCatch.setChannelSyncEnd(take.getDevice().getDeviceId(), null);
                                    }
                                }
                            }

                        }
                    } catch (Exception e) {
                        logger.warn("[收到通道] 发现未处理的异常, \r\n{}", evt.getRequest());
                        logger.error("[收到通道] 异常内容: ", e);
                    }
                }
                processing.set(false);
            });
        }

    }

本节设备信息查询和通道查询流程就梳理完了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值