由于业务需要,需要从海康ISC中获取历史视频,但是查找了API只有一个获取历史RTSP的url的接口,但是这个rtsp的url用VLC播放不了,就尝试自己去抓包解析。
一、以下为用wireshark抓的rtsp包,rtsp和http类似,只是文本的交互协议,具体交互过程这里不阐述。
OPTIONS rtsp://192.168.30.254:554/openUrl/yOqDCw0?beginTime=20210712T090000&endTime=20210712T090100&playBackMode=1&traceId=&spanId= RTSP/1.0
CSeq: 26359
User-Agent: StreamClient
RTSP/1.0 200 OK
CSeq: 26359
Server: MgcServer
Public: OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, HEARTBEAT, GET_PARAMETER, SET_PARAMETER
SupportAuth: AES BASE64
DESCRIBE rtsp://192.168.30.254:554/openUrl/yOqDCw0?beginTime=20210712T090000&endTime=20210712T090100&playBackMode=1&traceId=&spanId=&forceServerIp=192.168.30.254&forceServerPort=554 RTSP/1.0
CSeq: 26360
Accept: application/sdp
StandardStream: 0
Device-PushData: 0
Download: 0
User-Agent: StreamClient
Upgrade: StreamSystem4.1
RTSP/1.0 200 OK
CSeq: 26360
Server: MgcServer
Content-Type: application/sdp
Content-Base: rtsp://192.168.30.254:554/openUrl/yOqDCw0?beginTime=20210712T090000&endTime=20210712T090100&playBackMode=1&traceId=&spanId=&forceServerIp=192.168.30.254&forceServerPort=554
Content-Length: 746
v=0
o=- 10008660 10008660 IN IP4 0.0.0.0
s=hikvision
c=IN IP4 0.0.0.0
a=range:npt=now-
a=control:rtsp://192.168.30.254:554/openUrl/yOqDCw0?beginTime=20210712T090000&endTime=20210712T090100&playBackMode=1&traceId=&spanId=&forceServerIp=192.168.30.254&forceServerPort=554
t=0 0
m=video 0 RTP/AVP 96
c=IN IP4 0.0.0.0
a=rtpmap:96 MP4V-ES/90000
a=fmtp:96 profile-level-id=8;config=000001B0F5000001B50900000100000001200886C400670C58112051
a=control:rtsp://192.168.30.254:554/openUrl/yOqDCw0?beginTime=20210712T090000&endTime=20210712T090100&playBackMode=1&traceId=&spanId=&forceServerIp=192.168.30.254&forceServerPort=554/trackID=0
a=range:npt=now-
a=Media_header:MEDIAINFO=base64,SU1LSAIBAAACAAABEXEBEEAfAAAA+gAAAAAAAAAAAAAAAAAAAAAAAA==
SETUP rtsp://192.168.30.254:554/openUrl/yOqDCw0?beginTime=20210712T090000&endTime=20210712T090100&playBackMode=1&traceId=&spanId=&forceServerIp=192.168.30.254&forceServerPort=554/trackID=0 RTSP/1.0
CSeq: 26361
Transport: RTP/AVP/TCP;unicast;interleaved=0-1;ssrc=0
Private: p=10026-10027
User-Agent: StreamClient
Upgrade: StreamSystem4.1
RTSP/1.0 200 OK
CSeq: 26361
Transport: RTP/AVP/TCP;unicast;interleaved=0-1
Private: p=554-555
Session: 10008660;timeout=8
Server: MgcServer
PLAY rtsp://192.168.30.254:554/openUrl/yOqDCw0?beginTime=20210712T090000&endTime=20210712T090100&playBackMode=1&traceId=&spanId=&forceServerIp=192.168.30.254&forceServerPort=554 RTSP/1.0
CSeq: 26362
Session: 10008660
User-Agent: StreamClient
Upgrade: StreamSystem4.1
RTSP/1.0 200 OK
CSeq: 26362
Session: 10008660
Server: MgcServer
HEARTBEAT rtsp://192.168.30.254:554/openUrl/yOqDCw0?beginTime=20210712T090000&endTime=20210712T090100&playBackMode=1&traceId=&spanId=&forceServerIp=192.168.30.254&forceServerPort=554 RTSP/1.0
CSeq: 26363
Session: 10008660
User-Agent: StreamClient
RTSP/1.0 200 OK
CSeq: 26363
Session: 10008660
Server: MgcServer
Function: Heartbeat
使用netty按照抓的包的格式给ISC发rtsp信息就可以了。部分代码如下:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception{
if(msg instanceof DefaultHttpResponse){
DefaultHttpResponse response = (DefaultHttpResponse)msg;
if (response.status().code() == 200) {
if (response.headers().contains("CSeq")){
seq = Long.parseLong(response.headers().get("Cseq"));
if (RtspMsg.OPTIONS.equals(seqMap.get(seq))){
seq ++;
String data = RtspMsg.generateDescribe(getUrl(), getBeginTime(), getEndTime(), seq, getIp(), getPort());
send(ctx.channel(),data);
seqMap.put(seq,RtspMsg.DESCRIBE);
}else if (RtspMsg.DESCRIBE.equals(seqMap.get(seq))){
seq ++;
String data = RtspMsg.generateSetup(getUrl(), getBeginTime(), getEndTime(), seq, getIp(), getPort());
send(ctx.channel(),data);
seqMap.put(seq,RtspMsg.SETUP);
}else if (RtspMsg.SETUP.equals(seqMap.get(seq))){
seq ++;
String sessionHeader = response.headers().get("Session");
session = sessionHeader.substring(0,sessionHeader.indexOf(";"));
fos = new FileOutputStream((seqMap.get(RtspMsg.DOWNLOAD_FILE_NAME)+".rtsp"));
bos = new BufferedOutputStream(fos);
client = new RtpClient("192.168.30.254",554,session,bos);
client.start();
int count = 0;
while (!client.isReady()){
count++;
if (count > 3000){
throw new Exception("rtp client connect timeout");
}
Thread.sleep(1);
}
String data = RtspMsg.generatePlay(getUrl(), getBeginTime(), getEndTime(), seq, getIp(), getPort(),session);
send(ctx.channel(),data);
seqMap.put(seq,RtspMsg.PLAY);
}else if (RtspMsg.PLAY.equals(seqMap.get(seq))){
startHeart();
String data = RtspMsg.generateHeartBeat(getUrl(), getBeginTime(), getEndTime(), seq, getIp(), getPort(),session);
new Thread(new HeartThread(seq, seqMap, ctx, data)).start();
}
}
}
}else if(msg instanceof HttpContent){
HttpContent content = (HttpContent) msg;
if (content.content().isReadable()){
ByteBuf buf = (ByteBuf)content.content();
//创建目标大小的数组
byte[] barray = new byte[buf.readableBytes()];
//把数据从bytebuf转移到byte[]
buf.getBytes(0,barray);
buf.release();
}
}else if (msg instanceof DefaultHttpRequest){
DefaultHttpRequest request = (DefaultHttpRequest) msg;
if (request.method().name().contains("ANNOUNCE")){
String announceSession = request.headers().get("Session");
if (announceSession != null && announceSession.equals(session)){
//下载结束
stopHeart();
}
}
}
}
需要注意的点是:当isc回复了SETUP消息后,需要我们从新开一个端口去连isc554端口,然后立即把session发过去,视频数据就会发到这个新的端口,回复的数据是基于tcp的rtp数据通过rtsp发送过来的,包了好多层,通过抓包看到,最开始是rtsp的interleaved frame,相当于rtsp的数据头,包含4个字节,第一个自己固定,0x24(是个$符号),然后是channel,和之前回复的SETUP消息的Transport内容相关,不过这里都是0x00,然后两个字节是后面rtp数据的长度。
这里有一点非常重要:不知道是不是海康自己做了修改还是什么(有懂的同学帮忙解释下,这个问题我找了好久才解决的),这两个长度字节是经过修改的,后面的实际长度是两个字节都左移两位,然后两个字节交换位置。如下:
private static int getLength(byte[] data){
int high = (data[3] & 0xff) << 10;
int low = (data[2] & 0xff) << 2;
return high+low;
}
通过长度字段可以取到后面rtp包的数据了,rtp包的第3、4字节为rtp包的序号
RTP头信息一共有12字节(包含的信息这里就不展开讲了),去掉这12字节后面的为ps包数据,ps包内容参考https://blog.yasking.org/a/hikvision-rtp-ps-stream-parser.html
我的解码部分如下:
private void decode(BufferedInputStream bis, BufferedOutputStream bos) throws Exception{
byte[] head = new byte[4];
int headLen = 0;
while ((headLen = bis.read(head)) > 3){
if (isMatchPack(head)){
//读取pack数据,10个字节
byte[] pack = new byte[10];
int packLen = bis.read(pack);
if (packLen < 10){
break;
}
int stuffingLen = BitUtils.byteToInt(pack[9]) & 0x07;
bis.skip(stuffingLen);
}else if (isMatchPes(head)){
//读取总长度2字节,固定2字节,custom长度1字节
byte[] lenByte = new byte[5];
bis.read(lenByte);
int pesLen = BitUtils.byte2ToInt(lenByte[0],lenByte[1]);
int customLen = BitUtils.byteToInt(lenByte[4]);
bis.skip(customLen);
//后面的就是h264数据
int dataLen = pesLen - 3 - customLen;
byte[] data = new byte[dataLen];
int readLen = 0;
if (( readLen = bis.read(data)) == dataLen){
bos.write(data);
bos.flush();
}
}else if (isMatchMap(head) || isMatchDb(head)){
byte[] lenByte = new byte[2];
bis.read(lenByte);
int len = BitUtils.byte2ToInt(lenByte[0],lenByte[1]);
bis.skip(len);
}else if (isMatchEndCode(head)){
return;
}else {
bis.skip(1);
}
}
}
这里接出来就是h264的数据了,保存了可以用VLC播放了。
最后业务需要能直接通过url播放,所以通过javacv将h264转为了mp4再上传至minio,然后直接可通过网页播放了
javacv转换如下:
public class JavaCvH264ToMp4Converter {
/**
* 将H264文件转为mp4
*
* @param inputFile 输入文件
* @param outputFile 输出文件
* @throws Exception
*/
public void convert(String inputFile, String outputFile) throws Exception {
// 获取视频源
avutil.av_log_set_level(avutil.AV_LOG_ERROR);
FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(inputFile);
FFmpegLogCallback.set();
grabber.start();
// 流媒体输出地址,分辨率(长,高),是否录制音频(0:不录制/1:录制)
FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(outputFile, 1920, 1080, 0);
// 不进行转码时,编码格式默认为HFYU,使用VLC播放器时无法播放下载的视频 --可能和海康的摄像头有关
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);// avcodec.AV_CODEC_ID_H264,编码
recorder.setFormat("mp4");
recorder.setFrameRate(12.5);
recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
recorder.setVideoBitrate(800000);
// 开始取视频源
recordByFrame(grabber, recorder);
}
private void recordByFrame(FFmpegFrameGrabber grabber, FFmpegFrameRecorder recorder)
throws Exception {
try {
// 建议在线程中使用该方法
recorder.start();
Frame frame = null;
while ((frame = grabber.grabFrame()) != null){
recorder.record(frame);
}
recorder.stop();
recorder.release();
grabber.stop();
} finally {
if (grabber != null) {
grabber.stop();
}
}
}
}
总结:一部部按协议解析就行,只是在rtsp interleaved frame的长度字段上花费了一些时间,因为这长度字段是经过变换的,然后就是这个过程非常耗时,从下载到解析转换都耗时耗cpu。
最后由于很多东西都是第一次接触,自己也不是很懂,希望有大佬看到不对的地方帮忙指正,谢谢!