From 8a6ac7c26ed80616cb2f7a2a8805e49f4e34b5e7 Mon Sep 17 00:00:00 2001 From: "https://blog.iamtsm.cn" <1905333456@qq.com> Date: Sun, 16 Jul 2023 12:40:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=8D=95=E4=B8=AArtc?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5=E4=BF=A1=E6=81=AF=E6=9F=A5=E7=9C=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat: 支持远程连接语言,设备,网络,ice状态,ice类型展示 feat: 支持多端画布自适应比例 feat: 调整优化文本绘制 feat: 调整优化缓冲区满的日志 fix: 修复控制台版本打印 fix: 修复滚动条样式 fix: 修复页面多余节点 fix: 修复某些中英文引用 fix: 修复缓冲区满的阈值 --- PAY.md | 2 +- svr/conf/cfg.json | 2 +- svr/res/css/index.css | 27 +- svr/res/home.html | 4 +- svr/res/index.html | 67 +-- svr/res/js/comm.js | 102 ++--- svr/res/js/draw.js | 459 ++++++++++++++------- svr/res/js/index.js | 278 +++++++++---- svr/res/js/language.js | 26 +- svr/res/pay.html | 2 +- svr/src/socket/rtcCreateJoin/createJoin.js | 52 ++- svr/static/layui/font-ext/demo_index.html | 98 ++++- svr/static/layui/font-ext/iconfont.css | 22 +- svr/static/layui/font-ext/iconfont.js | 2 +- svr/static/layui/font-ext/iconfont.json | 28 ++ svr/static/layui/font-ext/iconfont.ttf | Bin 30868 -> 31892 bytes svr/static/layui/font-ext/iconfont.woff | Bin 17640 -> 18288 bytes svr/static/layui/font-ext/iconfont.woff2 | Bin 15076 -> 15652 bytes 18 files changed, 841 insertions(+), 330 deletions(-) diff --git a/PAY.md b/PAY.md index 4444eee..0862dab 100644 --- a/PAY.md +++ b/PAY.md @@ -16,7 +16,7 @@ - **价格:** 按功能大小,紧急程度,耗时,定制内容是否允许开源,等情况收费 ## 收费模式 -- **按小时计费:** 根据实际工作时间计算费用,目前个人定制功能2小时起,每小时200~300,企业定制另谈 +- **按小时计费:** 根据实际工作时间计算费用,目前个人定制功能2小时起,每小时200~300 - **阶段性付费:** 确认开发前,预付1/3,开发完成付1/3,交付上线1/3 ## 付款方式 diff --git a/svr/conf/cfg.json b/svr/conf/cfg.json index 0067843..9070541 100644 --- a/svr/conf/cfg.json +++ b/svr/conf/cfg.json @@ -1,5 +1,5 @@ { - "version": "10.2.5", + "version": "10.2.6", "ws": { "port": 8444, "host": "ws://127.0.0.1:8444" diff --git a/svr/res/css/index.css b/svr/res/css/index.css index 20da8ad..0c0a28d 100644 --- a/svr/res/css/index.css +++ b/svr/res/css/index.css @@ -284,7 +284,7 @@ body { } .tl-rtc-file-user-body { - display: flex; + display: -webkit-box; padding-top: 10px; } @@ -1134,3 +1134,28 @@ body { border-radius: 5px; } + +.remote_user_info{ + text-align: left; + padding: 20px 20px 0 20px; +} + +.remote_user_info div{ + padding-bottom: 5px; + font-weight: 200; + word-break: break-all; +} + +.remote_user_info div b{ + color: #548726; + margin-left: 5px; +} + +.layui-carousel[lay-indicator=outside] .layui-carousel-ind{ + top: 10px; +} + +.layui-carousel>[carousel-item]{ + overflow-y: auto; + overflow-x: hidden; +} diff --git a/svr/res/home.html b/svr/res/home.html index d535e30..1727569 100644 --- a/svr/res/home.html +++ b/svr/res/home.html @@ -7,8 +7,8 @@ - - + + diff --git a/svr/res/index.html b/svr/res/index.html index 6fef5ae..adb33b8 100644 --- a/svr/res/index.html +++ b/svr/res/index.html @@ -77,7 +77,7 @@ @@ -90,14 +90,14 @@ {{lang.sharing}}: {{screenShareTimes < 10 ? '0' + screenShareTimes : screenShareTimes}}{{lang.second}} - - {{lang.videoing}}: {{videoShareTimes < 10 ? '0' + videoShareTimes : - videoShareTimes}}{{lang.second}} - - {{lang.living}}: {{liveShareTimes < 10 ? '0' + liveShareTimes : - liveShareTimes}}{{lang.second}} - + + {{lang.videoing}}: {{videoShareTimes < 10 ? '0' + videoShareTimes : + videoShareTimes}}{{lang.second}} + + {{lang.living}}: {{liveShareTimes < 10 ? '0' + liveShareTimes : + liveShareTimes}}{{lang.second}} + @@ -258,7 +258,6 @@ {{lang.pickup_code}} -
@@ -316,10 +315,12 @@
- 【{{lang.owner}}】- - {{nickName}} - {{lang.self}} + 【{{lang.owner}}】- + 【{{lang.self}}】- {{nickName}} + + + {{socketId}} - {{socketId}}
@@ -346,7 +347,7 @@
-
+
【{{lang.owner}}】- - {{remote.nickName}} + {{remote.nickName}} - {{remote.id}} + + + : {{remote.langMode}} - {{remote.ua}} - {{remote.network}} - + : + {{remote.iceConnectionState}} - {{remote.p2pMode}} + {{remote.iceConnectionState}} - {{remote.p2pMode}} - {{remote.langMode}} - {{remote.id}}
- @@ -424,7 +434,7 @@
+ :style="{height: sendFileRecoderHeight+'px',overflowY: (sendFileRecoderList.length > 1 ? 'auto' : 'none') }">
@@ -615,7 +625,7 @@ style="cursor: pointer; right: 10px;position: absolute;">
+ :style="{height: chooseFileHeight+'px',overflowY: (chooseFileList.length > 1 ? 'auto' : 'none') }">
@@ -691,7 +701,7 @@ style="cursor: pointer; right: 10px;position: absolute;">
+ :style="{height: sendFileRecoderHistoryHeight+'px',overflowY: (sendFileRecoderHistoryList.length > 1 ? 'auto' : 'none') }">
@@ -771,7 +781,7 @@ style="cursor: pointer; right: 10px;position: absolute;">
+ :style="{height: receiveFileHeight+'px',overflowY: (receiveFileRecoderList.length > 1 ? 'auto' : 'none')}">
@@ -867,7 +877,7 @@
+ :style="{height: codeFileHeight+'px',overflowY: (receiveCodeFileList.length > 1 ? 'auto' : 'none') }">
@@ -955,7 +965,7 @@
-
+
{{log.time}}
@@ -964,7 +974,7 @@
+ style="max-height: 300px; overflow-y: auto;"> {{log.type}} {{log.type}} @@ -993,7 +1003,7 @@
-
+
@@ -1013,7 +1023,7 @@
-
+
@@ -1033,7 +1043,7 @@
-
+
@@ -1057,10 +1067,11 @@ layui.use([ 'layedit', 'form', 'layer', 'laytpl', 'upload', 'dropdown', 'carousel', 'util', 'colorpicker', - 'slider', 'dropdown' + 'slider', 'dropdown', 'carousel' ], function () { window.layer = layui.layer; window.form = layui.form; + window.carousel = layui.carousel; window.$ = layui.$; window.layedit = layui.layedit; window.laytpl = layui.laytpl; diff --git a/svr/res/js/comm.js b/svr/res/js/comm.js index ceb4e6c..0a1b4d5 100644 --- a/svr/res/js/comm.js +++ b/svr/res/js/comm.js @@ -286,38 +286,8 @@ window.tlrtcfile = { return false; } }, + getWebrtcStats: async function (peerConnection) { - // 候选者对 - "candidate-pair" | - // 证书相关的统计信息 - "certificate" | - // 当前音视频编解码器的统计信息 - "codec" | - // CSRC相关的统计信息 - "csrc" | - // 数据通道的相关统计信息 - "data-channel" | - // 传入数据流的相关统计信息 - "inbound-rtp" | - // 本地候选连接的相关统计信息 - "local-candidate" | - // 媒体源的相关统计信息 - "media-source" | - // 传出数据流的相关统计信息 - "outbound-rtp" | - // 对等连接的相关统计信息 - "peer-connection" | - // 对等连接的相关统计信息 - "remote-candidate" | - // 远程传入数据流的相关统计信息 - "remote-inbound-rtp" | - // 远程传出数据流的相关统计信息 - "remote-outbound-rtp" | - // 媒体轨道的相关统计信息 - "track" | - // 传输协议的相关统计信息 - "transport"; - if (!peerConnection) { return "RTCPeerConnection is not available"; } @@ -325,23 +295,63 @@ window.tlrtcfile = { return "RTCStatsReport is not available"; } - let result = { } - - let stats = await peerConnection.getStats(null); - - stats.forEach((report) => { - if (!report.type) return; - let data = {} - Object.keys(report).forEach((statName) => { - data[statName] = report[statName] - }); - result[report.type] = { - kind : report.kind, - data : data + function getTypeDescription(type) { + switch (type) { + case 'candidate-pair': + return '候选者对'; + case 'certificate': + return '证书相关的统计信息'; + case 'codec': + return '当前音视频编解码器的统计信息'; + case 'csrc': + return 'CSRC相关的统计信息'; + case 'data-channel': + return '数据通道的相关统计信息'; + case 'inbound-rtp': + return '传入数据流的相关统计信息'; + case 'local-candidate': + return '本地候选连接的相关统计信息'; + case 'media-source': + return '媒体源的相关统计信息'; + case 'outbound-rtp': + return '传出数据流的相关统计信息'; + case 'peer-connection': + return '对等连接的相关统计信息'; + case 'remote-candidate': + return '远程候选连接的相关统计信息'; + case 'remote-inbound-rtp': + return '远程传入数据流的相关统计信息'; + case 'remote-outbound-rtp': + return '远程传出数据流的相关统计信息'; + case 'track': + return '媒体轨道的相关统计信息'; + case 'transport': + return '传输协议的相关统计信息'; + case 'media-playout': + return '音频播放的相关统计数据' + default: + return '未知类型'; } - }); + } - return result + function getRTCStats(peerConnection) { + const statsMap = new Map(); + return new Promise((resolve) => { + peerConnection.getStats().then((stats) => { + stats.forEach((report) => { + const { type } = report; + if (!statsMap.has(type)) { + statsMap.set(type, []); + } + statsMap.get(type).push({ report, description: getTypeDescription(type) }); + }); + + resolve(statsMap); + }); + }); + } + + return await getRTCStats(peerConnection); }, copyTxt: function (id, content) { let that = this; diff --git a/svr/res/js/draw.js b/svr/res/js/draw.js index 6edc064..eb0205f 100644 --- a/svr/res/js/draw.js +++ b/svr/res/js/draw.js @@ -17,6 +17,8 @@ const draw = new Vue({ drawHistoryList: [], // 绘制历史操作列表, 用于回退 drawRollbackPoint: 0, // 绘制回退点 lineWidth: 1, // 画笔线宽 + lineCap : "round", + lineJoin : "round", strokeStyle: "#000000", // 画笔颜色 //line: 线条, circle: 圆形, rectangle: 矩形, text: 文字, delete: 擦除 drawMode: "line", // 画笔模式 @@ -26,7 +28,7 @@ const draw = new Vue({ starFill : false, //填充星星 rhomboidFill : false, //填充平行四边形 hexagonFill : false, //填充六边形 - circleStarPoint: { x: 0, y: 0 }, //圆形开始点 + circleStartPoint: { x: 0, y: 0 }, //圆形开始点 triangleStartPoint : { x: 0, y: 0 }, //三角形开始点 starStartPoint : { x: 0, y: 0 }, //星星开始点 rhomboidStartPoint : { x: 0, y: 0 }, //平行四边形开始点 @@ -106,39 +108,66 @@ const draw = new Vue({ if (!this.isOpenDraw) { return } + const { drawMode, event } = options; const canvas = document.getElementById('tl-rtc-file-mouse-draw-canvas'); - options.canvas = canvas; - options.context = canvas.getContext('2d'); - options.fromRemote = true; - - const { drawMode } = options; + const context = canvas.getContext('2d'); + options.remote.canvas = canvas; + options.remote.context = context; //收到结束标识,保存当前画板到缓存数据中 - if(options.event === 'end'){ - this.endDrawHandler(options) + if(event === 'end'){ + this.endDrawHandler({canvas, context}) return } + let { + width : remoteWidth, height : remoteHeight, lineWidth, + curPoint, prePoint, starStartPoint, circleStartPoint, triangleStartPoint, rectangleStartPoint + } = options.remote; + + //计算双方画布比例,按比例进行坐标放大/缩小 + const ratioWidth = canvas.width / remoteWidth; + const ratioHeight = canvas.height / remoteHeight; + + //调整画笔 + options.remote.lineWidth = lineWidth * (ratioWidth + ratioHeight) / 2 + curPoint.x = curPoint.x * ratioWidth; + curPoint.y = curPoint.y * ratioHeight; + options.remote.curPoint = curPoint; + if (drawMode === 'line') { + prePoint.x = prePoint.x * ratioWidth; + prePoint.y = prePoint.y * ratioHeight; + options.remote.prePoint = prePoint; this.drawLine(options); } else if (drawMode === 'circle') { + circleStartPoint.x = circleStartPoint.x * ratioWidth; + circleStartPoint.y = circleStartPoint.y * ratioHeight; + options.remote.circleStartPoint = circleStartPoint; this.drawCircle(options); } else if (drawMode === 'rectangle') { + rectangleStartPoint.x = rectangleStartPoint.x * ratioWidth; + rectangleStartPoint.y = rectangleStartPoint.y * ratioHeight; + options.remote.rectangleStartPoint = rectangleStartPoint; this.drawRectangle(options); } else if (drawMode === 'text') { this.drawText(options); } else if(drawMode === 'triangle'){ + triangleStartPoint.x = triangleStartPoint.x * ratioWidth; + triangleStartPoint.y = triangleStartPoint.y * ratioHeight; + options.remote.triangleStartPoint = triangleStartPoint; this.drawTriangle(options); } else if(drawMode === 'star'){ + starStartPoint.x = starStartPoint.x * ratioWidth; + starStartPoint.y = starStartPoint.y * ratioHeight; + options.remote.starStartPoint = starStartPoint; this.drawStar(options); } else { console.log("收到远程未知的绘制模式") } }, // 打开/关闭本地画笔 - openDraw: function ({ - openCallback, closeCallback, localDrawCallback - }) { + openDraw: function ({ openCallback, closeCallback, localDrawCallback }) { let that = this; if (this.isOpenDraw) { @@ -466,126 +495,185 @@ const draw = new Vue({ that.drawing = false; }; }, + // 开始绘制 startDrawHandler: function ({ canvas, context, localDrawCallback }) { //公共参数 - let commOptions = { - event : "start", + let localCommOptions = { canvas, context, localDrawCallback, - drawMode: this.drawMode, + devicePixelRatio : this.devicePixelRatio, + width : canvas.width, + height: canvas.height, + lineCap: this.lineCap, + lineJoin: this.lineJoin, lineWidth: this.lineWidth, strokeStyle: this.strokeStyle, fillStyle: this.strokeStyle, - lineCap: "round", - lineJoin: "round" } - + if (this.drawMode === 'delete') { - this.drawDelete(Object.assign(commOptions, { - prePoint: this.prePoint, - curPoint : this.prePoint - })) + this.drawDelete({ + event : "start", + drawMode: this.drawMode, + local : Object.assign(localCommOptions, { + prePoint: this.prePoint, + curPoint : this.prePoint + }), + }) } else if (this.drawMode === 'rectangle') { //开始的时候固定好矩形的起点 this.rectangleStartPoint = this.prePoint; - this.drawRectangle(Object.assign(commOptions, { - rectangleStartPoint: this.rectangleStartPoint, - curPoint: this.prePoint, - rectangleFill : this.rectangleFill - })) + this.drawRectangle({ + event : "start", + drawMode: this.drawMode, + local : Object.assign(localCommOptions, { + rectangleStartPoint: this.rectangleStartPoint, + curPoint: this.prePoint, + rectangleFill : this.rectangleFill + }), + }) } else if (this.drawMode === 'circle') { //开始的时候固定好圆的起点 this.circleStartPoint = this.prePoint; - this.drawCircle(Object.assign(commOptions, { - circleStartPoint: this.circleStartPoint, - curPoint: this.prePoint, - circleFill : this.circleFill, - })) + this.drawCircle({ + event : "start", + drawMode: this.drawMode, + local : Object.assign(localCommOptions, { + circleStartPoint: this.circleStartPoint, + curPoint: this.prePoint, + circleFill : this.circleFill, + }), + }) } else if(this.drawMode === 'triangle'){ //开始的时候固定好三角形的起点 this.triangleStartPoint = this.prePoint; - this.drawTriangle(Object.assign(commOptions, { - triangleStartPoint: this.triangleStartPoint, - curPoint: this.prePoint, - triangleFill : this.triangleFill - })); + this.drawTriangle({ + event : "start", + drawMode: this.drawMode, + local : Object.assign(localCommOptions, { + triangleStartPoint: this.triangleStartPoint, + curPoint: this.prePoint, + triangleFill : this.triangleFill + }), + }) } else if(this.drawMode === 'star'){ //开始的时候固定好星星的起点 this.starStartPoint = this.prePoint; - this.drawStar(Object.assign(commOptions, { - starStartPoint: this.starStartPoint, - curPoint: this.prePoint, - starFill : this.starFill - })); + this.drawStar({ + event : "start", + drawMode: this.drawMode, + local : Object.assign(localCommOptions, { + starStartPoint: this.starStartPoint, + curPoint: this.prePoint, + starFill : this.starFill + }), + }) } else if (this.drawMode === 'text') { - this.drawText(Object.assign(commOptions, { - curPoint: this.prePoint, - })) - this.endDrawHandler(Object.assign(commOptions, { + this.drawText({ + event : "start", + drawMode: this.drawMode, + local : Object.assign(localCommOptions, { + curPoint: this.prePoint, + }), + }) + this.endDrawHandler(Object.assign(localCommOptions, { curPoint: this.prePoint, })) } else if(this.drawMode === 'line'){ - this.drawLine(Object.assign(commOptions, { - prePoint: this.prePoint, - curPoint: this.prePoint, - })); + this.drawLine({ + event : "start", + drawMode: this.drawMode, + local : Object.assign(localCommOptions, { + prePoint: this.prePoint, + curPoint: this.prePoint, + }), + }) } }, + // 绘制中 drawingHandler: function ({ canvas, curPoint, context, localDrawCallback }) { if (!this.drawing) { return } + //公共参数 - let commOptions = { - event : "move", + let localCommOptions = { canvas, context, localDrawCallback, - drawMode: this.drawMode, + devicePixelRatio : this.devicePixelRatio, + width : canvas.width, + height: canvas.height, + lineCap: this.lineCap, + lineJoin: this.lineJoin, lineWidth: this.lineWidth, strokeStyle: this.strokeStyle, fillStyle: this.strokeStyle, - lineCap: "round", - lineJoin: "round" } if (this.drawMode === 'delete') { - this.drawDelete(Object.assign(commOptions, { - prePoint: this.prePoint, - curPoint - })) + this.drawDelete({ + event : "move", + drawMode: this.drawMode, + local : Object.assign(localCommOptions, { + prePoint: this.prePoint, + curPoint + }), + }) } else if (this.drawMode === 'rectangle') { - this.drawRectangle(Object.assign(commOptions, { - rectangleStartPoint: this.rectangleStartPoint, - curPoint, - rectangleFill : this.rectangleFill - })); + this.drawRectangle({ + event : "move", + drawMode: this.drawMode, + local : Object.assign(localCommOptions, { + rectangleStartPoint: this.rectangleStartPoint, + curPoint, + rectangleFill : this.rectangleFill + }), + }) } else if (this.drawMode === 'circle') { - this.drawCircle(Object.assign(commOptions, { - circleStartPoint: this.circleStartPoint, - curPoint, - circleFill : this.circleFill, - })) + this.drawCircle({ + event : "move", + drawMode: this.drawMode, + local : Object.assign(localCommOptions, { + circleStartPoint: this.circleStartPoint, + curPoint, + circleFill : this.circleFill, + }), + }) } else if(this.drawMode === 'triangle'){ - this.drawTriangle(Object.assign(commOptions, { - triangleStartPoint: this.triangleStartPoint, - curPoint, - triangleFill : this.triangleFill - })); + this.drawTriangle({ + event : "move", + drawMode: this.drawMode, + local : Object.assign(localCommOptions, { + triangleStartPoint: this.triangleStartPoint, + curPoint, + triangleFill : this.triangleFill + }), + + }) } else if(this.drawMode === 'star'){ - this.drawStar(Object.assign(commOptions, { - starStartPoint: this.starStartPoint, - curPoint, - starFill : this.starFill - })); + this.drawStar({ + event : "move", + drawMode: this.drawMode, + local : Object.assign(localCommOptions, { + starStartPoint: this.starStartPoint, + curPoint, + starFill : this.starFill + }), + }) } else if(this.drawMode === 'line'){ - this.drawLine(Object.assign(commOptions, { - prePoint: this.prePoint, - curPoint, - })); + this.drawLine({ + event : "move", + drawMode: this.drawMode, + local : Object.assign(localCommOptions, { + prePoint: this.prePoint, + curPoint, + }), + }) } }, + // 结束绘制 endDrawHandler: function ({ canvas, curPoint, context, localDrawCallback }) { //图像记录,用于回滚撤销操作 if ( @@ -600,13 +688,12 @@ const draw = new Vue({ //结束的时候通知下远程,可以保存画布到缓存列表中 localDrawCallback && localDrawCallback({ event : "end", - drawMode : this.drawMode + drawMode : this.drawMode, + remote : {} }) }, - //下载画布图片 - drawDownload: function ( options ) { - const { canvas, context, localDrawCallback } = options; - + // 下载画布图片 + drawDownload: function ({ canvas, context, localDrawCallback }) { let image = canvas.toDataURL("image/png").replace("image/png", "image/octet-stream"); let link = document.createElement('a'); link.href = image; @@ -614,16 +701,13 @@ const draw = new Vue({ link.click(); }, // 画布重置 - drawReset: function (options) { - const { canvas, context, localDrawCallback } = options; - + drawReset: function ({ canvas, context, localDrawCallback }) { context.clearRect(0, 0, canvas.width, canvas.height); this.drawHistoryList = []; this.drawRollbackPoint = 0; }, // 回退回滚的绘制 - drawUndoRollback: function (options) { - const { canvas, context, localDrawCallback } = options; + drawUndoRollback: function ({ canvas, context, localDrawCallback }) { //最多前进到最后一条记录 if (this.drawRollbackPoint < this.drawHistoryList.length - 1) { this.drawRollbackPoint = this.drawRollbackPoint + 1; @@ -637,8 +721,7 @@ const draw = new Vue({ } }, // 画布回退 - drawRollback: async function (options) { - const { canvas, context, localDrawCallback } = options; + drawRollback: async function ({ canvas, context, localDrawCallback }) { //最多回退到原点 if (this.drawRollbackPoint > 0) { this.drawRollbackPoint = this.drawRollbackPoint - 1; @@ -651,9 +734,17 @@ const draw = new Vue({ } }, // 画笔擦除 - drawDelete: function (options) { - const { canvas, context, prePoint, curPoint, localDrawCallback } = options; - context.lineWidth = this.lineWidth; + drawDelete: function ({ event, drawMode, local : { + canvas, context, lineWidth, curPoint, prePoint, lineCap, + width, height, devicePixelRatio, + lineJoin, strokeStyle, fillStyle + }}) { + context.lineWidth = lineWidth; + context.lineCap = lineCap; + context.lineJoin = lineJoin; + context.strokeStyle = strokeStyle; + context.fillStyle = fillStyle; + //防止移动过快,canvas渲染存在间隔,导致线条断层,在这里补充绘制间隔的点 let x = prePoint.x; let y = prePoint.y; @@ -666,14 +757,17 @@ const draw = new Vue({ while (i < distance) { x += xUnit; y += yUnit; - context.clearRect(x - 20, y - 20, this.lineWidth, this.lineWidth); + context.clearRect(x - 20, y - 20, lineWidth, lineWidth); i++; } }, // 图片渲染处理 - drawImage: function (options) { + drawImage: function ({ event, drawMode, local : { + canvas, context, lineWidth, curPoint, prePoint, lineCap, + width, height, devicePixelRatio, + lineJoin, strokeStyle, fillStyle, localDrawCallback + }}) { let that = this; - const { canvas, context, localDrawCallback } = options; let input = document.createElement("input"); input.setAttribute("type", "file"); input.setAttribute("accept", "image/*"); @@ -695,11 +789,14 @@ const draw = new Vue({ } }, // 画笔渲染处理, 两点式绘制 - drawLine: function (options) { - const { - canvas, context, localDrawCallback, prePoint, curPoint, - lineWidth, strokeStyle, fillStyle, lineCap, lineJoin, fromRemote, - } = options; + drawLine: function ({ event, drawMode, fromRemote, local, remote}) { + if(fromRemote){ + local = remote; + } + let { + canvas, context, lineWidth, curPoint, prePoint, lineCap, + width, height, devicePixelRatio, lineJoin, strokeStyle, fillStyle, localDrawCallback + } = local; // 设置画笔样式 context.lineWidth = lineWidth; @@ -728,16 +825,23 @@ const draw = new Vue({ i++; } - if (!fromRemote) { //本地的绘制数据回调给远端 - localDrawCallback && localDrawCallback(options); + //如果是本地绘制,完成绘制后数据回调给远端 + if (!fromRemote) { + localDrawCallback && localDrawCallback({ event, drawMode, fromRemote : true, remote : { + lineWidth, curPoint, prePoint, lineCap, width, height, devicePixelRatio, + lineJoin, strokeStyle, fillStyle, + }}); } }, // 星星渲染处理 - drawStar: function (options) { - const { - canvas, context, localDrawCallback, starStartPoint, curPoint, - lineWidth, strokeStyle, fillStyle, lineCap, lineJoin, fromRemote, starFill - } = options; + drawStar: function ({ event, drawMode, fromRemote, local, remote}) { + if(fromRemote){ + local = remote; + } + let { + canvas, context, lineWidth, curPoint, starStartPoint, lineCap, starFill, + width, height, devicePixelRatio, lineJoin, strokeStyle, fillStyle, localDrawCallback + } = local; // 设置画笔样式 context.lineWidth = lineWidth; @@ -778,16 +882,23 @@ const draw = new Vue({ } } - if (!fromRemote) { //本地的绘制数据回调给远端 - localDrawCallback && localDrawCallback(options); + //如果是本地绘制,完成绘制后数据回调给远端 + if (!fromRemote) { + localDrawCallback && localDrawCallback({ event, drawMode, fromRemote : true, remote : { + lineWidth, curPoint, starStartPoint, lineCap, width, height, devicePixelRatio, + lineJoin, strokeStyle, fillStyle, starFill + }}); } }, // 三角形渲染处理 - drawTriangle: function (options) { - const { - canvas, context, localDrawCallback, triangleStartPoint, curPoint, - lineWidth, strokeStyle, fillStyle, lineCap, lineJoin, fromRemote, triangleFill - } = options; + drawTriangle: function ({ event, drawMode, fromRemote, local, remote }) { + if(fromRemote){ + local = remote; + } + let { + canvas, context, lineWidth, curPoint, triangleStartPoint, lineCap, triangleFill, + width, height, devicePixelRatio, lineJoin, strokeStyle, fillStyle, localDrawCallback + } = local; // 设置画笔样式 context.lineWidth = lineWidth; @@ -827,16 +938,23 @@ const draw = new Vue({ } } - if (!fromRemote) { //本地的绘制数据回调给远端 - localDrawCallback && localDrawCallback(options); + //如果是本地绘制,完成绘制后数据回调给远端 + if (!fromRemote) { + localDrawCallback && localDrawCallback({ event, drawMode, fromRemote : true, remote : { + lineWidth, curPoint, triangleStartPoint, lineCap, width, height, devicePixelRatio, + lineJoin, strokeStyle, fillStyle, triangleFill + }}); } }, // 圆形渲染处理 - drawCircle: function (options) { - const { - canvas, context, localDrawCallback, circleStartPoint, curPoint, - lineWidth, strokeStyle, fillStyle, lineCap, lineJoin, fromRemote, circleFill - } = options; + drawCircle: function ({ event, drawMode, fromRemote, local, remote }) { + if(fromRemote){ + local = remote; + } + let { + canvas, context, lineWidth, curPoint, circleStartPoint, lineCap, circleFill, + width, height, devicePixelRatio, lineJoin, strokeStyle, fillStyle, localDrawCallback + } = local; // 设置画笔样式 context.lineWidth = lineWidth; @@ -869,16 +987,23 @@ const draw = new Vue({ } } - if (!fromRemote) { //本地的绘制数据回调给远端 - localDrawCallback && localDrawCallback(options); + //如果是本地绘制,完成绘制后数据回调给远端 + if (!fromRemote) { + localDrawCallback && localDrawCallback({ event, drawMode, fromRemote : true, remote : { + lineWidth, curPoint, circleStartPoint, lineCap, width, height, devicePixelRatio, + lineJoin, strokeStyle, fillStyle, circleFill + }}); } }, // 矩形渲染处理 - drawRectangle: function (options) { - const { - canvas, context, localDrawCallback, rectangleStartPoint, curPoint, - lineWidth, strokeStyle, fillStyle, lineCap, lineJoin, fromRemote, rectangleFill - } = options; + drawRectangle: function ({ event, drawMode, fromRemote, local, remote }) { + if(fromRemote){ + local = remote; + } + let { + canvas, context, lineWidth, curPoint, rectangleStartPoint, lineCap, rectangleFill, + width, height, devicePixelRatio, lineJoin, strokeStyle, fillStyle, localDrawCallback + } = local; // 设置画笔样式 context.lineWidth = lineWidth; @@ -890,8 +1015,8 @@ const draw = new Vue({ // 计算矩形的位置和尺寸 const x = Math.min(rectangleStartPoint.x, curPoint.x); const y = Math.min(rectangleStartPoint.y, curPoint.y); - const width = Math.abs(curPoint.x - rectangleStartPoint.x); - const height = Math.abs(curPoint.y - rectangleStartPoint.y); + const rwidth = Math.abs(curPoint.x - rectangleStartPoint.x); + const rheight = Math.abs(curPoint.y - rectangleStartPoint.y); if (this.drawRollbackPoint >= 0) { const img = new Image(); @@ -903,7 +1028,7 @@ const draw = new Vue({ context.drawImage(img, 0, 0, canvas.width, canvas.height); //绘制新矩形 context.beginPath(); - context.rect(x, y, width, height); + context.rect(x, y, rwidth, rheight); if(rectangleFill){ context.fill(); } @@ -911,43 +1036,58 @@ const draw = new Vue({ } } - if (!fromRemote) { //本地的绘制数据回调给远端 - localDrawCallback && localDrawCallback(options); + //如果是本地绘制,完成绘制后数据回调给远端 + if (!fromRemote) { + localDrawCallback && localDrawCallback({ event, drawMode, fromRemote : true, remote : { + lineWidth, curPoint, rectangleStartPoint, lineCap, width, height, devicePixelRatio, + lineJoin, strokeStyle, fillStyle, rectangleFill + }}); } }, // 文字渲染处理 - drawText: function (options) { - let { - canvas, context, localDrawCallback, curPoint, text, lineWidth, strokeStyle, - fillStyle, lineCap, lineJoin, fromRemote, - } = options; + drawText: function ({ event, drawMode, fromRemote, local, remote }) { + if(fromRemote){ + local = remote; + } + let { + canvas, context, lineWidth, curPoint, text, lineCap, + width, height, devicePixelRatio, lineJoin, strokeStyle, fillStyle, localDrawCallback + } = local; curPoint = { - x : curPoint.x / window.devicePixelRatio, - y : curPoint.y / window.devicePixelRatio + x : curPoint.x / devicePixelRatio, + y : curPoint.y / devicePixelRatio } // 设置字体样式 context.strokeStyle = strokeStyle; - context.font = "28px orbitron"; + context.font = Math.min(lineWidth * 7, 28) + "px orbitron"; context.textBaseline = "middle"; context.lineCap = lineCap; context.lineJoin = lineJoin; - context.lineWidth = 3; + context.lineWidth = 1; const canvasWidth = parseInt(canvas.style.width); const canvasHeight = parseInt(canvas.style.height); //文字渲染处理 function drawTextHandler(content){ - const textWidth = context.measureText(content).width; - // 如果文字超出画布宽度,将文字绘制到画布最右边 - let fixPointX = canvasWidth - textWidth > curPoint.x ? curPoint.x : canvasWidth - textWidth; - fixPointX = fixPointX < 10 ? 10 : fixPointX; - // 如果文字超出画布高度,将文字绘制到画布最下边 - let fixPointY = canvasHeight - 20 > curPoint.y ? curPoint.y : canvasHeight - 20; - fixPointY = fixPointY < 10 ? 10 : fixPointY; - context.strokeText(content, fixPointX * window.devicePixelRatio, fixPointY * window.devicePixelRatio); + let words = content.split(""); + let subWords = ""; + let wordHeight = 40; + for(let i = 0; i < words.length; i++){ + let curSubWords = subWords + words[i]; + const curSubWordsWidth = context.measureText(curSubWords).width; + + if(curPoint.x + curSubWordsWidth > canvasWidth && i > 0){ + context.fillText(subWords, curPoint.x * devicePixelRatio, curPoint.y * devicePixelRatio); + subWords = words[i]; + curPoint.y += wordHeight; + }else{ + subWords = curSubWords; + } + } + context.fillText(subWords, curPoint.x * devicePixelRatio, curPoint.y * devicePixelRatio); } if(fromRemote){ @@ -983,7 +1123,7 @@ const draw = new Vue({ } else if (curPoint.y < 100) { textarea.style.bottom = (canvasHeight - 100) + 'px'; } else { - textarea.style.top = (curPoint.y + 70)+ 'px'; + textarea.style.top = (curPoint.y + 170)+ 'px'; } parentDom.appendChild(textarea); textarea.focus(); @@ -992,8 +1132,11 @@ const draw = new Vue({ if (textarea.value !== '') { drawTextHandler(textarea.value); document.getElementById("drawLine").click() - options.text = textarea.value; - localDrawCallback && localDrawCallback(options); + + localDrawCallback && localDrawCallback({ event, drawMode, fromRemote : true, remote : { + lineWidth, curPoint, lineCap, width, height, devicePixelRatio, text : textarea.value, + lineJoin, strokeStyle, fillStyle, + }}); } }) } diff --git a/svr/res/js/index.js b/svr/res/js/index.js index 00ca647..1474e1b 100644 --- a/svr/res/js/index.js +++ b/svr/res/js/index.js @@ -204,8 +204,102 @@ axios.get("/api/comm/initData?turn="+useTurn, {}).then((initData) => { } }, methods: { + updateRemoteRtcState : async function(){ + for(let id in this.remoteMap){ + let stat = await window.tlrtcfile.getWebrtcStats( + this.getOrCreateRtcConnect(id) + ); + let remoteCandidate = stat.get("remote-candidate") || []; + let p2pModes = remoteCandidate.map(item => { + if(['host','srflx','prflx'].includes(item.report.candidateType)){ + return "直连" + }else if(item.report.candidateType === 'relay'){ + return "中继" + }else{ + return "未知" + } + }) + this.setRemoteInfo(id, { + //p2p连接模式: host, srflx, prflx, relay + p2pMode : Array.from(new Set(p2pModes)).join(",") + }) + } + this.$forceUpdate() + }, + showRemoteUser : async function(remote){ + let stat = await window.tlrtcfile.getWebrtcStats(this.getOrCreateRtcConnect(remote.id)); + const rtcStatList = []; + stat.forEach((value, key)=>{ + rtcStatList.push( + ...value.map(item => { + return Object.assign(item.report, { + description_zh: item.description + }) + }) + ) + }) + let rtcStatDomList = ''; + rtcStatList.forEach(statItem => { + let rtcStatDomVal = `
description_zh: ${statItem.description_zh}
`; + for(let key in statItem){ + if(key === 'description_zh'){ + continue + } + rtcStatDomVal += `
${key}: ${statItem[key]}
`; + } + rtcStatDomList += `
${rtcStatDomVal}
`; + }) + + let that = this; + layer.closeAll(function () { + layer.open({ + type: 1, + closeBtn: 0, + fixed: true, + maxmin: false, + shadeClose: true, + area: ['350px', '380px'], + title: `rtc连接实时统计信息`, + success: function (layero, index) { + document.querySelector(".layui-layer-title").style.borderTopRightRadius = "8px"; + document.querySelector(".layui-layer-title").style.borderTopLeftRadius = "8px"; + document.querySelector(".layui-layer").style.borderRadius = "8px"; + document.querySelector(".layui-layer").style.background = "#f8f8f8"; + + carousel.render({ + elem: '#tl-rtc-file-rtcinfo', + width: '100%', + autoplay : false, + indicator: 'outside' + }); + }, + content: ` + + ` + }) + }) + }, + iceOk : function(state){ + return ['completed', 'connected', 'checking', 'new'].includes(state); + }, consoleLogo : function(){ - window.console.log(`%c____ TL-RTC-FILE-V10.1.5 ____ \n____ FORK ME IN GITHUB ____ \n____ https://github.com/tl-open-source/tl-rtc-file ____`, this.logo) + window.console.log(`%c____ TL-RTC-FILE-V${this.version} ____ \n____ FORK ME IN GITHUB ____ \n____ https://github.com/tl-open-source/tl-rtc-file ____`, this.logo) }, changeLanguage: function () { let that = this; @@ -291,9 +385,7 @@ axios.get("/api/comm/initData?turn="+useTurn, {}).then((initData) => { let file = filterFile[0] if (file.size > this.uploadCodeFileMaxSize) { - if(window.layer){ - layer.msg(`${this.lang.max_saved} ${this.uploadCodeFileMaxSize / 1024 / 1024} ${this.lang.mb_file}`); - } + layer.msg(`${this.lang.max_saved} ${this.uploadCodeFileMaxSize / 1024 / 1024} ${this.lang.mb_file}`); return } @@ -363,7 +455,8 @@ axios.get("/api/comm/initData?turn="+useTurn, {}).then((initData) => { this.initSendFile(recoder); }, // 私聊弹窗 - startChatRoomSingle: function(remote){ + startChatRoomSingle: function(event, remote){ + event.stopPropagation(); this.chatRoomSingleSocketId = remote.id; let that = this; let options = { @@ -433,7 +526,7 @@ axios.get("/api/comm/initData?turn="+useTurn, {}).then((initData) => {
- shift+enter ${this.lang.enter_send} + shift+enter | ${this.lang.enter_send}
` @@ -626,7 +719,7 @@ axios.get("/api/comm/initData?turn="+useTurn, {}).then((initData) => { let fileRecorde = filterFile[0]; if (fileRecorde.size > this.previewFileMaxSize) { - layer.msg(`${this.lang.max_previewed} ${this.previewFileMaxSize / 1024 / 1024} ${mb_file}`); + layer.msg(`${this.lang.max_previewed} ${this.previewFileMaxSize / 1024 / 1024} ${this.lang.mb_file}`); return } @@ -853,7 +946,7 @@ axios.get("/api/comm/initData?turn="+useTurn, {}).then((initData) => {
- shift+enter ${this.lang.enter_send} + shift+enter | ${this.lang.enter_send}
` @@ -1007,11 +1100,11 @@ axios.get("/api/comm/initData?turn="+useTurn, {}).then((initData) => { layer.prompt({ formType: 1, - title: this.lang.please_enter_password + title: that.lang.please_enter_password }, function (value, index, elem) { that.createPasswordRoom(value); layer.close(index); - that.addUserLogs(this.lang.enter_password_room + that.roomId + `,${this.lang.password}:` + value); + that.addUserLogs(that.lang.enter_password_room + that.roomId + `,${that.lang.password}:` + value); }); }); } @@ -1124,12 +1217,12 @@ axios.get("/api/comm/initData?turn="+useTurn, {}).then((initData) => {
-

${this.lang.relay_server_current} ${useTurn ? this.lang.on : this.lang.off}

-

${this.lang.relay_server_current_detail}

-
+

${this.lang.relay_server_current} '${useTurn ? this.lang.on : this.lang.off}'

+

${this.lang.relay_server_current_detail}

+
- - + +
@@ -1180,11 +1273,11 @@ axios.get("/api/comm/initData?turn="+useTurn, {}).then((initData) => { }); that.clickMediaVideo(); that.isVideoShare = !that.isVideoShare; - that.addUserLogs(this.lang.start_video_call); + that.addUserLogs(that.lang.start_video_call); }else{ layer.prompt({ formType: 1, - title: this.lang.please_enter_video_call_room_num + title: that.lang.please_enter_video_call_room_num }, function (value, index, elem) { that.roomId = value; that.createMediaRoom("video"); @@ -1197,7 +1290,7 @@ axios.get("/api/comm/initData?turn="+useTurn, {}).then((initData) => { }); that.clickMediaVideo(); that.isVideoShare = !that.isVideoShare; - that.addUserLogs(this.lang.start_video_call); + that.addUserLogs(that.lang.start_video_call); }); } }, @@ -1239,7 +1332,7 @@ axios.get("/api/comm/initData?turn="+useTurn, {}).then((initData) => { }); that.clickMediaScreen(); that.isScreenShare = !that.isScreenShare; - that.addUserLogs(this.lang.start_screen_sharing); + that.addUserLogs(that.lang.start_screen_sharing); }else{ layer.prompt({ formType: 1, @@ -1256,7 +1349,7 @@ axios.get("/api/comm/initData?turn="+useTurn, {}).then((initData) => { }); that.clickMediaScreen(); that.isScreenShare = !that.isScreenShare; - that.addUserLogs(this.lang.this.lang.start_screen_sharing); + that.addUserLogs(that.lang.start_screen_sharing); }); } }, @@ -1289,9 +1382,25 @@ axios.get("/api/comm/initData?turn="+useTurn, {}).then((initData) => { return } let that = this; - if (window.layer) { - if(that.isShareJoin){ //分享进入 + if(that.isShareJoin){ //分享进入 + that.createMediaRoom("live"); + that.socket.emit('message', { + emitType: "startLiveShare", + room: that.roomId, + to : that.socketId + }); + that.clickMediaLive(); + that.isLiveShare = !that.isLiveShare; + that.addUserLogs(that.lang.start_live); + }else{ + layer.prompt({ + formType: 1, + title: this.lang.please_enter_live_room_num, + }, function (value, index, elem) { + that.roomId = value; that.createMediaRoom("live"); + layer.close(index) + that.socket.emit('message', { emitType: "startLiveShare", room: that.roomId, @@ -1299,30 +1408,13 @@ axios.get("/api/comm/initData?turn="+useTurn, {}).then((initData) => { }); that.clickMediaLive(); that.isLiveShare = !that.isLiveShare; - that.addUserLogs(this.lang.start_live); - }else{ - layer.prompt({ - formType: 1, - title: this.lang.please_enter_live_room_num, - }, function (value, index, elem) { - that.roomId = value; - that.createMediaRoom("live"); - layer.close(index) - - that.socket.emit('message', { - emitType: "startLiveShare", - room: that.roomId, - to : that.socketId - }); - that.clickMediaLive(); - that.isLiveShare = !that.isLiveShare; - that.addUserLogs(this.lang.start_live); - }); - } + that.addUserLogs(that.lang.start_live); + }); } }, // 打开画笔 openRemoteDraw : function(){ + let that = this; if (!this.switchData.openRemoteDraw) { layer.msg(this.lang.feature_close) this.addUserLogs(this.lang.feature_close) @@ -1338,26 +1430,26 @@ axios.get("/api/comm/initData?turn="+useTurn, {}).then((initData) => { // 触发draw.js中的方法 window.Bus.$emit("openDraw", { openCallback: () => { - this.socket.emit('message', { + that.socket.emit('message', { emitType: "startRemoteDraw", - room: this.roomId, - to: this.socketId + room: that.roomId, + to: that.socketId }); }, closeCallback: (drawCount) => { - this.socket.emit('message', { + that.socket.emit('message', { emitType: "stopRemoteDraw", - room: this.roomId, - to: this.socketId, + room: that.roomId, + to: that.socketId, drawCount : drawCount }); }, localDrawCallback : (data) => { - Object.entries(this.remoteMap).forEach(([id, remote]) => { + Object.entries(that.remoteMap).forEach(([id, remote]) => { if(remote && remote.sendDataChannel){ const sendDataChannel = remote.sendDataChannel; if (!sendDataChannel || sendDataChannel.readyState !== 'open') { - this.addSysLogs("sendDataChannel error in draw") + that.addSysLogs("sendDataChannel error in draw") return; } sendDataChannel.send(JSON.stringify(data)); @@ -1387,15 +1479,15 @@ axios.get("/api/comm/initData?turn="+useTurn, {}).then((initData) => { openCallback : () => { that.socket.emit('message', { emitType: "startScreen", - room: this.roomId, - to : this.socketId + room: that.roomId, + to : that.socketId }); - that.addUserLogs(this.lang.start_local_screen_recording); + that.addUserLogs(that.lang.start_local_screen_recording); }, closeCallback : (res) => { - this.receiveFileRecoderList.push({ - id: this.lang.web_screen_recording, - nickName : this.nickName, + that.receiveFileRecoderList.push({ + id: that.lang.web_screen_recording, + nickName : that.nickName, href: res.src, style: 'color: #ff5722;text-decoration: underline;', name: 'screen-recording-' + res.donwId + '.mp4', @@ -1406,14 +1498,14 @@ axios.get("/api/comm/initData?turn="+useTurn, {}).then((initData) => { start: 0, cost: res.times }) - this.socket.emit('message', { + that.socket.emit('message', { emitType: "stopScreen", - to : this.socketId, - room: this.roomId, + to : that.socketId, + room: that.roomId, size: res.size, cost: res.times }); - this.addUserLogs(this.lang.end_local_screen_recording); + that.addUserLogs(that.lang.end_local_screen_recording); } }); }, @@ -1461,7 +1553,7 @@ axios.get("/api/comm/initData?turn="+useTurn, {}).then((initData) => {
- shift+enter ${this.lang.enter_send} + shift+enter | ${this.lang.enter_send}
` @@ -1617,7 +1709,7 @@ axios.get("/api/comm/initData?turn="+useTurn, {}).then((initData) => {
- shift+enter ${this.lang.enter_send} + shift+enter | ${this.lang.enter_send}
` @@ -2118,7 +2210,9 @@ axios.get("/api/comm/initData?turn="+useTurn, {}).then((initData) => { type : 'password', password : '', nickName : this.nickName, - langMode : this.langMode + langMode : this.langMode, + ua: this.isMobile ? 'mobile' : 'pc', + network : this.network }); this.isJoined = true; this.addPopup({ @@ -2147,7 +2241,9 @@ axios.get("/api/comm/initData?turn="+useTurn, {}).then((initData) => { room: this.roomId, type: type, nickName : this.nickName, - langMode : this.langMode + langMode : this.langMode, + ua: this.isMobile ? 'mobile' : 'pc', + network : this.network }); this.isJoined = true; this.roomType = type; @@ -2183,7 +2279,9 @@ axios.get("/api/comm/initData?turn="+useTurn, {}).then((initData) => { type : 'password', password: password, nickName : this.nickName, - langMode : this.langMode + langMode : this.langMode, + ua: this.isMobile ? 'mobile' : 'pc', + network : this.network }); this.isJoined = true; this.addPopup({ @@ -2225,12 +2323,19 @@ axios.get("/api/comm/initData?turn="+useTurn, {}).then((initData) => { rtcConnect.oniceconnectionstatechange = (e) => { that.addSysLogs("iceConnectionState: " + rtcConnect.iceConnectionState); + that.setRemoteInfo(id, { + iceConnectionState : rtcConnect.iceConnectionState + }) } //保存peer连接 this.rtcConns[id] = rtcConnect; if (!this.remoteMap[id]) { - Vue.set(this.remoteMap, id, { id: id, receiveChatRoomSingleList : [] }) + Vue.set(this.remoteMap, id, { + id: id, + receiveChatRoomSingleList : [], + p2pMode : '识别中...' + }) } //数据通道 @@ -2467,11 +2572,20 @@ axios.get("/api/comm/initData?turn="+useTurn, {}).then((initData) => { }) } - // 缓冲区満了 + //缓冲区暂定 256kb + sendFileDataChannel.bufferedAmountLowThreshold = 16 * 1024 * 16; + //局域网一般不会走缓冲区,所以bufferedAmount一般为0,公网部分情况受限于带宽,bufferedAmount可能会逐渐堆积,从而进行排队 if (sendFileDataChannel.bufferedAmount > sendFileDataChannel.bufferedAmountLowThreshold) { - this.addSysLogs(this.lang.file_send_channel_buffer_full) + this.addSysLogs( + that.lang.file_send_channel_buffer_full + ",bufferedAmount=" + + sendFileDataChannel.bufferedAmount + ",bufferedAmountLowThreshold=" + + sendFileDataChannel.bufferedAmountLowThreshold + ) sendFileDataChannel.onbufferedamountlow = () => { - this.addSysLogs(this.lang.file_send_channel_buffer_recover) + that.addSysLogs( + that.lang.file_send_channel_buffer_recover + ",bufferedAmount=" + + sendFileDataChannel.bufferedAmount + ) sendFileDataChannel.onbufferedamountlow = null; that.sendFileToRemoteByLoop(event); } @@ -2844,15 +2958,17 @@ axios.get("/api/comm/initData?turn="+useTurn, {}).then((initData) => { for (let i = 0; i < data.peers.length; i++) { let otherSocketId = data.peers[i].id; - let otherSocketIdNickName = data.peers[i].nickName; - let otherSocketIdLangMode = data.peers[i].langMode; - let otherSocketIdOwner = data.peers[i].owner; let rtcConnect = that.getOrCreateRtcConnect(otherSocketId); // 处理完连接后,更新下昵称 that.setRemoteInfo(otherSocketId, { - nickName : otherSocketIdNickName, - langMode : otherSocketIdLangMode, - owner : otherSocketIdOwner + nickName : data.peers[i].nickName, + langMode : data.peers[i].langMode, + owner : data.peers[i].owner, + ua : data.peers[i].ua, + joinTime : data.peers[i].joinTime, + userAgent : data.peers[i].userAgent, + ip : data.peers[i].ip, + network : data.peers[i].network, }) await new Promise(resolve => { @@ -2895,7 +3011,11 @@ axios.get("/api/comm/initData?turn="+useTurn, {}).then((initData) => { nickName : data.nickName, owner : data.owner, langMode : data.langMode, - owner : false + ua : data.ua, + network : data.network, + joinTime : data.joinTime, + userAgent : data.userAgent, + ip : data.ip, }) // 处理音视频逻辑 if (data.type === 'screen') { @@ -3656,10 +3776,14 @@ axios.get("/api/comm/initData?turn="+useTurn, {}).then((initData) => { this.addSysLogs(this.lang.basic_data_get_done); this.addSysLogs(this.lang.window_event_init); - window.onresize = this.touchResize; + window.onresize = this.touchResize; setInterval(() => { this.touchResize() }, 1000); + + setInterval(async () => { + await this.updateRemoteRtcState() + }, 5000); this.addSysLogs(this.lang.window_event_init_done); this.addSysLogs(this.lang.message_box_init); diff --git a/svr/res/js/language.js b/svr/res/js/language.js index 6e19fbd..9310c6c 100644 --- a/svr/res/js/language.js +++ b/svr/res/js/language.js @@ -314,8 +314,28 @@ const local_lang = { "you_refresh_room": "You refreshed the room number, the current room number is", "your_browser": "Your browser", "your_ip_list": "Your IP list is", + "nickname" : "Nickname", + "userid" : "Userd", + "room_channel" : "Room channel", + "join_time" : "Join time", + "website_language" : "Website language", + "terminal_equipment" : "Terminal equipment", + "device_classification" : "Device classification", + "network_status" : "Network status", + "public_ip" : "Public IP", + "webrtc_ice_state" : "webrtc state" }, "zh": { + "webrtc_ice_state" : "webrtc状态", + "nickname" : "用户昵称", + "userid" : "用户ID ", + "room_channel" : "房间频道", + "join_time" : "加入时间", + "website_language" : "网站语言", + "terminal_equipment" : "终端设备", + "device_classification" : "设备分类", + "network_status" : "网络状态", + "public_ip" : "传输地址", "add_ice_candidate_failed": "addIceCandidate失败", "add_ice_candidate_success": "addIceCandidateSuccess成功", "ai_answering": "AI正在回答您的问题,请稍后再问", @@ -445,9 +465,9 @@ const local_lang = { "not_support": "不支持", "note_website_for_learing": "注意:示例网站仅用于学习演示,请勿他用", "notice": "通知", - "off": "已关闭", + "off": "关闭", "offer_failed": "offer失败", - "on": "已开启", + "on": "开启", "online": "当前在线人数", "online_number": "人在线", "only_show" : "仅展示 ", @@ -553,7 +573,7 @@ const local_lang = { "select_wait_send_record": "选择待发送记录中", "selected_file": "已选择文件", "selected_file_exist": "选择的文件已经存在相同的文件,不再重复添加", - "self": "我自己", + "self": "自己", "send": "发送", "send_all": "一键发送", "send_alone": "单独发送", diff --git a/svr/res/pay.html b/svr/res/pay.html index e338466..e21e8da 100644 --- a/svr/res/pay.html +++ b/svr/res/pay.html @@ -63,7 +63,7 @@

收费模式

    -
  • 按小时计费:根据实际工作时间计算费用,目前个人定制功能2小时起,每小时200~300,企业定制另谈
  • +
  • 按小时计费:根据实际工作时间计算费用,目前个人定制功能2小时起,每小时200~300
  • 阶段性付费:确认开发前,预付1/3,开发完成付1/3,交付上线1/3
diff --git a/svr/src/socket/rtcCreateJoin/createJoin.js b/svr/src/socket/rtcCreateJoin/createJoin.js index 407b0ac..3d70176 100644 --- a/svr/src/socket/rtcCreateJoin/createJoin.js +++ b/svr/src/socket/rtcCreateJoin/createJoin.js @@ -19,7 +19,10 @@ const check = require("./../../utils/check/content"); async function userCreateAndJoin(io, socket, tables, dbClient, data){ let {handshake, userAgent, ip} = utils.getSocketClientInfo(socket); - let {room, type, nickName, password = '', langMode = 'zh'} = data; + let { + room, type = 'file', nickName = '', password = '', + langMode = 'zh', ua = '', network = '' + } = data; if (room && room.length > 15) { room = room.toString().substr(0, 14); @@ -28,14 +31,37 @@ async function userCreateAndJoin(io, socket, tables, dbClient, data){ if(nickName && nickName.length > 20){ nickName = nickName.substr(0, 20); } - //设置昵称 - io.sockets.connected[socket.id].nickName = nickName; - io.sockets.connected[socket.id].langMode = langMode; + + if(['zh', 'en'].indexOf(langMode) === -1){ + langMode = 'zh' + } + + if(['file', 'screen', 'video', 'password', 'live'].indexOf(type) === -1){ + type = 'file' + } + + if(['pc', 'mobile'].indexOf(ua) === -1){ + ua = 'pc'; + } + + if(['wifi', '4g', '3g', '2g', '5g'].indexOf(network) === -1){ + network = '2g'; + } if(password && password.length > 6){ password = password.toString().substr(0,6); } + //设置连接信息 + io.sockets.connected[socket.id].nickName = nickName; + io.sockets.connected[socket.id].langMode = langMode; + io.sockets.connected[socket.id].ua = ua; + io.sockets.connected[socket.id].network = network; + io.sockets.connected[socket.id].ip = ip; + const joinTime = utils.formateDateTime(new Date(), "yyyy-MM-dd hh:mm:ss") + io.sockets.connected[socket.id].joinTime = joinTime; + io.sockets.connected[socket.id].userAgent = userAgent; + let recoderId = await daoRoom.createJoinRoom({ uid: "1", uname: nickName, @@ -105,6 +131,11 @@ async function userCreateAndJoin(io, socket, tables, dbClient, data){ type: type, recoderId : recoderId, langMode : langMode, + ua : ua, + network : network, + joinTime : joinTime, + ip : ip, + userAgent : userAgent }); let peers = new Array(); @@ -114,11 +145,22 @@ async function userCreateAndJoin(io, socket, tables, dbClient, data){ let peerNickName = io.sockets.connected[otherSocketId].nickName let peerOwner = io.sockets.connected[otherSocketId].owner let peerLangMode = io.sockets.connected[otherSocketId].langMode + let peerUa = io.sockets.connected[otherSocketId].ua + let peerNetwork = io.sockets.connected[otherSocketId].network + let peerJoinTime = io.sockets.connected[otherSocketId].joinTime + let peerIp = io.sockets.connected[otherSocketId].ip + let peerUserAgent = io.sockets.connected[otherSocketId].userAgent + peers.push({ id: otherSocketId, nickName: peerNickName, owner : peerOwner, - langMode : peerLangMode + langMode : peerLangMode, + ua : peerUa, + network : peerNetwork, + joinTime : peerJoinTime, + ip : peerIp, + userAgent : peerUserAgent }); } diff --git a/svr/static/layui/font-ext/demo_index.html b/svr/static/layui/font-ext/demo_index.html index 918da38..3864f52 100644 --- a/svr/static/layui/font-ext/demo_index.html +++ b/svr/static/layui/font-ext/demo_index.html @@ -54,6 +54,30 @@
    +
  • + +
    数据汇总
    +
    &#xe61d;
    +
  • + +
  • + +
    控桩终端
    +
    &#xe6bb;
    +
  • + +
  • + +
    浏览器
    +
    &#xe6e8;
    +
  • + +
  • + +
    连接流
    +
    &#xec57;
    +
  • +
  • 三角形
    @@ -534,9 +558,9 @@
    @font-face {
       font-family: 'iconfont';
    -  src: url('iconfont.woff2?t=1688291642630') format('woff2'),
    -       url('iconfont.woff?t=1688291642630') format('woff'),
    -       url('iconfont.ttf?t=1688291642630') format('truetype');
    +  src: url('iconfont.woff2?t=1688799262496') format('woff2'),
    +       url('iconfont.woff?t=1688799262496') format('woff'),
    +       url('iconfont.ttf?t=1688799262496') format('truetype');
     }
     

    第二步:定义使用 iconfont 的样式

    @@ -562,6 +586,42 @@
      +
    • + +
      + 数据汇总 +
      +
      .icon-rtc-file-daohang-shujufenxi +
      +
    • + +
    • + +
      + 控桩终端 +
      +
      .icon-rtc-file-kongzhuangzhongduan +
      +
    • + +
    • + +
      + 浏览器 +
      +
      .icon-rtc-file-liulanqi +
      +
    • + +
    • + +
      + 连接流 +
      +
      .icon-rtc-file-lianjieliu +
      +
    • +
    • @@ -1282,6 +1342,38 @@
        +
      • + +
        数据汇总
        +
        #icon-rtc-file-daohang-shujufenxi
        +
      • + +
      • + +
        控桩终端
        +
        #icon-rtc-file-kongzhuangzhongduan
        +
      • + +
      • + +
        浏览器
        +
        #icon-rtc-file-liulanqi
        +
      • + +
      • + +
        连接流
        +
        #icon-rtc-file-lianjieliu
        +
      • +
      • lu+!TJeyo(&TXWbSyE23wEiLnF2DC?B>u zo*i`)v~fa)zgLTQC*nQ5vYoZS$|fS8IdI&Y*fMGF_Pqj zX(kj!R=1(ePY9-}SZap1DcfM}^VHOBBauyi#?vQb=znYUq*6U8Unzk0=xX}(v|g#~ z>F(a6r?srU#{?Y$6T^}PUe+YqF`bhv3CY1afV+f%NAKWG(F7~na6Dd?bmo}i8u|(vFq}Y!8SEF^2g4Z+H$1C< zu0&7lc{<_z7K3ZR!;f<y%}^X#p&hdiZ0W3aj4BvMtZceaoXRP{ z%|;9Cqd<7Kn;JXCqix%9^YRCdPhm@cHiC7)-vL>#}VGDfRx<0GKA>qp@^*qbpP99Y*df>VncL-pVcy9N!P*dLORVj#7EbuYA zcQw>qX5e8aSc4bJ(^I%}fmgkMn|mL~(Z{$iEQE}GyzT<%e!}&5cHYS!1q{G4kD>v2 zv<`w3%SEqTY78z&1CnX&rxBSe@3_r0Y_gDj0o!=fC>DyMU#H%X2okdns8RC6y*i~L z_Sd}ky&K#l>p5H9;50wD{^B2e=cWJk1e$5ipx);9mil#|zUJS}{RYr~?&c%S^X#@> zuif`0(=J$^o+MLvCFVUaL;%Rtl3f+!8&h6Kdp0fz>O@orx{`=0s-j_AakoWBz> zG+C}Vc3c+)Inv1Z0+K}85J&~_8voUZWp6?#7C}@%2B0ZIgdW|0b`hE4oIsLw04A`_ zb;qbphz9dGJ{6cI+VBTmGUSu}-zStdr{m>v9B+%4h2}+>3n{&SFtZ)4z9_SOd*&ii zxd-omnV?c)mP?dM2|SJU=2@AGL|eQlb9VH?G%Mtw-FM#|EQNjJ^*5g31#&59fe|hW zx*X6eJB_oo>bpJQ$99$cx_L=#YAW8mG)71IN&CPEZKfvS-?{ktu1uzDcBHbtt82YB zF@tUh+kNAWZwKdpAV$F=-wZvZee8rfpKYCCnl|eZ{78YFF&69|a~dr#8Cy_FNjM7{ z9F~ZJwa%fFtISk&0dN{u*W_|N+r_i>v+4FkB5}69U5BT$w;tTJ^WJjD#PYj!)$3S3 zw)VYu$vuibv-NPH&{4j3=Pr~fB_`f`>iVlox}fl)Qca_OAEdJ~DpgMtFOKJ&o^tY|yGrHet#Q3z zJI=N-SF!C}Iwf4E4En)YLw`x0B`W~dT>H_JW9ByqkZ1e14$S`Bz}9}a;}qPlSP+MM zcwW(8k#CTHm7L3QPH;rtV0bp`+}W+;W25^o_}vBjN5|$PXy&>_&nyz|7lq(D{2F|Z z&(4390nGr%g7ZVe!(~vJ0F;P<<_MY}CCY5W>-PB6WIUHZmn8oF)$*$k zMMp-WYuS>T#H;AnNFPZw?@z=NlaqIxsnJ%}Dn+aC5{25sEf5ZJf_e&VHKGg4J(clnZP;IC^;FR@wyaD#+&+QHL zOJ%r9;WWcQi}wXgO{7s8teC9NBNgedHU`6E0~>4yQ7Jf75C$_1LqVc`DL_Sb7@VyY;t8s`R*ON)&sA9iR7$|l66f3kyXQkU59pKq!Ni{he%A% zMz9@wv>uN)cSC?4$ydJDRj9|&vKv>gTfJg={6^GcD;X;nAyUl7$#tGZrOLU;;0+y1 zD#E)DtsHBV+kH3MK-YBpQRMl*>>3Ogb+f*a5d~!HjaD3i(yEfx|)b7j65q&TY1`{X=1dUdvraHS; ztmsZyRttOvc0eXP<~>GEsc05lhC5Ojl;kCVvNy$n_BSz`j|@oe}JRy1R{g zn0uW2GWRt168B^7_eeqsltgWavFZj|g-%1;(3xmAIuBilE<#tLs~8JL`G8U67bjwQF!`nxMrW}$xNYUghT>9atv<~qV@UpLgAsJ#rg9_gR?08C_BwE$c3g@o*Wfw%6@v2PDCF%yQiIVEWuFj#HVRe=rl^#o_VXm25F>%S;5dXGI zCRVUm^K}+OukH`TMIckYJr~`6&c5p}Iyg2lF?R5x*XAP4hj;;LU|J`CDjms`tnV8+ zh3uHzVS*kL&9rrnC5kcZTxK@njw})B(3y5D8o|qWL3eU($x258s+>QvL6c;#o)V*# zVm7K;vI#Bh<|ENqMvBE!VceDD~xkEvvkhA9!#eZ}W=U z{3bPdS&S5`oB7SPwg@AC^v|vC&YFlR^JqD!!`RApufgvWWF!pl-8~G(3F-e-pU5AZ za7N5<*$G;=x9-jT)pb?4@TA|-*XjG6eI0&tTdE@k7djZ;oBty5R1hd{weD!<*o13t znj7h(V?KN5;&Gmfl4ll;bP#z3iNgBjdqp9WE_cO?Ni(fFsm?@yA(m*1#bYae&U?4)c)07#b!$2*=WThg`MWiL*PSL}r*?WzIx&>r?4dtw ztF6elufJh~E#+?>dX61i@*F$Ja*0^;$>H)FL)jm!G+V=FFS?Fz<xVToHi?&GxSQkDSIEYff*j2;I{gBwd1Qy`)pBOQ?PJ zn;nft2OjNz7#w3a-PLxJUbqU$b7JNtrV;lZy6rpGfBuuNvmJ+GTv7d69IawzZEE^6sa;#eyg5Y$;^H_ zQ}NPXX+;^IUS3gxDEfw>D%zLP?+ni~n%b9Dq^N22H|)GxiuvEF%~!{O3NmA+=b2+r z1*)=tc!2l6B(o&SnOrtF5o#^l2P?eU;m^k%?93Llv%Uo?wb44rS%h+B(i_Y!^x}HB z0myfv1}2|={p0MIS+X6%*KeVULdkX(&VnvmKeb*jX_n>N_7_x(iqfwVtNeG$$Vtlg zik|puiF`yh`OorE=_jIX>pU<1)GT-8<GPtt_Ewg{Qxv0$9Jc_x=$IsHG1y|FGUB@|zGBlnGUXxM~;!*Scf-ImCUrhNZI5o7(dgTwH>g4bqe zlEtEe-1h#YXsKB~)p5D4pQ*W{&8ux1b7Jh1dnADN|H~2#WZN@ z@zg*YeeOfTpV_h1XBFOy=6oN2GdxFTz!HsfX{Phqc{`6n(&|Ei$B<2&GlD}!uWA?V zD$l(RHFU?(e{+=_m#_Ps<{(E$k1+oOnSs2!9DVk^x^uOJrE6^sU#dIJ+$6g5jk*24 z58#)B|No(bThFxgyv??{lA#vIVVfkG#}oK65bjuOFG?D<4;3jpWu~=%Hntd^aKl!{ zF|)ftCTC~x_g{aRRK6cC#^LIkDJ);TyjX;w(0utj`7K&R_Y@(Lh#AwPy{eiUZW9uN zIn`4wJktbrIX$}#%~T@sc;qn_Oive=7YfUZkA=6&^z_#@Yep>Y2uf7%TSswrXli+f zi1C@n_iO8)(y!JL{Z&YR$Y3Lw+dA6Gxed+bar7oV3~tj#ED;eDPw(oQ?`%-Wwk-QC zb+Z!Q^{65^@tCn~uE$}r-yMl`AK%}>Kc&BHYR9euE}gtXGt<+LZF@_f{MciUy}><( z9y|WK#{Y!x62kKX)2JGp(#uwV=ZA_X|Hdz5r0y-J*QOy|c?5BP_r0ah?a57_lJ!@2 zRalXN`jzSFfOpT)VT|kHR)DP;QevPk9HT;@rL>2DLDzFE(PG)TAQ9A!V3~SpoE?IL z>VpCCer;^|`3HV;;QZxd^O5GN%XaT<3=cPU?*5m#$m|!UCNI7W@t0jZImKel*I5kz zhiNunGArieFj$>`=55F{D~#+hckQAUSp?-5H5f#i`)^v@VDOSYvx~KAK5lkqj9a12 z{|8R!OIZMToMT{QU|;~^pJEIR@%%Pl8Ms*(K;Xf#pSxi6|3ClhS+bcMfm{v-CXgrq zbBPXU0001ZoMT{QU|??e-@p*VlJ)-qA_Lw0&;P$H*$hAt6yOH{f3nsGgLs@{VPIfj zVfg=t1x(|@|NrBcrxKQ+q1j9zKjQI67HQ@I0FV#~uK)l5002G!d;rJ+Q~_cEb^(q7 zt^$+>fCmf*k_W&DPzcNkJ_-s7ehY*Q2n{F=<_>@k{tyfhd=U^4<`SqA2os(ZeiZN( z!WM=XFc?r702$UAR;C+}9CjW49_AnLAgCc+B4#64Bvd5=CR`^jC+aA&DZnZQDl#gz zD%dLiD?%)$EX*$uFR(A@Ff1^PF(fiRGIlcJGg>pIGy*h?004NLV_;-pVDMv;!9x=R z8v;E5g=Ximac!k$^ zgSU8x_xOO1SjQ)PMh6?{ViRA`!&iL6cWhxBKWcM1O^1@ILZ*DFkTm7FsMsHCqhi%- z9xLq?UHFlVV?VRMgK#;psg-Z>*cD52Y+Lv@%oD7jT&2zR-c-nxrJVHpzRRT+W>DWK z0v0=6e;G4pM~y=3`c+R%E(0$RHncEqE~G6wAIoY^HeuDYsjE$9MWdv-Q3I-46DDo- zBQM#M5!omh>4%Y{3*9C={hwYg%+Tx^hx)4>uT-XrY-NAvHA16GQ_|Gg>2-S~Q~?=g z+z~p78!}Mo?pEI?nOz`^W0GWRBnu7eBUz@29gFlk-Q|$VgdX+ZwdZ7hphD&i?d3RCo=_>T_AAXHO#X-k+709= zh*|Rh#7qXXSxx6_mg~ALHI-y4OtUm;WF}@`xX*kpmMEde$HBm;s-3xOs81Rw>3X9tF18}ejz#+=6iz*)CSMD?m6 z`~N>CH)IIbK-}t`BsbC%?DJp>lU_ER&4!;5sZP0}2zl@eoUMV;{57Tg@1ah|3K^!* z1AR!AADB@ReHTjF3jK6x8jtu>QG9a=YNfM|ICUy z>MJ6N(D1~e6cVA3Nt8eenXN7?f8~@_jd*W5BazE?-EEXvVhuxEWnl|ag z+<$pP-~lg-9TM3HnD<>-N!je~os`azY!!5pf$G8l!VF7t!$QL{_mBPV&%XN;6f_J} zaS$59P-XW3LCSr2c<%S=3!wiSu&&TYuBb&7?F@7_!pH_3@QiWuuhXiP>^Q?vEqS3E zD4GSXsNJU6@`LR7OO|a}$x9@Khz9K(MVTq=BcO^) zri!54^2&1N;M?c0{qStkWEhc)8;jgeT>JmdXu12(b0xQg#>8NlHCx)e<-bQkoBOW% zyC+vqMb`(KY%ykNy}7WPSw5hoMawFq0i!5d;!ULIHbmLLP<>Vv@WuQ4@{hV$Dvh=E zgDzhk^Ww)7(_giW0`*U=^wz-s6&g|FHe=c9JHAuCf%XS)8)%FB4OFIrhg?H_;Sms% z3l=3&kur1CXwYf3{U-dD-}(Q`Q)(}>|8*P#$VHJYgG5S@tYkgc{l!CnTYP8z4?g*s%y2iBd<%>DEch$J$FN~1HFEH;PB^Drkc(WqI6;=r1hBQ4IHyKt#Zt4&*$ox~EU z%uTKkDphKYR%dBtZDVU^?_g?2r#;8ERZi4)EgEZZrJd3QDjk@x7ly6~a9|xlz$$`- z9s~uw2pUca2Ko>z^dmSJK%8I@!NU+jfMJ9PBM1pb5i*P+6c|USFo8fYiNG+0&|n&& z!wljKvj_wB2@~cJ7R)1T=pr0wBwT1FJm??-C`1GYL=0<)91ckk9FbsXA(3!Kv~W&z za6zKrk|aPINrYCC9F{(6gfNJv04yV^u!5Li2}y%%NIG0c%y0wA2xB!m6W}J23Ad0e zSU|GjHj)E(kX*QnQDS;oRbEFJjAakIb z)W8mDfKJj0dt@~nll`zwCZHm}!3lW=HF*WQ`~oS9UX%*Z#Q&KofC!>?D8XeImb|bC z8~}hTaK>k90Ba|^kF{FOusUZD1(DkwRLJ4b>Gn$*{VAH48HJoiJVJz}Wh!X7U1H8H ziNYy40llQ;iHN8WEF)r8K=e$+k`_^A8MerXDj*?Cvc{Myk`EH-gp8)$)NhqzS@z3qMAy2)9c+`w$TpIG8y zm}Rk)+RYBu+qhE3#d(re8`n%O{VlUDQxw;`HeW&>kqC8uX~$62&D%+j-L$KkvXaz< ztZ2KsuIDfZW<;s^rOLb1*zUTG1=%m`=k6@?s;54O)bg?;?ut_EsnbjAR}{^wc-n+Y zez@vm2hE3{FImaGc-sE(D|B+}{rBhAM*Q&W;}0NBw80tMIib~vPDCP1aslfS2dE#Sxu3W;VyK~*GOlsYlIy2E? zw8b&CQNLPrOEv5rul@5rcW)HzrS}T+^o!tCGVaF^&AjxV$B^H>?cVAA-h1o6cHdyX zm23fdj>61MWUu?1x4{wDDQDim{?2=g?0G-(UqUS$oV2Phi0(nXIH_&z6KQWlej!~; zDa%r;OOO|FY9Pn6e`V`QQ<)BFyM8(L3t7VZ{5Spr)a0)#&w_zk{FkM#vQ_k*tJVOo z&3|o}oQm_(>HBFxvu##=csA)&W^B>^W-;S5I@GB=d-Cf5OKv0;r^)<4s)@`Nbw9-< zvNl}_dKQse>0CJe;~0~KU4GrmaYb11!VByf$5>oE>Z?l$AvU+2==~8qm!rtR&Hk9H(HLG>oOOz8c`MV*q?(VtuQ!Cun&D{jo25ta( ze>$$LJ2%*tY1CT%Q>#mr)juPO95Sm3Ubi?lEq*k(t~GcsqA2kQw7(l#p?SBjwyibE za#1kKQZY$jakb#*D8xy&4cVMTMz!6x%Tz%s%`tmlNobljZO2xEWUP`zS}KQoX9&5y zUJylI82a7rkRQGcz35#O^xpK`?(6PwMBh=F3H-~MJ4YEF2JSp&UxauYDSHq(cnl14 za{{^Z!J@Zte~$YL-eNHC&WY+=_-jH56Gn3Ly4g=l0ra&DR+(!VK5}|BkV5q6lpu+k4b@mNXAud>zKxFk za+$ATsRlQ$Kp~O@rJwn7L)05FQ3;J6OKHL-zunWC!f(2QxSw8oEIRQsjV2gG^|rS% zR?-VBi{_FRwPQ&G(gukoQk8sKL!rTL9W!;xi6vq-v19p*&m=|W5HJw-FpqdhZwWQk zT+!nYyOIt_5fSWc3{o?-18xJzGg@-$zuDuKOQlb29k_OFXezjc0Uu|nxu=I7Tbpgo!)5otL zZcWF4t7s$%_Jl1=E@M~#)k?t4Dj(K*HZ9sW2RoM*h4!UuaVA|AP=~9)c(!oZ<%srH zZ50-N5-5vE_xI&c&)uT<9rPqOw#4mr(OwHfSnl)Qog6*+ExrVx5C#P^QHW9|t6kWs zQNF3rNrS#En3UR7;4&N9$-v@phm5GctkhWfCQsczin6y43`F)<_q@9=8S+sxDl=O9 z=W_pLn34Y8E4%peD$4ilAOxcshuU87)daiBco&r|6XPAM_8Rjy>)-)ppLi)NL90vJ&=i zcn(<@dQpUZPafBn?RJQ|=>AhGvpY@O2`jAdKw$V8F8GK75%i|rd9A~V%ISzr8P9pp z-@LFk%4=j*Qe07rclUR*5{NpB-%w6;?8cUWHWJ3xqok54ju?ET6f5(~n+ZI(idwB4 zuiHjxZ;!ErPt<^-o)$KlS`Fy6ZL}?HpA_SjLE1=GC$8?i&$`HLt)bG6bI%!_*(FT_ zyVYW6_FVIxJ4H$?PyTw-v#4ikQSczSS-?%q=L3GQz8T#Gp+&4g+>iVG# z;!OLV1i!-z#N0^3siKaLKPxMd;U_{%5T#Vz$p*Gw*)bJ+nB=7Z;8q1m)W=Jy0<)(X z={-D6USa2du95AZb)Afk>#8hSV?ZWOuLBP&Yz)XynZDuL8fI_wSYJ+cR$cgZ@zMNO zb0bI7Cf`L+L!B&qA9c1c0J@)`LG-1ef5JW9J$^SFnde=GzKwp1LJ{q{(_;wskM#ts zy;)OQabJM&=xp-bI}T9adB8B}91c0Czg=5i`G~{bk*V0}*8<`Wd+6Y9cmGQfja)w- z9_)?3W>0rUKe%i^plU#@J`+ z)jC5vuYwn}TB5{@FqcDe_F~1&_Z~?Yga@RvOq_@s!p+k*>O;lv}JK zYfoa}*mwyjj)Q`~+0riCVz7z4KnF9WjV=j=`pBJF7zJ0l!0YIAn!sLib0$xy7 zVeuQVq`O)`ic1~<&JTtX#4vUl+0C=wxr|{YR!79$mijZ{BnI3+QjW#U^%c4BqWkU; z#K4Y3NFGZ*1eud_s6-@=EQsM=)mK0NM0@m8qZHY_N4e5}Uu)`^ugy$4AHnzheS5Vr z*2hW~=k-w_pz6~I6w*c#7y=^oFhqbgfp%-a~LxZ`=GSp_86=%j@`r)lge0YIhQ2D@RBQ>m{B5R^p>AfQrhi{u+TxI zMbfR{hS7`H9hH8K=A3ud)}9Hh)&=)GLuRv{LdmuP5w;jG5L|?-`;r4O=IBH+9Rto* z8eZbaAQ>B=oCMY`W7STkiJ)mF0QtoYW!W@(L*UG@72l1M9c>e*kb~TRM z{e)a~#znB;OOxVx^g_A6SPWG&CB&R z)N|ZzHA){p;+p(@n?Ol#DqFFDjIXp_o{daszIT;xi-+`XSwznxS)jCr3Gk*(E#7;b zJ1Dz(SUvIJG3FRb7}ZQMI=m!0w0P>)wrkB`&auNsvg`~Aj* zaL-r7nZ_F42e1d!ti&>zkrnM4ri$p0aj`9L#9$Qg{phb1!3d0G` zxyL2Dl6((K;!B95*92oeI&XT~ljJ}@bAlYZAhBB?=Kc5F|qk85GqkInkssLqG&)Fy`P_t5Qk1j8(c^&$?G<31U#n{;%rtcrcv9WIEF0vb&gWh!Q24YTNM`|VkP`cc13%}qFqDn7z+Ph z3q15_3~&7Pq*AR>qpo+?(8+N}xxKb*h#Z;cW+2T}Mm&dCqW*H$$C&Bk`SL%e!?zwy z=f({7~U(JJFEQT8-4m4Vw*%1s@alF|YQwNe;}U$sV71f(78 zEg+>T+1nPjLvvlvTq`sRl~o98;;?h&#hn#sJfMvh44?9NQ)GtqIAxBy&`Twwb0$GG zcXUiRey?zc8wgIbI!fY_&2~ZIp`|;%1Y#+YwV_8F)EFzLR7OgBr`8mjz}lhIq18b; zA6|D1GvVR2mox`-i&Wm#+gKIul|h1V*|B|D+jec>O0L!GKfPJ}8EFJ^&*9f_%__C` zfV|(=^OrZDM`Jyo?$o(eg3oM%R8dL19uqm$zPofyQ_b3y7vMf2wp|w zO0_Hb8xv_RV!Yp(nGHC6l~~1-!ugT^Ty`!%dVJ)=Qtr;jFL=)}x9EF=6SN}K=e62T zucw}O{UaPK+vP;E@@!zNb|E%7DG;q6J=D7QG#5YNbvOC5&+6v|1dxOIhSt>89rg#9yr1ScvuJzK2ykGY?duFef* z5k~tKk3Hh>*^vi(=6mnC>(B3|r?a*F=*o^4ywv)Cc=%Y5>B!>k(^*&f7UE2zd-Cbt zE}H@{zru=J7f$E_HBh6q41d94IR2bP6F0Y_%iEtb@}h?;;o{R!;CpGElG!RcTusY# znAVosBRrH*=zVu<8ZNAa-m1uf;ohQjdolFQ$JhHSBMSwl#L# zSaT*e#qKlT)--a?JvxwD@#)p9kC}t2>(8?&N}5DD->xqG!{1uZYdhZ;E2gfad;|0U zUBcINcGs)k$2ZofB7$I=M(A9Mn5|}#Na62p_Dc&-2pT9f_Dp_3e5zX0cLGCw-ivH6 zg!|Q(6BE^@M(_Q%-n-z9`&#(YEd?E~KIo5cWa@PO{6H=>tB?Ft!sI`d&JFuwwT`nl z*RI@%52>P*lR;-cW5(XAucjVe&IMwQ#DciGS;}I``n;HxsR@}jB>C$7-kvv()w0-J ze{tGsX_uRQPAlH!^Vqi;Qz_eDcmJ6S3G{By2qw2^s~YcU;uQ`R5PosV0}%H7YmKbZN@*zNq6 zd#S^0rvtIw!-W6+L;v>QlR*nqf0zJk2|oqUMqR7ptg%tmE!l?{NdRr9sXZNCuFVph7ksK;;T2u3%-v-=$NVQh! zvJzeRm}m-jUQB(y3_(4csGOsREzZrTU9Jk}Sna!R>Qr+xlgpD3z_y7SxjR3v7k=IRVcaI-SBx z*(4;vHnA1HG^^OQZa9`Y%SnK|+w%_e40zokxB!S~_4Zra3k}St%)^8BBD=|W_{ibI z`sjvrYhg*`A~F!6TFUYjIr8Ppc$-?)dI@wThrn*IH;#_{^J>VQ0bEh!^dAd45XyKT)H zv~YL9dXbQ0A2#cBXJ_mjp=f=9a5q}h)_fP4M6TPz(YmIL-XVi0ddJo}2MCm%%_?>k zN&?xpgT^P#-~Zpy`dNX0Yx8zx=D2d)Ez@nowd!DhK%dy4wwcHsW*Dk^Yx-iGaoY8& zrDsI{-R(ILY+e&5-C{i?IXbJoU-YmuAr{GKTA5$)GS#)r)Nb@in$QfCBYikeel{X? zj`f#4;)+AV;^%n{*u>AUF1Pd6cOd><`J=*b)G=m{8hk*T`9bne9zc`-qQ`}1{5AUK zU{}{sb3Xul8H{&z_COA?BjoWf%^+*gWSW5193gE#QrX0bDA5MHA2VP@VnWVAURWd% zTEdVop0SngV9h&dzM8y0M34`p+0K>p5~epIU&vSj5TRO;l;5K|B|H!2EJDU}h97OA)N)eD^NUn$eg&dBl8zI3KOK`2hq}2u&S%PW-sMl#8 zM)Sxs&J-n28buj9Dw#IsxNi?G3<@f&Ns!SRf0m#{F%8RSG$*w6bz`(u<@Swx{LF)S zC&bg@X7QBxL^8cu@=bCAdqOlJY8G7-K?{g)JO1ef^+EN)j;i|TB4I};RG*&TK4kVc z2RL)$4Ms*Iy&uXR3T47avPa&u=ox|p5HbYq+zfxSb$sE4CSx%Uq633<8i&W}Kj22u zSOkqhLtxRP%L>%K>kN$o7)3*wYD87Vb59mQtdy(fe4|d}nzaavI{)IY3|G>Foz7bk zm}9N&NrE->HnALuv}bx@pC$q19n5Qt~Lu%IC= z5mEi-qzTb_?3ZV~G9Yr-vf4hqM}m$o_m?6Hp^;aJqE+f7FP_R=jz9oFzfz+SJh+3g zVjL*oZ5wq5+kti9+Oc-+I0v@l!3wkxFre`-4ZA{ zB(^J?J-eU(CTu+NvFhZNg1s{QAVcrZU>m{|v$Y5mf_uayzW9Uj{%<34QZyQEE40M6 z%9C^Q&>Yx9*7Ibp}T94GP{2G`A>OWumd zmet_7%MwXZC}-Kmt)!nR+u(k}aWX|fCE?-^UD%pIiz1$|IJ5122#8ST1&E4>S)t)J ztsN{VPT8@lHDB=VKVow1)wcLAhZ?ejF4$J@HIaZRbL8f2ZIg8Cnbv?GP$p^fydQ;= zRfH3gxb45!32W<}x!$h_ObWkq3`)~b+XGt9K>x57lKy?4Cs@^*@=GyTptXnFW`$Nn z05^?xsaz|QY2~E~nN};4KM6P~Ff2Z`whrvz>8*5YZFW1Ton7XW@ zRQ?W;W7s@UTMKqdl!Ph5qr6~B5-_TwMkgL&>cYy} z+66sf^=dD{T373+(_cCDKgFM*PZR#kKlw~~xsM1_&J^$V7pcUNR#*4t_|CqI?X0QJ zrK8GF{!1A+pOjX{K1nr8&#D%m?bA!tWinz?`1=t(@YRas@(%@o5DGqiFZo!|l1%$y zWhPm59PPX6H^SeeM4mYyy(aNqJ@ptLC2?DWJHm~|jRLo5zEzi$o<#$csJNp~R;)-I z!XKOKK2Tcv4JAa<*AKTkKf&(tQ`r6OU)#!7=1AOHSBue3=b8R(kBLsbWn%t*;Pcjx zZk!u{w)?T7Kr53l7IxKRJ+OJ%_7%3m8(SwXtd?p5$Nd73 z=MRfgzu61?)P7H%bSx5``kL+V04DmU?2PW{o)<6W>B!HBf!wItrmb6>A}i}JRct#@ zG5`091QT_F8kMt`yk@ZHZ{^QzU-#kh`n;;E+nqcQx&qFNLy<^*FL|cDHaq4oip6$d z4A^#TEY^WFVCL=ccx>Dq`Q_|UJ!fDk876OVq@^UWr58hEglR$oNPNO;0Q3*sY^3y| z|8ad#QB!9W@ATRLw`heT+6{k6G>PXQeJOCC^bl#50s#_#iGB%>q;ejwQpbA>%>$*% z(?y5_l|J2%-=5ER>24a;n}te!x}ERR?&1YM{loX5p|yAElWVT8(lpChI7>)|IDmuT z$eedkv*``o%D_-ZM`Bf#BRLW58Smi{pJ_0klV`}TI^QGoy1mgX*Kx~N`f4KkBK;nI zoG`T^#nf?UX2dGO6#X9IG2tFwkN2|ZQ-n)3J9B;5&YDTwuFfT*%7&moK3Fr}&tC@f z>z54jvh#n0iEP8E1NQ$|22vD%-V_DDa#0$=O`R3Ro#Ow_8Vke;w@n$keIGYmuMCD! zqV9po@O_3b2#I`xggqU03VdTVjj|gx`joc%20S#1+C3Bm$Nw1|5(+bj9NYsU6r43V z(5LW?z24yB`8$6qimT3YBcv@_iKk4>i{g`5hX3pbP8rw)+K>6X?7=1Ve96={Aue!? zZ&^3H`?T6V&WKA;o295F4Qz-q(JdyXAQ-qv;J4{}qdB+?IN-HNLj@jG4*l*f8C+9MPTppBm=%;4cRoF940uz2u zyG7|Em-`wH2c%kN?^=Abk-Uy_lL1sa`~@K${T-=DM@;saide-uLb6)Ca;3s`CWWa? z%o-ERCybP)Bgkheuo@{`WnzU7A_^y&bVOUQi75&vrllan_=kjf4rCQ$EW@F!A%)tD^0G(k=%FNhT10?;fviXQ=Cl=kaIj)JG+C?5pWe3Flg!G=m&f zspP0SS^+O_tUM}KlXLB{P37G3{=RO{C9ft`T)BLEKv#Hz2ZU$uHB+Y#5vYUeQwe?EAI`hFu&Qn6+o04h*cim>9&nIrtqS zho16k!Qcc^>s4<72AXqv+{$g21qGkV_~Cpkzu@wCQc~j5f%}5|&@7+$`TrY8Oac#& z=Jn~fg;ib#I0$8g>bE9LEHLxQpk4%@<1!EzK+K-TkmXGwo6OJ!#EiTObpw*`f|!B+ zPjSGS6t_H+``Q4X+wW#1y@`9!J&QQv&U7CkLJo-oguXik*CVu`ZE*!Ltvtd!0dk`TaWs^^G36f`JdN+p8qisf5$Q3 zRgf^Bu1qYTGac+*B-dEy`Ly5HsCccrFf?+{i>e+5JxC~MbcblWW>Nc3KqGpNF0?P4 z#*!kvq;5BuyeHAu-T5TvGR-VHUS2{6;<*7Kxl5bD8&D5}B5-Oi$R-1vAhYhcg&J&) zEQC*pe-NDU?rdWFdik=NSOH!E^)?A3has`~Hc9E}&gs%7W)aiEbjOOS8`;)ihRRZ5 ztU#4vVH^!BX;rFwR*SKE+$R-=UUGtFiDJ@5l8qJ}v(@0>)jbk+s{|{-vCuWYkesD0 zZ(th)iq7oKp+HXjtR*`N-vpONTHuqS?SgaOT}@Kwbb4+yV=;^Iwy$cEGg5nwQ%RNk zA5kG6o0LphG?O%MH7P14%Bk~m7p?0Tt;DI1VkOy}qTLsK#zPw%B-m;7FGQ+U6qQDT zE@HDCtyDb;;=pW}1MS35&<@Q$NGBluj^X3Q)dkr5t^s^iT-d_nv!Vh#gdtA6%e!V^ zBTRk8D%Dkci<3C|63=}Cg~muwXJ$^NROI&I2Ix<`Hli#scQUa3=ud);?`y2-WFYPh z4CZnV)Vpu%i9%|~Q6CrmC^$z_?2o(5ziu=OFY9<5gV#cN?;hjqijVYhIlLcTt!j2- zSw1K8_S8Ylg@n!_79WSZQm#5{FFMDY0 zUe#jd3nJ|5=?^)5OoxO$-CybOJ&z3I1Z8zQsy`r5V*Ocfh6km(w`uGc5;o7=TTS+4 z9kA756d2n_C?;zo%NoLf-IhXO{G5v)@fZiNl93vu4y8tt&lpPg?ZygH5?4!8SH<8Y z>Ca!JYdl7xX&j+UY=WRfF@JC)LF$8Z+FRZr9AyljeM@96*;%Dl z3={nGKFb!cbC#+R{G6>s5k?juEQKw!p{; z5~>&~|MQ**Xnl!Ja3Zzz5ECH=u7zA#1X%ugLqfyVWJ~$`L+|#mSdmy$YPJ9J=IE** zjYs6dP49=DpUe)d``Gg+9RJ^mN3=UPQf343qNc{MDu6wPXp+508ycq@#d!?1uWdV( zHuS6O>zjKZ#v)NM>kk5z?!lnquBSnV7qIL=$sGN@^3dqdWLi82|0DQ4*ZR+?)+5Dn zSR8uILp}q?0^vlC(tu~vzFAI$jSZd{>|s|?$Kpa~r_63B0}I!ozyW-DFq5<7g*F5$1~r<&1ZdAOa9IyjO-Rm?_Cd8Gicfb@7bC$;X%T$0&5Q_s2qQ$SV>rzT z<=Je!&^i3Bftp4&mUP;5*FhM8ic zZb8zRGfUv*ksPg=La*UOu6Qd+9O&xec@P@10SuVu&my8ENFq{#BC-M;2sFY9qhUvJ z^-N!#){ej+>SI&w0W1;{G$e`T-;r|20Wf5%b+2!sIOtp#qdBlF@SZ6I+ZB-{AEW(Y zjp(j+%650xWih<-*Fp#8B@br3i8)J+d7u}@xWv3BzGnV6(_RDdXke2uG`~UA!@zQ8 z4pqysKztC?nX2Ng{L>(~(t(x$eC*YhwkzuvZhN$pOm450yfB#I>uVdQ(0=|EHT85I z%qUjr!8UlRvpa+0bXuqd@3P*pvI+IHoEbm(xJk~pF0&YyIz>{qR$3yda!8!k!ZIZA zOMyQSSha*VKY(q(y9YA7{CFWbJ_N*0c{3nmHtW7Ne@QGcQx|UGO)$6h%4@H>+Gb*r znQ3sL(|YGL^=u7uM5)ZSxwmO|7lnlP`R`|K^;*8MW0;a10l;^%WhJ=)!0BxW24ZoL z?cNApSHlTich2^)q^s!8xlK%WErvC97mdO6k{T9A&ssO~_(gs|Q|!s)S;<=fK%i@Z zboCW9g9nfm2A0!%+5>J!HQEt^zq5Dqv292f8B^;erHJSVc0c2V*@_Z(GdY%zwX#-_SCywcO=uZUgY2D}mw zTtDlELL1V=xZGi2*A^51!q{j@&!3wq_*JZ{NdwjR_0G|1Qo^6Ap#BFLXfK5hek(%9 z#Xkzpx^_1C(y^=Yl!uRsH!Q!&CoIpXaYAciVYnuiWW$Ikv>fZGK(sJsVK|{wR405%N6x$1_aVx*H=pYD5vndskk%M1anwh9SAb1=@fojlB^@ixrMt1QU z+0fWY$ETZNGixu#%&mi7;%@L>#_??AAmjg?wyKmhfT}+lI*R1EHWn0@og_h4DCQT4 z19K2g@JXjh7#Se_@bHeg)%jx;3xz;s2w!o$K^M5T4aM<~or=Lm7M+Av{aIeUCjBtq z3WK7*Ka)9SwCAp0+n^qYZ$#I``~kM`$@8-1Zq{_tr2!foY8I;{p)G_?)nJ#>V&Tbo zaZK;7Z-F~m7s2r3~+lCRWo$FA1|TV+4Bd)(re$XAi?+{K`%}6K(KNOji`jcd?4DRXMsc2W{@V$7@`ez%bySIl6wykch25a_Q@Xvl ztyqaO?QG~JV)epNEj#lJg%+q@a&N@NaVl)LIAlZ=I#P=p%d^Vz9~q7KA>#o>YMx=` zuR;$O^y_%ZwnQCBqnycPYe$889HF;ir^(f2B%bBQrlAYFDA}V9eaMJ-ci1U$=!k1k zV%T;J@ugykcSd=yj-?l=4tp*0+;Fz-b)!>)c0ox|E;EQJWU-lQ7G;yD?wYaF+llI_ z@uU`8V#z@QKa!tI2sv)1#e*s&J9Srs6J!@BJ*2_&aP@eul1fPzaS7|8cB0uy>vDG0 z>Z#kP{ZP0RWNRp;3vn18hSF1_DZ`Urli{l{m-m{-InShNkQ@?{+nJktCgz;Ru==EE z+q;LNNG?xKeed>O^IrKr?CTY{^(f_)c&SuJ#m^8zyA$9g^#G|7t$s9rxolz1?=|98 z(x1#jS-~~T>R3`)Jl5J52VxmuV1!&`y#g~CHEV4@qkFEJqz56T=;%(1H}lULy2_-C zT!y4fCf9oKY%_VDbNB;Yb!Dk-%kEjcn@P=C1UbRi>12-4-tqN&7mV(RE<}V(SxTrU z)nkzeXF5~kfzc^{gi}O2#aJGkDjuKNVFZj6PpBodxzg29JEJ(Rhc>J}q2?;I>n$@1 z)~<~QzT0V_4Rg7S>dbK|-}119UQ`%or{9)elIkX1@5pymw*1{v;H~lwwJsax?UOWn zD-1p&QaG_ld{jP&LRI1SUxE5}U36&$F6B>*%NO0TA=Dx*25~0Jf>dc&BO6<=)%Fi< zzC+BG89Q|IjE_0ms4+70MaKCyZCR6S3`@+sg8g@?UrW2$iL%>t#tr&&*?ZGLBog7< zT!>FbAlg?W5pq31UP^=UtxJ9>EMh_+pvZddx}*@dom0vM+)_@4XJtkvuGum*tGpC0 zZR1o{rk+tg>u#3S%2atditbj>-MtJ%T+Z(~Jzpy*tFG0j8?c4}mCScw=Km*A8>wy4 zF4O|GEJp2;_|emz#V*LCO=fcDWN|p98$FcCK7EHKF%)ESGGj~yWq+*lJ}q0`CC^mk zmS#t64T)a%?cUGE`P(OF(AKzi>6e~cce44GN!hqmY!=TfDplmlGnaSe$nxcahRlr* z*FXt01Fhbidopg7s$iqXsoM?bA1(!9q8eWxlgY=I@7scpX;Toe2I0-~!^%QSOGC@T zJ{ZKa4lNBU`w;3l2ppa!k@`EW75!zMRBC5dW``Dj+wraRu%hQG=l*@`{U?0|=D|Vp zj2QFt=jM&;;}--x4^jdIc;1dcKvT0>b`ohf28j~5*ta~0vgIqX<=x9&E1If~rbARq#IQ#82xw4cIOOIkMNuFb$jFN)y&5W8M7<7Vi2Fj z)>i5xtD@$%+Z1{7oMp?h<@pM9FSfXDqS$Xf-RXI%dU4Z5f|N(2>gnTk#sUPygegPH z>WYU3W&O#QEC^XO5=tQioE%hUvvs8(^uP~Z{h7Nv{vcahBryg(T}K;bYg^I~XD~f? zKD}Y1s;ZFC_E>@L?d*lUdCdTU_#^W21>t<;5CvvJ1&BIhdz5t2J|~&EgdW9+Ep;IF zl%xh?wIwfJjMM9*QF5f;7W)gt-V!aA<5reh!!UmjfVwo1IpsdZOyp8#-{jq#O&MTJ zc}y|DH^+lE-JFPuiikNnbbyIPR6J{=U#0tXRujmPb#=)y<++mv^P+ckWy*5plQDFb zH8$`~U1x1Gg0Ze=UbVm$;RshTrV)TL{WC56`A=Q(l-+sVHroad}Z7%!i0rkOF4;8&S_pAmzpz>>7I~*i}i_V8{gq>@#faTpcJ1z z;_f;JHyUKk(4_nNrfV`L6C_CRtUdYK3h*61N$EoL z3$`EF|AC?x>M0*SZU-J9DE(kSF#hgsOfgi1LlBqxO5eR3&u|GB_6tqUY~fVBKu--< z$aFfH!lYcC5eI+u5vwdt?+)rW{;@y`N(P7hA2fA}^7jl8H* zng9AQZFk_>eli7u6yGxx^c8@LUiH<0GyMr_%vBI_>@U`T%U$*mH=({eYdK+~WDm{{ig$`0i z@7$zTi4G-aOhixY>HwP^G?v8Eb84lhO+Rw`-R}Lbu!h(Dmq#R}5(0*@?ZTq->k9oX z!fxdTAg!-Dwdwox!G6Gw>79u%tT6xbmuC~6twY_292ah6WO{G!q=-TbR) zBE>jCBPXpl(AG3uC!2jjV{Z%-MGO7V+5y(bVa6~uBSYTJ|J=;v>kVsYhV@Bgj|`8@ z1EkMG7V+&{BGvKcZR6&89)j`ljPa8WnCN*Qm+ldlB1~m!^d9bJYm#JO@PHYZSMk+4 z^aN-KvxXEGhgiej84TF>9%g+PvYZgjDG1C72*?R6xR)S7f@ke}ua=4ZZQ9C6yFIc} zi#8ar?_FDowpx4R{o3QTTK(Gn_4xQbryO3|^tz;=yoW*axVHP>1&lC+-e!%1Y3kq_ zpqFOd{uKTH2YumU00{9nq$Z~Yk$O%+7}?^G=e@J;1*9Y1<-s@8(sshUi%wqLFf%2! zrjA6q0YpS_nl>(LTn(oXxHjN_k{}A=d?JWQkDKlSL0YvW6!j48vm@-4qZtVR=vF

        UCKcwC4|l;eU`$RSmllEp35`tc}!QWb=;N=K!*lcFtdQtio` zmbj$R5c&;LaCTB6X>(dp1CS_5C>j-U^6g(kBicDp-M>989fE9zn~R$OOR*FL{|~y<(R~c{Z?Vr82jB_;fO9*>C$0~70sj96c%s&H*{EN+ zb+`~mEYwr4s6QG_0TBCU0RNl&SnZzZ`Gy#|#0Hq9U&+n+HEV??-;UpKPW&0`tY7Sk zIkeNLf`k^bmBMeo7M-QZchhh5p87M!|P7LLde1)c5*upRXnt=4)e@xKvUuK{^_-j+##YvjwMOoEN+bvh?&33mx98c%V^>)`q zMeAc`#2Vt_6B3h>jVY<7G_e(ZoH|9VtfR_p7GMYbzH(NsRi-ZdA$;yU*O)c6Z}?6l zwJkz0{yu_%x&f{HIYh}as&CU9I|t-M7F)Zme`%-%YF+yPPAXt?8hVR-kA`%E;X2=v zba$p;pwijJ{`p#=1uMzjT|JeURBk560|Q;2poa3l3*54KnwT7s&6rB28}c+N%#i<7 z2cZWSpiWcv8~gc4V|ER7AA3N`0nDpOPEnBK0mvN}Wg5~J`G5B~6IQcTE@{1UG#gze zy2VGIWYzTwVogsF~w1Ytc$c z-AtOOZvA7Hk!>A-r76R}G|WSk36G2}7}M1WVwz+lt&!-$%!@Hwk^B6H(>5y>rAI(b zuc2#-AH`>URhc%_PGuo#o4<`47b**CR~Z{X0d^{|gIdm#3b05{h0B4wN$OoK=W7K` Kyk#f=00013X9tEJ8&5JWQcZ^(_+Xt_uVYap}w{P?F#uuUv zN>zVTSlTTzf?(*N@HYL(XbKO{&{Az2c@k0osQ58MDR~}#?b~_tXLoV4D5BXcLGw_o zbV3wQ7rZOP_hW6pzZ20Y+SGx{bcS?8i?jlX+!)~J*1uK1w<;EFgB}|d8w?l~L!w4( zln4k+B2`Kx4M6Wm3$KjCLi@xs`V{NV$d6HX|8!};*W^LlCGUpp=4MPMf$r`N2qY#p zu>}A-Q07yb1GMjN`M!L@NC-z5cU%YEOUNqG3RCOa00IO52G0TGQZ@Sl@>7G>yzjJ< z=H^J+W!VZ0EF?UDu*;I%X%7m=+#mbhpMCcy2ox4VPC5u}hrvni=d52oZu&7*7L8u4 zt_vZQ_Yemd4}l{XHU|h&B7lIr2~YeT2e{kaZUY-G35uh@GY``&kc2bF=HIrevCn(EKV2_|W~DU$hltR5No+(I*srQdo`@LeB}z zOaNzMk%w-PNx0#K;R@8qVK)$)qO2r8~& zeQVRf<@^0T@$)M}78>tdrniCf3Ywq+;}~nzHGD4@7=Mh@5RH4cE?Z!XL2A+TB_`)3 zP^26MmZ(spQ@{N#=ErBBDL;<=_4`kNh{$4l3J@V$-X80D(X-o!9p}^2mv4`MkH;Ld zw3Bd?l#774^U zBm&JyB$|+D%p)p?!$-`ulk1522sl<$FqyW>&60}7su#nWCHPVU2NIzB} z`>}vr#7g897U45c4E7QQP~k^$tC;nM4*@1=0XD#99#d_`(0dqj-umEbyWv73+2gjw4?i@(7%I%QvJO+!PxsX6#QJplFB*TLl057tMb z=EeuMNdN#_Y*nL1$FtK}?=g6s59L`S3D3HegNr*^ip3j5#qzwQb z>e(LcOyZ+sJHo?>yPsQ3z6!r=NWpkvq~JC&5{XcyhZz^f*Y#o)*LT~(CV@sr3}(bh z6h!ubH0QC?%&HY;kf2vt&reCzt~WLptrMA?B5}L+YzGAZ5sd$~Lt$A@!@j|Kw3e2O zq;lNS*IMhjkrELgdU{cLsK(T~-kr(5a69?!;I0L?9O&hx!$0$!*l;aFwXcqATl+-X z7UWmbwUn|fwT{6D0&4O+`!}{8H`VEowj1YSzmjFV&wuC7K~4U)`Y1@(~pZ(e}IT7dOncEpbvjJASdm`ynr)<&wWcxdpofny_nqvFwI)wZXIobxk{r-{l3 z-@xO54SI^2BArjx`0WE%)}HMj?PKrq}_ zOXX?bIfmSBH|Y1hUeAxBV}9>t&+EVH2i+H4H+mioM)Vz2n7}`sxpRQwK_Ii3eIDX% zr0hZD;BGL?T@%Qi4d%VM+t;{1=gkMR?ln=n7XG?WLZvVaIvuaw_FFB0?eTXaB2W7g zkeWa-Uqi<6fC4OX&<0HUw=)e} z({_>%SwLATYeHu=)5^N&c%c%NZl4L&*7+GJfWDr^Dtj@@M@g>+QivX(qLQfDP>YrF z7LkzbJLurAU-LCA)!^U?6e3BG{lb?UqF#uJN@?_1N)s;SO`g^ge#;fa?ey9s(Wzf( zG{GRMx4o6Ml3rk0G@rDn9ZedLHZYb*Rq{Cvg$BEI(9|g}mWbKJjuj3$n-rOY-$2BD zJdz>3UTS7@MUO%3s&qg~h+t=9plXdM0m@^wp-rP6!%@==>Of=`zzg8X)$ctP*^p`> zR|evrOe3SHcI~MxZE7&CUyheA-^orF?{0ax;wIFy#mcuy8Z?*I>iIr@wy|l!^4GEO zG>~00hc4|4Cu6`>v|kMNgiXg^W0-=hC3izH}|lq^knza1|KO z77nW&(cYk~bm3=#iimXod;{vqs}#SB9^^+BxKV`mTCWGoecrv1qpNU)F99S8gMxu_ zk13PYYV6b~-&E+N0pAu(N^L4|IT6~)z~XR+jA(vYsnN<6p1OY+Wp5uCi0sd9c{iUj z>8T%;KQKkhVEJ z+BAE&UpEA%$|^~pm7~2cJHf1*`>f>`v~EbLtnkjg;+LIoznQyxdmxI=c>${+uPjwJ zM%(Q-OsIVI^(+C>J839XMK+6NPi18q9TbQ)vqtWBu`Gu;c&9p8K7V!k#Dz-}$1r0m ztT*RXQfW78ty2Ay!#EI+h%}6YF!ua$i()uG*FBgqS0*F+jhOGF3cM2UBl!=7R4HME z6U7`*Cy?tefLyjTx!ABGbqc`Ku^E{lW zl8)Gv@rnohb#p8F1&ypmN{h?!XjhbzK-5Y6#&Y@xFRu@1Ghu8!Osd(^kikdFu?j!8 zj=+;ksTJmV!!}BLM~o$WqSA_bTG(V_8K4(8(zdXDQi@jxXfs(GyRhXpi;&q`L!}+( zmNPoDOPVHjD`dyFU-Vu)Man5Dh_wu9!dsy=mC8ykz>caYt06p9c#d&2(4;V5DTJn! zEdK6@nOMdZ=1J<9N}Nfm23xG;4q|ePlqzna~nMDOY#0fvs0@Ow}GHWhnr-RY4N< z_G+ra>}f`N3r~}0*!iDt-}bM%L54?lO_r?@AY;eR1HV_<7?7a~eZ$5&W^eXbUr7yC zoBMYD-t1S`h7M+pzv@Hn)zQNDQRfH-p!-Q0^uILpFSyI2LpOUvvpizx+y3uSPxQCm z=rRQR2fG5+Uo0;#zAZp6WIB2B6$jLJ?l25Gx*i7`FIQBS+~csje zyPk^v(4|AYJ>Ajg?BSOFkFFlLEhvFt(6)Ifmh)@JkL+p^m=3u)%k1rNVX?S`$PR-4$vcuH%Z zNLQU#$}QE9wZ}1V?7SJqx}akK)OXVm0riCVzSHLmvqk(R$6O$>s~|wAZ=Y}H0Fc&y zFgc=FN+jX(FXdcA)BiT3EHMk%swyK<%fzSh(+ABQvP zd<0($cka-}SRX4{oZCl$fU3_RP)r+1AOS?y-4FrR2nrebswBx^llCRam_i8|kW}Ig z<1N2EA`!(6or@I2yU_b&yXz|FLtB&g2f^+m?A^s=s)y`;4opAmK3TjERKK=8O@Q#h zM$5I%JuHa+fm~VB+B7Y=4}5WE;STSde!n1_#CtjZTp+Qna3F(lykwg3yGn^<(bI~y zw9vCk8MGTLoMTYDz3!qe_4hRkL?g_3OoB5W~WAh-yZy`D_R zn4=TPObj?%Hn_l%K{}SEoCMY`W6e%xh@cr@g8HTD7#B>Cl-IFyp$c+daC^!N#t)qu zPbO@gvFnx+ZFR$P!ZMUs60KZwZ~>(!S5}hKSz{?_u~JNuEiK2ViK?Ya@)%vizOb)F z*ff`}Jf8xRzo2S(`)P^GQumpF$<9>!!Z`x#vqF;A;tIgzQJaA}htL?5rOEL8#a0i7 zR2myxs751{trRTXzdLtjD;HKf+3KS^lQLaBj91z$2U8>E-0R|mIU(<_mw`w3;N>gv zuH9(GG90(&3AyToi-_~c!odZ2mKL-}C-WnDlO5%%ux=GYXSI||a>~N$q#_ZGk^B$= zYVwbE5VJ>st!I_KZA${i&G88v5dA@IkEDkusa+tkz6NC~=^-0-jxQnuN4Iy-#BsAu z5SAxe=NfIOC%GFoOCQ_sn*42>KuK>ZTQR?kpJJmj9Ujws_fpS#7wK8Q2fct~kE7%7LB-9(>WK%BF~?BCXe}3`gA4k5=Z{|9c(EC*IJR$pj_oHBU0UB-S4*WF zB1tXRTE;d^Q$c%H5jShWC~xCvO%C83N!b{0%&o-L*Dn%N9}LnBaPIZVFmBm;0D}}T z9|KQH{Qda>?)geMGg!m>;95R94d9jx=eW60IvOa`ViqZ?X1z?@5>d*Eqk@2i+g6+* zXfZo+Mc{w9aw97*r?~bYv|;-lf1p6Vu%u%=ixwFshoHo zFF}jSIX}kC9L-n$DIK}>csf5uMc71bH#<-(4P@c#w>k0UxKR1zL&Ey~73VO#6feet ztzFF%R$&w&HBuZ7Hp%`9Gj=yZYR`rHm=zeuSC``iK6qM#G(1{m+$AcWMy4`wTS~d6 zBU4gZ0HIck1M$OaWF} z`!C1D#rRl*A?L?4rU$OLu$;#8M<@LytD8 zF;-rwtd#Z^ttm8twOOgX%Yu9%wDJ&U!o}xa&>YYuQe~63(W<#u1_sS#$M$7?{fYIIo~|b_J_7h zB%_8(2l;5qsxp96F$^}dHrCuH%2HIL??P+T*flUDvv{A+!&>#vazcC5$+Hnwa72*@ z?jYpjzd6y1mi=m``ve~dp7lqSYFF|%CNf;a_#&q+OFMk2Sjv;a`H9b>$Sncs@}UpQ z`5PZU;a$gErSA>&`%WLK==GNN^Gm76k-wci6}ys1Rv)ED>ZfDl4+SFit%tFD!n>k@ za~zz5fEfeY8~?>;g`ed&wN9PR^RWAjx63Wot4#ZAT)KCII!U8nUdf5KsJ^fBv? zUD<%nZF*a}*&4B8Ycgj!6IEYbjoTU=1r{_tx|_G&WsYHWQ!ZcR1=D z=`E=&trkyiy?SSLm#dcWALm-Jl2z{V-kLL+-%AbqYx^6Un=dav9-CmdnQv>_cTV2> z#=`jIs_uQ}pxUZatlv+XM5WMHm;U8%XcV+vcs*81T|)UX=KouTuj%yGXWfr4FIOc5 zjcFR0lUtu*Mzti7!r#_fBrW_v&_JQFrwVi8UDcYt5*X_9USWH#w@ZEyitw?%$#)uc z9|VuQhyMNFyW#oZVry!+;Gb)ay2pb_9vsv@;>jfk*}dK|)B#h>tLr1#PW{C~_l0kK1#5kY z$ev&K?tOTs_w1wI2d9Tm-TzNtxoY1U^u!lx$8$WdWS$!LY;T8|H2ZGr$sMr4E%MDTc#6&fz(R=@; z_bPbdJ{P`p%R$F$4EW<4g*u(zKTyi8+ATkoF!^t#OM`yA*1*}Di{~%LkExQB<3VRX zXUg88FQjf>t^{K4i#c&&os^}r^?4~LQx9aCkmRfPyW3wlR%o$ni%OGLs9o*sbi#PV zXR&WIrdqMT@QNlon^yIvq3LrDT-Ya^;d|Eh(T;YavSho$>yPG7TJ73f?&W7$$>OB& z*+A=DJ6SFM@^h~0w3)q^519_XIUDz{or+t^%HJ-uKbZQ^*zLlXJE+5KrvtIw&V>K{ zLqB+DJP1Mcn+dR%@SWPWM`4Bs~U%H32M5`D3}qyZv4-iU2U)fSN!U2uQ-hrdrB;=k-oC>8@0UbFCIz=VEt6 zX6f2ydfq(%I=X%S_A{?vB|_Ay`!rllpL)g5Nu#${OMBe%bCxd8))3UB*k<+V^F7kl z1GkfY20TRhD|GsBN%3Bv-*-(z_0K84V=k#gb0Wq>xcC{@yEpyD){%c-joC2(YXm9r zr^YGR5#A)0q3Ucq*e9|&9Dc{Cdt1s=>C?{dcDfTTIt=z5{M}&{9XEM|2b*k4{OK$N z0pFj~nq#M0Fsj`g$*%dQ*?Ujom%N&KfrF&nuHqGyn?B=L2 zEwNM0)KCHGPVA($W)G)|1dqDg+N4GbcW12^2|2~%2Ca7QOfg3&TAwA{ja0Wb-GxVy zs`hX+*0Ga!NWc@FWowlM@Q|HJk=TdQfb9Dfl#k-y|KHK-x`10t({`KWX`5-=WZMnb z%0v7KJ%It0O$6>ZLti#nF%(S4Xx1y+PK&! zW@f#Nvo45lk8zHgQI8bCojFf_je%<|>o3`d<@)%BFLN9*i=UybZfC6Tfc@9|v%+=K zGF2Qn`hdFVgXG@~07?3r7Uq-m_vFICuCAkyNo4+CAg94vmY0eEDT-)) z=s(b6nZM!fWi)zK8cbHDSB}xM5`cQ0Y9C`B>6k^L=($r&dgh|}m^{1fUfG_W*%by8 zsqpJ~E)T9*9nuuhI@FEQl&ReXyYNK^GmeX2i<`vr;^Wb@Cdu~?j-!u@W<*V*^CD;f z_I<~!PEhSx?PV#e4$2XB_`nV6`R!u{H-o#gBHm~~5ZvjZ?4eL5d?b71L=6fNB!G}3 zXy+!m8LUSao=-FyWg%ElXzVmJP5TKmiA2Mw3@QwboLrTqa#^RZ6F>+mBECXYmb>&s z4#v`{GDV?e6W+g6^1(2(jFyPLu(bw;qc;Q$MaQjE=bbK=Lh~j z@Zeb6{O36-W_b}n)500Pav2Qv>^BxTrXj$}7f!^(T8{npY)}S>+_jX}Pw(MC$CvwU zuxxPR6)dMu73IiN8j4^r0H9s2PzxU1L77k%ot@;?M;ZIPR)QVjzMp+qjkZD|s6r^E(7kAIYrZlaqq)BnQ1 zrp@E|*t(OKb(X5CN)sNX%&Eij@bka-R#qxHPu5}i_<7oE{9l#-i#ylnjnnV=1j z8St#YjgV;yl@RsX=2d?MnZOzRQQRT$5FR^qoakC!y)+S;0S#)2r^!>ImU^nyb?~#! zKbl+{7V9Ls8#jhZO9eEk3Ujzhs`eOjb%&qZEb?6!H@K=?pFHVUBq;TU?a&A+=&Rz4 zZs?vDPnGG&&xsz~z{>iqTk8W#t1pynJ5W-4wIm{*GD8VW+e=zAI`EIpudQGA;qdy5 zvMbxM4hLO|apX`qK;O%rXs^qT+U23>4wN3gv z28Xo~#g<+SPT^nUBY^SoZvfCga=+@}h65*kMGZBFID4wO1~KYn{I)2h3EQfCm_=+d3@F7+-> z@XNn^dmcRSE^Y3B>wW54NsWa<6378KFpkV+D>a*5z?B>s>*z?V%F-mqy&S^r?ZcA| z=JRs!-c{vtgjTgTh-E!)`D$NIV4tVm!%gGoH>8+4?ko@M!_U+1;UDAg;dD4}0&O0D zp<-vQciUMphuO_@fvCD6$e$0^j1TaaUi|8nqrBA2pCJNUfAT=_HZ5tC1; z{WqYoNyP3kPbmH`;NYOIUS#1O5FwzHxsf4-OX&3)D~GH6`9Q8J#TK8iycb8FUlzzG zvh?4I51iDq@zkF)d8wl-tND`oZ9>ko+hBy;niP-FkSh;`=Q6{>j#1sHy z_~rI8OJ&uN$8(2Cjq6CoD{d~{Wps->?4mCs4UumZ4}B@-6%V}5Htuo?KIEBDM1mbF zZvRqz)7*J!mtdqK&xAuiHRG=Af!8E3;rF#$&Q15K()gk z5VDa!5OcJIXrCyDm8-=^tGT_s3fGzFtBhw>#Iu}zi3wV~e31gH5&e|$tnd~@e#Ce! z!R!^!6!{SnQV>G?UBWyIWE9^^!4_qW6WzIM|R)5|*bAGDQ z6T)cUhaIaP1pKfu6nI689x2?v&m-#m*oNZ0`!&FZzB|62+*=zrz*4EvFB4907!3hl zQDcXS_igp?JwG;5xM!ah80kA7>B+k}`U5PD7W=AUbOx?*tTqCADm^i*ciSaF)~5o# zA0N%nx-=aX71=g&UyvE9^NFANzmdo&;Nj7XA>FpH%18nZf=NEQtr0T|Oe;y0^T6lT zNwD96;5~KTtLwct8NlCRAsHpA8kkE%@Ie3P*nLf`ZAQG^T6gE$??R#$!X9)N5+>}J zb`u29BC!BI?@mJ1Fb&YUBF--pJR;gFJof|df+U}+iT9?I?WiggU?7rl_EKqCY;vxKiNh63}HcpAMrKQ{cv3!cmdFXdp z{jh<-;4|!z0(!2Uk$0YP0D}w}d_=cwqu|EaWJV%Th8?0ON^sTeFTJdBPYN?1kSuQw zDt0Yg7Y=Br>A=#bz;&Pyq_eGx);%hx;?obrTEffdkK}U_?Qg=b_A@nzRd8`kMxI3P z5oskbxZOzAA+C1bXc98NIev5a#)SPpin;WG!c3Y?WEPESVecYZhtg}QSJxFwz(j zvrD~{mPk*O)-!XMMy5NKQ(nh5c`=m6l9&=i0UE+k(UKM$)j+=(t;2j$qG)-?sm4Gi zb(m~2=a8)$1N9F`*ew#Y1j9ntT)orUnin852;`jJn??q){FIeDvKPGa1B}oK(RRUE zr>=S_J&~3h&+xJsZ~wA-IU{ahmO?Du|A+!Qo24Z3@dyA1KsU~-dNtHT_I@y_@P{-@=9?tRvdJJXE%dDq9lmZi{@iX(uXi3 zv?q=m5ys#oEr5 zB`uGmcWjgo?ul7k@{u+zhYz4DWlgp$(4kb?8)VWP2=p|zRL`QF~$7NLTZ_9#O8?M4qVimRcj%7QVH#OE&()%FuXR1ME6w&dJy{=Y}Oj{NKS67A$m zEmndx;3WTJJF@>L!5Od;iPB98h;!Y%(%Kpss zCj2SlRa&e*Vp0dz)CH5rRBL z?dXjJsrO22Z(e{|${0HHl1g5=vrMfR#`))*S2eJ6k}G6l*<6`Fu=?|c zh?*YW?0ivjWS zrY5jTfjx$3lD$h4oTeMO84Q(+c{_zV_J{SGn|naUa#1epPdtTY&!AwgCxC|=Shl}p zj;>#MNaSY{HJpR{8MvBm`pZe{iQF(W2D#=TpMhZk;Y12l1J7Q&q+szIYaCGM?_EV4 z3-ehVySSzRXk3Q?4&aKsn8YnW*%|2pMVh^*PmPyiRIMJ30QVdNF6n`)_D);bKBy*G z?x`;7Qbb57ErM^gf#J{hW_XLW4D1pgp4rUvS;FrcNsG%`k_e(ZN#Z(A6x1OmOZc4B zGJ*vXm8&5MPWnx{p9Q>yV55>y%nTcG3zWv3SOTpM;AjjS@&Hai&)Z01L6+w(1CgK^ zKmp6#SOkOwPJl}g1eUu6hJ=|QBxK1gpXtjJ+F>YIb!?{HokfI$1}C!II#MPq0E&q! z?VDQ&4l-9Sk^{>EubD!y?Fk?{#}q%T5Z%?x+wI-8S^@3+Bin*{$%9x6!DlGJ4|Kv{ ztKc_;H_UG{?F}HF^k_1MrUh6v1Q<`xp=u=-#2;l9a3&muZpR>FPti)?Rnb{`o6xG}6XQl7 z*UR~)y^Jw?VgtBa`x*ndB1oLo%+g2j^MPLwM3saOe*l>Q_a4ab-m}?cb_j@`bYeip zY}S2kes3rtSL)@ltUGJ|nAeqa=jXJ(IwYDFbXmAR zJDh1`WP7uXZvmFa6RTexB81PaWi^UA4nK{T#QSq492FQu;8kdP_Y2_pwp$z+Z=xaeb_( zkpPtA*3*MjB#%F{Jo_JHpgm;X>um_mihmZIvF@yQp`rV6|$N$FEMRz#`S5 z5L6LLG^0dhYMN^ejnBFhVy6R79uXy>>SB^euPzcO76fg{gZlP~roqde%Ol(O& z!04CeCMN24_Zr7=z-*wM>hac}3FzY0vB9yEj!!c{2G(AbnXWBf;BIhQ#c`!ns?)9i1S-SF^+kh&58{xxayK zI4B893Z^MjS|7vk7&HvWjU}-qIbV>!!Jm&a;0?dz5f|9O0d{mfc3+0mwky}`Ft&H+Kkop1@n``XIr=PubvYyN=kPgd)fWXkj;C{futUZOyg8#P6&bB%pP|New&sG@0t5T6u56%5u_o zx7d3^aCo1euVF6cM$f_bINgGxS~%+^c_br}3$&CYmh ztBG)yEt`tW?jom7T6EqM;@!R{#Xb|(Ig!5GQN)*vCfpcBgF2R-qde@m%3;IawBwCV z3DOE7MOcLpVo72H#UM&0QtZ@Yr*{y=LG3`vHHMNr1+GL_D{oTRBBMP;NWylP1II~L zSRJUw@i5gmt__8pDB|MRgY5(ZR_pS1Rq80)D8pd1DM(e5^BXZJ9*R7Wr!K&e-jJXx z5SRCc$2rHOYLMdXo!*(7dnVzO#<2eh(YE&tSrJ_n9rxb$z52b)d*82@p_ZfMSK>CQ zmVyfrg1aN2mDK=TDe8YzyIR(mcC|v>C;i1Rlod^3_75c%#6nGdu|HM-0ftFArU{tI zs6|uz8QC*wVh_CY(XO2mZ|0xTcNIt(=?qDMOs=u#nHTXKmheZqstQuu7ThyBH4&Rq z@N&G1)5)}$;*PIBTA{Q@wB90kY#Y9sSdE6m=rpF<9;LPU8HyF{6r*`ioOpWbLof&~ zo>57tOQrpRI|Dh^n>MaGuHq`R>&+oqYuAPYF59U7iFtLB3 ztiEWEjUkq6P_Wa1Mz~Vb53g%RmluC%bs1wehwRWUi+Rk^1Wu8dFOtr+Y6|LQQ)oi+ zW%Pe-uFdTREO~eQXZwEh=C6QF6`g`Jg5olE8{N#StvH#(8b zK6M4>_;|@m<|GHlXBA}1(`2i=zW>C&K!aSsU$7-mW?Kunq7PRk%3E z$2+_5U79+_Z;ArefL~LsZ-GyKzE6SghXnD0ee!(^K7=|Bc#OX$5{ElYCBp@s6iR1G za)$L6%&cO zQE-I7s(8zTKy#)dRo=ZymZr!sr&}3>api3h_HdTAi2Lhc~G)sc@I+~U`$<{O{z)qtE?tEHcBg)F);Py~~_U+=v!HgyV2KzJM((l4r z_!t>tf?2StnD#*FT=9}<=1N*1BQ)Ov+mjdPf!5@`cri_@4noM`u3L&Y)> zxC(~hM*vXeM>6N_=9!UP^5UDkn~TXKjCuQc2JqeTpjErXzof)}i53}PqW&e%xslK8 zewE1t@?>3IvSfMsoWZ=}U0untbopEipJI(pIMz_)Z}#;y`xpHaAfjg9=iL81e~_4N zDua;(y`5D~@R}d$Hy`Xcp2z($jE_jgzf0vk8-BYb<*Hl9`96t;wNfB1{Y=U2mMl83oN;IQ0!?H&yTi$j)OXa;w#btRSN z+Ov;9^2I!L>&fKWD`Jr`fQL6nL2>{2x2E77PydENFc{ni4gnDL{nH)60N4YdRuY05 za^P9OGBvT9DE&SIdlc^xp(HG2-(5iVU_0*Xtum~Gpr zwn=!v=77&Zn}cTl{9(mxEW#C|!vtZ}?`PN5M2P4DU^AADOb-F2%k;jgl8o3BxahQ| z)nRdIBgu9VNtjUQ;I{FdZX0fB$@YwO9wKZDsnJH3p>#BBzc{T!n%Z-2C=3;Y)O=3z z(UOtjgZjpX$Z!PIAPR1cb3J?tZ6R9Fr!X!F-*b~7hUaP12i5S2a=_2`pQk^i#or7n;K8gQ(E?*X-D-#j0mJYVw%g8}!gn|(iotTB_G zrX+Qui%X(9X)Zy61kbtWo*MyN#!ryDV8ep#2ljs;>x4S;hqK+02Qcz5Fd~?K_ck~e z%)!72Z7$Mx@1~Qi{Di~8c&DBpMJLcv{1h^+R;GwobW)~OgzW+E2aHtlJyExKUYv?- zx<&UqcD}+4W4dQ@s+YO-UT%oAAM<1u;&;{p3t&-p9wq#nlTh_?|6mZp)YH|0lZii=Qw=v`NZAwU9R&WH^5#+l@3m?BT@H!}=`cF3WeKbg0?HY2Co1cvj-+!izX}WlK zVL=kb`t$o$d*U_8zyH!MgS|T6GkW=|{(Iz)CHpww_mS;ZRZ`hKwrC*8Kj-ZZVn}3( zxxwR&tNPR z+a^=%Z)i41x#Pj(cG797ofqWt%Szis!ylrV*q9kADQdl*x~Aqj$>8i0dSjd*YIH?v zMpz%m8RL|s3}svY^D~gH*Q}xH*GG};lk7A1lfDF5gtu=ARL7dOO&jzQ1g58xrjOce zq{DqoqJ3DbFpjC#+1nYcNs^J#0|vm*$5(5g<3NqC$vZdK+vNMsV8DIrzNU8}%Nfy< zERQsI_cV{JdkGRGc+P$A*)npND%O~Q;^Kf(4bou1ee0T1q}A9$`OE%|O7+X`&Ghs= zx$x_@m1Oc?61e!fxo;2nLHlY3wBLWw@}pBcad(m`Zn)1wp8L*04*={Z-_g*TEU0Cr z?Sz%+=-Ca6GEz%g68$Uy7Qq>sxCt-D6N1zV@=p>#0h~t!5Lx49T0wx;ToOupi1xW* z_R7%$07d(|8JTdre-hY4U5>7faVIHGatj`CoQEfm4h~yIB!J}6Ttz6&l9CdHXy6tn z7mGC7vS$$jP0ulDHZD0jAj$xc*^|6g!e)6v1_m#sF6rWS|BrO2^5Z4Xse&+8`M~A4 z6M_KMs`ltjvllcPto#W{Trnw;v?YzG0Z5c26pe~F`u3k~SUW4K+qcdeo)Wphsri-J zrCoXTQMk5V_v!TSg9x%20>!P>ivAy*694&`ZLgmg?k)A6&xRV*5WI?<8}L8we*qq; zz37Tn{VJ`)#ds78@z@R3FB7#2+Spga|F0hH;)AH=uEfYC5)hWYN>KAPh%R4ydcGzx zGhZ{Y_O;QseCuuHoMx!HHH>g|=2Oz1__U-SpON;0x1N_ir|7fKOZ)GOdM}B_Votfd zNqkz(pmMLUVSkp}7R9R2DC{ zC%HuL%C0xRp}YeF2o@Z82ta?kiT*(GachAL3Q=iv29w3+aCv-z&`KnhNUddZg^jJ9 zy@R8Zv(iPSc6D?2@bvQb@%8f$(2!lSNA}49IV4Bqn4FMPaz@U{1-T?w^L$Uz-MNB+No$t7 zrz?d9jHEX=#mtORrD}ROaM0NiYRI0guaV8ev`qo|oU3HIAuBf&9Y@So=r*z?Q2})>(lR{=y*W7gT{CQ1(d31f1}M0#VcskYB(rm6 zjjUV$m}F#Ic;Ioy(Q^&+AY