记一次用java从海康ISC下载rtsp历史数据过程
????????由于業(yè)務(wù)需要,需要從海康ISC中獲取歷史視頻,但是查找了API只有一個(gè)獲取歷史RTSP的url的接口,但是這個(gè)rtsp的url用VLC播放不了,就嘗試自己去抓包解析。
一、以下為用wireshark抓的rtsp包,rtsp和http類似,只是文本的交互協(xié)議,具體交互過程這里不闡述。
OPTIONS rtsp://192.168.30.254:554/openUrl/yOqDCw0?beginTime=20210712T090000&endTime=20210712T090100&playBackMode=1&traceId=&spanId= RTSP/1.0 CSeq: 26359 User-Agent: StreamClientRTSP/1.0 200 OK CSeq: 26359 Server: MgcServer Public: OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, HEARTBEAT, GET_PARAMETER, SET_PARAMETER SupportAuth: AES BASE64DESCRIBE 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.1RTSP/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: 746v=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.1RTSP/1.0 200 OK CSeq: 26361 Transport: RTP/AVP/TCP;unicast;interleaved=0-1 Private: p=554-555 Session: 10008660;timeout=8 Server: MgcServerPLAY 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.1RTSP/1.0 200 OK CSeq: 26362 Session: 10008660 Server: MgcServerHEARTBEAT 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: StreamClientRTSP/1.0 200 OK CSeq: 26363 Session: 10008660 Server: MgcServer Function: Heartbeat使用netty按照抓的包的格式給ISC發(fā)rtsp信息就可以了。部分代碼如下:
@Overridepublic 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();//創(chuàng)建目標(biāo)大小的數(shù)組byte[] barray = new byte[buf.readableBytes()];//把數(shù)據(jù)從bytebuf轉(zhuǎn)移到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)){//下載結(jié)束stopHeart();}}}}需要注意的點(diǎn)是:當(dāng)isc回復(fù)了SETUP消息后,需要我們從新開一個(gè)端口去連isc554端口,然后立即把session發(fā)過去,視頻數(shù)據(jù)就會(huì)發(fā)到這個(gè)新的端口,回復(fù)的數(shù)據(jù)是基于tcp的rtp數(shù)據(jù)通過rtsp發(fā)送過來的,包了好多層,通過抓包看到,最開始是rtsp的interleaved frame,相當(dāng)于rtsp的數(shù)據(jù)頭,包含4個(gè)字節(jié),第一個(gè)自己固定,0x24(是個(gè)$符號(hào)),然后是channel,和之前回復(fù)的SETUP消息的Transport內(nèi)容相關(guān),不過這里都是0x00,然后兩個(gè)字節(jié)是后面rtp數(shù)據(jù)的長度。
?這里有一點(diǎn)非常重要:不知道是不是海康自己做了修改還是什么(有懂的同學(xué)幫忙解釋下,這個(gè)問題我找了好久才解決的),這兩個(gè)長度字節(jié)是經(jīng)過修改的,后面的實(shí)際長度是兩個(gè)字節(jié)都左移兩位,然后兩個(gè)字節(jié)交換位置。如下:
private static int getLength(byte[] data){int high = (data[3] & 0xff) << 10;int low = (data[2] & 0xff) << 2;return high+low;}通過長度字段可以取到后面rtp包的數(shù)據(jù)了,rtp包的第3、4字節(jié)為rtp包的序號(hào)
RTP頭信息一共有12字節(jié)(包含的信息這里就不展開講了),去掉這12字節(jié)后面的為ps包數(shù)據(jù),ps包內(nèi)容參考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數(shù)據(jù),10個(gè)字節(jié)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字節(jié),固定2字節(jié),custom長度1字節(jié)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數(shù)據(jù)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的數(shù)據(jù)了,保存了可以用VLC播放了。
最后業(yè)務(wù)需要能直接通過url播放,所以通過javacv將h264轉(zhuǎn)為了mp4再上傳至minio,然后直接可通過網(wǎng)頁播放了
javacv轉(zhuǎn)換如下:
public class JavaCvH264ToMp4Converter {/*** 將H264文件轉(zhuǎn)為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);// 不進(jìn)行轉(zhuǎn)碼時(shí),編碼格式默認(rèn)為HFYU,使用VLC播放器時(shí)無法播放下載的視頻 --可能和海康的攝像頭有關(guān)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();}}} }總結(jié):一部部按協(xié)議解析就行,只是在rtsp interleaved frame的長度字段上花費(fèi)了一些時(shí)間,因?yàn)檫@長度字段是經(jīng)過變換的,然后就是這個(gè)過程非常耗時(shí),從下載到解析轉(zhuǎn)換都耗時(shí)耗cpu。
最后由于很多東西都是第一次接觸,自己也不是很懂,希望有大佬看到不對(duì)的地方幫忙指正,謝謝!
總結(jié)
以上是生活随笔為你收集整理的记一次用java从海康ISC下载rtsp历史数据过程的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: xshell7 无法启动
- 下一篇: 8月8日云栖精选夜读 | 阿里资深技术专
