diff --git a/3rdpart/ZLToolKit b/3rdpart/ZLToolKit index fca6d232..04d1c47d 160000 --- a/3rdpart/ZLToolKit +++ b/3rdpart/ZLToolKit @@ -1 +1 @@ -Subproject commit fca6d2328871fc6af75e215f89c3f1092ba5bb21 +Subproject commit 04d1c47d2568f5ce1ff84260cefaf2754e514a5e diff --git a/3rdpart/media-server b/3rdpart/media-server index 043853ee..527c0f51 160000 --- a/3rdpart/media-server +++ b/3rdpart/media-server @@ -1 +1 @@ -Subproject commit 043853ee7c004e3e5d5bf3d06f7a82d97155b0d1 +Subproject commit 527c0f5117b489fda78fcd123d446370ddd9ec9a 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/CMakeLists.txt b/CMakeLists.txt index 96e79890..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}") @@ -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 () 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: 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/api/include/mk_events.h b/api/include/mk_events.h index 7e18263b..8f433a2a 100644 --- a/api/include/mk_events.h +++ b/api/include/mk_events.h @@ -177,6 +177,33 @@ typedef struct { */ 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); + + /** + * rtc数据通道发送数据回调 + * @param rtc_transport 数据通道对象 + * @param msg 数据 + * @param len 数据长度 + */ + void(API_CALL *on_mk_rtc_sctp_send)(mk_rtc_transport rtc_transport, const uint8_t *msg, size_t len); + + /** + * rtc数据通道接收数据回调 + * @param rtc_transport 数据通道对象 + * @param streamId 流id + * @param ppid 协议id + * @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); + } mk_events; diff --git a/api/include/mk_events_objects.h b/api/include/mk_events_objects.h index ad039b5c..d44ed666 100644 --- a/api/include/mk_events_objects.h +++ b/api/include/mk_events_objects.h @@ -352,6 +352,20 @@ API_EXPORT mk_auth_invoker API_CALL mk_auth_invoker_clone(const mk_auth_invoker */ API_EXPORT void API_CALL mk_auth_invoker_clone_release(const mk_auth_invoker ctx); +///////////////////////////////////////////WebRtcTransport///////////////////////////////////////////// +//WebRtcTransport对象的C映射 +typedef struct mk_rtc_transport_t *mk_rtc_transport; + +/** + * 发送rtc数据通道 + * @param ctx 数据通道对象 + * @param streamId 流id + * @param ppid 协议id + * @param msg 数据 + * @param 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 __cplusplus } #endif diff --git a/api/source/mk_common.cpp b/api/source/mk_common.cpp index 1f94cf28..48efeb1d 100644 --- a/api/source/mk_common.cpp +++ b/api/source/mk_common.cpp @@ -33,12 +33,11 @@ static TcpServer::Ptr shell_server; #ifdef ENABLE_RTPPROXY #include "Rtp/RtpServer.h" -static std::shared_ptr rtpServer; +static RtpServer::Ptr rtpServer; #endif #ifdef ENABLE_WEBRTC #include "../webrtc/WebRtcSession.h" -#include "../webrtc/WebRtcTransport.h" static UdpServer::Ptr rtcServer_udp; static TcpServer::Ptr rtcServer_tcp; #endif @@ -305,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](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/api/source/mk_events.cpp b/api/source/mk_events.cpp index ae454e60..7f5ed661 100644 --- a/api/source/mk_events.cpp +++ b/api/source/mk_events.cpp @@ -15,6 +15,10 @@ #include "Rtsp/RtspSession.h" #include "Record/MP4Recorder.h" +#ifdef ENABLE_WEBRTC +#include "webrtc/WebRtcTransport.h" +#endif + using namespace toolkit; using namespace mediakit; @@ -167,6 +171,43 @@ 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); + } + }); + + NoticeCenter::Instance().addListener(&s_tag, Broadcast::kBroadcastRtcSctpConnected,[](BroadcastRtcSctpConnectArgs){ + if (s_events.on_mk_rtc_sctp_connected) { + s_events.on_mk_rtc_sctp_connected((mk_rtc_transport)&sender); + } + }); + + NoticeCenter::Instance().addListener(&s_tag, Broadcast::kBroadcastRtcSctpFailed,[](BroadcastRtcSctpConnectArgs){ + if (s_events.on_mk_rtc_sctp_failed) { + s_events.on_mk_rtc_sctp_failed((mk_rtc_transport)&sender); + } + }); + + NoticeCenter::Instance().addListener(&s_tag, Broadcast::kBroadcastRtcSctpClosed,[](BroadcastRtcSctpConnectArgs){ + if (s_events.on_mk_rtc_sctp_closed) { + s_events.on_mk_rtc_sctp_closed((mk_rtc_transport)&sender); + } + }); + + NoticeCenter::Instance().addListener(&s_tag, Broadcast::kBroadcastRtcSctpSend,[](BroadcastRtcSctpSendArgs){ + if (s_events.on_mk_rtc_sctp_send) { + s_events.on_mk_rtc_sctp_send((mk_rtc_transport)&sender, data, len); + } + }); + + NoticeCenter::Instance().addListener(&s_tag, Broadcast::kBroadcastRtcSctpReceived,[](BroadcastRtcSctpReceivedArgs){ + if (s_events.on_mk_rtc_sctp_received) { + 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 1d74e0e1..c30fd0cd 100644 --- a/api/source/mk_events_objects.cpp +++ b/api/source/mk_events_objects.cpp @@ -18,6 +18,10 @@ #include "Http/HttpClient.h" #include "Rtsp/RtspSession.h" +#ifdef ENABLE_WEBRTC +#include "webrtc/WebRtcTransport.h" +#endif + using namespace toolkit; using namespace mediakit; @@ -497,4 +501,22 @@ API_EXPORT void API_CALL mk_auth_invoker_clone_release(const mk_auth_invoker ctx assert(ctx); Broadcast::AuthInvoker *invoker = (Broadcast::AuthInvoker *)ctx; delete invoker; -} \ No newline at end of file +} + +///////////////////////////////////////////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) { +#ifdef ENABLE_WEBRTC + assert(ctx && msg); + WebRtcTransport *transport = (WebRtcTransport *)ctx; + std::string msg_str(msg, len); + std::weak_ptr weak_trans = transport->shared_from_this(); + transport->getPoller()->async([streamId, ppid, msg_str, weak_trans]() { + // 切换线程后再操作 + if (auto trans = weak_trans.lock()) { + trans->sendDatachannel(streamId, ppid, msg_str.c_str(), msg_str.size()); + } + }); +#else + WarnL << "未启用webrtc功能, 编译时请开启ENABLE_WEBRTC"; +#endif +} diff --git a/conf/config.ini b/conf/config.ini index 4364ed99..3c8b67c8 100644 --- a/conf/config.ini +++ b/conf/config.ini @@ -277,6 +277,8 @@ sampleMS=500 fastStart=0 #MP4点播(rtsp/rtmp/http-flv/ws-flv)是否循环播放文件 fileRepeat=0 +#MP4录制写文件格式是否采用fmp4,启用的话,断电未完成录制的文件也能正常打开 +enableFmp4=0 [rtmp] #rtmp必须在此时间内完成握手,否则服务器会断开链接,单位秒 @@ -329,6 +331,13 @@ opus_pt=100 #如果不调用startSendRtp相关接口,可以置0节省内存 gop_cache=1 +#国标发送g711 rtp 打包时,每个包的语音时长是多少,默认是100 ms,范围为20~180ms (gb28181-2016,c.2.4规定), +#最好为20 的倍数,程序自动向20的倍数取整 +rtp_g711_dur_ms = 100 +#udp接收数据socket buffer大小配置 +#4*1024*1024=4196304 +udp_recv_socket_buffer=4194304 + [rtc] #rtc播放推流、播放超时时间 timeoutSec=15 @@ -348,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/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<:::::::::::::::::::::::size() > 2); + CHECK_RET(pkt->size() > 2); if (pkt->isConfigFrame()) { getTrack()->setExtraData((uint8_t *)pkt->data() + 2, pkt->size() - 2); return; diff --git a/ext-codec/G711Rtp.cpp b/ext-codec/G711Rtp.cpp index 2760f8a6..16a9c5c2 100644 --- a/ext-codec/G711Rtp.cpp +++ b/ext-codec/G711Rtp.cpp @@ -8,13 +8,27 @@ G711RtpEncoder::G711RtpEncoder(CodecId codec, uint32_t channels){ _channels = channels; } +void G711RtpEncoder::setOpt(int opt, const toolkit::Any ¶m) { + if (opt == RTP_ENCODER_PKT_DUR_MS) { + if (param.is()) { + auto dur = param.get(); + if (dur < 20 || dur > 180) { + WarnL << "set g711 rtp encoder duration ms failed for " << dur; + return; + } + // 向上 20ms 取整 + _pkt_dur_ms = (dur + 19) / 20 * 20; + } + } +} + bool G711RtpEncoder::inputFrame(const Frame::Ptr &frame) { auto dur = (_cache_frame->size() - _cache_frame->prefixSize()) / (8 * _channels); auto next_pts = _cache_frame->pts() + dur; if (next_pts == 0) { _cache_frame->_pts = frame->pts(); } else { - if ((next_pts + 20) < frame->pts()) { // 有丢包超过20ms + if ((next_pts + _pkt_dur_ms) < frame->pts()) { // 有丢包超过20ms _cache_frame->_pts = frame->pts() - dur; } } @@ -24,24 +38,20 @@ bool G711RtpEncoder::inputFrame(const Frame::Ptr &frame) { auto ptr = _cache_frame->data() + _cache_frame->prefixSize(); auto len = _cache_frame->size() - _cache_frame->prefixSize(); auto remain_size = len; - auto max_size = 160 * _channels; // 20 ms per rtp - int n = 0; - bool mark = false; + size_t max_size = 160 * _channels * _pkt_dur_ms / 20; // 20 ms per 160 byte + size_t n = 0; + bool mark = true; while (remain_size >= max_size) { - size_t rtp_size; - if (remain_size >= max_size) { - rtp_size = max_size; - } else { - break; - } + assert(remain_size >= max_size); + const size_t rtp_size = max_size; n++; - stamp += 20; - RtpCodec::inputRtp(getRtpInfo().makeRtp(TrackAudio, ptr, rtp_size, mark, stamp), false); + stamp += _pkt_dur_ms; + RtpCodec::inputRtp(getRtpInfo().makeRtp(TrackAudio, ptr, rtp_size, mark, stamp), true); ptr += rtp_size; remain_size -= rtp_size; } _cache_frame->_buffer.erase(0, n * max_size); - _cache_frame->_pts += 20 * n; + _cache_frame->_pts += (uint64_t)_pkt_dur_ms * n; return len > 0; } diff --git a/ext-codec/G711Rtp.h b/ext-codec/G711Rtp.h index 2d709e53..dab74f42 100644 --- a/ext-codec/G711Rtp.h +++ b/ext-codec/G711Rtp.h @@ -36,8 +36,11 @@ public: */ bool inputFrame(const Frame::Ptr &frame) override; + void setOpt(int opt, const toolkit::Any ¶m) override; + private: uint32_t _channels = 1; + uint32_t _pkt_dur_ms = 20; FrameImp::Ptr _cache_frame; }; 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/player/CMakeLists.txt b/player/CMakeLists.txt index 5b332bb9..e36b2a46 100644 --- a/player/CMakeLists.txt +++ b/player/CMakeLists.txt @@ -31,7 +31,9 @@ if(PKG_CONFIG_FOUND) list(APPEND LINK_LIBRARIES PkgConfig::SDL2) message(STATUS "found library: ${SDL2_LIBRARIES}") endif() -else() +endif() + +if(NOT SDL2_FOUND) find_package(SDL2 QUIET) if(SDL2_FOUND) include_directories(SYSTEM ${SDL2_INCLUDE_DIR}) diff --git a/postman/ZLMediaKit.postman_collection.json b/postman/ZLMediaKit.postman_collection.json index a6dc237f..ae3879b0 100644 --- a/postman/ZLMediaKit.postman_collection.json +++ b/postman/ZLMediaKit.postman_collection.json @@ -1,11 +1,10 @@ { "info": { - "_postman_id": "08e3bc35-5318-4949-81bb-90d854706194", + "_postman_id": "8b3cdc62-3e18-4700-9ddd-dc9f58ebce83", "name": "ZLMediaKit", "description": "媒体服务器", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "29185956", - "_collection_link": "https://lively-station-598157.postman.co/workspace/%E6%B5%81%E5%AA%92%E4%BD%93%E6%9C%8D%E5%8A%A1~1e119172-45b0-4ed6-b1fc-8a15d0e2d5f8/collection/29185956-08e3bc35-5318-4949-81bb-90d854706194?action=share&source=collection_link&creator=29185956" + "_exporter_id": "26338564" }, "item": [ { @@ -34,6 +33,72 @@ }, "response": [] }, + { + "name": "关闭多屏拼接(stack/stop)", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{ZLMediaKit_URL}}/index/api/getApiList?secret={{ZLMediaKit_secret}}&id=stack_test", + "host": [ + "{{ZLMediaKit_URL}}" + ], + "path": [ + "index", + "api", + "getApiList" + ], + "query": [ + { + "key": "secret", + "value": "{{ZLMediaKit_secret}}", + "description": "api操作密钥(配置文件配置)" + }, + { + "key": "id", + "value": "stack_test" + } + ] + } + }, + "response": [] + }, + { + "name": "添加多屏拼接(stack/start)", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"gapv\": 0.002,\r\n \"gaph\": 0.001,\r\n \"width\": 1920,\r\n \"url\": [\r\n [\r\n \"rtsp://kkem.me/live/test3\",\r\n \"rtsp://kkem.me/live/cy1\",\r\n \"rtsp://kkem.me/live/cy1\",\r\n \"rtsp://kkem.me/live/cy2\"\r\n ],\r\n [\r\n \"rtsp://kkem.me/live/cy1\",\r\n \"rtsp://kkem.me/live/cy5\",\r\n \"rtsp://kkem.me/live/cy3\",\r\n \"rtsp://kkem.me/live/cy4\"\r\n ],\r\n [\r\n \"rtsp://kkem.me/live/cy5\",\r\n \"rtsp://kkem.me/live/cy6\",\r\n \"rtsp://kkem.me/live/cy7\",\r\n \"rtsp://kkem.me/live/cy8\"\r\n ],\r\n [\r\n \"rtsp://kkem.me/live/cy9\",\r\n \"rtsp://kkem.me/live/cy10\",\r\n \"rtsp://kkem.me/live/cy11\",\r\n \"rtsp://kkem.me/live/cy12\"\r\n ]\r\n ],\r\n \"id\": \"89\",\r\n \"row\": 4,\r\n \"col\": 4,\r\n \"height\": 1080,\r\n \"span\": [\r\n [\r\n [\r\n 0,\r\n 0\r\n ],\r\n [\r\n 1,\r\n 1\r\n ]\r\n ],\r\n [\r\n [\r\n 3,\r\n 0\r\n ],\r\n [\r\n 3,\r\n 1\r\n ]\r\n ],\r\n [\r\n [\r\n 2,\r\n 3\r\n ],\r\n [\r\n 3,\r\n 3\r\n ]\r\n ]\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{ZLMediaKit_URL}}/index/api/stack/start?secret={{ZLMediaKit_secret}}", + "host": [ + "{{ZLMediaKit_URL}}" + ], + "path": [ + "index", + "api", + "stack", + "start" + ], + "query": [ + { + "key": "secret", + "value": "{{ZLMediaKit_secret}}", + "description": "api操作密钥(配置文件配置)" + } + ] + } + }, + "response": [] + }, { "name": "获取网络线程负载(getThreadsLoad)", "request": { @@ -1470,9 +1535,9 @@ "disabled": true }, { - "key": "only_audio", + "key": "only_track", "value": "1", - "description": "是否为单音频track,用于语音对讲", + "description": "是否为单音频/单视频track,0:不设置,1:单音频,2:单视频", "disabled": true }, { @@ -1523,9 +1588,9 @@ "description": "该端口绑定的流id\n" }, { - "key": "only_audio", + "key": "only_track", "value": "0", - "description": "是否为单音频track,用于语音对讲", + "description": "是否为单音频/单视频track,0:不设置,1:单音频,2:单视频", "disabled": true }, { diff --git a/server/VideoStack.cpp b/server/VideoStack.cpp new file mode 100644 index 00000000..d5168147 --- /dev/null +++ b/server/VideoStack.cpp @@ -0,0 +1,592 @@ +#if defined(ENABLE_X264) && defined(ENABLE_FFMPEG) +#include "VideoStack.h" +#include "Codec/Transcode.h" +#include "Common/Device.h" +#include "Util/logger.h" +#include "Util/util.h" +#include "json/value.h" +#include +#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(); + _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); }); + }); +} + +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([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); + InfoL << "VideoStack stop: " << id; + 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(round(width * gaph)); + int gapvPix = static_cast(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 diff --git a/server/VideoStack.h b/server/VideoStack.h new file mode 100644 index 00000000..99455b40 --- /dev/null +++ b/server/VideoStack.h @@ -0,0 +1,208 @@ +#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; + toolkit::EventPoller::Ptr _poller; +}; + +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 f13fd4f4..29fc773d 100755 --- a/server/WebApi.cpp +++ b/server/WebApi.cpp @@ -59,7 +59,11 @@ #endif #if defined(ENABLE_VERSION) -#include "version.h" +#include "ZLMVersion.h" +#endif + +#if defined(ENABLE_X264) && defined (ENABLE_FFMPEG) +#include "VideoStack.h" #endif using namespace std; @@ -115,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); }; } @@ -143,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); }; } @@ -163,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); }; } @@ -297,22 +301,71 @@ static inline void addHttpListener(){ }); } +template +class ServiceController { +public: + using Pointer = std::shared_ptr; + std::unordered_map _map; + mutable std::recursive_mutex _mtx; + + void clear() { + decltype(_map) copy; + { + std::lock_guard lck(_mtx); + copy.swap(_map); + } + } + + size_t erase(const std::string &key) { + std::lock_guard lck(_mtx); + return _map.erase(key); + } + + Pointer find(const std::string &key) const { + std::lock_guard lck(_mtx); + auto it = _map.find(key); + if (it == _map.end()) { + return nullptr; + } + return it->second; + } + + template + Pointer make(const std::string &key, _Args&& ...__args) { + // assert(!find(key)); + + auto server = std::make_shared(std::forward<_Args>(__args)...); + std::lock_guard lck(_mtx); + auto it = _map.emplace(key, server); + assert(it.second); + return server; + } + + template + Pointer makeWithAction(const std::string &key, function action, _Args&& ...__args) { + // assert(!find(key)); + + auto server = std::make_shared(std::forward<_Args>(__args)...); + action(server); + std::lock_guard lck(_mtx); + auto it = _map.emplace(key, server); + assert(it.second); + return server; + } +}; + //拉流代理器列表 -static unordered_map s_proxyMap; -static recursive_mutex s_proxyMapMtx; +static ServiceController s_player_proxy; //推流代理器列表 -static unordered_map s_proxyPusherMap; -static recursive_mutex s_proxyPusherMapMtx; +static ServiceController s_pusher_proxy; //FFmpeg拉流代理器列表 -static unordered_map s_ffmpegMap; -static recursive_mutex s_ffmpegMapMtx; +static ServiceController s_ffmpeg_src; #if defined(ENABLE_RTPPROXY) //rtp服务器列表 -static unordered_map s_rtpServerMap; -static recursive_mutex s_rtpServerMapMtx; +static ServiceController s_rtp_server; #endif static inline string getProxyKey(const string &vhost, const string &app, const string &stream) { @@ -415,47 +468,24 @@ Value makeMediaSourceJson(MediaSource &media){ } #if defined(ENABLE_RTPPROXY) -uint16_t openRtpServer(uint16_t local_port, const string &stream_id, int tcp_mode, const string &local_ip, bool re_use_port, uint32_t ssrc, bool only_audio, bool multiplex) { - lock_guard lck(s_rtpServerMapMtx); - if (s_rtpServerMap.find(stream_id) != s_rtpServerMap.end()) { +uint16_t openRtpServer(uint16_t local_port, const string &stream_id, int tcp_mode, const string &local_ip, bool re_use_port, uint32_t ssrc, int only_track, bool multiplex) { + if (s_rtp_server.find(stream_id)) { //为了防止RtpProcess所有权限混乱的问题,不允许重复添加相同的stream_id return 0; } - RtpServer::Ptr server = std::make_shared(); - server->start(local_port, stream_id, (RtpServer::TcpMode)tcp_mode, local_ip.c_str(), re_use_port, ssrc, only_audio, multiplex); + auto server = s_rtp_server.makeWithAction(stream_id, [&](RtpServer::Ptr server) { + server->start(local_port, stream_id, (RtpServer::TcpMode)tcp_mode, local_ip.c_str(), re_use_port, ssrc, only_track, multiplex); + }); server->setOnDetach([stream_id]() { //设置rtp超时移除事件 - lock_guard lck(s_rtpServerMapMtx); - s_rtpServerMap.erase(stream_id); + s_rtp_server.erase(stream_id); }); - //保存对象 - s_rtpServerMap.emplace(stream_id, server); //回复json return server->getPort(); } -void connectRtpServer(const string &stream_id, const string &dst_url, uint16_t dst_port, const function &cb) { - lock_guard lck(s_rtpServerMapMtx); - auto it = s_rtpServerMap.find(stream_id); - if (it == s_rtpServerMap.end()) { - cb(SockException(Err_other, "未找到rtp服务")); - return; - } - it->second->connectToServer(dst_url, dst_port, cb); -} - -bool closeRtpServer(const string &stream_id) { - lock_guard lck(s_rtpServerMapMtx); - auto it = s_rtpServerMap.find(stream_id); - if (it == s_rtpServerMap.end()) { - return false; - } - auto server = it->second; - s_rtpServerMap.erase(it); - return true; -} #endif void getStatisticJson(const function &cb) { @@ -546,23 +576,23 @@ void addStreamProxy(const string &vhost, const string &app, const string &stream const ProtocolOption &option, int rtp_type, float timeout_sec, const mINI &args, const function &cb) { auto key = getProxyKey(vhost, app, stream); - lock_guard lck(s_proxyMapMtx); - if (s_proxyMap.find(key) != s_proxyMap.end()) { + if (s_player_proxy.find(key)) { //已经在拉流了 cb(SockException(Err_other, "This stream already exists"), key); return; } //添加拉流代理 - auto player = std::make_shared(vhost, app, stream, option, retry_count); - s_proxyMap[key] = player; + 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; - if (timeout_sec > 0.1) { + if (timeout_sec > 0.1f) { //播放握手超时时间 (*player)[Client::kTimeoutMS] = timeout_sec * 1000; } @@ -570,28 +600,69 @@ void addStreamProxy(const string &vhost, const string &app, const string &stream //开始播放,如果播放失败或者播放中止,将会自动重试若干次,默认一直重试 player->setPlayCallbackOnce([cb, key](const SockException &ex) { if (ex) { - lock_guard lck(s_proxyMapMtx); - s_proxyMap.erase(key); + s_player_proxy.erase(key); } cb(ex, key); }); //被主动关闭拉流 player->setOnClose([key](const SockException &ex) { - lock_guard lck(s_proxyMapMtx); - s_proxyMap.erase(key); + s_player_proxy.erase(key); }); player->play(url); }; -template -static void getArgsValue(const HttpAllArgs &allArgs, const string &key, Type &value) { - auto val = allArgs[key]; - if (!val.empty()) { - value = (Type)val; + +void addStreamPusherProxy(const string &schema, + const string &vhost, + const string &app, + const string &stream, + const string &url, + int retry_count, + int rtp_type, + float timeout_sec, + const function &cb) { + auto key = getPusherKey(schema, vhost, app, stream, url); + auto src = MediaSource::find(schema, vhost, app, stream); + if (!src) { + cb(SockException(Err_other, "can not find the source stream"), key); + return; } + if (s_pusher_proxy.find(key)) { + //已经在推流了 + cb(SockException(Err_success), key); + return; + } + + //添加推流代理 + auto pusher = s_pusher_proxy.make(key, src, retry_count); + + //指定RTP over TCP(播放rtsp时有效) + pusher->emplace(Client::kRtpType, rtp_type); + + if (timeout_sec > 0.1f) { + //推流握手超时时间 + pusher->emplace(Client::kTimeoutMS, timeout_sec * 1000); + } + + //开始推流,如果推流失败或者推流中止,将会自动重试若干次,默认一直重试 + pusher->setPushCallbackOnce([cb, key, url](const SockException &ex) { + if (ex) { + WarnL << "Push " << url << " failed, key: " << key << ", err: " << ex; + s_pusher_proxy.erase(key); + } + cb(ex, key); + }); + + //被主动关闭推流 + pusher->setOnClose([key, url](const SockException &ex) { + WarnL << "Push " << url << " failed, key: " << key << ", err: " << ex; + s_pusher_proxy.erase(key); + }); + pusher->publish(url); } + /** * 安装api接口 * 所有api都支持GET和POST两种方式 @@ -657,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 @@ -973,59 +1044,6 @@ void installWebApi() { val["count_hit"] = (Json::UInt64)count_hit; }); - static auto addStreamPusherProxy = [](const string &schema, - const string &vhost, - const string &app, - const string &stream, - const string &url, - int retry_count, - int rtp_type, - float timeout_sec, - const function &cb) { - auto key = getPusherKey(schema, vhost, app, stream, url); - auto src = MediaSource::find(schema, vhost, app, stream); - if (!src) { - cb(SockException(Err_other, "can not find the source stream"), key); - return; - } - lock_guard lck(s_proxyPusherMapMtx); - if (s_proxyPusherMap.find(key) != s_proxyPusherMap.end()) { - //已经在推流了 - cb(SockException(Err_success), key); - return; - } - - //添加推流代理 - auto pusher = std::make_shared(src, retry_count); - s_proxyPusherMap[key] = pusher; - - //指定RTP over TCP(播放rtsp时有效) - (*pusher)[Client::kRtpType] = rtp_type; - - if (timeout_sec > 0.1) { - //推流握手超时时间 - (*pusher)[Client::kTimeoutMS] = timeout_sec * 1000; - } - - //开始推流,如果推流失败或者推流中止,将会自动重试若干次,默认一直重试 - pusher->setPushCallbackOnce([cb, key, url](const SockException &ex) { - if (ex) { - WarnL << "Push " << url << " failed, key: " << key << ", err: " << ex; - lock_guard lck(s_proxyPusherMapMtx); - s_proxyPusherMap.erase(key); - } - cb(ex, key); - }); - - //被主动关闭推流 - pusher->setOnClose([key, url](const SockException &ex) { - WarnL << "Push " << url << " failed, key: " << key << ", err: " << ex; - lock_guard lck(s_proxyPusherMapMtx); - s_proxyPusherMap.erase(key); - }); - pusher->publish(url); - }; - //动态添加rtsp/rtmp推流代理 //测试url http://127.0.0.1/index/api/addStreamPusherProxy?schema=rtmp&vhost=__defaultVhost__&app=proxy&stream=0&dst_url=rtmp://127.0.0.1/live/obs api_regist("/index/api/addStreamPusherProxy", [](API_ARGS_MAP_ASYNC) { @@ -1058,8 +1076,7 @@ void installWebApi() { api_regist("/index/api/delStreamPusherProxy", [](API_ARGS_MAP) { CHECK_SECRET(); CHECK_ARGS("key"); - lock_guard lck(s_proxyPusherMapMtx); - val["data"]["flag"] = s_proxyPusherMap.erase(allArgs["key"]) == 1; + val["data"]["flag"] = s_pusher_proxy.erase(allArgs["key"]) == 1; }); //动态添加rtsp/rtmp拉流代理 @@ -1069,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); } @@ -1100,8 +1117,7 @@ void installWebApi() { api_regist("/index/api/delStreamProxy",[](API_ARGS_MAP){ CHECK_SECRET(); CHECK_ARGS("key"); - lock_guard lck(s_proxyMapMtx); - val["data"]["flag"] = s_proxyMap.erase(allArgs["key"]) == 1; + val["data"]["flag"] = s_player_proxy.erase(allArgs["key"]) == 1; }); static auto addFFmpegSource = [](const string &ffmpeg_cmd_key, @@ -1112,25 +1128,21 @@ void installWebApi() { bool enable_mp4, const function &cb) { auto key = MD5(dst_url).hexdigest(); - lock_guard lck(s_ffmpegMapMtx); - if (s_ffmpegMap.find(key) != s_ffmpegMap.end()) { + if (s_ffmpeg_src.find(key)) { //已经在拉流了 cb(SockException(Err_success), key); return; } - FFmpegSource::Ptr ffmpeg = std::make_shared(); - s_ffmpegMap[key] = ffmpeg; + auto ffmpeg = s_ffmpeg_src.make(key); ffmpeg->setOnClose([key]() { - lock_guard lck(s_ffmpegMapMtx); - s_ffmpegMap.erase(key); + s_ffmpeg_src.erase(key); }); ffmpeg->setupRecordFlag(enable_hls, enable_mp4); ffmpeg->play(ffmpeg_cmd_key, src_url, dst_url, timeout_ms, [cb, key](const SockException &ex) { if (ex) { - lock_guard lck(s_ffmpegMapMtx); - s_ffmpegMap.erase(key); + s_ffmpeg_src.erase(key); } cb(ex, key); }); @@ -1164,15 +1176,14 @@ void installWebApi() { api_regist("/index/api/delFFmpegSource",[](API_ARGS_MAP){ CHECK_SECRET(); CHECK_ARGS("key"); - lock_guard lck(s_ffmpegMapMtx); - val["data"]["flag"] = s_ffmpegMap.erase(allArgs["key"]) == 1; + val["data"]["flag"] = s_ffmpeg_src.erase(allArgs["key"]) == 1; }); //新增http api下载可执行程序文件接口 //测试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) @@ -1198,12 +1209,17 @@ void installWebApi() { //兼容老版本请求,新版本去除enable_tcp参数并新增tcp_mode参数 tcp_mode = 1; } + auto only_track = allArgs["only_track"].as(); + if (allArgs["only_audio"].as()) { + // 兼容老版本请求,新版本去除only_audio参数并新增only_track参数 + only_track = 1; + } std::string local_ip = "::"; if (!allArgs["local_ip"].empty()) { local_ip = allArgs["local_ip"]; } auto port = openRtpServer(allArgs["port"], stream_id, tcp_mode, local_ip, allArgs["re_use_port"].as(), - allArgs["ssrc"].as(), allArgs["only_audio"].as()); + allArgs["ssrc"].as(), only_track); if (port == 0) { throw InvalidArgsException("该stream_id已存在"); } @@ -1220,11 +1236,16 @@ void installWebApi() { // 兼容老版本请求,新版本去除enable_tcp参数并新增tcp_mode参数 tcp_mode = 1; } + auto only_track = allArgs["only_track"].as(); + if (allArgs["only_audio"].as()) { + // 兼容老版本请求,新版本去除only_audio参数并新增only_track参数 + only_track = 1; + } std::string local_ip = "::"; if (!allArgs["local_ip"].empty()) { local_ip = allArgs["local_ip"]; } - auto port = openRtpServer(allArgs["port"], stream_id, tcp_mode, local_ip, true, 0, allArgs["only_audio"].as(),true); + auto port = openRtpServer(allArgs["port"], stream_id, tcp_mode, local_ip, true, 0, only_track,true); if (port == 0) { throw InvalidArgsException("该stream_id已存在"); } @@ -1235,22 +1256,27 @@ void installWebApi() { api_regist("/index/api/connectRtpServer", [](API_ARGS_MAP_ASYNC) { CHECK_SECRET(); CHECK_ARGS("stream_id", "dst_url", "dst_port"); - connectRtpServer( - allArgs["stream_id"], allArgs["dst_url"], allArgs["dst_port"], - [val, headerOut, invoker](const SockException &ex) mutable { - if (ex) { - val["code"] = API::OtherFailed; - val["msg"] = ex.what(); - } - invoker(200, headerOut, val.toStyledString()); - }); + auto cb = [val, headerOut, invoker](const SockException &ex) mutable { + if (ex) { + val["code"] = API::OtherFailed; + val["msg"] = ex.what(); + } + invoker(200, headerOut, val.toStyledString()); + }; + + auto server = s_rtp_server.find(allArgs["stream_id"]); + if (!server) { + cb(SockException(Err_other, "未找到rtp服务")); + return; + } + server->connectToServer(allArgs["dst_url"], allArgs["dst_port"], cb); }); api_regist("/index/api/closeRtpServer",[](API_ARGS_MAP){ CHECK_SECRET(); CHECK_ARGS("stream_id"); - if(!closeRtpServer(allArgs["stream_id"])){ + if(s_rtp_server.erase(allArgs["stream_id"]) == 0){ val["hit"] = 0; return; } @@ -1261,19 +1287,18 @@ void installWebApi() { CHECK_SECRET(); CHECK_ARGS("stream_id", "ssrc"); - lock_guard lck(s_rtpServerMapMtx); - auto it = s_rtpServerMap.find(allArgs["stream_id"]); - if (it == s_rtpServerMap.end()) { + auto server = s_rtp_server.find(allArgs["stream_id"]); + if (!server) { throw ApiRetException("RtpServer not found by stream_id", API::NotFound); } - it->second->updateSSRC(allArgs["ssrc"]); + server->updateSSRC(allArgs["ssrc"]); }); api_regist("/index/api/listRtpServer",[](API_ARGS_MAP){ CHECK_SECRET(); - lock_guard lck(s_rtpServerMapMtx); - for (auto &pr : s_rtpServerMap) { + std::lock_guard lck(s_rtp_server._mtx); + for (auto &pr : s_rtp_server._map) { Value obj; obj["stream_id"] = pr.first; obj["port"] = pr.second->getPort(); @@ -1289,7 +1314,11 @@ void installWebApi() { if (!src) { throw ApiRetException("can not find the source stream", API::NotFound); } - + auto type = allArgs["type"].as(); + if (!allArgs["use_ps"].empty()) { + // 兼容之前的use_ps参数 + type = allArgs["use_ps"].as(); + } MediaSourceEvent::SendRtpArgs args; args.passive = false; args.dst_url = allArgs["dst_url"]; @@ -1299,11 +1328,11 @@ void installWebApi() { args.is_udp = allArgs["is_udp"]; args.src_port = allArgs["src_port"]; args.pt = allArgs["pt"].empty() ? 96 : allArgs["pt"].as(); - args.use_ps = allArgs["use_ps"].empty() ? true : allArgs["use_ps"].as(); + args.type = (MediaSourceEvent::SendRtpArgs::Type)type; args.only_audio = allArgs["only_audio"].as(); args.udp_rtcp_timeout = allArgs["udp_rtcp_timeout"]; args.recv_stream_id = allArgs["recv_stream_id"]; - TraceL << "startSendRtp, pt " << int(args.pt) << " ps " << args.use_ps << " audio " << args.only_audio; + TraceL << "startSendRtp, pt " << int(args.pt) << " rtp type " << type << " audio " << args.only_audio; src->getOwnerPoller()->async([=]() mutable { src->startSendRtp(args, [val, headerOut, invoker](uint16_t local_port, const SockException &ex) mutable { @@ -1325,6 +1354,11 @@ void installWebApi() { if (!src) { throw ApiRetException("can not find the source stream", API::NotFound); } + auto type = allArgs["type"].as(); + if (!allArgs["use_ps"].empty()) { + // 兼容之前的use_ps参数 + type = allArgs["use_ps"].as(); + } MediaSourceEvent::SendRtpArgs args; args.passive = true; @@ -1332,12 +1366,12 @@ void installWebApi() { args.is_udp = false; args.src_port = allArgs["src_port"]; args.pt = allArgs["pt"].empty() ? 96 : allArgs["pt"].as(); - args.use_ps = allArgs["use_ps"].empty() ? true : allArgs["use_ps"].as(); + args.type = (MediaSourceEvent::SendRtpArgs::Type)type; args.only_audio = allArgs["only_audio"].as(); args.recv_stream_id = allArgs["recv_stream_id"]; //tcp被动服务器等待链接超时时间 args.tcp_passive_close_delay_ms = allArgs["close_delay_ms"]; - TraceL << "startSendRtpPassive, pt " << int(args.pt) << " ps " << args.use_ps << " audio " << args.only_audio; + TraceL << "startSendRtpPassive, pt " << int(args.pt) << " rtp type " << type << " audio " << args.only_audio; src->getOwnerPoller()->async([=]() mutable { src->startSendRtp(args, [val, headerOut, invoker](uint16_t local_port, const SockException &ex) mutable { @@ -1499,18 +1533,11 @@ void installWebApi() { api_regist("/index/api/getProxyPusherInfo", [](API_ARGS_MAP_ASYNC) { CHECK_SECRET(); CHECK_ARGS("key"); - decltype(s_proxyPusherMap.end()) it; - { - lock_guard lck(s_proxyPusherMapMtx); - it = s_proxyPusherMap.find(allArgs["key"]); - } - - if (it == s_proxyPusherMap.end()) { + auto pusher = s_pusher_proxy.find(allArgs["key"]); + if (!pusher) { throw ApiRetException("can not find pusher", API::NotFound); } - auto pusher = it->second; - val["data"]["status"] = pusher->getStatus(); val["data"]["liveSecs"] = pusher->getLiveSecs(); val["data"]["rePublishCount"] = pusher->getRePublishCount(); @@ -1520,18 +1547,11 @@ void installWebApi() { api_regist("/index/api/getProxyInfo", [](API_ARGS_MAP_ASYNC) { CHECK_SECRET(); CHECK_ARGS("key"); - decltype(s_proxyMap.end()) it; - { - lock_guard lck(s_proxyMapMtx); - it = s_proxyMap.find(allArgs["key"]); - } - - if (it == s_proxyMap.end()) { + auto proxy = s_player_proxy.find(allArgs["key"]); + if (!proxy) { throw ApiRetException("can not find the proxy", API::NotFound); } - auto proxy = it->second; - val["data"]["status"] = proxy->getStatus(); val["data"]["liveSecs"] = proxy->getLiveSecs(); val["data"]["rePullCount"] = proxy->getRePullCount(); @@ -1670,7 +1690,7 @@ void installWebApi() { //截图存在,且未过期,那么返回之 res_old_snap = true; - responseSnap(path, allArgs.getParser().getHeader(), invoker); + responseSnap(path, allArgs.parser.getHeader(), invoker); //中断遍历 return false; }); @@ -1701,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); }); }); @@ -1716,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; @@ -1734,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](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()); @@ -1781,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](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); }); @@ -1808,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"); @@ -1858,7 +1862,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; @@ -1894,44 +1898,50 @@ 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"); } }); + +#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); + }); + + api_regist("/index/api/stack/start", [](API_ARGS_JSON_ASYNC) { + CHECK_SECRET(); + auto ret = VideoStackManager::Instance().startVideoStack(allArgs.args()); + 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"]); + val["code"] = ret; + val["msg"] = ret ? "failed" : "success"; + invoker(200, headerOut, val.toStyledString()); + }); +#endif } void unInstallWebApi(){ - { - lock_guard lck(s_proxyMapMtx); - auto proxyMap(std::move(s_proxyMap)); - proxyMap.clear(); - } - - { - lock_guard lck(s_ffmpegMapMtx); - auto ffmpegMap(std::move(s_ffmpegMap)); - ffmpegMap.clear(); - } - - { - lock_guard lck(s_proxyPusherMapMtx); - auto proxyPusherMap(std::move(s_proxyPusherMap)); - proxyPusherMap.clear(); - } - - { + s_player_proxy.clear(); + s_ffmpeg_src.clear(); + s_pusher_proxy.clear(); #if defined(ENABLE_RTPPROXY) - RtpSelector::Instance().clear(); - lock_guard lck(s_rtpServerMapMtx); - auto rtpServerMap(std::move(s_rtpServerMap)); - rtpServerMap.clear(); + s_rtp_server.clear(); #endif - } + NoticeCenter::Instance().delListener(&web_api_tag); } diff --git a/server/WebApi.h b/server/WebApi.h index 43516463..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 @@ -233,9 +202,7 @@ void installWebApi(); 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, bool only_audio, 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); +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); #endif Json::Value makeMediaSourceJson(mediakit::MediaSource &media); diff --git a/server/main.cpp b/server/main.cpp index c946beab..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) @@ -392,8 +392,8 @@ int start_main(int argc,char *argv[]) { #endif//defined(ENABLE_WEBRTC) #if defined(ENABLE_SRT) - // srt udp服务器 - if(srtPort) { srtSrv->start(srtPort); } + // srt udp服务器 + if (srtPort) { srtSrv->start(srtPort); } #endif//defined(ENABLE_SRT) } catch (std::exception &ex) { diff --git a/src/Common/MediaSink.cpp b/src/Common/MediaSink.cpp index 837024af..f888b297 100644 --- a/src/Common/MediaSink.cpp +++ b/src/Common/MediaSink.cpp @@ -133,7 +133,7 @@ void MediaSink::checkTrackIfReady() { } GET_CONFIG(uint32_t, kMaxAddTrackMS, General::kWaitAddTrackMS); - if (_track_map.size() == 1 && _ticker.elapsedTime() > kMaxAddTrackMS) { + if (_track_map.size() == 1 && (_ticker.elapsedTime() > kMaxAddTrackMS || !_enable_audio)) { // 如果只有一个Track,那么在该Track添加后,我们最多还等待若干时间(可能后面还会添加Track) emitAllTrackReady(); return; @@ -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"); } } diff --git a/src/Common/MediaSource.h b/src/Common/MediaSource.h index 4c6b33e9..b156954c 100644 --- a/src/Common/MediaSource.h +++ b/src/Common/MediaSource.h @@ -92,10 +92,11 @@ public: class SendRtpArgs { public: + enum Type { kRtpRAW = 0, kRtpPS = 1, kRtpTS = 2 }; // 是否采用udp方式发送rtp bool is_udp = true; - // rtp采用ps还是es方式 - bool use_ps = true; + // rtp类型 + Type type = kRtpPS; //发送es流时指定是否只发送纯音频流 bool only_audio = false; //tcp被动方式 @@ -135,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(); @@ -242,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/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(); diff --git a/src/Common/Parser.cpp b/src/Common/Parser.cpp index ad33f575..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::UrlDecodeComponent(std::move(user)); - _passwd = strCoding::UrlDecodeComponent(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/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/src/Common/config.cpp b/src/Common/config.cpp index 357a9689..a34ee84f 100644 --- a/src/Common/config.cpp +++ b/src/Common/config.cpp @@ -58,6 +58,12 @@ const string kBroadcastStreamNoneReader = "kBroadcastStreamNoneReader"; const string kBroadcastHttpBeforeAccess = "kBroadcastHttpBeforeAccess"; const string kBroadcastSendRtpStopped = "kBroadcastSendRtpStopped"; const string kBroadcastRtpServerTimeout = "kBroadcastRtpServerTimeout"; +const string kBroadcastRtcSctpConnecting = "kBroadcastRtcSctpConnecting"; +const string kBroadcastRtcSctpConnected = "kBroadcastRtcSctpConnected"; +const string kBroadcastRtcSctpFailed = "kBroadcastRtcSctpFailed"; +const string kBroadcastRtcSctpClosed = "kBroadcastRtcSctpClosed"; +const string kBroadcastRtcSctpSend = "kBroadcastRtcSctpSend"; +const string kBroadcastRtcSctpReceived = "kBroadcastRtcSctpReceived"; } // namespace Broadcast @@ -291,6 +297,7 @@ const string kSampleMS = RECORD_FIELD "sampleMS"; const string kFileBufSize = RECORD_FIELD "fileBufSize"; const string kFastStart = RECORD_FIELD "fastStart"; const string kFileRepeat = RECORD_FIELD "fileRepeat"; +const string kEnableFmp4 = RECORD_FIELD "enableFmp4"; static onceToken token([]() { mINI::Instance()[kAppName] = "record"; @@ -298,6 +305,7 @@ static onceToken token([]() { mINI::Instance()[kFileBufSize] = 64 * 1024; mINI::Instance()[kFastStart] = false; mINI::Instance()[kFileRepeat] = false; + mINI::Instance()[kEnableFmp4] = false; }); } // namespace Record @@ -338,6 +346,8 @@ const string kH265PT = RTP_PROXY_FIELD "h265_pt"; const string kPSPT = RTP_PROXY_FIELD "ps_pt"; const string kOpusPT = RTP_PROXY_FIELD "opus_pt"; const string kGopCache = RTP_PROXY_FIELD "gop_cache"; +const string kRtpG711DurMs = RTP_PROXY_FIELD "rtp_g711_dur_ms"; +const string kUdpRecvSocketBuffer = RTP_PROXY_FIELD "udp_recv_socket_buffer"; static onceToken token([]() { mINI::Instance()[kDumpDir] = ""; @@ -348,6 +358,8 @@ static onceToken token([]() { mINI::Instance()[kPSPT] = 96; mINI::Instance()[kOpusPT] = 100; mINI::Instance()[kGopCache] = 1; + mINI::Instance()[kRtpG711DurMs] = 100; + mINI::Instance()[kUdpRecvSocketBuffer] = 4 * 1024 * 1024; }); } // namespace RtpProxy diff --git a/src/Common/config.h b/src/Common/config.h index a6d7af4d..98ab289c 100644 --- a/src/Common/config.h +++ b/src/Common/config.h @@ -109,6 +109,21 @@ extern const std::string kBroadcastReloadConfig; extern const std::string kBroadcastRtpServerTimeout; #define BroadcastRtpServerTimeoutArgs uint16_t &local_port, const string &stream_id,int &tcp_mode, bool &re_use_port, uint32_t &ssrc +// rtc transport sctp 连接状态 +extern const std::string kBroadcastRtcSctpConnecting; +extern const std::string kBroadcastRtcSctpConnected; +extern const std::string kBroadcastRtcSctpFailed; +extern const std::string kBroadcastRtcSctpClosed; +#define BroadcastRtcSctpConnectArgs WebRtcTransport& sender + +// rtc transport sctp 发送数据 +extern const std::string kBroadcastRtcSctpSend; +#define BroadcastRtcSctpSendArgs WebRtcTransport& sender, const uint8_t *&data, size_t& len + +// rtc transport sctp 接收数据 +extern const std::string kBroadcastRtcSctpReceived; +#define BroadcastRtcSctpReceivedArgs WebRtcTransport& sender, uint16_t &streamId, uint32_t &ppid, const uint8_t *&msg, size_t &len + #define ReloadConfigTag ((void *)(0xFF)) #define RELOAD_KEY(arg, key) \ do { \ @@ -339,6 +354,8 @@ extern const std::string kFileBufSize; extern const std::string kFastStart; // mp4文件是否重头循环读取 extern const std::string kFileRepeat; +// mp4录制文件是否采用fmp4格式 +extern const std::string kEnableFmp4; } // namespace Record ////////////HLS相关配置/////////// @@ -382,6 +399,11 @@ extern const std::string kPSPT; extern const std::string kOpusPT; // RtpSender相关功能是否提前开启gop缓存优化级联秒开体验,默认开启 extern const std::string kGopCache; +//国标发送g711 rtp 打包时,每个包的语音时长是多少,默认是100 ms,范围为20~180ms (gb28181-2016,c.2.4规定), +//最好为20 的倍数,程序自动向20的倍数取整 +extern const std::string kRtpG711DurMs; +// udp recv socket buffer size +extern const std::string kUdpRecvSocketBuffer; } // namespace RtpProxy /** 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 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 diff --git a/src/Common/strCoding.cpp b/src/Common/strCoding.cpp index 90fc7096..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; @@ -79,7 +63,7 @@ string strCoding::UrlEncodePath(const string &str) { out.push_back(ch); } else { char buf[4]; - sprintf(buf, "%%%X%X", (uint8_t) ch >> 4, (uint8_t) ch & 0x0F); + snprintf(buf, 4, "%%%X%X", (uint8_t) ch >> 4, (uint8_t) ch & 0x0F); out.append(buf); } } @@ -96,39 +80,13 @@ string strCoding::UrlEncodeComponent(const string &str) { out.push_back(ch); } else { char buf[4]; - sprintf(buf, "%%%X%X", (uint8_t) ch >> 4, (uint8_t) ch & 0x0F); + snprintf(buf, 4, "%%%X%X", (uint8_t) ch >> 4, (uint8_t) ch & 0x0F); out.append(buf); } } 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/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); 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); diff --git a/src/Record/HlsMakerImp.cpp b/src/Record/HlsMakerImp.cpp index e987f820..d62c4398 100644 --- a/src/Record/HlsMakerImp.cpp +++ b/src/Record/HlsMakerImp.cpp @@ -72,7 +72,7 @@ void HlsMakerImp::clearCache(bool immediately, bool eof) { std::list lst; lst.emplace_back(_path_hls); lst.emplace_back(_path_hls_delay); - if (!_path_init.empty()) { + if (!_path_init.empty() && eof) { lst.emplace_back(_path_init); } for (auto &pr : _segment_file_paths) { diff --git a/src/Record/MP4Muxer.cpp b/src/Record/MP4Muxer.cpp index 5d40be1d..46fda66f 100644 --- a/src/Record/MP4Muxer.cpp +++ b/src/Record/MP4Muxer.cpp @@ -31,7 +31,8 @@ void MP4Muxer::openMP4(const string &file) { MP4FileIO::Writer MP4Muxer::createWriter() { GET_CONFIG(bool, mp4FastStart, Record::kFastStart); - return _mp4_file->createWriter(mp4FastStart ? MOV_FLAG_FASTSTART : 0, false); + GET_CONFIG(bool, recordEnableFmp4, Record::kEnableFmp4); + return _mp4_file->createWriter(mp4FastStart ? MOV_FLAG_FASTSTART : 0, recordEnableFmp4); } void MP4Muxer::closeMP4() { 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为空 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); diff --git a/src/Rtp/PSEncoder.cpp b/src/Rtp/PSEncoder.cpp index 66e39bc7..9de7f43b 100644 --- a/src/Rtp/PSEncoder.cpp +++ b/src/Rtp/PSEncoder.cpp @@ -19,9 +19,17 @@ using namespace toolkit; namespace mediakit{ -PSEncoderImp::PSEncoderImp(uint32_t ssrc, uint8_t payload_type) : MpegMuxer(true) { - GET_CONFIG(uint32_t,video_mtu,Rtp::kVideoMtuSize); +PSEncoderImp::PSEncoderImp(uint32_t ssrc, uint8_t payload_type, bool ps_or_ts) : MpegMuxer(ps_or_ts) { + GET_CONFIG(uint32_t, s_video_mtu, Rtp::kVideoMtuSize); _rtp_encoder = std::make_shared(); + auto video_mtu = s_video_mtu; + if (!ps_or_ts) { + // 确保ts rtp负载部分长度是188的倍数 + video_mtu = RtpPacket::kRtpHeaderSize + (s_video_mtu - (s_video_mtu % 188)); + if (video_mtu > s_video_mtu) { + video_mtu -= 188; + } + } _rtp_encoder->setRtpInfo(ssrc, video_mtu, 90000, payload_type); auto ring = std::make_shared(); ring->setDelegate(std::make_shared([this](RtpPacket::Ptr rtp, bool is_key) { onRTP(std::move(rtp), is_key); })); diff --git a/src/Rtp/PSEncoder.h b/src/Rtp/PSEncoder.h index 16bbcd4f..e195913a 100644 --- a/src/Rtp/PSEncoder.h +++ b/src/Rtp/PSEncoder.h @@ -16,11 +16,19 @@ #include "Record/MPEG.h" #include "Common/MediaSink.h" -namespace mediakit{ +namespace mediakit { + class CommonRtpEncoder; -class PSEncoderImp : public MpegMuxer{ + +class PSEncoderImp : public MpegMuxer { public: - PSEncoderImp(uint32_t ssrc, uint8_t payload_type = 96); + /** + * 创建psh或ts rtp编码器 + * @param ssrc rtp的ssrc + * @param payload_type rtp的pt + * @param ps_or_ts true: ps, false: ts + */ + PSEncoderImp(uint32_t ssrc, uint8_t payload_type = 96, bool ps_or_ts = true); ~PSEncoderImp() override; protected: diff --git a/src/Rtp/RawEncoder.cpp b/src/Rtp/RawEncoder.cpp index 4bc17af7..c14254bd 100644 --- a/src/Rtp/RawEncoder.cpp +++ b/src/Rtp/RawEncoder.cpp @@ -34,6 +34,12 @@ bool RawEncoderImp::addTrack(const Track::Ptr &track) { auto ring = std::make_shared(); ring->setDelegate(std::make_shared([this](RtpPacket::Ptr rtp, bool is_key) { onRTP(std::move(rtp), true); })); _rtp_encoder->setRtpRing(std::move(ring)); + if (track->getCodecId() == CodecG711A || track->getCodecId() == CodecG711U) { + GET_CONFIG(uint32_t, dur_ms, RtpProxy::kRtpG711DurMs); + Any param; + param.set(dur_ms); + _rtp_encoder->setOpt(RtpCodec::RTP_ENCODER_PKT_DUR_MS, param); + } return true; } diff --git a/src/Rtp/RtpCache.h b/src/Rtp/RtpCache.h index c8778e18..bd570c47 100644 --- a/src/Rtp/RtpCache.h +++ b/src/Rtp/RtpCache.h @@ -40,7 +40,9 @@ private: class RtpCachePS : public RtpCache, public PSEncoderImp { public: - RtpCachePS(onFlushed cb, uint32_t ssrc, uint8_t payload_type = 96) : RtpCache(std::move(cb)), PSEncoderImp(ssrc, payload_type) {}; + RtpCachePS(onFlushed cb, uint32_t ssrc, uint8_t payload_type = 96, bool ps_or_ts = true) : + RtpCache(std::move(cb)), PSEncoderImp(ssrc, ps_or_ts ? payload_type : Rtsp::PT_MP2T, ps_or_ts) {}; + void flush() override; protected: @@ -56,6 +58,7 @@ protected: void onRTP(toolkit::Buffer::Ptr rtp, bool is_key = false) override; }; -}//namespace mediakit +} //namespace mediakit + #endif//ENABLE_RTPPROXY #endif //ZLMEDIAKIT_RTPCACHE_H diff --git a/src/Rtp/RtpProcess.cpp b/src/Rtp/RtpProcess.cpp index 1d95f6b0..b0161d91 100644 --- a/src/Rtp/RtpProcess.cpp +++ b/src/Rtp/RtpProcess.cpp @@ -199,8 +199,8 @@ void RtpProcess::setStopCheckRtp(bool is_check){ } } -void RtpProcess::setOnlyAudio(bool only_audio){ - _only_audio = only_audio; +void RtpProcess::setOnlyTrack(OnlyTrack only_track) { + _only_track = only_track; } void RtpProcess::onDetach() { @@ -259,8 +259,10 @@ void RtpProcess::emitOnPublish() { if (!option.stream_replace.empty()) { RtpSelector::Instance().addStreamReplace(strong_self->_media_info.stream, option.stream_replace); } - if (strong_self->_only_audio) { - strong_self->_muxer->setOnlyAudio(); + switch (strong_self->_only_track) { + case kOnlyAudio: strong_self->_muxer->setOnlyAudio(); break; + case kOnlyVideo: strong_self->_muxer->enableAudio(false); break; + default: break; } strong_self->_muxer->setMediaListener(strong_self); strong_self->doCachedFunc(); diff --git a/src/Rtp/RtpProcess.h b/src/Rtp/RtpProcess.h index b9d5009b..b680936c 100644 --- a/src/Rtp/RtpProcess.h +++ b/src/Rtp/RtpProcess.h @@ -24,6 +24,7 @@ public: friend class RtpProcessHelper; RtpProcess(const std::string &stream_id); ~RtpProcess(); + enum OnlyTrack { kAll = 0, kOnlyAudio = 1, kOnlyVideo = 2 }; /** * 输入rtp @@ -58,10 +59,10 @@ public: void setStopCheckRtp(bool is_check=false); /** - * 设置为单track,单音频时可以加快媒体注册速度 + * 设置为单track,单音频/单视频时可以加快媒体注册速度 * 请在inputRtp前调用此方法,否则可能会是空操作 */ - void setOnlyAudio(bool only_audio); + void setOnlyTrack(OnlyTrack only_track); /** * flush输出缓存 @@ -93,7 +94,7 @@ private: void doCachedFunc(); private: - bool _only_audio = false; + OnlyTrack _only_track = kAll; std::string _auth_err; uint64_t _dts = 0; uint64_t _total_bytes = 0; diff --git a/src/Rtp/RtpSender.cpp b/src/Rtp/RtpSender.cpp index 28b7770b..ed0d836e 100644 --- a/src/Rtp/RtpSender.cpp +++ b/src/Rtp/RtpSender.cpp @@ -40,10 +40,11 @@ void RtpSender::startSend(const MediaSourceEvent::SendRtpArgs &args, const funct if (!_interface) { //重连时不重新创建对象 auto lam = [this](std::shared_ptr> list) { onFlushRtpList(std::move(list)); }; - if (args.use_ps) { - _interface = std::make_shared(lam, atoi(args.ssrc.data()), args.pt); - } else { - _interface = std::make_shared(lam, atoi(args.ssrc.data()), args.pt, args.only_audio); + switch (args.type) { + case MediaSourceEvent::SendRtpArgs::kRtpPS: _interface = std::make_shared(lam, atoi(args.ssrc.data()), args.pt, true); break; + case MediaSourceEvent::SendRtpArgs::kRtpTS: _interface = std::make_shared(lam, atoi(args.ssrc.data()), args.pt, false); break; + case MediaSourceEvent::SendRtpArgs::kRtpRAW: _interface = std::make_shared(lam, atoi(args.ssrc.data()), args.pt, args.only_audio); break; + default: CHECK(0, "invalid rtp type:" + to_string(args.type)); break; } } diff --git a/src/Rtp/RtpServer.cpp b/src/Rtp/RtpServer.cpp index 51bb5555..84809488 100644 --- a/src/Rtp/RtpServer.cpp +++ b/src/Rtp/RtpServer.cpp @@ -42,12 +42,12 @@ public: } } - void setRtpServerInfo(uint16_t local_port,RtpServer::TcpMode mode,bool re_use_port,uint32_t ssrc, bool only_audio) { + void setRtpServerInfo(uint16_t local_port, RtpServer::TcpMode mode, bool re_use_port, uint32_t ssrc, int only_track) { _local_port = local_port; _tcp_mode = mode; _re_use_port = re_use_port; _ssrc = ssrc; - _only_audio = only_audio; + _only_track = only_track; } void setOnDetach(function cb) { @@ -61,7 +61,7 @@ public: void onRecvRtp(const Socket::Ptr &sock, const Buffer::Ptr &buf, struct sockaddr *addr) { if (!_process) { _process = RtpSelector::Instance().getProcess(_stream_id, true); - _process->setOnlyAudio(_only_audio); + _process->setOnlyTrack((RtpProcess::OnlyTrack)_only_track); _process->setOnDetach(std::move(_on_detach)); cancelDelayTask(); } @@ -142,7 +142,7 @@ private: private: bool _re_use_port = false; - bool _only_audio = false; + int _only_track = 0; uint16_t _local_port = 0; uint32_t _ssrc = 0; RtpServer::TcpMode _tcp_mode = RtpServer::NONE; @@ -156,7 +156,7 @@ private: EventPoller::DelayTask::Ptr _delay_task; }; -void RtpServer::start(uint16_t local_port, const string &stream_id, TcpMode tcp_mode, const char *local_ip, bool re_use_port, uint32_t ssrc, bool only_audio, bool multiplex) { +void RtpServer::start(uint16_t local_port, const string &stream_id, TcpMode tcp_mode, const char *local_ip, bool re_use_port, uint32_t ssrc, int only_track, bool multiplex) { //创建udp服务器 Socket::Ptr rtp_socket = Socket::createSocket(nullptr, true); Socket::Ptr rtcp_socket = Socket::createSocket(nullptr, true); @@ -174,7 +174,8 @@ void RtpServer::start(uint16_t local_port, const string &stream_id, TcpMode tcp_ } //设置udp socket读缓存 - SockUtil::setRecvBuf(rtp_socket->rawFD(), 4 * 1024 * 1024); + GET_CONFIG(int, udpRecvSocketBuffer, RtpProxy::kUdpRecvSocketBuffer); + SockUtil::setRecvBuf(rtp_socket->rawFD(), udpRecvSocketBuffer); TcpServer::Ptr tcp_server; _tcp_mode = tcp_mode; @@ -183,7 +184,7 @@ void RtpServer::start(uint16_t local_port, const string &stream_id, TcpMode tcp_ tcp_server = std::make_shared(rtp_socket->getPoller()); (*tcp_server)[RtpSession::kStreamID] = stream_id; (*tcp_server)[RtpSession::kSSRC] = ssrc; - (*tcp_server)[RtpSession::kOnlyAudio] = only_audio; + (*tcp_server)[RtpSession::kOnlyTrack] = only_track; if (tcp_mode == PASSIVE) { tcp_server->start(local_port, local_ip); } else if (stream_id.empty()) { @@ -200,7 +201,7 @@ void RtpServer::start(uint16_t local_port, const string &stream_id, TcpMode tcp_ //指定了流id,那么一个端口一个流(不管是否包含多个ssrc的多个流,绑定rtp源后,会筛选掉ip端口不匹配的流) helper = std::make_shared(std::move(rtcp_socket), stream_id); helper->startRtcp(); - helper->setRtpServerInfo(local_port, tcp_mode, re_use_port, ssrc, only_audio); + helper->setRtpServerInfo(local_port, tcp_mode, re_use_port, ssrc, only_track); bool bind_peer_addr = false; auto ssrc_ptr = std::make_shared(ssrc); _ssrc = ssrc_ptr; @@ -222,7 +223,8 @@ void RtpServer::start(uint16_t local_port, const string &stream_id, TcpMode tcp_ } else { //单端口多线程接收多个流,根据ssrc区分流 udp_server = std::make_shared(rtp_socket->getPoller()); - (*udp_server)[RtpSession::kOnlyAudio] = only_audio; + (*udp_server)[RtpSession::kOnlyTrack] = only_track; + (*udp_server)[RtpSession::kUdpRecvBuffer] = udpRecvSocketBuffer; udp_server->start(local_port, local_ip); rtp_socket = nullptr; } diff --git a/src/Rtp/RtpServer.h b/src/Rtp/RtpServer.h index 74250d6f..3654828e 100644 --- a/src/Rtp/RtpServer.h +++ b/src/Rtp/RtpServer.h @@ -44,7 +44,7 @@ public: * @param multiplex 多路复用 */ void start(uint16_t local_port, const std::string &stream_id = "", TcpMode tcp_mode = PASSIVE, - const char *local_ip = "::", bool re_use_port = true, uint32_t ssrc = 0, bool only_audio = false, bool multiplex = false); + const char *local_ip = "::", bool re_use_port = true, uint32_t ssrc = 0, int only_track = 0, bool multiplex = false); /** * 连接到tcp服务(tcp主动模式) @@ -81,7 +81,7 @@ protected: std::shared_ptr _rtcp_helper; std::function _on_cleanup; - bool _only_audio = false; + int _only_track = 0; //用于tcp主动模式 TcpMode _tcp_mode = NONE; }; diff --git a/src/Rtp/RtpSession.cpp b/src/Rtp/RtpSession.cpp index 006b1c82..95807637 100644 --- a/src/Rtp/RtpSession.cpp +++ b/src/Rtp/RtpSession.cpp @@ -23,7 +23,8 @@ namespace mediakit{ const string RtpSession::kStreamID = "stream_id"; const string RtpSession::kSSRC = "ssrc"; -const string RtpSession::kOnlyAudio = "only_audio"; +const string RtpSession::kOnlyTrack = "only_track"; +const string RtpSession::kUdpRecvBuffer = "udp_recv_socket_buffer"; void RtpSession::attachServer(const Server &server) { setParams(const_cast(server)); @@ -32,7 +33,13 @@ void RtpSession::attachServer(const Server &server) { void RtpSession::setParams(mINI &ini) { _stream_id = ini[kStreamID]; _ssrc = ini[kSSRC]; - _only_audio = ini[kOnlyAudio]; + _only_track = ini[kOnlyTrack]; + int udp_socket_buffer = ini[kUdpRecvBuffer]; + if (_is_udp) { + // 设置udp socket读缓存 + SockUtil::setRecvBuf(getSock()->rawFD(), + (udp_socket_buffer > 0) ? udp_socket_buffer : (4 * 1024 * 1024)); + } } RtpSession::RtpSession(const Socket::Ptr &sock) @@ -40,10 +47,6 @@ RtpSession::RtpSession(const Socket::Ptr &sock) socklen_t addr_len = sizeof(_addr); getpeername(sock->rawFD(), (struct sockaddr *)&_addr, &addr_len); _is_udp = sock->sockType() == SockNum::Sock_UDP; - if (_is_udp) { - // 设置udp socket读缓存 - SockUtil::setRecvBuf(getSock()->rawFD(), 4 * 1024 * 1024); - } } RtpSession::~RtpSession() = default; @@ -122,7 +125,7 @@ void RtpSession::onRtpPacket(const char *data, size_t len) { _delay_close = true; return; } - _process->setOnlyAudio(_only_audio); + _process->setOnlyTrack((RtpProcess::OnlyTrack)_only_track); _process->setDelegate(static_pointer_cast(shared_from_this())); } try { diff --git a/src/Rtp/RtpSession.h b/src/Rtp/RtpSession.h index 5b8d7e1f..2bff4f5f 100644 --- a/src/Rtp/RtpSession.h +++ b/src/Rtp/RtpSession.h @@ -24,7 +24,8 @@ class RtpSession : public toolkit::Session, public RtpSplitter, public MediaSour public: static const std::string kStreamID; static const std::string kSSRC; - static const std::string kOnlyAudio; + static const std::string kOnlyTrack; + static const std::string kUdpRecvBuffer; RtpSession(const toolkit::Socket::Ptr &sock); ~RtpSession() override; @@ -51,7 +52,7 @@ private: bool _is_udp = false; bool _search_rtp = false; bool _search_rtp_finished = false; - bool _only_audio = false; + int _only_track = 0; uint32_t _ssrc = 0; toolkit::Ticker _ticker; std::string _stream_id; diff --git a/src/Rtsp/RtpCodec.h b/src/Rtsp/RtpCodec.h index d63f1962..9a8e3396 100644 --- a/src/Rtsp/RtpCodec.h +++ b/src/Rtsp/RtpCodec.h @@ -93,6 +93,17 @@ public: RtpInfo &getRtpInfo() { return *_rtp_info; } + enum { + RTP_ENCODER_PKT_DUR_MS = 1 // 主要应用于g711 rtp 打包器每个包的时间长度,option_value 为int*, option_len 为4 + }; + /** + * @brief 设置rtp打包器与解包器的相关参数,主要应用与g711 rtp 打包器,使用方法类似setsockopt + * + * @param opt 设置的选项 + * @param param 设置的参数 + */ + virtual void setOpt(int opt, const toolkit::Any ¶m) {}; + private: std::unique_ptr _rtp_info; }; 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)) { // 分配端口失败 diff --git a/srt/SrtSession.cpp b/srt/SrtSession.cpp index 5211fd4c..8a671c96 100644 --- a/srt/SrtSession.cpp +++ b/srt/SrtSession.cpp @@ -16,41 +16,17 @@ SrtSession::SrtSession(const Socket::Ptr &sock) // TraceL<<"after addr len "<data(); - size_t size = buffer->size(); - - if (DataPacket::isDataPacket(data, size)) { - uint32_t socket_id = DataPacket::getSocketID(data, size); - auto trans = SrtTransportManager::Instance().getItem(std::to_string(socket_id)); - return trans ? trans->getPoller() : nullptr; - } - - if (HandshakePacket::isHandshakePacket(data, size)) { - auto type = HandshakePacket::getHandshakeType(data, size); - if (type == HandshakePacket::HS_TYPE_INDUCTION) { - // 握手第一阶段 - return nullptr; - } else if (type == HandshakePacket::HS_TYPE_CONCLUSION) { - // 握手第二阶段 - uint32_t sync_cookie = HandshakePacket::getSynCookie(data, size); - auto trans = SrtTransportManager::Instance().getHandshakeItem(std::to_string(sync_cookie)); - return trans ? trans->getPoller() : nullptr; - } else { - WarnL << " not reach there"; - } - } else { - uint32_t socket_id = ControlPacket::getSocketID(data, size); - auto trans = SrtTransportManager::Instance().getItem(std::to_string(socket_id)); - return trans ? trans->getPoller() : nullptr; - } - return nullptr; -} - void SrtSession::attachServer(const toolkit::Server &server) { SockUtil::setRecvBuf(getSock()->rawFD(), 1024 * 1024); } +extern SrtTransport::Ptr querySrtTransport(uint8_t *data, size_t size, const EventPoller::Ptr& poller); + +EventPoller::Ptr SrtSession::queryPoller(const Buffer::Ptr &buffer) { + auto transport = querySrtTransport((uint8_t *)buffer->data(), buffer->size(), nullptr); + return transport ? transport->getPoller() : nullptr; +} + void SrtSession::onRecv(const Buffer::Ptr &buffer) { uint8_t *data = (uint8_t *)buffer->data(); size_t size = buffer->size(); @@ -58,45 +34,7 @@ void SrtSession::onRecv(const Buffer::Ptr &buffer) { if (_find_transport) { //只允许寻找一次transport _find_transport = false; - - if (DataPacket::isDataPacket(data, size)) { - uint32_t socket_id = DataPacket::getSocketID(data, size); - auto trans = SrtTransportManager::Instance().getItem(std::to_string(socket_id)); - if (trans) { - _transport = std::move(trans); - } else { - WarnL << " data packet not find transport "; - } - } - - if (HandshakePacket::isHandshakePacket(data, size)) { - auto type = HandshakePacket::getHandshakeType(data, size); - if (type == HandshakePacket::HS_TYPE_INDUCTION) { - // 握手第一阶段 - _transport = std::make_shared(getPoller()); - - } else if (type == HandshakePacket::HS_TYPE_CONCLUSION) { - // 握手第二阶段 - uint32_t sync_cookie = HandshakePacket::getSynCookie(data, size); - auto trans = SrtTransportManager::Instance().getHandshakeItem(std::to_string(sync_cookie)); - if (trans) { - _transport = std::move(trans); - } else { - WarnL << " hanshake packet not find transport "; - } - } else { - WarnL << " not reach there"; - } - } else { - uint32_t socket_id = ControlPacket::getSocketID(data, size); - auto trans = SrtTransportManager::Instance().getItem(std::to_string(socket_id)); - if (trans) { - _transport = std::move(trans); - } else { - WarnL << " not find transport"; - } - } - + _transport = querySrtTransport(data, size, getPoller()); if (_transport) { _transport->setSession(static_pointer_cast(shared_from_this())); } diff --git a/srt/SrtTransport.cpp b/srt/SrtTransport.cpp index 7388f3a0..f88cf428 100644 --- a/srt/SrtTransport.cpp +++ b/srt/SrtTransport.cpp @@ -61,7 +61,7 @@ void SrtTransport::switchToOtherTransport(uint8_t *buf, int len, uint32_t socket BufferRaw::Ptr tmp = BufferRaw::create(); struct sockaddr_storage tmp_addr = *addr; tmp->assign((char *)buf, len); - auto trans = SrtTransportManager::Instance().getItem(std::to_string(socketid)); + auto trans = SrtTransportManager::Instance().getItem(socketid); if (trans) { trans->getPoller()->async([tmp, tmp_addr, trans] { trans->inputSockData((uint8_t *)tmp->data(), tmp->size(), (struct sockaddr_storage *)&tmp_addr); @@ -700,30 +700,30 @@ void SrtTransport::sendPacket(Buffer::Ptr pkt, bool flush) { } } -std::string SrtTransport::getIdentifier() { +std::string SrtTransport::getIdentifier() const { return _selected_session ? _selected_session->getIdentifier() : ""; } void SrtTransport::registerSelfHandshake() { - SrtTransportManager::Instance().addHandshakeItem(std::to_string(_sync_cookie), shared_from_this()); + SrtTransportManager::Instance().addHandshakeItem(_sync_cookie, shared_from_this()); } void SrtTransport::unregisterSelfHandshake() { if (_sync_cookie == 0) { return; } - SrtTransportManager::Instance().removeHandshakeItem(std::to_string(_sync_cookie)); + SrtTransportManager::Instance().removeHandshakeItem(_sync_cookie); } void SrtTransport::registerSelf() { if (_socket_id == 0) { return; } - SrtTransportManager::Instance().addItem(std::to_string(_socket_id), shared_from_this()); + SrtTransportManager::Instance().addItem(_socket_id, shared_from_this()); } void SrtTransport::unregisterSelf() { - SrtTransportManager::Instance().removeItem(std::to_string(_socket_id)); + SrtTransportManager::Instance().removeItem(_socket_id); } void SrtTransport::onShutdown(const SockException &ex) { @@ -739,7 +739,7 @@ void SrtTransport::onShutdown(const SockException &ex) { } } -size_t SrtTransport::getPayloadSize() { +size_t SrtTransport::getPayloadSize() const { size_t ret = (_mtu - 28 - 16) / 188 * 188; return ret; } @@ -792,15 +792,13 @@ SrtTransportManager &SrtTransportManager::Instance() { return s_instance; } -void SrtTransportManager::addItem(const std::string &key, const SrtTransport::Ptr &ptr) { +void SrtTransportManager::addItem(const uint32_t key, const SrtTransport::Ptr &ptr) { std::lock_guard lck(_mtx); _map[key] = ptr; } -SrtTransport::Ptr SrtTransportManager::getItem(const std::string &key) { - if (key.empty()) { - return nullptr; - } +SrtTransport::Ptr SrtTransportManager::getItem(const uint32_t key) { + assert(key > 0); std::lock_guard lck(_mtx); auto it = _map.find(key); if (it == _map.end()) { @@ -809,25 +807,23 @@ SrtTransport::Ptr SrtTransportManager::getItem(const std::string &key) { return it->second.lock(); } -void SrtTransportManager::removeItem(const std::string &key) { +void SrtTransportManager::removeItem(const uint32_t key) { std::lock_guard lck(_mtx); _map.erase(key); } -void SrtTransportManager::addHandshakeItem(const std::string &key, const SrtTransport::Ptr &ptr) { +void SrtTransportManager::addHandshakeItem(const uint32_t key, const SrtTransport::Ptr &ptr) { std::lock_guard lck(_handshake_mtx); _handshake_map[key] = ptr; } -void SrtTransportManager::removeHandshakeItem(const std::string &key) { +void SrtTransportManager::removeHandshakeItem(const uint32_t key) { std::lock_guard lck(_handshake_mtx); _handshake_map.erase(key); } -SrtTransport::Ptr SrtTransportManager::getHandshakeItem(const std::string &key) { - if (key.empty()) { - return nullptr; - } +SrtTransport::Ptr SrtTransportManager::getHandshakeItem(const uint32_t key) { + assert(key > 0); std::lock_guard lck(_handshake_mtx); auto it = _handshake_map.find(key); if (it == _handshake_map.end()) { diff --git a/srt/SrtTransport.hpp b/srt/SrtTransport.hpp index fe3dfe69..56b75b4c 100644 --- a/srt/SrtTransport.hpp +++ b/srt/SrtTransport.hpp @@ -45,7 +45,7 @@ public: virtual void inputSockData(uint8_t *buf, int len, struct sockaddr_storage *addr); virtual void onSendTSData(const Buffer::Ptr &buffer, bool flush); - std::string getIdentifier(); + std::string getIdentifier() const; void unregisterSelf(); void unregisterSelfHandshake(); @@ -89,7 +89,7 @@ private: void sendShutDown(); void sendMsgDropReq(uint32_t first, uint32_t last); - size_t getPayloadSize(); + size_t getPayloadSize() const; void createTimerForCheckAlive(); @@ -164,23 +164,23 @@ private: class SrtTransportManager { public: static SrtTransportManager &Instance(); - SrtTransport::Ptr getItem(const std::string &key); - void addItem(const std::string &key, const SrtTransport::Ptr &ptr); - void removeItem(const std::string &key); + SrtTransport::Ptr getItem(const uint32_t key); + void addItem(const uint32_t key, const SrtTransport::Ptr &ptr); + void removeItem(const uint32_t key); - void addHandshakeItem(const std::string &key, const SrtTransport::Ptr &ptr); - void removeHandshakeItem(const std::string &key); - SrtTransport::Ptr getHandshakeItem(const std::string &key); + void addHandshakeItem(const uint32_t key, const SrtTransport::Ptr &ptr); + void removeHandshakeItem(const uint32_t key); + SrtTransport::Ptr getHandshakeItem(const uint32_t key); private: SrtTransportManager() = default; private: std::mutex _mtx; - std::unordered_map> _map; + std::unordered_map> _map; std::mutex _handshake_mtx; - std::unordered_map> _handshake_map; + std::unordered_map> _handshake_map; }; } // namespace SRT diff --git a/srt/SrtTransportImp.cpp b/srt/SrtTransportImp.cpp index 087bb4ae..8f818655 100644 --- a/srt/SrtTransportImp.cpp +++ b/srt/SrtTransportImp.cpp @@ -24,6 +24,32 @@ SrtTransportImp::~SrtTransportImp() { } } + +SrtTransport::Ptr querySrtTransport(uint8_t *data, size_t size, const EventPoller::Ptr& poller) { + if (DataPacket::isDataPacket(data, size)) { + uint32_t socket_id = DataPacket::getSocketID(data, size); + return SrtTransportManager::Instance().getItem(socket_id); + } + + if (HandshakePacket::isHandshakePacket(data, size)) { + auto type = HandshakePacket::getHandshakeType(data, size); + if (type == HandshakePacket::HS_TYPE_INDUCTION) { + // 握手第一阶段 + return poller ? std::make_shared(poller) : nullptr; + } + + if (type == HandshakePacket::HS_TYPE_CONCLUSION) { + // 握手第二阶段 + uint32_t sync_cookie = HandshakePacket::getSynCookie(data, size); + return SrtTransportManager::Instance().getHandshakeItem(sync_cookie); + } + } + + uint32_t socket_id = ControlPacket::getSocketID(data, size); + return SrtTransportManager::Instance().getItem(socket_id); +} + + void SrtTransportImp::onHandShakeFinished(std::string &streamid, struct sockaddr_storage *addr) { SrtTransport::onHandShakeFinished(streamid,addr); // TODO parse stream id like this zlmediakit.com/live/test?token=1213444&type=push 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; } diff --git a/webrtc/Sdp.cpp b/webrtc/Sdp.cpp index bed1bf57..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] = "PCMU,PCMA,opus,mpeg4-generic"; + 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 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 7a39b8da..768c543e 100644 --- a/webrtc/WebRtcTransport.cpp +++ b/webrtc/WebRtcTransport.cpp @@ -31,7 +31,6 @@ #define RTP_CNAME "zlmediakit-rtp" #define RTP_LABEL "zlmediakit-label" #define RTP_MSLABEL "zlmediakit-mslabel" -#define RTP_MSID RTP_MSLABEL " " RTP_LABEL using namespace std; @@ -55,6 +54,9 @@ const string kStartBitrate = RTC_FIELD "start_bitrate"; const string kMaxBitrate = RTC_FIELD "max_bitrate"; const string kMinBitrate = RTC_FIELD "min_bitrate"; +// 数据通道设置 +const string kDataChannelEcho = RTC_FIELD "datachannel_echo"; + static onceToken token([]() { mINI::Instance()[kTimeOutSec] = 15; mINI::Instance()[kExternIP] = ""; @@ -65,6 +67,8 @@ static onceToken token([]() { mINI::Instance()[kStartBitrate] = 0; mINI::Instance()[kMaxBitrate] = 0; mINI::Instance()[kMinBitrate] = 0; + + mINI::Instance()[kDataChannelEcho] = true; }); } // namespace RTC @@ -250,22 +254,47 @@ void WebRtcTransport::OnDtlsTransportApplicationDataReceived( #ifdef ENABLE_SCTP void WebRtcTransport::OnSctpAssociationConnecting(RTC::SctpAssociation *sctpAssociation) { TraceL << getIdentifier(); + try { + NOTICE_EMIT(BroadcastRtcSctpConnectArgs, Broadcast::kBroadcastRtcSctpConnecting, *this); + } catch (std::exception &ex) { + WarnL << "Exception occurred: " << ex.what(); + } } void WebRtcTransport::OnSctpAssociationConnected(RTC::SctpAssociation *sctpAssociation) { InfoL << getIdentifier(); + try { + NOTICE_EMIT(BroadcastRtcSctpConnectArgs, Broadcast::kBroadcastRtcSctpConnected, *this); + } catch (std::exception &ex) { + WarnL << "Exception occurred: " << ex.what(); + } } void WebRtcTransport::OnSctpAssociationFailed(RTC::SctpAssociation *sctpAssociation) { WarnL << getIdentifier(); + try { + NOTICE_EMIT(BroadcastRtcSctpConnectArgs, Broadcast::kBroadcastRtcSctpFailed, *this); + } catch (std::exception &ex) { + WarnL << "Exception occurred: " << ex.what(); + } } void WebRtcTransport::OnSctpAssociationClosed(RTC::SctpAssociation *sctpAssociation) { InfoL << getIdentifier(); + try { + NOTICE_EMIT(BroadcastRtcSctpConnectArgs, Broadcast::kBroadcastRtcSctpClosed, *this); + } catch (std::exception &ex) { + WarnL << "Exception occurred: " << ex.what(); + } } void WebRtcTransport::OnSctpAssociationSendData( RTC::SctpAssociation *sctpAssociation, const uint8_t *data, size_t len) { + try { + NOTICE_EMIT(BroadcastRtcSctpSendArgs, Broadcast::kBroadcastRtcSctpSend, *this, data, len); + } catch (std::exception &ex) { + WarnL << "Exception occurred: " << ex.what(); + } _dtls_transport->SendApplicationData(data, len); } @@ -274,8 +303,18 @@ void WebRtcTransport::OnSctpAssociationMessageReceived( InfoL << getIdentifier() << " " << streamId << " " << ppid << " " << len << " " << string((char *)msg, len); RTC::SctpStreamParameters params; params.streamId = streamId; - // 回显数据 - _sctp->SendSctpMessage(params, ppid, msg, len); + + GET_CONFIG(bool, datachannel_echo, Rtc::kDataChannelEcho); + if (datachannel_echo) { + // 回显数据 + _sctp->SendSctpMessage(params, ppid, msg, len); + } + + try { + NOTICE_EMIT(BroadcastRtcSctpReceivedArgs, Broadcast::kBroadcastRtcSctpReceived, *this, streamId, ppid, msg, len); + } catch (std::exception &ex) { + WarnL << "Exception occurred: " << ex.what(); + } } #endif @@ -339,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); @@ -368,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 //// @@ -392,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)); @@ -412,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)) { @@ -422,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)) { @@ -494,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(); } @@ -635,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; @@ -667,9 +702,9 @@ void WebRtcTransportImp::onCheckAnswer(RtcSession &sdp) { // 发送的ssrc我们随便定义,因为在发送rtp时会修改为此值 ssrc.ssrc = m.type + RTP_SSRC_OFFSET; ssrc.cname = RTP_CNAME; - ssrc.label = RTP_LABEL; + ssrc.label = std::string(RTP_LABEL) + '-' + m.mid; ssrc.mslabel = RTP_MSLABEL; - ssrc.msid = RTP_MSID; + ssrc.msid = ssrc.mslabel + ' ' + ssrc.label; if (m.getRelatedRtxPlan(m.plan[0].pt)) { // rtx ssrc @@ -730,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 { @@ -744,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); } /////////////////////////////////////////////////////////////////// @@ -1239,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; @@ -1269,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; @@ -1322,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); }; @@ -1335,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; @@ -1356,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); }); }; @@ -1369,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..3f920c68 100644 --- a/webrtc/WebRtcTransport.h +++ b/webrtc/WebRtcTransport.h @@ -42,13 +42,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 +85,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 +249,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 +272,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 +288,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 +309,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 +332,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; 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 标签