wvp-GB28181-pro 源码分析-点播流程(三)

文章目录

  • 一 、28181-2016标准文档中的点播流程
  • 二 、点播流程源码分析
    • 2.1 页面发起点播请求
    • 2.2 与ZLM协商SSRC信息
    • 2.3 订阅zlmediakit的hook消息及发送invite信令
    • 2.4 处理invite信令响应并应答
    • 2.5 收到ZLM的推流通知
    • 2.6 播放成功
    • 2.7 停止点播流程


2024年6月20日下载的wvp-GB28181-pro,版本号为2.7.2,使用ZLMediakit主干版本。

本节阐述wvp摄像机点播流程。

一 、28181-2016标准文档中的点播流程

在这里插入图片描述
图中的媒体接收者,SIP服务器,媒体服务器和媒体发送者都是逻辑模块,在实际上可以不按照这样的步骤来完成。媒体发送者是摄像机,而媒体接收者、SIP服务器和媒体服务器是wvp和zlmediakit组成,wvp和zlmediakit内部之间通信并没有按照28181的步骤来,wvp只要实现上图的第4、5、7、19、20就可以播放视频和停止播放。至于网页、wvp跟zlmediakit之间是按照自己的私有接口格式来完成的。

附上wvp的点播流程图,可以对比分析下。
在这里插入图片描述

二 、点播流程源码分析

2.1 页面发起点播请求

接口控制类PlayController在如下包路径下:

com.genersoft.iot.vmp.vmanager.gb28181.play

在这里插入图片描述
调用playService.play(MediaServer mediaServerItem, String deviceId, String channelId, String ssrc, ErrorCallback callback),进入PlayServiceImpl类,在如下的包路径下:

com.genersoft.iot.vmp.service.impl

核心方法源码

    @Override
    public SSRCInfo play(MediaServer mediaServerItem, String deviceId, String channelId, String ssrc, ErrorCallback<Object> callback) {
        if (mediaServerItem == null) {
            logger.warn("[点播] 未找到可用的zlm deviceId: {},channelId:{}", deviceId, channelId);
            throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到可用的zlm");
        }
        Device device = redisCatchStorage.getDevice(deviceId);
        if (device.getStreamMode().equalsIgnoreCase("TCP-ACTIVE") && !mediaServerItem.isRtpEnable()) {
            logger.warn("[点播] 单端口收流时不支持TCP主动方式收流 deviceId: {},channelId:{}", deviceId, channelId);
            throw new ControllerException(ErrorCode.ERROR100.getCode(), "单端口收流时不支持TCP主动方式收流");
        }
        DeviceChannel channel = channelService.getOne(deviceId, channelId);
        if (channel == null) {
            logger.warn("[点播] 未找到通道 deviceId: {},channelId:{}", deviceId, channelId);
            throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到通道");
        }
        InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
        if (inviteInfo != null ) {
            if (inviteInfo.getStreamInfo() == null) {
                // 释放生成的ssrc,使用上一次申请的
                ssrcFactory.releaseSsrc(mediaServerItem.getId(), ssrc);
                // 点播发起了但是尚未成功, 仅注册回调等待结果即可
                inviteStreamService.once(InviteSessionType.PLAY, deviceId, channelId, null, callback);
                logger.info("[点播开始] 已经请求中,等待结果, deviceId: {}, channelId: {}", device.getDeviceId(), channelId);
                return inviteInfo.getSsrcInfo();
            }else {
                StreamInfo streamInfo = inviteInfo.getStreamInfo();
                String streamId = streamInfo.getStream();
                if (streamId == null) {
                    callback.run(InviteErrorCode.ERROR_FOR_CATCH_DATA.getCode(), "点播失败, redis缓存streamId等于null", null);
                    inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
                            InviteErrorCode.ERROR_FOR_CATCH_DATA.getCode(),
                            "点播失败, redis缓存streamId等于null",
                            null);
                    return inviteInfo.getSsrcInfo();
                }
                String mediaServerId = streamInfo.getMediaServerId();
                MediaServer mediaInfo = mediaServerService.getOne(mediaServerId);
                Boolean ready = mediaServerService.isStreamReady(mediaInfo, "rtp", streamId);
                if (ready != null && ready) {
                    callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo);
                    inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
                            InviteErrorCode.SUCCESS.getCode(),
                            InviteErrorCode.SUCCESS.getMsg(),
                            streamInfo);
                    logger.info("[点播已存在] 直接返回, deviceId: {}, channelId: {}", device.getDeviceId(), channelId);
                    return inviteInfo.getSsrcInfo();
                }else {
                    // 点播发起了但是尚未成功, 仅注册回调等待结果即可
                    inviteStreamService.once(InviteSessionType.PLAY, deviceId, channelId, null, callback);
                    storager.stopPlay(streamInfo.getDeviceID(), streamInfo.getChannelId());
                    inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
                }
            }
        }
        String streamId = String.format("%s_%s", device.getDeviceId(), channelId);
        /**
        * 与ZLM协商SSRC信息
        */
        SSRCInfo ssrcInfo = mediaServerService.openRTPServer(mediaServerItem, streamId, ssrc, device.isSsrcCheck(),  false, 0, false, !channel.getHasAudio(), false, device.getStreamModeForParam());
        if (ssrcInfo == null) {
            callback.run(InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getMsg(), null);
            inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
                    InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(),
                    InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getMsg(),
                    null);
            return null;
        }
        /**
        * 干两件事 1:订阅ZLM Hook信息;2:向设备发送invite信令,携带ZLM协商好的SSRC信息
        */
        play(mediaServerItem, ssrcInfo, device, channel, callback);
        return ssrcInfo;
    }

2.2 与ZLM协商SSRC信息

关键代码其实就是调用下面这句代码(上文中有注释,往上翻一翻),调用ZLM的API,获取视频流的端口,将来摄像机将视频流推送到此端口。
在这里插入图片描述
具体实现逻辑,核心就是与ZLM协商一个可用的SSRC信息,包括端口号,ssrc值等信息。
在这里插入图片描述

2.3 订阅zlmediakit的hook消息及发送invite信令

就是下面这句核心代码(往上翻,有注释),实际上干了两件核心的事情,一个就是订阅ZLM hook消息,一个就是向设备发送invite信令消息。
在这里插入图片描述

  1. 订阅zlmediakit的hook消息,zlmediakit收到流后,会往wvp发消息。
    在这里插入图片描述

  2. 发送invite请求还在 playStreamCmd方法体中在这里插入图片描述
    这里可以参考本文章的两个流程图,如果参考标准的GB流程,那么就对应步骤4;或者看wvp的流程图,对应步骤2。invite请求的sdp信息带有zlmediakit的IP和刚才获取到的视频流端口。

2.4 处理invite信令响应并应答

处理invite信令,对应GB的流程5,应答,对应GB流程7;如果是wvp的流程图,处理对应流程2,应答对应流程4。别搞混了啊。
处理类是InviteResponseProcessor,对应的包路径如下:

com.genersoft.iot.vmp.gb28181.transmit.event.response.impl
/**
	 * 处理invite响应
	 * 
	 * @param evt 响应消息
	 * @throws ParseException
	 */
	@Override
	public void process(ResponseEvent evt ){
		logger.debug("接收到消息:" + evt.getResponse());
		try {
			SIPResponse response = (SIPResponse)evt.getResponse();
			int statusCode = response.getStatusCode();
			// trying不会回复
			if (statusCode == Response.TRYING) {
			}
			// 成功响应
			// 下发ack
			if (statusCode == Response.OK) {
				ResponseEventExt event = (ResponseEventExt)evt;

				String contentString = new String(response.getRawContent());
				Gb28181Sdp gb28181Sdp = SipUtils.parseSDP(contentString);
				SessionDescription sdp = gb28181Sdp.getBaseSdb();
				SipURI requestUri = SipFactory.getInstance().createAddressFactory().createSipURI(sdp.getOrigin().getUsername(), event.getRemoteIpAddress() + ":" + event.getRemotePort());
				Request reqAck = headerProvider.createAckRequest(response.getLocalAddress().getHostAddress(), requestUri, response);

				logger.info("[回复ack] {}-> {}:{} ", sdp.getOrigin().getUsername(), event.getRemoteIpAddress(), event.getRemotePort());
				sipSender.transmitRequest( response.getLocalAddress().getHostAddress(), reqAck);
			}
		} catch (InvalidArgumentException | ParseException | SipException | SdpParseException e) {
			logger.info("[点播回复ACK],异常:", e );
		}
	}

逻辑不难,应该可以看的明白。

2.5 收到ZLM的推流通知

wvp收到zlmediakit的流消息后,回调到 PlayServiceImpl
在这里插入图片描述
通过callback继续回调到PlayController
在这里插入图片描述
通过以上步骤,最终把播放流媒体的地址应答回到网页端。

2.6 播放成功

在这里插入图片描述

2.7 停止点播流程

明白了开始点播的流程,停止点播再回头看起来就容易多了。对应流程步骤的BYE信令,就不一一列举了。
关键步骤我给截出来,方便大家梳理流程
在这里插入图片描述
请看代码注释

    @Override
    public void stopPlay(Device device, String channelId) {
        InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId);
        if (inviteInfo == null) {
            throw new ControllerException(ErrorCode.ERROR100.getCode(), "点播未找到");
        }
        if (InviteSessionStatus.ok == inviteInfo.getStatus()) {
            try {
                logger.info("[停止点播] {}/{}", device.getDeviceId(), channelId);
                //这句是核心,调用ZLM API来关闭zlmediakit的rtp服务,发bye命令给摄像机
                cmder.streamByeCmd(device, channelId, inviteInfo.getStream(), null, null);
            } catch (InvalidArgumentException | SipException | ParseException | SsrcTransactionNotFoundException e) {
                logger.error("[命令发送失败] 停止点播, 发送BYE: {}", e.getMessage());
                throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage());
            }
        }
        inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId);
        storager.stopPlay(device.getDeviceId(), channelId);
        channelService.stopPlay(device.getDeviceId(), channelId);
        //这句我考虑是为了保险,非空再关闭一次
        if (inviteInfo.getStreamInfo() != null) {
            mediaServerService.closeRTPServer(inviteInfo.getStreamInfo().getMediaServerId(), inviteInfo.getStream());
        }
    }

WVP和ZLM之间的通信没有完全按照GB的协议来走,好多都是直接相互调用API来操作。源码中有注释,自己看吧。

    /**
     * 视频流停止
     */
    @Override
    public void streamByeCmd(Device device, String channelId, String stream, String callId, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException, SsrcTransactionNotFoundException {
        if (device == null) {
            logger.warn("[发送BYE] device为null");
            return;
        }
        List<SsrcTransaction> ssrcTransactionList = streamSession.getSsrcTransactionForAll(device.getDeviceId(), channelId, callId, stream);
        if (ssrcTransactionList == null || ssrcTransactionList.isEmpty()) {
            logger.info("[发送BYE] 未找到事务信息,设备: device: {}, channel: {}", device.getDeviceId(), channelId);
            throw new SsrcTransactionNotFoundException(device.getDeviceId(), channelId, callId, stream);
        }

        for (SsrcTransaction ssrcTransaction : ssrcTransactionList) {
            logger.info("[发送BYE] 设备: device: {}, channel: {}, callId: {}", device.getDeviceId(), channelId, ssrcTransaction.getCallId());
            //释放ssrc
            mediaServerService.releaseSsrc(ssrcTransaction.getMediaServerId(), ssrcTransaction.getSsrc());
        	//调用ZLM API来关闭流,具体可以跟下去看
            mediaServerService.closeRTPServer(ssrcTransaction.getMediaServerId(), ssrcTransaction.getStream());
            streamSession.removeByCallId(ssrcTransaction.getDeviceId(), ssrcTransaction.getChannelId(), ssrcTransaction.getCallId());
            Request byteRequest = headerProvider.createByteRequest(device, channelId, ssrcTransaction.getSipTransactionInfo());
            //给设备发送BYE信令
            sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), byteRequest, null, okEvent);
        }
    }

本节点播流程及停止点播流程梳理完毕!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值