java 对接设备的代码资料较少,这里介绍
GB2818
的基本对接流程,有用自取👇
- java负责SIP信令的注册交互,推流、拉流鉴权
- 摄像头负责推流、流媒体负责拉流、转码
wvp-GB28181-pro项目 ,如果java对接各种摄像头,这个项目很👍,比较完善,可参考。进去
star
支持一波
做到需要播放摄像头视频需要:
- 摄像头:视频摄像数据的输出端,协议输出端。
- SIP服务端:java开发sip信令注册交互,流媒体(推流、播放)鉴权
- 流媒体服务器:负责注册rtp流媒体,摄像头推流输出端
概念:
国标协议2818 组成:
- SIP:会话初始协议(Session Initiation Protocol),是一个应用层的 点对点协议,用于初始、管理和终止网络中的语音和视频会话,是 GB28181 的核心之一
- 流媒体:音视频流的传输与转换
sip服务端:
- java实现一般采用
JAIN-SIP
- 项目启动时,初始化tpc、udp端口监听,当有接收sip信令时,会触发相关请求事件
流媒体:
- 用作视频、音频流的接入服务的,拉流、推流、解编码服务端;
- 一般用
ZLMediaKit
用做流媒体服务器,C++11 性能高、部署较方便- https://gitee.com/xia-chu/ZLMediaKit?_from=gitee_search
配置摄像头接入:
sip注册交互流程:
拉流交互过程:
摄像头返回推流端口:
invite 抓包:
部分代码示例:
<dependency> <groupId>javax.sipgroupId> <artifactId>jain-sip-riartifactId> <version>1.3.0-91version> dependency>
初始监听端口:
- 参考 panll / wvp-GB28181-pro 项目,👆上面有地址
import com.config.SipConfig;import com.sip.uitl.SipUtil;import gov.nist.javax.sip.SipProviderImpl;import gov.nist.javax.sip.SipStackImpl;import lombok.extern.slf4j.Slf4j;import org.springframework.boot.CommandLineRunner;import org.springframework.stereotype.Component;import org.springframework.util.ObjectUtils;import javax.annotation.Resource;import javax.sip.*;import java.util.*;import java.util.concurrent.ConcurrentHashMap;@Slf4j@Componentpublic class SipInitListen implements CommandLineRunner { @Resource private SipConfig sipConfig; @Resource private SipProcessListener sipProcessListener; private SipFactory sipFactory; private final Map<String, SipProviderImpl> tcpSipProviderMap = new ConcurrentHashMap<>(); private final Map<String, SipProviderImpl> udpSipProviderMap = new ConcurrentHashMap<>(); @Override public void run(String... args) { List<String> monitorIps = new ArrayList<>(); // 使用逗号分割多个ip String separator = ","; if (sipConfig.getIp().indexOf(separator) > 0) { String[] split = sipConfig.getIp().split(separator); monitorIps.addAll(Arrays.asList(split)); }else { monitorIps.add(sipConfig.getIp()); } sipFactory = SipFactory.getInstance(); sipFactory.setPathName("gov.nist"); if (monitorIps.size() > 0) { for (String monitorIp : monitorIps) { addListeningPoint(monitorIp, sipConfig.getPort()); } if (udpSipProviderMap.size() + tcpSipProviderMap.size() == 0) { System.exit(1); } } } private void addListeningPoint(String monitorIp, int port){ //sip协议栈 SipStackImpl sipStack; try { sipStack = (SipStackImpl)sipFactory.createSipStack(SipUtil.defaultProperties(monitorIp, Boolean.FALSE)); } catch (PeerUnavailableException e) { e.printStackTrace(); log.error("[Sip Server] SIP服务启动失败, 监听地址{}失败,请检查ip是否正确", monitorIp); return; } try { //创建 TCP传输监听 ListeningPoint tcpListeningPoint = sipStack.createListeningPoint(monitorIp, port, "TCP"); //tcp 消息处理实现 SipProviderImpl tcpSipProvider = (SipProviderImpl)sipStack.createSipProvider(tcpListeningPoint); tcpSipProvider.setDialogErrorsAutomaticallyHandled(); tcpSipProvider.addSipListener(sipProcessListener); tcpSipProviderMap.put(monitorIp, tcpSipProvider); log.info("[Sip Server] tcp://{}:{} 启动成功", monitorIp, port); } catch (TransportNotSupportedException | TooManyListenersException | ObjectInUseException | InvalidArgumentException e) { log.error("[Sip Server] tcp://{}:{} SIP服务启动失败,请检查端口是否被占用或者ip是否正确" , monitorIp, port); } try { //创建 UDP传输监听 ListeningPoint udpListeningPoint = sipStack.createListeningPoint(monitorIp, port, "UDP"); //udp 消息处理实现 SipProviderImpl udpSipProvider = (SipProviderImpl)sipStack.createSipProvider(udpListeningPoint); udpSipProvider.addSipListener(sipProcessListener); udpSipProviderMap.put(monitorIp, udpSipProvider); log.info("[Sip Server] udp://{}:{} 启动成功", monitorIp, port); } catch (TransportNotSupportedException | TooManyListenersException | ObjectInUseException | InvalidArgumentException e) { log.error("[Sip Server] udp://{}:{} SIP服务启动失败,请检查端口是否被占用或者ip是否正确" , monitorIp, port); } } public SipFactory getSipFactory() { return sipFactory; } public SipProviderImpl getUdpSipProvider(String ip) { if (ObjectUtils.isEmpty(ip)) { return null; } return udpSipProviderMap.get(ip); } public SipProviderImpl getUdpSipProvider() { if (udpSipProviderMap.size() != 1) { return null; } return udpSipProviderMap.values().stream().findFirst().get(); } public SipProviderImpl getTcpSipProvider() { if (tcpSipProviderMap.size() != 1) { return null; } return tcpSipProviderMap.values().stream().findFirst().get(); } public SipProviderImpl getTcpSipProvider(String ip) { if (ObjectUtils.isEmpty(ip)) { return null; } return tcpSipProviderMap.get(ip); } public String getLocalIp(String deviceLocalIp) { if (!ObjectUtils.isEmpty(deviceLocalIp)) { return deviceLocalIp; } return getUdpSipProvider().getListeningPoint().getIPAddress(); }}
摄像头消息监听
import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Component;import javax.annotation.Resource;import javax.sip.*;import javax.sip.header.CSeqHeader;import javax.sip.message.Response;@Component@Slf4jpublic class SipProcessListener implements SipListener { @Resource private SipEventService sipEventService; @Override public void processRequest(RequestEvent requestEvent) { log.info("收到摄像机服务请求"); String method = requestEvent.getRequest().getMethod(); log.info("method:"+method); if (method.equals("REGISTER")){ sipEventService.requestRegister(requestEvent); } if (method.equals("MESSAGE")){ sipEventService.requestMessage(requestEvent); } if (method.equals("BYE")){ sipEventService.requestBye(requestEvent); } } @Override public void processResponse(ResponseEvent responseEvent) { log.info("收到摄像机服务响应"); 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(); log.info("method:"+method); sipEventService.responseInvite(responseEvent); } else if ((status >= Response.TRYING) && (status < Response.OK)) { // 增加其它无需回复的响应,如101、180等 } else { log.warn("接收到失败的response响应!status:" + status + ",message:" + response.getReasonPhrase()); if (responseEvent.getDialog() != null) { responseEvent.getDialog().delete(); } } } @Override public void processTimeout(TimeoutEvent timeoutEvent) { log.info("收到摄像机 超时回调"); } @Override public void processIOException(IOExceptionEvent exceptionEvent) { log.info("收到摄像机 IO异常的回调"); } @Override public void processTransactionTerminated(TransactionTerminatedEvent transactionTerminatedEvent) { log.info("收到摄像机 事务中断回调"); } @Override public void processDialogTerminated(DialogTerminatedEvent dialogTerminatedEvent) { log.info("收到摄像机 对话框关闭事件"); }}
业务处理 service
import com.sip.bean.Device;import com.sip.bean.SSRCInfo;import javax.sip.RequestEvent;import javax.sip.ResponseEvent;public interface SipEventService { void requestRegister(RequestEvent requestEvent); void requestMessage(RequestEvent requestEvent); void responseInvite(ResponseEvent responseEvent); void requestBye(RequestEvent requestEvent); void sendInvite(Device device, SSRCInfo ssrcInfo); void getDeviceInfo(Device device);}
@Slf4j@Servicepublic class SipEventServiceImpl implements SipEventService { @Resource private RedisTemplate<String,Object> redisTemplate; @Resource private SipConfig sipConfig; @Resource private SIPSender sipSender; @Resource private SipInitListen sipInitListen; @Resource private SipProcessResponse sipProcessResponse; @Resource private SIPRequestHeaderProvider requestHeaderProvider; @SneakyThrows @Override public void requestRegister(RequestEvent evt) { RequestEventExt evtExt = (RequestEventExt) evt; String requestAddress = evtExt.getRemoteIpAddress() + ":" + evtExt.getRemotePort(); log.info("[注册请求] 开始处理: {}", requestAddress); SIPRequest sipRequest = (SIPRequest) evt.getRequest(); Response response = null; //密码是否正确 boolean passwordCorrect = false; // 注册标志 boolean registerFlag; FromHeader fromHeader = (FromHeader) sipRequest.getHeader(FromHeader.NAME); AddressImpl address = (AddressImpl) fromHeader.getAddress(); SipUri uri = (SipUri) address.getURI(); //设备ID(保留) String deviceId = uri.getUser(); //是否携带认证信息 AuthorizationHeader authHead = (AuthorizationHeader) sipRequest.getHeader(AuthorizationHeader.NAME); String password = sipConfig.getPassword(); if (authHead == null) { log.info("[注册请求] 摄像头未携带认证信息"); log.info("[注册请求] 回复401: {}", requestAddress); response = sipProcessResponse.getMessageFactory().createResponse(Response.UNAUTHORIZED, sipRequest); new DigestServerAuthenticationHelper().generateChallenge(sipProcessResponse.getHeaderFactory(), response, sipConfig.getDomain()); sipSender.send(sipRequest.getLocalAddress().getHostAddress(), response); return; } passwordCorrect = new DigestServerAuthenticationHelper().doAuthenticatePlainTextPassword(sipRequest, password); //密码验证失败 if (!passwordCorrect) { // 注册失败 log.info("[注册请求] 携带认证信息,但是密码验证错误"); response = sipProcessResponse.getMessageFactory().createResponse(Response.FORBIDDEN, sipRequest); response.setReasonPhrase("wrong password"); log.info("[注册请求] 密码/SIP服务器ID错误, 回复403: {}", requestAddress); sipSender.send(sipRequest.getLocalAddress().getHostAddress(), response); return; } // 携带授权头并且密码正确 response = sipProcessResponse.getMessageFactory().createResponse(Response.OK, sipRequest); // 添加date头 SIPDateHeader dateHeader = new SIPDateHeader(); // 使用自己修改的 WvpSipDate wvpSipDate = new WvpSipDate(Calendar.getInstance(Locale.ENGLISH).getTimeInMillis()); dateHeader.setDate(wvpSipDate); response.addHeader(dateHeader); if (sipRequest.getExpires() == null) { response = sipProcessResponse.getMessageFactory().createResponse(Response.BAD_REQUEST, sipRequest); sipSender.send(sipRequest.getLocalAddress().getHostAddress(), response); return; } // 添加Contact头 response.addHeader(sipRequest.getHeader(ContactHeader.NAME)); // 添加Expires头 response.addHeader(sipRequest.getExpires()); RemoteAddressInfo remoteAddressInfo = SipUtil.getRemoteAddressFromRequest(sipRequest,false); String key = "camera-device:"+deviceId; Device device = (Device)redisTemplate.opsForValue().get(key); if (device == null) { device = new Device(); device.setStreamMode("UDP"); device.setCharset("GB2312"); device.setGeoCoordSys("WGS84"); device.setTreeType("CivilCode"); device.setDeviceId(deviceId); device.setOnline(0); } device.setIp(remoteAddressInfo.getIp()); device.setPort(remoteAddressInfo.getPort()); device.setHostAddress(remoteAddressInfo.getIp().concat(":").concat(String.valueOf(remoteAddressInfo.getPort()))); device.setLocalIp(sipRequest.getLocalAddress().getHostAddress()); if (sipRequest.getExpires().getExpires() == 0) { // 注销成功 registerFlag = false; } else { // 注册成功 device.setExpires(sipRequest.getExpires().getExpires()); registerFlag = true; // 判断TCP还是UDP ViaHeader reqViaHeader = (ViaHeader) sipRequest.getHeader(ViaHeader.NAME); String transport = reqViaHeader.getTransport(); device.setTransport("TCP".equalsIgnoreCase(transport) ? "TCP" : "UDP"); } sipSender.send(sipRequest.getLocalAddress().getHostAddress(), response); // 注册成功 // 保存到redis if (registerFlag) { log.info("[注册成功] deviceId: {}->{}", deviceId, requestAddress); device.setRegisterTime(DateUtil.getNow()); device.setOnline(1); } else { log.info("[注销成功] deviceId: {}->{}", deviceId, requestAddress); device.setOnline(0); } redisTemplate.opsForValue().set(key,device); } @Override public void requestMessage(RequestEvent evt) { SIPRequest sipRequest = (SIPRequest)evt.getRequest(); log.info("接收到消息:" + evt.getRequest()); String deviceId = SipUtil.getUserIdFromFromHeader(evt.getRequest()); CallIdHeader callIdHeader = sipRequest.getCallIdHeader(); SIPRequest request = (SIPRequest) evt.getRequest(); // 查询设备是否存在 String key = "camera-device:"+deviceId; Device device = (Device)redisTemplate.opsForValue().get(key); // 查询上级平台是否存在 try { if (device == null) { // 不存在则回复404 sipProcessResponse.responseAck(request, Response.NOT_FOUND, "device "+ deviceId +" not found"); log.warn("[设备未找到 ]deviceId: {}, callId: {}", deviceId, callIdHeader.getCallId()); }else { Element rootElement = null; try { rootElement = sipProcessResponse.getRootElement(evt); if (rootElement == null) { log.error("处理MESSAGE请求 未获取到消息体{}", evt.getRequest()); sipProcessResponse.responseAck(request, Response.BAD_REQUEST, "content is null"); return; } //message 命令类型 String cmdType = XmlUtil.getText(rootElement, "CmdType"); switch (cmdType){ case "DeviceInfo"://厂家String manufacturer = XmlUtil.getText(rootElement, "Manufacturer");String Channel = XmlUtil.getText(rootElement, "DeviceID");sipProcessResponse.responseAck(sipRequest, Response.OK);return; case "Keepalive":sipProcessResponse.responseAck(sipRequest, Response.OK,"注册成功");return; default:sipProcessResponse.responseAck(sipRequest, Response.OK); } } catch (DocumentException e) { log.warn("解析XML消息内容异常", e); // 不存在则回复404 sipProcessResponse.responseAck(request, Response.BAD_REQUEST, e.getMessage()); } } } catch (SipException e) { log.warn("SIP 回复错误", e); } catch (InvalidArgumentException e) { log.warn("参数无效", e); } catch (ParseException e) { log.warn("SIP回复时解析异常", e); } } @Override public void responseInvite(ResponseEvent evt) { log.debug("响应invite:" + evt.getResponse()); try { SIPResponse response = (SIPResponse)evt.getResponse(); int statusCode = response.getStatusCode(); // trying不会回复 if (statusCode == Response.TRYING) { } // 成功响应 // 下发ack if (statusCode == Response.OK) { log.info("回复ACK,准备推流"); ResponseEventExt event = (ResponseEventExt)evt; String contentString = new String(response.getRawContent()); // jainSip不支持y=字段, 移除以解析。 int ssrcIndex = contentString.indexOf("y="); // 检查是否有y字段 SessionDescription sdp; if (ssrcIndex >= 0) { //ssrc规定长度为10字节,不取余下长度以避免后续还有“f=”字段 String substring = contentString.substring(0, contentString.indexOf("y=")); sdp = SdpFactory.getInstance().createSessionDescription(substring); } else { sdp = SdpFactory.getInstance().createSessionDescription(contentString); } SipURI requestUri = sipInitListen.getSipFactory().createAddressFactory().createSipURI(sdp.getOrigin().getUsername(), event.getRemoteIpAddress() + ":" + event.getRemotePort()); Request reqAck = requestHeaderProvider.createAckRequest(response.getLocalAddress().getHostAddress(), requestUri, response); log.info("[回复ack] {}-> {}:{} ", sdp.getOrigin().getUsername(), event.getRemoteIpAddress(), event.getRemotePort()); sipSender.send( response.getLocalAddress().getHostAddress(), reqAck); } } catch (InvalidArgumentException | ParseException | SipException | SdpParseException e) { log.info("[点播回复ACK],异常:", e ); } } @Override public void requestBye(RequestEvent evt) { log.info("处理BYE请求"); try { sipProcessResponse.responseAck((SIPRequest) evt.getRequest(), Response.OK); } catch (SipException | InvalidArgumentException | ParseException e) { log.error("[回复BYE信息失败],{}", e.getMessage()); } } @Override public void sendInvite(Device device,SSRCInfo ssrcInfo) { try { String channelId="34020000001320000001"; String sdpIp; if (!ObjectUtils.isEmpty(device.getSdpIp())) { sdpIp = device.getSdpIp(); }else { sdpIp = "192.168.1.250"; } //封装 sdp协议信息,告诉摄像头 推流配置 StringBuffer content = new StringBuffer(200); content.append("v=0\r\n"); content.append("o=" + channelId + " 0 0 IN IP4 " + sdpIp + "\r\n"); content.append("s=Play\r\n"); content.append("c=IN IP4 " + sdpIp + "\r\n"); content.append("t=0 0\r\n"); //is -SDP if (Boolean.FALSE) { if ("TCP-PASSIVE".equalsIgnoreCase(device.getStreamMode())) { content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 126 125 99 34 98 97\r\n"); } else if ("TCP-ACTIVE".equalsIgnoreCase(device.getStreamMode())) { content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 126 125 99 34 98 97\r\n"); } else if ("UDP".equalsIgnoreCase(device.getStreamMode())) { content.append("m=video " + ssrcInfo.getPort() + " RTP/AVP 96 126 125 99 34 98 97\r\n"); } content.append("a=recvonly\r\n"); content.append("a=rtpmap:96 PS/90000\r\n"); content.append("a=fmtp:126 profile-level-id=42e01e\r\n"); content.append("a=rtpmap:126 H264/90000\r\n"); content.append("a=rtpmap:125 H264S/90000\r\n"); content.append("a=fmtp:125 profile-level-id=42e01e\r\n"); content.append("a=rtpmap:99 H265/90000\r\n"); content.append("a=rtpmap:98 H264/90000\r\n"); content.append("a=rtpmap:97 MPEG4/90000\r\n"); if ("TCP-PASSIVE".equalsIgnoreCase(device.getStreamMode())) { // tcp被动模式 content.append("a=setup:passive\r\n"); content.append("a=connection:new\r\n"); } else if ("TCP-ACTIVE".equalsIgnoreCase(device.getStreamMode())) { // tcp主动模式 content.append("a=setup:active\r\n"); content.append("a=connection:new\r\n"); } } else { if ("TCP-PASSIVE".equalsIgnoreCase(device.getStreamMode())) { content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 97 98 99\r\n"); } else if ("TCP-ACTIVE".equalsIgnoreCase(device.getStreamMode())) { content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 97 98 99\r\n"); } else if ("UDP".equalsIgnoreCase(device.getStreamMode())) { content.append("m=video " + ssrcInfo.getPort() + " RTP/AVP 96 97 98 99\r\n"); } content.append("a=recvonly\r\n"); content.append("a=rtpmap:96 PS/90000\r\n"); content.append("a=rtpmap:98 H264/90000\r\n"); content.append("a=rtpmap:97 MPEG4/90000\r\n"); content.append("a=rtpmap:99 H265/90000\r\n"); if ("TCP-PASSIVE".equalsIgnoreCase(device.getStreamMode())) { // tcp被动模式 content.append("a=setup:passive\r\n"); content.append("a=connection:new\r\n"); } else if ("TCP-ACTIVE".equalsIgnoreCase(device.getStreamMode())) { // tcp主动模式 content.append("a=setup:active\r\n"); content.append("a=connection:new\r\n"); } } content.append("y=" + ssrcInfo + "\r\n");//ssrc System.out.println(content); CallIdHeader newCallIdHeader=sipSender.getNewCallIdHeader(sipInitListen.getLocalIp(device.getLocalIp()),device.getTransport()); Request request = requestHeaderProvider.createInviteRequest(device, channelId, content.toString(), SipUtil.getNewViaTag(), SipUtil.getNewFromTag(), null, ssrcInfo.getSsrc(), newCallIdHeader); sipSender.send(sipConfig.getIp(),request); } catch (ParseException | InvalidArgumentException | PeerUnavailableException e) { throw new RuntimeException(e); } } @Override public void getDeviceInfo(Device device) { try { sipSender.deviceInfoQuery(device); } catch (InvalidArgumentException | SipException | ParseException e) { throw new RuntimeException(e); } }}
消息发送:
@Slf4j@Componentpublic class SIPSender { @Autowired private SipInitListen sipInitListen; @Autowired private SIPRequestHeaderProvider headerProvider; @SneakyThrows public void send(String ip, Message message) { ViaHeader viaHeader = (ViaHeader)message.getHeader(ViaHeader.NAME); String transport = "UDP"; if (viaHeader == null) { log.warn("[消息头缺失]: ViaHeader, 使用默认的UDP方式处理数据"); }else { transport = viaHeader.getTransport(); } if (message.getHeader(UserAgentHeader.NAME) == null) { message.addHeader(SipUtil.createUserAgentHeader(sipInitListen.getSipFactory())); } switch (transport){ case "TCP": sendTCP(ip,message); return; case "UDP": sendUDP(ip,message); return; default: sendTCP(ip,message); } } private boolean sendUDP(String ip, Message message) throws SipException { SipProviderImpl sipProvider = sipInitListen.getUdpSipProvider(ip); if (sipProvider == null) { log.error("[发送信息失败] 未找到udp://{}的监听信息", ip); return true; } if (message instanceof Request) { sipProvider.sendRequest((Request) message); }else if (message instanceof Response) { sipProvider.sendResponse((Response) message); } return false; } private boolean sendTCP(String ip, Message message) throws SipException { SipProviderImpl tcpSipProvider = sipInitListen.getTcpSipProvider(ip); if (tcpSipProvider == null) { log.error("[发送信息失败] 未找到tcp://{}的监听信息", ip); return true; } if (message instanceof Request) { tcpSipProvider.sendRequest((Request) message); }else if (message instanceof Response) { tcpSipProvider.sendResponse((Response) message); } return false; } public CallIdHeader getNewCallIdHeader(String ip, String transport){ if (ObjectUtils.isEmpty(transport)) { return sipInitListen.getUdpSipProvider().getNewCallId(); } SipProviderImpl sipProvider; if (ObjectUtils.isEmpty(ip)) { sipProvider = transport.equalsIgnoreCase("TCP") ? sipInitListen.getTcpSipProvider() : sipInitListen.getUdpSipProvider(); }else { sipProvider = transport.equalsIgnoreCase("TCP") ? sipInitListen.getTcpSipProvider(ip) : sipInitListen.getUdpSipProvider(ip); } if (sipProvider == null) { sipProvider = sipInitListen.getUdpSipProvider(); } if (sipProvider != null) { return sipProvider.getNewCallId(); }else { log.warn("[新建CallIdHeader失败], ip={}, transport={}", ip, transport); return null; } } public void deviceInfoQuery(Device device) throws InvalidArgumentException, SipException, ParseException { StringBuffer catalogXml = new StringBuffer(200); String charset = device.getCharset(); catalogXml.append(" + charset + "\"?>\r\n"); catalogXml.append("\r\n" ); catalogXml.append("DeviceInfo \r\n"); catalogXml.append("" + (int) ((Math.random() * 9 + 1) * 100000) + "\r\n"); catalogXml.append("" + device.getDeviceId() + "\r\n"); catalogXml.append("\r\n"); Request request = headerProvider.createMessageRequest(device, catalogXml.toString(), SipUtil.getNewViaTag(), SipUtil.getNewFromTag(), null,getNewCallIdHeader(sipInitListen.getLocalIp(device.getLocalIp()),device.getTransport())); send(sipInitListen.getLocalIp(device.getLocalIp()), request); }}
来源地址:https://blog.csdn.net/hesqlplus730/article/details/130791859