心跳文本
//摄像头发送过来的Keepalive保活信息
MESSAGE sip:34020000002000000001@192.168.1.201:5060 SIP/2.0 //MESSAGE 方法名,类似http的get/post方法
Via: SIP/2.0/UDP 192.168.1.8:5060;rport;branch=z9hG4bK700933079 //这个不用理会,但需要拷贝该字段,回复200要用
From: <sip:34020000001110000003@192.168.1.8:5060>;tag=1896094222 //同上
To: <sip:34020000002000000001@192.168.1.201:5060> //同上
Call-ID: 78119256@192.168.1.8 //同上
CSeq: 20 MESSAGE //唯一标识
Max-Forwards: 70
User-Agent: Dahua SIP UAS V1.0
Content-Type: Application/MANSCDP+xml //消息体格式
Content-Length: 178 //消息体长度,不带消息头,下面会空一行才会读到消息体,解析时注意下读到空行
<?xml version="1.0" encoding="GB2312" ?> //编码
<Notify>
<CmdType>Keepalive</CmdType> //注意这里的消息体,大华发送的消息体有空格的,海康的消息体不带空格,需要做下处理,或者使用xml工具类,这里为了方便直接解析字符串
<SN>0</SN>
<DeviceID>34020000001110000003</DeviceID> //设备编号
<Status>OK</Status>
</Notify>
//服务器需要对其进行200回复,超过不回复次数就会断流
SIP/2.0 200 OK //回复200
CSeq: 20 MESSAGE //拷贝上面心跳信息
Call-ID: 78119256@192.168.1.8 //同上
From: <sip:34020000001110000003@192.168.1.8:5060>;tag=1896094222 //同上
To: <sip:34020000002000000001@192.168.1.201:5060> //同上
Via: SIP/2.0/UDP 192.168.1.8:5060;rport;branch=z9hG4bK700933079 //同上
Content-Length: 0 //没有消息体,直接写0
心跳保活回复模板
//摄像头心跳保活回复
private static final String str_Keepalive_ok=
"SIP/2.0 200 OK"+n+
"CSeq: {CSeq}"+n+
"Call-ID: {Call-ID}"+n+
"From: {From}"+n+
"To: {To}"+n+
"Via: {Via}"+n+
"Content-Length: 0"+n
+n;
心跳回复代码实现
//实现很简单,根据模板填充信息就行
//心跳和获取设备信息都是这个方法 获取设备信息自行抓包
if("MESSAGE".equals(map.get("method"))){
//心跳回复200,不回复会导致断流
if("Keepalive".equals(map.get("CmdType"))){
sendStr=str_Keepalive_ok;
sendStr=sendStr.replace("{CSeq}",map.get("CSeq"));
sendStr=sendStr.replace("{Call-ID}",map.get("Call-ID"));
sendStr=sendStr.replace("{From}",map.get("From"));
sendStr=sendStr.replace("{To}",map.get("To"));
sendStr=sendStr.replace("{Via}",map.get("Via"));
//获取ssrc更新,理论上ssrc不会变的,这里保险,每次都更新最新的
if(deviceInfo.getSsrc()==null){
String deviceId=map.get("deviceId");
deviceInfo.setSsrc(Utils.getSsrc("010000"+deviceId.substring(deviceId.length()-4)));
}
/*if(dataDeviceInfo.get(map.get("deviceId"))==null){
System.out.println("restart");
Map<String,String> d=new HashMap<>(2);
String via=map.get("Via");
via=via.split("\\s+")[1];
via=via.split(";")[0];
deviceInfo.setLocalIp(via.split(":")[0]);
deviceInfo.setLocalPort(via.split(":")[1]);
//断流检测
//bye(map.get("deviceId"));
}*/
}
}
推流文本
//推流需要发起invite请求 如果是大华的话这里的所有设备编号要换为通道id编号,所以为了方便起见,直接设置设备编号和通道id编号一样,海康的通道id无法设置,所以直接发设备编号也可以获取到流
INVITE sip:34020000001110000003@192.168.1.8:5060;transport=udp SIP/2.0
Call-ID: 34020000001110000003
CSeq: 1 INVITE
From: <sip:34020000002000000001@0.0.0.0:5060>;tag=live
To: "34020000001110000003" <sip:34020000001110000003@192.168.1.8:5060> //to这里不要加tag,加了的话海康会返回找到id,大华则没事
Via: SIP/2.0/UDP 0.0.0.0:5060;branch=branchlive
Max-Forwards: 70
Content-Type: Application/sdp //消息类型
Contact: <sip:34020000002000000001@0.0.0.0:5060>
Supported: 100re1 //这个好像不影响,百度的
Subject: 34020000001110000003:0100000003,34020000002000000001:0 //海康要加该请求头
User-Agent: fyl //自定义的一些信息
Content-Length: 232 //这里要计算的消息体大小
v=0
o=34020000001110000003 0 0 IN IP4 192.168.1.201
s=Play
c=IN IP4 192.168.1.201 //流媒体服务器ip
t=0 0
m=video 10002 TCP/RTP/AVP 96 98 97 //这里的TCP/RTP/AVP代表是tcp推流模式(tcp分为主动tcp和被动tcp,默认直接就是摄像头推流),udp推流为RTP/AVP 10002是流媒体服务器的端口号,建议偶数端口
a=recvonly
a=rtpmap:96 PS/90000
a=rtpmap:98 H264/90000
a=rtpmap:97 MPEG4/90000
y=0100000003 //这里y是ssrc信息,采用多个设备共享一个推流端口时需要使用这个标识来判断是那个摄像头的流
f=
//当我们发起推流请求之后,摄像头会回复3次信息,海康只回复2次,我们看最后一次信息回复就行
SIP/2.0 100 Trying
Via: SIP/2.0/UDP 0.0.0.0:5060;branch=branchlive;received=192.168.1.201
From: <sip:34020000002000000001@0.0.0.0:5060>;tag=live
To: "34020000001110000003" <sip:34020000001110000003@192.168.1.8:5060>
Call-ID: 34020000001110000003
CSeq: 1 INVITE
User-Agent: Dahua SIP UAS V1.0
Content-Length: 0
//第二次回复,测试的海康摄像头没有这个信息回复
SIP/2.0 101 Dialog Establishement
Via: SIP/2.0/UDP 0.0.0.0:5060;branch=branchlive;received=192.168.1.201
From: <sip:34020000002000000001@0.0.0.0:5060>;tag=live
To: "34020000001110000003" <sip:34020000001110000003@192.168.1.8:5060>;tag=29937641
Call-ID: 34020000001110000003
CSeq: 1 INVITE
Contact: <sip:34020000001110000003@192.168.1.8:5060>
User-Agent: Dahua SIP UAS V1.0
Content-Length: 0
//最后一次回复,注意这次回复,如果是200说明请求成功,只要我们回复ack摄像头就会开始推流,如果不是200说明信息有误,一般都是设备编号或者通道编号,再或者tag的一些信息有误,海康对tag比较敏感
SIP/2.0 200 OK
Via: SIP/2.0/UDP 0.0.0.0:5060;branch=branchlive;received=192.168.1.201
From: <sip:34020000002000000001@0.0.0.0:5060>;tag=live //这个字段要保存起来
To: "34020000001110000003" <sip:34020000001110000003@192.168.1.8:5060>;tag=29937641 //这个字段要也保存起来,可以看到tag是摄像头生成的
Call-ID: 34020000001110000003 //同上
CSeq: 1 INVITE //注意这里的标识,这个标识跟推流请求一样说明是对应推流的回复
Contact: <sip:34020000001110000003@192.168.1.8:5060>
User-Agent: Dahua SIP UAS V1.0
Content-Type: application/sdp
Content-Length: 260
v=0
o=34020000001110000003 0 0 IN IP4 192.168.1.8
s=Play
i=VCam Live Video
c=IN IP4 192.168.1.8
t=0 0
m=video 9702 TCP/RTP/AVP 96
a=sendonly
a=rtpmap:96 PS/90000
a=streamprofile:0
a=setup:active
a=connection:new
y=0100000003
f=v/0/0/0/0/0a/0/0/0
//收到200回复之后,保存from,to和Call-ID 这三个字段在断流时需要用到,如果三个字段有变动都无法进行断流,特别是海康,差一点都不行,会回复找不到callid
ACK sip:34020000001110000003@192.168.1.8:5060 SIP/2.0
Call-ID: 34020000001110000003
CSeq: 1 ACK
Via: SIP/2.0/UDP 34020000002000000001:5060;branch=z9hG4bK-353633-cc1d42f582e2ec40dfedb6812f9ab1b8
From: <sip:34020000002000000001@0.0.0.0:5060>;tag=live
To: "34020000001110000003" <sip:34020000001110000003@192.168.1.8:5060>;tag=29937641
Max-Forwards: 70
Content-Length: 0
//结束推流请求 把上面保存的值放进来
BYE sip:34020000001110000003@192.168.1.8:5060;transport=udp SIP/2.0
Call-ID: 34020000001110000003
CSeq: 6 BYE
From: <sip:34020000002000000001@0.0.0.0:5060>;tag=live
To: "34020000001110000003" <sip:34020000001110000003@192.168.1.8:5060>;tag=29937641
Via: SIP/2.0/UDP 0.0.0.0:5060;branch=branchbye
Contact: <sip:34020000002000000001@0.0.0.0:5060>
Max-Forwards: 70
Content-Length: 0
//摄像头回复200说明断流成功
SIP/2.0 200 OK
Via: SIP/2.0/UDP 0.0.0.0:5060;branch=branchbye;received=192.168.1.201
From: <sip:34020000002000000001@0.0.0.0:5060>;tag=live
To: "34020000001110000003" <sip:34020000001110000003@192.168.1.8:5060>;tag=29937641
Call-ID: 34020000001110000003
CSeq: 6 BYE
User-Agent: Dahua SIP UAS V1.0
Content-Length: 0
由于摄像头是公司的画面,就不放流画面效果图了
上面涉及到的信息模板
//请求推流 Stream-ID: stream:34020000001110000003:34020000001110000002
private static final String str_invite=
"INVITE sip:{deviceId}@{deviceLocalIp}:{deviceLocalPort};transport=udp SIP/2.0"+n+
"Call-ID: {Call-ID}"+n+
"CSeq: 1 INVITE"+n+
"From: <sip:{serverId}@{serverIp}:{serverPort}>;tag=live"+n+
"To: \"{deviceId}\" <sip:{deviceId}@{deviceLocalIp}:{deviceLocalPort}>"+n+
"Via: SIP/2.0/UDP {serverIp}:{serverPort};branch=branchlive"+n+
"Max-Forwards: 70"+n+
"Content-Type: Application/sdp"+n+
"Contact: <sip:{serverId}@{serverIp}:{serverPort}>"+n+
"Supported: 100re1"+n+
/*"Stream-ID: stream:{deviceId}:34020000001110000002"+n+*/
"Subject: {deviceId}:010000{ssrc},{serverId}:0"+n+
"User-Agent: fyl"+n+
"Content-Length: {Content-Length}"+n+
n;
//tcp
private static final String str_invite_sdp=
"v=0"+n+
"o={deviceId} 0 0 IN IP4 {mediaServerIp}"+n+
"s=Play"+n+
"c=IN IP4 {mediaServerIp}"+n+
"t=0 0"+n+
"m=video {mediaServerPort} TCP/RTP/AVP 96 98 97"+n+
"a=recvonly"+n+
"a=rtpmap:96 PS/90000"+n+
"a=rtpmap:98 H264/90000"+n+
"a=rtpmap:97 MPEG4/90000"+n+
"y=010000{ssrc}"+n+
"f="+n+
n;
//udp
private static final String str_invite_sdp_udp=
"v=0"+n+
"o={deviceId} 0 0 IN IP4 {mediaServerIp}"+n+
"s=Play"+n+
"c=IN IP4 {mediaServerIp}"+n+
"t=0 0"+n+
"m=video {mediaServerPort} RTP/AVP 96 98 97"+n+
"a=recvonly"+n+
"a=rtpmap:96 PS/90000"+n+
"a=rtpmap:98 H264/90000"+n+
"a=rtpmap:97 MPEG4/90000"+n+
"y=010000{ssrc}"+n+
"f="+n+
n;
//回复ack
private static final String str_ack=
"ACK sip:{deviceId}@{deviceLocalIp}:{deviceLocalPort} SIP/2.0"+n+
"Call-ID: {Call-ID}"+n+
"CSeq: 1 ACK"+n+
"Via: SIP/2.0/UDP {serverIp}:{serverPort};branch={branchId}"+n+
"From: {From}"+n+
"To: {To}"+n+
"Max-Forwards: 70"+n+
"Content-Length: 0"+n+
n;
private static final String str_bye=
"BYE sip:{deviceId}@{deviceLocalIp}:{deviceLocalPort};transport=udp SIP/2.0"+n+
"Call-ID: {Call-ID}"+n+
"CSeq: 6 BYE"+n+
"From: {From}"+n+
"To: {To}"+n+
"Via: SIP/2.0/UDP {serverIp}:{serverPort};branch=branchbye"+n+
"Contact: <sip:{serverId}@{serverIp}:{serverPort}>"+n+
"Max-Forwards: 70"+n+
"Content-Length: 0"+n+
n;
推流代码实现
//下达推流指令
public void play(String deviceId){
//检查udp通道
if(channel==null){
return;
}
logger.info("play start");
try {
//获取设备id对应的设备信息
DeviceInfo deviceInfo=getDeviceInfo(deviceId);
if(deviceInfo==null){
return;
}
//获取ip和端口(这里如果是公网的话保存的是公网的ip和端口,而公网ip和端口有时限的,必须通过保活心跳来实时更新ip和端口,以保证能通讯到内网,所以心跳信息间隔建议不超过1分钟)
InetSocketAddress inetSocketAddress=new InetSocketAddress(deviceInfo.getIp(),deviceInfo.getPort());
String sendStr=null;
//获取推流信息模板
sendStr=str_invite;
//String callID=Utils.getCallID(serverIp);
String callID=deviceId;
String ssrc=deviceId.substring(deviceId.length()-4);
sendStr=replaceAll("{Call-ID}", callID,sendStr);
sendStr=replaceAll("{deviceId}", deviceId,sendStr);
sendStr=replaceAll("{ssrc}", ssrc,sendStr);
sendStr=replaceAll("{deviceLocalIp}", deviceInfo.getLocalIp(),sendStr);
sendStr=replaceAll("{deviceLocalPort}", deviceInfo.getLocalPort(),sendStr);
sendStr=replaceAll("{serverId}", serverId,sendStr);
sendStr=replaceAll("{serverIp}", serverIp,sendStr);
sendStr=replaceAll("{serverPort}", serverPort,sendStr);
String sendStr2=str_invite_sdp;
sendStr2=replaceAll("{deviceId}", deviceId,sendStr2);
sendStr2=replaceAll("{ssrc}", ssrc,sendStr2);
sendStr2=replaceAll("{mediaServerIp}", mediaServerIp,sendStr2);
sendStr2=replaceAll("{mediaServerPort}", mediaServerPort,sendStr2);
sendStr=sendStr+sendStr2;
//String.valueOf(sendStr2.getBytes().length)
sendStr=replaceAll("{Content-Length}", String.valueOf(sendStr2.getBytes().length),sendStr);
if(sendStr!=null){
send(sendStr,inetSocketAddress);
Data.putScheduled(new SendTipsTask(deviceId + "下达推流指令。。。"));
}
}catch (Exception e){
logger.error("play error",e);
}
}
//拼装字符串方法,性能比较低,可以自行替换,大概的方向是这样
private static String replaceAll(String target,String replacement,String source){
if(target.contains("{")){
target=target.replace("{","\\{");
}
if(target.contains("}")){
target=target.replace("}","\\}");
}
Pattern p = Pattern.compile(target);
Matcher m = p.matcher(source);
return m.replaceAll(replacement);
}
回复ack
//摄像头响应信息 一般出现 在下达指令之后返回的
if("RESPONSE".equals(map.get("messageType"))){
if("200".equals(map.get("stateCode"))){
//cseq是INVITE说明是推流指令回复 需要回复ack进行推流
if(map.get("CSeq").contains("INVITE")){
//回复ack
sendStr=str_ack;
sendStr=replaceAll("{deviceId}",map.get("deviceId"),sendStr);
sendStr=replaceAll("{deviceLocalIp}",map.get("deviceLocalIp"),sendStr);
sendStr=replaceAll("{deviceLocalPort}",map.get("deviceLocalPort"),sendStr);
sendStr=replaceAll("{Call-ID}",map.get("Call-ID"),sendStr);
sendStr=replaceAll("{serverIp}",serverId,sendStr);
sendStr=replaceAll("{serverPort}",serverPort,sendStr);
sendStr=replaceAll("{branchId}", Utils.getBranchId(),sendStr);
sendStr=replaceAll("{From}",map.get("From"),sendStr);
sendStr=replaceAll("{To}",map.get("To"),sendStr);
String deviceId=map.get("deviceId");
//保存推流信息,以便断流使用
deviceInfo.setLiveCallID(map.get("Call-ID"));
deviceInfo.setLiveFromInfo(map.get("From"));
deviceInfo.setLiveToInfo(map.get("To"));
deviceInfo.setSsrc(Utils.getSsrc("010000"+deviceId.substring(deviceId.length()-4)));
deviceInfo.setLive(true);
Data.putScheduled(new SendTipsTask(deviceId + "回复推流ack指令。。。"));
Data.putScheduled(new SendDataTask(commonService));
}
//断流指令收到的回复 推流的所有参数都要保存,有些摄像头会校验参数(海康断流必须和推流参数基本一致,大华则不用)
if(map.get("CSeq").contains("BYE")){
if(deviceInfo.getLiveCallID()!=null&&map.get("Call-ID").equals(deviceInfo.getLiveCallID())){
deviceInfo.setLive(false);
Data.putScheduled(new SendTipsTask(map.get("deviceId") + "推流已关闭。。。"));
Data.putScheduled(new SendDataTask(commonService));
}
}
}
}
断流
//断流
public void bye(String deviceId){
if(channel==null){
return;
}
logger.info("bye start");
try {
DeviceInfo deviceInfo=getDeviceInfo(deviceId);
if(deviceInfo==null){
return;
}
InetSocketAddress inetSocketAddress=new InetSocketAddress(deviceInfo.getIp(),deviceInfo.getPort());
String sendStr=null;
sendStr=str_bye;
String callID=deviceInfo.getLiveCallID();
if(callID==null){
callID=deviceId;
}
sendStr=replaceAll("{Call-ID}", callID,sendStr);
sendStr=replaceAll("{deviceId}", deviceId,sendStr);
sendStr=replaceAll("{deviceLocalIp}", deviceInfo.getLocalIp(),sendStr);
sendStr=replaceAll("{deviceLocalPort}", deviceInfo.getLocalPort(),sendStr);
sendStr=replaceAll("{serverId}", serverId,sendStr);
sendStr=replaceAll("{serverIp}", serverIp,sendStr);
sendStr=replaceAll("{serverPort}", serverPort,sendStr);
sendStr=replaceAll("{From}", deviceInfo.getLiveFromInfo(),sendStr);
sendStr=replaceAll("{To}", deviceInfo.getLiveToInfo(),sendStr);
if(sendStr!=null){
send(sendStr,inetSocketAddress);
}
}catch (Exception e){
logger.error("bye error",e);
}
}