From 69800632feb9b90a7bac120c6bd72f8f8d1dae4d Mon Sep 17 00:00:00 2001 From: johzzy Date: Tue, 12 Mar 2024 23:48:17 -0300 Subject: [PATCH 01/60] feat(html): update webrtc page (#3361) optimized webrtc page --- www/webrtc/index.html | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/www/webrtc/index.html b/www/webrtc/index.html index 26a129b7..87c217b7 100644 --- a/www/webrtc/index.html +++ b/www/webrtc/index.html @@ -115,17 +115,10 @@ document.getElementsByName("method").forEach((el,idx) => { el.checked = el.value === type; el.onclick = function(e) { - let url = new URL(document.getElementById('streamUrl').value); + const url = new URL(document.getElementById('streamUrl').value); url.searchParams.set("type",el.value); document.getElementById('streamUrl').value = url.toString(); - - if(el.value == "play"){ - recvOnly = true; - }else if(el.value == "echo"){ - recvOnly = false; - }else{ - recvOnly = false; - } + recvOnly = 'play' === el.value; }; }); @@ -145,6 +138,25 @@ let h = parseInt(res.pop()); let w = parseInt(res.pop()); + const url = new URL(document.getElementById('streamUrl').value); + const newUrl = new URL(window.location.href); + let count = 0; + if (url.searchParams.has('app')) { + newUrl.searchParams.set('app', url.searchParams.get('app')); + count++; + } + if (url.searchParams.has('stream')) { + newUrl.searchParams.set('stream', url.searchParams.get('stream')); + count++; + } + if (url.searchParams.has('type')) { + newUrl.searchParams.set('type', url.searchParams.get('type')); + count++; + } + if (count > 0) { + window.history.pushState(null, null, newUrl); + } + player = new ZLMRTCClient.Endpoint( { element: document.getElementById('video'),// video 标签 From 8e16a698b6c0a7774df2a2bde57ea8a3d7eea6e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E5=BC=BA=E5=85=88=E7=94=9F?= <74175499+linshangqiang@users.noreply.github.com> Date: Wed, 13 Mar 2024 10:57:43 +0800 Subject: [PATCH 02/60] =?UTF-8?q?=E9=99=8D=E4=BD=8Ewebrtc=E6=8F=A1?= =?UTF-8?q?=E6=89=8B=E6=9C=AA=E7=BB=93=E6=9D=9F=EF=BC=8C=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=85=88=E5=88=B0=E7=9A=84=E6=97=A5=E5=BF=97=E7=AD=89=E7=BA=A7?= =?UTF-8?q?=20(#3368)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- webrtc/DtlsTransport.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/webrtc/DtlsTransport.cpp b/webrtc/DtlsTransport.cpp index 5ae4f4b0..83311736 100644 --- a/webrtc/DtlsTransport.cpp +++ b/webrtc/DtlsTransport.cpp @@ -741,8 +741,7 @@ namespace RTC if (!IsRunning()) { - MS_ERROR("cannot process data while not running"); - + MS_WARN_TAG(nullptr,"cannot process data while not running"); return; } From af155ef87a4c7fb77f6f23fa9880611dc22b30f8 Mon Sep 17 00:00:00 2001 From: xiongguangjie Date: Sat, 16 Mar 2024 18:56:35 +0800 Subject: [PATCH 03/60] no track throw error avoid addstreamproxy already exist --- src/Common/MediaSink.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Common/MediaSink.cpp b/src/Common/MediaSink.cpp index 41b4f415..f888b297 100644 --- a/src/Common/MediaSink.cpp +++ b/src/Common/MediaSink.cpp @@ -187,6 +187,8 @@ void MediaSink::emitAllTrackReady() { pr.second.for_each([&](const Frame::Ptr &frame) { MediaSink::inputFrame(frame); }); } _frame_unread.clear(); + } else { + throw toolkit::SockException(toolkit::Err_shutdown, "no vaild track data"); } } From c9c2706843a3b8aee8f078fa0bd2e4ae4f46a67e Mon Sep 17 00:00:00 2001 From: xiongguangjie Date: Sat, 16 Mar 2024 19:34:06 +0800 Subject: [PATCH 04/60] avoid addstreamproxy rtsp user or pass contain + --- src/Common/Parser.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Common/Parser.cpp b/src/Common/Parser.cpp index ad33f575..ab41c53e 100644 --- a/src/Common/Parser.cpp +++ b/src/Common/Parser.cpp @@ -294,8 +294,8 @@ void RtspUrl::setup(bool is_ssl, const string &url, const string &user, const st splitUrl(ip, ip, port); _url = std::move(url); - _user = strCoding::UrlDecodeComponent(std::move(user)); - _passwd = strCoding::UrlDecodeComponent(std::move(passwd)); + _user = strCoding::UrlDecode(std::move(user)); + _passwd = strCoding::UrlDecode(std::move(passwd)); _host = std::move(ip); _port = port; _is_ssl = is_ssl; From 1930d909f9ab5afca0d80cc6642168659d64db59 Mon Sep 17 00:00:00 2001 From: xia-chu <771730766@qq.com> Date: Fri, 15 Mar 2024 22:24:20 +0800 Subject: [PATCH 05/60] Fix the thread safety issue caused by poller thread switching when paced sender enabled --- src/Common/MultiMediaSourceMuxer.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Common/MultiMediaSourceMuxer.cpp b/src/Common/MultiMediaSourceMuxer.cpp index 5b3b8d72..fa46e4fa 100644 --- a/src/Common/MultiMediaSourceMuxer.cpp +++ b/src/Common/MultiMediaSourceMuxer.cpp @@ -44,6 +44,7 @@ public: } void resetTimer(const EventPoller::Ptr &poller) { + std::lock_guard lck(_mtx); std::weak_ptr weak_self = shared_from_this(); _timer = std::make_shared(_paced_sender_ms / 1000.0f, [weak_self]() { if (auto strong_self = weak_self.lock()) { @@ -55,6 +56,7 @@ public: } bool inputFrame(const Frame::Ptr &frame) override { + std::lock_guard lck(_mtx); if (!_timer) { setCurrentStamp(frame->dts()); resetTimer(EventPoller::getCurrentPoller()); @@ -66,6 +68,7 @@ public: private: void onTick() { + std::lock_guard lck(_mtx); auto dst = _cache.empty() ? 0 : _cache.back().first; while (!_cache.empty()) { auto &front = _cache.front(); @@ -110,6 +113,7 @@ private: OnFrame _cb; Ticker _ticker; Timer::Ptr _timer; + std::recursive_mutex _mtx; std::list> _cache; }; @@ -593,15 +597,17 @@ void MultiMediaSourceMuxer::resetTracks() { } } -bool MultiMediaSourceMuxer::onTrackFrame(const Frame::Ptr &frame) { +bool MultiMediaSourceMuxer::onTrackFrame(const Frame::Ptr &frame_in) { + auto frame = frame_in; if (_option.modify_stamp != ProtocolOption::kModifyStampOff) { // 时间戳不采用原始的绝对时间戳 - const_cast(frame) = std::make_shared(frame, _stamps[frame->getIndex()], _option.modify_stamp); + frame = std::make_shared(frame, _stamps[frame->getIndex()], _option.modify_stamp); } return _paced_sender ? _paced_sender->inputFrame(frame) : onTrackFrame_l(frame); } -bool MultiMediaSourceMuxer::onTrackFrame_l(const Frame::Ptr &frame) { +bool MultiMediaSourceMuxer::onTrackFrame_l(const Frame::Ptr &frame_in) { + auto frame = frame_in; bool ret = false; if (_rtmp) { ret = _rtmp->inputFrame(frame) ? true : ret; @@ -629,7 +635,7 @@ bool MultiMediaSourceMuxer::onTrackFrame_l(const Frame::Ptr &frame) { } if (_ring) { // 此场景由于直接转发,可能存在切换线程引起的数据被缓存在管道,所以需要CacheAbleFrame - const_cast(frame) = Frame::getCacheAbleFrame(frame); + frame = Frame::getCacheAbleFrame(frame); if (frame->getTrackType() == TrackVideo) { // 视频时,遇到第一帧配置帧或关键帧则标记为gop开始处 auto video_key_pos = frame->keyFrame() || frame->configFrame(); From 50f65c4ba46f705d4c5427394be5b2adab2ef458 Mon Sep 17 00:00:00 2001 From: xia-chu <771730766@qq.com> Date: Fri, 15 Mar 2024 22:24:33 +0800 Subject: [PATCH 06/60] Random port pool ensures that both UDP and TCP modes are available simultaneously --- src/Rtsp/Rtsp.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Rtsp/Rtsp.cpp b/src/Rtsp/Rtsp.cpp index 331daf15..dc2d0944 100644 --- a/src/Rtsp/Rtsp.cpp +++ b/src/Rtsp/Rtsp.cpp @@ -352,12 +352,20 @@ public: } void makeSockPair(std::pair &pair, const string &local_ip, bool re_use_port, bool is_udp) { - auto &sock0 = pair.first; - auto &sock1 = pair.second; auto sock_pair = getPortPair(); if (!sock_pair) { throw runtime_error("none reserved port in pool"); } + makeSockPair_l(sock_pair, pair, local_ip, re_use_port, is_udp); + + // 确保udp和tcp模式都能打开 + auto new_pair = std::make_pair(Socket::createSocket(), Socket::createSocket()); + makeSockPair_l(sock_pair, new_pair, local_ip, re_use_port, !is_udp); + } + + void makeSockPair_l(const std::shared_ptr &sock_pair, std::pair &pair, const string &local_ip, bool re_use_port, bool is_udp) { + auto &sock0 = pair.first; + auto &sock1 = pair.second; if (is_udp) { if (!sock0->bindUdpSock(2 * *sock_pair, local_ip.data(), re_use_port)) { // 分配端口失败 From 69738ad24e6c391bb3203c61f06144faedd98809 Mon Sep 17 00:00:00 2001 From: xia-chu <771730766@qq.com> Date: Fri, 15 Mar 2024 22:24:45 +0800 Subject: [PATCH 07/60] BugFix: configuration of `downloadRoot` cannot use absolute paths Fix for http api `/index/api/downloadFile` --- server/WebApi.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/WebApi.cpp b/server/WebApi.cpp index 669caa06..d0ab2e43 100755 --- a/server/WebApi.cpp +++ b/server/WebApi.cpp @@ -1879,7 +1879,7 @@ void installWebApi() { std::set ret; auto vec = toolkit::split(str, ";"); for (auto &item : vec) { - auto root = File::absolutePath(item, "", true); + auto root = File::absolutePath("", item, true); ret.emplace(std::move(root)); } return ret; From 3b7f16b75557a552d6907c60f87f4c06028c5f62 Mon Sep 17 00:00:00 2001 From: haorui wang <56127613+HR1025@users.noreply.github.com> Date: Sat, 16 Mar 2024 21:58:33 +0800 Subject: [PATCH 08/60] Fix the issue of failing to push streams to FMS 3.0 server. (#3362) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [how] 1. AMF 为简单类型时填插 AMF Null (参考 OBS 以及实际测试) 2. createStream 前附加 releaseStream 和 FCPublish, 兼容旧 FMS3.0 3. 正确处理 RTMP Header fmt 为 1 和 2 的业务逻辑 --- src/Rtmp/RtmpProtocol.cpp | 19 ++++++++++++++++++- src/Rtmp/RtmpProtocol.h | 4 ++++ src/Rtmp/RtmpPusher.cpp | 30 ++++++++++++++++++++++-------- 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/src/Rtmp/RtmpProtocol.cpp b/src/Rtmp/RtmpProtocol.cpp index 010e30d4..f98468bf 100644 --- a/src/Rtmp/RtmpProtocol.cpp +++ b/src/Rtmp/RtmpProtocol.cpp @@ -165,7 +165,14 @@ void RtmpProtocol::sendResponse(int type, const string &str) { void RtmpProtocol::sendInvoke(const string &cmd, const AMFValue &val) { AMFEncoder enc; - enc << cmd << ++_send_req_id << val; + if (val.type() == AMFType::AMF_OBJECT || val.type() == AMFType::AMF_NULL) + { + enc << cmd << ++_send_req_id << val; + } + else + { + enc << cmd << ++_send_req_id << AMFValue() << val; + } sendRequest(MSG_CMD, enc.data()); } @@ -619,12 +626,22 @@ const char* RtmpProtocol::handle_rtmp(const char *data, size_t len) { case 12: chunk_data.is_abs_stamp = true; chunk_data.stream_index = load_le32(header->stream_index); + _last_stream_index = chunk_data.stream_index; case 8: chunk_data.body_size = load_be24(header->body_size); chunk_data.type_id = header->type_id; + _last_body_size = chunk_data.body_size; + _last_type_id = chunk_data.type_id; case 4: chunk_data.ts_field = load_be24(header->time_stamp); } + switch (header->fmt) { + case 2: + chunk_data.type_id = _last_type_id; + chunk_data.body_size = _last_body_size; + case 1: + chunk_data.stream_index = _last_stream_index; + } auto time_stamp = chunk_data.ts_field; if (chunk_data.ts_field == 0xFFFFFF) { diff --git a/src/Rtmp/RtmpProtocol.h b/src/Rtmp/RtmpProtocol.h index 7ace93eb..2b7adfff 100644 --- a/src/Rtmp/RtmpProtocol.h +++ b/src/Rtmp/RtmpProtocol.h @@ -11,6 +11,7 @@ #ifndef SRC_RTMP_RTMPPROTOCOL_H_ #define SRC_RTMP_RTMPPROTOCOL_H_ +#include #include #include #include @@ -87,6 +88,9 @@ protected: private: bool _data_started = false; int _now_chunk_id = 0; + uint32_t _last_stream_index = 0; + size_t _last_body_size = 0; + uint8_t _last_type_id = 0; ////////////ChunkSize//////////// size_t _chunk_size_in = DEFAULT_CHUNK_LEN; size_t _chunk_size_out = DEFAULT_CHUNK_LEN; diff --git a/src/Rtmp/RtmpPusher.cpp b/src/Rtmp/RtmpPusher.cpp index 92363241..8f3596fc 100644 --- a/src/Rtmp/RtmpPusher.cpp +++ b/src/Rtmp/RtmpPusher.cpp @@ -163,14 +163,28 @@ void RtmpPusher::send_connect() { } void RtmpPusher::send_createStream() { - AMFValue obj(AMF_NULL); - sendInvoke("createStream", obj); - addOnResultCB([this](AMFDecoder &dec) { - //TraceL << "createStream result"; - dec.load(); - _stream_index = dec.load(); - send_publish(); - }); + // Workaround : 兼容较旧的 FMS3.0 + { + { + AMFValue obj(_stream_id); + sendInvoke("releaseStream", obj); + } + { + AMFValue obj(_stream_id); + sendInvoke("FCPublish", obj); + } + } + { + AMFValue obj(AMF_NULL); + sendInvoke("createStream", obj); + addOnResultCB([this](AMFDecoder &dec) { + //TraceL << "createStream result"; + dec.load(); + _stream_index = dec.load(); + send_publish(); + }); + } + } #define RTMP_STREAM_LIVE "live" From ff43fa507535493d56ef2fbde956212475dc0f35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BE=E9=B8=A3?= <94030128+ixingqiao@users.noreply.github.com> Date: Sat, 16 Mar 2024 22:01:06 +0800 Subject: [PATCH 09/60] Fix the issue of abnormal timestamps during MP4 recording. (#3363) Co-authored-by: xingqiao --- src/Record/MP4Recorder.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Record/MP4Recorder.cpp b/src/Record/MP4Recorder.cpp index f54c1d42..c8cee016 100644 --- a/src/Record/MP4Recorder.cpp +++ b/src/Record/MP4Recorder.cpp @@ -117,11 +117,13 @@ bool MP4Recorder::inputFrame(const Frame::Ptr &frame) { if (!(_have_video && frame->getTrackType() == TrackAudio)) { //如果有视频且输入的是音频,那么应该忽略切片逻辑 if (_last_dts == 0 || _last_dts > frame->dts()) { - //极少情况下dts时间戳可能回退 - _last_dts = frame->dts(); + //b帧情况下dts时间戳可能回退 + _last_dts = MAX(frame->dts(), _last_dts); + } + auto duration = 5; // 默认至少一帧5ms + if (frame->dts() > 0 && frame->dts() > _last_dts) { + duration = MAX(duration, frame->dts() - _last_dts); } - - auto duration = frame->dts() - _last_dts; if (!_muxer || ((duration > _max_second * 1000) && (!_have_video || (_have_video && frame->keyFrame())))) { //成立条件 // 1、_muxer为空 From 437ae778c0f85eeb094477e1a6da39625b097a0b Mon Sep 17 00:00:00 2001 From: KkemChen <704361748@qq.com> Date: Sat, 16 Mar 2024 22:56:32 +0800 Subject: [PATCH 10/60] feat: VideoStack (#3373) --- CMakeLists.txt | 6 + conf/novideo.yuv | 1 + postman/ZLMediaKit.postman_collection.json | 71 ++- server/VideoStack.cpp | 590 +++++++++++++++++++++ server/VideoStack.h | 207 ++++++++ server/WebApi.cpp | 34 ++ 6 files changed, 906 insertions(+), 3 deletions(-) create mode 100644 conf/novideo.yuv create mode 100644 server/VideoStack.cpp create mode 100644 server/VideoStack.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 96e79890..d951bc3b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -532,3 +532,9 @@ endif () file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/www" DESTINATION ${EXECUTABLE_OUTPUT_PATH}) file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/conf/config.ini" DESTINATION ${EXECUTABLE_OUTPUT_PATH}) file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/default.pem" DESTINATION ${EXECUTABLE_OUTPUT_PATH}) + +# 拷贝VideoStack 无视频流时默认填充的背景图片 +# Copy the default background image used by VideoStack when there is no video stream +if (ENABLE_FFMPEG AND ENABLE_X264) + file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/conf/novideo.yuv" DESTINATION ${EXECUTABLE_OUTPUT_PATH}) +endif () \ No newline at end of file diff --git a/conf/novideo.yuv b/conf/novideo.yuv new file mode 100644 index 00000000..37b41363 --- /dev/null +++ b/conf/novideo.yuv @@ -0,0 +1 @@ +ո|iamxt_PGEKQcqŠO9;=99A<<:V~X73==74:697B\ҦI??hفR<>>=;<<<;<<<@8?<<::MsA<<====<9>?<<=>=<<;<>8AC=Q|܏[B<;=?<:<8<=9B>;ZܥmG>:=>;:>:>>9@9=nǍL?9<=9:>;>=:B:LOB9;<99=9<<:7@6=>A8GmqTIC??HL>@BMiݸm`THMSEFFEM޾|oJACpΚE;ES{VD?;`֐g?;Mo۲y8B>LٗOGC*qnHB2PkΛWAA:?sDI@:hZBA@Lp﵀@9;Gl4;KGY꼉<:<:QiRD65FDzX<>=Aeаk9KeD7:=Bm‹M=:A9Jvݲt>5:>C[ȌQ@;:>EenC67@;GɐD67E??8CXӄ[96@?=>ʷyC9CC@@`۲jRBJ\wّQAD=7=EvŔhTQKBCGOZytK@>=98@JwܳdM98=;9;?9<98:HnؕY@===A:ZĊPC:>B=:;=9><==8L\kI@;>?977;6<;==<=<8LZ~M:=<;;>=:;>=B=>=7IMӘ]=<<39?K[DA<9:==:998>;=<7HDň62BA8CDOZI58::?9;:=:9=:=I[ܣPBA?:@;?69<<9;>:ATmuS:8=B<5IiŅZ98>?<4:<;:=>7D_٣f;6AB@9?Pݯk@7=><7A===?<8MuJ<@;=:;D^҄Q>=<<9D=>=;9BjaC:5====>|یYA;:<6<;>==JcU;6B?>:9X{P>BC}X<=aզmB9>99:>=hP<8C}ύJ:B998:8Pkٴu;<99;BMxXDAR{ɄP?:>:8@<=^ڟPA>=Vn?4<;5@:SzN=CBa]Q:C6;6D@J[ͫ^D=>F̔`<@<;:;>?::\~ЍOD>@QsԎXB:>86<>ExQC;?=̝U6C?8<:6CBW܎RFF:@Tp@B@:<>7B=>bحk>=:7RҚ`D=>??=?;?4SQ;B>97:;?=><>Y˄V?<;=Pբd>9:>@>?@;BT͟b?==8Cg}E9=:?=>B>:?xrK7B>;W[C@9=;9A@;67>;9B8LzmS:?<:Mh|EA;5:D>9A:CMۘDE9?;9ZݥkF89A?>?8@?6xÌG@>>9<=:>:;OtR9A?<>PYI@<79>:A9>@UϛU;5>C;Fx}W<:::><[ӫg76=<;<5C<6AQzφI>?A3;EsуE<><:;8F84@BPәe?89B8B[fI>=<:9Vݺn?A;A:;;<=>=>\֠hE7;@B:Sj։QE<@;:<<<<>=O{{RB?;8>:jkK:<;;;;<<>>EJ˒U>>?:8>>ΏX:9<<;;<<===;uqG;<79AJToE;;<;;<<<=8?YoxW>@?66BY{ՎZ:<=<<<<<<D˞[B6?D<:?eyC====;<<;?>8=tߦjE845;FTiω@77;>;;:@:<>AWԑg<>76<=FJ͕]8?C:@?@@;<=;IxlO:<:<@6C`rG=>9>>@@;=>7A6@=9;|涒@4H:<<::<<==A>XЈb@7>A?;6EM֬Z@B:<<;;<==>>:EogO<9==<>?==N„E>;>>99>?YԢm9C<==<<==>?A>B`ٖ\;9;?GjύC@======<>;;<=EjlF?>?@<<>BY[=?>>===<>>8:;7JÇQ??>;::=BDjĊP?>=====>A:;@:=gaB=<<989<@Ckۺr?>?====>?9@`ފP@>99>9>>>>>@@BCh?G;:8:?>8:BGiI>;=B<>===<>??@ixFC??<76:@A=@SuΎR:9:?;9;;:;<==>Fkٛ`E<9>;67@A8>?Jgf>67=;9=;<;;;<;>Sǎ]G7=B@<<=;BAEMViԊU;7>=?>>=<<<<=>HaʘwNC>BB<G[quC8=9@>=<====>==@[返hOE=:=;CGA;:=?BJ`~^C@1<=;;<<<=>@9@JgΠsRCAC?A>:>DC;A8:Jgϖf=0<=<;<>??>7F<=OڹZHB@>B>=@NduF0=><<<=>=<>B=>FeÞuP@;<<===;:B<>>BGm߫rNBA=>?7?<>@?<989EwrO><;<<>=<:?9@>B>IدoPC;;@:??===?ABB=g͛g8;:;<>><<::A:B@A>;<=?@>AjÀA=<;<=>>=9=?:A?;LqbE>9A@>===???=9BxݛcBA<<<===;@9;>O诈HD?;;<<<=?4?=9J?=l΀IAF<>?==??<;;>@bעX;?;:@7@<<<====<;O{Ϸ_M:7=A=8;8AD0B@[oK?=:<:><<<<===<7D\PE;9>?==?=:?;@@uj@<=:@<=======<;?Ch܊PA>=>=:;?>@7=EAKÊG>=:F:========A@>=BaSDC@>=;99;<9BB;Z~S<:B7=<======?;A<<=A8A?CuٰoA=<;==<<<<<=A;;=>;=CeՇGA;A=<<<<<<?;9<=;;:>D=8Qsޢ^><>>;<@@?=<:>=ATćQ;<;??<<==:>:<@;7EW~Q;79;;=<;<9Suh@<8=><<;;=7lʈV>;<<<=><;==<<@B>6IٸmEF@??=<;>=?CB;@Z迀Q?E;;A:=>?>=<;=G^ҔFA<7:<<<=<>?=<>???Fb՟VEBC;;A9=>?><;==KcI@;;<;;=><>?=<=?>AJvEAA@??@=>>=<;<=>a҆QE=;=;:=>9;@@==B===<<=>~߿z2;?B8;:<<======@:BGlצXH<=>==>==;?;=C789<=======:7>>V{M;==>=>>=>7TtQ8;98;<=====<=7;>7DcՈMD6<=<=>==;=A@NvB=@:<<=====<<>><;CBTX;@>;;==<=<<;<=;<========>9::A@Bgл[JB;9;<<<=<<:=@>9PN=;>========>7><<@>Kvlc_[XUJIGGGHJKUX^enxٯxJ>B99;;<;<<:;>>@Cf΢Y;=B<<====<=@:D?8A@COc`_^VZZTNHEFGECGFDDDFHIEFFGHLQURY^_bo}_C4@>>::<;<<:;:9CT{K;>=<;:9;=?B<:?@>;=EDB@?@ABA@?>@???AAAAAAAAAAAAAAAAAA@>AJWb~̞[;B=<;<>>G0Bw؝^><<<<;;<>?@=<@B>>?BBABBDEFCA?>>???AAAAAAAAAAAAAAAA?ACA=;;?7=AIa{K8@;:@;<<===>??D7VɆOA;=<==>@@>>@CCAACBCEGIKMMIFB@??@AAAAAAAAAAAAAAAAA@????DsY;===??@AA>@ABBCFJHJMQSUUURPLFB@ACAAAAAAAAAAAAAAAA=?AB@ACC69;8;??:B<@B:;?:?;;>??>=A^oP>=>?AAAA>@CBBGMRRTWZ\]\\\ZVOHDCCAAAAAAAAAAAAAAAAAA@>>=?A<=;:=CEC>>@?;=>:@;:==<=?Hzިq^????@@AA@BDCGOVY\]_`a``_bb_XOGCCAAAAAAAAAAAAAAAA@@@@@><:A>=:9884;=;;=>=???AEGHOY]\aabca`_^`cc^TJDBAAAAAAAAAAAAAAAA>?@ABCA@>?ADFD@=;<;<@>;?=;<>>=>>?AFIMV`a]aabba_^^^be`VLEBAAAAAAAAAAAAAAAAB@==>?@B@>;;<;<<<>>?B<9@<:<@>=>=<;;=<=====<>;;>=;FW۶kVEDE??DB??CC@@AGKUadaacba``bba`_``]VMFCAAAAAAAAAAAAAAAA@@@@AA@@??===<;;=<=====<>;;>=AB@ACJQ[debbeb``bdddc`bc`YOGBAAAAAAAAAAAAAAAA@@@@AA@@@?>=<<<;==<<<<<<<;<>=;BL~⸇fQHBDEB?ABAB??@A@BFNW`edaada``adeedacdaYPFBAAAAAAAAAAAAAAAA@@@@AA@@@@>=<<<<===<<<<<;:=?<;?GUfɢyR=?K@B@=?EGDC@@@??CIRZdeb`ac``_`bcbcbdd`XNFAAAAAAAAAAAAAAAAA@@AAAA@@@@>>=<;<<=====<<;;=><;>B>DLW^db``bb`_^_`aa`ccb]UKD?@AAAAAAAAAAAAAAA@@AAA@@@A@?>=<<<<<====<<;<=>=;=?9GD@]ʬa?;FID=A@BFFDBBEB@?==DN^bc`_bdba`_``a`_cb`ZQHB>@AAAAAAAAAAAAAAAAAAA@@@AAA?>======<<==<<<=<==>?@4?\֤vKJE@?BA>@>@DD@BIEC@><=EOadc_`dfbba`aaa``ba^WNFA=AAAAAAAAAAAAAAAAAAAA@@@AAA@?>><==<<====<===;<>@@>22CG>>@@@?>>??@ABBB??AA??FO_`bb``ad``abaabba\TLGC@=AAAAAAAAAAAAAAAAAAAAAAA@BA@@@@?@>==<<<<<<<<<====9998:@HNkຈ]HD===>??>>===>@ABB??AA>>DM^`bca```^_aba``_^XNFCA@?AAAAAAAAAAAAAAAAAAAAAAAABBAAAA@?===<<<<<<<<<====<<<;;=@CD^ṂVB;;=<=?BCB@?><<>@AA@@AA>=AIY]acb`^]aabb`^]\WQF?=>@AAAAAAAAAAAAAAAAAAAAAAAAABBAAAA@?===<<<<<<<<<====<=??=;;:3=Xر}UEDC>??AFKNMLIEA?@BBB@@@A>@BAAAAAAAAAAAAAAAAAAAAAAAABBAAAA@?===<<<<<<<<<====:;>?>=<<<=AS}ףxL<@CDD@@DLTYYWTMEA@BA@A?@A?<=BHNV]`aaa`_^[WTPMGC@>?@A@AAAAAAAAAAAAAAAAAAAAAAAABBAAAA@?===<<<<<<<<<====<<==>????><=QөoR=<><>F?@DNX^^\ZQF@?@?>A?@BA>=ABGNUXZ\\WVSQMIFDA@?@BB@?AAAAAAAAAAAAAAAAAAAAAAAABBAAAA@?===<<<<<<<<<====@?=<=>>>?E@@DNZ`_\]SF?>@?>A?@CC??A@CHLNOQRNLIFCB@@?>>?AA@>AAAAAAAAAAAAAAAAAAAAAAAABBAAAA@?===<<<<<<<<<====?<;<=>;;=BAFO[a_\_TG??AA@A?ADD@?AABEFFFHIHFDA@?@A?>==?@?>AAAAAAAAAAAAAAAAAAAAAAAABBAAAA@?===<<<<<<<<<====:89;>?<:>69DB8=Kvڮ|ECDCA@?ABCCCFNY`bbaXLECCCBAAAAAAAABBAAAAAAAAAAA@@@@@@@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@?><;;<<;<<<<<<<<<<==<<<<<<;;<<<;DrڰrM9MCA@A@ABCCCEKSXYXYQGBACCCAAAAAAAABBAAAAAAAAAA@@@@@@@@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@?><;<<<<<<<<<<<<=<==<<<<<<;;;;<<9EA@AA@ACCDCCFKMMKNHA>?BDDAAAAAAAABBBBBBBB@@@@@@@@@@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@?><;<<<<<<<<<<<<====<<<<;;<<;;;<8:N{|UGF>8?@AABBBCCBBCEEDBFC?=?BCDAAAAAAAABBBBBBBB@@@@@@@@@@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@?<;<==<<<<<<<<;====<<<<;;;<;;;<8@AO^JE=>@@@ABACCCAAABBBA@CB@?@ABBAAAAAAAABBBBBBBBAAAAAAAAAAAA@@@@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@?=<<==<=======<=<<<===<===<<<==:>>?UӋ]HCD9AG@@ABCBAA@AABBBAABBBAA@@@AAAAAAAABBBBBBBBAAAAAAAAAAAA@@@@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@?=<===========<=<<<========<<=<=6:CCU͢nG=>H@I@?AACCA@@?ABBBBBB@ABBA@??AAAAAAAABBBBBBAAAAAAAAAAAAAA@@@@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@A@=<=>>========<<=<<<<=========@BBA???AAAAAAAABBBBBBBBAAAAAAAAAAAAAA@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@B?<<<==<<<<<<<<<<=<=<<<<<<==<<<=?><;:DXkʈG@@@AAAAAAAAAAA@@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@@@A@@AAAAAAAA@@@@@@@@>>>>>>>>>>>>>>>>>====>>=`FAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@@AAAA@@@@@@@@?????????????????????????>?>??>?c´xUHM@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@@AAAA@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@????@@@@HgþxvtrnmjjhgdcbccdgffecdbbbZX]T@:?AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@@AAAA@@@@@@@@@@@@@@@@AAAAAAAAAAAAAAA@AAA@@@AA>JXdfbbfccdefhjkmnqswy{||yvutrpnkjhhfedc`^\[QPMKHFCCBB@@?BBD<<>>>???A<;>DC>:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@@AAAA@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@?????@@??@>>A>;=BDDCBBBABFGILORTVXZ]_abaalnqsuwz{pmhc^XSPJKJJIIIHKKJIHGFEECBAAAAACBCCCCCBCCBAA@??BA@?>?@@DDBCCBABBFFABGIEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@@AAAA@@@@@@@@AAAAAAAA@@@@@@@@@@@@@@@?????@@@@EBAABAAC@@??>>>>>??@ABCDCDEEEDBACEFFFFHIDEEFGIJKHGHHHHHITW]bfkor}~zwromnnkhd`\ZSPLIFC?EEEDEEFFBBBBAABBCCCCBAAABCDCA@AAFFEEDCBAFEDCB@?>>@DFIMQTVWZ^bfikmnqtz~ļ{xsqmgaZURKIEA?@AB???@@@@AAAA@?>==@?>@BCBBCCA@><;:AAAAAAABCBA@@@??AABA@@??A@@@@@@@==>>>>>=AAA@ABBA@?@CDDA=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@@AAAA@@@@@@@@AAAAAAAABBBBBBBBBBBBBBBBBBBBCCCC@DC?AFGCA@?@@@@A@AAABAAA@@@AACDDABCBAABCABBB@><;===>>???<<<;:;=?>??@@AAB??>=:9999=ACA@ACIOV\`emssv|xvsokhed``^][YXWPONLIGFEDDDCCCCCAAAAAAAAAAAAAAAAAAAAAAAA@@@@@@@@@@@@@@@@AAAA@@@@@@AAAAAAAAAAAAAAAAAAAAA@AAA@AAAA@@@AA@@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@@AAAA@@@@@@@@@@@@@@@@BBBBBBBBBBBBBBBBBBBBCCBBBBCCBBBCBBBCCCCCCBBBBCCCBBBBBBBBAAAAAAAA@@AAAAAA????>>>??=<<<=>?=======>;<;;<<<<<<<;<<<<=>@CFIKLVWY]`dfigiknqtwx}yomga\VPMKJIHFEDDCDCCBAA@EDDCBAA@AA@@@@@@AAAA@@@@A@A@A@A@A@A@A@AAAAAAAAAAAAAAAAAAAAAA@@@@@@AAAAAAAAAAAAAAAAAAAAAA@@@AAAA@@@@@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@@AAAA@@@@@@@@@@@@@@@@BBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCBCCCBCBCBBCBCCCCCCCCCCCCCAAAAAAAA@A@@@@@@@@@@@@@@?>===>?@>>>>>>>=<;;<<<<<;;<<<<<<<<==>>>?::;<=>??=>>?@@AAHLRY_fmrx|ü|wkid_ZWUTYWUQNJFEKHGFFECB?????@@A@AABBBBC@@@AABBB????????@@AAAAA@@A@A@A@A@A@A@A@@@@@@@@@@@@@@@@@AAAAAAAAA@@@@AAAAA@A@A@A@A@A@A@AAAAAAA@@@@AAA@@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@@AAAA@@@@@@@@@@@@@@@@BBBBBBBBBBBBBBBCCBBBBBBCCCCCCCCCCCCBCBCBBCBCCCCCCCCCCCCCAAAAAAAA@AAAAA@@@@AAAAAA>><=<>>@=>=>=>=>;<<<<;;<<<<;;;;;==<<;:::<<;;::98>?>>===<<=@CDEGIIKNQSUXZSW[]`cjoy|ɿ|{wpic\VROLIEBA@B??@AACCD?=>@BDDDAAA@@@@@@??@@AAA@@AABCDDAAAAAAABAAAA@@@A@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@AAAAAAAA@@@@AAAAA@@@@@@@@@@@@@@@@AAAA@@@@@A@AA@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@@AAAA@@@@@@@@@@@@@@@@BBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCBCBBBBBBCCCCCCCCDDDDDDDDAAAAAAAA@AAAA@@@ABBBBBBB>=<<<=>?=======>;<<<;;;<;<<<<<<<<<====>>=====>>>88888789@AA@;876>??>:9:;AACBBEIMRTX_fmrvy{ҿxfXLD@B?>>??@?BAA@AA?>BAA@????CDDDCEDEA@?>?>=;@???>>>>>>>>>>>>@@@@AAAABCBBBBBCAAAAAAAA@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@A@A@@@@@AAAA@@@@A@@@@@@@@@@@@@@@@@@@@@@AAA@A@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@@AAAA@@@@@@@@@@@@@@@@BBBBBBBBBBBBBBBAABBBBBAAAAAAAAAAABBBAAAAAAAAAABBCBBBBBBB@@@@@@@@@@@@@@@ABBBBBBBB?>===>?@>>>>>>>><<<<<<<<<<<<<;;<:::;<<==8899:::;??@@@@@@:<>><;;;;<==<<=?9;:;::;<;;:::;;<99999:;;BCISbvιvqf]RG?<:9BA?@CDED?>?@BCBB@?@ACCCB???????>ECAABA@?BBBCCDDEEFEEDDCDBCBBABABAAAAAAAA@@@@@@AA@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@AA@A@AAAA@@@@@@@@A@@@@@@@@@@@@@@@@@@@@AAAAA@A@A@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@@AAAA@@@@@@@@@@@@@@@@BBBBBBBBBBBBBBBBBBBBBBBAAAAAAAAABBBBBAAAAAAAAAAAAABABABAA@A@A@AA@@@A@AAABBBBBABA?>===>?@>>>>>>>>;;;;;;;;;;;;;<;;==<<<;;;>>>====<:::99888;=??>@@A8:;<<<>@==>????>==;:9888=<<;:9889:=DLV`dhp{ègQB>ADC@@DFHJCCA@???>FDBABA?>?@@AABAB@ABCDEEE=<<>@AA@??@AABCCCCCCBBBBEEDDDCCC??????@@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@A@@@@@@@@A@A@AAAAAAAAAAAAAAAA@@AAAA@@@@@@A@A@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@@AAAA@@@@@@@@@@@@@@@@BBBBBBBBBBBBBBBBAAAABBAAABBBBAAABAAAABBBBAAAAAAAAAAAAAAAAAAAAAAA@AAAAAAAA@@@@A@A>=<<<=>?========<;;;;;<<<<<<<<<==<;::;;;:::99??>=====<;;;978:;<=<:9:;88789::9:::;<=>?88:;<<=<<;99@BBI\{ġg\OJHCABDCABB@>=>==>@BBB@?>@AB@@AA@??@ABAABA@???CB@AAA@?BBBBBBBBDDDEEEEFBBBBBAAA@A@A@@@@@@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@@@@@@@@@@@@@@@A@AAAAAAAAAAAAA@@AAAA@@@@@@@@@@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@A@A@A@A@A@A@A@AAA@A@A@A@A@A@A@A@@A@AAAAAAAAAAAAAAAAAAAAA@@@@@A@@@@A@A@A@A@A@A@AABBBBBBBBBBBBBBBBAAAAABAAAAAABBBBAAAAAABBBBAAAAABAAAAAAAAAAAAAAAA@AAAAAAAAAAA@@@@?>=<==>?=====>=>;<;<;;;;;;<<<<<<;;;;;;::;;;<<===:;;<=>>>???><<=?:;<<;;;<><:9:;;=;;9:899:;<<=;;99<;;::<>><>@ADMZf®dTGDA>>?BC@?@@AABBBBAA@AAACCCBAA@???@@@@@@>??@@ABACB@>>@BD=>?@@>>;AAAAAAAAAAAAAAAAA@A@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@@@@@@@@@@AAAAAAAAAA@AAAAAAAAAAAAAAAAAAAAAAAAA@@@@@@@@@@@@@@@@@@@@@@@A@A@A@A@A@A@A@A@@A@@@@@@@@@@@@@@@@@@@@@@@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@A@A@@@@@@@@@AA@@??>>::;<=>>><<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;;<<<<<<<<<<<<<<<<<<<<<==========<=<=<=<=<=<=<<;;;;;<<<<<<<;;;>::<<==>><<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<========================<<<<<<<<<<<<<<<;;>QgṘv_I@???@BCDCBA@@AAAABB@@ABBCDDCCBAA@??A@@@@@@????????@EEDCBBA@BAA@@AAB@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@@AAAAAA@@@@@@@@@@@@@@@@@AAAAAAAAAAAAAAAAA@@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@@@@@@@@@@@@@@AAA@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@@??>>::<<==>><<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<========================<<<<<<<<<<<<<<<;:;=G[tֵV?EHMK?@@@ABBBDDDEEFFGHHHGFDCBCAA??@ABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@AAA@@@@AAAAAA@A@@@@@@AAAAAAAAAAAAAAAAAAAAAAAAA@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@@@@@@@@@@@@@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@@??>>;;<<====<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<========================<<<<<<<<<<<<<<<<9<68AANmҤ{`E=@99BBCBBA@@@@@@AAAABBCCCCCCBB@@@AAABBFFGIJKLMLLMOPQRSOOOONLIIEDB@@ABCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@@@@AAAAAAAAA@@AAAAAAAAAAAAAA@@AAAAAAAAAAAAAAAA@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@AA@@@@@@@@@@@@@@@@@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@@??>>;;<<====<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<F@HcoŜpRHBEF;6=DDCA?>>?@@@AAAABBDDDCCBBBBCDFHIKJPQRTUWXYYYZ[]^_`[[[ZXVTSLKHDBBBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@@@@AAAAAAAAA@@@@@@@@@@@@@@@@@@AAAAAAABBBBBBBB@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@@??>><<======<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<4558;59EKieNE;@@BJHBBBB@?>?@A@@AAAABBCCCCCCCCGHJMPSUXYYZZ[\]]__``aaabbbba`_]]YVQLGD@@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@A@AAAAAA@@@@@@A@@@@@@A@A@A@AA@@@@@@@@@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@A@A@A@A@A@A@A@@A@@@@@AAAAAAAA@@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@@??>><<==<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>E?@@@@@AA@@AAAABBABBCDEDDKMPTY\_accccccccbbbaa```cdccba``c`YRKE@?AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@AAAAAAA@@@@@@@@@@@@@@@@@@@AAA@AAAAAAABBBBBBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@A@A@A@A@A@A@AAA@@@AAAAAAAAAA@@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@@??>><<==<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<@7;;2;B839C[}XKFCDAAA@AAAA>@@@@AAA@ABCCBA@?ADEFJQV_`bcddcbbaaaaabbbbaaaaabaaabcca_c_XPHB>=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@@@?AA@@A@@@A@?@@@AA@?AABBAA@@AA@@???>AAAAAAAAAAAA@@@@ABBBBBBBAAAAAAAABBBBAAAA@@AAAAA?@A@B@A???@?A@A@BBBBBBBBBAAABABBB@@@@@@@@@@@@@@@@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA??@@@@@@>=>==>>><<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;<<<<<<=8;BKk忓`F@?@AAAA@A@AA?@@@@AAAAABCCBAACHNRTWZ^_`abcba`a``aaaabaabbbbbcababcb_]\YSLEA>>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA?>???@@BBAAABBAAAACCBA@@???@@@@?@@@@@@A@?>>>>>>>????>>>?????????????????@@@@@???@@@AA@@A@AABAB@A@A@A@A??BBAA@@@A@AA@????AAAAAAAA@@@@@@@@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA??@@@@@@>>>==>>><<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;<<<@@@??>>>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA??@@@@@@>>>===>><<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;<;<<<<>:<=;@]ͦeR@=BCAA@@@@@AAA@@@AAAABBBBCCBBBPV^decddcbaaa``__^`_``a`a`a`a`a``_^\ZVQNGEDCBABCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@@@??@@AAABAABBB@@@??>@@ABBBA@BABA@@???AAAAAAACCCCCCCCAAAABBBCBBBBBBBBCCCCCCCCCCCBBBBB@@@AABCDAA@AAABBBBBBBBBB???@@AAAAA@@@???@@@@@@@@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA??@@@@@@?>>>===><<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;;;;;;;=:;>;9FXئrMD=>AA?>>??A@@@BBA@@>==>?>====><<=<=<=<>=?>??@?========>>>>>>>>>====<<?@ABB??>>>?@@@@@@@@@?AAAAAAAA@????@AA@@@@@@@@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA??@@@@@@???>====<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;;;;<<<<:;=<:?G_|F@><>@@@>A@@A@@@@AAAABBBBDCCBBCCD[^`aa`bdcbba___`cccbaa``^]]\[ZYYSPMKGD@>@@ABCDDEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@@@@AA@@@@AAABCABBBBCCBABBDEFGHDEEEECCCDEFFFEFGDDDDDDDDCCDDEEFFEDDDDDDDEEEEEEEEDDCCCCBBCBA@@@BBFECBBBCDCBBAA@@@CBAA@@?>@A??????@@@@@@@@BBBBBBBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA??@@@@@@@??>====<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;<<;;<<;<=:9;<;=?AsƊZ=<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;;<;<;<<=<:::==:?ACAA@A@@A@AAABBBBCEDCBBCDEBGLPRUY]_^]\Z[]\XWVTRQPOKJHHEDABCA?>?ABB@@AAAA@?A@AAAAAAA@@@@@AAA@AAAAAAAAA@@@@@@@A@A@A@A@A@A@A@AA@@AAAAA@@AA@@@EFFEFFFFHJMPSVWY`behmqsu{|}ywvljfa\YTRMLKHECAACDDDDDDCBABABAAA@@@A@@AA@@@AAA@AAAAA@A@@@@@@@@@A@A@AAAAAA@@@AAA@A@A@@@@@@@@@@@@@AAAAAAAAAAA@@@@@AAAAAAA@@@AAAAAA@@???@@@A?>==<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;<<;;<;<<<;:9:>>:@9X번N:MBB@@??ABBCBBA@@@?@@@??ABAAAAAAAABBDEGIKKLLLLLLLLGGGGGGGGDDCA@>>>A@?>AABCBBBABA?>@@ACDC@>A@@@BBAA?A@@>???=ABCAA?@ABBC@?>@@A???@ABGJMPSVZ]h`[aijnqstx{~{xtk`XRPPPNNMMBCCA@@@????@BBCACBCCDDEEA@ABCCA@BBBCCBBCAA@@@@??CCBBBBA@@@A@????@@@@@AAAA@@@@@A@A@AAA@@@@@@@AAA@?@??@@@?>???=<<;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;<<<<<<<<<<;;;IBAA@@AAB@???@ABB@AAA@@@AAAAAAAAA>>??@ABCABBBBBBB@@@@@@@@BBBAA@>?A@?>@@AA=<=??@@@DCA@BBCCBA@?AB@AABCB@??@FGHHFEFFEGLORTXYWW[]bdijquyyponlihfeXWVTSPPOTROLJIEC???==<;;@>>>??>>@?????@@A?>?ABCB>>>????>A@ABACBCA@@@AAAA@@@@@@A@A@AAA@@@@@@AAA@@@@@@??@@>>??>=;;;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>?>>??????@@BBB@A????@@A@ABBAAAABAAB<;<<=>?@??>>=>@B?@BAAADE>>???>ABRW_hqyּ}{ywlhea\XURPOMKHEDCBA?@AA@@DBBAAABCBAAABB@?BABCBAAA?@?@@@AA@@@@@@@@@@@@@@@AA@AAAA@@@@AA@AAA@@@@??@@??@?>=;<;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<:@:E[ʙN?GG?@BAACC?>?>>?@@ACEFDB@?EECB@@AC@DJQY`fknsyϲ~|zxurigeb_]ZZ\YTPLJFEEDA@=>?>@@>>@@?>@?>???>>A@A@??>>AAA@@@@@@AAAAAAAAAAAAA@@@AA@AAAA?@@@??@@??@@?><<;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<8C?@ABCCA??@AA@AAAAAAAA@@?@?@AA@AAAAAA@>?>?>???@@@@@@BB@AA@?>>?==>=>ACC=>?@>?AB>????ACEZ_juͮ~vmg_ZWNMJHDCCACBAAAAA?@?@@?@ABBBBBABBB@@@AA@@@@AAAA@@@AAA@@@@@AAA@AAAA?@@@??@@??@@?><<;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<:@@CGKOQSKGA?@AA@AAAAAAAA>??@?@@@??@??????@@@@@@?A@@A@@@AAAA>;:<>A@?<<==>FILPV`ltw}̮}vtspljhf`\WSOIEAA@@@@ADD?AAABBBC@A@@@@@@AAAA@@A@A@@@@@@@@AA@AAAA?@@@??@@@@A@?><=;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;5A:PyN=F>?B@AABBAA@ADJQW\_`WPGBAA@?AAAAAAA@A@@AAAAAABBAAAAAAAAAAAAAA@AAAAA@?BA@?@FKKJKORVY]^dmu|β~wrje^ZSQLIFEFFHFFFECBBAAAAAAAAA@@@@@@A@@@@@@@@@AAAA@@A@@@@??@@@@AA@?<=;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;<3?8Br۸dCAAAAAAAA@A@@@@>?BBAAAABB@@@AA@AAABAAAA@?=@ABEIS[kovͱ|vohda`VUSPKIFDAAA@@@@AAAAA@@@A@@@@@@@@@@AAAAAA@@@@??@@@@AA@?==;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<:@7@7;`WAJA;CCABAAAA@@@HS]bb`^e]RHCBCCCCCCBBBBBA@?@A@>:@DB?ACCCCA?>>>@;=?CGLTXXclor{дqlrupkic_]YTOKFAA@?ACDCD@AA@>@AEA@@ABB?>@AA@??@@?@AAA@>?<===<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;>@ECCC>>@A?CBB>=<<=;AJVcp}ϳxj^RLAA@??>>=;>BEDA<9DB??>@@@@AA@??@@?@AAA@>?<===<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;><5Dc\B=E@@D=BBAAAA@@AGQ[bdcbaZPHCBBCCCCBBBBABCED@?@CCC@=@FEBIOV]fow|дɺ|umf]WSKF@=>ABBDB?@AAAA@AA@??@@?@AAA@>?<===<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;=:9<88ENB@D?@D;BBAAAA@@BFNW_bba\VNGCBBBCCBBBBAAGC?BGE@<<@CBDJJJgmzгǻ~xk_PFDCEE@A@BCCB@@AA@??@@?@AAA@>?<===<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;7=<8><9=~FCB@>@B=BBAAAA@@BDJQX\][VQJEBBAABBBBAAA@FCDGE@@BJYl{гveWMHDA?@??AAA@??@@?@AAA@>?<===<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;4?>9<><>u@@><=?ABBBAAAA@@BBDJPTSROKFBAAAABBBAAAAA:>GIB?L_eyϳrhYJB>@AAAA@??@@?@AAA@>?<===<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;6A=89;:=sӲp?@=<@>?DBBAAAA@@A@?CHKJGHFB@@AA@BBAAAA@?;=ENUa{гÿlZMEB@AA@??@@?@AAA@>?<===<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;:?:8<<8;cʧmCC??D??====<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<=<88@?9:Y˅a@@@?@AAAAAAAAAAAAAAAAAA@@AA@@A@A;DC:;GJB7KlгxUCCCA?>@BCA@A@AA>@>=<;;<<<<<<<<<<<;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<====<<<<;;;;<<<<<<<<<<<<<<<<<<<<<<<<;<;@>=<;;<<<<<<<<<<;;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<===<<<<;;;;<<<<<<<<<<<<<<<<<<<<<<;<<;<<;ObkJA@@@@ABBAAAAAAAAAAAAAAA@A@AA@AAAB<=HE;EZгjIDC>=FCA?@ABA?@>=<;;<<<<<<<<<<;;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<=<<<<;;;;;<<<<==<<<<<<<<<<<<<<<<<<<<;<<;DW`DA@@@@ABBAAAAAAAAAAAAAAA@A@A@AAA@D;=CCGdгŬjK>BDBC@?AAA@??>=<;;<<<<<<<<<<<;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<==<<<<;;<<<<<===<<<<<<<<<<<<<<<<<<<<;;<;=<;;<<<<<<<<<<<<<<<<<<<<;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;====<<<<<<<=====<<<<<<<<<<<<<<<<<<<<;;<;=<;;<<<<<<<<<<<<<<<<<<<<;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;>>>>>===<<====>><<<<<<<<<<<<<<<<<<<<;;<;?FIDA@@@@AAAAAAAAAAAAAAAAAA@AAAAA@@@A=?U|г±`HCB@@@ABA>?>=<;;<<<<<<<<<<<<<<<<;;;;;;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;???>>>>=<<<<<===<<<<<<<<<<<<<<<<<<<<<;<;BBDAA@@@@AAAAAAAAAAAAAAAAAAA@@@AA@A@G=FoгcHC@@AAA@?@>=<;;<<<<<<<<<<<<<<<<;;;;;;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<@@????>><<<<====<<<<<<<<<<<<<<<<<<<<<<;;C?ۚDBA@AAAA@@AAAAAAAAAAAAAAAA?AB@A@@?FL^γnOFACD==B@@?>=<<<<<<<<<<<<<<<<<=<===>?@@AADDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDCCBAA@??>==<<<<;;;;<<=<<<<<<<<<<<<<<;ԕCCAAAAAA@@AAAAAAAAAAAAAAAA@ABAAA???W{ϴűgF?BC=>AA@?>==<<<<<<<<<<<<<<;<<<==<<<<;;;<<=<<<<<<<<<<<<<<;BA@?>>=<<<<<<<<<<<=<<;;;<=<=<<;;<<=<<<<<<<<<<;;<<;<=;ˆAB@AAAAA@@AAAAAAAAAAAAAAA@@ABCB@AERϳȾWD>B@>AAA??>=<=<<<<<<<<<=<<<;;<=EEEFFGGHHIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIHHHGGFEDBA?>=<=<<<<<=<<<<<<<<<<;;<<<<<;{繁A@@AAAAA@@AAAAAAAAAAAAAAAABAADA?BJjγǣnO>BA?AAA@?>>===<<<<<<<<==<=<=?@FGFGIIJIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIJHHHHHHGGGGFDCA@>=>=<<<<<<<<<<<<<;;;;<<<<=DPϵ̴^@@A?@AA@??>===<<<<<<<<===>>?BCGGGGIJKKIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIJGGGGGGGGHHGFDCBA>>>=<<<<<<<<<<<<;;;;<<<<==mޡo>@@AAAA@@@AAAAAAAAAAAAAAA@D@?B?>HTӺȿmC=A@A@A@@?>=>=<<<<<<<<==>?@BEFFGFGIJJJGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGHGHHHHHHHHHGGFEDC??>=<<<<<<<<<<<<;;;;<<<;==f٘i>A@AAAAA@@AAAAAAAAAAAAAAA@C>=A?=IY־xD;<<<<<<<===?AEGHFFFFGHJIGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGHHHHIIIIIHHHGGFEE??>=<<<<<<<<<<<<;;;;<<<;=AAAAA@?==<<=====;>ACFGGGGGGHHHGHGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHGGGGGGGGHHHHHHHIC@=;;;<<<<<<<<<<;;;;<<<<;CS~݈ZE>@@A@A@@@AAAAAAAAAAAAAAAAB@A@C;awƔXKB:F=@@@@A@?>==<<===<=>ADFGGGGGGHHHHHGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHGGGGGGGGHHHHHHHID@=;;;<<<<<<<<<<;;;;<<<<;AMv܂UC?@@@@A@@@AAAAAAAAAAAAAAAA@@A@D?nľ̟hPA;C=@@@@A@?>==<<===<=?ADFGGGGGGHHHHHGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHGGGGGGGGHHHHHHHIEA=;;<<<<<<<<<<<;;;;<<<<;=Fl{MA@@@@@A@@@AAAAAAAAAAAAAAAA@?@?CCxþΩzV?==<<===<>@BDFGGGGGGHHHHHGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHGGGGGGGGHHHHHHHIEA><;<<<<<<<<<<<;;;;<<<<:;AfrH?A@@@@A@@@AAAAAAAAAAAAAAAABAB?BCȾ˯\>?C?@@@@A@?>==<<===A@@@@AA@@AAAAAAAAAAAAAAA@BAA?@Bžȵb>BC?@@@@A@?>==<<===<@ACEFGGGGGGHHHHHGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHGGGGGGGGHHHHHHHIGC?=<<<;<<<<<<<<;;;<<<;;:A@@@A@?>==<<===<@BDEGGGGGGGHHHHHGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHGGGGGGGGHHHHHHHIHD@=<<<;<<<<<<<<;;;<<<;;;>>X_@?B@@@@AA@@AAAAAAAAAAAAAAA@AADBCG¼Ǿk>D@?A@@@A@?>==<<===><<====>AEGGFGGHHHHHHHHGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHGGGGGGGGGGGGGGGHHGEA=;=><<<<<<<<;;;<<<;;;?:]ׯZE@><<====>AEGGFGGHHHHHHHHGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHGGGGGGGGGGGGGGGHHGEA><=><<<<<<<<;;<<<<;;<=8XҦWVjμX=B@A@@@AAAA?><<<==<>AEGGFGGHHHHHHHHGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHGGGGGGGGGGGGGGGHHGEB><==<<<<<<<<;;<<<<;;=<7TʝU>B@CB??AA@?AAAAAAAAAAAAAAAAACB>^}c?@@A@@@AAAA?>=<<<=<>AEGGFGGHHHHHHHHGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHGGGGGGGGGGGGGGGHHGFC?===<<<<<<<<;;<<<<;<<;8R’R?BCCB??AA@?AAAAAAAAAAAAAAA@ABC>fĽÕmC>?A@@@AAAA@?=<<<<;>AEGGFGGHHHHHHHHGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHGGGGGGGGGGGGGGGHHGFD@===<<<<<<<<;;<<<<;<<;9Q{N@BECB??AA@?AAAAAAAAAAAAAAA@B@C=mƠxG??A@@@AAAA@?=<<<<;>AEGGFGGHHHHHHHHGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHGGGGGGGGGGGGGGGHHGGEA>==<<<<<<<<;;<<<<;<<;;Or~J?@ECB??AA@?AAAAAAAAAAAAAAA@BAEGGFGGHHHHHHHHGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHGGGGGGGGGGGGGGGHGGGEB>==<<<<<<<<;;<<<<;<>;;JgxH>?DCB??AA@?AAAAAAAAAAAAAAA@A;C=xľ˰PBBA@@@@AAAA@><<<;:>AEGGFGGHHHHHHHHGGGGGGGGGGGGGGGGGHHGGGGHHHHHHHHGGGGGHHHHHGGGGGGGGGGGGGGGGGGGGHGHGGHFB><=<<<<<<<<;;;<<<;<@;:F_cOC?BBB@?@@@@AAAAAAAAAAAAAAA@=@9H~ƿWC>@@@@@@AA?@@=<<;=?ACFHHHGHHHHHHHHHHHHHHHHLEAEHFGKIIHFGIGEJIJKKIHFJGEFIJKIKKNMFGIBCGHFDDFGGHHHHHGGGHHFC><<::;;;;;;;;;<<<<;==;HV_MB@CBB@?@@@@AAAAAAAAAAAAAAA@?B>9CNXIAADBB@?@@@@AAAAAAAAAAAAAAA@AFAQʣbHB?@AAAABB?@@=<<;=>@CFGHGGHHHHHHHHHHHHHHHHDHIGEEFF@DJJDBN]vĺ~gSIKHGD>8>ESFAADBBA?@@@@AAAAAAAAAAAAAAA@@IFZƾΫlKB?@AAAABB?@@=<<;=>@CEGGGFHHHHHHHHHHHHHHHHFIHDCGIIHEK[guʸ~laSLLLHGEGGEEEGHHHHHGHGHHFC?<<::;;;;;;<;;<<;<<=>9>?}MDBBCBBA?@@@@AAAAAAAAAAAAAAA@>HMgϳwSB?@AAAABB?@@=<<;=>?BEGGGFHHHHHHHHHHHHHHHHEGHHJNMIVNTtšgEBEHEHLKHGKGGHHGGHHGHHFC?<<::;;;;;;<;;<<;<<;><@>x߉HBCCBBBA?@@@@AAAAAAAAAAAAAAA@=FRwͷ]A@@AAAABB?@@=<<;==?BEFGFFHHHHHHHHHHHHHHHHFFHKKGEG\tӴy^MFDHHFGMGGHHGGHHGHHFC?<<::;;;;;;<;;<<;<<:>=B>tځCADECBBA?@@@@AAAAAAAAAAAAAAAA;DYƲͻfC@@AAAABB?@@=<<;==?BDFFFEHHHHHHHHHHHHHHHHJFFJGEQcܭzfTDCGEEHGGGGGGHHGHHFC?<<::;;;;;;<;;<<;<<;=;AACEGHHHHHHHHHHGIIGFFGHMKJLTq޴hKICGHHFHGDGKJEGHHFC?<<::;;;;;;<;;<<;<<=9A=9ot>BB>A@AA@@@@@AAAAAAAAAAAAAAAAABkǵű~BD@?BC@?A?@?=>=;<=>@CEGHHHHHHHHHHHHFEFGHIHJLW|ҝ\FF?DEDFIIFGJIFGHHFC?<<::;;;;;;<;;<<;<<>:A<8kp>BA>@@AA@@@@@AAAAAAAAAAAAAAAADDpǵɾCD@?ACA@B?@?>>=;<=>@CEFGHHHHHHHHHHFDDGHGHEDPvНiA;@;9f޽k@B??@@AA@@@A@AAAAAAAAAAAAAAAAFIxǵǑFEA?ABAAC@@@>>=<<<>@BDFGHHHHHHHHHHEDFIHFGIIi؟lB1=ABIGGEHIHFFHIGHHFC?<<::;;;;;;<;;<<;<<=;?<;c۴hDC?@A@AA@@@AAAAAAAAAAAAAAAAAAGNǵ˗NFB?ABAAC@A@>?><=<=?BDFGGHHHHHHHHHEEIKGEGNk֞hK=7A=>JFDFFGGGFHJGHHFC?<<::;;;;;;<;;<<;<<<;><=<=?BDEFGHHHHHHHHHEFJKFHM[ӝ`;<:9:6Gc]VLGEFGGHJGHHFC?<<::;;;;;;<;;<<;<<;<==B]ӥ`FA;A?AAA@@@AAAAAAAAAAAAAAAAAAHcǵ˥nJDAAA@@BAAA??>==<=?ADEFGHHHHHHHHHFEHGENZٟoJ9@78=Hw~VLEFHHGHGHHFC?<<::;;;;;;<;;<<;<<;=<@A@@@@AAAAAAAAAAAAAAAAAAJjǵ̫zJDAAA?@BA@@?>>=>:=?ACEFGHHHHHHHHHDCECESdܠ^FC<:?DPjǠ^QFFHHFFGHHFC?<<:;;;;;;;;;;<<<<<<>;;BTΖf>HB>ABBABHyıǯZADCACEEFGGGGHHHHGEN@IAZաfJB?<=ABtoQ;IBJFHGGGGEA=<====<<<<<<<<====@<=B:\ȏb>ACA@BNñʵ^CDC;BB@@AA@??<<<=>@CEEFHHGGHHHHHEIFNKxٖkC79:<@KnܕkHMCHDGHHGGE@=<====<<<<<;<<====>;;A9W|\;D@>CA@@A@@@A@A@@@@@@B??BB@@CR;cEDD@CEEFHHGGHHHHJFDLQSͣhL;CQnʼnSPDIDHHHGGEA>=====<<<<<;<<=====::?8Qs|X@CEEFHHGGHHHHKHDONU٠kI=;@>;PmWLELHKHHGHFA>=====<<<<<;<<====<;9?9KiuW>B?AAA@@A@@@A@A@@@@@@BAAAA?AFWƶoHCB>B@@@AA@??<<<=>@CEEFHHGGHHHHHIEMQaҠkJ;9;=====<<<<<;<<====<=9@;GapUAB?B?AA@A@A@@@A@@@@@@A@AAA>BI[ȾȻsJBB>BAB@@AA??<<<=>@CEEFHHGGHHHHDIFK`ؠhID=<:?PxwZBKFFGHGHFC@?====<<<<<;<<====<>9A>CZiQAA?D?AA@@AA@@@@@@@@@@@@AA@?DL`ȼxKAA?CBC@@AA??<<<=>@CEEFHHGGHHHHEGHErϠoK?A:8:MxՙoDFEHGHGHGC@@====<<<<<;<<====:>8@?@TdN@?>E?@AA@AA@@@@@@@@@@@?AB@?FLeǼyJ?A?DCDA@@A?><<<=>@CEEFHHGGHHHHHIHB֞iKC>7<PVMFA>@?@AA@AA@@@@@@@@@@C=C?=BFUsȾQBG>=F?A@@A?><<<==>ADEFGGFGHHHHIMI@ˣqM;69>6Kv~CKIEIHGHGEB@?>=<<;<<<;<<====<;;?<;FQIC@>@@@AA@A@@@A@@@@@@@C=B?=CEXy»ʉZFH=>E?AA@A?>=<<==>ADEFGGFGHHHHGLHBײsB=@36GOtՃEJHFIHGHGEBA?>=<<;<<<;<<====<;;?<A@@AAA@@@@AAAA@@@@C>A??BD\ƼʔfKG>>D@AA@A?>==<==>ADEFGGFGHHHHGIHEХeI9CHA:>yGHFGJIGHHEBA?>=<<;<<<;<<====<<;><B@AAAA@@AAAAAA@@@@B@A?CABcʝoNE>?CAAA@A?>==<==>ADEFGGFGHHHGEIGHܠkEEC=5:ToJFFHJIGHHECA?>=<<;<<<;<<====;<;><=AFCAA?B?AAAA@@AAAAAAAA@@BA@?E@AkǾʢxO@?ABBAA@@@>==<==>ADEFGGFGHHHGDHFIӠfCC:;B9=<<;<<<;<<====;<;==>?DCBB@B?AAAA@@AAAAAAAA@@AA@?H?@sɨQ>@AACAAA@@?==<==>ADEFGGFGHHHGDHFHܣdB:957BQsHFHHKJHIIFCB?>=<<;<<<;<<====;<;==>=ޑAABB@B?AAAA@@AAAA@@AA@@AB@?J>?xĺɮV?AB?BAAA@@?==<==>ADEFGGFGHHHGFJGGեoH:=<<;<<<;<<====;=:<=?;֎>?AB@B@@AAAA@AAAAA@AA@A@D??I>@zʲX@BC=AAAAA@?==<==>ADEFGGFGHHHGFJGEڦoK>:6;8GuFKKGKJIHIGDB?>=<<;<<<;<<====;>:<>?9΃F:E???C@@BBCABAAA@@@A@@BA@@>GFʶWC@C=A@A??>?>><===?ADGIIHHIIIHFGHEӣjG=;<;<;:;;=<<;;<<<<<=8@:=AûE;D??AB?ABBBABAAAA@@A@@BA@@?DFźʽ[B@C=@@A?@>?>=<===?ADGIIHHIIIIHFHE՟zR><9:=RvzMEIIHHHHIIFC?><;:;;=;<;;<<<<<=:?:>AsD?>=<===?ADGIIHHIIIIIFFF͟eO?=>9=IytJEJHHHHHIIFC?><;:;;=;<;;<<<<;=:>:>@p|C=D?@AA?ABBBABA@A@@A@@@B>AE@CIżɯcE>D>@@A?@>?>=<===?ADGIIHHIIIIHFFHٞhE<<;:;;=;<;;<<<<;>;>:>>lݸxC>E?@B@?ABBBABA@@@@A@@@A?ADAGPžȱgG>C>A@A?@>?==<===?ADGIIHHIIIIFFFHuўiG=:;<9IzٛcGGJEHHHHIIFC?><;:;;=;<;;<<<<;>;>:?=gׯtB?E?AB@?ABBBABB@@@@@@@A@AAAAPZǾŶpN>B>B@A??>?==<===?ADGIIHHIIIIFHGGc֞eGB:659HozYFHIFGHHHIIFC?><;:;;=;<;;<<<<:?;=:?;cӦqA?E>AC??ABBBABB@@@@@@@A?CB>CXdſĺyU@A=BA@@?>>==<===?ADGIIHHIIIIFHIGSoϠkH=A>:<;:;;=;<;;<<<<:?<=:@:`ѢpA@E>AC??ABBBABA@A@AA@@A?EC=B_ižž~YBA==<===?ADGIIHHIIIIGJIDH_՝hH@<6BAJoGGEHHJGHHHIIFC?><;:;;=;<;;<<<<:@<=:@:^ӗg@?C@A???ABBBABAAA@AA@@@?CA>DYƻ`BE<@A@@??>==<=<<=@CGHHGGHHHHKDGID]ʤiEC3C3>JtĈQBIKGHIIIIJIFC@><;;;;<;<;;<<<<=><<>:AU}Ӕg@?C@A???ABBBABAA@AA@@@@?B@>E]ûǼdCD==<=<<=@CGHHGGHHHHJGGGEO}۞tI<@5;;OwtIDLKHKIIIIJIFC@><;;;;<;<;;<<<<=><<>:@S}юe@@C?A@??ABBBABA@AAA@@@@>BA?GdƽkDC=?AA@???==<=<<=@CGHHGGHHHHGIGHID]ѠkE3<=;7Or|^DFLJHKIIIIJIFC@><;;;;<;<;;<<<<<><;>;@Pz͇b?@B>A@??ABBBABA@AAA@A@@?AA>DiƿrDB??AA@@>?==<=<<=@CGHHGGHHHGEJFIPAJvۣcL9;D9GIwݝ`QGIKFDGIIIIJIFC@><;;;;<;<;;<<<<;?<;>BA??ABBBABA@AA@AA@@B@B?Cmžƿ{DB@?AA?@>?==<=<<=@CGHHGGHHHGFKDGPABe؞n@<<;;;;<;<;;<<<<:?<:><=Iqz\>BB=BB??ABBBABA@AA@AA@AC@C>AsľýDAA>AA?@>?==<=<<=@CGHHGGHHHGHJEELCBT_ɥdP@1;A;Au׉VMLJIHHIJJIIIJIFC@><;;;;<;<;;<<<<:?=:>=BBBzD@B>AA?@>?==<=<<=@CGHHGGHHHGGIGEFFGFNsoCGC4;@Q{ΘdHKKHGHJKJJIIIJIFC@><;;;;<;<;;<<<<9?=9>=>>=<=<<=@CGHGGHHGHGFEHHELMCHTgDJ9G9>@GobRGMKHEGIHEIIIIKIFC?><;<<;<;<;;<<<<9?=9=>@CA>>@?@@A@BB@@AA@@AA@<<<<=<>=>@DFGGGGGGHHGGHHGGHHKLJIGDDBEMqݩrRKNGGHHHGGFHGGIHHGEC@=;;<;;:::;<<<=<<=@@ViMD>?CA??@@AA@@AB@@@@A@@@A=AFKOF>DA@?>=<=<=<>=>@CFGGGGGGGGHHHHHHHHJIIHGGFFEsک{ZHFHGGHHHGGGHGHIHHGFC@=;;<;;:::;<<<=<<>@@VfJC>?BB@@@@AA@@AB@@@@A@@@A?BGR¿UH?CA@?>=<=<=<>==?CFGGGGGGGGHHHHHHHHGGGHHHHI|ڨpQLJGFGFHHHGGFHGHIHHGFC@=;;<;;:::;<<<<>>=<<>??UaFB>?BCB@@@AA@@AA@A@@AA@@@BAHY¾YJ@EA@?>=<=<=<===?BEGGGGGGGGHHHHHHHHGGGHHHII¡wNGNOJGFFHHHGGFHGHIHHGFC@=;;<;;:::;<<<<>>=<==>>R]CA?>ACB@@@AA@@AAA@@@AA@@>B?G`¾[KBGA@?>=<=<=<===>ADGGGGGGGGHHHHHHHHHHHHHHHG㳁hTGKOJEFFFHHHGGFHGHIHHGFC@=;;<;;:::;<<<<>======@CB@@@AA@@AAA@A@A@@@=C=Ge`NBG@@?>=<=<===<<>ACGGGGGGGGHHHHHHHHIIHHHGGG]|ٹ~aNKLNLECIFFGHHGGFHGHIHHGFC@=;;<;;:::;<<<<=====<<;NU@BA>@CB?@@AA@@AAA@@@A@@A=E=Hk¾fS@C@@?>=<=<===<<=@CGGGGGGGGHHHHHHHHHHHHHGGGIJWzТjOJEIKONKGGJGFGHHGGFHGHIHHGFC@=;;<;;:::;<<<<=====<;:LS?BB>?BB>@@AA@@AAAA@@A@@A>F>JnԽkV??@@?>=<=<===<<=@BGGGGGGGGHHHHHHHHGGGHHHHHFCL_p|ǾgPLL@DHIHJLJDGGGHHHFFHGHIHHGFC@=;;<;;:::;<<<<=====<;9KN@@@@AAAAAAAA@@@@A@@@A@A@E=<<<==>>==?@CEGGFFGHHHHHHHHHHHHHHHHHDEGIKQX\{ż~ZSJFFHIIHGGHHHHHHHGHHGGGEHJHGHIGC@><;;;:9:;;=<<<========<<<<=>>==?@CEGGFFGHHHHHHHHHHHHHHHHHJIIGFILPLUbjmnpruutpi]TOTNIGGIKKGGHHHHHHHHGGHGGGDHJHGHIGC@><;;;:9:;;=<<<========<<=<=>>==?@CEGGFFGHHHHHHHHHHHHHHHHHMMJFDCEFFILLKHGFJJJIHEDDKJHHHJJJHHHHHHHHHGGGHGGGDHJHGHIGC@><;;;:9:;;=<<<=======?>=<<=<=>>==?@CEGGFFGHHHHHHHHHHHHHHHHHLMJHDDGIKIIIIJIHEEEEEFGHEFGHJIIHHGHGHGHGGGGGHGGGDHJHGHIGC@><;;;:9:;;=<<<=======?>=<<=<=>>==?@CEGGFFGHHHHHHHHHHHHHHHHHJJJGFFHKHECDFHHHHGGGFFEEEGIKJIGFGHGHGHGHHHGGGFFFDHJHGHIGC@><;;;:9:;;=<<<=======?>=<<=<=>>==?@CEGGFFGHHHHHHHHHHHHHHHHHJJJHFFFHLJGGHIIIIIJJJHGFFHKJHFEDHHHHHHHHHHGGGFFFDHJHGHIGC@><;;;:9:;;=<<<=======FCW˴hBA@>?>=<<=<=>>==?@CEGGFFGHHHHHHHHHHHHHHHHHIIJHFEEGHHGFEEEEEFFFGEEDGJKJGFFGHHHHHHHHHHHHGFGGDHJHGHIGC@><;;;:9:;;=<<<=======ECW˴ĿiBB@>?><<==<=>><=?@CEGGFFHIHHHHHHHHHHHHHHHHIIIIGFGHEFHHGFGHJJJJKJJKGJKIGFGJGHHGGGGGGHHHGGGGDHJHGHIGC@><;;;:9:;;=<<<========FӟM?@AAAABB@@@@@@A@@@AAA@BB?BBU˳¿q@D>==;<;<=<>>?BDGIHFGGGGGGGGGGGGGGGGGHHHHHGHHGGHHGHHHHIIIIIIIHIHHHHHIIHHHHHHHHHHGHHFGGGJLIHIIHEB?===<<;;;<<<<<========KМL?@AAAABB@@@@AAA@@?@AAAAB=BCX̳¿q@E=>=;<<<;?=>=<=>?BDGHHGGHGGGGGGGGGGGGGGGGGHHHGHGGGGHGHHHHIIIIIIIIIIIIIIIHGHHHHGGGHHGHHFGGGJLIHIIHEB?===<<;;;<<<<<========J̘L?@AAAABB@@@@AAA@@@@AAAAB=@C\̳tAC===;<<<;?=>=<=>?BDGHHGGHGGGGGGGGGGGGGGGHHHGHHHGHHGGHHGGGIHIIIIIIIIIIIIIIHGGHHHHHHHGHHFGGGJLIHIIHEB?===<<;;;<<<<<========IƔL?@AAAABB@@@@AAA@@@@AAAAB=AD_̳xBB<=<;;<==<=>?BDGHHGGHGGGGGGGGGGGGGGGHHHHGGHHHHHHHGHHGIHIIIIIIIIIIIIIIHHHGGHHHHGHHHFGFFJLIHIIHEB?===<<;;;<<<<<========HK?@AAAABB@@@@AAA@@@@AAAAA>@Db̳|D?=<;;;<==<=>?BDGHHGGHGGGGGGGGGGGGGGGHHHHHHGGHHHHGGHHHIHHHHHHHHHHHHHHIHHHHHGGGGGHHHGFFFJLIHIIHEB?===<<;;;<<<<<========H컋K?@AAAABB@@@@AAAA@@@AA@BA@@Df̳¿E><;;;;===?=>=<=>?BDGHHGGHGGGGGGGGGGGGGGGHHHHHHHGGGGGGHHHHIIIIIIHIHHHHHHHHHHHHHHHGGGHHHGFFFJLIHIIHEB?===<<;;;<<<<<========G{궇K?@AAAABB@@@@AAAA@@@AA@BBC@Cf̳F>;<;;;=>=?=>=<=>?BDGHHGGHGGGGGGGGGGGGGGGHHHHHGGGGGGHHHHHHHIIIIIIIIIIIIIIIGGHHHHHHHHHHHGGGFJLIHIIHEB?===<<;;;<<<<<========Fx괅J?@AAAABB@@@@AAAA?@AAA@BAD?Dh̳¾E=;<<;;=>=?=>=<=>?BDGHHGGHGGGGGGGGGGGGGGGGGGGGGGGGGGHHHHHHIIIIIHHHHHIIIIIIHHHHHHHHHHHHHGGGFJLIHIIHEB?===<<;;;<<<<<========FuzJ?@AAAABBAA@@AA@@@@AAA@A@A@Pn̳Q?;;;;;<<;?=>=<=>?BDGHHGGHGGGGGGGGGGGGGGGGMIEEGIIHMKHFFHJLQUY[XQJEEKJEELMGGJLJHGFEHHHHHGGGFJLIHIIHEB?===<<;;;<<<<<========CqyJ?@AAAABBAA@@AA@@@@AAAA@@BBTs̳TA;;;;;<<;?=>=<=>?BDGHHGGHGGGGGGGGGGGGGGGGADHKKKLODHMQUZ^adgkljd\XYVSOJFEFOKECEJLLHHHHHGGGFJLIHIIHEB?===<<;;;<<<<<========CpxJ?@AAAABBAA@@AA@@@@AAAA@@CAXy̳ZD;;;<<<<;?=>=<=>?BDGHHGGHGGGGGGGGGGGGGGGGQMHJNMKETbvĽvdPKODFKNMGC@HHHHHGGGFJLIHIIHEB?===<<;;;<<<<<========BnߩuJ?@AAAABBAA@@AA@@@@AAAA@@D@]̳^F;::<<;;;?=>=<=>?BDGHHGGHGGGGGGGGGGGGGGGGIGDDJVdmѼqZOKIJGDBEMHHHHHGGGFJLIHIIHEB?===<<;;;<<<<<========AkޥrI?@AAAABBAA@@AA@@@@AAAA@@D?b̳aI:::<<;;;?=>=<=>?BDGHHGGHGGGGGGGGGGGGGGGGBILLPeȢ_TIHLKLLHHHHHGGGFJLIHIIHEB?===<<;;;<<<<<========@hޣoG?@AAAABBAA@@AA@@@@AAAA@AB>g̳cJ:;:<<;;;?=>=<=>?BDGHHGGHGGGGGGGGGGGGGGGGIJPbٿ|YNNMFAGHHHHGGGFJLIHIIHEB?===<<;;;<<<<<=======k̳fK:;:<<;;;?=>=<=>?BDGHHGGHGGGGGGGGGGGGGGGHINf쾇`MEHPGHHHHGGGFJLIHIIHEB?===<<;;;<<<<<=======<>dޠlD?@AAAABBAA@@AA@@A@@@@AAAF>o̳½hL:;;;<<<;>>>=<=>?CDGHHFGGHHHHGGGGGGGGGGHHXwȨ~WHKGHGGGGGGGJLIHIIHFB>===<<;;;<<<<<=======<><<<<;<=>BCFGFGFGHGHHHHIHHFFIKMPRz[JGBHOHAFIGJKIHJIGGD?>==<;<<<<<<<<<<<<===>><<<<;<=>BDFGGGGGGHHHHIHHIFHJGJ`zٰWPFEHHJMHGJKIHJIFGD@>==<;<<<<<<<<<<<<====@\fGCBAABCBAAA@@AA@@@A@AA@AABDv˲¿ºvO==<<=>>><<<<;<=>BDFGGGGGHHHGHHIIHDGKJZWJDEDKLDGJKIHJIFGD@>==<;<<<<<<<<<<<<====@\fGCBAABCBAAA@@AA@@@A@AA@AAACx˲¿yQ==<<===><<<<;<=>BDFGGGGGGHGHHHHHIFFL[~ݸnVIJHFHFGJKIHJIFGD@>==<;<<<<<<<<<<<<====@\eFCBAABCBAAA@@AA@@@A@AA@AA@Cz˲}S>=<<====<<<<;<=>BDFGGGGHHHHHHHHILKHPsoLKKGGHGJKIHJIFGD@>==<;<<<<<<<<<<<<====@\dECBAABCBAAA@@AA@@@@@A@A@@?D|˲þT>>=<====<<<<;<=>BDFGGGGHHHHHHHIHJJJWܒVGIHFEGJKIHJIFGD@>==<;<<<<<<<<<<<<====@\dECBAABCBAAA@@AA@@A@@A@A@@?D}˲ºU=>====;<<<<<;<=>BDFGGGGHHHHHHHIHEFHV}XGFHBGJKIHJIFGD@>==<;<<<<<<<<<<<<====@\cDCBAABCBAAA@@AA@@A@A@@A@@=D~˲V=>>===;<<<<<;<=>BDFGGGGHHHHHHHIIEFEMeޭvKEKHGJKIHJIFGD@>==<;<<<<<<<<<<<<====@\aEAAAAAAAAAA@@AA@@A@A@@A@@ABz˲¼Y<=<<<<<=<<<<<<==BDFGGGGHKEFHDHKEJEBEGPbtޕQIGHGIJJHHHIGEB?==<<====<<<<<<<<====>T`EAAAAAAAAAA@@AA@@AAA@AA@@AD{˲¼Y<<<<<<<=;<<<<<==BDFGGGGGJGFGEHJFCBCB>T^EAAAAAAAAAA@@AA@@AAA@AA@@@D|˲Y<<<<;<<=;<<<<<==BDFGGGGGHGGHHHHHKFA@?==>CDQi]IAGIJJHHHIGEB?==<<====<<<<<<<<====>Tߌ\DAAAAAAAAAA@@AA@@AA@@AAAA@E~˲ľZ<<<<;<<=;<<<<<==BDFGGGGHFHFGJGGNMC737>?<>:9DZvߢkG>GIJJHHHIGEB?==<<====<<<<<<<<====>TފZDAAAAAAAAAA@@AA@@AA@@AAAA?F˲Z<<<<;<<=;<<<<<==BDFGGGGHEJGFLGFTaaZK@?A?A>=>>AMZ|MEGIJJHHHIGEB?==<<====<<<<<<<<====>TއXDAAAAAAAAAA@@AA@@AA@@AAAA?F˲Z<<<<;<<=;<<<<<==BDFGGGGHCKGFMGG^jODA<<>A<7T݅VDAAAAAAAAAA@@AA@@AA@@AAAA>G˲[<<<<;<<=;<<<<<==BDFGGGGHCNFENGJe̦n\LA>@A?BIEAFVtaNGIJJHHHIGEB?==<<====<<<<<<<<====>T݄UCAAAAAAAAAA@@AA@@AA@@@A@A?G˲[<<<<<<==;<<<<<==BDFGGGGHCOFCOHLj׹lT@;<:78;=CHGKbzpWGIJJHHHIGEB?==<<====<<<<<<<<====>TނSBAAAAAAAAAA@@AA@?@@@@@AA@BE˲]==<;<<=<;<<<<<==BDFGGGGHJEFFMHMvݳoS=9A>=:9=DJLsSGJKIHJIFGGD?=?><<<<<<<<<<<<<====;M|ށSBAAAAAAAAAA@@AA@?@@@AAA@@AE˲¿]==<;<<=<<<<<<<==BDFGGGGHHDGELHP{ȡyVA7@BA;67@K7AUtWFJKIHJIFGGD?=?><<<<<<<<<<<<<===<;M|ށSBAAAAAAAAAA@@AA@?@@@AAA@@@E˲þ^==<;<<=<<<<<<<==BDFGGGGHGEHFJGTǮwVH=?@>=H\y̝ZFJKIHJIFGGD?=?><<<<<<<<<<<<<===<;M|ށSBAAAAAAAAAA@@AA@?@@@AAA@@?F˲Ŀ`==<;<<=<<<<<<<==BDFGGGGHFEIFIGV׿uZNJB:8=>?<<<<<<<<<<<<<<===<;M|ށSBAAAAAAAAAA@@AA@?@@A@AA@@>G˲¼a==<;<<=<<<<<<<==BDFGGGGHFFIFHGXţ_GAE:=CD?;@IQf٨YGJKIHJIFGGD?=?><<<<<<<<<<<<<===<;M|ށSBAAAAAAAAAA@@AA@?@@A@A@@A>G˲b==<;<<=<<<<<<<==BDFGGGGHGFIEHHXȪiXM@;=@<86:=I^{اXGJKIHJIFGGD?=?><<<<<<<<<<<<<===<;M|ށSBAAAAAAAAAA@@AA@?@AA@A@@A>H˲c==<;<<=<<<<<<<==BDFGGGGHJFHEHIWܱ\H9;AB@A@:9@IWo֥[GJKIHJIFGGD?=?><<<<<<<<<<<<<===<;M|ށSBAAAAAAAAAA@@AA@?@AA@A@@A=H˲d==<;<<=<<<<<<<==BDFGGGGHKHGDHIW̴|eP?8;B;;?>;J_y֦]GJKIHJIFGGD?=?><<<<<<<<<<<<<===<;M|܀PE@@AAAABBAA@@AA@?@AA@A@@A>I˲b?=<;=>><<<<<<<==BDFGGGGHJGJFFL[伒qYJE>;@S|њ\DHKIGIIGGGD?=?><<<<<<<<<<<<<===><<<<<<<==BDFGGGGGIEKFFKX{ƧlRB2/6<@E=AHP\p컈WDHKIGIIGGGD?=?><<<<<<<<<<<<<===><<<<<<<==BDFGGGGGGELFGJSrëu_PG@=<8>B>9CdثXCHKIGIIGGGD?=?><<<<<<<<<<<<<===><<<<<<<==BDFGGGGGFELFGIMiվpO@?E?@AA>>EOWjuQCHKIGIIGGGD?=?><<<<<<<<<<<<<===<>EvxID@@AAAABBAA@@AA@@@A@@@AA@=L˲c?=<;=>><<<<<<<==BDFGGGGGFELFGHI`ӵ|fYD=:=A=74>CJiyn^FDHKIGIIGGGD?=?><<<<<<<<<<<<<===<><<<<<<<==BDFGGGGGFFKEHHGZҡ|YM@<=>@@<@9?EGOIDHKIGIIGGGD?=?><<<<<<<<<<<<<===<;BssED@@AAAABBAA@@AA@@@A@@@AA@=N˲d?=<;=>><<<<<<<==BDFGGGGGGFKDHIEWjĪoWG>:9;C989@NKCHKIGIIGGGD?=?><<<<<<<<<<<<<===<9AsrED@@@@AABBAA@@@A?@@AAA@A@A>N˲d?=<;=>><<<<<<<==BDFGGGGGGGKCHJFUSԨq_NB9E>@FJO?CHKIGIIGGGD?=?><<<<<<<<<<<<<====9ArsC@@AAAAAAAAAAAA@A@@@@A@AA@?C˲b?=<;=>><<<<<<<==BDFGGGGHGGHHHHGHM~`A:=ABIKBFIIGFHHFGGD?=?><====<<<<<<<<====;>osCA@AAAAAAAAAAAAAA@@@@AAA@@@C˲b?=<;=>><<<<<<<==BDFGGGGHGGHHHHGHImܽkXLB@C@GIIGFHHFGGD?=?><====<<<<<<<<====;>osCA@AAAAAAAAAAAAAA@@@@AAA@@@B˲b?=<;=>><<<<<<<==BDFGGGGHGGHHHHGHMUקy^LDIKGIIGFHHFGGD?=?><====<<<<<<<<====;>osCA@AAAAAAAAAAAAAA@@@@AAA@@AB˲b?=<;=>><<<<<<<==BDFGGGGHGGHHHHGHRMhtWGAEIGIIGFHHFGGD?=?><====<<<<<<<<====;>osCA@AAAAAAAAAAAAAA@@@@AAA@@BA˲b?=<;=>><<<<<<<==BDFGGGGHGGHHHHGHHITxeOHDFFGIIGFHHFGGD?=?><====<<<<<<<<====;>osCA@AAAAAAAAAAAAAA@A@@AA@@@DA˲b?=<;=>><<<<<<<==BDFGGGGHGGHHGHGH@HJY͑^QNKIFFIIGFHHFGGD?=?><====<<<<<<<<====;>osCA@AAAAAAAAAAAAAA@A@@AA@@@DB˲b?=<;=>><<<<<<<==BDFGGGGHGGHHGGGGFEFLfԑiLGFDB?FIIGFHHFGGD?=?><====<<<<<<<<====;>osCA@AAAAAAAAAAAAAA@AAAAA@@ADB˲b?=<;=>><<<<<<<==BDFGGGGHGGHHGGGGMA?GOhԦkTHJHGJHFIIGFHHFGGD?=?><====<<<<<<<<====;>ovGD@AAAAAAAAAAAAAA@AAA@A@@A>G˲b?=<;=>><<<<<<<==BDFGGGGHGGGGGGHGKLMICOzҟyMLKJIHHGGHHHGGGGGGD?=?><====<<<<<<<<====G˲`?=<;=>><<<<<<<==BDFGGGGHGGGGGGHHCDGHCG\tsFNLKIIHGGHHHHGGGGGGD?=?><====<<<<<<<<=====EvvGD@AAAAAAAAAAAAAA@AAA@A@@A>E˲^?=<;=>><<<<<<<==BDFGGGGHHHGGGGHHFCFJIFGNqǛqUHMLKHHHFFHHHHGGGGGGD?=?><====<<<<<<<<=====EwvGD@AAAAAAAAAAAAAA@AAA@A@@A?E˲]?=<;=>><<<<<<<==BDFGGGGHHHGGGGHHLGEJKHFHOcmOGJLKIIHGFFHHHHGGGGGGD?=?><====<<<<<<<<====>FwvGD@AAAAAAAAAAAAAA@AAA@A@A@@E˲]?=<;=>><<<<<<<==BDFGGGGHHHGGGGHHIFEFHFGHMF\֪wZSOKFJJIHFFFFHHHHGGGGGGD?=?><====<<<<<<<<====>FxvGD@AAAAAAAAAAAAAA@AAA@A@A@AD˲û^?=<;=>><<<<<<<==BDFGGGGHHHGGGGHHDEHIHGFFLDIYlưvO@EKLNJIHGFFGGHHHHGGGGGGD?=?><====<<<<<<<<====?GyvGD@AAAAAAAAAAAAAA@AAA@A@A@BE˲º]?=<;=>><<<<<<<==BDFGGGGHHHGGGGGGFHJKJIHGFHJNRUgԪ~bRPRUQEDHHGFFEFFGHHHHGGGGGGD?=?><====<<<<<<<<====?HyvGD@AAAAAAAAAAAAAAA@AA@@A@@CD˲º\?=<;<>=<<<<<<<==BDFGGGGHHHGGGGHHIIFFGIJIGJFFLDG_jʻr[RJFEHFBFNGGFEEFFGHHHHGGGHGGC>=??=====<<<<<<<<===<@Hy|KC@AAAAAAAAAAAAAAAAAA@@@A@>J˲Y;<<;<===<<<<<<==BDFGGGGHGGGGGGGGGGGGGGGGGGHHHGHHJMR_rzaMHIKJHIHHHGGGHHHIHHGFHGGGHGGGHGB<<>>;<<<<<<<<<<<<===J˲Y;<<;<<==<<<<<<==BDFGGGGHGGGGGGGGGGGGGGGGHHHHHHHHIJKOWajorsttrlgd\UMIJMLIHHHHHGGGHHHIHHGFGGGGHGGGHGB<<>>;<<<<<<<<<<<<===H˲Y;<<;<<==<<<<<<==BDFGGGGHGGGGGGGGGGGGGGGGHHHHHHHHEEEFGHHHIJJHGEDBCEGKLKIFGGGHHHHHHHHIHHGFGGGGHGGGHGB<<>>;<<<<<<<<<<<<===>;<<<<<<<<<<<<===>;<<<<<<<<<<<<===>;<<<<<<<<<<<<===>;<<<<<<<<<<<<===>;<<<<<<<<<<<<===>;<<<<<<<<<<<<===<>WۆWA@AAAAAAAAAAAAAAA@A@@A@@@@Fx˲W<;<<===<<<<<<<==BDFGGGGHGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHHGHGHHHHGGGGGGGGGGHHHHHHHHHHHHHGGGHGFFHHGFHGB<<>>;<<<<<<<<<<<<===>;<<<<<<<<<<<<===>;<<<<<<<<<<<<===>;<<<<<<<<<<<<===>;<<<<<<<<<<<<===>;<<<<<<<<<<<<===>;<<<<<<<<<<<<===Ik̳wO?<<:<<<;<<<<<=>>CEHIHFGGGGGGGGGGGGGGGGGGGGHHGGHGGGGGGHHGGHHHGGHGGGGHGGHGHHHHHHHHHHHHHHHHHHGHJKIHHD?<===<<<<<<<<<<<<<===<<:;<;;=<<<<=>>CEHIHGGHGGGGGGGGGGGGGGGGGHHHGGGGHGGGHHHHHGHGGGHGHHHHHGHGGHHHHHHHHHHHHHHHHHGHJKIHHD?<===<<<<<<<<<<<<<===<@^bHCB@@AAA@AAAAAAAAAA@@@AA@=Ed̳nM?=;:;<;;=<<<<=>>CEHIHGGHGGGGGGGGGGGGGGGGHHHGHHHHHHHHGGGHHHHGGHGHGHHGHHHHHHHHHHHHHHHHHHHHHHGHJKIHHD?<===<<<<<<<<<<<<<===>CEHIHGGHGGGGGGGGGGGGGGGHHHGHGGHGHHHHHGHGHHHGHHGHHGHGHHGHHHHHHHHHHHHHHHHHHHGHJKIHHD?<===<<<<<<<<<<<<<===>CEHIHGGHGGGGGGGGGGGGGGGHHHHGGGHHHHHHHGGGHHHHHHHHHHHHGGHGHHHHHHHHHHHHHHHHHHGHJKIHHD?<===<<<<<<<<<<<<<===<@cbHCB@@AAA@AAAAAAAAAA@A@@A@B@T{̳VE?=;;;<;;=<<<<=>>CEHIHGGHGGGGGGGGGGGGGGGHGHHGHHHHHHGGGGHGGHGHGHHGHGGHHHGGHHHHHHHHHHHHHHHHHHGHJKIHHD?<===<<<<<<<<<<<<<====;;;<;;=<<<<=>>CEHIHGGHGGGGGGGGGGGGGGGHGHHHGGHHHHHHHGGHGHGHGHHGGGHGHHHHGHHHHHHHHHHHHHHHHHGHJKIHHD?<===<<<<<<<<<<<<<===<<;;<<;<<<<<=>>CEHIHGGHGGGGGGGGGGGGGGGGGGHHGGGGHGHGHHGHHHGGGGHHGGGHHHGHHHHHHHHHHHHHHHHHHHGHJKIHHD?<===<<<<<<<<<<<<<===JmɰG><;;;<==;<<<<<=>>CEHIHGGHGGGGGGGGGGGGGGGGIHHFGGGFJKJHEFKRHJIIHGGHGHHGFHIIHHHHHHHHHHHHHHHHHHGHJKIHGD><<<<<<<<<<<<<=======JkʱG>=<;;<==;<<<<<=>>CEHIHGGHGGGGGGGGGGGGGGGHHGHHHIIGEHKNU[bhpmjc\RIDGFFFFFGEHHHHHHHHHHHHHHHHHHGHJKIHGD><<<<<<<<<<<<<=======Hh˲F><<;;<==;<<<<<=>>CEHIHGGHGGGGGGGGGGGGGGGGFFGHJIHIDIR]jt{}~ypcVLHGFGGGFFGHHHHHHHHHHHHHHHHHGHJKIHGD><<<<<<<<<<<<<=======<;:;<==;<<<<<=>>CEHIHGGHGGGGGGGGGGGGGGGGGFHJIGGHITbp{~ui`LHEGJIHGHHHHHHHHHHHHHHHHHHGHJKIHGD><<<<<<<<<<<<<=======E`ʹwA><;:;=>=;<<<<<=>>CEHIHGGHGGGGGGGGGGGGGGGHHGHIGCCGOauysUJDFJHHIHHHHHHHHHHHHHHHHHHGHJKIHGD><<<<<<<<<<<<<=======<;:;=>=;<<<<<=>>CEHIHGGHGGGGGGGGGGGGGGGHIGGIGBEKWlaQFEGGFIHHHHHHHHHHHHHHHHHHGHJKIHGD><<<<<<<<<<<<<=======?<;:<=>=<<<<<<=>>CEHIHGGHGGGGGGGGGGGGGGGHIGFJIDHRewt_LHIFFJHHHHHHHHHHHHHHHHHHGHJKIHGD><<<<<<<<<<<<<=======@=;;;<>=<<<<<<=>>CEHIHFGGGGGGGGGGGGGGGGGHKFGKIFLWriRLKGHJHHHHHHHHHHHHHHHHHHGHJKIHGD><<<<<<<<<<<<<=======;=<======<<=<;;=<=<====<<=<;<<<<====<<=<;<<<<<<<<<<<<<===JӞNAAAAAAAAAAAAAAA@@@@@?@@@9<=====<<<==<;=ADHHFFGGHHHHHHHHHHHHHHHHHHHHHHHHGGMZl{tbROKHGHGGGHHHHHHHHHHHHHHHHGFFHIHGE@;<=;;><<<<<<<<<<<<<===K֡OAAAAAAAAAAAAAAA@?@@?@?@@@BAAOh}}pnokgfjkjkjkjkijijijijijijijijijijijijjkjkjkjkjkjkjkjkjjijiiijkklkljjjopppnnnnoppppoooppppppppppppppppppppppppooooooooqqqqqqqqqqqqqqqppoopnic`]\]\]\\]aabababacbcbcbcbcbbbbbbbbbbbbbbcbbaaaaabdbccablwiG=:======<<<==<;=ADHHFFGGHHHHHHHHHHHHHHHHHHGHGGGGHHHMVcr~|wn`SOLJHGGHHHHHHHHHHHHHHHHHHHGFFHIHGE@;<=;;><<<<<<<<<<<<<===KۥQAAAAAAAAAAAAAAA@>>>>>???ABB>=?=;======>>=======================================>==<<==?>A@A@@@@A@A@A@A@A@A@A@A@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@A@@@@E>=?@=;;==<=<=<=>?>?>?>?>?>?>?>?>?>?>?>??@?@?@?@>?>?>?>?????>>>?>?>>==<<>=>==<<<===<<<<<==<=<=<=<=<=<=<==<=<=<=<<;<;<;<;>?>?>?>?>?>?>?>?:<>>=<<<999:99:::99999999999999:;:;:;:;::9:9:9:9::;;:::;8:;=?@QkeE=;======<<<==<;=ADHHFFGGHHHHHHHHHHHHHHHHHHHGGHHGGGJHJS^iprzxtlbWOGMIHFFHGHGHHHHHHHHHHHHHHHHGFFHIHGE@;<=;;><<<<<<<<<<<<<===LߩRAAAAAAAAAAAAAAA@========:=@?>>;9====<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<=======<===>=>??>=>>>>=>>===============>>?>?>?>?>?>?>?>?>?>?>?>?>?>?>?>>>>=====::<>@>=?<<<<<<<<========================>>>>>>>>==========>=>>=>=>>>=<<;<<<;;::::::9999999999999999999999999999999999999<<<<<<<<<<<<<<<<9;==<<@A;;;<;<;;9999999988888888::::::::99999999;;:;:;:::<89:9Jl`B?<<<====<<<==<;=ADHHFFGGHHHHHHHHHHHHHHHHHHHGHHHHGHHIHKMSWZ_^\XQKGFJHGEFHHGGHHHHHHHHHHHHHHHHGFFHIHGE@;<=;;><<<<<<<<<<<<<===NSAAAAAAAAAAAAAAA@>>====>>==;;>>>===>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>?@@@?:;:;;:;:;;:;:;:;:;:;:;:;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<9:9:::99:<==;;:=========<<<<<<<<<<<<<<<<<<<<<<<<================<<;<;;<;>=>>=<=====>====;<;;<;<<<<;<;<;<;<;<;<;;;;;;;;;;<<<<<<<<>?>?>?>?>?>?>?>>??>===>????>?>>?????????>>>>>>>>>?>?>?>?>?>?>?>?@?@?@@@@?A<=@:Jt\??=<<<===<<<==<;=ADHHFFGGHHHHHHHHHHHHHHHHHHGGHHHHGHFHHIGFIKHGIJIFHKJHGFGIHGGHHHHHHHHHHHHHHHHGFFHIHGE@;<=;;><<<<<<<<<<<<<===OTAAAAAAAAAAAAAAA@=<<<====@><978:=<;:::999::::::::::::::::::::::::::::::::::::::::89:::899=<<==<<=================>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>=<=<<===<;;;;889:<;;;;;;;::::::::::::::::::::::::::::::::;;;;;;;;::;:;;:;;<<==<=<=====>=?<=<<=<==================================>>>>>>>>>>>>>>>=?>>===<;@?@?@?@@;;;;;;;;;;;;;;;;:;:;:;:;;<;<;<;<<;<<<<;<:<68@8GvW<==<<<===<<<==<;=ADHHFFGGHHHHHHHHHHHHHHHHHHHHHGHGGGDGGHFFGHDBEIIGHJIGFHIIGFGHHHHHHHHHHHHHHHHGFFHIHGE@;<=;;><<<<<<<<<<<<<===OTAAAAAAAAAAAAAAA@====<=<<::=??=::<;::9998:::::::::::::::::::::::::::::;;;<<<<<;<;<<<<;;::<<==<==<================;;;;;;;<;<<<<<<<<<<<<<<<<<<<<<<<=<<=====<=?=<=>=;;:::::::;;;;;::;;;;;;:::::::;;;;;::::::;<<<<<;;;;;:::;:99;:;;;<;:<;;<<<;;;;;;;;<<<<<<<<<<<<<;;;;<;;;;;;;;;;;;::<<<<<<<<<<<<<<<<;99<=><:<===<<==@@?@?@?@@A@A@A@A@@@@??@@AAAAAAAA?>>??????C:ADGGFFFGGGGHHHHHHHHHHHHHHHHHHGHGGGGFFGHIIFKGGJKFDGJIHGIHGGHHHHHHHHHHHHHHHHHGFFHIHGE@;<=;;><<<<<<<<<<<<<===PSDA@BA??BAAABCB@==<;;;;;;<;<;;<;<================================;<<====>>6::;;::::=9:=;7:A;;<<86899:===;;:<<<<<<<<<<<<<<<<<<;<<<<<<<<<<<<<=7;@=;=:><===;;=@?;9<=<:========?;8===:=:?>;;>@==<<=<=<==<98;>=;==97;AA>E<;?>88=<===<==<;C=58<=8B================7=@;89;;??=:<>><>???????A@=<>@@>A;9<=:::<;><@BDHHGFGFGHGGGGGGGGGGGGGGGGHHGHHHHGHHGGGGGGHHGGHHHHHGHHHHHHHHHHHHHHHHHHHHHHEFGGHGFE@>;:<<<<<<<<<<<<=>:;@<;BYUDA@AA??BAAABCB@=<=<;;;;<<<<<<<<<================================;<====<;968<;?@;=:::<;<<>?@=867:::::::::89;=;9:=6988=@@><<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<@;;??999<>???<<==<<=<======<<<<D=<==<====<;==>;;;?>@?<<><79;==?@?<====<====><>A=;>::A;<@8?:>?:=;6>===============<>?>=>@?<=????@@==>>>>>>>@><<=>>=;:A8?5;;=<==<<=ADIIGFGGGHGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHEFGGHGFE@>;:<<<<<<<<<<<<<>:;@=;D[XFA?AB@@AAAABCB@=<=<;;;;<<<<<<<<<================================<<====<>:<;=<;9<<<8<=>=>?@A;;;;;;;;9;=<<>>;==<<;<;;>;99=>><<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<:<<:8:?><<=======<<8:>;:48;=?><=>=;=========:=B@99==@>::==;5=?::??8<===<===?:8A>=@9?<@<;A?849:;>>=;<<=======>=<<<<<=::@:@ADIIGFGGGHGGGGGGGGGGGGGGGGIIIIIIIIIIIIIIIIHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHEFGGHGFE@>;:<<<<<<<<<<<<;=::?==F_[HB?AB@@AAAABCB@=<=<;;;;<<<<<<<<<================================<=====<;<;=5?7;:<=>;<<=;;<;7:??:<<<<<<<CA::==:;=><<>@@>=;;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<6<:7:;;=>?<:;=;:B=;=>=<<======<<<:>6@>B:==>>>?><======<=@98>@<;A;>>:;=>=>@=68=>9<===<===?89;=AB>B6>?=@:=9@9;@:88===============<<9:=>??AC<9<=:9<<========<=><:<@=><7:=>9=<==<===?:9==::;;;=;@;FwwH9<;;<;<<;;<==<;<>AEIIGFGGGHGGGGGGGGGGGGGGGGIIIIIIIIIIIIIIIIHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHEFGGHGFE@>;:<<<<<<<<<<<<:=::?=>Id_JC?@BA@AAAABCB@=<=<;;;;<<<<<<<<<========<<<<<<<<<<<<<<<<<<<<<<<<=====<;;<8;?lriW::<<;==AA<3EWkv~~xrbVG==<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<:@<:A>;CKZny|tiYG87;==;<<<<<<;;A=?:`sx]=<<>??>=<;<<<<;<@:887>Qenx~tfZF<6:>=?B<=======<;;7DhvcN69=?==EXpbO>=?=<@>???????>=>@=:?EctpW<9:8=<==<===?:9==::;;;=;@;FwrH<><;<;<<;;<==<;;:<<<<<<<<<<<<:=;;>=?KibLD>@BA@AAAABCB@=<=<;;;;<<<<<<<<<========<<<<<<<<<<<<<<<<<<<<<<<<==>==<;:C=>P«~E=8:;;DRLjI7@D<<<<<<<<::=A@>>AiͿX>:>?:<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<@98?9”A=;=??>?<;<;<;<;9?B>BYˮ[F9:?=>C<<====<=:>>9WoD;;8@?>XÁ::G===============<????????=?A=:BMnjD8><<=?BEIIGFGGGHGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHEFGGHGFE@>;:<<<<<<<<<<<<:><;>?BBAAAAABCB@=<=<;;;;<<<<<<<<<========<<<<<<<<<<<<<<<<<<<<<<<<>>>==<;9;8==<<<<=<<:=?;9<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<:?66=7MĘfH<8:><<<<<<;;:8<;G?;=><;<;<;<<4AMYsˠX@<=?=<<====<=;?@;`ޥfJ@7>@<^51<================<=6>nÆM:=<:<=>>>>>>>?=>@<:DSB38><===<===?:9==::;;;=;@;FwìfF><<=?BEIIGFGGGHGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHEFGGHGFE@>;:<<<<<<<<<<<<;?=;=;?MrfME>?BBAAAAABCB@=<=<;;;;<<<<<<<<<========<<<<<<<<<<<<<<<<<<<<<<<<>>>>=<;95=EP։^>=@;F\I>=7ExӣjC:=<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;A99@=cąY?57><<<<<<;;@>>:JB:<=<;=;<<;<;<<6?QrÂM:AC<<<===<===>>:_Շ]G8=?;jB;>================??9IW?AA=><=======><=?;9EVGBA?<<==<===?:9==::;;;=;@;Fw©cD;>:;<;<<;;<>>=<=?BFIIGFGGGHGGGGGGGGGGGGGGGGIIIIIIIIIIIIIIIIHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHEFGGHGFE@>;:<<<<<<<<<<<<<@=;=;?MtoQAC?=DABAAABCB@=<===<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>=====;9>;8YɎVP9=Kg><=<<=<<<==<>=?:DL븤ـR>:<<<<<<<<<<<<<<<<<<<<<<<<<<<<====>7<=BS͒׭xP;68<====<<;;==;=V<;<;>8>;<<;<;<<@CpȞ~zxc@;:@=<===<==;><<_шHKABAc<<:<<<<<<<<========9C?UhB>@A9<========?>@8@=XGA==:;<=<===?:9==::;;;><@=Hx~UC;:<<<<<<<<<<<<=====;9<>:XmF4;Kd=<<=<=<<===<>:<>==<<::=<;=V<<=<>8=;<;;<;<=<===<==;><;`jI7?Of<<=<<<<<<<<========6>Dh~P@<@@<========?>@8@=XGA==;;<=<<=<@=HxwOB<@:;<;<<;;<<=<<=@CGJHFFHHHGGGGGGGGGGGGGGGGGFFFFFFFFFFFFFFFFHHHHHHHHHHHHHHHHHHHHHHHHIIHHHHGGEFGGHGFE@>;:<<<<<<<<<<<<D@BAAABCB@=<===<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>=====;:8A;Xߤ_A6F`=;====<<===<=::=jÑTA;B?CzNF<<<<<<<<<<<<<<<<<<<<<<<<<<<<====9<::<>>>=<;::==;>V<<<<=8=;<;;<;<;;`ߝ^>;Pe:;?<<<<<<<<========68M֡eB:=B<========?>@8@=XGA>=;;<=<<=<@=HxnI?;:<<<<<<<<<<<<D@AAAABCB@=<===<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<=<====;:7A:Wфa;Ea=<<===<<===<==<>sk@9;F=5^ZH<<<<<<<<<<<<<<<<<<<<<<<<<<<<====:[eK9;?<=AA?AC?>>=<;::<=;>V;;=<=9=;<;;<;<=CA=98>OZBB9CED=<<<<==<;>;<_΋W<<<<<<<<========;9WzA;;<<========?>@8@=XGA>=;<;=<<=<@=HxfD=;@;;<<<<;<<<=<<=@CGJHFFHHHGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHHHHHHHHHHHJIIIIHHHEFGGHGFE@>;:<<<<<<<<<<<<<>:=9>;cτ[BA@?C?BAAABCB@=<===<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<=<===<::9=8Y筄LKg><<<==<<===<<5Lo\><<<<<<<<<<<<<<<<<<<<<<<<<<<<=====:@=;Hs罕mK@;7687445>>==<<:;==;>V;<=;>9>;<;;<;<B@EG>F;B7>>5==<<===<<>;<`vGDc;;=;<<<<<<<========<@8@=XF@>=;<;<<<=<@=Hxb@<;?:<<<<<;<<<=<<=@CGJHFFHHHGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGJJJJJJJJJJJJJJJJJJJJJJJJKKKKJJJJEFGGHGFE@>;:<<<<<<<<<<<<<>:>9=AAAABCB@=<===<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<=====<::;:9[ׯbQj>;<<==<<===<:@?>woK@=;;66GX^8<<<<<<<<<<<<<<<<<<<<<<<<<<<<====?8??:8TŸVJB@AA@@====<<;<=<<>V<<<<>9>;<<;<;<<\N<<;@_w~ja@@F@==<<==<<<>;<`խhOj>:<;<<<<<<<=======<;EzsVLD<;=========?>@8@=XF@><:<<<<<=<@=Hx^?<;?;<<<<<;<<<=<<=@CGJHFFHHHGGGGGGGGGGGGGGGGGFFFFFFFFFFFFFFFFIIIIIIIIIIIIIIIIIIIIIIIIJJJJJIIIEFGGHGFE@>;:<<<<<<<<<<<<;=:?9<=mԎ`C@@@C>AAAABCB@=<===<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<=====<;9=7<\pҔlk>;====<<===<;><;<;3:GP^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<====>8>=;6?dmN>::;:<<<<==<===<>U<;=<>9><<<;<;<<`wN75;MC5<=<=<<==<<<>;<`sӠnw><=;<<<<<<<=======@8@=XGA=<:<<<==<=?:9==::;;;><@=HxY><:><<<<<<;<<<=<<=@CGJHFFHHHGGGGGGGGGGGGGGGGGBBBBBBBBBBBBBBBBEEEEEEEEEEEEEEEEEEEEEEEEGFFFFEEEEFGGHGFE@>;:<<<<<<<<<<<<;=:@9;>pՐbD?A@C=AAAABCB@><===<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<=<==<<;9?5?\dΑi>;<<==<<<==<><8;{^??;=?8;;:::;<===><=<>U<;=<>9>;<<;;<<DY׵fC?;=<<<==<<<><<`[Ғ><>;;;<;<<<=======@8@=YF@><;<;<=<==?:9==::;;;><@=HxV==9>=<<<<<;<<;<<==@CGIGFFHHHGGGGGGGGGGGGGGGGGAA@A@A@A@A@A@A@A@@@@@@@@@@@@@@@@@@@@@@@@CCCBBBBBEFGGHGFE@>;:<<<<<<<<<<<<;=:@8:?r֠m=A>@@G>AAABBA?><===<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<=====<<=<=;9;C=UWl{=;6B==<<===@<:<96=BPbsգd?:8><<<<<==>;>?AS;<::?:<<<<<<;<;c}L2:GTZI>8===<<;<<=<=@_W^>:C:;;:<;<<=A6B:>?@Oդשa<;========<C=;;:;;:<<<;<:>;Hy»T9??7;<<;;;;<<==<=?BEGIHFGHIHHGGGGGGGG<<<<<<<=<<<<<<<<;;;;;;;;========================DDEEFGHHDEFGIHEB>=<;<<<<;;<<<<<<;<<;9>:zؤm>A>@?F?AAABBA@><===<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<=====<==<=;98@;VHIa?;:@==<<==<=;<:8hՄW?A59DETeW8<=======<<<<<<<<<<<<<<<<<<<<====><:<>86=?Ko{H>64<<<<;=>>8;<>S;<;:?9<<;<<<<<<]̔_;BA>XptVD?>=<=<<;<<:8:>^QMh:8=;;;<<<<<;========<;A<;<>XHEA;Hy»R9?@9=<<;;;;<<==<=?BEGIHFGHIHHGGGGGGGG???????@>>>>>>>>========????????????????????????EEFFGGHHEEFGIHDB>=<;<<<<;;<<<<<<;<<<9><|ݭq?@>A?F?AAABBA@><===<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<=====<=<<<;98>9WPHIy=;=>==<<==<=<><7agA>:H54Y{W=========<<<<<<<<<<<<<<<<<<<<====98;BDCDGB<:::Nw<;>>?S;<;:?9<<;<<<<<;Wܬo><9@DIOgS?>@=<=<<;<<<:<@_UFFu789;;<<<<<<;@9@9=:JeJC========<;A<<<>VEA=6359<::>B;;:;;:<<<;<:>;Hy»zN9>A<><<;<;;<<====@CFHIHFGHIHHGGGGGGGGBBBBBBBCCCCCCCCCBBBBBBBBDDDDDDDDDDDDDDDDDDDDDDDDGGHHHHHHEEFHIHDA>=<;<<<<<<<<<<<<;;;<9><uA@>A?E?AAABBA@><===<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<=====<==<<;99?8VDD?Q:9?<<=<<==<=A;:=b~L<5CGHyN6========<<<<<<<<<<<<<<<<<<<<====@@?83=D@L>:?>S;<;:?9<<;<<<;<;WύTB7C>>=<=<<;<?^XG;S7;<<<<<<<<<:?:?9=8VmWA<=======<;@<<==UICB?>>>AA:8>;;:;;:<<<;<:>;Hy»qJ9=@==<<;<;;<<===>@CGIIHFGHIHHGGGGGGGGEEEEEEEFHHHHHHHHGGGGGGGGIIIIIIIIIIIIIIIIIIIIIIIIIIIIHHHHEFFHIGDA>=<;<<<<<<<<<<<<<;;<9==yB@>A>E?AAABBA@><===<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<=====<====;9:?:WEB@A^;:?;<=<<==<=?6:>Ru弃cIJ[lʨqE;========<<<<<<<<<<<<<<<<<<<<====4======;;:?<=9=7d漒qJ<========;@;==A;:A=9@;;:;;:<<<;<:>;Hy»jF;=>>;<<;<;;<<=<=>ADHJIHFGHIHHGGGGGGGGHHHHHHHIIIIIIIIIHHHHHHHHJJJJJJJJJJJJJJJJJJJJJJJJJJJIIHHHFFGHIGC@>=<;<<<<<<<<<<<<<:;>:>@~D?>B>D@AAABBA@><===<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<====>=====;98@;Y\FBCJS><>=<=<<==<==9A=?Hߴ}{щR==;<<;<:<<>S;;;9?9;<;<<<<;<0Dxˑ~ldxݞ_<=@?====<;<;<9=?^M><:Da>>====;;;?>;8>8t͘[UYK{ĕU<========:?;==;P}jigb_F4=?;C;;:;;:<<<;<:>;Hy»bB<<=?:;<;<;;<<=<=>AEHJIHFGHIHHGGGGGGGGIIIIIIIJHHHHHHHHGGGGGGGGHHHHHHHHHHHHHHHHHHHHHHHHJJIIHHHGFFGHIGC@>=<;<<<<<<<<<<<<=:;>:=AҁE??B=D@AAABBA@><===<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<====>=====;:7@B?w<=<@<=<<<<<===C:=7tѷh@6;<=======<<<<<<<<<<<<<<<<<<<<====<8>;D…S?>6>>><;<<;<;==@R<<:9?:<<;;<<<;<@^N@>:<=u>>>>>>===;;>;NñuB?@8<;;:;;:<<<;<:>;Hy»Z><:=A;;<;<;;<;<<=>AEIKIHFGHIHHGGGGGGGGIIIIIIIJHHHHHHHHGGGGGGGGHHHHHHHHHHHHHHHHHHHHHHHHIIHHHHHHGGGHIGC@>=<;<<<<<<<<;;;;<8;>;<===<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<====>=====;:8@;SFAF7<<`y8<:B<=<<<<<<<<<5D<]tΛyFAA@<=======<<<<<<<<<<<<<<<<<<<<====A7>?@?8E=><<<<;<:;::R<<:9?:<;;;;<<<;QӰ[=?=99==<<<<;>>==;;=?@88@;zN7I?;BgܸV<========:>;>>:N<8@=C<;:;;:<<<;<:>;Hy»T;<;?D=;<;<;;;;<<=>AEIKIHFGHIHHGGGGGGGGIIIIIIIJJJJJJJJJIIIIIIIIJJJJJJJJJJJJJJJJJJJJJJJJHHHHHHHHGGGHIGC?>=<;<<<<<<<<;;;;<8:?:<===<<<<================<<<<<<<<;;;;;;;;;;;;<<<===<<====8;8PЗE@<<<;?FؒH4===<<<<<<<==<==<<=fШYD>;:<;<<<<<<<<<<<<<<<<<<<<<<<<<<<<==>@99>;;?<<<===<=;;5H˞CA=;;<<<;;;;<<<;>>>>BYΰ^P>79><9==<<====:@;5VߌE2C44C=O֔A>@<<<<<<;;@=;>=9Koѩk@8=;<@DӥV===<<<<<=<<=<<@Gڅ==?4?<;:;;:<<;=>:>=Jy»vH>>;@==;<;<<<<<>==?CGIJFGGGHIIIJIIHGFFEGGGGGGGHJJJJJJJJJJJJJJJJIIIIIIIIIIIIIIIIIIIIIIIIGGGGGGGGEGHIHDA>>=<;<<<=<<<<;;<<9?;;>@B@?@A@BAAAABBA@><===<<<<================<<<<<<<<;;;;;;;;;;;;<<<=<=<<====:=>GczdA>;=><>A_{a@;><=<<<<<<<===<===>::>;7@Sn|ufQD<=?;;?<<<===<=<>=@ZwwdB?;;;<<<;;;;<<<==>=;<<<<====>C?6Cd{c<7A78B6Hisf8?=<<<<<<;;=>=>=9E_||kI9>C?9A:bwnF===<<<<<=<<=<;@GbyX2<@9A<;:;;:<<;=>:>=Jy»tH=>;?==;<<<<<<<>==?CGIJFGGGHIIIJIIHGFFEGGGGGGGHGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHHHHHHHHHHHGGGGGGGGEGHIHDA>>=;;<<<=<<<<;;<<:?;:?ACB??@@BAAAABBA@><==<<<<<<<<<<<<<========<<<<<<<<;;;;;;;;;;;;<<<=<<<<====<:=A>9=F<<?;;?;<<<<<<<<<<<<<<<<<<<<<<<<<<<<==>;;=<<<>?B;9>@;;>======<=:BBB?>==>?39?A>==?<========@@;;?@=?;=>7:=K9@?69BC88C6?=4D?>==<<<<<=;<><;?E=>=:=ABB>;><;:;;:<<;=>:>=Jy»nD<>:><=;<<<<<<<===@DGIJFGGGHIIIJIIHGFFEDDDDDDDEEEEEEEEEEEEEEEEEFFFFFFFFFFFFFFFFFFFFFFFFGGGGGGGGEGHIHDA>==;;<<==<;<<;;<<CJE@?@@BAAAABBA@><=<<<<;;<<<<<<<<========<<<<<<<<;;;;;;;;;;;;<<<<<<<<<===@76@>68F;=>>>>=>><:;?=<<<<<<<=====<=<;<==<><>@?9:=;<<<<<<<<<<<<<<<<<<<<<<<<<<<<==>=?=<>A@======<<<9;==:A><<;:;=>9=@>;;>B<=======;;<>><;<9BA86;=???4=B;C?<<<<<<;;8@?;=<:=4:;:=B>27@:>=7B>>==<<<<<<;<><;>D@834887788:>=Jy»jB=?9=;=;<<<<<<<==>AEHIIFGGGHIIIJIIHGFFEAAAAAAABCCCCCCCCCCCCCCCCAAAAAAAAAAAAAAAAAAAAAAAAGGGGGGGGEGHIHDA>=<;;<<=><;<<;;<<=>=<;<<<;;;;<<<<<<<<========<<<<<<<<;;;;;;;;;;;;<<<<<===<<==C:6>C???>=??==AA=:@@?==;;<<;;=<<<<<<<<<<<<<<<<<<<<<<<<<<<<<==>>@>88;=<@:=DD=>??=;<;>BA>;;:9<=;<<<<<<<<<>;9:;::<=??@@>=;>>>=;;=?<===<<<:7989B9<>:;;??<<>>86;;CFCC;>==<<<<<<;<><:=CB;9<><;>B69?::?=<;:;;:<<;=>:>=Jy»c>=@9;:><<<<<<<<==?BFIIIFGGGHIIIJIIHGFFE>>>>>>>?????????????????========================GGGGGGGGEGHIHDA><<;;<=>><;<<;;<<>=>>:IYOE@@@B@AAABBA@>;<<;;;;;;;;;;;;;========<<<<<<<<;;;;;;;;;;;;<<<<<===<<===>==>>;7>?><>?<8:@?:<;;;;;<<<<<<<<<<=<;;=><;766;=><:<<<<<<<<<<<<<<<<<<<<<<<<<<<<<==>8<=;:;<<=:;@@:9<;<;;;;>@====<<<<:AB;8<=:;;;<<=<<<<<<<<<<:;>=:9;=;<=>>>=<;;=??>;:<===<<<<>::>>;:<@?:9>97@??>>==========<<@B@;9;?B>=>>>=<<===<<<<<;;=>=:78<;=?=<;:;;:<<;=>:>=Jy»^;<@:::><;<<<<<<==?CFIJIFGGGHIIIJIIHGFFE>>>>>>>?>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>GGGGGGGGEGHIHDA><<:;<=>?;;<<;;<<>:>?8MaTGAAAB@AAABBA@>;<;;;;::;;;;;;;;========<<<<<<<<;;;;;;;;;;;;<<<<<=======9>@>;==<;>?<;<>?=:<;;;;;<<<<;;;;;;@?=====<79;;<<;:<<<<<<<<<<<<<<<<<<<<<<<<<<<<<==>89;?A>=>>><>===><;::<=>>====<<==;@@><===<=<==>=;<<<<<<<<8;>><;=>==>=>>>>7:=?>=<;<===<<<<::;>?=<==<<::::;@<<9;?>>======<<=@;:=:9ACA?=:9<@B@@<8;;>>==<<<<<;;=?=:<@=?>:9<>><>><=>=:<;:;;:<<;=>:>=Jy»Y9=@:99><;<<<<<<==@CGIJIFGGGHIIIJIIHGFFECCCCCCCDBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBGGGGGGGGEGHIHDA>;;:;<=>?;;<<;<<<>9>?8PhWJBAAB@AAAABB@>;;;;;:::;;;;;;;;<=<=<=<=<;;;<;<;<<<<<<<<<<<<====<=<<<<<<;<=>>>=;8==;<>=9>:=A@;;D;<;<;<<<<<<<;;;;9:;=>>=;69:888<><<<<<<<<<<<<<<<<<;;;;<<<<<;;;==>D<:>?:8:=>=<;>>=@@>=>?=:<<======C<9?CA=;><<=?==;===<<<<==>><9999;<=>><:9<>?=::=?<=<<<=<=;=;:;<=;>?@929?::9<==<<<<<=>>;<>76A>:;=>;;;>;?=@C=>===<<<<<;;=?=:;@6;>;;>=88B@9;=<><<;;::<<;=>;>>Ky¼V8<;<<<<=<==@DGJJIFGGGHIIIKJIIGGFFGHGHGHGHHHHHHHHHHHHHHHHHEEEEEEEEEEEEEEEEEEEEEEEEGGGGGGGGEGHIHDA>;;::<=>?<;<<;;<?7QlbL@==H=>><=<;=<=<=<=<=<=<=<=<<=<<<<<<<<<<=<=<;;=>==>>>===<=<;;;:;:;;;;;;;:::;:::;;;;;999:9::;:::::::::::::::::;:::::;;>=>?A?;>>>>>>>>>>>>>>>>========================;:::;;;:;;;:;;:;<;<<;<<;=<<<==<===<<<<=<<;;;;<;<;<;;;;;<<;<<<<<;;;;;;<<;:::::::;;;;;;;;::=??<999:9:;;;;:;<<<<<<<<<<<<<<<:::::;;;;;;;;;;;;;;;;::;;>=8B>FyrN=9>;9;8:<;<===<>=>ADGGGHHHHHHHHHHHHHGGGHHIIIIHHGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGHHIJJJIIIIIIIIFGGGFC@?:;::<===;<<<<<<;=?:8@LxiQB>=H>>>?@@@@@@@@@@@@@@?@@@@@@@@>>>???@@<=====================<<<>?=<<;::999999999999999888888888888888888888888;;;;;;;;::;;:;;;;<<;<;<;;<;<<<<;;;;<<<;<::::::::;;;;;;;;;;;;;;;;;;;;;;;;================:<<<:;;<::;<<<<=?>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>?>???7<9EydL=9><9:8:;<=====>>>ADGGGHHHHHHHHHHHHHGGGHHIIIIHHGHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHGGHHHIIIJIIIIIIIIFGGGFC@>:;::<===<;;<<;<<<>99AQqVC?>F>>>>>>>>>>>>>>>BCGA=EG>AA@?@???A@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@AA@??>=<<=<<<=>=>=>>>>>>>>>>>>>>>>=======;;<<<<=>>>>>>>>>>>>>>>>>==<=====>=>===>B????????????????>>>>>>>>>>>>>>>>>>>>>>>>????????>??>????@??@?@?@?@@@@@@@????????>???????@@@@@@@@???????????????@:;;;;;;;<<<<<<<;>>>?@@@@:99:;;;:9:9:9:9:9:9:9:9::;:;:;:;:;:;:;:::;:::;:;=@B;>?L}SI<9>;:;9:;<=====>>?BEGGGHHHHHHHHHHHHHGGGHHIIIIHHGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHHHFGGFEC@>:;::<===<;;<<;<<<=::AVxZA@?C=B@AABB?>???????????????>8;:<9:;<======>?BFHHGHHHHHHHHHHHHHGGGHHIIIIHHIIHIHIHIHIHIHIHIHIHIHIHIHIHIHIHIHHHHHHHHHHHHHHHHGGGFEB?=:;::<===<;;<<;<<<<;:?Y]?A?A?B@AABB?>@@@@@@@@@@@@@@@@@Gnīy?C::=:;=::;<======>@CGHHGHHHHHHHHHHHHHGGGHHIIIIHHJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJHHHHGGGGHHHHHHHHGGGFEA><:;::<===<;;<<;<<=<=:<\Å`?A?@AB@AABA@>AAAAAAAAAAAAAAA@;=hĤ|>@9;=9<>::;<======>@DHIHFHHHHHHHHHHHHHGGGHHIIIIHHJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJGGGGGGGGHHHHHHHHGGGFDA=;:;::<===<;;<<;<<=;>:9`Ɏe@B?>AB@AAAA@?@AAAAAAAAAAAAAA@ECezq?=9;=9AEHIHFHHHHHHHHHHHHHGGGHHIIIIHHGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGHFEEFFGGGHHHHHHHHGGGFD@=;:;::<===<;;<<;<<=:?;9e͔iAC?=AB@@AAA@??@@@@@@@@@@@@@@@>AEHIHFHHHHHHHHHHHHHGGGHHIIIIHHFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFGEDEFFGGHHHHHHHHHGGGED@=::;::<===<;;<<;<<<9@<:iӣ`EA?D>A@@A@@AABBBBBBBBAAAAAAAA@AXŧÿd<9;<<=<;;::;<<<<<<@DGHHHHHHGGGGHHIHHGFEEDDDDDEEEEDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDEAAABCDEFFFHIIJJJHGFDB?<;:;;;=<<<<;;<=<==AA@A@AAABAAAAAAA@@@@@@@A@@SwƧ];9;<<=<;;::;<<<<<<@DGHHHHHHGGGGHHGFFEDCBA?@@@@AAA>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>?@?@ABCDEFGHIIJIIHGFDB?<;:;;;=<<<<;;<=<==<>:;@l׮fDB@B?AA@@AAAAAAAAAAAAAAAAAAAAB=KhƧuS<:;<<=<;;::;<<<<<<@DGHHHHHHGGGGHHEEDCA@??<<<<====<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<?@BCDDFGGHIIIIHGECB?<;:;;;=<<<<;;<=<==<=;;@q۷kCBAA@AA@@AA@@@@@@@@@@@@@@@@@?D>E[ƧiK<:;<<=<;;::;<<<<<<@DGHHHHHHGGGGHHFFEDBA@@===>>>>>?????????????????????????????????@ABCDEEFGGHIHHHGGECA><::;;;=<<<<;;<=<===<;:;<<=<;;::;<<<<<<@DGHHHHHHGGGGHHIIHGEDCCBBCCCCDDDDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCCCDEEFGHHGHHIIIHHGFECA>;::;;;=<<<<;;<=<===;<=>|t?AB@AAA@@AA@@?@@@@@@@AAAAAAAABB@IƨT@=:;<<=<;;::;<<<<<<@DGHHHHHHGGGGHHJJIIHGGFGGGGHHHHFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFHHHHHHHIGHHIIHHGGFDBA>;::;;;=<<<<;;<=<===9===x>AB@BAA@@AA@@@AAAAAAAAAAAAAA@@B@BzƨM=<:;<<=<;;::;<<<<<<@DGHHHHHHGGGGHHHHHHHGGGHHHIIIIIGHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHJJJIIHHHGHHHHHGGFFDB@=;9:;;;=<<<<;;<=<===9>>=y=AB?AAA@@AA@@AAAAAAAABBBBBBBA=B@@tƨJ;;9:<;=;;;::;<<<<<<@DGHHHHHHGGGGHHFFFFFGGGGGGHHHHIHIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIKJJHIGGFGGHGHGGEFFDC@=;9::;;=<<;<<;<====<8?>>>CA;BAA@@@A@@AAAAAAAAAAAAAAA@G:GBqŦǵyJB;==<<;<<<<<<<<<<<@BFGGGGGGHHHIIJJGGGFGGHHGGGHHIIIGHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHJJIJHIGHGHGHGHGHGFDB?>=;9:;<=<<;;;;;====<<;9:<<=<<;;;<;======@;UKHC?CAAA@@@@@AAAAAAAAAAAAAAA@A;D?`ŦhB?>===<<<<<<<<<<<<<@CFHHGGGHHHHHIIIIIIHHIIIJJJJKKKKIIIIIIIIHHHHHHHHHHHHHHHHHHHHHHHHJJIIIIHHHHHHHHHHGFDA?=;;9:<<=<<;;;<;====>=@>]UIB@CAA@@@A@@AAAAAAAAAAAAAAA@@>==<<<<<<<<<<<<<@CFHHGGHHHHHHHHHJIIHHHIIIIIIIIIIHHGHGHGHFGFGFGFGFGFGFGFGFGFGFGFGIIIIIIHHHHHHHHHHGECA?=;;9:<<=<<;;;<;======>@g_KA?AAA@@AA@@AAAAAAAAAAAAAAA@@>D@RgŦO><>>==<<<<<<<<<<<<=;:9:<<=<<;<;<;====<;==<<<<<<<<<<<<>>>>>>>@@@@@@@@@@@@@@@@@@@@@@@@GGHHHHIIHHHHHHHHFEC@><::9:<<=<<;<<;;====;<;F~oN@C@AA@@AA@@AAAAAAAAAAAAAAAABA@AIPŦh=?:>>==<<<<<<<<<<<<>>>===<<<;;<<<<<<<<=======================>GGGHHHIIHHHHHHHHFDB@><::9:<<=<<;<<;;=<==;===<<<<<<<<<<<<================================FGGGHHIIHHHHHHHHFDB@><:99:<<=<<;<<;<=<==P﹂PD>==<<<<<<<<<<<<=;:;;<<<<<;<<;<=<<<9:?PVG=DAA@@AA@@AAAAAAAAAAAAAAAA@F==<<<<<<<<<<<<=;:;;<<<<<<<;;<<<<<;;?QȘ[I>DAA@@AA@@AAAAAAAAAAAAAAAA?F=A?@tŦQ9>==<<<<<<<<<<<<<@BDFHIIIHHHHHHHHIIHHHHIILKKJJKKLHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHFD@>>=;:;;<<<<<<<;;<<=<<=<=SѢ]H@DAA@@AA@@AAAAAAAAAAAAAAAA?CA@=AiŦuM:<>?>==<<<<<<<<<<<<==;:;;<<<<<<<;;;<=<<==;Tة[EAD@@@@AA@@AAAAAAAAAAAAAAAAAACA?C`ŦfK>9==>==<<<<<<<<<<<<<>@BEGHHHHHHHHHHHHHGGGGHHGGFFFFGGGGFGFGFGFGFGFGFGFGFGFGFGFGFGFGFGIIIIIIIIHHHHHHHHEB?===;:;;<<<<<<<;;;<=<<<=:W߯[CBE@@@@AA@@AAAAAAAAAAAAAAAAA>D?@FU|ŦXF@8=<>==<<<<<<<<<<<<<=?ADFGHHHHHHHHHHHGGGGGGHHGGFFGGHFFFFFFFFEEEEEEEEEEEEEEEEFFFFFFFFIIIIIIIIHHHHHHHHDB?===;:;;<<<<<<;;;;<=<<9><\]CCD@@@@AA@@AAAAAAAAAAAAAAAACBGJkŦIBB7=;>==<<<<<<<<<<<<<<>@CFGHHHHHHHHHHIIHHHHIIHHHGGHHHGHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHGIIIIIIIIHHHHHHHHCA>===;:;;<<<<<<;;;;<=<<9@@baCBC@AAAA@@@AAAAAAAAAAAAAAAAC;B=AGC_Ŧy@>C6=:>==<<<<<<<<<<<<<<=@CFGHHHHHHHHHHJJIIIIJJHHGGGGHHIIIIIIIIHHHHHHHHHHHHHHHHIIIIIIIIIIIIIIIIHHHHHHHHC@><=<;:;;<<<<<<;;;<<<==8BCfaC?CCCA@@@@AAAAAAAAAAAAAAAA@????ADHMŦ`==A:6?<<<<<<<<<<<<<<<?ADGIGHIIHGGHIIIIIIIIHHHHHHHHIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIHHHHHHHHGGFFFFFF==<<;<<<<<<<<<<<<<<<;;;;>;LseF?CCBBA@@@AAAAAAAAAAAAAAAA@@@???CHKqŦxW;9?;:?<<<<<<<<<<<<<<<?ADFHFHIIHGHHIIIIIIIIHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHGGGGGGGGGGFFEDDC===<<<<<<<<<<<<<<<<<;;;;@<<<<<<<<<<<<<<<?@BDFFGIIHHHIIIIIIIIIHHHHHHHHFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFGGGGGGGGHGFECBA?===<<<<<<<<<<<<<<<<<;;;;A=WyT@CCBB@@@@AAAAAAAAAAAAAAAA@BCA@@ADGEvŦIC;7;??<<<<<<<<<<<<<<<<?@ABCDFHHHGHIIIIIIIIIHHHHHHHHGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGHHHHHHHHHGEDB@>====<<<<<<<<<<<<<<<<<;;;;A=^Ն]ACCCA@@@@BAAAAAAAAAAAAAAAABCAA?@CE@]Ŧf>?<8;@@;<<<<<<<<<<<<<<<>??@@BDFGFFGIHHHHHHHHHHHHHHHHGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGHHHHHHHHFEDB@>=;===<<<<<<<<<<<<<<<;;;;;<>>gّeACCCA@@@ABAAAAAAAAAAAAAAAABBA@?ACC=HoŦyN=?>9;@@<<<<<<<<<<<<<<<<<>?>>>>==?ACDEEFHGGGGGGGGFFFFFFFFHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHCBA@?=<;>==<<<<<<<<<<<<<<<;;;;;<;=oޚlBBCCA@@@ABAAAAAAAAAAAAAAA@@BB@@AAC=@UŦ^>?>;9;>?<<<<<<<<<<<<<<<<<=>==<<;:<>@BBCDFEEEEEEEEDDDDDDDDEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE??>==<<;>==<<<<<<<<<<<<<<<;;;;;<;>vߟoABCCAA@AABAAAAAAAAAAAAAAA@@@BAAABD>><<<<<<<<<<<<<<<<====<;:9:==<<<<<<<<<<<<<<<;;;;;<;={ܮu@FBCBA@AAAAAAAAAAAAAAAAAA@@A@A@AA@;FFD^ŦrN==<;;<<;;;<<<<<<<<<<<<<<<<<<<<<<<<========<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>><<<<<<<<<<<<<<<<<<<<<<<<<<;;;;;<;9߳zAFBCBAAA@AAAAAAAAAAAAAAAA@@A@@AA@ABA>>OqŦnRG@9;;<<<;;;<<<<<<<<<<<<<<<<<<<<<<<<========<<<<<<<<<<<<<<<<;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;<<<<<<<<<<<<<<<<<<<<<<<<<<;;;;;<=;⽁BGBCBAAA@AAAAAAAAAAAAAAAAA@AAA@AA@D<:>AMpŦmK<<>;;;<;;;;;<<<<<<<<<<<<<<<<========>>>>>>>>================;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;========<<<<<<<<<<<<<<<<<<;;;;;;=<ȋBEBCBAAA@AAAAAAAAAAAAAAAAA@@@@@@@AA=AEA?OiŦoKCA75A;;;;;;<<<<<<<<<<<<<<<<<<================================================================================<<<<<<<<<<<<<<<<<<;;;;;;;<ӓADBCBAAA@AAAAAAAAAAAAAAAA@A@@A@@@@@ACCA?BHiŦ¶pQ>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>========<<<<<<<<<<<<<<<<<<;;;;;;:<ݛBBBCBAA@@AAAAAAAAAAAAAAAAAAAAAA@@@AEA;>;;<;;;<;;<<<<<<<<<<<<<<<<================<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<========<<<<<<<<<<<<<<<<<<;;;;;;GL\wŦqYH=@?55>@:<;;;;<;;<<<<<<<<<<<<<<<<================<<<<<<<<<<<<<<<<;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;========<<<<<<<<<<<<<<<<<<;;;;;;BECCCCBAAA@AAAAAAAAAAAAAAAA@A@@AAA@A@??AAAA@FABISjƦsUC<;@<;=<89=<<;;;<;;<<<<<<<<<<<<<<<<========<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<========<<<<<<<<<<<<<<<<<<;;;;;;GJKGAA@BBCCBDDCDCDCDCDCDCDCDAAAAA@A@@A@A@@@ADCACL]kuƨľuhTF><:;;;;<<<<<;;;;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;<;<<;;I[XM@@AABBCBCBBBBBBBBBBBBBBB@@@@@@@@@@@@@@@@??>=?BEE]gxŨpeWNC:8998<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;<;;RhjW@@?AABABBBBBBBBBBBBBBBBBAAAAAAAAAAAAAAAABCEEDA>;?FNU\fqzƧÿ~yie^ZVQLH@=89:<<:<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;;;\|ǁ_>>??@AAA@@@@@@@@@@@@@@@@AAAAAAAAAAAAAAAA==>??@AB=?@?>@EJTYakt|ƨukaZUFB@===;8=<;=?@?=<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;;;dЙg?????A@@@@@@@@@@@@@@@@@@AAAAAAAAAAAAAAA@A@?>=?@A@BC@>>@BDGJKMNRTRV^kzƧvmeZRHFB=<;97B@?@?><;=<;;<;;:;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;;<;;;;jܰr=>=?@@?A?@@@@@@@@@@@@@@@AAAAAAAAAAAAAAA@DEFFEEBB?ACCAAA@ABB?=<==BBCJU_egtyƨ}zvsvtv{}wrphaXQMHC>>=:;=@BA68;=?>>>=;::9:;;;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;<<<;;;;t}=>>>?@?@????????????????@@@@@@@@@@@@@@@@;<>@AA@?@@@@?>?>??@@>?@@DDB?>?@@EB>:98;;<<;;=?;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;;;ӆ<=>>?>@@@@?@?@?@?@?@?@?@@A@A@A@A@A@A@A@@BAA??@BCCA@A@CEEBCDCB@@A>BED@>AD@AABEGLORW]adju~~Ũ|ywtonjgb`]\YVROMKIIACEEA::<=@BA?=;;?>>?@AA@;;<;;<=>9:;=<;<<;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;;;M:>===<==<<=<====<=======<<========<<=====<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<========C7A@69?QaA<;;>:?================>>>>>>>>>>>>>>>>AAA@@@A@?@???@??>??A@BBCBBCBCCCCAAABBBAB@@@@AA@@AEHJIKNSRVY^afilu}ywrke`\XRPNMJIGGGEBA>=<====<=<<<<====<======<<<<<=======<<<<<<<<<<<<<<<=<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<========B7>?;;JmQ9:<:5?;;;;;;;;;;;;;;;;<<<<<<<<<<<<<<<<>>????>?>>>>?>>??>>??@@BBBBBAAAAA@@@AAAA@@@@@@@?>@B?=;=>@@BA@?AAPU]eks{Ǧ}|yusppmjifdabaXVSQLGFC;::99:::9889::;<<;;;<:;;<=<=<<=<=<<<<<<<<======<=<<<==<==<==<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<========>9;<==Wժ`:8=<5?::::::::::::::::;;;;;;;;;;;;;;;;>=>=>>>=??>?>>>>>=>>>?@@AAAAAAAA@?@??@@?>?>>>????ACDCCEGCCDCA@@A8;===?@BBBBAAFMSciq{§|ywtkjf`[UROIGDB?>??<=>=>??@;:<<>>>@<<:;;<<=:;:;;;;;<===<<<<=<<<<<<<<<<<=<=<<<<<==<==<==<<<=<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<========;;>;<=>>==>>@@@?????>>>??>??==>>>===?@@@?>?@=?@@??@ABBCBA@AAAA?<;?FMEGMSW\adlnsvz}olkjhfeeca`_\YVTTLJIIFEDD?=;88798:9:9::99;<<==>>?>><;999::::;::::<=<==<==<==============<<=<<==<===<<====<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<========;>@:=====>=>>=>>>=>=<==<=<<<;;::::9=>??>>?A:;:<;=<>:<=?=>=>??AAABDD<<;:9:>>>@@@A?=<;;;<><;::::88;==<=====<;::::;99:::;;;<=<=<<<=================<=<<<<====<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<========;=@9A]uI7<:8<<<<<<<<<<<<<<<<;;;;;;;;;;;;;;;;<<<<<;;;;<<;;;<<====;;;;<====<==<==<====<=====<=:::;=???:;<<;:;<<;;;<=<@BEGKNNMMOLKOTQPRSVXYX[]`dgkmntuvx{}~|yxxmligdb`a^]ZWTQOOQRQPMLMNIHFCB@====>>>???>>>>>==<==;;;::::99:;;=<>==>>>>>>>==;<<>9::;<;;;<<===<<<<<<<========<<<==<<<===<==<<<=<=;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<========<9>;OŽU7=<:<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;<;;:;:;:;<=<<;;;<==<<<=<<;;<;<<<<;<<<<<;<<;;<=>>=<=>>==>>===>?@@??<99<>=;=>?=<;=><==;::=>;===<<>@=>>><;:98=>ADGILMMNRUY\___abdfijlmnoqsuvw{|}~}|zyvwwvusqmida``^\ZWURPPOLHECA@==>==<<<;<<<87:?<>==>?>?>>=<;:::====>>>=<===;:98:;;<<=<=:;;;:;;;==<;;:;::::;;=<<=<=<=<<<<<<<=========<===<<<===<==<<<===;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<=======><9;;;;<<<<<<<<<<<<<<<<<<<<<<<<<<;<<<<<;;;;<<<<<<<;;;;;;;;;<<<<<<<<<;;;;;<>?>?>?>?>?>?>??@?>=<;:9<<<;;;;;9:::9:::=<====<=>>>>>>>>>==<;;::@?????@@EEEEEEEDHHGFEDCB@@@@@@@@EEEEEEEEDDDEEEEEGGGHGGGGFFFFFFFEEEEEEEEE???@?@@??@@@????>>>>=>==<<<<<<===========<=<<<=<;;<;;;<;;;;;;;;<<<<<====<<;<<;<<======<=;;;;;;;;<<<<<===<<<<=<===<<=====================<<<<<<<======<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;;;8<:;<>E;Jq֔j:4<;;::;===;;;;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<================================?>>=<;;:<<<<<<<<;;;;;;;;========>>>>>>>>===<;;::================??>>=<;;=======<=>>>>>>>>>>>>>>>>>>>>>>>????????;<<<<<<<=======<=========<<<<<<<===============<<=======<<<<<<<<<<<<<<<<<<<<====;<;;;;<<==<<===<;<<<<<<<========================================================<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;;;:;<;:9::=;AjČF2@=;<;::=@<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<====<<<<<<<<<<<<<<<<<<<<===================<;;;:;;;;;;;;;;;;;;;;<<<;;;;:;;;;;;;:9:::::::::::::::;;;;;;;;::::::::99999999::::::::;;;;;;;;;;;;;;<<===============<<=======<<<<<<<<<<<<<<<<<<<<====;;;;;;<<==<<====;<<<<<<<========================================================<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;<<;;<<<:9:;::;>5OhDB@>>><9;?<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;<<<<====<<<<<<<<================<<<<<<<<==<<<;;;<<<<<<<<================<<<<<<<<;<<<<<<<<<<<<<<;>>>>>>>>;;;;;;;;========;;;;;;;;<<<<<<<<;;<<===>================<=======<<<<<<<;<<<<<<<<<<<<====;;;;;;<<==<<====;<<<<<<<========================================================<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;<<;;<<;:9<====@Ctϛb>><<>=99=;;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<====<<<<<<<<========<<<<<<<<;;;;;;;;<<<<<<;;========;;;;;;;;;;;;;<<<========================<<<<<<<<========;;;;;;;;<<<<<<<<========;;<<===>================<<<<<<<<;;;;;;;;<<<<<<<<<<<<====<<<<<<====<<<<==;<<<<<<<========================================================<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;<<;;<<=::>?;:=CbƃF@:7<=99=;;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<================================<<<<====<<<<<<<<========<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;<<<<<<<<<<<<<<::::::::::::::::;;;;;;;;;;;;;;;;::::::::;;;;;;;;<<<<<<<<;;;;;;<<================<<<<<<<<;;;;;;;;<<<<<<<<<<<<=====<<<<<====<<<<==;<<<<<<<========================================================<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;<<<;<<>;;=<79@RwK?8;>;:>;;;;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<====<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;<<<<<<<<<<<<<<================<<<<<<<<::::::::::::::::========;;;;;;;;;;;;;;;;;;;;;;;;<<<<<<<<=<<<<<<<================<<<<<<<<;;;;;;;;<<<<<<<<<<<<========<<<<<<<<====;<<<<<<<========================================================<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;<<<<;;>;;=:9BQݲ\J=;=;:=;;;;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<====<<<<<<<<<<<<<<<<<<<<<<<<<<<<========;;;<<<<<========;;;;;;;;;;;;;:::========<<<<<<<<<<<<<<<<========>>>>>>>>;;;;;;;;<<<<<<<<========????>>==================<<<<<<<<;;;;;;;;<<<<<<<<<<<<========<<<<<<<<===<<<<<<<<<========================================================<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;<<<<;;;8:=<>PeqB7<88><<;;;=>=<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<========================================================<<<<<<<<================<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;;;;;;;;;;;;;;;<<<<<<<<<<<<<<<<<<<<<<<<<<<<============================<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<========<<<::;==<@EA:HyפhH?>98;:977788<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<================================;;;;;;;;;;;;;;;;================================<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;;;;;;;;;;;;;;;<<<<<<<<<<<<<<<<<<<<<<<<<<<<============================<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<========::<===<;=36DOfϚgJA=7<=>>???B<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<================================================================================<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;;;;;;;<<<<<<<<<<<<<<<<<<<<<<<<<<<<============================<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<=========::;<;98::89:=>;:=<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<================================>>>>>>>>>>>>>>>>========<<<<<<<<================<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;;;;;;;<<<<<<<<<<<<<<<<<<<<<<<<<<<<============================<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<========>98:;<>;:=>:9:<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<================================;;;;;;;;;;;;;;;;========<<<<<<<<================<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;;;;;;;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<====<<<<<<<<<<<<<<<<========<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<========;8:==;=@EOz币\G>7:>==@;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<================================<<<<<<<<<<<<<<<<================================<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;;;;;;;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<====<<<<<<<<<<<<<<<<========<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<========;:=@;45>>>>>>>>>>>>>>>================================<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;;;;;;;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<====<<<<<<<<<<<<<<<<========<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<========>;>@=?Ocǜ|[KE>:<<<<<<<<<;<;;;;;;;<;<;<;<;<;<;<;<<<<<<<<<<<<<<<<<<<;<;<;<;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<================================<<;<;<;<;<;<;<;<========<<<<<<<<================<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;;;;;;;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<====<<<<<<<<<<<<<<<<========<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<=<<<<<==8:@I\rWHBBB>;<;9:>?;67;><:;:<;<;<;<;<;<;<;<<<=====>>>==<<;;;<;<;<;?>=<=<==;;;;;;;;;;;;;;;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;;;;;;;<<<<<<<<<<<<<<<;;;;;;;;;;;;;;;;;<<<<<<<<<<<<<<<<========<<<<<<<<==================<=<=<=;<;<;<;<;;;;;;;;;;;;;;;;;;;;;;;;<<<<<<<<;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;<<<<<<<<;;;;;;;;;;;;;;;;;;;;;;;;<<<<<<<<========<<<<<<<<<<<<<<<;;;;;;;;;;;;;;;;;;;;;;;<<<<<<<<<;<<<<;;;;=;;;<=>=>>=;;;;<==<::<==<997:::::986<;<;<;<;=<=<=<=<;;;;;:::::::;;;<99:9::::;::;<<==;;;;;;;;;;;;;;;;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;;;;;;;;<<<<<<<<<<<<<<;;;;;;;;;;;;;;;;;<<<<<<<<<<<<<<<<========<<<<<<<<==================<=<=<=;<;<;<;<;;;;;;;;;;;;;;;;;<;<;<;<<=<=<=<=;<;<;<;<;<;<;<;<;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;<<<<<<<<;;;;;;;;;;;;;;;;;;;;;;;;;<<<<<<<========<<<<<<<<<<<<<<<;;;;;;;;;;;;;;;;;;;<;;<<<<<<<<<<;<<<<<<<;:9:9:898===<<<;;======<<:9999999:::9999:888999998:=?@?>=9Mpٳq]QC=;::=9=@A?<<<<=;<;<;<:;:;:;:;========<<<<====>>=>====9::<<<<<;;;;;;;;;;;;;;;;;<;<;<;<;<;<;<;<;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;<;<;<;<;<;<;<;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;<;<;<;<;<;<;<;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;<<<<<<<<<<<<<<;<<<;;;<<==>=<;:@???>>>>???>>>>>????????<<<<;;;:@@@@@???B?:7:FU`t•~`J?::=89776689<<<<<<==;;;;<<<<99::::::;;;::999::::::::===>=<;:;;;;;;;;;;;;;;;;;;<;<;<;<;<;<;<;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:;<<<<<;<<<<<<<<<<<<<<<;799:9;::87899:;;:::99988=<<<<;;;======<<<;98889;@><>I`}¯s^RRKB==?@?::::::99;;;;;<<<<<<<<;;;==============<<;;;;<<<;<<<<<<<<<<<<<<<;<<<<<<<<<<<<<<<<;<<<<<<<<<<<<<<<;;;;;;;;;;;;;;;;;;<;<;<;<;<;<;<;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;<;<;<;<;<;<;<;<<<<<<<<<<<<<<<<;;;;;;;;;;;;;;;;;;<=<;::;;;;;;<<<;<<<<<;==;;;<=><;;;;;;;<<<<<<<=;<<<<<<<:::;;<<<=<;;>BGHQ^rغti\TSROJLLKIHFEEDCCBA@?@@@?=<:99899::;;;<<<<<<<<:;:9:<=><<<<<<<<<<<<<<<<;<<<<<<<<<<<<<<;<<<<<<<<<<<<<<<<::::::::;;;;;;;;<<<<<<<<<<<<<<<;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;;;;;;;<<<<<<<<;;;;;;;;;;;;;;;;;;;;;;;;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;;;;;;;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;;;;;;;<<<<<<<<<<<<<<<<<<<<<<<<;<<<<<<<;;;;;;;:<<<<<<<<<<<<<<<<<<;<;<;<;<;<;<;<:;<=<;:9;;;;;;<<;<;<<<<<><:9:::;<<;:98877789:;<=??ABDFGHGGIJLNOPGHJOV^ehĻ~{xvqpnkhecbWVSOLHEDAA@??>==>>>>>>??B@>=;;=><<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;;;;;;;<<<<<<<;<<<<<<<<<<<<<<<;<<<<<<<<<<<<<<<<;;;;;;;;;;;;;;;;::::::::;;;;;;;;::::::::::::::::;;;;;;;;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;;;;;;;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;;;;;;;<<<<<<<<<<<<<<<<<<<<<<<<;<<<<<<<;;;;;;;:<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<:;<>=;;:<<<<;;<<;;;;;;<;689<<<;;?>>>>>>>=>@DGKMNUXZ_chkmx{|þ~xtronlhea_]\\\\\\[[NLE@>;<=<<<<<<<<<<<<<<<;<<<<<<<<<<<<<<<<;;;;;;;;;;;;;;;;;;;;;;;;<<<<<<<<;;;;;;;;;;;;;;;;<<<<<<<<<<<<<<<;<<<<<<<<<<<<<<<<;;;;;;;;<<<<<<<<;;;;;;;;;;;;;;;;:;;;;;;;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;;;;;;;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<;;;;;;;;<<<<<<<<<<<<<<<<<<<<<<<<;;;;;;;;:::::::;<;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;9:<>><;:<<<;;<<<<;;<<<;=J::D><>>>>>>???8899::;;<<<<<<;;@AAAABBB?=;99:<>99::;;<<<<<<<===;;<<==>>9::;;<<<::;;<<==========:::;;<<<99:::;;;;;;;<<==;;;;;;<;::::::::88999999;:::;;<====>>???<<<<<<<<::::::::<<<<<<<<========AAAAAAAA========>>>>>>>>AA@??>==;;::999899999999;;::9988::::::::======>>========<<<<<<<=:;=>??@A<<<<<<;;>??><::;ABBA?>>?::;<=>>?E<9?C?>==<<9:::::::<<<<<<<<>??????????>>>==@@????>>>>>>????<<<<<<<======<<=;;;;;;;;;;;;;;;;::999:;;?????@@@>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>========;;;;;;;;99999999:::99988>>>>>???<<<===>>;;;<<<<=<<<<;;;;>>??@AABBA@?>=<:9::;<<===>@BDEFFHIKNQSUVZ\_`bdfgdfhjkloqwxz|~]:2667:9889999874564:.XJ0/0/-111122223332116:}{ynmkheb`_LKIFC@?>>=<;:877BB@?=<;;>?>==<<;@@??>>===<<;;::9AA@?=<;;AA@?=<;::::::::9====<<<<>>===<<<===========<<;;;;::999::=====<<<;;;;;;;;<<<<<<<<<<<<<<<<================????????========>???@@@@<=>?@ABC89:;<=>>:;<=>@AA=>>?@@AA::;=>?@ABBAA@?>==<=?@ACCNOQTVXZZfilptx{|ȃ=(-102000111010.12-81bF)*-,)....////1//0/.6Auttrrpponllihffddbb_^[ZXWVVUVTUTQRQQPQOPPQOONNMMKKKKKKKKJIIHHGFFGFEDDDDDBBBAAA@@>>>>>>>>@@@@@@@@DDDDDDDDFFFFFFFFLLLLLLLLPPPPPPPPRRRRRRRRWWXXYZ[[]^`bdfhhklmoqrststvxz|}~|}F+22.,+,,,,+,,-+//)31iG,-.-.,-....//0-.1--{f?<::==>>=<<<9<4::88pQ=>;8<998899::?5;8;9o?::<==>==<<<;>5;;9:qQ>?;9<:;::;;<<>7?:::xB::>====<<<==?7<<::rQ=>;9<;;;:;:;;:7@9?>>>==>>=@7==::rP=>;8;:::9889886=6APWA7=?>>>==>>>@7=<::qP<=:8;<<<::9:9;9=8J`aE6;>===<==<>@7=<::qS==<:<<<==<<===9=?RzߨwF<9====<==<>A8>>;:qR===;<=<<=<<==<;?>UJ=;======<<>@8>>::qR=>=;==<<=<<==;>@@8>=:9pR=>=;==<<<====9?@;aȖQ<<======<<=@8>=:9pR=>=<==<<<====8>>:iҟT<=======<<=?7==:9pQ=>><>==<<====9=<=t٪Z=======<<<=?7=<98oQ<>><>==<<====<<;Bb@>=====<<<>=>=<==<<==@<:FiDA=====<<<?=;;====<<<>=;;=<==;;<<>6DZɂSA<====<;:==;;=<==<<<<<7Gi͏_@<<===<;:;pO<>=;<====<<<<=;Kո:<=<<=<;::@8<<>=;<====<<<<<<<===<<;:@8<<>>5><=<<==<=7=^כJ@6@?:?;6?<=<<==<=:Df՜OD6??:?;<>==7;:AjܪO;<=7?<<====<<>;?;<>==8;:@i֨P<;=8@<<====<<=:?:===>9<:?hϦQ<;=8@=<<<<<<<:[ŢsT===;?:===>:=:>fȣR><=7>=<<<<<<<:b¿[?=<;?:=<=?<>;=e¡S@=>7=<=<<<<<<>k`@=<>7<=<<<=<<=CpaA;<><;==;89?A?H}iL8;@;:@=<RjpSA;:<;=?=;;@G[wZB>@=:;::>A>E\wcF89=<<;<=??@LlrQ@=<>;=C@@ObhMA>?=><=@G[~n]L@A:;GQXkhTJC>A?CN_v¿hTQLP]lzucWOPT^oxxz}zz~}}~}}}~}}}}~}}}}}z}~~~~}~~}~}}}~~~~}}~}~}~~}~~~}~}}~~}}~~~}}~}}}}}}}}~~}}}}}~}~}}~~~}}}}}}}zntywwyz}}rccgcefghklnmnpsvy}vgegaceeeggggcdegilopoprvy}{|yjjlgghhgggggfeefefffcdeeggghquux}~ytnjgX`ttdfjfeedcccccilihfcccghggfecccbcgp}ztp^VTPLGDA4Fgyhijffgghgghhccccdfggcceeegggigcdny}q^NIGA;?@@@@@@@?Daxggiefgggggggeeeefeegeeeefffgfigbg|~}p\I@@FD??@@@@@@@BB_ugfheefffffffffffffffffffffffcgg`g}~}jTI>;@FEA@@@@@@@@BB_ugfheefffffffffffffffffffffffcgg`g}~xQA<8;BFFB@@@@@@@@BB_ugfheefffffffffffffffffffffffcgg`g}~}[C?>>@DDC@@@@@@@@@BB_ugfheefffffffffffffffffffffffcgg`g}cD@BCDDDC@?@@@@@@@@BB_ugfheefffffffffffffffffffffffcgg`g}zO:ACCDC@>@B@@@@@@@@BB_ugfheefffffffffffffffffffffffcgg`g}iD9D@@@><<@EA@@@@@@@BB_ugfheefffffffffffffffffffffffcgg`g}\F@A@@@???@A@@@@@@@@BB_ugfheefffffffffffffffffffffffcgg`g}~|WGC@@@@@@@@@@@@@@@@@BB_ugfheefffffffffffffffffffffffdgg`g}~uRD@?@@@@@@@@@@@@@@@@BB_ugfheefffffffffffffffffffffffdgg`g}lMA@>>@@@@@@@@@@@@@@@BB_ugfheefffffffffffffffffffffffdgg`g}dF?@>>@@@@@@@@@@@@@@@BB_ugfheefffffffffffffffffffffffdgg`g}]B>@?@@@@@@@@@@@@@@@@BB_ugfheefffffffffffffffffffffffdgg`g}}Y@=B@@@@@@@@@@@@@@@@@BB_ugfheefffffffffffffffffffffffdgg`g}{V>=BA@@@@@@@@@@@@@@@@BB_ugfheefffffffffffffffffffffffdgg`g}pIA@<@@@@@@@@@@@@@@@@@BB_ugfheefffffffffffffffffffffffdgg`g}jDBC<@@@@@@@@@@@@@@@@@BB_ugfheefffffffffffffffffffffffdgg`g}dDCA<@@@@@@@@@@@@@@@@@BB_ugfheefffffffffffffffffffffffdgg`g}ZDCB=@@@@@@@@@@@@@@@@@BB_ugfheefffffffffffffffffffffffdgg`g}~}~~~}xRDD@?@@@@@@@@@@@@@@@@@BB_ugfheefffffffffffffffffffffffdgg`g}~~oIDC@@@@@@@@@@@@@@@@@@@BB_ugfheefffffffffffffffffffffffdgg`g}g@BD@@@@@@@@@@@@@@@@@@@BB_ugfheefffffffffffffffffffffffdgg`g}bB=?@@@@@@@@@@@@@@@BB_ugfheefffffffffffffffffffffffdgg`g}|}~vQ@D@=B??@@@@@@@@@@@@@@@BB_ugfheefffffffffffffffffffffffdgg`g}}}~~~pL@E@<@@@@@@@@@@@@@@@@@@BB_ugfheefffffffffffffffffffffffdgg`g}~~~~~~kH?E@9@@@@@@@@@@@@@@@@@@BB_ugfheefffffffffffffffffffffffdgg`g}~}~~~gD?E@8>A@@@@@@@@@@@@@@@@BB_ugfheefffffffffffffffffffffffdgg`g}~~cA>G@8>A@@@@@@@@@@@@@@@@BB_ugfheefffffffffffffffffffffffdgg`g}~}~z~T@@A@=@@ABA??@ACIJJKKKKKILfugfheefffffffffffffffffffffffdgg_e{||}~N@@@@@@@AB@>?@CEMOOOOOOOLNiugfheefffffffffffffffffffffffeig_ez|}}M@@@@@@@@A?>?AEINOOOOOOONOiugfheefffffffffffffffffffffffeig_ez~~~uK@@@@@@@@?>>@DILOOOOOOOONQiugfheefffffffffffffffffffffffeig_ez~~}|mI@@@@@@@?>>?BEKOOOOOOOOOOSjugfheefffffffffffffffffffffffeig_ez~}}fF@@@@@@@??>?BHMPOOOOOOOOPSjugfheefffffffffffffffffffffffeig_ez_D@@@@@@@@@@@BHMPPOOOOOOOQTjugfheefffffffffffffffffffffffeig_ez\D@@@@@@@AA@@BHNPPOOOOOOORUiugfheefffffffffffffffffffffffeig_ez}~XB@@@@@@@@@@BEIMPOOOOOOOOPTjugfheefffffffffffffffffffffffeig_ez~}}VB@@@@@@@@@@BEJMPNOOOOOOOPRjugfheefffffffffffffffffffffffeig_ez}}~}~zUB@@@@@@@??@ADIMOPOOOOOOOOQjugfheefffffffffffffffffffffffeig_ez}}}~~~yRA@@@@@@@>>?@DHLPOOOOOOOONQiugfheefffffffffffffffffffffffeig_ezwPB@@@@@@@>>?@CFKMOOOOOOOOLPgugfheefffffffffffffffffffffffeig_ezuM@@@@@@@@@@@ABDHJNOOOOOOOKNgugfheefffffffffffffffffffffffeig_ezrL@@@@@@@@@BA@ABDGMOOOOOOOJMgugfheefffffffffffffffffffffffeig_ezqJ@@@@@@@@CCA@@@CDLOOOOOOOJMgugfheefffffffffffffffffffffffeig_ezdJA@@@@@@@@@@@@@@@DFFFGGGGECfugfheefffffffffffffffffffffffeig_ez}_JA@@@@@@@@@@@@@@@ABBBBBBBD@dugfheefffffffffffffffffffffffeig_ez}^IA@@@@@@@@@@@@@@@@BBBBBBBC@dugfheefffffffffffffffffffffffeig_ez}[F@@@@@@@@@@@@@@@@@@@@@@@@C@dugfheefffffffffffffffffffffffeig_ez}ZD@@@@@@@@@@@@@@@@????????C@dugfheefffffffffffffffffffffffeig_ezWB@@@@@@@@@@@@@@@@@@@@@@@@C@dugfheefffffffffffffffffffffffeig_ezV@?@@@@@@@@@@@@@@@@@@@@@@@C@dugfheefffffffffffffffffffffffeig_ezU??@@@@@@@@@@@@@@@@@@@@@@@C@dugfheefffffffffffffffffffffffeig_ez}~||}~|PB@@@@@@@@@@@@@@@@@@@@@@@@C?cugfheefffffffffffffffffffffffeig_ez~}}~xMC@@@@@@@@@@@@@@@@@@@@@@@@B?cugfheefffffffffffffffffffffffeig_ez~}xMC@@@@@@@@@@@@@@@@@@@@@@@@B?cugfheefffffffffffffffffffffffeig_ez}}}}xMB@@@@@@@@@@@@@@@@@@@@@@@@B?cugfheefffffffffffffffffffffffeig_ez}}}~~wMB@@@@@@@@@@@@@@@@@@@@@@@@B?cugfheefffffffffffffffffffffffeig_ez~}|~|~wMB@@@@@@@@@@@@@@@@@@@@@@@@B?cugfheefffffffffffffffffffffffeig_ez}|~}vMA@@@@@@@@@@@@@@@@@@@@@@@@B?cugfheefffffffffffffffffffffffeig_ez}vKA@@@@@@@@@@@@@@@@@@@@@@@@B?cugfheefffffffffffffffffffffffeig_ez}pJ@@@@@@@@@@@@@@@@@@@@@@@@@B?cugfheefffffffffffffffffffffffggieax}mK@@@@@@@@@@@@@@@@@@@@@@@@@B?cugfheefffffffffffffffffffffffgfjg`u~~}lI@@@@@@@@@@@@@@@@@@@@@@@@@B?cugfheefffffffffffffffffffffffgdif_u}|}}|~lI@@@@@@@@@@@@@@@@@@@@@@@@@B?cugfheeffffffffffffffffffffffffchf_u~~lI@@@@@@@@@@@@@@@@@@@@@@@@@B?cugfheeffffffffffffffffeffffffecge`v~~~~lI@@@@@@@@@@@@@@@@@@@@@@@@@B?cugfheefffffffffffffffffffffffdbgd_v}~~}~lH?@@@@@@@@@@@@@@@@@@@@@@@@B?cugfheeffffffffffffffffffffffecafc_v}}}kH?@@@@@@@@@@@@@@@@@@@@@@@@B?cugfheefffffffffffffffffffffffcadc_u}~~}eA@@@@@@@@@@@@@@@@@@@@@@@@@B?cugfheeffffffffffffffffeeeeecc`_fe`v~}d@A@@@@@@@@@@@@@@@@@@@@@@@@B?cugfheefffffffffffffffeeeccccc`_fd`w}d@B@@@@@@@@@@@@@@@@@@@@@@@@B?cugfhedfffffffeeeeeeddeeeccccc`_fd`w}}}f@C@@@@@@@@@@@@@@@@@@@@@@@@B?cugfhedfffffeeecddccccecccccbb`_fd`w~}}}~||~}gCEA@@@@@@@@@@@@@@@@@@@@@@@B?cugfheefffffffdccccccccccccbbb`_fd`w~}~~}}}|~}gDEA@@@@@@@@@@@@@@@@@@@@@@@B?cugfheefffffffccccccccccccbb``__fd`w}}}}}}~~}gDFA@@@@@@@@@@@@@@@@@@@@@@@B?cugfheefffffffcbbbbbbbcccbb```__fd`w}hDFA@@@@@@@@@@@@@@@@@@@@@@@B?cugfheefffffffcbbbbbbbcccbb``a__fd`w}qLE@@@@@@@@@@@@@@@@@@@@@@@@B?cvffgdccccccddbbbbbbbbbbbbbbbb__fd`w}uMC@@@@@@@@@@@@@@@@@@@@@@@@B?cufegcccccccccbbbbbbbbbbbbbbbb`_fd`w}uMC@@@@@@@@@@@@@@@@@@@@@@@@B?cufegcccccccccbbbbbbbbbbbbbbbb`_fd`w}vMC@@@@@@@@@@@@@@@@@@@@@@@@B?cuffgcccccccccbbbbbbbbbbbbbbbb`_fd`w}xMB@@@@@@@@@@@@@@@@@@@@@@@@B?cvffgcccccccccbbbbbbbbbbbbbbbb`_fd`w}yNB@@@@@@@@@@@@@@@@@@@@@@@@B?cuffgcccccccccbbbbbbbbbbbbbbbb`_fd`w}yNB@@@@@@@@@@@@@@@@@@@@@@@@B?cuffgdccccccccbbbbbbbbbbbbbbbb`_fd`w}yNB@@@@@@@@@@@@@@@@@@@@@@@@B?cuffgdccccccccbbbbbbbbbbbbbbbb`_fd`w~N>????????@@@@@@@@AAA@AAAAB>ducacbbbbbbbbbbbbbbbbbbbbbbbbb`_fd`wN<>>>>????@@@@@@@@@@@@@@@@B>dwc_cbbbbbbbbbbbbbbbbbbbbbbbbb`_fd`wP<<==>==>>????????@@@@@@@@B=cwc_cbbbbbbbbbbbbbbbbbbbbbbbbb`_fd`wS<========>>>>>>>>????????A=cwc_cbbbbbbbbbbbbbbbbbbbbbbbbb`_fd`wV?<<<<<<<<<<<<<<<<========A>dwc_cbbbbbbbbbbbbbbbbbbbbbbbbb`_fd`wX?;;;;;;;;<<<<<<<<<<<<<<<cwc_cbbbbbbbbbbbbbbbbbbbbbbbbb`_fd`w{meiggs]@;:::::::;;;;;;;;<<<<<<<cwc_cbbbbbbbbbbbbbbbbbbbbbbbbb`_fd`wscX_^\g|lC99999999;;;;;;;;::::::::4>>>>>>?@@@@@@@>>>>>>>>Ilwffhdefffffffdccccccccdeeedccc`ccbyyg`^db^fusK=;;;;;;;<<<<<<<<;;;;;;;;4<]}scbb\________^^^^^^^^^____^]\a`ccbyl_\a__gxzZ@;;;;;;;;<<<<<<<;;;;;;;;=C`}ugghcefffffffggggggggffgffcccb`dcbyvh`aaeppmonnnnnnnnnnnnnnnnnnoooolk{|}{|}}}}}}}~||}}}ywtg`ccbyypilt}k`ccby}l`ccbykaca`y~iaca_y~iaca_y~iaca_y~iaca_y~iaca_y~iaca_y}~iaca_y}~iaca_y}~iaca_y}~iaca_y}~iaca_y}~i`ca_y}~i`ca_y}~i`ca_y}~i`ca_yl_cbbywj_ccbywcXMNTJMOOOOOOOOOOOOOOOXWlrtuppqqqqqqqqqqqqqqqppsusqnle_ccbywU@;??37;;;;;;;;;;;;;;;@@]wgff_cdddddddddddddddaadedccab_ccbyV;:@=689999999999999999:Ysccd^_```````````````^^`aaa_`b_ccby_@?A>@?===============<<]qcegccccccccccccccccca`bbbccbc_ccbygD=<7@=<<<<<<<<<<<<<<<=>^tefieddddddddddddddddccccccccc_ccbyuMB83A?===============>=]udcfbbcccccccccccccccccbb``aab_ccbyyL=:5@><<<<<<<========>;`vcd`bbbbbbbbbbbbbbbbbbbbbbbbbb_ec`w}M<;8>=<<<<<<<========>:bxbf_bbbbbbbbbbbbbbbbbbbbbbbbba`ec_vU@;9==<<<<<<<========>:bxbf_bbbbbbbbbbbbbbbbbbbbbbbbba`ec_v_D=;<<<<<<<<<========>:bxbf_bbbbbbbbbbbbbbbbbbbbbbbbba`ec_vjI<>:;<<<<<<<========>:bxbf_bbbbbbbbbbbbbbbbbbbbbbbbba`ec_vuM>A9:<<<<<<<========>:bxbf_bbbbbbbbbbbbbbbbbbbbbbbbba`ec_v}Q@B7:<<<<<<<========>:bxbf_bbbbbbbbbbbbbbbbbbbbbbbbba`ec_vS@C7:<<<<<<<========>:bxbf_bbbbbbbbbbbbbbbbbbbbbbbbba`ec_vnG7<><9:=<<<<<<<<>:bxbf_bbbbbbbbbbbbbbbbbbbbbbbbba_feaw}R<=?=:;>@>:;<<<<<<<>:bxbf_bbbbbbbbbbbbbbbbbbbbbbbbba_feawgI<><:<=?<79<<<<<<<>:bxbf_bbbbbbbbbbbbbbbbbbbbbbbbba_feaw}[D;989=><7;<<<<<<<>:bxbf_bbbbbbbbbbbbbbbbbbbbbbbbba_feawqS@966:==;<<<<<<<<>:bxbf_bbbbbbbbbbbbbbbbbbbbbbbbba_feawnTI@99<=<<<<<<<<<>:bxbf_bbbbbbbbbbbbbbbbbbbbbbbbba_feawvgSD=;===<<<<<<<>:bxbf_bbbbbbbbbbbbbbbbbbbbbbbbba_feawgP@<;<<<<<<<<<>:bxbf_bbbbbbbbbbbbbbbbbbbbbbbbba_feawp`RIDCA@?<<;1>>>>>>A;:?A:=I}{{zzxyxz}~z{}}}}}}}}{{}}}}{zzz{{}|}~}|||||||||||||||||||||||||||||||||||||||||||||||||||||||~}}}|}||||||||||||||||~}|}{{{{z{{z}~|{{{{}zzzzzyyyxxxwxxwwvvvvvvvvpsnthGAAAAA?>===>@@@@@@@>=======?;:?A8:8>?8==??=====<<<<<<<<========>:8>>7;GȢj__bdglpsuuuuvx{}{z{{}|}~}|||||||||||||||||||||||||||||||||||||||||||||||||||||||~}}~}{z{||||||||||||||||z{~xqnqtqppppqqqqqqqqppppqqqqqqqpuosgF>?================<<<<<<<<>87=>7;G˧cYZZ\]_cddfgikntx{zz{{}|}~}|||||||||||||||||||||||||||||||||||||||||||||||||||||||~}~~}|{{{}||||||||||||||||xolmqonnnpqqqqpppppqqqqqqqqqqnqkqfF>?<;;;<<<<<=======<<<<<<<<=87=>7;G˾̣aZ[XXXXWWWX[\_flsvzz{{}|}~}|||||||||||||||||||||||||||||||||||||||||||||||||||||||~~}~~}}}{{}}}}}}}}x{{x{}wpnpqqppqpppppppppppppppppppposnsgD==========================>:8>>8;IʥhXXXXXWWWWUUWZ^abenv}|z{}}}|||}}||||||}}||||||||||||||||||||||||||||||||||||||||}|~}~~~~}}{{}}}}}}}}xz{y}uppqqppppppppppppppppppppppppptntgF==========================?:8>?8?8?8?8?8?8?8>==<<<<=======>:8>?8:8>?8==========>:8>?8>=======<>:8>?8>>===<<>:8>?8>==<<>:8>?8==<<>:8>?8==<<>:8>?8:8>?8===?:8>?8===?:8>?8===?:8>?8===?:8>?8===?:8>?8===?:8>?8===?:8>?8===>:8>?8=<=>:8>?8:8>?8:8>?8=<==>:8>?8:8>?8?8=<<==?:8>?8=======>:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8===============>:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8?8:8>?8:8>?8:8>?8:8>?8:8>?8:8>?8A<::::::::::::::::::::::: +#include +#include +#include +#include + +// ITU-R BT.601 +// #define RGB_TO_Y(R, G, B) ((( 66 * (R) + 129 * (G) + 25 * (B)+128) >> 8)+16) +// #define RGB_TO_U(R, G, B) (((-38 * (R) - 74 * (G) + 112 * (B)+128) >> 8)+128) +// #define RGB_TO_V(R, G, B) (((112 * (R) - 94 * (G) - 18 * (B)+128) >> 8)+128) + +// ITU-R BT.709 +#define RGB_TO_Y(R, G, B) (((47 * (R) + 157 * (G) + 16 * (B) + 128) >> 8) + 16) +#define RGB_TO_U(R, G, B) (((-26 * (R)-87 * (G) + 112 * (B) + 128) >> 8) + 128) +#define RGB_TO_V(R, G, B) (((112 * (R)-102 * (G)-10 * (B) + 128) >> 8) + 128) + +INSTANCE_IMP(VideoStackManager) + +Param::~Param() +{ + VideoStackManager::Instance().unrefChannel( + id, width, height, pixfmt); +} + +Channel::Channel(const std::string& id, int width, int height, AVPixelFormat pixfmt) + : _id(id) + , _width(width) + , _height(height) + , _pixfmt(pixfmt) +{ + _tmp = std::make_shared(); + + _tmp->get()->width = _width; + _tmp->get()->height = _height; + _tmp->get()->format = _pixfmt; + + av_frame_get_buffer(_tmp->get(), 32); + + memset(_tmp->get()->data[0], 0, _tmp->get()->linesize[0] * _height); + memset(_tmp->get()->data[1], 0, _tmp->get()->linesize[1] * _height / 2); + memset(_tmp->get()->data[2], 0, _tmp->get()->linesize[2] * _height / 2); + + auto frame = VideoStackManager::Instance().getBgImg(); + _sws = std::make_shared(_pixfmt, _width, _height); + + _tmp = _sws->inputFrame(frame); +} + +void Channel::addParam(const std::weak_ptr& p) +{ + std::lock_guard lock(_mx); + _params.push_back(p); +} + +void Channel::onFrame(const mediakit::FFmpegFrame::Ptr& frame) +{ + std::weak_ptr weakSelf = shared_from_this(); + // toolkit::WorkThreadPool::Instance().getFirstPoller()->async([weakSelf, frame]() { + auto self = weakSelf.lock(); + if (!self) { + return; + } + self->_tmp = self->_sws->inputFrame(frame); + + self->forEachParam([self](const Param::Ptr& p) { self->fillBuffer(p); }); + // }); +} + +void Channel::forEachParam(const std::function& func) +{ + for (auto& wp : _params) { + if (auto sp = wp.lock()) { + func(sp); + } + } +} + +void Channel::fillBuffer(const Param::Ptr& p) +{ + if (auto buf = p->weak_buf.lock()) { + copyData(buf, p); + } +} + +void Channel::copyData(const mediakit::FFmpegFrame::Ptr& buf, const Param::Ptr& p) +{ + + switch (p->pixfmt) { + case AV_PIX_FMT_YUV420P: { + for (int i = 0; i < p->height; i++) { + memcpy(buf->get()->data[0] + buf->get()->linesize[0] * (i + p->posY) + p->posX, + _tmp->get()->data[0] + _tmp->get()->linesize[0] * i, + _tmp->get()->width); + } + //确保height为奇数时,也能正确的复制到最后一行uv数据 + for (int i = 0; i < (p->height + 1) / 2; i++) { + // U平面 + memcpy(buf->get()->data[1] + buf->get()->linesize[1] * (i + p->posY / 2) + p->posX / 2, + _tmp->get()->data[1] + _tmp->get()->linesize[1] * i, + _tmp->get()->width / 2); + + // V平面 + memcpy(buf->get()->data[2] + buf->get()->linesize[2] * (i + p->posY / 2) + p->posX / 2, + _tmp->get()->data[2] + _tmp->get()->linesize[2] * i, + _tmp->get()->width / 2); + } + break; + } + case AV_PIX_FMT_NV12: { + //TODO: 待实现 + break; + } + + default: + WarnL << "No support pixformat: " << av_get_pix_fmt_name(p->pixfmt); + break; + } +} +void StackPlayer::addChannel(const std::weak_ptr& chn) +{ + std::lock_guard lock(_mx); + _channels.push_back(chn); +} + +void StackPlayer::play() +{ + + auto url = _url; + //创建拉流 解码对象 + _player = std::make_shared(); + std::weak_ptr weakPlayer = _player; + + std::weak_ptr weakSelf = shared_from_this(); + + (*_player)[mediakit::Client::kWaitTrackReady] = false; + (*_player)[mediakit::Client::kRtpType] = mediakit::Rtsp::RTP_TCP; + + _player->setOnPlayResult([weakPlayer, weakSelf, url](const toolkit::SockException& ex) mutable { + TraceL << "StackPlayer: " << url << " OnPlayResult: " << ex.what(); + auto strongPlayer = weakPlayer.lock(); + if (!strongPlayer) { + return; + } + auto self = weakSelf.lock(); + if (!self) { + return; + } + + if (!ex) { + // 取消定时器 + self->_timer.reset(); + self->_failedCount = 0; + + } else { + self->onDisconnect(); + self->rePlay(url); + } + + auto videoTrack = std::dynamic_pointer_cast(strongPlayer->getTrack(mediakit::TrackVideo, false)); + //auto audioTrack = std::dynamic_pointer_cast(strongPlayer->getTrack(mediakit::TrackAudio, false)); + + if (videoTrack) { + //TODO:添加使用显卡还是cpu解码的判断逻辑 + //auto decoder = std::make_shared(videoTrack, 1, std::vector{ "hevc_cuvid", "h264_cuvid"}); + auto decoder = std::make_shared(videoTrack, 0, std::vector { "h264", "hevc" }); + + decoder->setOnDecode([weakSelf](const mediakit::FFmpegFrame::Ptr& frame) mutable { + auto self = weakSelf.lock(); + if (!self) { + return; + } + + self->onFrame(frame); + }); + + videoTrack->addDelegate((std::function)[decoder](const mediakit::Frame::Ptr& frame) { + return decoder->inputFrame(frame, false, true); + }); + } + }); + + _player->setOnShutdown([weakPlayer, url, weakSelf](const toolkit::SockException& ex) { + TraceL << "StackPlayer: " << url << " OnShutdown: " << ex.what(); + auto strongPlayer = weakPlayer.lock(); + if (!strongPlayer) { + return; + } + + auto self = weakSelf.lock(); + if (!self) { + return; + } + + self->onDisconnect(); + + self->rePlay(url); + }); + + _player->play(url); +} + +void StackPlayer::onFrame(const mediakit::FFmpegFrame::Ptr& frame) +{ + std::lock_guard lock(_mx); + for (auto& weak_chn : _channels) { + if (auto chn = weak_chn.lock()) { + chn->onFrame(frame); + } + } +} + +void StackPlayer::onDisconnect() +{ + std::lock_guard lock(_mx); + for (auto& weak_chn : _channels) { + if (auto chn = weak_chn.lock()) { + auto frame = VideoStackManager::Instance().getBgImg(); + chn->onFrame(frame); + } + } +} + +void StackPlayer::rePlay(const std::string& url) +{ + _failedCount++; + auto delay = MAX(2 * 1000, MIN(_failedCount * 3 * 1000, 60 * 1000)); //步进延迟 重试间隔 + std::weak_ptr weakSelf = shared_from_this(); + _timer = std::make_shared( + delay / 1000.0f, [weakSelf, url]() { + auto self = weakSelf.lock(); + if (!self) { + } + WarnL << "replay [" << self->_failedCount << "]:" << url; + self->_player->play(url); + return false; + }, + nullptr); +} + +VideoStack::VideoStack(const std::string& id, int width, int height, AVPixelFormat pixfmt, float fps, int bitRate) + : _id(id) + , _width(width) + , _height(height) + , _pixfmt(pixfmt) + , _fps(fps) + , _bitRate(bitRate) +{ + + _buffer = std::make_shared(); + + _buffer->get()->width = _width; + _buffer->get()->height = _height; + _buffer->get()->format = _pixfmt; + + av_frame_get_buffer(_buffer->get(), 32); + + _dev = std::make_shared(mediakit::MediaTuple { DEFAULT_VHOST, "live", _id }); + + mediakit::VideoInfo info; + info.codecId = mediakit::CodecH264; + info.iWidth = _width; + info.iHeight = _height; + info.iFrameRate = _fps; + info.iBitRate = _bitRate; + + _dev->initVideo(info); + //dev->initAudio(); //TODO:音频 + _dev->addTrackCompleted(); + + _isExit = false; +} + +VideoStack::~VideoStack() +{ + _isExit = true; + if (_thread.joinable()) { + _thread.join(); + } +} + +void VideoStack::setParam(const Params& params) +{ + if (_params) { + for (auto& p : (*_params)) { + if (!p) + continue; + p->weak_buf.reset(); + } + } + + initBgColor(); + for (auto& p : (*params)) { + if (!p) + continue; + p->weak_buf = _buffer; + if (auto chn = p->weak_chn.lock()) { + chn->addParam(p); + chn->fillBuffer(p); + } + } + _params = params; +} + +void VideoStack::start() +{ + _thread = std::thread([&]() { + uint64_t pts = 0; + int frameInterval = 1000 / _fps; + auto lastEncTP = std::chrono::steady_clock::now(); + while (!_isExit) { + if (std::chrono::steady_clock::now() - lastEncTP > std::chrono::milliseconds(frameInterval)) { + lastEncTP = std::chrono::steady_clock::now(); + + _dev->inputYUV((char**)_buffer->get()->data, _buffer->get()->linesize, pts); + pts += frameInterval; + } + } + }); +} + +void VideoStack::initBgColor() +{ + //填充底色 + auto R = 20; + auto G = 20; + auto B = 20; + + double Y = RGB_TO_Y(R, G, B); + double U = RGB_TO_U(R, G, B); + double V = RGB_TO_V(R, G, B); + + memset(_buffer->get()->data[0], Y, _buffer->get()->linesize[0] * _height); + memset(_buffer->get()->data[1], U, _buffer->get()->linesize[1] * _height / 2); + memset(_buffer->get()->data[2], V, _buffer->get()->linesize[2] * _height / 2); +} + +Channel::Ptr VideoStackManager::getChannel(const std::string& id, + int width, + int height, + AVPixelFormat pixfmt) +{ + + std::lock_guard lock(_mx); + auto key = id + std::to_string(width) + std::to_string(height) + std::to_string(pixfmt); + auto it = _channelMap.find(key); + if (it != _channelMap.end()) { + return it->second->acquire(); + } + + return createChannel(id, width, height, pixfmt); +} + +void VideoStackManager::unrefChannel(const std::string& id, + int width, + int height, + AVPixelFormat pixfmt) +{ + + std::lock_guard lock(_mx); + auto key = id + std::to_string(width) + std::to_string(height) + std::to_string(pixfmt); + auto chn_it = _channelMap.find(key); + if (chn_it != _channelMap.end() && chn_it->second->dispose()) { + _channelMap.erase(chn_it); + + auto player_it = _playerMap.find(id); + if (player_it != _playerMap.end() && player_it->second->dispose()) { + _playerMap.erase(player_it); + } + } +} + +int VideoStackManager::startVideoStack(const Json::Value& json) +{ + + std::string id; + int width, height; + auto params = parseParams(json, id, width, height); + + if (!params) { + ErrorL << "Videostack parse params failed!"; + return -1; + } + + auto stack = std::make_shared(id, width, height); + + for (auto& p : (*params)) { + if (!p) + continue; + p->weak_chn = getChannel(p->id, p->width, p->height, p->pixfmt); + } + + stack->setParam(params); + stack->start(); + + std::lock_guard lock(_mx); + _stackMap[id] = stack; + return 0; +} + +int VideoStackManager::resetVideoStack(const Json::Value& json) +{ + std::string id; + int width, height; + auto params = parseParams(json, id, width, height); + + if (!params) { + return -1; + } + + VideoStack::Ptr stack; + { + std::lock_guard lock(_mx); + auto it = _stackMap.find(id); + if (it == _stackMap.end()) { + return -2; + } + stack = it->second; + } + + for (auto& p : (*params)) { + if (!p) + continue; + p->weak_chn = getChannel(p->id, p->width, p->height, p->pixfmt); + } + + stack->setParam(params); + return 0; +} + +int VideoStackManager::stopVideoStack(const std::string& id) +{ + std::lock_guard lock(_mx); + auto it = _stackMap.find(id); + if (it != _stackMap.end()) { + _stackMap.erase(it); + return 0; + } + return -1; +} + +mediakit::FFmpegFrame::Ptr VideoStackManager::getBgImg() +{ + return _bgImg; +} + +Params VideoStackManager::parseParams(const Json::Value& json, + std::string& id, + int& width, + int& height) +{ + try { + id = json["id"].asString(); + + width = json["width"].asInt(); + height = json["height"].asInt(); + + int rows = json["row"].asInt(); //堆叠行数 + int cols = json["col"].asInt(); //堆叠列数 + float gapv = json["gapv"].asFloat(); //垂直间距 + float gaph = json["gaph"].asFloat(); //水平间距 + + //单个间距 + int gaphPix = static_cast(std::round(width * gaph)); + int gapvPix = static_cast(std::round(height * gapv)); + + // 根据间距计算格子宽高 + int gridWidth = cols > 1 ? (width - gaphPix * (cols - 1)) / cols : width; + int gridHeight = rows > 1 ? (height - gapvPix * (rows - 1)) / rows : height; + + auto params = std::make_shared>(rows * cols); + + for (int row = 0; row < rows; row++) { + for (int col = 0; col < cols; col++) { + std::string url = json["url"][row][col].asString(); + + auto param = std::make_shared(); + param->posX = gridWidth * col + col * gaphPix; + param->posY = gridHeight * row + row * gapvPix; + param->width = gridWidth; + param->height = gridHeight; + param->id = url; + + (*params)[row * cols + col] = param; + } + } + + //判断是否需要合并格子 (焦点屏) + if (!json["span"].empty() && json.isMember("span")) { + for (const auto& subArray : json["span"]) { + if (!subArray.isArray() || subArray.size() != 2) { + throw Json::LogicError("Incorrect 'span' sub-array format in JSON"); + } + std::array mergePos; + int index = 0; + + for (const auto& innerArray : subArray) { + if (!innerArray.isArray() || innerArray.size() != 2) { + throw Json::LogicError("Incorrect 'span' inner-array format in JSON"); + } + for (const auto& number : innerArray) { + if (index < mergePos.size()) { + mergePos[index++] = number.asInt(); + } + } + } + + for (int i = mergePos[0]; i <= mergePos[2]; i++) { + for (int j = mergePos[1]; j <= mergePos[3]; j++) { + if (i == mergePos[0] && j == mergePos[1]) { + (*params)[i * cols + j]->width = (mergePos[3] - mergePos[1] + 1) * gridWidth + (mergePos[3] - mergePos[1]) * gapvPix; + (*params)[i * cols + j]->height = (mergePos[2] - mergePos[0] + 1) * gridHeight + (mergePos[2] - mergePos[0]) * gaphPix; + } else { + (*params)[i * cols + j] = nullptr; + } + } + } + } + } + return params; + } catch (const std::exception& e) { + ErrorL << "Videostack parse params failed! " << e.what(); + return nullptr; + } +} + +bool VideoStackManager::loadBgImg(const std::string& path) +{ + _bgImg = std::make_shared(); + + _bgImg->get()->width = 1280; + _bgImg->get()->height = 720; + _bgImg->get()->format = AV_PIX_FMT_YUV420P; + + av_frame_get_buffer(_bgImg->get(), 32); + + std::ifstream file(path, std::ios::binary); + if (!file.is_open()) { + return false; + } + + file.read((char*)_bgImg->get()->data[0], _bgImg->get()->linesize[0] * _bgImg->get()->height); // Y + file.read((char*)_bgImg->get()->data[1], _bgImg->get()->linesize[1] * _bgImg->get()->height / 2); // U + file.read((char*)_bgImg->get()->data[2], _bgImg->get()->linesize[2] * _bgImg->get()->height / 2); // V + return true; +} + +Channel::Ptr VideoStackManager::createChannel(const std::string& id, + int width, + int height, + AVPixelFormat pixfmt) +{ + + std::lock_guard lock(_mx); + StackPlayer::Ptr player; + auto it = _playerMap.find(id); + if (it != _playerMap.end()) { + player = it->second->acquire(); + } else { + player = createPlayer(id); + } + + auto refChn = std::make_shared>(std::make_shared(id, width, height, pixfmt)); + auto chn = refChn->acquire(); + player->addChannel(chn); + + _channelMap[id + std::to_string(width) + std::to_string(height) + std::to_string(pixfmt)] = refChn; + return chn; +} + +StackPlayer::Ptr VideoStackManager::createPlayer(const std::string& id) +{ + std::lock_guard lock(_mx); + auto refPlayer = std::make_shared>(std::make_shared(id)); + _playerMap[id] = refPlayer; + + auto player = refPlayer->acquire(); + if (!id.empty()) { + player->play(); + } + + return player; +} +#endif \ No newline at end of file diff --git a/server/VideoStack.h b/server/VideoStack.h new file mode 100644 index 00000000..d4378ab3 --- /dev/null +++ b/server/VideoStack.h @@ -0,0 +1,207 @@ +#pragma once +#if defined(ENABLE_X264) && defined(ENABLE_FFMPEG) +#include "Codec/Transcode.h" +#include "Common/Device.h" +#include "Player/MediaPlayer.h" +#include "json/json.h" +#include +template +class RefWrapper { + public: + using Ptr = std::shared_ptr>; + + template + explicit RefWrapper(Args&&... args) + : _rc(0) + , _entity(std::forward(args)...) + { + } + + T acquire() + { + ++_rc; + return _entity; + } + + bool dispose() { return --_rc <= 0; } + + private: + T _entity; + std::atomic _rc; +}; + +class Channel; + +struct Param { + using Ptr = std::shared_ptr; + + int posX = 0; + int posY = 0; + int width = 0; + int height = 0; + AVPixelFormat pixfmt = AV_PIX_FMT_YUV420P; + std::string id {}; + + // runtime + std::weak_ptr weak_chn; + std::weak_ptr weak_buf; + + ~Param(); +}; + +using Params = std::shared_ptr>; + +class Channel : public std::enable_shared_from_this { + public: + using Ptr = std::shared_ptr; + + Channel(const std::string& id, int width, int height, AVPixelFormat pixfmt); + + void addParam(const std::weak_ptr& p); + + void onFrame(const mediakit::FFmpegFrame::Ptr& frame); + + void fillBuffer(const Param::Ptr& p); + + protected: + void forEachParam(const std::function& func); + + void copyData(const mediakit::FFmpegFrame::Ptr& buf, const Param::Ptr& p); + + private: + std::string _id; + int _width; + int _height; + AVPixelFormat _pixfmt; + + mediakit::FFmpegFrame::Ptr _tmp; + + std::recursive_mutex _mx; + std::vector> _params; + + mediakit::FFmpegSws::Ptr _sws; +}; + +class StackPlayer : public std::enable_shared_from_this { + public: + using Ptr = std::shared_ptr; + + StackPlayer(const std::string& url) + : _url(url) + { + } + + void addChannel(const std::weak_ptr& chn); + + void play(); + + void onFrame(const mediakit::FFmpegFrame::Ptr& frame); + + void onDisconnect(); + + protected: + void rePlay(const std::string& url); + + private: + std::string _url; + mediakit::MediaPlayer::Ptr _player; + + //用于断线重连 + toolkit::Timer::Ptr _timer; + int _failedCount = 0; + + std::recursive_mutex _mx; + std::vector> _channels; +}; + +class VideoStack { + public: + using Ptr = std::shared_ptr; + + VideoStack(const std::string& url, + int width = 1920, + int height = 1080, + AVPixelFormat pixfmt = AV_PIX_FMT_YUV420P, + float fps = 25.0, + int bitRate = 2 * 1024 * 1024); + + ~VideoStack(); + + void setParam(const Params& params); + + void start(); + + protected: + void initBgColor(); + + public: + Params _params; + + mediakit::FFmpegFrame::Ptr _buffer; + + private: + std::string _id; + int _width; + int _height; + AVPixelFormat _pixfmt; + float _fps; + int _bitRate; + + mediakit::DevChannel::Ptr _dev; + + bool _isExit; + + std::thread _thread; +}; + +class VideoStackManager { + public: + static VideoStackManager& Instance(); + + Channel::Ptr getChannel(const std::string& id, + int width, + int height, + AVPixelFormat pixfmt); + + void unrefChannel(const std::string& id, + int width, + int height, + AVPixelFormat pixfmt); + + int startVideoStack(const Json::Value& json); + + int resetVideoStack(const Json::Value& json); + + int stopVideoStack(const std::string& id); + + bool loadBgImg(const std::string& path); + + mediakit::FFmpegFrame::Ptr getBgImg(); + + protected: + Params parseParams(const Json::Value& json, + std::string& id, + int& width, + int& height); + + protected: + Channel::Ptr createChannel(const std::string& id, + int width, + int height, + AVPixelFormat pixfmt); + + StackPlayer::Ptr createPlayer(const std::string& id); + + private: + mediakit::FFmpegFrame::Ptr _bgImg; + + private: + std::recursive_mutex _mx; + + std::unordered_map _stackMap; + + std::unordered_map::Ptr> _channelMap; + + std::unordered_map::Ptr> _playerMap; +}; +#endif \ No newline at end of file diff --git a/server/WebApi.cpp b/server/WebApi.cpp index d0ab2e43..846f280a 100755 --- a/server/WebApi.cpp +++ b/server/WebApi.cpp @@ -62,6 +62,10 @@ #include "version.h" #endif +#if defined(ENABLE_X264) && defined (ENABLE_FFMPEG) +#include "VideoStack.h" +#endif + using namespace std; using namespace Json; using namespace toolkit; @@ -1925,6 +1929,36 @@ void installWebApi() { invoker(401, StrCaseMap {}, "None http access event listener"); } }); + +#if defined(ENABLE_X264) && defined(ENABLE_FFMPEG) + VideoStackManager::Instance().loadBgImg("novideo.yuv"); + NoticeCenter::Instance().addListener(nullptr, Broadcast::kBroadcastStreamNoneReader, [](BroadcastStreamNoneReaderArgs) { + auto id = sender.getMediaTuple().stream; + VideoStackManager::Instance().stopVideoStack(id); + InfoL << "VideoStack: " << id <<" stop"; + }); + + api_regist("/index/api/stack/start", [](API_ARGS_JSON_ASYNC) { + CHECK_SECRET(); + auto ret = VideoStackManager::Instance().startVideoStack(allArgs.getArgs()); + if (!ret) { + invoker(200, headerOut, "success"); + } else { + invoker(200, headerOut, "failed"); + } + }); + + api_regist("/index/api/stack/stop", [](API_ARGS_MAP_ASYNC) { + CHECK_SECRET(); + CHECK_ARGS("id"); + auto ret = VideoStackManager::Instance().stopVideoStack(allArgs["id"]); + if (!ret) { + invoker(200, headerOut, "success"); + } else { + invoker(200, headerOut, "failed"); + } + }); +#endif } void unInstallWebApi(){ From 5a137f8b8ed3aafce7f05dd0b0291554738a14ae Mon Sep 17 00:00:00 2001 From: xia-chu <771730766@qq.com> Date: Sun, 17 Mar 2024 10:28:10 +0800 Subject: [PATCH 11/60] Update submodule --- 3rdpart/media-server | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/3rdpart/media-server b/3rdpart/media-server index 8bc32a51..527c0f51 160000 --- a/3rdpart/media-server +++ b/3rdpart/media-server @@ -1 +1 @@ -Subproject commit 8bc32a516b279414f749d0dead8bdc2837d3c527 +Subproject commit 527c0f5117b489fda78fcd123d446370ddd9ec9a From 12d9351666ee762b63446884d43a7a2f5e72f559 Mon Sep 17 00:00:00 2001 From: xiongguangjie Date: Fri, 22 Mar 2024 20:41:14 +0800 Subject: [PATCH 12/60] Fix compile error for enable_webrtc is off ( #3393 #3397) --- api/source/mk_events.cpp | 6 +++++- api/source/mk_events_objects.cpp | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/api/source/mk_events.cpp b/api/source/mk_events.cpp index 52ac6e57..7f5ed661 100644 --- a/api/source/mk_events.cpp +++ b/api/source/mk_events.cpp @@ -14,7 +14,10 @@ #include "Http/HttpSession.h" #include "Rtsp/RtspSession.h" #include "Record/MP4Recorder.h" + +#ifdef ENABLE_WEBRTC #include "webrtc/WebRtcTransport.h" +#endif using namespace toolkit; using namespace mediakit; @@ -168,7 +171,7 @@ API_EXPORT void API_CALL mk_events_listen(const mk_events *events){ sender.getMediaTuple().stream.c_str(), ssrc.c_str(), ex.getErrCode(), ex.what()); } }); - +#ifdef ENABLE_WEBRTC NoticeCenter::Instance().addListener(&s_tag, Broadcast::kBroadcastRtcSctpConnecting,[](BroadcastRtcSctpConnectArgs){ if (s_events.on_mk_rtc_sctp_connecting) { s_events.on_mk_rtc_sctp_connecting((mk_rtc_transport)&sender); @@ -204,6 +207,7 @@ API_EXPORT void API_CALL mk_events_listen(const mk_events *events){ s_events.on_mk_rtc_sctp_received((mk_rtc_transport)&sender, streamId, ppid, msg, len); } }); +#endif }); } diff --git a/api/source/mk_events_objects.cpp b/api/source/mk_events_objects.cpp index 89b8c659..c30fd0cd 100644 --- a/api/source/mk_events_objects.cpp +++ b/api/source/mk_events_objects.cpp @@ -17,7 +17,10 @@ #include "Http/HttpClient.h" #include "Rtsp/RtspSession.h" + +#ifdef ENABLE_WEBRTC #include "webrtc/WebRtcTransport.h" +#endif using namespace toolkit; using namespace mediakit; From 66a62531600ea73b7742302e787283b4241c76ce Mon Sep 17 00:00:00 2001 From: ljx0305 Date: Fri, 22 Mar 2024 20:42:02 +0800 Subject: [PATCH 13/60] Fix compilation error issues (#3385) --- server/VideoStack.cpp | 8 ++++---- server/WebApi.h | 2 -- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/server/VideoStack.cpp b/server/VideoStack.cpp index 6b860fe5..ff4804f1 100644 --- a/server/VideoStack.cpp +++ b/server/VideoStack.cpp @@ -180,7 +180,7 @@ void StackPlayer::play() self->onFrame(frame); }); - videoTrack->addDelegate((std::function)[decoder](const mediakit::Frame::Ptr& frame) { + videoTrack->addDelegate([decoder](const mediakit::Frame::Ptr& frame) { return decoder->inputFrame(frame, false, true); }); } @@ -467,8 +467,8 @@ Params VideoStackManager::parseParams(const Json::Value& json, float gaph = json["gaph"].asFloat(); //水平间距 //单个间距 - int gaphPix = static_cast(std::round(width * gaph)); - int gapvPix = static_cast(std::round(height * gapv)); + int gaphPix = static_cast(round(width * gaph)); + int gapvPix = static_cast(round(height * gapv)); // 根据间距计算格子宽高 int gridWidth = cols > 1 ? (width - gaphPix * (cols - 1)) / cols : width; @@ -587,4 +587,4 @@ StackPlayer::Ptr VideoStackManager::createPlayer(const std::string& id) return player; } -#endif \ No newline at end of file +#endif diff --git a/server/WebApi.h b/server/WebApi.h index eff4c93f..1d1be694 100755 --- a/server/WebApi.h +++ b/server/WebApi.h @@ -234,8 +234,6 @@ void unInstallWebApi(); #if defined(ENABLE_RTPPROXY) uint16_t openRtpServer(uint16_t local_port, const std::string &stream_id, int tcp_mode, const std::string &local_ip, bool re_use_port, uint32_t ssrc, int only_track, bool multiplex=false); -void connectRtpServer(const std::string &stream_id, const std::string &dst_url, uint16_t dst_port, const std::function &cb); -bool closeRtpServer(const std::string &stream_id); #endif Json::Value makeMediaSourceJson(mediakit::MediaSource &media); From 5036aa5ec556adfb2603cec1b1e1bfebdde02627 Mon Sep 17 00:00:00 2001 From: xia-chu <771730766@qq.com> Date: Fri, 22 Mar 2024 20:29:22 +0800 Subject: [PATCH 14/60] BugFix: crashes when exceptions are thrown during destruction #3402 --- src/Rtmp/RtmpMediaSourceMuxer.h | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Rtmp/RtmpMediaSourceMuxer.h b/src/Rtmp/RtmpMediaSourceMuxer.h index 6572fae5..1d16d5de 100644 --- a/src/Rtmp/RtmpMediaSourceMuxer.h +++ b/src/Rtmp/RtmpMediaSourceMuxer.h @@ -29,7 +29,13 @@ public: getRtmpRing()->setDelegate(_media_src); } - ~RtmpMediaSourceMuxer() override { RtmpMuxer::flush(); } + ~RtmpMediaSourceMuxer() override { + try { + RtmpMuxer::flush(); + } catch (std::exception &ex) { + WarnL << ex.what(); + } + } void setListener(const std::weak_ptr &listener){ setDelegate(listener); From db4c570d19e763fa74c338f3a4b144cf9bb04ef5 Mon Sep 17 00:00:00 2001 From: xia-chu <771730766@qq.com> Date: Fri, 22 Mar 2024 20:30:48 +0800 Subject: [PATCH 15/60] WebRTC audio preferred PCMA --- conf/config.ini | 2 +- webrtc/Sdp.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/conf/config.ini b/conf/config.ini index 8f566338..3c8b67c8 100644 --- a/conf/config.ini +++ b/conf/config.ini @@ -357,7 +357,7 @@ tcpPort = 8000 rembBitRate=0 #rtc支持的音频codec类型,在前面的优先级更高 #以下范例为所有支持的音频codec -preferredCodecA=PCMU,PCMA,opus,mpeg4-generic +preferredCodecA=PCMA,PCMU,opus,mpeg4-generic #rtc支持的视频codec类型,在前面的优先级更高 #以下范例为所有支持的视频codec preferredCodecV=H264,H265,AV1,VP9,VP8 diff --git a/webrtc/Sdp.cpp b/webrtc/Sdp.cpp index bed1bf57..55f94e38 100644 --- a/webrtc/Sdp.cpp +++ b/webrtc/Sdp.cpp @@ -23,7 +23,7 @@ namespace Rtc { const string kPreferredCodecA = RTC_FIELD"preferredCodecA"; const string kPreferredCodecV = RTC_FIELD"preferredCodecV"; static onceToken token([]() { - mINI::Instance()[kPreferredCodecA] = "PCMU,PCMA,opus,mpeg4-generic"; + mINI::Instance()[kPreferredCodecA] = "PCMA,PCMU,opus,mpeg4-generic"; mINI::Instance()[kPreferredCodecV] = "H264,H265,AV1,VP9,VP8"; }); } From cfac61e55b459526099b7b4ca623d5ac7cb2ba1a Mon Sep 17 00:00:00 2001 From: xia-chu <771730766@qq.com> Date: Fri, 22 Mar 2024 20:34:07 +0800 Subject: [PATCH 16/60] BugFix: prevent the player's configuration from being overridden in addStreamProxy --- server/WebApi.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/WebApi.cpp b/server/WebApi.cpp index 846f280a..9ea3681d 100755 --- a/server/WebApi.cpp +++ b/server/WebApi.cpp @@ -584,8 +584,10 @@ void addStreamProxy(const string &vhost, const string &app, const string &stream //添加拉流代理 auto player = s_player_proxy.make(key, vhost, app, stream, option, retry_count); - // 先透传参数 - player->mINI::operator=(args); + // 先透传拷贝参数 + for (auto &pr : args) { + (*player)[pr.first] = pr.second; + } //指定RTP over TCP(播放rtsp时有效) (*player)[Client::kRtpType] = rtp_type; From e972ec5a22d631798832c2a08a9bb9442300037c Mon Sep 17 00:00:00 2001 From: xia-chu <771730766@qq.com> Date: Fri, 22 Mar 2024 20:35:11 +0800 Subject: [PATCH 17/60] Remove deprecated code --- src/Common/Parser.cpp | 4 +-- src/Common/strCoding.cpp | 63 ---------------------------------------- src/Common/strCoding.h | 2 -- src/Http/HttpSession.cpp | 12 -------- src/Http/HttpSession.h | 1 - 5 files changed, 2 insertions(+), 80 deletions(-) diff --git a/src/Common/Parser.cpp b/src/Common/Parser.cpp index ab41c53e..7e7de860 100644 --- a/src/Common/Parser.cpp +++ b/src/Common/Parser.cpp @@ -294,8 +294,8 @@ void RtspUrl::setup(bool is_ssl, const string &url, const string &user, const st splitUrl(ip, ip, port); _url = std::move(url); - _user = strCoding::UrlDecode(std::move(user)); - _passwd = strCoding::UrlDecode(std::move(passwd)); + _user = strCoding::UrlDecodeComponent(user); + _passwd = strCoding::UrlDecodeComponent(passwd); _host = std::move(ip); _port = port; _is_ssl = is_ssl; diff --git a/src/Common/strCoding.cpp b/src/Common/strCoding.cpp index 59a0b7e4..1a0f0236 100644 --- a/src/Common/strCoding.cpp +++ b/src/Common/strCoding.cpp @@ -53,22 +53,6 @@ char HexStrToBin(const char *str) { return (high << 4) | low; } -string strCoding::UrlEncode(const string &str) { - string out; - size_t len = str.size(); - for (size_t i = 0; i < len; ++i) { - char ch = str[i]; - if (isalnum((uint8_t) ch)) { - out.push_back(ch); - } else { - char buf[4]; - sprintf(buf, "%%%X%X", (uint8_t) ch >> 4, (uint8_t) ch & 0x0F); - out.append(buf); - } - } - return out; -} - string strCoding::UrlEncodePath(const string &str) { const char *dont_escape = "!#&'*+:=?@/._-$,;~()"; string out; @@ -103,32 +87,6 @@ string strCoding::UrlEncodeComponent(const string &str) { return out; } -string strCoding::UrlDecode(const string &str) { - string output; - size_t i = 0, len = str.length(); - while (i < len) { - if (str[i] == '%') { - if (i + 3 > len) { - // %后面必须还有两个字节才会反转义 - output.append(str, i, len - i); - break; - } - char ch = HexStrToBin(&(str[i + 1])); - if (ch == -1) { - // %后面两个字节不是16进制字符串,转义失败;直接拼接3个原始字符 - output.append(str, i, 3); - } else { - output += ch; - } - i += 3; - } else { - output += str[i]; - ++i; - } - } - return output; -} - string strCoding::UrlDecodePath(const string &str) { const char *dont_unescape = "#$&+,/:;=?@"; string output; @@ -185,27 +143,6 @@ std::string strCoding::UrlDecodeComponent(const std::string &str) { return output; } -#if 0 -#include "Util/onceToken.h" -static toolkit::onceToken token([]() { - auto str0 = strCoding::UrlDecode( - "rtsp%3A%2F%2Fadmin%3AJm13317934%25jm%40111.47.84.69%3A554%2FStreaming%2FChannels%2F101%3Ftransportmode%3Dunicast%26amp%3Bprofile%3DProfile_1"); - auto str1 = strCoding::UrlDecode("%j1"); // 测试%后面两个字节不是16进制字符串 - auto str2 = strCoding::UrlDecode("%a"); // 测试%后面字节数不够 - auto str3 = strCoding::UrlDecode("%"); // 测试只有% - auto str4 = strCoding::UrlDecode("%%%"); // 测试多个% - auto str5 = strCoding::UrlDecode("%%%%40"); // 测试多个非法%后恢复正常解析 - auto str6 = strCoding::UrlDecode("Jm13317934%jm"); // 测试多个非法%后恢复正常解析 - cout << str0 << endl; - cout << str1 << endl; - cout << str2 << endl; - cout << str3 << endl; - cout << str4 << endl; - cout << str5 << endl; - cout << str6 << endl; -}); -#endif - ///////////////////////////////windows专用/////////////////////////////////// #if defined(_WIN32) void UnicodeToGB2312(char* pOut, wchar_t uData) diff --git a/src/Common/strCoding.h b/src/Common/strCoding.h index 14371704..e715e74d 100644 --- a/src/Common/strCoding.h +++ b/src/Common/strCoding.h @@ -18,10 +18,8 @@ namespace mediakit { class strCoding { public: - [[deprecated]] static std::string UrlEncode(const std::string &str); //url utf8编码, deprecated static std::string UrlEncodePath(const std::string &str); //url路径 utf8编码 static std::string UrlEncodeComponent(const std::string &str); // url参数 utf8编码 - [[deprecated]] static std::string UrlDecode(const std::string &str); //url utf8解码, deprecated static std::string UrlDecodePath(const std::string &str); //url路径 utf8解码 static std::string UrlDecodeComponent(const std::string &str); // url参数 utf8解码 #if defined(_WIN32) diff --git a/src/Http/HttpSession.cpp b/src/Http/HttpSession.cpp index 3c34a661..ba74d803 100644 --- a/src/Http/HttpSession.cpp +++ b/src/Http/HttpSession.cpp @@ -683,18 +683,6 @@ void HttpSession::sendResponse(int code, AsyncSender::onSocketFlushed(data); } -string HttpSession::urlDecode(const string &str) { - auto ret = strCoding::UrlDecode(str); -#ifdef _WIN32 - GET_CONFIG(string, charSet, Http::kCharSet); - bool isGb2312 = !strcasecmp(charSet.data(), "gb2312"); - if (isGb2312) { - ret = strCoding::UTF8ToGB2312(ret); - } -#endif // _WIN32 - return ret; -} - string HttpSession::urlDecodePath(const string &str) { auto ret = strCoding::UrlDecodePath(str); #ifdef _WIN32 diff --git a/src/Http/HttpSession.h b/src/Http/HttpSession.h index 2bc1c353..0ffbf137 100644 --- a/src/Http/HttpSession.h +++ b/src/Http/HttpSession.h @@ -44,7 +44,6 @@ public: void onRecv(const toolkit::Buffer::Ptr &) override; void onError(const toolkit::SockException &err) override; void onManager() override; - [[deprecated]] static std::string urlDecode(const std::string &str); static std::string urlDecodePath(const std::string &str); static std::string urlDecodeComponent(const std::string &str); void setTimeoutSec(size_t second); From 49ddde28c014dcde6b8c1259612e3d6e802ed136 Mon Sep 17 00:00:00 2001 From: xia-chu <771730766@qq.com> Date: Fri, 22 Mar 2024 20:36:09 +0800 Subject: [PATCH 18/60] Refactor code --- ext-codec/H264Rtmp.cpp | 8 -------- ext-codec/H265Rtmp.cpp | 8 -------- src/Common/macros.h | 10 ++++++++++ 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/ext-codec/H264Rtmp.cpp b/ext-codec/H264Rtmp.cpp index 30bf10eb..6dd50807 100644 --- a/ext-codec/H264Rtmp.cpp +++ b/ext-codec/H264Rtmp.cpp @@ -14,14 +14,6 @@ using namespace std; using namespace toolkit; -#define CHECK_RET(...) \ - try { \ - CHECK(__VA_ARGS__); \ - } catch (AssertFailedException & ex) { \ - WarnL << ex.what(); \ - return; \ - } - namespace mediakit { void H264RtmpDecoder::inputRtmp(const RtmpPacket::Ptr &pkt) { diff --git a/ext-codec/H265Rtmp.cpp b/ext-codec/H265Rtmp.cpp index 2b88795d..69a27390 100644 --- a/ext-codec/H265Rtmp.cpp +++ b/ext-codec/H265Rtmp.cpp @@ -18,14 +18,6 @@ using namespace std; using namespace toolkit; -#define CHECK_RET(...) \ - try { \ - CHECK(__VA_ARGS__); \ - } catch (AssertFailedException & ex) { \ - WarnL << ex.what(); \ - return; \ - } - namespace mediakit { void H265RtmpDecoder::inputRtmp(const RtmpPacket::Ptr &pkt) { diff --git a/src/Common/macros.h b/src/Common/macros.h index 796bd326..b1187810 100644 --- a/src/Common/macros.h +++ b/src/Common/macros.h @@ -36,6 +36,16 @@ #define CHECK(exp, ...) ::mediakit::Assert_ThrowCpp(!(exp), #exp, __FUNCTION__, __FILE__, __LINE__, ##__VA_ARGS__) #endif // CHECK +#ifndef CHECK_RET +#define CHECK_RET(...) \ + try { \ + CHECK(__VA_ARGS__); \ + } catch (AssertFailedException & ex) { \ + WarnL << ex.what(); \ + return; \ + } +#endif + #ifndef MAX #define MAX(a, b) ((a) > (b) ? (a) : (b)) #endif // MAX From f5ca61d235f02f18a9e0a4602056c77f67a34e09 Mon Sep 17 00:00:00 2001 From: xia-chu <771730766@qq.com> Date: Fri, 22 Mar 2024 20:37:27 +0800 Subject: [PATCH 19/60] Enhance compatibility with non-compliant RTMP AAC streams --- ext-codec/AACRtmp.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext-codec/AACRtmp.cpp b/ext-codec/AACRtmp.cpp index e20db4df..72c96d5a 100644 --- a/ext-codec/AACRtmp.cpp +++ b/ext-codec/AACRtmp.cpp @@ -17,7 +17,7 @@ using namespace toolkit; namespace mediakit { void AACRtmpDecoder::inputRtmp(const RtmpPacket::Ptr &pkt) { - CHECK(pkt->size() > 2); + CHECK_RET(pkt->size() > 2); if (pkt->isConfigFrame()) { getTrack()->setExtraData((uint8_t *)pkt->data() + 2, pkt->size() - 2); return; From b05f515cccddb130dd99e9e894a5a3b3d157e74b Mon Sep 17 00:00:00 2001 From: xia-chu <771730766@qq.com> Date: Fri, 22 Mar 2024 20:37:51 +0800 Subject: [PATCH 20/60] Revert "Fix the issue of failing to push streams to FMS 3.0 server" This reverts commit 3b7f16b75557a552d6907c60f87f4c06028c5f62. --- src/Rtmp/RtmpProtocol.cpp | 19 +------------------ src/Rtmp/RtmpProtocol.h | 4 ---- src/Rtmp/RtmpPusher.cpp | 30 ++++++++---------------------- 3 files changed, 9 insertions(+), 44 deletions(-) diff --git a/src/Rtmp/RtmpProtocol.cpp b/src/Rtmp/RtmpProtocol.cpp index f98468bf..010e30d4 100644 --- a/src/Rtmp/RtmpProtocol.cpp +++ b/src/Rtmp/RtmpProtocol.cpp @@ -165,14 +165,7 @@ void RtmpProtocol::sendResponse(int type, const string &str) { void RtmpProtocol::sendInvoke(const string &cmd, const AMFValue &val) { AMFEncoder enc; - if (val.type() == AMFType::AMF_OBJECT || val.type() == AMFType::AMF_NULL) - { - enc << cmd << ++_send_req_id << val; - } - else - { - enc << cmd << ++_send_req_id << AMFValue() << val; - } + enc << cmd << ++_send_req_id << val; sendRequest(MSG_CMD, enc.data()); } @@ -626,22 +619,12 @@ const char* RtmpProtocol::handle_rtmp(const char *data, size_t len) { case 12: chunk_data.is_abs_stamp = true; chunk_data.stream_index = load_le32(header->stream_index); - _last_stream_index = chunk_data.stream_index; case 8: chunk_data.body_size = load_be24(header->body_size); chunk_data.type_id = header->type_id; - _last_body_size = chunk_data.body_size; - _last_type_id = chunk_data.type_id; case 4: chunk_data.ts_field = load_be24(header->time_stamp); } - switch (header->fmt) { - case 2: - chunk_data.type_id = _last_type_id; - chunk_data.body_size = _last_body_size; - case 1: - chunk_data.stream_index = _last_stream_index; - } auto time_stamp = chunk_data.ts_field; if (chunk_data.ts_field == 0xFFFFFF) { diff --git a/src/Rtmp/RtmpProtocol.h b/src/Rtmp/RtmpProtocol.h index 2b7adfff..7ace93eb 100644 --- a/src/Rtmp/RtmpProtocol.h +++ b/src/Rtmp/RtmpProtocol.h @@ -11,7 +11,6 @@ #ifndef SRC_RTMP_RTMPPROTOCOL_H_ #define SRC_RTMP_RTMPPROTOCOL_H_ -#include #include #include #include @@ -88,9 +87,6 @@ protected: private: bool _data_started = false; int _now_chunk_id = 0; - uint32_t _last_stream_index = 0; - size_t _last_body_size = 0; - uint8_t _last_type_id = 0; ////////////ChunkSize//////////// size_t _chunk_size_in = DEFAULT_CHUNK_LEN; size_t _chunk_size_out = DEFAULT_CHUNK_LEN; diff --git a/src/Rtmp/RtmpPusher.cpp b/src/Rtmp/RtmpPusher.cpp index 8f3596fc..92363241 100644 --- a/src/Rtmp/RtmpPusher.cpp +++ b/src/Rtmp/RtmpPusher.cpp @@ -163,28 +163,14 @@ void RtmpPusher::send_connect() { } void RtmpPusher::send_createStream() { - // Workaround : 兼容较旧的 FMS3.0 - { - { - AMFValue obj(_stream_id); - sendInvoke("releaseStream", obj); - } - { - AMFValue obj(_stream_id); - sendInvoke("FCPublish", obj); - } - } - { - AMFValue obj(AMF_NULL); - sendInvoke("createStream", obj); - addOnResultCB([this](AMFDecoder &dec) { - //TraceL << "createStream result"; - dec.load(); - _stream_index = dec.load(); - send_publish(); - }); - } - + AMFValue obj(AMF_NULL); + sendInvoke("createStream", obj); + addOnResultCB([this](AMFDecoder &dec) { + //TraceL << "createStream result"; + dec.load(); + _stream_index = dec.load(); + send_publish(); + }); } #define RTMP_STREAM_LIVE "live" From d6387543645e6d1b1b2da309d7ce0d8f8bbe6774 Mon Sep 17 00:00:00 2001 From: xiongziliang <771730766@qq.com> Date: Sat, 23 Mar 2024 20:44:25 +0800 Subject: [PATCH 21/60] Update authors --- AUTHORS | 6 +++++- README.md | 5 +++++ README_en.md | 5 +++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 6a25530e..fcc45452 100644 --- a/AUTHORS +++ b/AUTHORS @@ -44,7 +44,6 @@ Xinghua Zhao <(holychaossword@hotmail.com> [Dw9](https://github.com/Dw9) 明月惊鹊 cgm <2958580318@qq.com> -hejilin <1724010622@qq.com> alexliyu7352 cgm <2958580318@qq.com> [haorui wang](https://github.com/HaoruiWang) @@ -104,3 +103,8 @@ WuPeng [sandro-qiang](https://github.com/sandro-qiang) [Paul Philippov](https://github.com/themactep) [张传峰](https://github.com/zhang-chuanfeng) +[lidaofu-hub](https://github.com/lidaofu-hub) +[huangcaichun](https://github.com/huangcaichun) +[jamesZHANG500](https://github.com/jamesZHANG500) +[weidelong](https://github.com/wdl1697454803) +[小强先生](https://github.com/linshangqiang) \ No newline at end of file diff --git a/README.md b/README.md index c08ed51f..44c22aa9 100644 --- a/README.md +++ b/README.md @@ -358,6 +358,11 @@ bash build_docker_images.sh [sandro-qiang](https://github.com/sandro-qiang) [Paul Philippov](https://github.com/themactep) [张传峰](https://github.com/zhang-chuanfeng) +[lidaofu-hub](https://github.com/lidaofu-hub) +[huangcaichun](https://github.com/huangcaichun) +[jamesZHANG500](https://github.com/jamesZHANG500) +[weidelong](https://github.com/wdl1697454803) +[小强先生](https://github.com/linshangqiang) 同时感谢JetBrains对开源项目的支持,本项目使用CLion开发与调试: diff --git a/README_en.md b/README_en.md index 94ca9871..b41eea20 100644 --- a/README_en.md +++ b/README_en.md @@ -516,6 +516,11 @@ Thanks to all those who have supported this project in various ways, including b [sandro-qiang](https://github.com/sandro-qiang) [Paul Philippov](https://github.com/themactep) [张传峰](https://github.com/zhang-chuanfeng) +[lidaofu-hub](https://github.com/lidaofu-hub) +[huangcaichun](https://github.com/huangcaichun) +[jamesZHANG500](https://github.com/jamesZHANG500) +[weidelong](https://github.com/wdl1697454803) +[小强先生](https://github.com/linshangqiang) Also thank to JetBrains for their support for open source project, we developed and debugged zlmediakit with CLion: From 2e2823d4cfe273075a1e3b4c4051f37770f0bfca Mon Sep 17 00:00:00 2001 From: KkemChen Date: Sat, 23 Mar 2024 20:47:38 +0800 Subject: [PATCH 22/60] VideoStack: move sws execution to WorkThread and optimization interface (#3407) VideoStack: move sws execution to WorkThread and optimization interface --- server/VideoStack.cpp | 18 ++++++++++-------- server/VideoStack.h | 1 + server/WebApi.cpp | 17 ++++++----------- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/server/VideoStack.cpp b/server/VideoStack.cpp index ff4804f1..d5168147 100644 --- a/server/VideoStack.cpp +++ b/server/VideoStack.cpp @@ -62,15 +62,16 @@ void Channel::addParam(const std::weak_ptr& p) void Channel::onFrame(const mediakit::FFmpegFrame::Ptr& frame) { std::weak_ptr weakSelf = shared_from_this(); - // toolkit::WorkThreadPool::Instance().getFirstPoller()->async([weakSelf, frame]() { - auto self = weakSelf.lock(); - if (!self) { - return; - } - self->_tmp = self->_sws->inputFrame(frame); + _poller = _poller ? _poller : toolkit::WorkThreadPool::Instance().getPoller(); + _poller->async([weakSelf, frame]() { + auto self = weakSelf.lock(); + if (!self) { + return; + } + self->_tmp = self->_sws->inputFrame(frame); - self->forEachParam([self](const Param::Ptr& p) { self->fillBuffer(p); }); - // }); + self->forEachParam([self](const Param::Ptr& p) { self->fillBuffer(p); }); + }); } void Channel::forEachParam(const std::function& func) @@ -440,6 +441,7 @@ int VideoStackManager::stopVideoStack(const std::string& id) auto it = _stackMap.find(id); if (it != _stackMap.end()) { _stackMap.erase(it); + InfoL << "VideoStack stop: " << id; return 0; } return -1; diff --git a/server/VideoStack.h b/server/VideoStack.h index d4378ab3..99455b40 100644 --- a/server/VideoStack.h +++ b/server/VideoStack.h @@ -80,6 +80,7 @@ class Channel : public std::enable_shared_from_this { std::vector> _params; mediakit::FFmpegSws::Ptr _sws; + toolkit::EventPoller::Ptr _poller; }; class StackPlayer : public std::enable_shared_from_this { diff --git a/server/WebApi.cpp b/server/WebApi.cpp index 9ea3681d..9b1c8ba9 100755 --- a/server/WebApi.cpp +++ b/server/WebApi.cpp @@ -1937,28 +1937,23 @@ void installWebApi() { NoticeCenter::Instance().addListener(nullptr, Broadcast::kBroadcastStreamNoneReader, [](BroadcastStreamNoneReaderArgs) { auto id = sender.getMediaTuple().stream; VideoStackManager::Instance().stopVideoStack(id); - InfoL << "VideoStack: " << id <<" stop"; }); api_regist("/index/api/stack/start", [](API_ARGS_JSON_ASYNC) { CHECK_SECRET(); auto ret = VideoStackManager::Instance().startVideoStack(allArgs.getArgs()); - if (!ret) { - invoker(200, headerOut, "success"); - } else { - invoker(200, headerOut, "failed"); - } + val["code"] = ret; + val["msg"] = ret ? "failed" : "success"; + invoker(200, headerOut, val.toStyledString()); }); api_regist("/index/api/stack/stop", [](API_ARGS_MAP_ASYNC) { CHECK_SECRET(); CHECK_ARGS("id"); auto ret = VideoStackManager::Instance().stopVideoStack(allArgs["id"]); - if (!ret) { - invoker(200, headerOut, "success"); - } else { - invoker(200, headerOut, "failed"); - } + val["code"] = ret; + val["msg"] = ret ? "failed" : "success"; + invoker(200, headerOut, val.toStyledString()); }); #endif } From 029813402d582cc75986d911b62f83d230d4af20 Mon Sep 17 00:00:00 2001 From: johzzy Date: Sat, 23 Mar 2024 11:46:30 -0300 Subject: [PATCH 23/60] feat: update negotiateSdp and WebRtcArgs (#3371) - update negotiateSdp - update HttpAllArgs and alias - update onRtcConfigure - define setWebRtcArgs, handle set_webrtc_cands and setLocalIp --------- Co-authored-by: xiongziliang <771730766@qq.com> Co-authored-by: KkemChen --- api/source/mk_common.cpp | 6 +- server/WebApi.cpp | 89 +++++++++--------------- server/WebApi.h | 77 +++++++-------------- src/Common/MediaSource.h | 18 ++--- src/Common/Parser.h | 2 +- webrtc/WebRtcEchoTest.h | 1 - webrtc/WebRtcPlayer.cpp | 8 +-- webrtc/WebRtcPlayer.h | 5 +- webrtc/WebRtcPusher.cpp | 8 +-- webrtc/WebRtcPusher.h | 5 +- webrtc/WebRtcTransport.cpp | 137 ++++++++++++++++++++----------------- webrtc/WebRtcTransport.h | 34 ++++----- 12 files changed, 171 insertions(+), 219 deletions(-) diff --git a/api/source/mk_common.cpp b/api/source/mk_common.cpp index c4a36ea1..f41d2976 100644 --- a/api/source/mk_common.cpp +++ b/api/source/mk_common.cpp @@ -304,10 +304,10 @@ API_EXPORT void API_CALL mk_webrtc_get_answer_sdp2(void *user_data, on_user_data std::string offer_str = offer; std::shared_ptr ptr(user_data, user_data_free ? user_data_free : [](void *) {}); auto args = std::make_shared(url); - WebRtcPluginManager::Instance().getAnswerSdp(*session, type, *args, - [offer_str, session, ptr, cb](const WebRtcInterface &exchanger) mutable { + WebRtcPluginManager::Instance().negotiateSdp(*session, type, *args, [offer_str, session, ptr, cb, args](const WebRtcInterface &exchanger) mutable { + auto &handler = const_cast(exchanger); try { - auto sdp_answer = exchangeSdp(exchanger, offer_str); + auto sdp_answer = handler.getAnswerSdp(offer_str); cb(ptr.get(), sdp_answer.data(), nullptr); } catch (std::exception &ex) { cb(ptr.get(), nullptr, ex.what()); diff --git a/server/WebApi.cpp b/server/WebApi.cpp index 9b1c8ba9..4174a6ad 100755 --- a/server/WebApi.cpp +++ b/server/WebApi.cpp @@ -119,7 +119,7 @@ static HttpApi toApi(const function &cb) { //参数解析成map auto args = getAllArgs(parser); - cb(sender, headerOut, HttpAllArgs(parser, args), val, invoker); + cb(sender, headerOut, ArgsMap(parser, args), val, invoker); }; } @@ -147,7 +147,7 @@ static HttpApi toApi(const function &cb) { Json::Reader reader; reader.parse(parser.content(), args); - cb(sender, headerOut, HttpAllArgs(parser, args), val, invoker); + cb(sender, headerOut, ArgsJson(parser, args), val, invoker); }; } @@ -167,7 +167,7 @@ static HttpApi toApi(const function &cb) { Json::Value val; val["code"] = API::Success; - cb(sender, headerOut, HttpAllArgs(parser, (string &)parser.content()), val, invoker); + cb(sender, headerOut, ArgsString(parser, (string &)parser.content()), val, invoker); }; } @@ -662,13 +662,6 @@ void addStreamPusherProxy(const string &schema, pusher->publish(url); } -template -static void getArgsValue(const HttpAllArgs &allArgs, const string &key, Type &value) { - auto val = allArgs[key]; - if (!val.empty()) { - value = (Type)val; - } -} /** * 安装api接口 @@ -735,7 +728,7 @@ void installWebApi() { CHECK_SECRET(); auto &ini = mINI::Instance(); int changed = API::Success; - for (auto &pr : allArgs.getArgs()) { + for (auto &pr : allArgs.args) { if (ini.find(pr.first) == ini.end()) { #if 1 //没有这个key @@ -1093,7 +1086,7 @@ void installWebApi() { CHECK_ARGS("vhost","app","stream","url"); mINI args; - for (auto &pr : allArgs.getArgs()) { + for (auto &pr : allArgs.args) { args.emplace(pr.first, pr.second); } @@ -1190,7 +1183,7 @@ void installWebApi() { //测试url http://127.0.0.1/index/api/downloadBin api_regist("/index/api/downloadBin",[](API_ARGS_MAP_ASYNC){ CHECK_SECRET(); - invoker.responseFile(allArgs.getParser().getHeader(),StrCaseMap(),exePath()); + invoker.responseFile(allArgs.parser.getHeader(), StrCaseMap(), exePath()); }); #if defined(ENABLE_RTPPROXY) @@ -1697,7 +1690,7 @@ void installWebApi() { //截图存在,且未过期,那么返回之 res_old_snap = true; - responseSnap(path, allArgs.getParser().getHeader(), invoker); + responseSnap(path, allArgs.parser.getHeader(), invoker); //中断遍历 return false; }); @@ -1728,7 +1721,7 @@ void installWebApi() { File::delete_file(new_snap); rename(new_snap_tmp.data(), new_snap.data()); } - responseSnap(new_snap, allArgs.getParser().getHeader(), invoker, err_msg); + responseSnap(new_snap, allArgs.parser.getHeader(), invoker, err_msg); }); }); @@ -1743,7 +1736,7 @@ void installWebApi() { #ifdef ENABLE_WEBRTC class WebRtcArgsImp : public WebRtcArgs { public: - WebRtcArgsImp(const HttpAllArgs &args, std::string session_id) + WebRtcArgsImp(const ArgsString &args, std::string session_id) : _args(args) , _session_id(std::move(session_id)) {} ~WebRtcArgsImp() override = default; @@ -1761,40 +1754,26 @@ void installWebApi() { CHECK_ARGS("app", "stream"); return StrPrinter << "rtc://" << _args["Host"] << "/" << _args["app"] << "/" - << _args["stream"] << "?" << _args.getParser().params() + "&session=" + _session_id; + << _args["stream"] << "?" << _args.parser.params() + "&session=" + _session_id; } private: - HttpAllArgs _args; + ArgsString _args; std::string _session_id; }; api_regist("/index/api/webrtc",[](API_ARGS_STRING_ASYNC){ CHECK_ARGS("type"); auto type = allArgs["type"]; - auto offer = allArgs.getArgs(); + auto offer = allArgs.args; CHECK(!offer.empty(), "http body(webrtc offer sdp) is empty"); - std::string host = allArgs.getParser()["Host"]; - std::string localIp = host.substr(0, host.find(':')); - - auto isVaildIP = [](std::string ip)-> bool { - int a,b,c,d; - return sscanf(ip.c_str(),"%d.%d.%d.%d", &a, &b, &c, &d) == 4; - }; - if (!isVaildIP(localIp) || localIp=="127.0.0.1") { - localIp = ""; - } + auto &session = static_cast(sender); auto args = std::make_shared(allArgs, sender.getIdentifier()); - WebRtcPluginManager::Instance().getAnswerSdp(static_cast(sender), type, *args, [invoker, val, offer, headerOut, localIp](const WebRtcInterface &exchanger) mutable { - //设置返回类型 - headerOut["Content-Type"] = HttpFileManager::getContentType(".json"); - //设置跨域 - headerOut["Access-Control-Allow-Origin"] = "*"; - + WebRtcPluginManager::Instance().negotiateSdp(session, type, *args, [invoker, val, offer, headerOut, args](const WebRtcInterface &exchanger) mutable { + auto &handler = const_cast(exchanger); try { - setLocalIp(exchanger,localIp); - val["sdp"] = exchangeSdp(exchanger, offer); + val["sdp"] = handler.getAnswerSdp(offer); val["id"] = exchanger.getIdentifier(); val["type"] = "answer"; invoker(200, headerOut, val.toStyledString()); @@ -1808,26 +1787,24 @@ void installWebApi() { static constexpr char delete_webrtc_url [] = "/index/api/delete_webrtc"; static auto whip_whep_func = [](const char *type, API_ARGS_STRING_ASYNC) { - auto offer = allArgs.getArgs(); + auto offer = allArgs.args; CHECK(!offer.empty(), "http body(webrtc offer sdp) is empty"); auto &session = static_cast(sender); - auto location = std::string("http") + (session.overSsl() ? "s" : "") + "://" + allArgs["host"] + delete_webrtc_url; + auto location = std::string(session.overSsl() ? "https://" : "http://") + allArgs["host"] + delete_webrtc_url; auto args = std::make_shared(allArgs, sender.getIdentifier()); - WebRtcPluginManager::Instance().getAnswerSdp(session, type, *args, - [invoker, offer, headerOut, location](const WebRtcInterface &exchanger) mutable { - // 设置跨域 - headerOut["Access-Control-Allow-Origin"] = "*"; - try { - // 设置返回类型 - headerOut["Content-Type"] = "application/sdp"; - headerOut["Location"] = location + "?id=" + exchanger.getIdentifier() + "&token=" + exchanger.deleteRandStr(); - invoker(201, headerOut, exchangeSdp(exchanger, offer)); - } catch (std::exception &ex) { - headerOut["Content-Type"] = "text/plain"; - invoker(406, headerOut, ex.what()); - } - }); + WebRtcPluginManager::Instance().negotiateSdp(session, type, *args, [invoker, offer, headerOut, location, args](const WebRtcInterface &exchanger) mutable { + auto &handler = const_cast(exchanger); + try { + // 设置返回类型 + headerOut["Content-Type"] = "application/sdp"; + headerOut["Location"] = location + "?id=" + exchanger.getIdentifier() + "&token=" + exchanger.deleteRandStr(); + invoker(201, headerOut, handler.getAnswerSdp(offer)); + } catch (std::exception &ex) { + headerOut["Content-Type"] = "text/plain"; + invoker(406, headerOut, ex.what()); + } + }); }; api_regist("/index/api/whip", [](API_ARGS_STRING_ASYNC) { whip_whep_func("push", API_ARGS_VALUE, invoker); }); @@ -1835,7 +1812,7 @@ void installWebApi() { api_regist(delete_webrtc_url, [](API_ARGS_MAP_ASYNC) { CHECK_ARGS("id", "token"); - CHECK(allArgs.getParser().method() == "DELETE", "http method is not DELETE: " + allArgs.getParser().method()); + CHECK(allArgs.parser.method() == "DELETE", "http method is not DELETE: " + allArgs.parser.method()); auto obj = WebRtcTransportManager::Instance().getItem(allArgs["id"]); if (!obj) { invoker(404, headerOut, "id not found"); @@ -1921,11 +1898,11 @@ void installWebApi() { if (!save_name.empty()) { res_header.emplace("Content-Disposition", "attachment;filename=\"" + save_name + "\""); } - invoker.responseFile(allArgs.getParser().getHeader(), res_header, allArgs["file_path"]); + invoker.responseFile(allArgs.parser.getHeader(), res_header, allArgs["file_path"]); } }; - bool flag = NOTICE_EMIT(BroadcastHttpAccessArgs, Broadcast::kBroadcastHttpAccess, allArgs.getParser(), file_path, false, file_invoker, sender); + bool flag = NOTICE_EMIT(BroadcastHttpAccessArgs, Broadcast::kBroadcastHttpAccess, allArgs.parser, file_path, false, file_invoker, sender); if (!flag) { // 文件下载鉴权事件无人监听,不允许下载 invoker(401, StrCaseMap {}, "None http access event listener"); diff --git a/server/WebApi.h b/server/WebApi.h index 1d1be694..95562bbf 100755 --- a/server/WebApi.h +++ b/server/WebApi.h @@ -115,72 +115,41 @@ std::string getValue(const mediakit::Parser &parser, Args &args, const First &fi template class HttpAllArgs { + mediakit::Parser* _parser = nullptr; + Args* _args = nullptr; public: - HttpAllArgs(const mediakit::Parser &parser, Args &args) { - _get_args = [&args]() { - return (void *) &args; - }; - _get_parser = [&parser]() -> const mediakit::Parser & { - return parser; - }; - _get_value = [](HttpAllArgs &that, const std::string &key) { - return getValue(that.getParser(), that.getArgs(), key); - }; - _clone = [&](HttpAllArgs &that) { - that._get_args = [args]() { - return (void *) &args; - }; - that._get_parser = [parser]() -> const mediakit::Parser & { - return parser; - }; - that._get_value = [](HttpAllArgs &that, const std::string &key) { - return getValue(that.getParser(), that.getArgs(), key); - }; - that._cache_able = true; - }; - } + const mediakit::Parser& parser; + Args& args; - HttpAllArgs(const HttpAllArgs &that) { - if (that._cache_able) { - _get_args = that._get_args; - _get_parser = that._get_parser; - _get_value = that._get_value; - _cache_able = true; - } else { - that._clone(*this); + HttpAllArgs(const mediakit::Parser &p, Args &a): parser(p), args(a) {} + + HttpAllArgs(const HttpAllArgs &that): _parser(new mediakit::Parser(that.parser)), + _args(new Args(that.args)), + parser(*_parser), args(*_args) {} + ~HttpAllArgs() { + if (_parser) { + delete _parser; + } + if (_args) { + delete _args; } } template toolkit::variant operator[](const Key &key) const { - return (toolkit::variant)_get_value(*(HttpAllArgs*)this, key); + return (toolkit::variant)getValue(parser, args, key); } - - const mediakit::Parser &getParser() const { - return _get_parser(); - } - - Args &getArgs() { - return *((Args *) _get_args()); - } - - const Args &getArgs() const { - return *((Args *) _get_args()); - } - -private: - bool _cache_able = false; - std::function _get_args; - std::function _get_parser; - std::function _get_value; - std::function _clone; }; -#define API_ARGS_MAP toolkit::SockInfo &sender, mediakit::HttpSession::KeyValue &headerOut, const HttpAllArgs &allArgs, Json::Value &val +using ArgsMap = HttpAllArgs; +using ArgsJson = HttpAllArgs; +using ArgsString = HttpAllArgs; + +#define API_ARGS_MAP toolkit::SockInfo &sender, mediakit::HttpSession::KeyValue &headerOut, const ArgsMap &allArgs, Json::Value &val #define API_ARGS_MAP_ASYNC API_ARGS_MAP, const mediakit::HttpSession::HttpResponseInvoker &invoker -#define API_ARGS_JSON toolkit::SockInfo &sender, mediakit::HttpSession::KeyValue &headerOut, const HttpAllArgs &allArgs, Json::Value &val +#define API_ARGS_JSON toolkit::SockInfo &sender, mediakit::HttpSession::KeyValue &headerOut, const ArgsJson &allArgs, Json::Value &val #define API_ARGS_JSON_ASYNC API_ARGS_JSON, const mediakit::HttpSession::HttpResponseInvoker &invoker -#define API_ARGS_STRING toolkit::SockInfo &sender, mediakit::HttpSession::KeyValue &headerOut, const HttpAllArgs &allArgs, Json::Value &val +#define API_ARGS_STRING toolkit::SockInfo &sender, mediakit::HttpSession::KeyValue &headerOut, const ArgsString &allArgs, Json::Value &val #define API_ARGS_STRING_ASYNC API_ARGS_STRING, const mediakit::HttpSession::HttpResponseInvoker &invoker #define API_ARGS_VALUE sender, headerOut, allArgs, val diff --git a/src/Common/MediaSource.h b/src/Common/MediaSource.h index 1ce0d8c5..b156954c 100644 --- a/src/Common/MediaSource.h +++ b/src/Common/MediaSource.h @@ -136,6 +136,15 @@ private: toolkit::Timer::Ptr _async_close_timer; }; + +template +static void getArgsValue(const MAP &allArgs, const KEY &key, TYPE &value) { + auto val = ((MAP &)allArgs)[key]; + if (!val.empty()) { + value = (TYPE)val; + } +} + class ProtocolOption { public: ProtocolOption(); @@ -243,15 +252,6 @@ public: GET_OPT_VALUE(stream_replace); GET_OPT_VALUE(max_track); } - -private: - template - static void getArgsValue(const MAP &allArgs, const KEY &key, TYPE &value) { - auto val = ((MAP &)allArgs)[key]; - if (!val.empty()) { - value = (TYPE)val; - } - } }; //该对象用于拦截感兴趣的MediaSourceEvent事件 diff --git a/src/Common/Parser.h b/src/Common/Parser.h index 624e5f27..324658ba 100644 --- a/src/Common/Parser.h +++ b/src/Common/Parser.h @@ -30,7 +30,7 @@ struct StrCaseCompare { class StrCaseMap : public std::multimap { public: - using Super = multimap; + using Super = std::multimap; std::string &operator[](const std::string &k) { auto it = find(k); diff --git a/webrtc/WebRtcEchoTest.h b/webrtc/WebRtcEchoTest.h index 397406c1..e6249ff2 100644 --- a/webrtc/WebRtcEchoTest.h +++ b/webrtc/WebRtcEchoTest.h @@ -27,7 +27,6 @@ protected: void onRtp(const char *buf, size_t len, uint64_t stamp_ms) override; void onRtcp(const char *buf, size_t len) override; - void onRecvRtp(MediaTrack &track, const std::string &rid, RtpPacket::Ptr rtp) override {}; void onBeforeEncryptRtp(const char *buf, int &len, void *ctx) override {}; void onBeforeEncryptRtcp(const char *buf, int &len, void *ctx) override {}; diff --git a/webrtc/WebRtcPlayer.cpp b/webrtc/WebRtcPlayer.cpp index bdc82697..cfafae2b 100644 --- a/webrtc/WebRtcPlayer.cpp +++ b/webrtc/WebRtcPlayer.cpp @@ -17,9 +17,8 @@ namespace mediakit { WebRtcPlayer::Ptr WebRtcPlayer::create(const EventPoller::Ptr &poller, const RtspMediaSource::Ptr &src, - const MediaInfo &info, - bool preferred_tcp) { - WebRtcPlayer::Ptr ret(new WebRtcPlayer(poller, src, info, preferred_tcp), [](WebRtcPlayer *ptr) { + const MediaInfo &info) { + WebRtcPlayer::Ptr ret(new WebRtcPlayer(poller, src, info), [](WebRtcPlayer *ptr) { ptr->onDestory(); delete ptr; }); @@ -29,8 +28,7 @@ WebRtcPlayer::Ptr WebRtcPlayer::create(const EventPoller::Ptr &poller, WebRtcPlayer::WebRtcPlayer(const EventPoller::Ptr &poller, const RtspMediaSource::Ptr &src, - const MediaInfo &info, - bool preferred_tcp) : WebRtcTransportImp(poller,preferred_tcp) { + const MediaInfo &info) : WebRtcTransportImp(poller) { _media_info = info; _play_src = src; CHECK(src); diff --git a/webrtc/WebRtcPlayer.h b/webrtc/WebRtcPlayer.h index 8daa7f8b..ccacd410 100644 --- a/webrtc/WebRtcPlayer.h +++ b/webrtc/WebRtcPlayer.h @@ -19,7 +19,7 @@ namespace mediakit { class WebRtcPlayer : public WebRtcTransportImp { public: using Ptr = std::shared_ptr; - static Ptr create(const EventPoller::Ptr &poller, const RtspMediaSource::Ptr &src, const MediaInfo &info, bool preferred_tcp = false); + static Ptr create(const EventPoller::Ptr &poller, const RtspMediaSource::Ptr &src, const MediaInfo &info); MediaInfo getMediaInfo() { return _media_info; } protected: @@ -27,10 +27,9 @@ protected: void onStartWebRTC() override; void onDestory() override; void onRtcConfigure(RtcConfigure &configure) const override; - void onRecvRtp(MediaTrack &track, const std::string &rid, RtpPacket::Ptr rtp) override {}; private: - WebRtcPlayer(const EventPoller::Ptr &poller, const RtspMediaSource::Ptr &src, const MediaInfo &info, bool preferred_tcp); + WebRtcPlayer(const EventPoller::Ptr &poller, const RtspMediaSource::Ptr &src, const MediaInfo &info); private: //媒体相关元数据 diff --git a/webrtc/WebRtcPusher.cpp b/webrtc/WebRtcPusher.cpp index 47df47c9..cde07992 100644 --- a/webrtc/WebRtcPusher.cpp +++ b/webrtc/WebRtcPusher.cpp @@ -20,9 +20,8 @@ WebRtcPusher::Ptr WebRtcPusher::create(const EventPoller::Ptr &poller, const RtspMediaSource::Ptr &src, const std::shared_ptr &ownership, const MediaInfo &info, - const ProtocolOption &option, - bool preferred_tcp) { - WebRtcPusher::Ptr ret(new WebRtcPusher(poller, src, ownership, info, option,preferred_tcp), [](WebRtcPusher *ptr) { + const ProtocolOption &option) { + WebRtcPusher::Ptr ret(new WebRtcPusher(poller, src, ownership, info, option), [](WebRtcPusher *ptr) { ptr->onDestory(); delete ptr; }); @@ -34,8 +33,7 @@ WebRtcPusher::WebRtcPusher(const EventPoller::Ptr &poller, const RtspMediaSource::Ptr &src, const std::shared_ptr &ownership, const MediaInfo &info, - const ProtocolOption &option, - bool preferred_tcp) : WebRtcTransportImp(poller,preferred_tcp) { + const ProtocolOption &option) : WebRtcTransportImp(poller) { _media_info = info; _push_src = src; _push_src_ownership = ownership; diff --git a/webrtc/WebRtcPusher.h b/webrtc/WebRtcPusher.h index bd4775e2..19b04608 100644 --- a/webrtc/WebRtcPusher.h +++ b/webrtc/WebRtcPusher.h @@ -20,8 +20,7 @@ class WebRtcPusher : public WebRtcTransportImp, public MediaSourceEvent { public: using Ptr = std::shared_ptr; static Ptr create(const EventPoller::Ptr &poller, const RtspMediaSource::Ptr &src, - const std::shared_ptr &ownership, const MediaInfo &info, const ProtocolOption &option, bool preferred_tcp = false); - + const std::shared_ptr &ownership, const MediaInfo &info, const ProtocolOption &option); protected: ///////WebRtcTransportImp override/////// @@ -53,7 +52,7 @@ protected: private: WebRtcPusher(const EventPoller::Ptr &poller, const RtspMediaSource::Ptr &src, - const std::shared_ptr &ownership, const MediaInfo &info, const ProtocolOption &option, bool preferred_tcp); + const std::shared_ptr &ownership, const MediaInfo &info, const ProtocolOption &option); private: bool _simulcast = false; diff --git a/webrtc/WebRtcTransport.cpp b/webrtc/WebRtcTransport.cpp index 786e26c3..768c543e 100644 --- a/webrtc/WebRtcTransport.cpp +++ b/webrtc/WebRtcTransport.cpp @@ -378,6 +378,12 @@ void WebRtcTransport::setRemoteDtlsFingerprint(const RtcSession &remote) { } void WebRtcTransport::onRtcConfigure(RtcConfigure &configure) const { + SdpAttrFingerprint fingerprint; + fingerprint.algorithm = _offer_sdp->media[0].fingerprint.algorithm; + fingerprint.hash = getFingerprint(fingerprint.algorithm, _dtls_transport); + configure.setDefaultSetting( + _ice_server->GetUsernameFragment(), _ice_server->GetPassword(), RtpDirection::sendrecv, fingerprint); + // 开启remb后关闭twcc,因为开启twcc后remb无效 GET_CONFIG(size_t, remb_bit_rate, Rtc::kRembBitRate); configure.enableTWCC(!remb_bit_rate); @@ -407,12 +413,7 @@ std::string WebRtcTransport::getAnswerSdp(const string &offer) { setRemoteDtlsFingerprint(*_offer_sdp); //// sdp 配置 //// - SdpAttrFingerprint fingerprint; - fingerprint.algorithm = _offer_sdp->media[0].fingerprint.algorithm; - fingerprint.hash = getFingerprint(fingerprint.algorithm, _dtls_transport); RtcConfigure configure; - configure.setDefaultSetting( - _ice_server->GetUsernameFragment(), _ice_server->GetPassword(), RtpDirection::sendrecv, fingerprint); onRtcConfigure(configure); //// 生成answer sdp //// @@ -431,10 +432,6 @@ static bool isDtls(char *buf) { return ((*buf > 19) && (*buf < 64)); } -static string getPeerAddress(RTC::TransportTuple *tuple) { - return tuple->get_peer_ip(); -} - void WebRtcTransport::inputSockData(char *buf, int len, RTC::TransportTuple *tuple) { if (RTC::StunPacket::IsStun((const uint8_t *)buf, len)) { std::unique_ptr packet(RTC::StunPacket::Parse((const uint8_t *)buf, len)); @@ -451,7 +448,7 @@ void WebRtcTransport::inputSockData(char *buf, int len, RTC::TransportTuple *tup } if (isRtp(buf, len)) { if (!_srtp_session_recv) { - WarnL << "received rtp packet when dtls not completed from:" << getPeerAddress(tuple); + WarnL << "received rtp packet when dtls not completed from:" << tuple->get_peer_ip(); return; } if (_srtp_session_recv->DecryptSrtp((uint8_t *)buf, &len)) { @@ -461,7 +458,7 @@ void WebRtcTransport::inputSockData(char *buf, int len, RTC::TransportTuple *tup } if (isRtcp(buf, len)) { if (!_srtp_session_recv) { - WarnL << "received rtcp packet when dtls not completed from:" << getPeerAddress(tuple); + WarnL << "received rtcp packet when dtls not completed from:" << tuple->get_peer_ip(); return; } if (_srtp_session_recv->DecryptSrtcp((uint8_t *)buf, &len)) { @@ -533,8 +530,7 @@ void WebRtcTransportImp::OnDtlsTransportApplicationDataReceived(const RTC::DtlsT #endif } -WebRtcTransportImp::WebRtcTransportImp(const EventPoller::Ptr &poller,bool preferred_tcp) - : WebRtcTransport(poller), _preferred_tcp(preferred_tcp) { +WebRtcTransportImp::WebRtcTransportImp(const EventPoller::Ptr &poller) : WebRtcTransport(poller) { InfoL << getIdentifier(); } @@ -674,7 +670,7 @@ void WebRtcTransportImp::onCheckAnswer(RtcSession &sdp) { }); for (auto &m : sdp.media) { m.addr.reset(); - m.addr.address = extern_ips.empty() ? _localIp.empty() ? SockUtil::get_local_ip() : _localIp : extern_ips[0]; + m.addr.address = extern_ips.empty() ? _local_ip.empty() ? SockUtil::get_local_ip() : _local_ip : extern_ips[0]; m.rtcp_addr.reset(); m.rtcp_addr.address = m.addr.address; @@ -769,7 +765,7 @@ void WebRtcTransportImp::onRtcConfigure(RtcConfigure &configure) const { return ret; }); if (extern_ips.empty()) { - std::string local_ip = _localIp.empty() ? SockUtil::get_local_ip() : _localIp; + std::string local_ip = _local_ip.empty() ? SockUtil::get_local_ip() : _local_ip; if (local_udp_port) { configure.addCandidate(*makeIceCandidate(local_ip, local_udp_port, 120, "udp")); } if (local_tcp_port) { configure.addCandidate(*makeIceCandidate(local_ip, local_tcp_port, _preferred_tcp ? 125 : 115, "tcp")); } } else { @@ -783,12 +779,16 @@ void WebRtcTransportImp::onRtcConfigure(RtcConfigure &configure) const { } } -void WebRtcTransportImp::setIceCandidate(vector cands) { - _cands = std::move(cands); +void WebRtcTransportImp::setPreferredTcp(bool flag) { + _preferred_tcp = flag; } -void WebRtcTransportImp::setLocalIp(const std::string &localIp) { - _localIp = localIp; +void WebRtcTransportImp::setLocalIp(std::string local_ip) { + _local_ip = std::move(local_ip); +} + +void WebRtcTransportImp::setIceCandidate(vector cands) { + _cands = std::move(cands); } /////////////////////////////////////////////////////////////////// @@ -1278,21 +1278,14 @@ void WebRtcPluginManager::registerPlugin(const string &type, Plugin cb) { _map_creator[type] = std::move(cb); } -std::string exchangeSdp(const WebRtcInterface &exchanger, const std::string& offer) { - return const_cast(exchanger).getAnswerSdp(offer); -} - -void setLocalIp(const WebRtcInterface& exchanger, const std::string& localIp) { - return const_cast(exchanger).setLocalIp(localIp); -} void WebRtcPluginManager::setListener(Listener cb) { lock_guard lck(_mtx_creator); _listener = std::move(cb); } -void WebRtcPluginManager::getAnswerSdp(Session &sender, const string &type, const WebRtcArgs &args, const onCreateRtc &cb_in) { - onCreateRtc cb; +void WebRtcPluginManager::negotiateSdp(Session &sender, const string &type, const WebRtcArgs &args, const onCreateWebRtc &cb_in) { + onCreateWebRtc cb; lock_guard lck(_mtx_creator); if (_listener) { auto listener = _listener; @@ -1308,21 +1301,19 @@ void WebRtcPluginManager::getAnswerSdp(Session &sender, const string &type, cons auto it = _map_creator.find(type); if (it == _map_creator.end()) { - cb(WebRtcException(SockException(Err_other, "the type can not supported"))); + cb_in(WebRtcException(SockException(Err_other, "the type can not supported"))); return; } it->second(sender, args, cb); } -void echo_plugin(Session &sender, const WebRtcArgs &args, const WebRtcPluginManager::onCreateRtc &cb) { +void echo_plugin(Session &sender, const WebRtcArgs &args, const onCreateWebRtc &cb) { cb(*WebRtcEchoTest::create(EventPollerPool::Instance().getPoller())); } -void push_plugin(Session &sender, const WebRtcArgs &args, const WebRtcPluginManager::onCreateRtc &cb) { +void push_plugin(Session &sender, const WebRtcArgs &args, const onCreateWebRtc &cb) { MediaInfo info(args["url"]); - bool preferred_tcp = args["preferred_tcp"]; - - Broadcast::PublishAuthInvoker invoker = [cb, info, preferred_tcp](const string &err, const ProtocolOption &option) mutable { + Broadcast::PublishAuthInvoker invoker = [cb, info](const string &err, const ProtocolOption &option) mutable { if (!err.empty()) { cb(WebRtcException(SockException(Err_other, err))); return; @@ -1361,7 +1352,7 @@ void push_plugin(Session &sender, const WebRtcArgs &args, const WebRtcPluginMana push_src_ownership = push_src->getOwnership(); push_src->setProtocolOption(option); } - auto rtc = WebRtcPusher::create(EventPollerPool::Instance().getPoller(), push_src, push_src_ownership, info, option, preferred_tcp); + auto rtc = WebRtcPusher::create(EventPollerPool::Instance().getPoller(), push_src, push_src_ownership, info, option); push_src->setListener(rtc); cb(*rtc); }; @@ -1374,12 +1365,10 @@ void push_plugin(Session &sender, const WebRtcArgs &args, const WebRtcPluginMana } } -void play_plugin(Session &sender, const WebRtcArgs &args, const WebRtcPluginManager::onCreateRtc &cb) { +void play_plugin(Session &sender, const WebRtcArgs &args, const onCreateWebRtc &cb) { MediaInfo info(args["url"]); - bool preferred_tcp = args["preferred_tcp"]; - auto session_ptr = static_pointer_cast(sender.shared_from_this()); - Broadcast::AuthInvoker invoker = [cb, info, session_ptr, preferred_tcp](const string &err) mutable { + Broadcast::AuthInvoker invoker = [cb, info, session_ptr](const string &err) mutable { if (!err.empty()) { cb(WebRtcException(SockException(Err_other, err))); return; @@ -1395,7 +1384,7 @@ void play_plugin(Session &sender, const WebRtcArgs &args, const WebRtcPluginMana } // 还原成rtc,目的是为了hook时识别哪种播放协议 info.schema = "rtc"; - auto rtc = WebRtcPlayer::create(EventPollerPool::Instance().getPoller(), src, info, preferred_tcp); + auto rtc = WebRtcPlayer::create(EventPollerPool::Instance().getPoller(), src, info); cb(*rtc); }); }; @@ -1408,39 +1397,63 @@ void play_plugin(Session &sender, const WebRtcArgs &args, const WebRtcPluginMana } } -static void set_webrtc_cands(const WebRtcArgs &args, const WebRtcInterface &rtc) { - vector cands; +static void setWebRtcArgs(const WebRtcArgs &args, WebRtcInterface &rtc) { { - auto cand_str = trim(args["cand_udp"]); - auto ip_port = toolkit::split(cand_str, ":"); - if (ip_port.size() == 2) { + static auto is_vaild_ip = [](const std::string &ip) -> bool { + int a, b, c, d; + return sscanf(ip.c_str(), "%d.%d.%d.%d", &a, &b, &c, &d) == 4; + }; + std::string host = args["Host"]; + if (!host.empty()) { + auto local_ip = host.substr(0, host.find(':')); + if (!is_vaild_ip(local_ip) || local_ip == "127.0.0.1") { + local_ip = ""; + } + rtc.setLocalIp(std::move(local_ip)); + } + } + + bool preferred_tcp = args["preferred_tcp"]; + { + rtc.setPreferredTcp(preferred_tcp); + } + + { + vector cands; + { + auto cand_str = trim(args["cand_udp"]); + auto ip_port = toolkit::split(cand_str, ":"); + if (ip_port.size() == 2) { + // udp优先 + auto ice_cand = makeIceCandidate(ip_port[0], atoi(ip_port[1].data()), preferred_tcp ? 100 : 120, "udp"); + cands.emplace_back(std::move(*ice_cand)); + } + } + { + auto cand_str = trim(args["cand_tcp"]); + auto ip_port = toolkit::split(cand_str, ":"); + if (ip_port.size() == 2) { + // tcp模式 + auto ice_cand = makeIceCandidate(ip_port[0], atoi(ip_port[1].data()), preferred_tcp ? 120 : 100, "tcp"); + cands.emplace_back(std::move(*ice_cand)); + } + } + if (!cands.empty()) { // udp优先 - auto ice_cand = makeIceCandidate(ip_port[0], atoi(ip_port[1].data()), 120, "udp"); - cands.emplace_back(std::move(*ice_cand)); + rtc.setIceCandidate(std::move(cands)); } } - { - auto cand_str = trim(args["cand_tcp"]); - auto ip_port = toolkit::split(cand_str, ":"); - if (ip_port.size() == 2) { - // tcp模式 - auto ice_cand = makeIceCandidate(ip_port[0], atoi(ip_port[1].data()), 100, "tcp"); - cands.emplace_back(std::move(*ice_cand)); - } - } - if (!cands.empty()) { - // udp优先 - const_cast(rtc).setIceCandidate(std::move(cands)); - } } static onceToken s_rtc_auto_register([]() { +#if !defined (NDEBUG) + // debug模式才开启echo插件 WebRtcPluginManager::Instance().registerPlugin("echo", echo_plugin); +#endif WebRtcPluginManager::Instance().registerPlugin("push", push_plugin); WebRtcPluginManager::Instance().registerPlugin("play", play_plugin); - WebRtcPluginManager::Instance().setListener([](Session &sender, const std::string &type, const WebRtcArgs &args, const WebRtcInterface &rtc) { - set_webrtc_cands(args, rtc); + setWebRtcArgs(args, const_cast(rtc)); }); }); diff --git a/webrtc/WebRtcTransport.h b/webrtc/WebRtcTransport.h index 8e8d97fc..1a410dc9 100644 --- a/webrtc/WebRtcTransport.h +++ b/webrtc/WebRtcTransport.h @@ -35,6 +35,8 @@ extern const std::string kTcpPort; extern const std::string kTimeOutSec; }//namespace RTC +class WebRtcArgs; + class WebRtcInterface { public: virtual ~WebRtcInterface() = default; @@ -42,13 +44,10 @@ public: virtual const std::string& getIdentifier() const = 0; virtual const std::string& deleteRandStr() const { static std::string s_null; return s_null; } virtual void setIceCandidate(std::vector cands) {} - virtual void setLocalIp(const std::string &localIp) {} + virtual void setLocalIp(std::string localIp) {} + virtual void setPreferredTcp(bool flag) {} }; -std::string exchangeSdp(const WebRtcInterface &exchanger, const std::string& offer); - -void setLocalIp(const WebRtcInterface &exchanger, const std::string &localIp); - class WebRtcException : public WebRtcInterface { public: WebRtcException(const SockException &ex) : _ex(ex) {}; @@ -88,7 +87,7 @@ public: * @param offer offer sdp * @return answer sdp */ - std::string getAnswerSdp(const std::string &offer) override; + std::string getAnswerSdp(const std::string &offer) override final; /** * 获取对象唯一id @@ -252,14 +251,16 @@ public: void onSendRtp(const RtpPacket::Ptr &rtp, bool flush, bool rtx = false); void createRtpChannel(const std::string &rid, uint32_t ssrc, MediaTrack &track); - void setIceCandidate(std::vector cands) override; void removeTuple(RTC::TransportTuple* tuple); void safeShutdown(const SockException &ex); - void setLocalIp(const std::string &localIp) override; + void setPreferredTcp(bool flag) override; + void setLocalIp(std::string local_ip) override; + void setIceCandidate(std::vector cands) override; + protected: void OnIceServerSelectedTuple(const RTC::IceServer *iceServer, RTC::TransportTuple *tuple) override; - WebRtcTransportImp(const EventPoller::Ptr &poller,bool preferred_tcp = false); + WebRtcTransportImp(const EventPoller::Ptr &poller); void OnDtlsTransportApplicationDataReceived(const RTC::DtlsTransport *dtlsTransport, const uint8_t *data, size_t len) override; void onStartWebRTC() override; void onSendSockData(Buffer::Ptr buf, bool flush = true, RTC::TransportTuple *tuple = nullptr) override; @@ -273,7 +274,7 @@ protected: void onCreate() override; void onDestory() override; void onShutdown(const SockException &ex) override; - virtual void onRecvRtp(MediaTrack &track, const std::string &rid, RtpPacket::Ptr rtp) = 0; + virtual void onRecvRtp(MediaTrack &track, const std::string &rid, RtpPacket::Ptr rtp) {} void updateTicker(); float getLossRate(TrackType type); void onRtcpBye() override; @@ -289,7 +290,7 @@ private: void onCheckAnswer(RtcSession &sdp); private: - bool _preferred_tcp; + bool _preferred_tcp = false; uint16_t _rtx_seq[2] = {0, 0}; //用掉的总流量 uint64_t _bytes_usage = 0; @@ -310,8 +311,8 @@ private: //根据接收rtp的pt获取相关信息 std::unordered_map> _pt_to_track; std::vector _cands; - //源访问的hostip - std::string _localIp; + //http访问时的host ip + std::string _local_ip; }; class WebRtcTransportManager { @@ -333,21 +334,20 @@ private: class WebRtcArgs : public std::enable_shared_from_this { public: virtual ~WebRtcArgs() = default; - virtual variant operator[](const std::string &key) const = 0; }; +using onCreateWebRtc = std::function; class WebRtcPluginManager { public: - using onCreateRtc = std::function; - using Plugin = std::function; + using Plugin = std::function; using Listener = std::function; static WebRtcPluginManager &Instance(); void registerPlugin(const std::string &type, Plugin cb); - void getAnswerSdp(Session &sender, const std::string &type, const WebRtcArgs &args, const onCreateRtc &cb); void setListener(Listener cb); + void negotiateSdp(Session &sender, const std::string &type, const WebRtcArgs &args, const onCreateWebRtc &cb); private: WebRtcPluginManager() = default; From d8893877b25fc65f05478ce7517ae467bb12a447 Mon Sep 17 00:00:00 2001 From: xiongziliang <771730766@qq.com> Date: Sat, 23 Mar 2024 22:52:40 +0800 Subject: [PATCH 24/60] Delete invalid code --- api/source/mk_common.cpp | 2 +- server/WebApi.cpp | 4 ++-- webrtc/WebRtcTransport.h | 2 -- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/api/source/mk_common.cpp b/api/source/mk_common.cpp index f41d2976..48efeb1d 100644 --- a/api/source/mk_common.cpp +++ b/api/source/mk_common.cpp @@ -304,7 +304,7 @@ API_EXPORT void API_CALL mk_webrtc_get_answer_sdp2(void *user_data, on_user_data std::string offer_str = offer; std::shared_ptr ptr(user_data, user_data_free ? user_data_free : [](void *) {}); auto args = std::make_shared(url); - WebRtcPluginManager::Instance().negotiateSdp(*session, type, *args, [offer_str, session, ptr, cb, args](const WebRtcInterface &exchanger) mutable { + WebRtcPluginManager::Instance().negotiateSdp(*session, type, *args, [offer_str, session, ptr, cb](const WebRtcInterface &exchanger) mutable { auto &handler = const_cast(exchanger); try { auto sdp_answer = handler.getAnswerSdp(offer_str); diff --git a/server/WebApi.cpp b/server/WebApi.cpp index 4174a6ad..5f64e073 100755 --- a/server/WebApi.cpp +++ b/server/WebApi.cpp @@ -1770,7 +1770,7 @@ void installWebApi() { auto &session = static_cast(sender); auto args = std::make_shared(allArgs, sender.getIdentifier()); - WebRtcPluginManager::Instance().negotiateSdp(session, type, *args, [invoker, val, offer, headerOut, args](const WebRtcInterface &exchanger) mutable { + WebRtcPluginManager::Instance().negotiateSdp(session, type, *args, [invoker, val, offer, headerOut](const WebRtcInterface &exchanger) mutable { auto &handler = const_cast(exchanger); try { val["sdp"] = handler.getAnswerSdp(offer); @@ -1793,7 +1793,7 @@ void installWebApi() { auto &session = static_cast(sender); auto location = std::string(session.overSsl() ? "https://" : "http://") + allArgs["host"] + delete_webrtc_url; auto args = std::make_shared(allArgs, sender.getIdentifier()); - WebRtcPluginManager::Instance().negotiateSdp(session, type, *args, [invoker, offer, headerOut, location, args](const WebRtcInterface &exchanger) mutable { + WebRtcPluginManager::Instance().negotiateSdp(session, type, *args, [invoker, offer, headerOut, location](const WebRtcInterface &exchanger) mutable { auto &handler = const_cast(exchanger); try { // 设置返回类型 diff --git a/webrtc/WebRtcTransport.h b/webrtc/WebRtcTransport.h index 1a410dc9..3f920c68 100644 --- a/webrtc/WebRtcTransport.h +++ b/webrtc/WebRtcTransport.h @@ -35,8 +35,6 @@ extern const std::string kTcpPort; extern const std::string kTimeOutSec; }//namespace RTC -class WebRtcArgs; - class WebRtcInterface { public: virtual ~WebRtcInterface() = default; From 7aaafa18e782e4dc69a5091d22db78066ef97aca Mon Sep 17 00:00:00 2001 From: xiongziliang <771730766@qq.com> Date: Sat, 23 Mar 2024 23:08:10 +0800 Subject: [PATCH 25/60] Format code --- webrtc/Sdp.cpp | 435 +++++++++++++++++++++++----------------------- webrtc/Sdp.h | 461 ++++++++++++++++++++++++------------------------- 2 files changed, 436 insertions(+), 460 deletions(-) diff --git a/webrtc/Sdp.cpp b/webrtc/Sdp.cpp index 55f94e38..e1f1d045 100644 --- a/webrtc/Sdp.cpp +++ b/webrtc/Sdp.cpp @@ -20,19 +20,19 @@ namespace mediakit { namespace Rtc { #define RTC_FIELD "rtc." -const string kPreferredCodecA = RTC_FIELD"preferredCodecA"; -const string kPreferredCodecV = RTC_FIELD"preferredCodecV"; +const string kPreferredCodecA = RTC_FIELD "preferredCodecA"; +const string kPreferredCodecV = RTC_FIELD "preferredCodecV"; static onceToken token([]() { mINI::Instance()[kPreferredCodecA] = "PCMA,PCMU,opus,mpeg4-generic"; mINI::Instance()[kPreferredCodecV] = "H264,H265,AV1,VP9,VP8"; }); -} +} // namespace Rtc using onCreateSdpItem = function; static map sdpItemCreator; template -void registerSdpItem(){ +void registerSdpItem() { onCreateSdpItem func = [](const string &key, const string &value) { auto ret = std::make_shared(); ret->parse(value); @@ -47,52 +47,50 @@ public: virtual RtpDirection getDirection() const = 0; }; -class SdpDirectionSendonly : public SdpItem, public DirectionInterface{ +class SdpDirectionSendonly : public SdpItem, public DirectionInterface { public: - const char* getKey() const override { return getRtpDirectionString(getDirection());} - RtpDirection getDirection() const override {return RtpDirection::sendonly;} + const char *getKey() const override { return getRtpDirectionString(getDirection()); } + RtpDirection getDirection() const override { return RtpDirection::sendonly; } }; -class SdpDirectionRecvonly : public SdpItem, public DirectionInterface{ +class SdpDirectionRecvonly : public SdpItem, public DirectionInterface { public: - const char* getKey() const override { return getRtpDirectionString(getDirection());} - RtpDirection getDirection() const override {return RtpDirection::recvonly;} + const char *getKey() const override { return getRtpDirectionString(getDirection()); } + RtpDirection getDirection() const override { return RtpDirection::recvonly; } }; -class SdpDirectionSendrecv : public SdpItem, public DirectionInterface{ +class SdpDirectionSendrecv : public SdpItem, public DirectionInterface { public: - const char* getKey() const override { return getRtpDirectionString(getDirection());} - RtpDirection getDirection() const override {return RtpDirection::sendrecv;} + const char *getKey() const override { return getRtpDirectionString(getDirection()); } + RtpDirection getDirection() const override { return RtpDirection::sendrecv; } }; -class SdpDirectionInactive : public SdpItem, public DirectionInterface{ +class SdpDirectionInactive : public SdpItem, public DirectionInterface { public: - const char* getKey() const override { return getRtpDirectionString(getDirection());} - RtpDirection getDirection() const override {return RtpDirection::inactive;} + const char *getKey() const override { return getRtpDirectionString(getDirection()); } + RtpDirection getDirection() const override { return RtpDirection::inactive; } }; -class DirectionInterfaceImp : public SdpItem, public DirectionInterface{ +class DirectionInterfaceImp : public SdpItem, public DirectionInterface { public: - DirectionInterfaceImp(RtpDirection direct){ - direction = direct; - } - const char* getKey() const override { return getRtpDirectionString(getDirection());} - RtpDirection getDirection() const override {return direction;} + DirectionInterfaceImp(RtpDirection direct) { direction = direct; } + const char *getKey() const override { return getRtpDirectionString(getDirection()); } + RtpDirection getDirection() const override { return direction; } private: RtpDirection direction; }; -static bool registerAllItem(){ - registerSdpItem >(); - registerSdpItem >(); - registerSdpItem >(); - registerSdpItem >(); - registerSdpItem >(); - registerSdpItem >(); - registerSdpItem >(); - registerSdpItem >(); - registerSdpItem >(); +static bool registerAllItem() { + registerSdpItem>(); + registerSdpItem>(); + registerSdpItem>(); + registerSdpItem>(); + registerSdpItem>(); + registerSdpItem>(); + registerSdpItem>(); + registerSdpItem>(); + registerSdpItem>(); registerSdpItem(); registerSdpItem(); registerSdpItem(); @@ -138,11 +136,11 @@ DtlsRole getDtlsRole(const string &str) { return it == dtls_role_map.end() ? DtlsRole::invalid : it->second; } -const char* getDtlsRoleString(DtlsRole role){ +const char *getDtlsRoleString(DtlsRole role) { switch (role) { - case DtlsRole::active : return "active"; - case DtlsRole::passive : return "passive"; - case DtlsRole::actpass : return "actpass"; + case DtlsRole::active: return "active"; + case DtlsRole::passive: return "passive"; + case DtlsRole::actpass: return "actpass"; default: return "invalid"; } } @@ -159,12 +157,12 @@ RtpDirection getRtpDirection(const string &str) { return it == direction_map.end() ? RtpDirection::invalid : it->second; } -const char* getRtpDirectionString(RtpDirection val){ +const char *getRtpDirectionString(RtpDirection val) { switch (val) { - case RtpDirection::sendonly : return "sendonly"; - case RtpDirection::recvonly : return "recvonly"; - case RtpDirection::sendrecv : return "sendrecv"; - case RtpDirection::inactive : return "inactive"; + case RtpDirection::sendonly: return "sendonly"; + case RtpDirection::recvonly: return "recvonly"; + case RtpDirection::sendrecv: return "sendrecv"; + case RtpDirection::inactive: return "inactive"; default: return "invalid"; } } @@ -179,7 +177,7 @@ string RtcSdpBase::toString() const { return std::move(printer); } -RtpDirection RtcSdpBase::getDirection() const{ +RtpDirection RtcSdpBase::getDirection() const { for (auto &item : items) { auto attr = dynamic_pointer_cast(item); if (attr) { @@ -200,7 +198,7 @@ SdpItem::Ptr RtcSdpBase::getItem(char key_c, const char *attr_key) const { return item; } auto attr = dynamic_pointer_cast(item); - if (attr && !strcasecmp(attr->detail->getKey() , attr_key)) { + if (attr && !strcasecmp(attr->detail->getKey(), attr_key)) { return attr->detail; } } @@ -225,7 +223,7 @@ string RtcSessionSdp::getSessionInfo() const { return getStringItem('i'); } -SdpTime RtcSessionSdp::getSessionTime() const{ +SdpTime RtcSessionSdp::getSessionTime() const { return getItemClass('t'); } @@ -422,7 +420,7 @@ string SdpAttr::toString() const { return SdpItem::toString(); } -void SdpAttrGroup::parse(const string &str) { +void SdpAttrGroup::parse(const string &str) { auto vec = split(str, " "); CHECK_SDP(vec.size() >= 2); type = vec[0]; @@ -430,7 +428,7 @@ void SdpAttrGroup::parse(const string &str) { mids = std::move(vec); } -string SdpAttrGroup::toString() const { +string SdpAttrGroup::toString() const { if (value.empty()) { value = type; for (auto mid : mids) { @@ -441,14 +439,14 @@ string SdpAttrGroup::toString() const { return SdpItem::toString(); } -void SdpAttrMsidSemantic::parse(const string &str) { +void SdpAttrMsidSemantic::parse(const string &str) { auto vec = split(str, " "); CHECK_SDP(vec.size() >= 1); msid = vec[0]; token = vec.size() > 1 ? vec[1] : ""; } -string SdpAttrMsidSemantic::toString() const { +string SdpAttrMsidSemantic::toString() const { if (value.empty()) { if (token.empty()) { value = string(" ") + msid; @@ -459,7 +457,7 @@ string SdpAttrMsidSemantic::toString() const { return SdpItem::toString(); } -void SdpAttrRtcp::parse(const string &str) { +void SdpAttrRtcp::parse(const string &str) { auto vec = split(str, " "); CHECK_SDP(vec.size() == 4); port = atoi(vec[0].data()); @@ -468,14 +466,14 @@ void SdpAttrRtcp::parse(const string &str) { address = vec[3]; } -string SdpAttrRtcp::toString() const { +string SdpAttrRtcp::toString() const { if (value.empty()) { value = to_string(port) + " " + nettype + " " + addrtype + " " + address; } return SdpItem::toString(); } -void SdpAttrIceOption::parse(const string &str){ +void SdpAttrIceOption::parse(const string &str) { auto vec = split(str, " "); for (auto &v : vec) { if (!strcasecmp(v.data(), "trickle")) { @@ -489,7 +487,7 @@ void SdpAttrIceOption::parse(const string &str){ } } -string SdpAttrIceOption::toString() const{ +string SdpAttrIceOption::toString() const { if (value.empty()) { if (trickle && renomination) { value = "trickle renomination"; @@ -502,35 +500,35 @@ string SdpAttrIceOption::toString() const{ return value; } -void SdpAttrFingerprint::parse(const string &str) { +void SdpAttrFingerprint::parse(const string &str) { auto vec = split(str, " "); CHECK_SDP(vec.size() == 2); algorithm = vec[0]; hash = vec[1]; } -string SdpAttrFingerprint::toString() const { +string SdpAttrFingerprint::toString() const { if (value.empty()) { value = algorithm + " " + hash; } return SdpItem::toString(); } -void SdpAttrSetup::parse(const string &str) { +void SdpAttrSetup::parse(const string &str) { role = getDtlsRole(str); CHECK_SDP(role != DtlsRole::invalid); } -string SdpAttrSetup::toString() const { +string SdpAttrSetup::toString() const { if (value.empty()) { value = getDtlsRoleString(role); } return SdpItem::toString(); } -void SdpAttrExtmap::parse(const string &str) { - char buf[128] = {0}; - char direction_buf[32] = {0}; +void SdpAttrExtmap::parse(const string &str) { + char buf[128] = { 0 }; + char direction_buf[32] = { 0 }; if (sscanf(str.data(), "%" SCNd8 "/%31[^ ] %127s", &id, direction_buf, buf) != 3) { CHECK_SDP(sscanf(str.data(), "%" SCNd8 " %127s", &id, buf) == 2); direction = RtpDirection::sendrecv; @@ -540,30 +538,30 @@ void SdpAttrExtmap::parse(const string &str) { ext = buf; } -string SdpAttrExtmap::toString() const { +string SdpAttrExtmap::toString() const { if (value.empty()) { - if(direction == RtpDirection::invalid || direction == RtpDirection::sendrecv){ + if (direction == RtpDirection::invalid || direction == RtpDirection::sendrecv) { value = to_string((int)id) + " " + ext; } else { - value = to_string((int)id) + "/" + getRtpDirectionString(direction) + " " + ext; + value = to_string((int)id) + "/" + getRtpDirectionString(direction) + " " + ext; } } return SdpItem::toString(); } -void SdpAttrRtpMap::parse(const string &str) { - char buf[32] = {0}; +void SdpAttrRtpMap::parse(const string &str) { + char buf[32] = { 0 }; if (sscanf(str.data(), "%" SCNu8 " %31[^/]/%" SCNd32 "/%" SCNd32, &pt, buf, &sample_rate, &channel) != 4) { CHECK_SDP(sscanf(str.data(), "%" SCNu8 " %31[^/]/%" SCNd32, &pt, buf, &sample_rate) == 3); if (getTrackType(getCodecId(buf)) == TrackAudio) { - //未指定通道数时,且为音频时,那么通道数默认为1 + // 未指定通道数时,且为音频时,那么通道数默认为1 channel = 1; } } codec = buf; } -string SdpAttrRtpMap::toString() const { +string SdpAttrRtpMap::toString() const { if (value.empty()) { value = to_string((int)pt) + " " + codec + "/" + to_string(sample_rate); if (channel) { @@ -574,21 +572,21 @@ string SdpAttrRtpMap::toString() const { return SdpItem::toString(); } -void SdpAttrRtcpFb::parse(const string &str_in) { +void SdpAttrRtcpFb::parse(const string &str_in) { auto str = str_in + "\n"; - char rtcp_type_buf[32] = {0}; + char rtcp_type_buf[32] = { 0 }; CHECK_SDP(sscanf(str.data(), "%" SCNu8 " %31[^\n]", &pt, rtcp_type_buf) == 2); rtcp_type = rtcp_type_buf; } -string SdpAttrRtcpFb::toString() const { +string SdpAttrRtcpFb::toString() const { if (value.empty()) { value = to_string((int)pt) + " " + rtcp_type; } return SdpItem::toString(); } -void SdpAttrFmtp::parse(const string &str) { +void SdpAttrFmtp::parse(const string &str) { auto pos = str.find(' '); CHECK_SDP(pos != string::npos); pt = atoi(str.substr(0, pos).data()); @@ -596,31 +594,31 @@ void SdpAttrFmtp::parse(const string &str) { for (auto &item : vec) { trim(item); auto pos = item.find('='); - if(pos == string::npos){ + if (pos == string::npos) { fmtp.emplace(std::make_pair(item, "")); - } else { + } else { fmtp.emplace(std::make_pair(item.substr(0, pos), item.substr(pos + 1))); } } CHECK_SDP(!fmtp.empty()); } -string SdpAttrFmtp::toString() const { +string SdpAttrFmtp::toString() const { if (value.empty()) { value = to_string((int)pt); int i = 0; for (auto &pr : fmtp) { - value += (i++ ? ';' : ' '); + value += (i++ ? ';' : ' '); value += pr.first + "=" + pr.second; } } return SdpItem::toString(); } -void SdpAttrSSRC::parse(const string &str_in) { +void SdpAttrSSRC::parse(const string &str_in) { auto str = str_in + '\n'; - char attr_buf[32] = {0}; - char attr_val_buf[128] = {0}; + char attr_buf[32] = { 0 }; + char attr_val_buf[128] = { 0 }; if (3 == sscanf(str.data(), "%" SCNu32 " %31[^:]:%127[^\n]", &ssrc, attr_buf, attr_val_buf)) { attribute = attr_buf; attribute_value = attr_val_buf; @@ -631,7 +629,7 @@ void SdpAttrSSRC::parse(const string &str_in) { } } -string SdpAttrSSRC::toString() const { +string SdpAttrSSRC::toString() const { if (value.empty()) { value = to_string(ssrc) + ' '; value += attribute; @@ -650,14 +648,14 @@ void SdpAttrSSRCGroup::parse(const string &str) { CHECK(isFID() || isSIM()); vec.erase(vec.begin()); for (auto ssrc : vec) { - ssrcs.emplace_back((uint32_t) atoll(ssrc.data())); + ssrcs.emplace_back((uint32_t)atoll(ssrc.data())); } } -string SdpAttrSSRCGroup::toString() const { +string SdpAttrSSRCGroup::toString() const { if (value.empty()) { value = type; - //最少要求2个ssrc + // 最少要求2个ssrc CHECK(ssrcs.size() >= 2); for (auto &ssrc : ssrcs) { value += ' '; @@ -667,13 +665,13 @@ string SdpAttrSSRCGroup::toString() const { return SdpItem::toString(); } -void SdpAttrSctpMap::parse(const string &str) { - char subtypes_buf[64] = {0}; +void SdpAttrSctpMap::parse(const string &str) { + char subtypes_buf[64] = { 0 }; CHECK_SDP(3 == sscanf(str.data(), "%" SCNu16 " %63[^ ] %" SCNd32, &port, subtypes_buf, &streams)); subtypes = subtypes_buf; } -string SdpAttrSctpMap::toString() const { +string SdpAttrSctpMap::toString() const { if (value.empty()) { value = to_string(port); value += ' '; @@ -684,11 +682,11 @@ string SdpAttrSctpMap::toString() const { return SdpItem::toString(); } -void SdpAttrCandidate::parse(const string &str) { - char foundation_buf[40] = {0}; - char transport_buf[16] = {0}; - char address_buf[64] = {0}; - char type_buf[16] = {0}; +void SdpAttrCandidate::parse(const string &str) { + char foundation_buf[40] = { 0 }; + char transport_buf[16] = { 0 }; + char address_buf[64] = { 0 }; + char type_buf[16] = { 0 }; // https://datatracker.ietf.org/doc/html/rfc5245#section-15.1 CHECK_SDP(sscanf(str.data(), "%32[^ ] %" SCNu32 " %15[^ ] %" SCNu32 " %63[^ ] %" SCNu16 " typ %15[^ ]", @@ -715,10 +713,9 @@ void SdpAttrCandidate::parse(const string &str) { } } -string SdpAttrCandidate::toString() const { +string SdpAttrCandidate::toString() const { if (value.empty()) { - value = foundation + " " + to_string(component) + " " + transport + " " + to_string(priority) + - " " + address + " " + to_string(port) + " typ " + type; + value = foundation + " " + to_string(component) + " " + transport + " " + to_string(priority) + " " + address + " " + to_string(port) + " typ " + type; for (auto &pr : arr) { value += ' '; value += pr.first; @@ -730,10 +727,10 @@ string SdpAttrCandidate::toString() const { } void SdpAttrSimulcast::parse(const string &str) { - //https://www.meetecho.com/blog/simulcast-janus-ssrc/ - //a=simulcast:send/recv q;h;f - //a=simulcast:send/recv [rid=]q;h;f - //a=simulcast: recv h;m;l + // https://www.meetecho.com/blog/simulcast-janus-ssrc/ + // a=simulcast:send/recv q;h;f + // a=simulcast:send/recv [rid=]q;h;f + // a=simulcast: recv h;m;l // auto vec = split(str, " "); CHECK_SDP(vec.size() == 2); @@ -845,12 +842,12 @@ void RtcSession::loadFrom(const string &str) { for (auto &group : ssrc_groups) { if (group.isFID()) { have_rtx_ssrc = true; - //ssrc-group:FID字段必须包含rtp/rtx的ssrc + // ssrc-group:FID字段必须包含rtp/rtx的ssrc CHECK(group.ssrcs.size() == 2); - //根据rtp ssrc找到对象 + // 根据rtp ssrc找到对象 auto it = rtc_ssrc_map.find(group.ssrcs[0]); CHECK(it != rtc_ssrc_map.end()); - //设置rtx ssrc + // 设置rtx ssrc it->second.rtx_ssrc = group.ssrcs[1]; rtc_media.rtp_rtx_ssrc.emplace_back(it->second); } else if (group.isSIM()) { @@ -860,7 +857,7 @@ void RtcSession::loadFrom(const string &str) { } if (!have_rtx_ssrc) { - //按照sdp顺序依次添加ssrc + // 按照sdp顺序依次添加ssrc for (auto &attr : ssrc_attr) { if (attr.attribute == "cname") { rtc_media.rtp_rtx_ssrc.emplace_back(rtc_ssrc_map[attr.ssrc]); @@ -883,56 +880,56 @@ void RtcSession::loadFrom(const string &str) { CHECK(rid.direction == simulcast.direction); CHECK(rid_map.find(rid.rid) != rid_map.end()); } - //simulcast最少要求2种方案 + // simulcast最少要求2种方案 CHECK(simulcast.rids.size() >= 2); rtc_media.rtp_rids = simulcast.rids; } if (ssrc_group_sim) { - //指定了a=ssrc-group:SIM + // 指定了a=ssrc-group:SIM for (auto ssrc : ssrc_group_sim->ssrcs) { auto it = rtc_ssrc_map.find(ssrc); CHECK(it != rtc_ssrc_map.end()); rtc_media.rtp_ssrc_sim.emplace_back(it->second); } } else if (!rtc_media.rtp_rids.empty()) { - //未指定a=ssrc-group:SIM, 但是指定了a=simulcast, 那么只能根据ssrc顺序来对应rid顺序 + // 未指定a=ssrc-group:SIM, 但是指定了a=simulcast, 那么只能根据ssrc顺序来对应rid顺序 rtc_media.rtp_ssrc_sim = rtc_media.rtp_rtx_ssrc; } if (!rtc_media.supportSimulcast()) { - //不支持simulcast的情况下,最多一组ssrc + // 不支持simulcast的情况下,最多一组ssrc CHECK(rtc_media.rtp_rtx_ssrc.size() <= 1); } else { - //simulcast的情况下,要么没有指定ssrc,要么指定的ssrc个数与rid个数一致 - //CHECK(rtc_media.rtp_ssrc_sim.empty() || rtc_media.rtp_ssrc_sim.size() == rtc_media.rtp_rids.size()); + // simulcast的情况下,要么没有指定ssrc,要么指定的ssrc个数与rid个数一致 + // CHECK(rtc_media.rtp_ssrc_sim.empty() || rtc_media.rtp_ssrc_sim.size() == rtc_media.rtp_rids.size()); } auto rtpmap_arr = media.getAllItem('a', "rtpmap"); auto rtcpfb_arr = media.getAllItem('a', "rtcp-fb"); auto fmtp_aar = media.getAllItem('a', "fmtp"); - //方便根据pt查找rtpmap,一个pt必有一条 + // 方便根据pt查找rtpmap,一个pt必有一条 map rtpmap_map; - //方便根据pt查找rtcp-fb,一个pt可能有多条或0条 + // 方便根据pt查找rtcp-fb,一个pt可能有多条或0条 multimap rtcpfb_map; - //方便根据pt查找fmtp,一个pt最多一条 + // 方便根据pt查找fmtp,一个pt最多一条 map fmtp_map; for (auto &rtpmap : rtpmap_arr) { - //添加失败,有多条 + // 添加失败,有多条 CHECK(rtpmap_map.emplace(rtpmap.pt, rtpmap).second, "该pt存在多条a=rtpmap:", (int)rtpmap.pt); } for (auto &rtpfb : rtcpfb_arr) { rtcpfb_map.emplace(rtpfb.pt, rtpfb); } for (auto &fmtp : fmtp_aar) { - //添加失败,有多条 + // 添加失败,有多条 CHECK(fmtp_map.emplace(fmtp.pt, fmtp).second, "该pt存在多条a=fmtp:", (int)fmtp.pt); } for (auto &item : mline.fmts) { auto pt = atoi(item.c_str()); CHECK(pt < 0xFF, "invalid payload type: ", item); - //遍历所有编码方案的pt + // 遍历所有编码方案的pt rtc_media.plan.emplace_back(); auto &plan = rtc_media.plan.back(); auto rtpmap_it = rtpmap_map.find(pt); @@ -952,8 +949,7 @@ void RtcSession::loadFrom(const string &str) { if (fmtp_it != fmtp_map.end()) { plan.fmtp = fmtp_it->second.fmtp; } - for (auto rtpfb_it = rtcpfb_map.find(pt); - rtpfb_it != rtcpfb_map.end() && rtpfb_it->second.pt == pt; ++rtpfb_it) { + for (auto rtpfb_it = rtcpfb_map.find(pt); rtpfb_it != rtcpfb_map.end() && rtpfb_it->second.pt == pt; ++rtpfb_it) { plan.rtcp_fb.emplace(rtpfb_it->second.rtcp_type); } } @@ -971,7 +967,7 @@ void RtcSdpBase::toRtsp() { case 'i': case 't': case 'c': - case 'b':{ + case 'b': { ++it; break; } @@ -986,8 +982,7 @@ void RtcSdpBase::toRtsp() { case 'a': { auto attr = dynamic_pointer_cast(*it); CHECK(attr); - if (!strcasecmp(attr->detail->getKey(), "rtpmap") - || !strcasecmp(attr->detail->getKey(), "fmtp")) { + if (!strcasecmp(attr->detail->getKey(), "rtpmap") || !strcasecmp(attr->detail->getKey(), "fmtp")) { ++it; break; } @@ -1000,7 +995,7 @@ void RtcSdpBase::toRtsp() { } } -string RtcSession::toRtspSdp() const{ +string RtcSession::toRtspSdp() const { RtcSession copy = *this; copy.media.clear(); for (auto &m : media) { @@ -1056,17 +1051,17 @@ void addSdpAttrSSRC(const RtcSSRC &rtp_ssrc, RtcSdpBase &media, uint32_t ssrc_nu } } -RtcSessionSdp::Ptr RtcSession::toRtcSessionSdp() const{ +RtcSessionSdp::Ptr RtcSession::toRtcSessionSdp() const { RtcSessionSdp::Ptr ret = std::make_shared(); auto &sdp = *ret; - sdp.addItem(std::make_shared >(to_string(version))); + sdp.addItem(std::make_shared>(to_string(version))); sdp.addItem(std::make_shared(origin)); - sdp.addItem(std::make_shared >(session_name)); + sdp.addItem(std::make_shared>(session_name)); if (!session_info.empty()) { - sdp.addItem(std::make_shared >(session_info)); + sdp.addItem(std::make_shared>(session_info)); } sdp.addItem(std::make_shared(time)); - if(connection.empty()){ + if (connection.empty()) { sdp.addItem(std::make_shared(connection)); } sdp.addAttr(std::make_shared(group)); @@ -1124,21 +1119,21 @@ RtcSessionSdp::Ptr RtcSession::toRtcSessionSdp() const{ sdp_media.addAttr(std::make_shared("rtcp-rsize")); } - if(m.type != TrackApplication) { + if (m.type != TrackApplication) { for (auto &p : m.plan) { auto rtp_map = std::make_shared(); rtp_map->pt = p.pt; rtp_map->codec = p.codec; rtp_map->sample_rate = p.sample_rate; rtp_map->channel = p.channel; - //添加a=rtpmap + // 添加a=rtpmap sdp_media.addAttr(std::move(rtp_map)); - for (auto &fb : p.rtcp_fb) { + for (auto &fb : p.rtcp_fb) { auto rtcp_fb = std::make_shared(); rtcp_fb->pt = p.pt; rtcp_fb->rtcp_type = fb; - //添加a=rtcp-fb + // 添加a=rtcp-fb sdp_media.addAttr(std::move(rtcp_fb)); } @@ -1146,13 +1141,13 @@ RtcSessionSdp::Ptr RtcSession::toRtcSessionSdp() const{ auto fmtp = std::make_shared(); fmtp->pt = p.pt; fmtp->fmtp = p.fmtp; - //添加a=fmtp + // 添加a=fmtp sdp_media.addAttr(std::move(fmtp)); } } { - //添加a=msid字段 + // 添加a=msid字段 if (!m.rtp_rtx_ssrc.empty()) { if (!m.rtp_rtx_ssrc[0].msid.empty()) { auto msid = std::make_shared(); @@ -1164,14 +1159,14 @@ RtcSessionSdp::Ptr RtcSession::toRtcSessionSdp() const{ { for (auto &ssrc : m.rtp_rtx_ssrc) { - //添加a=ssrc字段 + // 添加a=ssrc字段 CHECK(!ssrc.empty()); addSdpAttrSSRC(ssrc, sdp_media, ssrc.ssrc); if (ssrc.rtx_ssrc) { addSdpAttrSSRC(ssrc, sdp_media, ssrc.rtx_ssrc); - //生成a=ssrc-group:FID字段 - //有rtx ssrc + // 生成a=ssrc-group:FID字段 + // 有rtx ssrc auto group = std::make_shared(); group->type = "FID"; group->ssrcs.emplace_back(ssrc.ssrc); @@ -1183,12 +1178,12 @@ RtcSessionSdp::Ptr RtcSession::toRtcSessionSdp() const{ { if (m.rtp_ssrc_sim.size() >= 2) { - //simulcast 要求 2~3路 + // simulcast 要求 2~3路 auto group = std::make_shared(); for (auto &ssrc : m.rtp_ssrc_sim) { group->ssrcs.emplace_back(ssrc.ssrc); } - //添加a=ssrc-group:SIM字段 + // 添加a=ssrc-group:SIM字段 group->type = "SIM"; sdp_media.addAttr(std::move(group)); } @@ -1221,17 +1216,17 @@ RtcSessionSdp::Ptr RtcSession::toRtcSessionSdp() const{ } } } - if(ice_lite) + if (ice_lite) { sdp.addAttr(std::make_shared("ice-lite")); - + } return ret; } -string RtcSession::toString() const{ +string RtcSession::toString() const { return toRtcSessionSdp()->toString(); } -string RtcCodecPlan::getFmtp(const char *key) const{ +string RtcCodecPlan::getFmtp(const char *key) const { for (auto &item : fmtp) { if (strcasecmp(item.first.data(), key) == 0) { return item.second; @@ -1240,7 +1235,7 @@ string RtcCodecPlan::getFmtp(const char *key) const{ return ""; } -const RtcCodecPlan *RtcMedia::getPlan(uint8_t pt) const{ +const RtcCodecPlan *RtcMedia::getPlan(uint8_t pt) const { for (auto &item : plan) { if (item.pt == pt) { return &item; @@ -1249,7 +1244,7 @@ const RtcCodecPlan *RtcMedia::getPlan(uint8_t pt) const{ return nullptr; } -const RtcCodecPlan *RtcMedia::getPlan(const char *codec) const{ +const RtcCodecPlan *RtcMedia::getPlan(const char *codec) const { for (auto &item : plan) { if (strcasecmp(item.codec.data(), codec) == 0) { return &item; @@ -1258,7 +1253,7 @@ const RtcCodecPlan *RtcMedia::getPlan(const char *codec) const{ return nullptr; } -const RtcCodecPlan *RtcMedia::getRelatedRtxPlan(uint8_t pt) const{ +const RtcCodecPlan *RtcMedia::getRelatedRtxPlan(uint8_t pt) const { for (auto &item : plan) { if (strcasecmp(item.codec.data(), "rtx") == 0) { auto apt = atoi(item.getFmtp("apt").data()); @@ -1294,7 +1289,7 @@ bool RtcMedia::supportSimulcast() const { return false; } -void RtcMedia::checkValid() const{ +void RtcMedia::checkValid() const { CHECK(type != TrackInvalid); CHECK(!mid.empty()); CHECK(!proto.empty()); @@ -1304,7 +1299,7 @@ void RtcMedia::checkValid() const{ bool send_rtp = (direction == RtpDirection::sendonly || direction == RtpDirection::sendrecv); if (!supportSimulcast()) { - //非simulcast时,检查有没有指定rtp ssrc + // 非simulcast时,检查有没有指定rtp ssrc CHECK(!rtp_rtx_ssrc.empty() || !send_rtp); } @@ -1318,7 +1313,7 @@ void RtcMedia::checkValid() const{ #endif } -void RtcSession::checkValid() const{ +void RtcSession::checkValid() const { CHECK(version == 0); CHECK(!origin.empty()); CHECK(!session_name.empty()); @@ -1337,15 +1332,15 @@ void RtcSession::checkValid() const{ case RtpDirection::sendrecv: case RtpDirection::sendonly: case RtpDirection::recvonly: have_active_media = true; break; - default : break; + default: break; } } CHECK(have_active_media, "必须确保最少有一个活跃的track"); } -const RtcMedia *RtcSession::getMedia(TrackType type) const{ - for(auto &m : media){ - if(m.type == type){ +const RtcMedia *RtcSession::getMedia(TrackType type) const { + for (auto &m : media) { + if (m.type == type) { return &m; } } @@ -1377,7 +1372,7 @@ bool RtcSession::isOnlyDatachannel() const { string const SdpConst::kTWCCRtcpFb = "transport-cc"; string const SdpConst::kRembRtcpFb = "goog-remb"; -void RtcConfigure::RtcTrackConfigure::enableTWCC(bool enable){ +void RtcConfigure::RtcTrackConfigure::enableTWCC(bool enable) { if (!enable) { rtcp_fb.erase(SdpConst::kTWCCRtcpFb); extmap.erase(RtpExtType::transport_cc); @@ -1387,7 +1382,7 @@ void RtcConfigure::RtcTrackConfigure::enableTWCC(bool enable){ } } -void RtcConfigure::RtcTrackConfigure::enableREMB(bool enable){ +void RtcConfigure::RtcTrackConfigure::enableREMB(bool enable) { if (!enable) { rtcp_fb.erase(SdpConst::kRembRtcpFb); extmap.erase(RtpExtType::abs_send_time); @@ -1397,7 +1392,7 @@ void RtcConfigure::RtcTrackConfigure::enableREMB(bool enable){ } } -static vector toCodecArray(const string &str){ +static vector toCodecArray(const string &str) { vector ret; auto vec = split(str, ","); for (auto &s : vec) { @@ -1409,7 +1404,7 @@ static vector toCodecArray(const string &str){ return ret; } -void RtcConfigure::RtcTrackConfigure::setDefaultSetting(TrackType type){ +void RtcConfigure::RtcTrackConfigure::setDefaultSetting(TrackType type) { rtcp_mux = true; rtcp_rsize = false; group_bundle = true; @@ -1421,47 +1416,43 @@ void RtcConfigure::RtcTrackConfigure::setDefaultSetting(TrackType type){ ice_renomination = false; switch (type) { case TrackAudio: { - //此处调整偏好的编码格式优先级 + // 此处调整偏好的编码格式优先级 GET_CONFIG_FUNC(vector, s_preferred_codec, Rtc::kPreferredCodecA, toCodecArray); CHECK(!s_preferred_codec.empty(), "rtc音频偏好codec不能为空"); preferred_codec = s_preferred_codec; - rtcp_fb = {SdpConst::kTWCCRtcpFb, SdpConst::kRembRtcpFb}; - extmap = { - {RtpExtType::ssrc_audio_level, RtpDirection::sendrecv}, - {RtpExtType::csrc_audio_level, RtpDirection::sendrecv}, - {RtpExtType::abs_send_time, RtpDirection::sendrecv}, - {RtpExtType::transport_cc, RtpDirection::sendrecv}, - //rtx重传rtp时,忽略sdes_mid类型的rtp ext,实测发现Firefox在接收rtx时,如果存在sdes_mid的ext,将导致无法播放 - //{RtpExtType::sdes_mid,RtpDirection::sendrecv}, - {RtpExtType::sdes_rtp_stream_id, RtpDirection::sendrecv}, - {RtpExtType::sdes_repaired_rtp_stream_id, RtpDirection::sendrecv} - }; + rtcp_fb = { SdpConst::kTWCCRtcpFb, SdpConst::kRembRtcpFb }; + extmap = { { RtpExtType::ssrc_audio_level, RtpDirection::sendrecv }, + { RtpExtType::csrc_audio_level, RtpDirection::sendrecv }, + { RtpExtType::abs_send_time, RtpDirection::sendrecv }, + { RtpExtType::transport_cc, RtpDirection::sendrecv }, + // rtx重传rtp时,忽略sdes_mid类型的rtp ext,实测发现Firefox在接收rtx时,如果存在sdes_mid的ext,将导致无法播放 + //{RtpExtType::sdes_mid,RtpDirection::sendrecv}, + { RtpExtType::sdes_rtp_stream_id, RtpDirection::sendrecv }, + { RtpExtType::sdes_repaired_rtp_stream_id, RtpDirection::sendrecv } }; break; } case TrackVideo: { - //此处调整偏好的编码格式优先级 + // 此处调整偏好的编码格式优先级 GET_CONFIG_FUNC(vector, s_preferred_codec, Rtc::kPreferredCodecV, toCodecArray); CHECK(!s_preferred_codec.empty(), "rtc视频偏好codec不能为空"); preferred_codec = s_preferred_codec; - rtcp_fb = {SdpConst::kTWCCRtcpFb, SdpConst::kRembRtcpFb, "nack", "ccm fir", "nack pli"}; - extmap = { - {RtpExtType::abs_send_time, RtpDirection::sendrecv}, - {RtpExtType::transport_cc, RtpDirection::sendrecv}, - //rtx重传rtp时,忽略sdes_mid类型的rtp ext,实测发现Firefox在接收rtx时,如果存在sdes_mid的ext,将导致无法播放 - //{RtpExtType::sdes_mid,RtpDirection::sendrecv}, - {RtpExtType::sdes_rtp_stream_id, RtpDirection::sendrecv}, - {RtpExtType::sdes_repaired_rtp_stream_id, RtpDirection::sendrecv}, - {RtpExtType::video_timing, RtpDirection::sendrecv}, - {RtpExtType::color_space, RtpDirection::sendrecv}, - {RtpExtType::video_content_type, RtpDirection::sendrecv}, - {RtpExtType::playout_delay, RtpDirection::sendrecv}, - //手机端推webrtc 会带有旋转角度,rtc协议能正常播放 其他协议拉流画面旋转 - //{RtpExtType::video_orientation, RtpDirection::sendrecv}, - {RtpExtType::toffset, RtpDirection::sendrecv}, - {RtpExtType::framemarking, RtpDirection::sendrecv} - }; + rtcp_fb = { SdpConst::kTWCCRtcpFb, SdpConst::kRembRtcpFb, "nack", "ccm fir", "nack pli" }; + extmap = { { RtpExtType::abs_send_time, RtpDirection::sendrecv }, + { RtpExtType::transport_cc, RtpDirection::sendrecv }, + // rtx重传rtp时,忽略sdes_mid类型的rtp ext,实测发现Firefox在接收rtx时,如果存在sdes_mid的ext,将导致无法播放 + //{RtpExtType::sdes_mid,RtpDirection::sendrecv}, + { RtpExtType::sdes_rtp_stream_id, RtpDirection::sendrecv }, + { RtpExtType::sdes_repaired_rtp_stream_id, RtpDirection::sendrecv }, + { RtpExtType::video_timing, RtpDirection::sendrecv }, + { RtpExtType::color_space, RtpDirection::sendrecv }, + { RtpExtType::video_content_type, RtpDirection::sendrecv }, + { RtpExtType::playout_delay, RtpDirection::sendrecv }, + // 手机端推webrtc 会带有旋转角度,rtc协议能正常播放 其他协议拉流画面旋转 + //{RtpExtType::video_orientation, RtpDirection::sendrecv}, + { RtpExtType::toffset, RtpDirection::sendrecv }, + { RtpExtType::framemarking, RtpDirection::sendrecv } }; break; } case TrackApplication: { @@ -1471,8 +1462,7 @@ void RtcConfigure::RtcTrackConfigure::setDefaultSetting(TrackType type){ } } -void RtcConfigure::setDefaultSetting(string ice_ufrag, string ice_pwd, RtpDirection direction, - const SdpAttrFingerprint &fingerprint) { +void RtcConfigure::setDefaultSetting(string ice_ufrag, string ice_pwd, RtpDirection direction, const SdpAttrFingerprint &fingerprint) { video.setDefaultSetting(TrackVideo); audio.setDefaultSetting(TrackAudio); application.setDefaultSetting(TrackApplication); @@ -1512,7 +1502,7 @@ void RtcConfigure::addCandidate(const SdpAttrCandidate &candidate, TrackType typ } } -void RtcConfigure::enableTWCC(bool enable, TrackType type){ +void RtcConfigure::enableTWCC(bool enable, TrackType type) { switch (type) { case TrackAudio: { audio.enableTWCC(enable); @@ -1530,7 +1520,7 @@ void RtcConfigure::enableTWCC(bool enable, TrackType type){ } } -void RtcConfigure::enableREMB(bool enable, TrackType type){ +void RtcConfigure::enableREMB(bool enable, TrackType type) { switch (type) { case TrackAudio: { audio.enableREMB(enable); @@ -1559,10 +1549,10 @@ shared_ptr RtcConfigure::createAnswer(const RtcSession &offer) const matchMedia(ret, m); } - //设置音视频端口复用 + // 设置音视频端口复用 if (!offer.group.mids.empty()) { for (auto &m : ret->media) { - //The remote end has rejected (port 0) the m-section, so it should not be putting its mid in the group attribute. + // The remote end has rejected (port 0) the m-section, so it should not be putting its mid in the group attribute. if (m.port) { ret->group.mids.emplace_back(m.mid); } @@ -1571,32 +1561,32 @@ shared_ptr RtcConfigure::createAnswer(const RtcSession &offer) const return ret; } -static RtpDirection matchDirection(RtpDirection offer_direction, RtpDirection supported){ +static RtpDirection matchDirection(RtpDirection offer_direction, RtpDirection supported) { switch (offer_direction) { - case RtpDirection::sendonly : { + case RtpDirection::sendonly: { if (supported != RtpDirection::recvonly && supported != RtpDirection::sendrecv) { - //我们不支持接收 + // 我们不支持接收 return RtpDirection::inactive; } return RtpDirection::recvonly; } - case RtpDirection::recvonly : { + case RtpDirection::recvonly: { if (supported != RtpDirection::sendonly && supported != RtpDirection::sendrecv) { - //我们不支持发送 + // 我们不支持发送 return RtpDirection::inactive; } return RtpDirection::sendonly; } - //对方支持发送接收,那么最终能力根据配置来决定 - case RtpDirection::sendrecv : return (supported == RtpDirection::invalid ? RtpDirection::inactive : supported); - case RtpDirection::inactive : return RtpDirection::inactive; + // 对方支持发送接收,那么最终能力根据配置来决定 + case RtpDirection::sendrecv: return (supported == RtpDirection::invalid ? RtpDirection::inactive : supported); + case RtpDirection::inactive: return RtpDirection::inactive; default: return RtpDirection::invalid; } } -static DtlsRole mathDtlsRole(DtlsRole role){ +static DtlsRole mathDtlsRole(DtlsRole role) { switch (role) { case DtlsRole::actpass: case DtlsRole::active: return DtlsRole::passive; @@ -1605,7 +1595,7 @@ static DtlsRole mathDtlsRole(DtlsRole role){ } } -void RtcConfigure::matchMedia(const std::shared_ptr &ret,const RtcMedia &offer_media) const { +void RtcConfigure::matchMedia(const std::shared_ptr &ret, const RtcMedia &offer_media) const { bool check_profile = true; bool check_codec = true; const RtcTrackConfigure *cfg_ptr = nullptr; @@ -1643,20 +1633,20 @@ RETRY: } const RtcCodecPlan *selected_plan = nullptr; for (auto &plan : offer_media.plan) { - //先检查编码格式是否为偏好 + // 先检查编码格式是否为偏好 if (check_codec && getCodecId(plan.codec) != codec) { continue; } - //命中偏好的编码格式,然后检查规格 + // 命中偏好的编码格式,然后检查规格 if (check_profile && !onCheckCodecProfile(plan, codec)) { continue; } - //找到中意的codec + // 找到中意的codec selected_plan = &plan; break; } if (!selected_plan) { - //offer中该媒体的所有的codec都不支持 + // offer中该媒体的所有的codec都不支持 continue; } RtcMedia answer_media; @@ -1682,24 +1672,23 @@ RETRY: answer_media.role = mathDtlsRole(offer_media.role); - //如果codec匹配失败,那么禁用该track - answer_media.direction = check_codec ? matchDirection(offer_media.direction, configure.direction) - : RtpDirection::inactive; + // 如果codec匹配失败,那么禁用该track + answer_media.direction = check_codec ? matchDirection(offer_media.direction, configure.direction) : RtpDirection::inactive; if (answer_media.direction == RtpDirection::invalid) { continue; } if (answer_media.direction == RtpDirection::sendrecv) { - //如果是收发双向,那么我们拷贝offer sdp的ssrc,确保ssrc一致 + // 如果是收发双向,那么我们拷贝offer sdp的ssrc,确保ssrc一致 answer_media.rtp_rtx_ssrc = offer_media.rtp_rtx_ssrc; } - //添加媒体plan + // 添加媒体plan answer_media.plan.emplace_back(*selected_plan); onSelectPlan(answer_media.plan.back(), codec); - set pt_selected = {selected_plan->pt}; + set pt_selected = { selected_plan->pt }; - //添加rtx,red,ulpfec plan + // 添加rtx,red,ulpfec plan if (configure.support_red || configure.support_rtx || configure.support_ulpfec) { for (auto &plan : offer_media.plan) { if (!strcasecmp(plan.codec.data(), "rtx")) { @@ -1726,7 +1715,7 @@ RETRY: } } - //对方和我方都支持的扩展,那么我们才支持 + // 对方和我方都支持的扩展,那么我们才支持 for (auto &ext : offer_media.extmap) { auto it = configure.extmap.find(RtpExt::getExtType(ext.ext)); if (it != configure.extmap.end()) { @@ -1743,10 +1732,10 @@ RETRY: auto &rtcp_fb_ref = answer_media.plan[0].rtcp_fb; rtcp_fb_ref.clear(); - //对方和我方都支持的rtcpfb,那么我们才支持 + // 对方和我方都支持的rtcpfb,那么我们才支持 for (auto &fp : selected_plan->rtcp_fb) { if (configure.rtcp_fb.find(fp) != configure.rtcp_fb.end()) { - //对方该rtcp被我们支持 + // 对方该rtcp被我们支持 rtcp_fb_ref.emplace(fp); } } @@ -1764,19 +1753,19 @@ RETRY: } if (check_profile) { - //如果是由于检查profile导致匹配失败,那么重试一次,且不检查profile + // 如果是由于检查profile导致匹配失败,那么重试一次,且不检查profile check_profile = false; goto RETRY; } if (check_codec) { - //如果是由于检查codec导致匹配失败,那么重试一次,且不检查codec + // 如果是由于检查codec导致匹配失败,那么重试一次,且不检查codec check_codec = false; goto RETRY; } } -void RtcConfigure::setPlayRtspInfo(const string &sdp){ +void RtcConfigure::setPlayRtspInfo(const string &sdp) { RtcSession session; video.direction = RtpDirection::inactive; audio.direction = RtpDirection::inactive; @@ -1784,15 +1773,15 @@ void RtcConfigure::setPlayRtspInfo(const string &sdp){ session.loadFrom(sdp); for (auto &m : session.media) { switch (m.type) { - case TrackVideo : { + case TrackVideo: { video.direction = RtpDirection::sendonly; _rtsp_video_plan = std::make_shared(m.plan[0]); video.preferred_codec.clear(); video.preferred_codec.emplace_back(getCodecId(_rtsp_video_plan->codec)); break; } - case TrackAudio : { - audio.direction = RtpDirection::sendonly; + case TrackAudio: { + audio.direction = RtpDirection::sendonly; _rtsp_audio_plan = std::make_shared(m.plan[0]); audio.preferred_codec.clear(); audio.preferred_codec.emplace_back(getCodecId(_rtsp_audio_plan->codec)); @@ -1803,21 +1792,21 @@ void RtcConfigure::setPlayRtspInfo(const string &sdp){ } } -static const string kProfile{"profile-level-id"}; -static const string kMode{"packetization-mode"}; +static const string kProfile { "profile-level-id" }; +static const string kMode { "packetization-mode" }; bool RtcConfigure::onCheckCodecProfile(const RtcCodecPlan &plan, CodecId codec) const { if (_rtsp_audio_plan && codec == getCodecId(_rtsp_audio_plan->codec)) { if (plan.sample_rate != _rtsp_audio_plan->sample_rate || plan.channel != _rtsp_audio_plan->channel) { - //音频采样率和通道数必须相同 + // 音频采样率和通道数必须相同 return false; } return true; } if (_rtsp_video_plan && codec == CodecH264 && getCodecId(_rtsp_video_plan->codec) == CodecH264) { - //h264时,profile-level-id + // h264时,profile-level-id if (strcasecmp(_rtsp_video_plan->fmtp[kProfile].data(), const_cast(plan).fmtp[kProfile].data())) { - //profile-level-id 不匹配 + // profile-level-id 不匹配 return false; } return true; diff --git a/webrtc/Sdp.h b/webrtc/Sdp.h index cec182a8..25436a9f 100644 --- a/webrtc/Sdp.h +++ b/webrtc/Sdp.h @@ -22,97 +22,87 @@ namespace mediakit { -//https://datatracker.ietf.org/doc/rfc4566/?include_text=1 -//https://blog.csdn.net/aggresss/article/details/109850434 -//https://aggresss.blog.csdn.net/article/details/106436703 -//Session description -// v= (protocol version) -// o= (originator and session identifier) -// s= (session name) -// i=* (session information) -// u=* (URI of description) -// e=* (email address) -// p=* (phone number) -// c=* (connection information -- not required if included in -// all media) -// b=* (zero or more bandwidth information lines) -// One or more time descriptions ("t=" and "r=" lines; see below) -// z=* (time zone adjustments) -// k=* (encryption key) -// a=* (zero or more session attribute lines) -// Zero or more media descriptions +// https://datatracker.ietf.org/doc/rfc4566/?include_text=1 +// https://blog.csdn.net/aggresss/article/details/109850434 +// https://aggresss.blog.csdn.net/article/details/106436703 +// Session description +// v= (protocol version) +// o= (originator and session identifier) +// s= (session name) +// i=* (session information) +// u=* (URI of description) +// e=* (email address) +// p=* (phone number) +// c=* (connection information -- not required if included in +// all media) +// b=* (zero or more bandwidth information lines) +// One or more time descriptions ("t=" and "r=" lines; see below) +// z=* (time zone adjustments) +// k=* (encryption key) +// a=* (zero or more session attribute lines) +// Zero or more media descriptions // -// Time description -// t= (time the session is active) -// r=* (zero or more repeat times) +// Time description +// t= (time the session is active) +// r=* (zero or more repeat times) // -// Media description, if present -// m= (media name and transport address) -// i=* (media title) -// c=* (connection information -- optional if included at -// session level) -// b=* (zero or more bandwidth information lines) -// k=* (encryption key) -// a=* (zero or more media attribute lines) +// Media description, if present +// m= (media name and transport address) +// i=* (media title) +// c=* (connection information -- optional if included at +// session level) +// b=* (zero or more bandwidth information lines) +// k=* (encryption key) +// a=* (zero or more media attribute lines) enum class RtpDirection { invalid = -1, - //只发送 + // 只发送 sendonly, - //只接收 + // 只接收 recvonly, - //同时发送接收 + // 同时发送接收 sendrecv, - //禁止发送数据 + // 禁止发送数据 inactive }; enum class DtlsRole { invalid = -1, - //客户端 + // 客户端 active, - //服务端 + // 服务端 passive, - //既可作做客户端也可以做服务端 + // 既可作做客户端也可以做服务端 actpass, }; -enum class SdpType { - invalid = -1, - offer, - answer -}; +enum class SdpType { invalid = -1, offer, answer }; DtlsRole getDtlsRole(const std::string &str); -const char* getDtlsRoleString(DtlsRole role); +const char *getDtlsRoleString(DtlsRole role); RtpDirection getRtpDirection(const std::string &str); -const char* getRtpDirectionString(RtpDirection val); +const char *getRtpDirectionString(RtpDirection val); class SdpItem { public: using Ptr = std::shared_ptr; virtual ~SdpItem() = default; - virtual void parse(const std::string &str) { - value = str; - } - virtual std::string toString() const { - return value; - } - virtual const char* getKey() const = 0; + virtual void parse(const std::string &str) { value = str; } + virtual std::string toString() const { return value; } + virtual const char *getKey() const = 0; - void reset() { - value.clear(); - } + void reset() { value.clear(); } protected: mutable std::string value; }; template -class SdpString : public SdpItem{ +class SdpString : public SdpItem { public: SdpString() = default; - SdpString(std::string val) {value = std::move(val);} + SdpString(std::string val) { value = std::move(val); } // *=* const char* getKey() const override { static std::string key(1, KEY); return key.data();} }; @@ -126,34 +116,34 @@ public: this->value = std::move(val); } - const char* getKey() const override { return key.data();} + const char *getKey() const override { return key.data(); } }; -class SdpTime : public SdpItem{ +class SdpTime : public SdpItem { public: - //5.9. Timing ("t=") - // t= - uint64_t start {0}; - uint64_t stop {0}; + // 5.9. Timing ("t=") + // t= + uint64_t start { 0 }; + uint64_t stop { 0 }; void parse(const std::string &str) override; std::string toString() const override; - const char* getKey() const override { return "t";} + const char *getKey() const override { return "t"; } }; -class SdpOrigin : public SdpItem{ +class SdpOrigin : public SdpItem { public: // 5.2. Origin ("o=") // o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5 // o= - std::string username {"-"}; + std::string username { "-" }; std::string session_id; std::string session_version; - std::string nettype {"IN"}; - std::string addrtype {"IP4"}; - std::string address {"0.0.0.0"}; + std::string nettype { "IN" }; + std::string addrtype { "IP4" }; + std::string address { "0.0.0.0" }; void parse(const std::string &str) override; std::string toString() const override; - const char* getKey() const override { return "o";} + const char *getKey() const override { return "o"; } bool empty() const { return username.empty() || session_id.empty() || session_version.empty() || nettype.empty() || addrtype.empty() || address.empty(); @@ -165,28 +155,28 @@ public: // 5.7. Connection Data ("c=") // c=IN IP4 224.2.17.12/127 // c= - std::string nettype {"IN"}; - std::string addrtype {"IP4"}; - std::string address {"0.0.0.0"}; + std::string nettype { "IN" }; + std::string addrtype { "IP4" }; + std::string address { "0.0.0.0" }; void parse(const std::string &str) override; std::string toString() const override; - const char* getKey() const override { return "c";} - bool empty() const {return address.empty();} + const char *getKey() const override { return "c"; } + bool empty() const { return address.empty(); } }; class SdpBandwidth : public SdpItem { public: - //5.8. Bandwidth ("b=") - //b=: + // 5.8. Bandwidth ("b=") + // b=: - //AS、CT - std::string bwtype {"AS"}; - uint32_t bandwidth {0}; + // AS、CT + std::string bwtype { "AS" }; + uint32_t bandwidth { 0 }; void parse(const std::string &str) override; std::string toString() const override; - const char* getKey() const override { return "b";} - bool empty() const {return bandwidth == 0;} + const char *getKey() const override { return "b"; } + bool empty() const { return bandwidth == 0; } }; class SdpMedia : public SdpItem { @@ -195,287 +185,284 @@ public: // m= ... TrackType type; uint16_t port; - //RTP/AVP:应用场景为视频/音频的 RTP 协议。参考 RFC 3551 - //RTP/SAVP:应用场景为视频/音频的 SRTP 协议。参考 RFC 3711 - //RTP/AVPF: 应用场景为视频/音频的 RTP 协议,支持 RTCP-based Feedback。参考 RFC 4585 - //RTP/SAVPF: 应用场景为视频/音频的 SRTP 协议,支持 RTCP-based Feedback。参考 RFC 5124 + // RTP/AVP:应用场景为视频/音频的 RTP 协议。参考 RFC 3551 + // RTP/SAVP:应用场景为视频/音频的 SRTP 协议。参考 RFC 3711 + // RTP/AVPF: 应用场景为视频/音频的 RTP 协议,支持 RTCP-based Feedback。参考 RFC 4585 + // RTP/SAVPF: 应用场景为视频/音频的 SRTP 协议,支持 RTCP-based Feedback。参考 RFC 5124 std::string proto; std::vector fmts; void parse(const std::string &str) override; std::string toString() const override; - const char* getKey() const override { return "m";} + const char *getKey() const override { return "m"; } }; -class SdpAttr : public SdpItem{ +class SdpAttr : public SdpItem { public: using Ptr = std::shared_ptr; - //5.13. Attributes ("a=") - //a= - //a=: + // 5.13. Attributes ("a=") + // a= + // a=: SdpItem::Ptr detail; void parse(const std::string &str) override; std::string toString() const override; - const char* getKey() const override { return "a";} + const char *getKey() const override { return "a"; } }; -class SdpAttrGroup : public SdpItem{ +class SdpAttrGroup : public SdpItem { public: - //a=group:BUNDLE line with all the 'mid' identifiers part of the - // BUNDLE group is included at the session-level. - //a=group:LS session level attribute MUST be included wth the 'mid' - // identifiers that are part of the same lip sync group. - std::string type {"BUNDLE"}; + // a=group:BUNDLE line with all the 'mid' identifiers part of the + // BUNDLE group is included at the session-level. + // a=group:LS session level attribute MUST be included wth the 'mid' + // identifiers that are part of the same lip sync group. + std::string type { "BUNDLE" }; std::vector mids; - void parse(const std::string &str) override ; - std::string toString() const override ; - const char* getKey() const override { return "group";} + void parse(const std::string &str) override; + std::string toString() const override; + const char *getKey() const override { return "group"; } }; class SdpAttrMsidSemantic : public SdpItem { public: - //https://tools.ietf.org/html/draft-alvestrand-rtcweb-msid-02#section-3 - //3. The Msid-Semantic Attribute + // https://tools.ietf.org/html/draft-alvestrand-rtcweb-msid-02#section-3 + // 3. The Msid-Semantic Attribute // - // In order to fully reproduce the semantics of the SDP and SSRC - // grouping frameworks, a session-level attribute is defined for - // signalling the semantics associated with an msid grouping. + // In order to fully reproduce the semantics of the SDP and SSRC + // grouping frameworks, a session-level attribute is defined for + // signalling the semantics associated with an msid grouping. // - // This OPTIONAL attribute gives the message ID and its group semantic. - // a=msid-semantic: examplefoo LS + // This OPTIONAL attribute gives the message ID and its group semantic. + // a=msid-semantic: examplefoo LS // // - // The ABNF of msid-semantic is: + // The ABNF of msid-semantic is: // - // msid-semantic-attr = "msid-semantic:" " " msid token - // token = + // msid-semantic-attr = "msid-semantic:" " " msid token + // token = // - // The semantic field may hold values from the IANA registries - // "Semantics for the "ssrc-group" SDP Attribute" and "Semantics for the - // "group" SDP Attribute". - //a=msid-semantic: WMS 616cfbb1-33a3-4d8c-8275-a199d6005549 - std::string msid{"WMS"}; + // The semantic field may hold values from the IANA registries + // "Semantics for the "ssrc-group" SDP Attribute" and "Semantics for the + // "group" SDP Attribute". + // a=msid-semantic: WMS 616cfbb1-33a3-4d8c-8275-a199d6005549 + std::string msid { "WMS" }; std::string token; void parse(const std::string &str) override; std::string toString() const override; - const char* getKey() const override { return "msid-semantic";} - bool empty() const { - return msid.empty(); - } + const char *getKey() const override { return "msid-semantic"; } + bool empty() const { return msid.empty(); } }; class SdpAttrRtcp : public SdpItem { public: // a=rtcp:9 IN IP4 0.0.0.0 - uint16_t port{0}; - std::string nettype {"IN"}; - std::string addrtype {"IP4"}; - std::string address {"0.0.0.0"}; - void parse(const std::string &str) override;; + uint16_t port { 0 }; + std::string nettype { "IN" }; + std::string addrtype { "IP4" }; + std::string address { "0.0.0.0" }; + void parse(const std::string &str) override; + ; std::string toString() const override; - const char* getKey() const override { return "rtcp";} - bool empty() const { - return address.empty() || !port; - } + const char *getKey() const override { return "rtcp"; } + bool empty() const { return address.empty() || !port; } }; class SdpAttrIceUfrag : public SdpItem { public: SdpAttrIceUfrag() = default; - SdpAttrIceUfrag(std::string str) {value = std::move(str);} - //a=ice-ufrag:sXJ3 - const char* getKey() const override { return "ice-ufrag";} + SdpAttrIceUfrag(std::string str) { value = std::move(str); } + // a=ice-ufrag:sXJ3 + const char *getKey() const override { return "ice-ufrag"; } }; class SdpAttrIcePwd : public SdpItem { public: SdpAttrIcePwd() = default; - SdpAttrIcePwd(std::string str) {value = std::move(str);} - //a=ice-pwd:yEclOTrLg1gEubBFefOqtmyV - const char* getKey() const override { return "ice-pwd";} + SdpAttrIcePwd(std::string str) { value = std::move(str); } + // a=ice-pwd:yEclOTrLg1gEubBFefOqtmyV + const char *getKey() const override { return "ice-pwd"; } }; class SdpAttrIceOption : public SdpItem { public: - //a=ice-options:trickle - bool trickle{false}; - bool renomination{false}; + // a=ice-options:trickle + bool trickle { false }; + bool renomination { false }; void parse(const std::string &str) override; std::string toString() const override; - const char* getKey() const override { return "ice-options";} + const char *getKey() const override { return "ice-options"; } }; class SdpAttrFingerprint : public SdpItem { public: - //a=fingerprint:sha-256 22:14:B5:AF:66:12:C7:C7:8D:EF:4B:DE:40:25:ED:5D:8F:17:54:DD:88:33:C0:13:2E:FD:1A:FA:7E:7A:1B:79 + // a=fingerprint:sha-256 22:14:B5:AF:66:12:C7:C7:8D:EF:4B:DE:40:25:ED:5D:8F:17:54:DD:88:33:C0:13:2E:FD:1A:FA:7E:7A:1B:79 std::string algorithm; std::string hash; void parse(const std::string &str) override; std::string toString() const override; - const char* getKey() const override { return "fingerprint";} + const char *getKey() const override { return "fingerprint"; } bool empty() const { return algorithm.empty() || hash.empty(); } }; class SdpAttrSetup : public SdpItem { public: - //a=setup:actpass + // a=setup:actpass SdpAttrSetup() = default; SdpAttrSetup(DtlsRole r) { role = r; } - DtlsRole role{DtlsRole::actpass}; + DtlsRole role { DtlsRole::actpass }; void parse(const std::string &str) override; std::string toString() const override; - const char* getKey() const override { return "setup";} + const char *getKey() const override { return "setup"; } }; class SdpAttrMid : public SdpItem { public: SdpAttrMid() = default; SdpAttrMid(std::string val) { value = std::move(val); } - //a=mid:audio - const char* getKey() const override { return "mid";} + // a=mid:audio + const char *getKey() const override { return "mid"; } }; class SdpAttrExtmap : public SdpItem { public: - //https://aggresss.blog.csdn.net/article/details/106436703 - //a=extmap:1[/sendonly] urn:ietf:params:rtp-hdrext:ssrc-audio-level + // https://aggresss.blog.csdn.net/article/details/106436703 + // a=extmap:1[/sendonly] urn:ietf:params:rtp-hdrext:ssrc-audio-level uint8_t id; - RtpDirection direction{RtpDirection::invalid}; + RtpDirection direction { RtpDirection::invalid }; std::string ext; void parse(const std::string &str) override; std::string toString() const override; - const char* getKey() const override { return "extmap";} + const char *getKey() const override { return "extmap"; } }; class SdpAttrRtpMap : public SdpItem { public: - //a=rtpmap:111 opus/48000/2 + // a=rtpmap:111 opus/48000/2 uint8_t pt; std::string codec; uint32_t sample_rate; - uint32_t channel {0}; + uint32_t channel { 0 }; void parse(const std::string &str) override; std::string toString() const override; - const char* getKey() const override { return "rtpmap";} + const char *getKey() const override { return "rtpmap"; } }; class SdpAttrRtcpFb : public SdpItem { public: - //a=rtcp-fb:98 nack pli - //a=rtcp-fb:120 nack 支持 nack 重传,nack (Negative-Acknowledgment) 。 - //a=rtcp-fb:120 nack pli 支持 nack 关键帧重传,PLI (Picture Loss Indication) 。 - //a=rtcp-fb:120 ccm fir 支持编码层关键帧请求,CCM (Codec Control Message),FIR (Full Intra Request ),通常与 nack pli 有同样的效果,但是 nack pli 是用于重传时的关键帧请求。 - //a=rtcp-fb:120 goog-remb 支持 REMB (Receiver Estimated Maximum Bitrate) 。 - //a=rtcp-fb:120 transport-cc 支持 TCC (Transport Congest Control) 。 + // a=rtcp-fb:98 nack pli + // a=rtcp-fb:120 nack 支持 nack 重传,nack (Negative-Acknowledgment) 。 + // a=rtcp-fb:120 nack pli 支持 nack 关键帧重传,PLI (Picture Loss Indication) 。 + // a=rtcp-fb:120 ccm fir 支持编码层关键帧请求,CCM (Codec Control Message),FIR (Full Intra Request ),通常与 nack pli 有同样的效果,但是 nack pli + // 是用于重传时的关键帧请求。 a=rtcp-fb:120 goog-remb 支持 REMB (Receiver Estimated Maximum Bitrate) 。 a=rtcp-fb:120 transport-cc 支持 TCC (Transport + // Congest Control) 。 uint8_t pt; std::string rtcp_type; void parse(const std::string &str) override; std::string toString() const override; - const char* getKey() const override { return "rtcp-fb";} + const char *getKey() const override { return "rtcp-fb"; } }; class SdpAttrFmtp : public SdpItem { public: - //fmtp:96 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f + // fmtp:96 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f uint8_t pt; - std::map fmtp; + std::map fmtp; void parse(const std::string &str) override; std::string toString() const override; - const char* getKey() const override { return "fmtp";} + const char *getKey() const override { return "fmtp"; } }; class SdpAttrSSRC : public SdpItem { public: - //a=ssrc:3245185839 cname:Cx4i/VTR51etgjT7 - //a=ssrc:3245185839 msid:cb373bff-0fea-4edb-bc39-e49bb8e8e3b9 0cf7e597-36a2-4480-9796-69bf0955eef5 - //a=ssrc:3245185839 mslabel:cb373bff-0fea-4edb-bc39-e49bb8e8e3b9 - //a=ssrc:3245185839 label:0cf7e597-36a2-4480-9796-69bf0955eef5 - //a=ssrc: - //a=ssrc: : - //cname 是必须的,msid/mslabel/label 这三个属性都是 WebRTC 自创的,或者说 Google 自创的,可以参考 https://tools.ietf.org/html/draft-ietf-mmusic-msid-17, - // 理解它们三者的关系需要先了解三个概念:RTP stream / MediaStreamTrack / MediaStream : - //一个 a=ssrc 代表一个 RTP stream ; - //一个 MediaStreamTrack 通常包含一个或多个 RTP stream,例如一个视频 MediaStreamTrack 中通常包含两个 RTP stream,一个用于常规传输,一个用于 nack 重传; - //一个 MediaStream 通常包含一个或多个 MediaStreamTrack ,例如 simulcast 场景下,一个 MediaStream 通常会包含三个不同编码质量的 MediaStreamTrack ; - //这种标记方式并不被 Firefox 认可,在 Firefox 生成的 SDP 中一个 a=ssrc 通常只有一行,例如: - //a=ssrc:3245185839 cname:Cx4i/VTR51etgjT7 + // a=ssrc:3245185839 cname:Cx4i/VTR51etgjT7 + // a=ssrc:3245185839 msid:cb373bff-0fea-4edb-bc39-e49bb8e8e3b9 0cf7e597-36a2-4480-9796-69bf0955eef5 + // a=ssrc:3245185839 mslabel:cb373bff-0fea-4edb-bc39-e49bb8e8e3b9 + // a=ssrc:3245185839 label:0cf7e597-36a2-4480-9796-69bf0955eef5 + // a=ssrc: + // a=ssrc: : + // cname 是必须的,msid/mslabel/label 这三个属性都是 WebRTC 自创的,或者说 Google 自创的,可以参考 https://tools.ietf.org/html/draft-ietf-mmusic-msid-17, + // 理解它们三者的关系需要先了解三个概念:RTP stream / MediaStreamTrack / MediaStream : + // 一个 a=ssrc 代表一个 RTP stream ; + // 一个 MediaStreamTrack 通常包含一个或多个 RTP stream,例如一个视频 MediaStreamTrack 中通常包含两个 RTP stream,一个用于常规传输,一个用于 nack 重传; + // 一个 MediaStream 通常包含一个或多个 MediaStreamTrack ,例如 simulcast 场景下,一个 MediaStream 通常会包含三个不同编码质量的 MediaStreamTrack ; + // 这种标记方式并不被 Firefox 认可,在 Firefox 生成的 SDP 中一个 a=ssrc 通常只有一行,例如: + // a=ssrc:3245185839 cname:Cx4i/VTR51etgjT7 uint32_t ssrc; std::string attribute; std::string attribute_value; void parse(const std::string &str) override; std::string toString() const override; - const char* getKey() const override { return "ssrc";} + const char *getKey() const override { return "ssrc"; } }; class SdpAttrSSRCGroup : public SdpItem { public: - //a=ssrc-group 定义参考 RFC 5576(https://tools.ietf.org/html/rfc5576) ,用于描述多个 ssrc 之间的关联,常见的有两种: - //a=ssrc-group:FID 2430709021 3715850271 - // FID (Flow Identification) 最初用在 FEC 的关联中,WebRTC 中通常用于关联一组常规 RTP stream 和 重传 RTP stream 。 - //a=ssrc-group:SIM 360918977 360918978 360918980 - // 在 Chrome 独有的 SDP munging 风格的 simulcast 中使用,将三组编码质量由低到高的 MediaStreamTrack 关联在一起。 - std::string type{"FID"}; + // a=ssrc-group 定义参考 RFC 5576(https://tools.ietf.org/html/rfc5576) ,用于描述多个 ssrc 之间的关联,常见的有两种: + // a=ssrc-group:FID 2430709021 3715850271 + // FID (Flow Identification) 最初用在 FEC 的关联中,WebRTC 中通常用于关联一组常规 RTP stream 和 重传 RTP stream 。 + // a=ssrc-group:SIM 360918977 360918978 360918980 + // 在 Chrome 独有的 SDP munging 风格的 simulcast 中使用,将三组编码质量由低到高的 MediaStreamTrack 关联在一起。 + std::string type { "FID" }; std::vector ssrcs; bool isFID() const { return type == "FID"; } bool isSIM() const { return type == "SIM"; } void parse(const std::string &str) override; std::string toString() const override; - const char* getKey() const override { return "ssrc-group";} + const char *getKey() const override { return "ssrc-group"; } }; class SdpAttrSctpMap : public SdpItem { public: - //https://tools.ietf.org/html/draft-ietf-mmusic-sctp-sdp-05 - //a=sctpmap:5000 webrtc-datachannel 1024 - //a=sctpmap: sctpmap-number media-subtypes [streams] + // https://tools.ietf.org/html/draft-ietf-mmusic-sctp-sdp-05 + // a=sctpmap:5000 webrtc-datachannel 1024 + // a=sctpmap: sctpmap-number media-subtypes [streams] uint16_t port = 0; std::string subtypes; uint32_t streams = 0; void parse(const std::string &str) override; std::string toString() const override; - const char* getKey() const override { return "sctpmap";} + const char *getKey() const override { return "sctpmap"; } bool empty() const { return port == 0 && subtypes.empty() && streams == 0; } }; class SdpAttrCandidate : public SdpItem { public: using Ptr = std::shared_ptr; - //https://tools.ietf.org/html/rfc5245 - //15.1. "candidate" Attribute - //a=candidate:4 1 udp 2 192.168.1.7 58107 typ host - //a=candidate:
typ + // https://tools.ietf.org/html/rfc5245 + // 15.1. "candidate" Attribute + // a=candidate:4 1 udp 2 192.168.1.7 58107 typ host + // a=candidate:
typ std::string foundation; - //传输媒体的类型,1代表RTP;2代表 RTCP。 + // 传输媒体的类型,1代表RTP;2代表 RTCP。 uint32_t component; - std::string transport {"udp"}; + std::string transport { "udp" }; uint32_t priority; std::string address; uint16_t port; std::string type; - std::vector > arr; + std::vector> arr; void parse(const std::string &str) override; std::string toString() const override; - const char* getKey() const override { return "candidate";} + const char *getKey() const override { return "candidate"; } }; -class SdpAttrMsid : public SdpItem{ +class SdpAttrMsid : public SdpItem { public: - const char* getKey() const override { return "msid";} + const char *getKey() const override { return "msid"; } }; -class SdpAttrExtmapAllowMixed : public SdpItem{ +class SdpAttrExtmapAllowMixed : public SdpItem { public: - const char* getKey() const override { return "extmap-allow-mixed";} + const char *getKey() const override { return "extmap-allow-mixed"; } }; -class SdpAttrSimulcast : public SdpItem{ +class SdpAttrSimulcast : public SdpItem { public: - //https://www.meetecho.com/blog/simulcast-janus-ssrc/ - //https://tools.ietf.org/html/draft-ietf-mmusic-sdp-simulcast-14 - const char* getKey() const override { return "simulcast";} + // https://www.meetecho.com/blog/simulcast-janus-ssrc/ + // https://tools.ietf.org/html/draft-ietf-mmusic-sdp-simulcast-14 + const char *getKey() const override { return "simulcast"; } void parse(const std::string &str) override; std::string toString() const override; bool empty() const { return rids.empty(); } @@ -483,11 +470,11 @@ public: std::vector rids; }; -class SdpAttrRid : public SdpItem{ +class SdpAttrRid : public SdpItem { public: void parse(const std::string &str) override; std::string toString() const override; - const char* getKey() const override { return "rid";} + const char *getKey() const override { return "rid"; } std::string direction; std::string rid; }; @@ -507,8 +494,8 @@ public: RtpDirection getDirection() const; - template - cls getItemClass(char key, const char *attr_key = nullptr) const{ + template + cls getItemClass(char key, const char *attr_key = nullptr) const { auto item = std::dynamic_pointer_cast(getItem(key, attr_key)); if (!item) { return cls(); @@ -516,7 +503,7 @@ public: return *item; } - std::string getStringItem(char key, const char *attr_key = nullptr) const{ + std::string getStringItem(char key, const char *attr_key = nullptr) const { auto item = getItem(key, attr_key); if (!item) { return ""; @@ -526,7 +513,7 @@ public: SdpItem::Ptr getItem(char key, const char *attr_key = nullptr) const; - template + template std::vector getAllItem(char key_c, const char *attr_key = nullptr) const { std::vector ret; std::string key(1, key_c); @@ -555,7 +542,7 @@ private: std::vector items; }; -class RtcSessionSdp : public RtcSdpBase{ +class RtcSessionSdp : public RtcSdpBase { public: using Ptr = std::shared_ptr; int getVersion() const; @@ -572,7 +559,7 @@ public: std::string getTimeZone() const; std::string getEncryptKey() const; std::string getRepeatTimes() const; - + std::vector medias; void parse(const std::string &str); std::string toString() const override; @@ -580,45 +567,45 @@ public: ////////////////////////////////////////////////////////////////// -//ssrc相关信息 -class RtcSSRC{ +// ssrc相关信息 +class RtcSSRC { public: - uint32_t ssrc {0}; - uint32_t rtx_ssrc {0}; + uint32_t ssrc { 0 }; + uint32_t rtx_ssrc { 0 }; std::string cname; std::string msid; std::string mslabel; std::string label; - bool empty() const {return ssrc == 0 && cname.empty();} + bool empty() const { return ssrc == 0 && cname.empty(); } }; -//rtc传输编码方案 -class RtcCodecPlan{ +// rtc传输编码方案 +class RtcCodecPlan { public: using Ptr = std::shared_ptr; uint8_t pt; std::string codec; uint32_t sample_rate; - //音频时有效 + // 音频时有效 uint32_t channel = 0; - //rtcp反馈 + // rtcp反馈 std::set rtcp_fb; - std::map fmtp; + std::map fmtp; std::string getFmtp(const char *key) const; }; -//rtc 媒体描述 -class RtcMedia{ +// rtc 媒体描述 +class RtcMedia { public: - TrackType type{TrackType::TrackInvalid}; + TrackType type { TrackType::TrackInvalid }; std::string mid; - uint16_t port{0}; + uint16_t port { 0 }; SdpConnection addr; SdpBandwidth bandwidth; std::string proto; - RtpDirection direction{RtpDirection::invalid}; + RtpDirection direction { RtpDirection::invalid }; std::vector plan; //////// rtp //////// @@ -629,20 +616,20 @@ public: std::vector rtp_rids; //////// rtcp //////// - bool rtcp_mux{false}; - bool rtcp_rsize{false}; + bool rtcp_mux { false }; + bool rtcp_rsize { false }; SdpAttrRtcp rtcp_addr; //////// ice //////// - bool ice_trickle{false}; - bool ice_lite{false}; - bool ice_renomination{false}; + bool ice_trickle { false }; + bool ice_lite { false }; + bool ice_renomination { false }; std::string ice_ufrag; std::string ice_pwd; std::vector candidate; //////// dtls //////// - DtlsRole role{DtlsRole::invalid}; + DtlsRole role { DtlsRole::invalid }; SdpAttrFingerprint fingerprint; //////// extmap //////// @@ -650,7 +637,7 @@ public: //////// sctp //////////// SdpAttrSctpMap sctpmap; - uint32_t sctp_port{0}; + uint32_t sctp_port { 0 }; void checkValid() const; const RtcCodecPlan *getPlan(uint8_t pt) const; @@ -679,7 +666,7 @@ public: void checkValid() const; std::string toString() const; std::string toRtspSdp() const; - const RtcMedia *getMedia(TrackType type) const; + const RtcMedia *getMedia(TrackType type) const; bool supportRtcpFb(const std::string &name, TrackType type = TrackType::TrackVideo) const; bool supportSimulcast() const; bool isOnlyDatachannel() const; @@ -705,7 +692,7 @@ public: std::string ice_ufrag; std::string ice_pwd; - RtpDirection direction{RtpDirection::invalid}; + RtpDirection direction { RtpDirection::invalid }; SdpAttrFingerprint fingerprint; std::set rtcp_fb; @@ -752,6 +739,6 @@ private: ~SdpConst() = delete; }; -}// namespace mediakit +} // namespace mediakit -#endif //ZLMEDIAKIT_SDP_H +#endif // ZLMEDIAKIT_SDP_H From 208f57e2cd69f58c948b58231f376f58c6861bde Mon Sep 17 00:00:00 2001 From: Jacob Su Date: Sun, 24 Mar 2024 17:18:18 +0800 Subject: [PATCH 26/60] Fix macOS compile error by rename version.h -> ZLMVersion.h (#3411 #3410) --- CMakeLists.txt | 6 +++--- version.h.ini => ZLMVersion.h.ini | 0 server/WebApi.cpp | 2 +- server/main.cpp | 2 +- src/Common/macros.cpp | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) rename version.h.ini => ZLMVersion.h.ini (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index d951bc3b..1adefab1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -141,8 +141,8 @@ if(GIT_FOUND) endif() configure_file( - ${CMAKE_CURRENT_SOURCE_DIR}/version.h.ini - ${CMAKE_CURRENT_BINARY_DIR}/version.h + ${CMAKE_CURRENT_SOURCE_DIR}/ZLMVersion.h.ini + ${CMAKE_CURRENT_BINARY_DIR}/ZLMVersion.h @ONLY) message(STATUS "Git version is ${BRANCH_NAME} ${COMMIT_HASH}/${COMMIT_TIME} ${BUILD_TIME}") @@ -537,4 +537,4 @@ file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/default.pem" DESTINATION ${EXECUTABLE_OUT # Copy the default background image used by VideoStack when there is no video stream if (ENABLE_FFMPEG AND ENABLE_X264) file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/conf/novideo.yuv" DESTINATION ${EXECUTABLE_OUTPUT_PATH}) -endif () \ No newline at end of file +endif () diff --git a/version.h.ini b/ZLMVersion.h.ini similarity index 100% rename from version.h.ini rename to ZLMVersion.h.ini diff --git a/server/WebApi.cpp b/server/WebApi.cpp index 5f64e073..edbf90bc 100755 --- a/server/WebApi.cpp +++ b/server/WebApi.cpp @@ -59,7 +59,7 @@ #endif #if defined(ENABLE_VERSION) -#include "version.h" +#include "ZLMVersion.h" #endif #if defined(ENABLE_X264) && defined (ENABLE_FFMPEG) diff --git a/server/main.cpp b/server/main.cpp index c25a89fb..8449f102 100644 --- a/server/main.cpp +++ b/server/main.cpp @@ -38,7 +38,7 @@ #endif #if defined(ENABLE_VERSION) -#include "version.h" +#include "ZLMVersion.h" #endif #if !defined(_WIN32) diff --git a/src/Common/macros.cpp b/src/Common/macros.cpp index 3cb95c7b..5e403b6a 100644 --- a/src/Common/macros.cpp +++ b/src/Common/macros.cpp @@ -14,7 +14,7 @@ using namespace toolkit; #if defined(ENABLE_VERSION) -#include "version.h" +#include "ZLMVersion.h" #endif extern "C" { @@ -44,4 +44,4 @@ const char kServerName[] = "ZLMediaKit-8.0(build in " __DATE__ " " __TIME__ ")" const char kServerName[] = "ZLMediaKit(git hash:" COMMIT_HASH "/" COMMIT_TIME ",branch:" BRANCH_NAME ",build time:" BUILD_TIME ")"; #endif -}//namespace mediakit \ No newline at end of file +}//namespace mediakit From 3e13e69724a116c7998258c0cd0839699df85a82 Mon Sep 17 00:00:00 2001 From: xiongziliang <771730766@qq.com> Date: Sun, 24 Mar 2024 22:01:56 +0800 Subject: [PATCH 27/60] BufFix: avoid may change data in splitter --- src/Http/HttpRequestSplitter.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Http/HttpRequestSplitter.cpp b/src/Http/HttpRequestSplitter.cpp index 32ab476f..9cc4cfcb 100644 --- a/src/Http/HttpRequestSplitter.cpp +++ b/src/Http/HttpRequestSplitter.cpp @@ -65,18 +65,18 @@ void HttpRequestSplitter::input(const char *data,size_t len) { _content_len = onRecvHeader(header_ptr, header_size); } - if(_remain_data_size <= 0){ - //没有剩余数据,清空缓存 - _remain_data.clear(); - return; - } - /* * 恢复末尾字节 * 移动到这来,目的是防止HttpRequestSplitter::reset()导致内存失效 */ tail_ref = tail_tmp; + if(_remain_data_size <= 0){ + //没有剩余数据,清空缓存 + _remain_data.clear(); + return; + } + if(_content_len == 0){ //尚未找到http头,缓存定位到剩余数据部分 _remain_data.assign(ptr,_remain_data_size); From 861be27ef89e0fac562324a7765f136d983131a6 Mon Sep 17 00:00:00 2001 From: ljx0305 Date: Tue, 26 Mar 2024 15:05:14 +0800 Subject: [PATCH 28/60] Fix compilation error issues (#3412) --- server/WebApi.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/WebApi.cpp b/server/WebApi.cpp index edbf90bc..29fc773d 100755 --- a/server/WebApi.cpp +++ b/server/WebApi.cpp @@ -1918,7 +1918,7 @@ void installWebApi() { api_regist("/index/api/stack/start", [](API_ARGS_JSON_ASYNC) { CHECK_SECRET(); - auto ret = VideoStackManager::Instance().startVideoStack(allArgs.getArgs()); + auto ret = VideoStackManager::Instance().startVideoStack(allArgs.args()); val["code"] = ret; val["msg"] = ret ? "failed" : "success"; invoker(200, headerOut, val.toStyledString()); From ecc05dae282d4b5b1d72e8b1cac8caffa70efa7e Mon Sep 17 00:00:00 2001 From: xia-chu <771730766@qq.com> Date: Sat, 30 Mar 2024 14:04:32 +0800 Subject: [PATCH 29/60] BugFix: fix the issue where modifying the default secret resulted in HTTP api authentication failures --- server/main.cpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/server/main.cpp b/server/main.cpp index 8449f102..f418f2f8 100644 --- a/server/main.cpp +++ b/server/main.cpp @@ -258,6 +258,15 @@ int start_main(int argc,char *argv[]) { //加载配置文件,如果配置文件不存在就创建一个 loadIniConfig(g_ini_file.data()); + auto &secret = mINI::Instance()[API::kSecret]; + if (secret == "035c73f7-bb6b-4889-a715-d9eb2d1925cc" || secret.empty()) { + // 使用默认secret被禁止启动 + secret = makeRandStr(32, true); + mINI::Instance().dumpFile(g_ini_file); + WarnL << "The " << API::kSecret << " is invalid, modified it to: " << secret + << ", saved config file: " << g_ini_file; + } + if (!File::is_dir(ssl_file)) { // 不是文件夹,加载证书,证书包含公钥和私钥 SSL_Initor::Instance().loadCertificate(ssl_file.data()); @@ -352,14 +361,6 @@ int start_main(int argc,char *argv[]) { InfoL << "已启动http hook 接口"; try { - auto &secret = mINI::Instance()[API::kSecret]; - if (secret == "035c73f7-bb6b-4889-a715-d9eb2d1925cc" || secret.empty()) { - // 使用默认secret被禁止启动 - secret = makeRandStr(32, true); - mINI::Instance().dumpFile(g_ini_file); - WarnL << "The " << API::kSecret << " is invalid, modified it to: " << secret - << ", saved config file: " << g_ini_file; - } //rtsp服务器,端口默认554 if (rtspPort) { rtspSrv->start(rtspPort); } //rtsps服务器,端口默认322 From 390c3740869fb9d1164719cc6ed9967183fa9d8a Mon Sep 17 00:00:00 2001 From: xia-chu <771730766@qq.com> Date: Sat, 30 Mar 2024 14:41:20 +0800 Subject: [PATCH 30/60] Optimize the code 1. change param_strs to params 2. move params from MediaInfo to MediaTuple 3. passing MediaTuple as a parameter for some functions --- api/source/mk_events_objects.cpp | 2 +- server/WebHook.cpp | 6 +++--- src/Common/MediaSource.cpp | 9 +++++---- src/Common/MediaSource.h | 1 - src/Record/HlsMakerImp.cpp | 6 ++---- src/Record/HlsMakerImp.h | 5 +---- src/Record/HlsRecorder.h | 2 +- src/Record/MP4Recorder.cpp | 6 ++---- src/Record/MP4Recorder.h | 2 +- src/Record/Recorder.cpp | 6 ++---- src/Record/Recorder.h | 1 + srt/SrtTransportImp.cpp | 12 ++++++------ tests/test_server.cpp | 12 ++++++------ 13 files changed, 31 insertions(+), 39 deletions(-) diff --git a/api/source/mk_events_objects.cpp b/api/source/mk_events_objects.cpp index c30fd0cd..3f5bf711 100644 --- a/api/source/mk_events_objects.cpp +++ b/api/source/mk_events_objects.cpp @@ -130,7 +130,7 @@ API_EXPORT const char* API_CALL mk_parser_get_content(const mk_parser ctx, size_ API_EXPORT const char* API_CALL mk_media_info_get_params(const mk_media_info ctx){ assert(ctx); MediaInfo *info = (MediaInfo *)ctx; - return info->param_strs.c_str(); + return info->params.c_str(); } API_EXPORT const char* API_CALL mk_media_info_get_schema(const mk_media_info ctx){ diff --git a/server/WebHook.cpp b/server/WebHook.cpp index aa99a94c..aacee038 100755 --- a/server/WebHook.cpp +++ b/server/WebHook.cpp @@ -225,7 +225,7 @@ static ArgsType make_json(const MediaInfo &args) { ArgsType body; body["schema"] = args.schema; dumpMediaTuple(args, body); - body["params"] = args.param_strs; + body["params"] = args.params; return body; } @@ -286,7 +286,7 @@ static string getPullUrl(const string &origin_fmt, const MediaInfo &info) { return ""; } // 告知源站这是来自边沿站的拉流请求,如果未找到流请立即返回拉流失败 - return string(url) + '?' + kEdgeServerParam + '&' + VHOST_KEY + '=' + info.vhost + '&' + info.param_strs; + return string(url) + '?' + kEdgeServerParam + '&' + VHOST_KEY + '=' + info.vhost + '&' + info.params; } static void pullStreamFromOrigin(const vector &urls, size_t index, size_t failed_cnt, const MediaInfo &args, const function &closePlayer) { @@ -498,7 +498,7 @@ void installWebHook() { return; } - if (start_with(args.param_strs, kEdgeServerParam)) { + if (start_with(args.params, kEdgeServerParam)) { // 源站收到来自边沿站的溯源请求,流不存在时立即返回拉流失败 closePlayer(); return; diff --git a/src/Common/MediaSource.cpp b/src/Common/MediaSource.cpp index 184550e0..96ae22f5 100644 --- a/src/Common/MediaSource.cpp +++ b/src/Common/MediaSource.cpp @@ -583,7 +583,7 @@ void MediaInfo::parse(const std::string &url_in){ auto url = url_in; auto pos = url.find("?"); if (pos != string::npos) { - param_strs = url.substr(pos + 1); + params = url.substr(pos + 1); url.erase(pos); } @@ -616,9 +616,10 @@ void MediaInfo::parse(const std::string &url_in){ stream = stream_id; } - auto params = Parser::parseArgs(param_strs); - if (params.find(VHOST_KEY) != params.end()) { - vhost = params[VHOST_KEY]; + auto kv = Parser::parseArgs(params); + auto it = kv.find(VHOST_KEY); + if (it != kv.end()) { + vhost = it->second; } GET_CONFIG(bool, enableVhost, General::kEnableVhost); diff --git a/src/Common/MediaSource.h b/src/Common/MediaSource.h index b156954c..49f16dd4 100644 --- a/src/Common/MediaSource.h +++ b/src/Common/MediaSource.h @@ -299,7 +299,6 @@ public: std::string full_url; std::string schema; std::string host; - std::string param_strs; }; bool equalMediaTuple(const MediaTuple& a, const MediaTuple& b); diff --git a/src/Record/HlsMakerImp.cpp b/src/Record/HlsMakerImp.cpp index d62c4398..cbd19fb7 100644 --- a/src/Record/HlsMakerImp.cpp +++ b/src/Record/HlsMakerImp.cpp @@ -195,10 +195,8 @@ std::shared_ptr HlsMakerImp::makeFile(const string &file, bool setbuf) { return ret; } -void HlsMakerImp::setMediaSource(const string &vhost, const string &app, const string &stream_id) { - _info.app = app; - _info.stream = stream_id; - _info.vhost = vhost; +void HlsMakerImp::setMediaSource(const MediaTuple& tuple) { + static_cast(_info) = tuple; _media_src = std::make_shared(isFmp4() ? HLS_FMP4_SCHEMA : HLS_SCHEMA, _info); } diff --git a/src/Record/HlsMakerImp.h b/src/Record/HlsMakerImp.h index b3bf77b9..1485e8d3 100644 --- a/src/Record/HlsMakerImp.h +++ b/src/Record/HlsMakerImp.h @@ -27,11 +27,8 @@ public: /** * 设置媒体信息 - * @param vhost 虚拟主机 - * @param app 应用名 - * @param stream_id 流id */ - void setMediaSource(const std::string &vhost, const std::string &app, const std::string &stream_id); + void setMediaSource(const MediaTuple& tuple); /** * 获取MediaSource diff --git a/src/Record/HlsRecorder.h b/src/Record/HlsRecorder.h index f6e84bfb..26894ccf 100644 --- a/src/Record/HlsRecorder.h +++ b/src/Record/HlsRecorder.h @@ -34,7 +34,7 @@ public: } void setMediaSource(const MediaTuple& tuple) { - _hls->setMediaSource(tuple.vhost, tuple.app, tuple.stream); + _hls->setMediaSource(tuple); } void setListener(const std::weak_ptr &listener) { diff --git a/src/Record/MP4Recorder.cpp b/src/Record/MP4Recorder.cpp index c8cee016..4e40bab3 100644 --- a/src/Record/MP4Recorder.cpp +++ b/src/Record/MP4Recorder.cpp @@ -22,12 +22,10 @@ using namespace toolkit; namespace mediakit { -MP4Recorder::MP4Recorder(const string &path, const string &vhost, const string &app, const string &stream_id, size_t max_second) { +MP4Recorder::MP4Recorder(const MediaTuple &tuple, const string &path, size_t max_second) { _folder_path = path; /////record 业务逻辑////// - _info.app = app; - _info.stream = stream_id; - _info.vhost = vhost; + static_cast(_info) = tuple; _info.folder = path; GET_CONFIG(uint32_t, s_max_second, Protocol::kMP4MaxSecond); _max_second = max_second ? max_second : s_max_second; diff --git a/src/Record/MP4Recorder.h b/src/Record/MP4Recorder.h index 7b9deb65..e1258029 100644 --- a/src/Record/MP4Recorder.h +++ b/src/Record/MP4Recorder.h @@ -26,7 +26,7 @@ class MP4Recorder final : public MediaSinkInterface { public: using Ptr = std::shared_ptr; - MP4Recorder(const std::string &path, const std::string &vhost, const std::string &app, const std::string &stream_id, size_t max_second); + MP4Recorder(const MediaTuple &tuple, const std::string &path, size_t max_second); ~MP4Recorder() override; /** diff --git a/src/Record/Recorder.cpp b/src/Record/Recorder.cpp index 2d67fd16..51b9e031 100644 --- a/src/Record/Recorder.cpp +++ b/src/Record/Recorder.cpp @@ -68,8 +68,7 @@ string Recorder::getRecordPath(Recorder::type type, const MediaTuple& tuple, con } return File::absolutePath(m3u8FilePath, hlsPath); } - default: - return ""; + default: return ""; } } @@ -85,13 +84,12 @@ std::shared_ptr Recorder::createRecorder(type type, const Me #else throw std::invalid_argument("hls相关功能未打开,请开启ENABLE_HLS宏后编译再测试"); #endif - } case Recorder::type_mp4: { #if defined(ENABLE_MP4) auto path = Recorder::getRecordPath(type, tuple, option.mp4_save_path); - return std::make_shared(path, tuple.vhost, tuple.app, tuple.stream, option.mp4_max_second); + return std::make_shared(tuple, path, option.mp4_max_second); #else throw std::invalid_argument("mp4相关功能未打开,请开启ENABLE_MP4宏后编译再测试"); #endif diff --git a/src/Record/Recorder.h b/src/Record/Recorder.h index 57f439cd..ea924618 100644 --- a/src/Record/Recorder.h +++ b/src/Record/Recorder.h @@ -22,6 +22,7 @@ struct MediaTuple { std::string vhost; std::string app; std::string stream; + std::string params; std::string shortUrl() const { return vhost + '/' + app + '/' + stream; } diff --git a/srt/SrtTransportImp.cpp b/srt/SrtTransportImp.cpp index 8f818655..60c0b59b 100644 --- a/srt/SrtTransportImp.cpp +++ b/srt/SrtTransportImp.cpp @@ -63,8 +63,8 @@ void SrtTransportImp::onHandShakeFinished(std::string &streamid, struct sockaddr return; } - auto params = Parser::parseArgs(_media_info.param_strs); - if (params["m"] == "publish") { + auto kv = Parser::parseArgs(_media_info.params); + if (kv["m"] == "publish") { _is_pusher = true; _decoder = DecoderImp::createDecoder(DecoderImp::decoder_ts, this); emitOnPublish(); @@ -98,10 +98,10 @@ bool SrtTransportImp::parseStreamid(std::string &streamid) { app = tmps[0]; stream_name = tmps[1]; } else { - if (_media_info.param_strs.empty()) { - _media_info.param_strs = it.first + "=" + it.second; + if (_media_info.params.empty()) { + _media_info.params = it.first + "=" + it.second; } else { - _media_info.param_strs += "&" + it.first + "=" + it.second; + _media_info.params += "&" + it.first + "=" + it.second; } } } @@ -118,7 +118,7 @@ bool SrtTransportImp::parseStreamid(std::string &streamid) { _media_info.app = app; _media_info.stream = stream_name; - TraceL << " mediainfo=" << _media_info.shortUrl() << " params=" << _media_info.param_strs; + TraceL << " mediainfo=" << _media_info.shortUrl() << " params=" << _media_info.params; return true; } diff --git a/tests/test_server.cpp b/tests/test_server.cpp index d94ccbeb..0b3c93ef 100644 --- a/tests/test_server.cpp +++ b/tests/test_server.cpp @@ -90,7 +90,7 @@ void initEventListener() { static onceToken s_token([]() { //监听kBroadcastOnGetRtspRealm事件决定rtsp链接是否需要鉴权(传统的rtsp鉴权方案)才能访问 NoticeCenter::Instance().addListener(nullptr, Broadcast::kBroadcastOnGetRtspRealm, [](BroadcastOnGetRtspRealmArgs) { - DebugL << "RTSP是否需要鉴权事件:" << args.getUrl() << " " << args.param_strs; + DebugL << "RTSP是否需要鉴权事件:" << args.getUrl() << " " << args.params; if (string("1") == args.stream) { // live/1需要认证 //该流需要认证,并且设置realm @@ -104,7 +104,7 @@ void initEventListener() { //监听kBroadcastOnRtspAuth事件返回正确的rtsp鉴权用户密码 NoticeCenter::Instance().addListener(nullptr, Broadcast::kBroadcastOnRtspAuth, [](BroadcastOnRtspAuthArgs) { - DebugL << "RTSP播放鉴权:" << args.getUrl() << " " << args.param_strs; + DebugL << "RTSP播放鉴权:" << args.getUrl() << " " << args.params; DebugL << "RTSP用户:" << user_name << (must_no_encrypt ? " Base64" : " MD5") << " 方式登录"; string user = user_name; //假设我们异步读取数据库 @@ -134,14 +134,14 @@ void initEventListener() { //监听rtsp/rtmp推流事件,返回结果告知是否有推流权限 NoticeCenter::Instance().addListener(nullptr, Broadcast::kBroadcastMediaPublish, [](BroadcastMediaPublishArgs) { - DebugL << "推流鉴权:" << args.getUrl() << " " << args.param_strs; + DebugL << "推流鉴权:" << args.getUrl() << " " << args.params; invoker("", ProtocolOption());//鉴权成功 //invoker("this is auth failed message");//鉴权失败 }); //监听rtsp/rtsps/rtmp/http-flv播放事件,返回结果告知是否有播放权限(rtsp通过kBroadcastOnRtspAuth或此事件都可以实现鉴权) NoticeCenter::Instance().addListener(nullptr, Broadcast::kBroadcastMediaPlayed, [](BroadcastMediaPlayedArgs) { - DebugL << "播放鉴权:" << args.getUrl() << " " << args.param_strs; + DebugL << "播放鉴权:" << args.getUrl() << " " << args.params; invoker("");//鉴权成功 //invoker("this is auth failed message");//鉴权失败 }); @@ -183,13 +183,13 @@ void initEventListener() { * 你可以在这个事件触发时再去拉流,这样就可以实现按需拉流 * 拉流成功后,ZLMediaKit会把其立即转发给播放器(最大等待时间约为5秒,如果5秒都未拉流成功,播放器会播放失败) */ - DebugL << "未找到流事件:" << args.getUrl() << " " << args.param_strs; + DebugL << "未找到流事件:" << args.getUrl() << " " << args.params; }); //监听播放或推流结束时消耗流量事件 NoticeCenter::Instance().addListener(nullptr, Broadcast::kBroadcastFlowReport, [](BroadcastFlowReportArgs) { - DebugL << "播放器(推流器)断开连接事件:" << args.getUrl() << " " << args.param_strs << "\r\n使用流量:" << totalBytes << " bytes,连接时长:" << totalDuration << "秒"; + DebugL << "播放器(推流器)断开连接事件:" << args.getUrl() << " " << args.params << "\r\n使用流量:" << totalBytes << " bytes,连接时长:" << totalDuration << "秒"; }); From 0602cc0c0b23e8ee816616633562dca9c5faa960 Mon Sep 17 00:00:00 2001 From: xia-chu <771730766@qq.com> Date: Sat, 30 Mar 2024 14:46:39 +0800 Subject: [PATCH 31/60] Add 'params' field to MediaSource tuple information --- server/WebApi.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/server/WebApi.cpp b/server/WebApi.cpp index 29fc773d..034db359 100755 --- a/server/WebApi.cpp +++ b/server/WebApi.cpp @@ -389,6 +389,7 @@ void dumpMediaTuple(const MediaTuple &tuple, Json::Value& item) { item[VHOST_KEY] = tuple.vhost; item["app"] = tuple.app; item["stream"] = tuple.stream; + item["params"] = tuple.params; } Value makeMediaSourceJson(MediaSource &media){ From af3ef996b0ae265e000344e7faf753577f9abf4e Mon Sep 17 00:00:00 2001 From: xia-chu <771730766@qq.com> Date: Sat, 30 Mar 2024 14:59:28 +0800 Subject: [PATCH 32/60] Avoid build warnings in the main code --- CMakeLists.txt | 2 +- api/include/mk_events_objects.h | 10 +++++----- api/source/mk_events_objects.cpp | 10 +++++----- server/WebApi.cpp | 4 ++-- src/Common/MediaSource.cpp | 2 +- src/Record/MP4Reader.cpp | 2 +- src/Record/MP4Recorder.cpp | 2 +- tests/test_flv.cpp | 2 +- 8 files changed, 17 insertions(+), 17 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1adefab1..72a4544e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -191,7 +191,7 @@ if(UNIX) set(COMPILE_OPTIONS_DEFAULT "-fPIC" "-Wall;-Wextra" - "-Wno-unused-function;-Wno-unused-parameter;-Wno-unused-variable" + "-Wno-unused-function;-Wno-unused-parameter;-Wno-unused-variable;-Wno-deprecated-declarations" "-Wno-error=extra;-Wno-error=missing-field-initializers;-Wno-error=type-limits") if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug") diff --git a/api/include/mk_events_objects.h b/api/include/mk_events_objects.h index d44ed666..ddbaf87d 100644 --- a/api/include/mk_events_objects.h +++ b/api/include/mk_events_objects.h @@ -142,11 +142,11 @@ API_EXPORT void API_CALL mk_media_source_find(const char *schema, void *user_data, on_mk_media_source_find_cb cb); -API_EXPORT const mk_media_source API_CALL mk_media_source_find2(const char *schema, - const char *vhost, - const char *app, - const char *stream, - int from_mp4); +API_EXPORT mk_media_source API_CALL mk_media_source_find2(const char *schema, + const char *vhost, + const char *app, + const char *stream, + int from_mp4); //MediaSource::for_each_media() API_EXPORT void API_CALL mk_media_source_for_each(void *user_data, on_mk_media_source_find_cb cb, const char *schema, const char *vhost, const char *app, const char *stream); diff --git a/api/source/mk_events_objects.cpp b/api/source/mk_events_objects.cpp index 3f5bf711..7046f1ab 100644 --- a/api/source/mk_events_objects.cpp +++ b/api/source/mk_events_objects.cpp @@ -278,11 +278,11 @@ API_EXPORT void API_CALL mk_media_source_find(const char *schema, cb(user_data, (mk_media_source)src.get()); } -API_EXPORT const mk_media_source API_CALL mk_media_source_find2(const char *schema, - const char *vhost, - const char *app, - const char *stream, - int from_mp4) { +API_EXPORT mk_media_source API_CALL mk_media_source_find2(const char *schema, + const char *vhost, + const char *app, + const char *stream, + int from_mp4) { assert(schema && vhost && app && stream); auto src = MediaSource::find(schema, vhost, app, stream, from_mp4); return (mk_media_source)src.get(); diff --git a/server/WebApi.cpp b/server/WebApi.cpp index 034db359..faac203d 100755 --- a/server/WebApi.cpp +++ b/server/WebApi.cpp @@ -1564,7 +1564,7 @@ void installWebApi() { api_regist("/index/api/deleteRecordDirectory", [](API_ARGS_MAP) { CHECK_SECRET(); CHECK_ARGS("vhost", "app", "stream", "period"); - auto tuple = MediaTuple{allArgs["vhost"], allArgs["app"], allArgs["stream"]}; + auto tuple = MediaTuple{allArgs["vhost"], allArgs["app"], allArgs["stream"], ""}; auto record_path = Recorder::getRecordPath(Recorder::type_mp4, tuple, allArgs["customized_path"]); auto period = allArgs["period"]; record_path = record_path + period + "/"; @@ -1603,7 +1603,7 @@ void installWebApi() { api_regist("/index/api/getMP4RecordFile", [](API_ARGS_MAP){ CHECK_SECRET(); CHECK_ARGS("vhost", "app", "stream"); - auto tuple = MediaTuple{allArgs["vhost"], allArgs["app"], allArgs["stream"]}; + auto tuple = MediaTuple{allArgs["vhost"], allArgs["app"], allArgs["stream"], ""}; auto record_path = Recorder::getRecordPath(Recorder::type_mp4, tuple, allArgs["customized_path"]); auto period = allArgs["period"]; diff --git a/src/Common/MediaSource.cpp b/src/Common/MediaSource.cpp index 96ae22f5..8de8202a 100644 --- a/src/Common/MediaSource.cpp +++ b/src/Common/MediaSource.cpp @@ -113,7 +113,7 @@ ProtocolOption::ProtocolOption() { ////////////////////////////////////////////////////////////////////////////////////////////////////////////// struct MediaSourceNull : public MediaSource { - MediaSourceNull() : MediaSource("schema", MediaTuple{"vhost", "app", "stream"}) {}; + MediaSourceNull() : MediaSource("schema", MediaTuple{"vhost", "app", "stream", ""}) {}; int readerCount() override { return 0; } }; diff --git a/src/Record/MP4Reader.cpp b/src/Record/MP4Reader.cpp index e70f9fe8..6437fcff 100644 --- a/src/Record/MP4Reader.cpp +++ b/src/Record/MP4Reader.cpp @@ -38,7 +38,7 @@ MP4Reader::MP4Reader(const std::string &vhost, const std::string &app, const std void MP4Reader::setup(const std::string &vhost, const std::string &app, const std::string &stream_id, const std::string &file_path, const ProtocolOption &option, toolkit::EventPoller::Ptr poller) { //读写文件建议放在后台线程 - auto tuple = MediaTuple{vhost, app, stream_id}; + auto tuple = MediaTuple{vhost, app, stream_id, ""}; _poller = poller ? std::move(poller) : WorkThreadPool::Instance().getPoller(); _file_path = file_path; if (_file_path.empty()) { diff --git a/src/Record/MP4Recorder.cpp b/src/Record/MP4Recorder.cpp index 4e40bab3..cd55d30a 100644 --- a/src/Record/MP4Recorder.cpp +++ b/src/Record/MP4Recorder.cpp @@ -118,7 +118,7 @@ bool MP4Recorder::inputFrame(const Frame::Ptr &frame) { //b帧情况下dts时间戳可能回退 _last_dts = MAX(frame->dts(), _last_dts); } - auto duration = 5; // 默认至少一帧5ms + auto duration = 5u; // 默认至少一帧5ms if (frame->dts() > 0 && frame->dts() > _last_dts) { duration = MAX(duration, frame->dts() - _last_dts); } diff --git a/tests/test_flv.cpp b/tests/test_flv.cpp index 49b78865..cf838822 100644 --- a/tests/test_flv.cpp +++ b/tests/test_flv.cpp @@ -27,7 +27,7 @@ using namespace mediakit; class FlvSplitterImp : public FlvSplitter { public: FlvSplitterImp() { - _src = std::make_shared(MediaTuple{DEFAULT_VHOST, "live", "test"}); + _src = std::make_shared(MediaTuple{DEFAULT_VHOST, "live", "test", ""}); } ~FlvSplitterImp() override = default; From 24689fefd1b913678a3aa4145c94a1f9fb68f2d6 Mon Sep 17 00:00:00 2001 From: ljx0305 Date: Mon, 1 Apr 2024 17:31:04 +0800 Subject: [PATCH 33/60] Fix compilation error (#3432) --- server/WebApi.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/WebApi.cpp b/server/WebApi.cpp index faac203d..a1ef1f65 100755 --- a/server/WebApi.cpp +++ b/server/WebApi.cpp @@ -1919,7 +1919,7 @@ void installWebApi() { api_regist("/index/api/stack/start", [](API_ARGS_JSON_ASYNC) { CHECK_SECRET(); - auto ret = VideoStackManager::Instance().startVideoStack(allArgs.args()); + auto ret = VideoStackManager::Instance().startVideoStack(allArgs.args); val["code"] = ret; val["msg"] = ret ? "failed" : "success"; invoker(200, headerOut, val.toStyledString()); From 2159e90f787ff07d27275ae74db4caa159c00dc5 Mon Sep 17 00:00:00 2001 From: xiongziliang <771730766@qq.com> Date: Fri, 5 Apr 2024 22:07:09 +0800 Subject: [PATCH 34/60] Add demo of reading H.264 file and pushing RTSP/RTMP stream --- api/tests/h264_pusher.c | 152 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 api/tests/h264_pusher.c diff --git a/api/tests/h264_pusher.c b/api/tests/h264_pusher.c new file mode 100644 index 00000000..bb87b03b --- /dev/null +++ b/api/tests/h264_pusher.c @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2016-present The ZLMediaKit project authors. All Rights Reserved. + * + * This file is part of ZLMediaKit(https://github.com/ZLMediaKit/ZLMediaKit). + * + * Use of this source code is governed by MIT-like license that can be found in the + * LICENSE file in the root of the source tree. All contributing project authors + * may be found in the AUTHORS file in the root of the source tree. + */ + +#include +#include +#include +#ifdef _WIN32 +#include "windows.h" +#else +#include "unistd.h" +#endif +#include "mk_mediakit.h" + +static int exit_flag = 0; +static void s_on_exit(int sig) { + exit_flag = 1; +} + +static void on_h264_frame(void *user_data, mk_h264_splitter splitter, const char *data, int size) { +#ifdef _WIN32 + Sleep(40); +#else + usleep(40 * 1000); +#endif + static int dts = 0; + mk_frame frame = mk_frame_create(MKCodecH264, dts, dts, data, size, NULL, NULL); + dts += 40; + mk_media_input_frame((mk_media)user_data, frame); + mk_frame_unref(frame); +} + +typedef struct { + mk_pusher pusher; + char *url; +} Context; + +void release_context(void *user_data) { + Context *ptr = (Context *)user_data; + if (ptr->pusher) { + mk_pusher_release(ptr->pusher); + } + free(ptr->url); + free(ptr); + log_info("停止推流"); +} + +void on_push_result(void *user_data, int err_code, const char *err_msg) { + Context *ptr = (Context *)user_data; + if (err_code == 0) { + log_info("推流成功: %s", ptr->url); + } else { + log_warn("推流%s失败: %d(%s)", ptr->url, err_code, err_msg); + } +} + +void on_push_shutdown(void *user_data, int err_code, const char *err_msg) { + Context *ptr = (Context *)user_data; + log_warn("推流%s中断: %d(%s)", ptr->url, err_code, err_msg); +} + +void API_CALL on_regist(void *user_data, mk_media_source sender, int regist) { + Context *ptr = (Context *)user_data; + const char *schema = mk_media_source_get_schema(sender); + if (strcasestr(ptr->url, schema) != ptr->url) { + // 协议匹配失败 + return; + } + + if (!regist) { + // 注销 + if (ptr->pusher) { + mk_pusher_release(ptr->pusher); + ptr->pusher = NULL; + } + } else { + // 注册 + if (!ptr->pusher) { + ptr->pusher = mk_pusher_create_src(sender); + mk_pusher_set_on_result2(ptr->pusher, on_push_result, ptr, NULL); + mk_pusher_set_on_shutdown2(ptr->pusher, on_push_shutdown, ptr, NULL); + // 开始推流 + mk_pusher_publish(ptr->pusher, ptr->url); + } + } +} + +int main(int argc, char *argv[]) { + if (argc < 3) { + log_error("Usage: /path/to/h264/file rtsp_or_rtmp_url"); + return -1; + } + mk_config config = { .ini = NULL, + .ini_is_path = 1, + .log_level = 0, + .log_mask = LOG_CONSOLE, + .log_file_path = NULL, + .log_file_days = 0, + .ssl = NULL, + .ssl_is_path = 1, + .ssl_pwd = NULL, + .thread_num = 0 }; + mk_env_init(&config); + + FILE *fp = fopen(argv[1], "rb"); + if (!fp) { + log_error("打开文件失败!"); + return -1; + } + + mk_media media = mk_media_create("__defaultVhost__", "live", "test", 0, 0, 0); + // h264的codec + codec_args v_args = { 0 }; + mk_track v_track = mk_track_create(MKCodecH264, &v_args); + mk_media_init_track(media, v_track); + mk_media_init_complete(media); + mk_track_unref(v_track); + + Context *ctx = (Context *)malloc(sizeof(Context)); + memset(ctx, 0, sizeof(Context)); + ctx->url = strdup(argv[2]); + + mk_media_set_on_regist2(media, on_regist, ctx, release_context); + + // 创建h264分帧器 + mk_h264_splitter splitter = mk_h264_splitter_create(on_h264_frame, media, 0); + signal(SIGINT, s_on_exit); // 设置退出信号 + signal(SIGTERM, s_on_exit); // 设置退出信号 + + char buf[1024]; + while (!exit_flag) { + int size = fread(buf, 1, sizeof(buf) - 1, fp); + if (size > 0) { + mk_h264_splitter_input_data(splitter, buf, size); + } else { + // 文件读完了,重新开始 + fseek(fp, 0, SEEK_SET); + } + } + + log_info("文件读取完毕"); + mk_h264_splitter_release(splitter); + mk_media_release(media); + fclose(fp); + return 0; +} \ No newline at end of file From 071f008108b1408ca7d3521ace97ebf213b67951 Mon Sep 17 00:00:00 2001 From: lidaofu-hub <61726674+lidaofu-hub@users.noreply.github.com> Date: Fri, 5 Apr 2024 22:09:40 +0800 Subject: [PATCH 35/60] add c api for MediaSource (#3433) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补充MediaSource C API 获取源地址 获取源类型 获取创建时间戳 --------- Co-authored-by: 李道甫 --- api/include/mk_common.h | 4 ++++ api/include/mk_events_objects.h | 10 ++++++++++ api/source/mk_events_objects.cpp | 24 ++++++++++++++++++++++++ api/source/mk_util.cpp | 4 ---- 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/api/include/mk_common.h b/api/include/mk_common.h index 90b4c8a6..b6f480cd 100755 --- a/api/include/mk_common.h +++ b/api/include/mk_common.h @@ -24,6 +24,10 @@ # define API_CALL #endif +#ifndef _WIN32 +#define _strdup strdup +#endif + #if defined(_WIN32) && defined(_MSC_VER) # if !defined(GENERATE_EXPORT) # if defined(MediaKitApi_EXPORTS) diff --git a/api/include/mk_events_objects.h b/api/include/mk_events_objects.h index ddbaf87d..428f9dc6 100644 --- a/api/include/mk_events_objects.h +++ b/api/include/mk_events_objects.h @@ -103,6 +103,16 @@ API_EXPORT int API_CALL mk_media_source_get_track_count(const mk_media_source ct API_EXPORT mk_track API_CALL mk_media_source_get_track(const mk_media_source ctx, int index); // MediaSource::broadcastMessage API_EXPORT int API_CALL mk_media_source_broadcast_msg(const mk_media_source ctx, const char *msg, size_t len); +// MediaSource::getOriginUrl() +API_EXPORT const char* API_CALL mk_media_source_get_origin_url(const mk_media_source ctx); +// MediaSource::getOriginType() +API_EXPORT int API_CALL mk_media_source_get_origin_type(const mk_media_source ctx); +// MediaSource::getCreateStamp() +API_EXPORT uint64_t API_CALL mk_media_source_get_create_stamp(const mk_media_source ctx); +// MediaSource::isRecording() 0:hls,1:MP4 +API_EXPORT int API_CALL mk_media_source_is_recording(const mk_media_source ctx, int type); + + /** * 直播源在ZLMediaKit中被称作为MediaSource, diff --git a/api/source/mk_events_objects.cpp b/api/source/mk_events_objects.cpp index 7046f1ab..758e38a3 100644 --- a/api/source/mk_events_objects.cpp +++ b/api/source/mk_events_objects.cpp @@ -228,6 +228,30 @@ API_EXPORT int API_CALL mk_media_source_broadcast_msg(const mk_media_source ctx, return src->broadcastMessage(any); } +API_EXPORT const char* API_CALL mk_media_source_get_origin_url(const mk_media_source ctx) { + assert(ctx); + MediaSource *src = (MediaSource *)ctx; + return _strdup(src->getOriginUrl().c_str()); +} + +API_EXPORT int API_CALL mk_media_source_get_origin_type(const mk_media_source ctx) { + assert(ctx); + MediaSource *src = (MediaSource *)ctx; + return static_cast(src->getOriginType()); +} + +API_EXPORT uint64_t API_CALL mk_media_source_get_create_stamp(const mk_media_source ctx) { + assert(ctx); + MediaSource *src = (MediaSource *)ctx; + return src->getCreateStamp(); +} + +API_EXPORT int API_CALL mk_media_source_is_recording(const mk_media_source ctx,int type) { + assert(ctx); + MediaSource *src = (MediaSource *)ctx; + return src->isRecording((Recorder::type)type); +} + API_EXPORT int API_CALL mk_media_source_close(const mk_media_source ctx,int force){ assert(ctx); MediaSource *src = (MediaSource *)ctx; diff --git a/api/source/mk_util.cpp b/api/source/mk_util.cpp index 66d0d3b2..eda27162 100644 --- a/api/source/mk_util.cpp +++ b/api/source/mk_util.cpp @@ -21,10 +21,6 @@ using namespace std; using namespace toolkit; using namespace mediakit; -#ifndef _WIN32 -#define _strdup strdup -#endif - API_EXPORT void API_CALL mk_free(void *ptr) { free(ptr); } From edca6622081f08ff209371c2bd1adcbad13a85d1 Mon Sep 17 00:00:00 2001 From: Dw9 Date: Fri, 5 Apr 2024 22:11:50 +0800 Subject: [PATCH 36/60] fix webrtc echo error (#3442) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 遵循着pr #3360 的修改方式解决webrtc echo模式失败的问题 --- webrtc/WebRtcEchoTest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webrtc/WebRtcEchoTest.cpp b/webrtc/WebRtcEchoTest.cpp index 40be2613..cf7527a8 100644 --- a/webrtc/WebRtcEchoTest.cpp +++ b/webrtc/WebRtcEchoTest.cpp @@ -45,7 +45,7 @@ void WebRtcEchoTest::onCheckSdp(SdpType type, RtcSession &sdp) { for (auto &m : sdp.media) { for (auto &ssrc : m.rtp_rtx_ssrc) { if (!ssrc.msid.empty()) { - ssrc.msid = "zlmediakit msid"; + ssrc.msid = "zlmediakit-mslabel zlmediakit-label-" + m.mid; } } } From e6506a96d438622923d6522fe8fc54dfa2848b5f Mon Sep 17 00:00:00 2001 From: Dw9 Date: Mon, 8 Apr 2024 10:16:45 +0800 Subject: [PATCH 37/60] Update h264_pusher.c, fix build issue (#3444) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复win构建失败 strcasestr->strstr --- api/tests/h264_pusher.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/tests/h264_pusher.c b/api/tests/h264_pusher.c index bb87b03b..81a018b0 100644 --- a/api/tests/h264_pusher.c +++ b/api/tests/h264_pusher.c @@ -68,7 +68,7 @@ void on_push_shutdown(void *user_data, int err_code, const char *err_msg) { void API_CALL on_regist(void *user_data, mk_media_source sender, int regist) { Context *ptr = (Context *)user_data; const char *schema = mk_media_source_get_schema(sender); - if (strcasestr(ptr->url, schema) != ptr->url) { + if (strstr(ptr->url, schema) != ptr->url) { // 协议匹配失败 return; } @@ -149,4 +149,4 @@ int main(int argc, char *argv[]) { mk_media_release(media); fclose(fp); return 0; -} \ No newline at end of file +} From 3dcd0ed46399b15cde86d4a77e498f4386b47b16 Mon Sep 17 00:00:00 2001 From: Aven Date: Sat, 13 Apr 2024 20:35:59 +0800 Subject: [PATCH 38/60] Enable video stack individually (#3469) --- CMakeLists.txt | 5 +++-- server/VideoStack.cpp | 2 +- server/VideoStack.h | 2 +- server/WebApi.cpp | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 72a4544e..d4b2519a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,7 +57,8 @@ option(ENABLE_SCTP "Enable SCTP" ON) option(ENABLE_WEBRTC "Enable WebRTC" ON) option(ENABLE_X264 "Enable x264" OFF) option(ENABLE_WEPOLL "Enable wepoll" ON) -option(DISABLE_REPORT "Disable report to report.zlmediakit.com" off) +option(ENABLE_VIDEOSTACK "Enable video stack" OFF) +option(DISABLE_REPORT "Disable report to report.zlmediakit.com" OFF) option(USE_SOLUTION_FOLDERS "Enable solution dir supported" ON) ############################################################################## # 设置socket默认缓冲区大小为256k.如果设置为0则不设置socket的默认缓冲区大小,使用系统内核默认值(设置为0仅对linux有效) @@ -535,6 +536,6 @@ file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/default.pem" DESTINATION ${EXECUTABLE_OUT # 拷贝VideoStack 无视频流时默认填充的背景图片 # Copy the default background image used by VideoStack when there is no video stream -if (ENABLE_FFMPEG AND ENABLE_X264) +if (ENABLE_VIDEOSTACK AND ENABLE_FFMPEG AND ENABLE_X264) file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/conf/novideo.yuv" DESTINATION ${EXECUTABLE_OUTPUT_PATH}) endif () diff --git a/server/VideoStack.cpp b/server/VideoStack.cpp index d5168147..21cb7ff6 100644 --- a/server/VideoStack.cpp +++ b/server/VideoStack.cpp @@ -1,4 +1,4 @@ -#if defined(ENABLE_X264) && defined(ENABLE_FFMPEG) +#if defined(ENABLE_VIDEOSTACK) && defined(ENABLE_X264) && defined(ENABLE_FFMPEG) #include "VideoStack.h" #include "Codec/Transcode.h" #include "Common/Device.h" diff --git a/server/VideoStack.h b/server/VideoStack.h index 99455b40..cc0bfaa8 100644 --- a/server/VideoStack.h +++ b/server/VideoStack.h @@ -1,5 +1,5 @@ #pragma once -#if defined(ENABLE_X264) && defined(ENABLE_FFMPEG) +#if defined(ENABLE_VIDEOSTACK) && defined(ENABLE_X264) && defined(ENABLE_FFMPEG) #include "Codec/Transcode.h" #include "Common/Device.h" #include "Player/MediaPlayer.h" diff --git a/server/WebApi.cpp b/server/WebApi.cpp index a1ef1f65..091db922 100755 --- a/server/WebApi.cpp +++ b/server/WebApi.cpp @@ -62,7 +62,7 @@ #include "ZLMVersion.h" #endif -#if defined(ENABLE_X264) && defined (ENABLE_FFMPEG) +#if defined(ENABLE_VIDEOSTACK) && defined(ENABLE_X264) && defined (ENABLE_FFMPEG) #include "VideoStack.h" #endif @@ -1910,7 +1910,7 @@ void installWebApi() { } }); -#if defined(ENABLE_X264) && defined(ENABLE_FFMPEG) +#if defined(ENABLE_VIDEOSTACK) && defined(ENABLE_X264) && defined(ENABLE_FFMPEG) VideoStackManager::Instance().loadBgImg("novideo.yuv"); NoticeCenter::Instance().addListener(nullptr, Broadcast::kBroadcastStreamNoneReader, [](BroadcastStreamNoneReaderArgs) { auto id = sender.getMediaTuple().stream; From d8cb75d3878919b84e4323fecf1cc791fb2338ae Mon Sep 17 00:00:00 2001 From: xiongguangjie Date: Sat, 13 Apr 2024 20:36:15 +0800 Subject: [PATCH 39/60] Improve url encode and decode code add user pass encode decode methond (#3468) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 去除url转义的冗余代码,添加用户名与密码的转义 --- src/Common/Parser.cpp | 4 +-- src/Common/strCoding.cpp | 60 +++++++++++++++++++++++++--------------- src/Common/strCoding.h | 2 ++ 3 files changed, 41 insertions(+), 25 deletions(-) diff --git a/src/Common/Parser.cpp b/src/Common/Parser.cpp index 7e7de860..47f3c2d8 100644 --- a/src/Common/Parser.cpp +++ b/src/Common/Parser.cpp @@ -294,8 +294,8 @@ void RtspUrl::setup(bool is_ssl, const string &url, const string &user, const st splitUrl(ip, ip, port); _url = std::move(url); - _user = strCoding::UrlDecodeComponent(user); - _passwd = strCoding::UrlDecodeComponent(passwd); + _user = strCoding::UrlDecodeUserOrPass(user); + _passwd = strCoding::UrlDecodeUserOrPass(passwd); _host = std::move(ip); _port = port; _is_ssl = is_ssl; diff --git a/src/Common/strCoding.cpp b/src/Common/strCoding.cpp index 1a0f0236..49a345ef 100644 --- a/src/Common/strCoding.cpp +++ b/src/Common/strCoding.cpp @@ -52,9 +52,7 @@ char HexStrToBin(const char *str) { } return (high << 4) | low; } - -string strCoding::UrlEncodePath(const string &str) { - const char *dont_escape = "!#&'*+:=?@/._-$,;~()"; +static string UrlEncodeCommon(const string &str,const char* dont_escape){ string out; size_t len = str.size(); for (size_t i = 0; i < len; ++i) { @@ -69,26 +67,7 @@ string strCoding::UrlEncodePath(const string &str) { } return out; } - -string strCoding::UrlEncodeComponent(const string &str) { - const char *dont_escape = "!'()*-._~"; - string out; - size_t len = str.size(); - for (size_t i = 0; i < len; ++i) { - char ch = str[i]; - if (isalnum((uint8_t) ch) || strchr(dont_escape, (uint8_t) ch) != NULL) { - out.push_back(ch); - } else { - char buf[4]; - snprintf(buf, 4, "%%%X%X", (uint8_t) ch >> 4, (uint8_t) ch & 0x0F); - out.append(buf); - } - } - return out; -} - -string strCoding::UrlDecodePath(const string &str) { - const char *dont_unescape = "#$&+,/:;=?@"; +static string UrlDecodeCommon(const string &str,const char* dont_unescape){ string output; size_t i = 0, len = str.length(); while (i < len) { @@ -114,6 +93,36 @@ string strCoding::UrlDecodePath(const string &str) { return output; } +string strCoding::UrlEncodePath(const string &str) { + const char *dont_escape = "!#&'*+:=?@/._-$,;~()"; + return UrlEncodeCommon(str,dont_escape); +} + +string strCoding::UrlEncodeComponent(const string &str) { + const char *dont_escape = "!'()*-._~"; + return UrlEncodeCommon(str,dont_escape); +} + +std::string strCoding::UrlEncodeUserOrPass(const std::string &str) { + // from rfc https://datatracker.ietf.org/doc/html/rfc3986 + // §2.3 Unreserved characters (mark) + //'-', '_', '.', '~' + // §2.2 Reserved characters (reserved) + // '$', '&', '+', ',', '/', ':', ';', '=', '?', '@', + // §3.2.1 + // The RFC allows ';', ':', '&', '=', '+', '$', and ',' in + // userinfo, so we must escape only '@', '/', and '?'. + // The parsing of userinfo treats ':' as special so we must escape + // that too. + const char *dont_escape = "$&+,;=-._~"; + return UrlEncodeCommon(str,dont_escape); +} + +string strCoding::UrlDecodePath(const string &str) { + const char *dont_unescape = "#$&+,/:;=?@"; + return UrlDecodeCommon(str,dont_unescape); +} + std::string strCoding::UrlDecodeComponent(const std::string &str) { string output; size_t i = 0, len = str.length(); @@ -143,6 +152,11 @@ std::string strCoding::UrlDecodeComponent(const std::string &str) { return output; } + +std::string strCoding::UrlDecodeUserOrPass(const std::string &str) { + const char *dont_unescape = ""; + return UrlDecodeCommon(str,dont_unescape); +} ///////////////////////////////windows专用/////////////////////////////////// #if defined(_WIN32) void UnicodeToGB2312(char* pOut, wchar_t uData) diff --git a/src/Common/strCoding.h b/src/Common/strCoding.h index e715e74d..bfddf7ff 100644 --- a/src/Common/strCoding.h +++ b/src/Common/strCoding.h @@ -22,6 +22,8 @@ public: static std::string UrlEncodeComponent(const std::string &str); // url参数 utf8编码 static std::string UrlDecodePath(const std::string &str); //url路径 utf8解码 static std::string UrlDecodeComponent(const std::string &str); // url参数 utf8解码 + static std::string UrlEncodeUserOrPass(const std::string &str); // url中用户名与密码编码 + static std::string UrlDecodeUserOrPass(const std::string &str); // url中用户名与密码解码 #if defined(_WIN32) static std::string UTF8ToGB2312(const std::string &str);//utf_8转为gb2312 static std::string GB2312ToUTF8(const std::string &str); //gb2312 转utf_8 From c341f8ebf6c492e2f0762ddb886c468d741b0ac6 Mon Sep 17 00:00:00 2001 From: renlu Date: Wed, 10 Apr 2024 13:14:53 +0800 Subject: [PATCH 40/60] =?UTF-8?q?startSendRtp=E4=B8=8D=E6=8C=87=E5=AE=9Aus?= =?UTF-8?q?e=5Fps=E6=97=B6=EF=BC=8C=E5=8F=91=E9=80=81=E7=9A=84=E4=B8=BA?= =?UTF-8?q?=E8=A3=B8=E5=8C=85=EF=BC=8C=E4=B8=8D=E5=85=BC=E5=AE=B9=E4=B9=8B?= =?UTF-8?q?=E5=89=8D=E7=9A=84=E6=97=A7=E7=89=88=E6=9C=AC=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/WebApi.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/WebApi.cpp b/server/WebApi.cpp index 091db922..a0690747 100755 --- a/server/WebApi.cpp +++ b/server/WebApi.cpp @@ -1315,7 +1315,7 @@ void installWebApi() { if (!src) { throw ApiRetException("can not find the source stream", API::NotFound); } - auto type = allArgs["type"].as(); + auto type = allArgs["type"].empty() ? (int)MediaSourceEvent::SendRtpArgs::kRtpPS : allArgs["type"].as(); if (!allArgs["use_ps"].empty()) { // 兼容之前的use_ps参数 type = allArgs["use_ps"].as(); @@ -1355,7 +1355,7 @@ void installWebApi() { if (!src) { throw ApiRetException("can not find the source stream", API::NotFound); } - auto type = allArgs["type"].as(); + auto type = allArgs["type"].empty() ? (int)MediaSourceEvent::SendRtpArgs::kRtpPS : allArgs["type"].as(); if (!allArgs["use_ps"].empty()) { // 兼容之前的use_ps参数 type = allArgs["use_ps"].as(); From 4aa330ab822d6b8d46d4de58c201432eb5591554 Mon Sep 17 00:00:00 2001 From: xiongguangjie Date: Tue, 16 Apr 2024 22:34:54 +0800 Subject: [PATCH 41/60] rtp g711 encoder rtp not key pos avoid gop cache useless (#3476) g711 rtp encoder input packet not key pos , avoid gop cache useless, fix the bug --- ext-codec/G711Rtp.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext-codec/G711Rtp.cpp b/ext-codec/G711Rtp.cpp index 16a9c5c2..766d600f 100644 --- a/ext-codec/G711Rtp.cpp +++ b/ext-codec/G711Rtp.cpp @@ -46,7 +46,7 @@ bool G711RtpEncoder::inputFrame(const Frame::Ptr &frame) { const size_t rtp_size = max_size; n++; stamp += _pkt_dur_ms; - RtpCodec::inputRtp(getRtpInfo().makeRtp(TrackAudio, ptr, rtp_size, mark, stamp), true); + RtpCodec::inputRtp(getRtpInfo().makeRtp(TrackAudio, ptr, rtp_size, mark, stamp), false); ptr += rtp_size; remain_size -= rtp_size; } From ff70ef233cf371ed0092b579521acb9866c3e1bf Mon Sep 17 00:00:00 2001 From: xia-chu <771730766@qq.com> Date: Sat, 20 Apr 2024 20:00:25 +0800 Subject: [PATCH 42/60] Fix bug where ts files are not written into m3u8 file during HLS playback. --- src/Record/HlsMaker.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Record/HlsMaker.cpp b/src/Record/HlsMaker.cpp index 11d5190a..bc11028f 100644 --- a/src/Record/HlsMaker.cpp +++ b/src/Record/HlsMaker.cpp @@ -28,7 +28,7 @@ void HlsMaker::makeIndexFile(bool include_delay, bool eof) { GET_CONFIG(uint32_t, segDelay, Hls::kSegmentDelay); GET_CONFIG(uint32_t, segRetain, Hls::kSegmentRetain); std::deque> temp(_seg_dur_list); - if (!include_delay) { + if (!include_delay && _seg_number) { while (temp.size() > _seg_number) { temp.pop_front(); } From 44d645710872c940e8c01969fc055981e726d9bc Mon Sep 17 00:00:00 2001 From: xia-chu <771730766@qq.com> Date: Sat, 20 Apr 2024 21:15:47 +0800 Subject: [PATCH 43/60] Update ZLToolKit, support kqueue for macOS/BSD --- 3rdpart/ZLToolKit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/3rdpart/ZLToolKit b/3rdpart/ZLToolKit index 04d1c47d..43004bef 160000 --- a/3rdpart/ZLToolKit +++ b/3rdpart/ZLToolKit @@ -1 +1 @@ -Subproject commit 04d1c47d2568f5ce1ff84260cefaf2754e514a5e +Subproject commit 43004bef30cb4b48a7f8238027cee1054733f5b1 From e4de454d5d2c18733a76f9d8987a56ec7f7536b5 Mon Sep 17 00:00:00 2001 From: xia-chu <771730766@qq.com> Date: Sat, 20 Apr 2024 22:25:21 +0800 Subject: [PATCH 44/60] Charset default to UTF-8 on Windows --- 3rdpart/ZLToolKit | 2 +- CMakeLists.txt | 2 +- conf/config.ini | 2 +- server/WebApi.cpp | 2 +- src/Common/config.cpp | 5 ----- src/Http/HttpClient.cpp | 3 ++- src/Http/HttpSession.cpp | 28 ++-------------------------- src/Http/HttpSession.h | 2 -- 8 files changed, 8 insertions(+), 38 deletions(-) diff --git a/3rdpart/ZLToolKit b/3rdpart/ZLToolKit index 43004bef..26d54bbc 160000 --- a/3rdpart/ZLToolKit +++ b/3rdpart/ZLToolKit @@ -1 +1 @@ -Subproject commit 43004bef30cb4b48a7f8238027cee1054733f5b1 +Subproject commit 26d54bbc7b1860a450434dce49bbc8fcbcbae88b diff --git a/CMakeLists.txt b/CMakeLists.txt index d4b2519a..cd0124a9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -204,7 +204,7 @@ elseif(WIN32) if (MSVC) set(COMPILE_OPTIONS_DEFAULT # TODO: /wd4819 应该是不会生效 - "/wd4566;/wd4819" + "/wd4566;/wd4819;/utf-8" # warning C4530: C++ exception handler used, but unwind semantics are not enabled. "/EHsc") # disable Windows logo diff --git a/conf/config.ini b/conf/config.ini index 3c8b67c8..872d2d32 100644 --- a/conf/config.ini +++ b/conf/config.ini @@ -219,7 +219,7 @@ timeout_sec=15 retry_count=3 [http] -#http服务器字符编码,windows上默认gb2312 +#http服务器字符编码集 charSet=utf-8 #http链接超时时间 keepAliveSecond=30 diff --git a/server/WebApi.cpp b/server/WebApi.cpp index a0690747..97260c0a 100755 --- a/server/WebApi.cpp +++ b/server/WebApi.cpp @@ -208,7 +208,7 @@ static ApiArgsType getAllArgs(const Parser &parser) { if (parser["Content-Type"].find("application/x-www-form-urlencoded") == 0) { auto contentArgs = parser.parseArgs(parser.content()); for (auto &pr : contentArgs) { - allArgs[pr.first] = HttpSession::urlDecodeComponent(pr.second); + allArgs[pr.first] = strCoding::UrlDecodeComponent(pr.second); } } else if (parser["Content-Type"].find("application/json") == 0) { try { diff --git a/src/Common/config.cpp b/src/Common/config.cpp index a34ee84f..5a538ab4 100644 --- a/src/Common/config.cpp +++ b/src/Common/config.cpp @@ -181,12 +181,7 @@ static onceToken token([]() { mINI::Instance()[kKeepAliveSecond] = 15; mINI::Instance()[kDirMenu] = true; mINI::Instance()[kVirtualPath] = ""; - -#if defined(_WIN32) - mINI::Instance()[kCharSet] = "gb2312"; -#else mINI::Instance()[kCharSet] = "utf-8"; -#endif mINI::Instance()[kRootPath] = "./www"; mINI::Instance()[kNotFound] = StrPrinter << "" diff --git a/src/Http/HttpClient.cpp b/src/Http/HttpClient.cpp index b9927934..84450d2b 100644 --- a/src/Http/HttpClient.cpp +++ b/src/Http/HttpClient.cpp @@ -66,7 +66,8 @@ void HttpClient::sendRequest(const string &url) { _http_persistent = true; if (_body && _body->remainSize()) { _header.emplace("Content-Length", to_string(_body->remainSize())); - _header.emplace("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); + GET_CONFIG(string, charSet, Http::kCharSet); + _header.emplace("Content-Type", "application/x-www-form-urlencoded; charset=" + charSet); } bool host_changed = (_last_host != host + ":" + to_string(port)) || (_is_https != is_https); diff --git a/src/Http/HttpSession.cpp b/src/Http/HttpSession.cpp index ba74d803..ba25f3f4 100644 --- a/src/Http/HttpSession.cpp +++ b/src/Http/HttpSession.cpp @@ -683,34 +683,10 @@ void HttpSession::sendResponse(int code, AsyncSender::onSocketFlushed(data); } -string HttpSession::urlDecodePath(const string &str) { - auto ret = strCoding::UrlDecodePath(str); -#ifdef _WIN32 - GET_CONFIG(string, charSet, Http::kCharSet); - bool isGb2312 = !strcasecmp(charSet.data(), "gb2312"); - if (isGb2312) { - ret = strCoding::UTF8ToGB2312(ret); - } -#endif // _WIN32 - return ret; -} - -string HttpSession::urlDecodeComponent(const string &str) { - auto ret = strCoding::UrlDecodeComponent(str); -#ifdef _WIN32 - GET_CONFIG(string, charSet, Http::kCharSet); - bool isGb2312 = !strcasecmp(charSet.data(), "gb2312"); - if (isGb2312) { - ret = strCoding::UTF8ToGB2312(ret); - } -#endif // _WIN32 - return ret; -} - void HttpSession::urlDecode(Parser &parser) { - parser.setUrl(urlDecodePath(parser.url())); + parser.setUrl(strCoding::UrlDecodePath(parser.url())); for (auto &pr : _parser.getUrlArgs()) { - const_cast(pr.second) = urlDecodeComponent(pr.second); + const_cast(pr.second) = strCoding::UrlDecodeComponent(pr.second); } } diff --git a/src/Http/HttpSession.h b/src/Http/HttpSession.h index 0ffbf137..9f328063 100644 --- a/src/Http/HttpSession.h +++ b/src/Http/HttpSession.h @@ -44,8 +44,6 @@ public: void onRecv(const toolkit::Buffer::Ptr &) override; void onError(const toolkit::SockException &err) override; void onManager() override; - static std::string urlDecodePath(const std::string &str); - static std::string urlDecodeComponent(const std::string &str); void setTimeoutSec(size_t second); void setMaxReqSize(size_t max_req_size); From 2d66dd7cb3f8a81159a43e1345a19fbea17e2c06 Mon Sep 17 00:00:00 2001 From: xia-chu <771730766@qq.com> Date: Sat, 20 Apr 2024 22:49:27 +0800 Subject: [PATCH 45/60] Fixing the problem of WebRTC handshake failure caused by unsupported RTP extensions (#3486) --- webrtc/RtpExt.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webrtc/RtpExt.cpp b/webrtc/RtpExt.cpp index d1ade570..e186cbf0 100644 --- a/webrtc/RtpExt.cpp +++ b/webrtc/RtpExt.cpp @@ -204,7 +204,8 @@ static unordered_map s_url_to_type = {RTP_EXT_M RtpExtType RtpExt::getExtType(const string &url) { auto it = s_url_to_type.find(url); if (it == s_url_to_type.end()) { - throw std::invalid_argument(string("未识别的rtp ext url类型:") + url); + WarnL << "unknown rtp ext url type: " << url; + return RtpExtType::padding; } return it->second; } From 664d0b67bc958ca7a9ecfba7e08a5a5608599ae3 Mon Sep 17 00:00:00 2001 From: xia-chu <771730766@qq.com> Date: Sun, 21 Apr 2024 11:31:43 +0800 Subject: [PATCH 46/60] Add the listRtpSender http api --- postman/ZLMediaKit.postman_collection.json | 41 ++++++++++++++++++++++ server/WebApi.cpp | 20 +++++++++++ src/Common/MultiMediaSourceMuxer.cpp | 6 ++++ src/Common/MultiMediaSourceMuxer.h | 2 ++ 4 files changed, 69 insertions(+) diff --git a/postman/ZLMediaKit.postman_collection.json b/postman/ZLMediaKit.postman_collection.json index ae3879b0..c3ddd249 100644 --- a/postman/ZLMediaKit.postman_collection.json +++ b/postman/ZLMediaKit.postman_collection.json @@ -2044,6 +2044,47 @@ }, "response": [] }, + { + "name": "获取rtp发送列表(listRtpSender)", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{ZLMediaKit_URL}}/index/api/listRtpSender?secret={{ZLMediaKit_secret}}&vhost={{defaultVhost}}&app=live&stream=test", + "host": [ + "{{ZLMediaKit_URL}}" + ], + "path": [ + "index", + "api", + "listRtpSender" + ], + "query": [ + { + "key": "secret", + "value": "{{ZLMediaKit_secret}}", + "description": "api操作密钥(配置文件配置)" + }, + { + "key": "vhost", + "value": "{{defaultVhost}}", + "description": "虚拟主机,例如__defaultVhost__" + }, + { + "key": "app", + "value": "live", + "description": "应用名,例如 live" + }, + { + "key": "stream", + "value": "test", + "description": "流id,例如 obs" + } + ] + } + }, + "response": [] + }, { "name": "获取版本信息(version)", "request": { diff --git a/server/WebApi.cpp b/server/WebApi.cpp index 97260c0a..8ea758f3 100755 --- a/server/WebApi.cpp +++ b/server/WebApi.cpp @@ -1347,6 +1347,26 @@ void installWebApi() { }); }); + api_regist("/index/api/listRtpSender",[](API_ARGS_MAP_ASYNC){ + CHECK_SECRET(); + CHECK_ARGS("vhost", "app", "stream"); + + auto src = MediaSource::find(allArgs["vhost"], allArgs["app"], allArgs["stream"]); + if (!src) { + throw ApiRetException("can not find the source stream", API::NotFound); + } + + auto muxer = src->getMuxer(); + CHECK(muxer, "get muxer from media source failed"); + + src->getOwnerPoller()->async([=]() mutable { + muxer->forEachRtpSender([&](const std::string &ssrc) mutable { + val["data"].append(ssrc); + }); + invoker(200, headerOut, val.toStyledString()); + }); + }); + api_regist("/index/api/startSendRtpPassive",[](API_ARGS_MAP_ASYNC){ CHECK_SECRET(); CHECK_ARGS("vhost", "app", "stream", "ssrc"); diff --git a/src/Common/MultiMediaSourceMuxer.cpp b/src/Common/MultiMediaSourceMuxer.cpp index fa46e4fa..fb00a668 100644 --- a/src/Common/MultiMediaSourceMuxer.cpp +++ b/src/Common/MultiMediaSourceMuxer.cpp @@ -173,6 +173,12 @@ std::string MultiMediaSourceMuxer::shortUrl() const { return _tuple.shortUrl(); } +void MultiMediaSourceMuxer::forEachRtpSender(const std::function &cb) const { + for (auto &pr : _rtp_sender) { + cb(pr.first); + } +} + MultiMediaSourceMuxer::MultiMediaSourceMuxer(const MediaTuple& tuple, float dur_sec, const ProtocolOption &option): _tuple(tuple) { if (!option.stream_replace.empty()) { // 支持在on_publish hook中替换stream_id diff --git a/src/Common/MultiMediaSourceMuxer.h b/src/Common/MultiMediaSourceMuxer.h index 9ca34370..a9775c8e 100644 --- a/src/Common/MultiMediaSourceMuxer.h +++ b/src/Common/MultiMediaSourceMuxer.h @@ -133,6 +133,8 @@ public: const MediaTuple &getMediaTuple() const; std::string shortUrl() const; + void forEachRtpSender(const std::function &cb) const; + protected: /////////////////////////////////MediaSink override///////////////////////////////// From 86348b86ee2b9b564ad6c3fc870c991d54c5f31a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=8F=E6=A5=9A?= <771730766@qq.com> Date: Tue, 30 Apr 2024 22:06:53 +0800 Subject: [PATCH 47/60] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 44c22aa9..59fd30a4 100644 --- a/README.md +++ b/README.md @@ -222,7 +222,7 @@ bash build_docker_images.sh - 请关注微信公众号获取最新消息推送: - - 也可以自愿有偿加入知识星球咨询和获取资料: + - 也可以自愿有偿加入知识星球咨询、获取资料以及加入微信技术群: From 046aaa3498a4dcd4f700c03653f04fef71a4773d Mon Sep 17 00:00:00 2001 From: yanggs Date: Wed, 1 May 2024 13:19:47 +0800 Subject: [PATCH 48/60] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=8B=89=E5=8F=96?= =?UTF-8?q?=E6=8C=87=E5=AE=9A=E6=B5=81=E6=97=B6=E6=97=B6=E9=95=BF=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E4=B8=A2=E5=A4=B1=E7=9A=84=E9=97=AE=E9=A2=98=20(#3500?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Rtsp/RtspPlayer.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Rtsp/RtspPlayer.cpp b/src/Rtsp/RtspPlayer.cpp index 1cf393e2..e83cc090 100644 --- a/src/Rtsp/RtspPlayer.cpp +++ b/src/Rtsp/RtspPlayer.cpp @@ -210,7 +210,8 @@ void RtspPlayer::handleResDESCRIBE(const Parser &parser) { if (play_track != TrackInvalid) { auto track = sdpParser.getTrack(play_track); _sdp_track.emplace_back(track); - sdp = track->toString(); + auto title_track = sdpParser.getTrack(TrackTitle); + sdp = (title_track ? title_track->toString() : "") + track->toString(); } else { _sdp_track = sdpParser.getAvailableTrack(); sdp = sdpParser.toString(); From 64fdd78330226afbad8a061fc5419040c54a7509 Mon Sep 17 00:00:00 2001 From: gongluck Date: Mon, 6 May 2024 11:07:23 +0800 Subject: [PATCH 49/60] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=87=BD=E6=95=B0mk=5F?= =?UTF-8?q?rtc=5Fsend=5Fdatachannel=E5=A3=B0=E6=98=8E=E3=80=81=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=E6=B2=A1=E5=AF=B9=E5=BA=94=E7=9A=84=E9=94=99=E8=AF=AF?= =?UTF-8?q?=20(#3508)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/source/mk_events_objects.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/source/mk_events_objects.cpp b/api/source/mk_events_objects.cpp index 758e38a3..8fdb38af 100644 --- a/api/source/mk_events_objects.cpp +++ b/api/source/mk_events_objects.cpp @@ -528,7 +528,7 @@ API_EXPORT void API_CALL mk_auth_invoker_clone_release(const mk_auth_invoker ctx } ///////////////////////////////////////////WebRtcTransport///////////////////////////////////////////// -API_EXPORT void API_CALL mk_rtc_sendDatachannel(const mk_rtc_transport ctx, uint16_t streamId, uint32_t ppid, const char *msg, size_t len) { +API_EXPORT void API_CALL mk_rtc_send_datachannel(const mk_rtc_transport ctx, uint16_t streamId, uint32_t ppid, const char *msg, size_t len) { #ifdef ENABLE_WEBRTC assert(ctx && msg); WebRtcTransport *transport = (WebRtcTransport *)ctx; From 8ac957f338609365d6a93dc00748b8c5c724b800 Mon Sep 17 00:00:00 2001 From: xia-chu <771730766@qq.com> Date: Thu, 9 May 2024 18:06:19 +0800 Subject: [PATCH 50/60] bugfix: Fix the memory overflow issue in H264Splitter --- api/source/mk_h264_splitter.cpp | 39 ++++++++++++--------------------- 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/api/source/mk_h264_splitter.cpp b/api/source/mk_h264_splitter.cpp index a6c412f4..b5cd06a3 100644 --- a/api/source/mk_h264_splitter.cpp +++ b/api/source/mk_h264_splitter.cpp @@ -27,8 +27,9 @@ protected: private: bool _h265 = false; + bool _have_decode_frame = false; onH264 _cb; - size_t _search_pos = 0; + toolkit::BufferLikeString _buffer; }; void H264Splitter::setOnSplitted(H264Splitter::onH264 cb) { @@ -42,11 +43,21 @@ H264Splitter::~H264Splitter() { } ssize_t H264Splitter::onRecvHeader(const char *data, size_t len) { - _cb(data, len); + auto frame = Factory::getFrameFromPtr(_h265 ? CodecH265 : CodecH264, (char *)data, len, 0, 0); + if (_have_decode_frame && (frame->decodeAble() || frame->configFrame())) { + // 缓存中存在可解码帧,且下一帧是可解码帧或者配置帧,那么flush缓存 + _cb(_buffer.data(), _buffer.size()); + _buffer.assign(data, len); + _have_decode_frame = frame->decodeAble(); + } else { + // 还需要缓存 + _buffer.append(data, len); + _have_decode_frame = _have_decode_frame || frame->decodeAble(); + } return 0; } -static const char *onSearchPacketTail_l(const char *data, size_t len) { +const char *H264Splitter::onSearchPacketTail(const char *data, size_t len) { for (size_t i = 2; len > 2 && i < len - 2; ++i) { //判断0x00 00 01 if (data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1) { @@ -60,28 +71,6 @@ static const char *onSearchPacketTail_l(const char *data, size_t len) { return nullptr; } -const char *H264Splitter::onSearchPacketTail(const char *data, size_t len) { - auto last_frame = data + _search_pos; - auto next_frame = onSearchPacketTail_l(last_frame, len - _search_pos); - if (!next_frame) { - return nullptr; - } - - auto last_frame_len = next_frame - last_frame; - Frame::Ptr frame; - if (_h265) { - frame = Factory::getFrameFromPtr(CodecH265, (char *)last_frame, last_frame_len, 0, 0); - } else { - frame = Factory::getFrameFromPtr(CodecH264, (char *)last_frame, last_frame_len, 0, 0); - } - if (frame->decodeAble()) { - _search_pos = 0; - return next_frame; - } - _search_pos += last_frame_len; - return nullptr; -} - //////////////////////////////////////////////////////////////////////////////////////////////////////// API_EXPORT mk_h264_splitter API_CALL mk_h264_splitter_create(on_mk_h264_splitter_frame cb, void *user_data, int is_h265) { From 2eed284e617c7833cf648474a3ac2ba2864a47f5 Mon Sep 17 00:00:00 2001 From: xia-chu <771730766@qq.com> Date: Thu, 9 May 2024 18:07:08 +0800 Subject: [PATCH 51/60] feat: Close the connection after authentication failed in HTTP API Improve the safety of HTTP API --- server/WebApi.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/WebApi.cpp b/server/WebApi.cpp index 8ea758f3..30a6c559 100755 --- a/server/WebApi.cpp +++ b/server/WebApi.cpp @@ -289,6 +289,8 @@ static inline void addHttpListener(){ it->second(parser, invoker, sender); } catch (ApiRetException &ex) { responseApi(ex.code(), ex.what(), invoker); + auto helper = static_cast(sender).shared_from_this(); + helper->getPoller()->async([helper, ex]() { helper->shutdown(SockException(Err_shutdown, ex.what())); }, false); } #ifdef ENABLE_MYSQL catch(SqlException &ex){ From 18c5bb36707fbf7de02573bdc77182d1080b0920 Mon Sep 17 00:00:00 2001 From: Lidaofu <746101210@qq.com> Date: Fri, 10 May 2024 14:27:23 +0800 Subject: [PATCH 52/60] =?UTF-8?q?=E6=B7=BB=E5=8A=A0C=20API=20on=5Frecord?= =?UTF-8?q?=5Fts=20=E5=9B=9E=E8=B0=83ts=E5=BD=95=E5=83=8F=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=20(#3520)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用时合并on_record_mp4相关接口 Co-authored-by: 李道甫 --- api/include/mk_events.h | 21 ++++++++++------- api/include/mk_events_objects.h | 39 +++++++++++++++++++++----------- api/source/mk_events.cpp | 8 ++++++- api/source/mk_events_objects.cpp | 20 ++++++++-------- api/tests/server.c | 22 +++++++++--------- 5 files changed, 67 insertions(+), 43 deletions(-) diff --git a/api/include/mk_events.h b/api/include/mk_events.h index 8f433a2a..d765a8fa 100644 --- a/api/include/mk_events.h +++ b/api/include/mk_events.h @@ -132,7 +132,12 @@ typedef struct { /** * 录制mp4分片文件成功后广播 */ - void (API_CALL *on_mk_record_mp4)(const mk_mp4_info mp4); + void (API_CALL *on_mk_record_mp4)(const mk_record_info mp4); + + /** + * 录制ts分片文件成功后广播 + */ + void (API_CALL *on_mk_record_ts)(const mk_record_info ts); /** * shell登录鉴权 @@ -175,16 +180,16 @@ typedef struct { * @param err 错误代码 * @param msg 错误提示 */ - void(API_CALL *on_mk_media_send_rtp_stop)(const char *vhost, const char *app, const char *stream, const char *ssrc, int err, const char *msg); + void (API_CALL *on_mk_media_send_rtp_stop)(const char *vhost, const char *app, const char *stream, const char *ssrc, int err, const char *msg); /** * rtc sctp连接中/完成/失败/关闭回调 * @param rtc_transport 数据通道对象 */ - void(API_CALL *on_mk_rtc_sctp_connecting)(mk_rtc_transport rtc_transport); - void(API_CALL *on_mk_rtc_sctp_connected)(mk_rtc_transport rtc_transport); - void(API_CALL *on_mk_rtc_sctp_failed)(mk_rtc_transport rtc_transport); - void(API_CALL *on_mk_rtc_sctp_closed)(mk_rtc_transport rtc_transport); + void (API_CALL *on_mk_rtc_sctp_connecting)(mk_rtc_transport rtc_transport); + void (API_CALL *on_mk_rtc_sctp_connected)(mk_rtc_transport rtc_transport); + void (API_CALL *on_mk_rtc_sctp_failed)(mk_rtc_transport rtc_transport); + void (API_CALL *on_mk_rtc_sctp_closed)(mk_rtc_transport rtc_transport); /** * rtc数据通道发送数据回调 @@ -192,7 +197,7 @@ typedef struct { * @param msg 数据 * @param len 数据长度 */ - void(API_CALL *on_mk_rtc_sctp_send)(mk_rtc_transport rtc_transport, const uint8_t *msg, size_t len); + void (API_CALL *on_mk_rtc_sctp_send)(mk_rtc_transport rtc_transport, const uint8_t *msg, size_t len); /** * rtc数据通道接收数据回调 @@ -202,7 +207,7 @@ typedef struct { * @param msg 数据 * @param len 数据长度 */ - void(API_CALL *on_mk_rtc_sctp_received)(mk_rtc_transport rtc_transport, uint16_t streamId, uint32_t ppid, const uint8_t *msg, size_t len); + void (API_CALL *on_mk_rtc_sctp_received)(mk_rtc_transport rtc_transport, uint16_t streamId, uint32_t ppid, const uint8_t *msg, size_t len); } mk_events; diff --git a/api/include/mk_events_objects.h b/api/include/mk_events_objects.h index 428f9dc6..d28dbc62 100644 --- a/api/include/mk_events_objects.h +++ b/api/include/mk_events_objects.h @@ -18,29 +18,42 @@ extern "C" { #endif -///////////////////////////////////////////MP4Info///////////////////////////////////////////// -//MP4Info对象的C映射 -typedef struct mk_mp4_info_t *mk_mp4_info; +///////////////////////////////////////////RecordInfo///////////////////////////////////////////// +//RecordInfo对象的C映射 +typedef struct mk_record_info_t *mk_record_info; // GMT 标准时间,单位秒 -API_EXPORT uint64_t API_CALL mk_mp4_info_get_start_time(const mk_mp4_info ctx); +API_EXPORT uint64_t API_CALL mk_record_info_get_start_time(const mk_record_info ctx); // 录像长度,单位秒 -API_EXPORT float API_CALL mk_mp4_info_get_time_len(const mk_mp4_info ctx); +API_EXPORT float API_CALL mk_record_info_get_time_len(const mk_record_info ctx); // 文件大小,单位 BYTE -API_EXPORT size_t API_CALL mk_mp4_info_get_file_size(const mk_mp4_info ctx); +API_EXPORT size_t API_CALL mk_record_info_get_file_size(const mk_record_info ctx); // 文件路径 -API_EXPORT const char* API_CALL mk_mp4_info_get_file_path(const mk_mp4_info ctx); +API_EXPORT const char *API_CALL mk_record_info_get_file_path(const mk_record_info ctx); // 文件名称 -API_EXPORT const char* API_CALL mk_mp4_info_get_file_name(const mk_mp4_info ctx); +API_EXPORT const char *API_CALL mk_record_info_get_file_name(const mk_record_info ctx); // 文件夹路径 -API_EXPORT const char* API_CALL mk_mp4_info_get_folder(const mk_mp4_info ctx); +API_EXPORT const char *API_CALL mk_record_info_get_folder(const mk_record_info ctx); // 播放路径 -API_EXPORT const char* API_CALL mk_mp4_info_get_url(const mk_mp4_info ctx); +API_EXPORT const char *API_CALL mk_record_info_get_url(const mk_record_info ctx); // 应用名称 -API_EXPORT const char* API_CALL mk_mp4_info_get_vhost(const mk_mp4_info ctx); +API_EXPORT const char *API_CALL mk_record_info_get_vhost(const mk_record_info ctx); // 流 ID -API_EXPORT const char* API_CALL mk_mp4_info_get_app(const mk_mp4_info ctx); +API_EXPORT const char *API_CALL mk_record_info_get_app(const mk_record_info ctx); // 虚拟主机 -API_EXPORT const char* API_CALL mk_mp4_info_get_stream(const mk_mp4_info ctx); +API_EXPORT const char *API_CALL mk_record_info_get_stream(const mk_record_info ctx); + +//// 下面宏保障用户代码兼容性, 二进制abi不兼容,用户需要重新编译链接 ///// +#define mk_mp4_info mk_record_info +#define mk_mp4_info_get_start_time mk_record_info_get_start_time +#define mk_mp4_info_get_time_len mk_record_info_get_time_len +#define mk_mp4_info_get_file_size mk_record_info_get_file_size +#define mk_mp4_info_get_file_path mk_record_info_get_file_path +#define mk_mp4_info_get_file_name mk_record_info_get_file_name +#define mk_mp4_info_get_folder mk_record_info_get_folder +#define mk_mp4_info_get_url mk_record_info_get_url +#define mk_mp4_info_get_vhost mk_record_info_get_vhost +#define mk_mp4_info_get_app mk_record_info_get_app +#define mk_mp4_info_get_stream mk_record_info_get_stream ///////////////////////////////////////////Parser///////////////////////////////////////////// //Parser对象的C映射 diff --git a/api/source/mk_events.cpp b/api/source/mk_events.cpp index 7f5ed661..a835a7c9 100644 --- a/api/source/mk_events.cpp +++ b/api/source/mk_events.cpp @@ -42,7 +42,13 @@ API_EXPORT void API_CALL mk_events_listen(const mk_events *events){ NoticeCenter::Instance().addListener(&s_tag,Broadcast::kBroadcastRecordMP4,[](BroadcastRecordMP4Args){ if(s_events.on_mk_record_mp4){ - s_events.on_mk_record_mp4((mk_mp4_info)&info); + s_events.on_mk_record_mp4((mk_record_info)&info); + } + }); + + NoticeCenter::Instance().addListener(&s_tag, Broadcast::kBroadcastRecordTs, [](BroadcastRecordTsArgs) { + if (s_events.on_mk_record_ts) { + s_events.on_mk_record_ts((mk_record_info)&info); } }); diff --git a/api/source/mk_events_objects.cpp b/api/source/mk_events_objects.cpp index 8fdb38af..3615bbcd 100644 --- a/api/source/mk_events_objects.cpp +++ b/api/source/mk_events_objects.cpp @@ -26,61 +26,61 @@ using namespace toolkit; using namespace mediakit; ///////////////////////////////////////////RecordInfo///////////////////////////////////////////// -API_EXPORT uint64_t API_CALL mk_mp4_info_get_start_time(const mk_mp4_info ctx){ +API_EXPORT uint64_t API_CALL mk_record_info_get_start_time(const mk_record_info ctx) { assert(ctx); RecordInfo *info = (RecordInfo *)ctx; return info->start_time; } -API_EXPORT float API_CALL mk_mp4_info_get_time_len(const mk_mp4_info ctx){ +API_EXPORT float API_CALL mk_record_info_get_time_len(const mk_record_info ctx) { assert(ctx); RecordInfo *info = (RecordInfo *)ctx; return info->time_len; } -API_EXPORT size_t API_CALL mk_mp4_info_get_file_size(const mk_mp4_info ctx){ +API_EXPORT size_t API_CALL mk_record_info_get_file_size(const mk_record_info ctx) { assert(ctx); RecordInfo *info = (RecordInfo *)ctx; return info->file_size; } -API_EXPORT const char* API_CALL mk_mp4_info_get_file_path(const mk_mp4_info ctx){ +API_EXPORT const char *API_CALL mk_record_info_get_file_path(const mk_record_info ctx) { assert(ctx); RecordInfo *info = (RecordInfo *)ctx; return info->file_path.c_str(); } -API_EXPORT const char* API_CALL mk_mp4_info_get_file_name(const mk_mp4_info ctx){ +API_EXPORT const char *API_CALL mk_record_info_get_file_name(const mk_record_info ctx) { assert(ctx); RecordInfo *info = (RecordInfo *)ctx; return info->file_name.c_str(); } -API_EXPORT const char* API_CALL mk_mp4_info_get_folder(const mk_mp4_info ctx){ +API_EXPORT const char *API_CALL mk_record_info_get_folder(const mk_record_info ctx) { assert(ctx); RecordInfo *info = (RecordInfo *)ctx; return info->folder.c_str(); } -API_EXPORT const char* API_CALL mk_mp4_info_get_url(const mk_mp4_info ctx){ +API_EXPORT const char *API_CALL mk_record_info_get_url(const mk_record_info ctx) { assert(ctx); RecordInfo *info = (RecordInfo *)ctx; return info->url.c_str(); } -API_EXPORT const char* API_CALL mk_mp4_info_get_vhost(const mk_mp4_info ctx){ +API_EXPORT const char *API_CALL mk_record_info_get_vhost(const mk_record_info ctx) { assert(ctx); RecordInfo *info = (RecordInfo *)ctx; return info->vhost.c_str(); } -API_EXPORT const char* API_CALL mk_mp4_info_get_app(const mk_mp4_info ctx){ +API_EXPORT const char *API_CALL mk_record_info_get_app(const mk_record_info ctx) { assert(ctx); RecordInfo *info = (RecordInfo *)ctx; return info->app.c_str(); } -API_EXPORT const char* API_CALL mk_mp4_info_get_stream(const mk_mp4_info ctx){ +API_EXPORT const char *API_CALL mk_record_info_get_stream(const mk_record_info ctx) { assert(ctx); RecordInfo *info = (RecordInfo *)ctx; return info->stream.c_str(); diff --git a/api/tests/server.c b/api/tests/server.c index 4f76fe60..28f44ada 100644 --- a/api/tests/server.c +++ b/api/tests/server.c @@ -387,7 +387,7 @@ void API_CALL on_mk_rtsp_auth(const mk_media_info url_info, /** * 录制mp4分片文件成功后广播 */ -void API_CALL on_mk_record_mp4(const mk_mp4_info mp4) { +void API_CALL on_mk_record_mp4(const mk_record_info mp4) { log_printf(LOG_LEV, "\nstart_time: %d\n" "time_len: %d\n" @@ -399,16 +399,16 @@ void API_CALL on_mk_record_mp4(const mk_mp4_info mp4) { "vhost: %s\n" "app: %s\n" "stream: %s\n", - mk_mp4_info_get_start_time(mp4), - mk_mp4_info_get_time_len(mp4), - mk_mp4_info_get_file_size(mp4), - mk_mp4_info_get_file_path(mp4), - mk_mp4_info_get_file_name(mp4), - mk_mp4_info_get_folder(mp4), - mk_mp4_info_get_url(mp4), - mk_mp4_info_get_vhost(mp4), - mk_mp4_info_get_app(mp4), - mk_mp4_info_get_stream(mp4)); + mk_record_info_get_start_time(mp4), + mk_record_info_get_time_len(mp4), + mk_record_info_get_file_size(mp4), + mk_record_info_get_file_path(mp4), + mk_record_info_get_file_name(mp4), + mk_record_info_get_folder(mp4), + mk_record_info_get_url(mp4), + mk_record_info_get_vhost(mp4), + mk_record_info_get_app(mp4), + mk_record_info_get_stream(mp4)); } /** From 1c89950ebeb5e45d3bd92ac1595993c1185a6f94 Mon Sep 17 00:00:00 2001 From: xiongguangjie Date: Sat, 11 May 2024 10:49:33 +0800 Subject: [PATCH 53/60] Fix record info size to int64 (#3527) --- src/Record/Recorder.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Record/Recorder.h b/src/Record/Recorder.h index ea924618..1913ae05 100644 --- a/src/Record/Recorder.h +++ b/src/Record/Recorder.h @@ -32,7 +32,7 @@ class RecordInfo: public MediaTuple { public: time_t start_time; // GMT 标准时间,单位秒 float time_len; // 录像长度,单位秒 - off_t file_size; // 文件大小,单位 BYTE + uint64_t file_size; // 文件大小,单位 BYTE std::string file_path; // 文件路径 std::string file_name; // 文件名称 std::string folder; // 文件夹路径 From bbdbd6a3e0b73123202965e1899791ffec351572 Mon Sep 17 00:00:00 2001 From: xia-chu <771730766@qq.com> Date: Sat, 11 May 2024 14:38:33 +0800 Subject: [PATCH 54/60] feat: Using kBeatIntervalMS option for rtsp heartbeat interval time and add kRtspBeatType for setting rtsp heartbeat type --- src/Common/config.cpp | 1 + src/Common/config.h | 3 +++ src/Rtsp/RtspPlayer.cpp | 27 ++++++++++++++++----------- src/Rtsp/RtspPlayer.h | 5 +++++ 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/Common/config.cpp b/src/Common/config.cpp index 5a538ab4..8b7031ae 100644 --- a/src/Common/config.cpp +++ b/src/Common/config.cpp @@ -361,6 +361,7 @@ static onceToken token([]() { namespace Client { const string kNetAdapter = "net_adapter"; const string kRtpType = "rtp_type"; +const string kRtspBeatType = "rtsp_beat_type"; const string kRtspUser = "rtsp_user"; const string kRtspPwd = "rtsp_pwd"; const string kRtspPwdIsMD5 = "rtsp_pwd_md5"; diff --git a/src/Common/config.h b/src/Common/config.h index 98ab289c..b10fe685 100644 --- a/src/Common/config.h +++ b/src/Common/config.h @@ -417,6 +417,9 @@ extern const std::string kNetAdapter; // 设置rtp传输类型,可选项有0(tcp,默认)、1(udp)、2(组播) // 设置方法:player[PlayerBase::kRtpType] = 0/1/2; extern const std::string kRtpType; +// rtsp播放器发送信令心跳还是rtcp心跳,可选项有0(同时发)、1(rtcp心跳)、2(信令心跳) +// 设置方法:player[PlayerBase::kRtspBeatType] = 0/1/2; +extern const std::string kRtspBeatType; // rtsp认证用户名 extern const std::string kRtspUser; // rtsp认证用用户密码,可以是明文也可以是md5,md5密码生成方式 md5(username:realm:password) diff --git a/src/Rtsp/RtspPlayer.cpp b/src/Rtsp/RtspPlayer.cpp index e83cc090..5d279fd7 100644 --- a/src/Rtsp/RtspPlayer.cpp +++ b/src/Rtsp/RtspPlayer.cpp @@ -28,6 +28,7 @@ using namespace std; namespace mediakit { enum PlayType { type_play = 0, type_pause, type_seek, type_speed }; +enum class BeatType : uint32_t { both = 0, rtcp, cmd }; RtspPlayer::RtspPlayer(const EventPoller::Ptr &poller) : TcpClient(poller) {} @@ -85,6 +86,8 @@ void RtspPlayer::play(const string &strUrl) { _play_url = url._url; _rtp_type = (Rtsp::eRtpType)(int)(*this)[Client::kRtpType]; + _beat_type = (*this)[Client::kRtspBeatType].as(); + _beat_interval_ms = (*this)[Client::kBeatIntervalMS].as(); DebugL << url._url << " " << (url._user.size() ? url._user : "null") << " " << (url._passwd.size() ? url._passwd : "null") << " " << _rtp_type; weak_ptr weakSelf = static_pointer_cast(shared_from_this()); @@ -642,23 +645,28 @@ void RtspPlayer::onBeforeRtpSorted(const RtpPacket::Ptr &rtp, int track_idx) { rtcp_ctx->onRtp(rtp->getSeq(), rtp->getStamp(), rtp->ntp_stamp, rtp->sample_rate, rtp->size() - RtpPacket::kRtpTcpHeaderSize); auto &ticker = _rtcp_send_ticker[track_idx]; - if (ticker.elapsedTime() < 3 * 1000) { - // 时间未到 + if (ticker.elapsedTime() < _beat_interval_ms) { + // 心跳时间未到 return; } - auto &rtcp_flag = _send_rtcp[track_idx]; - // 每3秒发送一次心跳,rtcp与rtsp信令轮流心跳,该特性用于兼容issue:642 - // 有些rtsp服务器需要rtcp保活,有些需要发送信令保活 + // 有些rtsp服务器需要rtcp保活,有些需要发送信令保活; rtcp与rtsp信令轮流心跳,该特性用于兼容issue:#642 + auto &rtcp_flag = _send_rtcp[track_idx]; + ticker.resetTime(); + + switch ((BeatType)_beat_type) { + case BeatType::cmd: rtcp_flag = false; break; + case BeatType::rtcp: rtcp_flag = true; break; + case BeatType::both: + default: rtcp_flag = !rtcp_flag; break; + } // 发送信令保活 if (!rtcp_flag) { if (track_idx == 0) { + // 两个track无需同时触发发送信令保活 sendKeepAlive(); } - ticker.resetTime(); - // 下次发送rtcp保活 - rtcp_flag = true; return; } @@ -680,9 +688,6 @@ void RtspPlayer::onBeforeRtpSorted(const RtpPacket::Ptr &rtp, int track_idx) { rtcp_sdes->chunks.ssrc = htonl(ssrc); send_rtcp(this, track_idx, std::move(rtcp)); send_rtcp(this, track_idx, RtcpHeader::toBuffer(rtcp_sdes)); - ticker.resetTime(); - // 下次发送信令保活 - rtcp_flag = false; } void RtspPlayer::onPlayResult_l(const SockException &ex, bool handshake_done) { diff --git a/src/Rtsp/RtspPlayer.h b/src/Rtsp/RtspPlayer.h index 66882c20..c40d72b7 100644 --- a/src/Rtsp/RtspPlayer.h +++ b/src/Rtsp/RtspPlayer.h @@ -114,6 +114,11 @@ private: //轮流发送rtcp与GET_PARAMETER保活 bool _send_rtcp[2] = {true, true}; + // 心跳类型 + uint32_t _beat_type = 0; + // 心跳保护间隔 + uint32_t _beat_interval_ms = 0; + std::string _play_url; std::vector _sdp_track; std::function _on_response; From 5df2b8075749af1a4d8107f71485b4e2bd222716 Mon Sep 17 00:00:00 2001 From: gongluck Date: Sun, 12 May 2024 18:15:11 +0800 Subject: [PATCH 55/60] =?UTF-8?q?Nack=E7=9B=B8=E5=85=B3=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E5=8F=AF=E9=85=8D=E7=BD=AE=E5=8C=96=20(#3510=20#3507)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- conf/config.ini | 18 +++++++++++ webrtc/Nack.cpp | 61 +++++++++++++++++++++++++++++++------- webrtc/Nack.h | 12 -------- webrtc/WebRtcTransport.cpp | 8 ++++- 4 files changed, 76 insertions(+), 23 deletions(-) diff --git a/conf/config.ini b/conf/config.ini index 872d2d32..89800db2 100644 --- a/conf/config.ini +++ b/conf/config.ini @@ -367,6 +367,24 @@ start_bitrate=0 max_bitrate=0 min_bitrate=0 +#nack接收端 +#Nack缓存包最早时间间隔 +maxNackMS=5000 +#Nack包检查间隔(包数量) +rtpCacheCheckInterval=100 + +#nack发送端 +#最大保留的rtp丢包状态个数 +nackMaxSize=2048 +#rtp丢包状态最长保留时间 +nackMaxMS=3000 +#nack最多请求重传次数 +nackMaxCount=15 +#nack重传频率,rtt的倍数 +nackIntervalRatio=1.0 +#nack包中rtp个数,减小此值可以让nack包响应更灵敏 +nackRtpSize=8 + [srt] #srt播放推流、播放超时时间,单位秒 timeoutSec=5 diff --git a/webrtc/Nack.cpp b/webrtc/Nack.cpp index 6197117e..4fd1f101 100644 --- a/webrtc/Nack.cpp +++ b/webrtc/Nack.cpp @@ -9,24 +9,56 @@ */ #include "Nack.h" +#include "Common/config.h" using namespace std; using namespace toolkit; namespace mediakit { -static constexpr uint32_t kMaxNackMS = 5 * 1000; -static constexpr uint32_t kRtpCacheCheckInterval = 100; +// RTC配置项目 +namespace Rtc { +#define RTC_FIELD "rtc." +//~ nack接收端 +// Nack缓存包最早时间间隔 +const string kMaxNackMS = RTC_FIELD "maxNackMS"; +// Nack包检查间隔(包数量) +const string kRtpCacheCheckInterval = RTC_FIELD "rtpCacheCheckInterval"; +//~ nack发送端 +//最大保留的rtp丢包状态个数 +const string kNackMaxSize = RTC_FIELD "nackMaxSize"; +// rtp丢包状态最长保留时间 +const string kNackMaxMS = RTC_FIELD "nackMaxMS"; +// nack最多请求重传次数 +const string kNackMaxCount = RTC_FIELD "nackMaxCount"; +// nack重传频率,rtt的倍数 +const string kNackIntervalRatio = RTC_FIELD "nackIntervalRatio"; +// nack包中rtp个数,减小此值可以让nack包响应更灵敏 +const string kNackRtpSize = RTC_FIELD "nackRtpSize"; + +static onceToken token([]() { + mINI::Instance()[kMaxNackMS] = 5 * 1000; + mINI::Instance()[kRtpCacheCheckInterval] = 100; + mINI::Instance()[kNackMaxSize] = 2048; + mINI::Instance()[kNackMaxMS] = 3 * 1000; + mINI::Instance()[kNackMaxCount] = 15; + mINI::Instance()[kNackIntervalRatio] = 1.0f; + mINI::Instance()[kNackRtpSize] = 8; +}); + +} // namespace Rtc void NackList::pushBack(RtpPacket::Ptr rtp) { auto seq = rtp->getSeq(); _nack_cache_seq.emplace_back(seq); _nack_cache_pkt.emplace(seq, std::move(rtp)); - if (++_cache_ms_check < kRtpCacheCheckInterval) { + GET_CONFIG(uint32_t, rtpcache_checkinterval, Rtc::kRtpCacheCheckInterval); + if (++_cache_ms_check < rtpcache_checkinterval) { return; } _cache_ms_check = 0; - while (getCacheMS() >= kMaxNackMS) { + GET_CONFIG(uint32_t, maxnackms, Rtc::kMaxNackMS); + while (getCacheMS() >= maxnackms) { // 需要清除部分nack缓存 popFront(); } @@ -148,10 +180,13 @@ void NackContext::makeNack(uint16_t max_seq, bool flush) { eraseFrontSeq(); // 最多生成5个nack包,防止seq大幅跳跃导致一直循环 auto max_nack = 5u; + GET_CONFIG(uint32_t, nack_rtpsize, Rtc::kNackRtpSize); + // kNackRtpSize must between 0 and 16 + nack_rtpsize = std::min(nack_rtpsize, FCI_NACK::kBitSize); while (_nack_seq != max_seq && max_nack--) { // 一次不能发送超过16+1个rtp的状态 uint16_t nack_rtp_count = std::min(FCI_NACK::kBitSize, max_seq - (uint16_t)(_nack_seq + 1)); - if (!flush && nack_rtp_count < kNackRtpSize) { + if (!flush && nack_rtp_count < nack_rtpsize) { // 非flush状态下,seq个数不足以发送一次nack break; } @@ -206,7 +241,9 @@ void NackContext::clearNackStatus(uint16_t seq) { _nack_send_status.erase(it); // 限定rtt在合理有效范围内 - _rtt = max(10, min(rtt, kNackMaxMS / kNackMaxCount)); + GET_CONFIG(uint32_t, nack_maxms, Rtc::kNackMaxMS); + GET_CONFIG(uint32_t, nack_maxcount, Rtc::kNackMaxCount); + _rtt = max(10, min(rtt, nack_maxms / nack_maxcount)); } void NackContext::recordNack(const FCI_NACK &nack) { @@ -222,7 +259,8 @@ void NackContext::recordNack(const FCI_NACK &nack) { ++i; } // 记录太多了,移除一部分早期的记录 - while (_nack_send_status.size() > kNackMaxSize) { + GET_CONFIG(uint32_t, nack_maxsize, Rtc::kNackMaxSize); + while (_nack_send_status.size() > nack_maxsize) { _nack_send_status.erase(_nack_send_status.begin()); } } @@ -230,13 +268,16 @@ void NackContext::recordNack(const FCI_NACK &nack) { uint64_t NackContext::reSendNack() { set nack_rtp; auto now = getCurrentMillisecond(); + GET_CONFIG(uint32_t, nack_maxms, Rtc::kNackMaxMS); + GET_CONFIG(uint32_t, nack_maxcount, Rtc::kNackMaxCount); + GET_CONFIG(float, nack_intervalratio, Rtc::kNackIntervalRatio); for (auto it = _nack_send_status.begin(); it != _nack_send_status.end();) { - if (now - it->second.first_stamp > kNackMaxMS) { + if (now - it->second.first_stamp > nack_maxms) { // 该rtp丢失太久了,不再要求重传 it = _nack_send_status.erase(it); continue; } - if (now - it->second.update_stamp < kNackIntervalRatio * _rtt) { + if (now - it->second.update_stamp < nack_intervalratio * _rtt) { // 距离上次nack不足2倍的rtt,不用再发送nack ++it; continue; @@ -245,7 +286,7 @@ uint64_t NackContext::reSendNack() { nack_rtp.emplace(it->first); // 更新nack发送时间戳 it->second.update_stamp = now; - if (++(it->second.nack_count) == kNackMaxCount) { + if (++(it->second.nack_count) == nack_maxcount) { // nack次数太多,移除之 it = _nack_send_status.erase(it); continue; diff --git a/webrtc/Nack.h b/webrtc/Nack.h index b0cd22b4..8ed98be9 100644 --- a/webrtc/Nack.h +++ b/webrtc/Nack.h @@ -41,18 +41,6 @@ class NackContext { public: using Ptr = std::shared_ptr; using onNack = std::function; - //最大保留的rtp丢包状态个数 - static constexpr auto kNackMaxSize = 2048; - // rtp丢包状态最长保留时间 - static constexpr auto kNackMaxMS = 3 * 1000; - // nack最多请求重传10次 - static constexpr auto kNackMaxCount = 15; - // nack重传频率,rtt的倍数 - static constexpr auto kNackIntervalRatio = 1.0f; - // nack包中rtp个数,减小此值可以让nack包响应更灵敏 - static constexpr auto kNackRtpSize = 8; - - static_assert(kNackRtpSize >=0 && kNackRtpSize <= FCI_NACK::kBitSize, "NackContext::kNackRtpSize must between 0 and 16"); NackContext(); diff --git a/webrtc/WebRtcTransport.cpp b/webrtc/WebRtcTransport.cpp index 768c543e..3d2aaa68 100644 --- a/webrtc/WebRtcTransport.cpp +++ b/webrtc/WebRtcTransport.cpp @@ -57,6 +57,9 @@ const string kMinBitrate = RTC_FIELD "min_bitrate"; // 数据通道设置 const string kDataChannelEcho = RTC_FIELD "datachannel_echo"; +// rtp丢包状态最长保留时间 +const string kNackMaxMS = RTC_FIELD "nackMaxMS"; + static onceToken token([]() { mINI::Instance()[kTimeOutSec] = 15; mINI::Instance()[kExternIP] = ""; @@ -69,6 +72,8 @@ static onceToken token([]() { mINI::Instance()[kMinBitrate] = 0; mINI::Instance()[kDataChannelEcho] = true; + + mINI::Instance()[kNackMaxMS] = 3 * 1000; }); } // namespace RTC @@ -800,7 +805,8 @@ public: _on_nack = std::move(on_nack); setOnSorted(std::move(cb)); //设置jitter buffer参数 - RtpTrackImp::setParams(1024, NackContext::kNackMaxMS, 512); + GET_CONFIG(uint32_t, nack_maxms, Rtc::kNackMaxMS); + RtpTrackImp::setParams(1024, nack_maxms, 512); _nack_ctx.setOnNack([this](const FCI_NACK &nack) { onNack(nack); }); } From 472d7d1e4fe650ad51088270160b26e20a32bb8c Mon Sep 17 00:00:00 2001 From: admin <964472638@qq.com> Date: Wed, 15 May 2024 11:17:12 +0800 Subject: [PATCH 56/60] =?UTF-8?q?=E5=85=BC=E5=AE=B9rtsp=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E7=AB=AF=E5=8F=91=E9=80=81=E7=BC=93=E5=86=B2=E5=8C=BA=E6=BA=A2?= =?UTF-8?q?=E5=87=BA=E8=A6=86=E7=9B=96=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Rtsp/RtspSplitter.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Rtsp/RtspSplitter.cpp b/src/Rtsp/RtspSplitter.cpp index 06e9f8c3..33e6e694 100644 --- a/src/Rtsp/RtspSplitter.cpp +++ b/src/Rtsp/RtspSplitter.cpp @@ -10,8 +10,9 @@ #include #include "RtspSplitter.h" -#include "Util/logger.h" #include "Util/util.h" +#include "Util/logger.h" +#include "Common/macros.h" using namespace std; using namespace toolkit; @@ -64,7 +65,18 @@ ssize_t RtspSplitter::onRecvHeader(const char *data, size_t len) { if (len == 4 && !memcmp(data, "\r\n\r\n", 4)) { return 0; } - _parser.parse(data, len); + try { + _parser.parse(data, len); + } catch (mediakit::AssertFailedException &ex){ + if (!_enableRecvRtp) { + // 还在握手中,直接中断握手 + throw; + } + // 握手已经结束,如果rtsp server存在发送缓存溢出的bug,那么rtsp信令可能跟rtp混在一起 + // 这种情况下,rtsp信令解析异常不中断链接,只丢弃这个包 + WarnL << ex.what(); + return 0; + } auto ret = getContentLength(_parser); if (ret == 0) { onWholeRtspPacket(_parser); From 65c2abb0b53997a3fc164ae40d8b0f8ce21c46dd Mon Sep 17 00:00:00 2001 From: admin <964472638@qq.com> Date: Wed, 15 May 2024 11:30:16 +0800 Subject: [PATCH 57/60] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E5=88=A0=E9=99=A4mp4=E5=BD=95=E5=83=8F=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/WebApi.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/server/WebApi.cpp b/server/WebApi.cpp index 30a6c559..988b55b5 100755 --- a/server/WebApi.cpp +++ b/server/WebApi.cpp @@ -1590,12 +1590,14 @@ void installWebApi() { auto record_path = Recorder::getRecordPath(Recorder::type_mp4, tuple, allArgs["customized_path"]); auto period = allArgs["period"]; record_path = record_path + period + "/"; + + bool recording = false; auto name = allArgs["name"]; if (!name.empty()) { + // 删除指定文件 record_path += name; - } - bool recording = false; - { + } else { + // 删除文件夹,先判断该流是否正在录制中 auto src = MediaSource::find(allArgs["vhost"], allArgs["app"], allArgs["stream"]); if (src && src->isRecording(Recorder::type_mp4)) { recording = true; From 0935da60a3d4674b962bbe78698c0bd4ad0cd689 Mon Sep 17 00:00:00 2001 From: PioLing <964472638@qq.com> Date: Sun, 19 May 2024 10:30:29 +0800 Subject: [PATCH 58/60] feat: Add on_mk_get_statistic_cb c api to obtain performance statistics data (#3549) --- api/include/mk_util.h | 9 +++ api/source/mk_util.cpp | 126 +++++++++++++++++++++++++++++++++++++++++ api/tests/server.c | 11 ++++ 3 files changed, 146 insertions(+) diff --git a/api/include/mk_util.h b/api/include/mk_util.h index 32f9da7a..fb79dc82 100644 --- a/api/include/mk_util.h +++ b/api/include/mk_util.h @@ -128,6 +128,15 @@ API_EXPORT char *API_CALL mk_ini_dump_string(mk_ini ini); * @param file 配置文件路径 */ API_EXPORT void API_CALL mk_ini_dump_file(mk_ini ini, const char *file); +///////////////////////////////////////////统计///////////////////////////////////////////// + +typedef void(API_CALL *on_mk_get_statistic_cb)(void *user_data, mk_ini ini); + +/** + * 获取内存数据统计 + * @param ini 存放统计结果 + */ +API_EXPORT void API_CALL mk_get_statistic(on_mk_get_statistic_cb cb, void *user_data, on_user_data_free free_cb); ///////////////////////////////////////////日志///////////////////////////////////////////// diff --git a/api/source/mk_util.cpp b/api/source/mk_util.cpp index eda27162..e2817161 100644 --- a/api/source/mk_util.cpp +++ b/api/source/mk_util.cpp @@ -15,6 +15,9 @@ #include "Util/util.h" #include "Util/mini.h" #include "Util/logger.h" +#include "Util/TimeTicker.h" +#include "Poller/EventPoller.h" +#include "Thread/WorkThreadPool.h" #include "Common/config.h" using namespace std; @@ -132,6 +135,129 @@ API_EXPORT void API_CALL mk_ini_dump_file(mk_ini ini, const char *file) { ptr->dumpFile(file); } +extern uint64_t getTotalMemUsage(); +extern uint64_t getTotalMemBlock(); +extern uint64_t getThisThreadMemUsage(); +extern uint64_t getThisThreadMemBlock(); +extern std::vector getBlockTypeSize(); +extern uint64_t getTotalMemBlockByType(int type); +extern uint64_t getThisThreadMemBlockByType(int type); + +namespace mediakit { +class MediaSource; +class MultiMediaSourceMuxer; +class FrameImp; +class Frame; +class RtpPacket; +class RtmpPacket; +} // namespace mediakit + +namespace toolkit { +class TcpServer; +class TcpSession; +class UdpServer; +class UdpSession; +class TcpClient; +class Socket; +class Buffer; +class BufferRaw; +class BufferLikeString; +class BufferList; +} // namespace toolkit + +API_EXPORT void API_CALL mk_get_statistic(on_mk_get_statistic_cb func, void *user_data, on_user_data_free free_cb) { + assert(func); + std::shared_ptr data(user_data, free_cb); + auto cb = [func, data](const toolkit::mINI &ini) { func(data.get(), (mk_ini)&ini); }; + auto obj = std::make_shared(); + auto &val = *obj; + + val["object.MediaSource"] = ObjectStatistic::count(); + val["object.MultiMediaSourceMuxer"] = ObjectStatistic::count(); + + val["object.TcpServer"] = ObjectStatistic::count(); + val["object.TcpSession"] = ObjectStatistic::count(); + val["object.UdpServer"] = ObjectStatistic::count(); + val["object.UdpSession"] = ObjectStatistic::count(); + val["object.TcpClient"] = ObjectStatistic::count(); + val["object.Socket"] = ObjectStatistic::count(); + + val["object.FrameImp"] = ObjectStatistic::count(); + val["object.Frame"] = ObjectStatistic::count(); + + val["object.Buffer"] = ObjectStatistic::count(); + val["object.BufferRaw"] = ObjectStatistic::count(); + val["object.BufferLikeString"] = ObjectStatistic::count(); + val["object.BufferList"] = ObjectStatistic::count(); + + val["object.RtpPacket"] = ObjectStatistic::count(); + val["object.RtmpPacket"] = ObjectStatistic::count(); +#ifdef ENABLE_MEM_DEBUG + auto bytes = getTotalMemUsage(); + val["memory.memUsage"] = bytes; + val["memory.memUsageMB"] = (int)(bytes / 1024 / 1024); + val["memory.memBlock"] = getTotalMemBlock(); + static auto block_type_size = getBlockTypeSize(); + { + int i = 0; + string str; + size_t last = 0; + for (auto sz : block_type_size) { + str.append(to_string(last) + "~" + to_string(sz) + ":" + to_string(getTotalMemBlockByType(i++)) + ";"); + last = sz; + } + str.pop_back(); + val["memory.memBlockTypeCount"] = str; + } +#endif + + auto thread_size = EventPollerPool::Instance().getExecutorSize() + WorkThreadPool::Instance().getExecutorSize(); + std::shared_ptr> thread_mem_info = std::make_shared>(thread_size); + + shared_ptr finished(nullptr, [thread_mem_info, cb, obj](void *) { + for (auto &val : *thread_mem_info) { + auto thread_name = val["name"]; + replace(thread_name, "...", "~~~"); + auto prefix = "thread-" + thread_name + "."; + for (auto &pr : val) { + (*obj).emplace(prefix + pr.first, std::move(pr.second)); + } + } + // 触发回调 + cb(*obj); + }); + + auto pos = 0; + auto lambda = [&](const TaskExecutor::Ptr &executor) { + auto &val = (*thread_mem_info)[pos++]; + val["load"] = executor->load(); + Ticker ticker; + executor->async([finished, &val, ticker]() { + val["name"] = getThreadName(); + val["delay"] = ticker.elapsedTime(); +#ifdef ENABLE_MEM_DEBUG + auto bytes = getThisThreadMemUsage(); + val["memUsage"] = bytes; + val["memUsageMB"] = bytes / 1024 / 1024; + val["memBlock"] = getThisThreadMemBlock(); + { + int i = 0; + string str; + size_t last = 0; + for (auto sz : block_type_size) { + str.append(to_string(last) + "~" + to_string(sz) + ":" + to_string(getThisThreadMemBlockByType(i++)) + ";"); + last = sz; + } + str.pop_back(); + val["memBlockTypeCount"] = str; + } +#endif + }); + }; + EventPollerPool::Instance().for_each(lambda); + WorkThreadPool::Instance().for_each(lambda); +} + API_EXPORT void API_CALL mk_log_printf(int level, const char *file, const char *function, int line, const char *fmt, ...) { va_list ap; va_start(ap, fmt); diff --git a/api/tests/server.c b/api/tests/server.c index 28f44ada..a61862ed 100644 --- a/api/tests/server.c +++ b/api/tests/server.c @@ -189,6 +189,14 @@ static void on_mk_webrtc_get_answer_sdp_func(void *user_data, const char *answer free((void *)answer); } } + +void API_CALL on_get_statistic_cb(void *user_data, mk_ini ini) { + const char *response_header[] = { NULL }; + char *str = mk_ini_dump_string(ini); + mk_http_response_invoker_do_string(user_data, 200, response_header, str); + mk_free(str); +} + /** * 收到http api请求广播(包括GET/POST) * @param parser http请求内容对象 @@ -247,6 +255,9 @@ void API_CALL on_mk_http_request(const mk_parser parser, mk_webrtc_get_answer_sdp(mk_http_response_invoker_clone(invoker), on_mk_webrtc_get_answer_sdp_func, mk_parser_get_url_param(parser, "type"), mk_parser_get_content(parser, NULL), rtc_url); + } else if (strcmp(url, "/index/api/getStatistic") == 0) { + //拦截api: /index/api/webrtc + mk_get_statistic(on_get_statistic_cb, mk_http_response_invoker_clone(invoker), (on_user_data_free) mk_http_response_invoker_clone_release); } else { *consumed = 0; return; From 968f6c69c3a0c67bb1ca6fa4d9144b69410b181c Mon Sep 17 00:00:00 2001 From: Lidaofu <746101210@qq.com> Date: Sun, 19 May 2024 10:34:46 +0800 Subject: [PATCH 59/60] =?UTF-8?q?=E4=BF=AE=E5=A4=8DWindows=E4=B8=8B?= =?UTF-8?q?=E9=93=BE=E6=8E=A5OpenSSL=E9=9D=99=E6=80=81=E5=BA=93=E7=BC=BA?= =?UTF-8?q?=E5=B0=91Crypt32=E5=BA=93=E7=9A=84=E9=97=AE=E9=A2=98=20=20(#355?= =?UTF-8?q?2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 李道甫 --- CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index cd0124a9..44e03587 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -403,6 +403,8 @@ if(OPENSSL_FOUND AND ENABLE_OPENSSL) update_cached_list(MK_LINK_LIBRARIES ${OPENSSL_LIBRARIES}) if(CMAKE_SYSTEM_NAME MATCHES "Linux" AND OPENSSL_USE_STATIC_LIBS) update_cached_list(MK_LINK_LIBRARIES ${CMAKE_DL_LIBS}) + elseif(CMAKE_SYSTEM_NAME MATCHES "Windows" AND OPENSSL_USE_STATIC_LIBS) + update_cached_list(MK_LINK_LIBRARIES Crypt32) endif() else() set(ENABLE_OPENSSL OFF) From 373620cfc6289d504bcf5a01cef62a5f3abda0be Mon Sep 17 00:00:00 2001 From: KkemChen Date: Thu, 23 May 2024 10:30:11 +0800 Subject: [PATCH 60/60] feat: add broadcast event for player count change (#3562) --- conf/config.ini | 2 ++ src/Common/MediaSource.cpp | 4 ++++ src/Common/config.cpp | 3 +++ src/Common/config.h | 6 ++++++ 4 files changed, 15 insertions(+) diff --git a/conf/config.ini b/conf/config.ini index 89800db2..95d3fa37 100644 --- a/conf/config.ini +++ b/conf/config.ini @@ -126,6 +126,8 @@ wait_track_ready_ms=10000 wait_add_track_ms=3000 #如果track未就绪,我们先缓存帧数据,但是有最大个数限制,防止内存溢出 unready_frame_cache=100 +#是否启用观看人数变化事件广播,置1则启用,置0则关闭 +broadcast_player_count_changed=0 [hls] #hls写文件的buf大小,调整参数可以提高文件io性能 diff --git a/src/Common/MediaSource.cpp b/src/Common/MediaSource.cpp index 8de8202a..039b156a 100644 --- a/src/Common/MediaSource.cpp +++ b/src/Common/MediaSource.cpp @@ -652,6 +652,10 @@ MediaSource::Ptr MediaSource::createFromMP4(const string &schema, const string & /////////////////////////////////////MediaSourceEvent////////////////////////////////////// void MediaSourceEvent::onReaderChanged(MediaSource &sender, int size){ + GET_CONFIG(bool, enable, General::kBroadcastPlayerCountChanged); + if (enable) { + NOTICE_EMIT(BroadcastPlayerCountChangedArgs, Broadcast::kBroadcastPlayerCountChanged, sender.getMediaTuple(), sender.totalReaderCount()); + } if (size || sender.totalReaderCount()) { //还有人观看该视频,不触发关闭事件 _async_close_timer = nullptr; diff --git a/src/Common/config.cpp b/src/Common/config.cpp index 8b7031ae..f2ec0354 100644 --- a/src/Common/config.cpp +++ b/src/Common/config.cpp @@ -64,6 +64,7 @@ const string kBroadcastRtcSctpFailed = "kBroadcastRtcSctpFailed"; const string kBroadcastRtcSctpClosed = "kBroadcastRtcSctpClosed"; const string kBroadcastRtcSctpSend = "kBroadcastRtcSctpSend"; const string kBroadcastRtcSctpReceived = "kBroadcastRtcSctpReceived"; +const string kBroadcastPlayerCountChanged = "kBroadcastPlayerCountChanged"; } // namespace Broadcast @@ -82,6 +83,7 @@ const string kEnableFFmpegLog = GENERAL_FIELD "enable_ffmpeg_log"; const string kWaitTrackReadyMS = GENERAL_FIELD "wait_track_ready_ms"; const string kWaitAddTrackMS = GENERAL_FIELD "wait_add_track_ms"; const string kUnreadyFrameCache = GENERAL_FIELD "unready_frame_cache"; +const string kBroadcastPlayerCountChanged = GENERAL_FIELD "broadcast_player_count_changed"; static onceToken token([]() { mINI::Instance()[kFlowThreshold] = 1024; @@ -96,6 +98,7 @@ static onceToken token([]() { mINI::Instance()[kWaitTrackReadyMS] = 10000; mINI::Instance()[kWaitAddTrackMS] = 3000; mINI::Instance()[kUnreadyFrameCache] = 100; + mINI::Instance()[kBroadcastPlayerCountChanged] = 0; }); } // namespace General diff --git a/src/Common/config.h b/src/Common/config.h index b10fe685..4b6423a5 100644 --- a/src/Common/config.h +++ b/src/Common/config.h @@ -124,6 +124,10 @@ extern const std::string kBroadcastRtcSctpSend; extern const std::string kBroadcastRtcSctpReceived; #define BroadcastRtcSctpReceivedArgs WebRtcTransport& sender, uint16_t &streamId, uint32_t &ppid, const uint8_t *&msg, size_t &len +// 观看人数变化广播 +extern const std::string kBroadcastPlayerCountChanged; +#define BroadcastPlayerCountChangedArgs const MediaTuple& args, const int& count + #define ReloadConfigTag ((void *)(0xFF)) #define RELOAD_KEY(arg, key) \ do { \ @@ -196,6 +200,8 @@ extern const std::string kWaitTrackReadyMS; extern const std::string kWaitAddTrackMS; // 如果track未就绪,我们先缓存帧数据,但是有最大个数限制(100帧时大约4秒),防止内存溢出 extern const std::string kUnreadyFrameCache; +// 是否启用观看人数变化事件广播,置1则启用,置0则关闭 +extern const std::string kBroadcastPlayerCountChanged; } // namespace General namespace Protocol {