文章目录
- 一 、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信令消息。
-
订阅zlmediakit的hook消息,zlmediakit收到流后,会往wvp发消息。
-
发送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);
}
}
本节点播流程及停止点播流程梳理完毕!