From 73473ae520d3e7281b44b754c5cdc896fed86303 Mon Sep 17 00:00:00 2001 From: Unity Technologies <@unity> Date: Wed, 9 Aug 2023 00:00:00 +0000 Subject: [PATCH] com.unity.renderstreaming@3.1.0-exp.7 ## [3.1.0-exp.7] - 2023-08-09 ### Added - Added configurable logger to enable users to customize logging for their environment. ### Changed - Upgrade the version of WebRTC package `3.0.0-pre.6`. - Add `AudioStreamSender.loopback` property. ### Fixed - Fixed error on HTTP signaling when using short polling interval. - Fixed `SignalingManager` so that ICE server configurations aren't effected. - Added a workaround to fix an issue where `InputField` wasn't worked when entering characters from browsers or a `Receiver` scene of package sample. ### Removed - Removed Furioos Integration. --- CHANGELOG.md | 25 +- Documentation~/audio-streaming.md | 3 +- .../images/audiostreamsender_inspector.png | Bin 22440 -> 14894 bytes .../ice-server-configuration-browser.png | Bin 0 -> 83633 bytes .../images/turn-server-settings.png | Bin 0 -> 86391 bytes Documentation~/index.md | 15 +- Documentation~/samples.md | 2 +- Documentation~/signaling-type.md | 5 - Documentation~/turnserver.md | 33 +- Editor/AudioStreamReceiverEditor.cs | 6 +- Editor/AudioStreamSenderEditor.cs | 25 +- Editor/ConfigInfoLine.cs | 10 +- Editor/CustomSignalingSettingsEditor.cs | 4 +- Editor/IRequestJob.cs | 12 +- Editor/InputSystem/InputReceiverEditor.cs | 34 +- Editor/PropertyDrawers/BitrateDrawer.cs | 2 +- Editor/PropertyDrawers/CodecDrawer.cs | 14 +- Editor/PropertyDrawers/FrameRateDrawer.cs | 4 +- .../RenderTextureDepthBufferDrawer.cs | 2 +- .../RenderTexureAntiAliasingDrawer.cs | 2 +- .../PropertyDrawers/ScaleResolutionDrawer.cs | 4 +- .../SignalingSettingsDrawer.cs | 18 +- Editor/PropertyDrawers/StreamingSizeDrawer.cs | 6 +- .../HDRPInitialSetupPostProcessor.cs | 2 +- .../URPInitialSetupPostProcessor.cs | 2 +- Editor/RenderStreamingProjectSettings.cs | 4 +- .../RenderStreamingProjectSettingsProvider.cs | 14 +- Editor/RenderStreamingSettingsEditor.cs | 4 +- Editor/RenderStreamingWizard.cs | 28 +- Editor/RequestExtensions.cs | 55 ++- Editor/RequestInfo/AddRequestInfo.cs | 28 +- Editor/RequestInfo/ListRequestInfo.cs | 34 +- Editor/RequestInfo/RemoveRequestInfo.cs | 28 +- Editor/RequestInfo/SearchAllRequestInfo.cs | 30 +- Editor/RequestInfo/SearchRequestInfo.cs | 34 +- Editor/RequestJob.cs | 189 ++++---- Editor/RequestJobManager.cs | 332 +++++++------ Editor/SignalingManagerEditor.cs | 62 ++- Editor/VideoStreamReceiverEditor.cs | 10 +- Editor/VideoStreamSenderEditor.cs | 26 +- Editor/WebAppDownloader.cs | 28 +- Runtime/Scripts/AudioCodecInfo.cs | 2 +- Runtime/Scripts/AudioStreamReceiver.cs | 5 +- Runtime/Scripts/AudioStreamSender.cs | 53 +- Runtime/Scripts/AutomaticStreaming.cs | 3 +- Runtime/Scripts/Broadcast.cs | 4 +- Runtime/Scripts/DataChannelBase.cs | 43 +- Runtime/Scripts/DateTimeExtension.cs | 9 +- Runtime/Scripts/InputReceiver.cs | 61 +-- Runtime/Scripts/InputSender.cs | 2 +- .../InputSystem/EmulateInputFieldEvent.cs | 251 ++++++++++ .../EmulateInputFieldEvent.cs.meta | 11 + .../InputSystem/InputDeviceExtension.cs | 2 +- Runtime/Scripts/InputSystem/InputManager.cs | 2 +- Runtime/Scripts/InputSystem/InputRemoting.cs | 16 +- Runtime/Scripts/InputSystem/Receiver.cs | 18 +- Runtime/Scripts/InputSystem/Sender.cs | 23 - Runtime/Scripts/PeerConnection.cs | 14 +- Runtime/Scripts/RenderStreaming.cs | 25 +- Runtime/Scripts/RenderStreamingSettings.cs | 6 +- Runtime/Scripts/Signaling/FurioosSignaling.cs | 291 ----------- .../Signaling/FurioosSignaling.cs.meta | 3 - .../Signaling/FurioosSignalingSettings.cs | 73 --- .../FurioosSignalingSettings.cs.meta | 3 - Runtime/Scripts/Signaling/HttpSignaling.cs | 45 +- .../Signaling/HttpSignalingSettings.cs | 6 +- Runtime/Scripts/Signaling/SignalingMessage.cs | 1 + .../Scripts/Signaling/SignalingSettings.cs | 2 +- .../Scripts/Signaling/WebSocketSignaling.cs | 32 +- .../Signaling/WebSocketSignalingSettings.cs | 17 +- Runtime/Scripts/SignalingHandlerBase.cs | 15 +- Runtime/Scripts/SignalingManager.cs | 38 +- Runtime/Scripts/SignalingManagerInternal.cs | 18 +- Runtime/Scripts/VideoCodecInfo.cs | 8 +- Runtime/Scripts/VideoStreamReceiver.cs | 10 +- Runtime/Scripts/VideoStreamSender.cs | 27 +- Runtime/Unity.RenderStreaming.Runtime.asmdef | 5 + .../ARFoundation/ARFoundationSample.cs | 6 +- .../Example/Bidirectional/Bidirectional.unity | 457 ++++++++++++++++-- .../Bidirectional/BidirectionalSample.cs | 10 +- Samples~/Example/Broadcast/BroadcastSample.cs | 22 +- .../Broadcast/SimpleCameraControllerV2.cs | 18 +- Samples~/Example/Broadcast/UIControllerV2.cs | 9 +- Samples~/Example/Gyro/GyroSample.cs | 24 +- Samples~/Example/Menu/Menu.unity | 2 - Samples~/Example/Multiplay/FollowTransform.cs | 2 +- Samples~/Example/Multiplay/Multiplay.cs | 2 +- .../Example/Multiplay/MultiplayChannel.cs | 4 +- Samples~/Example/Multiplay/MultiplaySample.cs | 6 +- .../Example/Multiplay/PlayerController.cs | 24 +- .../Example/Receiver/AudioSpectrumView.cs | 4 +- Samples~/Example/Receiver/ReceiverSample.cs | 4 +- Samples~/Example/Scripts/BackButton.cs | 2 +- Samples~/Example/Scripts/SampleManager.cs | 2 +- Samples~/Example/Scripts/SceneSelectUI.cs | 43 +- Samples~/Example/Stats/ShowStatsUI.cs | 4 +- .../WebBrowserInput/SimpleCameraController.cs | 34 +- .../Example/WebBrowserInput/UIController.cs | 7 +- .../RemoteInput.cs | 55 ++- Tests/Editor/EditorTest.cs | 6 +- Tests/Editor/RenderStreamingTest.cs | 2 +- Tests/Editor/RequestJobTest.cs | 6 +- Tests/Runtime/CommandLineParserTest.cs | 1 - Tests/Runtime/DataTimeExtensionTest.cs | 15 + Tests/Runtime/DataTimeExtensionTest.cs.meta | 11 + Tests/Runtime/InputPositionCorrectorTest.cs | 2 +- .../InputSystem/InputDeviceExtensionTest.cs | 3 +- .../Runtime/InputSystem/InputRemotingTest.cs | 6 +- Tests/Runtime/MockLogger.cs | 93 ++++ Tests/Runtime/MockLogger.cs.meta | 11 + Tests/Runtime/PrivateSignalingTest.cs | 6 +- Tests/Runtime/RenderStreamingTest.cs | 17 + Tests/Runtime/Signaling/MockSignaling.cs | 56 ++- Tests/Runtime/SignalingEventProviderTest.cs | 2 +- Tests/Runtime/SignalingHandlerTest.cs | 152 +++++- Tests/Runtime/SignalingManagerInternalTest.cs | 2 +- Tests/Runtime/SignalingManagerTest.cs | 12 +- Tests/Runtime/SignalingSettingsTest.cs | 28 +- Tests/Runtime/SignalingTest.cs | 69 ++- Tests/Runtime/StreamingComponentTest.cs | 40 +- package.json | 12 +- 121 files changed, 2218 insertions(+), 1355 deletions(-) create mode 100644 Documentation~/images/ice-server-configuration-browser.png create mode 100644 Documentation~/images/turn-server-settings.png create mode 100644 Runtime/Scripts/InputSystem/EmulateInputFieldEvent.cs create mode 100644 Runtime/Scripts/InputSystem/EmulateInputFieldEvent.cs.meta delete mode 100644 Runtime/Scripts/Signaling/FurioosSignaling.cs delete mode 100644 Runtime/Scripts/Signaling/FurioosSignaling.cs.meta delete mode 100644 Runtime/Scripts/Signaling/FurioosSignalingSettings.cs delete mode 100644 Runtime/Scripts/Signaling/FurioosSignalingSettings.cs.meta create mode 100644 Tests/Runtime/DataTimeExtensionTest.cs create mode 100644 Tests/Runtime/DataTimeExtensionTest.cs.meta create mode 100644 Tests/Runtime/MockLogger.cs create mode 100644 Tests/Runtime/MockLogger.cs.meta diff --git a/CHANGELOG.md b/CHANGELOG.md index 52d3eba..e1c939b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,28 @@ All notable changes to com.unity.renderstreaming package will be documented in t The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## [3.1.0-exp.6] - 2023-2-24 +## [3.1.0-exp.7] - 2023-08-09 + +### Added + +- Added configurable logger to enable users to customize logging for their environment. + +### Changed + +- Upgrade the version of WebRTC package `3.0.0-pre.6`. +- Add `AudioStreamSender.loopback` property. + +### Fixed + +- Fixed error on HTTP signaling when using short polling interval. +- Fixed `SignalingManager` so that ICE server configurations aren't effected. +- Added a workaround to fix an issue where `InputField` wasn't worked when entering characters from browsers or a `Receiver` scene of package sample. + +### Removed + +- Removed Furioos Integration. + +## [3.1.0-exp.6] - 2023-02-24 ### Added @@ -20,7 +41,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Websocket is in default for signaling protocol instead of HTTP polling. - Changed a unit of the HTTP polling interval, second to millisecond. -## [3.1.0-exp.5] - 2023-1-16 +## [3.1.0-exp.5] - 2023-01-16 ### Changed diff --git a/Documentation~/audio-streaming.md b/Documentation~/audio-streaming.md index 4f44dd6..e71077d 100644 --- a/Documentation~/audio-streaming.md +++ b/Documentation~/audio-streaming.md @@ -15,9 +15,10 @@ This component streams the audio rendering results from [`AudioListener`](https: | **Audio Source Type** | Choose the type of source for your audio streaming.
- *Audio Listener*
- *Audio Source*
- *Microphone* | *Audio Listener* | | *Audio Listener* | [`Audio Listener`](https://docs.unity3d.com/ScriptReference/AudioSource.html) instance for sending audio | | | *Audio Source* | [`Audio Source`](https://docs.unity3d.com/ScriptReference/AudioSource.html) instance for sending audio | | -| *Microphone Device Index* | The index of the video input device to be used. See [Microphone.devices](https://docs.unity3d.com/ScriptReference/Microphone-devices.html). | 0 | +| *Microphone Device Index* | The index of the microphone input device to be used. See [Microphone.devices](https://docs.unity3d.com/ScriptReference/Microphone-devices.html). | 0 | | *Auto Request User Authorization* | Whether request permission to use microphone. You don't need to enable it if you call [Application.RequestUserAuthorization](https://docs.unity3d.com/ScriptReference/Application.RequestUserAuthorization.html) yourself. | Enabled | | **Audio Codec** | *Default* option means trying to use all available codecs for negotiating other peers. | Default | +| *Loopback* | The sending audio is also played on sender side. | Disabled | | **Bitrate (kbits/sec)** | The bitrate of the audio streaming. | | | *Min* | The minimum value of the bitrate. | 0 | | *Max* | The maximum value of the bitrate. | 1000 | diff --git a/Documentation~/images/audiostreamsender_inspector.png b/Documentation~/images/audiostreamsender_inspector.png index bcdcfb4320d259436730b7e72dc7012085a0b1cc..efd3b674241748140426376d3e03ab62ec944881 100644 GIT binary patch literal 14894 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 z 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 MR#20#mNgCiKMQd3TL1t6 literal 22440 zcmb5WbyQUC+x{&$z|b?&-60J__s~i!0!p_?OLxQ2AV`O#pn!y=BGMfKH{FeN_weqy zzt8jgt@l~$S?_xPP?*K+nZ2)T?`xmO=Qz$CrKO>ak3)&`=+PtmXDSLW9z8;b1HWH_ zF@gUn!9jb#4>Z>o%5sk?hpBgf7a$wBI`EF_1l&6_4B$1kv&w7NM~_HZQ2)@LynHI)Gm(Vo9Hqoij3PY@g=pe7bYXja$PZS13wdpsI3a!WXKLo= z_HD)m-RRTWXxhYFIv60^F*w{dWctc)IOlN(U+s@tPtT?ww}A&v0iK>7e=}YpxBQn} zOAK?w-G#{+!XR=8bjv9t&$@?(8EdNBz;7)VH4pigaS%B$gF=$_LC|A@L>3h=@J5Wj zFR#At1-=dewgEFpGXMKiJ4FSA4g9fyz0X?>qCJaGDaf}mkajuAj1{;@X5wciY>)6P zles@jzP{Q0HgvKvFvQz>w^|WLIQ6Db3Ey|0gj4ry5(o7O<;m=<>|2c6c@KA=%S>94 z-7(L?=>o3W?k-y&gueZ{`O?5D1d&BMvseA2H3w3;hyMQab!cyy8ZgO zOuE3kKL%MejW)v#3m$V09y|)`)jhO!T?~%xY8_&`6J-{-yvO=n=0sNf{skw|5c6HF zvKK6_cgH(-mUoVDg% zIEQJ2p`T0brYh^D-!1-ZqLB7$yj0y$VGgNBU^5v1*ZOsStLVcj@uYD}XD$mqC1PJX+RnSqiwV zxYvkSlivO3t4(W9zHY^v%?K*TqhA!RTREPY7`UWfO9GlPq}*z)0rz`bUn`sY%MBWa z1AZMe@7kl?oDA>|NnOrcOj)7F&wSRT8!vxd8M{cY2x&E3R@NNHvXz z`_*rsUl2C`_v$*vP`UM7r8vrr!UXBriAtlnH-?jyMs@?t=a@8a%i-TgV#s)ffzv({ zn1uKSn~Q1J5vidJn_OENne&O49*HmNf1|r~LY2Lip3j!6K{#2#wNxY)UsOxb!vk+N z*kG4)O(ohx-J4q`0m7g7j-@Y(qnnP}afg@^B+ZeB56F=>PS0`Q zEovV5R+5Xmm1ao$H&Y1Pr#Fc6keh3Ew;OK_3^Z`2`LS0M zGc?WR$K$qIk^m_XZ9`uNL6-NgMgox*66Fs!Q>}YjwΝOVbe!GoJ-bUGiy^x5K#M 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 diff --git a/Documentation~/images/ice-server-configuration-browser.png b/Documentation~/images/ice-server-configuration-browser.png new file mode 100644 index 0000000000000000000000000000000000000000..277fb5c07fdbb6fd12c2be77985580b44ac7922f GIT binary patch literal 83633 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 b!b`a%S`j|wY`eK@xSzVRmQvXx%dr0ekKLa- literal 0 HcmV?d00001 diff --git a/Documentation~/images/turn-server-settings.png b/Documentation~/images/turn-server-settings.png new file mode 100644 index 0000000000000000000000000000000000000000..b3e91bbf0f4c5d148fd8d2e5b199bae37a5807f6 GIT binary patch literal 86391 zcmbT8by!sG_V)o1L_rXwTe=$rB&EB%Q(^#tp;H8;yIZ<*=#uX4Ryqcx8{WdN=PL}%4e*iA4h#&K83tz000xF95e5d^F11nd18_plR728CRu+a9 zIDP>G4~qx$95{jn{`14)|L0f?mKx^S|9uYJ1`gxj$4}q-*FOp1&(pjA`)k`z)(8V5 z2qP&Xq~Zd5uSoI&bf_D(n;l7-fE=mSPHau)Kqpi;mDwbHw|ui}b*Z%KoY zgF>TdqDfCb&<&uC?wj9x?Q4#Lk6K3g&5s;Xt4FsVjE}(OR;A$5^VS*g@_u#eDx^$O zP3Lp>;{X-BFv&k&1}gBB-d8^Z#p5V@KmPMH3H*cA$-G$#1J3{WEeX7sq``z@p2Qcl z7ry^^!NR}Gnnd{@KQmAO&LH~J1pSY5BbU@}t3oB$el&_R2{fSIh3|6grgh36(nR?sJw5E9)1iRb&;WQ&Z!|Ud->8N!p*~+E~TDtW9AWjXFml#+>};`wa?26dfZHAc=l!hfGyqfv$+u@}QN=p2_E}K?c1{s@VGDUhUA^B-e3rt?e zA%ypLze1R5sYGqJ)0}OLzshRUKeiEvucl>_9qr3|ox}tZx$QjFKFK`&dM7&~ycr=+ zloSo_zh4;de8+2rXT`LdZaaw|Dm);0&OmLnjDjfCv_TN`>LJ#_=54;C>&c7_=z4T{kIM; zj%1g5V!MYxLokL5UOw6Pgx`(x)7|zS!Y{Dp5!$YYZ4Zw=1(nNaq_-W1*}lC$7?Wmd zalGD4Aa+^v<+z=YX436UYbR-_bh&jrnJMoJ#gfELb4Es9t2#@t^7#l!_e0u|UPGnK zWmgKf=|t4DkM{qgkS=h>HvWeg!=?$kJ!4w2DY9|ylSBNs2UaDO2>6iUc*6VHwuke& zwtLyK6x-H@7?)v=*}HZ`ev*^T1bwr}R8Z|a=c09WHj>ZF(tcUf;r9^6feq;r+Pf)bc~4*j-pDM8+0_3GP&p)U7$TiOuy9@m&rYlBm%4%Q ze%7@>K^b=&RMH!jy+pSM)uT2o=QAPnRo`#|Otrs&ClbmlRF2JhvUCr%(U;thT4wdV zJr(;@JC7Ro%U&_o8_*+eU~|ke z657@(CZy;}TW|Kdtj^zAM#B7tU3&-}@pG)nEMz!JJEX1}3TYr!4=j9a2OB zJ3+p)O(yRKl}=H`HDKBe9cQKf%6PoXxA-PQJPP@?peU)BpkuC=Wzp9kqh zBr|HdLm`k#8I~y**n|nW6H~LCE;bQa2dY~x^78r>M1|AR6497rcyix?-8zc%8 zc1BI_?RU4gA2!gfTd%iDy~=Xmbe;~e$=~w~_~?!AZ6~L{bq>CP!1F&|kJjC27AKVu zYS}bw#!gD6mjCyP_jEi)>}yyntgO@Rc;JC0Fl|o)DOv>@f zhU`?*HoGeab3fjl7I$n9AFk_;@mpuXgEBN<3*6ZqcTr(jNw#w}Nv*EXU)`OTd#kFx zOR6pZ-jY6Bofx%%Lr zEY6PY9F5Q8Vx?GRN2rcx`~1Qj;dH_9{i{S(i$PqH=HnjxkI=U|H>zfFs%nJ~bIk}k z;mOYPrU|81FYS!CdwUf#ot8d}6J2qb{>_R!wJ9^RFR5l$khYmTG3-i3&Y+$Vi}7gL}i9b!H@yTg8U+0**#dA}yVM&;*9u zdta{$Rk7r-nIf9Sc3=e=q-yTEJ1`e0zR?`=$JWz0cU`t_9;O?bOw7so0IiUPechZ` zTX0hg?=?kyGZt82R25(BfawlOma9)bs3xQ zwCI)9Q}o7*S^4kVu;!X=l<9Z+A zvu}?(V|6*ntvs_94`!B}UK%Dc@OpuNC1_a|qIYU0<2E~-1=E#LcT5oYKCeL@NrMk2 zoI6>Pl&_ko3oYn!aPNEjETl1_5Yei6klzsWMJOO>WhE98<_JFGG&mD@RiR`0#=T#T zc6PbV?Ntejr;R^|)>op;LWkaFyZduHza?mL?mGxa?Luz9xs=`VUy=a0#OPCuSAYxmv zmGnMeLgEUyKWz%{qMxJ=>ba(hchp<2ZL};s@?d?Ln|2}}A|DUWf$djI z0ZG$=&e9Go^a;(B4l8w#uM+D$URmmvf%`To^jhu3x0mk+^&hIZW~q&DO5`_fO{+#> z{8hZ&Enb+FQy1qg!`g@4e)Qz5uET>;SaCSX&hM)!pi=zZg9GLsA)GN|mZpB&Y1+|S ziq3x5RnJQX(M8M>b>Z3S=w1Tn{aHC5p{fOB-av#?aBS5;6?DaZPt52wN^|sl^wG*e zCweExa<7!dV3D*hX9y7+5}%zX+9Aj8EIrH&Co6|Lk^>*P0cm5%NVg!8DPq;d&J}L0 zOru)Kp_raA{7tAe2KhhwJgz;G-Eh7{TJyktWmM)IY4PpAVgO*izq`GL9cZ|sm39JR z0Z3o~iw;Au%JRs6bq-icZHFjw&v z&Dk<%`>;TL5R#s|f82Iyid!M{YB!IcC|V}&kGKjAhZAYIVU{lQrLwSTb{MDS^$eG& z=9#v$L>W?ZRpRv}TgJ7F)pdzDB5}neShjh^ZGYMh$-3ePLR#WU+|Rn6%dINW3%~<~ z@?Fds1OE8YBx>^(p=svw$iQu}yL9$(Gi+0=v3&W=mjx_6*?G`L`_CswkG$e{)8(+C z9>{Y{2oVa*szsY7kmQWi@Sc@|yyC&e`MwKL0aKHbd^w7hm6bW95xq4`{{7#Zkp%vR zoY@*(YZtm7JEQM?Ka**>SkfrX#@%>#GKftm(QaMUPbredR~FF9Qsw+s6?52j{>1v| zBbI^pMaSN+{v~N&%*NL3{-(T=lk*eWehHoZ#vn#p<9E07wzrctEv6T^AA4uOeXkg5 zA}@_oKK*W_@n4j-PSACl2aKv!&xm_7Eg#a*d$h`KP-`CX`!&8iN) z=0lLKSsfH`zK4=8qI-7tT6b1j zD$s!b)`j6F<*gkrJ|Lj&+ln644tHJz)oFb(of~Ry;kB2xI{>^>yLMPn)uJ}}e4B|> zx4lA00n2Lw@RTCp)XVqFC0v-}w4Kjr7fZI0Q=Yi`BvjPtT{FM78eg-rKN}Z+3UDa+ zh7Tqe#m+2!^`dkSrt^qib8n!%Oige{@j{JF#3#=zvjL=!Dve4Kk7vVA29i{RFd8cj7UE-=v1Cp6{XtLtzMi#?D6}@{l#MW7NIc@R_la& zONv)W{`T!+m3ApGOIuMJN>j85KX<*UTXtH+nJqG_fpQYw0xdF?xB_!qT_@)J>zEtx z)D|Uf!!&i4dyom9T)o82MT%h%KIlcS{<4=DBr-;xzX38f!nNulD!}$>)nz@5V;=qu zGsQ8a+!G2!ot9Sn`%jS;C*{vS-F=+I_D|Yb3_GCNGbbtYOa#8oO?`-W{+$3a$B4UTit=mo=1Jyc7rGeA5zG@;SAI3ES%1S=ql ze-}b;gl%(@=zQ6*oxZ<=sjj0q@!@<*IjxzUJg%EJS)k#P`&igKXA{?JkfBb;zMp~+ zTFMWx$!^-kb|~XgKc?P#FB_}W@u8>C5nVkSM|A%}Jq^zi&c|a72ms~EAKnFQGBANo zTKBaw^wb~(VzW%1yV7(e6<=CrtmQ#JsyTTG`GH}s`BctdClgh1YFhmP+M7n)+2HOA zt!)pz`yp-CBK4CZ`TQCl*zr6ji#@F~r_c4_irQ0Hs-|{F(&?`0v-B<4-FJOI1Bt`& zx8%Cqy?0JIK;7uC|1K;g;=GVC!aVF68>wM|f4_IxNHxdTdPr*i5wZgR6YEb~828lJ zRwMAj246@FqQ41zc|Q8#)y1m!V`Fy80Gs&ieSf#OfIJ#DdmQrLZo?HRNPdR@LLi?A z8~&iF=e8xaziINS(_Ol@bR5ua$~b0qCJ&7^(Gbr+E2gB9{e7J=5T-;EKSa9T%RTfX zQ*hEGCMms)U!?c+f#-hvX8~Wxa2mF(bIsDj_xQ9!>sD>B9h;V1QC+E;?&n5$_De26 zdTH?2cz^yZPOxMQQxuLY!v|Uq5W-J=yWcpfh}jhQudBqAc@w zz2-@>k)UGUm9RBf!2a_NfTiyI2uN-&@AG2+xE%2IjXZFwOaB);^WUouD0c=fFGPe< zQF~+2|8Ys?Uq~Xt5m(Zw2myZ=E&$028P!)}=jYEvr9TV~@K)>taO#bB&}92R3-Xf` zsM-etWvPEw?Hd*I^*38XJRCUxcDnNB6Cg_8hRo`{iv05`I6m#!&vMgS~6EHkFb;~Nj z1Rxs5ueck)bH0~-`L|((h6Cs8{dl`7&wowMOF%PTDQeDehE`9x4=?QVSerZo#HQn; z7=4)19Dj>{*TTq|0d;Vgb&%dtAxA_zPGTostb;5@Q1kHvpz4JWZRM&yMK%BR>Xh{f zRx>$LlZqPal;zp{!z=*p9jX8qHAZ&Uk*E3JKl|5lr!3IB^+9>ff3_6R@lwF4uX=Or zbboJ-r;e{jLf*|)dqrQ-ZWO|xR&3IC^dxlx%c0xD^|sYU1!6o90z8?1z(-2=>FoqIB0mPSY`lHXkCEuKU@@!f zyet63%ukKD==LOYJl@Q<>3JM?bJ`1|HcdX++SR))u4K-vI@8$m$FV^EjceC&QS30* z&8rMS+p{|lK&|b0ENX>pz+A02U2(EM(o}UqviEo&FGE}I?WH+WA7Lg#+ z3RqY5@ywN}sHcs zrr&Ek55l|*FSoNAQ7l`gRlp~((n}HK2h1PWipx4+OSH)X`6_n4haA?ghYQ^OL_GpN zV_{6SU$N^%(an>jOcEglMDZ&i&i%wwg{6_MmA$xaby4}|>MUOXD%5zn;*OfImSNj^ z4Y*sLmllQ4W#`rV#np$4a_{^7xq>_DLHdMsIw0<+Rt^_HI(z5&FmIkg=v@?>{FjIw z*sefP^J|2AI~5SXA+fhxTy3r?fJ_N=+~u;f{?^h+f6r(+P&@bCZ9trW?iH6aa-;&9 zxbuQVK11!?_XU@*7uY&>KaPtYo)l&m)eNLY)>ll;%qZDNJe^O1^XJKUK>b|tm^9}< zO&aG(D_Z&7OGq&Ybl5R4&>jl&(|N8pV(vL^s{!2?TG9FXshP`~PX@+mo$AqxJMA!Y z33xVay>GP{?Dh)tpPFxf79FRjutu(b{iJwMic@72j^Q27PX-^`Jk9r5jVI9#g z@Tsbu6L)X_T9%Fd+L&;eyB)8GnWh0E7v2=Pn{yh{iN6qFCv(1tJ;)U94G~5p)@OPE;ITM8Q6@}` zPzl;wd+o#KXToQJ^yv)=ux8WtNln6=IfX&VQ(m-HrR;*&UWEd&qFJiuD?tC6h;|O+ zm>FY}JyQ|b7P#M)&$vG@?=R(&5{}T!z8UH^;y!9Vty-JTZanP!?NG*01e*XcE;!q; zfYv#$xKf$p9wGqFAJCP0&o#yy7@xfIjX`DFEvmI_;`@RX4T_qpW7%-2D$!FF}uU*P7A3 z)>nIMyCh^5Y&69zQo5RsyV}RLZj&jAB;QmiSCtk3MO_X7*!G|Yq;H}I4SL_Fx)f__ z4_z$U%HQ^nNK^rn{r=FoZGURxX^~ndJukaY)k-RL(ZPO6Pnt@bBou3#MD4oTzfaez zYhGDa&td-gZ8^=f0^HsdvqYt+aLm~T7NJ#@AQD(d+oNL;{e4_zR~h(mD|A5a6Qdv~ zNKem6oY>P5_cQu*K#IV1zr2S16{zS`3?FoMf8?E!6^$Y_NnQ_ll8>FRlz^SBn}mB7^xRH-5TwXYa*-@bC@&3fZxOMGfEt(`Y3 zt|omB(Zoqu=?YvN&3Sg3$(^vkM~yhI3zxiIHOOeSZImW~K zSG_c7YGi{ac z?dc6$r&*@yMV8nsDRw8JYlEI9uL(P3!%P;zqdwYWT`bzSd~k`H1w;%nq+A=;m-We8 zczd4PHl{b6!zMb*5?gGk5_#N|W>77x_PI zef2SB2~}s|;v>B8))zEr*G9k$w4LZ{n7``E#Vn&No~N~`=$Siw9#o8Y)8-s}f_$lR z{K>wvsMIKJDy+Y%G{50U%8e^wl>~c19trQTYvq;hs5YFL+_Iw^WILc^arK*=ls@KR z{AKF#Gm~I7*##t<1U<#~P0v$jJB#8oNf%y_1?Zp;Wz{G*+Q4Ja-aq#bGQ+MTm=n`XuMYkxZnZLQnFH8Ue!j?{~<*w1`>3`U>obn!#`PhV8r5*)m${o3fx z$dR4lipemMVJCWZUsRWD~UQcvW+hYr%z4<#7M{ zMi%_d$d?hFp5W(4T`9I!vM6TiD{*^DJ{x$v3C!ve^fJMP-+{3;gM zd45p;Xzz&Z&`8QHCZdInV|hKvNH>uBUF7DP%V$gsTzwP!{*7&{v5f1|=iVf;$O&p$ zjomvrI6Yz98ys#V(P82!Bgk1VFtZ(4dX~f*rI^>KZ^Ecq zYr5xM2mRaU1<|Ne{ZX>hy4S?uwjn+Q?~>;gG1&&bMrkfy<<)$Gf8=!Rqe)q|?|ZU1L2k19=2<=W-8shM^A>oYU`U=snr1KW{n@1Sb!@0j zW;?5`>N}cXC#^0C6?p~3v@tee(0L7yXt((U{V1@7UPQHX?M4!1oE}pAV)@t1B*=t4 zKilqQVDsfp)}^B~)dY{!voQPFdhy>ZBa9s5x~_5f4$=n*<4DWP5N<8=<5II^wMc|i zH>TKwLo115?zD|Dy*hdBh3i)&xFrFm(ZZ_;s}B7;I#RzGoK!)UQUmYWRSFA?uoK|& z1tiOBY{q38*LCbC<{!sD3N@c!MbUz1t`N#F0S(JB*8(5+e< zC)q`|E2Vt zT|^I6-o&5~J;5vBxgO@J)%Pvh-OCZEvWUUF5k`y?O}U!-3-a6nI(;O!1nKM(n~ZnO zkZzG6#BaoE>XYZHu7pGDzyz5PRb4sFl1Xh?*U2~W=Q7=vK;<6sj;?-v-@{k8Hx)lQ10dDw&`>1 zx4X;QvtPZz%6{@{h3NjVwFXg)?jS+lZ5^hQY+bZ17Gc;Z^k=BC*F|-Ps^U-Qsc&8N zIj^P2jfbhp1Ui`pVqUZOD1Re`tg4F_cej(M`brf{v!RThsqm$SvB?zU$3}*5ps;<> zzqx^6z=R`T(Ail3Trd7nycY+nM)i4{Zh899w}iFK_kbcv$j22nJDzRi{l`$lFj%29 zn@mXP1hVr2YnWN6Z5NJ|(I=AJuhB5L&)&!&H!sk0SO;ezud7IMAg71wrV7KYGq1<- zs3@_Zh!k$6B|am`hCYJ}I|i7O@^fB>nTPw!)wa;bGr=lcZ5kBsa}x)wQi@)ZruLkg zu#@XIQ;HUDTlb&Bx7$>#{~+r?Ez!nxs$K8p5hXth)@9+t;SoECpH(Q+L-)mAw^m!@*q%)Tm zOajElH{hf372-a`?tK(goJuGxkeZOteO@%68e2{5G-sGAbFSiw{r!Wh)UYg8Bs>Ky ztj{$-hp4o^fgE7~pZvoHoPy(A$?7Eh8}Aq{XH?toO{V&@K&r!|h}0 z)XSz-3pod3Al5S({uQKO96_bJr=J`;l`NQB9J{p-S+bkA5QLtF;m zij}Wa506bqrDaeneZxy+XrriEza=_7SL@VDeoP&Ly-k~9RoR0Q%o?ZTtvnS;989GL zo#UwVkvqJ^+eR;J`nBb%LG-#2BYm1~U^G7eI}OYz+Fo*w!0+~FgCD1Jpa43u_vsqi ziN=Rrch|A!Zo}muW2B?z*_pPZkRMZnDgBpP-{a$>PmQ2^df=6l2pemlG^ZiDVIZQv z9ok8f)ldoXzroxHRX0K=mTq^c%sbY$I?sly53ekCTJ z7GhZHH||1>jzRMG*n=JKZLQ=z28uiV`bJb}r6&;9Tj5d5x*Lf%$_<*rQy;`V}O1)4`IU3d`(22mB zNVc5qhKbDOe%>?7T$mPHjh~~b>Dio9Ydh)lLx7{<2!nZ}dU?mTDN=`qbjXSU+e_^2 z(clz~dzSFodpN|;N~`T<%^tR^ydRH)x)}^^;NlsyIY{|qem{tmqEb4Wzm#7^vaZ1f z2-Arq>}vgAk#$z+yaOYZ!S5-*Eh$gugvNoNb0{qG=?(Y^_td5sN}n>&+EYaN0rlPo9h`z{o<&%yEtU&WAw8#5SRCYgs~ zT+&*CHqY<#Z|GhuXGd!lfo0Si*Q7yam|QVr@8YkV_2z3;wzagHN>$dY^cDS=gXCQ@EbmUntGPEb{cqt(SUp}ZxnXhbFsK42|itJI$Ecyqo6km06{ zo~vYrKUkouv|J;}hvm}k`DjZORk_v8pm%8;E4E!A&=;zbl$v5?PLnFJ5*1O+hd!ea z%b1n?sw6kPxVhiX^{oqPok0JU!uu$??mpz%=ISm&m&PIrdo7uItiii-ofql~ZuJ`P zrThI$ZFVQqr8O#5*QU#;TXlL_Vb$rcRgn_VwXWV~OMF+UqY>ZN-?8j@T^SvaG=&K2 zK()JxSvVSkpPrD)99fGk&jlM?oSb1On@P;;7K*s7fY0hL>Odv-JPGBK{qF8y?cCY| z;gUNB4a-yM_z@_H(odNIe)cTNM1{=CVU{?K=jd0U?@5N^m=K9dC?Si4uxvlNy z;E=lPTKvYoV%v-$?BZ^4S?q)6q$Avo&q!fsK~Y5ZbAO-L{g3{I4Igfn+Pb|5NWZN5 zg$R5x&u&V|DYr4oeVs1BpXMO+25H$a`rjB)r<@PTFA=dQ8qLh?2n6p&!^kli!)a?% zB;rN0X&li*`#095g?{Ba{c_2*Moo~QI5T4RgUV+qA&C6hup;L0#&*^)U4CT{U1Xha z*}1GTvE7^HTzsRj>j(i-SV2{hDVJKdRfZ8XWF1a@hi@5r3R z{T@OY)%fjh9gdB%+g9#+)12HA87Ua*@(?h_G<@Fm0lUq}$YzD*0xf3G#?4%fPxs^m zdvJa{1w*=4D&-Jf#L2IjlmEo5l_Kro_}x?F%su%y2uRE6i;a|YGt_7gw!cES7p@@Z zO~!O&tRWw4PA>umJp|fzW-ue83u*2f@foSx;>PHyV#Qe0redGL_fZi=JG!IUcjjOweVB?fyH;n5y)b^)YnSFlpc$WnmYBNYGj)Jm}d8QheQy0QjWz%ofc*Vujbz*iU zp@~UGWLk{YPDo3ft;E+h%YES;@rW%=ukL>X(V!fJgN+i~hN`@|@0p!dQ|0hPitUe@ z0MqbY8W+dn`19$m?=1uRBuvdA58Wmp~-#JlJ<>a?c-2pP* z$3=V*NgZ2V{K;&*=8cty?7??O>mq}hY-6lXRAgtc5${JESH9P$MzS+Ftz3ENgxva_ zSt@CqZzFql>Sgk%a{E_FI$5 zB^U}{QgRb<_p?rzty{YGvmW5eVpkSt#HB{37d(L2xAbOS5w-3?3V+2#ec(=IH#c|V z#(0}fau>nY9W1%djana3ZX=0O0&s?7O-6$Qx@)r)qh$pr;Wx`P5L$1j_3Q<_KqiGA z6J|l1PY1N5cvUJ!VN;KBcky->nT)s$P&wz(B1rPFE>dh%#?&;(12&vxnG}e!Kr;Us zGJ0=t%;;)rSwz@=9WT@78Dos1N#FVbOwDr0OD` zO@$Ju1u0c6eOpqLJ`XkCqx{Y>4+G1KfM1TEas57o86qQ4>^f{Zu(V?bY`T$WSbx)6 z=Y=KHefC_M1!eU*wkQ;?*mE(DwWj3&A$CF7evvGH4=ShFc{@E7P|36(cAOEd+HS*_ z%+_q4v?fLNh)k!gZDav=nxKiWFfDpknHl908T}zs8#!~ZhdUsEk=i$Bbl=^K;uB6_ zaG^5!U>k~iX#%Gmd&aK<^digxobWTA1V6SS<$V{i+#6)Nu7nK$mL1Ez;@lA0+R>M1nykbxim64yIb2fpohr8%A zXrA_=&amu23o+_YadMwrDzj;-*Kf)egtx18|Jb3f9c{{x~Auz?oS(afkEN^k}P=`o=WFOALw!s_8YNMR~EX4swq|m9E%h)90|i) z39~6-dOY%3O<$SRlr^U4Y=YeIgPNHII?FOngF5_9aWPT<+j+{cA}%e-JUlf={Fm?j z4U0efz0_b@#!~8FZxf=D&W=S0cT|_nhNhyHwr^;bbke-b0guMW z!1;AYw}P<~iXBMV8;p#*-?k=jn6x4?^`p4V zh;iIQGQn>^@AcJg_j7AW%EA#^C%D0G65>^w?7HeRM4e^kTKiTTx}V0t15EKBKgtuf zuT79X8>qr1s?0f!S$!Ag_&oHzcC=fvw5?eVqiuYir+0J!&Mwb$Pl{}J<-nL0R3e*%?s?4zoBafYY7)$K=}u|mfX|{v)z;uXO6E* zW1ibft_Rrb8QS3G*423C#SZ1{Vj?+KT`RM+qZ_Dra!VDbaBik;koCwSKF5x*cVzb7 z=X;;n1;Qg-VM~>mS)CeKLW?e?`qqHtsTar}NYs(nsBjV`=ww=3XX4(7 zo~ORpC#UY%XGJEf7kdb0oNT&#Jwp$2;y5lANsE^@D2$b!c|hn_^=2e|=NG~^&omBf zozBVW?LQvs*J(6(^9?&joP78k$4B9KWdw2(L(OFcayja1-D~x9M)@4Pmun&k_mNnv-6!>EqV( zlrMwi^x*djTHbDkwFazY77@zR!K?<=PHoDYSn8zNagU--C5 z-@-!CddDGZ%7qeBLCFD714bzn`L}+|^l@C1=FuY|nYv`PrrUc%jZRwXRxe8SS9o0w zx9Tt?f3L5)3QpU@yH!tR->0>ZD5>HSBcD#0J?-N?Nw@Zt)Kzdgy2Bf%dW5E(}zE zfl)ZC0529{t8P*<)!S%uSc0{79gATXSCeXAl1lIYnhVygGrqP{ch5yDy^voGa$IBI zsmoA4O?dcDc}0V~-QEgX1{vFWCn-+%74)`uer_S7(q-l?;eRNe^f%wW5{)wUX%xTy zVXgZer0arHm83#9ron6YlE?!7%0Y{LF+Z<{ano<0}lwRFPOY$$yYs>)}4biG%p;!Lgs@Q=wg2ct{a4 zaC(A!L=}FA0NTO9s-39z2N;Ho0UK|AVeT(0aDSE~l0T*Ke^{FddZJ+fZeF^ZWm8nd z|ANg#e7wv&D2bm?xRrNJ(T!Z9M#2UrXo(l+qkW&Gwch;I8UF&*eg~j^Q#?fZksk9; z3{b`e$mXSLwu;C5BLNctf|^NzbE{|v+!p_Y{l1DDC|G6byKcq-l%Ig9aurf}P~qo) zd=KCZlYL|xpwu%>gmrLY3|abaySeK6p5U=;u$L8>5TBgg1yNwJR*7EZ8! zf-jx`(89cAOE|O0qJNh1YdgscAnoi`1pm!W2ve)X8)xhBY>ijL-rCpWqkcZAcr06CnI9fyTqVsP;H8CRTnR4J2Sapg?VHf68ojvcxr4LDa9z`3uGM1!z&!Au92I z%{H-!_M8~G#?ujA4Bsj~fXHGSV(tX;u{{AM@E+jJJa2aM#mh^^C4mf60EX+L$xmhF zNqW#<=%-Avt{&z*1#pzdTR$Kb`0@4KtoIX4`T8Ent;R=0cUuqR3fO-FJ33JX0D7>^ z6-ctu1@fc9Kb-XS61kU@PQVxv0Q6Ya(|0(AX^Tr(fdsq^mtR2~W&j5Db{&)O+jQS& z0uAFZ4wX)Awo%@r=Fbj$04WG6h0L&006>{@XbRgF{l8kAEE=GTzcrUws5}kOWx564!^9821FM2I0lwvSJi_Y?$ZI}(xIGfMDC%~(1rUCe77z3@ zVB>6LY!KHEedURv8EtdDSFRcb;G`!O4AA#)f2dpdW2yn9B-=uD$`snX)Ti8e(=(K* zQGioJz4riE%vJ!O!#787w5z{Zu+o12*O+VHLGRPr8K;Op`&X&&(KRe`<94>6^5CPT z84Hkk|E=M*I=(^$Ts8EK=myUWJ z09|K#n5z2T<@PfYyiO~g$&>lZ-Xl;l@2xiYo^F5o_bQ|UH^|g>2W+Z(Z^o(5B%;>? zIp}4LdqqX=*Hn}7^kt$`+#(cG{Yz4KEdV1bdY&jSs(B;+={Q%0>IT3S^LrcS_O>{u+u~z;g`BrM@%(3t$LCZKOQ_EGkX``T7=^H}Tpw-1{Zf!CxhE z+B)dU>fim;N`>3GeoC?hGIi5wRY6QgPh-Mu;#~hRw*2MwPgL)|*)?EBy8-zjqQBaJ z2L=oQv*w9Ll<<^!%3`cwo;_%RRZsc|q3bqHTWSG7$3=FNJ79hw(vP}M=+XgcF%{81 zFe^ZE&F-GPM9#p}pe*@O&7>TBt?35O8~}N)*{?cI%0?%N(WHsP>3g4L6Z1V>t>+Hi zq5RwX7=I$;wd$k))oGdfaBm3-&jCQScJwVFbU@`Q0U#-*^>5I-RnJa=%=k_<51_QG z0R2i{1F3q-!zzVkytM!U=}qKgy8!JBBuy)S9-5sZG3_<8+C9nVKc{&aN#-0^wU-Hh za}5-Yj`Fja_Tr}~S9bFZ4*IBYS{>8Y<1pK^q|iGuzqABeHCMMpAM)!bc0f`dE4UCh zgkefXC~nLsrx0nO9hm=ADb^M*`enIl11(*ERmDV)|L%7u@N5XRcQFf8i43U-r@(Ur zI9v_2e911K@=vQgHtFU@F*{;iE<*cSqzl34{HX7PoCm-*kHo>-veS1R>kH179wxfeusGT;qKXvjxMo68f^$>A&cmC$KU z=%Y(|%J`T(uy!z=XYJ)G%2S^Cz2?~ga8D&?N-^?$t?tCM=+(DPV`IocQiu4xIK>ORd=hb6Gt~c-) ziudor^1xs1jeq1h?3n9YP$h@axIm`W=&lp93$19sd0~ANT=*W^N-KVQH#h)NXUK^5 zr!*-o9og_SqlxNT1i)TJ`o@-iYsqeow6A$H1i68QHhEJ>2u#KUKr{fTC9mXz)dwNEJ<{%x#%q*uT zK9Qh+bdSB){rPOlpxt$eF?RKs@T<6kCtxs!^|`i`oiAhC-AS!U@fecVjb;vQWqV9L z=!+aqF@Ptk8F!};{|p%^(3^bwXooFLjMg8QhSUhuGHuYz;H}f3XLeUxZX=1$#CrQC*Ff%;={XZ?Xc=`9+)ggL&vC=(i91;clc%jm z1LObW?k&T*`o8EvK~cb_K|xVcx+O&rrB#p)>F#bt3F&T-M!LI132BfnL6Gj2P@H}J zmhhWrUe5EMdBX#I;KsS)Ch zx6~VZOv9o!8G0rt_&(!lT)SPXSkRtPwK~!wc=}1wgyxvZl~e`YUvL2R~u-N&)w z3@V>^hjItqR_T?IMROK>5Nu=Nn~246lg}Ccy_sHMaCJF%quwz(^skyPMy4zI#bMoa ztF6Y4#w<+_IA0bNXPcBfi@cI^BV21z>Qaps`4-rs$Z|;0i{*_L6;DdtY*7@$zKOQd zGe&XkW_!1DEA_i|rr9g1A-o$6*hcvyLwI6RrLFJx^jYpZT1ss^-i`QG@%;|9?kNTn zC9P58s<&BZyv?ihbo>fEV$V4`+K?ZPw6G~Z&!L(XHICmZ<>&S;dETF|e4c)$I!|Ap zW;6(<663YnB|Rl(OY#*@K^L0LHc^W-(`?#+GU2M#?z7e#3^$8*&MB31a`&~`+VG9; z6`MUXTK=$Ltfui-RTRZpnA+I&GLqMjm{{%l)%>fhS$W2RN?}4~QXaDFBjQ!>9iBW0 zZ?u>w36K-fWMVm|b9QJmOw-JnGMwPym*_&Wgs#zupFOcQ>7@a-U+{beHWjl^U%H!+ za-;pZ%~Oh*&RUIIa;o+-vN9uyBdgCF>y-<%C@rZTYf8}PId4YX#JOE-6hAOGyzlt! zVew$HQO?-lf=$~;p+X&{;%q)v0}(;v5yx!YaU+NUWlI8Yy;r)s)2)UYdS~N3)G$PR zR~bFXD!PyF!_-+qm8PkF&7(h(h>B}iR-G;FOm6Lg0p7BdpkwgpyPD3ALsg?wTZ?!m zWh*v>HjeaXUpT)!v>mRHXvq{!AC;_*O}(3C`_;6$s(D#fhP!`)RGckfx25GUNcCeb!eEHntuq2i=?{n^Jgs zJx1w~33Gc@D3w-c*X~4cTUIcE(f1+E%N~Z_xy^J-M(5H6*GOfbv?XTog!4;0t7AoR z&(^v#__WHWjNN8d+J6aT%r^{O@q`_zBiyUl(l`=6YNeF1;tsbTipI9TvEz@`{4sN-L>eiV2O~hPN-AU+Ga96rC?rTD@vp6e>C1FQ}q38`-Ac~r5xYQ zLui6^IZ&b5adrHw z`8zQ$)a6UNY(&}__&M^wmB~g}KHY1Si+}MU=n=_&RF_i|cW%y$*+a0DIIPT0Xnzp- zaMED4@Y0XjMa8mzn2ZELHD*=Ixb$7asLIYz<2JItmmL)JK>26atFmS?mR(WXOQ+2Z z!^yaZak<;PL~go_^eC;0arDHilB?L}-U?|Q81QdxR*HO?GVEPndLaB{Lg$%kBw287 z+VC?GPSyM3UiR)5FTxm(hQ_1%DDnv+DOs2%hyuN&T9&cXIzQGpw3D(w?(dG3iXJJv zH~Pro6b-A!nG|eJ>K)+?)Q@r%BWk#*q>znFaVq&3zHU>gsH7^w^Qegq)OSt7Z9whb9qhr&Y;%6k&&BZf_Q7pobk2IUkeMn{2%|`@qt#UGaoUk zijZWQEM(cbnLlwCn}f#c>pub217RrIyH!1}Z}HFS8Gg#vAuFgwCqCE{yIkf|<74Ce zR5F#nKlQ~yeQhzl8~pfQ{O!~h{YQWQ+O2N>>bgxjK>1G^j9DLBqqjb{#rORabR=HQ zr}3oY`9aM4$fQ+)Q0oFqaup#}v?1!MzMfQjGo>T; zAF1x&cl}#!v#D!k)LF%=y0uoI6~X=Go^H2;&0AY|+}`3Ka1?Wi4L|$hD6Rw67NG=d-u>7t@U0>yRi2|GASn&^Pgb z*w1!NLy>NX*f^kc^Yx10lXU7CY@l;L;B!bE-arL7y$P7=5FGmSt-PA&St=`n`Hqj8 zS_)RA5#VT&@KWRA&kq3;u0#1@nL71~ADz}tzubiWySU;RD0jP76io|=`Nm$|Ip7|? z(-n6MgpS@0;9=I8@r_X)3wsYXWBZ3t)Nef&Ykoc6ojgizHzmY>xAldGUhPCL)c4%Y zHIqD1M$PDgMPU5&vo4FyTl?9t=f9r#I#UqblnMt+HFJ>n6W{ER=Vb+qpNTjJDRnVJ z&HiJiMjTDG;%6VhkS`w!D4}?A7H0wIF;jO_V6o!&8Ym4WQ33hZKjPaAVxRT9XqZ9q zn63YrsS%sS-{IeX^OT?q<3hmg??51B#g!UjK^Z6a!>o?*cl1D6B!_9+VMkyp+fBNc z41pWi+^h5S`IkzYF7V~U1FQz{$G6=rcmr6kV8e{maKx0X5@>>0D1akxN*ZILaa!*u z$O#PqBM+!s*6hmp+#1&K--`4X*UMON6&ypHVSJFNDhocIaa>l~*s3|I0Faoq#;PqAC>^7L)Va!jrK^|@_J_(;zcss0+~a^J z`C!00bFbiiwXNNXGso z@je*YNZnCBvXHi=?`M4Uq^Jx-H1g?Bpfhs;k~z?q4r%!IeC%ZI;@Jvi;VOwl3Kb_{ zHhXlfcW_L!e;>{K0#Py~n3@m6%c_M+*f1x5`5ndo0?WFu%9b7Ec`W1hb8$QKfQv3= zOB=S00z^Tg0>u82xSxJIE!LtH93T$?BWEpiXgt}`K1iOjmLQmG6#Rf+NkNbD&dfY4 z8{=!fW|epx>;IPj-}ZvqO+5jhJ*^7zb;K{iAV0n2m6zU+A*ibZPV*4#Bp8^i@E^}2 zd?%D5lN(lW`RJzXMC8(AMYHQvq_&1;tY3DX7rb{k+_fmGQt96wGK&-m!wbT138pPM za^M&WMTBcK{W&^R&(Q2jU318CFyrJ~A0a^r@p= zAgCMhD0D~sVp5)K(bX_48uTolhnWEp6Onuq z@vz*iyw!E@S2aam{zpuB+PmO|@&iZ^5!xsUJz?XaZM(kQ7MtdWnJcS3zBDYVPB2tPOhJ6M?T7RslXU4XFRo^->8 z_3JI4p;&v-HVMpOa+F4R8?F^jN%__+c(s5PMaXu|a$#HMY7x+ME zSbZv))ty$(ulP+gdv4KXc1|VmW(<3>Ys8YHMss(}djqMAcW-9bG5VT%#*}4%_0X+? z%XhTLuo-b9EIf{I)%FX<-cpdMa+u~!eJ$?ZRs zFxP&NzlOK99kCbCnuAh${*2lsm9@o`g)N#Jo3i&p8kXyDTuR2DTYVPlsfLp89X ziSDmuK&7v77!8g8jYV-NLxgkEwz;Ad4XpS7?mY|1*l?-07g(8<279Hz_A~f=TU_ zH_T~NhS7juBZn30-n&EoFkF3<$;Xr>&qA_Kh$n(=sHE-)m8XXIj$w{rG?e9+c{rYa z;q+58tZ?|ud5^Mj2g8SocsoAooJiq~DX5KXd7a{7l7{de=g zcmuWftR>kcv4PH_MI!en6NUBK46_zWlC(6mujD>|L{sO~aXwll8ueL?!DA9}Qan;I z_Or6GyQ=Ys1%n$az9CK2aQ`{Wrr@SUZQR)p<1JFg3&pH2HgDmhoMQ4_?A!~>b4q>B8m=U0yw9Ozb+tI0_j`mL!MA>I&XHQZmEu=fEWYoA zpZ?F@2D^KY;=G1^DeZu)(vGGlqeMxDyuR56D7TnCeREY{9Jh#1Q#52AeO@r5*s7#? z_C|hcctiFF>ndiGuGd|v;sZ99OpAtU+Fb^X#W6;SRv?!nna5YQKfuz((;MURl3-5z zl&SL|IelIx*4Hl`;wm<@GzU>urYg1PsO;Z(wvy;K9wy*5#=rOYSL^%l;|mg1OUD}V z$r!P{S^t1ViBRc#_nnXZM?>0{3{_hFg6{gmYnkX@ur#;s6n3+Zh{tIR@ziLDidT)w zif~50PesMC6EUJ=MH$nf&3e^VRWh=NHNbD$uJMW9CrL37q$H+LK(!-OSBW_Vr^ce?%r4@sk+HZ7@FVnV`NEg$JF<;o<*8<&4kiZ8gD zJo$26tn9E1ZK!-WVKn>G&|SIKe=>jn-o%D?T_m@~Xk1(P=ikO^jz(`J?i_aa>^2nA zk6iOGe%x^`62GjYAm?IkMWhlTdkf>}Yv82SW$#-K@y$kRR`;|Vg2Yhh-O~#81QHcN z6HYV*N67XmJ`-=kUuiXxL>a)mo9y6_Q}CUgDe-%)*CJRcJe!1fRR(RSsRM23bgXYS zc`*vP@N$k~bbWu+&*7&g)3C;v!c+CLe~;r>qSDHhSDLARrW#Y}sp{BF^)7~HOW9I+ z%?B}}Dr3V&*obnDC~g)P6p<<@^{so*EwX1IWPF5%F@+r6k9x$YNL2d|5j<0~04O_n;fu--3OF_Z`?BQ(S?u5-+f)E(|J-e?qP3;jZZ zD@&+4LAxuyTKiEuI$_EHn?+Blx?{FIpr-awCfSo4E4LPEY`yyx;oQR1TT)J z))ePil9kALO3;6O?uh(53Hk3kcssZ*Qf&UJ*KI$d3n*)Ow`fQ0UBU0;ugJmpP)liZZcuK!L+t5Z zSX$4ayfJgWe&NbjkIB{+JKujO|F462g+m(nf#ZVbh?!xIG4&%Yn^LVC1{Ip5 z@583aio@pOA=b&!?% z7?|}4sH@8)m7Slw9T(hWin`pzhp16NV^9iO2a6xMMg}L~=la|P{Badd-I@DpnV@a~3mut~d*Azo=P{xl>?fo|z@+%1!N_T%7lsW*3s*c^zvE5>tW+ zS0ce9|1JF8k}sB=7`Ro{x}&+XaIh3ErK)Yv6GS7|Fix=_Bwz zSBW+dEJ7UTzZ!RV_`Jc+fjmF33qGg_sf1YYpGxEo4FSKR;7e7nn2}%peig?~oi(wIB)Whr9Sga9-=Z z`b*r|@6SvjQk>i11A+rDkBZ&7BJAe3x8;T%Qk$vvG;|%$qb%l{oB5n}W zfaVMA5l%+5P<88dQswC&nF|{s8wp~-I52T67pcK{s2;_ zFN|-0x|@wu1Nyyh!k~;@hbykoMp{=qesR!y@8EL)(l3FDzO>^moU{Z2&}twm+5rF| zx#fp6a#+V+t~xM)1H?9tI1VZ7uxKIhAT>g@wR$$L&x&^PJ|hog&JkrF8rOu=Rt@aw z?@SeZ39IJU57*wmC$k%@8YdryA$;E_CAM+l*Qa|&A4ud`!e0i5iICkLyvQ-O4%@3O zR%jk*A%?(U&H_Am4l-HU4I0Cx-^1(XRGkqa*#!9H2c4%eM}qi`rG_ zG*=%}`6Ww#Jd2CqO1)PRM3Y!@Yrp`}f8F-;c5@jW9WMo6ts1Z~l_8oD4{c4C?YABX z?n5Xtz`X-kRwsU_F;vdqQmrNP%N$@nN7!AAxQGE+br+a3gWuj=QY#3dfRF%~RLV5q z6Gtf;)5TdbCh~qdSnSrfOv_-z=@vv$TbllyvmhZzN26f0LcP!i#EuQ!qjQWU>Z0H` z#QW?Wow%iycsD%FLRC?nm@nlZb+j;KRYUFck4 z)|-QKtl{mQA>=(X>T~!CtNoX%m^$E5bbB7T->{#)`siHX1n66eW(Iu68*eIha#dq|N)Q&*_#m(QakC%mb@;iB}kq}Q@E$qJ^8G0Dond#V51 zS`Lig2Ek9wkbV3q7fT)pf3~+^G|mU}uVjP*&6alB0Zde@!QvJaiFzAaWN7qq3XulA z%m+~)C%IOsP}CB-*0#a6&g5O-c-}J_3>6&Odooh$5G5aV+SW=-VP!3Moe}(uHSi33 zw8s4cv&U>=&^UNXm0?8}#4W_Q^ruoyIP<@R&@M6_M9I7^Q+ByfeQ0v> z53y++w5*ZrR}GJ&LLz}vjr;gHDLzc|Tl!)HcdN#sctRBIX!VD-rzlna*0DV+5;D%O z*%Wfp(}Lf!y|q37qL|K+ErhF>HH2?wU~U16Q|C;&f}6;Ea9llmfXCFY#}VPp7R8E* z6$4e6?4&H%)8oMOx#$>I`{!`?bCr+jJSMwYO@0?Pz-*hhx^L}8Ux83(6s_b@9V~R_ zX*y}kU?x8{=kUq}ILlu!q=Hk?j+t`t%kSbVUM=Cq;=929Dm4P?v#0N_Kfkx(^*DV9 z90pat@QZU6u!9zKLe>wg5N*M9FFnn;av=>%@Pm2%^^2&ed^Knk#|^G2&{6x6m@4`U zW$4cD-RVhWGW-biCM5;xeh{$2M-P9vp%^YgcZ-Ip5hY3=SA{xnU29&^(}QLhL&H^m zlT>XZ%9>6ygPoZVpRW73%yCcX2NgY*=ze~RBB_{!PQ4dPLs;*H!sOypS4rMc!Pgix z9a5vMiF>)WL`oMrOIT^@m$XnWHZcrhtsbZor2JBflVza*70F%vZ$$!D?{0+&Z6>f| z+zQTh+1z=|dH$DiFV{-RH@y2!%(R0BfeErKeR*2>W(so*37%UNH=n3Ts1hK=A9ud= z;C2c#aohXKrg_O#8<&gZ2*sM`zHNOZL{=biVQ?FDm8k8E71=?Biq0>vU3u(2>iEGe zs7cm+0x$S@I!nC#%!N#aB3egUi7V^YRH`-gUz=6*M>yNd$2Q~MJDN2i4F43!N_fkW~_spWWP%+7%Gv3`+VGljU zbfXEnZl?Qf;|kiwH5^+BX&JtE^mR(O1}sa8FksJ4yuv8=Q9==;G@6B5I!)3$Q0L9; z%CaeU{6`d}8#-T%A1?+!O=R=16uua7s=^R6-jNR{vBwbV) z&pV?r{xocQir@v^@r!Iy57qIORXy=kvDoFZs85g?SNUINvn;0F zDiKn9bH|i`jP;S8MuN;cSsg|Ej^vp8Vb^TV*HAsYy~EyvHM6@^r=FY1J~LI_dPa_D zjkxc$E@Asqp{uzom)b4-WE&57&SP}kh;f&mj2+Fnwnmm5GQ46)cF)-7=>|@{FEv7( zKM$Z_3`Y+YdcP~ofv?H*zGE`3*$J~{afInnk&qn2s?@nG_YT66odS{%8BpUMI_#be-F@LMWh%6-kO5T=Sk~V!`rCt*+;e#Fl{TS=zi7!bV zBT0L!rD7j)DspuAb3Z;%iIITb%!_*Er4a|7>IE!-bKD-rliP)tg zC=r1WR1VsV-8XL_sTzF(xl_&Y=Ta@*o2O~S>QmtDOg-Usba+NRrvUTVSmM-%QLd;WgrDABKxV? zwpysDl(IPNQoU;4|JU*v8SPqPR9?dFGaXrXrGuubWO$^vbNTEwQ{ITqWzmIwHjj#O z6(VDsri)VBoafm&g2!FN?iC5+>lzu(hEb>CrH^K*s)*%P-T6Q>VSu{EqiLM`9Boh5 zPHHG^RQ%$h4QB&S=c38wB6e~Pi<+!mR?h?0$TEc!{?=ZBb&T?fD>_Y=mQ$tq&YbGf z^08K^ToxFkeA4;^HMq)%SH)vbwV+pbYFv1f`6+{{xX?DyhGTySZxmuRJ06;RZ40D} z4Ok{q@3nrn=ZGlsH;L+z?%b8bo>xJqlbXpPu)Sq494#-~S}9ktlQkZeBRXB?b^4>K zL+zne96w*Z*h>rg%)&;)OYYChI_hEJsNS_=A1;0P6TJO>+uogC`X!59?li#ArxlC7 z{B$kQU-Tzdy5!_-l(h?oZc#k9iXaH#I{!68+meH|LQXIOOIX_>W@pcrrL!Bmy~vKZ zlAn#3yCiOrHX=W$>d@bT11;IH53}zjNh+$z<6$4vdjN)v&(br@8LN`|J5*g$QJE0_ zB#_&``XuXufoC=v#c{-qP3H=(7DEboV$arXOF{=RrB+*WE-_&;a9hi@YUKyGa<{1l zo!bfI*WE8Bz?3cw_E5~<;YcRzSKsbx5>w(*3^(sNwNhbJDjW9&E!}_jL5feq3ZBNY zwMH|p7iVh^neoBV$4oN0wbp(`_1wW{A-y}{2?fdqgxC7xl}r^vl|qKyqUJ3^>ts|(IoK3l)9p+ zv0mdX?X~s{Xn-@v){@#RwQtXiE88iS>KWi$PT~95pM^3Gg@`OIGj8~be@CGk(CVfJ zVQWN7YDuV=R0%Kt`VvNVC2iZo-A;O?^)Z80X7}<9!Y{33YN+}GqO}RV!$P_w*zbI< z-NP1(bvan*^Y-WC@*7qQrQNWMjf$;#%C@TQeb=&7;a;qILB?K1(+>(fxtq9VO|5<5 zB+ObE&YBns?C8!4*SW>c+Gu;l4=ZV2xbCS%Db{Hu?DwfjurQuO$5Idl4d)5}?=P-t zx1YFOiRSEaV$;>2yoVC~w6NuBrqE}~@Pun{hU!i(*_&*YY-xj*uZL5%%Zk#7PFpS6 zzZw4L|NlOcsB8lI>5A8AHMPEGvmCs|QE{3=+Zy>aYnZo^bRO7tCR;8kyf}L{rDW4E;OCTqmATsWN+-l6Tnv&TJ>OS z1!nu{&1%Xh+b4d6EEJz^@reID3cm*>`X&D-e`3S)i7W0VsDl#}NAdiJyDLg%Cd8G= zzZg1x-8pt6zi9aL19`Rb@kbm}^)c3am|db}p_u27>4G^eQl~o4b6?T-&tFm{@TZ<9 z5Rf~-Ep`6mijhfXkHzTLiJj8;?}Yp7^JkSWEfZ;eVvg3nz>8$kY^t{3l8-oy5BZt$ z_q_b;&$9aqwZ+1}!F!S0@$pI}J7F))ldsU$PN7gs1dY9&g8Glk=jS^U&hVVia3EVY zN6~t*FQaMmLZYD-QvoU(9)s#wqTsoM_$uqArh8tPMu`%CJ}FlZ3|f`SP}Da>=r=}6 zjbjudEfZNJ#52TQ*8>uDyWTc+h1t6rhy8KK7xBKPcKgnxMPA|43fD!C)7KEy#>`eM z@%Grr+-@~`n{({&PUvodq;8j^W(Xy)f}2BcqK{pZw_l z+-UpxwT1>Em&s}y?M^OXRE8rfYQc3K|DQDUZe zOtJ%Ml0_&2lt^n>VK*L$1wsWng5D0u6~4}GvkW85tKAwJNosl9Q?Rg@s>n(DkEMv} zk!#bXMCpf!ko!J>5+il=xZn-Viu2uN2!%HW5n};qOX&UAXX5|~Z)0>tv(fZti8HJM zdsc0?F$INFu~ZR>*MU&&5yDs5Mta2TEcS1c{sP|DSKH&F#y4m)O6|KQyn=ZHXrLd7 zf}Z2>U~V{XKaJ;emIP)K1Sr$+T$pq-nC^KQItYj3_1j?f{;>j4al1}2P_jE8Y+oYg z3MG!kl?AIyXnopUYCIZr`%#lo3(iNmCKYS~-Q8pI*6!HpfRg( z04oZo5Ywm0FE5Tr?~Lmu`ymP^FKo&tOLC5(HoHxvq6-LV3W^ut1dQ=-*OGFi%RHj zLVh_!S;zFF8%FG#1dai+3WT;TTmoK2n1Z5gAz8K5Xc!buH?Rqte}ZPJ*PK{d>e}y% z%=q{-{lC|ie;*Tv)J?}d9JWQCHohB)iSeM z-Am|MdnYB|Y!Ivp>HDATQ3Rz*rA94A29*h){H{e*aHKFcM9-X+ z65B2y;Cq~Rx4J3$|Mo>m=GW|UA3vyyPw4~gVvB)D3r-oX-Wk`Kwg}5_#vlTkROWTq zp8rzC@v~+d*K;tRX*F8E<-t4B({VMuX%b*q+$J#Jw!Q%3`Iq|bzKJ*IR|fN*w~M%t zT*v`|M)g}db}|<7)z;ekbGsnG;x*_?Cwe^%BvK6DI;~1K&UlHT9O`VJ!aV65AqtHP zWyGPsJzOZslE)v5D=uyK5b5?g=xGFf5zxAUXxxAoM7`Yr?Qo=63QtXzv8FqFfz5jH znM2rISN!A4TkDhc7`FC=*S>W6zVPR55eug!jV8AwaPHt=g66IC#jD@eT!QJeo$eYw z$Nt{b&>mV&({|QS`8kLHb70Y)6V;)p>7>`&4Dp*oYdJlfrmrq{hu5inGLWc^>FIyl zsVgRSb3Om+`bxF+(m)g|nTP43IXB8$IHdo!Y z3~0FOk%I2QA-QVsknX;C^!4>+9R_dGF}H>cp2-_VMk)rTmpoHM*OUJoHtImnD_q&9 z#UoTO->|u5Wo3nA-AZNV0R>{pb!bJEVRi3L_gz%Ca;QlrFQb=^ULc35$6&9~&N9h* zJ2nbmg2Or+!*&x&Zf~nfl~~NQA~HHeq5nR*pqok1;`K{THeON|@~HPsE6{bq*pt`> zVTzgMRw(W)yAQ~u>wP3%71bw_6>jZ6|VLmNssR3Thp@@U8XKu*H%|x2MWN=QK;Z5GU4IW zP_({|BI(f{+MX;$4edU0;@UajkfWyW<3@1#Hbpc2iMa=c(IjRe5?SZ*;V>d^OYG;t zkuT5?VrDX#GZW?+xcG07ih_1W{^mW)Lgy_he-{sQ?^kx zJm-Ac-uchTnx#;^_|81$;z6qvbxVy2P4^WP0ljedxrNKl71a{nN$80p!>}$Q&7}je zTJM3v)y|W1NpdTwc?)(IZPL~C_BWnq#b8}(@25@Y=WLagn84HsfO!P#$6C?4DX zwGN;sEohf+1@+TpTS#XlQn%S4Y+N0QTC-iPxA}3|kA*8B z(hwnAPg{$|?L$uyA$<_=X?7-1KKjpa>RBc~?h-EfX!tUhSXxyMKN19pV@pZD8Z3TV zz2^O4^g@K%6Wh(9brM>s`EH%UZKOGPd8?d`=w=K}XYYczDckA|57 zdw~jXX)ON2P}QB@FD}r?W(z~-JLVXJtUvDVNdgKgol+nrnQkQOxlWANBmuc@;G^AG!ZR z8RWZk92CKM(Td>GKS1tp+*b!NKib(+yFZ>Fa@(R;&FHOCl|@Dq|GPu@jvnEomaP@=N`5IYv&`opMI8SnqSHSOll#tku$hUt|&UNUha^6p_F?uE*yL zoWdUalWqgJxO2K$_QZpqzR3uKB+&7e*Kw16=K3Xu4nf5-lNy8mJE;0D1{V@(-esiP zqh$Yyp)jeQ1fi-QS&}z)O)|UVT@@a*>;Dbd{@8JU-;rxE%qhi&u0JCF-)HZm3jC%# zPt?ZzKmVXw<6ylyWD9_VF_Rs*{i6+dsBf7#}jUkuuX4P)lBvz5}}0qr6`SdC;?@<0Z;zq;D$VB=3!U z{7Mb_)Qf*I2`FgZ)Qm<$jN>@JyFfuWkVq0j`B=43s|EJjWaxi14a*>%PRFM0>MOTy zxD0mF;Z9W%JpMW2vEi+9xf9P-`OIu*+#xjOa@*tVvxjKb2p(3Ba`Y2j-X(OaC7#;S z3lPMm8_NmDv+7IjURaemJ}IegyS{Mx9+JHd?P?l6{SGJP2S(AN&UX}fHu~V}nyWL| z7iyVB9J~1heueW^^WBs4$pIv627Tq`-&!k4TUzl`@Erh?WJ8+c`TOap;L&CB$IRqq zD}W2^USDK~ss#?6j_5F2_2xj*YbNESS?LUl#q$7C5l9%~h*)(Hakl_PaR?O>WR?_V zNJ%2d590SXw&uEif!5$2SeZ?$mi>WQkhLZ0l%*q7wR{Aq53)sT0Qu~gSOVV!Vp|$E z0H$qxm zrFIgqr1W4w3VkkiyvC6wExv!qad)`_dd4N>J2Qr5wkH&)#V_d8-Z9;+UP(Z~(pWl$8&R69fQ%g)-wF8~4u5a`U_@}re9zg-~btOWCJ|DP$T1Tyoj|csYB*>DBi*)Q z#lvk61gGW?aZ=y!X-hFdKQmAT12~-fsxjy)4ClKOk$zm71dyYS0O;$ff`rqi!eX-+ zUVZnX%E9Sm6-4vJKC9bWTxgqunDuScjC}7B>SNUxuL}@sXl(Kye{im^Fp>7DXQ(0k z5;S;deE;!?o;!X%el&Y`$7q4(r*JwYkKHRggGJvs%673iBxCPxfbohXF(068P(P7| z$^Qy?784PZx(N-GzqEvc>CM{V#4o<9wBPC%3i&{f(Sp3mfx7T9`63?#KwQ>~4O!KQ zuu1R|r(3~Ao-i->E9r5@lUCxq)MHd>KT;9Rlc!k+?7jz4`FZJ0xk~ zst#R=HuG<(H(#MI6`@v^R;XKsr6afC2Pm{Eo3;scwIGea4DHN4H3z z(P*|phamca=e|n!PS#-YIvbG`iR#tPbp7!Qor)UZE^^(_q4&cz)ifc`b%@~Y>t(TC zeeR>3B9JG4c7p}hDmOX?i#%kvx4r#3}GD9n@O(Nzx2xpA78 zs?ggAze+@Qlx|q|d@}xX^PwuPgT{c;^F=siKVE$o=&g1?kIyU^Ie}(;XQuaaoG)il zyuZsAHt!^AKm12qTS$4_R+RY{-+@{r($vLd4GFh5v|9AdD3f`&i^YgE9da9n9fcx1 ztv)KOOMt|oFN)3i$Hu33ms(7kGxdGP(%w11TRz7>@XT><(sLnO5_js+pH~E-$JiO7 z+iqz(!>^%b&s%$BfhR!y3CKWqRbz<#OP_B^=a`Hx=30wW?3c#rzhpkj-5d zHRPKy>)r$OrC68<0~4vCFj*3XmNuCU^JU)tmH;-B-m4Pm9hW0K!|F6h-l8#=()8g&|59x^|5U>sQmV0%lFS`*oS4Tw&BIQlfnVM1WG#Nf{3Kev7gv4u}9e>%_ zuAA90w49$rkn@#T?o1QgZh_@Z(KKnORwc za&4EFOy>%Qq^P_v?E;NAARpq5nW#_7V1FxA`vS{7w+>$|idY@;B1mzv^dJ5D{_E)Z z9rwh(GvgT#_^>`PZ$;fuS2+bavZxLmJI5V_%HCLqyD_;89 z?p~Cp0)Gs8lyx(Dso>KM4#Thl?=L`9HgIWK@ol~IEUzrxi%M<^xY0#uzdrzM*hh(W zp9VZydHxPf*Apw}KCapqJ=w@4?)(H(hECJCqKKwb>Id^_+;+r=V32rtRjJLY7t?*_>XF>7A(9|ND*DIOwzk(m6p(aiF3@Z<~k0- zq}|#bjHng@(hp5ol2TJ*gq;_5$e9>ato+th-MnK(ksGK zoK=+^DbjsxQ_v%?Y($IGyw?6QzWu_@cfmY-Upthd(4tSU_%He2M84S^5KgWZ1R^>! z>O{DLgN|C^-i=!l8(H@1cv$rk)g7DBjXpMq{CG|Y^I0fFy+SII*OHGx0jei=0aAP% zRxCVz9DX4TNWk>X6f>O&lmCBuczz#`Vx87d>LT4PRiJ4EF*oFoFK8%eME4V9vSj19 zZCCrVANVC|jdAT>6j>@cTi{Z_^qzhP-IPx1#!{e6Z#R7iZR z0k=9A$D%7~qT9vfc4;eqUc!=i<{7 z4;M2uj^DqgTYFHFCj0m_N#H*XsmE*R^OGn3MM9|VN@a$!{%FSWf9U3id36@mq5Qg|WVR1ShG?(_%b`NvQF5_N_YQe7m2Es7_e7LE$s!+b>DiCe=A z^@MlEUYXMH#JxSSf_qDPj(Xx1qk2LDFnq>5E<5%3nfX7%*!j3%{Lh%$qlfl7CRW+5 zjYC|voZA+`0ErfdU7nK=>&eY?t`?Yyayg3q@W;!3*Evj-{}+foxyj*bLDb9(C<7t@ z9eqm2mJssSRVU^GPP0NnyF=nT@u)(m6Yd{S-0hK8867`(XXy>~_pB_xUZI^=byy(p zxy&_xQ*s;19&~1@BSHa0y@2>Vj>|fl&2RvUleyJDx>;mY>^wV$2EDKQ|tC?#Xdfp$ck=nz!b9CW+r`V{@~_r;a@d?Lmr;5un4-^6g3zOA;Sk;^5Et7&OC zYjO{GY6!~}PM_ERQ4a~~xEh+b`_m0wh&LVy(AQ6DAbqoi6m@{QIh|ytu#tgD@cE>Cq?@C!PpH96<$n1vWvIe?uz(Lw$qmW?(L`LDHb=&0A z1$9kez}3D%p2KeeN+JkGw}n38{K=#t0F4=~H5;FcRSojd2(Z7ezbCvdE~+z1MCX2Y z7aG2PwR*4#kkk|E^pU6O@?-CbDkrkh1A0xkkeP)FlsbyjhUBo8#|Q-Er{G@HMJDV-<@yU`HT4z~^M?5>O=ZItCytZ{j} zw(qUMTxfnG89>5&_XlKV&TCV=U^yQ>Zasd4i%nQ~>E?oZnyt9)3Bu|o^^*WGUy1g0SI=6_yrx_uG~Huk|CvMb+U{IvwS!~=;T9^KAZvl;aFpj+>N zAqx)(71w{z3CS@W6dp=m(4tfALi{7FIy=q&nT<^(Eg$GFGL6k=NjFYxy1}S39D&?@ zZ&iD7X=L*|)=;hQv)nm+_2LM_xmkp-6yi!+&+N}TI=K}cN!n4m ziIr5s^ z_a}+DZN$_1DpX`-WOPYbPAtGFXIk0Z(g?R^J2dy0-b+ig4t~0N`(dB1kV%tH(G|Jb zOf~b&U7~)^o`c<$nElnIyWWm|qHXQh1#CNmM;cFU-X-KNX*+%wFH!F&_zLQ&s)y|_ z&NjWz-6o}cex<(-^T`O1@AlvU$dMmQ>-ji}nOyRA!0^HLf^@>h`u~9C8Tu!=mWg7Y zoZBOrppNK%Cg0D49kJBDEln!Fl9G^p1sd-C0@u+C+=%m}LMwN;;DZi6 z!Z>9~TE_zM`qkkg-B{(gs3_?`J6P#FRT_G#-F?PQW%oPVjl5563^-KZT*nG7qK(}2 zmk4X$RD%qe+MNwZBq2tf_C+{8`df+_CSNLpT;;_5h`HIriui6sjdUEQ+xhfE84eea zu#jcR-^PFH;Ad)=Tfl#75^*6#`W_-LH5BsGv0QFT*=Wj64&aOCP-DV+OFs_`T2N6_ zV-NFX4x!`QXCWdTQFa&@bAT&nv293wkJMAGi{=E*LGn&*vH-|MH(P}QszJySfm&nC zZV#x&8nq9v_JWMZuS(>ZichYWcbrXJLt3zh?@_pjn3oREh)ycPDDJ=9J0#LFEh8dKGk?jkmC&wy1X^quI?Ka^EdG9xN zAGIRlac`Uno$KGjJ0HqlHv~p7Rhr!JL?9hlbjnm>N}eKjR$1UKW42faYQIyvvLUDA z_!Fdl%hw2m40{beXo5?Nw5uFn;E9*0uj)|{Gj!%owXhe_44O#|+ zlFXL}<+1W;$`4}xP_@A%`jL04&FY4E!&TUHs4Tr6bI#zXY|=nJ?fGM|;}_{o!3e7z zn1$y-Ob$a?h+lan;)Cm$`;!v!W79MN9TickU)S?_jA+QGdt4ux(Ce z=C=Qy{m!Bc_L>Ury~^X+tWb|S@ML~Ugqty>csG#SSwEs{Uu|c~e4(#FcQ=?a=xf@E zAq~00=qCAmcGi$ig>R+N%3z8G0%hT5^CHW5($luQWzz8ORkhh&H z`u6B#AVzc_#{cZJ&iRKqsyvAv#D+OST#klgVS*ZKbe9Z2XFAWi#tjgT<&M_1Z%6iI z3oVhvtX?~L=~Uh5$clR_Pmz5!t)B)zbvH=hK+%x%G8fvnj);o=R)s9x z_i{2dh87&uDU{Mg>Q&mKC4;N?tR~wx40BZbt-JlRWNG8JCk3PCj0cVR=W3ODxNGbN zIErdrxLIDG)c;+?fn^HUY`7R-(UwHHzfi2Xz+vA=ifAIsKCATF^48jOo}C!K-Cx74 zGh%5kc9nJf&MjLh#^(g66xfXyy0eVRT#n^1pHBC5iH+*`GlPumq#o$3F2X~BBkYR> z9daEioLeh+HM^H}wPQjb=G>psT3U6QP4}da<}iP7Yvl#UsO|HBz5OM*ytp45$z;~^ z1(SiAd8<_&1=!OoPtl7Yn%o!aus!(5q;(LB|Pz?EMj)ID7(mr2r@ zT++C5xOcyPuNH}S%cP5BKd4cXR4i3{7-X=he=NFl^~N>hEXaytzJcQ< z$&8F_l0CCmLRMvugzT-XjO>i;QIx$$R*|e^6S7x`jO>|gWhB1m{k)%n&+pIQKi}8) zzvtD%?H<>4Ugvq7$8ns)8O*d))JKEt(s>b#$B1i?>a!p_{5jUB)!Vr|^IFxQ?tOLE zqa5ujtq%TpK8x`;lvuu|*+VhK7V|@?Hx350R%JITN=kZLH8NE8g!&#;4KS@=!J&!X0mD9LUp}~DyQi4#I zls$u}si!pPS#PDPWo%8abk$Yt`jqum-6tPz7}nhI6r8GYkLga7P_KMj*WottJC;ap zUg2=}h1+2MZ-(Fb*MD^OP}Mp9Dt8vlG+t8>{x$CSJ(qFWwfqD@J53&eDT%#yN*tH^ zxUD6uzt{{F=zg&I_Hb0Y$x(9g>h!yxU85f~)cMTE9`|(pGQP+&^Wim4O~1aV`=^jB ztYs~p%gcIIYcYuw2YK|UQ+XO*L%uimmxm7T^)z;6A3t^HSbL!@NeXoN~W1`Yeg3E0;~CNqZyLrqWYzlaK6W z*!#12mZPU~luaf$4Q|Pwrl04UK*}LoTUn-X;s{i}5)MJ^|3jDWy&7)Hw~>)8AI}Z7 zC2$=D>@J*2&juKJ->ByS4;jj;yfSS16dUy9R4iVz&NxUn=5pBNPB!#e?ivgin?jz< z=tjnCp)ADz8BZ8jY0@6?d73&)}v*vOrNiw_)b2ugqpx?n{A8SQy@`)zT&{) zNL4^K^MN_b>*%n#$F;W<{rDlBtK$Qe`_58-;4gp9H$TW8q9+H+U7i>F@SvXo*Hp{z zc%)!b{*02oTqGa+g4?fP(Y?R<1p0HO;%*j(6`K^Fnq9?A_NhdCrCVq&D zoxWxM;55gah;DDxc0GfozAOSpMXSMlt%=$v9kpDK`5>5b z(Co0Li7TSO2A*`C1;nHBf5=->`}&oCUzHCx-W9*>m{8(t^W;~)eKu8|LA(38q8F!r zOBo0_x>vTv;G1^rRyfh^{977FUlDJ)v)evID078OLZgA*lAMs~rVqdb%B;;>Q3l`g zU;g_-#68b9e5hT=;jE*Nz5l?oiv2yS(UTM3_3U|BB=SpL3;S}M_78iGi4QiQYSq=j zTPHVQniSLya@!4>?EgN+Uq9fZO0b@FFUv`q?6oL>&R>B`&8<}SXsQ8WQ*0wz>@RQB zf%YI_mSTU0RPfOxczTROAy(nA02XzChC6ELBsi@IhcCHwJ@uSM|9 z;28T2Wuiubj@HZ)6QZez($}V;_!K%O?7Z4Zo!Ti@$QSeAA6-&ZECS=|z7rYx1x>Qw zKmys)v6u?A{PKo~#xZ7VoicWJ56W8@M8hai2K;M;lyV3jnk_psY!@o?f>40YZiv?e zih>Z;K#9~pQEG~$A};ID*4K|3{IZalrQi7D?IgyhOV9{kOb^`Tz!`bjSq*UKAxJ70 zL7Z^=08B9tm+KBKp~JwXl%tr8_Vf8a#bU-<65&DWT8vjV?0|$6l;P~`h#A|lu zYmqTD@V}eT(AkH8JCgUtwW)eI26UjYPyd^Z`)=RF-!*4pHf%ggmO;m@t%hyD{h}19 zo+hchlA))$Z9cGrG*<-`%Is#E{cU5giE}i}0ir@!{oGz(88Xt|nSnaO@!~bzN?x-x z-m)cJb6-*F+V19lyI%nMip`F}e3q}f(7K)Ru`|wlTW0hdrj-OU1CyBT!X}Xrwp#d- zP1N&xgeRM_1jr(#^lMeS9=#|AjlMemCOdNm0 z^?7RN0fc3kn>hM2;c6nOC}C6<>{+q1tJYs;txfd-hg#GF5T65uQ0a|bV*sFmWeGJqOA)NVuHs+4s5?^>s&DZ$=}o1AQoXgjz_0l z)_08I^7(l!GfhZf)QZQ=MezKDGAMeS>d#&n17QFGgFHH8*1`hZB(jc4CECV}@qtDD z5f~QXOwOM(q&-T;0M)PsUH-0aq+&W4-M(~e7FHbr1;B1AZ8(hgU`#IsK2h{b3};f8 zJG))GeL$XB0@ehbY|arvl)=@10mN#d>SnwIy(?}*keg9q&I-`7>`BO1(m3+kN&19b zx)MeI@XrkOkqc}|o?zNxj>GD8fmyK4Fx_>RE(2@ZfTV*OAo&Egw!IQQ)VyR+2RAq+ z{!0&tOfZK07Iu0(b}<0fe|Ef%b`b3e6=u5a9=O@Q!aH>Ge-(g3E>nI8o;ER?cc$J6 zd({GmjM^HkIP9Lvw=UWKz4?KcB)KS|TiX{NvOHm*{p-?3E!2hE%YDfW-ZEyi)ZqV# z__7+WwF9B=>u=Z_Th`Gk{it71i#$Hjt$s7`#d!`cF}2=^g<4WnYkcFL&Hh*8@Xve$ zAjv|pZ#O?QO!|2HLN_3ya)G#e%zMC*`VEE{J7E91`Ti$j!g9D%*W+5^sDZDGP*7f@ zDJBa?$B9HQw#ZyISUkIfcEcy^`eb?=jLT_ELrW>7fKMetJ+*{E>cZA=j?9MB=A3Y# zF^*&zNN_aLw<550lrzYLi7Hx1y2a6@L>dLhAE8T<1ZBIFHx@+n#hHhHDOf3bM6Rq&IY&Q1j83M>z0vIk@$@ zK9ccy9onJq3q~tEhe9FdCdQ380w%Lg9^B}jCgB=_;F(3W~5u9vsMkIB)cf{agcRA`|Y^kALCqIX|%GE1XX;hIPZ zPQ61DGe62{$Ub^t1uw?0i^g-F_Q@wD(LK5s=fpO!goBH!;`aPJl%8#o)UnczUaM^> zx&F(gB|mc=N7U2t)y8>}3mxV6GKgcGXs?a{#{uIdsU#d>Sz@c@Gw4EbA4LI$|1LB6 zqH+<{OdbclL|qasV9j1oU>qTExp;s>lnUF;#};HXq?Ezms+@6|pLzV^o1Uxa^EzDc z!Y{c*v}OCuOG{j3tDK6chj{UpsTCIK?vRvbYqbtunj>Lo-wA;Z?f z!qpL*^FXKSZuk0&191Cejt#q|I}lOujk8#oGt2Jo5hIZ9~(dC^@>tm5;30=QoKEjA?MMGuUgLjG2Cn8cBT+?FA7Ed4|fje-|Bdfc@#NY82l5xzOSEeV1$~ zeXfc@mjnBjyjiwVz*;|-pJD+D5hhLF^S!->c9OOA7b%jm#y*ItW_v*~@nYLvV}%+q@$UoTHc8K7|h z9$My2+^>Wd-6(Ik@ecp>ey1xM(4N#CG%gtV^EAjUa3=RjwWd=GMyDFQ6x@^U`SdHb8BX83~}(*mOa=x`aQH@2d5yThrBKW0yy#bNV?yDdDTFR=6B z=*HBQJ*}Wj=V`t(S7vEWeCb2m1>Diaou{56eN-nL0#AjzOiVz#1;+S&l(EoG} zNDTDB&gYQ1EgwX+A0z2=@K(+fJ6J@<+|BX%E)r38^KkPVUHxDf20Er(z}Grx6)?JZQxF;Fokh=wpzoW?pc4(2L4# zTt?q!ijXXQF4FVx53P0$Y2 z?a|M@y_3%)nVfSwh62+h45KV8QfVcGhx_HyU2jHOLqTzQ#)hz?>F@c5f-L+(9+@a@ z^?ca2>A5Z;`&PXeoufJEiAONXPz5E>VWwyR<+J^FN4BT8JE>*DcrGQ=V9Pf@C+fhC zZcM(6vSgxw-axvo7|1Ud`sM4*&5<}_b<~@Ct>oD%Sl~a^5O)gTTh=>2OG$yUouBLf_u+pUtZ1Qw* z;fP2A-mlUmJ#`az%|r+9`NWl!VHaU=nbYtus+x)O%~+5mHR$u~uP9w#;bjPynsQN9 zC26QXupS&VW43KZ@mt`g;|Z_2SxJK&%k1CR4F zi7H5q+V3k|pmfBOyxO?q((F*2!p>?owEIN3idk2@`j0<-`O@IUNV>gNkuL~?OXvr` z98J@>BQp;P<$`mcPr9;xkPuYWayscs(C}4d0)x}2;*un-cx>EcZ|^$f2o4oHX0^o!Bv~}A%3sZPAl+{ z%t*Jmo6H42_4lNb-M{lx9dfj|pDl1tDH-K6CXdRu)&cKvCTP37Q*cC7`=S+8%Wh zW%AzpL+{BT`_BA89z&`j|K_KL>gsPSC1sg~@?;xAq34Q3`ypX&3*L?C{2loatHP44 zsi_)kUD>yVK3x+f9Qt(8))`R|4PWj&k2XbRlM^orxc9XqP3y85?&!}0g&|9OQE9uL zls67}Qlq(A6W3aItNHF$DO2XaU*Y3rBU(6r*@UM% z$4yvxR^uAP?=;?Eq@z?@vdm8aPge_To>aR#Qtbh zQ}YcAHBPlTaG52#2D5D_+!j9~p>Et)qQTuNP&>!AbZMI0fz2n}adZ6-pO6yw>NEd! z4$}RE#i!ZxP3NXe8(v#^TFIYXrH(OWAT+#m{VMBIoK>#Dxr>h{-7e8>C02#XQJCd% z6UXMz=vX|BOeH&>Jueil>+MX%gK=1noMWF@X!hMVI*#2hZb>$^?0*}77 z3Mgz#IEXJ1U;3ecxEbq0D8OS|3deLwoV?=IaH?HmHyCK~DNy0DS?(6$hK`{xiWiW3 zIfIOM*QaRwzf`;{pAI4Unv)>zIc@!L*Y@a_ZF<1=7(!Mr`YLQm;xK4V`}h}I;5z3t zc$V_*5ZU(4H_zxz-_RBb4hmfFd>VtySFHAB>Abx_^9h_L>%|k}0&%ED?fM|i4o9bO zLV2KS6sCYCVbPvbmFPf^JNqtz#u2~iZ*FzFZd%AkpEiDzr9rIW229l{@<;O-Eo~Vg zRb8rFcfHj^G4DU?tsvvwUDfV&&?-lnqsJSMy*yYYJD6m578`9oSe}ADaZGNF_ogj3 zw_&fSp|-ue6t~x}mD!i}H0H3O^R3Oky>ut-=(>N-(S95dfHp@o8%}&b74nVG;`Y}@ zADad3Szed!#N!vFB=+w%$u`Jz`tlW(JCH1ZN#HR$^|F3rr*R}RU&@Vmn)h8{7K&YL z;UkB(&)ZP)uf|ja-7ijxxDc|-L{l;J_Y)T?gOlvg)@Pts-yY)@kWQba`UERiZ8}m7?q7?FY#{JdV(1+Kn zd(D+n1hfQVs*O5@Y7&ZyIwj5c-hY**#?4fUPTl2lcvXh|Q%w4ee1o*7YVeeCF$UMU zfcP-GYvk|0aK`6|j>3I+_%t;in6%aVCjKyX5G)gQElz6C{TtFyK8s+9k1kmQ!8Pi` znVCSXl)o~t}&?aTjeA)V!ubHL;6@JH@tQG)QxteEV za1DOC3^w9oO>$k2t>~?mEGVOC5fbNZV7AYV4*VCwGi8J{$0@cG6A;4ln4Erbb00BB z3S|VPZ!X*Z%Xt)MM&H$2R4vCGyy=5UXuxvqZOFSjdG(h^=>EmXe2xgvC-xfz>W}2| zI*$MioPYdY9g|Sk+cf_5Ims8#GD1Ds|KkBFE1#`h^T)85A~(3%qrTlh*{ZuOX8`vt ze|B8wQz*{vo!ay_Cnbl2_d_)lomNKPPt*VKFW8l{I!DBmAP#7m>t~zL%c5^sN9fm^ z{(gyR;U<4LA?}!UBbSFJN?4VrP`*t3x$8n}{humLJn|Cg&DwMx zlD|3L_5i6GCsh9dd#u=t=cYA@ld`^qL?z@T1q- z?;lDtV-a(7N@`s`(d@vyTUrnFLkoTMSM3Gq&{QZl0lsZ^R9hq?x%9%#{u2Orn*I5p z)EtFGncos5>P|R6v+lu<*SP%z(cz_yEubqV2Tq^r*5_LIfG-6&kI**t&3#dpTs+Wu z2Ywj}@!Y1}NkTxs>`@AiIl&;AV8D|1Y!b~_hO7QaMB&B zR{1ETp+m#>gYyKnfJN5;G&IrUO=KeiF6)zIs;t7V*=7zyYOnGu{vY&qa~P`z96bi= zqN%+O?Edg1s-651ai0uC5c=2BKv;nP;l=HeS5j+wo!DYq_&&t^^VR?UctRLov62ZA zFPsAJd2hhS%y#awE;{u!6=BB-6}d*7`saRv)&(64jaeJFopAP(Ey{!laZyQ&SOQB4 zf#(V*Wq}DP@V^$bbIe2k!{y<&eGbq|tw(?}?-8kb_TOQu`#)fvJ}X<>IUjp_$T& zPl{An-x$vHd+qgMw#3?=nQ5HWiI!V-0e&hpHz8>U8@jR(j zj!X8SLu~H9rd2!(4!S|q4x{A&K7yzu+4YAX2EpOV#34J+x2O?V?Us>TCv~(;`F}Ym ziQEL9hOry)M5fEgrHLUCKh0O}l{&v@sQ7%sX1vkQ&v@yKO< zYLAj5May$83!t2FzW36{zrHdW3h*_A!5GjJfe($?!4xitugIMFJZ50P zkc|sXjJG$dy*v5I7y5}f?UwMpt92U=Y=h4N5I)pcFaCdP?yvt!@Pl_AS>DO7dVD#8 zMb#8ax4%t!I{nE@y5bo)A+pj)+R82ET7fvX_6FSu+C)2Z5CL z=Es{XKC(m>&=ye8E`Ma0#m_lh(A(KNYvQK%mEX?>Q)8I_c$^bAJdbo6Fc_3lIvYm( zZ>Xur>A!zSw6PV%td=WnVXu;><`?-}HScPgyX^5>&E@m~K{5ON(x7#F;3^UgmXam* zL)Qhg&V%K7#RMz<1wWQ8myf^iv=^voIAjAg?mM7p|xs0T!WQ$I;{gl&Y&jY<#c%>@!<+}$rSuCzO-qd8jd+MWYqWE)y3 zHo5KldpusDIkJzQLQ{4(|g&LIL@@_wIBmy8 z?6-dm_wQC*WOn|K1`?Hi=9@G)0%T54x!b;e59i=OLC{Z?w2MePFv9l*`{P-M569lu z95Y_+a==`PTm;YM#((-9RPX?@adN;%Tm}7qh zK7{ydSnmsaA?s-CP+xp#-tmsCg&xlAp2y{!Q>VQqTwyPE9E{>M18jYFfqW z(bZy*jy%|y{zNDU^w)A{s3LUX*oUEIh$sXyBL$H80)^Jp7Q_@xwtJg%Kq_tP%aSub zEGz<}EXS3R=)PL86WiNb$oaK@Qh%L`WI7qTz0fbQ2KO;t5Y>-WI0T1aXaV1MP?MZN zk>`pBw0=+<5gV%AjTsm)I*!*=_Mm?rG}vC>U+4S+`nD=I#*<(SxdA<;K{c0TYHTm?Iq7!X5XC-&8^fn;gYT!UBI7 z(J{j`9v@-l5g18k2OPPUTtqPfid7Sc<+rEsx_?6ZwGIfw>AYV5EktA$5y4@mo75o- z1eGlat4D*{wkp05d_bk3h5F@P^(H1YhE8xQSgXH)GI6s=zs|D-G7uQ^cL2)5rtdFd zT!Kv;x2dc=O)ov+Q6u%C_yCMZ?1 z3`pbgD*a(i^TxZb&?k!AaJZdmnGh7a@>(sYuTkDaQKYj4^f7vNpNJ4M_n_1}82*to zK%IBpn!hAgQUp##2#k!g|3UJ>K&0J_-yfQ--gHT1{s{Q+uw5LV*E%zr8Hjy&G8bY| z1t-I4ok8&Y!AqtBdZk~x<3*m)c*SqZL}tYOsqzEu)pME^oO$u_ZFZ^>1Q=&D8A}fF`gR&)wlteI$5Se7EAA$T z)nkk>3(cPrRlv(Zmadx2O`eZ&G`$1fi?Pr3_w9#mM$6diaD`?g?snbF*Zd5;l)FZM z#JGFnH>L;`Ips4c7j6&L`Nc&VPNDX6wmfGxG=}*5no#WcS?7%sO$T|=Lf)M;xnREL z%I)dq7d^8ylP3P0{BaKui(KyhIO`0Hm^CBc;NJZ3E*U*f{t=5;$dCc}oMw*Bi>ukh zZqWO0d;7>O$GkJn4sw!tr}A4Tqnatpq|TyXIq|G{_v7h;uNA<((jW^YR%-avjW)i+rJUP}!iUpX>$MlKT#m#Xv4 z5LXC;#Og$y7X`Y4^9IZkK{^Bh?Oc!Je-;4tTAH-no|gl}6itWgQT3UogXelGc<)iZ zx}MBe)0klq)hJ<9pf=I2NZkc#);#D%L1y#0Po;Qp11Vg0aCIQ+?p>-I@G70VO3Ch@?W?UtO zkDQr&lv9mgyRvbrd)=F@1-pqwX~x|J!s?sCVFuxQ-;Sprz=Tvv3sFyqd@C!X(6(Jp zX|~195*MZF8LSetxajtb&sh?nY!Wyga4=XuXvl`K0A?vi8ogq+6xkPF#V4vojezCN=i^lE;C-rRHqGw)c% zEN+c5Rb{g~uC1M7ltnH`-~k`o)Kv$l`xIN!h~9UlOsC(nYe6)*u8&D0Rv# zrf3U=)5iY09c3pJ=r32^IK8iD4)BUskhT(`MK?7GsnHP(UN7Q~roX-x&%y z)u&$s{5bx4j#dWoBOWhhmsjdNDK`M2h-T0Gee5Qg1bpb_Vmg5Y1WY}THTI9S7LW{% z;+Q^3tUfdIfs51AW>etlnSVZrxG5%QNEPS~OP^_0qH0^9>pms^#KIh*aHZ3$>_{l_ z#j$I*Yk4yzeCNriLM1o+`{zy;vQZh3>M`pPEuSuJo+7EX7`3r36S?N(sD6K!4H%Bi3=HG6;U9R>PY}$HKiWy&mXL zJ`CVV#P>L+C+rCFm^rW!MdYU}TOezOe4^&A(ZMc+cu%l7*?4uzL7)xpgLt+Nu?s{@ zazcn$fi&6h+g;%AqXDrnfV64Ly%rqvek~7O)2G=+OhWSAAy~A?mO;V~%*PGmm}j6_ zS^-9gzSp0}W>kaE5t~j)H>iDB2YTYTXw7_?Y^pxv`5^Ozz{n_!zVOiwh)Ce4e}jm{(Ql?HjJ-|BC_TJ z&LgC|;%>lUbG>5UcZc!k;^!F{jG#1^?;Q^4U7q*7uc-g-7@tkV*LdsaRh{%}v7U7# zVh&G%gxB}}Hw+XqfW!!khL{!W% zy{Bu33JyKF2a&x%PH60A?_2*G*l~btnalJ2iLfI+*RjMppIbZehYh4I0EB@7G&(7g zq$;-alOG%c%WF^JNT{xEcN)txFcpS~Cvuj9u@#snF>+={9F6v;9reX3P6~kd*Mu2( zaaxs*%({bN_&F*NTC=~v0Z(c?^s$YCtGnnfiMSg>OBiY6xloe_3MPr}%&s*gA$3r5 ztalng`mP{Ve@xU86?Eof2N-WkM!nFYM_bW@v@ZQOql0Z*Hd3|pVN;33;Ul?Tr~QQM z0_A=ex)G_N9J*|d47{U*#(|8xLw3e_w&Vc+K((#y-?;5GpgW{a8ANwF?!Rz z%rtigPbApz0AMQxP|_gf5eq>u9g-kl`XRn<4Q9BY8tL?Gn6*y~fffn2XC z1FS(_-J3lCI#SCM3JD8|Csz+hUKWL7^o%V_l}Nqe^|^JwvVVXP}(UcK2iMvi1IFVGqx1C$@0SF{*GNG0%V&DaVKGhkjb z{e1c?YLDg9Gm>c)4v62-jdd`yLPq=EA?4ue@h(>x{tdJu`U^f=izYC(ZI)viZGNf) z*k|}uX?R%lADdl)j?tU!Y(HM8W?|6n=D>YU9x}Q4sfO z2ExdiM5Fl4diIZh4q!Yn_mAn%7<>!+_C1KBELYvJEuFxkY3#x~G~IM=ND3(-UyGPy!LS+%Zu{BRM-MCCVelQn_*1+r0y(6HLtO#l zDy{p`{!pC_OlsNI)O-9zoyOt202h1uXXtD00Ar~rbRVfLW|4VrG41hNyGL&^IzYr6RtMt^xL)S) zO2EbyOTNy2H|3Q}ZDUtYS{TCCtxsIhbP;)X{9`bPg9>wU?`aF?=-qF-G2I2l%TKe3 z0(xJ|4flq7)G;39nlnpva2d@;AdX$#0-g<1`7;Wfk`a>n6qnYZqQTHbI%Ifak%9HT zlXNMe5dqq;^4G$ijG)qpY|=!4V?{iMH}f-`eGCYUx*5~GTvQ#et$o*nBbZZ?Yb~1n z2(kya)<;5cmbPEuOZ4BNLgB}MK-Lk*7%nS{Wq>VYcZJIfJsktfmG==8_R>m=Og?v( z6h!|%s%%6oV%fR?1hPOsiNjoIQaXB@l%_qo^YO|q7xUz6;Zt@ zvq`5pW^Ct6;bM=nof9rxHbMG;6%!4Q!XB{^UmgDf?gFeEJeJihOB-RPlMD;@Tf7A2 z>35OAe!wgDYphs7Dp0pK2w5{iqU9%uXUWR`97}K7Q9O?5zNhbic1AC3Is1)Ym&bMb z7x6h+I$I;(=iLFwZR2|KEaep^g>XuMrayXMq0eFEE`eLzQa%HDjek>MTSXTg!v&T8 zvx+;E8!{qEI9Dl*I$SQVPLiC9Bq<{$rQ$J}K;P3+O-u`lWAQVOf5lx)tU9A7v%t+C zXVj?2zbL36bC}6fBjn1qx7XUL=PB8=Yt0gqwbEwH>XIlXKPETjWOSSV9Dz3`4et)F zA8ke}r6O_3cHE~tu@P_M5znJ9uymVy z2;?H;hcAd|TK^;V9jw>1`{Cih_L-zC+A0R%>jMz_Y{nZAc_KELwiNh%P|3m_xj?US@GaF}wWOGATy! zc)XCufJLq%$=@Kb_wudvuMkoR^Bt|38f2E(J7>^JLgU|d)jycd!0Rf$e4na=v3(i?LLgNe- zFK}B1zo6M;d610}(VA=3e6*D~)!&JQ&KkmH^fZ=}$0bo0`;f73UdD9r2~(2x(-vwG zZNpu0O^@e!cLI6bmn0R(`v#r`3EyM3>OGtCgb6h;GvzQ-G#hjKKWAvb5!at^`$@E= zmuKB;+aGB08T+Yn-&cg@{+0WRl#LeV!W-!6YD*im#+kw9FFUx4LM;NjzeEJu2)OBx zc2nY}%ip{IoVt-beQ_?7*$OW-K$#~#Zj?XTke~aUg|RK?&$hR@BhRnm(@tLvVUX0s z7m4Tj!fPb-3*UAtv5bdIXgKnYQELQSIsIJg)3{u(F z1_Q%&r-ALmMqbBQf0ii{e;5GxtzL6~n#pQ${1uS*|2W zNm$sB%rEdnBzBNt8LHUy&9oGL6!|`Y?GMDXR{SK!HIH9?p(&j+|G^02OIVz(ynNlI z+sjE>l8A2rTGsmGR8C)uc&?r)N)^Ie7S8R=IlUL+o{&yKmVBKwCL(obzfjMP#Js~z zuBm{KeEJdo@Wm&E>CIt|rY3yBP3$!O8NOu_4*GHenMx7 z=2-|A@irh4$-BdKR}k%c5wFDbZmV~Fn0kVi5eur1R9KT)zTP;vu12IPC;42B@8LFj zpzz4I4Mk^qHfWJ^Ram)jT9~!8C4S5C2Z2QYmeQ_lKx6IiU<*S7r)r3ku1$?4xGB4S z(gQ+RO8(R3t1wY2&WkdPCY(7Rqowgmfr0!TYrnW8fuc{2WG|DhQ}!RP*3){S<%< zREKV%PF1td+)r8OYd4=x+${_6$mEH?9k(7AV;0j`k9+@C56cyi=t+!w9t8ODu0H&0 zFAhKAdWxHielh+oXgH2gx3v@_6%2K`3W#m}rWI$LrVqo;@-XC2eqw#Mc-1UlRCjoL zZ>yX0Uf;!;0@4m~Jh{7_m`-CoQIc-O?(x=}4tvuI&&BKnA~PZ#beWRczK%6^YE7nN zn@@ebAlKr+GvuTz$lRYJL_3s>o>ZRvX%w^#v8tmq$CabXR{$e;TE8hnZ-k5XzK^+O zeD*DZi!}L0XE=46 zctg%D@n^QgEua{3-QZTc`|MhiVhR^;(nMR+EWZLnsa)+v;fmOyd!JQ$sbb!-_8WsI}BE=1t=XP=&1DRJxNfj0-v2C}xA*vr_Y0??f4 zXE?f{g_h^=s=dgtkDGD;5ehUi?LQKX^~Dng_9Y<)%y4>rc8{>br?XfTe0yBm8*v(M z=#QeDO{egT^!d(}?Y*NyaGM}-D~-o?061Jg1lEh}b;!A&GHG1kXI%j|DX5AqdwsQokjcYt=FSCGo$flsL&H6oGyV&EH z6KT7~mFil7_hE{Q(sK0kz0JJ}wA%#|Dp(WmxJRrB6M4*>p-#@u@Y#iOS~RUd~nhclM--k3;8k zOYxyk1v+O(!t1x#U8b3K4=I;)Lbgc$tt1hcCG8C00_VlwxAGAinDL3dr)+5&f`QI# z`IhS1R42r`;yw~syQ+B-SlXVRs9~vFGZz0-Sszim@0>N$&|>ry;fajj9w&Gyj*o&t z=XZHYSAqaF3+N%2rA#zFy{}pgVa<@+qbltP349|A0d7lWvOPU&BTm5{z$-e z>QOnn+G^*R`+|GwlV*qUt(2W7Jqu%nJI>?wf5R(jm*94v)oC#`>)GXNF|WlVvDP)8D@AG;pFj3EOn3x^#KHkn zr<@DCQI=o@^pCnooowHV`@{i2gx#oc>^}c`w|!WG#KJYtZV|Q``apN%E!b4NdBYMo zYSDiT*~T11%GquZ1*u()Z*30o>q`glOtV zwTap9-__9p9o%G+MK2>DSAXu{?hv-R`hY|6rb$6Y2I7ceYHBJpp+|mvP{R#W^z^o_ z9$n9!K1$tio<#aQhxA1MVOhe~lzQ;$V9tjoRCCX;X;rzUrBQ=^6Ezim z`Qan**IxJ?<%y@DgE^Of40kcT(ugI`F{Vjws`&0(a?hQ?wa)CGm3Qme0%d3?-;U2y zg#H{fx+s0J--^jw1240mT-*296T`O=zX@G!;wJ0u6C@;vKnZ&FvG@6+H$eF01a%VC z4Yz^8K@Pp@T_`-pN*|~VvR?n5`%3dIVr|^v2ACQgT7h!qz(ID_^aK_NE|flq=hNy~ z#r00BlXrQA`aCq|UsgGR3L*u1g3#o%1ppXPsm;E)g-mH3hPt;zPYXJMh&Qzr(RHx= zf-?8I9vN0UKo~SBFKrT~w=N1!dY4&0VTx32bv*tWTHVi{OZhR;0PQxgx)en$u%Y^- zlw}MN1utnu;ib&?mP6ezbK|$V+3P0wrcg~cVO2r;JyT`#-|9x*;9D)AbaXh^*-q4M z&w*+&Pvt=VJT%p-fNgoLV)i8rdl2#KrM>l>jg{3MIs=Q6RG>HowwGB?3S~kcv@{JH z13o7MDBH)x^^jg=71WPQ9?-1(39fM&+q=yG4*do%7hT=1xbM(QmkK1&RMh7(Un%{g z2HZA>`OatL0s+{djnr6_H1myuWGk@_Eak|(pxbn5bNvJGbAYq$0P0JyGUI%*?}!&p!cf=>tjh;RPd+?@ExqLOEyXdnRr-{u%ghf=P!ZOgG`8>WQ%V4dp^PELO<_d8+*kQiAUKpTa3A z1)8T$Ne^Jaz~W}1>R&Ci2#~zxTIsaX-ns$|0v=-_S?n8~j`y~TnT&L&S9ZtDin)R+ zL4r>sMmdRniLvu}tay_Ag45kAzRj=DlbQ;GwhSNEr`_V(gOyLcY$XtxIWtV~zwi)6+9>6;5-lR-|?ulU_ zWwYO`Mb>gI%J{{uyR(4Ah{*=q`WeKfe^bkr56-zYD)n~zk4%-JZ%9Uza)T|5re?TL zpZLCMvDXs@hHKsyaSR8W+q+fA04gMAe#9DhfN%AY)no%j@{D|Gk}|`qWH9-~6Q;Q_ zVj9M~Ry$%EtD8Z{9~2MADXFSvR#;I#_QG>+WrnqPAx=OKY~{C23Q_$E!%X0enr6ls z&*sXPZ_cpzjAB-r^D)cyAC33_Vt9lzWD;7nrt-F)0OtnBkZw_oUy?pAuOYfty|u`d z0*+7t`+BZ(MB{rD3c3tpwpo=hvB8Cy*SrQ;p8phM&#&n=lNXX`DO_C+@xQOB`vVZU zoCxgbW?cW(<;Cq?nljhO%V$NF1N|Kbg+db&BKmcY$5iFCD@bGa&uF_`dOUmQ@6;ep z47*_l60;f(y>yDV(aFX4gk6?~=+WjC?soF7etvZUa2tR1xcbhQx}Y-_pxJAm=EWt!5C*c34;|=DY_xHB*Usg!xdo zXQ}^tWOJ@Vt2FF=ODaI2Q0w+%43C}UZegmYF&PS5!pM(#xj6+n6ZAhBDuE{RYWHWE zSXI=nC0DYmUXRAd23N-f!>{@C(zBfhQwa$Pxm*-iYXODVcok*gt47k@wP=~K2(WT9 z@i|&tTVojIK)BGyC<4LNEv&7ra_sYx6=%%#&V+0|iN!@cnj1MStfp}SE3@A;R7QiI zElDg~+Ym|at7cB4y#4A`D zLT<)Z|Iwnvf1%p*`#Etq{<0~iKb9IINgE+DB;@9V-nCO?-7D^Jnc@H2*L(_nO^(IU z})D~>sK5MW=%mRTcCS$RL3%6MMd1F69rPud3IM1 z7FJfCh3(v9>5MB&FR+T>Y@IWhS@G}wm}nFUm#?4v=nLC$xa@6sz0;XF!+#gGPc?zT z0e{_NQ1jotdpFAW*$$)2#c{&K%=+i&7enVuW<2JPH|%|WAjIPv|EHPDWZ{Fd=AQy| zqMH+X7M(T3Vj!0JmEDJYDez69|312@>*?Zf8spmMFg@7DgzGZk!04kf8!; zF8P%}v)QT7F^iDq3cg($Jw=-P|FJs$*GVnF;C(1m#BSW(`F{ET{1r?S{5Tig>{q1W z!&u2LT6|C41f~aYX8y`kPf`fd#ATq~4-=s}wu~WyHu;wzQ&=z4gRppUU>G@lkb7i zax(Q!+r5(&40E3b!L7hQ;DUz!b-?yLZ_?9Dcu6NR-X5>h!tiCix?!abSgf)?V6 zkH>faf(L4)j*s_(&g%B-GT&;8T($Vt4~e-uct_AJ=tKV%{P1x5uE7`|h?>wWrWaDG z&BP|-U2Gyy%76P7X$5U&b(Nju`CDlDcwnZ#xUa2J^13+Nxay?|U!PDy(s*GMG9M3I zIxrTH$&@sv(Dl=Sk+lt86DeHA0W^+< zo9~WAJ(v)6%jW!FBsz-u?RKte-Ik9z~aSkhw5T zrA>V_V(CATSxmrRV25^dPO{Vf;D_3ui4zA5WRjlULJKfZxAZcd90`hW0bT z`WeJ|twN6`#Tgg_e-S`#z98lWY)tS<9)KQo#5SyiUx8u=hUj6ER1IrDh89KtLw&hc3%^D6tDO2DFos#6Ja7&1^;tz~}DXQ}Pvk!$wb!q7~ zOx$Dr)0~Ni-IFSosyGu7UtS43g}d4+DmP_KP3M3|+#*8~N4XjCSHtzMQNB=MgNrm~ z)#1rhh|ogVr<$S_r)6-2-yz1DWV{Xwxvw7Zhr$oRxoInLHO~QI%V2DUV0Z`OkY82^ zfyu5~j+Z504>R;VS49sdouPChO;G?V`R)z0tGcX!WEq7RAd)dpF14~-jM^ug33qaV!CB$NDP->)r0AU%4JY}-GC-Pw51<^ zQ)-Kdq-Wk(Uku=e#7 zv5JFTm1C&qU*}4~EuhI2bpu^TA4GYstm;}?T1?B}*-{HdmaBmJxe!knHWhEz#aax5 zU@s|}53d~Ur5*)eV~nZL%r?X&<<88^q;ud8KXpe9tmo%?koxmoG#RJCnP4CqEQ96(-+s^(i5Jy`2_H1Sfu1s-VDN*jzTWf4 zzzkrhn-bD}fGe_IPo$x<0Rx*qr~iTSR7{uuWTkiZ{CC6(RU&Mz_-O6u@Yw?yZFF%f?YbT@%rR0Mq|x)Ljwowkr1=fwhsVSMR=kf3 zbVDKe`#W#=AIgU=`NW;l;Q=l4J*DdB_!5EB^*-m8)e{`jmp}a<8si{BV{}`q(KtnW zRK5nOiJM)#)`=@Cr~L$m*9UO3i9U5q4#>f}(9PSU~Kuy*805I=Y_}+wV%+TJw1M9?H_s<)H--9|-me{b4V( zJMp!}%RsovTpTgIaD}jdNk_M=i%dI(#?kovjgt_Na#}RNA0J+lub7^S4!rk}r~@5= z6VEf7AP(RjATtS;Z&|#5fnV;~F{x`ePpsHoWpC3;gtiSft^O5FfJ3Hm3^5^K8;^{fzt_7NiLf24nx&Nss0Gnv*aN!W|fg4=pXV%PIY}|6DZs)KIf7 zuL03(LOyV`R0}3)%wtoXJhc+^Ct;?8)uF7m`m>9(iq+o$YR${dy)FN21rpaHRDsh4 zfIsY{X=}62#`b7d0yX2`6kYA*LGW2UA)BG*F_X{#fh%xlAu@?7tFU2Q0}>mHq2qlv z2qivdh;gz{HQ=-`i!ev9HydY(LculnY;fwIALg#Q+MB^mUtp%UoV{CAACQOvRFoK^c7beZxlh@bueCv#?tT7qFB zQ3J#dM5tl?t@0;GRPyult07%_xlAeSxCkQ<7L!xNybclSRno`PX2dcOs&dYobUOf6 z4oh9~;?sprhs^%V1Lf!7w|Bu^%nKL<2f#NoA=v|6$_t%wFj<>}?8E_904Oz$PFGP4nq5~LWDs?Nb2cX|^))~0 z9*p3yv1ovC*mb*~u9e*mbR{6yJgIQ(Z9w5*j9Efk-+Y4%gu{rh({g>Gp54w(g}9to z&e&FOpq;Ds0=Ks(s}8B;Tt=wFVX6IE@y8;+IO`>YxbL7!8j|HG6S~%~w@7Ib8;dvy zC)HhV9kLJ1d-DcSyRNY6@3q6IE-6+REVK>ZWz22)E&I?_%acJ0Q>`A)t!3vi9Madj z?;cV3)g}$pKKVVXb{&SjzO$PF41W)7%_M|CTg8~?#B(VjWlQX6ucwVZ@q~i+JZ=4S z3!8gq_|f$AQ!@QdBD?U6tYnApoifyZfT);(T=z&x|CBk7KR|PP80Q6-f+$z7p zs-yy2xT!^?69B?7g+zX9u#{ZdOg#3~Fhh%`p4G2B0#BN_naB<-jHh%ProP*+R02Zr z|FHL-@m%-sANQFko3ay8nOPw#DHkOolUaI<~qK0 zo@ey?-@I;IH?Dgg^&Ow@XCCk4c)gyl+`B@|TN5Y8J21tW2Z&xdG|jz#_G>dw$0!vLS@Xo=S0 z6cO@dhQoav4qwX&h(%#^&>lE7I(~`Nex1dB?MWzJGF}#n)~Bd34BbPHqt5zHQBlz$ zM($flAY$^!o%G0AAzHN}7jWVIi7sC3O`)l%@OczzZx0gKbsl^1?TOJ^~-+`9dsBml?AP44W6|P(*}t`al-IaE#%B%@4X-;Tt!41 zW81@jd`=#MZ!G#hd}CS!-&m-sTr=vA&mn}VYyNA)Khv^27~lc-*>2G2>>VAk`IOd# zw;3hQJbC2a2f@c+?5#MOoOv3)L{f!;O~>_Q(X-DVbo@N{7|IEa>_2hKzEDG{{x8j0 z2+@L^DPJo5<8z+Lzc(RleLgG72ssB(P{3U^bM#Z|S)WP|TFAS1u!hZq!Nnr$j6KjN zZB4r}RP*724vEQQ_?HIuYfsY3xq%4;hH$uiSfm^b*O-AR*5!FtD+@6S$ea}y!nvpy2S`s_3A^gM%s+8G8heiq_s4)d}4Z4Vau zRi-}pieiQ~S5rV;5Vqv~VRC+tA~dKI%XDK_a3#Z;lW6BItC6niS(1TV4H6)mQi$}K ztz*ZzqRw0$(AF7+z1q%b>ioySaX@quQ#d>H{wQ$Si(kUrT77gM-#`h;D21=m;>kXHQk zziynH-lZ>jhGS(B%!S!QT`q0 zMu#IWyFAcmW74V>vM;whU~(-W#Yn%UOzNLrsM}Q{Q~{aiw=i5$qWc6=EDwKt;r(yt z{Bu1;YQb$_uRZgub61Xn86l6^C>k<2`v$i;7<8GsSQ-7%Zv8CjRvKBvmU7F29oQjHkKL&()1YE!7L}nJAINHcq#yW`)naJ&|-`KZadQ|jWcvg}vv_oBf z*NAkcH8VHn-#rN>&df)+W@E=o7}yWOb6fywkQ5Imx;;U>0z#C1%-7 zin#q@jjO$rnkO|GpKaITMQrt=tf$95{?oQAHoN5KO`Q%ao2~dO$}njhQ`e9mW*tVe z-hO=7tA|WFti#(d^wYTV%#V$S%(7(9rS|O9-98fTQ8Nm=ktolMD@&#!u@y5wq}wgY zq1|Qgj~E?tPZSlQ=DC1LECt2KNF`!A#P&x`A){4mS-QDW~aZ z-YgzQ;0}Zff|LdB2PFo!2UuzJ{29Ts1eddOc zBnT?khIO5GSjj%1kB4*u`D$0dosBwp!)#q&Cw~P^c|tr=TY0Rzw|v`+Xn&o- z|D5QXg^k#{COaep>5>;4Ma^86UlFXgezCJ(jf@;N#DM6J1lfbU;0WIVrbmUDVy`Es zgdOe16-#H6Na*&>`3sFEVq-=!M-=Zkbfo#D1(ti#`p3ykg>-UmELwFQ26t2?EvKy!Z0Of z*Rulr3{cy1fp>jLw%C$BHvfcoruBi4#SdN3=Yogb0vd+tOJ6)^tdtR#x z_}V>tqfU&X&AFv>M{<0he^Hh0c4_@}(|JDx{fa0b$BC&$8|Hog%<@4#E8m$>Gy6D< zfT5UxG=imD`?gEw9q~7^UhIwywGMbSM%&eY7Nch7n*KUT)gao&N4-8rlM+ZiI$iVT zh{HJR+i?8%UFQoY-(rhjCkaDxpKkz(C~t-FaBOS>>Wd_xJZ%EQG$588>=uQ+fYv;OLvMo1r{LV7}wF;UMf*IJQ_V*-0hAu{C zqiBgL{g>1;$!yj)wue3T57)q~)Sv(>E-=U)g7m`~@ClU8aQ1(L&IUp?a-B{xlwl}G z`@C}>B$wCJd*gXeiUnMC0H+LGY*ts3HY3_#p{@z+j;q~dUN*lvV{QWgHyBgg;oC`^ zD%0BgIv&H!tGAOfq3Sf~n@^k|Znwo5KzdZDBX;z)NL*du%Bo{c))CkKz^IZ)#RQLb z;AX(py^h2FiHb!O)-E%r=Jgj>3+HPtPK3)QiF6QMD{d;^96a0}iik=#k{BU$o$R_4 zA}d(ssH1DpPyXr!@8-(X#F&7`*!lZE_)b2L$qbtjzc)|t{8ke?lCkC_bLJj{0lemcz5qSnmVTB1#F zP@zPO6dLWoyo)0(Ql19USXF$Wapjt>?XQ)G+bBMy9h5p~MSfIj@+Up4RQTa@qO~9_ zlY0D_?;?|yip4x+ObcU{RZV-~d~SZdL_7bt5~+9G&{#?7e$ryAgrv_kCubMb9PgyX zb1M@%BWB9{UjF2A14X;coyNX#Lz&}w+pC?fHI3(P6l1kvU*ic%T$xhQW6~-pb9ywv zbd7BE)5+z@w~2>|L>Jxb)9H>tbm@)<$F1oODzWT`+kxzoh z^&|p%=z=CEV4|ZHQv1NH$Xju*HQbg}Z0YSKJl4n2%$K!o%d#zMi$gS55DlIBewfR& z!7&p1euS{0!b)1+=I-GY1n~s(4$TQ-!kHk4ez+d!dQ}hI-HI;A5QSI6@z-eYbsS8O z3fMwqS=v_6Xrfslkt~*JJ==4|@mRjzD3CLp{CLJSN`B6ol#Z@TujH=MX2r{*_0CYV z`O(?bATkdAmc@UtmfW`hE7~z0IcM-?W19f2U!<~S-CB9x#cXo8(r%CI$sFyHCDuwm zcVtqVZ(@Q%k?~m34_uv`M0}yC;3=HA!7c(`lTTMX9bh!`s36Trt`kS+yGePvz~jf9 zGul6F2A8Mnrc<7c%M=F7&$00i7a&>JuWLVF0zLoH@O`d_Qq*eyFb>B%fNY1$W$x$7y6H@3k_F46L|-=Ho=>J_)0_0?9H;P^Z#F zR|-h;8(4@O0AWMS`rXcx!_K#|{1K8A9%2_B6UkfKZ>_-yWL;zthNw=^derNPCXo?0 zVDWA~;c$>6U~M|NOn~Sjz-;C{7Vl)j?MV_6=T*=Z4I1E(cGqRJ`3DZp{$qkEh1+mU z)J{@_T0^jgjq00ng@cEh=r&zHX3M7-b#%Y)nvxZhEVf_KDaZOA8{{gz*K}{U2)80W zE{Bz*@Fsp&sj#8tAi|AnCHIoTJ??njvs;oRFcmF#A@N3Ee6hc=U;Hdy=c;W7`iFAW zQMn65Ic3C&n5g;FM?H@*EVdz{R-DtU?A@1yr6d84QVgD}NswR{M)p29BV(ubo}UQ=XX7J zx^LPfn__i$n2IHNFf3pTSBR}X@-P1JCyhodD@U;J{Dl_WFNldYa6a5fe*B1u$CsX1 ziF|54=s|SnlXFZoEp_)j8=LY=e=RZvO`%BU?02U7Fk4S-bW|IiRLplUiIH$c{C*(2 z^Dv1XD`%dQj8;Wi^o|-;i128Ywe-}B9K4N^s8>jb12|+B@*|J-=wgK~V2dU`N;Ec| z$*zI6nCui2xo;QA3ph2|GG0b(PFkWeRBt6Q-*)Eoxxn_Ax(;ulG^xOyS4t6KS4HcH z@RF@&a#;AF+io)MM-Z1Ql_4{FfQa2#H)oGe&51f>)J0*PJKsw>TJ7q3UE8r1554*D zPV%dmdDM*L`&=z+miy7(35jWo-Tvb_1eZZQdY=5&T9=zRZpM4^xIL*0%INQV!#jSY zy3bq?^Gd)9Rrf7g%KMamA&VUo$9tpK=31PRj3exp6Adw{l3(0M!7MaTs1L3@U@FpY zoGQ%Fjp@pZcmBr1%{^Hk;X4>G{PBelj;-Y$nts1g7_Re{4Br%4cgE{Rp{WYnbj{iD z(w8Wx96ipgS~g|*S3+Y;voOU-l{L!pSrzN!t@ZD4@mbkS(stw5p+6#wTC9hN;im=P zIa)toqGG)r`<~tD^=LRj8;6lWgXG*t^l2Z}n7jNKJkKbeB)dj^>eqhEzWNFDu2JDn z*F2b=C^eDrw(A~huGg0tBY;U2Vv*S9!!?nU45Z3oF9kGN&u!XOZN>n2Dkkd*PY}SB zB$TCn2T&iZT%-0#bSkR0%cND)V5WbIk1bj*o%<&1n|m26J3mwdzt)d%@8~@i=o_t= zn=@3R%>3N0Hd;})hvBu{5;@X;XJj1TX5dqu!(!hmc{r=pdfC1s|G`Qy+Vll2`>hpM z=ar-Fs5CC;+L9QVqdq2A7d^cePDQG)PWJEd&ODn)jG9*OoI*nM$2BaszTDH|e_p&q z?&78K)bNVb>Q4Tusk-%`>D$=4`zqN5OIgZo~FTb=8yE+-%TwsZsR2NZ}?Pvz~M{>P)3Chw~PDd(AOd3 zUs<{$vowTs38w<)wWBOLGPG=<{JXyhfh-Gv@vpX+!)&E=f~!F z6;P*{gJt#k1L4EWCu;m zv@7j;MlaNNMwgf>5WHbD%PrfGj8iLhvC3vzMq`SNEGr3EXQNuHN;iw+n%noqj>t7w zKH(3a>Q`pp$nLB1eU7$^h?2Z{TD_uQlP5oNNiPhpZJ+_B;FI_r?yG(@4Q&CZ$9F}KH z6OS~b^$5!GKTU}d7%wqO5tLZ+ZY?^8tmB=itCmKfGz+}h@{Ylut zsn_d%^2E^&PvRIj91mE;Q?F`D5lB$CC2xi@Ev)tXTp{GxbzDPw9y=(~YPw15MUX$e z+IyV_#dA=LDmr%UjtrNCynUYKBYXio_Pm-$EulA~NtZV?X_>f@+nmJhh3`OSPSh1n z@>@CgRX%sVwo@hwBt(63o!zR!0Zn;}O554&La5lQ$waj&(qef4{?Ttxxw8C8`|t+N3l-X#D)CJYMW@}U@G&d`r4xHY4z|?9 z4Vubo-eEnR{e;4>liB`CukDroQjOWrE4E>oV;nJeoa||oYh@iS=;57rwXVP_S2?s| zT@9EUrM;~^-LNRB&!{2bGRR-yxxeg?9v4@1bG+8QZG|HW0nSAY-fQh?+G4sqne!YU z<0NnN(pcKzaDZLLoeU+BD;WZ9fx9`6*?cKe(2PlyEA(|0pnB|9Z)MOe)7*2!onj@8 zSPNWl6X}kB`geglp?Ue@;Y>NcEFzL6{7y^`u3d?RY25{BT|HLkugEh_3+sBLTrqWML#(TL6x`D_4t^WZRbjgy z<4PS;Q#Lxua2z4W-^ou*)zPM}`K^&7W^Izz*Jl~jO9C(nT6jDEjn)T~_uZ4p70h?k zT8d8H+151F(W3pBcr~Ga#a?^{|21Sq=klEBq~%ClJw^DgAMoSO%>Cb zlgpR!Y{Rm~tWS(a_BoDb0&2j`)0Q9&eW7#JX{u%3pWTU0qKl7&zuvF z%&dyfK z1k7$sPsfQwt(!333iEJBn6*9cSvO`6q`B)fS)=FEh551X5Os> z)>Ba|`jmSa_UI=}_K6#SKi_6~{t~5*gpgbVTg%LGnl8?jCtCEXWI>=3t=pG(Dd&ka ztmO^<%H4s<=myMgZg0NcBGXi?aOk$JRaI;!%}m`dpEzQ2NR6D`GAo@8r4=BB3!wAg z2<}okxq9<#`_^FBTlIl?WcOP+}d_l|K%rG@g(vZx*Es(+C#n zXX!Zoe5@I2!WLreq!RXHw!%8MQM%Sq4vM?&S6A<2&uA8#@(rVL-WtWYAhbF4^vc!M zN~scQ)uE)ssE^1Qm)dDc1@)Nmb{eZw;W?l+y(?d|I54OF>7>`sl@xE|**J>Vrx7lZ z$=iEqnFuP4(CjD?PcwH*hI`0PfcNZUGO3MjHLDiBmG4m;Z(L})8oj}H8OJl!KykB-pdmCL_Fu} z!4+U&={8gH`rX^SM@F^TKNv!d1-C zZDIZNi@_n)%eOr=A)Pl;+%L_cVLf58W+&sBlq(y9W7dNAw(~j__idLIY&b&y29|JB z1wFkt9Q{1b{}1ht9EBL@syp70YP0%iz6%Kk zAdD|f-=Wh66(cbyoUBW1@7WS8UWsEms#}u0U08;#GMnls{l9jNW7I7v zq1H5duS<jPA?%yQ4mKDX9b(;C&-GedUt0uHx+hZmq4*Pk2!gPXjBg9CXr0S5;9 zAI$c?dr!dSHiNFMNR~IROo)&R1%mKZz9pDS(k~(Zz2T+bzZN98B}nz1pa-7c7b1RV zNAyi_g<&)g2(xCx1>U<3Kw(#_7i4^(INuoP|U#{kmdm50LJ-6 zWo0~fsnpK!x@V$lU|ASpCT;rU1<;~mJY$-7P4?rw<9vNLEu{)5t?O1^_~e6g;qBDR zGnu4v3rGqnGK^c2QWnn8NfCde)+O#hvO}~*wm$X84B1kVBC4|MfKWF3K&dL%W~WS_ zBjxPa_$3k}6SOY=Z@K%>caI_+Zh@83%s-}520;MM!+N^;+x`0MSPcLmZ)|_1RONSg zz)zKa7lIvdA=lw|!st(>jo=dA=eeKMcISy}*^kalq76imhA`v7?o8lpQE}FE{eBzm z8JoMQv!mu&ID8Np0-S&NO{JQik&%&^xf)iK`3k8?&(xtb8c7H}OAuy%@E*p<(JGI> z2P!rkYswA9&~U*zNWz&=C7nY`8K-vRs@g3XmS0kk2` z>b-V@Vr^1^{q8^*iv>BY)Ouhb`k~Y*QNc3slVbc9&upz=FFN>AsdQ0dcx-%nT3cEY zOgPmZ!x*x`%nj@G_PZ(53*uNg9omYEi@!|$_|aR0!*rC`gwtLJjHH;@viyAIbq#M@ znJe&x7i!B>G~b*)XJ*`LATAiuCQps`o2UXL))x)15`Y~{Syh#U?D=6MfcD0Bg&5AA zQi_PezfSiG5HoZvjp$GRaW9JmKULXYM&=nPJeb)s zGW;aLXv^G(;isii(q`VgzBQ0zf;x6(C2#PQ`y9BQx4JUbIq4teJwwAV;xv(FRNIFU zr*|Ae6n3kYs@T9v?Y*Ve^M0fS&EPzr8!IvfJq zgvu@tXV*%e(?7$2+6|;~+aRJTC@6^dW)%ONxVl{zuA>6jVIGlP!TrM+F(^l%=X~}* z(n?Fs_j!4idp~45iVMWA0JQyWj@oGl7aS+SNw8UsrM^H|dhW-pHlI0kaCzN;o6qs+ zE%iS?XTCK+H}zDT}50S-K0ACXa4ydilubTY`x{r5C2Akdu`&Ox;psx z2SVnFz&IWf@`1OWE5h3A`Sa%xW3Ld~Ku1EY(;FP3|K>piNkH(Y+us%!q<&vC1{Uz> zS3e~D^4IAz4Zi&luI!Et=P9hO&?b4XM{eGO$Y5p|)VKKklYjozPW4rA<-24caQ5M8 z+dPX+!ukv#mCcLEv|Yq*yYCH(rpwV9lJ6EAedwFcBDH<(*xOHlopiUwVAGwWJpErC zC!BvW2%vo5a?Wk^K6Q%7AgRX^sF9}EEj)#>qA=Q3O-7QBaUOVN9k=@yZuCq#S-bp4n=J&w)UV#=ZYzxf}#`IR?E`4ZdKG zCnZ}38z2akffz6boHme7IzZ(C7WQ$NQlJ@vNvNpzhb-h$gDjF8Kc?XV!JAAt{lK7) zUjnDi!oos#BAN!M^D0623RjHzMrnG@5N$@cmwNbRLUx@EKR|EX@r^eExc-jzx@w8~21lFN`|5H}QtP>k zR|QFFms1l1$^dMgkouzL<+-?n(Rr|I+(S>18%zX9I!tgym+cahkWnA^He``RhOC5G zFOvA6dllWe-kvbJh@I40U02q2`q1XX$-zmwO+%kBAGEs%081x`X68oRLw%XbUccWT z(vQ;(t9iG(2ZsY5s`TYRE8vGraDmN;&QUm=@^sHip!W*W$}35@3zvPg0QG2%k#7Nb z0dfc%o}m;5br@F+*I`!q3Ob)xO0*@qJTShi$+x8-c{>YX8jCWk5)XH7B+)mwhJXmf z-z$4c* zf^-=1@W6hRG(>S3;cyW?Y+C`{z(3@9{a6Q2`QG*JB5+J8^14uyUMoxXwjX*_WK6=> zCNfuF_dPT$Ov_>R9l|cdzQKVn<7tv&asnN+*xd+e&+aO485ws~lSw4T_>n?kf|#^H zwk>o+4iZ0&@M1iP+L8-#J@Z6#`x_s!Idn~zK&tmz29X6Rq|4(Ku`HEhYJhH~x+qKi zTzA~EuY?7Go$vmzaK~ssM6B+@xpd*2 z=`M{o;;C}snrA({`|OD=d-vT*J6SrYHSF;`sJHT}@kp0MBZMlJ>>* z5J!_AOK_Q%o9ZQJyeu@M*8m?LjbuGb@IDRyHWcg~IXkLJjpXZoN`V4-~e z+s0{WdL*K|*gCu=gkY)vWYD84c&EbXRf<|bjQPzKkOrh{ipjHI+5o^zf?Jn0(JGKy z;J%09uk(#{NyBxoRm;>|-C-Ml9nHwp>vDKOo8wn3CV!=-1sQ$m)g0O@vpGS;MxKai zCKgjK9w`Iv_$7j4R_~aPt8@aGU*`!7q(~kd2X%b@JLDRxf?gTU#E%)ALMuYu z6cX{*wM_cIn*B}lyDk6?|M}AVz_xEg4Gj(TZ;?amFKEkgR`&X}Q%j(2=lT#IK@X8( zO7x3hAQOo_ffd(2nD;KlY+RuqFK(;Q6|&e*S_Qhwh2nSH#SY9{s`@da4AM4Rl=-o? z8`euap|U{;o8_9^S9n{JdK9@$W2$TfD^*2&S%614qdJmPlaM^+?u@FqCBDRUN_HHJ zp6#tCIYqifp$7E8yy}R1}CI?g5x${Sz8Qo-^HA^39wZk-QwZ%)9n0K1dlL)hoF0@tG?~&H)0TQ3LIaLduiXHwK9B2a45L{RQpLRAj{kaa{@U@g@vG!s zPsnz5O{a^a_H1;k@XOYV+y0B30#peq^VY1L8Mfx@^QBg6$zN`wu%f|&%%hIdrz50O z=hVi}#3MjkHiq`QV^KybPy+vS`$HWFfGdsMBL!8}(pMSer+f(%0w{;fH(@jIjlWaVs4vhjCebZ^g; z%ffD8OW)YAbXoZUVR#p#%ur)qaW+uX1&O~r)<1q#OPUENE&!nYPnqoo&L1eyK>h7A zQOW&?xH5Cit_h}Joca;bz5hzn_be#H4N$2uXZb(kxtQIB%X87pM;gk1 z!SEB~6D#T)fxmz0O#WZ;?Cts@cG#Nk|Cd>fHV7emFSS=Oe*53A{?+NghQ0P{m2i6J zzeFlUsQi8H8aRG`$ z@Q}NMG29SJ0=idi41JOQ*I%0@KH?0qTgoivsCvQ^kqDW{cH?`J-~flGy|}B*v}BPx z^0`<$gwF}niK6nu2ltED5e5~|dLDo`=;jQ=%Q4{n6V@o%pXxX4QNiJwJrFGs(qcMt z`3D}BXtV>8S#Btng;AxwD4p|x1fRd6DShYbWqVqWi>S%eo5#zNTXt2yNAI%@efX~>q zE(Jt&?w_DM9R_ux+iedg>kslLl3F}&6VNZ3P6D1oYF=J2lhJ)|D|wJcPp0jB(ZOlV zdi9p~X|$Cg%gZe(=oR>0BwX)NYv!0p+C6$odyJsKvHAM=_$Xihddk;0gW%sIsMHC5 zGt{>YcA(_O8WU(1v*Kug@}L-U%5I130+ov@>@K*8e)I3QUYG98n>tOS) z2_}#UiT$lq8;@XOM~Wb*eo!z({T2KmICj}$y766DSktWpyp4?fj_ZQdjyFa0Kiv9s z2p;R!~HDJ(W2C}RnS zaHZ@aitqFqv@i~U$A@V;(zSH)ARLssRr0(qzdA_cK0#%lh=%ijgUlX2>l$ziN-&GV z^#z^ex%Uq@JDU&y{29a~LcN|S+>05UfoAqEg+e+i%R%-l%*~56DVb+H#Cj;)0yD?X z#OukV8ES>}ADlp`n~c?0dxLzX8}cg%3MbRGj_4a$XWg=qC*~2lwMpDS83XQ}u+>9E zJk{)}oF$4dl-+I_sZW13yWih9&nnxLbVZ0}w54(7vH*ix0qyLe6zGC_ylt{!->2qV zBl0AMy_2Qz$}i&}0QQt}Gm}{5pmv4o=f%RgWLmw)cq$_2IOyslL;^X-tQB52??Byv zSmP-GyZ=gB@v{S5CNnYa12V(lfD#w(ZJa|gOBl8X%4B9sn{Kl zwg+K}cnP~Z7BCrchro^35$d?`T_%FnBfLs^7U_s2B7tsq4UQ*0!tzKL_wE*$J8FS zmdgd*3V1Q{xUrar*!F*s<1nf+Lb7&VKOcL|9y4FsfV8@YJH$?->#+x=b#12ccc$u9 zG=XoA(7P@1Z(Fw9yGSN7Pa0>=vt*CghC9Kqu4>iMWw%$TcC27mnOPwR}b1Vu3iYMZEaluE=Nnein3}cdTf?S6%kL2)cM|}?W-P>UGjJ8=|#BQv9<<9olRh&HozY zQDv*IITLz^C~7H-{GLyeUTvtk>1lG`Q;dYw%>9tzqqLle6hUqNj66vMhPXpWcc~v% z@)z?EEFPKZHv)@NW&v@MF192?ty-^%tEKB$AHO&pMH-(Ika7yE9rUIg!j$@p-o4Ao zm6-loLnp=B_E+YJz1BxpjxWe^?6Pn~HL+_)(NJH#u7_Q2jmm~X5l!y-+Kc(To&P+S z_91UBdqjkDjTKexck}D7%FwD4cO&Y%PTF3@2Dw#_F3`H@g<0B5;o*GWQwictzQHs0 zyGVD7zD%L0*F2IM7l>uM$?kGtKy!h?wHy`2+nw|$3!jkB673#WydBWfrODt@}QQKb@{6DBf zRBzQa$$I0c((tvN*O%d{Onm$T9(ZrFV?0Kus0uun&0IF4)?1%_sEIq?O0h?`3!){` zkL7X?;^M`=iE{&=c(0Q|SLpHmrwDdQHxYt{f9+(q3AyI#b>V^mvbxL&M%jrC;8Zkd zwNR$c(FU@8q41!&+udX5HyUuU?%CXd_|6?!s>J(^)quml4>I^(v-(orCWvs$LtE751cbN#>;0w6d6x?B zJHPCvr1vQjMnha~i_Zft=a+o?{#4J1%{;P%RwO^eBeY?|JJ0@a!8IvY)@~w&8<){j z*w|ugqtR_ieA=|KV*CxlEqK+95IlUXzg8~E;@Vf?qOF;ff*)g-@OE7^_OpS@H-rzB zdZh3u-pCL6oy;@Wr9=?nD&yj2rzTQ+VDZ$j;dp!vaZeFID@Q{q``rkIThL)FX1tset!dVsJf>v} z3OkLEZ*OQsFhp9>=2?a#WO(7Buw@W-yQiLQT{nVDq(FH7`Dfu`Qobx;9m@O!;@kcL z;y?UCL5LNv$D6A?!tOcr8S}lr<10hiIv)u;!Z*a1h~(KxYJQitQ-=TLGEfo*6t-G+9il;|g z&$f0ABHwm7;}W+Lnl?{`Bm^BGVX{gS>qj1#$e6}cTIY!;KHM#p4Zl~=7CyfgKHn&ir?JHn$V**dEqTeNyGZ3ZtYQ<_fHyZlnny)I(lAX&_3#yPEvI-e8q2i&K%aqUW%@7V6_^z(IKK_tH9J(pRz>4-Ajz|Fxj+12Cs-&Ehr2~Tx32ftFQ43)=j?M zO`(mMorA8UOw!sS8I0yl;wxESD?cSgoqTyuB&oab!RUiN>B{;q$RpPDtUR18K@P7T zd2|m)%NQ-DOMPx*RdjDh2(`nL+7fh;Roa|qQL52~uH;MVv%X=OysZ-X82h|H36G=l zxyg^nvN;Kyly4%43^vIZ5m6Z(PLWN07PS>AaV|Wi(aKS^Wlu+$$59(N{j>2VI%%IY zx)W5`&k|_V`}L>XisE-2Cux)z)*ij1LxTF$+eh_Nz$)8p5c#U6Uew`e)q>EH;0EaEt-^? zvG?9l^~piCHoh}g%*Z`dGRG8Qbj~;AbM6Q*zIR*RaV=3*jRJWUi_?5BBQ9>sBr}Nt zxemJhwL5O?7F;+IQ-xyOr-4xK#Zi1NE4!ml%6Zb)ecfg$D4Fk`XbH;K#mRKsP`e^n z(x2C!^J5>qOF;Ys(W+)LGS57CFVPa>n-eWD;<84K9u{SH)_*jMG_vwa>&n3rK#4EsBO4!t&&?;)X-}8TdqRkic-7u z0xz^1trbO^DzGrfYAzj1e(=D1RWBf}IfB)Nnvc>&CgO57Ew_Len^rCM2qE*tKPt_iQ@1=OSVY=9 z#4vvSSAIssa7)+Y`&Vc(Zj1#@@rs?*Kp;UMiy9F5`xq?wk+Nl*;k)~Y@T&}?cT9n& z&q0`TDIgH}$%I9R-%*JL^qaaLo*wZ1|M2?%A720eEqHya_npezJG5Q(NQH%k@>uK3 zv~T=4VNU9@vjo4weK(ELL@1X$|0?E-Ovsq(h$(UN= zQk;ZkC9j`wZ3le^dz^{4WJybs%#D%zNgL zUYV&;&Jp%Xs@4-R(h@a=EcD&15pJd&M$55daZ~*=YfICmI%&LhJszWj+!>#uln;#N z>NbnAGY=w>?97$R@OqO{(y2b^bNLF>1tv?iJ@ys*{p}m_h!L1w=AY34r@McAmNO3= z*ANNm5VvUqaeFYh59fPfYajsukU(X;KX{Gri~Q3*iG)!Y?)U6fz`)wUFu4v8Al?0CUa{koI0x-`O2v_*7+T5?a1{5WP-uUZegylWN`!G$Z0KhU4o! zUWGoV{7dN;@ZsNo;smHAk8mf*Y`hB*qujdR`aZ^^mt`MpEn4*}sa!(DI|A|v;vFZP z*u)Ulde?6XaXb(Lm|Z#-8N@xwtW)6~he&gU(0g~`f_dov(VT$0GW`EQulF#GD+H;G zU=oQ6Y{C#@>Cq+V{s>qhxh>}Qbo~7Lc zX~9uctww}G0AMvYQ!I9csp-CuRY+2H7ZKRe?8uXZ}Kd+$XYizc1|vGhN0+VfN0VS zPJ$GDgXyCDo(gv!VK*D&;8QUJKUIIN#h_zx@FKG%#tTk-mz9Wae}`S+4WhF}OI3C0 zZ*5Oo#Bev3+yt+%XNPpz6N}#Gq0rBkK$O8dr`}zPj@rL+ZbcUq}uN6lA9imlb-RNDc*? zSQten8{fJu{U#)IBlzP}$vd5a592~9iJ5NQVx*TD3;34yEyN~CO3K0ir{4dhy7IvA zO`_WLU%Y&8V?Pn2MTn5yNYG26b@5&j!wo}cLiUg(oNC0OC?n$hTavTPGDXB<_L>|E z{esXa^5v${`t-$!MXBV2V?DJc+a4v4-rnAmIYYVhCB>MX=#+%5C2kw+b4bXjxZ#H9 z=(b*8)4KfiLsxSsLKt+M#x}x@VUV4jy%YCcpyTS|cjs;BbN@)`YL3X~R2rD*c&D1g zFruo#7+9&2l%vUu;x+pZ(u1YIV( z#dk6Rs_(bwUGZ+KJ}%C$?0HNtJ^w@EZW5LeJK?7O#{yb36Q#5W^`bDlc`-~Eo|tde z&f4*nLurwFaf7&NwKk?>j~_9}4iy)3q|;aYh%-_WZHjD4dun56s$4l4cOszFaChM= z=Fi_nA^5W95G~~7W=yq9K&F^OZE+4rf=4fT7vmd_%kHFC+d#*%;W9{*9GV&+1lFNv4#Zrybrwk&+lHqRGXel0I5+85U54HF z*FT&1L=)vhm)FL#d=jM}$&QEXO)4rX`H?ED^A!RWy*t#;E&D*DmFgOwt$CLp?yuR^ z$2`5IFue^H)t9h6xzp^GpVhZh(vAK`AN7B1?hz7%a~f}HwF0+^}OB3 zd(!=)mG817SJTqX?QZX-RmSc3(5nTT`}T_}EoU$Eo2Dx$DyfYWrbS6mMc@4Qh=g;( z6#2Om)(o)62WaL>A_ZkLeHC3CnzaXGPzs&ehiT^i2K8jLF+4 zKf!#kFPiUKaW!A2cV?-%8 z&t4Nn$3ZD6(=zzqpZNr_U-J*X7ev=+A;rwCk9Rj_u3S?&VXzw_ZDeHrcJmfP=U-M; zy>y*#6_onFteS@ST$&DHAWJ{&l+o9>+8NL5?8qK$m068VzY-(UovUY)5mkPxIn2f7 zV7jL;ck>GzJ-@MWe&COXw(f&Mc0FSdtKYhgP1PqSNk~vr_me_@L3cg?20ay$20KI2l);0o1zu98MLcf)yDGwU;Vfnl;4`5)^PRWR zuNrXY=2yG$G$x~qp7|JLYfvW~Pa;@lDrbxcWU` zewK-!bnv;p_&sp+1P$pOZJF z{qHeoMd1;fQN3{b$WpMd!D$fT zpm^o%!Ffx-#WA$@$0pRHAoQ}pWvU!9E>g=xpyH>NBZe>>kmWG5eJ<(_(OZ zzXBnKqLL+WVbc;8d6_+8JLVIVPX8h<5jQ~9AqLuO?ek#Ks)Px@=-155Oo%)x-Q^km zWCW90(3W71zgX-^LZl(B9kR#05t7~P?dCXr9Il4e08>gqZ5xz78qr#snoI#?B7*44 zfoGmA0SSWl!w=@om>C(j@ojBw(F1^shu%$W+K27HI zH%JxMKxSSn$cz)bBVffdVm7}7Fl>k^m@qB&O?Y*!0u~(?v!6-)Kg-&n^nHpA$60uW z1TqdBVKGvQJtCL;FDArVo;~Qr&x*b(N0bn4*HdVX=!IqNhF!>+y0Kno!_J*QfeS7FK0e+5u2ro_{%-pE=g48N zut6V~%=hhx@j-Eid$S{iw(gIo)u`!~#vYSo^ICsaQHt!=ah)UL<#BcYvRI6ax@=y85}S Ib4q9e069iQApigX literal 0 HcmV?d00001 diff --git a/Documentation~/index.md b/Documentation~/index.md index d0f86e9..f574d93 100644 --- a/Documentation~/index.md +++ b/Documentation~/index.md @@ -31,15 +31,16 @@ This version of Render Streaming is compatible with the following versions of th - **Unity 2020.3** - **Unity 2021.3** -- **Unity 2022.2** +- **Unity 2022.3** +- **Unity 2023.1** ### Platform -- **Windows** +- **Windows** (x64 only) - **Linux** -- **macOS** +- **macOS** (**Intel** and **Apple Slicon**) - **iOS** -- **Android** (**ARMv7** is not supported) +- **Android** (**ARM64** only. **ARMv7** is not supported) > [!NOTE] > This package depends on [the WebRTC package](https://docs.unity3d.com/Packages/com.unity.webrtc@3.0). If you build for mobile platform (iOS/Android), please see [the package documentation](https://docs.unity3d.com/Packages/com.unity.webrtc@3.0/manual/requirements.html#additional-notes) to know the requirements for building. @@ -64,9 +65,3 @@ Unity Render Streaming supports almost all browsers that can use WebRTC. ## Samples Please check [this page](samples.md). - -## Furioos compatibility - -Unity provides **[Furioos](https://www.furioos.com)** which is a web service to stream any 3D contents on any devices in real-time. - -This version of Unity Render Streaming doesn't support Furioos integration. diff --git a/Documentation~/samples.md b/Documentation~/samples.md index ef05def..944225e 100644 --- a/Documentation~/samples.md +++ b/Documentation~/samples.md @@ -40,7 +40,7 @@ You can change signaling settings below during runtime. | Parameter | Description | | --- | --- | | **Use Default Settings** | Refer to settings of [Project Settings](settings.md). | -| [**Signaling Type**](signaling-type.md) | *Http*, *WebSocket* or *Furioos*. | +| [**Signaling Type**](signaling-type.md) | *Http* or *WebSocket*. | | **Enable/Disable SSL** | Use **https** if set enable. | | **Host Address** | Set IP address or URL of your signaling server. | | **Interval (msec)** | Polling interval for communication of signaling.
This parameter effects Http signaling. | diff --git a/Documentation~/signaling-type.md b/Documentation~/signaling-type.md index 3da6038..90ebf7e 100644 --- a/Documentation~/signaling-type.md +++ b/Documentation~/signaling-type.md @@ -4,7 +4,6 @@ - `Http Signaling` - `WebSocket Signaling` -- `Furioos Signaling` In the example, the schema given to `URL Signaling` is used to determine which type to use. @@ -32,7 +31,3 @@ When the signaling server receives the Offer or Candidate, the server distribute > [!WARNING] > WebSocket does not work in iOS Safari on servers that use self-signed certificates. > If you want to verify the behavior of WebSocket signaling in iOS Safari, use a certificate issued by a trusted certification authority. Or try signaling with HTTP. - -## `Furioos Signaling` - -Please see [this page](deploy-to-furioos.md). diff --git a/Documentation~/turnserver.md b/Documentation~/turnserver.md index c132bed..71117f0 100644 --- a/Documentation~/turnserver.md +++ b/Documentation~/turnserver.md @@ -9,7 +9,7 @@ This document covers the process of linking Unity Render Streaming to a TURN ser ## Instance settings -[coturn](https://github.com/coturn/coturn) software is an open source implementation for TURN servers. +[coturn](https://github.com/coturn/coturn) software is an open source implementation for TURN servers. The following is an explanation for running coturn on a GCP instance. **ubuntu-minimal-1604-xenial-v20190628** is used in the instance image so that the `apt` command can be used to install coturn. If the distribution is supported by coturn, there shouldn't be any issues. See the [coturn documentation](https://github.com/coturn/coturn) for details on coturn. @@ -27,15 +27,15 @@ The port used by the TURN server needs to be public, so add the following settin ### Installing coturn -Log into the GCP instance with `ssh`. +Log into the GCP instance with `ssh`. Install `coturn`. ```shell sudo apt install coturn ``` -Change the settings for booting with a daemon to use coturn as a TURN server. -Edit the following file. +Change the settings for booting with a daemon to use coturn as a TURN server. +Edit the following file. ```shell sudo vim /etc/default/coturn @@ -72,7 +72,7 @@ realm=yourcompany.com log-file=/var/tmp/turn.log ``` -When finished, restart the coturn service. +When finished, restart the coturn service. ```shell sudo systemctl restart coturn @@ -96,25 +96,16 @@ Use the [webrtc sample](https://webrtc.github.io/samples/src/content/peerconnect ![TURN connection testing](images/turn-connection-testing.png) -Click `Gather candidates` to show a list of potential communication paths. Verify that a log is also printed on the TURN server side. +Click `Gather candidates` to show a list of potential communication paths. Verify that a log is also printed on the TURN server side. ### Browser side changes -Change the `config.iceServers` settings under `video-player.js` on the browser side. - -```javascript -config.iceServers = [{ - urls: ['stun:stun.l.google.com:19302'] - }, { - urls: ['turn:xx.xx.xx.xx:3478?transport=tcp'], - username: 'username', - credential: 'password' - } -]; -``` +Start the web server, access the site, and add the TURN server settings to each of the ICE servers as follows: + +![Set ICE Servers Configuration On Browser](images/ice-server-configuration-browser.png) -### Unity side changes +### Unity side changes -Add the TURN server settings to `Ice Server` in the `Render Streaming` inspector. +Open the Project Settings window and add the URL of the TURN server to the Render Streaming settings as shown below. -![TURN Render Streaming inspector](images/turn-renderstreaming-inspector.png) +![TURN Server Settings](images/turn-server-settings.png) diff --git a/Editor/AudioStreamReceiverEditor.cs b/Editor/AudioStreamReceiverEditor.cs index f4ce7e3..b84bbd6 100644 --- a/Editor/AudioStreamReceiverEditor.cs +++ b/Editor/AudioStreamReceiverEditor.cs @@ -1,6 +1,6 @@ #if UNITY_EDITOR -using UnityEngine; using UnityEditor; +using UnityEngine; namespace Unity.RenderStreaming.Editor { @@ -19,8 +19,8 @@ class Styles void OnEnable() { - m_codec = serializedObject.FindProperty("m_Codec"); - m_targetAudioSource = serializedObject.FindProperty("m_TargetAudioSource"); + m_codec = serializedObject.FindProperty(AudioStreamReceiver.CodecPropertyName); + m_targetAudioSource = serializedObject.FindProperty(AudioStreamReceiver.TargetAudioSourcePropertyName); } void OnDisable() diff --git a/Editor/AudioStreamSenderEditor.cs b/Editor/AudioStreamSenderEditor.cs index 1eb80a7..7084862 100644 --- a/Editor/AudioStreamSenderEditor.cs +++ b/Editor/AudioStreamSenderEditor.cs @@ -1,8 +1,8 @@ #if UNITY_EDITOR using System; -using UnityEngine; using UnityEditor; using UnityEditor.AnimatedValues; +using UnityEngine; namespace Unity.RenderStreaming.Editor { @@ -26,19 +26,21 @@ class Styles SerializedProperty m_audioListener; SerializedProperty m_microphoneDeviceIndex; SerializedProperty m_autoRequestUserAuthorization; + SerializedProperty m_loopback; SerializedProperty m_bitrate; static AnimBool[] m_sourceFade; void OnEnable() { - m_source = serializedObject.FindProperty("m_Source"); - m_audioSource = serializedObject.FindProperty("m_AudioSource"); - m_audioListener = serializedObject.FindProperty("m_AudioListener"); - m_microphoneDeviceIndex = serializedObject.FindProperty("m_MicrophoneDeviceIndex"); - m_autoRequestUserAuthorization = serializedObject.FindProperty("m_AutoRequestUserAuthorization"); - m_codec = serializedObject.FindProperty("m_Codec"); - m_bitrate = serializedObject.FindProperty("m_Bitrate"); + m_source = serializedObject.FindProperty(AudioStreamSender.SourcePropertyName); + m_audioSource = serializedObject.FindProperty(AudioStreamSender.AudioSourcePropertyName); + m_audioListener = serializedObject.FindProperty(AudioStreamSender.AudioListenerPropertyName); + m_microphoneDeviceIndex = serializedObject.FindProperty(AudioStreamSender.MicrophoneDeviceIndexPropertyName); + m_autoRequestUserAuthorization = serializedObject.FindProperty(AudioStreamSender.AutoRequestUserAuthorizationPropertyName); + m_codec = serializedObject.FindProperty(AudioStreamSender.CodecPropertyName); + m_bitrate = serializedObject.FindProperty(AudioStreamSender.BitratePropertyName); + m_loopback = serializedObject.FindProperty(AudioStreamSender.LoopbackPropertyName); if (m_sourceFade == null) { @@ -74,6 +76,13 @@ public override void OnInspectorGUI() EditorGUILayout.PropertyField(m_codec); } + EditorGUILayout.Space(); + EditorGUILayout.PropertyField(m_loopback); + if (target is AudioStreamSender sender && sender.isPlaying) + { + sender.loopback = m_loopback.boolValue; + } + EditorGUILayout.Space(); EditorGUILayout.PropertyField(m_bitrate, s_Styles.bitrateContent); diff --git a/Editor/ConfigInfoLine.cs b/Editor/ConfigInfoLine.cs index ed6f0ea..da3eba3 100644 --- a/Editor/ConfigInfoLine.cs +++ b/Editor/ConfigInfoLine.cs @@ -40,14 +40,14 @@ public ConfigInfoLine( m_haveFixer = resolver != null; m_dependTester = dependTester; - var testLabel = new Label(label) {name = "testLabel"}; - var fixer = new Button(resolver) {text = resolverButtonLabel, name = "resolver"}; - var testRow = new VisualElement() {name = "testRow"}; + var testLabel = new Label(label) { name = "testLabel" }; + var fixer = new Button(resolver) { text = resolverButtonLabel, name = "resolver" }; + var testRow = new VisualElement() { name = "testRow" }; testRow.Add(testLabel); if (m_visibleStatus) { - var statusOk = new Image {image = Style.ok, name = "statusOK"}; - var statusError = new Image {image = Style.error, name = "statusError"}; + var statusOk = new Image { image = Style.ok, name = "statusOK" }; + var statusError = new Image { image = Style.error, name = "statusError" }; testRow.Add(statusOk); testRow.Add(statusError); } diff --git a/Editor/CustomSignalingSettingsEditor.cs b/Editor/CustomSignalingSettingsEditor.cs index 1d3ff76..1feec4d 100644 --- a/Editor/CustomSignalingSettingsEditor.cs +++ b/Editor/CustomSignalingSettingsEditor.cs @@ -18,14 +18,14 @@ public class CustomSignalingSettingsEditor : Attribute private readonly string label; /// - /// + /// /// /// /// public CustomSignalingSettingsEditor(Type inspectedType, string label) { if (inspectedType == null) - Debug.LogError("Failed to load CustomEditor inspected type"); + RenderStreaming.Logger.Log(LogType.Error, "Failed to load CustomEditor inspected type"); this.inspectedType = inspectedType; this.label = label; } diff --git a/Editor/IRequestJob.cs b/Editor/IRequestJob.cs index c3e37bb..452fb43 100644 --- a/Editor/IRequestJob.cs +++ b/Editor/IRequestJob.cs @@ -1,11 +1,13 @@ -using UnityEditor.PackageManager; //StatusCode +using UnityEditor.PackageManager; //StatusCode -namespace Unity.RenderStreaming.Editor { +namespace Unity.RenderStreaming.Editor +{ -internal interface IRequestJob { - StatusCode Update(); + internal interface IRequestJob + { + StatusCode Update(); -} + } } //namespace Unity.RenderStreaming.Editor diff --git a/Editor/InputSystem/InputReceiverEditor.cs b/Editor/InputSystem/InputReceiverEditor.cs index 49e0680..03c6ed1 100644 --- a/Editor/InputSystem/InputReceiverEditor.cs +++ b/Editor/InputSystem/InputReceiverEditor.cs @@ -1,6 +1,6 @@ using System; -using System.Linq; using System.Collections.Generic; +using System.Linq; using UnityEditor; using UnityEngine; using UnityEngine.InputSystem; @@ -16,9 +16,11 @@ public void OnEnable() { InputUser.onChange += OnUserChange; - m_ActionsProperty = serializedObject.FindProperty("m_Actions"); - m_ActionEventsProperty = serializedObject.FindProperty("m_ActionEvents"); - m_DefaultActionMapProperty = serializedObject.FindProperty("m_DefaultActionMap"); + m_Local = serializedObject.FindProperty(DataChannelBase.LocalPropertyName); + m_Label = serializedObject.FindProperty(DataChannelBase.LabelPropertyName); + m_Actions = serializedObject.FindProperty(InputReceiver.ActionsPropertyName); + m_ActionEvents = serializedObject.FindProperty(InputReceiver.ActionEventsPropertyName); + m_DefaultActionMap = serializedObject.FindProperty(InputReceiver.DefaultActionMapPropertyName); } private void OnUserChange(InputUser user, InputUserChange change, InputDevice device) { @@ -29,12 +31,12 @@ public override void OnInspectorGUI() { EditorGUI.BeginChangeCheck(); - EditorGUILayout.PropertyField(serializedObject.FindProperty("local")); - EditorGUILayout.PropertyField(serializedObject.FindProperty("label")); + EditorGUILayout.PropertyField(m_Local); + EditorGUILayout.PropertyField(m_Label); // Action config section. EditorGUI.BeginChangeCheck(); - EditorGUILayout.PropertyField(m_ActionsProperty); + EditorGUILayout.PropertyField(m_Actions); if (EditorGUI.EndChangeCheck() || !m_ActionAssetInitialized) OnActionAssetChange(); ++EditorGUI.indentLevel; @@ -48,15 +50,15 @@ public override void OnInspectorGUI() { if (selected == 0) { - m_DefaultActionMapProperty.stringValue = null; + m_DefaultActionMap.stringValue = null; } else { // Use ID rather than name. - var asset = (InputActionAsset)m_ActionsProperty.objectReferenceValue; + var asset = (InputActionAsset)m_Actions.objectReferenceValue; var actionMap = asset.FindActionMap(m_ActionMapOptions[selected].text); if (actionMap != null) - m_DefaultActionMapProperty.stringValue = actionMap.id.ToString(); + m_DefaultActionMap.stringValue = actionMap.id.ToString(); } m_SelectedDefaultActionMap = selected; } @@ -85,7 +87,7 @@ public override void OnInspectorGUI() if (m_ActionMapIndices[i] != n) continue; - EditorGUILayout.PropertyField(m_ActionEventsProperty.GetArrayElementAtIndex(i), m_ActionNames[i]); + EditorGUILayout.PropertyField(m_ActionEvents.GetArrayElementAtIndex(i), m_ActionNames[i]); } } } @@ -144,7 +146,7 @@ private void OnActionAssetChange() m_ActionAssetInitialized = true; var playerInput = (InputReceiver)target; - var asset = (InputActionAsset)m_ActionsProperty.objectReferenceValue; + var asset = (InputActionAsset)m_Actions.objectReferenceValue; if (asset == null) { m_ActionMapOptions = null; @@ -246,12 +248,14 @@ void AddEntry(InputAction action, PlayerInput.ActionEvent actionEvent) [NonSerialized] private int[] m_ActionMapIndices; [NonSerialized] private int m_NumActionMaps; - [NonSerialized] private SerializedProperty m_ActionEventsProperty; + [NonSerialized] private SerializedProperty m_ActionEvents; [NonSerialized] private int m_SelectedDefaultActionMap; [NonSerialized] private GUIContent[] m_ActionMapOptions; - [NonSerialized] private SerializedProperty m_ActionsProperty; - [NonSerialized] private SerializedProperty m_DefaultActionMapProperty; + [NonSerialized] private SerializedProperty m_Local; + [NonSerialized] private SerializedProperty m_Label; + [NonSerialized] private SerializedProperty m_Actions; + [NonSerialized] private SerializedProperty m_DefaultActionMap; [NonSerialized] private bool m_ActionAssetInitialized; } } diff --git a/Editor/PropertyDrawers/BitrateDrawer.cs b/Editor/PropertyDrawers/BitrateDrawer.cs index 5723c83..3c0faa6 100644 --- a/Editor/PropertyDrawers/BitrateDrawer.cs +++ b/Editor/PropertyDrawers/BitrateDrawer.cs @@ -1,6 +1,6 @@ +using System.Reflection; using UnityEditor; using UnityEngine; -using System.Reflection; namespace Unity.RenderStreaming.Editor { diff --git a/Editor/PropertyDrawers/CodecDrawer.cs b/Editor/PropertyDrawers/CodecDrawer.cs index a51e37f..d98ba52 100644 --- a/Editor/PropertyDrawers/CodecDrawer.cs +++ b/Editor/PropertyDrawers/CodecDrawer.cs @@ -1,10 +1,10 @@ #if UNITY_EDITOR using System; -using System.Linq; using System.Collections.Generic; +using System.Linq; +using System.Reflection; using UnityEditor; using UnityEngine; -using System.Reflection; namespace Unity.RenderStreaming.Editor { @@ -56,7 +56,7 @@ public string optionTitle { get { - switch(codec_) + switch (codec_) { case H264CodecInfo h264Codec: return $"{h264Codec.profile} Profile, Level {h264Codec.level.ToString().Insert(1, ".")}"; @@ -102,7 +102,7 @@ public VideoCodec(VideoCodecInfo codec) IEnumerable codecs; string[] codecNames = new string[] { "Default" }; - string[] codecOptions = new string[] {}; + string[] codecOptions = new string[] { }; IEnumerable selectedCodecs; GUIContent codecLabel; @@ -119,7 +119,7 @@ public VideoCodec(VideoCodecInfo codec) static IEnumerable GetAvailableCodecs(UnityEngine.Object target) { - if(target is VideoStreamSender) + if (target is VideoStreamSender) { return VideoStreamSender.GetAvailableCodecs().Select(codec => new VideoCodec(codec)); } @@ -202,7 +202,7 @@ public override void OnGUI(Rect position, SerializedProperty property, GUIConten if (EditorGUI.EndChangeCheck()) { - if(0 < selectCodecIndex) + if (0 < selectCodecIndex) { string codecName = codecNames[selectCodecIndex]; selectedCodecs = codecs.Where(codec => codec.name == codecName).OrderBy(codec => codec.order); @@ -211,7 +211,7 @@ public override void OnGUI(Rect position, SerializedProperty property, GUIConten var codec = selectedCodecs.First(); propertyMimeType.stringValue = codec.mimeType; propertySdpFmtpLine.stringValue = codec.sdpFmtpLine; - if(propertyChannelCount != null) + if (propertyChannelCount != null) propertyChannelCount.intValue = codec.channelCount; if (propertySampleRate != null) propertySampleRate.intValue = codec.sampleRate; diff --git a/Editor/PropertyDrawers/FrameRateDrawer.cs b/Editor/PropertyDrawers/FrameRateDrawer.cs index 72e2ffa..1189efb 100644 --- a/Editor/PropertyDrawers/FrameRateDrawer.cs +++ b/Editor/PropertyDrawers/FrameRateDrawer.cs @@ -1,6 +1,6 @@ +using System.Reflection; using UnityEditor; using UnityEngine; -using System.Reflection; namespace Unity.RenderStreaming.Editor { @@ -67,7 +67,7 @@ public override void OnGUI(Rect position, SerializedProperty property, GUIConten } if (!Mathf.Approximately(value, newValue)) { - if(Application.isPlaying) + if (Application.isPlaying) { var objectReferenceValue = property.serializedObject.targetObject; var type = objectReferenceValue.GetType(); diff --git a/Editor/PropertyDrawers/RenderTextureDepthBufferDrawer.cs b/Editor/PropertyDrawers/RenderTextureDepthBufferDrawer.cs index 844031d..cd1a952 100644 --- a/Editor/PropertyDrawers/RenderTextureDepthBufferDrawer.cs +++ b/Editor/PropertyDrawers/RenderTextureDepthBufferDrawer.cs @@ -1,6 +1,6 @@ #if UNITY_EDITOR -using UnityEngine; using UnityEditor; +using UnityEngine; namespace Unity.RenderStreaming.Editor { diff --git a/Editor/PropertyDrawers/RenderTexureAntiAliasingDrawer.cs b/Editor/PropertyDrawers/RenderTexureAntiAliasingDrawer.cs index 63253c7..6e4a8d5 100644 --- a/Editor/PropertyDrawers/RenderTexureAntiAliasingDrawer.cs +++ b/Editor/PropertyDrawers/RenderTexureAntiAliasingDrawer.cs @@ -1,7 +1,7 @@ #if UNITY_EDITOR -using UnityEngine; using UnityEditor; using UnityEditor.IMGUI.Controls; +using UnityEngine; namespace Unity.RenderStreaming.Editor { diff --git a/Editor/PropertyDrawers/ScaleResolutionDrawer.cs b/Editor/PropertyDrawers/ScaleResolutionDrawer.cs index 1b46051..a6f4111 100644 --- a/Editor/PropertyDrawers/ScaleResolutionDrawer.cs +++ b/Editor/PropertyDrawers/ScaleResolutionDrawer.cs @@ -1,6 +1,6 @@ +using System.Reflection; using UnityEditor; using UnityEngine; -using System.Reflection; namespace Unity.RenderStreaming.Editor { @@ -61,7 +61,7 @@ public override void OnGUI(Rect position, SerializedProperty property, GUIConten { property.floatValue = newValue; - if(Application.isPlaying) + if (Application.isPlaying) { var objectReferenceValue = property.serializedObject.targetObject; var type = objectReferenceValue.GetType(); diff --git a/Editor/PropertyDrawers/SignalingSettingsDrawer.cs b/Editor/PropertyDrawers/SignalingSettingsDrawer.cs index bd32b75..49cd028 100644 --- a/Editor/PropertyDrawers/SignalingSettingsDrawer.cs +++ b/Editor/PropertyDrawers/SignalingSettingsDrawer.cs @@ -43,7 +43,9 @@ PopupField CreatePopUpSignalingType(SerializedProperty property, string var settings = fieldInfo.GetValue(property.serializedObject.targetObject) as SignalingSettings; var defaultValue = CustomSignalingSettingsEditor.FindLabelByInspectedType(settings.GetType()); var choices = CustomSignalingSettingsEditor.Labels().ToList(); - return new PopupField(label: label, choices: choices, defaultValue: defaultValue); + var field = new PopupField(label: label, choices: choices, defaultValue: defaultValue); + field.tooltip = "Choose the signaling type. \"WebSocket\" or \"HTTP Polling\"."; + return field; } static void ReplaceVisualElement(VisualElement oldValue, VisualElement newValue) @@ -67,7 +69,7 @@ void OnSignalingSettingsObjectChange(SerializedPropertyChangeEvent e, Serialized void OnPopupFieldValueChange(ChangeEvent e, SerializedProperty property) { - if(!(fieldInfo.GetValue(property.serializedObject.targetObject) is SignalingSettings settings)) + if (!(fieldInfo.GetValue(property.serializedObject.targetObject) is SignalingSettings settings)) return; // cache current settings. @@ -137,16 +139,4 @@ public VisualElement CreateInspectorGUI(SerializedProperty property) return root; } } - - [CustomSignalingSettingsEditor(typeof(FurioosSignalingSettings), "Furioos")] - internal class FurioosSignalingSettingsEditor : ISignalingSettingEditor - { - public VisualElement CreateInspectorGUI(SerializedProperty property) - { - VisualElement root = new VisualElement(); - root.Add(new PropertyField(property.FindPropertyRelative("m_url"), "URL")); - root.Add(new PropertyField(property.FindPropertyRelative("m_iceServers"), "ICE Servers")); - return root; - } - } } diff --git a/Editor/PropertyDrawers/StreamingSizeDrawer.cs b/Editor/PropertyDrawers/StreamingSizeDrawer.cs index ff43774..eb63945 100644 --- a/Editor/PropertyDrawers/StreamingSizeDrawer.cs +++ b/Editor/PropertyDrawers/StreamingSizeDrawer.cs @@ -1,6 +1,6 @@ +using System.Reflection; using UnityEditor; using UnityEngine; -using System.Reflection; namespace Unity.RenderStreaming.Editor { @@ -71,14 +71,14 @@ public override void OnGUI(Rect position, SerializedProperty property, GUIConten } else { - if(!IsCustomValue(value)) + if (!IsCustomValue(value)) { value = Vector2Int.zero; } cutomValueRect.height = EditorGUIUtility.singleLineHeight; newValue = EditorGUI.Vector2IntField(cutomValueRect, s_customValueLabel, value); } - if(property.vector2IntValue != newValue) + if (property.vector2IntValue != newValue) { if (Application.isPlaying) { diff --git a/Editor/RenderPipeline/HDRPInitialSetupPostProcessor.cs b/Editor/RenderPipeline/HDRPInitialSetupPostProcessor.cs index 71675f8..bc77db3 100644 --- a/Editor/RenderPipeline/HDRPInitialSetupPostProcessor.cs +++ b/Editor/RenderPipeline/HDRPInitialSetupPostProcessor.cs @@ -1,4 +1,4 @@ -#if URS_USE_HDRP_EDITOR +#if URS_USE_HDRP_EDITOR using UnityEngine; //Debug using UnityEditor; //AssetPostProcessor using UnityEngine.Rendering; //GraphicsSettings diff --git a/Editor/RenderPipeline/URPInitialSetupPostProcessor.cs b/Editor/RenderPipeline/URPInitialSetupPostProcessor.cs index b9779d6..4fd9aef 100644 --- a/Editor/RenderPipeline/URPInitialSetupPostProcessor.cs +++ b/Editor/RenderPipeline/URPInitialSetupPostProcessor.cs @@ -1,4 +1,4 @@ -#if URS_USE_URS_EDITOR +#if URS_USE_URS_EDITOR using UnityEngine; //Debug using UnityEditor; //AssetPostProcessor using UnityEngine.Rendering; //GraphicsSettings diff --git a/Editor/RenderStreamingProjectSettings.cs b/Editor/RenderStreamingProjectSettings.cs index 7bc3a05..1a1df95 100644 --- a/Editor/RenderStreamingProjectSettings.cs +++ b/Editor/RenderStreamingProjectSettings.cs @@ -60,7 +60,7 @@ static void Save() { if (s_Instance == null) { - Debug.Log("Cannot save ScriptableSingleton: no instance!"); + RenderStreaming.Logger.Log("Cannot save ScriptableSingleton: no instance!"); return; } @@ -70,7 +70,7 @@ static void Save() Directory.CreateDirectory(folderPath); } - InternalEditorUtility.SaveToSerializedFileAndForget(new Object[] {s_Instance}, filePath, + InternalEditorUtility.SaveToSerializedFileAndForget(new Object[] { s_Instance }, filePath, allowTextSerialization: true); } } diff --git a/Editor/RenderStreamingProjectSettingsProvider.cs b/Editor/RenderStreamingProjectSettingsProvider.cs index fe91df7..49e99ce 100644 --- a/Editor/RenderStreamingProjectSettingsProvider.cs +++ b/Editor/RenderStreamingProjectSettingsProvider.cs @@ -20,6 +20,9 @@ internal class RenderStreamingProjectSettingsProvider : SettingsProvider private int currentSelectedSettingsAsset; private RenderStreamingSettings settings; + const string LabelRenderStreamingSettingsAsset = "Render Streaming Settings Asset"; + const string LabelCreateSettingsButton = "Create New Settings Asset"; + const string kSettingsPath = "Project/Render Streaming"; const string kTemplatePath = "Packages/com.unity.renderstreaming/Editor/UXML/RenderStreamingProjectSettings.uxml"; const string kStylePath = "Packages/com.unity.renderstreaming/Editor/Styles/RenderStreamingProjectSettings.uss"; @@ -59,10 +62,11 @@ public override void OnActivate(string searchContext, VisualElement rootElement) var defaultIndex = ArrayHelpers.IndexOf(availableRenderStreamingSettingsAssets, AssetDatabase.GetAssetPath(settings)); var choices = availableRenderStreamingSettingsAssets.ToList(); - var selectPopup = new PopupField(label: label, choices: choices, defaultIndex: defaultIndex) + var selectPopup = new PopupField(label: LabelRenderStreamingSettingsAsset, choices: choices, defaultIndex: defaultIndex) { name = "renderStreamingSettingsSelectPopup" }; + selectPopup.tooltip = "Choose the Render Streaming Settings."; selectPopup.RegisterValueChangedCallback(evt => { currentSelectedSettingsAsset = selectPopup.index; @@ -77,7 +81,7 @@ public override void OnActivate(string searchContext, VisualElement rootElement) }); selectorContainer.Add(selectPopup); - var createSettingsButton = new Button {text = "Create New Settings Asset"}; + var createSettingsButton = new Button { text = LabelCreateSettingsButton }; createSettingsButton.clicked += () => { CreateNewSettingsAsset(); @@ -87,7 +91,7 @@ public override void OnActivate(string searchContext, VisualElement rootElement) var createAssetHelpBox = new HelpBox("Settings for the Render Streaming are not stored in an asset. Click the button above to create a settings asset you can edit.", HelpBoxMessageType.Info) { - style = {display = noSettingsInAssets ? DisplayStyle.Flex : DisplayStyle.None} + style = { display = noSettingsInAssets ? DisplayStyle.Flex : DisplayStyle.None } }; selectorContainer.Add(createAssetHelpBox); @@ -95,7 +99,7 @@ public override void OnActivate(string searchContext, VisualElement rootElement) // Disable UI when running in Playmode EditorApplication.playModeStateChanged += OnPlayModeStateChanged; - if(EditorApplication.isPlaying) + if (EditorApplication.isPlaying) rootVisualElement.SetEnabled(false); } @@ -133,7 +137,7 @@ private static void CreateNewSettingsAsset() var dataPath = Application.dataPath + "/"; if (!path.StartsWith(dataPath, StringComparison.CurrentCultureIgnoreCase)) { - Debug.LogError($"Render Streaming settings must be stored in Assets folder of the project (got: '{path}')"); + RenderStreaming.Logger.Log(LogType.Error, $"Render Streaming settings must be stored in Assets folder of the project (got: '{path}')"); return; } diff --git a/Editor/RenderStreamingSettingsEditor.cs b/Editor/RenderStreamingSettingsEditor.cs index 11878fc..f1a7e2d 100644 --- a/Editor/RenderStreamingSettingsEditor.cs +++ b/Editor/RenderStreamingSettingsEditor.cs @@ -10,8 +10,8 @@ internal class RenderStreamingSettingsEditor : UnityEditor.Editor public override VisualElement CreateInspectorGUI() { var root = new VisualElement(); - root.Add(new PropertyField(serializedObject.FindProperty("automaticStreaming"), "Automatic Streaming")); - root.Add(new PropertyField(serializedObject.FindProperty("signalingSettings"), "Signaling Settings")); + root.Add(new PropertyField(serializedObject.FindProperty(RenderStreamingSettings.AutomaticStreamingPropertyName), "Automatic Streaming")); + root.Add(new PropertyField(serializedObject.FindProperty(RenderStreamingSettings.SignalingSettingsPropertyName), "Signaling Settings")); return root; } } diff --git a/Editor/RenderStreamingWizard.cs b/Editor/RenderStreamingWizard.cs index 2c170f5..f8281c1 100644 --- a/Editor/RenderStreamingWizard.cs +++ b/Editor/RenderStreamingWizard.cs @@ -272,16 +272,16 @@ private static void FixSupportedGraphics() { case BuildTarget.StandaloneOSX: case BuildTarget.iOS: - PlayerSettings.SetGraphicsAPIs(target, new[] {GraphicsDeviceType.Metal}); + PlayerSettings.SetGraphicsAPIs(target, new[] { GraphicsDeviceType.Metal }); break; case BuildTarget.Android: - PlayerSettings.SetGraphicsAPIs(target, new[] {GraphicsDeviceType.OpenGLES3, GraphicsDeviceType.Vulkan}); + PlayerSettings.SetGraphicsAPIs(target, new[] { GraphicsDeviceType.OpenGLES3, GraphicsDeviceType.Vulkan }); break; case BuildTarget.StandaloneWindows64: - PlayerSettings.SetGraphicsAPIs(target, new[] {GraphicsDeviceType.Direct3D11, GraphicsDeviceType.Direct3D12, GraphicsDeviceType.Vulkan}); + PlayerSettings.SetGraphicsAPIs(target, new[] { GraphicsDeviceType.Direct3D11, GraphicsDeviceType.Direct3D12, GraphicsDeviceType.Vulkan }); break; case BuildTarget.StandaloneLinux64: - PlayerSettings.SetGraphicsAPIs(target, new[] {GraphicsDeviceType.OpenGLCore, GraphicsDeviceType.Vulkan}); + PlayerSettings.SetGraphicsAPIs(target, new[] { GraphicsDeviceType.OpenGLCore, GraphicsDeviceType.Vulkan }); break; default: throw new NotSupportedException($"{nameof(target)} is not supported."); @@ -454,11 +454,12 @@ private void BindCheckVersion() { var checkUpdateContainer = rootVisualElement.Q("checkUpdateContainer"); - var label = new TextElement {text = "Current Render Streaming version: checking..."}; + var label = new TextElement { text = "Current Render Streaming version: checking..." }; checkUpdateContainer.Add(label); var button = new Button(() => - UnityEditor.PackageManager.UI.Window.Open(packageName)) {text = "Check update"}; + UnityEditor.PackageManager.UI.Window.Open(packageName)) + { text = "Check update" }; button.AddToClassList("right-anchored-button"); checkUpdateContainer.Add(button); @@ -467,7 +468,7 @@ private void BindCheckVersion() var packageInfo = req.FindPackage(packageName); if (null == packageInfo) { - Debug.LogError($"Not found package \"{packageName}\""); + RenderStreaming.Logger.Log(LogType.Error, $"Not found package \"{packageName}\""); return; } @@ -479,7 +480,7 @@ private void BindCurrentSettings() { var checkUpdateContainer = rootVisualElement.Q("currentSettingsContainer"); - currentSettingsLabel = new Label {text = $"Current Render Streaming Settings: {GetSettingsAssetName()}"}; + currentSettingsLabel = new Label { text = $"Current Render Streaming Settings: {GetSettingsAssetName()}" }; currentSettingsLabel.AddToClassList("normal"); checkUpdateContainer.Add(currentSettingsLabel); @@ -492,7 +493,7 @@ private void BindCurrentSettings() currentSettingsHelpBox = new HelpBox("Current selected settings is default. If you want to change settings, open the Project Window and create or select another Settings.", HelpBoxMessageType.Info) { - style = {display = IsDefaultSetting() ? DisplayStyle.Flex : DisplayStyle.None} + style = { display = IsDefaultSetting() ? DisplayStyle.Flex : DisplayStyle.None } }; checkUpdateContainer.Add(currentSettingsHelpBox); } @@ -564,7 +565,8 @@ private void BindWebApp() var dstPath = EditorUtility.OpenFolderPanel("Select download folder", "", ""); WebAppDownloader.DownloadWebApp(version, dstPath, null); }); - }) {text = "Download latest version web app."}; + }) + { text = "Download latest version web app." }; webappButton.AddToClassList("large-button"); var showWebAppDocButton = new Button(() => @@ -574,7 +576,8 @@ private void BindWebApp() var url = WebAppDownloader.GetURLDocumentation(version); Application.OpenURL(url); }); - }) {text = "Show web app documentation."}; + }) + { text = "Show web app documentation." }; showWebAppDocButton.AddToClassList("large-button"); var showWebAppSourceButton = new Button(() => @@ -584,7 +587,8 @@ private void BindWebApp() var url = WebAppDownloader.GetURLSourceCode(version); Application.OpenURL(url); }); - }) {text = "Show web app source code."}; + }) + { text = "Show web app source code." }; showWebAppSourceButton.AddToClassList("large-button"); webappContainer.Add(webappButton); diff --git a/Editor/RequestExtensions.cs b/Editor/RequestExtensions.cs index b5df872..52266e8 100644 --- a/Editor/RequestExtensions.cs +++ b/Editor/RequestExtensions.cs @@ -1,38 +1,43 @@ -using UnityEditor.PackageManager.Requests; //ListRequest -using UnityEditor.PackageManager; //PackageCollection using System.Collections.Generic; //IEnumerable +using UnityEditor.PackageManager; //PackageCollection +using UnityEditor.PackageManager.Requests; //ListRequest -namespace Unity.RenderStreaming.Editor { - -/// -/// An extension class to extend the functionalities of UnityEditor.PackageManager.Requests classes -/// -internal static class RequestExtensions +namespace Unity.RenderStreaming.Editor { /// - /// Find a PackageInfo which has the passed parameter + /// An extension class to extend the functionalities of UnityEditor.PackageManager.Requests classes /// - /// list of Request object - /// the package name - /// The PackageInfo if found, otherwise null - public static PackageInfo FindPackage(this Request listRequest, string packageName) { - IEnumerable packageInfoCollection = listRequest.Result as IEnumerable; - if (null == packageInfoCollection) { - return null; - } + internal static class RequestExtensions + { - var enumerator = packageInfoCollection.GetEnumerator(); - while (enumerator.MoveNext()) { - PackageInfo curInfo = enumerator.Current; - if (curInfo.name == packageName) { - return curInfo; + /// + /// Find a PackageInfo which has the passed parameter + /// + /// list of Request object + /// the package name + /// The PackageInfo if found, otherwise null + public static PackageInfo FindPackage(this Request listRequest, string packageName) + { + IEnumerable packageInfoCollection = listRequest.Result as IEnumerable; + if (null == packageInfoCollection) + { + return null; + } + + var enumerator = packageInfoCollection.GetEnumerator(); + while (enumerator.MoveNext()) + { + PackageInfo curInfo = enumerator.Current; + if (curInfo.name == packageName) + { + return curInfo; + } } - } - return null; + return null; + } } -} } //namespace Unity.RenderStreaming.Editor diff --git a/Editor/RequestInfo/AddRequestInfo.cs b/Editor/RequestInfo/AddRequestInfo.cs index a6c41d3..2e32633 100644 --- a/Editor/RequestInfo/AddRequestInfo.cs +++ b/Editor/RequestInfo/AddRequestInfo.cs @@ -1,20 +1,22 @@ -using System; //Action -using UnityEditor.PackageManager.Requests; //ListRequest, AddRequest, etc +using System; //Action using UnityEditor.PackageManager; //PackageInfo +using UnityEditor.PackageManager.Requests; //ListRequest, AddRequest, etc -namespace Unity.RenderStreaming.Editor { +namespace Unity.RenderStreaming.Editor +{ -class AddRequestInfo { - internal string PackageName; - internal Action> OnSuccessAction; - internal Action> OnFailAction; - internal AddRequestInfo(string packageName, - Action> onSuccess, Action> onFail) + class AddRequestInfo { - PackageName = packageName; - OnSuccessAction = onSuccess; - OnFailAction = onFail; + internal string PackageName; + internal Action> OnSuccessAction; + internal Action> OnFailAction; + internal AddRequestInfo(string packageName, + Action> onSuccess, Action> onFail) + { + PackageName = packageName; + OnSuccessAction = onSuccess; + OnFailAction = onFail; + } } -} } //namespace Unity.RenderStreaming.Editor diff --git a/Editor/RequestInfo/ListRequestInfo.cs b/Editor/RequestInfo/ListRequestInfo.cs index 703e0d9..e70169c 100644 --- a/Editor/RequestInfo/ListRequestInfo.cs +++ b/Editor/RequestInfo/ListRequestInfo.cs @@ -1,23 +1,25 @@ -using System; //Action -using UnityEditor.PackageManager.Requests; //ListRequest, AddRequest, etc +using System; //Action using UnityEditor.PackageManager; //PackageCollection +using UnityEditor.PackageManager.Requests; //ListRequest, AddRequest, etc -namespace Unity.RenderStreaming.Editor { - -class ListRequestInfo { - internal bool OfflineMode; - internal bool IncludeIndirectIndependencies; - internal Action> OnSuccessAction; - internal Action> OnFailAction; +namespace Unity.RenderStreaming.Editor +{ - internal ListRequestInfo(bool offlineMode, bool includeIndirectDependencies, - Action> onSuccess, Action> onFail) + class ListRequestInfo { - OfflineMode = offlineMode; - IncludeIndirectIndependencies = includeIndirectDependencies; - OnSuccessAction = onSuccess; - OnFailAction = onFail; + internal bool OfflineMode; + internal bool IncludeIndirectIndependencies; + internal Action> OnSuccessAction; + internal Action> OnFailAction; + + internal ListRequestInfo(bool offlineMode, bool includeIndirectDependencies, + Action> onSuccess, Action> onFail) + { + OfflineMode = offlineMode; + IncludeIndirectIndependencies = includeIndirectDependencies; + OnSuccessAction = onSuccess; + OnFailAction = onFail; + } } -} } //namespace Unity.RenderStreaming.Editor diff --git a/Editor/RequestInfo/RemoveRequestInfo.cs b/Editor/RequestInfo/RemoveRequestInfo.cs index 4a7da8e..b53e538 100644 --- a/Editor/RequestInfo/RemoveRequestInfo.cs +++ b/Editor/RequestInfo/RemoveRequestInfo.cs @@ -1,19 +1,21 @@ -using System; //Action +using System; //Action -namespace Unity.RenderStreaming.Editor { +namespace Unity.RenderStreaming.Editor +{ -class RemoveRequestInfo{ - internal string PackageName; - internal Action OnSuccessAction; - internal Action OnFailAction; - - internal RemoveRequestInfo(string packageName, - Action onSuccess, Action onFail) + class RemoveRequestInfo { - PackageName = packageName; - OnSuccessAction = onSuccess; - OnFailAction = onFail; + internal string PackageName; + internal Action OnSuccessAction; + internal Action OnFailAction; + + internal RemoveRequestInfo(string packageName, + Action onSuccess, Action onFail) + { + PackageName = packageName; + OnSuccessAction = onSuccess; + OnFailAction = onFail; + } } -} } //namespace Unity.RenderStreaming.Editor diff --git a/Editor/RequestInfo/SearchAllRequestInfo.cs b/Editor/RequestInfo/SearchAllRequestInfo.cs index 22cb72b..f3b7401 100644 --- a/Editor/RequestInfo/SearchAllRequestInfo.cs +++ b/Editor/RequestInfo/SearchAllRequestInfo.cs @@ -1,21 +1,23 @@ -using System; //Action -using UnityEditor.PackageManager.Requests; //Request +using System; //Action using UnityEditor.PackageManager; //PackageInfo +using UnityEditor.PackageManager.Requests; //Request -namespace Unity.RenderStreaming.Editor { - -class SearchAllRequestInfo { - internal bool OfflineMode; - internal Action> OnSuccessAction; - internal Action> OnFailAction; +namespace Unity.RenderStreaming.Editor +{ - internal SearchAllRequestInfo(bool offlineMode, - Action> onSuccess, Action> onFail) + class SearchAllRequestInfo { - OfflineMode = offlineMode; - OnSuccessAction = onSuccess; - OnFailAction = onFail; + internal bool OfflineMode; + internal Action> OnSuccessAction; + internal Action> OnFailAction; + + internal SearchAllRequestInfo(bool offlineMode, + Action> onSuccess, Action> onFail) + { + OfflineMode = offlineMode; + OnSuccessAction = onSuccess; + OnFailAction = onFail; + } } -} } //namespace Unity.RenderStreaming.Editor diff --git a/Editor/RequestInfo/SearchRequestInfo.cs b/Editor/RequestInfo/SearchRequestInfo.cs index 7b0da90..f06eee4 100644 --- a/Editor/RequestInfo/SearchRequestInfo.cs +++ b/Editor/RequestInfo/SearchRequestInfo.cs @@ -1,23 +1,25 @@ -using System; //Action -using UnityEditor.PackageManager.Requests; //Request +using System; //Action using UnityEditor.PackageManager; //PackageInfo +using UnityEditor.PackageManager.Requests; //Request -namespace Unity.RenderStreaming.Editor { - -class SearchRequestInfo { - internal string PackageName; - internal bool OfflineMode; - internal Action> OnSuccessAction; - internal Action> OnFailAction; +namespace Unity.RenderStreaming.Editor +{ - internal SearchRequestInfo(string packageName, bool offlineMode, - Action> onSuccess, Action> onFail) + class SearchRequestInfo { - PackageName = packageName; - OfflineMode = offlineMode; - OnSuccessAction = onSuccess; - OnFailAction = onFail; + internal string PackageName; + internal bool OfflineMode; + internal Action> OnSuccessAction; + internal Action> OnFailAction; + + internal SearchRequestInfo(string packageName, bool offlineMode, + Action> onSuccess, Action> onFail) + { + PackageName = packageName; + OfflineMode = offlineMode; + OnSuccessAction = onSuccess; + OnFailAction = onFail; + } } -} } //namespace Unity.RenderStreaming.Editor diff --git a/Editor/RequestJob.cs b/Editor/RequestJob.cs index dbfbcfe..37a8dc5 100644 --- a/Editor/RequestJob.cs +++ b/Editor/RequestJob.cs @@ -1,123 +1,144 @@ -using UnityEditor.PackageManager.Requests; //Request -using UnityEditor.PackageManager; //StatusCode using System; //Action +using UnityEditor.PackageManager; //StatusCode +using UnityEditor.PackageManager.Requests; //Request -namespace Unity.RenderStreaming.Editor { - -//--------------------------------------------------------------------------------------------------------------------- -//Non-generics version -internal class RequestJob : IRequestJob { +namespace Unity.RenderStreaming.Editor +{ - internal RequestJob(Request req, Action onSuccess, Action onFail) { - m_request = req; - m_onSuccess = onSuccess; - m_onFail = onFail; - } + //--------------------------------------------------------------------------------------------------------------------- + //Non-generics version + internal class RequestJob : IRequestJob + { -//--------------------------------------------------------------------------------------------------------------------- - - public StatusCode Update() { - if (null == m_request) { - OnFail(); - return StatusCode.Failure; + internal RequestJob(Request req, Action onSuccess, Action onFail) + { + m_request = req; + m_onSuccess = onSuccess; + m_onFail = onFail; } - if (m_request.IsCompleted) { - if (StatusCode.Success == m_request.Status ) { - OnSuccess(); - return StatusCode.Success; - } else { - OnFail(); + //--------------------------------------------------------------------------------------------------------------------- + + public StatusCode Update() + { + if (null == m_request) + { + OnFail(); return StatusCode.Failure; } - } - return StatusCode.InProgress; + if (m_request.IsCompleted) + { + if (StatusCode.Success == m_request.Status) + { + OnSuccess(); + return StatusCode.Success; + } + else + { + OnFail(); + return StatusCode.Failure; + } + } - } + return StatusCode.InProgress; -//--------------------------------------------------------------------------------------------------------------------- + } + //--------------------------------------------------------------------------------------------------------------------- - void OnSuccess() { - if (null==m_onSuccess) - return; - m_onSuccess(); - } + void OnSuccess() + { + if (null == m_onSuccess) + return; -//--------------------------------------------------------------------------------------------------------------------- - - void OnFail() { - if (null==m_onFail) - return; + m_onSuccess(); + } - m_onFail(); - } + //--------------------------------------------------------------------------------------------------------------------- - Request m_request; - Action m_onSuccess; - Action m_onFail; -} //end RequestJob (non-generics version) + void OnFail() + { + if (null == m_onFail) + return; -//--------------------------------------------------------------------------------------------------------------------- + m_onFail(); + } -//RequestJob (generics version) -//Examples of T: PackageCollection(from ListRequest), PackageInfo (from AddRequest) -internal class RequestJob : IRequestJob { + Request m_request; + Action m_onSuccess; + Action m_onFail; + } //end RequestJob (non-generics version) - internal RequestJob(Request req, Action> onSuccess, Action> onFail) { - m_request = req; - m_onSuccess = onSuccess; - m_onFail = onFail; - } + //--------------------------------------------------------------------------------------------------------------------- -//--------------------------------------------------------------------------------------------------------------------- + //RequestJob (generics version) + //Examples of T: PackageCollection(from ListRequest), PackageInfo (from AddRequest) + internal class RequestJob : IRequestJob + { - public StatusCode Update() { - if (null == m_request) { - OnFail(); - return StatusCode.Failure; + internal RequestJob(Request req, Action> onSuccess, Action> onFail) + { + m_request = req; + m_onSuccess = onSuccess; + m_onFail = onFail; } - if (m_request.IsCompleted) { - if (StatusCode.Success == m_request.Status ) { - OnSuccess(); - return StatusCode.Success; - } else { - OnFail(); + //--------------------------------------------------------------------------------------------------------------------- + + public StatusCode Update() + { + if (null == m_request) + { + OnFail(); return StatusCode.Failure; } - } - return StatusCode.InProgress; + if (m_request.IsCompleted) + { + if (StatusCode.Success == m_request.Status) + { + OnSuccess(); + return StatusCode.Success; + } + else + { + OnFail(); + return StatusCode.Failure; + } + } - } + return StatusCode.InProgress; -//--------------------------------------------------------------------------------------------------------------------- + } + //--------------------------------------------------------------------------------------------------------------------- - void OnSuccess() { - if (null==m_onSuccess) - return; - m_onSuccess(m_request); - } + void OnSuccess() + { + if (null == m_onSuccess) + return; -//--------------------------------------------------------------------------------------------------------------------- + m_onSuccess(m_request); + } - void OnFail() { - if (null==m_onFail) - return; + //--------------------------------------------------------------------------------------------------------------------- - m_onFail(m_request); - } + void OnFail() + { + if (null == m_onFail) + return; -//--------------------------------------------------------------------------------------------------------------------- + m_onFail(m_request); + } + + //--------------------------------------------------------------------------------------------------------------------- - Request m_request; - Action> m_onSuccess; - Action> m_onFail; -} + Request m_request; + Action> m_onSuccess; + Action> m_onFail; + } } //Unity.RenderStreaming.Editor diff --git a/Editor/RequestJobManager.cs b/Editor/RequestJobManager.cs index 7a89eb9..505ed8e 100644 --- a/Editor/RequestJobManager.cs +++ b/Editor/RequestJobManager.cs @@ -1,197 +1,207 @@ +using System; //Action using System.Collections.Generic; //HashSet -using UnityEditor.PackageManager.Requests; //ListRequest, AddRequest, etc using UnityEditor.PackageManager; //PackageCollection -using System; //Action - - -namespace Unity.RenderStreaming.Editor { +using UnityEditor.PackageManager.Requests; //ListRequest, AddRequest, etc -/// -/// An editor class to manage requests to UnityEditor.PackageManager.Client -/// This class will perform its operations in background while Unity is running. -/// -internal class RequestJobManager +namespace Unity.RenderStreaming.Editor { - [UnityEditor.InitializeOnLoadMethod] - static void OnLoad() { - UnityEditor.EditorApplication.update+=UpdateRequestJobs; - } - -//--------------------------------------------------------------------------------------------------------------------- - - /// - /// Queue a job to list the packages the project depends on. - /// - /// Specifies whether or not the Package Manager requests the latest information about - /// the project's packages from the remote Unity package registry. When offlineMode is true, - /// the PackageInfo objects in the PackageCollection returned by the Package Manager contain information - /// obtained from the local package cache, which could be out of date. - /// Set to true to include indirect dependencies in the - /// PackageCollection returned by the Package Manager. Indirect dependencies include packages referenced - /// in the manifests of project packages or in the manifests of other indirect dependencies. Set to false - /// to include only the packages listed directly in the project manifest. - /// Action which is executed if the request succeeded - /// Action which is executed if the request failed - /// - public static void CreateListRequest(bool offlineMode, bool includeIndirectIndependencies, - Action> onSuccess, Action> onFail) - { - m_pendingListRequests.Enqueue(new ListRequestInfo(offlineMode, includeIndirectIndependencies, onSuccess, onFail)); - } -//--------------------------------------------------------------------------------------------------------------------- /// - /// Queue a job to add a package dependency to the project. + /// An editor class to manage requests to UnityEditor.PackageManager.Client + /// This class will perform its operations in background while Unity is running. /// - /// The name or ID of the package to add. If only the name is specified, - /// the latest version of the package is installed. - /// Action which is executed if the request succeeded - /// Action which is executed if the request failed - /// - public static void CreateAddRequest(string packageName, - Action> onSuccess, Action> onFail) + internal class RequestJobManager { - m_pendingAddRequests.Enqueue(new AddRequestInfo(packageName, onSuccess, onFail)); - } - -//--------------------------------------------------------------------------------------------------------------------- + [UnityEditor.InitializeOnLoadMethod] + static void OnLoad() + { + UnityEditor.EditorApplication.update += UpdateRequestJobs; + } - /// - /// Queue a job to removes a previously added package from the project. - /// - /// The name or ID of the package to add. - /// Action which is executed if the request succeeded - /// Action which is executed if the request failed - /// - public static void CreateRemoveRequest(string packageName, Action onSuccess, Action onFail) - { - m_pendingRemoveRequests.Enqueue(new RemoveRequestInfo(packageName, onSuccess, onFail)); - } + //--------------------------------------------------------------------------------------------------------------------- + + /// + /// Queue a job to list the packages the project depends on. + /// + /// Specifies whether or not the Package Manager requests the latest information about + /// the project's packages from the remote Unity package registry. When offlineMode is true, + /// the PackageInfo objects in the PackageCollection returned by the Package Manager contain information + /// obtained from the local package cache, which could be out of date. + /// Set to true to include indirect dependencies in the + /// PackageCollection returned by the Package Manager. Indirect dependencies include packages referenced + /// in the manifests of project packages or in the manifests of other indirect dependencies. Set to false + /// to include only the packages listed directly in the project manifest. + /// Action which is executed if the request succeeded + /// Action which is executed if the request failed + /// + public static void CreateListRequest(bool offlineMode, bool includeIndirectIndependencies, + Action> onSuccess, Action> onFail) + { + m_pendingListRequests.Enqueue(new ListRequestInfo(offlineMode, includeIndirectIndependencies, onSuccess, onFail)); + } -//--------------------------------------------------------------------------------------------------------------------- + //--------------------------------------------------------------------------------------------------------------------- + + /// + /// Queue a job to add a package dependency to the project. + /// + /// The name or ID of the package to add. If only the name is specified, + /// the latest version of the package is installed. + /// Action which is executed if the request succeeded + /// Action which is executed if the request failed + /// + public static void CreateAddRequest(string packageName, + Action> onSuccess, Action> onFail) + { + m_pendingAddRequests.Enqueue(new AddRequestInfo(packageName, onSuccess, onFail)); + } - /// - /// Queue a job to searches the Unity package registry for the given package. - /// - /// The name or ID of the package to add. - /// Specifies whether or not the Package Manager requests the latest information about - /// the project's packages from the remote Unity package registry. When offlineMode is true, - /// the PackageInfo objects in the PackageCollection returned by the Package Manager contain information - /// obtained from the local package cache, which could be out of date. - /// Action which is executed if the request succeeded - /// Action which is executed if the request failed - /// - public static void CreateSearchRequest(string packageName, bool offlineMode, - Action> onSuccess, Action> onFail) - { - m_pendingSearchRequests.Enqueue(new SearchRequestInfo(packageName, offlineMode, onSuccess, onFail)); - } + //--------------------------------------------------------------------------------------------------------------------- + + /// + /// Queue a job to removes a previously added package from the project. + /// + /// The name or ID of the package to add. + /// Action which is executed if the request succeeded + /// Action which is executed if the request failed + /// + public static void CreateRemoveRequest(string packageName, Action onSuccess, Action onFail) + { + m_pendingRemoveRequests.Enqueue(new RemoveRequestInfo(packageName, onSuccess, onFail)); + } -//--------------------------------------------------------------------------------------------------------------------- + //--------------------------------------------------------------------------------------------------------------------- + + /// + /// Queue a job to searches the Unity package registry for the given package. + /// + /// The name or ID of the package to add. + /// Specifies whether or not the Package Manager requests the latest information about + /// the project's packages from the remote Unity package registry. When offlineMode is true, + /// the PackageInfo objects in the PackageCollection returned by the Package Manager contain information + /// obtained from the local package cache, which could be out of date. + /// Action which is executed if the request succeeded + /// Action which is executed if the request failed + /// + public static void CreateSearchRequest(string packageName, bool offlineMode, + Action> onSuccess, Action> onFail) + { + m_pendingSearchRequests.Enqueue(new SearchRequestInfo(packageName, offlineMode, onSuccess, onFail)); + } - /// - /// Queue a job to search the Unity package registry for all packages compatible with the current Unity version. - /// - /// Specifies whether or not the Package Manager requests the latest information about - /// the project's packages from the remote Unity package registry. When offlineMode is true, - /// the PackageInfo objects in the PackageCollection returned by the Package Manager contain information - /// obtained from the local package cache, which could be out of date. - /// Action which is executed if the request succeeded - /// Action which is executed if the request failed - /// - public static void CreateSearchAllRequest(bool offlineMode, - Action> onSuccess, Action> onFail) - { - m_pendingSearchAllRequests.Enqueue(new SearchAllRequestInfo(offlineMode, onSuccess, onFail)); - } - -//--------------------------------------------------------------------------------------------------------------------- + //--------------------------------------------------------------------------------------------------------------------- + + /// + /// Queue a job to search the Unity package registry for all packages compatible with the current Unity version. + /// + /// Specifies whether or not the Package Manager requests the latest information about + /// the project's packages from the remote Unity package registry. When offlineMode is true, + /// the PackageInfo objects in the PackageCollection returned by the Package Manager contain information + /// obtained from the local package cache, which could be out of date. + /// Action which is executed if the request succeeded + /// Action which is executed if the request failed + /// + public static void CreateSearchAllRequest(bool offlineMode, + Action> onSuccess, Action> onFail) + { + m_pendingSearchAllRequests.Enqueue(new SearchAllRequestInfo(offlineMode, onSuccess, onFail)); + } - static void UpdateRequestJobs() - { - { //Process pending list requests - var enumerator = m_pendingListRequests.GetEnumerator(); - while (enumerator.MoveNext()) { - ListRequestInfo info = enumerator.Current; - ListRequest listReq = Client.List(info.OfflineMode, info.IncludeIndirectIndependencies); - m_requestJobs.Add(new RequestJob(listReq,info.OnSuccessAction,info.OnFailAction)); + //--------------------------------------------------------------------------------------------------------------------- + + static void UpdateRequestJobs() + { + { //Process pending list requests + var enumerator = m_pendingListRequests.GetEnumerator(); + while (enumerator.MoveNext()) + { + ListRequestInfo info = enumerator.Current; + ListRequest listReq = Client.List(info.OfflineMode, info.IncludeIndirectIndependencies); + m_requestJobs.Add(new RequestJob(listReq, info.OnSuccessAction, info.OnFailAction)); + } + m_pendingListRequests.Clear(); } - m_pendingListRequests.Clear(); - } - { //Process pending addrequests - var enumerator = m_pendingAddRequests.GetEnumerator(); - while (enumerator.MoveNext()) { - AddRequestInfo info = enumerator.Current; - AddRequest addReq = Client.Add(info.PackageName); - m_requestJobs.Add(new RequestJob(addReq,info.OnSuccessAction,info.OnFailAction)); + { //Process pending addrequests + var enumerator = m_pendingAddRequests.GetEnumerator(); + while (enumerator.MoveNext()) + { + AddRequestInfo info = enumerator.Current; + AddRequest addReq = Client.Add(info.PackageName); + m_requestJobs.Add(new RequestJob(addReq, info.OnSuccessAction, info.OnFailAction)); + } + m_pendingAddRequests.Clear(); } - m_pendingAddRequests.Clear(); - } - { //Process pending RemoveRequests - var enumerator = m_pendingRemoveRequests.GetEnumerator(); - while (enumerator.MoveNext()) { - RemoveRequestInfo info = enumerator.Current; - RemoveRequest removeReq = Client.Remove(info.PackageName); - m_requestJobs.Add(new RequestJob(removeReq,info.OnSuccessAction,info.OnFailAction)); + { //Process pending RemoveRequests + var enumerator = m_pendingRemoveRequests.GetEnumerator(); + while (enumerator.MoveNext()) + { + RemoveRequestInfo info = enumerator.Current; + RemoveRequest removeReq = Client.Remove(info.PackageName); + m_requestJobs.Add(new RequestJob(removeReq, info.OnSuccessAction, info.OnFailAction)); + } + m_pendingRemoveRequests.Clear(); } - m_pendingRemoveRequests.Clear(); - } - { //Process pending SearchRequests - var enumerator = m_pendingSearchRequests.GetEnumerator(); - while (enumerator.MoveNext()) { - SearchRequestInfo info = enumerator.Current; - SearchRequest searchReq = Client.Search(info.PackageName, info.OfflineMode); - m_requestJobs.Add(new RequestJob(searchReq,info.OnSuccessAction,info.OnFailAction)); + { //Process pending SearchRequests + var enumerator = m_pendingSearchRequests.GetEnumerator(); + while (enumerator.MoveNext()) + { + SearchRequestInfo info = enumerator.Current; + SearchRequest searchReq = Client.Search(info.PackageName, info.OfflineMode); + m_requestJobs.Add(new RequestJob(searchReq, info.OnSuccessAction, info.OnFailAction)); + } + m_pendingSearchRequests.Clear(); } - m_pendingSearchRequests.Clear(); - } - { //Process pending SearchAllRequests - var enumerator = m_pendingSearchAllRequests.GetEnumerator(); - while (enumerator.MoveNext()) { - SearchAllRequestInfo info = enumerator.Current; - SearchRequest searchReq = Client.SearchAll(info.OfflineMode); - m_requestJobs.Add(new RequestJob(searchReq,info.OnSuccessAction,info.OnFailAction)); + { //Process pending SearchAllRequests + var enumerator = m_pendingSearchAllRequests.GetEnumerator(); + while (enumerator.MoveNext()) + { + SearchAllRequestInfo info = enumerator.Current; + SearchRequest searchReq = Client.SearchAll(info.OfflineMode); + m_requestJobs.Add(new RequestJob(searchReq, info.OnSuccessAction, info.OnFailAction)); + } + m_pendingSearchAllRequests.Clear(); } - m_pendingSearchAllRequests.Clear(); - } - { //Update and register completed jobs - var enumerator = m_requestJobs.GetEnumerator(); - while (enumerator.MoveNext()) { - StatusCode code = enumerator.Current.Update(); - if (StatusCode.Failure == code || StatusCode.Success == code) { - m_jobsToDelete.Add(enumerator.Current); + { //Update and register completed jobs + var enumerator = m_requestJobs.GetEnumerator(); + while (enumerator.MoveNext()) + { + StatusCode code = enumerator.Current.Update(); + if (StatusCode.Failure == code || StatusCode.Success == code) + { + m_jobsToDelete.Add(enumerator.Current); + } } } - } - - { //delete completed jobs - var enumerator = m_jobsToDelete.GetEnumerator(); - while (enumerator.MoveNext()) { - m_requestJobs.Remove(enumerator.Current); + + { //delete completed jobs + var enumerator = m_jobsToDelete.GetEnumerator(); + while (enumerator.MoveNext()) + { + m_requestJobs.Remove(enumerator.Current); + } + m_jobsToDelete.Clear(); } - m_jobsToDelete.Clear(); - } - } + } -//--------------------------------------------------------------------------------------------------------------------- + //--------------------------------------------------------------------------------------------------------------------- - static Queue m_pendingListRequests = new Queue(); - static Queue m_pendingAddRequests = new Queue(); - static Queue m_pendingRemoveRequests = new Queue(); - static Queue m_pendingSearchRequests = new Queue(); - static Queue m_pendingSearchAllRequests = new Queue(); + static Queue m_pendingListRequests = new Queue(); + static Queue m_pendingAddRequests = new Queue(); + static Queue m_pendingRemoveRequests = new Queue(); + static Queue m_pendingSearchRequests = new Queue(); + static Queue m_pendingSearchAllRequests = new Queue(); - static System.Collections.Generic.HashSet m_requestJobs = new HashSet(); - static System.Collections.Generic.List m_jobsToDelete = new List(); + static System.Collections.Generic.HashSet m_requestJobs = new HashSet(); + static System.Collections.Generic.List m_jobsToDelete = new List(); -} + } } //namespace Unity.RenderStreaming.Editor diff --git a/Editor/SignalingManagerEditor.cs b/Editor/SignalingManagerEditor.cs index 9133a03..702177d 100644 --- a/Editor/SignalingManagerEditor.cs +++ b/Editor/SignalingManagerEditor.cs @@ -19,23 +19,47 @@ internal class SignalingManagerEditor : UnityEditor.Editor const string DefaultSignalingSettingsLoadPath = "Packages/com.unity.renderstreaming/Runtime/SignalingSettings.asset"; + SerializedProperty m_UseDefault; + SerializedProperty m_SignalingSettingsObject; + SerializedProperty m_SignalingSettings; + SerializedProperty m_Handlers; + SerializedProperty m_RunOnAwake; + SerializedProperty m_EvaluateCommandlineArguments; + VisualElement root; Button openProjectSettingsButton; PopupField signalingSettingsPopupField; PropertyField signalingSettingsField; + private void OnEnable() + { + EditorApplication.projectChanged += OnProjectChanged; + + m_UseDefault = serializedObject.FindProperty(SignalingManager.UseDefaultPropertyName); + m_SignalingSettingsObject = serializedObject.FindProperty(SignalingManager.SignalingSettingsObjectPropertyName); + m_SignalingSettings = serializedObject.FindProperty(SignalingManager.SignalingSettingsPropertyName); + m_Handlers = serializedObject.FindProperty(SignalingManager.HandlersPropertyName); + m_RunOnAwake = serializedObject.FindProperty(SignalingManager.RunOnAwakePropertyName); + m_EvaluateCommandlineArguments = serializedObject.FindProperty(SignalingManager.EvaluateCommandlineArgumentsPropertyName); + } + + private void OnDisable() + { + EditorApplication.projectChanged -= OnProjectChanged; + } + public override VisualElement CreateInspectorGUI() { root = new VisualElement(); - bool useDefault = serializedObject.FindProperty("m_useDefault").boolValue; + bool useDefault = m_UseDefault.boolValue; - var useDefaultField = new PropertyField(serializedObject.FindProperty("m_useDefault"), "Use Default Settings in Project Settings"); + var useDefaultField = new PropertyField(m_UseDefault, "Use Default Settings in Project Settings"); useDefaultField.RegisterValueChangeCallback(OnChangeUseDefault); openProjectSettingsButton = new Button { text = "Open Project Setings" }; openProjectSettingsButton.clicked += OnClickedOpenProjectSettingsButton; - signalingSettingsPopupField = CreatePopUpSignalingType(serializedObject.FindProperty("signalingSettingsObject"), "Signaling Settings Asset"); + signalingSettingsPopupField = CreatePopUpSignalingType(m_SignalingSettingsObject, "Signaling Settings Asset"); signalingSettingsPopupField.RegisterValueChangedCallback(OnValueChangeSignalingSettingsObject); - signalingSettingsField = new PropertyField(serializedObject.FindProperty("signalingSettings"), "Signaling Settings"); + signalingSettingsField = new PropertyField(m_SignalingSettings, "Signaling Settings"); signalingSettingsField.RegisterValueChangeCallback(OnValueChangeSignalingSettings); root.Add(useDefaultField); @@ -51,11 +75,9 @@ public override VisualElement CreateInspectorGUI() { openProjectSettingsButton.style.display = DisplayStyle.None; } - root.Add(new ReorderableListField(serializedObject.FindProperty("handlers"), "Signaling Handler List")); - root.Add(new PropertyField(serializedObject.FindProperty("runOnAwake"), "Run On Awake")); - root.Add(new PropertyField(serializedObject.FindProperty("evaluateCommandlineArguments"), "Evaluate Commandline Arguments")); - - EditorApplication.projectChanged += OnProjectChanged; + root.Add(new ReorderableListField(m_Handlers, "Signaling Handler List")); + root.Add(new PropertyField(m_RunOnAwake, "Run On Awake")); + root.Add(new PropertyField(m_EvaluateCommandlineArguments, "Evaluate Commandline Arguments")); // Disable UI when running in Playmode EditorApplication.playModeStateChanged += OnPlayModeStateChanged; @@ -70,6 +92,7 @@ PopupField CreatePopUpSignalingType(SerializedProperty var paths = GetAvailableSignalingSettingsPath(); var field = new PopupField(label: label); + field.tooltip = "Choose the signaling settings."; field.formatSelectedValueCallback = v => AssetDatabase.GetAssetPath(v); field.formatListItemCallback = v => AssetDatabase.GetAssetPath(v); if (paths.Length == 0) @@ -104,7 +127,7 @@ void CreateDefaultSignalingSettings() { if (!AssetDatabase.CopyAsset(DefaultSignalingSettingsLoadPath, DefaultSignalingSettingsSavePath)) { - Debug.LogError("CopyAssets is failed."); + RenderStreaming.Logger.Log(LogType.Error, "CopyAssets is failed."); return; } asset = AssetDatabase.LoadAssetAtPath(DefaultSignalingSettingsSavePath); @@ -129,17 +152,19 @@ private void OnPlayModeStateChanged(PlayModeStateChange e) private void OnProjectChanged() { + if (root == null) + return; var paths = GetAvailableSignalingSettingsPath(); // Force to use default settings if there are no available settings in project folder. if (paths.Length == 0) { - serializedObject.FindProperty("m_useDefault").boolValue = true; + m_UseDefault.boolValue = true; serializedObject.ApplyModifiedProperties(); return; } - var asset = serializedObject.FindProperty("signalingSettingsObject").objectReferenceValue; + var asset = m_SignalingSettingsObject.objectReferenceValue; var availableObjects = paths.Select(path => AssetDatabase.LoadAssetAtPath(path)).ToArray(); var defaultIndex = ArrayHelpers.IndexOf(availableObjects, asset); if (defaultIndex < 0) @@ -151,7 +176,6 @@ private void OnProjectChanged() } signalingSettingsPopupField.choices = availableObjects.ToList(); signalingSettingsPopupField.index = defaultIndex; - } private void OnClickedOpenProjectSettingsButton() @@ -174,8 +198,7 @@ private void OnChangeUseDefault(SerializedPropertyChangeEvent e) signalingSettingsField.style.display = DisplayStyle.Flex; openProjectSettingsButton.style.display = DisplayStyle.None; - var property = serializedObject.FindProperty("signalingSettingsObject"); - if (!IsValidSignalingSettingsObject(property.objectReferenceValue as SignalingSettingsObject)) + if (!IsValidSignalingSettingsObject(m_SignalingSettingsObject.objectReferenceValue as SignalingSettingsObject)) { CreateDefaultSignalingSettings(); } @@ -187,16 +210,15 @@ private void OnValueChangeSignalingSettingsObject(ChangeEvent callback) { - GetPackageVersion("com.unity.renderstreaming", (version) => { + public static void DownloadCurrentVersionWebApp(string dstPath, System.Action callback) + { + GetPackageVersion("com.unity.renderstreaming", (version) => + { DownloadWebApp(version, dstPath, callback); }); } @@ -75,12 +77,16 @@ public static void DownloadWebApp(string version, string dstPath, System.Action< client.DownloadFileCompleted += (sender, e) => { EditorUtility.ClearProgressBar(); - if (e.Error != null) { + if (e.Error != null) + { //Try downloading using the latest known version to work. - if (version != LatestKnownVersion) { + if (version != LatestKnownVersion) + { DownloadWebApp(LatestKnownVersion, dstPath, callback); - } else { - Debug.LogError($"Failed downloading web server from:{url}. Error: {e.Error}"); + } + else + { + RenderStreaming.Logger.Log(LogType.Error, $"Failed downloading web server from:{url}. Error: {e.Error}"); } callback?.Invoke(false); return; @@ -88,7 +94,7 @@ public static void DownloadWebApp(string version, string dstPath, System.Action< if (!System.IO.File.Exists(tmpFilePath)) { - Debug.LogError($"Download failed. url:{url}"); + RenderStreaming.Logger.Log(LogType.Error, $"Download failed. url:{url}"); callback?.Invoke(false); return; } @@ -105,7 +111,7 @@ public static void DownloadWebApp(string version, string dstPath, System.Action< client.DownloadProgressChanged += (object sender, DownloadProgressChangedEventArgs e) => { var progress = e.ProgressPercentage / 100f; - if(EditorUtility.DisplayCancelableProgressBar("Downloading", url, progress)) + if (EditorUtility.DisplayCancelableProgressBar("Downloading", url, progress)) { client.CancelAsync(); } @@ -121,7 +127,7 @@ public static void GetPackageVersion(string packageName, System.Action c var packageInfo = req.FindPackage(packageName); if (null == packageInfo) { - Debug.LogError($"Not found package \"{packageName}\""); + RenderStreaming.Logger.Log(LogType.Error, $"Not found package \"{packageName}\""); return; } callback(packageInfo.version); diff --git a/Runtime/Scripts/AudioCodecInfo.cs b/Runtime/Scripts/AudioCodecInfo.cs index 613084d..680b249 100644 --- a/Runtime/Scripts/AudioCodecInfo.cs +++ b/Runtime/Scripts/AudioCodecInfo.cs @@ -1,6 +1,6 @@ using System; -using UnityEngine; using Unity.WebRTC; +using UnityEngine; namespace Unity.RenderStreaming { diff --git a/Runtime/Scripts/AudioStreamReceiver.cs b/Runtime/Scripts/AudioStreamReceiver.cs index 4a6a366..be0f45c 100644 --- a/Runtime/Scripts/AudioStreamReceiver.cs +++ b/Runtime/Scripts/AudioStreamReceiver.cs @@ -1,6 +1,6 @@ using System; -using System.Linq; using System.Collections.Generic; +using System.Linq; using Unity.WebRTC; using UnityEngine; @@ -12,6 +12,9 @@ namespace Unity.RenderStreaming [AddComponentMenu("Render Streaming/Audio Stream Receiver")] public class AudioStreamReceiver : StreamReceiverBase { + internal const string CodecPropertyName = nameof(m_Codec); + internal const string TargetAudioSourcePropertyName = nameof(m_TargetAudioSource); + /// /// /// diff --git a/Runtime/Scripts/AudioStreamSender.cs b/Runtime/Scripts/AudioStreamSender.cs index f5c843e..918fef4 100644 --- a/Runtime/Scripts/AudioStreamSender.cs +++ b/Runtime/Scripts/AudioStreamSender.cs @@ -1,7 +1,7 @@ using System; -using System.Linq; using System.Collections; using System.Collections.Generic; +using System.Linq; using Unity.Collections; using Unity.WebRTC; using UnityEngine; @@ -40,6 +40,15 @@ public class AudioStreamSender : StreamSenderBase static readonly uint s_defaultMinBitrate = 0; static readonly uint s_defaultMaxBitrate = 200; + internal const string SourcePropertyName = nameof(m_Source); + internal const string AudioSourcePropertyName = nameof(m_AudioSource); + internal const string AudioListenerPropertyName = nameof(m_AudioListener); + internal const string MicrophoneDeviceIndexPropertyName = nameof(m_MicrophoneDeviceIndex); + internal const string AutoRequestUserAuthorizationPropertyName = nameof(m_AutoRequestUserAuthorization); + internal const string CodecPropertyName = nameof(m_Codec); + internal const string BitratePropertyName = nameof(m_Bitrate); + internal const string LoopbackPropertyName = nameof(m_Loopback); + [SerializeField] private AudioStreamSource m_Source; @@ -61,6 +70,9 @@ public class AudioStreamSender : StreamSenderBase [SerializeField, Bitrate(0, 1000)] private Range m_Bitrate = new Range(s_defaultMinBitrate, s_defaultMaxBitrate); + [SerializeField] + private bool m_Loopback = false; + private int m_sampleRate = 0; private AudioStreamSourceImpl m_sourceImpl = null; @@ -112,7 +124,32 @@ public uint maxBitrate } /// - /// The index of WebCamTexture.devices. + /// Play or not sending to remote audio in local. + /// + public bool loopback + { + get + { + return m_Loopback; + } + set + { + if (m_Loopback == value) + { + return; + } + + m_Loopback = value; + + if (Track is AudioStreamTrack audioTrack) + { + audioTrack.Loopback = value; + } + } + } + + /// + /// The index of Microphone.devices. /// public int sourceDeviceIndex { @@ -197,7 +234,7 @@ public void SetBitrate(uint minBitrate, uint maxBitrate) { RTCError error = transceiver.Sender.SetBitrate(m_Bitrate.min, m_Bitrate.max); if (error.errorType != RTCErrorType.None) - Debug.LogError(error.message); + RenderStreaming.Logger.Log(LogType.Error, error.message); } } @@ -294,13 +331,13 @@ private protected override void OnDisable() base.OnDisable(); } - public void SetData(ref NativeArray nativeArray, int channels) + public void SetData(NativeArray.ReadOnly nativeArray, int channels) { if (m_Source != AudioStreamSource.APIOnly) throw new InvalidOperationException("To use this method, please set AudioStreamSource.APIOnly to source property"); if (!isPlaying) return; - (m_sourceImpl as AudioStreamSourceAPIOnly)?.SetData(ref nativeArray, channels, m_sampleRate); + (m_sourceImpl as AudioStreamSourceAPIOnly)?.SetData(nativeArray, channels, m_sampleRate); } abstract class AudioStreamSourceImpl : IDisposable @@ -466,13 +503,13 @@ public override WaitForCreateTrack CreateTrack() { var instruction = new WaitForCreateTrack(); m_audioTrack = new AudioStreamTrack(); - instruction.Done(new AudioStreamTrack()); + instruction.Done(m_audioTrack); return instruction; } - public void SetData(ref NativeArray nativeArray, int channels, int sampleRate) + public void SetData(NativeArray.ReadOnly nativeArray, int channels, int sampleRate) { - m_audioTrack?.SetData(ref nativeArray, channels, sampleRate); + m_audioTrack?.SetData(nativeArray, channels, sampleRate); } public override void Dispose() diff --git a/Runtime/Scripts/AutomaticStreaming.cs b/Runtime/Scripts/AutomaticStreaming.cs index 06b4a37..839295f 100644 --- a/Runtime/Scripts/AutomaticStreaming.cs +++ b/Runtime/Scripts/AutomaticStreaming.cs @@ -85,7 +85,8 @@ private void OnAudioFilterRead(float[] data, int channels) } var nativeArray = new NativeArray(data, Allocator.Temp); - sender.SetData(ref nativeArray, channels); + sender.SetData(nativeArray.AsReadOnly(), channels); + nativeArray.Dispose(); } private void OnDestroy() diff --git a/Runtime/Scripts/Broadcast.cs b/Runtime/Scripts/Broadcast.cs index 89ad900..01741ac 100644 --- a/Runtime/Scripts/Broadcast.cs +++ b/Runtime/Scripts/Broadcast.cs @@ -48,7 +48,7 @@ private void Disconnect(string connectionId) { RemoveReceiver(connectionId, receiver); } - foreach (var channel in streams.OfType()) + foreach (var channel in streams.OfType().Where(c => c.ConnectionId == connectionId)) { RemoveChannel(connectionId, channel); } @@ -65,7 +65,7 @@ public void OnOffer(SignalingEventData data) { if (connectionIds.Contains(data.connectionId)) { - Debug.Log($"Already answered this connectionId : {data.connectionId}"); + RenderStreaming.Logger.Log($"Already answered this connectionId : {data.connectionId}"); return; } connectionIds.Add(data.connectionId); diff --git a/Runtime/Scripts/DataChannelBase.cs b/Runtime/Scripts/DataChannelBase.cs index 6d71777..ff73391 100644 --- a/Runtime/Scripts/DataChannelBase.cs +++ b/Runtime/Scripts/DataChannelBase.cs @@ -1,56 +1,65 @@ +using System; using Unity.WebRTC; using UnityEngine; namespace Unity.RenderStreaming { /// - /// + /// /// public abstract class DataChannelBase : MonoBehaviour, IDataChannel { + internal const string LocalPropertyName = nameof(local); + internal const string LabelPropertyName = nameof(label); + /// - /// + /// /// [SerializeField] protected bool local = false; /// - /// + /// /// [SerializeField] protected string label; /// - /// + /// /// public bool IsLocal => local; /// - /// + /// /// public string Label => label; /// - /// + /// /// public bool IsConnected => Channel != null && Channel.ReadyState == RTCDataChannelState.Open; /// - /// + /// + /// + public string ConnectionId { get; protected set; } + + /// + /// /// public RTCDataChannel Channel { get; protected set; } /// - /// + /// /// public OnStartedChannelHandler OnStartedChannel { get; set; } /// - /// + /// /// public OnStoppedChannelHandler OnStoppedChannel { get; set; } /// - /// + /// /// /// /// @@ -59,10 +68,12 @@ public virtual void SetChannel(string connectionId, RTCDataChannel channel) Channel = channel; if (Channel == null) { + ConnectionId = String.Empty; OnStoppedChannel?.Invoke(connectionId); return; } + ConnectionId = connectionId; label = Channel.Label; Channel.OnOpen += () => { OnOpen(connectionId); }; Channel.OnClose += () => { OnClose(connectionId); }; @@ -75,7 +86,7 @@ public virtual void SetChannel(string connectionId, RTCDataChannel channel) } /// - /// + /// /// /// public virtual void Send(byte[] msg) @@ -84,7 +95,7 @@ public virtual void Send(byte[] msg) } /// - /// + /// /// /// public virtual void Send(string msg) @@ -93,7 +104,7 @@ public virtual void Send(string msg) } /// - /// + /// /// /// public virtual void SetChannel(SignalingEventData data) @@ -102,7 +113,7 @@ public virtual void SetChannel(SignalingEventData data) } /// - /// + /// /// /// protected virtual void OnMessage(byte[] bytes) @@ -110,7 +121,7 @@ protected virtual void OnMessage(byte[] bytes) } /// - /// + /// /// /// protected virtual void OnOpen(string connectionId) @@ -118,7 +129,7 @@ protected virtual void OnOpen(string connectionId) OnStartedChannel?.Invoke(connectionId); } /// - /// + /// /// /// protected virtual void OnClose(string connectionId) diff --git a/Runtime/Scripts/DateTimeExtension.cs b/Runtime/Scripts/DateTimeExtension.cs index 69badcd..4be14c7 100644 --- a/Runtime/Scripts/DateTimeExtension.cs +++ b/Runtime/Scripts/DateTimeExtension.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Unity.RenderStreaming { @@ -16,12 +16,5 @@ public static long ToJsMilliseconds(this DateTime dt) { return (long)((dt.ToUniversalTime().Ticks - DatetimeMinTimeTicks) / 10000); } - - public static DateTime ParseHttpDate(string text) - { - return DateTime.ParseExact(text, - "ddd, dd MMM yyyy HH:mm:ss Z", - System.Globalization.CultureInfo.InvariantCulture); - } } } diff --git a/Runtime/Scripts/InputReceiver.cs b/Runtime/Scripts/InputReceiver.cs index 519724d..ea48ee5 100644 --- a/Runtime/Scripts/InputReceiver.cs +++ b/Runtime/Scripts/InputReceiver.cs @@ -1,14 +1,13 @@ using System; using System.Linq; +using Unity.RenderStreaming.InputSystem; using Unity.WebRTC; using UnityEngine; using UnityEngine.InputSystem; -using UnityEngine.InputSystem.Utilities; using UnityEngine.InputSystem.Users; -using Unity.RenderStreaming.InputSystem; - -using Inputs = UnityEngine.InputSystem.InputSystem; +using UnityEngine.InputSystem.Utilities; using InputRemoting = Unity.RenderStreaming.InputSystem.InputRemoting; +using Inputs = UnityEngine.InputSystem.InputSystem; namespace Unity.RenderStreaming { @@ -20,13 +19,17 @@ namespace Unity.RenderStreaming [AddComponentMenu("Render Streaming/Input Receiver")] public class InputReceiver : InputChannelReceiverBase { + internal const string ActionsPropertyName = nameof(m_Actions); + internal const string ActionEventsPropertyName = nameof(m_ActionEvents); + internal const string DefaultActionMapPropertyName = nameof(m_DefaultActionMap); + /// /// /// public override event Action onDeviceChange; /// - /// + /// /// public InputActionAsset actions { @@ -63,22 +66,22 @@ public InputActionAsset actions } /// - /// + /// /// public bool inputIsActive => m_InputActive; /// - /// + /// /// public InputUser user => m_InputUser; /// - /// + /// /// public ReadOnlyArray devices => m_InputUser.pairedDevices; /// - /// + /// /// public InputActionMap currentActionMap { @@ -92,7 +95,7 @@ public InputActionMap currentActionMap } /// - /// + /// /// public string defaultActionMap { @@ -101,7 +104,7 @@ public string defaultActionMap } /// - /// + /// /// public ReadOnlyArray actionEvents { @@ -119,7 +122,7 @@ public ReadOnlyArray actionEvents } /// - /// + /// /// protected virtual void OnEnable() { @@ -133,7 +136,7 @@ protected virtual void OnEnable() } /// - /// + /// /// protected virtual void OnDisable() { @@ -146,7 +149,7 @@ protected virtual void OnDisable() } /// - /// + /// /// public void ActivateInput() { @@ -161,7 +164,7 @@ public void ActivateInput() } /// - /// + /// /// public void DeactivateInput() { @@ -171,7 +174,7 @@ public void DeactivateInput() } /// - /// + /// /// /// public void SwitchCurrentActionMap(string mapNameOrId) @@ -179,14 +182,14 @@ public void SwitchCurrentActionMap(string mapNameOrId) // Must be enabled. if (!m_Enabled) { - Debug.LogError($"Cannot switch to actions '{mapNameOrId}'; input is not enabled", this); + RenderStreaming.Logger.Log(LogType.Error, (object)$"Cannot switch to actions '{mapNameOrId}'; input is not enabled", this); return; } // Must have actions. if (m_Actions == null) { - Debug.LogError($"Cannot switch to actions '{mapNameOrId}'; no actions set on PlayerInput", this); + RenderStreaming.Logger.Log(LogType.Error, (object)$"Cannot switch to actions '{mapNameOrId}'; no actions set on PlayerInput", this); return; } @@ -194,7 +197,7 @@ public void SwitchCurrentActionMap(string mapNameOrId) var actionMap = m_Actions.FindActionMap(mapNameOrId); if (actionMap == null) { - Debug.LogError($"Cannot find action map '{mapNameOrId}' in actions '{m_Actions}'", this); + RenderStreaming.Logger.Log(LogType.Error, (object)$"Cannot find action map '{mapNameOrId}' in actions '{m_Actions}'", this); return; } @@ -202,7 +205,7 @@ public void SwitchCurrentActionMap(string mapNameOrId) } /// - /// + /// /// /// public void PerformPairingWithDevice(InputDevice device) @@ -211,7 +214,7 @@ public void PerformPairingWithDevice(InputDevice device) } /// - /// + /// /// public void PerformPairingWithAllLocalDevices() { @@ -222,7 +225,7 @@ public void PerformPairingWithAllLocalDevices() } /// - /// + /// /// /// public void UnpairDevices(InputDevice device) @@ -254,7 +257,7 @@ public override void SetChannel(string connectionId, RTCDataChannel channel) } /// - /// + /// /// /// Texture Size. /// Region of the texture in world coordinate system. @@ -264,7 +267,7 @@ public void CalculateInputRegion(Vector2Int size, Rect region) } /// - /// + /// /// /// public void SetEnableInputPositionCorrection(bool enabled) @@ -274,7 +277,7 @@ public void SetEnableInputPositionCorrection(bool enabled) /// - /// + /// /// protected virtual void OnDestroy() { @@ -371,15 +374,15 @@ private void InitializeActions() if (!string.IsNullOrEmpty(actionEvent.actionName)) { // We have an action name. Show in message. - Debug.LogError( - $"Cannot find action '{actionEvent.actionName}' with ID '{actionEvent.actionId}' in '{m_Actions}", + RenderStreaming.Logger.Log(LogType.Error, + (object)$"Cannot find action '{actionEvent.actionName}' with ID '{actionEvent.actionId}' in '{m_Actions}", this); } else { // We have no action name. Best we have is ID. - Debug.LogError( - $"Cannot find action with ID '{actionEvent.actionId}' in '{m_Actions}", + RenderStreaming.Logger.Log(LogType.Error, + (object)$"Cannot find action with ID '{actionEvent.actionId}' in '{m_Actions}", this); } } diff --git a/Runtime/Scripts/InputSender.cs b/Runtime/Scripts/InputSender.cs index 845a29d..c2cdf6e 100644 --- a/Runtime/Scripts/InputSender.cs +++ b/Runtime/Scripts/InputSender.cs @@ -1,7 +1,7 @@ using System; +using Unity.RenderStreaming.InputSystem; using Unity.WebRTC; using UnityEngine; -using Unity.RenderStreaming.InputSystem; namespace Unity.RenderStreaming { diff --git a/Runtime/Scripts/InputSystem/EmulateInputFieldEvent.cs b/Runtime/Scripts/InputSystem/EmulateInputFieldEvent.cs new file mode 100644 index 0000000..c4c6b42 --- /dev/null +++ b/Runtime/Scripts/InputSystem/EmulateInputFieldEvent.cs @@ -0,0 +1,251 @@ +using System.Collections.Generic; +#if URS_USE_TEXTMESHPRO +using TMPro; +#endif +using UnityEngine; +using UnityEngine.UI; +using UnityEngine.InputSystem; +using UnityEngine.InputSystem.Controls; +using UnityEngine.InputSystem.LowLevel; + +namespace Unity.RenderStreaming.InputSystem +{ + /// + /// This partial class is for workaround to support Unity UI InputField. + /// + partial class Receiver + { + private static readonly Dictionary s_KeyMap = new Dictionary() + { + { (int)Key.Backspace, KeyCode.Backspace }, + { (int)Key.Tab, KeyCode.Tab }, + { (int)Key.Enter, KeyCode.Return }, + { (int)Key.Space, KeyCode.Space }, + { (int)Key.Comma, KeyCode.Comma }, + { (int)Key.Minus, KeyCode.Minus }, + { (int)Key.Period, KeyCode.Period }, + { (int)Key.Slash, KeyCode.Slash }, + { (int)Key.Digit0, KeyCode.Alpha0 }, + { (int)Key.Digit1, KeyCode.Alpha1 }, + { (int)Key.Digit2, KeyCode.Alpha2 }, + { (int)Key.Digit3, KeyCode.Alpha3 }, + { (int)Key.Digit4, KeyCode.Alpha4 }, + { (int)Key.Digit5, KeyCode.Alpha5 }, + { (int)Key.Digit6, KeyCode.Alpha6 }, + { (int)Key.Digit7, KeyCode.Alpha7 }, + { (int)Key.Digit8, KeyCode.Alpha8 }, + { (int)Key.Digit9, KeyCode.Alpha9 }, + { (int)Key.Semicolon, KeyCode.Semicolon }, + { (int)Key.Equals, KeyCode.Equals }, + { (int)Key.LeftBracket, KeyCode.LeftBracket }, + { (int)Key.Backslash, KeyCode.Backslash }, + { (int)Key.RightBracket, KeyCode.RightBracket }, + { (int)Key.Backquote, KeyCode.BackQuote }, + { (int)Key.Quote, KeyCode.Quote }, + { (int)Key.A, KeyCode.A }, + { (int)Key.B, KeyCode.B }, + { (int)Key.C, KeyCode.C }, + { (int)Key.D, KeyCode.D }, + { (int)Key.E, KeyCode.E }, + { (int)Key.F, KeyCode.F }, + { (int)Key.G, KeyCode.G }, + { (int)Key.H, KeyCode.H }, + { (int)Key.I, KeyCode.I }, + { (int)Key.J, KeyCode.J }, + { (int)Key.K, KeyCode.K }, + { (int)Key.L, KeyCode.L }, + { (int)Key.M, KeyCode.M }, + { (int)Key.N, KeyCode.N }, + { (int)Key.O, KeyCode.O }, + { (int)Key.P, KeyCode.P }, + { (int)Key.Q, KeyCode.Q }, + { (int)Key.R, KeyCode.R }, + { (int)Key.S, KeyCode.S }, + { (int)Key.T, KeyCode.T }, + { (int)Key.U, KeyCode.U }, + { (int)Key.V, KeyCode.V }, + { (int)Key.W, KeyCode.W }, + { (int)Key.X, KeyCode.X }, + { (int)Key.Y, KeyCode.Y }, + { (int)Key.Z, KeyCode.Z }, + { (int)Key.F1, KeyCode.F1 }, + { (int)Key.F2, KeyCode.F2 }, + { (int)Key.F3, KeyCode.F3 }, + { (int)Key.F4, KeyCode.F4 }, + { (int)Key.F5, KeyCode.F5 }, + { (int)Key.F6, KeyCode.F6 }, + { (int)Key.F7, KeyCode.F7 }, + { (int)Key.F8, KeyCode.F8 }, + { (int)Key.F9, KeyCode.F9 }, + { (int)Key.F10, KeyCode.F10 }, + { (int)Key.F11, KeyCode.F11 }, + { (int)Key.F12, KeyCode.F12 }, + { (int)Key.None, KeyCode.None }, + { (int)Key.LeftArrow, KeyCode.LeftArrow }, + { (int)Key.RightArrow, KeyCode.RightArrow }, + { (int)Key.UpArrow, KeyCode.UpArrow }, + { (int)Key.DownArrow, KeyCode.DownArrow }, + { (int)Key.LeftShift, KeyCode.LeftShift }, + { (int)Key.RightShift, KeyCode.RightShift }, + { (int)Key.Delete, KeyCode.Delete }, + { (int)Key.Escape, KeyCode.Escape }, + { (int)Key.LeftAlt, KeyCode.LeftAlt }, + { (int)Key.RightAlt, KeyCode.RightAlt }, + { (int)Key.LeftApple, KeyCode.LeftApple }, + { (int)Key.RightApple, KeyCode.RightApple } + }; + + interface IInputField + { + void ProcessEvent(Event e); + void ForceLabelUpdate(); + void AppendText(char character); + } + + class UGUIInputField : IInputField + { + InputField m_field; + public UGUIInputField(InputField field) + { + m_field = field; + } + + public void ProcessEvent(Event e) + { + m_field.ProcessEvent(e); + } + public void ForceLabelUpdate() + { + m_field.ForceLabelUpdate(); + } + public void AppendText(char character) + { + m_field.text += character; + } + } + +#if URS_USE_TEXTMESHPRO + class TMProInputField : IInputField + { + TMP_InputField m_field; + public TMProInputField(TMP_InputField field) + { + m_field = field; + } + + public void ProcessEvent(Event e) + { + m_field.ProcessEvent(e); + } + public void ForceLabelUpdate() + { + m_field.ForceLabelUpdate(); + } + public void AppendText(char character) + { + m_field.text += character; + } + } +#endif + + IInputField FindInputField(GameObject obj) + { + var field = obj.GetComponent(); + if (field != null) + return new UGUIInputField(field); + +#if URS_USE_TEXTMESHPRO + var tmpField = obj.GetComponent(); + if (tmpField != null) + return new TMProInputField(tmpField); +#endif + return null; + } + + (EventModifiers, KeyCode) GetEventModifiersAndKeyCode(InputEventPtr ptr) + { + EventModifiers modifiers = EventModifiers.None; + KeyCode keyCode = KeyCode.None; + foreach (var control in ptr.GetAllButtonPresses()) + { + if (control is KeyControl keyControl) + { + var key = keyControl.keyCode; + if (key == Key.LeftShift || key == Key.RightShift) + { + modifiers |= EventModifiers.Shift; + } + else if (key == Key.LeftCtrl || key == Key.RightCtrl) + { + modifiers |= EventModifiers.Control; + } + else if (key == Key.LeftAlt || key == Key.RightAlt) + { + modifiers |= EventModifiers.Alt; + } + else if (key == Key.LeftCommand || key == Key.RightCommand) + { + modifiers |= EventModifiers.Command; + } + else if (key == Key.CapsLock) + { + modifiers |= EventModifiers.CapsLock; + } + else if (s_KeyMap.TryGetValue((int)key, out var value)) + { + keyCode = value; + } + } + } + return (modifiers, keyCode); + } + + unsafe Event CreateEvent(InputEventPtr ptr) + { + var (modifiers, keyCode) = GetEventModifiersAndKeyCode(ptr); + + if (ptr.type == TextEvent.Type) + { + var textEventPtr = (TextEvent*)ptr.ToPointer(); + var utf32Char = textEventPtr->character; + if (utf32Char >= 0x10000) + { + // todo: not supported multibyte character. + return null; + } + + return new Event + { + type = EventType.KeyDown, + character = (char)utf32Char, + keyCode = keyCode, + modifiers = modifiers + }; + } + + return new Event + { + type = EventType.KeyDown, + keyCode = keyCode, + modifiers = modifiers + }; + } + + private void EmulateInputFieldEvent(InputEventPtr ptr) + { + var obj = UnityEngine.EventSystems.EventSystem.current?.currentSelectedGameObject; + if (obj == null) + return; + + var field = FindInputField(obj); + if (field == null) + return; + Event e = CreateEvent(ptr); + if (e != null) + { + field.ProcessEvent(e); + field.ForceLabelUpdate(); + } + } + } +} diff --git a/Runtime/Scripts/InputSystem/EmulateInputFieldEvent.cs.meta b/Runtime/Scripts/InputSystem/EmulateInputFieldEvent.cs.meta new file mode 100644 index 0000000..2e7b809 --- /dev/null +++ b/Runtime/Scripts/InputSystem/EmulateInputFieldEvent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a3d7722788597434b979a31af26668c1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/InputSystem/InputDeviceExtension.cs b/Runtime/Scripts/InputSystem/InputDeviceExtension.cs index 71e8892..ebec4bc 100644 --- a/Runtime/Scripts/InputSystem/InputDeviceExtension.cs +++ b/Runtime/Scripts/InputSystem/InputDeviceExtension.cs @@ -1,7 +1,7 @@ using System; using System.Reflection; -using UnityEngine.InputSystem.Layouts; using UnityEngine.InputSystem; +using UnityEngine.InputSystem.Layouts; namespace Unity.RenderStreaming.InputSystem { diff --git a/Runtime/Scripts/InputSystem/InputManager.cs b/Runtime/Scripts/InputSystem/InputManager.cs index b466217..33867c3 100644 --- a/Runtime/Scripts/InputSystem/InputManager.cs +++ b/Runtime/Scripts/InputSystem/InputManager.cs @@ -158,7 +158,7 @@ public virtual InputControlLayout LoadLayout(string name) public virtual void RegisterControlLayout(string json, string name = null, bool isOverride = false) { - if(isOverride) + if (isOverride) InputSystem.RegisterLayoutOverride(json, name); else InputSystem.RegisterLayout(json, name); diff --git a/Runtime/Scripts/InputSystem/InputRemoting.cs b/Runtime/Scripts/InputSystem/InputRemoting.cs index da06f6c..7364cad 100644 --- a/Runtime/Scripts/InputSystem/InputRemoting.cs +++ b/Runtime/Scripts/InputSystem/InputRemoting.cs @@ -6,10 +6,10 @@ using System.Text; using Unity.Collections.LowLevel.Unsafe; using UnityEngine; +using UnityEngine.InputSystem; using UnityEngine.InputSystem.Layouts; using UnityEngine.InputSystem.LowLevel; using UnityEngine.InputSystem.Utilities; -using UnityEngine.InputSystem; ////TODO: show remote device IDs in the debugger @@ -97,7 +97,7 @@ private set static InputRemoting() { #if UNITY_EDITOR - // + // // note: This lines are for avoiding issues when running the editor // on background. When moved the focus from the editor, input events // from another process are ignored. @@ -232,7 +232,7 @@ private void SendAllGeneratedLayouts() { // todo(kazuki):: // layputBuilders property is not published from InputSystem - // + // //foreach (var entry in m_LocalManager.m_Layouts.layoutBuilders) // SendLayout(entry.Key); @@ -513,7 +513,7 @@ public struct Data layout = sender.m_LocalManager.LoadLayout(new InternedString(layoutName)); if (layout == null) { - Debug.Log(string.Format( + RenderStreaming.Logger.Log(string.Format( "Could not find layout '{0}' meant to be sent through remote connection; this should not happen", layoutName)); return null; @@ -521,7 +521,7 @@ public struct Data } catch (Exception exception) { - Debug.Log(string.Format( + RenderStreaming.Logger.Log(string.Format( "Could not load layout '{0}'; not sending to remote listeners (exception: {1})", layoutName, exception)); return null; @@ -620,7 +620,7 @@ public static void Process(InputRemoting Receiver, Message msg) foreach (var entry in devices) if (entry.remoteId == data.deviceId) { - Debug.LogError(string.Format( + RenderStreaming.Logger.Log(LogType.Error, string.Format( "Already received device with id {0} (layout '{1}', description '{3}) from remote {2}", data.deviceId, data.layout, msg.participantId, data.description)); @@ -637,7 +637,7 @@ public static void Process(InputRemoting Receiver, Message msg) } catch (Exception exception) { - Debug.LogError( + RenderStreaming.Logger.Log(LogType.Error, $"Could not create remote device '{data.description}' with layout '{data.layout}' locally (exception: {exception})"); return; } @@ -653,7 +653,7 @@ public static void Process(InputRemoting Receiver, Message msg) var deviceFlagsRemote = 1 << 3; device.SetDeviceFlags(device.GetDeviceFlags() | deviceFlagsRemote); - if(data.usages != null) + if (data.usages != null) foreach (var usage in data.usages) Receiver.m_LocalManager.AddDeviceUsage(device, usage); diff --git a/Runtime/Scripts/InputSystem/Receiver.cs b/Runtime/Scripts/InputSystem/Receiver.cs index 9bd8a31..01b8edc 100644 --- a/Runtime/Scripts/InputSystem/Receiver.cs +++ b/Runtime/Scripts/InputSystem/Receiver.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using Unity.WebRTC; -using Unity.Collections.LowLevel.Unsafe; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.InputSystem.LowLevel; @@ -15,7 +14,7 @@ namespace Unity.RenderStreaming.InputSystem /// /// /// - class Receiver : InputManager, IDisposable + partial class Receiver : InputManager, IDisposable { public override event Action onMessage; public new event Action onDeviceChange; @@ -28,6 +27,7 @@ class Receiver : InputManager, IDisposable private readonly List _registeredRemoteLayout = new List(); private InputPositionCorrector _corrector; private Action _onEvent; + /// /// /// @@ -160,7 +160,7 @@ public override void RegisterControlLayout(string json, string name = null, bool public override void RemoveLayout(string name) { - if(_registeredRemoteLayout.Contains(name)) + if (_registeredRemoteLayout.Contains(name)) { base.RemoveLayout(name); _registeredRemoteLayout.Remove(name); @@ -182,6 +182,18 @@ public override void QueueEvent(InputEventPtr ptr) { base.QueueEvent(ptr); } + + // workaround: + // UnityEngine.UI.InputField and TMP_InputField depends on Event.PopEvent. + // Event.PopEvent is old event API, therefore EventSystem.QueueEvent doesn't queue events. + var eventType = ptr.type; + if (device is Keyboard && + (eventType == StateEvent.Type || + eventType == DeltaStateEvent.Type || + eventType == TextEvent.Type)) + { + EmulateInputFieldEvent(ptr); + } } /// diff --git a/Runtime/Scripts/InputSystem/Sender.cs b/Runtime/Scripts/InputSystem/Sender.cs index 8c6a323..1aabb02 100644 --- a/Runtime/Scripts/InputSystem/Sender.cs +++ b/Runtime/Scripts/InputSystem/Sender.cs @@ -41,29 +41,6 @@ public void Dispose() InputSystem.onLayoutChange -= OnLayoutChange; } - /// - /// - /// - public override ReadOnlyArray devices - { - get - { - return InputSystem.devices; - } - } - - /// - /// - /// - public override IEnumerable layouts - { - get - { - // todo(kazuki):: filter layout - return InputSystem.ListLayouts(); - } - } - /// /// /// diff --git a/Runtime/Scripts/PeerConnection.cs b/Runtime/Scripts/PeerConnection.cs index eb0b4cf..a5291ca 100644 --- a/Runtime/Scripts/PeerConnection.cs +++ b/Runtime/Scripts/PeerConnection.cs @@ -165,7 +165,7 @@ public void SendOffer() { if (_processingSetDescription) { - Debug.LogWarning($"{this} already processing other set description"); + RenderStreaming.Logger.Log(LogType.Warning, $"{this} already processing other set description"); return; } @@ -203,7 +203,7 @@ private IEnumerator SendOfferCoroutine() if (opLocalDesc.IsError) { - Debug.LogError($"{this} {opLocalDesc.Error.message}"); + RenderStreaming.Logger.Log(LogType.Error, $"{this} {opLocalDesc.Error.message}"); _processingSetDescription = false; yield break; } @@ -224,7 +224,7 @@ public void SendAnswer() { if (_processingSetDescription) { - Debug.LogWarning($"{this} already processing other set description"); + RenderStreaming.Logger.Log(LogType.Warning, $"{this} already processing other set description"); return; } @@ -243,7 +243,7 @@ private IEnumerator SendAnswerCoroutine() if (opLocalDesc.IsError) { - Debug.LogError($"{this} {opLocalDesc.Error.message}"); + RenderStreaming.Logger.Log(LogType.Error, $"{this} {opLocalDesc.Error.message}"); _processingSetDescription = false; yield break; } @@ -264,7 +264,7 @@ public IEnumerator OnGotDescription(RTCSessionDescription description, Action on if (_ignoreOffer) { - Debug.LogWarning($"{this} glare - ignoreOffer."); + RenderStreaming.Logger.Log(LogType.Warning, $"{this} glare - ignoreOffer."); yield break; } @@ -276,7 +276,7 @@ public IEnumerator OnGotDescription(RTCSessionDescription description, Action on yield return remoteDescOp; if (remoteDescOp.IsError) { - Debug.LogError($"{this} {remoteDescOp.Error.message}"); + RenderStreaming.Logger.Log(LogType.Error, $"{this} {remoteDescOp.Error.message}"); _srdAnswerPending = false; _processingSetDescription = false; yield break; @@ -292,7 +292,7 @@ public bool OnGotIceCandidate(RTCIceCandidate candidate) if (!_peer.AddIceCandidate(candidate)) { if (!_ignoreOffer) - Debug.LogWarning($"{this} this candidate can't accept on state."); + RenderStreaming.Logger.Log(LogType.Warning, $"{this} this candidate can't accept on state."); return false; } diff --git a/Runtime/Scripts/RenderStreaming.cs b/Runtime/Scripts/RenderStreaming.cs index e751f63..b6c2bd7 100644 --- a/Runtime/Scripts/RenderStreaming.cs +++ b/Runtime/Scripts/RenderStreaming.cs @@ -20,6 +20,7 @@ public static class RenderStreaming private static RenderStreamingSettings s_settings; private static GameObject s_automaticStreamingObject; + private static ILogger s_logger; private static bool m_running; @@ -44,7 +45,7 @@ internal static RenderStreamingSettings Settings if (m_running && s_settings.signalingSettings != value.signalingSettings) { - Debug.LogWarning("Signaling settings doesn't change on already started signaling instance."); + RenderStreaming.Logger.Log(LogType.Warning, "Signaling settings doesn't change on already started signaling instance."); } s_settings = value; @@ -72,6 +73,28 @@ public static T GetSignalingSettings() where T : SignalingSettings return s_settings.signalingSettings as T; } + /// + /// Get & set the logger to use when logging debug messages inside the RenderStreaming package. + /// By default will use Debug.unityLogger. + /// + /// Throws if setting a null logger. + public static ILogger Logger + { + get + { + if (s_logger == null) + { + return Debug.unityLogger; + } + + return s_logger; + } + set + { + s_logger = value ?? throw new ArgumentNullException(nameof(value)); + } + } + static RenderStreaming() { #if UNITY_EDITOR diff --git a/Runtime/Scripts/RenderStreamingSettings.cs b/Runtime/Scripts/RenderStreamingSettings.cs index 2a7ff09..dc22c7f 100644 --- a/Runtime/Scripts/RenderStreamingSettings.cs +++ b/Runtime/Scripts/RenderStreamingSettings.cs @@ -7,10 +7,14 @@ namespace Unity.RenderStreaming /// public class RenderStreamingSettings : ScriptableObject { + internal const string AutomaticStreamingPropertyName = nameof(automaticStreaming); + internal const string SignalingSettingsPropertyName = nameof(signalingSettings); + /// /// /// - [SerializeField] public bool automaticStreaming; + [SerializeField, Tooltip("Automatically performs the necessary setup for streaming and starts streaming.")] + public bool automaticStreaming; [SerializeReference, SignalingSettings] public SignalingSettings signalingSettings = new WebSocketSignalingSettings(); diff --git a/Runtime/Scripts/Signaling/FurioosSignaling.cs b/Runtime/Scripts/Signaling/FurioosSignaling.cs deleted file mode 100644 index 696353b..0000000 --- a/Runtime/Scripts/Signaling/FurioosSignaling.cs +++ /dev/null @@ -1,291 +0,0 @@ -using System; -using System.Text; -using System.Threading; -using Unity.WebRTC; -using UnityEngine; -using WebSocketSharp; - -namespace Unity.RenderStreaming.Signaling -{ - [Serializable] - public class FurioosRoutedMessage - { - public string from; - public string to; - public T message; - } - - [Flags] - enum SslProtocolsHack - { - Tls = 192, - Tls11 = 768, - Tls12 = 3072 - } - - public class FurioosSignaling : ISignaling - { - private float m_timeout; - private bool m_running; - private SynchronizationContext m_mainThreadContext; - private Thread m_signalingThread; - private AutoResetEvent m_wsCloseEvent; - private WebSocket m_webSocket; - - public delegate void OnSignedInHandler(ISignaling sender); - - public string Url { get { return string.Empty; } } - - public FurioosSignaling(SignalingSettings signalingSettings, SynchronizationContext mainThreadContext) - { - m_timeout = 5.0f; - m_mainThreadContext = mainThreadContext; - m_wsCloseEvent = new AutoResetEvent(false); - } - - public string connectionId - { - get - { - throw new NotImplementedException(); - } - } - - public void Start() - { - m_running = true; - m_signalingThread = new Thread(WSManage); - m_signalingThread.Start(); - } - - public void Stop() - { - if (m_running) - { - m_running = false; - m_webSocket.Close(); - - if (m_signalingThread.ThreadState == ThreadState.WaitSleepJoin) - { - m_signalingThread.Abort(); - } - else - { - m_signalingThread.Join(1000); - } - m_signalingThread = null; - } - } - - //todo(kazuki):: remove warning CS0067 -#pragma warning disable 0067 - public event OnStartHandler OnStart; - public event OnSignedInHandler OnSignedIn; - public event OnConnectHandler OnCreateConnection; - public event OnDisconnectHandler OnDestroyConnection; - public event OnOfferHandler OnOffer; - public event OnAnswerHandler OnAnswer; - public event OnIceCandidateHandler OnIceCandidate; -#pragma warning restore 0067 - public void SendOffer(string connectionId, RTCSessionDescription offer) - { - throw new NotImplementedException(); - } - - public void SendAnswer(string connectionId, RTCSessionDescription answer) - { - DescData data = new DescData(); - data.connectionId = connectionId; - data.sdp = answer.sdp; - data.type = "answer"; - - FurioosRoutedMessage routedMessage = new FurioosRoutedMessage(); - routedMessage.to = connectionId; - routedMessage.message = data; - - WSSend(routedMessage); - } - - public void SendCandidate(string connectionId, RTCIceCandidate candidate) - { - CandidateData data = new CandidateData(); - data.connectionId = connectionId; - data.candidate = candidate.Candidate; - data.sdpMLineIndex = candidate.SdpMLineIndex.GetValueOrDefault(0); - data.sdpMid = candidate.SdpMid; - - FurioosRoutedMessage routedMessage = new FurioosRoutedMessage(); - routedMessage.to = connectionId; - routedMessage.message = data; - - WSSend(routedMessage); - } - - public void OpenConnection(string connectionId) - { - this.WSSend("{\"type\":\"connect\"}"); - } - - public void CloseConnection(string connectionId) - { - throw new NotImplementedException(); - } - - private void WSManage() - { - while (m_running) - { - WSCreate(); - - m_wsCloseEvent.WaitOne(); - - Thread.Sleep((int)(m_timeout * 1000)); - } - - Debug.Log("Signaling: WS managing thread ended"); - } - - private void WSCreate() - { - m_webSocket = new WebSocket("ws://127.0.0.1:8081"); - m_webSocket.OnOpen += WSConnected; - m_webSocket.OnMessage += WSProcessMessage; - m_webSocket.OnError += WSError; - m_webSocket.OnClose += WSClosed; - - Monitor.Enter(m_webSocket); - - Debug.Log($"Signaling: Connecting to Furioos Server"); - m_webSocket.ConnectAsync(); - } - - private void WSProcessMessage(object sender, MessageEventArgs e) - { - var content = Encoding.UTF8.GetString(e.RawData); - Debug.Log($"Signaling: Receiving message: {content}"); - - try - { - var routedMessage = JsonUtility.FromJson>(content); - - SignalingMessage msg; - if (!string.IsNullOrEmpty(routedMessage.from)) - { - msg = routedMessage.message; - } - else - { - msg = JsonUtility.FromJson(content); - } - - if (!string.IsNullOrEmpty(msg.type)) - { - if (msg.type == "signIn") - { - if (msg.status == "SUCCESS") - { - Debug.Log("Signaling: Slot signed in."); - this.WSSend("{\"type\":\"furioos\",\"task\":\"enableStreaming\",\"streamType\":\"RenderStreaming\",\"streamProtocols\":[\"WebRTC\"],\"controlsTypes\":[\"RenderStreaming\"]}"); - - OnSignedIn?.Invoke(this); - } - else - { - Debug.LogError("Signaling: Sign-in error : " + msg.message); - } - } - else if (msg.type == "reconnect") - { - if (msg.status == "SUCCESS") - { - Debug.Log("Signaling: Slot reconnected."); - } - else - { - Debug.LogError("Signaling: Reconnect error : " + msg.message); - } - } - - if (msg.type == "offer") - { - if (!string.IsNullOrEmpty(routedMessage.from)) - { - DescData offer = new DescData(); - offer.connectionId = routedMessage.from; - offer.sdp = msg.sdp; - offer.polite = false; - - m_mainThreadContext.Post(d => OnOffer?.Invoke(this, offer), null); - } - else - { - Debug.LogError("Signaling: Received message from unknown peer"); - } - } - } - else if (!string.IsNullOrEmpty(msg.candidate)) - { - if (!string.IsNullOrEmpty(routedMessage.from)) - { - CandidateData candidate = new CandidateData(); - candidate.connectionId = routedMessage.from; - candidate.candidate = msg.candidate; - candidate.sdpMLineIndex = msg.sdpMLineIndex; - candidate.sdpMid = msg.sdpMid; - - m_mainThreadContext.Post(d => OnIceCandidate?.Invoke(this, candidate), null); - } - else - { - Debug.LogError("Signaling: Received message from unknown peer"); - } - } - } - catch (Exception ex) - { - Debug.LogError("Signaling: Failed to parse message: " + ex); - } - } - - private void WSConnected(object sender, EventArgs e) - { - Debug.Log("Signaling: WS connected."); - this.WSSend("{\"type\" :\"signIn\",\"peerName\" :\"Unity Test App\"}"); - } - - - private void WSError(object sender, ErrorEventArgs e) - { - Debug.LogError($"Signaling: WS connection error: {e.Message}"); - } - - private void WSClosed(object sender, CloseEventArgs e) - { - Debug.Log($"Signaling: WS connection closed, code: {e.Code}"); - - m_wsCloseEvent.Set(); - m_webSocket = null; - } - - private void WSSend(object data) - { - if (m_webSocket == null || m_webSocket.ReadyState != WebSocketState.Open) - { - Debug.LogError("Signaling: WS is not connected. Unable to send message"); - return; - } - - if (data is string s) - { - Debug.Log("Signaling: Sending WS data: " + s); - m_webSocket.Send(s); - } - else - { - string str = JsonUtility.ToJson(data); - Debug.Log("Signaling: Sending WS data: " + str); - m_webSocket.Send(str); - } - } - } -} diff --git a/Runtime/Scripts/Signaling/FurioosSignaling.cs.meta b/Runtime/Scripts/Signaling/FurioosSignaling.cs.meta deleted file mode 100644 index e7afb41..0000000 --- a/Runtime/Scripts/Signaling/FurioosSignaling.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 297659e9af19444f96fa3faaf9e07cd7 -timeCreated: 1586218415 \ No newline at end of file diff --git a/Runtime/Scripts/Signaling/FurioosSignalingSettings.cs b/Runtime/Scripts/Signaling/FurioosSignalingSettings.cs deleted file mode 100644 index e32a774..0000000 --- a/Runtime/Scripts/Signaling/FurioosSignalingSettings.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Unity.RenderStreaming.Signaling; -using UnityEngine; - -namespace Unity.RenderStreaming -{ - /// - /// - /// - [Serializable] - public class FurioosSignalingSettings : SignalingSettings - { - /// - /// - /// - public override Type signalingClass => typeof(FurioosSignaling); - - /// - /// - /// - public override IReadOnlyCollection iceServers => m_iceServers; - - /// - /// - /// - public string url => m_url; - - [SerializeField] - protected string m_url; - [SerializeField] - protected IceServer[] m_iceServers; - - /// - /// - /// - /// - /// - public FurioosSignalingSettings(string url, IceServer[] iceServers = null) - { - if (url == null) - throw new ArgumentNullException("url"); - if (!Uri.IsWellFormedUriString(url, UriKind.RelativeOrAbsolute)) - throw new ArgumentException("url is not well formed Uri"); - - m_url = url; - m_iceServers = iceServers == null ? Array.Empty() : iceServers.Select(server => server.Clone()).ToArray(); - } - - /// - /// - /// - public FurioosSignalingSettings() - { - m_url = "http://127.0.0.1"; - m_iceServers = new[] - { - new IceServer (urls: new[] {"stun:stun.l.google.com:19302"}) - }; - } - - /// - /// - /// - /// - /// - public override bool ParseArguments(string[] arguments) - { - return true; - } - } -} diff --git a/Runtime/Scripts/Signaling/FurioosSignalingSettings.cs.meta b/Runtime/Scripts/Signaling/FurioosSignalingSettings.cs.meta deleted file mode 100644 index 9dc5f98..0000000 --- a/Runtime/Scripts/Signaling/FurioosSignalingSettings.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 0144784cda4001d489615dee132aa6e6 -timeCreated: 1674112040 \ No newline at end of file diff --git a/Runtime/Scripts/Signaling/HttpSignaling.cs b/Runtime/Scripts/Signaling/HttpSignaling.cs index d7480c2..0f4ac5c 100644 --- a/Runtime/Scripts/Signaling/HttpSignaling.cs +++ b/Runtime/Scripts/Signaling/HttpSignaling.cs @@ -22,7 +22,7 @@ public class HttpSignaling : ISignaling private string m_sessionId; private long m_lastTimeGetAllRequest; - public string Url { get { return m_url; } } + public string Url { get { return m_url; } } public HttpSignaling(SignalingSettings signalingSettings, SynchronizationContext mainThreadContext) { @@ -42,7 +42,7 @@ public HttpSignaling(SignalingSettings signalingSettings, SynchronizationContext if (instances.Any(x => x.Url == m_url)) { - Debug.LogWarning($"Other {nameof(HttpSignaling)} exists with same URL:{m_url}. Signaling process may be in conflict."); + RenderStreaming.Logger.Log(LogType.Warning, $"Other {nameof(HttpSignaling)} exists with same URL:{m_url}. Signaling process may be in conflict."); } instances.Add(this); @@ -60,6 +60,7 @@ public void Start() throw new InvalidOperationException("This object is already started."); m_running = true; m_signalingThread = new Thread(HTTPPolling); + m_signalingThread.IsBackground = true; m_signalingThread.Start(); } @@ -69,14 +70,20 @@ public void Stop() if (m_signalingThread != null) { - if (m_signalingThread.ThreadState == ThreadState.WaitSleepJoin) + try { - m_signalingThread.Abort(); + // Note: Allow for twice the configured m_timeout duration when joining to account for the polling sleep + // and the time it takes to send a disconnect to the signaling server. + if (!m_signalingThread.Join(m_timeout * 2)) + { + m_signalingThread.Abort(); + } } - else + catch (Exception e) { - m_signalingThread.Join(1000); + RenderStreaming.Logger.Log(LogType.Error, "Signaling: HTTP stopping thread error : " + e); } + m_signalingThread = null; } } @@ -162,12 +169,12 @@ private void HTTPPolling() } catch (Exception e) { - Debug.LogError("Signaling: HTTP polling error : " + e); + RenderStreaming.Logger.Log(LogType.Error, "Signaling: HTTP polling error : " + e); } } HTTPDelete(); - Debug.Log("Signaling: HTTP polling thread ended"); + RenderStreaming.Logger.Log("Signaling: HTTP polling thread ended"); } private static HttpWebResponse HTTPGetResponse(HttpWebRequest request) @@ -182,7 +189,7 @@ private static HttpWebResponse HTTPGetResponse(HttpWebRequest request) } else { - Debug.LogError($"Signaling: {response.ResponseUri} HTTP request failed ({response.StatusCode})"); + RenderStreaming.Logger.Log(LogType.Error, $"Signaling: {response.ResponseUri} HTTP request failed ({response.StatusCode})"); response.Close(); } } @@ -192,7 +199,7 @@ private static HttpWebResponse HTTPGetResponse(HttpWebRequest request) } catch (Exception e) { - Debug.LogError($"Signaling: HTTP request error. url:{request.RequestUri} exception:{e}"); + RenderStreaming.Logger.Log(LogType.Error, $"Signaling: HTTP request error. url:{request.RequestUri} exception:{e}"); } return null; @@ -243,14 +250,14 @@ private bool HTTPCreate() request.KeepAlive = false; request.ContentLength = 0; - Debug.Log($"Signaling: Connecting HTTP {m_url}"); + RenderStreaming.Logger.Log($"Signaling: Connecting HTTP {m_url}"); OpenSessionData resp = HTTPParseJsonResponse(HTTPGetResponse(request)); if (resp != null) { m_sessionId = resp.sessionId; - Debug.Log("Signaling: HTTP connected, sessionId : " + m_sessionId); + RenderStreaming.Logger.Log("Signaling: HTTP connected, sessionId : " + m_sessionId); m_mainThreadContext.Post(d => OnStart?.Invoke(this), null); return true; @@ -269,7 +276,7 @@ private bool HTTPDelete() request.KeepAlive = false; request.Headers.Add("Session-Id", m_sessionId); - Debug.Log($"Signaling: Removing HTTP connection from {m_url}"); + RenderStreaming.Logger.Log($"Signaling: Removing HTTP connection from {m_url}"); return (HTTPParseTextResponse(HTTPGetResponse(request)) != null); } @@ -279,7 +286,7 @@ private bool HTTPPost(string path, object data) string str = JsonUtility.ToJson(data); byte[] bytes = new System.Text.UTF8Encoding().GetBytes(str); - Debug.Log("Signaling: Posting HTTP data: " + str); + RenderStreaming.Logger.Log("Signaling: Posting HTTP data: " + str); HttpWebRequest request = (HttpWebRequest)WebRequest.Create($"{m_url}/{path}"); request.Method = "POST"; @@ -317,7 +324,7 @@ private bool HTTPConnect(string connectionId) if (data == null) return false; - Debug.Log($"Signaling: HTTP create connection, connectionId: {connectionId}, polite:{data.polite}"); + RenderStreaming.Logger.Log($"Signaling: HTTP create connection, connectionId: {connectionId}, polite:{data.polite}"); m_mainThreadContext.Post(d => OnCreateConnection?.Invoke(this, data.connectionId, data.polite), null); return true; } @@ -342,7 +349,7 @@ private bool HTTPDisonnect(string connectionId) if (data == null) return false; - Debug.Log("Signaling: HTTP delete connection, connectionId : " + connectionId); + RenderStreaming.Logger.Log("Signaling: HTTP delete connection, connectionId : " + connectionId); m_mainThreadContext.Post(d => OnDestroyConnection?.Invoke(this, connectionId), null); return true; } @@ -361,15 +368,15 @@ private bool HTTPGetAll() if (data == null) return false; - m_lastTimeGetAllRequest = DateTimeExtension.ParseHttpDate(response.Headers[HttpResponseHeader.Date]) - .ToJsMilliseconds(); + m_lastTimeGetAllRequest = + long.TryParse(data.datetime, out var result) ? result : DateTime.Now.ToJsMilliseconds(); foreach (var msg in data.messages) { if (string.IsNullOrEmpty(msg.type)) continue; - if(msg.type == "disconnect") + if (msg.type == "disconnect") { m_mainThreadContext.Post(d => OnDestroyConnection?.Invoke(this, msg.connectionId), null); } diff --git a/Runtime/Scripts/Signaling/HttpSignalingSettings.cs b/Runtime/Scripts/Signaling/HttpSignalingSettings.cs index 485b995..de7e9e8 100644 --- a/Runtime/Scripts/Signaling/HttpSignalingSettings.cs +++ b/Runtime/Scripts/Signaling/HttpSignalingSettings.cs @@ -29,11 +29,11 @@ public class HttpSignalingSettings : SignalingSettings /// public int interval => m_interval; - [SerializeField] + [SerializeField, Tooltip("Set the polling frequency (in milliseconds) to the signaling server.")] private int m_interval; - [SerializeField] + [SerializeField, Tooltip("Set the signaling server URL. you should specify a URL starting with \"http\" or \"https\".")] protected string m_url; - [SerializeField] + [SerializeField, Tooltip("Set a list of STUN/TURN servers.")] protected IceServer[] m_iceServers; /// diff --git a/Runtime/Scripts/Signaling/SignalingMessage.cs b/Runtime/Scripts/Signaling/SignalingMessage.cs index baf7bab..975a146 100644 --- a/Runtime/Scripts/Signaling/SignalingMessage.cs +++ b/Runtime/Scripts/Signaling/SignalingMessage.cs @@ -100,6 +100,7 @@ class CandidateContainerResData class AllResData { public SignalingMessage[] messages; + public string datetime; } #pragma warning restore 0649 diff --git a/Runtime/Scripts/Signaling/SignalingSettings.cs b/Runtime/Scripts/Signaling/SignalingSettings.cs index 40a5304..b2a36f1 100644 --- a/Runtime/Scripts/Signaling/SignalingSettings.cs +++ b/Runtime/Scripts/Signaling/SignalingSettings.cs @@ -85,7 +85,7 @@ public class IceServer /// /// /// - public static implicit operator RTCIceServer(IceServer server) + public static explicit operator RTCIceServer(IceServer server) { var iceServer = new RTCIceServer { diff --git a/Runtime/Scripts/Signaling/WebSocketSignaling.cs b/Runtime/Scripts/Signaling/WebSocketSignaling.cs index 1e4af72..969a509 100644 --- a/Runtime/Scripts/Signaling/WebSocketSignaling.cs +++ b/Runtime/Scripts/Signaling/WebSocketSignaling.cs @@ -26,9 +26,9 @@ public class WebSocketSignaling : ISignaling public WebSocketSignaling(SignalingSettings signalingSettings, SynchronizationContext mainThreadContext) { - if(signalingSettings == null) + if (signalingSettings == null) throw new ArgumentNullException(nameof(signalingSettings)); - if(!(signalingSettings is WebSocketSignalingSettings settings)) + if (!(signalingSettings is WebSocketSignalingSettings settings)) throw new ArgumentException("signalingSettings is not WebSocketSignalingSettings"); m_url = settings.url; m_timeout = 5.0f; @@ -37,7 +37,7 @@ public WebSocketSignaling(SignalingSettings signalingSettings, SynchronizationCo if (instances.Any(x => x.Url == m_url)) { - Debug.LogWarning($"Other {nameof(WebSocketSignaling)} exists with same URL:{m_url}. Signaling process may be in conflict."); + RenderStreaming.Logger.Log(LogType.Warning, $"Other {nameof(WebSocketSignaling)} exists with same URL:{m_url}. Signaling process may be in conflict."); } instances.Add(this); @@ -84,10 +84,10 @@ public void Stop() public event OnConnectHandler OnCreateConnection; public event OnDisconnectHandler OnDestroyConnection; public event OnOfferHandler OnOffer; - #pragma warning disable 0067 +#pragma warning disable 0067 // this event is never used in this class public event OnAnswerHandler OnAnswer; - #pragma warning restore 0067 +#pragma warning restore 0067 public event OnIceCandidateHandler OnIceCandidate; public void SendOffer(string connectionId, RTCSessionDescription offer) @@ -165,7 +165,7 @@ private void WSManage() } } - Debug.Log("Signaling: WS managing thread ended"); + RenderStreaming.Logger.Log("Signaling: WS managing thread ended"); } private void WSCreate() @@ -184,14 +184,14 @@ private void WSCreate() Monitor.Enter(m_webSocket); - Debug.Log($"Signaling: Connecting WS {m_url}"); + RenderStreaming.Logger.Log($"Signaling: Connecting WS {m_url}"); m_webSocket.ConnectAsync(); } private void WSProcessMessage(object sender, MessageEventArgs e) { var content = Encoding.UTF8.GetString(e.RawData); - Debug.Log($"Signaling: Receiving message: {content}"); + RenderStreaming.Logger.Log($"Signaling: Receiving message: {content}"); try { @@ -250,31 +250,31 @@ private void WSProcessMessage(object sender, MessageEventArgs e) else if (routedMessage.type == "error") { msg = JsonUtility.FromJson(content); - Debug.LogError(msg.message); + RenderStreaming.Logger.Log(LogType.Error, msg.message); } } } catch (Exception ex) { - Debug.LogError("Signaling: Failed to parse message: " + ex); + RenderStreaming.Logger.Log(LogType.Error, "Signaling: Failed to parse message: " + ex); } } private void WSConnected(object sender, EventArgs e) { - Debug.Log("Signaling: WS connected."); + RenderStreaming.Logger.Log("Signaling: WS connected."); m_mainThreadContext.Post(d => OnStart?.Invoke(this), null); } private void WSError(object sender, ErrorEventArgs e) { - Debug.LogError($"Signaling: WS connection error: {e.Message}"); + RenderStreaming.Logger.Log(LogType.Error, $"Signaling: WS connection error: {e.Message}"); } private void WSClosed(object sender, CloseEventArgs e) { - Debug.Log($"Signaling: WS connection closed, code: {e.Code}"); + RenderStreaming.Logger.Log($"Signaling: WS connection closed, code: {e.Code}"); m_wsCloseEvent.Set(); m_webSocket = null; @@ -284,19 +284,19 @@ private void WSSend(object data) { if (m_webSocket == null || m_webSocket.ReadyState != WebSocketState.Open) { - Debug.LogError("Signaling: WS is not connected. Unable to send message"); + RenderStreaming.Logger.Log(LogType.Error, "Signaling: WS is not connected. Unable to send message"); return; } if (data is string s) { - Debug.Log("Signaling: Sending WS data: " + s); + RenderStreaming.Logger.Log("Signaling: Sending WS data: " + s); m_webSocket.Send(s); } else { string str = JsonUtility.ToJson(data); - Debug.Log("Signaling: Sending WS data: " + str); + RenderStreaming.Logger.Log("Signaling: Sending WS data: " + str); m_webSocket.Send(str); } } diff --git a/Runtime/Scripts/Signaling/WebSocketSignalingSettings.cs b/Runtime/Scripts/Signaling/WebSocketSignalingSettings.cs index 34f27b6..2e95145 100644 --- a/Runtime/Scripts/Signaling/WebSocketSignalingSettings.cs +++ b/Runtime/Scripts/Signaling/WebSocketSignalingSettings.cs @@ -27,9 +27,10 @@ public class WebSocketSignalingSettings : SignalingSettings /// public string url => m_url; - [SerializeField] + [SerializeField, Tooltip("Set the signaling server URL. you should specify a URL starting with \"ws\" or \"wss\".")] protected string m_url; - [SerializeField] + + [SerializeField, Tooltip("Set a list of STUN/TURN servers.")] protected IceServer[] m_iceServers; /// @@ -74,9 +75,9 @@ public override bool ParseArguments(string[] arguments) { CommandLineInfo info = CommandLineParser.ImportJson.Value.Value; - if(info.signalingUrl != null) + if (info.signalingUrl != null) m_url = info.signalingUrl; - if(info.iceServers != null && info.iceServers.Length != 0) + if (info.iceServers != null && info.iceServers.Length != 0) m_iceServers = info.iceServers.Select(v => new IceServer(v)).ToArray(); } if (CommandLineParser.SignalingUrl.Value != null) @@ -95,12 +96,12 @@ public override bool ParseArguments(string[] arguments) ? CommandLineParser.IceServerUrls.Value : null; - if(m_iceServers.Length > 0) + if (m_iceServers.Length > 0) m_iceServers[0] = m_iceServers[0].Clone( - username:username, - credential:credential, + username: username, + credential: credential, credentialType: credentialType, - urls:urls); + urls: urls); else m_iceServers = new IceServer[] { diff --git a/Runtime/Scripts/SignalingHandlerBase.cs b/Runtime/Scripts/SignalingHandlerBase.cs index 8a27bb0..3a3df72 100644 --- a/Runtime/Scripts/SignalingHandlerBase.cs +++ b/Runtime/Scripts/SignalingHandlerBase.cs @@ -1,7 +1,7 @@ using System; -using System.Linq; using System.Collections; using System.Collections.Generic; +using System.Linq; using Unity.WebRTC; using UnityEngine; @@ -160,10 +160,10 @@ public virtual void AddSender(string connectionId, IStreamSender sender) private IEnumerator AddSenderCoroutine(string connectionId, IStreamSender sender) { - if(sender.Track == null && sender is StreamSenderBase senderBase) + if (sender.Track == null && sender is StreamSenderBase senderBase) { var op = senderBase.CreateTrack(); - if(op.Track == null) + if (op.Track == null) yield return op; senderBase.SetTrack(op.Track); } @@ -198,7 +198,7 @@ public virtual void RemoveSender(string connectionId, IStreamSender sender) } /// - /// + /// /// /// /// @@ -353,7 +353,7 @@ public interface IStreamReceiver } /// - /// + /// /// public interface IDataChannel { @@ -372,6 +372,11 @@ public interface IDataChannel /// string Label { get; } + /// + /// + /// + string ConnectionId { get; } + /// /// /// diff --git a/Runtime/Scripts/SignalingManager.cs b/Runtime/Scripts/SignalingManager.cs index 162a5d4..b9f8a22 100644 --- a/Runtime/Scripts/SignalingManager.cs +++ b/Runtime/Scripts/SignalingManager.cs @@ -1,11 +1,11 @@ using System; -using System.Linq; using System.Collections.Generic; +using System.Linq; using System.Reflection; using System.Threading; -using UnityEngine; -using Unity.WebRTC; using Unity.RenderStreaming.Signaling; +using Unity.WebRTC; +using UnityEngine; #if UNITY_EDITOR using UnityEditor; @@ -16,8 +16,15 @@ namespace Unity.RenderStreaming [AddComponentMenu("Render Streaming/Signaling Manager")] public sealed class SignalingManager : MonoBehaviour { + internal const string UseDefaultPropertyName = nameof(m_useDefault); + internal const string SignalingSettingsObjectPropertyName = nameof(signalingSettingsObject); + internal const string SignalingSettingsPropertyName = nameof(signalingSettings); + internal const string HandlersPropertyName = nameof(handlers); + internal const string RunOnAwakePropertyName = nameof(runOnAwake); + internal const string EvaluateCommandlineArgumentsPropertyName = nameof(evaluateCommandlineArguments); + #pragma warning disable 0649 - [SerializeField] + [SerializeField, Tooltip("Use settings in Project Settings Window.")] private bool m_useDefault = true; [SerializeField] @@ -47,6 +54,9 @@ public sealed class SignalingManager : MonoBehaviour private SignalingEventProvider m_provider; private bool m_running; + /// + /// + /// public bool Running => m_running; static ISignaling CreateSignaling(SignalingSettings settings, SynchronizationContext context) @@ -85,7 +95,7 @@ public void SetSignalingSettings(SignalingSettings settings) } /// - /// + /// /// /// public SignalingSettings GetSignalingSettings() @@ -187,11 +197,17 @@ private void _Run( { if (!EvaluateCommandlineArguments(ref settings, arguments)) { - Debug.LogError("Command line arguments are invalid."); + RenderStreaming.Logger.Log(LogType.Error, "Command line arguments are invalid."); } } #endif - RTCIceServer[] iceServers = settings.iceServers.OfType().ToArray(); + int i = 0; + RTCIceServer[] iceServers = new RTCIceServer[settings.iceServers.Count()]; + foreach (var iceServer in settings.iceServers) + { + iceServers[i] = (RTCIceServer)iceServer; + i++; + } RTCConfiguration _conf = conf.GetValueOrDefault(new RTCConfiguration { iceServers = iceServers }); @@ -265,7 +281,13 @@ void Awake() return; var settings = m_useDefault ? RenderStreaming.GetSignalingSettings() : signalingSettings; - RTCIceServer[] iceServers = settings.iceServers.OfType().ToArray(); + int i = 0; + RTCIceServer[] iceServers = new RTCIceServer[settings.iceServers.Count()]; + foreach (var iceServer in settings.iceServers) + { + iceServers[i] = (RTCIceServer)iceServer; + i++; + } RTCConfiguration conf = new RTCConfiguration { iceServers = iceServers }; ISignaling signaling = CreateSignaling(settings, SynchronizationContext.Current); _Run(conf, signaling, handlers.ToArray()); diff --git a/Runtime/Scripts/SignalingManagerInternal.cs b/Runtime/Scripts/SignalingManagerInternal.cs index cd986b6..2901c97 100644 --- a/Runtime/Scripts/SignalingManagerInternal.cs +++ b/Runtime/Scripts/SignalingManagerInternal.cs @@ -2,9 +2,9 @@ using System.Collections; using System.Collections.Generic; using System.Linq; -using UnityEngine; using Unity.RenderStreaming.Signaling; using Unity.WebRTC; +using UnityEngine; namespace Unity.RenderStreaming { @@ -154,7 +154,7 @@ public void Dispose() _signaling.OnAnswer -= OnAnswer; _signaling.OnIceCandidate -= OnIceCandidate; - foreach(var pair in _mapConnectionIdAndPeer) + foreach (var pair in _mapConnectionIdAndPeer) pair.Value.Dispose(); this._disposed = true; @@ -312,7 +312,7 @@ IEnumerator ResendOfferCoroutine() { failedConnections.Add(peer.Key); } - else if(peer.Value.waitingAnswer) + else if (peer.Value.waitingAnswer) { peer.Value.SendOffer(); } @@ -366,7 +366,7 @@ PeerConnection CreatePeerConnection(string connectionId, bool polite) peer.OnConnectHandler += () => onConnect?.Invoke(connectionId); peer.OnDisconnectHandler += () => onDisconnect?.Invoke(connectionId); - peer.OnDataChannelHandler += channel => onAddChannel?.Invoke(connectionId, channel);; + peer.OnDataChannelHandler += channel => onAddChannel?.Invoke(connectionId, channel); ; peer.OnTrackEventHandler += e => onAddTransceiver?.Invoke(connectionId, e.Transceiver); peer.SendOfferHandler += desc => _signaling?.SendOffer(connectionId, desc); peer.SendAnswerHandler += desc => _signaling?.SendAnswer(connectionId, desc); @@ -389,11 +389,11 @@ void OnAnswer(ISignaling signaling, DescData e) { if (!_mapConnectionIdAndPeer.TryGetValue(e.connectionId, out var pc)) { - Debug.LogWarning($"connectionId:{e.connectionId}, peerConnection not exist"); + RenderStreaming.Logger.Log(LogType.Warning, $"connectionId:{e.connectionId}, peerConnection not exist"); return; } - RTCSessionDescription description = new RTCSessionDescription {type = RTCSdpType.Answer, sdp = e.sdp}; + RTCSessionDescription description = new RTCSessionDescription { type = RTCSdpType.Answer, sdp = e.sdp }; _startCoroutine(pc.OnGotDescription(description, () => onGotAnswer?.Invoke(e.connectionId, e.sdp))); } @@ -406,7 +406,9 @@ void OnIceCandidate(ISignaling signaling, CandidateData e) RTCIceCandidateInit option = new RTCIceCandidateInit { - candidate = e.candidate, sdpMLineIndex = e.sdpMLineIndex, sdpMid = e.sdpMid + candidate = e.candidate, + sdpMLineIndex = e.sdpMLineIndex, + sdpMid = e.sdpMid }; pc.OnGotIceCandidate(new RTCIceCandidate(option)); } @@ -419,7 +421,7 @@ void OnOffer(ISignaling signaling, DescData e) pc = CreatePeerConnection(connectionId, e.polite); } - RTCSessionDescription description = new RTCSessionDescription {type = RTCSdpType.Offer, sdp = e.sdp}; + RTCSessionDescription description = new RTCSessionDescription { type = RTCSdpType.Offer, sdp = e.sdp }; _startCoroutine(pc.OnGotDescription(description, () => onGotOffer?.Invoke(connectionId, e.sdp))); } } diff --git a/Runtime/Scripts/VideoCodecInfo.cs b/Runtime/Scripts/VideoCodecInfo.cs index fafec94..ef07054 100644 --- a/Runtime/Scripts/VideoCodecInfo.cs +++ b/Runtime/Scripts/VideoCodecInfo.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -using UnityEngine; using Unity.WebRTC; +using UnityEngine; namespace Unity.RenderStreaming { @@ -122,7 +122,7 @@ protected Dictionary parameters static internal VideoCodecInfo Create(RTCRtpCodecCapability caps) { - switch(caps.mimeType) + switch (caps.mimeType) { case "video/H264": return new H264CodecInfo(caps); @@ -149,7 +149,7 @@ internal VideoCodecInfo(RTCRtpCodecCapability caps) m_SdpFmtpLine = caps.sdpFmtpLine; string[] subs = m_SdpFmtpLine.Split(';'); - foreach(string sub in subs) + foreach (string sub in subs) { string[] pair = sub.Split('='); parameters.Add(pair[0], pair[1]); @@ -194,7 +194,7 @@ public VP9Profile? profile { get { - if(parameters.TryGetValue(KeyProfileId, out var value)) + if (parameters.TryGetValue(KeyProfileId, out var value)) { return (VP9Profile)Enum.ToObject(typeof(VP9Profile), Convert.ToInt32(value)); } diff --git a/Runtime/Scripts/VideoStreamReceiver.cs b/Runtime/Scripts/VideoStreamReceiver.cs index 64e6000..033756c 100644 --- a/Runtime/Scripts/VideoStreamReceiver.cs +++ b/Runtime/Scripts/VideoStreamReceiver.cs @@ -1,9 +1,9 @@ using System; using System.Collections; -using System.Linq; using System.Collections.Generic; -using UnityEngine; +using System.Linq; using Unity.WebRTC; +using UnityEngine; namespace Unity.RenderStreaming { @@ -28,6 +28,10 @@ public enum VideoRenderMode [AddComponentMenu("Render Streaming/Video Stream Receiver")] public class VideoStreamReceiver : StreamReceiverBase { + internal const string CodecPropertyName = nameof(m_Codec); + internal const string RenderModePropertyName = nameof(m_RenderMode); + internal const string TargetTexturePropertyName = nameof(m_TargetTexture); + /// /// /// @@ -144,7 +148,7 @@ private void StoppedStream(string connectionId) { m_texture = null; OnUpdateReceiveTexture?.Invoke(m_texture); - if(m_coroutine != null) + if (m_coroutine != null) { StopCoroutine(m_coroutine); m_coroutine = null; diff --git a/Runtime/Scripts/VideoStreamSender.cs b/Runtime/Scripts/VideoStreamSender.cs index 3c852b0..b63117e 100644 --- a/Runtime/Scripts/VideoStreamSender.cs +++ b/Runtime/Scripts/VideoStreamSender.cs @@ -1,11 +1,11 @@ -using Unity.WebRTC; -using UnityEngine; using System; using System.Collections; using System.Collections.Generic; using System.Linq; -using UnityEngine.Rendering; +using Unity.WebRTC; +using UnityEngine; using UnityEngine.Experimental.Rendering; +using UnityEngine.Rendering; namespace Unity.RenderStreaming { @@ -114,12 +114,23 @@ public enum VideoStreamSource public class VideoStreamSender : StreamSenderBase { static readonly float s_defaultFrameRate = 30; - static readonly uint s_defaultMinBitrate = 0; static readonly uint s_defaultMaxBitrate = 1000; - static readonly int s_defaultDepth = 16; + internal const string SourcePropertyName = nameof(m_Source); + internal const string CameraPropertyName = nameof(m_Camera); + internal const string TexturePropertyName = nameof(m_Texture); + internal const string WebCamDeviceIndexPropertyName = nameof(m_WebCamDeviceIndex); + internal const string CodecPropertyName = nameof(m_Codec); + internal const string TextureSizePropertyName = nameof(m_TextureSize); + internal const string FrameRatePropertyName = nameof(m_FrameRate); + internal const string BitratePropertyName = nameof(m_Bitrate); + internal const string ScaleFactorPropertyName = nameof(m_ScaleFactor); + internal const string DepthPropertyName = nameof(m_Depth); + internal const string AntiAliasingPropertyName = nameof(m_AntiAliasing); + internal const string AutoRequestUserAuthorizationPropertyName = nameof(m_AutoRequestUserAuthorization); + //todo(kazuki): remove this value. [SerializeField, StreamingSize] private Vector2Int m_TextureSize = new Vector2Int(1280, 720); @@ -246,7 +257,7 @@ public WebCamTexture sourceWebCamTexture { get { - if(m_sourceImpl is VideoStreamSourceWebCam source) + if (m_sourceImpl is VideoStreamSourceWebCam source) { return source.webCamTexture; } @@ -515,7 +526,7 @@ public override WaitForCreateTrack CreateTrack() if (m_renderTexture.graphicsFormat != format) { - Debug.LogWarning( + RenderStreaming.Logger.Log(LogType.Warning, $"This color format:{m_renderTexture.graphicsFormat} not support in unity.webrtc. Change to supported color format:{format}."); m_renderTexture.Release(); m_renderTexture.graphicsFormat = format; @@ -793,7 +804,7 @@ IEnumerator ConvertFrame() public override void Dispose() { - if(m_coroutineConvertFrame != null) + if (m_coroutineConvertFrame != null) { m_parent.StopCoroutine(m_coroutineConvertFrame); m_parent = null; diff --git a/Runtime/Unity.RenderStreaming.Runtime.asmdef b/Runtime/Unity.RenderStreaming.Runtime.asmdef index 3f807a1..4989625 100644 --- a/Runtime/Unity.RenderStreaming.Runtime.asmdef +++ b/Runtime/Unity.RenderStreaming.Runtime.asmdef @@ -40,6 +40,11 @@ "expression": "", "define": "URS_USE_AR_SUBSYSTEMS" }, + { + "name": "com.unity.textmeshpro", + "expression": "", + "define": "URS_USE_TEXTMESHPRO" + }, { "name": "com.unity.inputsystem", "expression": "1.1", diff --git a/Samples~/Example/ARFoundation/ARFoundationSample.cs b/Samples~/Example/ARFoundation/ARFoundationSample.cs index 015fbd2..f008168 100644 --- a/Samples~/Example/ARFoundation/ARFoundationSample.cs +++ b/Samples~/Example/ARFoundation/ARFoundationSample.cs @@ -1,9 +1,9 @@ #if URS_USE_AR_FOUNDATION using System.Collections; using UnityEngine; -using UnityEngine.UI; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Controls; +using UnityEngine.UI; using UnityEngine.XR.ARFoundation; namespace Unity.RenderStreaming.Samples @@ -55,7 +55,7 @@ IEnumerator Start() renderStreaming.Run(); } - if ((ARSession.state == ARSessionState.None ) || + if ((ARSession.state == ARSessionState.None) || (ARSession.state == ARSessionState.CheckingAvailability)) { yield return ARSession.CheckAvailability(); @@ -124,7 +124,7 @@ private void UpdateQuaternion(InputAction.CallbackContext context) void CreateConnection() { - if(settings != null) + if (settings != null) receiveVideoViewer.SetCodec(settings.ReceiverVideoCodec); _connectionId = System.Guid.NewGuid().ToString("N"); diff --git a/Samples~/Example/Bidirectional/Bidirectional.unity b/Samples~/Example/Bidirectional/Bidirectional.unity index 952b5df..01d5629 100644 --- a/Samples~/Example/Bidirectional/Bidirectional.unity +++ b/Samples~/Example/Bidirectional/Bidirectional.unity @@ -506,10 +506,10 @@ RectTransform: m_Father: {fileID: 932364532} m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 0, y: 0} - m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 0, y: 50} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 188.24998, y: -25} + m_SizeDelta: {x: 376.49997, y: 50} m_Pivot: {x: 0.5, y: 0.5} --- !u!114 &185332756 MonoBehaviour: @@ -1164,10 +1164,10 @@ RectTransform: m_Father: {fileID: 932364532} m_RootOrder: 1 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 0, y: 0} - m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 0, y: 50} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 376.49997, y: -60} + m_SizeDelta: {x: 376.49997, y: 50} m_Pivot: {x: 1, y: 1} --- !u!114 &346362251 MonoBehaviour: @@ -1839,6 +1839,81 @@ RectTransform: m_AnchoredPosition: {x: 0, y: 0} m_SizeDelta: {x: 0, y: 0} m_Pivot: {x: 0, y: 0} +--- !u!1 &640709220 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 640709221} + - component: {fileID: 640709223} + - component: {fileID: 640709222} + m_Layer: 5 + m_Name: Checkmark + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &640709221 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 640709220} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 2028247180} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 20, y: 20} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &640709222 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 640709220} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10901, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &640709223 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 640709220} + m_CullTransparentMesh: 1 --- !u!1 &678698531 GameObject: m_ObjectHideFlags: 0 @@ -2080,6 +2155,85 @@ Transform: m_Father: {fileID: 0} m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &848285179 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 848285180} + - component: {fileID: 848285182} + - component: {fileID: 848285181} + m_Layer: 5 + m_Name: Label + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &848285180 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 848285179} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 1134039333} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 19, y: 0} + m_SizeDelta: {x: -48, y: -2} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &848285181 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 848285179} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 3 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Audio Loopback +--- !u!222 &848285182 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 848285179} + m_CullTransparentMesh: 1 --- !u!1 &932364531 GameObject: m_ObjectHideFlags: 0 @@ -2114,10 +2268,10 @@ RectTransform: m_Father: {fileID: 1423621364} m_RootOrder: 1 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 0, y: 0} - m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 0, y: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 396.49997, y: -180} + m_SizeDelta: {x: 376.49997, y: 170} m_Pivot: {x: 0, y: 0} --- !u!114 &932364533 MonoBehaviour: @@ -2252,12 +2406,12 @@ RectTransform: m_Children: - {fileID: 172843649} m_Father: {fileID: 2134522315} - m_RootOrder: 2 + m_RootOrder: 3 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 0, y: 0} - m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 0, y: 50} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 376.49997, y: -160} + m_SizeDelta: {x: 376.49997, y: 50} m_Pivot: {x: 1, y: 1} --- !u!114 &1013695940 MonoBehaviour: @@ -2668,6 +2822,132 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1094835989} m_CullTransparentMesh: 1 +--- !u!1 &1134039332 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1134039333} + - component: {fileID: 1134039334} + - component: {fileID: 1134039336} + - component: {fileID: 1134039335} + m_Layer: 5 + m_Name: AudioLoopbackToggle + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1134039333 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1134039332} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 2028247180} + - {fileID: 848285180} + m_Father: {fileID: 2134522315} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 188.24998, y: -135} + m_SizeDelta: {x: 376.49997, y: 30} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1134039334 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1134039332} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9085046f02f69544eb97fd06b6048fe2, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 2028247181} + toggleTransition: 1 + graphic: {fileID: 640709222} + m_Group: {fileID: 0} + onValueChanged: + m_PersistentCalls: + m_Calls: [] + m_IsOn: 0 +--- !u!114 &1134039335 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1134039332} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10907, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &1134039336 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1134039332} + m_CullTransparentMesh: 1 --- !u!1 &1136971053 GameObject: m_ObjectHideFlags: 0 @@ -2855,10 +3135,10 @@ RectTransform: m_Father: {fileID: 131784669} m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 0, y: 0} - m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 0, y: 250} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 0, y: -250} + m_SizeDelta: {x: 782.99994, y: 250} m_Pivot: {x: 0, y: 0} --- !u!114 &1180904221 MonoBehaviour: @@ -2919,10 +3199,10 @@ RectTransform: m_Father: {fileID: 1180904220} m_RootOrder: 1 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 0, y: 0} - m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 0, y: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 587.24994, y: -130} + m_SizeDelta: {x: 371.49997, y: 220} m_Pivot: {x: 0.5, y: 0.5} --- !u!114 &1221238573 MonoBehaviour: @@ -3430,10 +3710,10 @@ RectTransform: m_Father: {fileID: 1180904220} m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 0, y: 0} - m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 0, y: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 195.74998, y: -130} + m_SizeDelta: {x: 371.49997, y: 220} m_Pivot: {x: 0.5, y: 0.5} --- !u!114 &1363584101 MonoBehaviour: @@ -3495,6 +3775,7 @@ MonoBehaviour: m_Bitrate: min: 8 max: 208 + m_Loopback: 0 --- !u!1 &1423621363 GameObject: m_ObjectHideFlags: 0 @@ -3528,10 +3809,10 @@ RectTransform: m_Father: {fileID: 131784669} m_RootOrder: 1 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 0, y: 0} - m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 0, y: 200} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 391.49997, y: -350} + m_SizeDelta: {x: 782.99994, y: 200} m_Pivot: {x: 0.5, y: 0.5} --- !u!114 &1423621365 MonoBehaviour: @@ -4410,10 +4691,10 @@ RectTransform: m_Father: {fileID: 2134522315} m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 0, y: 0} - m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 0, y: 50} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 188.24998, y: -25} + m_SizeDelta: {x: 376.49997, y: 50} m_Pivot: {x: 0.5, y: 0.5} --- !u!114 &1859585770 MonoBehaviour: @@ -4549,6 +4830,7 @@ MonoBehaviour: renderStreaming: {fileID: 1915034402} webcamSelectDropdown: {fileID: 1859585770} microphoneSelectDropdown: {fileID: 2125586392} + audioLoopbackToggle: {fileID: 1134039334} startButton: {fileID: 1013695940} setUpButton: {fileID: 346362251} hangUpButton: {fileID: 2128695120} @@ -4692,6 +4974,82 @@ MonoBehaviour: m_SdpFmtpLine: m_ChannelCount: 0 m_SampleRate: 0 +--- !u!1 &2028247179 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2028247180} + - component: {fileID: 2028247182} + - component: {fileID: 2028247181} + m_Layer: 5 + m_Name: Background + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2028247180 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2028247179} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 640709221} + m_Father: {fileID: 1134039333} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 20, y: -15} + m_SizeDelta: {x: 20, y: 20} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &2028247181 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2028247179} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &2028247182 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2028247179} + m_CullTransparentMesh: 1 --- !u!1 &2063941924 GameObject: m_ObjectHideFlags: 0 @@ -4929,10 +5287,10 @@ RectTransform: m_Father: {fileID: 2134522315} m_RootOrder: 1 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 0, y: 0} - m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 0, y: 50} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 188.24998, y: -85} + m_SizeDelta: {x: 376.49997, y: 50} m_Pivot: {x: 0.5, y: 0.5} --- !u!114 &2125586392 MonoBehaviour: @@ -5065,10 +5423,10 @@ RectTransform: m_Father: {fileID: 932364532} m_RootOrder: 2 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 0, y: 0} - m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 0, y: 50} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 376.49997, y: -120} + m_SizeDelta: {x: 376.49997, y: 50} m_Pivot: {x: 1, y: 1} --- !u!114 &2128695120 MonoBehaviour: @@ -5182,14 +5540,15 @@ RectTransform: m_Children: - {fileID: 1859585769} - {fileID: 2125586391} + - {fileID: 1134039333} - {fileID: 1013695939} m_Father: {fileID: 1423621364} m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 0, y: 0} - m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 0, y: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 10, y: -220} + m_SizeDelta: {x: 376.49997, y: 210} m_Pivot: {x: 0, y: 0} --- !u!114 &2134522316 MonoBehaviour: diff --git a/Samples~/Example/Bidirectional/BidirectionalSample.cs b/Samples~/Example/Bidirectional/BidirectionalSample.cs index bac22e8..404c1ac 100644 --- a/Samples~/Example/Bidirectional/BidirectionalSample.cs +++ b/Samples~/Example/Bidirectional/BidirectionalSample.cs @@ -10,6 +10,7 @@ class BidirectionalSample : MonoBehaviour [SerializeField] private SignalingManager renderStreaming; [SerializeField] private Dropdown webcamSelectDropdown; [SerializeField] private Dropdown microphoneSelectDropdown; + [SerializeField] private Toggle audioLoopbackToggle; [SerializeField] private Button startButton; [SerializeField] private Button setUpButton; [SerializeField] private Button hangUpButton; @@ -52,6 +53,12 @@ void Awake() webCamStreamer.OnStartedStream += id => receiveVideoViewer.enabled = true; webCamStreamer.OnStartedStream += _ => localVideoImage.texture = webCamStreamer.sourceWebCamTexture; + audioLoopbackToggle.onValueChanged.AddListener(isOn => + { + microphoneStreamer.loopback = isOn; + }); + microphoneStreamer.OnStartedStream += id => microphoneStreamer.loopback = audioLoopbackToggle.isOn; + settings = SampleManager.Instance.Settings; if (settings != null) { @@ -88,7 +95,8 @@ private void SetUp() setUpButton.interactable = false; hangUpButton.interactable = true; connectionIdInput.interactable = false; - if(settings != null) + + if (settings != null) { receiveVideoViewer.SetCodec(settings.ReceiverVideoCodec); webCamStreamer.SetCodec(settings.SenderVideoCodec); diff --git a/Samples~/Example/Broadcast/BroadcastSample.cs b/Samples~/Example/Broadcast/BroadcastSample.cs index 698ddd8..e829aed 100644 --- a/Samples~/Example/Broadcast/BroadcastSample.cs +++ b/Samples~/Example/Broadcast/BroadcastSample.cs @@ -2,10 +2,10 @@ using System.Collections.Generic; using System.Linq; using UnityEngine; -using UnityEngine.UI; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Layouts; using UnityEngine.InputSystem.XR; +using UnityEngine.UI; namespace Unity.RenderStreaming.Samples { @@ -120,27 +120,27 @@ private void Awake() videoSourceTypeSelector.onValueChanged.AddListener(ChangeVideoSourceType); bandwidthSelector.options = bandwidthOptions - .Select(pair => new Dropdown.OptionData {text = pair.Key}) + .Select(pair => new Dropdown.OptionData { text = pair.Key }) .ToList(); - bandwidthSelector.options.Add(new Dropdown.OptionData {text = "Custom"}); + bandwidthSelector.options.Add(new Dropdown.OptionData { text = "Custom" }); bandwidthSelector.onValueChanged.AddListener(ChangeBandwidth); scaleResolutionDownSelector.options = scaleResolutionDownOptions - .Select(pair => new Dropdown.OptionData {text = pair.Key}) + .Select(pair => new Dropdown.OptionData { text = pair.Key }) .ToList(); - scaleResolutionDownSelector.options.Add(new Dropdown.OptionData {text = "Custom"}); + scaleResolutionDownSelector.options.Add(new Dropdown.OptionData { text = "Custom" }); scaleResolutionDownSelector.onValueChanged.AddListener(ChangeScaleResolutionDown); framerateSelector.options = framerateOptions - .Select(pair => new Dropdown.OptionData {text = pair.Key}) + .Select(pair => new Dropdown.OptionData { text = pair.Key }) .ToList(); - framerateSelector.options.Add(new Dropdown.OptionData {text = "Custom"}); + framerateSelector.options.Add(new Dropdown.OptionData { text = "Custom" }); framerateSelector.onValueChanged.AddListener(ChangeFramerate); resolutionSelector.options = resolutionOptions - .Select(pair => new Dropdown.OptionData {text = pair.Key}) + .Select(pair => new Dropdown.OptionData { text = pair.Key }) .ToList(); - resolutionSelector.options.Add(new Dropdown.OptionData {text = "Custom"}); + resolutionSelector.options.Add(new Dropdown.OptionData { text = "Custom" }); resolutionSelector.onValueChanged.AddListener(ChangeResolution); } @@ -183,7 +183,7 @@ private void Start() if (renderStreaming.runOnAwake) return; - if(settings != null) + if (settings != null) renderStreaming.useDefaultSettings = settings.UseDefaultSettings; if (settings?.SignalingSettings != null) renderStreaming.SetSignalingSettings(settings.SignalingSettings); @@ -272,7 +272,7 @@ private void SyncDisplayVideoSenderParameters() framerateSelector.SetValueWithoutNotify(framerateIndex); } - var target = new Vector2Int((int) videoStreamSender.width, (int) videoStreamSender.height); + var target = new Vector2Int((int)videoStreamSender.width, (int)videoStreamSender.height); var resolutionIndex = Array.IndexOf(resolutionOptions.Values.ToArray(), target); if (resolutionIndex < 0) { diff --git a/Samples~/Example/Broadcast/SimpleCameraControllerV2.cs b/Samples~/Example/Broadcast/SimpleCameraControllerV2.cs index 8af6f02..594c473 100644 --- a/Samples~/Example/Broadcast/SimpleCameraControllerV2.cs +++ b/Samples~/Example/Broadcast/SimpleCameraControllerV2.cs @@ -100,15 +100,15 @@ void OnDeviceChange(InputDevice device, InputDeviceChange change) switch (change) { case InputDeviceChange.Added: - { - playerInput.PerformPairingWithDevice(device); - return; - } + { + playerInput.PerformPairingWithDevice(device); + return; + } case InputDeviceChange.Removed: - { - playerInput.UnpairDevices(device); - return; - } + { + playerInput.UnpairDevices(device); + return; + } } } @@ -121,7 +121,7 @@ private void OnEnable() private void FixedUpdate() { // Tracked Device - if(inputPosition.HasValue && inputRotation.HasValue) + if (inputPosition.HasValue && inputRotation.HasValue) { transform.position = inputPosition.Value; transform.rotation = inputRotation.Value; diff --git a/Samples~/Example/Broadcast/UIControllerV2.cs b/Samples~/Example/Broadcast/UIControllerV2.cs index bd33bba..7f71371 100644 --- a/Samples~/Example/Broadcast/UIControllerV2.cs +++ b/Samples~/Example/Broadcast/UIControllerV2.cs @@ -1,6 +1,6 @@ using UnityEngine; -using UnityEngine.UI; using UnityEngine.InputSystem; +using UnityEngine.UI; namespace Unity.RenderStreaming.Samples { @@ -11,7 +11,8 @@ class UIControllerV2 : MonoBehaviour [SerializeField] CanvasGroup canvasGroup; [SerializeField] Image pointer; [SerializeField] GameObject noticeTouchControl; - [SerializeField] private AnimationCurve transitionCurve = + [SerializeField] + private AnimationCurve transitionCurve = new AnimationCurve( new Keyframe(0.75f, 1f, 0f, 0f), new Keyframe(1f, 0f, 0f, 0f)); @@ -45,7 +46,7 @@ public void OnPressAnyKey(InputAction.CallbackContext context) { var keyboard = context.control.device as Keyboard; - if(!isSubscribing) + if (!isSubscribing) { keyboard.onTextInput += OnTextInput; isSubscribing = true; @@ -61,7 +62,7 @@ void OnTextInput(char c) public void OnPoint(InputAction.CallbackContext context) { - if(m_rectTransform == null) + if (m_rectTransform == null) return; var position = context.ReadValue(); var screenSize = new Vector2Int(Screen.width, Screen.height); diff --git a/Samples~/Example/Gyro/GyroSample.cs b/Samples~/Example/Gyro/GyroSample.cs index 2e0deb2..75293c5 100644 --- a/Samples~/Example/Gyro/GyroSample.cs +++ b/Samples~/Example/Gyro/GyroSample.cs @@ -1,7 +1,7 @@ using UnityEngine; -using UnityEngine.UI; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Controls; +using UnityEngine.UI; using Gyroscope = UnityEngine.InputSystem.Gyroscope; namespace Unity.RenderStreaming.Samples @@ -11,21 +11,21 @@ namespace Unity.RenderStreaming.Samples class GyroSample : MonoBehaviour { #pragma warning disable 0649 - [SerializeField] private SignalingManager renderStreaming; - [SerializeField] private Button sendOfferButton; - [SerializeField] private RawImage remoteVideoImage; - [SerializeField] private VideoStreamReceiver receiveVideoViewer; - [SerializeField] private SingleConnection connection; - [SerializeField] private Text textVelocityX; - [SerializeField] private Text textVelocityY; - [SerializeField] private Text textVelocityZ; - [SerializeField] private InputAction vector3Action; + [SerializeField] private SignalingManager renderStreaming; + [SerializeField] private Button sendOfferButton; + [SerializeField] private RawImage remoteVideoImage; + [SerializeField] private VideoStreamReceiver receiveVideoViewer; + [SerializeField] private SingleConnection connection; + [SerializeField] private Text textVelocityX; + [SerializeField] private Text textVelocityY; + [SerializeField] private Text textVelocityZ; + [SerializeField] private InputAction vector3Action; #pragma warning restore 0649 private RenderStreamingSettings settings; void Awake() { - if(Gyroscope.current != null) + if (Gyroscope.current != null) InputSystem.EnableDevice(Gyroscope.current); else Debug.LogError("Gyroscope is not supported on this device."); @@ -82,7 +82,7 @@ private void UpdateVector3(InputAction.CallbackContext context) void SendOffer() { - if(settings != null) + if (settings != null) receiveVideoViewer.SetCodec(settings.ReceiverVideoCodec); var connectionId = System.Guid.NewGuid().ToString("N"); diff --git a/Samples~/Example/Menu/Menu.unity b/Samples~/Example/Menu/Menu.unity index 4008e24..08f5d35 100644 --- a/Samples~/Example/Menu/Menu.unity +++ b/Samples~/Example/Menu/Menu.unity @@ -1590,8 +1590,6 @@ MonoBehaviour: m_Image: {fileID: 0} - m_Text: Http m_Image: {fileID: 0} - - m_Text: Furioos - m_Image: {fileID: 0} m_OnValueChanged: m_PersistentCalls: m_Calls: [] diff --git a/Samples~/Example/Multiplay/FollowTransform.cs b/Samples~/Example/Multiplay/FollowTransform.cs index 61c6f33..695c8c2 100644 --- a/Samples~/Example/Multiplay/FollowTransform.cs +++ b/Samples~/Example/Multiplay/FollowTransform.cs @@ -14,7 +14,7 @@ private void Update() { if (followPosition) transform.localPosition = target.localPosition + offset; - if(followRotation) + if (followRotation) transform.localRotation = target.localRotation; } } diff --git a/Samples~/Example/Multiplay/Multiplay.cs b/Samples~/Example/Multiplay/Multiplay.cs index 4af2d82..b96591c 100644 --- a/Samples~/Example/Multiplay/Multiplay.cs +++ b/Samples~/Example/Multiplay/Multiplay.cs @@ -1,5 +1,5 @@ -using System.Linq; using System.Collections.Generic; +using System.Linq; using UnityEngine; namespace Unity.RenderStreaming.Samples diff --git a/Samples~/Example/Multiplay/MultiplayChannel.cs b/Samples~/Example/Multiplay/MultiplayChannel.cs index 567129e..a242093 100644 --- a/Samples~/Example/Multiplay/MultiplayChannel.cs +++ b/Samples~/Example/Multiplay/MultiplayChannel.cs @@ -20,7 +20,7 @@ class Message /// /// [Serializable] - class ChangeLabelEvent : UnityEvent {}; + class ChangeLabelEvent : UnityEvent { }; /// /// @@ -33,7 +33,7 @@ protected override void OnMessage(byte[] bytes) { string str = System.Text.Encoding.UTF8.GetString(bytes); var message = JsonUtility.FromJson(str); - switch(message.type) + switch (message.type) { case ActionType.ChangeLabel: OnChangeLabel?.Invoke(message.argument); diff --git a/Samples~/Example/Multiplay/MultiplaySample.cs b/Samples~/Example/Multiplay/MultiplaySample.cs index 70fceb5..77b2585 100644 --- a/Samples~/Example/Multiplay/MultiplaySample.cs +++ b/Samples~/Example/Multiplay/MultiplaySample.cs @@ -91,7 +91,7 @@ void SetUpHost(string username) renderStreaming.useDefaultSettings = settings.UseDefaultSettings; if (settings?.SignalingSettings != null) renderStreaming.SetSignalingSettings(settings.SignalingSettings); - renderStreaming.Run(handlers: new SignalingHandlerBase[] {handler}); + renderStreaming.Run(handlers: new SignalingHandlerBase[] { handler }); } IEnumerator SetUpGuest(string username, string connectionId) @@ -104,7 +104,7 @@ IEnumerator SetUpGuest(string username, string connectionId) renderStreaming.useDefaultSettings = settings.UseDefaultSettings; if (settings?.SignalingSettings != null) renderStreaming.SetSignalingSettings(settings.SignalingSettings); - renderStreaming.Run(handlers: new SignalingHandlerBase[] {handler}); + renderStreaming.Run(handlers: new SignalingHandlerBase[] { handler }); videoImage.gameObject.SetActive(true); var receiveVideoViewer = guestPlayer.GetComponent(); @@ -113,7 +113,7 @@ IEnumerator SetUpGuest(string username, string connectionId) var channel = guestPlayer.GetComponent(); channel.OnStartedChannel += _ => { StartCoroutine(ChangeLabel(channel, username)); }; - if(settings != null) + if (settings != null) receiveVideoViewer.SetCodec(settings.ReceiverVideoCodec); // todo(kazuki): diff --git a/Samples~/Example/Multiplay/PlayerController.cs b/Samples~/Example/Multiplay/PlayerController.cs index e2021f7..00d375a 100644 --- a/Samples~/Example/Multiplay/PlayerController.cs +++ b/Samples~/Example/Multiplay/PlayerController.cs @@ -1,6 +1,6 @@ +using System.Linq; using UnityEngine; using UnityEngine.InputSystem; -using System.Linq; namespace Unity.RenderStreaming.Samples { @@ -23,7 +23,7 @@ class PlayerController : MonoBehaviour Vector2 inputLook; Vector3 initialPosition; bool inputJump; - float cooldownJumpDelta = CooldownJump; + float cooldownJumpDelta = CooldownJump; protected void Awake() { @@ -36,17 +36,17 @@ void OnDeviceChange(InputDevice device, InputDeviceChange change) switch (change) { case InputDeviceChange.Added: - { - playerInput.PerformPairingWithDevice(device); - CheckPairedDevices(); - return; - } + { + playerInput.PerformPairingWithDevice(device); + CheckPairedDevices(); + return; + } case InputDeviceChange.Removed: - { - playerInput.UnpairDevices(device); - CheckPairedDevices(); - return; - } + { + playerInput.UnpairDevices(device); + CheckPairedDevices(); + return; + } } } diff --git a/Samples~/Example/Receiver/AudioSpectrumView.cs b/Samples~/Example/Receiver/AudioSpectrumView.cs index 6c89026..6529c12 100644 --- a/Samples~/Example/Receiver/AudioSpectrumView.cs +++ b/Samples~/Example/Receiver/AudioSpectrumView.cs @@ -37,7 +37,7 @@ void Start() array = new Vector3[positionCount]; // This line object is used as a template. - if(line.gameObject.activeInHierarchy) + if (line.gameObject.activeInHierarchy) line.gameObject.SetActive(false); AudioSettings.OnAudioConfigurationChanged += OnAudioConfigurationChanged; @@ -71,7 +71,7 @@ void Update() { if (target.clip == null) { - if(lines.Count > 0) + if (lines.Count > 0) ResetLines(0); clip = null; return; diff --git a/Samples~/Example/Receiver/ReceiverSample.cs b/Samples~/Example/Receiver/ReceiverSample.cs index b74a145..dd75197 100644 --- a/Samples~/Example/Receiver/ReceiverSample.cs +++ b/Samples~/Example/Receiver/ReceiverSample.cs @@ -50,7 +50,7 @@ void Awake() { startButton.onClick.AddListener(OnStart); stopButton.onClick.AddListener(OnStop); - if(connectionIdInput != null) + if (connectionIdInput != null) connectionIdInput.onValueChanged.AddListener(input => connectionId = input); receiveVideoViewer.OnUpdateReceiveTexture += OnUpdateReceiveTexture; @@ -122,7 +122,7 @@ private void OnStart() connectionIdInput.text = connectionId; } connectionIdInput.interactable = false; - if(settings != null) + if (settings != null) receiveVideoViewer.SetCodec(settings.ReceiverVideoCodec); receiveAudioViewer.targetAudioSource = remoteAudioSource; diff --git a/Samples~/Example/Scripts/BackButton.cs b/Samples~/Example/Scripts/BackButton.cs index 71a8d75..9a73967 100644 --- a/Samples~/Example/Scripts/BackButton.cs +++ b/Samples~/Example/Scripts/BackButton.cs @@ -1,6 +1,6 @@ using UnityEngine; -using UnityEngine.SceneManagement; using UnityEngine.InputSystem; +using UnityEngine.SceneManagement; namespace Unity.RenderStreaming.Samples { diff --git a/Samples~/Example/Scripts/SampleManager.cs b/Samples~/Example/Scripts/SampleManager.cs index f6877f6..8e19c46 100644 --- a/Samples~/Example/Scripts/SampleManager.cs +++ b/Samples~/Example/Scripts/SampleManager.cs @@ -26,7 +26,7 @@ public RenderStreamingSettings Settings public void Initialize() { - if(m_settings == null) + if (m_settings == null) m_settings = new RenderStreamingSettings(); } } diff --git a/Samples~/Example/Scripts/SceneSelectUI.cs b/Samples~/Example/Scripts/SceneSelectUI.cs index 74da155..0af6bb4 100644 --- a/Samples~/Example/Scripts/SceneSelectUI.cs +++ b/Samples~/Example/Scripts/SceneSelectUI.cs @@ -18,7 +18,6 @@ internal enum SignalingType { WebSocket, Http, - Furioos } internal class RenderStreamingSettings @@ -26,7 +25,7 @@ internal class RenderStreamingSettings public const int DefaultStreamWidth = 1280; public const int DefaultStreamHeight = 720; - private bool useDefaultSettings = false; + private bool useDefaultSettings = true; private SignalingType signalingType = SignalingType.WebSocket; private string signalingAddress = "localhost"; private int signalingInterval = 5000; @@ -71,31 +70,23 @@ public SignalingSettings SignalingSettings { switch (signalingType) { - case SignalingType.Furioos: - { - var schema = signalingSecured ? "https" : "http"; - return new FurioosSignalingSettings - ( - url: $"{schema}://{signalingAddress}" - ); - } case SignalingType.WebSocket: - { - var schema = signalingSecured ? "wss" : "ws"; - return new WebSocketSignalingSettings - ( - url: $"{schema}://{signalingAddress}" - ); - } + { + var schema = signalingSecured ? "wss" : "ws"; + return new WebSocketSignalingSettings + ( + url: $"{schema}://{signalingAddress}" + ); + } case SignalingType.Http: - { - var schema = signalingSecured ? "https" : "http"; - return new HttpSignalingSettings - ( - url: $"{schema}://{signalingAddress}", - interval: signalingInterval - ); - } + { + var schema = signalingSecured ? "https" : "http"; + return new HttpSignalingSettings + ( + url: $"{schema}://{signalingAddress}", + interval: signalingInterval + ); + } } throw new InvalidOperationException(); } @@ -178,7 +169,7 @@ static string CodecTitle(VideoCodecInfo codec) void Start() { SampleManager.Instance.Initialize(); - settings = SampleManager.Instance.Settings; + settings = SampleManager.Instance.Settings; toggleUseDefaultSettings.isOn = settings.UseDefaultSettings; dropdownSignalingType.value = (int)settings.SignalingType; diff --git a/Samples~/Example/Stats/ShowStatsUI.cs b/Samples~/Example/Stats/ShowStatsUI.cs index 66d3b05..d0f2f55 100644 --- a/Samples~/Example/Stats/ShowStatsUI.cs +++ b/Samples~/Example/Stats/ShowStatsUI.cs @@ -115,7 +115,7 @@ private IEnumerator CollectStats() var coroutine = StartCoroutine(UpdateStats(receiver)); coroutines.Add(coroutine); } - foreach(var coroutine in coroutines) + foreach (var coroutine in coroutines) { yield return coroutine; } @@ -247,7 +247,7 @@ private void SetUpReceiverBase(StreamReceiverBase receiverBase) receiverBase.OnStartedStream += id => { - if(activeReceiverList.TryGetValue(receiverBase, out var hashSet)) + if (activeReceiverList.TryGetValue(receiverBase, out var hashSet)) { hashSet.Add(receiverBase.Transceiver.Receiver); } diff --git a/Samples~/Example/WebBrowserInput/SimpleCameraController.cs b/Samples~/Example/WebBrowserInput/SimpleCameraController.cs index 6f29524..14c9b94 100644 --- a/Samples~/Example/WebBrowserInput/SimpleCameraController.cs +++ b/Samples~/Example/WebBrowserInput/SimpleCameraController.cs @@ -74,7 +74,7 @@ public void UpdateTransform(Transform t) [Header("Movement Settings")] [Tooltip("Movement Sensitivity Factor."), Range(0.001f, 1f)] - [SerializeField] float m_movementSensitivityFactor = 0.1f; + [SerializeField] float m_movementSensitivityFactor = 0.1f; [Tooltip("Exponential boost factor on translation, controllable by mouse wheel.")] [SerializeField] @@ -140,7 +140,7 @@ void OnDeviceChange(InputDevice device, InputDeviceChange change) } } - void SetDevice(InputDevice device, bool add=true) + void SetDevice(InputDevice device, bool add = true) { uiController?.SetDevice(device, add); @@ -159,13 +159,13 @@ void SetDevice(InputDevice device, bool add=true) listKeyboard.Remove(keyboard); return; case Touchscreen screen: - if(add) + if (add) listScreen.Add(screen); else listScreen.Remove(screen); return; case Gamepad pad: - if(add) + if (add) listGamepad.Add(pad); else listGamepad.Remove(pad); @@ -199,9 +199,11 @@ void OnEnable() m_InterpolatingCameraState.SetFromTransform(transform); } -//--------------------------------------------------------------------------------------------------------------------- - Vector3 GetTranslationFromInput(Vector2 input) { - if (!invertY) { + //--------------------------------------------------------------------------------------------------------------------- + Vector3 GetTranslationFromInput(Vector2 input) + { + if (!invertY) + { input.y *= -1; } @@ -211,11 +213,12 @@ Vector3 GetTranslationFromInput(Vector2 input) { return dir; } -//--------------------------------------------------------------------------------------------------------------------- + //--------------------------------------------------------------------------------------------------------------------- void UpdateTargetCameraStateFromInput(Vector2 input) { - if (!invertY) { + if (!invertY) + { input.y *= -1; } float mouseSensitivityFactor = mouseSensitivityCurve.Evaluate(input.magnitude); @@ -224,7 +227,7 @@ void UpdateTargetCameraStateFromInput(Vector2 input) m_TargetCameraState.pitch += input.y * mouseSensitivityFactor; } -//--------------------------------------------------------------------------------------------------------------------- + //--------------------------------------------------------------------------------------------------------------------- Vector3 GetInputTranslationDirection() { @@ -392,14 +395,17 @@ void ResetCamera() } -//--------------------------------------------------------------------------------------------------------------------- - static bool IsMouseDragged(Mouse m, bool useLeftButton) { + //--------------------------------------------------------------------------------------------------------------------- + static bool IsMouseDragged(Mouse m, bool useLeftButton) + { if (null == m) return false; - if (Screen.safeArea.Contains(m.position.ReadValue())) { + if (Screen.safeArea.Contains(m.position.ReadValue())) + { //check left/right click - if ((useLeftButton && m.leftButton.isPressed) || (!useLeftButton && m.rightButton.isPressed)) { + if ((useLeftButton && m.leftButton.isPressed) || (!useLeftButton && m.rightButton.isPressed)) + { return true; } } diff --git a/Samples~/Example/WebBrowserInput/UIController.cs b/Samples~/Example/WebBrowserInput/UIController.cs index 4ae2c6f..46703bf 100644 --- a/Samples~/Example/WebBrowserInput/UIController.cs +++ b/Samples~/Example/WebBrowserInput/UIController.cs @@ -1,7 +1,7 @@ using System.Linq; using UnityEngine; -using UnityEngine.UI; using UnityEngine.InputSystem; +using UnityEngine.UI; namespace Unity.RenderStreaming.Samples { @@ -12,7 +12,8 @@ class UIController : MonoBehaviour [SerializeField] CanvasGroup canvasGroup; [SerializeField] Image pointer; [SerializeField] GameObject noticeTouchControl; - [SerializeField] private AnimationCurve transitionCurve = + [SerializeField] + private AnimationCurve transitionCurve = new AnimationCurve( new Keyframe(0.75f, 1f, 0f, 0f), new Keyframe(1f, 0f, 0f, 0f)); @@ -94,7 +95,7 @@ void FixedUpdate() } } -//---------------------------------------------------------------------------------------------------------------------- + //---------------------------------------------------------------------------------------------------------------------- bool HighlightPointerFromMouse(Mouse mouse, Vector2Int screenSize) { if (mouse == null) diff --git a/Samples~/Example/WebBrowserInput/WebBrowserInputChannelReceiver/RemoteInput.cs b/Samples~/Example/WebBrowserInput/WebBrowserInputChannelReceiver/RemoteInput.cs index 819e57c..601b2fc 100644 --- a/Samples~/Example/WebBrowserInput/WebBrowserInputChannelReceiver/RemoteInput.cs +++ b/Samples~/Example/WebBrowserInput/WebBrowserInputChannelReceiver/RemoteInput.cs @@ -1,11 +1,11 @@ using System; +using System.Collections.Generic; +using System.Linq; using UnityEngine.InputSystem; -using UnityEngine.InputSystem.Users; -using UnityEngine.InputSystem.LowLevel; using UnityEngine.InputSystem.EnhancedTouch; +using UnityEngine.InputSystem.LowLevel; +using UnityEngine.InputSystem.Users; using UE = UnityEngine; -using System.Collections.Generic; -using System.Linq; namespace Unity.RenderStreaming.Samples { @@ -26,14 +26,16 @@ enum EventType Gamepad = 5, } - enum GamepadEventType { - ButtonUp = 0, - ButtonDown = 1, - ButtonPressed = 2, - Axis = 3 + enum GamepadEventType + { + ButtonUp = 0, + ButtonDown = 1, + ButtonPressed = 2, + Axis = 3 } - enum GamepadKeyCode { + enum GamepadKeyCode + { Button0 = 0, Button1, Button2, @@ -161,13 +163,13 @@ public void Dispose() GC.SuppressFinalize(this); } -//--------------------------------------------------------------------------------------------------------------------- + //--------------------------------------------------------------------------------------------------------------------- public void ProcessInput(byte[] bytes) { if (bytes == null) throw new ArgumentNullException(); - if(bytes.Length == 0) + if (bytes.Length == 0) throw new ArgumentException("byte length is zero"); switch ((EventType)bytes[0]) @@ -222,7 +224,7 @@ public void ProcessInput(byte[] bytes) ChangeEndStateUnusedTouches(touches); } } - + break; case EventType.ButtonClick: var elementId = BitConverter.ToInt16(bytes, 1); @@ -231,7 +233,7 @@ public void ProcessInput(byte[] bytes) case EventType.Gamepad: { GamepadEventType gamepadEventType = (GamepadEventType)bytes[1]; - + switch (gamepadEventType) { case GamepadEventType.ButtonDown: @@ -240,7 +242,7 @@ public void ProcessInput(byte[] bytes) { var buttonIndex = bytes[2]; var value = BitConverter.ToDouble(bytes, 3); - ProcessGamepadButtonEvent(gamepadEventType, (GamepadKeyCode) buttonIndex, value); + ProcessGamepadButtonEvent(gamepadEventType, (GamepadKeyCode)buttonIndex, value); } break; case GamepadEventType.Axis: @@ -248,7 +250,7 @@ public void ProcessInput(byte[] bytes) var buttonIndex = bytes[2]; var x = BitConverter.ToDouble(bytes, 3); var y = BitConverter.ToDouble(bytes, 11); - ProcessGamepadAxisEvent(x, y, (GamepadKeyCode) buttonIndex); + ProcessGamepadAxisEvent(x, y, (GamepadKeyCode)buttonIndex); } break; } @@ -263,7 +265,7 @@ void ProcessGamepadButtonEvent(GamepadEventType state, GamepadKeyCode buttonInde { GamepadButton buttonToUpdate = GamepadButton.DpadUp; GamepadState gamepadState = m_gamepadState; - switch(buttonIndex) + switch (buttonIndex) { case GamepadKeyCode.DpadUp: buttonToUpdate = GamepadButton.DpadUp; @@ -301,17 +303,17 @@ void ProcessGamepadButtonEvent(GamepadEventType state, GamepadKeyCode buttonInde break; case GamepadKeyCode.Button7: buttonToUpdate = GamepadButton.RightTrigger; - gamepadState.rightTrigger = (float) value; + gamepadState.rightTrigger = (float)value; break; case GamepadKeyCode.Axis0Button: buttonToUpdate = GamepadButton.LeftStick; break; case GamepadKeyCode.Axis1Button: buttonToUpdate = GamepadButton.RightStick; - break; + break; default: UE.Debug.Log("Unmapped button code: " + buttonIndex); - break; + break; } m_gamepadState = gamepadState.WithButton(buttonToUpdate, GamepadEventType.ButtonDown == state || GamepadEventType.ButtonPressed == state); } @@ -319,17 +321,17 @@ void ProcessGamepadButtonEvent(GamepadEventType state, GamepadKeyCode buttonInde void ProcessGamepadAxisEvent(double x, double y, GamepadKeyCode axisKeyCode) { GamepadState gamepadState = m_gamepadState; - if(axisKeyCode == GamepadKeyCode.Axis0) + if (axisKeyCode == GamepadKeyCode.Axis0) gamepadState.leftStick = new UE.Vector2((float)x, (float)y); - if(axisKeyCode == GamepadKeyCode.Axis1) + if (axisKeyCode == GamepadKeyCode.Axis1) gamepadState.rightStick = new UE.Vector2((float)x, (float)y); m_gamepadState = gamepadState; } -#endregion + #endregion void ProcessKeyEvent(KeyboardEventType state, bool repeat, byte keyCode, char character) { - switch(state) + switch (state) { case KeyboardEventType.KeyDown: if (!repeat) @@ -337,7 +339,7 @@ void ProcessKeyEvent(KeyboardEventType state, bool repeat, byte keyCode, char ch m_keyboardState.Set((Key)keyCode, true); UnityInputSystem.QueueStateEvent(RemoteKeyboard, m_keyboardState); } - if(character != 0) + if (character != 0) { UnityInputSystem.QueueTextEvent(RemoteKeyboard, character); } @@ -353,7 +355,8 @@ void ProcessMouseMoveEvent(short x, short y, byte button) { UnityEngine.Vector2Int pos = new UnityEngine.Vector2Int(x, y); UnityEngine.Vector2Int delta = pos - m_prevMousePos; - UnityInputSystem.QueueStateEvent(RemoteMouse, new MouseState { + UnityInputSystem.QueueStateEvent(RemoteMouse, new MouseState + { position = pos, delta = delta, buttons = button diff --git a/Tests/Editor/EditorTest.cs b/Tests/Editor/EditorTest.cs index 5d3d2ea..55077b1 100644 --- a/Tests/Editor/EditorTest.cs +++ b/Tests/Editor/EditorTest.cs @@ -1,11 +1,11 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; -using UnityEngine; +using NUnit.Framework.Interfaces; using UnityEditor; using UnityEditor.TestTools; +using UnityEngine; using UnityEngine.TestRunner; -using NUnit.Framework.Interfaces; [assembly: TestPlayerBuildModifier(typeof(Unity.RenderStreaming.EditorTest.BuildModifier))] [assembly: TestRunCallback(typeof(Unity.RenderStreaming.EditorTest.TestListener))] @@ -40,7 +40,7 @@ public void RunStarted(ITest testsToRun) public void RunFinished(ITestResult testResults) { - if(temp != null) + if (temp != null) RenderStreaming.Settings = temp; } diff --git a/Tests/Editor/RenderStreamingTest.cs b/Tests/Editor/RenderStreamingTest.cs index 30bb655..73e091e 100644 --- a/Tests/Editor/RenderStreamingTest.cs +++ b/Tests/Editor/RenderStreamingTest.cs @@ -48,7 +48,7 @@ public void SignalingSettings() Assert.That(() => RenderStreamingEditor.SetSignalingSettings(null), Throws.ArgumentNullException); var url = "wss://127.0.0.1:8081"; - var iceServers = new IceServer[] {new IceServer(new string[] {"stun:stun.l.google.com:19302"})}; + var iceServers = new IceServer[] { new IceServer(new string[] { "stun:stun.l.google.com:19302" }) }; var signalingSettings = new WebSocketSignalingSettings(url, iceServers); Assert.That(() => RenderStreamingEditor.SetSignalingSettings(signalingSettings), Throws.Nothing); diff --git a/Tests/Editor/RequestJobTest.cs b/Tests/Editor/RequestJobTest.cs index 33b9ab3..aafd7d7 100644 --- a/Tests/Editor/RequestJobTest.cs +++ b/Tests/Editor/RequestJobTest.cs @@ -1,9 +1,9 @@ -using NUnit.Framework; //Timeout, Assert using System.Collections; //IEnumerator -using UnityEngine.TestTools; //UnityTest +using NUnit.Framework; //Timeout, Assert using Unity.RenderStreaming.Editor; //RequestJobManager -using UnityEditor.PackageManager.Requests; //ListRequest, AddRequest, etc using UnityEditor.PackageManager; //PackageCollection +using UnityEditor.PackageManager.Requests; //ListRequest, AddRequest, etc +using UnityEngine.TestTools; //UnityTest namespace Unity.RenderStreaming.EditorTest { diff --git a/Tests/Runtime/CommandLineParserTest.cs b/Tests/Runtime/CommandLineParserTest.cs index 3a460a8..3123500 100644 --- a/Tests/Runtime/CommandLineParserTest.cs +++ b/Tests/Runtime/CommandLineParserTest.cs @@ -1,6 +1,5 @@ using System.IO; using NUnit.Framework; -using Unity.RenderStreaming.Signaling; using Unity.WebRTC; using UnityEngine; using UnityEngine.TestTools; diff --git a/Tests/Runtime/DataTimeExtensionTest.cs b/Tests/Runtime/DataTimeExtensionTest.cs new file mode 100644 index 0000000..edd1e90 --- /dev/null +++ b/Tests/Runtime/DataTimeExtensionTest.cs @@ -0,0 +1,15 @@ +using System; +using NUnit.Framework; + +namespace Unity.RenderStreaming.RuntimeTest +{ + + public class DataTimeExtensionTest + { + [Test] + public void ToJsMilliseconds() + { + Assert.That(DateTime.Now.ToJsMilliseconds(), Is.GreaterThan(0)); + } + } +} diff --git a/Tests/Runtime/DataTimeExtensionTest.cs.meta b/Tests/Runtime/DataTimeExtensionTest.cs.meta new file mode 100644 index 0000000..a6d68ae --- /dev/null +++ b/Tests/Runtime/DataTimeExtensionTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2e4d23d9326e94e46ad84b32ceb0d7b2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/InputPositionCorrectorTest.cs b/Tests/Runtime/InputPositionCorrectorTest.cs index ff7d990..b67bfa6 100644 --- a/Tests/Runtime/InputPositionCorrectorTest.cs +++ b/Tests/Runtime/InputPositionCorrectorTest.cs @@ -16,7 +16,7 @@ void OnEvent(InputEventPtr ptr, InputDevice device) [Test] public void Invoke() { - System.Action< InputEventPtr, InputDevice> onEvent = OnEvent; + System.Action onEvent = OnEvent; var corrector = new InputPositionCorrector(onEvent); Assert.That(corrector.inputRegion, Is.EqualTo(Rect.zero)); Assert.That(corrector.outputRegion, Is.EqualTo(Rect.zero)); diff --git a/Tests/Runtime/InputSystem/InputDeviceExtensionTest.cs b/Tests/Runtime/InputSystem/InputDeviceExtensionTest.cs index 434209d..da998ff 100644 --- a/Tests/Runtime/InputSystem/InputDeviceExtensionTest.cs +++ b/Tests/Runtime/InputSystem/InputDeviceExtensionTest.cs @@ -1,8 +1,7 @@ using NUnit.Framework; +using Unity.RenderStreaming.InputSystem; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Layouts; -using Unity.RenderStreaming.InputSystem; - using Assert = NUnit.Framework.Assert; namespace Unity.RenderStreaming.RuntimeTest diff --git a/Tests/Runtime/InputSystem/InputRemotingTest.cs b/Tests/Runtime/InputSystem/InputRemotingTest.cs index 9b03cff..754a830 100644 --- a/Tests/Runtime/InputSystem/InputRemotingTest.cs +++ b/Tests/Runtime/InputSystem/InputRemotingTest.cs @@ -1,16 +1,16 @@ using System.Collections; using NUnit.Framework; -using UnityEngine.InputSystem; using Unity.RenderStreaming.InputSystem; using Unity.RenderStreaming.RuntimeTest.Signaling; using Unity.WebRTC; using UnityEngine; +using UnityEngine.InputSystem; using UnityEngine.TestTools; using Assert = NUnit.Framework.Assert; namespace Unity.RenderStreaming.RuntimeTest { - using InputRemoting = Unity.RenderStreaming.InputSystem.InputRemoting; + using InputRemoting = InputSystem.InputRemoting; class MessageSerializerTest { @@ -156,6 +156,7 @@ public void Sender() var sender = new Sender(); Assert.That(sender.layouts, Is.Not.Empty); Assert.That(sender.devices, Is.Not.Empty); + Assert.That(sender.GetDeviceById(0), Is.Null); var senderInput = new InputRemoting(sender); var senderDisposer = senderInput.Subscribe(new Observer(_channel1)); senderInput.StartSending(); @@ -170,6 +171,7 @@ public void Receiver() var receiver = new Receiver(_channel1); Assert.That(receiver.remoteDevices, Is.Empty); Assert.That(receiver.remoteLayouts, Is.Empty); + Assert.That(receiver.GetDeviceById(0), Is.Null); var receiverInput = new InputRemoting(receiver); var receiverDisposer = receiverInput.Subscribe(receiverInput); receiverInput.StartSending(); diff --git a/Tests/Runtime/MockLogger.cs b/Tests/Runtime/MockLogger.cs new file mode 100644 index 0000000..20246aa --- /dev/null +++ b/Tests/Runtime/MockLogger.cs @@ -0,0 +1,93 @@ +using System; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace Unity.RenderStreaming.RuntimeTest +{ + public class MockLogger : ILogger + { + public void LogFormat(LogType logType, Object context, string format, params object[] args) + { + throw new NotImplementedException(); + } + + public void LogException(Exception exception, Object context) + { + throw new NotImplementedException(); + } + + public bool IsLogTypeAllowed(LogType logType) + { + throw new NotImplementedException(); + } + + public void Log(LogType logType, object message) + { + throw new NotImplementedException(); + } + + public void Log(LogType logType, object message, Object context) + { + throw new NotImplementedException(); + } + + public void Log(LogType logType, string tag, object message) + { + throw new NotImplementedException(); + } + + public void Log(LogType logType, string tag, object message, Object context) + { + throw new NotImplementedException(); + } + + public void Log(object message) + { + throw new NotImplementedException(); + } + + public void Log(string tag, object message) + { + throw new NotImplementedException(); + } + + public void Log(string tag, object message, Object context) + { + throw new NotImplementedException(); + } + + public void LogWarning(string tag, object message) + { + throw new NotImplementedException(); + } + + public void LogWarning(string tag, object message, Object context) + { + throw new NotImplementedException(); + } + + public void LogError(string tag, object message) + { + throw new NotImplementedException(); + } + + public void LogError(string tag, object message, Object context) + { + throw new NotImplementedException(); + } + + public void LogFormat(LogType logType, string format, params object[] args) + { + throw new NotImplementedException(); + } + + public void LogException(Exception exception) + { + throw new NotImplementedException(); + } + + public ILogHandler logHandler { get; set; } + public bool logEnabled { get; set; } + public LogType filterLogType { get; set; } + } +} diff --git a/Tests/Runtime/MockLogger.cs.meta b/Tests/Runtime/MockLogger.cs.meta new file mode 100644 index 0000000..47fdf48 --- /dev/null +++ b/Tests/Runtime/MockLogger.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e2595de35a1ada64f923eed1a5e42aa3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/PrivateSignalingTest.cs b/Tests/Runtime/PrivateSignalingTest.cs index 5cab30f..9d80429 100644 --- a/Tests/Runtime/PrivateSignalingTest.cs +++ b/Tests/Runtime/PrivateSignalingTest.cs @@ -1,7 +1,7 @@ using System; using System.Collections; -using System.Threading; using System.Diagnostics; +using System.Threading; using NUnit.Framework; using Unity.RenderStreaming.RuntimeTest.Signaling; using Unity.RenderStreaming.Signaling; @@ -15,7 +15,7 @@ namespace Unity.RenderStreaming.RuntimeTest [TestFixture(typeof(WebSocketSignaling))] [TestFixture(typeof(HttpSignaling))] [TestFixture(typeof(MockSignaling))] - [UnityPlatform(exclude = new[] {RuntimePlatform.IPhonePlayer})] + [UnityPlatform(exclude = new[] { RuntimePlatform.IPhonePlayer })] [ConditionalIgnore(ConditionalIgnore.IL2CPP, "Process.Start does not implement in IL2CPP.")] class PrivateSignalingTest : IPrebuildSetup { @@ -162,7 +162,7 @@ public IEnumerator UnitySetUp() { RTCConfiguration config = default; RTCIceCandidate candidate_ = null; - config.iceServers = new[] {new RTCIceServer {urls = new[] {"stun:stun.l.google.com:19302"}}}; + config.iceServers = new[] { new RTCIceServer { urls = new[] { "stun:stun.l.google.com:19302" } } }; var peer1 = new RTCPeerConnection(ref config); var peer2 = new RTCPeerConnection(ref config); diff --git a/Tests/Runtime/RenderStreamingTest.cs b/Tests/Runtime/RenderStreamingTest.cs index 2101479..f3ccc03 100644 --- a/Tests/Runtime/RenderStreamingTest.cs +++ b/Tests/Runtime/RenderStreamingTest.cs @@ -28,6 +28,8 @@ public void TearDown() { RenderStreaming.Settings = temp; } + + RenderStreaming.Logger = Debug.unityLogger; } void IPrebuildSetup.Setup() @@ -91,5 +93,20 @@ public void AutomaticStreaming() Object.DestroyImmediate(settings); } + + [Test] + public void Logger() + { + Assert.NotNull(RenderStreaming.Logger); + Assert.AreEqual(RenderStreaming.Logger, Debug.unityLogger); + + Assert.That(() => RenderStreaming.Logger = null, Throws.ArgumentNullException); + + MockLogger logger = new MockLogger(); + Assert.That(() => RenderStreaming.Logger = logger, Throws.Nothing); + Assert.AreEqual(logger, RenderStreaming.Logger); + + Assert.That(() => RenderStreaming.Logger = Debug.unityLogger, Throws.Nothing); + } } } diff --git a/Tests/Runtime/Signaling/MockSignaling.cs b/Tests/Runtime/Signaling/MockSignaling.cs index 19874b1..4472e2f 100644 --- a/Tests/Runtime/Signaling/MockSignaling.cs +++ b/Tests/Runtime/Signaling/MockSignaling.cs @@ -24,34 +24,56 @@ interface IMockSignalingManager class MockPublicSignalingManager : IMockSignalingManager { - private List list = new List(); + private Dictionary> signalingToConnectionLookup = new Dictionary>(); + private Dictionary> connectionToSignalingLookup = new Dictionary>(); private const int MillisecondsDelay = 10; public async Task Add(MockSignaling signaling) { await Task.Delay(MillisecondsDelay); - list.Add(signaling); + signalingToConnectionLookup[signaling] = new HashSet(); + signaling.OnStart?.Invoke(signaling); } public async Task Remove(MockSignaling signaling) { await Task.Delay(MillisecondsDelay); - list.Remove(signaling); + + if (signalingToConnectionLookup.ContainsKey(signaling)) + { + foreach (var connectionId in signalingToConnectionLookup[signaling]) + { + foreach (var signalingToDisconnect in connectionToSignalingLookup[connectionId]) + { + signalingToDisconnect.OnDestroyConnection?.Invoke(signaling, connectionId); + } + + connectionToSignalingLookup.Remove(connectionId); + } + + signalingToConnectionLookup.Remove(signaling); + } } public async Task OpenConnection(MockSignaling signaling, string connectionId) { await Task.Delay(MillisecondsDelay); + addToLookups(signaling, connectionId); + signaling.OnCreateConnection?.Invoke(signaling, connectionId, true); } public async Task CloseConnection(MockSignaling signaling, string connectionId) { await Task.Delay(MillisecondsDelay); - foreach (var element in list) + + if (connectionToSignalingLookup.ContainsKey(connectionId)) { - element.OnDestroyConnection?.Invoke(element, connectionId); + foreach (var signalingToDisconnect in connectionToSignalingLookup[connectionId]) + { + signalingToDisconnect.OnDestroyConnection?.Invoke(signalingToDisconnect, connectionId); + } } } @@ -59,7 +81,7 @@ public async Task Offer(MockSignaling owner, DescData data) { await Task.Delay(MillisecondsDelay); data.polite = false; - foreach (var signaling in list.Where(e => e != owner)) + foreach (var signaling in signalingToConnectionLookup.Keys.Where(e => e != owner)) { signaling.OnOffer?.Invoke(signaling, data); } @@ -68,8 +90,9 @@ public async Task Offer(MockSignaling owner, DescData data) public async Task Answer(MockSignaling owner, DescData data) { await Task.Delay(MillisecondsDelay); - foreach (var signaling in list.Where(e => e != owner)) + foreach (var signaling in signalingToConnectionLookup.Keys.Where(e => e != owner)) { + addToLookups(owner, data.connectionId); signaling.OnAnswer?.Invoke(signaling, data); } } @@ -77,11 +100,28 @@ public async Task Answer(MockSignaling owner, DescData data) public async Task Candidate(MockSignaling owner, CandidateData data) { await Task.Delay(MillisecondsDelay); - foreach (var signaling in list.Where(e => e != owner)) + foreach (var signaling in signalingToConnectionLookup.Keys.Where(e => e != owner)) { signaling.OnIceCandidate?.Invoke(signaling, data); } } + + private void addToLookups(MockSignaling signaling, string connectionId) + { + if (!connectionToSignalingLookup.TryGetValue(connectionId, out var signalingSet)) + { + signalingSet = new HashSet(); + connectionToSignalingLookup[connectionId] = signalingSet; + } + signalingSet.Add(signaling); + + if (!signalingToConnectionLookup.TryGetValue(signaling, out var connectionSet)) + { + connectionSet = new HashSet(); + signalingToConnectionLookup[signaling] = connectionSet; + } + connectionSet.Add(connectionId); + } } class MockPrivateSignalingManager : IMockSignalingManager diff --git a/Tests/Runtime/SignalingEventProviderTest.cs b/Tests/Runtime/SignalingEventProviderTest.cs index 256e331..6d6bebd 100644 --- a/Tests/Runtime/SignalingEventProviderTest.cs +++ b/Tests/Runtime/SignalingEventProviderTest.cs @@ -1,7 +1,7 @@ using System; -using UnityEngine; using NUnit.Framework; using Unity.WebRTC; +using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.TestTools; diff --git a/Tests/Runtime/SignalingHandlerTest.cs b/Tests/Runtime/SignalingHandlerTest.cs index 11a16f2..aa2b16b 100644 --- a/Tests/Runtime/SignalingHandlerTest.cs +++ b/Tests/Runtime/SignalingHandlerTest.cs @@ -142,7 +142,7 @@ public void SetUp() //todo:: crash in dispose process on standalone linux [Test] [UnityPlatform(exclude = new[] { RuntimePlatform.LinuxPlayer })] - public void AddStreamSource() + public void AddAndRemoveStreamSource() { var container = TestContainer.Create("test"); var streamer = container.test.gameObject.AddComponent(); @@ -150,7 +150,11 @@ public void AddStreamSource() Assert.That(streamer.Track, Is.Null); Assert.That(streamer.Transceivers, Is.Empty); + Assert.That(container.test.component.Streams, Is.Empty); container.test.component.AddComponent(streamer); + Assert.That(container.test.component.Streams, Has.Count.EqualTo(1)); + container.test.component.RemoveComponent(streamer); + Assert.That(container.test.component.Streams, Is.Empty); container.Dispose(); } @@ -303,6 +307,152 @@ public IEnumerator SetCodec() container1.Dispose(); container2.Dispose(); } + + //todo:: crash in dispose process on standalone linux + [UnityTest, Timeout(10000)] + [UnityPlatform(exclude = new[] { RuntimePlatform.LinuxPlayer, RuntimePlatform.Android })] + public IEnumerator DataChannelDisconnectWithMultipleConnections() + { + // Ignore signaling log errors due to multiple connections + LogAssert.ignoreFailingMessages = true; + + string connectionId = "12345"; + string connectionId2 = "54321"; + var container1 = TestContainer.Create("test1"); + var container2 = TestContainer.Create("test2"); + var container4 = TestContainer.Create("test4"); + + var channel1 = container1.test.gameObject.AddComponent(); + bool isStartedChannel1 = false; + bool isStoppedChannel1 = false; + channel1.OnStartedChannel += _ => isStartedChannel1 = true; + channel1.OnStoppedChannel += _ => isStoppedChannel1 = true; + container1.test.component.AddComponent(channel1); + + container1.test.component.CreateConnection(connectionId); + yield return new WaitUntil(() => container1.test.component.ExistConnection(connectionId)); + + var channel2 = container2.test.gameObject.AddComponent(); + bool isStartedChannel2 = false; + bool isStoppedChannel2 = false; + channel2.OnStartedChannel += _ => isStartedChannel2 = true; + channel2.OnStoppedChannel += _ => isStoppedChannel2 = true; + + channel2.SetLocal(true); + channel2.SetLabel("test"); + + Assert.That(channel2.IsConnected, Is.False); + Assert.That(channel2.IsLocal, Is.True); + Assert.That(channel2.Label, Is.EqualTo("test")); + + container2.test.component.AddComponent(channel2); + container2.test.component.CreateConnection(connectionId); + yield return new WaitUntil(() => container2.test.component.ExistConnection(connectionId)); + yield return new WaitUntil(() => isStartedChannel1 && isStartedChannel2); + Assert.That(isStartedChannel1, Is.True); + Assert.That(isStartedChannel2, Is.True); + + Assert.That(channel1.IsLocal, Is.False); + Assert.That(channel1.Label, Is.EqualTo("test")); + Assert.That(channel1.IsConnected, Is.True); + Assert.That(channel1.Channel, Is.Not.Null); + Assert.That(channel1.ConnectionId, Is.EqualTo(connectionId)); + + Assert.That(channel2.IsConnected, Is.True); + Assert.That(channel2.Channel, Is.Not.Null); + Assert.That(channel2.ConnectionId, Is.EqualTo(connectionId)); + + var channel3 = container1.test.gameObject.AddComponent(); + bool isStartedChannel3 = false; + bool isStoppedChannel3 = false; + channel3.OnStartedChannel += _ => isStartedChannel3 = true; + channel3.OnStoppedChannel += _ => isStoppedChannel3 = true; + container1.test.component.AddComponent(channel3); + + container1.test.component.CreateConnection(connectionId2); + yield return new WaitUntil(() => container1.test.component.ExistConnection(connectionId2)); + + var channel4 = container4.test.gameObject.AddComponent(); + bool isStartedChannel4 = false; + bool isStoppedChannel4 = false; + channel4.OnStartedChannel += _ => isStartedChannel4 = true; + channel4.OnStoppedChannel += _ => isStoppedChannel4 = true; + + channel4.SetLocal(true); + channel4.SetLabel("test2"); + + Assert.That(channel4.IsConnected, Is.False); + Assert.That(channel4.IsLocal, Is.True); + Assert.That(channel4.Label, Is.EqualTo("test2")); + + container4.test.component.AddComponent(channel4); + container4.test.component.CreateConnection(connectionId2); + yield return new WaitUntil(() => container4.test.component.ExistConnection(connectionId2)); + yield return new WaitUntil(() => isStartedChannel3 && isStartedChannel4); + Assert.That(isStartedChannel3, Is.True); + Assert.That(isStartedChannel4, Is.True); + + Assert.That(channel3.IsLocal, Is.False); + Assert.That(channel3.Label, Is.EqualTo("test2")); + Assert.That(channel3.IsConnected, Is.True); + Assert.That(channel3.Channel, Is.Not.Null); + Assert.That(channel3.ConnectionId, Is.EqualTo(connectionId2)); + + Assert.That(channel4.IsConnected, Is.True); + Assert.That(channel4.Channel, Is.Not.Null); + Assert.That(channel4.ConnectionId, Is.EqualTo(connectionId2)); + + // send message from channel1 to channel2 + string sendMessage = "hello"; + string receivedMessage = null; + channel2.OnReceiveMessage = message => { receivedMessage = message; }; + channel1.Send(sendMessage); + yield return new WaitUntil(() => !string.IsNullOrEmpty(receivedMessage)); + Assert.That(receivedMessage, Is.EqualTo(sendMessage)); + + // send message from channel2 to channel1 + receivedMessage = null; + channel1.OnReceiveMessage = message => { receivedMessage = message; }; + channel2.Send(sendMessage); + yield return new WaitUntil(() => !string.IsNullOrEmpty(receivedMessage)); + Assert.That(receivedMessage, Is.EqualTo(sendMessage)); + + // send message from channel3 to channel4 + receivedMessage = null; + channel4.OnReceiveMessage = message => { receivedMessage = message; }; + channel3.Send(sendMessage); + yield return new WaitUntil(() => !string.IsNullOrEmpty(receivedMessage)); + Assert.That(receivedMessage, Is.EqualTo(sendMessage)); + + // send message from channel4 to channel3 + receivedMessage = null; + channel3.OnReceiveMessage = message => { receivedMessage = message; }; + channel4.Send(sendMessage); + yield return new WaitUntil(() => !string.IsNullOrEmpty(receivedMessage)); + Assert.That(receivedMessage, Is.EqualTo(sendMessage)); + + container1.test.component.DeleteConnection(connectionId); + container2.test.component.DeleteConnection(connectionId); + + yield return new WaitUntil(() => isStoppedChannel1 && isStoppedChannel2); + Assert.That(isStoppedChannel1, Is.True); + Assert.That(isStoppedChannel2, Is.True); + Assert.That(isStoppedChannel3, Is.False); + Assert.That(isStoppedChannel4, Is.False); + + container1.test.component.DeleteConnection(connectionId2); + container4.test.component.DeleteConnection(connectionId2); + + yield return new WaitUntil(() => isStoppedChannel3 && isStoppedChannel4); + Assert.That(isStoppedChannel3, Is.True); + Assert.That(isStoppedChannel4, Is.True); + + container1.Dispose(); + container2.Dispose(); + container4.Dispose(); + + LogAssert.ignoreFailingMessages = false; + } } class SingleConnectionTest diff --git a/Tests/Runtime/SignalingManagerInternalTest.cs b/Tests/Runtime/SignalingManagerInternalTest.cs index f3a39ed..d2c313f 100644 --- a/Tests/Runtime/SignalingManagerInternalTest.cs +++ b/Tests/Runtime/SignalingManagerInternalTest.cs @@ -1,6 +1,6 @@ using System; -using System.Linq; using System.Collections; +using System.Linq; using NUnit.Framework; using Unity.RenderStreaming.RuntimeTest.Signaling; using Unity.WebRTC; diff --git a/Tests/Runtime/SignalingManagerTest.cs b/Tests/Runtime/SignalingManagerTest.cs index 1ee313c..50e3d82 100644 --- a/Tests/Runtime/SignalingManagerTest.cs +++ b/Tests/Runtime/SignalingManagerTest.cs @@ -59,7 +59,7 @@ public void RunDefault() ISignaling mock = new MockSignaling(); component.runOnAwake = false; component.gameObject.SetActive(true); - component.Run(handlers:handlers); + component.Run(handlers: handlers); } @@ -71,8 +71,8 @@ public void ThrowExceptionIfHandlerIsNullOrEmpty() component.gameObject.SetActive(true); Assert.That(() => component.Run(signaling: mock), Throws.InvalidOperationException); - var handlers = new SignalingHandlerBase[] {}; - Assert.That(() => component.Run(signaling: mock, handlers:handlers), + var handlers = new SignalingHandlerBase[] { }; + Assert.That(() => component.Run(signaling: mock, handlers: handlers), Throws.InvalidOperationException); } @@ -85,9 +85,9 @@ public void RunAgain() ISignaling mock = new MockSignaling(); component.runOnAwake = false; component.gameObject.SetActive(true); - component.Run(signaling:mock, handlers:handlers); + component.Run(signaling: mock, handlers: handlers); component.Stop(); - component.Run(signaling:mock, handlers:handlers); + component.Run(signaling: mock, handlers: handlers); } [Test] @@ -142,7 +142,7 @@ public void EvaluateCommandlineArguments() settings = new HttpSignalingSettings("http://127.0.0.1"); Assert.That(settings.iceServers, Is.Empty); - arguments = new string[]{ "-signalingType", "websocket" }; + arguments = new string[] { "-signalingType", "websocket" }; Assert.That(SignalingManager.EvaluateCommandlineArguments(ref settings, arguments), Is.True); Assert.That(settings, Is.TypeOf()); Assert.That(settings.iceServers, Is.Not.Empty); diff --git a/Tests/Runtime/SignalingSettingsTest.cs b/Tests/Runtime/SignalingSettingsTest.cs index 4f3d5a7..ca1f59d 100644 --- a/Tests/Runtime/SignalingSettingsTest.cs +++ b/Tests/Runtime/SignalingSettingsTest.cs @@ -11,10 +11,10 @@ class IceServerTest public void Clone() { var iceServer = new IceServer( - urls: new[]{"stun:stun.l.google.com:19302"}, - username:"username", - credentialType:IceCredentialType.Password, - credential:"password"); + urls: new[] { "stun:stun.l.google.com:19302" }, + username: "username", + credentialType: IceCredentialType.Password, + credential: "password"); var copied = iceServer.Clone(); Assert.That(copied.urls, Is.EqualTo(iceServer.urls)); @@ -46,13 +46,13 @@ public void WebSocketSignalingSettings() Assert.That(() => new WebSocketSignalingSettings(url: null), Throws.Exception.TypeOf()); var url = "ws://localhost"; - settings = new WebSocketSignalingSettings(url:url); + settings = new WebSocketSignalingSettings(url: url); Assert.That(settings.url, Is.EqualTo(url)); Assert.That(settings.iceServers, Is.Empty); var iceUrl = "stun:stun.l.google.com:19302"; var iceServers = new[] { new IceServer(urls: new[] { iceUrl }) }; - settings = new WebSocketSignalingSettings(url: url, iceServers:iceServers); + settings = new WebSocketSignalingSettings(url: url, iceServers: iceServers); Assert.That(settings.iceServers.Count, Is.EqualTo(1)); Assert.That(settings.iceServers.ElementAt(0).username, Is.Null); Assert.That(settings.iceServers.ElementAt(0).credential, Is.Null); @@ -76,21 +76,5 @@ public void HttpSignalingSettings() Assert.That(settings.url, Is.EqualTo(url)); Assert.That(settings.iceServers, Is.Empty); } - - [Test] - public void FurioosSignalingSettings() - { - var settings = new FurioosSignalingSettings(); - Assert.That(settings.signalingClass, Is.EqualTo(typeof(FurioosSignaling))); - Assert.That(settings.url, Is.Not.Empty); - Assert.That(settings.iceServers, Is.Not.Empty); - - Assert.That(() => new HttpSignalingSettings(url: null), Throws.Exception.TypeOf()); - - var url = "http://localhost"; - settings = new FurioosSignalingSettings(url: url); - Assert.That(settings.url, Is.EqualTo(url)); - Assert.That(settings.iceServers, Is.Empty); - } } } diff --git a/Tests/Runtime/SignalingTest.cs b/Tests/Runtime/SignalingTest.cs index 6d26da0..1e495d5 100644 --- a/Tests/Runtime/SignalingTest.cs +++ b/Tests/Runtime/SignalingTest.cs @@ -1,7 +1,7 @@ using System; using System.Collections; -using System.Threading; using System.Diagnostics; +using System.Threading; using NUnit.Framework; using Unity.RenderStreaming.RuntimeTest.Signaling; using Unity.RenderStreaming.Signaling; @@ -15,7 +15,7 @@ namespace Unity.RenderStreaming.RuntimeTest [TestFixture(typeof(WebSocketSignaling))] [TestFixture(typeof(HttpSignaling))] [TestFixture(typeof(MockSignaling))] - [UnityPlatform(exclude = new[] {RuntimePlatform.IPhonePlayer})] + [UnityPlatform(exclude = new[] { RuntimePlatform.IPhonePlayer })] [ConditionalIgnore(ConditionalIgnore.IL2CPP, "Process.Start does not implement in IL2CPP.")] class SignalingTest : IPrebuildSetup { @@ -129,7 +129,7 @@ ISignaling CreateSignaling(Type type, SynchronizationContext mainThread) { var settings = new HttpSignalingSettings ( - url: $"http://localhost:{TestUtility.PortNumber}", + url: $"http://localhost:{TestUtility.PortNumber}", interval: 100 ); return new HttpSignaling(settings, mainThread); @@ -147,7 +147,7 @@ public IEnumerator UnitySetUp() { RTCConfiguration config = default; RTCIceCandidate candidate_ = null; - config.iceServers = new[] {new RTCIceServer {urls = new[] {"stun:stun.l.google.com:19302"}}}; + config.iceServers = new[] { new RTCIceServer { urls = new[] { "stun:stun.l.google.com:19302" } } }; var peer1 = new RTCPeerConnection(ref config); var peer2 = new RTCPeerConnection(ref config); @@ -226,7 +226,8 @@ public IEnumerator OnOffer() signaling2.Start(); yield return new WaitUntil(() => startRaised1 && startRaised2); - signaling1.OnCreateConnection += (s, connectionId, polite) => { + signaling1.OnCreateConnection += (s, connectionId, polite) => + { connectionId1 = connectionId; }; signaling1.OnDestroyConnection += (signaling, id) => @@ -235,7 +236,8 @@ public IEnumerator OnOffer() }; signaling1.OpenConnection(_connectionId); - signaling2.OnCreateConnection += (s, connectionId, polite) => { + signaling2.OnCreateConnection += (s, connectionId, polite) => + { connectionId2 = connectionId; }; signaling1.OnDestroyConnection += (signaling, id) => @@ -276,11 +278,13 @@ public IEnumerator OnAnswer() signaling2.Start(); yield return new WaitUntil(() => startRaised1 && startRaised2); - signaling1.OnCreateConnection += (s, connectionId, polite) => { + signaling1.OnCreateConnection += (s, connectionId, polite) => + { connectionId1 = connectionId; }; signaling1.OpenConnection(Guid.NewGuid().ToString()); - signaling2.OnCreateConnection += (s, connectionId, polite) => { + signaling2.OnCreateConnection += (s, connectionId, polite) => + { connectionId2 = connectionId; }; signaling2.OpenConnection(Guid.NewGuid().ToString()); @@ -314,11 +318,13 @@ public IEnumerator OnCandidate() signaling2.Start(); yield return new WaitUntil(() => startRaised1 && startRaised2); - signaling1.OnCreateConnection += (s, connectionId, polite) => { + signaling1.OnCreateConnection += (s, connectionId, polite) => + { connectionId1 = connectionId; }; signaling1.OpenConnection(Guid.NewGuid().ToString()); - signaling2.OnCreateConnection += (s, connectionId, polite) => { + signaling2.OnCreateConnection += (s, connectionId, polite) => + { connectionId2 = connectionId; }; signaling2.OpenConnection(Guid.NewGuid().ToString()); @@ -341,5 +347,48 @@ public IEnumerator OnCandidate() signaling2.SendCandidate(connectionId1, m_candidate); yield return new WaitUntil(() => candidateRaised2); } + + [UnityTest, Timeout(10000)] + public IEnumerator OnStop() + { + bool startRaised1 = false; + bool startRaised2 = false; + bool offerRaised = false; + bool answerRaised = false; + bool connectionClosed2 = false; + string connectionId1 = null; + string connectionId2 = null; + + signaling1.OnStart += s => { startRaised1 = true; }; + signaling2.OnStart += s => { startRaised2 = true; }; + signaling1.Start(); + signaling2.Start(); + yield return new WaitUntil(() => startRaised1 && startRaised2); + + signaling1.OnCreateConnection += (s, connectionId, polite) => + { + connectionId1 = connectionId; + }; + signaling1.OpenConnection(Guid.NewGuid().ToString()); + signaling2.OnCreateConnection += (s, connectionId, polite) => + { + connectionId2 = connectionId; + }; + signaling2.OpenConnection(Guid.NewGuid().ToString()); + yield return new WaitUntil(() => + !string.IsNullOrEmpty(connectionId1) && !string.IsNullOrEmpty(connectionId2)); + + signaling2.OnOffer += (s, e) => { offerRaised = true; }; + signaling1.SendOffer(connectionId1, m_DescOffer); + yield return new WaitUntil(() => offerRaised); + + signaling1.OnAnswer += (s, e) => { answerRaised = true; }; + signaling2.SendAnswer(connectionId1, m_DescAnswer); + yield return new WaitUntil(() => answerRaised); + + signaling2.OnDestroyConnection += (s, e) => { connectionClosed2 = true; }; + signaling1.Stop(); + yield return new WaitUntil(() => connectionClosed2); + } } } diff --git a/Tests/Runtime/StreamingComponentTest.cs b/Tests/Runtime/StreamingComponentTest.cs index a3ea98a..819adc6 100644 --- a/Tests/Runtime/StreamingComponentTest.cs +++ b/Tests/Runtime/StreamingComponentTest.cs @@ -1,14 +1,14 @@ using System; -using System.Linq; using System.Collections; using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using Unity.Collections; -using UnityEngine; using Unity.WebRTC; -using UnityEngine.TestTools; +using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Users; +using UnityEngine.TestTools; namespace Unity.RenderStreaming.RuntimeTest { @@ -23,7 +23,7 @@ public void GetAvailableCodec() Assert.That(codecs.Any(codec => codec.name == "VP9")); Assert.That(codecs.Any(codec => codec.name == "AV1")); - foreach(var codec in codecs) + foreach (var codec in codecs) { Assert.That(codec.name, Is.Not.Empty); Assert.That(codec.mimeType, Is.Not.Empty); @@ -298,7 +298,7 @@ public void GetAvailableCodec() { IEnumerable codecs = AudioStreamSender.GetAvailableCodecs(); Assert.That(codecs, Is.Not.Empty); - foreach(var codec in codecs) + foreach (var codec in codecs) { Assert.That(codec.name, Is.Not.Empty); Assert.That(codec.mimeType, Is.Not.Empty); @@ -389,6 +389,29 @@ public IEnumerator ReplaceTrack() UnityEngine.Object.DestroyImmediate(go); } + [UnityTest] + public IEnumerator AudioLoopback() + { + var go = new GameObject(); + var sender = go.AddComponent(); + + sender.source = AudioStreamSource.AudioListener; + var audioListener = go.AddComponent(); + sender.audioListener = audioListener; + var op = sender.CreateTrack(); + yield return op; + var track = op.Track as AudioStreamTrack; + Assert.That(track, Is.Not.Null); + sender.SetTrack(track); + + sender.loopback = true; + Assert.That(track.Loopback, Is.True); + sender.loopback = false; + Assert.That(track.Loopback, Is.False); + + UnityEngine.Object.DestroyImmediate(go); + } + [Test] public void SelectCodecCapabilities() { @@ -397,7 +420,6 @@ public void SelectCodecCapabilities() Assert.That(codecs.Count(), Is.EqualTo(caps.Count())); } - [Test] public void SetEnabled() { @@ -464,10 +486,10 @@ public void SetData() var sender = go.AddComponent(); NativeArray nativeArray = new NativeArray(256, Allocator.Temp); - Assert.That(() => sender.SetData(ref nativeArray, 2), Throws.Exception.TypeOf()); + Assert.That(() => sender.SetData(nativeArray.AsReadOnly(), 2), Throws.Exception.TypeOf()); sender.source = AudioStreamSource.APIOnly; - Assert.That(() => sender.SetData(ref nativeArray, 2), Throws.Nothing); + Assert.That(() => sender.SetData(nativeArray.AsReadOnly(), 2), Throws.Nothing); nativeArray.Dispose(); } @@ -569,7 +591,7 @@ public void SetChannel() Assert.That(receiver.currentActionMap, Is.Null); receiver.currentActionMap = new InputActionMap(); Assert.That(receiver.actionEvents, Is.Not.Null); - receiver.actionEvents = new PlayerInput.ActionEvent[]{}; + receiver.actionEvents = new PlayerInput.ActionEvent[] { }; receiver.SwitchCurrentActionMap(mapName); diff --git a/package.json b/package.json index 92e9ddd..5ef9eea 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "com.unity.renderstreaming", "displayName": "Unity Render Streaming", - "version": "3.1.0-exp.6", + "version": "3.1.0-exp.7", "unity": "2020.3", "description": "This is a package for using Unity Render Streaming technology.", "dependencies": { - "com.unity.webrtc": "3.0.0-pre.4", - "com.unity.inputsystem": "1.4.4", + "com.unity.webrtc": "3.0.0-pre.6", + "com.unity.inputsystem": "1.5.1", "com.unity.ugui": "1.0.0", "com.unity.modules.screencapture": "1.0.0" }, @@ -18,15 +18,15 @@ } ], "_upm": { - "changelog": "### Added\n\n- Support Automatic Streaming.\n- Support Render Streaming Wizard.\n- Support Command line arguments.\n- Add Render Streaming Settings in the Project Settings window.\n\n### Changed\n\n- Rename classes.\n - `RenderStreaming` > `SignalingManager`\n- Websocket is in default for signaling protocol instead of HTTP polling.\n- Changed a unit of the HTTP polling interval, second to millisecond." + "changelog": "### Added\n\n- Added configurable logger to enable users to customize logging for their environment.\n\n### Changed\n\n- Upgrade the version of WebRTC package `3.0.0-pre.6`.\n- Add `AudioStreamSender.loopback` property.\n\n### Fixed\n\n- Fixed error on HTTP signaling when using short polling interval.\n- Fixed `SignalingManager` so that ICE server configurations aren't effected.\n- Added a workaround to fix an issue where `InputField` wasn't worked when entering characters from browsers or a `Receiver` scene of package sample.\n\n### Removed\n\n- Removed Furioos Integration." }, "upmCi": { - "footprint": "0387ba8d1f3f8f42273e981a7acaa116558fbe4d" + "footprint": "8b2f5eeadcf42fdb20bc7093d52df42cecb60834" }, "documentationUrl": "https://docs.unity3d.com/Packages/com.unity.renderstreaming@3.1/manual/index.html", "repository": { "url": "https://github.com/Unity-Technologies/UnityRenderStreaming.git", "type": "git", - "revision": "cdf3a002530722d8a464d1b5025bd92e9fb1abb6" + "revision": "d6e6249765871f7ccb1acc1c55ba9e6acaf8d65f" } }