| | |
| | | System.exit(1); |
| | | } |
| | | |
| | | if (mediaIp.equals("localhost") || mediaIp.equals("127.0.0.1")) { |
| | | if (mediaIp.equals("localhost") || (mediaIp.equals("127.0.0.1") && mediaWanIp == null)) { |
| | | logger.warn("mediaIp.ip使用 {} ,将无法收到网络内其他设备的推流!!!", mediaIp ); |
| | | } |
| | | |
New file |
| | |
| | | package com.genersoft.iot.vmp.conf; |
| | | |
| | | import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform; |
| | | import com.genersoft.iot.vmp.gb28181.bean.ParentPlatformCatch; |
| | | import com.genersoft.iot.vmp.gb28181.event.EventPublisher; |
| | | import com.genersoft.iot.vmp.storager.IRedisCatchStorage; |
| | | import com.genersoft.iot.vmp.storager.IVideoManagerStorager; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.boot.CommandLineRunner; |
| | | import org.springframework.core.annotation.Order; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 系统启动时控制设备离线 |
| | | */ |
| | | @Component |
| | | @Order(value=4) |
| | | public class SipDeviceRunner implements CommandLineRunner { |
| | | |
| | | @Autowired |
| | | private IVideoManagerStorager storager; |
| | | |
| | | @Autowired |
| | | private IRedisCatchStorage redisCatchStorage; |
| | | |
| | | @Override |
| | | public void run(String... args) throws Exception { |
| | | // 设置所有设备离线 |
| | | storager.outlineForAll(); |
| | | } |
| | | } |
| | |
| | | import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform; |
| | | import com.genersoft.iot.vmp.gb28181.bean.ParentPlatformCatch; |
| | | import com.genersoft.iot.vmp.gb28181.event.EventPublisher; |
| | | import com.genersoft.iot.vmp.media.zlm.ZLMRTPServerFactory; |
| | | import com.genersoft.iot.vmp.storager.IRedisCatchStorage; |
| | | import com.genersoft.iot.vmp.storager.IVideoManagerStorager; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | |
| | | @Autowired |
| | | private EventPublisher publisher; |
| | | |
| | | @Autowired |
| | | private ZLMRTPServerFactory zlmrtpServerFactory; |
| | | |
| | | |
| | | @Override |
| | | public void run(String... args) throws Exception { |
| | | // 设置所有平台离线 |
| | |
| | | // 清理所有平台注册缓存 |
| | | redisCatchStorage.cleanPlatformRegisterInfos(); |
| | | |
| | | // 停止所有推流 |
| | | // zlmrtpServerFactory.closeAllSendRtpStream(); |
| | | |
| | | List<ParentPlatform> parentPlatforms = storager.queryEnableParentPlatformList(true); |
| | | |
| | | for (ParentPlatform parentPlatform : parentPlatforms) { |
| | |
| | | http.headers().contentTypeOptions().disable(); |
| | | http.authorizeRequests() |
| | | // 放行接口 |
| | | .antMatchers("/api/user/login","/index/hook/**").permitAll() |
| | | .antMatchers("/#/**", "/api/user/login","/index/hook/**").permitAll() |
| | | // 除上面外的所有请求全部需要鉴权认证 |
| | | .anyRequest().authenticated() |
| | | // 异常处理(权限拒绝、登录失效等) |
| | |
| | | // 获取失效的key |
| | | String expiredKey = message.toString(); |
| | | logger.info(expiredKey); |
| | | if(!expiredKey.startsWith(VideoManagerConstants.PLATFORM_PREFIX)){ |
| | | logger.info("收到redis过期监听,但开头不是"+VideoManagerConstants.PLATFORM_PREFIX+",忽略"); |
| | | if(!expiredKey.startsWith(VideoManagerConstants.PLATFORM_KEEPLIVEKEY_PREFIX)){ |
| | | logger.debug("收到redis过期监听,但开头不是"+VideoManagerConstants.PLATFORM_KEEPLIVEKEY_PREFIX+",忽略"); |
| | | return; |
| | | } |
| | | // 平台心跳到期,需要重发, 判断是否已经多次未收到心跳回复, 多次未收到,则重新发起注册, 注册尝试多次未得到回复,则认为平台离线 |
| | |
| | | |
| | | publisher.platformKeepaliveExpireEventPublish(platformGBId); |
| | | }else if (expiredKey.startsWith(VideoManagerConstants.PLATFORM_REGISTER_PREFIX)) { |
| | | logger.info("11111111111111"); |
| | | String platformGBId = expiredKey.substring(VideoManagerConstants.PLATFORM_REGISTER_PREFIX.length(),expiredKey.length()); |
| | | |
| | | publisher.platformNotRegisterEventPublish(platformGBId); |
| | |
| | | // 获取失效的key
|
| | | String expiredKey = message.toString();
|
| | | if(!expiredKey.startsWith(VideoManagerConstants.KEEPLIVEKEY_PREFIX)){
|
| | | logger.info("收到redis过期监听,但开头不是"+VideoManagerConstants.KEEPLIVEKEY_PREFIX+",忽略");
|
| | | logger.debug("收到redis过期监听,但开头不是"+VideoManagerConstants.KEEPLIVEKEY_PREFIX+",忽略");
|
| | | return;
|
| | | }
|
| | |
|
| | |
| | | if (parentPlatformCatch.getKeepAliveReply() >= 3) { |
| | | // 有3次未收到心跳回复, 设置平台状态为离线, 开始重新注册 |
| | | logger.warn("有3次未收到心跳回复,标记设置平台状态为离线, 并重新注册 平台国标ID:" + event.getPlatformGbID()); |
| | | storager.updateParentPlatformStatus(event.getPlatformGbID(), false); |
| | | publisher.platformNotRegisterEventPublish(event.getPlatformGbID()); |
| | | parentPlatformCatch.setKeepAliveReply(0); |
| | | }else { |
| | |
| | | package com.genersoft.iot.vmp.gb28181.event.platformNotRegister; |
| | | |
| | | import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform; |
| | | import com.genersoft.iot.vmp.gb28181.bean.SendRtpItem; |
| | | import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderFroPlatform; |
| | | import com.genersoft.iot.vmp.media.zlm.ZLMRTPServerFactory; |
| | | import com.genersoft.iot.vmp.storager.IRedisCatchStorage; |
| | | import com.genersoft.iot.vmp.storager.IVideoManagerStorager; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.context.ApplicationListener; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | import java.util.HashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * @Description: 平台未注册事件,来源有二: |
| | |
| | | |
| | | @Autowired |
| | | private IVideoManagerStorager storager; |
| | | @Autowired |
| | | private IRedisCatchStorage redisCatchStorage; |
| | | |
| | | @Autowired |
| | | private SIPCommanderFroPlatform sipCommanderFroPlatform; |
| | | |
| | | @Autowired |
| | | private ZLMRTPServerFactory zlmrtpServerFactory; |
| | | |
| | | // @Autowired |
| | | // private RedisUtil redis; |
| | |
| | | @Override |
| | | public void onApplicationEvent(PlatformNotRegisterEvent event) { |
| | | |
| | | logger.debug("平台未注册事件触发,平台国标ID:" + event.getPlatformGbID()); |
| | | logger.info("平台未注册事件触发,平台国标ID:" + event.getPlatformGbID()); |
| | | |
| | | ParentPlatform parentPlatform = storager.queryParentPlatByServerGBId(event.getPlatformGbID()); |
| | | if (parentPlatform == null) { |
| | | logger.debug("平台未注册事件触发,但平台已经删除!!! 平台国标ID:" + event.getPlatformGbID()); |
| | | logger.info("平台未注册事件触发,但平台已经删除!!! 平台国标ID:" + event.getPlatformGbID()); |
| | | return; |
| | | } |
| | | // 查询是否有推流, 如果有则都停止 |
| | | List<SendRtpItem> sendRtpItems = redisCatchStorage.querySendRTPServer(event.getPlatformGbID()); |
| | | logger.info("停止[ {} ]的所有推流size", sendRtpItems.size()); |
| | | if (sendRtpItems != null && sendRtpItems.size() > 0) { |
| | | logger.info("停止[ {} ]的所有推流", event.getPlatformGbID()); |
| | | StringBuilder app = new StringBuilder(); |
| | | StringBuilder stream = new StringBuilder(); |
| | | for (int i = 0; i < sendRtpItems.size(); i++) { |
| | | if (app.length() != 0) { |
| | | app.append(","); |
| | | } |
| | | app.append(sendRtpItems.get(i).getApp()); |
| | | if (stream.length() != 0) { |
| | | stream.append(","); |
| | | } |
| | | stream.append(sendRtpItems.get(i).getStreamId()); |
| | | redisCatchStorage.deleteSendRTPServer(event.getPlatformGbID(), sendRtpItems.get(i).getChannelId()); |
| | | } |
| | | Map<String, Object> param = new HashMap<>(); |
| | | param.put("vhost","__defaultVhost__"); |
| | | param.put("app", app.toString()); |
| | | param.put("stream", stream.toString()); |
| | | zlmrtpServerFactory.stopSendRtpStream(param); |
| | | |
| | | } |
| | | sipCommanderFroPlatform.register(parentPlatform); |
| | | } |
| | | } |
| | |
| | | if (totalRecordList.size() < this.recordInfo.getSumNum()) { |
| | | logger.info("已获取" + totalRecordList.size() + "项录像数据,共" + this.recordInfo.getSumNum() + "项"); |
| | | } else { |
| | | logger.info("录像数据已全部获取,共" + this.recordInfo.getSumNum() + "项"); |
| | | logger.info("录像数据已全部获取,共 {} 项", this.recordInfo.getSumNum()); |
| | | this.recordInfo.setRecordList(totalRecordList); |
| | | for (int i = 0; i < cacheKeys.size(); i++) { |
| | | redis.del(cacheKeys.get(i).toString()); |
| | |
| | | param.put("vhost","__defaultVhost__");
|
| | | param.put("app",sendRtpItem.getApp());
|
| | | param.put("stream",streamId);
|
| | | param.put("ssrc",sendRtpItem.getSsrc());
|
| | | logger.info("停止向上级推流:" + streamId);
|
| | | zlmrtpServerFactory.stopSendRtpStream(param);
|
| | | redisCatchStorage.deleteSendRTPServer(platformGbId, channelId);
|
| | |
| | | GbStream gbStream = storager.queryStreamInParentPlatform(requesterId, channelId);
|
| | | // 不是通道可能是直播流
|
| | | if (channel != null || gbStream != null ) {
|
| | | if (channel.getStatus() == 0) {
|
| | | logger.info("通道离线,返回400");
|
| | | responseAck(evt, Response.BAD_REQUEST, "channel [" + channel.getChannelId() + "] offline");
|
| | | return;
|
| | | }
|
| | | responseAck(evt, Response.CALL_IS_BEING_FORWARDED); // 通道存在,发181,呼叫转接中
|
| | | }else {
|
| | | logger.info("通道不存在,返回404");
|
| | |
| | | getServerTransaction(evt).sendResponse(response);
|
| | | }
|
| | |
|
| | | private void responseAck(RequestEvent evt, int statusCode, String msg) throws SipException, InvalidArgumentException, ParseException {
|
| | | Response response = getMessageFactory().createResponse(statusCode, evt.getRequest());
|
| | | response.setReasonPhrase(msg);
|
| | | getServerTransaction(evt).sendResponse(response);
|
| | | }
|
| | |
|
| | | /**
|
| | | * 回复带sdp的200
|
| | | * @param evt
|
| | |
| | | try {
|
| | | Element rootElement = getRootElement(evt);
|
| | | String deviceId = XmlUtil.getText(rootElement, "DeviceID");
|
| | | // 检查设备是否存在, 不存在则不回复
|
| | | if (storager.exists(deviceId)) {
|
| | | Device device = storager.queryVideoDevice(deviceId);
|
| | | // 检查设备是否存在并在线, 不存在则不回复
|
| | | if (device != null && device.getOnline() == 1) {
|
| | | // 回复200 OK
|
| | | responseAck(evt);
|
| | | if (offLineDetector.isOnline(deviceId)) {
|
| | | publisher.onlineEventPublish(deviceId, VideoManagerConstants.EVENT_ONLINE_KEEPLIVE);
|
| | | } else {
|
| | | }
|
| | | }else {
|
| | | logger.warn("收到[ "+deviceId+" ]心跳信息, 但是设备" + (device == null? "不存在":"离线") + ", 心跳信息不予以回复");
|
| | | }
|
| | | } catch (ParseException | SipException | InvalidArgumentException | DocumentException e) {
|
| | | e.printStackTrace();
|
| | |
| | | // 注册成功
|
| | | // 保存到redis
|
| | | // 下发catelog查询目录
|
| | | if (registerFlag == 1 && device != null) {
|
| | | if (registerFlag == 1 ) {
|
| | | logger.info("注册成功! deviceId:" + device.getDeviceId());
|
| | | // boolean exists = storager.exists(device.getDeviceId());
|
| | | device.setRegisterTimeMillis(System.currentTimeMillis());
|
| | |
| | | // 注册/注销成功 |
| | | logger.info(String.format("%s %s成功", platformGBId, action)); |
| | | redisCatchStorage.delPlatformRegisterInfo(callId); |
| | | parentPlatform.setStatus(true); |
| | | parentPlatform.setStatus("注册".equals(action)); |
| | | // 取回Expires设置,避免注销过程中被置为0 |
| | | ParentPlatform parentPlatformTmp = storager.queryParentPlatByServerGBId(platformGBId); |
| | | String expires = parentPlatformTmp.getExpires(); |
| | | parentPlatform.setExpires(expires); |
| | | storager.updateParentPlatform(parentPlatform); |
| | | parentPlatform.setId(parentPlatformTmp.getId()); |
| | | storager.updateParentPlatformStatus(platformGBId, "注册".equals(action)); |
| | | |
| | | redisCatchStorage.updatePlatformRegister(parentPlatform); |
| | | |
| | |
| | | package com.genersoft.iot.vmp.media.zlm; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.alibaba.fastjson.JSONArray; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import com.genersoft.iot.vmp.conf.MediaServerConfig; |
| | | import com.genersoft.iot.vmp.media.zlm.dto.StreamProxyItem; |
| | | import com.genersoft.iot.vmp.media.zlm.dto.StreamPushItem; |
| | | import com.genersoft.iot.vmp.gb28181.bean.GbStream; |
| | |
| | | storager.mediaOutline(app, streamId); |
| | | } |
| | | } |
| | | |
| | | public void clearAllSessions() { |
| | | logger.info("清空所有国标相关的session"); |
| | | JSONObject allSessionJSON = zlmresTfulUtils.getAllSession(); |
| | | MediaServerConfig mediaInfo = redisCatchStorage.getMediaInfo(); |
| | | HashSet<String> allLocalPorts = new HashSet(); |
| | | if (allSessionJSON.getInteger("code") == 0) { |
| | | JSONArray data = allSessionJSON.getJSONArray("data"); |
| | | if (data.size() > 0) { |
| | | for (int i = 0; i < data.size(); i++) { |
| | | JSONObject sessionJOSN = data.getJSONObject(i); |
| | | Integer local_port = sessionJOSN.getInteger("local_port"); |
| | | if (!local_port.equals(Integer.valueOf(mediaInfo.getHttpPort())) && |
| | | !local_port.equals(Integer.valueOf(mediaInfo.getHttpSSLport())) && |
| | | !local_port.equals(Integer.valueOf(mediaInfo.getRtmpPort())) && |
| | | !local_port.equals(Integer.valueOf(mediaInfo.getRtspPort())) && |
| | | !local_port.equals(Integer.valueOf(mediaInfo.getRtspSSlport())) && |
| | | !local_port.equals(Integer.valueOf(mediaInfo.getHookOnFlowReport()))){ |
| | | allLocalPorts.add(sessionJOSN.getInteger("local_port") + ""); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | if (allLocalPorts.size() > 0) { |
| | | List<String> result = new ArrayList<>(allLocalPorts); |
| | | String localPortSStr = String.join(",", result); |
| | | zlmresTfulUtils.kickSessions(localPortSStr); |
| | | } |
| | | } |
| | | } |
| | |
| | | param.put("force", 1); |
| | | return sendPost("close_streams",param, null); |
| | | } |
| | | |
| | | public JSONObject getAllSession() { |
| | | return sendPost("getAllSession",null, null); |
| | | } |
| | | |
| | | public void kickSessions(String localPortSStr) { |
| | | Map<String, Object> param = new HashMap<>(); |
| | | param.put("local_port", localPortSStr); |
| | | sendPost("kick_sessions",param, null); |
| | | } |
| | | } |
| | |
| | | |
| | | private Logger logger = LoggerFactory.getLogger("ZLMRTPServerFactory"); |
| | | |
| | | @Value("${media.rtp.udpPortRange}") |
| | | private String udpPortRange; |
| | | @Value("${media.rtp.portRange}") |
| | | private String portRange; |
| | | |
| | | @Autowired |
| | | private ZLMRESTfulUtils zlmresTfulUtils; |
| | | |
| | | private int[] udpPortRangeArray = new int[2]; |
| | | private int[] portRangeArray = new int[2]; |
| | | |
| | | private int currentPort = 0; |
| | | |
| | |
| | | |
| | | Map<String, Object> param = new HashMap<>(); |
| | | int result = -1; |
| | | int newPort = getPortFromUdpPortRange(); |
| | | int newPort = getPortFromportRange(); |
| | | param.put("port", newPort); |
| | | param.put("enable_tcp", 1); |
| | | param.put("stream_id", streamId); |
| | |
| | | return result; |
| | | } |
| | | |
| | | private int getPortFromUdpPortRange() { |
| | | private int getPortFromportRange() { |
| | | if (currentPort == 0) { |
| | | String[] udpPortRangeStrArray = udpPortRange.split(","); |
| | | udpPortRangeArray[0] = Integer.parseInt(udpPortRangeStrArray[0]); |
| | | udpPortRangeArray[1] = Integer.parseInt(udpPortRangeStrArray[1]); |
| | | String[] portRangeStrArray = portRange.split(","); |
| | | portRangeArray[0] = Integer.parseInt(portRangeStrArray[0]); |
| | | portRangeArray[1] = Integer.parseInt(portRangeStrArray[1]); |
| | | } |
| | | |
| | | if (currentPort == 0 || currentPort++ > udpPortRangeArray[1]) { |
| | | currentPort = udpPortRangeArray[0]; |
| | | return udpPortRangeArray[0]; |
| | | if (currentPort == 0 || currentPort++ > portRangeArray[1]) { |
| | | currentPort = portRangeArray[0]; |
| | | return portRangeArray[0]; |
| | | } else { |
| | | if (currentPort % 2 == 1) { |
| | | currentPort++; |
| | |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | public void closeAllSendRtpStream() { |
| | | |
| | | } |
| | | } |
| | |
| | | mediaServerConfig.setLocalIP(mediaIp); |
| | | mediaServerConfig.setWanIp(StringUtils.isEmpty(mediaWanIp)? mediaIp: mediaWanIp); |
| | | redisCatchStorage.updateMediaInfo(mediaServerConfig); |
| | | |
| | | // 清空所有session |
| | | // zlmMediaListManager.clearAllSessions(); |
| | | |
| | | // 更新流列表 |
| | | zlmMediaListManager.updateMediaList(); |
| | | // 恢复流代理 |
| | |
| | | import com.genersoft.iot.vmp.gb28181.bean.ParentPlatformCatch; |
| | | import com.genersoft.iot.vmp.gb28181.bean.SendRtpItem; |
| | | |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | public interface IRedisCatchStorage { |
| | |
| | | */ |
| | | SendRtpItem querySendRTPServer(String platformGbId, String channelId); |
| | | |
| | | List<SendRtpItem> querySendRTPServer(String platformGbId); |
| | | |
| | | /** |
| | | * 删除RTP推送信息缓存 |
| | | * @param platformGbId |
| | |
| | | */
|
| | | public boolean outline(String deviceId);
|
| | |
|
| | | /**
|
| | | * 更新所有设备离线
|
| | | *
|
| | | * @return true:更新成功 false:更新失败
|
| | | */
|
| | | public boolean outlineForAll();
|
| | |
|
| | |
|
| | | /**
|
| | | * 查询子设备
|
| | |
| | | * @param streamId
|
| | | */
|
| | | void mediaOutline(String app, String streamId);
|
| | |
|
| | | /**
|
| | | * 设置平台在线/离线
|
| | | * @param online
|
| | | */
|
| | | void updateParentPlatformStatus(String platformGbID, boolean online);
|
| | | }
|
| | |
| | | |
| | | @Delete("DELETE FROM device WHERE deviceId=#{deviceId}") |
| | | int del(String deviceId); |
| | | |
| | | @Update("UPDATE device SET online=0") |
| | | int outlineForAll(); |
| | | } |
| | |
| | | ParentPlatform getParentPlatById(int id); |
| | | |
| | | @Update("UPDATE parent_platform SET status=false" ) |
| | | void outlineForAllParentPlatform(); |
| | | int outlineForAllParentPlatform(); |
| | | |
| | | @Update("UPDATE parent_platform SET status=#{online} WHERE serverGBId=#{platformGbID}" ) |
| | | int updateParentPlatformStatus(String platformGbID, boolean online); |
| | | } |
| | |
| | | return (SendRtpItem)redis.get(key); |
| | | } |
| | | |
| | | @Override |
| | | public List<SendRtpItem> querySendRTPServer(String platformGbId) { |
| | | String key = VideoManagerConstants.PLATFORM_SEND_RTP_INFO_PREFIX + platformGbId + "_*"; |
| | | List<Object> queryResult = redis.scan(key); |
| | | List<SendRtpItem> result= new ArrayList<>(); |
| | | |
| | | for (int i = 0; i < queryResult.size(); i++) { |
| | | String keyItem = (String) queryResult.get(i); |
| | | result.add((SendRtpItem)redis.get(keyItem)); |
| | | } |
| | | |
| | | return result; |
| | | } |
| | | |
| | | /** |
| | | * 删除RTP推送信息缓存 |
| | | * @param platformGbId |
| | |
| | | } |
| | | |
| | | /** |
| | | * 更新所有设备离线 |
| | | * |
| | | * @return true:更新成功 false:更新失败 |
| | | */ |
| | | @Override |
| | | public synchronized boolean outlineForAll() { |
| | | logger.info("更新所有设备离线"); |
| | | int result = deviceMapper.outlineForAll(); |
| | | return result > 0; |
| | | } |
| | | |
| | | /** |
| | | * 清空通道 |
| | | * @param deviceId |
| | | */ |
| | |
| | | gbStreamMapper.setStatus(app, streamId, false); |
| | | } |
| | | |
| | | |
| | | @Override |
| | | public void updateParentPlatformStatus(String platformGbID, boolean online) { |
| | | platformMapper.updateParentPlatformStatus(platformGbID, online); |
| | | } |
| | | } |
| | |
| | | autoApplyPlay: false |
| | | # [可选] 部分设备需要扩展SDP,需要打开此设置 |
| | | seniorSdp: false |
| | | # 启用udp多端口模式 |
| | | # 启用多端口模式, 多端口模式使用端口区分每路流,兼容性更好。 单端口使用流的ssrc区分, 点播超时建议使用多端口测试 |
| | | rtp: |
| | | # [可选] 是否启用udp多端口模式, 开启后会在udpPortRange范围内选择端口用于媒体流传输 |
| | | # [可选] 是否启用多端口模式, 开启后会在portRange范围内选择端口用于媒体流传输 |
| | | enable: true |
| | | # [可选] 在此范围内选择端口用于媒体流传输, 不只是udp, 使用TCP被动传输模式时,也是从这个范围内选择端口 |
| | | udpPortRange: 30000,30500 # 端口范围 |
| | | # [可选] 在此范围内选择端口用于媒体流传输, |
| | | portRange: 30000,30500 # 端口范围 |
| | | |
| | | # [可选] 日志配置, 一般不需要改 |
| | | logging: |