Merge remote-tracking branch 'origin/wvp-28181-2.0' into wvp-28181-2.0

This commit is contained in:
648540858 2023-03-15 22:57:20 +08:00
commit be6666db03
12 changed files with 160 additions and 59 deletions

View File

@ -122,7 +122,7 @@ public interface ISIPCommander {
*/ */
void downloadStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId, void downloadStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId,
String startTime, String endTime, int downloadSpeed, InviteStreamCallback inviteStreamCallback, InviteStreamCallback hookEvent, String startTime, String endTime, int downloadSpeed, InviteStreamCallback inviteStreamCallback, InviteStreamCallback hookEvent,
SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException; SipSubscribe.Event errorEvent,SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException;
/** /**
* 视频流停止 * 视频流停止
@ -221,7 +221,6 @@ public interface ISIPCommander {
* *
* @param device 视频设备 * @param device 视频设备
* @param channelId 通道id非通道则是设备本身 * @param channelId 通道id非通道则是设备本身
* @param frontCmd 上级平台的指令如果存在则直接下发
* @param enabled 看守位使能1 = 开启0 = 关闭 * @param enabled 看守位使能1 = 开启0 = 关闭
* @param resetTime 自动归位时间间隔开启看守位时使用单位:(s) * @param resetTime 自动归位时间间隔开启看守位时使用单位:(s)
* @param presetIndex 调用预置位编号开启看守位时使用取值范围0~255 * @param presetIndex 调用预置位编号开启看守位时使用取值范围0~255

View File

@ -470,8 +470,9 @@ public class SIPCommander implements ISIPCommander {
*/ */
@Override @Override
public void downloadStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId, public void downloadStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId,
String startTime, String endTime, int downloadSpeed, InviteStreamCallback inviteStreamCallback, InviteStreamCallback hookEvent, String startTime, String endTime, int downloadSpeed,
SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException { InviteStreamCallback inviteStreamCallback, InviteStreamCallback hookEvent,
SipSubscribe.Event errorEvent,SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException {
logger.info("{} 分配的ZLM为: {} [{}:{}]", ssrcInfo.getStream(), mediaServerItem.getId(), mediaServerItem.getIp(), ssrcInfo.getPort()); logger.info("{} 分配的ZLM为: {} [{}:{}]", ssrcInfo.getStream(), mediaServerItem.getId(), mediaServerItem.getIp(), ssrcInfo.getPort());
String sdpIp; String sdpIp;
@ -540,11 +541,14 @@ public class SIPCommander implements ISIPCommander {
content.append("a=downloadspeed:" + downloadSpeed + "\r\n"); content.append("a=downloadspeed:" + downloadSpeed + "\r\n");
content.append("y=" + ssrcInfo.getSsrc() + "\r\n");//ssrc content.append("y=" + ssrcInfo.getSsrc() + "\r\n");//ssrc
logger.debug("此时请求下载信令的ssrc===>{}",ssrcInfo.getSsrc());
HookSubscribeForStreamChange hookSubscribe = HookSubscribeFactory.on_stream_changed("rtp", ssrcInfo.getStream(), true, null, mediaServerItem.getId()); HookSubscribeForStreamChange hookSubscribe = HookSubscribeFactory.on_stream_changed("rtp", ssrcInfo.getStream(), true, null, mediaServerItem.getId());
// 添加订阅 // 添加订阅
CallIdHeader newCallIdHeader = sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()), device.getTransport());
String callId=newCallIdHeader.getCallId();
subscribe.addSubscribe(hookSubscribe, (MediaServerItem mediaServerItemInUse, JSONObject json) -> { subscribe.addSubscribe(hookSubscribe, (MediaServerItem mediaServerItemInUse, JSONObject json) -> {
hookEvent.call(new InviteStreamInfo(mediaServerItem, json,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport()).getCallId(), "rtp", ssrcInfo.getStream())); logger.debug("sipc 添加订阅===callId {}",callId);
hookEvent.call(new InviteStreamInfo(mediaServerItem, json,callId, "rtp", ssrcInfo.getStream()));
subscribe.removeSubscribe(hookSubscribe); subscribe.removeSubscribe(hookSubscribe);
hookSubscribe.getContent().put("regist", false); hookSubscribe.getContent().put("regist", false);
hookSubscribe.getContent().put("schema", "rtsp"); hookSubscribe.getContent().put("schema", "rtsp");
@ -553,7 +557,7 @@ public class SIPCommander implements ISIPCommander {
(MediaServerItem mediaServerItemForEnd, JSONObject jsonForEnd) -> { (MediaServerItem mediaServerItemForEnd, JSONObject jsonForEnd) -> {
logger.info("[录像]下载结束, 发送BYE"); logger.info("[录像]下载结束, 发送BYE");
try { try {
streamByeCmd(device, channelId, ssrcInfo.getStream(),sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport()).getCallId()); streamByeCmd(device, channelId, ssrcInfo.getStream(),callId);
} catch (InvalidArgumentException | ParseException | SipException | } catch (InvalidArgumentException | ParseException | SipException |
SsrcTransactionNotFoundException e) { SsrcTransactionNotFoundException e) {
logger.error("[录像]下载结束, 发送BYE失败 {}", e.getMessage()); logger.error("[录像]下载结束, 发送BYE失败 {}", e.getMessage());
@ -561,15 +565,24 @@ public class SIPCommander implements ISIPCommander {
}); });
}); });
Request request = headerProvider.createPlaybackInviteRequest(device, channelId, content.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport()), ssrcInfo.getSsrc()); Request request = headerProvider.createPlaybackInviteRequest(device, channelId, content.toString(), null, SipUtils.getNewFromTag(), null,newCallIdHeader, ssrcInfo.getSsrc());
if (inviteStreamCallback != null) { if (inviteStreamCallback != null) {
inviteStreamCallback.call(new InviteStreamInfo(mediaServerItem, null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport()).getCallId(), "rtp", ssrcInfo.getStream())); inviteStreamCallback.call(new InviteStreamInfo(mediaServerItem, null,callId, "rtp", ssrcInfo.getStream()));
} }
sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, errorEvent, okEvent -> { sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, errorEvent, event -> {
ResponseEvent responseEvent = (ResponseEvent) okEvent.event; ResponseEvent responseEvent = (ResponseEvent) event.event;
SIPResponse response = (SIPResponse) responseEvent.getResponse(); SIPResponse response = (SIPResponse) responseEvent.getResponse();
streamSession.put(device.getDeviceId(), channelId, response.getCallIdHeader().getCallId(), ssrcInfo.getStream(), ssrcInfo.getSsrc(), mediaServerItem.getId(), response, VideoStreamSessionManager.SessionType.download); String contentString =new String(response.getRawContent());
int ssrcIndex = contentString.indexOf("y=");
String ssrc=ssrcInfo.getSsrc();
if (ssrcIndex >= 0) {
ssrc = contentString.substring(ssrcIndex + 2, ssrcIndex + 12);
}
logger.debug("接收到的下载响应ssrc====>{}",ssrcInfo.getSsrc());
logger.debug("接收到的下载响应ssrc====>{}",ssrc);
streamSession.put(device.getDeviceId(), channelId, response.getCallIdHeader().getCallId(), ssrcInfo.getStream(), ssrc, mediaServerItem.getId(), response, VideoStreamSessionManager.SessionType.download);
okEvent.response(event);
}); });
} }
@ -801,7 +814,6 @@ public class SIPCommander implements ISIPCommander {
* *
* @param device 视频设备 * @param device 视频设备
* @param channelId 通道id非通道则是设备本身 * @param channelId 通道id非通道则是设备本身
* @param frontCmd 上级平台的指令如果存在则直接下发
* @param enabled 看守位使能1 = 开启0 = 关闭 * @param enabled 看守位使能1 = 开启0 = 关闭
* @param resetTime 自动归位时间间隔开启看守位时使用单位:(s) * @param resetTime 自动归位时间间隔开启看守位时使用单位:(s)
* @param presetIndex 调用预置位编号开启看守位时使用取值范围0~255 * @param presetIndex 调用预置位编号开启看守位时使用取值范围0~255

View File

@ -163,7 +163,11 @@ public class RecordInfoResponseMessageHandler extends SIPRequestProcessorParent
public void releaseRequest(String deviceId, String sn,RecordInfo recordInfo){ public void releaseRequest(String deviceId, String sn,RecordInfo recordInfo){
String key = DeferredResultHolder.CALLBACK_CMD_RECORDINFO + deviceId + sn; String key = DeferredResultHolder.CALLBACK_CMD_RECORDINFO + deviceId + sn;
// 对数据进行排序 // 对数据进行排序
if(recordInfo!=null && recordInfo.getRecordList()!=null) {
Collections.sort(recordInfo.getRecordList()); Collections.sort(recordInfo.getRecordList());
}else{
recordInfo.setRecordList(new ArrayList<>());
}
RequestMessage msg = new RequestMessage(); RequestMessage msg = new RequestMessage();
msg.setKey(key); msg.setKey(key);

View File

@ -635,11 +635,8 @@ public class PlayServiceImpl implements IPlayService {
hookCallBack.call(downloadResult); hookCallBack.call(downloadResult);
streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream()); streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream());
}; };
InviteStreamCallback hookEvent = (InviteStreamInfo inviteStreamInfo) -> {
try { logger.info("收到订阅消息: " + inviteStreamInfo.getCallId());
cmder.downloadStreamCmd(mediaServerItem, ssrcInfo, device, channelId, startTime, endTime, downloadSpeed, infoCallBack,
inviteStreamInfo -> {
logger.info("收到订阅消息: " + inviteStreamInfo.getResponse().toJSONString());
dynamicTask.stop(downLoadTimeOutTaskKey); dynamicTask.stop(downLoadTimeOutTaskKey);
StreamInfo streamInfo = onPublishHandler(inviteStreamInfo.getMediaServerItem(), inviteStreamInfo.getResponse(), deviceId, channelId); StreamInfo streamInfo = onPublishHandler(inviteStreamInfo.getMediaServerItem(), inviteStreamInfo.getResponse(), deviceId, channelId);
streamInfo.setStartTime(startTime); streamInfo.setStartTime(startTime);
@ -651,7 +648,62 @@ public class PlayServiceImpl implements IPlayService {
downloadResult.setMediaServerItem(inviteStreamInfo.getMediaServerItem()); downloadResult.setMediaServerItem(inviteStreamInfo.getMediaServerItem());
downloadResult.setResponse(inviteStreamInfo.getResponse()); downloadResult.setResponse(inviteStreamInfo.getResponse());
hookCallBack.call(downloadResult); hookCallBack.call(downloadResult);
}, errorEvent); };
try {
cmder.downloadStreamCmd(mediaServerItem, ssrcInfo, device, channelId, startTime, endTime, downloadSpeed, infoCallBack,
hookEvent, errorEvent, eventResult ->
{
if (eventResult.type == SipSubscribe.EventResultType.response) {
ResponseEvent responseEvent = (ResponseEvent) eventResult.event;
String contentString = new String(responseEvent.getResponse().getRawContent());
// 获取ssrc
int ssrcIndex = contentString.indexOf("y=");
// 检查是否有y字段
if (ssrcIndex >= 0) {
//ssrc规定长度为10字节不取余下长度以避免后续还有f=字段 TODO 后续对不规范的非10位ssrc兼容
String ssrcInResponse = contentString.substring(ssrcIndex + 2, ssrcIndex + 12);
// 查询到ssrc不一致且开启了ssrc校验则需要针对处理
if (ssrcInfo.getSsrc().equals(ssrcInResponse)) {
return;
}
logger.info("[回放消息] 收到invite 200, 发现下级自定义了ssrc: {}", ssrcInResponse);
if (!mediaServerItem.isRtpEnable() || device.isSsrcCheck()) {
logger.info("[回放消息] SSRC修正 {}->{}", ssrcInfo.getSsrc(), ssrcInResponse);
if (!mediaServerItem.getSsrcConfig().checkSsrc(ssrcInResponse)) {
// ssrc 不可用
// 释放ssrc
mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc());
streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream());
eventResult.msg = "下级自定义了ssrc,但是此ssrc不可用";
eventResult.statusCode = 400;
errorEvent.response(eventResult);
return;
}
// 单端口模式streamId也有变化需要重新设置监听
if (!mediaServerItem.isRtpEnable()) {
// 添加订阅
HookSubscribeForStreamChange hookSubscribe = HookSubscribeFactory.on_stream_changed("rtp", ssrcInfo.getStream(), true, "rtsp", mediaServerItem.getId());
subscribe.removeSubscribe(hookSubscribe);
hookSubscribe.getContent().put("stream", String.format("%08x", Integer.parseInt(ssrcInResponse)).toUpperCase());
subscribe.addSubscribe(hookSubscribe, (MediaServerItem mediaServerItemInUse, JSONObject response) -> {
logger.info("[ZLM HOOK] ssrc修正后收到订阅消息 " + response.toJSONString());
dynamicTask.stop(downLoadTimeOutTaskKey);
// hook响应
onPublishHandlerForPlayback(mediaServerItemInUse, response, device.getDeviceId(), channelId, hookCallBack);
hookEvent.call(new InviteStreamInfo(mediaServerItem, null, eventResult.callId, "rtp", ssrcInfo.getStream()));
});
}
// 关闭rtp server
mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream());
// 重新开启ssrc server
mediaServerService.openRTPServer(mediaServerItem, ssrcInfo.getStream(), ssrcInResponse, device.isSsrcCheck(), true, ssrcInfo.getPort());
}
}
}
});
} catch (InvalidArgumentException | SipException | ParseException e) { } catch (InvalidArgumentException | SipException | ParseException e) {
logger.error("[命令发送失败] 录像下载: {}", e.getMessage()); logger.error("[命令发送失败] 录像下载: {}", e.getMessage());

View File

@ -344,10 +344,10 @@ public interface DeviceChannelMapper {
"select * " + "select * " +
"from device_channel " + "from device_channel " +
"where deviceId=#{deviceId}" + "where deviceId=#{deviceId}" +
" <if test='parentId != null and length != null' > and parentId = #{parentId} or left(channelId, #{parentId.length()}) = #{parentId} and length(channelId)=#{length} </if>" + " <if test='parentId != null and length != null' > and parentId = #{parentId} or left(channelId, LENGTH(#{parentId})) = #{parentId} and length(channelId)=#{length} </if>" +
" <if test='parentId == null and length != null' > and parentId = #{parentId} or length(channelId)=#{length} </if>" + " <if test='parentId == null and length != null' > and parentId = #{parentId} or length(channelId)=#{length} </if>" +
" <if test='parentId == null and length == null' > and parentId = #{parentId} </if>" + " <if test='parentId == null and length == null' > and parentId = #{parentId} </if>" +
" <if test='parentId != null and length == null' > and parentId = #{parentId} or left(channelId, #{parentId.length()}) = #{parentId} </if>" + " <if test='parentId != null and length == null' > and parentId = #{parentId} or left(channelId, LENGTH(#{parentId})) = #{parentId} </if>" +
" </script>"}) " </script>"})
List<DeviceChannel> getChannelsWithCivilCodeAndLength(String deviceId, String parentId, Integer length); List<DeviceChannel> getChannelsWithCivilCodeAndLength(String deviceId, String parentId, Integer length);

View File

@ -177,12 +177,14 @@ public class RedisCatchStorageImpl implements IRedisCatchStorage {
@Override @Override
public boolean startDownload(StreamInfo stream, String callId) { public boolean startDownload(StreamInfo stream, String callId) {
boolean result; boolean result;
String key=String.format("%S_%s_%s_%s_%s_%s_%s", VideoManagerConstants.DOWNLOAD_PREFIX,
userSetting.getServerId(), stream.getMediaServerId(), stream.getDeviceID(), stream.getChannelId(), stream.getStream(), callId);
if (stream.getProgress() == 1) { if (stream.getProgress() == 1) {
result = RedisUtil.set(String.format("%S_%s_%s_%s_%s_%s_%s", VideoManagerConstants.DOWNLOAD_PREFIX, logger.debug("添加下载缓存==已完成下载=》{}",key);
userSetting.getServerId(), stream.getMediaServerId(), stream.getDeviceID(), stream.getChannelId(), stream.getStream(), callId), stream); result = RedisUtil.set(key, stream);
}else { }else {
result = RedisUtil.set(String.format("%S_%s_%s_%s_%s_%s_%s", VideoManagerConstants.DOWNLOAD_PREFIX, logger.debug("添加下载缓存==未完成下载=》{}",key);
userSetting.getServerId(), stream.getMediaServerId(), stream.getDeviceID(), stream.getChannelId(), stream.getStream(), callId), stream, 60*60); result = RedisUtil.set(key, stream, 60*60);
} }
return result; return result;
} }
@ -617,7 +619,7 @@ public class RedisCatchStorageImpl implements IRedisCatchStorage {
stream, stream,
callId callId
); );
List<Object> streamInfoScan = RedisUtil.scan(key); List<Object> streamInfoScan = RedisUtil.scan2(key);
if (streamInfoScan.size() > 0) { if (streamInfoScan.size() > 0) {
return (StreamInfo) RedisUtil.get((String) streamInfoScan.get(0)); return (StreamInfo) RedisUtil.get((String) streamInfoScan.get(0));
}else { }else {

View File

@ -881,7 +881,13 @@ public class RedisUtil {
return new ArrayList<>(resultKeys); return new ArrayList<>(resultKeys);
} }
public static List<Object> scan2(String query) {
if (redisTemplate == null) {
redisTemplate = SpringBeanFactory.getBean("redisTemplate");
}
Set<String> keys = redisTemplate.keys(query);
return new ArrayList<>(keys);
}
// ============================== 消息发送与订阅 ============================== // ============================== 消息发送与订阅 ==============================
public static void convertAndSend(String channel, JSONObject msg) { public static void convertAndSend(String channel, JSONObject msg) {
if (redisTemplate == null) { if (redisTemplate == null) {

View File

@ -1,5 +1,6 @@
<template> <template>
<div ref="container" @dblclick="fullscreenSwich" style="width:100%;height:100%;background-color: #000000;margin:0 auto;"> <div ref="container" @dblclick="fullscreenSwich"
style="width:100%;height:100%;background-color: #000000;margin:0 auto;">
<div class="buttons-box" id="buttonsBox"> <div class="buttons-box" id="buttonsBox">
<div class="buttons-box-left"> <div class="buttons-box-left">
<i v-if="!playing" class="iconfont icon-play jessibuca-btn" @click="playBtnClick"></i> <i v-if="!playing" class="iconfont icon-play jessibuca-btn" @click="playBtnClick"></i>
@ -12,7 +13,7 @@
<span class="jessibuca-btn">{{ kBps }} kb/s</span> <span class="jessibuca-btn">{{ kBps }} kb/s</span>
<!-- <i class="iconfont icon-file-record1 jessibuca-btn"></i>--> <!-- <i class="iconfont icon-file-record1 jessibuca-btn"></i>-->
<!-- <i class="iconfont icon-xiangqing2 jessibuca-btn" ></i>--> <!-- <i class="iconfont icon-xiangqing2 jessibuca-btn" ></i>-->
<i class="iconfont icon-camera1196054easyiconnet jessibuca-btn" @click="jessibuca.screenshot('截图','png',0.5)" <i class="iconfont icon-camera1196054easyiconnet jessibuca-btn" @click="screenshot"
style="font-size: 1rem !important"></i> style="font-size: 1rem !important"></i>
<i class="iconfont icon-shuaxin11 jessibuca-btn" @click="playBtnClick"></i> <i class="iconfont icon-shuaxin11 jessibuca-btn" @click="playBtnClick"></i>
<i v-if="!fullscreen" class="iconfont icon-weibiaoti10 jessibuca-btn" @click="fullscreenSwich"></i> <i v-if="!fullscreen" class="iconfont icon-weibiaoti10 jessibuca-btn" @click="fullscreenSwich"></i>
@ -92,29 +93,49 @@ export default {
jessibucaPlayer[this._uid] = new window.Jessibuca(Object.assign( jessibucaPlayer[this._uid] = new window.Jessibuca(Object.assign(
{ {
container: this.$refs.container, container: this.$refs.container,
videoBuffer: 0.2, // autoWasm: true,
isResize: true, background: "",
decoder: "static/js/jessibuca/decoder.js", controlAutoHide: false,
useMSE: false,
showBandwidth: false,
isFlv: true,
// text: "WVP-PRO",
// background: "static/images/zlm-logo.png",
loadingText: "加载中",
hasAudio: typeof (this.hasAudio) == "undefined" ? true : this.hasAudio,
debug: false, debug: false,
supportDblclickFullscreen: false, // decoder: "static/js/jessibuca/decoder.js",
forceNoOffscreen: true,
hasAudio: typeof (this.hasAudio) == "undefined" ? true : this.hasAudio,
hasVideo: true,
heartTimeout: 5,
heartTimeoutReplay: true,
heartTimeoutReplayTimes: 3,
hiddenAutoPause: false,
hotKey: false,
isFlv: false,
isFullResize: false,
isNotMute: this.isNotMute,
isResize: false,
keepScreenOn: false,
loadingText: "请稍等, 视频加载中......",
loadingTimeout: 10,
loadingTimeoutReplay: true,
loadingTimeoutReplayTimes: 3,
openWebglAlignment: false,
operateBtns: { operateBtns: {
fullscreen: false, fullscreen: false,
screenshot: false, screenshot: false,
play: false, play: false,
audio: false, audio: false,
recorder: false, record: false
}, },
record: "record", recordType: "webm",
vod: this.vod, rotate: 0,
forceNoOffscreen: this.forceNoOffscreen, showBandwidth: false,
isNotMute: this.isNotMute, supportDblclickFullscreen: false,
timeout: 10,
useMSE: location.hostname !== "localhost" && location.protocol !== "https:",
useOffscreen: false,
useWCS: location.hostname === "localhost" || location.protocol === "https",
useWebFullScreen: false,
videoBuffer: 0,
wasmDecodeAudioSyncVideo: true,
wasmDecodeErrorReplay: true,
wcsUseVideoRender: true
}, },
options options
)); ));
@ -243,6 +264,11 @@ export default {
this.err = ""; this.err = "";
this.performance = ""; this.performance = "";
}, },
screenshot: function () {
if (jessibucaPlayer[this._uid]) {
jessibucaPlayer[this._uid].screenshot();
}
},
mute: function () { mute: function () {
if (jessibucaPlayer[this._uid]) { if (jessibucaPlayer[this._uid]) {
jessibucaPlayer[this._uid].mute(); jessibucaPlayer[this._uid].mute();

File diff suppressed because one or more lines are too long

View File

@ -62,7 +62,7 @@ declare namespace Jessibuca {
/** /**
* 1. `true`,canvas区域,,, `setScaleMode(2)` * 1. `true`,canvas区域,,, `setScaleMode(2)`
*/ */
isFullSize?: boolean; isFullResize?: boolean;
/** /**
* 1. `true`ws协议不检验是否以.flv为依据 * 1. `true`ws协议不检验是否以.flv为依据
*/ */
@ -120,7 +120,7 @@ declare namespace Jessibuca {
/** /**
* *
*/ */
loadingText?: boolean; loadingText?: string;
/** /**
* *
*/ */

File diff suppressed because one or more lines are too long