通过仔细学习《GBT 28181-2016 公共安全视频监控联网系统信息传输、交换、控制技术要求》技术规范文档。整篇主要是基于SIP协议对视频监控系统互联中涉及的消息内容格式进行了规范,由于GB28181是适用于各个领域各种情况下的视频监控系统信令交互,涉及的信令太多,在实际应用中往往只使用其中一部份信令,今天我主要讲几个最常用的信令应用。
设备注册
当有新的监控设备要接入视频监控平台时,首先要在服务端进行注册操作,其中涉及用户注册时身份认证是本信令的难点。
- SIP代理终端(视频监控设备)向 SIP服务器发送 Register请求并携带设备信息,表明有新设备要接入监控平台。
- SIP服务器向SIP代理终端回401,增加响应消息头域 WWW-Authenticate信息,要求终端进行身份验证。
- SIP代理终端收到401响应后,取出nonce字段,根据服务端身份认证要求对设备信息进行加密处理(一般是MD5),携带认证信息重新发起Register请求。
- 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);
}
}
}
测试工具程序运行结果如下图
通过点击【注册】按钮,测试程序向服务端发送Register请求,但服务端一般都会回复401响应,要求客户端身份认证。
客户端收到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);
}
}
到这里可以看到,服务器返回“设备ID 注册成功”,表明新客户端注册完成。
媒体点播
在GB28182规范所有应用中,要说最常用的信令,必然是媒体流点播,这个信令如果从规范里看是有些复杂,它规定了媒体流在网络中传输涉及整个生命周期的所有细节。所以这个信令从结构上看就显的繁琐,但必须承认从理想方面设计每个细节的规定都是合理的,可惜现实与理想总是有些差距。在实际的项目应用中都会对此规范进行不同程度的简化。
Invite标准版
- 媒体流接收者向SIP服务器发送携带SDP消息体的Invite消息。
- SIP服务器收到Invite请求后,向媒体服务器转发送此Invite消息,此消息不携带SDP消息体。
- 媒体服务器收到SIP服务器的Invite请求后,回复200OK 响应,携带SDP消息体。
- SIP服务器收到媒体服务器返回的200OK 响应后,向媒体流发送者发送携带SDP消息的Invite请求。
- 媒体流发送者收到SIP服务器的Invite请求后,回复200OK 响应,携带SDP消息体。
- SIP服务器收到媒体流发送者返回的200OK 响应后,向媒体服务器发送 ACK 请求。
- SIP服务器收到媒体流发送者返回的200OK 响应后,向媒体流发送者发送 ACK 请求,表明媒体流发送者的Invite会话建立过程,媒体流发送者收到ACK请求后开始推流。
- SIP服务器向媒体服务器发送Invite,表明Invite请求已建立。
- 媒体服务器回复200,表明已开始接收数据流信息,返回携带SDP的信息。
- SIP服务器将消息9转发给媒体流接收者。
- 媒体流接收者收到200OK 响应后,回复 ACK 消息,完成与SIP服务器的Invite会话建立
过程。 - SIP服务器将消息11转发给媒体服务器,完成与媒体服务器的Invite会话建立过程。
- 媒体流接收者向SIP服务器发送 BYE消息,断开消息1、10、11建立的同媒体流接收者的
Invite会话。 - SIP服务器收到 BYE消息后回复200OK 响应,会话断开。
- SIP服务器收到 BYE消息后向媒体服务器发送 BYE消息,断开消息8、9、12建立的同媒体
服务器的Invite会话。 - 媒体服务器收到 BYE消息后回复200OK 响应,会话断开。
- SIP服务器向媒体服务器发送 BYE 消息,断开消息2、3、6建立的同媒体服务器的Invite
会话。 - 媒体服务器收到 BYE消息后回复200OK 响应,会话断开。
- SIP 服务器向媒体流发送者发送 BYE 消息,断开消息4、5、7建立的同媒体流发送者的
Invite会话。 - 媒体流发送者收到 BYE消息后回复200OK 响应,会话断开
Invite简化版
- 媒体流接收者向SIP服务器发送携带SDP消息体的Invite消息。
- SIP服务器收到Invite消息后更新媒体服务器的信息转发给媒体发送者。
- 媒体发送者收到Invite后,回复携带SDP信息的ok响应。
- SIP服务器收到ok后根据媒体服务器的信息回复媒体流接收者ok消息。
- 媒体发送者开始向媒体服务器尝试推流。
- 媒体流接收者根据回复消息从媒体服务器拉流播放。
- 媒体流接收者在挂断的时候向SIP服务器发送Bye消息。
- SIP服务器收到Bye后,立刻向媒体发送者转发此挂断请求。
- 媒体发送者收到Bye后,立刻停止推流,并回复Ok消息。
- 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请求,收到服务器回复后通过点击【拉流】完成媒体流的点播全过程。
至此GB2828规范中的新设备注册和视频流点播信令测试操作就完成,在和社会主流视频监控厂家(如:海康、大华)对接的时候,还要根据实际服务回复的信息做些参数调整,总之万变不其宗。