记一次用java从海康ISC下载rtsp历史数据过程

本文介绍了如何通过抓包解析海康威视ISC提供的历史视频RTSP URL,详细展示了使用Wireshark抓取RTSP包的过程,并通过Netty实现按照协议发送RTSP信息。在解析过程中,重点讲解了RTSP帧长度字段的特殊处理,以及如何解码得到H264数据并利用JavaCV转换为MP4。最后,讨论了整个流程的时间和CPU消耗问题。
摘要由CSDN通过智能技术生成

        由于业务需要,需要从海康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。

最后由于很多东西都是第一次接触,自己也不是很懂,希望有大佬看到不对的地方帮忙指正,谢谢!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值